xxl-job源码阅读二(服务端)
1、源码入口
xxl-job-admin是一个简单的springboot工程,简单翻看源码,可以很快发现XxlJobAdminConfig入口。
@Override
public void afterPropertiesSet() throws Exception {
adminConfig = this;
xxlJobScheduler = new XxlJobScheduler();
xxlJobScheduler.init();
}
我们就可以顺着这个XxlJobScheduler,分析下这个xxl-job-admin做了些什么。
2、初始化七大步
在XxlJobScheduler.init() 方法中,主要做了如下七件事情:
public void init() throws Exception {
// init i18n
initI18n();
// admin trigger pool start
JobTriggerPoolHelper.toStart();
// admin registry monitor run
JobRegistryHelper.getInstance().start();
// admin fail-monitor run
JobFailMonitorHelper.getInstance().start();
// admin lose-monitor run ( depend on JobTriggerPoolHelper )
JobCompleteHelper.getInstance().start();
// admin log report start
JobLogReportHelper.getInstance().start();
// start-schedule ( depend on JobTriggerPoolHelper )
JobScheduleHelper.getInstance().start();
logger.info(">>>>>>>>> init xxl-job admin success.");
}
按照代码的顺序,逐一看看这七步到底做了些什么。这里的7个方法分别对应下面的2.1 ~ 2.7小节
2.1、国际化
真正实现国际化的,可不是这个initI18n(),这个方法只是设置了几个title。真正实现国际化的实现是:
com.xxl.job.admin.core.util.I18nUtil 加载 国际化资源文件
这块实现充分利用spring的 EncodedResource和PropertiesLoaderUtils.loadProperties,嗯,利用好现有的轮子!
CookieInterceptor 把I18nUtil对象返回到ftl页面上去
在templates/common/common.macro.ftl中定义宏
<#global I18n = I18nUtil.getMultString()?eval />
var I18n = ${I18nUtil.getMultString()};
在ftl页面中使用,比如
<h1>${I18n.job_dashboard_name}</h1>
2.2、触发器
JobTriggerPoolHelper.toStart();里面启动了2个线程池,一快一慢。
默认情况下,会使用fastTriggerPool。如果1分钟窗口期内任务耗时达500ms超过10次,则该窗口期内判定为慢任务,慢任务自动降级进入”Slow”线程池,避免耗尽调度线程,提高系统稳定性;
这个JobTriggerPoolHelper里面有个很重要的方法,就是addTrigger
public void addTrigger(final int jobId,
final TriggerTypeEnum triggerType,
final int failRetryCount,
final String executorShardingParam,
final String executorParam,
final String addressList) {
// choose thread pool
ThreadPoolExecutor triggerPool_ = fastTriggerPool;
AtomicInteger jobTimeoutCount = jobTimeoutCountMap.get(jobId);
if (jobTimeoutCount!=null && jobTimeoutCount.get() > 10) { // job-timeout 10 times in 1 min
triggerPool_ = slowTriggerPool;
}
// trigger
triggerPool_.execute(new Runnable() {
@Override
public void run() {
long start = System.currentTimeMillis();
try {
// do trigger
XxlJobTrigger.trigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam, addressList);
} catch (Exception e) {
logger.error(e.getMessage(), e);
} finally {
// check timeout-count-map
long minTim_now = System.currentTimeMillis()/60000;
if (minTim != minTim_now) {
minTim = minTim_now;
jobTimeoutCountMap.clear();
}
// incr timeout-count-map
long cost = System.currentTimeMillis()-start;
if (cost > 500) { // ob-timeout threshold 500ms
AtomicInteger timeoutCount = jobTimeoutCountMap.putIfAbsent(jobId, new AtomicInteger(1));
if (timeoutCount != null) {
timeoutCount.incrementAndGet();
}
}
}
}
});
}
具体到真正触发执行的地方,就得看看XxlJobTrigger.trigger了。
XxlJobTrigger.trigger中,先查表看有哪些执行器(其实就是业务服务器,可以用来跑job的),如果配置的路由策略是分片,则组装好分片参数。
参数组装好之后,调用processTrigger方法,然后调到runExecutor去执行。
public static ReturnT<String> runExecutor(TriggerParam triggerParam, String address){
ReturnT<String> runResult = null;
try {
ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address);
runResult = executorBiz.run(triggerParam);
} catch (Exception e) {
logger.error(">>>>>>>>>>> xxl-job trigger error, please check if the executor[{}] is running.", address, e);
runResult = new ReturnT<String>(ReturnT.FAIL_CODE, ThrowableUtil.toString(e));
}
StringBuffer runResultSB = new StringBuffer(I18nUtil.getString("jobconf_trigger_run") + ":");
runResultSB.append("<br>address:").append(address);
runResultSB.append("<br>code:").append(runResult.getCode());
runResultSB.append("<br>msg:").append(runResult.getMsg());
runResult.setMsg(runResultSB.toString());
return runResult;
}
这里首先利用执行器的地址,构造一个ExecutorBizClient对象,然后调用run方法。
public class ExecutorBizClient implements ExecutorBiz {
@Override
public ReturnT<String> run(TriggerParam triggerParam) {
return XxlJobRemotingUtil.postBody(addressUrl + "run", accessToken, timeout, triggerParam, String.class);
}
}
前面在阅读xxl-job-core的代码的时候,就看到过,客户端基于netty创建了一个EmbedServer ,默认监听9999 端口,接收job-admin端发过来的任务处理命令。
这里的调用,就是调到客户端的执行,即触发客户端执行任务!
触发之后,客户端会立即有返回触发是否成功。真正任务执行是否成功,则是异步返回给admin端的。(可以回头看下 TriggerCallbackThread )
(调度触发 和 job执行是两码事,所以xxl_job_log表里面有trigger_code 和 handle_code,分别来存着两个动作的结果)
2.3、执行器维护
这里的执行器,其实就是业务服务器。很好理解,谁执行job,谁就是执行器。
接下来看看JobRegistryHelper.getInstance().start()。
- 开启一个线程池 registryOrRemoveThreadPool ,用来注册或者删除。对象对外提供registry和registryRemove方法
- 开启一个线程registryMonitorThread,每sleep 30秒(心跳时间) 移除失活业务服务器记录, 读取存活的xxl_job_registry信息,更新到 xxl_job_group 里面去 。 这个线程被设置为守护线程,通过改变变量标记toStop退出执行
- registry和registryRemove方法被进一步封装到 AdminBizImpl 中,供外部使用
2.4、失败处理器
任务调度执行的时候,会写xxl_job_log 记录。如果调度执行失败,需要重试或者发邮件通知。代码执行逻辑则是:
admin服务端启动一个 monitorThread线程,每隔10秒,扫描失败的记录
通过sql的cas的形式,逐条锁定失败的日志记录来处理
UPDATE xxl_job_log
SET
`alarm_status` = #{newAlarmStatus}
WHERE `id`= #{logId} AND `alarm_status` = #{oldAlarmStatus}
如果配置的重试次数大于0,则先重试
JobTriggerPoolHelper.trigger(log.getJobId(), TriggerTypeEnum.RETRY,
(log.getExecutorFailRetryCount()-1), /*重试次数 -1 */
log.getExecutorShardingParam(),
log.getExecutorParam(), null);
如果配置了email,则发邮件告警
处理完之后,再通过sql的case更新 alarm_status
2.5、 job完成后续处理
如果job正常执行,那admin端只需要正常等着,接收client端的执行结果报告就行了。但是如果执行了好久没返回呢?所以在JobCompleteHelper里面做了这么几件事:
构造一个 callbackThreadPool 线程池,主要用来更新 xxl_job_log 记录 的执行结果
构造一个 monitorThread 线程,处理执行超时的。
找到【调度记录停留在 "运行中" 状态超过10min,且对应执行器心跳注册失败不在线,则将本地调度主动标记失败;】
更新job_log状态结果
2.6、定期清理日志
JobLogReportHelper里面做的事情主要就3点:
- 启动logrThread守护线程,定时扫描xxl_job_log 表
- 统计执行成功失败的数据,这个其实也是admin里面 执行报表数据的来源
- 根据配置,清理久远的xxl_job_log历史日志
2.7、 调度
调度中心的调度逻辑,最源头在这里--- JobScheduleHelper。这个类里主要做了如下几件事情:
启动scheduleThread
通过执行sql,获取数据库锁 (分布式锁),通过这种方式避免多个admin端重复调度
select * from xxl_job_lock where lock_name = 'schedule_lock' for update
根据线程算力,计算出preReadCount,从xxl_job_info 表中找出 未来5秒内要执行的job,暂存 scheduleList
- 超时未调度(超过调度时间5秒)的任务,本次忽略,基于当前时间计算下次执行时间。
- 超过调度时间但未超时(超过5秒之内)的任务,立即放入执行线程池触发一次,再修改执行时间,接着判断下次执行时间若在5秒之内,加入timewheel的map后再次修改下次执行时间。
- 调度时间在未来5秒之内的(预读5s),基于timewheel时间轮(map<秒数,list<任务实体>>),根据5秒内即将执行的任务的执行时间的秒数,将其放到timeheel对应秒数的list中,修改下次执行时间。
启动ringThread ,处理timeRing里面的job
至此,除开某些细节,代码层面上基本差不多了。
3、总结
看完客户端+服务端的代码,现在来回顾小节一下。
3.1、 一次完整的任务调度通讯流程
- “调度中心”向“执行器”发送http调度请求: “执行器”中接收请求的服务,实际上是一台内嵌Server,默认端口9999;
- “执行器”执行任务逻辑;
- “执行器”http回调“调度中心”调度结果: “调度中心”中接收回调的服务,是针对执行器开放一套API服务;
3.2、 整体架构图

3.3、 xxl-job的优点
优点真的很明显:简单、轻量级、易扩展。
框架确实非常清晰、简单。代码写的也很有启发性。比如:
- 充分利用Spring现有的工具类,不重复造轮子
- 使用ThreadPoolExecutor的有参构造方法去创建线程池(阿里巴巴的开发手册里面貌似特意提到这一点)
- 尽量定义枚举类型,比如ExecutorRouteStrategyEnum(这里又将枚举和路由策略结合在一起,算是策略模式的一个变种吧)
- 遵循迪米特法则。比如说:XxlJobAdminConfig中,所有dao都通过这里注入、对外提供。不零散分布在各个类中
- .....
3.3、 xxl-job的建议
目前xxl-job只分了2个子模块:admin和core,admin依赖core
个人觉得再多分出一个模块,结构会更好一些【admin,core,common】,依赖关系改成这样子:

xxl-job源码阅读二(服务端)的更多相关文章
- muduo库源码剖析(二) 服务端
一. TcpServer类: 管理所有的TCP客户连接,TcpServer供用户直接使用,生命期由用户直接控制.用户只需设置好相应的回调函数(如消息处理messageCallback)然后TcpSer ...
- sofa-bolt源码阅读(1)-服务端的启动
Bolt服务器的核心类是RpcServer,启动的时候调用父类AbstractRemotingServer的startup方法. com.alipay.remoting.AbstractRemotin ...
- zookeeper源码分析之五服务端(集群leader)处理请求流程
leader的实现类为LeaderZooKeeperServer,它间接继承自标准ZookeeperServer.它规定了请求到达leader时需要经历的路径: PrepRequestProcesso ...
- zookeeper源码分析之四服务端(单机)处理请求流程
上文: zookeeper源码分析之一服务端启动过程 中,我们介绍了zookeeper服务器的启动过程,其中单机是ZookeeperServer启动,集群使用QuorumPeer启动,那么这次我们分析 ...
- Netty 4源码解析:服务端启动
Netty 4源码解析:服务端启动 1.基础知识 1.1 Netty 4示例 因为Netty 5还处于测试版,所以选择了目前比较稳定的Netty 4作为学习对象.而且5.0的变化也不像4.0这么大,好 ...
- Nacos(二)源码分析Nacos服务端注册示例流程
上回我们讲解了客户端配置好nacos后,是如何进行注册到服务器的,那我们今天来讲解一下服务器端接收到注册实例请求后会做怎么样的处理. 首先还是把博主画的源码分析图例发一下,让大家对整个流程有一个大概的 ...
- Netty源码分析之服务端启动过程
一.首先来看一段服务端的示例代码: public class NettyTestServer { public void bind(int port) throws Exception{ EventL ...
- Spring Cloud系列(三):Eureka源码解析之服务端
一.自动装配 1.根据自动装配原理(详见:Spring Boot系列(二):Spring Boot自动装配原理解析),找到spring-cloud-starter-netflix-eureka-ser ...
- 4. 源码分析---SOFARPC服务端暴露
服务端的示例 我们首先贴上我们的服务端的示例: public static void main(String[] args) { ServerConfig serverConfig = new Ser ...
随机推荐
- 如何优雅地学习计算机编程-C++1
如何优雅的学习计算机编程--C++ 0.导入 如何优雅地学习计算机编程.我们得首先了解编程是什么?打个比方--写信. 大家都知道写信所用的语言双方都懂,这样的信才做到了信息交流,人和计算机也是如此人和 ...
- 从新建文件夹开始构建UtopiaEngine(1)
序言 在苦等了半年多之后,我终于开始了向往已久的实时NPR游戏引擎项目--Utopia Engine,这半年多一直为了构建这个引擎在做很多准备:多线程.动态链接库.脚本引擎.立即渲染GUI--统统吃了 ...
- Java中的IO流 - 入门篇
前言 大家好啊,我是汤圆,今天给大家带来的是<Java中的IO流-入门篇>,希望对大家有帮助,谢谢 由于Java的IO类有很多,这就导致我刚开始学的时候,感觉很乱,每次用到都是上网搜,结果 ...
- Android Studio 之 TextView基础
•引言 在开始本节内容前,先要介绍下几个单位: dp(dip) : device independent pixels(设备独立像素). 不同设备有不同的显示效果,这个和设备硬件有关 一般我们为了支持 ...
- Nginx记录用户请求Header到access log
为了统计和其它用途,经常有人需要自定义Nginx日志,把http请求中的某个字段记录到日志中,刚好在看lua+nginx的文章,第一想到的是用lua赋值来做,但是想想有点小恶心,于是Google了一番 ...
- 使用CSS3中Canvas 实现两张图片合成一张图片【常用于合成二维码图片】
CSS3 Canvas 实现两张图片合成一张图片 需求 需求:在项目中遇到将一张固定图片和一张二维码图片合成一张新图片,并且用户能够将图片保存下载到本地. 思路:使用 CSS3 中的 Canvas 将 ...
- 「新特性」Spring Boot 全局懒加载机制了解一下
关于延迟加载 在 Spring 中,默认情况下所有定的 bean 及其依赖项目都是在应用启动时创建容器上下文是被初始化的.测试代码如下: @Slf4j @Configuration public cl ...
- Object.assign()和解构赋值:给对象赋值的两种方法
一.Object.assign()方法给对象赋值 Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象.它将返回目标对象. 拷贝的是属性值 如果目标对象中的属性 ...
- SpringBoot - yml写法
1 #区分大小写 2 server: 3 port: 8081 4 path: hello 5 6 #字面量:普通的值(数字,字符串,布尔): 7 #字符串:双引号 - 不转义 单引号 - 转义 8 ...
- GO-01-GoLang的快捷键