Kafka事务实现

本文深入探讨了Kafka的消息传输保障机制,包括atmostonce、atleastonce和exactlyonce三种级别,并重点解析了Kafka如何通过幂等性和事务特性实现exactlyonce。Kafka生产者开启幂等性可以防止消息重复写入,而事务则确保跨分区操作的原子性。消费者方面,事务保证了一定的消息语义,但无法完全避免消息丢失或重复。文章还介绍了事务的使用方法,包括事务的开启、提交、中止以及与消费端isolation.level参数的关系。最后,展示了消费-转换-生产模式的应用示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

介绍事务之前先说一下消息传输保障:

一、消息传输保障

一般而言,消息中间件的消息传输保障有3 个层级,分别如下。
( 1 ) at most once:至多一次。消息可能会丢失,但绝对不会重复传输。
( 2 ) at least once : 最少一次。消息绝不会丢失,但可能会重复传输。
( 3 ) exactly once :恰好一次。每条消息肯定会被传输一次且仅传输一次。

Kafka 的消息传输保障机制非常直观。当生产者向Kafka发送消息时,一旦消息被成功提交到日志文件,由于多副本机制的存在,这条消息就不会丢失。如果生产者发送消息到Kafka之后,遇到了网络问题而造成通信中断,那么生产者就无法判断该消息是否己经提交。虽然Kafka无法确定网络故障期间发生了什么,但生产者可以进行多次重试来确保消息已经写入Kafka,这个重试的过程中有可能会造成消息的重复写入,所以这里Kafka提供的消息传输保障为at least once 。

对消费者而言,消费者处理消息和提交消费位移的顺序在很大程度上决定了消费者提供哪一种消息传输保障。如果消费者在拉取完消息之后,应用逻辑先处理消息后提交消费位移,那么在消息处理之后且在位移提交之前消费者看机了,待它重新上线之后,会从上一次位移提交的位置拉取,这样就出现了重复消费,因为有部分消息已经处理过了只是还没来得及提交消费位移,此时就对应at least once。如果消费者在拉完消息之后,应用逻辑先提交消费位移后进行消息处理,那么在位移提交之后且在消息处理完成之前消费者岩机了,待它重新上线之后,会从己经提交的位移处开始重新消费,但之前尚有部分消息未进行消费,如此就会发生消息丢失,此时就对应at most once 。

Kafka 从0.11.0.0 版本开始引入了军等和事务这两个特性,以此来实现EOS ( exactly once
semantics ,精确一次处理语义) 。

二、幂等

所谓的幕等,简单地说就是对接口的多次调用所产生的结果和调用一次是一致的。生产者在进行重试的时候有可能会重复写入消息,而使用Kafka的幕等性功能之后就可以避免这种情况。

开启幕等性功能的方式很简单,只需要显式地将生产者客户端参数enab le.idempotence设置为true
即可(这个参数的默认值为false ),参考如下: properties.put(ProducerConfig.ENABLE_IDEMPOTENCECONFIG, true);
#或者
properties.put (“enable.idempotence”, true);

不仅仅这些配置,其他配置这里就不赘述了

三、事务

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

对流式应用( Stream Processing Applications )而言, 一个典型的应用模式为“ consumetransform-produce ” 。在这种模式下消费和生产并存: 应用程序从某个主题中消费消息, 然后经过一系列转换后写入另一个主题,消费者可能在提交消费位移的过程中出现问题而导致重复消费, 也有可能生产者重复生产消息。Kafka 中的事务可以使应用程序将消费消息、生产消息、提交消费位移当作原子操作来处理,同时成功或失败,即使该生产或消费会跨多个分区。

为了实现事务,应用程序必须提供唯一的transactionalld ,这个transactionalld 通过客户端参数transactional.id 来显式设置,参考如下:
properties.put(ProducerConfig.TRANSACTIONAL ID CONFIG,”transaction Id” ) ;
#或者
properties.put ("transactional.id”,”transactionid”),

事务要求生产者开启幕等特性,因此通过将transactional.id 参数设置为非空从而开启事务特性的同时需要将enable.idempotence 设置为true ( 如果未显式设置,则KafkaProducer 默认会将它的值设置为true ) ,如果用户显式地将enable.idempotence 设置为false,则会报出ConfigException:
org.apache.kafka.common.config.ConfigException:Cannot set a transactional.id without also enabling idempotence

transactionalld 与PID 一一对应,两者之间所不同的是transactionalld 由用户显式设置, 而PID 是由Kafka 内部分配的。另外,为了保证新的生产者启动后具有相同transactionalld 的旧生产者能够立即失效,每个生产者通过transactionalld 获取PID 的同时,还会获取一个单调递增的producer epoch ( 对应下面要讲述的KafkaProducer.initTransactions()方法〉。如果使用同一个transactionalld 开启两个生产者,那么前一个开启的生产者会报出如下的错误:

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 同PID 和序列号一样在5.2.5 节中就讲过了,对应v2 版的日志格式中RecordBatch 的pr oducer epoch 字段(参考图5 -7 )。

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

而从消费者的角度分析, 事务能保证的语义相对偏弱。出于以下原因, Kafka 并不能保证
己提交的事务中的所有消息都能够被消费:

  • 对采用日志压缩策略的主题而言,事务中的某些消息有可能被清理(相同key 的消息,后写入的消息会覆盖前面写入的消息)。
  • 事务中消息可能分布在同一个分区的多个日志分段( LogSegment )中,当老的日志分段被删除时,对应的消息可能会丢失。
  • 消费者可以通过seekO方法访问任意offset 的消息,从而可能遗漏事务中的部分消息。
  • 消费者在消费时可能没有分配到事务内的所有分区,如此它也就不能读取事务中的所
    有消息。

KafkaProducer 提供了5 个与事务相关的方法,详细如下:

  • void initTransactions();
  • void beginTransaction() throws ProducerFencedException ;
  • void sendOffsetsToTransaction(Map<Top 工cPart 工tion , OffsetAndMetadata> offsets ,String consumerGroupid) throws ProducerFencedException;
  • void commitTransaction() throws ProducerFencedException ;
  • void abortTransaction() throws ProducerFencedException ;

initTransactions()
方法用来初始化事务,这个方法能够执行的前提是配置了transactionalld,如果没有则会报出IllegalStateException:

  • beginTransaction() 方法用来开启事务
  • sendOffsetsToTransaction() 方法为消费者提供在事务内的位移提交的操作;
  • commitTransaction() 方法用来提交事务
  • abortTransaction() 方法用来中止事务,类似于事务回滚。

一个典型的事务消息发送的操作如代码清单7 -2 所示。

代码清单7.2 事务消息发送示例

Properties properties= new Properties();
properties.put(ProducerConfig.KEY_SERIALIZER一CLASS_CONFIG,StringSerializer.class . getName()) ;
properties.put(ProducerConfig . VALUE SERIALIZER_CLASS_CONFIG ,StringSerializer.class . getName());
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG , brokerList);
properties.put(ProducerConfig.TRANSACTIONALIDCONFIG , transactionid);

KafkaProducer<String , String> producer= new KafkaProducer<>(properties );
producer.initTransactions ();
producer.beginTransaction();

try {
	// 处理业务逻辑并创建ProducerRecord
	ProducerRecord<String, String> recordl =new ProducerRecord<>(topic,”msgl ”);
	producer.send(recordl) ;
	ProducerRecord<String, String> record2 =new ProducerRecord<>(topic,”msg2 ”);
	producer.send(record2);
	ProducerRecord<String, String> record3 =new ProducerRecord<>(topic,”msg3 ”);
	producer.send(record3);
	// 处理一些其他逻辑
	producer.commitTransaction() ;
} catch (ProducerFencedException e) {
	producer.abortTransaction() ;
}
producer.close();

在消费端有一个参数isolation.level ,与事务有着莫大的关联,这个参数的默认值为“read uncommitted”,意思是说消费端应用可以看到(消费到)未提交的事务, 当然对于己提交的事务也是可见的。这个参数还可以设置为“read committed ”,表示消费端应用不可以看到尚未提交的事务内的消息。举个例子,如果生产者开启事务并向某个分区值发送3条消息msgl 、msg2 和msg3,在执行commitTransaction()或abortTransaction()方法前,设置为“read_committed”的消费端应用是消费不到这些消息的,不过在KafkaConsumer内部会缓存这些消息,直到生产者执行commitTransaction()方法之后它才能将这些消息推送给消费端应用。反之,如果生产者执行了abortTransaction()方法,那么KafkaConsumer会将这些缓存的消息丢弃而不推送给消费端应用。

日志文件中除了普通的消息,还有一种消息专门用来标志一个事务的结束,它就是控制消息( Contro!Batch )。控制消息一共有两种类型: COMMIT 和ABORT ,分别用来表征事务己经成功提交或己经被成功中止。KafkaConsumer 可以通过这个控制消息来判断对应的事务是被提交了还是被中止了,然后结合参数isolation.level 配置的隔离级别来决定是否将相应的消息返回给消费端应用,如图7-19 所示。注意Contro!Batch 对消费端应用不可见,后面还会对它有更加详细的介绍。
在这里插入图片描述本节开头就提及了consume-transform-produce 这种应用模式,这里还涉及在代码清单7-2中尚未使用的s巳ndOffsetsToTransaction()方法。该模式的具体结构如图7-20 所示。与此对应的应用示例如代码清单7-3 所示。
在这里插入图片描述代码清单 7-3 消费一转换一生产模式示例

public c l ass TransactionConsumeTransformProduce {
	public static final String brokerList = ”localhost:9092;
	public static Properties getCosumerProperties() {
		Properties props= new Properties();
		props.put(ConsumerConfig.BOOTSTRAP_SERVERS CONFIG, brokerList) ;
		props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG ,StringDeserializer.class.getName()) ;
		props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS CONFIG ,StringDeserializer.class.getName());
		props.put(ConsumerConfig ENABLE AUTO_COMMI T一CONFIG , false) ;
		props.put(ConsumerConfig.GROUP_ID_CONFIG,”group Id” ) ;
		return props;
	}
	
	public static Properties getProducerProperties() {
		Properties props= new Properties ();
		props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG ,brokerList) ;
		props.put(ProducerConfig.KEYSERIALIZER_CLASS_CONFIG ,
		StringSerializer.class.getName());
		props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG ,
		StringSerializer.class.getName()) ;
		props.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG,”transactionalId”) ;
		return props ;
	}
		
	public static void main(String[] args ) (
		//初始化生产者和消费者
		KafkaConsumer<String , String> consumer =new KafkaConsumer<>(getConsumerProperties()) ;
		consumer.subscribe(Collections.singletonList(”topic-source"));
		KafkaProducer <String , String> producer = new KafkaProducer<>(getProducerProperties()) ;
		//初始化事务
		producer.initTransactions( );
		while (true ) {
		ConsumerRecords<String , String> records = consumer.poll(Duration.ofMillis (1000));
		if (records. isEmpty () ) {
		Map<TopicPartition,OffsetAndMetadata> offsets = new HashMap<>();
		//开启事务
		producer.beginTransaction() ;
		try {
			for (TopicPartition partition : records . partitions () ) {
				List<ConsumerRecord<String , String> partitionRecords = records.records(partition) ;
				for (ConsumerRecord<String , String> record : partitionRecords) {
					//do some logical processing .
					ProducerRecord<String, String> producerRecord = new ProducerRecord<>(”topic-sink”,record.key() ,record.value()) ;
					// 消费一生产模型
					producer.send(producerRecord);
				}
				long lastConsumedOffset = partitionRecords.get(partitionRecords.size()-1).offset();
				offsets.put(partition ,new OffsetAndMetadata(lastConsumedOffset + 1));
			}
			// 提交消费位移
			producer.sendOffsetsToTransaction(offsets ,”groupid”);
			// 提交事务
			producer.commitTransaction ();
		} catch (ProducerFencedException e) {
			//log the except 工0口
			// 中止事务
			producer.abortTransaction();
		}
	}

}
<think>我们被要求回答关于Kafka事务实现原理及使用方法的问题。根据提供的引用材料,我们可以整理出以下关键点: 引用[1]提到了事务配置的约束,引用[2]指出Kafka事务通常与幂等机制配合使用以实现Exactly Once语义,引用[3]说明了生产者对消息可靠性的不同需求,引用[4]详细比较了幂等和事务的特性,并提到事务实现(两阶段提交、事务日志等),引用[5]则提到了消费者位移提交的问题。 根据这些信息,我们组织回答如下: 1. **Kafka事务概述**:Kafka事务用于保证跨多个分区和Topic的消息发送的原子性(要么全部成功,要么全部失败)以及跨生产者会话的持久性(生产者重启后可以继续未完成的事务)。通常与幂等生产者结合使用以实现Exactly Once语义。 2. **实现原理**: - **事务协调器(Transaction Coordinator)**:每个生产者通过配置的`transactional.id`与一个事务协调器关联,协调器负责管理事务的状态(类似于2PC中的协调者)。 - **事务日志(Transaction Log)**:协调器将事务状态(如BEGIN、PREPARE、COMMIT、ABORT)持久化到内部Topic(`__transaction_state`)中,确保故障恢复。 - **两阶段提交(2PC)**: - **阶段1:Prepare**:生产者发送消息到各个分区(这些消息会被标记为未提交状态,消费者在`read_committed`隔离级别下不可见),然后向协调器发送提交请求,协调器将事务状态置为PREPARE_COMMIT并写入日志。 - **阶段2:Commit**:协调器向所有涉及的分区发送COMMIT标记(写入事务结束控制消息),然后更新事务状态为COMPLETE。此时消息变为已提交状态,消费者可见。 - **生产者ID(PID)与事务Epoch**:生产者初始化事务时,协调器会分配一个PID并递增Epoch(用于防止旧的生产者实例产生僵尸实例),确保只有当前活跃的生产者可以操作事务。 3. **使用方法**: - **生产者配置**: ```java properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, "true"); // 启用幂等 properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "my-transactional-id"); // 设置事务ID ``` - **代码示例**: ```java producer.initTransactions(); // 初始化事务 try { producer.beginTransaction(); producer.send(new ProducerRecord<>("topic1", "key1", "value1")); producer.send(new ProducerRecord<>("topic2", "key2", "value2")); producer.commitTransaction(); } catch (ProducerFencedException e) { producer.close(); } catch (KafkaException e) { producer.abortTransaction(); } ``` - **消费者配置**:设置隔离级别为`read_committed`(默认是`read_uncommitted`),这样消费者只会读取已提交的消息。 ```java properties.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed"); ``` 4. **注意事项**: - 同一个`transactional.id`只能被一个生产者实例使用,否则会抛出`ProducerFencedException`。 - 事务超时:如果事务长时间未提交,协调器会将其标记为中止。 - 事务恢复:生产者重启后,通过相同的`transactional.id`可以恢复之前未完成的事务(协调器会检查Epoch并终止旧实例的事务)。 5. **与幂等性的关系**: - 幂等性(`enable.idempotence=true`)是事务的基础,它通过PID、序列号和分区号来避免单分区内的重复。 - 事务扩展了幂等性,提供了跨分区的原子性和持久性。 6. **性能影响**:由于两阶段提交和额外的日志写入,事务会带来一定的性能开销,因此仅在需要原子性写入的场景使用。 相关问题: 1. Kafka事务如何保证原子性和持久性? 2.Kafka事务中,如果协调器发生故障会怎样? 3. Kafka事务的Exactly Once语义与消费者位移提交有什么关系? 4. 如何配置Kafka消费者以读取事务消息?</think>### Apache Kafka事务机制实现原理及使用方法 #### **一、事务机制概述** Kafka事务机制通过**两阶段提交协议(2PC)**实现跨分区消息的原子性写入,确保多个消息要么全部成功提交,要么全部丢弃。核心目标是解决生产者端**Exactly Once语义**问题(与幂等性配合使用)[^2][^4]。 #### **二、实现原理** 1. **事务协调器(Transaction Coordinator)** - 每个生产者通过`transactional.id`绑定唯一协调器。 - 协调器管理事务状态(`BEGIN`、`PREPARE_COMMIT`、`COMMIT`/`ABORT`)并持久化到内部Topic `__transaction_state`[^4]。 2. **两阶段提交流程** - **Prepare阶段**:生产者发送消息到各分区(消息标记为"未提交"状态)。 - **Commit阶段**: 1. 协调器写入`PREPARE_COMMIT`到事务日志。 2. 向所有涉及分区写入**事务结束标记**(控制消息)。 3. 更新事务状态为`COMMIT`,消息对消费者可见[^4]。 3. **故障恢复机制** - **事务ID(transactional.id)**:生产者重启时,相同ID会触发事务恢复流程。 - **Epoch递增**:协调器通过递增Epoch值防止"僵尸生产者"(旧实例被隔离)[^4]。 #### **三、关键配置与代码示例** 1. **生产者配置** ```java properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, "true"); // 必须开启幂等 properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "txn-001"); // 事务ID ``` 2. **事务操作代码** ```java producer.initTransactions(); // 初始化事务 try { producer.beginTransaction(); producer.send(new ProducerRecord<>("topic1", "key1", "value1")); producer.send(new ProducerRecord<>("topic2", "key2", "value2")); producer.commitTransaction(); // 提交事务 } catch (KafkaException e) { producer.abortTransaction(); // 中止事务 } ``` 3. **消费者配置**(读取已提交消息) ```java properties.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed"); ``` #### **四、与幂等性的关系** | **特性** | **幂等生产者** | **事务机制** | |-------------------|----------------------|--------------------------| | 消息去重范围 | 单分区内有效 | 跨分区原子性 | | 生产者故障恢复 | 不支持(重启失效) | 支持(通过事务ID恢复) | | 性能开销 | 低(序列号校验) | 中高(两阶段提交) | | 典型场景 | 简单消息生产 | 流处理、跨分区业务逻辑 | [^4] #### **五、使用场景与限制** - **适用场景**: - 流处理中精确一次计算(如Kafka Streams)。 - 需要原子更新多个分区的业务(如订单+库存)。 - **限制**: - 事务中不能包含消费者位移提交(需单独处理)[^5]。 - 事务超时默认1分钟,超时自动中止。 - 同一`transactional.id`只能被一个生产者实例使用。 #### **六、性能优化建议** 1. 减少事务内消息数量,避免大事务阻塞。 2. 调整`transaction.timeout.ms`适应业务延迟。 3. 事务日志分区数(`transaction.state.log.num.partitions`)根据集群规模调整[^4]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值