This article reprinted from: https://www.elastic.co/guide/cn/elasticsearch/guide/current/index.html , ES version 6.3.0
Dealing with conflict
when we use index API update the document, the original document can be read one time, make our changes, and then re-index the entire document. Recent requests index will prevail: no matter what the final document is indexed, will be the only store in Elasticsearch in. If the other person to change the document at the same time, their changes will be lost.
In many cases this is not a problem. Perhaps our primary data storage is a relational database, we simply copy the data to Elasticsearch and it can be searched. Maybe two people in the same document to change the probability is very small. Or for our business occasionally lose the changes are not very serious problem.
But sometimes lost is a very serious change. Imagine the number of our online store merchandise inventory we use Elasticsearch store, every time we sell a product, we will reduce the number of Elasticsearch inventory.
One day, the management decided to do a promotion. Suddenly, one second we sell several commodities. Suppose there are two web run in parallel, each simultaneously process sales of all goods, FIG 7 "Consequence of no concurrency control" as shown in FIG.
web_1 stock_count changes made have been lost, because it does not know web_2 copy stock_count has expired. We think the results will have more than the actual number of items of inventory because the inventory of goods sold to the customer does not exist, we will make them very disappointed.
Changes more frequent, longer gaps read data and updating data, the more changes may be lost.
In the database world, there are two methods typically used to ensure that the change is not lost when concurrent UPDATE:
pessimistic concurrency control
of this method is widely used relational database, it assumes that there is a change of potential conflict, thus blocking access to resources to prevent conflicts . A typical example is the first row of data before reading it locks, lock ensures that only the thread is placed on the data can be modified.
Optimistic concurrency control
this Elasticsearch method used can not happen when the assumed conflict, and does not block the operation being attempted. However, if the metadata is modified in reading and writing, the update will fail. Next, the application will determine how to resolve the conflict. For example, it is possible to retry the update with the new data, or to report relevant information to the user.
乐观并发控制
Elasticsearch是分布式的。当文档创建、更新或删除时,新版本的文档必须复制到集群中的其他节点。Elasticsearch也是异步和并发的,这意味着这些请求被并行发送,并且到达目的地时也许 顺序是乱的。 Elasticsearch 需要一种方法确保文档的旧版本不会覆盖新的版本。
当我们之前讨论 INDEX, GET 和 DELETE请求时,我们指出每个文档都有一个 _version (版本)号,当文档被修改时版本号递增。 Elasticsearch 使用这个 _version 号来确保变更以正确顺序得到执行。如果旧版本的文档在新版本之后到达,它可以被简单的忽略。
我们可以利用 _version 号来确保 应用中相互冲突的变更不会导致数据丢失。我们通过指定想要修改文档的 version 号来达到这个目的。 如果该版本不是当前版本号,我们的请求将会失败。
PUT /website/blog/1/_create
{
"title": "My first blog entry",
"text": "Just trying this out..."
}
响应体告诉我们,这个新创建的文档 _version 版本号是 1 。现在假设我们想编辑这个文档:我们加载其数据到 web 表单中, 做一些修改,然后保存新的版本。
首先我们检索文档:
GET /website/blog/1
响应体包含相同的 _version 版本号 1 :
{
"_index": "website",
"_type": "blog",
"_id": "1",
"_version": 1,
"found": true,
"_source": {
"title": "My first blog entry",
"text": "Just trying this out..."
}
}
现在,当我们尝试通过重建文档的索引来保存修改,我们指定 version 为我们的修改会被应用的版本:
PUT /website/blog/1?version=1
{
"title": "My first blog entry",
"text": "Just trying this out..."
}
我们想这个在我们索引中的文档只有现在的 _version 为 1 时,本次更新才能成功。
此请求成功,并且响应体告诉我们 _version 已经递增到 2 :
{
"_index": "website",
"_type": "blog",
"_id": "1",
"_version": 2,
"result": "updated",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 1,
"_primary_term": 1
}
然而,如果我们重新运行相同的索引请求,仍然指定 version=1 , Elasticsearch 返回 409 ConflictHTTP 响应码,和一个如下所示的响应体:
{
"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": "g6JAk0OMRsauokdYGafEHA",
"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": "g6JAk0OMRsauokdYGafEHA",
"shard": "3",
"index": "website"
},
"status": 409
}
这告诉我们在Elasticsearch中这个文档的当前_version是2,但我们指定的更新版本号为1。我们现在怎么做取决于我们的应用需求。我们可以告诉用户说其他人已经修改了文档,并且在再次保存之前检查这些修改内容。或者,在之前的stock_coun场景,我们可以获取到最新的文档并尝试重新应用这些修改。
所有文档的更新或删除API,都可以接受version参数,这允许你在代码中使用乐观的并发控制,这是一种明智的做法。
通过外部系统使用版本控制
一个常见的设置是使用其他数据库作为主要的数据存储,使用Elasticsearch做数据检索,这意味着主数据库的所有更改发生时都需要被复制到Elasticsearh,如果多个进程负责这一数据同步,你可能遇到类似于之前 描述的并发问题。
如果你的主数据库已经有了版本号 — 或一个能作为版本号的字段值比如 timestamp — 那么你就可以在 Elasticsearch 中通过增加 version_type=external 到查询字符串的方式重用这些相同的版本号, 版本号必须是大于零的整数, 且小于 9.2E+18 — 一个 Java 中 long 类型的正值。外部版本号的处理方式和我们之前讨论的内部版本号的处理方式有些不同, Elasticsearch 不是检查当前 _version 和请求中指定的版本号是否相同, 而是检查当前 _version 是否 小于 指定的版本号。 如果请求成功,外部的版本号作为文档的新 _version 进行存储。
外部版本号不仅在索引和删除请求是可以指定,而且在 创建 新文档时也可以指定。
例如,要创建一个新的具有外部版本号 5 的博客文章,我们可以按以下方法进行
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 :
{
"_index": "website",
"_type": "blog",
"_id": "2",
"_version": 5,
"result": "created",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 0,
"_primary_term": 1
}
现在我们更新这个文档,指定一个新的 version 号是 10
PUT /website/blog/2?version=10&version_type=external
{
"title": "My first external blog entry",
"text": "This is a piece of cake..."
}
请求成功并将当前 _version 设为 10 :
{
"_index": "website",
"_type": "blog",
"_id": "2",
"_version": 10,
"result": "updated",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 1,
"_primary_term": 1
}
如果你要重新运行此请求时,它将会失败,并返回像我们之前看到的同样的冲突错误, 因为指定的外部版本号不大于 Elasticsearch 的当前版本号。
文档的部分更新
在 更新整个文档 , 我们已经介绍过 更新一个文档的方法是检索并修改它,然后重新索引整个文档,这的确如此。然而,使用 update API 我们还可以部分更新文档,例如在某个请求时对计数器进行累加。
我们也介绍过文档是不可变的:他们不能被修改,只能被替换。 update API 必须遵循同样的规则。 从外部来看,我们在一个文档的某个位置进行部分更新。然而在内部, update API 简单使用与之前描述相同的 检索-修改-重建索引 的处理过程。 区别在于这个过程发生在分片内部,这样就避免了多次请求的网络开销。通过减少检索和重建索引步骤之间的时间,我们也减少了其他进程的变更带来冲突的可能性。
update请求最简单的一种形式是接收文档的一部分作为doc参数,他只是与现有的文档进行合并。对象被合并到一起,覆盖现有的字段,增加新的字段。例如,我们增加字段tags和views到我们的博客文档,如下所示:
POST /website/blog/1/_update
{
"doc" : {
"tags" : [ "testing" ],
"views": 0
}
}
请求成功后的相应:
{
"_index": "website",
"_type": "blog",
"_id": "1",
"_version": 3,
"result": "updated",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 2,
"_primary_term": 1
}
检索文档显示了更新后的 _source 字段:
GET /website/blog/1/_source
{
"title": "My first blog entry",
"text": "Just trying this out...",
"views": 0,
"tags": [
"testing"
]
}
可以看到views和tags字段已被添加到_source中
使用脚本部分更新文档
脚本可以在 update API中用来改变 _source 的字段内容, 它在更新脚本中称为 ctx._source 。 例如,我们可以使用脚本来增加博客文章中 views 的数量:
POST website/blog/1/_update
{
"script": "ctx._source.views+=1"
}
返回体
{
"_index": "website",
"_type": "blog",
"_id": "1",
"_version": 4,
"result": "updated",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 3,
"_primary_term": 1
}
我们也可以通过使用脚本给tags数组添加一个新的标签。在这个例子中,我们制定新的标签作为参数,而不是每次我们想添加标签时都要对新脚本重新编译
POST /website/blog/1/_update
{
"script" : "ctx._source.tags.add(params.new_tag)",
"params" : {
"new_tag" : "search"
}
}
获取文档并显示结果
{
"_index": "website",
"_type": "blog",
"_id": "1",
"_version": 7,
"found": true,
"_source": {
"title": "My first blog entry",
"text": "Just trying this out...",
"views": 1,
"tags": [
"testing",
"search"
]
}
}
可以看到search 标签已追加到 tags 数组中
我们甚至可以选择通过设置 ctx.op 为 delete 来删除基于其内容的文档:
POST /website/blog/1/_update
{
"script": {
"source": "ctx.op = ctx._source.views == params.count ? 'delete': 'none'",
"params": {
"count": 1
}
}
}
更新不存在的文档
想象我们要在Elasticsearch中存储浏览量计数器。每当有用户访问页面,我们增加这个页面的浏览量。但如果这是个新页面,我们并不确定这个计数器存在与否。当我们试图更新一个不存在的文档,更新将失败。
在这种情况下,我们可以使用upsert参数定义文档来使其不存在时被创建。
POST /website/pageviews/1/_update
{
"script" : "ctx._source.views+=1",
"upsert": {
"views": 1
}
}
第一次执行这个请求,upsert值被索引为一个新文档,初始化views字段为1.接下来文档已经存在,所以script被更新代替,增加views数量。
notes:Elasticsearch6.x不允许索引下有多个文档类型
更新和冲突
这这一节的介绍中,我们介绍了如何在检索(retrieve)和重建索引(reindex)中保持更小的窗口,如何减少冲突性变更发生的概率,不过这些无法被完全避免,像一个其他进程在update进行重建索引时修改了文档这种情况依旧可能发生。
为了避免丢失数据,update API在检索(retrieve)阶段检索文档的当前_version,然后在重建索引(reindex)阶段通过index请求提交。如果其他进程在检索(retrieve)和重建索引(reindex)阶段修改了文档,_version将不能被匹配,然后更新失败。
对于多用户的局部更新,文档被修改了并不要紧。例如,两个进程都要增加页面浏览量,增加的顺序我们并不关心——如果冲突发生,我们唯一要做的仅仅是重新尝试更新既可。
这些可以通过retry_on_conflict参数设置重试次数来自动完成,这样update操作将会在发生错误前重试——这个值默认为0。
POST /website/pageviews/1/_update?retry_on_conflict=5 #在错误发生前重试更新5次
{
"script" : "ctx._source.views+=1",
"upsert": {
"views": 0
}
}
这适用于像增加计数这种顺序无关的操作,但是还有一种顺序非常重要的情况。例如index API,使用“保留最后更新(last-write-wins)”的update API,但它依旧接受一个version参数以允许你使用乐观并发控制(optimistic concurrency control)来指定你要更细文档的版本。