分布式系统中的双写问题
Source: Dev.to
(请提供您想要翻译的具体文本内容,我将为您翻译成简体中文,并保持原有的格式、Markdown 语法以及技术术语不变。)
Overview
双写问题发生在单个逻辑操作必须更新两个(或更多)独立系统时——例如,将数据持久化到数据库 并且 将事件发布到像 Kafka 这样的消息中间件。由于这些系统不共享事务协调器,实现原子“全有或全无”行为极其困难。
分布式事务协议
- 基于共识的协议(Paxos、Raft)为状态机提供强一致性,广泛用于分布式数据库和配置存储。
- TrueTime + Paxos(Google Spanner)提供全局 ACID 保证。
- 在典型的微服务架构中,这类重量级协议很少被采用,导致双写挑战。
示例场景
假设一个 用户服务 需要:
BEGIN;
INSERT INTO users (id, name) VALUES (...);
-- send a "UserCreated" event to Kafka
COMMIT;
如果数据库插入成功而 Kafka 发布失败(或反之),系统将处于不一致状态——一侧反映了更改,而另一侧没有。这说明了双写问题。
核心问题
- 没有 global transaction manager 协调这两个操作。
- 网络故障、进程崩溃或 retries 可能导致 partial updates。
- Retries 可能产生 duplicate events 或 out‑of‑order processing。
后果
| 失败模式 | 结果 |
|---|---|
| 数据库写入成功,事件未发出 | 下游服务永远不会得知新实体的存在。 |
| 事件已发出,数据库写入失败 | 消费者会对不存在的数据进行操作。 |
| 部分重试 | 产生重复事件或多次数据库插入。 |
Source: …
常见解决方案 / 模式
事务性 Outbox 模式
- 在同一个事务中同时写入业务数据 以及 事件到同一数据库。
- 后台进程(或 CDC 工具)读取 “outbox” 表并将事件发布到 Kafka。
优点
- 数据库与消息之间保持强一致性。
- 当你同时控制存储和消息系统时实现简单。
缺点
- 增加运维复杂度。
- 消费者必须处理可能出现的重复事件。
更改数据捕获(CDC)
- 使用 CDC 工具(例如 Debezium、Oracle GoldenGate)监控数据库变更并自动发出事件。
优点
- 应用代码中无需双写逻辑。
- 若 CDC 流程可靠,可实现强一致性。
缺点
- 可能存在事件延迟。
- 需要稳定的模式和可靠的 CDC 基础设施。
幂等 & 可重试安全设计
- 将操作设计为幂等(可安全重复)。
- 使用唯一请求 ID 并在消费者侧进行去重。
优点
- 可在异构系统之间使用。
缺点
- 仍需仔细设计;不能解决顺序问题。
Source: …
事务性 Outbox 解决方案(详细示例)
模式
-- Business table
CREATE TABLE orders (
id UUID PRIMARY KEY,
customer_id UUID NOT NULL,
total NUMERIC NOT NULL,
created_at TIMESTAMP DEFAULT now()
);
-- Outbox table
CREATE TABLE outbox (
id UUID PRIMARY KEY,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMP DEFAULT now(),
published BOOLEAN DEFAULT FALSE
);
应用事务(原子写入)
import uuid, json
import psycopg2
conn = psycopg2.connect(...)
order_id = uuid.uuid4()
event = {
"event_id": str(uuid.uuid4()),
"type": "OrderCreated",
"order_id": str(order_id)
}
try:
with conn.cursor() as cur:
# Insert business data
cur.execute(
"INSERT INTO orders (id, customer_id, total) VALUES (%s, %s, %s)",
(order_id, "some-customer-id", 123.45)
)
# Insert outbox event
cur.execute(
"""
INSERT INTO outbox (id, event_type, payload)
VALUES (%s, %s, %s)
""",
(event["event_id"], event["type"], json.dumps(event))
)
conn.commit() # ✅ both rows are persisted atomically
except Exception as e:
conn.rollback()
raise
Outbox 处理器(异步发布者)
一个轻量级工作者(或 CDC 工具)会持续扫描 outbox,查找 published = FALSE 的行。对每一行:
- 将
payload发布到 Kafka。 - 将该行标记为
published = TRUE。
如果工作者在发布过程中崩溃,事件仍保持未标记状态,将会被重新尝试,从而保证至少一次投递且不丢失一致性。
目标: 防止在向数据库写入数据并向消息中间件发布事件时出现不一致,解决双写问题。