最终一致性定义
最终一致性是分布式计算里的一种内存一致性模型,它指对于已改变写的数据的读取,最终都能取得已更新的数据,但不完全保证能立即取得已更新的数据。这种模型通常可以实现较高的可用性。
[1]最终一致性,通过乐观复制,或称延迟复制(lazy replication)实现。
[2]这种概念最初始于移动应用,后来在各类分布式系统中也有广泛的应用。
[3]达到最终一致性的分布式系统被称为副本达到了“收敛(converged)”状态。
[4]最终一致性是一种较弱的保证。如果某个系统满足更强的一致性约束(例如线性一致性),它就同时具有最终一致性,但是反过来则未必成立,仅保证最终一致性的系统无法保证更强的约束。
最终一致性一般发生在主从节点架构情况下,主从复制要求所有写请求都经由主节点,而任何副本只能接收读请求,对于读操作密集的负载业务,这是一个不错的选择:创建多个从副本,将读请求分发给这些从副本,从而减轻主节点负载并允许读请求就近读取。
在这种扩展体系下,只需要添加更多的从副本,就可以提高读请求的服务吞吐量。但是,这种方法实际上只能用于异步复制,如果尝试同步复制所有从节点,则单个节点故障或者网络中断将使整个系统无法写入,而且节点越多,发生故障的概率越高,所以完全同步的配置现实反而非常不可靠。
不幸的是,如果一个应用正好从一个异步的从节点读取数据,而该副本落后于主节点,则应用可能会读到过期的信息。这会导致数据库中出现明显不一致:由于并发所有的写入都反映在从节点上,如果同时对主节点、从节点发起相同请求,可能会得到不同结果。不过,这种不一致只是一个暂时的状态,如果停止写数据库,经过一段时间后,从节点最终会赶上并与主节点一致,这种效应被称为最终一致性。
而"最终"的滞后时间太长时,导致的不一致性不仅仅一个理论存在的问题,而是实实在在的现实问题。这里重点介绍三个复制滞后可能出现的问题,并给出相应的解决思路。
读自己的写
在异步复制存在这样一个问题,用户写入不久马上查看数据,则新数据可能尚未到达从节点。对用户来说,看起来似乎刚刚提交的数据丢失了,显然用户要不高兴了。
对于这种情况,我们需要“写后读一致性”,也称为读写一致性。该机制保证如果用户重新加载页面,他们总能看到自己最近提交的更新。但对其他用户则没有任何保证,其他用户的更新可能会稍后才能刷新看到。
读后一致性
主从复制的系统该如何实现写后读一致性呢?下面列举几种方案:
如果用户访问可能会被修改的内容,从主节点读取,否则从读取。这背后就要求有一些方法在实际执行查询之前,就已经知道内容是否可能会被修改。例如:社交网络上的用户首页通常只能由所有者编辑,而其他人无法编辑。这就可以设置一个简单规则:总是在主节点读取用户自己的首页配置信息,而在从节点读取其他用户的配置信息。
如果应用的大部分都可能被用户修改,方法1不太有效,他会导致大部分请求必经主节点。这时候需要用其他方案判断是否主节点读取。例如:跟踪最近更新的时间,如果更新后一分钟之内,则总是主节点读取;并监控从节点复制滞后程度,避免从那些滞后时间超过一分钟的从节点读取。(其实这种方案比较理想化,实现起来复杂,比如客户端如何知道在一分钟更新了?)
3.客户端还可以记住最近更新的时间戳,并附带在读请求中,系统可以确保对该用户提供读服务时都应该走主节点或者已最新节点。这里我们项目中就是采取这种方案。
举个实际例子:我们现在在构造一个中台,数据的持久化是使用MongoDB分片集群+ElasticSearch集群,数据的写入都是使用MongoDB,通过ES集群通过订阅 mongo change stream(类似binlog的东西)。,碰到的问题就是,前端页面当业务做写操作成功后,返回的列表页面,没有新更新的数据。这个案例中,ES集群其实被看做一个异化只读从节点。我们是这样解决的,当写成功返回时附带返回一个ticket,ticket可以是userId+时间戳,也可以是UUID,,同时ticket写入到Redis上并设置30秒超时。然后前端的读请求会附带上这个ticket,后端根据这个ticket来进行路由判断:如果ticket命中,则去走mongodb,否则走ES集群。
4.如果副本分布在多数据中心(例如考虑与用户的地理接近,以及高可用性),情况会更复杂些。必须把请求路由到主节点所在的数据中心(该数据中心可能离用户很远)
单调读
假定用户从不同副本进行了多次读取,用户不停的刷新同一个页面,读请求随机路由到不同的从节点(先是少量滞后的从节点、然后是滞后很大的从节点),出现数据不一致的问题。
出现这种情况,一般的解决方案使基于用户ID的哈希而不是随机副本,但如果该副本发生失效,则重新路由到另一个副本上。