一文带你解读微服务架构下软件开发中的最大难点——事务管理

事务管理

◎ 事务概述

◎ CAP理论

◎ BASE理论

◎ 解决方案

◎ 对账是最后的屏障

事务管理一直都是软件开发中的难点,即使很多优秀的框架能够帮助我们处理一些简单的逻辑,如在单体式架构中使用AOP的事务管理框架来管理事务,但在微服务架构下,或者任何分布式架构的系统中,事务管理的难度增大了。

而且事务管理本身除了技术上的难点,更复杂的还是事务在业务上的逻辑,现实世界中的一些场景在微服务架构下,它的业务可能会被拆分成多个服务来共同处理。那么,面对分布式的业务,分布式事务又该如何处理呢?

事务概述

在介绍分布式事务之前,有必要说一下事务的概念,以确保对这个问题的概念理解一致。那么,什么是事务?

事务(Transaction)的原意是指交易,在商业中通常是指买方和卖方为交换资产在进行支付时的金融交易。在计算机领域中提到事务,是指一系列信息交换相关操作的不可分割性,这里信息交换是对数据的相关操作,如添加、更新或删除数据库的信息,这里事务的重点在于不可分割,每个事务都是一个完整的单元。

例如, 上购买一部手机,假设这时商家的库存有200部,下单成功后,库存的数据应该更新为199部,这个下单和减库存就应该是一个单元,不能下单成功而库存没减,或者下单失败但库存减了,只要有一个操作没有成功,另一个操作也要跟着失败,下单和减库存这两个操作只能都成功或都失败,这样的一系列操作就是事务操作,如图11.1所示。

当我们谈论事务时,更多的会涉及数据库事务,之前提到过事务中通常会有数据交换,而数据交换通常发生在数据库中,如数据库的批量删除,或者同时删除一笔数据和更新另一笔数据。这样的场景在平时的工作和学习中并不陌生,在数据库事务中也对事务有着明确的定性,那就是ACID属性,具体如下。

原子性(Atomicity):一个事务中的所有操作,或全部完成,或全部不完成,不会在中间的新环节结束。在数据库中,事务在执行过程中发生错误,则数据会被回滚到事务开始前的状态。数据就像没有变化一样,即事务是不可分割的。

一致性(Consistency):比较官方的解释是在事务开始之前和事务结束以后,数据库的完整性没有被破坏,写入的数据必须完全符合所有的预设约束、触发器、级联回滚等。这和我们通常提到的分布式事务的一致性并不一样,数据库的一致性指的是内部一致性,主要是指数据丢失修改、不可重复读、脏读和幻读等问题。

隔离性(Isolation):数据库允许多个并发事务的同时有对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read Uncommitted)、读提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。

持久性(Durability):指事务处理结束后对数据的修改永久生效,即便系统故障也不会丢失。

CAP理论

在了解事务的基本概念后,事务管理通常就是要保证事务的ACID,但这在分布式系统中是很难完成的事情。由于分布式系统中通常存在多个数据库,因此无法运用数据本身的ACID特性,而针对分布式系统的特点,加州大学伯克利分校的计算机科学家Eric Brewer总结出了新的特性,也就是CAP定理,具体如下。

一致性(Consistency):每次读取都会收到最近的写入或错误,等同于所有节点访问同一份最新的数据副本,获取到的数据都是最新的数据。

可用性(Availability):每个请求都会收到(非错误)响应,但不保证它包含最新的写入,即保证数据的正确性,但是不保证获取的数据为最新数据。

分区容错性(Partition tolerance):无论节点之间的 络丢失(或延迟)多少数量的消息,系统仍继续运行。

CAP原则如图11.2所示。

图11.2 CAP原则

Eric Brewer指出,在分布式系统中只可能满足CAP中的两项,不能三项都满足,这又是为什么?

我们先来分析一下分区容错性(P),在分布式系统中,分区一定存在,因为我们的服务或数据都是分布式存在的,一般来说,分区容错无法避免,所以可以理解为CAP中的P是百分之百存在的。

再来看看一致性(C)和可用性(A),这里的一致性与数据库事务的ACID中的一致性不一样,CAP中的一致性通常是指外部的一致性。也就是说,在写操作之后的读操作必须读到最新状态的值。可用性是指无论数据是什么状态,每个读操作都会读到一个值,无论这个值是否最新。所以,要想保证一致性通常会牺牲一部分可用性。

因为在现实中可能存在 络错误,即分区错误一定存在,所以无法保证每个节点的数据都一致。如果要保证一致性,当一个分区发生写操作时,锁住其他分区的读写操作,数据同步后才重新开放,这就会造成其他分区出现不可用的情况。如果要保证可用性,就不能锁住其他分区,这必然会出现数据不一致的情况,也不能满足一致性。

综上所述,在做分布式事务管理时只能选择一个目标,要么追求一致性,要么追求可用性。所以,我们在谈论一些分布式事务的框架或相关技术时都会提到是基于AP原则的实现还是基于CP原则的实现。

BASE理论

我们了解到分布式事务的CAP原则,即对于分布式系统而言,只能从CAP中选择两个条件,由于P的确定性,当面临CP原则或AP原则的选择时,在大部分2C软件中都会选择CP原则,牺牲一致性而保障系统的可用性,而在一些金融系统中,为了保证财产的安全往往选择AP原则,牺牲可用性而保障数据的一致性。

当然,这里所说的牺牲并不是完全放弃某一个特性,更多的是牺牲强特性而换取弱特性,如AP原则,就是牺牲了强一致性而换取了强可用性和弱一致性;而CP原则就是牺牲了强可用性而换取强一致性和弱可用性。那么,如何理解强和弱呢?

BASE理论很好地解释了强和弱的概念,它就是对于CAP原则的衍生,最初由eBay的架构师Dan Pritchett提出,是基于eBay在分布式系统中实践总结的关于一致性和可用性权衡的结果,BASE是以下3个概念的缩写。

基本可用(Basically Available):系统在出现故障时,允许损失部分可用性,保证核心功能可用,通常又可以细化分为两种,即损失时间上的可用性和损失功能上的可用性。

软状态(Soft State):允许系统中存在中间状态,这个状态不影响系统可用性,即允许不同分区副本的数据出现一定时间内的不一致。

最终一致性(Eventually Consistent):指经过一段时间后,所有节点数据都将达到一致,即系统不可能一直存在软状态,在一定时间内会保证数据最终的一致性。

BASE被认为是ACID的替代品,与ACID的思路截然相反。ACID悲观地控制每个数据操作,保证数据在操作结束时的一致性,而BASE则会乐观地允许数据出现暂时的不一致,保证数据在操作结束时的强一致性。所以,BASE理论上其实是一个基于AP原则的设计,即通过牺牲强一致性而获得系统的高可用性。

解决方案

基于这些理论和大量行业先辈的探索,虽然分布式事务管理很难,但还是总结了不少解决方案,如常见的两段提交、基于消息的事务管理,以及基于最终一致性原则的TCC模式,虽然这些方式已经运用在很多场景下,但微服务下事务管理的复杂度还居高不下,所以最好的方式还是尽量避免过度的设计而导致过多的分布式事务的存在。

基于可靠消息的事务管理

什么是消息中间件?消息中间件就是通过 络,使用队列的形式 , 采用发布和订阅的模式 对消息进行传递,如 RabbitMQ 、RocketMQ、ActiveMQ和Kafka,都是一些比较常用的消息中间件,消息发布者可以通过这些消息中间件发布想要传递的消息,而这些消息的订阅者就会收到相应的消息。

那么,基于消息中间件如何实现微服务的事务管理?有个说法称为“基于可靠消息服务的分布式事务管理”。可见,如果要使用消息中间件来管理事务,首要条件是可靠,那么如何做到可靠?消息中间件既然是用来发布和订阅消息的,可靠的条件就是发布可靠和订阅可靠两个要素,具体如下。

(1)消息一定要发布成功,即发布者的消息一定要被消息中间件接收到。

(2)消息一定要被消费成功,即订阅者一定要成功地将消息接收到。

如何做到这两点?方式有很多,对于发布可靠通常的做法是在消息发布者的本地建立一个消息状态记录表,如图11.3所示。

消息发布者在本地事务完成的同时,会在本地的消息表中插入一笔消息记录,并标记状态为未发送,插入操作与本地事务在同一个事务中,如果本地事务执行失败,就不会有消息记录产生,如果消息记录插入失败,那么本地事务也会回滚,这时就会有一个定时任务来轮询消息表中未发送的消息,并且将这些消息发往消息中间件,然后更新消息的状态。当然这个过程也可能失败,不过定时任务会反复执行,从而最大限度地保证消息发布的成功率,但反复执行也要有限度,我们可以给系统设置一个阈值,当发生消息的重试次数超过了这个阈值,就表示消息中间件不可用,需要预警进行人工修复。

对于订阅可靠就需要消息中间件的特性支持,当消息投递给订阅者时,都会有回应。例如,订阅者会返回消息投递成功的消息给消息中间件,当消息中间件接收到订阅者的成功回复后,就代表消息投递成功,否则需要继续重试投递。所以,我们在选择使用消息中间件作为事务管理机制时,就需要尽量考虑选择可以支持重试消息投递的消息中间件。

当然,消息发送失败可能是下游系统确实没有成功接收到消息,或者下游系统接收到消息,但返回响应时出现了错误导致消息中间件没有接收到消息订阅者的正常成功返回。那么,消息中间件都将进行消息重发,这就导致了一个新的问题,就是消息订阅者可能收到多份同样的消息。这里就引入了一个新的概念:幂等性。

解决分布式事务的问题时,通常都会提到幂等性,什么是幂等性?简单来说,一个服务能够反复消费同一个消息且保持业务数据状态不变,这就是幂等性。所以,要保证消息中间件的订阅可靠,消息订阅者需要做幂等处理,做法有很多种,比较常见的做法和消息发布者的处理方式类似,在消息订阅者的本地也维护一个消息消费记录表,来记录一个消息是否被正常消费。如果没有记录,那么进行正常的业务处理,并记录一笔消费,如果消费过就直接返回成功,不会再重复消费,本地消息状态记录表如图11.4所示。

那么,在保证了消息中间件的可靠性后,就可以利用消息的传递来完成事务的控制。消息中间件处理事务示意图如图11.5所示。

在图11.5中,通过消息中间件设计,上游系统处理完本地事务后,将消息发送给下游系统,下游系统再处理自己的本地事务。消息收发的可靠性设计,可以保证下游系统一定会收到消息。如果下游系统处理本地事务时失败了,上游系统此时事务已经提交,那该怎么处理?

很简单,还是利用消息中间件,如果下游系统事务处理失败,就返回失败的消息给消息中间件,消息中间件就会再次将消息重新发送给下游系统,由于下游系统已做了幂等处理,因此可以多次重试,直到本地事务处理成功为止。不过重试次数也要有限度,可以在下游系统做本地事务失败次数的记录,下游系统事务重试示意图如图11.6所示。

这样就通过可靠的消息中间件实现了微服务系统的事务管理。使用消息中间件管理事务的方式还有很多,如超时回滚等,此处给大家介绍的便是尽最大努力通知的方式,也是比较简单和普遍采用的一种方式。这种方式的优点是会尽量保证系统业务处理的成功;缺点是实时性会差一些,但最终会保证数据的正确性。

两段提交事务

两段提交(Two-Phase Commit,2PC协议)算是比较简单的方式来处理分布式事务,它的成立主要基于以下假设。

(1)系统中存在一个中心节点作为协调者,其他节点作为参与者,当然,节点之间可以进行 络通信。

(2)所有节点都采用预写式日志,且日志被写入后即被保存在可靠的存储设备上,即使节点损坏不会导致日志数据的消失。

(3)所有节点不会永久性损坏,即使损坏后仍然可以恢复。

下面来看两段提交的方式。

1. 第一段提交

第一阶段的主要工作是询问,协调者将询问所有的参与者是否可以执行提交操作,即要求所有的参与者都准备好提交。也就是说,让所有参与者执行预提交,参与者会告诉协调者是否准备好或是否完成预提交。

2. 第二段提交

如果所有的参与者在第一阶段的返回为成功时,协调者就会再次向所有参与者发送正式提交的请求,完成所有事务的最终提交,如果有任何一个参与者在第一阶段的返回是失败,协调者就会通知所有参与者进行事务回滚操作。

两段提交的模式相对比较好理解,而且很多数据库开始对两段提交进行支持 , 在 数 据 库 中这种方式被称为 XA ( eXtended Architecture)事务管理,如图11.7所示。

在图11.7中,事务管理器就是全局的协调者,资源管理器就是分区的参与者,通过两次提交而最终保证数据的一致性。可以看出,两段提交是基于CP协议的事务管理实现,这也是两段提交最大的缺点。因为在事务管理器执行期间,所有的节点都处于阻塞状态,必须等到所有节点正常响应后才可以进行提交,只要有某个节点出现高负载或响应较慢,就会连带影响其他节点进入等待状态,所以两段提交的系统可用性不好。此外,还有事务管理器的单点故障问题,一旦事务管理器本身出现问题,那么整个分布式事务管理都将无法进行。

  1. 下篇文章给大家讲解的是TCC模式事务管理;
  2. 觉得文章不错的朋友可以转发此文关注小编;
  3. 感谢大家的支持!

声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2021年6月28日
下一篇 2021年6月28日

相关推荐