21、观察者模式与消息队列:观察者模式与MQ(如RabbitMQ、Kafka)的异同
这个话题很有意思。很多刚入行的朋友会问我:「观察者模式和消息队列,看起来不都是发布-订阅吗?有什么区别?」
嗯,表面上看确实像。但实际用起来,完全是两码事。我当年在一个物联网项目中,一开始用观察者模式做设备状态同步,后来数据量一上来,整个系统差点崩了。最后换成了Kafka,才算稳住。
今天咱们就把这两个东西掰开揉碎,看看它们到底哪里像、哪里不像。
先说共同点:都是「发布-订阅」的思想
观察者模式和消息队列,核心都是解耦。一个对象(发布者)发出消息,多个对象(订阅者)接收并处理。发布者不需要知道订阅者是谁,订阅者也不需要关心消息从哪来。
说白了,就是「我不找你,你来找我」。这种思想在分布式系统里特别重要。
核心共同点:
- 都实现了发布者与订阅者的解耦
- 都支持一对多的消息分发
- 都遵循事件驱动的设计理念
再看不同点:本质差异很大
我习惯从三个维度来对比:通信方式、可靠性、应用场景。你想想看,观察者模式是在同一个进程里玩的,消息队列是跨进程、跨机器的。这能一样吗?
| 对比维度 | 观察者模式 | 消息队列(RabbitMQ/Kafka) |
|---|---|---|
| 通信范围 | 同一进程内(JVM内) | 跨进程、跨网络、跨机器 |
| 消息持久化 | 无,内存中传递 | 支持磁盘持久化 |
| 可靠性保证 | 无,订阅者挂了消息就丢了 | ACK机制、重试、死信队列 |
| 消息顺序 | 同步调用,顺序有保证 | 分区内有序,全局无序(Kafka) |
| 流量控制 | 无,发布者可能压垮订阅者 | 削峰填谷、限流、背压 |
| 扩展性 | 受限于单机内存和线程 | 支持集群、水平扩展 |
| 典型场景 | GUI事件、状态变更通知 | 异步任务、日志收集、事件溯源 |
看到这个表格,你应该能感受到差距了。观察者模式适合「轻量级、低延迟、同进程」的场景。消息队列则面向「分布式、高可靠、大数据量」的场景。
代码对比:一看就明白
咱们用Java写两个简单的例子,感受一下差异。
观察者模式实现(同一进程内)
// 观察者接口
interface Observer {
void update(String message);
}
// 被观察者(主题)
class Subject {
private List<Observer> observers = new ArrayList<>();
public void attach(Observer observer) {
observers.add(observer);
}
public void notifyObservers(String message) {
for (Observer observer : observers) {
observer.update(message); // 同步调用,阻塞
}
}
}
// 使用
Subject subject = new Subject();
subject.attach(msg -> System.out.println("订阅者A收到:" + msg));
subject.attach(msg -> System.out.println("订阅者B收到:" + msg));
subject.notifyObservers("订单已创建");
这段代码很简单。但注意一个问题:notifyObservers 是同步的。如果某个订阅者处理慢了,后面的都得等着。我在项目中就踩过这个坑——一个日志订阅者写文件太慢,导致主流程卡住了。
消息队列实现(跨进程)
// 生产者(发布者)
public class OrderProducer {
private KafkaTemplate<String, String> kafkaTemplate;
public void sendOrderCreated(String orderId) {
kafkaTemplate.send("order-topic", orderId);
// 发送完就返回,不等待消费者处理
}
}
// 消费者(订阅者)
@Component
public class OrderConsumer {
@KafkaListener(topics = "order-topic")
public void handleOrder(String orderId) {
System.out.println("处理订单:" + orderId);
// 处理失败会自动重试
}
}
看到了吗?生产者发送完消息就完事了。消费者哪怕挂了,消息还在Kafka里存着,重启后继续消费。这就是可靠性的差异。
我的建议:
如果你只是在单体应用里做模块间通知,用观察者模式就够了。一旦涉及跨服务、高并发、数据不丢,立刻上消息队列。别犹豫。
SVG:观察者模式 vs 消息队列架构对比
什么时候用观察者模式?
我个人觉得,下面这些场景用观察者模式就够了:
- GUI事件处理:按钮点击、鼠标移动,这些都在同一个UI线程里
- 状态变更通知:比如用户登录后,更新多个UI组件
- 轻量级事件总线:微服务内部模块间的简单通信
- 单元测试:模拟依赖行为,验证调用关系
避坑指南:
我曾经在一个项目中,用观察者模式处理订单状态变更。结果订单量一上来,十几个订阅者串行执行,一个写数据库慢了几百毫秒,整个订单创建接口就超时了。后来改成消息队列,问题立刻解决。
记住:观察者模式不适合做「跨服务」和「高并发」的消息传递。
什么时候上消息队列?
这些场景,我建议直接上MQ:
- 异步处理:用户下单后,发短信、发邮件、更新积分,这些都不需要同步等待
- 削峰填谷:秒杀场景下,瞬间流量太大,用MQ缓冲一下
- 日志收集:多个服务产生的日志,统一发送到Kafka,再由Logstash消费
- 事件溯源:记录所有状态变更事件,用于审计或回放
- 跨语言/跨平台通信:Java服务发消息,Python服务消费,MQ是通用语言
两者能结合吗?
当然可以。我见过不少架构是这么玩的:
在微服务内部,用观察者模式做模块间通知。当需要跨服务通信时,通过MQ把消息发出去。这样既保证了内部的高效,又实现了外部的解耦。
举个例子:订单服务收到一个订单,内部用观察者模式通知「库存模块」和「支付模块」做本地处理。同时,通过Kafka把「订单已创建」事件发出去,让「物流服务」和「通知服务」去消费。
这种组合拳,才是实际项目中最常见的做法。
一句话总结:
观察者模式是「进程内的轻量级通知」,消息队列是「分布式的高可靠通信」。两者不是替代关系,而是互补关系。选哪个,取决于你的场景和需求。
公众号:蓝海资料掘金营,微信deep3321