lockforupdate() 必须在db::transaction()闭包内调用且where条件必须命中索引,否则行锁失效、升级为表锁或完全不生效;单独调用会因自动提交导致锁立即释放,无法保护后续写操作。

直接说结论:lockForUpdate() 必须在 DB::transaction() 闭包内调用,且 WHERE 条件必须命中索引,否则它不锁行、只锁表,甚至完全失效。
lockForUpdate() 必须和 DB::transaction() 一起用
单独写 Order::where('id', 123)->lockForUpdate()->first(),锁在查询返回后立刻释放——后续的 $order->save() 完全不受保护,并发请求仍会读到旧值并覆盖写入。
真正起作用的只有包裹在事务里的完整读-改-写流程:
DB::transaction(function () { $order = Order::where('id', 123)->lockForUpdate()->first(); $order->status = 'shipped'; $order->save(); });- 事务未提交前,其他事务对同一行执行
lockForUpdate()或sharedLock()会被阻塞 - 如果业务逻辑里有多个模型操作(如扣库存+建订单),整个流程都得塞进同一个事务闭包
WHERE 条件不走索引 → 行锁变表锁
哪怕加了 lockForUpdate(),只要 MySQL 没法用索引定位具体行,InnoDB 就会升级为全表扫描 + 表级锁。线上最容易被忽略的性能雪球就从这儿滚起来。
验证方法很简单:
- 执行
EXPLAIN SELECT * FROM orders WHERE user_id = 456 FOR UPDATE; - 看输出中
key列是否非 NULL,type是否为const/ref;如果是ALL,说明没走索引 - 主键或唯一索引最稳;普通字段(如
status)必须显式加索引,否则where('status', 'pending')就是定时炸弹
sharedLock() 和 lockForUpdate() 的区别不是“读 vs 写”,而是“谁被阻塞”
sharedLock() 加的是共享锁(S 锁),允许其他事务并发读,但会阻塞其他事务的 lockForUpdate() 和写操作;lockForUpdate() 加的是排他锁(X 锁),连其他事务的 sharedLock() 都会等。
典型误用场景:
- 用
sharedLock()做库存预占 —— 两个事务都能读到相同库存值,最终都扣减成功,超卖 - 用
lockForUpdate()查大量 pending 订单再逐条处理 —— 锁住几百行,后续所有更新都排队,吞吐骤降 - 正确做法:优先用主键精确查单行;批量场景改用
DB::update('UPDATE ... WHERE id = ? AND stock >= 1')这类原子语句
死锁不是小概率事件,而是锁顺序不一致的必然结果
两个事务分别按不同顺序加锁,比如事务 A 先锁 user_id=100 再锁 user_id=200,事务 B 反过来先锁 200 再锁 100,MySQL 检测到循环等待就会抛 SQLSTATE[HY000]: General error: 1205 Deadlock found when trying to get lock。
规避方式很实际:
- 所有涉及多行锁定的业务,强制按主键升序排列条件:用
whereIn('id', [200, 100])前先sort()数组 - 事务超时时间要设短:
DB::transaction(..., 10),避免一个慢查询拖垮整个连接池 - 缓存锁(如
cache()->lock('stock_lock:'.$id))可做前置拦截,但它和数据库锁不是替代关系,漏掉任意一层都可能出问题
最常被跳过的动作是验证索引有效性,以及把锁当成“万能胶”贴在任意查询上——其实它只在精确命中索引的短事务里才可靠。一旦 WHERE 条件模糊、事务过长、或锁范围失控,悲观锁反而成了系统瓶颈的源头。











