1. 用于处理冲突
通常,当数据库因为请求冲突发生幻读问题时,有两种解决策略:
- 悲观并发控制如:行级锁;
这种方法被关系型数据库广泛使用,它假定有变更冲突可能发生,因此阻塞访问资源以防止冲突。一个典型的例子是读取一行数据之前先将其锁住,确保只有放置锁的线程能够对这行数据进行修改。
- 乐观并发控制如:写操作时发现数据发生变化,操作失败。
它假定冲突是不可能发生的,所以不会阻塞正在尝试的操作。 然而,如果源数据在读写当中被修改,更新将会失败。应用程序接下来将决定该如何解决冲突。 例如,可以获取新的数据,重试更新、或者将相关情况报告给用户。
ElasticSearch采用乐观并发控制。Elasticsearch是分布式的,当文档被创建、更新或删除,文档的新版本会被复制到集群的其它节点。Elasticsearch即是同步的又是异步的,意思是这些复制请求都是平行发送的,并无序(out of sequence)的到达目的地。这就需要一种方法确保老版本的文档永远不会覆盖新的版本。
这时就用到了元数据“_version
”,每个文档都有一个_version
号码,这个号码在文档被改变时加1
,Elasticsearch使用这个_version
保证所有修改都被正确排序。当一个旧版本出现在新版本之后,它会被简单的忽略。
2. _version
的运用
我们利用_version的这一优点确保数据不会因为修改冲突而丢失。我们可以指定文档的version来做想要的更改。如果那个版本号不是现在的,我们的请求就失败了。
如:先插入一条数据
PUT test_index_01/default/1
{
"name":"lzz"
}
响应体告诉我们这是一个新建的文档,它的_version
是1。
{
“_index”: "test_index_01",
"_type":"default",
"_id":"1",
"_version":1,
"result":"created",
"_shards": {
"total": 2,
"successful": 2,
"failed": 0
},
"created": true
}
现在假设我们要编辑这个文档:
PUT test_index_01/default/1?version=1
{
"name":"lcc"
}
请求成功,响应体告诉我们_version已经增加到2。
{
“_index”: "test_index_01",
"_type":"default",
"_id":"1",
"_version":2,
"result":"updated",
"_shards": {
"total": 2,
"successful": 2,
"failed": 0
},
"created": false
}
现在,如果我们重新运行相同的索引请求,依旧指定version=1
,Elasticsearch将返回409
版本冲突version_conflict_engine_exception
异常的HTTP响应。这告诉我们当前提供的_version
是1
,但是我们指定想要更新的数据版本是2
。
{
"error": {
"root_cause": [
{
"type": "version_conflict_engine_exception",
"reason": "[default][1]: version conflict, current version [2] is different than the one provided [1]",
"index_uuid": "avGv62ylTJCaTdsUBa96oQ",
"shard": "3",
"index": "test_index_01"
}
],
"type": "version_conflict_engine_exception",
"reason": "[default][1]: version conflict, current version [2] is different than the one provided [1]",
"index_uuid": "avGv62ylTJCaTdsUBa96oQ",
"shard": "3",
"index": "test_index_01"
},
"status": 409
}
3. 使用外部版本
一种常见的结构是使用一些其他的数据库做为主数据库,然后使用Elasticsearch搜索数据,这意味着所有主数据库发生变化,就要将其拷贝到Elasticsearch中。如果有多个进程负责这些数据的同步,就会遇到上面提到的并发问题。
如果主数据库有版本字段(或一些类似于timestamp等可以用于版本控制的字段)时,你就可以在Elasticsearch的查询字符串后面添加version_type=external
来使用这些版本号。版本号必须是整数,大于零小于9.2e+18
(Java中的正的long)。
外部版本号
与之前说的内部版本号
在处理的时候有些不同,它不再检查_version
是否与请求中指定的一致,而是检查是否小于指定的版本。如果请求成功,外部版本号就会被存储到_version
中。
外部版本号不仅在索引和删除请求中指定,也可以在创建(create)新文档中指定。
例如,创建一个包含外部版本号5的数据,我们可以这样做:
PUT test_index_02/default/1?version=5&version_type=external
{
"name":"lpp"
}
在响应中,我们能看到当前的_version
号码是5
:
{
“_index”: "test_index_02",
"_type":"default",
"_id":"1",
"_version":5,
"result":"created",
"_shards": {
"total": 2,
"successful": 2,
"failed": 0
},
"created": true
}
现在我们更新这个文档,指定一个新version
号码为10
:
PUT test_index_02/default/1?version=10&version_type=external
{
"name":"loo"
}
请求成功,该条数据当前_version
变为10
:
{
“_index”: "test_index_02",
"_type":"default",
"_id":"1",
"_version":10,
"result":"updated",
"_shards": {
"total": 2,
"successful": 2,
"failed": 0
},
"created": false
}
如果你重新运行这个请求,就会返回一个像之前一样的冲突错误,因为指定的外部版本号不大于当前在Elasticsearch中的版本。