主从复制要求所有写请求都经由主节点,任何从节点都只能接受只读查询。对于读操作密集型的负载,这是一个不错的选择,创建多个从节点,将请求分发给这些从节点从而减轻主节点负载。在这种体系下只需要添加更多的从节点就可以提高读请求的服务吞吐量,但是这种方法实际上只能用异步复制,因为如果是同步复制则单个节点故障就会导致整个系统无法写入,而从节点越多发生故障额概率就越高。异步复制的一个问题在于从节点和主节点可能存在不一致性,而且这种不一致性没有上限,如果系统已经接近设计上限或者网络存在问题,则滞后的时间可能长达几秒甚至几分钟经不等
当滞后的时间太长时,导致的不一致性就是个实实在在的问题,本文将重点介绍三个复制滞后可能出现的问题并介绍相应的解决思路
读自己的写
读自己的写是一个非常常见的应用场景,特点是读紧随写之后,例如用户发表一个评论,发表之后就要立即在评论列表里显示出这条评论。然后对于异步复制来说,由于可能的复制滞后,所以可能导致用户在写入之后立即查看数据会在从节点查不到数据,看上去效果就像是数据丢失了,这是个很严重的问题
对于这种情况,我们需要“写后读一致性”,也称为读写一致性。该机制可以确保用户重新加载页面后能看到自己最近的提交(但对其他用户不做任何承诺,其他用户可能会在稍后才能看到该用户的提交内容)。基于主从复制的系统如何实现读写一致性呢?有以下几种可行的方案:
- 如果用法访问的内容是可变内容,则主节点读取,反之在从节点读取。这里的前提是知晓内容是可修改的。里如,网站的用户信息只有所有者能编辑,其他人无权编辑,则所有者从主节点读取,其他用户到从节点读取
- 如果应用的大多数内容都可以被修改,则上述方案会导致大部分读都是从主节点从而丧失了读操作的扩展性。此时需要用其他方案来判断从哪里读取。例如可以跟踪最近更新的时间,如果在更新后一分钟之内,则从主节点读取
- 客户端可以记住最近更新的时间戳并附带在请求中,据此信息系统可以确保对该用户的读服务应该至少包含改时间戳的更新。如果不够新,要么交由另一个从节点处理,要么等待从节点接收到了最近的更新
- 如果副本分布在多数据中心,那么情况更复杂,需要先把请求路由到主节点所在的数据中心
如果要考虑到跨设备访问数据,例如一个桌面web
浏览器和一个移动端app
,情况会更复杂,此时需要用户在某个设备上输入信息然后在其它设备上也可见刚刚所输入的内容。在这种情况下需要考虑一些问题:
- 记住用户上次更新的时间戳等元数据需要做到全局共享,跨设备可访问
- 如果存在多数据中心,则无法保证不同设备都连接到同一个数据中心
单调读
用户不做写操作,只读,在复制滞后的情况下依然会出现问题。
假定用户从不同副本进行了多次读,按上图所示,用户每刷新一次页面,请求都可能被随机打到某个节点,导致不同访问得到的结果不同。这里我们需要做到单调读一致性,该一致性比强一致性弱,比最终一致性要强。
实现单调一致性的方式是,确保每个用户总是从固定的节点读取,例如可以根据用户id哈希的方法而不是随机选择副本。但如果副本失效,则用户的查询必须更新路由到另一个副本
前缀一致读
复制滞后会导致因果反常,例如正常情况下是先有问后有答,但复制滞后的存在,可能导致一个第三方的观察者先看到了答后看到了问,导致因果错位,逻辑混乱。防止这种异常需要引入一种保证,即前缀一致性。该保证是说,对于一系列按照某个顺序发生的写请求,那么读取这些内容的时候也会按照当时写入的顺序。这是分区数据库中出现的一个特殊问题,后面介绍分区的时候我们会详细介绍。大体上来说,如果数据库总是以相同的顺序写入,则读取总是能看到已知的序列,不会发生这种异常。但在许多分布式数据库中,不同的分区独立运行,因此不存在全局写入顺序,这就导致当用户从数据库中读数据时,可能会看到某部分旧值和另一部分新值
解决方案是确保任何具有因果关系的写入都交给一个分区来完成,但这样会导致实现效率不高。现在有一些新的算法可以显示的追踪事件因果关系,后面我们会深入介绍
复制滞后的解决方案
我们在进行系统设计时,需要事先思考这样的问题:如果复制延迟增加到几分钟或几小时,那么应用层会怎么样?如果答案是没有影响,那么就无所谓。但如果是会出问题,则在设计时就需要考虑更强的一致性
事务是数据库提供提供更强正确保证的一种方式,单节点上的事务已经很成熟,但转到分布式时(支持复制与分区)很多系统选择了放弃支持事务。关于事务,有更多的内容需要讨论和研究,后面我们会专门进行介绍