共计 5310 个字符,预计需要花费 14 分钟才能阅读完成。
Seata 是阿里开源的基于 Java 的分布式事务解决方案
AT,XA,TCC,Saga
Seata 提供四种模式解决分布式事务场景,AT,XA,TCC,Saga。简略叨咕叨咕我对这几种模式的了解
AT
这是 Seata 的一大特色,AT 对业务代码齐全无侵入性,应用非常简单,革新成本低。咱们只须要关注本人的业务 SQL,Seata 会通过剖析咱们业务 SQL,反向生成回滚数据
AT 蕴含两个阶段
- 一阶段,所有参加事务的分支,本地事务 Commit 业务数据和回滚日志(undoLog)
- 二阶段,事务协调者依据所有分支的状况,决定本次全局事务是 Commit 还是 Rollback(二阶段是齐全异步)
XA
也是咱们常说的二阶段提交,XA 要求数据库自身提供对标准和协定的反对。XA 用起来的话,也是对业务代码无侵入性的。
上述其余三种模式,都是属于 弥补型,无奈保障全局一致性。啥意思呢,例如刚刚说的 AT 模式,咱们是可能读到这一次分布式事务的中间状态,而 XA 模式不会。
弥补型 事务处理机制构建在 事务资源(数据库)之上(要么在中间件层面,要么在利用层面),事务资源 自身对分布式事务是无感知的,这也就导致了弥补型事务无奈做到真正的 全局一致性。
比方,一条库存记录,处在 弥补型 事务处理过程中,由 100 扣减为 50。此时,仓库管理员连贯数据库,查问统计库存,就看到以后的 50。之后,事务因为意外回滚,库存会被弥补回滚为 100。显然,仓库管理员查问统计到的 50 就是 脏 数据。
如果是 XA 的话,两头态数据库存 50 由数据库自身保障,不会被仓库管理员读到(当然隔离级别须要 读已提交 以上)
然而全局一致性带来的后果就是 数据的锁定(AT 模式也是存在全局锁的,然而隔离级别无奈保障,后边咱们会具体说),例如全局事务中有一条 update 语句,其余事务想要更新同一条数据的话,只能期待全局事务完结
传统 XA 模式是存在一些问题的,Seata 也是做了相干的优化,更多对于 Seata XA 的内容,传送门????http://seata.io/zh-cn/blog/se…
TCC
TCC 模式同样蕴含两个阶段
- Try 阶段:所有参加分布式事务的分支,对业务资源进行检查和预留
- 二阶段 Confirm:所有分支的 Try 全副胜利后,执行业务提交
- 二阶段 Cancel:勾销 Try 阶段预留的业务资源
比照 AT 或者 XA 模式来说,TCC 模式须要咱们本人形象并实现 Try,Confirm,Cancel 三个接口,编码量会大一些,然而因为事务的每一个阶段都由开发人员自行实现。而且相较于 AT 模式来说,缩小了 SQL 解析的过程,也没有全局锁的限度,所以 TCC 模式的性能是优于 AT、XA 模式。
PS:果然简略和高效难以两全的
Saga
Saga 是长事务解决方案,每个参与者须要实现事务的 正向操作和弥补操作。当参与者正向操作执行失败时,回滚本地事务的同时,会调用上一阶段的弥补操作,在业务失败时最终会使事务回到初始状态
Saga 与 TCC 相似,同样没有全局锁。因为相比短少锁定资源这一步,在某些适宜的场景,Saga 要比 TCC 实现起来更简略。
因为 Saga 和 TCC 都须要咱们手动编码实现,所以在开发时咱们须要参考一些设计上的标准,因为不是本文重点,这里就不多说了,能够参考分布式事务 Seata 及其三种模式详解
在咱们理解完四种分布式事务的原理之后,咱们回到本文重点 AT 模式
AT 如何应用
模仿需要:以下订单为例,在分布式的电商场景中,订单服务和库存服务可能是两个数据库
咱们先来看看 AT 模式下的代码是什么样的,这里疏忽了 Seata 的相干配置,只看业务局部
在须要开启分布式事务的办法上标记 @GlobalTransactional,而后执行别离执行扣减库存和扣减库存操作的,事务的参与者能够是本地的数据源,或者 RPC 的近程调用(近程调用的话须要携带全局事务 ID,也就是上图的 xid)
AT 一阶段
之前说过 AT 模式分为两个阶段,第一阶段包含提交业务数据和回滚日志(undoLog),第一阶段具体流程如下图
GlobalTransactional 切面
标记 @GlobalTransactional
的办法通过 AOP 实现了,开启全局事务和提交全局事务两个操作,与 Spring 事务机制相似,当 GlobalTransactionalInterceptor 在事务执行过程中捕捉到 Throwable 时,会发动全局事务回滚
0.1 步骤中会生成一个全局事务 ID
0.2 所有事务参与者执行完结后,一阶段事务提交
undoLog
咱们先来看看 Seata undoLog 的构造
// 省略了相干办法
public class SQLUndoLog {
// insert, update ...
private SQLType sqlType;
private String tableName;
private TableRecords beforeImage;
private TableRecords afterImage;
}
Seata 在执行业务 SQL 前后,会生成 beforeImage 和 afterImage,在须要回滚时,依据 SQLType,决定具体的回滚策略,例如 SQLType=update 时,将数据回滚到 beforeImage 的状态,如果 SQLType=insert,则依据 afterImage 删除数据
如 2.4 所示,每条业务 SQL,执行胜利后,会为这条 SQL 生成 LockKey,格局为tableName:PrimaryKey
注册分支事务
在 3.1 步骤注册分支事务时,client 会把所有的 LockKey 拼到一起作为 全局锁 发送给 Seata-server。如果注册胜利,写入 undoLog,并提交本地事务,一阶段完结,期待二阶段反馈
如果以后有其余分支事务曾经持有了雷同的锁(即其余事务也在解决雷同表的同一行),则 client 注册事务分支失败。client 会依据客户端定义的重发工夫和重发次数进行一直的尝试,如果重试完结依然没有取得锁,则一阶段失败,本地事务回滚。如果该全局事务存在曾经注册胜利分支事务,Seata-server 进行二阶段回滚
全局锁会在分支事务二阶段完结后开释
Seata 全局锁的设计是为了什么?
以扣减库存场景为例,TX1 实现库存扣减的一阶段,库存从 100 扣减为 99,正在期待二阶段的告诉。TX2 也要扣减同一商品的库存,如果没有全局锁的限度,TX2 库存从 99 扣减为 98,这时如果 TX1 接管到回滚告诉,进行回滚把库存从 98 回滚到 100。因为没有全局锁,造成了 脏写
AT 二阶段
二阶段是齐全异步化的并且齐全由 Seata 管制,Seata 依据所有事务参与者的提交状况决定二阶段如何解决
- 如果所有事务提交胜利,则二阶段的工作就是删除一阶段生成 的 undoLog,并开释 全局锁
- 如果局部事务参与者提交失败,则须要依据 undoLog 对曾经注册的事务分支进行回滚,并开释 全局锁
对 Seata 提出的疑难
至此咱们曾经初步理解了 Seata 的 AT 模式是如何实现的了
如果你也和我一样,认真思考了上述过程,可能会提出一些问题,这边我列举一下我在学习 Seata 时,遇到的问题,以及我得出的论断
问题 1. Seata 如何做到无侵入的剖析业务 SQL 生成 undoLog,注册事务分支等操作?
Seata 代理了 DataSource,咱们能够通过在代码注入一个 DataSource 来验证我的说法,目前的 DataSource 是 io.seata.rm.datasource.DataSourceProxy
所有的 Java 长久化框架,最终在操作数据库时都会通过 DataSource 接口获取 Connection,通过 Connection 实现对数据库的增删改查,事务管制。
Seata 通过代理 Connection 的形式,做到了无侵入的生成 undoLog,注册事务分支,具体源码能够查看io.seata.rm.datasource.ConnectionProxy
问题 2. ConnectionProxy 如何判断以后事务是全局事务,还是本地事务?
通过以后线程是否绑定了全局事务 id,在进行全局事务之前,须要调用RootContext.bind(xid);
问题 3. 全局事务并发更新
还是以下订单扣减库存的场景为例,如果 TX1 和 TX2 同时扣减 product_id 为 1 的库存,这时 Seata 会不会生成雷同的 beforeImage?
举个例子,TX1 读库存为 100,TX1 扣减库存 1,此时 BeforeImage 为 100
紧接着 如果 TX2 读库存也为 100,那么就有问题了,不论 TX2 扣减多少库存,如果 TX1 回滚那么相当于笼罩了 TX2 扣减的库存,呈现了脏写
Seata 是如何解决这个问题的?
源码地位:io.seata.rm.datasource.exec.AbstractDMLBaseExecutor::executeAutoCommitFalse
能够看到这里的逻辑和我下面画的图统一,证实我没有瞎说 ????
咱们来看一下 beforeImage()
,这是一个形象办法,看一下他的子类UpdateExecutor
是如何实现的
通过 Debug,能够看出 Seata 这边也是的确思考了这个问题,间接简略而无效的解决了这个问题
回到咱们的例子,因为 SELECT FOR UPDATE
的存在,TX2 如果也想读同一条数据的话,只能等到 TX1 提交事务后,能力读到。所以问题解决
问题 4. 全局事务外的更新
咱们当初能够确认在 Seata 的保障下,全局事务,不会造成数据的脏写,然而全局事务外会!
什么意思呢?
还以库存为例
- 用户正在抢购,用户 A 实现了 1 阶段的库存扣减,这个时候库存为 99。
- 此时库存管理员上线了,他查了一下库存为 99。嗯 … 太少了,我加 100 个,库存管理员把库存更新为 200。
- 而此时 seata 给用户 A 生成 beforeImage 为 100,如果此时用户 A 的全局事务失败了,产生了回滚,再次将库存更新为 100… 再次出现脏写
Seata 针对这个问题,提供了 @GlobalLock
注解,标记该注解时,会像全局事务一样进行 SQL 剖析,竞争全局锁,就不会呈现上述问题了
对于这个问题能够参考 Seata 的 FAQ 文档 http://seata.io/zh-cn/docs/ov…
问题 5. @GlobalTransactional 和 @Transactional 同时应用会怎么样
咱们上文中曾经说过了 @GlobalTransactional 的作用了,他是负责开启全局事务 / 提交事务 1 阶段,说白了 @GlobalTransactional 只和 Seata-server 交互,而 @Transactional 治理的是本地数据库的事务,所以二者不发生冲突。
然而须要留神 @GlobalTransactional AOP 覆盖范围肯定要大于 @Transactional
问题 6. 如果其中某一个事务分支超时未提交,会产生什么
这个我并没有看源码,而是通过跑 demo,验证的
例如当初有 A,B 两个事务分支
- A 失常提交,并向 Seata 注册分支胜利
- B 2 分钟后提交事务,并向 Seata 发动注册
Seata 的全局事务超时工夫,默认是 1 分钟,Seata-server 在检测到有超时的全局事务时,会向所有已提交的分支,发动回滚。而超时提交的事务,向 Seata-server 发动分支注册时,响应后果为事务已超时,或者事务不存在,也会回滚本地事务
问题 7. Seata-client 如何接管 Seata-server 发动的告诉
Seata-client 蕴含了 Netty 服务,在启动时 Netty 会监听端口,并向 Seata-server 发动注册。server 中存储了 client 的调用地址。
总结
咱们学习了 Seata 的 AT 模式是如何工作的,能够看出 Seata 模式在开发上是非常简单的,然而 Seata 的背地为了维持分布式事务的数据一致性,做了大量的工作,AT 模式非常适合现有的业务模型间接迁徙。
然而他的毛病也很显著,性能并不是那么的优良。例如咱们刚刚看到的全局锁的问题,为了数据不会产生脏写,Seata 就义了业务的并发能力。在十分要求性能的场景,可能还是须要思考 TCC,SAGA,可靠消息等计划
在应用 Seata 开发前,倡议大家先去浏览一下 FAQ 文档,防止踩坑 https://seata.io/zh-cn/docs/o…
DEMO
https://github.com/TavenYin/t…
参考
- Seata 是什么 – http://seata.io/zh-cn/docs/overview/what-is-seata.html
- Seata 常见问题 – http://seata.io/zh-cn/docs/overview/faq.html
- 分布式事务中间件 Seata 的设计原理 – http://seata.io/zh-cn/blog/seata-at-mode-design.html
- 分布式事务 Seata 及其三种模式详解 – http://seata.io/zh-cn/blog/seata-at-tcc-saga.html
- 分布式事务如何实现?深刻解读 Seata 的 XA 模式 – http://seata.io/zh-cn/blog/seata-xa-introduce.html