众所周知,基本上各大电商平台并且不仅仅只是电商平台,还有教育系统,金融系统,SNS系统都会因为业务的需求而用上分布式。那么意味着什么呢?意味着你要是2020年还不懂分布式,基本上就与大厂大平台无缘了,那么分布式到底是什么呢,让我们来一起看一下
引言
一般像平时我们在单机上写 demo 时,所有逻辑直接在一个数据库中完成,那么本地事务就可以很简单胜任这份工作。但现在毕竟是微服务架构的时代,一个业务逻辑很可能需要多个服务来完成,而且需要依靠不同的服务操作不同的数据库,由于本地事务不能跨库操作,很显然是无法适合这样的场景的。
可以跨库的事务属于分布式事务。所谓分布式事务,便是把分布式系统中两个相关操作看成是一个单元,比如创建订单和修改库存的操作,该单元要么一起成功,要么一起失败,没有别的选择。
什么是事务
不知道你是否遇到过这样的情况,去小卖铺买东西,付了钱,但是店主因为处理了一些其他事,居然忘记你付了钱,又叫你重新付。
又或者在网上购物明明已经扣款,但是却告诉我没有发生交易。这一系列情况都是因为没有事务导致的。这说明了事务在生活中的一些重要性。
有了事务,你去小卖铺买东西,那就是一手交钱一手交货。有了事务,你去网上购物,扣款即产生订单交易。
事务的具体定义
事务提供一种机制将一个活动涉及的所有操作纳入到一个不可分割的执行单元,组成事务的所有操作只有在所有操作均能正常执行的情况下方能提交,只要其中任一操作执行失败,都将导致整个事务的回滚。
简单地说,事务提供一种“要么什么都不做,要么做全套(All or Nothing)”机制。
数据库事务
在介绍分布式事务之前,我们先来讲解一下数据库事务。
事务(Transaction)是并发控制的基本单位。所谓的事务,它是一个操作序列,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位。事务是数据库维护数据一致性的单位,在每个事务结束时,都能保持数据一致性。
事务的四大特征 ACID 为:
原子性(Atomicity):事务必须是原子工作单元;对于其数据修改,要么全都执行,要么全都不执行。不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。(就像你买东西要么交钱收货一起都执行,要么发不出货,就退钱。)
一致性(Consistency):事务的一致性指的是在一个事务执行之前和执行之后数据库都必须处于一致性状态。事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于有效状态。如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态。
隔离性(Isolation):关于事务的隔离性数据库提供了多种隔离级别:一个事务的执行不能干扰其它事务。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。(打个比方,你买东西这个事情,是不影响其他人的。)
持久性(Durability):事务完成之后,它对于数据库中的数据改变是永久性的。该修改即使出现系统故障也将一直保持。即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。(打个比方,你买东西的时候需要记录在账本上,即使老板忘记了那也有据可查。)
分布式事务
什么是分布式事务
分布式事务指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。
简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。
本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
分布式事务产生的原因
从上面本地事务来看,我们可以分为两块:
- Service 产生多个节点
- Resource 产生多个节点
Service 多个节点
随着互联网快速发展,微服务,SOA 等服务架构模式正在被大规模的使用。
举个简单的例子,一个公司之内,用户的资产可能分为好多个部分,比如余额,积分,优惠券等等。
在公司内部有可能积分功能由一个微服务团队维护,优惠券又是另外的团队维护。
这样的话就无法保证积分扣减了之后,优惠券能否扣减成功。
Resource多个节点
同样的,互联网发展得太快了,我们的 MySQL 一般来说装千万级的数据就得进行分库分表。
对于一个支付宝的转账业务来说,你给朋友转钱,有可能你的数据库是在北京,而你的朋友的钱是存在上海,所以我们依然无法保证他们能同时成功。
CAP 定理
我们之前说过数据库的 ACID 四大特性,已经无法满足我们分布式事务,这个时候又有一些新的大佬提出一些新的理论。
CAP 定理是加州大学伯克利分校一位教授提出的,这个理论指出 WEB 服务无法同时满足以下3个属性:
- 一致性(Consistency) : 在分布式系统中数据一旦更新,所有数据变动都是同步的
- 可用性(Availability) : 好的响应性能,每个操作都必须有预期的响应结束
- 分区容错性(Partition tolerance) : 在网络分区的情况下,即使出现单个节点无法可用,系统依然正常对外提供服务
熟悉 CAP 的人都知道,三者不能共有,具体地讲,在分布式系统中,在任何数据库设计中,一个 WEB 应用至多只能同时支持上面的两个属性。显然,任何横向扩展策略都要依赖于数据分区。因此,设计人员必须在一致性与可用性之间做出选择。如果感兴趣可以搜索 CAP 的证明,在分布式系统中,网络无法 100% 可靠,分区其实是一个必然现象。
如果我们选择了 CA 而放弃了 P,那么当发生分区现象时,为了保证一致性,这个时候必须拒绝请求,但是 A 又不允许,所以分布式系统理论上不可能选择 CA 架构,只能选择 CP 或者 AP 架构。
对于 CP 来说,放弃可用性,追求一致性和分区容错性,我们的 ZooKeeper 其实就是追求的强一致。
对于 AP 来说,放弃一致性(这里说的一致性是强一致性),追求分区容错性和可用性,这是很多分布式系统设计时的选择,后面的 BASE 也是根据 AP 来扩展。
顺便一提,CAP 理论中是忽略网络延迟,也就是当事务提交时,从节点 A 复制到节点 B 没有延迟,但是在现实中这个是明显不可能的,所以总会有一定的时间是不一致。
同时 CAP 中选择两个,比如你选择了 CP,并不是叫你放弃 A。因为 P 出现的概率实在是太小了,大部分的时间你仍然需要保证 CA。
就算分区出现了你也要为后来的 A 做准备,比如通过一些日志的手段,是其他机器回复至可用。
BASE 理论
根据 CAP 定理,我们知道在一致性与可用性之间我们只能二选一。但在分布式系统中,我们往往追求的是可用性,它的重要程序比一致性要高,那么如何实现高可用性呢?前人已经给我们提出了另外一个理论,那就是 BASE 理论,它是用来对 CAP 定理进行进一步扩充的。BASE 理论如下:
- 基本可用(Basically Available):分布式系统在出现不可预知的故障的时候,允许损失部分可用性,但这绝不等价于系统不可用
- 软状态(Soft state):和硬状态对应,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统不同节点的数据副本之间进行数据同步的过程存在延时
- 最终一致性(Eventually consistent):系统所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要保证系统数据的强一致性。
BASE 理论是对 CAP 中的一致性和可用性进行一个权衡的结果,也就是说是对CAP中AP的一个扩展。理论的核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。
两阶段提交(2PC)
实现分布式事务有好几种方式,我们首先来介绍两阶段提交。两阶段提交中有两个很重要的角色,分别叫做事务协调者和事务参与者。
- 第一阶段,事务协调者会向所有事务参与者发送 Prepare 请求,在接到 Prepare 请求之后,每一个事务参与者会各自执行与事务有关的数据更新,写入 Undo Log 和 Redo Log。如果参与者执行成功,暂时不提交事务,而是向协调者返回完成消息。当事务协调者接到了所有参与者的返回消息,整个分布式事务将会进入第二阶段。
- 第二阶段,如果事务协调者在之前所收到的都是正确返回,那么它将会向所有事务参与者发出 Commit 请求。接到 Commit 请求之后,事务参与者会各自进行本地的事务提交,并释放锁资源。当本地事务完成提交后,将会向事务协调者返回完成消息。当事务协调者接收到所有事务参与者的完成反馈,整个分布式事务完成。
很完美的方案!确实,一帆风顺的时候确实完美,但如果发生失败,又会怎样呢?
在第一阶段,如果某个事务参与者反馈失败消息,说明该节点的本地事务执行不成功,必须回滚。于是在第二阶段,事务协调者向所有的事务参与者发送 Abort 请求。接收到 Abort 请求之后,各个事务参与者需要在本地进行事务的回滚操作,回滚操作依照 Undo Log 来进行。
总结一下,两阶段提交固然有很多优点,例如尽量保证了数据的强一致(牺牲了一部分可用性来换取的一致性,其实也不能100%保证强一致),但它的缺点也不少:
- 性能问题:协议遵循强一致性。在事务执行过程中,各个节点占用着数据库资源,只有当所有节点准备完毕,事务协调者才会通知提交,参与者提交后释放资源。这样的过程有着非常明显的性能问题。
- 协调者单点故障问题:事务协调者是整个模型的核心,一旦事务协调者节点挂掉,参与者收不到提交或是回滚通知,参与者会一直处于中间状态无法完成事务。
丢失消息导致的不一致问题:在协议的第二个阶段,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收到提交消息,那么就导致了节点之间数据的不一致。
三阶段提交(3PC)
三阶段提交在两阶段提交的基础上增加了 CanCommit 阶段,并且引入了超时机制。一旦事务参与者迟迟没有接到协调者的 commit 请求,会自动进行本地 commit。这样有效解决了协调者单点故障的问题,但是性能问题和不一致的问题仍然没有根本解决。
补偿事务(TCC)
TCC 的核心思想为针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。TCC 一共分为三个阶段:
- Try:对业务系统做检测及资源预留
- Confirm:对业务系统做确认提交,只要 Try 成功,Confirm 一定成功
- Cancel:业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放
TCC 与 2PC 相比,实现以及流程相对简单了一些,但数据的一致性比 2PC 也要差一些。而且 TCC 在 2,3 步中都有可能失败,TCC 属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码。
本地消息表(异步确保)
本地消息表的核心思想是将分布式事务拆分成本地事务进行处理。
其思路如下:
- 消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过 MQ 发送到消息的消费方。如果消息发送失败,会进行重试发送。
- 消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。
生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。
这种方案遵循 BASE 理论,采用的是最终一致性,比较适合实际业务场景,不会出现像 2PC 那样复杂的实现(当调用链很长的时候,2PC 的可用性是非常低的),也不会像 TCC 那样可能出现确认或者回滚不了的情况。
该方式的优点为避免了分布式事务,实现了最终一致性,但它的缺点为消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。
MQ 事务消息
有部分 MQ 支持事务消息,例如 RocketMQ,方式与二阶段提交相类似,但也有部分 MQ 是不支持的,例如 RabbitMQ 和 Kafka。
其思路如下:
- 第一阶段:Prepared 消息,会拿到消息的地址
- 第二阶段:执行本地事务
- 第三阶段:通过第一阶段拿到的地址去访问消息,并修改状态
在业务方法内要向消息队列提交两次请求,一次发送消息和一次确认消息。如果确认消息发送失败了,RocketMQ 会定期扫描消息集群中的事务消息,这时候发现了 Prepared 消息,它会向消息发送者确认,所以生产方需要实现一个 check 接口。
RocketMQ 会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。
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)
总结
分布式事务,本质上是对多个数据库的事务进行统一控制,按照控制力度可以分为:不控制、部分控制和完全控制。不控制就是不引入分布式事务,部分控制就是各种变种的两阶段提交,包括上面提到的 MQ 事务消息、TCC 模式,而完全控制就是完全实现两阶段提交。部分控制的好处是并发量和性能很好,缺点是数据一致性减弱了,完全控制则是牺牲了性能,保障了一致性,具体用哪种方式,最终还是取决于业务场景。
简而言之就是:能不用分布式事务就不用,如果非得使用的话,结合自己的业务分析,看看自己的业务比较适合哪一种,是在乎强一致,还是最终一致即可。