Yii2中forUpdate()必须配合事务才生效,否则数据库不执行SELECT...FOR UPDATE;正确用法是用transaction()包裹Query::forUpdate()->one()或AR::find()->forUpdate()->one(),并确保异常时rollBack()。

Yii2 中 forUpdate() 必须配合事务,否则无效
直接调用 forUpdate() 但没开启事务,锁根本不会生效——数据库层面压根不执行 SELECT ... FOR UPDATE。Yii 的 Query Builder 默认是只读查询,forUpdate() 只是“标记”,真正触发加锁靠的是事务提交前的执行时机。
实操建议:
- 必须用
Yii::$app->db->transaction()包裹整个操作,不能只在查询时加forUpdate() - 事务内第一次执行带
forUpdate()的createCommand()->queryOne()或ActiveRecord::findOne()才会真正发锁语句 - 如果事务里先做了其他查询(比如无锁
SELECT),再调forUpdate(),锁依然只在后者执行时加,不影响前面的读
ActiveRecord 方式下 forUpdate() 的写法和常见翻车点
很多人以为 User::findOne(['id' => 1])->forUpdate() 就行了,其实这是错的:AR 实例上的 forUpdate() 是链式调用的“构建器方法”,它不修改当前实例,而是返回一个新 Query 对象,原 findOne() 已经查完数据、没锁。
正确写法只有两种:
- 用 Query 构建器:
(new \yii\db\Query())->from('user')->where(['id' => 1])->forUpdate()->one() - 用 AR 的静态
find():User::find()->where(['id' => 1])->forUpdate()->one()—— 注意是User::find()开始,不是User::findOne() - 别在
save()前临时加锁:AR 的save()默认走 INSERT/UPDATE,不复用之前查出的锁;要更新,得在同一个事务里查+改
MySQL 和 PostgreSQL 对 FOR UPDATE 的行为差异
Yii 把 forUpdate() 编译成原生 SQL,但底层数据库实现不同,直接影响锁范围和阻塞逻辑。
关键区别:
- MySQL(InnoDB):
SELECT ... FOR UPDATE在可重复读隔离级别下会对扫描到的索引记录加 next-key 锁(行锁 + 间隙锁),可能锁住不存在的“幻行” - PostgreSQL:默认只锁命中的行,不锁间隙;想锁范围得显式用
SELECT ... FOR UPDATE OF table_name或配合SKIP LOCKED - 如果查询没走索引,MySQL 会升级为表级锁(全表扫描),PG 则可能锁大量无关行——务必确认执行计划里用了索引
并发更新时漏掉事务回滚导致死锁或连接卡死
加了 forUpdate() 后,如果业务逻辑抛异常但没手动 $transaction->rollBack(),事务不会自动释放,连接池里的连接会被占着,后续请求可能卡在 acquire connection 或直接超时。
安全写法必须带 try/catch:
$transaction = Yii::$app->db->beginTransaction();
try {
$row = User::find()->where(['id' => $id])->forUpdate()->one();
$row->balance += 100;
$row->save(false);
$transaction->commit();
} catch (\Exception $e) {
$transaction->rollBack(); // 这行不能省
throw $e;
}
更隐蔽的问题是:锁等待超时时间由 MySQL 的 innodb_lock_wait_timeout 控制(默认 50 秒),不是 PHP 脚本超时;一旦卡住,日志里看到的可能是 SQLSTATE[HY000]: General error: 1205 Deadlock found when trying to get lock 或 SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded,而不是 PHP 报错。
这事关连接生命周期,不是“锁住了就完了”,而是“没释放就坏了”。










