1.基本介绍

1.1.Seata

Seata 是⼀款开源的分布式事务解决⽅案, 致⼒于提供⾼性能和简单易⽤的分布式事务服务,Seata 将为

⽤⼾提供了AT、TCC、SAGA 和 XA 事务模式, 为⽤⼾打造⼀站式的分布式解决⽅案.

1.2.分布式事务

(1)事务

事务必须满⾜ACID特性: Atomicity (原⼦性), Consistency (⼀致性), Isolation (隔离性)和 Durability (持久性)

Atomicity (原⼦性):⼀个事务中的所有操作, 要么全部成功, 要么全部失败, 不会出现只执⾏了⼀半的情况, 如果事务在执⾏过程中发⽣错误, 会回滚 ( Rollback ) 到事务开始前的状态, 就像这个事务从来没有执⾏过⼀样 .

Consistency (⼀致性):在事务开始之前和事务结束以后, 数据库的完整性不会被破坏. 这表⽰写⼊的数据必须完全符合所有的预设规则, 包括数据的精度、关联性以及关于事务执⾏过程中服务器崩溃后如何恢复.

Isolation (隔离性):数据库允许多个并发事务同时对数据进⾏读写和修改, 隔离性可以防⽌多个事务并发执⾏时由于交叉执⾏⽽导致数据的不⼀致. 事务可以指定不同的隔离级别, 以权衡在不同的应⽤场景下数据库性能和安全 .

Durability (持久性):事务处理结束后, 对数据的修改将永久的写⼊存储介质, 即便系统故障也不会丢失.

(2)分布式事务

分布式事务是指在分布式系统中, 为了保证数据的⼀致性和完整性, 对多个节点上的数据进⾏操作的事务. 当⼀个事务涉及到多个不同的数据库、服务或应⽤实例时, 就构成了分布式事务.

2.分布式事务问题理论模型

分布式事务问题也叫分布式数据⼀致性问题. 简单来说就是如何在分布式场景中保证多个节点数据的⼀致性. 分布式事务产⽣的核⼼原因在于存储资源的分布性, ⽐如多个数据库, 或者MySQL和Redis两种不同存储设备的数据⼀致性等

2.1CAP理论

⼀致性(Consistency) :CAP理论中的⼀致性, 指的是强⼀致性. 所有节点在同⼀时间具有相同的数据

可⽤性(Availability) :保证每个请求都有响应(响应结果可能不对)

分区容错性(Partition Tolerance) :当出现⽹络分区后, 系统仍然能够对外提供服务

在分布式系统中, 系统间的⽹络不能100%保证健康, 服务⼜必须对外保证服务. 因此Partition Tolerance不可避免. 那就只能在C和A中选择⼀个. 也就是CP或者AP架构

2.2BASE理论

BASE理论是由于CAP中⼀致性和可⽤性不可兼得⽽衍⽣出来的⼀种新的思想, BASE理论的核⼼思想是通过牺牲数据的强⼀致性来获得⾼可⽤性

Basically Available (基本可⽤): 分布式系统在出现故障时, 允许损失⼀部分功能的可⽤性, 保证核⼼功能的可⽤

Soft state (软状态): 允许系统中的数据存在中间状态, 也就是允许系统中不同节点的数据副本之间的同步存在延时, 这个状态不影响系统的可⽤性

Eventually Consistent (最终⼀致性): 中间状态的数据在经过⼀段时间之后, 会达到⼀个最终的数据⼀致性

BASE理论不要求数据的强⼀致, ⽽是允许数据在⼀段时间内是不⼀致的, 但是数据最终会在某个时间点实现⼀致

2.3X/Open 分布式事务模型

(1)模型介绍

X/Open 是⼀个组织, X/Open DTP 是X/Open这个组织定义的⼀套分布式事务的标准. 这个标准提出了使⽤两阶段提交(2PC,Two-Phase-Commit) 来保证分布式事务的完整性

X/Open DTP参考模型包含三种⻆⾊:

AP(Application):应⽤程序.

RM(Resource Manager): 资源管理器, ⽐如数据库. 应⽤程序可以通过资源管理器对相应的资源进⾏有效的控制

TM(Transaction Manager):事务管理器, ⼀般指事务协调者, 负责协调和管理各个⼦事务, 可以理解

为管理RM

在分布式系统中, 会有多个节点, 每⼀个节点都能够明确的知道⾃⼰在进⾏事务操作过程中的结果是成功或失败, 但⽆法直接获取到其他分布式节点的操作结果, 因此, 当⼀个事务操作需要跨越多个分布式节点的时候, 为了保证事务处理的ACID特性, 就需要引⼊⼀个"协调者"的组件来统⼀调度所有分布式节点的执⾏逻辑, 这些被调度的节点则称为"参与者", 协调者负责调度参与者的⾏为, 并最终决定这些参与者是否要把事务真正进⾏提交.

TM就是"协调者", RM就是"参与者"

(2)执行流程

  • 配置TM, 把多个RM注册到TM
  • AP从TM管理的RM中获取连接, ⽐如JDBC连接
  • AP向TM发起⼀个全局事务, ⽣成全局事务ID(XID), XID会通知各个RM
  • AP通过第⼆步获得的连接直接操作RM完成数据操作. AP在每次操作时会把XID传递给RM
  • AP结束全局事务, TM会通知各个RM全局事务结束. 根据各个RM的事务执⾏结果, 执⾏提交或者回滚操作

2.4两阶段提交

X/Open DTP 标准提出了使⽤两阶段提交(2PC, Two-Phase-Commit) 来保证分布式事务的完整性, TM对多个RM事务的管理, 就会涉及两个阶段的提交. 第⼀个阶段是事务的准备阶段, 第⼆个是事务的提交或者回滚阶段.

(1)准备阶段

  • 协调者发送准备请求: 协调者向所有参与者发送 prepare 请求, 询问它们是否准备好提交事务. 这个请求包含了事务的详细信息, 要求参与者对事务进⾏预处理, 并准备好回滚或提交事务所需的所有资源.
  • 参与者响应准备请求: 参与者在收到 prepare 请求后, 会执⾏事务操作, 但不提交. 如果参与者成功执⾏了事务操作, 它会将事务的执⾏结果和准备状态记录在本地⽇志中, 并向协调者发送 ready 消息, 表⽰已经准备好提交事务 .如果执⾏失败或⽆法准备, 则向协调者发送 abort 消息.

(2)提交阶段

  • 协调者根据准备阶段的反馈进⾏决策:协调者收到所有参与者的响应后, 会根据反馈结果做出决策. 如果所有参与者都返回 ready , 则协调者决定提交事务. 如果有任何⼀个参与者返回abort , 则协调者决定回滚事务.
  • 协调者发送提交或回滚请求:

提交事务:如果协调者决定提交事务, 它会向所有参与者发送 commit 请求. 参与者在收到 commit 请求后, 会正式提交事务, 并释放所有资源, 然后向协调者发送 ack 消息, 表⽰事务已成功提交.

回滚事务:如果协调者决定回滚事务, 它会向所有参与者发送 rollback 请求. 参与者在收 到 rollback 请求后, 会回滚事务, 并释放所有资源, 然后向协调者发送 ack 消息, 表⽰事 务已成功回滚.

2.5三阶段提交

3PC,是2PC的改进版本, 共分为 CanCommit , PreCommit 和 DoCommit 三个阶段.

  • CanCommit阶段

协调者发起请求: 协调者向所有参与者发送 CanCommit 请求, 询问它们是否可以执⾏事务提交操作. 此阶段不涉及实际的数据修改, 只是确认每个参与者是否有⾜够的资源和条件来完成事务.

参与者响应: 参与者根据⾃⾝情况返回Yes或No. 如果所有参与者都返回Yes, 则进⼊PreCommit阶段.

  • PreCommit阶段

协调者发送PreCommit请求: 协调者向所有参与者发送 PreCommit 请求, 询问是否可以进⾏事务的预提交操作.

参与者准备事务: 参与者执⾏事务操作, 并将事务执⾏结果和准备状态 (Yes/No ) 发送给协调者. 参与者会记录预提交⽇志, 并确保这些⽇志是持久化的.

协调者收集反馈并决策: 如果所有参与者都返回Yes, 则进⼊DoCommit阶段. 如果有任何⼀个参与者返回No或超时未响应, 协调者会发送 abort 请求, 通知所有参与者回滚事务.

  • DoCommit阶段

协调者发送DoCommit请求: 协调者向所有参与者发送 DoCommit 请求, 指⽰它们正式提交事务.

参与者执⾏提交: 参与者收到 DoCommit 请求后, 执⾏事务提交操作, 并向协调者发送 Ack 消息, 表⽰事务已提交.

超时机制: 如果参与者在等待 DoCommit 请求时超时, 会默认执⾏提交操作.

2.6TCC事务

TCC (Try-Confirm-Cancel ) 是⼀种分布式事务解决⽅案, TCC事务相对于传统两阶段, 其特征在于它不依赖资源管理器(RM)对XA的⽀持, ⽽是通过对 (由业务系统提供的 ) 业务逻辑的接⼝调⽤来实现分布式事务.

TCC 通过将事务操作拆分为三个阶段:

  • Try阶段:尝试执⾏业务操作, 完成所有业务检查, 并预留必要的业务资源. 这个阶段不真正执⾏事务, 只是进⾏资源的预占.
  • Confirm阶段: 如果所有参与者在Try阶段都成功, 那么进⼊Confirm阶段, 正式完成操作, 使⽤之前预 留的资源.
  • Cancel阶段:如果任何⼀个参与者在Try阶段失败, 那么进⼊Cancel阶段, 所有参与者回滚在Try阶段执⾏的操作, 释放预留的资源.

3.Seata

3.1术语介绍

  • TC (Transaction Coordinator) - 事务协调者:维护全局和分⽀事务的状态, 驱动全局事务提交或回滚.
  • TM (Transaction Manager) - 事务管理器:定义全局事务的范围:开始全局事务、提交或回滚全局事务.
  • RM (Resource Manager) - 资源管理器 :管理分⽀事务处理的资源, 与TC交谈以注册分⽀事务和报告分⽀事务的状态, 并驱动分⽀事务提交或回滚

3.2下载部署

(1)下载并解压

下载地址:https://seata.apache.org/zh-cn/download/seata-server/

  • seata-namingserver: Seata 原⽣的注册中⼼
  • seata-server : Seata 的事务协调服务端, 负责全局事务的协调和管理

(2)修改配置

修改 /seata-server/conf/application.yml 中seata相关的配置

seata:
  config:
# support: nacos, consul, apollo, zk, etcd3
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      namespace: ''
      group: SEATA_GROUP
  registry:
  # support: nacos, eureka, redis, zk, consul, etcd3, sofa
    type: nacos
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP

(3)修改存储模式

Server端存储模式(store.mode) ⽀持file, db, redis, raft

  • file模式为单机模式, 全局事务会话信息内存中读写并异步(默认)持久化本地⽂件root.data, 性能较 ⾼;
  • db模式为⾼可⽤模式, 全局事务会话信息通过db共享, 相应性能差些

如果使⽤file模式, ⽆需改动, 直接启动即可, 下面讲解使⽤DB的启动步骤.

  • 初始化数据库

全局事务会话信息由3块内容构成, 全局事务–>分⽀事务–>全局锁,对应表global_table、branch_table、lock_table

CREATE DATABASE IF NOT EXISTS seata;

建表语句在: /seata-server/script/server/db/mysql.sql

  • 修改store.mode

修改 /seata-server/conf/application.yml 中 store.mode 相关的配置

配置内容参考: /seata-server/conf/application.example.yml ,将其db相关配置复制⾄application.yml,进⾏修改store.db相关属性

store:
# support: file 、 db 、 redis 、 raft
  mode: db
  db:
    datasource: druid
    db-type: mysql
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/seata?rewriteBatchedStatements=true
    user: root
    password: root
    min-conn: 10
    max-conn: 100
    global-table: global_table
    branch-table: branch_table
    lock-table: lock_table
    distributed-lock-table: distributed_lock
    vgroup-table: vgroup_table
    query-limit: 1000
    max-wait: 5000

(4)启动

windows

访问 http://127.0.0.1:7091/, ⽤⼾名密码: seata/seata

Linux

执⾏ /bin/seata-server.sh 并传⼊相应参数即可.

#解压⽂件到seata⽬录
tar zxvf apache-seata-2.2.0-incubating-bin.tar.gz -C ../seata
#启动seata, 端⼝号为8091
bash ./bin/seata-server.sh -h XX.XX.XX.XX -p 8091
#停⽌服务
bash ./bin/seata-server.sh stop

开通端⼝号7091, 8091

7091:⽤于客⼾端与 Seata Server 的 TCP 通信

8091:Seata Server的默认服务端端⼝, ⽤于接收客⼾端的事务请求并进⾏事务管理

3.3微服务集成Seata

(1)引入依赖

<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

(2)修改配置文件

seata:
  registry: #定义了Seata Server的注册中⼼配置, 微服务根据配置信息去注册中⼼获取tc服务地址
    type: nacos #指定注册中⼼的类型
    nacos:
      application: seata-server #Seata Server在Nacos中的应⽤名称
      server-addr: 47.108.157.13:8848 #Nacos服务器地址
      group : "SEATA_GROUP" #Seata Server在Nacos中的分组名称
      namespace: "" #Nacos的命名空间, 设置为空, 表⽰使⽤默认的命名空间public
  tx-service-group: default_tx_group #定义事务服务组的名称
  service:
    vgroup-mapping:
      default_tx_group: default

4.Seata各事务模式

4.1XA模式

4.1.1模式介绍

XA 规范 是 X/Open 组织定义的分布式事务处理标准. Seata XA模式是利⽤事务资源 (数据库、消息服务等 ) 对 XA 协议的⽀持, 以 XA 协议的机制来管理分⽀事务的⼀种事务模式.

Seata 对原始的XA模式做了简单的封装和改造, 以适应⾃⼰的事务模型, 在 Seata 定义的分布式事务框架内, 利⽤事务资源 (数据库、消息服务等 ) 对 XA 协议的⽀持, 以 XA 协议的机制来管理分⽀事务的⼀种事务模式.

  • 开启事务: 事务管理器 (TM ) 开启⼀个全局事务, 并与事务协调器 (TC ) 建⽴连接, TC返回⼀个全局事务ID (XID ) 给TM
  • 分⽀事务注册与执⾏:资源管理器 (RM ) 收到业务操作请求后, 会向TC注册分⽀事务, 执⾏业务SQL, 并携带XID以保证事务的⼀致性.
  • 分⽀事务状态报告:RM执⾏完分⽀事务后, 向TC报告分⽀事务的执⾏状态.
  • 事务提交或回滚决策:TM在所有分⽀事务执⾏完毕后, 会通知TC事务结束. TC接收到事务结束通知后, 会检查各分⽀事务的执⾏状态. 如果所有分⽀事务都成功, 则TC通知所有RM提交事务. 如果有任意⼀个分⽀事务失败, 则TC通知所有RM回滚事务.
  • 分⽀事务提交或回滚:RM接收到TC的提交或回滚指令后, 执⾏相应的commit或rollback操作.
4.1.2配置与使用

(1)在application.yml中配置seata的事务模式.

seata:
  data-source-proxy-mode: XA

(2)给发起全局事务的⼊⼝⽅法添加 @GlobalTransactional 注解

@Override
@GlobalTransactional
public Long create(OrderInfo orderInfo) {
try {
    //插⼊订单
    orderMapper.insert(orderInfo);
    //扣库存
    storageApi.deduct(orderInfo.getCommodityCode(), orderInfo.getCount());
    //扣余额
    accountApi.deduct(orderInfo.getUserId(), orderInfo.getMoney());
}catch (Exception e){
    log.error("下单失败, e: ", e);
    throw new RuntimeException("下单失败, e:", e);
}
return orderInfo.getId();
}
4.1.3优缺点

优点

  • 事务强⼀致性:XA模式能够满⾜ACID原则, 确保分布式事务的强⼀致性.
  • 实现简单且⽆代码侵⼊: 常⽤数据库都⽀持XA协议, 使⽤Seata的XA模式⽆需修改业务代码, 只需进⾏简单的配置即可.

缺点

  • 性能较差:⼀阶段需要锁定数据库资源, 等待⼆阶段结束才释放, 导致事务资源⻓时间得不到释放, 锁定周期⻓, 从⽽影响性能
  • 依赖关系型数据库: XA模式依赖数据库实现事务, 对于⼀些⾮关系型数据库或不⽀持XA协议的数据库, ⽆法使⽤.

4.2AT模式

4.2.1模式介绍

AT 模式是 Seata 创新的⼀种⾮侵⼊式的分布式事务解决⽅案. Seata 在内部做了对数据库操作的代理层, 我们使⽤ Seata AT 模式时, 实际上⽤的是 Seata ⾃带的数据源代理 DataSourceProxy, Seata 在这层代理中加⼊了很多逻辑, ⽐如插⼊回滚 undo_log ⽇志, 检查全局锁等.

Seata AT模式针对两阶段提交协议的演变:

  • ⼀阶段:业务数据和回滚⽇志记录在同⼀个本地事务中提交, 释放本地锁和连接资源.
  • ⼆阶段:提交异步化, ⾮常快速地完成. 回滚通过⼀阶段的回滚⽇志进⾏反向补偿.

⼀阶段:

  • 注册分⽀事务:TM注册全局事务, 资源管理器 (RM ) 向事务协调器 (TC ) 注册分⽀事务
  • 记录undo_log:RM在执⾏业务SQL操作前, 会先解析SQL语句, 记录SQL更新前的快照和更新后的快照到undo_log⽇志表中. undo_log记录了⾜够的信息, 以便在需要回滚时能够恢复数据.
  • 执⾏SQL并提交本地事务:RM执⾏业务SQL操作, 并直接提交本地事务, 此时数据会真实地提交到数据库中.
  • 报告事务状态:RM向TC报告分⽀事务的执⾏状态, 告知其本地事务已提交.

⼆阶段:

  • 提交成功:如果所有分⽀事务都成功, TC会通知RM清理undo_log相关的补偿信息, 完成整个分布式事务的处理.
  • 提交失败:如果有任意⼀个分⽀事务失败, TC会通知RM进⾏回滚. RM根据undo_log中的补偿信息对数据进⾏反向补偿, 从⽽实现事务的回滚.

Seata的AT模式解决思路就是引⼊全局锁的概念, 在释放本地锁之前, 先拿到全局锁, 避免同⼀时刻有另外⼀个事务来操作当前数据. ⼀阶段本地事务提交前, 需要确保先拿到 全局锁 . 拿不到 全局锁 , 不能提交本地事务. 拿全局锁0的尝试被限制在⼀定范围内, 超出范围将放弃, 并回滚本地事务, 释放本地锁

4.2.2读写隔离

(1)写隔离

在多线程并发操作同⼀个数据时, 有可能会出现脏写问题

Seata的AT模式解决思路就是引⼊全局锁的概念, 在释放本地锁之前, 先拿到全局锁, 避免同⼀时刻有另外⼀个事务来操作当前数据. ⼀阶段本地事务提交前, 需要确保先拿到 全局锁 . 拿不到全局锁 , 不能提交本地事务. 拿全局锁的尝试被限制在⼀定范围内, 超出范围将放弃, 并回滚本地事务, 释放本地锁.

举例说明:

两个全局事务 tx1 和 tx2, 分别对 a 表的 m 字段进⾏更新操作, m 的初始值 1000. tx1 先开始, 开启本地事务, 拿到本地锁, 更新操作 m = 1000 - 100 = 900. 本地事务提交前, 先拿到该记录的全局锁 , 本地提交释放本地锁. tx2 后开始, 开启本地事务, 拿到本地锁, 更新操作 m = 900 - 100 =800. 本地事务提交前, 尝试拿该记录的全局锁, tx1全局提交前, 该记录的全局锁被 tx1 持有, tx2 需要重试等待全局锁 .tx1 ⼆阶段全局提交, 释放 全局锁. tx2 拿到 全局锁提交本地事务

如果 tx1 的⼆阶段全局回滚, 则 tx1 需要重新获取该数据的本地锁, 进⾏反向补偿的更新操作, 实现分⽀的回滚. 此时, 如果 tx2 仍在等待该数据的 全局锁, 同时持有本地锁, 则 tx1 的分⽀回滚会失败. 分⽀的回滚会⼀ 直重试, 直到 tx2 的 全局锁 等锁超时, 放弃 全局锁 并回滚本地事务释放本地锁, tx1 的分⽀回滚最终成 功. 因为整个过程 全局锁 在 tx1 结束前⼀直是被 tx1 持有的, 所以不会发⽣ 脏写 的问题.

(2)读隔离

在数据库本地事务隔离级别 读已提交 (Read Committed) 或以上的基础上, Seata (AT 模式) 的默认全局隔离级别是读未提交 (Read Uncommitted) . 如果应⽤在特定场景下, 必需要求全局的读已提交 , ⽬前Seata 的⽅式是通过 SELECT FOR UPDATE 语句的代理.

SELECT FOR UPDATE 语句的执⾏会申请 全局锁 , 如果全局锁 被其他事务持有, 则释放本地锁 (回滚

SELECT FOR UPDATE 语句的本地执⾏ ) 并重试. 这个过程中, 查询是被 block 住的, 直到全局锁拿到,

即读取的相关数据是 已提交 的, 才返回. 出于总体性能上的考虑, Seata ⽬前的⽅案并没有对所有 SELECT 语句都进⾏代理, 仅针对 FOR UPDATE 的 SELECT 语句

4.2.3工作机制

(1)⼀阶段

  • 解析 SQL:得到 SQL 的类型 (UPDATE ) , 表 (product ) , 条件 (where name = ‘TXC’ ) 等相关的信息.
  • 查询前镜像:根据解析得到的条件信息, ⽣成查询语句, 定位数据.

select id, name, since from product where name = ‘TXC’;

  • 执⾏业务 SQL:更新这条记录的 name 为 ‘GTS’.
  • 查询后镜像:根据前镜像的结果, 通过主键定位数据

select id, name, since from product where id = 1;

  • 插⼊回滚⽇志:把前后镜像数据以及业务 SQL 相关的信息组成⼀条回滚⽇志记录, 插⼊UNDO_LOG 表中.
  • 申请 product 表中主键值等于1的记录的全局锁 .
  • 本地事务提交:业务数据的更新和前⾯步骤中⽣成的UNDO LOG⼀并提交.
  • 将本地事务提交的结果上报给TC
{
"branchId": 641789253,
"undoItems": [{
              "afterImage": {
              "rows": [{
           "fields": [{
           "name": "id",
           "type": 4,
           "value": 1
           }, {
           "name": "name",
           "type": 12,
           "value": "GTS"
           }, {
           "name": "since",
           "type": 12,
           "value": "2014"
           }]
         }],
              "tableName": "product"
              },
              "beforeImage": {
              "rows": [{
         "fields": [{
           "name": "id",
           "type": 4,
           "value": 1
           }, {
           "name": "name",
           "type": 12,
           "value": "TXC"
           }, {
           "name": "since",
           "type": 12,
           "value": "2014"
           }]
         }],
              "tableName": "product"
              },
              "sqlType": "UPDATE"
              }],
"xid": "xid:xxx"
}

(2)二阶段-回滚

  • 通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录.
  • 数据校验:拿 UNDO LOG 中的后镜与当前数据进⾏⽐较, 如果有不同, 说明数据被当前全局事务之 外的动作做了修改. 这种情况, 需要根据配置策略来做处理,
  • 根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息⽣成并执⾏回滚的语句:
  • 提交本地事务. 并把本地事务的执⾏结果 (即分⽀事务回滚的结果 ) 上报给TC

(3)二阶段-提交

  • 收到 TC 的分⽀提交请求, 把请求放⼊⼀个异步任务的队列中, ⻢上返回提交成功的结果给 TC.
  • 异步任务阶段的分⽀提交请求将异步和批量地删除相应 UNDO LOG 记录.
4.2.4配置与使用

(1)在微服务关联的数据库创建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`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

(2)配置事务模式

seata:
  data-source-proxy-mode: AT

4.3TCC模式

4.3.1模式介绍

TCC模式是Seata⽀持的⼀种由业务⽅细粒度控制的侵⼊式分布式事务解决⽅案, 是继 AT 模式后第⼆种⽀持的事务模式, 最早由蚂蚁⾦服贡献. 其分布式事务模型直接作⽤于服务层, 不依赖底层数据库, 可以灵活选择业务资源的锁定粒度, 减少资源锁持有时间, 可扩展性好, 可以说是为独⽴部署的 SOA 服务⽽设计的.

TCC 模式, 不依赖于底层数据资源的事务⽀持:

  • ⼀阶段 prepare ⾏为:调⽤ ⾃定义 的 prepare 逻辑.
  • ⼆阶段 commit ⾏为:调⽤ ⾃定义 的 commit 逻辑.
  • ⼆阶段 rollback ⾏为:调⽤ ⾃定义 的 rollback 逻辑.

所谓 TCC 模式, 是指⽀持把⾃定义的分⽀事务纳⼊到全局事务的管理中.

TCC 是⼀种⽐较成熟的分布式事务解决⽅案, 可⽤于解决跨数据库、跨服务业务操作的数据⼀致性问题. TCC 其 Try、Confirm、Cancel 3 个⽅法均由业务编码实现, 故 TCC 可以被称为是服务化的资源管理器.TCC 的 Try 操作作为⼀阶段, 负责资源的检查和预留. Confirm 操作作为⼆阶段提交操作, 执⾏真正的业务. Cancel 是⼆阶段回滚操作, 执⾏预留资源的取消, 使资源回到初始状态.

⽤⼾实现 TCC 服务之后, 该 TCC 服务将作为分布式事务的其中⼀个资源, 参与到整个分布式事务中. 事务管理器分 2 阶段协调 TCC 服务, 在第⼀阶段调⽤所有 TCC 服务的 Try ⽅法, 在第⼆阶段执⾏所有 TCC 服务的 Confirm 或者 Cancel ⽅法. 最终所有 TCC 服务要么全部都是提交的, 要么全部都是回滚的

4.3.2TCC设计

(1)业务分析

  • Try 操作:资源的检查和预留.

在扣库存场景下, Try 操作要做的事情就是先检查 A 商品库存是否⾜够, 再冻结要扣的 20 个 (预留资源 ) 此阶段不会发⽣真正的扣库存.

  • Confirm 操作:执⾏真正业务的提交.

在扣库存场景下, Confirm 阶段⾛的事情就是发⽣真正的扣库存, 把A商品中已经冻结的 30 个库存扣掉.

  • Cancel 操作:预留资源的是否释放.

在扣库存场景下, 扣库存操作取消, Cancel 操作执⾏的任务是释放 Try 操作冻结的 20个库存, 使 A 商品回到初始状态.

(2)并发控制

在⼀阶段 Try 操作中, 分布式事务 T1 和分布式事务 T2 分别冻结资⾦的那⼀部分资⾦, 相互之间⽆⼲扰.这样在分布式事务的⼆阶段, ⽆论 T1 是提交还是回滚, 都不会对 T2 产⽣影响, 这样 T1 和 T2 在同⼀笔业务数据上并⾏执⾏.

(3)允许空回滚

事务协调器在调⽤ TCC 服务的⼀阶段 Try 操作时, 可能会出现因为丢包⽽导致的⽹络超时, 此时事务管理器会触发⼆阶段回滚, 调⽤ TCC 服务的 Cancel 操作, ⽽ Cancel 操作调⽤未出现超时. TCC 服务在未收到 Try 请求的情况下收到 Cancel 请求, 这种场景被称为空回滚. 空回滚在⽣产环境经常出现, ⽤⼾在实现TCC服务时, 应允许空回滚的执⾏, 即收到空回滚时返回成功

(4)防悬挂控制

事务协调器在调⽤ TCC 服务的⼀阶段 Try 操作时, 可能会出现因⽹络拥堵⽽导致的超时, 此时事务管理器会触发⼆阶段回滚, 调⽤ TCC 服务的 Cancel 操作, Cancel 调⽤未超时. 在此之后, 拥堵在⽹络上的⼀阶段 Try 数据包被 TCC 服务收到, 出现了⼆阶段 Cancel 请求⽐⼀阶段 Try 请求先执⾏的情况, 此 TCC 服务在执⾏晚到的 Try 之后, 将永远不会再收到⼆阶段的 Confirm 或者 Cancel , 造成 TCC 服务悬挂.

⽤⼾在实现 TCC 服务时, 要允许空回滚, 但是要拒绝执⾏空回滚之后 Try 请求, 要避免出现悬挂.

(5)幂等控制

⽆论是⽹络数据包重传, 还是异常事务的补偿执⾏, 都会导致 TCC 服务的 Try、Confirm 或者 Cancel 操作被重复执⾏. ⽤⼾在实现 TCC 服务时, 需要考虑幂等控制, 即 Try、Confirm、Cancel 执⾏⼀次和执⾏多次的业务结果是⼀样的.

(6)解决方法

TCC 模式中存在的三⼤问题是幂等、悬挂和空回滚. 在 Seata1.5.1 版本中, 增加了⼀张事务控制表tcc_fence_log,包含事务的 XID 和 BranchID 信息,

CREATE TABLE IF NOT EXISTS `tcc_fence_log`
(
  `xid` VARCHAR(128) NOT NULL COMMENT 'global id',
  `branch_id` BIGINT NOT NULL COMMENT 'branch id',
  `action_name` VARCHAR(64) NOT NULL COMMENT 'action name',
  `status` TINYINT NOT NULL COMMENT
  'status(tried:1;committed:2;rollbacked:3;suspended:4)',
  `gmt_create` DATETIME(3) NOT NULL COMMENT 'create time',
  `gmt_modified` DATETIME(3) NOT NULL COMMENT 'update time',
  PRIMARY KEY (`xid`, `branch_id`),
  KEY `idx_gmt_modified` (`gmt_modified`),
  KEY `idx_status` (`status`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
  • 空回滚

在 Try ⽅法执⾏时插⼊⼀条记录, 表⽰⼀阶段执⾏了, 执⾏ Cancel ⽅法时读取这条记录, 如果记录不存在, 说明 Try ⽅法没有执⾏, 以此来避免空回滚

  • 悬挂

在 Rollback 阶段, 如果查询到事务控制表中没有记录, 说明Cancle先于Try执⾏了. 因此插⼊⼀条 status=4 状态的记录. 当Try阶段执⾏时, 判断status=4 , 则说明有⼆阶段 Cancel 已执⾏, 并返回 false以阻⽌⼀阶段 Try ⽅法执⾏成功

  • 幂等

在 TCC 事务控制表中增加⼀个记录状态的字段 status, 该字段有 4 个值, 分别为: tried(1) : 表⽰ Try 阶段已经执⾏过 , committed(2): 表⽰⼆阶段 Commit 已经执⾏完成 , rollbacked(3): 表⽰⼆阶段 Rollback 已经执⾏完成 ,suspended(4): 表⽰空回滚/悬挂/中⽌状态. ⼆阶段 Confirm/Cancel ⽅法执⾏后, 将状态改为 committed 或 rollbacked 状态. 当重复调⽤⼆阶段 Confirm/Cancel ⽅法时, 判断事务状态即可解决幂等问题

4.3.3TCC实现

(1)创建事务控制表

在业务数据库中执⾏以下SQL

CREATE TABLE IF NOT EXISTS `tcc_fence_log`
(
`xid` VARCHAR(128) NOT NULL COMMENT 'global id',
`branch_id` BIGINT NOT NULL COMMENT 'branch id',
`action_name` VARCHAR(64) NOT NULL COMMENT 'action name',
`status` TINYINT NOT NULL COMMENT
'status(tried:1;committed:2;rollbacked:3;suspended:4)',
`gmt_create` DATETIME(3) NOT NULL COMMENT 'create time',
`gmt_modified` DATETIME(3) NOT NULL COMMENT 'update time',
PRIMARY KEY (`xid`, `branch_id`),
KEY `idx_gmt_modified` (`gmt_modified`),
KEY `idx_status` (`status`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;

(2)添加冻结字段

ALTER TABLE storage_tbl ADD COLUMN freeze_count INT(11) unsigned DEFAULT 0
COMMENT '冻结库存';

(3)修改对应实体类

@Data
@TableName("storage_tbl")
public class StorageInfo {
    @TableId
    private Long id;
    private String commodityCode;
    private Integer count;
    private Integer freezeCount;
}

(4)TCC接口定义实现

TCC的Try、Confirm、Cancel⽅法都需要⾃⼰来实现.

public interface StorageTccService {
/**
* 扣减库存
*/
void deduct(String commodityCode, Integer count);
boolean confirm(BusinessActionContext context);
boolean cancel(BusinessActionContext context);
} 

接口实现

@Slf4j
@Service
@LocalTCC
public class StorageTccServiceImpl implements StorageTccService {
@Autowired
private StorageMapper storageMapper;
@Override
@TwoPhaseBusinessAction(name = "storageTccDeduct", commitMethod ="confirm",rollbackMethod = "cancel", useTCCFence = true)
public void deduct(@BusinessActionContextParameter("commodityCode")StringcommodityCode,
@BusinessActionContextParameter("count")Integer count)
{
log.info("⼀阶段try....");
//扣减可⽤库存, 记录冻结库存
try {
    UpdateWrapper<StorageInfo> updateWrapper = new UpdateWrapper<>();
    updateWrapper.lambda().setSql("count = count - "+ count)
    .setSql("freeze_count = freeze_count + "+ count)
    .eq(StorageInfo::getCommodityCode, commodityCode);
    storageMapper.update(updateWrapper);
    int a = 10/0;
} catch (Exception e) {
    log.error("扣减库存失败, e:", e);
    throw new RuntimeException("扣减库存失败!", e);
}
}
    
@Override
public boolean confirm(BusinessActionContext context) {
    log.info("⼆阶段Confirm....");
    //真实扣除冻结库存
    Integer count = (Integer) context.getActionContext("count");
    String commodityCode = (String)
    context.getActionContext("commodityCode");
    UpdateWrapper<StorageInfo> updateWrapper = new UpdateWrapper<>();
    updateWrapper.lambda().setSql("freeze_count = freeze_count - "+ count).eq(StorageInfo::getCommodityCode, commodityCode);
    int result = storageMapper.update(updateWrapper);
    return result==1;
}
@Override
public boolean cancel(BusinessActionContext context) {
    log.info("⼆阶段Cancel....");
    //恢复可⽤库存, 清除冻结库存
    Integer count = (Integer) context.getActionContext("count");
    String commodityCode = (String)
    context.getActionContext("commodityCode");
    UpdateWrapper<StorageInfo> updateWrapper = new UpdateWrapper<>();
    updateWrapper.lambda().setSql("count = count + "+ count)
    .setSql("freeze_count = freeze_count - "+ count)
    .eq(StorageInfo::getCommodityCode, commodityCode);
    int result = storageMapper.update(updateWrapper);
    return result == 1;
}
}

注解@LocalTCC要修饰在实现类上

注解@TwoPhaseBusinessAction要修饰在实现类⽅法prepare上, 可以通过 BusinessActionContext来传递参数

4.4Saga模式

Saga 模式是 SEATA 提供的⻓事务解决⽅案, 在 Saga 模式中, 业务流程中每个参与者都提交本地事务, 当出现某⼀个参与者失败则补偿前⾯已经成功的参与者, ⼀阶段正向服务和⼆阶段补偿服务都由业务开发实现.

Saga由⼀系列sub-transaction Ti组成, 每个Ti 都有对应的补偿动作Ci,补偿动作⽤于撤销T造 成的数据变更结果. 它和TCC相⽐, 少了Try这个预留动作, 每⼀个T操作都真实地影响到数据库.

4.5模式对比

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐