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的"脆弱"韧带转变为可靠的输送机:保证的原子状态固定(尽管"至少一次")出版物,偶然的订阅者和受控的稀土。通过正确的遥测,限制和电路学科,它可以产生实际的异常行为,降低分布式交易的复杂性,并增强系统对故障和峰值负载的抵抗力。