库存锁定不能只靠数据库行锁,因其无法跨服务实例协同且缺乏业务语义隔离;必须用redis+lua实现“检查-占用-过期”原子操作,并通过唯一token、ttl合理设置与lua安全解锁保障分布式一致性。

库存锁定为什么不能只靠数据库行锁
直接在 UPDATE inventory SET stock = stock - 1 WHERE sku_id = ? AND stock >= 1 上加事务,看似能防超卖,但实际在高并发下会暴露两个硬伤:一是锁住的是单行,无法防止同一商品被多个分布式服务实例同时扣减(比如订单服务和营销服务都走这套逻辑);二是没做业务语义隔离——比如“预占库存”和“真实扣减”要分阶段,而纯 SQL 行锁不感知业务状态。
GoLand 本身不干预逻辑设计,但它能帮你快速定位这类问题:打开 Database 工具窗口执行该 SQL,配合 Explain 查看是否走了索引;在代码里 Ctrl+Click 跳转到 DAO 层时,立刻能看出有没有包裹在 tx.Begin() 里;更重要的是,它会在你写 if rowsAffected == 0 后自动提示“可能需要重试或抛出业务异常”,而不是静默失败。
用 Redis + Lua 实现原子化锁定与 TTL 自动释放
电商场景下,库存锁定必须满足:一次操作完成“检查余量 + 占用 + 设置过期”。纯 Go 写 GET + SET 有竞态,必须用 Lua 脚本保证原子性。GoLand 对 redis.Eval 的参数校验很严格——如果你传入的 keys 是空 slice 或 args 类型错乱,它会在编辑器里标红并提示 “expected []string, got []interface{}”。
常见错误现象:(error) ERR Error running script (call to f_...): @user_script:1: user_script:1: attempt to compare number with nil,通常是因为 Lua 脚本里用了 tonumber(ARGV[1]) 但传了空字符串。
- 脚本里所有数值比较必须先
if not ARGV[1] or tonumber(ARGV[1]) == nil then return 0 end - TTL 建议设为 15–30 分钟,不是订单超时时间(比如 30 分钟),而是“最长可能等待支付完成的时间”,否则用户刚点支付,锁就没了
- GoLand 的 Run Configuration 里,记得勾选 “Include system environment variables”,否则本地调试时
REDIS_URL可能为空导致连接失败
延迟释放锁的触发时机必须和业务事件强绑定
“延迟释放”不是定时任务扫表,而是由明确的业务信号驱动:支付成功回调、订单取消、用户主动关闭下单页(前端发 /api/v1/order/cancel-prelock)。GoLand 的 HTTP Client 插件能直接测试这些接口,右键请求文件 → “Send Request”,比 curl 快且自动带 token。
GoLand 2026.1.1 是 2026.1 发布后的首个维护修正版本,适合已经开始体验 2026.1 新功能并希望同步补丁的开发者。它更适合用于入门项目、现有项目迁移测试和 IDE 行为验证。
容易踩的坑:
- 支付回调通知是异步的,你的 handler 里调
redis.Del失败不能直接返回 HTTP 500,得记录日志并进重试队列(比如用github.com/hibiken/asynq) - 不要在订单创建事务里直接释放锁——此时订单还没落库,万一事务回滚,锁却没了,会导致超卖
- 前端页面关闭时的取消请求,建议加防抖(
debounce 500ms),避免用户快速切页触发多次UNLOCK,Redis 里删不存在的 key 没问题,但会浪费 QPS
本地调试时如何模拟分布式竞争
在 GoLand 里开多个 Run Configuration,分别启动订单服务、支付模拟器、库存管理后台,端口错开(如 :8081, :8082, :8083),然后用内置 Terminal 并发发压测请求:
for i in {1..10}; do curl -X POST http://localhost:8081/api/v1/order/lock?sku_id=1001 & done
这时候观察 Redis CLI:redis-cli --scan --pattern "lock:sku:*" | wc -l,如果数量远大于预期(比如发了 10 次但看到 15 个 key),说明 Lua 脚本没正确判断库存余量,或者 SETNX 和 EXPIRE 拆成了两步。
真正难调试的点在于:GoLand 的 Debugger 在 Lua 脚本里断不了点,你得把关键变量打日志——比如在 Lua 里 redis.log(redis.LOG_WARNING, "stock_remain:" .. stock_remain),然后去 Redis 日志里翻(tail -f /usr/local/var/log/redis.log)。这个步骤很多人漏掉,结果以为逻辑没问题,其实是脚本根本没按预期执行。










