当时光的列车缓缓驶过酋长球场,32岁的亨利就在那里,深情的目光望过去都是自己22岁的影子;还是那辆列车,随着时光送走了匆匆过客,静静开往另一片大陆,33岁的亨利就在那里,深情的目光望去依稀浮现自己25岁的模样;远方的期许固然美好,而列车的短暂停留更好像岁月的美丽回眸,当时光奏出回家的天籁,35岁的亨利就在那里,深情的目光望去勾勒出自己29岁的动人画面;渐行渐远的车辙,默默带走了属于四座城市的喧嚣,却指引着一路跟随的人们去追寻着那段逝去的时光,当岁月含泪悄悄转身,37岁的亨利就在那里,深情的目光望过去,试图回到原点,那个出发的站台,记起自己背起行囊时,那十七岁的样子。

~

Elasticsearch 是一个分布式可扩展的实时搜索和分析引擎.

Elasticsearch【文档数据库】 是一个建立在全文搜索引擎 Apache Lucene™ 基础上的搜索引擎。 当然 Elasticsearch 并不仅仅是 Lucene 那么简单,它不仅包括了全文搜索功能,还可以进行以下工作:

  • 分布式实时文件存储,并将每一个字段都编入索引,使其可以被搜索。
  • 实时分析的分布式搜索引擎。
  • 可以扩展到上百台服务器,处理PB级别的结构化或非结构化数据。

在介绍Elasticsearch索引之前,先了解下Lucene。

官方文档:https://www.elastic.co/cn/elasticsearch

JAVA特别强调,这里安装的版本务必要和之后SpringBoot中的elasticsearch的版本一致才行,要不然后面会报错。

Lucene执行原理

介绍:Lucene是apache下的一个开放源代码的全文检索引擎工具包。提供了完整的查询引擎和索引引擎,部分文本分析引擎。Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能。

**应用场景:**对于数据量大、数据结构不固定的数据可采用全文检索方式搜索,比如百度、Google等搜索引擎、论坛站内搜索、电商网站站内搜索等。

索引和搜索原理

全文索引和搜索流程图:

image-20240601191634869

1、绿色表示索引过程,对要搜索的原始内容进行索引构建一个索引库,索引过程包括:

确定原始内容即要搜索的内容–>采集文档–>创建文档–>分析文档–>索引文档

2、红色表示搜索过程,从索引库中搜索内容,搜索过程包括:

用户通过搜索界面–>创建查询–>执行搜索,从索引库搜索–>渲染搜索结果

创建索引

对文档索引的过程,将用户要搜索的文档内容进行索引,索引存储在索引库(index)中。

这里我们要搜索的文档是磁盘上的文本文件,根据案例描述:凡是文件名或文件内容包括关键字的文件都要找出来,这里要对文件名和文件内容创建索引。

1) 获取原始文档

原始文档 是指要索引和搜索的内容。原始内容包括互联网上的网页(爬虫)、数据库中的数据(sql查询)、磁盘上的文件(IO流获取)等。

从互联网上、数据库、文件系统中等获取需要搜索的原始信息,这个过程就是信息采集,信息采集的目的是为了对原始内容进行索引。

在Internet上采集信息的软件通常称为爬虫或蜘蛛,也称为网络机器人,爬虫访问互联网上的每一个网页,将获取到的网页内容存储起来。

  • Lucene不提供信息采集的类库,需要自己编写一个爬虫程序实现信息采集,也可以通过一些开源软件实现信息采集,如下:

  • Nutch(http://lucene.apache.org/nutch), Nutch是apache的一个子项目,包括大规模爬虫工具,能够抓取和分辨web网站数据。

  • jsoup(http://jsoup.org/ ),jsoup 是一款Java 的HTML解析器,可直接解析某个URL地址、HTML文本内容。它提供了一套非常省力的API,可通过DOM,CSS以及类似于jQuery的操作方法来取出和操作数据。

  • heritrix(http://sourceforge.net/projects/archive-crawler/files/),Heritrix 是一个由 java 开发的、开源的网络爬虫,用户可以使用它来从网上抓取想要的资源。其最出色之处在于它良好的可扩展性,方便用户实现自己的抓取逻辑。

获取磁盘上文件的内容,可以通过文件流来读取文本文件的内容,对于pdf、doc、xls等文件可通过第三方提供的解析工具读取文件内容,比如Apache POI读取doc和xls的文件内容。

2)创建文档对象

获取原始内容的目的是为了索引,在索引前需要将原始内容创建成文档(Document),文档中包括一个一个的域(Field),域中存储内容。

这里我们可以将磁盘上的一个文件当成一个document,Document中包括一些Field(file_name文件名称、file_path文件路径、file_size文件大小、file_content文件内容),如下图:

image-20240601191751345

注意:每个Document可以有多个Field,不同的Document可以有不同的Field,同一个Document可以有相同的Field(域名和域值都相同)

每个文档都有一个唯一的编号,就是文档id。

3) 分析文档

将原始内容创建为包含域(Field)的文档(document),需要再对域中的内容进行分析,分析的过程是经过对原始文档提取单词、将字母转为小写、去除标点符号、去除停用词等过程生成最终的语汇单元,可以将语汇单元理解为一个一个的单词。

比如下边的文档经过分析如下:

原文档内容:

Lucene is a Java full-text search engine. Lucene is not a complete

application, but rather a code library and API that can easily be used

to add search capabilities to applications.

分析后得到的语汇单元:

lucene、java、full、search、engine。。。。

每个单词叫做一个Term,不同的域中拆分出来的相同的单词是不同的term。term中包含两部分一部分是文档的域名,另一部分是单词的内容。

例如:文件名中包含apache和文件内容中包含的apache是不同的term。

4) 创建索引

对所有文档分析得出的语汇单元进行索引,索引的目的是为了搜索,最终要实现只搜索被索引的语汇单元从而找到Document(文档)。

image-20240601191816646

注意:创建索引是对语汇单元索引,通过词语找文档,这种索引的结构叫倒排索引结构

传统方法是根据文件找到该文件的内容,在文件内容中匹配搜索关键字,这种方法是顺序扫描方法,数据量大、搜索慢。

倒排索引结构是根据内容(词语)找文档。

倒排索引结构也叫反向索引结构,包括索引和文档两部分,索引即词汇表,它的规模较小,而文档集合较大。

查询索引

查询索引也是搜索的过程。搜索就是用户输入关键字,从索引(index)中进行搜索的过程。根据关键字搜索索引,根据索引找到对应的文档,从而找到要搜索的内容(这里指磁盘上的文件)。

1) 用户查询接口

全文检索系统提供用户搜索的界面供用户提交搜索的关键字,搜索完成展示搜索结果。

Lucene不提供制作用户搜索界面的功能,需要根据自己的需求开发搜索界面。

2) 创建查询

用户输入查询关键字执行搜索之前需要先构建一个查询对象,查询对象中可以指定查询要搜索的Field文档域、查询关键字等,查询对象会生成具体的查询语法,

例如:语法 “fileName:lucene”表示要搜索Field域的内容为“lucene”的文档

3) 执行查询

搜索索引过程:

根据查询语法在倒排索引词典表中分别找出对应搜索词的索引,从而找到索引所链接的文档链表。

比如搜索语法为“fileName:lucene”表示搜索出fileName域中包含Lucene的文档。

搜索过程就是在索引上查找域为fileName,并且关键字为Lucene的term,并根据term找到文档id列表。

4) 渲染结果

以一个友好的界面将查询结果展示给用户,用户根据搜索结果找自己想要的信息,为了帮助用户很快找到自己的结果,提供了很多展示的效果,比如搜索结果中将关键字高亮显示,百度提供的快照等。

Elasticsearch索引

基本概念

先说Elasticsearch的文件存储,Elasticsearch是面向文档型数据库,一条数据在这里就是一个文档,用JSON作为文档序列化的格式,比如下面这条用户数据:

1
2
3
4
5
6
7
8
{
"name" : "John",
"sex" : "Male",
"age" : 25,
"birthDate": "1990/05/01",
"about" : "I love to go rock climbing",
"interests": [ "sports", "music" ]
}

MySQL这样的数据库存储就会容易想到建立一张User表,有balabala的字段等,在Elasticsearch里这就是一个文档,当然这个文档会属于一个User的类型,各种各样的类型存在于一个索引当中。这里有一份简易的将Elasticsearch和关系型数据术语对照表:

关系数据库 ⇒ 数据库 ⇒ 表 ⇒ 行 ⇒ 列(Columns)

Elasticsearch ⇒ 索引 ⇒ 类型 ⇒ 文档 ⇒ 字段(Fields)

一个 Elasticsearch 集群可以包含多个索引(数据库),也就是说其中包含了很多类型(表)。这些类型中包含了很多的文档(行),然后每个文档中又包含了很多的字段(列)。

Elasticsearch的交互,可以使用Java API,也可以直接使用HTTP的Restful API方式,比如我们打算插入一条记录,可以简单发送一个HTTP的请求:

1
2
3
4
5
6
7
8
PUT /megacorp/employee/1
{
"name" : "John",
"sex" : "Male",
"age" : 25,
"about" : "I love to go rock climbing",
"interests": [ "sports", "music" ]
}

更新,查询也是类似这样的操作,具体操作手册可以参见Elasticsearch权威指南

索引

Elasticsearch最关键的就是提供强大的索引能力了,其实InfoQ的这篇时间序列数据库的秘密(2)——索引写的非常好,我这里也是围绕这篇结合自己的理解进一步梳理下,也希望可以帮助大家更好的理解这篇文章。

Elasticsearch索引的精髓:

一切设计都是为了提高搜索的性能

另一层意思:为了提高搜索的性能,难免会牺牲某些其他方面,比如插入/更新,否则其他数据库不用混了:)

前面看到往Elasticsearch里插入一条记录,其实就是直接PUT一个json的对象,这个对象有多个fields,比如上面例子中的name, sex, age, about, interests,那么在插入这些数据到Elasticsearch的同时,Elasticsearch还默默1的为这些字段建立索引–倒排索引,因为Elasticsearch最核心功能是搜索。

Elasticsearch是如何做到快速索引的

InfoQ那篇文章里说Elasticsearch使用的倒排索引比关系型数据库的B-Tree索引快,为什么呢?

什么是B-Tree索引?

上大学读书时老师教过我们,二叉树查找效率是logN,同时插入新的节点不必移动全部节点,所以用树型结构存储索引,能同时兼顾插入和查询的性能。

因此在这个基础上,再结合磁盘的读取特性(顺序读/随机读),传统关系型数据库采用了B-Tree/B+Tree这样的数据结构

为了提高查询的效率,减少磁盘寻道次数,将多个值作为一个数组通过连续区间存放,一次寻道读取多个数据,同时也降低树的高度。

倒排索引的概念在之前已经了解过了

image-20240601192226162

继续上面的例子,假设有这么几条数据(为了简单,去掉about, interests这两个field):

ID Name Age Sex
1 Kate 24 Female
2 John 24 Male
3 Bill 29 Male

ID是Elasticsearch自建的文档id,那么Elasticsearch建立的索引如下:

Name:

Term Posting List
Kate 1
John 2
Bill 3

Age:

Term Posting List
24 [1,2]
29 3

Sex:

Term Posting List
Female 1
Male [2,3]

Posting List

Elasticsearch分别为每个field都建立了一个倒排索引,Kate, John, 24, Female这些叫term,而[1,2]就是Posting List。Posting list就是一个int的数组,存储了所有符合某个term的文档id。

看到这里,不要认为就结束了,精彩的部分才刚开始…

通过posting list这种索引方式似乎可以很快进行查找,比如要找age=24的同学,爱回答问题的小明马上就举手回答:我知道,id是1,2的同学。但是,如果这里有上千万的记录呢?如果是想通过name来查找呢?

Term Dictionary

Elasticsearch为了能快速找到某个term,将所有的term排个序,二分法查找term,logN的查找效率,就像通过字典查找一样,这就是Term Dictionary。现在再看起来,似乎和传统数据库通过B-Tree的方式类似啊,为什么说比B-Tree的查询快呢?

Term Index

B-Tree通过减少磁盘寻道次数来提高查询性能,Elasticsearch也是采用同样的思路,直接通过内存查找term,不读磁盘,但是如果term太多,term dictionary也会很大,放内存不现实,于是有了Term Index,就像字典里的索引页一样,A开头的有哪些term,分别在哪页,可以理解term index是一颗树:

image-20240601192341799

这棵树不会包含所有的term,它包含的是term的一些前缀。通过term index可以快速地定位到term dictionary的某个offset,然后从这个位置再往后顺序查找。

image-20240601192401034

所以term index不需要存下所有的term,而仅仅是他们的一些前缀与Term Dictionary的block之间的映射关系,再结合FST(Finite State Transducers)的压缩技术,可以使term index缓存到内存中。从term index查到对应的term dictionary的block位置之后,再去磁盘上找term,大大减少了磁盘随机读的次数。

这时候爱提问的小明又举手了:”那个FST是神马东东啊?”

一看就知道小明是一个上大学读书的时候跟我一样不认真听课的孩子,数据结构老师一定讲过什么是FST。但没办法,我也忘了,这里再补下课:

FSTs are finite-state machines that map a term (byte sequence) to an arbitrary output.

假设我们现在要将mop, moth, pop, star, stop and top(term index里的term前缀)映射到序号:0,1,2,3,4,5(term dictionary的block位置)。最简单的做法就是定义个Map<String, Integer>,大家找到自己的位置对应入座就好了,但从内存占用少的角度想想,有没有更优的办法呢?答案就是:FST.

FST简单介绍

FST的设计非常紧凑,它通过状态(State)和转换(Transition)来表示数据。每个状态都代表一个特定的字节序列(如一个单词或短语),转换则代表从一个状态到另一个状态的过程。每个状态和转换都可以附带一个输出值。所有这些信息都可以用极小的空间存储,因为它们主要是索引和引用,而不是实际的数据。

此外,FST使用了一种称为“前缀共享”的技术来进一步减少内存使用。这意味着如果两个或更多的字节序列有相同的前缀(即它们的开始部分相同),那么这些序列的状态和转换将被共享,而不是为每个序列单独存储。这大大减少了需要存储的状态和转换的数量。

例如,对于单词 “cat” 和 “car”,在FST中,它们的前两个字母 “c” 和 “a” 的状态和转换将被共享,只有最后一个字母 “t” 和 “r” 的状态和转换需要单独存储。

因此,通过这种紧凑的数据结构和前缀共享的技术,FST能够在内存占用方面非常高效,即使处理大量的字节序列数据也能保持较低的内存使用率。

压缩技巧

Elasticsearch里除了上面说到用FST压缩term index外,对posting list也有压缩技巧。 小明喝完咖啡又举手了:”posting list不是已经只存储文档id了吗?还需要压缩?”

嗯,我们再看回最开始的例子,如果Elasticsearch需要对同学的性别进行索引(这时传统关系型数据库已经哭晕在厕所……),会怎样?如果有上千万个同学,而世界上只有男/女这样两个性别,每个posting list都会有至少百万个文档id。 Elasticsearch是如何有效的对这些文档id压缩的呢?

Frame Of Reference

增量编码压缩,将大数变小数,按字节存储

首先,Elasticsearch要求posting list是有序的(为了提高搜索的性能,再任性的要求也得满足),这样做的一个好处是方便压缩,看下面这个图例:

image-20240601192621925

后-前

如果数学不是体育老师教的话,还是比较容易看出来这种压缩技巧的。

原理就是通过增量,将原来的大数变成小数仅存储增量值,再精打细算按bit排好队,最后通过字节存储,而不是大大咧咧的尽管是2也是用int(4个字节)来存储。

Roaring bitmaps

说到Roaring bitmaps,就必须先从bitmap说起。Bitmap是一种数据结构,假设有某个posting list:

[1,3,4,7,10]

对应的bitmap就是:

[1,0,1,1,0,0,1,0,0,1]

非常直观,用0/1表示某个值是否存在,比如10这个值就对应第10位,对应的bit值是1,这样用一个字节就可以代表8个文档id,旧版本(5.0之前)的Lucene就是用这样的方式来压缩的,但这样的压缩方式仍然不够高效,如果有1亿个文档,那么需要12.5MB的存储空间,这仅仅是对应一个索引字段(我们往往会有很多个索引字段)。于是有人想出了Roaring bitmaps这样更高效的数据结构。

Bitmap的缺点是存储空间随着文档个数线性增长,Roaring bitmaps需要打破这个魔咒就一定要用到某些指数特性:

将posting list按照65535为界限分块,比如第一块所包含的文档id范围在0~65535之间,第二块的id范围是65536~131071,以此类推。再用**<商,余数>的组合表示每一组id**,这样每组里的id范围都在0~65535内了,剩下的就好办了,既然每组id不会变得无限大,那么我们就可以通过最有效的方式对这里的id存储。

image-20240601192657405

细心的小明这时候又举手了:”为什么是以65535为界限?”

程序员的世界里除了1024外,65535也是一个经典值,因为它=2^16-1,正好是用2个字节能表示的最大数,一个short的存储单位,注意到上图里的最后一行“If a block has more than 4096 values, encode as a bit set, and otherwise as a simple array using 2 bytes per value”,如果是大块,用节省点用bitset存,小块就豪爽点,2个字节我也不计较了,用一个short[]存着方便。

那为什么用4096来区分采用数组还是bitmap的阀值呢?

这个是从内存大小考虑的,当block块里元素超过4096后,用bitmap更剩空间: 采用bitmap需要的空间是恒定的: 65536/8 = 8192bytes 而如果采用short[],所需的空间是: 2*N(N为数组元素个数) 小明手指一掐N=4096刚好是边界:

image-20240601192729149

还是节省内存使用率!

联合索引

上面说了半天都是单field索引,如果多个field索引的联合查询,倒排索引如何满足快速查询的要求呢?

  • 利用跳表(Skip list)的数据结构快速做“与”运算,或者
  • 利用上面提到的bitset按位“与”

image-20240601192846071

如果使用跳表,对最短的posting list中的每个id,逐个在另外两个posting list中查找看是否存在,最后得到交集的结果。

如果使用bitset,就很直观了,直接按位与,得到的结果就是最后的交集。

总结

Elasticsearch的索引思路:

将磁盘里的东西尽量搬进内存,减少磁盘随机读取次数(同时也利用磁盘顺序读特性),结合各种奇技淫巧的压缩算法,用及其苛刻的态度使用内存。

所以,对于使用Elasticsearch进行索引时需要注意:

  • 不需要索引的字段,一定要明确定义出来,因为默认是自动建索引的
  • 同样的道理,对于String类型的字段,不需要analysis的也需要明确定义出来,因为默认也是会analysis的
  • 选择有规律的ID很重要,随机性太大的ID(比如java的UUID)不利于查询

参考资料:

QA

为什么 Elasticsearch 可以存储更多数据?

Elasticsearch 相较于 MySQL 能存储更多数据,主要原因在于其分布式架构数据存储机制索引结构的优化以及扩展性设计。以下简明比较说明了两者在这些方面的差异:

  1. 分布式架构 vs 单节点架构(默认)
  • Elasticsearch:原生支持分布式存储,数据被自动切分为多个分片(Shards),分布在不同的**节点(Nodes)**上。随着数据量增大,只需增加更多节点,它会自动调整存储和处理能力。这种设计使得 Elasticsearch 能够轻松处理海量数据。
  • MySQL:默认是单节点架构,所有数据存储在一个节点上。虽然 MySQL 支持主从复制、分片等分布式特性,但这些功能需要手动配置,且复杂度较高,扩展性不如 Elasticsearch。

总结:Elasticsearch 的分布式设计使它能够轻松扩展到数百个节点,处理大规模数据存储和查询,而 MySQL 扩展性较差。

  1. 索引机制:倒排索引 vs B-tree
  • Elasticsearch:基于倒排索引(Inverted Index),这是全文搜索引擎的核心数据结构。倒排索引为每个词项建立索引,指向包含该词的文档,这使得它在处理非结构化数据(如文本、日志等)时效率极高,并且可以快速搜索海量文档。
  • MySQL:使用B-tree哈希索引 进行数据存储和检索,适用于结构化数据精确匹配范围查询。但当数据规模巨大时,B-tree 索引效率会下降,且 MySQL 不擅长处理非结构化数据的全文搜索。

总结:Elasticsearch 的倒排索引专为海量数据的全文搜索设计,能够高效地处理和存储大量非结构化数据,而 MySQL 的索引机制更适合小规模、结构化数据的存储和查询。

  1. 压缩与存储优化
  • Elasticsearch:对存储进行了大量优化,支持压缩索引稀疏存储等技术。Elasticsearch 通过将数据压缩成二进制形式,减少了存储空间的占用。其索引和存储设计使得即使在存储海量数据时,也能保持较低的存储开销。
  • MySQL:虽然 MySQL 也支持某些压缩技术(例如 InnoDB 的压缩表),但整体上没有 Elasticsearch 那样专门为大规模数据存储进行优化。对于海量数据的存储,MySQL 的存储开销通常更大。

总结:Elasticsearch 的压缩和稀疏存储技术使其能够更高效地存储大规模数据,相比之下,MySQL 的压缩和存储优化相对有限。

  1. 扩展性:水平扩展 vs 垂直扩展
  • Elasticsearch:主要基于水平扩展(Horizontal Scaling),通过增加更多节点来扩展集群的容量和性能。当数据量增大时,Elasticsearch 可以自动将数据分片分布到新节点上,轻松应对海量数据。
  • MySQL:默认只能依赖垂直扩展(Vertical Scaling),通过增加服务器的硬件资源(如 CPU、内存、存储)来提升性能。当单节点达到物理极限时,需要手动进行分片或使用复杂的集群方案,且扩展难度较大。

总结:Elasticsearch 的水平扩展能力使其在数据规模和查询性能上具有极高的灵活性,而 MySQL 的扩展能力在面对大规模数据时往往乏力。

  1. 数据类型处理:非结构化 vs 结构化
  • Elasticsearch:专为非结构化数据(如文本、日志、文档)设计,能够高效存储和检索大量复杂的文本数据。它的全文搜索、复杂查询能力使其能够处理各种类型的数据,包括 JSON 文档等半结构化数据。
  • MySQL:主要用于存储结构化数据,擅长处理表格化的数据和关系型查询。虽然 MySQL 可以存储 JSON 数据,但对非结构化数据的处理能力较弱,全文搜索性能也不如 Elasticsearch。

总结:Elasticsearch 在处理和存储非结构化数据时表现更好,而 MySQL 更适合处理结构化数据。

ElasticSearch为什么快?

  1. 分布式存储
    • 举例:假设有 10 万条日志数据需要被索引和搜索。Elasticsearch 会将这些数据分布到多个节点上进行存储和索引处理。这样,每个节点只需处理部分数据,极大地减轻了单个节点的压力,提升了系统整体的查询能力。
  2. 索引分片
    • 举例:当你查询一个包含大量文档的索引时,Elasticsearch 会将查询分解成多个子任务,每个子任务分别在不同的分片上执行。假如该索引有 5 个分片,查询请求会并行地在 5 个分片上执行,提升了查询速度。
  3. 全文索引
    • 举例:比如你要搜索一篇文档中提到的“机器学习”这个关键词,Elasticsearch 通过全文索引技术,将文档内容索引成倒排索引结构,快速定位出“机器学习”所在的所有文档,快速返回查询结果。
  4. 倒排索引
    • 举例:在一个新闻网站的数据库中,当用户搜索“经济新闻”时,Elasticsearch 使用倒排索引快速定位包含“经济新闻”关键词的所有文章,而不需要遍历数据库中的每篇文章,极大提升了查询速度。
  5. 索引优化
    • 举例:Elasticsearch 可以自动对索引进行压缩和合并。例如,旧的日志数据可以通过索引优化压缩成更小的存储文件,同时保持高效的查询能力,减小了存储空间占用并加快了查询速度。
  6. 预存储索引
    • 举例:对于热门查询,比如“世界杯新闻”,Elasticsearch 可以提前将查询结果存储起来。当用户再次查询相同关键词时,直接返回已经计算好的结果,避免重新索引计算,大幅提升查询速度。
  7. 高级查询引擎
    • 举例:假设你要根据多个条件(如日期范围、关键词、地域等)对某个新闻数据库进行复杂查询,Elasticsearch 的高级查询引擎可以根据这些条件快速构建优化的查询计划,并从多个维度返回精准的结果。
  8. 异步请求处理
    • 举例:当系统负载较高时,比如网站用户突然增多,大量请求涌入,Elasticsearch 能将部分用户请求转换为异步处理,这样用户可以立即收到响应,系统再稍后处理查询任务,避免用户长时间等待。
  9. 内存存储
    • 举例:如果你的系统经常查询同一批次的数据,Elasticsearch 会将这些数据缓存在内存中。当再次查询时,直接从内存中返回结果,大幅提高查询速度,而无需再次从磁盘中读取数据。

Elasticsearch支持事务吗?

ES虽然也可以认为是一个数据库,但是他并不支持传统意义上的ACID事务,因为ES它被设计出来是主要用作搜索引擎的,主要是提升查询效率的。如果支持复杂的事务操作意味着就要牺牲性能优势。

虽然Elasticsearch 不支持传统的事务,但是他是可以确保单个文档的更改(如创建、更新、删除)是原子性的。这意味着任何文档级的操作都完整地成功或完整地失败,但不保证跨多个文档或操作的一致性。

什么是ElasticSearch的深度分页问题?

深度分页问题 是指在 Elasticsearch 中,随着查询结果页数的增加,查询性能显著下降的问题。具体表现为,当用户请求 非常靠后的结果页 时(如第 10000 页),查询的响应时间变得非常慢,甚至耗尽系统资源。

原因:

  1. 每次分页都需要遍历大量数据
    • Elasticsearch 在分页查询时,仍然需要从头开始扫描、排序和跳过之前的所有文档,直到找到目标页的数据。例如,查询第 10000 页的结果时,需要跳过前 9999 页的数据,这导致了大量的计算和内存开销。
  2. 大数据量的排序开销
    • Elasticsearch 在分页时会对匹配条件的所有文档进行排序(通常基于 _score 或指定字段)。当查询结果页数很大时,排序的文档数量非常多,导致内存消耗剧增,影响性能。

举例:

如果你在一个包含上百万条商品记录的电商网站上查询第 10000 页的商品数据,Elasticsearch 需要扫描所有前 9999 页的数据,再根据分页参数 fromsize 跳过这些数据,这会随着页数的增加,导致查询时间和资源消耗急剧上升。

解决方案:

  1. 使用 search_after
    • 通过 search_after 参数,基于上一个查询结果的排序标记来获取下一页数据,避免深度分页时的跳过计算。
  2. 使用 scroll API
    • 如果需要处理大量数据,可以使用 滚动查询scroll),它在一次查询中锁定数据快照,分批次读取大数据集,避免每次重新排序和扫描。
  3. 限制最大分页深度
    • 许多系统会限制用户只能分页到一定深度(如 100 页),以避免资源占用过高。

MySQL 和 Elasticsearch 数据类型

MySQL 类型 Elasticsearch 类型 说明
VARCHAR text, keyword 根据是否需要全文搜索或精确搜索选择使用 textkeyword
CHAR keyword 通常映射为 keyword,因为它们用于存储较短的、不经常变化的字符串。
BLOB/TEXT text 大文本块使用 text 类型,支持全文检索。
INT, BIGINT long 大多数整数类型映射为 long,以支持更大的数值。
TINYINT byte 较小的整数可以映射为 byte 类型。
DECIMAL, FLOAT, DOUBLE double, float 根据精确度需求选择 doublefloat
DATE, DATETIME, TIMESTAMP date 所有的日期时间类型均可映射为 date
TINYINT(1) boolean TINYINT(1) 通常用于布尔值,映射为 Elasticsearch 的 boolean 类型。

说明:

  • text:用于存储需要进行全文检索的长文本字段,适合搜索内容。
  • keyword:用于存储短字符串,适合需要精确匹配的字段,如标签、ID 等。
  • long:用于存储大整数,适合 MySQL 中的 INTBIGINT 类型。
  • byte:适合存储小整数,如 TINYINT
  • double, float:适合存储浮点数,具体选择取决于精度要求。
  • date:用于存储日期或时间戳。
  • boolean:用于存储布尔值,适合 TINYINT(1) 类型。

如何保证ES和数据库的数据一致性?

在实际应用中,通常会使用关系型数据库(如 MySQL)作为主数据存储,而使用 ElasticSearch 进行全文检索快速查询。为了确保两者之间的数据一致性,需要采取一些策略和设计模式。

1. 双写方案

在双写方案中,应用程序在写入数据库的同时,也同步写入 ElasticSearch。即每次对数据库进行增删改操作时,都会同时更新 ES 索引。

1.1 问题

  • 数据不一致风险:双写过程中,如果数据库写入成功,但 ElasticSearch 写入失败,数据会出现不一致的情况。
  • 网络延迟:由于数据库和 ElasticSearch 的写入是两个独立的操作,网络延迟或故障可能导致某个操作失败。

1.2 解决方案:事务性保证

可以通过分布式事务补偿机制来解决双写过程中数据不一致的问题。例如:

  • 事务消息:使用消息队列(如 Kafka)来进行数据库和 ES 的双写,将数据更新操作放入消息队列中,保证数据库和 ES 的更新要么都成功,要么都失败(确保最终一致性)。
  • 最终一致性:即使发生网络故障,系统可以检测到数据不一致并通过定期对账机制进行修复,确保最终数据一致。

【补偿任务(Compensation Task)通常是指在系统或流程中,为了纠正或弥补之前的错误、失败或不完整操作而执行的任务】

举例:

  • 订单系统:当用户下单时,数据库会记录订单信息,同时通过异步任务或消息队列将订单数据写入 ElasticSearch。若 ElasticSearch 写入失败,系统可以通过重试机制或补偿任务来确保订单数据最终能够同步到 ES 中。

2. 基于消息队列的异步同步方案

这是使用最广泛的一种方案。核心思想是使用消息队列(如 Kafka、RabbitMQ)来保证数据库和 ElasticSearch 之间的同步。

步骤:

  1. 数据库写入:应用程序先操作数据库,执行增、删、改。
  2. 发布消息:操作完成后,生成变更事件并发布到消息队列中。
  3. 消费消息:消费者从消息队列中读取消息,执行对 ElasticSearch 的增删改,保证 ElasticSearch 中的数据与数据库保持同步。

2.1 优势

  • 解耦:通过消息队列解耦数据库和 ElasticSearch 的写入,可以处理高并发和数据同步延迟问题。
  • 可靠性:消息队列可以设置消息的持久化,保证即使系统故障后,消息仍然不会丢失。
  • 可扩展性:消息队列可以水平扩展,支持高负载下的异步处理。

2.2 问题

  • 延时:由于 ElasticSearch 的写入是异步的,可能会存在数据延迟,无法做到强一致性。
  • 消息丢失:如果消息队列中消息丢失,可能导致数据不一致。

2.3 解决方案

  • 消息持久化:确保消息队列中的消息持久化,防止消息丢失。
  • 重试机制:如果 ElasticSearch 写入失败,可以通过重试机制确保数据最终写入 ES。
  • 定期对账:可以定期从数据库中获取数据与 ES 中的数据进行比对,修复不一致的数据。

举例:

  • 电商系统的商品同步:当数据库中商品信息发生变更时,生成一条变更消息并发送到 Kafka。ElasticSearch 消费者从 Kafka 中获取消息,更新商品数据到 ES 索引。如果更新失败,消费者可以通过重试机制确保最终同步成功。

3.扫描表定时同步

这种方式是通过定期任务扫描数据库表,批量更新 ES。适合对数据实时性要求较低的场景。

优点:

  • 无侵入性:不需要修改业务代码,完全通过定时任务进行数据同步。
  • 实现简单:不依赖其他复杂的机制,直接从数据库读取数据并更新到 ES。

缺点:

  • 实时性差:数据更新的延迟较大,无法做到实时同步。
  • 性能问题:频繁扫描数据库会给数据库带来额外的负担,影响数据库的性能。

举例:

  • 数据报表系统:如果一个报表系统的数据更新频率较低,可以每小时定时扫描数据库表,将最新数据批量更新到 ES 中,供后续查询使用。

4.监听 Binlog 同步

这种方式通过监听数据库的 binlog 日志,同步数据库的变更操作到 ES。可以使用成熟的框架(如 Canal)来捕获 binlog 事件并更新到 ES。

步骤:

  1. MySQL 写入数据:当用户执行订单操作(如创建订单、支付订单、发货等)时,订单信息会首先写入 MySQL 数据库,生成一条增、删、改操作的记录。
  2. 生成 Binlog 日志:MySQL 会自动生成一条 Binlog 日志,记录该订单的变化(如新增、修改、删除操作),包括订单的详细信息(如订单 ID、订单状态等)。
  3. 监听 Binlog:使用像 Canal 这样的工具,监听 MySQL 的 Binlog 日志。当有新的日志产生时,Canal 会捕获该 Binlog 事件,并解析出订单的变化信息。
  4. 同步到 ElasticSearch
    • Canal 解析出具体的数据库变更操作(如订单的创建、状态更新等)。
    • 解析后的数据通过 Canal 的客户端或自行开发的同步服务,更新到 ElasticSearch 中。
    • 比如,如果订单状态更新为“已支付”,Canal 会将这一变更信息推送至 ElasticSearch,更新对应订单的状态。

举例:

  • 电商系统订单同步:当用户下单、支付或者取消订单时,数据库会记录这些变更,并生成 binlog。通过监听这些 binlog 事件,可以实时将订单状态更新到 ES 中,确保搜索时能够获取最新的订单状态。

ES锁的支持

支持乐观,不支持悲观~