一、Redis 扣减成功但 MQ 发送失败的解决方案
1. 核心原则:确保 “扣减库存” 与 “发送消息” 的原子性
Redis 扣减库存与 MQ 发送消息是两个独立操作,若中间失败(如 MQ 宕机),会导致 “库存已扣但订单未生成” 的不一致问题。解决方案如下:
方案 1:本地消息表 + 重试机制(最终一致性)
-
步骤:
- Redis 扣减库存:使用 Lua 脚本原子执行
GET库存 → 校验 → 扣减
流程,确保扣减成功。 - 写入本地消息表:在业务库中创建
seckill_message
表,记录消息状态(status=0
表示待发送),字段包括order_id
、user_id
、goods_id
、stock
、create_time
等。- 此操作需与 Redis 扣减在同一本地事务中(若 Redis 扣减成功但表写入失败,需回滚 Redis 扣减)。
- 发送 MQ 消息:若发送成功,更新消息表
status=1
;若失败,status
保持0
。 - 定时任务重试:后台定时扫描
status=0
且超过一定时间(如 30 秒)的消息,重新发送 MQ,重试次数上限(如 3 次),超过后标记为失败(人工介入)。
- Redis 扣减库存:使用 Lua 脚本原子执行
-
优势:通过本地事务保证 “扣减库存” 与 “消息记录” 的原子性,重试机制确保消息最终送达。
方案 2:Redis 事务 + 消息队列确认机制
-
步骤:
- Redis 扣减 + 消息暂存:使用 Redis 事务,先扣减库存,再将消息内容(订单信息)存入 Redis 临时队列(如
seckill_pending_messages
),确保两者同时成功或失败。 - 异步发送 MQ:单独的线程消费
seckill_pending_messages
,发送消息到 MQ,若收到 MQ 的确认回调(如 RabbitMQ 的confirm
机制),则从临时队列删除消息;否则保留,等待重试。 - 重试兜底:定时任务扫描临时队列,对超时未发送成功的消息重新发送。
- Redis 扣减 + 消息暂存:使用 Redis 事务,先扣减库存,再将消息内容(订单信息)存入 Redis 临时队列(如
-
优势:纯内存操作,性能高,适合超高频场景。
二、整体设计的补充优化建议
1. 前端优化
- 静态资源 CDN:秒杀页面的静态资源(图片、CSS、JS)部署到 CDN,减少源站压力。
- 按钮防重复点击:通过前端防抖(如 3 秒内禁止重复点击),减少无效请求。
- 预加载与倒计时:活动开始前预加载商品信息,倒计时结束后再开放请求,避免提前流量冲击。
2. 网关层优化
- 令牌桶限流:基于用户 ID/IP 的令牌桶算法限流(如每秒允许 10 次请求),比简单 IP 限流更灵活。
- 黑名单机制:拦截历史恶意请求的 IP / 用户 ID,直接拒绝服务。
- 协议压缩:启用 HTTP/2 或 gzip 压缩,减少传输数据量,提升响应速度。
3. 缓存层优化
- 库存预热与过期时间:秒杀前将商品库存写入 Redis(如
seckill:stock:{goodsId}
),设置合理过期时间(略长于活动时间),避免缓存雪崩。 - 分布式锁防超卖:Redis 扣减库存必须用原子操作(如
DECR
+ 校验结果,或 Lua 脚本),确保并发安全。lua
-- Lua脚本:检查库存并扣减,确保原子性 local stock = redis.call('get', KEYS[1]) if stock and tonumber(stock) > 0 then return redis.call('decr', KEYS[1]) end return -1 -- 库存不足
4. MQ 层优化
- 消息幂等性:消费端通过
订单ID
或用户ID+商品ID+时间戳
作为唯一键,结合 Redis / 数据库的幂等表(如seckill_order_unique
),防止重复下单。 - 死信队列:MQ 中无法消费的消息(如数据库暂时不可用)进入死信队列,后续人工处理,避免消息丢失。
- 流量削峰:MQ 设置合理的队列长度和消息过期时间,避免消息堆积导致内存溢出。
5. 数据库层优化
- 分库分表:订单表按用户 ID 哈希分表,库存表按商品 ID 分表,减少单表压力。
- 库存冻结机制:Redis 扣减库存后,数据库库存可设为 “总库存 = 可售库存 + 冻结库存”,MQ 消费时将冻结库存转为实际扣减,避免超卖。
- 读写分离:订单查询走从库,写入走主库,减少主库压力。
三、一致性校验与监控
-
库存对账:
- 定时任务(如每 5 分钟)对比 Redis 库存与数据库库存,若有差异,以数据库为准修正 Redis(避免 Redis 因网络 / 重启丢失数据)。
- 秒杀结束后,生成库存差异报表,人工核对异常订单。
-
全链路监控:
- 接入 APM 工具(如 SkyWalking),监控 Redis 扣减成功率、MQ 消息延迟、数据库写入耗时等指标,及时预警瓶颈。
- 日志埋点:记录每个环节的关键日志(如 Redis 扣减、MQ 发送、订单生成),便于问题追溯。
总结
秒杀系统的核心是 “流量分层过滤” 和 “异步化削峰”:前端过滤无效请求,网关拦截恶意流量,缓存承接高频读写,MQ 削峰填谷,数据库保证最终一致性。针对 “Redis 扣减成功但 MQ 发送失败” 的问题,通过本地消息表 + 重试或Redis 临时队列 + 确认机制可实现最终一致性,结合全链路监控和对账机制,确保系统稳定可靠。