高并发电商下单防超卖方案设计
一、核心问题分析
在电商秒杀或大促场景下,可能出现 1000+ 用户同时下单 同一商品的情况。系统面临两大挑战:
-
防超卖:库存只有 N 件,不能因为并发导致实际卖出 > N 件
-
高性能:直接操作数据库会因行锁、连接池瓶颈导致响应缓慢甚至崩溃
本质:在极高并发下,如何保证“库存扣减”操作的原子性、一致性,同时兼顾吞吐量。
二、整体架构: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 消费者逻辑
-
开启数据库事务
-
再次检查数据库库存(防止 Redis 与 DB 不一致)
-
执行
UPDATE inventory SET stock = stock - 1 WHERE sku_id = ? AND stock > 0 -
创建订单记录,状态为“已支付”或“待支付”(根据业务)
-
提交事务,发送“订单创建成功”事件
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 延迟消息(推荐)
实现步骤:
-
用户下单成功(Redis 扣减 + 创建待支付订单)后,向 MQ 发送一条 延迟消息,延迟时间为 15 分钟
-
消息内容:
{ orderId, skuId, quantity } -
延迟消息消费者:
-
检查订单状态是否为“待支付”
-
若是,则执行释放库存逻辑
-
释放库存操作顺序(关键!):
1. 先更新 Redis:INCR inventory(加回库存)
2. 再更新数据库:UPDATE inventory SET stock = stock + ? WHERE sku_id = ?
3. 更新订单状态为“已取消”
-
为什么先 Redis 后 DB?
因为 Redis 是后续新订单的第一道防线,必须先释放才能让新请求看到库存。DB 最终一致即可。
更多推荐


所有评论(0)