直接用 get + set 会超卖,因为两个命令非原子性,高并发下多个请求同时读到相同库存值并扣减导致库存为负;必须用 lua 脚本保证“读-判-减”三步原子执行。

为什么直接用 GET + SET 会超卖
很多人在 Gin 里写库存逻辑,习惯先 redisClient.Get(ctx, "stock:1001").Val(),再判断是否 > 0,然后 redisClient.Set(ctx, "stock:1001", newStock, 0)。这看似合理,但两个命令之间没有任何保护——100 个并发请求几乎同时拿到 "5",全判断为“有库存”,接着全执行扣减,结果库存变成 -95。
必须用 Lua 脚本保证原子性
Redis 执行 Lua 是原子的,整个脚本要么全执行、要么不执行,中间不会被其他命令打断。这是防超卖最轻量也最可靠的方式。
- 脚本内容必须包含「读取当前值 → 判断是否足够 → 扣减或返回失败」三步,不能拆开
- 推荐用
DECRBY配合条件判断,而不是INCRBY取负数,语义更清晰 - Gin 中调用时,用
redisClient.Eval(ctx, luaScript, []string{"stock:1001"}, "1"),其中"1"是扣减数量
示例 Lua:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
if redis.call('get', KEYS[1]) == false then
return -1
end
local stock = tonumber(redis.call('get', KEYS[1]))
if stock
<h3>Redisson 不适合 Gin 直接集成</h3>
<p>Redisson 是 Java 生态的 SDK,底层依赖 Netty 和 Spring 生命周期管理,在 Go(Gin)项目里无法直接使用。有人试图用 Redisson 的 REST API 或封装 HTTP 调用,但会引入额外延迟、连接池管理复杂、锁续期机制失效,反而增加出错概率。</p>
- Go 生态已有成熟替代:如
go-redsync(基于 Redlock)、redislock(轻量级),但它们仍需手动处理锁过期和续期 - 真正需要分布式锁的场景(比如扣减后还要发 MQ、写 DB),建议用
redis.Client.SetNX+redis.Client.Expire组合,并严格校验返回值是否为true - 只要库存扣减本身走 Lua,绝大多数超卖问题已解决;锁只用于后续非原子步骤的串行化
扣减成功后记得检查 Lua 返回值
很多人写了 Lua,却忽略 Eval 的返回结果。它不是布尔值,而是整数:1 表示扣减成功,0 表示库存不足,-1 表示 key 不存在。Gin handler 里必须显式判断:
- 返回
1→ 继续生成订单 - 返回
0→ 返回{"code": 400, "msg": "库存不足"} - 返回
-1→ 触发告警,说明初始化漏了或 key 被误删 - 发生
redis.Nil错误(key 不存在)或网络错误,要重试或降级到 DB 校验
线上最常被忽略的是 -1 分支——它不报错,但意味着业务流程断在源头,比超卖更隐蔽。










