SeataAT模式入门
Seata架构
Seata将分布式事务理解为一个全局事务,它由若干个分支事务组成,一个分支事务就是一个满足ACID的本地事务。
Seata架构中有三个角色:
TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
刚入门时,可能不太理解这三个角色的关系,举个例子来描述:假设我们有一个下单业务,用户发起下单(Create_Order)后需要进行支付扣款,扣库存。Create_Order需要调用其他两个服务进行数据的更新。
这里OrderService这个微服务就是TM,它发起了全局事务, 库存和支付这两个微服务都是事务的参与者,也就是RM,而全局事务的进行需要TC协调两个RM完成
下图能更直观地感受它们的分工:
(图侵删)
全局事务的执行过程:
- TM向TC申请开启一个全局事务,TC创建全局事务后,会生成一个全局唯一的XID,存入
global_table
中。 XID会在全局事务上下文中传播 - RM请求TC将本地事务注册成全局事务的分支事务,该分支事务和全局事务有相同的XID
- TM向TC发起全局提交或回滚
- TC调度XID下的分支事务完成提交或回滚。
AT模式
AT模式是Seata默认模式。它从两阶段提交演化而来,改善了XA模式中资源占用过长导致性能下降的问题。
和XA的区别:AT模式下的第一阶段RM将在执行完事务后,会保存记录一份快照,然后直接提交并释放资源,若第二阶段通知事务需要回滚,再进行反向补偿,修改为提交前的数据。
在这个过程中,Seata做了不少增强:我们的业务Sql实际上不是直接就在DB中运行,而是被Seata的数据源代理拦截,那么Seata做了什么工作呢,我们接下来继续说:
第一阶段:
- Seata拦截业务sql并解析,根据解析到的信息去查询数据库得到更新前的数据镜像,称为before image。
- 再获取一次执行完后的数据,称为after image
- 将before_image和after_image的数据都插入到undo_log表中
- 在事务提交前,向TC注册分支,申请获得数据库被写的行的全局锁
- 提交本地事务和undo_log表,并将本地事务提交结果报告给TC
第二阶段:
- 二阶段提交:若事务全部执行成功,RM就可以删除undo_log表并直接提交了。
- 二阶段回滚收到TC的回滚请求,则根据undo_log表中的
befor_image
和业务sql的相关信息生成回滚语句在数据库中执行。 进行数据的回滚。
这样一来,事务就不需要像XA模式一样,一直占用着本地锁了,而是引用了全局锁对数据库的某行进行锁定。接下来我们介绍AT模式下脏写和脏读的问题
AT模式的脏读问题
分布式事务的脏读,脏写问题: 首先我们知道本地事务的脏读是:一个事务没有提交的数据被另外一个事务读到,而全局事务是多个本地事务的集合,如果在全局事务下,某个本地事务提交了,如果没有全局控制,那么这个提交的事务也有可能被其他事务读到,也是一种脏写。这个问题的本质是:全局事务中的某个本地事务完成不代表全局事务也完成
当我们不引入全局锁,来看两个事务:
很明显,线程B就发生了脏读。如何解决? Seata引入了全局锁的概念:一阶段分支事务提交前,需要申请获得数据库的全局锁(锁定的是被修改的行),在二阶段中如果有其他线程想要修改这一行的数据,除了获取本地锁之外,还需要申请得到全局锁才能修改数据,如下图:
也许有同学会问,如果tx1拥有全局锁,二阶段回滚想要获取本地锁,同时tx2拥有本地锁而想要获取全局锁,那不就死锁了吗?不会,因为Seata给申请锁添加了超时放弃的机制:图中tx2申请全局锁超时后,事务回滚释放了本地锁,tx1获得本地锁成功回滚。
AT模式脏写问题
和脏读类似,如果不引入全局锁,tx1一阶段修改了数据库某条数据并提交,在第二阶段tx2先进入修改了该行。过后tx1如果要利用快照回滚,tx2就发生了脏写。
Seata引入了全局锁后,一阶段本地事务提交前,需要确保先拿到全局锁,只有拿到全局锁才能提交,在二阶段如果需要回滚,全局锁在事务隔离上就派上了用场:
上图中tx2尝试获取全局锁失败后回滚,tx1重试多次后终于拿到本地锁回滚成功。这个过程中由于全局锁一直由tx1持有,不会出现脏写问题。