php定时任务-想要实现一个任务调度系统,看这篇文章就够了

当我读到《定时任务框架的选择》一文时,有网友留言:

我看过那么多所谓的教程,大部分都是教“如何使用工具”的,教“如何制作工具”的不多,而能教“如何模仿工具”的早已凤毛麟角。 中国软件业,缺少的是真正能“制造工具”的程序员,更不缺“使用工具”的程序员! ……”这个行业最不需要的不是“会使用XX工具的工程师”,而是“创意软件工程师”!行业里所有的工作岗位本质上都是由“创意软件工程师”提供下来的!

写这篇文章,我想和大家从头到脚谈谈任务调度。 希望您读完后能够了解实现任务调度系统的核心逻辑。

1石英

Quartz是一个Java开源任务调度框架,也是很多Java工程师接触任务调度的起点。

右图展示了任务调度的整体流程:

Quartz 的核心是三个组件。

上面代码中Quartz的JobStore是RAMJobStore,Trigger和Job都存放在显存中。

执行任务调度的核心类是QuartzSchedulerThread。

调度线程从JobStore中获取需要执行的触发器列表,并改变触发器的状态; 触发触发器,更改触发器信息(上次执行触发器的时间,以及触发器状态),并保存。 最后创建具体的执行任务对象,通过工作线程池执行任务。

拿出来说一下Quartz的集群部署方案。

Quartz的集群部署方案需要在不同数据库类型(MySQL、ORACLE)的数据库实例上创建Quartz表。 JobStore 是:JobStoreSupport。

这些解决方案是分布式的,没有节点负责集中管理。 而是采用数据库行级锁的方法来实现集群环境下的并发控制。

调度器实例首先在集群模式下获取{0}LOCKS表中的行锁,Mysql获取行锁的语句:

{0} 将替换为配置文件中默认配置的 QRTZ_。 sched_name是应用集群的实例名称,lock_name是行级锁名称。 Quartz主要有行级锁触发访问锁(TRIGGER_ACCESS)和状态访问锁(STATE_ACCESS)两种。

该架构解决了任务的分布式调度问题。 同一任务只有一个节点可以运行,其他节点不会执行该任务。 当遇到大量短任务时,各个节点频繁争夺数据库锁。 节点越多,性能就越好。 更差。

2 分布式锁模式

Quartz的集群模式可以水平扩展,也可以分布式,但需要业务方在数据库中添加相应的表,侵入性很强。

为了防止这些侵入,很多研发朋友也探索了分布式锁模式。

业务场景:对于一个电商项目php定时任务,如果用户下单后一段时间没有付款,系统会在超时后关闭订单。

一般我们会每两分钟做一次定时任务,查看前半个小时的订单,查询未付款的订单列表php定时任务,然后恢复订单中商品的库存,然后将订单设置为无效。

我们使用SpringSchedule的方法来做一个定时任务。

@Scheduled(cron = "0 */2 * * * ? ")
public void doTask() {
   log.info("定时任务启动");
   //执行关闭订单的操作
   orderService.closeExpireUnpayOrders();
   log.info("定时任务结束");
 }

当单台服务器正常运行时,考虑到高可用性,业务量会急剧增加,架构将演变成集群模式。 多个服务同时执行一个定时任务,可能会导致业务疲软。

解决方案是使用Redis分布式锁来解决任务执行过程中的此类问题。

@Scheduled(cron = "0 */2 * * * ? ")
public void doTask() {
    log.info("定时任务启动");
    String lockName = "closeExpireUnpayOrdersLock";
    RedisLock redisLock = redisClient.getLock(lockName);
    //尝试加锁,最多等待3秒,上锁以后5分钟自动解锁
    boolean locked = redisLock.tryLock(3, 300, TimeUnit.SECONDS);
    if(!locked){
        log.info("没有获得分布式锁:{}" , lockName);
        return;
    }
    try{
       //执行关闭订单的操作
       orderService.closeExpireUnpayOrders();
    } finally {
       redisLock.unlock();
    }
    log.info("定时任务结束");
}

Redis具有优异的读写性能,分布式锁比Quartz数据库行级锁更轻。 其实Redis锁也可以用Zookeeper锁代替,是一样的机制。

在大型项目中,使用:定时任务框架(Quartz/SpringSchedule)和分布式锁(redis/zookeeper)有很好的效果。

然而? 我们可以看到这些组合存在两个问题:

分布式场景下定时任务可能会空运行,但任务不能分片; 如果要手动触发任务,则必须添加额外的代码来完成。 3ElasticJob-Lite框架

ElasticJob-Lite定位为轻量级去中心化解决方案,使用jar为分布式任务提供协调服务。

在应用程序内部定义任务类,实现SimpleJob套接字,并编译自己任务的实际业务流程。

public class MyElasticJob implements SimpleJob {
    @Override
    public void execute(ShardingContext context) {
        switch (context.getShardingItem()) {
            case 0:
                // do something by sharding item 0
                break;
            case 1:
                // do something by sharding item 1
                break;
            case 2:
                // do something by sharding item 2
                break;
            // case n: ...
        }
    }
}

示例:应用程序A有5个任务需要执行,分别是A、B、C、D、E。任务E需要划分为4个子任务,应用程序部署在两台机器上。

应用程序A启动后,五个任务由Zookeeper协调并分发到两台机器上,不同的任务由QuartzScheduler分别执行。

定时任务表达式_定时任务php源码_php定时任务

本质上来说,ElasticJob底层的任务调度仍然是通过Quartz。 与Redis分布式锁或者Quartz分布式部署相比,它的优势在于可以依靠Zookeeper这个大杀器,通过负载均衡算法将任务分配给应用程序。 QuartzScheduler 容器。

从用户的角度来看,它非常简单易用。 但从架构上来看,调度器和执行器仍然在同一个应用端JVM中,容器启动后,仍然需要做负载均衡。 如果应用频繁重启,不断选择master,对分片进行负载均衡,这些都是比较繁重的操作。

另外,ElasticJob的控制台也比较粗糙。 它通过读取注册表数据来突出显示作业状态,并更新注册表数据以更改全局任务配置。

4种集中类型

中心化的原理是将调度和任务执行分离为调度中心和执行器两部分。 调度中心模块只需要负责任务调度属性和触发调度命令。 执行器接收调度命令执行具体的业务逻辑,但两者都可以进行分布式扩展。

4.1 MQ模式

先说一下我在携程推广团队接触到的第一个中心化架构。

调度中心依托Quartz集群模式,在任务调度时向RabbitMQ发送消息。 业务应用收到任务消息后,消费任务信息。

这些模型充分利用了MQ前馈的特点。 调度中心发送任务,应用端作为执行者接收并执行任务。

但这些设计强烈依赖消息队列、可扩展性和功能性,系统负载与消息队列有很大关系。 这些架构设计要求架构师非常熟悉消息队列。

4.2XXL-工作

XXL-JOB是一个分布式任务调度平台。 其核心设计目标是快速开发、易学习、轻量级、易扩展。 源代码现已开放,并连接到许多公司的在线产品线,开箱即用。

我们重点分析现成的照明:

▍网络通信server-worker模型

调度中心和执行器两个模块之间的通信是Server-Worker模式。 调度中心本身是一个SpringBoot项目,启动时会监听8080端口。

执行器启动后,会启动一个外部服务(EmbedServer)来监听9994端口,这样双方就可以互相发送命令了。

调度中心如何知道执行器的地址信息? 上图中,执行器会定期发送注册命令,以便调度中心获取在线执行器列表。

通过执行者列表,可以根据为任务配置的路由策略选择节点来执行任务。 常见的路由策略如下:

▍调度程序

调度器是任务调度系统中特别核心的组件。 XXL-JOB 的初始版本依赖于 Quartz。

不过在v2.1.0版本中,Quartz依赖被彻底去除,原本需要创建的Quartz表也被替换成了自研的表。

核心调度类是:JobTriggerPoolHelper。 调用start方法后,会启动两个线程:scheduleThread和ringThread。

首先,scheduleThread会定期从数据库中加载需要调度的任务,这本质上是基于数据库行锁来保证只有一个调度中心节点同时触发任务调度。

Connection conn = XxlJobAdminConfig.getAdminConfig()
                  .getDataSource().getConnection();
connAutoCommit = conn.getAutoCommit();
conn.setAutoCommit(false);
preparedStatement = conn.prepareStatement(
"select * from xxl_job_lock where lock_name = 'schedule_lock' for update");
preparedStatement.execute();
# 触发任务调度 (伪代码)
for (XxlJobInfo jobInfo: scheduleList) {
  // 省略代码
}
# 事务提交
conn.commit();

调度线程会根据任务的“下次触发时间”采取不同的动作:

需要立即执行的过期任务直接加载到线程池中触发执行,需要在五秒内执行的任务放在ringData对象中。

ringThread启动后,会定期从ringData对象中获取要执行的任务列表,并将其加载到线程池中触发执行。

5 巨人右臂自主研发

2018年,我有过自研任务调度系统的经历。

背景是:兼容技术团队开发的RPC框架,技术团队无需更改代码,RPC注解方法可以托管在任务调度系统中,直接作为任务执行。

在自主开发的过程中,我通读了XXL-JOB的源码,同时也从阿里云的分布式任务调度SchedulerX中学到了很多东西。

我们模仿了SchedulerX的模块,架构设计如右图:

我选择了RocketMQ源码的通信模块remoting作为自研调度系统的通信框架。 基于以下两点:

我对业界知名的Dubbo不太熟悉,而且我已经做了很多remoting的轮子,相信我能搞定;

在阅读SchedulerX1.0client的源码中,我发现SchedulerX的通信框架很多地方与RocketMQRemoting类似。 其源码中有现成的工程实现,完全就是一个宝藏。

我把RocketMQremoting模块中的name子服务代码去掉了,并做了一定程度的定制。

RocketMQ的remoting中,服务端采用的是Processor模式。

调度中心需要注册两个处理器:反弹结果处理器CallBackProcessor和脉冲处理器HeartBeatProcessor。 执行器需要注册触发任务处理器TriggerTaskProcessor。

public void registerProcessor(
             int requestCode,
             NettyRequestProcessor processor,
             ExecutorService executor);

处理器插座:

public interface NettyRequestProcessor {
 RemotingCommand processRequest(
                 ChannelHandlerContext ctx,
                 RemotingCommand request) throws Exception;
 boolean rejectRequest();
}

对于通信框架,我不需要关注通信细节,我只需要实现处理器套接字。

以TriggerTaskProcessor为例:

网络通信解决之后,调度器应该如何设计呢? 最终我选择了Quartz集群模式。 主要基于以下几个原因:

当调度量不大时,Quartz集群模式足够稳定,而且兼容原有的XXL-JOB任务; 如果用时间轮的话,就是实践经验不够,怕出问题。 另外,如何让任务由不同的调度服务(schedule-server)触发,需要一个协调器。 所以我想到了动物园管理员。 但在这种情况下,引入了一个新组件。 开发周期不能太长,我们希望尽快出成果。

自主开发版本的调度服务历时一个半月才上线。 系统运行非常稳定,开发团队接入顺利。 调度量并不大,四个月的调度总量接近4000万到5000万。

说实话,我的脑海里经常能看到自研版本的困境。 数据量大了,我可以分库分表处理,但是Quartz集群是基于行级锁模式的,所以上限注定不会太高。

为了解除苦恼,我写了一个轮子DEMO,看看是否有效:

去掉内置的注册中心,由调度服务(schedule-server)管理会话; 引入zookeeper通过zk来协调调度服务。 而且HA机制非常粗糙,相当于一个任务调度服务运行,另一个服务备用; Quartz被时间轮替代(参考Dubbo中的时间轮源码)。

该Demo版本可以在开发环境中运行,但有很多细节需要优化。 它只是一个玩具,没有机会在生产环境中运行。

最近看了阿里云的一篇文章《如何通过任务调度实现百万规则上报》,SchedulerX2.0的高可用架构如右图:

文章中提到:

每个应用都会做三备份,通过ZK抢锁,一主两备。 如果一台服务器挂掉了,就会进行故障转移,其他服务器会接管调度任务。

从结构上看,自主研发的任务调度系统并不复杂。 它实现了XXL-JOB的核心功能,也兼容技术团队的RPC框架,但没有实现工作流和mapreduce分片。

SchedulerX基于升级到2.0后的新Akka框架。 这些框架可以称为高性能工作流引擎,实现进程间通信,减少网络通信代码。

在我监督的开源任务调度系统中,PowerJob也是基于Akka框架,同时也实现了工作流和MapReduce执行模式。

我对PowerJob特别感兴趣,学习练习后我也会输出相关文章,敬请关注。

6 技术选型

首先,我们将任务调度开源产品和商业产品SchedulerX放在一起,生成一个对比表:

Quartz和ElasticJob本质上仍然属于框架层面。

中心化产品在架构上越来越清晰,调度层面也更加灵活,可以支持更复杂的调度(mapreduce动态分片、工作流)。

XXL-JOB从产品层面已经做到了简单、开箱即用,调度模式可以满足大多数研发团队的需求。 简单易用+可玩,所以很受大家欢迎。

虽然每个技术团队的技术储备不同,面临的场景也不同,所以技术选型不能一概而论。

无论使用哪种技术,在编译任务业务代码时仍然需要注意两点:

7 到最后

2015年似乎是非常有趣的一年。 两种不同类型的任务调度项目 ElasticJob 和 XXL-JOB 都是开源的。

在XXL-JOB源码中,还有开源中国徐学礼老师的动态截图:

刚刚写的任务调度框架,web动态管理任务,实时生效,暖心。 不出意外的话,今天下午就会推送到git.osc了。 哈哈,我们下楼去炒点面加个荷包蛋庆祝一下吧。

听到这个截图,内心深处其实有一种感同身受,下巴不禁上扬。

我又想起了:2016年,ElasticJob的作者张亮先生开源了Sharding-jdbc。 我在github上创建了一个私有项目,参考sharding-jdbc源码,自己实现了分库分表的功能。 第一个类的名称为:ShardingDataSource,时间固定为2016/3/29。

我不知道如何定义“创意软件工程师”,但我相信,一个有好奇心、勤奋、乐于分享、乐于帮助别人的工程师,运气绝对不会太差。

如果您觉得对您有帮助,请给作者点个“赞”和“收藏”,我们上期再见。