废话少说,直接进入正题。

相信大家对XXL-JOB都很了解,故本文对源码不进行过多介绍,侧重的是看源码过程中想到的几个知识点,不一定都对,请大神们批评指正。

XXL-JOB简介

  • XXL-JOB是一个轻量级分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。
  • XXL-JOB分为调度中心、执行器、数据中心,调度中心负责任务管理及调度、执行器管理、日志管理等,执行器负责任务执行及执行结果回调。

任务调度 - “类时间轮”的实现

时间轮

时间轮出自Netty中的HashedWheelTimer,是一个环形结构,可以用时钟来类比,钟面上有很多bucket,每一个bucket上可以存放多个任务,使用一个List保存该时刻到期的所有任务,同时一个指针随着时间流逝一格一格转动,并执行对应bucket上所有到期的任务。任务通过取模决定应该放入哪个bucket。和HashMap的原理类似,newTask对应put,使用List来解决 Hash 冲突。

以上图为例,假设一个bucket是1秒,则指针转动一轮表示的时间段为8s,假设当前指针指向 0,此时需要调度一个3s后执行的任务,显然应该加入到(0+3=3)的方格中,指针再走3s次就可以执行了;如果任务要在10s后执行,应该等指针走完一轮零2格再执行,因此应放入2,同时将round(1)保存到任务中。检查到期任务时只执行round为0的,bucket上其他任务的round减1。

当然,还有优化的“分层时间轮”的实现,请参考https://cnkirito.moe/timer/。

XXL-JOB中的“时间轮”

  • XXL-JOB中的调度方式从Quartz变成了自研调度的方式,很像时间轮,可以理解为有60个bucket且每个bucket为1秒,但是没有了round的概念。

  • 具体可以看下图。

  • XXL-JOB中负责任务调度的有两个线程,分别为ringThreadscheduleThread,其作用如下。

1、scheduleThread:对任务信息进行读取,预读未来5s即将触发的任务,放入时间轮。 2、ringThread:对当前bucket和前一个bucket中的任务取出并执行。

  • 下面结合源代码看下,为什么说是“类时间轮”,关键代码附上了注解,请大家留意观看。
// 环状结构
private volatile static Map<Integer, List<Integer>> ringData = new ConcurrentHashMap<>();// 任务下次启动时间(单位为秒) % 60
int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);// 任务放进时间轮
private void pushTimeRing(int ringSecond, int jobId){// push async ringList<Integer> ringItemData = ringData.get(ringSecond);if (ringItemData == null) {ringItemData = new ArrayList<Integer>();ringData.put(ringSecond, ringItemData);}ringItemData.add(jobId);}
复制代码
// 同时取两个时间刻度的任务
List<Integer> ringItemData = new ArrayList<>();
int nowSecond = Calendar.getInstance().get(Calendar.SECOND);
// 避免处理耗时太长,跨过刻度,向前校验一个刻度;
for (int i = 0; i < 2; i++) {List<Integer> tmpData = ringData.remove( (nowSecond+60-i)%60 );if (tmpData != null) {ringItemData.addAll(tmpData);}
}
// 运行
for (int jobId: ringItemData) {JobTriggerPoolHelper.trigger(jobId, TriggerTypeEnum.CRON, -1, null, null);
}
复制代码

一致性Hash路由中的Hash算法

  • 大家也知道,XXL-JOB在执行任务时,任务具体在哪个执行器上运行是根据路由策略来决定的,其中有一个策略是一致性Hash策略(源码在ExecutorRouteConsistentHash.java),自然而然想到了一致性Hash算法
  • 一致性Hash算法是为了解决分布式系统中负载均衡的问题时候可以使用Hash算法让固定的一部分请求落到同一台服务器上,这样每台服务器固定处理一部分请求(并维护这些请求的信息),起到负载均衡的作用。
  • 普通的余数hash(hash(比如用户id)%服务器机器数)算法伸缩性很差,当新增或者下线服务器机器时候,用户id与服务器的映射关系会大量失效。一致性hash则利用hash环对其进行了改进。
  • 一致性Hash算法在实践中,当服务器节点比较少的时候会出现上节所说的一致性hash倾斜的问题,一个解决方法是多加机器,但是加机器是有成本的,那么就加虚拟节点
  • 具体原理请参考https://www.jianshu.com/p/e968c081f563。
  • 下图为带有虚拟节点的Hash环,其中ip1-1是ip1的虚拟节点,ip2-1是ip2的虚拟节点,ip3-1是ip3的虚拟节点。

可见,一致性Hash算法的关键在于Hash算法,保证虚拟节点Hash结果的均匀性, 而均匀性可以理解为减少Hash冲突,Hash冲突的知识点请参考从HashMap,Redis 字典看【Hash】。。。。

  • XXL-JOB中的一致性Hash的Hash函数如下。
// jobId转换为md5
// 不直接用hashCode() 是因为扩大hash取值范围,减少冲突
byte[] digest = md5.digest();// 32位hashCode
long hashCode = ((long) (digest[3] & 0xFF) << 24)| ((long) (digest[2] & 0xFF) << 16)| ((long) (digest[1] & 0xFF) << 8)| (digest[0] & 0xFF);long truncateHashCode = hashCode & 0xffffffffL;
复制代码
  • 看到上图的Hash函数,让我想到了HashMap的Hash函数
f(key) = hash(key) & (table.length - 1)
// 使用>>> 16的原因,hashCode()的高位和低位都对f(key)有了一定影响力,使得分布更加均匀,散列冲突的几率就小了。
hash(key) = (h = key.hashCode()) ^ (h >>> 16)
复制代码
  • 同理,将jobId的md5编码的高低位都对Hash结果有影响,使得Hash冲突的概率减小。

分片任务的实现 - 维护线程上下文

  • XXL-JOB的分片任务实现了任务的分布式执行,其实是笔者调研的重点,日常开发中很多定时任务都是单机执行,对于后续数据量大的任务最好有一个分布式的解决方案。

  • 分片任务的路由策略,源代码作者提出了分片广播的概念,刚开始还有点摸不清头脑,看了源码逐渐清晰了起来。

  • 想必看过源码的也遇到过这么一个小插曲,路由策略咋没实现?如下图所示。

public enum ExecutorRouteStrategyEnum {FIRST(I18nUtil.getString("jobconf_route_first"), new ExecutorRouteFirst()),LAST(I18nUtil.getString("jobconf_route_last"), new ExecutorRouteLast()),ROUND(I18nUtil.getString("jobconf_route_round"), new ExecutorRouteRound()),RANDOM(I18nUtil.getString("jobconf_route_random"), new ExecutorRouteRandom()),CONSISTENT_HASH(I18nUtil.getString("jobconf_route_consistenthash"), new ExecutorRouteConsistentHash()),LEAST_FREQUENTLY_USED(I18nUtil.getString("jobconf_route_lfu"), new ExecutorRouteLFU()),LEAST_RECENTLY_USED(I18nUtil.getString("jobconf_route_lru"), new ExecutorRouteLRU()),FAILOVER(I18nUtil.getString("jobconf_route_failover"), new ExecutorRouteFailover()),BUSYOVER(I18nUtil.getString("jobconf_route_busyover"), new ExecutorRouteBusyover()),// 说好的实现呢???竟然是nullSHARDING_BROADCAST(I18nUtil.getString("jobconf_route_shard"), null);
复制代码
  • 再继续追查得到了结论,待我慢慢道来,首先分片任务执行参数传递的是什么?看XxlJobTrigger.trigger函数中的一段代码。
...
// 如果是分片路由,走的是这段逻辑
if (ExecutorRouteStrategyEnum.SHARDING_BROADCAST == ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null)&& group.getRegistryList() != null && !group.getRegistryList().isEmpty()&& shardingParam == null) {for (int i = 0; i < group.getRegistryList().size(); i++) {// 最后两个参数,i是当前机器在执行器集群当中的index,group.getRegistryList().size()为执行器总数processTrigger(group, jobInfo, finalFailRetryCount, triggerType, i, group.getRegistryList().size());}}
...
复制代码
  • 参数经过自研RPC传递到执行器,在执行器中具体负责任务执行的JobThread.run中,看到了如下代码。
// 分片广播的参数比set进了ShardingUtil
ShardingUtil.setShardingVo(new ShardingUtil.ShardingVO(triggerParam.getBroadcastIndex(), triggerParam.getBroadcastTotal()));
...
// 将执行参数传递给jobHandler执行
handler.execute(triggerParamTmp.getExecutorParams())
复制代码
  • 接着看ShardingUtil,才发现了其中的奥秘,请看代码。
public class ShardingUtil {// 线程上下文private static InheritableThreadLocal<ShardingVO> contextHolder = new InheritableThreadLocal<ShardingVO>();// 分片参数对象public static class ShardingVO {private int index;  // sharding indexprivate int total;  // sharding total// 次数省略 get/set}// 参数对象注入上下文public static void setShardingVo(ShardingVO shardingVo){contextHolder.set(shardingVo);}// 从上下文中取出参数对象public static ShardingVO getShardingVo(){return contextHolder.get();}}
复制代码
  • 显而易见,在负责分片任务的ShardingJobHandler里取出了线程上下文中的分片参数,这里也给个代码把~
@JobHandler(value="shardingJobHandler")
@Service
public class ShardingJobHandler extends IJobHandler {@Overridepublic ReturnT<String> execute(String param) throws Exception {// 分片参数ShardingUtil.ShardingVO shardingVO = ShardingUtil.getShardingVo();XxlJobLogger.log("分片参数:当前分片序号 = {}, 总分片数 = {}", shardingVO.getIndex(), shardingVO.getTotal());// 业务逻辑for (int i = 0; i < shardingVO.getTotal(); i++) {if (i == shardingVO.getIndex()) {XxlJobLogger.log("第 {} 片, 命中分片开始处理", i);} else {XxlJobLogger.log("第 {} 片, 忽略", i);}}return SUCCESS;}}
复制代码
  • 由此得出,分布式实现是根据分片参数indextotal来做的,简单来讲,就是给出了当前执行器的标识,根据这个标识将任务的数据或者逻辑进行区分,即可实现分布式运行。
  • 题外话:至于为什么用外部注入分片参数的方式,不直接execute传递?

1、可能是因为只有分片任务才用到这两个参数 2、IJobHandler只有String类型参数

看完源码后的思考

  • 1、经过此次看源代码,XXL-JOB的设计目标确实符合开发迅速、学习简单、轻量级、易扩展
  • 2、至于自研RPC还没有具体考量,具体接入应该会考虑公司的RPC框架。
  • 3、作者给出的Quartz调度的不足,笔者得继续深入了解。
  • 4、框架中很多对宕机、故障、超时等异常状况的兼容值得学习。
  • 5、Rolling日志以及日志系统实现需要继续了解。

参考文献

  • www.xuxueli.com/xxl-job/#/?…
  • cnkirito.moe/timer/
  • www.jianshu.com/p/e968c081f…

揪出XXL-JOB中的细节相关推荐

  1. 这个仅2M的宝藏小工具,竟无需群发就能揪出微信所有单向好友!

    背景: 很多人平时微信跟人发消息的时候,发现自己已经被对方清理掉了,因此可能想把自己的微信单向好友都清理出去."转账验证好友"是大部分人已经都知道的方法,但是好友太多,我们实在是无 ...

  2. Android 杀后台太狠,谷歌:看不下去了,势必要揪出“凶手”!

    整理 | 郑丽媛 出品 | CSDN(ID:CSDNnews) 手机杀后台严重的痛苦你体会过吗? 打着游戏,看到消息切出去看一眼,回来游戏就得重开:看着视频,想去浏览器搜不懂的词汇,回来视频 App ...

  3. 黑科技DeepFake检测方法:利用心跳做信号,还能「揪出」造假模型

    本文转载自机器之心. 选自arXiv 作者:Umur Aybars Ciftci等 机器之心编译 编辑:陈萍 DeepFake 真的可以达到以假乱真的效果吗?未必.来自宾汉姆顿大学.英特尔公司的研究人 ...

  4. 开发者被要求向破解者道歉,竟揪出“阿里云假员工”,网友:这人有前科

    文末包邮送3本技术书! 浩楠 鱼羊 发自 凹非寺 量子位 报道 | 公众号 QbitAI 咄咄怪事,起于知乎. 「我是如何被逼到向我软件的破解者道歉的?」 没错,就是这样一则愤怒和无奈溢出屏幕的&qu ...

  5. 揪出“凶手”——实战WinDbg分析电脑蓝屏原因

    http://www.appinn.com/blue-screen-search-code/ 蓝屏代码查询器 – 找出蓝屏的元凶 11 文章标签: windows / 系统 / 蓝屏. 蓝屏代码查询器 ...

  6. 如何找出电脑里的流氓软件_啥拦截软件都挡不住?教你一键揪出乱弹窗的流氓软件...

    [PConline 应i用]最近很多朋友都向笔者表示了想要购买新MacBook的想法,一方面是由于M1芯片性能非常强劲,另一方面也在于想要体验一下macOS--Windows系统上的流氓软件弹窗,实在 ...

  7. 揪出MySQL延迟上千秒的元凶

    揪出MySQL延迟上千秒的元凶 背景 Part1:写在最前 MySQL的延迟告警想必大家一定不陌生,MySQL引起从库延迟的原因有很多,从硬件上讲可能是网卡,磁盘,内存达到瓶颈,从数据库层面来讲,可能 ...

  8. 探测电磁波就能揪出恶意软件,网友:搁这给电脑把脉呢?

    博雯 萧箫 发自 凹非寺 量子位 | 公众号 QbitAI 不安装任何杀毒软件,"悬丝诊脉"也能揪出计算机病毒? 而且准确率达99.82%,杀毒软件看了都汗颜. 先请出我们的&qu ...

  9. 让Windows 7揪出每一个暗中运行程序

    让Windows 7揪出每一个暗中运行程序 Windows用户都知道,可以通过任务管理器查看当前都有哪些程序在运行,并可以查看到各程序占用的CPU和内存等情况.但是,通过Windows系统内置的任务管 ...

  10. 装mysql最后一步没响应_每天14点遭遇惊魂时刻,如何一步一步揪出真凶?

    " 笔者所在的公司有一款大 DAU(日活)的休闲游戏.这款游戏的后端架构很简单,可以简单理解为通讯-逻辑-存储三层结构.其中存储层大量使用了 Redis 和 MySQL. 图片来自 Pexe ...

最新文章

  1. 从属关系mysql_关系型数据库基础概念:MySQL系列之开篇
  2. Visual Studio 2010 -新特性,但我要的比这个更多!
  3. V1-bug Alpha阶段项目展示
  4. python学习笔记 --- 随机数进阶
  5. Windows下DNS ID欺骗的原理与实现
  6. 简述linux中的passwd结构,51CTO博客-专业IT技术博客创作平台-技术成就梦想
  7. sublim插件(待续)
  8. c# itextsharp根据模板导出pdf报表
  9. 计算机管理系统工具共享文件夹,win7一键共享工具【管理方式】
  10. HTML 字体 大小 颜色对照表
  11. onesignal php,swoole生产环境并发高时偶尔出现 WARNING swSignalfd_onSignal (ERRNO 707)
  12. 《从0到1:CTFer成长之路》
  13. 2020-10-13携程招聘算法题
  14. 手动引入jar包,解决Dependency ‘XXX‘ not found的两种方式
  15. 中华之剑,禁毒纪录片,已转码支持流媒体播放,欢迎下载、欢迎传播
  16. 寻觅那款你心仪的国际象棋app
  17. 图像处理3D 人体模型
  18. 湿冷天扫除前挡玻璃视障 应该怎么选?
  19. 语音识别引擎_linux 语音识别引擎_中文实时语音识别引擎 - 云+社区 - 腾讯云
  20. 数学七大基本思想方法

热门文章

  1. 《openssl编程》之配置文件
  2. 贝壳app Authorization参数分析
  3. 第17讲:aiohttp 异步爬虫实战
  4. python实现单例模式的三种方法
  5. 微服务架构统一安全认证设计与实践
  6. 低至4.7折起!戴尔OptiPlex商用台式机限时特惠,重磅来袭!
  7. 音视频技术开发周刊 | 208
  8. 我们和计算机系的老教授聊了聊TCP优化与产学矛盾
  9. Cloudflare的HTTP/2优化策略
  10. LiveVideoStackCon 2018社区编辑门票兑换启动