使用 Debezium 實現真正的原子微服務以確保資料一致性 – brainDOSE

banq發表於2021-11-04

傳統的微服務發件箱模式實現需要開發人員手動建立發件箱事件表並編寫程式碼將資料從發件箱表傳送到相應的訊息平臺。Debezium 發件箱事件路由器和發件箱 Quarkus 擴充套件一起解決了這個問題,並通過宣告性實現強制執行標準方法來做到這一點。這使開發人員可以專注於業務邏輯實現並實現更快的應用程式交付。

 

概述

您可能已經知道微服務設計的最佳實踐之一是使用它自己的私有資料服務(在有狀態應用程式的情況下)實現應用程式服務,如每個服務模式的資料庫中所述。最終,當您的應用程式環境發展壯大時,您將擁有許多微服務和資料服務,如果不是數千個,也可能是數百個。這些服務作為獨立的可部署模組執行,但它們很可能需要在它們之間或與 3rd 方系統交換資料。

例如,當一個支付請求被提交到 CASA 服務時,它需要自動更新自己的資料庫,同時將交易資料傳送到其他感興趣的消費者服務,例如核心銀行。大多數情況下,您還需要在每個處理階段(CASA 和核心銀行業務)記錄交易的審計跟蹤副本。您不希望 CASA 服務負責更新審計跟蹤資料服務或直接呼叫核心銀行服務,而不是原子性,它們現在彼此緊密依賴。

您可能會考慮跨兩個服務更新資料服務,但是微服務沒有乾淨的方法(像傳統的 2PC 事務管理一樣的自動化方法)來跨越多個系統的事務會話,在大多數情況下,系統可能不支援 2PC(兩個-階段提交),例如訊息平臺、檔案系統等。情況會很快變得複雜,可能會導致資料一致性和完整性問題。

這是發件箱模式來拯救的地方。

 

發件箱模式

根據發件箱模式,不是讓應用程式在多個資料服務上執行 CRUD 操作或呼叫其他服務來執行此操作,應用程式服務應該只在其自己的資料庫上執行 CRUD 操作。為了與其他消費者服務共享資料,它應該將相關資料插入到同一資料庫或模式中的發件箱表中。這將確保全部失敗或全部成功的情況,因為所有這些 CRUD 操作都在同一個本地事務會話中執行。

需要一個單獨的獨立機制將這些資料從發件箱表中繼到相應的消費者服務。在我們的示例中,需要使用資料進行進一步處理的核心銀行業務。

說到這種訊息傳遞機制,其中一個選項就是流行的事件驅動平臺——Apache Kafka,本文將在示例中使用它,如下圖基於 Apache Kafka 和 Debezium 的發件箱模式所示。

使用 Debezium 實現真正的原子微服務以確保資料一致性 – brainDOSE

注意每個應用服務都有自己的資料庫(這裡是PostgreSQL資料庫),每個資料庫都有自己的業務資料表和對應的發件箱事件表。Debezium 負責使用這些發件箱表訊息並將它們生成給 Apache Kafka。

您可以看到這是理想的微服務架構,它促進了原子性,但同時防止了資料一致性問題。最重要的是,Debezium 發件箱模式與發件箱 Quarkus 擴充套件一起消除了許多手動步驟,讓開發人員專注於業務邏輯實現。

 

什麼是Debezium?

Debezium是一個用於變更資料捕獲 (CDC) 的開源分散式平臺。它提供非侵入式 CDC 來捕獲應用程式提交的資料庫插入、更新和刪除,並將這些更改流式傳輸到 Apache Kafka。

Debezium 建立在 Apache Kafka 之上。它是使用Kafka Connect 框架實現的。每個資料庫整合都是捕獲資料庫更改的 Kafka 源聯結器實現。

Debezium 目前提供以下聯結器:

您可以使用簡單訊息轉換 (SMT)應用宣告性訊息轉換。SMT 允許您為 Kafka 訊息轉換定義謂詞。謂詞指定如何有條件地將轉換應用於聯結器處理的訊息子集。您可以將謂詞分配給您為源聯結器(例如 Debezium)或接收器聯結器配置的轉換。

Debezium 提供了多種 SMT,您可以使用它們在 Kafka Connect 將記錄儲存到 Kafka 主題之前修改事件記錄。

以下是 Debezium 提供的 SMT 列表。

  • 主題路由:根據應用於原始主題名稱的正規表示式將記錄重新路由到不同的主題。
  • 基於內容的路由:根據事件內容將選定的事件重新路由到其他主題。
  • 新記錄狀態提取:從 Debezium 更改事件中提取欄位名稱和值的平面結構,促進無法處理 Debezium 複雜事件結構的接收器聯結器。
  • MongoDB 新文件狀態提取[url=https://debezium.io/documentation/reference/1.6/transformations/event-flattening.html]新記錄狀態提取[/url] :SMT特定於 MongoDB 的對應部分 。
  • 發件箱事件路由器:提供一種在多個(微)服務之間安全可靠地交換資料的方法。
  • 訊息過濾:根據聯結器的內容,將過濾器應用於聯結器發出的更改事件。這使您可以僅傳播與您相關的那些記錄。

 

使用 Debezium 實現發件箱

Debezium 引入了發件箱事件路由器,試圖從非常早期的版本開始就提供發件箱模式實現。

來自 Red Hat 的Gunnar MorlingReliable Microservices Data Exchange with the Outbox Pattern 中寫了一篇關於這個概念的有趣且鼓舞人心的文章,它提供瞭如何使用 Debezium 實現這個發件箱模式的藍圖。

在文章中,Gunnar 概述了單獨使用 Debezium 執行此操作的手動方法。今天,我們將研究如何使用Debezium Outbox RouterDebezium Quarkus Extension自動執行大部分手動步驟。通過這個新的 Quarkus 擴充套件,它提供了一種宣告式方法,使開發人員的生活更輕鬆,例如自動建立發件箱表、基於事件的路由等。

Debezium Quarkus 擴充套件功能目前處於孵化狀態,即根據收到的反饋,確切的語義、配置選項等可能會在未來的修訂版中發生變化。

 

使用 Debezium 發件箱模式的虛構支付交易實現

讓我們通過瀏覽我根據下圖建立的示例來深入瞭解細節。

使用 Debezium 實現真正的原子微服務以確保資料一致性 – brainDOSE

如圖所示,casa-service做了兩件事:

  • 為 CASA 事務請求和查詢提供 REST 介面。它在自己的名為casa-postgres的資料庫上執行必要的資料庫插入和查詢(casa表)。
  • 通過 Kafka Topic ( casa.response.events ) 使用來自核心服務的響應訊息,並根據收到的響應更新casa表。

core-service 消費使用來自casa.events主題的Kafka訊息,並按照core-postgres資料庫中casa 表在各自的賬戶實現賬戶平衡。

如您所見,這 2 個業務服務純粹是在執行自己的業務邏輯處理並維護自己的私有資料庫。

每個資料庫中都有發件箱表(CasaOutboxEvent和ResponseOutboxEvent)。Debezium 在 Quarkus 擴充套件的幫助下提供了開箱即用的發件箱表實現、永續性和事件流。作為開發人員,您可以自由地專注於您的業務邏輯實現。

那麼這些發件箱表格和訊息是如何建立的呢?

您需要做一些事情。讓我們以casa-service為例。

 

Maven 依賴

首先,通過在pom.xml 中插入以下 maven 依賴項以及其他依賴項,使您的 Quarkus 應用程式能夠使用 Debezium Outbox Quarkus 擴充套件。

<dependency>
   <groupId>io.debezium</groupId>
   <artifactId>debezium-quarkus-outbox</artifactId>
   <version>1.7.0.Alpha1</version>
</dependency>

事件資料模型

建立一個資料模型來表示您希望傳送給消費者的資料,在這種情況下,消費者是core-service。下面是 POJO CasaEventData.java程式碼片段的樣子。這個 POJO 資料模型將由我們稍後要建立的CasaEvent.java使用。

/**
 * Provide the event data model for Casa outbox event implementation.
 * Provides the implementation for Debezium Outbox Pattern.
 */
@RegisterForReflection
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public class CasaEventData {
    /**
     * Casa unique transaction id.
     */
    private String id;
    private String recipientAccountNo;
    private String sourceAccountNo;
    private double amount;
    /**
     * Casa created timestamp to indicates the timestamp when the Casa create event is being fired.
     */
    private Instant createdTimestamp;
    /**
     * Core processed timestamp to indicates the timestamp when the core processed the transaction.
     */
    private Instant coreProcessedTimestamp;
    /**
     * Audit timestamp to indicates the timestamp when the audit event is being fired.
     */
    private Instant auditTimestamp;
    /**
     * Response received timestamp to indicates the timestamp when the response from core is received by origination.
     */
    private Instant responseReceivedTimestamp;
    /**
     * Referecense information for recipient.
     */
    private String recipientReference;
    /**
     * Payment type. @see blog.braindose.paygate.model.PaymentTypes
     */
    private PaymentTypes paymentType;
    /**
     * Status of casa processing. @see blog.braindose.paygate.model.Status
     */
    private Status status;
    /**
     * Messages response from core backend if any. This could be error message when processing failed.
     */
    private String responseMessages;
 
    /**
     * Kafka header id
     * This is an unique id for each kafka message. Can be used to perform deduplication in the event of duplicated message sent by producer in the event of message resent due to unforeseen failure.
     */
    private String messageId;
 
    /**
     * Event source. This should be unique identifiable value for auditing purpose. Suggest to use Class.getName()
     */
    private String eventSources;
    /**
     * Event timestamp to provides timestamp when the event is being fired.
     */
    private Instant eventTimestamp;
 
   /// ... more omitted codes

 

發件箱事件實現

CasaEvent.java是實現io.debezium.outbox.quarkus.ExportedEvent的 Java 類。它為事件資訊提供了必要的實現,例如有效載荷、聚合、事件型別等。有效載荷應為JsonNode型別,這是我們決定將哪些資料作為有效載荷傳送的地方。

/**
 * Casa Event for Outbox pattern implementation using Debezium.
 */
@Immutable
public class CasaEvent implements ExportedEvent<String, JsonNode> {
 
    private static ObjectMapper mapper = new ObjectMapper();
 
    /**
     * Unique Casa transaction id. @see blog.braindose.opay.casa.Casaid
     */
    private final String id;
    /**
     * Payload to be sent to Kafka in JSON format.
     */
    private final JsonNode casa;
    /**
     * Timestamp for outbox pattern implementation. Defaulted to Casa transactionTimestamp. @see blog.braindose.opay.casa.Casa#transactionTimestamp
     */
    private final Instant timestamp;
 
    public CasaEvent(CasaEventData casa) {
        this.id = casa.getId();
        this.timestamp = casa.getEventTimestamp();
        this.casa = convertToJson(casa);
    }
 
    private JsonNode convertToJson(CasaEventData casa) {
        ObjectNode asJson = mapper.createObjectNode()
                .put("id", casa.getId())
                .put("recipientAccountNo", casa.getRecipientAccountNo())
                .put("sourceAccountNo", casa.getSourceAccountNo())
                .put("amount", casa.getAmount())
                .put("recipientReference", casa.getRecipientReference())
                .put("paymentType", casa.getPaymentType().toString())
                .put("createdTimestamp", casa.getCreatedTimestamp().toString())
                .put("eventSources", casa.getEventSources().toString())
                .put("eventTimestamp", casa.getEventTimestamp().toString())
                .put("status", casa.getStatus().toString());
        return asJson;
    }
 
    @Override
    public String getAggregateId() {
        return id;
    }
 
    @Override
    public String getAggregateType() {
        return "casa";
    }
 
    @Override
    public JsonNode getPayload() {
        return casa;
    }
 
    @Override
    public String getType() {
        return "payment";
    }
 
    @Override
    public Instant getTimestamp() {
        return timestamp;
    }
 
}

該CasaEvent將deserialised並填充到由Quarkus擴充套件API下表結構。當在您的程式碼中呼叫javax.enterprise.event.Event 中的fire(ExportedEvent)方法時,由CasaEvent表示的資料將插入到此表中。我們稍後會談到這一點。

Column     |          Type   | Modifiers
--------------+------------------------+-----------
id        | uuid            | not null
aggregatetype | character varying(255) | not null
aggregateid  | character varying(255) | not null
type       | character varying(255) | not null
payload     | jsonb                  |

以下資訊取自 Debezium 文件。SMT 將使用通過CasaEvent.java實現提供的這些資訊來構造要傳送到 Apache Kafka 的正確訊息。

  • ID:包含事件的唯一 ID。在發件箱訊息中,此值是一個頭部。例如,您可以使用此 ID 刪除重複訊息。要從不同的發件箱表格列中獲取事件的唯一 ID,請 在聯結器配置中設定 table.field.event.id SMT 選項
  • aggregatetype:包含 SMT 附加到聯結器向其發出發件箱訊息的主題名稱的值。預設行為是此值替換 SMT 選項中的預設 ${routedByValue} 變數 route.topic.replacement。例如,在預設配置中,  route.by.field SMT 選項設定為 aggregatetype ,  route.topic.replacement SMT 選項設定為 outbox.event.${routedByValue}。假設您的應用程式向發件箱表中新增了兩條記錄。在第一條記錄中, aggregatetype 列中的值為 customers。在第二條記錄中, aggregatetype 列中的值為 orders。聯結器向outbox.event.customers 主題發出第一條記錄 。聯結器向outbox.event.orders 主題發出第二條記錄 。要從不同的發件箱表格列中獲取此值,請設定 route.by.field 聯結器配置中的 SMT 選項
  • aggregateid:包含事件鍵,它為負載提供 ID。SMT 使用此值作為發出的發件箱訊息中的鍵。這對於維護 Kafka 分割槽中的正確順序很重要。要從不同的發件箱表格列中獲取事件金鑰 ,請在聯結器配置中設定 table.field.event.key SMT 選項
  • payload:發件箱更改事件的表示。預設結構是 JSON。預設情況下,Kafka 訊息值僅由該 payload 值組成。但是,如果發件箱事件配置為包含附加欄位,則 Kafka 訊息值包含一個封裝了有效負載和附加欄位的信封,並且每個欄位都單獨表示。有關更多資訊,請參閱 使用附加欄位傳送訊息。要從不同的發件箱表列獲取事件負載 ,請在聯結器配置中設定 table.field.event.payload SMT 選項
  • 其他自定義列:發件箱表中的任何其他列都可以 新增到 有效負載部分內的發件箱事件中,也可以作為訊息標題新增到發件箱事件中。一個例子可能是一列 eventType ,它傳達了一個使用者定義的值,有助於對事件進行分類或組織。

 

觸發發件箱事件

您需要做的下一件事是在您的應用程式程式碼中觸發發件箱事件。讓我們看一下CasaResource.java,它提供了用於建立 CASA 請求的 REST 介面。

/**
 * Provides REST interfaces for Casa services.
 */
@Path("casa")
public class CasaResource {
 
    @Inject
    Event<ExportedEvent<?, ?>> event;
     
    private static final Logger LOGGER = Logger.getLogger(CasaResource.class);
    private CasaEventData casaEventData = null;
    private boolean failed = false;
 
    /**
     * Create a new Casa transaction
     * @param casa
     * @return
     */
    @POST
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    @Transactional
    public Casa add(Casa casa) {
        try{
            casa.id = GenTxnId.id(TxnTypes.CASA);
            casa.createdTimestamp = Instant.now();
            casa.status = Status.SUBMITTED;
            casaEventData = new CasaEventData(casa.id, casa.recipientAccountNo, casa.sourceAccountNo, casa.amount, casa.createdTimestamp, casa.recipientReference, casa.paymentType, casa.status);
            casaEventData.setEventTimestamp(casa.createdTimestamp);
            casaEventData.setEventSources(Casa.class.getName());
            casa.persistAndFlush();
            event.fire(new CasaEvent(casaEventData));
        }
        catch(PersistenceException e){
            failed = true;
            if (casaEventData != null){
                casaEventData.setStatus(Status.FAILED);
                casaEventData.setResponseMessages("Error creating the Casa record in database.");
            }
            LOGGER.error("Error creating the Casa record in database.", e);
            throw e;
        }
        finally{
            if (casaEventData != null){
                event.fire(new CasaAuditEvent(casaEventData, EventTypes.PAYMENT, AggregateTypes.AUDIT_CASA));
                if (failed) event.fire(new CasaFailedEvent(casaEventData));
            }
        }
        return casa;
    }
    /// more codes omitted
    ...
    ...
    ...
 
}

從上面的程式碼可以看出,你注入javax.enterprise.event.Event作為事件

@Inject
Event<ExportedEvent<?, ?>> event;

然後使用它來觸發以CasaEventData作為引數的發件箱事件。從編碼的角度來看,這就是您需要做的所有事情。簡單幹淨!

event.fire(new CasaEvent(casaEventData));

 

Application.Properties

我們需要做的最後一件事是配置application.properties。這是為您希望命名的發件箱表指定名稱的地方。您還可以配置是否要在插入發件箱表中的事件資料後將其刪除。對於生產,您可能希望這樣做以節省資料庫儲存使用量。一旦插入事件資料,您就不需要將事件資料保留在發件箱表中,因為 Debezium 已經捕獲了插入事件,之後就不需要它們了。

# Debezium outbox
quarkus.debezium-outbox.table-name=CasaOutboxEvent
%dev.quarkus.debezium-outbox.remove-after-insert=false
%prod.quarkus.debezium-outbox.remove-after-insert=true

還有許多其他配置可用於 Quarkus 實現來自定義發件箱行為。

 

卡夫卡連線叢集

一旦應用程式實現準備就緒。您需要有一個正在執行的 Kafka Connect 叢集,並帶有適當的 Debezium 聯結器外掛。您可以配置聯結器外掛,在此示例中是用於 PostgresQLDebezium 聯結器,還有一些額外的配置,如下面的casa-service示例。

{
    "name": "outbox-connector-casa",
    "config" : {
        "connector.class": "io.debezium.connector.postgresql.PostgresConnector",
        "tasks.max": "1",
        "database.hostname": "casa-postgres",
        "database.port": "5432",
        "database.user": "casa",
        "database.password": "casa",
        "database.dbname": "casa",
        "database.server.name": "casa-event",
        "schema.include.list": "payment",
        "table.include.list": "payment.CasaOutboxEvent",
        "tombstones.on.delete": "false",
        "transforms": "outbox",
        "transforms.outbox.type": "io.debezium.transforms.outbox.EventRouter",
        "transforms.outbox.route.topic.replacement": "${routedByValue}.events",
        "transforms.outbox.table.field.event.timestamp": "timestamp",
        "transforms.outbox.table.field.event.id": "aggregateid",
        "transforms.outbox.table.fields.additional.placement": "type:header:eventType"
    }
}

Kafka Connector 容器在Docker Hub 中可用。您還可以從 GitHub獲取Dockerfile的副本並自己構建一個版本。

我們正在使用由transforms鍵指定的發件箱轉換。緊隨其後的是其他 Debezium Outbox SMT 配置,可以通過以“ transforms.outbox ”開頭的鍵輕鬆識別。“。可以在此處找到這些 SMT 設定的詳細資訊。其他是標準的 Kafka Connector 配置,例如與資料庫伺服器相關的設定。

請注意為transforms.outbox.route.topic.replacement配置的值。$ {} routedByValue是指aggregateType在CasaEvent.java。這允許我們將不同型別事務的相應訊息動態路由到不同的 Kafka 主題。在這種情況下,值為“casa”,Kafka Topic 變為casa.events。

所述transforms.outbox.table.field.event.id被配置為將aggregateId,其意。使用唯一的事務 ID 作為 Kafka 訊息鍵,我們可以在本示例的後面部分使用 Kafka Streams 輕鬆地對來自不同主題的這些訊息進行轉換。

請注意transforms.outbox.table.fields.additional.placement,它指定通過從CasaEvent.java注入eventType將其他頭欄位放入 Kafka 訊息頭中。這是非常有用的調整,你可能必須場景中使用單一卡夫卡主題捕捉到相同的交易型別的事件,而是針對不同的狀態,如例如通過貢納爾Morling給出。就我而言,我現在正在使用它並保持原樣。

 

一些額外的實現

有了上面的內容,我已經為核心服務複製了類似的方法,並且在很短的時間內完成了它。

為了讓事情變得更復雜(實際上並非如此),我還使用相同的方法來實現casa-service和core-service的審計跟蹤事件,正如您從以下程式碼片段中看到的那樣。由於我使用的是相同的發件箱表結構,我基本上可以重用我的許多Java類,並且眨眼間完成了事件跟蹤事件實現。

if (casaEventData != null){
   event.fire(new CasaAuditEvent(casaEventData, EventTypes.PAYMENT, AggregateTypes.AUDIT_CASA));
   if (failed) event.fire(new CasaFailedEvent(casaEventData));
}

隨著它變得更容易,我還實現了捕獲FailedEvent,以便可以將這些失敗的事務捕獲到另一個 Kafka 主題中,以便可能的人工干預或使用工作流進行一些自動化處理。在這種情況下,casa-service和core-service將不需要擔心如何處理那些失敗的事務。整潔的!

 

使用 Kafka Streams 進行審計跟蹤聚合

審計事件是來自casa-service和core-service的資訊片段。我們需要聚合這些斷開連線的資訊,以便為每個事務建立一個完整的單一審計跟蹤條目,並將其儲存到資料庫中以進行安全儲存和驗證(本示例中未涵蓋)。這就是 Kafka Streams 派上用場的地方。我基本上做了一個簡單的join(),然後是一個reduce()來做到這一點。這很容易完成,因為我的 Kafka Message 鍵是我之前配置的唯一事務 ID。

負載使用的是JsonNode,我注意到它在初始階段被捕獲時被反序列化為帶有額外雙引號的 JSON 字串。使用ObjectMapperSerde將無法直接序列化為 Java 物件,我別無選擇,只能將其序列化為 String 格式。我希望這可以在將來 GA 時得到改進。

builder.stream(
            KAFKA_TOPIC_CASA_AUDIT,
            Consumed.with(Serdes.String(), Serdes.String()))        // Serialized JSON string from JsonNode creates extra double quotes, causing it is not possible to use Jackson to deserialize into Java object
            .join(
                coreAuditStream,
                (casaAudit, coreAudit) -> {
                    // Multiple audit entries since Casa service received the response from core service.
                    List<AuditEntry> auditEntries = new ArrayList<>();
                    try {
                        LOGGER.debug("Processing casa audit trail...");
                        CasaEventData casaAuditObj = createCasaEventData(casaAudit);
                        auditEntries.add(createAuditEntry(casaAuditObj));
                         
                        LOGGER.debug("Processing core audit trail for casa transaction ...");
 
                        auditEntries.add(createAuditEntry(createCasaEventData(coreAudit)));
                         
                        AuditData<Casa> auditData = new AuditData<>(
                            casaAuditObj.getId(), 
                            auditEntries, 
                            new Casa(casaAuditObj.getRecipientAccountNo(), casaAuditObj.getSourceAccountNo(), casaAuditObj.getAmount(), casaAuditObj.getRecipientReference()), 
                            Instant.now().toString(), 
                            casaAuditObj.getStatus().toString());
 
                        String jsonInString = mapper.writeValueAsString(auditData);
                        LOGGER.debug("Joined result = " + jsonInString);
                        return jsonInString;
                         
                    } catch (JsonProcessingException e) {
                        LOGGER.error("Problem parsing Kafka message into JSON.");
                        throw new RuntimeException("Problem parsing Kafka message into JSON", e);
                    }
                },
                JoinWindows.of(Duration.ofMinutes(KAFKA_STREAMS_JOINWINDOW_DURATION)),
                Joined.with(Serdes.String(), Serdes.String(), Serdes.String())
            )
            .groupByKey()
            .reduce(            // deduplication of audit trail ... 
                (value1, value2) -> AuditData.reduce(value1, value2),
                Materialized.with(Serdes.String(), Serdes.String())
            )
            .toStream()
            //.print(org.apache.kafka.streams.kstream.Printed.toSysOut())
            .to(KAFKA_TOPIC_PAYMENT_AUDIT, Produced.with(Serdes.String(), Serdes.String()))
        ;

 

執行示例

請前往GitHub並在本地磁碟中克隆專案的副本。在命令提示符中導航到模組目錄。按照README.md 中的說明構建模組,然後執行以下docker compose命令以將所有服務作為容器啟動。

docker compose up --build

使用docker ps檢查所有容器的狀態,等待它們變得健康。

使用以下配置建立必要的 Kafka 聯結器。這裡有 3 個聯結器需要註冊。

  • outbox-connector-casa – 這是casa-postgres資料庫中發件箱表的 Debezium 聯結器
  • outbox-connector-core – 這是core-postgres資料庫中發件箱表的 Debezium 聯結器
  • mongodb-sink – 這是審計表的 Kafka MongoDB 聯結器。

{
    "name": "outbox-connector-casa",
    "config" : {
        "connector.class": "io.debezium.connector.postgresql.PostgresConnector",
        "tasks.max": "1",
        "database.hostname": "casa-postgres",
        "database.port": "5432",
        "database.user": "casa",
        "database.password": "casa",
        "database.dbname": "casa",
        "database.server.name": "casa-event",
        "schema.include.list": "payment",
        "table.include.list": "payment.CasaOutboxEvent",
        "tombstones.on.delete": "false",
        "transforms": "outbox",
        "transforms.outbox.type": "io.debezium.transforms.outbox.EventRouter",
        "transforms.outbox.route.topic.replacement": "${routedByValue}.events",
        "transforms.outbox.table.field.event.timestamp": "timestamp",
        "transforms.outbox.table.field.event.id": "aggregateid",
        "transforms.outbox.table.fields.additional.placement": "type:header:eventType"
    }
}
{
    "name": "outbox-connector-core",
    "config" : {
        "connector.class": "io.debezium.connector.postgresql.PostgresConnector",
        "tasks.max": "1",
        "database.hostname": "core-postgres",
        "database.port": "5432",
        "database.user": "core",
        "database.password": "core",
        "database.dbname": "core",
        "database.server.name": "core-event",
        "schema.include.list": "core",
        "table.include.list": "core.ResponseOutboxEvent",
        "tombstones.on.delete": "false",
        "transforms": "outbox",
        "transforms.outbox.type": "io.debezium.transforms.outbox.EventRouter",
        "transforms.outbox.route.topic.replacement": "${routedByValue}.events",
        "transforms.outbox.table.field.event.timestamp": "timestamp",
        "transforms.outbox.table.field.event.id": "aggregateid",
        "transforms.outbox.table.fields.additional.placement": "type:header:eventType"
    }
}
{
    "name": "mongodb-sink",
    "config": {
        "connector.class": "com.mongodb.kafka.connect.MongoSinkConnector",
        "tasks.max": 1,
        "topics": "payment.audit.events",
        "connection.uri": "mongodb://audit:audit@audit-mongodb:27017",
        "database": "audit",
        "collection": "payment",
        "key.converter": "org.apache.kafka.connect.json.JsonConverter",
        "key.converter.schemas.enable": false,
        "value.converter": "org.apache.kafka.connect.json.JsonConverter",
        "value.converter.schemas.enable": false,
        "max.num.retries": 3
    }
}

Kafka Connect 叢集 URL 是http://localhost:9080/connectors。使用curl或Postman為前面提到的每個聯結器執行 HTTP 釋出以建立上述聯結器。

以下是您將在審計表中看到的結果。

rs0:PRIMARY> use audit
switched to db audit
rs0:PRIMARY> db.payment.find().pretty();
{
    "_id" : ObjectId("613981a793f9aa4f64c33eb6"),
    "payload" : {
        "amount" : 50.58,
        "recipientAccountNo" : "1-987654-1234-4569",
        "recipientReference" : "Payment for lunch",
        "sourceAccountNo" : "1-234567-4321-9876"
    },
    "id" : "1-20210909-033739893-17839",
    "lastStatus" : "COMPLETED",
    "auditEntries" : [
        {
            "eventSource" : "blog.braindose.opay.casa.Casa",
            "eventTimestamp" : "2021-09-09T03:37:39.905947Z",
            "responseMessages" : null,
            "status" : "SUBMITTED"
        },
        {
            "eventSource" : "blog.braindose.opay.core.casa.ConsumeCasa",
            "eventTimestamp" : "2021-09-09T03:37:42.180891Z",
            "responseMessages" : null,
            "status" : "COMPLETED"
        },
        {
            "eventSource" : "blog.braindose.opay.casa.ConsumeCasaResponse",
            "eventTimestamp" : "2021-09-09T03:37:43.271790Z",
            "responseMessages" : null,
            "status" : "COMPLETED"
        }
    ],
    "eventTimestamp" : "2021-09-09T03:37:45.720514Z"
}

 

GitHub 上的示例程式碼

相關文章