3.5 知识点关联

1.乐观锁

Elasticsearch引入了乐观锁机制来解决并发写过程中数据冲突的问题,其实乐观锁在多个维度均有应用。

在数据库中,我们用乐观锁来控制表结构,减少长事务中数据库加锁的开销,达到数据表“读多写少”场景下的高性能;

在Java中,Java引入了CAS(Compare And Swap)乐观锁实现机制实现多线程同步的原子指令,如AtomicInteger。

命名的艺术

本章重点介绍了Elasticsearch的核心概念,这些概念的英文命名方法很值得我们学习借鉴,如Shard英文原意为碎片,这个词很形象地解释了倒排索引分解的结果,我们通过这个单词就能见名知意。

其实,命名的学问不仅在Elasticsearch中用得很巧,在Java中也随处可见。如研发人员经常使用的“<>”操作符,英文原意为Diamond Operator。这个命名很有想象力,“<>”很像一个菱形,而菱形的英文单词是Diamond,同时Diamond还表示钻石。

2.配置文件格式

前面我们介绍了配置文件格式YML。该文件格式是由Clark Evans、Ingy döt Net和Oren Ben-Kiki在2001年首次发表的。

YAML是“YAML Ain't a Markup Language”(YAML不是一种置标语言)的首字母缩写。有意思的是,在开发这种语言时,YAML的初衷本是“Yet Another Markup Language”(仍是一种置标语言)。后来为了强调YAML语言以数据作为中心,而不是以置标语言为重点,因而采用返璞词来重新命名。

配置文件先后经历了ini格式、JSON格式、XML格式、Properties格式和HOCON格式。其中,HOCON(Human-Optimized Config Object Notation)格式由Lightbend公司开发,它被用于Sponge,以及利用SpongeAPI的独立插件以储存重要的数据,HOCON文件通常以.conf作为后缀名。

在配置文件的格式变迁中,我们能看到配置的方式都在追求语法简单、能继承、支持注释等特性。

如表3-1所示,Elasticsearch中的索引(Index)如果对标关系数据库中的数据库(DataBase)的话,则表(Table)与类型(Type)对应——一个数据库下面可以有多张表(Table),就像1个索引(Index)下面有多种类型(Type)一样。

表3-1

行(ROW)与文档(Document)对应——一个数据库表(Table)下的数据由多行(ROW)组成,就像1个类型Type由多个文档(Document)组成一样。

列(column)与字段(Field)对应——数据库表(Table)中一行数据由多列(column)组成,就像1个文档(Document)由多个字段(Field)组成一样。

关系数据库中的schema与Elasticsearch中的映射(Mapping)对应——在关系数据库中,schema定义了表、表中字段、表和字段之间的关系,就像在Elasticsearch中,Mapping定义了索引下Type的字段处理规则,即索引的建立、索引的类型、是否保存原始索引JSON文档、是否压缩原始JSON文档、是否需要分词处理、如何进行分词处理等。

关系数据库中的增(Insert)删(Delete)改(Update)查(Select)操作可与Elasticsearch中的增(Put/Post)删(Delete)改(Update)查(GET)一一对应。

其实,Elasticsearch中的分片与关系数据库中常用的分库分表方法有异曲同工之妙!

3.副本

副本技术是分布式系统中常见的一种数据组织形式,在日常工作中,“副本”技术也十分常见。比如各级领导都需要指定和培养“二责”人选,当自己出差或请假时,“二责”可以组织团队中的工作。又如在团队中,团队成员之间的工作往往需要多人间相互备份,防止某位成员有事或离职时,相关工作不能继续展开。

在分布式系统中,副本是如何由来的,为什么这么有必要性呢?

副本(Replica或称Copy)一般指在分布式系统中为数据或服务提供的冗余。这种冗余设计是提高分布式系统容错率、提高可用性的常用手段。

在服务副本方面,一般指的是在不同服务器中部署同一份代码。如Tomcat/Jetty集群部署服务,集群中任意一台服务器都是集群中其他服务器的备份或称副本。

在数据副本方面,一般指的是在不同的节点上持久化同一份数据。当某节点中存储的数据丢失时,系统就可以从副本中读到数据了。

可以说,数据副本是分布式系统解决数据丢失或异常的唯一手段,因此副本协议也成为贯穿整个分布式系统的理论核心。

副本的数据一致性

分布式系统通过副本控制协议,让用户通过一定的方式即可读取分布式系统内部各个副本的数据,这些数据在一定的约束条件下是相同的,即副本数据一致性(Consistency)。副本数据一致性是针对分布式系统中各个节点而言的,不是针对某节点的某个副本而言的。

在分布式系统中,一致性分为强一致性(Strong Consistency)、弱一致性(Week Consistency),还有介于二者之间的会话一致性(Session Consistency)和最终一致性(Eventual Consistency)。

其中,强一致性最难实现。强一致性要求任何时刻用户都可以读到最近一次成功更新的副本数据。弱一致性与强一致性正好相反,数据更新后,用户无法在一定时间内读到最新的值,因此在实际中使用很少。

会话一致性指的是在一次会话内,用户一旦读到某个数据的某个版本的更新数据,则在这个会话中就不会再读到比当前版本更老旧的数据。最终一致性指的是集群中各个副本的数据最终能达到完全一致的状态。

从副本的角度而言,强一致性是最佳的,但对于分布式系统而言,还要考虑其他方面,如分布式系统的整体性能(即系统的吞吐)、系统的可用性、系统的可拓展性等。这也是系统设计要全盘考虑的原因。

副本数据的分布方式

副本的数据是如何分发到位的呢?这就涉及数据的分布方式。

一般来说,数据的分布方式主要有哈希方式、按数据范围分布、按数据量分布和一致性哈希方式(Consistent Hashing)等。

其中哈希方式最为简单,简单是其最大的优势,但缺点同样明显。一方面,可扩展性不高——一旦存储规模需要扩大,则所有数据都需要重新按哈希值分发;另一方面,哈希方式容易导致存储空间的数据分布不均匀。

按数据范围分布也比较常见,一般来说,是将数据按特征值的范围划分为不同的区间,使得集群中不同的服务器处理不同区间的数据。这种方式可以避免哈希值带来的存储空间数据分布不均匀的情况。

按数据量分布和按数据范围分布核心思路比较接近,一般是将数据看作一个顺序增长的,并将数据集按照某一较为固定的大小划分为若干数据块,把不同的数据块分布到不同的服务器上。

而一致性哈希是在工程实践中使用较为广泛的数据分布方式。一致性哈希的基本思路是使用一个哈希函数计算数据的哈希值,而哈希函数的输出值会作为一个封闭的环,我们会根据哈希值将节点随机分布到这个环上,每个节点负责处理从自己开始顺时针至下一个节点的全部哈希值域上的数据。

有了数据分布的方法,那么数据以何种形态进行分布呢?一般来说有两种,一种以服务器为核心,另一种是以数据为核心。

以机器为核心时,机器之间互为副本,副本机器之间的数据完全相同。以机器为核心的策略适用于上述各种数据分布方式,最主要的优点就是简单,容易落地;而缺点也很明显,一旦数据出问题,在数据恢复时就需要恢复多台服务器中的数据,效率很低;而且增加服务器后,会带来可扩展性低的问题。

以数据为核心时,一般将数据拆分为若干个数据段,以数据段为单位去分发。一般来说,每个数据段的大小尽量相等,而且限制数据量大小的上线。在不同的系统中,数据段有很多不同的称谓,如在Lucene和Elasticsearch中称之为segment,在Kafka中称之为chunk和partition等。

以数据为核心并不适合所有数据分布方式,一般会采用哈希方式或一致性哈希方式。

将数据拆分为数据段意味着副本的管理将以数据段为单位进行展开,因此副本与机器不再强相关,每台机器都可以负责一定数据段的副本。这带来的好处是当某台服务器中的数据有问题时,我们可以从集群中的任何其他服务器恢复数据,因此数据的恢复效率很高。

副本分发策略

副本分发策略指的是主节点和副本节点之间副本数据同步的方法。一般来说分为两大类:中心化方式和去中心化方式。

中心化方式的基本上线思路是由一个中心节点协调副本数据的更新、维护副本之间的一致性。数据的更新可以是主节点主动向副本节点推送,也可以是副本节点向主节点推送。中心化方式的优点是设计思路较为简单,而缺点也很明显,数据的同步及系统的可用性都有“单点依赖”的风险,即依赖于中心化节点。一旦中心化节点发生异常,则数据同步和系统的可用性都会受到影响。

在去中心化方式中则没有中心节点,所有的节点都是P2P形式,地位对等,节点之间通过平等协商达到一致,因此去中心化节点不会因为某个节点的异常而导致系统的可用性受到影响。但有得必有失,去中心化方式的最大的缺点在于各个节点达成共识的过程较长,需要反复进行消息通信来确认内容,实现较为复杂。去中心化方式在区块链中有广泛应用,其共识达成的算法可以参见《区块链底层设计Java实战》一书中的“共识算法”部分。