基本概念
kafka 是什么?有什么作用?⭐
Kafka 是一个分布式的流式处理平台,它以高吞吐、可持久化、可水平扩展、支持流数据处理等多种特性而被广泛使用
主要功能体现于三点:
-
消息系统:kafka与传统的消息中间件都具备系统解耦、冗余存储、流量削峰、缓冲、异步通信、扩展性、可恢复性等功能。与此同时,kafka还提供了大多数消息系统难以实现的消息顺序性保障及回溯性消费的功能。
-
存储系统:kafka把消息持久化到磁盘,相比于其他基于内存存储的系统而言,有效的降低了消息丢失的风险。这得益于其消息持久化和多副本机制。也可以将kafka作为长期的存储系统来使用,只需要把对应的数据保留策略设置为“永久”或启用主题日志压缩功能。
-
流式处理平台:kafka为流行的流式处理框架提供了可靠的数据来源,还提供了一个完整的流式处理框架,比如窗口、连接、变换和聚合等各类操作。
Kafka 都有哪些特点?
-
高吞吐量、低延迟:kafka每秒可以处理几十万条消息,它的延迟最低只有几毫秒,每个topic可以分多个partition, consumer group 对partition进行consume操作。
-
可扩展性:kafka集群支持热扩展
-
持久性、可靠性:消息被持久化到本地磁盘,并且支持数据备份防止数据丢失
-
容错性:允许集群中节点失败(若副本数量为n,则允许n-1个节点失败)
-
高并发:支持数千个客户端同时读写
请简述下你在哪些场景下会选择 Kafka?
-
日志收集:一个公司可以用Kafka可以收集各种服务的log,通过kafka以统一接口服务的方式开放给各种consumer,例如hadoop、HBase、Solr等。最经典的就是用它与FileBeats和ELK组成典型的日志收集、分析处理以及展示的框架(Kafka在框架中,作为消息缓冲队列)
-
消息系统:解耦和生产者和消费者、缓存消息等。
-
用户活动跟踪:Kafka经常被用来记录web用户或者app用户的各种活动,如浏览网页、搜索、点击等活动,这些活动信息被各个服务器发布到kafka的topic中,然后订阅者通过订阅这些topic来做实时的监控分析,或者装载到hadoop、数据仓库中做离线分析和挖掘,也可以保存到数据库
-
运营指标:Kafka也经常用来记录运营监控数据。包括收集各种分布式应用的数据,生产各种操作的集中反馈,比如报警和报告。
-
流式处理:比如spark streaming和 Flink
kafka 架构是怎么样的⭐(熟悉)
一个典型的 kafka 体系架构包括
-
若干 Producer
-
若干 Consumer
-
一个 Zookeeper 集群(在2.8.0版本中移除了 Zookeeper,通过 KRaft 进行自己的集群管理)
Producer 将消息发送到 Broker,Broker 负责将收到的消息存储到磁盘中,而 Consumer 负责从 Broker 订阅并消费消息。
Kafka 基本概念:
-
Producer :生产者,负责将消息发送到 Broker
-
Consumer :消费者,从 Broker 接收消息
-
Consumer Group :消费者组,由多个 Consumer 组成。消费者组内每个消费者负责消费不同分区的数据,一个分区只能由一个组内消费者消费,消费者组之间互不影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。
-
Broker :可以看做一个独立的 Kafka 服务节点或 Kafka 服务实例。如果一台服务器上只部署了一个 Kafka 实例,那么我们也可以将 Broker 看做一台 Kafka 服务器。
-
Topic :一个逻辑上的概念,包含很多 Partition,同一个 Topic 下的 Partiton 的消息内容是不相同的。
-
Partition :为了实现扩展性,一个非常大的 topic 可以分布到多个 broker 上,一个 topic 可以分为多个 partition,每个 partition 是一个有序的队列。
-
Replica :副本,同一分区的不同副本保存的是相同的消息,为保证集群中的某个节点发生故障时,该节点上的 partition 数据不丢失,且 kafka 仍然能够继续工作,- kafka 提供了副本机制,一个 topic 的每个分区都有若干个副本,一个 leader 和若干个 follower。
-
Leader :每个分区的多个副本中的"主副本",生产者以及消费者只与 Leader 交互
-
Follower :每个分区的多个副本中的"从副本",负责实时从 Leader 中同步数据,保持和 Leader 数据的同步。Leader 发生故障时,从 Follower 副本中重新选举新的 Leader 副本对外提供服务。
ISR、OSR、AR 是什么?
-
ISR:In-Sync Replicas 副本同步队列,所有与 Leader 副本保持一定程度同步的Replica(包括 Leader 副本在内)组成 ISR
-
OSR:Out-of-Sync Replicas,与 Leader 副本同步滞后过多的 Replica 组成了 OSR
-
AR:Assigned Replicas,分区中的所有 Replica
ISR是由 Leader 维护,follower从leader同步数据延迟超过相应的阈值会被剔除出 ISR, 存入OSR(Out-of-Sync Replicas )列表,新加入的follower也会先存放在OSR中
所以有:AR=ISR+OSR
Kafka Replicas是怎么管理的?
Leader 负责维护和跟踪 ISR 集合中所有 Follower 副本的滞后状态
-
当 Follower 副本落后过多时,就会将其放入 OSR 集合
-
当 Follower 副本追上了 Leader 的进度时,就会将其放入 ISR 集合。
默认情况下,只有 ISR 中的副本才有资格晋升为 Leader。
LEO、HW、LSO、LW等分别代表什么
分区相当于一个日志文件
-
LEO:是 LogEndOffset 的简称,代表当前日志文件中下一条
-
HW:水位或水印(watermark)一词,也可称为高水位(high watermark),通常被用在流式处理领域(比如Apache Flink、Apache Spark等),以表征元素或事件在基于时间层面上的进度。在Kafka中,水位的概念反而与时间无关,而是与位置信息相关。严格来说,它表示的就是位置信息,即位移(offset)。取 partition 对应的 ISR中 最小的 LEO 作为 HW,consumer 最多只能消费到 HW 所在的位置上一条信息。
-
LSO:是 LastStableOffset 的简称,对未完成的事务而言,LSO 的值等于事务中第一条消息的位置(firstUnstableOffset),对已完成的事务而言,它的值同 HW 相同
-
LW:Low Watermark 低水位, 代表 AR 集合中最小的 logStartOffset 值。
如何确定当前能读到哪一条消息?
确定分区HW所在位置,之前的消息都能够消费。
分区 ISR 集合中的每个副本都会维护自己的 LEO,而 ISR 集合中最小的LEO 即为分区的 HW
Kafka 缺点?
-
由于是批量发送,数据并非真正的实时;
-
对于mqtt协议不支持;
-
不支持物联网传感数据直接接入;
-
仅支持统一分区内消息有序,无法实现全局消息有序;
-
监控不完善,需要安装插件;
-
依赖zookeeper进行元数据管理;
Kafka 为何这么快,如何实现高吞吐率?⭐
Kafka是分布式消息系统,需要处理海量的消息
Kafka的设计是把所有的消息都写入速度低容量大的硬盘,以此来换取更强的存储能力,但实际上使用硬盘并没有带来过多的性能损失。
主要使用了以下几个方式实现了超高吞吐率:
-
顺序读写
-
Kafka采用的是顺序写,直接追加数据到末尾。实际上,磁盘顺序写的性能极高,在磁盘个数一定,转数一定的情况下,基本和内存速度一致
-
-
Page Cache
-
为了优化读写性能,Kafka 利用了操作系统本身的 Page Cache,就是利用操作系统自身的内存而不是JVM空间内存
-
-
零拷贝
-
直接将数据从内核空间的读缓冲区直接拷贝到内核空间的 socket 缓冲区,然后再写入到 NIC 缓冲区,避免了在内核空间和用户空间之间穿梭
-
-
分区分段+索引
-
Kafka 的 message 是按 topic 分类存储的,topic 中的数据又是按照一个个 partition 即分区存储到不同 broker 节点。每个 partition 对应了操作系统上一个文件夹,partition 实际上又是按照 segment 分段存储的
-
通过这种分区分段的设计,Kafka 的 message 消息实际上是分布式存储在一个个小的 segment 中的,每次文件操作也是直接操作的 segment
-
为了进一步查询优化,Kafka 又默认为分段后的数据文件建立了索引文件,就是文件系统上的
.index
文件。这种分区分段+索引的设计,不仅提升了数据读取的效率,同时也提高了数据操作的并行度。
-
-
批量读写
-
Kafka 数据读写是批量的,而不是单条的
-
可以避免在网络上频繁传输单个消息带来的延迟和带宽开销。假设网络带宽为10MB/S,一次性传输10MB的消息比传输1KB的消息10000万次显然要快得多
-
-
批量压缩
-
Kafka 把所有的消息都变成一个批量的文件,并且进行合理的批量压缩,减少网络 IO 损耗,通过
mmap
提高 I/O 速度,写入数据的时候由于单个Partion是末尾添加所以速度最优,读取数据的时候配合 sendfile 进行直接读取
-
ZooKeeper
Kafka与Zookeeper是什么关系?
Kafka的数据会存储在zookeeper上。包括broker和消费者consumer的信息
-
broker信息:包含各个 broker 的服务器信息、Topic 信息
-
消费者信息:主要存储每个消费者消费的 topic 的 offset 的值
ZooKeeper在Kafka中的作用是什么?
Apache Kafka是一个使用Zookeeper构建的分布式系统
虽然,Zookeeper的主要作用是在集群中的不同节点之间建立协调
但是,如果任何节点失败,我们还使用Zookeeper从先前提交的偏移量中恢复,因为它做周期性提交偏移量工作。
生产者
Kafka Producer 的执行过程?
-
Producer生产消息
-
从Zookeeper找到Partition的Leader
-
推送消息
-
通过ISR列表通知给Follower
-
Follower从Leader拉取消息,并发送ack
-
Leader收到所有副本的ack,更新Offset,并向Producer发送ack,表示消息写入成功。
比较RabbitMQ与Apache Kafka
-
功能
-
Apache Kafka 是分布式的、持久的和高度可用的,这里共享和复制数据
-
RabbitMQ中没有此类功能
-
-
性能速度
-
Apache Kafka–达到每秒100000条消息。
-
RabbitMQ–每秒20000条消息。
-
比较传统队列系统与Apache Kafka
-
消息保留
-
传统的队列系统 - 它通常从队列末尾处理完成后删除消息。
-
Apache Kafka中,消息即使在处理后仍然存在。这意味着Kafka中的消息不会因消费者收到消息而被删除。
-
-
基于逻辑的处理
-
传统队列系统不允许基于类似消息或事件处理逻辑。
-
Apache Kafka允许基于类似消息或事件处理逻辑。
-
消息
发送消息的分区策略有哪些?
1.轮询:依次将消息发送该topic下的所有分区,如果在创建消息的时候 key 为 null,Kafka 默认采用这种策略。
2.key 指定分区:在创建消息是 key 不为空,并且使用默认分区器,Kafka 会将 key 进行 hash,然后根据hash值映射到指定的分区上。这样的好处是 key 相同的消息会在一个分区下,Kafka 并不能保证全局有序,但是在每个分区下的消息是有序的,按照顺序存储,按照顺序消费。在保证同一个 key 的消息是有序的,这样基本能满足消息的顺序性的需求。但是如果 partation 数量发生变化,那就很难保证 key 与分区之间的映射关系了。
3.自定义策略:实现 Partitioner 接口就能自定义分区策略。
4.指定 Partiton 发送
你知道 Kafka 是如何做到消息的有序性?
kafka 中的每个 partition 中的消息在写入时都是有序的,而且单独一个 partition 只能由一个消费者去消费,可以在里面保证消息的顺序性。但是分区之间的消息是不保证有序的。
Kafka 的可靠性是怎么保证的?⭐
1.acks
这个参数用来指定分区中有多少个副本收到这条消息,生产者才认为这条消息是写入成功的,这个参数有三个值:
-
1.acks = 1,默认为1。生产者发送消息,只要 leader 副本成功写入消息,就代表成功。这种方案的问题在于,当返回成功后,如果 leader 副本和 follower 副本还没有来得及同步,leader 就崩溃了,那么在选举后新的 leader 就没有这条消息,也就丢失了。
-
2.acks = 0。生产者发送消息后直接算写入成功,不需要等待响应。这个方案的问题很明显,只要服务端写消息时出现任何问题,都会导致消息丢失。
-
3.acks = -1 或 acks = all。生产者发送消息后,需要等待 ISR 中的所有副本都成功写入消息后才能收到服务端的响应。毫无疑问这种方案的可靠性是最高的,但是如果 ISR 中只有leader 副本,那么就和 acks = 1 毫无差别了。
2.消息发送的方式
生产者发送消息有三种方式,发完即忘,同步和异步。
可以通过同步或者异步获取响应结果,失败做重试来保证消息的可靠性。
3.手动提交偏移offset
默认情况下,当消费者消费到消息后,就会自动提交位移
但是如果消费者消费出错,没有进入真正的业务处理,那么就可能会导致这条消息消费失败,从而丢失
我们可以开启手动提交offset,等待业务正常处理完成后,再提交offset。
4.通过副本 LEO 来确定分区 HW
Kafka的缓冲池满了怎么办?
无论消息是否被消费,kafka都会保留所有消息。
而当消息的大小,大于设置的最大值log.retention.bytes(默认为1073741824)的值,也就是说这个缓冲池满了的时候,Kafka便会清除掉旧消息
topic的分区partitions,被分为一个个小segment,按照segment为单位进行删除,由时间从远到近的顺序进行删除
此外,Kafka还支持基于时间策略进行删除数据
Kafka在什么情况下会出现消息丢失?
以下几个阶段,都有可能会出现消息丢失的情况
-
消息发送的时候,如果发送出去以后,消息可能因为网络问题并没有发送成功
-
消息消费的时候,消费者在消费消息的时候,若还未做处理的时候,服务挂了,那这个消息不就丢失了
-
分区中的leader所在的broker挂了之后
分区
Kafka 分区的目的?
分区对于 Kafka 集群的好处是:实现负载均衡
分区对于消费者来说,提高并发度,提高效率
发送消息的分区策略有哪些?
-
轮询:依次将消息发送该 topic 下的所有分区,如果在创建消息时 key 为 null,Kafka 默认采用该策略。
-
key 指定分区:在创建消息是 key 不为空,并且使用默认分区器,Kafka 会将 key 进行 hash,然后根据hash值映射到指定的分区上。这样的好处是 key 相同的消息会在一个分区下,Kafka 并不能保证全局有序,但是在每个分区下的消息是有序的,按照顺序存储,按照顺序消费。在保证同一个 key 的消息是有序的,这样基本能满足消息的顺序性的需求。但是如果 partation 数量发生变化,那就很难保证 key 与分区之间的映射关系了。
-
自定义策略:实现 Partitioner 接口就能自定义分区策略。
-
指定 Partiton 发送
Kafka 分区数可以增加或减少吗?为什么?
可以使用 bin/kafka-topics.sh
命令对 Kafka 增加 Kafka 的分区数据,但是 Kafka 不支持减少分区数。
Kafka新建的分区会在哪个目录下创建
在启动 Kafka 集群之前,我们需要配置好 log.dirs 参数,其值是 Kafka 数据的存放目录,这个参数可以配置多个目录,目录之间使用逗号分隔,通常这些目录是分布在不同的磁盘上用于提高读写性能。
当然我们也可以配置 log.dir 参数,含义一样。只需要设置其中一个即可。
如果 log.dirs 参数只配置了一个目录,那么分配到各个 Broker 上的分区肯定只能在这个目录下创建文件夹用于存放数据。
但是如果 log.dirs 参数配置了多个目录,Kafka 会在含有分区目录最少的文件夹中创建新的分区目录,分区目录名为 Topic名+分区ID。注意,是分区文件夹总数最少的目录,而不是磁盘使用量最少的目录!也就是说,如果你给 log.dirs 参数新增了一个新的磁盘,新的分区目录肯定是先在这个新的磁盘上创建直到这个新的磁盘目录拥有的分区目录不是最少为止。
Kafka创建Topic时如何将分区放置到不同的Broker中
-
副本因子不能大于 Broker 的个数;
-
第一个分区(编号为0)的第一个副本放置位置是随机从 brokerList 选择的;
-
其他分区的第一个副本放置位置相对于第0个分区依次往后移。也就是如果我们有5个 Broker,5个分区,假设第一个分区放在第四个 Broker 上,那么第二个分区将会放在第五个 Broker 上;第三个分区将会放在第一个 Broker 上;第四个分区将会放在第二个 Broker 上,依次类推;
-
剩余的副本相对于第一个副本放置位置其实是由 nextReplicaShift 决定的,而这个数也是随机产生的
分区再分配是做什么的?解决了什么问题?
分区再分配主要是用来维护 kafka 集群的负载均衡
分区再分配的原理就是通过控制器给分区新增新的副本,然后通过网络把旧的副本数据复制到新的副本上,在复制完成后,将旧副本清除。
为了不影响集群正常的性能,在此复制期间还会有一系列保证性能的操作,比如复制限流。
Kafka Partition 副本 leader 是怎么选举的?
常用选主机制的缺点:
-
split-brain (脑裂)
这是由ZooKeeper的特性引起的,虽然ZooKeeper能保证所有Watch按顺序触发,但是网络延迟,并不能保证同一时刻所有Replica“看”到的状态是一样的,这就可能造成不同Replica的响应不一致,可能选出多个领导“大脑”,导致“脑裂”
-
herd effect (羊群效应)
如果宕机的那个Broker上的Partition比较多, 会造成多个Watch被触发,造成集群内大量的调整,导致大量网络阻塞
-
ZooKeeper负载过重
每个Replica都要为此在ZooKeeper上注册一个Watch,当集群规模增加到几千个Partition时ZooKeeper负载会过重。
优势:
Kafka的Leader Election方案解决了上述问题,它在所有broker中选出一个controller,所有Partition的Leader选举都由controller决定。
controller会将Leader的改变直接通过RPC的方式(比ZooKeeper Queue的方式更高效)通知需为此作为响应的Broker。
Kafka partition leader的选举:
由 controller 执行:
-
从Zookeeper中读取当前分区的所有ISR(in-sync replicas)集合
-
调用配置的分区选择算法选择分区的leader
分区数越多越好吗?吞吐量就会越高吗?
在一定条件下,分区数的数量是和吞吐量成正比的,分区数和性能也是成正比的。
1.客户端/服务器端需要使用的内存成本大
服务端在很多组件中都维护了分区级别的缓存,分区数越大缓存成本也越大。
消费端的消费线程数是和分区数挂钩的,分区数越大消费线程数也就越多,线程的开销成本也就越大
生产者发送消息有缓存的概念,会为每个分区缓存消息,当积累到一定程度或者时间时会将消息发送到分区,分区越多,这部分的缓存也就越大
2.文件句柄的开销
每个 partition 都会对应磁盘文件系统的一个目录。
在 Kafka 的数据日志文件目录中,每个日志数据段都会分配两个文件,一个索引文件和一个数据文件。每个 broker 会为每个日志段文件打开一个 index 文件句柄和一个数据文件句柄。
随着 partition 的增多,所需要保持打开状态的文件句柄数也就越多,最终可能超过底层操作系统配置的文件句柄数量限制
3.越多的分区可能增加端对端的延迟
Kafka 会将分区 HW 之前的消息暴露给消费者。分区越多则副本之间的同步数量就越多,在默认情况下,每个 broker 从其他 broker 节点进行数据副本复制时,该 broker 节点只会为此工作分配一个线程,该线程需要完成该 broker 所有 partition 数据的复制
4.降低高可用性
分区数量越多,那么恢复时间也就越长,而如果发生宕机的 broker 恰好是 controller 节点时:在这种情况下,新 leader 节点的选举过程在 controller 节点恢复到新的 broker 之前不会启动。controller 节点的错误恢复将会自动地进行,但是新的 controller 节点需要从 zookeeper 中读取每一个 partition 的元数据信息用于初始化数据。例如,假设一个Kafka 集群存在 10000个partition,从 zookeeper 中恢复元数据时每个 partition 大约花费 2 ms,则 controller 的恢复将会增加约 20 秒的不可用时间窗口。
消费者
Kafka 新旧消费者的区别
旧的 Kafka 消费者 API 主要包括:SimpleConsumer(简单消费者) 和 ZookeeperConsumerConnectir(高级消费者)
SimpleConsumer 名字看起来是简单消费者,但是其实用起来很不简单,可以使用它从特定的分区和偏移量开始读取消息。
高级消费者和现在新的消费者有点像,有消费者群组,有分区再均衡,不过它使用 ZK 来管理消费者群组,并不具备偏移量和再均衡的可操控性。
讲一下你使用 Kafka Consumer 消费消息时的线程模型,为何如此设计?
Thread-Per-Consumer Model,这种多线程模型是利用Kafka的topic分多个partition的机制来实现并行:每个线程都有自己的consumer实例,负责消费若干个partition。各个线程之间是完全独立的,不涉及任何线程同步和通信,所以实现起来非常简单。
Kafka 消费者是否可以消费指定分区消息?
Kafa consumer消费消息时,向broker发出fetch请求去消费特定分区的消息,consumer指定消息在日志中的偏移量(offset),就可以消费从这个位置开始的消息,customer拥有了offset的控制权,可以向后回滚去重新消费之前的消息,这是很有意义的
Kafka消息是采用Pull模式,还是Push模式?
Kafka遵循了一种大部分消息系统共同的传统的设计:producer将消息push到broker,consumer从broker中pull消息。
pull模式好处是:
1)Kafka可以根据consumer的消费能力以适当的速率消费消息
2)消费者可以控制自己的消费方式:可以使用批量消费,也可以选择逐条消费
3)消费者还可以选择不同的提交方式来实现不同的传输语义,要是使用了push的方式,就没有这些优点了
pull模式缺点是:
如果Kafka没有数据,消费者会专门有个线程去等待数据,可能会陷入循环等待中——可以通过在拉请求中设置参数,允许消费者请求在等待数据到达的“长轮询”中进行阻塞(并且可选地等待到给定的字节数,以确保大的传输大小)来避免这一问题
如果是Push模式的话
-
优点是,相对于pull的方式来说,它不需要专门有一个消息去等待,而可能造成线程循环等待的问题
-
缺点是,push(推)模式一般是会以同样的速率将消息推给消费者,很难适应消费速率不同的消费者,这样很容易造成有些消费能力比较低的consumer来不及处理消息,导致出现拒绝服务以及网络拥塞的情况
谈一谈 Kafka 的再均衡
在Kafka中,当有新消费者加入或者订阅的topic数发生变化时,会触发Rebalance(再均衡:在同一个消费者组当中,分区的所有权从一个消费者转移到另外一个消费者)机制,Rebalance顾名思义就是重新均衡消费者消费。Rebalance的过程如下:
第一步:所有成员都向coordinator发送请求,请求入组。一旦所有成员都发送了请求,coordinator会从中选择一个consumer担任leader的角色,并把组成员信息以及订阅信息发给leader。
第二步:leader开始分配消费方案,指明具体哪个consumer负责消费哪些topic的哪些partition。一旦完成分配,leader会将这个方案发给coordinator。coordinator接收到分配方案之后会把方案发给各个consumer,这样组内的所有成员就都知道自己应该消费哪些分区了。
存储
Kafka 高效文件存储设计特点
-
Kafka把topic中一个parition大文件分成多个小文件段,通过多个小文件段,就容易定期清除或删除已经消费完文件,减少磁盘占用。
-
通过索引信息可以快速定位message和确定response的最大大小。
-
通过index元数据全部映射到memory,可以避免segment file的IO磁盘操作。
-
通过索引文件稀疏存储,可以大幅降低index文件元数据占用空间大小
事务
数据传输的事务有几种?⭐
数据传输的事务定义通常有以下三种级别:
-
最多一次(<=1): 消息不会被重复发送,最多被传输一次,但也有可能一次不传输
-
最少一次 (>=1):消息不会被漏发送,最少被传输一次,但也有可能被重复传输
-
精确的一次(Exactly once)(=1): 不会漏传输也不会重复传输,每个消息都传输被一次而且仅仅被传输一次,这是大家所期望的那么,每种传输,分别是怎样实现的呢?
consumer先读消息,记录offset,最后再处理消息
这样,不可避免地存在一种可能:在记录offset之后,还没处理消息就出现故障了,新的consumer会继续从这个offset处理,那么就会出现有些消息永远不会被处理。那么这种机制,就是消息最多被处理一次
consumer可以先读取消息,处理消息,最后记录offset
当然如果在记录offset之前就crash了,新的consumer会重复的消费一些消息。那么这种机制,就是消息最多被处理一次
可以通过将提交分为两个阶段来解决:
保存了offset后提交一次,消息处理成功之后再提交一次。
当然也可以直接将消息的offset和消息被处理后的结果保存在一起,这样就能够保证消息能够被精确地消费一次