在上一节中我们介绍了使用Swoole发送单个邮件,那么如果是大量的邮件需要发送,比如给2万个用户发送优惠活动邮件,这是一个比较耗时的过程,而PHP本身不适合处理这种耗时多任务场景。本节为给大家介绍使用Swoole+Redis来实现发送批量邮件的例子。

建立任务

接上一节代码,编辑src/App/Mail.php文件代码,在public function onTask()方法中增加批量队列发送邮件的代码:


    public function onTask(swoole_server $serv, $task_id, $from_id, $data)
    {
        $res['result'] = 'failed';
        $req = json_decode($data, true);
        $action = $req['action'];
        echo date('Y-m-d H:i:s')." onTask: [".$action."].n";
        switch ($action) {
            case 'sendMail': //发送单个邮件
                $mailData = [
                    'emailAddress' => 'abc@example.com', //接收方,改成自己的邮箱可以测试接收邮件
                    'subject' => 'swoole实验室',
                    'body' => '测试This is the HTML message body.',
                    'attach' => '/home/swoole/public/a.jpg'
                ];
                $this->sendMail($mailData);
                break;

            case 'sendMailQueue': // 批量队列发送邮件
                $this->sendMailQueue();
                break;
            
            default:
                break;
        }
    }

建立Redis队列

由于发送的邮件比较多,我们把邮件列表事先保存在Redis队列中。我们知道Redis的使用场景很多,其中就可以用它来做简单的队列。

我们在任务中调用了sendMailQueue()方法,继续在Mail.php中添加:


    // 邮件发送队列
    private function sendMailQueue()
    {
        $redis = new Redis();
        $redis->connect('127.0.0.1', 6379);
       
        $password = '123';
        $redis->auth($password);

        swoole_timer_tick(1000, function($timer) use ($redis) { // 启用定时器,每1秒执行一次
            $value = $redis->lpop('mailerlist');
            if($value){
                //echo '获取redis数据:' . $value;
                $json = json_decode($value, true);
                $start = microtime(true);
                $rs = $this->sendMail($json);
                $end = microtime(true);
                if ($rs) {
                    echo '发送成功!'.$value.', 耗时:'. round($end - $start, 3).'秒'.PHP_EOL;
                } else { // 把发送失败的加入到失败队列中,人工处理
                    $redis->rpush("mailerlist_err", $value);
                }
            }else{
                swoole_timer_clear($timer); // 停止定时器
                echo "Emaillist出队完成";
            }
        });
    }

上述代码中,先尝试连接Redis,然后使用Swoole的swoole_timer_tick()函数,它是个定时器,这个函数跟js的interval()函数一样,意思是每隔一定时间执行一次,它可以定义毫秒级粒度。很显然,上述代码中,每隔1000毫秒(1秒)从Redis队列mailerlist中取出一条,即一个邮件对象,然后执行发送邮件sendMail(),当发送完所有邮件后,使用swoole_timer_clear()关闭定时器即可。定时器的间隔时间可以调整。

客户端

在客户端,我们先往Redis队列里添加邮件内容,然后向服务端发起sendMailQueue批量发邮件指令。

<?php 
class Client
{
    private $client;
    
    public function __construct() {
        $this->client = new swoole_client(SWOOLE_SOCK_TCP);
    }

    public function connect() {
        if( !$this->client->connect("127.0.0.1", 9502 , 1) ) {
            echo "Error: {$this->client->errMsg}[{$this->client->errCode}]n";
        }
        
        $action = 'sendMailQueue';
        $time = time();
        $key = 'MYgGnQE33ytd2jDFADS39DSEWsdD24sK';
        $token = md5($action.$time.$key);
        $data = [
            'action' => $action,
            'token' => $token,
            'timestamp' => $time
        ];
        $msg = json_encode($data);

        $this->client->send( $msg );
        $message = $this->client->recv();
        echo "Get Message From Server:{$message}n";
    }
}


$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

$password = '123';
$redis->auth($password);

$arr = [];

$arr[0] = [
    'subject' => '国庆大酬宾,全场1折',
    'emailAddress' => 'axxxx@example.com',
    'body' => '您好,国庆期间大酬宾,全场所有商品统统1折甩卖。'
];
$arr[1] = [
    'subject' => '注册会员送100金币',
    'emailAddress' => 'bxxxx@example.com',
    'body' => '邮件内容'
];
$arr[2] = [
    'subject' => '国庆大酬宾,全场1折',
    'emailAddress' => 'cxxxxx@example.com',
    'body' => '邮件内容2'
];
      
foreach ($arr as $k=>$v) {
    $redis->rpush("mailerlist", json_encode($v, JSON_UNESCAPED_UNICODE));
}


$client = new Client();
$client->connect();

验证

根据上一节内容,我们应该先启动服务端,看到Swoole服务端启动好了,我们再运行客户端:

php mailClient.php

然后你可以去查看对方邮箱是否收到相关邮件。

本文中使用了redis作为简单队列,你也可以使用复杂点的队列rabbitmq。你也可以使用Crontab来做定时任务,不过它最小粒度是分钟级别的。当然对于批量发送邮件,如果你不用php的话,可以用Python或者Java,它们都有相当成熟的解决方案。