laravel 11 中用 lockforupdate 扣减库存需确保事务闭环:必须用 db::transaction 包裹、lockforupdate 置于查询链首、where 条件走索引、事务内禁用耗时操作、多表按固定顺序加锁以避免死锁。

在 Laravel 11 中用 lockForUpdate 做库存扣减,核心不是“加了锁就安全”,而是整套流程是否闭环:事务必须包裹、查询必须走索引、锁不能提前释放、业务逻辑不能拖沓。否则容易出现假锁、表锁、死锁或超时排队。
必须用 DB::transaction 包裹,且 lockForUpdate 要在查询链最前端
常见错误是先查再锁,比如:
✘ 错误写法$goods = Goods::find($id); // 查询已执行,脱离事务上下文<br>$goods->lockForUpdate(); // 此时调用无效,不加锁
正确做法是把 lockForUpdate() 直接连在查询构造器上,并确保整个操作在事务闭包内:
DB::transaction(function () use ($id) {<br> $goods = Goods::where('id', $id)->lockForUpdate()->firstOrFail();<br> if ($goods->stock throw new Exception('库存不足');<br> }<br> $goods->decrement('stock');<br> // 或用原生 update 避免模型事件干扰<br> // Goods::where('id', $id)->update(['stock' => DB::raw('stock - 1')]);<br>});
WHERE 条件必须命中索引,否则会升级为表锁
lockForUpdate 的行锁能力完全依赖 MySQL 的索引机制。如果查询条件没走索引(例如用 name 模糊查、或字段无索引),InnoDB 会退化为临键锁甚至全表锁,高并发下所有请求串行排队,吞吐量归零。
务必检查执行计划:
- 主键或唯一索引查询(如
where id = ?)→ 精确行锁 ✅ - 普通索引 + 等值查询(如
where sku = ?,且sku有唯一索引)→ 行锁 ✅ - 范围查询(如
where stock > 0)→ 间隙锁/临键锁,可能锁住多行 ❗ - 无索引字段(如
where remark like '%abc%')→ 全表锁 ⚠️
上线前用 EXPLAIN SELECT * FROM goods WHERE id = 123 FOR UPDATE; 确认 key 字段非 NULL。
事务内禁止耗时操作,锁持有时间越短越好
锁从 lockForUpdate() 执行开始,到事务 commit 才释放。期间任何延迟都会让后续请求卡住。
以下操作必须移出事务:
- HTTP 请求(调第三方 API、支付回调通知)
- 文件读写、图片处理、日志写入(除非异步队列)
-
sleep()、循环计算、复杂数组处理 - Eloquent 模型的访问器(accessor)、修改器(mutator)或事件(如
updating)若含数据库查询或远程调用
推荐策略:事务内只做「查锁 → 判库存 → 原子更新」三步;其他动作(如记录日志、发消息、通知用户)放到事务外或通过队列异步执行。
避免死锁:多表操作按固定顺序加锁
库存扣减常伴随订单创建、优惠券核销等,若多个请求以不同顺序锁定商品表和订单表,极易触发死锁。
统一约定加锁顺序:
- 先锁商品(
goods),再锁优惠券(coupons),最后锁用户账户(accounts) - 所有服务模块严格遵守该顺序,不可颠倒
- 单次事务尽量只操作一张表;跨表必须拆成原子步骤并加锁对齐
MySQL 检测到死锁会自动回滚其中一方事务,Laravel 默认重试 0 次,建议手动捕获异常并有限重试(如最多 2 次),避免前端无限等待。











