微服务Spring Boot 整合Redis 阻塞队列实现异步秒杀下单思路详解
Bug 终结者 人气:0⛅引言
本章节,介绍使用阻塞队列实现秒杀的优化,采用异步秒杀完成下单的优化!
一、秒杀优化 - 异步秒杀思路
当用户发起请求,此时会请求nginx,nginx会访问到tomcat,而tomcat中的程序,会进行串行操作,分成如下几个步骤
- 查询优惠卷
- 判断秒杀库存是否足够
- 查询订单
- 校验是否是一人一单
- 扣减库存
- 创建订单,完成
在以上6个步骤中,我们可以采用怎样的方式来优化呢?
整体思路:当用户下单之后,判断库存是否充足只需要导redis中去根据key找对应的value是否大于0即可,如果不充足,则直接结束,如果充足,继续在redis中判断用户是否可以下单,如果set集合中没有这条数据,说明他可以下单,如果set集合中没有这条记录,则将userId和优惠卷存入到redis中,并且返回0,整个过程需要保证是原子性的,我们可以使用Lua来操作
当以上逻辑走完后,我们可以根据返回的结果来判断是否是0,如果是0,则可以下单,可以存入 queue 队列中,然后返回,前端可以通过返回的订单id来判断是否下单成功。
二、秒杀优化 - 基于Redis完成秒杀资格判断
需求:
- 新增秒杀优惠卷的同时,需要将优惠卷信息保存在redis中
- 基于Lua脚本实现,判断秒杀库存、一人一单,决定用户是否抢购成功
- 如果抢购成功,将优惠卷id和用户id封装后存入阻塞队列
- 开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能
新增优惠卷时,将优惠卷信息存入Redis
VoucherService
@Override @Transactional public void addSeckillVoucher(Voucher voucher) { // 保存优惠券 save(voucher); // 保存秒杀信息 SeckillVoucher seckillVoucher = new SeckillVoucher(); seckillVoucher.setVoucherId(voucher.getId()); seckillVoucher.setStock(voucher.getStock()); seckillVoucher.setBeginTime(voucher.getBeginTime()); seckillVoucher.setEndTime(voucher.getEndTime()); seckillVoucherService.save(seckillVoucher); // 保存秒杀库至redis seckill:stock stringRedisTemplate.opsForValue().set(RedisConstants.SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString()); }
新增优惠卷时,可存入redis信息
编写 Lua 脚本,实现秒杀资格判断
seckill Lua 秒杀脚本
-- 1.参数列表 -- 1.1 优惠卷id local voucherId = ARGV[1] -- 1.2 用户id local userId = ARGV[2] -- 2. 数据key -- 2.1 库存key 拼接 .. local stockKey = 'seckill:stock:' .. voucherId -- 2.2 订单key 拼接 .. local orderKey = "seckill:order" .. voucherId -- 3. 脚本业务 -- 3.1 判断库存是否充足 if (tonumber(redis.call('get', stockKey)) <= 0) then -- 3.2 库存不足 return 1 end -- 3.2 判断用户是否下单 SISMEMBER orderKey userId if (redis.call('sismember', orderKey, userId) == 1) then -- 3.3 存在,证明是重复下单 return 2 end -- 3.4 扣库存 incrby stockKey -1 redis.call('incrby', stockKey, -1) -- 3.5 下单 保存用户 sadd orderKey userId redis.call('sadd', orderKey, userId) return 0
三、基于阻塞队列完成异步秒杀下单
基于阻塞队列实现异步秒杀下单
核心思路:将请求存入阻塞队列中 进行缓存,开启线程池读取任务并依次处理。
VoucherOrderService
private static final DefaultRedisScript<Long> SECKILL_SCRIPT; static { SECKILL_SCRIPT = new DefaultRedisScript<>(); SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua")); SECKILL_SCRIPT.setResultType(Long.class); } private BlockingQueue<VoucherOrder> orderTasks =new ArrayBlockingQueue<>(1024 * 1024); private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor(); //项目启动后执行该方法 @PostConstruct private void init() { SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler()); } // 用于线程池处理的任务 // 当初始化完毕后 就会去从对列中去拿信息 private class VoucherOrderHandler implements Runnable { @Override public void run() { while (true){ try { // 1.获取队列中的订单信息 VoucherOrder voucherOrder = orderTasks.take(); // 2.创建订单 handleVoucherOrder(voucherOrder); } catch (Exception e) { log.error("处理订单异常", e); } } } } private void handleVoucherOrder(VoucherOrder voucherOrder) { //1.获取用户 Long userId = voucherOrder.getUserId(); // 2.创建锁对象 RLock lock = redissonClient.getLock("lock:order:" + userId); // 3.尝试获取锁 boolean isLock = lock.tryLock(); // 4.判断是否获得锁成功 if (!isLock) { // 获取锁失败,直接返回失败或者重试 log.error("不允许重复下单!"); return; } try { //注意:由于是spring的事务是放在threadLocal中,此时的是多线程,事务会失效 proxy.createVoucherOrder(voucherOrder); } finally { // 释放锁 lock.unlock(); } } // 代理对象 private IVoucherOrderService proxy; @Override public Result seckillVoucher(Long voucherId) { // 获取用户 Long userId = UserHolder.getUser().getId(); // 获取订单id 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 ? "库存不足" : "不允许重复下单"); } // 2.2 为0,有购买资格 把下单信息保存到阻塞队列 // 2.2 有购买的资格,创建订单放入阻塞队列中 VoucherOrder voucherOrder = new VoucherOrder(); // 2.3.订单id voucherOrder.setId(orderId); // 2.4.用户id voucherOrder.setUserId(userId); // 2.5.代金券id voucherOrder.setVoucherId(voucherId); // 2.6.放入阻塞队列 orderTasks.add(voucherOrder); //3.获取代理对象 proxy = (IVoucherOrderService)AopContext.currentProxy(); // 2.3 返回订单id return Result.ok(orderId); } @Transactional public void createVoucherOrder (VoucherOrder voucherOrder){ // 5.一人一单逻辑 // 5.1.用户id Long userId = voucherOrder.getUserId(); // 判断是否存在 int count = query().eq("user_id", userId) .eq("voucher_id", voucherOrder.getId()).count(); // 5.2.判断是否存在 if (count > 0) { // 用户已经购买过了 log.error("用户已经购买过了"); } //6,扣减库存 boolean success = seckillVoucherService.update() .setSql("stock= stock -1") //set stock = stock -1 .eq("voucher_id", voucherOrder.getVoucherId()).gt("stock",0).update(); //where id = ? and stock > 0 // .eq("voucher_id", voucherId).eq("stock",voucher.getStock()).update(); //where id = ? and stock = ? if (!success) { //扣减库存 log.error("库存不足!"); } save(voucherOrder); }
四、测试程序
ApiFox 测试程序
测试成功,查看Redis
成功添加订单信息
库存信息
数据库信息
Jmeter 进行压力测试
恢复数据,进行压力测试
关于测试:新增了1000条用户信息,存入数据库和Redis,token,Jmeter使用Tokens文件测试1000条并发
相关资料见下文
进行压测
经过检测,性能提升了几十倍!
数据库
五、源码地址
Jmeter测试文件:https://www.bilibili.com/video/av251263036/
以上就是【Bug 终结者】对 微服务Spring Boot 整合Redis 阻塞队列实现异步秒杀下单 的简单介绍,在分布式系统下,高并发的场景下,使用阻塞队列来优化秒杀下单,但依旧不是最优解,持续更新中!下章节 采用消息队列优化秒杀下单!
加载全部内容