Kafka技術知識總結之二——Kafka事務

接上篇《Kafka技術知識總結之一——Kafka 的元素,組成,架構》

二. Kafka 事務

參考地址:
《【乾貨】Kafka 事務特性分析》

2.1 Kafka 事務簡述

Kafka 事務與數據庫的事務定義基本類似,主要是一個原子性:多個操作要麼全部成功,要麼全部失敗。Kafka 中的事務可以使應用程序將消費消息、生產消息、提交消費位移當作原子操作來處理。
爲了實現事務,Producer 應用程序必須做到:

  1. 提供唯一的 transactionalId
    • properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, “transacetionId”);
    • Kafka 可以通過相同的 transactionalId 確定唯一的生產者。對於相同 transactionalId 的新生 Producer 實例被創建且工作時,舊的 Producer 實例將不再工作。即消息跨生產者的的冪等性
  2. 要求 Producer 開啓冪等特性
    • 將 enable.idempotence 設置爲 true;

注:

  1. transactionalId 與 PID 一一對應,爲了保證新的 Producer 啓動之後,具有相同的 transactionalId 的舊生產者立即失效,每個 Producer 通過 transactionalId 獲取 PID 的時候,還會獲取一個單調遞增的 producer epoch
  2. Kafka 的事務主要是針對 Producer 而言的。對於 Consumer,考慮到日誌壓縮(相同 Key 的日誌被新消息覆蓋)、可追溯的 seek() 等原因,Consumer 關於事務語義較弱。
  3. 對於 Kafka Consumer,在實現事務配置時,一定要關閉自動提交的選項,即 props.put(“ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false”);

2.2 消費-轉換-生產模式

消費-轉換-生產模式是一種常見的,又比較複雜的情況,由於同時存在消費與生產,所以整個過程通常需要事務化。
通常在實現該模式時,需要同時構建一個用於拉取原消息的 Consumer,一個將原消息處理後,將處理後消息投遞出去的 Producer。在代碼上主要有五個步驟:

// 初始化事務
producer.initTransactions();
// 開啓事務
producer.beginTransaction();
// 消費 - 生產模型
producer.send(producerRecord);
// 提交消費位移
producer.sendOffsetsToTransaction(offsets, "groupId");
// 提交事務
producer.commitTransaction();

上述過程全部被 try… catch…,如果中間出現錯誤,需要在 catch 塊中執行:

// 中止事務
producer.abortTransaction();

2.3 Kafka 事務的實現

實現 Kafka 事務,主要使用到 Broker 端的事務協調器 (TransactionCoordinator)。每個 Producer 都會被指定一個特定的 TransactionalCoordinator,用來負責處理其事務,與消費者 Rebalance 時的 GroupCoordinator 作用類似。實現事務的流程如下圖所示:
消費-轉換-生產流程

基本步驟如下:

2.3.1 查找事務協調者

生產者首先會發起一個查找事務協調者 (TransactionalCoordinator) 的請求 (FindCoordinatorRequest),Broker 集羣根據 Request 中包含的 transactionalId 查找對應的 TransactionalCoordinator 節點並返回給 Producer。

2.3.2 獲取 Producer ID

生產者獲得協調者信息後,向剛剛找到的 TransactionalCoordinator 發送 InitProducerIdRequest 請求,爲當前 Producer 分配一個 Producer ID。分兩種情況:

  • 不包含 transactionId:直接生成一個新的 Producer ID,返回給生產者客戶端;
  • 包含 transactionId:根據 transactionId 獲取 PID,這個對應關係保存在事務日誌中(上圖中的 2a 步驟);

注:如果 TransactionalCoordinator 第一次收到包含該 transactionalId 的消息,則將相關消息存入主題 __transaction_state 中。

2.3.3 開啓事務

生產者通過方法 producer.beginTransaction() 啓動事務,此時只是生產者內部狀態記錄爲事務開始。對於事務協調者,直到生產者發送第一條消息,才認爲已經發起了事務。

2.3.4 消費-轉換-生產

前面的階段都是開始階段,該階段包含了整個事務的處理過程,消費者和生產者互相配合,共同完成事務。需要做如下工作:

  1. 存儲對應關係,通過請求增加分區
    • Producer 在向新分區發送數據之前,首先向 TransactionalCoordinator 發送請求,使 TransactionalCoordinator 存儲對應關係 (transactionalId, TopicPartition) 到主題 __transaction_state 中。
  2. 生產者發送消息
    • 基本與普通的發送消息相同,生產者調用 producer.send() 方法,發送數據到分區;
    • 發送的請求中,包含 pid, epoch, sequence number 字段;
  3. 增加消費 offset 到事務
    • 生產者通過 producer.senOffsetsToTransaction() 接口,發送分區的 Offset 信息到事務協調者,協調者將分區信息增加到事務中;
  4. 事務提交位移
    • 在前面生產者調用事務提交 offset 接口後,會發送一個 TxnOffsetCommitRequest 請求到消費組協調者,消費組協調者會把 offset 存儲到 Kafka 內部主題 __consumer_offsets 中。協調者會根據請求的 pid 與 epoch 驗證生產者是否允許發起這個請求。
    • epoch:生產者用於標識同一個事務 ID 在一次事務中的輪數,每次初始化事務的時候,都會遞增,從而讓服務端知道生產者請求是否爲舊的請求。
    • 只有當事務提交之後,offset 纔會對外可見。
  5. 提交或回滾事務
    • 用戶調用 producer.commitTransaction()abortTransaction() 方法,提交或回滾事務;
    • EndTxnRequest:生產者完成事務之後,客戶端需要顯式調用結束事務,或者回滾事務。前者使消息對消費者可見,後者使消息標記爲 abort 狀態,對消費者不可見。無論提交或者回滾,都會發送一個 EndTxnRequest 請求到事務協調者,同時寫入 PREPARE_COMMIT 或者 PREPARE_ABORT 信息到事務記錄日誌中。
    • WriteTxnMarkerRequest:事務協調者收到 EndTxnRequest 之後,其中包含消息是否對消費者可見的信息,然後就需要向事務中各分區的 Leader 發送消息,告知消費者當前消息時哪個事務,該消息應該接受還是丟棄。每個 Broker 收到請求之後,會把該消息應該 commit 或 abort 的信息寫到數據日誌中。

2.4 消息事務

參考地址:《利用事務消息實現分佈式事務》

很多場景下,我們發消息的過程,目的往往是通知另外一個系統或者模塊去更新數據。消息隊列中的事務,主要**解決消息生產者和消息消費者的數據一致性問題**。

舉一個例子:用戶在電商 APP 上購物時,先把商品加到購物車裏,然後幾件商品一起下單,最後支付,完成購物流程。
這個過程中有一個需要用到消息隊列的步驟:訂單系統創建訂單後,發消息給購物車系統,將已下單的商品從購物車中刪除。因爲從購物車刪除已下單商品這個步驟,並不是用戶下單支付這個主要流程中必要的步驟,使用消息隊列來異步清理購物車是更加合理。

訂單系統結構
對於訂單系統,它創建訂單的過程實際執行了 2 個步驟的操作:

  1. 在訂單庫中插入一條訂單數據,創建訂單;
  2. 發消息給消息隊列,消息的內容就是剛剛創建的訂單;

對於購物車系統:訂閱相應的主題,接收訂單創建的消息,然後清理購物車,在購物車中刪除訂單的商品。

在分佈式系統中,上面提到的步驟,任何一個都有可能失敗,如果不做任何處理,那就有可能出現訂單數據與購物車數據不一致的情況,比如:

  • 創建了訂單,沒有清理購物車;
  • 訂單沒創建成功,購物車裏面的商品卻被清掉了。

所以我們需要解決的問題爲:在上述任意步驟都有可能失敗的情況下,還要保證訂單庫和購物車庫這兩個庫的數據一致性。所以在這種跨庫的事務操作中,需要使用到分佈式事務。分佈式事務見數據庫篇,在多種適用於不同場景下的分佈式事務方法中,其中一種方式是消息事務

事務消息需要消息隊列提供相應的功能才能實現,kafka 和 RocketMQ 都提供了事務相關功能。依舊以上面的訂單系統爲例,有兩個操作:在本地數據庫中插入訂單數據,以及向消息隊列中發送訂單信息,訂單系統如何才能保證這兩個操作同時成功,同時失敗呢?

  1. 開啓消息隊列的生產者事務;
    • Kafka 的 producer.beginTransaction();
  2. 向消息隊列發送半消息
    • 半消息,即向發送一個完整的消息給消息隊列,但消費者不可見;也就是說,生產者不將消息提交出去,而是等待某些狀態確認後才執行提交 commit 操作;
    • Kafka 的 producer.send(); 方法;
  3. 開啓本地數據庫事務,執行插入操作;
  4. 插入操作的結果,決定是否把消息提交;
    • 如果本地數據庫事務執行成功,則提交 (commit) 事務;
    • 如果事務執行失敗,則回滾 (abort) 事務;
  5. 如果發送提交 / 回滾消息事務的請求出現異常(如超時等),不同的消息隊列有不同的解決方式;
    • Kafka:提交時錯誤會拋出異常,此時由業務自行決定如何處理。可以嘗試重複執行提交,直到重試成功;或者也可以進行一個補償操作,將已經存入數據庫中的訂單刪除;
    • RocketMQ:提供事務反查機制;RocketMQ 的 Broker 沒有收到提交或回滾請求,Broker 會定期去 Producer 上反查該事務的本地數據庫事務狀態,根據反查結果決定提交/回滾該事務。同時也需要業務代碼自行實現本地事務狀態的反查接口。
      訂單系統 - 實現流程

注:此外,該流程也可以用於支付流水的業務場景:保證存入一條支付流水,以及發送支付流水消息,兩者之間的原子性。(來自於筆者阿里二面面試題)

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