AT模式是当前应用较多的分布式事务模式,支持本地 ACID 事务的关系型数据库(mysql、oracle)的Java 应用的分布式事务管理。如下图电商购物下单的业务为例,不同的微服务需要操作多个数据库,需要保证它们同时成功或同时失败。
下面以阿里巴巴的seata框架为例介绍AT事务模式:
首先介绍AT事务模式下分布式事务实现的架构,在 Seata 的架构中,一共有三个角色:
(1)TC (Transaction Coordinator) - 事务协调者
独立于各个微服务,起到各个微服务的本地和全局事务创建提交的协调作用。
(2)TM (Transaction Manager) - 事务管理器
主管全局事务的发起、提交和回滚的决策。
(3)RM (Resource Manager) - 资源管理器
发起和管理本地分支事务。
其中TC为单独部署的 Server 服务端,TM和RM为嵌入到应用中的 Client 客户端;
这么说肯定还是不懂,所以具体来看:
①(发起全局事务的那个微服务的)TM请求TC开启一个全局事务,TC会生成一个XID作为该全局事务的编号(XID会在微服务的调用链路中传播,保证将多个微服务的子事务关联在一起);
②(发起处和各个链路上被调用的微服务处的)RM都会请求TC将本地事务注册为全局事务的分支事务,通过全局事务的XID进行关联;
③ TM请求TC告诉XID对应的全局事务是进行提交还是回滚;
④ TC驱动RM将XID对应的自己的本地事务进行提交还是回滚。
1 机制剖析
整体机制:二阶段提交
一阶段:
-
TM向TC提请开启全局事务,获取全局事务XID
-
定位到要操作的数据,查询并生成业务操作前镜像;
-
获取本地锁(重试等待),执行业务操作;
-
生成业务操作后镜像;
-
向undo_log表中插入回滚日志;
-
向TC注册分支事务,申请该表中操作数据主键所在记录的全局锁;
-
在本地事务中提交业务和回滚日志记录,并释放本地锁和连接资源;
二阶段:
没有异常发生:
-
TM向TC提请全局事务提交决策;
-
TC向各个微服务RM下发分支本地事务提交请求;
-
各个微服务异步提交本地事务并把结果返回给TC;(提交失败则要进行回滚)
-
释放全局锁。
异常发生:
-
TM向TC提请全局事务回滚决策;
-
TC向各个微服务RM下发分支本地事务回滚请求;
-
分支事务通过通过 XID 和 Branch ID 查找到相应的 undo_log 记录并进行回滚:
-
数据校验:拿 UNDO LOG 中的后镜像与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改,这种情况,需要人工来处理;
-
尝试获取本地锁(重试等待),开启本地事务;
-
根据前镜像和后镜像,执行回滚语句;
-
提交本地事务,并把结果上报给TC。
-
-
全局锁释放。
很明显看到:二阶段的方式是一种异步的体现,高并发情况下各种请求很多,所以可以把请求放在异步任务队列中。
注意:
区分这里提到的全局锁(1)和MySQL中的全局锁(2)
锁(1)实际上是“分布式行锁”,是针对要操作的行数据进行锁定,把分支事务数据库中的数据的主键的某个值注册到 TC,之所以称为全局锁,是因为它在所有分布式微服务之间互斥。锁(1)是 Seata 自己实现的,保证了先拿到全局锁的全局事务做完了所有事之后,其它全局事务才能提交本地事务并且,高并发下它也不会出现死锁,只是会有等待,性能有点衰减。
锁(2)是针对单个数据库节点的“库锁”,是对库内的所有的表进行锁定。
区分本地事务回滚和全局事务回滚
相同之处:逆向补偿(新增操作回滚会占用一个主键)
不同之处:
本地事务回滚是基于MySQL的事务机制,借助了MySQL自带的undo_log日志,在事务提交时如果发生异常则会触发回滚到上个事务前的版本。
全局事务回滚是 Seata 提供的分布式事务机制,借助了undo_log表,在本地事务提交后若在全局事务范围内出现异常则会触发回滚,将各个分支事务进行回滚。
在微服务的框架中,业务诸如修改商品价格只涉及一个微服务和数据库,这时可以在方法上不加@GlobalTransaction注解,而改用@GlobalLock
2 例子
eg01
两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。
tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的 全局锁 ,本地提交释放本地锁。 tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的 全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待 全局锁 。
tx1 二阶段全局提交,释放 全局锁 。tx2 拿到 全局锁 提交本地事务。
eg02:
如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。
此时,如果 tx2 仍在等待该数据的 全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的 全局锁 等锁超时,放弃 全局锁 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。
因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的,所以不会发生 脏写 的问题。
3 事务隔离级别
在Seata中,AT 模式的默认全局隔离级别是 读未提交(Read Uncommitted)。
需要用select for update查询,这时是已提交隔离级,加上for update后需要拿到全局锁才能读取数据,不然会一直堵塞这时不会产生脏读问题。
4 TCC事务模式
提一嘴,TCC模式整体的逻辑与AT是类似的,不同在于就是把自定义的分支事务的提交和回滚并纳入到全局事务管理中;通俗来说,Seata的TCC模式就是手工版本的AT模式,它允许你自定义两阶段的处理逻辑而不需要依赖AT模式的undo_log回滚表;
好处是可以不局限于关系型数据库,可以把Redis、消息队列等都囊括进来,坏消息是实现代码需要自己手敲,啊?o(╥﹏╥)o
内容和图片参考: 动力字节SpringCloud Alibaba课程