欢迎光临
我们一直在努力

Quartz Job 异常处理:ERROR 状态与 OOM 的挽回

Quartz Job 异常处理:ERROR 状态与 OOM 的挽回

一、问题现象

Quartz 定时任务跑了一段时间后,Trigger 状态变成了 ERROR,Job 不再执行。但系统本身还在正常运行,其他 Job 也没问题。


二、Trigger 变 ERROR 的机制

Quartz 在执行 Job 时,如果 executeInternal 方法抛出了任何未被捕获的 Throwable(包括 Exception 和 Error),Quartz 框架就会把这个 Trigger 标记为 ERROR。

一旦 Trigger 进入 ERROR 状态,就不会再自动触发,相当于这个 Job 悄悄死了。


三、所有可能导致 ERROR 的异常类型

3.1 异常全景图

Throwable(任何未捕获的都会导致 Trigger 变 ERROR)

├── Error(catch Exception 抓不住)
│ ├── OutOfMemoryError → 堆内存不足
│ ├── StackOverflowError → 方法调用栈过深
│ ├── NoClassDefFoundError → 运行时类找不到
│ └── NoSuchMethodError → 运行时方法找不到

└── Exception(catch Exception 可以抓住)
├── RuntimeException
│ ├── NullPointerException → 空指针
│ ├── IndexOutOfBoundsException → 数组/列表越界
│ ├── ClassCastException → 类型转换错误
│ ├── IllegalArgumentException → 非法参数
│ ├── UnsupportedOperationException → 不支持的操作
│ └── FeignException → Feign 调用失败(超时、服务不可用等)

├── IOException → IO 异常
├── SQLException → 数据库异常
└── …

3.2 按场景分类

场景一:网络与远程调用
异常触发原因能否被 catch(Exception) 抓住
FeignException 对端服务不可用、超时、返回异常 ✅ 能
SocketTimeoutException 网络连接超时 ✅ 能
ConnectException 无法建立连接 ✅ 能
RetryableException Feign 重试耗尽 ✅ 能

典型代码:

integrationFeignClient.mixedTrigger(); // 对端服务挂了 → FeignException

场景二:内存与资源
异常触发原因能否被 catch(Exception) 抓住
OutOfMemoryError 堆内存不足(处理大数据、内存泄漏) ❌ 抓不住
StackOverflowError 递归过深 ❌ 抓不住

典型代码:

List<Data> all = feignClient.queryAll(); // 数据量太大 → OOM

场景三:数据库与持久化
异常触发原因能否被 catch(Exception) 抓住
SQLException 数据库连接失败、SQL 语法错误 ✅ 能
PessimisticLockingFailureException 数据库锁竞争(集群模式下常见) ✅ 能
DataIntegrityViolationException 数据约束冲突 ✅ 能
场景四:代码逻辑
异常触发原因能否被 catch(Exception) 抓住
NullPointerException 对象为 null 时调用方法 ✅ 能
ClassCastException 类型转换错误 ✅ 能
IndexOutOfBoundsException 集合越界访问 ✅ 能
场景五:类加载与依赖
异常触发原因能否被 catch(Exception) 抓住
NoClassDefFoundError 运行时找不到类(jar 包冲突或缺失) ❌ 抓不住
NoSuchMethodError 运行时找不到方法(依赖版本不一致) ❌ 抓不住
ClassNotFoundException 动态加载类失败 ✅ 能
场景六:Quartz 框架层面
异常触发原因能否被业务代码抓住
JobPersistenceException JDBC JobStore 操作失败 ❌ 在框架层,不在 executeInternal 内
集群 check-in 超时 GC 停顿导致节点未及时心跳 ❌ 框架层面

四、项目中实际存在的风险模式

4.1 完全没有 try-catch(最高风险)

// SalesQuarterDateJob
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
Result result = basicFeignClient.generateData();
// 没有任何 try-catch!
// 任何异常都会直接穿透到 Quartz → Trigger 变 ERROR
}

风险:Feign 调用失败、NPE、OOM,任何一种发生,Job 就永久停止。

4.2 catch 了但 re-throw(等于没 catch)

// CheckMQStatusJob
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
try {
calculationFeignClient.schedule(new ScheduleTriggerCommand());
} catch (Exception ex) {
throw ex; // 抓住了又抛出去,等于没抓
}
}

风险:和没有 try-catch 完全一样,任何 Exception 都会导致 Trigger 变 ERROR。

4.3 catch 了 Exception 但抓不住 Error(中等风险)

// CommonMixedProcessJob
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
integrationProcess();
}

private void integrationProcess() {
try {
integrationFeignClient.mixedTrigger();
} catch (Exception e) {
// ✅ 能抓住 Feign 调用异常、NPE 等
// ❌ 抓不住 OutOfMemoryError、NoClassDefFoundError 等
log.info("执行失败 " + e.getMessage()); // 用的 log.info,不是 log.error
}
}

风险:常规异常没问题,但 OOM 等 Error 会穿透,Trigger 变 ERROR。

4.4 风险等级总结

模式风险等级能防住什么防不住什么
没有 try-catch 🔴 最高 什么都防不住 一切
catch + re-throw 🔴 最高 什么都防不住 一切
catch(Exception) 🟡 中等 常规异常 OOM、StackOverflow 等 Error
catch(Throwable) 🟢 最低 一切 只有 JVM 彻底崩溃才防不住

五、如何最大化规避 ERROR

5.1 第一道防线:所有 Job 统一使用 catch(Throwable)

@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
try {
// 业务逻辑
doWork();
} catch (Throwable t) {
log.error("[JobName] 执行失败: ", t);
// 不抛出 → Trigger 不变 ERROR → 下次调度继续执行
}
}

为什么是 Throwable 而不是 Exception:

Throwable
├── Error ← OOM、StackOverflow 在这里,只有 Throwable 能抓住
└── Exception ← 常规异常在这里,Exception 就能抓住

5.2 第二道防线:Feign 调用必须配置超时

没有超时的 Feign 调用,一旦对端服务无响应,线程就会被永久占用:

feign:
client:
config:
default:
connectTimeout: 5000 # 连接超时 5 秒
readTimeout: 30000 # 读取超时 30 秒

5.3 第三道防线:大数据量场景使用分页或流式处理

// ❌ 危险:一次性加载全量数据
List<Data> all = feignClient.queryAll(); // 数据量大 → OOM

// ✅ 安全:分页处理
int page = 0;
while (true) {
List<Data> batch = feignClient.queryByPage(page, 1000);
if (batch.isEmpty()) break;
process(batch);
page++;
}

5.4 第四道防线:日志级别要正确

// ❌ 失败用 info,排查问题时根本看不到
catch (Throwable t) {
log.info("执行失败 " + t.getMessage()); // 没有堆栈,没有 ERROR 级别
}

// ✅ 失败用 error,带上完整堆栈
catch (Throwable t) {
log.error("[JobName] 执行失败: ", t); // ERROR 级别 + 完整堆栈
}

5.5 第五道防线:JVM 参数兜底

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/dump

OOM 时自动生成 heap dump,事后用 MAT 或 VisualVM 分析。


六、已经变 ERROR 了怎么恢复

6.1 方式一:重启服务

最简单粗暴,重启后 Quartz 重新加载所有 Trigger,状态恢复为 NORMAL。

6.2 方式二:通过代码恢复

// 恢复指定 Trigger
TriggerKey triggerKey = TriggerKey.triggerKey(jobClassName, jobGroupName);
scheduler.resumeTrigger(triggerKey);

// 恢复整个 Job 组
scheduler.resumeJobs(GroupMatcher.jobGroupEquals(groupName));

6.3 方式三:扩展 JobController 提供恢复接口

项目中的 JobController 已有 listJob、page 等管理接口,可扩展一个恢复接口:

@ApiOperation(value = "Resume Error Job")
@PostMapping("/resumeJob")
public Result resumeJob(@RequestParam String jobName, @RequestParam String groupName) throws Exception {
TriggerKey triggerKey = TriggerKey.triggerKey(jobName, groupName);
scheduler.resumeTrigger(triggerKey);
return ResultGenerator.genSuccessResult();
}


七、总结

7.1 异常捕获决策树

executeInternal 执行

├─ 发生 Exception(NPE、Feign 超时等)
│ ├─ 有 catch(Exception) 或 catch(Throwable) → 被抓住 → Job 继续
│ └─ 没有 catch 或 re-throw → 穿透 → Trigger 变 ERROR

├─ 发生 Error(OOM、StackOverflow 等)
│ ├─ 有 catch(Throwable) → 被抓住 → Job 继续
│ └─ 只有 catch(Exception) 或没有 catch → 穿透 → Trigger 变 ERROR

└─ Quartz 框架层面异常(JobStore 失败等)
└─ 业务代码抓不住 → 需要运维介入或重启

7.2 防御层级

层级措施防什么
代码层 catch (Throwable t) + log.error 防止任何未捕获异常导致 Job 静默死亡
配置层 Feign 超时配置 防止线程被无限占用
设计层 分页/流式处理大数据 防止 OOM
JVM 层 HeapDump 参数 事后分析 OOM 根因
运维层 监控 Trigger 状态 及时发现 ERROR 的 Job
赞(0)
未经允许不得转载:171主机测评 » Quartz Job 异常处理:ERROR 状态与 OOM 的挽回
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址