分布式系统中,不同服务之间的交互可能会出现各种问题,如网络、异常等,可能会导致服务间的数据产生不一致的情况,如何避免?本文将详细讲述分布式事务的原理和解决方案。
目前大多是互联网公司都选择的是分布式系统架构,随之而来暴露本地事务出现的问题。所以了解分布式事务之前,需要了解什么是本地事务。
那么本地事务,在分布式系统中会出现哪些问题?这里我们用订单服务、库存服务和优惠券服务来举例说明。
第一种情况:保存订单成功,调用库存服务时,库存锁定更改成功,但是由于服务器卡顿等原因,导致调用超时,订单服务报调用超时异常,并回滚数据,此时库存已锁定更改,但是订单数据已全部回滚,导致数据不一致。
第二种情况:保存订单成功,调用库存服务也成功,接着调用优惠券服务去锁定优惠券,这时优惠券服务报了异常,订单和优惠券服务进行了回滚,但是已经执行过事务的库存服务无法回滚,导致数据不一致。
总的而言,本地事务在分布式系统,只能控制住自己的回滚,控制不了其他服务的回滚,出现异常很可能产生数据不一致的情况。所以需要分布式事务来解决此类问题。
了解分布式事务之前,我们还需要了解 CAP 原则和 BASE 理论。
CAP 原则又称 CAP 定律,指的是在一个分布式系统中,一致性、可用性、分区容错性,三者不可兼得。
特性 | 说明 |
---|---|
一致性(Consistency) | 所有节点访问的是同一份最新的数据副本,属于强一致性。 |
可用性(Availability) | 服务一直可以访问,且是正常访问时间。 |
分区容错性(Partition tolerance) | 分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供服务。 |
既然三者不可兼得,对待数据就有以下几种不同的一致性策略。
一致性策略 | 说明 |
---|---|
强一致性 | 所有节点访问的是同一份最新的数据副本。 |
弱一致性 | 数据更新后,能容忍后续的访问只能访问到部分或者全部访问不到。 |
最终一致性 | 在一段时间后,节点间的数据会最终达到一致状态。 |
我们假设一个分布式网络中,有两个节点Host1
和Host2
,分别对应数据库Data0
和Data1
,我们将一个数据备份到这两个数据中,假设这个数据值为age = 18
,此时**Data0**
更改数据值为**age = 28**
。
接下来,我们分析如何才能满足 CAP 原则:
Data0
中更改age = 28
,则Data1
中必须同步更改;Host1
或Host2
,都会在正常时间内响应结果。Host1
或Host2
故障的时候,仍然能够对外提供服务。最后,我们根据不同情况进行分析证明:
保证 C 和 P 的情况下:一致性要求Data0
须将值复制给Data1
,分区容错代表Data0
和Data1
可能会有一个出错,这时Data1
并不能及时同步数据,为了保证数据一致性只能阻塞等待数据同步,此时则无法保证可用性了。
保证 A 和 P 的情况下:可用性要求Host1
和Host2
在正常时间内响应,同样由于网络问题**Data1**
还没来得及同步数据,但为了保证可用性直接返回数据,返回的可能是旧的数据,此时则无法保证一致性了。
保证 C 和 A 的情况下:一致性和可用性,必须保证网络可靠不出故障的情况下才可能实现,此时只有不分区才能实现,这样则无法保证分区容错性,且此时将不再算是分布式系统了。
综上所述,在一个分布式系统中,一致性、可用性、分区容错性,三者不可兼得。
舍弃分区容错性,既不分区,违背了分布式系统的设计初衷。所以对于一个分布式系统来说,P 是一个基本要求,CAP 三者中,只能在 CA 两者之间做权衡,并且要想尽办法提升 P。
如关系型数据库 Oracle、MySQL 就是 CA。
舍去可用性,要求数据强一致性,此时分区需要数据痛,而网络故障或消息丢失可能导致同步时间无限延长,可能影响用户体验,等数据完全一致后,才能成功响应。
如非关系型数据看 Redis、HBASE等,分布式系统中常用的 Zookeeper 也是选择优先保证 CP。
还有跨行转账场景中,一次转账请求要等待双方银行系统都完成整个事务才算完成。
舍弃一致性,保证可用性和分区容。前提是用户能够接受在一定时间内,查询到的数据不一定是最新的。这种通常会保证最终一致性,后面的 BASE 理论就是根据 AP 来扩展的。
如抢购场景,你看到的可购库存数量是有的,你点进去就会显示购买失败,库存已空,这种就是舍弃了一致性,这里属于强一致性,但是最后会保证数据最终一致性。
BASE 是 Basically Available、Soft State 和 Eventually Consistent 的缩写,含义分别是基本可用、软状态和最终一致性。
我们已了解分布式系统在保证 AP 性质的同时,无法做到强一致性。所以基本可用的核心思想是,即便无法保证强一致性,也可以根据应用特点采取适当措施,来达到最终一致性的效果。
基本可用,本质是一种妥协,就是说当出现节点故障或系统过载时,通过牺牲非核心功能的可用性,保证核心功能的稳定运行。
实现基本可用的策略:
策略 | 示例 |
---|---|
流量削峰 | 秒杀活动将热门商品时间错开,削弱请求峰值。 |
延迟响应、异步处理 | 抢购商品,基于队列收到下单请求,排队异步处理,延迟响应。 |
体验降级 | 看到的非实时数据,采用缓存数据提供服务。压缩图片质量,提高性能。 |
熔断、限流 | 直接拒绝掉部分请求,或当请求队列满后移除部分请求,保证整体系统可用。 |
故障隔离 | 出现故障,做到故障隔离,避免影响其他服务。 |
软状态,允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延迟。
与硬状态对应,硬状态要求多个节点的数据副本都是一致的。
最终一致性,指系统中的所有数据副本,再经过一定时间的同步后,最终能达到一致的状态。在没有发生故障的前提下,这里的“一定时间”取决于网络延迟,系统负载和数据复制方案设计等因素。
本质就是系统保证数据最终一致,而不需要实时保证数据强一致性。
总的来说,BASE 理论面向的是大型高可用可扩展的分布式系统,和传统事务的 ACID 是相反的,它完全不同于 ACID 的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间是不一致的。
分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。
简单说就是这个事务需要多个系统通过网络协同完成,来保证分布式系统中的数据一致性。
2PC 和 3PC 都是基于XA
协议实现的分布式事务。XA
接口提供了事务管理器和本地资源管理器,其中本地资源管理器多由数据库实现,如 Oracle、MySQL。
2PC 就是两阶段是提交,第一阶段为准备阶段,若所有事务参与者都预留资源成功,则第二阶段进行提交,否则事务协调者回滚全部资源。
如图,由事务协调者询问通知各个事务参与者,是否准备好了执行事务。
详细流程:
协调者收到各个参与者的准备消息后,根据反馈情况通知各个参与者提交或回滚。
如图,当第一阶段所有参与者都反馈同意时,协调者发起正式提交事务的请求,当所有参与者都回复同意时,则意味着完成事务,具体流程如下:
如果任意一个参与者节点在第一阶段返回的消息为中止,或者协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时,那么这个事务将会被回滚,具体流程如下:
它是一个强一致性的同步阻塞协议,事务执⾏过程中需要将所需资源全部锁定,也就是俗称的刚性事务。所以它比较适⽤于执⾏时间确定的短事务,整体性能比较差。
一旦事务协调者宕机或者发生网络抖动,会让参与者一直处于锁定资源的状态或者只有一部分参与者提交成功,导致数据的不一致。因此,在⾼并发性能⾄上的场景中,基于 XA 协议的分布式事务并不是最佳选择。
3PC,三阶段提交协议,是二阶段提交协议的改进版本,三阶段提交有两个改动点:
2PC 和3PC 都无法保证数据绝对的一致性,一般为了预防这种问题,可以添加一个报警,比如监控到事务异常的时候,通过脚本自动补偿差异的信息。
两阶段提交的一个变种,不同的是 TCC 为在业务层编写代码实现的两阶段提交。TCC 分别指 Try、Confirm、Cancel ,一个业务操作要对应的写这三个方法。
TCC 不存在资源阻塞的问题,因为每个方法都直接进行事务的提交,一旦出现异常通过则 Cancel 来进行回滚补偿,这也就是常说的补偿性事务。
TCC 对业务的侵入性很强,原本一个方法,现在却需要三个方法来支持 ,而且这种模式并不能很好地被复用,会导致开发量激增。
还要考虑到网络波动等原因,为保证请求一定送达都会有重试机制,所以考虑到接口的幂等性。
如下图,可将执行流程分为两个阶段:
以下单扣库存为例,Try 阶段去锁库存,不真正删减,Confirm 阶段则实际扣库存,如果库存扣减失败 Cancel 阶段进行回滚,释放库存。
TCC 事务相比于上面介绍的 XA 事务机制,有以下优点:
缺点是 TCC 的 Try、Confirm 和 Cancel 操作功能要按具体业务来实现,业务耦合度较高,提高了开发成本。
其核心思想是将长事务拆分为多个本地短事务并依次正常提交,如果所有短事务均执行成功,那么分布式事务提交;如果出现某个参与者执行本地事务失败,则由 Saga 事务协调器协调根据相反顺序调用补偿操作,回滚已提交的参与者,使分布式事务回到最初始的状态。
与TCC事务补偿机制相比,TCC有一个预留(Try)动作,相当于先报存一个草稿,然后才提交。Saga事务没有预留动作,直接提交。
如图, Sage 事务基本协议:
如图,当执行事务失败时,补偿所有已完成的事务,撤销掉之前所有成功的子事务。
对于执行不通过的事务,会尝试重试事务直到成功,不需要补偿,这种方式适用于必须要成功的场景。
由于 Saga 模型没有 Prepare 阶段,因此事务间不能保证隔离性。当多个 Saga 事务操作同一资源时,就会产生更新丢失、脏数据读取等问题。这时需要在业务层控制并发,在应用层面加锁。
核心思路就是将分布式事务拆分成本地事务进行处理,有事务主动方和事务被动方两种角色。
事务主动发起方需要额外新建事务消息表,用来在本地事务中和记录事务消息、完成业务处理,并轮询事务消息表的数据发送事务消息,事务被动方基于消息中间件消费事务消息表中的事务。
这样可以避免以下两种情况导致的数据不一致性:
一些必要的容错处理如下:
优点:从应用设计开发的角度实现了消息数据的可靠性,消息数据的可靠性不依赖于消息中间件,弱化了对 MQ 中间件特性的依赖。
缺点:
消息的可靠性依赖于消息中间件,本质上是对本地消息表的封装,整体流程与本地消息表一致,唯一不同的就是将本地消息表存在了MQ内部,而不是业务数据库中,如下图。
不同的 MQ 有不同的实现方式,详细介绍放到对应的中间件介绍。
相比本地消息表方案,优点是:
缺点:
最大努力通知也称为定期校对,是对基于可靠消息的进一步优化。它在事务主动方增加了消息校对的接口,如果事务被动方没有接收到主动方发送的消息,此时可以调用事务主动方提供的消息校对的接口主动获取。
在可靠消息事务中,事务主动方需要将消息发送出去,并且让接收方成功接收消息,这种可靠性发送是由事务主动方保证的。
而最大努力通知中,事务主动方仅是通过重试、轮询,将事务发送给事务接收方,所以存在事务被动方接收不到消息的情况,此时需要事务被动方主动调用事务主动方的消息校对接口查询业务消息并消费,这种通知的可靠性是由事务被动方保证的。
所以最大努力通知适用于业务通知类型,例如微信交易的结果,就是通过最大努力通知方式通知各个商户,既有回调通知,也有交易查询接口。
方案 | 特性 | 场景 |
---|---|---|
2PC/3PC | 刚性事务 | 适合传统的单体应用,在同一个方法中存在跨库操作的情况,不适合高并发和高性能要求的场景。 |
TCC | 柔性事务 | 只要能把 Try、Confirm、Cancel 各阶段的逻辑捋清楚就可以使用TCC了, 但是存在业务耦合。 |
基于 MQ | 柔性事务 | 适用于事务中参与方支持操作幂等,业务上能容忍数据不一致。 |
Saga | 柔性事务 | 由于 Saga 事务不能保证隔离性,需要在业务层控制并发,适合于业务场景事务并发操作同一资源较少的情况。Saga 由于缺少预提交动作,导致补偿动作的实现比较麻烦,例如业务是发送短信,补偿动作则得再发送一次短信说明撤销,用户体验比较差。所以,Saga 事务较适用于补偿动作容易处理的场景 |
一些分布式事务中间件总结:
Seata
参考:
[1] 高级互联网专家. 分布式事务理论.
[2] 还能在学一小时. 分布式常见解决方案.
[2] 程序员小富. 五种分布式方案,最终选择了Seata.
今天来说一个老生常谈的问题,来看一个实际案例:业务中往往都会通过缓存来提高查询效率,降低数据库的压力,尤其是在分布式高并发场景下,大量的请求直接访问Mysql很容易造成性能问题。
由于数据库的承载能力是有限的,当业务增长量达到一定规模后,数据库的性能就会达到瓶颈。于是产生了分库分表的解决方案,本文将详细讲解什么是分库分表,以及分库分表的原因和可能产生的问题。