codeigniter 4 事务失败主因有四:未执行查询导致事务未真正开启;ddl语句引发隐式提交;未手动调用transrollback()或时机错误;模型使用独立数据库实例绕过事务。

事务没开启或被隐式提交了
CodeIgniter 4 的 $db->transStart() 不是“自动开启事务”,它只是标记事务块起点;真正开启依赖后续第一个查询执行。如果 transStart() 后没执行任何数据库操作(比如只写了日志、做了计算),transRollback() 就无事可滚。
更常见的是隐式提交:执行 CREATE、ALTER、DROP 或 TRUNCATE 时,MySQL 会立刻提交当前事务,后续调用 transRollback() 对已提交部分完全无效。
- 检查事务块内是否混入 DDL 语句 —— 即使是
DB::table('xxx')->truncate()也会触发隐式提交 - 确认未启用自动提交:
$db->transOff()不能代替$db->transStart(),必须显式调用前者关闭自动提交,再调用后者开启事务 - 避免在事务中调用模型的
save()或insertBatch()等方法前未检查其内部是否含 DDL(例如某些迁移辅助方法)
rollback() 被跳过或执行时机不对
CI4 的事务回滚不是“发生异常就自动触发”,它完全依赖你手动调用 $db->transRollback()。如果你在 try/catch 中捕获了异常但忘了写这行,或者把它写在了 transComplete() 之后,回滚就永远不会发生。
另外,transComplete() 是个“智能提交/回滚”封装:它内部会检查 transStatus() 返回值,仅当为 true 才提交,否则回滚 —— 但它**不会抛出异常**,也不会中断流程。所以你可能误以为“事务失败了”,其实只是 transComplete() 默默回滚并继续往下走。
- 不要只依赖
transComplete(),关键路径建议显式判断:if (! $db->transStatus()) { $db->transRollback(); } - 确保
transRollback()在异常处理分支中被调用,且不在事务已结束(如已调用transComplete())后重复调用 - 注意控制器里多个数据库实例(如
\Config\Database::connect('second'))—— 回滚必须在对应实例上调用,跨实例不生效
模型方法绕过了事务上下文
CI4 模型默认使用独立数据库连接实例($this->db),而你在控制器里用 \Config\Database::connect() 获取的 $db 是另一个实例。两者事务状态互不影响。这意味着你在控制器开启事务,再调用模型的 update(),该更新很可能走的是模型自己的连接,不在事务范围内。
这个问题在使用多数据库配置或自定义模型构造时尤其隐蔽。即使你把模型设为 protected $DBGroup = 'default',也不能保证它复用控制器中开启事务的那个连接对象。
- 统一使用同一个数据库实例:在控制器中获取
$db,然后传给模型方法,或通过依赖注入方式让模型接收该实例 - 避免在模型中直接调用
$this->db->transStart()—— 容易和外层事务冲突,也违反事务边界清晰原则 - 检查模型是否启用了
protected $useSoftDeletes = true:软删除的delete()实际执行UPDATE,若该字段有触发器或约束,可能引发意外提交
连接断开或超时导致 rollback 失效
事务回滚本质是向数据库发送一条 ROLLBACK 命令。如果在调用 transRollback() 前连接已断开(如 MySQL wait_timeout 触发、网络闪断、连接池回收),命令发不出去,事务就卡在“未提交未回滚”状态,下次同连接复用时可能报错或行为异常。
这种问题在长事务、高延迟网络、或使用连接池(如 PDO 的 persistent 连接)时更容易出现。CI4 默认不校验连接活性,transRollback() 失败时也只返回 false,不抛异常。
- 在调用
transRollback()前加简单探测:if (! $db->connID) { $db->initialize(); } - 缩短 MySQL 的
wait_timeout(如设为 60 秒),配合应用层心跳检测,比依赖超长连接更可靠 - 避免在事务中做耗时操作(如文件读写、HTTP 请求)—— 这些不参与事务控制,却拉长事务持有时间,增加连接失效风险
最常被忽略的一点:CI4 的 transStatus() 返回 null 并不总代表失败,它也可能表示“事务尚未开始”或“连接不可用”。别只看真假值,得结合 $db->transOff() 状态、$db->connID 是否有效、以及 MySQL 的 SELECT @@in_transaction 结果交叉验证。











