1、前言
在开发过程中,很多业务都存在这样的场景:
- 商城订单:商城订单长时间处于未支付状态,则自动取消
- 打卡提醒:在指定任意时间后触发某一个打卡的任务提醒
- 预约提醒:当我们预约医生时,需要在预约时间前提醒我们
以上业务场景都有一个共同的特点,那就是需要一段时间后触发某一个任务,那么在开发过程中应该怎么去实现类似的的业务场景呢?可以考虑自己实现一个可以延时触发任务的组件
2、延时组件是什么
延时组件是什么呢?举个简单的例子,闹钟可以设置在任意一个时间打铃,如果有多个人设置了这个闹钟,那么闹钟就会在不同的时间执行打铃。其实延时组件需要实现的也是类似的事情,只不过是把打铃换成其它任意的任务,比如打卡的提醒,或者超时订单的取消。
3、场景梳理
现在来看看实现组件需要做什么,首先需要支持任务的提交,然后需要有一个调度器,按照任务的执行时间完成任务的调度,然后就是任务的执行,那么其实延时任务执行的业务场景就已经抽离出来了,任务提交-> 任务调度 -> 任务执行
其实业界已经有一些比较成熟的分布式任务调度框架了,比如xxl-job、Quartz等,本文自实现延时组件其实也是参考了xxl-job的思路,话不多说,开始造轮子~
4、详细设计
延时组件最核心的部分就是任务的调度,一批任务提交过来,如何及时地完成调度呢?任务提交的时候会自带要被执行的时间,这个参数就是一个Cron表达式,Cron 表达式是一种用于表示定时任务调度的字符串表达式,常用于任务调度框架中。它由6个或7个字段组成,用空格分隔,每个字段表示一个时间单位。Cron 表达式的字段如下:
- 秒(0-59)
- 分钟(0-59)
- 小时(0-23)
- 日期(1-31)
- 月份(1-12 或 JAN-DEC)
- 星期几(0-7 或 SUN-SAT)
- 年份(可选,1970-2099)
每个字段可以是一个具体的值,也可以是一个范围或者通配符。
一些常见的 Cron 表达式示例:
- 每天的 8 点执行:0 0 8 * * ?
- 每小时的第 30 分钟执行:0 30 * * * ?
- 每周一到周五的 9 点执行:0 0 9 ? * MON-FRI
- 每个月的第一个星期天的 10 点执行:0 0 10 ? * 1#1
Cron 表达式非常灵活,可以表示各种复杂的定时任务调度规则。在任务调度框架中,可以使用 Cron 表达式来配置任务的执行时间,框架会根据表达式来触发任务的执行。这样我们通过解析这些Cron表达式就可以知道哪些任务应该要被执行或者将要被执行了。
任务调度
方式1:轮询Cron
比较直观的一种调度实现就是轮询这些任务的Cron表达式,每次都把任务的Cron表达式读取出来,然后通过计算找出最近5秒即将要执行的任务,后面再执行其它操作
这样调度是比较直观简易的,但是每次都要把Cron表达式全部加载出来解析一遍,性能不好
方式2: 时间轮
那么有什么方式实现任务的调度吗?答案肯定是有的,时间轮算法就可以很好地解决这个问题,时间轮算法是一种用于高效处理定时任务的算法。它是基于时间轮的概念,通过将时间划分为一系列的时间槽,每个时间槽代表一段时间间隔,来管理和调度定时任务的执行。
时间轮算法的基本原理如下:
- 时间轮的结构:时间轮由多个时间槽组成,每个时间槽代表一个时间间隔。时间轮按照固定的速度旋转,每次旋转一个时间槽。
- 定时任务的添加:当需要添加一个定时任务时,根据任务的触发时间计算出任务应该被放置在时间轮上的哪个时间槽中。
- 时间轮的旋转:随着时间轮的旋转,当前时间槽中的任务将被触发执行。执行完后,可以将该时间槽中的任务移除或者重新放置到时间轮的其他时间槽中。
- 时间轮的级联:当一个时间槽中的任务触发时,可以将该时间槽中的任务作为下一级时间轮的任务继续处理,实现更精细的任务调度。
时间轮算法的优点是具有高效的任务调度和执行能力,适用于需要频繁触发定时任务的场景。它通过时间轮的旋转和任务的放置,实现了高效的任务管理和调度,避免了传统的遍历任务列表查找需要执行的任务的低效性。
如何应用到我们的业务场景上去呢,可以提前生成cron表达式下次执行执行时间triggerNextTime,调度线程拿当前时间+5s去查询数据库triggerNextTime,查询规则是triggerNextTime <= nowTime+5s,然后执行下面的程序:
- 如果取出的任务已经过期5s,则直接丢弃,计算下次的执行时间
- 如果取出的任务过期时间小于5s,则直接执行,并计算下次调度时间,如果下次调度时间小于当前时间+5s,则加到时间轮
- 如果调度时间未过期,即triggerNextTime > nowTime, 计算下次调度时间,加到时间轮
- 全量更新当前任务的下次执行时间
时间轮的刻度是60,对应的分发线程每隔1s轮询一次,取走对应刻度的全部任务执行。
任务执行
上文提到组件的调度部分,任务的执行同样是延时组件重要的一部分,试想别人提交了任务,你通过调度组件完成了任务的调度,到了任务执行的时间,怎么才能通知到提交方执行对应的任务呢?这就要求任务的执行方可以与调度中心保持通信,在启动时就向任务调度中心注册自己的信息,包括执行器名称、地址、端口等。同时和调度中心保持心跳连接,获取分配给自己的任务,这样调度中心完成调度后,执行器就可以及时响应,根据任务参数执行自己的任务,如下图所示:
5、结语
这样我们就实现了一个简单的延时组件,任务调度与任务执行组成了组件的核心部分,时间轮完成任务的调度,执行器与调度中心通信实现任务的注册和执行