1 Kafka高階API特性
1.1 Offset的自動控制
1.1.1 消費者offset初始策略
一般來說每個消費者消費之後,都會把自己消費到分割槽的位置(也就是offset提交給Kafka叢集),但是對於沒有消費過該分割槽的消費者,他之前並未提交給叢集自身偏移量的資訊。
Kafka消費者預設對於未訂閱的topic的offset的時候,也就是系統並沒有儲存該消費者的消費分割槽的記錄資訊(offset),預設Kafka消費者的預設首次消費策略:latest。
配置項為:auto.offset.reset=latest
可以在官方文件,找到對於各個配置項的解釋,例如 http://kafka.apache.org/20/documentation.html#brokerconfigs 可以找到auto.offset.reset配置項。
-
earliest - 自動將偏移量重置為最早的偏移量
-
latest - 自動將偏移量重置為最新的偏移量
-
none - 如果未找到消費者組的先前偏移量,則向消費者丟擲異常
消費者的配置中增加凸顯預設配置,latest可以換成earliest:
// 預設配置,如果系統中沒有該消費組的偏移量,該消費者組讀取最新的偏移量
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"latest");
// 配置earliest,如果叢集沒有該消費者組的偏移量,系統會讀取該分割槽最早的偏移量開始消費
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest");
1.1.2 消費者offset自動提交策略
Kafka消費者在消費資料的時候預設會定期的提交消費的偏移量,這樣就可以保證所有的訊息至少可以被消費者消費1次,使用者可以通過以下兩個引數配置:
enable.auto.commit = true 預設
auto.commit.interval.ms = 5000 預設
如果使用者需要自己管理offset的自動提交,可以關閉offset的自動提交,手動管理offset提交的偏移量,注意使用者提交的offset偏移量永遠都要比本次消費的偏移量+1,因為提交的offset是kafka消費者下一次抓取資料的位置。
// 消費者自動提交開啟
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,true);
// 配置offset自動提交時間間隔,10秒自動提交offset
props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG,10000);
自定義偏移量提交策略,先關閉偏移量自定提交配置後,每次消費完,提交偏移量資訊給叢集:
public class KafkaConsumerDemo_02 {
public static void main(String[] args) {
//1.建立Kafka連結引數
Properties props=new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"CentOSA:9092,CentOSB:9092,CentOSC:9092");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
props.put(ConsumerConfig.GROUP_ID_CONFIG,"group01");
// 關閉offset自動提交
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false);
//2.建立Topic消費者
KafkaConsumer<String,String> consumer=new KafkaConsumer<String, String>(props);
//3.訂閱topic開頭的訊息佇列
consumer.subscribe(Pattern.compile("^topic.*$"));
while (true){
ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
Iterator<ConsumerRecord<String, String>> recordIterator = consumerRecords.iterator();
while (recordIterator.hasNext()){
ConsumerRecord<String, String> record = recordIterator.next();
String key = record.key();
String value = record.value();
long offset = record.offset();
int partition = record.partition();
// offset維護的Map
Map<TopicPartition, OffsetAndMetadata> offsets=new HashMap<TopicPartition, OffsetAndMetadata>();
// 自己維護offset,每次提交當前資訊的offset加1
offsets.put(new TopicPartition(record.topic(),partition),new OffsetAndMetadata(offset + 1));
// 非同步提交偏移量給叢集,且回撥列印
consumer.commitAsync(offsets, new OffsetCommitCallback() {
@Override
public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {
System.out.println("完成:"+offset+"提交!");
}
});
System.out.println("key:"+key+",value:"+value+",partition:"+partition+",offset:"+offset);
}
}
}
}
1.2 Acks & Retries(應答和重試)
Kafka生產者在傳送完一個的訊息之後,要求Leader所在的Broker在規定的時間Ack應答,如果沒有在規定時間內應答,Kafka生產者會嘗試n次重新傳送訊息(超時重傳)。目的是確保我們的訊息,一定要傳送的佇列中去
acks=1 預設
面試常問,kafka為什麼存在資料的寫入丟失?其中一種情況為下面的第一點
1、acks=1表示:Leader會將Record寫到其本地日誌中,但會在不等待所有Follower的完全確認的情況下做出響應。在這種情況下,如果Leader在確認記錄後立即失敗,但在Follower複製記錄之前失敗,則記錄將丟失,常用在不重要的日誌收集時。
2、acks=0表示:生產者根本不會等待伺服器的任何確認。該記錄將立即新增到網路套接字緩衝區中並視為已傳送。在這種情況下,不能保證伺服器已收到記錄。這種情況是不可靠的,但是效能高
3、acks=all表示:這意味著Leader將等待全套同步副本確認記錄。這保證了只要至少一個同步副本仍處於活動狀態,記錄就不會丟失。這是最有力的保證。這等效於acks = -1設定。用在一些比較重要的系統,不允許丟資料。
如果生產者在規定的時間內,並沒有得到Kafka的Leader的Ack應答,Kafka可以開啟reties機制。
request.timeout.ms = 30000 預設(30s沒有收到leader的ack則重試)
retries = 2147483647 預設(重試次數為Max_Value,預設一直重試)
public class KafkaProducerDemo_01{
public static void main(String[] args) {
//1.建立連結引數
Properties props=new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"CentOSA:9092,CentOSB:9092,CentOSC:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());
props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,UserDefineProducerInterceptor.class.getName());
props.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG,1);
props.put(ProducerConfig.ACKS_CONFIG,"-1");
props.put(ProducerConfig.RETRIES_CONFIG,10);
//2.建立生產者
KafkaProducer<String,String> producer=new KafkaProducer<String, String>(props);
//3.封賬訊息佇列
for(Integer i=0;i< 1;i++){
ProducerRecord<String, String> record = new ProducerRecord<>("topic01", "key" + i, "value" + i);
producer.send(record);
}
producer.close();
}
}
可以通過生產者自定義配置重複傳送的次數:
// 不包括第一次傳送,如果嘗試傳送三次,失敗,則系統放棄傳送
props.put(ProducerConfig.RETRIES_CONFIG, 3);
public class KafkaProducerDemo_02 {
public static void main(String[] args) {
//1.建立連結引數
Properties props=new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"CentOSA:9092,CentOSB:9092,CentOSC:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());
props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,UserDefineProducerInterceptor.class.getName());
props.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG,1);
props.put(ProducerConfig.ACKS_CONFIG,"-1");
props.put(ProducerConfig.RETRIES_CONFIG,3);
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG,true);
//2.建立生產者
KafkaProducer<String,String> producer=new KafkaProducer<String, String>(props);
//3.封賬訊息佇列
for(Integer i=0;i< 1;i++){
ProducerRecord<String, String> record = new ProducerRecord<>("topic01", "key" + i, "value" + i);
producer.send(record);
}
producer.close();
}
}
總結:應答和重試機制,可以盡最大可能保證我們把資料傳送到Kafka叢集。但也會伴隨著一些問題,比如重複資料的產生。在一些訂單業務場景中,比如使用者下訂單的記錄,是絕對不能出現重複資料的。怎麼保證?Kafka提供了冪等和事務機制,來解決重複資料的問題
1.3 Kafka冪等寫機制
1.3.1 Kafka冪等概念
1、HTTP/1.1中對冪等性的定義是:一次和多次請求某一個資源對於資源本身應該具有同樣的結果(網路超時等問題除外)。也就是說,其任意多次執行對資源本身所產生的影響均與一次執行的影響相同。
2、Kafka在0.11.0.0版本支援增加了對冪等的支援。冪等是針對生產者角度的特性。冪等可以保證生產者傳送的訊息,不會丟失(底層retries重試機制支撐),而且不會重複(冪等去重機制保證)。實現冪等的關鍵點就是服務端可以區分請求是否重複,過濾掉重複的請求。要區分請求是否重複的有兩點:
- 唯一標識:要想區分請求是否重複,請求中就得有唯一標識。例如支付請求中,訂單號就是唯一標識
- 記錄下已處理過的請求標識:光有唯一標識還不夠,還需要記錄下那些請求是已經處理過的,這樣當收到新的請求時,用新請求中的標識和處理記錄進行比較,如果處理記錄中有相同的標識,說明是重複記錄,拒絕掉。
1.3.2 Kafka冪等實現策略
1、冪等又稱為exactly once(精準一次)。要停止多次處理訊息,必須僅將其持久化到Kafka Topic中僅僅一次。在初始化期間,kafka會給生產者生成一個唯一的ID稱為Producer ID或PID。
2、PID和序列號與訊息捆綁在一起,然後傳送給Broker。由於序列號從零開始並且單調遞增,因此,僅當訊息的序列號比該PID / TopicPartition對中最後提交的訊息正好大1時,Broker才會接受該訊息。如果不是這種情況,則Broker認定是生產者重新傳送該訊息。
3、對應配置項:enable.idempotence= false 預設關閉,開啟設定為true
4、注意:在使用冪等性的時候,要求必須開啟retries=true和acks=all(保證不丟)
5、max.in.flight.requests.per.connection配置項預設是5,如果我們要保證嚴格有序,我們可以設定為1。該配置項表達的意思為:在發生阻塞之前,客戶端的一個連線上允許出現未確認請求的最大數量。
Tips: 精準一次的概念嚐嚐出現在流式處理中
程式碼配置實現:
public class KafkaProducerDemo_02 {
public static void main(String[] args) {
//1.建立連結引數
Properties props=new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"CentOSA:9092,CentOSB:9092,CentOSC:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());
props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,UserDefineProducerInterceptor.class.getName());
// 將檢測超時時間設定為1ms,方便觸發看到重試機制
props.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG,1);
// ACKS要設定為all
props.put(ProducerConfig.ACKS_CONFIG,"all");
// 重試3次
props.put(ProducerConfig.RETRIES_CONFIG,3);
// 開啟冪等
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG,true);
// 開啟冪等,客戶端的一個連線上允許出現未確認請求的最大數量要大於1小於5。設定為1可以保證順序。
// 如果有一個傳送不成功,就阻塞,一直等待傳送成功為止
props.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION,1);
//2.建立生產者
KafkaProducer<String,String> producer=new KafkaProducer<String, String>(props);
ProducerRecord<String, String> record = new ProducerRecord<>("topic01", "idempotence", "test idempotence");
producer.send(record);
producer.close();
}
}
總結:開啟冪等會保證不會重複傳送訊息到訊息佇列,客戶端的一個連線上允許出現未確認請求的最大數量設定為1的話,可以保證順序不會亂。要需要使用冪等功能,kafka的版本需要保證在0.11.0.0版本以上
以上的保證順序,保證唯一,只是針對一個分割槽而言,如果kafka有多個分割槽,那麼就需要用Kafka的事務來控制原子性,事務能控制不重複,但無法控制多分割槽全域性有序
1.4 Kafka的事務控制
1、Kafka的冪等性,只能保證一條記錄的在分割槽傳送的原子性,但是如果要保證多條記錄(多分割槽不重複,但多分割槽無法有序,參照第一章)之間的完整性,這個時候就需要開啟kafk的事務操作。事務一般是把消費者和生產者繫結,中間業務系統對下游Kafka的生產失敗了,中間業務系統消費過上游的Kafka偏移量不提交
2、在Kafka0.11.0.0除了引入的冪等性的概念,同時也引入了事務的概念。通常Kafka的事務分為 生產者事務Only、消費者&生產者事務。一般來說預設消費者消費的訊息的級別是read_uncommited資料,這有可能讀取到事務失敗的資料,所有在開啟生產者事務之後,需要使用者設定消費者的事務隔離級別。
3、預設配置項為:isolation.level = read_uncommitted
4、該選項有兩個值read_committed|read_uncommitted,如果開始事務控制,消費端必須將事務的隔離級別設定為read_committed,能夠保證在回滾後清除kafka中儲存的該條傳送資訊
5、開啟的生產者事務的時候,只需要指定transactional.id屬性即可,一旦開啟了事務,預設生產者就已經開啟了冪等性。但是要求"transactional.id"的取值必須是唯一的,同一時刻只能有一個"transactional.id"儲存在,其他的將會被關閉。
1.4.1 生產者事務only使用場景
1、生產者
public class KafkaProducerDemo02 {
public static void main(String[] args) {
//1.生產者&消費者的配置項
KafkaProducer<String,String> producer=buildKafkaProducer();
producer.initTransactions();//1、初始化事務
try{
while(true){
//2、開啟事務控制
producer.beginTransaction();
for(i=0; i<10; i++) {
if(i == 8) {
// 異常
int j = 10/0;
}
//建立Record
ProducerRecord<String,String> producerRecord=
new ProducerRecord<String,String>("topic01","transation","error......");
producer.send(producerRecord);
// 事務終止前,把之前資料刷入kafka佇列
ptoducer.flush();
}
//3、提交事務
producer.sendOffsetsToTransaction(offsets,"group01");
producer.commitTransaction();
}
}catch (Exception e){
producer.abortTransaction();//4、終止事務
}finally {
producer.close();
}
}
// 生產者在生產環境的一些常規配置
public static KafkaProducer<String,String> buildKafkaProducer(){
Properties props=new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"CentOSA:9092,CentOSB:9092,CentOSC:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());
// 必須配置事務id,且唯一
props.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG,"transaction-id" + UUID.randomUUID().toString());
// 配置批處理大小,達到1024位元組需要提交
props.put(ProducerConfig.BATCH_SIZE_CONFIG,1024);
// 當沒達到1024位元組,但是時間達到了5ms,也需要提交給叢集的topic
props.put(ProducerConfig.LINGER_MS_CONFIG,5);
// 配置冪等,和重試
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG,true);
// ack
props.put(ProducerConfig.ACKS_CONFIG,"all");
// 請求超時重發時間20ms
props.put(ProducerConfig.REQUERT_TIMEOUT_MS_CONFIG,20000);
return new KafkaProducer<String, String>(props);
}
public static KafkaConsumer<String,String> buildKafkaConsumer(String group){
Properties props=new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"CentOSA:9092,CentOSB:9092,CentOSC:9092");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
props.put(ConsumerConfig.GROUP_ID_CONFIG,group);
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false);
props.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG,"read_committed");
return new KafkaConsumer<String, String>(props);
}
}
2、消費者
public class KafkaConsumerDemo {
public static void main(String[] args) {
//1.建立Kafka連結引數
Properties props=new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"CentOSA:9092,CentOSB:9092,CentOSC:9092");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
// 消費者所屬的消費組
props.put(ConsumerConfig.GROUP_ID_CONFIG,"group01");
// 設定消費者消費事務的隔離級別read_committed,消費者不可能讀到未提交的資料
props.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG,"read_committed");
//2.建立Topic消費者
KafkaConsumer<String,String> consumer=new KafkaConsumer<String, String>(props);
//3.訂閱topic開頭的訊息佇列
consumer.subscribe(Pattern.compile("topic01"));
while (true){
ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
Iterator<ConsumerRecord<String, String>> recordIterator = consumerRecords.iterator();
while (recordIterator.hasNext()){
ConsumerRecord<String, String> record = recordIterator.next();
String key = record.key();
String value = record.value();
long offset = record.offset();
int partition = record.partition();
System.out.println("key:"+key+",value:"+value+",partition:"+partition+",offset:"+offset);
}
}
}
}
1.4.1 生產者消費者事務
public class KafkaProducerDemo02 {
public static void main(String[] args) {
//1.生產者&消費者
KafkaProducer<String,String> producer=buildKafkaProducer();
KafkaConsumer<String, String> consumer = buildKafkaConsumer("group01");
// 消費者先訂閱消費topic資料
consumer.subscribe(Arrays.asList("topic01"));
producer.initTransactions();//初始化事務
try{
while(true){
// 消費者1秒鐘拉取一次資料
ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
// 獲取的訊息迭代
Iterator<ConsumerRecord<String, String>> consumerRecordIterator = consumerRecords.iterator();
//開啟事務控制
producer.beginTransaction();
// 維護偏移量
Map<TopicPartition, OffsetAndMetadata> offsets=new HashMap<TopicPartition, OffsetAndMetadata>();
// 消費者讀取到訊息後進行業務處理
while (consumerRecordIterator.hasNext()){
ConsumerRecord<String, String> record = consumerRecordIterator.next();
//業務處理,這裡建立Record,通過消費topic01的訊息記錄,傳送到topic02
ProducerRecord<String,String> producerRecord=
new ProducerRecord<String,String>("topic02",record.key(),record.value()+"to topic02");
producer.send(producerRecord);
//記錄後設資料下次需要提交的便宜量
offsets.put(new TopicPartition(record.topic(),record.partition()),
new OffsetAndMetadata(record.offset()+1));
}
//提交事務,先提交消費者的偏移量,需要指定消費者組
producer.sendOffsetsToTransaction(offsets,"group01");
// 提交事務,再提交生產者的偏移量
producer.commitTransaction();
}
}catch (Exception e){
// 消費者端業務處理邏輯出現錯誤,要捕獲回滾
producer.abortTransaction();//終止事務
}finally {
producer.close();
}
}
public static KafkaProducer<String,String> buildKafkaProducer(){
Properties props=new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"CentOSA:9092,CentOSB:9092,CentOSC:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());
// 必須配置事務id,且唯一
props.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG,"transaction-id" + UUID.randomUUID().toString());
// 配置批處理大小,達到1024位元組需要提交
props.put(ProducerConfig.BATCH_SIZE_CONFIG,1024);
// 當沒達到1024位元組,但是時間達到了5ms,也需要提交給叢集的topic
props.put(ProducerConfig.LINGER_MS_CONFIG,5);
// 配置冪等,和重試
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG,true);
// ack
props.put(ProducerConfig.ACKS_CONFIG,"all");
// 請求超時重發時間20ms
props.put(ProducerConfig.REQUERT_TIMEOUT_MS_CONFIG,20000);
return new KafkaProducer<String, String>(props);
}
public static KafkaConsumer<String,String> buildKafkaConsumer(String group){
Properties props=new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"CentOSA:9092,CentOSB:9092,CentOSC:9092");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
props.put(ConsumerConfig.GROUP_ID_CONFIG,group);
// 消費者自動提交offset策略,關閉,必須設定
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false);
// 設定消費者消費事務的隔離級別read_committed,消費者不可能讀到未提交的資料
props.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG,"read_committed");
return new KafkaConsumer<String, String>(props);
}
}
總結:消費端掌握偏移量控制,生產者端掌握超時重傳應答重試和分割槽冪等。實際的生產開發過程中,要熟練掌握事務控制,包括生產者only和生產者&消費者事務控制。kafka事務在分散式微服務的開發中,有比較強的應用。