如何设计实现一个定时任务执行器 - SpringBoot环境下的最佳实践

如何设计实现一个定时任务执行器 - SpringBoot环境下的最佳实践

在企业级应用开发中,定时任务是一个常见而重要的需求。无论是数据清理、报表生成、缓存更新还是其他周期性工作,一个高效可靠的定时任务执行器都是系统不可或缺的组成部分。本文将深入探讨如何在SpringBoot环境下设计并实现一个功能完善的定时任务执行器。

一、需求分析

设计定时任务执行器前,我们需要明确其核心功能:

  1. 任务调度 - 按照预设时间或周期执行任务
  2. 任务管理 - 动态添加、修改、删除任务
  3. 状态监控 - 监控任务执行状态和历史记录
  4. 异常处理 - 妥善处理任务执行过程中的异常
  5. 集群支持 - 在分布式环境下避免任务重复执行

二、技术选型

在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("admin@example.com");
        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);
    }
}

六、性能优化

  1. 批处理任务设计:对于数据量大的任务,采用分页或批处理方式:
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) {
        // 处理一批用户数据
    }
}
  1. 使用线程池:对于可并行处理的任务,使用线程池提高吞吐量:
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() // 拒绝策略
        );
    }
}

七、总结

通过本文介绍的方案,我们设计了一个功能完善的定时任务执行器,具有以下特点:

  1. 功能完善:支持任务的CRUD操作,提供任务运行、暂停、恢复等功能
  2. 可靠性高:通过分布式锁确保集群环境下任务不重复执行
  3. 可监控:记录任务执行记录,统计执行情况
  4. 易扩展:基于SpringBoot和Quartz框架,易于扩展新功能

在实际应用中,还可以根据具体需求进一步优化,如:

  • 接入登录认证和权限控制
  • 开发友好的Web管理界面
  • 增加任务依赖关系支持
  • 集成更复杂的调度策略
  • 支持任务执行结果的处理和回调

通过这样一个完善的定时任务执行器,可以大大提高系统运维效率,确保系统中的定时任务可靠执行。

相关推荐
武子康6 分钟前
大数据-145 Apache Kudu 架构与实战:RowSet、分区与 Raft 全面解析
大数据·后端·nosql
间彧7 分钟前
Spring @ControllerAdvice详解与应用实战
后端
间彧11 分钟前
@ControllerAdvice与AOP切面编程在处理异常时有什么区别和各自的优势?
后端
间彧36 分钟前
什么是Region多副本容灾
后端
爱敲代码的北37 分钟前
WPF容器控件布局与应用学习笔记
后端
爱敲代码的北37 分钟前
XAML语法与静态资源应用
后端
清空mega39 分钟前
从零开始搭建 flask 博客实验(5)
后端·python·flask
爱敲代码的北43 分钟前
UniformGrid 均匀网格布局学习笔记
后端
一只叫煤球的猫1 小时前
从1996到2025——细说Java锁的30年进化史
java·后端·性能优化
喵个咪1 小时前
开箱即用的GO后台管理系统 Kratos Admin - 数据脱敏和隐私保护
后端·go·protobuf