分布式系统的数据一致性

536 阅读6分钟

假设我们的分布式系统有两个节点,这两个节点作为集群的主从节点,不管A是主节点还是B是主节点,都可以一致看待。在这个模型里,如果A和B的数据副本始终保持一致的,那么这个模型就满足一致性(C)原则。如果A和B二者之一始终能对外提供服务,那么这个模型就满足可用性(A)原则。但是在现实世界里,A和B的通信,始终会有因为网络原因而导致数据不互通的情况(即网络分区),如果不会出现网络分区,那么这么模型就满足分区容错(P)原则。

3E16ADCD-3D40-4E82-9574-424ACC64AB48.png

所以我们在设计分布式系统时,到底是选择满足CP还是满足AP,无非是因为网络分区不可避免。这就是CAP原理。

副本是每个节点的数据状态。

一、主从架构

646791B4-29ED-46B6-AEB8-70835FB8EACB.png

首先来看只有一个主节点,有多个从节点的数据同步问题。

所有读请求由主节点转发给从节点处理,所有写请求有主节点处理,写完成后,再将主节点的副本同步到从节点。

副本同步有两个方式,同步和异步:

如果采用同步,也就是说,为了达到所有节点的副本一致性,当一个写请求进来时,主节点A必须等待所有副本同步成功,才将结果返回给客户端,这种情况,就无法保证可用性。

如果采用异步的话,主节点A处理完写请求后可以直接返回,不需要等待副本同步情况,如果在同步过程中,B挂了,或者因为网络中断,必然会引起B的副本落后于主节点A的副本,所以B在重启或者网络连接后,需要读取上次同步位置,继续同步副本。

单主节点还有一个问题,如果主节点挂了,那么整个系统将不可用。当主节点挂了,可以从从节点挑一个作为主节点(假设是B),继续提供服务。

如果B的副本在A挂之前,就已经跟A是同步的,那就不会有什么问题,如果B是落后于A的,倘若此时A恢复了,A就变成从节点,同步A的副本,这种情况如果A是很快就恢复了,A的副本明显是最新的,那必然会导致数据丢失的情况 有可能A以为自己还是主节点继续提供服务,那就出现了脑裂情况。

最终一致性

读写一致性

0F12D381-7D17-4771-AB42-96717F7C0296.png

在A节点同步副本到B和C节点期间,可能因为同步给C时,因为网络延迟,或者A的问题,导致同步延迟,此时A刚好再次读value的值,可能会读取到C节点未被更新的旧值,这种情况就是读写不一致。

解决方案可以是,直接从主节点A读取,但这又容易变成单节点服务了。

单调一致性

81581AB6-E61B-46E4-BA11-1C66461CD61C.png

假设有个用户user1要更新value值为2,主节点更新后,然后同步副本到B和C节点,当同步到B节点未同步到C节点时,用户user2刚好读取节点B的值,此时值为2没有问题。假如网络问题或者主节点A服务器本身问题,导致同步副本到C节点延迟了,倘若此时用户user2再次读取value的值,而读节点刚好落在C节点,C节点的value值还是原来的1,这对用户user2来说,两次读取的值竟然不一致,这就是单调不一致问题。

解决方案可以是,就是对user2来说,第一次读取和第二次读取都让它的请求落在节点B,相当于记住user2的状态。

顺序一致性

18506F61-38D1-40C3-ACF9-F9E5FAB380C3.png

假如有两个分区1和2,在一开始用户user1发出set value=2命令,而后再发出set value=3命令,这两个命令假设时有先后顺序,即必须先执行完set value=2,才能执行set value=3。由于服务器或者网络原因,分区1的A1副本同步到B1发生滞后,而此时分区2的B2,都已经同步完set value=3了。如果此时又个用户user2在观察数据,会发现此时B2的value=3,过会再观察B1,value=2,那这种情况就时顺序不一致的问题。

解决方案可以是,如果这种有先后顺序的写入,我们可以让其发生在一个分区内。

如果最终一致性的延迟时间很长,足以影响业务,那可能不得不考虑使用强一致性。

二、多主架构

BE3FB14C-A8B3-4DDA-B296-84F253E7A3D9.png

主从架构里如果主节点挂了,那么整个系统将不可用,所以单主节点的瓶颈由此可见,所以对主节点进行冗余,进而形成多主架构。多主架构的每个分区内的副本复制情况,和主从架构是一样,但是主节点之间还会进行相互之间的副本复制。

主节点之间复制很大的问题就是写冲突,比如A1进行set value=1,然后同步给A2,而此时A2也执行set value=2,然后同步给A1,两个主节点同时对value修改,那应该以谁为准?

解决方案1: 交给应用层或者用户处理,由用户决定该如何合并冲突。

解决方案2: 同步写请求,然而这样做反而让多主节点的意义变得不大了。

解决方案3: 其实处理多主节点的写冲突,没有很好的解决方案,可能比较好的是避免冲突吧。如果应用层可以对特定的写请求(即会发生写冲突的)分到一个分区处理,那么就不会发生写冲突。

三、无主架构

EBC4649B-8CCE-48F9-AECC-29066DA76C7E.png

无主架构就没有主节点了,所有读写由客户端发起,比如发起写请求,对三个节点都发起,读请求也是一样。上图所示,客户端发起set value=2,version=2,节点A和B都成功了,节点C失败了,那么A和B都能更新到最新值,而C还是旧值。当发起读请求时,如果至少读取两个节点,取最新版本的值,就不会有问题,如果只取一个节点,则可能会读取到旧值。

假设有n个副本,则有w + r > n,w为写请求需成功的节点数,r为读请求需成功的节点数。上图中: 2 + 2 > 3,表示可以允许1个节点写失败,1个节点读失败。

这就是读写quorum(读写仲裁)