codeigniter事务死锁根本原因是数据库行锁竞争,非框架缺陷;典型场景为多事务以相反顺序更新同一组记录(如a→b与b→a转账),触发innodb循环等待。

CodeIgniter事务里为啥会死锁?
根本原因不是框架本身,而是你写的事务逻辑撞上了数据库的行锁竞争。InnoDB 行锁 + 多个事务交叉更新同一组记录(比如转账时 A→B 和 B→A 同时发生),就容易触发循环等待。CI 的 $this->db->trans_start() 和 $this->db->trans_complete() 只是包装,底层完全依赖 MySQL 的锁机制和事务调度。
常见诱因包括:
- 两个事务以相反顺序更新相同主键的行(如事务1先改
id=1再改id=2,事务2反之) - UPDATE 带范围条件(如
WHERE status = 'pending')且没走索引,导致锁住大量无关行 - 事务体过大、执行时间过长,持锁时间拉长,提高冲突概率
- 模型方法内部重新初始化 DB 实例,导致部分操作脱离当前事务上下文,锁状态不一致
怎么快速定位是不是死锁?
别猜,直接看 MySQL 的实时反馈。死锁发生后,被回滚的那个事务会抛出错误,但 CI 默认不透出具体原因。你需要主动查库:
在 MySQL 客户端执行:SHOW ENGINE INNODB STATUS\G
重点关注输出末尾的 LATEST DETECTED DEADLOCK 段——它会明确告诉你哪两条 SQL、哪个事务 ID、锁住了哪些行、为什么被选为牺牲者。这是唯一可信的死锁证据源。
补充手段:
- 开启 CI 查询日志:
$this->db->save_queries = TRUE;,在trans_complete()后立刻 dump$this->db->queries,确认 BEGIN/UPDATE/COMMIT/ROLLBACK 是否按预期发出 - 检查应用日志中是否出现
Deadlock found when trying to get lock或错误码1213
事务代码怎么写才不容易死锁?
核心原则:缩短锁持有时间 + 统一资源访问顺序。CI 层面能做的优化很实在:
- 所有涉及多行更新的操作,强制按主键升序排列。例如转账前对账户 ID 排序:
sort([$from_id, $to_id]),确保总是先操作小 ID,再操作大 ID - 把事务控制收拢到控制器或 service 类,避免在事务块内调用多个模型方法;若必须调用,统一传入当前
$this->db实例作为参数,防止模型内部新建连接 - 禁用自动事务管理:
$this->db->trans_off(),改用显式流程:$this->db->trans_begin()→ 执行 SQL → 成功则$this->db->trans_commit(),失败则$this->db->trans_rollback() - 避免在事务中做 HTTP 请求、文件读写、sleep() 等耗时操作
锁表检测和基础优化怎么做?
“锁表”在 InnoDB 里其实是误称——真正锁的是索引记录(行锁)。所谓“锁表”,往往是因为缺失索引导致全表扫描,进而锁住所有行。检测和优化要分两步走:
先查有没有隐式锁表行为:
- 执行
EXPLAIN SELECT ...或EXPLAIN UPDATE ...,看type是否为ALL(全表扫描)或key是否为NULL - 检查 WHERE 条件字段是否有对应索引,尤其是复合条件(如
WHERE status = ? AND created_at > ?)要考虑联合索引顺序
再做针对性优化:
- 给高频 WHERE、ORDER BY、JOIN 字段加索引,优先覆盖查询,减少回表和锁行数
- 避免在事务中使用
SELECT ... FOR UPDATE无必要地扩大锁范围;如只需校验余额,可用普通 SELECT + 乐观锁(version 字段)替代 - 调整 MySQL 配置(需 DBA 配合):
innodb_lock_wait_timeout缩短至 30 秒,让死锁失败更快暴露;innodb_deadlock_detect保持 ON(默认)
最易被忽略的一点:死锁不是孤立事件,而是并发模式暴露的问题。单测永远复现不了,必须在压测或线上流量下观察 SHOW ENGINE INNODB STATUS 的规律——高频死锁总集中在某几张表、某几类 SQL,盯住它们优化,比泛泛调参有效得多。










