Kafka的消息处理语义
1.消息交付语义
client端常见的3钟消息交付语义:
- 最多一次:消息可能丢失也可能被处理,但最多只会被处理一次。
- 至少一次:消息不会丢失,但可能被处理多次
- 精确一次:消息被处理且只会被处理一次
在0.11.0.0版本之前,Kafka producer默认提供的是at least once语义。设想一下这个场景:如当producer向broker发送新消息后,分区leader副本所在的broker成功地将该消息写入本地磁盘,然后发送响应给producer,此时假设网络出现了故障导致响应没有发送成功,那么未接收到响应的producer会认为消息请求失败而重新发送,若网络恢复之后,那么同一条消息被写入日志两次,在极端的条件下,同一消息可能会被发送多次。
在Kafka0.11.0.0版本之后推出了幂等性producer和对事务的支持,完美解决了消息重复发送的问题。
2.幂等性producer(idempotent producer)
幂等性producer是0.11.0.0版本用于实现EOS(Exactly-Once semantics)的第一个利器。若一个操作执行多次的结果与只运行一次的结果是相同的,那么我们称该操作为幂等操作。
0.11.0.0版本引入的幂等性producer表示它的发送操作是幂等的,同一条消息被producer发送多次,但在broker端这条消息只会被写入日志一次,如果要启用幂等性producer以及获取其提供的EOS语义,用户需要显示设置producer端参数enable.idempotence为true
// 此时ack默认被设置为-1(all)
props.put("enable.idempotence",true);
幂等性的producer设计思路
发送到broker端的每批消息都会被赋予一个序列号(sequence number)用于消息去重。kafka会把它们保存到底层日志,这样即使分区leader副本挂掉,新选出来的leader broker也能执行消息去重的工作。
kafka还会为每个producer实例分配一个producer id(PID),消息要被发送到每个分区都有对应的序列号值,它们总是从0开始单调增加,对于PID、分区、序列号三者的关系,可以设想为一个map,key就是(PID,分区),value就是序列号,即每对(PID,分区)都有一个特定的序列号(seqID),如果发送消息的seqID小于等于broker端保存的seqID,那么broker会拒绝接收这一条消息。
在单会话幂等性中介绍,kafka通过引入pid和seq来实现单会话幂等性,但正是引入了pid,当应用重启时,新的producer并没有old producer的状态数据。可能重复保存。
当前设计只能保证单个producer的EOS语义,无法实现多个producer实例一起提供EOS语义。
代码分析
在Sender.run(long now)方法中,maybeWaitForProducerId()方法会生成一个producerID
void run(long now) { if (transactionManager != null) { try { if (transactionManager.shouldResetProducerStateAfterResolvingSequences()) // Check if the previous run expired batches which requires a reset of the producer state. transactionManager.resetProducerId(); if (!transactionManager.isTransactional()) { // this is an idempotent producer, so make sure we have a producer id maybeWaitForProducerId(); } else if (transactionManager.hasUnresolvedSequences() && !transactionManager.hasFatalError()) { .... } long pollTimeout = sendProducerData(now); client.poll(pollTimeout, now); }
3.事务
对事务的支持是kafka实现EOS的第二个利器,引入事务使得clients端程序能够将一组消息放入一个原子性单元统一处理。
kafka事务属性是指一系列的生产者生产消息和消费者提交偏移量的操作在一个事务,或者说是是一个原子操作),同时成功或者失败。
kafka为实现事务要求应用程序必须提供一个唯一的id表征事务,这个id被称为事务id,他必须在应用正序所有的会话上是唯一的。transactionID和Pid是不同的,前者是用户显式提供的,后者是producer自行分配的。
3.1 事务操作的API
producer提供了initTransactions, beginTransaction, sendOffsets, commitTransaction, abortTransaction 五个事务方法。
/** * 初始化事务。需要注意的有: * 1、前提 * 需要保证transation.id属性被配置。 * 2、这个方法执行逻辑是: * (1)Ensures any transactions initiated by previous instances of the producer with the same * transactional.id are completed. If the previous instance had failed with a transaction in * progress, it will be aborted. If the last transaction had begun completion, * but not yet finished, this method awaits its completion. * (2)Gets the internal producer id and epoch, used in all future transactional * messages issued by the producer. * */ public void initTransactions(); /** * 开启事务 */ public void beginTransaction() throws ProducerFencedException ; /** * 为消费者提供的在事务内提交偏移量的操作 */ public void sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata> offsets, String consumerGroupId) throws ProducerFencedException ; /** * 提交事务 */ public void commitTransaction() throws ProducerFencedException; /** * 放弃事务,类似回滚事务的操作 */ public void abortTransaction() throws ProducerFencedException ;
3.2 事务属性的应用实例
在一个原子操作中,根据包含的操作类型,可以分为三种情况,前两种情况是事务引入的场景,最后一种情况没有使用价值。
- 只有Producer生产消息;
- 消费消息和生产消息并存,这个是事务场景中最常用的情况,就是我们常说的“consume-transform-produce ”模式
- 只有consumer消费消息,这种操作其实没有什么意义,跟使用手动提交效果一样,而且也不是事务属性引入的目的,所以一般不会使用这种情况
3.3 相关属性配置
使用kafka的事务api时的一些注意事项:
- 需要消费者的自动模式设置为false,并且不能子再手动的进行执行consumer#commitSync或者consumer#commitAsyc
- 生产者配置transaction.id属性
- 生产者不需要再配置enable.idempotence,因为如果配置了transaction.id,则此时enable.idempotence会被设置为true
- 消费者需要配置Isolation.level。在consume-trnasform-produce模式下使用事务时,必须设置为READ_COMMITTED。
3.4 只有写
创建一个事务,在这个事务操作中,只有生成消息操作。代码如下:
/** * 在一个事务只有生产消息操作 */ public void onlyProduceInTransaction() {
// 创建生成者,代码如下,需要:
// 配置transactional.id属性
// 配置enable.idempotence属性
Producer producer = buildProducer(); // 1.初始化事务 producer.initTransactions(); // 2.开启事务 producer.beginTransaction(); try { // 3.kafka写操作集合 // 3.1 do业务逻辑 // 3.2 发送消息 producer.send(new ProducerRecord<String, String>("test", "transaction-data-1")); producer.send(new ProducerRecord<String, String>("test", "transaction-data-2")); // 3.3 do其他业务逻辑,还可以发送其他topic的消息。 // 4.事务提交 producer.commitTransaction(); } catch (Exception e) { // 5.放弃事务 producer.abortTransaction(); } }
3.5 消费-生产并存(consume-transform-produce)
/** * 在一个事务内,即有生产消息又有消费消息 */ public void consumeTransferProduce() { // 1.构建上产者 Producer producer = buildProducer(); // 2.初始化事务(生成productId),对于一个生产者,只能执行一次初始化事务操作 producer.initTransactions(); // 3.构建消费者和订阅主题
// 创建消费者代码,需要:
// 将配置中的自动提交属性(auto.commit)进行关闭
// 而且在代码里面也不能使用手动提交commitSync( )或者commitAsync( )
// 设置isolation.level
Consumer consumer = buildConsumer(); consumer.subscribe(Arrays.asList("test")); while (true) { // 4.开启事务 producer.beginTransaction(); // 5.1 接受消息 ConsumerRecords<String, String> records = consumer.poll(500); try { // 5.2 do业务逻辑; System.out.println("customer Message---"); Map<TopicPartition, OffsetAndMetadata> commits = Maps.newHashMap(); for (ConsumerRecord<String, String> record : records) { // 5.2.1 读取消息,并处理消息。print the offset,key and value for the consumer records. System.out.printf("offset = %d, key = %s, value = %s\n", record.offset(), record.key(), record.value()); // 5.2.2 记录提交的偏移量 commits.put(new TopicPartition(record.topic(), record.partition()), new OffsetAndMetadata(record.offset())); // 6.生产新的消息。比如外卖订单状态的消息,如果订单成功,则需要发送跟商家结转消息或者派送员的提成消息 producer.send(new ProducerRecord<String, String>("test", "data2")); } // 7.提交偏移量 producer.sendOffsetsToTransaction(commits, "group0323"); // 8.事务提交 producer.commitTransaction(); } catch (Exception e) { // 7.放弃事务 producer.abortTransaction(); } } }