XXL-Job 任务超时控制、失败重试与阻塞处理策略
你的任务跑了 2 个小时还没结束。
是数据量太大?还是执行器挂了?还是代码死循环了?
如果继续等下去,会不会把执行器资源耗尽,影响其他任务?
今天,我们来看 XXL-Job 如何处理这些问题。
三个核心问题
┌─────────────────────────────────────────────────────────────┐
│ 三个核心问题 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 问题1:任务执行超时怎么办? │
│ ───────────────────────────────────────────── │
│ 设置超时时间,超时后强制中断 │
│ │
│ 问题2:任务执行失败怎么办? │
│ ───────────────────────────────────────────── │
│ 自动重试,重新执行 │
│ │
│ 问题3:上一个任务还没结束,下一个任务又触发了怎么办? │
│ ───────────────────────────────────────────── │
│ 阻塞处理策略,决定是排队还是丢弃 │
│ │
└─────────────────────────────────────────────────────────────┘一、任务超时控制
配置超时时间
在调度中心配置任务时,可以设置「任务超时时间」:
任务名称:订单同步任务
超时时间:0(不超时)← 默认
3600(1小时)
600(10分钟)超时处理机制
java
public class JobExecutor {
private void executeJob(JobInfo jobInfo, TriggerParam triggerParam) {
// 1. 记录开始时间
long startTime = System.currentTimeMillis();
// 2. 执行任务(异步)
Future<ReturnT> future = executorService.submit(() -> {
return runJob(jobInfo, triggerParam);
});
// 3. 设置超时
int timeout = jobInfo.getExecutorTimeout();
if (timeout > 0) {
try {
// 等待任务完成或超时
ReturnT result = future.get(timeout, TimeUnit.SECONDS);
return result;
} catch (TimeoutException e) {
// 超时了!
future.cancel(true); // 中断任务线程
return new ReturnT(ReturnT.FAIL_CODE, "任务执行超时");
}
} else {
// 不设置超时,等待完成
return future.get();
}
}
}超时时间设置建议
| 任务类型 | 建议超时时间 | 原因 |
|---|---|---|
| 简单查询/计算 | 60-300 秒 | 数据量小,应该快速完成 |
| 数据同步 | 3600-7200 秒 | 数据量大,需要时间 |
| 报表生成 | 1800-3600 秒 | 复杂计算 |
| 外部 API 调用 | 30-60 秒 | 网络不稳定 |
| 脚本执行 | 视脚本而定 | 根据脚本复杂度 |
注意事项
超时后任务会被中断,但需要注意:
java
// ❌ 错误:在 finally 中继续执行
public void execute() {
try {
// 业务逻辑
doSomething();
} finally {
// 如果超时,线程会被 interrupt,但这里可能还会执行
doCleanup(); // 可能会导致数据不一致
}
}
// ✅ 正确:检查中断标志
public void execute() {
try {
doSomething();
} catch (InterruptedException e) {
// 收到中断信号,直接退出
Thread.currentThread().interrupt();
throw new RuntimeException("任务被中断");
}
}二、失败重试机制
配置重试次数
在调度中心配置:
任务名称:订单同步任务
失败重试次数:0(不重试)
1(重试1次)
3(重试3次)
失败重试间隔:60(失败后60秒重试)重试机制流程
┌─────────────────────────────────────────────────────────────┐
│ 失败重试流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 任务执行失败 │
│ │ │
│ ▼ │
│ ┌───────────────────┐ │
│ │ 获取重试次数 │ │
│ │ retryCount > 0? │ │
│ └─────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────┐ │
│ │ 记录失败日志 │ │
│ │ 更新重试状态 │ │
│ └─────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────┐ │
│ │ 等待重试间隔 │ │
│ │ sleep(retryInterval) │
│ └─────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────┐ │
│ │ 重新触发执行 │ │
│ │ retryCount-- │ │
│ └─────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌──────┴──────┐ │
│ │ │ │
│ ▼ ▼ │
│ 成功 失败 ──▶ 继续重试,直到次数用完 │
│ │
└─────────────────────────────────────────────────────────────┘重试间隔策略
java
// 固定间隔
public class FixedRetryStrategy implements RetryStrategy {
@Override
public long getRetryInterval(int retryCount, int retryInterval) {
return retryInterval * 1000; // 固定秒数
}
}
// 指数退避
public class ExponentialBackoffRetryStrategy implements RetryStrategy {
@Override
public long getRetryInterval(int retryCount, int retryInterval) {
// 1, 2, 4, 8, 16... 秒
return (long) Math.pow(2, retryCount - 1) * retryInterval * 1000;
}
}重试注意事项
不是所有失败都需要重试:
java
public class MyJob extends XxlJobSimpleJob {
@Override
public void execute() throws Exception {
try {
// 执行业务逻辑
Result result = doSomething();
// 业务错误,不需要重试
if (result.isBusinessError()) {
// 直接返回,不重试
XxlJobHelper.handleSuccess("业务验证失败:" + result.getMessage());
return;
}
// 系统错误,可以重试
if (result.isSystemError()) {
throw new RuntimeException("系统错误,需要重试");
}
} catch (TemporaryException e) {
// 临时性错误,应该重试
throw e;
} catch (PermanentException e) {
// 永久性错误,不重试
XxlJobHelper.handleFail("永久性错误:" + e.getMessage());
}
}
}三、阻塞处理策略
什么是阻塞?
┌─────────────────────────────────────────────────────────────┐
│ 阻塞场景 │
├─────────────────────────────────────────────────────────────┤
│ │
│ cron: 每 5 分钟执行一次 │
│ 任务执行时间:15 分钟 │
│ │
│ 触发 T=0min ──▶ 执行中...(预计 T=15min 完成) │
│ 触发 T=5min ──▶ 任务1还没结束,任务2怎么办? │
│ 触发 T=10min ──▶ 任务1、2都没结束,任务3怎么办? │
│ │
│ 阻塞队列: │
│ [任务2] [任务3] [任务4] ... │
│ │
└─────────────────────────────────────────────────────────────┘四种阻塞处理策略
XXL-Job 提供了四种阻塞处理策略:
| 策略 | 说明 | 适用场景 |
|---|---|---|
| SINGLE | 串行执行,新任务加入队列,等待上一个任务完成 | 任务不可并发 |
| QUEUE | 队列模式,新任务加入队列,依次执行 | 任务可排队 |
| DISCARD_LATER | 丢弃后续触发,只执行当前 | 不重要任务 |
| COVER_EARLY | 丢弃队列中的任务,只执行新任务 | 只需要最新结果 |
策略对比
场景:任务执行 15 分钟,cron 每 5 分钟触发一次
SINGLE(串行):
┌────────────────────────────────────────────────────────────┐
│ T=0: [任务1 ────────────────] │
│ T=5: [等待] [任务2 ────────────────] │
│ T=10: [等待] [等待] [任务3 ────────────────] │
│ │
│ 执行顺序:任务1 → 任务2 → 任务3 │
└────────────────────────────────────────────────────────────┘
QUEUE(队列):
┌────────────────────────────────────────────────────────────┐
│ T=0: [任务1 ────────────────] │
│ T=5: [队列:任务2] │
│ T=10: [队列:任务2, 任务3] │
│ T=15: [任务1完成] → [任务2 ────────────────] │
│ T=30: [任务2完成] → [任务3 ────────────────] │
│ │
│ 执行顺序:任务1 → 任务2 → 任务3(排队依次执行) │
└────────────────────────────────────────────────────────────┘
DISCARD_LATER(丢弃后续):
┌────────────────────────────────────────────────────────────┐
│ T=0: [任务1 ────────────────] │
│ T=5: [丢弃任务2] │
│ T=10: [丢弃任务3] │
│ │
│ 只执行:任务1 │
└────────────────────────────────────────────────────────────┘
COVER_EARLY(覆盖早期):
┌────────────────────────────────────────────────────────────┐
│ T=0: [任务1 ────────────────] │
│ T=5: [丢弃任务1,加入任务2] │
│ T=10: [丢弃任务2,加入任务3] │
│ T=10: [任务3 ────────────────] │
│ │
│ 只执行:任务3(最新的) │
└────────────────────────────────────────────────────────────┘代码实现
java
public class BlockStrategyHandler {
// SINGLE:串行执行
public void handleSingle(JobInfo jobInfo, Runnable newTask) {
synchronized (jobInfo) {
// 使用 synchronized 保证串行
newTask.run();
}
}
// QUEUE:队列模式
public void handleQueue(JobInfo jobInfo, Runnable newTask) {
// 使用阻塞队列
BlockingQueue<Runnable> queue = jobInfo.getQueue();
queue.offer(newTask);
// 如果没有正在执行,启动执行
if (jobInfo.getRunningCount() == 0) {
processQueue(jobInfo);
}
}
// DISCARD_LATER:丢弃
public void handleDiscard(JobInfo jobInfo, Runnable newTask) {
// 什么都不做,直接丢弃
}
// COVER_EARLY:覆盖
public void handleCover(JobInfo jobInfo, Runnable newTask) {
// 清空队列
jobInfo.getQueue().clear();
// 只执行新任务
jobInfo.setRunningTask(newTask);
newTask.run();
}
}最佳实践
场景一:数据同步任务
任务特点:
· 数据量大,执行时间长
· 任务之间有依赖(不能并发)
· 可以接受延迟
推荐配置:
· 超时时间:7200 秒(2小时)
· 失败重试:3 次
· 重试间隔:300 秒(5分钟)
· 阻塞策略:SINGLE(串行)
理由:
· 超时设置较长,避免正常执行被中断
· 失败重试给临时故障恢复机会
· 串行保证数据一致性场景二:心跳检测任务
任务特点:
· 执行时间短(秒级)
· 可以并发
· 需要及时响应
推荐配置:
· 超时时间:30 秒
· 失败重试:1 次
· 重试间隔:10 秒
· 阻塞策略:DISCARD_LATER(丢弃)
理由:
· 心跳不需要排队,丢了就丢了
· 下一次触发会补偿
· 节省资源场景三:报表生成任务
任务特点:
· 执行时间长
· 只需要最新结果
· 旧报表没意义
推荐配置:
· 超时时间:3600 秒(1小时)
· 失败重试:0 次(人工检查)
· 阻塞策略:COVER_EARLY(覆盖)
理由:
· 旧报表没用,直接丢弃
· 失败重试可能产生更多垃圾报表
· 人工介入更安全总结
| 配置项 | 作用 | 设置建议 |
|---|---|---|
| 超时时间 | 防止任务无限执行 | 根据任务正常耗时 × 2 |
| 失败重试次数 | 自动恢复临时故障 | 重要任务 2-3 次,简单任务 0-1 次 |
| 重试间隔 | 控制重试频率 | 指数退避或固定 5-10 分钟 |
| 阻塞策略 | 控制并发行为 | 根据任务特性选择 |
思考题
一个任务配置了:
- 超时时间:60 秒
- 失败重试:3 次
- 阻塞策略:SINGLE
任务执行到第 50 秒时抛出异常,会发生什么?
是算超时还是失败重试?任务会中断还是执行完?
