You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 04:07:46