XXL-JOB原理--定时任务框架简介
一、完整介绍地址:官方介绍
https://www.xuxueli.com/xxl-job/#/?id=%E4%B8%80%E3%80%81%E7%AE%80%E4%BB%8B
二、最新版本架构图:
三、介绍
目前我们在项目中可能接触到定时任务框架quartz,应用也是比较广泛的,其也是支持分布式任务调度的,通过数据库竞争锁来实现,当然会有很多的局限性(可能这也是xxl-job出现的原因),quartz支持多种数据库(quartz/quartz-core/src/main/resources/org/quartz/impl/jdbcjobstore at master · quartz-scheduler/quartz · GitHub),xxl-job其实也是在quartz的基础上实现的,但是修改了任务调度的模式,并且任务调度采用注册和RPC调用方式来实现。
管理后台:
四、技术栈
mysql、SSM,内置jetty作为RPC服务调用、quartz
五、xxl-job支持Postgresql数据库
目前由于xxl-job只支持mysql数据库,目前在github上拉了一个分支支持Postgresql 地址GitHub地址
六、 xxl-job 底层依赖 quartz 实现,架构如下
--------------------------------------------------------------------------------------------------------------------------------------------------
1、xxl-job添加执行器到任务调度中心有两种方式
(1)客户端执行器自动将名称和机器地址注册到任务调度中心
(2)可以在任务调度中心手动录入执行器名称和相关的机器地址(多个机器地址用逗号隔开)
2、自动注册流程
(1)在执行器客户端配置执行器名称和任务调度中心地址:
### xxl-job admin address list, such as "http://address" or "http://address01,http://address02"
##任务调度中心地址
xxl.job.admin.addresses=http://127.0.0.1:8080
### xxl-job executor address
##任务调度器名称和机器信息
xxl.job.executor.appname=xxl-job-executor-sample
xxl.job.executor.ip=127.0.0.1
xxl.job.executor.port=9998
(2)相关注册执行代码:
在执行器启动时会读取配置,当存在任务调度中心地址会依次向任务调度中心注册其地址
XxlJobExecutor类在进行初始化时会进行如下操作。
//当存在多个任务调度中心时,创建代理类并注册,在NetComClientProxy
private static void initAdminBizList(String adminAddresses, String accessToken) throws Exception {
if (adminAddresses!=null && adminAddresses.trim().length()>0) {
for (String address: adminAddresses.trim().split(",")) {
if (address!=null && address.trim().length()>0) {
String addressUrl = address.concat(AdminBiz.MAPPING);
AdminBiz adminBiz = (AdminBiz) new NetComClientProxy(AdminBiz.class, addressUrl, accessToken).getObject();
if (adminBizList == null) {
adminBizList = new ArrayList<AdminBiz>();
}
adminBizList.add(adminBiz);
}
}
}
}
在XxlJobExecutor被调用时执行getObject方法,完成向任务调度中心发送请求进行服务注册操作。
@Override
public Object getObject() throws Exception {
return Proxy.newProxyInstance(Thread.currentThread()
.getContextClassLoader(), new Class[] { iface },
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// filter method like "Object.toString()"
if (Object.class.getName().equals(method.getDeclaringClass().getName())) {
logger.error(">>>>>>>>>>> xxl-rpc proxy class-method not support [{}.{}]", method.getDeclaringClass().getName(), method.getName());
throw new RuntimeException("xxl-rpc proxy class-method not support");
}
// request
RpcRequest request = new RpcRequest();
request.setServerAddress(serverAddress);
request.setCreateMillisTime(System.currentTimeMillis());
request.setAccessToken(accessToken);
request.setClassName(method.getDeclaringClass().getName());
request.setMethodName(method.getName());
request.setParameterTypes(method.getParameterTypes());
request.setParameters(args);
// send
//向任务调度中心发送请求进行服务注册
RpcResponse response = client.send(request);
// valid response
if (response == null) {
throw new Exception("Network request fail, response not found.");
}
if (response.isError()) {
throw new RuntimeException(response.getError());
} else {
return response.getResult();
}
}
});
在JettyClient中调用send方法完成服务注册操作
public RpcResponse send(RpcRequest request) throws Exception {
try {
// serialize request
byte[] requestBytes = HessianSerializer.serialize(request);
// reqURL
String reqURL = request.getServerAddress();
if (reqURL!=null && reqURL.toLowerCase().indexOf("http")==-1) {
reqURL = "http://" + request.getServerAddress() + "/"; // IP:PORT, need parse to url
}
//发送post请求进行服务注册,简单注册一下IP和端口信息等
// remote invoke
byte[] responseBytes = HttpClientUtil.postRequest(reqURL, requestBytes);
if (responseBytes == null || responseBytes.length==0) {
RpcResponse rpcResponse = new RpcResponse();
rpcResponse.setError("Network request fail, RpcResponse byte[] is null");
return rpcResponse;
}
// deserialize response
RpcResponse rpcResponse = (RpcResponse) HessianSerializer.deserialize(responseBytes, RpcResponse.class);
return rpcResponse;
} catch (Exception e) {
logger.error(e.getMessage(), e);
RpcResponse rpcResponse = new RpcResponse();
rpcResponse.setError("Network request error: " + e.getMessage());
return rpcResponse;
}
}
服务注册地址:http://127.0.0.1:8080/api
相关注册信息如下:
总结:其实执行器注册到任务调度的信息非常简单,可以就简单的认为为应用的一些基本信息,IP、端口和应用名称等等,并不用将具体的任务类等信息注册到任务调度中心,所以任务调度中心无法感知执行器一些具体信息,只能需要靠运维和技术人员在任务调度中心进行选择配置,否则可能会无法正常执行任务。
————————————————
一、注册地址
地址:http://127.0.0.1:8080/api
任务调度中心对外提供注册地址/api用来接受任务执行器注册的相关服务器信息
1、xxl-job admin通过JobApiController来对外提供/api接口
@Controller
public class JobApiController {
private static Logger logger = LoggerFactory.getLogger(JobApiController.class);
private RpcResponse doInvoke(HttpServletRequest request) {
try {
// deserialize request
byte[] requestBytes = HttpClientUtil.readBytes(request);
if (requestBytes == null || requestBytes.length==0) {
RpcResponse rpcResponse = new RpcResponse();
rpcResponse.setError("RpcRequest byte[] is null");
return rpcResponse;
}
//反序列化数据
RpcRequest rpcRequest = (RpcRequest) HessianSerializer.deserialize(requestBytes, RpcRequest.class);
// invoke
//调用服务注册方法
RpcResponse rpcResponse = NetComServerFactory.invokeService(rpcRequest, null);
return rpcResponse;
} catch (Exception e) {
logger.error(e.getMessage(), e);
RpcResponse rpcResponse = new RpcResponse();
rpcResponse.setError("Server-error:" + e.getMessage());
return rpcResponse;
}
}
//对外提供api接口
@RequestMapping(AdminBiz.MAPPING)
@PermessionLimit(limit=false)
public void api(HttpServletRequest request, HttpServletResponse response) throws IOException {
// invoke
RpcResponse rpcResponse = doInvoke(request);
// serialize response
byte[] responseBytes = HessianSerializer.serialize(rpcResponse);
response.setContentType("text/html;charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
//baseRequest.setHandled(true);
OutputStream out = response.getOutputStream();
out.write(responseBytes);
out.flush();
}
}
在NetComServerFactory类中调用invokeService方法,根据反射调用AdminBiz接口的实现类AdminBizImpl的register方法完成服务注册操作
public static RpcResponse invokeService(RpcRequest request, Object serviceBean) {
if (serviceBean==null) {
serviceBean = serviceMap.get(request.getClassName());
}
if (serviceBean == null) {
// TODO
}
RpcResponse response = new RpcResponse();
if (System.currentTimeMillis() - request.getCreateMillisTime() > 180000) {
response.setResult(new ReturnT<String>(ReturnT.FAIL_CODE, "The timestamp difference between admin and executor exceeds the limit."));
return response;
}
if (accessToken!=null && accessToken.trim().length()>0 && !accessToken.trim().equals(request.getAccessToken())) {
response.setResult(new ReturnT<String>(ReturnT.FAIL_CODE, "The access token[" + request.getAccessToken() + "] is wrong."));
return response;
}
try {
//接口AdminBiz的实现类AdminBizImpl
Class<?> serviceClass = serviceBean.getClass();
//AdminBiz的register方法
String methodName = request.getMethodName();
Class<?>[] parameterTypes = request.getParameterTypes();
Object[] parameters = request.getParameters();
FastClass serviceFastClass = FastClass.create(serviceClass);
//调用AdminBizImpl的register方法
FastMethod serviceFastMethod = serviceFastClass.getMethod(methodName, parameterTypes);
Object result = serviceFastMethod.invoke(serviceBean, parameters);
response.setResult(result);
} catch (Throwable t) {
t.printStackTrace();
response.setError(t.getMessage());
}
return response;
}
调用的类:
参数:基本的服务器信息
在AdminBiz的实现类AdminBizImpl中调用dao完成注册信息入库操作:
@Override
public ReturnT<String> registry(RegistryParam registryParam) {
//在xxl_job_qrtz_trigger_registry表中添加或更新数据
int ret = xxlJobRegistryDao.registryUpdate(registryParam.getRegistGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue());
if (ret < 1) {
xxlJobRegistryDao.registrySave(registryParam.getRegistGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue());
}
return ReturnT.SUCCESS;
}
表中数据:
在任务调度中心会列表展示数据:
总结:简单来说执行器将其服务器信息通过http请求发送到任务调度中心,调度中心将相关注册信息插入或更新到xxl_job_qrtz_trigger_registry表中,在任务调度中心通过列表展示,接下来相关任务执行时会根据服务器信息通过http发送调用请求。
————————————————
在任务调度中心可以进行新建任务,新建任务之后可以在任务列表中查看相关任务,任务可以根据我们配置的cron表达式进行任务调度,或者也可以在任务列表中执行、暂停、删除和查看相关运行日志等操作。
一、任务调度中心管理
1、新建任务
2、任务列表任务操作
二、任务创建与操作
我们了解到xxl-job是基于quartz来实现定时任务的(其实任务调度中心任务执行基于quartz,任务执行器就是一个被调用执行的服务)
1、任务创建
调用JobInfoController的add方法添加任务
@RequestMapping("/add")
@ResponseBody
public ReturnT<String> add(XxlJobInfo jobInfo) {
//添加定时器
return xxlJobService.add(jobInfo);
}
调用XxlJobService的add方法,新建quartz任务
@Override
public ReturnT<String> add(XxlJobInfo jobInfo) {
// valid
//获取任务分组
XxlJobGroup group = xxlJobGroupDao.load(jobInfo.getJobGroup());
//省略部分任务相关信息检查的代码
// add in db
//将任务持久化到数据库的xxl_job_qrtz_trigger_info
xxlJobInfoDao.save(jobInfo);
if (jobInfo.getId() < 1) {
return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_add")+I18nUtil.getString("system_fail")) );
}
// add in quartz
String qz_group = String.valueOf(jobInfo.getJobGroup());
String qz_name = String.valueOf(jobInfo.getId());
try {
//添加任务到quartz中
XxlJobDynamicScheduler.addJob(qz_name, qz_group, jobInfo.getJobCron());
//XxlJobDynamicScheduler.pauseJob(qz_name, qz_group);
return ReturnT.SUCCESS;
} catch (SchedulerException e) {
logger.error(e.getMessage(), e);
try {
xxlJobInfoDao.delete(jobInfo.getId());
XxlJobDynamicScheduler.removeJob(qz_name, qz_group);
} catch (SchedulerException e1) {
logger.error(e.getMessage(), e1);
}
return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_add")+I18nUtil.getString("system_fail"))+":" + e.getMessage());
}
}
数据库持久化数据:
在XxlJobDynamicScheduler中调用quartz的创建任务方法构建定时任务,定时任务执行类为QuartzJobBean的子类RemoteHttpJobBean,在定时任务执行时RemoteHttpJobBean会调用任务执行器接口,执行相关任务
public static boolean addJob(String jobName, String jobGroup, String cronExpression) throws SchedulerException {
// TriggerKey : name + group
//创建定时器别名
TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);
JobKey jobKey = new JobKey(jobName, jobGroup);
// TriggerKey valid if_exists
if (checkExists(jobName, jobGroup)) {
logger.info(">>>>>>>>> addJob fail, job already exist, jobGroup:{}, jobName:{}", jobGroup, jobName);
return false;
}
// CronTrigger : TriggerKey + cronExpression // withMisfireHandlingInstructionDoNothing 忽略掉调度终止过程中忽略的调度
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression).withMisfireHandlingInstructionDoNothing();
//创建cron定时器
CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity(triggerKey).withSchedule(cronScheduleBuilder).build();
// JobDetail : jobClass
//定时执行类
Class<? extends Job> jobClass_ = RemoteHttpJobBean.class; // Class.forName(jobInfo.getJobClass());
//创建任务
JobDetail jobDetail = JobBuilder.newJob(jobClass_).withIdentity(jobKey).build();
/*if (jobInfo.getJobData()!=null) {
JobDataMap jobDataMap = jobDetail.getJobDataMap();
jobDataMap.putAll(JacksonUtil.readValue(jobInfo.getJobData(), Map.class));
// JobExecutionContext context.getMergedJobDataMap().get("mailGuid");
}*/
// schedule : jobDetail + cronTrigger
//添加定时任务到quartz
Date date = scheduler.scheduleJob(jobDetail, cronTrigger);
logger.info(">>>>>>>>>>> addJob success, jobDetail:{}, cronTrigger:{}, date:{}", jobDetail, cronTrigger, date);
return true;
}
2、删除任务
删除任务简单来说就是将任务从quartz中移除,不再执行就好
JobInfoController中提供了删除任务接口
@RequestMapping("/remove")
@ResponseBody
public ReturnT<String> remove(int id) {
return xxlJobService.remove(id);
}
在XxlJobService中根据逻辑id删除任务,并且删除数据库中相关持久化数据
@Override
public ReturnT<String> remove(int id) {
XxlJobInfo xxlJobInfo = xxlJobInfoDao.loadById(id);
String group = String.valueOf(xxlJobInfo.getJobGroup());
String name = String.valueOf(xxlJobInfo.getId());
try {
//从quartz中移除任务,并将数据库中相关数据删除
XxlJobDynamicScheduler.removeJob(name, group);
xxlJobInfoDao.delete(id);
xxlJobLogDao.delete(id);
xxlJobLogGlueDao.deleteByJobId(id);
return ReturnT.SUCCESS;
} catch (SchedulerException e) {
logger.error(e.getMessage(), e);
}
return ReturnT.FAIL;
}
在XxlJobDynamicScheduler中调用removeJob方法删除相关任务
public static boolean removeJob(String jobName, String jobGroup) throws SchedulerException {
// TriggerKey : name + group
TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);
boolean result = false;
if (checkExists(jobName, jobGroup)) {
//删除任务
result = scheduler.unscheduleJob(triggerKey);
logger.info(">>>>>>>>>>> removeJob, triggerKey:{}, result [{}]", triggerKey, result);
}
return true;
}
3、暂停任务
暂停任务同意调用quartz的接口进行任务暂停处理
JobInfoController中提供接口处理
@RequestMapping("/pause")
@ResponseBody
public ReturnT<String> pause(int id) {
return xxlJobService.pause(id);
}
调用XxlJobService的paus进行任务停止
@Override
public ReturnT<String> pause(int id) {
XxlJobInfo xxlJobInfo = xxlJobInfoDao.loadById(id);
String group = String.valueOf(xxlJobInfo.getJobGroup());
String name = String.valueOf(xxlJobInfo.getId());
try {
boolean ret = XxlJobDynamicScheduler.pauseJob(name, group); // jobStatus do not store
return ret?ReturnT.SUCCESS:ReturnT.FAIL;
} catch (SchedulerException e) {
logger.error(e.getMessage(), e);
return ReturnT.FAIL;
}
}
调用XxlJobDynamicScheduler的pauseJob进行任务停止,最终还是执行quartz 的接口完成任务暂停
public static boolean pauseJob(String jobName, String jobGroup) throws SchedulerException {
// TriggerKey : name + group
TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);
boolean result = false;
if (checkExists(jobName, jobGroup)) {
//任务暂停
scheduler.pauseTrigger(triggerKey);
result = true;
logger.info(">>>>>>>>>>> pauseJob success, triggerKey:{}", triggerKey);
} else {
logger.info(">>>>>>>>>>> pauseJob fail, triggerKey:{}", triggerKey);
}
return result;
}
4、恢复任务
恢复任务就是将暂停的任务继续重新执行
JobInfoController中提供接口重新执行任务
@RequestMapping("/resume")
@ResponseBody
public ReturnT<String> resume(int id) {
return xxlJobService.resume(id);
}
调用XxlJobService的resume接口继续任务
@Override
public ReturnT<String> resume(int id) {
XxlJobInfo xxlJobInfo = xxlJobInfoDao.loadById(id);
String group = String.valueOf(xxlJobInfo.getJobGroup());
String name = String.valueOf(xxlJobInfo.getId());
try {
//继续任务
boolean ret = XxlJobDynamicScheduler.resumeJob(name, group);
return ret?ReturnT.SUCCESS:ReturnT.FAIL;
} catch (SchedulerException e) {
logger.error(e.getMessage(), e);
return ReturnT.FAIL;
}
}
在XllJobDynamicScheduler中调用quartz的接口重新开启任务
public static boolean resumeJob(String jobName, String jobGroup) throws SchedulerException {
// TriggerKey : name + group
TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);
boolean result = false;
if (checkExists(jobName, jobGroup)) {
//继续任务
scheduler.resumeTrigger(triggerKey);
result = true;
logger.info(">>>>>>>>>>> resumeJob success, triggerKey:{}", triggerKey);
} else {
logger.info(">>>>>>>>>>> resumeJob fail, triggerKey:{}", triggerKey);
}
return result;
}
5、更新任务
任务更新操作就是更新一下任务相关参数
XxlJobController中提供update接口,完成任务更新操作
@RequestMapping("/update")
@ResponseBody
public ReturnT<String> update(XxlJobInfo jobInfo) {
return xxlJobService.update(jobInfo);
}
在XxlJobService中调用update更新任务,并更新相关数据库中的数据
@Override
public ReturnT<String> update(XxlJobInfo jobInfo) {
//省略部分判断代码
// stage job info
XxlJobInfo exists_jobInfo = xxlJobInfoDao.loadById(jobInfo.getId());
if (exists_jobInfo == null) {
return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_id")+I18nUtil.getString("system_not_found")) );
}
//String old_cron = exists_jobInfo.getJobCron();
exists_jobInfo.setJobCron(jobInfo.getJobCron());
exists_jobInfo.setJobDesc(jobInfo.getJobDesc());
exists_jobInfo.setAuthor(jobInfo.getAuthor());
exists_jobInfo.setAlarmEmail(jobInfo.getAlarmEmail());
exists_jobInfo.setExecutorRouteStrategy(jobInfo.getExecutorRouteStrategy());
exists_jobInfo.setExecutorHandler(jobInfo.getExecutorHandler());
exists_jobInfo.setExecutorParam(jobInfo.getExecutorParam());
exists_jobInfo.setExecutorBlockStrategy(jobInfo.getExecutorBlockStrategy());
exists_jobInfo.setExecutorFailStrategy(jobInfo.getExecutorFailStrategy());
exists_jobInfo.setExecutorTimeout(jobInfo.getExecutorTimeout());
exists_jobInfo.setChildJobId(jobInfo.getChildJobId());
xxlJobInfoDao.update(exists_jobInfo);
// fresh quartz
String qz_group = String.valueOf(exists_jobInfo.getJobGroup());
String qz_name = String.valueOf(exists_jobInfo.getId());
try {
//重新配置定时任务
boolean ret = XxlJobDynamicScheduler.rescheduleJob(qz_group, qz_name, exists_jobInfo.getJobCron());
return ret?ReturnT.SUCCESS:ReturnT.FAIL;
} catch (SchedulerException e) {
logger.error(e.getMessage(), e);
}
return ReturnT.FAIL;
}
在XxlJobDynamicScheduler中调用quartz相关方法重新配置定时任务
public static boolean rescheduleJob(String jobGroup, String jobName, String cronExpression) throws SchedulerException {
// TriggerKey valid if_exists
if (!checkExists(jobName, jobGroup)) {
logger.info(">>>>>>>>>>> rescheduleJob fail, job not exists, JobGroup:{}, JobName:{}", jobGroup, jobName);
return false;
}
// TriggerKey : name + group
TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);
CronTrigger oldTrigger = (CronTrigger) scheduler.getTrigger(triggerKey);
//任务存在直接更新一下
if (oldTrigger != null) {
// avoid repeat
String oldCron = oldTrigger.getCronExpression();
if (oldCron.equals(cronExpression)){
return true;
}
// CronTrigger : TriggerKey + cronExpression
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression).withMisfireHandlingInstructionDoNothing();
oldTrigger = oldTrigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(cronScheduleBuilder).build();
// rescheduleJob
scheduler.rescheduleJob(triggerKey, oldTrigger);
} else {
//不存在和直接创建新的任务类似
// CronTrigger : TriggerKey + cronExpression
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression).withMisfireHandlingInstructionDoNothing();
CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity(triggerKey).withSchedule(cronScheduleBuilder).build();
// JobDetail-JobDataMap fresh
JobKey jobKey = new JobKey(jobName, jobGroup);
JobDetail jobDetail = scheduler.getJobDetail(jobKey);
/*JobDataMap jobDataMap = jobDetail.getJobDataMap();
jobDataMap.clear();
jobDataMap.putAll(JacksonUtil.readValue(jobInfo.getJobData(), Map.class));*/
// Trigger fresh
HashSet<Trigger> triggerSet = new HashSet<Trigger>();
triggerSet.add(cronTrigger);
scheduler.scheduleJob(jobDetail, triggerSet, true);
}
logger.info(">>>>>>>>>>> resumeJob success, JobGroup:{}, JobName:{}", jobGroup, jobName);
return true;
}
6、立即执行任务
在任务调度中心可以立即进行任务调用
在JobInfoController中提供接口直接进行任务调用
@RequestMapping("/trigger")
@ResponseBody
public ReturnT<String> triggerJob(int id) {
return xxlJobService.triggerJob(id);
}
在XxlJobService中调用接口triggerJob可以直接进行任务调度
@Override
public ReturnT<String> triggerJob(int id) {
JobTriggerPoolHelper.trigger(id);
return ReturnT.SUCCESS;
在JobTriggerPoolHelper的trigger方法会将任务id添加多线程中,最终的实现同样是调用RemoteHttpJobBean的executeInternal进行任务调度,不是依赖quartz实现,接下来我们会用一篇博客详细讲解一下。
————————————————
一、任务调度中心发送任务执行请求
任务发送执行的操作有两种:
(1)根据配置的cron表达式周期性执行相关任务
(2)在任务调度中心主动执行任务
在注册quartz定时任务时已经注册执行类为RemoteHttpJobBean,所以周期性执行定时任务会调用RemoteHttpJobBean的executeInternal方法,在executeInternal中会调用JobTriggerPoolHelper.trigger(jobId),通过任务调度中心主动执行任务时也是会调用JobTriggerPoolHelper.trigger(jobId)方法,所以接下来我们要看的是JobTriggerPoolHelper.trigger(jobId)中做的逻辑处理就好。
public class RemoteHttpJobBean extends QuartzJobBean {
private static Logger logger = LoggerFactory.getLogger(RemoteHttpJobBean.class);
@Override
protected void executeInternal(JobExecutionContext context)
throws JobExecutionException {
// load jobId
JobKey jobKey = context.getTrigger().getJobKey();
Integer jobId = Integer.valueOf(jobKey.getName());
// trigger
//XxlJobTrigger.trigger(jobId);
JobTriggerPoolHelper.trigger(jobId);
}
}
JobTriggerPoolHelper.trigger所做的操作是将任务提交给一个线程池(任务调度中心默认开启50个线程),在线程池中调用XxlJobTrigger.trigger。
public void addTrigger(final int jobId){
triggerPool.execute(new Runnable() {
@Override
public void run() {
XxlJobTrigger.trigger(jobId);
}
});
}
在XxlJobTrigger.trigger中会根据jobId获取任务的基本配置信息(阻塞策略、路由策略、失败重试测试、分组服务器列表等等),然后根据路由策略选择是广播还是单播等,接下来就是组装消息体调用runExecutor方法发送http请求到任务执行器。
public static void trigger(int jobId) {
//获取任务信息
// load data
XxlJobInfo jobInfo = XxlJobDynamicScheduler.xxlJobInfoDao.loadById(jobId); // job info
if (jobInfo == null) {
logger.warn(">>>>>>>>>>>> trigger fail, jobId invalid,jobId={}", jobId);
return;
}
//根据任务的分组信息找到分组,分组中存在服务器的IP和端口地址等
XxlJobGroup group = XxlJobDynamicScheduler.xxlJobGroupDao.load(jobInfo.getJobGroup()); // group info
//阻塞策略
ExecutorBlockStrategyEnum blockStrategy = ExecutorBlockStrategyEnum.match(jobInfo.getExecutorBlockStrategy(), ExecutorBlockStrategyEnum.SERIAL_EXECUTION); // block strategy
//失败策略
ExecutorFailStrategyEnum failStrategy = ExecutorFailStrategyEnum.match(jobInfo.getExecutorFailStrategy(), ExecutorFailStrategyEnum.NULL); // fail strategy
//执行路由测试
ExecutorRouteStrategyEnum executorRouteStrategyEnum = ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null); // route strategy
//服务器地址
ArrayList<String> addressList = (ArrayList<String>) group.getRegistryList();
//广播模式
// broadcast
if (ExecutorRouteStrategyEnum.SHARDING_BROADCAST == executorRouteStrategyEnum && CollectionUtils.isNotEmpty(addressList)) {
//依次调用所有的服务器
for (int i = 0; i < addressList.size(); i++) {
String address = addressList.get(i);
// 1、save log-id
XxlJobLog jobLog = new XxlJobLog();
jobLog.setJobGroup(jobInfo.getJobGroup());
jobLog.setJobId(jobInfo.getId());
XxlJobDynamicScheduler.xxlJobLogDao.save(jobLog);
logger.debug(">>>>>>>>>>> xxl-job trigger start, jobId:{}", jobLog.getId());
// 2、prepare trigger-info
//jobLog.setExecutorAddress(executorAddress);
jobLog.setGlueType(jobInfo.getGlueType());
jobLog.setExecutorHandler(jobInfo.getExecutorHandler());
jobLog.setExecutorParam(jobInfo.getExecutorParam());
jobLog.setTriggerTime(new Date());
ReturnT<String> triggerResult = new ReturnT<String>(null);
StringBuffer triggerMsgSb = new StringBuffer();
triggerMsgSb.append(I18nUtil.getString("jobconf_trigger_admin_adress")).append(":").append(IpUtil.getIp());
triggerMsgSb.append("<br>").append(I18nUtil.getString("jobconf_trigger_exe_regtype")).append(":")
.append( (group.getAddressType() == 0)?I18nUtil.getString("jobgroup_field_addressType_0"):I18nUtil.getString("jobgroup_field_addressType_1") );
triggerMsgSb.append("<br>").append(I18nUtil.getString("jobconf_trigger_exe_regaddress")).append(":").append(group.getRegistryList());
triggerMsgSb.append("<br>").append(I18nUtil.getString("jobinfo_field_executorRouteStrategy")).append(":").append(executorRouteStrategyEnum.getTitle()).append("("+i+"/"+addressList.size()+")"); // update01
triggerMsgSb.append("<br>").append(I18nUtil.getString("jobinfo_field_executorBlockStrategy")).append(":").append(blockStrategy.getTitle());
triggerMsgSb.append("<br>").append(I18nUtil.getString("jobinfo_field_executorFailStrategy")).append(":").append(failStrategy.getTitle());
triggerMsgSb.append("<br>").append(I18nUtil.getString("jobinfo_field_timeout")).append(":").append(jobInfo.getExecutorTimeout());
// 3、trigger-valid
if (triggerResult.getCode()==ReturnT.SUCCESS_CODE && CollectionUtils.isEmpty(addressList)) {
triggerResult.setCode(ReturnT.FAIL_CODE);
triggerMsgSb.append("<br>----------------------<br>").append(I18nUtil.getString("jobconf_trigger_address_empty"));
}
if (triggerResult.getCode() == ReturnT.SUCCESS_CODE) {
// 4.1、trigger-param
TriggerParam triggerParam = new TriggerParam();
triggerParam.setJobId(jobInfo.getId());
triggerParam.setExecutorHandler(jobInfo.getExecutorHandler());
triggerParam.setExecutorParams(jobInfo.getExecutorParam());
triggerParam.setExecutorBlockStrategy(jobInfo.getExecutorBlockStrategy());
triggerParam.setExecutorTimeout(jobInfo.getExecutorTimeout());
triggerParam.setLogId(jobLog.getId());
triggerParam.setLogDateTim(jobLog.getTriggerTime().getTime());
triggerParam.setGlueType(jobInfo.getGlueType());
triggerParam.setGlueSource(jobInfo.getGlueSource());
triggerParam.setGlueUpdatetime(jobInfo.getGlueUpdatetime().getTime());
triggerParam.setBroadcastIndex(i);
triggerParam.setBroadcastTotal(addressList.size()); // update02
// 4.2、trigger-run (route run / trigger remote executor)
//远程调用服务接口,执行任务
triggerResult = runExecutor(triggerParam, address); // update03
triggerMsgSb.append("<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>"+ I18nUtil.getString("jobconf_trigger_run") +"<<<<<<<<<<< </span><br>").append(triggerResult.getMsg());
// 4.3、trigger (fail retry)
if (triggerResult.getCode()!=ReturnT.SUCCESS_CODE && failStrategy == ExecutorFailStrategyEnum.FAIL_TRIGGER_RETRY) {
triggerResult = runExecutor(triggerParam, address); // update04
triggerMsgSb.append("<br><br><span style=\"color:#F39C12;\" > >>>>>>>>>>>"+ I18nUtil.getString("jobconf_fail_trigger_retry") +"<<<<<<<<<<< </span><br>").append(triggerResult.getMsg());
}
}
// 5、save trigger-info
jobLog.setExecutorAddress(triggerResult.getContent());
jobLog.setTriggerCode(triggerResult.getCode());
jobLog.setTriggerMsg(triggerMsgSb.toString());
XxlJobDynamicScheduler.xxlJobLogDao.updateTriggerInfo(jobLog);
// 6、monitor trigger
JobFailMonitorHelper.monitor(jobLog.getId());
logger.debug(">>>>>>>>>>> xxl-job trigger end, jobId:{}", jobLog.getId());
}
} else {
//单播模式
// 1、save log-id
XxlJobLog jobLog = new XxlJobLog();
jobLog.setJobGroup(jobInfo.getJobGroup());
jobLog.setJobId(jobInfo.getId());
XxlJobDynamicScheduler.xxlJobLogDao.save(jobLog);
logger.debug(">>>>>>>>>>> xxl-job trigger start, jobId:{}", jobLog.getId());
// 2、prepare trigger-info
//jobLog.setExecutorAddress(executorAddress);
jobLog.setGlueType(jobInfo.getGlueType());
jobLog.setExecutorHandler(jobInfo.getExecutorHandler());
jobLog.setExecutorParam(jobInfo.getExecutorParam());
jobLog.setTriggerTime(new Date());
ReturnT<String> triggerResult = new ReturnT<String>(null);
StringBuffer triggerMsgSb = new StringBuffer();
triggerMsgSb.append(I18nUtil.getString("jobconf_trigger_admin_adress")).append(":").append(IpUtil.getIp());
triggerMsgSb.append("<br>").append(I18nUtil.getString("jobconf_trigger_exe_regtype")).append(":")
.append( (group.getAddressType() == 0)?I18nUtil.getString("jobgroup_field_addressType_0"):I18nUtil.getString("jobgroup_field_addressType_1") );
triggerMsgSb.append("<br>").append(I18nUtil.getString("jobconf_trigger_exe_regaddress")).append(":").append(group.getRegistryList());
triggerMsgSb.append("<br>").append(I18nUtil.getString("jobinfo_field_executorRouteStrategy")).append(":").append(executorRouteStrategyEnum.getTitle());
triggerMsgSb.append("<br>").append(I18nUtil.getString("jobinfo_field_executorBlockStrategy")).append(":").append(blockStrategy.getTitle());
triggerMsgSb.append("<br>").append(I18nUtil.getString("jobinfo_field_executorFailStrategy")).append(":").append(failStrategy.getTitle());
triggerMsgSb.append("<br>").append(I18nUtil.getString("jobinfo_field_timeout")).append(":").append(jobInfo.getExecutorTimeout());
// 3、trigger-valid
if (triggerResult.getCode()==ReturnT.SUCCESS_CODE && CollectionUtils.isEmpty(addressList)) {
triggerResult.setCode(ReturnT.FAIL_CODE);
triggerMsgSb.append("<br>----------------------<br>").append(I18nUtil.getString("jobconf_trigger_address_empty"));
}
if (triggerResult.getCode() == ReturnT.SUCCESS_CODE) {
// 4.1、trigger-param
TriggerParam triggerParam = new TriggerParam();
triggerParam.setJobId(jobInfo.getId());
triggerParam.setExecutorHandler(jobInfo.getExecutorHandler());
triggerParam.setExecutorParams(jobInfo.getExecutorParam());
triggerParam.setExecutorBlockStrategy(jobInfo.getExecutorBlockStrategy());
triggerParam.setExecutorTimeout(jobInfo.getExecutorTimeout());
triggerParam.setLogId(jobLog.getId());
triggerParam.setLogDateTim(jobLog.getTriggerTime().getTime());
triggerParam.setGlueType(jobInfo.getGlueType());
triggerParam.setGlueSource(jobInfo.getGlueSource());
triggerParam.setGlueUpdatetime(jobInfo.getGlueUpdatetime().getTime());
triggerParam.setBroadcastIndex(0);
triggerParam.setBroadcastTotal(1);
// 4.2、trigger-run (route run / trigger remote executor)
//路由后远程调用服务接口,执行任务
triggerResult = executorRouteStrategyEnum.getRouter().routeRun(triggerParam, addressList);
triggerMsgSb.append("<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>"+ I18nUtil.getString("jobconf_trigger_run") +"<<<<<<<<<<< </span><br>").append(triggerResult.getMsg());
// 4.3、trigger (fail retry)
if (triggerResult.getCode()!=ReturnT.SUCCESS_CODE && failStrategy == ExecutorFailStrategyEnum.FAIL_TRIGGER_RETRY) {
triggerResult = executorRouteStrategyEnum.getRouter().routeRun(triggerParam, addressList);
triggerMsgSb.append("<br><br><span style=\"color:#F39C12;\" > >>>>>>>>>>>"+ I18nUtil.getString("jobconf_fail_trigger_retry") +"<<<<<<<<<<< </span><br>").append(triggerResult.getMsg());
}
}
// 5、save trigger-info
jobLog.setExecutorAddress(triggerResult.getContent());
jobLog.setTriggerCode(triggerResult.getCode());
jobLog.setTriggerMsg(triggerMsgSb.toString());
XxlJobDynamicScheduler.xxlJobLogDao.updateTriggerInfo(jobLog);
// 6、monitor trigger
JobFailMonitorHelper.monitor(jobLog.getId());
logger.debug(">>>>>>>>>>> xxl-job trigger end, jobId:{}", jobLog.getId());
}
}
runExecutor方法中根据address服务器地址,XxlJobDynamicScheduler.getExecutorBiz中会获取代理类最终调用JettyClient的send方法。
public static ReturnT<String> runExecutor(TriggerParam triggerParam, String address){
ReturnT<String> runResult = null;
try {
//获取代理对象
ExecutorBiz executorBiz = XxlJobDynamicScheduler.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, ""+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());
runResult.setContent(address);
return runResult;
}
在XxlJobDynamicScheduler的getExecutorBiz中会通过NetComClientProxy生成代理对象,在执行时会调用其方法。
public static ExecutorBiz getExecutorBiz(String address) throws Exception {
// valid
if (address==null || address.trim().length()==0) {
return null;
}
// load-cache
address = address.trim();
ExecutorBiz executorBiz = executorBizRepository.get(address);
if (executorBiz != null) {
return executorBiz;
}
// set-cache
executorBiz = (ExecutorBiz) new NetComClientProxy(ExecutorBiz.class, address, accessToken).getObject();
executorBizRepository.put(address, executorBiz);
return executorBiz;
}
getObject中生成代理对象,执行会执行JettyClient的send方法。
@Override
public Object getObject() throws Exception {
return Proxy.newProxyInstance(Thread.currentThread()
.getContextClassLoader(), new Class[] { iface },
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// filter method like "Object.toString()"
if (Object.class.getName().equals(method.getDeclaringClass().getName())) {
logger.error(">>>>>>>>>>> xxl-rpc proxy class-method not support [{}.{}]", method.getDeclaringClass().getName(), method.getName());
throw new RuntimeException("xxl-rpc proxy class-method not support");
}
// request
RpcRequest request = new RpcRequest();
request.setServerAddress(serverAddress);
request.setCreateMillisTime(System.currentTimeMillis());
request.setAccessToken(accessToken);
request.setClassName(method.getDeclaringClass().getName());
request.setMethodName(method.getName());
request.setParameterTypes(method.getParameterTypes());
request.setParameters(args);
//发起http调用,执行任务
// send
RpcResponse response = client.send(request);
// valid response
if (response == null) {
throw new Exception("Network request fail, response not found.");
}
if (response.isError()) {
throw new RuntimeException(response.getError());
} else {
return response.getResult();
}
}
});
}
任务调度中心向任务执行器发送的任务请求数据如下。
二、任务执行器接收任务执行
在任务执行器会根据内置的jetty提供web服务,提供请求处理器JettyServerHandler接收处理任务调度中心发送过来的任务
public class JettyServerHandler extends AbstractHandler {
private static Logger logger = LoggerFactory.getLogger(JettyServerHandler.class);
//接收请求,处理任务
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
// invoke
//调用任务执行
RpcResponse rpcResponse = doInvoke(request);
// serialize response
byte[] responseBytes = HessianSerializer.serialize(rpcResponse);
response.setContentType("text/html;charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
baseRequest.setHandled(true);
OutputStream out = response.getOutputStream();
out.write(responseBytes);
out.flush();
}
private RpcResponse doInvoke(HttpServletRequest request) {
try {
// deserialize request
byte[] requestBytes = HttpClientUtil.readBytes(request);
if (requestBytes == null || requestBytes.length==0) {
RpcResponse rpcResponse = new RpcResponse();
rpcResponse.setError("RpcRequest byte[] is null");
return rpcResponse;
}
//反序列化数据
RpcRequest rpcRequest = (RpcRequest) HessianSerializer.deserialize(requestBytes, RpcRequest.class);
// invoke
//通过反射调用任务
RpcResponse rpcResponse = NetComServerFactory.invokeService(rpcRequest, null);
return rpcResponse;
} catch (Exception e) {
logger.error(e.getMessage(), e);
RpcResponse rpcResponse = new RpcResponse();
rpcResponse.setError("Server-error:" + e.getMessage());
return rpcResponse;
}
}
}
在invokeService根据发送过来的类名com.xxl.job.core.biz.ExecutorBiz和方法run,通过反射机制调用
public static RpcResponse invokeService(RpcRequest request, Object serviceBean) {
if (serviceBean==null) {
serviceBean = serviceMap.get(request.getClassName());
}
if (serviceBean == null) {
// TODO
}
RpcResponse response = new RpcResponse();
if (System.currentTimeMillis() - request.getCreateMillisTime() > 180000) {
response.setResult(new ReturnT<String>(ReturnT.FAIL_CODE, "The timestamp difference between admin and executor exceeds the limit."));
return response;
}
if (accessToken!=null && accessToken.trim().length()>0 && !accessToken.trim().equals(request.getAccessToken())) {
response.setResult(new ReturnT<String>(ReturnT.FAIL_CODE, "The access token[" + request.getAccessToken() + "] is wrong."));
return response;
}
try {
Class<?> serviceClass = serviceBean.getClass();
String methodName = request.getMethodName();
Class<?>[] parameterTypes = request.getParameterTypes();
Object[] parameters = request.getParameters();
FastClass serviceFastClass = FastClass.create(serviceClass);
FastMethod serviceFastMethod = serviceFastClass.getMethod(methodName, parameterTypes);
Object result = serviceFastMethod.invoke(serviceBean, parameters);
response.setResult(result);
} catch (Throwable t) {
t.printStackTrace();
response.setError(t.getMessage());
}
return response;
}
我们看看com.xxl.job.core.biz.ExecutorBiz的run方法中做了什么处理操作。
在ExecutorBiz中根据发送过来的消息,根据demoJobHandler找到接口的实现类,接下来就可以新起线程去执行实现类DemoJobHandler了。
@Override
public ReturnT<String> run(TriggerParam triggerParam) {
// load old:jobHandler + jobThread
//创建执行线程
JobThread jobThread = XxlJobExecutor.loadJobThread(triggerParam.getJobId());
//如果存在则直接使用老的线程
IJobHandler jobHandler = jobThread!=null?jobThread.getHandler():null;
String removeOldReason = null;
// valid:jobHandler + jobThread
GlueTypeEnum glueTypeEnum = GlueTypeEnum.match(triggerParam.getGlueType());
if (GlueTypeEnum.BEAN == glueTypeEnum) {
// new jobhandler
//根据类名找到任务执行类
IJobHandler newJobHandler = XxlJobExecutor.loadJobHandler(triggerParam.getExecutorHandler());
// valid old jobThread
if (jobThread!=null && jobHandler != newJobHandler) {
// change handler, need kill old thread
removeOldReason = "change jobhandler or glue type, and terminate the old job thread.";
jobThread = null;
jobHandler = null;
}
// valid handler
if (jobHandler == null) {
jobHandler = newJobHandler;
if (jobHandler == null) {
return new ReturnT<String>(ReturnT.FAIL_CODE, "job handler [" + triggerParam.getExecutorHandler() + "] not found.");
}
}
} else if (GlueTypeEnum.GLUE_GROOVY == glueTypeEnum) {
// valid old jobThread
if (jobThread != null &&
!(jobThread.getHandler() instanceof GlueJobHandler
&& ((GlueJobHandler) jobThread.getHandler()).getGlueUpdatetime()==triggerParam.getGlueUpdatetime() )) {
// change handler or gluesource updated, need kill old thread
removeOldReason = "change job source or glue type, and terminate the old job thread.";
jobThread = null;
jobHandler = null;
}
// valid handler
if (jobHandler == null) {
try {
IJobHandler originJobHandler = GlueFactory.getInstance().loadNewInstance(triggerParam.getGlueSource());
jobHandler = new GlueJobHandler(originJobHandler, triggerParam.getGlueUpdatetime());
} catch (Exception e) {
logger.error(e.getMessage(), e);
return new ReturnT<String>(ReturnT.FAIL_CODE, e.getMessage());
}
}
} else if (glueTypeEnum!=null && glueTypeEnum.isScript()) {
// valid old jobThread
if (jobThread != null &&
!(jobThread.getHandler() instanceof ScriptJobHandler
&& ((ScriptJobHandler) jobThread.getHandler()).getGlueUpdatetime()==triggerParam.getGlueUpdatetime() )) {
// change script or gluesource updated, need kill old thread
removeOldReason = "change job source or glue type, and terminate the old job thread.";
jobThread = null;
jobHandler = null;
}
// valid handler
if (jobHandler == null) {
jobHandler = new ScriptJobHandler(triggerParam.getJobId(), triggerParam.getGlueUpdatetime(), triggerParam.getGlueSource(), GlueTypeEnum.match(triggerParam.getGlueType()));
}
} else {
return new ReturnT<String>(ReturnT.FAIL_CODE, "glueType[" + triggerParam.getGlueType() + "] is not valid.");
}
// executor block strategy
if (jobThread != null) {
ExecutorBlockStrategyEnum blockStrategy = ExecutorBlockStrategyEnum.match(triggerParam.getExecutorBlockStrategy(), null);
if (ExecutorBlockStrategyEnum.DISCARD_LATER == blockStrategy) {
// discard when running
if (jobThread.isRunningOrHasQueue()) {
return new ReturnT<String>(ReturnT.FAIL_CODE, "block strategy effect:"+ExecutorBlockStrategyEnum.DISCARD_LATER.getTitle());
}
} else if (ExecutorBlockStrategyEnum.COVER_EARLY == blockStrategy) {
// kill running jobThread
if (jobThread.isRunningOrHasQueue()) {
removeOldReason = "block strategy effect:" + ExecutorBlockStrategyEnum.COVER_EARLY.getTitle();
jobThread = null;
}
} else {
// just queue trigger
}
}
// replace thread (new or exists invalid)
//起线程执行任务
if (jobThread == null) {
jobThread = XxlJobExecutor.registJobThread(triggerParam.getJobId(), jobHandler, removeOldReason);
}
// push data to queue
ReturnT<String> pushResult = jobThread.pushTriggerQueue(triggerParam);
return pushResult;
}
总结:任务执行器提供web服务,任务调度中心根据任务分组及分组服务器发送http请求,任务执行器收到请求,根据请求中的数据调用对应的任务。