分布式事务处理及分布式事务处理的特性包括哪些

Yanyan 1163 2023-10-23

分布式事务的六种处理方式

严格意义来说事务实现应该是具备原子性、一致性、隔离性和持久性,简称ACID

分布式事务处理及分布式事务处理的特性包括哪些

原子性(Atomicity),可以理解为一个事务内的所有操作要么都执行,要么就都不执行。

一致性(Consistency),可以理解为数据是满足完整约束的,也就是说不会存在中间状态的数据,比如说你有400块,我有100块,你给我两百块,此时你的手中绝对不可能还是400块,应该是只有200块,而我也会变成300块,并不会是原来的100块,不会存在钱没扣没增的中间状态。

隔离性(Isolation),指的是多个事务并发执行的时候不会相互打扰,即使一个事务与其他事务隔离。

持久性(Durability),指的是一个事务执行完毕后他的数据会被永久保存下来,后面的其他操作和执行结果都不会对其造成任何影响,除非是人为破坏。

通俗的说事务就是使一些更新操作要么都成功,要么都失败。

事务提交

事务的提交是指事务里的所有操作都正常完成。

事务回滚

事务的回滚是指程序或数据处理错误,将程序或数据恢复到上一次正确状态的行为。

这里就有一个变数,那就是redis,因为redis的事务不能保证原子性,但为什么它也叫事务。

首先你要知道一般的中间件都会夸大其词,不吹牛逼,怎么来吸引更多的人使用他们的产品。

一般而言他们既然敢说出他们实现了什么什么,要么是真的实现了,要么是在某种特殊、特定或者极短的环境下才能满足功能。

Redis是这样说的:值得注意的是,即使一个命令失败了,队列中的所有其他命令都被处理了,redis也不会停止对命令的处理。这句话告诉了大家事务中的某个命令如果失败了,之后的命令还是会被处理,redis不会停止处理命令,这也意味着它也不会回滚。这都已经偏离了事务最核心的本质了。但是你可以看看redis是怎么解释的:redis命令只有在使用错误的语法调用时才会失败,并且在命令排队期间不能检测到问题,或者针对持有错误数据类型的键:这意味着,在实践中,失败的命令式编程错误的结果,而且这种错误可能在开发过程中检测到,而不是在生产中。Redis内部简化更快,因为它不需要回滚的能力。

Redis官方解释了为什么不支持回滚,他们首先说命令出错是语法使用的问题,是自己变成出错,在开发的时候就应该检测出来,不应该在生产环境中出现。

Redis就是为了块!不需要回滚。

还有redis说就算提供回滚也没用,你这代码都写错了,回滚并不能使你免于编程错误。而且一般这种错也不可能把它带入生产环境中去,所以他的方法简单粗暴并且快速,并不支持回滚。难道这一切都是自己的问题吗,如果代码检测不出来错误,在生产时突然报错怎么办,那么只要是语法错误,redis就不提供回滚嘛,这个就是redis的一个不好的地方。

分布式事务

分布式事务顾名思义就是要在分布式系统中实现事务,他其实是由多个本地事务组合而成。

对于分布式事务而言几乎满足不了ACID,其实对于单机事务而言大部分情况下也没有满足ACID,不然怎么会有四种隔离级别,所以更别说分布在不同数据库或者不同应用上的分布式事务了。

2PC

2PC(Two-phase commit protocol),中文叫二阶段提交。二阶段提交是一种强一致性设计,2PC平常引入一个事务协调者的角色来协调管理各参与者(也可称之为各本地资源)的提交和回滚,二阶段分别指的是准备(投票)合体较量各个阶段。

主义者只是协议或者说是理论指导,之阐述了大方向,具体落地还是会有差异的。

接下来是两个阶段的具体流程。

准备阶段

协调者会给各参与者发送准备命令,你可以把准备命令理解成除了提交事务之外啥事都做完了。

同步等待所有资源的相应之后就进入第二阶段即提交阶段(知已提交阶段不一定是提交事务,也可能是回滚事务)。

加入在第一阶段所有参与者都返回准备成功,那么协调者则想所有参与者发送提交事务命令,然后等待所有事物都提交成功之后,返回事务执行成功。 

加入在第一简短有一个参与者返回失败,那么协调者就会向所有参与者发送会话事务的请求,即分布式事务执行失败。 

如果二阶段提交失败了,有两种情况。

第一种是第二阶段执行的是回滚事务操作,那么答案就是不断重试,直到所有参与者都回滚了,不然那些在第一阶段准备成功的参与者会一直阻塞着。

第二种是第二阶段执行的是提交事务操作,那么答案也是不断重试,因为有可能一些参与者的事务已经提交成功了,这时候只有一条路,就是头铁往前冲,不断地充实,知道提交成功,到最后真的不行只能人工介入处理。

大体上二阶段提交的流程就是这样的,我们再来看看细节。

首相2PC是一个同步阻塞协议,想第一阶段协调者会等待所有参与者响应才会进行下一步操作了,当然第一阶段的协调者有超时机制,假设因为网络原因没有收到某参与者的响应或者某参与者挂了,那么超时后就会判断事务失败,向所有参与者发送回滚命令。

在第二阶段协调者的没法超市,因为按照我嗯上面分析只能不断重试!

协调者故障分析

协调者是一个单点,存在单点故障问题

假设协调者在发送准备命令之前挂了,那就等于事务还没开始。

假设协调和在发送准备命令之后挂了,这就不太行了,有些参与者等于都执行了处于事务资源锁定的状态。不仅事务执行不下去,还会因为锁定了一些公共资源而阻塞系统其他的操作。

假设协调者在发送回滚事务命令之前挂了,那么事务也是执行不下去,且在第一阶段那些准备成功参与者都阻塞着。

假设协调者在发送回滚事务之后挂了,这个还行,至少命令发出去了,很大的概率都会回滚成功,资源都会释放。但是如果出现网络分区问题,某些参与者将因为收不到命令而阻塞着。

假设协调者在发送提交事务命令之前挂了,这个不行,直接蹦,这下是所有资源都阻塞着。

假设协调者在发送提交事务命令之后挂了,这个还行,也是至少是命令发出去了,很大概率都会提交成功,然后释放资源,但是如果出现网络分区问题某些参与者将因为收不到命令而阻塞着。

协调者故障,通过选举得到新的协调者

因为协调者单点问题,因此我们可以通过选举的操作选出一个新协调者来顶替。

如果出于第一阶段,其实影响不大都回滚好了,在第一阶段事务肯定还没提交。

如果处于第二阶段,假设参与者都没有挂,此时新协调者可以向所有参与者确认他们自身情况来推断下一步操作。

假设有个别参与者挂了!这就有点僵硬了,比如协调者发送了回滚命令,此是第一个参与者收到了并执行,然后协调者与第一个参与者都挂了。

此时其他参与者都没收到请求,然后新协调者来了,它询问其他参与者都说OK,但他不知道挂了的那个参与者O不OK,所以它傻了。

问题其实就出在每个参与者自身的状态只有自己和协调者知道,因此新协调者无法通过在场的参与者的状态推断出挂了的参与者是什么情况。

虽然协议上没说,不过在实现的时候我们可以灵活的让协调者将自己发过的请求在哪个地方记一下,也就是日志记录,这样新协调者来的时候不就知道此时该不该发了嘛!

但就算协调者知道自己该法提交请求,那么在参与者也一起挂了的情况下没有,因为你不知道参与者在挂之前有没有提交事务。

如果参与者在挂之前事务提交成功,新协调者确定存活者的参与者都没问题,那肯定得向其他参与者发送提交事务命令才能保证数据一致。

如果参与者在挂起之前事务还未提交成功,参与者恢复了数据之后是回滚的,此时协调者必须是向其他参与者发送回滚事务命令才能保证事务的一致。

所以说极端情况下还是避免不了数据不一致的问题。

talk is cheep 让我们再来看下代码,可能会更加的清晰,可能更加的清晰。以下代码取自 <<Distributed System: Principles and Paradigms>>。

这个代码就是实现了 2PC,但是相比于2PC增加了写日志的动作、参与者之间还会互相通知、参与者也实现了超时。这里要注意,一般所说的2PC,不含上述功能,这都是实现的时候添加的。

协调者:


    write START_2PC to local log; //开始事务
    multicast VOTE_REQUEST to all participants; //广播通知参与者投票
    while not all votes have been collected {
        wait for any incoming vote;
        if timeout { //协调者超时
            write GLOBAL_ABORT to local log; //写日志
            multicast GLOBAL_ABORT to all participants; //通知事务中断
            exit;
        }
        record vote;
    }
    //如果所有参与者都ok
    if all participants sent VOTE_COMMIT and coordinator votes COMMIT {
        write GLOBAL_COMMIT to local log;
        multicast GLOBAL_COMMIT to all participants;
    } else {
        write GLOBAL_ABORT to local log;
        multicast GLOBAL_ABORT to all participants;
    }

参与者:


write INIT to local log; //写日志
    wait for VOTE_REQUEST from coordinator;
    if timeout { //等待超时
        write VOTE_ABORT to local log;
        exit;
    }
    if participant votes COMMIT {
        write VOTE_COMMIT to local log; //记录自己的决策
        send VOTE_COMMIT to coordinator;
        wait for DECISION from coordinator;
        if timeout {
            multicast DECISION_REQUEST to other participants; //超时通知
            wait until DECISION is received;  /* remain blocked*/
            write DECISION to local log;
        }
        if DECISION == GLOBAL_COMMIT
            write GLOBAL_COMMIT to local log;
        else if DECISION == GLOBAL_ABORT
            write GLOBAL_ABORT to local log;
    } else {
        write VOTE_ABORT to local log;
        send VOTE_ABORT to coordinator;
    }


每个参与者维护一个线程处理其它参与者的DECISION_REQUEST请求:


    while true {
        wait until any incoming DECISION_REQUEST is received;
        read most recently recorded STATE from the local log;
        if STATE == GLOBAL_COMMIT
            send GLOBAL_COMMIT to requesting participant;
        else if STATE == INIT or STATE == GLOBAL_ABORT;
            send GLOBAL_ABORT to requesting participant;
        else
            skip;  /* participant remains blocked */
    }

至此我们已经详细的分析了2PC的各种细节,总结一下。

2PC是一种尽量保证强一致性的分布式事务,因此他是同步阻塞的,而同步阻塞就导致长久的资源锁定问题,总体而言效率低,并且存在单点故障问题,在极端条件下存在数据不一致的风险。

淡然具体的实现可以变形,并且2PC也有变种,例如Tree2PC、Dynamic2PC。

还有一点,2PC适用于数据库层面的分布式事务场景,而我们业务需求有时候不仅仅关乎数据库,也有可能是上传一张图片或者发送一条短息。

而且像Java中的JTA只能解决一个应用下多数据库的分布式事务问题,跨服务了就不能用了。

简单说下Java的JTA,他是基于XA规范实现的事务接口,这里的XA你可以简单理解为基于数据库的XA规范来实现的2PC。

接下来我们来扩展一下知识,数据库的XA规范是什么?XA规范如何定义?

XA是有X/Open组织提出的分布式事务规范,XA规范主要定义了事务协调者(Transaction Manager)和资源管理器(Resource Manager)之间的接口。

事务协调者(Transaction Manager):因为XA事务是基于两阶段提交协议的,所以需要有一个协调者,来保证所有的十五参与者都完成了准备工作,也就是2PC的第一阶段。如果事务协调者收到所有参与者都准备好的消息,就会通知所有事务者可以提交,也就是2PC的第二阶段。

在前面的内容中我们提到过,之所以需要引入事务协调者,是因为在分布式系统中,两台机器理论上无法达到一致的状态,需要引入一个单点进行协调。协调者,也就是事务管理器控制着全局事务,管理事务的生命周期,并协调资源。

资源管理器(Resource Manager):负责控制和管理实际资源,比如数据库活JMS队列。

目前,主流数据可都提供了对XA的支持,在JMS规范中,记Java消息服务中,也给予XA定义了对事物的支持。

这里又不得不说一下JMS队列了,JMS就是Java消息服务应用程序接口,是一个Java平台中关于面向消息中间件的API,用于在两个应哟个程序之间,或分布式系统中发送消息,进行异步通信。Java消息服务是一个与具体平台无关的API,绝大多数MOM(面向消息额中间件)提供商都对JMS提供支持。

JMS是一种与厂商无关的API,用来访问消息收发系统消息,它类似于JDBC。

为什么使用JMS,三种状态:解耦、异步、削峰。

1、解耦:

传统模式: 

缺点:如果存在多个系统,每个系统间的耦合性都机枪。如果后来又心得系统准备接入,那么座位被接入的系统,将可能需要修改代码以适应新系统的需求。

中间件模式:

优点:

将消息写入消息队列,需要消息的系统自己从消息队列中订阅,从而系统A不需要做任何修改。实现了代码上的解耦,即使新添了系统,也只需要订阅该消息队列上的谋和主题即可实现传统模式的代码嵌合作用。

例如:

支付系统和交易系统在此时就已经进行分离了,支付系统只需要在用户支付成功,往消息队列放入消息,订阅它的交易系统就会消费这个消息,进行一系列的活动。就算以后的系统需要接入支付系统,也只需要订阅该主题即可。

2.异步:

传统模式:

缺点:许多业务逻辑以同步方式运行,太耗费时间。

中间件模式:

优点:将消息写入消息队列,非必要的业务逻辑以异步的方式运行,加快响应速度

例如:邮箱发送和手机短信发送,现在都是采用异步的形式,可以更快响应的用户。但也可能会出现没有发送,所以大部分会允许用户再发一次的操作。

3.削峰:

传统模式:

缺点:并发量大的时候,直接怼导数据库,造成数据库连接异常。

中间件模式:

优点:

系统A慢慢的按照数据库能处理的并发量,从消息队列中慢慢拉取消息。在生产中,这个短暂的高峰期积压是允许的。例如,秒杀活动和限流活动中适合使用,目前关于这方面了解的比较少,还需要再学习中得到具体应用。

消息队列的有事

JMS在官方说法有两大优势:异步,可靠。

异步:如上面所说,JMS天然支持异步。消费者获取消息,不需要主动发送请求,消息会自动退给消费者。此处的消费者指的就是客户端,服务端就是生产者。关于生产者和消费者等词语概念,下面会描述。

可靠:JMS保证消息只会递送一次。大家都遇到过重复创建消息问题,而JMS能帮你避免该问题,只是避免而不是杜绝,所以在一些糟糕的环境下还是有可能出现重复。

使用消息队列的缺点

系统可用性降低:因为多了个消息队列,便多了不确定性。一旦消息队列宕机,将会导致系统北葵,所以系统可用性降低。

系统复杂性增加:添加了消息队列,就需要考虑构成一个高可用的消息队列。需要考虑一致性,考虑消息不被重复消费,如何保证消息可靠传输等等问题,增加业务逻辑等工作量。

JMS消息模型

JMS定义了这两种消息发送模型的规范,他们相互独立。任何JMS的提供者可以实现其中的一种或者两种模型,这是他们自己的选择。JMS规范提供了通用接口保证我们给予JMS API编写的程序适用于任何一种模型。

点对点消息传送模型(P2P)

再改模型,存在三种角色:消息队列,发送者,接收者。发送者发送一个消息给消息队列,该队列保存了所有发送给他的消息(除了被接受着消费掉的和过期的消息)。

特性

每个消息只有一个接收者。

消息发送者和消息接收者并没有时间依赖性。

消息发送者发送消息的时候,无论接收者程序在不在运行,都恒获取到消息

当接收者收到消息的时候,会发送确认收到通知(acknowledgement)ACK包。

点对点消息模型图:

发布、订阅消息传送模型

这有点向redis的发布订阅和Java的观察者模式,存在订阅主题的概念。

在发布/订阅消息模型中,发布者发布一个消息,该消息通过topic传递给所有的客户端。在这种模型中,发布者和订阅者彼此不知道地方,是匿名的且可以动态发布和订阅topic。Topic主要用于保存和消息传递,且会一直保存消息知道消息被传递给客户端。

特性;

一个消息可以传递给多个订阅者。

发布者和订阅者有时间依赖性,只有当客户端创建订阅后才能接受消息,且订阅者需要一致保持活动状态已接受消息。

为了缓和这样严格的事件相关性,JMS允许订阅者创建一个可持久化的订阅。这样,及时订阅者没有被激活(运行),它也能接受到发布者的消息。

发布/订阅消息模型图:

在JMS中,消息的接受可以用以下两种方式

1.同步方式:使用同步方式接收消息的话,消息订阅者调用receive()方法。在receive中,消息未达到或在到达指定时间之前,

(1).目的地是Queue

(2).目的地是Destination


2.异步方式:使用异步方式接收消息的话,消息订阅者需要注册一个消息监听者,类似于事件***,只要消息到达,JMS服务提供者会通过调用***的onMessage()递送消息。

异步接受是采用了***方式

JMS编程接口

JMS应用程序由如下基本模块组成:

管理对象(Administered objects) -连接工厂(Connection Factories)和目的地(Destination)。

连接对象(Connections)。

会话(Sessions)。

消息生产者(Message Producers)。

消息消费者(Message Consumers)。

消息监听者(Message Listeners)。

.Connection Factories

创建Connection对象的工厂,针对两种不同额JMS消息模型,分别又Queue Connection Factory和Topic Connection Factory两种。可以通过JNDI来查找Connection Factory对象。客户端使用一个连接工厂对象连接到JMS服务提供者,他创建了JMS服务提供者和客户端之间的连接。JMS哭护短(可如果发送者或接受着)会在JNDI名字空间中搜索并获取该连接。使用该链接,客户端能公寓目的地铜须,往队列或话题发送/接收消息。

.Destination

目的地指明消息被发送的目的地以及客户端接收消息的来源。JMS使用两种目的地,队列和话题。

.Connection

Connection表示在哭护短和JMS系统之间建立的连接(对TCP/IP socket的包装)。Connection可以生产一个或多个Session。跟Connection Factory一样,Connection也有两种类型:Queue Connection和Topic Connection。

连接对象封装了与JMS提供者之间的虚拟连接,如果我们有一个Connection Factory对象,可以使用它来创建一个连接。

.Session

Session是我么对消息进行操作的接口,可以通过session创建生产者,消费者,消息等。S二十四哦那提供了事务的功能,如果需要使用Session发送/接收多个消息是,可以将这些发送/接收动作放到一个事务中。

.Producer

消息生产者又Session创建,用于往目的地发送消息。生产者实现Message Producer接口,我们可以为目的地,队列或话题创建生产者。

.Consumer

消息消费者又Session创建,用于接受被发送到Destination的消息。

.Message Listener

消息***。如果注册了消息***,一旦消息到达,将自动东调用***的onMessage方法。EJB中的MDB(Message-Drive Bean)就是一种Message Listener。

JMS消息结构

JMS消息分为三部分组成:消息头,消息属性,消息体。

消息头

预定义了若干字段用于客户端也JMS提供者之间识别和发送消息

消息属性

我们可以给消息设置自定义属性,这些主要是提供给应用程序的。对于实现消息过滤功能,消息属性非常有用,JMS API定义了一些标准属性,JMS服务提供者可以选择性额提供部分标准属性。

消息体

在消息体中,JMS API定义了五种模式的消息格式,让我们可以以不同的形式发送和接收消息,并提供了对已有消息格式的兼容。

消息队列的选用

基于RabbitMQ,rocketMQ,KAFKA,ActiveMQ作比较

保证消息队列是高可用的

如何保证消息队列是高可用的呢?重点就在于集群模式,因为集群模式下消息队列不容易谈话,会有其他队列进行替补消费。以Rocket MQ为例:Rocket MQ有多种模式。单机模式,多master模式,多master多slave异步复制模式,多master多slave同步双写模式(rocket league 4-pack的中文翻译,rocket league 4-pack,火箭联赛4包)等等。

且Rocket MQ并不完全遵循JMS规范,有Name Server group,Producer group,Broker集群,consumer group

保证消息消费的幂等性

消息消费的幂等性,也就是消息不重复消费。

造成重复消费的原因其实都是类似的,在于回馈机制。正常状态下,消费者在消费消息的时候,消费完毕后,会发送一个确认信息给消息队列,消息队列就知道该消息被消费了,就会将该消息从消息队列中删除。

不同的消息队列发送的确认信息形式不同,例如Rabbit MQ是发送一个ACK确认消息,Rocket MQ时返回一个CONSUME_SUCCESS成功标志,Kafka实际上哟个offset的概念。

造成重复消费的原因,就是因为网络传输等等故障,确认信息没有传送到消息队列,导致消息队列不知道自己已经消费过该消息了,再次将该消息分发给其他的消费者。

那么我们如何来保证消息消费的幂等性呢,实际上我们只需要保证多条相同的数据过来的时候只处理一条或者说多条处理和处理一条造成的结果相同即可,但是具体怎么做要根据业务需求来定。比如:

.拿到这个消息做数据库的insert操作。给这个消息做一个唯一主键或者唯一约束,那么就算出现重复消费的情况,就会导致主键冲突。

。拿到这个消息做redis的set的操作,redis就是天然的幂等性。

.准备一个第三方戒指,来做消费记录。以redis为例,给消息分配一个全局ID,只要消费过该消息,将<id,message>以K-V形式写入redis。那消费者开始消费前,先去redis中查询有没有消费记录即可。

XA事务的执行流程

XA事务是两阶段提交的一种实现方式,根据2PC规范,XA将一次事务分割成了两个阶段,即Prepare和Commit阶段。

Prepare阶段:TM向所有RM发送prepare指令,RM接收到指令后,执行数据修改和日志记录等操作,然后返回可以提交或者不提交的消息给TM。如果事务协调者TM收到所有参与者都准备好的消息,或通知所有的事务提交,然后进入第二阶段。

Commit阶段:TM接受到所有RM的prepare结果,如果有M返回是不可提交或者超时,那么向所有RM发送Rollback命令;如果所有RM都返回可以提交,那么向所有RM发送Commit命令,完成一次实务操作。

MySQL如何实现XA规范

MySQL中XA事务有两种情况,内部XA和外部XA,其区别是事务发生在MySQL服务器单机上,还是发生在多个外部节点上。

内部XA

再MySQL的InnoDB储存引擎中,开启binlog的情况下,MySQL会同时会晤binlog日治与InnoDB的redo log,为了保证这两个日志的一致性,MySQL使用了XA事务,由于是在MySQL单机上工作,所以被称为内部XA。内部XA事务由binlog作为协调者,在事务提交时,则需要将提交信息写入二进制日志,也就是说,binlog的参与者是MySQL本身。

外部XA

外部XA就是典型的分布式事务,MySQL支持XA START/END/PREPARE/Commit这些SQL语句,通过使用这些命令,可以完成分布式事务。

MySQL外部XA主要应用在数据库代理曾,实现对MySQL数据库的分布式事务支持,例如开源的数据库中间层,比如淘宝的***、阿里巴巴B2B的Codar等。外部XA一般是针对跨多MySQL实例的分布式事务,需要应用层作为协调者,比如我们在写业务代码,在代码中决定提交还是回滚,并且在崩溃时进行恢复。

Binlog中的Xid

当事务提交时,再binlog依赖的内部XA中,额外添加了Xid结构,binlog有多种数据类型,包括以下三种:

1、Statement格式,记录为基本语句,包含Commit。

2、Row格式,记录为基于行。

3、Mixed格式,日志记录使用混合格式。

不论是statement还是row格式,binlog都会添加一个XID_EVENT作为事务的结束,该事件记录了事务的ID也就是Xid,再MySQL进行崩溃恢复时根据binlog中提交的情况来决定如何恢复。

Binlog同步过程

当有事务提交时:

第一步,InnoDB进入Prepare阶段,并且write/sync redo log,将事务的XID写入到redo日志中,binlog不做任何操作;

进行write/sync Binlog,写binlog日治,也会把XID写到Binlog中;

调用InnoDB引擎的Commit完成事务的提交,将Commit信息写入到redo日志中。

如果实在第一步和第二步失败,则整个事务回滚;如果是在第三个事务失败,则MySQL再重启后会检查XID是否已经提交,若没有提交,也就是事务需要重新执行,就会在存储引擎中再执行一次提交操作,保障redo log和binlog数据的一致性,防止数据丢失。

3PC

3PC的出现是为了解决2PC的一些问题,想不一2PC他在参与者中也引入了超时机制,并且新增了一个阶段使得参与者可以李彤这一个阶段同一个字的状态。

让我们来详细看一下。

3PC包含了三个阶段,分别是准备阶段、与提交阶段和提交阶段,对应的英文就是:Can Commit

PreCommit和DoCommit

看起来是吧2PC的提交阶段变成了预提交阶段和提交阶段,但是3PC的准备阶段协调者只是询问参与者的自身状况,比如你现在还好嘛?负载重不重之类的。

而预提交阶段就是和2PC的准备阶段一样,除了事务的提交该做的都做了。

提交阶段和2PC的一样,让我们再看一下图。

不管哪一个阶段有参与者返回失败都会宣布事务失败,这和2PC是一样的(当然到最后的提交阶段和2PC一样,只要是提交请求就只能不断充实)。

我们先来看一下2PC的阶段变更又什么影响。

首先准备阶段的变更成不会直接执行事务,而是会先去询问此时的参与者是否由条件接这个事务,因此不会一来就干活直接锁资源,使得在某些资源不可用的情况下所有参与者都阻塞着。

而预提交阶段的引入起到了一个统一状态的作用,他像一道栅栏,表明再预提交阶段前所有参与者其实还未都回应,在预处理阶段表明所有参与者都已经回应了。

假如你是一位参与者,你知道自己进入了预提交阶段那你就可以推断出来其他参与者也都进入了预提交状态。

但是多引入一个阶段也多一个交互,因此性能会差一些,而且绝大部分的情况下资源应该都是可用的,这样等于每次明知可用执行还得询问一次。

我们再来看下参与者超时能带来什么样的影响。

我们知道2PC是同步阻塞的,上面我们已经分析了协调者挂在了提交请求还未发出去的时候是最伤的,所有参与者都已经锁定资源并且阻塞等待着。

那么引入了超时机制,财渔者就不会傻等了,如果是等待提交命令超时,那么参与者就会提交事务了,因为都到了这一阶段了大概率是提交的,如果是等待预提交命令超时,那该干啥就干啥了,反正本来啥也没干。

当然3PC协调者超时还是在的,具体部分了和2PC是一样的。

从维基百科上看,3PC的引入是为了解决提交阶段2PC协调者和某参与者都挂了之后新选举的协调者不知道当前应该是提交还是回滚的问题。

新版协调者来的时候发现有一个参与者处于预提交或者提交阶段,那么表明已经经过了所有参与者的确认了,所以此时执行的就是提交命令。

所以说3PC就是通过引入预提交阶段来是的参与者之间的状态得到统一,也就是留了一个阶段让大家同步一下。

但是这也只能让协调者知道该如何做,但不能保证这样做一定对,这其实和上面2PC分析一直,因为挂了的参与者到底有没有执行事务无法确定。

所以说3PC通过预提交阶段可以减少故障恢复时候的复杂性,但是不能保证数据一直,除非挂了的哪个参与者回复。

然我们来一起总结一下,3PC相对于2PC做了一定的改进:引入了参与者的超时机制,并且增加了预提交阶段是的故障恢复之后协调者的决策复杂度降低,但整体的交互过程更长了,性能有所下降,并且还是会存在数据不一致的问题。

所以2PC和3PC都不能保证数据100%一直,因此一般都需要有定时扫描补偿机制。

我再说下3PC我没有找到具体的实现,所以我认为3PC只是纯的理论上的东西,而且可以看到相比于2PC它是做了一些努力但是效果甚微,所以只做了解即可。

TCC

2PC和3PC都是数据库层面的,而TCC是业务层面的分布式事务,就像我前面说的分布式事务不仅仅包括数据库的操作,还包括发送短信等,这时候TCC就派上用场了!

TCC指的是Try - Confirm - Cancel。

Try指的是预留,即资源的预留和锁定,注意是预留。

Confirm指的是确认操作,这一步其实就是真正的执行了。

Cancel指的是撤销操作,可以理解为吧预留阶段的动作撤销了。

其实从思想上看和2PC差不多,都是先试探性的执行,如果都可以那就真正的执行,如果不行就回滚。

比如说一个事务要执行A,S,D三个操作,那么先对三个操作执行预留动作。如果预留成功了那么就执行确认操作,如果有一个预留失败了那就都执行撤销动作。

一起来看一下流程,TCC模型还有个事务管理者的角色,用来记录TCC全局事务状态并提交或者回滚事务。

可以看到流程还是很简单的,难点在于业务上的定义,对于每一个操作你都需要定义三个动作分别对应Try-Confirm-Cancel。

因此TCC对业务的侵入较大和业务紧耦合,需要根据特定的场景和业务逻辑来设计相应的操作。

还有一点要注意,撤销和确认操作的执行可能需要充实,因此还需要保证操作的幂等。

相对于2PC、3PC、TCC使用的范围更大,但是开发量也更大,毕竟都在业务上实现,而且有事后你会发现这三个方法还真不好写。不过也因为是在业务上实现的,所以TCC可以跨数据库、跨不同的业务系统来实现事务。

本地消息表

本地消息表其实就是利用了各系统本地的事务来实现分布式事务。

本地消息表顾名思义就是会有一张存放本地消息的表,一般都是放在数据库中,然后执行业务的时候将业务的执行和将消息放入消息表中的操作放在同一个事务中,这样就能保证消息放入本地表中业务肯定是执行成功的。

然后再去调用下一个操作,如果下一个操作调用成功了还好说,消息表的消息状态可以直接改成已成功。

如果调用失败也没事,会有后台任务定时去读取本地消息表,筛选出还未成功的消息在调用对应的服务,服务更新成功了再变更消息的状态。

这时候有可能消息对应的操作不成功,因此也需要重试,重试就得保证对应服务的方法是幂等的,而且一般重试会有最大次数,超过最大次数可以记录下报警让人工处理。

可以看到本地消息表其实实现的是最终一致性,容忍了数据暂时不一致的情况。

消息事务

Rocket MQ就很好的迟滞了消息事务,让我们来看一下如何通过消息实现事务。

第一步献给Broker发送事务消息即半消息,半消息不是说一半消息,而是这个消息对消费者来说不可见,然后发送成功后发送方再执行本地事务。

再根据本地事务的结果向Broker发送Commit或者Roll Back命令。

并且Rocket MQ的发送方会提供一个反查事务状态接口,如果一段时间内半消息没有收到任何操作请求,那么Broker会通过反查接口得知发送方事务是否执行成功,然后执行Commit或者Roll Back命令。

如果是Commit,那么订阅方就能收到这条消息,然后在做对应的操作,做完了之后再消费者条消息即可。

如果是Roll Back那么订阅方收不到这条消息,等于事务就没执行过。

可以看到通过Rocket MQ还是比较容易实现的,Rocket MQ提供了十五消息的功能,我们只需要定义好事务反查接口即可。

可以看到消息十五实现的也是最终一致性

最大努力通知

其实我觉得本地消息表也可以算最大努力,事务消息也可以算最大努力。

就本地消息表来说会有后台任务定时去查看未完成的消息,然后去调用对应的服务,当一个消息多次调用都失败的时候可以记录下然后引入人工,或者直接舍弃。这其实算是最大努力了。

十五消息也一样,当半消息被commit了之后确实就是普通消息了,如果订阅者一直不消费或者消费不了则会一直充实,到最后进入了死信队列。其实这也算最大努力。

所以最大努力通知其实只是表明了一种柔性事务的思想:我已经尽力我最大的努力想达成事务的最终一致了。

适用于对时间不敏感的业务,例如短信通知。

总结

可以看出2PC和3PC是一种强一致性事务,不过还是有数据不一致,阻塞等风险,而且只能用在数据库层面。

而TCC是一种补偿性事务思想,使用的范围更广,在业务层面实现,因此对业务的侵入性较大,没一个操作都需要实现对应的三个方法。

本地消息、十五消息和最大努力通知其实都是最终一致性事务,因此适用于一些对时间不敏感的业务。


分布式事务的特性以及解决分布式事务的方案

事务的四大特性(ACID)

原⼦性(Atomicity):事务作为⼀个整体被执⾏,包含在其中的对数据库的操作要么全部被执⾏,要么都不执⾏。

⼀致性(Consistency):事务应确保数据库的状态从⼀个⼀致状态转变为另⼀个⼀致状态。⼀致状态是指数据库中的数据应满⾜完整性约束。除此之外,⼀致性还有另外⼀层语义,就是事务的中间状态不能被观察到(这层语义也有说应该属于原⼦性)。

隔离性(Isolation):多个事务并发执⾏时,⼀个事务的执⾏不应影响其他事务的执⾏,如同只有这⼀个操作在被数据库所执⾏⼀样。

持久性(Durability):已被提交的事务对数据库的修改应该永久保存在数据库中。在事务结束时,此操作将不可逆转


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:云原生数据库技术有哪些?云原生技术及概念说明
下一篇:微服务分布式事务及微服务分布式事务解决方案
相关文章