深度解析!-- 阿里开源分布式事务框架 Seata
一、什么是 Seata?
Seata 是一款由阿里开源的分布式事务解决框架,致力于提供高性能和简单易用的分布式事务服,Seata 支持 AT、TCC、SAGA 和 XA 事务模式,为开发者打造一站式的分布式解决方案。
Seata 的前身为 TXC(Taobao Transaction Constructor),阿里巴巴中间件团队 2014 年起启动该项目,以满足应用程序架构从单一服务变为微服务所导致的分布式事务问题。
到了 2016 年作为整个集团的分布式事务中间件,更名为 GTS(Global Transaction Service)收费版本
到了 2019 年将其开源并更名为 FESCAR, 最后更名为 Seata,就是现在的 Seata
二、Seata 的特点
提供了(AT、TCC、Saga、XA )多种不同事务处理模式
阿里开源,社区活跃,有大厂背书
丰富的使用案例包括:滴滴、58 同城、阿里云、蚂蚁金融等
支持各种分布式框架(Spring Cloud,Dubbo 等)与关系型数据库(mysql,oracle 等)
简单易用、支持集群部署,高可用与可扩展
三、Seata 角色与运行机制
1)Transaction Coordinator(TC): 这是一个独立的服务,是一个独立的 JVM 进程,里面不包含任何业务代码,它的主要职责:维护着整个事务的全局状态,负责通知 RM 执行回滚或提交;
2)Transaction Manager(TM): TM 在微服务架构中可对应为聚合服务,即将不同的微服务组合起来成一个完成的业务流程,TM 的职责是开启一个全局事务或者提交或回滚一个全局事务;
3)Resource Manager(RM):RM 在微服务框架中对应具体的某个微服务为事务的分支,RM 的职责是:执行每个事务分支的操作,
这三种角色这么说其实比较晦涩难懂,下面我们通过一个场景用画图的方式来解释一下
假设在电商系统中,用户通过 App 下了一个订单,下单请求通过负载均衡、网关最终到订单聚合服务。假设聚合服务要做三个操作来完成订单(创建订单,增加积分,减少库存,先不考虑付款)。
显然,这是一个典型的分布式事务场景:一个业务服务调用多个不同的微服务来完成下单操作,同时各个微服务运作在独立的进程中,并且有自己独立的数据库,只要有任何一个微服务出问题(如网络异常,服务宕机,),对于这个业务场景,Seata 要经过如上图所示的 5 个关键步骤:
1、TM 开启全局事务:TM 收到请求之后,开启一个全局的事务并生成一个全局的 XID 编号,并将 XID 编号发送给 TC,同时在 TM 中通过远程调用 RM,发起具体的业务服务调用;
2、RM 完成本地操作:RM 收到 TM 发出的请求调用,RM 先完成本地操作之后(AT 与 TCC 与 Saga 模式各有不同),然后再向 TC 发起上报;
3、RM 向 TC 上报分支事务: RM 完成本地事务操作(未提交),向 TC 上报分支事务(申请全局锁)
4、TM 向 TC 提交全局事务:TM 如果顺利的完成 3 个微服务的调用(没有异常,没有超时),就向 TC 提交全局事务,如果有任何异常或超时,TM 向 TC 提交全局回滚。
5、RM 执行提交或回滚操作:RM 收到 TC 的提交或回滚后,执行具体的提交或回滚操作,--事务执行完成。
以上就是 Seata TM、RM、TC 三种角色各自的职责与交互过程,核心是两阶段提交方案,在第一阶段执行各个分支本地事务的预处理,第二阶段统一执行真正的提交或回滚,如果读者现在不理解也没有关系,下面文章内容还会对每一个具体的过程进行介绍。
四、Seata AT 模式
上面我们说过 Seata 支持 AT,TCC、Sage 分布式事务模式,本文主要介绍 AT、TCC 两种模式,先看 AT 模式,假设数据库中有 product(产品)表,原始数据如下。
上面从整理的角度对 Seata 的运行过程进行介绍,现在我们来具体的看看在 AT 模式下 RM 是如何运行的的,为了更清楚的说明,我们把整个全局事务分为三个部分来讲,第一阶段执行,第二阶段提交,第二阶段回滚,先看第一阶段执行可总结为关键的 9 个步骤,如下图所示:
4.1 第一阶段执行
1、解析 sql 语句
RM 收到远程调用后,RM 会通过数据库连接代理,解析 Sql 语句得到 Sql 元数据,内容包括:sql 语句的类型 update,insert 等,操作的表格(product),where 条件等相关信息;
update product set price = 6000 where name = ’IPhone11’;
根据上面的 sql 语句解析:操作类型:update,表:product,条件:name=’IPhone11’;
2、开启一个本地事务
RM 首先要开启一个本地事务,后面的几个操作都在本地事务中完成(即图上画虚线中的执行过程),当然,能否开启成功要看别的事务是否持有本地资源锁(后面在隔离性章节会重点介绍)
3、得到操作前镜像
通过 sql 中的条件到数据库中查询(如下面 sql)到数据(注意这里是通过条件),并根据查询结果生成操作前镜像,即:执行事务之前数据库中的状态。
select id, name, price from product where name=IPhone11’;
得到操作前数据镜像如下:
4、执行业务 sql 语句
执行 sql 语句将 name 为‘’IPhone11’’的记录价格改为 6000,此时数据库的值为 6000。
update product set price = 6000 where name = ’IPhone11’;
执行 sql 后数据库中数据如下:
5、得到操作后镜像
根据前镜像的结果,根据主键(ID)再次到数据库中查询记录(注意这里是根据主键 id),因此,对于业务表需要有主键,如果是组合主键目前支持 mysql 数据库。
select id, name, price from product where id=1;
生成操作后镜像如下:
6、生成并插入回滚日志
上面的几个过程:“解析 Sql,生成前镜像,执行 SQL 语句,生成后镜像”,目前就是为了生成一个回滚日志,回滚日志就是将 Sql 元信息,前镜像,后镜像,组织为一条回滚日志并插入到 UNDO_LOG 表中,回滚日志记录了业务操作前后的数据,当要执行全局事务回滚时,根据回滚日志进行补偿即可(不考虑冲突)如下是回滚日志的格式,已 JOSN 方式存储。
7、向 TC 注册分支申请全局锁
以上的几部操作是在一个本地事务中完成,但是完成之后并不会马上执行本地事务的提交,而是先要向 TC 申请一个全局事务锁, 全局锁的内容为:全局事务 ID,producet 表 ID,目的是为保证全局事务的隔离性,关于隔离性下面会详细的说到。
8、提交本地事务
如果全局事务申请成功,则将上述过程中的操作一并提交,这也是 AT 模式下能保证数据一致性的关键,即:通过数据库本地事务,保证业务数据与回滚日志数据的强一致,这也是为什么 AT 模式必须数据库支持 ACID 事务的原因。
9、向 TC 提交本地事务执行结果
第一阶段的最后一步,将本地事务的执行结果提交给 TC,TC 拿到该提交信息来判断第二阶段要执行什么操作
以上就是 AT 模式下第一阶段执行的具体过程,我们再来看看,第二阶段回滚的执行过程。
4.2 第二阶段-回滚过程
1、TC 发送回滚操作
首先,TM 监测到异常后超时向 TC 执行全局回滚, TC 再向每个分支 RM 发送回滚请求。
2、获取回滚日志
RM 收到回滚后,通过全局事务 XID 与分支事务 BranchID 找到回滚日志记录,在回滚日志表中有 XID 与 BranchID 两个字段,并有唯一性约束,下面是 undo_log 的表结构。
3、效验后镜像与当前数据
通过回滚日志中前镜像与当前数据(重新查找数据)的进行数据比较,如果数据不一致,说明在本事务之外有别的事务对数据进行了操作,这时就需要根据配置策略进行处理(用前镜像数据,还是用数据库中数据,还是不做处理送通知人工处理等等)
4、执行回滚 Sql 语句
通过回滚日志中前镜像信息与业务 Sql 语句相关信息,生成补偿 Sql 并执行,将 id 为 1 商品的价格再改回 5999.
5、提交本地事务
查询回滚日志,查询当前数据,执行回滚 Sql 在一个 ACID 数据库事务中执行并提交。
6、向 TC 上报本地事务执行结果
如果本地事务执行失败或超时等,TC 会根据配置定时重新发送回滚操作,保证高可用。
第二阶段事务的回滚,就是根据第一阶段保存的回滚日志数据,进行反向补偿操作,下面我们再来看第二阶段正常提交。
4.3 第二阶段-提交过程
第二阶段提交的操作处理上比较简单,只要删除回滚日志即可,但是为了性能考虑,Seata 并不是同步一条一条的去删除,而是使用异步批量的删除。
1、TC 发送提交操作
RM 收到提交操作后,先将请求放入到一个消息队列中并直接返回成功;
2、异步、批量删除回滚日志
RM 将提交的回滚任务异步、批量的执行(删除回滚日志),全局事务提交,要做的就是删除日志,删除日志的操作几乎对整个事务执行没什么影响,为了性能考虑(对于 TC 快速收到成功消息,对于 UNOD_LOG 表不用频繁执行删除操作),Seata 采用异步、批量的方式删除。
五、Seata TCC 模式
Seata TCC 模型在处理流程与处理逻辑上与 AT 是一样的(即上文所讲的 Seata 各角色与整体运行机制章节),但是的具体的实现上有些区分,下面是 Seata 官网提供地 TCC 模式运行过程图:
重点内容:
通过与 AT 模式比较,不难发现 TCC 主要区别在 RM 的处理上,在 AT 模式中 RM 利用数据库连接的代理与数据库本地事务特征,根据业务 sql 自动生成操作日志,并自动生成提交与回滚的操作逻辑。而 TCC 模式需要在业务代码中手工预留资源,手工定义提交逻辑,手工定义回滚逻辑。
1)AT 模式基于支持本地 ACID 事务的关系型数据库
一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录
二阶段 commit 行为:马上成功结束,自动异步批量清理回滚日志
二阶段 rollback 行为:通过回滚日志,自动生成补偿操作,完成数据回滚
2)TCC 模式,不依赖于底层数据资源的事务支持
一阶段 prepare 行为:调用自定义的 prepare 逻辑
二阶段 commit 行为:调用自定义的 commit 逻辑
二阶段 rollback 行为:调用自定义的 rollback 逻辑
六、AT 与 TCC 比较与适应场景
我们再来看看 AT 模式 TCC 模式的差异与适应场景,可以从如下四个方面进行对比分析。
1、性能
AT 模式在第一阶段与第二阶段需要解析 sql,执行本地事务等,比如一个 update 操作在 RM 中就多了 3 个 sql 语句(查询前镜像,查询后镜像,插入镜像)在性能上比较差,TCC 模式不依赖本地事务也不需要额外的其他操作,性能要远优于 AT 模型。
2、灵活性
AT 模式需要依赖数据事务与数据库连接,并且提交与回滚逻辑由 Seata 框架自动完成,灵活性不高,TCC 模式不依赖数据库本地事务,提交与回滚完全由自己控制,可以根据自己的实际情况灵活调整。
3、代码入侵性
AT 模式 Seata 框架通过 Spring AOP、自动装配与注解等技术,生成 JDBC 数据源代理,并对业务方法进行拦截,自动实现分布式事务的处理,业务代码只需标注注解即可,对数据库也只需添加一张新的表,接入成本低。而 TCC 模式要将业务代码拆分为 3 个方法,每个方法都要进行业务的重构,对数据库要增加资源预留字段,接入成本高。
4、适应场景
AT 模式适用于关系数据库,热点数据并发量不高的场景,TCC 模式适用于可预留资源,非关系型数据库,对并发要求高的场景。
七、Seata 如何保证高可用?
上面章节中所讨论的问题都是在正常情况的处理。那么,在非正常情况下 Seata 还能不能保证分布式事务的一致性?即 Seata 能否支持高可用?我们可以将 Seata 工作过程中可能出现异常的情况进行划分,再来看每种异常情况,Seata 是如何处理的。
如下图所示:我们假设 Service1 有两个负载,里面有 TM 与 RM,TC 有已集群的方式部署,TC 的状态信息存储与数据中,所有的节点都在注册中心进行注册。
1、TC(Seata- server)宕机(多个或全部)
TC 是一个无状态服务,即纯粹的计算节点,支持集群部署,各个服务状态数据存储于共享的数据库中,当有一个或多个 TC 宕机时,注册中心会剔除该服务,其 TC 服务还能继续使用,如果所有的 TC 都宕机了也没有关系,当 TC 重新启动后会拉取数据库中的状态,继续自动执行宕机前的事务操作。
2、Service1 到 TC 网络不可用或超时
当 TM、RM 与 TC 之间的网络不可用或超时,如果事务开始前 TM 与 TC 网络不可用,事务不会执行,数据肯定是一致的,第一阶段如果某些分支事务发生网络异常,RM 会执行事务回滚来保证一致性,如果是第二阶段发生网络异常,TC 会进行多次重试,当网络通了之后继续执行后续操作。
3、客户端服务宕机(多个或全部)
比如客户端 Service1 执行完第一阶段的动作宕机了,Service1~可以接管第二阶段的执行任务,TC 可以将回滚或提交操发送给 Service1~,如果 Service1 与 Service1~全部都宕机了,也没有关系 TC 会进行重试,当客户端重新启动之后会继续执行后面的操作,来保证数据的一致性。
4、依赖服务不可用(注册中心、配置中心)
在运作中注册中心与配置中心不可用,也没有关系,客户端与服务端都会在内存中有数据镜像,可以使用内存中的数据,有影响的是有新的配置变更时 Seata 没法及时响应,但是不会导致整个 Seata 不可用。
当然,我们说高可用并不是绝对的高可用,而是从逻辑上相对的高可用,任何系统也不能保证,绝对 100%的可用。
八、Seata 隔离性
讨论 Seata 隔离性之前,我们先来复习一下隔离性的基础知识。
8.1 什么是隔离性?
所谓隔离性是指,当多个事务同时并行操作数据时,要保证事务之间不能相互影响,不能产生数据不一致问题。如果不考虑隔离性,不做任何处理,多个事务并行操作数据会出现哪些问题呢?一般会出现下面的三种问题:
脏读:A 事务读到了 B 事务未提交的数据,如果 B 事务回滚,那 A 读到数据就是脏数据
不可重复读:在一个事务内,多次重复读同一个数据,但值不一样。如果 A 事务第一次读取数据后没有结束事务,而 B 事务修改了该数据,A 事务第二次读到数据时与第一读的数据就会不一样,这样 A 事务在一个事务内两次读到的数据不一致。
幻读(虚度):幻读与不可重复度,在定义上都是一样的:既 A 事务在一个事务内两次读到的数据不一致,只是不可重复读时数据的值不一致,而幻读是数据条数不一致(如第一次读只有一条数据,而第二次读到了两条数据)
为了避免上面 3 种数据不一致问题的出现,要对并行事务进行处理,就有了隔离级别的概念,不同的级别可以避免上述不同的数据不一致问题,隔离级别衡量程序对并发事务的处理情况,隔离级别分为如下 4 种。
未提交读(读未提交):允许 A 事务读 B 事务未提交的数据,这是最低的隔离级别,未提交读不能解决上面任何数据不一致问题。
提交读(读已提交):提交读跟未提交读对应,只允许 A 事务读取 B 事务已经提交的数据,提交读可以解“决脏读”问题。
可重复读:可重复读保存同一事务中多次读取的数据内容一致,但是不保证数据条数一致。因此可重复读解决了“不可重复读”问题,但是不能解决“幻读”问题。
串行化:串性化要求并行的事务排队按顺序执行,只有等前一个事务结束,才能进入第二事务,因为是并行执行,所以不存在上面说任何一项数据一致性问题,串行化是最高级别的隔离。但是因为要对每一条数据加锁,会引起争抢锁与超时的情况,性能很差。
我们可以看到,不同的事务隔离级别,实际上是在一致性与并发性之间的权衡,事务隔离级别越高数据的一致性越高,但效率与并发性越差,系统要根据自己不同的实际情况,支持不同的隔离级别,默认情况下 mysql 可重复读,oracle,sql server 读已提交。
理解了事务隔离性的基本知识,再来看看 Seata 是如何保证事务的隔离性的,我们可以“写隔离”,“读隔离”两个角度来分析。
8.2 Seata 写隔离
写隔离,顾名思义用来解决多个分布式事务同时对数据写入的问题。假设数据库中有一条数据 M,原来的值为 100,分布式事务先 TX1 修改 M=M+20,分布式事务 TX2 修改值为 M=M+30,在这种情况下来看 Seata 会如何保证隔离性,如下图所示:
1)TX1 的处理过程(如上图左边)
1、当 TX1 开始执行时,先开启本地事务,并且获取数据 M 的锁;
2、然后执行业务操作与日志操作,但不会马上提交本地事务,mysql 默认的隔离机制是可重复读,因此在第一个事务没有提交之前,第二事务是读不到数据 M 的,第二事务会一直等待第一个本地事务提交释放 M 的锁,才能操作数据(读取,更新)。
3、TX1 向 TC 申请该全局事务对于数据 M 的全局锁(TC 状态数据库中插入一条锁记录),全局锁申请成功后,提交本地事务。
4、接着 TX1 执行其他操作,比如其他分支事务执行操作其他业务。
5、TX1 所有分支事务全部执行成功,并全局提交。
6、TX1 执行完成,释放全局锁。
2)TX2 处理过程(如上图右边)
1、先获取数据 M 的本地锁,由于在 TX1 本地事务提交之前,TX2 是拿不到本地锁的,而 TX1 只有申请到了全局锁才能提交本地事务,此时,TX1 持有本地锁与全局锁。
2、当 TX1 申请到了全局锁,再提交了本地事务之后,TX2 才获取了本地锁,开始执行本地事务操作,但这时提交不了本地事务。
3、TX2 会尝试获取全局锁,直到 TX1 释放。
4、TX2 获取到全局锁,并提交本地事务。
以上讨论的是第二阶段正常提交的情况,如果在第二阶段执行的是回滚操作,Seata 会如何处理呢?在第一阶段 TX1 与 TX2 的处理与上面讨论的是完全一致的,我们重点来看第二阶段回滚的处理过程。
如下图所示:
3)当全局事务 TX1 遇到异常要进行回滚时做如下处理:
1、XT1 尝试获取本地锁:TX1 执行回滚操作前,首先要申请本地锁,而此时本地锁被 TX2 持有,TX1 会不停的尝试直到成功获取本地锁。
2、TX2 获取全局锁:TX2 要提交本地事务,先要申请全局锁,而此时全局锁被 TX1 持有,TX2 会不停的尝试(默认 30 毫秒尝试一次,尝试 10 次),TX2 尝试次数用完,自动放弃获取全局锁、执行本地回滚,并释放本地锁。
3、TX1 获取到本地锁:在上面步骤中 TX2 释放了本地锁,此时 TX1 获取到了本地锁。
4、TX1 执行回滚:TX1 持有本地锁后执行具体回滚操作(具体过程可参照上面的内容)
5、提交本地事务:此时 TX1 已经持有全局锁,可以直接提交本地事务
6、释放全局事务:回滚执行完成,释放全局锁,全局事务执行完成
我们可以想一下,如果不做任何事务处理,不加全局锁会发生什么情况?在第一阶段,TX1 执行 M=M+20 后并提交本地事务,同时 XT2 执行 M=M+30 并提交本地事务,这时如果 TX1 在第二阶段进行全局事务回滚,这时数据库中 M 的值为 150,而不是 TX1 事务前的 100,数据出现了不一致。
通过上面的分析我们可以总结写隔离的核心:通过持有全局锁来控制本地事务的提交,在一个分布式事务没有全部完成之前(包括提交与回滚),会一直持有全局锁,而别的分布式事只能等待,直到别的事务释放了全局锁。
8.3 Seata 读隔离
所谓读隔离是指:一个全局事务更新数据,而另一个全局事务同时查询同一数据的情况。
在上面讨论事务隔离性基础理论时,我们说 mysql 默认的隔离级别为“可重复读”,解决了脏读与不可重复读问题。Seata 框架在 AT 模式下,默认的隔离性级别为“读未提交”,即:一个事务可以读取另一个事务未完成全局提交或全局回滚的数据。这里一定要注意,是全局提交或回滚而不是分支事务的本地提交或回滚。
如下图所示,当 TX2 执行一个查询任务时,先要获取到跟查询任务对应的数据的全局事务锁,如果该全局锁被别是事务持有,那说明别的事务还没有进行全局提交,只能等待并释放本地锁,Block 查询,直到获取到了全局锁,才能执行查询语句。
如果想让 Seata 支持更高级别的隔离级别“读已提交”,需要显示的在查询语句后面加 SELECT FOR UPDATE 语句,为了性能的考虑,Seata 仅针对 FOR UPDATE select 的语句进行代理,来实现“读已提交”级别的隔离。
九 、Seata 总结
本文我们针对 Seata 的基本概念,Seata 的运行机制原理,对 AT 模式的执行过程进行了详细的说明,也比较了 AT 模式与 TCC 模式的差异性与适应场景,还简单的讲解了 Seata 如何保证高可用与隔离性,相信通过这篇文章,我们对 Seata 有个初步的认识,最后再对 Seata 做一个总结。
9.1 两阶段提交方案
Seata AT 模式的整体运行机制是两阶段提交方案,即整个事务的完成分为两个阶段
第一阶段:在各个 RM 中将业务数据与回滚的日志数据在一个本地事务中完成(保存业务数据与回滚日志数据的强一致),业务数据操作与回滚日志操作在同一本地事务中非常关键。
第二阶段:如果所有 RM 本地事务顺利完成,执行全局提交,如果 RM 中有任何一个异常则执行全局事务回滚,全局回滚的操作就是根据第一阶段的回滚日志对业务数据进行反向补偿。
9.2 JDBC 代理自动创建回滚日志
Seata AT 模式的关键就是对原始业务方法进行拦截,Seata 通过对 JDBC 连接操作进行代理,再通过解析 Sql 语句生成回滚日志,并自动保存回滚日志,回滚日志中包含了业务操作前的数据状态与业务操作后的状态。
正是因为 Seata 自动生成回滚日志,自动生成提交与回滚逻辑,AT 模式对业务代码的入侵性几乎为零,在对系统进行分布式事务改造时,只需要对原有代码做相关的配置与标签化即可。
9.3 全局事务锁保证事务的隔离性
Seata AT 通过全局事务锁来保证隔离性,每次执行本地提交或回滚操作都必须要持有全局锁,一个全局事务未执行完成之前,会一直持有全局锁,其他事务只能等待或放弃。
当前微服务“大行其道”,而微服务很大的一痛点就是分布式服务如何解决,Seata 为了我们提供了开箱即用的解决方案,并且因其易用、活跃的社区、高效的性能等成为分布式解决方案的不二之选。
分布式事务本身就是一个复杂的话题,涉及的知识点非常多,作者水平有限,如有不到之处,还请大家指正,谢谢。
版权声明: 本文为 InfoQ 作者【攀鱼飞岩】的原创文章。
原文链接:【http://xie.infoq.cn/article/f5800e3e602cf60054ed97ef5】。文章转载请联系作者。
评论 (4 条评论)