Outbox模式
Outbox是一種體系結構模式,其中域服務將業務更改和相關事件記錄在其存儲中的單個本地事務中。將事件發布到外部總線/隊列是由單獨的安全過程(publisher)異步執行的,該過程讀取「outbox」表並轉發記錄。這種方法消除了「首先進入DB,然後進入輪胎」的競賽,即使在發生故障時也能提供可靠的交付。
1)何時應用
適合:- 帶有上下文之間事件的微服務和模塊化整體。
- 必須確保「狀態已固定↔事件不能丟失」。
- 需要相容性和受控的再交付。
- 強硬的全球多資源交易至關重要(比具有明確合同的TSS/傳奇更好)。
- 沒有專用的真理源(狀態存儲在事件生成的地方)。
2)目標和屬性
Atomic write:域名條目+outbox-在一個事務中。
上一次發布:允許重播,排除損失。
消費者的平均水平:訂戶一方的雙打保護。
有效的exactly-once:通過outbox+idempotent consumer+dedup組合實現。
清晰遙測:業務運營和事件的相關性。
3)數據圖(示例)
sql
-- Domain table (example: orders)
CREATE TABLE orders (
id UUID PRIMARY KEY,
tenant_id TEXT NOT NULL,
status TEXT NOT NULL,
total_amount NUMERIC(12,2) NOT NULL,
updated_at TIMESTAMP NOT NULL DEFAULT now()
);
-- Outbox
CREATE TABLE outbox (
id UUID PRIMARY KEY, -- event_id aggregate_type TEXT NOT NULL, -- 'order'
aggregate_id UUID NOT NULL, -- order_id tenant_id TEXT NOT NULL,
type TEXT NOT NULL, -- 'OrderCreated'
payload JSONB NOT NULL, -- serialized headers event JSONB NOT NULL DEFAULT '{}':: jsonb,
occurred_at TIMESTAMP NOT NULL, -- time in domain transaction available_at TIMESTAMP NOT NULL, -- earliest publish time (backoff)
published_at TIMESTAMP, - is filled by the attempts INT NOT NULL DEFAULT 0,
error TEXT
);
CREATE INDEX ON outbox (available_at) WHERE published_at IS NULL;
CREATE INDEX ON outbox (tenant_id, available_at) WHERE published_at IS NULL;
4)事務性模板(應用程序層)
pseudo begin tx domainChange () # INSERT/UPDATE in domain table insert into outbox (event) # event with aggregate/tenant commit tx keys
如果commit成功,則outbox中的事件保證存在。如果應用程序在Commit之後下降-Publlisher將趕上。
5)Publisher(讀者→出版商)
任務是:- 定期閱讀batchami未發布的事件(「published_at IS NULL」和「available_at<=now()」)。
- 嘗試發布到總線/隊列;成功時-標記「published_at」。
- 如果出現錯誤,則增加「attempts」,將「available_at」設置為未來(外在背面),並編寫「error」。
- 尊重tenant/keys (fairness)的限制,不要阻止。
pseudo loop:
events = select from outbox where published_at is null and available_at <= now()
order by occurred_at limit BATCH_SIZE for update skip locked
for e in events:
try:
broker. publish(topicFor(e), serialize(e. payload), headers(e))
markPublished(e. id, now())
except Retryable:
backoff = computeBackoff(e. attempts)
reschedule(e. id, now()+backoff, attempts+1, last_error)
except NonRetryable:
moveToDLQ (e) or markError (e) # by sleep (POLL_INTERVAL) policy
6)相似性和重復數據消除
消費者方面(Inbox/Idempotency store):sql
CREATE TABLE inbox (
consumer_name TEXT,
event_id UUID,
processed_at TIMESTAMP NOT NULL,
PRIMARY KEY (consumer_name, event_id)
);
算法:接收事件時-首先在「inbox」中嘗試「INSERT」;如果密鑰沖突-事件已→「無操作」處理。接下來是業務邏輯。
在公用事業方面:頭部中的「Idempotency-Key」(例如「event_id」),以便總線/經紀人/代理人可以過濾重復。
7)順序和因果關系
「Aggregate_id」上的本地順序由「occurred_at」排序和「按鍵」發布提供。
對於具有參與性的日誌總線-使用「aggregate_id」/「tenant_id」鍵進行參與,以使單個單元的事件處於同一參與狀態。
如果訂單是關鍵的,則避免在公共場所進行跨線比賽。
8) CDC (Change Data Capture)
可以使用CDC代替活動的Publisher:引擎讀取DB事務日誌並將字符串「outbox」廣播到總線。優點是DB的最小負載,精確的順序,不加壓。缺點是操作的復雜性和對DBMS特征的束縛。兩種方法都是有效的;根據能力和SLO進行選擇。
9)錯誤,DLQ和redrive
Retryable(網絡、限制)-放大「attempts」,推遲「available_at」 (exponential backoff+ jitter)。
非retryable(非驗證電路/合同)-帶有豐富的元數據轉移到DLQ/Dead-Letter Topic。
安全稀有:batchi, rate-limit,方案驗證,優先次序低於prod流量。
10)多重性與限制
強制性標簽: 'tenant_id'、'plan'、'region'-在'outbox中。headers`.
Per-tenant fairness: Publisher將出版物的「窗口」和嘗試限制分配給租戶。
駐留:將outbox存儲在域名數據的同一區域;區域間出版物-僅匯總/摘要。
11)安全性和合規性
關於Tenant/Region政策的PII版Payload/headers。
如果總線「陌生」,則簽名/加密有效載荷。
審核所有狀態轉換:創建、發布、錯誤、重新分區。
12)可觀察性
度量標準:- 出版時差('now-occurred_at' p50/p95/p99)。
- 成功率,錯誤率,原因分布。
- outbox大小(未發布),嘗試/秒。
- Throughput和lag的特寫圖形。
- 「event_id」/「aggregate_id」/「saga_id」相關性;演唱「db-tx」,「publish」,「retry」。
- 註釋:「attempt」,「backoff_ms」,「dlq=true」。
- 成功簡短記錄;Error/Redrive上的完整部件。
13)測試和混亂
Atomicity測試:在域名交易發布之前,人為地「下降」-事件必須在稍後發布。
Duplicate測試:我們多次發布同一事件-用戶恰好執行一種效果(inbox)。
順序測試:每個單元的事件包-序列/等效性檢查。
混亂:經紀人拒絕,DB潛伏期上升,分裂大腦公益員,時鐘滑行。
14)配置模板(示例)
yaml outbox:
poll_interval_ms: 200 batch_size: 200 order_by: occurred_at backoff:
strategy: exponential_full_jitter initial_ms: 250 max_ms: 10_000 max_attempts: 20 fairness:
per_tenant_parallelism: 4 per_key_serial: true
publisher:
rate_limit_per_sec: 500 headers:
idempotency_key: event_id schema_version: v3 dlq:
enabled: true topic: myapp. events. dlq include_metadata:
- error
- attempts
- source_table
- tenant_id
- aggregate_id
15)與傳奇和復古的集成
Outbox是傳奇步驟的「安全傳輸」:本地事務編寫效果和命令/事件;出版物-可靠且可計量。
重播和回放策略必須與「Retry-After」和Circuit Breaker保持一致;避免「暴風雨」。
16)典型錯誤
我們編寫了域狀態公差後的事件-跌倒時可能會丟失。
「outbox」中沒有索引/存檔→發布延遲增加。
沒有「SKIP LOCKED」或沒有搖搖欲墜的公眾是競爭和封鎖。
消費者的平均水平不足是雙倍和副作用。
在DLQ/Logs中不偽裝的PII混合。
一個沒有公平的全球出版隊列-「嘈雜」的tenant阻礙了所有人。
缺乏滯後監測→潛在退化。
17)快速選擇策略
起始級別:來自DB的投擲,100-500的蹦床,全擊球後衛,消費者的收件箱。
高負荷:CDC來自交易日誌,在「tenant_id/aggregate_id」上搖搖欲墜,按租戶劃分WFQ。
嚴格按集合順序排列:按鍵(mutex)連續發布,按鍵分期。
合規性/PII: payload加密,DLQ中的修訂版,區域outbox。
18)售前支票清單
- 「outbox」中的域更改和寫入發生在單個事務中。
- Pablisher處理蹦床,使用「SKIP LOCKED」,帶有噴射器和極限的背景。
- Consumers是偶然的(表'inbox'/dedup雜誌)。
- 已配置DLQ和安全重新分區。
- p95/p99閾值上的滯後/錯誤和差值度量。
- 鑰匙順序得到保證(批次/序列)。
- 存檔/重新制作「outbox」並清除已發布的記錄。
- PII策略和狀態轉換審核。
- Commit和出版物之間的下降測試,重復和順序。
- 事件合同文檔(模式/版本/兼容性)。
結論
Outbox模式將DB ↔ shin的「脆弱」韌帶轉變為可靠的輸送機:保證的原子狀態固定(盡管「至少一次」)出版物,偶然的訂閱者和受控的稀土。通過正確的遙測,限制和電路學科,它可以產生實際的異常行為,降低分布式交易的復雜性,並增強系統對故障和峰值負載的抵抗力。