Laravel 队列与任务调度

小飞兽 Laravel 6 次阅读 2026-07-29

Laravel 队列系统允许你将耗时的任务(如发送邮件、文件处理)异步执行,显著提升 Web 请求响应速度。任务调度器则让你可以用简洁的代码配置复杂的定时任务。本文详细介绍两者的使用方法。

队列配置

// config/queue.php 关键配置
return [
    "default" => env("QUEUE_CONNECTION", "database"),

"connections" => [
"sync" => ["driver" => "sync"], // 同步执行(调试用)
"database" => [ // 数据库驱动(推荐开发/小规模)
"driver" => "database",
"table" => "jobs",
"queue" => "default",
"retry_after" => 90, // 任务超时时间
],
"redis" => [ // Redis 驱动(生产推荐)
"driver" => "redis",
"connection" => "default",
"queue" => env("REDIS_QUEUE", "default"),
"retry_after" => 90,
"block_for" => 5,
],
],

"failed" => [
"driver" => env("QUEUE_FAILED_DRIVER", "database-uuids"),
"database" => env("DB_CONNECTION", "mysql"),
"table" => "failed_jobs",
],
];

// .env
QUEUE_CONNECTION=redis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

创建队列任务

// 创建任务类
php artisan make:job SendWelcomeEmail
php artisan make:job ProcessPodcast --sync // 同步任务(不用队列)

// app/Jobs/SendWelcomeEmail.php
namespace App\Jobs;

use App\Models\User;
use App\Mail\WelcomeMail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
use Illuminate\Queue\Middleware\RateLimited;

class SendWelcomeEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

public int $tries = 3; // 最多重试 3 次
public int $backoff = 60; // 失败后 60 秒再重试
public int $timeout = 120; // 超时时间 120 秒
public int $maxExceptions = 2;

public function __construct(
public User $user
) {
$this->onQueue("emails"); // 指定队列名
$this->onConnection("redis"); // 指定连接
}

public function handle(): void
{
// 业务逻辑
Mail::to($this->user->email)->send(new WelcomeMail($this->user));
}

public function failed(\Throwable $exception): void
{
// 任务失败后调用(如记录日志、发通知)
\Log::error("SendWelcomeEmail failed for user {$this->user->id}: " . $exception->getMessage());
}

// 限流中间件
public function middleware(): array
{
return [new RateLimited("emails")];
}
}

// app/Jobs/ProcessPodcast.php
class ProcessPodcast implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

public function __construct(
public Podcast $podcast,
public string $audioPath
) {
$this->onQueue("media");
}

public function handle(): void
{
// 1. 下载/处理音频文件
// 2. 生成缩略图
// 3. 上传到 OSS
// 4. 更新数据库
}

// 根据媒体类型决定队列
public function uniqueId(): string
{
return $this->podcast->id;
}

// 相同任务在 60 秒内不重复执行
public function uniqueFor(): int
{
return 60;
}
}

分发任务

// 方式1:同步分发(立即执行)
SendWelcomeEmail::dispatchSync($user);

// 方式2:普通分发(异步)
SendWelcomeEmail::dispatch($user);

// 方式3:指定队列
SendWelcomeEmail::dispatch($user)->onQueue("high");

// 方式4:指定连接 + 队列
SendWelcomeEmail::dispatch($user)
->onConnection("redis")
->onQueue("emails");

// 方式5:延迟分发
SendWelcomeEmail::dispatch($user)->delay(now()->addMinutes(5));

// 方式6:如果没有相同任务正在运行则分发
(new ProcessPodcast($podcast, $path))
->unless(Job::hasUsingConnector("database") && Job::isQueued($podcast))
->dispatch();

// 方式7:链式任务(按顺序执行,任一失败后续取消)
ProcessPodcast::withChain([
new SendNotificationJob($user),
new UpdatePodcastStatusJob($podcast),
])->dispatch($podcast);

// 控制器中分发
class RegisterController extends Controller
{
public function store(Request $request)
{
$user = User::create($request->validated());
SendWelcomeEmail::dispatch($user); // 异步发送欢迎邮件
return response()->json(["user" => $user], 201);
}
}

队列 worker 运行

// 启动 worker(开发环境)
php artisan queue:work

// 生产环境使用 Supervisor 保持运行
// /etc/supervisor/conf.d/laravel-worker.conf
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /path/to/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=ubuntu
numprocs=4
redirect_stderr=true
stdout_logfile=/var/log/laravel-worker.log
stopwaitsecs=3600

// 相关命令
php artisan queue:work redis --queue=high,default # 高优先级先处理
php artisan queue:work redis --sleep=3 --tries=3 # 重试3次,间隔3秒
php artisan queue:retry # 重试所有失败任务
php artisan queue:retry 5 # 重试指定 ID
php artisan queue:failed # 查看失败任务列表
php artisan queue:flush # 清空所有失败任务
php artisan queue:prune-failed # 删除 7 天前的失败任务

任务调度器

// app/Console/Kernel.php
namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
protected function schedule(Schedule $schedule): void
{
// 每分钟执行一次(必须配置 cron)
$schedule->command("queue:restart")->everyMinute();

// 每天凌晨清理过期会话
$schedule->command("session:prune")->daily();

// 每天凌晨 2 点执行
$schedule->command("backup:run")->dailyAt("02:00");

// 每小时执行
$schedule->command("report:generate")->hourly();

// 每周一早上 9 点
$schedule->command("newsletter:send")->weeklyOn(1, "09:00");

// 每月第一天
$schedule->command("billing:generate")->monthly();

// 每 5 分钟
$schedule->command("sync:data")->everyFiveMinutes();

// 每小时,运行两次(:30 时)
$schedule->command("check:status")->everyThirtyMinutes();

// 自定义 cron
$schedule->command("report:weekly")->cron("0 8 * * 1"); // 每周一 8 点

// 任务调度中使用队列
$schedule->job(new SyncDataJob)->everyFiveMinutes();
$schedule->job(new CleanupJob)->daily();

// 执行 Shell 命令
$schedule->exec("node /home/ubuntu/build.js")->daily();

// 防止任务重叠
$schedule->command("report:generate")->withoutOverlapping();

// 在维护模式下也执行
$schedule->command("maintenance:check")->runInMaintenanceMode();

// 任务成功后记录日志
$schedule->command("sync:data")->onSuccess(function () {
\Log::info("Sync completed successfully");
});
}

protected function commands(): void
{
$this->load(__DIR__ . "/Commands");
require base_path("routes/console.php");
}
}

// crontab 配置(服务器上执行)
// * * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
// Laravel 会自动判断哪个任务该执行,无需写具体 cron 表达式

常见问题

    • Q: queue:work 和 queue:listen 有什么区别?
      A:queue:work 启动后不重启进程,只处理新任务(推荐生产环境,更省内存);queue:listen 每次接受新连接都会 fork 新进程(适合开发环境,代码修改后自动生效不需要重启)。
    • Q: 任务失败后如何重试?
      A:使用 queue:retry <id> 手动重试单个任务;queue:retry-all 重试所有失败任务;failed_jobs 表记录了失败任务的详情和错误信息。
    • Q: withoutOverlapping() 有什么用?
      A:防止同一任务在上次未执行完时再次触发。如果任务执行时间可能超过调度间隔(如 5 分钟内可能执行 2 次),使用 withoutOverlapping() 确保上一次执行完成后再执行下一次。
    • Q: 队列连接选 Redis 还是 Database?
      A:小规模应用或开发环境用 Database(零配置);生产环境推荐 Redis(速度快、支持优先级队列)。如果服务器内存足够,sync 驱动适合短时一次性任务。

    延伸阅读

  • Laravel 任务调度官方文档
  • Laravel Horizon:队列监控与管理面板