秒杀库存超卖的根本原因是查库存与扣库存非原子操作;应改用带条件的update语句、redis预减并校验返回值、基于压测设定channel容量、订单异步落库。

秒杀库存扣减为什么总超卖?
根本原因:查库存和扣库存不是原子操作。并发时多个 goroutine 同时读到 stock > 0,都执行扣减,数据库里只剩 0,却写了 100 次成功。
常见错误写法:
if stock > 0 { // 非原子判断
db.Exec("UPDATE goods SET stock = stock - 1 WHERE id = ?")
}
正确做法是把判断和更新压进一条 SQL:
-
UPDATE seckill_goods SET stock = stock - 1 WHERE goods_id = ? AND stock > 0—— 利用 MySQL 行锁 + 条件过滤,影响行数为 0 就代表抢空 - 务必检查
res.RowsAffected(),不能只看 err - 高并发场景下,这条 SQL 仍可能因锁竞争拖慢响应,需配合 Redis 预减库存降压
Redis 预扣库存怎么写才不翻车?
用 DECR 是对的,但直接 DECR 后不校验返回值,会导致库存变成负数,后续逻辑全乱。
典型翻车点:
- 没处理
redis.Nil(key 不存在)或网络超时,直接 panic - 扣完不看返回值就创建订单,
DECR返回 -1 也照单全收 - 失败后忘记
INCR回滚,导致库存永久少 1
安全写法要点:
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
stock, err := redisCli.Decr(ctx, key).Result()
if errors.Is(err, redis.Nil) {
return false, errors.New("活动未开始")
}
if err != nil {
return false, err
}
if stock <h3>用 channel 做限流,100 这个数字怎么定?</h3><p><code>var sem = make(chan struct{}, 100)</code> 里的 100 不是拍脑袋写的,它直接决定你系统吞吐和崩溃边界。</p><p>这个值必须基于压测反馈来调,不是越小越稳、也不是越大越好:</p>
- 观察
Redis INFO commandstats中cmdstat_decr的 p99 延迟,超过 5ms 就该降并发 - 看 MySQL 的
Threads_running峰值,持续 > 200 就说明 DB 开始排队 - 盯住机器 CPU 使用率,长期 > 70% 或内存水位逼近 90%,就得缩容 channel 容量
- 如果用了 Gin,直接在中间件里统一塞
和 <code>defer func(){ ,别每个 handler 自己写一套
异步落单为什么不是“优化项”,而是必选项?
同步写库扛不住秒杀洪峰——哪怕你 Redis 预扣成功了,MySQL 插订单一卡,整个请求就卡住,channel 被占满,新请求直接 503。
异步不是为了快,是为了活:
- 秒杀接口只做三件事:校验用户、预扣 Redis 库存、发消息到队列(如 Kafka / NSQ / Redis Stream)
- 单独起 worker 消费队列,重试机制要配好(比如失败后 1s/3s/10s 三次重试),避免丢单
- 用户看到“下单成功”时,其实只是“抢到资格”,订单状态要轮询或 WebSocket 推送
- 别用
sync.WaitGroup等落库完成——它只管 goroutine 结束,不管并发数,起 1000 个照样打崩 DB
真正难的不是代码怎么写,是每层缓冲的容量、超时、重试、监控指标之间怎么咬合。一个参数调歪,整条链路就从“高可用”变成“高不可用”。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










