Apache Pulsar 消息确认机制原理与源码分析


前言

分布式消息系统的可靠性保证机制通常有如下三种:

  • At Most Once:指消息最多只被传递一次,但有可能出现消息丢失的情况。这种保证方式适用于对数据可靠性和一致性要求不高的场景。
  • At Least Once:消息确保至少被处理一次,但由于系统设计或不可抗力导致的处理失败,可能会出现重复处理的情况。大多数常用的消息队列,如RocketMQ、RabbitMQ和Kafka,都默认提供“至少一次”的保证。Pulsar默认也是。
  • Exactly Once:指发送到消息系统的消息只能被消费端处理且仅处理一次,即使生产端重试消息发送导致某消息重复投递,该消息在消费端也只被消费一次。Exactly-Once语义是消息系统和流式计算系统中消息流转的最理想状态,但是在业界并没有太多理想的实现。因为真正意义上的Exactly-Once依赖消息系统的服务端、消息系统的客户端和用户消费逻辑这三者状态的协调。需要三者参与实现分布式事务。Pulsar也能保证这种,设计原理见:Apache Pulsar 技术系列 - Pulsar事务实现原理

在Pulsar中保证消息至少被消费者消费一次是通过Ack机制实现的,Pulsar有四种Ack机制,即单条消息确认机制、累积消息确认机制、批消息中的单个消息确认、否定应答Nack。在了解这四种Ack机制前需要先了解Pulsar的订阅类型。订阅决定了一组消费者(一个或多个)消费 Topic 中消息的方式,同一个 Topic 可以同时支持使用多个不同的订阅类型。在实际使用中,这种设计理念也方便我们根据需要为不同的应用程序配置不同的消费模式。目前 Pulsar 支持以下四种订阅类型:
在这里插入图片描述

  • Exclusive:Pulsar 默认的订阅类型,每次仅允许一个消费者消费订阅(即订阅的topic中的消息)中的消息。如果有其他消费者想要接入该订阅,请求将会被 Broker 拒绝。也就是说只有一个消费者处理topic的所有分区消息。该订阅类型能够确保消息的顺序性。
  • Failover:可以允许多个消费者附加到一个订阅上。消费者的队列分配策略根据Topic类型有所区别。如果是分区Topic,broker服务端会根据消费者优先级和消费者名称字典顺序进行排序,然后Broker会将Topic中的分区平均分配给高优先级的消费者,低优先级消费者会成为分区的备消费者,类似于 Apache Kafka 中的消费分区rebalance机制。 如果是非分区Topic,Broker按照消费者订阅Topic的顺序选择主消费者,其他的成为备消费者。
  • Shared:允许多个消费者共同消费同一订阅的消息。在此模式下,多个消费实例会以 Round-robin 的方式接收 Topic 中的消息。因此,这种类型并不能保证顺序性。虽然每个消费者都能接收到消息,但正常情况下并不会出现消息重复消费的情况。如果某个消费者突然宕机或断开连接,所有发送给该消费者且未确认的消息会被重新发送给其他消费者确认。
  • Key_Shared:与 Shared 订阅类型相似,Key_Shared 也支持多个消费者共同消费同一订阅的消息,但发送给不同消费者的消息根据 Key 决定,即具有相同 Key 的消息会发送给相同的消费者。

在Pulsar中消息Ack机制是通过Cursor实现的,每个订阅会和一个Cursor绑定。根据订阅类型的不同,Cursor可能会记录一个或多个消费者的消费位置信息。例如,使用 Shared 或 Key_Shared 订阅时,消费者会共享同一订阅的Cursor。如果与 Kafka 类比,Pulsar 中的订阅类似于 Kafka 中的消费组,Cursor有点像偏移量(Offset)的概念。

需要注意的是,由于每个 Topic 不一定只有一个订阅,你可以针对单个 Topic 创建多个类型不同的订阅,这意味着多个消费组可以从同一个 Topic 中读取数据。在这种情况下,每个订阅的Cursor针对各自的消费组记录组内消费者的消费位置信息,互不影响。

上述说到Pulsar四种Ack机制,需要详细介绍一下:

  • 单条消息确认机制:指每条消息都需要客户端单独ack确认,客户端宕机或ack超时只会重新推送未ack的消息。假设消息1、2、3发送给了Consumer-A,消息4、5、6发送给了Consumer-B,而Consumer-B又消费的比较快,先ack了消息4,此时Cursor中会单独记录消息4为已ack状态。如果其他消息都被消费,但没有被ack,并且两个消费者都下线或ack超时,则Broker会只推送消息1、2、3、5、6,已经被ack的消息4不会被再次推送。
  • 累积消息确认机制:指ack消息N表示N之前的消息也被成功消费。如Consumer接受到了消息1、2、3、4、5,为了提升ack的性能,Consumer可以不分别ack5条消息,只需要调用acknowledgeCumulative,然后把消息5传入,Broker会把消息5以及之前的消息全部标记为已ack。
  • 批消息中的单个消息确认:这种消息确认模式,调用的接口和单条消息的确认一样,但是这个能力需要Broker开启配置项acknowledgmentAtBatchIndexLevelEnabled。当开启后,Pulsar可以支持只ack一个batch里面的某些消息。假设Consumer拿到了一个批消息,里面有消息1、2、3,如果不开启这个选项,我们只能消费整个batch再ack,否则Broker会以批为单位重新全部投递一次。前面介绍的选项开启之后,我们可以通过acknowledge方法来确认批消息中的单条消息。该机制可以避免每次消费重试时都重复推送整批的消息。
  • 否定应答Nack:客户端发送一个redeliverUnacknowledgedMessages命令给Broker,明确告知Broker,当前Consumer无法消费这条消息,消息将会被重新投递。

注意并不是所有的订阅类型都能使用上述Ack类型,如shared或者key_shared订阅类型就不支持累积消息确认机制,原因是在shared或者key_shared订阅类型下前面的消息不一定是被当前consumer消费的,如果使用累积消息确认会把别人的消息也一起ack。订阅类型与Ack类型之间的适配关系如下:
在这里插入图片描述
本文将主要介绍单条消息确认机制和累积消息确认机制的原理与源码分析。

单条消息确认机制

单条消息确认机制下的Ack请求会携带多个MessageIdData,每个MessageIdData表示确认一个entry。Ack请求数据结构如下:
在这里插入图片描述
MessageIdData数据结构如下:
在这里插入图片描述
采用区间的形式记录全部消息的ack情况,假设在Ledger-1中我们的区间信息如下:
在这里插入图片描述
则我们存储的区间信息如下,即会用区间来表示已经连续Ack的范围:
[ (1:-1, 1:2] , (1:3, 1:6] ]
如果旧的MarkDeletePosition存在已ack的区间,则将MarkDeletePosition更新为最新已ack区间的右边界,如示例中新的MarkDeletePosition=1:2。然后将新的MarkDeletePosition和ack区间存储到cursor ledger或zk中。
zk中存储的数据结构如下:
在这里插入图片描述
在这里插入图片描述
cursor ledger中存储的数据结构如下:
在这里插入图片描述
接下来进行源码分析,Broker ServerCnx在handleSubscribe中会触发topic加载(若已加载则无需重复加载)接着创建consumer并将consumer添加到subscription中,创建成功的consumer会加入consumers中(Map<consumerId, CompletableFuture<Consumer>),这部分源码分析放到后面。处理客户端ack请求的是ServerCnx的handleAck方法,该方法先从consumers获取consumer并调用其messageAcked方法,代码如

<think>嗯,用户想要获取关于消息队列的学习提纲,设计一个学习路径,包括基本概念、实现原理、使用场景和最佳实践。首先,我需要回顾自己掌握的消息队列知识,然后结合用户提供的引用内容,整理出一个结构化的学习路径。 首先看用户给出的引用,引用[1]提到了Zookeeper实现分布式消息队列的核心概念,比如节点(ZNode),这可能涉及到消息队列的底层存储结构。引用[2]对比了RocketMQ、Kafka和RabbitMQ,说明用户可能需要了解不同消息队列的特点和区别。引用[3]提到面试中常被问到的设计问题,可能用户希望深入理解消息队列的设计原理。引用[4]涉及动态切换数据源的方法,其中提到了消息队列的应用场景,比如解耦系统组件。引用[5]则是用户请求的初始问题,明确需要学习提纲。 接下来,我需要确定学习路径的结构。用户要求包括基本概念、实现原理、使用场景和最佳实践,可能还需要添加其他相关部分,比如常见组件对比、实战应用等。 基本概念部分应该包括消息队列的定义、核心模型(生产者、消费者、队列等)、核心术语(如Topic、Partition、Broker)、传输模式(点对点、发布订阅)以及核心特性(解耦、异步、削峰)。这部分可以结合引用[1]中的节点概念,说明消息队列如何利用这些结构进行消息存储和传递。 实现原理需要深入消息队列的内部机制,比如消息存储(持久化、日志结构)、消息传输协议(AMQP、MQTT、Kafka协议)、高可用机制(主从复制、集群分片)、消息顺序性保证、事务消息和消息重试/死信队列。引用[1]提到的Zookeeper实现可能涉及到高可用和集群管理,这里可以联系说明。 使用场景部分,用户引用[4]提到了解耦系统组件、异步处理、流量削峰、数据同步和分布式事务。需要详细解释每个场景的应用实例,比如电商下单流程中使用消息队列进行异步处理,或者日志收集中的流数据处理。 最佳实践可能包括如何选择消息队列(引用[2]中的对比)、消息设计规范(大小、序列化)、可靠性保障(持久化、ACK机制)、监控运维(指标监控、故障处理)、安全控制(权限、加密)和性能优化(批处理、压缩)。这部分需要结合实际应用中的常见问题和解决方案,比如如何避免消息堆积,如何处理重复消息。 常见组件对比部分,根据引用[2],详细列出RocketMQ、Kafka、RabbitMQ的特点、适用场景和性能差异,帮助用户根据需求选择合适的工具。 实战应用部分,可以结合引用[4]中的动态数据源切换案例,说明消息队列在其中的作用,比如通过消息通知实现数据源切换,或者解耦数据源更新和业务逻辑。此外,还可以提供简单的代码示例,比如使用Spring Boot集成RabbitMQ,展示发送和接收消息的基本操作。 最后,扩展思考部分,可以引导用户思考如何设计一个消息队列,结合引用[3]提到的面试问题,探讨设计中的关键问题,如网络通信、存储引擎、高可用方案等。同时,推荐学习资源,如官方文档、开源项目、书籍和在线课程,帮助用户进一步深入学习。 需要确保每个部分内容准确,引用相关文献,比如在讲解Zookeeper实现时引用[1],在对比消息队列时引用[2]。同时,保持语言简洁,结构清晰,符合用户要求的中文回答,使用正确的LaTeX格式,行内公式用$...$,独立公式用$$...$$,并检查是否有遗漏的部分,比如是否覆盖了所有用户提到的要点,是否有冗余内容等。 总结下来,学习路径的大纲应该是: 1. 基本概念 2. 实现原理 3. 使用场景 4. 最佳实践 5. 常见组件对比 6. 实战应用 7. 扩展思考 8. 学习资源推荐 每个部分下再分小点,结合引用内容和实际知识展开,确保用户能够循序渐进地掌握消息队列的相关知识。</think># 消息队列学习路径设计 --- ## 一、基本概念 1. **定义作用** 消息队列是分布式系统中**异步通信**的核心组件,通过存储-转发机制实现系统解耦、流量削峰和异步处理[^4]。 2. **核心模型** - 生产者(Producer):发送消息到队列 - 消费者(Consumer):从队列拉取消息 - 代理(Broker):消息存储路由核心 - 队列(Queue)/主题(Topic):消息分类容器[^1] 3. **核心术语** - Topic(主题)、Partition(分区)、Offset(偏移量) - 持久化、ACK确认、死信队列(Dead-Letter Queue) 4. **传输模式** - 点对点(Queue):单消费者模型 - 发布订阅(Pub/Sub):多消费者广播模型 5. **核心特性** $$ \text{系统可靠性} = f(\text{持久化}, \text{重试机制}, \text{事务支持}) $$ 需满足**至少一次(At-Least-Once)**或**精确一次(Exactly-Once)**投递语义[^2] --- ## 二、实现原理 1. **消息存储机制** - 日志结构存储(如Kafka的Segment文件) - 索引优化:$O(1)$时间复杂度查询设计 2. **消息传输协议** | 协议类型 | 特点 | 适用场景 | |----------|----------------------|-------------------| | AMQP | 标准化、强事务保证 | 金融交易 | | MQTT | 轻量级、低功耗 | IoT设备通信 | | Kafka协议| 高吞吐、分区有序 | 日志流处理[^2] | 3. **高可用机制** - 主从复制(Master-Slave) - 集群分片(如Kafka的Partition分布) 4. **消息顺序性保障** ```python # 分区键哈希算法保证顺序性 def assign_partition(msg_key, total_partitions): return hash(msg_key) % total_partitions ``` --- ## 三、使用场景 1. **系统解耦** 订单系统库存系统通过消息队列异步通信,避免直接依赖 2. **异步处理** 用户注册后异步发送邮件/SMS,提升接口响应速度 3. **流量削峰** 电商秒杀场景:将瞬时流量转化为队列消费 4. **数据同步** 数据库Binlog通过CDC工具写入消息队列,实现跨系统数据同步 5. **分布式事务** 基于**最终一致性**的Saga模式实现(需配合事务消息) --- ## 四、最佳实践 1. **队列选择策略** | 场景需求 | 推荐方案 | |-------------------|-----------------| | 高吞吐日志处理 | Kafka | | 复杂路由需求 | RabbitMQ | | 金融级事务保证 | RocketMQ[^2] | 2. **消息设计规范** - 单条消息大小建议 ≤ 1MB - 使用Protocol Buffers/Avro等高效序列化格式 3. **可靠性保障** ```java // RocketMQ事务消息示例 TransactionSendResult result = producer.sendMessageInTransaction(msg, null); if(result.getLocalTransactionState() == LocalTransactionState.COMMIT_MESSAGE){ // 提交持久化操作 } ``` 4. **监控运维** - 核心指标:堆积量(Backlog)、消费延迟(Lag) - 自动扩缩容策略:基于队列深度动态调整消费者数量 --- ## 五、常见组件对比 | 特性 | Kafka | RabbitMQ | RocketMQ | |-----------------|----------------|----------------|----------------| | 吞吐量 | 百万级/秒 | 万级/秒 | 十万级/秒 | | 消息有序性 | 分区有序 | 队列有序 | 队列/分区有序 | | 事务支持 | 0.11+版本支持 | 插件实现 | 原生支持 | | 典型应用场景 | 日志流 | 企业级应用 | 电商交易[^2] | --- ## 六、实战应用 **案例:动态数据源切换** 通过监听配置变更消息,实现数据库连接池的动态切换 ```java // Spring Boot监听消息示例 @KafkaListener(topics = "config-change") public void handleDataSourceChange(ConfigChangeEvent event) { DynamicDataSource.switchTo(event.getNewDataSource()); } ``` --- ## 七、扩展思考 1. **如何设计消息队列?** - 网络通信层:TCP长连接 vs HTTP/2 - 存储引擎:自研文件系统 vs 依赖DB - 高可用方案:Raft共识算法 vs Zookeeper协调[^1][^3] 2. **未来趋势** - Serverless消息服务 - 流式计算平台深度融合(如Flink+Kafka) --- ## 八、学习资源推荐 1. 官方文档:Kafka官方指南、RabbitMQ Tutorials 2. 开源项目:Apache Pulsar源码分析 3. 书籍:《Kafka权威指南》《分布式消息中间件实践》 4. 在线课程:极客时间《消息队列高手课》 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值