场景应用:分布式系统如何保证数据的最终一致性?

Posted 流楚丶格念

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了场景应用:分布式系统如何保证数据的最终一致性?相关的知识,希望对你有一定的参考价值。

文章目录

太多了记不住啊,我的天啊,太难了,枯了,啊啊啊啊!

引言

DTS(分布式事务处理模型)

国际开放标准组织Open Group定义了DTS(分布式事务处理模型),模型中包含4种角色:应用程序、事务管理器、资源管理器和通信资源管理器。事务管理器是统管全局的管理者,资源管理器和通信资源管理器是事务的参与者。

JEE(Java企业版)规范也包含此分布式事务处理模型的规范,并在所有AppServer中进行实现。在JEE规范中定义了TX协议和XA协议,TX协议定义应用程序与事务管理器之间的接口,XA协议则定义事务管理器与资源管理器之间的接口。在过去使用 AppServer如WebSphere、 WebLogic、JBoss等配置数据源时会看见类似XADatasource的数据源,这就是实现了分布式事务处理模型的关系型数据库的数据源。在企业级开发JEE中,关系型数据库、JMS服务扮演资源管理器的角色,而EJB容器扮演事务管理器。

下面我们介绍两阶段提交协议、三阶段提交协议及阿里巴巴提出的 TCC,它们都是根据DTS这一思想演变而来的。

解决方案:

两阶段提交协议

两阶段提交协议把分布式事务分为两个阶段,一个是准备阶段,另一个是提交阶段。准备阶段和提交阶段都是由事务管理器发起的,为了接下来讲解方便,我们将事务管理器称为协调者,将资源管理器称为参与者。

两阶段提交协议的流程如下所述:

  • 准备阶段(第一阶段):协调者向参与者发起指令,参与者评估自己的状态,如果参与者评估指令可以完成,则会写redo或者undo日志,然后锁定资源,执行操作,但是并不提交。

  • 提交阶段(第二阶段):如果每个参与者明确返回准备成功,也就是预留资源和执行操作成功,则协调者向参与者发起提交指令,参与者提交资源变更的事务,释放锁定的资源;如果任何一个参与者明确返回准备失败,也就是预留资源或者执行操作失败,则协调者向参与者发起中止指令,参与者取消已经变更的事务,执行undo日志,释放锁定的资源。

两阶段提交协议的成功场景如下图所示:


我们看到两阶段提交协议在准备阶段锁定资源,这是一个重量级的操作,能保证强一致性,但是实现起来复杂、成本较高、不够灵活,更重要的是它有如下致命的问题:

  • 阻塞:从上面的描述来看,对于任何一次指令都必须收到明确的响应,才会继续进行下一步,否则处于阻塞状态,占用的资源被一直锁定,不会被释放。
  • 单点故障:如果协调者宕机,参与者没有协调者指挥,则会一直阻塞,尽管可以通过选举新的协调者替代原有协调者,但是如果协调者在发送一个提交指令后宕机,而提交指令仅仅被一个参与者接收,并且参与者接收后也宕机,则新上任的协调者无法处理这种情况。
  • 脑裂:协调者发送提交指令,有的参与者接收到并执行了事务,有的参与者没有接收到事务就没有执行事务,多个参与者之间是不一致的。

上面的所有问题虽然很少发生,但都需要人工干预处理,没有自动化的解决方案,因此两阶段提交协议在正常情况下能保证系统的强一致性,但是在出现异常的情况下,当前处理的操作处于错误状态,需要管理员人工干预解决,因此可用性不够好,这也符合CAP协议的一致性和可用性不能兼得的原理。

三阶段提交协议

三阶段提交协议是两阶段提交协议的改进版本。它通过超时机制解决了阻塞的问题,并且把两个阶段增加为以下三个阶段。

  • 询问阶段:协调者询问参与者是否可以完成指令,协调者只需要回答是或不是,而不需要做真正的操作,这个阶段超时会导致中止。
  • 准备阶段:如果在询问阶段所有参与者都返回可以执行操作,则协调者向参与者发送预执行请求,然后参与者写redo和undo日志,执行操作但是不提交操作;如果在询问阶段任意参与者返回不能执行操作的结果,则协调者向参与者发送中止请求,这里的逻辑与两阶段提交协议的准备阶段是相似的。
  • 提交阶段:如果每个参与者在准备阶段返回准备成功,也就是说预留资源和执行操作成功,则协调者向参与者发起提交指令,参与者提交资源变更的事务,释放锁定的资源;如果任何参与者返回准备失败,也就是说预留资源或者执行操作失败,则协调者向参与者发起中止指令,参与者取消已经变更的事务,执行 undo 日志,释放锁定的资源,这里的逻辑与两阶段提交协议的提交阶段一致。

三阶段提交协议的成功场景示意图如下图所示:


三阶段提交协议与两阶段提交协议主要有以下两个不同点:

  • 增加了一个询问阶段,询问阶段可以确保尽可能早地发现无法执行操作而需要中止的行为,但是它并不能发现所有这种行为,只会减少这种情况的发生。

  • 在准备阶段以后,协调者和参与者执行的任务中都增加了超时,一旦超时,则协调者和参与者都会继续提交事务,默认为成功,这也是根据概率统计超时后默认为成功的正确性最大。

三阶段提交协议与两阶段提交协议相比,具有如上优点,但是一旦发生超时,系统仍然会发生不一致,只不过这种情况很少见,好处是至少不会阻塞和永远锁定资源

TCC

签名讲解了两阶段提交协议和三阶段提交协议,实际上它们能解决常见的分布式事务的问题,但是遇到极端情况时,系统会产生阻塞或者不一致的问题,需要运营或者技术人员解决。两阶段及三阶段方案中都包含多个参与者、多个阶段实现一个事务,实现复杂,性能也是一个很大的问题,因此,在互联网的高并发系统中,鲜有使用两阶段提交和三阶段提交协议的场景。

后来有人提出了TCC协议,TCC协议将一个任务拆分成Try、Confirm、Cancel三个步骤,正常的流程会先执行Try,如果执行没有问题,则再执行Confirm,如果执行过程中出了问题,则执行操作的逆操作Cancel。从正常的流程上讲,这仍然是一个两阶段提交协议,但是在执行出现问题时有一定的自我修复能力,如果任何参与者出现了问题,则协调者通过执行操作的逆操作来Cancel之前的操作,达到最终的一致状态。

可以看出,从时序上来说,如果遇到极端情况,则TCC会有很多问题,例如,如果在取消时一些参与者收到指令,而另一些参与者没有收到指令,则整个系统仍然是不一致的。对于这种复杂的情况,系统首先会通过补偿的方式尝试自动修复,如果系统无法修复,则必须由人工参与解决。

从TCC的逻辑上看,可以说TCC是简化版的三阶段提交协议,解决了两阶段提交协议的阻塞问题,但是没有解决极端情况下会出现不一致和脑裂的问题。然而,TCC通过自动化补偿手段,将需要人工处理的不一致情况降到最少,也是一种非常有用的解决方案。某著名的互联网公司在内部的一些中间件上实现了TCC模式。

我们给出一个使用TCC的实际案例,在秒杀的场景中,用户发起下订单请求,应用层先查询库存,确认商品库存还有余量,则锁定库存,此时订单状态为待支付,然后指引用户去支付,由于某种原因用户支付失败或者支付超时,则系统会自动将锁定的库存解锁以供其他用户秒杀。

TCC协议的使用场景如下图所示:

在大规模、高并发服务化系统中,一个功能被拆分成多个具有单一功能的子功能,一个流程会有多个系统的多个单一功能的服务组合实现,如果使用两阶段提交协议和三阶段提交协议,则确实能解决系统间的一致性问题。

除了这两个协议的自身问题,其实现也比较复杂、成本比较高,最重要的是性能不好,相比来看,TCC协议更简单且更容易实现,但是TCC协议由于每个事务都需要执行Try,再执行Confirm,略显臃肿,因此,现实系统的底线是仅仅需要达到最终一致性,而不需要实现专业的、复杂的一致性协议。

实现最终一致性有一些非常有效、简单的模式,下面就介绍这些模式及其应用场景。

其他简单的一致性模式

查询模式

任何服务操作都需要提供一个查询接口,用来向外部输出操作执行的状态。服务操作的使用方可以通过查询接口得知服务操作执行的状态,然后根据不同的状态来做不同的处理操作。

为了能够实现查询,每个服务操作都需要有唯一的流水号标识,也可使用此次服务操作对应的资源ID来标识,例如:请求流水号、订单号等。首先,单笔查询操作是必须提供的,也鼓励使用单笔订单查询,这是因为每次调用需要占用的负载是可控的。批量查询则根据需要来提供,如果使用了批量查询,则需要有合理的分页机制,并且必须限制分页的大小,以及对批量查询的吞吐量有容量评估、熔断、隔离和限流等措施。

补偿模式

有了上面的查询模式,在任何情况下,我们都能得知具体的操作所处的状态,如果整个操作都处于不正常的状态,则我们需要修正操作中有问题的子操作,这可能需要重新执行未完成的子操作,后者取消已经完成的子操作,通过修复使整个分布式系统达到一致。为了让系统最终达到一致状态而做的努力都叫作补偿。

对于服务化系统中同步调用的操作,若业务操作发起方还没有收到业务操作执行方的明确返回或者调用超时,业务发起方需要及时地调用业务执行方来获得操作执行的状态,这里使用在前面学习的查询模式。在获得业务操作执行方的状态后,如果业务执行方已经完成预设工作,则业务发起方向业务的使用方返回成功;如果业务操作执行方的状态为失败或者未知,则会立即告诉业务使用方失败,也叫作快速失败策略,然后调用业务操作的逆向操作,保证操作不被执行或者回滚已经执行的操作,让业务使用方、业务操作发起方和业务操作执行方最终达到一致状态

异步确保模式

异步确保模式是补偿模式的一个典型案例,经常应用到使用方对响应时间要求不太高的场景中,通常把这类操作从主流程中摘除,通过异步的方式进行处理,处理后把结果通过通知系统通知给使用方。这个方案的最大好处是能够对高并发流量进行消峰,例如:电商系统中的物流、配送,以及支付系统中的计费、入账等。

在实践中将要执行的异步操作封装后持久入库,然后通过定时捞取未完成的任务进行补偿操作来实现异步确保模式,只要定时系统足够健壮,则任何任务最终都会被成功执行。

异步确保模式如下图所示:

定期校对模式

系统在没有达到一致之前,系统间的状态是不一致的,甚至是混乱的,需要通过补偿操作来达到最终一致性的目的,但是如何来发现需要补偿的操作呢?

在操作主流程中的系统间执行校对操作,可以在事后异步地批量校对操作的状态,如果发现不一致的操作,则进行补偿,补偿操作与补偿模式中的补偿操作是一致的。

另外,实现定期校对的一个关键就是分布式系统中需要有一个自始至终唯一的ID,生成全局唯一ID有以下两种方法:

实现方法说明
持久型使用数据库表自增字段或者Sequence生成,为了提高效率,每个应用节点可以缓存一个批次的ID,如果机器重启则可能会损失一部分ID,但是这并不会产生任何问题。
时间型一般由机器号、业务号、时间、单节点内自增ID组成,由于时间一般精确到秒或者毫秒,因此不需要持久就能保证在分布式系统中全局唯一、粗略递增等。

可靠消息模式

在分布式系统中,对于主流程中优先级比较低的操作,大多采用异步的方式执行,也就是前面提到的异步确保模型,为了让异步操作的调用方和被调用方充分解耦,也由于专业的消息队列本身具有可伸缩、可分片、可持久等功能,我们通常通过消息队列实现异步化。对于消息队列,我们需要建立特殊的设施来保证可靠的消息发送及处理机的幂等性。

缓存一致性模式

在大规模、高并发系统中的一个常见的核心需求就是亿级的读需求,显然,关系型数据库并不是解决高并发读需求的最佳方案,互联网的经典做法就是使用缓存来抗住读流量。

如果性能要求不是非常高,则尽量使用分布式缓存,而不要使用本地缓存。

写缓存时数据一定要完整,如果缓存数据的一部分有效,另一部分无效,则宁可在需要时回源数据
库,也不要把部分数据放入缓存中。

使用缓存牺牲了一致性,为了提高性能,数据库与缓存只需要保持弱一致性,而不需要保持强一致
性,否则违背了使用缓存的初衷。

读的顺序是先读缓存,后读数据库,写的顺序要先写数据库,后写缓存。

以上是关于场景应用:分布式系统如何保证数据的最终一致性?的主要内容,如果未能解决你的问题,请参考以下文章

如何保证分布式系统的消息最终一致性

场景应用:如何设计一个分布式系统?

场景应用:如何设计一个分布式系统?

分布式场景下如何保证消息队列实现最终一致性

ZooKeeper 如何保证数据一致性

亿级分布式事务架构思维模型,微服务下如何保证最终一致性?