lockForUpdate() 必须配合事务使用,且WHERE条件需走索引、锁范围要小;单独调用、无索引或大范围锁会导致失效或表锁。

lockForUpdate() 不是万能的,不配事务、不走索引、不控范围,它就等于没锁。
lockForUpdate() 必须和 DB::transaction() 一起用
单独调用 lockForUpdate() 后立刻执行 first(),锁在查询返回后就释放了——根本拦不住并发写。MySQL 的 SELECT ... FOR UPDATE 只在事务内持续生效。
- ✅ 正确写法:
DB::transaction(function () { $order = Order::where('id', 123)->lockForUpdate()->first(); ... }); - ❌ 错误写法:
$order = Order::find(123)->lockForUpdate();(find()已执行完查询,lockForUpdate()无效) - ⚠️ 注意:Eloquent 的
first()、get()等方法内部会立即执行 SQL;锁必须在查询语句生成阶段就声明,不能“事后补锁”
WHERE 条件不走索引 → 行锁变表锁
哪怕写了 lockForUpdate(),如果 WHERE 匹配不到索引(比如用 LIKE '%abc'、或字段没建索引),InnoDB 会升级为全表扫描 + 表级锁,整个表被堵死。
- 验证方式:
EXPLAIN SELECT * FROM orders WHERE user_id = 456 FOR UPDATE;,看key列是否非NULL,type是否为const/ref,不是ALL - 主键或唯一索引字段最安全;普通字段务必加索引
- 避免在锁查询中触发 Eloquent 的隐式关联加载(如
->with('user')),可能引入额外 JOIN 和无索引扫描
锁粒度太大导致阻塞加剧
一个事务锁住 100 行,其他请求想更新其中任意一行都得等——这不是防冲突,是在制造瓶颈。
- 优先用主键精确查询:
where('id', $id),而不是where('status', 'pending') - 避免
lockForUpdate()配合get()批量锁行;真要批量处理,改用DB::update()原子语句或分块chunkById()+ 单行锁 - 扣库存这类场景,可先用原子 SQL 尝试更新:
DB::update('UPDATE products SET stock = stock - 1 WHERE id = ? AND stock >= 1', [$id]);,只影响 0 或 1 行,失败直接报错,省去锁开销
cache()->lock() 和 lockForUpdate() 不是替代关系
Redis 缓存锁(如 cache()->lock('stock_lock:123'))和数据库行锁解决的是不同层面的问题:前者跨进程协调,后者保证单次事务内数据一致性。混用时容易漏掉关键环节。
- 缓存锁 key 必须带业务标识,例如
'order_lock:123',不能写死成'order_lock' - 缓存锁过期时间(ttl)要大于事务最大耗时,否则锁自动释放后,另一个请求进来又读到旧值 —— 这就是“锁失效+读-改-写”双重风险
- 最稳的组合是:先用缓存锁做入口拦截(防大量请求同时进 DB),再在事务内用
lockForUpdate()做最终校验(防缓存延迟/穿透)
真正难的不是写对那一行 lockForUpdate(),而是确认 WHERE 走了索引、事务足够短、锁范围足够小——这三者漏掉任何一个,锁就从保险丝变成火药桶。


















