一文彻底搞懂分布式事务

发布时间:2024年01月21日

目录

什么是分布式事务

分布式事务-理论模型

X/Open分布式事务模型

两阶段提交协议

三阶段提交协议

CAP定理和BASE理论

分布式事务-常见柔性解决方案

TCC

基于可靠性消息的最终一致性

最大努力通知型

分布式事务框架Seata

AT模式

Saga模式

Seata主推的AT模式实现原理

第一阶段实现原理

第二阶段实现原理

事务的隔离性——写隔离和读隔离


什么是分布式事务

说到事务,我们第一个想到的应该是数据库的事务,一个逻辑单元中的多个业务操作,要么全部成功,要么全部失败,必须满足ACID四个特性,它们分别是:

  • 原子性(Atomicity):事务必须是不可继续分割的原子工作单元,要么全部成功,要么全部失败。
  • 一致性(Consistency):事务完成时所有的数据必须一致。
  • 隔离性(Isolation):不同事务之间必须互不影响。
  • 持久性(Durability):事务执行完成后对系统的影响是永久的。

这里的ACID特性,是针对单体架构下单库单表的事务需要的。随着业务的发展,单体架构慢慢演变为微服务架构,同时跟着变化的是业务的拆分和数据库的拆分。一个比较典型的场景是,电商系统的订单和库存业务,分别拆分成了独立的服务,用户下单的时候,订单服务完成订单的创建,库存服务完成库存的扣减,如下图所示:

在微服务架构这样的场景下,原本单个库的事务操作变成了多个库的事务操作,每个库的执行情况相互之间是不知道的,可能订单创建成功,但是库存扣减失败。这就是我们接下来要聊的分布式事务问题。

那如果要给分布式事务下一个定义,会是怎么样的呢?下面是一个参考的定义:

事务的参与者、支持事务的服务器、资源服务器及事务管理器分别位于分布式系统的不同节点上。?

分布式事务-理论模型

分布式事务产生的核心原因是存储资源的分布性,比如多个数据库,或者MySQL和Redis两种不同存储设备的数据一致性等。这里会有一个特例,一个数据库,不同的服务跨了JVM,也会产生分布式事务问题。我们在实际应用中,能避免分布式事务问题就应该去避免,以免为系统带来额外的复杂度。

X/Open分布式事务模型

X/Open DTP(X/Open Distributed Transaction Processing Reference Model)是X/Open 这个组织定义的一套分布式事务的标准。这个标准提出了使用两阶段提交来保证分布式事务的完整性。如下图所示,X/Open DTP中包含以下三种角色:

  • AP(Application):应用程序
  • RM(Resource Manager):资源管理器,如数据库。
  • TM(Transaction Manager):事务管理器,一般指事务协调者,负责协调和管理事务。

事务执行流程图如下:

  • 配置TM,把多个RM注册到TM,相当于TM注册RM作为数据源。
  • AP从TM管理的RM中获取连接,如果RM是数据库则获取JDBC连接。
  • AP向TM发起一个全局事务,生成全局事务ID(XID),XID会通知各个RM。
  • AP通过第二步获得的连接直接操作RM完成数据操作。这时,AP在每次操作时会把XID传递给RM。
  • AP结束全局事务,TM 会通知各个RM全局事务结束。
  • 根据各个RM的事务执行结果,执行提交或者回滚操作。

?需要注意的是,TM和多个RM之间的事务控制,是基于XA协议(XA Specification)来完成的。XA协议是X/Open提出的分布式事务处理规范,也是分布式事务处理的工业标准,它定义了xa_和ax_系列的函数原型及功能描述、约束等。目前 Oracle、MySQL、DB2都实现了XA接口,所以它们都可以作为RM。

接下来我们再来继续看看大家常说的两阶段提交协议和三阶段提交协议。

两阶段提交协议

从上面我们可以看出,TM实现了对多个RM事务的管理,这里会涉及两个阶段的提交,第一个阶段是事务的准备阶段,第二个阶段是事务的提交或者回滚阶段,这两个阶段都是由TM事务管理器发起的。具体执行流程如下:

  • 准备阶段:TM通知RM准备分支事务,并记录事务日志,然后告诉TM自己的准备结果。
  • 提交/回滚阶段:如果所有的RM在准备阶段都返回成功,那么TM向所有RM发起事务提交指令完成数据的修改。否则,如果有任何一个RM在准备阶段返回失败,那么TM向所有RM发起事务回滚指令。

完整的执行流程图如下:

存在的问题:

  • 同步阻塞——所有的RM都是同步阻塞的,对每一个指令都要有明确的响应才会继续往下执行。
  • 过于保守——任何一个节点失败都会导致事务回滚。
  • TM的单点故障——如果TM出现故障,所有RM都会处于等待状态。
  • "脑裂"导致数据不一致——如果因为网络问题,导致在第二阶段,只有部分RM接收到了提交事务的指令,然后完成数据的更新。最会会导致系统数据不一致。

三阶段提交协议

三阶段提交协议在二阶段的基础上,增加了超时机制来解决同步阻塞的问题。具体执行流程如下:

  • CanCommit(询问阶段):TM向所有RM发送事务执行请求,询问是否可以执行,RM只需要回答是或者否,不需要执行任何操作。这个阶段会有超时中止机制。
  • PreCommit(准备阶段):如果询问阶段RM都返回可以执行,那么TM会发送PreCommit请求,各个RM收到请求后写redo和undo日志,执行本地事务但不提交事务,然后等待TM的进一步通知。如果询问阶段有任何一个RM返回否,TM会发送事务中断请求。
  • DoCommit(提交/回滚阶段):根据准备阶段的结果,如果都返回成功,那么TM发送事务提交指令。如果任何一个RM返回失败,那么TM发送事务回滚指令。

完整的执行流程图如下:

?三阶段提交协议,相对于两阶段提交协议,它的优势在于:

  • 增加了询问阶段(CanCommit),询问所有RM是否可以执行事务,尽早发现无法执行的操作,提前中止后续的行为。
  • 在准备阶段,TM和RM都引入了超时机制,一旦超时TM和RM会继续提交事务,并且默认事务成功。

CAP定理和BASE理论

不管是两阶段提交协议,还是三阶段提交协议,本质上都是保证数据一致性的强一致性解决方案,是XA协议解决分布式数据一致性问题的基本原理。在实际应用当中,除非对数据一致性有严格要求必须强一致性,比如银行、金融等系统,一般情况下我们不会选择强一致性解决方案。

接下来我们要说一下分布式事务的两个理论模型:CAP定理和BASE理论。

CAP定理

在分布式系统中,不可能同时满足一致性(C-Consistency,多个副本的数据要保持一致)、可用性(A-Avalibility,服务一直处于可用)、分区容错性(P-Partition Tolerance,任何网络分区故障,系统任然可对外提供服务)这三个需求,最多同时满足两个。

由于网络通信并不是绝对可靠的,即使出现网络故障,系统任要能对外提供服务。所以分区容错性是一定存在的,最后我们就只能选择AP或者CP模式来保证分布式系统中的数据一致性问题。为何不能是CA模式呢?如果选择了CA,那么网络通信是绝对可靠的。如果网络出现故障,为了保证C,那么就要拒绝客户端的请求,这就没办法保证A。

AP:放弃强一致性,保证最终一致性,这是目前多数场景下解决分布式数据一致性的方案。

CP:放弃高可用,实现强一致性,前面的两阶段或三阶段提交协议就是这种模式,用户的体验可能没那么好,需要等待更长的时间。

BASE理论

由于CAP定理中无法同时满足一致性和可用性,就衍生出了BASE理论,其核心思想就是通过牺牲一致性来保证可用性,只要数据能最终达到一致就行,它的三个特性如下:

  • 基本可用(Basically Available):分布式系统出现故障,在保证核心功能可用的情况下,可以让一些功能不可用,比如通过限流、降级、熔断等方式。
  • 软状态(Soft State):不同数据副本,允许存在不一致的情况,也就是一些中间状态。
  • 最终一致性(Eventually Consistent):中间状态的数据经过一段时间后会达到最终一致性。

在实际应用当中,大多数情况下我们会选择BASE理论来实现数据的最终一致性,对用户来说,系统的可用性大部分时候更重要。

分布式事务-常见柔性解决方案

TCC

TCC补偿型方案把一个完整的业务拆分成了下面三个步骤:

  • Try:校验数据或者预留资源;
  • Confirm:执行对应的操作,操作的是Try阶段预留的资源;
  • Cancel:取消操作,释放Try阶段预留的资源。

TCC执行流程图如下:

?该方案比较明显的缺点就是对业务侵入性比较大,每一个业务操作都要准备一个Confirm方法和Cancel方法。

基于可靠性消息的最终一致性

该方案是利用消息中间件的可靠机制来实现数据的一致性保证。其执行流程图如下:

  • 生产者发送Half消息到消息队列,此时消费者无法消费;
  • 生产者执行本地事务;
  • 上一步执行成功,发送Commit消息到消息队列,此时消费者可以消费;否则,前面发送的Half消息会被删除;
  • 如果生产者因为某些原因一直没有发送确认消息给消息队列,消息队列会定时回查,通过生产者提供的回查接口检查本地事务执行结果,然后根据结果确认消息是否需要投递给消费者;
  • 消息队列上的消息经过生产者确认之后,消费者就可以消费了,消费成功之后会发送消息队列一个成功标识,说明消息以及被消费者成功处理。

最大努力通知型

该方法和基于可靠性消息的最终一致性方案类似,最典型的应用场景就是支付场景的回调。

?这里的最大努力通知是指,在客户端如果没有一个确定的状态时,也就是因为一些异常原因,没有成功更新业务系统的订单状态,支付宝会不断重试,直到客户端收到消息确认或者达到最大重试次数。

分布式事务框架Seata

Seata是一款开源的分布式事务解决方案,它提供了AT、TCC、Saga和XA四种事务模式,为开发者提供了一站式的分布式事务解决方案。在实际开发过程中,只需要一个注解即可解决分布式事务问题。

AT模式

AT模式是Seata主推的分布式事务解决方案,基于XA演变而来,它同样有TM(事务管理器)、RM(资源管理器)和TC(事务控制器)三种角色,其中TC作为Seata的服务端独立部署。AT事务模式如下图:

其具体执行流程如下:

  • TM向TC注册全局事务,并生成全局唯一的XID(事务ID)。
  • RM向TC注册分支事务,同时归入到XID的事务管理范围。
  • RM向TC汇报资源的准备状态。
  • TC汇总所有事务参与者(RM)的执行状态,然后决定分布式事务是提交还是回滚。
  • TC通知所有RM提交或者回滚各自的事务操作。

Saga模式

Saga模式又叫长事务解决方案,它的核心思想是把一个业务流程中的长事务拆分为多个本地短事务。

从上图我们看到,Saga模式是由一系列短事务Ti组成,每一个Ti都对应了一个补偿事务,每一个Ti都会真实影响数据库数据。?该模式我们在这里就只做简单了解,本文我们主要还是深入学习主推的AT模式。

Seata主推的AT模式实现原理

AT模式的整体机制本质上是一个改进版的两阶段提交协议,它的两个阶段分别是:

  • 第一个阶段:同一个本地事务中完成业务数据的操作和回滚日志的记录,释放本地锁和连接资源。
  • 第二个阶段:异步提交,快速完成。如果失败回滚,只需要通过第一阶段记录的回滚日志进行反向补偿。

下面我们就一起来分析AT模式每一个阶段的实现原理,下面是我们分析要用的测试表结构(表名为tbl_repo)及数据。

第一阶段实现原理

这里我们拿库存扣减作为例子,在执行库存扣减的数据库操作时,seata会解析执行的sql,然后将修改前后的业务数据保存到undo_log日志表中,业务数据的修改和回滚日志的记录在本地同一个事务中完成。完整的执行流程如下图:

?假设这里我们要进行库存扣减的业务操作sql如下:

update tbl_repo set count = count - 1 where product_code = 'GP20200202001'

?那么第一阶段做了如下事情:

  • seata解析业务sql,得到sql的类型为update更新操作,对应的表是tbl_repo,要操作的数据需要满足的条件where等信息。
  • 获取业务sql执行之前的数据镜像,根据前面解析得到的条件进行查询,这里可以得到数据对应的主键,后面会用到。
select id, product_code, name, count from tbl_repo where product_code = 'GP20200202001'
  • 执行业务操作update。
  • 获取业务操作之后的数据镜像,这里需要根据前面获取到的主键进行查询(其他信息可能已经发生变化,所以这里不能用业务sql同样的条件进行查询)。
  • 保存回滚日志,业务操作执行前后的数据镜像和业务sql组成一条日志记录,保存到UNDO_LOG表中。

?这里的rollback_info信息会是下面这个样子:

  • 本地事务提交之前,先向事务控制器TC注册分支事务,这里会申请tbl_repo表中对应记录的全局锁?(全局锁后面会有分析)。
  • 提交本地事务,业务操作和UNDO_LOG表同时提交。
  • 本地事务执行结果报告给TC。

第二阶段实现原理

事务控制器TC收到所有分支事务上报的结果之后,决定全局事务是提交还是回滚。

事务提交

如果是全局事务提交,那么只需要清理对应的UNDO_LOG日志即可,因为这个时候所有分支事务已经完成提交。

事务回滚

如果是全局事务回滚,那么就根据记录的UNDO_LOG日志,进行相应事务的补偿。

从前面的分析,我们可以知道,UNDO_LOG日志中记录了业务数据修改之后的数据镜像,通过全局事务ID和分支事务ID 找到UNDO_LOG记录,拿到记录中的afterImage,然后和当前业务表中的数据进行比较,如果相同,说明可以回滚;如果不相同,说明其他事务对业务表中的数据进行了修改,不能进行回滚。

事务的隔离性——写隔离和读隔离

我们知道,在多个并发事务同时操作同一个表的同一条数据的时候,它们之间不能相互干扰,这就是我们接下来要说的事务的隔离性。

AT模式下,事务的隔离性是通过全局锁来实现的。

写隔离

在第一个阶段本地事务提交之前,确保拿到全局锁,如果拿不到全局锁,则不能提交本地事务。并且获取全局锁的尝试会有一个范围限制,如果超出范围将会放弃全局锁的获取,并且回滚该事务,释放本地锁。

案例:假设有两个全局事务TX1和TX2,分别对 tbl_repo表的count字段进行更新操作,count的初始值为100。

TX1先执行,开启本地事务,拿到本地锁(数据库级别锁),更新count=count-1=99。在本地事务提交之前,需要拿到该记录的全局锁,然后提交本地事务并释放本地锁。

TX2接着执行,同样先开启本地事务,拿到本地锁,更新count=count-1=98。本地事务提交之前,也尝试获取该记录的全局锁(全局锁由TC控制,由于该全局锁已经被TX1获取了,所以TX2需要等待以重新获取全局锁。如果全局事务执行整体提交,那么提交时图如下图所示:

如果TX1在第二阶段执行全局回滚,那么TX1需要重新获得该数据的本地锁,然后根据UNDO_LOG进行事务回滚。此时,如果TX2仍然在等待该记录的全局锁,同时持有本地锁,那 TX1分支事务的回滚会失败。TX1分支事务的回滚过程会一直重试,直到TX2的全局锁获取超时,放弃全局锁并回滚本地事务、释放本地锁,之后TX1的分支事务才会回滚成功。而在整个过程中,全局锁在TX1结束之前一直被TX1持有,所以不会发生脏写的问题。全局事务回滚时序图如下图所示:

读隔离

数据库事务的4中隔离级别:Read Uncommitted(读未提交);Read Committed(读已提交);Repeatable Read(可重复读);Serializable(可串行化)。

在数据库本地事务隔离级别为Read Committed 或者以上时,Seata AT事务模式的默认全局事务隔离级别是Read Uncommitted。在该隔离级别,所有事务都可以看到其他未提交事务的执行结果,产生脏读。这在最终一致性事务模型中是允许存在的,并且在大部分分布式事务场景中都可接受脏读。

在某些特定场景中要求事务隔离级别必须为Read Comitted,目前Seata是通过SelectForUpdateExecutor执行器对SELECT FOR UPDATE语句进行代理的,SELECT FOR UPDAT语句在执行时会申请全局锁。

如下图所示,如果全局锁已经被其他分支事务持有,则释放本地锁(回滚SELECT FOR UPDATE语句的本地执行)并重试。在这个过程中,查询请求会被“BLOCKING”,直到全局锁被拿到,也就是读取的相关数据己提交时才返回。

文章来源:https://blog.csdn.net/weixin_39283212/article/details/134634007
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。