ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

浅谈分布式事务

2021-10-02 15:59:18  阅读:143  来源: 互联网

标签:事务 浅谈 本地 回滚 提交 一致性 分布式


文章目录

前几天面试,面试官让我谈谈分布式事务的ACID,当时我才发现一谈论ACID立马想到的都是关系型数据库、基于单机的分布式事务,而对于分布式事务,我仅仅依稀记得什么CAP定理、BASE特性。看来有必要总结一番了

理解事务

首先,单看事务这个概念,它是一组操作,这组操作只能有两个完成状态——要么成功,要么失败。
完成一个事务,最终能够从一个一致性状态(某个正确的状态),转移到另一个一致性状态。我们将事务可以细分为四个特性:原子性、隔离性、持久性、一致性。

总结:事务就是将一组操作看作一个原子操作执行,这些原子操作之间是隔离的,不能被打断,并且一旦执行完毕就永久生效。执行完毕一个原子操作,执行前正确的状态将转移至另一个正确的状态

上面这四个特性都是上层的东西、是概念性的东西,支持事务的产品需要进行实现。但是产品考虑到本身的定位、性能无法完全的实现以上四个特性。(标准是好的,但是也要考虑场景和成本)
例如,一些关系型数据库如mysql、oracle等在隔离性进行了妥协,没有采用严格的隔离性,而是提出隔离级别的概念包括读未提交、读已提交、可重复读、串行化。
再比如,内存数据库redis的事务更像是一个打包执行的命令,它不提供严格意义上的原子性、隔离性、持久性,包括一致性也没有实际的体现。它只是实现了“自解释”的事务罢了。

以上提到的产品实现了不严格的事务,单个服务器内容的事务称为单机事务或本地事务,而分布式事务则更加复杂,分布式事务中的各个操作往往分散在各个的单机节点中,往往依赖各节点的本地进行实现。

总结:事务是一个上层概念,ACID也是通过提取特性,为事务的实现提供依据,但是各个产品往往考虑到自身定位、性能而不去实现严格的事务,往往将其中某些特性进行弱化。分布式事务的实现也可以依据ACID的标准,但是往往也不能完全具有严格的ACID特性。

业务层事务与分布式事务

数据库产品一般都实现了事务如oracle、innoDB等。我们称他们为一个数据库事务,最简单情况就是单数据源(DB)的DAO接口调用,这个事务我们的业务逻辑实现直接依赖数据库事务即可,但是随着业务逐渐复杂,我们的业务逻辑往往需要操作多个数据源,甚至多个子系统,这个时候数据库事务就无法进行保证。

业务层(service)事务可以用来协调多个DAO接口调用的事务,如果我们去自己实现,往往需要通过try/catch包裹多个commit和rollback。如果service接口内部出现异常,那么将在catch块中依次回滚。引入spring框架后,我们也可以将逻辑层事务控制的任务交给spring提供的transactionManager进行管理,它将为各个声明了事务的service方法基于springAOP的动态代理技术,切入事务控制的逻辑。
transactionManager本身是不支持事务的,它只是一个协调者,根据不同的时机负责开启事务以及调用提交或者回滚接口。它底层还是依赖数据库实例的事务功能。

总结:最简单的单数据源场景,我们可以直接依赖数据库事务,而多数据源场景,我们需要引入一个事务协调者,控制各个数据库提交和回滚的时机。

上面的事务也可以看作分“步(step)”式事务,而现在我们要谈论的基于子系统调用的分布式(distribution)事务和上面的思路类似。基于子系统实现的分布式事务,其实就是依赖各个子系统本地的事务,我们需要找到一个更上层的事务协调者(如日志、中间件),来协调各个系统事务的步伐

分布式事务的目的是使得各个子系统构成的完整系统能够从一个一致性状态(正确的状态)到达另一个一致性状态。不止是多个数据源之间的事务协助,它是各个子系统之间的基于某种业务逻辑的事务协助

个人理解,分布式事务的实现思路也是类似业务层事务的实现思路,我们需要通过实现一个事务协调者,通过try/catch检测接口调用的情况。只不过这个接口不再是本地调用,需要通过网络实现远程调用,还需要考虑超时的问题,并且提供相应的容错机制。

ACID、CAP和BASE

CAP理论是设计分布式系统的基础理论,它指出一个分布式系统中,一致性(consistency)可用性(availability)分区容错性(partition tolerance)。最多只能满足其中两个。

一致性指的就是数据的强一致性,任意时刻,各个分布式子系统的数据必须是一样的。客户对数据进行修改操作,要么在所有的数据部分全部成功,要么全部失败——修改操作对于一份数据的所有副本(整个系统)而言,是原子的操作
可用性指的是正常响应,你调用分布式系统的一个接口,必须立刻得到响应(或者可以忍受的网络延迟下响应)。
分布式系统部署在多台机器上,空间上分布随意,往往没有主从之分,所有计算机节点都是对等的,互相之间是通过网络相连的,各个子系统的子集都可以看作一个“区”,如果子系统之间是连通的,那么他们是“连通的整体”,如果存在节点之间的网络故障,那么便从整体变成了若干个分区
除了所有节点故障以外,任何一个子节点集合的故障都不可以导致整个系统的不正常响应——分布式系统需要容忍子节点网络分区的问题,除非全部节点故障,否则依然能够运行。(例如订单服务崩了,注册服务仍然可以运行)

C和A是矛盾的,因为同步需要时间,访问接口的时候不能总是保证数据的强一致性,因此分布式系统根据场景一般采用AP或者CP架构。(所以这里应该看作,发送网络分区的时候,如果希望继续服务,系统的强一致性和可用性只能保证一样

一般的网络环境下,出现分区不可避免,因此分布式系统必须具备分区容错性。(CAP是针对分布式系统的理论,单机不存在“分区”的概念,整个系统都是一个war包部署在服务器上,所以存在单点故障的问题)

在分布式系统的环境中,如果出现了网络分区,我们对任意分区的节点进行读写请求,如果这个请求被执行(可用),那么其他本区的数据必然不一致(网络无法达到,不能同步)。而如果要保证数据的强一致性,此时必须禁止用户对其中任一区域的节点进行读写。因此CA是相悖的

对一致性要求高的场景,需要保证CP,例如zookeeper,在服务节点间数据同步的时候,服务对外不可用,但是保证每次读请求拿到的数据都是一致的。还有分布式缓存redis、mysql集群等分布式数据库集群。保证CP的应用,在数据同步的时候会阻塞用户的请求,会牺牲部分用户体验。(往往支持哨兵,当服务不可用的时候可以被哨兵进行发现并且选举leader执行故障转移)

对可用性要求比较高的场景需要AP,例如服务注册中心(目录检索服务)eureka就保证了可用性,每个节点都是对等的,eureka保证只要还存在一个节点,就可以继续提供服务。但是这个节点的数据可能不是最新的。

ACID要求事务是强一致性的,这里的一致指的是数据一致性。但是一味地追求强一致性对于分布式事务来说,并不是一个合理的解决方案。BASE是对AP的一个扩展(最终一致性理论),BASE通过牺牲强一致性来换取可用性,但是它保证了事务的最终一致性

BASE理论(BA 基本可用性、S 软状态、 E 最终一致性)是对CAP中强一致性和可用性的一个权衡的结果,它可以看作对CAP理论中AP方案的一个补充。我们无法做到强一致性,但每个应用都可以根据自身的业务特点,采用适当的方式来使得系统达到最终一致性

基本可用:当分布式系统出现不可预料的故障时,允许损失部分可用性。包括响应时间增加、部分非核心系统不可用
软状态:允许系统中的数据存在中间状态(不一致状态),但是该状态不影响整体可用性。允许数据同步的过程中,存在部分不一致状态。
最终一致性:系统保证在一段时间内使得各系统的数据达到最终一致的状态。

总结:ACID是事务强一致性的理论,更多用于单机事务尤其是数据库事务中,而分布式事务中,强一致性和可用性不可兼得,往往采用(更加强调可用性)最终一致性的方案实现事务,也可以看作是依据最终一致性BASE理论设计的。(当然了,分布式事务仍然可以采用强一致性设计如2PC、TCC等)

个人理解:ACID是用于实现事务本身的,而数据库产品如innoDB、oracle都提供了事务的实现,支持ACID,而分布式事务、业务层事务等底层依赖数据库事务保证ACID,但是从上层看,它们处于性能或者可用性考虑无法保证ACID,一般退而求其次,例如仅保证事务前后的最终一致性。

XA事务

各种组织机构为分布式事务提供了各种各样的解决方案和规范,XA是其中一种分布式事务的规范,XA规范主要定义了全局事务管理器和局部资源管理器之间的接口。本地数据库如innoDB在XA中扮演的角色就是资源管理器,而主服务的binlog就是一个协调者,通过xid实现。

事务协调者负责协调多个数据库的事务。第一阶段会询问各个数据库是否准备好,如果所有数据库处于prepared状态,则正式提交事务。如果任何一个没有准备好就回滚事务。

XA规范是基于2PC的,它实现了2PC协议。

2PC

2PC是一种强一致性设计。是一个同步阻塞协议。第一阶段具有超时机制,但是第二节点没法超时,只能不断重试(因为如果进行提交或回滚,可能有一部分人能够成功,那么剩下的另一部分事务只能不断重试,真不行就需要人工介入了),由于强调强一致性,因此协调者是一个单点,存在单点故障的风险。
2PC适用于数据库层面的分布式事务场景——一个应用多数据源的情况,类似业务层事务。Java的JTA是基于XA规范实现的事务接口(基于数据库的规范来实现2PC)

2PC即二阶段提交,引入了一个事务协调者去协调各个本地资源的提交与回滚。
【1】准备阶段,协调者向各个本地资源发送准备命令。(准备好:什么都处理完毕,只剩提交),这个阶段是同步等待的,只有收到各个资源的响应之后才会进入第二阶段。
【2】提交或者回滚阶段。根据第一节点本地资源的响应情况,协调各个事务进行统一地提交或者回滚。

第一阶段超时可以看作本地事务失败,但是第二阶段为了保证各个本地事务的一致性,必须不断重试,甚至人工介入。

由于严重依赖数据层面来搞定复杂的事务,效率很低,而且在分布式场景需要牺牲可用性,来换取一致性。因此不适合高并发场景。

mysql的XA事务

Mysql在存储引擎与插件之间的事务或存储引擎之间的事务,可以看作内部XA事务。最常见的分布式事务就是binlog和innoDB存储引擎之间的事务,由于主从复制的需要,绝大多数的数据库开启binlog。

Mysql的分布式事务指的是内部的多个事务之间的“分布式”,Mysql innoDB支持XA事务,需要定义一个全局唯一的XID,并告知每个事务分支要进行的操作。

这个XA事务最开始作为mysql innodb中binlog 和 redo log之间的事务保障,其中binlog是需要发给从服务器进行重放操作的。Binlog 中记录了XID,用来表示binlog是否成功调用sync。此时Binlog属于XA事务的协调器以binlog是否成功调用sync作为事务提交的标志。崩溃恢复后,将redo log中的xid与binlog的xid进行比较,如果binlog存在对应的xid则说明完整,redo log提交事务redo log在prepare阶段已经调用了redo log的sync调用,并且保存了binlog的最后一个xid),否则回滚事务。

在innoDB的事务提交阶段,引入XA事务的目的,是为了保证binlog写入和redo log写入是一个原子操作(为bin log与redo log开启事务保证)

引入prepare状态之前,事务只有提交与未提交两种状态,而且事务是基于存储引擎层面实现的。
如果没有prepare状态,mysql恢复后只有两种可能
【1】事务已提交,啥事情没有。
【2】事务未提交,那么就会回滚事务,但是这时有可能binlog已经落盘。这时候slave和master就会数据不一致。(innoDB的事务并没有保证binlog和redo log能够原子更新,因此需要引入XA事务
引入prepare后,mysql恢复后会多一个判断,如果处于prepare且binlog数据落盘,则事务直接提交。否则回滚(因为提交失败了)

XA事务的崩溃恢复:
【1】扫描最后一个binlog,提取xid(标识binlog的第几个event,xid标准标志着事务已经完成)
【2】xid也会写入redo,将redo 中prepare状态的xid和最后一个binlog的xid进行比较,如果存在则提交。(事务是不会跨binlog的)

3PC

XA事务是2PC的落地,3PC的提出用于解决2PC的一些缺点,但是整体开销更大,而且没有落地实现

3PC将分布式事务分为三个阶段:准备阶段、预提交阶段和提交阶段。其中准备阶段,协调者只是简单询问参与者的状态,而预提交阶段和提交阶段对应2PC的两个阶段。

多引入一个准备阶段,在正式提交之前询问资源的可用状态,一定程度避免同步阻塞阶段不必要的等待,但是多引入一次交互也会降低性能。另一方面,通过阶段的细分,可以降低崩溃恢复后的协调者的决策难度

3PC中,本地事务(参与者)引入了超时机制,如果协调者下线后,防止本地事务无限等待。

TCC

2pc和3pc都是数据库层面的,TCC是业务层面的分布式事务

针对每个操作,都有注册一个与其对应的确认和撤销(补偿)操作
1、try阶段,对各个服务的资源做检测,以及对资源进行锁定或者预留(冻结金额)。Try操作可以根据业务资源调整粒度
2、Confirm阶段,在各个服务中执行实际的操作(将之前预留的资源扣除)
3、Cancel阶段,如果任何一个服务等待业务方法执行出错,那么这里就需要进行补偿,执行已经成功的业务逻辑的回滚操作(将之前预留的资源释放)
三个步骤都是独立事务
如果其中任何一个事务执行操作,之前提交过的事务需要执行反向操作(补偿代码需要由程序员完成)
优势:不会出现锁定的长时间等待,性能高。
缺点:业务复杂,代码侵入性高,开发成本高。尤其需要考虑大量安全性(幂等性接口)
(整体来说不是强一致,补偿成功后算是最终一致性)

对应每个接口,需要定义TCC三个动作,因此对业务的侵入比较大,和业务紧耦合。其中conform和cancel可能设计多次调用,需要保证幂等性。

TCC仅提供两阶段原子提交协议,保证分布式事务的原子性。隔离性交给业务逻辑去实现——通过对业务的该值,将对数据库资源层面加锁上移至对业务层面加锁,从而释放底层数据库锁资源,拓展分布式事务锁协议,提供并发性

设计幂等

对请求进行唯一标识,一个请求最多只能成功执行一次。或者直接通过唯一约束实现这个唯一标识。例如我司通常为业务表额外创建流水表,只有将请求对应的记录成功插入流水表,才能执行后面的流程。当一个请求再次被请求,首先查询流水表,如果存在响应的记录则直接返回。
如果考虑提高效率,也可以将唯一标识存入redis中间件。

以上讨论的主要是post对应的insert操作,对应delete和update操作可以考虑乐观锁(version字段),对应每个修改或者删除请求,同时附带version条件,只有当version命中的时候才会执行响应操作。

本地消息表

本地消息表利用各系统本地的事务来实现分布式事务。
事务的执行 和 将消息放入本地消息表 这两个操作,需要放入同一个事务中

在这里插入图片描述

【1】A系统在自己本地的事务里操作的同时,插入一条数据到消息表。(这两个操作在同一个事务中)
【2】接着A系统将这个消息发送到MQ中
【3】B系统收到消息之后,在一个事务里,将自己本地信息表中插入一条数据,同时执行其他的业务操作,如果这个消息已经被处理过了,那么这个事务会回滚,保证不会重复处理消息。(该操作已经执行过了,避免二次执行,回滚该操作)
【4】B系统执行成功后,就会更新自己本地消息表的状态,以及A系统消息表的状态
【5】如果B系统处理失败了,就不会更新消息表的状态,此时系统A会定时扫描自己的消息表,如果有未处理的消息,就会再次发送到MQ中,让B再次处理
【6】这个方案保证了一致性,哪怕B事务失败了,但是A会不断重发消息,直到B成功。

一个操作调用成功则将消息表的状态修改为“已成功”,否则使用后台定时任务去读取本地消息表,筛选出未成功调用的消息对应的服务入队,重试。一般重试都有最大次数,如果超过最大次数可以报警,人工介入。

本地消息表,表现为最终一致性,容忍了数据暂时不一致的情况

该方案严重依赖数据库的消息表来管理事务,高并发的情况下很难扩展,同时还需要在数据库中额外添加一个与实际业务无关的消息表来实现分布式事务。耦合度高,需要在业务系统引入消息中间件,将导致系统复杂度增加

消息事务

使用MQ替代本地消息表,通过引入队列进行业务解耦。

将本地操作和发送消息的动作放入同一个本地事务,下游应用从消息系统订阅该消息,收到消息后执行相应的操作,本质上是依赖消息的重试机制达到最终一致性

部分队列产品支持事务消息,如rocketMQ,类似2pc。

RocketMQ在事务实现中,增加了事务反查的机制来解决时序信息提交失败的问题,如果producer在提交或者回滚事务的时候发生网络异常,rocketMQ的broker没有收到提交或者回滚的请求,broker会定期去producer上,反查这个事务在本地事务的状态,然后决定是否回滚或者提交这个事务

我们的业务代码一般需要实现一个反查本地事务状态的接口,告知rocketMQ 中间件,本地事务是成功的还是失败的。

消息事务本质上,将本地消息表放到队列上,解决生产者的消息发送与本地事务执行的原子性问题

尽最大努力通知

本地消息表、消息事务其实都算是“最大努力”。
【1】本地消息表,会有后台任务定时查看未完成的消息,并调用对应的方法。如果失败次数超过一个阈值则进行人工介入、或直接舍弃。
【2】事务消息类似,如果订阅者一直不消费,或者消费一直失败,则进入坏死信息队列

最大努力同时表明了一种柔性事务——仅最大努力达成事务的最终一致性。适用于对时间不敏感的业务如短信通知。

Acid对一致性要求很高,事务执行的过程中必须对所有资源进行锁定。而柔性事务的理念:通过业务逻辑,将互斥锁从资源层面转移到业务层面,放宽对强一致性的要求,换取系统吞吐量的上升。(例如,先用户查出提示:请不要介意我们的时延)。由于分布式事务可能会出现超时重试的情况,因此柔性事务的操作必须是幂等的

系统A的本地事务执行完毕之后,就发送到MQ。这里有一个专门消费MQ的最大努力通知服务。这个服务会消费MQ中的记录,然后数据库中记录下来,或者放入内存队列,接着调用系统B的接口。如果系统B调用失败,则最大努力通知服务就定时尝试重新调用系统B,重试直到一个阈值

没有回滚功能(只能不断提交重试,最终可能需要人工补偿),但是对场景要求很严苛。
优点:无锁定资源时间、性能损坏小。
缺点:尝试多次提交失败后无法回滚,仅适用于事务最终一定能够成功的业务场景。
它通过对事务回滚功能的妥协,换取性能的提升

对于分布式事务,建议在跨数据分片的情况下使用柔性事务,保证数据最终一致性,换取最佳性能。同一数据分片内,使用本地事务,满足ACID特性

补充:SAGA模型
适用于长事务场景,sage模型将一个分布式事务拆分为多个本地事务,每个本地事务都有相应的执行模块(transaction)和补偿模块(compensation)。任何一个本地事务出错,都可以调用相关的补偿方法,实现事务的最终一致性。

总结

分布式事务,每个节点本身的事务是具有ACID特性的,但是多个节点之间如果需要保证ACID就需要通过一些解决方案去控制。例如2PC或3PC以及TCC,记录每个事务的执行状态,如果有一个出现错误或者超时就进行全部回滚或者补偿。这就保证了原子性。
每个事务节点都是具备持久性的和隔离性的,因此分布式事务的持久性依赖每个节点本身提供了功能。以上对原子性、隔离性和持久性的保证,共同去保证了一致性。

一味地追求强一致性对于分布式事务来说,并不是一个合理的解决方案
对于分布式事务,一般在跨数据分片的情况下使用柔性事务,保证数据的最终一致性,获取最佳性能。在同一数据分片使用本地事务,满足ACID强一致性。

2pc和3pc是一种强一致性事务,不过仍然存在数据不一致、阻塞的情况,只能用于数据库层面。
TCC是一种补偿性事务思想,适用范围更广,基于业务层面实现,因此对业务的侵入性更大。每一个操作需要提供三种方法。
本地消息表、事务消息和最大努力通知都是最终一致性事务,适用于对时间不敏感的业务。(如邮件通知、短信通知有时候能隔半分钟)

标签:事务,浅谈,本地,回滚,提交,一致性,分布式
来源: https://blog.csdn.net/qq_44793993/article/details/120459905

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有