关于java:分布式事务-Seata-AT模式原理与实战

27次阅读

共计 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

正文完
 0