ES现在已经越来越火,很多公司会把mysql里面的数据导入到ES,用ES来做海量数据的实时查询。那为什么mysql做不了海量数据的实时查询呢?为什么ES能做?
这个问题很有意思,我自己也很感兴趣,因此进行了一些分析和对比,得出结论如下:
1.es天生的分布式架构,天然支持海量数据的分片和查询,而mysql不行
2.mysql和es底层索引结构导致即便是单片数据查询,es也更适合做查询引擎
具体分析过程如下:
为什么mysql做不了海量数据的实时查询?
mysql的架构就不是分布式的架构,就算基于一些开源组件或者自研组件做了分库分表,也只能解决数据的存储问题,解决不了问题的查询问题。目前所有的分库分表中间件都会要求必须带上分库分表id去查询,也就是分库分表之后,你的查询,最终还是要落到某个库某张表去做单库单表的实时查询。
而ES为啥能够做海量数据的实时查询?
ES天生就是分布式架构,假如有一个学生表超过10亿条数据,导入es。首先第一件事情就是你要给这些数据做分片,假如分了10片后,用户想基于name去查询,es是怎么做的?这里我们假设是最简单的部署架构,es的节点A接收到查询请求后,会把请求转发到其他datanode,然后每个datanode接收到数据后,会在本机进行数据查询,然后将查询结果id,score返回节点A,然后由节点A对所有返回结果再进行排序和分页,然后节点A再去各个datanode,依据id拿到原始数据合并后返回给用户。
为什么mysql不这样做呢?为什么mysql的分开分表中间件不像es这样用一个节点去合并和处理数据呢?
个人理解,不是mysql或者分库分表中间件不想做,而是根本做不了。原因是单库单表mysql也支持不了复杂查询条件下的实时查询,比如任意列的相互组合查询。
想要快速查询,避免全表扫描,就需要索引。mysql可以对单列增加索引,也可以对多列增加索引,或者按照一定顺序建立多个字段的组合索引。但mysql的索引存在很多问题:
mysql单列索引的问题是通常用户查询条件不会仅仅只有一个。
mysql多列索引问题是mysql引擎执行是只会选择最严格那一个索引执行,然后在其“中间结果集”上进行其他条件的过滤(后面可以看到es不是这样做的)。这意味着建多列索引实际上可能是无用的。
mysql组合索引的问题是建立组合索引,是要按照顺序的,而且执行的时候是从左到右匹配执行的,一旦查询条件不符合索引组合次序,或者查询条件多变,组合索引就会毫无用处。
大家都知道mysql的默认索引数据结构是B+tree,每增加一个索引,都是增加一个B+tree,就是因为这种数据结构导致了上面种种问题。
那es的底层索引又是什么结构呢?为什么es可以做到多个字段任意组合查询呢?
es的底层是lucene,lucene的索引叫倒排索引(Inverted Index)。
下面是一个倒排索引的示例图:
倒排索引(Inverted Index):倒排索引是实现“单词-文档矩阵”的一种具体存储形式,通过倒排索引,可以根据单词快速获取包含这个单词的文档列表。比如上图中,lucene这个单词对应的documentId list就是2,3,10,35,92;
倒排索引主要由两个部分组成:“单词词典”和“倒排列表”。
单词词典(Lexicon):搜索引擎的通常索引单位是单词,单词词典是由文档集合中出现过的所有单词构成的字符串集合,单词词典内每条索引项记载单词本身的一些信息以及指向“倒排列表”的指针。
倒排列表(PostingList):倒排列表记载了出现过某个单词的所有文档的文档列表及单词在该文档中出现的位置信息,每条记录称为一个倒排项(Posting)。根据倒排列表,即可获知哪些文档包含某个单词。
倒排文件(Inverted File):所有单词的倒排列表往往顺序地存储在磁盘的某个文件里,这个文件即被称之为倒排文件。
关于这些概念之间的关系,通过下图可以比较清晰的看出来。
想了解更多倒排索引知识,请参考:https://blog.csdn.net/starzhou/article/details/87519973
怎么从词典里面快速找到单词呢。这里可以使用的数据结构有很多,lucene使用了一种叫FST的数据结构:
| 数据结构 | 优缺点 |
|---|---|
| 排序列表Array/List | 使用二分法查找,不平衡 |
| HashMap/TreeMap | 性能高,内存消耗大,几乎是原始数据的三倍 |
| Skip List | 跳跃表,可快速查找词语,在lucene、redis、Hbase等均有实现。相对于TreeMap等结构,特别适合高并发场景 |
| Trie | 适合英文词典,如果系统中存在大量字符串且这些字符串基本没有公共前缀,则相应的trie树将非常消耗内存 |
| Double Array Trie | 适合做中文词典,内存占用小,很多分词工具均采用此种算法 |
| Ternary Search Tree | 三叉树,每一个node有3个节点,兼具省空间和查询快的优点 |
| Finite State Transducers (FST) | 一种有限状态转移机,Lucene 4有开源实现,并大量使用 |
想了解,FST是怎么实现的请参考lucene字典实现原理:https://www.cnblogs.com/LBSer/p/4119841.html
上面讲的单个字段的搜索,假如是多个字段的搜索,lucene是如何对多个字段的搜索结果进行合并的呢?
倒排表PostingList在lucene是用skiplist数据结构存储的。
假如我们有下面三个倒排链需要进行合并。
在lucene中会采用下列顺序进行合并:
1.在termA开始遍历,得到第一个元素docId=1
2.Set currentDocId=1
3.在termB中 search(currentDocId) = 1 (返回大于等于currentDocId的一个doc),
因为currentDocId ==1,继续
如果currentDocId 和返回的不相等,执行2,然后继续
1.到termC后依然符合,返回结果
2.currentDocId = termC的nextItem
3.然后继续步骤3 依次循环。直到某个倒排链到末尾。
整个合并步骤我可以发现,如果某个链很短,会大幅减少比对次数,并且由于SkipList结构的存在,在某个倒排中定位某个docid的速度会比较快不需要一个个遍历。可以很快的返回最终的结果。
综上所述从倒排索引的定位,查询,合并整个流程组成了lucene的查询过程,和传统数据库的索引相比,lucene合并过程中的优化减少了读取数据的IO,倒排合并的灵活性也解决了mysql B+tree索引难以支持多条件查询的问题。
更多lucene查询过程,请参考:https://segmentfault.com/a/1190000014480912