上一篇文章,我们介绍了分布式事务的基本理论和几种实现方式,现在,我们来了解下Seata。
Seata 是阿里开源的基于Java的分布式事务解决方案,共提供了4种模式解决分布式事务场景,分别是AT,XA,TCC,Saga。其中XA,TCC,Saga咱们都介绍过,现在来看下下AT。AT模式是阿里的 GTS(Seata 由 GTS 开源而来)所提出的一种事务模式,这是Seata的一大特色,AT对业务代码完全无侵入性,使用非常简单,改造成本低。我们只需要关注自己的业务SQL,Seata会通过分析我们业务SQL,反向生成回滚数据。
AT包含两个阶段:
- 一阶段,Seata 会拦截“业务 SQL”,首先解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,然后执行“业务 SQL”更新业务数据,在业务数据更新之后,再将其保存成“after image”,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
- 二阶段,如果是提交的话,因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。
接下来,我们来看Seata的模块组成
1)TM:事务发起者。负责告知 TC,分布式事务的开始,提交,回滚。
2)RM:资源管理者。管理各个分支事务的资源,每一个 RM 都会作为一个分支事务注册在 TC。
3)TC :事务协调者。负责我们的事务ID的生成,事务注册、提交、回滚等。
本篇我们着重介绍Seata的AT模式
在Seata的AT模式中,TM和RM都作为SDK的一部分集成在业务系统,我们可以认为是Client端。TC是Server端。
工作流程
我们用一个比较简单的业务场景来描述一下Seata AT模式的工作过程。有个充值业务,现在有两个服务,一个负责管理用户的余额,另外一个负责管理用户的积分。当用户充值的时候,首先增加用户账户上的余额,然后增加用户的积分。
AT流程分为两阶段,主要逻辑全部在第一阶段,第二阶段主要做回滚或日志清理的工作。其中,第一阶段流程如下:
积分服务中也有TM,但是由于没有用到,因此直接可以忽略。
1)余额服务中的TM,向TC申请开启一个全局事务,TC会返回一个全局的事务ID。
2)余额服务在执行本地业务之前,RM会先向TC注册分支事务。
3)余额服务依次生成undo log、执行本地事务、生成redo log,最后直接提交本地事务。
4)余额服务的RM向TC汇报,事务状态是成功的。
5)余额服务发起远程调用,把事务ID传给积分服务。
6)积分服务在执行本地业务之前,也会先向TC注册分支事务。
7)积分服务次生成undo log、执行本地事务、生成redo log,最后直接提交本地事务。
8)积分服务的RM向TC汇报,事务状态是成功的。
9)积分服务返回远程调用成功给余额服务。
10)余额服务的TM向TC申请全局事务的提交/回滚。
第二阶段的逻辑就比较简单了。Client和TC之间是有长连接的,如果是正常全局提交,则TC通知多个RM异步清理掉本地的redo和undo log即可。如果是回滚,则TC通知每个RM回滚数据即可。
至此我们已经初步了解了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. 全局事务并发更新
Seata的写隔离级别是全局独占的,事务开启之前,TM会在TC中获取全局锁(用select for update尝试,如果出现锁冲突,那么不断进行重试,直到占本地锁,而后获取全局锁)。锁的的key会以行数据的维度来确定,即同一个数据库中的某个表中的某行数据,在同一时间只会被一个事务操作。
而为了保证高效性,读的隔离级别是Read Uncommitted,当全局事务未提交,而本地数据提交时,对其他全局事务是可见的,不过也没关系,由于全局锁的限制,其他全局事务不能操作该条数据,必须等当前全局事务提交。
举个例子,产品份额有 5W,A 用户买了 2W,份额Branch一阶段完毕(本地事务份额已经扣除 Commit),但是在下单的时候异常了。因为本地事务读已提交,这时候 Seata 允许业务访问该条数据 3W,在 A 用户的份额Branch未回滚成功前,对其他用户可见,但其他用户并不能购买该产品,必须等到产品份额回滚到 5W,其他用户才可以操作产品数据。
当然,如果应用一定要达到Read Committed级别,可以在sql中使用SELECT FOR UPDATE 语句,Seata会锁定持有数据的行锁,直到全局锁是已提交的,才返回。
问题4. 全局事务外的更新
有些全局事务外的方法,它可能并不需要@GlobalTransactional的事务管理,但是我们又希望它对数据的修改能够加入到seata机制当中。那么这时候就需要@GlobalLock了。加上了@GlobalLock,在事务提交的时候就会去checkLock校验一下全局锁。
问题5. @GlobalTransactional 和 @Transactional 同时使用会怎么样
@GlobalTransactional他是负责开启全局事务/提交事务1阶段,说白了@GlobalTransactional 只和Seata-server 交互,而 @Transactional 管理的是本地数据库的事务,所以二者不发生冲突。
问题6. 如果其中某一个事务分支超时未提交,会发生什么
Seata的全局事务超时时间,默认是1分钟,Seata-server 在检测到有超时的全局事务时,会向所有已提交的分支,发起回滚。而超时提交的事务,向Seata-server发起分支注册时,响应结果为事务已超时,或者事务不存在,也会回滚本地事务。
问题7. Seata-client 如何接收Seata-server发起的通知
Seata-client 包含了Netty服务,在启动时Netty会监听端口,并向Seata-server 发起注册。server中存储了client 的调用地址。
总结
对比起TCC等其他事务模型,Seata的AT模式可以应对大多数的业务场景,并且基本可以做到无业务入侵、开发者无感知。
整个事务的协调、提交或回滚操作,都可以通过AOP完成,开发者只需要关注业务即可。
引用:
https://www.pianshen.com/article/55481052910/
http://seata.io/zh-cn/blog/seata-at-tcc-saga.html
https://www.jianshu.com/p/e5dc4d07e24e