session 共享问题:多台 Tomcat 并不共享 session 存储空间,当请求切换到不同 tomcat 服务时导致数据丢失的问题。
登陆验证流程:
拦截器优化:
缓存穿透是指客户端的数据在缓存中和数据中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
常见的解决方案有两种:
解决方案:
缓存击穿问题也叫热点 Key 问题,就是一个被高并发访问并且缓存冲击就按业务比较复杂的 Key 突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
常见的解决方案有两种:
获取的锁是分布式锁
优化前:查询MySQL速度慢,而且下一步操作需要等待上一步执行完成
优化后:
(1)新增秒杀优惠券的同时,将优惠券信息保存到Redis中
(2)基于Lua脚本,判断秒杀库存、一人一单,决定用户是否抢购成功
(3)如果抢购成功,将优惠券id和用户id封装后发送到消息队列
(4)开启线程任务,不断从消息队列中获取信息,实现异步下单功能
- -- 1.参数列表
- -- 1.1.优惠券id
- local voucherId = ARGV[1]
- -- 1.2.用户id
- local userId = ARGV[2]
- -- 1.3.订单id
- local orderId = ARGV[3]
-
- -- 2.数据key
- -- 2.1.库存key
- local stockKey = 'seckill:stock:' .. voucherId
- -- 2.2.订单key
- local orderKey = 'seckill:order:' .. voucherId
-
- -- 3.脚本业务
- -- 3.1.判断库存是否充足 get stockKey
- if(tonumber(redis.call('get', stockKey)) <= 0) then
- -- 3.2.库存不足,返回1
- return 1
- end
- -- 3.2.判断用户是否下单 SISMEMBER orderKey userId
- if(redis.call('sismember', orderKey, userId) == 1) then
- -- 3.3.存在,说明是重复下单,返回2
- return 2
- end
- -- 3.4.扣库存 incrby stockKey -1
- redis.call('incrby', stockKey, -1)
- -- 3.5.下单(保存用户)sadd orderKey userId
- redis.call('sadd', orderKey, userId)
- -- 3.6.发送消息到队列中, XADD stream.orders * k1 v1 k2 v2 ...
- redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
- return 0
- public Result seckillVoucher(Long voucherId) {
- Long userId = UserHolder.getUser().getId();
- long orderId = redisIdWorker.nextId("order");
- // 1.执行lua脚本
- Long result = stringRedisTemplate.execute(
- SECKILL_SCRIPT,
- Collections.emptyList(),
- voucherId.toString(), userId.toString(), String.valueOf(orderId)
- );
- int r = result.intValue();
- // 2.判断结果是否为0
- if (r != 0) {
- // 2.1.不为0 ,代表没有购买资格
- return Result.fail(r == 1 ? "库存不足" : "不能重复下单");
- }
- // 3.返回订单id
- return Result.ok(orderId);
- }
- private class VoucherOrderHandler implements Runnable {
-
- @Override
- public void run() {
- while (true) {
- try {
- // 1.获取消息队列中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 >
- List
> list = stringRedisTemplate.opsForStream().read( - Consumer.from("g1", "c1"),
- StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
- StreamOffset.create("stream.orders", ReadOffset.lastConsumed())
- );
- // 2.判断订单信息是否为空
- if (list == null || list.isEmpty()) {
- // 如果为null,说明没有消息,继续下一次循环
- continue;
- }
- // 解析数据
- MapRecord
record = list.get(0); - Map
- VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
- // 3.创建订单
- createVoucherOrder(voucherOrder);
- // 4.确认消息 XACK
- stringRedisTemplate.opsForStream().acknowledge("s1", "g1", record.getId());
- } catch (Exception e) {
- log.error("处理订单异常", e);
- handlePendingList();
- }
- }
- }
-
- private void handlePendingList() {
- while (true) {
- try {
- // 1.获取pending-list中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 0
- List
> list = stringRedisTemplate.opsForStream().read( - Consumer.from("g1", "c1"),
- StreamReadOptions.empty().count(1),
- StreamOffset.create("stream.orders", ReadOffset.from("0"))
- );
- // 2.判断订单信息是否为空
- if (list == null || list.isEmpty()) {
- // 如果为null,说明没有异常消息,结束循环
- break;
- }
- // 解析数据
- MapRecord
record = list.get(0); - Map
- VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
- // 3.创建订单
- createVoucherOrder(voucherOrder);
- // 4.确认消息 XACK
- stringRedisTemplate.opsForStream().acknowledge("s1", "g1", record.getId());
- } catch (Exception e) {
- log.error("处理订单异常", e);
- }
- }
- }
- }
- private void createVoucherOrder(VoucherOrder voucherOrder) {
- Long userId = voucherOrder.getUserId();
- Long voucherId = voucherOrder.getVoucherId();
- // 创建锁对象
- RLock redisLock = redissonClient.getLock("lock:order:" + userId);
- // 尝试获取锁
- boolean isLock = redisLock.tryLock();
- // 判断
- if (!isLock) {
- // 获取锁失败,直接返回失败或者重试
- log.error("不允许重复下单!");
- return;
- }
-
- try {
- // 5.1.查询订单
- int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
- // 5.2.判断是否存在
- if (count > 0) {
- // 用户已经购买过了
- log.error("不允许重复下单!");
- return;
- }
-
- // 6.扣减库存
- boolean success = seckillVoucherService.update()
- .setSql("stock = stock - 1") // set stock = stock - 1
- .eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0
- .update();
- if (!success) {
- // 扣减失败
- log.error("库存不足!");
- return;
- }
-
- // 7.创建订单
- save(voucherOrder);
- } finally {
- // 释放锁
- redisLock.unlock();
- }
- }
场景:线程1执行完业务,需要释放分布式锁时,先判断锁的标识为自己,但是释放锁的操作阻塞,此时锁被超时释放,被另外线程2获取之后,线程1已经认为是自己的锁,然后执行释放操作。因此使用Redis提供的Lua脚本,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性