Kafka科普系列 | Kafka中的事務是什麼樣子的?

朱小廝發表於2019-06-01

事務,對於大家來說可能並不陌生,比如資料庫事務、分散式事務,那麼Kafka中的事務是什麼樣子的呢?

在說Kafka的事務之前,先要說一下Kafka中冪等的實現。冪等和事務是Kafka 0.11.0.0版本引入的兩個特性,以此來實現EOS(exactly once semantics,精確一次處理語義)。

冪等,簡單地說就是對介面的多次呼叫所產生的結果和呼叫一次是一致的。生產者在進行重試的時候有可能會重複寫入訊息,而使用Kafka的冪等性功能之後就可以避免這種情況。

開啟冪等性功能的方式很簡單,只需要顯式地將生產者客戶端引數enable.idempotence設定為true即可(這個引數的預設值為false)。

Kafka是如何具體實現冪等的呢?Kafka為此引入了producer id(以下簡稱PID)和序列號(sequence number)這兩個概念。每個新的生產者例項在初始化的時候都會被分配一個PID,這個PID對使用者而言是完全透明的。

對於每個PID,訊息傳送到的每一個分割槽都有對應的序列號,這些序列號從0開始單調遞增。生產者每傳送一條訊息就會將對應的序列號的值加1。

broker端會在記憶體中為每一對維護一個序列號。對於收到的每一條訊息,只有當它的序列號的值(SN_new)比broker端中維護的對應的序列號的值(SN_old)大1(即SN_new = SN_old + 1)時,broker才會接收它。

如果SN_new< SN_old + 1,那麼說明訊息被重複寫入,broker可以直接將其丟棄。如果SN_new> SN_old + 1,那麼說明中間有資料尚未寫入,出現了亂序,暗示可能有訊息丟失,這個異常是一個嚴重的異常。

引入序列號來實現冪等也只是針對每一對而言的,也就是說,Kafka的冪等只能保證單個生產者會話(session)中單分割槽的冪等。冪等性不能跨多個分割槽運作,而事務可以彌補這個缺陷。

事務可以保證對多個分割槽寫入操作的原子性。操作的原子性是指多個操作要麼全部成功,要麼全部失敗,不存在部分成功、部分失敗的可能。

為了使用事務,應用程式必須提供唯一的transactionalId,這個transactionalId通過客戶端引數transactional.id來顯式設定。事務要求生產者開啟冪等特性,因此通過將transactional.id引數設定為非空從而開啟事務特性的同時需要將enable.idempotence設定為true(如果未顯式設定,則KafkaProducer預設會將它的值設定為true),如果使用者顯式地將enable.idempotence設定為false,則會報出ConfigException的異常。

transactionalId與PID一一對應,兩者之間所不同的是transactionalId由使用者顯式設定,而PID是由Kafka內部分配的。

另外,為了保證新的生產者啟動後具有相同transactionalId的舊生產者能夠立即失效,每個生產者通過transactionalId獲取PID的同時,還會獲取一個單調遞增的producer epoch。如果使用同一個transactionalId開啟兩個生產者,那麼前一個開啟的生產者會報錯。

從生產者的角度分析,通過事務,Kafka可以保證跨生產者會話的訊息冪等傳送,以及跨生產者會話的事務恢復。

前者表示具有相同transactionalId的新生產者例項被建立且工作的時候,舊的且擁有相同transactionalId的生產者例項將不再工作。

後者指當某個生產者例項當機後,新的生產者例項可以保證任何未完成的舊事務要麼被提交(Commit),要麼被中止(Abort),如此可以使新的生產者例項從一個正常的狀態開始工作。

KafkaProducer提供了5個與事務相關的方法,詳細如下:

void initTransactions();
void beginTransaction() throws ProducerFencedException;
void sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata> offsets,
                              String consumerGroupId)
        throws ProducerFencedException;
void commitTransaction() throws ProducerFencedException;
void abortTransaction() throws ProducerFencedException;
複製程式碼

initTransactions()方法用來初始化事務;beginTransaction()方法用來開啟事務;sendOffsetsToTransaction()方法為消費者提供在事務內的位移提交的操作;commitTransaction()方法用來提交事務;abortTransaction()方法用來中止事務,類似於事務回滾。

在消費端有一個引數isolation.level,與事務有著莫大的關聯,這個引數的預設值為“read_uncommitted”,意思是說消費端應用可以看到(消費到)未提交的事務,當然對於已提交的事務也是可見的。

這個引數還可以設定為“read_committed”,表示消費端應用不可以看到尚未提交的事務內的訊息。

舉個例子,如果生產者開啟事務並向某個分割槽值傳送3條訊息msg1、msg2和msg3,在執行commitTransaction()或abortTransaction()方法前,設定為“read_committed”的消費端應用是消費不到這些訊息的,不過在KafkaConsumer內部會快取這些訊息,直到生產者執行commitTransaction()方法之後它才能將這些訊息推送給消費端應用。反之,如果生產者執行了abortTransaction()方法,那麼KafkaConsumer會將這些快取的訊息丟棄而不推送給消費端應用。

在這裡插入圖片描述

日誌檔案中除了普通的訊息,還有一種訊息專門用來標誌一個事務的結束,它就是控制訊息(ControlBatch)。控制訊息一共有兩種型別:COMMIT和ABORT,分別用來表徵事務已經成功提交或已經被成功中止。

RecordBatch中attributes欄位的第6位用來標識當前訊息是否是控制訊息。如果是控制訊息,那麼這一位會置為1,否則會置為0,如上圖所示。

attributes欄位中的第5位用來標識當前訊息是否處於事務中,如果是事務中的訊息,那麼這一位置為1,否則置為0。由於控制訊息也處於事務中,所以attributes欄位的第5位和第6位都被置為1。

在這裡插入圖片描述
KafkaConsumer可以通過這個控制訊息來判斷對應的事務是被提交了還是被中止了,然後結合引數isolation.level配置的隔離級別來決定是否將相應的訊息返回給消費端應用,如上圖所示。注意ControlBatch對消費端應用不可見。

我們在上一篇Kafka科普系列中還講過LSO——《Kafka科普系列 | 什麼是LSO》,它與Kafka的事務有著密切的聯絡,看著下圖,你回憶起來了嘛。

在這裡插入圖片描述


歡迎支援筆者小冊:《圖解Kafka之實戰指南》和《圖解Kafka之核心原理

Kafka科普系列 | Kafka中的事務是什麼樣子的?


歡迎支援筆者新作:《深入理解Kafka:核心設計與實踐原理》和《RabbitMQ實戰指南》,同時歡迎關注筆者的微信公眾號:朱小廝的部落格。

Kafka科普系列 | Kafka中的事務是什麼樣子的?

相關文章