【elasticsearch】elasticsearch入门教程 es整合spring教程

发布时间:2023-07-20 16:00

文章目录

  • linux安装es
  • springboot-data整合es
  • es高阶
    • 分片策略
    • 推迟分片分配
    • 路由选择
      • 不带routing查询:
      • 带routing查询:
    • 写入速度优化
      • 优化存储设备
      • 合理使用合并
      • 减少 Refresh 的次数
      • 加大 Flush 设置
      • 减少副本的数量
      • 内存设置
    • 重要配置
    • Elasticsearch面试题
      • 为什么要使用 Elasticsearch?
      • Elasticsearch 的 master 选举流程?
      • Elasticsearch 集群脑裂问题?
      • Elasticsearch 搜索的流程?
      • es更新和删除文档的流程?
      • 并发下如何保证读写一致

linux安装es

windows启动可能会遇到的报错, 基本和linux是差不多的 可以参考解决方法

  1. 官网下载es 。本文以es 7.6 举例 , (es7.15会有api的弃用调整 ,7.15之前的其它7.x版本应该差距较小,但是es大版本之间的差距还是比较大 需要注意api的使用变更)

    下载地址 :https://www.elastic.co/products/elasticsearch ,找到历史版本进行下载, 这种网站都时不时会挂掉,建议下载之后备份一份到云盘。或者直接在linux上面用wget命令+下载地址进行下载, 如果找不到历史版本的下载地址,那么点击最新版的下载,下载前会弹出一个url, url中会带版本号,把这个版本号改成我们要的就可以了。

  2. 解压tar包

  3. es不能使用root启动,必须使用普通用户启动 , 为了方便 这里直接把普通用户权限改成所有权限

# 创建一个用户名为es的账号
useradd es
# 创建密码
passwd xxxxx

# 编辑权限配置
visudo
# 在root All=(ALL) ALL 这行下面 添加
es All=(ALL) ALL

# 切换到es账户
su es

  1. 修改elasticsearch配置文件
    (位于解压后es的config目录)

【elasticsearch】elasticsearch入门教程 es整合spring教程_第1张图片

  1. 修改jvm.option文件
    根据自己的服务器内存大小进行合理的调整
    【elasticsearch】elasticsearch入门教程 es整合spring教程_第2张图片

  2. 启动并观察报错提示:

# cd 到es解压后目录的bin目录 启动
./elasticsearch

报错解决:
注: 使用es用户执行命令
【elasticsearch】elasticsearch入门教程 es整合spring教程_第3张图片
【elasticsearch】elasticsearch入门教程 es整合spring教程_第4张图片

springboot-data整合es

  1. 引入jar包

我这里使用的springboot版本是2.3.4.RELEASE ,2.5.12版本之前(spring5.3.18)会有CVE漏洞,生产项目根据实际情况去考虑是否要解决 不升级版本可以通过代码补救,这里教程不再介绍

           <dependency>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
               <version>${springboot.version}</version>
           </dependency>
  1. 代码配置
/**
 * 配置类需要继承AbstractElasticsearchConfiguration
 *
 * @author qkj
 */

@ConfigurationProperties(prefix = "elasticsearch")
@Configuration
@Data
public class ElasticsearchConfig extends AbstractElasticsearchConfiguration {

    /**
     * yml里有配置 直接读取即可
     */
    private String host;
    private Integer port;

    @Override
    public RestHighLevelClient elasticsearchClient() {
        RestClientBuilder builder = RestClient.builder(new HttpHost(host, port));
        RestHighLevelClient restHighLevelClient = new
                RestHighLevelClient(builder);
        return restHighLevelClient;
    }


}
  1. 测试代码

实体类


/**
 * Document 属性:
 * indexName(); //索引名称,好⽐MySQL的数据库名
 * type(); //类型 类比数据库table,es7已弃⽤ 早期为了和关系型数据库对应 但因为是倒排索引 且只和index有关 type反而会影响性能所以废弃
 *
 * useServerConfiguration() :
 * Use server-side settings when creating the index.
 * 翻译过来就是:创建索引时使⽤服务器端设置。
 * 这⾥默认为false,如果改为true,表⽰在Spring创建索引时,Spring不会在创建的索引中设置以下设
 * 置:shards、replicas、refreshInterval和indexStoreType。这些设置将是 Elasticsearch 默认
 * 值(服务器配置),也就是说,我们⾃定义的某些配置将不会⽣效。
 *
 * short shards() default 1; // 默认分区数
 * short replicas() default 1;// 默认每个分区的备份数
 * String refreshInterval() default "1s"; //索引默认的刷新时间间隔
 * String indexStoreType() default "fs"; //索引⽂件存储类型
 * boolean createIndex() default true; //当spring容器启动时,如果索引不存在,则⾃动创建索引
 * VersionType versionType() default VersionType.EXTERNAL;//默认的版本管理类型
 *
 */
@Data
@Accessors(chain = true)
@Document(indexName = "product",shards = 3,replicas = 1)
public class Product {

    /**
     * 必要参数 等同于 es 中的"_id"
     * 产品唯一标识
     */
    @Id
    private Long id;

    /**
     * type : 字段数据类型 (当前版本是必须要有type的 注意不是table对应那个type 是字段的)
     * analyzer : 分词器类型
     * ik_max_word : 最大粒度拆分(ik分词器的策略之一)
     */
    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String title;

    /**
     * 不建立索引 即不会被查询到
     * index : 是否索引(默认:true)
     */
    @Field(index = false,type = FieldType.Text)
    private String realName;

    /**
     * 价格
     */
    @Field(type = FieldType.Double)
    private Double price;

    /**
     *  Keyword : 短语,不进行分词
     */
    @Field(type = FieldType.Keyword)
    private String shortName;

    /**
     * 测试一下用Auto
     */
    @Field(index = true,type = FieldType.Auto)
    private String content;
}




mapper层

/**
 * @author qkj
 */
@Repository
public interface ProductMapper extends ElasticsearchRepository<Product, Long> {
//    ⽅法命名规则查询的基本语法findBy + 属性 + 关键词 + 连接符(属性 + 关键词 + 连接符)

    List<Product> findByTitleOrContent(String title, String content);

    List<Product> findByTitle(String title);

    List<Product> findByTitleOrContent(String title, String content, Pageable pageable);

    List<Product> findByTitleLike(String title);

    /**
     * 根据多个条件查询时 参数也必须是多个
     * @param shortName
     * @param title
     */
    List<Product> findByShortNameOrTitle(String shortName, String title);

    List<Product> findByRealNameOrTitle(String realName, String title);
}

用 ElasticsearchRestTemplate 代替 ElasticsearchTemplate
因为ElasticsearchTemplate基于 TransportClient 在es8中已移除
ElasticsearchRestTemplate基于RestHighLevelClient


import cn.hutool.core.util.IdUtil;
import com.qiuhuanhen.springroot.infrastructure.DO.elasticsearch.Product;
import com.qiuhuanhen.springroot.infrastructure.mapper.ProductMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * es的测试类
 * @author qkj
 */
@RestController
@RequestMapping("/testes")
public class ElasticsearchController {
    private Long id = 1L;
    @Lazy
    @Autowired
    private ElasticsearchRestTemplate elasticsearchRestTemplate;

    @Autowired
    private ProductMapper productMapper;

    /**
     * es是支持restful风格接口直接操作的
     * 如: 创建索引 使用put请求es地址+索引 直接发起即可
     */

    /**
     * 在实体类声明索引 默认会创建索引(当不存在时)
     * 如已创建会报错提示索引已存在  这里仅做测试 且方法请求方式保持restful风格统一
     * put请求:幂等
     * 创建索引(在实体)
     */
    @PutMapping("/{idx}")
    public String createIndex() {
        // 已过期
        // elasticsearchRestTemplate.createIndex(Product.class或idx);
        // 新版使用这个
        try {
            elasticsearchRestTemplate.indexOps(Product.class).create();
        } catch (Exception e) {
            e.printStackTrace();
            return "fail";
        }

        return "success";
    }


    @DeleteMapping("/{idx}")
    public String deleteIndex() {
        // 已过期
        // elasticsearchRestTemplate.deleteIndex(idx);
        try {
            elasticsearchRestTemplate.indexOps(Product.class).delete();
        } catch (Exception e) {
            return "fail";
        }
        return "success";
    }

    @PostMapping("/_doc")
    public String createContent() {

        Product product = new Product().setId(id++)
                .setPrice(100.00)
                .setRealName("真名")
                .setShortName("缩写")
                .setTitle("标题");

        productMapper.save(product);

        return "success";
    }


    @DeleteMapping("/_doc/{id}")
    public String deleteContent(@PathVariable String id) {

        productMapper.deleteById(Long.valueOf(id));
        return "success";
    }

    @PutMapping("/_doc/{id}")
    public Product updateContent(@PathVariable String id) {

        Product product = new Product()
                .setId(Long.valueOf(id))
                .setPrice(100.00)
                .setContent("真名")
                .setRealName("真名")
                .setShortName("缩写")
                .setTitle("标题"+IdUtil.simpleUUID());
        return productMapper.save(product);
    }

    /**
     * 根据id查询
     */
    @GetMapping("_doc/{id}")

    public Product getProductById(@PathVariable String id) {
        return productMapper.findById(Long.valueOf(id)).get();
    }

    /**
     * 根据内容查询
     */
    @GetMapping
    public List<List<Product>> getProduct(@RequestBody Product product) {

//        // 已过时写法
//        TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("title", "小米");
//        productMapper.search(termQueryBuilder);

        // 新写法 直接在mapper里面创建方法 但要注意命名规则
        return  List.of(productMapper.findByShortNameOrTitle("缩写","缩写"),productMapper.findByRealNameOrTitle("真名","真名")) ;

    }

}

es高阶

分片策略

分片和副本的设计为 ES 提供了支持分布式和故障转移的特性,但并不意味着分片和副本是可以无限分配的。而且索引的分片完成分配后由于索引的路由机制,我们是不能重新修改分片数的。

分片并不是越多越好,所以需要合理设置分片数。

一个分片的底层即为一个 Lucene 索引,会消耗一定文件句柄、内存、以及 CPU运转。

每一个搜索请求都需要命中索引中的每一个分片,如果每一个分片都处于不同的节点还好, 但如果多个分片都需要在同一个节点上竞争使用相同的资源就有些糟糕了。

用于计算相关度的词项统计信息是基于分片的。如果有许多分片,每一个都只有很少的数据会导致很低的相关度。

一个业务索引具体需要分配多少分片可能需要架构师和技术人员对业务的增长有个预先的判断,横向扩展应当分阶段进行。为下一阶段准备好足够的资源。 只有当你进入到下一个阶段,你才有时间思考需要作出哪些改变来达到这个阶段。一般来说,我们遵循一些原则:

1.控制每个分片占用的硬盘容量不超过 ES 的最大 JVM 的堆空间设置
(一般设置不超过 32G,参考下文的 JVM 设置原则),
因此,如果索引的总容量在 500G 左右,那分片大小在 16 个左右即可;
当然,最好同时考虑原则 2。

2.考虑一下 node 数量,一般一个节点有时候就是一台物理机,如果分片数过多,
大大超过了节点数,很可能会导致一个节点上存在多个分片,
一旦该节点故障,即使保持了 1 个以上的副本,
同样有可能会导致数据丢失,集群无法恢复。所以, 一般都设置分片数不超过节点数的 3 倍。

3.主分片,副本和节点最大数之间数量,我们分配的时候可以参考以下关系:
节点数<=主分片数 *(副本数+1)

推迟分片分配

对于节点瞬时中断的问题,默认情况,集群会等待一分钟来查看节点是否会重新加入,如果这个节点在此期间重新加入,重新加入的节点会保持其现有的分片数据,不会触发新的分片分配。这样就可以减少 ES 在自动再平衡可用分片时所带来的极大开销。

通过修改参数 delayed_timeout ,可以延长再均衡的时间,可以全局设置也可以在索引级别进行修改:

#PUT /_all/_settings
{
	"settings": {
		"index.unassigned.node_left.delayed_timeout": "5m"
	}
}

路由选择

当我们查询文档的时候, Elasticsearch 如何知道一个文档应该存放到哪个分片中呢?它其实是通过下面这个公式来计算出来:

shard = hash(routing) % number_of_primary_shards

routing 默认值是文档的 id,也可以采用自定义值,比如用户 id。

不带routing查询:

在查询的时候因为不知道要查询的数据具体在哪个分片上,所以整个过程分为2个步骤

分发:请求到达协调节点后,协调节点将查询请求分发到每个分片上。
聚合:协调节点搜集到每个分片上查询结果,在将查询的结果进行排序,之后给用户返回结果。

带routing查询:

查询的时候,可以直接根据routing 信息定位到某个分配查询,不需要查询所有的分配,经过协调节点排序。向上面自定义的用户查询,如果routing 设置为userid 的话,就可以直接查询出数据来,效率提升很多。

写入速度优化

ES 的默认配置,是综合了数据可靠性、写入速度、搜索实时性等因素。实际使用时,我们需要根据公司要求,进行偏向性的优化。

针对于搜索性能要求不高,但是对写入要求较高的场景,我们需要尽可能的选择恰当写优化策略。综合来说,可以考虑以下几个方面来提升写索引的性能:

加大Translog Flush,目的是降低Iops、Writeblock。
增加Index Refesh间隔,目的是减少Segment Merge的次数。
调整Bulk 线程池和队列。
优化节点间的任务分布。
优化Lucene层的索引建立,目的是降低CPU及IO。

优化存储设备

ES 是一种密集使用磁盘的应用,在段合并的时候会频繁操作磁盘,所以对磁盘要求较高,当磁盘速度提升之后,集群的整体性能会大幅度提高。

合理使用合并

Lucene 以段的形式存储数据。当有新的数据写入索引时, Lucene 就会自动创建一个新的段。

随着数据量的变化,段的数量会越来越多,消耗的多文件句柄数及 CPU 就越多,查询效率就会下降。

由于 Lucene 段合并的计算量庞大,会消耗大量的 I/O,所以 ES 默认采用较保守的策略,让后台定期进行段合并。

减少 Refresh 的次数

Lucene 在新增数据时,采用了延迟写入的策略,默认情况下索引的refresh_interval 为1 秒。

Lucene 将待写入的数据先写到内存中,超过 1 秒(默认)时就会触发一次 Refresh,然后 Refresh 会把内存中的的数据刷新到操作系统的文件缓存系统中。

如果我们对搜索的实效性要求不高,可以将 Refresh 周期延长,例如 30 秒。

这样还可以有效地减少段刷新次数,但这同时意味着需要消耗更多的 Heap 内存。

加大 Flush 设置

Flush 的主要目的是把文件缓存系统中的段持久化到硬盘,当 Translog 的数据量达到 512MB 或者 30 分钟时,会触发一次 Flush。

index.translog.flush_threshold_size 参数的默认值是 512MB,我们进行修改。

增加参数值意味着文件缓存系统中可能需要存储更多的数据,所以我们需要为操作系统的文件缓存系统留下足够的空间。

减少副本的数量

ES 为了保证集群的可用性,提供了 Replicas(副本)支持,然而每个副本也会执行分析、索引及可能的合并过程,所以 Replicas 的数量会严重影响写索引的效率。

当写索引时,需要把写入的数据都同步到副本节点,副本节点越多,写索引的效率就越慢。

如果我们需要大批量进行写入操作,可以先禁止Replica复制,设置
index.number_of_replicas: 0 关闭副本。在写入完成后, Replica 修改回正常的状态。

内存设置

ES 默认安装后设置的内存是 1GB,对于任何一个现实业务来说,这个设置都太小了。如果是通过解压安装的 ES,则在 ES 安装文件中包含一个 jvm.option 文件,添加如下命令来设置 ES 的堆大小, Xms 表示堆的初始大小, Xmx 表示可分配的最大内存,都是 1GB。

确保 Xmx 和 Xms 的大小是相同的,其目的是为了能够在 Java 垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小而浪费资源,可以减轻伸缩堆大小带来的压力。

假设你有一个 64G 内存的机器,按照正常思维思考,你可能会认为把 64G 内存都给ES 比较好,但现实是这样吗, 越大越好?虽然内存对 ES 来说是非常重要的,但是答案是否定的!

因为 ES 堆内存的分配需要满足以下两个原则:

不要超过物理内存的 50%: Lucene 的设计目的是把底层 OS 里的数据缓存到内存中。
Lucene 的段是分别存储到单个文件中的,这些文件都是不会变化的,
所以很利于缓存,同时操作系统也会把这些段文件缓存起来,以便更快的访问。
如果我们设置的堆内存过大, Lucene 可用的内存将会减少,
就会严重影响降低 Lucene 的全文本查询性能。


堆内存的大小最好不要超过 32GB:在 Java 中,所有对象都分配在堆上,
然后有一个 Klass Pointer 指针指向它的类元数据。
这个指针在 64 位的操作系统上为 64 位, 64 位的操作系统可以使用更多的内存(2^64)。在 32 位
的系统上为 32 位, 32 位的操作系统的最大寻址空间为 4GB(2^32)。
但是 64 位的指针意味着更大的浪费,因为你的指针本身大了。
浪费内存不算,更糟糕的是,
更大的指针在主内存和缓存器(例如 LLC, L1 等)之间移动数据的时候,会占用更多的带宽。

最终我们都会采用 31 G 设置

-Xms 31g
-Xmx 31g

假设你有个机器有 128 GB 的内存,你可以创建两个节点,每个节点内存分配不超过 32 GB。也就是说不超过 64 GB 内存给 ES 的堆内存,剩下的超过 64 GB 的内存给 Lucene。

重要配置

【elasticsearch】elasticsearch入门教程 es整合spring教程_第5张图片

Elasticsearch面试题

为什么要使用 Elasticsearch?

系统中的数据, 随着业务的发展,时间的推移, 将会非常多, 而业务中往往采用模糊查询进行数据的搜索, 而模糊查询会导致查询引擎放弃索引,导致系统查询数据时都是全表扫描,在百万级别的数据库中,查询效率是非常低下的,而我们使用 ES 做一个全文索引,将经常查询的系统功能的某些字段,比如说电商系统的商品表中商品名,描述、价格还有 id 这些字段我们放入 ES 索引库里,可以提高查询速度。

Elasticsearch 的 master 选举流程?

Elasticsearch的选主是ZenDiscovery模块负责的,主要包含Ping(节点之间通过这个RPC来发现彼此)
和Unicast(单播模块包含-一个主机列表以控制哪些节点需要ping通)这两部分。
对所有可以成为master的节点(node master: true)根据nodeId字典排序,每次选举每个节点都把自
己所知道节点排一次序,然后选出第一个(第0位)节点,暂且认为它是master节点。
如果对某个节点的投票数达到一定的值(可以成为master节点数n/2+1)并且该节点自己也选举自己,
那这个节点就是master。否则重新选举一直到满足上述条件。
master节点的职责主要包括集群、节点和索引的管理,不负责文档级别的管理;data节点可以关闭http
功能。

Elasticsearch 集群脑裂问题?

“脑裂”问题可能的成因:

网络问题:集群间的网络延迟导致一些节点访问不到master, 
认为master 挂掉了从而选举出新的master,并对master上的分片和副本标红,分配新的主分片。

节点负载:主节点的角色既为master又为data,
访问量较大时可能会导致ES停止响应造成大面积延迟,
此时其他节点得不到主节点的响应认为主节点挂掉了,会重新选取主节点。

内存回收:data 节点上的ES进程占用的内存较大,
引发JVM的大规模内存回收,造成ES进程失去响应。

脑裂问题解决方案:

减少误判:discovery.zen ping_ timeout 节点状态的响应时间,默认为3s,
可以适当调大,如果master在该响应时间的范围内没有做出响应应答,
判断该节点已经挂掉了。调大参数(如6s,discovery.zen.ping_timeout:6),可适当减少误判。

选举触发:discovery.zen.minimum. _master_ nodes:1,
该参數是用于控制选举行为发生的最小集群主节点数量。
当备选主节点的个數大于等于该参数的值,且备选主节点中有该参数个节点认为主节点挂了,
进行选举。官方建议为(n / 2) +1, n为主节点个数(即有资格成为主节点的节点个数)。

角色分离:即master节点与data节点分离,限制角色
	主节点配置为:node master: true,node data: false
	从节点配置为:node master: false,node data: true

Elasticsearch 搜索的流程?

搜索被执行成一个两阶段过程,我们称之为 Query Then Fetch;

在初始查询阶段时,查询会广播到索引中每一个分片拷贝(主分片或者副本分片)。 每个分片在本地执行搜索并构建一个匹配文档的大小为 from + size 的优先队列。 PS:在搜索的时候是会查询Filesystem Cache 的,但是有部分数据还在 Memory Buffer,所以搜索是近实时的。

每个分片返回各自优先队列中 所有文档的 ID 和排序值 给协调节点,它合并这些值到自己的优先队列中来产生一个全局排序后的结果列表。

接下来就是取回阶段, 协调节点辨别出哪些文档需要被取回并向相关的分片提交多个 GET 请求。每个分片加载并丰富文档,如果有需要的话,接着返回文档给协调节点。一旦所有的文档都被取回了,协调节点返回结果给客户端。

Query Then Fetch 的搜索类型在文档相关性打分的时候参考的是本分片的数据,这样在文档数量较少的时候可能不够准确, DFS Query Then Fetch 增加了一个预查询的处理,询问 Term 和 Document frequency,这个评分更准确,但是性能会变差。

es更新和删除文档的流程?

删除和更新也都是写操作,但是 Elasticsearch 中的文档是不可变的,因此不能被删除或者改动以展示其变更;

磁盘上的每个段都有一个相应的.del 文件。当删除请求发送后,文档并没有真的被删除,而是在.del文件中被标记为删除。该文档依然能匹配查询,但是会在结果中被过滤掉。当段合并时,在.del 文件中被标记为删除的文档将不会被写入新段。

在新的文档被创建时, Elasticsearch 会为该文档指定一个版本号,当执行更新时,旧版本的文档在.del文件中被标记为删除,新版本的文档被索引到一个新段。旧版本的文档依然能匹配查询,但是会在结果中被过滤掉。

并发下如何保证读写一致

可以通过版本号使用乐观并发控制,以确保新版本不会被旧版本覆盖,由应用层来处理具体的冲突;

另外对于写操作,一致性级别支持 quorum/one/all,默认为 quorum,即只有当大多数分片可用时才允许写操作。但即使大多数可用,也可能存在因为网络等原因导致写入副本失败,这样该副本被认为故障,分片将会在一个不同的节点上重建。

对于读操作,可以设置 replication 为 sync(默认),这使得操作在主分片和副本分片都完成后才会返回;如果设置 replication 为 async 时,也可以通过设置搜索请求参数_preference 为 primary 来查询主分片,确保文档是最新版本。

ItVuer - 免责声明 - 关于我们 - 联系我们

本网站信息来源于互联网,如有侵权请联系:561261067@qq.com

桂ICP备16001015号