Laravel 8队列任务重复执行致账本余额更新异常及死锁求助
解决Laravel 8 Database队列任务重复执行及死锁问题
核心原因分析
你遇到的任务重复执行+死锁报错(SQLSTATE[40001]),本质是Database队列的事务冲突:
- 队列进程完成任务后,会尝试删除
jobs表对应记录,若此时多个进程同时操作jobs表或关联的账本表,InnoDB引擎会触发死锁; - 死锁导致任务删除失败,Laravel判定任务未完成,触发重试机制,最终造成账本余额重复更新。
具体解决方案
1. 优化jobs表索引(必做)
Database队列的性能和锁冲突直接依赖jobs表的索引,默认生成的表可能缺少关键索引:
- 给
queue、reserved_at字段添加联合索引:
ALTER TABLE jobs ADD INDEX idx_queue_reserved (queue, reserved_at);
- 确保
jobs表使用InnoDB引擎(MyISAM不支持行锁,会加剧锁冲突),且id为自增主键。
2. 调整任务重试与退避策略
当前的$retryAfter和$timeout设置合理,但添加退避间隔可减少短时间内的重试冲突:
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public $retryAfter = 85; public $timeout = 80; public $tries = 3; // 添加退避间隔,避免频繁重试加剧冲突 public $backoff = [10, 30, 60]; // 第一次等10秒,第二次30秒,第三次60秒
3. 账本更新添加行级锁(解决重复更新直接手段)
在更新账本余额时,使用lockForUpdate()获取行级排他锁,确保同一时间只有一个进程能修改该账本:
public function handle() { // 排他锁锁定账本记录,避免并发更新 $ledger = \App\Models\Ledger::where('id', $this->ledger_id)->lockForUpdate()->firstOrFail(); // 执行余额更新逻辑(示例) if ($this->type === 'income') { $ledger->balance += $this->amount; } else { $ledger->balance -= $this->amount; } $ledger->save(); }
即使任务因死锁重试,也会等待锁释放后执行,不会重复累加/扣除余额。
4. 优化Supervisor队列配置
- 控制进程数:不要开启过多队列进程,建议按服务器CPU核心数设置(如4核服务器开4-6个进程),减少并发冲突;
- 添加休眠参数:在Supervisor的启动命令中加入
--sleep=3,降低队列进程轮询jobs表的频率:
command=php /var/www/vhosts/poshbooks.in/httpdocs/artisan queue:work database --sleep=3 --tries=3 --timeout=80
5. 捕获死锁异常并触发合理重试
在任务handle()方法中捕获死锁异常,确保Laravel按预期重试:
use Illuminate\Database\QueryException; use Illuminate\Queue\MaxAttemptsExceededException; public function handle() { try { // 你的账本更新逻辑(包含行级锁) } catch (QueryException $e) { // 判定为死锁异常 if ($e->getCode() === '40001') { // 抛出异常触发重试,直到达到最大尝试次数 throw new MaxAttemptsExceededException($e->getMessage(), $e->getCode(), $e); } // 其他数据库异常直接抛出 throw $e; } }
6. 长期方案:迁移至Redis队列驱动
Database队列天生存在事务冲突的风险,若上述优化后仍有问题,建议切换到Redis队列驱动:
- Redis的任务获取是原子操作,不会出现多个进程拿到同一任务的情况;
- 无需处理数据库死锁,性能更稳定。
内容的提问来源于stack exchange,提问作者Ashu
相关产品推荐
相关产品推荐

