定时任务从入门到防坑,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. 分布式场景加锁!防止多实例重复执行,避免数据错乱。

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

相关推荐
用户268516121075629 分钟前
常见的 Git 分支命名策略和实践
后端
程序员小假30 分钟前
我们来说一下 MySQL 的慢查询日志
java·后端
南囝coding32 分钟前
《独立开发者精选工具》第 025 期
前端·后端
独自破碎E1 小时前
Java是怎么实现跨平台的?
java·开发语言
To Be Clean Coder1 小时前
【Spring源码】从源码倒看Spring用法(二)
java·后端·spring
xdpcxq10291 小时前
风控场景下超高并发频次计算服务
java·服务器·网络
想用offer打牌1 小时前
你真的懂Thread.currentThread().interrupt()吗?
java·后端·架构
橘色的狸花猫2 小时前
简历与岗位要求相似度分析系统
java·nlp
独自破碎E2 小时前
Leetcode1438绝对值不超过限制的最长连续子数组
java·开发语言·算法
用户91743965392 小时前
Elasticsearch Percolate Query使用优化案例-从2000到500ms
java·大数据·elasticsearch