订阅更新

try-catch写在循环里还是循环外?——Java异常机制和大数据异常处理

Java手册 第 15 题 约 10 分钟读完

本文目录
  1. 先说结论
  2. Java异常体系
  3. try-catch在循环里 vs 循环外
  4. Flink的异常处理最佳实践——侧输出流
  5. Spark的异常处理——accumulator计数
  6. 实战踩坑:一条null数据杀死了整个Flink Job
  7. try-catch的性能影响
  8. 面试高频追问
  9. 总结

先说结论

大数据作业的异常处理核心原则:不要让一条脏数据杀死整个作业。在数据处理的核心逻辑(map/flatMap/processElement)里必须用try-catch包裹,catch住异常后记录日志+输出到侧输出流(Side Output),而不是让异常向上抛导致Task/Job失败。try-catch放在循环里还是循环外取决于业务逻辑:如果要跳过异常数据继续处理就放循环里,如果一条异常就要终止就放循环外。

先把这段讲顺;面试官追问时,再展开下面的原理和细节。

详细讲解

异常处理看起来是基础中的基础,但在大数据场景中特别重要——Flink/Spark处理的数据量是亿级的,其中一定有脏数据。如果一条脏数据抛了异常导致整个作业崩溃,几百万条正常数据也白处理了。在某元宇宙社交公司写Flink DataStream处理虚拟礼物交易时,有一次上游发了一条amount字段为null的数据,NullPointerException直接把整个Job搞挂了——几千万条正常数据的Checkpoint全部丢失。从那以后我对异常处理特别重视。

Java异常体系

# Java异常类层次
#
# Throwable
# ├── Error(严重错误,不应该catch)
# │   ├── OutOfMemoryError
# │   ├── StackOverflowError
# │   └── NoClassDefFoundError
# └── Exception
#     ├── 受检异常(Checked,必须catch或throws声明)
#     │   ├── IOException
#     │   ├── SQLException
#     │   └── ClassNotFoundException
#     └── 非受检异常(Unchecked / RuntimeException)
#         ├── NullPointerException
#         ├── NumberFormatException
#         ├── ArrayIndexOutOfBoundsException
#         └── ClassCastException
类型必须catch?典型场景大数据中的出现
Error不要catchJVM级错误(OOM等)Executor OOM、StackOverflow
受检异常必须处理IO/网络/SQL读HDFS失败、连接超时
非受检异常建议处理代码逻辑Bug、脏数据NullPointer、数据格式错误

try-catch在循环里 vs 循环外

// 方式一:try-catch在循环外(一条异常整个循环终止)
try {
    for (String line : lines) {
        processLine(line);
    }
} catch (Exception e) {
    log.error('Processing failed', e);
    // 一条数据出错,后面的数据都不处理了!
}
 
// 方式二:try-catch在循环里(异常数据跳过,继续处理)
for (String line : lines) {
    try {
        processLine(line);
    } catch (Exception e) {
        log.warn('Skip bad line: ' + line, e);
        errorCounter.inc(); // 记录异常数量
        // 跳过这条数据,继续处理下一条
    }
}
 
// 大数据场景几乎都用方式二!
// 因为不能让一条脏数据杀死整个作业
// 但要记录异常数据的数量和内容,方便排查

Flink的异常处理最佳实践——侧输出流

// Flink推荐的异常处理模式:Side Output
// 正常数据走主流,异常数据走侧流
 
final OutputTag<String> errorTag = new OutputTag<String>('errors'){};
 
SingleOutputStreamOperator<Event> mainStream = inputStream
    .process(new ProcessFunction<String, Event>() {
        @Override
        public void processElement(String value, Context ctx,
                                   Collector<Event> out) {
            try {
                Event event = parseEvent(value);
                if (event.getAmount() == null) {
                    throw new IllegalArgumentException('amount is null');
                }
                out.collect(event);  // 正常数据输出到主流
            } catch (Exception e) {
                ctx.output(errorTag, value); // 异常数据输出到侧流
            }
        }
    });
 
// 异常数据写入专门的Topic/表,方便排查
DataStream<String> errorStream = mainStream.getSideOutput(errorTag);
errorStream.addSink(new KafkaSink('error_events'));

这个模式的好处:① 主流不受异常影响,正常数据继续处理;② 异常数据不丢失,写入专门的存储方便后续排查和修复;③ 可以对异常数量做监控——异常率超过阈值自动告警。

Spark的异常处理——accumulator计数

// Spark中异常处理的模式
LongAccumulator errorCount = sc.longAccumulator('errorCount');
 
Dataset<Event> cleanData = rawData.map(
    (MapFunction<String, Event>) line -> {
        try {
            return parseEvent(line);
        } catch (Exception e) {
            errorCount.add(1);  // 记录异常数量
            return null;         // 返回null
        }
    }, Encoders.bean(Event.class))
    .filter((FilterFunction<Event>) event -> event != null);  // 过滤null
 
// 作业结束后检查异常数量
long errors = errorCount.value();
if (errors > totalCount * 0.01) {  // 异常率超过1%
    throw new RuntimeException('Too many errors: ' + errors);
}

实战踩坑:一条null数据杀死了整个Flink Job

在某元宇宙社交公司,Flink Job处理虚拟礼物交易流水。上游Kafka的数据格式是JSON,其中amount字段应该是double类型。有一天上游系统Bug导致发了一条amount为null的数据。

结果:Flink的map算子里直接调了event.getAmount().doubleValue(),NullPointerException没有catch。Task失败后Flink自动重试,重试时又读到这条脏数据,又失败。重试4次后Job永久失败。几千万条正常数据的Checkpoint全部丢失,恢复后需要从Kafka重新消费。

修复:① 在所有数据处理的核心逻辑里加try-catch,异常数据走侧输出流;② 对关键字段加null检查(Defensive Programming);③ 在侧输出流上接告警——异常数据量突增时立刻通知开发排查上游问题。

教训:大数据作业处理的是'脏数据的海洋中的正常数据'——你永远不知道上游会发什么奇怪的数据过来。防御性编程和异常处理不是可选的,是必须的。

try-catch的性能影响

很多人担心try-catch会影响性能。实际测试:

场景try-catch的影响说明
不抛异常时几乎零开销JIT编译后try块和普通代码一样快
抛异常时很大开销(微秒级)创建异常对象+填充堆栈信息是昂贵的
高频抛异常严重性能问题每秒抛上万次异常会拖慢整个JVM

大数据最佳实践:try-catch本身不影响性能(不抛异常时几乎零开销),但抛异常的开销很大。所以正确做法是:把异常当作'异常情况'而不是'控制流'——先用if判断过滤明显的脏数据(不抛异常),只对真正意外的情况用catch。比如先检查amount != null再调doubleValue(),而不是catch NullPointerException。

面试高频追问

追问一:finally块一定会执行吗?

几乎一定会——除了三种极端情况:① System.exit()(JVM直接退出);② JVM崩溃(OOM导致的kill等);③ 所在线程被强制kill。大数据场景中Flink的close()方法和Spark的finally块用于释放资源(关闭连接、释放文件句柄),一般都能正常执行。

追问二:异常吞掉(catch但不处理)有什么危害?

最大的危害是'静默失败'——数据错了但你不知道。大数据作业处理亿级数据,如果catch后什么都不做,可能有百万条脏数据被静默跳过,报表数字是错的但没人知道。最佳实践:catch后至少做三件事——① 记日志(包含异常数据内容);② 计数器+1;③ 写入侧输出流/异常表。

追问三:Flink的Restart Strategy和异常处理有什么关系?

Flink的重启策略决定了作业失败后怎么恢复——固定间隔重启、失败率重启或不重启。但重启不能解决脏数据问题——重启后从Checkpoint恢复,又会读到同一条脏数据,又失败,陷入重启循环。所以异常处理(在代码层面catch掉脏数据)和重启策略是互补的——catch处理可预见的异常,重启策略处理不可预见的瞬时故障。

总结

异常处理面试讲清楚三层:① Java异常体系(Error不catch、受检异常必须处理、非受检异常建议处理);② try-catch在循环里vs循环外(大数据场景放循环里,跳过脏数据继续处理);③ 大数据异常处理最佳实践(Flink用侧输出流、Spark用accumulator、异常数据写入专门存储+监控告警)。能讲出一条null数据杀死整个Flink Job的真实踩坑和完整的修复方案,面试官会觉得你有生产级别的代码质量意识。

下一篇聊Java反射:Flink的反射为什么比直接调用慢?反射在大数据框架中有哪些应用?

你们大数据作业怎么处理脏数据?用了侧输出流吗?评论区聊聊~

关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长

Java 面试通关手册

第16-20篇:高级特性与面试总结

Wood聊大数据

9年大数据老兵,聊技术、聊职场、聊成长

刷题进度保存在当前浏览器