女友问敖丙:什么是分布式事务?

你知道的越多,你不知道的越多数据库

前言

上一篇文章已经讲完分布式了,那暖男说要讲分布式事务那就必定会讲,只是我估计你们没料到暖男这么快就肝好了吧?编程

事务想必你们并不陌生,至于什么是 ACID,也是老生常谈了。不过暖男为了保证文章的完整性确保全部人都听得懂,我仍是得先说说 ACID,而后再来介绍下什么是分布式事务和常见的分布式事务包括 2PC、3PC、TCC、本地消息表、消息事务、最大努力通知。网络

事务

严格意义上的事务实现应该是具有原子性、一致性、隔离性和持久性,简称 ACID。并发

  • 原子性(Atomicity),能够理解为一个事务内的全部操做要么都执行,要么都不执行。
  • 一致性(Consistency),能够理解为数据是知足完整性约束的,也就是不会存在中间状态的数据,好比你帐上有400,我帐上有100,你给我打200块,此时你帐上的钱应该是200,我帐上的钱应该是300,不会存在我帐上钱加了,你帐上钱没扣的中间状态。
  • 隔离性(Isolation),指的是多个事务并发执行的时候不会互相干扰,即一个事务内部的数据对于其余事务来讲是隔离的。
  • 持久性(Durability),指的是一个事务完成了以后数据就被永远保存下来,以后的其余操做或故障都不会对事务的结果产生影响。

而通俗意义上事务就是为了使得一些更新操做要么都成功,要么都失败。分布式

说到这里可能有人会说,不对啊 Redis 的事务不能保证全部操做要么都执行要么都不执行,为何它也叫事务啊?ide

首先你要知晓通常的中间件都会夸大其效果,人家团队也是想更出名,吸引更多的人来使用他们的产品,因此咱们得以辩证的角度来看待。性能

通常而言他们既然敢说出他们实现了什么什么,要么是真的实现了,要么是在某种特殊、定制或者极端的条件下才能知足功能。线程

咱们来看看 Redis 怎么说的。
女友问敖丙:什么是分布式事务?
这句话就是告诉你们事务中的某个命令失败了,以后的命令仍是会被处理,Redis 不会中止命令,意味着也不会回滚。设计

你说这不是扯嘛?这都偏离事务最核心的本意了啊。日志

别急,我们来看看 Redis 怎么解释的。
女友问敖丙:什么是分布式事务?
Redis 官网解释了为何不支持回滚,他们说首先若是命令出错那都是语法使用错误,是大家本身编程出错,并且这种状况应该在开发的时候就被检测出来,不该在生产环境中出现。

而后 Redis 就是为了快!不须要提供回滚。

下面还有一段话我就不截图了,就是说就算提供回滚也没用,你这代码都写错了,回滚并不能使你免于编程错误。并且通常这种错也不可能进入到生产环境,因此选择更加简单、快速的方法,咱们不支持回滚。

你看看这说的好像颇有道理,咱们不提供回滚,由于咱们不须要为你的编程错误买单!

但好像哪里不对劲?角度、立场不一样,你们本身品。

就下来就开始分布式事务。

分布式事务

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

对于分布式事务而言几乎知足不了 ACID,其实对于单机事务而言大部分状况下也没有知足 ACID,否则怎么会有四种隔离级别呢?因此更别说分布在不一样数据库或者不一样应用上的分布式事务了。
咱们先来看下 2PC。

2PC

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

注意这只是协议或者说是理论指导,只阐述了大方向,具体落地仍是有会有差别的。

让咱们来看下两个阶段的具体流程。

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

同步等待全部资源的响应以后就进入第二阶段即提交阶段(注意提交阶段不必定是提交事务,也多是回滚事务)。

假如在第一阶段全部参与者都返回准备成功,那么协调者则向全部参与者发送提交事务命令,而后等待全部事务都提交成功以后,返回事务执行成功。

让咱们来看一下流程图。
女友问敖丙:什么是分布式事务?
假如在第一阶段有一个参与者返回失败,那么协调者就会向全部参与者发送回滚事务的请求,即分布式事务执行失败。
女友问敖丙:什么是分布式事务?
那可能就有人问了,那第二阶段提交失败的话呢?

这里有两种状况。

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

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

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

首先 2PC 是一个同步阻塞协议,像第一阶段协调者会等待全部参与者响应才会进行下一步操做,固然第一阶段的协调者有超时机制,假设由于网络缘由没有收到某参与者的响应或某参与者挂了,那么超时后就会判断事务失败,向全部参与者发送回滚命令。

在第二阶段协调者的无法超时,由于按照咱们上面分析只能不断重试!

协调者故障分析

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

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

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

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

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

假设协调者在发送提交事务命令以前挂了,这个不行,傻了!这下是全部资源都阻塞着。

假设协调者在发送提交事务命令以后挂了,这个还行,也是至少命令发出去了,很大几率都会提交成功,而后释放资源,可是若是出现网络分区问题某些参与者将由于收不到命令而阻塞着。
女友问敖丙:什么是分布式事务?

协调者故障,经过选举获得新协调者

由于协调者单点问题,所以咱们能够经过选举等操做选出一个新协调者来顶替。

若是处于第一阶段,其实影响不大都回滚好了,在第一阶段事务确定还没提交。

若是处于第二阶段,假设参与者都没挂,此时新协调者能够向全部参与者确认它们自身状况来推断下一步的操做。

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

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

问题其实就出在每一个参与者自身的状态只有本身和协调者知道,所以新协调者没法经过在场的参与者的状态推断出挂了的参与者是什么状况。

虽然协议上没说,不过在实现的时候咱们能够灵活的让协调者将本身发过的请求在哪一个地方记一下,也就是日志记录,这样新协调者来的时候不就知道此时该不应发了?
女友问敖丙:什么是分布式事务?
可是就算协调者知道本身该发提交请求,那么在参与者也一块儿挂了的状况下没用,由于你不知道参与者在挂以前有没有提交事务。
若是参与者在挂以前事务提交成功,新协调者肯定存活着的参与者都没问题,那确定得向其余参与者发送提交事务命令才能保证数据一致。

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

因此说极端状况下仍是没法避免数据不一致问题。
talk is cheap 让咱们再来看下代码,可能更加的清晰。如下代码取自 <>。

这个代码就是实现了 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 也有变种,例如 Tree 2PC、Dynamic 2PC。

还有一点不知道大家看出来没,2PC 适用于数据库层面的分布式事务场景,而咱们业务需求有时候不只仅关乎数据库,也有多是上传一张图片或者发送一条短信。

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

简单说下 Java 中 JTA,它是基于XA规范实现的事务接口,这里的 XA 你能够简单理解为基于数据库的 XA 规范来实现的 2PC。(至于XA规范究竟是啥,篇幅有限,下次有机会再说)
接下来咱们再来看看 3PC。

3PC

3PC 的出现是为了解决 2PC 的一些问题,相比于 2PC 它在参与者中也引入了超时机制,而且新增了一个阶段使得参与者能够利用这

一个阶段统一各自的状态。

让咱们来详细看一下。

3PC 包含了三个阶段,分别是准备阶段、预提交阶段和提交阶段,对应的英文就是:CanCommit、PreCommit 和 DoCommit。

看起来是把 2PC 的提交阶段变成了预提交阶段和提交阶段,可是 3PC 的准备阶段协调者只是询问参与者的自身情况,好比你如今还好吗?负载重不重?这类的。

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

提交阶段和 2PC 的同样,让咱们来看一下图。
女友问敖丙:什么是分布式事务?
无论哪个阶段有参与者返回失败都会宣布事务失败,这和 2PC 是同样的(固然到最后的提交阶段和 2PC 同样只要是提交请求就只能不断重试)。

咱们先来看一下 3PC 的阶段变动有什么影响。

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

而预提交阶段的引入起到了一个统一状态的做用,它像一道栅栏,代表在预提交阶段前全部参与者其实还未都回应,在预处理阶段代表全部参与者都已经回应了。

假如你是一位参与者,你知道本身进入了预提交状态那你就能够推断出来其余参与者也都进入了预提交状态。

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

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

咱们知道 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、B、C三个操做,那么先对三个操做执行预留动做。若是都预留成功了那么就执行确认操做,若是有一个预留失败那就都执行撤销动做。

咱们来看下流程,TCC模型还有个事务管理者的角色,用来记录TCC全局事务状态并提交或者回滚事务。
女友问敖丙:什么是分布式事务?
能够看到流程仍是很简单的,难点在于业务上的定义,对于每个操做你都须要定义三个动做分别对应Try - Confirm - Cancel。

所以 TCC 对业务的侵入较大和业务紧耦合,须要根据特定的场景和业务逻辑来设计相应的操做。

还有一点要注意,撤销和确认操做的执行可能须要重试,所以还须要保证操做的幂等。

相对于 2PC、3PC ,TCC 适用的范围更大,可是开发量也更大,毕竟都在业务上实现,并且有时候你会发现这三个方法还真很差写。不过也由于是在业务上实现的,因此TCC能够跨数据库、跨不一样的业务系统来实现事务。

本地消息表

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

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

而后再去调用下一个操做,若是下一个操做调用成功了好说,消息表的消息状态能够直接改为已成功。

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

这时候有可能消息对应的操做不成功,所以也须要重试,重试就得保证对应服务的方法是幂等的,并且通常重试会有最大次数,超过最大次数能够记录下报警让人工处理。

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

消息事务

RocketMQ 就很好的支持了消息事务,让咱们来看一下如何经过消息实现事务。

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

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

而且 RocketMQ 的发送方会提供一个反查事务状态接口,若是一段时间内半消息没有收到任何操做请求,那么 Broker 会经过反查接口得知发送方事务是否执行成功,而后执行 Commit 或者 RollBack 命令。

若是是 Commit 那么订阅方就能收到这条消息,而后再作对应的操做,作完了以后再消费这条消息便可。
若是是 RollBack 那么订阅方收不到这条消息,等于事务就没执行过。

能够看到经过 RocketMQ 仍是比较容易实现的,RocketMQ 提供了事务消息的功能,咱们只须要定义好事务反查接口便可。
女友问敖丙:什么是分布式事务?
能够看到消息事务实现的也是最终一致性。

最大努力通知

其实我以为本地消息表也能够算最大努力,事务消息也能够算最大努力。

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

事务消息也是同样,当半消息被commit了以后确实就是普通消息了,若是订阅者一直不消费或者消费不了则会一直重试,到最后进入死信队列。其实这也算最大努力。

因此最大努力通知其实只是代表了一种柔性事务的思想:我已经尽力我最大的努力想达成事务的最终一致了。
适用于对时间不敏感的业务,例如短信通知。

总结

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

而 TCC 是一种补偿性事务思想,适用的范围更广,在业务层面实现,所以对业务的侵入性较大,每个操做都须要实现对应的三个方法。

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

原本码字的预算就在4000字左右的,一不当心就多写了2000多字,主要是分布式事务的常看法决方案太多了,都要介绍一下确实须要这么多的篇幅,文章内容很长,但愿你们都好好的看一下,这我review了好几回以为仍是值得反复斟酌的一篇文章,我这个该死的魅力啊。

我是敖丙,你知道的越多,你不知道的越多,咱们下期见!