异步秒杀
秒杀是一个非常经典的场景,秒杀的核心是高并发下的库存扣减。
正常操作是将进行如查询库存、一人一单、扣减库存等操作。
但是在高并发的情况下,这些操作可能有有性能低下的问题。
这时需要使用异步秒杀的方式来解决这个问题。
优化
异步秒杀的核心是将秒杀的操作进行异步化处理。
使用 Redis 来完成查询库存以及一人一单的操作。
这个操作完成后将所需信息写入阻塞队列中,然后返回必要信息给用户,如订单号等,这时业务逻辑已经完成。
之后的写入数据库操作的时效性就不那么重要了,可以使用异步的方式来完成,降低数据库的压力。

代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
| 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 { VoucherOrder voucherOrder = orderTasks.take(); handleVoucherOrder(voucherOrder); } catch (Exception e) { log.error("处理订单异常", e); } } } }
private void handleVoucherOrder(VoucherOrder voucherOrder) { Long userId = voucherOrder.getUserId(); RLock lock = redissonClient.getLock("lock:order:" + userId); boolean isLock = lock.tryLock(); if (!isLock) { log.error("不允许重复下单"); return; } try { proxy.createVoucherOrder(voucherOrder); } finally { lock.unlock(); } }
private IVoucherOrderService proxy;
@Override public Result seckillVoucher(Long voucherId) { Long userId = UserHolder.getUser().getId();
Long result = stringRedisTemplate.execute( SECKILL_SCRIPT, Collections.emptyList(), voucherId.toString(), userId.toString() ); int r = result.intValue(); if (r != 0) { return Result.fail(r == 1 ? "库存不足" : "不能重复下单"); } VoucherOrder voucherOrder = new VoucherOrder(); long orderId = redisIdWorker.nextId("order"); voucherOrder.setId(orderId); voucherOrder.setUserId(userId); voucherOrder.setVoucherId(voucherId); orderTasks.add(voucherOrder);
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return Result.ok(orderId); }
@Transactional public void createVoucherOrder(VoucherOrder voucherOrder) { Long userId = voucherOrder.getUserId();
int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();
if (count > 0) { log.error("用户已经购买过"); return; }
boolean success = seckillVoucherService.update() .setSql("stock = stock - 1") .eq("voucher_id", voucherOrder.getVoucherId()) .gt("stock", 0) .update(); if (!success) { log.error("扣减库存失败"); return; }
save(voucherOrder); }
|
其中lua脚本的内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
local voucherId = ARGV[1]
local userId = ARGV[2]
local stockKey = 'seckill:stock:' .. voucherId
local orderKey = 'seckill:order:' .. voucherId
if (tonumber(redis.call('get', stockKey)) <= 0) then return 1 end
if (redis.call('sismember', orderKey, userId) == 1) then return 2 end
redis.call('incrby', stockKey, -1)
redis.call('sadd', orderKey, userId)
|
总结
秒杀业务的优化思路:
- 先利用 Redis 完成库存余量、一人一单判断,完成抢单业务
- 再将下单业务放入阻塞队列,利用独立线程异步下单
基于阻塞队列的异步秒杀的问题:
- 内存限制问题:
阻塞队列是基于内存的,内存有限,阻塞队列的长度有限制
- 数据安全问题:
如果阻塞队列满了,后续的请求会被拒绝
如果在执行阻塞队列的过程中,发生了异常,可能会导致数据丢失