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 消息队列架构对比

观察者模式(进程内) 发布者(Subject) 内存消息总线 订阅者A 订阅者B 订阅者C ⚠ 同步阻塞 ⚠ 无持久化 ⚠ 单机局限 消息队列(跨进程) 生产者(Producer) 消息队列集群 RabbitMQ / Kafka 💾 磁盘持久化 / 分区 / 副本 消费者A 消费者B 消费者C ✅ 异步非阻塞 ✅ 消息持久化 ✅ 分布式扩展

什么时候用观察者模式?

我个人觉得,下面这些场景用观察者模式就够了:

  • GUI事件处理:按钮点击、鼠标移动,这些都在同一个UI线程里
  • 状态变更通知:比如用户登录后,更新多个UI组件
  • 轻量级事件总线:微服务内部模块间的简单通信
  • 单元测试:模拟依赖行为,验证调用关系

避坑指南:

我曾经在一个项目中,用观察者模式处理订单状态变更。结果订单量一上来,十几个订阅者串行执行,一个写数据库慢了几百毫秒,整个订单创建接口就超时了。后来改成消息队列,问题立刻解决。

记住:观察者模式不适合做「跨服务」和「高并发」的消息传递

什么时候上消息队列?

这些场景,我建议直接上MQ:

  • 异步处理:用户下单后,发短信、发邮件、更新积分,这些都不需要同步等待
  • 削峰填谷:秒杀场景下,瞬间流量太大,用MQ缓冲一下
  • 日志收集:多个服务产生的日志,统一发送到Kafka,再由Logstash消费
  • 事件溯源:记录所有状态变更事件,用于审计或回放
  • 跨语言/跨平台通信:Java服务发消息,Python服务消费,MQ是通用语言

两者能结合吗?

当然可以。我见过不少架构是这么玩的:

在微服务内部,用观察者模式做模块间通知。当需要跨服务通信时,通过MQ把消息发出去。这样既保证了内部的高效,又实现了外部的解耦。

举个例子:订单服务收到一个订单,内部用观察者模式通知「库存模块」和「支付模块」做本地处理。同时,通过Kafka把「订单已创建」事件发出去,让「物流服务」和「通知服务」去消费。

这种组合拳,才是实际项目中最常见的做法。

一句话总结:

观察者模式是「进程内的轻量级通知」,消息队列是「分布式的高可靠通信」。两者不是替代关系,而是互补关系。选哪个,取决于你的场景和需求。


公众号:蓝海资料掘金营,微信deep3321