ThinkPHP6定时任务本身不自动开启事务,但手动使用Db::transaction()等可能因并发、锁序不一致或事务过长引发MySQL死锁;需控制事务粒度、统一加锁顺序、确保WHERE走索引、避免耗时操作,并用Redis预占、消息队列等方式降低锁竞争。

ThinkPHP6 定时任务本身不自动开启事务,但一旦你在任务逻辑中手动调用 Db::transaction() 或 Db::startTrans(),就可能因并发执行、锁顺序不一致或事务过长,触发 MySQL 死锁。关键不是“定时任务会不会死锁”,而是“你写的任务代码有没有埋下死锁隐患”。
控制事务粒度:只锁真正需要的数据行
死锁多发生在多个定时任务同时更新不同表、且加锁顺序不一致时。比如任务 A 先锁 user 再锁 order,任务 B 反过来操作,就极易形成环形等待。
- 用
lock(true)显式指定行锁,避免全表扫描导致的间隙锁扩大(尤其在 WHERE 条件无索引时) - 所有涉及更新的 SQL,WHERE 必须走索引;若需批量处理,按主键升序遍历,统一加锁顺序
- 事务内不做耗时操作(如 HTTP 请求、文件读写、复杂计算),防止锁持有时间过长
避开数据库锁竞争:用 Redis 预占 + 异步落库
对高并发场景下的定时结算、分佣、库存同步等任务,不要让多个实例直接争抢同一组数据库记录。
- 用 Redis SET 原子指令预占任务资源,例如:
$redis->set('task:commission:20261029', $workerId, ['nx', 'ex' => 3600]) - 抢占失败则直接退出,不进入数据库事务;成功后再执行业务逻辑
- 核心数据变更改用“预扣+消息队列”方式:先写入中间状态表或 Redis,再由独立消费者异步提交到主库
主动暴露与定位死锁上下文
定时任务常在后台静默运行,出错难发现。必须让死锁可查、可追溯。
- MySQL 开启
innodb_print_all_deadlocks = ON,确保每次死锁都记入 error log - TP6 中为每个定时任务设置唯一 trace_id,并在日志中透传;使用
Log::record()记录事务开始/结束及关键 SQL - 在事务外提前检查前置条件(如是否已处理、状态是否允许),减少不必要的事务开启
用非事务方式替代部分场景
不是所有定时逻辑都需要事务。识别可降级操作,降低数据库压力。
- 统计类任务(如日活汇总)用
INSERT ... ON DUPLICATE KEY UPDATE,避免先查后更 - 清理类任务(如过期日志删除)用分页 LIMIT 删除,每次只删 1000 条,防止锁表
- 状态轮询类任务,加
WHERE status = 'pending' AND updated_time ,跳过正在被其他任务处理的记录
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











