写点什么

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

  • 2025-06-12
    福建
  • 本文字数:8288 字

    阅读完需:约 27 分钟

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


为什么需要分布式事务?


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


典型案例:银行转账


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

  1. 从小张账户扣除 100 元

  2. 向小丽账户增加 100 元


单体架构中,如果两个账户在同一个数据库中,我们可以简单地使用数据库事务来保证操作的原子性:


BEGIN TRANSACTION;  UPDATE account SET balance = balance - 100 WHERE user_id = '小张';  UPDATE account SET balance = balance + 100 WHERE user_id = '小丽';COMMIT;
复制代码


这种情况下,数据库事务能确保两个操作要么都成功,要么都失败,不会出现小张扣款而小丽未收款的情况。


然而在分布式系统中,情况变得复杂:

  • 小张和小丽的账户可能存储在不同的数据库中

  • 扣款和收款可能是两个独立的微服务

  • 网络调用可能出现延迟或失败


此时,传统的数据库事务就无法保证跨服务的操作一致性了,我们需要分布式事务来解决这个问题。


分布式事务的典型场景


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

  1. 跨 JVM 进程:微服务架构中,不同服务通过远程调用完成事务操作,如订单服务调用库存服务减库存。

  2. 跨数据库实例:单体系统访问多个数据库实例,如用户信息和订单信息分别存储在两个 MySQL 实例中。

  3. 多服务访问同一数据库:即使多个微服务访问同一个数据库,由于它们持有不同的数据库连接,也会产生分布式事务问题。


分布式事务的核心挑战


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

  • 网络不确定性:分布式系统中的网络延迟、分区、消息丢失等问题

  • 性能瓶颈:全局锁和同步阻塞导致系统吞吐量下降

  • 可用性降低:参与节点越多,整体可用性越低(如三个 99.9%可用性的服务组合后可用性降为 99.7%)

  • 复杂性增加:需要处理各种异常情况和恢复机制

这些挑战促使了分布式事务技术的不断演进,从早期的两阶段提交到现代的柔性事务解决方案。


分布式事务的技术演进过程


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


第一阶段:单数据库事务


适用场景:所有操作都在同一个数据库中完成。

实现原理:直接利用数据库的 ACID 事务特性,通过BEGIN TRANSACTIONCOMMITROLLBACK等命令保证操作的原子性。


银行转账示例

BEGIN TRANSACTION;  -- 扣除小张账户100元  UPDATE account SET balance = balance - 100 WHERE user_id = '小张';  -- 增加小丽账户100元  UPDATE account SET balance = balance + 100 WHERE user_id = '小丽';COMMIT;
复制代码


异常处理

  • 如果在任一 UPDATE 语句执行时出现异常,整个事务会回滚

  • 即使在 COMMIT 时出现异常,数据库也能保证事务的原子性


优点

  • 实现简单,完全依赖数据库内置机制

  • 性能高,没有跨节点协调开销

  • 100%保证数据一致性


缺点

  • 仅适用于单数据库场景

  • 无法满足微服务架构和分库分表的需求


[协调者]       [数据库]  |-- BEGIN TRANSACTION -->|  |---- UPDATE 小张 ------->|  |---- UPDATE 小丽 ------->|  |------ COMMIT -------->|
复制代码


随着用户量增长,单数据库无法承受压力,于是产生了数据库垂直拆分的需求,将不同业务表拆分到不同数据库中,这就进入了分布式事务的领域。


第二阶段:基于后置提交的多数据库事务


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


实现原理

  1. 在所有参与数据库上执行 SQL 但不提交

  2. 如果所有 SQL 执行成功,则逐个提交各数据库事务

  3. 如果任何 SQL 执行失败,则回滚所有数据库事务


银行转账示例

// 数据库1:账户库Connection conn1 = db1.getConnection();conn1.setAutoCommit(false);// 数据库2:交易库  Connection conn2 = db2.getConnection();conn2.setAutoCommit(false);
try { // 第一步:在所有数据库上执行SQL但不提交 stmt1 = conn1.prepareStatement("UPDATE account SET balance=balance-100 WHERE user_id='小张'"); stmt1.executeUpdate(); stmt2 = conn2.prepareStatement("INSERT INTO transaction(from_user,to_user,amount) VALUES('小张','小丽',100)"); stmt2.executeUpdate(); // 第二步:全部执行成功后,逐个提交 conn1.commit(); conn2.commit();} catch (Exception e) { // 任何一步失败则回滚所有 conn1.rollback(); conn2.rollback(); throw e;}
复制代码


异常处理

  • SQL 执行阶段异常:可以回滚所有数据库事务

  • 提交阶段异常:如果第一个事务提交成功但第二个失败,会导致数据不一致


优点

  • 比简单的"执行-立即提交"模式更能保证一致性

  • 实现相对简单


缺点

  • 提交阶段出现异常时无法保证一致性

  • 事务持有时间较长,影响并发性能


[协调者]       [DB1]        [DB2]  |-- BEGIN -->|  |-- UPDATE小张-->|  |-- BEGIN -->|  |-- INSERT交易记录-->|  |-- COMMIT DB1-->| (成功)  |-- COMMIT DB2-->| (失败!)   // 此时DB1已提交无法回滚,数据不一致
复制代码


为解决后置提交的缺陷,计算机科学家们提出了两阶段提交协议(2PC),这成为分布式事务的经典解决方案。


第三阶段:两阶段提交(2PC/XA)


两阶段提交协议通过引入准备阶段来解决后置提交的问题。


2PC 基本流程


阶段一:准备阶段

  1. 协调者向所有参与者发送 prepare 请求

  2. 参与者执行事务操作但不提交,记录 undo/redo 日志

  3. 参与者回复是否可以提交


阶段二:提交/回滚阶段

  • 如果所有参与者都回复"同意":协调者发送 commit 命令参与者完成事务提交并释放锁

  • 如果有任何参与者回复"中止":协调者发送 rollback 命令参与者使用 undo 日志回滚事务



XA 规范实现


XA 是 X/Open 组织提出的分布式事务规范,主流数据库如 MySQL、Oracle 等都支持 XA 协议。


Java 中使用 XA 示例(使用 Atomikos):

// 初始化XA数据源AtomikosDataSourceBean ds1 = new AtomikosDataSourceBean();ds1.setXaDataSourceClassName("com.mysql.jdbc.jdbc2.optional.MysqlXADataSource");// 配置略...
AtomikosDataSourceBean ds2 = new AtomikosDataSourceBean(); ds2.setXaDataSourceClassName("com.mysql.jdbc.jdbc2.optional.MysqlXADataSource");// 配置略...
// 获取连接Connection conn1 = ds1.getConnection();Connection conn2 = ds2.getConnection();
// 执行分布式事务UserTransaction utx = com.atomikos.icatch.jta.UserTransactionManager();try { utx.begin(); PreparedStatement ps1 = conn1.prepareStatement("UPDATE account SET balance=balance-100 WHERE user_id='小张'"); ps1.executeUpdate(); PreparedStatement ps2 = conn2.prepareStatement("INSERT INTO transaction(from_user,to_user,amount) VALUES('小张','小丽',100)"); ps2.executeUpdate(); utx.commit(); // 两阶段提交} catch (Exception e) { utx.rollback(); throw e;}
复制代码


优点

  • 标准化协议,主流数据库都支持

  • 强一致性保证

  • 对业务代码侵入较小


缺点 :

  • 同步阻塞:参与者在准备阶段后处于阻塞状态,持有资源锁

  • 单点故障:协调者宕机可能导致参与者一直等待

  • 数据不一致:在极端情况下(协调者与参与者同时宕机)仍可能出现不一致

  • 性能问题:多轮网络通信和持久化日志导致延迟高

由于 2PC 的这些缺陷,在微服务架构流行后,出现了更适合服务化场景的 TCC 模式


第四阶段:TCC 模式


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


TCC 核心思想


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

  1. Try:尝试执行业务,完成所有一致性检查,预留必要资源

  2. Confirm:确认执行业务,真正提交(使用 Try 阶段预留的资源)

  3. Cancel:取消执行业务,释放 Try 阶段预留的资源



TCC 实现示例


以转账为例,我们需要改造原有服务,为每个操作提供三个接口:


账户服务

// Try接口:冻结金额@PostMapping("/account/freeze")public boolean freeze(@RequestParam String userId,                      @RequestParam BigDecimal amount) {    return accountService.freezeAmount(userId, amount);}
// Confirm接口:扣除冻结金额 @PostMapping("/account/confirm")public boolean confirm(@RequestParam String userId, @RequestParam BigDecimal amount) { return accountService.debitFrozenAmount(userId, amount);}
// Cancel接口:解冻金额@PostMapping("/account/cancel")public boolean cancel(@RequestParam String userId, @RequestParam BigDecimal amount) { return accountService.unfreezeAmount(userId, amount);}
复制代码


交易服务

// Try接口:创建待确认交易记录@PostMapping("/transaction/prepare")public String prepare(@RequestBody TransactionDTO dto) {    return transactionService.prepare(dto);}
// Confirm接口:确认交易@PostMapping("/transaction/confirm")public boolean confirm(@RequestParam String txId) { return transactionService.confirm(txId);}
// Cancel接口:取消交易 @PostMapping("/transaction/cancel")public boolean cancel(@RequestParam String txId) { return transactionService.cancel(txId);}
复制代码


事务协调器

public class TccTransferService {    @Autowired    private AccountClient accountClient;    @Autowired    private TransactionClient transactionClient;        public boolean transfer(String fromUserId, String toUserId, BigDecimal amount) {        // 生成全局事务ID        String xid = UUID.randomUUID().toString();                try {            // 阶段一:Try            boolean accountPrepared = accountClient.freeze(fromUserId, amount);            String txId = transactionClient.prepare(                new TransactionDTO(xid, fromUserId, toUserId, amount));                            if (!accountPrepared || txId == null) {                throw new RuntimeException("Try阶段失败");            }                        // 阶段二:Confirm            boolean accountConfirmed = accountClient.confirm(fromUserId, amount);            boolean txConfirmed = transactionClient.confirm(txId);                        return accountConfirmed && txConfirmed;        } catch (Exception e) {            // 阶段二:Cancel            accountClient.cancel(fromUserId, amount);            transactionClient.cancel(txId);            throw e;        }    }}
复制代码


异常情况处理

  • 空回滚:Try 未执行但收到了 Cancel 请求,需实现幂等性处理

  • 幂等控制:Confirm/Cancel 可能会重试,需保证多次执行效果相同

  • 悬挂问题:Cancel 比 Try 先到,需记录操作日志进行防护


优点 :

  • 避免了长事务,性能较好

  • 适用于跨服务的分布式事务

  • 可以自定义业务逻辑的补偿操作


缺点

  • 对业务侵入性强,每个操作需要改造为三个接口

  • 实现复杂度高,需要考虑各种异常情况

  • 一致性较弱,Confirm 阶段仍可能失败


第五阶段:Saga 模式


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


Saga 核心思想


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

  • 执行实际业务操作

  • 发布事件触发下一个本地事务

  • 提供补偿操作用于回滚


Saga 有两种协调方式:

  1. 编排式(Choreography):通过事件总线自然流转,无中心协调者

  2. 编导式(Orchestration):由 Saga 协调器集中控制流程



Saga 实现示例


以订单创建为例,涉及订单服务、库存服务和支付服务:


订单服务

public class OrderSaga {    @Autowired    private InventoryClient inventoryClient;    @Autowired    private PaymentClient paymentClient;        @Transactional    public void createOrder(Order order) {        // 1. 创建订单(待支付状态)        orderRepository.save(order);                // 2. 扣减库存        inventoryClient.reduceStock(order.getProductId(), order.getQuantity());                // 3. 发起支付        paymentClient.createPayment(order.getId(), order.getAmount());    }        // 补偿操作    @Transactional    public void cancelOrder(Long orderId) {        Order order = orderRepository.findById(orderId);        if (order != null) {            order.setStatus("CANCELLED");            orderRepository.save(order);        }    }}
复制代码


库存服务

public class InventorySaga {    @Transactional    public void reduceStock(String productId, int quantity) {        inventoryRepository.reduceStock(productId, quantity);    }        // 补偿操作    @Transactional     public void compensateReduceStock(String productId, int quantity) {        inventoryRepository.addStock(productId, quantity);    }}
复制代码


Saga 协调器(编导式)

public class OrderSagaOrchestrator {    public void execute(Order order) {        Saga saga = new Saga("create_order_" + order.getId());                try {            // 步骤1:创建订单            saga.addStep(                () -> orderService.createOrder(order),                () -> orderService.cancelOrder(order.getId())            );                        // 步骤2:扣减库存            saga.addStep(                () -> inventoryService.reduceStock(order.getProductId(), order.getQuantity()),                () -> inventoryService.compensateReduceStock(order.getProductId(), order.getQuantity())            );                        // 步骤3:创建支付            saga.addStep(                () -> paymentService.createPayment(order.getId(), order.getAmount()),                null // 最后一步无需补偿            );                        saga.execute();        } catch (Exception e) {            saga.rollback();            throw e;        }    }}
复制代码


优点 :

  • 适用于长事务,不需要长期锁定资源

  • 事件驱动架构,服务间耦合度低

  • 性能较好,支持并行执行子事务


缺点

  • 编程模型复杂,需要设计补偿操作

  • 不保证隔离性,可能出现脏读

  • 调试困难,特别是编排式 Saga


第六阶段:可靠消息最终一致性


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


核心思想

  1. 消息生产者与本地事务一起提交消息

  2. 消息中间件保证消息投递

  3. 消费者保证消息处理幂等



本地消息表示例


生产者端

@Transactionalpublic void makePayment(Long orderId, BigDecimal amount) {    // 1. 业务操作:更新支付状态    paymentDao.updateStatus(orderId, "PAID");        // 2. 记录消息(与业务操作同库同事务)    messageDao.save(        new Message(UUID.randomUUID().toString(),                    "payment_completed",                    orderId.toString())    );}
// 定时任务轮询发送消息@Scheduled(fixedRate = 5000)public void pollAndSendMessages() { List<Message> messages = messageDao.findUnsent(); for (Message msg : messages) { try { rocketMQTemplate.send(msg.getTopic(), msg.getContent()); messageDao.markAsSent(msg.getId()); } catch (Exception e) { log.error("发送消息失败", e); } }}
复制代码


消费者端


@RocketMQMessageListener(topic = "payment_completed", consumerGroup = "order_group")public class PaymentCompletedConsumer implements RocketMQListener<String> {    @Override    @Transactional    public void onMessage(String orderId) {        // 幂等处理:检查是否已处理过        if (orderDao.isProcessed(orderId)) {            return;        }                // 更新订单状态        orderDao.updateStatus(orderId, "PAID");                // 记录处理标记        orderDao.markAsProcessed(orderId);    }}
复制代码


优点 :

  • 完全异步,性能最好

  • 对业务侵入小

  • 适合高并发场景


缺点

  • 只能保证最终一致性

  • 需要处理幂等性问题

  • 调试和问题排查困难


第七阶段:Seata AT 模式


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


核心思想

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

  2. 二阶段:提交:异步删除 undo log 回滚:根据 undo log 生成反向 SQL 补偿


+----------+     +----------+     +----------+|    TM    |     |    RM    |     |    TC    ||(事务管理器)|-----|(资源管理器)|-----|(事务协调器)|+----------+     +----------+     +----------+     |                |                |     v                v                v业务应用          数据库代理       全局事务控制
复制代码


Seata AT 示例


配置


@Configurationpublic class SeataConfig {    @Bean    public GlobalTransactionScanner globalTransactionScanner() {        return new GlobalTransactionScanner("order-service", "my_test_tx_group");    }}
复制代码


业务代码

@GlobalTransactionalpublic void createOrder(Order order) {    // 1. 扣减库存    inventoryDao.reduceStock(order.getProductId(), order.getQuantity());        // 2. 创建订单    orderDao.insert(order);        // 3. 扣减账户余额    accountDao.reduceBalance(order.getUserId(), order.getAmount());}
复制代码


工作原理

  1. 业务方法开始时,Seata 会拦截并开启全局事务

  2. 每个 SQL 执行时,Seata 代理会:前置镜像:查询修改前的数据执行业务 SQL 后置镜像:查询修改后的数据生成 undo log 并注册分支事务

  3. 如果所有操作成功,全局事务提交,异步清理 undo log

  4. 如果任何操作失败,全局事务回滚,根据 undo log 执行补偿


优点 :

  • 对业务代码几乎无侵入

  • 性能优于 XA,不需要数据库支持 XA 协议

  • 支持读已提交隔离级别


缺点

  • 需要部署 Seata Server

  • 回滚时可能遇到数据冲突

  • 全局锁可能成为性能瓶颈


现代分布式事务技术对比


根据不同的业务场景和一致性要求,我们可以选择合适的分布式事务解决方案:


表 1:主流分布式事务方案对比


选型建议

  1. 必须强一致:XA/2PC(适用于同机房、事务量中等场景)

  2. 金融场景:TCC(需要精确控制每一步操作)

  3. 长业务流程:Saga(适合订单、审批等流程)

  4. 高并发最终一致:可靠消息+本地事务(电商下单等场景)

  5. 一站式解决方案:Seata AT(国内微服务常用)


分布式事务的未来发展


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

  1. Service Mesh 集成:将分布式事务能力下沉到基础设施层

  2. Saga 模式增强:结合事件溯源(Event Sourcing)提供更好的可观测性

  3. 混合事务:结合强一致和最终一致的优势

  4. 新数据库支持:如 Google Spanner 的 TrueTime API 提供全局一致性


总结


分布式事务技术的发展经历了从单数据库到多数据库,再到微服务架构的演进过程。从最初的 XA/2PC 强一致性方案,到后来的 TCC、Saga 等最终一致性方案,再到现在的 Seata AT 等混合方案,每一种技术都是为了解决特定场景下的分布式一致性问题。


在实际应用中,没有完美的解决方案,只有最适合业务场景的方案。理解各种技术的原理和优缺点,才能做出合理的架构决策。未来,随着新技术的出现,分布式事务领域还将继续演进,为构建可靠的分布式系统提供更多可能性。


文章转载自:佛祖让我来巡山

原文链接:https://www.cnblogs.com/sun-10387834/p/18923401

体验地址:http://www.jnpfsoft.com/?from=001YH

用户头像

还未添加个人签名 2025-04-01 加入

还未添加个人简介

评论

发布
暂无评论
分布式事务:从基础概念到现代解决方案的全面解析_分布式_量贩潮汐·WholesaleTide_InfoQ写作社区