掘金 后端 ( ) • 2024-06-25 14:48

本文首发于公众号:托尼学长,立个写 1024 篇原创技术面试文章的flag,欢迎过来视察监督~

在技术面试中,遇到过这样一个有意思的场景,且听我娓娓道来。

面试官:“在你的系统里,都做过哪些性能优化,可以讲讲吗?”

候选人:“有些数据库查询检索类操作,我们用MySQL数据库会比较慢,所以全部挪到ElasticSearch(简称ES)中去做了,性能马上就提升了挺多。”

面试官:“为什么挪到ES里面性能就提升了,可以从技术原理上讲一讲吗?”

候选人:“因为ES用的是倒排索引啊,而MySQL用的是B+ Tree索引,前者的性能要快很多。”

面试官:“为什么倒排索引比B+ Tree索引快很多,可以从实现机制上讲讲吗?”

候选人:“这个。。。具体实现我就不清楚了。”

面试官:“那你真的认为ElasticSearch比MySQL快吗?”

候选人挠了挠头,说:“确实是快,但您这么一问,我确实答不上来ES到底快在哪里了。”

对话到此为止,下面我来回答一下这个问题。

ES的底层实现

先来介绍一下大家众所周知的ES特性 —— 倒排索引。

嗯,善于举一反三的同学看到“倒排索引”这四个字,马上就在想,与其相应对应的是不是“正排索引”?

没错儿,是的,这两个我一起介绍。

正排索引(Forward Index)的实现方式为,通过文档ID去查找整个文档内容,适用于全部文档遍历或根据某个文档ID查找内容的场景。

图片

倒排索引(Inverted Index)的实现方式,则是通过文档中的关键词去查找文档ID列表,这也就是其非常适用于全文检索的原因。

图片

这里面再补充一句,上图中的单词列,是通过ES的分词器按照一定的逻辑,将一段文本转换成一系列单词来实现的。

很多同学都认为ES是通过倒排索引来实现的查询检索,其实这种说法并不全面,而且倒排索引的结构也并非这么简单。

如下图所示:

图片

注:Term Index = 单词索引,Term Dictionary = 单词字典,Posting List = 倒排列表。

(1)当用户搜索“database”关键字的时候,会先通过以树结构(Burst-Trie)存储在内存中的Term Index中的“d”,查找到Term Dictionary中的“database”。

需要说明的是,Term Index不会存储完整的Term,仅仅存储Term的前缀来节省内存,通过Term Index可以快速地定位到Term Dictionary中的某个位置,再从这个位置再向后顺序查找。

打个比方,该步骤很像我们在上大学时候背的英语四六级字典,根据字母的顺序ABC在目录中查找到对应的单词。

(2)定位到Term Dictionary中的“database”关键字后,即可查找到Posting List中所对应的文档ID为3。

这个步骤就像根据目录中的单词,对应到其在字典中所对应的页数。

(3)再通过为3的文档ID,从ES的正排索引中查找到对应的数据信息“MySQL is a relational database”,并返回最终结果。

这个步骤就像我们最后翻到字典中某个页数,找到了对于该单词的详细解释,包括:音标、中文、例句等。

ES复杂查询的实现方式和全文检索比较类似,同样是用到了倒排索引 + 正排索引的实现机制。

假设有如下一张student表:

图片

当将该表中的数据保存到ES时,ES会为表中的每个字段都建立一个倒排索引。以name和age字段为例,如下图所示:

图片

接下来,我们看看具体的执行步骤。

图片

(1)当用户在name字段中查询“Mary”的时候,会先通过以树结构(Burst-Trie)存储在内存中的Term Index中的“M”,查找到Term Dictionary中的“Mary”。

(2)定位到Term Dictionary中的“Mary”关键字后,即可查找到Posting List中所对应的文档ID为3。

(3)再通过为3的文档ID,从ES的正排索引中查找到对应的数据列,并返回最终结果。

MySQL的底层实现

有一件出乎大家意料的事情,那就是MySQL InnoDB是支持全文检索的,并且其底层也是通过倒排索引来实现的。

在InnoDB中,倒排索引是通过full inverted index的方式来实现的,存放在全文检索的辅助表(Auxiliary Table)中。该表中有两个字段,word和ilist 字段,word字段上有设有索引,ilist中包含 (Documentld,Position)。

如下图所示:

图片

图片

btw:该截图来自于《MySQL技术内幕 —— InnoDB存储引擎》一书,表 5-8第一行中的“code”实为“cold”。

同样,InnoDB也提供了红黑树结构的FTS Index Cache(全文检索索引缓存)来提高性能。

由此来看,InnoDB的实现机制是不是跟ES的很像?但毕竟术业有专攻,ES在全文检索的性能上还是优于MySQL InnoDB的,在这里就不展开解释了。

但至少,“ES是通过倒排索引来实现的查询检索,而MySQL是通过B+ Tree来实现的”这句话,就显得不那么准确了。

说完了MySQL的全文检索,接下来我们说说MySQL是如何实现查询操作的,这个想必有很多同学都是清楚的。

如下图所示:

图片

一共分为两步,先通过name的二级索引树查询’Mary‘所对应的记录ID,拿到记录ID后去表记录索引树中查到对应的行记录。

照此看来,MySQL在查询上跟ES的实现方式也是大同小异的,无非是根据B+ Tree索引或倒排索引去查询对应的记录ID,然后再进行二次回表而已。

甚至MySQL如果可以用到覆盖索引,那连回表操作也都省去了,没准还比ES查询效率更高呢。

So,那ES真的比MySQL快吗?如何从底层实现原理的角度去说服面试官呢?

性能高的最大原因

假设有这样一个业务场景,如下图所示:

图片

在该场景下,不仅有千万量级的数据记录,多达二十几个的搜索条件,而且这些搜索项和返回的数据结果并不在一个数据表中,需要进行多表关联查询。

我们需要思考的一个问题是,如何给在这个场景下进行索引创建?

由于MySQL InnoDB的联合索引,很大程度上是要遵循最左前缀原则的,这就意味着在多维复杂查询的某些场景中,你需要通过(a,b,c),(a,c,b),(b,a,c),(b,c,a),(c,a,b),(c,b,a)这种排列组合创建联合索引的方式,保证用户的查询操作可以完全命中索引。

那上述业务场景中有20多个查询项的情况下,通过最左前缀原则、排列组合创建联合索引的方式一定是覆盖不全,况且都不在一个数据库表中,这是更加不可控的。

Btw:之所以在上文中用到了“很大程度上”,是因为MySQL 8引入了Skip Scan Range Access Method,它在一定条件下可以不遵守最左前缀原则,利用了范围扫描来替代了全表扫描的发生。

其实现原理为:MySQL隐式的构造了前缀查询条件,使一条查询就变成了多次查询,执行计划type = range。适用于最左条件区分度较低的情况,否则生成SQL过多,与全表扫描相比并无优势。

而该场景下,通过ES是可以轻松解决的,其实现机制是“结果合并策略”。

假设我们需要查出来姓名为lily,并且年龄为12岁的学生记录,如下图所示:

图片

图片

在ES中,支持SkipList和Bitset两种方式进行数据结果合并,前者是采用相互跳跃对比并得出合集的方式,后者则通过倒排列表的值计算出各自的Bitset,并进行AND操作合并。

通过该种方式,则可以轻松解决上述20多个查询项、千万量级数据记录的业务场景中,为其创建索引的问题。

我认为,这就是我们常说的ES比MySQL查询性能高的最大原因。

结语

洋洋洒洒地写了这么多,希望能在这道常见的技术面试题上,对同学们能够有所启发。