分布式事务

Posted by ZhouJ000 on March 8, 2019

事务

事务提供一种机制将一个活动涉及的所有操作纳入到一个不可分割的执行单元,组成事务的所有操作只有在所有操作均能正常执行的情况下方能提交,只要其中任一操作执行失败,都将导致整个事务的回滚。简单讲,事务提供一种”要么都不做,要么全做(All or Nothing)”的机制

ACID

数据库事务的四个基本要素:
1、原子性(Atomicity):整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样
2、一致性(Consistency):事务必须始终保持系统处于一致的状态,不管在任何给定的时间并发事务有多少
3、隔离性(Isolation):比如有两个事务,运行在相同的时间内,执行相同的功能,事务的隔离性将确保每一事务在系统中认为只有该事务在使用系统。这种属性有时称为串行化,为了防止事务操作间的混淆,必须串行化或序列化请求,使得在同一时间仅有一个请求用于同一数据
4、持久性(Durability):在事务完成以后,该事务对数据库所作的更改便永久的保存在数据库之中

隔离级别

并发会引起以下几个问题:
1、更新丢失,这是在完全没有隔离事务造成的
2、脏读,一个事务读到另一个事务未提交的更新数据
3、不可重复读,一个事务读到另一个事务已提交的更新数据,对于提交事务也会造成覆盖更新,即一个事务覆盖另一个事务已提交的更新数据,而后查询时发现自己提交的更新数据已经变了
4、幻读,一个事务读到另一个事务已提交的新插入的数据

由于隔离性要求多个事务串行执行,彼此之间不会受到任何干扰,虽然能够完全保证数据的安全性,但在实际业务系统中这种方式性能不高。因此数据库定义了四种隔离级别,其中隔离级别越低,性能就越好,但问题越大:
1、Read uncommitted(读未提交):一个事务对一行数据修改时增加写锁,不允许其他事务修改但允许读,因此不会出现更新丢失,会出现脏读、不可重复读、幻读
2、Read committed(读提交):在1的基础上,写数据会锁住相应行,因此未提交的该行数据不会被其他事务访问,不会出现脏读,然而读取数据加上共享锁,读完后立即释放(瞬间),因此还是出现不可重复读、幻读
3、Repeatable read(重复读):在1的基础上,事务的读加共享锁直到事务结束才释放,写加排他锁防止其他事务修改,因此不会出现脏读、不可重复读,Mysql的InnoDB和XtraDB存储引擎通过多版本并发控制MVCC也解决了幻读的问题
4、Serializable(序列化):所有事务中读写都必须串行执行,读写数据会锁表,避免一切因并发引起的问题,但效率最差

本地事务

在条件允许的情况下,我们应该尽可能地使用本地事务,因为在本地事务里,无需额外协调其他数据源,减少了网络交互时间消耗以及协调时所需的存储IO消耗,而且实现方便、性能更高,大部分数据库都提供了事务的支持

try{
   //...执行增删改查sql
   conn.commit();	//提交事务
}catch (Exception e) {
   conn.rollback();	//事务回滚
}finally{
   conn.close();
}

MySQL的本地事务由资源管理器进行管理,而事务的ACID是通过存储引擎(InnoDB)的日志和锁来保证。事务的隔离性是通过数据库锁的机制实现的,持久性通过Redo Log(重做日志)来实现,原子性和一致性通过Undo Log来实现

然而由于业务逻辑解耦等因素,对数据库进行了垂直拆分;或者由于业务量增大的性能压力,对数据库进行了水平拆分之后,数据分布于多个数据库;或者使用SOA进行服务化,这时如果需要对多个数据库的多个数据进行协调、统一变更,就需要使用分布式事务

分布式事务

CAP定理

CAP定理提出在一个分布式系统中,无法同时满足以下三个特性:
1、一致性(Consistency):在分布式系统中的所有数据备份,在同一时刻是否同样的值,即所有节点访问同一份最新的数据副本
2、可用性(Availability):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求,即每个读写操作都必须以可预期的响应结束
3、分区容错性(Partition tolerance):分区相当于对通信的时限要求,如果不能在时限内达成数据一致性,就意味着发生了分区的情况

由于当前的网络硬件肯定会出现延迟丢包等问题,所以分区容错性一般来说无法避免,因此CAP定理告诉我们剩下的一致性和可用性无法同时满足,因为可能通信失败(即出现分区容错),所以我们需要在C和A之间做出选择

然而由于CAP作为定理也受到了许多质疑,提出者发出声明:
1、”3个中的2个”这个表述是不准确的,在某些分区极少发生的情况下,三者能顺畅地在一起配合
2、CAP不仅仅是发生在整个系统中,可能是发生在某个子系统或系统的某个阶段
随后又缩小了CAP适用的定义,消除了质疑的场景:
1、把CAP理论的证明局限在原子读写的场景,并申明不支持数据库事务之类的场景
2、一致性场景不会引入用户agent,只是发生在后台集群之内
3、把分区容错归结为一个对网络环境的陈述,而非之前一个独立条件
4、引入了活性(liveness)和安全属性(safety),在一个更抽象的概念下研究分布式系统,并认为CAP是活性与安全熟悉之间权衡的一个特例。其中的一致性属于liveness,可用性属于safety

因此CAP并不适合再作为一个适应任何场景的定理,它的正确性更加适合基于原子读写的NoSQL场景。而无论如何C、A、P这个三个概念始终存在任何分布式系统,只是不同的模型会对其有不同的呈现,可能某些场景对三者之间的关系敏感,而另一些不敏感。现在分布式系统还有很多特性,比如扩展性、优雅降级等

BASE理论

分布式一致性是一个相当重要且被广泛探索与论证问题,在不同场景下对数据一致性的需求是不一样的。而在分布式系统中要解决的一个重要问题就是数据的复制,数据复制在可用性和性能方面给分布式系统带来了巨大好处,然而由于延时(网络异常、分区、超时)可能出现不同副本之间的数据不一致。对保证数据一致性和不影响系统运行的性能之间进行权衡,会出现强一致性、弱一致性和最终一致性。而BASE理论就是为了解决关系数据库强一致性引起的问题而引起的可用性降低而提出的解决方案,它是基于CAP定理逐步演化而来的:
1、基本可用(Basically Available):在出现不可预知故障的时候,允许损失部分可用性,比如响应时间上和系统功能上的损失(降级)
2、软状态(Soft State):与硬状态相对,允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时
3、最终一致性(Eventually Consistent):系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态,而不需要实时保证系统数据的强一致性
理论的核心思想就是:我们无法做到强一致性,但每个应用都可以根据自身的业务特点,牺牲强一致性来获得可用性,采用适当的方式来使系统在一段时间后达到最终一致性,也就是最终一致性的柔性事务

X/Open DTP模型

DTP模型由5个基本元素构成:
1、应用程序(Application Program,AP):用于定义事务边界(即定义事务的开始和结束),并且在事务边界内对资源进行操作
2、资源管理器(Resource Manager,RM):如数据库、文件系统等,并提供访问资源的方式
3、事务管理器(Transaction Manager,TM):负责分配事务唯一标识,监控事务的执行进度,并负责事务的提交、回滚等
4、通信资源管理器(Communication Resource Manager,CRM):控制一个TM域(TM domain,即一个或多个模型实例)内或者跨TM域的分布式应用之间的通信
5、通信协议(Communication Protocol,简称CP):提供CRM提供的分布式应用节点之间的底层通信服务
一个DTP模型实例,至少有3个组成部分:AP、RM(s)、TM

2个模型实例,通过CRM来进行通信,CRM提供通行能力,事务传播能力(底层采用OSI TP通信服务): crm

当一个TM domain中,存在多个模型实例时,会形成一种树形条用关系,即全局事务树形结构(Global Transaction Tree Structure): tmdomain-tree

XA规范

在DTP本地模型实例中,由AP、RMs和TM组成,不需要其他元素。而它们之间彼此都需要进行交互。而XA规范的最主要作用是,定义了RM-TM的交互接口(XA Interface),除此之外,还对两阶段提交协议进行了优化,其中两阶段协议是在OSI TP标准中提出的,XA规范只是定义了两阶段提交协议中需要使用到的接口,也就是上述提到的RM-TM交互的接口,因为两阶段提交过程中的参与方,只有TM和RMs

XA规范中定义的RM 和 TM交互的接口: xa-interface

XA规范对两阶段提交协议有2点优化:
1、只读断言:在阶段1中,RM可以断言”我这边不涉及数据增删改”来答复TM的prepare请求,从而让这个RM脱离当前的全局事务,从而免去了阶段2
2、一阶段提交:如果需要增删改的数据都在同一个RM上,TM可以使用一阶段提交 —— 跳过两阶段提交中的阶段1,直接执行阶段2

基于2PC

一般说到分布式事务,最先想到的就是这种方式。但是其适用于参与者较少,单个本地事务执行时间较少,并且参与者自身可用性很高的场景,否则,其很可能导致性能下降严重。2PC在实际业务高并发场景中不使用,比较重,可以作为了解,其与数据库XA事务一样,两阶段提交使用了XA协议的原理

两阶段提交牺牲了一部分可用性来换取的一致性: 2pc

两阶段提交协议存在的问题:
1、同步阻塞问题:两阶段提交方案下全局事务的ACID特性,是依赖于RM的。一个全局事务内部包含了多个独立的事务分支,各个事务分支的ACID特性共同构成了全局事务的ACID特性。即使在本地事务,如果对操作读很敏感,我们也需要将事务隔离级别设置为SERIALIZABLE。而对于分布式事务来说,更是如此,可重复读隔离级别不足以保证分布式事务一致性。而事务隔离级别为SERIALIZABLE,即串行化,隔离级别最高,执行效率最低
2、单点故障:由于协调者的重要性,一旦协调者TM发生故障。参与者RM会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
3、数据不一致:在二阶段提交的阶段2中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这会导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作,但是其他未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据不一致性的现象

基于3PC

三阶段提交(3PC),作为二阶段提交(2PC)的改进版本,与两阶段提交不同的是,三阶段提交有两个改动点:
1、引入超时机制。同时在协调者和参与者中都引入超时机制
2、在阶段1和阶段2中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的,也就是3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段

3pc 1、CanCommit阶段:与2PC的准备阶段很像。协调者向参与者发送canCommit请求,参与者如果可以提交就返回Yes响应并进入预备状态,否则返回No响应
2、PreCommit阶段:协调者根据参与者的反应情况来决定是否可以进行事务的preCommit操作,分两种情况:
–>1)所有反馈都是YES:协调者向参与者发送preCommit请求,并进入Prepared阶段,参与者接收到preCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中,如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令
–>2)任意一个反馈是NO或超时未响应:协调者向所有参与者发送abort请求,参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),都将执行事务的中断
3、DoCommit阶段:进行真正的事务提交,也分为两种情况:
–>1)收到所有ACK响应:协调者向所有参与者发送doCommit请求,参与者接收到doCommit请求之后,执行正式的事务提交,事务提交完之后,向协调者发送ACK响应并释放所有事务资源,最后协调者接收到所有参与者的ack响应之后完成事务
–>2)任何一个ACK响应未收到或超时未响应:协调者向所有参与者发送abort请求,参与者接收到abort请求之后,利用其在阶段2记录的undo信息来执行事务的回滚操作并向协调者发送ACK消息,协调者接收到参与者反馈的ACK消息之后执行事务的中断
PS.特别的,在DoCommit阶段,如果参与者没有及时收到doCommit或者rebort请求,会在等待超时之后继续进行事务的提交

3PC比2PC改进的地方:解决了单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。而不会一直持有事务资源并处于阻塞状态。但是这种机制也会导致数据一致性问题,由于网络原因abort请求没有收到而commit,导致与其他参与者之间存在数据不一致的情况

Google Chubby的作者Mike Burrows说过:”there is only one consensus protocol, and that’s Paxos – all other approaches are just broken versions of Paxos.”。意为世上只有一种一致性算法,那就是Paxos

基于补偿

有些场景下整个事务的提交与否不能由发起者决定,因而如果从业务数据出现异常,需要主业务进行相应的补偿回滚,在这种情况下,可以使用基于补偿的分布式事务。比如订单与支付流程,在不同服务下的时候,订单是否完成还与用户是否有足够的钱并且支付成功有关。即订单服务在创建订单数据后,暂不提交本地事务,发起远程调用给支付服务以完成扣款,支付成功后再提交订单服务的本地事务,另外也可以同时调用库存服务中进行减库存操作。在正常的流程下没问题,然而无论哪一个步骤出现异常,就需要回滚补偿,需要额外发送远程调用给支付服务退款,或者调用库存服务加回库存

在这种情况下,事务发起者不但需要写自己的本地事务,还有额外的补偿返还逻辑。基于补偿的分布式事务相比于基于消息实现的分布式事务更为复杂,需要额外开发相关的业务回滚方法,也失去了服务间流量削峰填谷的功能,同时基于远程调用是同步的。但其仅仅只比基于消息的事务复杂一点,若不能使用基于消息队列的最终一致性事务,那么应该优先考虑使用基于补偿的事务形态

阿里GTS也是利用补偿实现的,只不过补偿代码自动生成,无需业务干预,同时接管应用数据源,禁止业务修改处于全局事务状态中的记录

补偿是一个独立的支持ACID特性的本地事务,用于在逻辑上取消服务提供者上一个ACID事务造成的影响,对于一个长事务,与其实现一个巨大的分布式ACID事务,不如使用基于补偿性的方案,把每一次服务调用当做一个较短的本地ACID事务来处理,执行完就立即提交

TCC(两阶段补偿)

TCC对基于补偿的分布式事务进行改进,能适应更多的场景。比如前面提到的订单和支付流程,在整个事务未提交、订单未完成也未取消时,由于支付服务进行本地事务提交了扣款,但是在订单成功前不能让用户看到已经扣除了现金。或者比如进行飞机订票,如果需要转机订2个航空公司的机票,去调用不同航空公司的订票接口,那么只能买到1张是没有任何意义的,这种情况下退票的补偿可能需要支付额外的手续费。在这些情况下,可以使用TCC实现的分布式事务,与基于补偿的事务形态类似,从业务支付不直接进行支付扣款,而是采取冻结现金的try操作;从业务订票机构不直接进行购票,而是进行预留的try操作,同时也保证了并发过程中同时扣款导致余额不足或机票超卖的情况。成功以后返回订单服务,之后订单服务就可以提交并远程调用confirm操作,给支付服务或订票服务去扣除冻结的现金或真正购票。这种情况下如果出现异常,则调用cancel操作进行补偿,解冻现金或取消预留机票

TCC相比基于补偿的分布式事务,需要额外增加try冻结/预留、confirm扣除/购买、cancel解冻/取消的逻辑,实现上更为复杂,而且性能稍低

那么来详细看看TCC两阶段补偿,从名字中可以看出对应了Try、Confirm和Cancel三种操作:
1、Try:完成所有业务检查(一致性),预留业务资源(准隔离性)
2、Confirm:确认执行业务操作,不做任何业务检查,只使用Try阶段预留的业务资源
3、Cancel:取消Try阶段预留的业务资源
如果将应用看做资源管理器的话,TCC相当于应用层的两阶段补偿性事务

TCC与2PC对比,有点类似: tcc_2pc 1、在阶段1:在XA中各个RM准备提交各自的事务分支,事实上就是准备提交资源的更新操作(insert、delete、update等);而在TCC中,是主业务活动请求(try)各个从业务服务预留资源
2、在阶段2:XA根据阶段1每个RM是否都prepare成功,判断是要提交还是回滚。如果都prepare成功,那么就commit每个事务分支,反之则rollback每个事务分支。TCC中,如果在第一阶段所有业务资源都预留成功,那么confirm各个从业务服务,否则取消(cancel)所有从业务服务的资源预留请求
TCC与XA两阶段提交的区别:
1、XA是资源层面的分布式事务,强一致性,在两阶段提交的整个过程中,一直会持有资源的锁,导致性能低下
2、TCC是业务层面的分布式事务,最终一致性,不会一直持有资源的锁。try、confirm/cancel在执行过程中,一般都会开启各自的本地事务,来保证方法内部业务逻辑的ACID特性。在try过程的本地事务,是保证资源预留的业务逻辑的正确性;而在confirm/cancel执行的本地事务逻辑确认/取消预留资源,以保证最终一致性,也就是补偿型事务

TCC事务模型与DTP模型对比: tcc_dtp 1、TCC模型中的主业务服务相当于DTP模型中的AP应用程序,TCC模型中的从业务服务相当于DTP模型中的RM资源管理器
2、TCC模型中,从业务服务提供的try、confirm、cancel接口,相当于DTP模型中RM提供的prepare、commit、rollback接口
3、TCC模型中,阶段1的try接口是主业务服务调用(绿色箭头),阶段2的confirm、cancel接口是事务管理器TM调用(红色箭头),这就是TCC分布式事务模型的二阶段异步化功能。而在DTP模型中,阶段1的try和阶段2的commit、rollback,都是由TM进行调用的

各个参与方需要提供的API:
1、主业务服务:不需要提供接口,仅需要调用接口,发起并完成整个业务活动
2、从业务服务:需要提供try、confirm、cancel三个接口
3、事务管理/协调器:登记主业务服务上报的事务日志,即try阶段各个从业务服务活动资源是否预留成功的信息,并在业务活动提交时确认confirm操作和调用cancel操作

基于消息

单纯基于消息实现的分布式事务适用于分布式事务的提交或回滚只取决于事务发起方的业务需求,其他数据源的数据变更跟随发起方进行的业务场景。比如在用户订单成功后,为该用户增加积分,在这个场景下,订单服务为事务发起方,而积分服务为事务跟随者,只需要向积分服务发送增加积分的消息即可

依赖于MQ的消息可靠性(还有些MQ支持事务,类似2PC)、异步、高性能、高可用的特点,这种基于消息的事务形态过程简单,性能消耗小,发起方与跟随方之间的流量峰谷可以使用队列填平,同时业务开发工作量也基本与本地事务没有差别,都不需要编写反向的业务补偿逻辑,因此基于消息队列实现的分布式事务是可选场景下最优先考虑的

本地消息表(异步确保)

源于ebay,可以将分布式事务拆分成本地事务进行处理,可以看做一个异步的、利用消息队列实现的补偿事务,其适用于无需马上返回业务发起方最终状态的场景。比如跨行转账流程,首先需要一张消息表,流程开始先在汇款服务中的同一个事务内提交转账和消息存储,汇款状态为处理中,然后消息通过MQ发送到消费者,收款服务(需要保持幂等性)收到消息后处理本地事务,最后消息通知生产者汇款服务修改消息表状态,并根据成功与否修改汇款状态成功,或进行业务补偿,汇款回滚或收款重试。而且生产者还会定时扫描本地消息表,定时校对,对未完成或需要重试的消息再发送一遍,实现最终一致性

local-msg-table

本地消息表相对来说是对基于消息的事务形态和基于补偿的事务形态的结合,所有的本地子事务中都无需等待其他调用子事务执行,减少了加锁时间,同时利用消息队列进行通信,异步并具有削峰填谷的作用,还能对异常进行业务补偿。因此在不需要同步返回发起方执行最终结果、可以进行补偿、对性能要求较高、不介意额外编码的业务场景进行使用,在实际项目中还是比较常用的。由于不像TCC有资源预留,所以也可以进行适当改造,变成与TCC类似进行资源预留

特别的,有些服务本身没有本地事务,而是远程调用了其他RPC服务,那么这时候可以借鉴本地消息表的思路,采取外部事务表,每次调用前预先生成全局序列号id,记录这次事务需要调用哪些接口,每次调用成功后更新状态为成功。由于会存在多次调用的问题,被调用方需要做好幂等防重措施,并且在出现失败进行补偿机制

Saga(长时间运行事务)

Saga事务模型又叫做长时间运行事务(Long-running-transaction),是一种分布式异步事务,最终一致性的柔性事务。相对来说和本地消息表有点类似,然而Saga更适合做长流程的长时间运行事务。其核心思想就是拆分分布式系统中的长事务为多个短事务(或叫本地事务),然后由Sagas工作流引擎(流程管理器Process Manager)负责协调,如果整个流程正常结束,那么就算是业务成功完成,如果在这过程中实现失败,那么Sagas工作流引擎就会以相反的顺序调用补偿操作,重新进行业务回滚

Saga有两种形态:Choreography(无中心协调者)和Orchestration(有中心协调者)。无中心协调者的Saga方式需要使用事件/编排的概念,会增加开发复杂度,而有中心协调者的Saga方式需要可能存在协调者本身失败的单点风险,但是能够方便减轻业务应用的开发量,由协调者来命令/协调流程的前进和回退

扩展:
Saga分布式事务解决方案与实践

其他

除了基于消息的事务形态,比如可以监听数据库的binlog日志,通过canal等中间件把数据变更日志推送出去,然后再调用后续服务

确保兜底方案,必要时人工介入

总结

总体而言,本地事务肯定是最好的,当实际场景满足不了的时候,如果参与者较少且事务较轻,可以采用同步的TCC二阶段补偿方式。其余情况一般使用基于消息的事务形态,然后根据场景的需求由小到大依次采用:消息 > 本地消息表 > Saga,另外附加兜底方案,比如定期对账补账机制等

扩展:
微服务架构下分布式事务解决方案——阿里GTS