提示、技巧與範例
手動指派所有分割區
假設您想要總是從所有分割區讀取所有記錄(例如,當使用壓縮主題來載入分散式快取時),手動指派分割區而不是使用 Kafka 的群組管理會很有用。當分割區很多時,這樣做可能會很笨拙,因為您必須列出分割區。如果分割區數量隨時間變化,也會產生問題,因為每次分割區計數變更時,您都必須重新編譯應用程式。
以下範例說明如何在應用程式啟動時,使用 SpEL 運算式的強大功能來動態建立分割區清單
@KafkaListener(topicPartitions = @TopicPartition(topic = "compacted",
partitions = "#{@finder.partitions('compacted')}"),
partitionOffsets = @PartitionOffset(partition = "*", initialOffset = "0")))
public void listen(@Header(KafkaHeaders.RECEIVED_MESSAGE_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
註解啟動 DB 交易。DB 交易會先提交;如果 Kafka 交易提交失敗,則會重新傳遞記錄,因此 DB 更新應該是冪等的。
@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://127.0.0.1/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
將使其交易與 DB 交易同步,並且提交/回滾會在資料庫之後發生。
如果您希望先提交 Kafka 交易,並且僅在 Kafka 交易成功時才提交 DB 交易,請使用巢狀 @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)會實例化這些物件,除非您將它們直接注入到消費者和生產者工廠中。如果您希望使用屬性來組態(反)序列化器,但希望使用自訂 ObjectMapper
,只需建立一個子類別,並將自訂 mapper 傳遞到 super
建構函式中。例如
public class CustomJsonSerializer extends JsonSerializer<Object> {
public CustomJsonSerializer() {
super(customizedObjectMapper());
}
private static ObjectMapper customizedObjectMapper() {
ObjectMapper mapper = JacksonUtils.enhancedObjectMapper();
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return mapper;
}
}