Kafka vs Rocketmq如何实现exactly once语义

本文详细介绍了分布式消息队列中exactlyonce语义的挑战和实现方式,主要以Kafka为例。Kafka通过幂等性和事务特性实现了消息的exactlyonce,包括PID和序列号的概念以及事务的开始、提交和回滚操作。同时,提到了消费者端的消费隔离级别对exactlyonce的影响。RocketMQ则没有在生产端提供exactlyonce语义,需要消费者端自行处理。
摘要由CSDN通过智能技术生成

消息语义

消息队列发送消息的三种语义

在分布式系统中,构成系统的任何节点都是被定义为可以彼此独立失败的。根据producer处理此类故障所采取的提交策略类型,我们可以获得不同的语义:

  • at least once

消息至少被写入一次。producer发送消息到服务端后,收到服务端返回的确认ack,表示消息写入成功。如果消息写入服务端后,服务端因为宕机等原因没有发送ack给producer,producer重试再次发送消息,导致一条消息写入多次。

  • at most once

消息至多被写入一次。producer发送消息到服务端后,不管是否收到服务端的确认ack消息,都不再重试。如果消息发送到服务端后,服务端因为gc等原因导致消息写入失败,producer不再重试发送失败的消息,导致消息丢失。

  • exactly once

消息确定被写入一次。producer发送消息到服务端,服务端保证重试的多条消息只被存储一次;producer保证发送失败的消息能再次重试,直到发送成功。

为什么大部分消息队列都不支持exactly once语义?

生产端实现exactly once必然要求发送的消息存储到服务端具备幂等性,进而要么要求消息能够被判断是否已经存在,要么要求消息具有严格的顺序,这对消息队列的性能尤其是吞吐能力有直接的限制;一方面分布式场景下做到精准一次的语义十分复杂,要求具备跨分区事务的能力,并且需要能够在broker主从切换、分区扩容等场景下仍能保证exactly once语义。而消费端实现exactly once语义则相对容易和灵活的多,所以大部分消息队列选择将exactly once的语义交给业务端来完成。

Kafka如何实现exactly once语义?

Kafka 为了支持消息语义的exactly once,kafka从0.11.0.0版本引入了幂等和事务两个特性。

幂等

幂等简单的说就是对接口的多次调用所产生的结果和调用一次是一致的。

开启幂等功能的方式很简单,只需要显示的将生产者客户端参数enable.idempotence = true即可(这个参数的默认值为false),参考如下:

properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
# 或者
properties.put("enable.idempotence", true);

如果要确保幂等功能正常,还需要确保生产者客户端的retires、acks、max.in.flight.requests.per.connection这几个参数不被配置错误。实际上在使用幂等功能的时候,用户完全可以不用配置这几个参数。默认情况下,KafkaProducer会将retries参数置为Integer.MAX_VALUE,max.in.flight.requests.per.connection参数为5,acks为-1.

为了实现生产者的幂等性,kafka为此引入了producer id(简称为PID)和序列号(sequence number)两个概念。这两个概念对应日志格式中RecordBatch的producer id和first sequence两个字段。每个新的生产者实例在初始化的时候都会被分配一个PID,这个PID对用是完全透明的。对于每个PID,消息发送到的每个分区都有对应的序列号,这些序列号从0开始单调递增。生产者每发送一条消息就会将<PID, 分区>对应的序列号的值加1.

broker端会在内存中为每一对<PID, 分区>维护一个序列号。对于收到的每一条消息,只有当它的序列号的值(SN_NEW)比broker端中维护的对应的序列号的值(SN_OLD)大1(即SN_NEW=SN_OLD+1)时,broker才会接受它。如果SN_NEW<SN_OLD+1,那么说明消息被重复写入,broker可以直接将其丢弃。如果SN_NEW>SN_OLD+1,那么说明中间有数据尚未写入,出现了乱序,对应的生产者会抛出OutOfOrderSequcenceException。

引入序列号来实现幂等只是针对每一对<PID, 分区>而言的,也就是说,Kafka的幂等只能保证单个生产者会话中单个分区的幂等。

事务

幂等性不能跨多个分区运作,而事务可以弥补这个缺陷。事务可以保证对多个分区写入操作的原子性。操作的原子性是指多个操作要么全部成功,要么全部失败,不存在部分成功、部分失败的可能。

transactionId

为了实现事务,应用程序必须提供唯一的transactionId,这个transactionId通过客户端参数transactional.id来显示设置,参考如下:

properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "${transactionId}");
# 或者
properties.put("transactional.id", "${transactionId}");

事务要求生产者开启幂等特性,因此通过将transactional.id参数设置为非空从而开启事务特性的同时需要将enable.idempotence设置为true(如果不显示的设置,则KafkaProducer默认会将它的值设置为true)。

transactinalId与PID一一对应,两者之间不同的是transactionId由用户显示设置,而PID是由Kafka内部分配的。为了保证新的生产者启动后具有相同transactionalId的旧生产者能够立即失效,每个生产者通过transactionId获取PID的同时,还会获取一个单调递增的producer epoch。如果使用同一个transactionalId开启两个生产者,那么前一个开启的生产者会报如下错误:

Caused by: org.apache.kafka.common.errors.ProducerFencedException: Producer attempted an operation with an old epoch. Either there is a newer producer with the same transactionalId, or the producer's transaction has been expired by the broker.

producer epoch对应日志格式中RecordBatch的producer epoch字段。

从生产者角度,通过事务,Kafka可以保证跨生产者会话的消息幂等发送,以及跨生产者会话的事务恢复。前者表示具有相同的transactionalId的新生产者实例被创建且工作的时候,旧的且拥有相同transactionId的生产者实例将不再工作。后者指当某个生产者实例宕机后,新的生产者实例可以保证未完成的旧事务要么被提交,要么被中止,如此可以使新的生产者实例从一个正常的状态开始工作。

KafkaProducer提供的与事务相关的方法

void initTransaction() :初始化事务,前提条件是手动指定transactionId
void beginTransaction():开始事务
void sendOffsetToTransaction():为消费者提供事务内的消费位移提交的操作
void commitTransaction():提交事务
void abortTransaction():中止事务,类似于回滚

事务消息发送示例

Properties prop = new Properties();
prop.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, ${brokerList});
prop.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
prop.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
prop.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, ${transactionId});

KafkaProducer<String, String> producer = new KafkaProducer<String, String>(prop);
//1、开启事务功能,事先得开启幂等,配置transactionId
producer.initTransactions();
producer.beginTransaction();

try {
  ProducerRecord<String, String> record =
    new ProducerRecord<>("topic1", "hello world" + System.currentTimeMillis())
    producer.send(record);
  //2、提交事务
  producer.commitTransaction();
} catch (ProducerFencedException e) {
  e.printStackTrace();
  //3、如果发生异常,终止事务
  producer.abortTransaction();
}

producer.close();

消费者端消费事务消息的隔离级别

在消费端有一个参数isolation.level,来控制消费端应用是否可以消费到未提交的事务消息。这个参数默认值为"read_uncommitted",表示消费端应用可以消费到已调教的事务消息和未提交的事务消息。这个参数还可以设置为"read_committed",表示消费端应用不可以看到尚未提交的事务内的消息。

日志文件中除了普通的消息,还有一种消息专门用来标志一个事务的结束,它就是控制消息(ControlBatch)。控制消息一共有两种,COMMIT和ABORT,分别用来表示事务已经成功提交或已经被成功中止。KafkaConsumer可以通过这个控制消息来判断对应的事务是被提交还是被中止了,然后结合参数isolation.level配置的隔离级别来决定是否将相应的消息返回给消费端应用。ControlBatch对消费端应用不可见。

Rocketmq如何实现exactly once语义?

rocketmq生产端并没有实现exactly once语义。首先生产者发送的消息不支持幂等性,另外虽然其也有事务的概念,但其提供的事务配合解决分布式事务的事务消息,和kafka中的事务消息并不是一个概念。

rocketmq消费端的exactly once也需要消费端自己取处理。

参考

消息队列RocketMQ版-使用Exactly-Once投递语义收发消息;

Kafka幂等性原理及实现剖析;

Kafka-之消息传输保障

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值