springboot2.x +kafka使用和源码分析四(kafka事务)

本文介绍了从0.11.0.0版开始,Kafka对事务支持的引入,以及如何在SpringBoot 2.x应用中配合使用Kafka事务。通过配置`spring.kafka.producer.transaction-id-prefix`或自定义初始化bean来启用事务。强调了在事务环境下,消息发送必须在`@Transactional`注解或`kafkaTemplate.executeInTransaction()`内进行。同时,文章对Kafka事务的源码进行了分析,并提供了一个Demo项目的GitHub链接,以展示如何在事务中处理Kafka和MySQL的数据一致性问题。

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

kafka对于事务的支持(0.11.0.0客户端库开始添加了对事务的支持,kafka针对于事务机制新增名为 __transaction_state topic用以保存数据):

  • KafkaTransactionManager:与spring提供的事务机制一起使用(@TransactionalTransactionTemplate等等)。

  • 使用 KafkaMessageListenerContainer 事务性监听容器(消费者保证消费Exactly Once仅消费处理一次)

  • 使用KafkaTemplate

如果需要开启事务机制,使用默认配置需要在yml添加spring.kafka.producer.transaction-id-prefix配置。

或者自己初始化bean在上述KafkaProducerConfigure中添加


    /**
     * 构建生产者工厂类
     */
    @Bean
    public ProducerFactory<Integer, String> producerFactory() {

        Map<String, Object> configs = producerConfigs();

        DefaultKafkaProducerFactory<Integer,String>  producerFactory =  new DefaultKafkaProducerFactory(configs,new IntegerSerializer(),new StringSerializer());

        //设置事务Id前缀 开启事务
        producerFactory.setTransactionIdPrefix("tx-");
        return producerFactory;
    }

    @Bean
    public KafkaTransactionManager<Integer, String> kafkaTransactionManager(ProducerFactory<Integer, String> producerFactory) {
		return new KafkaTransactionManager<>(producerFactory);
	}

将KafkaTransactionManager注入到spring中。如果开启的事务,则后续发送消息必须使用@Transactional注解或者使用kafkaTemplate.executeInTransaction() ,否则抛出java.lang.IllegalStateException: No transaction is in process; possible solutions: run the template operation within the scope of a template.executeInTransaction() operation, start a transaction with @Transactional before invoking the template method, run in a transaction started by a listener container when consuming a record

源码分析:

   /**
     *信息的发送最终会执行此方法
     */
    protected ListenableFuture<SendResult<K, V>> doSend(final ProducerRecord<K, V> producerRecord) {
        //判断是否开启线程
        if (this.transactional) {
            //判断当前消息是通过事务的方式发送
            Assert.state(inTransaction(),
                    "No transaction is in process; "
                            + "possible solutions: run the template operation within the scope of a "
                            + "template.executeInTransaction() operation, start a transaction with @Transactional "
                            + "before invoking the template method, "
                            + "run in a transaction started by a listener container when consuming a record");
        }
        //获取Producer 对象用于发送消息
        final Producer<K, V> producer = getTheProducer();
        this.logger.trace(() -> "Sending: " + producerRecord);
        //定义发送结果回调对象
        final SettableListenableFuture<SendResult<K, V>> future = new SettableListenableFuture<>();
        producer.send(producerRecord, buildCallback(producerRecord, producer, future));
        //是否开启自动刷新
        if (this.autoFlush) {
            flush();
        }
        this.logger.trace(() -> "Sent: " + producerRecord);
        return future;
    }

    /**
     *判断方法的执行是否在事务中
     */
    public boolean inTransaction() {
        return this.transactional && (
                //当前执行线程在ThreadLocal中是否保存过producer如果有说明已在事务中
                this.producers.get() != null
                || TransactionSynchronizationManager.getResource(this.producerFactory) != null
                || TransactionSynchronizationManager.isActualTransactionActive());
    }

 

1:本地事务支持(不支持事务嵌套)

 /**
     * 测试kafka事务机制
     */
    @RequestMapping("sendSyncPersonInfoStrByTransaction")
    public void sendSyncPersonInfoStrByTransaction(){
        JSONObject j = new JSONObject();

        j.put("name","张三测试事务");
        j.put("sex","男");
        j.put("age",18);

        Integer key = new Random().nextInt(100);

        /**
         * 如果KafkaTransactionManager正在进行一个事务,则不使用它。而是使用新的“嵌套”事务。
         */
        boolean flag =kafkaTemplate.executeInTransaction(t->{
    
                //如果在这里这些任何异常抛出 代表此次事务需要进行数据回滚

                t.send("springboot_test_topic",key,j.toJSONString())
                        .get(5, TimeUnit.SECONDS);

                j.put("sex","女");
                t.send("springboot_test_topic",key+10,j.toJSONString())
                        .get(5, TimeUnit.SECONDS);
                int i = 0/0;

                return true;    
           
        });

        System.out.println(flag);

    }

运行上述测试代码,当代码运行报错( int i = 0/0; 除零异常)时,不会发送任何信息到kafka中

查看executeInTransaction方法源码可以知道

public <T> T executeInTransaction(KafkaOperations.OperationsCallback<K, V, T> callback) {

        Assert.notNull(callback, "'callback' cannot be null");
        Assert.state(this.transactional, "Producer factory does not support transactions");
        //在ThreadLocal保证线程安全
        Producer<K, V> producer = this.producers.get();
        Assert.state(producer == null, "Nested calls to 'executeInTransaction' are not allowed");

        String transactionIdSuffix;
        if (this.producerFactory.isProducerPerConsumerPartition()) {
            transactionIdSuffix = TransactionSupport.getTransactionIdSuffix();
            TransactionSupport.clearTransactionIdSuffix();
        }
        else {
            transactionIdSuffix = null;
        }
        //创建producer 事务的操作由此对象处理
        producer = this.producerFactory.createProducer(this.transactionIdPrefix);

        try {
            //开启事务
            producer.beginTransaction();
        }
        catch (Exception e) {
            //如果发送异常关闭producer 不占用资源
            closeProducer(producer, false);
            throw e;
        }
        //将
        this.producers.set(producer);
        try {
            //执行业务代码
            T result = callback.doInOperations(this);
            try {
                //提交事务
                producer.commitTransaction();
            }
            catch (Exception e) {
                throw new KafkaTemplate.SkipAbortException(e);
            }
            return result;
        }
        catch (KafkaTemplate.SkipAbortException e) { // NOSONAR - exception flow control
            throw ((RuntimeException) e.getCause()); // NOSONAR - lost stack trace
        }
        catch (Exception e) {
            //发生异常 终止事务
            producer.abortTransaction();
            throw e;
        }
        finally {
            //设置事务id
            if (transactionIdSuffix != null) {
                TransactionSupport.setTransactionIdSuffix(transactionIdSuffix);
            }
            //在ThreadLocal移除Producer
            this.producers.remove();
            //关闭资源
            closeProducer(producer, false);
        }
    }

2:使用@Transactional(transactionManager = "kafkaTransactionManager",rollbackFor = Exception.class) 使用

@RequestMapping("sendSyncPersonInfoStrByTransactionZJ")
    @Transactional(transactionManager = "kafkaTransactionManager",rollbackFor = Exception.class)
    public void sendSyncPersonInfoStrByTransactionZJ(){
        JSONObject j = new JSONObject();

        j.put("name","张三测试事务");
        j.put("sex","男");
        j.put("age",18);

        Integer key = new Random().nextInt(100);

        kafkaTemplate.send("transaction_test_topic",20,j.toJSONString());

        j.put("sex","女");
        kafkaTemplate.send("transaction_test_topic",10,j.toJSONString());

//        int i = 0/0;

    }

 

3:嵌套事务

在业务系统可能会存在以下的需求,当发送一条消息时,需要记录一条日志到业务数据库(mysql)中,那么这里面存在两种数据源(kafka,mysql)。这也就是我们说的嵌套事务,那么如何保证去数据一致性。spring提供了自己的解决方案(需要结合Consumer的Listen,后续表明)

Demo项目github地址:https://github.com/fangyuan94/kafkaDemo

### Spring Boot Kafka集成源码分析教程 #### 了解Spring Boot与Kafka的整合机制 Spring Boot通过`spring-kafka`库简化了与Apache Kafka消息系统的交互过程[^1]。此库提供了生产者消费者配置自动化的支持,使得开发者可以更专注于业务逻辑而非框架细节。 对于希望深入理解两者如何协同工作的开发人员来说,研究官方GitHub仓库中的测试案例是一个很好的起点。这些例子展示了不同场景下发送接收消息的方法实现方式。此外,在`org.springframework.kafka.listener`包内包含了监听器容器的核心组件定义;而消息转换则主要发生在`org.springframework.kafka.support.converter`命名空间下的类里。 #### 关键接口解析 - **ProducerFactory**: 负责创建`KafkaTemplate`使用的`Producer`实例对象。 - **ConsumerFactory**: 构建用于订阅主题并处理传入记录流的对象。 - **KafkaListenerContainerFactory**: 提供给定配置参数构建监听器容器的能力。 - **DefaultKafkaProducerFactory & DefaultKafkaConsumerFactory**: 实现上述两个工厂的具体子类。 - **ConcurrentMessageListenerContainer**: 支持并发操作的消息监听器容器,默认情况下会为每个分区分配一个线程来执行消费任务。 ```java // 创建生产者的简单示例 @Bean public ProducerFactory<String, String> producerFactory() { Map<String, Object> configProps = new HashMap<>(); configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); return new DefaultKafkaProducerFactory<>(configProps); } // 定义消费者属性 @Bean public ConsumerFactory<String, String> consumerFactory() { Map<String, Object> props = new HashMap<>(); props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); props.put(ConsumerConfig.GROUP_ID_CONFIG, "test-group"); props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); return new DefaultKafkaConsumerFactory<>(props); } ``` #### 深度探索:事务管理 当涉及到可靠性的需求时——比如确保消息不会丢失也不会重复处理——就需要考虑启用事务功能。这可以通过设置`enable.idempotence=true`以及指定合适的隔离级别(`read_committed`)来达成目的。此时,所有的写入都将被标记为幂等(idempotent),从而防止因网络错误等原因造成的重试过程中产生的重复数据条目。 #### 性能优化建议 为了提高吞吐量,应该调整诸如批量大小(batch.size)、压缩类型(compression.type)这样的高级选项。同时也要注意合理规划消费者的数量以匹配集群规模,并适当增加心跳检测间隔时间(session.timeout.ms)以便更好地适应高延迟环境。
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值