XXL-JOB

1564 字
8 分钟
XXL-JOB

核心执行逻辑(版本2.2.0)#

com.xxl.job.admin.core.thread.JobScheduleHelper#start

scheduleThread#

该线程负责从数据库中预读将要执行的任务,并将其放入到 ringData 中用于后续真正的调度线程进行调度

1. 线程初始化

scheduleThread = new Thread(new Runnable() {
@Override
public void run() {
// 线程执行的逻辑
}
});
  • 创建了一个新的线程 scheduleThread,并实现了 Runnable 接口的 run 方法。

2. 线程启动前的延迟

try {
TimeUnit.MILLISECONDS.sleep(5000 - System.currentTimeMillis() % 1000);
} catch (InterruptedException e) {
if (!scheduleThreadToStop) {
logger.error(e.getMessage(), e);
}
}
  • 线程启动后,首先会休眠一段时间,确保线程在整秒时刻启动(即 System.currentTimeMillis() % 1000 为 0 的时刻)。这样可以使得后续的调度任务能够对齐到整秒。

3. 预读任务数量计算

int preReadCount = (XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax() + XxlJobAdminConfig.getAdminConfig().getTriggerPoolSlowMax()) * 20;
  • 计算预读任务的数量。preReadCount 是根据线程池的大小和触发器的 QPS(每秒查询率)来计算的。假设每个触发器耗时 50ms,那么 QPS 为 20(1000ms / 50ms)

4. 主循环

while (!scheduleThreadToStop) {
// 主循环逻辑
}
  • 主循环会一直运行,直到 scheduleThreadToStop 被设置为 true

5. 数据库连接与事务管理

Connection conn = null;
Boolean connAutoCommit = null;
PreparedStatement preparedStatement = null;
try {
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 update 语句锁定 xxl_job_lock 表中的 schedule_lock 记录,确保在同一时间只有一个调度线程在执行
  • 这里要思考如果获取到数据库悲观锁之后,JVM挂掉,会不会一直无法释放锁导致其他节点无法获取数据库悲观锁

6. 预读任务

List<XxlJobInfo> scheduleList = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleJobQuery(nowTime + PRE_READ_MS, preReadCount);
  • 从数据库中预读即将触发的任务列表。nowTime + PRE_READ_MS 表示预读的时间范围,preReadCount 是预读的任务数量。

7. 任务处理

for (XxlJobInfo jobInfo: scheduleList) {
if (nowTime > jobInfo.getTriggerNextTime() + PRE_READ_MS) {
// 任务过期处理
} else if (nowTime > jobInfo.getTriggerNextTime()) {
// 任务触发处理
} else {
// 任务预读处理
}
}
  • 遍历预读的任务列表,根据任务的触发时间进行不同的处理:
    • 任务过期:如果任务的触发时间已经超过预读时间范围,则标记任务为过期,并更新下一次触发时间。
    • 任务触发:如果任务的触发时间在当前时间和预读时间之间,则立即触发任务,并更新下一次触发时间。
    • 任务预读:如果任务的触发时间在未来,则将任务放入时间环中,等待后续触发。

8. 更新任务信息

if (conn != null) {
try {
conn.commit();
} catch (SQLException e) {
if (!scheduleThreadToStop) {
logger.error(e.getMessage(), e);
}
}
// 恢复自动提交状态并关闭连接
}
  • 更新数据库中任务的信息,确保任务的触发时间和状态是最新的。

9. 事务提交与资源释放

if (conn != null) {
try {
conn.commit();
} catch (SQLException e) {
if (!scheduleThreadToStop) {
logger.error(e.getMessage(), e);
}
}
// 恢复自动提交状态并关闭连接
}
  • 提交事务,并释放数据库连接和 PreparedStatement 资源。

10. 等待下一次调度

long cost = System.currentTimeMillis()-start;
if (cost < 1000) {
try {
TimeUnit.MILLISECONDS.sleep((preReadSuc ? 1000 : PRE_READ_MS) - System.currentTimeMillis() % 1000);
} catch (InterruptedException e) {
if (!scheduleThreadToStop) {
logger.error(e.getMessage(), e);
}
}
}
  • 如果本次调度耗时小于 1 秒,则线程会休眠一段时间,确保下一次调度在整秒时刻执行。

ringThread#

该线程负责时间轮的推进(单位为秒),并执行对应的任务

1. 线程初始化

ringThread = new Thread(new Runnable() {
@Override
public void run() {
// 线程执行的逻辑
}
});

2. 线程启动前的延迟

try {
TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis() % 1000);
} catch (InterruptedException e) {
if (!ringThreadToStop) {
logger.error(e.getMessage(), e);
}
}
  • 熟悉的操作,线程启动后,首先会休眠一段时间,确保线程在整秒时刻启动(即 System.currentTimeMillis() % 1000 为 0 的时刻)。这样可以使得后续的时间环任务能够对齐到整秒。

3. 主循环

while (!ringThreadToStop) {
// 主循环逻辑
}

4. 获取当前秒数

int nowSecond = Calendar.getInstance().get(Calendar.SECOND);
  • 获取当前的秒数(0-59),用于确定当前时间环的位置。

5. 从时间环中获取任务

List<Integer> ringItemData = new ArrayList<>();
for (int i = 0; i < 2; i++) {
List<Integer> tmpData = ringData.remove((nowSecond + 60 - i) % 60);
if (tmpData != null) {
ringItemData.addAll(tmpData);
}
}
  • 从时间环数据结构 ringData 中获取当前秒和前一秒的任务列表。
    • ringData 是一个以秒为索引的哈希表,存储了每个秒数对应的任务列表。
    • (nowSecond + 60 - i) % 60 用于计算当前秒和前一秒的索引,避免跨过刻度。
    • 如果某个秒数对应的任务列表存在,则将其合并到 ringItemData 中。

6. 触发任务

if (ringItemData.size() > 0) {
for (int jobId : ringItemData) {
JobTriggerPoolHelper.trigger(jobId, TriggerTypeEnum.CRON, -1, null, null, null);
}
ringItemData.clear();
}
  • 如果 ringItemData 中有任务,则遍历任务列表并触发每个任务。
    • JobTriggerPoolHelper.trigger 是触发任务的核心方法,会根据任务 ID 执行相应的任务。
  • 触发完成后,清空 ringItemData,以便下一次使用。

7. 异常处理

catch (Exception e) {
if (!ringThreadToStop) {
logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread error:{}", e);
}
}
  • 捕获并记录线程运行过程中发生的异常。如果线程未被要求停止(ringThreadToStop 为 false),则记录错误日志

8. 等待下一次触发

try {
TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis() % 1000);
} catch (InterruptedException e) {
if (!ringThreadToStop) {
logger.error(e.getMessage(), e);
}
}
  • 线程休眠一段时间,确保下一次触发在整秒时刻执行。休眠时间通过 1000 - System.currentTimeMillis() % 1000 计算,保证对齐到整秒。

com.xxl.job.admin.core.thread.JobScheduleHelper#toStop

停止时,会先停止预读任务线程,如果有待执行的任务,还会 sleep 8秒等待相应的任务被调度完成

public void toStop(){
// 1、stop schedule
scheduleThreadToStop = true;
try {
TimeUnit.SECONDS.sleep(1); // wait
} catch (InterruptedException e) {
logger.error(e.getMessage(), e);
}
if (scheduleThread.getState() != Thread.State.TERMINATED){
// interrupt and wait
scheduleThread.interrupt();
try {
scheduleThread.join();
} catch (InterruptedException e) {
logger.error(e.getMessage(), e);
}
}
// if has ring data
boolean hasRingData = false;
if (!ringData.isEmpty()) {
for (int second : ringData.keySet()) {
List<Integer> tmpData = ringData.get(second);
if (tmpData!=null && tmpData.size()>0) {
hasRingData = true;
break;
}
}
}
if (hasRingData) {
try {
TimeUnit.SECONDS.sleep(8);
} catch (InterruptedException e) {
logger.error(e.getMessage(), e);
}
}
// stop ring (wait job-in-memory stop)
ringThreadToStop = true;
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
logger.error(e.getMessage(), e);
}
if (ringThread.getState() != Thread.State.TERMINATED){
// interrupt and wait
ringThread.interrupt();
try {
ringThread.join();
} catch (InterruptedException e) {
logger.error(e.getMessage(), e);
}
}
logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper stop");
}

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

XXL-JOB
https://blog.sephy.top/posts/xxl-job/
作者
虾米
发布于
2024-07-01
许可协议
CC BY-NC-SA 4.0
随机文章随机推荐
Profile Image of the Author
虾米
coder
分类
标签
站点统计
文章
61
分类
4
标签
52
总字数
64,663
运行时长
0
最后活动
0 天前
站点信息
构建平台
GitHub Actions
博客版本
Firefly v6.13.9
文章许可
CC BY-NC-SA 4.0