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中就是呼叫DaprClient
的ExecuteStateTransactionAsync(...)
方法(得先完成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)的訂閱者
中實現的,具體如下:
步驟X
:appID
為order-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