观察者模式中多个观察者的通知顺序如何设计?
简化版
多个观察者的通知顺序不能依赖集合遍历的偶然结果,要根据业务语义显式设计。若观察者之间没有依赖,最好不要承诺顺序;若确实有先后要求,可以使用 order、优先级、阶段分组或事件拆分。顺序一旦影响正确性,就说明观察者之间存在隐式依赖,需要考虑是否应该回到主流程、责任链或编排服务。
详细版
观察者模式强调发布方和订阅方解耦,但解耦不等于通知顺序可以随便。比如用户注册成功后,有 3 个观察者:
- 初始化用户画像;
- 发放新人券;
- 发送欢迎短信。
如果短信内容依赖新人券发放结果,那么“发券必须在短信之前”。这时顺序就是业务契约,不能靠 List 原始顺序或 Bean 扫描顺序碰运气。
常见做法有:
- 无依赖观察者:不承诺顺序,互相独立;
- 弱顺序要求:使用
@Order或order(); - 强流程要求:把关键步骤放回主流程或编排服务;
- 多阶段要求:先发布
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 名称、开始结束时间、执行线程和结果。
九、加强记忆
- 无依赖观察者不承诺顺序。
- 弱顺序用 order 或优先级显式表达。
- 强依赖流程不要藏在监听器里。
- 顺序复杂时可以拆分阶段事件。
- 异步只能在特定条件下保证局部顺序。
- 顺序一旦是契约,就要测试和日志证明。