学习分布式事务(一)

分布式事务是企业集成中的一个技术难点,也是每个分布式系统架构中都会涉及到的一个东西,特别是在这几年愈来愈火的微服务架构中,几乎能够说是没法避免,本文就围绕分布式事务各方面与你们进行介绍。数据库

一. 事务网络

1.1 什么是事务架构

数据库事务(简称:事务,Transaction)是指数据库执行过程当中的一个逻辑单位,由一个有限的数据库操做序列构成。并发

事务拥有如下四个特性,习惯上被称为ACID特性:app

原子性(Atomicity):事务做为一个总体被执行,包含在其中的对数据库的操做要么所有被执行,要么都不执行。框架

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

隔离性(Isolation):多个事务并发执行时,一个事务的执行不该影响其余事务的执行,如同只有这一个操做在被数据库所执行同样。分布式

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

1.2 本地事务微服务

起初,事务仅限于对单一数据库资源的访问控制:

架构服务化之后,事务的概念延伸到了服务中。假若将一个单一的服务操做做为一个事务,那么整个服务操做只能涉及一个单一的数据库资源:

 

这类基于单个服务单一数据库资源访问的事务,被称为本地事务(Local Transaction)。

二. 分布式事务应用架构

本地事务主要限制在单个会话内,不涉及多个数据库资源。可是在基于SOA(Service-Oriented Architecture,面向服务架构)的分布式应用环境下,愈来愈多的应用要求对多个数据库资源,多个服务的访问都能归入到同一个事务当中,分布式事务应运而生。

最先的分布式事务应用架构很简单,不涉及服务间的访问调用,仅仅是服务内操做涉及到对多个数据库资源的访问。

 

当一个服务操做访问不一样的数据库资源,又但愿对它们的访问具备事务特性时,就须要采用分布式事务来协调全部的事务参与者。

对于上面介绍的分布式事务应用架构,尽管一个服务操做会访问多个数据库资源,可是毕竟整个事务仍是控制在单一服务的内部。若是一个服务操做须要调用另一个服务,这时的事务就须要跨越多个服务了。在这种状况下,起始于某个服务的事务在调用另一个服务的时候,须要以某种机制流转到另一个服务,从而使被调用的服务访问的资源也自动加入到该事务当中来。下图反映了这样一个跨越多个服务的分布式事务:

若是将上面这两种场景(一个服务能够调用多个数据库资源,也能够调用其余服务)结合在一块儿,对此进行延伸,整个分布式事务的参与者将会组成以下图所示的树形拓扑结构。在一个跨服务的分布式事务中,事务的发起者和提交均系同一个,它能够是整个调用的客户端,也能够是客户端最早调用的那个服务。

 

较之基于单一数据库资源访问的本地事务,分布式事务的应用架构更为复杂。在不一样的分布式应用架构下,实现一个分布式事务要考虑的问题并不彻底同样,好比对多资源的协调、事务的跨服务传播等,实现机制也是复杂多变。尽管有这么多工程细节须要考虑,但分布式事务最核心的仍是其 ACID 特性。所以,想要了解一个分布式事务,就先从了解它是怎么实现事务 ACID 特性开始。

下文将从两个最多见的分布式事务模型入手,着重分析分布式事务的基础共通点,即如何保证分布式事务的 ACID 特性。

三. 常见分布式事务模型 ACID 实现分析

3.1 X/Open XA 协议

最先的分布式事务模型是 X/Open 国际联盟提出的 X/Open Distributed Transaction Processing(DTP)模型,也就是你们常说的 X/Open XA 协议,简称XA 协议。

 

DTP 模型中包含一个全局事务管理器(TM,Transaction Manager)和多个资源管理器(RM,Resource Manager)。全局事务管理器负责管理全局事务状态与参与的资源,协同资源一块儿提交或回滚;资源管理器则负责具体的资源操做。

XA 协议描述了 TM 与 RM 之间的接口,容许多个资源在同一分布式事务中访问。

基于 DTP 模型的分布式事务流程大体以下:

 

1.应用程序(AP,Application)向 TM 申请开始一个全局事务。

2.针对要操做的 RM,AP 会先向 TM 注册(TM 负责记录 AP 操做过哪些 RM,即分支事务),TM 经过 XA 接口函数通知相应 RM 开启分布式事务的子事务,接着 AP 就能够对该 RM 管理的资源进行操做。

3.当 AP 对全部 RM 操做完毕后,AP 根据执行状况通知 TM 提交或回滚该全局事务,TM 经过 XA 接口函数通知各 RM 完成操做。TM 会先要求各个 RM 作预提交,全部 RM 返回成功后,再要求各 RM 作正式提交,XA 协议要求,一旦 RM 预提交成功,则后续的正式提交也必须能成功;若是任意一个 RM 预提交失败,则 TM 通知各 RM 回滚。

4.全部 RM 提交或回滚完成后,全局事务结束。

3.1.1 原子性

XA 协议使用 2PC(Two Phase Commit,两阶段提交)原子提交协议来保证分布式事务原子性。

两阶段提交是指将提交过程分为两个阶段,即准备阶段(投票阶段)和提交阶段(执行阶段):

准备阶段:

TM 向每一个 RM 发送准备消息。若是 RM 的本地事务操做执行成功,则返回成功;若是 RM 的本地事务操做执行失败,则返回失败。

提交阶段

若是 TM 收到了全部 RM 回复的成功消息,则向每一个 RM 发送提交消息;不然发送回滚消息;RM 根据 TM 的指令执行提交或者回滚本地事务操做,释放全部事务处理过程当中使用的锁资源。

3.1.2 隔离性

XA 协议中没有描述如何实现分布式事务的隔离性,可是 XA 协议要求DTP 模型中的每一个 RM 都要实现本地事务,也就是说,基于 XA 协议实现的分布式事务的隔离性是由每一个 RM 本地事务的隔离性来保证的,当一个分布式事务的全部子事务都是隔离的,那么这个分布式事务自然的就实现了隔离性。

以 MySQL 来举例,MySQL 使用 2PL(Two-Phase Locking,两阶段锁)机制来控制本地事务的并发,保证隔离性。2PL 与 2PC 相似,也是将锁操做分为加锁和解锁两个阶段,而且保证两个阶段彻底不相交。加锁阶段,只加锁,不放锁。解锁阶段,只放锁,不加锁。

 

如上图所示,在一个本地事务中,每执行一条更新操做以前,都会先获取对应的锁资源,只有获取锁资源成功才会执行该操做,而且一旦获取了锁资源就会持有该锁资源直到本事务执行结束。

MySQL 经过这种 2PL 机制,能够保证在本地事务执行过程当中,其余并发事务不能操做相同资源,从而实现了事务隔离。

3.1.3 一致性

前面提到一致性有两层语义,一层是确保事务执行结束后,数据库从一个一致状态转变为另外一个一致状态。另外一层语义是事务执行过程当中的中间状态不能被观察到。

前一层语义的实现很简单,经过原子性、隔离性以及 RM 自身一致性的实现就能够保证。至于后一层语义,咱们先来看看单个 RM 上的本地事务是怎么实现的。仍是以 MySQL 举例,MySQL 经过 MVCC(Multi Version Concurrency Control,多版本并发控制)机制,为每一个一致性状态生成快照(Snapshot),每一个事务看到的都是各Snapshot对应的一致性状态,从而也就保证了本地事务的中间状态不会被观察到。

虽然单个 RM 上实现了Snapshot,可是在分布式应用架构下,会遇到什么问题呢?

 

如上图所示,在 RM1 的本地子事务提交完毕到 RM2 的本地子事务提交完毕之间,只能读到 RM1 上子事务执行的内容,读不到 RM2 上的子事务。也就是说,虽然在单个 RM 上的本地事务是一致的,可是从全局来看,一个全局事务执行过程的中间状态被观察到了,全局一致性就被破坏了。

XA 协议并无定义怎么实现全局的 Snapshot,像 MySQL 官方文档里就建议使用串行化的隔离级别来保证分布式事务一致性: “As with nondistributed transactions, SERIALIZABLE may be preferred if your applications are sensitive to read phenomena. REPEATABLE READ may not be sufficient for distributed transactions.”(对于分布式事务来讲,可重复读隔离级别不足以保证事务一致性,若是你的程序有全局一致性读要求,能够考虑串行化隔离级别.)

固然,因为串行化隔离级别的性能较差,因此不少分布式数据库都本身实现了分布式 MVCC 机制来提供全局的一致性读。一个基本思路是用一个集中式或者逻辑上单调递增的东西来控制生成全局 Snapshot,每一个事务或者每条 SQL 执行时都去获取一次,从而实现不一样隔离级别下的一致性。好比 Google 的 Spanner 就是用 TrueTime 来控制访问全局 Snapshot。

3.1.4 小结

XA 协议一般实如今数据库资源层,直接做用于资源管理器上。所以,基于 XA 协议实现的分布式事务产品,不管是分布式数据库,仍是分布式事务框架,对业务几乎都没有侵入,就像使用普通数据库同样。

XA 协议严格保障事务 ACID 特性,可以知足全部业务领域的功能需求,可是,这一样是一把双刃剑。

因为隔离性的互斥要求,在事务执行过程当中,全部的资源都被锁定,只适用于执行时间肯定的短事务。同时,整个事务期间都是独占数据,对于热点数据的并发性能可能会很低,实现了分布式 MVCC 或乐观锁(optimistic locking)之后,性能可能会有所提高。

同时,为了保障一致性,要求全部 RM 同等可信、可靠,要求故障恢复机制可靠、快速,在网络故障隔离的状况下,服务基本不可用。

3.2 TCC 模型

TCC(Try-Confirm-Cancel)分布式事务模型相对于 XA 等传统模型,其特征在于它不依赖资源管理器(RM)对分布式事务的支持,而是经过对业务逻辑的分解来实现分布式事务。

TCC 模型认为对于业务系统中一个特定的业务逻辑,其对外提供服务时,必须接受一些不肯定性,即对业务逻辑初步操做的调用仅是一个临时性操做,调用它的主业务服务保留了后续的取消权。若是主业务服务认为全局事务应该回滚,它会要求取消以前的临时性操做,这就对应从业务服务的取消操做。而当主业务服务认为全局事务应该提交时,它会放弃以前临时性操做的取消权,这对应从业务服务的确认操做。每个初步操做,最终都会被确认或取消。

所以,针对一个具体的业务服务,TCC 分布式事务模型须要业务系统提供三段业务逻辑:

初步操做 Try:完成全部业务检查,预留必须的业务资源。

确认操做 Confirm:真正执行的业务逻辑,不做任何业务检查,只使用 Try 阶段预留的业务资源。所以,只要 Try 操做成功,Confirm 必须能成功。另外,Confirm 操做需知足幂等性,保证一笔分布式事务有且只能成功一次。

取消操做 Cancel:释放 Try 阶段预留的业务资源。一样的,Cancel 操做也须要知足幂等性。

 

 

TCC 分布式事务模型包括三部分:

主业务服务:主业务服务为整个业务活动的发起方,服务的编排者,负责发起并完成整个业务活动。

从业务服务:从业务服务是整个业务活动的参与方,负责提供 TCC 业务操做,实现初步操做(Try)、确认操做(Confirm)、取消操做(Cancel)三个接口,供主业务服务调用。

业务活动管理器:业务活动管理器管理控制整个业务活动,包括记录维护 TCC 全局事务的事务状态和每一个从业务服务的子事务状态,并在业务活动提交时调用全部从业务服务的 Confirm 操做,在业务活动取消时调用全部从业务服务的 Cancel 操做。

一个完整的 TCC 分布式事务流程以下:

主业务服务首先开启本地事务;

主业务服务向业务活动管理器申请启动分布式事务主业务活动;

而后针对要调用的从业务服务,主业务活动先向业务活动管理器注册从业务活动,而后调用从业务服务的 Try 接口;

当全部从业务服务的 Try 接口调用成功,主业务服务提交本地事务;若调用失败,主业务服务回滚本地事务;

若主业务服务提交本地事务,则 TCC 模型分别调用全部从业务服务的 Confirm 接口;若主业务服务回滚本地事务,则分别调用 Cancel 接口;

全部从业务服务的 Confirm 或 Cancel 操做完成后,全局事务结束。

3.2.1 原子性

TCC 模型也使用 2PC 原子提交协议来保证事务原子性。Try 操做对应2PC 的一阶段准备(Prepare);Confirm 对应 2PC 的二阶段提交(Commit),Cancel 对应 2PC 的二阶段回滚(Rollback),能够说 TCC 就是应用层的 2PC。

3.2.2 隔离性

TCC 分布式事务模型仅提供两阶段原子提交协议,保证分布式事务原子性。事务的隔离交给业务逻辑来实现。

隔离的本质是控制并发,防止并发事务操做相同资源而引发的结果错乱。

举个例子,好比金融行业里管理用户资金,当用户发起交易时,通常会先检查用户资金,若是资金充足,则扣除相应交易金额,增长卖家资金,完成交易。若是没有事务隔离,用户同时发起两笔交易,两笔交易的检查都认为资金充足,实际上却只够支付一笔交易,结果两笔交易都支付成功,致使资损。

能够发现,并发控制是业务逻辑执行正确的保证,可是像两阶段锁这样的并发访问控制技术要求一直持有数据库资源锁直到整个事务执行结束,特别是在分布式事务架构下,要求持有锁到分布式事务第二阶段执行结束,也就是说,分布式事务会加长资源锁的持有时间,致使并发性能进一步降低。

所以,TCC 模型的隔离性思想就是经过业务的改造,在第一阶段结束以后,从底层数据库资源层面的加锁过渡为上层业务层面的加锁,从而释放底层数据库锁资源,放宽分布式事务锁协议,提升业务并发性能。

仍是以上面的例子举例:

第一阶段:检查用户资金,若是资金充足,冻结用户本次交易资金,这笔资金被业务隔离,不容许除本事务以外的其它并发事务动用。

第二阶段:扣除第一阶段预冻结的用户资金,增长卖家资金,完成交易。 采用业务加锁的方式,隔离用户冻结资金,在第一阶段结束后直接释放底层资源锁,该用户和卖家的其余交易均可以马上并发执行,而不用等到整个分布式事务结束,能够得到更高的并发交易能力。

3.2.3 一致性

再来看看 TCC 分布式事务模型下的一致性实现。与 XA 协议实现一致性第一层语义相似,经过原子性保证事务的原子提交、业务隔离性控制事务的并发访问,实现分布式事务的一致性状态转变。

至于第二层语义:事务的中间状态不能被观察到。咱们来看看,在 SOA分布式应用环境下是不是必须的。

仍是以帐务服务举例。转帐业务(用户 A  用户 B),由交易服务和帐务服务组成分布式事务,交易服务做为主业务服务,帐务服务做为从业务服务,帐务服务的 Try 操做预冻结用户 A 的资金;Commit 操做扣除用户 A 的预冻结资金,增长用户 B 的可用资金;Cancel 操做解冻用户 A 的预冻结资金。

当帐务服务执行完 Try 阶段后,交易主业务就能够 Commit 了,而后由TCC 框架调用帐务的 Commit 阶段。在帐务 Commit 阶段还没执行结束的时候,用户 A 能够查询到本身的余额已扣除,可是,此时用户 B 的可用资金还没增长。

从系统的角度来看,确实有问题与不肯定性。在第一阶段执行结束到第二阶段执行结束之间,有一段时间的延时,在这段时间内,看似任何用户都不享有这笔资产。

可是,从用户的角度来考虑这个问题的话,这个时间间隔可能就无所谓或者根本就不存在。特别是当这个时间间隔仅仅是几秒钟,对于具体沟通资产转移的用户来说,这个过程是隐蔽的或确实能够接受的,且保证告终果的最终一致性。

固然,对于这样的系统,若是确实须要查看系统的某个一致性状态,能够采用额外的方法实现。

通常来说,服务之间的一致性比服务内部的一致性要更加容易弱化,这也是为何 XA 等直接在资源层面上实现通用分布式事务的模型会注重一致性的保证,而当上升到服务层面,服务与服务之间已经实现了功能的划分,逻辑的解耦,也就更容易弱化一致性,这就是 SOA 架构下 BASE 理论的最终一致性思想。

BASE 理论是指 BA(Basic Availability,基本业务可用性);S(Soft state,柔性状态);E(Eventual consistency,最终一致性)。该理论认为为了可用性、性能与降级服务的须要,能够适当下降一点一致性的要求,即“基本可用,最终一致”。

业内一般把严格遵循 ACID 的事务称为刚性事务;而基于 BASE 思想实现的事务称为柔性事务。柔性事务并非彻底放弃了 ACID,仅仅是放宽了一致性要求:事务完成后的一致性严格遵循,事务中的一致性可适当放宽;

3.2.4 小结

TCC 分布式事务模型的业务实现特性决定了其能够跨 DB、跨服务实现资源管理,将对不一样的 DB 访问、不一样的业务操做经过 TCC 模型协调为一个原子操做,解决了分布式应用架构场景下的事务问题。

TCC 模型经过 2PC 原子提交协议保证分布式事务的的原子性,把资源层的隔离性上升到业务层,交给业务逻辑来实现。TCC 的每一个操做对于资源层来讲,就是单个本地事务的使用,操做结束则本地事务结束,规避了资源层在 2PC 和 2PL 下对资源占用致使的性能低下问题。

同时,TCC 模型也能够根据业务须要,作一些定制化的功能,好比交易异步化实现削峰填谷等。

可是,业务接入 TCC 模型须要拆分业务逻辑成两个阶段,并实现 Try、Confirm、Cancel 三个接口,定制化程度高,开发成本高。

四. 总结

本文首先介绍了典型的分布式事务的架构场景。分布式事务刚开始是为解决单服务多数据库资源的场景而诞生的。随着技术的发展,特别是 SOA 分布式应用架构以及微服务时代的到来,服务变成了基本业务单元。所以,又产生了跨服务的分布式事务需求。而后从 XA 和 TCC 两种经常使用的分布式事务模型入手,介绍了其实现机制,着重分析了各模型是如何实现分布式事务 ACID 特性的。