type
status
date
slug
summary
tags
category
icon
password
AI summary
Last edited time
Mar 11, 2025 03:50 PM
核心执行逻辑(版本2.2.0)
com.xxl.job.admin.core.thread.JobScheduleHelper#start
scheduleThread
该线程负责从数据库中预读将要执行的任务,并将其放入到 ringData 中用于后续真正的调度线程进行调度
1. 线程初始化
- 创建了一个新的线程
scheduleThread
,并实现了Runnable
接口的run
方法。
2. 线程启动前的延迟
- 线程启动后,首先会休眠一段时间,确保线程在整秒时刻启动(即
System.currentTimeMillis() % 1000
为 0 的时刻)。这样可以使得后续的调度任务能够对齐到整秒。
3. 预读任务数量计算
- 计算预读任务的数量。
preReadCount
是根据线程池的大小和触发器的 QPS(每秒查询率)来计算的。假设每个触发器耗时 50ms,那么 QPS 为 20(1000ms / 50ms)
4. 主循环
- 主循环会一直运行,直到
scheduleThreadToStop
被设置为true
5. 数据库连接与事务管理
- 获取数据库连接,并开启事务。通过
for update
语句锁定xxl_job_lock
表中的schedule_lock
记录,确保在同一时间只有一个调度线程在执行
- 这里要思考如果获取到数据库悲观锁之后,JVM挂掉,会不会一直无法释放锁导致其他节点无法获取数据库悲观锁
6. 预读任务
- 从数据库中预读即将触发的任务列表。
nowTime + PRE_READ_MS
表示预读的时间范围,preReadCount
是预读的任务数量。
7. 任务处理
- 遍历预读的任务列表,根据任务的触发时间进行不同的处理:
- 任务过期:如果任务的触发时间已经超过预读时间范围,则标记任务为过期,并更新下一次触发时间。
- 任务触发:如果任务的触发时间在当前时间和预读时间之间,则立即触发任务,并更新下一次触发时间。
- 任务预读:如果任务的触发时间在未来,则将任务放入时间环中,等待后续触发。
8. 更新任务信息
- 更新数据库中任务的信息,确保任务的触发时间和状态是最新的。
9. 事务提交与资源释放
- 提交事务,并释放数据库连接和
PreparedStatement
资源。
10. 等待下一次调度
- 如果本次调度耗时小于 1 秒,则线程会休眠一段时间,确保下一次调度在整秒时刻执行。
ringThread
该线程负责时间轮的推进(单位为秒),并执行对应的任务
1. 线程初始化
2. 线程启动前的延迟
- 熟悉的操作,线程启动后,首先会休眠一段时间,确保线程在整秒时刻启动(即
System.currentTimeMillis() % 1000
为 0 的时刻)。这样可以使得后续的时间环任务能够对齐到整秒。
3. 主循环
4. 获取当前秒数
- 获取当前的秒数(0-59),用于确定当前时间环的位置。
5. 从时间环中获取任务
- 从时间环数据结构
ringData
中获取当前秒和前一秒的任务列表。 ringData
是一个以秒为索引的哈希表,存储了每个秒数对应的任务列表。(nowSecond + 60 - i) % 60
用于计算当前秒和前一秒的索引,避免跨过刻度。- 如果某个秒数对应的任务列表存在,则将其合并到
ringItemData
中。
6. 触发任务
- 如果
ringItemData
中有任务,则遍历任务列表并触发每个任务。 JobTriggerPoolHelper.trigger
是触发任务的核心方法,会根据任务 ID 执行相应的任务。
- 触发完成后,清空
ringItemData
,以便下一次使用。
7. 异常处理
- 捕获并记录线程运行过程中发生的异常。如果线程未被要求停止(
ringThreadToStop
为false
),则记录错误日志
8. 等待下一次触发
- 线程休眠一段时间,确保下一次触发在整秒时刻执行。休眠时间通过
1000 - System.currentTimeMillis() % 1000
计算,保证对齐到整秒。
com.xxl.job.admin.core.thread.JobScheduleHelper#toStop
停止时,会先停止预读任务线程,如果有待执行的任务,还会 sleep 8秒等待相应的任务被调度完成