Seata分布式事务解决方案

Seata

Seata是阿里巴巴开源的分布式事务解决方案,旨在为微服务架构提供高性能和简单易用的分布式事务服务。它的核心设计思想,是把一个复杂的分布式事务拆解为多个本地事务,并由一个中心化的协调者来统一管理。

Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

Seata核心角色

TC (Transaction Coordinator) - 事务协调者
  • 决策者

维护全局和分支事务的状态,协调全局事务提交或回滚。

TM (Transaction Manager) - 事务管理器
  • 执行者

定义全局事务的范围:开始全局事务、提交或回滚全局事务。

RM (Resource Manager) - 资源管理器
  • 管理本地事务

管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

架构图

Seata执行流程

简化流程:
  1. TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID。
  2. XID在微服务调用链路的上下文中传播。
  3. RM向TC注册分支事务,接着执行这个分支事务并提交(重点:RM在第一阶段就已经执行了本地事务提交/回滚),最后将执行结果汇报给TC。
  4. TM根据TC中所有的分支事务的执行情况,发起全局提交或回滚决议。
  5. TC调度XID下管辖的全部分支事务完成提交或回滚请求。

订单服务 调用 库存服务 完成下单扣库存为例:

第一步、预备动作:开启全局事务
  1. 入口请求到达 :用户请求进入订单服务的下单方法,该方法被 @GlobalTransactional 注解标记。
  2. TM 申请全局事务 :订单服务内置的 TM(事务管理器) 向独立的 TC(事务协调者) 申请开启一个全局事务。
  3. TC 颁发 XID :TC 生成一个全局唯一的 XID ,并返回给 TM。此时,全局事务状态机已就绪
第二步、第一阶段:分支事务执行与注册

核心特征 :每个本地事务执行完立即提交,不等待最终指令。资源(数据库连接、本地锁)在此阶段结束即释放。

步骤 2.1:订单服务执行本地事务
  1. 拦截 SQL :订单服务(此时扮演 RM )执行 INSERT INTO order ...
  2. 记录前镜像 :Seata 数据源代理先查询出准备修改的数据(若为插入则为空)。
  3. 执行并记录后镜像:执行插入 SQL,再查询插入后的数据行。
  4. 组装 UndoLog:将前镜像(空)、后镜像(新数据)组装成一条回滚日志。
  5. 同库事务提交
    • 核心原子操作 :将 业务数据UndoLog 日志 写入同一个本地数据库事务 中并立即提交
    • 结果:订单数据落库,数据库连接释放,本地行锁释放。
  6. 注册并上报 :RM 向 TC 注册分支事务(关联全局 XID),并报告"分支 1 执行成功"。
步骤 2.2:库存服务执行本地事务
  1. RPC 调用传递 XID :订单服务通过 Feign/Dubbo 调用库存服务,HTTP 请求头中携带了 XID
  2. 库存服务拦截请求:库存服务的 Seata 拦截器解析出 XID,将其绑定到当前线程。
  3. 执行 SQL :执行 UPDATE stock SET count = count - 1 ...
  4. 记录镜像
    • 前镜像 :查询当前库存数量(例如 100)。
    • 后镜像 :执行 SQL 后查询库存数量(例如 99)。
  5. 同库事务提交
    • UPDATE 语句和包含 100 -> 99 镜像的 UndoLog 在同一个事务中提交。
    • 数据库连接释放,该行锁释放。
  6. 注册并上报:库存 RM 向 TC 注册分支事务,并报告"分支 2 执行成功"。
第三步、第二阶段:全局决议与异步收尾

核心特征 :TC 根据所有分支结果通知 各 RM,RM 异步 执行收尾工作(删除日志或回滚),不阻塞主业务流程

场景 A:一切顺利(全局提交)
  1. TM 发起提交:订单服务方法执行结束,TM 告知 TC:"XID=123 可以全局提交了"。
  2. TC 通知 RM :TC 检索该 XID 下的所有分支,异步 向订单库 RM 和库存库 RM 发送 Commit 指令。
  3. RM 清理日志 :各 RM 收到指令后,仅做一件事 ------删除第一阶段写入的那条 UndoLog 记录。
  4. 流程结束:此时若删除日志失败也无妨,后台线程会定时清理过期 UndoLog,不影响业务数据正确性。
场景 B:库存不足(全局回滚)
  1. TM 发起回滚:库存服务抛出异常,订单服务捕获异常后,TM 告知 TC:"XID=123 需要全局回滚"。
  2. TC 通知 RM :TC 向订单库 RM 和库存库 RM 发送 Rollback 指令。
  3. RM 执行反向补偿 (以订单库为例):
    • RM 读取第一步写入的 UndoLog 记录。
    • 前镜像校验 :对比当前数据库数据与 UndoLog 中的后镜像 是否一致,防止脏写
    • 生成反向 SQL :根据前后镜像,生成一条 DELETE FROM order WHERE id = ... 语句。
    • 执行补偿并提交 :执行删除操作,提交本地事务,删除 UndoLog
  4. 结果:订单记录被物理删除,数据恢复如初。

Seata 事务解决方案

Seata 提供了四种模式来应对不同业务场景。你的选择,本质上是在"对代码的侵入程度 "和"对性能与灵活性的要求"之间做权衡。

模式 核心原理与流程 侵入性 一致性 性能 适用场景
AT 模式 基于本地事务,一阶段 执行业务SQL并记录前/后镜像undo_log表,随后立即提交本地事务;二阶段 全局提交时删除日志,回滚时根据镜像执行补偿 无侵入(代码零修改) 最终一致 较好 默认模式 ,适合大多数通用业务场景,如电商订单、库存管理
TCC 模式 手动编码 实现 Try(资源预留)、Confirm(确认提交)、Cancel(取消释放)三个阶段的逻辑 高侵入(需手写逻辑) 最终一致 很高 对性能和灵活性有极高要求的核心业务 ,如支付、账户余额操作
Saga 模式 将长事务拆分为一系列有序的本地子事务,每个子事务都有对应的补偿操作 。当某个环节失败时,会按相反顺序执行补偿来恢复状态 高侵入(需定义补偿逻辑) 最终一致 长流程、跨系统 的业务,如订单履约、调用无法改造的第三方服务
XA 模式 遵循 XA 协议的两阶段提交 :一阶段各分支事务不提交 并锁定资源,二阶段由 TC 统一指挥提交或回滚 无侵入 强一致性 较低 对数据一致性有最严格要求 的传统场景,如金融记账、基于 XA 协议的老系统迁移
AT 模式:默认的无侵入方案

AT 模式的核心是将资源锁定周期从全局事务的整个周期缩短到本地事务的执行瞬间 。它通过数据源代理拦截SQL,在本地事务中同时记录undo_log(包含数据修改前后的快照),并立即提交事务,从而释放数据库连接和本地锁。在第二阶段,Seata TC 会根据全局事务的最终决议,通知各RM异步删除 日志,或基于undo_log生成反向SQL进行数据补偿。

TCC 模式:高性能的手动补偿方案

TCC 模式将控制权完全交给开发者,通过编码实现三个核心阶段:

  • Try:检查并预留业务资源(如冻结库存)。

  • Confirm:执行真正的业务逻辑,确认提交。

  • Cancel:执行补偿逻辑,释放 Try 阶段预留的资源。

这种方案性能最高,但开发复杂,需自行处理接口的幂等性空回滚问题。

Saga 模式:长流程的异步事件方案

Saga 模式专为长流程、跨系统的场景设计。它将一个长事务拆解为多个独立的本地事务,并为每个正向操作定义一个逆向的补偿操作。当流程中某一步失败时,Saga 会驱动一个补偿流程,按相反顺序执行已成功节点的补偿操作,使系统恢复原状。它天然支持异步和事件驱动,吞吐量高。

XA 模式:传统强一致方案

XA 模式是传统的两阶段提交协议的实现,追求严格的 ACID 强一致性。其代价是在整个分布式事务期间,数据库资源(如数据行)会被持续锁定,导致并发性能显著下降。

SpringCloud整合Seata框架

第一步:部署TC协调者服务器。

Seata服务器下载地址:https://github.com/apache/incubator-seata/releases

1. 在MySql中创建Seata库

打开下载好的Seata压缩包,打开D:\cloud\seata-server-2.0.0\seata\script\server\db目录,执行目录下的mysql.sql

  • 我们需要提前创建好库seata
  • 执行完成脚本后,默认创建四个数据库。
sql 复制代码
global_table
branch_table
lock_table
distributed_lock
2. 在Nacos配置中心添加Seata外部配置文件

采用Nacos作为配置中心:

  • 在nacos的命名空间下,新建配置Data IDseata-server.propertiesGroupDEFAULT_GROUP(默认选项)的配置。

  • 打开文件D:\cloud\seata-server-2.0.0\seata\script\config-center\config.txt,复制配置到nacos。

  • 特点注意:

    • store.mode=file:file改为db
    • store.db.datasource=druid:druid改为hikari
yaml 复制代码
#使用数据库存储会话与锁信息
store.mode=db
store.lock.mode=db
store.session.mode=db
sql 复制代码
# 使用 HikariCP 高性能连接池,默认为druid
store.db.datasource=hikari
# mysql
store.db.dbType=mysql
# 驱动
store.db.driverClassName=com.mysql.jdbc.Driver
# 数据库地址
store.db.url=jdbc:mysql://192.168.58.129:3306/seata?useUnicode=true&rewriteBatchedStatements=true
# 数据库用户名
store.db.user=root
# 数据库密码
store.db.password=123456
3. 修改seata配置文件yaml,连接到Nacos配置和注册中心
配置中心

路径:D:\cloud\seata-server-2.0.0\seata\conf\application.yaml

  • 原始配置
yaml 复制代码
seata:  
  config:  
    # support: nacos, consul, apollo, zk, etcd3  
    type: file  
  registry:  
    # support: nacos, eureka, redis, zk, consul, etcd3, sofa  
    type: file  
  store:  
    # support: file 、 db 、 redis 、 raft    mode: file
yaml 复制代码
seata:  
  config:  
    # support: nacos, consul, apollo, zk, etcd3  
    type: nacos  
    nacos:  
      # nacos 配置中心地址  
      server-addr: 127.0.0.1:8848  
      # 名称空间  
      namespace: d0247d5f-d454-47cd-ad12-e59c70bd207c
      # 用户名  
      username:  
      # 密码  
      password:  
      context-path:  
      # 在nacos配置的文件名称  
      data-id: seataServer.properties
注册中心
  • 原始配置:
yaml 复制代码
registry:  
  # support: nacos, eureka, redis, zk, consul, etcd3, sofa  
  type: file
yaml 复制代码
registry:  
  # 注册中心nacos
  type: nacos  
  nacos:  
	# 我seata服务器名称
    application: seata-server  
    server-addr: 127.0.0.1:8848  
    # 分组需要与其他微服务分组保持一致  
    group: DEV-01  
    # 名称空间要保持一致
    namespace: d0247d5f-d454-47cd-ad12-e59c70bd207c  
    cluster: default  
    username:  
    password:
3. 启动服务

启动服务D:\cloud\seata-server-2.0.0\seata\bin\seata-server.bat

第二步:注册RM分支事务到TC服务器。

1. lqb-user模块添加Seata依赖项
  • 排除alibaab.seata自带Seata场景启动器。
pom 复制代码
<!-- seata:分布式事务-->  
<dependency>  
    <groupId>com.alibaba.cloud</groupId>  
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>  
    <exclusions>        
    <!--排除自带的场景启动器-->  
        <exclusion>  
            <groupId>io.seata</groupId>  
            <artifactId>seata-spring-boot-starter</artifactId>  
        </exclusion>    
    </exclusions>
</dependency>  

<!-- 手动导入依赖项 -->
<dependency>  
    <groupId>io.seata</groupId>  
    <artifactId>seata-spring-boot-starter</artifactId>  
    <version>2.0.0</version>  
</dependency>
2. 在Nacos中配置创建Seata配置文件
  • 我们当前是lqb_bank模块,因此模块名称事务为bank_tx_group
yaml 复制代码
seata:  
  registry:  
    type: nacos  
    nacos:  
      # 我们配置Seata TC协调者服务器的名称  
      application: seata-server  
      server-addr: localhost:8848  
      # 必须要再强调一遍,如果我们的cloud项目,配置了名称空间和分组  
      # 则必须同时在seata TC协调者服务器中设置  
      # 也必须在分支事务即当前模块配置  
      # 切记切记  
      namespace: d0247d5f-d454-47cd-ad12-e59c70bd207c  
      group: DEV-01  
  # 配置事务分组映射:我们当前是lqb_user模块,因此模块名称事务为user_tx_group  
  tx-service-group: user_tx_group  
  service:  
    vgroup-mapping:  
      user_tx_group: default  
  # AT模式
  data-source-proxy-mode: AT
  • 事务分组映射:对应的是Nacos中的seata-server.properties配置文件,即第一步中的2小节。
yaml 复制代码
  # 配置事务分组映射:必须与nacos中的seata-server.properties,保持一致 
	tx-service-group: default_tx_group  
	service:  
	  vgroup-mapping:  
		default_tx_group: default  
	data-source-proxy-mode: AT
  • tx-service-group (user_tx_group) :是你的分机名
  • vgroup-mapping :是公司的交换机(映射表)
  • default :是具体的办公室(TC 集群)

如果没有映射(直接连):

  • 你的代码里必须写:seata-server-address: 192.168.1.100:8091
    • 麻烦点:如果这台服务器挂了,或者你换了新服务器,你得去修改几十个微服务的配置文件,逐一重启。

有了映射(间接连):

  • 你的代码只写:tx-service-group: default_tx_group
    • 好处 :如果服务器换了,你只需要在 Nacos 配置中心改一下映射关系(把 default_tx_group 指向新的 cluster 名字),微服务不需要重启就能通过 Nacos 自动发现新的 TC 地址。
3. 加载nacos远程配置文件
yaml 复制代码
spring:  
  # spring下的config标签,与cloud同级  
  config:  
    # 使用import导入nacos配置  
    import:  
      # 导入分布式事务配置  
      - nacos:user_seata.yaml
  • 需要导入配置中心的依赖,和配置中心的地址。
4. 在业务数据库中创建undo_log表(注意:AT模式)
  • 我们使用了 Seata 的 AT 模式data-source-proxy-mode: AT)。
    该模式的工作原理是:在更新业务数据前,先将数据的"前后快照"保存到一张名为 undo_log 的表中,以便在事务失败时回滚。
  • 因此我们还需要在lqb_user模块业务lqb_user数据库中创建这张表undo_log
  • https://github.com/apache/incubator-seata/blob/2.x/script/client/at/db/mysql.sql
sql 复制代码
CREATE TABLE IF NOT EXISTS `undo_log`
(
    `branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(128) NOT NULL COMMENT 'global transaction id',
    `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
    `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
    `log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',
    `log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);
  • 如果你使用的是XA模式,则不需要创建该表。直接忽略该步即可。

第三步:业务代码添加注解。

  • 正常情况下@GlobalTransactional事务注解在Service层方法上加
  • 因为是笔记,所以加在了Controller层
java 复制代码
@GlobalTransactional  
@PutMapping("update/{id}")  
public User updateById(@PathVariable Long id) {  
    LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();  
    Wrapper<User> updateWrapper = wrapper.eq(User::getId, id);  
    wrapper.set(User::getUsername, RandomUtil.getPositiveInt() + id.toString());  
    int i= 1/0;  
    userService.update(updateWrapper);  
    return userService.getById(id);  
}
  • 查看执行情况:触发异常执行回滚

    Load [io.seata.rm.datasource.undo.UndoExecutorHolder] class fail: no service files found in 'META-INF/seata/'.
    xid 192.168.195.1:8091:6846238502089228298 branch 6846238502089228299, undo_log deleted with GlobalFinished
    branch rollback success, xid:192.168.195.1:8091:6846238502089228298, branchId:6846238502089228299
    Branch Rollbacked result: PhaseTwo_Rollbacked
    branch rollback result:BranchRollbackResponse{xid='192.168.195.1:8091:6846238502089228298', branchId=6846238502089228299, branchStatus=PhaseTwo_Rollbacked, resultCode=Success, msg='null'}
    write message:BranchRollbackResponse{xid='192.168.195.1:8091:6846238502089228298', branchId=6846238502089228299, branchStatus=PhaseTwo_Rollbacked, resultCode=Success, msg='null'}, channel:[id: 0x7a5b698d, L:/192.168.195.1:54566 - R:/192.168.195.1:8091],active?true,writable?true,isopen?true
    io.seata.core.rpc.netty.TmNettyRemotingClient@fca63a6 msgId:35, body:GlobalRollbackResponse{globalStatus=Rollbacked, resultCode=Success, msg='null'}
    transaction end, xid = 192.168.195.1:8091:6846238502089228298
    unbind 192.168.195.1:8091:6846238502089228298
    [192.168.195.1:8091:6846238502089228298] rollback status: Rollbacked
    Failed to complete request: java.lang.RuntimeException: try to proceed invocation error
    Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.RuntimeException: try to proceed invocation error] with root cause

相关推荐
zz0723208 天前
深入理解Seata:微服务分布式事务解决方案
微服务·seata·分布式事务
苏渡苇16 天前
Seata 番外篇:使用 docker-compose 部署 Seata Server(TC)及 K8S 部署 Seata 高可用
spring boot·docker·微服务·容器·kubernetes·seata·springcloud
梵得儿SHI2 个月前
SpringCloud 进阶拓展:分布式事务终极解决方案 Seata AT/TCC 模式全栈实战(含生产级避坑指南)
分布式·spring·spring cloud·seata·分布式事务·tcc·tc集群部署
zz0723202 个月前
Seata ——微服务分布式事务
分布式·微服务·架构·seata
都说名字长不会被发现2 个月前
Saga 补偿型分布式事务设计与实现
分布式事务·saga·tcc·事务性发件箱·订单与库存分布式事务
__土块__2 个月前
Java 大厂一面模拟:从本地缓存到分布式事务的连环追问
seata·分布式事务·caffeine·java面试·spring事务·本地缓存·大厂一面
sniper_fandc2 个月前
Spring Cloud系列—Seata分布式事务解决方案AT模式
spring cloud·seata
鬼先生_sir2 个月前
SpringCloud Seata 四大模式(AT/TCC/SAGA/XA)全解析
seata·springcloud·分布式事务
better_liang2 个月前
每日Java面试场景题知识点之-分布式事务
java·微服务·seata·分布式事务·一致性·saga·tcc