分布式系统的一个重要问题是数据的复制。对数据进行复制一般是为了增强系统的可靠性和提高性能。举例来说,当一个数据库的副本被破坏以后,系统只需要转换到其他副本就能继续运行。另外一个例子,当访问单一服务器管理的数据的进程数不断增加时,系统就需要对服务器的数量进行扩充,此时,对服务器进行复制,随后让它们分担工作负荷,就可以提高性能。
但复制数据同时也带来了一个难点,那就是如何保持各个副本数据的一致性。换句话说,更新其中任意一个副本时,必须确保同时更新其他副本;否则,数据的各个副本将不再相同(数据不一致)。本节就来探讨如何实现数据的一致性。
一致性模型实质上是进程和数据存储之间的一个约定。在正常情况下,在一个数据项上执行读操作时,它期待该操作返回的是该数据在其最后一次写操作之后的结果。在没有全局时钟的情况下,很难精确地定义哪次写操作是最后一次写操作,于是就产生了一系列用其他方式定义的一致性模型。
任意读操作都要读到最新的写的结果。严格一致性是限制性最强的模型,依赖于绝对的全局时钟,但是在分布式系统中实现这种模型的代价太大,所以在实际系统中的运用有限,基本上不可能做到。
有多种不同的方法来为应用程序指定它们能容忍哪些不一致性,其中有一种通用的方法,它定义了区分不一致性的三个互相独立的坐标轴:副本之间的数值偏差、副本之间的新旧程度偏差,以及更新操作顺序的偏差。这些偏差形成了持续一致性的范围。
数值偏差可以这样理解:已应用于其他的副本,但还没有应用于给定副本的更新数目。比如,Web缓存可能还没有得到Web服务器执行的一批操作。
新旧程度偏差与副本最近一次的更新有关。对于某些应用,只要副本提供的数据不是很旧,都是可以容忍的,比如,天气预报通常会滞后一段时间。
更新操作顺序的偏差是指,只要可以界定副本之间的差异,就允许不同的副本采用不同的更新顺序。
任何执行结果都是相同的,就好像所有进程对数据存储的读或写操作是按某种序列顺序执行的一样,并且每个进程的操作按照程序所制定的顺序出现在这个序列中。
也就是说,任何读或写操作的交叉都是可接受的,但是所有进程都看到相同的操作交叉。
所有进程必须以相同的顺序看到具有潜在因果关系的写操作。不同机器上的进程可以以不同的顺序看到并发的写操作。
假设P1和P2是有因果关系的两个进程,如果P2的写操作信赖于P1的写操作,那么P1和P2对x的修改顺序,在P3和P4看来一定是一样的。但如果P1和P2没有关系,那么P1和P2对x的修改顺序,在P3和P4看来可以是不一样的。
相比顺序一致性,因果一致性去掉了那些没有联系的操作需达成一致顺序观点的要求,只保留了那些必要的顺序(有因果关系)。
入口一致性其实也就是对每个共享的数据定义一个同步变量(锁)。当然,没有进行同步就进行读操作,是不能保证结果正确的。
以客户为中心的一致性是指从用户的视角来看数据是一致的。客户只关心数据最终是否一致。
只要保证同一个用户访问的数据是一致的就可以了。如果用户只是访问一个副本,则很好实现,否则就需要一定的策略了。当没有更多的更新时,要保证当前的更新最终会传播到所有副本上。著名的例子有DNS系统、万维网。
最终一致性需要注意一个典型的问题,即当客户访问不同的副本时,问题就出现了。更具体的例子比如,作者在博客上更改了一篇博文的内容,在 A 地的用户先访问到最新的内容,而 B 地由于离博客服务器远,读者看到的还是原先的内容。
对于最终一致性的数据存储而言,这个示例很有代表性。问题是由用户有时可能对不同的副本进行操作引起的。以客户为中心的一致性分为如下几大类。
当进程从一个地方读出数据x,那么以后再读到的x应该是和当前x相同或比当前更新的版本。也就是说,如果进程迁移到了别的位置,那么对x的更新应该比进程先到达。
以分布式邮件数据库系统为例。每个用户的邮箱可能分布式地复制在多台机器上。邮件可能被插入任何一个位置的邮箱。但是,数据更新是以一种懒惰的方式传播的。假设用户在杭州读取了他的邮件(假定只读取邮件不会影响其他邮箱,也就是说,消息不会被删除,甚至不会被标记为已读),当用户飞到惠州后,单调读一致性可以保证当他在惠州打开他的邮箱时,邮箱中仍然有杭州邮箱里的那些消息。
跟单调读相似,如果一个进程写一个数据 x,那么它在本地或迁移到别的地方再进行写操作的时候,原来的写操作必须先传播到这个位置。也就是说,进程要在任何地方至少和上一次写一样新的数据。
读写一致性指一个进程对于数据x的写操作,进程无论到任何副本上都应该能被后续读操作看到这个写操作的影响,也就是看到写操作的影响或更新的值。
也就是说,写操作总是在同一个进程执行的后续读操作之前完成的,而不管这个后续读操作发生在什么位置。
顾名思义,写读一致性就是在读操作后面的写操作基于至少跟上一次读出来一样新的值。也就是说,如果进程在地点1读了x,那么在地点2要写x的副本的话,至少写的时候应该基于和地点1读出的一样新的值。
举个例子,用户先读了文章 A,然后他回复了一篇文章 B。为了满足读写一致性,B 被写入任何副本之前,需要保证A必须已经被写入那个副本。即,当原文章存储在某个本地副本上时,该文章的回应文章才能被存储到这个本地副本上。