TP5学习笔记九 队列

依赖

redis-server

redis php扩展

参考此教程

安装

composer

最好通过composer安装 composer require topthink/think-queue 1.1.4

手动安装

https://github.com/top-think/think-queue
把包 下载下来 放入 项目的Vendor里

然后在 common.php 公共函数库里 加入这么一句:

require './vendor/topthink/think-queue/src/common.php';

当你在命令行里 切换到项目根目录后, 执行 php think queue:work -h

能出现以下 结果 就表示think-queue的 安装好了

[图片上传中...(image-5d191d-1548303820123-0)]

配置

配置文件位于 application/extra/queue.php

公共配置

[
    'connector'=>'sync' //驱动类型,可选择 sync(默认):同步执行,database:数据库驱动,redis:Redis驱动,topthink:Topthink驱动
                   //或其他自定义的完整的类名
]

驱动配置

各个驱动的具体可用配置项在think\queue\connector目录下各个驱动类里的options属性中,写在上面的queue配置里即可覆盖

这里我们只需记住 redis 和sync类型的就好了,因为database是官方不推荐的,topthink 完全不开放。

下面是我的配置:

return [
    'connector'=>'redis',
    // 'connector'=>'sync',
    'expire'     => 0,
    'default'    => 'default',
    'host'       => '127.0.0.1',
    'port'       => 6379,
    'password'   => '',
    'select'     => 0,
    'timeout'    => 0,
    'persistent' => false
];

使用

队列的特点是 先进先出,本质是将同步的调用,转成别的进程里的 异步任务。

think-queue 是基于cli 的php任务管理。

入队

首先,入队是queue的 push 和delay 两个方法。但是 两个方法的名称和参数不一样。因此,我的建议是 再包一层,封装成一个方法。

    /**
     * 添加队列任务
     *
     * @param string $job_name 队列执行的类路径 不带走类fire方法 带@方法 走类@的方法
     * @param array $data 传入数据
     * @param mixed $queue_name 队列名 null 或字符串
     * @param integer $delay  延迟执行的时间  单位秒
     * @return void
     */
    public function push_job($job_name, $data, $queue_name = null, $delay = 0){
        trace($queue_name);
        config('default_return_type', 'json');
        $class_name = \strstr($job_name, '@', true);
        if(class_exists($class_name)){
            if($delay > 0){
                $ret = \think\Queue::later($delay, $job_name, $data, $queue_name);
            }else{
                trace($job_name);
                $ret = \think\Queue::push($job_name, $data, $queue_name);
            }
            trace(sprintf("加入任务%s, 时间%s", $job_name, datetime()));
            return $ret;
        }
        return $this->error('job类 '.$job_name.'不存在');
    }

think-queue 支持普通队列和延迟队列两种,分别对应push 和delay。

然后 这两个方法都有共同参数 job,data, $queue。job是队列入队的任务执行路径,比如’A\B@c’

如果队列任务类只有一个方法需要队列执行,请定义为fire, job里只要指向类路径就可以了。如果有多个job里 要@后接方法名。

如果是push的话返回的是一个队列任务的id ,延迟的话返回null

执行队列任务

队列任务 类 ,最好以job为后缀,放入相关job目录里。如果就一个任务,将队列执行方法 直接命名为fire, 多个子任务的话,方法可以定义多个。在入队时 用@指定消费的任务+方法>

且传递过来的 参数基本 Job $job, $data

举例:

    public function test(Job $job, $data){
        // trace("正在执行队列:{$job->getName()}");
        // $time = rand(0, 4);
        // sleep($time);
        $isJobDone = $this->send($data);
        trace($data);
        if ($isJobDone) {
            //如果任务执行成功, 记得删除任务
            $job->delete();
            trace("队列执行完成:".__FUNCTION__." ". datetime());
        } else {
            $try_nums = $job->attempts();
            trace("报警内容:".__FUNCTION__.",报警失败{$try_nums }次, 执行时间:". datetime());
            // $job->release();
            // $job->delete();
        }
    }

队列的数据data 用户自己定义,通常是一些查询需要的条件,或者第三方服务需要的参数。

这里执行的时候注意一定不要抛异常,容易把队列进程搞挂了。

出队

当队列任务执行完毕,最好将任务删除掉。不然堆积在队列里,不断重试。

$job->delete()

再入队

$job->release()

当默认开启队列 最大重试次数参数时, 如果一直失败 一直release 的话他会无限重试。

所以最好设置一个最大重试次数。

整个任务失败

当任务重试超过最大尝试次数后, 最后会执行当前任务类的 failed方法 但是参数只有$data, 当一个类里如果有多个子任务的话 不好区分哪个任务。

失败事件

首先,我们添加 queue_failed 事件标签, 及其对应的回调方法

// 文件路径: \application\tags.php
// 应用行为扩展定义文件
return [
    // 应用初始化
    'app_init'     => [],
    // 应用开始
    'app_begin'    => [],
    // 模块初始化
    'module_init'  => [],
    // 操作开始执行
    'action_begin' => [],
    // 视图内容过滤
    'view_filter'  => [],
    // 日志写入
    'log_write'    => [],
    // 应用结束
    'app_end'      => [],

    // 任务失败统一回调,有四种定义方式
    'queue.failed'=> [

         // 数组形式,[ 'ClassName' , 'methodName']
        ['application\\behavior\\MyQueueFailedLogger', 'logAllFailedQueues']

         // 字符串(静态方法),'StaicClassName::methodName'
         // 'MyQueueFailedLogger::logAllFailedQueues'   

         // 字符串(对象方法),'ClassName',此时需在对应的ClassName类中添加一个名为 queueFailed 的方法
         // 'application\\behavior\\MyQueueFailedLogger'

         // 闭包形式
         /*
         function( &$jobObject , $extra){
             // var_dump($jobObject);
             return true;
         }
         */
    ]
];

这里,我们选择数组形式的回调方式,新增 \application\behavior\MyQueueFailedLogger 类,添加一个 logAllFailedQueues() 方法

<?php
/**
 * 文件路径: \application\behavior\MyQueueFailedLogger.php
 * 这是一个行为类,用于处理所有的消息队列中的任务失败回调
 */

namespace application\behavior;

class MyQueueFailedLogger {

    const should_run_hook_callback = true;

    /**
     * @param $jobObject   \think\queue\Job   //任务对象,保存了该任务的执行情况和业务数据
     * @return bool     true                  //是否需要删除任务并触发其failed() 方法
     */
    public function logAllFailedQueues(&$jobObject){

        $failedJobLog = [
            'jobHandlerClassName'   => $jobObject->getName(), // 'application\index\job\Hello'
            'queueName' => $jobObject->getQueue(),             // 'helloJobQueue'    
            'jobData'   => $jobObject->getRawBody()['data'],  // '{'a': 1 }'
            'attempts'  => $jobObject->attempts(),            // 3
        ];
        var_export(json_encode($failedJobLog,true));

           // $jobObject->release();     //重发任务
          //$jobObject->delete();         //删除任务
          //$jobObject->failed();     //通知消费者类任务执行失败

        return self::should_run_hook_callback;         
    }
}

需要注意该回调方法的返回值:

  • 返回 true 时,系统会自动删除该任务,并且自动调用消费者类中的 failed() 方法
  • 返回 false 时,系统不会自动删除该任务,也不会自动调用消费者类中的 failed() 方法,需要开发者另行处理失败任务的删除和通知。

最后,在消费者类中,添加 failed() 方法

/**
 * 文件路径: \application\index\job\HelloJob.php
 */

/**
 * 该方法用于接收任务执行失败的通知,你可以发送邮件给相应的负责人员
 * @param $jobData  string|array|...      //发布任务时传递的 jobData 数据
 */
public function failed($jobData){
    send_mail_to_somebody() ; 

    print("Warning: Job failed after max retries. job data is :".var_export($data,true)."\n"; 
}

这样,就可以做到任务失败的记录与告警

启动队列监听服务

  • Work 模式

    php think queue:work \
    --daemon            //是否循环执行,如果不加该参数,则该命令处理完下一个消息就退出
    --queue  helloJobQueue  //要处理的队列的名称
    --delay  0 \        //如果本次任务执行抛出异常且任务未被删除时,设置其下次执行前延迟多少秒,默认为0
    --force  \          //系统处于维护状态时是否仍然处理任务,并未找到相关说明
    --memory 128 \      //该进程允许使用的内存上限,以 M 为单位
    --sleep  3 \        //如果队列中无任务,则sleep多少秒后重新检查(work+daemon模式)或者退出(listen或非daemon模式)
    --tries  2          //如果任务已经超过尝试次数上限,则触发‘任务尝试次数超限’事件,默认为0
    
    
  • Listen 模式

    php think queue:listen \
    --queue  helloJobQueue \   //监听的队列的名称
    --delay  0 \         //如果本次任务执行抛出异常且任务未被删除时,设置其下次执行前延迟多少秒,默认为0
    --memory 128 \       //该进程允许使用的内存上限,以 M 为单位
    --sleep  3 \         //如果队列中无任务,则多长时间后重新检查,daemon模式下有效
    --tries  0 \         //如果任务已经超过重发次数上限,则进入失败处理逻辑,默认为0
    --timeout 60         //创建的work子进程的允许执行的最长时间,以秒为单位
    
    

    可以看到 listen 模式下,不包含 --deamon 参数,原因下面会说明

注意点

安全

redis 默认是没密码的,可以去服务器配置 auth password

模式区别

两者都可以用于处理消息队列中的任务

区别在于:

  • 执行原理不同

  • work 命令是单进程的处理模式。

    按照是否设置了 --daemon 参数,work命令又可分为单次执行和循环执行两种模式。

    • 单次执行:不添加 --daemon参数,该模式下,work进程在处理完下一个消息后直接结束当前进程。当不存在新消息时,会sleep一段时间然后退出。
    • 循环执行:添加了 --daemon参数,该模式下,work进程会循环地处理队列中的消息,直到内存超出参数配置才结束进程。当不存在新消息时,会在每次循环中sleep一段时间。
  • listen 命令是 父进程 + 子进程 的处理模式。

    listen命令所在的父进程会创建一个单次执行模式的work子进程,并通过该work子进程来处理队列中的下一个消息,当这个work子进程退出之后,listen命令所在的父进程会监听到该子进程的退出信号,并重新创建一个新的单次执行的work子进程

  • 退出时机不同

  • work 命令的退出时机在上面的执行原理部分已叙述,此处不再重复

  • listen 命令中,listen所在的父进程正常情况会一直运行,除非遇到下面两种情况:

    • 创建的某个work子进程的执行时间超过了 listen命令行中的--timeout 参数配置,此时work子进程会被强制结束,listen所在的父进程也会抛出一个 ProcessTimeoutException 异常并退出。开发者可以选择捕获该异常,让父进程继续执行,也可以选择通过 supervisor 等监控软件重启一个新的listen命令。
    • listen 命令所在的父进程因某种原因存在内存泄露,则当父进程本身占用的内存超过了命令行中的 --memory 参数配置时,父子进程均会退出。正常情况下,listen进程本身占用的内存是稳定不变的。
  • 性能不同

    • work 命令是在脚本内部做循环,框架脚本在命令执行的初期就已加载完毕;

    • 而listen模式则是处理完一个任务之后新开一个work进程,此时会重新加载框架脚本。

      因此: work 模式的性能会比listen模式高。

      注意:当代码有更新时,work 模式下需要手动去执行 php think queue:restart 命令重启队列来使改动生效;而listen 模式会自动生效,无需其他操作。

  • 超时控制能力

    • work 模式本质上既不能控制进程自身的运行时间,也无法限制执行中的任务的执行时间。

      举例来说,假如你在某次上线之后,在上文中的 \application\index\job\Hello.php 消费者的fire方法中添加了一段死循环 :

      public function fire(){
         while(true){ //死循环
             $consoleOutPut->writeln("<info>I am looping forever inside a job.</info> \n");
             sleep(1);
         }
      }  
      
      

      那么这个循环将永远不能停止,直到任务所在的进程超过内存限制或者由管理员手动结束。这个过程不会有任何的告警。更严重的是,如果你配置了expire ,那么这个死循环的任务可能会污染到同样处理 helloJobQueue 队列的其他work进程,最后好几个work进程将被卡死在这段死循环中。详情后文会说明。

      work 模式下的超时控制能力,实际上应该理解为 多个work 进程配合下的过期任务重发能力。

    • 而 listen命令可以限制其创建的work子进程的超时时间。

      listen 命令可通过 --timeout 参数限制work子进程允许运行的最长时间,超过该时间限制仍未结束的子进程会被强制结束;

    • 这里有必要补充一下 expire 和 timeout 之间的区别:

      • expire 在配置文件中设置,timeout 在 listen命令 的命令行参数中设置,而且,expire 和 timeout 是两个不同层次上的概念:

      • expire 是指任务的过期时间。这个时间是全局的,影响到所有的work进程。(不管是独立的work命令还是 listen 模式下创建的的work子进程) 。expire 针对的对象是 任务。

      • timeout 是指work子进程的超时时间。这个时间只对当前执行的listen 命令有效。timeout 针对的对象是 work子进程。

  • 使用场景不同

    根据上面的介绍,可以看到,

    work 命令的适用场景是:

    • 任务数量较多
    • 性能要求较高
    • 任务的执行时间较短
    • 消费者类中不存在死循环,sleep() ,exit() ,die() 等容易导致bug的逻辑

    listen命令的适用场景是:

    • 任务数量较少
    • 任务的执行时间较长(如生 成大型的excel报表等),
    • 任务的执行时间需要有严格限制

调试方式

首先,队列的日志在runtime里cli.log 而不是web的log

因此最好的方式 时自定义一个日志统一存储位置。

然后,队列由两部分组成,业务代码和队列部分。

辅助终端神器vscode

编辑代码时直接 ctrl+` 打开终端 可以开启多个终端 添加多个队列

业务测试

最方便的方式是本地将队列改为同步, 将队列的任务从推入到消费。都给测试一便。

确保队列业务逻辑部分没有异常。

队列的测试

主要测试 业务执行失败后,会不会无限循环入队列,队列失败后,有没有进入报警和写入错误日志。

还有队列进程的内存,是否会挂掉。

思考

队列传送数据格式的定义?

什么时候该用队列重试,什么时候直接报警让技术查问题? 是不是应该在告警时留有手动补救执行队列的方式的url?

延迟队列是不是 sleep设为0 才精确

大队列数据时 是不是多几个进程能加速?

参考文档:

think-queue 笔记

官方文档

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,825评论 4 377
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,887评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,425评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,801评论 0 224
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,252评论 3 299
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,089评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,216评论 2 322
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 31,005评论 0 215
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,747评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,883评论 2 255
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,354评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,694评论 3 265
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,406评论 3 246
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,222评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,996评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,242评论 2 287
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 36,017评论 2 281

推荐阅读更多精彩内容