序言在单体架构时代我们依赖数据库原生事务和 Spring 的Transactional注解就能轻松解决所有数据一致性问题。但随着微服务架构的普及系统被拆分为多个独立部署的服务每个服务拥有自己的数据库传统的本地事务机制彻底失效。一次简单的电商下单操作可能需要跨订单、库存、账户、支付四个服务调用任何一个环节出现网络超时或服务宕机都会导致严重的数据不一致 —— 订单生成了但库存没扣钱付了但订单没创建这些问题轻则造成用户投诉重则导致企业资产流失。SeataSimple Extensible Autonomous Transaction Architecture作为阿里巴巴开源的分布式事务解决方案凭借其高性能、低侵入、易上手的特点已经成为微服务架构下解决分布式事务问题的事实标准。本文将从分布式事务的本质问题出发深入解析 Seata 的核心原理、四种工作模式的实现细节并结合完整的代码案例带你从理论到实践全面掌握 Seata最终实现生产环境的高可用部署。一、问题的提出从单体服务到微服务的事务困境1.1 单体架构下的事务简单而美好在单体架构中所有业务逻辑运行在同一个进程中共享同一个数据库连接。此时的事务管理非常简单我们只需要在方法上添加Transactional注解Spring 事务管理器会自动帮我们处理事务的开启、提交和回滚。Service public class OrderService { Autowired private OrderMapper orderMapper; Autowired private StockMapper stockMapper; Transactional(rollbackFor Exception.class) public Long createOrder(Order order) { // 1. 创建订单 orderMapper.insert(order); // 2. 扣减库存 stockMapper.decreaseStock(order.getProductId(), order.getQuantity()); return order.getId(); } }代码在单体架构下完美工作它依赖数据库的ACID特性来保证原子性要么两个操作都成功提交要么任何一个失败都回滚到事务开始前的状态。数据库通过锁机制和事务日志Redo Log、Undo Log来实现这一点整个过程对开发者完全透明。1.2 微服务架构下的事务噩梦的开始当我们将系统拆分为微服务后订单服务和库存服务变成了两个独立部署的应用分别连接自己的数据库。此时上面的代码就会出现严重的问题这里利用 Feign 进行远程调用不了解的小伙伴可以看看微服务的相关内容。Service public class OrderService { Autowired private OrderMapper orderMapper; Autowired private StockFeignClient stockFeignClient; Transactional(rollbackFor Exception.class) public Long createOrder(Order order) { // 1. 订单服务本地操作创建订单 orderMapper.insert(order); // 2. 远程调用库存服务扣减库存 stockFeignClient.decreaseStock(order.getProductId(), order.getQuantity()); return order.getId(); } }这段代码看起来和单体架构下几乎一样但实际上存在致命的缺陷Transactional注解只能管理订单服务本地的数据库事务无法控制远程调用的库存服务事务。我们来分析一个典型的失败场景用户发起下单请求订单服务创建订单本地事务执行成功订单服务调用库存服务扣减库存如果库存服务某一时刻因为网络超时或服务宕机扣减操作失败订单服务抛出异常本地事务回滚订单被删除此时就出现了数据不一致订单已经回滚了但如果库存服务实际上已经扣减了库存只是响应超时了就会导致库存凭空消失造成企业资产损失。反之如果订单创建成功库存扣减失败但订单没有回滚就会出现超卖问题。这就是微服务架构下的分布式事务难题一次业务操作跨越多个服务和多个数据库本地事务无法保证全局数据的一致性。1.3 分布式事务的核心命题分布式事务的本质是要保证在分布式系统中一系列跨服务、跨数据源的操作要么全部成功要么全部失败最终确保全局数据的一致性。为了解决这个问题我们需要一个全局的事务协调者来协调各个分支事务的执行状态。所有分支事务都要向协调者报告自己的执行结果协调者根据所有分支的结果来决定最终是全局提交还是全局回滚。二、理论基础分布式系统的 CAP 与 BASE 定理在深入了解 Seata 之前我们必须先掌握分布式系统的两个核心理论CAP 定理和 BASE 理论它们是所有分布式事务解决方案的理论基础。2.1 CAP 定理分布式系统的不可能三角1998 年加州大学的计算机科学家 Eric Brewer 提出了著名的 CAP 定理一个分布式系统不可能同时满足一致性Consistency、可用性Availability和分区容错性Partition tolerance这三个基本需求最多只能同时满足其中的两个。2.1.1 一致性Consistency一致性指的是所有用户在同一时间访问分布式系统中的任意节点得到的数据必须是完全一致的。也就是说当一个数据更新操作成功后所有节点上的该数据都应该同时更新到最新状态。例如在一个分布式数据库集群中当用户在节点 A 更新了一条数据后其他用户在节点 B、C、D 上读取这条数据时都应该能看到更新后的结果而不是旧数据。2.1.2 可用性Availability可用性指的是用户访问集群中的任意健康节点必须能在合理的时间内得到合理的响应而不是超时或错误。也就是说系统必须一直处于可用状态能够正常处理用户的请求。例如一个电商网站在双十一期间即使流量暴增也必须保证用户能够正常浏览商品、下单和支付不能出现大面积的服务不可用。2.1.3 分区容错性Partition tolerance分区容错性指的是当分布式系统中的部分节点因为网络故障而失去连接形成独立的分区时整个系统仍然能够继续对外提供服务。在分布式系统中网络故障是不可避免的。节点之间可能会因为网络延迟、丢包、断网等原因无法通信形成多个独立的分区。分区容错性要求系统在这种情况下仍然能够正常工作不能因为网络分区而崩溃。2.1.4 CAP 的取舍CAP 定理告诉我们分布式系统无法同时满足这三个特性我们必须在其中做出取舍。那么我们应该如何选择呢首先分区容错性P是必须满足的。因为在分布式系统中网络分区是必然会发生的我们无法避免。如果一个系统不具备分区容错性那么当网络出现问题时整个系统就会崩溃这在分布式环境下是不可接受的。因此我们只能在一致性C和可用性A之间做出选择选择 C 而放弃 A当网络分区发生时为了保证数据的一致性系统必须拒绝用户的请求直到所有节点的数据都同步完成。这种系统被称为 CP 系统例如 ZooKeeper、HBase 等。选择 A 而放弃 C当网络分区发生时为了保证系统的可用性系统可以继续对外提供服务但可能会返回不一致的数据。当网络恢复后系统再通过异步同步的方式来达到最终一致性。这种系统被称为 AP 系统例如 Eureka、Cassandra 等。2.2 BASE 理论CAP 的折中方案CAP 定理告诉我们分布式系统无法同时满足强一致性和高可用性。但在实际的业务场景中很多时候我们并不需要强一致性只需要保证最终一致性即可。BASE 理论就是对 CAP 定理的一种折中它允许系统在一段时间内处于不一致的状态但最终要达到一致。BASE 理论包含三个核心思想2.2.1 基本可用Basically Available基本可用指的是当分布式系统出现故障时允许损失部分可用性保证核心功能可用。例如在电商网站的秒杀活动中当流量暴增时系统可以将非核心功能如商品推荐、用户评价降级只保证下单和支付这些核心功能可用。或者系统可以对用户进行限流只允许部分用户进入避免整个系统崩溃。2.2.2 软状态Soft State软状态指的是允许系统中的数据存在中间状态并且这个中间状态不会影响系统的整体可用性。例如在转账业务中当用户 A 向用户 B 转账时系统可以先将 A 的账户余额扣除然后将 B 的账户余额标记为 “待入账” 状态。此时系统处于一个中间状态A 和 B 的账户余额加起来不等于转账前的总和。但这个中间状态是暂时的当转账完成后B 的账户余额会被更新系统最终达到一致状态。2.2.3 最终一致性Eventually Consistent最终一致性指的是虽然系统在短时间内可能处于不一致的状态但经过一段时间后所有节点的数据最终会达到一致。最终一致性是 BASE 理论的核心。在分布式系统中我们通常不需要保证数据的强一致性只需要保证最终一致性即可。例如在电商系统中用户下单后库存可能不会立即扣减而是在几秒钟后才更新。但只要最终库存和订单数量是一致的就不会影响业务的正常运行。2.3 分布式事务的两种模式基于 CAP 和 BASE 理论分布式事务可以分为两种模式CP 模式强一致性事务。所有分支事务执行后互相等待同时提交同时回滚达成强一致。但在事务等待过程中系统处于弱可用状态。典型的实现有 2PC、3PC、Seata 的 XA 模式。AP 模式最终一致性事务。各分支事务分别执行和提交允许出现结果不一致然后通过补偿措施来恢复数据实现最终一致。这种模式具有高可用性是大多数互联网业务的首选。典型的实现有Seata的 TCC、SAGA、AT 模式。三、Seata 核心概念三大角色与整体架构Seata 是一款开源的分布式事务解决方案它通过三个核心角色的协同工作来实现分布式事务的管理。这三个角色分别是事务协调器TC、事务管理器TM和资源管理器RM。3.1 三大核心角色3.1.1 事务协调器TCTransaction CoordinatorTC 是全局事务的 “总指挥”是 Seata 的核心服务。它负责维护全局事务和分支事务的状态根据所有分支事务的执行结果协调驱动各分支事务最终提交或回滚。TC 的主要职责包括接收 TM 的请求开启全局事务生成全局唯一的 XID接收 RM 的注册请求管理分支事务记录全局事务和分支事务的状态当所有分支事务执行完毕后根据 TM 的决议向所有 RM 发送全局提交或回滚指令处理事务超时、异常等情况3.1.2 事务管理器TMTransaction ManagerTM 定义全局事务的范围与边界通常由业务系统的入口服务担任。它负责向 TC 发起开启全局事务的请求在业务逻辑结束时发起全局提交或回滚的决议。TM 的主要职责包括向 TC 申请开启一个全局事务获取全局唯一的 XID将 XID 传播到所有参与全局事务的微服务中监控所有分支事务的执行状态当所有分支事务都执行成功时向 TC 发起全局提交请求当任何一个分支事务执行失败时向 TC 发起全局回滚请求3.1.3 资源管理器RMResource ManagerRM 管理实际的数据库或服务资源每个微服务节点都可以作为一个 RM。它负责向 TC 注册自己为该 XID 下的一个分支事务执行本地业务逻辑并严格接收并执行 TC 发出的提交或回滚指令。RM 的主要职责包括向 TC 注册分支事务报告分支事务的执行状态执行本地业务操作管理本地事务接收 TC 的指令执行本地事务的提交或回滚在 AT 模式下生成和管理 Undo Log用于数据回滚3.2 Seata 的整体工作流程Seata 的分布式事务执行过程可以分为五个步骤开启全局事务TM 向 TC 申请开启一个全局事务TC 创建全局事务并返回一个全局唯一的 XID作为全局事务的标识。注册分支事务TM 依次调用各微服务RM1、RM2 等。RM 在执行本地事务前向 TC 注册自己为该 XID 下的一个分支事务。执行分支事务各个 RM 执行各自的本地业务逻辑如扣减库存、创建订单并将执行结果上报给 TC。全局决议当所有分支事务执行完毕TM 根据收集到的业务执行结果向 TC 发起最终的全局提交或全局回滚决议。全局提交 / 回滚TC 协调所有 RM 执行统一的提交或回滚操作确保所有微服务的数据要么全部成功提交要么一起回滚达成最终一致性。3.3 Seata 的整体架构Seata 采用了微服务架构主要由以下几个部分组成TC Server事务协调器服务独立部署负责全局事务的协调和管理(TM 和 RM都要向 TC 发送请求)。TM Client事务管理器客户端集成在业务服务中负责开启、提交和回滚全局事务。RM Client资源管理器客户端集成在业务服务中负责管理本地资源执行本地事务的提交和回滚。注册中心用于 TC 和微服务之间的服务发现支持 Nacos、Eureka、Consul 等。配置中心用于存储 Seata 的配置信息支持 Nacos、Apollo 等。TC 服务的部署需要打开你下载好的 seata 的 conf 目录下的 register.conf 目录进行修改(这里给出 nacos 的相关配置)注册中心配置中心在下载 seata 服务时要注意1.注意 jdk 的版本不同 seata 版本的垃圾回收器不同注意与当前环境变量中的 jdk 兼容或者更改bin目录下 seata-server.bat 配置文件的 jdk 指定。2.注意 jvm 内存分配问题同上。3.注意pom文件中的 mysql 驱动的版本(大多数情况是没问题的)。如果是数据库配置时区问题 则在 nacos 连接数据库的 url 加 serverTimezoneAsia/Shanghai 如果发送请求时出现超时问题则可能是 mysql 驱动版本出现问题。四、Seata 的四种工作模式详解Seata 提供了四种不同的分布式事务解决方案分别是 XA 模式、AT 模式、TCC 模式和 SAGA 模式。每种模式都有其适用的场景和优缺点我们可以根据业务需求选择合适的模式。4.0 SpringBoot 集成 Seata在讲解 Seata 的模式前我们需要先在框架中集成 Seata这里以 SpringBoot 为例引入 Seata 相关依赖 (和你下载的 seata 版本保持一致)dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-seata/artifactId exclusions !--版本较低1.3.0因此排除-- exclusion artifactIdseata-spring-boot-starter/artifactId groupIdio.seata/groupId /exclusion /exclusions /dependency !--seata starter 采用1.4.2版本-- dependency groupIdio.seata/groupId artifactIdseata-spring-boot-starter/artifactId version${seata.version}/version /dependencyapplication.yml 的配置目的是让微服务通过注册中心找到 seata 服务seata: registry: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: group: DEFAULT_GROUP application: seata-server username: nacos password: nacos tx-service-group: seata-demo # 事务名称根据这个获取TC服务的cluster名称 service: vgroup-mapping: # 事务组与TC服务cluster的映射关系 seata-demo: SH4.1 XA 模式强一致性的两阶段提交XA 模式是基于数据库原生的 XA 规范实现的强一致性事务模式。XA 规范是 X/Open 组织定义的分布式事务处理标准几乎所有主流的关系型数据库如 MySQL、Oracle、PostgreSQL都对 XA 规范提供了支持。4.1.1 XA 模式的执行流程XA 模式采用两阶段提交协议2PC整个过程分为准备阶段和提交 / 回滚阶段第一阶段准备阶段TM 向 TC 申请开启全局事务获取 XID。TM 调用各个微服务的接口。RM 向 TC 注册分支事务。RM 执行本地业务 SQL但不提交事务。RM 将本地事务的执行状态成功或失败上报给 TC。第二阶段提交 / 回滚阶段TC 检查所有分支事务的执行状态。如果所有分支事务都执行成功TC 向所有 RM 发送提交指令。RM 提交本地事务释放数据库资源。如果有任何一个分支事务执行失败TC 向所有 RM 发送回滚指令。RM 回滚本地事务释放数据库资源。4.1.2 XA 模式的代码实现要在 Seata 中使用 XA 模式非常简单只需要两步第一步修改 application.yml 文件开启 XA 模式seata: # 开启数据源代理的XA模式 >Service public class OrderServiceImpl implements OrderService { Autowired private OrderMapper orderMapper; Autowired private AccountFeignClient accountFeignClient; Autowired private StockFeignClient stockFeignClient; Override GlobalTransactional(rollbackFor Exception.class) public Long create(Order order) { // 1. 创建订单 orderMapper.insert(order); // 2. 扣减用户余额 accountFeignClient.decrease(order.getUserId(), order.getMoney()); // 3. 扣减商品库存 stockFeignClient.decrease(order.getProductId(), order.getQuantity()); return order.getId(); } }4.1.3 XA 模式的优缺点优点强一致性满足 ACID 原则所有分支事务要么全部提交要么全部回滚数据完全一致。无代码侵入Seata 已经完成了 XA 模式的自动装配开发者只需要添加一个注解即可使用不需要修改业务代码。数据库原生支持几乎所有主流的关系型数据库都支持 XA 规范实现简单。缺点性能差在第一阶段RM 执行完 SQL 后不提交事务会一直持有数据库锁直到第二阶段结束才释放。这会导致数据库资源被长时间占用并发性能差。依赖关系型数据库只能用于支持 XA 规范的关系型数据库无法用于非关系型数据库如 Redis、MongoDB。4.1.4 XA 模式的适用场景XA 模式适用于对数据一致性和隔离性有极高要求的业务场景例如银行转账、金融交易等。但由于其性能较差不适合高并发的互联网业务。4.2 AT 模式高性能的最终一致性模式默认推荐AT 模式是 Seata 的默认模式也是最常用的模式。它同样采用两阶段提交协议但弥补了 XA 模式中资源锁定周期过长的缺陷实现了高性能的最终一致性。4.2.1 AT 模式的执行流程AT 模式的执行流程也分为两个阶段第一阶段TM 向 TC 申请开启全局事务获取 XID。TM 调用各个微服务的接口。RM 向 TC 注册分支事务。RM 执行业务 SQL并生成 Undo Log数据快照。RM 提交本地事务释放数据库资源。RM 将执行结果上报给 TC。第二阶段提交如果所有分支事务都执行成功TC 向所有 RM 发送提交指令。RM 只需要删除 Undo Log 即可不需要执行其他操作。第二阶段回滚如果有任何一个分支事务执行失败TC 向所有 RM 发送回滚指令。RM 根据 Undo Log 中的数据快照将数据恢复到更新前的状态。RM 删除 Undo Log。4.2.2 Undo Log 的结构Undo Log 是 AT 模式实现回滚的关键它记录了数据更新前后的快照。Undo Log 存储在每个微服务的数据库中表名为undo_log。undo_log表的结构如下CREATE TABLE undo_log ( id bigint(20) NOT NULL AUTO_INCREMENT, branch_id bigint(20) NOT NULL, xid varchar(100) NOT NULL, context varchar(128) NOT NULL, rollback_info longblob NOT NULL, log_status int(11) NOT NULL, log_created datetime NOT NULL, log_modified datetime NOT NULL, ext varchar(100) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY ux_undo_log (xid,branch_id) ) ENGINEInnoDB DEFAULT CHARSETutf8;其中rollback_info字段存储了数据更新前后的二进制快照。当需要回滚时Seata 会解析这个字段生成反向的 SQL 语句将数据恢复到原来的状态。4.2.3 AT 模式的脏写问题与全局锁AT 模式在第一阶段就提交了本地事务释放了数据库锁。这会带来一个问题如果两个全局事务同时修改同一行数据就可能出现脏写。例如全局事务 1 修改账户 A 的余额从 100 减到 90提交本地事务释放数据库锁。全局事务 2 修改账户 A 的余额从 90 减到 80提交本地事务释放数据库锁。全局事务 1 因为某个分支事务失败需要回滚。此时它会根据 Undo Log 将余额恢复到 100覆盖了全局事务 2 的修改。这就是 AT 模式的脏写问题。为了解决这个问题Seata 引入了全局锁机制。全局锁机制流程全局锁由 TC 维护它记录了当前正在操作某行数据的全局事务 XID。当一个全局事务要修改某行数据时必须先向 TC 申请获取该行数据的全局锁。只有获取到全局锁的事务才能执行修改操作。如果全局事务 1 已经获取了某行数据的全局锁那么全局事务 2 在申请全局锁时会失败它会不断重试直到全局事务 1 释放全局锁或者超时这里获取全局锁时会进行获取重试当达到一定时间时仍获取不到会释放全局锁并回滚之前的操作避免发生死锁。这样就保证了同一时间只有一个全局事务能够修改某行数据避免了脏写问题。另一个脏些问题当然可能还会出现一种情况一个不受全局锁控制的事务这时也来修改资源这个事务无需全局锁就可以修改资源也会发生脏写问题当然这种情况其实发生的概率很小具体原因如下1.全局事务的执行大多数情况是成功的二阶段是提交而不是回滚。2.分布式事务的耗时比较长(业务链路比较长)因此并发比较低不会出现同时操作资源。3.在实际业务中会尽量避免多个事务同时操作资源。但是也不能保证百分百避免seata 里其实有一个双快照的解决方案上述的案例的事务1在修改资源前 保存一次快照称为 before-image修改资源后再保存一次快照称为 after-image。如果事务1在二阶段要进行回滚它会拿 after-image 与数据库目前的资源情况进行比较如果一致说明之前没有其他事务(不受全局锁控制)对资源修改正常回滚如果不一致则说明有事务对资源进行了修改此时需要记录异常发送警告、人工介入。4.2.4 AT 模式的代码实现使用 AT 模式同样非常简单第一步在每个微服务的数据库中创建 undo_log 表执行上面给出的undo_log表创建 SQL。第二步修改 application.yml 文件开启 AT 模式seata: # 开启数据源代理的AT模式默认就是AT可以不写 >CREATE TABLE lock_table ( row_key varchar(128) NOT NULL, xid varchar(128) DEFAULT NULL, transaction_id bigint(20) DEFAULT NULL, branch_id bigint(20) NOT NULL, resource_id varchar(256) DEFAULT NULL, table_name varchar(32) DEFAULT NULL, pk varchar(36) DEFAULT NULL, gmt_create datetime DEFAULT NULL, gmt_modified datetime DEFAULT NULL, PRIMARY KEY (row_key), KEY idx_branch_id (branch_id) ) ENGINEInnoDB DEFAULT CHARSETutf8;第四步给业务入口方法添加 GlobalTransactional 注解和 XA 模式一样只需要在入口方法上添加GlobalTransactional注解即可。4.2.5 AT 模式的优缺点优点性能好一阶段完成就提交本地事务释放数据库资源锁持有时间短并发性能高。无代码侵入和 XA 模式一样开发者只需要添加一个注解即可使用不需要修改业务代码。读写隔离通过全局锁机制实现了读写隔离避免了脏写问题。缺点最终一致性两阶段之间属于软状态数据可能存在短暂的不一致最终才会达到一致。快照生成有性能开销生成 Undo Log 会有一定的性能开销但比 XA 模式的性能好很多。4.2.6 AT 模式的适用场景AT 模式适用于绝大多数基于关系型数据库的分布式事务场景尤其是高并发的互联网业务如电商下单、库存管理等。它是 Seata 推荐的默认模式。4.3 TCC 模式细粒度控制的补偿模式TCCTry-Confirm-Cancel模式是一种手动实现补偿的模式它将一个分布式事务拆分为三个阶段Try 阶段、Confirm 阶段和 Cancel 阶段。TCC 模式通过业务逻辑层定义操作和补偿为分布式事务提供了更细粒度的资源控制能力。4.3.1 TCC 模式的三个阶段Try 阶段资源检测和预留。检查业务参数和状态锁定并预留后续业务逻辑执行所需的全部资源保证业务操作的前置条件满足。Confirm 阶段确认执行。使用 Try 阶段预留的资源真正执行业务逻辑完成最终的状态变更。该阶段必须保证操作的幂等性。Cancel 阶段取消回滚。当事务执行失败时释放 Try 阶段锁定的所有资源将数据恢复到事务开始前的初始状态。该阶段也必须保证操作的幂等性。4.3.2 TCC 模式的执行流程TM 向 TC 申请开启全局事务获取 XID。TM 调用各个微服务的 Try 接口。RM 执行 Try 阶段的逻辑预留资源并向 TC 注册分支事务。所有 Try 阶段执行完毕后TM 根据执行结果向 TC 发起全局提交或回滚。如果所有 Try 都成功TC 向所有 RM 发送 Confirm 指令RM 执行 Confirm 阶段的逻辑。如果有任何一个 Try 失败TC 向所有 RM 发送 Cancel 指令RM 执行 Cancel 阶段的逻辑。4.3.3 TCC 模式的常见问题与解决方案TCC 模式虽然提供了更细粒度的控制但也带来了一些复杂的问题需要开发者手动处理1. 幂等性问题Confirm 和 Cancel 接口可能会被 Seata 重复调用因此必须保证幂等性。也就是说无论调用多少次最终的结果都应该是一样的。解决方案在数据库中记录每个分支事务的执行状态每次调用 Confirm 或 Cancel 时先检查状态如果已经执行过就直接返回成功。2. 空回滚问题当某分支事务的 Try 阶段因为网络阻塞而没有执行时全局事务可能会因为超时而触发 Cancel 操作。此时Cancel 接口被调用但 Try 还没有执行这就是空回滚。解决方案在 Cancel 接口中先判断该分支事务是否已经执行了 Try 阶段。如果没有执行就直接返回成功不需要执行回滚逻辑。3. 业务悬挂问题对于已经空回滚的业务如果后续 Try 阶段的请求到达并执行了那么这个分支事务就永远不会有 Confirm 或 Cancel 操作了这就是业务悬挂。解决方案在 Try 接口中先判断该分支事务是否已经执行过 Cancel 操作。如果已经执行过就拒绝执行 Try 逻辑。4.3.4 TCC 模式的代码实现我们以账户服务扣减余额为例实现一个完整的 TCC 接口。第一步创建账户冻结表为了记录冻结金额和事务状态我们需要创建一个account_freeze_tbl表CREATE TABLE account_freeze_tbl ( xid varchar(128) NOT NULL, user_id varchar(255) DEFAULT NULL COMMENT 用户id, freeze_money int(11) unsigned DEFAULT 0 COMMENT 冻结金额, state int(1) DEFAULT NULL COMMENT 事务状态0:try1:confirm2:cancel, PRIMARY KEY (xid) USING BTREE ) ENGINEInnoDB DEFAULT CHARSETutf8 ROW_FORMATCOMPACT;第二步定义 TCC 接口LocalTCC public interface AccountTCCService { /** * Try阶段冻结用户余额 * param userId 用户id * param money 扣减金额 * return 是否成功 */ TwoPhaseBusinessAction(name accountTCC, commitMethod confirm, rollbackMethod cancel) boolean tryDecrease(BusinessActionContextParameter(paramName userId) String userId, BusinessActionContextParameter(paramName money) int money); /** * Confirm阶段确认扣减 * param context 事务上下文 * return 是否成功 */ boolean confirm(BusinessActionContext context); /** * Cancel阶段回滚扣减 * param context 事务上下文 * return 是否成功 */ boolean cancel(BusinessActionContext context); }第三步实现 TCC 接口Service public class AccountTCCServiceImpl implements AccountTCCService { Autowired private AccountMapper accountMapper; Autowired private AccountFreezeMapper freezeMapper; Override Transactional(rollbackFor Exception.class) public boolean tryDecrease(String userId, int money) { // 1. 获取XID String xid RootContext.getXID(); // 2. 避免业务悬挂判断是否已经执行过Cancel AccountFreeze freeze freezeMapper.selectById(xid); if (freeze ! null freeze.getState() 2) { // 已经执行过Cancel拒绝执行Try return false; } // 3. 检查可用余额是否充足 Account account accountMapper.selectById(userId); if (account.getMoney() money) { // 余额不足扣减失败 return false; } // 4. 扣减可用余额 accountMapper.decrease(userId, money); // 5. 记录冻结金额和事务状态 freeze new AccountFreeze(); freeze.setXid(xid); freeze.setUserId(userId); freeze.setFreezeMoney(money); freeze.setState(0); // 0: try freezeMapper.insert(freeze); return true; } Override Transactional(rollbackFor Exception.class) public boolean confirm(BusinessActionContext context) { // 1. 获取XID String xid context.getXid(); // 2. 幂等性判断如果已经执行过Confirm直接返回成功 AccountFreeze freeze freezeMapper.selectById(xid); if (freeze null || freeze.getState() 1) { return true; } // 3. 删除冻结记录 freezeMapper.deleteById(xid); return true; } Override Transactional(rollbackFor Exception.class) public boolean cancel(BusinessActionContext context) { // 1. 获取XID String xid context.getXid(); // 2. 幂等性判断如果已经执行过Cancel直接返回成功 AccountFreeze freeze freezeMapper.selectById(xid); if (freeze null || freeze.getState() 2) { return true; } // 3. 空回滚判断如果Try还没执行直接返回成功 if (freeze.getState() ! 0) { return true; } // 4. 恢复可用余额 accountMapper.increase(freeze.getUserId(), freeze.getFreezeMoney()); // 5. 更新冻结记录状态为Cancel freeze.setState(2); freezeMapper.updateById(freeze); return true; } }第四步在业务入口使用 TCC 服务Service public class OrderServiceImpl implements OrderService { Autowired private OrderMapper orderMapper; Autowired private AccountTCCService accountTCCService; Autowired private StockFeignClient stockFeignClient; Override GlobalTransactional(rollbackFor Exception.class) public Long create(Order order) { // 1. 创建订单 orderMapper.insert(order); // 2. 调用账户服务的TCC Try接口冻结余额 accountTCCService.tryDecrease(order.getUserId(), order.getMoney()); // 3. 扣减商品库存 stockFeignClient.decrease(order.getProductId(), order.getQuantity()); return order.getId(); } }4.3.5 TCC 模式的优缺点优点性能最好一阶段完成就提交本地事务释放数据库资源不需要生成快照也不需要全局锁性能最高。细粒度控制可以对每个资源进行精确的控制适合对性能要求极高的场景。不依赖数据库事务可以用于非关系型数据库如 Redis、MongoDB。缺点代码侵入性高需要开发者手动编写 Try、Confirm、Cancel 三个接口开发成本高。复杂度高需要手动处理幂等性、空回滚、业务悬挂等问题容易出错。最终一致性属于最终一致性事务。4.3.6 TCC 模式的适用场景TCC 模式适用于对性能要求极高、对数据一致性要求也很高的核心业务场景例如金融支付、交易系统等。它也适合需要使用非关系型数据库参与事务的场景。4.4 SAGA 模式长事务的解决方案SAGA 模式适用于处理长流程、长周期的分布式事务。它将分布式事务拆分为多个可独立提交的本地事务通过 “正向执行 反向补偿” 的机制保证数据最终一致性。4.4.1 SAGA 模式的执行流程SAGA 模式的执行逻辑非常简单正向执行按顺序依次执行每个本地事务 T1 → T2 → T3 → ... → Tn。反向恢复如果某一步骤 Ti 执行失败则按相反顺序执行补偿操作 C (i-1) → ... → C2 → C1撤销之前已经完成的步骤。例如一个订单全流程包括创建订单、扣减库存、支付、发货四个步骤执行 T1创建订单执行 T2扣减库存执行 T3支付失败执行 C2恢复库存执行 C1取消订单4.4.2 SAGA 模式的两种实现方式SAGA 模式有两种常见的实现方式编排式由一个中央协调者来控制整个事务的流程协调者调用各个服务的接口并根据执行结果决定下一步是继续执行还是执行补偿操作。合唱式(协同式)每个服务在完成自己的事务后发布一个事件其他服务监听这个事件并执行自己的事务。如果某个服务执行失败它会发布一个补偿事件其他服务监听这个事件并执行补偿操作。Seata 目前支持编排式的 SAGA 模式通过状态机来定义事务流程和补偿逻辑。4.4.3 SAGA 模式的优缺点优点性能好每个本地事务都独立提交释放数据库资源性能高。适合长事务可以处理流程很长、周期很长的事务例如包含人工审核的业务流程。无锁不需要全局锁并发性能高。缺点代码侵入性高需要为每个业务步骤编写对应的正向操作和反向补偿操作开发成本高。无隔离性没有事务隔离机制可能会出现脏写、脏读等问题。复杂度高当事务流程很长时补偿逻辑会变得非常复杂难以维护。4.4.4 SAGA 模式的适用场景SAGA 模式适用于业务流程长、业务参与者多的场景例如订单全流程状态流转、金融跨行转账、包含人工审核 / 干预的长事务场景等。4.5 四种模式对比为了方便大家选择合适的模式我们将四种模式进行对比特性XA 模式AT 模式TCC 模式SAGA 模式一致性强一致最终一致最终一致最终一致隔离性完全隔离基于全局锁隔离基于资源预留隔离无隔离代码侵入无无有需编写三个接口有需编写状态机和补偿业务性能差好非常好非常好开发成本低低高高运维复杂度低中高高适用场景对一致性、隔离性有高要求的业务基于关系型数据库的大多数分布式事务场景对性能要求较高的事务有非关系型数据库参与的事务业务流程长、业务流程多参与者包含其它公司或遗留系统服务五、Seata 的高可用TC 服务作为 Seata 的核心服务必须保证高可用否则整个分布式事务系统将无法工作。Seata 支持基于 Nacos 的集群部署实现 TC 服务的高可用。TC 集群部署步骤准备多台服务器安装相同版本的 Seata TC。配置所有 TC 服务使用同一个数据库存储事务信息。配置所有 TC 服务注册到同一个 Nacos 注册中心使用相同的服务名称。微服务配置文件中不需要修改Seata 会自动从 Nacos 中发现所有可用的 TC 服务并进行负载均衡。常见问题1. MySQL 驱动版本问题Seata 对 MySQL 驱动版本有一定的要求如果驱动版本不兼容可能会出现各种奇怪的问题。建议使用 5.1.47 或 8.0.20 以上版本的 MySQL 驱动。2. 时区问题如果数据库和应用服务器的时区不一致可能会导致事务超时、日志时间错误等问题。建议在数据库连接 URL 中添加serverTimezoneAsia/Shanghai参数指定时区为东八区。3. 事务超时问题Seata 默认的事务超时时间是 60 秒如果业务执行时间超过 60 秒事务会被自动回滚。可以通过修改 TC 服务的service.default.grouplist配置来调整超时时间。4. 幂等性问题无论是哪种模式都必须保证业务接口的幂等性。因为网络问题Seata 可能会重复调用同一个接口如果接口不幂等就会导致数据重复操作。5. 数据一致性校验即使使用了 Seata也建议定期进行数据一致性校验特别是在系统出现异常或故障后。可以通过编写定时任务对比订单、库存、账户等数据确保数据一致。六、总结Seata 作为一款优秀的开源分布式事务解决方案为微服务架构下的数据一致性问题提供了完整的解决方案。它通过 TC、TM、RM 三个核心角色的协同工作实现了四种不同的事务模式能够满足各种业务场景的需求。在实际的项目中我们应该根据业务的特点和需求选择合适的事务模式对于大多数普通业务场景AT 模式是首选它无代码侵入、性能好、易于使用。对于对一致性要求极高的金融业务可以选择 XA 模式。对于对性能要求极高的核心业务可以选择 TCC 模式。对于长流程、长周期的业务可以选择 SAGA 模式。当然Seata 也不是银弹。在引入 Seata 之前我们应该仔细评估业务是否真的需要分布式事务。如果业务可以通过其他方式如本地事务 异步消息来保证最终一致性那么就不需要引入 Seata避免过度设计。随着微服务架构的不断普及分布式事务问题会越来越常见。Seata 作为目前最成熟、最实用的分布式事务解决方案之一将会在未来的微服务开发中发挥越来越重要的作用。
阅读完成 · 觉得有帮助?