Redis Decr原子扣库存可避免超卖,需结合过期时间、失败回滚、SETNX防重及channel限流。

用 redis.Decr 原子扣库存,别在数据库里“先查后减”
高并发下秒杀失败或超卖,八成出在库存校验逻辑上。最典型错误是写两行 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 防重复下单,而不是靠前端限制或 session 判断
用户刷新页面、F5 重发、脚本模拟,都可能让同一个账号多次提交。只校验登录态或前端 disabled 按钮毫无意义。
必须在服务端做幂等控制,推荐用 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 并发度,别无脑 go handle()
写个 for i := 0; i 看似简单,实际极易触发系统级问题:文件描述符耗尽、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/内存水位 - 别用
sync.WaitGroup单纯等 goroutine 结束——它不控并发,只管收尾 - 如果用了 Gin,可在中间件里统一加这层限流,而非每个 handler 自己写
异步落单不是“可选优化”,而是保证可用性的必要分层
秒杀成功的瞬间,只要 Redis 库存扣减成功、订单号生成并返回给用户,就该立即响应。后续创建订单记录、扣减真实库存、发 MQ 通知风控等,全得扔进异步队列。
原因很实在:
- MySQL 写入延迟波动大,高峰期 insert 一条订单可能卡 50ms+,而 Redis
Decr稳定在 0.2ms 内 - 一旦 DB 主从延迟或慢查询拖住事务,整个秒杀接口就会雪崩
- 用
redis.RPush推消息到 list,再起一个 goroutine 消费(BLPop阻塞等待),比引入 Kafka/RabbitMQ 更轻量且够用
真正难的不是写异步逻辑,而是怎么兜底:消费失败要不要重试?消息丢了怎么办?这些得靠 Redis key 过期 + 定时任务扫描补偿,而不是指望“一次成功”。










