Redis.Decr原子扣库存可防超卖,需配合过期设置、失败回滚、SETNX防重、channel限流及异步落单。

用 redis.Decr 原子扣库存,别写“先查后减” SQL
高并发下秒杀失败或超卖,八成出在库存校验逻辑上。最典型错误是两行 SQL:SELECT stock FROM seckill_goods WHERE id=? 然后判断再 UPDATE SET stock=stock-1——中间任何并发请求都会绕过检查。
正确做法是把“判断 + 扣减”压进一条原子操作。redis.Decr 天然支持:它返回扣减后的值,你只需检查是否 ≥0 就能确认成功与否。
-
Decr是线程安全的,不依赖客户端加锁,也不受 Go 协程调度影响 - 务必设置 key 过期时间,例如:
SETEX seckill:stock:123 3600 100,避免场次结束库存残留 - 扣减失败时要立刻
Incr回滚,否则库存会永久变负(尤其在 panic 或网络中断时)
用 SETNX 防重复下单,别信前端 disabled 或 session
用户刷新页面、F5 重发、脚本模拟,都可能让同一个账号多次提交。只校验登录态或前端按钮置灰毫无意义。
必须在服务端做幂等控制,推荐用 Redis SETNX(set if not exists)打唯一标记:
立即学习“go语言免费学习笔记(深入)”;
- key 设计为
seckill:order:123:user456(场次 ID + 用户 ID) - value 可存随机 token 或时间戳,过期时间略长于支付超时(比如 15 分钟)
- 只有
SETNX返回 1 才允许走后续流程;返回 0 直接返回 “您已参与本场秒杀” - 不能用本地
map或sync.Map,分布式部署下无效;也不能用 MySQL 唯一索引替代——写库太慢,扛不住瞬时洪峰
用带缓冲的 channel 控制 goroutine 并发度
写个 for i := 0; i < 10000; i++ { go handle() } 看似简单,实际极易触发系统级问题:文件描述符耗尽、GC 频繁、Redis 连接池被打爆、甚至进程被 OOM killer 杀掉。
真正可控的方式是用带缓冲的 channel 做信号量:
var sem = make(chan struct{}, 100) // 最多同时处理 100 个请求
func seckillHandler() {
sem <- struct{}{} // 获取令牌
defer func() { <-sem }() // 归还令牌
// 执行秒杀逻辑
}- 数值 100 不是拍脑袋定的,应基于压测结果:观察 Redis
INFO commandstats中cmdstat_decr延迟、MySQLThreads_running峰值、以及机器 CPU/内存水位 - 如果用了 Gin,可在中间件里统一加这层限流,而非每个 handler 自己写
- 别用
sync.WaitGroup单纯等 goroutine 结束——它不控并发,只管收尾
异步落单不是“可选优化”,而是分层保命手段
秒杀成功 ≠ 订单落地。数据库写入是慢操作,强同步会拖垮整个链路。所有非实时强一致环节,必须下沉到消息队列异步处理。
- 秒杀通过后,只写 Redis 库存和订单预占标记,立即返回“抢购成功”
- 订单创建、优惠券核销、库存最终落库、通知推送等,全部丢进 Kafka/RabbitMQ 异步消费
- 若用 go-zero,可直接配
mq代理组件;自研则建议封装Producer和Consumer接口,避免硬编码 broker 地址 - 注意消息幂等性:消费者需根据订单号去重,防止重复下单
真正难的从来不是“怎么扣库存”,而是当十万请求撞上来时,哪一层该挡、哪一层该放、哪一层该延后——这些边界一旦模糊,系统就从高并发变成高崩溃。


















