最终一致分布式事务方案解析

Posted 不能说的秘密go

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了最终一致分布式事务方案解析相关的知识,希望对你有一定的参考价值。

业来主流的分布式事务的解决方案主要归位两大类:强一致性分布式事务和最终一致性分布式事务,本文不对强一致性分布式事务做过多描述,主要针对最终一致性方案解析。

根据笔者的工作经验来看,最终一致性方案适用用大部分互联网场景主要原因如下:

  • 由于微服务间无法直接进行数据访问,微服务间互相调用通常通过RPC(Dubbo)或Http API(Spring Cloud)进行,所以已经无法使用TM统一管理微服务的RM。
  • 不同的微服务使用的数据源类型可能完全不同,如果微服务使用了NoSQL之类不支持事务的数据库,则强一致性事务根本无从谈起。
  • 即使微服务使用的数据源都支持事务,那么如果使用一个大事务将许多微服务的事务管理起来,这个大事务维持的时间,将比本地事务长几个数量级。如此长时间的事务及跨服务的事务,将为产生很多锁及数据不可用,严重影响系统性能。

既然无法满足传统的ACID事务,在微服务下的事务管理必然要遵循新的法则--BASE理论。

BASE理论由eBay的架构师Dan Pritchett提出,BASE理论是对CAP理论的延伸,核心思想是即使无法做到强一致性,应用应该可以采用合适的方式达到最终一致性。BASE是指基本可用(Basically Available)、软状态( Soft State)、最终一致性( Eventual Consistency)。

  • 基本可用:指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。
  • 软状态:允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有三个副本,允许不同节点间副本同步的延时就是软状态的体现。
  • 最终一致性:最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。

BASE中的最终一致性是对于微服务下的事务管理的根本要求,即虽然基于微服务的事务管理无法达到强一致性,但必须保证最终一致性,这就是所说的柔性事务。

实现事务最终一致性的方案主要有事件通知模式、事务补偿模式两种。

事件通知模式
事件通知模式的设计理念比较容易理解,即是主服务完成后将结果通过事件(常常是消息队列)传递给从服务,从服务在接受到消息后进行消费,完成业务,从而达到主服务与从服务间的消息一致性。


设计理念很简单,如果不考虑任何意外情况,上述的逻辑很完美,但是架构的设计主要是为了考虑异常情况。
考虑如下异常情况:

  • 在微服务的架构下,有可能出现网络IO问题或者服务器宕机的问题,如果这些问题出现在时序图的第7步,使得消息投递后无法正常通知主服务(网络问题),或无法继续提交事务(宕机),那么主服务将会认为消息投递失败,会滚主服务业务,然而实际上消息已经被从服务消费,那么就会造成主服务和从服务的数据不一致
  • 事件服务(在这里就是消息服务)与业务过于耦合,如果消息服务不可用,会导致业务不可用。应该将事件服务与业务解耦,独立出来异步执行,或者在业务执行后先尝试发送一次消息,如果消息发送失败,则降级为异步发送。

事件通知模式之本地异步事件服务模式
为了解决上述同步事件中描述的同步事件的问题,异步事件通知模式被发展了出来,既业务服务和事件服务解耦,事件异步进行,由单独的事件服务保证事件的可靠投递。

当业务执行时,在同一个本地事务中将事件写入本地事件表,同时投递该事件,如果事件投递成功,则将该事件从事件表中删除。如果投递失败,则由定时任务异步统一地处理投递失败的事件,进行重新投递,直到事件被正确投递,并将事件从事件表中删除。这个在本服务内部的定时任务一般叫它本地事件服务。这种方式最大可能地保证了事件投递的实效性,并且当第一次投递失败后,也能使用本地事件服务保证事件至少被投递一次。

这个方案原则的比较稳定,缺陷就是业务仍旧与事件服务有一定耦合(第一次同步投递时),更为严重的是,本地事务需要负责额外的事件表的操作,为数据库带来了压力,在高并发的场景,由于每一个业务操作就要产生相应的事件表操作,几乎将数据库的可用吞吐量砍了一半。正是因为这样的原因,本地异步事件服务模式进一步地发展,将事件服务独立出主业务服务,主业务服务不在对事件服务有任何强依赖。

  • 业务服务在提交前,向事件服务发送事件,事件服务只记录事件,并不发送。业务服务在提交或回滚后通知事件服务,事件服务发送事件或者删除事件。不用担心业务系统在提交或者会滚后宕机而无法发送确认事件给事件服务,因为事件服务会定时获取所有仍未发送的事件并且向业务系统查询,根据业务系统的返回来决定发送或者删除该事件。
  • 外部事件虽然能够将业务系统和事件系统解耦,但是也带来了额外的工作量:外部事件服务比起本地事件服务来说多了两次网络通信开销(提交前、提交/回滚后),同时也需要业务系统提供单独的查询接口给事件系统用来判断未发送事件的状态。

事件通知模式之基于MQ事件服务模式
上述说的基于外部事件服务来解耦,如果不想自己开发(当然自己开发也是开始的)。可以考虑使用MQ消息队列。假如使用的MQ本身支持事务消息,这样业务应用就能以某种方式确保消息正确投递到MQ。

消息事务的一种实现思路是通过保证多条消息的同时可见性来保证事务一致性。但是此类消息事务实现机制更多的是用在 consume-transform-produce(Kafka支持)场景中,其本质上还是用来保证消息自身事务,并没有把外部事务包含进来。

还有一种思路是依赖于 AMQP 协议(RabbitMQ支持)来确保消息发送成功。AMQP需要在发送事务消息时进行两阶段提交,首先进行 tx_select 开启事务,然后再进行消息发送,最后执行 tx_commit 或tx_rollback。这个过程可以保证在消息发送成功的同时,本地事务也一定成功执行。但事务粒度不好控制,而且会导致性能急剧下降,同时也无法解决本地事务执行与消息发送的原子性问题。

不过,RocketMQ事务消息设计解决了上述的本地事务执行与消息发送的原子性问题。在RocketMQ的设计中,broker和producer的双向通信能力使得broker天生可以作为一个事务协调者存在。而RocketMQ本身提供的存储机制,则为事务消息提供了持久化能力。RocketMQ 的高可用机制以及可靠消息设计,则为事务消息在系统在发生异常时,依然能够保证事务的最终一致性达成。

以rocketmq为例,事务消息的设计流程同样借鉴了两阶段提交理论,流程图如下:

  • 事务发起方首先发送 prepare 消息到 MQ。
  • 在发送 prepare 消息成功后执行本地事务。
  • 根据本地事务执行结果返回 commit 或者是 rollback。
  • 如果消息是 rollback,MQ 将删除该 prepare 消息不进行下发,如果是 commit 消息,MQ 将会把这个消息发送给 consumer 端。
  • 如果执行本地事务过程中,执行端挂掉,或者超时,MQ 将会不停的询问其同组的其他 producer 来获取状态。
  • Consumer 端的消费成功机制有 MQ 保证。

如果事务消息发送到MQ上后,会回调本地事务执行器;但是此时事务消息是prepare状态,对消费者还不可见,需要本地事务执行器返回RMQ一个确认消息。只有当确认完之后,才会将消息的状态由消费端不可见的prepare状态更新为消费者端可见的commied状态,如果本地事务执行器返回的是rollbak,则RMQ直接删除该prepare状态的消息。

为了处理由于异常情况导致RMQ收不到本地事务执行器的确认消息的问题,RMQ会通过服务器回查客户端Listener接口反查prepare状态事务消息最终应该的状态,从而将消息由消费端不可见的prepare状态更新为消费者端可见的commied状态或直接删除prepare状态的消息。

其实业界各种MQ均有各自的适用场景,很多通用性MQ关注点是消息的高效流转,如果加上事务消息这一特性会导致MQ的性能打一些折扣。而解决消息的可靠投递并不一定需要使用事务消息方案,采用上面介绍的两种方法也可以。一句话,技术上并不是一定要追求架构的最优,还是要考虑综合效能。

事件最大努力通知

上面说到的3种模式,均可以保证事件消息可靠地投递到下游服务那儿去。但有些场景是允许一定程度地丢消息的。于是就发展出事件最大努力通知模式。最大努力通知型的特点是,业务服务在提交事务后,进行有限次数(设置最大次数限制)的消息发送,比如发送三次消息,若三次消息发送都失败,则不予继续发送。所以有可能导致消息的丢失。同时,主业务方需要提供查询接口给从业务服务,用来恢复丢失消息。最大努力通知型对于时效性保证比较差(既可能会出现较长时间的软状态),所以对于数据一致性的时效性要求比较高的系统无法使用。这种模式通常使用在不同业务平台服务或者对于第三方业务服务的通知,如银行通知、商户通知等。

最后来个图整理本文设计的最终一致分布式事务方案的几种类型

以上是关于最终一致分布式事务方案解析的主要内容,如果未能解决你的问题,请参考以下文章

MQ关于实现最终一致性分布式事务原理解析

分布式事务解决方案-柔性事务(可靠消息保证最终一致性)

分布式事务--消息补偿的最终一致

分布式事务常见解决方案与最终一致性

分布式事务之可靠消息设计要点汇总

最后的分布式事务 有用