跳到主要内容
版本:4.1.1

技巧、窍门与示例

Doubao 中英对照 Tips, Tricks and Examples

手动分配所有分区​

假设你希望总是读取所有分区的全部记录(例如在使用压缩主题加载分布式缓存时),手动分配分区而不使用 Kafka 的组管理可能会很有用。当分区很多时,这样做会很繁琐,因为你必须列出所有分区。如果分区数量随时间变化,这也是个问题,因为每次分区数变更你都必须重新编译应用程序。

下面是一个如何利用 SpEL 表达式的强大能力,在应用程序启动时动态创建分区列表的示例:

@KafkaListener(topicPartitions = @TopicPartition(topic = "compacted",
partitions = "#{@finder.partitions('compacted')}",
partitionOffsets = @PartitionOffset(partition = "*", initialOffset = "0")))
public void listen(@Header(KafkaHeaders.RECEIVED_KEY) String key, String payload) {
...
}

@Bean
public PartitionFinder finder(ConsumerFactory<String, String> consumerFactory) {
return new PartitionFinder(consumerFactory);
}

public static class PartitionFinder {

private final ConsumerFactory<String, String> consumerFactory;

public PartitionFinder(ConsumerFactory<String, String> consumerFactory) {
this.consumerFactory = consumerFactory;
}

public String[] partitions(String topic) {
try (Consumer<String, String> consumer = consumerFactory.createConsumer()) {
return consumer.partitionsFor(topic).stream()
.map(pi -> "" + pi.partition())
.toArray(String[]::new);
}
}

}

将此与 ConsumerConfig.AUTO_OFFSET_RESET_CONFIG=earliest 结合使用,会在每次应用程序启动时加载所有记录。你还应将容器的 AckMode 设为 MANUAL,以防止容器为 null 消费者组提交偏移量。从 3.1 版本开始,当手动主题分配在没有消费者 group.id 的情况下使用时,容器会自动将 AckMode 强制为 MANUAL。不过,从 2.5.5 版本开始,如上所示,你可以为所有分区应用一个初始偏移量;更多信息请参阅主题分区分配。

Kafka 事务与其他事务管理器协同示例​

下面的 Spring Boot 应用程序是将数据库事务与 Kafka 事务链接起来的示例。监听器容器启动 Kafka 事务,@Transactional 注解启动数据库事务。数据库事务先提交;如果 Kafka 事务提交失败,记录将被重新投递,因此数据库更新应当是幂等的。

@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

@Bean
public ApplicationRunner runner(KafkaTemplate<String, String> template) {
return args -> template.executeInTransaction(t -> t.send("topic1", "test"));
}

@Bean
public DataSourceTransactionManager dstm(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}

@Component
public static class Listener {

private final JdbcTemplate jdbcTemplate;

private final KafkaTemplate<String, String> kafkaTemplate;

public Listener(JdbcTemplate jdbcTemplate, KafkaTemplate<String, String> kafkaTemplate) {
this.jdbcTemplate = jdbcTemplate;
this.kafkaTemplate = kafkaTemplate;
}

@KafkaListener(id = "group1", topics = "topic1")
@Transactional("dstm")
public void listen1(String in) {
this.kafkaTemplate.send("topic2", in.toUpperCase());
this.jdbcTemplate.execute("insert into mytable (data) values ('" + in + "')");
}

@KafkaListener(id = "group2", topics = "topic2")
public void listen2(String in) {
System.out.println(in);
}

}

@Bean
public NewTopic topic1() {
return TopicBuilder.name("topic1").build();
}

@Bean
public NewTopic topic2() {
return TopicBuilder.name("topic2").build();
}

}
spring.datasource.url=jdbc:mysql://localhost/integration?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

spring.kafka.consumer.auto-offset-reset=earliest
spring.kafka.consumer.enable-auto-commit=false
spring.kafka.consumer.properties.isolation.level=read_committed

spring.kafka.producer.transaction-id-prefix=tx-

#logging.level.org.springframework.transaction=trace
#logging.level.org.springframework.kafka.transaction=debug
#logging.level.org.springframework.jdbc=debug
create table mytable (data varchar(20));

对于仅生产者的事务,事务同步是可行的:

@Transactional("dstm")
public void someMethod(String in) {
this.kafkaTemplate.send("topic2", in.toUpperCase());
this.jdbcTemplate.execute("insert into mytable (data) values ('" + in + "')");
}

KafkaTemplate 会将其事务与数据库事务同步,提交/回滚在数据库之后发生。

如果你希望先提交 Kafka 事务,并且只有在 Kafka 事务成功时才提交数据库事务,请使用嵌套的 @Transactional 方法:

@Transactional("dstm")
public void someMethod(String in) {
this.jdbcTemplate.execute("insert into mytable (data) values ('" + in + "')");
sendToKafka(in);
}

@Transactional("kafkaTransactionManager")
public void sendToKafka(String in) {
this.kafkaTemplate.send("topic2", in.toUpperCase());
}

定制 JsonSerializer 与 JsonDeserializer​

序列化器和反序列化器支持使用属性进行多种定制,更多信息请参阅 JSON。实例化这些对象的是 kafka-clients 代码,而非 Spring,除非你直接将它们注入消费者和生产者工厂。如果你希望用属性配置(反)序列化器,但又希望使用例如自定义的 JsonMapper,只需创建一个子类并将自定义 mapper 传入 super 构造器即可。例如:

public class CustomJsonSerializer extends JacksonJsonSerializer<Object> {

public CustomJsonSerializer() {
super(customizedJsonMapper());
}

private static JsonMapper customizedJsonMapper() {
return JacksonMapperUtils.enhancedJsonMapper()
.rebuild()
// .enable() and/or .disable() features according to your needs
.build();
}
}