Dapr Outbox 執行流程

寻己Tenleft發表於2024-04-23

Dapr Outbox 是1.12中的功能。
本文只介紹Dapr Outbox 執行流程,Dapr Outbox基本用法請閱讀官方文件 。本文中appID=order-processor,topic=orders

本文前提知識:熟悉Dapr狀態管理Dapr釋出訂閱Outbox 模式
Outbox 模式的核心是在同一個資料庫事務中儲存業務資料和待發布的事件訊息,再由某個“定時任務”讀取待發布的事件訊息併發布事件(並刪除資料庫中事件訊息)
相關文章:
.NET中實現Outbox模式的框架CAP,作者Savorboard
使用 dotnetcore/CAP 的本地訊息表模式,聖傑

先在內部釋出一個主題(topic)

要使用Dapr Outbox,在.NET中就是呼叫DaprClientExecuteStateTransactionAsync(...)方法(得先完成Outbox相關的配置!),呼叫此方法會完成事務操作(儲存業務資料和待發布的事件訊息)併發布事件訊息。

string DAPR_STORE_NAME = "statestoresql";
var client = new DaprClientBuilder().Build();
var orderId = 1;
var order = new Order(orderId);

var bytes = JsonSerializer.SerializeToUtf8Bytes(order);
var upsert = new List<StateTransactionRequest>()
{
    new StateTransactionRequest(orderId.ToString(), bytes, StateOperationType.Upsert)
};

// 儲存狀態,併發布事件訊息
await client.ExecuteStateTransactionAsync(DAPR_STORE_NAME, upsert);

public record Order([property: JsonPropertyName("orderId")] int orderId);
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: orderpubsub # 釋出訂閱元件
spec:
  type: pubsub.redis
  version: v1
  metadata:
  - name: redisHost
    value: localhost:6379
  - name: redisPassword
    value: ""
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: statestoresql  # 狀態元件
spec:
  type: state.mysql
  version: v1
  metadata:
    - name: connectionString
      value: "root:mysecret@tcp(localhost:3306)/?allowNativePasswords=true"
    - name: outboxPublishPubsub
      value: orderpubsub
    - name: outboxPublishTopic
      value: orders

呼叫ExecuteStateTransactionAsync(...)方法時,此方法把請求轉發給sidecar,sidecar會釋出一個內部主題。所謂內部,就是供Dapr使用,使用者不用操作;所謂主題(Topic)就是一個事件;此主題格式為:namespace + appID + topic + "outbox" ,假設appID=order-processor,topic=orders,則內部主題(Topic)名就是order-processorordersoutbox(namespace 是與k8s有關),此主題用於判斷事務是否執行成功。

注:該內部主題(topic)預設和事件訊息使用同一個Dapr釋出/訂閱元件,可以透過配置狀態元件的後設資料(metadata配置)欄位outboxPubsub單獨指定內部主題所使用的釋出/訂閱元件。相關配置請看官方文件

主題內容CloudEvent格式,釋出的事件資料如下(真正的待發布事件訊息就是json中的data欄位,後面就是讀取的此值):

{
    "data":"{\"orderId\":1}",
    "datacontenttype":"text/plain",
    "id":"outbox-a53e45f3-d646-4e4e-bcbf-0692ec7b9dd0",
    "pubsubname":"orderpubsub",
    "source":"order-processor",
    "specversion":"1.0",
    "time":"2024-01-25T17:12:31+08:00",
    "topic":"",
    "traceid":"",
    "traceparent":"",
    "tracestate":"",
    "type":"com.dapr.event.sent"
}

有了事件的釋出者,那事件的訂閱者是誰呢?appID=order-processor的Dapr sidecar例項。可以是執行儲存狀態的sidecar程式,或者是appID=order-processor的其他sidecar。

在同一事務中儲存狀態和事件訊息

  • 在內部主題(Topic)釋出成功,會在同一事務中儲存狀態和事件訊息,也就是將方法client.ExecuteStateTransactionAsync(...)中的資料儲存到資料庫。id為outbox-a53e45f3-d646-4e4e-bcbf-0692ec7b9dd0的表示需待發布事件訊息,id為order-processor||1表示狀態資料。事件訊息和狀態資料儲存在同一張表state中,在mysql中其表結構和資料如下所示。

  • 如果此內部主題(Topic)釋出失敗,呼叫方直接拋異常,不會執行事務操作!state表不會有下面兩條資料。

  • "eyJvcmRlcklkIjoxfQ=="既是狀態資料又是待發布的事件資料;經過Base64解碼,得到該值為json格式,即:{"orderId":1}

CREATE TABLE `state`  (
  `id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `value` json NOT NULL,
  `isbinary` tinyint(1) NOT NULL,
  `insertDate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updateDate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `eTag` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `expiredate` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `expiredate_idx`(`expiredate` ASC) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
id value isbinary insertDate updateDate eTag expiredate
outbox-a53e45f3-d646-4e4e-bcbf-0692ec7b9dd0 "0" 0 2024-01-25 09:22:14 2024-01-25 09:22:14 07884eed-eb5d-4887-8399-051c71206ed5
order-processor||1 "eyJvcmRlcklkIjoxfQ==" 1 2024-01-25 09:12:31 2024-01-25 09:22:14 3d1e368f-f6d8-4ccd-946d-c10090c7cc42

內部主題(Topic)的訂閱者釋出事件訊息

資料庫事務執行成功後,什麼時候把事件訊息釋出出去呢?
事件訊息釋出出去是在內部主題(Topic)的訂閱者中實現的,具體如下:

步驟XappIDorder-processor的sidecar接收到內部主題(Topic)傳送的事件,然後透過查詢判斷id為outbox-a53e45f3-d646-4e4e-bcbf-0692ec7b9dd0的資料是否存在?

  • 如果存在,表示狀態資料和事件訊息都已儲存在mysql中,則釋出事件訊息(事件資料就前面提到的data欄位)。事件釋出成功後,則刪除id為outbox-a53e45f3-d646-4e4e-bcbf-0692ec7b9dd0的記錄。
  • 如果不存在就直接退出,停止後續操作;事件的訂閱者會多次收到訂閱訊息,即重複步驟X過程。

這裡會有一個問題:接收到內部主題(Topic)後,狀態和事件訊息可能沒有持久化到mysql(前面提到過,Dapr sidecar是先釋出一個內部主題,再在同一事務中儲存狀態和事件訊息)。所以獲取狀態執行以下重試策略。刪除狀態時也是此重試策略。

bo := &backoff.ExponentialBackOff{
    InitialInterval:     time.Millisecond * 500,// 初始間隔
    MaxInterval:         time.Second * 3,       // 最大間隔。重試時間超過此值時,以此值為準
    MaxElapsedTime:      time.Second * 10,      // 累計重試時間
    Multiplier:          3,                     // 遞增倍數  
    Clock:               backoff.SystemClock,
    RandomizationFactor: 0.1,                   // 隨機因子  
}

總結

Dapr Outbox 執行流程簡單說就是:先釋出一個內部事件,再執行儲存業務資料和事件訊息,內部事件的訂閱者再發布真正的事件訊息。Dapr輪詢資料庫中待發布事件訊息是透過訂閱一個內部主題(Topic)實現的。
因為狀態儲存和事件釋出是在sidecar中執行,所以業務程式碼和事件訊息不在同一個事務中!!!Dapr Outbox是把業務的狀態資料和事件訊息在同一個事務中儲存,也就是程式碼client.ExecuteStateTransactionAsync(...);並且狀態資料和事件訊息是儲存到同一張表state中。

參考:

程式碼

Enable the transactional outbox pattern

outbox.go

Outbox issues

相關文章