微服务架构实现最终一致性的三种模式

  根据CAP理论,必须在可用性(Availability)和一致性(Consistency)之间做出选择。如果选择提供一致性,则需要付出在满足一致性之前阻塞其他并发访问的代价。这可能持续一段不确定的时间,尤其是系统已经表现出高延迟或者由于网络故障而失去连接时。
  依据目前的成功经验,选择可用性一般更有利于微服务架构的构建,但是在服务和数据库之间维护数据一致性是非常根本的需求,在微服务架构中应该选择满足最终一致性。当然,选择满足最终一致性就要保证从业务开始到结束之间的延时要在用户可接受的范围之内
  一致性的本质是保证一个业务逻辑中包含的服务要么都成功,要么都失败。那么选择保证成功(重试)还是选择保证失败(回滚)呢?这往往由业务模式来决定。
在这里插入图片描述
  实现最终一致性有3中业务模式:可靠事件模式、业务补偿模式、TCC模式。

1 可靠事件模式

  可靠消息模式属于事件驱动架构。当某件重要的事情发生时,例如更新一个业务实体,微服务会向消息代理发布一个事件消息代理会向订阅事件的微服务推送事件,当订阅这些事件的微服务接收此消息时,就可以完成自己的业务,也可能引发更多的事件发布。
  例如,订单服务创建一个待支付的订单,然后会发布一个“创建订单”的时间,如图所示:
在这里插入图片描述
  支付服务消费“创建订单”时间,支付完成后发布一个“支付成功”事件,如图所示:
在这里插入图片描述
  订单服务消费“支付成功”事件,订单状态更新为待出库,如图所示:
在这里插入图片描述
  至此,便完成了整个业务流程。

  但是这并不是一个完美的流程,在这个过程中可能出现多种数据不一致的场景。
1、如果微服务在更新了业务实体后,却发布事件失败;
2、虽然微服务发布事件成功,但是消息代理未能正确推送事件到订阅的微服务;3、接受事件的微服务重复消费了事件。
  可靠事件模型的特点在于保证可靠事件投递避免重复消费。可靠事件投递要求:
1、每个服务都要进行原子性业务操作事件发布
2、消息代理确保事件至少投递一次
  避免重复消费要求服务实现幂等性,如支付服务不能因为重复收到事件而多次支付。现在流行的消息队列都实现了事件的持久化和at least once(至少一次)的投递模式,第二个特性(消息代理确保事件至少投递一次)已经满足,此处不做展开。
  下面主要从可靠事件投递和实现幂等性两方面来讨论。先来看可靠事件投递。我们一如下代码片段为例

public void trans(){
	try{
		// 1、操作数据库
		bool result = dao.update(model);//操作数据库失败会抛出异常
		// 2、如果第一步成功,则操作消息队列(投递消息)
		if(resul){
			mq.append(model);//如果消息投递失败,会抛出异常
		}
	}catch(Exception e){
		// 3、如果发生异常,回滚事务
		rollback();
	}
}

  根据上述代码及注释,初看可能会发现操作数据库时会有3中情况
1、操作数据库成功,消息投递成功
2、操作数据库失败,不会投递消息
3、操作数据库成功,消息投递失败,数据库操作被回滚。
  从上面的分析来看,貌似没有问题。但是仔细分析不难发现缺陷所在,在前面的处理过程中存在一个隐患窗口期
  如果,在微服务A投递事件的时候,消息代理可能已经处理成功,但是响应的时候却出现网络异常,导致微服务A判定事件发布失败,从而回滚数据。最终的结果是事件被投递,数据库却被回滚。
在这里插入图片描述

  在事件投递完成和数据库进行提交操作之间,如果微服务A宕机,也将造成数据库操作因为连接异常关闭而被回滚。最终的结果还是事件被投递,数据库却被回滚。
在这里插入图片描述

  这个实现往往运行很长事件都不会出现问题,但是一旦出现问题便会很难发现问题所在。

本地事件表

  本地消息事件表方法将事件和业务数据保存在同一个数据库中,使用一个额外的事件恢复服务来恢复事件,有本地事务保证更新业务和发布事件的原子性。考虑到事件恢复可能会有一定的延时,所以服务在完成本地事务后可立即向消息代理发布一个事件,如下图所示:
在这里插入图片描述
  本地事件表的说明如下:
1、微服务在同一个本地事务中记录业务数据和事件
2、当微服务实时发布一个事件时,立即通知关联的业务服务,如果事件发布成功则立即删除记录的事件。
3、事件恢复服务定时从本地事务表中恢复未发布成功的事件,并重新发布,直到重新发布成功时才删除记录的事件。
  其中操作 2 主要是为了增加发布事件的实时性,操作 3 是为了保证事件一定被发布。
  本地事件表方法的业务系统和事务系统耦合比较紧密,额外的数据库事件操作会给数据库带来压力,可能会成为数据库的性能瓶颈。
  思考:这里主要是为了保证事件一定被发布,这里应该可以通过重试事件发布直到成功为止,确保事件发布至少一次,同时实现事件消费的幂等性,最终保证数据操作和事件发布的原子性。

外部事件表

  外部事件表方法将事件持久化到外部的事件系统,事件系统需要提供实时事件消息服务以接收微服务发布的事件,同时事件系统还需要提供事件恢复服务来确认和恢复事件,如下图所示:
在这里插入图片描述
  外部事件表的说明如下:
1、业务服务在事务提交前进行,通过实时事件服务事件服务系统请求发送事件,事件系统只记录事件并不真正发送。
2、业务服务提交后,通过实时事件服务向事件系统确认事件需要被成功发送,确认“事件需要被成功发送”后事件系统才真正将事件发布到消息代理中。
3、业务服务在业务回滚时,通过实时事件服务取消事件系统中的事件。
4、如果业务服务在发送确认或取消之前停止了服务怎么办?事件系统的事件恢复服务会定期找到未确认发送的事件,并向业务服务查询事件状态,根据业务服务返回的状态决定事件是发布还是取消。该方法将业务系统和事件系统独立解耦,使它们可以独立伸缩。但是,这种方式需要一次额外的发送操作,并且需要发布者提供额外的查询接口。

2 业务补偿模式

  为了方便描述业务补偿模式(以下简称补偿模式),这里先定义如下两个概念。
业务异常:业务逻辑产生错误的情况,如账户余额不足、商品库存不足等。
技术异常:非业务逻辑产生的异常,如网络连接异常、网络超时等。
  补偿模式使用一个额外的协调服务(补偿框架)来协调各个需要保证一致性的微服务,协调服务按顺序调用各个服务,如果某个微服务调用异常(包括业务异常和技术异常),则取消之前所有已经调用成功的微服务。
  建议将补偿模式用于不能避免出现业务异常的情况,尽量通过优化业务模式的方式避免要求补偿事务。如此,账户余额不足的业务异常可以通过预先冻结金额的方式避免,商品库存不足可以通过要求商家准备额外的库存来解决,等等。
  我们通过一个例子来说明补偿模式。一家旅行公司提供预订行程的业务,可以通过公司的网站预订飞机票、火车票、酒店等。客户提交旅行订单后,旅行公司的预订形成业务开始按顺序串联调用航班预订服务、酒店预订服务、火车预订服务。直到最后的火车预订服务成功后,整个预订业务才算完成。如下图所示:
在这里插入图片描述
  如果火车预订服务没有调用成功,那么之前预订的航班、酒店都要取消。取消之前预订的酒店、航班即为补偿过程。如下图所示:
在这里插入图片描述
  为了降低开发的复杂性和提高效率,将协调服务实现为一个通用的补偿框架。补偿框架提供服务编排和自动完成补偿的功能。
  要实现补偿过程,我们需要做到如下两点
  首先,要确定失败的步骤和状态,从而确定要补偿的范围,在上面的例子中,我们不仅需要知道步骤3(预订火车)失败了,还要知道失败的原因。如果是因为预订火车服务返回无票,那么补偿过程只需要取消前两个步骤就可以了;但是如果失败的原因是因为网络超时,那么补偿过程除了取消前两个步骤外,还需要包括取消步骤3。
  其次,要能提供补偿操作使用到的业务数据。
  做到上面两点的办法是记录完整的业务流水,可以挺过业务流水的状态来确定需要补偿的步骤,同时业务流水可以为补偿提供需要的业务数据。
  业务补偿模式需要应对的难点:
1、预先知道微服务需要记录的业务数据是不可能的,因此需要通过框架表记录关键的业务数据key,同时还要定义如何根据业务数据key查询业务数据详情的方式,实现这样的通用能力并非易事。
2、补偿过程作为一个服务调用过程,同样存在调用不成功的情况,这时需要通过重试机制来保证补偿的成功。如果失败的原因不是暂时性的,即使重试也无法执行成功,这时需要终止重试。如果失败的原因是一些罕见的异常,比如网络异常,可以立即重试。。如果失败的原因是系统繁忙,例如超时等,可以等待一段时间再重试。重试操作一般会指定重试次数上限,如果重试次数达到了上限就不会再进行重试了,这时应该通过某种手段通知相关人员进行处理。对于等待重试策略,如果重试时任然出现错误,那么可以主键增加等待时间,知道达到一个上限,以上限作为等待时间。如果某个时刻聚集了大量请求重试的操作,那么补偿框架需要控制重试请求重试的流量,比防止对工作服务造成过大的压力。
  业务补偿模式的建议:
1、微服务模式下,实现补偿操作不是简单地回退到业务发生时的状态,因为可能还有其他并发请求同时更改了状态,一般都需要逆操作的方式进行补偿。
2、补偿过程不需要严格按照与业务发生相反的顺序执行,可以依据工作服务的重要程度优先执行,甚至可以并发执行。
3、有些服务的补偿过程是由依赖关系的,如果被依赖的服务的补偿操作没有执行成功,那么就要及时终止补偿过程。

3 TCC(Try-Confirm-Cancel)模式

  一个完整的TCC业务由一个主业务服务和若干个从业务服务组成,主业务服务发起并完成整个业务服务。TCC模式要求服务提供三个接口:Try、Confirm、Cancel,如下图所示
在这里插入图片描述
Try:完成所有业务检查,预留必要的业务资源。
Confirm:真正执行业务的接口,不做任何业务检查,只使用Try阶段预留的业务资源,其操作满足幂等性。
Cancel:释放Try阶段预留的业务资源,其操作满足幂等性。
  整个TCC业务分成两个阶段完成,如下图所示
  第一个阶段:主业务服务分别调用所有从业务服务的Try操作,并在活动管理器中登记所有从业务操作。当所有从业务服务的Try操作都调用成功或者某个从业务服务的Try操作失败时,进入第二阶段。
  第二阶段:活动管理器根据第一阶段执行结果来执行Confirm或Cancel操作。如果第一阶段的所有Try操作都调用成功,则活动管理器调用所有从业务活动的Confirm操作,否则调用所有从业务服务的Cancel操作。

在这里插入图片描述

  需要注意的是,第二阶段的Confirm和Cancel操作本身也是满足最终一致性的过程,在调用Confirm或Cancel的时候也可能因为某种原因导致调用失败,所有需要活动管理器支持重试的能力,同时也就要求Confirm和Cancel操作具有幂等性。

补偿模式 VS TCC模式

  补偿模式一个比较明显的缺陷是没有隔离性。从第一个服务步骤开始一直到所有工作服务完成(或者补偿过程完成),不一致都是对其他服务可见的。另外,最终一致性的保证还充分依赖了协调服务的健壮性,如果协调服务异常,就没有办法达到一致性。
  TCC模式在一定程度上弥补了上述缺点。在TCC模式中,从开始到Confirm操作,所有的业务操作都是隔离的(由业务层面保证)。另外,工作服务可以通过指定Try操作的超时时间,主动对预留的业务资源进行Cancel操作,从而实现微服务的自治。
  TCC模式和补偿模式一样需要协调服务和工作服务,协调服务也可以作为通用服务实现为一般矿建。与补偿模式不同的是,TCC服务框架不需要记录详细的业务流水,完成Confirm和Cancel操作的业务要素由业务服务提供
原文《架构宝典》
https://www.docin.com/p-1810236935.html

发布了88 篇原创文章 · 获赞 317 · 访问量 35万+

猜你喜欢

转载自blog.csdn.net/sunxianghuang/article/details/103020275