乐观锁必须使用整数类型的version字段,yii自动在save()中添加where version=条件,失败返回false并报“optimistic lock exception”,需捕获重试;禁用updateall(),避免手动修改version或未刷新数据直接save。

乐观锁必须加 version 字段且设为整数类型
Yii 模型的乐观锁机制完全依赖数据库中一个名为 version 的整数字段(默认名,可改但不推荐),它不是靠时间戳或 UUID。如果字段不存在、类型是 VARCHAR 或 TIMESTAMP,updateCounters() 和带条件的 save() 都会静默失效——查不到报错,但并发更新时数据就直接覆盖了。
实操建议:
-
version字段必须在数据库表里建好,类型为INT UNSIGNED(MySQL)或INTEGER(PostgreSQL),初始值设为0 - 模型类里要显式启用乐观锁:
public $optimisticLock = 'version';(默认就是这个值,但写出来更明确) - 别用
updateAll()直接改数据,它绕过乐观锁校验;要用updateCounters(['version' => 1])或带条件的save()
save() 时自动追加 WHERE version = ? 条件
只要模型启用了乐观锁,调用 save() 时 Yii 会自动在 SQL 的 WHERE 子句里加上 version = 当前读取到的值。如果数据库里该行的 version 已被别人改过,SQL 影响行数就是 0,save() 返回 false,且 $model->getErrors() 里会有 "Optimistic lock exception."。
常见错误现象:
- 没检查
save()返回值,以为更新成功,实际没生效 - 手动改了
$model->version再save(),导致条件恒不成立(比如从 5 改成 6,但 DB 里已是 7) - 用
load()后没重新find()就直接save(),version值早已过期
正确做法是捕获失败并重试:
$model = Post::findOne($id);
while (!$model->save()) {
if ($model->hasError('version')) {
$model = Post::findOne($id); // 重新读最新数据
// 可选:合并用户修改、提示冲突等
} else {
break; // 其他验证错误,不用重试
}
}
自定义字段名或多个锁字段?别折腾
Yii 的乐观锁只支持单个整数字段,且硬编码了递增逻辑(version + 1)。想用 updated_at、revision 或组合字段(如 version + status)来实现“类乐观锁”,本质上已脱离 Yii 原生机制——你得自己写 updateCounters()、自己拼 WHERE、自己处理失败,等于重造轮子。
参数差异和风险:
- 改
$optimisticLock只能换字段名,不能换逻辑;字段仍需是整数,仍会自动 +1 - 如果业务真需要多维度校验(比如“只有草稿状态才能编辑”),应该用
beforeSave()+ 自定义查询判断,而不是塞进乐观锁 - 高并发下,频繁重试可能引发雪崩;此时应考虑队列或应用层加锁,而非强依赖 DB 的
version字段
测试时容易漏掉并发场景,本地跑不出问题
本地开发用单进程 Web 服务器(如 Yii 内置的 php yii serve),两个请求基本不会真并发执行,version 冲突很难复现。结果是代码上线后,在 Nginx + PHP-FPM 多 worker 环境下突然大量更新失败,日志里全是乐观锁异常。
实操建议:
- 写单元测试时,用
fork()或并发 HTTP 请求(如ab -n 10 -c 2)模拟两个线程同时读-改-存 - 日志里别只记
save() failed,要明确打出当前$model->version和 DB 实际值(可通过SELECT version FROM xxx WHERE id = ?单独查) - 监控报警可以盯
Optimistic lock exception出现频次,持续升高说明业务逻辑有串行化瓶颈,不是锁本身的问题
真正难的不是加 version 字段,而是判断哪里该用、哪里不该用——比如用户头像上传这种无状态操作,加锁纯属增加负担;而订单支付状态流转,漏掉一次就可能双花。










