家人们谁懂啊!以前为了跑个凌晨 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 = 周日) | ? | 忽略周(日和周不能同时指定,否则会 "打架") |
避坑小技巧:
- 别手搓 cron!推荐用 cron 在线生成工具 ,选时间点自动生成,避免写错;
- 周和日别同时设具体值!比如写 0 0 2 1 ?(每月 1 号凌晨 2 点)没问题,但写 0 0 2 1 1 ?(1 月 1 号且周日凌晨 2 点)就可能出 bug,除非你明确要这两个条件同时满足;
- 秒位别写*!除非你想让任务每秒执行一次(大部分场景用不上,还会搞崩系统),一般写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;
}
}
这样一来,多个任务就能同时干活,再也不用排队啦~
五、定时任务白名单:不是谁都能 "指挥" 自动打工人!
你以为定时任务写好就完事了?大错特错!要是有人恶意加个 "每分钟删一次数据库" 的任务,那不得直接跑路?所以必须搞个白名单,只有在白名单里的任务才能执行。
实现思路超简单:
- 在配置文件(application.yml)里定义白名单,列出允许执行的任务方法名:
yaml
timed-task:
whitelist:
- cancelTimeoutOrder # 允许执行的订单取消任务
- syncOrderStatus # 允许执行的订单同步任务
# 没列出来的任务,比如deleteAllData,就不让执行
- 写个工具类,判断当前执行的任务是否在白名单里:
typescript
@Component
@ConfigurationProperties(prefix = "timed-task")
public class TimedTaskWhitelist {
private List<String> whitelist;
// 判断任务是否在白名单中
public boolean isAllowed(String taskName) {
return whitelist.contains(taskName);
}
// get/set方法省略
}
- 在定时任务执行前加过滤:
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("开始执行超时订单取消任务...");
}
}
这样一来,就算有人偷偷加了非法任务,也跑不起来,安全感拉满!
六、核心过滤逻辑:让定时任务 "聪明" 干活,不做无用功
除了白名单,还得给定时任务加层 "智能过滤",避免它做无用功。比如 "取消超时订单",总不能每次都查全表吧?咱得让它只处理 "真正超时" 的订单。
核心过滤思路:
- 时间过滤:只查 "创建时间超过 15 分钟且未支付" 的订单(假设超时时间是 15 分钟);
- 状态过滤:只查 "待支付" 状态的订单(已支付、已取消的就别凑热闹了);
- 幂等性过滤:加个分布式锁,避免多线程同时执行同一个任务(比如 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 倍!
最后:定时任务避坑总结
- 别手搓 cron!用在线工具生成,避免格式错误;
- 单线程不够用!记得配置线程池,让任务并行执行;
- 必须加白名单!防止非法任务执行;
- 过滤逻辑要到位!别让任务做无用功,避免全表扫描;
- 分布式场景加锁!防止多实例重复执行,避免数据错乱。
其实定时任务不难,关键是把 "到点干活" 变成 "到点聪明地干活"。你们平时用定时任务踩过哪些坑?评论区分享下,咱一起避坑,实现真正的摸鱼自由~