当我们使用index
API更新文档时,可以一次性读取原始文档,然后重新索引整个文档,最近的索引请求将获胜:无论最后哪一个文档被索引,都将被唯一存储在ElasticSearch中。如果其他人同时更改这个文档,他们的更改将丢失。
很多时候这是没有问题的,也许我们的主数据存储是一个关系型数据库,我们只是将其复制到ElasticSearch中,使其可以被搜索,也许两个人同时更改同一个文档的几率很小,或者对于我们业务来说,偶尔丢失更改也不是很大的问题,但有时丢失了一个变更是非常严重的。
在数据库领域中,有两种方法通常被用来确保并发更新时更新不会丢失:
- 悲观并发控制:这种方法被关系型数据库广泛使用,它假定有变更冲突可能发生,因此阻塞访问资源以防止冲突。一个典型的例子是读取一行数据之前先将其锁住,确保只有放置锁的线程才能对该行进行修改。
- 乐观并发控制:ElasticSearch中使用的这种方法假定冲突是不可能发生的,并且不会阻塞正在尝试的操作。然而,如果源数据在读写当中被修改,更新将会失败。应用程序接下来将决定该如何解决冲突。例如:可以尝试更新、使用新的数据、或将相关报告情况报告给用户
乐观并发控制
ElasticSearch是分布式的。当文档创建、更新或删除时,新版本的文档必须复制到集群中其他节点。ElasticSearch也是异步和并发的,这意味着这些复制请求被并行发送,并且达到目的时也许顺序是乱的。ElasticSearch需要一种方法确保文档的旧版本不会覆盖新版本。
当我们之前介绍的GET
、DELETE
请求时,我们输出的每一个文档都有一个_version
版本号,当文档被修改时版本号递增。ElasticSearch使用这个_version
确保变更以正确的顺序得到执行。如果旧版本的文档在新版本之后到达,它可以被简单的忽略。
我们可以用_version
来确保应用中相互冲突的变更不会导致数据的丢失。我们通过指定想要修改文档的_version
来达到这个目的。如果该版本不是当前版本号,我们请求将失败
接下来将通过一步步简单的操作来叙述:
- 创建一篇博客文章
curl -XPUT "http://localhost:9200/csdn/blog/1" -d '
{
"title":"test"
"desc":"测试"
}
'
此时会得到一个响应体告诉我们_version
为1
- 修改博客内容,指定版本修改
curl -XPUT "http://localhost:9200/csdn/blog/1?version=1"
此时会得到一个响应体告诉我们_version
已经递增为2了
- 然而我们如果再次相同的请求体,即version=1,ElasticSearch会返回409错误,因为此时的
_version
已经为2了
{
"error": {
"root_cause": [
{
"type": "version_conflict_engine_exception",
"reason": "[blog][1]: version conflict, current version [2] is different than the one provided [1]",
"index_uuid": "3t42ZzdSRoO8RPkL0jDYbg",
"shard": "3",
"index": "csdn"
}
],
"type": "version_conflict_engine_exception",
"reason": "[blog][1]: version conflict, current version [2] is different than the one provided [1]",
"index_uuid": "3t42ZzdSRoO8RPkL0jDYbg",
"shard": "3",
"index": "csdn"
},
"status": 409
}
所有文档的更新或删除API,都可以接受一个version
参数,这允许你在代码中使用乐观的并发控制,这是一种明智的做法。
通过外部系统使用版本控制
一个常见的设置是使用其他数据库作为主要的数据存储,使用ElasticSearch做数据检索,意味着主数据库的所有更改都要更新到ElasticSearch,如果多个进程同时更改同一数据,你可能遇到类似于之前描述的并发问题。
在ElasticSearch中可以增加version_type=external
到查询字符串的方式重用这些相同的版本号,版本号必须是大于零的整数, 且小于 9.2E+18
,一个 Java 中 long 类型的正值。
外部版本号处理方式和我们之前讨论的内部版本号的处理方式有些不同,ElasticSearch不是检查当前_version
和请求中指定的版本号是否相同,而是检查当前_version
是否小于指定的版本号。如果请求成功,外部的版本号作为文档的新的_version
作为存储。
接下来将通过一步步简单的操作来叙述:
- 创建一篇博客文章,版本为1
curl -XPUT "http://localhost:9200/csdn/blog/1?version=1&version_type=external" -d '
{
"title":"test"
"desc":"测试"
}
'
将会获得如下数据:
{
"_index" : "csdn",
"_type" : "blog",
"_id" : "1",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"created" : true
}
- 更新这篇博客,将版本号升级为2
curl -XPUT "http://localhost:9200/csdn/blog/1?version=2&version_type=external" -d '
{
"title":"test1"
"desc":"测试1"
}
'
将会获得如下数据:
{
"_index" : "csdn",
"_type" : "blog",
"_id" : "1",
"_version" : 2,
"result" : "updated",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"created" : false
}
- 再次执行第2步操作,版本不变,将会报
409
错误,因为外部的版本号,必须大于当前的_version
{
"error" : {
"root_cause" : [
{
"type" : "version_conflict_engine_exception",
"reason" : "[blog][1]: version conflict, current version [2] is higher or equal to the one provided [2]",
"index_uuid" : "3t42ZzdSRoO8RPkL0jDYbg",
"shard" : "3",
"index" : "csdn"
}
],
"type" : "version_conflict_engine_exception",
"reason" : "[blog][1]: version conflict, current version [2] is higher or equal to the one provided [2]",
"index_uuid" : "3t42ZzdSRoO8RPkL0jDYbg",
"shard" : "3",
"index" : "csdn"
},
"status" : 409
}