2026/4/18 14:26:14
网站建设
项目流程
英语网站开发的背景,太原网站制作计划,xrea免费 wordpress 加速,wordpress 外链大家好#xff0c;聊下分布式事务。
在单体应用时代#xff0c;Transactional 一个注解就能搞定一切#xff0c;ACID#xff08;原子性、一致性、隔离性、持久性#xff09;那是数据库给我们的承诺。但自从微服务大行其道#xff0c;数据库被拆得七零八落#xff0c;这…大家好聊下分布式事务。在单体应用时代Transactional一个注解就能搞定一切ACID原子性、一致性、隔离性、持久性那是数据库给我们的承诺。但自从微服务大行其道数据库被拆得七零八落这个承诺就失效了。特别是在金融场景你转账转丢了 100 块钱用户能把你系统投诉到下架。很多同学在面试时满嘴 CAP、BASE 理论真到了落地要么是用性能极差的 XA 两阶段提交要么就是写了一堆难以维护的补偿代码。So 接下来带大家彻底把Seata、2PC、Saga这些概念揉碎了看看在 2026 年的今天我们到底该怎么做分布式事务。一、 痛苦的根源当 ACID 遇到网络延迟在聊方案前先对齐一下认知。为什么微服务下事务这么难本质是因为网络是不可靠的而数据库是独立的。场景很简单服务 A订单服务扣减库存。服务 A调用服务 B支付服务扣款。服务 B扣款成功但网络超时服务 A以为失败了回滚了库存。结果库存没少钱扣了。这就是典型的数据不一致。在金融场景下我们追求的往往不是强一致性那会把系统吞吐量拖垮而是最终一致性。我们要保证无论中间发生了什么异常钱和货最终必须对得上账。二、 上古神兽与现代兵器从 2PC 到 Seata1. 2PC (XA)强一致性的代价两阶段提交2PC是数据库层面的标准。第一阶段Prepare协调者问所有参与者“你们能提交吗”参与者锁定资源写 Undo/Redo Log但不提交。第二阶段Commit/Rollback如果大家都说 Yes协调者喊“提交”只要有一个 No全部“回滚”。痛点 这玩意儿在微服务里就是性能杀手。它在 Prepare 阶段会长时间持有数据库锁。如果你的微服务链路长整个系统的吞吐量会瞬间跌到谷底。而且它还有“单点故障”和“数据不一致”Commit 阶段挂了的风险。2. Seata AT 模式无侵入的“黑科技”阿里开源的 SeataSimple Extensible Autonomous Transaction Architecture之所以火是因为它的AT 模式实在是太好用了。它的核心逻辑是拦截 SQL自动生成反向 SQL。你不需要改业务代码只需要加上GlobalTransactional。Seata 代理了你的 DataSource在第一阶段提交前它会解析你的 SQL保存“更新前镜像”和“更新后镜像”到undo_log表。如果需要回滚它就用镜像生成反向 SQL 还原数据。这里有个逻辑图大家先看懂 Seata AT 的原理三、 核心实战Seata 在 Spring Boot 3 中的落地光说不练假把式。我们来看代码。环境基于Java 21Spring Boot 3.2Seata 2.0。示例 1 依赖配置 (Gradle Kotlin DSL)别再用老掉牙的 Maven XML 了跟上时代。// build.gradle.kts dependencies { implementation(org.springframework.boot:spring-boot-starter-web) implementation(com.alibaba.cloud:spring-cloud-starter-alibaba-seata:2023.0.0.0-RC1) // 注意版本兼容 implementation(org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3) // 必须引入 JDBC 驱动 runtimeOnly(com.mysql:mysql-connector-j) }示例 2 业务入口 - 全局事务发起这是最简单的部分也是 AT 模式的魅力。package com.howell.arch.transaction.service; import io.seata.spring.annotation.GlobalTransactional; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; Service public class OrderService { private final StockClient stockClient; private final PaymentClient paymentClient; private final OrderMapper orderMapper; public OrderService(StockClient stockClient, PaymentClient paymentClient, OrderMapper orderMapper) { this.stockClient stockClient; this.paymentClient paymentClient; this.orderMapper orderMapper; } /** * 下单核心逻辑 * GlobalTransactional: 开启 Seata 全局事务 * name: 事务名称方便在 Seata 控制台追踪 * rollbackFor: 指定回滚异常 */ GlobalTransactional(name create-order-tx, rollbackFor Exception.class) Transactional // 本地事务也要开启 public void createOrder(OrderDTO orderDto) { // 1. 本地落单 Order order new Order(); order.setUserId(orderDto.userId()); order.setAmount(orderDto.amount()); order.setStatus(CREATED); orderMapper.insert(order); // 2. 远程扣减库存 (RPC 调用) // 如果这里失败Seata 会感知到异常触发 Phase 2 的回滚 var stockResult stockClient.decrease(orderDto.productId(), orderDto.count()); if (!stockResult.isSuccess()) { throw new RuntimeException(库存扣减失败: stockResult.getMessage()); } // 3. 远程扣款 (RPC 调用) // 模拟如果这里抛出异常Order 表的数据会被 Seata 自动回滚库存也会被回滚 var payResult paymentClient.pay(orderDto.userId(), orderDto.amount()); if (!payResult.isSuccess()) { throw new RuntimeException(支付失败: payResult.getMessage()); } } }运行结果说明 当paymentClient.pay抛出异常时Seata TC 会通知 OrderService 和 StockService 进行回滚。Order 表刚插入的数据会被删除Stock 表扣减的库存会被加回来。这一切对业务代码是透明的。四、 进阶当 AT 模式搞不定时 —— Saga 模式AT 模式虽然爽但在金融场景或者长事务场景下它有致命弱点全局锁AT 模式为了隔离性写隔离需要持有全局锁。如果热点数据比如爆款商品的库存并发极高AT 模式会因为争抢全局锁导致性能雪崩。非数据库资源如果你的服务不是操作数据库而是调用第三方的 API比如调用银行接口转账Seata 没法去解析 SQL 生成 undo_log。这时候Saga 模式登场。Saga 的核心思想是把长事务拆分成多个本地短事务由状态机引擎协调。每个操作都有一个对应的“补偿操作”。正向操作T1 - T2 - T3如果 T2 失败执行 C2 - C1 (C 代表 Compensate 补偿)示例 3 Saga 状态机定义 (JSON 方式)Seata 的 Saga 模式通常使用 JSON 来定义状态机流程。这比写代码更直观也支持可视化编排。{ Name: financialTransferSaga, Comment: 金融转账Saga事务, StartState: DeductMoney, Version: 1.0, States: { DeductMoney: { Type: ServiceTask, ServiceName: accountService, ServiceMethod: deduct, CompensateState: CompensateDeductMoney, Next: DepositMoney, Input: [ $.[businessKey], $.[amount] ] }, CompensateDeductMoney: { Type: ServiceTask, ServiceName: accountService, ServiceMethod: compensateDeduct, Input: [ $.[businessKey], $.[amount] ] }, DepositMoney: { Type: ServiceTask, ServiceName: accountService, ServiceMethod: deposit, Input: [ $.[targetAccount], $.[amount] ] } } }示例 4 Saga 模式的业务实现 (Java)在 Java 代码中你需要实现正向方法和补偿方法。package com.howell.arch.transaction.saga; import org.springframework.stereotype.Service; import java.math.BigDecimal; Service(accountService) public class AccountService { /** * 正向操作扣款 * 这里的入参通常需要包含业务流水号用于幂等控制 */ public boolean deduct(String businessKey, BigDecimal amount) { System.out.println(Saga正向操作扣除账户余额流水号 businessKey); // 实际业务逻辑update account set balance balance - amount where ... // 必须记录流水防止重复扣款 return true; } /** * 补偿操作回滚扣款 * 当后续步骤失败时Seata Saga 引擎会调用此方法 */ public boolean compensateDeduct(String businessKey, BigDecimal amount) { System.out.println(Saga补偿操作返还账户余额流水号 businessKey); // 实际业务逻辑update account set balance balance amount where ... // 关键点必须处理“空回滚”和“防悬挂” return true; } /** * 正向操作入账 */ public boolean deposit(String targetAccount, BigDecimal amount) { System.out.println(Saga正向操作目标账户入账); // 模拟失败触发回滚 if (amount.compareTo(new BigDecimal(10000)) 0) { throw new RuntimeException(金额过大触发风控拦截); } return true; } }运行结果说明 当deposit方法抛出异常时Saga 引擎会捕获该异常并自动查找状态机定义反向调用compensateDeduct方法打印出“Saga补偿操作返还账户余额…”从而保证数据的最终一致性。五、 深度解析Saga 模式的逻辑视图Saga 模式不仅是代码更是一种架构风格。它分为编排式 (Orchestration)和协同式 (Choreography)。Seata 采用的是编排式有一个中心化的 Coordinator。六、 生产环境的“雷区”与“邪修”方案架构师的价值不在于会用工具而在于知道什么时候不用工具或者怎么魔改工具。1. 只有新手才相信“自动回滚”在金融场景Saga 的补偿方法Compensate极其重要但也是最容易出 Bug 的地方。 这里有三个必须解决的难题空回滚Seata 第一阶段还没执行比如网络丢包直接触发了第二阶段回滚。你的补偿代码去“还钱”结果发现根本没“扣钱”。结果用户白赚一笔。幂等性网络抖动Seata 调用了两次扣款或者调用了两次补偿。结果资金错乱。防悬挂二阶段的回滚Compensate比一阶段的正向操作Try/Action先到了网络拥堵导致。处理了回滚后正向操作才到。结果先退款了然后又扣款了。示例 5 解决“空回滚”与“防悬挂”的通用模板我通常会设计一张transaction_control表来记录每个分支事务的状态。package com.howell.arch.transaction.utils; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; Component public class TccAnsSagaGuardian { /** * 这是一个通用的幂等与防悬挂控制逻辑 */ Transactional public boolean isActionAllowed(String xid, String branchId, String opType) { // 1. 查询当前事务分支记录 var record repository.findByXidAndBranchId(xid, branchId); // 防悬挂检查如果是正向操作(Action)但发现已经有回滚(Rollback)记录 if (ACTION.equals(opType) record ! null ROLLBACK.equals(record.getStatus())) { return false; // 拒绝执行 } // 幂等检查如果当前操作已经执行过 if (record ! null opType.equals(record.getStatus())) { return false; // 已经执行过直接返回成功 } // 插入或更新状态 upsertStatus(xid, branchId, opType); return true; } }2. “邪修”架构本地消息表 最终一致性如果你觉得 Seata 太重或者不想引入一个新的中间件TC Server 也是要维护的那么我推荐一个“返璞归真”的方案——本地消息表。这是 eBay 最早提出的方案也是我认为最稳健的方案。思路业务数据和“消息记录”在同一个本地事务里提交到数据库。有一个定时任务或监听 Binlog不断轮询消息表把消息投递到 MQ。下游消费 MQ成功后回调删除消息。这种方案完全不需要分布式事务管理器依赖的是本地事务 重试。示例 6 本地消息表逻辑 (伪代码)Transactional public void placeOrder(Order order) { // 1. 存业务数据 orderRepository.save(order); // 2. 存消息数据 (状态待发送) // 这一步必须和上一步在同一个 Transactional 下 LocalMessage msg new LocalMessage(); msg.setTopic(order-created); msg.setContent(toJson(order)); msg.setStatus(PENDING); messageRepository.save(msg); } // 事务提交后业务和消息同时落地绝不会丢失。逻辑图解七、 架构师思维总结写了这么多最后做个总结。作为架构师在做技术选型时不要只看“新”和“热”。能不分就不分如果业务允许尽量在一个微服务内闭环用本地事务解决。微服务拆分过细是万恶之源。Seata AT 是首选对于大多数非金融核心、并发量中等的业务Seata AT 模式是开发效率最高的几乎零侵入。Saga 是长事务利器涉及跨机构、跨语言、流程极长的业务如复杂的审批流、跨境转账Saga 是唯一解。TCC/手动补偿是底牌当性能要求极致或者需要强控每一个步骤时手写 TCC 或 Saga 补偿逻辑。但要做好心理准备代码量会翻倍。本地消息表是“核武器”当你不信任任何分布式事务框架或者系统极度核心如账务中心本地消息表对账系统 才是最稳的。Takeaway: 分布式事务没有银弹。一致性Consistency、可用性Availability、分区容错性Partition tolerance你只能取其二。在互联网架构中我们通常选择AP 最终一致性。而 Seata 和 Saga就是实现最终一致性的那座桥。希望这篇文章能让你在面对分布式事务时不再是“两眼一抹黑”而是能从容地掏出架构图告诉团队“这事儿我有三种方案。”