使用 Laravel 实现阿里云短信服务队列

更新日期: 2019-07-08 阅读次数: 17670 字数: 1660 分类: Laravel

首先,基于 Laravel 5.2 实现一个任务队列,用于存储待发送短信的相关信息,及 seeder/worker 的处理逻辑

  • 短信模板 ID
  • 模板参数
  • 短信签名
  • 目标手机号码

创建存储任务的数据表

第一步,首先创建表 (表结构是 Laravel 默认的)

php artisan queue:table
php artisan queue:failed-table
php artisan migrate

第二个表 failed_jobs 是存储失败任务的

CREATE TABLE `jobs` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  # 这是否意味着可以有多个 queue, 以名字区分?是的,可以在程序中指定
  `queue` varchar(255) COLLATE utf8_unicode_ci NOT NULL,   
  # payload 是啥,难道是将逻辑代码写入 payload? worker 怎么识别 payload?  
  # 逻辑代码在 app/Jobs/XXX.php 的 handle 中,payload 存错的应该是 dispatch 的参数      
  `payload` longtext COLLATE utf8_unicode_ci NOT NULL, 
  `attempts` tinyint(3) unsigned NOT NULL,
  # reserve 预定跟 available 有什么区别?                                      
  `reserved_at` int(10) unsigned DEFAULT NULL,
  `available_at` int(10) unsigned NOT NULL,
  `created_at` int(10) unsigned NOT NULL,
  PRIMARY KEY (`id`),
  KEY `jobs_queue_reserved_at_index` (`queue`,`reserved_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

CREATE TABLE `failed_jobs` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  # 这是啥意思?从 config/queue.php 来看,connection 代表存储 jobs 的方式,例如,database/sync/redis
  # 但是这玩意不是已经在 .env 中指定了么,为何还要说明一次?
  `connection` text COLLATE utf8_unicode_ci NOT NULL, 
  `queue` text COLLATE utf8_unicode_ci NOT NULL,
  `payload` longtext COLLATE utf8_unicode_ci NOT NULL,
  `exception` longtext COLLATE utf8_unicode_ci NOT NULL,
  `failed_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

.env 中需要修改

QUEUE_DRIVER=database

注意,如果不修改这个配置的话,job 就不会存入数据表,而是同步执行。

创建队列的 job worker

php artisan make:job SendSms

对应的,在 app/Jobs/ 目录下出现了对应的 worker 文件 SendSms.php, 在生成的代码基础上添加 pseudo handler (worker). 这里只打印个日志意思一下

<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Log;

class SendSms implements ShouldQueue
{
    use InteractsWithQueue, Queueable, SerializesModels;

    protected $number;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct($number)
    {
        $this->number = $number;
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        Log::info('This is an sms sent to ' . $this->number);
    }
}

注意!注意!注意!

一定不要漏了 protected $xxx; 否则你会发现插入到数据表中的 payload 缺少了对应参数,从而导致 worker 在执行时报错

local.ERROR: exception 'ErrorException' with message 'Undefined property: App\Jobs\SendSms::$number'

创建队列的 job seeder

php artisan make:controller SmsController

添加逻辑

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Jobs\SendSms;

class SmsController extends Controller
{
    public function sendSms(Request $request) {
        $this->dispatch(new SendSms("13800000000"));
        return "ok";
    }
}

如何启动 worker

我们在 app/Jobs 中只是定义了如何存储任务,而执行任务的需要我们手动启动

php artisan queue:listen

如果,不执行这个命令,那么存入数据的任务永远不会被执行处理。

我的疑问是,这个服务起来之后,只会处理新到的任务,还是把历史没有处理完的任务都一次处理? 历史任务都会被处理。

jobs 表中都存储了些什么

mysql> select * from jobs \G;
*************************** 1. row ***************************
          id: 46
       queue: default
     payload: {"job":"Illuminate\\Queue\\CallQueuedHandler@call","data":{"commandName":"App\\Jobs\\SendSms","command":"O:16:\"App\\Jobs\\SendSms\":5:{s:9:\"\u0000*\u0000number\";s:11:\"13800000000\";s:6:\"\u0000*\u0000job\";N;s:10:\"connection\";N;s:5:\"queue\";N;s:5:\"delay\";N;}"}}
    attempts: 0
 reserved_at: NULL
available_at: 1481031619
  created_at: 1481031619
1 row in set (0.00 sec)

如何将 job 加入指定队列

以发送邮件为例

 public function sendReminderEmail(Request $request, $id)
    {
        $user = User::findOrFail($id);

        $job = (new SendReminderEmail($user))->onQueue('emails');

        $this->dispatch($job);
    }

onQueue 即是指定了队列,当是仍然是存错在一个数据表中,只不过是 jobs 表的 queue 字段不同而已。

这个特性非常的方便,例如我们有4个不同业务/产品/客户,我们需要独立统计,那么就可以通过指定不同的 queue 来达到统计的目的。

如何提交手机号列表

假设有 5000 个手机号,那么通过 http post 的方式,一次性提交是否可行。参考 php - What is the size limit of a post request? - Stack Overflow 对于不可控的参数长度,最好还是换个方案。

一种替代方案是

  • 在前端使用 ajax 的方式提交手机号列表,例如一次提交 10 个手机号。
  • 后台接收之后,逐个验证手机号码格式,并添加队列。避免一次处理几千个耗时过长,用户在前端等不耐烦了怎么办
  • 后台对于格式错误的手机号,后台通过 json 数据返回给前端,单独显示出来。我觉得会有不少固定电话的格式。
  • 前端对于格式错误的手机号,在单独一列给出来。或者,给出一列错误信息。我觉得这种方式比较麻烦,担心错误号码多了,用户也懒得去一个一个改。还不如发送之后集中给出显示。

推广短信

对于推广短信需要注意的事项

  • 短信模板需要单独提交审核。每次更改文案,都需要再审核
  • 单价比普通短信要贵 1 分钱
  • 短信签名也需要单独申请审核。好在阿里云的审核速度比较快,基本10分钟就能通过人工审核
  • 推广短信模板中不能使用变量。对应的 $request->setParamString("{}"); 参数写成空 array 即可。

就算短信签名完全相同,但是如果你使用了“验证码或者短信通知”的短信签名,那么会被报错

InvalidSignName.MalformedThe specified sign name is wrongly formed.

没错,阿里云的 sdk 就是写的这么 low, 错误信息让人抓狂,代码风格也烂的要死。

如何处理失败任务

默认如果不限制 retry 的次数,从测试的结果看,对应的失败任务会被无数次重试。但是是在其他任务执行之后。实现的原理是每次将 job id 置成最大。

延迟发送的方法

$job = (new SendSMSMessages($member, $message))->delay(60);

整点定时发送的时间差计算方法

Carbon::tomorrow()->startOfDay()->diffInSeconds(Carbon::now())

完成的任务会被 delete 掉么?

会的

Laravel Queue 默认的 sync driver 是何物

文档上只是说,sync driver 只适合本地使用,即开发环境。具体原理并未说明。

从 sync 这个名字上我猜测是,调用 dispatch 之后,这个 job 会被立即执行。看了几个介绍 Laravel Queue 的文章,也验证了我的这种猜测。

测试环境

Laravel 5.2

参考

关于作者 🌱

我是来自山东烟台的一名开发者,有敢兴趣的话题,或者软件开发需求,欢迎加微信 zhongwei 聊聊, 查看更多联系方式

谈笑风生

aa

谢谢大侠,你的 签名无效的 说明,解决了烦扰我半天的问题,感谢!