
乐观锁:用 version 字段检测并发修改
乐观锁不真正加锁,而是靠数据版本控制来识别冲突。Laravel 本身不内置乐观锁机制,但很容易自己实现:在模型中加一个 version(或 lock_version)字段,每次更新时检查当前值是否匹配。
常见错误是直接写 where('version', $oldVersion) 却没检查 affectedRows === 0,导致静默失败。
- 更新前先查出当前
version值,比如$post = Post::find(123) - 更新时带上条件:
Post::where('id', 123)->where('version', $post->version)->update(['title' => 'new', 'version' => $post->version + 1]) - 如果返回
0,说明已被他人抢先更新,需重试或报错 - 务必把
version字段加到模型的$fillable或$casts中,避免被批量赋值过滤
悲观锁:用 sharedLock() 和 lockForUpdate() 阻塞式加锁
Laravel 的 sharedLock() 对应 SQL 的 SELECT ... LOCK IN SHARE MODE,lockForUpdate() 对应 SELECT ... FOR UPDATE,它们依赖数据库事务和行级锁支持(MySQL InnoDB 有效,SQLite 不支持)。
典型误用是没包裹在事务里——单独调用 lockForUpdate() 在自动提交模式下会立刻释放锁,完全无效。
- 必须用
DB::transaction()包裹查询+更新逻辑 -
lockForUpdate()适合「读取后修改」场景,比如扣库存:Stock::where('product_id', 1)->lockForUpdate()->first() -
sharedLock()适合只读校验(如检查余额是否充足),允许多个读,但阻塞写 - 注意死锁风险:多个事务按不同顺序加锁同一组记录时容易触发,建议固定加锁顺序(如按 ID 升序)
什么时候该选乐观锁,什么时候必须用悲观锁?
没有银弹。乐观锁适合冲突概率低、重试成本小的场景(如文章点赞数、用户资料更新);悲观锁适合冲突高、业务不允许出错、且操作链路短的场景(如支付扣款、秒杀库存扣除)。
容易踩的坑是拿乐观锁硬扛高并发扣减——重试次数一多,响应延迟飙升,还可能引发雪崩。
- 乐观锁适用:后台管理类更新、非核心计数器、用户偏好设置
- 悲观锁适用:资金账户变动、订单状态机推进、库存预占
- 混合用法也常见:先用
lockForUpdate()查库存,再用乐观锁更新订单状态,分层控制粒度 - 别忘了设置事务超时:
DB::transaction(..., 10)防止锁等待太久拖垮连接池
MySQL 行锁生效的前提你可能忽略了
哪怕写了 lockForUpdate(),如果查询没走索引,InnoDB 会升级为表锁,整个表被堵住——这是线上最隐蔽的性能杀手之一。
验证方法很简单:执行 EXPLAIN SELECT ... FOR UPDATE,看 key 列是否显示用了索引,type 是否为 range 或 ref,而不是 ALL。
- 确保
WHERE条件字段有单列索引或复合索引前缀 - 避免在锁查询中用函数或隐式类型转换,比如
where('created_at', date('Y-m-d'))会让索引失效 - 批量加锁慎用:
whereIn('id', [...])如果 ID 数量大,可能锁住大量无关行,考虑分批或改用乐观锁 - 注意间隙锁(Gap Lock):范围查询如
where id > 100会锁住“100 之后的空隙”,防止幻读,但也可能意外阻塞插入










