一、核心问题分析

在电商秒杀或大促场景下,可能出现 1000+ 用户同时下单 同一商品的情况。系统面临两大挑战:

  1. 防超卖:库存只有 N 件,不能因为并发导致实际卖出 > N 件

  2. 高性能:直接操作数据库会因行锁、连接池瓶颈导致响应缓慢甚至崩溃

本质:在极高并发下,如何保证“库存扣减”操作的原子性、一致性,同时兼顾吞吐量。

二、整体架构:Redis + 消息队列组合

用户请求 → 网关 → 限流 → Redis 原子扣减(成功则进入队列)→ 消息队列 → 消费者 → 数据库最终扣减
                       ↓ 失败
                   直接返回“抢购失败”

设计思路

  • Redis 做前置拦截:利用单线程原子操作快速过滤掉大部分无效请求

  • 消息队列做异步削峰:将成功请求异步落库,避免数据库瞬时压力

三、Redis 原子扣减(第一道防线)

3.1 为什么选 Redis?

  • 内存操作,性能极高(10w+ QPS)

  • DECR / DECRBY 天然原子性,无并发安全问题

3.2 核心命令

-- Lua 脚本保证原子性:检查库存 > 0 再扣减
local key = KEYS[1]
local stock = tonumber(redis.call('get', key))
if stock and stock > 0 then
    redis.call('decr', key)
    return 1   -- 扣减成功
else
    return 0   -- 库存不足
end

3.3 扣减成功后做什么?

  • 生成预下单记录(状态:待确认),写入消息队列

  • 立即返回用户“下单中,请稍后”

3.4 注意点

  • Redis 库存预热:活动开始前将数据库库存同步到 Redis

  • 设置合理过期时间:避免长期占用内存

  • 使用 Redisson 等客户端内置的分布式限流器,配合令牌桶做流量平滑

四、消息队列异步处理(第二道防线)

4.1 为什么需要 MQ?

  • 防止瞬间海量请求穿透 Redis 后直接冲击数据库

  • 解耦下单主流程与库存落库操作,提升用户体验

4.2 消息内容示例

{
  "orderId": "123456",
  "skuId": "10001",
  "quantity": 1,
  "userId": "888",
  "timestamp": 1700000000
}

4.3 消费者逻辑

  1. 开启数据库事务

  2. 再次检查数据库库存(防止 Redis 与 DB 不一致)

  3. 执行 UPDATE inventory SET stock = stock - 1 WHERE sku_id = ? AND stock > 0

  4. 创建订单记录,状态为“已支付”或“待支付”(根据业务)

  5. 提交事务,发送“订单创建成功”事件

4.4 消费失败处理

  • 使用 重试机制(如 RocketMQ 的重试队列)

  • 设置最大重试次数(如 3 次),超过则进入死信队列,人工介入

五、数据一致性保障

5.1 本地事务表 + MQ 重试

场景:消费者扣减数据库库存成功,但向 MQ 确认消费时网络闪断,导致 MQ 认为消费失败,重新投递消息 → 可能重复扣减。

解决方案

  • 建立本地消息表:消费者处理前,先插入一条处理记录(幂等键)

  • 处理逻辑:INSERT IGNORE INTO process_log(tx_id) ...,若已存在则直接返回成功

  • 数据库扣减操作与插入幂等记录放在同一本地事务中

流程

消费消息 → 开启事务 → 检查幂等表 → 扣减库存 → 插入幂等记录 → 提交事务 → 向 MQ 返回 SUCCESS

5.2 定时任务兜底:同步 Redis 与 DB 库存差异

尽管 Redis 扣减和 DB 扣减最终一致,但可能因网络、程序 bug 导致偏差。需要定时对账。

任务逻辑(每 5 分钟执行)

-- 找出 DB 与 Redis 中库存差异超过阈值的商品
SELECT sku_id, db_stock, redis_stock 
FROM (
    SELECT sku_id, stock AS db_stock FROM inventory
) t1
JOIN (
    -- 从 Redis 批量获取
) t2
WHERE ABS(db_stock - redis_stock) > 0
  • 若差异较小(如 1~2 件),以 DB 为准 修正 Redis

  • 若差异较大,触发告警,人工排查是否存在超卖

六、异常处理:Redis 故障时熔断降级

6.1 故障场景

  • Redis 连接超时、主从切换、内存满载等

6.2 熔断策略

  • 使用 Hystrix 或 Resilience4j 包裹 Redis 扣减操作

  • 统计错误率,超过阈值(如 50% 错误率持续 10 秒)则开启熔断

6.3 降级方案

  • 熔断后,所有请求直接走数据库扣减(性能会大幅下降,但保证核心功能可用)

  • 返回提示:“当前活动火爆,请稍后重试”

  • 同时发送告警,通知运维介入

6.4 恢复机制

  • 半开状态:定时允许少量请求通过 Redis,若成功则关闭熔断

七、订单超时释放库存

7.1 业务场景

用户下单后未在 15 分钟 内支付,需自动释放已占用的库存,供其他用户购买。

7.2 方案一:MQ 延迟消息(推荐)

实现步骤

  1. 用户下单成功(Redis 扣减 + 创建待支付订单)后,向 MQ 发送一条 延迟消息,延迟时间为 15 分钟

  2. 消息内容:{ orderId, skuId, quantity }

  3. 延迟消息消费者:

    • 检查订单状态是否为“待支付”

    • 若是,则执行释放库存逻辑

释放库存操作顺序(关键!):

1. 先更新 Redis:INCR inventory(加回库存)
2. 再更新数据库:UPDATE inventory SET stock = stock + ? WHERE sku_id = ?
3. 更新订单状态为“已取消”

  • 为什么先 Redis 后 DB?
    因为 Redis 是后续新订单的第一道防线,必须先释放才能让新请求看到库存。DB 最终一致即可。

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐