yii2的activerecord需手动实现乐观锁:添加version字段并声明为safe、用updateall带version条件更新、失败后主动重试并加事务保护。

Yii2 的 ActiveRecord(AR)本身不内置乐观锁支持,但可以通过手动加 version 字段 + 自定义更新逻辑实现。关键不是“自动拦截”,而是把版本校验逻辑显式写进业务层,失败后由你决定是否重试、重试几次、怎么重试。
1. 数据库与模型准备
在目标表中添加 version 字段(INT 类型,默认值 1):
ALTER TABLE `order_info` ADD COLUMN `version` INT NOT NULL DEFAULT 1 COMMENT '乐观锁版本号';在 AR 模型中声明字段并加入 rules() 白名单(否则 save() 会过滤掉):
- public $version;
- 在 rules() 中加上:['version', 'safe'] 或 ['version', 'integer', 'default' => 1]
- 确保数据库字段允许非空且有默认值,避免插入时因 NULL 导致 WHERE 条件失效
2. 更新时强制校验 version
不要直接调用 $model->save(),而是手写 SQL 或使用 updateAll() 带条件更新:
- 先查出当前数据和 version:
$model = Order::findOne($id); - 构造带 version 校验的更新语句:
Order::updateAll([
'status' => 'paid',
'version' => new \yii\db\Expression('version + 1')
], ['id' => $id, 'version' => $model->version]); - 检查返回值:
若返回 0 → 说明 version 不匹配,数据已被他人更新;
若返回 1 → 更新成功,可继续后续逻辑
3. 重试逻辑要主动控制
Yii2 不会自动重试,必须你自己封装循环或递归。推荐做法:
- 设定最大重试次数(如 3 次),避免死循环
- 每次重试前 sleep 短暂时间(如 10–50ms),缓解竞争压力
- 每次重试都重新查询最新数据(含新 version),再构造新 update
- 示例片段:
$maxRetries = 3;
for ($i = 0; $i $model = Order::findOne($id);
if (!$model) break;
$rows = Order::updateAll([...], ['id' => $id, 'version' => $model->version]);
if ($rows === 1) {
// 成功,跳出
break;
}
usleep(10000); // 10ms
}
if ($rows !== 1) {
throw new \Exception('并发更新失败,已重试 ' . $maxRetries . ' 次');
}
4. 注意 Yii2 特有的坑
这些点容易导致“看似重试了,实则没生效”:
-
rules() 忘加 version:即使 updateAll() 绕过了模型验证,但如果用的是
$model->attributes = [...]; $model->save(),version 会被静默丢弃 - 事务未包裹重试块:若重试期间其他操作已提交,可能造成状态不一致;建议把整个读-判-更流程放在一个事务里
- CLI 环境下 session 或行为干扰:某些自定义 behavior 可能在 beforeSave 中修改字段,导致 version 被覆盖或校验错位
-
没检查 getDirtyAttributes():调试时可用
var_dump($model->getDirtyAttributes())确认 version 是否真被纳入更新字段列表











