一般我们在更新文档时,主要的操作流程时:读取文档->修改->提交保存。数据中心等保存的都是最新一次提交的内容。
大部分时候,这都没有什么问题。但是如果两个或更多的请求同时修改一个文档时,非常容易产生冲突,因为上述的流程无法保证原子性,也不可能保证。
冲突解决常用的两种策略:
- 悲观锁并发策略
在关系性数据库中,通过阻塞并排队的方式,来避免发生冲突,例如在读取数据行时阻塞,来保证正在修改行数据的请求完成正常操作后,以读取到最新的数据。这种方式的前提假设是数据冲突更有可能发生。 - 乐观锁并发策略
Elasticsearch中采用的是乐观锁的并发策略,这种方式的前期假设是数据冲突一般不会发生,从而避免阻塞数据请求。然而,在读和写之间,如果数据发生改变,更新就失败了,然后由程序决定如果进行后续的处理。
Elasticsearch是分布式的,文档的创建/变更等都会同步到其他节点。由于其异步性和并发的特点,这些同步请求都是并行的,因此并不能保证数据的是按照修改顺序依次到达的。Elasticsearch保证了一个老版本的数据永远无法重写或覆盖更新版本的数据。在 index
get
和 delete
请求中,都存在一个 _version
字段。数据的变更均会导致_version
的值增大。Elasticsearch通过该字段来保证小于该值的数据会被忽略掉。通过数字版本的方式也可以避免ABA的数据问题,即数据A修改为B而后又修改为A,对于应用端来说,数据是没有任何变化的。
创建文档:
1 PUT /website/blog/1/_create 2 { 3 "title": "My first blog entry", 4 "text": "Just trying this out..." 5 }
获取文档:
1 GET /website/blog/1 2 结果: 3 { 4 "_index" : "website", 5 "_type" : "blog", 6 "_id" : "1", 7 "_version" : 1, 8 "found" : true, 9 "_source" : { 10 "title": "My first blog entry", 11 "text": "Just trying this out..." 12 } 13 }
此时 _version
为1
修改数据:
1 PUT /website/blog/1?version=1 2 { 3 "title": "My first blog entry", 4 "text": "Starting to get the hang of this..." 5 }
1 { 2 "_index": "website", 3 "_type": "blog", 4 "_id": "1", 5 "_version": 2 6 "created": false 7 }
此时操作成功。
如果在_version为1的基础我们再次提交修改:
1 { 2 "error": { 3 "root_cause": [ 4 { 5 "type": "version_conflict_engine_exception", 6 "reason": "[blog][1]: version conflict, current [2], provided [1]", 7 "index": "website", 8 "shard": "3" 9 } 10 ], 11 "type": "version_conflict_engine_exception", 12 "reason": "[blog][1]: version conflict, current [2], provided [1]", 13 "index": "website", 14 "shard": "3" 15 }, 16 "status": 409 17 }
此时,显示存在版本冲突。此时应用可以据此作出相应的处理,获取最新数据Merge处理亦或其他处理方式。
问题解决:在请求上加上retry_on_conflict的参数,在冲突发生时,重试提交数据:
1 POST /website/pageviews/1/_update?retry_on_conflict=5 2 { 3 //some update 4 }