PHP--简单实现多个定时调度任务配置

最近回顾

近期在开发一个新项目时,多个过程需要与第三方平台交互,由于其回调是众所周知的不可靠,在接收回调的同时,需要主动查询结果。那么理所当然的,定时器触发任务又成了必需。

其实在2018年包括之前时间,也有使用总结过自己认为的异步方式和如何使用redis来实现定时任务的并发锁。

先来谈谈日常中使用较多的定时任务方式。

  1. 如果是较为关键的业务,crontab专门设置一行用于PHP命令行运行或HTTP请求,目前我们主要以HTTP为主,这对多机器的负载均衡分配更好,且crontab的时间触发方式已经满足了要求,若想要实现秒级的任务触发,一个简单的sh程序即可;
  2. 另外crontab触发一段脚本执行逻辑,在此脚本文件内,以单线程方式陆续运行0-N个需要执行的任务;
  3. 以前也描述过,通过curl的CURLOPT_TIMEOUT配置项,以浪费1秒钟的代价实现某相关业务的伪异步。

简单列举以上三种方式,各有各的不好。
① 任务不多也倒还好,若存在5个、10个,甚至更多的任务需要定时触发,维护起来并不方便。
② 一个脚本单线程运行N个任务,太容易由于某个任务运行错误或运行时间超长导致后面的任务异常了。单就目前来观察,有的任务必须在凌晨4点前执行完毕,但是前面也有个时间要求度高的程序走着走着就超过了约定,这可不就歇了嘛,更不要说由于某个人的代码bug,导致抛错误被迫终止,一堆任务没排上队。另外又可能因为日志系统的疏落,不同功能日志夹杂在一块儿,出错定位极其不方便。
③ 必须“点对点”触发,用户的一个请求延伸出新请求来执行业务逻辑。这样问题也来了,如果不巧业务执行异常,或由于第三方等不可控系统的停摆,业务未执行完毕,那赶路一半没成功的请求可永远不会再来一遍的。更不幸的是,很多任务是需要延迟并且多次执行的,这总不能真while+sleep到底吧。又由于其“点对点”的特性,一个HTTP新增HTTP,占用了apache、nginx的连接数,无形中削弱了整个系统并发健壮能力。

这次的项目又重新引起了我的思考。我负责的模块类似与一个中间件的项目,前接与APP通信的接口API,后连第三方平台。既要前推各种通知,又要后查所有结果。任务数量多,第一个方案不能满足;任务各自无关联性,同时优先级也都高,如第二个方案不能满足;推送若失败,需间隔一定时间重复的推,查询若失败,需根据2的次方时间逐级延长再重新操作,这样一来第三个方案也提不上台面。


crontab一次配置,多次有效

最初参考网上TP5如何实现定时任务,其实基本介绍的都是老套路,crontab设置触发command,参考意义不是很大。
接着参考Laravel定时任务,诶发现有点不一样。crontab触发到一个Kernel文件,这没有什么不同。但是其Shedule的实现大大增长了我的见识,居然组装了everyMinute()等指定时间的方法,可以达到每分钟、每小时、指定周几的精度触发业务,withoutOverlapping()基于缓存文件锁方式来避免业务并发执行。那么仍存在的问题是,一个任务的耗时导致下一个任务的等待。workerman倒是能设置定时器,不过一个进程只能启动一个,总不能一个任务对应到一个进程吧?另外reload仅能更新业务文件,主启动文件(控制进程数量+定时器数量)需完全关闭后再开启,我个人觉得总差了那么点感觉。当然,我对workerman等命令行创建进程工具的了解非常非常浅陋。

然后呢,参考各方面资料,我已初步找到了思路,不谈中间的思考过程,最终我选定的方式是,crontab设置1分钟触发一个指定脚本任务,在Mysql中新增一张表,语句如下:

CREATE TABLE `schedule_plan` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '定时任务调度表',
  `route` varchar(50) NOT NULL COMMENT '路由地址',
  `is_serial` tinyint(4) NOT NULL DEFAULT '1' COMMENT '脚本执行堵塞标记0:并行1:串行',
  `description` varchar(100) NOT NULL COMMENT '定时脚本描述说明',
  `url` varchar(100) NOT NULL COMMENT '定时任务URL地址',
  `type` tinyint(4) NOT NULL COMMENT '类型0:定点时间1:间隔时间',
  `execute_area` varchar(10) NOT NULL DEFAULT '-1' COMMENT '定时器执行小时区间-1:无限制|7-15(7-15点执行)',
  `hour_time` tinyint(4) NOT NULL DEFAULT '-1' COMMENT '小时',
  `minute_time` tinyint(4) NOT NULL DEFAULT '0' COMMENT '分钟',
  `week_day` tinyint(4) NOT NULL DEFAULT '-1' COMMENT '周日、周一……周六|0-6',
  `month_day` tinyint(4) NOT NULL DEFAULT '-1' COMMENT '月-日数',
  `state` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态0:正常1:禁用',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `last_time` datetime DEFAULT NULL COMMENT '上次执行时间',
  `error_msg` varchar(100) DEFAULT NULL COMMENT '规则不符合原因',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

原理

每次定时器脚本查询Mysql的待运行任务,判断每个任务的执行条件,结合curl的CURLOPT_TIMEOUT配置项,每一个待执行的任务对应一个HTTP请求,之间互不干扰,也加以通过Redis的setnx命令来加锁避免任务的重复执行。
谈谈目前我觉得的好处,可通过仅配置一个crontab并发运行多个任务,每一个任务的HTTP触发耗时1秒,若后续担心任务数量增加,也很方便在crontab增加主控制脚本定时任务,并在Mysql表中增设一个type字段来对应两~多个主脚本,各自触发所属任务。定时任务通过操作网站的方式添加,设置串行和并行方式。

更多的内容就以代码形式说明吧。代码地址:php-schedule


守护进程实现任务调度

话说今天翻阅资料又让我眼前一亮,据说是有赞的设计思路,实现延迟队列触发任务。核心是以redis存储需执行的任务信息,PHP创建的守护进程各司其职。客户端以HTTP的方式发送到A进程将待异步调度任务存入到待执行队列(会携带允许被执行的时间点)。B进程持续扫描redis待执行队列比对时间等,若满足条件,则转移到可执行队列。C进程从可执行队列中又获取到数据了,以HTTP的方式请求已预设好的回调地址,实现任务的异步逻辑。这种方案将异步调度中心分离出来自成一体,让它专注于本身,专注于解决:一个任务应该何时被执行和如何通知执行,HTTP只是一种通知上的形式,我们更关注是真正的业务逻辑实现,被完完全全的解放出来了。比较之下,扫表的操作会增加不少负担,按需处理才是值得推崇的。

有赞延迟队列设计说明

有其他coder已经实现的开源项目,代码地址:php-delayqueue


总结

综上,我个人认为,不同的项目各自存在不同职责的定时任务脚本,我们应该试着关注如何能更好更方便的运行、管理,这真的是一件非常美妙和有趣的事情。

题外话,master-worker的进程模型有看到好多了,比如nginx、php-fpm、workerman、及刚讲的基于PHP实现的延迟队列,慢慢学慢慢学。

你可能感兴趣的:(PHP)