Kafka的消費者(三)

消費者和消費組

消費者(Consumer)負責訂閱 Kafka 中的主題(Topic),並且從訂閱的主題上拉取消息。與其他一些消息中間件不同的是:在 Kafka 的消費理念中還有一層消費組(Consumer Group)的概念,每個消費者都有一個對應的消費組。當消息發佈到主題後,只會被投遞給訂閱它的每個消費組中的一個消費者。

消費者和消費組與分區的關係

如上圖,某個主題中共有4個分區(Partition):P0、P1、P2、P3。有兩個消費組A和B都訂閱了這個主題,消費組A中有4個消費者(C0、C1、C2和C3),消費組B中有2個消費者(C4和C5)。按照 Kafka 默認的規則,最後的分配結果是消費組A中的每一個消費者分配到1個分區,消費組B中的每一個消費者分配到2個分區,兩個消費組之間互不影響。每個消費者只能消費所分配到的分區中的消息。換言之,每一個分區只能被一個消費組中的一個消費者所消費

消費者個數變化所對應的分區分配的變化

假設目前某消費組內只有一個消費者C0,訂閱了一個主題,這個主題包含7個分區:P0、P1、P2、P3、P4、P5、P6。也就是說,這個消費者C0訂閱了7個分區,如下。

3-2

 

當消費組內又加入了一個新的消費者C1,C2,按照既定的邏輯,需要將原來消費者C0的部分分區分配給消費者C1,C2消費,如下圖所示。消費者C0,C1和C2各自負責消費所分配到的分區,彼此之間並無邏輯上的干擾。

 

 

3-4

再均衡

我們從上面的消費者演變圖中可以知道這麼一個過程:最初是一個消費者訂閱一個主題並消費其全部分區的消息,後來有一個消費者加入羣組,隨後又有更多的消費者加入羣組,而新加入的消費者實例分攤了最初消費者的部分消息,這種把分區的所有權通過一個消費者轉到其他消費者的行爲稱爲再均衡,英文名也叫做 Rebalance 。

再均衡非常重要,它爲消費者羣組帶來了高可用性和伸縮性,我們可以放心的添加消費者或移除消費者,不過,在再均衡期間,消費者無法讀取消息,造成整個消費者組在再均衡的期間都不可用。另外,當分區被重新分配給另一個消費者時,消息當前的讀取狀態會丟失,比如消費者消費完某個分區中的一部分消息時還沒有來得及提交消費位移就發生了再均衡操作,之後這個分區又被分配給了消費組內的另一個消費者,原來被消費完的那部分消息又被重新消費一遍,也就是發生了重複消費。所以一般情況下,應儘量避免不必要的再均衡的發生。它有可能還需要去刷新緩存,在它重新恢復狀態之前會拖慢應用程序。

消費者通過向組織協調者(Kafka Broker)發送心跳來維護自己是消費者組的一員並確認其擁有的分區。對於不同不的消費羣體來說,其組織協調者可以是不同的。只要消費者定期發送心跳,就會認爲消費者是存活的並處理其分區中的消息。當消費者檢索記錄或者提交它所消費的記錄時就會發送心跳。

如果過了一段時間 Kafka 停止發送心跳了,會話(Session)就會過期,組織協調者就會認爲這個 Consumer 已經死亡,就會觸發一次再均衡。如果消費者宕機並且停止發送消息,組織協調者會等待幾秒鐘,確認它死亡了纔會觸發再均衡。在這段時間裏,死亡的消費者將不處理任何消息。在清理消費者時,消費者將通知協調者它要離開羣組,組織協調者會觸發一次再均衡,儘量降低處理停頓。

再均衡是一把雙刃劍,它爲消費者羣組帶來高可用性和伸縮性的同時,還有有一些明顯的缺點(bug),而這些 bug 到現在社區還無法修改。

再均衡的過程對消費者組有極大的影響。因爲每次再均衡過程中都會導致STW,也就是說,在再均衡期間,消費者組中的消費者實例都會停止消費,等待再均衡的完成。而且再均衡這個過程很慢......

消費者與消費組這種模型可以讓整體的消費能力具備橫向伸縮性,我們可以增加(或減少)消費者的個數來提高(或降低)整體的消費能力。對於分區數固定的情況,一味地增加消費者並不會讓消費能力一直得到提升,如果消費者過多,出現了消費者的個數大於分區個數的情況,就會有消費者分配不到任何分區。以上的分配邏輯都是基於默認的分區分配策略進行分析的,可以通過消費者客戶端參數 partition.assignment.strategy 來設置消費者與訂閱主題之間的分區分配策略。

消息投遞模式

對於消息中間件而言,一般有兩種消息投遞模式:點對點(P2P,Point-to-Point)模式和發佈/訂閱(Pub/Sub)模式。

點對點模式是基於隊列的,消息生產者發送消息到隊列,消息消費者從隊列中接收消息。

發佈訂閱模式定義瞭如何向一個內容節點發布和訂閱消息,這個內容節點稱爲主題(Topic),主題可以認爲是消息傳遞的中介,消息發佈者將消息發佈到某個主題,而消息訂閱者從主題中訂閱消息。主題使得消息的訂閱者和發佈者互相保持獨立,不需要進行接觸即可保證消息的傳遞,發佈/訂閱模式在消息的一對多廣播時採用。Kafka 同時支持兩種消息投遞模式,而這正是得益於消費者與消費組模型的契合:

  • 如果所有的消費者都隸屬於同一個消費組,那麼所有的消息都會被均衡地投遞給每一個消費者,即每條消息只會被一個消費者處理,這就相當於點對點模式的應用。
  • 如果所有的消費者都隸屬於不同的消費組,那麼所有的消息都會被廣播給所有的消費者,即每條消息會被所有的消費者處理,這就相當於發佈/訂閱模式的應用。

消費組是一個邏輯上的概念,它將旗下的消費者歸爲一類,每一個消費者只隸屬於一個消費組。每一個消費組都會有一個固定的名稱,消費者在進行消費前需要指定其所屬消費組的名稱,這個可以通過消費者客戶端參數 group.id 來配置,默認值爲空字符串。

消費者並非邏輯上的概念,它是實際的應用實例,它可以是一個線程,也可以是一個進程。同一個消費組內的消費者既可以部署在同一臺機器上,也可以部署在不同的機器上。

 

消費者客戶端開發

消費者的hello world

public class KafkaCosumer {

        public static final String brokerList = "localhost:9092";
        public static final String topic = "topic-demo";
        public static final String groupId = "group.demo";
        public static final AtomicBoolean isRunning = new AtomicBoolean(true);

        public static Properties initConfig(){
            Properties props = new Properties();
            props.put("key.deserializer",
                    "org.apache.kafka.common.serialization.StringDeserializer");
            props.put("value.deserializer",
                    "org.apache.kafka.common.serialization.StringDeserializer");
            props.put("bootstrap.servers", brokerList);
            props.put("group.id", groupId);
            props.put("client.id", "consumer.client.id.demo");
            return props;
        }

        public static void main(String[] args) {
            Properties props = initConfig();
            KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
            consumer.subscribe(Arrays.asList(topic));

            try {
                while (isRunning.get()) {
                    ConsumerRecords<String, String> records =
                            consumer.poll(Duration.ofMillis(1000));
                    for (ConsumerRecord<String, String> record : records) {
                        System.out.println("topic = " + record.topic()
                                + ", partition = "+ record.partition()
                                + ", offset = " + record.offset());
                        System.out.println("key = " + record.key()
                                + ", value = " + record.value());
                        //do something to process record.
                    }
                }
            } catch (Exception e) {
                log.error("occur exception ", e);
            } finally {
                consumer.close();
            }
        }

必要的參數配置

在創建真正的消費者實例之前需要做相應的參數配置,在 Kafka 消費者客戶端 KafkaConsumer 中也有3個參數是必填的。

  • bootstrap.servers:和生產者客戶端 KafkaProducer 中的相同,用來指定連接 Kafka 集羣所需的 broker 地址清單,具體內容形式爲 host1:port1,host2:post,可以設置一個或多個地址,中間用逗號隔開,此參數的默認值爲“”。注意這裏並非需要設置集羣中全部的 broker 地址,消費者會從現有的配置中查找到全部的 Kafka 集羣成員。這裏設置兩個以上的 broker 地址信息,當其中任意一個宕機時,消費者仍然可以連接到 Kafka 集羣上。
  • group.id:消費者隸屬的消費組的名稱,默認值爲“”。如果設置爲空,則會報出異常:Exception in thread "main" org.apache.kafka.common.errors.InvalidGroupIdException: The configured groupId is invalid。一般而言,這個參數需要設置成具有一定的業務意義的名稱。
  • key.deserializer 和 value.deserializer:與生產者客戶端 KafkaProducer 中的 key.serializer和value.serializer 參數對應。消費者從 broker 端獲取的消息格式都是字節數組(byte[])類型,所以需要執行相應的反序列化操作才能還原成原有的對象格式。這兩個參數分別用來指定消息中 key 和 value 所需反序列化操作的反序列化器,這兩個參數無默認值。注意這裏必須填寫反序列化器類的全限定名,比如示例中的 org.apache.kafka.common.serialization.StringDeserializer。

此外,同kafkaProducer一樣,initConfig() 方法裏還設置了一個參數 client.id,這個參數用來設定 KafkaConsumer 對應的客戶端id,默認值也爲“”。如果客戶端不設置,則 KafkaConsumer 會自動生成一個非空字符串,內容形式如“consumer-1”、“consumer-2”,即字符串“consumer-”與數字的拼接。還可以根據業務應用的實際需求來修改這些參數的默認值,以達到靈活調配的目的。我們可以直接使用客戶端中的 org.apache.kafka.clients.consumer.ConsumerConfig 類來做一定程度上的預防,每個參數在 ConsumerConfig 類中都有對應的名稱:

public class ConsumerConfig extends AbstractConfig {
    private static final ConfigDef CONFIG;
    public static final String GROUP_ID_CONFIG = "group.id";
    private static final String GROUP_ID_DOC = "A unique string that identifies the consumer group this consumer belongs to. This property is required if the consumer uses either the group management functionality by using <code>subscribe(topic)</code> or the Kafka-based offset management strategy.";
    public static final String MAX_POLL_RECORDS_CONFIG = "max.poll.records";
    private static final String MAX_POLL_RECORDS_DOC = "The maximum number of records returned in a single call to poll().";
    public static final String MAX_POLL_INTERVAL_MS_CONFIG = "max.poll.interval.ms";
    private static final String MAX_POLL_INTERVAL_MS_DOC = "The maximum delay between invocations of poll() when using consumer group management. This places an upper bound on the amount of time that the consumer can be idle before fetching more records. If poll() is not called before expiration of this timeout, then the consumer is considered failed and the group will rebalance in order to reassign the partitions to another member. ";
    public static final String SESSION_TIMEOUT_MS_CONFIG = "session.timeout.ms";
    private static final String SESSION_TIMEOUT_MS_DOC = "The timeout used to detect consumer failures when using Kafka's group management facility. The consumer sends periodic heartbeats to indicate its liveness to the broker. If no heartbeats are received by the broker before the expiration of this session timeout, then the broker will remove this consumer from the group and initiate a rebalance. Note that the value must be in the allowable range as configured in the broker configuration by <code>group.min.session.timeout.ms</code> and <code>group.max.session.timeout.ms</code>.";
    public static final String HEARTBEAT_INTERVAL_MS_CONFIG = "heartbeat.interval.ms";
    private static final String HEARTBEAT_INTERVAL_MS_DOC = "The expected time between heartbeats to the consumer coordinator when using Kafka's group management facilities. Heartbeats are used to ensure that the consumer's session stays active and to facilitate rebalancing when new consumers join or leave the group. The value must be set lower than <code>session.timeout.ms</code>, but typically should be set no higher than 1/3 of that value. It can be adjusted even lower to control the expected time for normal rebalances.";
    public static final String BOOTSTRAP_SERVERS_CONFIG = "bootstrap.servers";
    public static final String ENABLE_AUTO_COMMIT_CONFIG = "enable.auto.commit";
    private static final String ENABLE_AUTO_COMMIT_DOC = "If true the consumer's offset will be periodically committed in the background.";
    public static final String AUTO_COMMIT_INTERVAL_MS_CONFIG = "auto.commit.interval.ms";
    private static final String AUTO_COMMIT_INTERVAL_MS_DOC = "The frequency in milliseconds that the consumer offsets are auto-committed to Kafka if <code>enable.auto.commit</code> is set to <code>true</code>.";
    public static final String PARTITION_ASSIGNMENT_STRATEGY_CONFIG = "partition.assignment.strategy";
    private static final String PARTITION_ASSIGNMENT_STRATEGY_DOC = "The class name of the partition assignment strategy that the client will use to distribute partition ownership amongst consumer instances when group management is used";
    public static final String AUTO_OFFSET_RESET_CONFIG = "auto.offset.reset";
    public static final String AUTO_OFFSET_RESET_DOC = "What to do when there is no initial offset in Kafka or if the current offset does not exist any more on the server (e.g. because that data has been deleted): <ul><li>earliest: automatically reset the offset to the earliest offset<li>latest: automatically reset the offset to the latest offset</li><li>none: throw exception to the consumer if no previous offset is found for the consumer's group</li><li>anything else: throw exception to the consumer.</li></ul>";
    public static final String FETCH_MIN_BYTES_CONFIG = "fetch.min.bytes";
    private static final String FETCH_MIN_BYTES_DOC = "The minimum amount of data the server should return for a fetch request. If insufficient data is available the request will wait for that much data to accumulate before answering the request. The default setting of 1 byte means that fetch requests are answered as soon as a single byte of data is available or the fetch request times out waiting for data to arrive. Setting this to something greater than 1 will cause the server to wait for larger amounts of data to accumulate which can improve server throughput a bit at the cost of some additional latency.";
    public static final String FETCH_MAX_BYTES_CONFIG = "fetch.max.bytes";
    private static final String FETCH_MAX_BYTES_DOC = "The maximum amount of data the server should return for a fetch request. Records are fetched in batches by the consumer, and if the first record batch in the first non-empty partition of the fetch is larger than this value, the record batch will still be returned to ensure that the consumer can make progress. As such, this is not a absolute maximum. The maximum record batch size accepted by the broker is defined via <code>message.max.bytes</code> (broker config) or <code>max.message.bytes</code> (topic config). Note that the consumer performs multiple fetches in parallel.";
    public static final int DEFAULT_FETCH_MAX_BYTES = 52428800;
    public static final String FETCH_MAX_WAIT_MS_CONFIG = "fetch.max.wait.ms";
    private static final String FETCH_MAX_WAIT_MS_DOC = "The maximum amount of time the server will block before answering the fetch request if there isn't sufficient data to immediately satisfy the requirement given by fetch.min.bytes.";
    public static final String METADATA_MAX_AGE_CONFIG = "metadata.max.age.ms";
    public static final String MAX_PARTITION_FETCH_BYTES_CONFIG = "max.partition.fetch.bytes";
    private static final String MAX_PARTITION_FETCH_BYTES_DOC = "The maximum amount of data per-partition the server will return. Records are fetched in batches by the consumer. If the first record batch in the first non-empty partition of the fetch is larger than this limit, the batch will still be returned to ensure that the consumer can make progress. The maximum record batch size accepted by the broker is defined via <code>message.max.bytes</code> (broker config) or <code>max.message.bytes</code> (topic config). See fetch.max.bytes for limiting the consumer request size.";
    public static final int DEFAULT_MAX_PARTITION_FETCH_BYTES = 1048576;
    public static final String SEND_BUFFER_CONFIG = "send.buffer.bytes";
    public static final String RECEIVE_BUFFER_CONFIG = "receive.buffer.bytes";
    public static final String CLIENT_ID_CONFIG = "client.id";
    public static final String RECONNECT_BACKOFF_MS_CONFIG = "reconnect.backoff.ms";
    public static final String RECONNECT_BACKOFF_MAX_MS_CONFIG = "reconnect.backoff.max.ms";
    public static final String RETRY_BACKOFF_MS_CONFIG = "retry.backoff.ms";
    public static final String METRICS_SAMPLE_WINDOW_MS_CONFIG = "metrics.sample.window.ms";
    public static final String METRICS_NUM_SAMPLES_CONFIG = "metrics.num.samples";
    public static final String METRICS_RECORDING_LEVEL_CONFIG = "metrics.recording.level";
    public static final String METRIC_REPORTER_CLASSES_CONFIG = "metric.reporters";
    public static final String CHECK_CRCS_CONFIG = "check.crcs";
    private static final String CHECK_CRCS_DOC = "Automatically check the CRC32 of the records consumed. This ensures no on-the-wire or on-disk corruption to the messages occurred. This check adds some overhead, so it may be disabled in cases seeking extreme performance.";
    public static final String KEY_DESERIALIZER_CLASS_CONFIG = "key.deserializer";
    public static final String KEY_DESERIALIZER_CLASS_DOC = "Deserializer class for key that implements the <code>org.apache.kafka.common.serialization.Deserializer</code> interface.";
    public static final String VALUE_DESERIALIZER_CLASS_CONFIG = "value.deserializer";
    public static final String VALUE_DESERIALIZER_CLASS_DOC = "Deserializer class for value that implements the <code>org.apache.kafka.common.serialization.Deserializer</code> interface.";
    public static final String CONNECTIONS_MAX_IDLE_MS_CONFIG = "connections.max.idle.ms";
    public static final String REQUEST_TIMEOUT_MS_CONFIG = "request.timeout.ms";
    private static final String REQUEST_TIMEOUT_MS_DOC = "The configuration controls the maximum amount of time the client will wait for the response of a request. If the response is not received before the timeout elapses the client will resend the request if necessary or fail the request if retries are exhausted.";
    public static final String DEFAULT_API_TIMEOUT_MS_CONFIG = "default.api.timeout.ms";
    public static final String DEFAULT_API_TIMEOUT_MS_DOC = "Specifies the timeout (in milliseconds) for consumer APIs that could block. This configuration is used as the default timeout for all consumer operations that do not explicitly accept a <code>timeout</code> parameter.";
    public static final String INTERCEPTOR_CLASSES_CONFIG = "interceptor.classes";
    public static final String INTERCEPTOR_CLASSES_DOC = "A list of classes to use as interceptors. Implementing the <code>org.apache.kafka.clients.consumer.ConsumerInterceptor</code> interface allows you to intercept (and possibly mutate) records received by the consumer. By default, there are no interceptors.";
    public static final String EXCLUDE_INTERNAL_TOPICS_CONFIG = "exclude.internal.topics";
    private static final String EXCLUDE_INTERNAL_TOPICS_DOC = "Whether records from internal topics (such as offsets) should be exposed to the consumer. If set to <code>true</code> the only way to receive records from an internal topic is subscribing to it.";
    public static final boolean DEFAULT_EXCLUDE_INTERNAL_TOPICS = true;
    static final String LEAVE_GROUP_ON_CLOSE_CONFIG = "internal.leave.group.on.close";
    public static final String ISOLATION_LEVEL_CONFIG = "isolation.level";
    public static final String ISOLATION_LEVEL_DOC = "<p>Controls how to read messages written transactionally. If set to <code>read_committed</code>, consumer.poll() will only return transactional messages which have been committed. If set to <code>read_uncommitted</code>' (the default), consumer.poll() will return all messages, even transactional messages which have been aborted. Non-transactional messages will be returned unconditionally in either mode.</p> <p>Messages will always be returned in offset order. Hence, in  <code>read_committed</code> mode, consumer.poll() will only return messages up to the last stable offset (LSO), which is the one less than the offset of the first open transaction. In particular any messages appearing after messages belonging to ongoing transactions will be withheld until the relevant transaction has been completed. As a result, <code>read_committed</code> consumers will not be able to read up to the high watermark when there are in flight transactions.</p><p> Further, when in <code>read_committed</mode> the seekToEnd method will return the LSO";
    public static final String DEFAULT_ISOLATION_LEVEL;

同樣,key.deserializer 和 value.deserializer 參數對應類的全限定名比較長,也比較容易寫錯,這裏通過 Java 中的技巧來做進一步的改進,如下:

        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, 
                StringDeserializer.class.getName());
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
                StringDeserializer.class.getName());

 

在KafkaConsumer 配置相關的內容基本上和介紹 KafkaProducer 配置時的一樣,除了配置對應的反序列化器,只多了一個必要的 group.id 參數。

訂閱主題和分區

在創建好消費者之後,我們就需要爲該消費者訂閱相關的主題了。一個消費者可以訂閱一個或多個主題,hello world代碼中我們使用 subscribe() 方法訂閱了一個主題,對於這個方法而言,既可以以集合的形式訂閱多個主題,也可以以正則表達式的形式訂閱特定模式的主題。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)

對於消費者使用集合的方式(subscribe(Collection))來訂閱主題而言。如果前後兩次訂閱了不同的主題,那麼消費者以最後一次的爲準。

如果消費者採用的是正則表達式的方式(subscribe(Pattern))訂閱,在之後的過程中,如果有人又創建了新的主題,並且主題的名字與正則表達式相匹配,那麼這個消費者就可以消費到新添加的主題中的消息。如果應用程序需要消費多個主題,並且可以處理不同的類型,那麼這種訂閱方式就很有效。在 Kafka 和其他系統之間進行數據複製時,這種正則表達式的方式就顯得很常見。正則表達式的方式訂閱的示例如下:

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

消費者不僅可以通過 KafkaConsumer.subscribe() 方法訂閱主題,還可以直接訂閱某些主題的特定分區,在 KafkaConsumer 中還提供了一個 assign() 方法來實現這些功能,此方法的具體定義如下:

public void assign(Collection<TopicPartition> partitions)

這個方法只接受一個參數 partitions,用來指定需要訂閱的分區集合。其中TopicPartition 類,在 Kafka 的客戶端中,它用來表示分區,這個類的部分內容如下所示。

public final class TopicPartition implements Serializable {

    private final int partition;
    private final String topic;

    public TopicPartition(String topic, int partition) {
        this.partition = partition;
        this.topic = topic;
    }

    public int partition() {
        return partition;
    }

    public String topic() {
        return topic;
    }
    //省略hashCode()、equals()和toString()方法
}

TopicPartition 類只有2個屬性:topic 和 partition,分別代表分區所屬的主題和自身的分區編號,這個類可以和我們通常所說的主題—分區的概念映射起來。如下訂閱 topic-demo 主題中分區編號爲0的分區:

consumer.assign(Arrays.asList(new TopicPartition("topic-demo", 0)));

獲取主題中的所有分區的信息

由於我們事先並不知道主題中有多少個分區,在KafkaConsumer 中的 partitionsFor() 方法可以用來查詢指定主題的元

數據信息,由此得到分區編號的信息,partitionsFor() 方法的具體定義如下:

public List<PartitionInfo> partitionsFor(String topic)

其中 PartitionInfo 類型即爲主題的分區元數據信息,此類的主要結構如下:

public class PartitionInfo {
    private final String topic;
    private final int partition;
    private final Node leader;
    private final Node[] replicas;
    private final Node[] inSyncReplicas;
    private final Node[] offlineReplicas;
	    //這裏省略了構造函數、屬性提取、toString等方法
}

PartitionInfo 類中的屬性 topic 表示主題名稱,partition 代表分區編號,leader 代表分區的 leader 副本所在的位置,replicas 代表分區的 AR 集合,inSyncReplicas 代表分區的 ISR 集合,offlineReplicas 代表分區的 OSR 集合。 通過 partitionFor() 方法的協助,我們可以通過 assign() 方法來實現訂閱主題(全部分區)的功能,如下:

List<TopicPartition> partitions = new ArrayList<>();
List<PartitionInfo> partitionInfos = consumer.partitionsFor(topic);
if (partitionInfos != null) {
    for (PartitionInfo tpInfo : partitionInfos) {
        partitions.add(new TopicPartition(tpInfo.topic(), tpInfo.partition()));
    }
}
consumer.assign(partitions);

取消訂閱

方式1:可以使用 KafkaConsumer 中的 unsubscribe() 方法來取消主題的訂閱。這個方法既可以取消通過 subscribe(Collection) 方式和 subscribe(Pattern) 方式實現的主題訂閱,也可以取消通過 assign(Collection) 方式實現的分區訂閱。如下:

consumer.unsubscribe();

方式2:將 subscribe(Collection) 或 assign(Collection) 中的集合參數設置爲空集合,那麼作用等同於 unsubscribe() 方法,下面示例中的三行代碼的效果相同:

consumer.unsubscribe();
consumer.subscribe(new ArrayList<String>());
consumer.assign(new ArrayList<TopicPartition>());

如果沒有訂閱任何主題或分區,那麼再繼續執行消費程序的時候會報出 IllegalStateException 異常:

java.lang.IllegalStateException: Consumer is not subscribed to any topics or assigned any partitions

訂閱的狀態

集合訂閱的方式 subscribe(Collection)、正則表達式訂閱的方式 subscribe(Pattern) 和指定分區的訂閱方式 assign(Collection) 分表代表了三種不同的訂閱狀態:AUTO_TOPICS、AUTO_PATTERN 和 USER_ASSIGNED(如果沒有訂閱,那麼訂閱狀態爲 NONE)。然而這三種狀態是互斥的,在一個消費者中只能使用其中的一種,否則會報出 IllegalStateException 異常:

java.lang.IllegalStateException: Subscription to topics, partitions and pattern are mutually exclusive.

通過 subscribe() 方法訂閱主題具有消費者自動再均衡的功能,在多個消費者的情況下可以根據分區分配策略來自動分配各個消費者與分區的關係。當消費組內的消費者增加或減少時,分區分配關係會自動調整,以實現消費負載均衡及故障自動轉移。而通過 assign() 方法訂閱分區時,是不具備消費者自動均衡的功能的,其實這一點從 assign() 方法的參數中就可以看出端倪,兩種類型的 subscribe() 都有 ConsumerRebalanceListener 類型參數的方法,而 assign() 方法卻沒有。ConsumerRebalanceListener這個是用來設置相應的再均衡監聽器的

 

消息的反序列化

與 KafkaProducer 對應的序列化器相對應的就是KafkaConsumer 的反序列化器。Kafka 所提供的反序列化器有: ByteBufferDeserializer、ByteArrayDeserializer、BytesDeserializer、DoubleDeserializer、FloatDeserializer、IntegerDeserializer、LongDeserializer、ShortDeserializer、StringDeserializer,它們分別用於 ByteBuffer、ByteArray、Bytes、Double、Float、Integer、Long、Short 及 String 類型的反序列化,這些序列化器也都實現了 Deserializer 接口,與 KafkaProducer 中提及的 Serializer 接口一樣,Deserializer 接口也有三個方法。

public void configure(Map<String, ?> configs, boolean isKey)//用來配置當前類。
public byte[] deserialize(String topic, byte[] data)//用來執行反序列化。如果 data 爲 null,那麼處理的時候直接返回 null 而不是拋出一個異常。
public void close()//用來關閉當前序列化器。

在自定義的序列化器來序列化自定義的 Company 類,下面是Company對應的反序列化器 CompanyDeserializer 的具體實現

public class CompanyDeserializer implements Deserializer<Company> {
    public void configure(Map<String, ?> configs, boolean isKey) {}

    public Company deserialize(String topic, byte[] data) {
        if (data == null) {
            return null;
        }
        if (data.length < 8) {
            throw new SerializationException("Size of data received " +
                    "by DemoDeserializer is shorter than expected!");
        }
        ByteBuffer buffer = ByteBuffer.wrap(data);
        int nameLen, addressLen;
        String name, address;

        nameLen = buffer.getInt();
        byte[] nameBytes = new byte[nameLen];
        buffer.get(nameBytes);
        addressLen = buffer.getInt();
        byte[] addressBytes = new byte[addressLen];
        buffer.get(addressBytes);

        try {
            name = new String(nameBytes, "UTF-8");
            address = new String(addressBytes, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new SerializationException("Error occur when deserializing!");
        }

        return new Company(name,address);
    }

    public void close() {}
}

configure() 方法和 close() 方法都是空實現,而 deserializer() 方法就是將字節數組轉換成對應 Company 對象。同樣地,在使用自定義的反序列化器的時候只需要將相應的 value.deserializer 參數配置爲 CompanyDeserializer 即可,示例如下:

props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
        CompanyDeserializer.class.getName());

注意如無特殊需要,不建議使用自定義的序列化器或反序列化器,因爲這樣會增加生產者與消費者之間的耦合度

 

消費消息

Kafka 中的消費是基於拉模式的。消息的消費一般有兩種模式:推模式和拉模式。推模式是服務端主動將消息推送給消費者,而拉模式是消費者主動向服務端發起請求來拉取消息。

在hello world代碼中,Kafka 中的消息消費是一個不斷輪詢的過程,消費者所要做的就是重複地調用 poll() 方法,而 poll() 方法返回的是所訂閱的主題(分區)上的一組消息,對於 poll() 方法而言,如果訂閱的某些分區中沒有可供消費的消息,那麼此分區對應的消息拉取的結果就爲空;如果訂閱的所有分區中都沒有可供消費的消息,那麼 poll() 方法返回爲空的消息集合。

poll() 方法的具體定義如下:

public ConsumerRecords<K, V> poll(final Duration timeout)

注意到 poll() 方法裏還有一個超時時間參數 timeout,用來控制 poll() 方法的阻塞時間,在消費者的緩衝區裏沒有可用數據時會發生阻塞。(這裏 timeout 的類型是 Duration,它是 JDK8 中新增的一個與時間有關的類型。在 Kafka 2.0.0 之前的版本中,timeout 參數的類型爲 long),timeout 的設置取決於應用程序對響應速度的要求,比如需要在多長時間內將控制權移交給執行輪詢的應用線程。可以直接將 timeout 設置爲0,這樣 poll() 方法會立刻返回,而不管是否已經拉取到了消息。如果應用線程唯一的工作就是從 Kafka 中拉取並消費消息,則可以將這個參數設置爲最大值 Long.MAX_VALUE。

poll() 方法的返回值類型是 ConsumerRecords,通過遍歷消費者可以獲取到的每條消息的類型爲 ConsumerRecord(注意與 ConsumerRecords 的區別),這個和生產者發送的消息類型 ProducerRecord 相對應,不過 ConsumerRecord 中的內容更加豐富,結構參考如下:

public class ConsumerRecord<K, V> {
    private final String topic;//消息所屬主題
    private final int partition;//消息所在分區
    private final long offset;//所在分區的offset
    private final long timestamp;//時間戳
    private final TimestampType timestampType;//對應的 timestampType 表示時間戳的類型
//有兩種類型:CreateTime和LogAppendTime,分別代表消息創建的時間戳和消息追加到日誌的時間戳
    private final int serializedKeySize;//key 經過序列化之後的大小,如果key爲空,則這個值爲-1
    private final int serializedValueSize;//value經過序列化之後的大小,value爲空,則這個值爲-1
    private final Headers headers;//消息的頭部內容
    private final K key;//消息的key
    private final V value;//消息的value
    private volatile Long checksum;//CRC32 的校驗值
	    //省略若干方法
}

 返回值ConsumerRecords 用來表示一次拉取操作所獲得的消息集,內部包含了若干 ConsumerRecord,常用的方法如下:

//獲取消息集中指定分區的消息
public List<ConsumerRecord<K, V>> records(TopicPartition partition)

//獲取指定主題的消息
public Iterable<ConsumerRecord<K, V>> records(String topic)

//獲取消息集中所有分區的消息
public Set<TopicPartition> partitions()


//迭代的方法,用來循環遍歷消息集內部的消息
public Iterable<ConsumerRecord<K, V>> records(String topic) 


//計算出消息集中的消息個數
public int count()

//用來判斷消息集是否爲空
public boolean isEmpty()

//用來獲取一個空的消息集,返回類型是 ConsumerRecords<K,V>。
public static <K, V> ConsumerRecords<K, V> empty() 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章