悲观锁在 Laravel 中生效需满足三条件:事务包裹、WHERE 走索引、锁操作位于事务最前端;否则无效,易致并发覆盖。

悲观锁在 Laravel 中不是“加个方法就完事”,它要真正起作用,必须满足三个硬性条件:在事务中、WHERE 条件命中索引、锁操作位于事务最前端。缺一不可,否则看起来加了锁,实际等于没锁。
必须用 DB::transaction() 包裹整个流程
单独写 $product = Product::where('id', 123)->lockForUpdate()->first(); 是无效的——查询一返回,锁就释放了,后续的 $product->save() 完全不受保护。并发请求仍会读到旧值并覆盖写入。
- ✅ 正确做法:把读、改、写全部放进事务闭包里
- ✅ 锁必须在事务开启后第一时间执行,不能先查再进事务
- ❌ 不要用
Product::find(123)->lockForUpdate(),因为find()已执行完 SQL,锁无法回溯生效
WHERE 条件必须走索引,否则行锁变表锁
MySQL 的 InnoDB 在无法用索引定位行时,会升级为全表扫描 + 表级锁。一个请求锁住整张表,所有其他写请求都会排队卡死。
- 用
EXPLAIN SELECT * FROM products WHERE user_id = 456 FOR UPDATE;检查执行计划 - 看
key列是否非 NULL,type是否为const或ref;如果是ALL,说明没走索引 - 主键或唯一索引最稳;普通字段(如
status)必须显式建索引,否则where('status', 'pending')就是隐患
推荐显式写法,避开 Eloquent 隐式行为
Eloquent 的 lockForUpdate() 看似方便,但可能因隐式关联加载(如 ->with('user'))、访问器、模型事件等触发额外查询,延长锁持有时间,增加死锁风险。
- ✅ 推荐:
DB::select('SELECT * FROM products WHERE id = ? FOR UPDATE', [$id]) - ✅ 手动控制 SQL,避免 Eloquent 的中间层干扰
- ✅ 后续更新直接用
DB::update()原生语句,不走模型生命周期
锁不是万能解药,得看场景选对策略
悲观锁适合“必须先读再改、且逻辑复杂”的场景,比如校验多表状态、调用外部服务、生成唯一单号。但如果只是简单扣库存,原子 SQL 更轻量、更安全。
- ✅ 简单条件更新(如库存 ≥1 才扣减):优先用
DB::update('UPDATE ... WHERE id = ? AND stock >= 1', [...]) - ✅ 影响行数为 0 时直接判定失败,无需重试或异常捕获
- ❌ 不要用
$model->stock--;$model->save(),这是典型的竞态温床


















