尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Spring Boot定时任务全解析:从@Scheduled到Quartz的实战选型指南

Spring Boot定时任务全解析:从@Scheduled到Quartz的实战选型指南 1. 项目概述为什么定时任务是后端开发的“标配”在任何一个稍具规模的后端应用里定时任务几乎都是绕不开的基础设施。无论是每天凌晨的报表统计、每五分钟的数据同步还是每小时的缓存刷新这些不需要用户即时触发但需要系统在特定时间点自动执行的任务就是定时任务。在 Spring Boot 生态中实现定时任务的门槛被降到了极低但随之而来的问题是选择太多反而让人困惑。到底该用哪种方式是追求极简的Scheduled注解还是需要精细控制的ThreadPoolTaskScheduler亦或是功能强大的 Quartz 框架我见过不少项目初期为了图省事直接上Scheduled结果随着业务增长任务越来越多线程池被打满任务相互阻塞日志混乱不堪排查问题如同大海捞针。也见过为了“技术先进”而引入重型框架 Quartz结果 80% 的功能用不上反而增加了系统的复杂度和维护成本。今天我就结合自己踩过的坑和项目实战经验把这三种主流方式的原理、适用场景、配置细节和避坑指南给你一次讲透。无论你是刚接触 Spring Boot 的新手还是正在为现有项目定时任务架构选型的老手这篇文章都能给你提供可直接落地的参考。2. 方案选型三种方式的本质区别与核心考量在动手写代码之前我们必须搞清楚这三种方式到底有什么区别以及背后各自的设计哲学。这决定了你的选择不是盲目的而是基于清晰的技术判断。2.1 Scheduled 注解轻量级单机任务的“瑞士军刀”Scheduled是 Spring 框架自带的最简单的定时任务支持。它的核心思想是“约定大于配置”。你只需要在一个 Bean 的方法上加上Scheduled注解并配置好 Cron 表达式或固定延迟/频率Spring 在应用启动后就会自动在后台创建一个线程池来调度这些方法。它的本质是一个基于 Spring 容器生命周期和内置TaskScheduler的轻量级调度器。它管理着一个默认的线程池ThreadPoolTaskScheduler所有被Scheduled标记的任务都会被提交到这个池子里排队执行。核心考量与适用场景优点使用极其简单零额外依赖与 Spring 生态无缝集成。适合执行时间短、逻辑简单、并发度不高的后台作业。缺点单机性任务定义在代码中多实例部署时会每个实例都执行导致任务重复。除非你通过分布式锁或数据库标志位等额外手段解决。调度能力弱不支持动态增删改任务任务配置如Cron表达式硬编码在注解中修改需要重启应用。监控与管理缺失没有内置的 UI 控制台任务执行日志、成功失败状态、历史记录等都需要自己打日志实现。结论适用于开发测试环境、小型项目、或那些即便重复执行也无严重后果的清理、通知类任务。2.2 ThreadPoolTaskScheduler需要手动控制的“编程式”调度ThreadPoolTaskScheduler是 Spring 提供的、用于编程式创建和管理定时任务的类。它实际上就是Scheduled注解背后默认使用的那个调度器。当你直接使用它时就相当于跳过了注解的“魔法”直接通过 API 进行控制。它的本质是一个更底层的、可编程的调度器接口实现。它允许你以代码的方式动态创建任务ScheduledFuture并允许你对线程池参数核心线程数、队列容量等进行更精细的配置。核心考量与适用场景优点动态性可以在运行时动态地添加、取消和修改定时任务。比如根据配置中心的开关动态启停某个数据同步任务。可控性可以显式地配置任务线程池避免所有Scheduled任务共享默认线程池可能带来的资源竞争问题。你可以为不同类型的任务创建不同的ThreadPoolTaskScheduler实例。缺点相比Scheduled更复杂需要自己编写任务调度和生命周期管理的代码。同样不具备分布式调度能力。结论适用于任务需要根据运行时条件动态调整或者需要对任务执行线程池进行隔离和精细化管理的场景。它是从“注解声明式”到“框架分布式”之间的一个灵活过渡方案。2.3 Quartz 集成企业级分布式调度的“重型武器”Quartz 是一个功能完整、历史悠久的开源作业调度框架。Spring Boot 通过spring-boot-starter-quartz可以很方便地集成它。它的本质是一个独立、强大、支持持久化和集群的调度系统。它将“任务Job”、“触发器Trigger”和“调度器Scheduler”解耦并将调度信息如 JobDetail, Trigger持久化到数据库如 MySQL。核心考量与适用场景优点分布式与高可用通过数据库持久化多个应用实例可以共享同一套任务定义和调度状态。当一个实例宕机时任务会被其他实例接管避免单点故障。强大的调度能力支持复杂的 Cron 表达式、日历排除、错过触发策略Misfire Instruction等。任务持久化任务和触发器信息存储在数据库应用重启后任务状态不会丢失。管理性有官方和第三方的管理界面如 Quartz 自带简陋 UI或使用如quartz-manager等开源项目可以可视化地管理任务。缺点架构最重依赖多配置复杂。需要引入额外数据库表增加了系统复杂度。对于简单场景属于“杀鸡用牛刀”。结论适用于中大型分布式系统对任务的可靠性、可维护性、动态管理有较高要求的场景。是生产环境复杂定时任务架构的标配选择。选择心法简单任务用注解动态控制编程式生产集群上 Quartz。不要盲目追求技术复杂度适合当前业务规模和未来半年到一年预期的就是最好的。3. 核心细节解析与实操要点理解了宏观选型我们深入到每种方式的具体实现细节和那些文档里不会写的“坑”。3.1 Scheduled 注解的“魔鬼细节”你以为加个注解就完事了这里面的门道可不少。1. 线程池的“秘密”与配置默认情况下所有Scheduled任务共享一个ThreadPoolTaskScheduler其默认核心线程数是1。这意味着如果你的任务执行时间较长或者有多个任务在同一时间点触发它们会排队串行执行可能导致任务延迟。Configuration EnableScheduling public class SchedulerConfig implements SchedulingConfigurer { Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { ThreadPoolTaskScheduler taskScheduler new ThreadPoolTaskScheduler(); // 设置核心线程数根据任务数量调整建议大于可能并发执行的任务数 taskScheduler.setPoolSize(10); taskScheduler.setThreadNamePrefix(my-scheduled-task-pool-); taskScheduler.initialize(); taskRegistrar.setTaskScheduler(taskScheduler); } }实操要点务必根据你的任务数量和预期并发度调整poolSize。我一般设置为(任务数量 * 1.5)并监控线程池活跃线程数。2. Cron 表达式与固定延迟/速率的抉择cron: 使用 Unix Cron 表达式如“0 0 2 * * ?”表示每天凌晨2点执行。它关注的是“时刻”。fixedDelay: 在上一次任务执行结束后间隔指定时间再次执行。Scheduled(fixedDelay 5000)表示任务结束后等5秒再执行下一次。它保证了执行间隔但不确定开始时间。fixedRate: 在上一次任务开始执行后间隔指定时间再次执行。Scheduled(fixedRate 5000)表示无论上次任务是否执行完每5秒尝试开始一次。它关注执行频率但可能产生任务堆积。踩坑记录曾经用一个fixedRate 60000每分钟一次的任务去处理一批数据但某次数据量暴增处理用了70秒。由于fixedRate不等待上次完成导致任务重叠数据库锁竞争最终雪崩。对于执行时间不固定的任务强烈建议使用fixedDelay。3. 单机重复执行的“土办法”如果你的应用是多实例部署又暂时不想上 Quartz一个常见的防重复执行策略是“数据库标志位法”在任务开始时尝试更新一个代表“任务正在执行”的状态字段比如update task_lock set ownerinstance_id where task_nameXXX and owner is null。如果更新成功影响行数0说明抢到了锁执行任务逻辑。任务执行完毕后释放锁将 owner 设为 null。如果更新失败说明其他实例正在执行本实例直接跳过。这个方法简单但要注意锁的超时和清理避免死锁。3.2 ThreadPoolTaskScheduler 的动态控制艺术使用编程式调度核心是ThreadPoolTaskScheduler的schedule方法族。它返回一个ScheduledFuture对象这是你控制任务的“手柄”。Service public class DynamicTaskService { Autowired private ThreadPoolTaskScheduler taskScheduler; private ConcurrentHashMapString, ScheduledFuture? taskMap new ConcurrentHashMap(); /** * 动态添加一个Cron任务 */ public void addCronTask(String taskId, Runnable runnable, String cronExpression) { ScheduledFuture? future taskScheduler.schedule(runnable, new CronTrigger(cronExpression)); taskMap.put(taskId, future); log.info(动态任务 {} 已添加Cron表达式: {}, taskId, cronExpression); } /** * 取消一个任务 */ public void cancelTask(String taskId) { ScheduledFuture? future taskMap.get(taskId); if (future ! null !future.isCancelled()) { // true 表示尝试中断正在执行的任务 boolean result future.cancel(true); taskMap.remove(taskId); log.info(取消任务 {} 结果: {}, taskId, result); } } /** * 修改任务先取消再重新添加 */ public void updateTask(String taskId, Runnable runnable, String newCronExpression) { cancelTask(taskId); addCronTask(taskId, runnable, newCronExpression); } }实操要点任务标识务必用一个唯一标识如taskId来管理你的ScheduledFuture通常放在一个ConcurrentHashMap中。取消策略future.cancel(true)中的true参数表示尝试中断线程。如果你的任务逻辑没有正确处理中断Thread.interrupt()可能无法立即停止。对于需要优雅停止的任务需要在Runnable内检查Thread.currentThread().isInterrupted()。内存泄漏如果任务被取消或应用关闭记得从Map中移除对应的ScheduledFuture引用避免内存泄漏。可以在PreDestroy方法中遍历Map并取消所有任务。3.3 Quartz 集成的核心JobDataMap 与 Misfire 策略Quartz 的核心是Job任务内容、JobDetail任务定义、Trigger触发器和Scheduler调度器。1. JobDataMap如何向 Job 传递参数这是新手最容易困惑的地方。你不能通过构造函数或字段注入的方式直接给Job实例传参因为Job实例是由 Quartz 框架每次执行时实例化的。正确的方式是使用JobDataMap。// 定义Job public class MyDataSyncJob implements Job { Override public void execute(JobExecutionContext context) { JobDataMap dataMap context.getJobDetail().getJobDataMap(); String syncTarget dataMap.getString(syncTarget); int batchSize dataMap.getInt(batchSize); // 使用参数执行任务... } } // 创建并调度Job Service public class QuartzManagerService { Autowired private Scheduler scheduler; public void scheduleDataSyncJob(String jobName, String syncTarget, int batchSize) throws SchedulerException { JobDetail jobDetail JobBuilder.newJob(MyDataSyncJob.class) .withIdentity(jobName, dataSyncGroup) .usingJobData(syncTarget, syncTarget) // 传递参数 .usingJobData(batchSize, batchSize) .build(); Trigger trigger TriggerBuilder.newTrigger() .withIdentity(jobName Trigger, dataSyncGroup) .withSchedule(CronScheduleBuilder.cronSchedule(0 0/30 * * * ?)) // 每30分钟 .build(); scheduler.scheduleJob(jobDetail, trigger); } }2. Misfire 策略任务错过触发后怎么办这是 Quartz 生产环境必须配置的所谓 Misfire就是指触发器该触发的时候调度器因为某种原因如应用关闭、线程池满、任务排队等没有触发。Quartz 提供了丰富的策略withMisfireHandlingInstructionIgnoreMisfires()忽略错过立即补执行一次然后按原计划继续。withMisfireHandlingInstructionFireAndProceed()CronTrigger默认立即触发一次然后按当前时间计算下一次触发时间。这可能导致触发节奏变化。withMisfireHandlingInstructionDoNothing()什么都不做等待下一次触发。这是最常用的策略对于不允许补执行的财务对账等任务尤其重要。Trigger trigger TriggerBuilder.newTrigger() .withIdentity(myTrigger) .withSchedule(CronScheduleBuilder.cronSchedule(0 0 2 * * ?) .withMisfireHandlingInstructionDoNothing()) // 明确设置Misfire策略 .build();3. 持久化与集群配置在application.yml中开启持久化到数据库如 MySQLspring: quartz: job-store-type: jdbc # 使用JDBC存储 jdbc: initialize-schema: always # 首次启动自动建表生产环境用never手动执行SQL properties: org.quartz.jobStore.isClustered: true # 开启集群模式 org.quartz.jobStore.clusterCheckinInterval: 20000 # 集群节点检入间隔(ms) org.quartz.scheduler.instanceId: AUTO # 实例ID自动生成注意集群模式下各个节点的系统时间必须同步使用 NTP 服务否则调度会混乱。4. 实操过程与核心环节实现我们以一个模拟的“订单状态自动关闭”任务为例分别用三种方式实现。4.1 基于 Scheduled 的简单实现Service Slf4j public class OrderAutoCloseService { Scheduled(cron 0 */5 * * * ?) // 每5分钟执行一次 public void closeExpiredOrders() { log.info(开始执行订单自动关闭任务...); try { // 1. 查询超时未支付的订单例如创建时间超过30分钟 ListOrder expiredOrders orderRepository.findByStatusAndCreateTimeBefore( OrderStatus.PENDING_PAYMENT, LocalDateTime.now().minusMinutes(30)); if (expiredOrders.isEmpty()) { log.info(暂无超时订单。); return; } // 2. 批量更新订单状态为“已关闭” for (Order order : expiredOrders) { order.setStatus(OrderStatus.CLOSED); order.setCloseTime(LocalDateTime.now()); order.setCloseReason(超时未支付); // 可以在这里发送通知等... } orderRepository.saveAll(expiredOrders); log.info(成功关闭 {} 笔超时订单。, expiredOrders.size()); } catch (Exception e) { // 3. 必须捕获异常否则会导致调度线程终止后续所有定时任务失效 log.error(订单自动关闭任务执行失败, e); } } }关键实现点异常捕获这是Scheduled任务的铁律。任务方法内必须用try-catch捕获所有异常防止单个任务失败导致整个调度线程池中的线程异常退出使得其他定时任务也停止执行。日志记录清晰的开始、结束、关键步骤日志是后期排查问题的生命线。批量操作涉及数据库操作时尽量使用批量查询和更新避免在循环中频繁操作数据库。4.2 基于 ThreadPoolTaskScheduler 的动态实现假设我们需要根据运营配置动态调整检查订单超时的时间间隔比如大促期间改为每分钟检查。Configuration public class DynamicTaskConfig { Bean public ThreadPoolTaskScheduler taskScheduler() { ThreadPoolTaskScheduler scheduler new ThreadPoolTaskScheduler(); scheduler.setPoolSize(5); scheduler.setThreadNamePrefix(dynamic-order-task-); scheduler.setWaitForTasksToCompleteOnShutdown(true); // 应用关闭时等待任务完成 scheduler.setAwaitTerminationSeconds(60); // 等待最多60秒 return scheduler; } } Service Slf4j public class DynamicOrderCloseService { Autowired private ThreadPoolTaskScheduler taskScheduler; Autowired private OrderRepository orderRepository; private ScheduledFuture? future; /** * 启动或重新配置任务 * param cronExpression 从配置中心或数据库读取 */ public void startOrUpdateOrderCloseTask(String cronExpression) { // 如果已有任务在运行先取消 if (future ! null !future.isCancelled()) { future.cancel(true); // 尝试中断当前执行的任务 log.info(已取消旧的订单关闭任务。); } Runnable task () - { log.info(动态任务执行关闭超时订单...); // ... 任务逻辑同上此处省略 ... }; // 使用新的Cron表达式调度任务 future taskScheduler.schedule(task, new CronTrigger(cronExpression)); log.info(已启动新的订单关闭任务Cron表达式: {}, cronExpression); } PreDestroy public void destroy() { if (future ! null) { future.cancel(true); } } }关键实现点优雅关闭配置setWaitForTasksToCompleteOnShutdown(true)和setAwaitTerminationSeconds确保应用关闭时正在运行的任务有机会完成而不是被强制杀死。任务更新逻辑更新取消旧任务创建新任务。确保旧任务的ScheduledFuture被正确取消和释放。4.3 基于 Quartz 的持久化集群实现首先定义 Quartz 的 Job 类。注意Job 类需要是一个无状态的 Bean因为每次执行都会创建新实例。Component DisallowConcurrentExecution // 关键注解禁止同一JobDetail并发执行 public class QuartzOrderCloseJob implements Job { private static final Logger log LoggerFactory.getLogger(QuartzOrderCloseJob.class); // 注意这里不能直接Autowired因为Job实例不是Spring管理的。 // 需要通过SchedulerContext或JobFactory注入。 // 我们使用Spring Boot的Quartz集成它会自动将Spring管理的Bean注入到Job中。 // 需要配置 spring.quartz.properties.org.quartz.scheduler.jobFactory.class: org.springframework.scheduling.quartz.SpringBeanJobFactory // 或者使用更现代的 AutowiringSpringBeanJobFactory Override public void execute(JobExecutionContext context) { // 可以通过context.getMergedJobDataMap()获取参数 String jobGroup context.getJobDetail().getKey().getGroup(); log.info(Quartz Job [{}] 开始执行., context.getJobDetail().getKey()); // 如何获取Spring Bean一种方式是从SchedulerContext中获取 SchedulerContext schedulerContext; try { schedulerContext context.getScheduler().getContext(); OrderRepository orderRepository (OrderRepository) schedulerContext.get(orderRepository); // 使用orderRepository执行业务逻辑... ListOrder expiredOrders orderRepository.findByStatusAndCreateTimeBefore( OrderStatus.PENDING_PAYMENT, LocalDateTime.now().minusMinutes(30)); // ... 处理逻辑 log.info(Quartz Job [{}] 执行完毕处理{}条订单。, context.getJobDetail().getKey(), expiredOrders.size()); } catch (SchedulerException e) { log.error(从SchedulerContext获取Bean失败, e); // Quartz Job 抛出JobExecutionException调度器会处理如触发Misfire throw new JobExecutionException(e); } } }然后需要一个服务来管理 Job 的注册。这里简化处理在应用启动后初始化一个任务。Service public class QuartzOrderJobInitializer { Autowired private Scheduler scheduler; PostConstruct public void initOrderCloseJob() throws SchedulerException { JobDetail jobDetail JobBuilder.newJob(QuartzOrderCloseJob.class) .withIdentity(orderAutoCloseJob, orderGroup) .storeDurably() // 即使没有Trigger关联也保留JobDetail .build(); // 创建一个每天凌晨2点执行的触发器并设置Misfire策略为“忽略” Trigger trigger TriggerBuilder.newTrigger() .forJob(jobDetail) .withIdentity(orderAutoCloseTrigger, orderGroup) .withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(2, 0) .withMisfireHandlingInstructionDoNothing()) .build(); // 如果Job不存在则调度如果已存在则更新其触发器 if (!scheduler.checkExists(jobDetail.getKey())) { scheduler.scheduleJob(jobDetail, trigger); } else { // 重新调度更新触发器 scheduler.rescheduleJob(trigger.getKey(), trigger); } } }关键实现点DisallowConcurrentExecution这是 Quartz Job 上非常重要的一个注解。它保证对于同一个JobDetail由 name 和 group 确定即使上一次执行还没结束到了下一次触发时间也不会并发执行新的实例。这对于需要避免资源竞争的任务至关重要。Job 中获取 Spring Bean由于 Quartz 自己实例化 Job所以不能直接用Autowired。Spring Boot 的QuartzAutoConfiguration默认使用了AutowiringSpringBeanJobFactory它支持在 Job 实例化后注入 Spring Bean。但更传统和清晰的做法是通过SchedulerContext来传递 Bean 引用如上例或者在自定义的JobFactory中进行注入。storeDurably()将 JobDetail 设置为持久化即使暂时没有触发器与之关联它也会保存在数据库中。这方便你动态地为其添加或更换触发器。5. 常见问题与排查技巧实录在实际开发和运维中定时任务总会遇到各种稀奇古怪的问题。下面是我总结的一些高频问题和排查思路。5.1 任务不执行了从这几点开始查检查 Spring 容器是否已启用调度确保启动类或配置类上有EnableScheduling注解对于Scheduled。对于 Quartz检查spring-boot-starter-quartz依赖是否引入配置是否正确。检查 Cron 表达式这是最常见的问题。使用在线 Cron 表达式验证工具如 cron.qqe2.com检查你的表达式是否正确特别是月份和周几的字段注意 Quartz 的周几 1-7 对应周日-周六而 Linux Cron 是 0-6 对应周日-周六。Spring 的Scheduled(cron””)支持 6位秒 分 时 日 月 周几或7位加上年表达式Quartz 也是。检查线程池是否已满/任务是否被阻塞对于Scheduled如果任务执行时间过长且默认单线程池会导致其他任务排队。查看日志中是否有任务开始的记录但很久没有结束记录。使用jstack pid命令或 Arthas 等工具查看调度线程池如scheduling-1的线程状态是否处于WAITING或BLOCKED。检查是否有未捕获的异常回顾4.1节Scheduled方法内未捕获的异常会导致执行线程退出。务必检查任务方法是否有try-catch并查看应用错误日志。对于 Quartz检查数据库连接和表确认 Quartz 的 JDBC 连接配置正确且qrtz_系列表已成功创建。查看qrtz_triggers表中对应触发器的STATE字段。WAITING表示正常等待触发PAUSED表示暂停ERROR表示错误。如果状态异常需要排查日志或手动恢复。5.2 任务重复执行了多实例部署场景现象可能原因解决方案每个应用实例的日志都显示执行了任务Scheduled或编程式调度在多实例下自然重复1.上策改用 Quartz 等支持集群的调度框架。2.中策使用分布式锁Redis/ZooKeeper任务开始前抢锁。3.下策数据库乐观锁或状态标志位见3.1节。Quartz 集群下同一个任务在短时间内被执行了多次1. 集群节点时间不同步。2.org.quartz.jobStore.clusterCheckinInterval设置过长节点失联后未及时被其他节点发现。1. 为所有服务器配置NTP 时间同步服务。2. 适当调小clusterCheckinInterval默认15000ms并确保网络通畅。5.3 任务执行时间漂移或越来越慢固定速率fixedRate的陷阱如果任务执行时间超过周期会导致任务堆积。比如fixedRate 5s但任务要跑8秒那么第二次会在第5秒尝试启动但第一次还没完实际启动会被延迟长期下来节奏全乱。改用 fixedDelay。数据库或外部依赖变慢任务逻辑中如果有数据库查询、API 调用这些外部系统的性能下降会直接导致任务变慢。需要为任务关键步骤添加耗时日志定位瓶颈。Full GC 导致应用暂停观察应用监控的 GC 情况如果定时任务执行期间发生了长时间的 Full GC整个应用都会暂停。需要优化 JVM 参数和代码减少大对象创建。5.4 Quartz 表锁与性能问题在集群模式下Quartz 使用数据库的行锁来实现集群协调。当任务非常多或触发非常频繁时可能会对数据库特别是qrtz_locks表造成压力。优化建议减少不必要的锁默认情况下Quartz 会获取TRIGGER_ACCESS锁。如果业务允许可以考虑将不重要的任务设置为DisallowConcurrentExecution以避免状态锁竞争。调整获取锁的间隔org.quartz.scheduler.idleWaitTime默认30秒可以适当调小但会增加数据库查询次数需权衡。数据库优化为 Quartz 表建立合适的索引特别是qrtz_triggers表的next_fire_time和trigger_state字段。考虑分库分表对于超大规模调度可以考虑对 Quartz 表进行分库分表或者使用官方不推荐的Terracotta集群模式无需数据库。5.5 监控与告警让定时任务“可观测”定时任务在后台默默运行没有监控就等于“盲人摸象”。关键指标监控执行次数每个任务的成功、失败次数。执行耗时每次任务的执行时间可以统计 P50, P95, P99 分位值。是否存活任务是否在预期的时间点被触发。实现方式手动打点在每个任务开始、结束、异常时通过日志或 Micrometer 等指标库记录信息。AOP 切面定义一个针对Scheduled注解或Job.execute方法的切面统一收集执行指标并发送到监控系统如 Prometheus。健康检查对于核心任务可以暴露一个 HTTP 端点检查任务最近一次成功执行的时间。如果超过阈值则健康检查失败并触发告警。Component Aspect Slf4j public class ScheduledTaskMonitorAspect { private final MeterRegistry meterRegistry; public ScheduledTaskMonitorAspect(MeterRegistry meterRegistry) { this.meterRegistry meterRegistry; } Around(annotation(org.springframework.scheduling.annotation.Scheduled)) public Object monitorScheduledTask(ProceedingJoinPoint pjp) throws Throwable { String taskName pjp.getSignature().toShortString(); Timer.Sample sample Timer.start(meterRegistry); Counter.builder(scheduled.task.execution) .tag(name, taskName) .tag(status, started) .register(meterRegistry).increment(); try { Object result pjp.proceed(); sample.stop(Timer.builder(scheduled.task.duration) .tag(name, taskName) .tag(outcome, success) .register(meterRegistry)); Counter.builder(scheduled.task.execution) .tag(name, taskName) .tag(status, success) .register(meterRegistry).increment(); return result; } catch (Exception e) { sample.stop(Timer.builder(scheduled.task.duration) .tag(name, taskName) .tag(outcome, failure) .register(meterRegistry)); Counter.builder(scheduled.task.execution) .tag(name, taskName) .tag(status, failure) .register(meterRegistry).increment(); log.error(Scheduled task {} execution failed, taskName, e); throw e; } } }定时任务虽小却是系统稳定性的基石。从简单的Scheduled到强大的 Quartz每一种选择都对应着不同的场景和代价。我的经验是在项目早期用最简单的方式快速验证业务逻辑同时在心里为未来可能的重构留好位置。当任务数量增多、可靠性要求提高时平滑地过渡到更健壮的架构。记住没有最好的方案只有最适合你当前和可预见未来业务场景的方案。在每次技术选型时多问一句“如果业务量翻十倍这个方案还撑得住吗”能帮你避开很多后期的大坑。
返回列表