Bootstrap

深度解析!--阿里开源分布式事务框架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条件等相关信息;

根据上面的sql语句解析:操作类型:update,表:product,条件:name=’IPhone11’;

 

2、开启一个本地事务

RM首先要开启一个本地事务,后面的几个操作都在本地事务中完成(即图上画虚线中的执行过程),当然,能否开启成功要看别的事务是否持有本地资源锁(后面在隔离性章节会重点介绍)

3、得到操作前镜像

通过sql中的条件到数据库中查询(如下面sql)到数据(注意这里是通过条件),并根据查询结果生成操作前镜像,即:执行事务之前数据库中的状态。

得到操作前数据镜像如下:

4、执行业务sql语句

执行sql语句将name为‘IPhone11’’的记录价格改为6000,此时数据库的值为6000。

执行sql后数据库中数据如下:

 

5、得到操作后镜像

根据前镜像的结果,根据主键(ID)再次到数据库中查询记录(注意这里是根据主键id),因此,对于业务表需要有主键,如果是组合主键目前支持mysql数据库。

生成操作后镜像如下:

6、生成并插入回滚日志

上面的几个过程:“解析Sql,生成前镜像,执行SQL语句,生成后镜像”,目前就是为了生成一个回滚日志,回滚日志就是将Sql元信息,前镜像,后镜像,组织为一条回滚日志并插入到UNDO_LOG表中,回滚日志记录了业务操作前后的数据,当要执行全局事务回滚时,根据回滚日志进行补偿即可(不考虑冲突)如下是回滚日志的格式,已JOSN方式存储。

{
    // 分支事务ID
    "branchId": XXXXXX,
    "undoItems": [
        {
            // 前镜像
            "afterImage": {
                "rows": [
                    {
                        "fields": [
                            {
                                "name": "id",
                                "type": 4,
                                "value": 1
                            },
                            {
                                "name": "name",
                                "type": 12,
                                "value": "IPhone11"
                            },
                            {
                                "name": "price",
                                "type": 12,
                                "value": "5999"
                            }
                        ]
                    }
                ],
                "tableName": "product"
            },
            // 后镜像
            "beforeImage": {
                "rows": [
                    {
                        "fields": [
                            {
                                "name": "id",
                                "type": 4,
                                "value": 1
                            },
                            {
                                "name": "name",
                                "type": 12,
                                "value": "IPhone11"
                            },
                            {
                                "name": "price",
                                "type": 12,
                                "value": "6000"
                            }
                        ]
                    }
                ],
                // 操作表
                "tableName": "product"
            },
            // 操作类型
            "sqlType": "UPDATE"
        }
    ],
    "xid": "xid:xxx"
}

 

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的表结构。

CREATE TABLE `undo_log` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `branch_id` bigint(20) NOT NULL,
    `xid` varchar(100) NOT NULL,
    `context` varchar(128) NOT NULL,
    `rollback_info` longblob NOT NULL,
    `log_status` int(11) NOT NULL,
    `log_created` datetime NOT NULL,
    `log_modified` datetime NOT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;

3、效验后镜像与当前数据

通过回滚日志中前镜像与当前数据(重新查找数据)的进行数据比较,如果数据不一致,说明在本事务之外有别的事务对数据进行了操作,这时就需要根据配置策略进行处理(用前镜像数据,还是用数据库中数据,还是不做处理送通知人工处理等等)

4、执行回滚Sql语句

通过回滚日志中前镜像信息与业务Sql语句相关信息,生成补偿Sql并执行,将id为1商品的价格再改回5999.

update product set price = 5999 where id =1;

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为了我们提供了开箱即用的解决方案,并且因其易用、活跃的社区、高效的性能等成为分布式解决方案的不二之选。

分布式事务本身就是一个复杂的话题,涉及的知识点非常多,作者水平有限,如有不到之处,还请大家指正,谢谢。