Directorio de artículos
guía
xxl-job es una plataforma de programación de tareas distribuidas, que es muy apreciada por los programadores de la industria. Este capítulo lo llevará a comprender el código fuente de xxl-job y comprender su lógica operativa.
Leer el código fuente de xxl-job mejorará su comprensión y aplicación de subprocesos múltiples y la sublimación de ideas de programación.
Este capítulo lo llevará a analizar un poco la lógica subyacente del diseño de xxl-job, para que realmente pueda comprender cada módulo en la figura a continuación y le permita saber qué es y por qué lo es.
estructura del proyecto
Despliegue el código, primero observamos la estructura del proyecto, xxl-job es un proyecto principal-secundario estándar, vemos que hay tres subproyectos
- servidor xxl-job-admin
- El servidor y el cliente harán referencia al paquete principal xxl-job-core
- xxl-job-executor-samples algunas demostraciones
Interpretación del código fuente - Temporizador
bien, ahora necesitamos saber qué hace el servidor xxl-job cuando se inicia, primero ubique esta claseXxlJobAdminConfig
Se puede ver que la función central de xxl-job se ejecuta a lo largo del ciclo de vida del bean
@Component
public class XxlJobAdminConfig implements InitializingBean, DisposableBean {
@Override
public void afterPropertiesSet() throws Exception {
adminConfig = this;
xxlJobScheduler = new XxlJobScheduler();
// 初始化
xxlJobScheduler.init();
}
@Override
public void destroy() throws Exception {
xxlJobScheduler.destroy();
}
}
sigue el códigocom.xxl.job.admin.core.scheduler.XxlJobScheduler#init
Se puede ver que aquí se inicializan varios ayudantes centrales, y cada ayudante usa el modo singleton hambriento, por lo que no habrá creación repetida.
public void init() throws Exception {
// 触发池
JobTriggerPoolHelper.toStart();
// 服务监听器
JobRegistryHelper.getInstance().start();
// 失败告警
JobFailMonitorHelper.getInstance().start();
// 回调监听器
JobCompleteHelper.getInstance().start();
// 日志回调
JobLogReportHelper.getInstance().start();
// 定时器
JobScheduleHelper.getInstance().start();
}
Primero localizacom.xxl.job.admin.core.thread.JobScheduleHelper#start
El ayudante es un temporizador 心脏
, y su función es la temporización y el predisparo.
Comenzó dos subprocesos de daemon
-
hilo de análisis de tareas de ScheduleThread
Durante el ciclo, siga escaneando las próximas tareas, use
mysql for update实现排他锁
, evite que otros servicios escaneen en paralelo y almacene en caché las próximas tareas时间轮
en -
subproceso de ejecución ringThread
时间轮
tarea que se desencadena
rueda del tiempo
Echemos un vistazo a cómo se realiza la rueda del tiempo, que suena alta.
De la siguiente manera, la esencia es uno concurrentHashMap
, la clave es el segundo de ejecución, el valor es la identificación del trabajo que se ejecutará y scheduleThread
el subproceso colocará la tarea en la lista de la rueda de tiempo con 5 a 10 segundos de anticipación.
private volatile static Map<Integer, List<Integer>> ringData = new ConcurrentHashMap<>();
// ringSecond 下一次执行的时间
private void pushTimeRing(int ringSecond, int jobId){
// push async ring
List<Integer> ringItemData = ringData.get(ringSecond);
if (ringItemData == null) {
ringItemData = new ArrayList<Integer>();
ringData.put(ringSecond, ringItemData);
}
ringItemData.add(jobId);
}
Activar tareas en la rueda del tiempo
ringThread = new Thread(new Runnable() {
@Override
public void run() {
while (!ringThreadToStop) {
try {
// 时间对其
TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis() % 1000);
} catch (InterruptedException e) {
if (!ringThreadToStop) {
logger.error(e.getMessage(), e);
}
}
try {
// 时间轮数据处理
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);
}
}
if (ringItemData.size() > 0) {
// 触发任务
for (int jobId: ringItemData) {
// 触发任务
JobTriggerPoolHelper.trigger(jobId, TriggerTypeEnum.CRON, -1, null, null, null);
}
ringItemData.clear();
}
} catch (Exception e) {
if (!ringThreadToStop) {
logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread error:{}", e);
}
}
}
}
});
Como se muestra arriba, la duración total de la rueda del tiempo es 60, que corresponde a cada segundo en un minuto. Como se mencionó anteriormente, scheduleThread pondrá las tareas en la rueda del tiempo con 5 a 10 segundos de anticipación, y ringThread transferirá las tareas para ser ejecutada desde la rueda del tiempo Quitada, es decir, no hay operación concurrente de acceso. Debido al negocio especial de las tareas programadas, la concurrencia de la rueda del tiempo también es muy baja.
preguntas de entrevista
¿Por qué usar dos hilos?
Aísle el subproceso de exploración del subproceso de ejecución, ya que el subproceso de exploración necesita interactuar con la base de datos y utiliza un bloqueo exclusivo, lo que da como resultado un rendimiento lento.
El subproceso de ejecución no interactúa con el middleware y escanea directamente la rueda del tiempo, que tiene un alto rendimiento y se utiliza principalmente para garantizar la activación precisa de las tareas.
¿Cuáles son los beneficios de la rueda del tiempo?
La lógica no tiene bloqueos, el rendimiento es eficiente y no hay conflicto de ejecución entre los dos subprocesos.
Cómo garantizar la ejecución en el momento preciso
Alinee la hora actual con el segundo completo a través del siguiente código, que puede tener una precisión de milisegundos
TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis() % 1000);
Interpretación del código fuente: disparador
Esta sección rastreará la tarea de trabajo xxl desde el temporizador -> disparador -> solicitud http -> tarea de disparador
com.xxl.job.admin.core.thread.JobTriggerPoolHelper#start
El desencadenador inicializa dos grupos de subprocesos para desencadenar tareas
com.xxl.job.admin.core.thread.JobTriggerPoolHelper#addTrigger
Cuando se llama al activador, no se programará directamente, sino que se ejecutará utilizando el grupo de subprocesos. El propósito no es bloquear el subproceso de programación.
public void addTrigger(final int jobId,
final TriggerTypeEnum triggerType,
final int failRetryCount,
final String executorShardingParam,
final String executorParam,
final String addressList) {
// 默认使用fastTriggerPool
ThreadPoolExecutor triggerPool_ = fastTriggerPool;
AtomicInteger jobTimeoutCount = jobTimeoutCountMap.get(jobId);
// 如果发现任务一分钟内有大于10次的慢执行,换slowTriggerPool线程池
if (jobTimeoutCount!=null && jobTimeoutCount.get() > 10) {
triggerPool_ = slowTriggerPool;
}
// 线程池执行
triggerPool_.execute(new Runnable() {
@Override
public void run() {
long start = System.currentTimeMillis();
try {
// 触发
XxlJobTrigger.trigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam, addressList);
} catch (Exception e) {
logger.error(e.getMessage(), e);
} finally {
// 到达下一个周期则清理上一个周期数据
long minTim_now = System.currentTimeMillis()/60000;
if (minTim != minTim_now) {
minTim = minTim_now;
jobTimeoutCountMap.clear();
}
// 记录慢任务执行次数
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();
}
}
}
}
});
}
Siga el enlace a continuación
com.xxl.job.admin.core.trigger.XxlJobTrigger#processTrigger com.xxl.job.admin.core.trigger.XxlJobTrigger#runExecutor
Aquí se explica cómo desencadenar la tarea.
runResult = executorBiz.run(triggerParam);
Hay dos implementaciones aquí, el lado del trabajo xxl usa ExecutorBizClient, y nuestra propia aplicación tiene efecto con ExecutorBizImpl
A continuación, observe la implementación de ExecutorBizImpl
Nota: En este momento, el lado del servidor se envió a nuestro propio proyecto a través de ExecutorBizClient
@Override
public ReturnT<String> run(TriggerParam triggerParam) {
// 获得执行控制器
JobThread jobThread = XxlJobExecutor.loadJobThread(triggerParam.getJobId());
IJobHandler jobHandler = jobThread!=null?jobThread.getHandler():null;
String removeOldReason = null;
GlueTypeEnum glueTypeEnum = GlueTypeEnum.match(triggerParam.getGlueType());
if (GlueTypeEnum.BEAN == glueTypeEnum) {
// bean模式触发
IJobHandler newJobHandler = XxlJobExecutor.loadJobHandler(triggerParam.getExecutorHandler());
if (jobThread!=null && jobHandler != newJobHandler) {
removeOldReason = "change jobhandler or glue type, and terminate the old job thread.";
jobThread = null;
jobHandler = null;
}
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) {
// 原生模式触发
if (jobThread != null &&
!(jobThread.getHandler() instanceof GlueJobHandler
&& ((GlueJobHandler) jobThread.getHandler()).getGlueUpdatetime()==triggerParam.getGlueUpdatetime() )) {
removeOldReason = "change job source or glue type, and terminate the old job thread.";
jobThread = null;
jobHandler = null;
}
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()) {
// 脚本触发
if (jobThread != null &&
!(jobThread.getHandler() instanceof ScriptJobHandler
&& ((ScriptJobHandler) jobThread.getHandler()).getGlueUpdatetime()==triggerParam.getGlueUpdatetime() )) {
removeOldReason = "change job source or glue type, and terminate the old job thread.";
jobThread = null;
jobHandler = null;
}
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.");
}
// 阻塞处理策略
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
}
}
if (jobThread == null) {
// 创建执行控制器
jobThread = XxlJobExecutor.registJobThread(triggerParam.getJobId(), jobHandler, removeOldReason);
}
// 将数据放入执行队列
ReturnT<String> pushResult = jobThread.pushTriggerQueue(triggerParam);
return pushResult;
}
Tenga en cuenta que esto no espera a que se complete la ejecución de la tarea, sino que devuelve directamente el resultado del activador después de colocarlo en la cola, y el resultado de la ejecución se notificará al lado del servidor más tarde.
Ingresarcom.xxl.job.core.thread.JobThread#pushTriggerQueue
public ReturnT<String> pushTriggerQueue(TriggerParam triggerParam) {
// 防止重复触发的set
if (triggerLogIdSet.contains(triggerParam.getLogId())) {
logger.info(">>>>>>>>>>> repeate trigger job, logId:{}", triggerParam.getLogId());
return new ReturnT<String>(ReturnT.FAIL_CODE, "repeate trigger job, logId:" + triggerParam.getLogId());
}
triggerLogIdSet.add(triggerParam.getLogId());
// 加入待执行队列
triggerQueue.add(triggerParam);
return ReturnT.SUCCESS;
}
Esta clase es el controlador que ejecuta el controlador. Hereda Thread. Echemos un vistazo al método de ejecución.
com.xxl.job.core.thread.JobThread#run
@Override
public void run() {
// init
try {
handler.init();
} catch (Throwable e) {
logger.error(e.getMessage(), e);
}
while(!toStop){
running = false;
idleTimes++; // 增加空闲的次数
TriggerParam triggerParam = null;
try {
// 将队列中待执行待任务poll出来
triggerParam = triggerQueue.poll(3L, TimeUnit.SECONDS);
if (triggerParam!=null) {
running = true;
idleTimes = 0;
triggerLogIdSet.remove(triggerParam.getLogId());
// 记录上下文对象,用于数据分片,日志记录等动作
String logFileName = XxlJobFileAppender.makeLogFileName(new Date(triggerParam.getLogDateTime()), triggerParam.getLogId());
XxlJobContext xxlJobContext = new XxlJobContext(
triggerParam.getJobId(),
triggerParam.getExecutorParams(),
logFileName,
triggerParam.getBroadcastIndex(),
triggerParam.getBroadcastTotal());
XxlJobContext.setXxlJobContext(xxlJobContext);
XxlJobHelper.log("<br>----------- xxl-job job execute start -----------<br>----------- Param:" + xxlJobContext.getJobParam());
if (triggerParam.getExecutorTimeout() > 0) {
// 有设置执行时间的话通过FutureTask实现等待超时的动作
Thread futureThread = null;
try {
FutureTask<Boolean> futureTask = new FutureTask<Boolean>(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
// 记录上下文对象,用于数据分片,日志记录等动作
XxlJobContext.setXxlJobContext(xxlJobContext);
handler.execute();
return true;
}
});
// 创建并执行任务线程
futureThread = new Thread(futureTask);
futureThread.start();
Boolean tempResult = futureTask.get(triggerParam.getExecutorTimeout(), TimeUnit.SECONDS);
} catch (TimeoutException e) {
// 执行超时处理
XxlJobHelper.log("<br>----------- xxl-job job execute timeout");
XxlJobHelper.log(e);
XxlJobHelper.handleTimeout("job execute timeout ");
} finally {
futureThread.interrupt();
}
} else {
// 如果有设置执行超时时间直接执行
handler.execute();
}
// 校验执行状态
if (XxlJobContext.getXxlJobContext().getHandleCode() <= 0) {
XxlJobHelper.handleFail("job handle result lost.");
} else {
// 截取日志长度,防止过长影响性能
String tempHandleMsg = XxlJobContext.getXxlJobContext().getHandleMsg();
tempHandleMsg = (tempHandleMsg!=null&&tempHandleMsg.length()>50000)
?tempHandleMsg.substring(0, 50000).concat("...")
:tempHandleMsg;
XxlJobContext.getXxlJobContext().setHandleMsg(tempHandleMsg);
}
XxlJobHelper.log("<br>----------- xxl-job job execute end(finish) -----------<br>----------- Result: handleCode="
+ XxlJobContext.getXxlJobContext().getHandleCode()
+ ", handleMsg = "
+ XxlJobContext.getXxlJobContext().getHandleMsg()
);
} else {
if (idleTimes > 30) {
if(triggerQueue.size() == 0) {
// 当空闲次数大于30次且队列中无待执行时移除控制器,释放资源
XxlJobExecutor.removeJobThread(jobId, "excutor idel times over limit.");
}
}
}
} catch (Throwable e) {
// 异常,记录错误日志
if (toStop) {
XxlJobHelper.log("<br>----------- JobThread toStop, stopReason:" + stopReason);
}
// handle result
StringWriter stringWriter = new StringWriter();
e.printStackTrace(new PrintWriter(stringWriter));
String errorMsg = stringWriter.toString();
XxlJobHelper.handleFail(errorMsg);
XxlJobHelper.log("<br>----------- JobThread Exception:" + errorMsg + "<br>----------- xxl-job job execute end(error) -----------");
} finally {
// 将执行结果和日志通知给xxl-job
if(triggerParam != null) {
if (!toStop) {
TriggerCallbackThread.pushCallBack(new HandleCallbackParam(
triggerParam.getLogId(),
triggerParam.getLogDateTime(),
XxlJobContext.getXxlJobContext().getHandleCode(),
XxlJobContext.getXxlJobContext().getHandleMsg() )
);
} else {
TriggerCallbackThread.pushCallBack(new HandleCallbackParam(
triggerParam.getLogId(),
triggerParam.getLogDateTime(),
XxlJobContext.HANDLE_CODE_FAIL,
stopReason + " [job running, killed]" )
);
}
}
}
}
// 将队列中的任务回调标记失败处理
while(triggerQueue !=null && triggerQueue.size()>0){
TriggerParam triggerParam = triggerQueue.poll();
if (triggerParam!=null) {
TriggerCallbackThread.pushCallBack(new HandleCallbackParam(
triggerParam.getLogId(),
triggerParam.getLogDateTime(),
XxlJobContext.HANDLE_CODE_FAIL,
stopReason + " [job not executed, in the job queue, killed.]")
);
}
}
try {
handler.destroy();
} catch (Throwable e) {
logger.error(e.getMessage(), e);
}
logger.info(">>>>>>>>>>> xxl-job JobThread stoped, hashCode:{}", Thread.currentThread());
}
presta atención a este párrafo
XxlJobContext.setXxlJobContext(xxlJobContext);
xxl-job的日志系统
Use threadlocal para mantener el objeto de contexto de xxl-job, que es muy útil, y algunos de los datos que se mantienen en él se usarán durante la ejecución de la tarea.
A través del código anterior, podemos ver que xxl-job hace que nuestra aplicación espere a que se complete la ejecución sin bloquearse. Volverá inmediatamente después de que la programación sea exitosa. El resultado de la ejecución y el registro de ejecución se notifican a xxl-job después de la solicitud. La ventaja de esto es que xxl-job puede saber rápidamente si el servidor se programó correctamente y no ocupará la cantidad de conexiones del sistema porque la tarea no se completó. xxl-job liberará recursos automáticamente cuando el subproceso de ejecución esté inactivo y no seguirá ocupando recursos del sistema.