9.1 冲突处理
如果两个线程同时修改一个文档,这时就会发生冲突。
比如某件商品存货100件,用户1下单买走1件,剩余99件;与此同时用户2也下单买走1件,但是用户2不知道用户1已经下单,看到剩余商品仍然是99件。这样造成系统中显示商品总数比实际数量要多,这种情况在商业系统中肯定是不能容忍的。
在数据库领域中,有两种方法通常被用来确保并发更新时变更不会丢失: 1、悲观并发控制 这种方法被关系型数据库广泛使用,它假定有变更冲突可能发生,因此阻塞访问资源以防止冲突。 一个典型的例子是读取一行数据之前先将其锁住,确保只有放置锁的线程能够对这行数据进行修改。 2、乐观并发控制 Elasticsearch 中使用的这种方法假定冲突是不可能发生的,并且不会阻塞正在尝试的操作。 然而,如果源数据在读写当中被修改,更新将会失败。应用程序接下来将决定该如何解决冲突。 例如,可以重试更新、使用新的数据、或者将相关情况报告给用户。
9.2 乐观并发控制
当我们之前讨论 index , GET 和 delete 请求时,我们指出每个文档都有一个 _version (版本)号,当文档被修改时版本号递增。 Elasticsearch 使用这个 _version 号来确保变更以正确顺序得到执行。如果旧版本的文档在新版本之后到达,它可以被简单的忽略。
我们可以利用 _version 号来确保 应用中相互冲突的变更不会导致数据丢失。我们通过指定想要修改文档的 version 号来达到这个目的。 如果该版本不是当前版本号,我们的请求将会失败。
代码语言:javascript复制PUT website
代码语言:javascript复制{
"acknowledged": true,
"shards_acknowledged": true,
"index": "website"
}
添加一个文档
代码语言:javascript复制PUT website/blog/1/_create
{
"title": "My first blog entry",
"text": "Just trying this out..."
}
代码语言:javascript复制{
"_index": "website",
"_type": "blog",
"_id": "1",
"_version": 1,
"result": "created",
"_shards": {
"total": 2,
"successful": 2,
"failed": 0
},
"created": true
}
响应体告诉我们,这个新创建的文档 _version 版本号是 1 。 现在假设我们想编辑这个文档:我们加载其数据到 web 表单中, 做一些修改,然后保存新的版本。
代码语言:javascript复制GET website/blog/1
代码语言:javascript复制{
"_index": "website",
"_type": "blog",
"_id": "1",
"_version": 1,
"found": true,
"_source": {
"title": "My first blog entry",
"text": "Just trying this out..."
}
}
现在,当我们尝试通过重建文档的索引来保存修改,我们指定 version 为我们的修改会被应用的版本
代码语言:javascript复制PUT /website/blog/1?version=1
{
"title": "My first blog entry",
"text": "Starting to get the hang of this..."
}
此请求成功,并且响应体告诉我们 _version 已经递增到 2 :
代码语言:javascript复制{
"_index": "website",
"_type": "blog",
"_id": "1",
"_version": 2,
"result": "updated",
"_shards": {
"total": 2,
"successful": 2,
"failed": 0
},
"created": false
}
然而,如果我们重新运行相同的索引请求,仍然指定 version=1 , Elasticsearch 返回 409 Conflict HTTP 响应码,和一个如下所示的响应体:
代码语言:javascript复制{
"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": "ekHPTnDgRH63lHUpvxqQBA",
"shard": "3",
"index": "website"
}
],
"type": "version_conflict_engine_exception",
"reason": "[blog][1]: version conflict, current version [2] is different than the one provided [1]",
"index_uuid": "ekHPTnDgRH63lHUpvxqQBA",
"shard": "3",
"index": "website"
},
"status": 409
}
响应体告诉我们在 Elasticsearch 中这个文档的当前 _version 号是 2 ,但我们指定的更新版本号为 1
我们现在怎么做取决于我们的应用需求。我们可以告诉用户说其他人已经修改了文档,并且在再次保存之前检查这些修改内容。 或者,在之前的商品 stock_count 场景,我们可以获取到最新的文档并尝试重新应用这些修改。
所有文档的更新或删除 API,都可以接受 version 参数,这允许你在代码中使用乐观的并发控制,这是一种明智的做法。
9.3 通过外部系统使用版本控制
一个常见的设置是使用其它数据库作为主要的数据存储,使用 Elasticsearch 做数据检索, 这意味着主数据库的所有更改发生时都需要被复制到 Elasticsearch
Elasticsearch 中通过增加 version_type=external 方式指定外部版本号,如果外部版本号是否大于当前文档版本,则可以执行更新操作。
例如,要更新一个新的具有外部版本号 5 的博客文章,我们可以按以下方法进行:
代码语言:javascript复制PUT /website/blog/2?version=5&version_type=external
{
"title": "My first external blog entry",
"text": "Starting to get the hang of this..."
}
在响应中,我们能看到当前的 _version 版本号是 5
代码语言:javascript复制{
"_index": "website",
"_type": "blog",
"_id": "2",
"_version": 5,
"result": "created",
"_shards": {
"total": 2,
"successful": 2,
"failed": 0
},
"created": true
}
当我们再次执行上面的外部版本号更新时,报错。因为外部版本号不大于当前版本号5.
代码语言:javascript复制{
"error": {
"root_cause": [
{
"type": "version_conflict_engine_exception",
"reason": "[blog][2]: version conflict, current version [5] is higher or equal to the one provided [5]",
"index_uuid": "ekHPTnDgRH63lHUpvxqQBA",
"shard": "2",
"index": "website"
}
],
"type": "version_conflict_engine_exception",
"reason": "[blog][2]: version conflict, current version [5] is higher or equal to the one provided [5]",
"index_uuid": "ekHPTnDgRH63lHUpvxqQBA",
"shard": "2",
"index": "website"
},
"status": 409
}