Laravel 10中MySQL死锁(SQLSTATE[40001])问题排查与修复
Laravel队列死锁(SQLSTATE[40001] 错误码1213)原因分析与修复方案
原因分析
死锁的核心是两个或多个数据库操作互相持有对方需要的锁,形成循环等待,结合你的场景,具体触发逻辑如下:
- Laravel数据库队列的worker处理任务时,会先执行
select * from jobs where queue = ? and reserved_at is null order by id asc limit 1 for update锁定待处理任务,更新reserved_at标记为已占用,任务完成后执行delete from jobs where id = ?删除任务。 - 你的两个定时命令都往默认队列批量分发Job,在任务分发高峰期,多个worker会同时竞争队列中的任务:
- Worker A通过
select ... for update锁定了任务ID 1,准备执行删除但尚未完成; - Worker B同时通过
select ... for update锁定了任务ID 2; - 若此时出现锁顺序反转(比如Worker A尝试获取ID 2的锁,Worker B尝试获取ID 1的锁),或InnoDB的间隙锁(因批量插入任务产生的范围锁)与行锁冲突,就会触发循环等待,导致死锁。
- Worker A通过
- 另外,默认的REPEATABLE READ事务隔离级别下,InnoDB会生成更多间隙锁,进一步增加了死锁的触发概率。
修复方案
1. 拆分队列,减少锁竞争
将两个定时命令的Job分发到独立队列,避免不同类型的任务在同一队列中竞争锁:
- 修改定时命令的分发代码,指定不同队列:
// GoogleIndexSearchedCarsCommand GoogleIndexSearchedJob::dispatch($car)->onQueue('google-searched'); // GoogleIndexHiddenCarsCommand GoogleIndexHiddenJob::dispatch($car)->onQueue('google-hidden'); - 启动对应队列的worker:
php artisan queue:work --queue=google-searched php artisan queue:work --queue=google-hidden
2. 启用skip_locked跳过锁定任务(MySQL 8.0+)
在config/queue.php的database连接配置中添加skip_locked参数,让worker直接跳过已被其他进程锁定的任务,避免等待锁引发死锁:
'connections' => [ 'database' => [ 'driver' => 'database', 'table' => 'jobs', 'queue' => 'default', 'retry_after' => 90, 'skip_locked' => true, // 新增此配置 ], ],
3. 优化批量Job插入逻辑
如果定时命令是批量生成Job,使用数据库批量插入替代单个插入,减少事务次数和锁竞争:
// 替代多次单个dispatch,批量组装任务数据后插入 $jobData = collect($cars)->map(function ($car) { return [ 'queue' => 'google-searched', 'payload' => json_encode([ 'job' => '\App\Jobs\GoogleIndexSearchedJob', 'data' => ['car' => $car] ]), 'created_at' => now(), 'updated_at' => now(), ]; })->toArray(); DB::table('jobs')->insert($jobData);
4. 调整数据库事务隔离级别
将MySQL的事务隔离级别从默认的REPEATABLE READ改为READ COMMITTED,减少间隙锁的产生:
在config/database.php的mysql连接中添加:
'connections' => [ 'mysql' => [ // 其他配置 'isolation_level' => 'READ COMMITTED', ], ],
5. 增加Job重试机制
在Job类中添加重试配置,让死锁导致失败的任务自动重试(死锁通常是瞬时的):
class GoogleIndexSearchedJob implements ShouldQueue { public $tries = 3; // 重试次数 public $backoff = [1, 3, 5]; // 重试间隔(秒) // 任务逻辑... }
6. 合理调整worker数量
避免启动过多的worker进程竞争同一队列,根据服务器CPU核心数和队列任务量调整,一般worker数不超过CPU核心数的2倍。
内容的提问来源于stack exchange,提问作者Nikolai92
相关产品推荐
相关产品推荐

