Seata 1.4.2

Seata是 2019 年 1 月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。致力于提供高性能和简单易用的分布式事务服务,为用户打造一站式的分布式解决方案。
官网地址:http://seata.io/

该版本是 1.4.2,后续版本都不需要专门启动TC服务,直接引入依赖即可

Seata 三大核心角色

1. TM 事务发起者:加了@GlobalTransactional 的服务,负责开启全局事务、生成 XID、通知 TC 提交/回滚。

2. RM 资源管理者:所有参与分布式事务的微服务,负责执行本地事务、上报分支状态、执行回滚。

3. TC 事务协调器:就是你启动的 Seata Server(独立服务,端口8091),全局事务的“总指挥”。

核心真理所有分布式事务的协调、记录、回滚、提交,全部由 TC 完成。

1779608218262

Seata 的两个核心配置文件

file.conf

控制 Seata Server(TC) 的事务数据持久化方式,注:仅当registry.conf 中的 config配置来源为 file 时生效。即:registry.conf 中的优先级高,若 registry.conf 中 config 设为 nacos 则优先使用 Nacos 配置,设为 file 才会读取本地 file.conf 配置。

## transaction log store, only used in seata-server
store {
  ## store mode: file、db、redis
  mode = "db"
  publicKey = ""
  file {
    dir = "sessionStore"
    maxBranchSessionSize = 16384
    maxGlobalSessionSize = 512
    fileWriteBufferCacheSize = 16384
    sessionReloadReadSize = 100
    flushDiskMode = async
  }

  ## database store property
  db {
    datasource = "druid"
    dbType = "mysql"
    driverClassName = "com.mysql.jdbc.Driver"
    url = "jdbc:mysql://127.0.0.1:3306/seata?rewriteBatchedStatements=true"
    user = "mysql"
    password = "mysql"
    minConn = 5
    maxConn = 100
    globalTable = "global_table"
    branchTable = "branch_table"
    lockTable = "lock_table"
    queryLimit = 100
    maxWait = 5000
  }

  ## redis store property
  redis {
    mode = "single"
    single {
      host = "127.0.0.1"
      port = "6379"
    }
    password = ""
    database = "0"
    minConn = 1
    maxConn = 10
    maxTotal = 100
    queryLimit = 100
  }
}

核心参数:store.mode 只有三种:file / db / redis

  • store.mode=file(默认、本地开发用)
    • 无需数据库、无需建任何表
    • 事务数据存在本地文件
    • 重启 Seata 事务数据丢失
    • 适合:本地测试
  • store.mode=db(生产、集群用)
    • 必须连接 MySQL
    • 必须在 对应seata 库执行 3 张核心表:global_table、branch_table、lock_table
    • 持久化事务数据,重启不丢
    • 适合:上线、集群、正式环境

registry.conf

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"

  nacos {
    application = "seata-tc-server"
    serverAddr = "127.0.0.1:8848"
    group = "DEFAULT_GROUP"
    namespace = ""
    cluster = "syc"
    username = ""
    password = ""
  }
  file {
    name = "file.conf"
  }
   .......
}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "nacos"

  nacos {
    serverAddr = "127.0.0.1:8848"
    namespace = ""
    group = "DEFAULT_GROUP"
    username = ""
    password = ""
    dataId = "seataServer.properties"
  }
  file {
    name = "file.conf"
  }
    .......
}

Seata 去哪找、怎么被发现

分为两块,和业务微服务完全无关:

  • registry 注册:Seata Server 是否注册到注册中心(Nacos/ZK)
  • config 配置:Seata Server 去哪读取自身配置(type=file:读取本地的 file.conf 配置 、type=nacos:读取nacos 中对应的配置

核心参数:type = file / nacos

如果 type=file

seata 不注册到任何注册中心

Seata 客户端(你的微服务)不通过 Nacos 发现 Seata 服务端

微服务客户端必须硬写死 Seata 地址


#type=file
seata:
  registry:
    type: file
  # 配置方式 = file
  config:
    type: file
  tx-service-group: seata-demo
  # 重点!file 模式必须写死 TC 地址
  service:
    grouplist:
      default: 127.0.0.1:8091


#type=nacos
seata:
  registry:
    type: nacos
    nacos:
      # 通过nacos找对应 TC服务
      server-addr: 127.0.0.1:8848
      namespace: ""
      group: DEFAULT_GROUP
      application: seata-tc-server
      username: nacos
      password: nacos
  tx-service-group: seata-demo

核心原理(跨服务检测)

常用场景举例:订单服务(TM)调用库存服务(RM)、支付服务(RM)

第一步:TM 开启全局事务,向TC申请唯一XID

接口添加 @GlobalTransactional 后,全局事务自动开启:

  • 订单服务(TM)向 TC 发起请求,生成全局唯一 XID
  • TC 记录事务初始化状态,XID 绑定当前线程上下文,贯穿所有服务
  • 订单服务(TM角色)主动向 TC(127.0.0.1:8091) 发起TCP请求
  • TC 生成一个全局唯一的XID(全局事务唯一标识,贯穿所有服务)
  • TC 写入一条「全局事务初始化记录」(store.mode=file存文件,store.mode=db存global_table表)
  • XID 绑定当前全局事务,状态标记为:进行中
  • TM 拿到XID,绑定到当前线程上下文,全程携带

第二步:跨服务调用,XID自动透传(事务关联核心)

订单服务通过Feign调用下游服务时,Seata 拦截器自动完成 XID 透传:

  • 自动将线程中的 XID 放入请求头,传递给下游
  • 库存、支付服务自动提取 XID,纳入同一全局事务,成为分支 RM

核心关联逻辑:所有微服务共享同一个 XID,TC 以此判定所有服务属于同一个全局事务。

  • 拦截Feign请求,从当前线程上下文取出全局XID
  • 将XID放入HTTP请求请求头(Seata自定义请求头)
  • 携带XID调用下游微服务,无需手动传参

下游所有被调用的服务(库存、支付),接收请求后:

第三步:所有RM执行本地事务,同步上报TC

所有下游 RM 执行本地业务事务,遵循 AT 模式核心流程:

  • 执行SQL前生成 undo_log 回滚日志,留存数据快照
  • 执行并提交本地事务,同时直连 TC 上报分支执行状态
  • TC 记录分支信息与数据全局锁,防止并发脏写

整个事务上报、锁记录过程仅为 RM 与 TC 直连交互

  1. 执行本地SQL前,先查询数据库快照,生成undo_log回滚日志(存自身业务库)
  2. 执行本地业务事务,提交本地SQL
  3. 向TC上报:当前分支事务执行成功,TC在branch_table记录分支状态
  4. 同时TC在lock_table记录数据全局锁,防止其他事务脏写冲突

整个过程:所有RM(多个微服务)直接TCP直连TC8091端口,和Nacos无任何交互,Nacos仅负责Feign的服务地址发现。

第四步:TM收尾,TC统一全局决策(提交/回滚)

所有分支事务执行完毕后,由 TM 统一收尾,TC 全局决策:

  • 无异常:TM 通知 TC 全局提交,所有 RM 清理本地 undo_log,事务正常结束
  • 任意服务异常:TM 触发全局回滚,TC 通知所有 RM,通过 undo_log 还原数据

最终实现所有服务数据要么全部成功、要么全部回滚,保证分布式数据一致性。

  • 无异常场景:TM向TC发送「全局提交」指令,TC通知所有RM,删除各自undo_log日志,全局事务结束,数据永久生效
  • 任意服务异常/超时:只要有一个分支报错、抛出异常,TM感知后向TC发送「全局回滚」指令

TC收到回滚指令后,根据XID查询所有关联的分支事务,批量通知所有RM执行回滚

搭建TC服务

  1. 修改配置文件,注册到nacos
registry {
  # tc服务的注册中心类,这里选择nacos,也可以是eureka、zookeeper等
  # 
  type = "nacos"

  nacos {
    # seata tc 服务注册到 nacos的服务名称,可以自定义
    application = "seata-tc-server"
    serverAddr = "127.0.0.1:8848"
    group = "DEFAULT_GROUP"
    # 命名空间
    namespace = ""
    # 集群名称
    cluster = "syc" 
    username = ""
    password = ""
  }
}

config {
  # 读取tc服务端的配置文件的方式,这里是从nacos配置中心读取,这样如果tc是集群,可以共享配置
  type = "nacos"
  #  ---------------------------------------------------------------
  #  Seata 启动时会优先去 Nacos 配置中心 拉取配置(seataServer.properties),而不是读取本地的 .conf 文件中的配置
   #  ---------------------------------------------------------------
  # 配置nacos地址等信息
  nacos {
    serverAddr = "127.0.0.1:8848"
    namespace = ""
    group = "SEATA_GROUP"
    username = "nacos"
    password = "nacos"
    dataId = "seataServer.properties"
  }
  
  consul {
    serverAddr = "127.0.0.1:8500"
    aclToken = ""
  }
  apollo {
    appId = "seata-server"
    ## apolloConfigService will cover apolloMeta
    apolloMeta = "http://192.168.1.204:8801"
    apolloConfigService = "http://192.168.1.204:8080"
    namespace = "application"
    apolloAccesskeySecret = ""
    cluster = "seata"
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
    nodePath = "/seata/seata.properties"
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
  }
  file {
    name = "file.conf"
  }
}
  1. 在nacos中配置registry.conf中声明的 seata的配置文件seataServer.properties
# 数据存储方式,db代表数据库
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/sovzn?useUnicode=true&rewriteBatchedStatements=true&serverTimezone=Asia/Shanghai
store.db.user=root
store.db.password=root
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
# 事务、日志等配置
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000

# 客户端与服务端传输方式
transport.serialization=seata
transport.compressor=none
# 关闭metrics功能,提高性能
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
  1. 创建数据库表 global_table、branch_table、lock_table
  1. 运行seata-server.bat 启动即可,在nacos 的服务列表会看到名为seata-tc-server的服务

1780138738657

微服务集成Seata

引入依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    <exclusions>
        <!--版本较低,1.3.0,因此排除-->
        <exclusion>
            <artifactId>seata-spring-boot-starter</artifactId>
            <groupId>io.seata</groupId>
        </exclusion>
    </exclusions>
</dependency>
<!--seata starter 采用1.4.2版本-->
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>${seata.version}</version>
</dependency>

修改配置文件

需要修改每个微服务的application.yml文件,增加以下内容:

# 【注册中心配置】:微服务 TC 服务注册中心的配置,微服务根据这些信息去注册中心获取 TC 服务地址
seata:
  registry: 
    # 参考tc服务自己的registry.conf中的配置
    type: nacos
    nacos: # tc
      server-addr: 127.0.0.1:8848
      namespace: ""
      group: DEFAULT_GROUP
      # Seata 服务器(TC)在 Nacos 中注册的服务名。微服务会根据这个名字去 Nacos 里查找 Seata 服务器。
      application: seata-tc-server 
      # 对应的集群名称
      cluster: syc
      
  tx-service-group: seata-demo # 事务组名称 自定义
  # 不同的微服务需要参与到同一个分布式事务中(比如订单服务调用库存服务一起完成下单),那么它们所有服务的 tx-service-group(事务组名称)都必须配置成一模一样的。
      
# 【配置中心配置】:微服务外部配置中心,拉取Seata框架的全局运行参数,规范当前微服务调用TC服务 
  config:
    type: nacos  # 指定配置中心的类型为 Nacos
    nacos:
      server-addr: 127.0.0.1:8848  # Nacos 配置中心服务器的 IP 地址和端口
      username: nacos  
      password: nacos  
      group: DEFAULT_GROUP  # 指定配置所在的分组
      data-id: client.properties  # 指定在 Nacos 中存放 Seata 客户端详细配置的配置文件名

在nacos中配置client.properties

# 【注】事务组映射关系 将微服务yaml中定义的事务组seata-demo映射到集群 syc,即:该事务组的事物由 syc 集群里的 TC服务来调度执行
service.vgroupMapping.seata-demo=syc

service.enableDegrade=false
service.disableGlobalTransaction=false
# 与TC服务的通信配置
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=false
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
# RM配置
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=false
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
# TM配置
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000

# undo日志配置
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
client.log.exceptionRate=100

Seata 四种模式

seata:
  data-source-proxy-mode: AT

XA模式

(数据库原生协议模式)

  • 核心原理:基于数据库原生的 XA 协议实现经典的两阶段提交(2PC),Seata 仅作为事务协调器(TC)。

  • 执行流程:

    • 一阶段Prepare(准备阶段):各数据库执行本地 SQL 但不提交,锁定资源,并向 TC 报告准备状态。
    • 二阶段Commit/Rollback(提交/回滚阶段):若所有数据库准备成功,TC 通知全部提交并释放锁;若有失败,则通知全部回滚。
  • 特点:保证强一致性,对业务无侵入,但由于一阶段全程持有数据库锁,性能最差,且依赖数据库对 XA 协议的支持。

  • 适用场景:必须保证强一致性、服务少且事务短、全数据库支持 XA 的非高并发企业系统(如银行核心账务、证券交易、政府财务系统)。

如下代码, 扣用户余额或扣库存失败整体都会回滚

@Override
@GlobalTransactional
public Long create(Order order) {
    // 创建订单
    orderMapper.insert(order);
    try {
        // 扣用户余额 
        accountClient.deduct(order.getUserId(), order.getMoney());
        // 扣库存
        storageClient.deduct(order.getCommodityCode(), order.getCount());

    } catch (FeignException e) {
        log.error("下单失败,原因:{}", e.contentUTF8(), e);
        throw new RuntimeException(e.contentUTF8(), e);
    }
    return order.getId();
}

AT 模式

(Automatic Transaction,自动事务模式)

  • 核心原理:基于改进的两阶段提交模型,通过拦截业务 SQL,自动生成回滚日志(undo_log)和全局锁来实现自动补偿。
  • 执行流程:
    • 一阶段:执行业务 SQL直接提交 并生成前后镜像(undo_log表的 rollback_info字段),与业务数据在同一个本地事务中提交,随后释放本地锁和数据库连接。
    • 二阶段:若全局提交,则异步快速清理删除 undo_log数据;若全局回滚,则根据 undo_log 生成反向补偿 SQL 恢复数据。
  • 特点:对业务代码零侵入,实现简单,性能较好,但属于最终一致性。
  • 适用场景:单体改微服务、业务逻辑简单(如 CRUD 操作)、一致性要求较高且开发资源有限的场景(如电商订单创建、企业内部管理系统)。

在 Seata AT 模式中,事务的隔离性保证与传统单机数据库有所不同。AT 模式的核心设计思想是在性能与一致性之间寻找平衡,因此它通过全局锁(Global Lock)来实现写隔离,而读隔离则默认较弱,需要依赖业务层面的配合。

以下是 AT 模式写隔离与脏读机制的详细解析:

1. 写隔离(防止脏写)

脏写:

脏写场景还原
假设数据库初始数据为 money = 100,事务1和事务2并发修改该数据:
事务1:获取本地数据库锁,保存更新前快照(money: 100),执行 SQL 将 money 更新为 90,随后提交本地事务并释放数据库锁(此时数据库值为 90)。
事务2:获取本地数据库锁,保存更新前快照(money: 90),执行 SQL 将 money 更新为 80,随后提交本地事务并释放数据库锁(此时数据库值为 80)。
触发回滚:此时如果事务1所在的全局事务失败,需要回滚。事务1会根据自己记录的快照将数据恢复为 100。
脏写发生:事务2的更新(80)被事务1的回滚操作(100)直接覆盖,导致事务2的修改丢失,这就是脏写。

为了解决脏写问题 AT 模式通过全局锁来防止脏写,其核心逻辑是“先放数据库锁,再卡全局锁”。

获取锁:当全局事务需要操作数据库资源时,会向 lock_table 中插入一条锁记录。如果资源已被其他全局事务锁定,当前事务将进入等待重试状态,直到超时或资源被释放。

释放锁:在全局事务二阶段完成提交或回滚后,Seata 会自动清除 lock_table 中相应的锁记录。

lock_table 通过 xid(全局事务ID)和 branch_id(分支事务ID)维护了与 global_tablebranch_table 之间的关联。其核心字段包括

  • 机制原理:在一阶段本地事务提交前,资源管理器(RM)必须先向事务协调器(TC)申请该数据行的全局锁。如果拿不到全局锁,就不能提交本地事务。
  • 执行流程:
    1. 事务 T1 执行修改 SQL,获取本地数据库锁,执行完毕后在提交前获取全局锁,然后提交本地事务并释放本地锁(此时全局锁仍由 T1 持有)。
    2. 事务 T2 修改同一条数据,能正常获取本地锁并执行 SQL,但在提交前尝试获取全局锁时,发现锁被 T1 占用,T2 会进入等待重试状态。
    3. 如果 T1 全局提交,释放全局锁,T2 获取锁后继续提交;如果 T1 全局回滚,TC 会通知 T2 回滚,T2 根据 undo_log 撤销本地已提交的修改,从而避免脏写。
  • 本质:用轻量级的“全局锁”替代了传统 XA 模式中长时间持有的“重量级数据库锁”,在保证事务正确性的同时,最大化了数据库的并发效率。

2. 读隔离(防止脏读)

AT 模式的读隔离机制分为默认状态和升级状态。

  • 默认隔离级别:读未提交(Read Uncommitted)
    在 AT 模式下,一阶段本地事务提交后,数据在数据库中就已经真实可见,但此时全局事务可能尚未完成二阶段决议。因此,普通的 SELECT 语句可能会读到其他全局事务未最终提交的数据(即脏读)。Seata 默认采用此级别,是为了避免全局锁竞争影响系统吞吐量。
  • 升级为读已提交(Read Committed)
    如果在特定业务场景下(如资金操作)不允许读到中间态数据,可以通过在查询方法上添加 @GlobalLock 注解并结合 SELECT FOR UPDATE 语句来实现:
    • 机制原理SELECT FOR UPDATE 语句的执行会向 TC 申请全局锁。如果该数据的全局锁被其他事务持有,当前查询会释放本地锁并阻塞等待,直到全局锁释放(即前面的全局事务完成提交或回滚)后,才返回已提交的最新数据。
    • 注意事项@GlobalLock 必须结合 SELECT FOR UPDATE 使用才能生效。此外,这种悲观读锁方案会阻塞并发读,可能对性能产生影响,建议仅在强一致性要求的场景下使用。

与XA的区别

XA
  • 锁载体:数据库原生行锁
  • 持锁逻辑:一阶段 Prepare 加锁 → 全程持有 → 二阶段结束才释放
  • 本质:悲观长锁,锁跟着分布式事务走,跨网络、跨阶段一直占着
  • 效率:低,并发争抢热点数据时排队严重
AT
  • 数据库锁:本地短行锁,SQL 执行完、本地事务提交就立刻释放,持锁只有毫秒级,和普通本地事务一致
  • 全局排他标记:框架层面加的逻辑锁,只用来防多事务并发修改同一条数据(脏写),不阻塞数据库操作
  • 本质:数据库短锁 + 轻量逻辑排他,没有长阻塞
  • 效率:远高于 XA,适配高并发

TCC模式

(Try-Confirm-Cancel,预留确认取消模式)

  • 核心原理:一种业务层面的两阶段提交,需要开发者手动实现 Try、Confirm、Cancel 三个操作。

  • 执行流程

    • Try(一阶段):检查业务合法性并预留资源(如冻结库存、冻结金额),不真正扣减。
    • Confirm(二阶段):确认提交,真正执行业务操作(如扣减冻结库存)。
    • Cancel(二阶段):取消回滚,释放预留的资源(如解冻库存)。
  • 特点无全局锁,性能极高,但代码侵入性强,需要手动编写补偿逻辑,且需考虑幂等和空回滚等问题。

  • 适用场景:高并发核心业务、性能要求极高、跨数据库/中间件、需精细控制资源的场景(如电商秒杀、金融核心交易)。

一、空回滚(Empty Rollback)

  • 产生原因:当某分支事务的 Try 阶段因网络拥堵阻塞、服务宕机或发生异常而未能成功执行时,全局事务可能因超时而触发回滚。此时,事务协调器(TC)会向该分支发送 Cancel 指令。由于 Try 阶段根本没有执行,Cancel 阶段实际上并没有预留的资源可以释放,这就形成了“空回滚”。
  • 业务影响:如果 Cancel 方法没有识别出这是空回滚,直接执行了“释放资源”的逻辑(例如凭空增加了可用余额),就会导致数据不一致。
  • 解决思路:Cancel 接口在执行前,必须能够识别出 Try 阶段是否已经执行过。如果 Try 未执行,Cancel 应直接返回成功,不做任何业务操作。

二、业务悬挂(Suspension)

  • 产生原因:悬挂是空回滚的衍生问题。在发生空回滚后,如果之前因网络拥堵而阻塞的 Try 请求“迟到”了,并在 Cancel 执行之后才真正到达并执行成功(成功预留了资源)。此时,全局事务已经结束,后续再也不会有 Confirm 或 Cancel 来释放这部分被预留的资源,导致资源被永久“悬挂”。
  • 业务影响:被悬挂的资源(如冻结的库存或资金)无法被再次使用,造成业务故障。
  • 解决思路:Try 接口在执行前,必须检查该事务是否已经执行过 Cancel。如果 Cancel 已经执行过,Try 必须拒绝执行,从而避免资源悬挂。
示例 :余额扣除

冻结金额表

@Data
public class AccountFreeze {
    @TableId(type = IdType.INPUT)
    private String xid;          // 全局事务ID(主键!)
    private String userId;       // 用户ID
    private Integer freezeMoney; // 冻结金额
    private State state;         

    public enum State {
        TRY, CONFIRM, CANCEL
    }
}

TCC 接口

import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;

@LocalTCC // 跨服务必加
public interface AccountTCCService {

    @TwoPhaseBusinessAction(
        name = "deductAccountTCC",
        commitMethod = "confirm",
        rollbackMethod = "cancel"
    )
    void deduct(
        BusinessActionContext context,
        @BusinessActionContextParameter("userId") String userId,
        @BusinessActionContextParameter("money") int money
    );

    boolean confirm(BusinessActionContext ctx);
    boolean cancel(BusinessActionContext ctx);
}

TCC 实现类

import io.seata.core.context.RootContext;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;

@Service
public class AccountTCCServiceImpl implements AccountTCCService {

    @Resource
    private AccountMapper accountMapper;
    @Resource
    private AccountFreezeMapper freezeMapper;

    // ====================== Try 阶段 ======================
    @Override
    public void deduct(BusinessActionContext context, String userId, int money) {
        // 0. 获取全局事务 XID
        String xid = RootContext.getXID();

        // ========== 【关键】防悬挂:如果 CANCEL 已经执行,直接拒绝 ==========
        AccountFreeze freeze = freezeMapper.selectById(xid);
        if (freeze != null && freeze.getState() == AccountFreeze.State.CANCEL) {
            throw new RuntimeException("事务已回滚,拒绝执行Try");
        }

        // 1. 扣减可用余额,余额不足直接报错
        accountMapper.deduct(userId, money);

        // 2. 记录冻结记录,状态=TRY
        AccountFreeze accountFreeze = new AccountFreeze();
        accountFreeze.setXid(xid);
        accountFreeze.setUserId(userId);
        accountFreeze.setFreezeMoney(money);
        accountFreeze.setState(AccountFreeze.State.TRY);

        freezeMapper.insert(accountFreeze);
    }

    // ====================== Confirm 阶段 ======================
    @Override
    public boolean confirm(BusinessActionContext ctx) {
        String xid = ctx.getXid();

        // 幂等:已经删过了,直接返回成功
        AccountFreeze freeze = freezeMapper.selectById(xid);
        if (freeze == null) {
            return true;
        }

        // 删除冻结记录 = 提交成功
        int count = freezeMapper.deleteById(xid);
        return count == 1;
    }

    // ====================== Cancel 阶段 ======================
    @Override
    public boolean cancel(BusinessActionContext ctx) {
        String xid = ctx.getXid();
        AccountFreeze freeze = freezeMapper.selectById(xid);

        // ========== 【关键】空回滚:Try 没执行,直接返回成功 ==========
        if (freeze == null) {
            return true;
        }

        // 幂等:已经回滚过
        if (freeze.getState() == AccountFreeze.State.CANCEL) {
            return true;
        }

        // 1. 恢复余额
        accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());

        // 2. 冻结记录清零 + 状态改为 CANCEL
        freeze.setFreezeMoney(0);
        freeze.setState(AccountFreeze.State.CANCEL);
        int count = freezeMapper.updateById(freeze);

        return count == 1;
    }
}

调用方(订单服务)

@Service
public class OrderService {

    @Resource
    private OrderMapper orderMapper;
    @Resource
    private AccountFeignClient accountFeignClient;

    @GlobalTransactional // 开启全局事务
    public void createOrder(String userId, int money) {
        // 1. 创建订单
        orderMapper.create(userId, money);
        
        // 2. 跨服务扣余额
        accountFeignClient.deduct(userId, money);
    }
}

SAGA 模式

(长事务模式)

  • 核心原理:将长事务拆分为一系列本地短事务,每个短事务都有对应的补偿操作。如果某个环节失败,按相反顺序执行补偿操作。
  • 执行流程:
    • 正向执行:按业务顺序依次执行各个本地短事务。
    • 补偿执行:若某个短事务失败,从失败点开始反向执行各个补偿事务(需手动编写补偿)。
  • 特点:适合长事务、无锁高并发、允许最终一致性,但补偿逻辑可能较为复杂。
  • 适用场景:执行时间长(如大于1分钟)、涉及多个服务的复杂业务流程(如电商订单履约、银行跨行转账、企业工作流系统)。