前言
最近,因需要组建新的技术团队,面试了近百位的后端研发人员。当然,也不是每个都会聊到分布式事务的话题,一般对那些期望薪酬较高,工作经验相对长的应聘者才会掰扯这个事情。分布式事务在实际研发中的落地是一个具体情况具体分析的问题,甚至还要结合具体业务流程来求取最优解,但发现大多数应聘者连一些基本实现思路都抓不住,所以尝试结合经验,梳理总结一下。
事务
一般的定义是指一系列的操作必须同时成功或者撤销。而在实际研发中,可以定义得更具体一点,事务是指一系列数据持久化操作必须同时成功或者撤销。在简单的架构中,事务一般是由所使用的数据库来保证的。基本上,主流的关系型数据库对事务的支持已经相当成熟了。MONGODB在4.0版本也增加了对事务特性的支持。
事务具有四个属性:原子性、一致性、隔离性和持久性,简称ACID属性。
原子性(atomicity):原子性是指事务上下文的所有事务操作必须同时成功或者撤销。事务操作,可以更具体的认为是数据库操作。
隔离性(isolation):隔离性是不同事务之间相互独立,在事务没完成之前不会对其它事务造成任何影响。在具体落地时,数据库实现了四种隔离级别以应对不同场景的需要,分别是”读未提交”、“读已提交”、“可重复度”和“串行化”。
持久性(durability):持久性是指事务处理完成后所提交的数据状态会被保存下来,不会随着事务结束而消失。
一致性(consistency):一致性是指事务执行完成后,数据的状态是从一个“正确”的状态转换到另一个“正确”的状态,绝不会出现“错误”的中间状态。
持久性是前提,原子性和隔离性是手段,而一致性是最终目的。因为数据需要持久性,所以才需要对其状态进行管控,保证数据的状态在任何时间都是“正确”的。这个任何时间“正确”就是一致性。而原子性和隔离性是达到这个目标的手段。
分布式事务
分布式事务是指在分布式架构中因事务操作执行上下文分离或数据持久化分区治理所带来的数据一致性问题。在非分布式事务中,事务操作是在一个执行上下文完成,依赖数据库所提供的事务机制就可以轻松实现事务的“原子性”和“隔离性”控制。但在事务操作被分离到不同的上下文的时候,比如微服务中“会员服务在充值成功后调用积分服务赠送积分”,如何保证相关事务操作的特性呢?另外,数据持久化分区治理后,可简单理解成数据存储在不同的数据库实例,即使事务操作在同一个上下文完成,但也无法像非分布式事务那样依赖数据库轻松实现事务控制。
进一步分析下,可以得到这样的一个结论:分布式事务是在分布式系统中通过某种方式追求事务的“原子性”而达到“某种程度”的一致性,而对于“隔离性”的保证则被放在较低的优先级。数据的一致性,在分布式架构中,引入了“强一致性”、“弱一致性/最终一致性”。非分布式事务一般都是追求强一致的。
强一致性:所有分区的数据状态在任何一个时刻都是一致性的。在非分布式架构中,数据分区只有一个,很容易达成强一致的目标。
弱一致性/最终一致性:数据状态更新后,允许部分分区的数据状态暂时不一致,但最终随着时间的推移,所有分区的数据状态恢复一致。弱一致强调是各分区的数据状态允许暂时不同步,而最终一致强调是各分区数据状态一定会最终同步。有些人会把这两个概念独立理解,我觉得应该放在一起理解,它们只是阐述了同一个问题的不同点而已。
另外,根据读和写,还可以尽一步把追求一致性的目标细化到读一致性和写一致性。
CAP原则
CAP原则是指在一个分布式数据存储中,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错),三者不可兼得。
一致性:wikipedia给出的定义是“对分布式数据存储的每次数据读取都能读取到最新的数据或者返回错误”,也可以简单理解为分布式数据存储中所有分区的同一数据在任何时刻都是相同的。它与分布式事务中的一致性是不同的概念。
可用性:wikipedia给出的定义是“每个请求都能获得一个非错误的响应,但不保证所响应的数据是最新的数据”,也可以简单理解为分布数据存储所能处理的读写并发量。
分区容错:wikipedia给出的定义是“分布式数据存储中各分区能容忍数据不一致或数据同步延迟的程度”。多数情况是因为网络中断或者分区节点自身的原因,分区数据同步失败导致暂时数据处于不相同的状态。
从实际研发而言:可用性为0的系统是没有意义的;因网络失败不可避免,所以数据分区错误也是必然会发生的;而一致性则可以在合理情况下折衷处理。我偏向使用下图描述CAP三元素的损益关系。
一致性和分区容错是相斥的,就像天平的两端,而可用性就是天平的平衡状态,完全端平还是有所偏向则根据所设计的产品取向可以有所不同。
有些应聘者直接把分布式数据存储换成分布式系统来讨论CAP原则,我个人认为是不严谨的。CAP原则所讨论的对象是数据被分布式存储后的问题,而不是分布式系统问题。如果分布式系统中的数据没有被分区存储,其实不在CAP原则的讨论范围。另外,也有应聘者把CAP原则和分布式事务混淆在一起。这里尝试简单区分一下:CAP所讨论的场景是数据被分区存储后如何应对数据同步失败的问题,它的一致性是指同一数据在不同分区存储是否相同或者延迟多久能达到相同。分布式事务的一致性是指完成操作后,数据状态从一个“正确”的状态进入另一个“正确”的状态。而CAP一致性强调的是不同分区中的同一份数据在任何时刻是否相同或者说以多快的速度达到相同;分布式事务的一致性是指完成操作后,数据状态从一个“正确”的状态进入另一个“正确”的状态。
BASE理论
Basically Available/Soft State/Eventual Consistency,基本可用/软状态/最终一致性理论是CAP原则在选择牺牲一致性获取可用性(AP)后的进一步延伸。
厘清分布式事务的问题边界
这里对CAP原则/BASE理论的说明,是想划清它们与分布式事务的问题边界。现在互联网上很多的技术文章都把分布式事务作为CAP原则/BASE理论框架下面应用实践,而我始终觉得需要加以区分。比如一个带读副本的主从架构的MYSQL数据库,是一个分布式存储架构,主从节点就是不同的数据分区,对主从分区数据一致性的讨论符合CAP和BASE范围,但不在分布式事务的问题边界内。而以“充值送积分”为例,充值金额和积分分别存储在不同的数据库,那如何保证金额余额和积分余额状态的迁移是一致的呢?这个是分布式事务的问题,但不CAP和BASE的讨论范围。假如,有不同的数据分区存储了积分数据,那这个积分数据如何保持同步,则变成了CAP和BASE的讨论范围。
阐述得好像有点绕圈子,一言蔽之:分布式事务处理的是数据关联状态迁移的一致性;CAP/BASE讨论的是同一份数据在不同分区中的同步问题。
我个人认为不存在CP偏向的设计。既能容忍高分区错误,又能保持数据一致性,这是什么设计?
两阶段提交/2PC
2PC是指协调者(Coordinator)通过“请求”和“执行”两个阶段协调各参与节点的本地事务处理,从而实现分布式事务的“原子性”和“隔离性”。
另外,若期望分布式提交有比较好的实际表现,节点要满足以下的条件:
1. 本地事务节点必须能可靠保存数据,即使节点损坏,数据也能恢复
2. 节点之间的网络通信必须稳定可靠,并且耗时较少,比如节点一次通信在20毫秒以内
请求阶段:
1. 协调者向参与者询问是否可用进行提交操作
2. 参与者执行本地事务操作,保存回滚/重试的必要数据,并根据执行结果返回”同意“或”终止“
执行阶段:
1. 若获取到所有参与者的”同意“响应,协调者向参与者发出”执行提交”请求;若任一参与者返回“终止”响应或者超时无响应,协调者则向参与者发出“执行回滚“请求
2. 协调者接收到所有参与者执行完成响应,完成或取消事务
XA规范是开放群组关于分布式事务处理 (DTP)的规范,描述了全局的事务管理器与局部的资源管理器之间的接口。XA使用了两阶段提交算法。
两阶段提交中协调者在协调本地事务时以阻塞的方式执行,追求比较强的一致性,所以必然会牺牲可用性,损害系统的并发处理能力。此外,两阶段提交在请求阶段会对目标数据上锁,避免在执行阶段完成前被其它事务修改,当分布式系统之间远程调用相对耗时时,会对整个系统的稳定性和吞吐量造成严重影响。数据库的事务一般倾向于比较快完成,大概50到100毫秒。因此,微服务架构不推荐使用2PC。另外,当第二阶段执行时出现网络故障可能会导致部分参与者收不到"执行提交”请求而导致数据出现不一致的状态。
三阶段提交/3PC
3PC将2PC的“执行阶段”拆解成“预备”和“完成”两个阶段。
预备阶段执行修改,但不提交,直到所有参与者都完成预备修改后,参与者在收到协调者的“完成”指示或者等待超时后完成提交或回滚。3PC将执行阶段细化成预备和完成两阶段后,当协调者发出”预备“指示后崩溃,恢复的协调者可以通过询问所有参与者的目前的状态来恢复事务的上下文;而当发出“完成”指示后崩溃,则同样会导致数据出现不一致状态。所以,3PC是减轻2PC在发生节点崩溃后所造成的损害,并不能完全解决。我目前的观点是没有任何一个方法可以完全消灭崩溃所造成的影响。另外,网上有提到3PC相对于2PC是”非阻塞”的。对于这个“非阻塞”的理解,暂时只能是指参与者引入”超时提交“后,对并发做了一些优化。但这和在2PC中引入参与者”超时回滚“后,其实并没有本质上的区别。
TCC(Try-Confirm-Cancel)
TCC也分成三个阶段,但与3PC有着本质上不同。3PC是通过引入协调者尝试从基础上去解决分布式事务的问题;而TCC则从业务逻辑构建方面寻找解决方式,要求实现业务逻辑分拆三个阶段来实现,比如把”转账“分拆”尝试转账”、“确认转账”和“取消转账”。另外,要求所分拆出来的三阶段逻辑具有幂等性。TCC必然会增加业务逻辑实现的复杂性,具体研发的时候有比较多的细节,也需要依赖研发人员的能力水平。此外,TCC一般要结合“本地消息”等进行事务补偿。
本地消息日志/事务补偿
本地消息日志是指本地事务在执行数据修改的同时保存相应记录到本地消息日志表,在出现异常情况下,利用本地消息日志表进行事务补偿,从而使数据达到最终一致。本地消息日志一般是作为“异步”分布式事务方案中的一个组成部分,还需要结合可靠消息的中间件来协调整体事务的执行和操作。另外,也可以结合TCC。
微服务架构下分布式事务
不建议在微服务架构使用2PC和3PC这种追求“强”一致的阻塞型方案,而TCC则根据实际情况来选用。TCC其实定义的是实现业务逻辑的方式,它本身并不是一个完备的解决分布式事务的方案,需要结合本地消息/可靠消息机制。在微服务架构下,首先我们要调整一下事务的追求目标:最终一致性和弱隔离。最终一致性,文章前面已经做了介绍。至于弱隔离是指,事务执行过程中会出现部分数据已经提交并且能被其它事务读取的情况。为了应对弱隔离所带来的问题,一般需要在业务层面构建适当的“状态机”,比如“转账”,非分布式事务下执行完成即转账完成,但在非阻塞型的分布式事务方案下,则还会有中间状态,如“转账中”等。
微服务架构建议依赖可靠消息和本地消息日志实现非阻塞分布式事务:
1. 调用链(RPC链)的开始生成“事务唯一标识”,标识一个跨服务的全局事务。”事务唯一标识“作为参数传递到各服务使用。
2. 服务执行完成后,使用”全局事务标识“生成本地消息日志,以提供事务”回滚“时使用。
3. 服务RPC失败重试3次,防止因暂时网络原因,导致事务失败。
4. 利用可靠消息完成事务失败通知(通知中带事务唯一标识),各服务收到通知执行相应事务回滚。
5. 根据实际情况实现”业务“层面的回滚通知,为用户提供相对友好的事务处理异常体验
另外,有部分人在讨论完全依赖可靠消息来进行事务控制,包括提交和回滚。我个人感觉研发落地比较复杂。以上所提议的方案中,是同步和异步相结合的一个方案。服务之间使用同步RPC执行,当异常发生后使用可靠消息通知所有相关的服务参与方进行回滚。如果回滚操作失败,则生成系统异常日志,运维人员介入进行数据正确性检查。