day40 Redis秒杀优化与阻塞队列异步下单

1. 今天做了什么

  • Redis秒杀优化:使用Redis + Lua脚本 + 阻塞队列实现异步秒杀下单

2. 核心知识点

原来的业务逻辑存在以下问题影响性能:

  1. 2次数据库查询 + 2次修改数据库,数据库操作比较耗时间
  2. 一次请求,所有操作串行执行,等到全部执行完才能返回订单id

优化思路:

  1. 使用Redis提前缓存优惠券库存(新增秒杀券时),并使用set维护优惠券已下单用户
  2. 将判断库存与一人一单检查全部使用lua脚本执行
  3. 判断库存与一人一单检查通过,直接返回订单id
  4. 将优惠券id、用户id、订单添加到阻塞队列
  5. 开启异步线程,基于阻塞队列实现异步下单

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
28
-- 优惠券id
local voucherId = ARGV[1]
-- 用户id
local userId = ARGV[2]

-- 库存key
local stockKey = 'seckill:stock:' .. voucherId
-- 订单key
local orderKey = 'seckill:order:' .. voucherId

-- 查库存,如果库存不足,则返回1
if(tonumber( redis.call("get",stockKey) )<1) then
return 1
end

-- 一人一单,如果用户已下单,则返回2
if(redis.call("SISMEMBER",orderKey,userId) == 1) then
return 2
end

-- 减库存
redis.call("DECR",stockKey)

-- 下单
redis.call("SADD",orderKey,userId)

-- 返回0
return 0
  • 由于lua脚本中已经完成了对一人一单的判断,过程是原子操作,且Redis是单线程的,不存在线程安全问题,所以后续库存扣减与订单保存不需要判断一人一单,也就不需要加锁

阻塞队列:获取时如果队列为空,则阻塞等待,直到有元素加入队列,则返回该元素

1
2
//阻塞队列,获取时如果队列为空,则阻塞等待,直到有元素加入队列,则返回该元素
BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024*1024);

3. 深度思考

基于阻塞队列实现异步下单会有的问题:

  • 内存限制:阻塞队列存储在内存中,内存空间有限
  • 数据安全问题:服务突然宕机,内存数据会丢失

4. 踩过的坑

  • 在编码过程中,在异步线程中使用 Long userId = UserHolder.getUser().getId(); 造成了异常,因为ThreadLocal是基于线程的,异步线程获取不到主线程存储的用户信息

5. 遗留问题 / 想不通

  • 以上优化未对秒杀时间做判断,虽然前端会限制秒杀时间,但是也应该在后端做处理,可以将字符串存库存改为哈希存库存与开始结束时间,在lua脚本中对时间做判断