Elasticsearch 为什么会产生文档版本冲突?如何避免?

网友投稿 434 2022-11-05


Elasticsearch 为什么会产生文档版本冲突?如何避免?

1、Elasticsearch 版本冲突复现

先让大家直观的看到 Elasticsearch 文档版本冲突。

1.1 场景1:create 场景

DELETE my-index-000001# 执行创建并写入PUT my-index-000001/_create/1{ "@timestamp": "2099-11-15T13:12:00", "message": "GET /search HTTP/1.1 200 1070000", "user": { "id": "kimchy" }}# 再次执行会报版本冲突错误。# 报错信息:[1]: version conflict, document already exists (current version [1])PUT my-index-000001/_create/1{ "@timestamp": "2099-11-15T13:12:00", "message": "GET /search HTTP/1.1 200 1070000", "user": { "id": "kimchy" }}

1.2 场景2:批量更新场景模拟

模拟脚本1:循环写入数据 index.sh。

模拟脚本2:循环update_by_query 批量更新数据 update.sh。

由于:写入脚本 index.sh 比更新脚本 update.sh (执行一次,休眠1秒)执行要快,所以更新获取的版本较写入的最新版本要低,会导致版本冲突如下图所示:

1.3 场景3:批量删除场景模拟

写入脚本 index.sh 不变。

删除脚本 delete.sh 如下:

和更新原因一致,由于:写入脚本 index.sh 比删除脚本 delete.sh (执行一次,休眠1秒)执行要快,所以删除获取的版本较写入的最新版本要低,会导致版本冲突如下图所示:

2、Elasticsearch 文档版本定义

执行:

GET test/_doc/1

召回结果如下:

这里的 version 代表文档的版本。

当我们在 Elasticsearch 中创建一个新文档时,它会为该文档分配一个_version: 1。当我们对该文档进行任何后续更新(更新 update、索引 index 或删除 delete)时,_version都会增加 1。

一句话:Elasticsearch 使用_version来鉴别文档是否已更改。

3、Elasticsearch 文档版本产生背景

试想一下,如果没有文档版本?当有并发访问会怎么办?

前置条件:Elasticsearch 从写入到被检索的时间间隔是由刷新频率 refresh_interval 设定的,该值可以更新,但默认最快是 1 秒。

如上图所示,假设我们有一个人们用来评价 T 恤设计的网站。网站很简单,仅列出了T恤设计,允许用户给T恤投票。如果顺序投票,没有并发请求,直接发起update更新没有问题。

但是,在999累计投票数后,碰巧小明同学和小红同学两位同时(并发)发起投票请求,这时候,如果没有版本控制,将导致最终结果不是预期的1001,而是1000。

所以,为了处理上述场景以及比上述更复杂的并发场景,Elasticsearch 亟需一个内置的文档版本控制系统。这就是 _version 的产生背景。

​​悲观锁

悲观锁,又名:悲观并发控制,英文全称:"Pessimistic Concurrency Control",缩写“PCC”,是一种并发控制的方法。

悲观锁本质:在修改数据之前先锁定,再修改。悲观锁优点:采用先锁定后修改的保守策略,为数据处理的安全提供了保证。悲观锁缺点:加锁会有额外的开销,还会增加产生死锁的机会。悲观锁应用场景:比较适合写入操作比较频繁的场景。

4.2 乐观锁

乐观锁,又名:乐观并发控制,英文全称:“Optimistic Concurrency Control”,缩写OCC”,也是一种并发控制的方法。

乐观锁本质:假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,正在提交的事务会进行回滚。乐观锁优点:“胆子足够大,足够乐观”,直到提交的时候才去锁定,不会产生任何锁和死锁。乐观锁缺点:并发写入会有问题,需要有冲突避免策略补救。乐观锁应用场景:数据竞争(data race)不大、冲突较少的场景、比较适合读取操作比较频繁的场景,确保比其他并发控制方法(如悲观锁)更高的吞吐量。

这里要强调的是,Elasticsearch 采用的乐观锁的机制来处理并发问题。

Elasticsearch 乐观锁本质是:没有给数据加锁,而是基于 version 文档版本实现。每次更新或删除数据的时候,都需要对比版本号。

5、Elasticsearch 文档版本冲突的本质

一句话,Elasticsearch 文档冲突的本质——老版本覆盖掉了新版本。

6、如何解决或者避免 Elasticsearch 文档版本冲突?

6.1 external 外部控制版本号

“external”——我的理解就是“简政放权”,交由外部的数据库或者更确切的说,是写入的数据库或其他第三方库来做控制。

版本号可以设置为外部值(例如,如果在数据库中维护)。要启用此功能,version_type应设置为 external。

使用外部版本类型 external 时,系统会检查传递给索引请求的版本号是否大于当前存储文档的版本。

如果为真,也就是新版本大于已有版本,则文档将被索引并使用新的版本号。如果提供的值小于或等于存储文档的版本号,则会发生版本冲突,索引操作将失败。

好处:不论何时,ES 中只有最新版本的数据,借助 external 相对有效的解决版本冲突问题。

实战一把:

如果没有 external,执行如下命令:

PUT my-index-000001/_doc/1?version=2{ "user": { "id": "elkbee" }}

报错如下:

{ "error" : { "root_cause" : [ { "type" : "action_request_validation_exception", "reason" : "Validation Failed: 1: internal versioning can not be used for optimistic concurrency control. Please use `if_seq_no` and `if_primary_term` instead;" } ],.......省略2行...... "status" : 400}

啥意思呢?内部版本控制(internal)不能用于乐观锁,也就是直接使用 version 是不可以的。需要使用:​​if_seq_no​​​ 和 ​​if_primary_term​​,它俩的用法,后文会有专门解读。

如果用 external,执行如下命令:

PUT my-index-000001/_doc/1?version=2&version_type=external{ "user": { "id": "elkbee" }}

执行结果如下:

{ "_index" : "my-index-000001", "_type" : "_doc", "_id" : "1", "_version" : 2, "result" : "updated", "_shards" : { "total" : 2, "successful" : 1, "failed" : 0 }, "_seq_no" : 1, "_primary_term" : 1}

相比于之间没有加 external,加上 external 后,可以实现基于version的文档更新操作。

external_gt 和 external_gte的用法见官方文档,本文不展开,原理同 external。

​​通过if_seq_no 和 if_primary_term 唯一标识避免冲突

索引操作(Index,动词)是有条件的,并且只有在对文档的最后修改分配了由 if_seq_no 和 if_primary_term 参数指定的序列号和 primary term specified(翻译起来拗口,索性用英文)才执行。

如果检测到不匹配,该操作将产生一个 VersionConflictException 409 的状态码。

Step1:写入数据

DELETE products_001PUT products_001/_doc/1567{ "product" : "r2d2", "details" : "A resourceful astromech droid"}# 查看ifseqno 和 ifprimaryterm GET products_001/_doc/1567

返回:

{ "_index" : "products_001", "_type" : "_doc", "_id" : "1567", "_version" : 1, "_seq_no" : 0, "_primary_term" : 1, "found" : true, "_source" : { "product" : "r2d2", "details" : "A resourceful astromech droid" }}

Step2:以这种方式更新,前提是先拿到 if_seq_no 和 if_primary_term

# 模拟数据打tag 过程PUT products_001/_doc/1567?if_seq_no=0&if_primary_term=1{ "product": "r2d2", "details": "A resourceful astromech droid", "tags": [ "droid" ]}# 再获取数据GET products_001/_doc/1567

返回:

{ "_index" : "products_001", "_type" : "_doc", "_id" : "1567", "_version" : 2, "_seq_no" : 1, "_primary_term" : 1, "found" : true, "_source" : { "product" : "r2d2", "details" : "A resourceful astromech droid", "tags" : [ "droid" ] }}

step2 更新数据的时候,是在 step1 的获取已写入文档的 if_seq_no=0 和 if_primary_term=1 基础上完成的。

这样能有效避免冲突。

6.3 批量更新和批量删除忽略冲突实现

如下是在开篇的基础上加了:cnotallow=proceed。

conflicts 默认值是终止,而 proceed 代表继续。

POST test/_update_by_query?conflicts=proceed{ "query": { "match": { "name": "update" } }, "script": { "source": "ctx._source['foo'] = '123ss'", "lang": "painless" }}

cnotallow=proceed 的本质——告诉进程忽略冲突并继续更新其他文档。

开篇不会报 409 错误了,但依然会有版本冲突。但,某些企业级场景是可以用的。

同理,delete_by_query 参数及返回结果均和 update_by_query 一致。

扩展:单个更新 update (区别于批量更新:update_by_query)有 ​​retry_on_conflict​​ 参数,可以设置冲突后重试次数。

7、关于频繁更新带来的性能问题

正如文章开篇演示的,并发更新或者并发删除可能会导致版本冲突。

除了并发性和正确性之外,请注意,非常频繁地更新文档可能会导致性能下降。

如果更新了尚未写入段(segment)的文档,将会导致刷新操作。而刷新频率越小(企业级咨询我见过设置小于1s的,不推荐),势必会导致写入低效。

更多探讨推荐阅读:

​​https://elastic.co/guide/en/elasticsearch/reference/8.1/docs-index_.html#index-versioning

比同事抢先一步学习进阶干货!


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:快递100查询API(快递100查询官方网站)
下一篇:springboot bean扫描路径的实现
相关文章

 发表评论

暂时没有评论,来抢沙发吧~