定时任务从入门到防坑,cron 表达式看这篇就够

家人们谁懂啊!以前为了跑个凌晨 3 点的数据分析脚本,定个闹钟从被窝里爬起来开电脑,现在想想简直是大冤种行为 ------ 直到我把定时任务玩明白,才算真正实现了 "到点自动干活" 的摸鱼自由!今天就把定时任务那点事儿掰开揉碎了讲,从基础概念到实战防坑,小白也能轻松拿捏~

一、定时任务:后端系统的 "自动打工人" 有多香?

先别急着学技术,咱先搞懂 "定时任务" 到底是个啥 ------ 它就是后端系统里不用你催、到点就干活的 "自动打工人",专门处理那些 "重复且有时间规律" 的活儿:

  • 电商场景:订单支付超时 15 分钟自动取消(总不能让运营小姐姐盯着表手动关吧);
  • 办公场景:每天早上 8 点自动发前一天的业务日报(打工人何苦为难打工人);
  • 运维场景:每周日凌晨 2 点自动备份数据库(总不能让运维小哥熬夜守着吧)。

简单说,只要你能明确 "啥时候干、干啥",定时任务就能替你把活儿扛了,从此告别闹钟跑脚本的苦日子!

二、定时任务调度技术:选对工具少加班!

想让 "自动打工人" 听话,得先选个靠谱的 "调度工具"。后端圈常用的就这仨,各有各的脾气,按需 Pick 就行:

调度技术 性格特点 适用场景 一句话总结
Spring Task 轻量级 "小萌新" 单体项目、简单定时需求 不用额外引包,Spring 自带,够用就上
Quartz 全能 "老大哥" 复杂定时(如动态调整任务) 功能强到离谱,但配置略麻烦
XXL-Job 分布式 "社交牛" 微服务项目、多机器调度 支持可视化管理,还能查执行日志

我日常写 demo 或小项目,直接用 Spring Task;要是公司项目涉及分布式,XXL-Job 直接冲 ------ 毕竟谁不想在可视化界面上点两下就配置好任务呢,比写一堆 XML 香多了~

三、cron 表达式:定时任务的 "作息时间表" 怎么写?

不管用啥调度工具,都绕不开一个核心 ------cron 表达式,这玩意儿就是给 "自动打工人" 画的 "作息时间表",格式是「秒 分 时 日 月 周 年(可选)」。

很多人一看到这串字符就头大,比如 0 0 2 * * ? ,其实拆开来超简单,咱用 "每天凌晨 2 点执行" 举个例子:

位置 含义 取值范围 例子中的值 解释
1 0-59 0 第 0 秒开始(避免每秒执行)
2 0-59 0 第 0 分开始(整分执行)
3 0-23 2 凌晨 2 点
4 1-31 * 每天都执行
5 1-12 * 每月都执行
6 1-7(1 = 周日) ? 忽略周(日和周不能同时指定,否则会 "打架")

避坑小技巧:

  1. 别手搓 cron!推荐用 cron 在线生成工具 ,选时间点自动生成,避免写错;
  1. 周和日别同时设具体值!比如写 0 0 2 1 ?(每月 1 号凌晨 2 点)没问题,但写 0 0 2 1 1 ?(1 月 1 号且周日凌晨 2 点)就可能出 bug,除非你明确要这两个条件同时满足;
  1. 秒位别写*!除非你想让任务每秒执行一次(大部分场景用不上,还会搞崩系统),一般写0就行。

再给几个常用案例,直接抄作业:

  • 每小时执行:0 0 * * * ?
  • 每天中午 12 点执行:0 0 12 * * ?
  • 每周一凌晨 3 点执行:0 0 3 ? * 2(记住 1 是周日,2 是周一!)

四、实战:Spring Boot 下准备定时任务类,3 步搞定!

光说不练假把式,咱用最常用的 Spring Task 举个例子,3 步就能让定时任务跑起来:

第一步:加注解开启定时任务

在 Spring Boot 启动类上加个 @EnableScheduling ,相当于告诉 Spring:"我要启用定时任务啦!"

less 复制代码
@SpringBootApplication
@EnableScheduling // 开启定时任务功能
public class TimedTaskApplication {
    public static void main(String[] args) {
        SpringApplication.run(TimedTaskApplication.class, args);
    }
}

第二步:写定时任务类

新建一个任务类,用 @Component 交给 Spring 管理,再用 @Scheduled(cron="xxx") 指定 "作息时间表":

csharp 复制代码
@Component
public class OrderTimedTask {
    // 每天凌晨2点执行:取消超时未支付的订单
    @Scheduled(cron = "0 0 2 * * ?")
    public void cancelTimeoutOrder() {
        System.out.println("开始执行超时订单取消任务...");
        // 这里写具体逻辑:查数据库里超时未支付的订单,批量取消
        // orderMapper.cancelTimeoutOrders();
        System.out.println("超时订单取消完成!");
    }
    // 每5分钟执行一次:同步订单状态到统计系统
    @Scheduled(cron = "0 */5 * * * ?")
    public void syncOrderStatus() {
        System.out.println("同步订单状态中...");
        // 具体同步逻辑
    }
}

第三步:注意!别踩 "单线程" 的坑

Spring Task 默认是单线程执行的!如果多个任务同时到点,会排队等待,比如 A 任务执行要 10 分钟,B 任务到点了也得等 A 完事。

解决办法:配置线程池,让任务并行执行!新建一个配置类:

java 复制代码
@Configuration
public class ScheduledConfig {
    @Bean
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(5); // 5个线程,够日常用了
        scheduler.setThreadNamePrefix("timed-task-"); // 线程名前缀,方便日志排查
        return scheduler;
    }
}

这样一来,多个任务就能同时干活,再也不用排队啦~

五、定时任务白名单:不是谁都能 "指挥" 自动打工人!

你以为定时任务写好就完事了?大错特错!要是有人恶意加个 "每分钟删一次数据库" 的任务,那不得直接跑路?所以必须搞个白名单,只有在白名单里的任务才能执行。

实现思路超简单:

  1. 在配置文件(application.yml)里定义白名单,列出允许执行的任务方法名:
yaml 复制代码
timed-task:
  whitelist:
    - cancelTimeoutOrder # 允许执行的订单取消任务
    - syncOrderStatus    # 允许执行的订单同步任务
    # 没列出来的任务,比如deleteAllData,就不让执行
  1. 写个工具类,判断当前执行的任务是否在白名单里:
typescript 复制代码
@Component
@ConfigurationProperties(prefix = "timed-task")
public class TimedTaskWhitelist {
    private List<String> whitelist;
    // 判断任务是否在白名单中
    public boolean isAllowed(String taskName) {
        return whitelist.contains(taskName);
    }
    // get/set方法省略
}
  1. 在定时任务执行前加过滤:
kotlin 复制代码
@Component
public class OrderTimedTask {
    @Autowired
    private TimedTaskWhitelist whitelist;
    @Scheduled(cron = "0 0 2 * * ?")
    public void cancelTimeoutOrder() {
        // 先判断是否在白名单,不在就直接返回
        if (!whitelist.isAllowed("cancelTimeoutOrder")) {
            System.out.println("任务不在白名单,拒绝执行!");
            return;
        }
        // 在白名单里,再执行具体逻辑
        System.out.println("开始执行超时订单取消任务...");
    }
}

这样一来,就算有人偷偷加了非法任务,也跑不起来,安全感拉满!

六、核心过滤逻辑:让定时任务 "聪明" 干活,不做无用功

除了白名单,还得给定时任务加层 "智能过滤",避免它做无用功。比如 "取消超时订单",总不能每次都查全表吧?咱得让它只处理 "真正超时" 的订单。

核心过滤思路:

  1. 时间过滤:只查 "创建时间超过 15 分钟且未支付" 的订单(假设超时时间是 15 分钟);
  1. 状态过滤:只查 "待支付" 状态的订单(已支付、已取消的就别凑热闹了);
  1. 幂等性过滤:加个分布式锁,避免多线程同时执行同一个任务(比如 A 线程正在处理订单 123,B 线程就别再碰了)。

代码示例(结合 MyBatis):

kotlin 复制代码
@Component
public class OrderTimedTask {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private RedissonClient redisson; // 用Redisson实现分布式锁
    @Scheduled(cron = "0 0 2 * * ?")
    public void cancelTimeoutOrder() {
        // 1. 白名单过滤(省略,同上)
        // 2. 分布式锁:避免多实例重复执行
        RLock lock = redisson.getLock("timed-task:cancelTimeoutOrder");
        try {
            // 尝试加锁,5秒等待,30秒自动释放
            if (!lock.tryLock(5, 30, TimeUnit.SECONDS)) {
                System.out.println("其他实例正在执行该任务,本次跳过~");
                return;
            }
            // 3. 时间+状态过滤:只查真正需要处理的订单
            LocalDateTime timeoutTime = LocalDateTime.now().minusMinutes(15);
            List<Order> timeoutOrders = orderMapper.selectTimeoutOrders(
                0, // 0=待支付状态
                timeoutTime // 创建时间早于15分钟前
            );
            // 4. 执行取消逻辑
            if (CollectionUtils.isEmpty(timeoutOrders)) {
                System.out.println("没有超时订单,不用干活啦~");
                return;
            }
            orderMapper.batchCancelOrders(timeoutOrders.stream()
                .map(Order::getId)
                .collect(Collectors.toList()));
            System.out.println("成功取消" + timeoutOrders.size() + "个超时订单!");
        } catch (InterruptedException e) {
            System.out.println("任务执行出错:" + e.getMessage());
        } finally {
            // 释放锁
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

对应的 Mapper.xml 查询逻辑:

sql 复制代码
<select id="selectTimeoutOrders" resultType="com.example.entity.Order">
    SELECT id, order_no, status, create_time 
    FROM `order` 
    WHERE status = #{status} 
      AND create_time < #{timeoutTime} 
      AND pay_status = 0 -- 未支付
</select>

这样过滤下来,定时任务每次只处理 "该处理" 的订单,效率高还不浪费资源,比全表扫描香 100 倍!

最后:定时任务避坑总结

  1. 别手搓 cron!用在线工具生成,避免格式错误;
  1. 单线程不够用!记得配置线程池,让任务并行执行;
  1. 必须加白名单!防止非法任务执行;
  1. 过滤逻辑要到位!别让任务做无用功,避免全表扫描;
  1. 分布式场景加锁!防止多实例重复执行,避免数据错乱。

其实定时任务不难,关键是把 "到点干活" 变成 "到点聪明地干活"。你们平时用定时任务踩过哪些坑?评论区分享下,咱一起避坑,实现真正的摸鱼自由~

相关推荐
明天过后01229 分钟前
PDF文件中的相邻页面合并成一页,例如将第1页和第2页合并,第3页和第4页合并
java·python·pdf
tingting011911 分钟前
Spring Boot 外部配置指定不生效的原因与解决
java·spring boot·后端
用户03321266636721 分钟前
Java 设置 Excel 行高列宽:告别手动调整,拥抱自动化高效!
java·excel
2501_9096867022 分钟前
基于SpringBoot的网上点餐系统
java·spring boot·后端
neoooo27 分钟前
Spring Boot 3 + Kafka 实战指南
java·spring boot·kafka
天天摸鱼的java工程师28 分钟前
聊聊线程池中哪几种状态,分别表示什么?8 年 Java 开发:从业务踩坑到源码拆解(附监控实战)
java·后端
杨杨杨大侠32 分钟前
第4篇:AOP切面编程 - 无侵入式日志拦截
java·后端·开源
末央&42 分钟前
【JavaEE】文件IO操作
java·服务器·java-ee
渣哥1 小时前
面试官问我Java继承本质,我用一个故事征服了他
java