tp的transaction()不处理死锁重试,因其仅封装pdo基础事务方法,不识别死锁异常类型,需手动捕获“deadlock found”错误并实现带退避延迟与最大重试次数的重试逻辑。

死锁不能靠自动重试解决,TP 默认不提供死锁重试机制;必须手动捕获 Deadlock found when trying to get lock 错误并控制重试逻辑。
为什么 TP 的 transaction() 不处理死锁重试
ThinkPHP 的 transaction() 方法本质是封装了 PDO 的 beginTransaction() / commit() / rollback(),它只保证“失败时回滚”,但不会识别死锁错误类型、也不会重试。死锁是数据库层面的并发冲突,PDO 抛出的是普通异常(PDOException),TP 不做区分处理。
- 死锁错误信息固定为:
SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock(MySQL)或类似变体 - TP 的事务回调返回
false或抛出异常都会触发回滚,但不会自动再进一次回调 - 盲目重试可能加剧竞争,必须加最大重试次数和退避延迟
手动实现死锁重试的可靠写法
核心思路:用 try/catch 包裹事务逻辑,捕获死锁异常,计数重试,超限则抛出原异常。
$maxRetries = 3;
for ($i = 0; $i lock(true)->dec('balance', 100);
Order::create($data);
});
break; // 成功则跳出循环
} catch (\PDOException $e) {
if ($i === $maxRetries - 1 || !str_contains($e->getMessage(), 'Deadlock')) {
throw $e; // 非死锁或已达重试上限,直接抛出
}
usleep(50000 * ($i + 1)); // 指数退避:50ms, 100ms, 150ms
}
}
-
lock(true)是关键:显式加行锁(SELECT ... FOR UPDATE),避免无锁更新引发隐式锁升级冲突 - 不要在事务外查数据再传入回调——可能产生脏读或版本错位;所有读写必须在
transaction()回调内完成 -
usleep()延迟必须有,否则重试瞬间撞上同一把锁,等于没 retry
哪些场景特别容易触发死锁,要重点加重试
不是所有事务都需要死锁重试,但以下模式高发死锁,建议统一包装:
- 多表交叉更新:如 A 表先锁再更新 B 表,而另一请求 B 表先锁再更新 A 表
- 批量操作未按主键顺序执行:
whereIn('id', [3,1,2])可能导致锁顺序不一致,应先sort()再查 - 嵌套事务(TP 不支持真嵌套,但业务层模拟时易忽略外层 rollback)
- 长事务中混用缓存读取(如 Redis 查余额)+ 数据库扣减,缓存与 DB 状态不同步会拉长锁持有时间
事务回滚本身不等于数据恢复成功
回滚只是撤销当前事务内的 SQL 影响,但不保证业务状态一致。例如:
- 事务中调用了外部 HTTP 接口(如支付回调),回滚无法撤回该请求
- 事务前已发 MQ 消息,回滚后消息仍存在,需配合本地事务表或半消息
- 使用
Db::raw()执行存储过程,若过程内有 COMMIT,TP 无法感知,会导致事务失控
真正健壮的事务边界,得从接口幂等性、最终一致性设计入手,而不是依赖框架级“自动重试”。死锁重试只是并发控制里很小一环,锁粒度、索引优化、更新顺序才是根因所在。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











