ThinkPHP 6.x 消息队列【queue】

1,283 阅读4分钟

额,第一次写 有点小紧张,不知如何去把自己的想法用文字形式表现出来。

做这个功能的业务场景缘由是:
1、很多业务需要处理,例如我们做完一系列的业务逻辑之后需要发送公众号消息或者小程序消息或者短信给用户,或者秒别的耗时长的操作,而且触发给的用户可能不止一位,当然我们的业务走完已经耗时挺长的了,如果不剥离消息推送模块出来的话再让我们的程序继续等待每一条消息的发送,这样是不是就会造成程序等待时间过长?客户端响应时间过长?漫长的等待还可能会造成链接中断程序意外结束。
2、因此就有了这个消息队列的用法,来解决次问题。
3、 一般来说,可以抽离的任务具有以下的特点:

  • 允许延后|异步|并行处理 (相对于传统的 即时|同步|串行 的执行方式)
    • 允许延后
      抢购活动时,先快速缓冲有限的参与人数到消息队列,后续再排队处理实际的抢购业务;
    • 允许异步
      业务处理过程中的邮件,短信等通知
    • 允许并行
      用户支付成功之后,邮件通知,微信通知,短信通知可以由多个不同的消费者并行执行,通知到达的时间不要求先后顺序。
  • 允许失败和重试
    • 强一致性的业务放入核心流程处理
    • 无一致性要求或最终一致即可的业务放入队列处理 thinkphp-queue 是thinkphp 官方提供的一个消息队列服务,它支持消息队列的一些基本特性:
  • 消息的发布获取执行删除重发失败处理延迟执行超时控制
  • 队列的多队列内存限制启动停止守护
  • 消息队列可降级为同步执行
1. 在框架中加载queue扩展
composer require topthink/think-queue
1. 框架中的 config/queue.php

这里我使用的是Redis 驱动

/* queue.php */ // 文件名
return [
    'default'     => 'redis',
    'connections' => [
        // 'sync'     => [
        //     'type' => 'sync',
        // ],
        // 'database' => [
        //     'type'       => 'database',
        //     'queue'      => 'default',
        //     'table'      => 'jobs',
        //     'connection' => null,
        // ],
        'redis'    => [
            'type'       => 'redis',
            'queue'      => 'default',
            'host'       => env('redis.host', '127.0.0.1'),
            'port'       => env('redis.port', 6379),
            'password'   => env('redis.password', ''),
            'select'     => 0,
            'timeout'    => 0,
            'persistent' => false,
        ],
    ],
    'failed'      => [
        'type'  => 'none',
        'table' => 'failed_jobs',
    ],
    // 任务归属的队列名称-- 这里我就用数据库的名称来作为队列昵称了,懒得自己去定义
    'jobQueueName'=> env('database.database')
];
生产者类
namespace app\lib\job;

use app\lib\exception\ExceptionHandle;
use app\lib\exception\ParameterException;
use think\Exception;
use think\facade\Queue;

class TaskWriteQueue
{
    private $jobData;

    /**
     * TaskWriteQueue constructor.
     * @param array $jobData 数据体
     * @param string $sendType weChatPublic|SMS
     * @param string $templateID 对应的模板ID
     */
    public function __construct($jobData = array(), $sendType = 'weChatPublic', $templateID = '')
    {
        $jData = call_user_func_array(
            ['app\lib\job\TaskWriteQueue', 'organizeData'],
            [$jobData, $sendType, $templateID]
        );
        $this->jobData = $jData;
    }

    private function organizeData($jobData, $sendType, $templateID)
    {
        $newData = [];
        foreach ($jobData as $k => $v) {
            $newData[] = [
                'data' => ['toObj' => $k, 'toData' => $v],
                'templateID' => $templateID,
                'sendType' => $sendType ? $sendType : null
            ];
        }
        return $newData;
    }

    public function actionWithTaskJob()
    {
        # 1.当前任务将由哪个类来负责处理。
        # 当轮到该任务时,系统将生成一个该类的实例,并调用其 fire 方法
        $jobHandler = "app\lib\job\TaskDestruction";
        # 2.当前任务归属的队列名称,如果为新队列,会自动创建
        // $jobQueueName = "API-TEST";
        $jobQueueName = config('queue.jobQueueName');
        # 3.当前任务所需的业务数据 . 不能为 resource 类型,其他类型最终将转化为json形式的字符串
        # ( jobData 为对象时,需要在先在此处手动序列化,否则只存储其public属性的键值对)
        # 4.将该任务推送到消息队列,等待对应的消费者去执行
        try {
            $i = 0;
            foreach ($this->jobData as $v) {
                $isPushed = Queue::push($jobHandler, $v, $jobQueueName);
                # database 驱动时,返回值为 1|false  ;   redis 驱动时,返回值为 随机字符串|false
                if ($isPushed !== false) {
                    $i++;
                } else {
                    echo 'Oops, something went wrong.';
                }
            }
            if ($i) {
                return true;
            }
        // } catch (Exception $e) {
        } catch (ExceptionHandle $e) {
            throw new ParameterException(['msg' => '消息写入失败']);
        }
    }
}
消费者类
namespace app\lib\job;

use app\lib\model\ErrMsg;
use think\queue\Job;

class TaskDestruction
{
    public function fire(Job $job, $jobData)
    {
        /*print_r('hhhh' . $jobData);
        echo $jobData['data']['toObj'];
        echo $jobData['sendType'];
        exit();*/
        if (empty($jobData['data']['toObj'])) {                                 // 如果推送对象为空就没必要推送了,直接返回并删除任务。
            $this->setError(json_encode($jobData['data']), $jobData['sendType']);    // 写入异常记录表
            $job->delete();                                             // 删除任务
            print('【' . date('Y-m-d H:i:s') . '】' . $jobData['sendType'] . "当前队列无消费对象,已记录errMsg。" . "\r\n");
            return false;                                               // 终止
        }
        switch ($jobData['sendType']) {
            // 发送公众号消息
            case 'weChatPublic':
                $weChatObj = (new WeChatTemplate($jobData['templateID']))->OBJ;
                $weRes = $this->weChatPublic($weChatObj, $jobData['data']);
                $weRData = json_decode($weRes, true); // 转换为数组
                if ($weRData['errcode'] !== 0) {
                    $this->setError($weRes, 'sendError');
                }
                $job->delete();
                break;
            // 发送短信消息
            case 'SMS':
                break;
        }
        // 任务执行超过2次,则删除任务
        if ($job->attempts() > 2) {
            print("Hello Job has been retried more than 5 times!" . "\n");
            $job->delete();
        }
        //在此执行具体任务,示例只简单打印执行次数
        #$count = $job->attempts();

        #Log::write('run the ' . $count . ' round');
//        //如果任务执行成功后 记得删除任务,不然这个任务会重复执行,直到达到最大重试次数后失败后,执行failed方法
//        $job->delete();
        // 也可以重新发布这个任务,此为延时2秒发布
//        $job->release(2);
    }

    public function failed($data)
    {
        halt(6666);
        // ...任务达到最大重试次数后,失败了
    }

    /**
     * 记录异常错误
     * @param $data
     * @param string $type
     */
    public function setError($data, $type = "weChatPublic")
    {
        (new ErrMsg())->save(['content' => $data, 'type' => $type]);
    }

    /**
     * 推送模板消息
     * @param $tmpObj
     * @param $data
     * @return mixed
     */
    public function weChatPublic($tmpObj, $data)
    {
        return $tmpObj->index($data['toObj'], $data['toData']);
    }
}

这样我们就完成了代码的逻辑,也就是发布消息,消费消息。

启动队列 测试

接下来我们启动这个队列
启动队列有两种方式

  • work
  • listen work 方式启动。这种方式是单进程运行。如果你更新了代码需要手动重启队列:
&> php think queue:work --queue xxxx //我们定义的队列名称

listen 方式启动。这种方式是单进程运行。如果你更新了代码需要手动重启队列:

&> php think queue:listen --queue xxxx //我们定义的队列名称

我更推荐listen方式来运行,这种方式更新代码后也不需要手动重启

测试时候可以打开命令行终端进行测试,需要切换到项目所在的根目录进行
在根目录下执行以上命令

工具:

宝塔面板搭建站点

  1. Redis 缓存数据库
  2. Supervisor 管理器

image.png

php think queue:listen --queue xxxx

Supervisor 管理器 新增守护进程 image.png