SpringBoot定时任务:从简单到集群

23 阅读11分钟

《SpringBoot定时任务:从简单到集群》

我是小坏,今天咱们聊聊定时任务。你还在用Linux的crontab吗?还在自己写个线程死循环吗?SpringBoot的定时任务,简单到哭,强大到爆!

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

一、定时任务的痛点

看看这些场景

场景1:凌晨2点,老板突然打电话:"为啥昨天数据没统计?" 你:"卧槽,服务器时间不对,crontab没执行!"

场景2:开发、测试、生产三个环境,crontab配置了3套,改一次要改3个地方。

场景3:双机部署,定时任务执行了两次,数据重复了。

SpringBoot定时任务能解决啥

  • 配置简单,跟代码在一起
  • 多环境统一配置
  • 集群防重复执行
  • 监控执行情况
  • 动态修改执行时间

二、5分钟快速上手

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

2.1 加个注解就行

@SpringBootApplication
@EnableScheduling  // 开启定时任务
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

2.2 写个最简单的任务

@Component
@Slf4j
public class SimpleTask {
    
    // 每5秒执行一次
    @Scheduled(fixedRate = 5000)
    public void doSomething() {
        log.info("定时任务执行,当前时间:{}", LocalDateTime.now());
    }
    
    // 每天凌晨2点执行
    @Scheduled(cron = "0 0 2 * * ?")
    public void dailyTask() {
        log.info("每日任务执行,开始清理日志...");
        clearLogs();
    }
    
    // 每周一上午10点执行
    @Scheduled(cron = "0 0 10 ? * MON")
    public void weeklyTask() {
        log.info("周任务执行,开始生成周报...");
        generateWeeklyReport();
    }
}

2.3 cron表达式速查

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

秒 分 时 日 月 星期
*  *  *  *  *  *
│  │  │  │  │  │
│  │  │  │  │  └─ 星期 (0-7, 07都是周日)
│  │  │  │  └──── 月 (1-12)
│  │  │  └─────── 日 (1-31)
│  │  └────────── 时 (0-23)
│  └───────────── 分 (0-59)
└──────────────── 秒 (0-59)

常用表达式:
0 0 2 * * ?    每天凌晨20 0 12 * * ?   每天中午120 0/5 * * * ?  每5分钟
0 0 9-18 * * ? 朝九晚六的每个整点
0 0 0 1 * ?    每月1号凌晨

三、进阶功能

3.1 动态定时任务

需求:定时任务的时间要从数据库读取,可随时修改

@Component
@Slf4j
public class DynamicTask {
    
    @Autowired
    private TaskConfigRepository configRepository;
    
    // 定时任务注册器
    @Autowired
    private ThreadPoolTaskScheduler taskScheduler;
    
    // 存储正在运行的任务
    private final Map<String, ScheduledFuture<?>> taskMap = new ConcurrentHashMap<>();
    
    /**
     * 初始化动态任务
     */
    @PostConstruct
    public void init() {
        // 从数据库加载所有任务配置
        List<TaskConfig> configs = configRepository.findAll();
        
        for (TaskConfig config : configs) {
            if (config.getEnabled()) {
                startTask(config);
            }
        }
    }
    
    /**
     * 启动一个动态任务
     */
    public void startTask(TaskConfig config) {
        // 如果任务已经在运行,先停止
        if (taskMap.containsKey(config.getTaskName())) {
            stopTask(config.getTaskName());
        }
        
        // 创建任务
        Runnable task = () -> {
            try {
                executeTask(config);
            } catch (Exception e) {
                log.error("任务执行失败:{}", config.getTaskName(), e);
            }
        };
        
        // 解析cron表达式
        CronTrigger trigger = new CronTrigger(config.getCronExpression());
        
        // 提交任务
        ScheduledFuture<?> future = taskScheduler.schedule(task, trigger);
        
        // 保存任务引用
        taskMap.put(config.getTaskName(), future);
        
        log.info("任务启动成功:{},cron:{}", config.getTaskName(), config.getCronExpression());
    }
    
    /**
     * 停止任务
     */
    public void stopTask(String taskName) {
        ScheduledFuture<?> future = taskMap.get(taskName);
        if (future != null) {
            future.cancel(true);
            taskMap.remove(taskName);
            log.info("任务停止成功:{}", taskName);
        }
    }
    
    /**
     * 更新任务时间
     */
    public void updateTaskCron(String taskName, String newCron) {
        TaskConfig config = configRepository.findByTaskName(taskName);
        if (config != null) {
            config.setCronExpression(newCron);
            configRepository.save(config);
            
            // 重启任务
            stopTask(taskName);
            startTask(config);
        }
    }
}

3.2 异步执行

@Configuration
@EnableAsync  // 开启异步
@EnableScheduling
public class AsyncTaskConfig {
    
    // 配置任务线程池
    @Bean
    public ThreadPoolTaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(10);  // 线程池大小
        scheduler.setThreadNamePrefix("scheduled-task-");
        scheduler.setAwaitTerminationSeconds(60);
        scheduler.setWaitForTasksToCompleteOnShutdown(true);
        return scheduler;
    }
}

// 异步执行任务
@Component
@Slf4j
public class AsyncTask {
    
    // 异步执行,不阻塞主线程
    @Async
    @Scheduled(fixedDelay = 5000)
    public void asyncTask() {
        log.info("异步任务开始执行");
        
        // 模拟耗时操作
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        
        log.info("异步任务执行完成");
    }
}

四、集群部署:防重复执行

问题:2台服务器,定时任务执行了2次 解决:用Redis分布式锁

4.1 基于Redis的分布式锁

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

@Component
@Slf4j
public class ClusterTask {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    // 每天凌晨执行,集群中只有一台服务器会执行
    @Scheduled(cron = "0 0 0 * * ?")
    public void dailyReportTask() {
        String lockKey = "task:lock:dailyReport";
        String requestId = UUID.randomUUID().toString();
        
        try {
            // 尝试获取锁,有效期30秒
            Boolean locked = redisTemplate.opsForValue()
                .setIfAbsent(lockKey, requestId, 30, TimeUnit.SECONDS);
            
            if (Boolean.TRUE.equals(locked)) {
                // 获取到锁,执行任务
                log.info("获取到分布式锁,开始执行任务");
                generateDailyReport();
            } else {
                // 没获取到锁,其他服务器正在执行
                log.info("未获取到锁,跳过执行");
            }
        } finally {
            // 释放锁(确保是自己加的锁)
            String currentRequestId = redisTemplate.opsForValue().get(lockKey);
            if (requestId.equals(currentRequestId)) {
                redisTemplate.delete(lockKey);
            }
        }
    }
    
    // 更简单的方案:使用Redisson
    @Autowired
    private RedissonClient redissonClient;
    
    @Scheduled(cron = "0 0 1 * * ?")
    public void monthlyReportTask() {
        RLock lock = redissonClient.getLock("task:lock:monthlyReport");
        
        try {
            // 尝试加锁,最多等待5秒,锁有效期60秒
            if (lock.tryLock(5, 60, TimeUnit.SECONDS)) {
                log.info("获取到分布式锁,开始执行月报任务");
                generateMonthlyReport();
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

4.2 基于数据库的分布式锁

@Component
@Slf4j
public class DatabaseLockTask {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    @Scheduled(cron = "0 0 3 * * ?")
    public void backupTask() {
        String lockName = "backup_task_lock";
        
        try {
            // 尝试获取锁(数据库行锁)
            int affected = jdbcTemplate.update(
                "INSERT INTO task_lock (lock_name, locked_at) VALUES (?, ?) " +
                "ON DUPLICATE KEY UPDATE locked_at = IF(TIMESTAMPDIFF(SECOND, locked_at, NOW()) > 3600, VALUES(locked_at), locked_at)",
                lockName, new Timestamp(System.currentTimeMillis())
            );
            
            if (affected > 0) {
                // 获取到锁
                log.info("获取到数据库锁,开始执行备份任务");
                doBackup();
            } else {
                // 锁还在有效期内,其他服务器已获取
                log.info("锁被占用,跳过执行");
            }
        } catch (Exception e) {
            log.error("获取锁失败", e);
        }
    }
}

五、任务监控与告警

5.1 记录执行日志

@Aspect
@Component
@Slf4j
public class TaskMonitorAspect {
    
    @Autowired
    private TaskLogRepository taskLogRepository;
    
    // 监控所有@Scheduled注解的方法
    @Around("@annotation(org.springframework.scheduling.annotation.Scheduled)")
    public Object monitorTask(ProceedingJoinPoint joinPoint) throws Throwable {
        String taskName = joinPoint.getSignature().getName();
        long startTime = System.currentTimeMillis();
        
        TaskLog taskLog = new TaskLog();
        taskLog.setTaskName(taskName);
        taskLog.setStartTime(new Timestamp(startTime));
        
        try {
            // 执行任务
            Object result = joinPoint.proceed();
            
            long endTime = System.currentTimeMillis();
            taskLog.setStatus("SUCCESS");
            taskLog.setDuration(endTime - startTime);
            taskLog.setEndTime(new Timestamp(endTime));
            
            log.info("任务执行成功:{},耗时:{}ms", taskName, endTime - startTime);
            return result;
            
        } catch (Exception e) {
            long endTime = System.currentTimeMillis();
            taskLog.setStatus("FAILED");
            taskLog.setErrorMessage(e.getMessage());
            taskLog.setDuration(endTime - startTime);
            taskLog.setEndTime(new Timestamp(endTime));
            
            log.error("任务执行失败:{},耗时:{}ms", taskName, endTime - startTime, e);
            throw e;
        } finally {
            // 保存日志
            taskLogRepository.save(taskLog);
            
            // 检查是否超时
            if (taskLog.getDuration() > 300000) {  // 超过5分钟
                sendTimeoutAlert(taskName, taskLog.getDuration());
            }
        }
    }
}

5.2 健康检查

@Component
@Slf4j
public class TaskHealthChecker {
    
    @Autowired
    private TaskLogRepository taskLogRepository;
    
    // 每小时检查一次任务健康状态
    @Scheduled(cron = "0 0 * * * ?")
    public void checkTaskHealth() {
        // 检查最近1小时内的任务执行情况
        LocalDateTime oneHourAgo = LocalDateTime.now().minusHours(1);
        
        List<TaskLog> recentLogs = taskLogRepository.findRecentLogs(oneHourAgo);
        
        Map<String, TaskHealthStats> stats = new HashMap<>();
        
        for (TaskLog log : recentLogs) {
            String taskName = log.getTaskName();
            TaskHealthStats stat = stats.computeIfAbsent(taskName, k -> new TaskHealthStats());
            
            if ("SUCCESS".equals(log.getStatus())) {
                stat.successCount++;
            } else {
                stat.failureCount++;
            }
            stat.totalDuration += log.getDuration();
        }
        
        // 生成健康报告
        generateHealthReport(stats);
        
        // 发送告警
        for (Map.Entry<String, TaskHealthStats> entry : stats.entrySet()) {
            TaskHealthStats stat = entry.getValue();
            
            if (stat.failureCount > 0) {
                sendAlert(entry.getKey(), "任务执行失败", 
                         String.format("失败次数:%d", stat.failureCount));
            }
            
            if (stat.successCount > 0) {
                double avgDuration = stat.totalDuration / (double) stat.successCount;
                if (avgDuration > 60000) {  // 平均执行时间超过1分钟
                    sendAlert(entry.getKey(), "任务执行缓慢", 
                             String.format("平均耗时:%.2fms", avgDuration));
                }
            }
        }
    }
}

5.3 暴露任务端点

# 开启任务监控端点
management:
  endpoints:
    web:
      exposure:
        include: scheduledtasks, health, metrics
@RestController
@RequestMapping("/api/tasks")
public class TaskController {
    
    @Autowired
    private ScheduledTaskRegistrar taskRegistrar;
    
    // 获取所有定时任务
    @GetMapping
    public List<TaskInfo> getTasks() {
        List<TaskInfo> tasks = new ArrayList<>();
        
        Set<ScheduledTask> scheduledTasks = taskRegistrar.getScheduledTasks();
        for (ScheduledTask task : scheduledTasks) {
            TaskInfo info = new TaskInfo();
            info.setTaskName(task.getTask().toString());
            info.setCronExpression(task.getTrigger() instanceof CronTrigger ? 
                ((CronTrigger) task.getTrigger()).getExpression() : null);
            tasks.add(info);
        }
        
        return tasks;
    }
    
    // 手动触发任务
    @PostMapping("/{taskName}/trigger")
    public String triggerTask(@PathVariable String taskName) {
        // 通过反射找到对应方法并执行
        // ...
        return "任务触发成功";
    }
    
    // 暂停任务
    @PostMapping("/{taskName}/pause")
    public String pauseTask(@PathVariable String taskName) {
        // 找到任务并暂停
        // ...
        return "任务暂停成功";
    }
    
    // 恢复任务
    @PostMapping("/{taskName}/resume")
    public String resumeTask(@PathVariable String taskName) {
        // 恢复任务
        // ...
        return "任务恢复成功";
    }
}

六、实战:电商系统定时任务

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

6.1 订单超时取消

@Component
@Slf4j
public class OrderTimeoutTask {
    
    @Autowired
    private OrderService orderService;
    
    // 每分钟检查一次超时订单
    @Scheduled(cron = "0 * * * * ?")
    public void checkTimeoutOrders() {
        log.info("开始检查超时订单");
        
        // 查询30分钟前创建的未支付订单
        LocalDateTime timeoutTime = LocalDateTime.now().minusMinutes(30);
        List<Order> timeoutOrders = orderService.findUnpaidOrdersBefore(timeoutTime);
        
        for (Order order : timeoutOrders) {
            try {
                orderService.cancelOrder(order.getId(), "超时未支付");
                log.info("取消超时订单:{}", order.getId());
            } catch (Exception e) {
                log.error("取消订单失败:{}", order.getId(), e);
            }
        }
        
        log.info("超时订单检查完成,共处理{}个订单", timeoutOrders.size());
    }
}

6.2 每日数据统计

@Component
@Slf4j
public class DailyStatTask {
    
    @Autowired
    private OrderService orderService;
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private StatService statService;
    
    // 每天凌晨1点执行
    @Scheduled(cron = "0 0 1 * * ?")
    public void generateDailyStats() {
        log.info("开始生成每日统计");
        
        LocalDate yesterday = LocalDate.now().minusDays(1);
        
        // 1. 统计订单数据
        OrderStats orderStats = orderService.getDailyOrderStats(yesterday);
        
        // 2. 统计用户数据
        UserStats userStats = userService.getDailyUserStats(yesterday);
        
        // 3. 统计商品数据
        ProductStats productStats = orderService.getDailyProductStats(yesterday);
        
        // 4. 保存统计结果
        DailyStat dailyStat = new DailyStat();
        dailyStat.setStatDate(yesterday);
        dailyStat.setOrderStats(orderStats);
        dailyStat.setUserStats(userStats);
        dailyStat.setProductStats(productStats);
        
        statService.saveDailyStat(dailyStat);
        
        // 5. 发送统计报表
        sendDailyReport(dailyStat);
        
        log.info("每日统计生成完成");
    }
}

6.3 缓存预热

@Component
@Slf4j
public class CacheWarmUpTask {
    
    @Autowired
    private ProductService productService;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    // 每天凌晨4点预热缓存
    @Scheduled(cron = "0 0 4 * * ?")
    public void warmUpCache() {
        log.info("开始缓存预热");
        
        // 1. 预热热门商品
        List<Product> hotProducts = productService.getHotProducts(100);
        for (Product product : hotProducts) {
            String key = "product:" + product.getId();
            redisTemplate.opsForValue().set(key, product, 24, TimeUnit.HOURS);
        }
        
        // 2. 预热商品分类
        List<Category> categories = productService.getAllCategories();
        redisTemplate.opsForValue().set("categories", categories, 24, TimeUnit.HOURS);
        
        // 3. 预热配置信息
        Map<String, String> configs = getSystemConfigs();
        redisTemplate.opsForHash().putAll("system:config", configs);
        
        log.info("缓存预热完成,预热商品:{}个,分类:{}个", 
                 hotProducts.size(), categories.size());
    }
}

七、避坑指南

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

坑1:任务阻塞

// ❌ 错误:任务执行时间太长,阻塞后续任务
@Scheduled(fixedRate = 5000)  // 每5秒执行一次
public void longRunningTask() {
    // 执行10秒
    Thread.sleep(10000);
    // 这个任务还没执行完,下一个任务又开始了
}

// ✅ 正确:使用异步执行
@Async
@Scheduled(fixedRate = 5000)
public void longRunningTask() {
    // 异步执行,不会阻塞
    Thread.sleep(10000);
}

坑2:任务异常处理

// ❌ 错误:不处理异常,任务失败就停止了
@Scheduled(fixedDelay = 5000)
public void taskWithException() {
    // 这里可能抛出异常
    int result = 1 / 0;  // 抛出异常后,这个任务就再也不会执行了
}

// ✅ 正确:捕获异常
@Scheduled(fixedDelay = 5000)
public void taskWithException() {
    try {
        int result = 1 / 0;
    } catch (Exception e) {
        log.error("任务执行异常", e);
        // 记录日志,但任务会继续执行
    }
}

坑3:集群环境时间同步

// ❌ 错误:多台服务器时间不一致
@Scheduled(cron = "0 0 2 * * ?")  // 每天凌晨2点
public void dailyTask() {
    // 服务器A:时间是2:00
    // 服务器B:时间是2:01
    // 任务执行时间不一致
}

// ✅ 正确:使用分布式锁
@Scheduled(cron = "0 0 2 * * ?")
public void dailyTask() {
    if (tryAcquireLock("dailyTask")) {  // 只有一台服务器能获取到锁
        // 执行任务
    }
}

八、今日要点总结

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

  1. @Scheduled注解:最简单的定时任务
  2. cron表达式:强大的时间控制
  3. 动态任务:从数据库读取配置,随时修改
  4. 集群部署:用分布式锁防止重复执行
  5. 任务监控:记录执行日志,健康检查
  6. 异步执行:避免任务阻塞
  7. 电商实战:订单超时、数据统计、缓存预热

九、最佳实践

  1. 任务要幂等:执行多次结果要一样
  2. 任务要短小:单个任务不要做太多事
  3. 任务要容错:异常要捕获,不能影响其他任务
  4. 任务要监控:执行情况要可观测
  5. 任务要隔离:不同业务的任务要分开

十、思考题

场景:你要设计一个秒杀系统的定时任务

  1. 秒杀开始前要预热什么?
  2. 秒杀结束后要统计什么?
  3. 如何保证秒杀开始时间精确到秒?
  4. 集群部署如何防止重复执行?

评论区聊聊你的方案,明天咱们开始新系列!


新系列预告:《微服务7天从入门到精通》明天开始!

今日福利:关注后回复"定时任务",获取完整电商定时任务源码。


零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

公众号运营小贴士:

💡 互动

  1. 你们项目用定时任务多吗?最复杂的任务是什么?
  2. 投票:你们用哪种方式防止重复执行?
  3. 留言分享你的定时任务踩坑经历

🎁 福利

  1. 留言区抽3人送《SpringBoot实战》
  2. 转发截图送定时任务监控系统源码