终于有人把“分布式事务”讲的这么简单明了

又或者在网上购物明明已经扣款,可是却告诉我没有发生交易。这一系列状况都是由于没有事务致使的。这说明了事务在生活中的一些重要性。git

有了事务,你去小卖铺买东西,那就是一手交钱一手交货。有了事务,你去网上购物,扣款即产生订单交易。github

事务的具体定义数据库

事务提供一种机制将一个活动涉及的全部操做归入到一个不可分割的执行单元,组成事务的全部操做只有在全部操做均能正常执行的状况下方能提交,只要其中任一操做执行失败,都将致使整个事务的回滚。服务器

简单地说,事务提供一种“要么什么都不作,要么作全套(All or Nothing)”机制。网络

数据库本地事务架构

ACID并发

说到数据库事务就不得不说,数据库事务中的四大特性 ACID:异步

A:原子性(Atomicity),一个事务(transaction)中的全部操做,要么所有完成,要么所有不完成,不会结束在中间某个环节。分布式

事务在执行过程当中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务历来没有执行过同样。微服务

就像你买东西要么交钱收货一块儿都执行,要么发不出货,就退钱。

C:一致性(Consistency),事务的一致性指的是在一个事务执行以前和执行以后数据库都必须处于一致性状态。

若是事务成功地完成,那么系统中全部变化将正确地应用,系统处于有效状态。

若是在事务中出现错误,那么系统中的全部变化将自动地回滚,系统返回到原始状态。

I:隔离性(Isolation),指的是在并发环境中,当不一样的事务同时操纵相同的数据时,每一个事务都有各自的完整数据空间。

由并发事务所作的修改必须与任何其余并发事务所作的修改隔离。事务查看数据更新时,数据所处的状态要么是另外一事务修改它以前的状态,要么是另外一事务修改它以后的状态,事务不会查看到中间状态的数据。

打个比方,你买东西这个事情,是不影响其余人的。

D:持久性(Durability),指的是只要事务成功结束,它对数据库所作的更新就必须永久保存下来。

即便发生系统崩溃,从新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。

打个比方,你买东西的时候须要记录在帐本上,即便老板忘记了那也有据可查。

InnoDB 实现原理

InnoDB 是 MySQL 的一个存储引擎,大部分人对 MySQL 都比较熟悉,这里简单介绍一下数据库事务实现的一些基本原理。

在本地事务中,服务和资源在事务的包裹下能够看作是一体的,以下图:

咱们的本地事务由资源管理器进行管理:

而事务的 ACID 是经过 InnoDB 日志和锁来保证。事务的隔离性是经过数据库锁的机制实现的,持久性经过 Redo Log(重作日志)来实现,原子性和一致性经过 Undo Log 来实现。

Undo Log 的原理很简单,为了知足事务的原子性,在操做任何数据以前,首先将数据备份到一个地方(这个存储数据备份的地方称为 Undo Log)。而后进行数据的修改。

若是出现了错误或者用户执行了 Rollback 语句,系统能够利用 Undo Log 中的备份将数据恢复到事务开始以前的状态。

和 Undo Log 相反,Redo Log 记录的是新数据的备份。在事务提交前,只要将 Redo Log 持久化便可,不须要将数据持久化。

当系统崩溃时,虽然数据没有持久化,可是 Redo Log 已经持久化。系统能够根据 Redo Log 的内容,将全部数据恢复到最新的状态。对具体实现过程有兴趣的同窗能够去自行搜索扩展。

分布式事务

什么是分布式事务

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

简单的说,就是一次大的操做由不一样的小操做组成,这些小的操做分布在不一样的服务器上,且属于不一样的应用,分布式事务须要保证这些小操做要么所有成功,要么所有失败。

本质上来讲,分布式事务就是为了保证不一样数据库的数据一致性。

分布式事务产生的缘由

从上面本地事务来看,咱们能够分为两块:

  • Service 产生多个节点
  • Resource 产生多个节点

Service 多个节点

随着互联网快速发展,微服务,SOA 等服务架构模式正在被大规模的使用。

举个简单的例子,一个公司以内,用户的资产可能分为好多个部分,好比余额,积分,优惠券等等。

在公司内部有可能积分功能由一个微服务团队维护,优惠券又是另外的团队维护。

这样的话就没法保证积分扣减了以后,优惠券可否扣减成功。

Resource多个节点

一样的,互联网发展得太快了,咱们的 MySQL 通常来讲装千万级的数据就得进行分库分表。

对于一个支付宝的转帐业务来讲,你给朋友转钱,有可能你的数据库是在北京,而你的朋友的钱是存在上海,因此咱们依然没法保证他们能同时成功。

分布式事务的基础

从上面来看分布式事务是随着互联网高速发展应运而生的,这是一个必然。

咱们以前说过数据库的 ACID 四大特性,已经没法知足咱们分布式事务,这个时候又有一些新的大佬提出一些新的理论。

CAP

CAP 定理,又被叫做布鲁尔定理。对于设计分布式系统(不只仅是分布式事务)的架构师来讲,CAP 就是你的入门理论。

C (一致性):对某个指定的客户端来讲,读操做能返回最新的写操做。

对于数据分布在不一样节点上的数据来讲,若是在某个节点更新了数据,那么在其余节点若是都能读取到这个最新的数据,那么就称为强一致,若是有某个节点没有读取到,那就是分布式不一致。

A (可用性):非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)。可用性的两个关键一个是合理的时间,一个是合理的响应。

合理的时间指的是请求不能无限被阻塞,应该在合理的时间给出返回。合理的响应指的是系统应该明确返回结果而且结果是正确的,这里的正确指的是好比应该返回 50,而不是返回 40。

P (分区容错性):当出现网络分区后,系统可以继续工做。打个比方,这里集群有多台机器,有台机器网络出现了问题,可是这个集群仍然能够正常工做。

熟悉 CAP 的人都知道,三者不能共有,若是感兴趣能够搜索 CAP 的证实,在分布式系统中,网络没法 100% 可靠,分区实际上是一个必然现象。

若是咱们选择了 CA 而放弃了 P,那么当发生分区现象时,为了保证一致性,这个时候必须拒绝请求,可是 A 又不容许,因此分布式系统理论上不可能选择 CA 架构,只能选择 CP 或者 AP 架构。

对于 CP 来讲,放弃可用性,追求一致性和分区容错性,咱们的 ZooKeeper 其实就是追求的强一致。

对于 AP 来讲,放弃一致性(这里说的一致性是强一致性),追求分区容错性和可用性,这是不少分布式系统设计时的选择,后面的 BASE 也是根据 AP 来扩展。

顺便一提,CAP 理论中是忽略网络延迟,也就是当事务提交时,从节点 A 复制到节点 B 没有延迟,可是在现实中这个是明显不可能的,因此总会有必定的时间是不一致。

同时 CAP 中选择两个,好比你选择了 CP,并非叫你放弃 A。由于 P 出现的几率实在是过小了,大部分的时间你仍然须要保证 CA。

就算分区出现了你也要为后来的 A 作准备,好比经过一些日志的手段,是其余机器回复至可用。

BASE

BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)三个短语的缩写,是对 CAP 中 AP 的一个扩展。

基本可用:分布式系统在出现故障时,容许损失部分可用功能,保证核心功能可用。

软状态:容许系统中存在中间状态,这个状态不影响系统可用性,这里指的是 CAP 中的不一致。

最终一致:最终一致是指通过一段时间后,全部节点数据都将会达到一致。

BASE 解决了 CAP 中理论没有网络延迟,在 BASE 中用软状态和最终一致,保证了延迟后的一致性。

BASE 和 ACID 是相反的,它彻底不一样于 ACID 的强一致性模型,而是经过牺牲强一致性来得到可用性,并容许数据在一段时间内是不一致的,但最终达到一致状态。

分布式事务解决方案

有了上面的理论基础后,这里开始介绍几种常见的分布式事务的解决方案。

是否真的要分布式事务

在说方案以前,首先你必定要明确你是否真的须要分布式事务?

上面说过出现分布式事务的两个缘由,其中有个缘由是由于微服务过多。我见过太多团队一我的维护几个微服务,太多团队过分设计,搞得全部人疲劳不堪。

而微服务过多就会引出分布式事务,这个时候我不会建议你去采用下面任何一种方案,而是请把须要事务的微服务聚合成一个单机服务,使用数据库的本地事务。

由于不论任何一种方案都会增长你系统的复杂度,这样的成本实在是过高了,千万不要由于追求某些设计,而引入没必要要的成本和复杂度。

若是你肯定须要引入分布式事务能够看看下面几种常见的方案。

2PC

说到 2PC 就不得不聊数据库分布式事务中的 XA Transactions。

在 XA 协议中分为两阶段:

  • 事务管理器要求每一个涉及到事务的数据库预提交(precommit)此操做,并反映是否能够提交。
  • 事务协调器要求每一个数据库提交数据,或者回滚数据。

优势:

  • 尽可能保证了数据的强一致,实现成本较低,在各大主流数据库都有本身实现,对于 MySQL 是从 5.5 开始支持。

缺点:

  • 单点问题:事务管理器在整个流程中扮演的角色很关键,若是其宕机,好比在第一阶段已经完成,在第二阶段正准备提交的时候事务管理器宕机,资源管理器就会一直阻塞,致使数据库没法使用。
  • 同步阻塞:在准备就绪以后,资源管理器中的资源一直处于阻塞,直到提交完成,释放资源。
  • 数据不一致:两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能。

好比在第二阶段中,假设协调者发出了事务 Commit 的通知,可是由于网络问题该通知仅被一部分参与者所收到并执行了 Commit 操做,其他的参与者则由于没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。

总的来讲,XA 协议比较简单,成本较低,可是其单点问题,以及不能支持高并发(因为同步阻塞)依然是其最大的弱点。

TCC

关于 TCC(Try-Confirm-Cancel)的概念,最先是由 Pat Helland 于 2007 年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。

TCC 事务机制相比于上面介绍的 XA,解决了以下几个缺点:

  • 解决了协调者单点,由主业务方发起并完成这个业务活动。业务活动管理器也变成多点,引入集群。
  • 同步阻塞:引入超时,超时后进行补偿,而且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小。
  • 数据一致性,有了补偿机制以后,由业务活动管理器控制一致性。

对于 TCC 的解释:

  • Try 阶段:尝试执行,完成全部业务检查(一致性),预留必需业务资源(准隔离性)。
  • Confirm 阶段:确认真正执行业务,不做任何业务检查,只使用 Try 阶段预留的业务资源,Confirm 操做知足幂等性。要求具有幂等设计,Confirm 失败后须要进行重试。
  • Cancel 阶段:取消执行,释放 Try 阶段预留的业务资源,Cancel 操做知足幂等性。Cancel 阶段的异常和 Confirm 阶段异常处理方案基本上一致。

举个简单的例子:若是你用 100 元买了一瓶水, Try 阶段:你须要向你的钱包检查是否够 100 元并锁住这 100 元,水也是同样的。

若是有一个失败,则进行 Cancel(释放这 100 元和这一瓶水),若是 Cancel 失败不论什么失败都进行重试 Cancel,因此须要保持幂等。

若是都成功,则进行 Confirm,确认这 100 元被扣,和这一瓶水被卖,若是 Confirm 失败不管什么失败则重试(会依靠活动日志进行重试)。

对于 TCC 来讲适合一些:

  • 强隔离性,严格一致性要求的活动业务。
  • 执行时间较短的业务。

实现参考:https://github.com/liuyangming/ByteTCC/。

本地消息表

本地消息表这个方案最初是 eBay 提出的,eBay 的完整方案 https://queue.acm.org/detail.cfm?id=1394128。

此方案的核心是将须要分布式处理的任务经过消息日志的方式来异步执行。消息日志能够存储到本地文本、数据库或消息队列,再经过业务规则自动或人工发起重试。

人工重试更多的是应用于支付场景,经过对帐系统对过后问题的处理。

对于本地消息队列来讲核心是把大事务转变为小事务。仍是举上面用 100 元去买一瓶水的例子。

1. 当你扣钱的时候,你须要在你扣钱的服务器上新增长一个本地消息表,你须要把你扣钱和减去水的库存写入到本地消息表,放入同一个事务(依靠数据库本地事务保证一致性)。

2. 这个时候有个定时任务去轮询这个本地事务表,把没有发送的消息,扔给商品库存服务器,叫它减去水的库存,到达商品服务器以后,这时得先写入这个服务器的事务表,而后进行扣减,扣减成功后,更新事务表中的状态。

3. 商品服务器经过定时任务扫描消息表或者直接通知扣钱服务器,扣钱服务器在本地消息表进行状态更新。

4. 针对一些异常状况,定时扫描未成功处理的消息,进行从新发送,在商品服务器接到消息以后,首先判断是不是重复的。

若是已经接收,再判断是否执行,若是执行在立刻又进行通知事务;若是未执行,须要从新执行由业务保证幂等,也就是不会多扣一瓶水。

本地消息队列是 BASE 理论,是最终一致模型,适用于对一致性要求不高的状况。实现这个模型时须要注意重试的幂等。

MQ 事务

在 RocketMQ 中实现了分布式事务,其实是对本地消息表的一个封装,将本地消息表移动到了 MQ 内部。

下面简单介绍一下MQ事务,若是想对其详细了解能够参考:https://www.jianshu.com/p/453c6e7ff81c。

基本流程以下:

  • 第一阶段 Prepared 消息,会拿到消息的地址。
  • 第二阶段执行本地事务。
  • 第三阶段经过第一阶段拿到的地址去访问消息,并修改状态。消息接受者就能使用这个消息。

若是确认消息失败,在 RocketMQ Broker 中提供了定时扫描没有更新状态的消息。

若是有消息没有获得确认,会向消息发送者发送消息,来判断是否提交,在 RocketMQ 中是以 Listener 的形式给发送者,用来处理。

若是消费超时,则须要一直重试,消息接收端须要保证幂等。若是消息消费失败,这时就须要人工进行处理,由于这个几率较低,若是为了这种小几率时间而设计这个复杂的流程反而得不偿失。

Saga 事务

Saga 是 30 年前一篇数据库伦理提到的一个概念。其核心思想是将长事务拆分为多个本地短事务,由 Saga 事务协调器协调,若是正常结束那就正常完成,若是某个步骤失败,则根据相反顺序一次调用补偿操做。

Saga 的组成:每一个 Saga 由一系列 sub-transaction Ti 组成,每一个 Ti 都有对应的补偿动做 Ci,补偿动做用于撤销 Ti 形成的结果。这里的每一个 T,都是一个本地事务。

能够看到,和 TCC 相比,Saga 没有“预留 try”动做,它的 Ti 就是直接提交到库。

Saga 的执行顺序有两种:

  • T1,T2,T3,...,Tn。
  • T1,T2,...,Tj,Cj,...,C2,C1,其中 0 < j < n 。

Saga 定义了两种恢复策略:

  • 向后恢复,即上面提到的第二种执行顺序,其中 j 是发生错误的 sub-transaction,这种作法的效果是撤销掉以前全部成功的 sub-transation,使得整个 Saga 的执行结果撤销。
  • 向前恢复,适用于必需要成功的场景,执行顺序是相似于这样的:T1,T2,...,Tj(失败),Tj(重试),...,Tn,其中 j 是发生错误的 sub-transaction。该状况下不须要 Ci。

这里要注意的是,在 Saga 模式中不能保证隔离性,由于没有锁住资源,其余事务依然能够覆盖或者影响当前事务。

仍是拿 100 元买一瓶水的例子来讲,这里定义:

  • T1 = 扣 100 元,T2 = 给用户加一瓶水,T3 = 减库存一瓶水。
  • C1 = 加100元,C2 = 给用户减一瓶水,C3 = 给库存加一瓶水。

咱们一次进行 T1,T2,T3 若是发生问题,就执行发生问题的 C 操做的反向。

上面说到的隔离性的问题会出如今,若是执行到 T3 这个时候须要执行回滚,可是这个用户已经把水喝了(另一个事务),回滚的时候就会发现,没法给用户减一瓶水了。

这就是事务之间没有隔离性的问题。能够看见 Saga 模式没有隔离性的影响仍是较大,能够参照华为的解决方案:从业务层面入手加入一 Session 以及锁的机制来保证可以串行化操做资源。

也能够在业务层面经过预先冻结资金的方式隔离这部分资源, 最后在业务操做的过程当中能够经过及时读取当前状态的方式获取到最新的更新。(具体实例:能够参考华为的 Service Comb)

最后

仍是那句话,能不用分布式事务就不用,若是非得使用的话,结合本身的业务分析,看看本身的业务比较适合哪种,是在意强一致,仍是最终一致便可。

最后在总结一些问题,你们能够下来本身从文章找寻答案:

  • ACID 和 CAP 的 CA 是同样的吗?
  • 分布式事务经常使用的解决方案的优缺点是什么?适用于什么场景?
  • 分布式事务出现的缘由?用来解决什么痛点?

原文地址:http://developer.51cto.com/art/201808/581174.htm

相关文章
相关标签/搜索