diff --git a/notes/pics/mq-decouple.png b/notes/pics/mq-decouple.png new file mode 100644 index 00000000..2a2ae372 Binary files /dev/null and b/notes/pics/mq-decouple.png differ diff --git a/notes/pics/mq-loss-points.png b/notes/pics/mq-loss-points.png new file mode 100644 index 00000000..a31fc5ce Binary files /dev/null and b/notes/pics/mq-loss-points.png differ diff --git a/notes/pics/mq-order-by-key.png b/notes/pics/mq-order-by-key.png new file mode 100644 index 00000000..981b7084 Binary files /dev/null and b/notes/pics/mq-order-by-key.png differ diff --git a/notes/消息队列.md b/notes/消息队列.md index 5e70c9f8..62e2eaa0 100644 --- a/notes/消息队列.md +++ b/notes/消息队列.md @@ -4,18 +4,18 @@ * [一、消息模型](#一消息模型) * [点对点](#点对点) * [发布/订阅](#发布订阅) - * [二、使用场景](#二使用场景) + * [二、使用场景](#二使用场景) 🔥🔥 * [异步处理](#异步处理) * [流量削峰](#流量削峰) * [应用解耦](#应用解耦) * [与 RPC 调用的区别](#与-rpc-调用的区别) - * [三、可靠性](#三可靠性) + * [三、可靠性](#三可靠性) 🔥🔥 * [发送端的可靠性](#发送端的可靠性) * [消息队列的可靠性](#消息队列的可靠性) * [接收端的可靠性](#接收端的可靠性) - * [四、顺序性](#四顺序性) - * [五、消息堆积](#五消息堆积) - * [六、延迟消息与死信队列](#六延迟消息与死信队列) + * [四、顺序性](#四顺序性) 🔥 + * [五、消息堆积](#五消息堆积) 🔥 + * [六、延迟消息与死信队列](#六延迟消息与死信队列) 🔥 * [延迟消息](#延迟消息) * [死信队列](#死信队列) * [七、Kafka](#七kafka) @@ -57,7 +57,7 @@ Kafka、RocketMQ 等通过消费者组把两种模型结合起来:一条消息

-## 二、使用场景 +## 二、使用场景 🔥🔥 ### 异步处理 @@ -85,6 +85,8 @@ Kafka、RocketMQ 等通过消费者组把两种模型结合起来:一条消息 例如订单支付成功后,积分、物流、短信和数据分析四个服务都要处理。订单服务逐个调用它们时,每新增一个下游都要修改订单服务,任何一个下游超时都会拖慢支付流程;改为发送一条“订单 1024 已支付”的消息后,各下游用自己的消费者组订阅,新增下游不需要修改订单服务,某个下游故障时消息留在队列中,恢复后继续处理。 +

+ 引入消息队列的代价是系统多了一个需要保证高可用的组件,并且需要处理下面的可靠性和顺序性问题。 ### 与 RPC 调用的区别 @@ -93,9 +95,13 @@ RPC 是同步的请求-响应:调用方等待被调用方返回结果,被调 选择时可以问两个问题:调用方的下一步是否依赖这次调用的结果?被调用方暂时不可用时,调用方是否也应该失败?两个答案都是“是”时用 RPC,否则考虑消息队列。例如支付前必须知道库存是否扣减成功,用 RPC 调用库存服务;支付成功后加积分,积分服务晚几秒处理、甚至宕机几分钟都不影响支付,用消息队列。 -## 三、可靠性 +## 三、可靠性 🔥🔥 -一条消息要经过发送、存储、消费三个环节,每个环节都可能丢失消息。三种常用的消息队列在这三个环节上的做法: +一条消息要经过发送、存储、消费三个环节,每个环节都可能丢失消息。以 Kafka 为例: + +

+ +三种常用的消息队列在这三个环节上的做法: | 环节 | Kafka | RabbitMQ | RocketMQ(4.x) | | --- | --- | --- | --- | @@ -147,17 +153,19 @@ COMMIT; 需要注意的是,消息队列提供的“恰好一次”(Exactly Once)不能代替业务的幂等。Kafka 的恰好一次指的是“从 Kafka 读取、处理、再写回 Kafka”这个过程中,用事务把输出的消息和消费的偏移量一起提交。消费者把结果写入数据库或者调用外部接口时,数据库的提交和偏移量的提交仍然是两件事,两者之间崩溃就会重复处理,Kafka 文档也说明写入其它系统时需要那个系统配合。 -去重记录也可以放在 Redis 中,但如果用 SETNX 标记之后、处理完成之前消费者崩溃,重新投递的消息会被当作已经处理而跳过,这条消息就丢了。因此 Redis 中要区分“处理中”和“已完成”两种状态,“处理中”设置过期时间,过期后允许重新处理。通用的幂等方法见 [分布式中的幂等](分布式.md#九幂等)。 +去重记录也可以放在 Redis 中,但如果用 SETNX 标记之后、处理完成之前消费者崩溃,重新投递的消息会被当作已经处理而跳过,这条消息就丢了。因此 Redis 中要区分“处理中”和“已完成”两种状态,“处理中”设置过期时间,过期后允许重新处理。过期后重新处理时,前一个处理者可能还没有结束,所以业务写入仍要用唯一索引或状态条件兜底。通用的幂等方法见 [分布式中的幂等](分布式.md#九幂等)。 -## 四、顺序性 +## 四、顺序性 🔥 消息队列通常只保证单个队列(Kafka 中的分区)内的消息有序,多个队列之间没有顺序保证。并且在点对点模式下,一个消费者处理失败的消息会被重新投递给另一个消费者,顺序也会被打乱。 要保证同一业务的消息有序,需要让发送端把这些消息按业务 Key(例如订单号)路由到同一个队列,并且接收端对同一个队列只用一个线程顺序消费。 +

+ RabbitMQ 中一个队列内的消息是先进先出的,但一个队列有多个消费者时,消息被拒绝后重新入队会打乱顺序。需要顺序时按业务 Key 把消息路由到多个队列,每个队列只有一个消费者;或者开启单活跃消费者(Single Active Consumer),同一时刻只有一个消费者接收消息,它断开后才由另一个消费者接替。Kafka 中这两件事由分区和消费者组完成:同一个 key 进入同一个分区,一个分区只分配给组内的一个消费者。 -## 五、消息堆积 +## 五、消息堆积 🔥 消费速度长期低于生产速度时,消息会在队列中堆积。堆积的程度用积压量(Lag)衡量,Kafka 中每个分区的积压量等于日志末尾的偏移量减去消费者组已提交的偏移量。例如积分服务只消费了 3000 条就停止,用 kafka-consumer-groups.sh 查看(Kafka 4.4 实测,省略了后三列,见[附录 A3](#a3-消费积压)): @@ -188,7 +196,7 @@ points points-events 2 3000 11186 8186 RabbitMQ 的队列默认没有长度上限,积压的消息占用内存和磁盘,达到内存或磁盘的水位线后,RabbitMQ 会阻塞所有发布消息的连接;设置了长度上限(x-max-length)的队列满了之后,默认丢弃队头最早的消息,配置了死信交换机时转入死信队列。 -## 六、延迟消息与死信队列 +## 六、延迟消息与死信队列 🔥 ### 延迟消息