資料更改事件的三種型別

banq發表於2024-03-17


資料變更事件是Debezium等變更資料捕獲 (CDC) 解決方案的核心。它們描述對資料庫中特定記錄所做的更改,並允許事件使用者根據此資訊採取行動,從而實現廣泛的用例,例如實時 ETL(透過將更新的資料傳播到下游資料儲存,例如資料倉儲、分析資料庫或全文搜尋索引)、微服務資料交換或審計日誌記錄。

變更事件中到底包含什麼?存在哪些型別的變更事件,什麼時候應該使用哪個?這些是我想在這篇文章中透過開發資料更改事件的分類法來回答的一些問題,討論三種型別的事件:

  • 完整事件,包含更改記錄的完整狀態,
  • 增量delta事件,其中包含記錄的變異欄位,以及
  • 僅 ID 事件,僅包含已更改記錄的 id(主鍵)。

完整事件
讓我們從大多數 CDC 使用者可能都熟悉的事件型別開始:完整或完整的資料變更事件。每當源資料儲存中的記錄發生變化時,這種變化事件就會包含該記錄的完整狀態。舉例來說,表 customers 包含 id、first_name 和 last_name 列以及一個陣列型別的 emails 列。如果客戶記錄的 first_name 值被更新,而其他欄位沒有變化,那麼相應的變更事件使用 JSON 符號可以如下所示:

{
  <font>"id" : 42,
 
"first_name" : "Barry",
 
"last_name" : "Wilson",
 
"emails" : ["barry@example.com", "bwilson@example.com"]
}

更改事件是完全獨立的。它描述了記錄被修改時的完整狀態,特別是記錄修改後的新狀態。許多 CDC 解決方案都會在其更改事件中公開被修改記錄的新舊狀態(有時也稱為新舊 "行映像"),例如 Debezium 就會將其命名為之前和之後:

{
  <font>"before": {
   
"id" : 42,
   
"first_name" : "Billy",
   
"last_name" : "Wilson",
   
"emails" : ["barry@example.com", "bwilson@example.com"]
  },
 
"after": {
   
"id" : 42,
   
"first_name" : "Barry",
   
"last_name" : "Wilson",
   
"emails" : ["barry@example.com", "bwilson@example.com"]
  },
}

事件中包含哪些部分取決於資料變化的型別:

  • 對於表示插入記錄的事件,只顯示新行的影像、
  • 對於更新事件,新舊記錄都會出現。
  • 對於刪除事件,只有後資料塊中的舊行影像才會出現。

在插入和更新事件中,舊行映像是否實際存在也取決於源資料庫的配置。通常情況下,必須明確啟用保留舊行映像的功能,因為這樣做會增加資料庫系統的磁碟空間消耗。例如,要在 Postgres 的變更事件中發出舊行版本,必須將表的副本標識設定為 "FULL"。

資料變更事件和 Apache Kafka:透過 Apache Kafka 等分割槽系統傳輸資料變更事件時,還需要為訊息定義金鑰。它定義了一條記錄將被髮送到變更事件主題的哪個分割槽,以確保具有相同金鑰的所有記錄的正確排序。對於資料變更事件,鍵應來自源資料儲存中代表記錄的主鍵。這樣,一條記錄的所有變更事件都將進入相應變更事件主題的同一個分割槽,消費者將按照它們在源資料庫中發生的順序接收它們。

Delta增量事件
接下來讓我們看看 delta 事件或部分更改事件。它們並不包含所代表記錄的完整狀態,而只包含值實際發生變化的列或欄位以及記錄的 id。換句話說,它們準確描述了與記錄的上一版本相比發生了哪些變化(但僅此而已)。對於代表插入操作的事件來說,這些是記錄的所有屬性,而對於更新操作來說,這些只是發生變化的屬性。對於刪除操作,只有 id 屬性。

部分更改事件有兩種不同的設計方式。第一種是在變更事件中發出任何已修改的屬性。讓我們再看看上一節的例子:客戶 42 的名字被修改,而姓氏和電子郵件地址保持不變。再次使用 JSON 符號,並只關注新的行圖片,相應的更改事件可以如下所示:

{
  <font>"id" : 42,
 
"first_name" : "Barry"
}

根據所選擇的序列化格式,在處理空值時會有一些微妙之處。特別是,它必須允許您區分被設定為空值的(可選)屬性和完全未發生變化的屬性。在 JSON 中,您可以透過為欄位傳送空值與從事件有效負載中省略空值來區分這兩種情況。

部分資料更改事件的第二個選項是描述哪些操作具體應用於哪些屬性。這在處理陣列值屬性時尤其有用。在更新的情況下,當變更事件格式包含完整的新值時,一個微小的變更就可能導致寫入放大,例如,在一個有 20 個條目的陣列中新增或刪除一個元素。在這種情況下,JSON Patch 等格式非常有用,因為它們可以更精細地描述變化:

{
  <font>"id" : 42,
 
"patch" : [
    {
"op": "replace", "path": "/first_name", "value": "Barry" }
         {
"op": "add", "path": "/emails/-", "value": { "berry@example.com" } }
    ]
}

與完整事件不同, delta增量資料更改事件並非完全獨立。當接收到部分更新事件時,事件消費者必須能夠訪問該記錄之前的狀態,才能應用該補丁事件。例如,如果消費系統是一個 SQL 資料庫,則可以釋出 UPDATE 語句來更新受影響的列。

但是,如果匯資料系統不支援部分更新,而總是要求在更新發生時攝取完整記錄,該怎麼辦呢?在這種情況下,有狀態流處理(例如使用 Apache Flink)可能是一個有用的選擇。您可以將這種流處理器放在事件源和匯之間,它將對完整事件進行 "再水化",這意味著將所有傳入的部分事件一個接一個地應用起來。為此,它會利用內部狀態儲存(如 Flink 的 RocksDB)。在處理記錄的插入更改事件時,該事件將被放入狀態儲存中,然後再向下游傳送。

之後,在處理更新事件時,流處理器可以從狀態儲存中獲取傳入部分事件中缺失的任何屬性值,從而只向下遊事件消費者公開完整事件。雖然類似的先讀後寫方法也可以在匯資料儲存中實施,但在流處理管道中實施這種方法可以一次性構建再水化邏輯,然後讓多個匯從中受益。

在 CDC 系統大部分時間都會發出完整的資料變更事件,但在某些情況下可能會發出部分事件的情況下,這種技術也能派上用場。Debezium 的 Postgres 聯結器就是一個例子,如果 TOAST 列的值未發生變化,該聯結器就不會發出這些列的值。如上所述的有狀態流處理可以幫助消費者避免這種行為,並始終向事件消費者暴露完整的事件。

純 ID 事件
資料更改事件的最後一種也是最基本的一種形式是純標識事件。它們僅描述源資料庫中受更改影響的記錄。為此,事件必須包含記錄的 id(例如,RDBMS 中記錄的主鍵值):

{
    <font>"id" : 42
}

除嚴格意義上的資料庫和 CDC 外,Id-only 事件還可用於其他場合。Amazon S3 事件通知就是一個例子,您可以用它來訂閱 S3 儲存桶中發生的變化,如檔案的新增或刪除。由於在相應的更改事件中公開整個檔案狀態不切實際,因此這裡使用了僅 ID 事件樣式。

就其本質而言,這種只有 id 的事件不會告訴你所代表的記錄到底發生了什麼變化。因此,這種事件型別只能在相當小的應用範圍內使用。例如,可以用它來使快取中的專案失效,但不能用它本身來更新快取。使用僅 ID 事件的系統包括 Microsoft SQL Server 的更改跟蹤功能、CockroachDB 的 "key only "模式和 DynamoDB 的 KEYS_ONLY 流檢視型別。

如果想獲取整條記錄,除了從源儲存中重新選擇外,別無選擇。這可以由變更事件消費者自己完成,也可以由流處理器完成,然後由流處理器向下遊消費者發出完整的變更事件。這樣做時有幾件事需要考慮。

最重要的是,CDC 工具以非同步方式發出變更事件,這意味著當你執行查詢以獲取完整的行狀態時,該行可能已經再次發生了變更。查詢將返回行的當前狀態,而不是最初觸發變更事件時的有效狀態。如果該行在很近的時間內發生了多次更改,則可能無法提取該記錄的所有中間版本。

以這種方式重新水化的事件仍然非常有用,例如用於將資料變更傳播到全文搜尋引擎中;一般來說,只需在索引中保留記錄的最新版本即可,無需應用短時間內發生的所有中間更新。另一方面,如果您使用 CDC 來跟蹤採購訂單的狀態轉換並觸發相應的下游操作,或者用於維護審計日誌,那麼跟蹤每一次資料變化都是至關重要的,而這種技術就派不上用場了。

實施重新select策略時,應考慮一次檢索多條記錄。例如,當收到十條客戶記錄的變更事件時,與其執行十個查詢逐一檢索,不如將它們批次合併到一個查詢中,從而大大減少源資料庫的負載。另一個有趣的選擇是,不只檢索特定記錄本身,而是檢索整個資料集合。例如,當接收到客戶 42 的僅 ID 事件時,可以執行一個查詢,透過連線所有相關表來檢索客戶資料以及他們的地址資訊和銀行賬戶詳細資訊。

在比較三種資料變化事件型別並討論它們各自的優缺點之前,還有一個問題值得關注,那就是事件後設資料,即描述事件上下文資訊的資料。

更改事件後設資料
除了代表資料更改本身的實際更改事件有效載荷外,為事件提供額外的後設資料通常也很有用。這通常包括

  • 更改型別(插入、更新、刪除)
  • 事件發生的時間戳
  • 原始資料庫、模式和表的名稱
  • 事務 ID
  • 事件在源資料庫事務日誌中的位置
  • 觸發更改的查詢

舉例來說,下面是 Postgres 的 Debezium 聯結器發出的更新事件,在 ts_ms、op 和 source 欄位中包含一系列事件後設資料(在 Maxwell's Daemon 等其他 CDC 工具發出的事件中也能找到類似的後設資料):

{
  <font>"before": {
   
"id": 1004,
   
"first_name": "Billy",
   
"last_name": "Wilson",
   
"email": "bwilson@example.com"
  },
 
"after": {
   
"id": 1004,
   
"first_name": "Barray",
   
"last_name": "Wilson",
   
"email": "bwilson@example.com"
  },
 
"source": {
   
"version": "2.5.0.Final",
   
"connector": "postgresql",
   
"name": "dbserver1",
   
"ts_ms": 1705663711187,
   
"snapshot": "false",
   
"db": "postgres",
   
"sequence": "[\&#3434471328\&#34,\&#3434494376\&#34]",
   
"schema": "inventory",
   
"table": "customers",
   
"txId": 773,
   
"lsn": 34494376,
   
"xmin": null
  },
 
"op": "u",
 
"ts_ms": 1705663711220
}

變更事件後設資料可以在消費者端實現許多有趣的應用。例如,有關事件源自哪個事務的資訊也可用於將相同的事務語義傳播到資料管道的匯資料儲存區:您可以緩衝一個事務的事件,並在一個事務中將所有事件一次性應用到匯資料儲存區,而不是逐個攝取傳入的事件。

這樣,針對匯資料儲存的查詢就會受到與源資料庫相同的隔離保證。另一個有趣的後設資料欄位是 Debezium 的 Postgres 聯結器發出的 sequence 屬性,客戶端可利用它在資料管道中進行重複資料刪除,並至少使用一次語義。

比較
在探討了三種資料變更事件之後,您應該使用哪一種呢?很多時候,這個問題並沒有統一的答案。每種型別都有各自的優缺點,您需要根據具體情況做出明智的決定。

1、對於消費系統來說,完整的資料變更事件往往最容易處理。傳入的事件可以使用 "upsert "語義簡單地寫入匯資料儲存,覆蓋之前可能存在的任何版本。在透過分散式日誌系統(如 Apache Kafka)傳播變更事件時,可以壓縮包含完整變更事件的主題。由於每個事件都是完全獨立的,因此只需在日誌中保留每條記錄的最新變更事件即可,而且仍有可能將資料集的完整狀態傳播給消費者(如果變更事件包含新舊行映像,那麼即使在壓縮的變更事件主題中,也會保留每條記錄的最後兩個版本)。僅從分散式日誌中的狀態來引導新的事件消費者也是很容易實現的。

完整事件的缺點是體積較大。

2、標識事件則更為簡潔,因為除了已更改記錄的標識外,它們不傳遞任何其他資訊。為了檢索實際的事件狀態,您需要再次查詢源系統,這樣做的風險是,您可能會錯過從觸發變更事件到處理該事件這段時間內對記錄進行的任何中間更新。因此,它們的用途非常有限,但在某些用例(如快取失效)中卻能派上用場。

3、增量事件是一個有趣的中間地帶。它們只傳遞已更改記錄的已修改欄位,比完整事件佔用的空間更少。但為了將它們傳播到資料儲存池,資料儲存池必須具備進行部分更新的能力,即只更新記錄欄位的子集,而不是重寫整個記錄。如果無法做到這一點,可以在 CDC 工具和匯資料儲存之間使用有狀態流處理管道來重新建立完整事件。不能壓縮包含 delta 事件的變更事件主題,否則消費者可能會錯過重新建立所代表源記錄所需的更新事件。因此,當更新量很大時,包含部分事件的主題甚至可能比包含完整事件的(壓縮)主題佔用更多空間。

總結
使用 Apache Flink 等解決方案進行實時流處理,是 Debezium 等 CDC 工具的強大輔助工具,可在需要時轉換和修改變更事件流。例如,透過從源資料庫中選擇整個行狀態來擴充套件僅 ID 的變更事件,以及透過使用狀態儲存從 delta 變更事件中提取完整事件。

相關文章