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

更新日期: 2019-07-08 阅读次数: 13402 字数: 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

参考

谈笑风生

aa

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

爱评论不评论

近期节日

2020年08月15日 日本投降日
2020年08月22日 处暑
2020年08月25日 七夕
2020年09月02日 中元节
2020年09月03日 抗日胜利纪念日
2020年09月07日 白露
2020年09月08日 国际扫盲日
2020年09月10日 教师节
2020年09月16日 国际臭氧层保护日
2020年09月16日 世界清洁地球日
2020年09月18日 "九一八"事变纪念日
2020年09月20日 国际爱牙日
查看更多节日