使用嵌入式Debezium和SpringBoot捕獲更改資料事件(CDC) - Sohan Ganapathy

banq發表於2019-11-22

在處理資料或複製資料來源時,您可能已經聽說過術語更改資料捕獲(CDC)。顧名思義,“ CDC”是一種設計模式,可以持續識別並捕獲資料的增量更改。該模式用於跨實時資料庫到分析資料來源或只讀副本的實時資料複製。它還可以用於根據資料更改觸發事件,例如OutBox模式
大多數現代資料庫透過事務日誌支援CDC 。事務日誌是對資料庫所做的所有更改的順序記錄,而實際資料包含在單獨的檔案中。
在這個部落格中,我想集中精力使用CDC常用的框架,並將其嵌入SpringBoot。

什麼是Debezium?
Debezium是為CDC構建的分散式平臺,它使用資料庫事務日誌並在行級別更改時建立事件流。偵聽這些事件的應用程式可以基於增量資料更改來執行所需的操作。
Debezium提供了一個聯結器庫,支援當今可用的各種資料庫。這些聯結器可以監視和記錄資料庫模式中的行級更改,然後將更改釋出到諸如Kafka的流服務上。
通常,將一個或多個聯結器部署到Kafka Connect 叢集中,並配置為監視資料庫並將資料更改事件釋出到Kafka。分散式Kafka Connect群集可提供所需的容錯能力和可伸縮性,從而確保所有已配置的聯結器始終處於執行狀態。

什麼是嵌入式Debezium?
不需要容錯和可靠性水平的應用程式,或者希望將執行整個平臺的成本降至最低的應用程式,可以在應用程式中執行Debezium聯結器。這是透過嵌入Debezium引擎並將聯結器配置為在應用程式中執行來完成的。在發生資料更改事件時,聯結器會將它們直接傳送到應用程式。

使用SpringBoot執行Debezium
我們有一個SpringBoot應用程式“ Student CDC Relay”,它執行嵌入式Debezium,並追加包含“ Student”表的Postgres資料庫的事務日誌。當在“Student”表上執行諸如插入/更新/刪除之類的資料庫操作時,在SpringBoot應用程式中配置的Debezium聯結器將在應用程式內呼叫一個方法。該方法對這些事件起作用,並在ElasticSearch上的Student索引中同步資料。
示例程式碼可在此處找到。

安裝所需工具
可以在下面的docker-compose檔案中安裝所有必需的工具。這將在埠5432上啟動Postgres資料庫,並在埠9200(HTTP)和9300(Transport)上啟動Elastic Search 。

version: "3.5"

services:
  # Install postgres and setup the student database.
  postgres:
    container_name: postgres
    image: debezium/postgres
    ports:
      - 5432:5432
    environment:
      - POSTGRES_DB=studentdb
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password

  # Install Elasticsearch.
  elasticsearch:
    container_name: elasticsearch
    image: docker.elastic.co/elasticsearch/elasticsearch:6.8.0
    environment:
    - discovery.type=single-node
    ports:
      - 9200:9200
      - 9300:9300



我們使用映象debezium/postgres,因為它預先構建了邏輯解碼功能。這是一種機制,它允許提取已提交到事務日誌的更改,從而使CDC成為可能。可以在此處找到將外掛安裝到Postgres的文件。

瞭解程式碼
第一步是為debezium-embedded和定義Maven依賴項debezium-connector。該示例從Postgres讀取更改,因此我們使用Postgres聯結器。

<dependency>
    <groupId>io.debezium</groupId>
    <artifactId>debezium-embedded</artifactId>
    <version>${debezium.version}</version>
</dependency>
<dependency>
    <groupId>io.debezium</groupId>
    <artifactId>debezium-connector-postgres</artifactId>
    <version>${debezium.version}</version>
</dependency>

然後,我們配置聯結器,該聯結器偵聽Student表上的更改。我們使用PostgresConnector,對於connector.class這是由Debezium提供設定。這是追加資料庫的聯結器的Java類的名稱。
聯結器還具有一個重要的設定- offset.storage,它可以幫助應用程式從事務日誌中跟蹤已處理的數量。如果應用程式在處理過程中失敗,它可以從重新啟動後失敗的地方繼續讀取更改。有多種儲存偏移量的方法,但是在此示例中,我們使用類FileOffsetBackingStore將偏移量儲存在由定義的本地檔案中offset.storage.file.filename。聯結器會記錄檔案中的偏移量,對於它讀取的每個更改,Debezium引擎都會根據setting定期將偏移量重新整理到檔案中offset.flush.interval.ms。
聯結器的其他引數是容納Student表的Postgres資料庫屬性。

@Bean
public io.debezium.config.Configuration studentConnector() {
    return io.debezium.config.Configuration.create()
            .with("connector.class", "io.debezium.connector.postgresql.PostgresConnector")
            .with("offset.storage",  "org.apache.kafka.connect.storage.FileOffsetBackingStore")
            .with("offset.storage.file.filename", "/path/cdc/offset/student-offset.dat")
            .with("offset.flush.interval.ms", 60000)
            .with("name", "student-postgres-connector")
            .with("database.server.name", studentDBHost+"-"+studentDBName)
            .with("database.hostname", studentDBHost)
            .with("database.port", studentDBPort)
            .with("database.user", studentDBUserName)
            .with("database.password", studentDBPassword)
            .with("database.dbname", studentDBName)
            .with("table.whitelist", STUDENT_TABLE_NAME).build();
}


設定嵌入式Debezium的最後更改是在應用程式啟動時啟動它。為此,我們使用類EmbeddedEngine,該類充當聯結器的包裝器並管理聯結器的生命週期。使用聯結器配置和為每個資料更改事件將呼叫的函式(在我們的示例中為method)建立引擎handleEvent()。

private CDCListener(Configuration studentConnector, StudentService studentService) {
    this.engine = EmbeddedEngine
            .create()
            .using(studentConnector)
            .notifying(this::handleEvent).build();

    this.studentService = studentService;
}


在handleEvent()解析每個事件時,StudentService使用Spring Data JPA for Elasticsearch對發生的操作進行標識並呼叫,以在Elastic Search上執行建立/更新/刪除操作。
現在我們已經設定好了,EmbeddedEngine我們可以使用該Executor服務非同步啟動它了。

private final Executor executor = Executors.newSingleThreadExecutor();

...

@PostConstruct
private void start() {
    this.executor.execute(engine);
}

@PreDestroy
private void stop() {
    if (this.engine != null) {
        this.engine.stop();
    }
}


看到實際的程式碼
一旦我們使用命令執行docker-compose檔案並使用命令docker-compose up -d啟動'Student CDC Relay' 啟動了所有必需的工具mvn spring-boot:run。我們可以透過執行以下指令碼來設定Student表:

CREATE TABLE public.student
(
    id integer NOT NULL,
    address character varying(255),
    email character varying(255),
    name character varying(255),
    CONSTRAINT student_pkey PRIMARY KEY (id)
);


為了檢視執行中的程式碼,我們在剛建立的表上進行了資料更改。
將記錄插入到學生表中:
執行下面的SQL將記錄插入到Postgres的Student表中。

INSERT INTO STUDENT(ID, NAME, ADDRESS, EMAIL) VALUES('1','Jack','Dallas, TX','jack@gmail.com');


確認Elastic Search上資料已經更改:

$ curl -X GET http://localhost:9200/student/student/1?pretty=true
{
  "_index" : "student",
  "_type" : "student",
  "_id" : "1",
  "_version" : 31,
  "_seq_no" : 30,
  "_primary_term" : 1,
  "found" : true,
  "_source" : {
    "id" : 1,
    "name" : "Jack",
    "address" : "Dallas, TX",
    "email" : "jack@gmail.com"
  }
}


更新:

UPDATE STUDENT SET EMAIL='jill@gmail.com', NAME='Jill' WHERE ID = 1;


我們可以在Elastic Search上驗證資料已更改為“Jill”

$ curl -X GET http://localhost:9200/student/student/1?pretty=true
{
  "_index" : "student",
  "_type" : "student",
  "_id" : "1",
  "_version" : 32,
  "_seq_no" : 31,
  "_primary_term" : 1,
  "found" : true,
  "_source" : {
    "id" : 1,
    "name" : "Jill",
    "address" : "Dallas, TX",
    "email" : "jill@gmail.com"
  }
}


最後的想法
這種方法確實非常簡單,只有很少的活動部件,但是在縮放方面受到更大的限制,並且對故障的容忍度也大大降低。
當CDC-Relay應用程式執行良好時,將完全處理一次 源記錄,但是在CDC-Relay應用程式重新啟動後,底層應用程式確實需要容忍接收重複事件。
我們可以透過在另一個埠上啟動“ Student CDC Relay”的另一個例項來測試圍繞縮放的限制,我們看到以下異常:

2019-11-20 12:33:17.901 ERROR 59453 --- [pool-2-thread-1] io.debezium.embedded.EmbeddedEngine      : Error while trying to run connector class 'io.debezium.connector.postgresql.PostgresConnector'

Caused by: org.postgresql.util.PSQLException: ERROR: replication slot "debezium" is active for PID <>
    at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2440) ~[postgresql-42.2.5.jar:42.2.5]
    at org.postgresql.core.v3.QueryExecutorImpl.processCopyResults(QueryExecutorImpl.java:1116) ~[postgresql-42.2.5.jar:42.2.5]
    at org.postgresql.core.v3.QueryExecutorImpl.startCopy(QueryExecutorImpl.java:842) ~[postgresql-42.2.5.jar:42.2.5]
    at org.postgresql.core.v3.replication.V3ReplicationProtocol.initializeReplication(V3ReplicationProtocol.java:58) ~[postgresql-42.2.5.jar:42.2.5]
    at org.postgresql.core.v3.replication.V3ReplicationProtocol.startLogical(V3ReplicationProtocol.java:42) ~[postgresql-42.2.5.jar:42.2.5]
    at org.postgresql.replication.fluent.ReplicationStreamBuilder$1.start(ReplicationStreamBuilder.java:38) ~[postgresql-42.2.5.jar:42.2.5]
    at org.postgresql.replication.fluent.logical.LogicalStreamBuilder.start(LogicalStreamBuilder.java:37) ~[postgresql-42.2.5.jar:42.2.5]




如果您的應用程式需要所有訊息的至少一次傳送保證,最好將完整的分散式Debezium系統與Kafka-Connect一起使用。

 

相關文章