ヒント、コツ、例

すべてのパーティションを手動で割り当てる

常にすべてのパーティションからすべてのレコードを読み取りたい場合 (最適化されたトピックを使用して分散キャッシュをロードする場合など)、パーティションを手動で割り当て、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 と組み合わせて使用すると、アプリケーションが起動されるたびにすべてのレコードがロードされます。また、コンテナーが null コンシューマーグループのオフセットをコミットしないように、コンテナーの AckMode を MANUAL に設定する必要があります。バージョン 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://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 はそのトランザクションを 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 を参照してください。Spring ではなく kafka-clients コードは、これらのオブジェクトをコンシューマーおよびプロデューサーのファクトリに直接注入しない限り、これらのオブジェクトをインスタンス化します。プロパティを使用して(デ)シリアライザーを構成したいが、たとえばカスタム ObjectMapper を使用したい場合は、サブクラスを作成し、カスタムマッパーを 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;
    }

}