ThinkPHP中MySQL死锁抛出错误码1213的PDOException,需用$e->getCode() === '1213'精确判断,配合PDO::ERRMODE_EXCEPTION配置、显式事务及连接重建的指数退避重试机制。

死锁异常在ThinkPHP里到底长什么样
ThinkPHP 6.x 默认用 PDO 连接 MySQL,死锁触发时抛出的不是 PDOException 的泛型错误,而是具体错误码为 1213 的异常,消息里带 Deadlock found when trying to get lock。你如果只 catch Throwable 却没检查错误码,重试逻辑就完全失效。
常见错误现象:事务中执行 update 或 select for update 突然报错,但日志里只显示“SQLSTATE[40001]: Serialization failure”,这是误判——MySQL 死锁是 40001 吗?不是,那是序列化失败;死锁固定是 HY000 + 1213。
- 务必用
$e->getCode() === '1213'判断,别信 SQLSTATE - ThinkPHP 的
Db::transaction()不会自动捕获或重试,得自己包一层 - 注意 PDO 的
ATTR_ERRMODE必须设为PDO::ERRMODE_EXCEPTION,否则死锁直接静默失败
手动实现带重试的事务函数(非装饰器)
别依赖第三方重试库,ThinkPHP 场景下最稳的是写个带指数退避的裸循环。关键不是“重试几次”,而是“每次重试前必须释放当前 PDO 连接并重建事务上下文”——否则第二次 beginTransaction() 会失败。
使用场景:库存扣减、订单状态互斥更新、积分流水对账等强一致性写操作。
立即学习“PHP免费学习笔记(深入)”;
- 重试前调用
$connection->close(),再通过Db::connect()拿新连接 - 最大重试次数建议 ≤ 3,超过说明业务逻辑本身有锁竞争设计缺陷
- 退避时间从 50ms 起步,每次 ×1.5(比如 50 → 75 → 112),避免雪崩式重试
- 示例核心逻辑:
for ($i = 0; $i < 3; $i++) { try { Db::transaction(function () use ($data) { // 你的 update/select for update 操作 }); return true; } catch (\PDOException $e) { if ($e->getCode() !== '1213') throw $e; if ($i === 2) throw $e; usleep((int)(50 * pow(1.5, $i) * 1000)); } }
PDO 配置项对死锁感知的影响
ThinkPHP 的数据库配置里,params 字段直接透传给 PDO 构造函数。几个关键项不设对,死锁根本捕不到或重试无效。
-
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION是前提,缺了就 catch 不到 -
PDO::ATTR_TIMEOUT => 3建议显式设为 3 秒,防止锁等待卡死整个请求 -
PDO::MYSQL_ATTR_INIT_COMMAND => "SET innodb_lock_wait_timeout = 3"要配,否则 MySQL 默认锁等待 50 秒,重试毫无意义 - 别开
PDO::ATTR_PERSISTENT,持久连接会让事务状态跨请求污染,死锁重试时极易复现“Cannot execute queries while other unbuffered queries are active”
为什么不能在模型 save() 里直接套重试
save() 内部调用的是 insert() 或 update(),它们默认不开启事务,也不暴露底层 PDO 实例。你 wrap 一个 save() 并 catch 死锁,大概率 catch 不到——因为真正触发死锁的可能是它内部的关联查询或事件回调里的额外 SQL。
容易踩的坑:
- 在
afterSave钩子里做Db::table()->where()->update(),这个 update 才是死锁源,但外层 save() 的 try/catch 完全不覆盖它 - 用
Model::update()静态方法时,它走的是非事务批量更新,根本不会报死锁(而是直接阻塞超时) - 正确做法:把所有可能冲突的操作收拢到一个显式
Db::transaction()块里,而不是分散在模型生命周期钩子中
复杂点在于,死锁不是纯技术问题,它暴露的是行锁粒度和执行顺序的设计盲区。重试只是兜底,真要稳定,得看 SQL 是否命中索引、where 条件是否导致锁升级、事务里有没有混合读写顺序颠倒——这些比写重试逻辑难得多。



















