go语言库存扣减不能只用mutex,因锁粒度大拖慢吞吐,且网络调用或panic易致锁泄露;应优先用redis+lua脚本保证原子性,辅以数据库最终一致性校验和异步核对机制。

Go语言里库存扣减为什么不能只用 mutex
高并发下直接用 sync.Mutex 或 sync.RWMutex 保护库存变量,表面看能避免超卖,但实际会严重拖慢吞吐——锁粒度太大,所有请求串行排队。更隐蔽的问题是:一旦扣减逻辑里涉及网络调用(比如查用户权限、写日志)、数据库事务或 panic 恢复不全,锁可能被长期持有甚至泄露。
- 别在扣减函数里做非原子操作,比如先
db.QueryRow再db.Exec;必须合并为一条带条件的 SQL,例如UPDATE goods SET stock = stock - 1 WHERE id = ? AND stock > 0 - 如果用 Redis,优先选
DECRBY+GET组合,而不是GET→ 判断 →SET,后者有竞态窗口 - Go 的
sync/atomic只适用于整数型库存且无业务校验(如限购、用户等级),真实场景几乎不够用
用 CAS 实现无锁库存扣减的实操要点
Go 标准库没提供类似 Java 的 AtomicInteger.compareAndSet,但可以用 atomic.CompareAndSwapInt64 模拟,前提是库存值能映射成单一整数且更新逻辑足够简单。
- 把库存存进
int64类型的指针变量,比如var stock int64 = 100,然后用atomic.LoadInt64(&stock)读,atomic.CompareAndSwapInt64(&stock, old, old-1)尝试扣减 - 注意:CAS 成功只代表数值变了,不代表业务成功——比如用户已下单取消、库存已被冻结,这些状态必须额外记录,不能全压在原子变量上
- 失败时别死循环重试,加个
runtime.Gosched()让出时间片,或者退回到带 DB 回滚的事务流程
Redis + Lua 脚本是更稳妥的落地选择
本地内存和数据库都难兼顾一致性与性能,Redis 原子执行 Lua 是目前最常用解法。关键是脚本必须返回明确结果,且 Go 客户端要正确解析。
- Lua 脚本里用
redis.call("GET", KEYS[1])读库存,用redis.call("DECR", KEYS[1])扣减,但得先判断是否大于 0,否则DECR会变成负数——正确写法是if tonumber(current) > 0 then redis.call("DECR", KEYS[1]) return 1 else return 0 end - Go 侧调用要用
redis.Eval,传入脚本、key 列表、参数列表;返回值是int64,1 表示扣减成功,0 表示库存不足 - 别忽略连接池配置:
redis.Options.PoolSize至少设为并发 QPS 的 2–3 倍,否则 Redis 连接等待会成为瓶颈
超卖兜底必须依赖数据库最终一致性
无论用哪种方案,线上都得有异步核对机制。缓存和内存可能丢失、网络分区、脚本执行失败却返回成功——这些都会导致“扣了但没记账”。
- 每次扣减成功后,往消息队列(如 Kafka、NATS)发一条
deduct_success事件,包含订单 ID、商品 ID、扣减数量 - 单独起一个消费者服务,从 DB 查该订单对应的实际库存变更记录,比对是否一致;不一致就触发告警并人工介入
- DB 的库存字段建议加
version字段,每次更新都SET version = version + 1,防止重复扣减被覆盖
真正麻烦的不是怎么扣,而是扣完怎么证明没扣错——所有中间环节都要留痕,日志、指标、事件缺一不可。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











