分布式事务Seata

一、概述

Seata是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata提供了ATTCCSAGAXA事务模式,为用户打造一站式的分布式解决方案。

1.1 简介

Seata开源之前,Seata对应的内部版本在阿里经济体内部一直扮演着分布式一致性中间件的角色,帮助经济体平稳的度过历年的双11,对各BU业务进行了有力的支撑。商业化产品GTS先后在阿里云、金融云进行售卖。

1.2 三大模块

Seata分三大模块:

  • TC:事务协调者。负责我们的事务ID的生成,事务注册、提交、回滚等。
  • TM:事务发起者。定义事务的边界,负责告知TC,分布式事务的开始,提交,回滚。
  • RM:资源管理者。管理每个分支事务的资源,每一个RM都会作为一个分支事务注册在TC

SeataAT模式中,TMRM都作为SDK的一部分和业务服务在一起,我们可以认为是ClientTC是一个独立的服务,通过服务的注册、发现将自己暴露给Client们。

Seata中有三大模块中,TMRM是作为Seata的客户端与业务系统集成在一起,TC作为Seata的服务端独立部署。

20210815.png

1.3 执行流程

Seata中,分布式事务的执行流程:

  • TM开启分布式事务(TMTC注册全局事务记录);
  • 按业务场景,编排数据库、服务等事务内资源(RMTC汇报资源准备状态);
  • TM结束分布式事务,事务一阶段结束(TM通知TC提交/回滚分布式事务);
  • TC汇总事务信息,决定分布式事务是提交还是回滚;
  • TC通知所有RM提交/回滚资源,事务二阶段结束;

20210710161530943.png

SeataTCTMRM三个角色,是不是和XA模型很像。下图是XA模型的事务大致流程:

20210710161627404.png

X/Open DTP(Distributed Transaction Process)模型里面,有三个角色:

  • APApplication,应用程序。也就是业务层。哪些操作属于一个事务,就是AP定义的。
  • TMTransaction Manager,事务管理器。接收AP的事务请求,对全局事务进行管理,管理事务分支状态,协调RM的处理,通知RM哪些操作属于哪些全局事务以及事务分支等等。这个也是整个事务调度模型的核心部分。
  • RMResource Manager,资源管理器。一般是数据库,也可以是其他的资源管理器,如消息队列(如JMS数据源),文件系统等。

20210709140731779.png

1.4 分布式事务解决方案

Seata4种分布式事务解决方案,分别是AT模式、TCC模式、Saga模式和XA模式。

2021081501.png

二、AT模式

AT模式是最早支持的模式。AT模式是指Automatic (Branch) Transaction Mode自动化分支事务。AT模式是增强型二阶段提交(2pc)模式,或者说是增强型的XA模型。总体来说,AT模式,是2pc两阶段提交协议的演变,不同的地方,AT模式不会一直锁表。

2.1 AT模式的使用前提

  • 基于支持本地ACID事务的关系型数据库。

    比如,在MySQL 5.1之前的版本中,默认的搜索引擎是MyISAM,从MySQL 5.5之后的版本中,默认的搜索引擎变更为InnoDBMyISAM存储引擎的特点是:表级锁、不支持事务和全文索引。所以,基于MyISAM的表,就不支持AT模式。

  • Java应用,通过JDBC访问数据库。

2.2 AT模型图

两阶段提交协议的演变:

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
  • 二阶段:
    • 提交异步化,非常快速地完成。
    • 或回滚通过一阶段的回滚日志进行反向补偿

完整的ATSeata所制定的事务模式下的模型图:

20201009172509465.png

2.3 AT模式的例子

我们用一个比较简单的业务场景来描述一下AT模式的工作过程。有个充值业务,现在有两个服务,一个负责管理用户的余额,另外一个负责管理用户的积分。

当用户充值的时候,首先增加用户账户上的余额,然后增加用户的积分。

AT模式分为两阶段,主要逻辑全部在第一阶段,第二阶段主要做回滚或日志清理的工作。

2.3.1 第一阶段流程

第一阶段流程如

20191117211026990.png

  1. 余额服务中的TM,向TC申请开启一个全局事务,TC会返回一个全局的事务ID
  2. 余额服务在执行本地业务之前,RM会先向TC注册分支事务。
  3. 余额服务依次生成undo log、执行本地事务、生成redo log,最后直接提交本地事务。
  4. 余额服务的RMTC汇报,事务状态是成功的。
  5. 余额服务发起远程调用,把事务ID传给积分服务。
  6. 积分服务在执行本地业务之前,也会先向TC注册分支事务。
  7. 积分服务次生成undo log、执行本地事务、生成redo log,最后直接提交本地事务。
  8. 积分服务的RMTC汇报,事务状态是成功的。
  9. 积分服务返回远程调用成功给余额服务。
  10. 余额服务的TMTC申请全局事务的提交/回滚。

积分服务中也有TM,但是由于没有用到,因此直接可以忽略。

如果使用Spring框架的注解式事务,远程调用会在本地事务提交之前发生。但是,先发起远程调用还是先提交本地事务,这个其实没有任何影响。

2.3.2 第二阶段流程

第二阶段的逻辑就比较简单了。

ClientTC之间是有长连接的,如果是正常全局提交,则TC通知多个RM异步清理掉本地的redoundo log即可。如果是回滚,则TC通知每个RM回滚数据即可。这里就会引出一个问题,由于本地事务都是自己直接提交了,后面如何回滚,由于我们在操作本地业务操作的前后,做记录了undoredo log,因此可以通过undo log进行回滚。由于undoredo log和业务操作在同一个事务中,因此肯定会同时成功或同时失败。但是还会存在一个问题,因为每个事务从本地提交到通知回滚这段时间里,可能这条数据已经被别的事务修改,如果直接用undo log回滚,会导致数据不一致的情况。此时,RM会用redo log进行校验,对比数据是否一样,从而得知数据是否有别的事务修改过。注意:undo log是被修改前的数据,可以用于回滚;redo log是被修改后的数据,用于回滚校验。如果数据未被其他事务修改过,则可以直接回滚;如果是脏数据,再根据不同策略处理。

2.4 AT模式在电商下单场景的使用

下面描述AT模式的工作原理使用的电商下单场景的使用。如下图所示:

20210711210909395.png

在上图中,协调者shopping-service先调用参与者repo-service扣减库存,后调用参与者order-service生成订单。这个业务流使用Seata in XA mode后的全局事务流程如下图所示:

20210711210932176.png

上图描述的全局事务执行流程为:

  1. shopping-serviceSeata注册全局事务,并产生一个全局事务标识XID
  2. repo-service.repo_dborder-service.order_db的本地事务执行到待提交阶段,事务内容包含对repo-service.repo_dborder-service.order_db进行的查询操作以及写每个库的undo_log记录;
  3. repo-service.repo_dborder-service.order_dbSeata注册分支事务,并将其纳入该XID对应的全局事务范围;
  4. 提交repo-service.repo_dborder-service.order_db的本地事务;
  5. repo-service.repo_dborder-service.order_dbSeata汇报分支事务的提交状态;
  6. Seata汇总所有的DB的分支事务的提交状态,决定全局事务是该提交还是回滚;
  7. Seata通知repo-service.repo_dborder-service.order_db提交/回滚本地事务,若需要回滚,采取的是补偿式方法。

其中1至5属于第一阶段,6、7属于第二阶段。

2.4.1 电商业务场景中Seata in AT mode工作流程详述

在上面的电商业务场景中,购物服务调用库存服务扣减库存,调用订单服务创建订单,显然这两个调用过程要放在一个事务里面。即:

start global_trx

call 库存服务的扣减库存接口

call 订单服务的创建订单接口

commit global_trx

在库存服务的数据库中,存在如下的库存表t_repo

20210711210955548.png

在订单服务的数据库中,存在如下的订单表t_order

20210711211013809.png

现在,id40002的用户要购买一只商品代码为20002的鼠标,整个分布式事务的内容为:

  1. 在库存服务的库存表中将记录

20210711211030835.png

修改为

20210711211044143.png

  1. 在订单服务的订单表中添加一条记录

20210711211102524.png

以上操作,在AT模式的第一阶段的流程图如下:

20210711211149471.png

20210711211222402.png

AT模式第一阶段的流程来看,分支的本地事务在第一阶段提交完成之后,就会释放掉本地事务锁定的本地记录。这是AT模式和XA最大的不同点,在XA事务的两阶段提交中,被锁定的记录直到第二阶段结束才会被释放。所以AT模式减少了锁记录的时间,从而提高了分布式事务的处理效率。AT模式之所以能够实现第一阶段完成就释放被锁定的记录,是因为Seata在每个服务的数据库中维护了一张undo_log表,其中记录了对t_order/t_repo进行操作前后记录的镜像数据,即便第二阶段发生异常,只需回放每个服务的undo_log中的相应记录即可实现全局回滚。

undo_log的表结构:

20210711211250507.png

第一阶段结束之后,Seata会接收到所有分支事务的提交状态,然后决定是提交全局事务还是回滚全局事务。

  1. 若所有分支事务本地提交均成功,则Seata决定全局提交。

Seata将分支提交的消息发送给各个分支事务,各个分支事务收到分支提交消息后,会将消息放入一个缓冲队列,然后直接向Seata返回提交成功。之后,每个本地事务会慢慢处理分支提交消息,处理的方式为:删除相应分支事务的undo_log记录。之所以只需删除分支事务的undo_log记录,而不需要再做其他提交操作,是因为提交操作已经在第一阶段完成了(这也是ATXA不同的地方)。这个过程如下图所示:

20210711211745801.png

分支事务之所以能够直接返回成功给Seata,是因为真正关键的提交操作在第一阶段已经完成了,清除undo_log日志只是收尾工作,即便清除失败了,也对整个分布式事务不产生实质影响。

  1. 若任一分支事务本地提交失败,则Seata决定全局回滚,将分支事务回滚消息发送给各个分支事务,由于在第一阶段各个服务的数据库上记录了undo_log记录,分支事务回滚操作只需根据undo_log记录进行补偿即可。全局事务的回滚流程如下图所示:

20210711211811749.png

这里对图中的2、3步做进一步的说明:

  1. 由于上文给出了undo_log的表结构,所以可以通过xidbranch_id来找到当前分支事务的所有undo_log记录;
  2. 拿到当前分支事务的undo_log记录之后,首先要做数据校验,如果afterImage中的记录与当前的表记录不一致,说明从第一阶段完成到此刻期间,有别的事务修改了这些记录,这会导致分支事务无法回滚,向Seata反馈回滚失败;如果afterImage中的记录与当前的表记录一致,说明从第一阶段完成到此刻期间,没有别的事务修改这些记录,分支事务可回滚,进而根据beforeImageafterImage计算出补偿SQL,执行补偿SQL进行回滚,然后删除相应undo_log,向Seata反馈回滚成功。

2.5 数据隔离性

SeataAT模式主要实现逻辑是数据源代理,而数据源代理将基于如MySQLOracle等关系事务型数据库实现,基于数据库的隔离级别为read committed。换而言之,本地事务的支持是Seata实现AT模式的必要条件,这也将限制SeataAT模式的使用场景。

2.5.1 写隔离

从前面的工作流程,我们可以很容易知道,Seata的写隔离级别是全局独占的。

首先,我们理解一下写隔离的流程

分支事务1-开始
 |
 V 获取 本地锁
 |
 V 获取 全局锁    分支事务2-开始
 |                |
 V 释放 本地锁     V 获取 本地锁
 |                |
 V 释放 全局锁     V 获取 全局锁
 |
 V 释放 本地锁
 |
 V 释放 全局锁

如上所示,一个分布式事务的锁获取流程是这样的

  1. 先获取到本地锁,这样你已经可以修改本地数据了,只是还不能本地事务提交;
  2. 而后,能否提交就是看能否获得全局锁;
  3. 获得了全局锁,意味着可以修改了,那么提交本地事务,释放本地锁;
  4. 当分布式事务提交,释放全局锁。这样就可以让其它事务获取全局锁,并提交它们对本地数据的修改了。

可以看到,这里有两个关键点

  1. 本地锁获取之前,不会去争抢全局锁;
  2. 全局锁获取之前,不会提交本地锁。

这就意味着,数据的修改将被互斥开来。也就不会造成写入脏数据。全局锁可以让分布式修改中的写数据隔离。

写隔离的原则:

  • 一阶段本地事务提交前,需要确保先拿到全局锁
  • 拿不到全局锁,不能提交本地事务。
  • 全局锁的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。

以一个示例来说明:

两个全局事务tx1tx2,分别对a表的m字段进行更新操作,m的初始值1000

tx1先开始,开启本地事务,拿到本地锁,更新操作m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的全局锁,本地提交释放本地锁。
tx2后开始,开启本地事务,拿到本地锁,更新操作m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的全局锁tx1全局提交前,该记录的全局锁被tx1持有,tx2需要重试等待全局锁

20210710164507421.png

tx1二阶段全局提交,释放全局锁tx2拿到全局锁提交本地事务。如果tx1的二阶段全局回滚,则tx1需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。

2021071016463052.png

此时,如果tx2仍在等待该数据的全局锁,同时持有本地锁,则tx1的分支回滚会失败。分支的回滚会一直重试,直到tx2的全局锁等锁超时,放弃全局锁并回滚本地事务释放本地锁,tx1的分支回滚最终成功。因为整个过程全局锁在tx1结束前一直是被tx1持有的,所以不会发生脏写的问题。

2.5.2 读的隔离级别

在数据库本地事务隔离级别读已提交(Read Committed)或以上的基础上,Seata(AT模式)的默认全局隔离级别是读未提交(Read Uncommitted)
如果应用在特定场景下,必需要求全局的读已提交,目前Seata的方式是通过SELECT FOR UPDATE语句的代理。

20210710165141349.png

SELECT FOR UPDATE语句的执行会申请全局锁,如果全局锁被其他事务持有,则释放本地锁(回滚SELECT FOR UPDATE语句的本地执行)并重试。这个过程中,查询是被block住的,直到全局锁拿到,即读取的相关数据是已提交的,才返回。

出于总体性能上的考虑,Seata目前的方案并没有对所有SELECT语句都进行代理,仅针对FOR UPDATESELECT语句。

2.6 Spring Cloud集成AT模式

AT模式是指Automatic (Branch) Transaction Mode自动化分支事务,使用AT模式的前提是

  • 基于支持本地ACID事务的关系型数据库。
  • Java应用,通过JDBC访问数据库。

seata-at的使用步骤

  1. 引入seata框架,配置好seata基本配置,建立undo_log表;
  2. 消费者引入全局事务注解@GlobalTransactional
  3. 生产者引入全局事务注解@GlobalTransactional

三、TCC模式

3.1 简介

TCCAT事务一样都是两阶段事务,它与AT事务的主要区别为:

  • TCC对业务代码侵入严重

    每个阶段的数据操作都要自己进行编码来实现,事务框架无法自动处理。

  • TCC性能更高

    不必对数据加全局锁,允许多个事务同时操作数据。

    a

TCC整体是两阶段提交的模型。一个分布式的全局事务,全局事务是由若干分支事务组成的,分支事务要满足两阶段提交的模型要求,即需要每个分支事务都具备自己的:

  • 一阶段prepare行为
  • 二阶段commitrollback行为

20210710224912234.png

根据两阶段行为模式的不同,我们将分支事务划分为Automatic (Branch) Transaction ModeTCC (Branch) Transaction Mode

AT模式(参考链接TBD)基于支持本地ACID事务关系型数据库

  • 一阶段prepare行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
  • 二阶段commit行为:马上成功结束,自动异步批量清理回滚日志。
  • 二阶段rollback行为:通过回滚日志,自动生成补偿操作,完成数据回滚。

相应的,TCC模式,不依赖于底层数据资源的事务支持:

  • 一阶段prepare行为:调用自定义prepare逻辑。
  • 二阶段commit行为:调用自定义commit逻辑。
  • 二阶段rollback行为:调用自定义rollback逻辑。

所谓TCC模式,是指支持把自定义的分支事务纳入到全局事务的管理中。

第一阶段Try

以账户服务为例,当下订单时要扣减用户账户金额:

a

假如用户购买100元商品,要扣减100元。

TCC事务首先对这100元的扣减金额进行预留,或者说是先冻结这100元:

a

第二阶段Confirm

如果第一阶段能够顺利完成,那么说明“扣减金额”业务(分支事务)最终肯定是可以成功的。当全局事务提交时,TC会控制当前分支事务进行提交,如果提交失败,TC会反复尝试,直到提交成功为止。

当全局事务提交时,就可以使用冻结的金额来最终实现业务数据操作:

a

第二阶段Cancel

如果全局事务回滚,就把冻结的金额进行解冻,恢复到以前的状态,TC会控制当前分支事务回滚,如果回滚失败,TC会反复尝试,直到回滚完成为止。

a

多个事务并发的情况

多个TCC全局事务允许并发,它们执行扣减金额时,只需要冻结各自的金额即可:

a

四、Saga模式

Saga模式是Seata提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。

Saga模式示意图

理论基础:Hector & Kenneth发表论⽂Sagas(1987)

4.1 适用场景

  • 业务流程长、业务流程多
  • 参与者包含其它公司或遗留系统服务,无法提供TCC模式要求的三个接口

4.2 优势

  • 一阶段提交本地事务,无锁,高性能
  • 事件驱动架构,参与者可异步执行,高吞吐
  • 补偿服务易于实现

4.3 缺点

  • 不保证隔离性(应对方案见后面文档)

4.4 Saga的实现

4.4.1 基于状态机引擎的Saga实现

目前Seata提供的Saga模式是基于状态机引擎来实现的,机制是:

  1. 通过状态图来定义服务调用的流程并生成json状态语言定义文件
  2. 状态图中一个节点可以是调用一个服务,节点可以配置它的补偿节点
  3. 状态图json由状态机引擎驱动执行,当出现异常时状态引擎反向执行已成功节点对应的补偿节点将事务回滚

注意:异常发生时是否进行补偿也可由用户自定义决定

  1. 可以实现服务编排需求,支持单项选择、并发、子流程、参数转换、参数映射、服务执行状态判断、异常捕获等功能

示例状态图:

示例状态图

五、XA模式

5.1 使用XA模式的前提

  • 支持XA事务的数据库。
  • Java应用,通过JDBC访问数据库。

5.2 XA模式的整体机制

Seata定义的分布式事务框架内,利用事务资源(数据库、消息服务等)对XA协议的支持,以XA协议的机制来管理分支事务的一种事务模式。

注意这里的重点:利用事务资源对XA协议的支持,以XA协议的机制来管理分支事务。

img

5.3 XA模式的工作机制

5.3.1 整体运行机制

XA模式运行在Seata定义的事务框架内:

xa-fw

5.3.2 数据源代理

XA模式需要获取XAConnection,获取XAConnection两种方式:

  • 方式一:要求开发者配置XADataSource
  • 方式二:根据开发者的普通DataSource来创建。

第一种方式,给开发者增加了认知负担,需要为XA模式专门去学习和使用XA数据源,与透明化XA编程模型的设计目标相违背。
第二种方式,对开发者比较友好,和AT模式使用一样,开发者完全不必关心XA层面的任何问题,保持本地编程模型即可。

我们优先设计实现第二种方式:数据源代理根据普通数据源中获取的普通JDBC连接创建出相应的XAConnection

类比AT模式的数据源代理机制,如下:

ds1

实际上,这种方法是在做数据库驱动程序要做的事情。不同的厂商、不同版本的数据库驱动实现机制是厂商私有的,我们只能保证在充分测试过的驱动程序上是正确的,开发者使用的驱动程序版本差异很可能造成机制的失效。这点在Oracle上体现非常明显。参见Druid issuehttps://github.com/alibaba/druid/issues/3707

综合考虑,XA模式的数据源代理设计需要同时支持第一种方式:基于XA数据源进行代理。类比AT模式的数据源代理机制,如下:

ds2

XA start需要Xid参数。这个Xid需要和Seata全局事务的XIDBranchId关联起来,以便由TC驱动XA分支的提交或回滚。目前SeataBranchId是在分支注册过程,由TC统一生成的,所以XA模式分支注册的时机需要在XA start之前。将来一个可能的优化方向:把分支注册尽量延后。类似AT模式在本地事务提交之前才注册分支,避免分支执行失败情况下,没有意义的分支注册。这个优化方向需要BranchId生成机制的变化来配合。BranchId不通过分支注册过程生成,而是生成后再带着BranchId去注册分支。

5.4 XA模式的使用

从编程模型上,XA模式与AT模式保持完全一致。可以参考Seata官网的样例:seata-xa。样例场景是Seata经典的,涉及库存、订单、账户3个微服务的商品订购业务。在样例中,上层编程模型与AT模式完全相同。只需要修改数据源代理,即可实现XA模式与AT模式之间的切换。

@Bean("dataSource")
public DataSource dataSource(DruidDataSource druidDataSource) {
    // DataSourceProxy for AT mode
    // return new DataSourceProxy(druidDataSource);

    // DataSourceProxyXA for XA mode
    return new DataSourceProxyXA(druidDataSource);
}

六、模式选择

SeataAT模式和TCC模式是在生产中最常用。

  • 强一致性模型,AT强一致方案模式主要用于核心模块,例如交易/订单等。
  • 弱一致性模型,TCC弱一致方案一般用于边缘模块例如库存,通过TC的协调,保证最终一致性,也可以业务解耦。

关于如何解决分布式事务问题的,可以分场景:

6.1 强一致性场景

对于那些特别严格的场景,用的是AT模式来保证强一致性;

例子:严格要求数据绝对不能错的场景(如电商交易交易中的库存和订单、优惠券),可以使用成熟的如中间件AT模式。

20210711130223963.png

AT模式,保障强一致性,支持跨多个库修改数据;

  • 订单库:增加订单
  • 商品库:扣减库存
  • 优惠券库:预扣优惠券

6.2 弱一致性场景

对于数据一致性要求没有那些特别严格、或者由不同系统执行子事务的场景,可以使用TCC保障弱一致性方案。

准备好例子:严格对数据一致性要求、或者由不同系统执行子事务的场景,如电商订单支付服务,更新订单状态,发送成功支付成功消息,只需要保障弱一致性即可。

20210711130524193.png

TCC模式,保障弱一致性,支持跨多个服务和系统修改数据,在上面的场景中,使用TCC模式事务。

  • 订单服务:修改订单状态
  • 通知服务:发送支付状态

6.3 最终一致性场景

基于可靠消息的最终一致性,各个子事务可以较长时间内异步,但数据绝对不能丢的场景。可以使用异步确保型事务事。可以使用基于MQ的异步确保型事务,比如电商平台的通知支付结果:

  • 积分服务:增加积分
  • 会计服务:生成会计记录

20210711130854311.png

posted @ 2022-04-25 15:37  夏尔_717  阅读(342)  评论(0编辑  收藏  举报