分布式系统不同模块之间通信,除了远程服务调用(RPC),**消息中间件(消息队列)**是另一个重要手段。本文整合消息队列的核心基础:应用场景、两种消费模型与集群/广播消费、以及顺序消费与幂等这两大消费端难点。
消息队列(Message Queue)是传递消息的队列。参与消息传递的双方是生产者(Producer) 和 消费者(Consumer),两者可以是单实例或集群部署:
现代消息队列(Kafka、RocketMQ、RabbitMQ 等)在其基础上增加了持久化、集群、副本、消费组等能力,成为分布式系统的关键基础设施。
系统解耦:消息队列在系统中隐含地加入一个"对外扩展接口",方便业务解耦:调用方只需发送消息,无需关注下游逻辑如何执行。与 RPC 相比,RPC 需在调用方显式编码业务逻辑、耦合度高;消息队列实现依赖倒转,系统之间更独立,符合开闭原则。
异步处理:将非即时必需的请求异步化,提升响应速度与吞吐。典型场景是流量削峰:秒杀等场景瞬时流量极高,通过消息队列 + 限流,把超过系统阈值的请求暂存到队列,待高峰过去再慢慢处理,平滑流量曲线。
请求缓冲:利用队列做缓冲层,平滑各业务系统处理性能的差异,类似早期企业集成中的 **ESB(企业服务总线)**思想——在生产者与消费者之间垫一层缓冲,避免强同步导致的对性能差敏感。
数据分发:基于发布订阅的一对多广播机制实现数据分发。典型场景:订阅数据库 binlog——主库 binlog 只有一份,但下游可能有文件索引、离线数据库等多个消费者,通过 MQ 把 binlog 数据广播分发到各消费方。
此外,消息队列还可用于实现分布式事务(本地消息表 + MQ)。
点对点模型(Point-to-Point):生产者向特定队列发布消息,消费者从队列读取,每条消息只被一个消费者处理一次。特点是有明确"独占"消费关系,类比一对一工作队列。
发布/订阅模型(Publish/Subscribe):消费者通过 **Topic(主题)**订阅消息,生产者发布到该 Topic,一条消息可被多个消费者各自消费一次。特点是一对多广播,类比订阅频道。两者核心区别:消息能否被多个消费者消费。
JMS 与 AMQP:JMS(Java Message Service)定义点对点与发布订阅两种基础模型(ActiveMQ 是典型实现);AMQP 是跨语言协议,额外引入 Exchange + Binding 角色(生产者先发到 Exchange,由 Binding 路由到不同队列),支持 direct、topic 等模型(RabbitMQ 是典型实现)。
推模式与拉模式:Broker 主动把消息送达消费者是推模式(RabbitMQ),消费节奏由 Broker 驱动,消费者处理慢时易被压垮;消费者主动请求拉取是拉模式(Kafka 的 poll),消费速率由消费端掌控。RocketMQ 采用**长轮询(Long Polling)**折中:消费者发起请求后无消息时 Broker 挂起连接,有消息立即返回,兼顾实时性与主动权。
Kafka 基于发布订阅模型,核心角色:
| 角色 | 说明 |
|---|---|
| Producer | 生产者,发布消息到 Broker |
| Consumer | 消费者,从 Broker 读取消息 |
| Broker | 消息存储与处理节点,集群由一个或多个 Broker 组成 |
| Topic | 消息分类主题,每条消息必须指定 Topic |
| ConsumerGroup | 消费组,对消费端分组;每个消费者属于一个消费组 |
Kafka 消费特点:基于 Topic 的发布订阅,消息持久化、消费后不立即删除、保留历史,天然支持多消费者订阅。早期用 ZooKeeper 维护 Offset 与节点管理(新版本已用 KRaft 替代)。
RocketMQ 也是典型的发布订阅模型,由 NameServer、Broker、Producer、Consumer 构成。相比 Kafka 的额外机制:
RocketMQ 的消费模式分两种:
集群消费(Cluster):同一消费组内的消息,任意一条只被其中一个消费者处理——对点对点模型的扩展。消费组内各实例通过负载均衡分配消息,保证消息不重复分发。负载均衡策略(AllocateMessageQueueStrategy):平均分配(默认)、环形分配、手动配置、机房分配、一致性哈希分配。
广播消费(Broadcast):消费组中的消息会被组内每个消费者分别消费一次——发布订阅模型。所有订阅了该 Topic 的实例都能收到全量消息。场景:需要所有节点都感知的数据同步,如配置更新、公共缓存刷新。
集群消费 vs 广播消费:
| 维度 | 集群消费 | 广播消费 |
|---|---|---|
| 消息处理 | 每条消息只被组内一个实例消费 | 每条消息被组内所有实例消费 |
| 语义 | 点对点扩展(竞争消费) | 发布订阅(广播复制) |
| 负载均衡 | 需要(分配消息到实例) | 不需要(全员消费) |
| 适用 | 任务处理、业务消息(一次处理) | 配置同步、缓存刷新(全员感知) |
消息队列中的队列是有序数据结构,消息传递顺序,但绝对有序性在分布式场景下很难保证:
结论:保证消息绝对有序非常困难,除非在服务器内部且一个生产者对应一个消费者——但这种场景无法应用于实际业务。
从消息队列自身角度分全局有序和局部有序。全局有序无法用多分区做性能优化,实际开发一般应用局部有序:把业务消息分发到固定的分区(单个队列内),实现业务上的有序要求。
Kafka 顺序消息:Kafka 保证消息在 Partition 内的顺序,需要确保顺序的消息发送到同一个 Partition 即可。单分区天然有序;多分区通过分发策略(如按订单 ID)把同一类消息分发到同一 Partition。消息失败重发场景下,开启幂等生产者(enable.idempotence=true)后 max.in.flight.requests.per.connection ≤ 5 仍能保证分区有序;不开启幂等时需将其设为 1。
RocketMQ 顺序消息:与 Kafka 类似,RocketMQ 保证消息在同一个 Queue 内的顺序(满足队列 FIFO)。把对应一个业务主键的消息路由到同一个 Queue 即可实现有序传输,且 RocketMQ 额外支持 Tag 进一步拆分。
考虑顺序是否必要:解决一个问题,正面没有好方案时,考虑是否绕过它。例如订单状态消息流转场景,若功能是根据发货状态进行物流通知,因为订单状态单调不可逆,忽略顺序、只关注"最后是否已发货",就不需要关心消费时序。
业务上实现有序的几种方式:
总结:绝对的时间有序实现困难且实际意义不大。消息队列只是消息传输的解决方案,不是银弹。一般通过业务场景合理设计实现业务上的有序性。
幂等不是"问题",而是业务的一个特性。幂等问题体现在:对不满足幂等性的业务,在消息重复消费或远程调用失败重试时,出现数据不一致、业务错乱。
幂等源自数学概念:用相同参数执行多次,结果一致。以 HTTP 方法为例:GET、DELETE 是幂等的(多次调用不影响资源结果),POST 不是。注意:业务幂等指操作不影响资源本身,而非每次读取结果都一致。
对数据库 CRUD:Create 不幂等(重复插入会重复);Update 可能幂等也可能不幂等:
-- 幂等:结果固定
UPDATE order SET status = 1 WHERE id = 100;
-- 不幂等:每执行一次 price 就 +1
UPDATE order SET price = price + 1 WHERE id = 100;Read / Delete 幂等。
| 投递语义 | 含义 | 特点 |
|---|---|---|
| At most once | 最多投递一次 | 消息可能丢,但绝不重复;适合可容忍丢失的场景(日志、监控) |
| At least once | 至少投递一次 | 消息不丢,但可能重复;绝大多数 MQ 默认支持,应用最广 |
| Exactly once | 恰好投递一次 | 不丢也不重复;成本极高,通用方案几乎不可能 |
绝大多数业务采用 At least once(保证不丢,允许重复),因此消费端必须通过幂等来规避重复投递带来的问题。RocketMQ 通过消费端 ACK 机制实现:消息消费完成后才返回 ACK,未消费则不 ACK,从而保证不丢(但可能重复)。
天然幂等的业务:部分业务天然幂等(如设置状态、幂等 Update),允许重复调用,可直接配合 MQ 重试提升成功率。
数据库去重(唯一约束/去重表):利用数据库唯一性保证幂等。如根据订单流转消息写订单 Log 表,把订单 ID + 修改时间戳设为唯一索引。重复消费时,除第一条外后续写入都会因唯一约束失败,从而保证最终一致。
全局唯一消息 ID / 业务 ID:给每条业务消息附加唯一 ID,消费端记录已消费 ID 实现去重:消息携带唯一 ID → 消费前检查是否已处理(查 Redis 或去重表)→ 用分布式锁 / SETNX 保证并发下同一 ID 只处理一次 → 处理完成记录已消费,后续重复消息跳过。
状态机校验:对有明显状态流转的业务(订单:待支付→已支付→已发货),消费时校验当前状态——只有符合预期状态才执行更新,避免重复操作推进错误状态。
消息消费重试与微服务调用重试本质相同——都是通过重试解决网络抖动导致的偶发失败。Dubbo 等 RPC 框架提供集群容错(Failover 失败重试、Failfast 快速失败等),写操作重试必须幂等,否则会重复下单/扣款。