找回密码
 立即注册
首页 业界区 业界 【分布式事务】从基础概念到现代解决方案的全面解析 ...

【分布式事务】从基础概念到现代解决方案的全面解析

尸酒岐 2025-6-11 09:21:35
分布式事务:从基础概念到现代解决方案的全面解析

分布式事务是构建现代分布式系统的关键技术之一,它解决了在多个独立服务或数据库间保持数据一致性的难题。本文将系统性地介绍分布式事务的必要性、技术演进历程以及当前主流解决方案的实现原理。我们将从最简单的单数据库事务开始,逐步深入到复杂的微服务场景下的分布式事务处理,涵盖2PC、TCC、Saga、可靠消息、Seata AT等主流技术,并结合实际案例和图示分析各种技术的优缺点及适用场景。
为什么需要分布式事务?

在单体应用架构中,我们通常使用数据库的ACID事务来保证数据一致性。然而随着系统规模扩大和微服务架构的普及,数据和服务被拆分到不同的节点上,传统的单机事务机制已无法满足需求,这就产生了对分布式事务的需求。
典型案例:银行转账

考虑一个经典的银行转账场景:程序员小张要向女友小丽转账100元。这个操作需要两个步骤:

  • 从小张账户扣除100元
  • 向小丽账户增加100元
单体架构中,如果两个账户在同一个数据库中,我们可以简单地使用数据库事务来保证操作的原子性:
  1. BEGIN TRANSACTION;
  2.   UPDATE account SET balance = balance - 100 WHERE user_id = '小张';
  3.   UPDATE account SET balance = balance + 100 WHERE user_id = '小丽';
  4. COMMIT;
复制代码
这种情况下,数据库事务能确保两个操作要么都成功,要么都失败,不会出现小张扣款而小丽未收款的情况。
然而在分布式系统中,情况变得复杂:

  • 小张和小丽的账户可能存储在不同的数据库中
  • 扣款和收款可能是两个独立的微服务
  • 网络调用可能出现延迟或失败
此时,传统的数据库事务就无法保证跨服务的操作一致性了,我们需要分布式事务来解决这个问题。
分布式事务的典型场景

分布式事务主要出现在以下三种场景中:

  • 跨JVM进程:微服务架构中,不同服务通过远程调用完成事务操作,如订单服务调用库存服务减库存。
  • 跨数据库实例:单体系统访问多个数据库实例,如用户信息和订单信息分别存储在两个MySQL实例中。
  • 多服务访问同一数据库:即使多个微服务访问同一个数据库,由于它们持有不同的数据库连接,也会产生分布式事务问题。
分布式事务的核心挑战

分布式事务面临的主要挑战包括:

  • 网络不确定性:分布式系统中的网络延迟、分区、消息丢失等问题
  • 性能瓶颈:全局锁和同步阻塞导致系统吞吐量下降
  • 可用性降低:参与节点越多,整体可用性越低(如三个99.9%可用性的服务组合后可用性降为99.7%)
  • 复杂性增加:需要处理各种异常情况和恢复机制
这些挑战促使了分布式事务技术的不断演进,从早期的两阶段提交到现代的柔性事务解决方案。
分布式事务的技术演进过程

分布式事务技术随着系统架构的演变而不断发展。下面我们将按照技术演进的顺序,详细介绍各阶段的核心解决方案。
第一阶段:单数据库事务

适用场景:所有操作都在同一个数据库中完成。
实现原理:直接利用数据库的ACID事务特性,通过BEGIN TRANSACTION、COMMIT、ROLLBACK等命令保证操作的原子性。
银行转账示例
  1. BEGIN TRANSACTION;
  2.   -- 扣除小张账户100元
  3.   UPDATE account SET balance = balance - 100 WHERE user_id = '小张';
  4.   -- 增加小丽账户100元
  5.   UPDATE account SET balance = balance + 100 WHERE user_id = '小丽';
  6. COMMIT;
复制代码
异常处理

  • 如果在任一UPDATE语句执行时出现异常,整个事务会回滚
  • 即使在COMMIT时出现异常,数据库也能保证事务的原子性
优点

  • 实现简单,完全依赖数据库内置机制
  • 性能高,没有跨节点协调开销
  • 100%保证数据一致性
缺点

  • 仅适用于单数据库场景
  • 无法满足微服务架构和分库分表的需求
图1:单数据库事务时序图
  1. [协调者]       [数据库]
  2.   |-- BEGIN TRANSACTION -->|
  3.   |---- UPDATE 小张 ------->|
  4.   |---- UPDATE 小丽 ------->|
  5.   |------ COMMIT -------->|
复制代码
随着用户量增长,单数据库无法承受压力,于是产生了数据库垂直拆分的需求,将不同业务表拆分到不同数据库中,这就进入了分布式事务的领域。
第二阶段:基于后置提交的多数据库事务

当账户表和交易记录表被拆分到不同数据库后,简单的单数据库事务不再适用。最初的解决方案是后置提交策略。
实现原理

  • 在所有参与数据库上执行SQL但不提交
  • 如果所有SQL执行成功,则逐个提交各数据库事务
  • 如果任何SQL执行失败,则回滚所有数据库事务
银行转账示例
  1. // 数据库1:账户库
  2. Connection conn1 = db1.getConnection();
  3. conn1.setAutoCommit(false);
  4. // 数据库2:交易库  
  5. Connection conn2 = db2.getConnection();
  6. conn2.setAutoCommit(false);
  7. try {
  8.     // 第一步:在所有数据库上执行SQL但不提交
  9.     stmt1 = conn1.prepareStatement("UPDATE account SET balance=balance-100 WHERE user_id='小张'");
  10.     stmt1.executeUpdate();
  11.    
  12.     stmt2 = conn2.prepareStatement("INSERT INTO transaction(from_user,to_user,amount) VALUES('小张','小丽',100)");
  13.     stmt2.executeUpdate();
  14.    
  15.     // 第二步:全部执行成功后,逐个提交
  16.     conn1.commit();
  17.     conn2.commit();
  18. } catch (Exception e) {
  19.     // 任何一步失败则回滚所有
  20.     conn1.rollback();
  21.     conn2.rollback();
  22.     throw e;
  23. }
复制代码
异常处理

  • SQL执行阶段异常:可以回滚所有数据库事务
  • 提交阶段异常:如果第一个事务提交成功但第二个失败,会导致数据不一致
优点

  • 比简单的"执行-立即提交"模式更能保证一致性
  • 实现相对简单
缺点

  • 提交阶段出现异常时无法保证一致性
  • 事务持有时间较长,影响并发性能
图2:后置提交策略的潜在问题
  1. [协调者]       [DB1]        [DB2]
  2.   |-- BEGIN -->|
  3.   |-- UPDATE小张-->|
  4.   |-- BEGIN -->|
  5.   |-- INSERT交易记录-->|
  6.   |-- COMMIT DB1-->| (成功)
  7.   |-- COMMIT DB2-->| (失败!)
  8.   // 此时DB1已提交无法回滚,数据不一致
复制代码
为解决后置提交的缺陷,计算机科学家们提出了两阶段提交协议(2PC),这成为分布式事务的经典解决方案。
第三阶段:两阶段提交(2PC/XA)

两阶段提交协议通过引入准备阶段来解决后置提交的问题。
2PC基本流程

阶段一:准备阶段

  • 协调者向所有参与者发送prepare请求
  • 参与者执行事务操作但不提交,记录undo/redo日志
  • 参与者回复是否可以提交
阶段二:提交/回滚阶段

  • 如果所有参与者都回复"同意":

    • 协调者发送commit命令
    • 参与者完成事务提交并释放锁

  • 如果有任何参与者回复"中止":

    • 协调者发送rollback命令
    • 参与者使用undo日志回滚事务

图3:2PC正常提交流程
sequenceDiagram    participant C as 协调者    participant P1 as 参与者1(账户库)    participant P2 as 参与者2(交易库)        C->>1: prepare    C->>2: prepare    P1-->>C: 同意    P2-->>C: 同意    C->>1: commit    C->>2: commit    P1-->>C: ack    P2-->>C: ackXA规范实现

XA是X/Open组织提出的分布式事务规范,主流数据库如MySQL、Oracle等都支持XA协议。
Java中使用XA示例(使用Atomikos):
  1. // 初始化XA数据源
  2. AtomikosDataSourceBean ds1 = new AtomikosDataSourceBean();
  3. ds1.setXaDataSourceClassName("com.mysql.jdbc.jdbc2.optional.MysqlXADataSource");
  4. // 配置略...
  5. AtomikosDataSourceBean ds2 = new AtomikosDataSourceBean();
  6. ds2.setXaDataSourceClassName("com.mysql.jdbc.jdbc2.optional.MysqlXADataSource");
  7. // 配置略...
  8. // 获取连接
  9. Connection conn1 = ds1.getConnection();
  10. Connection conn2 = ds2.getConnection();
  11. // 执行分布式事务
  12. UserTransaction utx = com.atomikos.icatch.jta.UserTransactionManager();
  13. try {
  14.     utx.begin();
  15.    
  16.     PreparedStatement ps1 = conn1.prepareStatement("UPDATE account SET balance=balance-100 WHERE user_id='小张'");
  17.     ps1.executeUpdate();
  18.    
  19.     PreparedStatement ps2 = conn2.prepareStatement("INSERT INTO transaction(from_user,to_user,amount) VALUES('小张','小丽',100)");
  20.     ps2.executeUpdate();
  21.    
  22.     utx.commit(); // 两阶段提交
  23. } catch (Exception e) {
  24.     utx.rollback();
  25.     throw e;
  26. }
复制代码
优点

  • 标准化协议,主流数据库都支持
  • 强一致性保证
  • 对业务代码侵入较小
缺点

  • 同步阻塞:参与者在准备阶段后处于阻塞状态,持有资源锁
  • 单点故障:协调者宕机可能导致参与者一直等待
  • 数据不一致:在极端情况下(协调者与参与者同时宕机)仍可能出现不一致
  • 性能问题:多轮网络通信和持久化日志导致延迟高
由于2PC的这些缺陷,在微服务架构流行后,出现了更适合服务化场景的TCC模式
第四阶段:TCC模式

当系统从直接操作数据库演进为服务化架构后,XA协议不再适用。TCC(Try-Confirm-Cancel)模式成为服务化场景下分布式事务的主流解决方案。
TCC核心思想

TCC将业务操作分为三个阶段:

  • Try:尝试执行业务,完成所有一致性检查,预留必要资源
  • Confirm:确认执行业务,真正提交(使用Try阶段预留的资源)
  • Cancel:取消执行业务,释放Try阶段预留的资源
图4:TCC模式时序图
sequenceDiagram    participant T as TM(事务管理器)    participant A as 账户服务    participant B as 交易服务        T->>A: Try(冻结小张100元)    T->>B: Try(创建待确认交易记录)    alt 所有Try成功        T->>A: Confirm(扣除冻结的100元)        T->>B: Confirm(确认交易记录)    else 任一Try失败        T->>A: Cancel(解冻100元)        T->>B: Cancel(删除交易记录)    endTCC实现示例

以转账为例,我们需要改造原有服务,为每个操作提供三个接口:
账户服务
  1. // Try接口:冻结金额
  2. @PostMapping("/account/freeze")
  3. public boolean freeze(@RequestParam String userId,
  4.                      @RequestParam BigDecimal amount) {
  5.     return accountService.freezeAmount(userId, amount);
  6. }
  7. // Confirm接口:扣除冻结金额  
  8. @PostMapping("/account/confirm")
  9. public boolean confirm(@RequestParam String userId,
  10.                       @RequestParam BigDecimal amount) {
  11.     return accountService.debitFrozenAmount(userId, amount);
  12. }
  13. // Cancel接口:解冻金额
  14. @PostMapping("/account/cancel")
  15. public boolean cancel(@RequestParam String userId,
  16.                      @RequestParam BigDecimal amount) {
  17.     return accountService.unfreezeAmount(userId, amount);
  18. }
复制代码
交易服务
  1. // Try接口:创建待确认交易记录
  2. @PostMapping("/transaction/prepare")
  3. public String prepare(@RequestBody TransactionDTO dto) {
  4.     return transactionService.prepare(dto);
  5. }
  6. // Confirm接口:确认交易
  7. @PostMapping("/transaction/confirm")
  8. public boolean confirm(@RequestParam String txId) {
  9.     return transactionService.confirm(txId);
  10. }
  11. // Cancel接口:取消交易  
  12. @PostMapping("/transaction/cancel")
  13. public boolean cancel(@RequestParam String txId) {
  14.     return transactionService.cancel(txId);
  15. }
复制代码
事务协调器
  1. public class TccTransferService {
  2.     @Autowired
  3.     private AccountClient accountClient;
  4.     @Autowired
  5.     private TransactionClient transactionClient;
  6.    
  7.     public boolean transfer(String fromUserId, String toUserId, BigDecimal amount) {
  8.         // 生成全局事务ID
  9.         String xid = UUID.randomUUID().toString();
  10.         
  11.         try {
  12.             // 阶段一:Try
  13.             boolean accountPrepared = accountClient.freeze(fromUserId, amount);
  14.             String txId = transactionClient.prepare(
  15.                 new TransactionDTO(xid, fromUserId, toUserId, amount));
  16.                
  17.             if (!accountPrepared || txId == null) {
  18.                 throw new RuntimeException("Try阶段失败");
  19.             }
  20.             
  21.             // 阶段二:Confirm
  22.             boolean accountConfirmed = accountClient.confirm(fromUserId, amount);
  23.             boolean txConfirmed = transactionClient.confirm(txId);
  24.             
  25.             return accountConfirmed && txConfirmed;
  26.         } catch (Exception e) {
  27.             // 阶段二:Cancel
  28.             accountClient.cancel(fromUserId, amount);
  29.             transactionClient.cancel(txId);
  30.             throw e;
  31.         }
  32.     }
  33. }
复制代码
异常情况处理

  • 空回滚:Try未执行但收到了Cancel请求,需实现幂等性处理
  • 幂等控制:Confirm/Cancel可能会重试,需保证多次执行效果相同
  • 悬挂问题:Cancel比Try先到,需记录操作日志进行防护
优点

  • 避免了长事务,性能较好
  • 适用于跨服务的分布式事务
  • 可以自定义业务逻辑的补偿操作
缺点

  • 对业务侵入性强,每个操作需要改造为三个接口
  • 实现复杂度高,需要考虑各种异常情况
  • 一致性较弱,Confirm阶段仍可能失败
第五阶段:Saga模式

对于长事务场景,TCC模式的资源锁定时间仍然过长。Saga模式通过事件驱动补偿事务提供了另一种解决方案。
Saga核心思想

Saga将分布式事务拆分为一系列本地事务,每个本地事务:

  • 执行实际业务操作
  • 发布事件触发下一个本地事务
  • 提供补偿操作用于回滚
Saga有两种协调方式:

  • 编排式(Choreography):通过事件总线自然流转,无中心协调者
  • 编导式(Orchestration):由Saga协调器集中控制流程
图5:编排式Saga示例(转账场景)
sequenceDiagram    participant A as 账户服务    participant T as 交易服务    participant N as 通知服务        A->>A: 本地事务:扣款    A->>T: 发布"扣款成功"事件    T->>T: 本地事务:记录交易    T->>N: 发布"交易完成"事件    N->>N: 本地事务:发送通知Saga实现示例

以订单创建为例,涉及订单服务、库存服务和支付服务:
订单服务
  1. public class OrderSaga {
  2.     @Autowired
  3.     private InventoryClient inventoryClient;
  4.     @Autowired
  5.     private PaymentClient paymentClient;
  6.    
  7.     @Transactional
  8.     public void createOrder(Order order) {
  9.         // 1. 创建订单(待支付状态)
  10.         orderRepository.save(order);
  11.         
  12.         // 2. 扣减库存
  13.         inventoryClient.reduceStock(order.getProductId(), order.getQuantity());
  14.         
  15.         // 3. 发起支付
  16.         paymentClient.createPayment(order.getId(), order.getAmount());
  17.     }
  18.    
  19.     // 补偿操作
  20.     @Transactional
  21.     public void cancelOrder(Long orderId) {
  22.         Order order = orderRepository.findById(orderId);
  23.         if (order != null) {
  24.             order.setStatus("CANCELLED");
  25.             orderRepository.save(order);
  26.         }
  27.     }
  28. }
复制代码
库存服务
  1. public class InventorySaga {
  2.     @Transactional
  3.     public void reduceStock(String productId, int quantity) {
  4.         inventoryRepository.reduceStock(productId, quantity);
  5.     }
  6.    
  7.     // 补偿操作
  8.     @Transactional
  9.     public void compensateReduceStock(String productId, int quantity) {
  10.         inventoryRepository.addStock(productId, quantity);
  11.     }
  12. }
复制代码
Saga协调器(编导式)
  1. public class OrderSagaOrchestrator {
  2.     public void execute(Order order) {
  3.         Saga saga = new Saga("create_order_" + order.getId());
  4.         
  5.         try {
  6.             // 步骤1:创建订单
  7.             saga.addStep(
  8.                 () -> orderService.createOrder(order),
  9.                 () -> orderService.cancelOrder(order.getId())
  10.             );
  11.             
  12.             // 步骤2:扣减库存
  13.             saga.addStep(
  14.                 () -> inventoryService.reduceStock(order.getProductId(), order.getQuantity()),
  15.                 () -> inventoryService.compensateReduceStock(order.getProductId(), order.getQuantity())
  16.             );
  17.             
  18.             // 步骤3:创建支付
  19.             saga.addStep(
  20.                 () -> paymentService.createPayment(order.getId(), order.getAmount()),
  21.                 null // 最后一步无需补偿
  22.             );
  23.             
  24.             saga.execute();
  25.         } catch (Exception e) {
  26.             saga.rollback();
  27.             throw e;
  28.         }
  29.     }
  30. }
复制代码
优点

  • 适用于长事务,不需要长期锁定资源
  • 事件驱动架构,服务间耦合度低
  • 性能较好,支持并行执行子事务
缺点

  • 编程模型复杂,需要设计补偿操作
  • 不保证隔离性,可能出现脏读
  • 调试困难,特别是编排式Saga
第六阶段:可靠消息最终一致性

对于对实时一致性要求不高的场景,可靠消息最终一致性模式提供了更轻量级的解决方案。
核心思想


  • 消息生产者与本地事务一起提交消息
  • 消息中间件保证消息投递
  • 消费者保证消息处理幂等
图6:可靠消息实现方案对比
方案实现方式优点缺点本地消息表业务与消息同库,轮询发送简单可靠需要轮询,延迟高RocketMQ事务消息两阶段消息,无需本地表无侵入,高性能依赖特定MQCDC监听监听数据库binlog变化完全解耦实现复杂本地消息表示例

生产者端
  1. @Transactional
  2. public void makePayment(Long orderId, BigDecimal amount) {
  3.     // 1. 业务操作:更新支付状态
  4.     paymentDao.updateStatus(orderId, "PAID");
  5.    
  6.     // 2. 记录消息(与业务操作同库同事务)
  7.     messageDao.save(
  8.         new Message(UUID.randomUUID().toString(),
  9.                    "payment_completed",
  10.                    orderId.toString())
  11.     );
  12. }
  13. // 定时任务轮询发送消息
  14. @Scheduled(fixedRate = 5000)
  15. public void pollAndSendMessages() {
  16.     List<Message> messages = messageDao.findUnsent();
  17.     for (Message msg : messages) {
  18.         try {
  19.             rocketMQTemplate.send(msg.getTopic(), msg.getContent());
  20.             messageDao.markAsSent(msg.getId());
  21.         } catch (Exception e) {
  22.             log.error("发送消息失败", e);
  23.         }
  24.     }
  25. }
复制代码
消费者端
  1. @RocketMQMessageListener(topic = "payment_completed", consumerGroup = "order_group")
  2. public class PaymentCompletedConsumer implements RocketMQListener<String> {
  3.     @Override
  4.     @Transactional
  5.     public void onMessage(String orderId) {
  6.         // 幂等处理:检查是否已处理过
  7.         if (orderDao.isProcessed(orderId)) {
  8.             return;
  9.         }
  10.         
  11.         // 更新订单状态
  12.         orderDao.updateStatus(orderId, "PAID");
  13.         
  14.         // 记录处理标记
  15.         orderDao.markAsProcessed(orderId);
  16.     }
  17. }
复制代码
优点

  • 完全异步,性能最好
  • 对业务侵入小
  • 适合高并发场景
缺点

  • 只能保证最终一致性
  • 需要处理幂等性问题
  • 调试和问题排查困难
第七阶段:Seata AT模式

Seata AT(Automatic Transaction)模式是阿里开源的分布式事务解决方案,结合了XA和TCC的优点。
核心思想


  • 一阶段:执行业务SQL,自动生成undo log并获取全局锁
  • 二阶段

    • 提交:异步删除undo log
    • 回滚:根据undo log生成反向SQL补偿

图7:Seata AT架构
  1. +----------+     +----------+     +----------+
  2. |    TM    |     |    RM    |     |    TC    |
  3. |(事务管理器)|-----|(资源管理器)|-----|(事务协调器)|
  4. +----------+     +----------+     +----------+
  5.      |                |                |
  6.      v                v                v
  7. 业务应用          数据库代理       全局事务控制
复制代码
Seata AT示例

配置
  1. @Configuration
  2. public class SeataConfig {
  3.     @Bean
  4.     public GlobalTransactionScanner globalTransactionScanner() {
  5.         return new GlobalTransactionScanner("order-service", "my_test_tx_group");
  6.     }
  7. }
复制代码
业务代码
  1. @GlobalTransactional
  2. public void createOrder(Order order) {
  3.     // 1. 扣减库存
  4.     inventoryDao.reduceStock(order.getProductId(), order.getQuantity());
  5.    
  6.     // 2. 创建订单
  7.     orderDao.insert(order);
  8.    
  9.     // 3. 扣减账户余额
  10.     accountDao.reduceBalance(order.getUserId(), order.getAmount());
  11. }
复制代码
工作原理

  • 业务方法开始时,Seata会拦截并开启全局事务
  • 每个SQL执行时,Seata代理会:

    • 前置镜像:查询修改前的数据
    • 执行业务SQL
    • 后置镜像:查询修改后的数据
    • 生成undo log并注册分支事务

  • 如果所有操作成功,全局事务提交,异步清理undo log
  • 如果任何操作失败,全局事务回滚,根据undo log执行补偿
优点

  • 对业务代码几乎无侵入
  • 性能优于XA,不需要数据库支持XA协议
  • 支持读已提交隔离级别
缺点

  • 需要部署Seata Server
  • 回滚时可能遇到数据冲突
  • 全局锁可能成为性能瓶颈
现代分布式事务技术对比

根据不同的业务场景和一致性要求,我们可以选择合适的分布式事务解决方案:
表1:主流分布式事务方案对比
方案一致性性能侵入性适用场景代表实现XA/2PC强一致低低同构数据库,短事务Atomikos, NarayanaTCC最终一致高高金融支付,需精确控制Seata TCC, HmilySaga最终一致高中长事务,流程编排Axon, Temporal可靠消息最终一致最高低异步场景,高并发RocketMQ, KafkaSeata AT近强一致中低同构关系型数据库Seata选型建议

  • 必须强一致:XA/2PC(适用于同机房、事务量中等场景)
  • 金融场景:TCC(需要精确控制每一步操作)
  • 长业务流程:Saga(适合订单、审批等流程)
  • 高并发最终一致:可靠消息+本地事务(电商下单等场景)
  • 一站式解决方案:Seata AT(国内微服务常用)
分布式事务的未来发展

随着云原生和Serverless架构的兴起,分布式事务技术也在不断演进:

  • Service Mesh集成:将分布式事务能力下沉到基础设施层
  • Saga模式增强:结合事件溯源(Event Sourcing)提供更好的可观测性
  • 混合事务:结合强一致和最终一致的优势
  • 新数据库支持:如Google Spanner的TrueTime API提供全局一致性
总结

分布式事务技术的发展经历了从单数据库到多数据库,再到微服务架构的演进过程。从最初的XA/2PC强一致性方案,到后来的TCC、Saga等最终一致性方案,再到现在的Seata AT等混合方案,每一种技术都是为了解决特定场景下的分布式一致性问题。
在实际应用中,没有完美的解决方案,只有最适合业务场景的方案。理解各种技术的原理和优缺点,才能做出合理的架构决策。未来,随着新技术的出现,分布式事务领域还将继续演进,为构建可靠的分布式系统提供更多可能性。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册