分布式事务框架seata原理
Seata简介
Seata(原名Fescar) 是阿里18年开源的分布式事务的框架。Fescar的开源对分布式事务框架领域影响 很大。作为开源大户,Fescar来自阿里的GTS,经历了好几次双十一的考验,一经开源便颇受关注。同时Fescar也保留了接近0业务入侵的优点,只需要简单的配置Fescar的数据代理和加个注解,加一个 Undolog表,就可以达到我们想要的目的。
Seata原理
seata将一个本地事务作为一个分布式事务的分支,若干个分支分布在不同的微服务上,组成一个全局事务.seata包含三个结构:
Transaction Coordinator (TC): 事务协调器,维护全局事务的运行状态 ,负责协调驱动全局事务的提交或者回滚.
Transaction Manager (TM): 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
其中TM向TC申请一个全局事务,并生成一个全局唯一的XID.XID会在全局事务调用分支事务时在调用链路的上下文中传播,RM会携带XID向TC申请注册分支事务,TC调度XID下的所有分支事务的提交和回滚.
一个典型的分布式事务过程:
TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。
XID 在微服务调用链路的上下文中传播。
RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖,并会执行分支事务并提交(RM在第一阶段就已经执行了本地事务的提交或回滚),最后将执行结果汇报给TC
TM根据TC中的分支事务执行情况 向 TC 发起针对 XID 的全局提交或回滚决议。
TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
Seata模式
seata提供了三种实现分布式事务的模式:AT模式,MT模式和混合模式
AT模式
业务逻辑不需要关注事务机制,分支与全局事务的交互过程自动进行。
AT****模式:主要关注多 DB 访问的数据一致性,实现起来比较简单,对业务的侵入较小。AT模式部分代码如下:不需要关注执行状态,对业务代码侵入较小。类似代码如下,只需要为方法添 加 @GlobalTransactional 注解即可。
AT模式的核心是对业务无侵入,是一种改进后的两阶段提交.
详细流程
第一阶段
第一步:解析sql语句,得到 SQL 的类型(UPDATE),表(product),条件(where name = 'TXC')等相关的信息。
第二步:查询老数据,根据上面的where语句sql,去数据库查询原始的数据。
如 select * from product where name = 'TXC';得到原始的数据,如该行id=1,然后记录下来。
第三步:执行第一步的sql语句,即执行update,修改数据库的该记录的值。
第四步:查询修改后的值,select * from product where id =1.得到该行值,记录下来。
第五步:插入回滚日志,将老值、新值以及sql语句组成一个将来可用于回滚的日志,插入到UNDO_LOG表。
{
"branchId": 641789253,
"undoItems": [{
"afterImage": { //更新后的数据
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "GTS"
}, {
"name": "since",
"type": 12,
"value": "2014"
}]
}],
"tableName": "product" //数据表名
},
"beforeImage": { //更新前的数据
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "TXC"
}, {
"name": "since",
"type": 12,
"value": "2014"
}]
}],
"tableName": "product" //数据表名
},
"sqlType": "UPDATE" //sql语句类型
}],
"xid": "xid:xxx" //全局事务id
}
第六步:向TC server注册分支,申请product表,id=1的行的全局锁。注意,这个全局锁是相对于所有可能的同时在执行的分布式事务而言的。一旦某个分支,获取了该记录的全局锁,在解锁之前,任何其他的分布式事务,不能修改该数据。
第七步:本地事务提交,将自己的本地事务、和前面的UNDO LOG一起提交。
第八步:将本地事务提交的结果上报给TC server。如成功、失败。
第二阶段
成功的情况:分支收到了TC下发的成功请求,立马返回我已OK的结果给TC,然后异步执行删除UNDO LOG的操作。因为成功了,所以用来回滚的UNDO LOG就没意义了,异步删除掉就好。
失败的情况:
1 分支收到了TC下发的失败请求,开始执行回滚逻辑。
2 通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。
3 数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理,详细的说明在另外的文档中介绍。
4 根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句。
update product set name = 'TXC' where id = 1;
5 提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。
结论:
可以看到,整体来说,这个分布式事务是比较迅速的,在不等待全局锁的情况下,基本和本地事务没什么区别。回滚时,也不依赖数据库本身的回滚能力,都由自己业务来实现回滚操作。
脏读和脏写
脏读:在RM提交本地事务之后,TC提交全局事务之前,由于本地事务已提交,其他事务便可以读取到已提交事务修改的数据,但在TC回滚全局事务之后,其他事务读取到的数据又会变为更新前的数据
官方给出的解决方案为:脏读取Select语句用于更新,代理方法使用@ GlobalLock + @ Transactional或@GlobalTransaction
脏写:在TC通知分支事务回滚后,分支事务回滚之前,发现涉及的数据已经被修改,无法和UNDO LOG记录中的旧数据匹配上,则会回滚失败!
官方给出的解决方案为: 脏写您必须使用@globaltransaction注意:如果要查询的业务接口不使用@globaltransactional批注,这意味着该方法不需要分布式事务,则可以在该方法上批注@ globallock + @ Transactional批注方法,然后在查询中添加for update语句。如果您的查询接口在事务链接的外边缘具有@globaltransactional批注,则只需在查询中添加for update语句即可。设计此批注的原因是,在分布式注释可用之前,分布式事务需要查询已提交的数据,而业务则不需要分布式事务。使用GlobalTransactional批注会增加一些不必要的RPC额外开销,例如开始返回xid,提交事务等。
@globaltransactional注解会开启全局事务和本地事务
@ globallock 声明事务仅执行在本地RM中,但是本次事务确保在更新状态下的操作记录不会被其他全局事务操作。即将本地事务的执行纳入seata分布式事务的管理,一起竞争全局锁,保证全局事务在执行的时候,本地业务不可以操作全局事务中的记录。
@ globallock + @ Transactional注解只会开启本地事务和获取全局锁
一阶段本地事务提交前,需要确保先拿到 全局锁 。
拿不到 全局锁 ,不能提交本地事务。
拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。
举例:
两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。
tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的 全局锁 ,本地提交释放本地锁。
tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的 全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待 全局锁 。