分布式任务调度平台 XXL-JOB 源码解析
XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。详细介绍请阅读官网文档 : https://www.xuxueli.com/xxl-job/
源码地址:https://github.com/xuxueli/xxl-job/releases/tag/2.3.1
文章中出现“…”,代表省略不重要代码
如:Spring的Aware、InitializingBean;Java的 List,volatile,ConcurrentHashMap,Thread,ThreadPool,BlockingQueue,NIO netty;Cron,Glue,代理模式,单例模式,策略模式…
设计思想
“调度”和“任务”两部分可以相互解耦,提高系统整体稳定性和扩展性
将调度行为抽象形成“调度中心”公共平台,“调度中心”负责发起调度请求。
将任务抽象成分散的JobHandler,交由“执行器”统一管理,“执行器”负责接收调度请求并执行对应的JobHandler中业务逻辑。
系统组成
调度模块(调度中心 admin):负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码。调度系统与任务解耦,提高了系统可用性和稳定性,同时调度系统性能不再受限于任务模块;
执行模块(执行器 executor):负责接收调度请求并执行任务逻辑。
下面详情看图:
biz包定义了心跳,回调,注册,移除注册等方法,定义了执行器和调度中心的交互方式,定义交互接口,并实现的处理逻辑。
context:XxlJobHelper,封装了XxlJob对象,提供了get/set方法
executor:初始化方法,根据不同框架的不同的实现
handler:执行器Handler的顶级父类,定义了@xxljob注解;定义了 init,execute,destroy 三个方法,来实现Job的生命周期
server:NIO实现处理网络请求
thread:Job的实际工作线程
public class XxlJobScheduler {
...
public void init() throws Exception {
// 0、初始化国际化i18n
initI18n();
// 1、初始化任务触发器线程池助手,主要职能:快慢两个线程池异步触发远程执行器
JobTriggerPoolHelper.toStart();
// 2、初始化任务注册助手,主要职能:两个线程分别用于任务执行器注册或删除和轮训(30s)监控执行器注册状态
JobRegistryHelper.getInstance().start();
// 3、初始化任务失败故障监视器助手,主要职能:启动故障任务监视线程查询失败日志进行重试,从而触发告警逻辑和任务重试逻辑。
JobFailMonitorHelper.getInstance().start();
// 4、初始化任务完成助手,主要职能:任务回调线程池 处理执行器响应的任务处理回调接口;任务丢失监视器线程 任务结果丢失处理。
JobCompleteHelper.getInstance().start();
// 5、初始化任务日志报告助手,主要职能:日志报表线程,处理日志统计和清理
JobLogReportHelper.getInstance().start();
// 6、初始化任务调度助手,主要职能:任务调度线程,定时调度任务,任务投递和下次执行时间维护
JobScheduleHelper.getInstance().start();
logger.info(">>>>>>>>> init xxl-job admin success.");
}
}
本文从XxlJobScheduler 开始;i18n不重要,略过
JobTriggerPoolHelper.toStart() 代码如下:
public class JobTriggerPoolHelper {
...
// fast/slow thread pool
private ThreadPoolExecutor fastTriggerPool = null;
private ThreadPoolExecutor slowTriggerPool = null;
public void start(){
// 创建了一个最大核心线程数为10的,最多拥有200个线程数的,空闲线程存活时间为60s的线程池
fastTriggerPool = new ThreadPoolExecutor(...);
// 创建了一个最大核心线程数为10的,最少拥有100个线程数的,空闲线程存活时间为60s的线程池
slowTriggerPool = new ThreadPoolExecutor(...);
}
...
/**
* 触发执行器 add trigger
*/
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;
// 获取超时次数 tips:AtomicInteger用于多线程下线程安全的数据读写操作,避免使用锁同步,底层采用CAS实现,内部的存储值使用volatile修饰,因此多线程之间是修改可见的。
AtomicInteger jobTimeoutCount = jobTimeoutCountMap.get(jobId);
// 一分钟内超时10次,则采用慢触发器执行 job-timeout 10 times in 1 min
if (jobTimeoutCount!=null && jobTimeoutCount.get() > 10) {
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 job执行后和执行前不是同一个分钟值,则清空jobTimeoutCountMap,意义是为了每分钟清空慢作业累计缓存
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 临界点
// 执行时间超过500ms,则记录执行次数。无则put,有则返回值
AtomicInteger timeoutCount = jobTimeoutCountMap.putIfAbsent(jobId, new AtomicInteger(1));
if (timeoutCount != null) {
timeoutCount.incrementAndGet();
}
}
}
}
});
}
JobFailMonitorHelper.getInstance().start(); 代码如下:
/**
* 主要是一个monitorThread线程,每隔10秒查询一次执行日志获取1000执行失败的日志,进行打标(告警标识和重试标识),从而触发告警逻辑和任务重试逻辑。
* @author xuxueli 2015-9-1 18:05:56
*/
public class JobFailMonitorHelper {
...
while (!toStop) {
try {
List<Long> failLogIds = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findFailJobLogIds(1000);
if (failLogIds!=null && !failLogIds.isEmpty()) {
for (long failLogId: failLogIds) {
// lock log
int lockRet = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateAlarmStatus(failLogId, 0, -1);
if (lockRet < 1) {
continue;
}
XxlJobLog log = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().load(failLogId);
XxlJobInfo info = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().loadById(log.getJobId());
// 1、fail retry monitor
if (log.getExecutorFailRetryCount() > 0) {
JobTriggerPoolHelper.trigger(log.getJobId(), TriggerTypeEnum.RETRY, (log.getExecutorFailRetryCount()-1), log.getExecutorShardingParam(), log.getExecutorParam(), null);
String retryMsg = "
>>>>>>>>>>>"+ I18nUtil.getString("jobconf_trigger_type_retry") +"<<<<<<<<<<<
";
log.setTriggerMsg(log.getTriggerMsg() + retryMsg);
XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateTriggerInfo(log);
}
// 2、故障报警监视 fail alarm monitor
int newAlarmStatus = 0; // 告警状态:0-默认、-1=锁定状态、1-无需告警、2-告警成功、3-告警失败
if (info != null) {
boolean alarmResult = XxlJobAdminConfig.getAdminConfig().getJobAlarmer().alarm(info, log);
newAlarmStatus = alarmResult?2:3;
} else {
newAlarmStatus = 1;
}
XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateAlarmStatus(failLogId, -1, newAlarmStatus);
...
try {
TimeUnit.SECONDS.sleep(10);
} catch (Exception e) {
...
JobScheduleHelper.getInstance().start(); 代码如下:任务调度助手 主要有两个线程和一个map,scheduleThread ringThread ringData
public class JobScheduleHelper {
...
public static final long PRE_READ_MS = 5000; // pre read
private Thread scheduleThread;
private Thread ringThread;
private volatile boolean scheduleThreadToStop = false;
private volatile boolean ringThreadToStop = false;
/**
* 这里保存的是以秒为单位,下次执行秒 为key, jobId集合为value
*/
private volatile static Map<Integer, List<Integer>> ringData = new ConcurrentHashMap<>();
/**
* 调度器执行任务(两个线程 + 线程池执行调度逻辑)
* 1. 调度线程50s执行一次;查询5s秒内执行的任务,并按照不同逻辑执行
* 2. 时间轮线程每1秒执行一次;时间轮算法,并向前跨一个时刻;
*/
public void start(){
// 计划线程 schedule thread 查询未来5秒内要执行的数据,同时更新下次执行时间,同时将要执行任务ID和几秒后执行存入map中
scheduleThread = new Thread(new Runnable() {
@Override
public void run() {
try {
// 保证5秒执行一次,确保整点(秒)触发,System.currentTimeMillis()%1000 通过取余的方式获取当前毫秒精度,1000 - System.currentTimeMillis()%1000 差多少毫秒到下一秒,
// 如果集群服务器(xxl-job admin server)上的时间不统一就可能导致多次触发的问题,就需要调整时间精度
TimeUnit.MILLISECONDS.sleep(5000 - System.currentTimeMillis()%1000 );
} catch (InterruptedException e) {
if (!scheduleThreadToStop) {
logger.error(e.getMessage(), e);
}
}
logger.info(">>>>>>>>> init xxl-job admin scheduler success.");
// pre-read count: treadpool-size * trigger-qps (each trigger cost 50ms, qps = 1000/50 = 20)
// 本次最大负载 = 每秒处理20个任务 * (200个FastMax线程 + 100个SlowMax线程)
int preReadCount = (XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax() + XxlJobAdminConfig.getAdminConfig().getTriggerPoolSlowMax()) * 20;
// 开启自旋
while (!scheduleThreadToStop) {
// Scan Job
long start = System.currentTimeMillis();
Connection conn = null;
Boolean connAutoCommit = null;
PreparedStatement preparedStatement = null;
boolean preReadSuc = true;
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();
// tx start
long nowTime = System.currentTimeMillis();
// 一、pre read 获取当前时间后5秒未执行的job,同时最多负载的分页条数(6000)
List<XxlJobInfo> scheduleList = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleJobQuery(nowTime + PRE_READ_MS, preReadCount);
if (scheduleList!=null && scheduleList.size()>0) {
/**
* 二、push time-ring 将超时和将要执行的job放入时间轮中
*
* 这个循环中分为三个判断:
* 1、当前时间 大于(任务的下一次触发时间 + 预读时间):已超时的 和 未来5秒要执行的,要立即根据调度过期策略进行调用
* 2、当前时间 大于 任务的下一次触发时间:这个还没想到什么场景会触发
* 3、当前时间 小于 下一次触发时间:5秒后要执行的
*/
for (XxlJobInfo jobInfo: scheduleList) {
// time-ring jump 时间轮
if (nowTime > jobInfo.getTriggerNextTime() + PRE_READ_MS) {
// 2.1、trigger-expire > 5s:pass && make next-trigger-time
logger.warn(">>>>>>>>>>> xxl-job, schedule misfire, jobId = " + jobInfo.getId());
/**
* 1、调度过期判断 misfire match
* 调度过期策略:
* - 忽略:调度过期后,忽略过期的任务,从当前时间开始重新计算下次触发时间;
* - 立即执行一次:调度过期后,立即执行一次,并从当前时间开始重新计算下次触发时间;
*/
MisfireStrategyEnum misfireStrategyEnum = MisfireStrategyEnum.match(jobInfo.getMisfireStrategy(), MisfireStrategyEnum.DO_NOTHING);
if (MisfireStrategyEnum.FIRE_ONCE_NOW == misfireStrategyEnum) {
// FIRE_ONCE_NOW 》 trigger 触发
JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.MISFIRE, -1, null, null, null);
logger.debug(">>>>>>>>>>> xxl-job, schedule push trigger : jobId = " + jobInfo.getId() );
}
// 2、fresh next 更新下次执行时间
refreshNextValidTime(jobInfo, new Date());
} else if (nowTime > jobInfo.getTriggerNextTime()) {
// 2.2、trigger-expire < 5s:direct-trigger && make next-trigger-time
// 1、trigger 触发
JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.CRON, -1, null, null, null);
logger.debug(">>>>>>>>>>> xxl-job, schedule push trigger : jobId = " + jobInfo.getId() );
// 2、fresh next 更新下次执行时间
refreshNextValidTime(jobInfo, new Date());
// 下次触发时间在当前时间往后5秒范围内,直接放进TimeRing时间轮里面待调度 next-trigger-time in 5s, pre-read again
if (jobInfo.getTriggerStatus()==1 && nowTime + PRE_READ_MS > jobInfo.getTriggerNextTime()) {
// 1、当前任务下一次触发时间所处一分钟的第N秒,make ring second
int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);
// 2、添加到时间轮 push time ring
pushTimeRing(ringSecond, jobInfo.getId());
// 3、更新下次执行时间 fresh next
refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));
}
} else {
// 2.3、trigger-pre-read:time-ring trigger && make next-trigger-time
// 1、当前任务下一次触发时间所处一分钟的第N秒,make ring second
int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);
// 2、添加到时间轮 push time ring
pushTimeRing(ringSecond, jobInfo.getId());
// 3、更新下次执行时间 fresh next
refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));
}
}
// 3、更新执行时间和上次执行时间到数据库 update trigger info
for (XxlJobInfo jobInfo: scheduleList) {
XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleUpdate(jobInfo);
}
} else {
preReadSuc = false;
}
// tx stop
} catch (Exception e) {
if (!scheduleThreadToStop) {
logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#scheduleThread error:{}", e);
}
} finally {
// commit
if (conn != null) {
try {
conn.commit();
} catch (SQLException e) {
if (!scheduleThreadToStop) {
logger.error(e.getMessage(), e);
}
}
try {
conn.setAutoCommit(connAutoCommit);
} catch (SQLException e) {
if (!scheduleThreadToStop) {
logger.error(e.getMessage(), e);
}
}
try {
conn.close();
} catch (SQLException e) {
if (!scheduleThreadToStop) {
logger.error(e.getMessage(), e);
}
}
}
// close PreparedStatement
if (null != preparedStatement) {
try {
preparedStatement.close();
} catch (SQLException e) {
if (!scheduleThreadToStop) {
logger.error(e.getMessage(), e);
}
}
}
}
long cost = System.currentTimeMillis()-start;
// 如果1秒内执行完了,则对齐1秒 Wait seconds, align second
if (cost < 1000) { // scan-overtime, not wait
try {
// pre-read period: success > scan each second; fail > skip this period; 如果没有任务会对齐5秒
TimeUnit.MILLISECONDS.sleep((preReadSuc?1000:PRE_READ_MS) - System.currentTimeMillis()%1000);
} catch (InterruptedException e) {
if (!scheduleThreadToStop) {
logger.error(e.getMessage(), e);
}
}
}
}
logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper#scheduleThread stop");
}
});
scheduleThread.setDaemon(true);
scheduleThread.setName("xxl-job, admin JobScheduleHelper#scheduleThread");
scheduleThread.start();
// 环线程 ring thread,每次整秒校验一次map中是否有要触发的任务,如果有则投递到JobTriggerPoolHelper的线程池中
ringThread = new Thread(new Runnable() {
@Override
public void run() {
while (!ringThreadToStop) {
// align second
try {
TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis() % 1000);
} catch (InterruptedException e) {
if (!ringThreadToStop) {
logger.error(e.getMessage(), e);
}
}
try {
// second data
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);
}
}
// ring trigger
logger.debug(">>>>>>>>>>> xxl-job, time-ring beat : " + nowSecond + " = " + Arrays.asList(ringItemData) );
if (ringItemData.size() > 0) {
// do trigger
for (int jobId: ringItemData) {
// do trigger
JobTriggerPoolHelper.trigger(jobId, TriggerTypeEnum.CRON, -1, null, null, null);
}
// clear
ringItemData.clear();
}
} catch (Exception e) {
if (!ringThreadToStop) {
logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread error:{}", e);
}
}
}
logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread stop");
}
});
ringThread.setDaemon(true);
ringThread.setName("xxl-job, admin JobScheduleHelper#ringThread");
ringThread.start();
}
@Configuration
public class XxlJobConfig {
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
...
return xxlJobSpringExecutor;
}
}
public class XxlJobSpringExecutor extends XxlJobExecutor implements ApplicationContextAware, SmartInitializingSingleton, DisposableBean {
@Override
public void afterSingletonsInstantiated() {
// init JobHandler Repository
/*initJobHandlerRepository(applicationContext);*/
// 将类中含有@xxljob的方法,保存到this.jobHandlerRepository中 init JobHandler Repository (for method)
initJobHandlerMethodRepository(applicationContext);
// 刷新胶水工厂 refresh GlueFactory
GlueFactory.refreshInstance(1);
// 调用父类 super start
super.start();
}
...
private void initJobHandlerMethodRepository(ApplicationContext applicationContext) {
if (applicationContext == null) {
return;
}
// init job handler from method
String[] beanDefinitionNames = applicationContext.getBeanNamesForType(Object.class, false, true);
for (String beanDefinitionName : beanDefinitionNames) {
Object bean = applicationContext.getBean(beanDefinitionName);
Map<Method, XxlJob> annotatedMethods = null; // referred to :org.springframework.context.event.EventListenerMethodProcessor.processBean
try {
// 工具类MethodIntrospector 定义了搜索元数据相关方法的算法,包括接口和父类,同时也处理了参数化的方法和基于接口和类的代理所遇到的常见情况
annotatedMethods = MethodIntrospector.selectMethods(bean.getClass(),
new MethodIntrospector.MetadataLookup<XxlJob>() {
@Override
public XxlJob inspect(Method method) {
// 搜索注解的工具类,元注解和可重复注解的常规实用程序方法,请注意,JDK的内省工具本身不提供此类的功能
// AnnotatedElementUtils为Spring的元注解编程模型定义了公共API,并支持注解属性覆盖。如果您不需要支持注解属性覆盖,请考虑使用AnnotationUtils。
return AnnotatedElementUtils.findMergedAnnotation(method, XxlJob.class);
}
});
} catch (Throwable ex) {
logger.error("xxl-job method-jobhandler resolve error for bean[" + beanDefinitionName + "].", ex);
}
if (annotatedMethods==null || annotatedMethods.isEmpty()) {
continue;
}
// 这里将所有类中包含xxljob的注解的方法
for (Map.Entry<Method, XxlJob> methodXxlJobEntry : annotatedMethods.entrySet()) {
Method executeMethod = methodXxlJobEntry.getKey();
XxlJob xxlJob = methodXxlJobEntry.getValue();
// regist
registJobHandler(xxlJob, bean, executeMethod);
}
}
}
}
XxlJobExecutor 中的start方法有嵌入执行服务调用了 initEmbedServer(address, ip, port, appname, accessToken)方法,实例了EmbedServer启动了netty对外提供端口监听 :
public class XxlJobExecutor {
public void start() throws Exception {
// 一、init logpath 初始化日志目录,用来存储调度日志执行指令到磁盘
XxlJobFileAppender.initLogPath(logPath);
// 二、init invoker 初始化rpc调用地址,将addresses + accessToken封装AdminBizClient对象放入adminBizList
initAdminBizList(adminAddresses, accessToken);
// 三、init JobLogFileCleanThread 清除过期日志(默认为30,小于3天不清理) 根据存储路径目录的日志(目录名为时间),根据其目录时间进行删除(每天/1次)
JobLogFileCleanThread.getInstance().start(logRetentionDays);
// 四、init TriggerCallbackThread 回调调度中心任务执行状态
TriggerCallbackThread.getInstance().start();
/**
* 五、init executor-server 嵌入执行服务
* 1.使用netty开放端口,等待服务端调用
* 2.维护心跳时间到服务端(心跳30S)
* 3.向服务端申请剔除服务
*/
initEmbedServer(address, ip, port, appname, accessToken);
}
public class EmbedServer {
public void start(final String address, final int port, final String appname, final String accessToken) {
...
ThreadPoolExecutor bizThreadPool = new ThreadPoolExecutor(....);
...
ServerBootstrap bootstrap = new ServerBootstrap();
...
bootstrap.addLast(new EmbedHttpServerHandler(executorBiz, accessToken, bizThreadPool));
...
// start registry 开始注册,这里调用了ExecutorRegistryThread
startRegistry(appname, address);
// wait util stop netty持续运行,等到停止
future.channel().closeFuture().sync();
}
}
public static class EmbedHttpServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
...
@Override
protected void channelRead0(final ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
...
// do invoke
Object responseObj = process(httpMethod, uri, requestData, accessTokenReq);
...
}
private Object process(HttpMethod httpMethod, String uri, String requestData, String accessTokenReq) {
...
// services mapping
try {
if ("/beat".equals(uri)) {
return executorBiz.beat();
} else if ("/idleBeat".equals(uri)) {
IdleBeatParam idleBeatParam = GsonTool.fromJson(requestData, IdleBeatParam.class);
return executorBiz.idleBeat(idleBeatParam);
} else if ("/run".equals(uri)) { // 触发执行器
TriggerParam triggerParam = GsonTool.fromJson(requestData, TriggerParam.class);
return executorBiz.run(triggerParam);
} else if ("/kill".equals(uri)) {
KillParam killParam = GsonTool.fromJson(requestData, KillParam.class);
return executorBiz.kill(killParam);
} else if ("/log".equals(uri)) {
LogParam logParam = GsonTool.fromJson(requestData, LogParam.class);
return executorBiz.log(logParam);
} else {
return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, uri-mapping("+ uri +") not found.");
}
...
}
上面的process方法,目的就是接收调度器的统一调度请求后,判断要执行哪种逻辑。
ExecutorRegistryThread {
...
public void start(final String appname, final String address){
...
// 遍历所有的调度中心
for (AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()) {
// 注册到调度中心
// registry往下看就是RPC调用,采用的是HTTP传输协议,并采用了JSON作为序列化。
// 可以再细看 com.xxl.job.core.util.XxlJobRemotingUtil,postBody采用就是Http协议,GsonTool将对象转成JSON。
ReturnT<String> registryResult = adminBiz.registry(registryParam);
}
}
...
}
本文介绍了xxl-job的原理和重要代码分析,并对调用流程进行了分析画出流程图。框架中使用了大量的线程,线程池和Queue队列等技术来实现功能,使用的设计模式也是非常巧妙,非常值得学习和使用。将笔记整理出来发布,请各位大佬多多指正。
本人学习时阅读了这篇文章,感谢作者
链接: https://blog.csdn.net/segegefe/article/details/126064861