Java Spring Kafka Chained KafkaTransactionManager不';t与JPA Spring数据事务同步

Java Spring Kafka Chained KafkaTransactionManager不';t与JPA Spring数据事务同步,java,spring-boot,apache-kafka,spring-data,spring-kafka,Java,Spring Boot,Apache Kafka,Spring Data,Spring Kafka,我阅读了大量Gary Russell的答案和帖子,但没有找到以下序列同步常见用例的实际解决方案: receive from topic A=>save to DB via Spring data=>send to topic B 正如我正确理解的那样:在这种情况下,无法保证完全原子化处理,我需要在客户端处理消息重复数据消除,但主要问题是ChainedKafkaTransactionManager不与JpaTransactionManager同步(请参见下面的@KafkaListener) 卡夫卡

我阅读了大量Gary Russell的答案和帖子,但没有找到以下序列同步常见用例的实际解决方案:

receive from topic A=>save to DB via Spring data=>send to topic B

正如我正确理解的那样:在这种情况下,无法保证完全原子化处理,我需要在客户端处理消息重复数据消除,但主要问题是ChainedKafkaTransactionManager不与JpaTransactionManager同步(请参见下面的
@KafkaListener

卡夫卡配置:

@Production
@EnableKafka
@Configuration
@EnableTransactionManagement
public class KafkaConfig {

    private static final Logger log = LoggerFactory.getLogger(KafkaConfig.class);

    @Bean
    public ConsumerFactory<String, byte[]> commonConsumerFactory(@Value("${kafka.broker}") String bootstrapServer) {

        Map<String, Object> props = new HashMap<>();
        props.put(BOOTSTRAP_SERVERS_CONFIG, bootstrapServer);

        props.put(AUTO_OFFSET_RESET_CONFIG, 'earliest');
        props.put(SESSION_TIMEOUT_MS_CONFIG, 10000);
        props.put(ENABLE_AUTO_COMMIT_CONFIG, false);
        props.put(MAX_POLL_RECORDS_CONFIG, 10);
        props.put(MAX_POLL_INTERVAL_MS_CONFIG, 17000);
        props.put(FETCH_MIN_BYTES_CONFIG, 1048576);
        props.put(FETCH_MAX_WAIT_MS_CONFIG, 1000);
        props.put(ISOLATION_LEVEL_CONFIG, 'read_committed');

        props.put(KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        props.put(VALUE_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class);

        return new DefaultKafkaConsumerFactory<>(props);
    }

    @Bean
    public ConcurrentKafkaListenerContainerFactory<String, byte[]> kafkaListenerContainerFactory(
            @Qualifier("commonConsumerFactory") ConsumerFactory<String, byte[]> consumerFactory,
            @Qualifier("chainedKafkaTM") ChainedKafkaTransactionManager chainedKafkaTM,
            @Qualifier("kafkaTemplate") KafkaTemplate<String, byte[]> kafkaTemplate,
            @Value("${kafka.concurrency:#{T(java.lang.Runtime).getRuntime().availableProcessors()}}") Integer concurrency
    ) {

        ConcurrentKafkaListenerContainerFactory<String, byte[]> factory = new ConcurrentKafkaListenerContainerFactory<>();
        factory.getContainerProperties().setMissingTopicsFatal(false);
        factory.getContainerProperties().setTransactionManager(chainedKafkaTM);

        factory.setConsumerFactory(consumerFactory);
        factory.setBatchListener(true);
        var arbp = new DefaultAfterRollbackProcessor<String, byte[]>(new FixedBackOff(1000L, 3));
        arbp.setCommitRecovered(true);
        arbp.setKafkaTemplate(kafkaTemplate);

        factory.setAfterRollbackProcessor(arbp);
        factory.setConcurrency(concurrency);

        factory.afterPropertiesSet();

        return factory;
    }

    @Bean
    public ProducerFactory<String, byte[]> producerFactory(@Value("${kafka.broker}") String bootstrapServer) {

        Map<String, Object> configProps = new HashMap<>();

        configProps.put(BOOTSTRAP_SERVERS_CONFIG, bootstrapServer);

        configProps.put(BATCH_SIZE_CONFIG, 16384);
        configProps.put(ENABLE_IDEMPOTENCE_CONFIG, true);

        configProps.put(KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        configProps.put(VALUE_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class);

        var kafkaProducerFactory = new DefaultKafkaProducerFactory<String, byte[]>(configProps);
        kafkaProducerFactory.setTransactionIdPrefix('kafka-tx-');

        return kafkaProducerFactory;
    }

    @Bean
    public KafkaTemplate<String, byte[]> kafkaTemplate(@Qualifier("producerFactory") ProducerFactory<String, byte[]> producerFactory) {
        return new KafkaTemplate<>(producerFactory);
    }

    @Bean
    public KafkaTransactionManager kafkaTransactionManager(@Qualifier("producerFactory") ProducerFactory<String, byte[]> producerFactory) {
        KafkaTransactionManager ktm = new KafkaTransactionManager<>(producerFactory);
        ktm.setTransactionSynchronization(SYNCHRONIZATION_ON_ACTUAL_TRANSACTION);
        return ktm;
    }

    @Bean
    public ChainedKafkaTransactionManager chainedKafkaTM(JpaTransactionManager jpaTransactionManager,
                                                         KafkaTransactionManager kafkaTransactionManager) {
        return new ChainedKafkaTransactionManager(kafkaTransactionManager, jpaTransactionManager);
    }

    @Bean(name = "transactionManager")
    public JpaTransactionManager transactionManager(EntityManagerFactory em) {
        return new JpaTransactionManager(em);
    }
}

值得一提的是,Kafka Consumer方法中没有活动事务(
TransactionSynchronizationManager.isActualTransactionActive()
)。

是什么让您认为它没有同步?您确实不需要
@Transactional
,因为容器将启动这两个事务

您不应该在事务中使用
SeekToCurrentErrorHandler
,因为它发生在事务中。改为配置后回滚处理器。默认ARBP使用
固定回退(0L,9)
(10次尝试)

这对我来说很好;并在尝试4次交付后停止:

@springboot应用程序
公共类SO58804826应用程序{
公共静态void main(字符串[]args){
run(So58804826Application.class,args);
}
@豆子
公共JpaTransactionManager事务管理器(){
返回新的JpaTransactionManager();
}
@豆子
公共链KafkatransActionManager chainedTxM(jpa,
卡夫卡旅行社经理(卡夫卡){
kafka.setTransactionSynchronization(实际事务上的同步);
返回新的ChainedKafkaTransactionManager(kafka,jpa);
}
@自动连线
私人储蓄者;
@卡夫卡列斯汀(id=“so58804826”,topics=“so58804826”)
公共void侦听(字符串输入){
System.out.println(“存储:+in”);
这个.saver.save(in);
}
@豆子
公共新话题(){
返回TopicBuilder.name(“so58804826”)
.分区(1)
.副本(1)
.build();
}
@豆子
公共应用程序运行程序(KafkaTemplate模板){
返回参数->{
//template.executeInTransaction(t->t.send(“so58804826”,“foo”);
};
}
}
@组成部分
类ContainerFactoryConfigurer{
ContainerFactoryConfigurer(ConcurrentKafkaListenerContainerFactory工厂,
ChainedKafkatRactionManager tm){
factory.getContainerProperties().setTransactionManager(tm);
factory.setAfterRollbackProcessor(新的DefaultAfterRollbackProcessor(新的FixedBackOff(1000L,3)));
}
}
@组成部分
节课器{
@自动连线
私人MyEntityRepo回购;
私有最终AtomicInteger ID=新的AtomicInteger();
@事务(“chainedTxM”)
公共作废保存(字符串输入){
this.repo.save(新的MyEntity(in,this.ids.incrementAndGet());
抛出新的运行时异常(“foo”);
}
}
我从两个TXM中看到“参与现有事务”

使用
@Transactional(“transactionManager”)
,我只是从JPATm获得了它,正如人们所期望的那样

编辑

对于批处理侦听器没有“恢复”的概念-框架不知道批处理中需要跳过哪个记录。在2.3中,我们在使用手动确认模式时为批处理侦听器添加了一个新功能

从版本2.3开始,确认接口有两个附加方法nack(长睡眠)和nack(int-index,长睡眠)。第一个用于记录侦听器,第二个用于批处理侦听器。为侦听器类型调用错误的方法将引发IllegalStateException

使用批处理侦听器时,可以指定发生故障的批内的索引。调用
nack()
时,在对失败记录和丢弃记录的分区执行索引和查找之前,将提交记录的偏移量,以便在下次轮询()时重新传递这些记录。这是对SeekToCurrentBatchErrorHandler的改进,后者只能查找整个批次进行重新交付

但是,失败的记录仍将无限期地重放

您可以跟踪不断失败的记录,并使用nack
index+1
跳过它

但是,由于您的JPA tx已回滚;这对你不起作用


使用批处理侦听器,您必须处理侦听器代码中的批处理问题。

哇,非常感谢!!!我今晚会查的!但在我的示例中,在抛出RuntimeException之前,记录被持久化。在“你真的不需要@Transactional”下,你是什么意思?我不需要在存储库或消费者上使用@Transactional?哦,对不起,现在我明白了:我不需要消费者/监听器上的事务性,但我需要使用ChainedTM配置Spring数据,而不仅仅是普通的JpaTransactionManager,对吗?不;我是说
@Transactional(“chainedTxM”)
是多余的,因为侦听器容器在调用侦听器之前启动事务。这没有什么坏处(因为无论是哪一个,我们都会参与现有的事务),但这是不必要的。我检查了设置,发现两个问题:1。Spring数据无法在3 TM(kafka,chain,jpa)之间进行描述,因此它无法启动,因此我需要在我的存储库类上设置@Transactional(“chainedTM”),2.如果在侦听方法开始时引发运行时异常(在任何模板或存储库使用之前)它无限循环,ARBP不起作用。在无限循环的情况下,无论ARBP如何,它总是寻求相同的偏移量:INFO o.a.k.clients.consumer.kafkanconsumer-…寻求偏移量1…请看第二次编辑的答案。这没有意义;我的示例
@KafkaListener(groupId = "${group.id}", idIsGroup = false, topics = "${topic.name.import}")
public void consume(List<byte[]> records, @Header(KafkaHeaders.OFFSET) Long offset) {
    for (byte[] record : records) {
        // cause infinity rollback (perhaps due to batch listener)
        if (true)
            throw new RuntimeExcetion("foo");

        // spring-data storage with @Transactional("chainedKafkaTM"), since Spring-data can't determine TM among transactionManager, chainedKafkaTM, kafkaTransactionManager
        var result = storageService.persist(record);

        kafkaTemplate.send(result);
    }
}
DefaultAfterRollbackProcessor
...
@Override
    public void process(List<ConsumerRecord<K, V>> records, Consumer<K, V> consumer, Exception exception,
            boolean recoverable) {

        if (SeekUtils.doSeeks(((List) records), consumer, exception, recoverable,
                getSkipPredicate((List) records, exception), LOGGER)
                    && isCommitRecovered() && this.kafkaTemplate != null && this.kafkaTemplate.isTransactional()) {
            ConsumerRecord<K, V> skipped = records.get(0);
            this.kafkaTemplate.sendOffsetsToTransaction(
                    Collections.singletonMap(new TopicPartition(skipped.topic(), skipped.partition()),
                            new OffsetAndMetadata(skipped.offset() + 1)));
        }
    }