拒绝分散,试试任务引擎

前言:

大家做后端开发,一定都遇到过很多需求需要依靠定时任务去完成,可是不同的部门不同的开发会写在不同的项目中。定时任务也会越来越多越来越不好管理。既然大家的需求都是定时完成一些任务,只是任务内容不同,那我们就可以将管理任务执行任务这部分功能抽象出来做成一个服务。我称这样的服务为任务引擎

流程

流程大致如上图,我们可以来拆解一下。

  1. 首先需要做任务,那么就必须得有任务。写入任务其实也就是写表。这个应该需要封装成接口对外提供
  2. 根据任务状态获取出100条数据
  3. 将取出的数据遍历循环
  4. 对应单条数据的type值实例化对应的类
  5. 调用对应类上面的处理任务方法
  6. 修改任务状态
  7. 打印日志

在这整个流程中,各个需要使用任务引擎的业务方其实只需要关注,写入任务执行任务这两个事件。其余的任务引擎会自动完成

表设计、架构设计

数据表结构如下:

字段 类型 注释
id int(11) 唯一主键
bus_type int(11) 业务类型
bus_key bigint(20) 业务id
remark varchar(255) 备注
params varchar(255) 可能会用到的参数
status tinyint(1) 任务状态 1完成 2待执行 3结束 4失败重试
create_time int(11) 创建时间
update_time int(11) 更新时间

索引设计,结合业务

索引名 索引字段
idx_status_create_time INDEX cp_schedule_task(status ASC, create_time DESC)

开发框架我使用的是PHP7.3+ThinkPHP6.0
首先使用composer create-project topthink/think tp6拉取一个框架代码。
框架生成后,需要配置一些数据库连接之类的东西我就不介绍了,感兴趣的朋友可以前tp6官方手册里面查看
首先我们将需要写的代码分为三部分

  1. 添加任务
  2. 做任务
  3. 修改任务状态

首先来看添加任务
添加任务实质上就是实现一个写表的操作,封装成接口对外调用就可以了

创建模型类

class ScheduleTask extends Model
{
    protected $autoWriteTimestamp=true;//配置参数,配置之后,会自动写create_time和update_time
}

创建业务类,并且创建新增方法

/**
 * 处理任务业务类
 * Class ScheduleTaskService
 * @package app\schedule\service
 */
class ScheduleTaskService
{
 /**
     * @title 新增任务方法
     * @param $data ["bus_id"=>1234,"bus_type"=>1,"remark"=>"hhhh","params"=>"{'lll':'hhh'}"]
     * @special bus_id 和bus_type必填
     */
    public function addTask($data){
        ScheduleTask::create($data);
    }
}

创建控制器,并且创建对外API

class Task extends  BaseController
{

    /**
     * @title 新增任务接口
     */
    public function add()
    {
        $post=$this->request->param();
        //其实需要对入参做判断,这里就先省略
        $service=new ScheduleTaskService();
        $service->addTask($post);
        response("加入成功",200,[],"json");
    }

}

第二步就是任务的重点做任务
由于各个业务方的需求共同点是做任务,而任务的内容又不尽相同。所以我们需要将做这个任务给抽象出接口(interface)而任务内容由各个业务方自己实现

先创建一个对象接口,定义执行任务的方法。有了这个接口,后面业务方需要添加不同的业务都可以继承这个接口并实现run方法

interface ScheduleTaskComponent
{
    /**
     * 执行任务
     * @param $data array 从表里出的一条数据
     * @return mixed
     */
    public function run($data);
}

实现一个假装发送消息的任务消耗类

class SendMessageSchedule implements ScheduleTaskComponent
{
    /**
     * 执行任务
     * @param $data array 从表里出的一条数据
     * @return mixed
     */
    public function run($data)
    {
        echo "假装发送了一条消息".$data['bus_id'].$data['params'].PHP_EOL;
    }
}

再实现一个假装写日志的任务消耗类


class AddLogSchedule implements ScheduleTaskComponent
{

    /**
     * 执行任务
     * @param $data array 从表里出的一条数据
     * @return mixed
     */
    public function run($data)
    {
        echo "假装写了一条日志".$data['remark'].PHP_EOL;
    }
}

那么现在消耗任务的方法有了,怎么才能让任务池里的任务根据不同的业务调用不同的方法呢?这也是本文的关键。
既然是计划任务,传统的方式一般是,写一个控制器里面全部都是计划任务的业务。然后通过linux自带的crontab来定时发起curl请求。这样做方便是方便,但是还是有几个缺点~
比如linux的计划任务最小时间单位是分钟,还有就是通过curl请求会走一次公网域名解析产生回环调用。就算你直接写ip不走域名。也会对nginx造成压力。
为了解决这个问题,我就想到使用php的CLI模式去调用,这样就会直接执行php脚本,不走nginx。比较节约资源,而且更加灵活。
起初我是打算写控制器然后cli的形式调用就好,但是没想到tp6不支持这样调用了。随后我研究了下tp的文档,发现tp6应该是更希望你使用他的自定义指令。看了文档之后发现其实非常的简单

  1. 先定义一个自定义命令
php think make:command app\\schedule\\command\\scheduleStart 任务引擎启动

这里上面的命令有个小问题,和文档里有些出入,文档里说的是不需要转移也就是只需要用一个\隔开命名空间的。但是试了下有问题。我看了下源码,应该是他们的bug。已经给作者反应了,应该在下个版本会修复

随后就会帮你生成好一个用于命令行的类


生成的类中会有两个方法 一个configureexecute

configure可以配置接收参数,不过我们这次没有用。直接来看execute方法。顾名思义,这个是用来执行的方法
我们来看代码

protected function execute(Input $input, Output $output)
    {
        // 指令输出
        $output->writeln('任务引擎启动');
        while (true){//死循环
            $service = new ScheduleTaskService();
            //在表里获取状态为未执行、失败重试的100条任务
            $task100 = $service->get100Task();
            foreach ($task100 as $item) {
                $updateId=$item['id'];
                $updateRemark='';
                try {
                    $type = $item['bus_type'];
                    //工厂模式,通过type生产不同的类实例
                    $taskComponent = ScheduleTaskService::getTaskComponent($type);
                    //调用类上的run方法
                    $taskComponent->run($item);
                    $updateStatus=1;
                } catch (BusinessException $be) {//约定的业务异常
                    $updateRemark=$be->getMessage();
                    //如果异常是101就视为失败,不会重复执行
                    if ($be->getCode()=="101") {
                        //报错,不执行
                        $updateStatus=3;
                    }else{
                        //如果不是101就说明,还有的救,改为状态为再执行
                        $updateStatus=4;
                    }
                } catch (Exception $e) {//系统异常
                    //系统异常
                    Log::error($e->getMessage());
                    $updateStatus=3;//将执行状态改为失败
                    $updateRemark=$e->getMessage();
                }
                //修改任务状态
                $service->updateTask($updateId,$updateStatus,$updateRemark);
            }
            //打印本次操作了多少条
            Log::info("Command/scheduleStart 任务完成数{num}",['num'=>$task100->count()]);
            //线程挂起60秒
            sleep(60);
        }
    }

上面的代码中,有一个生产对应任务的对象的方法getTaskComponent。值得拆开讲一下
入参是一个任务的bus_type。每个type代表不同的业务,也代表了不同的类。而type和类的对应关系呢,我选择了tp内部的配置文件来做。其实也可以用户数据库记录对应关系,也可以直接冗余到每条数据上。看大家的需求

 /**
     * 获取一个任务类的实现对象
     * @param $type int 任务的typeId
     * @return ScheduleTaskComponent
     * @throws BusinessException
     */
    public static function getTaskComponent($type){
        //根据配置type获取配置
        $className=config("register.$type");
        if (empty($className)) {
            //如果没有配置,则抛出不再执行的业务异常
            Log::error("配置参数{type}在注册文件中不存在",['type',$type]);
            throw new BusinessException("101","配置参数错误");
        }
        //实例化对象
        return new $className();
    }

配置的话 我就在config文件夹下新建了一个配置


配置目录

配置内容如下

return [
    1=>"app\schedule\impl\SendMessageSchedule",//假装发消息
    2=>"app\schedule\impl\AddLogSchedule"//假装写日志
];

这样就可以通过配置取到不同业务的处理任务类。最后调用从接口继承的run方法完成调用

启动

准备4条数据
#在项目根目录使用命令启动自定义命令
 php think scheduleStart
执行结果

由于我的任务执行类就是打印一个字符串,所以能在控制台里看到


数据表里的状态

数据库里的任务状态也是修改为完成状态了。这个时候,任务引擎会持续的监听数据表里的任务。发现有任务就会去执行它。任劳任怨永不停止

本文 源码 在github上面,需要自取

结语

首先很感谢你能看到这里,本次介绍的工具只为学习使用,有很多细节还有待考究,如果需要在生产环境使用,一定要控制好入参校验和错误预警。如果你觉得我的博客对你有帮助的话,点个赞再走呗。最后在新年之际,给各位读者拜个年! 大家新年快乐~

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

推荐阅读更多精彩内容

  • 一、简历准备 1、个人技能 (1)自定义控件、UI设计、常用动画特效 自定义控件 ①为什么要自定义控件? Andr...
    lucas777阅读 5,186评论 2 54
  • 高中的时候,自己的同桌是个话痨,而且不知道为什么哪里的事情他都能听得到,所以自己莫名其妙一个比较宅,没有故事的人,...
    不像话的故事阅读 98评论 0 0
  • 父母和性别父母对于孩子如何形成性别认同有着强烈的影响,你的宝宝并不是生来就知道自己是男孩还是女孩的,或者这两个词是...
    潼宝的开心果阅读 102评论 0 0
  • Binner阅读 461评论 0 49
  • 第十三章 勇敢的向前一步 桑德伯格邀请了她女性好友们来探讨女性平等问题,这里库纳尔.莫迪是麦肯锡咨询公司顾...
    冰清_e7cc阅读 220评论 0 0