Yii2事务必须显式调用commit()或rollBack(),否则锁长期持有易致死锁;更新前须用FOR UPDATE显式加锁,避免并发覆盖;事务内禁用非DB操作,批量更新应使用单条SQL。

必须在事务中显式调用 commit() 或 rollBack(),否则事务不会结束,锁可能长期持有,甚至导致死锁或连接耗尽。
事务开启与结束必须成对出现
Yii2 的事务不是自动提交的,beginTransaction() 只是标记一个起点,后续所有数据库操作都绑定到该事务上下文。一旦开启,就必须手动控制终点。
- 未
commit():事务处于挂起状态,连接不释放,行锁(如FOR UPDATE)持续占用,其他事务会阻塞等待 - 未
rollBack()且发生异常:PHP 脚本终止后连接可能被回收,但 MySQL 侧事务未必及时回滚,尤其在长连接池场景下易残留锁 - 忘记
try/catch包裹:哪怕只有一行更新失败,整个事务逻辑就中断,但没回滚 → 数据库状态半途而废
更新前加锁必须用 FOR UPDATE 显式写法
ActiveRecord 的 find()->one() 默认不加锁,即使在事务里也只读。要防止并发覆盖,必须在 SQL 层面声明排他锁。
- 错误写法:
User::find()->where(['id' => 5])->one()→ 无锁,高并发下读到旧值后更新,覆盖他人修改 - 正确写法:
User::findBySql('SELECT * FROM '.User::tableName().' WHERE id = :id FOR UPDATE', [':id' => 5])->one() - 注意:
FOR UPDATE只在 InnoDB 有效;若查询条件不命中索引(如无主键、无索引字段),会升级为表锁,严重拖慢整体吞吐 - 别用
updateAll()替代:它绕过模型层,无法配合FOR UPDATE锁定后再更新,属于“先更新后校验”,非“先锁再判再更”
避免在事务中做非数据库操作
事务生命周期内应只做 DB 相关动作。任何外部调用(HTTP 请求、文件写入、缓存更新、日志落盘)都可能延长事务时间,放大锁竞争风险。
- 例如:查出用户余额 → 调用支付网关回调接口 → 再扣减余额 →
commit():网关响应慢 2 秒,这 2 秒整张表(或行)都被锁住 - 正确顺序:锁 → 读 → 计算 → 更新 →
commit()→ 再触发外部动作(如发消息队列、异步写缓存) - 事务内不要调用
Yii::info()或file_put_contents():日志/文件 I/O 延迟不可控,且失败可能导致事务逻辑误判
批量更新务必用原生 SQL 或 createCommand()->update()
用 foreach + $model->update() 是典型反模式:每条记录触发一次 SQL,N 条就是 N 次 round-trip,还可能因中间失败导致部分更新成功、部分失败。
- 安全做法:用单条 SQL 完成全部更新,例如
UPDATE order SET status = 2 WHERE user_id = 123 AND status = 1 - Yii2 推荐:
Yii::$app->db->createCommand()->update('order', ['status' => 2], ['user_id' => 123, 'status' => 1])->execute() - 如果条件复杂(如按子查询结果更新),直接走
createCommand()->execute('UPDATE ...'),比拼 ActiveQuery 更可控 - 注意:批量更新前若需校验数据一致性(比如库存是否充足),仍得先
SELECT ... FOR UPDATE锁定关键行,再执行UPDATE
最容易被忽略的是锁的粒度和生命周期——FOR UPDATE 不是“执行完 update 就释放”,而是直到事务 commit 或 rollBack 才释放。哪怕你只更新了 1 行,只要事务没结束,这行就一直被锁着。


















