《数据密集型应用系统设计2》--数据复制与数据分片
这篇文章是数据密集型应用系统设计2读书笔记的一部分。
目前包含的文章有:
- 数据复制与数据分片(本文)
- 事务一致性和隔离性
数据复制 (replication)
数据复制是分布式系统中一个很重要的概念,而且也正是由于复制的存在,引入了很多不一致问题。同时,复制也有多种实现方式,综合了解每种实现方式解决的问题更利于在选型时认准不同组件的区别和适用条件。总结来说,有以下这几种复制方式:
- 主从复制:最好理解的一种,写请求只能由主副本处理,然后主副本将修改同步到所有从副本中。这种方式遇到的不一致和冲突问题最少,但可能存在写入瓶颈,不过写入瓶颈可以考虑用分片解决,这是后话。
- 多主复制:拥有多个主节点,主节点们都可以处理写请求,但通常会专注于一部分特定的写请求,交集很少。当某个主节点处理完写请求之后,会将这次修改同步到其它主节点中。常见的多主复制例子有多数据中心同步(同一个数据中心内部还可以使用基于主从复制的方案),比如同一个APP地理位置位于美洲用户的请求由美国数据中心处理,地理位置位于欧洲用户的请求由欧洲数据中心处理,同时两个数据中心会同步数据,这样可以确保进行跨国旅行的用户数据一致且有良好的使用体验(数据就近写入,延迟较低,不用绕地球大半圈到注册地的数据中心)。
- 无主复制:不再拥有主节点的概念,每个副本都是平等的。写入时,用户需要写入多个副本,读取时用户也需要读取多个节点,并且读取副本中最新的数据版本。读取的过程有点类似于办公室吃瓜,比如你希望知道“XX明星的女朋友”,同事A说,一年前是YY,同事B反驳说昨天看了个新闻,爆出和ZZ有恋情了,那我们选择同事B的答案,因为这是最新的版本。用户可以配置写入时需要写多少个副本、读取时要读取多少个副本,来满足对读取最新数据的要求。
简单起见,我们可以认为现在讨论的情况是数据没有分片,完整地储存在一个节点中,各个从节点也是也是存有完整数据的副本。如果数据是分片的,本节所述的原理也适用于每个分片内部的复制逻辑。因此将复制和分片解耦讨论更能降低理解的难度。
主从复制
主从复制最简单,因此我们可以从主从复制的实现细节入手,来理解和复制相关的概念和可能出现的问题。在这一节提到的几乎所有概念,都适用于所有复制方案。
同步和异步复制
复制的一个关键概念是,主副本是同步地将最新的修改复制到从副本,还是异步地复制。这里的同步指的是主副本需要收到其它副本成功写入的响应后,才能通知客户端写入成功,因此一旦客户端成功写入一个修改,其它副本也已经同步了这一更改;而异步指的是在从副本写入成功之前,就可以返回客户端写入成功,因此客户端成功写入一个修改时,只能保证主副本和一部分从副本(取决于配置)同步了最新的修改,如果此刻从其它副本读取,很可能读到的还是旧的数据。
从两种复制方式的优点上看,同步复制增加了副本间数据的一致性;而异步复制更能容忍节点失效,并可以降低客户端请求的延迟。
同步复制和异步复制的概念也适用于后面的多主复制和无主复制。
全量复制与增量复制
通常各节点健康、持续运行的过程中,节点之间(包括主从或者非主从复制)的复制都是增量复制,只需将最新的写入同步到其它节点即可。但是当出现了网络的不稳定、从节点下线等情况,主副本可能有较多的修改没有同步到从节点,所以主副本需要缓存这部分未同步的修改记录。
假如一个副本落后主副本过多(比如副本已经下线很久),而随着服务运行时间增加,主副本记录的修改记录会占据很大的空间,主副本需要时不时清理过老的数据。因此在将数据同步到新节点或者落后主节点过多的节点时,通常主节点中已经没有完整的写入记录,这时就会使用全量复制来将从节点同步到和主节点相同(或者接近相同)的状态。
全量复制与增量复制通常也会配合使用,比如在增加新副本时。主节点首先生成一份当前的数据库快照,并记录接下来提交的更改。从节点先恢复到快照时的状态,然后再按顺序应用快照后的修改,恢复到和主节点相同的状态。
复制日志的实现方式
主从副本之间同步,需要主节点将数据变更记录以某种格式记录下来(由于是增量更新,也可以称之为日志),再发送给从节点。通常有以下几种日志格式:
- 基于执行语句的日志:比方说最简单的就是将客户端提交的每句SQL都按顺序记录下来,再在从节点上复现。它的好处在于比较节约日志存储空间,因为通常一条SQL语句可以修改多行数据。它的问题在于客户端可能执行一些非确定行为函数,比如“当前时间”,“随机数”等,导致从节点与主节点状态不一致
- 直写日志(write-ahead log,WAL)传输:WAL通常用于在基于BTree索引的存储上,维护一个较小的WAL缓冲区来存储事务日志(而不是立即插入到BTree中),以较小的性能损失换取持久性保障。因此数据库系统可以直接将WAL当成复制日志,传输给从节点。它的问题在于WAL与存储引擎紧密耦合,格式很可能随着版本升级而变化,造成兼容性问题,同时也对第三方复制工具不够友好。
- 基于行更改的逻辑日志:将额外定义的日志格式用于复制日志,通常每一条日志包含一行的一次更改(同时也需要包含足够的信息来确定修改的行,比如行主键)。比如MySQL就定义了这样的逻辑日志用于复制,称为二进制日志(binlog)。这种格式通常便于第三方复制工具解析,比如业界就有很多利用MySQL二进制日志来同步数据库变更记录的工具。
处理节点失效
复制通常是为了避免单点失效造成系统失效的问题,因此节点失效是复制算法必须考虑的问题。节点可能有各种理由失效,比如滚动升级、网络故障等。接下来分别讨论主从节点失效的问题。
从节点失效问题比较好解决,如果从节点落后的时间比较短,主节点依然保留着相关的更改记录,那从节点恢复后可以直接同步变更记录;如果从节点落后的时间太久,主节点上已经没有节点失效后完整的变更记录,那就将从节点视为加入的新节点,执行全量复制(配合增量复制来同步全量快照之后的变更)。
主节点失效是大部分节点失效切换算法主要考虑的问题。通常由以下几个部分组成:
- 确认主节点失效:通常让节点之间定期发送心跳包,当超过一定周期没有收到心跳包,即认为节点失效
- 选举一个新的主节点,通常已经同步了最新更改的副本最容易成为主副本
- 确认新的主节点,写请求将发送给新的主节点。如果旧的主节点重新上线,副本集可以强制让主节点转换为从节点。
在纯异步复制的配置下,失效的主节点还未同步到其它副本的数据会丢失,这可能会打破事务的持久性保证。
复制滞后问题
最后我们讨论复制滞后可能带来的不一致问题。
许多主从复制系统只支持从主副本读取(比如Kafka),另外一些主从复制系统为了提高读性能,支持客户端从从副本读取。支持从从副本读取的系统通常会出现复制滞后带来的不一致问题。
想必大家一定听说过著名的BASE理论,描述了分布式系统的一些设计原则,给那些不支持ACID的NoSQL开脱。其中最广为流传的概念就是“最终一致性”,也就是说假如系统不接受任何写入,“最终”最终系统内的所有副本都会收敛到一致的状态。然而最终一致性给出的保证非常弱,多久才是“最终”呢?另外,就像事务的隔离性有多种级别,“最终一致性”有没有更细粒度的一致性级别呢?这一节列出了多种弱一致性级别,以及实现这些一致性的可能算法。
写后读一致性:保证能读自己先前的写入
在异步复制系统中,如果写入后立刻读取从副本的数据,容易出现写后读不一致。比如刚将自己的头像换了,再点开可能发现显示的还是之前的头像。这是可能时由于最新的写入还没有同步到所有副本,而后来读取的是这些存有过时信息的副本。
为了实现写后读一致性,可以让用户在写后的一段时间内都只读取主副本,或者对于这些需要较强一致性数据的场景使用同步复制或只从主副本读取。
单调读一致性:确保读取的数据一定比之前读取的新
当用户可以从任意副本读取时,可能会出现第一次从副本1,第二次同从副本2读取。假如这两个副本的数据更新状态不一致,就可能导致第二次读取到的反而是更老的数据。比方说你在刷文字版的体育直播,刚刷到XX进了一个球,一刷新页面,可能这行字又没了,过一段时间才会出来。这个情况就称为单调读不一致。
实现单调读一致性的简单方法是确保客户端每次都只从一个副本读取。
前缀一致性:保证读取更改的顺序和写入顺序相同
这里的“前缀”翻译得有点生硬,它指的是客户端读取到的事件顺序与写入顺序一致。前缀一致性问题通常发生在分片的系统中,比如Kafka,同一个topic有多个partition,通常只能保证单个partition内读取的消息顺序与写入一致。生产者通常会将消息写入多个partition,而消费者从多个partition消费时,很可能丧失了顺序,比如将1,2,3,4写入两个分区,其中13在一个分区,24在另一个分区,消费者读取时,1234、1324、2413、1243等都是可能出现的实际消息顺序。因此为了达成前缀一致性,通常要求写入请求都会按序写入某一分片,读取时也需要从单一分片读取。
和其它弱一致性不同,前缀一致性是分片系统(在本文后半部分讨论)而不是复制系统需要解决的问题,放在复制这一节解释只是为了将相关的知识归纳到一起。
多主复制
分布式系统将节点失效视为常态,而单主节点的复制方案要求所有写入都需要经过单一节点,假如由于一些网络分区问题无法连上主节点,或者即使能连接上,延迟也太高(比如中国的客户需要连接某个美国的app)。在这种情况下,可以使用多个主节点来保证不同地区的客户都有较低的延迟。
多主节点复制会大大增加系统的复杂性,因此在设计多主节点复制的系统时,通常有如下考虑:
- 不同的主节点通常位于不同地区的数据中心,因为处于相同地区的主节点既没有解决网络问题,又引入了很高的冲突概率
- 主节点之间通常是异步复制的,因为多主架构出现的原因就是为了缓解网络不稳定带来的可用性影响,如果同步复制,无法解决网络波动带来的影响
- 多主复制很可能带来写入冲突问题,比如同一个账号在中国和美国都登录了并修改了同一项用户配置,那就发生了写入冲突。所以通常会尽量避免让同一个用户写入多个主节点,比如可以在服务端路由到账户地区所属的数据中心来避免竞争条件,并且降低跨地区访问的网络延迟
多主复制拓扑模型
- 环形:各节点首尾相连成一个环,每个节点只将修改同步到下一个节点,下一个节点将前一个节点的修改附加自己的修改,同步到它的下一个节点。每个节点要过滤掉自己上一次发送的更改,避免重复写入数据。
- 星型:有一个中心节点,其余节点都与中心节点相连,将修改同步到中心节点,然后再由中心节点同步到其它节点。
- 全连接型:每个节点都会将修改直接同步到其它所有节点。
环形和星型的缺点是:
- 由于节点两两之间并不完全直接互联,因此一条变更记录可能要通过多次传递才能复制到目标节点,所以需要记录源id,避免重复复制。
- 如果环形复制中的一个节点挂了、或者星型复制中的中心节点挂了,很可能导致复制算法失效。
多主复制案例
除了多区域数据中心、多区域容灾的例子,以下的一些案例也是多主复制适用的情况。
可离线使用的软件
可离线使用的软件的同步功能,也是一种多主复制模型。比如一些电子书APP支持离线看书,而当APP再次上线时,可能需要将用户最新的笔记和进度同步到服务器上,假如用户在多端使用了这个APP,就可能造成写入冲突问题。
在线协作软件
在线文档允许多人同时编辑一个文档,git允许多人开发同一代码仓库,都是典型的例子。
如何解决写入冲突
首先我们要尽量避免写入冲突。比如可以在多数情况下将多主复制转化为单主节点问题,比方说永远把用户的请求路由到地区所属的数据中心。但是它无法永远生效,比如用户刚注册时,并没有地区信息;比如用户修改了地区信息,那在地区修改没同步到所有主节点的过程中就可能存在写入到两个数据中心的风险;比如网络分区问题导致用户无法访问自己账号所属的数据中心;比如在线协作软件必然会出现多位用户修改同一文档的情况……虽然写入冲突无法避免,但是我们可以优先考虑避免冲突,让冲突出现的数量最小化,然后再解决一小部分无法避免的冲突。
为了达成最终一致性,我们要求解决冲突的算法面对同样的修改时,能让所有副本都收敛到相同状态。比如“最后写入获胜”这种算法,通常按照某个时间戳字段(通常是服务端确认写入的时间戳)来决定谁是最后的写入。那有没有无法收敛到最终一致的算法呢?比如如果当冲突出现时,我们随机选择生效的写入,那很可能造成不同节点的最终状态不一致,打破最终一致性。
不同场景的写入冲突差异很大,为了自动解决冲突,通常先根据不同的数据结构的特点设计不同的合并冲突算法,比如:
- 如果是计数器这样增量相加有效的数据,我们就将增量相加;
- 如果是哈希表,则只用merge对相同key的修改
然后再针对不同的数据类型做冲突合并。比如对于字符串修改,比较优雅的算法有,操作转换和无冲突数据类型两种:
- 操作转换(Operational Transformation):每次修改记录的是发生修改的下标,比方说操作1是“在位置0写入了字符A”,操作2是“在位置1写入字符B”等。假如发生了这两次操作,操作1已经生效了,算法检查到由于位置0在位置1之前,操作1会影响操作2的准确性,因此它会把操作2转换为“在位置2写入字符B”.当然,假如操作2先生效,它不会影响操作1的准确性;假如操作1是替换某个字符,那也不会影响插入操作的准确性,这两种情况下都不需要转换后面发生的操作。
- 无冲突数据结构:CRDT算法通常会给每个字符一个id,然后会记录每次修改都是相对于哪个已有字符id进行的,比如在id为1的字符后面插入了字母a,在id为2的字符后面插入了字母b等。这样当写入冲突发生时,只需按顺序执行发生的修改即可,无需手动合并冲突。
我们看到以上两种方式,将修改的范围最小化到字符级别,避免了粗粒度冲突检测。粗粒度的冲突检测是指,比方说对于git,如果修改同一行,或者在两行之间插入了内容,git都会要求手动解决冲突,说明git的冲突检测基本上是以行为级别。但这并不是说git的冲突解决算法就很差劲,git和在线文档的使用场景不同:git的每个提交通常都包含多行更改,而且在代码中,每一行代码都有特定的语法,自动解决行内的修改冲突反而会弄巧成拙,造成很多语法错误。
无主复制
除了单主节点和多主节点复制,我们还可以干脆把主节点去掉,实现无主节点的复制。无主节点场景下,一致性主要靠客户端保证。简单来说:
- 写入时需要写入多个副本,客户端可以配置需要同步写入副本的最少数量。比方说有3个副本,用户同步写入2个就算成功,另外1个副本异步写入
- 读取时用户也需要读取多个副本,并且使用副本中最新的数据版本。客户端也可以配置读取时需要读取的副本数量。比方说还是3个副本,客户端可以配置读取到2个副本就结束读取过程
再谈一致性问题
看到这里,我相信你一定会有问题,比如
- 如果客户端异步写入失败,那所有副本都会收敛到一致状态吗?
- 听起来读到的副本数据很可能是不一致的,那无主复制是不是没法保证读取的数据的一致性?
先回答第一个问题,除了客户端主动向副本写入数据之外,副本之间也有同步过程,当发现数据不一致时会将最新的更改同步到整个集群。那怎么发现数据不一致呢?大致有以下两种算法:
- 读时修复:客户端读取数据时,如果发现多个副本之间存在不一致情况,那客户端就会将最新的数据写入不一致的副本中
- 背景线程扫描:每个副本都会有背景线程在扫描数据之间是否有不一致情况。为了减少实现难度,通常这种背景同步也只是定时随机选取一部分数据进行对比
再回答第二个问题,无主复制是不是很难保证一致性?其实我们要理解为什么需要“一致性”。我们在主从复制和多主复制中多次提及副本之间的一致性,是因为通常在主从复制和多主复制场景下,客户端都只从一个副本读取数据,因此多个副本之间是否一致性就会对读到的数据有很大影响。我们要看到,一致性是为了保证读取到的数据是最新写入的版本,我们并不是为了一致而一致。在无主复制中,客户端通常从多个副本读取数据,为了保证读到最新的写入,我们可以多读几个副本,比如说最极端的情况——等待所有副本返回,那我们就一定能读到最新的写入。当然我们也可以换种思路,写入时就保证写入所有的副本,那读取时即使只读取一个副本也可以读到的是最新的。
同步写入副本数和最小读取副本数
刚刚我们定性地分析了读写副本数对读到最新数据的影响,比如当读/写所有副本时会发生什么。现在我们进入经典的八股环节,分析同步写入和读取副本数该如何设置,有何影响。我们取读取副本数为r
,写入副本数为w
,副本总数为n
对读取最新数据的影响
我们常见的八股都说,最好将写入副本数和读取副本数设置为恰好是半数以上(比如当副本数为3,则要求写入2,读取2),即可保证每次读取到读到都是最新的写入。在理解了无主复制的大致原理之后,我们可以认为这句话是对的,因为读取的两个副本中,至少有一个是同步写入的,包含最新的修改,所以一定能读取到最新的修改。
另一个常见的八股说,只需要保证写入和读取副本数之和大于副本总数就行,这句话更通用,因为保证能读取到最新数据的条件是读副本和同步写入的副本有交集,如果读写副本数加起来大于副本总数,那说明两个集合中,至少有一个是重合的,因此能读取到最新的修改。这就好像在问,一个屋子里至少有多少个人,就能保证有两个人的出生在在同一个月?显然这个问题的答案是13.
对可用性的影响
除了能读到最新修改的考虑之外,可用性也是选取读写副本数的重要考虑。比如共有3个副本,同步写2个就算成功,那这个配置可以容忍1个副本失效;读也是同样的道理。因此在保证能读取到最新数据的条件下,将读写副本设置为半数以上是比较中庸的配置,能容忍半数以下的节点失效,因为如果一个集群需要进行读写,那根据木桶原理,我们认为这个集群可用时的最大失效节点数是min(r,w)
,除非这个集群只读不写或者只写不读。因此我们尽量让读写可容忍的失效节点数相等。
如果不要求读取到最新数据,也可以减少r/w
的数量,这样可以提高可用性和降低延迟。
r+w>n
,一定能读到最新数据吗
看起来半数保证很简单也很靠谱,但是在面对特殊情况时,最新数据也可能丢失,或者无法读取,比如
- 某个包含最新写入数据的节点突然下线了,导致实际的
w
减小,不满足w+r>n
条件 - 当写入操作在某些节点失效,但是又无法回滚时
- 当出现并发写入时,可能出现丢失更新或者写入冲突
处理节点失效
无主复制无需考虑主节点宕机的问题,因为根本就没有主节点。因此也无需繁杂的选举算法来选举主节点,如果节点失效那就将其下线,如果新节点加入那就复制所有数据。
主从复制、多主复制和无主复制的比较
对于跨区域、多数据中心场景,多主复制和无主复制皆可适用。相对来说多主复制用的比较多,因为客户端可以只和一个副本相连进行写入,而无主复制则要与多个数据中心的副本相连,跨区域的网络波动远远大于区域内部的网络波动。
对于同一数据中心内部的副本复制,主从复制更好提供一致性数据(比如永远从主副本读),但是,而且在可以容忍读取旧数据的情况下,。
- 可用性:主节点的可用性基本上决定了主从复制的可用性,因此更容易出现单点故障导致的集群不可用问题,无主复制对少量节点失效的容忍度很高
- 读性能:如果不要求读取最新数据,那二者性能相同,客户端都可以从任意一个节点读取。如果要求读取最新数据,那无主复制的读性能相对更高,因为客户端可以只等一部分副本返回,但是个人觉得这个优势不太明显。
- 一致性:主从复制可以设置为永远从主副本读,但容易造成主副本过载;无主复制可以设置为
r+w>n
,而且不容易出现单节点过载问题。
因此总体来说,无主复制的最大特点是能容忍节点失效的同时,兼顾读到最新数据,但是其一致性保证不如具有主节点的复制直观,也容易让客户端的设计更复杂。大部分情况下,单区域内使用主从复制,多区域内使用多主复制。
数据分片 (shading)
这一章主要讨论的是OLTP系统的数据分片,主要关注点查询。只有在分布式二级索引这一节会讨论范围查询。
分片的优劣:
- 优点:使系统具有横向扩展能力,尤其是基于key-value存储的数据
- 缺点:分片一定是按照某个字段(通常是主键)将数据拆分为多份,因此如果按二级索引查询的话,无法避免要在多个分片上执行查询语句
分片依据
和索引一样,数据分片也主要分为哈希和范围,它们的优劣也类似:范围分片让范围查询更高效,而哈希则可以在没有先验知识的前提下分得更均匀些(如果数据在分片key上没有严重的数据倾斜问题,那么哈希通常可以在没有任何数据分布信息的情况下分得很均匀;但范围分片很可能需要计算数据的分布特征之后才能分得很均匀)。
范围分片
范围分片更容易造成热点问题,比如将传感器数据按时间戳分片,那最新时间的写入通常会集中于某个分片上。在查询时,通常最新的数据也会最经常被扫描,因此存有较新数据的分片的负担最重。
范围分片通常需要根据时间来调整范围,以适应数据分布的变化。比如当某个分片数据量过大或者吞吐量过大,它就可以分裂为两个分片(很像BTree的分裂机制对吧?);当然当分片的数据量太少时,也可以缩容。这样系统的分片数量就会一直维持在一个合理的范围,减少系统处理分片时的额外成本(比如原本在一个分片内的查询不用在两个分片上同步执行)。但是重新分片的操作的成本也很高,需要将数据复制到另一个节点上。
哈希分片
最简单的计算哈希值与分片关系的方式,是通过哈希值取模来计算应该放到哪个分片上。但是这样会导致扩容操作耗费的资源巨大:几乎所有分片都要重新分配数据!所以一种简单的方式是,先取一个非常大的数N作为取模的除数,比方让N=100,000,然后让每个分片都认领取模操作后,一定范围内的分片。比如让mod = hash mod N
, mod
值为1-1000的放在分片1,1000-1500的放在分片2等。这种分片方式就使得分区再平衡(rebalancing)操作的成本大大降低,一个分片的再平衡操作不会影响其它分片的数据;如果涉及所有分区的再平衡操作,那也只需将每个分区内的一小部分迁移到其它分片。
比如像Redis,每个分片都存有一个记录mod
值(Redis称之为slot,槽)到分片的字典,也就是说每个分片可以持有任意的槽,这些槽可以是不连续的(不是非得存1000-1500这样的连续范围,也可以存1,3,5,7,9这样的不连续值)。
将用户请求路由到正确的分片上
一旦使用了分片机制,用户请求很可能会发送到没有存有目标数据的分片上,比如用户连接到了分片1,查询某个特定id的数据,但这个数据在分片2上。大约有三种机制来处理这种路由问题:
- 允许用户访问任意节点,并由该节点转发用户的请求到正确的节点上:ES使用了类似的方式,因为ES查询请求通常是基于二级索引而不是分片id的,查询本身就需要在所有分片上执行、汇总并返回给用户,所以ES让每个节点都可以成为“协调者”,来分担处理用户请求的压力。
- 让用户首先连接一个代理节点,代理节点负责将用户请求转发到正确的节点上:相当于将1#中处理用户请求的功能抽取出来成为单独的服务。很多SQL数据库分片中间件使用类似的方式,在MySQL这类单机数据库上构建分布式数据库。ES也可以配置让一些节点只负责处理用户请求,而不进行数据存储,从而实现2#的方案
- 用户只能访问正确的节点来获取数据:方案可以是由用户端缓存、计算分片和节点的映射关系,或者由服务端提供正确节点的信息。比如Redis在用户请求错误的节点时,会返回重定向错误,并告诉用户正确的节点是哪个,客户端会自动重定向到正确的节点
分布式二级索引
单机运行的数据库通常支持丰富的索引,比如BTree索引、哈希索引等。而分布式数据库的二级索引就复杂得多。
和单机数据库一样,分布式数据库也有主键的概念,而且通常是按照主键进行分片的。这意味着如果按照主键进行查询,则比较好进行优化,让查询只在存有数据的分片上进行,比如按id进行点查询就只需要查询一个分片。但二级索引就比较复杂了,比方说像ES这样的查询引擎,用户可能查询各种关键字,书本的标题、价格、作者等。由于数据是按照id分片,满足条件的数据可能存在于多个分片中,需要查询多个分片就不可避免。二级索引通常有两种组织方式:
- 局部索引:每个分片负责维护自己分片内数据的二级索引,不会存储其它分片的索引信息。因此可以将每个分片都看作是独立的数据库,每个分片都可以查询该分片内是否存有指定的数据库。当服务端接受用户查询二级索引时,它会将这些查询分发到每个分片中,每个分片都会独立地执行这些请求,并将结果合并之后返回给用户。
- 优势:更新数据比较简单,每个分片只用更新自己的数据和自己的索引。
- 劣势:无法提高查询的吞吐量,因为所有查询都需要在每个分片上进行一遍,毫无疑问会造成额外消耗。
- MangoDB、ES就用的是这种索引方式。
- 全局索引:并不是所有分片的信息只会存储在单独的分片中,全局索引依然会进行分片,以保证很好的扩展性——通常是按范围分片。比方说从a-g开头的书名索引存在分片1,h-o的存在分片2,……这样当按照二级索引查询时,可以先去指定的分片查询对应的索引,找到数据所在的分片,然后再在指定的分片上查询。
- 优势:当按索引进行点查询时(比如:作者为XX),只需要额外请求一个分片的索引即可知道数据的位置,然后请求数据所在的分片即可,避免查询无关分片
- 劣势:多种索引条件匹配时比较复杂,比如既要查询书本类别,又要查询出版社,那就需要获取多个分片上的索引并作与,因此需要节点之间较多通信;此外更新索引的操作复杂,因为一条数据的更新可能导致多个分片的索引信息都要被修改,需要分布式事务的支持。