1.Seata介绍
Seata 是⼀款开源的分布式事务解决⽅案,致⼒于提供⾼性能和简单易⽤的分布式事务服务. Seata 将为 ⽤⼾提供了 AT、TCC、SAGA 和 XA事务模式, 为用户打造了⼀站式的分布式解决⽅案.
Seata官网介绍
2. 什么是分布式事务
2.1 回顾事务
学习数据库的时候,接触过事务,就是把一组sql语句打包成一个整体,一起执行,要么所有都执行成功即成功,如果有一个sql失败,那么这一组sql整体就失败。
事务必须满⾜ACID特性: Atomicity (原⼦性), Consistency (⼀致性), Isolation (隔离性) 和 Durability (持久性)。
• Atomicity (原⼦性):⼀个事务中的所有操作, 要么全部成功, 要么全部失败, 不会出现只执⾏了 ⼀半的情况, 如果事务在执⾏过程中发⽣错误, 会回滚 ( Rollback ) 到事务开始前的状态, 就像这个事务从来没有执⾏过⼀样.
• Consistency (⼀致性):在事务开始之前和事务结束以后, 数据库的完整性不会被破坏. 这表⽰ 写⼊的数据必须完全符合所有的预设规则, 包括数据的精度、关联性以及关于事务执⾏过程中服务 器崩溃后如何恢复.
• Isolation (隔离性):数据库允许多个并发事务同时对数据进⾏读写和修改, 隔离性可以防⽌多 个事务并发执⾏时由于交叉执⾏⽽导致数据的不⼀致. 事务可以指定不同的隔离级别(读未提交,读已提交,不可重复读,序列化), 以权衡在不同 的应⽤场景下数据库性能和安全
• Durability (持久性):事务处理结束后, 对数据的修改将永久的写⼊存储介质, 即便系统故障也 不会丢失.
以上是针对单库多表的情况事务所要满⾜的特性
2.2 分布式事务
在微服务架构下, 随着业务服务的拆分及数据库的拆分会存在如下图所⽰的场景, 订单和库存分别拆分 成了两个独⽴的数据库, 当客⼾端发起⼀个下单操作时, 需要在订单服务对应的数据库中创建订单, 同时 需要调⽤库存服务完成商品库存的扣减.
分布式事务是指在分布式系统中, 为了保证数据的⼀致性和完整性, 对多个节点上的数据进⾏操作的事 务. 当⼀个事务涉及到多个不同的数据库、服务或应⽤实例时, 就构成了分布式事务.
3. 分布式事务问题演⽰
3.1 项⽬搭建
上图中项⽬架构有些复杂, 我们稍作修改, 也可以演⽰分布式事务的问题. ⽤⼾下订单后, 调⽤库存服务扣减库存和⽤⼾服务扣减余额.
3.2 问题演⽰
观察数据库表:此时库存表中编号2001的商品库存为150
我们创建一个请求,购买2001商品1000个(库存不足的情况下)
money修改为500
现在这个情况不符合事务的原子性。
4. 分布式事务问题的理论模型
分布式事务问题也叫分布式数据⼀致性问题. 简单来说就是如何在分布式场景中保证多个节点数据的⼀ 致性. 分布式事务产⽣的核⼼原因在于存储资源的分布性, ⽐如多个数据库, 或者MySQL和Redis两种不 同存储设备的数据⼀致性等. 在实际应⽤中, 我们应该尽可能地从设计层⾯去避免分布式事务的问题, 因 为任何⼀种解决⽅案都会增加系统的复杂度. 接下来我们了解⼀下分布式事务问题的常⻅解决⽅案. 在学习分布式事务解决⽅案之前, 我们需要先了解⼀些相关的基础理论
4.1 CAP理论
CAP 理论是分布式系统设计中最基础, 也是最为关键的理论。
•⼀致性(Consistency) CAP理论中的⼀致性, 指的是强⼀致性. 所有节点在同⼀时间具有相同的数据
•可⽤性(Availability) 保证每个请求都有响应(响应结果可能不对)。
•分区容错性(Partition Tolerance) 当出现⽹络分区后, 系统仍然能够对外提供服务。
CAP 理论告诉我们: ⼀个分布式系统不可能同时满⾜数据⼀致性, 服务可⽤性和分区容错性这三个基本 需求, 最多只能同时满⾜其中的两个.
在分布式系统中, 系统间的⽹络不能100%保证健康, 服务⼜必须对外保证服务. 因此Partition Tolerance不可避免. 那就只能在C和A中选择⼀个. 也就是CP或者AP架构
CP架构: 为了保证分布式系统对外的数据⼀致性, 于是选择不返回任何数据
AP架构: 为了保证分布式系统的可⽤性, 节点2返回V0版本的数据(即使这个数据不正确)
4.2 BASE理论
BASE理论是由于CAP中⼀致性和可⽤性不可兼得⽽衍⽣出来的⼀种新的思想, BASE理论的核⼼思想是 通过牺牲数据的强⼀致性来获得⾼可⽤性.
它有如下三个特性:
• Basically Available (基本可⽤): 分布式系统在出现故障时, 允许损失⼀部分功能的可⽤性, 保证核⼼功能的可⽤
• Soft state (软状态): 允许系统中的数据存在中间状态, 也就是允许系统中不同节点的数据副本 之间的同步存在延时, 这个状态不影响系统的可⽤性
• Eventually Consistent (最终⼀致性): 中间状态的数据在经过⼀段时间之后, 会达到⼀个最 终的数据⼀致性
BASE理论不要求数据的强⼀致, ⽽是允许数据在⼀段时间内是不⼀致的, 但是数据最终会在某个时间点 实现⼀致. 在互联⽹产品中, ⼤部分都会采⽤BASE理论来实现数据的⼀致, 因为产品的可⽤性对于用户来说更加重要.
与CAP理论的对⽐
CAP理论指出: ⼀个分布式系统不可能同时满⾜⼀致性 (C ) 、可⽤性 (A ) 和分区容错性 (P ) 这三个特性. BASE理论则是CAP理论的补充, 通过放宽对⼀致性的严格要求, 换取系统更⾼的可⽤性和灵活性. BASE理论的核⼼思想是: 如果不是必须的话, 不推荐使⽤事务或强⼀致性, ⿎励可⽤性和性能优先. 允许 在牺牲⼀定⼀致性的前提下获得更⾼的可⽤性.
4.3 X/Open 分布式事务模型
X/Open 是⼀个组织, X/Open DTP ( Distributed Transaction Process Reference Model) 是X/Open这个组织定义的⼀套分布式事务的标准. 这个标准提出了使⽤两阶段提交(2PC,Two-Phase-Commit) 来保证分布式事务的完整性.
这套标准主要定义了实现分布式事务的规范和API, 具体的实现则交给相应的⼚商来实现
相关⽂档:
DTP 参考模型:https://pubs.opengroup.org/onlinepubs/9294999599/toc.pdf
X/Open DTP参考模型包含三种⻆⾊:
• AP: Application, 应⽤程序.
• RM: Resource Manager, 资源管理器, ⽐如数据库. 应⽤程序可以通过资源管理器对相应的资源进⾏ 有效的控制
• TM: Transaction Manager, 事务管理器, ⼀般指事务协调者, 负责协调和管理各个⼦事务, 可以理解 为管理RM.
在分布式系统中, 会有多个节点, 每⼀个节点都能够明确的知道⾃⼰在进⾏事务操作过程中的结果是 成功或失败, 但⽆法直接获取到其他分布式节点的操作结果, 因此, 当⼀个事务操作需要跨越多个分布 式节点的时候, 为了保证事务处理的ACID特性, 就需要引⼊⼀个"协调者"的组件来统⼀调度所有分布 式节点的执⾏逻辑, 这些被调度的节点则称为"参与者", 协调者负责调度参与者的⾏为, 并最终决定这 些参与者是否要把事务真正进⾏提交. TM就是"协调者", RM就是"参与者"
上图简单介绍
1. 应⽤程序(AP) 通过资源管理器操作多个资源.
2. 应⽤程序通过TM提供的接⼝, 定义事务边界
3. TM和RM交换事务信息(执⾏成功或失败).
TM和RM之间的事务控制, 是基于XA协议来完成的, 并且提出了分布式事务的规范⸺XA协议, 该协议 主要定义了 (全局 ) 事务管理器和 (局部 ) 资源管理器之间的接⼝
X/Open DTP模型的执⾏流程:
1. 配置TM, 把多个RM注册到TM
2. AP从TM管理的RM中获取连接, ⽐如JDBC连接
3. AP向TM发起⼀个全局事务, ⽣成全局事务ID(XID), XID会通知各个RM
4. AP通过第⼆步获得的连接直接操作RM完成数据操作. AP在每次操作时会把XID传递给RM
5. AP结束全局事务, TM会通知各个RM全局事务结束. 根据各个RM的事务执⾏结果, 执⾏提交或者回滚 操作
4.4 两阶段提交
X/Open DTP 标准提出了使⽤两阶段提交(2PC, Two-Phase-Commit) 来保证分布式事务的完整性, 在上图中也有体现. TM对多个RM事务的管理, 就会涉及两个阶段的提交. 第⼀个阶段是事务的准备阶段, 第⼆个是事务的提交或者回滚阶段.
1. 准备阶段 (Prepare Phase )
◦ 协调者发送准备请求: 协调者向所有参与者发送 prepare 请求, 询问它们是否准备好提交事务. 这个请求包含了事务的详细信息,要求参与者对事务进⾏预处理,并准备好回滚或提交事务所需 的所有资源.
◦ 参与者响应准备请求: 参与者在收到 prepare 请求后, 会执⾏事务操作, 但不提交. 如果参与者 成功执⾏了事务操作, 它会将事务的执⾏结果和准备状态记录在本地⽇志中, 并向协调者发送 ready 消息, 表⽰已经准备好提交事务 .如果执⾏失败或⽆法准备, 则向协调者发送 abort 消 息.
2. 提交阶段 (Commit Phase )
◦ 协调者根据准备阶段的反馈进⾏决策:协调者收到所有参与者的响应后, 会根据反馈结果做出决 策. 如果所有参与者都返回 ready , 则协调者决定提交事务. 如果有任何⼀个参与者返回 abort , 则协调者决定回滚事务.
◦ 协调者发送提交或回滚请求:
▪ 提交事务:如果协调者决定提交事务, 它会向所有参与者发送 commit 请求. 参与者在收到 commit 请求后, 会正式提交事务, 并释放所有资源, 然后向协调者发送 ack 消息, 表⽰事务 已成功提交.
▪ 回滚事务:如果协调者决定回滚事务, 它会向所有参与者发送 rollback 请求. 参与者在收 到 rollback 请求后, 会回滚事务, 并释放所有资源, 然后向协调者发送 ack 消息, 表⽰事 务已成功回滚.
两阶段提交把⼀个事务的处理过程分为准备和提交/回滚两个阶段, 采⽤简单的⽅式来解决分布式事务 的问题, 但是这个过程中, 存在以下缺点:
1. 阻塞问题: 两个阶段都是事务阻塞型的, 对于每⼀个指令都需要有明确的响应, 如果在这个过程中, TM宕机或者⽹络出现故障, 则会⼀直处于阻塞状态. ⽐如第⼀阶段完成后TM宕机或⽹络出现故障了, 此时RM会⼀直阻塞, ⽆法进⾏其他操作. 所以3PC针对此问题, 加⼊了timeout机制.
2. 资源占⽤: 参与者在收到准备请求后, 会锁定相关资源以保证事务的原⼦性. 在整个两阶段提交过程 中, 这些资源⼀直被锁定, 直到事务提交或回滚完成. 这会导致资源利⽤率降低, 其他事务可能因⽆法 获取所需资源⽽等待
3. 数据不⼀致: 第⼆阶段中, TM向所有的RM发送commit请求, 由于局部⽹络异常, 导致只有⼀部分RM 收到了commit请求, 这些RM节点执⾏commit操作, 没有收到commit请求的节点由于事务⽆法提 交, 出现数据不⼀致的情况
相应也存在以下优点
• 保证事务的原⼦性:2PC通过两个阶段的严格控制, 确保了事务要么全部提交, 要么全部回滚, 从⽽保 证了事务的原⼦性.
• 实现相对简单:相⽐于其他分布式事务协议, 2PC的实现相对简单, 易于理解和实现
4.5 三阶段提交
3PC(Three-Phase-Commit), 是2PC的改进版本, 共分为 CanCommit , PreCommit 和 DoCommit 三个阶段.
1. CanCommit阶段
◦ 协调者发起请求: 协调者向所有参与者发送 CanCommit 请求, 询问它们是否可以执⾏事务提交 操作.此阶段不涉及实际的数据修改, 只是确认每个参与者是否有⾜够的资源和条件来完成事务.
◦ 参与者响应: 参与者根据⾃⾝情况返回Yes或No. 如果所有参与者都返回Yes, 则进⼊PreCommit 阶段.
2. PreCommit阶段
◦ 协调者发送PreCommit请求: 协调者向所有参与者发送 PreCommit 请求, 询问是否可以进⾏事 务的预提交操作.
◦ 参与者准备事务: 参与者执⾏事务操作, 并将事务执⾏结果和准备状态 (Yes/No ) 发送给协调者. 参与者会记录预提交⽇志, 并确保这些⽇志是持久化的.
◦ 协调者收集反馈并决策: 如果所有参与者都返回Yes, 则进⼊DoCommit阶段. 如果有任何⼀个参 与者返回No或超时未响应, 协调者会发送 abort 请求, 通知所有参与者回滚事务.
3. DoCommit阶段
◦ 协调者发送DoCommit请求: 协调者向所有参与者发送 DoCommit 请求, 指⽰它们正式提交事 务.
◦ 参与者执⾏提交: 参与者收到 DoCommit 请求后, 执⾏事务提交操作, 并向协调者发送 Ack 消 息, 表⽰事务已提交.
◦ 超时机制: 如果参与者在等待 DoCommit 请求时超时, 会默认执⾏提交操作.
优点
• 减少阻塞: 3PC通过引⼊超时机制, 减少了2PC中的阻塞问题. 避免了资源被永久锁定.
• 增强容错能⼒: 即使协调者在DoCommit阶段之前出现故障, 参与者也可以基于其预提交的状态⾃主决定继续提交或回滚事务, 从⽽减少了对协调者的依赖.
缺点
• 实现复杂度⾼:3PC的实现⽐2PC更复杂, 增加了系统的开发和维护成本
• 数据不⼀致⻛险:在某些情况下, 如⽹络分区, 参与者在收到 PreCommit 消息后, 如果⽹络出现故 障, 协调者和参与者⽆法进⾏后续通信, 参与者在超时后可能会⾃⾏提交事务, 导致数据不⼀致.
4.6 TCC事务
TCC (Try-Confirm-Cancel ) 是⼀种分布式事务解决⽅案, 是由Pat Helland在2007年发表的论⽂《Life beyond Distributed Transactions: An Apostate’s Opinion》中提出. TCC事务相对于传统两阶段, 其 特征在于它不依赖资源管理器(RM)对XA的⽀持, ⽽是通过对 (由业务系统提供的 ) 业务逻辑的接⼝调⽤ 来实现分布式事务.
TCC 通过将事务操作拆分为三个阶段:
1. Try阶段:尝试执⾏业务操作, 完成所有业务检查, 并预留必要的业务资源. 这个阶段不真正执⾏事 务, 只是进⾏资源的预占.
2. Confirm阶段: 如果所有参与者在Try阶段都成功, 那么进⼊Confirm阶段, 正式完成操作, 使⽤之前预 留的资源.
3. Cancel阶段:如果任何⼀个参与者在Try阶段失败, 那么进⼊Cancel阶段, 所有参与者回滚在Try阶 段执⾏的操作, 释放预留的资源
TCC事务属于两阶段提交思想的变体, 它在设计上借鉴了两阶段提交的核⼼理念, 第⼀阶段通过Try进⾏ 准备⼯作, 第⼆阶段Confirm/Cancel表⽰Try阶段操作的确认和回滚.
在主业务⽅法中, 会先调⽤业务服务对外提供的 Try ⽅法来做资源预留, 如果业务服务 Try ⽅法处理 都正常, TCC事务协调器就会调⽤ Confirm ⽅法对预留资源进⾏实际应⽤. 否则就会调⽤各个服务的 Cancel ⽅法进⾏回滚, 从⽽保证数据的⼀致性.
优点
• ⽆需依赖第三⽅中间件或数据库来实现分布式事务, 降低了系统复杂度和成本
• ⽆需锁定全局资源, 提⾼了系统的并发性能和可⽤性
• 适⽤于各种类型的业务场景, 只要能够定义出清晰的Try、Confirm和Cancel逻辑
缺点:
• 需要开发⼈员⼿动编写三个阶段的业务逻辑, 并保证其正确性和⼀致性, 增加了开发难度和维护成本
• 需要考虑各种异常情况和边界情况, 并提供相应的补偿策略和重试机制, 增加了系统复杂度和⻛险
5. 初识Seata
5.1 Seata术语介绍
• TC (Transaction Coordinator) - 事务协调者
维护全局和分⽀事务的状态, 驱动全局事务提交或回滚.
• TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务.
• RM (Resource Manager) - 资源管理器
管理分⽀事务处理的资源, 与TC交谈以注册分⽀事务和报告分⽀事务的状态, 并驱动分⽀事务提交或 回滚
5.2 Seata 下载和部署
1. Seata下载&解压
下载地址https://seata.apache.org/zh-cn/download/seata-server/
下载后解压得到以下文件:
• seata-namingserver: Seata 原⽣的注册中⼼
操作可参考https://seata.apache.org/zh-cn/docs/user/registry/namingserver/
• seata-server : Seata 的事务协调服务端, 负责全局事务的协调和管理
bin是运行脚本
• seata-server.sh: Linux系统下启动Seata服务的脚本. 通过执⾏该脚本并传⼊相应参数, 即可启动 Seata服务. 如 sh ./bin/seata-server.sh -m nio -p 8091
• seata-server.bat:Windows系统下启动Seata服务的脚本. 双击即可启动Seata服务.
conf: 配置⽂件
• ⽤于配置Seata服务的⼀些⾼级参数,如存储模式、数据库连接信息等
lib: 依赖库
script: ⼀些脚本⽂件.⽐如与配置中⼼相关的脚本, 数据库相关的脚本, 如建表语句等.
2. 修改配置
Seata⽀持多种配置中⼼
• nacos • consul • apollo • etcd • zookeeper • file (读本地⽂件, 包含conf、properties、yml配置⽂件的⽀持)
此处我们使⽤Nacos来作为Seata的配置中⼼
1修改配置中心
2修改注册中心,同上
3. 修改Seata存储模式
Server端存储模式(store.mode) ⽀持file, db, redis, raft
• file模式为单机模式, 全局事务会话信息内存中读写并异步(默认)持久化本地⽂件root.data, 性能较 ⾼;
• db模式为⾼可⽤模式, 全局事务会话信息通过db共享, 相应性能差些.
如果使⽤file模式, ⽆需改动, 直接启动即可。
使用db模式
根据地址找到文件mysql文件:apache-seata-2.2.0-incubating-bin\seata-server\script\server\db
创建一个数据库(命名都可以)把sql语句在数据库中运行,得到以下新建的数据库
修改store.mode
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: 123456 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: 50004. 启动Seata
window系统下
直接双击bin目录下的bat文件
访问 http://127.0.0.1:7091/, ⽤⼾名密码: seata/seata
Linux
1.把压缩包直接拖拽到服务器中。
2.新建一个文件夹用来存放seata解压好的文件。(可以不创建,但是有点乱解压后)
3.对压缩包进行解压:这个压缩包解压,存储到seata文件中
tar zxvf apache-seata-2.2.0-incubating-bin.tar.gz -C seata/
4.打开/seata/seata-server/conf这个目录:找到application。yml文件,修改这个文件中的配置,如同windows中配置的一样,配置配置中心,配置注册中心....
这里可以直接把window中配置好的,直接拿来用。
先用rm application.yml把这个文件删除
再把配置文件直接拉进来即可。
5通过vi application.yml命令打开配置文件配置nacos配置中心,把端口号改成127.0.0.1:10020
再配置nacos注册中心,把端口号改成127.0.0.1:10020,配置数据库,把账号改成Linux装上的数据库的账号密码。
6打开数据库,导入数据。
先查询建表的语句,再这个目录下/seata/seata-server/script/server/db
用pwd打印出当前路径,复制路径
通过这个命令再数据库中执行导入数据表
source /root/seata/seata-server/script/server/db/mysql.sql
7启动seata
通过这个路径找到启动文件:
/seata/seata-server/bin
通过这个命令启动seata服务,指定端口号为8091
bash seata-server.sh -p 8091
观察nacos,启动成功:上面的是linux中的服务,下面的是window中启动的服务。
但是发现Linux中的ip是内网IP,也就是局域网IP,这样的话就访问不通
解决这个问题就需要我们在启动的时候指定ip
1先杀掉现在启动的seata服务
查找进程
ps -ef|grep seata
杀掉进程
kill 进程号
再次启动
bash seata-server.sh -h 49.232.195.59 -p 8091
通过-h来指定IP地址
启动成功
5.3 微服务集成Seata
5.3.1 引⼊依赖
在需要的微服务中引⼊seata依赖.
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency>
5.3.2 修改配置⽂件
在application.yml, 添加seata相关配置
根据配置信息确定TC服务地址信息
seata: registry: #定义了Seata Server的注册中⼼配置, 微服务根据配置信息去注册中⼼获取tc服务地址 type: nacos #指定注册中⼼的类型 nacos: application: seata-server #Seata Server在Nacos中的应⽤名称 server-addr: 49.232.195.59:10020 #Nacos服务器地址 group: "SEATA_GROUP" #Seata Server在Nacos中的分组名称 namespace: "" #Nacos的命名空间, 设置为空, 表⽰使⽤默认的命名空间public tx-service-group: default_tx_group #定义事务服务组的名称 service: vgroup-mapping: default_tx_group: default6事务模式
6.1 XA模式
XA实现的原理是基于两阶段提交.
Seata 对原始的XA模式做了简单的封装和改造, 以适应⾃⼰的事务模型, 在 Seata 定义的分布式事务框 架内, 利⽤事务资源 (数据库、消息服务等 ) 对 XA 协议的⽀持, 以 XA 协议的机制来管理分⽀事务的⼀种 事务模式.
配置与使用
1. 在application.yml中配置seata的事务模式.
seata:
/>
开启服务
当输入错误信息的时候,没有执行任何数据库操作,下订单失败。
6.2 AT 模式
两阶段提交协议的演变:
一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
二阶段:
- 提交异步化,非常快速地完成。
- 回滚通过一阶段的回滚日志进行反向补偿
解释上图:一阶段中干了5件事情:
1:TM注册全局事务。
2:TM调用分支。
3:RM注册分支任务。
4:RM执行业务,真实的提交到了数据库,执行的过程中会记录更新快照,用来当回滚的时候回滚到没有执行任务之前。
5:RM执行完毕后报告分支事务的状态。
第二阶段:当所有的分支任务都执行完。
提交成功:所有的都而成功了,TC会通知RM清理log日志,也就是更新快照。完成了整个分布式事务的处理。
提交失败:如果有任意一个事务没有执行成功,那么TC就会通知RM进行回滚,RM会根据更新快照进行回滚事务,从而实现全局事务的回滚。
6写隔离
https://seata.apache.org/zh-cn/docs/dev/mode/at-mode
配置与使用
在微服务关联的数据库创建undo_log 表, 建表SQL参考
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;seata:
cellpadding="1" cellspacing="1" style="width:500px">
AT XA 实现⽅式 通过记录快照日志(undo_log表)来实现回滚的
几乎适用于任何的数据库,但是要在业务上创建undo_log表
依赖数据库对XA协议的⽀持通过XA规范来实现事务的提交和回滚.
不需要导入undo_log表,但要求数据库对协议的支持
⼀致性 最终一致性 强一致性 性能 性能较好,一阶段就提交了,不锁定资源 性能较差,一阶段锁定资源,等二阶段结束的时候才释放资源 数据库⽀持 适用于所有数据库 仅支持有XA协议的数据库 代码侵⼊ 无 无 使用场景 业务对性能要求较高推荐使用AT 要求强一致性,例如银行,对业务要求性能不高 6.3 TCC模式
TCC模式就是对TCC事务的实现
TCC设计
了解了TCC模式之后, 我们先来使⽤ Seata TCC 模式实现⼀个分布式事务.
业务操作分析
接⼊ TCC 前, 业务操作只需要⼀步就能完成, 但是在接⼊ TCC 之后, 需要考虑如何将其分成 2 阶段完成, 把资源的检查和预留放在⼀阶段的 Try 操作中进⾏, 把真正的业务操作的执⾏放在⼆阶段的 Confirm 操 作中进⾏.
用仓库存储为例:
• Try 操作:资源的检查和预留.
在扣库存场景下, Try 操作要做的事情就是先检查 A 商品库存是否⾜够, 再冻结要扣的 20 个 (预留资源 ) . 此阶段不会发⽣真正的扣库存.
• Confirm 操作:执⾏真正业务的提交.
在扣库存场景下, Confirm 阶段做的事情就是发⽣真正扣库存, 把A商品中已经冻结的 30 个库存扣掉.
• Cancel 操作:预留资源的是否释放.
在扣库存场景下, 扣库存操作取消, Cancel 操作执⾏的任务是释放 Try 操作冻结的 20个库存, 使 A 商品 回到初始状态.
并发情况下:
可以在并发的情况下顺利执行
允许空回滚
TCC服务(RM)没有执行try,却收到了Cancle的请求,就是空回滚。
空回滚操作的时候,要判断try有没有被执行。
防悬挂控制
一种情况:当执行一阶段的时候,事务管理器向资源管理器发送prepare请求,请求途中发生拥堵,这个时候事务管理器对资源管理器执行了空回滚操作,但是,空回滚操作后,perpare请求发送到了,然后执行try操作,这就发生了业务悬挂
用户在实现 TCC 服务时, 要允许空回滚, 但是要拒绝执⾏空回滚之后 Try 请求, 要避免出现悬挂.
解决这种场景就需要有一个日志:在执行空回滚的时候看看try有没有执行,在执行try的时候看看有没有发生空回滚
幂等控制
⽆论是⽹络数据包重传, 还是异常事务的补偿执⾏, 都会导致 TCC 服务的 Try、Confirm 或者 Cancel 操 作被重复执⾏,⽤⼾在实现 TCC 服务时, 需要考虑幂等控制, 即 Try、Confirm、Cancel 执⾏⼀次和执⾏ 多次的业务结果是⼀样的.
seata解决这些问题给出的解决方法
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 个值, 分别为:
a. tried(1) : 表⽰ Try 阶段已经执⾏过
b. committed(2): 表⽰⼆阶段 Commit 已经执⾏完成
c. rollbacked(3): 表⽰⼆阶段 Rollback 已经执⾏完成
d. suspended(4): 表⽰空回滚/悬挂/中⽌状态.
⼆阶段 Confirm/Cancel ⽅法执⾏后, 将状态改为 committed 或 rollbacked 状态. 当重复调⽤⼆阶段 Confirm/Cancel ⽅法时, 判断事务状态即可解决幂等问题
TCC实现
1添加数据表
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修改相应的实体类
Seata 会把一个 TCC 接口当成一个 Resource,也叫 TCC Resource。在业务接口中核心的注解是
@TwoPhaseBusinessAction,表示当前方法使用 TCC 模式管理事务提交,并标明了 Try,Confirm,Cancel 三个阶段。name属性,给当前事务注册了一个全局唯一的的 TCC bean name。同时 TCC 模式的三个执行阶段分别是:
- Try 阶段,预定操作资源(Prepare) 这一阶段所以执行的方法便是被
@TwoPhaseBusinessAction所修饰的方法。如示例代码中的prepare方法。- Confirm 阶段,执行主要业务逻辑(Commit) 这一阶段使用
commitMethod属性所指向的方法,来执行Confirm 的工作。- Cancel 阶段,事务回滚(Rollback) 这一阶段使用
rollbackMethod属性所指向的方法,来执行 Cancel 的工作。其次,可以在 TCC 模式下使用
BusinessActionContext在事务上下文中传递查询参数。如下属性:
xid全局事务idbranchId分支事务idactionName分支资源id,(resource id)actionContext业务传递的参数,可以通过@BusinessActionContextParameter来标注需要传递的参数。创建一个服务层的接口
package com.bite.storage.service; import io.seata.rm.tcc.api.BusinessActionContext; public interface StorageTccService { boolean prepare(String commodityCode, Integer count); boolean commit(BusinessActionContext actionContext); boolean rollback(BusinessActionContext actionContext); }实现这个接口
package com.bite.storage.service.impl; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.bite.storage.entity.StorageInfo; import com.bite.storage.mapper.StorageMapper; import com.bite.storage.service.StorageTccService; import io.seata.rm.tcc.api.BusinessActionContext; import io.seata.rm.tcc.api.BusinessActionContextParameter; import io.seata.rm.tcc.api.LocalTCC; import io.seata.rm.tcc.api.TwoPhaseBusinessAction; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service @Slf4j @LocalTCC public class StorageTccServiceImpl implements StorageTccService { @Autowired private StorageMapper storageMapper; //被该注解标识的方法,即为try阶段执行的方法 commit表示的是二阶段执行的方法 rollback表示的是二阶段指定的方法 //useTCCFence = true 加上这个配置项目seata就会自动处理空回滚,业务悬挂,幂等 @Override @TwoPhaseBusinessAction( name = "storageDeduct", commitMethod = "commit", rollbackMethod = "rollback",useTCCFence = true) public boolean prepare(@BusinessActionContextParameter("commodityCode")String commodityCode, @BusinessActionContextParameter("count")Integer count) { // @BusinessActionContextParameter这个注解就是让下面BusinessActionContext actionContext 拿到里面参数的值 log.info("一阶段try执行......."); try { //检查一下库存是否充足 if(cheakCount(commodityCode,count)){ UpdateWrapper<StorageInfo> updateWrapper = new UpdateWrapper<>(); updateWrapper.lambda().setSql("count = count - "+ count) .setSql("freeze_count=freeze_count+"+count) .eq(StorageInfo::getCommodityCode, commodityCode); storageMapper.update(updateWrapper); return true; }else { throw new RuntimeException("库存不足"); } } catch (Exception e) { log.error("扣减库存失败, e:", e); throw new RuntimeException("扣减库存失败!", e); } } @Override public boolean commit(BusinessActionContext actionContext) { log.info("二阶段commit执行......."); //进行冻结库存的扣减 String commodityCode=(String) actionContext.getActionContext("commodityCode"); Integer count=(Integer) actionContext.getActionContext("count"); UpdateWrapper<StorageInfo> updateWrapper = new UpdateWrapper<>(); updateWrapper.lambda() .setSql("freeze_count=freeze_count-"+count) .eq(StorageInfo::getCommodityCode, commodityCode); Integer result=storageMapper.update(updateWrapper); return result==1; } @Override public boolean rollback(BusinessActionContext actionContext) { log.info("二阶段rollback执行......."); //释放冻库存恢复原有的库存 String commodityCode=(String) actionContext.getActionContext("commodityCode"); Integer count=(Integer) actionContext.getActionContext("count"); UpdateWrapper<StorageInfo> updateWrapper = new UpdateWrapper<>(); updateWrapper.lambda() .setSql("count = count +"+ count) .setSql("freeze_count=freeze_count-"+count) .eq(StorageInfo::getCommodityCode, commodityCode); Integer result=storageMapper.update(updateWrapper); return result==1; } private Boolean cheakCount(String commodityCode, Integer count) { QueryWrapper<StorageInfo> queryWrapper=new QueryWrapper<>(); queryWrapper.select().lambda().eq(StorageInfo::getCommodityCode, commodityCode); StorageInfo storageInfo = storageMapper.selectOne(queryWrapper); Integer count1 = storageInfo.getCount(); if(count>count1){ return false; } return true; } }在controller中调用这个服务
6.4Saga模式
Saga模式是seata提供的长事务解决方案,在业务流程中每一个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
使用场景:
业务流程长业务流程多
参与者包含其他公司或遗留服务,无法提供tcc模式要求的三个接口(就是一些老的服务,不能进行修改和维护的)同时没有任何分布式事务引入。
AT XA Tcc SAGA 实现⽅式 通过记录快照日志(undo_log表)来实现回滚的
几乎适用于任何的数据库,但是要在业务上创建undo_log表
依赖数据库对XA协议的⽀持通过XA规范来实现事务的提交和回滚.
不需要导入undo_log表,但要求数据库对协议的支持
开发人员手动实现,try,confirm,Cancel 事件驱动,每个事务包含正向操作和逆向补偿操作
失败时按顺序执行逆向补偿
⼀致性 最终一致性 强一致性 最终一致性(通过业务实现) 最终一致性 性能 性能较好,一阶段就提交了,不锁定资源 性能较差,一阶段锁定资源,等二阶段结束的时候才释放资源 较高,但开成本高(需要处理空回滚,业务悬挂等) 高(无锁)适合常事务 数据库⽀持 适用于所有数据库 仅支持有XA协议的数据库 不依赖底层数据库事务机制 不依赖底层数据的事务机制 代码侵⼊ 无 无 有,需要手动编写三个接口 有,需要手动编写状态机和补偿业务 使用场景 业务对性能要求较高推荐使用AT 要求强一致性,例如银行,对业务要求性能不高 高并发的场景,资金/库存 业务流程长业务流程多,参与者包含其他公司或遗留服务,无法提供tcc模式要求的三个接口(就是一些老的服务,不能进行修改和维护的)同时没有任何分布式事务引入