如何防止Laravel中相同user_id的队列Job并发运行,实现4秒间隔执行
Laravel队列同user_id任务并发执行问题解决方案
问题根因
- 锁驱动不兼容:如果使用database、file等非Redis驱动,Laravel的原子锁无法保证多进程下的互斥性,会出现锁击穿导致任务并发执行
- 锁过期时间配置过短:你配置的
$uniqueFor和WithoutOverlapping的expireAfter均为4秒,若任务执行时长超过预期、或队列有积压导致任务排队时间过长,锁会提前释放,下一个任务会提前获取到锁 - Unique机制作用范围限制:
ShouldBeUnique仅能防止任务分发阶段重复入队,已经进入队列的多个同user_id任务无法被该机制拦截,积压时多个任务会依次触发锁争抢,容易出现并发
解决方案
- 统一锁与队列驱动为Redis:修改
config/cache.php默认缓存驱动为redis,config/queue.php默认队列驱动为redis,保证原子锁的互斥性 - 调大锁过期时间:根据任务最大执行时长调整锁过期时间,如任务最长执行1秒,可将
$uniqueFor改为10秒,WithoutOverlapping的expireAfter改为6秒,避免锁提前过期:
// 任务类内修改 public $uniqueFor = 10; public function middleware() { return [(new WithoutOverlapping($this->user_id))->releaseAfter(4)->expireAfter(6)]; }
- 新增handle层二次锁校验:在任务执行逻辑最开头新增手动原子锁校验,未获取到锁直接延迟4秒放回队列,执行完成后手动释放锁,同时新增最后执行时间校验,严格保证两次执行间隔不低于4秒,避免依赖框架中间件的锁逻辑疏漏:
use Illuminate\Support\Facades\Cache; public function handle() { // 先校验执行间隔 $lastExecKey = 'user_action_last_exec_'.$this->user_id; $gap = time() - Cache::get($lastExecKey, 0); if ($gap < 4) { return $this->release(4 - $gap); } // 手动加锁保证互斥 $lock = Cache::lock('user_action_running_'.$this->user_id, 6); if (!$lock->get()) { return $this->release(4); } try { Log::debug(''); Log::debug('Started Time: '.date('Y-m-d H:i:s')); // 原有业务逻辑 // 执行完成更新最后执行时间 Cache::put($lastExecKey, time(), 10); } catch (Exception $e) { Log::critical('Error occurred.'); Log::critical($e); $this->job->fail($e); } finally { $lock->release(); } }
- 优化队列消费速度:如果
available_at与执行时间差值过大,说明队列worker数量不足,可适当增加worker进程数,避免任务长时间积压导致锁逻辑失效
内容的提问来源于stack exchange,提问作者mehri abbasi
相关产品推荐
相关产品推荐

