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

定时任务开发:@Scheduled 与分布式定时方案 定时任务开发Scheduled 与分布式定时方案凌晨三点数据库自动备份、超时订单自动取消、每小时生成一份销售报表——这些不该是人工半夜爬起来做的事交给定时任务吧。一、定时任务的四大应用场景定时任务在后端开发中无处不在堪称后勤部队数据同步每天凌晨从外部 API 拉取数据同步到本地库报表生成每周一早 8 点生成上周运营周报并邮件发送订单超时关闭30 分钟未支付的订单自动取消释放库存库存检查低库存商品自动生成补货提醒场景虽多但核心问题就一个定时触发 逻辑执行。SpringBoot 从轻量到重量有三级方案。二、Spring 内置 Scheduled最轻量方案2.1 启用定时任务SpringBootApplicationEnableScheduling// 开启定时任务支持publicclassApplication{publicstaticvoidmain(String[]args){SpringApplication.run(Application.class,args);}}EnableScheduling是总开关不加这个所有 Scheduled 都不会生效。2.2 Scheduled 的四种参数ComponentpublicclassDemoTask{Scheduled(fixedRate5000)publicvoidfixedRateTask(){System.out.println(fixedRatenewDate());// 从任务开始算每5秒执行一次不管上次是否执行完}Scheduled(fixedDelay5000)publicvoidfixedDelayTask(){System.out.println(fixedDelaynewDate());// 从任务结束算等5秒后再执行}Scheduled(initialDelay10000,fixedRate5000)publicvoidinitialDelayTask(){// 首次延迟10秒启动之后每5秒执行}Scheduled(cron0 0 3 * * ?)publicvoidcronTask(){// 每天凌晨3点执行System.out.println(凌晨3点跑批任务);}}2.3 Cron 表达式语法详解cron 表达式有 7 个字段从左到右是位置含义取值范围1秒0-592分0-593时0-234日(月)1-315月1-126星期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:150 0/5 * * * ?每 5 分钟三、单机定时任务的致命问题多实例重复执行Scheduled 写起来是很爽但生产环境一部署 3 个实例同一个定时任务执行了 3 遍——订单关闭了 3 次、报表发了 3 份、数据同步了 3 遍。这就是单机定时任务的天花板它不知道还有其他实例也在跑。四、分布式定时方案一数据库唯一索引抢锁最朴素的方案建一张schedule_lock表用一个唯一索引做排他抢锁。CREATETABLEschedule_lock(task_nameVARCHAR(100)NOTNULL,lock_timeDATETIME,PRIMARYKEY(task_name));publicvoiddistributedTask(){StringtaskNamecloseOrder;try{// 尝试插入抢锁记录利用主键冲突实现互斥scheduleLockMapper.insert(newScheduleLock(taskName,newDate()));// 抢到锁执行业务closeTimeoutOrders();// 执行完删除锁记录scheduleLockMapper.deleteById(taskName);}catch(DuplicateKeyExceptione){// 没抢到锁其他实例正在执行log.info(任务已由其他实例执行跳过);}}优点是不引入额外中间件缺点也很明显没有过期机制如果执行实例挂了锁永远不会释放。五、分布式定时方案二Redis 分布式锁Redisson用 Redisson 代替数据库做锁利用看门狗机制和过期时间解决死锁问题ComponentpublicclassOrderScheduleTask{AutowiredprivateRedissonClientredissonClient;Scheduled(cron0 0/1 * * * ?)// 每分钟执行一次publicvoidcloseTimeoutOrder(){StringlockKeytask:closeOrder;RLocklockredissonClient.getLock(lockKey);try{// tryLock 只传一个参数不等待锁30秒自动释放看门狗自动续期if(lock.tryLock(0,30,TimeUnit.SECONDS)){// 查询超时未支付的订单并关闭ListOrdertimeoutOrdersorderService.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.size4异常处理定时方法中务必 try-catch否则一个异常导致该任务后续不再触发任务串行fixedRate 不会等上一次结束小心任务堆积耗尽线程池幂等性任务失败重试可能导致重复执行业务逻辑必须有幂等设计监控告警关键任务必须有执行结果通知日志告警不能挂了都没人知道数据量避免一次查询全部数据每次处理固定条数循环推进定时任务本质是自动化运维简单场景 Scheduled 足够多实例部署加层 Redis 锁任务量大起来再上 XXL-JOB。技术选型没有最好只有最合适的。