4微服务架构设计模式

使用消息传递

异步通信模式

消息通道发布-订阅事件溯源

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第4章:使用消息传递(Using Messaging for Communication)

一、导读

1.1 本章学习目标

异步消息传递是微服务解耦的关键手段。第4章深入讲解消息传递的两种模型、主流消息代理的选型、以及事件驱动架构中的高级概念。Richardson 通过大量实例展示了如何用消息传递实现服务间的松耦合协作。

本章的核心学习目标包括:

  • 掌握两种消息模型:点对点(队列)和发布/订阅(主题)的区别和适用场景。
  • 学会消息代理选型:Kafka vs RabbitMQ vs ActiveMQ 的特点和适用场景。
  • 理解事件溯源(Event Sourcing):以事件序列作为系统的真实数据源。
  • 理解 CQRS 模式:命令查询职责分离的设计思想。
  • 掌握消息可靠性保障:至少一次、恰好一次、至多一次语义的实现。
  • 理解消息顺序性:分区有序 vs 全局有序的权衡。

1.2 为什么本章如此重要

Richardson 在书中反复强调:微服务架构中,异步消息传递是实现服务解耦的最有力工具。同步调用(REST/gRPC)虽然简单,但它要求调用方和被调用方同时可用,形成了时间上的耦合。而异步消息传递通过消息队列解耦了这种时间依赖——发送方发送消息后可以立即返回,接收方可以在任何时间处理消息。

本章还引入了事件驱动架构(EDA)的两个核心模式——事件溯源和 CQRS。这两个模式是构建高可扩展性微服务的关键技术,它们从根本上改变了数据的存储和查询方式。

1.3 核心问题引导

点对点和发布/订阅消息模型分别适用于什么场景?

Kafka 和 RabbitMQ 的核心设计差异是什么?如何选型?

事件溯源解决了什么问题?它有什么代价?

CQRS 模式中,命令端和查询端如何保持数据同步?

如何保证消息不丢失、不重复、按顺序处理?

事务消息(Transactional Messaging)如何保证业务操作和消息发送的原子性?


二、核心概念详解

2.1 消息传递的两种模型

2.1.1 点对点模型(Point-to-Point)

消息发送到队列(Queue),只被一个消费者处理。适合任务分发场景。

// 点对点模型:
// [Producer] → [Queue] → [Consumer 1]  (消息只被一个消费者处理)
//                         [Consumer 2]
//                         [Consumer 3]
//
// 特点:
// 1. 消息被消费后即删除(或标记为已消费)
// 2. 多个消费者竞争消费队列中的消息
// 3. 每条消息只被处理一次(在单个消费者组内)
//
// 适用场景:
// - 任务队列:订单处理、邮件发送、图片转码
// - 工作分发:多个 Worker 竞争处理任务
// - 削峰填谷:突发流量通过队列缓冲

2.1.2 发布/订阅模型(Publish/Subscribe)

消息发送到主题(Topic),被所有订阅者接收。适合事件通知场景。

// 发布/订阅模型:
// [Publisher] → [Topic] → [Subscriber A]  (所有订阅者都收到消息)
//                         → [Subscriber B]
//                         → [Subscriber C]
//
// 特点:
// 1. 消息不被删除(保留一段时间)
// 2. 所有订阅者都收到消息的副本
// 3. 订阅者之间互不影响
//
// 适用场景:
// - 事件通知:订单创建通知支付、库存、配送服务
// - 数据同步:用户信息变更同步到多个下游系统
// - 审计日志:所有操作记录到审计日志服务

2.2 消息代理选型

2.2.1 Apache Kafka

Kafka 是一个高吞吐量的分布式日志系统,最初由 LinkedIn 开发。

// Kafka 的核心设计:
// 1. Topic 分为多个 Partition(分区)
// 2. 每个 Partition 是一个有序的、不可变的消息日志
// 3. 消息按 Offset(偏移量)定位
// 4. Consumer Group 内的 Consumer 分配不同的 Partition

// Kafka 的优势:
// - 超高吞吐量(百万级消息/秒)
// - 消息持久化(可回溯历史消息)
// - Consumer 控制消费进度(可重新消费)
// - 分区有序(同一 Key 的消息在同一分区内有序)

// Kafka 的局限:
// - 不支持复杂的路由规则
// - 消息确认机制相对简单
// - 不适合低延迟场景(批量发送优化吞吐)

// 典型应用场景:
// - 日志收集
// - 事件流处理
// - 数据管道
// - 微服务事件总线

2.2.2 RabbitMQ

RabbitMQ 是一个功能丰富的消息代理,支持灵活的路由规则。

// RabbitMQ 的核心设计:
// 1. Producer 发送消息到 Exchange(交换机)
// 2. Exchange 根据路由规则将消息分发到 Queue
// 3. Consumer 从 Queue 消费消息

// Exchange 类型:
// - Direct:精确匹配路由键
// - Fanout:广播到所有绑定的队列
// - Topic:通配符匹配路由键(*.order、#.payment)
// - Headers:基于消息头属性路由

// RabbitMQ 的优势:
// - 灵活的路由规则
// - 丰富的消息确认机制
// - 支持优先级队列、延迟消息
// - 低延迟(单消息处理)

// RabbitMQ 的局限:
// - 吞吐量不如 Kafka
// - 消息持久化性能不如 Kafka
// - 不支持消息回溯

// 典型应用场景:
// - 任务队列
// - 复杂路由场景
// - 需要消息确认的场景
// - 需要延迟消息的场景

2.2.3 选型对比

特性KafkaRabbitMQActiveMQ
吞吐量极高(百万/秒)中等(万/秒)低(千/秒)
延迟毫秒级微秒级毫秒级
消息路由简单(Topic+Partition)灵活(多种 Exchange)灵活
消息回溯支持(按 Offset)不支持不支持
消息确认基础丰富丰富
协议自定义AMQPAMQP, OpenWire
适用场景事件流、日志任务队列、复杂路由企业集成

2.3 事件溯源(Event Sourcing)

2.3.1 核心思想

传统的数据存储方式是保存当前状态(CRUD)。事件溯源则保存导致状态变更的事件序列——当前状态是事件序列的重放结果。

// 传统 CRUD 方式:
// 数据库保存订单的当前状态
// UPDATE orders SET status = 'PAID' WHERE id = 123
// → 旧状态被覆盖,无法追溯变更历史

// 事件溯源方式:
// 数据库保存订单的所有事件
// Event 1: OrderCreated(orderId=123, customerId=456, items=[...])
// Event 2: OrderApproved(orderId=123, approvedBy=system)
// Event 3: PaymentProcessed(orderId=123, amount=99.99)
// Event 4: OrderConfirmed(orderId=123)
//
// 当前状态 = 重放所有事件
// OrderCreated → OrderApproved → PaymentProcessed → OrderConfirmed
// 最终状态:CONFIRMED

// 事件溯源的优势:
// 1. 完整的审计日志(每个状态变更都有记录)
// 2. 可以回溯到任意时间点的状态
// 3. 可以派生出新的视图(不需要修改事件)
// 4. 天然支持 CQRS

// 事件溯源的代价:
// 1. 查询当前状态需要重放事件(性能问题 → 需要物化视图)
// 2. 事件 Schema 演化复杂(旧事件需要兼容新格式)
// 3. 学习曲线陡峭
// 4. 不适合所有场景(简单 CRUD 不需要事件溯源)

2.3.2 事件存储

// 事件存储的数据库设计:
CREATE TABLE events (
    event_id VARCHAR(64) PRIMARY KEY,
    aggregate_type VARCHAR(64) NOT NULL,   -- 如 'Order'
    aggregate_id VARCHAR(64) NOT NULL,     -- 如 '123'
    event_type VARCHAR(64) NOT NULL,       -- 如 'OrderCreated'
    event_data JSONB NOT NULL,             -- 事件数据
    event_metadata JSONB,                  -- 元数据(用户ID、时间戳等)
    event_version INT NOT NULL,            -- 事件版本号
    created_at TIMESTAMP DEFAULT NOW(),
    UNIQUE(aggregate_type, aggregate_id, event_version)
);

CREATE INDEX idx_aggregate ON events(aggregate_type, aggregate_id);
CREATE INDEX idx_created_at ON events(created_at);

// 查询某个订单的所有事件:
SELECT * FROM events
WHERE aggregate_type = 'Order' AND aggregate_id = '123'
ORDER BY event_version;

2.4 CQRS 模式

2.4.1 核心思想

CQRS(Command Query Responsibility Segregation)将系统的命令端(写操作)和查询端(读操作)分离为不同的模型。

// 传统方式:同一个模型处理读写
// [Client] → [Service] → [Database]
// 读和写使用同一个数据模型

// CQRS 方式:读写分离
// 命令端(Write Side):
// [Client] → [Command Service] → [Event Store]
//   → 处理业务逻辑,产生事件
//
// 查询端(Read Side):
// [Client] → [Query Service] → [Read Database]
//   → 直接查询物化视图,无需业务逻辑
//
// 同步:
// [Event Store] → [Event Handler] → [Read Database]
//   → 订阅事件,更新物化视图

// CQRS 的优势:
// 1. 读写性能独立优化
// 2. 读模型可以针对查询优化(如反范式化)
// 3. 写端保证业务一致性,读端保证查询效率
// 4. 天然支持事件溯源

// CQRS 的代价:
// 1. 架构复杂度增加
// 2. 读写数据可能暂时不一致
// 3. 需要维护事件处理器

2.5 消息可靠性

2.5.1 三种消息语义

// At-most-once(至多一次):
// 消息可能丢失,但不会重复
// 实现:发送后不确认
// 适用:可容忍丢失的场景(如日志收集)

// At-least-once(至少一次):
// 消息不会丢失,但可能重复
// 实现:发送后等待确认,超时重发
// 适用:不能丢失但可去重的场景(如订单处理)
// 消费者需要实现幂等性

// Exactly-once(恰好一次):
// 消息不丢失且不重复
// 实现:需要分布式事务或幂等机制
// Kafka 的幂等 Producer + 事务可实现
// 适用:金融交易等严格场景

2.5.2 消息顺序性

// 全局有序:
// 所有消息按顺序处理
// 需要单分区 → 吞吐量受限
// 适用:银行流水等严格有序场景

// 分区有序:
// 同一 Key 的消息在同一分区内有序
// 不同 Key 的消息可以并行处理
// 例:同一订单的事件有序,不同订单的事件可以并行
//
// Kafka 实现:
// Producer 发送消息时指定 Key
// producer.send({
//   topic: 'order-events',
//   key: 'order-123',    // 同一订单的消息进入同一分区
//   value: '{"type":"OrderCreated",...}'
// });

三、实战案例

3.1 案例一:Kafka 事件流实现

javascript
// Producer:订单服务发布事件
const { Kafka } = require('kafkajs');
const kafka = new Kafka({ brokers: ['kafka:9092'] });
const producer = kafka.producer();

async function createOrder(orderData) {
  await producer.connect();
  
  const order = await orderRepository.save(orderData);
  
  await producer.send({
    topic: 'order-events',
    messages: [{
      key: order.id,
      value: JSON.stringify({
        type: 'OrderCreated',
        orderId: order.id,
        customerId: order.customerId,
        restaurantId: order.restaurantId,
        items: order.items,
        totalAmount: order.totalAmount,
        timestamp: Date.now()
      })
    }]
  });
  
  return order;
}

// Consumer:支付服务消费事件
const consumer = kafka.consumer({ groupId: 'payment-service' });

async function startPaymentProcessor() {
  await consumer.connect();
  await consumer.subscribe({ topic: 'order-events', fromBeginning: false });
  
  await consumer.run({
    eachMessage: async ({ topic, partition, message }) => {
      const event = JSON.parse(message.value.toString());
      
      switch (event.type) {
        case 'OrderCreated':
          await processPayment(event);
          break;
        case 'OrderCancelled':
          await refundPayment(event);
          break;
      }
    }
  });
}

3.2 案例二:事件溯源 + CQRS 实现

java
// 命令端:订单聚合根
public class Order {
    private String id;
    private String customerId;
    private List<OrderItem> items;
    private OrderStatus status;
    private List<DomainEvent> changes = new ArrayList<>();

    public static Order create(String id, String customerId, List<OrderItem> items) {
        Order order = new Order();
        order.apply(new OrderCreatedEvent(id, customerId, items));
        return order;
    }

    public void approve() {
        if (status != OrderStatus.CREATED) {
            throw new IllegalStateException("Cannot approve order in status: " + status);
        }
        apply(new OrderApprovedEvent(id));
    }

    public void cancel() {
        if (status == OrderStatus.DELIVERED) {
            throw new IllegalStateException("Cannot cancel delivered order");
        }
        apply(new OrderCancelledEvent(id));
    }

    private void apply(DomainEvent event) {
        changes.add(event);
        mutate(event);
    }

    private void mutate(DomainEvent event) {
        if (event instanceof OrderCreatedEvent e) {
            this.id = e.orderId();
            this.customerId = e.customerId();
            this.items = e.items();
            this.status = OrderStatus.CREATED;
        } else if (event instanceof OrderApprovedEvent) {
            this.status = OrderStatus.APPROVED;
        } else if (event instanceof OrderCancelledEvent) {
            this.status = OrderStatus.CANCELLED;
        }
    }

    public List<DomainEvent> getUncommittedEvents() {
        return changes;
    }
}

// 查询端:物化视图
public class OrderView {
    private String orderId;
    private String customerId;
    private String status;
    private BigDecimal totalAmount;
    private LocalDateTime createdAt;
}

// 事件处理器:将事件投影到物化视图
@Component
public class OrderViewProjector {
    @Autowired
    private OrderViewRepository repository;

    @KafkaListener(topics = "order-events", groupId = "view-projector")
    public void handleEvent(String eventJson) {
        DomainEvent event = deserialize(eventJson);
        
        if (event instanceof OrderCreatedEvent e) {
            OrderView view = new OrderView();
            view.setOrderId(e.orderId());
            view.setCustomerId(e.customerId());
            view.setStatus("CREATED");
            view.setTotalAmount(calculateTotal(e.items()));
            view.setCreatedAt(LocalDateTime.now());
            repository.save(view);
        } else if (event instanceof OrderApprovedEvent e) {
            repository.findByOrderId(e.orderId())
                .ifPresent(v -> { v.setStatus("APPROVED"); repository.save(v); });
        }
    }
}

3.3 案例三:事务消息(Outbox Pattern)

java
// Outbox 模式:保证业务操作和消息发送的原子性
// 问题:如何保证"保存订单"和"发送 OrderCreated 事件"的原子性?
// 方案:将事件写入同一个数据库的 Outbox 表

@Transactional
public Order createOrder(CreateOrderCommand command) {
    Order order = Order.create(command.getId(), command.getCustomerId(), command.getItems());
    orderRepository.save(order);
    
    // 将事件写入 Outbox 表(同一个事务)
    OutboxEvent event = new OutboxEvent();
    event.setAggregateType("Order");
    event.setAggregateId(order.getId());
    event.setEventType("OrderCreated");
    event.setPayload(serialize(order.getUncommittedEvents()));
    outboxRepository.save(event);
    
    return order;
}

// 独立的 Event Publisher 进程定期扫描 Outbox 表
// 将事件发送到 Kafka,发送成功后删除 Outbox 记录
@Scheduled(fixedDelay = 1000)
public void publishEvents() {
    List<OutboxEvent> events = outboxRepository.findUnpublished();
    for (OutboxEvent event : events) {
        kafkaProducer.send(event.getAggregateType(), event.getPayload());
        outboxRepository.markPublished(event.getId());
    }
}

四、本章小结

本章深入讲解了微服务中的消息传递机制:

核心知识点:

  • 点对点模型适合任务分发,发布/订阅模型适合事件通知。
  • Kafka 适合高吞吐事件流,RabbitMQ 适合灵活路由和任务队列。
  • 事件溯源以事件序列为真实数据源,提供完整审计和回溯能力。
  • CQRS 将读写分离,命令端处理业务逻辑,查询端优化查询效率。
  • 消息可靠性分为至多一次、至少一次、恰好一次三种语义。
  • Outbox 模式保证业务操作和消息发送的原子性。

五、思考与练习

思考题

在你的项目中,哪些场景适合使用事件溯源?哪些不适合?

CQRS 模式中读写不一致的窗口期如何处理?

Kafka 的分区有序特性如何影响你的消息 Key 设计?

练习题

搭建 Kafka 集群,实现订单创建 → 支付处理 → 库存扣减的事件流。

实现一个 Outbox 模式的 Event Publisher,保证业务操作和消息发送的原子性。

设计一个 CQRS 架构:命令端使用事件溯源,查询端使用物化视图。

4/12使用消息
0%