多表写入必须用事务包裹,涉及两张及以上表的增删改操作需用事务保证数据一致性;ci3中推荐用trans_begin()、trans_commit()/trans_rollback()显式控制,并避免嵌套、长事务及事务中重定向。

多表写入必须用事务包裹
只要一次业务涉及两张及以上表的 insert、update 或 delete,就必须用事务。比如用户注册时要同时写 users 表和 user_profiles 表,中间任一失败,另一张表不能留脏数据。
CI3 中最简写法是:$this->db->trans_start() 开头,$this->db->trans_complete() 结尾,中间放所有 DB 操作。框架会自动判断是否出错并回滚——前提是所有操作走的是同一个 $this->db 实例,且数据库引擎是 InnoDB。
- MyISAM 不支持事务,开了也白开
- 跨模型调用时,若各自 new 了一个 DB 实例,事务会失效
-
trans_complete()必须执行到,否则事务状态卡住,可能影响后续请求
有 PHP 异常风险的操作必须手动回滚
CI3 的 trans_complete() 只捕获 SQL 错误(如字段不存在、外键冲突),不捕获 PHP 层异常(如 file_get_contents() 失败、数组越界)。一旦这类异常发生,事务不会自动回滚,已执行的 SQL 会意外提交。
正确做法是显式控制流程:
- 用
$this->db->trans_begin()替代trans_start() - 所有 DB 操作后,检查逻辑条件(如
if (!$result))或用try/catch - 成功则
$this->db->trans_commit(),失败则$this->db->trans_rollback()
典型场景:转账前先校验余额,这个校验是 PHP 逻辑,失败就得主动回滚,不能等 trans_complete()。
读操作不需要事务,但“读-改-写”要小心
单纯 get() 或 get_where() 不需要事务。但如果你先 get() 查出金额,再 PHP 计算新值,最后 update(),这就构成“读-改-写”,并发下可能超扣款。
这时要么用 SELECT ... FOR UPDATE(需手写原生查询 + 事务包裹),要么改用原子 SQL,例如:
$this->db->query("UPDATE accounts SET balance = balance - 100 WHERE id = 1 AND balance >= 100");
注意:WHERE balance >= 100 是关键,它把校验移到数据库层,避免 PHP 层竞争。
事务不是万能锁,嵌套和长事务要避开
CI3 不支持嵌套事务,trans_start() 嵌套调用会静默失效。想在子函数里“假装”开启事务,实际只是共用外层上下文。
长事务(比如含文件上传、HTTP 请求)极易拖垮数据库连接池,还可能锁表太久。真正该放事务里的,只限纯粹的数据库语句;外部 I/O、API 调用、耗时计算一律剥离出去,放到事务之外。
最容易被忽略的一点:事务期间不能 redirect() 或提前输出响应,否则 trans_complete() 可能无法执行,导致连接未释放、事务悬停。











