kafka的事务指的是2个点   ① 生产者到kafka服务端的事务保障    ②消费者从kafka拉取数据的事务

kafka提供的事务机制是 第①点,  对于第②点来说 只能自己在消费端实现幂等性。

 

我们来介绍第①点, 因为生产者producer写到kafka可能会出现消息重复,比如 设置ack=all,写入到kafka的leader时,leader挂掉了,

没有及时反馈ack,导致生产者再次发送消息就会出现重复消息落盘。这种情况可以设置kafka的属性用来开启幂等。但是这种幂等

只能保证 producer没有挂掉的情况下,因为幂等的原理是 kafka缓存了一份 pid,partition,seqnumber 的数据,如果命中则说明之前缓存了,

但是如果producer挂掉了重启后,它的pid就会变化,partition也有可能变化,就会导致消息会出现重复状况。所以kafka 0.11版本加入了事务机制

开启时事务后,会存在 transaction_id , 封装成( transaction_id, pid,partition,seqnumber, 消费到哪条记录等等) 保存在kafka上,如果producer 挂了重新

启动的时候,会自动寻找kafka中的这个 transaction_id,找到的话就会恢复到挂掉之前的状态 ,然后进行消费。kafka事务保证了  要么全部成功,要么全部失败。

 

还有一个很重要的点是 要在consumer端 设置   isolation.level 为 read_committed状态,它默认是read_uncommitted状态,这是什么意思呢? 接下来详细说明一下:

目前producer是双线程设计,后台的Sender线程负责实际的消息发送。当Sender线程构造消息batch发送时,它会尝试去读取事务状态,如果发现已经abort,则立即将未发送的batch全部fail掉——这就是为什么你注释Thread.sleep后则不能发送的原因。当你加入了Thread.sleep之后batch发送时主线程在休眠,尚未执行到abortTransaction,故Sender线程成功地发送了消息到Kafka broker。

另外,你需要为consumer端配置isolation.level = read_committed。这样不管哪种情况你都不会读取到任何未提交的消息。默认是read_uncommitted,即使abort的消息,只要是成功发送到Kafka了,consumer就能读取到。


1、也就是开启事务之后,生产者调用send发送数据时他就会直接向kafka插入数据,只不过是这个数据后面追加了一个状态,这个状态是read_uncommited代表未提交,只有producer调用了commitTransaction时候 这些数据在kafka中才会都标记为read_commited。
所以 如果在 consumer消费方没有设置 isolation.level 为 read_committed状态(默认是read_uncommited),那么当producer 出现 异常或者宕机或者在事务提交之前发送的数据也依然能读取到,因为前面说了 这些数据是send的时候依然插入到kafka中只不过状态标记为uncommited而已,所以要想实现事务,还得在consumer方设置 隔离级别 isolation.level 为 read_committed,表示只能读取提交状态的记录,这样不管在任何条件下都不会读取到任何未提交的消息
 
2、还有一个情况是,当生产者中代码捕获到了异常,并进行abortTransaction,而消费者并没有设置隔离级别为read_committed,但却读不到消息呢,那我们可以 想象成当生产者调用abortTransaction时接下来的消息肯定不会发送到服务器,并且已发送到服务器上的
消息还会直接删掉,这样理解就可以了
 

 

producer实现代码如下:

public class producer {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Properties props = new Properties();
        props.put("bootstrap.servers","hadoop102:9092,hadoop103:9092,hadoop104:9092");
        props.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
        props.put("acks","all");
        props.put("retries","2");
        props.put("batch.size","16384");
        props.put("transactional.id","tran-wb2");  //事务ID,开启事务下面幂等也要开启
        props.put("enable.idempotence", "true");  //开启幂等
        // 一定要在消费者方设置 isolation.level为 read_committed,表示只读取已提交事务状态的记录
        Producer<Object, Object> producer = new KafkaProducer<>(props);

        producer.initTransactions();
        producer.beginTransaction();
        try {
            for (int i = 0; i <100 ; i++) {
                Future<RecordMetadata> first = producer.send(new ProducerRecord<>("first", i + "sad ", i + 1 + "s d"));
                //first.get();  加上get可以实现同步发送操作
                if (i==20){
                    throw new RuntimeException("测试异常回滚");
                }
            }
        } catch (RuntimeException e){
            System.out.println(e.toString());
            producer.abortTransaction();  //出现异常,就进行回滚,这样所有消息都会失败
            producer.close();
            return;
        }

        producer.commitTransaction(); //没有异常就 事务提交
        producer.close();
    }
}

消费者代码

public class consumer {

    public static void main(String[] args) throws InterruptedException {

        Properties properties = new Properties();
        properties.put("bootstrap.servers", "hadoop102:9092,hadoop103:9092,hadoop104:9092");
        properties.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        properties.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        properties.put("group.id", "wangbingsaa");
        properties.put("isolation.level", "read_committed"); //一定要设置 只拉取 已提交事务状态的记录,这样无论什么条件都可以
        // properties.put("auto.offset.reset","earliest"); //设置拉取的位置
        properties.put("enable.auto.commit", "false"); //关闭自动提交
        properties.put("auto.commit.interval.ms", "1000"); //自动提交间隔


        Consumer<String, String> consumer = new KafkaConsumer<>(properties);

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

        ConsumerRecords<String, String> records = consumer.poll(4000); //如果拉取时长超过4000毫秒 就不拉取
        for (ConsumerRecord<String, String> record : records) {
            System.out.printf("offset = %d, partitoon = %d key = %s, value = %s%n", record.offset(), record.partition(),record.key(), record.value());
        }
        consumer.commitSync(); //手动提交
    }
}

 

posted on 2021-05-26 18:15  菜霸  阅读(676)  评论(0编辑  收藏  举报