GANKUDADIZ
BACK_TO_BLOG
TECH_LOG :: 2026.04.03

给博客加上异步邮件通知:Laravel Queue 实践小记

Avatar
By Gankudadiz · 1 min read · 22 views

最近在折腾博客的评论系统改造,虽然是个没什么流量的小破站,但之前一直有个很影响体验的痛点:每次有人提交评论,页面总要卡顿个两三秒才能响应。

查了一下原因,其实很典型——因为我在后台设置了"有新评论时发邮件通知站长"。系统在处理完评论入库后,会同步去调用 SMTP 服务器发邮件。发邮件这种网络请求的延迟是不可控的,结果就是把这几秒钟的等待时间全转嫁给了评论的用户。

为了解决这个问题,我决定把发邮件的动作扔到后台去异步执行。这就轮到 Laravel Queue 出场了。

怎么用起来的

Laravel 对队列的封装非常优雅,改造成本比我想象的低得多。

首先,我把原本的邮件类 CommentOwnerNotificationMail 稍微改了一下,让它实现 ShouldQueue 接口,并引入 Queueable trait。这相当于给这个邮件打了个标记:"别急着发,去队列里排队"。

class CommentOwnerNotificationMail extends Mailable implements ShouldQueue
{
    use Queueable, SerializesModels;

    public function __construct(public Comment $comment)
    {
        $this->comment->loadMissing('post');
    }
}

然后,在触发邮件的地方(CommentOwnerNotificationService),把原本的 send() 替换成 queue() 就可以了:

Mail::to($recipientEmail, $recipientName)->queue(new CommentOwnerNotificationMail($comment));

接着是配置队列驱动。因为是个体量不大的博客,专门去跑个 Redis 实在有点大炮打蚊子,Laravel 自带的 database 驱动刚好满足需求。我把 .env 里的队列连接从同步改成了数据库:

最后,为了让队列里的任务真正跑起来,需要在后台启动一个 worker 进程去监听。在本地开发时,我跑了一下 php artisan queue:work 测试:

看着终端里弹出的绿色的 DONE,强迫症得到了极大的满足。提交评论瞬间顺滑了,页面可以说是秒回,发邮件的工作则在后台默默完成。

背后是怎么回事

用起来简单,但理清它的原理还是挺有意思的。

当我们调用 queue() 时,Laravel 其实并没有真正去发邮件,而是把这个任务(包含了邮件类以及它所依赖的 Eloquent 模型数据,这就是 SerializesModels 的作用)序列化成了一段 JSON 数据,存到了后端的存储里。在我的配置下,它被存到了数据库的 jobs 表里。

用户的 HTTP 请求到这一步就结束了,直接返回响应。

与此同时,我们在后台跑的那个 queue:work 进程就像个不知疲倦的打工人,一直在轮询 jobs 表。一旦发现有新任务,它就会把这段 JSON 取出来,反序列化还原成原来的对象,然后去执行真正的耗时操作(连接 SMTP 发邮件)。如果执行失败了,它还可以根据配置自动重试。

Queue 和定时任务,到底用哪个

说到异步处理,顺便把另一个容易混淆的东西也理一理:定时任务(Schedule)和我上面说的 Queue 是完全不同的东西。

先说定时任务。它解决的是"定时定点执行"的问题。比如每天凌晨要跑一个脚本清理三个月前的日志,或者每周一早上给所有用户发一封周报邮件。定时任务是基于时间的,到了那个点就触发,跟有没有用户请求没关系。

而 Queue 解决的是"异步消化"的问题。它的触发时机是某个事件(比如用户提交了评论),任务放进队列之后由 worker 在后台慢慢处理,不阻塞当前请求。

用一个表格来对比:

Queue(队列) Schedule(定时任务)
触发时机 事件驱动,有请求了才入队 时间驱动,到了点就执行
执行逻辑 worker 不断轮询,来了任务就处理 定时器定时触发,执行频率可精确控制
典型场景 发邮件、推送通知、第三方 API 调用 数据清理、报表生成、定期同步
实时性 取决于 worker 繁忙程度,通常很快 精确到分钟/小时级别
失败重试 失败的任务可以自动重试,放回队列 失败后需要手动处理或额外的重试机制

听起来区别挺明显的,但实际用起来有时候会纠结。我的经验是:如果一个任务是由用户行为触发的,优先用 Queue;如果一个任务跟用户无关、只是时间到了就该执行,用 Schedule。

举两个常见的纠结场景:

场景一:给新用户发送欢迎邮件 用 Queue。用户在注册时触发邮件发送,不需要等他等在注册页面看加载动画。

场景二:每天凌晨给所有用户发送数据统计邮件 用 Schedule。跟用户行为无关,定时执行即可。

当然,也有些场景需要两者配合。比如定时任务触发后,批量给 1000 个用户发邮件,这时候可以定时任务负责生成邮件任务并逐个入队,Queue 的 worker 负责真正发送。这样既保证了一对一发送失败的独立重试,又不会因为发送量太大而拖垮服务器。

还能用在哪

通过这次改动,我对异步任务的适用场景有了更直观的感受。本质上,只要是耗时较长且用户不需要立刻知道结果的操作,都应该扔给队列。

比如:

  • 消息通知:像这次的邮件通知、或者发短信、推送到企业微信/钉钉。
  • 重度计算:导出生成报表、批量处理数据。
  • 文件处理:用户上传了一张大图,后台异步去裁剪、压缩、生成缩略图。
  • 第三方 API 调用:比如请求一些响应比较慢的外部接口。

虽然只是给一个小博客优化了评论体验,但完整走一遍 Laravel Queue 的流程还是挺有收获的。技术这东西,平时看文档觉得懂了是一回事,真正动手在一个具体场景里用起来,又是另一回事了。

评论 (0)

还没有评论,来留下第一条想法吧。
ACTION:

留下你的评论