第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 选型对比
| 特性 | Kafka | RabbitMQ | ActiveMQ |
|---|---|---|---|
| 吞吐量 | 极高(百万/秒) | 中等(万/秒) | 低(千/秒) |
| 延迟 | 毫秒级 | 微秒级 | 毫秒级 |
| 消息路由 | 简单(Topic+Partition) | 灵活(多种 Exchange) | 灵活 |
| 消息回溯 | 支持(按 Offset) | 不支持 | 不支持 |
| 消息确认 | 基础 | 丰富 | 丰富 |
| 协议 | 自定义 | AMQP | AMQP, 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 事件流实现
// 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 实现
// 命令端:订单聚合根
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)
// 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 架构:命令端使用事件溯源,查询端使用物化视图。