【架构实战】分布式事务:从CAP定理到Seata实战,一文讲透跨服务数据一致性
昨天聊了微服务架构下的 API 网关和认证授权,有个核心问题一直没展开:跨服务的数据一致性。
一个典型的事故场景:下单服务减了库存,微服务 A 通知支付服务收款,用户支付成功后,发货服务要生成物流单。如果中间任何一步失败了,怎么处理?库存减了但钱没收、钱收了但货没发------这些"半成品状态"是企业级系统里最让人头大的问题。
分布式事务,说白了就是在多个服务、多个数据库之间,保证"要么全做,要么全不做"。这个问题困扰了架构师十几年,到今天也没有银弹,但有成熟可落地的解法。这篇从 CAP 定理开始,到 TCC、Saga 模式,再到生产级框架 Seata,把完整链路讲清楚。
一、CAP 定理:理解分布式系统一致性的基础
任何讨论分布式事务的文章,不讲 CAP 定理就是在耍流氓。
CAP 定理由加州大学伯克利分校的 Eric Brewer 在 2000 年提出,由 Seth Gilbert 和 Nancy Lynch 在 2002 年证明。内容很简单:在一个分布式系统里,不可能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition Tolerance)三者,最多同时满足两个。
- 一致性(Consistency):每次读操作都能读到最新写入的数据,所有节点看到的数据一致。
- 可用性(Availability):每个请求都能在有限时间内得到响应,不保证数据是最新的。
- 分区容错性(Partition Tolerance):系统能在网络分区(节点之间通信中断)的情况下继续运行。
为什么不能同时满足?因为网络分区是一定会发生的(机房断电、交换机故障、跨地域网络抖动),一旦发生,系统必须在"停服等一致"和"继续服务"之间二选一。没有第三个选项。
CP 系统:优先保证一致性,牺牲可用性。典型:ZooKeeper、HBase、Redis 主从同步模式(从节点在同步未完成时拒绝读写)。
AP 系统:优先保证可用性,牺牲一致性。典型:Cassandra、DynamoDB、Eureka(注册中心)、Redis 集群模式(从节点可读但可能是旧数据)。
现实工程选择:在网络分区必然发生的前提下,工程上通常选择 AP,然后通过补偿机制尽可能达到最终一致性。强一致性的代价太大了------跨机房同步锁在网络抖动时会让整个系统hang死,这是用户无法接受的。
二、强一致性方案:两阶段提交(2PC)
两阶段提交(Two-Phase Commit, 2PC)是最经典的强一致性协议,理解它是理解所有其他方案的基础。
2.1 流程
阶段一:Prepare(准备阶段)
协调者(Coordinator)向所有参与者(Participant)发送 Prepare 请求,询问"你们准备好了吗"。每个参与者在本地执行事务(锁定资源、写 undo/redo 日志),但不提交。然后返回 Ready 或 Abort。
阶段二:Commit(提交阶段)
- 如果所有参与者都返回 Ready:协调者发送 Commit,所有参与者正式提交事务。
- 如果任何一个返回 Abort:协调者发送 Rollback,所有参与者回滚。
2.2 缺陷
2PC 有三个致命问题,生产环境基本不用:
同步阻塞:Prepare 阶段参与者会锁定资源,其他事务无法访问。如果某个参与者挂了,协调者会一直等,超时后才决定回滚。这期间整个系统处于锁定状态。
单点故障:协调者是唯一的,如果它在 Commit 阶段挂了,参与者会一直锁定资源等待指令。没有协调者,谁都不敢提交,也不敢回滚。
数据不一致:如果协调者发了部分 Commit(有些发了,有些还没发就挂了),部分参与者提交了,部分没提交,数据就不一致了。
工程结论:2PC 不适合高并发互联网场景,适合对一致性要求极高、参与节点少的金融/银行核心系统(配合硬件和专网)。普通业务用 2PC 就是给自己挖坑。
三、柔性事务:Seata 的 AT 模式和 TCC 模式
3.1 什么是柔性事务
柔性事务(Flexible Transaction)是对强一致性的妥协:放弃实时强一致性,追求最终一致性(Eventually Consistency)。即允许短暂的数据不一致,但通过补偿机制,在合理时间内自行修复到一致状态。
Seata(Simple Extensible Autonomous Transaction Architecture)是阿里巴巴开源的分布式事务解决方案,在国内生产环境使用非常广泛。它提供了四种模式:
- AT 模式:自动处理,对业务无侵入(基于 undo log 逆向 SQL)
- TCC 模式:Try-Confirm-Cancel,三阶段编程式补偿
- Saga 模式:长事务编排,适合长链路、多步骤
- XA 模式:强一致性,基于 XA 协议(需数据库支持)
国内工程实践的主流选择:AT 模式(常规业务)+ TCC 模式(高并发金融场景)。
3.2 AT 模式:自动化的数据补偿
AT 模式是 Seata 最省心的模式,业务代码几乎不用改。它的工作原理:
一阶段解析 SQL :Seata 的 DataSource Proxy 拦截业务 SQL,确定操作类型(UPDATE/INSERT/DELETE)和表名。根据主键生成逆向 SQL(undo log),写入 undo_log 表。
二阶段提交:提交成功后,异步删除 undo log。由于全局锁已释放,其他事务可以并发处理。
异常回滚 :发生异常时,Seata 从 undo_log 表读取逆向 SQL,执行补偿,恢复到修改前的状态。全局锁由 TC(Transaction Coordinator)管理。
yaml
# seata-server.yml(TC 服务端配置)
server:
port: 8091
store:
mode: db
db:
datasourceType: druid
dbType: mysql
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://mysql:3306/seata?useUnicode=true
username: root
password: root
xml
<!-- 各业务服务的依赖 -->
<dependency>
<groupId>com.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.7.0</version>
</dependency>
yaml
# application.yml(业务服务配置)
seata:
tx-service-group: my_tx_group
enable-auto-data-source-proxy: true
config:
type: nacos
nacos:
namespace: seata
serverAddr: nacos:8848
registry:
type: nacos
nacos:
application: seata-server
serverAddr: nacos:8848
业务代码完全不用改,Seata 自动拦截 DataSource 操作:
java
@Service
public class OrderServiceImpl implements OrderService {
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
@Override
public void createOrder(CreateOrderRequest request) {
// 1. 创建订单记录
Order order = new Order();
order.setId(UUID.randomUUID().toString());
order.setUserId(request.getUserId());
order.setAmount(request.getAmount());
order.setStatus("PENDING");
orderMapper.insert(order);
// 2. 调用库存服务扣减库存(feign远程调用)
storageClient.deduct(request.getProductId(), request.getQuantity());
// 3. 调用账户服务扣款
accountClient.deduct(request.getUserId(), request.getAmount());
// 4. 更新订单状态
order.setStatus("PAID");
orderMapper.updateById(order);
// 任何一步抛异常,Seata 自动回滚全部
}
}
只需要加一个 @GlobalTransactional 注解,Seata 自动管理分布式事务。一阶段的 undo log 由框架自动生成、回滚时执行,二阶段也是框架处理。
3.3 TCC 模式:精准控制的补偿
AT 模式依赖 undo log,适用于大多数场景。但如果需要精准控制每个阶段的业务逻辑,或者对性能要求极高(如每秒数万笔交易),就需要 TCC 模式。
TCC 把每个操作拆成三个阶段:
Try(预留):尝试执行,锁定资源,但不做最终确认。比如冻结库存、预扣金额。
Confirm(确认):所有 Try 都成功后,执行真正的业务操作。比如真正减库存、真正扣款。
Cancel(取消):任何一个 Try 失败,执行补偿,释放资源。比如解冻库存、退还预扣金额。
java
@LocalTCC
public interface StorageTccService {
@TwoPhaseBusinessAction(name = "deductStorage", commitMethod = "confirm", rollbackMethod = "cancel")
boolean tryDeduct(
@BusinessActionContextParameter(paramName = "productId") String productId,
@BusinessActionContextParameter(paramName = "quantity") int quantity,
@BusinessActionContextParameter(paramName = "xid") String xid
);
boolean confirm(BusinessActionContext context);
boolean cancel(BusinessActionContext context);
}
@Service
public class StorageTccServiceImpl implements StorageTccService {
@Autowired
private StorageMapper storageMapper;
@Override
@Transactional
public boolean tryDeduct(String productId, int quantity, String xid) {
// Try 阶段:检查库存是否足够,如果足够则预扣(冻结)
Storage storage = storageMapper.selectByProductId(productId);
if (storage.getAvailable() < quantity) {
throw new BusinessException("库存不足");
}
// 冻结 quantity 库存(available 减少,frozen 增加)
storageMapper.freezeStock(productId, quantity);
return true;
}
@Override
@Transactional
public boolean confirm(BusinessActionContext context) {
// Confirm 阶段:正式扣减冻结的库存
String productId = context.getActionContext("productId", String.class);
int quantity = context.getActionContext("quantity", Integer.class);
storageMapper.confirmFrozen(productId, quantity);
return true;
}
@Override
@Transactional
public boolean cancel(BusinessActionContext context) {
// Cancel 阶段:释放冻结的库存
String productId = context.getActionContext("productId", String.class);
int quantity = context.getActionContext("quantity", Integer.class);
storageMapper.unfreezeStock(productId, quantity);
return true;
}
}
TCC 的关键设计原则:
幂等性:Confirm 和 Cancel 都会重试,必须设计成幂等的(重复执行结果相同)。用主键+状态机判断,或者用 Redis 记录 xid 做去重。
空回滚:Try 没执行成功(网络超时),Cancel 被调用了。此时不应该报错,应该查一下有没有这个 xid 的记录,没有就跳过。
悬挂:Cancel 比 Try 先执行(比如网络问题 Try 超时了,但 Cancel 消息先到了)。此时 Confirm 不会执行,Cancel 做了空回滚之后,要防止 Try 最后又成功执行了。
四、Saga 模式:长事务编排
当事务链路非常长(10+ 步骤),且每一步都有明确的正向和逆向操作时,TCC 的复杂度会急剧上升。Saga 模式是更适合的选择。
Saga 把长事务拆成一系列本地事务,每个本地事务都有对应的补偿操作。执行顺序由编排器(Choreography 或 Orchestration)决定。
编排模式(Saga Orchestrator):有一个中心编排器定义执行顺序。
java
@SagaStart
public class OrderSaga {
@Autowired
private SagaController sagaController;
public void execute(CreateOrderRequest request) {
sagaController.execute(new Step[] {
new Step("createOrder", "创建订单", this::createOrderStep, this::createOrderCompensate),
new Step("deductStorage", "扣减库存", this::deductStorageStep, this::deductStorageCompensate),
new Step("deductAccount", "扣减账户", this::deductAccountStep, this::deductAccountCompensate),
new Step("sendMessage", "发送通知", this::sendMessageStep, this::sendMessageCompensate),
});
}
}
事件驱动模式(Saga Choreography):每个服务通过消息队列(MQ)通知下一个服务,不需要中心编排器。服务之间通过事件解耦,适合松耦合的微服务架构,但调试和追踪困难。
五、生产环境落地:避坑指南
5.1 全局锁的性能问题
Seata 的 AT 模式在 Update 操作时会加全局锁,这是性能的主要瓶颈。高并发场景下,如果一个全局锁持有时间过长,会阻塞其他涉及同一行数据的事务。
优化策略:
- 拆分热点数据:不要把库存全放一张表,按商品ID哈希分表,让不同商品的扣减操作不竞争同一把锁。
- 读写分离:AT 模式默认读写都在主库,可以考虑配置只读库分担读压力(Seata 支持)。
- 降级方案:库存扣减这种强一致性场景用 TCC,查询类场景直接读从库,不过 Seata。
5.2 TCC 空回滚和悬挂的防御代码
这是生产代码里最容易漏掉的部分,一定要写:
java
@Override
public boolean cancel(BusinessActionContext context) {
String xid = context.getXid();
String productId = (String) context.getActionContext("productId");
Integer quantity = (Integer) context.getActionContext("quantity");
// 空回滚防御:查询是否存在 try 记录
// 如果没有,说明 try 根本没执行,直接返回成功
if (!frozenLogMapper.existsByXidAndProductId(xid, productId)) {
log.info("空回滚检测: xid={}, productId={},跳过", xid, productId);
return true;
}
// 幂等防御:检查是否已经 cancel 过
if (frozenLogMapper.isCanceled(xid, productId)) {
return true;
}
// 执行解冻
storageMapper.unfreezeStock(productId, quantity);
frozenLogMapper.markCanceled(xid, productId);
return true;
}
5.3 事务日志的可靠性
Seata 的事务协调器(TC Server)状态存储推荐用数据库(MySQL/PostgreSQL),而不是文件或 Redis。数据库有 ACID 保证,TC Server 重启后能恢复到正确状态。Redis 存储在服务器重启时可能丢数据,导致事务状态不一致。
5.4 监控告警
分布式事务出了问题是很难排查的,因为涉及多个服务。几个必须监控的指标:
- 全局事务成功率:突然下降说明有异常
- 分支事务超时率:超时会导致全局回滚
- TC Server 的活跃会话数:爆了说明并发过高
- 补偿动作频率:Cancel/Confirm 频繁执行说明业务失败率高
六、选型建议:什么时候用什么模式
| 场景 | 推荐模式 | 原因 |
|---|---|---|
| 大多数业务场景(库存扣减、订单创建) | AT 模式 | 对业务无侵入,改造成本最低 |
| 高并发金融支付,强一致性要求 | TCC 模式 | 精准控制,性能好 |
| 超长链路(10+步骤),事件驱动架构 | Saga 模式 | 避免长时间锁定 |
| 强一致性要求,数据库支持 XA | XA 模式 | 两阶段提交,数据库原生支持 |
一个系统的不同业务模块完全可以混用不同模式:订单创建用 AT 模式(通用),支付清算用 TCC 模式(金融级精准),长账期对账用 Saga 模式(链路长)。
七、总结
分布式事务的核心矛盾:强一致性 vs 高可用。没有银弹,只有取舍。
- CAP 定理告诉我们,分区一定会发生,要在做 CP 还是 AP 之间做选择。
- 2PC 强一致但有致命缺陷(同步阻塞、单点故障),不适合互联网高并发场景。
- AT 模式最省心,适合 80% 的业务场景,Seata 自动处理回滚。
- TCC 模式精准可控,适合金融支付类高一致性场景,但需要写三阶段补偿代码。
- Saga 模式适合长链路编排,MQ 驱动解耦,但调试困难。
工程实践里,绝大多数场景用 AT 模式就够了 。只有在真正遇到性能瓶颈或强一致性要求时,才值得为 TCC 的复杂度买单。记住:最合适的架构是能够解决你当前问题的最简单方案,而不是技术最先进的那一个。
我是做架构的,关注我,一起搞定分布式系统里的那些坑。