《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, 0和7都是周日)
│ │ │ │ └──── 月 (1-12)
│ │ │ └─────── 日 (1-31)
│ │ └────────── 时 (0-23)
│ └───────────── 分 (0-59)
└──────────────── 秒 (0-59)
常用表达式:
0 0 2 * * ? 每天凌晨2点
0 0 12 * * ? 每天中午12点
0 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 ,获取本文所有示例代码、配置模板及导出工具。
- @Scheduled注解:最简单的定时任务
- cron表达式:强大的时间控制
- 动态任务:从数据库读取配置,随时修改
- 集群部署:用分布式锁防止重复执行
- 任务监控:记录执行日志,健康检查
- 异步执行:避免任务阻塞
- 电商实战:订单超时、数据统计、缓存预热
九、最佳实践
- 任务要幂等:执行多次结果要一样
- 任务要短小:单个任务不要做太多事
- 任务要容错:异常要捕获,不能影响其他任务
- 任务要监控:执行情况要可观测
- 任务要隔离:不同业务的任务要分开
十、思考题
场景:你要设计一个秒杀系统的定时任务
- 秒杀开始前要预热什么?
- 秒杀结束后要统计什么?
- 如何保证秒杀开始时间精确到秒?
- 集群部署如何防止重复执行?
评论区聊聊你的方案,明天咱们开始新系列!
新系列预告:《微服务7天从入门到精通》明天开始!
今日福利:关注后回复"定时任务",获取完整电商定时任务源码。
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目
资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。
公众号运营小贴士:
💡 互动:
- 你们项目用定时任务多吗?最复杂的任务是什么?
- 投票:你们用哪种方式防止重复执行?
- 留言分享你的定时任务踩坑经历
🎁 福利:
- 留言区抽3人送《SpringBoot实战》
- 转发截图送定时任务监控系统源码