ThinkPHP6高并发数据库死锁非框架缺陷,而是事务资源竞争所致;需捕获错误码1213并指数退避重试(50ms→100ms→200ms)、统一多表操作顺序(如user→order→item→log)、缩小事务粒度、移出HTTP/Redis等耗时操作、优化索引避免间隙锁冲突。

数据库死锁在 ThinkPHP 部署后高并发场景下很常见,不是框架有 bug,而是事务资源竞争的自然结果。关键不是“完全不出现”,而是让系统能自动识别、安全恢复、并大幅降低发生频率。
必须捕获错误码 1213 并指数退避重试
TP6 默认不处理死锁异常,需手动包裹事务逻辑:
- 确保数据库配置中开启 PDO 异常模式:
'options' => [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] - 在
try-catch中执行事务,检查$e->getCode() === '1213' - 每次重试前调用
$db->beginTransaction()重新开启事务,不能复用已中断的事务 - 最多重试 3 次,延迟建议为 50ms → 100ms → 200ms(避免固定 sleep 导致集群排队雪崩)
严格统一多表操作顺序
约 70% 死锁源于不同接口对相同表加锁顺序不一致。例如:
- 订单创建流程:先更新
user表,再写order,再扣item库存,最后记log - 库存扣减流程:也必须按
user → order → item → log执行,不能反着来 - 在 base service 或文档中明确定义该全局顺序,并强制所有模型、Db 查询、事务回调遵守
把耗时操作移出事务体
事务内只保留真正需要原子性的数据库写操作:
- ❌ 不要在
Db::transaction()里调用微信支付回调、写本地日志、执行 Redisset、发 MQ 消息 - ✅ 把这些操作移到
$transaction->commit()之后,可用 TP6 的afterCommit()钩子或事件监听封装 - 提交后失败不影响数据一致性,只影响下游动作,可异步补偿
优化索引与语句,减少间隙锁冲突
InnoDB 在可重复读级别下,以下写法容易触发间隙锁死锁:
-
UPDATE ... WHERE name LIKE 'abc%'(没走主键/唯一索引) -
UPDATE ... WHERE id > 100 AND id < 200(范围条件无精确索引) - 解决办法:确保
WHERE条件命中主键或覆盖索引;避免无索引字段参与更新条件;必要时改用乐观锁(version 字段 +WHERE id = ? AND version = ?)
不复杂但容易忽略



















