KafkaProducer
1.發送消息流程
生產者與服務端完成一次網絡通信步驟如下:
-
生產者客戶端應用程序產生消息
-
客戶端連接對象將消息包裝到請求中,發送到服務端
-
服務短連接對象負責接收請求,並將消息以文件形式存儲
-
服務端返回響應結果給生產者客戶端
以上流程對應到代碼中如下: -
創建ProducerRecord對象,此對象包含目標主題和要發送的內容,還可以指定鍵或者分區
-
生產者對ProducerRecord對象進行序列化
-
分區器接收到消息後,先來選擇一個分區
-
這條記錄被添加到一個記錄批次裏,這個批次會被髮送到相同的主題和分區上
-
獨立線程負責把這些記錄批次,發送到相應的broker上
-
broker就是服務器,在接收到消息時,會返回一個響應,如果成功,返回一個RecordMetaData對象,它包含了主題和分區信息,以及記錄在分區裏的偏移量。如果失敗,就會返回一個錯誤
-
生產者如果接收到錯誤之後,會嘗試重新發送,幾次嘗試後,還是失敗,就返回錯誤信息
生產者發送的代碼在clients包下,下面我們詳細講解下發送流程,附有源碼和流程圖,方便大家理解。
2.實例
要往Kafka裏面寫入消息,首先要創建一個生產者對象,並設置一些屬性。
配置屬性:
- bootstrap.servers
該屬性指定broker的地址清單,地址的格式爲host:port - key.serializer
指定了序列化的類,生產者會使用這個類把鍵對象序列化成字節數組 - value.serializer
指定了序列化的類,生產者會使用這個類把值序列化 - client.id
客戶端的id
發送消息的方式: - 發送並忘記
我們把消息發送給服務器,但並不關心它是否正常到達 - 同步發送
我們使用send()方法發送消息,它會返回一個Future對象,調用get()方法進行等待,就可以知道消息是否發送成功 - 異步發送
我們調用send()方法,並指定一個回調 函數,服務器在返回響應時調用該函數
我們來看下源碼中的例子
public class Producer extends Thread {
private final KafkaProducer<Integer, String> producer;
private final String topic;
//定義消息的發送方式:異步發送還是同步發送
private final Boolean isAsync;
public Producer(String topic, Boolean isAsync) {
Properties props = new Properties();
//Kafka服務端的主機名和端口號
props.put("bootstrap.servers", "localhost:9092");
//客戶端的id
props.put("client.id", "DemoProducer");
//key的序列化方法
props.put("key.serializer", "org.apache.kafka.common.serialization.IntegerSerializer");
//value的序列化方法
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
producer = new KafkaProducer<>(props);
this.topic = topic;
this.isAsync = isAsync;
}
public void run() {
//消息的key
int messageNo = 1;
while (true) {
//消息的value
String messageStr = "Message_" + messageNo;
long startTime = System.currentTimeMillis();
if (isAsync) { // Send asynchronously 異步發送
//第一個參數是ProducerRecord對象,封裝了目標Topic、消息的key,消息的value
//第二個參數是一個Callback對象,當生產者接收到Kafka發來的ACK確認消息的時候,
//會調用此CallBack對象的onCompletion()方法,實現回調
producer.send(new ProducerRecord<>(topic,
messageNo,
messageStr), new DemoCallBack(startTime, messageNo, messageStr));
} else { // Send synchronously 同步發送
try {
//send()返回的是一個Future<RecordMetadata>這裏通過Future.get()方法,阻塞當前線程
//等待Kafka服務端的ACK響應
producer.send(new ProducerRecord<>(topic,
messageNo,
messageStr)).get();
System.out.println("Sent message: (" + messageNo + ", " + messageStr + ")");
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
//遞增消息的Key
++messageNo;
}
}
}
//回調對象
class DemoCallBack implements Callback {
//開始發送消息的時間戳
private final long startTime;
//消息的Key
private final int key;
//消息的value
private final String message;
public DemoCallBack(long startTime, int key, String message) {
this.startTime = startTime;
this.key = key;
this.message = message;
}
/**
* 生產者成功發送消息,收到Kafka服務端發來的ACK確認消息後,會調用此回調函數
*
* @param metadata 生產者發送的消息的元數據,如果發送過程中出現異常,此參數爲null
* @param exception 發送過程中出現的異常,如果發送成功,則此參數爲null
*/
public void onCompletion(RecordMetadata metadata, Exception exception) {
long elapsedTime = System.currentTimeMillis() - startTime;
if (metadata != null) {
//RecordMetadata中包含了分區信息、offset信息等
System.out.println(
"message(" + key + ", " + message + ") sent to partition(" + metadata.partition() +
"), " +
"offset(" + metadata.offset() + ") in " + elapsedTime + " ms");
} else {
exception.printStackTrace();
}
}
}
KafkaProducer只用了一個send方法,就可以完成同步和異步兩種模式的消息發送。這是因爲send方法返回的是一個Future。基於Future,我們可以實現同步或異步的消息發送語義。
- 同步。調用send返回Future時,需要立即調用get,因爲Future.get在沒有返回結果時會一直阻塞。
- 異步。提供一個回調,調用send後,可以繼續發送消息而不用等待。當有結果返回時,會自動執行回調函數。
源碼中的註釋寫的很清楚了,就不再贅述了,大家有興趣也可以把源碼從github上下載下來。
3.kafkaProducer分析
我們來看下發送消息時的類圖
消息發送的流程如上,主要有3個模塊,主線程,負責生產消息並序列化,選擇分區。然後將消息存放到RecordAccumulator中,相當於記錄的緩衝區,當緩衝區滿了之後,sender線程會從緩衝區中讀取數據,併發送到Kafka的服務器上。
- ProducerInterceptors對消息進行攔截
- Serializer對消息的key和value進行序列化
- Patitioner爲消息選擇合適的Partition
- RecordAccumulator收集消息,實現批量發送
- Sernder線程從RecordAccumulator中收集消息,實現批量發送
- 構造ClientRequest
- 將ClientRequest交給NetworkClient,準備發送
- NetworkClient將請求放入KafkaChannel
- 執行網絡IO,發送請求
- 收到響應,調用ClientRequest的回調函數
- 調用RecordBatch的回調函數,最終調用每個消息上註冊的回調函數
代碼調用的時序圖如下:
解釋一下上圖的主要流程:
1.1:調用ProducerInterceptors的onSend()方法,對消息進行加工
1.2:實際發送doSend()方法
1.2.1:waitOnMetadata判斷是否要更新元數據,以及wakeUp sender線程去更新元數據
1.2.2:序列化key
1.2.3:序列化value
1.2.4:獲取分區信息
1.2.5:保證記錄的size小於最大的請求size,小於totalSize
1.2.6:將記錄追加到RecordAccumulator中
1.2.7:wakeup sender線程,去發送數據
現在呢,我們大體流程已經都瞭解了,下面我們深入去看下里面涉及到的部分邏輯
3.1 ProducerInterceptors
ProducerInterceptors裏面維護了一個ProducerInterceptor的list。這個類,主要是在消息被髮送到Kafka服務器前,或者接收到了kafka對消息處理信息之後,進行加工處理。
ProducerInterceptor中主要有三個方法
onSend():在消息發送到kafka服務器之前對消息進行處理
onAcknowledgement():在收到kafka服務器響應之後(包含正常響應和異常響應),對信息進行處理,會先於用戶自定義的回調方法
close():關閉時使用
在ProducerInterceptors裏面調用onSend和onAcknowledgement都不會響應錯誤,只會打印錯誤日誌,並繼續執行下一個攔截。所以我們自定義ProducerInterceptor時,需要自己在方法內,校驗,並處理異常邏輯。我們看一下源代碼
public ProducerRecord<K, V> onSend(ProducerRecord<K, V> record) {
ProducerRecord<K, V> interceptRecord = record;
for (ProducerInterceptor<K, V> interceptor : this.interceptors) {
try {
interceptRecord = interceptor.onSend(interceptRecord);
} catch (Exception e) {
// 不傳播攔截器異常,記錄並繼續調用其他攔截器,注意這裏不會返回異常
if (record != null)
log.warn("Error executing interceptor onSend callback for topic: {}, partition: {}", record.topic(), record.partition(), e);
else
log.warn("Error executing interceptor onSend callback", e);
}
}
return interceptRecord;
}
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
for (ProducerInterceptor<K, V> interceptor : this.interceptors) {
try {
interceptor.onAcknowledgement(metadata, exception);
} catch (Exception e) {
// do not propagate interceptor exceptions, just log
log.warn("Error executing interceptor onAcknowledgement callback", e);
}
}
}
onAcknowledgement方法中會把異常傳遞到實現類中,我們就可以根據這個來判斷,消息是正常返回,還是異常返回了
kafka源碼中有使用ProducerInterceptor的例子,我們來看一下
private class AppendProducerInterceptor implements ProducerInterceptor<Integer, String> {
private String appendStr = "";
private boolean throwExceptionOnSend = false;
private boolean throwExceptionOnAck = false;
public AppendProducerInterceptor(String appendStr) {
this.appendStr = appendStr;
}
@Override
public void configure(Map<String, ?> configs) {
}
@Override
public ProducerRecord<Integer, String> onSend(ProducerRecord<Integer, String> record) {
//在發送時,統計發送的個數
onSendCount++;
//此異常在其他方法中手動設置的
if (throwExceptionOnSend)
throw new KafkaException("Injected exception in AppendProducerInterceptor.onSend");
ProducerRecord<Integer, String> newRecord = new ProducerRecord<>(
record.topic(), record.partition(), record.key(), record.value().concat(appendStr));
return newRecord;
}
@Override
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
//統計ack響應的次數
onAckCount++;
if (exception != null) {
//如果存在exception,則代表error,統計error的個數
onErrorAckCount++;
// 處理metadata情況
if (metadata != null && metadata.topic().length() >= 0) {
onErrorAckWithTopicSetCount++;
if (metadata.partition() >= 0)
onErrorAckWithTopicPartitionSetCount++;
}
}
if (throwExceptionOnAck)
throw new KafkaException("Injected exception in AppendProducerInterceptor.onAcknowledgement");
}
@Override
public void close() {
}
// if 'on' is true, onSend will always throw an exception
public void injectOnSendError(boolean on) {
throwExceptionOnSend = on;
}
// if 'on' is true, onAcknowledgement will always throw an exception
public void injectOnAcknowledgementError(boolean on) {
throwExceptionOnAck = on;
}
}
總之,上面的例子就是統計發送,發送成功,發送失敗,的消息個數。
3.2 集羣元數據
在前面我們發送消息的例子中,我們只指定了topic的信息,但是我們在發送到服務器的時候,是需要知道,發送的哪個分區上面,哪臺機器,端口號是啥,leader副本是哪個等等。kafka是會把這些分區信息維護在生產者本地的,用Metadata來維護,我們詳細看看這個Metadata是如何來維護這些信息的。
在KafkaProducer中,使用Node,TopicPartition,PartitionInfo封窗了Kafka集羣相關的元數據。
- Node:表示集羣中的一個節點,Node記錄了這個節點的host,ip,port等信息
- TopicPartition:表示某Topic的一個分區,其中的topic字段是Topic的名稱,partition字段則是此分區在Topic中的分區編號(ID)
- PartitionInfo:分區的詳細信息,leader字段記錄了Leader副本所在節點的id,replica字段記錄了全部副本所在的節點信息,inSyncReplicas字段記錄了ISR集合中所有副本所在的節點信息。
通過組合這三個類,我們就可以完整的描述集羣元數據信息,這些元數據保存在cluster中,並按照不同的映射方法進行存放。如下:
//kafka集羣中的節點信息
private final List<Node> nodes;
private final Set<String> unauthorizedTopics;
//記錄了TopicPartition和PartitionInfo之間的關係
private final Map<TopicPartition, PartitionInfo> partitionsByTopicPartition;
//記錄了Topic和PartitionInfo的映射關係,可以按照Topic名稱查詢其中全部的partition信息
private final Map<String, List<PartitionInfo>> partitionsByTopic;
//topic與PartitionInfo的映射關係,List中存在的分區,必須是有Leader副本的分區
private final Map<String, List<PartitionInfo>> availablePartitionsByTopic;
//記錄了Node與PartitionInfo的映射關係,可以按照節點id查詢以上分佈的全部分區的詳細信息
private final Map<Integer, List<PartitionInfo>> partitionsByNode;
//BrokerId 與Node節點中的對應關係,方便按照BrokerId進行檢索
private final Map<Integer, Node> nodesById;
Metadata中封裝了cluster對象,並保存Cluster數據的最後更新時間,版本號,是否需要更新等信息,看下Metadata中的主要屬性
//兩次發出更新Cluster保存的元數據信息最小的時間差,默認100ms
private final long refreshBackoffMs;
//每隔多久就更新一次
private final long metadataExpireMs;
//記錄了元數據版本的版本號,Kafka集羣元數據,每更新一次就+1,
private int version;
//上一次更新元數據的時間戳,包含失敗
private long lastRefreshMs;
//上一次成功更新的時間戳
private long lastSuccessfulRefreshMs;
//記錄了topic最新的元數據
private Cluster cluster;
private boolean needUpdate;
//記錄了當前已知的所有topic
private final Set<String> topics;
//監聽metaData更新的監聽器集合,在更新cluster中的字段前,會通知listener集合中全部的Listner對象
private final List<Listener> listeners;
//是否需要更新全部的topic數據
private boolean needMetadataForAllTopics;
下面我們重點講一個更新的流程
private long waitOnMetadata(String topic, long maxWaitMs) throws InterruptedException {
//1.檢查topics集合中是否包含指定的Topic,如果沒有則添加進去
if (!this.metadata.containsTopic(topic))
this.metadata.add(topic);
//2.成功獲取分區的詳細信息
if (metadata.fetch().partitionsForTopic(topic) != null)
return 0;
long begin = time.milliseconds();
long remainingWaitMs = maxWaitMs;
//3.如果從metadata中獲得當前topic的partitions爲null,那麼就去更新metadata數據
while (metadata.fetch().partitionsForTopic(topic) == null) {
log.trace("Requesting metadata update for topic {}.", topic);
int version = metadata.requestUpdate();
//3.1喚醒sender線程
sender.wakeup();
//3.2阻塞,等待元數據更新完畢
metadata.awaitUpdate(version, remainingWaitMs);
long elapsed = time.milliseconds() - begin;
//3.3檢測超時時間
if (elapsed >= maxWaitMs)
throw new TimeoutException("Failed to update metadata after " + maxWaitMs + " ms.");
//3.4檢測權限
if (metadata.fetch().unauthorizedTopics().contains(topic))
throw new TopicAuthorizationException(topic);
remainingWaitMs = maxWaitMs - elapsed;
}
return time.milliseconds() - begin;
}
以上是啓動更新的各種條件,我們再去metadata.awaitUpdate(version, remainingWaitMs);方法中看看他是如何等待更新元數據的
public synchronized void awaitUpdate(final int lastVersion, final long maxWaitMs) throws InterruptedException {
if (maxWaitMs < 0) {
throw new IllegalArgumentException("Max time to wait for metadata updates should not be < 0 milli seconds");
}
long begin = System.currentTimeMillis();
long remainingWaitMs = maxWaitMs;
//直到當前version>lastVrsion
while (this.version <= lastVersion) {
//主線程與Sender線程通過wait/notify同步,更新元數據的操作則交給Sender線程去完成
if (remainingWaitMs != 0)
wait(remainingWaitMs);
long elapsed = System.currentTimeMillis() - begin;
//是否超時
if (elapsed >= maxWaitMs)
throw new TimeoutException("Failed to update metadata after " + maxWaitMs + " ms.");
remainingWaitMs = maxWaitMs - elapsed;
}
}
while循環中,判斷當前的version是否大於lastVersion,如果大於的話,則代表已經更新成功了。那麼具體是哪裏去更新的呢?又是哪裏獲取更新的結果的呢?我們繼續分析
前面是標記了,我們需要更新metadata,那我們又是在哪裏去發起更新請求的呢?答案在NetworkClient裏面,至於如何會調用到此方法,我們在後面消息發生部分會講到
public List<ClientResponse> poll(long timeout, long now) {
//更新Metadata
long metadataTimeout = metadataUpdater.maybeUpdate(now);
try {
//執行IO操作
this.selector.poll(Utils.min(timeout, metadataTimeout, requestTimeoutMs));
} catch (IOException e) {
log.error("Unexpected error during I/O", e);
}
......
}
maybeUpdate我們默認是在DefaultMetadataUpdater中,這個類是NetworkClient的內部類,下面這個類主要是判斷是否需要更新MetaData
public long maybeUpdate(long now) {
//調用Metadata.timeToNextUpdate()方法,其中會檢測needUpdate的值,退避時間,
// 是否長時間未更新,最終得到一個下次更新集羣元數據的時間戳
long timeToNextMetadataUpdate = metadata.timeToNextUpdate(now);
//獲取下次嘗試重新連接服務端的時間戳
long timeToNextReconnectAttempt = Math.max(this.lastNoNodeAvailableMs + metadata.refreshBackoff() - now, 0);
//檢測是否已經發送了MetadataRequest請求
long waitForMetadataFetch = this.metadataFetchInProgress ? Integer.MAX_VALUE : 0;
// 計算當前距離下次可以發送MetadataRequest請求的時間差
long metadataTimeout = Math.max(Math.max(timeToNextMetadataUpdate, timeToNextReconnectAttempt),
waitForMetadataFetch);
//允許發送MetadataRequest請求
if (metadataTimeout == 0) {
//請注意,此方法的行爲以及對poll()的超時計算都是 高度依賴leastLoadedNode的行爲。
//找到負載最小的Node,若沒有可用Node,則返回null
Node node = leastLoadedNode(now);
//創建並緩存MetadataRequest,等待下次poll()方法纔會真正發送
maybeUpdate(now, node);
}
return metadataTimeout;
}
如果需要更新的話,那麼就調用maybeUpdate,實際發送請求,調用doSend()將數據寫入到send屬性中,下次調用poll方法就會把數據發送出去了
private void maybeUpdate(long now, Node node) {
//檢測是否有node可用
if (node == null) {
log.debug("Give up sending metadata request since no node is available");
// 設置lastNoNodeAvailableMs
this.lastNoNodeAvailableMs = now;
return;
}
String nodeConnectionId = node.idString();
//檢測是否允許向此Node發送請求
if (canSendRequest(nodeConnectionId)) {
this.metadataFetchInProgress = true;
MetadataRequest metadataRequest;
//指定需要更新元數據的Topic
if (metadata.needMetadataForAllTopics())
metadataRequest = MetadataRequest.allTopics();
else
metadataRequest = new MetadataRequest(new ArrayList<>(metadata.topics()));
//將MetadataRequest封裝成ClientRequest
ClientRequest clientRequest = request(now, nodeConnectionId, metadataRequest);
log.debug("Sending metadata request {} to node {}", metadataRequest, node.id());
//緩存請求,在下次poll()操作中會將其發送出去
doSend(clientRequest, now);
} else if (connectionStates.canConnect(nodeConnectionId, now)) {
log.debug("Initialize connection to node {} for sending metadata request", node.id());
//初始化連接
initiateConnect(node, now);
} else {
//已經成功連接到指定節點,但不能發送請求,則更新lastNoNodeAvailableMs後等待
this.lastNoNodeAvailableMs = now;
}
}
上面我們講完了,如何發送這個流程,下面來看在哪裏處理這個結果的
在NetworkClient中的poll方法中,這個poll方法使用來處理sockets的讀寫事件的,所以在這裏可以拿到服務器返回的Metadata的信息。調用 handleCompletedReceives(responses, updatedNow);處理CompletedReceives隊列。我們看下這個方法
/**
* 處理所有已完成的接收,並使用接收到的響應更新響應列表。
*/
private void handleCompletedReceives(List<ClientResponse> responses, long now) {
//遍歷completedReceives
for (NetworkReceive receive : this.selector.completedReceives()) {
//非關鍵代碼不展示
...
//如果是Metadata更新請求的響應
// 那麼調用MetadataUpdater.maybeHandleCompleltedReceive()方法處理
// 重點:MetadataResponse,其中會更新Metadata中記錄的集羣元數據,並喚醒所有等待Metadata 更新完成的線程
if (!metadataUpdater.maybeHandleCompletedReceive(req, now, body))
//如果不是MetadataResponse,則創建ClientResponse並添加到responses集合
responses.add(new ClientResponse(req, now, false, body));
}
}
其他代碼先不看,我們看“重點”部分,如果返回是metadata的話,直接調用metadataUpdater.maybeHandleCompletedReceive(req, now, body)來處理。
public boolean maybeHandleCompletedReceive(ClientRequest req, long now, Struct body) {
short apiKey = req.request().header().apiKey();
//如果是的Metadata類型的化,我們就解析響應
if (apiKey == ApiKeys.METADATA.id && req.isInitiatedByNetworkClient()) {
handleResponse(req.request().header(), body, now);
return true;
}
return false;
}
看一下具體咋解析的
private void handleResponse(RequestHeader header, Struct body, long now) {
this.metadataFetchInProgress = false;
MetadataResponse response = new MetadataResponse(body);
//Cluster是不可變對象,所以我們創建新的Cluster對象,並覆蓋Metadata.cluster字段
Cluster cluster = response.cluster();
Map<String, Errors> errors = response.errors();
if (!errors.isEmpty())
log.warn("Error while fetching metadata with correlation id {} : {}", header.correlationId(), errors);
if (cluster.nodes().size() > 0) {
//在此方法中,首先通知Metadata上的監聽器,之後更新cluster字段,最後喚醒等待Metadata更新完成的線程
this.metadata.update(cluster, now);
} else {
//更新metadata失敗,只是更新lastRefreshMs字段
log.trace("Ignoring empty metadata response with correlation id {}.", header.correlationId());
this.metadata.failedUpdate(now);
}
}
簡單來說,就是先新建Cluster對象,然後調用metadata的update方法去更新
public synchronized void update(Cluster cluster, long now) {
//不再需要更新
this.needUpdate = false;
//最後更新時間爲現在
this.lastRefreshMs = now;
//上次成功更新時間爲現在
this.lastSuccessfulRefreshMs = now;
//當前版本+1
this.version += 1;
//監聽
for (Listener listener: listeners)
listener.onMetadataUpdate(cluster);
this.cluster = this.needMetadataForAllTopics ? getClusterForCurrentTopics(cluster) : cluster;
//更新完元數據後,喚醒等待此元數據的所有線程
notifyAll();
log.debug("Updated cluster metadata version {} to {}", this.version, this.cluster);
}
至此,metadata.awaitUpdate中,拿到的version版本就大於lastVersion了,整個更新流程就結束了
3.3 Serialiser&DeseriaLizer
主要就是序列化和反序列化,這個就不詳細展開了
3.4 Partitioner
在3.2中,我們已經更新了元數據信息,下一步就是從Metadata中選擇分區了,分區選擇主要分爲兩個部分
- 如果用戶指定了partition,那麼校驗用戶指定的partition,校驗合格就直接使用
- 如果用戶沒有指定,那麼就去計算partition
在KafkaProducer中
private int partition(ProducerRecord<K, V> record, byte[] serializedKey , byte[] serializedValue, Cluster cluster) {
Integer partition = record.partition();
//如果record指定了partition,那麼直接使用用戶指定的partition
if (partition != null) {
List<PartitionInfo> partitions = cluster.partitionsForTopic(record.topic());
int lastPartition = partitions.size() - 1;
//校驗partition是否真實存在
if (partition < 0 || partition > lastPartition) {
throw new IllegalArgumentException(String.format("Invalid partition given with record: %d is not in the range [0...%d].", partition, lastPartition));
}
return partition;
}
//用戶沒有指定partition,則返回的計算的分區
return this.partitioner.partition(record.topic(), record.key(), serializedKey, record.value(), serializedValue,
cluster);
}
用戶沒有指定partition,則去計算分區,在DefaultPartitioner中
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
//從Cluster中獲取對應的topic的分區信息
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
//分區的個數
int numPartitions = partitions.size();
//如果消息的key爲null
if (keyBytes == null) {
//遞增counter
int nextValue = counter.getAndIncrement();
//選擇availablePartitions
List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
if (availablePartitions.size() > 0) {
//nextValue取絕對值,並對可用的分區數取餘
int part = DefaultPartitioner.toPositive(nextValue) % availablePartitions.size();
return availablePartitions.get(part).partition();
} else {
// 沒有可用的分區,就提供一個不可用的分區,隨機提供一個partitionId
return DefaultPartitioner.toPositive(nextValue) % numPartitions;
}
} else {
// 對key用murumur2算法進行hash,選擇分區
return DefaultPartitioner.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
}
}
至此,選擇分區的過程也結束了