原来kafka也有事务啊,再也不担心消息不一致了

前言

现在假定这么一个业务场景,从kafka中的topic获取消息数据,经过一定加工处理后,发送到另外一个topic中,要求整个过程消息不能丢失,也不能重复发送,即实现端到端的Exactly-Once精确一次消息投递。这该如何实现呢?

kafka事务介绍

针对上面的业务场景,kafka已经替我们想到了,在kafka 0.11版本以后,引入了一个重大的特性:幂等性和事务。

幂等性

这里提到幂等性的原因,主要是因为事务的启用必须要先开启幂等性,那么什么是幂等性呢?

幂等性是指生产者无论向kafka broker发送多少次重复的数据,broker 端只会持久化一条,保证数据不会重复。

幂等性通过生产者配置项enable.idempotence=true开启,默认情况下为true。

幂等性实现原理

  1. 每条消息都有一个主键,这个主键由 <PID, Partition, SeqNumber>组成。

  • PIDProducerID,每个生产者启动时,Kafka 都会给它分配一个 IDProducerID 是生产者的唯一标识,需要注意的是,Kafka 重启也会重新分配 PID

  • Partition:消息需要发往的分区号。

  • SeqNumber:生产者,他会记录自己所发送的消息,给他们分配一个自增的 ID,这个 ID 就是 SeqNumber,是该消息的唯一标识,每发送一条消息,序列号加 1。

  1. 对于主键相同的数据,kafka 是不会重复持久化的,它只会接收一条。

幂等性缺点

根据幂等性的原理,我们发现它存在下面的缺点:

  • 只能保证单分区、单会话内的数据不重复

  • kafka 挂掉,重新给生产者分配了 PID,还是有可能产生重复的数据

那么如何实现跨分区、kafka broker重启也能保证不重复呢?这就要使用事务了。

事务

所谓事务,就是要求保证原子性,要么全部成功,要么全部失败。那么具体该如何开启呢?

  1. kafka要想开启事务必须要启用幂等性,即生产者配置enable.idempotence=true

  2. kafka生产者需要配置唯一的事务idtransactional.id, 最好为其设置一个有意义的名字。

  3. kafka消费端也有一个配置项isolation.level和事务有很大关系。

  • read_uncommitted:默认值,消费端应用可以看到(消费到)未提交的事务,当然对于已提交的事务也是可见的。

  • read_committed:消费端应用只能消费到提交的事务内的消息。

kafka事务 API

现在我们用java的api来实现一下前面这个“消费-处理-生产“的例子吧。

  1. 引入依赖

<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>3.4.0</version>
</dependency>

  1. 创建事务的生产者

Properties prodcuerProps = new Properties();

prodcuerProps.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"localhost:9092");

prodcuerProps.setProperty(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

prodcuerProps.setProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());

producerProps.put("enable.idempotence", "true");

producerProps.put("transactional.id", "prod-1");
KafkaProducer<String, String> producer = new KafkaProducer(prodcuerProps);

  • enable.idempotence配置项目为true

  • 设置transactional.id

  1. 创建事务的消费者

Properties consumerProps = new Properties();
consumerProps.put("bootstrap.servers", "localhost:9092");
consumerProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); 
consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
consumerProps.put("group.id", "my-group-id");

consumerProps.put("enable.auto.commit", "false");

consumerProps.put("isolation.level", "read_committed");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(consumerProps);

consumer.subscribe(Collections.singletonList("topic1"));

  • enable.auto.commit=false,设置手动提交消费者offset

  • 设置isolation.level=read_committed,消费事务已提交的消息

  1. 核心逻辑


producer.initTransactions();
while(true) {
	
	ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000L));
    if(!records.isEmpty()){
        
        HashMap<TopicPartition, OffsetAndMetadata> offsetsMap = new HashMap<>();
        
        producer.beginTransaction();
        try {
            
            Set<TopicPartition> partitions = records.partitions();
            
            for (TopicPartition partition : partitions) {
                
                List<ConsumerRecord<String, String>> partitionRecords = records.records(partition);
                
                for (ConsumerRecord<String, String> record : partitionRecords) {
                    
                    ProducerRecord<String, String> outRecord = new ProducerRecord<>("topic2", record.key(), record.value().toUpperCase());
                    
                    producer.send(outRecord);
                }

                
                long offset = partitionRecords.get(partitionRecords.size() - 1).offset();
                
                offsetsMap.put(partition,new OffsetAndMetadata(offset+1));
            }
            
            producer.sendOffsetsToTransaction(offsetsMap,"groupid");
            
            producer.commitTransaction();
        } catch(Exeception e) {
            e.printStackTrace();
            
            producer.abortTransaction();
        }
    }
}

  • initTransactions(): 初始化事务

  • beginTransaction(): 开启事务

  • sendOffsetsToTransaction(): 在事务内提交已经消费的偏移量(主要用于消费者)

  • commitTransaction(): 提交事务

  • abortTransaction(): 放弃事务

kafka事务实现原理

kafka事务的实现引入了事务协调器,如下图所示:

  1. 生产者使用事务必须配置事务id, kafka根据事务id计算分配事务协调器

  2. 事务协调器返回pid,前面的幂等性中需要

  3. 开始发送消息到topic中,不过这些消息与普通的消息不同,它们带着一个字段标识自己是事务消息

  4. 当生产者事务内的消息发送完毕,会向事务协调器发送 commitabort 请求,等待 kafka 响应

  5. 事务协调器收到请求后先持久化到内置事务主题__transaction_state中,__transaction_state默认有50个分区,每个分区负责一部分事务。事务划分是根据transactional.idhashcode%50,计算出该事务属于哪个分区。 该分区Leader副本所在的broker节点即为这个transactional.id对应的Transaction Coordinator节点,这也是上面第一步中的计算逻辑。

  6. 事务协调器后台会跟topic通信,告诉它们事务是成功还是失败的。

  • 如果是成功,topic会汇报自己已经收到消息,协调者收到主题的回应便确认了事务完成,并持久化这一结果。

  • 如果是失败的,主题会把这个事务内的消息丢弃,并汇报给协调者,协调者收到所有结果后再持久化这一信息,事务结束。

  1. 持久化第6步中的事务成功或者失败的信息, 如果kafka broker配置max.transaction.timeout.ms之前既不提交也不中止事务, kafka broker将中止事务本身。 此属性的默认值为 15 分钟。

总结

本文讲解了通过kafka事务可以实现端到端的精确一次的消息语义,通过事务机制,KAFKA 实现了对多个 topic 的多个 partition 的原子性的写入,通过一个例子了解了一下如何使用事物。同时也简单介绍了事务实现的原理,它底层必须要依赖kafka的幂等性机制,同时通过类似“二段提交”的方式保证事务的原子性。

作者:【JAVA旭阳】

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

技术小羊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值