通过异步使用消息队列优化秒杀

打印 上一主题 下一主题

主题 847|帖子 847|积分 2541


同步秒杀流程

  1.     public Result seckillVoucher(Long voucherId) throws InterruptedException {
  2.         SeckillVoucher seckillVoucher = iSeckillVoucherService.getById(voucherId);
  3.         LocalDateTime beginTime = seckillVoucher.getBeginTime();
  4.         if (LocalDateTime.now().isBefore(beginTime)) {
  5.             return Result.fail("秒杀还未开始");
  6.         }
  7.         LocalDateTime endTime = seckillVoucher.getEndTime();
  8.         if (LocalDateTime.now().isAfter(endTime)) {
  9.             return Result.fail("秒杀已经结束");
  10.         }
  11. //        查看库存
  12.         Integer stock = seckillVoucher.getStock();
  13.         if (stock < 1) {
  14.             return Result.fail("库存不足");
  15.         }
  16.         Long userId = UserHolder.getUser().getId();
  17. //        redis集群实现
  18.         RLock lock = redissonClient.getLock("order:" + userId);
  19.         boolean isLock = lock.tryLock(1L, TimeUnit.SECONDS);
  20.         if (!isLock) {
  21.             return Result.fail("你已经购买优惠卷");
  22.         }
  23.         try {
  24.             IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
  25.             return proxy.createVoucherOrder(voucherId, userId);
  26.         } finally {
  27. //            redisLock.releaseLock();
  28.             lock.unlock();
  29.         }
  30.     }
  31.   @Transactional
  32.     public Result createVoucherOrder(Long voucherId, Long userId) {
  33.         Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
  34.         if (count > 0) {
  35.             return Result.fail("已经购买优惠卷");
  36.         }
  37. //        基于乐观锁避免超卖问题,在乐观锁的基础上做出了改进,当要修改库存时判断库存是否大于0,但是也给数据库带来了巨大的压力
  38.         boolean success = iSeckillVoucherService.update().setSql("stock=stock-1")
  39.                 .eq("voucher_id", voucherId).
  40.                 gt("stock", 0).update();
  41.         if (!success) {
  42.             return Result.fail("库存不足");
  43.         }
  44.         VoucherOrder voucherOrder = new VoucherOrder();
  45.         long orderId = redisWorker.getGlobalId("order");
  46.         voucherOrder.setId(orderId);
  47.         voucherOrder.setVoucherId(voucherId);
  48.         voucherOrder.setUserId(userId);
  49.         save(voucherOrder);
  50.         return Result.ok(orderId);
  51.     }
复制代码
以上是我们同步秒杀的流程,起首会从数据库中查询优惠卷,检查库存和是否在秒杀阶段,然后使用redisson基于Redis实现分布式锁解决一人一单题目,如果都通过后就会修改库存并且生成订单,整体的流程如下:

在这个流程中,我们发现整个步骤都是同步,都是交给主线程来执行,负责检验库存包管一人一单然厥后修改数据库数据,我们是否可以再请一个人来优化这个流程呢?比如A来负责检验,检验乐成后交给B来完成数据库的修改,这样是不是可以或许优化秒杀的业务
异步优化秒杀

我们可以这样做,让主线程来负责判定这个人能不能抢杀,如果抢杀乐成交给另一个人来负责修改数据库,主线程直接返回订单id。
那怎样让主线程快速的库存和一人一单的判定呢?Redis
我们可以将库存生存在Redis的字符串类型中,Redis的性能高于直接查询数据库的性能,而一人一单的校验我们可以使用Redis的set聚集,校验是否一人一单就看Redis的set聚会合是否含有效户id,而当主线程通过校验后让Redis中的库存-1并且将用户id添加到set聚会合。
而我们怎样让修改数据库的任务交给另一个人来处理呢?使用壅闭队列或者消息队列,为了实现简朴,暂且使用壅闭队列,我们将要修改数据库的任务放入壅闭队列中,创建一个线程池,让线程池来负责执行修改数据库的任务。
异步秒杀流程


从流程中看出我们主线程只负责举行库存校验和确保一人一单后,生成订单添加到壅闭队列或消息队列中就直接返回,大大提高了业务的性能。
基于lua脚本包管Redis操作原子性

如果我们使用以上的操作来实今世码,会发生并发安全题目,由于校验Redis操作和更新Redis操作不具有原子性,就可能发生题目,例如一个线程通过校验Redis库存等还没有举行修改,另一个线程直接进入比较没有修改的库存,就发生了线程安全题目,我们需要包管这些操作具有原子性
代码实现

lua脚本:
  1. --优惠卷id
  2. local voucherId = ARGV[1];
  3. --用户id
  4. local userId = ARGV[2];
  5. --库存键
  6. local stockKey = "voucher:stock:" + voucherId;
  7. --下过订单的用户键
  8. local orderKey = "voucher:order:" + voucherId;
  9. --查看卷库存是否足够
  10. if (tonumber(redis.call("get", stockKey)) <= 0) then
  11. return 1;
  12. end
  13. if (redis.call("sismember",orderKey,userId)==1) then
  14. return 2
  15. end
  16. --扣库存
  17. redis.call("incrby",stockKey,-1);
  18. --添加用户id
  19. redis.call("sadd",orderKey,userId)
  20. return 0
复制代码
  1.     /**
  2.      * 异步秒杀
  3.      */
  4.     @Override
  5.     public Result seckillVoucher(Long voucherId) throws InterruptedException {
  6.         Long userId = UserHolder.getUser().getId();
  7.         Long result = redisTemplate.execute(SECkILL_SCRIPT, Collections.emptyList(), voucherId.toString(), userId.toString());
  8.         int re = result.intValue();
  9.         if (re != 0) {
  10.             return re == 1 ? Result.fail("库存不足") : Result.fail("不能重复下单");
  11.         }
  12.         long orderId = redisWorker.getGlobalId("voucher:order");
  13.         VoucherOrder voucherOrder = new VoucherOrder();
  14.         voucherOrder.setVoucherId(voucherId);
  15.         voucherOrder.setId(orderId);
  16.         voucherOrder.setUserId(userId);
  17.         VOUCHER_PROXY = (IVoucherOrderService) AopContext.currentProxy();
  18.         BLOCKING_QUEUE.add(voucherOrder);
  19.         return Result.ok(orderId);
  20.     }
复制代码
  1. // 初始化lua脚本环境
  2.     private static final DefaultRedisScript<Long> SECkILL_SCRIPT;
  3.     static {
  4.         SECkILL_SCRIPT = new DefaultRedisScript<>();
  5.         SECkILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
  6.         SECkILL_SCRIPT.setResultType(Long.class);
  7.     }
  8.     //    在类初始化完之后执行
  9.     @PostConstruct
  10.     private void init() {
  11.         EXECUTOR_SERVICE.execute(new VoucherHandler());
  12.     }
  13. // 阻塞队列获取任务处理
  14.     private class VoucherHandler implements Runnable {
  15.         @Override
  16.         public void run() {
  17.             while (true){
  18.                 try {
  19. //                从阻塞队列中获取订单信息
  20.                     VoucherOrder voucherOrder = BLOCKING_QUEUE.take();
  21. //                  更新数据库库存以及创建优惠卷订单
  22.                     VOUCHER_PROXY.createVoucherOrder(voucherOrder.getVoucherId(), voucherOrder.getUserId());
  23.                 } catch (InterruptedException e) {
  24.                     e.printStackTrace();
  25.                 }
  26.             }
  27.         }
  28.     }
  29.     @Transactional
  30.     public Result createVoucherOrder(Long voucherId, Long userId) {
  31.         Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
  32.         if (count > 0) {
  33.             return Result.fail("已经购买优惠卷");
  34.         }
  35. //        基于乐观锁避免超卖问题,在乐观锁的基础上做出了改进,当要修改库存时判断库存是否大于0,但是也给数据库带来了巨大的压力
  36.         boolean success = iSeckillVoucherService.update().setSql("stock=stock-1")
  37.                 .eq("voucher_id", voucherId).
  38.                 gt("stock", 0).update();
  39.         if (!success) {
  40.             return Result.fail("库存不足");
  41.         }
  42.         VoucherOrder voucherOrder = new VoucherOrder();
  43.         long orderId = redisWorker.getGlobalId("order");
  44.         voucherOrder.setId(orderId);
  45.         voucherOrder.setVoucherId(voucherId);
  46.         voucherOrder.setUserId(userId);
  47.         save(voucherOrder);
  48.         return Result.ok(orderId);
  49.     }
复制代码
壅闭队列的缺点

在我们举行数据库的修改任务时我们使用了壅闭队列来实现,在实际的业务中,我们需要使用一些消息队列来代替壅闭队列,壅闭队列使用的是JVM中的内存,当消息过多时会造成JVM内存爆满,并且功能不敷强大,我们可以使用Redis的Stream或者MQ来实现消息队列来代替壅闭队列

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

傲渊山岳

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表