作者:朱培 ID:sdksdk0
最近有朋友问到Elasticsearch的一些问题,所以我这边重新总结了一些关于搜索引擎的底层原理、分布式文档系统、ES的并发控制。
一、背景知识
1、搜索的分类
我们想要寻找某些信息的时候,一般会直接去百度、谷歌、搜歌、360搜索等,搜索分为垂直搜索、互联网搜索、IT系统的搜索。搜索,就是在任何场景下,找寻你想要的信息,这个时候,会输入一段你要搜索的关键字,然后就期望找到这个关键字相关的有些信息。
2、如果用数据库做搜索会怎么样?
做软件开发的话,或者对IT、计算机有一定的了解的话,都知道,数据都是存储在数据库里面的,比如说电商网站的商品信息,招聘网站的职位信息,新闻网站的新闻信息,等等吧。所以说,很自然的一点,如果说从技术的角度去考虑,如何实现如说,电商网站内部的搜索功能的话,就可以考虑,去使用数据库去进行搜索。
a、例如,每条记录的指定字段的文本,可能会很长,比如说“商品描述”或者“文章详情”字段的长度,有长达数千个,甚至数万个字符,这个时候,每次都要对每条记录的所有文本进行扫描,去判断,你包不包含我指定的这个关键词(比如说“鞋子”)
b、还不能将搜索词拆分开来,尽可能去搜索更多的符合你的期望的结果,比如输入“暴走事件”,就搜索不出来“暴走大事件”
用数据库来实现搜索,是不太靠谱的。通常来说,性能会很差的。
当然如果你的数据量非常非常小,你直接使用like这样的sql搜索我也不予评说。
本文使用的搜索引擎是Elasticsearch,以下简称为ES。
3、全文检索和Lucene
全文检索,一般使用的是倒排索引的算法来实现的。
lucene,就是一个jar包,里面包含了封装好的各种建立倒排索引,以及进行搜索的代码,包括各种算法。我们就用java开发的时候,引入lucene jar,然后基于lucene的api进行去进行开发就可以了。用lucene,我们就可以去将已有的数据建立索引,lucene会在本地磁盘上面,给我们组织索引的数据结构。
举例说明全文检索的过程:
例如现在有下面四个语句
- 1
- 2
- 3
- 4
- 5
,我们来进行分词,假设我们现在分为画、江、湖、江湖、电影、海报、文章、新闻这几个词(实际上会更多),然后我们通过一个倒排索引来匹配。
如图所示:
利用倒排索引,进行搜索的话,假设现在有100万条数据,拆分出来的词语,假设有1000万个,那么在倒排索引中,就有1000万行,我们并不需要搜索1000万次,我们就可以找到这个搜索词对应的数据。
4、ES的安装
这里我们使用的是ES5.2版本的,需要JDK至少1.8.0——73以上版本。
- 1、安装JDK,至少1.8.0_73以上版本,java -version
- 2、下载和解压缩Elasticsearch安装包,目录结构
3、启动Elasticsearch:bin\elasticsearch.bat,es本身特点之一就是开箱即用,如果是中小型应用,数据量少,操作不是很复杂,直接启动就可以用了
4、检查ES是否启动成功:http://localhost:9200/?pretty
如果网页中显示如下信息就说明已经安装成功了。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 5、修改集群名称:elasticsearch.yml
- 6、下载和解压缩Kibana安装包,使用里面的开发界面,去操作elasticsearch,作为我们学习es知识点的一个主要的界面入口
- 7、启动Kibana:bin\kibana.bat
- 8、进入Dev Tools界面
- 9、GET _cluster/health
5、ES的集群状态
从简单方面来说,ES的集群只要再重新解压一个elasticsearch-5.2.0.zip这个安装包,然后就可以自动找到已经启动的节点并自动构成一个集群。
集群的健康状况:如果只安装了一台ES,那么节点的状态就是yellow的状态,至少两台才会变成green状态 。我们可以通过以下命令来查看:
- 1
- 2
显示的结果如下:
epoch timestamp cluster status node.total node.data shards pri relo init unassign pending_tasks max_task_wait_time active_shards_percent
1509779009 15:03:29 elasticsearch yellow 1 1 1 1001 0 - 50.0%
green:每个索引的primary shard和replica shard都是active状态的
yellow:每个索引的primary shard都是active状态的,但是部分replica shard不是active状态,处于不可用的状态
red:不是所有索引的primary shard都是active状态的,部分索引有数据丢失了
为什么现在会处于一个yellow状态?
我们现在就一个笔记本电脑,就启动了一个es进程,相当于就只有一个node。现在es中有一个index,就是kibana自己内置建立的index。由于默认的配置是给每个index分配5个primary shard和5个replica shard,而且primary shard和replica shard不能在同一台机器上(为了容错)。现在kibana自己建立的index是1个primary shard和1个replica shard。当前就一个node,所以只有1个primary shard被分配了和启动了,但是一个replica shard没有第二台机器去启动。
二、document基本使用
2.1基本使用
ES是面向文档的,文档中存储的数据结构,与面向对象的数据结构是一样的,基于这种文档数据结构,es可以提供复杂的索引,全文检索,分析聚合等功能。它使用json数据格式来表达。
例如,我们在java中创建的一个bean,这里以两个表来举例,客户信息表,Employee里面包含了EmployyeeInfo这个表也就是对于employee对象:里面包含了Employee类自己的属性,还有一个EmployeeInfo对象。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
以及
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
那么,对于ES中的数据结构类型就会像下面这个样子的。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
1、查看集群中有哪些索引:
- 1
- 2
2、创建索引
- 1
- 2
3、删除索引:
- 1
- 2
4、新增文档并建立索引
语法格式为:
- 1
- 2
- 3
- 4
- 5
index指索引名、type指索引的类型、id是这条数据的id。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
创建成功之后返回:
ES会自动建立index和type,不需要提前创建,而且es默认会对document每个field都建立倒排索引,让其可以被搜索。
5、对于查询,我们使用的是GET
- 1
- 2
6、 修改
修改分为全部修改和部分修改,
全部修改就是直接替换,这种替换方式有一个不好,即使必须带上所有的field,才能去进行信息的修改
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
部分修改就是只更新部分:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
partal update实现原理:
partial update指的就是部分更新操作。例如
PUT /index/type/id,创建文档&替换文档,就是一样的语法
一般对应到应用程序中,每次的执行流程基本是这样的:
(1)应用程序先发起一个get请求,获取到document,展示到前台界面,供用户查看和修改
(2)用户在前台界面修改数据,发送到后台
(3)后台代码,会将用户修改的数据在内存中进行执行,然后封装好修改后的全量数据
(4)然后发送PUT请求,到es中,进行全量替换
(5)es将老的document标记为deleted,然后重新创建一个新的document
其实es内部对partal update的实际执行,跟传统的全量替换方式,几乎是一样的。
7、删除
直接DELETE就可以了
- 1
- 2
8、query string search
- 1
- 2
query sring search的由来,因为search参数都t是以http请求的query string来附带的 ,查询返回的参数详解:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
搜索商品名称中包含“鞋子”的商品,而且按照售价降序排序:GET /shoes/product/_search?q=name:NB&sort=price:desc
适用于临时的在命令行使用一些工具,比如curl,快速的发出请求,来检索想要的信息;但是如果查询请求很复杂,是很难去构建的
在实际的生产环境中,几乎很少使用query string search
9、query DSL
DSL:Domain Specified Language,特定领域的语言
http request body:请求体,可以用json的格式来构建查询语法,比较方便,可以构建各种复杂的语法,比query string search肯定强大多了
查询所有的商品
- 1
- 2
- 3
- 4
- 5
查询名称包含NB的商品,同时按照价格降序排序
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
分页查询商品,总共3条商品,假设每页就显示1条商品,现在显示第2页,所以就查出来第2个商品
- 1
- 2
- 3
- 4
- 5
- 6
- 7
指定要查询出来商品的名称和价格就可以
- 1
- 2
- 3
- 4
- 5
- 6
更加适合生产环境的使用,可以构建复杂的查询
10、query filter
搜索商品名称包含NB,而且售价大于300元的商品
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
11、full-text search(全文检索)
因为我们之前没有对比的数据,所以这里先post数据进去
POST /shoes/product/2
{
“name” : “NB 鞋子”,
“desc” : “特别好的鞋子”,
“price” : 720,
“producer” : “NB two producer”,
}
然后再来查询,producer这个字段,会先被拆解,建立倒排索引
:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
12、phrase search(短语搜索)
跟全文检索相对应,相反,全文检索会将输入的搜索串拆解开来,去倒排索引里面去一一匹配,只要能匹配上任意一个拆解后的单词,就可以作为结果返回
phrase search,要求输入的搜索串,必须在指定的字段文本中,完全包含一模一样的,才可以算匹配,才能作为结果返回
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
13、highlight search(高亮搜索结果)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
2.2document的核心元数据
1、_index元数据
- 1
- 2
- 3
- 4
- 5
2、_type元数据
- 1
- 2
- 3
- 4
3、_id元数据
(1)代表document的唯一标识,与index和type一起,可以唯一标识和定位一个document
(2)我们可以手动指定document的id(put /index/type/id),也可以不指定,由es自动为我们创建一个id
手动指定documnet id:一般从某些地方导入数据到es中,会采取这种方式。如果将数据导入到es中,比较适合用数据库中已经有的primary key。
1、手动指定document id
(1)根据应用情况来说,是否满足手动指定document id的前提:
一般来说,是从某些其他的系统中,导入一些数据到es时,会采取这种方式,就是使用系统中已有数据的唯一标识,作为es中document的id。举个例子,比如说,我们现在在开发一个电商网站,做搜索功能,或者是OA系统,做员工检索功能。这个时候,数据首先会在网站系统或者IT系统内部的数据库中,会先有一份,此时就肯定会有一个数据库的primary key(自增长,UUID,或者是业务编号)。如果将数据导入到es中,此时就比较适合采用数据在数据库中已有的primary key。
如果说,我们是在做一个系统,这个系统主要的数据存储就是es一种,也就是说,数据产生出来以后,可能就没有id,直接就放es一个存储,那么这个时候,可能就不太适合说手动指定document id的形式了,因为你也不知道id应该是什么,此时可以采取让es自动生成id的方式。
(2)put /index/type/id
- 1
- 2
- 3
- 4
- 5
(3)自动生成的id,长度为20个字符,URL安全,base64编码,GUID,分布式系统并行生成时不可能会发生冲突。
2、自动生成document id
(1)post /index/type
- 1
- 2
- 3
- 4
- 5
3、ES的核心机制
3.1、主备机制
ES是一个分布式的,那么对于分布式的架构,肯定是由主备机制的,在ES中,就是有shard和replica机制。(敲黑板,接下来的是重点啦)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
3.2、容错机制
Elasticsearch容错机制:master选举,replica容错,数据恢复。
- 1
- 2
- 3
- 4
- 5
容错的步骤为:
- 1
- 2
- 3
- 4
- 5
3.3、ES的并发控制
背景:
在电商平台中,用户抢购鞋子,原库存为100,线程A和线程B同时进来了,总有一个线程是先到的,假设现在线程A先到,这个时候线程A把100改为99,线程B也改为99,然而实际上两个用户抢购之后,库存应该是98,会导致数据不准确,所以这样就出现并发控制的问题。
并发解决方案
分为悲观锁和乐观锁,我们先来看悲观锁:
悲观锁方案
常见于关系型数据库中。在各种情况下都上锁,就只有一个线程可以操作这条数据,不同的场景下,有行级锁、表级锁、读锁和写锁。
工作流程:
读取鞋子数据的时候,就在这行加锁,线程A先不改数据,
线程b去读的时候会发现都不到数据,被卡住不能动。
线程A把库存改为99。
线程b可以读取库存数据了,是99件。
然后线程B再把库存改为98。
乐观锁方案
乐观锁是不加锁的,每个线程都可以操作,通过一个version号来识别,看是不是一样的,例如线程A操作后是version=1,库存改为99,同一时间有一个用户B也读到了这数据,下单也把库存变为99件,version=2,线程B去判断,version=1与es中的版本version=2,说明数据已经被其他人修改过了,然后就重新去es中读取最新版本数据,也就是99,然后再减一,就变为98件了。
总之:99—version1 98–version2.
悲观锁的优点:方便,对应用程序来说透明,缺点:并发能力低。
乐观锁:并发能力高,都需要重新比对版本号,然后可能需要重新加载数据,再写,这个过程,可能需要重复好几次
每次执行修改和删除,这个version的版本号会自动加1
在删除一个document之后,并没有物理删除掉的,因为它的版本号信息还是保留着的,先删除一条document,再重新创建这条document,其实会在delete version基础之上,再把version号加1.
es的后台都是多线程异步的,多个修改请求,是没有顺序的,例如后修改的先到。
es内部的多线程异步并发是基于自己的verison版本号进行乐观锁控制的,在后修改的先到时,filed=test3,version =2,修改后到时,先修改filed=test2,veriosn=1,此时会比较veriosn,是否相等,如果不相等的话,就直接丢弃这条数据了。
实验:——version进行乐观锁并发控制
1、先建数据:
- 1
- 2
- 3
- 4
- 5
2、然后开两个Kinana的页面,同时去获取数据:
- 1
- 2
3、其中一个客户端,先更新了一下这个数据,同时带上es中的版本号。
- 1
- 2
- 3
- 4
- 5
更新成功,数据为:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
4、另一个客户端尝试基于version=1的数据区修改,带上verison版本号。
- 1
- 2
- 3
- 4
- 5
这个时候就会报错,说明这个version已经被别人修改过了:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
5、在乐观锁成功阻止并发问题之后,尝试正确的完成更新
- 1
- 2
基于最新的数据和版本号,去进行修改,修改后,带上最新的版本号,可能这个步骤会需要反复执行好几次,才能成功,特别是在多线程并发更新同一条数据很频繁的情况下
- 1
- 2
- 3
- 4
- 5
这个时候就会更新成功:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
实验二、external verison进行乐观锁进行并发控制。
也就是说不用它内部的version版本号来控制。es提供了一个feature,就是说,你可以不用它提供的内部_version版本号来进行并发控制,可以基于你自己维护的一个版本号来进行并发控制。举个列子,加入你的数据在mysql里也有一份,然后你的应用系统本身就维护了一个版本号,无论是什么自己生成的,程序控制的。这个时候,你进行乐观锁并发控制的时候,可能并不是想要用es内部的_version来进行控制,而是用你自己维护的那个version来进行控制。
?version=1
?version=1&version_type=external
version_type=external,唯一的区别在于,_version,只有当你提供的version与es中的_version一模一样的时候,才可以进行修改,只要不一样,就报错;当version_type=external的时候,只有当你提供的version比es中的_version大的时候,才能完成修改
es,_version=1,?version=1,才能更新成功
es,_version=1,?version>1&version_type=external,才能成功,比如说?version=2&version_type=external
(1)先构造一条数据
- 1
- 2
- 3
- 4
- 5
2)模拟两个客户端同时查询到这条数据
- 1
- 2
(3)第一个客户端先进行修改,此时客户端程序是在自己的数据库中获取到了这条数据的最新版本号,比如说是3
- 1
- 2
- 3
- 4
- 5
结果为:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
(4)模拟第二个客户端,同时拿到了自己数据库中维护的那个版本号,也是3,同时基于version=3发起了修改
- 1
- 2
- 3
- 4
- 5
这个时候是修改不成功的,结果会报错,例如:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
(5)在并发控制成功后,重新基于最新的版本号发起更新
- 1
- 2
然后把版本号GET到的版本号要大,然后再去PUT。
- 1
- 2
- 3
- 4
- 5
然后才能更新成功
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
3.4 ES的bulk操作
1、bulk语法
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
每一个操作要两个json串,语法如下:
- 1
- 2
- 3
举例,比如你现在要创建一个文档,放bulk里面,看起来会是这样子的:
- 1
- 2
- 3
有哪些类型的操作可以执行呢?
(1)delete:删除一个文档,只要1个json串就可以了
(2)create:PUT /index/type/id/_create,强制创建
(3)index:普通的put操作,可以是创建文档,也可以是全量替换文档
(4)update:执行的partial update操作
bulk api对json的语法,有严格的要求,每个json串不能换行,只能放一行,同时一个json串和一个json串之间,必须有一个换行,否则就会报错。bulk操作中,任意一个操作失败,是不会影响其他的操作的,但是在返回结果里,会告诉你异常日志。
2、bulk size最佳大小
bulk request会加载到内存里,如果太大的话,性能反而会下降,因此需要反复尝试一个最佳的bulk size。一般从1000~5000条数据开始,尝试逐渐增加。另外,如果看大小的话,最好是在5~15MB之间。
3.5 document路由到shard
路由算法: shard = hash(routing) % number_of_primary_shards
举个例子,一个index有3个primary shard,P0,P1,P2
每次增删改查一个document的时候,都会带过来一个routing number,默认就是这个document的_id(可能是手动指定,也可能是自动生成)
routing = _id,假设_id=1
会将这个routing值,传入一个hash函数中,产出一个routing值的hash值,hash(routing) = 21
然后将hash函数产出的值对这个index的primary shard的数量求余数,21 % 3 = 0
就决定了,这个document就放在P0上。
决定一个document在哪个shard上,最重要的一个值就是routing值,默认是_id,也可以手动指定,相同的routing值,每次过来,从hash函数中,产出的hash值一定是相同的
无论hash值是几,无论是什么数字,对number_of_primary_shards求余数,结果一定是在0~number_of_primary_shards-1之间这个范围内的。0,1,2。
(3)_id or custom routing value
默认的routing就是_id
也可以在发送请求的时候,手动指定一个routing value,比如说put /index/type/id?routing=user_id
手动指定routing value是很有用的,可以保证说,某一类document一定被路由到一个shard上去,那么在后续进行应用级别的负载均衡,以及提升批量读取的性能的时候,是很有帮助的
3.6 document的CRUD原理
1、客户端发送请求到任意一个node,成为coordinate node
2、coordinate node对document进行路由,将请求转发到对应的node,此时会使用round-robin随机轮询算法,在primary shard以及其所有replica中随机选择一个,让读请求负载均衡
3、接收请求的node返回document给coordinate node
4、coordinate node返回document给客户端
5、特殊情况:document如果还在建立索引过程中,可能只有primary shard有,任何一个replica shard都没有,此时可能会导致无法读取到document,但是document完成索引建立之后,primary shard和replica shard就都有了。
总结:目前为止都是elasticsearch的一些基本使用,底层原理,本文主要讲述了使用搜索引擎来做搜索和用sql做搜索的对比、ES的基本安装,使用kibana网页版进行基本的增删改查操作、es的document的基本操作、es的主备机制、容错机制和并发控制、bulk操作、路由算法等。对于基础的了解,这对后面更加深入的搜索应用起到了很重要的作用,俗话说“万丈高楼平地起”,基础一定要扎实。