Skip to content

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 秒时抛出异常,会发生什么?

是算超时还是失败重试?任务会中断还是执行完?

基于 VitePress 构建