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");}文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!
相关文章智能推荐
1
重新认识 .gitignore:目录级规则与 .git/info/exclude
编程从目录级 .gitignore 的作用域、匹配优先级和常见陷阱出发,理解 Git 的忽略机制,并掌握 .git/info/exclude 的使用方法。
2
从一票海运入仓开始,梳理海外仓常见业务术语
业务用一批家具从宁波港发往洛杉矶海外仓的例子,串起 FCL、LCL、ETD、ETA、LFD、Demurrage、Detention 等常见术语。
3
团队多人协同维护项目时,如何统一管理 skill
AI把项目级 skill 当成依赖管理,解决团队成员使用不同版本和不同规则的问题。
4
把 GitHub Action 抽成跨项目复用
服务器记录把飞书通知 Action 从单个项目迁移到共享 actions 仓库后的使用方式。
5
zsh Tab 补全卡住排查:iTerm2 tmux Control Mode 与 fzf-tab popup 冲突
技术分享记录一次 zsh Tab 补全问题的排查:先为 completion 增加大小写不敏感匹配,再定位 iTerm2 tmux Control Mode 下 fzf-tab popup 后端导致远程 shell 看似卡住的问题,并用条件配置修复。
随机文章随机推荐












