一. 为什么定时任务可以定时执行
定时任务可以定时执行的原理是通过操作系统提供的定时器实现的。
以下是定时任务能够准时执行的基本原理和相关技术:
操作系统的调度器: 操作系统(如Linux、Windows等)内部都有一个调度器,负责管理和调度各种进程和任务。这个调度器通常基于时间片(time slice)或者其他调度算法来分配CPU时间给不同的任务。
定时器和时钟: 操作系统会维护一个时钟系统,它通常是一个硬件计时器或者软件计时器,用于定期触发中断或事件。基于这些时钟,操作系统能够准确追踪时间,并在指定的时刻触发特定的任务。
定时任务的注册和管理: 在操作系统或者特定的任务调度器中,可以注册和管理定时任务。这些任务通常由操作系统或者相关的服务来执行,并在指定的时间点或时间间隔内触发执行。
精确时间的管理: 现代操作系统能够提供比较精确的时间管理,如毫秒级或者更高的精度,这使得定时任务可以按照预期的时间间隔或者具体的时间点执行。
持久化和可靠性: 对于需要持久化的定时任务,操作系统或者相关的任务调度器通常会保证在系统重启后能够恢复原有的调度状态,确保定时任务不会因为系统重启而中断。
二. 为什么Cron表达式可以定时执行
Cron表达式可以定时执行底层也是基于操作系统的定时器的机制。在常见的计算机操作系统中,都提供了一种定时器机制,可以设置定时器来触发某个操作或执行某个任务。在我们在系统中设置了一个Cron任务后,Cron服务会工具Cron表达式计算出任务下一次应该执行的时间点,并将这个时间点与当前时间点进行比较,如果当前时间点已经超过了任务的执行时间点,那么Cron服务会立即执行该任务;否则Cron服务会将任务的执行时间点记录下来,并在这个时间点到来时再执行任务。
在 Unix/Linux 系统中,Cron 服务通常是通过一个名为 cron 的系统服务来实现的。这个服务会周期性地检查系统中已经配置好的 crontab 文件,根据其中的配置信息来决定哪些任务应该被执行。
crontab 文件中包含了多条定时任务的配置信息,其中每条任务都由五个时间字段和一个命令行指令组成。这五个时间字段分别表示分钟、小时、日期、月份和星期几,cron 会根据这些时间信息来判断任务何时应该被执行。当定时器达到指定时间时,cron 会根据 crontab 文件中的配置信息,启动相应的命令行指令来执行任务。这样,定时任务就可以按照预定的时间定时执行了。
三. Java中实现定时任务的方式
1. java中原生特性(JDK自带的功能)实现定时任务:
Timer类和TimerTask类: Timer类是Java SE5之前的一个定时器工具类,可用于执行定时任务。TimerTask类则表示一个可调度的任务,通常通过继承该类来实现自己的任务,然后使用Timer.schedule()方法来安排任务的执行时间。
ScheduledExecutorService类: ScheduledExecutorService是Java SE5中新增的一个定时任务执行器,它可以比Timer更精准地执行任务,并支持多个任务并发执行。通过调用ScheduledExecutorService.schedule(或ScheduledExecutorService.scheduleAtFixedRate()方法来安排任务的执行时间。
DelayQueue: DelayQueue是一个带有延迟时间的无界阻塞队列,它的元素必须实现Delayed接口。当从DelayQueue中取出一个元素时,如果其延迟时间还未到达,则会阻塞等待,直到延迟时间到达。因此,我们可以通过将任务封装成实现Delaved接口的元素,将其放入DelayQueue中再使用一个线程不断地从DelayQueue中取出元素并执行任务,从而实现定时任务的调度。
2. 需要引入第三方框架实现定时任务方式:
Spring的@Scheduled注解: Spring框架提供了一个方便的定时任务调度功能,可以使用@Scheduled注解来实现定时任务。通过在需要执行定时任务的方法上加上@Scheduled注解,并指定执行的时间间隔即可。
Quartz框架: Quartz是一个流行的开源任务调度框架,它支持任务的并发执行和动态调度。通过创建JobDetail和Trigger对象,并将它们交给Scheduler进行调度来实现定时任务。
xxl-job: xxl-job是一款分布式定时任务调度平台,可以实现各种类型的定时任务调度,如定时执行Java代码、调用HTTP接口、执行Shel脚本等。xxl-job采用分布式架构,支持集群部署,可以满足高并发、大数据量的任务调度需求。
Elastic-Job: Elastic-Job是一款分布式任务调度框架,可以实现各种类型的定时任务调度,如简单任务、数据流任务、脚本任务、Spring
Bean任务等。Elastic-Job提供了丰富的任务调度策略可以通过配置cron表达式、固定间隔等方式实现定时任务调度。Elastic-Job支持分布式部署,提供了高可用性和灵活的扩展性,可以满足高并发、大数据量的任务调度需求,
3. java中原生特性(JDK自带的功能)实现定时任务与引入第三方框架实现定时任务区别:
java中原生特性实现定时任务,相比于xxl-job这种定时任务调度框架来说,原生特性实现起来简单,不用依赖第三方的调度框架和类库。方案更加轻量级。缺点是原生特性方案都是基于JVM内存的,需要把定时任务提前放进去,如果数据量太大的话,可能会导致OOM的问题;另外,基于JVM内存的方案,一旦机器重启了,里面的数据就都没有了,所以一般都需要配合数据库的持久化一起用,并且在应用启动的时候也需要做重新加载。
四. 实现一个定时任务,可以用什么数据结构和算法
1.小顶堆:可以使用小顶堆来管理定时任务,其中每个事件包含触发时间戳和要执行的任务信息。最先要触发的任务处于堆的根部,这样可以高效地找到最近触发的定时事件,并在触发时执行相应的任务。
小顶堆(Min Heap)是一种特殊的二又堆数据结构。对于堆中的任意节点i,其父节点的值小于等于节点i的值。换句话说,堆中的最小值总是位于堆的根节点上。
在定时器的实现中,每个定时事件可以表示为一个包含触发时间戳的数据结构。从堆顶不断取出需要执行的定时任务即可。
2.时间轮算法:时间轮算法是一种时间管理算法,可以高效地处理定时任务。是一种用于处理定时任务和调度的常见算法。它把时间划分成若干个时间槽,并使用循环队列来存储在每个时间槽上触发的任务,从而避免了遍历整个定时事件集合的开销。
时间轮(TimingWheel)是一个 存储定时任务的环形队列,底层采用数组实现,数组中的每个元素可以存放一个定时任务列表(TimerTaskList)。TimerTaskList 是一个环形的双向链表,链表中的每一项表示的都是定时任务项(TimerTaskEntry),其中封装了真正的定时任务 TimerTask。
简单时间轮算法:
时间轮算法主要需要定义一个时间轮盘,在一个时间轮盘中划分出多个槽位,每个槽位表示一个时间段,这个段可以是秒级、分钟级、小时级等等。如以下就是把一个时间轮分为了60个时间槽,每一个槽代表一秒钟。
当我们有定时任务需要执行的时候,就把他们挂在到这些槽位中,这个任务将要在哪个槽位中执行,就把他挂在到哪个槽位的链表上。比如当前如果是0秒,那么要3秒后执行,那就挂在槽位为3的那个位置上。
缺点:这种时间轮存在一个问题,就是分了多少槽位,就只支持多少以内定时。
简单时间轮加round:
在时间轮中增加一个round的标识,标识运行的圈数,比如上面的60s的时间轮,如果我要200s之后运行,那么我在设置这个任务的时候,就把他的round设置为200/60=3,然后再把它放到200%60=20的这个槽位上。
有了这个round之后,每一次current移动到某个槽位时,检查任务的round是不是为0,如果不为0,则减一。这样时间轮转到第三圈时,round的值会变成0,再第四运行到current=20的时候,发现round=0了,那么就可以执行这个任务了。
缺点:round的检查过程,需要把所有任务都遍历一遍,效率不高。
分层时间轮:
专为大批量定时任务管理而生。比如要支持触发时间是一年的精度为秒级别的时间轮,如果单纯的用一个秒级的时间轮:3652460*60 这都三千多万个时间格了,造成大量资源开销。而分层的话,那么可分为四个层次,天级别的时间轮,小时级时间轮,分钟级时间轮,秒级时间轮,他们的时间格数分别为365,24,60,60,总时间格数只有365+24+60+60=509个。这样解决了上面两种时间轮的缺点。
3.链表(用的比较少):链表可以用于管理定时事件,每个节点包含触发时间戳和任务信息。链表中的节点按照触发时间戳从小到大排序,通过遍历链表,可以找到最近触发的定时事件并执行任务。
五.xxl-job怎么保证任务只会触发一次
xxl-job 作为一个定时任务调度工具,需要确保同一时间内同一任务只会在一个执行器上执行。这个特性对于避免任务的重复执行非常关键,特别是在分布式环境中,多个执行器实例可能同时运行相同的任务。这个特性被xxl-job描述为“调度一致性”,“调度中心”通过DB锁保证集群分布式调度的一致性,一次任务调度只会触发一次执行。
几个关键点:
任务调度器: xxl-job 的调度器负责按照设定的调度策略(如 cron表达式)定时触发任务执行。调度器确保在预定的时间点触发任务,避免任务重复触发。
执行器与任务执行状态管理: 执行器是实际执行任务逻辑的组件,它负责执行具体的任务代码。xxl-job会在任务执行前记录任务的执行状态,并在执行完成后更新任务状态,以确保同一个任务实例不会重复执行。
任务幂等性: 开发者需要在编写任务逻辑时考虑任务的幂等性,确保任务的多次执行不会产生错误结果或者重复操作。xxl-job 并未提供特定的JobScheduleHelper 类或方法来保证任务只会触发一次,而是通过任务的设计和调度管理来实现这一目标。
调度中心管理任务触发: xxl-job 的核心组件是调度中心,它负责管理注册的所有任务及其调度配置(如CRON表达式)。调度中心根据任务的执行时间点和配置,决定何时触发任务的执行。调度中心在XXL-JOB中负责管理所有任务的调度,当到达指定的执行时间点,调度中心会选择一个执行器实例来执行任务。
数据库锁确保任务唯一执行: 为了确保任务在同一时间点只会被触发一次执行,xxl-job 使用了数据库锁机制。具体来说,当调度中心准备触发某个任务时,它会尝试获取数据库中的锁。这些锁可以是行级锁或表级锁,用于控制对任务执行状态的并发访问。
JobScheduleHelper 协调任务调度: JobScheduleHelper 是 xxl-job 中负责协调任务调度逻辑的组件之一。它利用数据库锁基于数据的悲观锁实现的一个加锁过程,确保同一时间点只有一个执行器实例能够获取并执行特定任务。其他执行器实例在获取锁失败时会等待或重试,以避免重复执行。
六.xxl-job支持分片任务的实现原理:
分片任务非常适用于处理大数据的任务,其实就是可以将一个大任务划分为多个子任务并行执行来提高效率。
分片任务的实现原理主要包含以下几个核心步骤:
任务分配: 当一个分片任务被触发时,调度器会根据任务的分片参数决定需要多少个执行器参与任务。每个执行器或执行线程会接收到一个分片索引(shard index)和分片总数(shard total)。
分片参数: 分片索引(从0开始)标识了当前执行器处理的是哪一部分数据。分片总数告诉执行器总共有多少个分片。
并行执行: 每个执行器根据分配到的分片索引并行执行其任务。例如,如果一个任务被分为10个片,那么每个执行器可能负责处理10%的数据。
处理逻辑: 开发者在任务实现时需要根据分片索引和分片总数来调整处理逻辑,确保每个分片处理正确的数据段。
结果汇总: 分片执行完毕后,各个执行器的执行结果可以被独立处理,或者可以通过某种机制进行结果的汇总和整合。
当一个任务被分片任务调度的时候,会带着shardlndex和shardTotal两个参数过来,我们就可以解析这两个参数进行分片执行。
例子如下:
public ReturnT<String>orderTime0utExecute(){
int shardIndex=XxlJobHelper.getShardIndex();
int shardTotal =XxlJobHelper.getShardTotal();
if(userId %shardTotal == shardIndex){
// 执行任务
System.out.println("执行任务:用户"+ userId);
}else {
//不执行任务
System.out.println("用户"+userId +" 不执行任务");
}
}