在企业级应用开发中,定时任务是一个非常常见的需求。比如每天凌晨统计前一天的订单数据、定期清理临时文件、发送营销邮件等。Spring Boot 提供了多种实现定时任务的方式,本文将从入门到进阶,全面剖析几种主流的实现方案,并通过实际案例帮助你选择最适合自己项目的方案。

一、Spring Boot 实现定时任务的四种方式

Spring Boot 中实现定时任务主要有四种方式:

  1. @Scheduled注解(Spring Boot 内置)
  2. Spring Task(可编程方式动态管理任务)
  3. Quartz(功能强大的任务调度框架)
  4. XXL-Job(分布式任务调度平台)

下面我们逐一详细介绍。

二、@Scheduled 注解(最简单的方式)

1. 基本使用

这是 Spring Boot 内置的最简单实现方式,只需两步即可完成:

步骤 1:启用定时任务

在启动类上添加@EnableScheduling注解:

@SpringBootApplication
@EnableScheduling
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

步骤 2:创建定时任务类

@Component
public class ScheduledTasks {

    private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class);
    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");

    // 每隔5秒执行一次
    @Scheduled(fixedRate = 5000)
    public void reportCurrentTime() {
        log.info("当前时间:{}", dateFormat.format(new Date()));
    }

    // 每天凌晨1点执行
    @Scheduled(cron = "0 0 1 * * ?")
    public void dailyTask() {
        log.info("执行每日任务");
    }
}

2. @Scheduled 注解的几种模式

@Scheduled注解支持多种执行模式,使用场景各不相同:

  • fixedRate:固定速率执行,任务按照严格的时间间隔执行,不考虑上次任务的执行时间
  • fixedDelay:固定延迟执行,上次执行完成后,延迟指定时间再执行
  • initialDelay:首次延迟执行,与 fixedRate 或 fixedDelay 结合使用
  • cron:使用 cron 表达式指定执行时间

下面是几个实际例子:

// 固定速率:每3秒执行一次,不管任务执行要多久
@Scheduled(fixedRate = 3000)
public void taskWithFixedRate() {
    log.info("固定速率任务开始");
    // 任务逻辑
}

// 固定延迟:上次执行完成后等待3秒再执行
@Scheduled(fixedDelay = 3000)
public void taskWithFixedDelay() {
    log.info("固定延迟任务开始");
    // 任务逻辑
}

// 组合使用:首次延迟5秒,之后每3秒执行一次
@Scheduled(initialDelay = 5000, fixedRate = 3000)
public void taskWithInitialDelay() {
    log.info("首次延迟任务开始");
    // 任务逻辑
}

// Cron表达式:每分钟的第0秒执行一次
@Scheduled(cron = "0 * * * * ?")
public void taskWithCron() {
    log.info("Cron任务开始");
    // 任务逻辑
}

3. Cron 表达式详解

Cron 表达式格式为:秒 分 时 日 月 周(年),其中年是可选的。当"日"和"周"字段同时存在时,必须有一个设为?来避免冲突。

下面是一些常用的 Cron 表达式示例:

Cron 表达式 含义
0 0 12 * * ? 每天中午 12 点执行
0 15 10 ? * * 每天上午 10:15 执行
0 15 10 * * ? 每天上午 10:15 执行
0 0 10,14,16 * * ? 每天上午 10 点、下午 2 点、4 点执行
0 0/30 9-17 * * ? 每天 9:00 至 17:00 之间每半小时执行
0 0 12 ? * WED 每周三中午 12 点执行
0 0 12 1 * ? 每月 1 日中午 12 点执行

4. @Scheduled 的线程池配置

默认情况下,Spring Boot 中的@Scheduled任务是由单线程执行的,这意味着如果一个任务执行时间过长,会阻塞其他任务。在实际应用中,通常需要配置线程池:

@Configuration
public class SchedulingConfig implements SchedulingConfigurer {

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        // 创建一个线程池调度器
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        // 设置线程池大小
        taskScheduler.setPoolSize(10);
        // 设置线程名前缀
        taskScheduler.setThreadNamePrefix("scheduled-task-pool-");
        // 设置等待任务完成再关闭线程池
        taskScheduler.setWaitForTasksToCompleteOnShutdown(true);
        // 等待时间(单位:秒)
        taskScheduler.setAwaitTerminationSeconds(60);
        taskScheduler.initialize();

        taskRegistrar.setTaskScheduler(taskScheduler);
    }
}

这样配置后,多个定时任务可以并行执行,互不影响。

下面是单线程和多线程执行的对比时序图:

三、Spring Task(动态管理任务)

如果需要在运行时动态管理任务(创建、修改、删除),可以使用 Spring Task:

@Service
public class DynamicTaskService {

    @Autowired
    private ThreadPoolTaskScheduler taskScheduler;

    // 存储任务Future的Map
    private final Map<String, ScheduledFuture<?>> scheduledTasks = new ConcurrentHashMap<>();

    // 添加一个新的定时任务
    public void addCronTask(String taskId, String cronExpression, Runnable task) {
        // 验证cronExpression是否有效
        if (taskId == null || taskId.trim().isEmpty()) {
            throw new IllegalArgumentException("任务ID不能为空");
        }

        try {
            // 检查cron表达式的合法性
            if (!CronExpression.isValidExpression(cronExpression)) {
                throw new IllegalArgumentException("无效的cron表达式: " + cronExpression);
            }
        } catch (Exception e) {
            throw new IllegalArgumentException("cron表达式错误: " + e.getMessage(), e);
        }

        // 如果任务已存在,先移除
        if (scheduledTasks.containsKey(taskId)) {
            cancelTask(taskId);
        }

        // 创建触发器
        CronTrigger trigger = new CronTrigger(cronExpression);
        // 调度任务并保存future
        ScheduledFuture<?> future = taskScheduler.schedule(task, trigger);
        scheduledTasks.put(taskId, future);
    }

    // 取消任务
    public boolean cancelTask(String taskId) {
        ScheduledFuture<?> future = scheduledTasks.get(taskId);
        if (future != null) {
            boolean cancelled = future.cancel(true);
            if (cancelled) {
                scheduledTasks.remove(taskId);
            }
            return cancelled;
        }
        return false;
    }

    // 获取所有任务ID
    public Set<String> getAllTaskIds() {
        return scheduledTasks.keySet();
    }
}

这种方式特别适合从配置中心或数据库加载定时任务配置的场景。

四、Quartz(功能完备的调度框架)

对于需要持久化、集群、精确调度的场景,Quartz 是更好的选择。

1. 基本配置

首先添加 Quartz 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

然后配置 Quartz:

# 应用属性文件中配置Quartz
spring.quartz.job-store-type=jdbc
spring.quartz.properties.org.quartz.scheduler.instanceName=MyClusteredScheduler
spring.quartz.properties.org.quartz.scheduler.instanceId=AUTO
spring.quartz.properties.org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
spring.quartz.properties.org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
spring.quartz.properties.org.quartz.jobStore.tablePrefix=QRTZ_
spring.quartz.properties.org.quartz.jobStore.isClustered=true
spring.quartz.properties.org.quartz.jobStore.clusterCheckinInterval=20000
spring.quartz.properties.org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
spring.quartz.properties.org.quartz.threadPool.threadCount=10
spring.quartz.properties.org.quartz.threadPool.threadPriority=5
spring.quartz.jdbc.initialize-schema=always

2. 创建 Quartz 任务

定义 Job

@DisallowConcurrentExecution  // 防止同一个任务实例被并发执行
@PersistJobDataAfterExecution // 更新JobDataMap
public class DataCleanupJob implements Job {

    private static final Logger log = LoggerFactory.getLogger(DataCleanupJob.class);

    @Autowired
    private DataCleanupService dataCleanupService;

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        JobDataMap dataMap = context.getJobDetail().getJobDataMap();
        int daysToKeep = dataMap.getInt("daysToKeep");
        String dataType = dataMap.getString("dataType");

        log.info("开始清理{}数据,保留{}天", dataType, daysToKeep);

        try {
            // 使用业务主键或状态字段确保幂等性(避免重复处理)
            int cleanedCount = dataCleanupService.cleanupData(dataType, daysToKeep);
            log.info("成功清理{}条{}数据", cleanedCount, dataType);

            // 更新JobDataMap,记录最后执行时间
            dataMap.put("lastExecutionTime", new Date().getTime());
            dataMap.put("lastCleanedCount", cleanedCount);
        } catch (Exception e) {
            log.error("清理数据失败", e);
            throw new JobExecutionException(e);
        }
    }
}

注册 Job 和 Trigger

@Configuration
public class QuartzConfig {

    @Bean
    public JobDetail dataCleanupJobDetail() {
        return JobBuilder.newJob(DataCleanupJob.class)
                .withIdentity("dataCleanupJob", "maintenance")
                .usingJobData("daysToKeep", 30)
                .usingJobData("dataType", "logs")
                .storeDurably()
                .build();
    }

    @Bean
    public Trigger dataCleanupTrigger() {
        // 创建CronScheduleBuilder
        CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0 0 1 * * ?");

        // 配置失败重试策略
        scheduleBuilder.withMisfireHandlingInstructionFireAndProceed();

        return TriggerBuilder.newTrigger()
                .forJob(dataCleanupJobDetail())
                .withIdentity("dataCleanupTrigger", "maintenance")
                .withDescription("每天凌晨1点执行日志清理")
                .withSchedule(scheduleBuilder)
                .build();
    }
}

3. Quartz 任务管理服务

创建一个服务类用于动态管理 Quartz 任务:

@Service
public class QuartzJobService {

    @Autowired
    private Scheduler scheduler;

    // 添加新任务
    public void addJob(Class<? extends Job> jobClass, String jobName, String jobGroup,
                      String cronExpression, Map<String, Object> jobData) throws Exception {

        JobDetail jobDetail = JobBuilder.newJob(jobClass)
                .withIdentity(jobName, jobGroup)
                .storeDurably()
                .build();

        // 设置JobDataMap
        if (jobData != null && !jobData.isEmpty()) {
            jobDetail.getJobDataMap().putAll(jobData);
        }

        CronTrigger trigger = TriggerBuilder.newTrigger()
                .withIdentity(jobName + "Trigger", jobGroup)
                .withSchedule(CronScheduleBuilder.cronSchedule(cronExpression))
                .build();

        scheduler.scheduleJob(jobDetail, trigger);
    }

    // 修改任务执行时间
    public void updateJobCron(String jobName, String jobGroup, String cronExpression) throws Exception {
        TriggerKey triggerKey = TriggerKey.triggerKey(jobName + "Trigger", jobGroup);
        CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);

        if (trigger == null) {
            throw new IllegalArgumentException("找不到对应的触发器");
        }

        // 创建新的触发器
        CronTrigger newTrigger = TriggerBuilder.newTrigger()
                .withIdentity(triggerKey)
                .withSchedule(CronScheduleBuilder.cronSchedule(cronExpression))
                .build();

        // 重新调度任务
        scheduler.rescheduleJob(triggerKey, newTrigger);
    }

    // 暂停任务
    public void pauseJob(String jobName, String jobGroup) throws Exception {
        JobKey jobKey = JobKey.jobKey(jobName, jobGroup);
        scheduler.pauseJob(jobKey);
    }

    // 恢复任务
    public void resumeJob(String jobName, String jobGroup) throws Exception {
        JobKey jobKey = JobKey.jobKey(jobName, jobGroup);
        scheduler.resumeJob(jobKey);
    }

    // 删除任务
    public void deleteJob(String jobName, String jobGroup) throws Exception {
        JobKey jobKey = JobKey.jobKey(jobName, jobGroup);
        scheduler.deleteJob(jobKey);
    }

    // 获取所有任务
    public List<Map<String, Object>> getAllJobs() throws Exception {
        List<Map<String, Object>> jobList = new ArrayList<>();

        for (String groupName : scheduler.getJobGroupNames()) {
            for (JobKey jobKey : scheduler.getJobKeys(GroupMatcher.jobGroupEquals(groupName))) {
                Map<String, Object> jobMap = new HashMap<>();
                jobMap.put("jobName", jobKey.getName());
                jobMap.put("jobGroup", jobKey.getGroup());

                List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey);
                for (Trigger trigger : triggers) {
                    jobMap.put("nextFireTime", trigger.getNextFireTime());

                    if (trigger instanceof CronTrigger) {
                        CronTrigger cronTrigger = (CronTrigger) trigger;
                        jobMap.put("cronExpression", cronTrigger.getCronExpression());
                    }

                    Trigger.TriggerState triggerState = scheduler.getTriggerState(trigger.getKey());
                    jobMap.put("triggerState", triggerState.name());
                }

                jobList.add(jobMap);
            }
        }

        return jobList;
    }
}

4. Quartz 集群配置

Quartz 支持集群部署,以便在多个节点上实现高可用和负载均衡。关键是使用数据库来协调各个节点:

Quartz 集群通过数据库锁实现任务互斥的原理如下图所示。当任务触发时,集群中的节点会竞争获取数据库锁(SELECT FOR UPDATE),确保同一任务只在一个节点上执行。

为了启用 Quartz 集群,首先需要创建 Quartz 相关的数据库表。Quartz 提供了各种数据库的初始化脚本,例如 MySQL 脚本位于:

quartz-2.3.0/src/main/resources/org/quartz/impl/jdbcjobstore/tables_mysql.sql

其中qrtz_locks表是实现分布式锁的关键,存储锁名称并通过数据库行锁机制确保任务互斥。

五、XXL-Job(分布式任务调度平台)

对于复杂的分布式系统,XXL-Job 提供了更全面的解决方案,包括可视化管理界面、任务分片、失败告警等特性。

1. 基本架构

XXL-Job 由两部分组成:

  • 调度中心:负责管理任务、调度任务
  • 执行器:负责接收调度并执行任务

2. 集成步骤

步骤 1:添加依赖

<dependency>
    <groupId>com.xuxueli</groupId>
    <artifactId>xxl-job-core</artifactId>
    <version>2.3.1</version>
</dependency>

步骤 2:配置执行器

# application.properties
xxl.job.admin.addresses=http://localhost:8080/xxl-job-admin
xxl.job.accessToken=default_token
xxl.job.executor.appname=my-xxl-job-executor
xxl.job.executor.ip=
xxl.job.executor.port=9999
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
xxl.job.executor.logretentiondays=30

步骤 3:创建配置类

@Configuration
public class XxlJobConfig {

    private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);

    @Value("${xxl.job.admin.addresses}")
    private String adminAddresses;

    @Value("${xxl.job.accessToken}")
    private String accessToken;

    @Value("${xxl.job.executor.appname}")
    private String appname;

    @Value("${xxl.job.executor.ip}")
    private String ip;

    @Value("${xxl.job.executor.port}")
    private int port;

    @Value("${xxl.job.executor.logpath}")
    private String logPath;

    @Value("${xxl.job.executor.logretentiondays}")
    private int logRetentionDays;

    @Bean
    public XxlJobSpringExecutor xxlJobExecutor() {
        logger.info(">>>>>>>>>>> xxl-job config init.");
        XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
        xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
        xxlJobSpringExecutor.setAppname(appname);
        xxlJobSpringExecutor.setIp(ip);
        xxlJobSpringExecutor.setPort(port);
        xxlJobSpringExecutor.setAccessToken(accessToken);
        xxlJobSpringExecutor.setLogPath(logPath);
        xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);

        return xxlJobSpringExecutor;
    }
}

步骤 4:创建任务执行器

@Component
public class OrderTaskHandler {

    private static Logger logger = LoggerFactory.getLogger(OrderTaskHandler.class);

    @Autowired
    private OrderService orderService;

    @XxlJob("cancelTimeoutOrderHandler")
    public void cancelTimeoutOrder() {
        logger.info("开始处理超时未支付订单...");

        try {
            // 查询所有创建时间超过30分钟且状态为未支付的订单
            Date thirtyMinutesAgo = new Date(System.currentTimeMillis() - 30 * 60 * 1000);

            // 为避免一次处理过多数据导致内存问题,使用分页处理
            int pageSize = 100;
            int pageNum = 1;
            int total = 0;

            while (true) {
                List<Order> timeoutOrders = orderService.findTimeoutOrders(thirtyMinutesAgo, pageNum, pageSize);
                if (timeoutOrders.isEmpty()) {
                    break;
                }

                for (Order order : timeoutOrders) {
                    try {
                        // 使用订单状态确保幂等性,避免重复取消
                        orderService.cancelOrder(order.getId());
                        total++;
                    } catch (Exception e) {
                        logger.error("取消订单{}失败", order.getId(), e);
                        // 可以记录失败订单,后续重试
                    }
                }

                pageNum++;
            }

            logger.info("成功取消{}个超时订单", total);

        } catch (Exception e) {
            logger.error("处理超时订单异常", e);
            // 抛出异常,XXL-Job会记录任务失败,并根据配置重试
            throw new RuntimeException(e);
        }
    }
}

3. 分片任务

XXL-Job 支持分片任务,适合需要并行处理大量数据的场景:

@Component
public class UserPointsHandler {

    private static Logger logger = LoggerFactory.getLogger(UserPointsHandler.class);

    @Autowired
    private UserService userService;

    @XxlJob("calculateUserPointsHandler")
    public void calculateUserPoints() {
        // 分片参数
        int shardIndex = XxlJobHelper.getShardIndex();  // 当前分片索引
        int shardTotal = XxlJobHelper.getShardTotal();  // 总分片数

        logger.info("用户积分计算任务开始,当前分片:{}/{}", shardIndex, shardTotal);

        try {
            // 任务参数(可在XXL-Job管理界面配置)
            String param = XxlJobHelper.getJobParam();
            Integer pointsToAdd = StringUtils.hasText(param) ? Integer.parseInt(param) : 10;

            // 根据用户ID分片,例如:用户ID % 分片总数 == 当前分片索引
            List<User> users = userService.findUsersForShard(shardIndex, shardTotal);

            int count = 0;
            for (User user : users) {
                try {
                    userService.addPoints(user.getId(), pointsToAdd);
                    count++;
                } catch (Exception e) {
                    logger.error("为用户{}添加积分失败", user.getId(), e);
                }
            }

            logger.info("分片{}/{}完成,成功处理{}个用户", shardIndex, shardTotal, count);

        } catch (Exception e) {
            logger.error("用户积分计算任务异常", e);
            // 设置任务结果和错误信息
            XxlJobHelper.handleFail("任务执行异常: " + e.getMessage());
            return;
        }

        // 设置任务结果
        XxlJobHelper.handleSuccess("任务执行成功");
    }
}

这个分片任务的特点是:

  1. 通过XxlJobHelper.getShardIndex()获取当前分片索引
  2. 通过XxlJobHelper.getShardTotal()获取总分片数
  3. 根据分片参数过滤需要处理的数据
  4. 每个执行器只处理属于自己分片的数据

分片任务与路由策略结合使用可以实现更精细的负载均衡:分片决定每个执行器处理哪部分数据,路由策略决定调度中心将任务路由到哪些执行器。

六、实际应用场景与方案选择

1. 单体应用,简单定时任务

场景:每天统计网站访问量

推荐方案@Scheduled注解

@Component
public class StatisticsTask {

    @Autowired
    private StatisticsService statisticsService;

    // 每天凌晨2点执行
    @Scheduled(cron = "0 0 2 * * ?")
    public void dailyStatistics() {
        statisticsService.calculateDailyStatistics();
    }
}

2. 需要动态调整执行时间的任务

场景:根据业务需求调整报表生成时间

推荐方案:Spring Task

@RestController
@RequestMapping("/api/tasks")
public class TaskController {

    @Autowired
    private DynamicTaskService taskService;

    @Autowired
    private ReportService reportService;

    @PostMapping("/report")
    public ResponseEntity<String> updateReportSchedule(@RequestParam String cronExpression) {
        try {
            taskService.addCronTask("generateReport", cronExpression, () -> {
                reportService.generateDailyReport();
            });
            return ResponseEntity.ok("报表任务调度时间已更新为: " + cronExpression);
        } catch (Exception e) {
            return ResponseEntity.badRequest().body("更新失败: " + e.getMessage());
        }
    }

    @DeleteMapping("/report")
    public ResponseEntity<String> cancelReportTask() {
        boolean result = taskService.cancelTask("generateReport");
        if (result) {
            return ResponseEntity.ok("报表任务已取消");
        } else {
            return ResponseEntity.notFound().build();
        }
    }
}

3. 分布式应用,需要集群高可用

场景:订单系统需要定期清理过期订单,要求高可用

推荐方案:Quartz 集群

@Service
public class OrderCleanupService {

    @Autowired
    private QuartzJobService quartzJobService;

    public void initOrderCleanupJob() throws Exception {
        Map<String, Object> jobData = new HashMap<>();
        jobData.put("daysToKeep", 90);
        jobData.put("orderStatus", "CANCELED");

        quartzJobService.addJob(
            OrderCleanupJob.class,
            "orderCleanupJob",
            "orderManagement",
            "0 0 3 * * ?",  // 每天凌晨3点执行
            jobData
        );
    }
}

// Job实现
public class OrderCleanupJob implements Job {

    @Autowired
    private OrderService orderService;

    @Override
    @Transactional
    public void execute(JobExecutionContext context) throws JobExecutionException {
        JobDataMap dataMap = context.getJobDetail().getJobDataMap();
        int daysToKeep = dataMap.getInt("daysToKeep");
        String status = dataMap.getString("orderStatus");

        try {
            int count = orderService.cleanupOldOrders(daysToKeep, status);
            // 记录执行结果
            dataMap.put("lastExecutionTime", System.currentTimeMillis());
            dataMap.put("lastCleanupCount", count);
        } catch (Exception e) {
            throw new JobExecutionException("清理订单失败", e);
        }
    }
}

4. 大规模分布式系统,需要任务分片

场景:电商大促前,需要为所有用户发放优惠券

推荐方案:XXL-Job

@Component
public class CouponDistributionHandler {

    @Autowired
    private CouponService couponService;

    @Autowired
    private UserService userService;

    @XxlJob("distributeCouponHandler")
    public void distributeCoupon() {
        int shardIndex = XxlJobHelper.getShardIndex();
        int shardTotal = XxlJobHelper.getShardTotal();

        String couponId = XxlJobHelper.getJobParam();
        if (StringUtils.isEmpty(couponId)) {
            XxlJobHelper.handleFail("优惠券ID不能为空");
            return;
        }

        try {
            // 根据用户ID分片
            List<User> users = userService.findActiveUsersForShard(shardIndex, shardTotal);

            int successCount = 0;
            for (User user : users) {
                try {
                    // 检查幂等性,避免重复发放
                    if (!couponService.hasCoupon(user.getId(), couponId)) {
                        couponService.issueCoupon(user.getId(), couponId);
                        successCount++;
                    }
                } catch (Exception e) {
                    log.error("为用户{}发放优惠券{}失败", user.getId(), couponId, e);
                }
            }

            XxlJobHelper.handleSuccess(String.format("成功为%d个用户发放优惠券", successCount));
        } catch (Exception e) {
            log.error("发放优惠券任务异常", e);
            XxlJobHelper.handleFail(e.getMessage());
        }
    }
}

七、常见问题与解决方案

1. 任务重复执行问题

在分布式环境中,如果多个节点部署了相同的定时任务,可能导致任务重复执行。解决方案:

  • 使用分布式锁:基于 Redis 或 ZooKeeper 实现分布式锁
  • 使用 Quartz 集群模式:自动处理任务互斥
  • 使用 XXL-Job 调度中心:统一管理任务调度

以下是使用 Redis 分布式锁的示例:

@Component
public class DistributedTask {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private TaskService taskService;

    @Scheduled(cron = "0 0 12 * * ?")
    public void executeTask() {
        String lockKey = "task_lock:daily_task";
        // 获取锁,60秒超时
        Boolean acquired = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 60, TimeUnit.SECONDS);

        if (Boolean.TRUE.equals(acquired)) {
            try {
                // 获取锁成功,执行任务
                taskService.executeDailyTask();
            } finally {
                // 释放锁
                redisTemplate.delete(lockKey);
            }
        } else {
            // 未获取到锁,任务已被其他节点执行
            log.info("任务已被其他节点执行,跳过");
        }
    }
}

2. 任务执行时间过长问题

对于执行时间长的任务,可能会影响其他任务调度或导致任务重叠执行。解决方案:

  • 异步执行:结合@Async注解或线程池
  • 任务分片:将大任务拆分为多个小任务并行执行
  • 增加超时控制:避免任务无限期执行
@Service
public class ReportService {

    @Autowired
    private ThreadPoolTaskExecutor taskExecutor;

    // 异步执行耗时任务
    public Future<String> generateReportAsync() {
        return taskExecutor.submit(() -> {
            // 设置超时控制
            try {
                return CompletableFuture.supplyAsync(this::generateReport)
                        .orTimeout(30, TimeUnit.MINUTES)
                        .get();
            } catch (TimeoutException e) {
                log.error("报表生成超时");
                throw new RuntimeException("报表生成超时", e);
            } catch (Exception e) {
                log.error("报表生成异常", e);
                throw new RuntimeException("报表生成失败", e);
            }
        });
    }

    private String generateReport() {
        // 报表生成逻辑
        return "报表生成完成";
    }
}

3. 任务失败重试与告警

任务执行失败时,需要有重试机制和告警通知。解决方案:

  • Quartz 重试:使用SimpleTrigger配置重试次数和间隔
  • XXL-Job 内置重试:在管理界面配置失败重试次数
  • 自定义重试逻辑:结合 Spring Retry 实现
// XXL-Job任务失败重试示例
@XxlJob("retryableTask")
public void executeWithRetry() {
    try {
        // 业务逻辑
        someBusinessLogic();
    } catch (Exception e) {
        // 记录异常,任务将根据XXL-Job管理界面的重试配置自动重试
        XxlJobHelper.log("任务执行失败: " + e.getMessage());
        throw e;  // 抛出异常,触发重试
    }
}

// Spring Retry示例
@Service
public class RetryService {

    // 最多重试3次,间隔1秒
    @Retryable(value = {DataAccessException.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000))
    public void doWithRetry() {
        // 可能失败的业务逻辑
    }

    // 所有重试都失败后执行
    @Recover
    public void recover(DataAccessException e) {
        // 发送告警通知
        notifyAdmins("任务执行失败,请检查: " + e.getMessage());
    }
}

八、四种实现方式对比

特性/方案 @Scheduled Spring Task Quartz XXL-Job
复杂度 中高
动态调度 不支持 支持(编程式) 支持(API 操作) 支持(界面配置)
持久化 不支持 不支持 支持 支持
集群支持 不支持 不支持 支持 支持
分布式 不支持 不支持 支持(基于数据库) 支持(调度中心)
任务监控 需自定义 内置监控界面
失败处理 需自定义 需自定义 支持(触发器配置) 内置重试与告警
管理界面 无(可自行开发) 内置完善管理界面
任务分片 不支持 不支持 不支持(需自行实现) 内置支持
动态修改执行时间 不支持 支持(编程式) 支持(API 操作) 支持(界面配置)
重试策略 需手动实现 需手动实现 触发器配置支持 内置支持
监控与管理界面 需自定义 可视化界面
学习成本 中高
社区活跃度 高(Spring 生态) 高(Spring 生态) 高(成熟开源项目) 中高(国产开源项目)
生态支持 Spring Boot Spring Boot 多框架支持 多框架支持,Docker 部署

九、总结

本文详细介绍了 Spring Boot 中实现定时任务的四种方式:

  1. @Scheduled 注解:最简单的方式,适合单体应用的简单定时任务。
  2. Spring Task:支持动态管理任务,适合需要在运行时调整任务的场景。
  3. Quartz:功能完备的调度框架,支持持久化和集群,适合需要高可用的企业级应用。
  4. XXL-Job:分布式任务调度平台,提供可视化管理界面和任务分片功能,适合大规模分布式系统。

在选择实现方式时,需要根据具体需求(如并发要求、持久化需求、分布式部署等)进行权衡。对于简单场景,@Scheduled 足够使用;对于复杂的企业级应用,Quartz 或 XXL-Job 会是更好的选择。

无论选择哪种方式,都需要注意任务的幂等性设计、失败重试机制、性能优化以及监控告警,确保定时任务能够稳定、可靠地运行。

希望本文能帮助你在 Spring Boot 项目中选择和实现适合自己需求的定时任务方案!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐