优惠券秒杀模块方案设计
本文章针对优惠券秒杀场景所进行的方案设计,考虑不周的地方,烦请指正。
在我们兑换/秒杀优惠券模板的接口中,可能会存在以下三个难点:
- 高并发流量压力:秒杀活动往往会瞬间吸引大量用户访问系统,导致流量骤增,如果直接访问数据库,可能会让数据库负载过重,甚至导致宕机。
- 库存超卖问题:由于并发请求,多个用户同时抢购可能会导致系统超卖,即多个用户同时购买到同一库存。
- 用户超领问题:优惠券中会有一个限制,每个用户限流几张,应该如何避免用户领取超过这个限制。
针对上面的问题,整个方案的流程如下:
1. 用户请求
- 用户(Actor)通过前端接口请求领取优惠券。
- 请求触发优惠券相关接口的调用。
2. 优惠券模板检查
- 系统通过缓存检查当前优惠券模板是否存在以及是否有效:
- 如果缓存中有该优惠券模板并且有效,则继续下一步。
- 如果缓存中不存在或无效,立即拒绝请求,返回失败响应。
3. Lua 脚本缓存控制
- 使用 Lua 脚本来处理高并发下的请求验证和缓存更新,降低数据库压力:
- 判断 Redis 中是否有库存:检查 Redis 中的优惠券库存是否充足。
- 判断用户领取次数:检查用户是否已达到优惠券的领取限制。
- 新增用户领取次数:如果用户未超限,Lua 脚本更新用户的领取次数。
- 扣减库存:库存足够时,直接扣减 Redis 中的库存数。
-- Lua 脚本: 检查用户是否达到优惠券领取上限并记录领取次数
-- 参数列表:
-- KEYS[1]: 优惠券库存键 (coupon_stock_key)
-- KEYS[2]: 用户领取记录键 (user_coupon_key)
-- ARGV[1]: 优惠券有效期结束时间 (timestamp)
-- ARGV[2]: 用户领取上限 (limit)
local function combineFields(firstField, secondField)
-- 确定 SECOND_FIELD_BITS 为 14,因为 secondField 最大为 9999
local SECOND_FIELD_BITS = 14
-- 根据 firstField 的实际值,计算其对应的二进制表示
-- 由于 firstField 的范围是0-2,我们可以直接使用它的值
local firstFieldValue = firstField
-- 模拟位移操作,将 firstField 的值左移 SECOND_FIELD_BITS 位
local shiftedFirstField = firstFieldValue * (2 ^ SECOND_FIELD_BITS)
-- 将 secondField 的值与位移后的 firstField 值相加
return shiftedFirstField + secondField
end
-- 获取当前库存
local stock = tonumber(redis.call('HGET', KEYS[1], 'stock'))
-- 判断库存是否大于 0
if stock <= 0 then
return combineFields(1, 0) -- 库存不足
end
-- 获取用户领取的优惠券次数
local userCouponCount = tonumber(redis.call('GET', KEYS[2]))
-- 如果用户领取次数不存在,则初始化为 0
if userCouponCount == nil then
userCouponCount = 0
end
-- 判断用户是否已经达到领取上限
if userCouponCount >= tonumber(ARGV[2]) then
return combineFields(2, userCouponCount) -- 用户已经达到领取上限
end
-- 增加用户领取的优惠券次数
if userCouponCount == 0 then
-- 如果用户第一次领取,则需要添加过期时间
redis.call('SET', KEYS[2], 1)
redis.call('EXPIRE', KEYS[2], ARGV[1])
else
-- 因为第一次领取已经设置了过期时间,第二次领取沿用之前即可
redis.call('INCR', KEYS[2])
end
-- 减少优惠券库存
redis.call('HINCRBY', KEYS[1], 'stock', -1)
return combineFields(0, userCouponCount)
4. 失败处理
- 如果用户请求未通过验证(如库存不足、领取次数超限等),直接返回失败信息。
5. 编程式事务
- 库存扣减成功后,执行编程式事务进行后续业务逻辑,包括:
- 数据库操作:
- 乐观锁扣减库存:使用乐观锁更新数据库库存,避免并发导致的库存超卖。
- 记录用户领取信息:将用户领取记录写入数据库。
- 数据库操作:
6. 缓存与持久化同步
- 更新缓存:在数据库更新后,将用户的领取记录写入缓存,确保缓存与数据库一致。
- 方案一:直接同步到 Redis,提高读取性能,保证高并发下的响应速度。
- 方案二:通过 Canal 监听数据库 Binlog,将数据库更新操作实时同步到 Redis,确保最终一致性。
7. 定时任务和缓存清理
- 定时任务:使用xxl-job定时任务删除过期的缓存,避免无用数据占用缓存空间。
- 清理策略:使用RocketMQ的延时消息的功能,发送延时消息队列,等待优惠券到期后,将优惠券信息从缓存中删除
8. 流程结束
- 用户接收到领取结果:
- 成功:返回领取成功信息,用户可使用领取的优惠券。
- 失败:返回领取失败的原因,如库存不足或达到领取限制。