Kafka プロデューサー API とコンシューマー API の例

プロデューサーAPI の

 

通常の運用ロジックでは次の手順が必要です。

  1. プロデューサー パラメーターを構成し、対応するプロデューサー インスタンスを作成する

  2. 送信するメッセージを構築する

  3. メッセージを送ります

  4. プロデューサー インスタンスを閉じる

デフォルトのパーティション分割方法を使用してメッセージをハッシュし、各パーティションに送信します。

 

package com.doitedu;

import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;

import java.util.Properties;

public class KafkaProducerDemo {
    public static void main(String[] args) throws InterruptedException {
        /**
         * 1.构建一个kafka的客户端
         * 2.创建一些待发送的消息,构建成kafka所需要的格式
         * 3.调用kafka的api去发送消息
         * 4.关闭kafka生产实例
         */
        //1.创建kafka的对象,配置一些kafka的配置文件
        //它里面有一个泛型k,v
        //要发送数据的key
        //要发送的数据value
        //他有一个隐含之意,就是kafka发送的消息,是一个key,value类型的数据,但是不是必须得,其实只需要发送value的值就可以了
        Properties pros = new Properties();
        //指定kafka集群的地址
        pros.setProperty("bootstrap.servers", "linux01:9092,linux02:9092,linux03:9092");
        //指定key的序列化方式
        pros.setProperty("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        //指定value的序列化方式
        pros.setProperty("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        //ack模式,取值有0,1,-1(all),all是最慢但最安全的  服务器应答生产者成功的策略
        pros.put("acks", "all");
        //这是kafka发送数据失败的重试次数,这个可能会造成发送数据的乱序问题
        pros.setProperty("retries", "3");
        //数据发送批次的大小 单位是字节
        pros.setProperty("batch.size", "10000");
        //一次数据发送请求所能发送的最大数据量
        pros.setProperty("max.request.size", "102400");
        //消息在缓冲区保留的时间,超过设置的值就会被提交到服务端
        pros.put("linger.ms", 10000);
        //整个Producer用到总内存的大小,如果缓冲区满了会提交数据到服务端
        //buffer.memory要大于batch.size,否则会报申请内存不足的错误
        pros.put("buffer.memory", 10240);

        KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(pros);
        for (int i = 0; i < 1000; i++) {
            //key value  0 --> doit32+-->+0
            //key value  1 --> doit32+-->+1
            //key value  2 --> doit32+-->+2
            //2.创建一些待发送的消息,构建成kafka所需要的格式
            ProducerRecord<String, String> record = new ProducerRecord<>("test01", i + "", "doit32-->" + i);
            //3.调用kafka的api去发送消息
            kafkaProducer.send(record);
            Thread.sleep(100);
        }
        kafkaProducer.flush();
        kafkaProducer.close();
    }
}

 プロパティ設定を記述する 2 番目の方法では、比較的エラーが発生しません。簡単な例を次に示します。

public static void main(String[] args) {
    Properties pros = new Properties();
    pros.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "linux01:9092,linux02:9092,linux03:9092");
    pros.setProperty(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
    pros.setProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
}

 

1. Kafka プロデューサーはトピックにデータを継続的に送信できますか?

できる

2. Kafak のプロデューサーが設定する必要があるパラメータは何ですか:

//kafka クラスターのアドレスを指定しますpros.setProperty("bootstrap.servers", "linux01:9092,linux02:9092,linux03:9092"); //キーのシリアル化メソッドを指定しますpros.setProperty("key. serializer", "org.apache.kafka.common.serialization.StringSerializer"); //値のシリアル化方法を指定しますpros.setProperty("value.serializer","org.apache.kafka.common.serialization.StringSerializer") ;

3. Kafka プロデューサがデータを送信するとき、JDK シリアライザを使用してデータをシリアル化できますか?

いいえ、kafka には指定されたシリアル化インターフェイス org.apache.kafka.common.serialization.Serializer があります。

4. Kafka プロデューサーを構築した後、データをどのトピックに送信する必要があるかは決まりましたか?

いいえ、プロデューサー オブジェクトを構築するときにトピックを指定する必要はなく、送信データ オブジェクトを構築するときにのみ指定します。

 コンシューマAPI の

通常の消費ロジックでは、次の手順が必要です。

  1. コンシューマ クライアント パラメータを設定し、対応するコンシューマ インスタンスを作成します。

  2. トピックトピックを購読します。

  3. メッセージをプルして消費します。

  4. 消費変位オフセットを __consumer_offsets トピックに定期的に送信します。

  5. コンシューマインスタンスを閉じる

 

package com.doitedu;

import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.header.Header;
import org.apache.kafka.common.header.Headers;
import org.apache.kafka.common.record.TimestampType;
import org.apache.kafka.common.serialization.StringDeserializer;

import java.time.Duration;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Optional;
import java.util.Properties;

public class ConsumerDemo {
    public static void main(String[] args) {
        //1.创建kafka的消费者对象,附带着把配置文件搞定
        Properties props = new Properties();
        //props.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"linux01:9092,linux02:9092,linux03:9092");
        //props.setProperty(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        //props.setProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        // 定义kakfa 服务的地址,不需要将所有broker指定上
       // props.put("bootstrap.servers", "linux01:9092,linux02:9092,linux03:9092");
        // 制定consumer group
        props.put("group.id", "g3");
        // 是否自动提交offset  __consumer_offset   有多少分区  50 
        props.put("enable.auto.commit", "true");
        // 自动提交offset的时间间隔   -- 这玩意设置的大小怎么控制
        props.put("auto.commit.interval.ms", "5000");  //50000   1000
        // key的反序列化类
        props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        // value的反序列化类
        props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        // 如果没有消费偏移量记录,则自动重设为起始offset:latest, earliest, none
        props.put("auto.offset.reset","earliest");
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);

        //2.订阅主题(确定需要消费哪一个或者多个主题)
        consumer.subscribe(Arrays.asList("test02"));
        //3.开始从topic中获取数据
        while (true){
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(Integer.MAX_VALUE));
            for (ConsumerRecord<String, String> record : records) {
                //这是数据所属的哪一个topic
                String topic = record.topic();
                //该条数据的偏移量
                long offset = record.offset();
                //这条数据是哪一个分区的
                int partition = record.partition();
                //这条数据记录的时间戳,但是这个时间戳有两个类型
                long timestamp = record.timestamp();
                //上面时间戳的类型,这个类型有两个,一个是CreateTime(这条数据创建的时间), LogAppendTime(这条数据往日志里面追加的时间)
                TimestampType timestampType = record.timestampType();
                //这个数据key的值
                String key = record.key();
                //这条数据value的值
                String value = record.value();
                //分区leader的纪元
                Optional<Integer> integer = record.leaderEpoch();
                //key的长度
                int keySize = record.serializedKeySize();
                //value的长度
                int valueSize = record.serializedValueSize();
                //数据的头部信息
                Headers headers = record.headers();
//            for (Header header : headers) {
//                String hKey = header.key();
//                byte[] hValue = header.value();
//                String valueString = new String(hValue);
//                System.out.println("header的key值 = " + hKey + "header的value的值 = "+ valueString);
//            }
                System.out.printf("topic = %s ,offset = %d, partition = %d, timestampType = %s ,timestamp = %d , key = %s , value = %s ,leader的纪元 = %d , key序列化的长度 = %d ,value 序列化的长度 = %d \r\n" ,
                        topic,offset,partition,timestampType + "",timestamp,key,value,integer.get(),keySize,valueSize);
            }
        }

        //4.关闭消费者对象
//        consumer.close();
    }
}

 購読トピックを購読する

 subscribe には次のオーバーロードされたメソッドがあります。

public void subscribe(Collection<String> topics,ConsumerRebalanceListener listener) 
public void subscribe(Collection<String> topics) 
public void subscribe(Pattern pattern, ConsumerRebalanceListener listener) 
public void subscribe(Pattern pattern)
  1. トピックをサブスクライブするための収集方法を指定します

 consumer.subscribe(Arrays.asList(topicl ));

2. 定期的にトピックを購読する

コンシューマが正規表現 (subscribe(Pattern)) を使用してサブスクライブする場合、後続のプロセスで誰かが新しいトピックを作成し、トピック名が正規表現と一致すると、コンシューマは新しく追加されたトピックにメッセージを取り込むことができます。このサブスクリプション方法は、アプリケーションが複数のトピックを使用する必要があり、さまざまなタイプを処理できる場合に効果的です。

正規表現サブスクリプションの例

 consumer.subscribe(Pattern.compile ("topic.*" ));

 正規表現を使用してトピックをサブスクライブし、動的なサブスクリプションを実現します

 サブスクリプショントピックの割り当て

コンシューマーは、KafkaConsumer.subscribe() メソッドを通じてトピックをサブスクライブするだけでなく、特定のトピックの指定されたパーティションを直接サブスクライブすることもできます。

これらの機能を実装するために KafkaConsumer には assign() メソッドが提供されており、このメソッドの具体的な定義は次のとおりです。

 

package com.doitedu;

import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.header.Headers;
import org.apache.kafka.common.record.TimestampType;
import org.apache.kafka.common.serialization.StringDeserializer;

import java.time.Duration;
import java.util.Arrays;
import java.util.Optional;
import java.util.Properties;

public class ConsumerDemo1 {
    public static void main(String[] args) {
        //1.创建kafka的消费者对象,附带着把配置文件搞定
        Properties props = new Properties();
        props.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"linux01:9092,linux02:9092,linux03:9092");
        props.setProperty(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        props.setProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        props.setProperty(ConsumerConfig.GROUP_ID_CONFIG,"doit01");

        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);

        //2.订阅主题(确定需要消费哪一个或者多个主题)
//        consumer.subscribe(Arrays.asList("test03"));

//        consumer.poll(Duration.ofMillis(Integer.MAX_VALUE));
//        //我现在想手动指定,我需要从哪边开始消费
//        //如果用subscribe去订阅主题的时候,他内部会给这个消费者组来一个自动再均衡
//        consumer.seek(new TopicPartition("test03",0),2);
        TopicPartition tp01 = new TopicPartition("test03", 0);

        //他就是手动去订阅主题和partition,有了这个就不需要再去订阅subscribe主题了,手动指定以后,他的内部就不会再来自动均衡了
        consumer.assign(Arrays.asList(tp01)); // 手动订阅指定主题的指定分区的指定位置
        consumer.seek(new TopicPartition("test03",0),2);

        //3.开始从topic中获取数据
        while (true){
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(Integer.MAX_VALUE));
            for (ConsumerRecord<String, String> record : records) {
                //这是数据所属的哪一个topic
                String topic = record.topic();
                //该条数据的偏移量
                long offset = record.offset();
                //这条数据是哪一个分区的
                int partition = record.partition();
                //这条数据记录的时间戳,但是这个时间戳有两个类型
                long timestamp = record.timestamp();
                //上面时间戳的类型,这个类型有两个,一个是CreateTime(这条数据创建的时间), LogAppendTime(这条数据往日志里面追加的时间)
                TimestampType timestampType = record.timestampType();
                //这个数据key的值
                String key = record.key();
                //这条数据value的值
                String value = record.value();

                //分区leader的纪元
                Optional<Integer> integer = record.leaderEpoch();
                //key的长度
                int keySize = record.serializedKeySize();
                //value的长度
                int valueSize = record.serializedValueSize();
                //数据的头部信息
                Headers headers = record.headers();
//            for (Header header : headers) {
//                String hKey = header.key();
//                byte[] hValue = header.value();
//                String valueString = new String(hValue);
//                System.out.println("header的key值 = " + hKey + "header的value的值 = "+ valueString);
//            }
                System.out.printf("topic = %s ,offset = %d, partition = %d, timestampType = %s ,timestamp = %d , key = %s , value = %s ,leader的纪元 = %d , key序列化的长度 = %d ,value 序列化的长度 = %d \r\n" ,
                        topic,offset,partition,timestampType + "",timestamp,key,value,integer.get(),keySize,valueSize);
            }
        }

        //4.关闭消费者对象
//        consumer.close();
    }
}

 このメソッドは、サブスクライブする必要があるパーティション セットを指定するために使用されるパラメータ パーティションのみを受け入れます。例は次のとおりです。

consumer.assign(Arrays.asList(new TopicPartition ("tpc_1" , 0),new TopicPartition(“tpc_2”,1))) ;

 購読と割り当ての違い

 

  • subscribe() メソッドを介してトピックをサブスクライブすると、コンシューマの自動リバランスの機能が得られます。

複数のコンシューマの場合、各コンシューマとパーティションの間の関係は、パーティション割り当て戦略に従って自動的に割り当てることができます。コンシューマ グループ内のコンシューマの数が増減すると、パーティション割り当て関係が自動的に調整され、コンシューマのロード バランシングと自動フェイルオーバーが実現されます。

  • assign() メソッドがパーティションをサブスクライブする場合、自動コンシューマー・バランシングの機能はありません。

実際、これは assign メソッドのパラメータからもわかります。両方のタイプの submit() には ConsumerRebalanceListener タイプのパラメータを持つメソッドがありますが、assign() メソッドにはありません。

 メッセージ消費パターン

Kafka での消費はプル モデルに基づいています。

 通常、メッセージ消費にはプッシュ モードとプル モードの 2 つのモードがあります。プッシュ モードでは、サーバーがメッセージをコンシューマにアクティブにプッシュします。一方、プル モードでは、コンシューマがメッセージをプルするためにサーバーへのリクエストをアクティブに開始します。

 

public class ConsumerRecord<K, V> {
    public static final long NO_TIMESTAMP = RecordBatch.NO_TIMESTAMP;
    public static final int NULL_SIZE = -1;
    public static final int NULL_CHECKSUM = -1;

    private final String topic;
    private final int partition;
    private final long offset;
    private final long timestamp;
    private final TimestampType timestampType;
    private final int serializedKeySize;
    private final int serializedValueSize;
    private final Headers headers;
    private final K key;
    private final V value;

    private volatile Long checksum;
  • トピック パーティション これら 2 つの属性は、それぞれ、メッセージが属するトピックの名前と、メッセージが配置されているパーティションの番号を表します。

  • offset は、メッセージが属するパーティション内のメッセージのオフセットを表します。

  • timestamp はタイムスタンプを表し、対応する timestampType はタイムスタンプのタイプを表します。

  • timestampType には、CreateTime と LogAppendTime の 2 つのタイプがあり、それぞれメッセージ作成のタイムスタンプとログに追加されたメッセージのタイムスタンプを表します。

  • headers はメッセージのヘッダーの内容を表します。

  • キー値はそれぞれメッセージのキーとメッセージの値を表し、通常、ビジネス アプリケーションが読み取る必要があるのは値です。

  • generatedKeySize と generatedValueSize は、それぞれシリアル化後のキーと値のサイズを表します。キーが空の場合、serializedKeySize の値は -1 です。同様に、値が空の場合、serializedValueSize の値も -1 になります。

  • checksum は CRC32 チェック値です。

サンプルコードスニペット 

/**
 * 订阅与消费方式2
 */
TopicPartition tp1 = new TopicPartition("x", 0);
TopicPartition tp2 = new TopicPartition("y", 0);
TopicPartition tp3 = new TopicPartition("z", 0);
List<TopicPartition> tps = Arrays.asList(tp1, tp2, tp3);
consumer.assign(tps);
​
while (true) {
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
    for (TopicPartition tp : tps) {
        List<ConsumerRecord<String, String>> rList = records.records(tp);
        for (ConsumerRecord<String, String> r : rList) {
            r.topic();
            r.partition();
            r.offset();
            r.value();
            //do something to process record.
        }
    }
}

 変位消費の指定

場合によっては、特定のディスプレイスメントからメッセージの取得を開始できるようにする、よりきめ細かい制御が必要になります。KafkaConsumer の Seek() メソッドは、まさにこの機能を提供し、前方または後方へのコンシュームを可能にします。

Seek() メソッドの具体的な定義は次のとおりです。

seek都是和assign这个方法一起用 指定消费位置
public void seek(TopicPartiton partition,long offset)

 コード例:

public class ConsumerDemo3指定偏移量消费 {
    public static void main(String[] args) {

        Properties props = new Properties();
        props.setProperty(ConsumerConfig.GROUP_ID_CONFIG,"g002");
        props.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"doit01:9092");
        props.setProperty(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        props.setProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
        props.setProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"latest");
        // 是否自动提交消费位移
        props.setProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"true");

        // 限制一次poll拉取到的数据量的最大值
        props.setProperty(ConsumerConfig.FETCH_MAX_BYTES_CONFIG,"10240000");
         KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);

        // assign方式订阅doit27-1的两个分区
        TopicPartition tp0 = new TopicPartition("doit27-1", 0);
        TopicPartition tp1 = new TopicPartition("doit27-1", 1);
        
        consumer.assign(Arrays.asList(tp0,tp1));
        // 指定分区0,从offset:800开始消费    ;  分区1,从offset:650开始消费
        consumer.seek(tp0,200);
        consumer.seek(tp1,250);

        // 开始拉取消息
        while(true){
            ConsumerRecords<String, String> poll = consumer.poll(Duration.ofMillis(3000));
            for (ConsumerRecord<String, String> rec : poll) {
                System.out.println(rec.partition()+","+rec.key()+","+rec.value()+","+rec.offset());
            }
        }
    }
}

 コンシューマ オフセットを自動的にコミットする

 

Kafka での消費ディスプレイスメントのデフォルトの送信方法は自動送信です。これは、コンシューマ クライアント パラメータのenable.auto.commit によって構成されます。デフォルト値は true です。もちろん、このデフォルトの自動送信は、メッセージが消費されるたびに一度送信されるのではなく、定期的に送信されます。この定期的なサイクル タイムは、クライアント パラメーター auto.commit.interval.ms によって構成されます。デフォルト値は 5 秒です。このパラメータが有効になるのは、enable.auto.commit パラメータが true である場合です。

デフォルト モードでは、コンシューマーは 5 秒ごとにプルする各パーティションで最大メッセージ ディスプレイスメントをコミットします。自動ディスプレイスメント送信のアクションは、poll() メソッドのロジックで完了します。実際の各プル リクエストがサーバーに開始される前に、ディスプレイスメントを送信できるかどうかがチェックされます。送信できる場合は、最後のポーリングのディスプレイスメントが送信されます。提出されます。

ディスプレイスメントの送信は、Kafka 消費のプログラミング ロジックにおける大きな問題です。消費ディスプレイスメントを自動的に送信する方法は非常に簡単です。これにより、複雑なディスプレイスメントの送信ロジックが排除され、コーディングがより簡潔になります。しかし、その後に、繰り返しの消費とメッセージの損失の問題が発生します

  • リピート消費

消費ディスプレイスメントを送信したばかりで、消費するメッセージのバッチをプルしたとします。次の消費ディスプレイスメントが自動的に送信される前に、コンシューマがクラッシュします。その後、最後のディスプレイスメントが送信された場所から消費を再開する必要があります。 、繰り返し消費が発生する現象 (リバランス状況にも同じことが当てはまります)。ディスプレイスメント送信の時間間隔を短くすることで、繰り返されるメッセージのウィンドウ サイズを小さくすることができますが、これによって繰り返しの消費メッセージの送信が回避されるわけではなく、ディスプレイスメント送信の頻度も高くなります。

 

 

  • 失われたメッセージ

一般的な思考ロジックによれば、自動送信は送信の遅延であり、繰り返し消費されることは理解できますが、では、どのような状況でメッセージの損失が発生するのでしょうか? 下の写真の状況を見てみましょう。

プル スレッドは継続的にメッセージをプルし、ローカル キャッシュに保存します。たとえば、BlockingQueue では、別の処理スレッドがキャッシュからメッセージを読み取り、対応する論理処理を実行します。y+l 番目のプルと m 番目のディスプレイスメントの送信が現在進行中であると仮定します。つまり、x+6 より前のディスプレイスメントが確認され、送信されていますが、処理スレッドはまだ x+3 メッセージを処理中です。この時点処理スレッドで例外が発生しました。スレッドが回復すると、 にある m 番目のディスプレイスメント送信からx+6

 

  

 コンシューマ オフセットを手動でコミットする ( kafka apiを呼び出す)

 

自動ディスプレイスメント送信方法では、通常の状況ではメッセージの損失や繰り返し消費が発生しませんが、プログラミングの世界では例外は避けられず、同時に、自動ディスプレイスメント送信では正確なディスプレイスメント管理を実現できません。Kafka は、手動でディスプレイスメントを送信する方法も提供します。これにより、開発者は、消費ディスプレイスメントをより柔軟に管理および制御できます。

多くの場合、これはメッセージの取得後に消費が完了することを意味するのではなく、メッセージをデータベースに書き込むか、ローカル キャッシュに書き込むか、またはより複雑なビジネス処理を行う必要があることを意味します。これらのシナリオでは、すべてのビジネス処理が完了した後でのみ、メッセージが正常に消費されたとみなされる必要があります。

手動送信方法を使用すると、開発者はプログラムのロジックに従って適切なタイミングでディスプレイスメントを送信できます。手動送信機能を有効にするための前提条件は、コンシューマ クライアント パラメータのenable.auto.commit が false に設定されていることです。例は次のとおりです。

props.put(ConsumerConf.ENABLE_AUTO_COMMIT_CONFIG, false); 

 

手動送信は、KafkaConsumer の commitSync() と非同期送信に対応する、同期送信と非同期送信に細分化できます。

commitAsync() の 2 種類のメソッド。

  • 同期送信方法

commitSync() メソッドは次のように定義されます。

 

/**
 * 手动提交offset
 */
while (true) {
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
    for (ConsumerRecord<String, String> r : records) {
        //do something to process record.
    }
    consumer.commitSync();
}

 commitSync() を使用したパラメーターなしのメソッドの場合、消費ディスプレイスメントを送信する頻度は、バッチ メッセージを取得してバッチ メッセージを処理する頻度と同じになります。よりきめ細かく、より正確な送信を求める場合は、別のパラメーター化されたメソッドを使用する必要があります。 commitSync() のメソッドは具体的に次のように定義されています。

public void commitSync(final Map<TopicPartition,OffsetAndMetadata> offsets)

 サンプルコードは次のとおりです。

while (true) {
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
    for (ConsumerRecord<String, String> r : records) {
        long offset = r.offset();
        //do something to process record.

        TopicPartition topicPartition = new TopicPartition(r.topic(), r.partition());
        consumer.commitSync(Collections.singletonMap(topicPartition,new OffsetAndMetadata(offset+1)));
    }
}

 送信されたオフセット = 消費されたレコードのオフセット + 1

__consumer_offsets に記録された消費オフセットは、コンシューマーが次に読み取る位置を表すためです。

  • 非同期送信方法

非同期送信メソッド (commitAsync()) は、実行中にコンシューマ スレッドをブロックしません。送信された消費ディスプレイスメントの結果が返される前に、新しいプルが開始される可能性があります。非同期送信により、消費者のパフォーマンスをある程度向上させることができます。commitAsync メソッドには、次のように定義される別のオーバーロードされたメソッドがあります。

 

 

/**
 * 异步提交offset
 */
while (true) {
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
    for (ConsumerRecord<String, String> r : records) {
        long offset = r.offset();

        //do something to process record.
        TopicPartition topicPartition = new TopicPartition(r.topic(), r.partition());
        consumer.commitSync(Collections.singletonMap(topicPartition,new OffsetAndMetadata(offset+1)));
        consumer.commitAsync(Collections.singletonMap(topicPartition, new OffsetAndMetadata(offset + 1)), new OffsetCommitCallback() {
     @Override
     public void onComplete(Map<TopicPartition, OffsetAndMetadata> map, Exception e) {
                if(e == null ){
                    System.out.println(map);
                }else{
                    System.out.println("error commit offset");
                }
            }
        });
    }
}

 変位を手動で送信(タイミングの選択)

 

  • データ処理が完了する前にオフセットをコミットする

処理漏れ(データ損失)が発生する可能性がありますが、この方法では、最大 1 回のデータ処理 (送信) セマンティクスが実現されます。

  • データ処理が完了したらオフセットを送信します

処理が繰り返される現象 (データの重複)が発生する可能性があります。一方で、この方法では、少なくとも 1 回のデータ処理 (送信) セマンティクスが実現されます。もちろん、データ処理 (送信) の理想的なセマンティクスは、次のとおりです。一度) Kafka 正確に 1 回だけ実行することもできます (kafka に基づくトランザクション メカニズム)

コード例:

package com.doitedu;

import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.serialization.StringDeserializer;

import java.sql.*;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.Properties;

public class CommitOffsetByMyself {
    public static void main(String[] args) throws SQLException {

        //获取mysql的连接对象
        Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/football", "root", "123456");
        connection.setAutoCommit(false);
        PreparedStatement pps = connection.prepareStatement("insert into user values (?,?,?)");
        PreparedStatement pps_offset = connection.prepareStatement("insert into offset values (?,?) on duplicate key update offset = ?");

        Properties props = new Properties();
        props.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "linux01:9092,linux02:9092,linux03:9092");
        props.setProperty(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        props.setProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        //设置手动提交偏移量参数,需要将自动提交给关掉
        props.setProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
        //设置从哪里开始消费
//        props.setProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest");
        //设置组id
        props.setProperty(ConsumerConfig.GROUP_ID_CONFIG, "group001");

        KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);
        //订阅主题
        consumer.subscribe(Arrays.asList("kafka2mysql"), new ConsumerRebalanceListener() {
            @Override
            public void onPartitionsRevoked(Collection<TopicPartition> collection) {

            }

            @Override
            public void onPartitionsAssigned(Collection<TopicPartition> collection) {
                for (TopicPartition topicPartition : collection) {
                    try {
                        PreparedStatement get_offset = connection.prepareStatement("select offset from offset where topic_partition = ?");
                        String topic = topicPartition.topic();
                        int partition = topicPartition.partition();
                        get_offset.setString(1, topic + "_" + partition);
                        ResultSet resultSet = get_offset.executeQuery();
                        if (resultSet.next()){
                            int offset = resultSet.getInt(1);
                            System.out.println("发生了再均衡,被分配了分区消费权,并且查到了目标分区的偏移量"+partition+" , "+offset);
                            //拿到了offset后就可以定位消费了
                            consumer.seek(new TopicPartition(topic, partition), offset);
                        }
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        //拉去数据后写入到mysql
        while (true) {
            try {
                ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(Integer.MAX_VALUE));
                for (ConsumerRecord<String, String> record : records) {
                    String data = record.value();
                    String[] arr = data.split(",");
                    String id = arr[0];
                    String name = arr[1];
                    String age = arr[2];

                    pps.setInt(1, Integer.parseInt(id));
                    pps.setString(2, name);
                    pps.setInt(3, Integer.parseInt(age));
                    pps.execute();

                    //埋个异常,看看是不是真的是这样
//                    if (Integer.parseInt(id) == 5) {
//                        throw new SQLException();
//                    }

                    long offset = record.offset();
                    int partition = record.partition();
                    String topic = record.topic();
                    pps_offset.setString(1, topic + "_" + partition);
                    pps_offset.setInt(2, (int) offset + 1);
                    pps_offset.setInt(3, (int) offset + 1);
                    pps_offset.execute();
                    //提交jdbc事务
                    connection.commit();
                }
            } catch (Exception e) {
                connection.rollback();
            }
        }
    }
}

 消費者がオフセットを提出する方法の概要

消費者の消費変位の提出方法:

  • 全自動

    • auto.offset.commit = true

    • Consumer_offsets に定期的に送信する

  • 半自動

    • auto.offset.commit = false;

    • 次に、送信 Consumer.commitSync(); を手動でトリガーします。

    • Consumer_offsets に送信する

  • フルマニュアル

    • auto.offset.commit = false;

    • 独自のコードを記述して、消費ディスプレイスメントを独自の場所 mysql/zk/redis/ に保存します。

    • これを関係するストレージに送信します。初期化中に、カスタム ストレージから消費変位をクエリする必要もあります。

 

 

おすすめ

転載: blog.csdn.net/m0_53400772/article/details/131035869
おすすめ