第三篇:消息队列如何保证消息可靠性?

消息队列如何保证消息可靠性?

上一篇我们聊了《消息为什么能够解耦系统?》

消息队列之所以能够解耦,本质上是因为它在生产者和消费者之间增加了一个Broker,让双方不再直接调用,而是通过消息进行通信。

但是,新的问题也来了。

以前:

订单服务

短信服务

订单服务调用成功,就说明短信服务已经收到了请求。

现在:

订单服务

消息队列

短信服务

消息需要先发送到 Broker,再由消费者处理,中间多了很多环节,如果其中某个环节出问题,消息会不会丢?消息到底会在哪里丢?

很多人认为,消息丢失只有一种情况:Broker 宕机,其实,一条消息从发送到消费,要经历完整的一条链路。

Producer

发送消息

Broker

持久化

Consumer

业务处理

消费确认

每一个阶段,都有可能发生异常。

例如:

生产者发送时网络断开;Broker 收到消息,还没来得及写入磁盘就宕机;消费者处理成功,但是确认消息失败。所以讨论消息可靠性,不能只看 Broker。而是要看整条消息链路。

第一关:生产者如何知道消息发送成功?

来看一段最简单的代码。

kafkaTemplate.send("order-topic", order);

或者:

rocketMQTemplate.syncSend("order-topic", order);

看到这里会觉得:send() 方法执行结束,消息应该已经发送成功了。其实并不一定。

例如:

Producer

发送消息

网络异常

这时候生产者根本不知道:消息到底有没有到达 Broker。如果直接认为发送成功,就可能造成消息丢失。所以几乎所有 MQ 都会设计:发送确认(ACK)机制。只有 Broker 明确告诉生产者:我已经收到消息。生产者才认为发送成功。

如果没有收到确认:

Producer

超时

重新发送

这也是为什么很多 MQ 客户端都支持:

同步发送

异步发送

重试机制

因为第一步,就要尽可能保证:消息能够成功进入 Broker。

第二关:Broker 收到消息,就安全吗?

很多人会回答当然,Broker 都已经收到消息了。

其实依然不安全。

来看下面这个场景:

Producer

Broker(内存)

服务器突然断电

如果消息只是放在内存里,服务器一旦宕机,消息依然会消失,所以 Broker 收到消息以后,并不会立即返回成功。

真正重要的是:消息有没有成功持久化。这里不同 MQ 实现略有区别。

例如 Kafka,会把消息顺序追加到日志文件,RocketMQ,则会先写入 CommitLog。虽然实现不同,但目标一致:尽快把消息保存到磁盘。

为什么不是每收到一条消息就立刻刷盘?

很多人第一次看到这里,会想到:既然刷盘最安全。为什么不每条消息都立即刷盘?

原因和 Redis 很像,上一篇 Redis 我们讲过:每次修改都刷盘,性能会急剧下降,MQ 也是一样。

假设:

收到消息

立即刷盘

返回成功

一次消息,就对应一次磁盘 IO。高并发下,Broker 很快就成为瓶颈。所以很多 MQ 都提供两种策略:

同步刷盘:

收到消息

写磁盘

返回 ACK

优点:可靠

缺点:延迟更高

异步刷盘:

收到消息

先返回 ACK

后台统一刷盘

优点:吞吐量更高

缺点:如果服务器在刷盘之前宕机,可能丢失少量消息。

看到这里是不是很熟悉?Redis 的 AOF,也是类似的设计。本质上都是可靠性和性能之间的权衡。

第三关:Broker 宕机怎么办?

假设:消息已经成功写入 Broker,结果下一秒:

Broker

宕机

如果整个集群只有这一台 Broker,消息依然会丢。所以真正的 MQ,都会设计:

副本机制。

例如:

Producer

Leader Broker

├─────────┐

│ │

▼ ▼

Follower Follower

Leader 保存消息,Follower 同步数据,当 Leader 宕机时,其他副本仍然可以继续提供服务。

Kafka 有 ISR 副本机制。RocketMQ 也支持主从复制。虽然实现不同,但都是为了提高可靠性。

第四关:消费者收到消息,就结束了吗?

还没有。来看下面这个流程:

Consumer

收到消息

更新数据库

程序崩溃

数据库已经更新成功。但是消费者还没有告诉 Broker:这条消息我已经处理完了。

Broker 会认为:这条消息还没有消费。于是:重新投递同一条消息。再次消费。

这就是为什么:可靠性和重复消费,往往是一起出现的。为了避免消息丢失。

MQ 宁愿:多投一次,也不会少投一次。所以大多数 MQ 保证的是:至少投递一次(At Least Once),至于重复消费的问题,我们下一篇专门讨论。

为什么不存在绝对可靠的 MQ?

看到这里,很多人可能会问有没有一种 MQ:既不会丢消息,又没有重复消费,还能保持高性能。

答案是:几乎没有。原因很简单,如果:每一条消息:

发送

同步刷盘

等待所有副本同步

等待消费者确认

返回成功

那么可靠性确实最高。但是性能一定下降。

如果:

收到消息

立即返回

性能很高。

但是:极端情况下,又可能丢失数据。所以 MQ 的设计,从来不是追求绝对可靠。

而是在:可靠性、性能、延迟之间寻找平衡。不同 MQ,只是选择了不同的侧重点。

总结:消息队列如何保证消息可靠性?

不是依靠某一个功能,而是整条消息链路共同完成。

Producer

发送确认

Broker 持久化

副本同步

Consumer 消费

消费确认

每一个环节,都有对应的保护机制,也正因为如此。MQ 并不能保证消息绝对不会丢。

它真正做到的是:即使某个环节发生故障,也尽可能发现问题、恢复消息,并降低消息丢失的概率。这也是为什么:可靠性越高,系统需要付出的性能成本也越高。

下一篇,我们继续讨论另一个很多人都遇到过的问题:

为什么消息会重复消费?

上一篇:《消息为什么能够解耦系统?》

下一篇:《消息为什么会重复消费?》

相关推荐
布鲁飞丝1 天前
kafka 副本集设置和理解
分布式·kafka
香菜TTT2 天前
Kafka_深度解析_从架构原理到生产实践
分布式·架构·kafka·linq
java1234_小锋2 天前
【免费】基于Spark实时金融交易风险监控与预测系统(Java版本+可视化大屏+Kafka+SpringBoot+Vue3) 锋哥原创出品,必属精品
大数据·spark·kafka·金融交易风险监控与预测
梦想画家2 天前
FilePulse:Kafka Connect 的“智能文件网关”,重塑数据接入新范式
kafka·文件连接器·filepulse
Devin~Y2 天前
从本地生活电商到 AI RAG:互联网大厂 Java 面试场景完整实战
java·spring boot·redis·elasticsearch·spring cloud·kafka·rag
斯普润布特2 天前
Kafka KRaft 三节点 ARM64 Docker 部署
分布式·kafka
梦想画家2 天前
告别轮询:基于PostgreSQL CDC构建实时数据管道
数据库·postgresql·kafka·实时湖仓
后端观测站2 天前
第一篇:为什么需要消息队列?
kafka·java-rocketmq
重庆小透明3 天前
带你从不同视角了解三大MQ
java·学习·spring·kafka·rabbitmq·rocketmq