ELasticsearch是分布式的。当文档创建、更新或删除时,新版本的文档必须复制到集群中的其他节点。Elasticsearch也是异步和并非的,这意味着这些复制请求被并行发送,并且到达目的地时也许顺序时乱的。Elasticsearch需要一种方法确保文档的旧版本不会覆盖新的版本。

当我们之前讨论index,GET 和delete 请求时,我们指出每个文档都有一个_version(版本)号,当文档被修改时版本号递增。Elasticsearch使用这个_version 号来确保变更以正确顺序得到执行。如果旧版本的文档在新版本之后到达,他可以被简单的忽略。

我们可以利用 _version 号来确保应用中相互冲突的变更不会导致数据丢失。我们通过指定想要修改文档的version 号来达到这个目的。如果该版本不是当前版本号,我们的请求就会失败。

让我们创建一个新的博客文章:

  1. PUT /website/blog/1/_create
  2. {
  3. "title": "My first blog entry",
  4. "text": "Just trying this out..."
  5. }

响应体告诉我们,这个新创建的文档 _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":  "Starting to get the hang of this..."
}

我们想这个在我们索引中的文档只有现在的 _version 为1时,本次更新才能成功。

此时请求成功,并且响应体告诉我们 _version 已经递增到2:

{
  "_index":   "website",
  "_type":    "blog",
  "_id":      "1",
  "_version": 2
  "created":  false
}

然而,如果我们重新运行相同的索引请求,仍然指定version=1,Elasticsearch返回409 Conflict HTTP 响应码。和一个如下所示的响应体:

{
   "error": {
      "root_cause": [
         {
            "type": "version_conflict_engine_exception",
            "reason": "[blog][1]: version conflict, current [2], provided [1]",
            "index": "website",
            "shard": "3"
         }
      ],
      "type": "version_conflict_engine_exception",
      "reason": "[blog][1]: version conflict, current [2], provided [1]",
      "index": "website",
      "shard": "3"
   },
   "status": 409
}

这告诉我们在Elasticsearch中这个文档的当前 _veriosn 号是2,但我们指定的更新版本号为1。

我们现在怎么做取决于我们的应用需求。我们可以告诉用户说其他人已经修改了文档,并且在再次保存之前检查这些修改内容。或者,在之前的商品 stock_count 场景,我们可以获取到最新的文档并尝试重新应用这些修改。

所有文档的更新或删除API,都可以接受version 参数,这允许你在代码中使用乐观的并发控制,这是一种明智的做法。

通过外部系统使用版本控制

一个常见的设置是使用其他数据库作为主要的数据存储,使用Elasticsearch做数据检索,这意味着主数据的所有更改发生时都需要被复制到Elasticsearch,如果多个进程负责这一数据同步,你可能遇到类似于之前描述的并非问题。

如果你的主数据已经有了版本号——或一个能作为版本号的字段值,比如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,
  "created":  true
}

现在我们更新这个文档,指定一个新的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,
  "created":  false
}

如果你要重新运行此请求时,他将会失败,并返回像我们之前看到的同样的冲突错误,因为指定的外部版本号不大于Elasticsearch的当前版本号。