Seata 是阿里开源的基于Java的分布式事务解决方案
Seata 提供四种模式解决分布式事务场景,AT,XA,TCC,Saga。简单叨咕叨咕我对这几种模式的理解html
这是Seata的一大特点,AT对业务代码彻底无侵入性,使用很是简单,改形成本低。咱们只须要关注本身的业务SQL,Seata会经过分析咱们业务SQL,反向生成回滚数据java
AT 包含两个阶段git
也是咱们常说的二阶段提交,XA要求数据库自己提供对规范和协议的支持。XA用起来的话,也是对业务代码无侵入性的。github
上述其余三种模式,都是属于补偿型,没法保证全局一致性。啥意思呢,例如刚刚说的AT模式,咱们是可能读到这一次分布式事务的中间状态,而XA模式不会。spring
补偿型 事务处理机制构建在 事务资源(数据库) 之上(要么在中间件层面,要么在应用层面),事务资源 自己对分布式事务是无感知的,这也就致使了补偿型事务没法作到真正的 全局一致性 。
好比,一条库存记录,处在 补偿型 事务处理过程当中,由 100 扣减为 50。此时,仓库管理员链接数据库,查询统计库存,就看到当前的 50。以后,事务由于意外回滚,库存会被补偿回滚为 100。显然,仓库管理员查询统计到的 50 就是 脏 数据。
若是是XA的话,中间态数据库存 50 由数据库自己保证,不会被仓库管理员读到(固然隔离级别须要 读已提交 以上)
可是全局一致性带来的结果就是数据的锁定(AT模式也是存在全局锁的,可是隔离级别没法保证,后边咱们会详细说),例如全局事务中有一条update语句,其余事务想要更新同一条数据的话,只能等待全局事务结束sql
传统XA模式是存在一些问题的,Seata也是作了相关的优化,更多关于Seata XA的内容,传送门? http://seata.io/zh-cn/blog/se...
TCC 模式一样包含两个阶段数据库
对比AT或者XA模式来讲,TCC模式须要咱们本身抽象并实现Try,Confirm,Cancel三个接口,编码量会大一些,可是因为事务的每个阶段都由开发人员自行实现。并且相较于AT模式来讲,减小了SQL解析的过程,也没有全局锁的限制,因此TCC模式的性能是优于AT 、XA模式。
PS:果真简单和高效难以两全的
Saga 是长事务解决方案,每一个参与者须要实现事务的正向操做和补偿操做。当参与者正向操做执行失败时,回滚本地事务的同时,会调用上一阶段的补偿操做,在业务失败时最终会使事务回到初始状态springboot
Saga与TCC相似,一样没有全局锁。因为相比缺乏锁定资源这一步,在某些适合的场景,Saga要比TCC实现起来更简单。
因为Saga和TCC都须要咱们手动编码实现,因此在开发时咱们须要参考一些设计上的规范,因为不是本文重点,这里就很少说了,能够参考 分布式事务 Seata 及其三种模式详解
在咱们了解完四种分布式事务的原理以后,咱们回到本文重点AT模式并发
模拟需求:如下订单为例,在分布式的电商场景中,订单服务和库存服务多是两个数据库框架
咱们先来看看AT模式下的代码是什么样的,这里忽略了Seata的相关配置,只看业务部分
在须要开启分布式事务的方法上标记@GlobalTransactional,而后执行分别执行扣减库存和扣减库存操做的,事务的参与者能够是本地的数据源,或者RPC的远程调用(远程调用的话须要携带全局事务ID,也就是上图的xid)
以前说过AT模式分为两个阶段,第一阶段包括提交业务数据和回滚日志(undoLog),第一阶段具体流程以下图
标记@GlobalTransactional
的方法经过AOP实现了,开启全局事务和提交全局事务两个操做,与Spring 事务机制相似,当 GlobalTransactionalInterceptor 在事务执行过程当中捕获到Throwable时,会发起全局事务回滚
0.1 步骤中会生成一个全局事务ID
0.2 全部事务参与者执行结束后,一阶段事务提交
咱们先来看看 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。由于没有全局锁,形成了 脏写
二阶段是彻底异步化的而且彻底由Seata控制,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的保证下,全局事务,不会形成数据的脏写,可是全局事务外会!
什么意思呢?
还以库存为例
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两个事务分支
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...
https://github.com/TavenYin/t...