Guide
In the work, you will encounter some business scenarios where users customize timed tasks. Common open source frameworks (such as XXL-Job, Quartz) are designed to be used by developers and are not suitable for opening users to create a large number of custom tasks. This article draws on the idea of timed task assignments in the open source framework, combined with the ScheduledExecutor of juc, provides a way to implement timed tasks to solve the problem of user-defined timed task scenarios. I hope to be helpful.
Author: Yang Kai | Senior Development Engineer, NetEase Smart Enterprise
User-defined timing tasks
When it comes to the realization of timed tasks, our priority is to introduce excellent open source framework solutions to solve them. Common open source products have also been mentioned above, such as Quartz, XXL-Job, ElasticJob, etc., but the open source framework is applied to user-defined On the task, there are the following need problems or shortcomings:
- The open source framework has a set of standard solutions from task creation to execution. When and where user-defined tasks are inserted in compliance with the open source framework standard tasks and can be controlled to take effect and stop is a complex issue that needs to be considered.
- Open source frameworks (such as XXL-Job) decouple task management and business containers. If users want to complete task creation and modification, business services need to call the operation task center in reverse, which does not conform to the task center design principle.
- The original intention of the open source framework design is to create and control tasks for program developers. Under normal circumstances, the task execution strategy and purpose are relatively clear, unlike user-defined tasks where frequent modification and multiple task definitions with the same business background use the same processing logic.
- The open source framework does not provide a user-friendly task configuration interface.
In addition to the above issues, the design of user-defined task components also needs to consider the characteristics of user-defined tasks from the user's point of view:
- Controllable start and end
User-defined timed tasks are highly dependent on business, and tasks can be created and updated multiple times, but they will not be executed, and they will also be manually stopped during the execution of the task. Therefore, the task component should distinguish between the creation of business tasks and the creation of job tasks, and only create and load tasks that the user has determined to perform.
- Execution strategy and execution time are user-friendly
Program developers create timed tasks. The execution strategy (single task cycle, single) and execution time are determined by the configured cron expression, but the cron expression is not user-friendly and prone to configuration errors. User-defined timing tasks need to provide a user-friendly configuration interface when setting timing strategies and execution time, and the task components are converted into corresponding cron expressions.
- Controllable execution time range
After completing the first and second steps of the configuration, you need to provide the user with a time range for task execution, within which the task will be executed. The interface of a simple user-defined timed task is as follows:
To understand the characteristics of user-defined timing tasks, define the task model TaskScheduleDefine as:
Attributes | Annotation |
---|---|
id | Unique ID of the task |
busId | The ID of the business dimension: it can be unique or designated according to the business background |
taskName | mission name |
beanName | Task processing class instance name |
cron | cron expression |
startTime | User-defined start time |
endTime | User-defined end time |
isPermanent | Permanent task |
multiple | Whether to allow parallel execution of tasks at the same time |
once | Single task |
valid | Whether the task is valid |
Scheduled task execution cycle
Timing tasks can be divided into the following stages from creation to execution:
- Create: interface configuration (such as XXL-Job), code configuration (such as Quartz, spring-schedule).
- Loading: The task is loaded into the application cache and can be performed when it is created, but in fact task creation and loading task are separated. For example, when a task is modified, there is actually an update process, which can be called a task Of overloading.
- Scheduling: Determine whether the loaded task meets the execution conditions (if distributed scheduling is supported, which server should be determined to execute), if so, start execution.
- Execution: The open source framework will complete the above three steps (the dispatch center or the application itself), and business developers only need to focus on the business logic part to achieve decoupling of task scheduling and business execution.
The task component introduced in this article is also based on this idea to implement user-defined tasks.
User-defined task design
When the application starts, initialize the task loading thread and task scheduling thread (similar to scheduleThread and ringThread of XXL-Job)
//上传+加载,支持本地和数据库任务
uploadAndLoadDefinition();
//初始化调度, 调度由维护任务来处理,由调度任务来唤起相应的具体执行
internalScheduledExecutor.scheduleAtFixedRate(new SpringTaskMonitor(), 10, 45, TimeUnit.SECONDS);
//定义维护
internalScheduledExecutor.scheduleAtFixedRate(new SpringTaskDefinitionMonitor(), 1, 2, TimeUnit.MINUTES);
Task creation
Associate the execution and stop of business tasks with the creation and invalidation of job tasks to achieve the original intention of user-defined timing tasks, and the job tasks are completely determined by the user.
Task loading
Task loading uses the scheduleAtFixedRate method of the scheduled thread pool ScheduledThreadPoolExecutor provided by juc to periodically trigger task loading to ensure timely update of tasks in the cache. The difference is that user-defined tasks are generally created in advance and do not need to be continuously inquired. Moreover, the start and end time can be used to ensure that the task is triggered correctly.
Part of the logic of the registration task:
//获取全部任务列表defineList更新任务
defineList.forEach(t -> {
String key = t.getBeanName() + t.getBusId();
val task = TaskDefinitions.registered(key);
//没有(并且有效),就添加
if (task == null) {
if (t.getValid()) {
TaskDefinitions.registerTask(new ScheduleTask(t));
changedList.add(t);
}
}
//有,就替换定义
else {
boolean changed = task.updateDefine(t);
if (changed) {
changedList.add(t);
}
}
});
//打印变化的任务日志
}
//ScheduleTask任务定义,updateDefine这个对象的属性
public class ScheduleTask {
private long id;
private TaskScheduleDefine localScheduleDefine;
private CronSequenceGenerator cronGenerator;
public ScheduleTask(TaskScheduleDefine taskScheduleDefine) {
this.id = taskScheduleDefine.getId();
this.localScheduleDefine = taskScheduleDefine;
}
}
Task scheduling
Part of the logic of scheduling tasks:
public class SpringTaskMonitor implements Runnable {
private static Date DATE_INIT = new Date();
@Override
public void run() {
ExceptionUtils.doActionLogE(this::doRun);
}
private void doRun() throws Throwable {
val taskScheduleDefineMapper = ApplicationContextUtils.getReadyApplicationContext().getBean(TaskScheduleDefineMapper.class);
val taskScheduleRecordMapper = ApplicationContextUtils.getReadyApplicationContext().getBean(TaskScheduleRecordMapper.class);
TaskDefinitions.getTaskMap().values().forEach(t -> {
//1.无效任务
if (!t.getLocalScheduleDefine().getValid()) {
return;
}
//2.设置了过期时间
Date now = new Date();
if (!t.getLocalScheduleDefine().getIsPermanent()) {
Date endTime = t.getLocalScheduleDefine().getEndTime();
if (null == endTime || endTime.before(now)) {
TaskDefinitions.getTaskMap().remove(t.getLocalScheduleDefine().getBeanName() + t.getLocalScheduleDefine().getBusId());
taskScheduleDefineMapper.updateTaskValid(t.getLocalScheduleDefine().getId(), false);
return;
}
}
val lastRecord = taskScheduleRecordMapper.getLast(t.getLocalScheduleDefine().getId());
Date date = lastRecord == null ? DATE_INIT : lastRecord.getExecuteDate();
boolean shouldRun = false;
Date nextDate = t.cronGenerator().next(date);
//首次执行且执行时间未到重置开始时间
if (null != t.getLocalScheduleDefine().getStartTime() && nextDate.before(t.getLocalScheduleDefine().getStartTime())) {
DATE_INIT = new Date();
log.warn("任务执行时间未到设置的开始时间,重新设置系统时间{},本次任务忽略:{}", DateUtil.formatDate(DATE_INIT, "yyyy-MM-dd HH:mm:ss"), GsonUtil.toJson(t));
return;
}
if (DateUtils.addSeconds(nextDate, 30).before(now)) {
shouldRun = true;
}
if (shouldRun) {
TaskWork localWork = (TaskWork) ApplicationContextUtils.getReadyApplicationContext().getBean(t.getLocalScheduleDefine().getBeanName());
SpringTaskExecutor.getExecutorService().submit(() -> localWork.runJob(t));
}
});
}
}
The above process clearly restores some of the main logic of task scheduling. It can be seen from the part of the code of task scheduling that the exception of the entire scheduling process is caught, and the occurrence of an exception will not affect the next scheduling execution. The misfire problem handling strategy of the task is:
- The task will not be executed after the user's set time
- The task will not be executed before the user's set time
- The first execution of the task is abnormal (subject to the database execution record), the current time is used as the trigger frequency to trigger an execution immediately, and then it is executed in sequence according to the cron frequency (similar to the default withMisfireHandlingInstructionFireAndProceed mode similar to Quartz)
- The timed task has been executed and executed at the first frequency time that was missed. After redoing all the missed frequency cycles, when the time of the next trigger frequency is greater than the current time, it will be executed in sequence according to the normal cron frequency (similar to WithMisfireHandlingInstructionIgnoreMisfires mode in Quartz)
In addition, you need to consider that in the same business scenario, users will create multiple task definitions, but the business logic they perform is the same (execution strategy, execution time, etc.).
Task execution
The tasks submitted by task scheduling are processed by the thread pool, and some general processing is performed on the tasks according to the task definition before and after execution (the yellow box part), and the specific execution business logic is handed over to the execute() method of the interface LocalWork implementation class.
/**
* description: 辅助来完成默认的localWork方法
*/
public class TaskWorkUtils {
static void helpRun(TaskWork localWork, ScheduleTask scheduleTask) {
//部分伪代码如下
}
}
//是否任务有执行过
boolean executed = false;
TaskScheduleRecord record = null;
Date executeDate = new Date();
try {
//根据需要决定是否获取锁后执行(redisLock,zkLock,dbLock都可以,保证任务唯一执行)
String lockName = localWork.getClass().getSimpleName() + scheduleTask.getLocalScheduleDefine().getBusId();
//获取不到锁return
//获取到执行下面逻辑
record = ExceptionUtils.doFunLogE(() -> {
TaskScheduleRecord newRecord = buildRecord(scheduleTask, executeDate);
newRecord.setId(taskRecordService.save(newRecord));
return newRecord;
});
//如果不能保存成功,表示出现了数据库异常,相应状态不能存取,则直接返回,不再执行
if (record == null) {
return;
}
executed = true;
localWork.execute(record);
} catch (Throwable throwable) {
log.error("执行任务时出现异常信息:{}", throwable.getMessage(), throwable);
e = throwable;
} finally {
//释放锁:releaseLock()
//记录异常日志,更新任务状态和失败原因
if (record != null) {
}
}
if (!scheduleTask.getLocalScheduleDefine().getOnce()&&executed) {
Date next = scheduleTask.cronGenerator().next(executeDate);
long delay = next.getTime() - executeDate.getTime();
SpringTaskExecutor.getExecutorService().schedule(() -> localWork.runJob(scheduleTask), delay, TimeUnit.MILLISECONDS);
}
}
If you want to ensure that the unique execution of tasks in the cluster can be achieved through distributed locks, the specific keys have been given for reference, because the function of cluster node registration is not provided, and the scheduling of load balancing can only rely on the randomness of the nodes in the cluster to obtain locks, that is That node acquires the lock, and on which node the task is executed.
When a task execution error occurs (after the execution record is saved), the execution of the next task will not be affected, but the result of this task execution and the reason for the failure will be updated.
Task design summary
When the application starts, the task is initialized, the task loading thread is turned on, and the task scheduling thread is turned on. The task loading thread periodically obtains all tasks from the DB and updates the task instances in the cache; the scheduling thread is responsible for making a series of judgments on the task definition instances and deciding whether to hand it over to the execution thread pool for execution. Task loading and calling can use one Timed thread pool.
private ScheduledExecutorService internalScheduledExecutor = new ScheduledThreadPoolExecutor(2,
new ThreadFactoryBuilder().setNameFormat("task-internal-%d").build());
The thread pool that executes the task receives the submitted task, and performs unified processing before and after execution, and the specific business logic of task execution is handed over to the specific implementation class to do. In the entire processing flow, two tables (task definition table + task execution record table) are required, and two timing thread pools can be completed.
to sum up
Based on the characteristics of user-defined timed tasks, this article introduces the task execution process in detail from four aspects: task creation, task loading, task scheduling, and task execution. Common problems and processing procedures in timed tasks are attached with part of the code for reference. , While supporting general timing tasks, it provides a practical method for user-defined timing tasks.
For more technical dry goods, please pay attention to [Netease Smart Enterprise Technology+] WeChat public account
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。