分布式数据系统:主从节点
1. 主从模式
1.1 同步复制
对于关系型数据库,同步或者异步通常是一个可选项,而其他系统可能是硬性指定二选一。
同步复制的优点是:一旦向用户确认,从节点可以保证已经完成了与主节点的同步,数据已经处于最新版本。如果主节点发生故障,总是可以确保之后继续访问从节点的数据一定是最新的。缺点是:如果同步的从节点无法确认成功,整个写入就不能成功,主节点会阻塞之后的所有写操作,直到同步副本确认。由于该缺点的存在,把所有从节点都配置为同步复制有些不切实际。在实践中,如果数据库启用了同步复制,通常意味着其中某个从节点是同步的,其他从节点则是异步的。如果同步的从节点不可用或者性能下降,则将另一个从节点升级为同步。这样可以保证至少有两个节点拥有最新的数据,这种配置也被称为半同步。
在主从模式下,如果要提高读性能,需要添加更多的从节点。但是,这种方式实际上只能用于异步模式,因为随着从节点的增加,全同步模式需要同步的从节点数量也会增加,任何单节点的故障或者网络中断都会导致整个集群无法写入,节点的增加也会提高故障机率,所以全同步模式在实践中是非常不可靠的。
1.2 异步复制
主从复制通常会被配置为全异步模式。此时,如果主节点失败且不可恢复,则所有尚未同步的从节点的写请求都会丢失,即使已经向客户端确认了写操作完成,仍然无法保证数据的持久化。全异步模式的优点是,不管从节点的数据多么滞后,总是可以继续响应写请求,具有较高吞吐性。异步模式听起来很不靠谱,但是却被广泛使用,特别是那些从节点数据巨大,或者分布于广域地理环境的情况。
异步模式下节点的数据同步可能存在滞后,意味着对主节点和从节点同时发起相同的查询,返回的结果可能是不一致的。但是,这种不一致只是暂时的状态,在不写数据库的情况下,从节点最终会与主节点的数据保持一致,这种效应也被称为最终一致性。
理论上,复制的滞后并没有上限。正常情况下,这个延迟可能不到 $1s$ ,实践中通常不会有太大影响。但是,如果系统的性能抵达上限,或者存在网络问题,延迟可能会达到几秒甚至几分钟。
1.2.1 读写一致性
许多应用让用户提交数据,并在之后查询这些数据。用户向主节点提交数据后,之后的查询可能是在从节点上,在大多数情况下,这是个很合适的方案。然而,异步模式下,同步可能存在滞后,意味着,返回的数据是旧数据,在用户看来,代表他刚刚提交的数据丢了。
这种情况,我们需要读写一致性,或者叫写后读一致性。该机制保证,如果用户重新加载页面,总是能看到最近提交的更新。有几种方式可以实现读写一致性:
- 如果用户访问可能会被修改的内容,从主节点读取。这种方式要球一些方法在执行实际查询前,就知道内容是否改变。比如,社交网络上的用户信息通常只能由所有者编辑,因此,可以让用户总是从主节点读取自己的用户信息,从节点读其他人的用户信息;
- 如果应用的大部分内容都可以被所有用户修改,这种方式就不太有效了。此时需要其他方式来判断,比如跟踪更新时间,在更新后的一分钟内,总是从主节点读取;或者监控从节点的复制滞后程度,避免从滞后超过一分钟的从节点读取数据;
- 客户端记录最近更新的时间戳,附带在请求中,节点可以通过该时间戳确保返回该时间戳之后的更新,如果无法返回,交由另外一个节点处理。时间戳可以是逻辑时间戳 ( 比如日志序列号 ) 或者实际系统时间;
- 如果副本分布在多个数据中心,这时情况会比较复杂,应当将请求路由到主节点所在的数据中心,即使该数据中心可能离用户距离很远。
如果同一用户从多端访问 ( 比如Web
端和移动端 ),情况就更加复杂了,不仅要提供跨设备的读写一致性,还有新问题:
- 时间戳方式难以实现,因为一台设备并不知道另一台设备的操作,如果需要实现时间戳方式,元数据需要做到全局共享;
- 如果副本分布在多数据中心,无法保证来自不同设备的连接路由到同一个数据中心。
1.2.2 单调读
假定用户发起多次读取,读请求可能会被路由到不同节点,则可能会出现请求返回不同结果的情况。单调读一致性可以确保不会发生这种异常。单调读一致性比强一致性弱,但是比最终一致性强。读取数据时,单调读保证同一个用户一次发起的多次读取不会看到回滚 ( 数据不一致 ) 现象。一种实现单调读的方式是:确保每个用户总是固定读同一个节点,例如基于用户ID
哈希。
1.2.3 前缀一致读
用户发起两个请求,后一个请求的内容依赖于前一个请求,比如用户先写入 $1$ ,再递增为 $2$ 。从用户的角度,这个顺序没有问题。但是在其他观察者的角度,可能会存在逻辑问题,比如由于网络延迟,观察者先观察到后一个请求,这时顺序就变成了用户先写入 $2$ ,再写入 $1$ 。为了防止这种异常,需要引入前缀一致性,即对于一系列按照某个顺序发起的写请求,读取的时候也应该按照这个顺序。
这个问题是存在于分区数据库的一个特殊问题。如果数据库总是以相同的顺序写入,那么读取的时候看到的会是一致的序列。但是,分区数据库的不同分区之间是独立运行的,所以没有一个全局的写入顺序,导致用户读取的时候,会读到一部分新值和一部分旧值。一种解决方案是:将所有具有因果关系的写入都交给同一个分区完成,但是会导致效率大打折扣。
1.3 节点失效
1.3.1 从节点失效
从节点的磁盘上保存了数据变更日志,如果从节点崩溃,或者与主节点之间发生暂时性的网络中断,可以通过该日志获取故障前处理的最后一个事务,向主节点请求该事务之后的中断期内所有数据变更,将其应用到本地即可,之后就可以像正常情况一样持续接收来自主节点的数据流变化。
1.3.2 主节点失效
主节点失效,则需要选择某个从节点,将其提升为主节点。同时,客户端也要将之后的写请求发送给新的主节点。故障切换可以是手动的,也可以是自动的,自动切换的步骤如下:
- 确认主节点失效。主节点可能出于多种原因失效,比如系统崩溃、停电、网络中断等,并没有什么办法可以检测出失效原因,所以大多数系统都采用了超时机制判断。节点间会持续地互相发送心跳消息,如果发现某个节点在一段时间 ( 比如 $30s$ ) 内都没有响应,就认为该节点已经失效;
- 选举新的主节点。新的主节点可以通过一种共识算法来选举,或者由控制节点指定。候选节点最好是与主节点之间数据差异最小的,从而最小化数据丢失的风险;
- 重新配置系统使得新主节点生效。客户端需要将写请求发送给新的主节点,如果原来的主节点之后重新上线,需要将其降级为从节点,并认可新的主节点。
切换过程中可能存在很多变数:
- 如果使用异步复制,新的主节点相比原主节点的数据存在滞后,在选举完成后,原主节点又很快恢复并加入集群,接下来的写操作要怎么处理?新的主节点可能会收到冲突的写请求,因为原主节点并没有意识到发生了主节点切换,仍然会尝试同步其他从节点。一种常见的解决方案是,直接丢弃这些冲突的写请求,虽然这会违背数据更新持久化的承诺;
- 如果有其他依赖于数据库的组件在一起协同使用,丢弃数据的方案就很危险。例如,在
GitHub
的一个事故中,主节点在未完全同步的情况下失效,新主节点被选举,由于存在滞后,原主节点已经分配出去,存储在Redis
中的自增主键,被新主节点再次使用,导致了MySQL
与Redis
之间的数据不一致; - 某些情况下,可能会发生两个节点都认为自己是主节点的情况,称为脑裂。脑裂非常危险,它可能会导致两个主节点同时接收写请求,并且没有很好的解决数据冲突,导致数据被丢失或者被破坏。有些系统会通过强制关闭其中一个节点的方式,来解决脑裂问题。然而,如果设计或者实现考虑不周,也是有可能出现两个节点都被关闭的情况;
- 如果设置超时时间?超时时间越厂,确认主节点失效的时间也就越长,意味着总体的恢复时间就越长。例如,突发的负载峰值会导致节点的响应时间变长甚至超时,或者由于网络故障导致延迟增加,如果这时系统已经处于高负载情况,或者网络严重拥堵的情况,不必要的切换只会使情况更糟。
1.4 日志
1.4.1 语句复制
主节点记录执行的写请求语句,将该操作语句作为日志,发送给从节点。在关系型数据库中,意味着发送 $INSERT$ 、$UPDATE$ 等语句,从节点分析并执行这些语句,像来自客户端那样。基于语句复制存在一些不适用场景:
- 调用非确定性函数的语句,比如 $NOW(\ )$ 或者 $RAND(\ )$ 函数,这会导致在不同副本产生不一样的值;
- 依赖于自增列,或者现有数据的语句,这意味着所有副本必须按照完全相同的顺序执行,否则会产生不同的结果。进而,如果存在多个并发事务,也会影响事务之间的执行;
- 存在副作用的语句(如触发器、存储过程、用户定义函数等),可能会在不同副本上产生不同副作用。
这些问题可以采取某些措施来解决,比如主节点记录操作语句时将非确定性函数替换为确定结果。遗憾的是,存在太多边界条件,因此基于语句的复制通常不是首选。
1.4.2 WAL
通常,数据库的每个写操作,都会以追加形式写入WAL
日志中,可以使用完全相同的日志在另一个节点上构建副本,即主节点不仅将WAL
日志写入磁盘,还会通过网络发送给从节点。
基于WAL
复制的缺点是日志描述的数据结果非常底层,比如关系型数据库,WAL
会记录磁盘块的哪些字节发生改变,以及其他细节等,从而与具体的存储方案高度耦合,如果数据的存储格式发生了改变,之前的同步方式也会失效。如果复制协议允许从节点的软件版本比主节点更新,那么可以实现数据库软件的不停机升级;相反,如果要求版本严格一致,那么升级就只能以停机为代价。
1.4.3 行复制
基于行的复制将复制和存储引擎分离,复制与存储引擎采用不同的日志,这时复制日志称为逻辑日志。
- 对于行插入,日志包含所有相关列的新值;
- 对于行删除,日志应该以某种方式标识已删除的行 ( 通常是主键 );
- 对于行更新,日志应该以某种方式标识被更新的行,以及被更新列的新值。
如果一个事务涉及多行数据修改,就会产生多条行日志,并在之后跟着一条记录,指出该事务已经提交。MySQL
的binlog
支持该种方式。由于逻辑日志与存储引擎解耦,因此数据库存储可以向后兼容,主从节点可以基于不同版本的软件,甚至是不同存储引擎运行。
对于外部应用,逻辑日志也更容易被解析。解析逻辑日志也被称为变更数据捕获。
1.4.4 触发器
在某些时候,我们可能需要一种具有更高灵活性的复制方式,比如只复制一部分数据,或者从一个数据库复制到另一种数据库,或者指定冲突解决逻辑等,这种情况下,可以借助许多关系数据库都支持的功能:触发器和存储过程。触发器支持注册自己的应用层代码,当数据库系统发生数据改变时,自动执行这些代码。基于触发器的复制通常具有比其他方式更高的开销,也更容易出错。
2. 多主模式
目前为止,我们只考虑了单主节点的主从复制架构,这也是较为常见的方式。但是,也存在一些其他方式。主从复制的缺点很明显:只存在一个主节点,所有写入都必须经过主节点。如果存在某种原因,比如网络中断等,导致主节点无法连接,那么整个集群的写入都会受到影响。
2.1 多主节点
多主节点是一种对主从复制模型的扩展,允许配置多个主节点,每个主节点都可以接收写操作,并将数据更改转发到其他节点。这样,每个主节点既扮演主节点,也扮演其他主节点的从节点。
多主节点存在以下适用场景:
- 多数据中心:为了容忍数据中心级别的故障,或者在地理上更接近用户,可以把数据库的副本横跨多个数据中心。在常规的主从复制模式中,主节点只能放在其中一个数据中心,而多主节点允许在每个数据中心都配置一个主节点。这样每个写操作都可以在本地数据中心内快速响应,并且,同数据中心之间的网络性能和稳定性通常会更好,意味着用户的体验也会更好。但是这种情况也要考虑用户同时修改相同数据的情况,必须解决潜在的写冲突;
- 离线客户端操作:如果应用需要在网络断开后继续工作,那么应当在设备提供一个充当主节点的本地数据库,所有设备之间采用异步方式同步副本,滞后时间可能是几小时或者数天。从架构层面来看,这基本上等同于数据中心之间的多主复制,但是数据中心的概念缩小到了某个具体设备上,并且网络连接十分不可靠;
- 协作编辑:实时协作编辑应用允许多个用户同时编辑文档,虽然这不能完全等价于数据库复制,但是在很多方面具有相似性。用户编辑文档时需要将修改立即应用到本地副本,再通过异步方式同步到服务器,以及一起编辑文档的其他用户。要确保不会发生编辑冲突,应用程序需要将文档锁定,再进行编辑。这也意味着一个用户要编译时,必须先等另一个用户提交修改并释放锁。为了加快编辑效率,锁的粒度应该很小,单个按键甚至可以是全程无锁的。
2.2 写冲突
多主复制的最大问题是存在写冲突。
2.2.1 同步与异步检测
如果是主从复制,发生冲突时,第二个写请求会被阻塞,直到第一个完成,或者被中止。在多主节点模式下,这两个写请求都会成功,并且只能在之后的异步检测时才能发现冲突。理论上,同步检测冲突也是可行的,但是需要写请求等待所有副本完成同步,反而会失去多主节点的优势。
2.2.2 冲突避免
如果应用层可以保证对某个特定记录的写总是经过同一节点,就不会发生冲突。实践中,不少主节点复制模型锁实现的冲突解决方案存在瑕疵,因此,冲突避免通常是首选方案。比如,一个用户需要更新自己的数据,我们可以通过用户ID
哈希的方式,保证同一个用户的写请求总是发送到同一个数据中心。从用户角度来看,等同于主从复制。有时候,可能需要改变指定的节点,比如数据中心故障,或者用户漫游到另一个位置,需要换到更近的数据中心,这时这种方案就不再有效了。
2.2.3 一致收敛
主从模型下,数据更新符合顺序性原则,即对同一个字段的多次更新,字段最终值由最后一个更新操作决定。多主节点模型中,由于不存在这个顺序,所以最终值也是不确定的。如果每个副本都是按照其看到的顺序写入,那么数据最终可能会不一致。因此,数据库必须以一种收敛趋同的方式解决冲突,这也意味着所有更改被复制、同步之后,所有副本的最终值相同。一致收敛有如下解决方案:
- 为写入分配一个唯一
ID
,比如时间戳、哈希或者随机数,选择ID
最大的作为胜者,丢弃其他写入。如果基于时间戳,则称为最后写入者获胜。虽然这种方法很常见,但是容易造成数据丢失; - 为每个副本分配一个
ID
,制定一个规则,比如ID
较大的副本写入优于ID
较小的副本。同样的,这种方式也会导致数据丢失; - 通过某种方式将值合并,比如字母顺序排序等;
- 利用预定义格式记录和保留冲突信息,再通过应用层逻辑解决冲突,可能会经过用户。
2.2.4 自定义冲突解决逻辑
解决冲突最合适的还是依赖于应用层代码,所以大多数多主模型都支持用户编写应用代码解决冲突:
- 写入时执行:数据库系统在复制变更日志时检测到冲突,调用应用层代码解决;
- 读取时执行:写入时保留冲突值,读取时返回多个版本的数据,应用层可能自动解决冲突,或者将冲突交给用户解决,并将最终结果返回给数据库。
冲突解决通常针对某一行或者某个文档,而不是整个事务。如果有一个原子事务包含多个不同写请求,每个写请求仍然需要分开来考虑。
2.3 拓扑结构
多主模式下,写请求从一个节点传播到其他节点的路径,可以通过拓扑结构描述。最常见的拓扑结构是全链路拓扑:主节点将写入同步到其他所有主节点。除此之外,还存在着其他一些拓扑结构,例如,默认情况下MySQL
只支持环形拓扑结构,即所有主节点围成一个环,每个节点只接收来自前序节点的写入,只会写后续节点。星形结构也是另一种流行的拓扑结构,一个指定的根节点会将写入转发给所有其他节点,其他节点只会写该根节点。类似的,也可以对星形结构进行推广,拓展到树形结构。
环形结构和星形结构下,写请求需要通过多个节点才能到达所有副本,中间节点需要转发从其他节点收到的数据变更。为了防止循环,每个节点需要一个唯一ID
,并在变更日志中标识已经经过的节点ID
。环形和星形的问题是,如果某个节点发生故障,会影响其他节点的转发。可以通过重新配置的方式排除故障节点。全链路拓扑也有自己的问题,通常发生在网络拥塞的情况下,某些网络链路可能会比其他网络链路更快,导致变更日志的互相覆盖。
3. 无主模式
目前为止讨论的所有复制方法,都要求客户端先向主节点发送写请求,再由数据库复制到其他副本。主节点决定写顺序,从节点重放。一些数据存储系统则采取了不同的思路,放弃主节点,允许任何节点接收来自客户端的写请求。最早的数据复制系统就是无主节点的,但是在后来被渐渐遗忘了,当亚马逊采用Dynamo
系统之后,无主模式又成为了一种时髦的架构,Riak
、Cassandra
和Voldemort
都是受Dynamo
启发设计的无主节点的开源数据库系统,也被称为Dynamo
风格数据库。
在一些无主节点实现中,客户端直接将写请求发送给多副本。而在一些其他实现中,会有一个协调者代表客户端写入。与主节点不同,协调者并不负责维护写入顺序。
3.1 节点失效
假设存在一个三副本数据库,一个副本不可用。用户向三个副本发起写请求,有两个副本成功确认,一个副本无法处理。用户在接收到两个成功确认后,认为写入成功。之后,失效节点重新上线,由于滞后数据尚未同步,客户端读取该节点数据,会得到过期数据。一种解决方式是,客户端读取时,不是只向一个副本发起请求,而是并行向多个副本发起请求,并通过版本号来确定哪些值更新。
3.1.1 读修复和反熵
节点失效滞后重新上线,由于数据滞后,所以需要重新同步。Dynamo
风格数据库通常使用以下两种机制:
- 读修复:客户端并行读取多个副本时,可以通过版本号等方式,检测过期的返回值;
- 反熵:一些数据存储的后台进程会不断检测副本之间的数据差异,同步滞后的数据。与主从同步不同,这种方式并不保证顺序写入,并且会有明显的同步滞后。
要注意的是,并非所有的数据库都支持以上机制,例如Voldemort
没有反熵。
3.2 quorum
如果有 $n$ 个副本,写入需要 $w$ 个节点确认,读取必须至少要查询 $r$ 个节点,则只需要 $w + r$ > $n$ 就可以确保读取的节点一定会包含最新值。满足这些的 $r$ 、$w$ 值称为法定票数读 ( 或仲裁读 ) 或法定票数写 ( 或仲裁写 )。也可以认为 $r$ 和 $w$ 是用于判定读、写是否有效的最低票数。
在Dynamo
风格数据库中,$n$ 、$r$ 和 $w$ 通常是可以配置的,常见的是配置 $n$ 为奇数,$w = r = \lceil (n + 1) / 2 \rceil$ ,也可以根据需求灵活调整。$w + r$ > $n$ 定义了系统可容忍的失效节点数:
- $w$ < $n$ ,如果一个节点不可用,仍然可以处理写入;
- $r$ < $n$ ,如果一个节点不可用,仍然可以处理读取;
如果可用节点数小于 $r$ 或者 $w$ ,写入或者读取就会返回错误。通常 $r$ 和 $w$ 会被设置为简单多数节点,但是quorum
并不一定要求多数,只要读写节点之间存在重合节点即可。即使在 $w + r$ > $n$ 的情况下,也可能存在返回旧值的边界条件:
- 采用了 $sloppy\ quorum$ 的写操作节点和读操作节点可能完全不同,从而无法保证存在重合节点;
- 如果两个写操作同时发生,无法确定顺序,这时唯一安全的解决方案是合并写入。如果根据时间戳选择,会存在时钟不一致的问题;
- 如果读写同时发生,写操作可能只在一部分副本上进行,读取时返回的是新值还是旧值并不确定;
- 如果某些副本上写入成功,另一些副本写入失败,并且总成功副本数少于 $w$ ,已成功副本不会回滚,意味着之后的读操作可能返回已失败的新值;
- 如果具有新值的节点失效,之后重新加入集群后恢复的数据来自旧值,那么新值的副本数就会少于 $w$ ;
quorum
也存在一些线性化读取的问题。
Dynamo
风格数据库是针对最终一致性场景设计的,$w$ 和 $r$ 不应该是一种绝对保证,而是一种可以灵活调配的配置。配置适当quorum
的数据库系统可以容忍某些节点故障,也不需要执行故障切换。它还可以容忍某些节点变慢,因为请求并不需要等待所有 $n$ 个节点响应。对于一些需要高可用和低延迟的场景,还可以容忍偶尔读取旧值。
但是,quorum
并不是总会提供高容错能力,一个网络中断可以很容易切断客户端到多数节点的连接。在断掉连接的客户端看来,相当于集群全部失效。在一个大规模集群中,客户端可能在网络中断时还可以连接到某些节点,但是这个节点数量又不能满足仲裁,数据库就面临一个选择:直接返回错误还是接收写请求并写入暂时可访问的节点?后一种方式称为宽松的仲裁:读写仍然需要 $r$ 和 $w$ 个成功的响应,但包含了那些并不在先前指定的 $n$ 个节点的响应。一旦网络问题解决,临时节点需要把接收到的所有写入发送给原始节点,即数据回传。$sloppy\ quorum$ 并非传统 $quorum$ ,更像是为了保证数据持久性设计的一个保证措施,除非回传结束,否则无法保证客户端一定能从 $r$ 个节点中读到新值。
4. 并发写
Dynamo
风格数据库允许多个客户端对相同主键发起写操作,意味着写冲突的存在。此外,读修复或者反熵也会导致写冲突。一个核心问题是,由于网络延迟或者部分节点失效,请求在不同节点上的顺序是不同的。如果节点每次收到写请求时就直接覆盖原有值,那么这些节点将永远也无法一致。
4.1 最后写入者获胜
一种实现收敛的方法是,每个副本总是保留最新的值。假定每个写请求都会最终同步到每个副本,我们需要一个明确的方法来确定哪个写入是最新的。多个客户端向节点发送请求时,一个客户端无法发现另一个客户端,也不知道哪个请求先发起。既然无法确定请求的自然排序,我们可以强制以某种方式排序,比如以时间戳排序。这种解决方法称为最后写入者获胜 ( $last\ write\ wins$ , $LWW$ )。LWW
可以实现最终收敛,代价是数据持久性。如果同一个主键有多个并发写,即使这些写都返回成功,但是最后只会有一个值获胜,其他的将被丢弃。
在一些场景,比如缓存中,覆盖写是可以接受的。如果覆盖、丢失不可接受,LWW
就不是很好的解决方案了。确保LWW
安全无副作用的唯一方法是,只写一次然后将写入值视为不可变。例如,Cassandra
的一个推荐使用方法就是使用UUID
作为主键,这样每个写操作都针对不同、系统唯一的主键。
4.2 Happens-before
如何判断两个操作是否并发,可以通过以下两个例子分辨:
- 一个插入操作发生在增量修改操作之前,并且增量操作基于插入的行,也就是这两个操作间存在因果关系;
- 多个设备同时写入,一个设备并不知道另一个设备是否也对同一个主键进行写入,即操作间不存在因果关系。
如果操作 $B$ 知道操作 $A$ ,或者 $B$ 依赖于 $A$ ,则称 $A$ 和 $B$ 之间存在Happens-before
( 先序发生 ) 关系,这就是定义并发的关键。因此,对于两个操作 $A$ 和 $B$ ,存在三种可能:$A$ 先发于 $B$ 、$B$ 先发于 $A$ 、$A$ 和 $B$ 并发。如果属于并发,就需要解决潜在的冲突问题。
4.3 合并并发写的值
通过版本号,服务器可以在不依赖值的情况下判断写是否并发。流程如下:
- 服务器为每个主键维护一个版本号,每当主键新值写入时递增版本号;
- 客户端读取时,服务器返回所有值以及最新版本号,并且在客户端写之前,要求必须先进行一次读;
- 客户端写主键,写请求必须包含之前读到的版本号、读到的值和新值合并之后的集合。写请求可以返回当前值;
- 服务收到写请求时,根据版本号,选择性地覆盖该版本号或者更低版本的所有值 ( 这些值已经被合并到更高版本集合中 )。
如果写请求带有版本号,说明它修改的是以前的状态。如果没有版本号,将会与其他操作同时进行,不会覆盖已有值。
上述算法适用于单副本的情况,但是对于多副本的情况,它们之间的版本号并不相同。为了保证一致,我们需要为每个副本和每个主键均定义一个版本号。每个副本在写入时增加自己的版本号,跟踪从其他副本看到的版本号。所有副本的版本号集合称为版本矢量。当读取数据时,版本矢量会被返回给客户端,并在随后写入时包含在请求中一起发送给数据库。