定时任务开发:@Scheduled 与分布式定时方案

定时任务开发:@Scheduled 与分布式定时方案

凌晨三点数据库自动备份、超时订单自动取消、每小时生成一份销售报表——这些不该是人工半夜爬起来做的事,交给定时任务吧。

一、定时任务的四大应用场景

定时任务在后端开发中无处不在,堪称"后勤部队":

  • 数据同步:每天凌晨从外部 API 拉取数据同步到本地库
  • 报表生成:每周一早 8 点生成上周运营周报并邮件发送
  • 订单超时关闭:30 分钟未支付的订单自动取消,释放库存
  • 库存检查:低库存商品自动生成补货提醒

场景虽多,但核心问题就一个:定时触发 + 逻辑执行。SpringBoot 从轻量到重量,有三级方案。

二、Spring 内置 @Scheduled:最轻量方案

2.1 启用定时任务

@SpringBootApplication@EnableScheduling// 开启定时任务支持publicclassApplication{publicstaticvoidmain(String[]args){SpringApplication.run(Application.class,args);}}

@EnableScheduling是总开关,不加这个,所有 @Scheduled 都不会生效。

2.2 @Scheduled 的四种参数

@ComponentpublicclassDemoTask{@Scheduled(fixedRate=5000)publicvoidfixedRateTask(){System.out.println("fixedRate:"+newDate());// 从任务开始算,每5秒执行一次,不管上次是否执行完}@Scheduled(fixedDelay=5000)publicvoidfixedDelayTask(){System.out.println("fixedDelay:"+newDate());// 从任务结束算,等5秒后再执行}@Scheduled(initialDelay=10000,fixedRate=5000)publicvoidinitialDelayTask(){// 首次延迟10秒启动,之后每5秒执行}@Scheduled(cron="0 0 3 * * ?")publicvoidcronTask(){// 每天凌晨3点执行System.out.println("凌晨3点跑批任务");}}

2.3 Cron 表达式语法详解

cron 表达式有 7 个字段,从左到右是:

位置含义取值范围
10-59
20-59
30-23
4日(月)1-31
51-12
6星期1-7 (1=周日)
7年(可选)1970-2099

特殊字符:*任意值、?不指定(日和星期互斥使用)、,多值、-区间、/步长。

常用 cron 示例

表达式含义
0 0 2 * * ?每天凌晨 2 点
0 0/30 * * * ?每 30 分钟
0 0 9-18 ? * MON-FRI周一至周五 9:00-18:00 每小时执行
0 0 8 ? * 1每周一早 8 点
0 15 10 1 * ?每月 1 号 10:15
0 0/5 * * * ?每 5 分钟

三、单机定时任务的致命问题:多实例重复执行

@Scheduled 写起来是很爽,但生产环境一部署 3 个实例,同一个定时任务执行了 3 遍——订单关闭了 3 次、报表发了 3 份、数据同步了 3 遍。

这就是单机定时任务的天花板:它不知道还有其他实例也在跑

四、分布式定时方案一:数据库唯一索引抢锁

最朴素的方案:建一张schedule_lock表,用一个唯一索引做排他抢锁。

CREATETABLEschedule_lock(task_nameVARCHAR(100)NOTNULL,lock_timeDATETIME,PRIMARYKEY(task_name));
publicvoiddistributedTask(){StringtaskName="closeOrder";try{// 尝试插入抢锁记录,利用主键冲突实现互斥scheduleLockMapper.insert(newScheduleLock(taskName,newDate()));// 抢到锁,执行业务closeTimeoutOrders();// 执行完删除锁记录scheduleLockMapper.deleteById(taskName);}catch(DuplicateKeyExceptione){// 没抢到锁,其他实例正在执行log.info("任务已由其他实例执行,跳过");}}

优点是不引入额外中间件,缺点也很明显:没有过期机制,如果执行实例挂了,锁永远不会释放。

五、分布式定时方案二:Redis 分布式锁(Redisson)

用 Redisson 代替数据库做锁,利用看门狗机制和过期时间解决死锁问题:

@ComponentpublicclassOrderScheduleTask{@AutowiredprivateRedissonClientredissonClient;@Scheduled(cron="0 0/1 * * * ?")// 每分钟执行一次publicvoidcloseTimeoutOrder(){StringlockKey="task:closeOrder";RLocklock=redissonClient.getLock(lockKey);try{// tryLock 只传一个参数:不等待,锁30秒自动释放(看门狗自动续期)if(lock.tryLock(0,30,TimeUnit.SECONDS)){// 查询超时未支付的订单并关闭List<Order>timeoutOrders=orderService.findTimeoutOrders();for(Orderorder:timeoutOrders){orderService.closeOrder(order.getId());}log.info("超时订单处理完成,共{}笔",timeoutOrders.size());}}catch(InterruptedExceptione){Thread.currentThread().interrupt();}finally{if(lock.isHeldByCurrentThread()){lock.unlock();}}}}

tryLock(0, 30, SECONDS)三个参数解读:

  • 参数 1 (0):不等待,抢不到直接返回 false
  • 参数 2 (30):锁过期时间 30 秒
  • 看门狗会在锁未释放时每 10 秒自动续期,业务执行多长都不怕锁提前过期

六、分布式定时方案三:XXL-JOB 简介

当定时任务数量多、需要可视化管理时,自建方案就吃力了。XXL-JOB 是国产开源的分布式任务调度平台,提供:

  • Web 管理界面:可视化创建任务、查看执行日志、手动触发
  • 自带故障转移:执行器宕机自动转移到其他节点
  • 分片广播:支持数据分片并行处理
  • 任务依赖:子任务在父任务成功后自动触发
  • 失败重试:失败邮件告警

架构简单:调度中心(Scheduler)+ 执行器(Executor),中间通过 HTTP 通信。中小企业足够用,不必一上来就搞太重的中间件。

七、定时任务注意事项清单

注意点说明
线程池配置@Scheduled 默认单线程,多任务会排队。需配置线程池:spring.task.scheduling.pool.size=4
异常处理定时方法中务必 try-catch,否则一个异常导致该任务后续不再触发
任务串行fixedRate 不会等上一次结束,小心任务堆积耗尽线程池
幂等性任务失败重试可能导致重复执行,业务逻辑必须有幂等设计
监控告警关键任务必须有执行结果通知(日志+告警),不能"挂了都没人知道"
数据量避免一次查询全部数据,每次处理固定条数,循环推进

定时任务本质是"自动化运维",简单场景 @Scheduled 足够,多实例部署加层 Redis 锁,任务量大起来再上 XXL-JOB。技术选型没有最好,只有最合适的。