优惠券秒杀模块方案设计

本文章针对优惠券秒杀场景所进行的方案设计,考虑不周的地方,烦请指正。

在我们兑换/秒杀优惠券模板的接口中,可能会存在以下三个难点:
- 高并发流量压力:秒杀活动往往会瞬间吸引大量用户访问系统,导致流量骤增,如果直接访问数据库,可能会让数据库负载过重,甚至导致宕机。
- 库存超卖问题:由于并发请求,多个用户同时抢购可能会导致系统超卖,即多个用户同时购买到同一库存。
- 用户超领问题:优惠券中会有一个限制,每个用户限流几张,应该如何避免用户领取超过这个限制。

针对上面的问题,整个方案的流程如下:
image

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. 流程结束

  • 用户接收到领取结果:
    • 成功:返回领取成功信息,用户可使用领取的优惠券。
    • 失败:返回领取失败的原因,如库存不足或达到领取限制。
posted @ 2024-10-16 11:39  b1uesk9  阅读(20)  评论(0编辑  收藏  举报