3. 分布式事务进阶:三阶段提交(3PC)、TCC模式、Saga模式、本地消息表
在分布式系统中,两阶段提交(2PC)虽然提供了强一致性保证,但其同步阻塞、单点故障和脑裂风险在高并发场景下逐渐成为瓶颈。本章深入剖析三阶段提交(3PC)、TCC模式、Saga模式与本地消息表四种进阶方案,从设计原理、适用场景到可观测性埋点策略,帮助你在连接池水位巡检与OpenTelemetry集成中精准定位事务异常。
3.1 三阶段提交(3PC)—— 引入超时与预提交
3PC是2PC的改进版本,将2PC的“提交请求-提交”两阶段拆分为“CanCommit、PreCommit、DoCommit”三个阶段,并在协调者和参与者中均引入超时机制,降低阻塞范围。
核心改进点: 参与者拥有超时后的自动中止能力,避免2PC中参与者无限等待协调者恢复的“悬挂”问题。
| 对比维度 | 2PC | 3PC |
|---|---|---|
| 阶段数 | 2(准备+提交) | 3(CanCommit+PreCommit+DoCommit) |
| 阻塞点 | 参与者准备后阻塞等待 | PreCommit后参与者可超时中止 |
| 脑裂风险 | 协调者故障时参与者无法决策 | 参与者超时后自行中止,降低脑裂 |
| 网络开销 | 2轮交互 | 3轮交互,延迟更高 |
| 一致性强度 | 强一致性 | 最终一致性(可能部分中止) |
运维警示: 3PC在PreCommit阶段后如果网络分区,部分参与者可能提交而部分中止,导致数据不一致。在连接池水位巡检中,需监控PreCommit阶段的超时次数和参与者响应分布。
3.2 TCC模式——Try-Confirm-Cancel
TCC模式是一种补偿型事务方案,将每个服务操作拆分为Try(预留资源)、Confirm(确认执行)、Cancel(取消回滚)三个阶段。业务方需要自行实现正向和反向操作,适用于短事务、高吞吐场景。
最佳实践: TCC的Try阶段应尽量轻量,只做资源预留而非实际提交;Confirm和Cancel必须幂等,以应对网络重试。
TCC与2PC/3PC的对比
| 特性 | 2PC/3PC | TCC |
|---|---|---|
| 资源锁定 | 数据库层面锁定(XA) | 业务层面预留(如冻结库存) |
| 性能 | 低(锁竞争+多轮交互) | 高(Try轻量,无长锁) |
| 侵入性 | 低(中间件代理) | 高(需业务实现Try/Confirm/Cancel) |
| 一致性 | 强一致性 | 最终一致性 |
| 典型场景 | 跨库转账、金融核心 | 订单支付、库存扣减 |
可观测性埋点建议: 在TCC的Try、Confirm、Cancel三个入口和出口分别记录Span,标记事务ID、服务名、资源类型、耗时和结果。连接池水位巡检中,若Try阶段连接池占用率持续高于70%,需排查资源预留是否未及时释放。
3.3 Saga模式——长事务的补偿艺术
Saga模式将一个大事务拆分为多个本地事务,每个本地事务对应一个补偿操作。Saga有两种执行方式:Choreography(编排)和Orchestration(编排器)。前者通过事件驱动,后者由中央协调器控制流程。
选型建议: 服务数量少(≤5)且流程固定时用Choreography;服务多、流程复杂或需要统一监控时用Orchestration。
Saga与TCC的核心差异
| 维度 | TCC | Saga |
|---|---|---|
| 资源预留 | Try阶段预留资源 | 不预留,直接提交本地事务 |
| 补偿时机 | Cancel阶段释放预留 | 失败后执行反向补偿操作 |
| 隔离性 | Try阶段提供一定隔离 | 无隔离,需业务层处理脏读 |
| 实现复杂度 | 高(需三阶段逻辑) | 中(需正向+补偿逻辑) |
| 典型场景 | 短事务、高并发扣减 | 长事务、跨多服务流程 |
连接池水位风险: Saga的补偿操作可能因网络延迟或服务故障而重试多次,导致数据库连接池被补偿事务占满。建议在补偿操作中设置独立的连接池隔离池,并监控补偿事务的并发数。
3.4 本地消息表——最终一致性的经典实现
本地消息表将分布式事务拆解为“本地事务+消息队列”的组合:业务操作和消息写入在同一个本地事务中完成,消息由异步任务确保最终投递到下游服务。该方案无XA开销,适用于对实时性要求不高的场景。
核心原理: 消息表与业务表在同一个数据库中,利用本地事务保证“业务操作”和“消息记录”的原子性;后台定时任务扫描未投递消息并重试。
本地消息表执行步骤
- 开启本地事务: 在服务A中开启数据库事务。
- 执行业务操作: 更新业务表(如订单状态)。
- 插入消息记录: 在本地消息表中插入一条状态为“待投递”的消息。
- 提交本地事务: 事务提交后,业务数据和消息同时持久化。
- 异步投递消息: 后台定时任务或消息队列客户端读取“待投递”消息,发送到MQ。
- 下游消费: 服务B消费消息并执行本地操作,成功后回调确认消息状态为“已投递”。
- 失败重试: 投递失败或下游处理失败时,消息保持“待投递”状态,定时任务重试。
本地消息表结构示例
CREATE TABLE local_message (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
business_id VARCHAR(64) NOT NULL COMMENT '业务ID',
message_body TEXT NOT NULL COMMENT '消息体(JSON)',
status TINYINT NOT NULL DEFAULT 0 COMMENT '0:待投递 1:已投递 2:已消费',
retry_count INT NOT NULL DEFAULT 0 COMMENT '重试次数',
max_retry INT NOT NULL DEFAULT 5 COMMENT '最大重试次数',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_status_retry (status, retry_count)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
四种方案综合对比
| 方案 | 一致性 | 隔离性 | 性能 | 侵入性 | 典型场景 |
|---|---|---|---|---|---|
| 3PC | 强(可能降级) | 高 | 低 | 低 | 跨库强一致 |
| TCC | 最终 | 中(Try预留) | 高 | 高 | 高并发扣减 |
| Saga | 最终 | 低(无隔离) | 高 | 中 | 长事务流程 |
| 本地消息表 | 最终 | 低 | 高 | 中 | 异步解耦 |
可观测性集成要点: 在本地消息表的定时任务扫描逻辑中,记录每次扫描的消息数量、投递成功/失败次数、重试分布。结合OpenTelemetry,为每条消息生成独立的Span,追踪从写入到消费的全链路耗时,帮助定位消息堆积或投递延迟。