← 返回题目列表

观察者模式中多个观察者的通知顺序如何设计?

高频 中等 第 10 / 27 题 更新于 2026/08/02
观察者模式通知顺序优先级事件驱动

简化版

多个观察者的通知顺序不能依赖集合遍历的偶然结果,要根据业务语义显式设计。若观察者之间没有依赖,最好不要承诺顺序;若确实有先后要求,可以使用 order、优先级、阶段分组或事件拆分。顺序一旦影响正确性,就说明观察者之间存在隐式依赖,需要考虑是否应该回到主流程、责任链或编排服务。

详细版

观察者模式强调发布方和订阅方解耦,但解耦不等于通知顺序可以随便。比如用户注册成功后,有 3 个观察者:

  1. 初始化用户画像;
  2. 发放新人券;
  3. 发送欢迎短信。

如果短信内容依赖新人券发放结果,那么“发券必须在短信之前”。这时顺序就是业务契约,不能靠 List 原始顺序或 Bean 扫描顺序碰运气。

常见做法有:

  • 无依赖观察者:不承诺顺序,互相独立;
  • 弱顺序要求:使用 @Orderorder()
  • 强流程要求:把关键步骤放回主流程或编排服务;
  • 多阶段要求:先发布 UserRegisteredEvent,再发布 CouponGrantedEvent

记忆钩子:观察者模式适合“并列响应”,不适合把强依赖流程藏在一堆监听器顺序里。

完整版教学

面试提示:通知顺序题要先判断业务是否真的依赖顺序,再说明同步、异步、分组和补偿方案。

场景顺序要求常见方案
审计日志通常较弱异步监听,允许最终一致
库存扣减后发券局部较强拆分事务边界或显式编排
多监听器共享状态较强且危险尽量消除共享状态,必要时引入流程编排

一、为什么通知顺序是高频追问

观察者模式常用来处理“一件事发生后,多方响应”。但多个响应动作之间是否有顺序,决定了设计复杂度。

例如订单支付成功后:

OrderPaidEvent
  -> addPoints
  -> sendSms
  -> issueInvoice
  -> notifyWarehouse

如果这些动作都只依赖“订单已支付”这个事实,顺序不重要;如果 sendSms 必须展示积分到账结果,那么它就依赖 addPoints

二、无依赖观察者最好不承诺顺序

如果观察者之间没有依赖,最好的设计是让它们互相独立:

interface EventListener<E> {
    void onEvent(E event);
}

发布方只保证“事件会通知给订阅者”,不保证谁先谁后。这样新增观察者不会影响旧观察者,也符合观察者模式的解耦目标。

无顺序承诺的好处是:

  • 更容易并行执行;
  • 新增监听器风险小;
  • 不会让调用方依赖隐藏顺序;
  • 更容易迁移到消息队列或事件总线。

三、弱顺序可以使用优先级

如果确实需要先后顺序,可以显式建模:

interface OrderedListener<E> {
    int order();
    void onEvent(E event);
}

发布时排序:

listeners.stream()
    .sorted(Comparator.comparingInt(OrderedListener::order))
    .forEach(listener -> listener.onEvent(event));

也可以在 Spring 中使用 @Order。但要注意:@Order 更像技术顺序,不应该承载过多核心业务流程。

四、强依赖流程不适合藏在观察者里

如果 B 必须依赖 A 的执行结果,甚至 A 失败时 B 不能执行,那么它们不再是简单观察者关系。

例如:

扣库存成功 -> 创建物流单 -> 发送发货短信

这更像主业务流程或责任链。把它们拆成 3 个监听器,并靠 order 保证顺序,会让调用链很隐蔽,排查困难。

这时可以改为:

inventoryService.lock(order);
logisticsService.create(order);
smsService.send(order);

或者使用明确的编排服务。

五、可以用事件拆分表达阶段

观察者顺序复杂时,可以拆成多个事件:

UserRegisteredEvent
  -> InitProfileListener
  -> GrantCouponListener

CouponGrantedEvent
  -> SendWelcomeSmsListener

短信不再依赖“注册事件中的另一个监听器先执行”,而是订阅“优惠券已发放”这个更明确的事实。

六、异步通知下顺序更难保证

同步通知可以在单线程内排序,异步通知则要考虑:

  • 多线程执行顺序不稳定;
  • 消息队列可能按分区保证局部顺序;
  • 重试可能导致晚到事件覆盖早到事件;
  • 消费端并发会破坏处理顺序。

如果业务要求同一订单事件严格有序,常见做法是按 orderId 分区,保证同一 key 进入同一队列分区。

七、顺序设计要可测试和可观测

如果顺序是契约,就要写测试:

given OrderPaidEvent
when publish
then listeners execute in [points, coupon, sms]

线上也要记录:

eventId=E100
listeners=[points:success:12ms, coupon:success:20ms, sms:success:8ms]

否则顺序问题只会在故障时变成“谁也说不清”。

八、常见误区与追问

  • 误区:观察者的执行顺序天然等于注册顺序。 不同集合、容器、框架和异步执行器都可能改变顺序。
  • 误区:加一个 @Order 就能解决所有业务依赖。 强依赖流程应该显式编排,不应藏在监听器顺序中。
  • 误区:顺序越明确越好。 无依赖场景不要承诺顺序,否则会增加无意义约束。
  • 误区:异步观察者也能轻松保证全局顺序。 异步只能在特定 key、分区或串行消费下保证局部顺序。
  • 追问:监听器之间有依赖怎么办? 优先拆事件或改成编排流程,弱依赖才用优先级。
  • 追问:Spring 中怎么控制监听器顺序? 可用 @Order 或实现 Ordered,但要明确这是技术顺序,不应滥用。
  • 追问:顺序问题怎么排查? 打印 eventId、listener 名称、开始结束时间、执行线程和结果。

九、加强记忆

  1. 无依赖观察者不承诺顺序。
  2. 弱顺序用 order 或优先级显式表达。
  3. 强依赖流程不要藏在监听器里。
  4. 顺序复杂时可以拆分阶段事件。
  5. 异步只能在特定条件下保证局部顺序。
  6. 顺序一旦是契约,就要测试和日志证明。