为什么使用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)
特点
- 精确存储:不像 FLOAT/DOUBLE 有精度损失
- 固定点数:存储格式为字符串,按指定精度存储
- 存储效率:每 4 字节存储 9 位数字
- 计算准确:所有运算都保持精确
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 可能导致数据丢失或过时的数据被错误地写入。
- 主分片P1处理了版本1→2→3的更新
- P1所在节点崩溃,尚未同步到副本
- 新选举的主分片可能只有版本2的数据
- 系统现在认为最新版本是2,导致版本3的更新"消失”
Elasticsearch 6.7+ 引入的
seq_no
和primary_term
组合机制从根本上解决了这个问题:核心组件
- seq_no (序列号):
- 每个分片维护自己的单调递增序列号
- 每次操作(创建/更新/删除)都会使seq_no+1
- 保证操作顺序的可追踪性
- primary_term (主分片任期号):
- 每次主分片重新选举时递增
- 唯一标识一个主分片的任期周期
- 解决"脑裂"场景下的冲突
关键规则:只有被多数分片持久化的操作才会向客户端返回成功
支持悲观锁吗? #
不支持