微服务通信模式选型
微服务间怎么通信,是架构设计最先要定的事。选错模式,后面改造成本极高。本文给出完整决策框架:三种模式对比、同步/异步取舍、一致性保证。
三大通信模式
模式全景
| 模式 | 交互方式 | 代表技术 | 耦合度 | 一致性 |
|---|---|---|---|---|
| RPC 同步调用 | 请求-响应 | OpenFeign、Dubbo、gRPC | 紧耦合 | 强一致 |
| 消息异步 | 发送-接收 | Kafka、RabbitMQ、RocketMQ | 松耦合 | 最终一致 |
| 事件驱动 | 发布-订阅 | Kafka、Stream、Spring Events | 最松 | 最终一致 |
RPC: 服务A ──请求──▶ 服务B ◀──响应── 服务A
消息: 服务A ──发消息──▶ 队列 ──消费──▶ 服务B
事件: 服务A ──发事件──▶ 事件总线 ──订阅──▶ 服务B/C/DRPC 同步调用
适用场景
- 实时性要求高(查询、下单确认)
- 逻辑强依赖(必须拿到结果才能继续)
- 交互简单(一问一答)
优点与缺点
优点:
✅ 模型简单直观,调试容易
✅ 天然同步,结果立等可取
✅ 分布式事务(XA)可用
缺点:
❌ 强耦合:调用方依赖服务地址与接口
❌ 链式调用放大风险:A→B→C→D,一个慢全部慢
❌ 扩展性差:无法削峰填谷
❌ 级联故障:下游挂了上游也跟着挂同步调用的典型问题
订单服务 ──▶ 库存服务
│ │
└──▶ 支付服务 ─┴──▶ 风控服务 ──▶ 营销服务任一环节慢,整个请求链都慢。一个下游超时 3 秒,上游所有线程都被占用。
如何用好 RPC
- 设置超时(连接/读取)与重试策略
- 熔断降级(Sentinel / Resilience4j)
- 控制链路深度(不要超过 3-4 跳)
- 弱依赖异步化(营销、日志、通知都别同步调)
消息异步
适用场景
- 削峰填谷(秒杀、大促)
- 解耦生产/消费速率
- 不要求即时结果
- 需要可靠投递与重试
核心组件
生产者 ──▶ Broker(消息中间件)──▶ 消费者
│
├─ Topic/Queue(分区)
├─ 持久化
├─ 重试
└─ 死信队列优点与缺点
优点:
✅ 削峰:请求先进队列,消费端按能力拉取
✅ 解耦:生产者不关心谁消费
✅ 可靠:Broker 持久化 + ACK + 重试
✅ 异步:生产者发完即返回
缺点:
❌ 一致性弱:消费可能延迟,需最终一致
❌ 消息丢失/重复:需幂等 + 补偿
❌ 排错难:链路不直观,需跟踪消息
❌ 顺序性:多分区顺序难保证事件驱动
本质
事件驱动是消息模式的特化:发布者不指定消费者,只发布"发生了什么";订阅者按兴趣消费。
传统 RPC: 下单服务"调用"库存服务(命令式)
事件驱动: 下单服务"发布"订单已创建事件(声明式)
库存服务/通知服务/营销服务自行订阅事件与命令
| 概念 | 方向 | 语义 | 例子 |
|---|---|---|---|
| 命令(Command) | 明确目标 | 我要你做 X | 扣减库存 |
| 事件(Event) | 无目标 | 我已经做了 X | 订单已创建 |
事件驱动优点
- 服务间零直接依赖(新增订阅者不动发布者)
- 天然支持多消费者(一个事件多个服务响应)
- 天然支持回放(事件流可重放)
同步 vs 异步取舍
决策矩阵
| 场景特征 | 建议模式 | 原因 |
|---|---|---|
| 实时查询 | RPC | 需要立即结果 |
| 强一致性操作 | RPC + 分布式事务 | 必须同步确认 |
| 高流量写入 | 消息 | 削峰 |
| 跨服务通知 | 事件 | 解耦 |
| 长流程 | 消息 + 状态机 | 异步推进 |
| 大数据量处理 | 消息/事件 | 背压控制 |
混合架构(实际生产)
同步 RPC:核心链路(下单主流程)
│
├─ 扣库存(同步)
├─ 扣余额(同步)
└─ 发消息:订单已创建 ──▶ Kafka
├─▶ 发短信(异步)
├─▶ 更新搜索索引(异步)
└─▶ 通知营销(异步)原则:核心强依赖同步,周边弱依赖异步。
最终一致性保证
为什么异步只有最终一致
下单成功 → 发消息"订单已创建"
→ 库存服务稍后消费扣减
→ 在这中间,订单状态是"已创建但未扣库存"
→ 一段时间后达到一致三种实现方案
| 方案 | 原理 | 特点 |
|---|---|---|
| 本地消息表 | 业务 + 消息同库同事务,轮询发消息 | 简单可靠,需轮询 |
| 事务消息 | 半消息 + 回查(RocketMQ) | 无轮询,Broker 回查 |
| 事件溯源 | 事件为唯一事实源,重放 | 最强一致性,复杂 |
本地消息表
1. 业务事务内:写订单 + 写本地消息表(同库同事务)
2. 定时任务:扫描未发送消息 → 发到 MQ
3. 发送成功 → 标记已发送
4. 失败 → 重试(幂等)事务消息(RocketMQ)
1. 发送半消息(对消费者不可见)
2. 执行本地事务(下单)
3. 提交/回滚事务消息
4. 若步骤 3 未确认 → Broker 回查本地事务状态幂等消费(最终一致的基石)
消费侧必须幂等:
1. 数据库唯一键(订单号)
2. 状态机检查(只有待扣减状态才扣减)
3. 去重表(消费记录表)事件驱动实践框架
事件模型
java
// 事件基类
public abstract class DomainEvent {
private String eventId; // 事件唯一 ID(幂等用)
private String aggregateId; // 聚合 ID
private long timestamp;
}发布
java
// 业务代码里发布事件
@Service
public class OrderService {
@Transactional
public void createOrder(OrderRequest req) {
Order order = orderRepository.save(new Order(req));
// 同事务发布事件(本地消息表)
eventPublisher.publish(new OrderCreatedEvent(order.getId()));
}
}订阅
java
@Component
public class OrderEventListener {
@EventListener // Spring 事件
public void onOrderCreated(OrderCreatedEvent event) {
// 更新搜索索引、发短信等
}
}事件命名规范
<过去时动词> + <聚合>
订单已创建:OrderCreated
库存已扣减:StockDeducted
支付已完成:PaymentCompleted模式选型检查清单
选择通信模式前,回答这几个问题:
1. 调用方需要立即拿到结果吗?
是 → RPC 否 → 消息/事件
2. 消费者是谁?
明确单一 → 消息 多个/未来新增 → 事件
3. 数据一致性要求?
强一致 → RPC+事务 最终一致 → 消息
4. 流量是否波动大?
是 → 消息削峰 否 → RPC
5. 失败后需要重试吗?
需要 → 消息(可靠投递)常见问题
- RPC 链路太长怎么办? 拆异步:把非关键环节改消息;或 CQRS 拆分读写。
- 消息丢失如何兜底? Broker 持久化 + 生产端确认 + 消费端手动 ACK + 死信/补偿任务。
- 消息重复消费怎么处理? 消费侧幂等(唯一键/状态机/去重表)。
- 事件风暴如何避免? 明确事件边界,只发布"业务事实",不发布内部实现细节。
- RPC 与事件怎么共存? 主链路 RPC 保证体验,周边事件驱动解耦,二者互补不互斥。