如何设计实现一个定时任务执行器 - SpringBoot环境下的最佳实践
在企业级应用开发中,定时任务是一个常见而重要的需求。无论是数据清理、报表生成、缓存更新还是其他周期性工作,一个高效可靠的定时任务执行器都是系统不可或缺的组成部分。本文将深入探讨如何在SpringBoot环境下设计并实现一个功能完善的定时任务执行器。
一、需求分析
设计定时任务执行器前,我们需要明确其核心功能:
- 任务调度 - 按照预设时间或周期执行任务
- 任务管理 - 动态添加、修改、删除任务
- 状态监控 - 监控任务执行状态和历史记录
- 异常处理 - 妥善处理任务执行过程中的异常
- 集群支持 - 在分布式环境下避免任务重复执行
二、技术选型
在SpringBoot环境下实现定时任务,主要有以下几种方案:
1. Spring自带的@Scheduled注解
最简单的实现方式,适合简单场景:
java
@Component
public class SimpleTask {
@Scheduled(cron = "0 0 12 * * ?")
public void executeTask() {
System.out.println("定时任务执行:" + new Date());
}
}
2. Spring Task + 数据库存储
通过TaskScheduler实现,结合数据库进行任务管理:
java
@Service
public class DynamicTaskService {
@Autowired
private TaskScheduler taskScheduler;
private Map<String, ScheduledFuture<?>> scheduledTasks = new ConcurrentHashMap<>();
public void addTask(String taskId, String cronExpression, Runnable task) {
ScheduledFuture<?> scheduledFuture = taskScheduler.schedule(
task,
new CronTrigger(cronExpression)
);
scheduledTasks.put(taskId, scheduledFuture);
}
public void cancelTask(String taskId) {
ScheduledFuture<?> scheduledFuture = scheduledTasks.get(taskId);
if (scheduledFuture != null) {
scheduledFuture.cancel(true);
scheduledTasks.remove(taskId);
}
}
}
3. Quartz调度框架
功能丰富的企业级调度框架,支持集群:
java
@Configuration
public class QuartzConfig {
@Bean
public JobDetail sampleJobDetail() {
return JobBuilder.newJob(SampleJob.class)
.withIdentity("sampleJob")
.storeDurably()
.build();
}
@Bean
public Trigger sampleJobTrigger() {
SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
.withIntervalInHours(2)
.repeatForever();
return TriggerBuilder.newTrigger()
.forJob(sampleJobDetail())
.withIdentity("sampleTrigger")
.withSchedule(scheduleBuilder)
.build();
}
}
4. XXL-JOB等开源分布式调度平台
提供更完善的功能和友好的管理界面。
三、设计方案
综合考虑,我推荐设计一个基于Quartz + 数据库 + RestAPI的定时任务执行器。
1. 系统架构
![系统架构图]
核心组件:
- 任务存储层:使用MySQL存储任务配置和执行记录
- 任务调度层:基于Quartz实现任务调度
- 业务逻辑层:实现任务的具体执行逻辑
- API接口层:提供REST接口进行任务管理
- 监控统计层:记录和展示任务执行情况
2. 数据模型设计
任务表:
sql
CREATE TABLE `task_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`task_name` varchar(100) NOT NULL COMMENT '任务名称',
`task_class` varchar(255) NOT NULL COMMENT '任务类名',
`cron_expression` varchar(50) NOT NULL COMMENT 'cron表达式',
`params` text COMMENT '任务参数',
`status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态:0-停止,1-运行',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_task_name` (`task_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
执行记录表:
sql
CREATE TABLE `task_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`task_id` bigint(20) NOT NULL COMMENT '任务ID',
`start_time` datetime NOT NULL COMMENT '开始时间',
`end_time` datetime DEFAULT NULL COMMENT '结束时间',
`execution_time` int(11) DEFAULT NULL COMMENT '执行时长(ms)',
`status` tinyint(4) NOT NULL COMMENT '状态:0-失败,1-成功',
`error_msg` text COMMENT '错误信息',
PRIMARY KEY (`id`),
KEY `idx_task_id` (`task_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
四、核心实现
1. 配置Quartz
java
@Configuration
public class QuartzConfig {
@Bean
public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) {
SchedulerFactoryBean factory = new SchedulerFactoryBean();
factory.setDataSource(dataSource);
factory.setQuartzProperties(quartzProperties());
// 延时启动
factory.setStartupDelay(10);
factory.setApplicationContextSchedulerContextKey("applicationContext");
factory.setOverwriteExistingJobs(true);
factory.setAutoStartup(true);
return factory;
}
@Bean
public Properties quartzProperties() {
Properties prop = new Properties();
// 调度器实例名称
prop.put("org.quartz.scheduler.instanceName", "TaskScheduler");
// 调度器实例编号自动生成
prop.put("org.quartz.scheduler.instanceId", "AUTO");
// 持久化方式配置
prop.put("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX");
// 持久化方式配置数据库驱动
prop.put("org.quartz.jobStore.driverDelegateClass", "org.quartz.impl.jdbcjobstore.StdJDBCDelegate");
// 集群配置
prop.put("org.quartz.jobStore.isClustered", "true");
prop.put("org.quartz.jobStore.clusterCheckinInterval", "15000");
prop.put("org.quartz.jobStore.maxMisfiresToHandleAtATime", "1");
prop.put("org.quartz.jobStore.misfireThreshold", "12000");
prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_");
return prop;
}
}
2. 任务基类
java
public abstract class BaseTask implements Job {
protected final Logger logger = LoggerFactory.getLogger(getClass());
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
JobDataMap dataMap = context.getMergedJobDataMap();
Long taskId = dataMap.getLong("taskId");
String params = dataMap.getString("params");
// 记录执行开始
TaskLogService taskLogService = SpringContextUtil.getBean(TaskLogService.class);
Long logId = taskLogService.startTaskLog(taskId);
try {
// 执行具体任务
doExecute(params);
// 记录执行成功
taskLogService.endTaskLog(logId, true, null);
} catch (Exception e) {
logger.error("Task execution error: ", e);
// 记录执行失败
taskLogService.endTaskLog(logId, false, e.getMessage());
throw new JobExecutionException(e);
}
}
/**
* 具体任务实现
*/
protected abstract void doExecute(String params) throws Exception;
}
3. 任务管理服务
java
@Service
public class TaskService {
@Autowired
private Scheduler scheduler;
@Autowired
private TaskInfoMapper taskInfoMapper;
/**
* 添加任务
*/
@Transactional
public void addTask(TaskInfo taskInfo) throws Exception {
// 保存任务信息
taskInfoMapper.insert(taskInfo);
// 创建任务
if (taskInfo.getStatus() == 1) {
createScheduleJob(taskInfo);
}
}
/**
* 更新任务
*/
@Transactional
public void updateTask(TaskInfo taskInfo) throws Exception {
TaskInfo oldTask = taskInfoMapper.selectById(taskInfo.getId());
// 更新数据库中的任务信息
taskInfoMapper.updateById(taskInfo);
// 如果任务正在运行,则更新任务
if (oldTask.getStatus() == 1) {
// 先删除旧任务
deleteScheduleJob(oldTask);
}
// 创建新任务
if (taskInfo.getStatus() == 1) {
createScheduleJob(taskInfo);
}
}
/**
* 删除任务
*/
@Transactional
public void deleteTask(Long taskId) throws Exception {
TaskInfo taskInfo = taskInfoMapper.selectById(taskId);
if (taskInfo != null) {
// 删除运行的任务
if (taskInfo.getStatus() == 1) {
deleteScheduleJob(taskInfo);
}
// 删除数据库中的任务信息
taskInfoMapper.deleteById(taskId);
}
}
/**
* 手动执行一次任务
*/
public void runTaskOnce(Long taskId) throws Exception {
TaskInfo taskInfo = taskInfoMapper.selectById(taskId);
if (taskInfo == null) {
throw new RuntimeException("任务不存在");
}
JobKey jobKey = getJobKey(taskInfo);
JobDetail jobDetail = scheduler.getJobDetail(jobKey);
if (jobDetail == null) {
throw new RuntimeException("任务不在调度器中");
}
scheduler.triggerJob(jobKey);
}
/**
* 暂停任务
*/
@Transactional
public void pauseTask(Long taskId) throws Exception {
TaskInfo taskInfo = taskInfoMapper.selectById(taskId);
if (taskInfo == null || taskInfo.getStatus() != 1) {
return;
}
// 更新任务状态
taskInfo.setStatus(0);
taskInfoMapper.updateById(taskInfo);
// 暂停任务
deleteScheduleJob(taskInfo);
}
/**
* 恢复任务
*/
@Transactional
public void resumeTask(Long taskId) throws Exception {
TaskInfo taskInfo = taskInfoMapper.selectById(taskId);
if (taskInfo == null || taskInfo.getStatus() != 0) {
return;
}
// 更新任务状态
taskInfo.setStatus(1);
taskInfoMapper.updateById(taskInfo);
// 创建任务
createScheduleJob(taskInfo);
}
/**
* 创建定时任务
*/
private void createScheduleJob(TaskInfo taskInfo) throws Exception {
// 构建任务
Class<? extends Job> jobClass = (Class<? extends Job>) Class.forName(taskInfo.getTaskClass());
JobDetail jobDetail = JobBuilder.newJob(jobClass)
.withIdentity(getJobKey(taskInfo))
.build();
// 设置任务参数
jobDetail.getJobDataMap().put("taskId", taskInfo.getId());
jobDetail.getJobDataMap().put("params", taskInfo.getParams());
// 构建触发器
CronTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity(getTriggerKey(taskInfo))
.withSchedule(CronScheduleBuilder.cronSchedule(taskInfo.getCronExpression()))
.build();
// 调度任务
scheduler.scheduleJob(jobDetail, trigger);
// 如果任务已暂停则暂停任务
if (taskInfo.getStatus() == 0) {
scheduler.pauseJob(getJobKey(taskInfo));
}
}
/**
* 删除定时任务
*/
private void deleteScheduleJob(TaskInfo taskInfo) throws Exception {
scheduler.deleteJob(getJobKey(taskInfo));
}
/**
* 获取JobKey
*/
private JobKey getJobKey(TaskInfo taskInfo) {
return JobKey.jobKey("TASK_" + taskInfo.getId(), "TASK_GROUP");
}
/**
* 获取TriggerKey
*/
private TriggerKey getTriggerKey(TaskInfo taskInfo) {
return TriggerKey.triggerKey("TRIGGER_" + taskInfo.getId(), "TRIGGER_GROUP");
}
}
4. RESTful API 接口
java
@RestController
@RequestMapping("/api/tasks")
public class TaskController {
@Autowired
private TaskService taskService;
@Autowired
private TaskLogService taskLogService;
/**
* 任务列表
*/
@GetMapping
public Result<List<TaskInfo>> list() {
return Result.success(taskService.listTasks());
}
/**
* 添加任务
*/
@PostMapping
public Result<?> add(@RequestBody TaskInfo taskInfo) {
try {
taskService.addTask(taskInfo);
return Result.success();
} catch (Exception e) {
return Result.error(e.getMessage());
}
}
/**
* 修改任务
*/
@PutMapping("/{id}")
public Result<?> update(@PathVariable Long id, @RequestBody TaskInfo taskInfo) {
try {
taskInfo.setId(id);
taskService.updateTask(taskInfo);
return Result.success();
} catch (Exception e) {
return Result.error(e.getMessage());
}
}
/**
* 删除任务
*/
@DeleteMapping("/{id}")
public Result<?> delete(@PathVariable Long id) {
try {
taskService.deleteTask(id);
return Result.success();
} catch (Exception e) {
return Result.error(e.getMessage());
}
}
/**
* 运行一次
*/
@PostMapping("/{id}/run")
public Result<?> run(@PathVariable Long id) {
try {
taskService.runTaskOnce(id);
return Result.success();
} catch (Exception e) {
return Result.error(e.getMessage());
}
}
/**
* 暂停任务
*/
@PostMapping("/{id}/pause")
public Result<?> pause(@PathVariable Long id) {
try {
taskService.pauseTask(id);
return Result.success();
} catch (Exception e) {
return Result.error(e.getMessage());
}
}
/**
* 恢复任务
*/
@PostMapping("/{id}/resume")
public Result<?> resume(@PathVariable Long id) {
try {
taskService.resumeTask(id);
return Result.success();
} catch (Exception e) {
return Result.error(e.getMessage());
}
}
/**
* 任务执行记录
*/
@GetMapping("/{id}/logs")
public Result<List<TaskLog>> logs(@PathVariable Long id) {
return Result.success(taskLogService.getTaskLogs(id));
}
}
五、进阶特性
1. 任务执行状态监控
实时监控任务执行状态,包括执行时长、成功率等指标:
java
@Service
public class TaskMonitorService {
@Autowired
private TaskLogMapper taskLogMapper;
/**
* 获取任务执行统计
*/
public TaskStatistics getTaskStatistics(Long taskId) {
// 查询总执行次数
int totalCount = taskLogMapper.countByTaskId(taskId);
// 查询成功次数
int successCount = taskLogMapper.countByTaskIdAndStatus(taskId, 1);
// 查询失败次数
int failCount = taskLogMapper.countByTaskIdAndStatus(taskId, 0);
// 查询平均执行时长
int avgTime = taskLogMapper.getAvgExecutionTime(taskId);
// 查询最大执行时长
int maxTime = taskLogMapper.getMaxExecutionTime(taskId);
TaskStatistics statistics = new TaskStatistics();
statistics.setTaskId(taskId);
statistics.setTotalCount(totalCount);
statistics.setSuccessCount(successCount);
statistics.setFailCount(failCount);
statistics.setAvgTime(avgTime);
statistics.setMaxTime(maxTime);
return statistics;
}
}
2. 邮件通知
任务执行失败时发送邮件通知:
java
@Service
public class TaskNotificationService {
@Autowired
private JavaMailSender mailSender;
@Value("${spring.mail.username}")
private String from;
@Autowired
private TaskInfoMapper taskInfoMapper;
/**
* 发送任务执行失败通知
*/
public void sendFailureNotification(Long taskId, String errorMsg) {
TaskInfo taskInfo = taskInfoMapper.selectById(taskId);
if (taskInfo == null) {
return;
}
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(from);
message.setTo("[email protected]");
message.setSubject("任务执行失败通知: " + taskInfo.getTaskName());
message.setText("任务ID: " + taskId + "\n"
+ "任务名称: " + taskInfo.getTaskName() + "\n"
+ "失败原因: " + errorMsg + "\n"
+ "失败时间: " + new Date());
mailSender.send(message);
}
}
3. 分布式锁确保集群环境下任务不重复执行
java
@Component
public class DistributedLockJobListener implements JobListener {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String LOCK_PREFIX = "task_lock:";
@Override
public String getName() {
return "distributedLockJobListener";
}
@Override
public void jobToBeExecuted(JobExecutionContext context) {
JobDetail jobDetail = context.getJobDetail();
String taskId = jobDetail.getJobDataMap().getString("taskId");
String lockKey = LOCK_PREFIX + taskId;
Boolean acquired = redisTemplate.opsForValue().setIfAbsent(
lockKey,
context.getFireInstanceId(),
5, // 锁定5分钟,防止死锁
TimeUnit.MINUTES
);
if (acquired == null || !acquired) {
// 获取锁失败,不执行任务
throw new JobExecutionException("任务已在其他节点执行,本次执行取消");
}
logger.info("任务开始执行,已获取分布式锁: {}", lockKey);
}
@Override
public void jobExecutionVetoed(JobExecutionContext context) {
// 任务被否决,释放锁
releaseLock(context);
}
@Override
public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
// 任务执行完成,释放锁
releaseLock(context);
if (jobException != null) {
logger.error("任务执行失败", jobException);
}
}
private void releaseLock(JobExecutionContext context) {
JobDetail jobDetail = context.getJobDetail();
String taskId = jobDetail.getJobDataMap().getString("taskId");
String lockKey = LOCK_PREFIX + taskId;
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else " +
"return 0 " +
"end";
redisTemplate.execute(
(RedisCallback<Object>) connection -> connection.eval(
script.getBytes(),
ReturnType.BOOLEAN,
1,
lockKey.getBytes(),
context.getFireInstanceId().getBytes()
)
);
logger.info("任务执行完成,已释放分布式锁: {}", lockKey);
}
}
六、性能优化
- 批处理任务设计:对于数据量大的任务,采用分页或批处理方式:
java
@Component
public class BatchProcessingTask extends BaseTask {
@Autowired
private UserService userService;
@Override
protected void doExecute(String params) throws Exception {
JSONObject paramsObj = JSON.parseObject(params);
int batchSize = paramsObj.getIntValue("batchSize", 100);
long totalCount = userService.count();
long pages = (totalCount + batchSize - 1) / batchSize;
for (int i = 0; i < pages; i++) {
List<User> users = userService.listByPage(i, batchSize);
processUserBatch(users);
}
}
private void processUserBatch(List<User> users) {
// 处理一批用户数据
}
}
- 使用线程池:对于可并行处理的任务,使用线程池提高吞吐量:
java
@Configuration
public class ThreadPoolConfig {
@Bean
public ThreadPoolExecutor taskExecutor() {
return new ThreadPoolExecutor(
5, // 核心线程数
20, // 最大线程数
60, // 空闲线程存活时间
TimeUnit.SECONDS, // 时间单位
new LinkedBlockingQueue<>(1000), // 工作队列
new ThreadFactoryBuilder().setNameFormat("task-pool-%d").build(), // 线程工厂
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
}
}
七、总结
通过本文介绍的方案,我们设计了一个功能完善的定时任务执行器,具有以下特点:
- 功能完善:支持任务的CRUD操作,提供任务运行、暂停、恢复等功能
- 可靠性高:通过分布式锁确保集群环境下任务不重复执行
- 可监控:记录任务执行记录,统计执行情况
- 易扩展:基于SpringBoot和Quartz框架,易于扩展新功能
在实际应用中,还可以根据具体需求进一步优化,如:
- 接入登录认证和权限控制
- 开发友好的Web管理界面
- 增加任务依赖关系支持
- 集成更复杂的调度策略
- 支持任务执行结果的处理和回调
通过这样一个完善的定时任务执行器,可以大大提高系统运维效率,确保系统中的定时任务可靠执行。