Elastic Search面试题

为什么使用Elastic Search?和传统关系型数据库有什么不同? #

数据类型 #

ElasticSearch是基于文档的搜索引擎,它使用json文档来存储数据。在Elastic search中,相关数据通常存储在一个文档中,而不是分数在多个表中。

mysql是一个关系型数据库管理系统,它使用表、行和列的结构来组织数据。数据通过外键关系分散在多个表中。

查询语言 #

ElasticSearch使用Query DSL,是一种非常灵活的查询语言,基于json,支持全文搜索、复合查询、过滤以及聚合等。

Mysql使用SQL,是一种强类型和非常成熟的语言,专门用于查询和管理关系型数据库。

全文搜索 #

ElasticSerach的**核心功能是全文搜索。**它对数据进行索引时会自动建立全文搜索引擎,使其在搜索大量文本数据时表现优异。

Mysql虽然也提供了全文搜索功能,但其主要设计目标是处理结构化数据的存储和查询,对全文搜索的支持不如Elastic Search.

事务支持 #

Elasticsearch不支持传统事务。虽然它确保了单个文档操作的原子性,但不适用于跨多个文档的复杂事务。

主要场景和优势 #

Elasticsearch是一个开源的分布式搜索和分析引擎,主要适用于以下场景:

  • 搜索引擎:用于快速检索文档、商品、新闻等
  • 日志分析:通过分析日志数据,帮助企业了解其业务性能情况。
  • 数据分析:帮助数据科学家和数据分析师进行数据分析,以获取有价值信息。
  • 商业智能:帮助企业制定数据驱动决策,已实现商业上的成功
  • 实时监控:帮助企业实时检测系统性能、监控数据变化,以保证系统正常运行。
  • 安全性:帮助企业保证数据安全性,保证数据不被非法窃取
  • 应用程序开发:帮助开发人员开发基于搜索的应用程序,以增加用户体验。

优势:

  • 高性能:具有高性能搜索和分析能力,其中涵盖了多种查询语言和数据结构
  • 可扩展性:是分布式的,可以通过增加节点数量扩展搜索和分析能力
  • 灵活性:支持多种数据类型,支持多种语言,支持动态映射,允许快速地调整模型以适应不同需求
  • 实时分析:支持实时分析,可以对数据进行实时查询,这对快速检索数据非常有用
  • 可靠性:具有可靠性和高可用性,支持数据备份和恢复。

Elasticsearch为什么这么快? #

Elasticsearch是一个高性能、分布式搜索引擎,它之所以快,主要有以下几个原因:

  • 分布式存储:Elasticsearch使用分布式存储技术,将数据存储在多个节点上,从而减少单个节点的压力,提高整体性能。
  • 索引分片:Elasticsearch把每个索引划分成多个分片,这样可以让查询操作并行化,从而提高查询速度。
  • 全文索引:Elasticsearch使用了高效的全文索引技术,把文档转化成可搜索的结构化数据,使得搜索操作快速高效。
  • 倒排索引:Elasticsearch支持倒排索引这种数据结构,**倒排索引将文档中的每个词与该词出现在哪些文档中进行映射,并存储这些信息。**当搜索请求发生时,ES可以快速查找包含所有搜索词的文档,从而返回结果。
  • 索引优化:Elasticsearch通过索引优化技术,可以使查询速度更快。例如,它支持索引覆盖、索引下推等优化技术,使得查询速度更快。
  • 预存储结果:Elasticsearch在插入数据时,对数据进行预处理,把结果预存储到索引中,从而在查询时不需要再重新计算,提高查询速度。
  • 高效的查询引擎:Elasticsearch使用了高效的查询引擎,支持各种类型的查询,并对复杂查询提供了优化策略,从而提高查询速度。
  • 异步请求处理:ES使用了异步请求处理机制,能够在请求到达时立即返回,避免长时间的等待,提高用户体验。
  • 内存存储:ES使用了内存存储技术,能够在读写数据时大大减少磁盘访问次数,提高数据存储和查询效率。

倒排索引是什么? #

在 ElasticSearch 中,倒排索引是一种常用的索引结构,用于快速搜索文档中的某个词汇。

倒排索引的结构与传统的索引结构相反,传统的索引结构是由文档构成的,每个文档包含了若干个词汇,然后根据这些词汇建立索引。而倒排索引是由词汇构成的,每个词汇对应了若干个文档,然后根据这些文档建立索引。

对于一个包含多个词汇的文档,倒排索引会将每个词汇作为一个关键字(Term),然后记录下该词汇所在的文档编号(Document ID)及该词汇在文档中的位置(Term Position)。这样,当用户输入一个关键字时,就可以快速地查找到包含该关键字的文档编号,然后通过文档编号再查找到对应的文档内容。

**倒排索引的优点在于它可以快速定位包含关键字的文档,而且可以支持复杂的搜索操作,如词组搜索、通配符搜索等。**同时,由于倒排索引是由词汇构成的,因此在进行数据分析和统计时也非常有用。在 ElasticSearch 中,倒排索引是一种非常重要的索引结构,它被广泛应用于搜索引擎、日志分析、推荐系统等领域。

倒排索引建立过程 #

ES中的倒排索引建立过程主要有2个步骤,分别是分词、建立倒排索引

比如我们现在有三份文档内容,分别是

id content
1 深入理解Java核心技术—Hollis
2 深入理解Java虚拟机—周志明
3 Java编程思想—布鲁斯·埃克尔
  • 分词

    在倒排索引建立过程中,首先需要将文档中的原始文本分解成一个个词项(Term)。Elasticsearch 中默认使用标准分词器(Standard Analyzer)进行分词。

    以上三个文本内容,我们经过分词之后,就会包含了"深入"、“理解”、“Java”、“核心”、“技术”、“编程”、“思想”、“Hollis”、“周志明”、“布鲁斯·埃克尔"等词

  • 生成倒排索引

    将分开的词,当做索引,与对应的文档ID进行关联,形成倒排表。

    词条 文档ID
    深入 1,2
    理解 1,2
    Java 1,2,3
    虚拟机 2
    核心 1
    技术 1
    编程 3
    思想 3

    在生成了倒排表后,还会对倒排表进行压缩,减少空间占用。常用的压缩算法包括Variable Byte Encoding和Simple9等。最后再将压缩后的倒排表存储在磁盘中,以便后续的搜索操作能够快速地访问倒排表。

如何保证ES和数据的数据一致性? #

  • 双写

    在代码中,对数据库和ES进行双写,并且先操作本地数据库,后操作ES,而且还需要把两个操作放到同一个事务中:

    如果写数据库成功,写ES失败,那么事务会回滚。

    如果写数据库成功,写ES超时,实际上ES操作成功,这时候数据库会回滚,导致数据不一致。这时候需要重试来保证最终一致性。

    这个方案的好处就是简单,容易实现。并且实时性比较高。

    缺点首先是需要改代码,有侵入性,还有就是存在不一致的情况。并且在本地事务中发生了外调(外部调用,调ES),大大拖长了事务,白白占用数据库链接,影响整体的吞吐量。

  • MQ异步消费

    在应用中,如果我要更新数据库了,那么就抛一个消息出去,然后数据库和ES各自有一个监听者,监听消息之后各自去做数据变更,如果失败了就基于消息的重试在重新执行。

    或者像之前那个方案一样,先操作数据库,然后异步通知ES去更新,这时候就可以借助本地消息表的方式来保证最终一致性了。

    这个方案的好处是用了MQ,起到了解耦的作用,而且还做到了异步,提升了整体性能。

    缺点就是MQ可能存在延迟,并且需要引入新的中间件,复杂度有所提升。

  • 扫表定时同步

    如果是ES中的数据变更的实时性要求不高,可以考虑定时任务扫表, 然后批量更新ES。

    这个方案优点是没有侵入性,数据库的写操作处不需要改代码。

    缺点是实时性很差,并且轮询可能存在性能问题、效率问题以及给数据库带来压力。

  • 监听binlog同步

    利用数据库变更时产生的binlog来更新ES。通过监听binlog来更新ES中的数据,也有成熟的框架可以做这样的事情

    好处就是对业务代码完全没有侵入性,业务也非常解耦,不需要关心这个ES的更新操作。

    缺点就是需要基于binlog监听,需要引入第三方框架。存在一定的延迟。

总结一下,目前业内比较流行的方案是基于binlog监听的这种,首先一般业务量小的业务也不太需要用ES,所以用了ES的团队,一般并不太会关心引入新框架的复杂度问题,而且ES这种搜索,一般来说,毫秒级的延迟都是可以接受的,所以,综合来讲,基于canal做数据同步的方案,是比较合适的。

什么是Elastic search的深度分页问题?如何解决? #

在Elastic Search中进行分页查询通常是哟form和size参数。当我们对Elastic Search发起一个带有分页参数的查询时,ES需要遍历所有匹配的文档直到达到指定起始点(from),然后返回从这一点开始的size个文档。

GET /your_index/_search
{
  "from": 20,
  "size": 10,
  "query": {
    "match_all": {}
  }
}

在这个例子中:

  • from 参数定义了要跳过的记录数。在这里,它跳过了前20条记录。
  • size 参数定义了返回的记录数量。在这里,它返回了10条记录。

from + size 的总数不能超过Elasticsearch索引的index.max_result_window设置,默认为10000。这意味着如果你设置from为9900,size为100,查询将会成功。但如果from为9900,size为101,则会失败。

ES的检索机制决定了,当进行分页查询时,Elasticsearch需要先找到并处理所有位于当前页之前的记录。例如,如果你请求第1000页的数据,并且每页显示10条记录,系统需要先处理前9990条记录,然后才能获取到你请求的那10条记录。这意味着,随着页码的增加,数据库需要处理的数据量急剧增加,导致查询效率降低。

这就是ES的深度分页的问题,深度分页需要数据库在内存中维护大量的数据,并对这些数据进行排序和处理,这会消耗大量的CPU和内存资源。随着分页深度的增加,查询响应时间会显著增加。在某些情况下,这可能导致查询超时或者系统负载过重。

scroll #

Scroll API在Elasticsearch中的主要目的是为了能够遍历大量的数据,它通常用于数据导出或者进行大规模的数据分析。可以用于处理大量数据的深度分页问题。

GET /your_index/_search?scroll=1m
{
  "size": 10,  // 每页10条记录
  "query": {
    "match_all": {}
  }
}

如上方式初始化一个带有scroll参数的搜索请求。这个请求返回一个scroll ID,用于后续的滚动。Scroll参数指定了scroll的有效期,例如1m表示一分钟。

接下来就可以使用返回的scroll ID来获取下一批数据。每次请求也会更新scroll ID的有效期。

GET /_search/scroll
{
  "scroll": "1m",
  "scroll_id": "your_scroll_id"
}

我们需要重复以上操作直到到达想要的页数。比如第10页,则需要执行9次滚动操作,然后第10次请求将返回第10页的数据。

Scroll API可以解决深度分页问题,主要是因为他有以下几个特点:

  • 避免重复排序:

    在传统的分页方式中,每次分页请求都需要对所有匹配的数据进行排序,以确定分页的起点。Scroll避免了这种重复排序,因为它保持了一个游标。

  • 稳定视图:

    Scroll提供了对数据的“稳定视图”。当你开始一个scroll时,Elasticsearch会保持搜索时刻的数据快照,这意味着即使数据随后被修改,返回的结果仍然是一致的。

  • 减少资源消耗:

    由于不需要重复排序,Scroll减少了对CPU和内存的消耗,特别是对于大数据集。

Scroll非常适合于处理需要访问大量数据但不需要快速响应的场景,如数据导出、备份或大规模数据分析。

但是,需要知道,使用Scroll API进行分页并不高效因为你需要先获取所有前面页的数据。Scroll API主要用于遍历整个索引或大量数据,而不是用于快速访问特定页数的数据。

search_after #

search_after 是 Elasticsearch 中用于实现深度分页的一种机制。与传统的分页方法(使用 from 和 size 参数)不同,search_after 允许你基于上一次查询的结果来获取下一批数据,这在处理大量数据时特别有效。

在第一次查询时,你需要定义一个排序规则。不需要指定 search_after 参数:

GET /your_index/_search
{
  "size": 10,
  "query": {
    "match_all": {}
  },
  "sort": [
    {"timestamp": "asc"},
    {"id": "asc"}
  ]
}

这个查询按 timestamp 字段排序,并在相同 timestamp 的情况下按 _id 排序。

在后续的查询中,使用上一次查询结果中最后一条记录的排序值。

GET /your_index/_search
{
  "size": 10,
  "query": {
    "match_all": {}
  },
  "sort": [
    {"timestamp": "asc"},
    {"id": "asc"}
  ],
  "search_after": [1609459200000, 10000]
}

在这个例子中,search_after 数组包含了 timestamp 和 _id 的值,对应于上一次查询结果的最后一条记录。

search_after 可以有效解决深度分页问题,原因如下:

  • **避免重复处理数据:**与传统的分页方式不同,search_after 不需要处理每个分页请求中所有先前页面上的数据。这大大减少了处理数据的工作量。
  • **提高查询效率:**由于不需要重复计算和跳过大量先前页面上的数据,search_after 方法能显著提高查询效率,尤其是在访问数据集靠后部分的数据时。

但是这个方案有一些局限,一方面需要有一个全局唯一的字段用来排序,另外虽然一次分页查询时不需要处理先前页面中的数据,但实际需要依赖上一个页面中的查询结果。

对比 #

使用场景 实现方式 优点 缺点
传统分页 适用于小数据集和用户界面中的标准分页,如网站上的列表分页。 通过指定from(起始位置)和size(页面大小)来实现分页。 实现简单,适用于小数据集,易于理解和使用。 不适用于深度分页。当from值很大时,性能急剧下降。Elasticsearch默认限制from + size不超过10000。
scroll 适用于大规模数据的导出、备份或处理,而不是实时用户请求。 初始化一个scroll请求,然后使用返回的scroll id来连续地获取后续数据。 可以有效处理大量数据。提供了数据快照,保证了查询过程中数据的一致性。 不适合实时请求。初始化scroll会占用更多资源,因为它在后端维护了数据的状态。
search_after 适用于深度分页和大数据集的遍历。 基于上一次查询结果的排序值来获取下一批数据。 解决了深度分页的性能问题。更适合于处理大数据量,尤其是当需要顺序遍历整个数据集时。 不适用于随机页访问。需要精确的排序机制,并在每次请求中维护状态。
  • 对于小型数据集和需要随机页面访问的标准分页场景,传统的分页是最简单和最直接的选择。
  • 对于需要处理大量数据但不需要随机页面访问的场景,尤其是深度分页,search_after提供了更好的性能和更高的效率。
  • 当需要处理非常大的数据集并且对数据一致性有要求时(如数据导出或备份),Scroll API是一个更好的选择。

如何优化ElasticSearch搜索性能? #

集群和硬件优化 #

  • 负载均衡: 确保查询负载在集群中均衡分配。
  • 硬件资源: 根据需要增加 CPU、内存或改善 I/O 性能(例如使用 SSD)。
  • 配置 JVM: 优化 JVM 设置,如堆大小,以提高性能。

合理分片和副本 #

虽然更多的分片可以提高写入吞吐量,因为可以并行写入多个分片。但是,查询大量分片可能会降低查询性能,因为每个分片都需要单独处理查询。而且分片数量过多可能会增加集群的管理开销和降低查询效率,尤其是在内存和文件句柄方面。所以,需要考虑数据量和硬件资源,合理设置分片数量。

但是这个说起来比较玄学,毕竟没有一种“一刀切”的方法来确定最优的分片和副本数量,因为这取决于多种因素,包括数据的大小、查询的复杂性、硬件资源和预期的负载等。

在ES每个节点上可以存储的分片数量与可用的堆内存大小成正比关系,但是 Elasticsearch 并未强制规定固定限值。这里有一个很好的经验法则:确保对于节点上已配置的每个 GB,将分片数量保持在 20 以下。如果某个节点拥有 30GB 的堆内存,那其最多可有 600 个分片,但是在此限值范围内,您设置的分片数量越少,效果就越好。一般而言,这可以帮助集群保持良好的运行状态。(来源参考:https://www.elastic.co/cn/blog/how-many-shards-should-i-have-in-my-elasticsearch-cluster )

精确的映射和索引设置 #

映射(Mapping)是定义如何存储和索引文档中字段的规则。我们可以在以下几个方面做一些优化:

确切定义字段类型:为每个字段指定正确的数据类型(如 text, keyword, date, integer 等),这是因为不同的数据类型有不同的存储和索引方式。需要注意的是:text 类型用于全文搜索,它会被分析(analyzed),即分解为单个词项。keyword 类型用于精确值匹配,过滤,排序和聚合。它不会被分析。

根据需要选择合适的分析器(Analyzer),对于 text 类型的字段,可以指定分析器来定义文本如何被分割和索引。对于不需要全文搜索的字段,使用 keyword 类型以避免分析开销。

查询优化 #

很多人用ES很慢,是因为自己的查询本身就用的不对,我们可以尝试着优化一下你的查询。如:

  • 避免高开销查询: 如 wildcard、regexp 等类型的查询往往开销较大,尽量避免使用或优化其使用方式。
  • 使用过滤器: 对于不需要评分的查询条件,使用 filter 而不是 query,因为 filter 可以被缓存以加快后续相同查询的速度。
  • 查询尽可能少的字段: 只返回查询中需要的字段,减少数据传输和处理时间。
  • 避免深度分页: 避免深度分页,对于需要处理大量数据的情况,考虑使用 search_after。
  • 避免使用脚本:尽量避免使用脚本(Script)查询,因为它们通常比简单查询要慢。(脚本执行通常比静态查询更消耗资源。每次执行脚本时,都需要进行编译(除非缓存)和运行,这会增加CPU和内存的使用。脚本执行不能利用索引,因此可能需要全面扫描文档。)
  • 使用 term 而非 match 查询文本字段: 一般来说,term 查询比 match 查询快,因为 term 查询不需要对查询内容进行分词。term 查询直接查找精确的值,索引定位效率高。
  • 避免使用通配符、正则表达式:这类查询往往非常消耗资源,特别是以通配符开头的(如 *text)。
  • 合理使用聚合:聚合可以用于高效地进行数据分析,但复杂的聚合也可能非常消耗资源。优化聚合查询,如通过限制桶的数量,避免过度复杂的嵌套聚合。

使用缓存 #

  • 请求缓存: 对于不经常变化的数据,利用 ES 的请求缓存机制。
  • 清理缓存: 定期清理不再需要的缓存,释放资源。

监控和分析 #

  • 监控: 使用 Kibana、Elasticsearch-head、Elastic HQ 等工具监控集群状态和性能。

  • 慢查询日志: 启用慢查询日志来识别和优化慢查询。

ES支持哪些数据类型,和MySQL之间的映射关系是怎么样的? #

  • Text: 用于存储全文文本数据,如文章或书籍内容。支持全文搜索和分析。
  • Keyword: 用于存储文本值,通常用于索引结构化内容,如邮件地址、标签或任何需要精确匹配的内容。
  • Date: 存储日期或日期和时间。
  • Long, Integer, Short, Byte, Double, Float: 这些是数值类型,用于存储各种形式的数字。
  • Boolean: 存储 true 或 false 值。
  • Binary: 用于存储二进制数据。
  • Object: 用于嵌套文档,即文档内部可以包含文档。
  • Nested: 类似于 Object 类型,但用于存储数组列表,其中列表中的每个元素都是完全独立且可搜索的。

我们经常会把MySQL 中的数据同步到 ES 中,他们之间的类型的映射关系如下:

MySQL 类型 Elasticsearch 类型 说明
VARCHAR text, keyword 根据是否需要全文搜索或精确搜索选择使用 text 或 keyword。
CHAR keyword 通常映射为 keyword,因为它们用于存储较短的、不经常变化的字符序列。
BLOB/TEXT text 大文本块使用 text 类型,支持全文检索。
INT, BIGINT long 大多数整数类型映射为 long,以支持更大的数值。
TINYINT byte 较小的整数可以映射为 byte 类型。
DECIMAL, FLOAT, DOUBLE double, float 根据精确度需求选择 double 或 float。
DATE, DATETIME, TIMESTAMP date 所有的日期时间类型均可映射为 date。
TINYINT(1) boolean

text和keyword有啥区别? #

text 类型被设计用于全文搜索。这意味着当文本被存储为 text 类型时,Elasticsearch 会对其进行分词,把文本分解成单独的词或短语,便于搜索引擎进行全文搜索。因为 text 字段经过分词,它不适合用于排序或聚合查询。

text适用于存储需要进行全文搜索的内容,比如新闻文章、产品描述等。

**keyword 类型用于精确值匹配,不进行分词处理。**这意味着存储在 keyword 字段的文本会被当作一个完整不可分割的单元进行处理。**因为 keyword 类型字段是作为整体存储,它们非常适合用于聚合(如计数、求和、过滤唯一值等)和排序操作。**由于不进行分词,keyword 类型字段不支持全文搜索,但可以进行精确匹配查询。

keyword适用于需要进行精确搜索的场景,比如标签、ID 编号、邮箱地址等。

ES不支持decimal,如何避免丢失精度? #

DECIMAL 是 MySQL 中用于存储精确小数的数据类型,特别适合需要高精度计算的场景(如财务数据)。

语法格式

DECIMAL(M, D)
  • M (Precision):总位数,范围 1-65(默认 10)
  • D (Scale):小数位数,范围 0-30(默认 0)

特点

  1. 精确存储:不像 FLOAT/DOUBLE 有精度损失
  2. 固定点数:存储格式为字符串,按指定精度存储
  3. 存储效率:每 4 字节存储 9 位数字
  4. 计算准确:所有运算都保持精确

ES 不支持 decimal 类型的,只有 double、float 等类型,那么,MySQL 中的 decimal 类型,同步到 ES 之后,如何避免丢失精度呢?

price DECIMAL(10, 2)

如以上 price 字段,在 es 中如何表示呢?有以下几种方式:

使用字符串类型(推荐) #

将 decimal 数据作为字符串类型存储在 Elasticsearch 中。这种方式可以保证数字的精度不会丢失,因为字符串会保留数字的原始表示形式。

  • 优点:完全保留数字的精度。简单易于实现,数据迁移时不需特别处理。
  • 缺点:作为字符串存储的数字不能直接用于数值比较或数学运算,需要在应用层处理转换。
{
  "properties": {
    "price": {
      "type": "keyword"
    }
  }
}

扩大浮点类型的精度(推荐) #

虽然 double 类型在理论上可能会有精度损失,但实际上 double 类型提供的精度对于许多业务需求已经足够使用。如果决定使用这种方法,可以在数据迁移或同步时适当扩大数值范围以尽量减小精度损失。

优点:可以直接进行数值比较和数学运算。

缺点:在非常高精度的需求下可能存在精度损失。

{
  "properties": {
    "amount": {
      "type": "double"
    }
  }
}

使用scaled_float(推荐) #

Elasticsearch 的 scaled_float 类型是一种数值数据类型,专门用于存储浮点数。其**特点是通过一个缩放因子(scaling factor)将浮点数转换为整数来存储,**从而在一定范围内提高存储和计算的效率。

他使用一个缩放因子将浮点数转换为整数存储。例如,如果缩放因子是 100,那么值 123.45 会存储为 12345。这样可以避免浮点数存储和计算中的精度问题。

{
  "mappings": {
    "properties": {
      "price": {
        "type": "scaled_float",
        "scaling_factor": 100
      }
    }
  }
}

使用多个字段 #

在某些情况下,可以将 decimal 数值拆分为两个字段存储:一个为整数部分,另一个为小数部分。这样做可以在不丢失精度的情况下,将数值分开处理。

优点:保持数值精确,同时可进行部分数学运算。

缺点:增加了数据处理的复杂性,需要在应用层重建数值。

{
  "properties": {
    "total_price_yuan": {
      "type": "integer"
    },
    "total_price_cents": {
      "type": "integer"
    }
  }
}

使用自定义脚本 #

在查询时,可以使用 Elasticsearch 的脚本功能(如 Painless 脚本)来处理数值计算,确保在处理过程中控制精度。

优点:灵活控制数据处理逻辑。

缺点:可能影响查询性能,增加系统复杂性。

ES支持事务吗,为什么? #

**ES 虽然也可以认为是一个数据库,但是他并不支持传统意义上的 ACID 事务,因为 ES它被设计出来是主要用作搜索引擎的,主要是提升查询效率的。**如果支持复杂的事务操作意味着就要牺牲性能优势。

虽然 Elasticsearch 不支持传统的事务,但是**他是可以确保单个文档的更改(如创建、更新、删除)是原子性的。**这意味着任何文档级的操作都完整地成功或完整地失败,但不保证跨多个文档或操作的一致性。

ES支持乐观锁吗?如何实现的? #

支持,Elasticsearch 支持通过使用文档版本控制来实现乐观锁。

在 ES 中,每个文档存储时都有一个 _version 字段,这个版本号在每次文档更新时自动增加。当我们执行更新、删除或者使用脚本处理文档时,可以指定这个版本号来确保正在操作的文档是预期中的版本。如果操作中的版本号与存储在索引中的文档版本号不一致,说明文档已被其他操作更改,当前操作将会失败。(CAS)

但是,从Elasticsearch 6.7 版本开始,使用 _version 关键字进行乐观锁已经被废弃了,替代方法是使用 if_seq_no 和 if_primary_term 来指定版本。

假设有一个文档:

{
  "_index": "products",
  "_type": "_doc",
  "_id": "1",
  "_version": 10,
  "_source": {
    "name": "Coffee",
    "price": 20
  }
}

基于他,我们可以在更新时进行乐观锁控制,避免发生并发修改:

POST /products/_doc/1?if_seq_no=312&if_primary_term=2
{
  "name": "Coffee",
  "price": 22
}

这里的 if_seq_no 和 if_primary_term 是 Elasticsearch 中的字段,用于管理乐观锁。如果文档自上次你读取以来没有被更改,if_seq_no 和 if_primary_term 会匹配,你的更改就会被应用。如果不匹配,更新操作会失败。

seq_no 是一个递增的序列号,表示文档的每次修改;

primary_term 表示主分片的当前任期,每当主分片发生变化时,这个值会增加。

主分片(Primary Shard)是 Elasticsearch 中数据存储和索引的基本单元,每个索引被分成若干个主分片,这些分片分布在集群的不同节点上。

为什么废弃version #

原来的 _version 机制是基于单一递增整数,它主要适用于简单的冲突检测,但在复杂的分布式系统中,仅依靠版本号可能无法准确地反映数据的历史和复制状态。尤其是在发生网络分区或节点故障时,仅凭 _version 可能导致数据丢失或过时的数据被错误地写入。

  1. 主分片P1处理了版本1→2→3的更新
  2. P1所在节点崩溃,尚未同步到副本
  3. 新选举的主分片可能只有版本2的数据
  4. 系统现在认为最新版本是2,导致版本3的更新"消失”

Elasticsearch 6.7+ 引入的 seq_noprimary_term组合机制从根本上解决了这个问题:

核心组件

  1. seq_no (序列号)
  • 每个分片维护自己的单调递增序列号
  • 每次操作(创建/更新/删除)都会使seq_no+1
  • 保证操作顺序的可追踪性
  1. primary_term (主分片任期号)
  • 每次主分片重新选举时递增
  • 唯一标识一个主分片的任期周期
  • 解决"脑裂"场景下的冲突

关键规则:只有被多数分片持久化的操作才会向客户端返回成功

支持悲观锁吗? #

不支持