2pc/3pc理论或原理
在分布式系统中,2PC(两阶段提交)和 3PC(三阶段提交)是保证多个节点在执行事务时达成一致性的核心协议。简单来说,它们就是为了解决“要么大家一起成功,要么大家一起失败”的问题。
一、 2PC (Two-Phase Commit) 二阶段提交
2PC 是最基础的原子提交协议,将事务的处理过程分为两个阶段:投票阶段和执行阶段。
1. 运行流程
- 阶段一:表决阶段 (Prepare Phase)
- 协调者(Coordinator)向所有参与者(Participants)发送准备请求。
- 参与者执行事务操作,将 Undo/Redo 日志写入磁盘,但不提交。
- 参与者向协调者回复
Yes(准备就绪)或No(失败)。
- 阶段二:执行阶段 (Commit Phase)
- 如果所有人都说 Yes:协调者发送
Commit通知,参与者正式提交并释放锁。 - 如果有人说 No 或超时:协调者发送
Rollback通知,参与者利用 Undo 日志回滚。
- 如果所有人都说 Yes:协调者发送
2. 核心问题
- 同步阻塞:所有参与者在等待协调者指令时都处于阻塞状态,占用资源。
- 单点故障:如果协调者在第二阶段宕机,参与者会一直锁定资源,导致整个系统“瘫痪”。
- 数据不一致:如果在 Commit 阶段,只有部分参与者收到了指令(由于网络分区),就会导致数据状态不一。
二、 3PC (Three-Phase Commit) 三阶段提交
为了解决 2PC 的痛点(尤其是阻塞和单点故障),3PC 引入了超时机制并增加了一个预提交阶段。
1. 运行流程
- 阶段一:CanCommit
- 协调者询问参与者:你们环境正常吗?能执行这个事务吗?(此时不执行具体事务逻辑,只做检查)。
- 阶段二:PreCommit
- 协调者根据反馈发送预执行指令。参与者执行事务操作,记录日志,但仍不真正提交。
- 阶段三:DoCommit
- 真正的提交阶段。
2. 3PC 的改进点
- 引入超时机制:如果参与者在 PreCommit 后没等到协调者的指令,它会自动提交。这解决了 2PC 永久阻塞的问题。
- 降低阻塞概率:第一阶段的询问确保了大家都有成功的可能,减少了后续资源浪费。
3. 仍然存在的问题
- 数据不一致风险:虽然解决了阻塞,但如果网络出现分区,协调者发出了回滚指令,而某个参与者因为超时而自动提交了,数据依然会乱套。
三、 2PC vs 3PC 核心对比
| 特性 | 2PC | 3PC |
|---|---|---|
| 阶段数 | 2 阶段 | 3 阶段 |
| 核心机制 | 阻塞式请求 | 引入超时机制 + 中间预备状态 |
| 协调者故障 | 可能导致参与者永久阻塞 | 参与者可通过超时机制自动提交 |
| 数据一致性 | 较好,但在网络分区时有风险 | 牺牲了一定的一致性来换取可用性 |
| 性能 | 延迟较低 | 延迟较高(多一轮网络交互) |
一句话总结:2PC 简洁但容易卡死;3PC 通过增加步骤和超时虽然防卡死,但增加了复杂度且依然无法完美解决网络分区下的数据一致性。
seata如何解决的
Seata 是阿里开源的一个分布式事务解决方案,它最核心的突破在于对 2PC(两阶段提交) 进行了深度改良。
针对传统 2PC 的高并发性能差和资源锁定时间长的问题,Seata 提供了四种模式:AT、TCC、Saga 和 XA。其中,AT 模式(Automatic Transaction) 是最常用、最具代表性的方案。
一、 Seata AT 模式:对 2PC 的优化
Seata AT 模式将 2PC 的过程通过 RM(资源管理器) 和 TC(事务协调者) 进行了自动化管理,核心逻辑如下:
1. 阶段一:业务提交(本地事务执行)
在传统的 2PC 中,第一阶段不提交事务;而在 Seata AT 中,第一阶段直接提交本地事务。
- 解析 SQL:Seata 会拦截业务 SQL。
- 记录回滚日志(Undo Log):在提交本地事务前,Seata 会查询数据的“前镜像”(Before Image)和“后镜像”(After Image),保存到
undo_log表中。 - 注册全局锁:向 TC 申请该行记录的全局锁。
- 本地提交:业务数据和
undo_log一起在一个本地事务中提交。 - 优势:立即释放本地数据库连接和锁,极大提高了并发能力。
2. 阶段二:异步处理
- 如果全局提交:TC 通知 RM。RM 只需要异步删除对应的
undo_log记录即可,速度极快。 - 如果全局回滚:RM 收到回滚请求,提取
undo_log中的前镜像数据,通过反向 SQL 补偿数据,实现回滚。
二、 Seata 解决了 2PC 的哪些痛点?
| 痛点 | Seata AT 的解决方案 |
|---|---|
| 同步阻塞 | 异步提交。第一阶段就释放了本地事务锁,不需要像 2PC 那样等待全局决定。 |
| 单点故障 | TC 集群化。Seata 的协调者(TC)支持高可用部署,不再是单点。 |
| 运维成本 | 无侵入。开发者只需关注业务 SQL,Seata 自动生成回滚逻辑(对比 TCC 手写 Confirm/Cancel)。 |
| 数据孤岛 | 全局锁管理。通过 TC 维护全局行锁,防止不同全局事务之间的脏写。 |
三、 Seata 的其他模式(针对不同场景)
为了应对 2PC 无法覆盖的复杂场景,Seata 提供了多样化武器库:
- TCC 模式:
- 原理:手动实现
Try-Confirm-Cancel。 - 解决:非关系型数据库、或需要跨行、跨系统的自定义补偿逻辑(如库存预留)。
- 原理:手动实现
- Saga 模式:
- 原理:长事务解决方案,每个步骤有对应的补偿操作。
- 解决:业务流程长、涉及第三方服务的场景(无法回滚对方数据库)。
- XA 模式:
- 原理:原生支持数据库的 XA 协议。
- 解决:对强一致性要求极高,且不希望有
undo_log表侵入的场景。
四、 总结:核心区别
Seata 的本质是将“重”的协议(2PC)拆解为“轻”的本地事务 + “异步”的补偿机制。它牺牲了极短时间内的强一致性(变为最终一致性),换取了互联网场景下至关重要的高性能。
TCC
如果说 Seata 的 AT 模式是“自动挡”,那么 TCC (Try-Confirm-Cancel) 就是“手动挡”。它不依赖于数据库的撤销日志(Undo Log),而是将分布式事务的控制权完全交给了业务代码。
一、 TCC 的三个阶段
TCC 要求业务方针对每一个操作,都要实现三个方法:
1. Try(预留业务资源)
- 核心任务:完成所有业务检查(一致性),并预留必须的业务资源。
- 例子:在扣减 100 元余额时,Try 阶段不是直接减掉 100 元,而是冻结 100 元。
- 状态:此时订单处于“处理中”状态。
2. Confirm(确认执行业务)
- 核心任务:真正执行业务。只使用 Try 阶段预留的资源。
- 特点:只要 Try 成功,Confirm 理论上一定会成功。如果失败,TC 会不断重试。
- 例子:将冻结的 100 元正式划转走。
3. Cancel(取消执行业务)
- 核心任务:释放 Try 阶段预留的业务资源。
- 特点:如果 Try 阶段失败或超时,或者其他参与者失败,则执行 Cancel 进行回滚。
- 例子:解冻那 100 元。
二、 TCC 解决的 2PC 核心痛点
- 性能极高:它在 Try 阶段只预留资源,不锁定数据库行。这意味着不同的事务可以并发地预留各自的资源,不会发生严重的行锁竞争。
- 跨数据库/服务:2PC 和 AT 模式通常要求数据库支持(如 MySQL),而 TCC 纯粹是业务层面的逻辑,你可以跨 Redis、MongoDB 甚至是调用第三方 API 进行分布式事务。
- 最终一致性:它通过不断重试 Confirm/Cancel,确保系统最终达到一致状态。
三、 TCC 的“三大魔咒”(必须解决的设计问题)
使用 TCC 必须手动处理以下三个异常情况,否则会产生脏数据:
| 异常情况 | 定义 | 解决方案 |
|---|---|---|
| 空回滚 (Empty Rollback) | Try 方法还没执行(比如网络丢包),就收到了 Cancel 请求。 | Cancel 时需判断 Try 是否执行过,若没执行,直接返回成功。 |
| 幂等 (Idempotence) | 网络抖动导致 Confirm 或 Cancel 被多次调用。 | 记录事务状态控制表,确保每个方法只生效一次。 |
| 悬挂 (Hanging) | Cancel 比 Try 先到了(网络延迟),导致 Try 后到并成功预留了资源,但再也没人来处理它。 | Try 执行前先检查此事务是否已经 Cancel 过,若是则不执行。 |
四、 AT vs TCC:该选哪一个?
| 特性 | AT 模式 | TCC 模式 |
|---|---|---|
| 开发难度 | 极低(几乎零代码侵入) | 高(每个接口要写 3 个方法) |
| 性能 | 较高(有全局锁和本地事务损耗) | 极高(完全自定义,无锁设计) |
| 一致性 | 弱强一致性 | 最终一致性 |
| 适用场景 | 关系型数据库、中等并发 | 高并发、跨非关系型数据库、金融级核心链路 |
简单一句话总结: 如果你的业务对并发量要求极高(如双十一秒杀),或者涉及非关系型数据库,请选 TCC;普通的内部管理系统或 O2O 业务,选 AT 能让你少加很多班。
TCC代码实现 Demo,或者了解 Seata 是如何自动处理“空回滚”和“悬挂”
要用好 TCC,理解 Seata 如何在底层帮你处理“空回滚”和“悬挂”是关键,因为这些都是分布式环境下网络抖动必然会带来的挑战。
一、 Seata 如何自动处理异常?
在 Seata 的 TCC 模式中,它通过一张事务状态控制表(tcc_fence_log)来自动管理状态,开发者只需要开启 @TwoPhaseBusinessAction 注解即可。
1. 空回滚 (Empty Rollback)
- 场景:由于网络原因,
Try请求丢失了,但分布式事务已经决定回滚,于是向参与者发送了Cancel。 - 解决:Seata 在执行
Cancel前,会先去tcc_fence_log表查询是否有该事务的Try记录。如果没有,说明Try没成功,Seata 会直接插入一条状态为ROLLBACKED的记录,并返回成功,这就是“空回滚”处理。
2. 悬挂 (Hanging)
- 场景:
Cancel请求比Try先到达(网络拥堵导致Try迟到)。如果先执行了Cancel,后续迟到的Try到达并成功预留了资源,由于事务已经回滚,这部分资源将永远无法释放。 - 解决:当
Try请求到达时,Seata 先去tcc_fence_log检查。如果发现已经有了该事务的ROLLBACKED记录(说明Cancel已经跑过了),Try方法将拒绝执行,直接返回失败。
二、 TCC 代码实现 Demo (伪代码)
假设我们有一个积分扣减的业务,我们需要定义一个 TCC 接口:
1. 定义接口
@LocalTCC
public interface PointService {
/**
* Try: 冻结积分
* @TwoPhaseBusinessAction 声明 TCC 模式,并指定 Confirm 和 Cancel 的方法名
*/
@TwoPhaseBusinessAction(name = "deductPoints", commitMethod = "confirm", rollbackMethod = "cancel")
boolean prepareDeduct(BusinessActionContext context, @BusinessActionContextParameter(paramName = "userId") String userId, int points);
boolean confirm(BusinessActionContext context);
boolean cancel(BusinessActionContext context);
}
2. 业务逻辑实现
public class PointServiceImpl implements PointService {
@Override
public boolean prepareDeduct(BusinessActionContext context, String userId, int points) {
// 1. 检查余额
// 2. 扣减可用积分,增加冻结积分 (SQL: update account set points = points - 100, frozen_points = frozen_points + 100 where user_id = ?)
return true;
}
@Override
public boolean confirm(BusinessActionContext context) {
// 1. 从 context 获取参数
String userId = context.getActionContext("userId").toString();
// 2. 正式扣除冻结积分 (SQL: update account set frozen_points = frozen_points - 100 where user_id = ?)
return true;
}
@Override
public boolean cancel(BusinessActionContext context) {
// 1. 恢复可用积分,减少冻结积分 (SQL: update account set points = points + 100, frozen_points = frozen_points - 100 where user_id = ?)
return true;
}
}
三、 避坑指南:设计 TCC 时的核心原则
- 并发可见性:
Try阶段必须确保资源被独占。例如余额扣减,一定要有“冻结字段”,否则在Confirm前,这部分钱可能会被用户通过其他非 TCC 接口花掉。 - 数据轻量化:
Confirm和Cancel方法中,尽量只操作Try阶段预留的数据,不要再去做复杂的业务校验。 - Confirm/Cancel 必须幂等:虽然 Seata 帮你做了不少工作,但在手动编写 SQL 时,仍建议使用
update ... where status = 'TRYING'这种自带状态检查的语句。
写订单系统的 TCC 事务,最核心的设计思想是“预留资源”。
在订单场景中,通常涉及订单状态(待支付/已支付)、库存(锁定/扣减)和资金(冻结/扣划)。下面我以一个典型的“创建订单并扣减库存”为例,为你展示伪代码实现。
一、 核心设计:库存表结构
为了支持 TCC,库存表通常需要两个字段:
available_qty: 可用库存frozen_qty: 冻结库存(用于 TCC 预留)
二、 TCC 接口定义
使用 Seata 的 @TwoPhaseBusinessAction 注解来定义三个阶段的方法。
@LocalTCC
public interface StockService {
/**
* Try: 锁定库存
* @param actionContext 事务上下文,用于传递参数到第二阶段
* @param productId 产品ID
* @param count 购买数量
*/
@TwoPhaseBusinessAction(name = "deductStock", commitMethod = "confirm", rollbackMethod = "cancel")
boolean prepareDeduct(BusinessActionContext actionContext,
@BusinessActionContextParameter(paramName = "productId") Long productId,
@BusinessActionContextParameter(paramName = "count") Integer count);
/**
* Confirm: 真正扣减锁定库存
*/
boolean confirm(BusinessActionContext actionContext);
/**
* Cancel: 释放锁定库存
*/
boolean cancel(BusinessActionContext actionContext);
}
三、 详细业务实现
1. Try 阶段 (锁定资源)
核心逻辑:检查余额 \(\rightarrow\) 扣减可用库存 \(\rightarrow\) 增加冻结库存。
public boolean prepareDeduct(BusinessActionContext actionContext, Long productId, Integer count) {
// SQL: UPDATE stock_tab SET available_qty = available_qty - ?, frozen_qty = frozen_qty + ?
// WHERE product_id = ? AND available_qty >= ?
int affectedRows = stockMapper.freezeStock(productId, count);
if (affectedRows == 0) {
throw new RuntimeException("库存不足,锁定失败");
}
return true;
}
2. Confirm 阶段 (正式扣减)
核心逻辑:减少冻结库存(因为第一阶段已经把可用库存扣过了)。
public boolean confirm(BusinessActionContext actionContext) {
Long productId = Long.valueOf(actionContext.getActionContext("productId").toString());
Integer count = Integer.valueOf(actionContext.getActionContext("count").toString());
// SQL: UPDATE stock_tab SET frozen_qty = frozen_qty - ?
// WHERE product_id = ? AND frozen_qty >= ?
// 注意:这里需要保证幂等,防止重复扣减
return stockMapper.reduceFrozenStock(productId, count) > 0;
}
3. Cancel 阶段 (回滚/释放)
核心逻辑:将冻结库存还给可用库存。
public boolean cancel(BusinessActionContext actionContext) {
Long productId = Long.valueOf(actionContext.getActionContext("productId").toString());
Integer count = Integer.valueOf(actionContext.getActionContext("count").toString());
// SQL: UPDATE stock_tab SET available_qty = available_qty + ?, frozen_qty = frozen_qty - ?
// WHERE product_id = ? AND frozen_qty >= ?
// 自动处理“空回滚”:如果 Try 没执行成功,frozen_qty 会不够扣,返回 0 即可
return stockMapper.unfreezeStock(productId, count) > 0;
}
四、 整体流程图示
为什么这样写能解决问题?
- 高并发:在
Try阶段结束后,数据库事务就已经提交并释放了物理锁。如果有其他用户也要买这个商品,只要available_qty足够,他们可以并行修改同一行数据,而不需要互相等待。 - 数据安全:即便在
Confirm阶段宕机了,Seata 的 TC(事务协调者)会不断重试Confirm方法。由于我们写的是frozen_qty - count,所以数据最终一定会一致。 - 防止超卖:通过
available_qty >= count的 SQL 条件判断,保证了即使并发再高也不会出现负数。
在分布式系统中,当一个订单业务涉及多个微服务(如:订单、库存、账户、积分)同时执行 TCC 时,调用顺序和状态一致性就成了架构设计的核心。
通常情况下,我们采用 “同步 Try,异步 Confirm/Cancel” 的策略。
如何处理更复杂的“多个服务同时进行 TCC”时的调用顺序问题
一、 多个 TCC 服务的调用链路
在 Seata 中,通常由一个“发起者”(通常是订单服务)作为事务管理器(TM),通过 RPC 远程调用其他服务的 TCC 接口。
1. 核心流程:
- 开启全局事务:订单服务通过
@GlobalTransactional开启事务。 - 串行/并行 Try:
- 订单服务:
Try(创建预处理订单,状态为“待支付”)。 - 库存服务:
Try(锁定库存,frozen_qty + 1)。 - 账户服务:
Try(扣减余额,frozen_balance + 100)。
- 订单服务:
- TC 决策:
- 全部 Try 成功:TC 向所有服务发起
Confirm。此时订单状态改为“已完成”,库存和余额的冻结部分真正扣除。 - 任意一个 Try 失败:TC 向所有服务发起
Cancel。订单状态改为“已取消”,库存和余额原路返还。
- 全部 Try 成功:TC 向所有服务发起
二、 深度优化:如何处理复杂的调用顺序?
在复杂的电商场景中,并不是所有 Try 都能并行的。
1. 嵌套 TCC(Nested TCC)
如果 A 服务调用 B 服务,B 服务又调用 C 服务,且三者都是 TCC 模式:
- 原则:父服务的
Try必须包含子服务的Try。 - 风险:调用链路越长,第一阶段占用的系统资源(如 HTTP 连接、RPC 线程)就越多。
- Seata 支持:Seata 自动支持这种嵌套结构,它会记录整个调用树,确保
Confirm/Cancel按照正确的反向顺序执行。
2. 混合模式(AT + TCC)
实际生产中,有些老系统不支持 TCC,你可以混合使用:
- 库存(高并发):用 TCC 避免长时间行锁。
- 订单/日志(并发低):用 AT 减少代码开发量。
- Seata 优势:Seata 支持在一个全局事务中混合不同模式,TC 会统一协调它们的提交与回滚。
三、 一个致命陷阱:Try 阶段的“过度预留”
在设计调用顺序时,一定要注意资源释放的原子性。
反面教材:
如果你在Try阶段直接扣了钱(没用冻结字段),由于Confirm是异步的,万一Confirm还没执行时用户去查余额,会发现钱少了但订单还没成功。
正确做法:
永远在Try阶段保持“中间态”,让用户在 UI 上看到“处理中”或“冻结中”,这才是真正的最终一致性体验。
四、 总结:TCC 落地三部曲
- 原子性设计:数据库表必须增加“冻结/预留”相关字段。
- 防御性编码:必须处理空回滚、幂等、悬挂(利用 Seata 的
tcc_fence_log)。 - 配置优化:
Confirm和Cancel必须尽量简单,不包含耗时的外部调用。- 设置合理的超时时间,防止 TC 频繁重试导致系统雪崩。
这是一份基于 Spring Cloud (Nacos/Eureka) + Feign + Seata 2.x 的 TCC 模式落地配置模板。
我们将配置分为三部分:依赖管理、关键配置 (YAML) 和 数据库表准备。
基于 Spring Cloud + Seata 的完整配置模板
一、 核心依赖 (Maven)
确保微服务项目中引入了 Seata 客户端。建议版本保持一致,避免协议兼容性问题。
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
二、 关键配置 (application.yml)
每个参与 TCC 事务的微服务都需要如下配置,重点在于 tx-service-group 的映射。
seata:
enabled: true
application-id: ${spring.application.name}
# 事务分组:必须与 Seata Server 端配置的集群名对应
tx-service-group: my_test_tx_group
service:
vgroup-mapping:
my_test_tx_group: default # 映射到 Seata Server 的 default 集群
registry:
type: nacos # 注册中心类型
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
namespace: ""
config:
type: nacos
tcc:
# 开启 TCC 防悬挂/空回滚日志记录(关键!)
fence:
enabled: true
clean-period: 1h # 定期清理已完成的事务记录
三、 数据库表准备 (TCC 控制表)
这是防止“空回滚”和“悬挂”的核心。Seata 会在执行 Try、Confirm、Cancel 时自动操作这张表。你需要在每个业务数据库(如订单库、库存库)中创建它。
-- TCC 事务控制日志表(Seata 2.0+ 标准结构)
CREATE TABLE IF NOT EXISTS `tcc_fence_log`
(
`xid` VARCHAR(128) NOT NULL COMMENT 'global id',
`branch_id` BIGINT NOT NULL COMMENT 'branch id',
`action_name` VARCHAR(64) NOT NULL COMMENT 'action name',
`status` TINYINT NOT NULL COMMENT 'status(tried:1;committed:2;rollbacked:3;suspended:4)',
`gmt_create` DATETIME(3) NOT NULL COMMENT 'create time',
`gmt_modified` DATETIME(3) NOT NULL COMMENT 'modified time',
PRIMARY KEY (`xid`, `branch_id`),
KEY `idx_gmt_modified` (`gmt_modified`),
KEY `idx_status` (`status`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
四、 核心启动逻辑
1. 业务调用方 (TM)
在 Controller 或 Service 的入口方法上开启全局事务:
@GlobalTransactional(name = "create-order-tx", rollbackFor = Exception.class)
public void createOrder(OrderDTO order) {
// 1. 调用库存服务 TCC (Feign)
stockClient.prepareDeduct(order.getProductId(), order.getCount());
// 2. 调用账户服务 TCC (Feign)
accountClient.prepareReduce(order.getUserId(), order.getAmount());
// 3. 订单自身逻辑...
}
2. 参与者服务 (RM)
在实现类上使用 @LocalTCC 并在方法上使用 @TwoPhaseBusinessAction(如之前伪代码所示)。
五、 落地小贴士
- 关于
undo_log表:AT 模式需要undo_log表,而 TCC 模式不需要undo_log,它只需要tcc_fence_log。 - 隔离性:TCC 在第一阶段会释放数据库锁,为了保证隔离性(防止脏写),建议在业务上通过状态机或冻结字段来控制数据权限。
- 重试机制:由于
Confirm和Cancel是异步重试的,里面的逻辑必须是幂等的。即使 Seata 帮你管了tcc_fence_log,你的 SQL 语句最好也带上状态判断(如where status = 'FREEZED')。
AT和TCC模式能混合用
。Seata 支持在同一个全局事务(Global Transaction)中混合使用 AT 模式和 TCC 模式。
Seata 的设计初衷就是为了解决复杂的分布式场景,它通过 事务协调者(TC) 统一管理不同类型的 分支事务(Branch Transaction)。
一、 为什么需要混合使用?
在真实的微服务架构中,不同的服务往往有不同的业务特性和技术背景:
- 订单/日志服务(适合 AT):逻辑简单,并发量中等,希望代码无侵入,直接用
@GlobalTransactional配合 AT 模式最省心。 - 库存/支付服务(适合 TCC):并发极高,或者需要操作非关系型数据库(如 Redis、MongoDB),或者需要调用第三方接口(如支付宝、微信支付),此时必须手动实现 TCC。
- 遗留系统:有些服务已经是写好的 TCC 接口,而新服务想用更简单的 AT 模式。
二、 混合模式的工作原理
Seata 的 TC 会根据每个分支事务注册时的 类型(BranchType) 来决定如何调度:
- 全局开启:入口方法标注
@GlobalTransactional。 - 分支注册:
- 调用 AT 服务时,RM(资源管理器)自动向 TC 注册一个
AT类型的分支。 - 调用 TCC 服务时,RM 向 TC 注册一个
TCC类型的分支。
- 调用 AT 服务时,RM(资源管理器)自动向 TC 注册一个
- 二阶段提交/回滚:
- 如果全局提交:TC 通知 AT 分支异步删除
undo_log;通知 TCC 分支执行Confirm方法。 - 如果全局回滚:TC 通知 AT 分支根据
undo_log反向补偿;通知 TCC 分支执行Cancel方法。
- 如果全局提交:TC 通知 AT 分支异步删除
三、 混合模式的代码示例(伪代码)
假设一个下单流程:订单用 AT,库存用 TCC。
// 1. 业务发起方(TM)
@Service
public class OrderBusiness {
@GlobalTransactional // 开启全局事务
public void createOrder(String userId, String productId, int count) {
// AT 模式:订单服务直接操作数据库,Seata 自动拦截并记录 undo_log
orderDAO.insert(new Order(userId, productId, count));
// TCC 模式:调用库存服务提供的 Try 接口(prepareDeduct)
// 内部会进行库存冻结:available_qty - count, frozen_qty + count
stockTccClient.prepareDeduct(null, productId, count);
}
}
四、 混合使用的注意事项(避坑指南)
虽然技术上可行,但在落地时要注意以下三点:
1. 事务隔离性差异
- AT 模式:通过全局锁(Global Lock)保证隔离。
- TCC 模式:隔离性完全由你的业务代码(如“冻结字段”)保证。
- 风险:如果 TCC 阶段没有做好资源预留(冻结),而 AT 阶段又依赖这个资源,可能会产生数据冲突。
2. 性能瓶颈
全局事务的整体性能取决于最慢的那个分支。如果 TCC 模式做得很快,但 AT 模式因为数据库大表锁竞争导致二阶段提交变慢,整个全局事务的响应时间都会拉长。
3. 配置统一
确保所有参与混合事务的微服务都连接到同一个 Seata Server (TC) 集群,并且 tx-service-group(事务分组) 映射配置正确。
五、 总结对比
| 维度 | AT 模式分支 | TCC 模式分支 |
|---|---|---|
| 数据一致性 | 基于数据库镜像,回滚更准确 | 基于业务补偿,逻辑更灵活 |
| 代码侵入 | 几乎零侵入 | 较高(需写 3 个方法) |
| 回滚机制 | 自动生成 SQL | 手写逻辑 |
| 适合对象 | 关系型数据库 (MySQL/Oracle) | 任何能实现补偿逻辑的资源 (Redis/API) |
raft+kafka是否可以替代分布式事务
这是一个非常硬核且深刻的问题。简单来说:Kafka(基于 Raft 的新架构)和 Seata 解决的是完全不同维度的一致性问题,它们之间无法直接替代,但可以互补。
要理解为什么不能替代,我们需要区分 “组件内部的高可用(Consensus)” 与 “业务跨服务的一致性(Distributed Transaction)”。
一、 核心区别:Raft 解决什么?Seata 解决什么?
1. Raft + Kafka (组件级:共识算法)
Kafka 现在的 KRaft 模式是用 Raft 协议替代了 ZooKeeper。
- 目标:解决 元数据的一致性 和 消息的不丢失。
- 场景:当 Kafka 集群中的一个 Broker 挂了,通过 Raft 协议快速选出新的 Leader,保证集群自己“不乱阵脚”,数据在多副本间是一致的。
- 范围:它只管 Kafka 内部的数据。
2. Seata (业务级:分布式事务)
Seata 解决的是 跨库、跨服务 的原子性。
- 目标:解决 业务操作的原子性。
- 场景:下单成功(订单库更新)+ 扣款成功(账户服务)+ 库存减一(库存服务)。
- 范围:它横跨了多个不同的数据库和微服务。
二、 为什么 Kafka 不能替代 Seata?
如果你想用 Kafka 来实现分布式事务(即著名的“可靠消息最终一致性”方案),你会面临以下挑战:
1. 业务侵入性极高
使用 Kafka 做事务,你通常需要实现 本地消息表。
- 你必须在一个本地事务中:更新业务数据 + 插入一条消息到消息表。
- 然后由一个后台任务异步发送到 Kafka。
- 对比 Seata AT:Seata 只需要一个注解
@GlobalTransactional,代码几乎不用动。
2. 回滚极其困难
- Seata:支持自动回滚(AT 模式)或业务回滚(TCC 模式)。
- Kafka:消息一旦发出并被消费,产生的影响(如扣费)很难“撤回”。你必须手动写一套逆向业务逻辑(补偿机制)来处理失败,本质上这变成了手动实现的 Saga 模式,开发成本巨大。
3. 实时性问题
Kafka 方案是最终一致性。这意味着在消息堆积时,用户可能已经下单成功了,但过了 5 分钟库存才扣减。
- 而在金融、库存预扣等场景,往往需要 TCC 这种准实时的强一致性感知,这正是 Seata 的强项。
三、 Raft + Kafka + Seata 的协作关系
在高级架构中,它们其实是协同工作的:
- 存储层(Raft):Seata Server (TC) 如果部署为高可用模式,它内部的数据库(如 MySQL 集群)或者 Seata 的某些内部组件可能会利用 Raft 保证自己不挂。
- 传输层(Kafka):业务系统可以使用 Kafka 发送审计日志、大数据分析。
- 业务层(Seata):负责订单、支付、库存之间那几个核心数据库操作的“同生共死”。
四、 总结:什么时候选谁?
| 需求场景 | 推荐方案 |
|---|---|
| 集群内部节点选主、日志同步 | Raft (Kafka 内部已集成) |
| 多个微服务、多个数据库的强原子性操作 | Seata (AT/TCC) |
| 解耦服务、允许延迟的最终一致性 | Kafka (事务消息) |
| 长流程、涉及第三方系统的事务 | Seata (Saga) |
一句话总结: Kafka 是解决“消息怎么传不丢”,Seata 是解决“逻辑怎么执行不乱”。如果你现在面临的是多个微服务数据库不一致的问题,Raft 救不了你,还是得用 Seata。
思考 Kafka 自带的“事务消息”(Transactional Messaging)是如何工作的?它虽然不能替代 Seata,但在某些异步场景下非常强大。
传统的 2PC/3PC 性能太差,而最终一致性又无法满足某些核心金融业务。请结合 Raft 协议 或者 高性能消息队列(如 Pulsar 或新版 Kafka),如何设计一个“跨机房双活”且能保证“强一致性”的写入链路
这是一个非常硬核的架构设计问题。要实现跨机房双活(Active-Active)且保证强一致性(Strong Consistency),我们必须绕过传统 2PC 的协调者单点和同步阻塞瓶颈,利用底层协议的共识(Consensus)能力来驱动上层业务。
传统的 2PC 性能差是因为它在业务层做锁竞争;而基于 Raft 或 新版 Kafka/Pulsar 的方案是在底层日志层做顺序共识。
以下是结合 Raft 状态机同步 与 高性能消息队列 的双活强一致性架构设计方案:
一、 核心思路:从“事务管理”转向“共识日志同步”
在双活架构中,核心矛盾是网络时延(RTT)。传统的 2PC 需要多次往返,且在提交前锁定资源。
高性能方案: 将事务抽象为一条有序日志(Log),只要日志在两个机房的多数派节点上达成共识,业务就视为写入成功。
二、 方案设计:基于 Raft 的强一致性写入链路
1. 架构组件
- 全局序列号生成器(Global Sequencer):利用 Raft 协议在两地三中心部署,确保生成的事务 ID 是全局递增且唯一的。
- 分布式存储层(如 TiDB 或基于 Raft 的 KV 存储):数据库不再是单机 MySQL,而是原生支持 Raft 协议的分布式数据库。
- Leader 调度策略:通过 Raft 的 Region Leader 机制,将用户请求调度到距离其最近的 Leader 节点。
2. 写入链路流程
- 请求接入:机房 A 的应用接收写入请求。
- 日志预写(Append Log):Leader 节点将操作封装为一条 Raft Log。
- 跨机房同步:Leader 将 Log 同步给本机房的 Follower 和对端机房的 Follower。
- 多数派确认:只要本地机房的节点 + 对端机房的一个节点回执成功(满足 \(N/2 + 1\)),日志即被视为 Committed。
- 状态机应用(Apply):各节点异步将日志应用到状态机(内存/磁盘 DB),返回用户成功。
三、 方案设计:基于 Pulsar/Kafka 的事务消息链路
如果你希望解耦业务,可以使用支持 分布式事务隔离 的消息队列。
1. 跨机房同步机制
- Geo-Replication:Pulsar 原生支持跨机房同步。
- 原子写入(Atomic Writes):利用 Kafka 2.x+ 的
initTransactions和sendOffsetsToTransaction。
2. 写入链路流程
- 开启消息事务:生产者向机房 A 的 Broker 发送事务消息。
- 同步复制:Broker 设置
acks=all,要求至少一个副本必须存在于对端机房,确保跨机房数据不丢。 - 提交指令:当业务本地逻辑执行完,发送
Commit。 - 消费端保证:消费端设置
isolation.level=read_committed,确保对端机房只有在事务完全同步后才能看到数据。
四、 为什么这能超越 2PC?
| 维度 | 传统 2PC | 基于 Raft / 高性能队列的共识方案 |
|---|---|---|
| 锁定时间 | 整个事务期间锁定数据库行 | 仅在日志同步阶段有微小延迟,无业务锁 |
| 阻塞模型 | 同步阻塞,等待所有参与者响应 | 多数派准入,不要求所有节点都活着 |
| 一致性等级 | 强一致,但易因故障导致不确定状态 | 线性一致性(Linearizability) |
| 双活能力 | 很难实现(协调者跨机房性能极差) | 天然支持,Leader 在哪,哪就是写中心 |
五、 金融级双活的特殊处理(关键优化)
- 分片(Sharding)与局部性:
将用户数据按 ID 分片。A 机房用户作为分片 1 的 Raft Leader,B 机房用户作为分片 2 的 Leader。这样 90% 的请求在本机房内就能达成多数派,只有 10% 跨机房,大幅降低 RTT 损耗。 - Learner 节点:
在第三个机房部署 Learner 节点,只读不参与投票,用于灾备和数据离线分析,不影响写入性能。 - Follower Read:
利用ReadIndex或Lease Read机制,允许在从机房进行强一致性读,实现真正的“读写双活”。
实践中这种架构能否解决分布事务
简单直接的回答是:Raft/Kafka 这种底层协议能解决“数据同步一致性”,但它不能直接替代“业务逻辑原子性”。
在实践中,这种架构通常被作为分布式事务的底层基石,而不是全部。
一、 协议层 vs. 业务层:缺了什么?
要理解为什么 Raft/Kafka 不能直接解决分布式事务,我们需要看这两个场景的区别:
- Raft/Kafka 解决的是“副本一致性”: 只要 Leader 写入了
A=10,它能保证集群内所有节点最终(或强一致地)都变成A=10。 - 分布式事务解决的是“跨资源原子性”: 这是一个逻辑集合。比如
账户减100和红包加100。这两个操作可能在不同的 Raft 组(不同的数据库分片或不同的微服务)里。
Raft 只能保证单个 Raft 组内的一致性,无法原生保证跨组的操作。
二、 实践中的真实例子:分布式数据库 (如 TiDB / CockroachDB)
在高性能实践中,我们确实用 Raft 替代了传统的 2PC 锁等待,但它是这样分层的:
1. 底层:Raft 保证高可用
数据被切分成很多小块(Region)。每个 Region 是一个 Raft 组。这样解决了跨机房双活的数据安全和自动选主问题。
2. 上层:在 Raft 之上跑 2PC (优化版)
当你要做一个跨 Region(跨服务)的事务时,依然需要类似 2PC 的逻辑,但性能极高:
- 提议阶段:向 Raft 组 A 写入一个“预写(Lock)”记录,向 Raft 组 B 写入一个“预写(Lock)”记录。
- 共识达成:这两个写入操作在各自的 Raft 组内通过“多数派确认”完成。
- 决议阶段:一旦两边都写成功,事务就逻辑上提交了。
三、 为什么说这种架构“解决”了分布式事务的痛点?
虽然逻辑上还是 2PC,但有了 Raft/Kafka 架构后,性能和稳定性有了质变:
- 消除单点故障:
传统的 2PC 如果协调者宕机,整个事务锁死。而在 Raft 架构下,如果某个分片的 Leader 宕机,Raft 会在毫秒级选出新 Leader 恢复事务处理。 - 异步化性能:
Raft 允许“多数派确认”即返回。在跨机房场景下,如果你有 3 个机房(A, B, C),只要 A 和 B 确认了,就不需要等物理距离最远的 C。这解决了传统 2PC 必须等待所有节点响应的“长尾效应”。 - 日志即真理:
利用 Kafka 或 Raft 的顺序日志,我们可以实现 “确定性状态机”。只要日志顺序定了,不管节点怎么崩溃,重启后按顺序回放日志,最终状态一定是一致的。
四、 结论:实践中的选择
在工程实践中,我们不会用 Raft 替代 分布式事务,而是用 Raft 来构建高性能的分布式资源:
- 如果你追求超高性能和极致双活: 你应该选择 NewSQL 数据库(如 TiDB) 或 基于 Raft 的 KV 存储。它们内部屏蔽了 Raft 和分布式事务的复杂交互,对业务透明。
- 如果你在做微服务解耦: 你应该使用 Seata + 高可用配置。此时 Seata 的控制表(
tcc_fence_log等)可以存放在基于 Raft 的高可用数据库中。
总结: Raft 和 Kafka 提供了可靠的、有序的通道,而分布式事务是在这个通道上跑的业务逻辑。没有 Raft,分布式事务很脆弱;有了 Raft,分布式事务才能跑得又快又稳。
基于这种架构下,seata应该怎么使用
当我们将底层架构升级为基于 Raft 的共识系统(如 TiDB、CockroachDB 或高可用 Kafka/Pulsar)时,Seata 的角色从“苦力”变成了“指挥官”。
在这种架构下,Seata 不再需要担心底层数据库死锁或单点崩溃,而是专注于跨机房的业务编排。以下是实践中的最佳落地姿势:
一、 部署架构:将 Seata 挂载在共识层之上
为了配合底层的高性能,Seata 自身的存储必须也具备 Raft 级别的可靠性。
1. 存储层(Store Mode)切换为 DB
不要使用 file 模式。将 Seata 的 global_table、branch_table 存放在支持 Raft 的分布式数据库(如 TiDB)中。
- 好处:Seata 状态机本身实现了“跨机房双活”。即使 A 机房的 Seata Server 挂了,B 机房的 Seata Server 连上 TiDB 就能立刻接管所有未完成的事务。
2. 注册中心(Registry)跨机房同步
使用 Nacos 或 ETCD(本身基于 Raft)作为注册中心。确保微服务在调用 Seata 时,能自动感知最近的 TC 节点。
二、 模式选择:AT 还是 TCC?
在这种高性能架构下,模式的选择会发生微妙变化:
1. 优先使用 AT 模式(利用 NewSQL 的特性)
如果底层是 TiDB 等支持分布式事务的数据库,Seata 的 AT 模式性能会大幅提升。
- 原理:TiDB 内部已经解决了行级锁的 Raft 共识。Seata 只需要在上面加一层轻量级的
undo_log。 - 适用场景:大部分 80% 的业务场景。
2. 核心链路使用 TCC(配合消息队列)
对于跨机房延迟极高的核心链路,使用 TCC + 异步 Confirm。
- 实践思路:
Try阶段:在本地机房的分布式数据库中预留资源。Confirm阶段:通过 Raft 协议同步的消息队列(如 Pulsar)异步通知对端机房执行。
- 适用场景:跨国/跨长距离机房的资金划转。
三、 实战优化:如何“压榨”性能
1. 开启“全局锁查询”优化
在分布式数据库中,查询全局锁(Global Lock)开销较大。
- 优化点:在
file.conf或 Nacos 配置中,调高client.rm.lock.retryInterval(重试间隔),减少对底层 Raft 组的频繁请求压力。
2. 利用 Raft 的“分片局部性”
如果你的分布式数据库按 user_id 分片,那么 Seata 的事务分组(Service Group)也应该按 user_id 映射。
- 操作:让特定的
user_id事务总是命中同一个机房的 Seata TC 和数据库 Leader。这样可以避免跨机房的 RPC 往返,响应速度接近单机。
四、 这种架构下的异常处理流程
当底层有了 Raft 保证,Seata 处理异常变得非常简单:
| 故障场景 | Raft 层的表现 | Seata 层的表现 |
|---|---|---|
| 单机房断电 | 对端机房 Raft 自动选出新 Leader,数据不丢。 | Seata 发现事务超时,在存活机房发起全局回滚。 |
| 网络分区(脑裂) | 少数派分区停止写入,保证数据不乱。 | Seata 的 TC 处于少数派,无法拿到全局锁,事务自动失败,保护业务。 |
| 数据库慢 SQL | Raft 复制延迟变大。 | Seata 监控到事务执行时间过长,主动触发回滚,防止占满连接池。 |
五、 总结建议
在基于 Raft/Kafka 的强一致性链路上使用 Seata,你的准则是:底层管“稳”,Seata 管“快”。
- 基础设施:Seata Server 的数据库一定要用分布式强一致数据库。
- 业务设计:尽量让一个事务内的所有操作落入同一个 Raft 调度周期内(即分片一致性)。
- 最终屏障:利用 Seata 的
@GlobalCheck机制,在 Raft 复制完成后做最后一次数据校验。
这种架构虽然强悍,但运维复杂度较高。