观察者模式中某个观察者异常会影响其他观察者吗?
简化版
会不会影响取决于通知模型和异常处理策略。同步通知里,如果发布方不捕获异常,一个观察者抛错可能中断后续观察者并回滚主流程;如果逐个捕获异常,则可以隔离失败。异步通知通常能隔离主流程,但仍要处理重试、告警、死信和幂等。工程上要按观察者是否关键来决定:关键观察者失败应阻断,非关键观察者失败应隔离并记录。
详细版
观察者模式常见风险是“一个监听器拖垮一串监听器”。比如订单支付成功后通知多个观察者:
积分监听器 -> 短信监听器 -> 发票监听器 -> 营销监听器
如果积分监听器抛异常,短信和发票是否还执行?这个问题没有固定答案,要看业务语义。
常见策略:
- fail-fast:关键监听器失败,直接中断;
- fail-safe:单个监听器失败不影响其他监听器;
- 分级处理:核心监听器同步阻断,非核心监听器异步隔离;
- 收集错误:全部执行完,再统一返回失败列表。
记忆钩子:观察者异常处理不是“catch 或不 catch”的代码细节,而是业务可靠性契约。
完整版教学
面试提示:异常隔离题要回答“谁失败、谁感知、是否继续、如何补偿”,否则容易停留在 try-catch 层面。
一、同步通知默认容易被异常打断
最简单的发布逻辑通常这样写:
for (Observer observer : observers) {
observer.onEvent(event);
}
如果第 2 个观察者抛异常,第 3 个、第 4 个就不会执行。异常还可能继续向上抛,让发布事件的主流程失败。
这对某些场景是正确的,例如权限校验、风控拦截;但对短信、日志、营销通知可能就不合适。
二、逐个捕获可以隔离非关键观察者
如果观察者是非关键动作,可以逐个隔离:
for (Observer observer : observers) {
try {
observer.onEvent(event);
} catch (Exception ex) {
log.error("listener failed, listener={}, eventId={}",
observer.name(), event.id(), ex);
}
}
这样一个观察者失败,不会阻塞其他观察者。但这不代表失败可以被忽略,日志、指标、告警仍然要有。
三、关键观察者应该明确标记
可以给观察者增加元信息:
interface EventListener<E> {
String name();
boolean required();
void onEvent(E event);
}
执行时:
try {
listener.onEvent(event);
} catch (Exception ex) {
if (listener.required()) {
throw ex;
}
log.warn("optional listener failed: {}", listener.name(), ex);
}
这样业务能清楚区分“必须成功”和“尽力而为”。
四、异步通知隔离主流程但不等于可靠
异步通知通常不会阻塞主流程:
publish event -> submit to executor -> return
但异步带来新问题:
- 线程池满了怎么办;
- 任务执行失败是否重试;
- 重试是否会重复执行;
- 失败任务是否进入死信或补偿表;
- 监听器延迟是否可观测。
所以异步只是隔离延迟和异常传播,不自动保证可靠投递。
五、不同业务动作应该选择不同失败策略
| 观察者动作 | 失败策略 | 原因 |
|---|---|---|
| 风控校验 | 阻断 | 安全优先 |
| 扣库存 | 阻断或补偿 | 防止超卖 |
| 发短信 | 隔离并重试 | 不应影响支付成功 |
| 写审计日志 | 高可靠记录 | 合规要求 |
| 营销埋点 | 隔离丢弃或补偿 | 非核心 |
面试回答时要强调“按业务重要性分级”,不要把所有监听器一刀切。
六、收集错误适合批量通知
有时希望所有监听器都尝试执行,然后返回失败明细:
List<ListenerError> errors = new ArrayList<>();
for (EventListener listener : listeners) {
try {
listener.onEvent(event);
} catch (Exception ex) {
errors.add(new ListenerError(listener.name(), ex.getMessage()));
}
}
if (!errors.isEmpty()) {
throw new EventPublishException(errors);
}
这种方式适合管理后台、批处理、测试框架等需要完整错误报告的场景。
七、异常隔离必须配合可观测性
失败被隔离后,调用方可能感知不到,所以要补监控:
event_type=OrderPaid
listener=SendSmsListener
status=failed
error=TimeoutException
event_id=E202
retry=1
对关键监听器要报警;对非关键监听器也要有失败率指标,避免长时间静默失败。
八、常见误区与追问
- 误区:观察者异常一定不能影响主流程。 风控、权限、库存等关键观察者失败时就应该阻断或补偿。
- 误区:捕获异常后打印日志就结束了。 还需要指标、告警、重试、死信或人工补偿。
- 误区:异步通知就不会丢事件。 线程池拒绝、进程崩溃、任务失败都可能导致事件丢失。
- 误区:所有监听器都用同一种异常策略。 应按核心链路、合规链路、非核心链路分级。
- 追问:如何防止一个慢监听器拖慢整体? 同步场景设置超时,非关键动作异步化,线程池隔离。
- 追问:失败后要不要重试? 幂等动作可以重试,非幂等动作要先设计去重键和状态机。
- 追问:异常隔离会不会掩盖问题? 会,所以隔离必须同时配套可观测和告警。
九、加强记忆
- 同步通知不捕获异常会中断后续观察者。
- 非关键观察者可隔离失败,但不能静默失败。
- 关键观察者要明确 required 语义。
- 异步隔离主流程,不等于可靠投递。
- 失败策略按业务重要性分级。
- 异常隔离必须配日志、指标和告警。