最终一致性:BASE论文笔记

最终一致性:BASE论文笔记

简述

Base论文是ebay的架构师于2008年提交的一篇论文。主要用来阐述在分布式架构设计下,基于BASE的设计思想和方案。所谓BASE就是basically available(基本的可用性),soft state(软状态,所谓的软状态,指的是暂时的不一致,后文会详细展开),eventually consistent(最终一致性)。在分布式领域,有著名的CAP理论,也就是一致性,可用性,分区容错性便可靠性,这三者没法同时得到。而base理论就是在牺牲部分一致性的基础上,来达到可用性的大幅提高的一种方案理念。
欢迎加入技术交流群186233599讨论交流,也欢迎关注技术公众号:风火说。java

分区容错性

这是BASE里相对简单好操做的一个地方。好比咱们须要存储用户数据,经过部署多个物理实例,将用户数据均匀的分散在其上。即便其中的一个服务器发生宕机,也不会影响到其余的数据。容忍了局部失败而提供了总体上必定的容错性。
容错的问题经过将数据部署多个节点来保证。那就带来下面的问题,数据的一致性如何保证。传统的业务解决方式就是2PC。依赖于数据库提供的XA事务来实现分布式下数据一致性。sql

传统的数据库事务方式在分布式领域的问题

考虑一个这样的场景,A给B进行转帐。2个用户在不一样的银行,他们的数据库部署在不一样的物理节点。而转帐是一个事务操做。传统意义上有所谓的分布式事务,也就是2PC这种协议来保证分布式状况下的事务。可是2PC协议的开销很大,不利于在大规模的状况下的性能表现。而且XA只是数据库层面的协议,若是应用自己是分布式的,还须要额外的落地支持。在实现上也不简单。数据库

BASE方式来解决

这个转帐例子有两个数据库,若是A成功了,B失败了,此时就须要回滚A。若是咱们不回滚,而是重试直到B成功也是一种可行的方案。对于同一个数据库实例,在一个链接中可使用事务操做不一样的表。基于以上的两点,咱们给A增长一个消息表,用来存储须要别的库异步配合的执行消息。在这个例子中,用于存储向B发送的消息。那么咱们的操做以下服务器

  1. 在A中开启事务,对用户执行扣钱sql,而且往消息表中新增一个消息,消息的内容是要求B执行加钱的操做。
  2. 提交事务。若是事务失败则回滚,该业务失败。此时B彻底无感知。
  3. 若是事务提交成功,则向B发出调用,执行增长钱的操做。若是B回复成功,将消息表中的该消息删除。
  4. 若是B回复失败,则发起重试,或者依靠定时任务不断重试,直到成功。
  5. 成功后删除消息表中的该消息。

咱们来分析下上面的作法,首先经过A中的事务保证了在A上转帐的操做落地完成而且有记录能够查询(消息表中的就是未完成的记录)。在事务提交成功后再执行和B相关的操做,返回成功才删除消息表,这样就保证操做最后老是能够成功(由于B返回了成功消息)。能够看到,在A事务成功到B调用成功这之间,数据在两个数据库上存在不一致的状况,这也是BASE理论中牺牲的强一致性的地方,可是经过这样的作法,数据在两个系统中最终是能够达到一致的,也便是所谓的最终一致性。经过牺牲强一致性,提升了系统的吞吐。而且这个不一致的时间窗口实际上对于通常的用户是无感知的。可能就是在几十毫秒到两三秒之间,用户是能够容许也是理解这样的延迟的。
那么上面的方案是否就已经能够解决问题了呢,答案是不。网络

幂等

在上面的流程能够看到,在A事务成功之后要调用B的接口,若是调用失败是须要重复调用直到成功的。问题在于,因为须要网络传输调用结果,有可能B调用其实是成功了,可是网络中断致使A没法收到消息。那么A就会认为是调用失败,从而再次发起调用。那么B就将一个加钱的动做执行了2次。此时两边的数据处于不一致的状态,而且没法修复。为了解决这个问题,咱们引入幂等约束。所谓幂等操做也就是说对于该操做,一次或屡次的调用产生的结果是相同的。上面的问题就是因为调用B加钱的操做不是幂等,而A在理论上必然存在重复调用的状况(由于网络是不可靠的),进而致使数据不一致错误。那么怎么让B的加钱操做是幂等的呢?A中存在一个消息表,用于存放须要执行操做的消息,那么给B中也增长一个更新表。对于A中的每个消息都存在一个全局惟一的id。那么调用B的加钱操做的流程修改成以下架构

  1. B开启事务。检查更新表中是否存在消息id。若是存在消息id,直接忽略该操做。而且返回A成功消息。
  2. 若是不存在消息id,执行用户的加钱操做,而且往更新表中插入该消息。提交事务。提交事务成功返回A成功消息,提交失败则返回失败消息。

使用这样的逻辑,则B的加钱操做就成为了一个幂等操做,能够承受屡次调用。异步

简单的幂等

上面方案的幂等依靠本地的更新表记录了全部的消息id进行比对进而防止屡次的重复调用。这样须要一个更新表而且要存储全部的消息,比较重一些。若是咱们给于消息一个不断递增的序号,而且b的数据表中新增一个序号字段。b只要执行消息前会比对消息的序号和自身数据的序号。若是消息序号大于自身序号才能够执行。也就是执行以下的伪代码分布式

begin transaction
update b set money=message.money+b.money,version=message.version where b.id = message.userid and b.version > message.version
commit
end transaction

经过这样的方式,就不须要启用一个单独的更新表。而后对于B的业务表有侵入性的修改。性能

中间总结

在有了上面的例子,如今咱们来稍微总结。经过将一致性要求从强一致性下降到最终一致性,咱们能够避免2PC这样的高成本协议,而且让业务具备更强的伸缩性。将上面具体的方案抽象下,其中的思路仍是比较清晰的。架构设计

  1. 将一个分布式的事务拆分红多个本地事务,引入消息概念。
  2. 将本地数据更新和消息的新增绑定为一个事务,做为总体进行提交。
  3. 在在一个本地事务成功的状况下,进行下一个远端的事务操做。而且要求该远端事务操做具有幂等性,能够承受重复的屡次调用而不会致使数据错误。
  4. 消息能够被本地被屡次尝试或者在异步组件中尝试直到消息送达而且操做成功。最终让全部系统的数据达到一致。

上面的思路重点就是在异步消息这个概念的引入。而幂等的保证方式能够有两种,一种是被调用方,也就是接口提供方,自身保存一份执行过的消息表,用于在执行操做前进行比对,避免执行重复操做。一种是将消息引入序号改变,被调用方只执行比自身序号大的消息。

TCC类型的幂等

上面的基于存储消息方式的幂等因为须要存储执行过的消息会带来额外的存储开销。而且执行过的消息理论上已经失去了其意义(假设调用方执行成功,后续就不会再去调用,那么就没有判重的需求了)。这种方式中,消息的序号是由消息的发送方来生成的,而且被调用方始终须要存储着全部的操做历史,历史数据会愈来愈多,而且都是无用的历史数据。那咱们换个思路,消息的序号是由消息的收取方来生成如何。具体的操做以下

  1. A开启事务,执行本地业务更新,调用B的try接口传递业务参数。B的try接口调用成功则返回一个全局惟一的消息id
  2. A将这个消息id写入到消息表中。提交事务。
  3. 若是事务提交成功,调用B的confirm接口。接口的参数只有消息id。代表该业务确认须要执行。B将真实执行该业务,而且将confirm执行结果返回给予A。若是成功则删除消息表中的该消息。若是失败则经过异步组件发生重试。
  4. 若是事务提交失败,则调用B的cancel接口,接口参数只有消息id。代表取消该业务的执行。

在这种方式中,消息的id是由被调用方被调用try接口时产生。此时被调用方存储该消息id。在被调用confirm或者cancel接口时,首先检查该id是否存在,存在才执行下一步的操做。对于cancel接口而言,操做只是简单的删除该消息便可。而对于confirm操做,则须要开启事务,而且在事务中执行对应的操做,而且删除消息。最终提交事务。若是事务成功提交则返回成功消息,不然返回失败。若是消息id不存在,有两种可能。

  • 消息id是错误的
  • 该消息已经被执行了。

在内部可信的系统内,排除第一种状况,只有第二种状况。故而在这种状况,confirm接口的调用也是返回成功消息。这种方式,被调用方只须要存储必定规模的消息id,由于被成功执行的消息都会被删除。再给全部存储的消息一个过时时间。由后台定时组件按期扫描删除便可。这样,消息的堆积大小也是可控的了。

相关文章
相关标签/搜索