Laravel同步模式下TestSyncJob任务不重叠的实现问题
解决同步队列任务执行重叠问题
问题原因
dispatchSync() 是同步执行任务,Laravel 原生的 WithoutOverlapping 中间件针对队列异步任务设计,在同步模式下检测到锁存在时会直接跳过任务,不会等待锁释放,因此无法满足你需要等待的需求。
解决方案
方案一:手动实现分布式阻塞锁(推荐)
利用 Laravel 的缓存锁实现分布式阻塞等待,确保同一时间只有一个 TestSyncJob 执行,其他请求会等待锁释放后再运行。
修改 TestSyncJob 的 handle 方法:
class TestSyncJob implements ShouldQueue { public function handle(): void { // 定义全局唯一的锁名称 $lockName = 'test-sync-job-lock'; // 设置锁超时时间(需大于任务最长执行时间,避免死锁) $lockTimeout = 20; // 获取锁并阻塞等待,直到拿到锁或超时(传 null 表示一直等待) $lock = \Illuminate\Support\Facades\Cache::lock($lockName, $lockTimeout); if ($lock->get()) { try { \Log::info('Sync job started!'); sleep(10); \Log::info('Sync job finished!'); } finally { // 无论任务成功失败,都必须释放锁 $lock->release(); } } } }
说明:
- 缓存锁基于 Redis/Memcached 实现,支持多进程、多服务器场景下的分布式锁
finally块确保锁一定会被释放,避免死锁- 若需要限制最长等待时间,可将
$lock->get()改为$lock->block(60)(表示最多等待60秒)
方案二:使用单进程专属队列(异步场景适用)
如果不需要 TestQueueJob 等待 TestSyncJob 执行完成,可将 TestSyncJob 改为异步任务,并分配到一个仅启动单进程的专属队列,天然保证任务串行执行。
- 配置 Supervisor 专属队列进程:
[program:laravel-queue-sync] process_name=%(program_name)s command=php /path/to/your/laravel/artisan queue:work database --queue=sync-jobs --sleep=3 --tries=3 autostart=true autorestart=true user=www-data numprocs=1 redirect_stderr=true stdout_logfile=/path/to/laravel/storage/logs/queue-sync.log
- 修改任务调度方式:
在TestQueueJob的handle方法中,将dispatchSync()改为异步调度到专属队列:
class TestQueueJob implements ShouldQueue { public function handle(): void { // some code TestSyncJob::dispatch()->onQueue('sync-jobs'); // some code } }
说明:此方案下 TestQueueJob 会继续执行后续代码,不会等待 TestSyncJob 完成,适合不需要同步等待的场景。
方案三:自定义阻塞式无重叠中间件
基于原生 WithoutOverlapping 中间件改造,让同步任务也能阻塞等待锁释放。
- 创建自定义中间件:
<?php namespace App\Queue\Middleware; use Illuminate\Contracts\Cache\Repository as Cache; use Illuminate\Queue\Middleware\WithoutOverlapping as BaseMiddleware; class BlockingWithoutOverlapping extends BaseMiddleware { protected $cache; protected $waitTimeout; public function __construct($key, $waitTimeout = 60) { parent::__construct($key); $this->waitTimeout = $waitTimeout; $this->cache = app(Cache::class); } public function handle($job, $next) { $lockKey = $this->getLockKey($job); // 尝试获取锁,若失败则阻塞等待指定时长 $lock = $this->cache->lock($lockKey, $this->expireAfter); $lock->block($this->waitTimeout); try { return $next($job); } finally { $lock->release(); } } }
- 在
TestSyncJob中使用自定义中间件:
use App\Queue\Middleware\BlockingWithoutOverlapping; class TestSyncJob implements ShouldQueue { public function handle(): void { \Log::info('Sync job started!'); sleep(10); \Log::info('Sync job finished!'); } public function middleware() { return [new BlockingWithoutOverlapping(1)]; } }
说明:此方案保留了中间件的使用方式,同时适配了同步任务的阻塞需求,适合习惯用中间件管理任务约束的场景。
内容的提问来源于stack exchange,提问作者hehoboy
相关产品推荐
相关产品推荐

