随着业务的发展,微服务架构逐渐成为当下业务中台的主流架构形式,它不但解决了各个应用之间的解耦问题,同时也解决了单体应用的性能问题实现可扩展可动态伸缩的能力。如下图所示,业务中台就是将平台的通用能力进行下沉,避免重复建设,形成底座平台能力,上层的各个应用服务都是基于中台能力进行快速构建。但是随着应用规模的扩大,原本在单体应用中不是问题的问题,在微服务架构中可能就是比较严重的问题,本文所要探讨的服务之间的数据一致性便是其中最具代表性的问题。本文将结合常见的电商下单场景来说明业务中台数据一致性方案。
在探讨业务中台数据一致性方案之前,我们先来一起回顾下数据库事务的相关内容,通过对数据库事务的分析,我们可以看出来在微服务架构中想要保证数据的一致性将会遇到什么样的问题。
1、本地事务
事务的概念对于程序猿来说一定不陌生,这里的事务指的是数据库事务。所谓数据库事务,简单来理解就是一套关于数据一致性维护的数据库机制。 我们都知道,实际业务平台大部分的业务数据还是保存在关系型数据库中,在单体应用的时代,数据库实例本身可以保证事务的有效性。
数据库事务需要满足四个基本特征:
(1)原子性(Atomicity):极端主义者,要么大家一起成功,有一个失败都不行
(2)一致性(Consistency): 数据具有一致性,不存在状态不确定的状况
(3)隔离性(Isolation):事务之间互相不干扰,你走你的阳关道,我走我的独木桥
(4)永久性(Durability):一旦事务提交后,数据就记录就会被持久化
都说王守义 13 香,笔者最近也下单了一部 pro 准备换掉三年前的 iphone。那么我们以下单购买 iphone13 进行举例说明,我们暂时将如下图所示,如果在一个完整事务中,存在生成订单、扣减库存、增加积分以及发放优惠券这四项业务,那么要么这四项都成功,下单够购买 13 香这个业务才算是成功,中间有一项失败就会造成业务数据的不一致,因此需要进行事务回滚,回滚到下单前的状态,以保证业务数据的一致性。
2、分布式事务
随着业务的不断发展,业务复杂度也在不断的增长,企业基于微服务架构向下沉淀出了通用的业务中台,数据的访问形式变得复杂了,服务节点间的数据访问通过 API 接口进行。原本单数据库实例只能保证数据库实例内部的事务,但是在跨数据库实例以及分布式业务调用过程中,单数据库实例已经无法保证全局事务的有效性。因此我们需要分布式的事务机制来保证各个服务节点之间的数据逻辑一致,否则就会出现如下的数据不一致的问题。
针对分布式场景下的数据一致性问题,业界提出了 CAP 理论以及 BASE 理论,同时在这些理论的基础之上产生了相应的分布式事务解决方案。我们先来看下什么是 CAP 理论以及 BASE 理论。
CAP 理论
Consistency:数据一致性
Avalibility:数据可用性
Partition tolerance:分区容错性
任何一个分布式系统是没法同时满足 CAP 中的三项的,为什么这么说呢?我们来举个简单的例子来进行说明。如下图所示,订单服务将生成的订服务写入订单数据主库,同时将数据同步到订单数据从库中,订单服务从从库中进行订单数据查询,从人实现订单数据的读写分离。那么我们继续来看,当系统满足分区容错性之后,数据一致性和数据可用性之间存在怎样的矛盾。
如果必须实现数据的一致性,那么当订单数据写入主库的时候,由于此时主库还未将最新的订单数据同步到从库当中,因此主库和从库出现了数据不一致的情况,但是一致性又要求必须实现数据的强一致,那么此时的只好将从库锁住不对外提供服务,直到主库数据同步到从库后再开放订单数据查询。因此在 这个过程中无法同时满足数据一致性以及可用性。
对应的 BASE 理论,其实就是一种 CAP 理论的实际权衡结果,既然无法做到强一致性,那么各个服务节点可以根据自身的业务特点实现数据的最终一致。所谓 BASE 理论指的就是:
a、Basically Available --- 基本可用,毕竟对于分布式系统来说,可用性比数据一致性性要重要的多
b、Soft state --- 软状态 指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程中存在延时
c、Eventually consistent --- 最终一致性,强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。最终一致性需要保证数据最终能够一致而不需要保证数据实时的一致性。
看吧,实际上我们也不需要太为难我们自己,既然很难实现强一致性,那么实现最终一致性相对来说是一个非常划算以及可行性较高的数据一致性解决方案。 有了前人大佬们总结的分布式理论,我们一起来看下几种常见的分布式事务场景吧。
有了前人大佬们总结的分布式理论,我们一起来看下几种常见的分布式事务场景吧。
(1)一个事务中包含了多数据库操作
我们还是以上面购买 13 香来举个栗子,由于业务量的不断攀升,之前的单数据库实例已经无法满足当前业务要求。因此我们将数据库按照业务域进行了拆分。我们简化下购物的业务流程,简化后包括生成订单、扣减库存以及增加积分,因此一个购物事务中包括了三个数据库的操作,但是数据库实例只能保证自身的事务特性,不能保证全局的事务特性。如果订单生成,但是库存没有扣减,积分没有增加,将数显数据不一致问题,因此出现了分布式事务问题。
(2)一个事务中包含了多服务访问同一数据库
随着业务的发展,原先单体项目的模块越来越多,维护起来成本较高,比如订单模块修改了但是库存模块没有修改,但是发布的时候还是需要发布整个应用,万一有个 Bug 啥的还要回滚,不能做到功能和维护上面的隔离。因此我们需要对应用不同的模块进行拆分,那么原本的内部调用变成了两个服务之间的远程调用,原本的本地事务就演变成了分布式事务。
(3)一个事务包含了多个微服务调用数据不一致引发的问题
在微服务架构体系下面,原有的服务中的各个业务模块经过纵向拆分后,成为一个个独立的服务,如前面的购物业务流程,整个过程涉及到多个微服务,因此数据库提供的事务机制,只能保证一个微服务节点的事务,同样不能保证全局的事务。因此当一个微服务需要调用多个其他微服务完成对应的业务时,分布式事务的问题就会出现。
正是因为分布式微服务的复杂结构,因此给维护数据一致性带来了一定的挑战,但是由于分布式理论的发展与实践,为我们解决分布式系统提供了理论依据。
分布式系统数据一致性的保证的关键点就在于如何实现和单系统一样的事务控制,在单点系统阶段,数据的一致性通过数据库本身的机制进行保证。但是在分布式中台系统中,数据一致性需要借助于外部的力量进行保证。
当下已经有较为成熟的数据一致性解决方案了,下面我们来具体分析下各个解决方案,我们按照分布式系统是否强调数据的强一致性,我们可以将分布式事务分为刚性事务以及柔性事务。
1、刚性事务
所谓的刚性事务就是追求数据的强一致性,必须满足数据库事务的 ACID 特性。典型的刚性事务解决方案就是 XA 模型。它通过引入一个事务协调者的角色,站在全局的角度来看分布式事务,将各个子域合并为一个大的分布式事务来实现数据的一致性。
但是在实际的高并发场景下基本不会使用这样的分布式解决方案,主要原因有以下几点,我们以 XA 模型中最常见的两阶段提交的方案来说明其存在的不足之处。
(1)单点故障问题: 由于引入了分布式事务的全局协调者的角色,那么如果一旦全局协调者产生故障,那么各个子事务参与者并不能获取事务执行结果状态,导致子事务阻塞,因此我们需要花费很大精力去保证事务协调者的高可用。
(2)性能问题: 在大型分布式系统高并发场景下,由于参与分布式事务的 RM 过多,因此网络通信次数、重试以及通信时间都会增加,导致可能的阻塞时间也会变长,从而降低了整个系统的吞吐状况。
2、柔性事务
既然刚性事务在高并发场景下存在上述的问题,那么有没有更好的数据一致性解决方案呢?这时候柔性分布式事务就派上用场了。柔性事务尊属分布式事务的 BASE 理论,它允许一段时间内的系统之间数据的不一致,但是在最终状态下需要保证事务的一致性。
(1)TCC 模式
所谓 TCC 模式即为 Try-Confirm-Cancel,它是二阶段提交的一种实现方式。它包含的主要操作如下所示:
Try: 尝试执行业务,但是实际并没有真正执行,只是进行数据检查,锁定业务资源,便于后续业务执行需要
Confirm: 执行具体的业务操作,使用之前阶段预留的业务资源数据
Cancel: 如果在 try 阶段某个事务执行失败,则取消之前的业务操作
Try 阶段:
这个阶段主要实现尝试执行对应的业务,可以理解为一种预备执行的状态。因为在完成业务流程之前,并不知道各个业务节点或者可以理解为子事务是否可以正常执行,因此尝试在各个子事务去预先执行,看看能不能正常处理。
回到我们这个购买 13 香的例子当中,订单中心首先将订单状态修改为 UPDATING 状态,而不是 COMPLETED 状态。库存中心可以将 13 香的库存锁住,积分中心同样可以进行预增加积分。
Confirm 阶段:
如果有幸进入这个阶段,说明前期的 try 阶段都已经处理成功了,即为订单的状态成功变更为 UPDATING 状态,库存中的订单数据量已经被锁定,用户对应的购物积分已经预先增加了,这三个步骤都已经完美实现了。对应的 TCC 框架已经感知到各个 Try 阶段的执行结果,因此在执行 Confirm 时候需要执行对应服务提供的 Confirm 接口去完成实际的数据提交。
TCC 框架需要分别调用各个服务的确认提交接口完成对应的本地事务提交。订单服务需要将订单状态修改为订单完成状态、 库存服务需要将将库存进行真实的扣减,用户积分服务为用户增加相应的用户积分。
Cancel 阶段:
如果不幸走到这个阶段,无论在哪个阶段都需要对之前执行的所有擦偶作执行回滚,恢复原有数据。如在执行到积分添加的过程中出现异常,那么代表这个分支事务在执行中出现了问题,无法完成正常的事务提交。因此为了保证数据的一致性,需要将之前的数据进行回滚操作。
在整个 TCC 处理过程之中,还有一件事情需要特别注意,那就是为了保证业务的成功率,各个业务服务向 TCC 框架进行 confirm 以及 TCC 向各个业务服务进行 confirm 以及 cancel 的时候都需要进行异常重试,以保证执行的成功率,因此对应的业务服务需要进行幂等处理,防止重试导致的重复操作。
可以看得出来,TCC 模式下的微服务需要业务代码重度耦合,实际编码的体感很不好,需要借助于外部的 TCC 框架,同时需要在业务代码中增加 Try、Cancel 处理流程需要的接口。上述的 TCC 解决方案,需要在用户执行完下单操作之后依次执行订单生成接口、库存扣减接口以及用户积分接口来完成整体的业务操作,但是在实际的业务场景中,我们大概率不会这么同步调用多个接口来完成具体业务,下面我们看看另外一种分布式数据一致性解决方案。
(2)可靠消息最终一致性
在实际的平台中,我们通常使用消息事件来解决各个微服务之间的耦合问题。我们结合之前的购买 13 香的实际案例来进行说明,可靠消息的事务模型实际上就是基于事件驱动架构,当用户在购买 13 香之后,创建了 13 香的订单并完成支付,向消息中间件发送订单已支付事件消息,订阅了订单支付支付之间消息的库存服务、积分服务等,接收到对应的订单支付消息之后,执行其对应的业务流程,如扣减库存以及增加用户积分等。
从上述描述中我们可以看出来,可靠消息最终一致性的方案中,消息的可靠投递是一切后续业务的重要前提,同时需要避免消息的重复消费,因此对应监听消息的服务的业务接口需要实现幂等性。我们来看如下的伪代码。
public void generateOrder() {
try{
boolean result = orderRepo.saveOrder(orderMpdel);
if(result) {
mqSender.sendMessage(orderModel);
}
} catch(Exception e) {
rollback();
}
}
在上述代码中,无论是本地订单数据保存(本地事务)处理失败还是异步消息发送异常,我们都会进行订单数据回滚,这代码看上去没有什么毛病,但是我们再仔细分析下是不是真的没有什么问题吗?
由于引入了消息中间件,服务之间的调用不再是依次的同步调用,各自服务通过消费对应的订单消息来实现各自的业务。当用户进行下单操作后,订单服务会生成对应的订单信息,而后发送订单生成消息。但是由于是分布式系统,受网络等因素的影响,有可能出现消息发送完成后订单服务未接收到消息中间件返回的响应信息,因此订单服务将之前的订单数据进行了回滚,但是积分服务已经将 MQ 中的订单信息进行了消费,增加了用户积分。这就造成了订单与积分数据不一致的情况。 另外如果在订单生成之后,订单服务挂掉了无法正常投递消息也会造成数据不一致的情况。
a、本地消息表
通过本地消息表的方式将分布式事务拆解为本地事务的实现,如下图所示,将订单生成以及消息记录表包裹在一个本地事务中,生成订单信息后同时在本地消息表中插入一条订单消息发送记录用以记录消息发送的状态。如果消息发送失败则记录状态,订单服务进行重试发送,超过重试次数后可以由定时服务进行定时扫描本地未完成状态的消息进行重新发行消息,以保证消息的正常投递。
当消息到达 MQ 之后,如果 MQ 进行了正常的响应则业务可以继续。但是如果 MQ 未正常响应,则订单服务认为 MQ 未能正常接收消息需要不断进行重试。
MQ 接收到消息并进行持久化后,则响应订单服务说我这里已经接收到你的订单消息了,接下来的事情就交给我我吧,此时订单服务不再进行消息发送重试,本地消息表中的消息状态为已发送。
消息发送成功后,积分服务将会消费对应的订单消息,但是如果积分服务在执行本地积分服务失败后需要通知订单服务将原来的订单信息进行回滚。
b、事务消息
关于事务消息,可以参见之前写的文章,主要的思想是借助于 RocketMQ 的事务消息机制,将分布式事务转换为两阶段提交的解决方案,从而实现依托于消息中间件的事务一致性解决方案。
本文以最常见的电商购物案例为实际背景,围绕如何实现业务中台的数据一致性进行了详细的说明,分别从分布式系统数据一致性问题产生的背景、相关的分布式事务理论以及基于理论之上产生的相应的解决方案总结了业务中台的数据一致性的解决方案。重点阐述了柔分布式事务解决方案在业务中台数据一致性实践中的应用。