Beego框架不解决库存超卖,必须用Redis+Lua原子脚本实现扣减;禁止在Controller中分步GET/DECR,需封装Lua脚本一次性执行校验与扣减,并做好降级与一致性兜底。

Beego 框架本身不解决库存超卖问题,它只是 HTTP 路由和控制器容器;真正防超卖必须靠 Redis + Lua 原子脚本,且不能把扣减逻辑写在 Beego Controller 里裸调 GET + SET。
为什么不能在 Beego Controller 里直接操作 Redis 扣库存
常见错误是这样写:
func (c *OrderController) Submit() {
stock := c.Redis.Get("stock:1001").Val()
if stock > 0 {
c.Redis.Decr("stock:1001") // ❌ 两步非原子
c.Data["json"] = map[string]interface{}{"code": 0}
}
}
这会导致大量请求在 GET 和 DECR 之间“插队”,瞬间超卖。Beego 的协程并发能力越强,这个问题越严重。
- Beego 默认启用 Goroutine 处理每个请求,
GET→DECR中间可能有几十个并发请求同时读到相同库存值 - Redis 单线程只保证单命令原子性,不保证多个命令组合的原子性
- 即使加了 Beego 的
middleware限流,也拦不住已进来的请求在 Redis 层面竞争
必须用 Lua 脚本封装检查+扣减逻辑
Beego 中正确做法是:把库存校验与扣减封装成一个 Lua 脚本,通过 redis.Script.Eval 一次性提交执行。Beego 只负责传参、收结果、做后续异步落库。
- 脚本必须包含三件事:读当前值、判断是否 ≥ 扣减数、足够则
DECRBY并返回结果,缺一不可 - 不要用
GET+INCR组合,要用DECRBY配合前置判断,避免负数库存 - 脚本返回值要明确区分:1=成功、0=库存不足、-1=key 不存在,Beego Controller 根据这个码做分支处理
示例脚本(注册到 Redis 客户端后复用):
local key = KEYS[1]
local num = tonumber(ARGV[1])
local stock = tonumber(redis.call('GET', key) or '0')
if stock < num then return 0 end
redis.call('DECRBY', key, num)
return 1
Beego 中集成 Redis Lua 的实操要点
Beego v2.x 使用 github.com/go-redis/redis/v8 或 v9,注意版本兼容性。关键不是“怎么写 Controller”,而是“怎么安全初始化和复用脚本”。
- 脚本对象(
*redis.Script)应在app.conf加载后、服务启动前初始化一次,不要每次请求都redis.NewScript - Beego 的
models层或独立service包里封装Deduct(ctx, skuID string, count int64) (int64, error)方法,内部调用script.Eval - Controller 中只做轻量转发:
result, err := stockSvc.Deduct(c.Ctx.Request.Context(), c.GetString("sku"), count) - 别在 Beego 的
Prepare()或Finish()里做扣减——它们不是事务边界,失败无法回滚
容易被忽略的兜底和降级点
哪怕 Lua 脚本跑通了,系统仍可能因消息丢失、DB 写失败、Redis 故障而出现不一致。Beego 层要预留干预入口,而不是假定“脚本成功=订单成立”。
- 扣减成功后,必须发消息到 Kafka/RocketMQ,**不能同步更新 MySQL**;Beego Controller 只管发,不管消费结果
- 提供管理接口(如
/admin/stock/check?sku=1001),直接查 Redis 当前值和 DB 当前值比对,用于人工核验 - 当 Redis 不可用时,Beego 应快速降级到数据库乐观锁方案(
UPDATE ... WHERE stock >= ?),但需提前在 Model 层准备好 fallback 逻辑 - 缓存 key 设计要分片(如
stock:1001:%d% (skuID%8)),否则单 key 成瓶颈,Beego 再快也扛不住 Redis 线程排队


















