本站首页 返回顶部 关于博主

在Laravel中使用队列发送邮件

PDF版

写在前面

当执行长时间的任务时,如果采取同步的方式实现,用户的等待时间会很长。一种可行的方式是采取异步的方式,把它放到列队中,并即时返回。放到队列中的任务按照指定的顺序执行。

本文介绍了 Laravel 5.2 和 6.x 中如何使用队列来发送邮件,以避免用户的长时间等待。

背景

在上篇文章 《使用Poste自建邮件服务器》 末尾,我提到了后续计划,申请国外的VPS自建邮箱服务器,用以代替在局域网内的邮箱服务器。最近,我的确这么做了。也如我所料,发邮件的速度大不如从前。

有两个微信小程序需要发送邮件:扫码签到工具扫码e签。之前发送一封邮件大约 3 秒,替换成国外 VPS 之后事件延长到 7 ~ 8 秒。发一封邮件让用户等待的时间变成了原来的 2 倍以上,体验大打折扣。如何改善呢?两个小程序的服务端都是 Laravel 开发的,正好 Laravel 支持队列,决定把同步邮件改成队列发送吧。

技术方案

当用户发送邮件时,把每封需要发送的邮件当成任务放到队列中,放到队列之后就返回。相对同步方案,不必等到邮件发送完成再返回。另一方面,队列中的任务按照既定顺序,依次执行发送邮件的任务。

在这个方案中,分两部分:1. 把发邮件的任务加到队列中;2.执行队列中的任务。

在我的案例中,扫码签到工具 小程序使用了 Laravel 5.2,扫码e签 小程序使用了 Laravel 6.x。在方案的两部分中,把任务加入到队列中的实现稍有不同,执行队列的任务是完全一样的。

实现步骤

1. 把发邮件的任务添加到队列中。

把发邮件的任务添加到队列中,有两种实现方式,一种方式是使用 Mail::queue 实现,另外一种方式是创建一个 job,在 job 中实现发邮件的功能,然后 dispatch 创建的 job。

因为 Mail::queue 实现起来相对简单,我采用了它。以下分别针对 Laravel 5.2 和 Laravel 6.x 描述如何实现。

在之前的同步方式中,Laravel 5.2 和 6.x 发送邮件的核心代码基本一样:

Mail::send('emails.template', 
   ['input_argument'=>$input_argument],
   function($message)use( $email){
      $to = $email;
      $message ->to($to)->subject('邮件标题');
   });

1) 先讲 Laravel 5.2 的实现方式。

查看 Mail 的源代码,发现 Mail::send 与 Mail::queue 这两个函数接收的参数完全一样。

第一步,把 Mail::send 直接修改成 Mail::queue。发送邮件,失败。日志中提示在 emails.template 中访问了不存在的 Object 属性。

问题出在哪里呢?

思考一下队列的实现原理。在发送邮件时,把发送的任务添加到队列中,然后队列依次执行任务。在把任务添加到队列中时,会把任务中涉及的内容序列化;而执行任务时,会把序列化之后的内容反序列化成需要执行的任务。

问题出来序列化上。从错误日志中看出,系统把 Mail::queue 第二个参数(数组)中的某个元素 $input_argument 解释成了它原先的数据类型(App\Models\Applies),在实际情况中,我在这个 Model 的基础上删除了某些属性,并添加了一些属性,导致在序列化时,还是按照它的原始类型进行的,添加的属性遗失了,因而产生了“访问不存在属性”的问题。

是不是把第二个参数(数组)中包含的所有元素修改成原始数据类型(如 stdClass、array等)就可以序列化了呢?试试看。

在我的案例里,需要把 App\Models\Applies中修改成 stdClass,代码如下

$new_input_argument = new stdClass();
    $input_argument_array = $input_argument->attributesToArray();
    foreach ($input_argument_array as $key => $value) {
        $new_input_argument->$key = $value;
    }

之后,用 $new_input_argument 替换 $input_argument,作为参数传到 Mail::queue 中就可以了。

测试发送邮件,成功,说明第一步是正确的。

稍等,说好的邮件队列,怎么在速度上并没有变快,和同步发送邮件没有什么不同呢?还有几步要做:

  1. 打开 .env 文件,发现 QUEUE_DRIVER 的值为 sync,这代表这邮件会同步发送,而不是推送到队列中。把它修改为 database(当然,还可以设为 redis 或其他值);
    QUEUE_DRIVER=database
  2. 执行如下命令,创建数据迁移文件,并执行数据迁移,生成 jobs 表,就是队列,用于存放执行的任务:
    php artisan queue:table php artisan migrate

此时,再发送邮件,发现即时返回了,并且在 jobs 表中多了一条记录。至此,表明任务已经成功的推送到队列中。

2) Laravel 6.x 的实现方式稍有不同。

开始尝试与 5.2 相同的方式调用 Mail::queue,提示错误,看框架的源代码才发现在 6.x 版本中, Mail::send 与 Mail::queue 接收的参数不一样。

需要先创建一个“可邮寄”类,它位于 app\Mail 下,可使用 make:mail 创建。在我的案例里,命令如下:

 php artisan make:mail SendReportMail

生成的类要实现两个方法。在构造函数里,传入参数,等同于 Laravel 5.2 中调用 Mail:queue 时传入的参数。在 build() 函数,构建邮件内容。然后在代码中调用如下函数:

 Mail::to($email)
        ->queue( new SendReportMail( $input_array ) );

测试发送邮件,发送成功。

再次强调下, $input_array 中的元素,必须是可以序列化的,如果是自定义的类型,不要增删变量

接下来的工作,就与 Laravel 5.2 基本一致了:修改队列的驱动,选择 database,生成数据库迁移文件,再执行 migrate 生成存放任务的表。稍加说明,在 6.x 里,.env 文件中的驱动变量是 QUEUE_CONNECTION ,而不是 QUEUE_DRIVER。

2. 执行队列中的任务。

在前面的操作中,已经成功地把任务推送到队列中,下一步,就是执行队列的任务了。有两个命令, queue:listen 和 queue:work。

运行如下命令,会执行 jobs 表中的任务。成功执行完一个任务之后,这条记录会删掉:

php artisan queue:work

实测发现,在 Laravel 5.2 中,queue:work 在执行完一条任务之后就会退出,而 queue:listen 会常驻内存;在 Laravel 6.x 中,两条命令都会常驻内存。

Supervisor 监控进程

Laravel 官方文档推荐使用 supervisor 来监控 queue:work 或 queue:listener 进程。Supervisor 是 Linux 操作系统下中的一个进程监控器,它可以在 queue:work 挂掉时自动重启之。先安装:

sudo apt-get install supervisor

Superivosr 配置文件放在 /etc/supervisor/conf.d/ 目录下。在我的案例 Laravel 5.2 中,我的配置文件命名为 larave-worker.config,内容如下:

[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=/opt/lampp/bin/php /opt/lampp/htdocs/signin_php/artisan queue:listen
autostart=true
autorestart=true
user=root
numprocs=3
redirect_stderr=true
stdout_logfile=/opt/lampp/htdocs/signin_php/storage/logs/queue_work.log

在第 3 行中,是执行 queue:listen (在 Laravel 6.x 中,可以改成 queue:work)命令,注意需要写明 php 的全路径,以免出现找不到的情况。第 7 行为运行的进程数目,我把它设为 3 。

把 laravel-worker.config 复制到目录 /etc/superivor/conf.d/,执行如下命令:

sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-worker:*

此时会启动 3 个 queue:listen 进程来执行队列中的任务。在启动之后,可以使用如下命令查看守护进程的状态:

sudo supervisorctl status

Supervisor无法运行的小插曲

我的 Laravel 环境部署在 docker 中,安装完 supervisor 后,执行 superviosrctl reread 提示 /var/run/supervisor.sock no such file,创建这个文件并运行supervisord即可解决。

sudo touch /var/run/supervisor.sock
sudo chmod 777 /var/run/supervisor.sock

然后运行如下命令:

supervisord

问题解决。

总结

至此,已经成功使用 Laravel 的队列发送邮件。

在实现过程中分为 2 步:1. 把发送邮件的任务推送到任务表中; 2. 执行任务表中的任务。在执行第 1 步时,可以把驱动设为 sync 用以测试是否推送成功。测试证明成功之后,把 QUEUE_DRIVER(或 QUEUE_CONNECTION )改为 database 或 redis, 再进行第 2 步的操作。

与同步发送邮件相比,队列可以即时返回,减少用户的等待时间。当邮箱服务器性能不高时,尤其是对时间不敏感的邮件,能很大程度提升用户的体验。


参考资料




请你留言