Java Spring Kafka Chained KafkaTransactionManager不';t与JPA Spring数据事务同步
我阅读了大量Gary Russell的答案和帖子,但没有找到以下序列同步常见用例的实际解决方案: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) 卡夫卡
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的改进,后者只能查找整个批次进行重新交付
但是,失败的记录仍将无限期地重放
您可以跟踪不断失败的记录,并使用nackindex+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)));
}
}