Update 消息队列.md

更新 RocketMQ 5.x 定时消息的上限与 Kafka 幂等生产者的说明,补充交换机类型、拉模式的原因与 ISR 和多数派的对比。
This commit is contained in:
CyC2018
2026-10-31 00:23:19 +08:00
parent eaa1e52670
commit 6d308b9d78
+8 -6
View File
@@ -211,12 +211,12 @@ RabbitMQ 的队列默认没有长度上限,积压的消息占用内存和磁
延迟消息指的是发送后不立即投递,等到指定时间才能被消费的消息。典型的场景是订单 30 分钟未支付自动取消:下单时发送一条 30 分钟后投递的消息,到时消费者检查订单,仍未支付就取消。相比[定时任务扫描全表](分布式.md#定时扫描与延迟消息),延迟消息只处理到期的订单。
延迟消息也可能丢失、晚到或重复,所以取消时以订单的当前状态为条件:UPDATE orders SET status = 'CANCELLED' WHERE id = 1024 AND status = 'UNPAID',支付回调同样带状态条件,取消后才到的支付成功要退款;要求高的业务再用低频扫描兜底。
延迟消息也可能丢失、晚到或重复,所以取消时以订单的当前状态为条件:UPDATE orders SET status = 'CANCELLED' WHERE id = 1024 AND status = 'UNPAID',支付回调同样带状态条件,取消后才到的支付成功要退款;要求高的业务再用低频扫描兜底。取消前先向支付平台关单,用户就不能在取消后再付款,见[超时关单](系统设计基础.md#超时关单)。
各消息队列的支持情况:
- RocketMQ 4.x 只支持 18 个固定的延迟级别:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h,消息先存入内部的定时主题,每个级别一个队列,同一队列中先写入的先到期,只需检查队头;
- RocketMQ 5.x 改用时间轮,支持指定任意的投递时间,默认最长 24 小时,精度为秒级;
- RocketMQ 5.x 改用时间轮,支持指定任意的投递时间,默认精度为 1 秒;最长时长由配置决定,开源 5.5.1 中经 Proxy 发送的默认 1 天、Broker 默认 3 天,超出时拒绝写入(以官方最新文档为准);
- RabbitMQ 常用的做法是给消息设置过期时间(TTL),过期后转入死信交换机,由死信队列的消费者处理。过期的消息到达队头才会被转出,前面 30 分钟的消息会挡住后面已到期的 1 分钟的消息,所以每种延迟时长单独建一个队列;4.4 起仲裁队列也支持延迟投递(以官方最新文档为准);
- Kafka 不支持延迟消息,需要自己实现,例如按延迟时长分成几个主题,消费者读到未到期的消息就暂停等待。
@@ -255,10 +255,12 @@ Kafka 把消息保存为按分区追加的日志,消息被消费后不会删
| --- | --- | --- | --- |
| 存储 | 分区日志,按保留时间删除,与是否被消费无关 | 队列,消息被确认后删除 | 所有主题的消息顺序写入同一个 CommitLog,每个队列另有消费索引,按保留时间删除 |
| 消费 | 消费者拉取,提交偏移量,可以把偏移量调回去重新消费 | Broker 主动推送给消费者,用 prefetch 限制未确认的消息数 | 消费者按队列拉取(Push 模式用长轮询实现),可以按时间重置消费位置 |
| 路由 | 生产者按 key 选择分区 | 交换机按路由键把消息投递到一个或多个队列,规则灵活 | 主题加 Tag 过滤 |
| 路由 | 生产者按 key 选择分区 | 交换机按类型和绑定规则把消息投递到一个或多个队列:direct 精确匹配,topic 按通配符匹配,fanout 广播给所有绑定的队列,headers 按消息首部匹配 | 主题加 Tag 过滤 |
| 业务功能 | 本身不支持延迟消息,死信只有共享组支持(4.4 起) | 死信交换机、TTL、优先级队列,4.4 起仲裁队列支持延迟消息 | 延迟消息、事务消息、消费重试和死信队列 |
| 适用场景 | 日志、埋点、数据同步、流处理等吞吐量大的场景 | 消息量不大、路由规则复杂的业务消息 | 订单、支付等需要事务消息、延迟消息和重试的业务消息 |
Kafka 选择拉模式,是因为推送的速率由 Broker 决定,需要额外的流控(例如 RabbitMQ 的 prefetch)才能匹配消费者的处理速度;拉取时消费者按自己的速度读取,一次可以拉走当前位置之后的一批消息。没有新消息时,拉取请求在 Broker 上等待数据到达(长轮询),避免空转。
RabbitMQ 也提供了只追加的 Stream 类型,消息确认后不删除,可以重复读取。
选型时先看业务需要什么:
@@ -273,7 +275,7 @@ Redis 也常被用作轻量的消息队列,但可靠性不如专门的消息
- Pub/Sub 不保存消息,发布时没有订阅者或者订阅者断线,消息就丢了,实测没有订阅者时 PUBLISH 返回 0;
- List 用 LPUSH 和 BRPOP 实现队列,消息取出后就从 List 中删除,消费者处理时崩溃就丢了,也不支持多个消费者组;
- Stream(Redis 5.0 起)支持消费者组和确认:XREADGROUP 读出的消息进入待确认列表,XACK 之后才移除,消费者崩溃时其它消费者可以用 XAUTOCLAIM 接管它没有确认的消息(见[附录 A4](#a4-redis-stream-的确认))。
- Stream(Redis 5.0 起)支持消费者组和确认:XREADGROUP 读出的消息进入待确认列表,XACK 之后才移除,消费者崩溃时其它消费者可以用 XCLAIM(6.2 起也可用 XAUTOCLAIM)接管它没有确认的消息(见[附录 A4](#a4-redis-stream-的确认))。
即使使用 Stream,数据也保存在内存中,默认配置下持久化和主从复制都是异步的,宕机时可能丢失最近写入的消息,能保存的消息量也受内存限制。消息量不大、能接受少量丢失时可以用 Redis,否则用专门的消息队列。
@@ -295,7 +297,7 @@ Kafka 4.2 起提供共享组(Share Group):一个分区的消息可以分
### 副本与可靠性
每个分区有多个副本,分布在不同的 broker 上,其中一个是 Leader,负责处理读写,其余的 Follower 不断从 Leader 拉取数据。与 Leader 保持同步的副本集合称为 ISR(In-Sync Replicas),Follower 超过 replica.lag.time.max.ms(默认 30 秒)没有追上 Leader 就会被移出 ISR。ISR 中所有副本都已写入的位置称为高水位(High Watermark, HW),消费者只能读到高水位之前的消息。Kafka 4.0 起完全移除了 ZooKeeper,Leader 选举等元数据由内置的 KRaft 控制器管理,它基于 [Raft](分布式.md#六raft) 协议实现。
每个分区有多个副本,分布在不同的 broker 上,其中一个是 Leader,负责处理读写,其余的 Follower 不断从 Leader 拉取数据。与 Leader 保持同步的副本集合称为 ISR(In-Sync Replicas),Follower 超过 replica.lag.time.max.ms(默认 30 秒)没有追上 Leader 就会被移出 ISR。ISR 中所有副本都已写入的位置称为高水位(High Watermark, HW),消费者只能读到高水位之前的消息。Kafka 4.0 起完全移除了 ZooKeeper,Leader 选举等元数据由内置的 KRaft 控制器管理,它基于 [Raft](分布式.md#六raft) 协议实现。分区数据的复制没有用 Raft 的多数派:多数派要有 2f+1 个副本,才能在 f 个副本故障后保住已提交的消息并选出新 Leader;Kafka 的消息要写入 ISR 中的全部副本才算提交,提交时 ISR 有 f+1 个副本,损失其中 f 个仍能保住这条消息。代价是提交要等 ISR 中最慢的副本;故障后能否继续写入还取决于下文的 min.insync.replicas。
例如创建 3 个分区、3 个副本的主题,然后用 kill -9 停掉 broker 3(3 个节点的 KRaft 集群实测):
@@ -324,7 +326,7 @@ Follower 落后几条消息时仍在 ISR 中,Leader 宕机后它可以成为
acks=all 要配合 min.insync.replicas 使用,否则 ISR 只剩 Leader 一个时,acks=all 就退化成了 acks=1。min.insync.replicas 设为 2 时,ISR 少于 2 个副本,broker 就拒绝 acks=all 的写入。上面停掉 broker 3 之后 ISR 还有 2 个副本,写入正常;把 min.insync.replicas 改为 3 再写入,生产者重试几次后报错 NotEnoughReplicasException;改用 acks=1 时写入成功,但高水位不前进,停掉的 broker 回到 ISR 后消费者才能读到这条消息(Kafka 4.4 实测,见[附录 A8](#a8-mininsyncreplicas-与-acks))。常用的配置是 3 个副本、min.insync.replicas=2、acks=all,一个副本故障时既不丢消息,也不停止写入。
生产者没有收到确认就会重试,这可能让同一条消息写入两次。Kafka 3.0 起默认开启幂等生产者:broker 给每个生产者分配一个 ID,生产者给发往每个分区的消息编上序号,broker 发现序号重复就丢弃。后文查看 .log 文件时,每个批次都带有 producerId: 1001 和 baseSequence 字段。幂等只保证单个分区内不重复;要让写入多个分区的消息同时成功或失败,需要使用事务,消费者设置 isolation.level=read_committed 后只读取已提交事务中的消息。
生产者没有收到确认就会重试,这可能让同一条消息写入两次。Kafka 3.0 起默认开启幂等生产者(3.0.0、3.1.0 因缺陷未生效):broker 给每个生产者分配一个 ID,生产者给发往每个分区的消息编上序号,broker 发现序号重复就丢弃。后文查看 .log 文件时,每个批次都带有 producerId: 1001 和 baseSequence 字段。幂等只保证单个分区内、生产者自身重试时不重复,应用自己重发的消息不能去重;要让写入多个分区的消息同时成功或失败,需要使用事务,消费者设置 isolation.level=read_committed 后只读取已提交事务中的消息。
消费者处理完消息后提交偏移量,下次从提交的位置继续读取。默认每 5 秒自动提交一次,已经处理但还没提交时崩溃,重启后就会重复消费。例如消费者读到 3 条扣款消息后被 kill -9,这时组的偏移量仍然为空;它在 session.timeout.ms(默认 45 秒)之后被移出消费者组,重新启动的消费者又收到了这 3 条消息(见[附录 A2](#a2-重复消费))。因此接收端必须做到幂等,方法见 [接收端的可靠性](#接收端的可靠性)。反过来,如果先提交偏移量再处理,处理失败的消息就丢失了。