秒杀系统设计与实现:从限流到最终一致性的完整方案
秒杀系统设计与实现:从限流到最终一致性的完整方案
本文基于 Go 语言实现,介绍一个高并发秒杀系统的核心设计思路,涵盖网关限流、用户频控、Redis + Lua 预扣库存、MQ 异步落库,以及 Redis 与 MySQL 之间数据一致性的权衡方案。
目录
一、整体架构概览
1 | ┌──────────────────────────────────────┐ |
核心思想:
- Redis 承担 99% 的流量过滤,挡在 MySQL 前面
- MySQL 的
WHERE条件 UPDATE 是超售的最后防线 - 允许 Redis 与 MySQL 之间短暂的数据不一致,通过异步同步兜底
- 不做加锁强一致——在秒杀场景下,CAP 中选择 AP,容忍短暂 C 不一致
二、前置网关限流
在请求到达业务层之前,API Gateway 做第一层流量整形。防止瞬时流量直接把下游冲垮。
方案:令牌桶限流
使用 Go 的 golang.org/x/time/rate 或自实现令牌桶。
1 | package gateway |
思维导图
1 | 网关限流 |
三、单用户购买限制
防止同一个用户用脚本疯狂刷请求。使用 Redis 记录每个用户最近一次请求时间,N 秒内只允许一次。
1 | package service |
流程图
1 | 请求到达 |
四、Redis + Lua 库存预扣减
这是秒杀系统最核心的环节。通过 Lua 脚本保证 Redis 操作的原子性,实现库存的预扣减。
数据结构
| Redis Key | 类型 | 说明 |
|---|---|---|
seckill:stock:{activityID} |
String (int) | 基准库存,初始化时从 MySQL 同步 |
seckill:pending:{activityID} |
String (int) | 待扣减订单流水计数(已预扣但还未落库确认) |
预扣减逻辑
1 | 可用库存 = 基准库存 - 待扣减订单流水数 |
Lua 脚本实现
1 | -- seckill_deduct.lua |
Go 调用代码
1 | package service |
确认与回滚 Lua 脚本
1 | -- seckill_confirm.lua |
1 | -- seckill_rollback.lua |
为什么失败回滚只减 pending 而不恢复 stock?
因为 stock(基准库存)代表的是 MySQL 数据库层面的真实初始库存。预扣时只动了 pending,stock 没有变。
失败时回滚 pending 即可,stock 保持不变,后续请求看到的可用 = stock - pending 自然就恢复了。
完整预扣流程思维导图
1 | Redis + Lua 预扣减流程 |
五、MQ 异步落库 MySQL
预扣成功后不直接写 MySQL,而是丢到消息队列,由消费者异步处理。这样:
- 业务层响应极快(只是写了一条 Redis + 发了一条 MQ)
- 数据库压力可控(消费者可以控制消费速率)
MySQL 防超售的 WHERE 条件 UPDATE
1 | -- 核心 SQL:一条带条件的 UPDATE 天然防止超售 |
affected_rows = 0 说明库存不够,这条 UPDATE 没有实际执行——这是 MySQL 层面的最后一道防线。
Go 消费者实现
1 | package consumer |
为什么用 MQ 而不是直接写 MySQL?
1 | 同步方案: |
六、数据一致性方案
核心观点:秒杀场景下,引入 Redis 本身就意味着会出现短暂数据不一致。只要 MySQL 的 WHERE 条件 UPDATE 能保证不超售,短暂的不一致是可以接受的。
追求绝对的实时一致 = 放弃 Redis,回到纯 MySQL 。所以我采取的策略是 “最终一致性 + 多种兜底方案组合”。
方案 A:Binlog 订阅同步(推荐,毫秒级误差)
通过 Canal / Maxwell / go-mysql 等工具订阅 MySQL binlog,实时同步到 Redis。
1 | MySQL → binlog → Canal → 解析变更 → 更新 Redis |
Go 伪代码实现(基于 go-mysql)
1 | package sync |
优点: 正常情况毫秒级延迟,几乎实时
缺点: 依赖 MySQL binlog 基础设施,运维复杂度稍高
方案 B:后台强一致同步(慎用,加锁性能差)
当发现大量不一致,不一致超过阀值,可考虑使用此方法强同步一次。
1 | package sync |
优点: 绝对一致,同步完的那一刻 Redis == MySQL
缺点: FOR UPDATE 锁住整行,秒杀中的 UPDATE 语句会被阻塞 导致 吞吐下降
使用场景: 发现大量不一致需要紧急修复时手动触发,非常用方案,紧急修复时用
方案 C:定时对比同步(有误差)
后台定时任务,每隔一段时间(比如 30s)扫描一次,发现不一致时以 MySQL 为准修正 Redis。
1 | package sync |
优点: 实现简单,资源消耗低
缺点: 存在一个周期内的误差(比如 30s),用户可能看到”明明显示有货但买不到”
适用: 作为兜底 + 辅助方案
三方案组合策略
1 | ┌────────────────────────────┐ |
为什么不追求绝对实时一致?
1 | 秒杀场景下,短暂的不一致换来的性能提升是值得的。MySQL 的 WHERE 条件 UPDATE 保证了不超售这个底线。 |
七、总结与权衡
架构职责分布
| 组件 | 职责 | 容忍度 |
|---|---|---|
| API Gateway | 限流,拒绝超过系统承载能力的请求 | 宁可误杀,不能漏过 |
| 用户频控 | 防止同一用户刷请求 | 宽松但有 |
| Redis + Lua | 预扣减库存,99% 请求在此被拦截(已售罄的) | 允许短暂不准 |
| MQ | 削峰填谷,异步化 | 允许堆积(可扩容消费者) |
| MySQL | 真实库存落库,防止超售的最后防线 | 必须准确 |
| Binlog 同步 | 毫秒级异步矫正 Redis | 主力方案 |
| 定时对比 | 兜底矫正 | 辅助方案 |
| 强一致同步 | 紧急修复 | 慎用 |
关键要点
MySQL
WHERE stock >= NUPDATE 是不超售的底线。 只要这条 SQL 跑了,就不会多卖。Redis 只是”加速层”,不是”真实层”。 它的数据可以滞后,可以偏离,只要最终被拉回到和 MySQL 一致就行。
预扣减的回滚逻辑: 成功确认 → 减 stock + 减 pending;失败回滚 → 只减 pending。
三层同步兜底: Binlog 毫秒级 → 定时任务秒级 → 强一致手动触发。
完整代码示例可参考:[GitHub 仓库链接(TODO)]
如有疑问或建议,欢迎在评论区交流讨论。
- 标题: 秒杀系统设计与实现:从限流到最终一致性的完整方案
- 作者: Jie
- 创建于 : 2026-06-02 00:00:00
- 更新于 : 2026-06-03 13:00:53
- 链接: https://your-domain.com/flash-sale-system/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。