目录
一、分布式事物:本地事务和分布式事务(2PC+3PC)+传统分布式事务的问题
(一)本地事务和分布式事务(2PC+3PC)
(1)两阶段提交协议2PC
(2)三阶段提交协议3PC
(二)对于微服务,传统分布式事务存在的问题
二、CAP理论和BASE思想
1.CAP理论
一致性Consistency:
可用性Availability:
分区容错性PartitionTolerance:
2.BASE思想
BasicallyAvailiable(基本可用):
SoftState(软状态):
EventualConsistency(最终一致性):
3.CAP理论和BASE思想的关联性
三、可靠事件模式
1.基本思路
(1)用户下单
(2)交易支付
(3)订单更新
2.关键点
3.解决方案
4.实现策略
(1)事件确认组件
(2)事件恢复组件
(3)实时消息传递组件
四、补偿模式
1.基本思路
2.关键点
3.解决方案
五、Sagas长事务模式--错误管理模式,同时用于控制复杂事务的执行和回滚
1.基本思路
2.解决方案
六、TCC模式
1.基本思路
2.解决方案
3.实现策略
七、最大努力通知模式
1.基本思路
2.解决方案
八、人工干预模式
1.基本思路
2.解决方案
九、数据一致性模式总结
参考书籍、文献和资料:
传统单体应用一般都会使用一个关系型数据库,好处是使用ACID事务特性,保证数据一致性只需要开启一个事务,然后执行更新操作,最后提交事务或回滚事务。更方便的是可以以借助于Spring等数据访问技术和框架后只需要关注引起数据改变的业务本身即可。
但是随着组织规模不断扩大、业务量不断增加,单块应用不足以支持庞大业务量和数据量,就需要对服务和数据进行拆分,拆分后就会出现两个或两个以上的数据库的情况,此时不能依靠本地事务得以解决,需要借助于分布式事务来保证一致性,常说的就是保持强一致性的两阶段提交协议和三阶段提交协议。
准备阶段:由协调者提议并收集其他节点参与者的反馈(提议的节点为协调者,参与决议的节点为参与者)。
执行阶段:根据反馈决定提交或中止事务。
如图,协调者发起一个提议分别询问各参与者是否接受场景,然后协调者根据参与者的反馈,提交或中止事务。
注意:只有当所有参与者都同意才提交,否则只有有一个不同意就中止。
但是,2PC有其固有的三大问题:
问题一:同步阻塞问题
执行过程中,所有参与者都是事务阻塞型的。当参与者占用公共资源时,其他第三方节点访问公共资源就不得不处于阻塞状态。
问题二:单点故障
一旦协调者发生故障,参与者会一直阻塞下去。特别是在第二阶段,协调者发生故障,则所有参与者还处于锁定资源的状态中,但是无法完成后续的事务操作。
问题三:数据不一致
当协调者向参与者发送提交请求后发生了局部网络异常,或者在发送提交请求过程中协调者发生了故障,就会导致只有一部分参与者接收到了提交请求,这部分参与者接到请求后就会执行提交操作,而未接收到提交请求的机器就无法执行事务提交,于是就出现了数据不一致的问题。
与2PC相比,3PC主要有两个改动点:在协调者和参与者之间都引入了超时机制+把准备阶段一分为二
3PC:CanCommit + PreCommit + DoCommit,具体操作如下
CanCommit阶段:协调者向参与者发送提交请求,参与者如果可以提交就返回Yes响应,否则返回No响应。
PreCommit阶段:协调者根据参与者的响应情况来决定是否可以进行事务的PreCommit操作。
如果协调者从所有参与者获得反馈都是Yes响应,那么就执行事务的预执行;
如果有任何一个参与者向协调者发送了No响应,或者等待超时后没有收到参与者的响应,那么就执行事务的中断。
DoCommit阶段:执行提交或中断事务。当协调者没有收到参与者发送的ACK响应,就会执行中断事务。
可见,3PC主要解决了2PC的单点问题和同步阻塞问题。
在微服务架构中,传统分布式事务并不是实现数据一致性的最佳选择,主要有三大问题:
问题一:对于微服务架构来说,数据访问变得更加复杂,为达到微服务间的松耦合和高度独立,数据是微服务私有的,唯一可访问的方式就是通过API,采用2PC/3PC难度太大。
问题二:不同的微服务经常使用不同的数据库,但是在微服务架构中,服务会产生不同类型的数据,关系数据库不一定是最佳选择,很多微服务会采用SQL和NoSQL结合模式,如搜索引擎、图数据库等NoSQL数据库大多数都不支持2PC/3PC。
问题三:当数据被拆分了或者在不同的数据库存在重复数据的时候,锁定资源和序列化数据来保证一致性就会变成一个非常昂贵的操作,会给系统的吞吐量以及扩展性带来巨大的挑战。
对于微服务架构,建议采用一种更为松散的方式来维护一致性,也就是所谓的最终一致性,对于开发者而言,实现最终一致性的方案可以根据不同的业务场景做不同的选择。
指的是在一个分布式系统中,无法同时实现一致性Consistency、可用性Availability和分区容错性PartitionTolerance。
对于典型分布式系统而言,如图所以,这三个概念可以做以下解释:
指分布式系统中的所有数据备份在同一时刻是否拥有同样的值。
指在集群中一部分节点故障后,集群整体是否还能正常响应请求。
相当于通信的时限要求,系统如果不能在一定时限内达到数据一致性,就意味着发生了分区情况,也就是说整个分布式系统就不在互联了。
由于当前网络硬件肯定会出现延迟丢包等通信异常问题,三态性不可避免,所以分区容错性必须实现。
BASE=BasicallyAvailiable(基本可用)+SoftState(软状态)+EventualConsistency(最终一致性)
BASE理论是对CAP理论的延伸,基本思想是即使无法做到强一致性,应用可以采用适合的方式达到最终一致性。
指分布式系统在出现故障的时候,可以损失部分可用性,需要保证和核心可用。服务限流和降级就是其基本表现。
指允许系统存在中间状态,而中间状态不会影响整体可用状态。分布式存储中一般一份数据都有有若干个副本,允许不同节点间副本同步的延时就是软状态的体现。
指系统中所有的数据副本经过一定时间后,最终能够达到一致的状态。
CAP的一致性就是强一致性,这种一致性级别是最符合用户直觉的,用户体验好,但实现起来对系统性能影响较大弱一致性正好相反。BASE中的最终一致性可以看做是弱一致性的一种特殊情况。
CAP理论和BASE思想实际上是有关联的,基本关系如图所示:
CAP理论和BASE思想讨论的意义在于即使无法做到强一致性,我们也可以采用合适的方式达到最终一致性,这点对微服务架构很重要。最终一致性的方法重要有以下六种分别讲解。
在微服务架构中,比如,对于一个完整的下单操作而言,订单服务和支付服务都是业务闭环中的一部分,在一个完整的业务操作流程中需要保证各自数据的正确性和一致性。
尝试将订单和支付两个微服务进行分别管理,并需要一个媒介用于这两个微服务之间进行数据传递,一般而言,消息中间件MOM适合扮演数据传递媒介的角色。引入消息中间件后下单操作流程可以拆分为如下三个步骤:
当用户使用订单服务下单时,一方面订单服务需要对所产生的订单数据进行持久化操作,另一方面,也需要同时发送一条创建订单的消息到消息中间件。
当消息中间件接收到订单创建消息,就会把消息发送到支付服务。
支付服务接收到订单创建消息后,同样对该消息进行业务处理并持久化。当所有关于支付相关的业务逻辑执行完成以后,支付服务需要向消息中间件发送一条支付成功的消息。
支付成功消息通过消息中间件传递到订单服务时,订单服务根据支付的结果处理后续业务流程,一般涉及订单状态更新、向用户发送通知内容等内容。
对于以上三个闭环管理,仔细分析整个过程,不难发现存在如下三个基本问题:
(1)某个服务在更新了业务实体后发布消息失败;
(2)服务发布事件成功,但是消息中间件未能正确推送事件到订阅的服务;
(3)接受事件的服务重复消费事件。
针对第三个问题,是最为容易解决的,一般处理方法是由业务代码控制幂等性,例如支付服务传入一个订单时,可以通过判断该订单所对应的唯一ID是否已经处理方式来避免对其再次处理。
但是前两个问题概括起来就是要解决消息传递的可靠性问题,这个是可靠性事件模式实现数据最终一致性的关键点。要做到可靠消息传递,需要消息中间件确保至少投递一次消息,目前主流的消息中间件都支持消息持久化和至少一次投递的功能。所以,我们需要考虑的是,如何原子性的完成业务操作和发布消息。
订单服务同时需要处理数据持久化和消息发送两个操作,这就需要考虑两个场景:
(1)如果数据持久化操作失败,显然消息不该被发送;
(2)如果数据持久化操作成功,但消息发送失败,那么已经被持久化的数据需要被回滚以还原到初始状态。
对应这两个场景基本实现流程图如下:
在逻辑上是可行的,但在运行中,需要考虑很多意想不到的场景,主要有以下两个实际问题:
(1)实际问题一:典型的依旧是分布式环境下所固有的网络通信异常问题,消息中间件返回通信发生故障,如下图分析:
(2)实际问题二:订单服务投递消息后等待消息返回,当消息返回时,订单服务挂了,也会导致数据不一致,如下图分析:
解决上面的问题可以使用一个本地事件表。
微服务在进行业务操作时需要将业务数据和事件保存在同一个本地事务中,由本地事务保证更新业务和发布事件的原子性。
发布的事件被保存在本地事务表中,然后该服务实时发布一个事件通知关联的业务服务,如果事件发布成功则立即删除本地事件表中的事件。
由于事件消息发布可能会失败或无法获取返回结果,我们需要使用一个额外的“事件恢复”服务来恢复事件,该事件恢复服务定时从事件表中恢复未发布成功的事件并重新发布,只有重新发布才删除保存在本地事件表中的事件。
注意,事件恢复服务保证了事件一定会被发布,从而确保数据的最终一致性。
在实现上,首先考虑在可靠事件模式中存在一个事件生产者。该事件生产者处于操作的主导地位, 并根据业务操作通过事件的方式发送业务操作的结果(在上例中,订单服务就是事件的生产者),其次,事件消费者是被动方,负责根据事件来处理自身业务逻辑(上例中的支付服务属于事件消费者)。
有了事件生产者和事件消费者后,我们关注事件服务,事件服务的主要作用就是管理本地事件表,它能存储、确认并发送事件,同时根据不同状态查询事件信息并确定事件已被事件消费者成功消费。事件服务有三大组件:
表现为一种定时机制,用于处理事件没有被成功发送的场景。
例如,订单服务在完成业务操作之后需要发送事件到本地事件表,如果这个过程中事件没有发送成功,我们就需要对这些事件重新发送,这个过程为事件确认。
同样表现为一种定时机制,根据本地事件表中的事件状态,专门处理状态为已确认但还没有成功消费且已超时的事件。
基本的事件恢复策略就是向消费者重新发送事件,并在消费成功后更新事件状态,并在本地事件表中进行逻辑删除。
基于特定的消息中间件工具和框架将事件作为消息进行发送的组件。
目前可供选择的主流消息中间件包括RabbitMQ、ActiveMQ、RocketMQ、Kafka等。
基本思路在于使用一个额外的补偿服务来协调各个需要保证一致性的微服务,补偿服务按顺序依次调用各个微服务,如果某个微服务调用失败就撤销之前所有已经完成的微服务,补偿服务对需要保证一致性的微服务提供补偿操作。
举例当中涉及两个微服务,订单微服务和支付微服务,为其提供补偿操作,如果支付服务失败,就需要取消之前的下单服务。
为降低开发的复杂性和提高效率,补偿服务通常实现为一个通用的补偿框架,补偿框架提供服务编排和自动完成补偿的能力。
对于补偿服务而言,所有服务的操作记录是一个关键点,操作记录是执行取消操作的前提。
举例中,订单服务与支付服务需要保存详细的操作记录和日志,这些日志和记录有助于确定失败的步骤和状态,进而明确需要补偿的范围,然后获取所需补偿的业务数据。
如果只是订单服务失败,那么只需要补偿一个服务就可以,如果支付服务也失败了,对两个服务进行回滚。
补偿操作要求业务数据包括支付时的业务流水号、账号和金额。理论上可以根据唯一的业务流水号就能完成补偿操作,但提供更多的数据有益于微服务健壮性。
实现补偿模式的关键要素就是记录完整的业务流水,可以通过业务流水为补偿操作提供需要的业务数据。
补偿服务可以从业务流水的状态中知道补偿的范围,补偿过程中需要的业务数据同样可以从记录的业务流水中获取。
补偿服务作为一个服务调用过程同样存在调用不成功的情况,需要通过一定的健壮性机制来保证补偿的成功率,补偿的相关操作本身需要具有幂等性。
补偿服务健壮性策略:需要根据服务执行失败的原因来选择不同的重试策略,如图所示:
(1)服务重启:如果失败的原因不是暂时的,而是由业务因素导致的业务错误,需要对问题进行修正后重新执行。
(2)立即重试:对于网络失败或数据库锁等瞬时异常,重试在很大程度上能够确保任务正常执行。
(3)定时调用:一般会指定调用的次数上限,如果调用次数达到上限也就不再进行重试。
如果通过服务重启、立即重试、定时调用等策略依旧不能解决问题,则需要通知相关人员进行处理,即人工干预模式。
长时间持续的事务无法简单地通过一些典型的ACID模型以及使用多段提交配合持有锁的方式来实现。Sagas用于解决这个问题,和多段式分布式事务处理不同,Sagas会将工作分成单独的事务,包含正常额操作和回滚的操作。
对于开发者而言,不是将所有微服务都定义为分布式的ACID事务,以下单行为为例,将其定义为一个整体,其中包含如何去生成订单以及如何去取消订单,对于支付而言也需要提供同样的逻辑。
可以将订单和支付服务组合在一起构成一个服务链,然后将整个服务链加密,这样只有该服务链的接收者才能够操控这个服务链。
当一个服务完成后,会将完成的信息记录到一个集合中,之后可以通过这个集合访问到对应的服务。
当一个服务失败时,服务本身将本地清理完毕并将消息发送给该集合,从而路由到之前执行成功的服务,然后回滚所有的事务。
在Sagas事务模型中,一个长事务是由一个预定义好执行顺序的子事务集合和他们对应的补偿子事务集合所组成。
典型的一个完整的交易由T1、T2、......、Tn等多个业务活动组成,每个业务活动可以是本地操作或者是远程操作,而每个业务活动都有对应的取消活动C1、C2、......、Cn。
所有的业务活动在Sagas事务下要么全部成功,要不全部回滚,不存在中间状态。对于一个Sagas链路而言,各个业务活动执行过程中都会依赖上下文,每个业务活动都是一个原子操作,并提供执行和取消两个入口。
需要设计一个存储模型来保存执行上下文并通过该存储模型来索引到对应的服务。
存储模型中包含两个内部结构,一个是完成的任务,一个是等待执行的任务。如果成功就会将任务向前执行,如果失败就向后执行。
实现上的一种思路可以采用队列和栈数据结构,一方面使用队列来向前执行,另一方面使用栈来向后执行。
注意,当执行取消操作进行事务操作失败时需要记录失败事务日志,通过重试策略进行重试,对重试失败的执行定时重试,在有问题时则进行人工干预。
一个完整的TCC业务由一个主服务和若干个从服务组成,主服务发起并完成整个业务流程。
从服务提供三个接口:Try、Confirm、Cancel:
Try接口:完成所有业务规则检查,预留业务资源。
Confirm接口:真正执行业务,其自身不做任何业务检查,只使用Try阶段预留的业务资源,同时该操作需要满足幂等性。
Cancel接口:释放Try阶段预留的业务资源,同样也需要满足幂等性。
举例来看,订单系统拆分成订单下单和订单支付两个场景,使用TCC模式后执行效果如下:
(1)Try阶段:尝试执行业务。
一方面完成所有业务检查,如针对该次订单下单操作,需要验证商品的可用性以及用户账户金额是否够。
另一方面需要预留业务资源,如把用户账户余额进行冻结用于支付该订单,确保不会出现其他并发进程扣减账户余额导致后续支付无法进行。
(2)Confirm阶段:执行业务。
Try阶段一切正常,则执行下单操作并扣除用户账号中的支付金额。
(3)Cancel阶段:取消执行业务。
释放Try阶段预留的业务资源,如果Try阶段部分成功,如商品可用且正常下单,但账户余额不够而冻结失败,则需要对产品下单做取消操作,释放被占用的该商品。
TCC服务框架不需要记录详细的业务流水,完成Confirm和Cancel操作的业务由业务服务提供。
TCC模式同样有两个阶段组成
第一阶段:
主业务服务分别调用所有从业务的Try操作,并在活动管理器中登记所有从业务服务。当所有从业务服务的Try操作都调用成功或者某个从业务的Try操作失败,进入第二个阶段。
第二阶段:
主业务根据第一阶段的执行结果来执行Comfirm或Cancel操作。
如果第一阶段所有Try操作都成功,则主业务服务调用所有从业务活动的Confirm操作。
如果第一阶段中失败,则调用Cancel操作。
整体上,一个完整的TCC事务参与方包括三个部分:
(1)主业务服务
整个业务的发起方,在订单处理场景中,订单应用系统即是主业务服务。
(2)从业务服务
负责提供TCC业务操作,是整个业务活动的操作方。
从业务必须实现Try、Confirm和Cancel三个接口,供主业务服务调用。Confirm和Cancel接口需要具备幂等性,订单的下单服务与支付服务即是从业务服务。
(3)业务活动管理
管理控制整个业务活动,包括记录维护TCC全局事务状态和每个从业务服务的子事务状态,并在业务活动提交时确认所有从业务服务Confirm操作,在业务活动取消时调用所有从业务服务的Cancel操作。
在实现TCC模式上,最重要的工作是设计一个稳定的、高可用的、扩展性强的TCC事务管理器。
在一个跨服务的业务操作中,首先通过Try锁住服务中业务资源进行资源预留,只有资源预留成功了,后续的操作才能正常进行。Confirm操作是在Try之后进行的对Try阶段锁定的资源进行业务操作,Cancel在所有操作失败时用于回滚。
TCC的操作都需要业务方提供对应的功能,在开发成本上比较高,推介TCC框架有:
(1)http://github.com/protera/spring-cloud-rest-tcc;
(2)http://github.com/changmingxie/tcc-transaction;
(3)http://github.com/liuyangming/ByteTCC;
(4)http://github.com/QNJR-GROUUP/EasyTransaction;
(5)http://github.com/yu199195/happylifeplat-tcc;
(6)https://www.atomikos.om/Blog/TCCForTrasationMangementAcrossMicroservices。
本质上是一种通知类的实现方案。
基本思路是通知发送方在完成业务处理后向通知接收方发送通知消息。
当消息接收方没有成功消费消息时,消息的发送方还需要进行重复发送直到消费者消费成功或达到某种发送总之条件。
消息发送方可以设置复杂的通知规则,利于采用阶梯式事件通知方式。
通知接收方也可以使用发送方所提供的查询和对账接口获取数据,用于恢复通知失败所导致的业务数据。
以支付宝为例,通知回调商户提供的回调接口,通过多次通知、查询对账等手段完成交易业务平台间的商户通知。
实现上比较简单,基本系统结构中的通知服务包括三大组件:
(1)查询组件
通发送方处理业务并把业务记录保存起来,查询组件提供查询入口供通知接收方主动查询业务数据,避免数据丢失。
(2)确认组件
当通知接收方成功接收到通知时,需要与通知发送方确认通知已被正常接收。确认组件接收到确认消息之后就会更新业务记录中的状态,通知组件根据状态就不需要再发送通知。
(3)通知组件
通知组件根据业务记录向通知接收方发送通知,在发送通知的过程中需要保存通知记录,并根据业务记录的状态以及现有的通知记录确定下一次发送通知的策略。
注:最大努力通知模式适合于业务最终一致性的时间敏感度比较低的场景,一般用于类似支付宝与商户集成类跨企业的业务活动。
严格意义上并不是一种数据一致性的实现机制,当前面讲的各种模式都无法满足需要时,人工干预模式更多的是一种替代方案。
对于一些重要的业务场景下,由于前面几种模式中因为网络三态性无法解决问题的情况,需要人工干预来保证真正的一致性。常用手段为定期对账等。
实施前提是需要一个后台管理系统,提供操作不一致的基本入口。
周期性的对账机制需求,对账机制基于业务数据,业务双方根据各自系统内所产生的订单或支付记录,相互对比发现数据不一致的情况,然后通过线下付款等形式形成一致的操作结果。
对于金融、支付等业务体系,数据一致性要求极高,需要保证严格的实时一致性要求。
对于基于社交类的应用场景,可以采用局部实时一致、最终全局一致的实现思路。
微服务架构里面建议“兜底”思维,即不管实现方案是否完美,最后都要有一个备选方案,备选方案不一定满足日常业务场景,但当出现异常情况时,可通过备选完成正常业务的闭环。
【1】郑天民. 微服务设计原理与架构. 北京:人民邮电出版社,2018.05
【2】尹吉欢. Spring Cloud微服务全栈技术与案例分析. 北京:机械工业出版社,2018.08