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
在 PVE 上给软路由虚拟机加一个网络 Watchdog
技术分享在 PVE 宿主机上用 Bash 和 systemd timer 实现一个网络 watchdog:先检测公网连通性,连续失败后重启软路由虚拟机,多次自愈无效再升级为重启 PVE 宿主机。
2
命令行自动登录 JumpServer:SSH、TOTP 与密码提示的自动化
技术分享把 JumpServer 登录流程拆成凭据读取和终端交互两层:zsh 从密码管理器安全读取 TOTP seed 与可选密码,expect 只负责响应 SSH 登录过程中的 password 和 Code 提示。
3
git 个人开发工作流
技术分享介绍个人 Git 分支协作流程:以基线分支(如 feature-a)+ 个人开发分支(feature-a-xxx)进行开发,开发分支定期 rebase 同步上游,最终通过 merge(必要时先 squash 整理提交)合回基线;并提供 grebase/gmergebase 等脚本,支持自动推导基线分支、预览计划、带 autostash 的安全 rebase,以及可选 squash/推送/删除分支的安全 merge-back。
4
使用 docker-atrust 连入深信服
技术分享使用 Docker 容器运行深信服 aTrust 客户端,通过 VNC 完成登录,并结合本机 mihomo 配置实现公司内网的 DNS 解析与按域名、IP 的精确代理分流;宿主机暴露 1080 与 8888 端口供 socks5 和 HTTP 代理使用,从而在不安装原生客户端的情况下实现零信任接入并降低对本机的影响。
5
mihomo 使用
技术分享2025-09-30
随机文章随机推荐













