乐观锁靠version字段+where version=?实现,不阻塞;悲观锁靠select...for update加行级排他锁,会阻塞但需事务开启、索引命中、隔离级别匹配,否则静默失效。

直接说结论:乐观锁靠 version 字段 + WHERE version = ? 条件更新实现,不阻塞;悲观锁靠 SELECT ... FOR UPDATE 在事务中加行级排他锁,会真实阻塞其他写操作——但前提是事务开启、索引命中、隔离级别匹配,否则大概率静默失效。
为什么 selectForUpdate() 有时不阻塞?
常见错误现象:SQLSTATE[HY000]: General error: 1205 Deadlock found when trying to get lock 或者完全没锁住,两个请求同时扣减成功。
- 脱离
DB::transaction()单独调用lockForUpdate(),MySQL 会立即提交语句,锁在语句结束就释放,起不到保护作用 -
WHERE条件未命中索引(比如查name但没建索引),InnoDB 会升级为表锁或间隙锁,不仅性能差,还可能锁住不该锁的行 - MySQL 默认
AUTOCOMMIT=1,即使写了DB::transaction(),若底层 PDO 没关自动提交(PDO::ATTR_AUTOCOMMIT => false),事务仍不生效 - 可重复读(RR)隔离级别下,
SELECT ... FOR UPDATE默认加临键锁(Next-Key Lock),可能锁住“幻行”,需确认业务是否真需要这种强度
version 字段更新为何不能用 $model->save()?
典型误用:$user->version++; $user->save(); —— 这是两步:先读 version=1,再发 UPDATE version=2,中间有竞态窗口。
- 必须把读取旧值、条件校验、更新动作压进**单条 SQL**,例如:
User::where('id', 123)->where('version', $oldVersion)->update(['balance' => DB::raw('balance - 100'), 'version' => DB::raw('version + 1')]) - 检查返回值:
$affected === 0表示更新失败(别人抢先改了),此时应重试或抛异常,不能忽略 -
increment()是原子的,但只适用于纯数值累加(如点击计数),无法带业务逻辑做“读-改-写” - 字段名必须显式声明为
version(或自定义名),Laravel 不会自动识别,需在 Model 中重写optimisticLock()方法返回字段名
sharedLock() 和 lockForUpdate() 到底差在哪?
关键区别不在“能不能读”,而在“别人能不能写”:
-
sharedLock()等价于SELECT ... LOCK IN SHARE MODE:允许并发读,但会阻塞其他事务的FOR UPDATE和UPDATE,**不防丢失更新**(两个事务都能读到同一版本,都去 update,后一个覆盖前一个) -
lockForUpdate()加的是排他锁(X Lock):不仅阻塞写,也阻塞其他事务的锁定性读(如另一个FOR UPDATE),能真正串行化“读-改-写”流程 - 二者都依赖事务包裹,且只对当前查询命中的行生效;若
WHERE条件为空或全表扫描,sharedLock()可能锁整张表 - 不要指望
sharedLock()实现乐观锁效果——它不是乐观锁,也不提供版本校验能力
最易被忽略的一点:乐观锁的 version 字段必须参与每次更新的 WHERE 条件,且更新语句里要同步递增它;悲观锁的事务边界必须严格覆盖从 SELECT ... FOR UPDATE 到最终 UPDATE 的全过程,少一个环节,锁就形同虚设。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











