Elasticsearch
简介
ElasticSearch是什么?
ElasticSearch简称ES,是一个开源的高扩展的分布式全文搜索引擎,是整个Elastic技术栈的核心(Elastic技术栈包括ElasticSearch、Kibana、Beats和Logstach),它可以近乎实时的存储、检索数据;本身扩展性很好,可以扩展到上百台服务器,处理PB级别数据。
全文搜索
对于搜索引擎Google、Baidu等,它们都是根据网页中的关键字生成索引,我们在搜索的时候输入关键字,在后台模糊匹配返回,对于关系型数据库,根据最左匹配原则,有时候也能够处理一下模拟匹配(如**'keyword%'这种格式),但是在很多时候,比如’%keyword‘,’%keyword%‘**等就需要检索全文显得很鸡肋,一般情况下也不会用关系型数据库存储全文,即使建立了索引,维护起来也比较麻烦,用主流的技术则显得事半功倍,总结:
- 搜索的数据对象是大量的非结构化的文本数据。
- 文件记录量达到数十万或数百万个甚至更多。
- 支持大量基于交互式文本的查询。
- 需求非常灵活的全文搜索查询。
- 对高度相关的搜索结果的有特殊需求,但是没有可用的关系数据库可以满足。
- 对不同记录类型、非文本数据操作或安全事务处理的需求相对较少的情况。为了解决结构化数据搜索和非结构化数据搜索性能问题,我们就需要专业,健壮,强大的全文搜索引擎 。
安装
LinuxES单实例安装(以5.2.2为例)
ES在lunix下不支持root启动
下载ES:wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-5.2.2.tar.gz
创建es目录 mkdir es
解压:tar -zxvf elasticsearch-5.2.2.tar.gz -C /es
创建es用户 adduser es
初始化密码 passwd es
赋予es用户文件权限 chmod -R es es
创建目录 mkdir -p /usr/app/es
移动es到 /usr/app/es下 mv /es /usr/app/es
切换用户 su es
切换目录cd /usr/app/es
启动es sh ./bin/elasticsearch
测试 curl http://localhost:9200/
Window
直接下载相关的包,运行bin目录下的elasticsearch.bat 文件启动 ES服务即可,除此之前我在网上爬取了一些数据,直接用于学习测试。
常用操作
Restful简介
ES支持使用Restful风格进行调用,Restful中文名为表属性状态转移,是Web应用前后端交互的一种协议、架构和原子,可以简单地理解根据这种风格的URL可以得出“请求的资源对象和想要对资源对象进行何种操作”。
ES常用操作
创建索引
创建了一个名为job2的索引,acknowledged:响应结果,shards_acknowledged:分片结果,index:索引名称。
查看所有的索引
- 表头 含义
- health 当前服务器健康状态: green(集群完整) yellow(单点正常、集群不完整) red(单点不正常)
- status 索引打开、关闭状态
- index 索引名
- uuid 索引统一编号
- pri 主分片数量
- rep 副本数量
- docs.count 可用文档数量
- docs.deleted 文档删除状态(逻辑删除)
- store.size 主分片和副分片整体占空间大小
- pri.store.size 主分片占空间大小
查看单个索引
命令:http://121.4.56.103:9200/job
返回结果:
{
"job": {
"aliases": {}, //别名
"mappings": {} //映射
},
"settings": {
"index": {
"creation_date": "1622888417389", //创建时间
"number_of_shards": "1", //分片数量
"number_of_replicas": "1",//副本数量
"uuid": "vxSX-ot_S660o6BcCGQ8fQ", //唯一标识索引Id
"version": {
"created": "7070099" //版本号
},
"provided_name": "job" //提交的索引名
}
}
}
}
删除索引
创建文档
返回结果:
{
“_index”: “job”, //索引名称
“_type”: “_doc”, //类型 默认为doc
“_id”: “20210613”, //索引ID
“_version”: 1, //索引版本号
“result”: “created”, //结果
“_shards”: {
“total”: 2,
“successful”: 1,
“failed”: 0
},
“_seq_no”: 104364,
“_primary_term”: 2
}
如果没有指定Id,ES将随机生成一个UUID
索引文档
根据Id索引
查询所有
由于数据太大,这里选择分页,from:偏移量,size:每页数量
修改文档
全量修改
局部修改
删除文档
条件查询
查询所有
GET /job/_search
{
“query”: {
“match_all”: {}
}
, “from”: 0
,“size”: 200
}
查询结果返回指定字段
GET /job/_search
{
“_source”: [“job_name”,“job_company”],
“query”: {
“match_all”: {}
}
, “from”: 0
,“size”: 20
}
查询结果排序
GET /job/_search
{
“_source”: [“job_name”,“job_company”],
“query”: {
“match_all”: {}
}
, “from”: 0
,“size”: 20
}
多条件查询
GET /job/_search
{
“query”: {
“bool”: {
“must”: [
{“match”: {
“job_area”: “广州”
}
},{“match”: {
“kind_name”: “java”
}
}]
}
}
, “from”: 0
,“size”: 20
}
按照job_area和kind_name查询,类型数据库的and操作,如果是or,把must换成or即可
范围查询
GET /job/_search
{
“query”: {
“bool”: {
“must”: [
{“match”: {
“job_area”: “广州”
}
},{“match”: {
“kind_name”: “java”
}
}],
“filter”: {
“range”: {
“max_salary”: {
“gte”: 20000,
“lte”: 30000
}
}
}
}
}
, “from”: 0
,“size”: 20
}
查找工作地在广州、类别为java,工资在20000到30000的文档,条数为20
分词和精准查询
GET /job/_search
{
“query”: {
“match”: {
“job_name”: “java AI”
}
}
, “from”: 0
,“size”: 20
}
GET /job/_search
{
“query”: {
“match_phrase”: {
“job_name”: “java”
}
}
, “from”: 0
,“size”: 20
}
上面会对“java AI”进行分期,会查找“java”和“AI”的结果集,下面会精准匹配"java"
分组
GET /job/_search
{
“aggs”: {
“area_group”: {
“terms”: {
“field”: “kind_name”
}
}
}
, “size”: 0
}
按照kind_name进行分组,有时候text格式是默认禁止分组等聚合操作,我需要对此进行设置
PUT job/_mapping
{
“properties”: {
“kind_name”: {
“type”: “text”,
“fielddata”: true
}
}
}
求平均值
GET /job/_search
{
“aggs”: {
“area_group”: {
“terms”: {
“field”: “kind_name”
}
}
}
, “size”: 0
}
Java高级版客户端基本操作
Java提供了两个客户端对ES进行操作,分别Java Low Level REST Client和Java High Level REST Client:
- Java Low Level REST Client: 低级别的REST客户端,通过http与集群交互,用户需自己编组请求JSON串,及解析响应JSON串。兼容所有ES版本。
- Java High Level REST Client: 高级别的REST客户端,基于低级别的REST客户端,增加了编组请求JSON串、解析响应JSON串等相关api。使用的版本需要保持和ES服务端的版本一致,否则会有版本问题。
POM引入
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.10.10</version>s
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
配置
@Bean
public RestHighLevelClient getClient(){
RestHighLevelClient client=new RestHighLevelClient(
RestClient.builder(new HttpHost("121.4.56.103",9200,"http")));
return client;
}
基本操作
创建索引
@org.junit.jupiter.api.Test
public void createIndex() throws IOException {
CreateIndexRequest createIndexRequest = new CreateIndexRequest("job2");
CreateIndexResponse response = restHighLevelClient.indices().create(createIndexRequest, RequestOptions.DEFAULT);
System.out.println(response.isAcknowledged());
}
删除索引
@org.junit.jupiter.api.Test
public void deleteIndex() throws IOException {
DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest("job2");
AcknowledgedResponse response = restHighLevelClient.indices().delete(deleteIndexRequest, RequestOptions.DEFAULT);
System.out.println(response.isAcknowledged());
}
根据Id查找
@org.junit.jupiter.api.Test
public void selectAll() throws IOException {
GetRequest getRequest = new GetRequest("job", "20210613");
GetResponse response = restHighLevelClient.get(getRequest, RequestOptions.DEFAULT);
System.out.println(JSON.toJSONString(response.getSource()));
}
新增
@org.junit.jupiter.api.Test
public void insertOne() throws IOException {
IndexRequest indexRequest = new IndexRequest("job");
indexRequest.id("202106131");
JobData data = new JobData();
data.setId(202106131);
data.setJob_name("java后台开发");
data.setJob_area("上海");
indexRequest.source(new ObjectMapper().writeValueAsString(data),XContentType.JSON);
IndexResponse indexResponse = restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
System.out.println(indexResponse.getResult());
}
批量新增
@org.junit.jupiter.api.Test
public void batchInsert() throws IOException {
int begin = 0;
int end = 100;
while (begin<=end){
BulkRequest bulkRequest = new BulkRequest();
List<JobData> jobData = jobDataMapper.selectList(begin, begin + 2000);
for (JobData data:jobData){
IndexRequest indexRequest = new IndexRequest("job");
indexRequest.id(data.getId().toString());
indexRequest.source(JSONObject.toJSONString(data), XContentType.JSON);
bulkRequest.add(indexRequest);
}
BulkResponse bulk = restHighLevelClient.bulk(bulkRequest,RequestOptions.DEFAULT);
int status = bulk.status().getStatus();
System.out.println("==============================begin:"+begin+"===========status:"+status);
begin+=2000;
}
}
分页
@org.junit.jupiter.api.Test
public void pageSelect() throws IOException {
SearchRequest request = new SearchRequest("job");
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.from(0);
builder.size(200);
builder.sort(new FieldSortBuilder("max_salary").order(SortOrder.DESC));
request.source(builder);
SearchResponse search = restHighLevelClient.search(request, RequestOptions.DEFAULT);
SearchHit[] hits = search.getHits().getHits();
for (SearchHit searchHit:hits){
System.out.println(searchHit.getSourceAsString());
}
}
条件查询
@org.junit.jupiter.api.Test
public void conditionalQuery() throws IOException {
SearchRequest request = new SearchRequest("job");
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.from(0);
builder.size(200);
builder.sort(new FieldSortBuilder("max_salary").order(SortOrder.DESC));
builder.query(QueryBuilders.termQuery("job_area.keyword","广州"));
request.source(builder);
System.out.println(request.source().toString());
SearchResponse search = restHighLevelClient.search(request, RequestOptions.DEFAULT);
SearchHit[] hits = search.getHits().getHits();
for (SearchHit searchHit:hits){
System.out.println(searchHit.getSourceAsString());
}
}
多条件组合查询
@org.junit.jupiter.api.Test
public void combinedQuery() throws IOException {
SearchRequest request = new SearchRequest("job");
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.from(0);
builder.size(200);
builder.sort(new FieldSortBuilder("max_salary").order(SortOrder.DESC));
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
MatchQueryBuilder matchQuery1 = QueryBuilders.matchQuery("job_area.keyword", "广州");
MatchQueryBuilder matchQuery2 = QueryBuilders.matchQuery("kind_name.keyword", "java");
MatchQueryBuilder matchQuery3 = QueryBuilders.matchQuery("academic_requirement.keyword", "本科");
boolQuery.must().add(matchQuery1);
boolQuery.must().add(matchQuery2);
boolQuery.must().add(matchQuery3);
builder.query(boolQuery);
request.source(builder);
System.out.println(request.source().toString());
SearchResponse search = restHighLevelClient.search(request, RequestOptions.DEFAULT);
SearchHit[] hits = search.getHits().getHits();
for (SearchHit searchHit:hits){
System.out.println(searchHit.getSourceAsString());
}
}
Window集群部署
节点1配置文件
# ---------------------------------- Cluster -----------------------------------
cluster.name: my-es
# ------------------------------------ Node ------------------------------------
node.name: es_node1
node.master: true
node.data: true
# ----------------------------------- Paths ------------------------------------
# ----------------------------------- Memory -----------------------------------
# ---------------------------------- Network -----------------------------------
network.host: localhost
http.port: 9200
transport.tcp.port: 9300
# --------------------------------- Discovery ----------------------------------
# ---------------------------------- Gateway -----------------------------------
# ---------------------------------- Various -----------------------------------
http.cors.enabled: true
http.cors.allow-origin: "*"
节点2配置文件
# ======================== Elasticsearch Configuration =========================
# ---------------------------------- Cluster -----------------------------------
cluster.name: my-es
# ------------------------------------ Node ------------------------------------
node.name: es_node2
node.master: true
node.data: true
# ----------------------------------- Paths ------------------------------------
# ----------------------------------- Memory -----------------------------------
# ---------------------------------- Network -----------------------------------
network.host: localhost
http.port: 9201
transport.tcp.port: 9301
# --------------------------------- Discovery ----------------------------------
discovery.seed_hosts: ["localhost:9300"]
discovery.zen.fd.ping_timeout: 1m
discovery.zen.fd.ping_retries: 5
# ---------------------------------- Gateway -----------------------------------
# ---------------------------------- Various -----------------------------------
http.cors.enabled: true
http.cors.allow-origin: "*"
节点3配置文件
cluster.name: my-es
# ------------------------------------ Node ------------------------------------
node.name: es_node3
node.master: true
node.data: true
# ----------------------------------- Paths ------------------------------------
# ---------------------------------- Network -----------------------------------
network.host: localhost
http.port: 9202
transport.tcp.port: 9302
# --------------------------------- Discovery ----------------------------------
discovery.seed_hosts: ["localhost:9301","localhost:9302"]
discovery.zen.fd.ping_timeout: 1m
discovery.zen.fd.ping_retries: 5
# ---------------------------------- Gateway -----------------------------------
# ---------------------------------- Various -----------------------------------
http.cors.enabled: true
http.cors.allow-origin: "*"
在文件复制时得删除相关的data,本地操作时遇到集群无法发现节点,删除data即可
创建一个user索引,分片为3副本为1
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l9zbCYHe-1624090823231)(C:\Users\Administrator\Desktop\笔记\创建集群节点.png)]
如图所示,黑体加粗的代表分片master,正常代表备份节点。
核心概念学习
Index(索引)
索引是一种特殊的数据结构,用来组织数据,加快数据的读取。在ES中,索引一般由一个和多个分片组成,索引名字唯一。
Type(类别)
类别是指索引内部的逻辑分区,通过type的名字在索引内部进行唯一性表示,高版本已废弃,默认是_doc。
Document(文档)
索引内部的一条数据叫做一个文档,与关系型数据相比,是数据库表中的行。
Fields(字段)
字段即是类似于数据库中的列,是文档的组成部分。
Mapping(映射)
Mapping规定了索引中字段的存储类型、分词方式、是否存储等信息,类似于数据库中的表结构。
Node(节点)
节点是组成ES集群的基本单元,每个运行的实例都是一个节点。
Cluster(集群)
ES集群,是由具有相同的cluster.name的一个或多个节点组成的,协同工作,共享数据的服务群。
Shards(分片)
当一个索引的数据量太大时,受限于单个节点的内存、磁盘处理能力等,节点无法足够快地响应客户端的请求,此时需要将一个索引上的数据进行水平拆分,拆分出来的每个数据部分称之为一个分片。一般来讲,分片处在不同的服务器上。
Replicas(备份)
备份也称为副本,是对主分片的备份,这种备份是精确的,当请求过来时会在主分片上进行索引建立,再分发到备份分片上进行索引建立,最后返回结果。
进阶知识点学习
路由控制
分片的选择依据hash算法来决定
shard = hash(routing) % number_of_primary_shards
routing表示文档的id,number_of_primary_shards表示分片的数量,
分片的数量一旦确定下来就永远不会改变。
写数据流程
新建索引和删除请求等写操作,首先在主分片上完成,才会进一步分发到副本上。客户端收到响应表明已经完成了在主分片和副本上的工作,但是当副本太多时,会影响性能,这时候你可以根据实际对参数设置来选择,
consistency
即一致性。在默认设置下,即使仅仅是在试图执行一个写操作之前,主分片都会要求必须要有规定数量quorum(或者换种说法,也即必须要有大多数)的分片副本处于活跃可用状态,才会去执行写操作(其中分片副本 可以是主分片或者副本分片)。这是为了避免在发生网络分区故障(network partition)的时候进行写操作,进而导致数据不一致。 规定数量即: int((primary + number_of_replicas) / 2 ) + 1
consistency 参数的值可以设为:
- one :只要主分片状态 ok 就允许执行写操作。
- all:必须要主分片和所有副本分片的状态没问题才允许执行写操作。
- quorum:默认值为quorum , 即大多数的分片副本状态没问题就允许执行写操作。
注意,规定数量的计算公式中number_of_replicas指的是在索引设置中的设定副本分片数,而不是指当前处理活动状态的副本分片数。如果你的索引设置中指定了当前索引拥有3个副本分片,那规定数量的计算结果即:int((1 primary + 3 replicas) / 2) + 1 = 3,如果此时你只启动两个节点,那么处于活跃状态的分片副本数量就达不到规定数量,也因此您将无法索引和删除任何文档。
timeout
如果没有足够的副本分片会发生什么?Elasticsearch 会等待,希望更多的分片出现。默认情况下,它最多等待 1 分钟。 如果你需要,你可以使用timeout参数使它更早终止:100是100 毫秒,30s是30秒。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c6AyiOm7-1624090823237)(C:\Users\Administrator\Desktop\笔记\写流程.png)]
数据读取流程
数据读取时,客户端会将数据随机发生给一个节点,再由这个节点来选择合适的副本分片进行请求分发,这个节点称为协调节点。
文档的更新流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gsS88IET-1624090823239)(C:\Users\Administrator\Desktop\笔记\数据更新.png)]
部分更新一个文档的步骤如下:
- 客户端向Node 1发送更新请求。
- 它将请求转发到主分片所在的Node 3 。
- Node 3从主分片检索文档,修改_source字段中的JSON,并且尝试重新索引主分片的文档。如果文档已经被另一 个进程修改,它会重试步骤3 ,超过retry_on_conflict次后放弃。
- 如果 Node 3成功地更新文档,它将新版本的文档并行转发到Node 1和 Node 2上的副本分片,重新建立索引。一旦所有副本分片都返回成功,Node 3向协调节点也返回成功,协调节点向客户端返回成功。
当主分片把更改转发到副本分片时, 它不会转发更新请求。 相反,它转发完整文档的新版本。请记住,这些更改将会异步转发到副本分片,并且不能保证它们以发送它们相同的顺序到达。 如果 Elasticsearch 仅转发更改请求,则可能以错误的顺序应用更改,导致得到损坏的文档。
倒排索引
倒排索引跟正排索引概念相反
- 正排索引。正排索引将文档跟某个id关联起来,要找个字段,先找这个id,再根据id找到文档,最后对比文档中的字段是否满足条件。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6q8yy9d8-1624090823240)(C:\Users\Administrator\Desktop\笔记\正排.png)]
- 倒排索引是利用分词,把文档进行分词,将字段与文档关联起来,通过字段反向找到文档,最后进行比较。
倒排索引不可变
早期的全文检索会为整个文档集合建立一个很大的倒排索引并将其写入到磁盘。 一旦新的索引就绪,旧的就会被其替换,这样最近的变化便可以被检索到。
倒排索引被写入磁盘后是不可改变的:它永远不会修改。
- 不需要锁。如果你从来不更新索引,你就不需要担心多进程同时修改数据的问题。
- 一旦索引被读入内核的文件系统缓存,便会留在哪里,由于其不变性。只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提升。
- 其它缓存(像filter缓存),在索引的生命周期内始终有效。它们不需要在每次数据改变时被重建,因为数据不会变化。
写入单个大的倒排索引允许数据被压缩,减少磁盘IO和需要被缓存到内存的索引的使用量。
当然,一个不变的索引也有不好的地方。主要事实是它是不可变的! 你不能修改它。如果你需要让一个新的文档可被搜索,你需要重建整个索引。这要么对一个索引所能包含的数据量造成了很大的限制,要么对索引可被更新的频率造成了很大的限制。
分段存储
索引数据会被分为多个子文件,每个子文件就叫做段。且段存在不可变性,一旦索引数据写入硬盘就不能被修改
为什么存在分段存储?
因为全部文件存在一个很大的倒排索引中,且数据量还在增加,当我们需要全量更新当前倒排索引文件时,这会使数据更新时效率很差,且耗费大量的资源。
当分段写入硬盘后会生成一个提交点,提交点意味着一个用来记录所有段信息的文件已经生成,一旦拥有了提交点就意味着该段失去写权限。
新增数据
新增数据,只需在当前文档新增一个段,写入数据即可。
删除数据
删除数据并不会把数据从段中删除,会新增一个.del文件,它会记录这些被删除文档的段信息。被标记删除的文档仍然可以被查询匹配到,但它会在最终结果被返回前通过.del文件将其从结果集中删除。
更新数据
由于段的不可变性,段的更新操作变为两个操作,即先删除后新增。ElasticSearch会将旧文档从.del文件中标记删除,然后将文档的新版本索引到一个新的段中。在查询数据时,每个版本的数据都会被查询到,但它最终结果会通过.del文件将其从结果集中移除。
段的不可变性主要缺点是存储空间占用量大,当删除数据时,旧数据不会被马上删除,而是在.del文件中被标记为删除。而旧数据只能等到段更新时才能被移除,这样就会导致存储空间的浪费。特别是频繁更新数据,则每次更新都是新增新的数据到分段,并标记旧的分段中的数据,存储空间的浪费会更多。
除此,在查询结果时会包含所有的结果集,因此主节点需要排除被标记删除的旧数据,随之带来的是查询的负担。
延迟写策略
ES中,索引写磁盘的过程是异步的。为了提高写性能,ES并没有每新增一条数据就增加一个段到磁盘上,而是采取延迟写策略,执行过程如下:每当有新的数据写入时,先将其写入JVM内存中,当达到默认的时间或者内存的数据量达到一定量时,就会触发一次刷新操作。刷新操作将内存中的数据生成到一个新的分段上并缓存到文件缓存系统,稍后刷新到磁盘并生成提交点。
为了预防数据的丢失,ES引入了事务日志。
- 新文档索引后先被写入到内存中,为了防止数据丢失,ES会追加一份数据到事务日志中。
- 新的文档持续写入内存中,同时也会记录到事务日志中,此时数据还不能被检索到。
- 当达到默认的刷新时间或内存中的数据量达到一定量后,ElasticSearch会触发一次刷新,将内存中的数据以一个新的段形式刷新到文件缓存系统并清空内存。这时新段虽未被提交到磁盘,但已经可以对外提交文档检索功能且不能被修改。
- 随着新文档索引被不断写入,当日志数据大小超过某个值,或者超过一定时间,ES会触发一次Flush,此时内存中的数据被写入一个新段,同时被写入文件缓存系统,文件缓存系统的数据通过Fsync刷新到磁盘中,生成提交点。而日志文件被删除,创建一个空日志文件。
段合并
在ES自动刷新流程中,每秒都会创建一个新的段。这自然会导致短时间段的猛增,而当段数据太多时会带来较大的资源消耗,如对文件句柄、内存和CPU的消耗。而在内容搜索阶段,由于搜索请求要检查到每一个段,然后合并查询结果,因此段越多,搜索速度越慢。
为此,ES引入段合并。段合并在后台定时进行,小的段被合并成大的段,然后这些大的段再被合并到更大的段,在合并过程中,旧的已经删除文档会被从文件系统中删除。
乐观锁
ES引入乐观锁解决冲突问题,每个数据都对于一个版本,当修改数据时发生版本变化时会重复修改这个动作。
副本的一致性
分布式系统通过副本控制协议,让用户通过一定的方法即可读取分布式系统内部各个副本的数据,这些数据在一定约束条件下是相同的,即副本的一致性。副本数据一致性是针对分布式系统的各个节点而言,不是针对某节点的副本而言的。
在分布式系统中,一致性分为强一致性、弱一致性和介于二者之间的会话一致性和最终一致性。
强一致性,要求任何时刻用户都可以读到最近一次成功更新的副本数据。弱一致性则在数据更新之后,用户无法在一定时间内读到数据最值的值,因此在实际使用很少。
会话一致性是指在会话中,用户一旦读到某个版本的数据,则不会读到比此更老的数据。最终一致性指的是集群中各个副本的数据最终能达到完全一致的状态。
从副本的角度,强一致性是最佳的,但在CAP理论中,一旦选择CP,在会短时间失去应用的可用性,因为分布式应用在同步数据时会存在延迟,在考虑整体性能、可用性、可拓展性等情况下,根据业务来选择。
副本数据的分布方式
数据的分布主要有哈希分布、按数据范围分布、按数据量分布和一致性哈希方式等。
- Hash分布最简单,直接对关键字取hash然后与机器的数据取模,但是缺点同样明显。一方面,可扩展性不强,一旦数据需要扩大,则所有数据都需要重新按hash值分布;另一方面,hash容易导致存储空间的数据分布不均。
- 按数据范围分布比较常见,一般来讲按数据特征值划分不同的区间使得集群中不同服务器处理不同区间的数据。这种方式可以避免哈希值带来的存储空间数据分布不均的情况。
- 按数据量分布和按数据范围分布比较接近,一般是将数据看做一个顺序增长,并且将数据集按照某一较为固定大小划分为若干数据块,把不同的数据块分布到不同服务器上。
- 而一致性hash,有一个hash环,数据会根据hash值分布到离自己最近的节点上(扩展)。