kafka 保证exactly once语义的方式 幂等+事务

kafka 保证exactly once语义的方式 幂等+事务

首先要知道 Kafka 的幂等性机制。
什么是幂等 其实就是 多次执行同一操作结果一样

kafka幂等性引入的目的:生产者重复生产消息。生产者进行retry会产生重试时,会重复产生消息。有了幂等性之后,在进行retry重试时,只会生成一个消息。

Kafka为了实现幂等性,它在底层设计架构中引入了
ProducerID:在每个新的Producer初始化时,会被分配一个唯一的ProducerID,这个ProducerID对客户端使用者是不可见的。
SequenceNumber:对于每个ProducerID,Producer发送数据的每个Topic和Partition都对应一个从0开始单调递增的SequenceNumber值。
在实例化一个producer对象时
Producer<String, String> producer = new KafkaProducer<>(props);
在run()中有一个maybeWaitForPid()会生成一个ProducerID

源码

private void maybeWaitForPid() {
        if (transactionState == null)
            return;

        while (!transactionState.hasPid()) {
            try {
                Node node = awaitLeastLoadedNodeReady(requestTimeout);
                if (node != null) {
                    ClientResponse response = sendAndAwaitInitPidRequest(node);
                    if (response.hasResponse() && (response.responseBody() instanceof InitPidResponse)) {
                        InitPidResponse initPidResponse = (InitPidResponse) response.responseBody();
                        transactionState.setPidAndEpoch(initPidResponse.producerId(), initPidResponse.epoch());
                    } else {
                        log.error("Received an unexpected response type for an InitPidRequest from {}. " +
                                "We will back off and try again.", node);
                    }
                } else {
                    log.debug("Could not find an available broker to send InitPidRequest to. " +
                            "We will back off and try again.");
                }
            } catch (Exception e) {
                log.warn("Received an exception while trying to get a pid. Will back off and retry.", e);
            }
            log.trace("Retry InitPidRequest in {}ms.", retryBackoffMs);
            time.sleep(retryBackoffMs);
            metadata.requestUpdate();
        }
    }

kafka的幂等性只能保证同一个topic的同一个parition中的消息的幂等,对于跨partition的就无能为力了,如果一个producer向多个partition发送消息时怎么样保证要么都成功,要么都失败呢,只能依靠事务了
在流式处理中,我们经常会碰到这样一个场景:先从Kafka中消费消息,然后对消息进行处理,处理完之后再将结果以消息的形式写到Kafka中。这种场景我们称之为consumer-transform-producer模式,在这种场景下,由于程序的失败,consumer很容易重复消费数据,这样就导致producer发送了重复的数据了,那么可以使用事务来解决这种场景的消息重复问题,将consumer-transform-producer中的所有与Kafka的操作都放在同一个事务中,作为原子操作。
幂等加事务用来保证exactly once语义
Kafka的事务是指:在同一个事务中一系列的生产者生产消息和消费者消费消息的操作是原子操作
事务的几种方法

 // 初始化事务,需要注意确保transation.id属性被分配
void initTransactions();

// 开启事务
void beginTransaction() throws ProducerFencedException;

// 为Consumer提供的在事务内Commit Offsets的操作
void sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata> offsets,
                              String consumerGroupId) throws ProducerFencedException;

// 提交事务
void commitTransaction() throws ProducerFencedException;

// 放弃事务,类似于回滚事务的操作
void abortTransaction() throws ProducerFencedException;
public static void main(String[] args) {
    consumeTransformProduce();
}



/**
 * 在一个事务内,即有生产消息又有消费消息
 */
public static void consumeTransformProduce() {
    // 1.构建生产者
    Producer producer = buildTransactionProducer();
    // 2.初始化事务,对于一个生产者,只能执行一次初始化事务操作
    producer.initTransactions();
    // 3.构建消费者和订阅主题
    Consumer consumer = buildConsumer();
    consumer.subscribe(Arrays.asList("my-topic"));
    while (true) {
        // 4.开启事务
        producer.beginTransaction();
        // 5.1 消费消息
        ConsumerRecords<String, String> records = consumer.poll(500);
        try {
            // 5.2 do业务逻辑;
            // 这个是用于保存需要提交保存的consumer消费的offset
            Map<TopicPartition, OffsetAndMetadata> commits = new HashMap<>();
            for (ConsumerRecord<String, String> record : records) {
                // 5.2.1 读取消息,并处理消息。
                // 这里作为示例,只是简单的将消息打印出来而已
                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>("other-topic", "data2"));
            }
            // 7.提交偏移量,
            // 其实这里本质上是将消费者消费的offset信息发送到名为__consumer-offset中的topic中
            producer.sendOffsetsToTransaction(commits, "group1");

            // 8.事务提交
            producer.commitTransaction();
        } catch (Exception e) {
            // 7.放弃事务。回滚
            producer.abortTransaction();
        }
    }
}

/**
 * 需要:
 * 1、关闭自动提交 enable.auto.commit
 * 2、isolation.level为read_committed
 */
public static Consumer buildConsumer() {
    Properties props = new Properties();
    props.put("bootstrap.servers", "master:9092");
    props.put("group.id", "group1");
    
    // 设置隔离级别
    // 如果设置成read_committed的话,那么consumer.poll(500)的时候只会读取事务已经提交了的消息
    // 如果设置成read_uncommitted,那么consumer.poll(500)则会消费所有的消息,即使事务回滚了的消息也会消费
    // 默认的行为是read_uncommitted
    props.put("isolation.level","read_committed");
    // 关闭自动提交
    // 必须关闭自动提交保存消费者消费的offset,因为是用事务中的API来提交了
    props.put("enable.auto.commit", "false");

    props.put("key.deserializer",
            "org.apache.kafka.common.serialization.StringDeserializer");
    props.put("value.deserializer",
            "org.apache.kafka.common.serialization.StringDeserializer");

    KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);

    return consumer;

}

通过如上方式可以保证kafka的exactly once语义

猜你喜欢

转载自blog.csdn.net/a724952091/article/details/106219873