ThinkPHP中数据库死锁本质是MySQL InnoDB高并发下事务争抢行锁/间隙锁导致的循环等待,需通过InnoDB状态日志定位、异常重试机制(错误码1213)、统一SQL执行顺序、缩小事务粒度、确保索引覆盖及乐观锁等手段系统性降低概率。

ThinkPHP 应用中出现数据库死锁,本质不是框架问题,而是底层 MySQL InnoDB 在高并发下多个事务争抢行锁、间隙锁时发生的循环等待。错误提示通常是 “Deadlock found when trying to get lock; try restarting transaction”(错误码 1213),伴随事务回滚、接口偶发失败。解决重点不在“彻底消灭”,而在于快速识别、稳定重试、系统性降低发生概率。
快速定位死锁源头
死锁日志是唯一可靠依据,ThinkPHP 自身不记录死锁细节,必须依赖 MySQL 原生能力:
- 登录数据库执行
SHOW ENGINE INNODB STATUS\G,查找 LATEST DETECTED DEADLOCK 区块,它会明确列出两个(或多个)冲突事务的 SQL、持有锁和等待锁的资源、事务 ID 和线程号 - 检查 ThinkPHP 日志(
runtime/log/)中对应时间点的报错堆栈,结合 SQL 语句与 InnoDB 日志中的 SQL 对照,锁定具体业务方法(如OrderService::pay()或StockModel::decrease()) - 若使用 MySQL 5.7+,可查
sys.innodb_lock_waits视图获取实时阻塞关系;配合information_schema.INNODB_TRX查看长时间未提交的事务
ThinkPHP 中必须实现的重试机制
PDO 默认不抛异常,需主动开启并捕获死锁错误。在事务操作中不能靠前端刷新或 Nginx 重发——事务已中断,连接不可复用:
- 确保数据库配置启用异常模式:
'pdo_attr' => [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] - 用
try-catch包裹整个事务块,严格判断错误码是否为'1213'(不是字符串"HY000"或其他) - 重试次数控制在 2–3 次,超过说明逻辑或索引设计存在根本缺陷;每次重试前调用
$this->db->startTrans()重新开启事务 - 避免固定 sleep,推荐指数退避:第 1 次延迟 50ms,第 2 次 100ms,第 3 次 200ms
从代码层降低死锁概率
ThinkPHP 的模型操作本身不引入死锁,但开发者编排顺序不当会直接触发:
立即学习“PHP免费学习笔记(深入)”;
-
统一多表操作顺序:例如所有涉及订单、用户、库存的事务,强制按
user → order → stock → log顺序执行 UPDATE/INSERT,禁止一个接口先改库存再写订单,另一个反向操作 -
缩小事务粒度:把 HTTP 请求、Redis 写入、文件处理等非数据库操作全部移出
startTrans()和commit()之间;只包裹真正需要原子性的 DB 操作 -
确保 WHERE 条件走索引:ThinkPHP 的
where('status', 1)->update()若status无索引,InnoDB 会升级为表级锁或全表扫描加间隙锁,大幅增加冲突面 -
慎用范围锁:避免在事务中执行
where('create_time', 'between', [$s, $e])->lock(true)这类语句,间隙锁极易与并发 INSERT 冲突;如需范围校验,改用主键 IN 列表或乐观锁替代
补充防御手段:乐观锁与监控
对高频更新的单条记录(如商品库存、账户余额),悲观锁(forUpdate())并非最优解:
- 在数据表添加
version字段,查询时一并读出;更新时用where(['id' => $id, 'version' => $oldVersion]),通过affectedRows()判断是否更新成功,失败则重试或提示“已被修改” - 在 ThinkPHP 的中间件或全局异常处理器中,对错误码 1213 单独统计并上报(如写入 Redis 计数器或推送企业微信告警),当每分钟死锁超 5 次即触发人工介入
- 定期检查慢查询日志与
innodb_row_lock_waits状态变量,若锁等待陡增但死锁未增,说明存在严重锁竞争,需优化索引或拆分热点事务



















