Laravel 无内置乐观锁,需手动用 version 字段实现原子更新:where('version', $old)->update(['version' => DB::raw('version + 1')]),返回0则冲突;updated_at 可用但精度低、易误判;$model->save() 因非原子操作而失效。

直接说结论:Laravel 没有内置的乐观锁支持,必须靠手动加 version 或 updated_at 字段 + 带条件的 update() 实现;用错方式(比如先 $model->version++ 再 save())会彻底失效。
怎么用 version 字段实现真正的乐观锁
核心是“一次原子更新 + 版本校验”,不能拆成两步读写。常见错误是把版本递增和保存分开,导致两个请求读到相同 version,都写入 version = 2,丢失一次更新。
- 迁移中添加字段:
$table->unsignedInteger('version')->default(1),并加索引(如$table->index('version')) - 读取时记下当前
version:$user = User::find(1); $expectedVersion = $user->version; - 更新必须用
where()校验版本,并在 SQL 中原子递增:User::where('id', 1)->where('version', $expectedVersion)->update(['name' => 'John', 'version' => DB::raw('version + 1')]) - 检查返回值:
if ($affected === 0) { throw new \Exception('数据已被修改'); }
updated_at 能当乐观锁用吗
可以,但风险更高:它不是专为并发设计的字段,updated_at 可能被其他无关更新触发变更,导致误判失败;且 PHP 时间精度(秒级)在高并发下容易撞车。
- 只适用于低频、非关键业务(如用户偏好设置)
- 必须确保模型启用了时间戳:
public $timestamps = true; - 更新前保存原始值:
$originalUpdatedAt = $user->updated_at; - 更新语句要严格匹配:
User::where('id', $user->id)->where('updated_at', $originalUpdatedAt)->update([...]) - 注意 MySQL 的
datetime精度(微秒需显式配置),否则where updated_at = ?可能因截断不匹配
为什么不能直接用 $model->save() 配合 version
因为 $model->save() 是两阶段操作:先 SELECT 出整行,再拼 UPDATE 语句——中间存在时间窗口,且 Eloquent 不会自动把旧 version 塞进 WHERE 条件里。你看到的“版本没变”只是 PHP 层面的错觉。
-
$user->version++和$user->save()是分离的,不是数据库原子操作 - 即使你重写了
save(),Eloquent 默认的update()也不带where version = ?条件 - 想走模型方法,必须自己封装逻辑(如 Trait),并在
update()前手动注入where('version', $oldVersion) - 更稳妥的做法是绕过模型,用
DB::table('users')->where(...)->update(...)控制 SQL 细节
什么时候该放弃乐观锁
当冲突概率明显上升(比如每秒几十次写同一条记录)、或业务要求强一致性(如资金扣减、库存预占)时,乐观锁的重试成本会急剧升高,响应延迟不可控,甚至引发雪崩。
- 典型信号:日志里频繁出现
OptimisticLockException或重试超过 3 次 - 此时应切换为
lockForUpdate()悲观锁,但必须包裹在DB::transaction()中,且WHERE条件命中索引 - 还有一种折中:用原生
UPDATE ... WHERE balance >= ?做原子校验扣减,不依赖锁也不依赖版本,但无法处理复杂业务逻辑
真正容易被忽略的是:乐观锁的“乐观”建立在低冲突假设上,一旦实际场景偏离这个前提,它就从省资源变成拖后腿。上线前务必用压测模拟真实并发密度,别只靠本地单线程验证逻辑通不通。


















