IBM Cloud Docs
文档版本控制和 MVCC

文档版本控制和 MVCC

多版本并行控制 (MVCC) IBM® Cloudant® for IBM Cloud® 数据库如何确保数据库集群中的所有节点仅包含文档的 最新版本

由于 IBM Cloudant 数据库 最终一致,因此需要防止由于在过时文档之间进行同步而导致节点之间出现不一致。

多版本并行控制 (MVCC) 支持对 IBM Cloudant 数据库进行并发读写访问。 MVCC 是 乐观并行的一种形式。 它使 IBM Cloudant 数据库上的读和写操作更快,因为不需要数据库锁定读或写操作。 MVCC 还支持在 IBM Cloudant 数据库节点之间同步。

修订版

IBM Cloudant 数据库中的每个文档都有一个 _rev 字段,用于指示其修订版号。

在插入或修改文档时,服务器会将修订版号添加到文档中。 更改或读取文档时,服务器响应中会包含此编号。 _rev 值是通过使用简单计数器和文档散列的组合来构造的。

修订号的两个主要用途是在以下情况下提供帮助:

  1. 确定必须在服务器之间复制哪些文档。
  2. 确认客户端是否在尝试修改文档的最新版本。

您必须在 更新文档 时指定先前的 _rev ,否则请求将失败并返回 409 错误

_rev 不得用于构建版本控制系统,因为它是服务器使用的内部值。 因此,文档的较旧修订版是瞬态的,并且定期除去。

但是,您可以使用其 _rev来查询特定修订版,但称为 " 压缩。 您可以使用其 _rev 来查询特定文档修订版,以获取文档的修订版历史记录。 但是,压缩的结果是您无法依赖成功的响应。 如果需要文档的版本历史记录,解决方案是对每个修订版创建新文档

分布式数据库和冲突

分布式数据库在不与 IBM Cloudant 上的主数据库(本身是分布式的)建立持续连接的情况下运行,因此基于同一个先前版本的更新仍可能存在冲突。

要查找冲突,请在检索文档时添加查询参数 conflicts=true 。 响应会包含带有所有冲突修订版的 _conflicts 数组。

要在数据库中查找多个文档的冲突,请编写视图。

以下 map 函数是一个示例,它针对每个有冲突的文档发出所有有冲突的修订版。

请参阅映射函数的以下示例以查找具有冲突的文档:

function (doc) {
    if (doc._conflicts) {
        emit(null, [doc._rev].concat(doc._conflicts));
    }
}

您可以定期查询此视图并根据需要解决冲突,或者在每次复制后查询视图。

解决冲突的步骤

找到冲突后,您可以通过四个步骤来解决该冲突: 获取,合并,上载和删除,如后所示。

让我们来考虑一个如何解决冲突的示例。 假设您有一个在线商店的产品数据库。 文档的第一个版本可能类似于以下示例:

{
    "_id": "74b2be56045bed0c8c9d24b939000dbe",
    "_rev": "1-7438df87b632b312c53a08361a7c3299",
    "name": "Samsung Galaxy S4",
    "description": "",
    "price": 650
}

由于文档还没有描述,因此有人可能会添加一个描述。

请参阅通过添加描述创建的文档的第二个版本:

{
    "_id": "74b2be56045bed0c8c9d24b939000dbe",
    "_rev": "2-61ae00e029d4f5edd2981841243ded13",
    "name": "Samsung Galaxy S4",
    "description": "Latest smartphone from Samsung",
    "price": 650
}

同时,使用已复制数据库的其他某个人也降低了价格。

由于 price 值不同,请查看与先前版本冲突的不同修订版:

{
    "_id": "74b2be56045bed0c8c9d24b939000dbe",
    "_rev": "2-f796915a291b37254f6df8f6f3389121",
    "name": "Samsung Galaxy S4",
    "description": "",
    "price": 600
}

然后将复制这两个数据库。 文档版本的差异会导致冲突。

获取冲突修订版

您可以使用 conflicts=true 选项来识别具有冲突的文档。

请参阅以下查找具有冲突的文档的示例:

https://$ACCOUNT.cloudant.com/products/$_ID?conflicts=true

请参阅以下示例响应,其中显示影响文档的冲突修订版:

{
    "_id":"74b2be56045bed0c8c9d24b939000dbe",
    "_rev":"2-f796915a291b37254f6df8f6f3389121",
    "name":"Samsung Galaxy S4",
    "description":"",
    "price":600,
    "_conflicts":["2-61ae00e029d4f5edd2981841243ded13"]
}

具有已更改价格的版本被任意选择为文档的最新版本。 通过在 _conflicts 数组中提供另一个版本的标识来记录与另一个版本的冲突。 在大多数情况下,此数组只有一个元素,但可能存在许多有冲突的修订版。

合并更改

要比较修订版以查看更改的内容,应用程序将从数据库中获取所有版本。

请参阅以下示例命令以从数据库检索文档的所有版本:

https://$ACCOUNT.cloudant.com/products/$_ID
https://$ACCOUNT.cloudant.com/products/$_ID?rev=2-61ae00e029d4f5edd2981841243ded13
https://$ACCOUNT.cloudant.com/products/$_ID?rev=1-7438df87b632b312c53a08361a7c3299

由于文档的不同字段存在有冲突的更改,因此很容易将它们合并在一起。

对于更复杂的冲突,可能需要其他解决策略:

  • 基于时间-使用第一次或最后一次编辑。
  • 用户干预-向用户报告冲突,并让他们决定最佳解决方案。
  • 复杂算法-例如, 3-way 合并文本字段。

有关如何实现变更合并的实际示例,请参阅此项目以及 样本代码

上传新修订版

下一步是创建解决了冲突的文档,并使用该文档来更新数据库。

请参阅以下示例文档,该文档合并了来自两个冲突修订版的更改:

{
    "_id": "74b2be56045bed0c8c9d24b939000dbe",
    "_rev": "3-daaecd7213301a1ad5493186d6916755",
    "name": "Samsung Galaxy S4",
    "description": "Latest smartphone from Samsung",
    "price": 600
}

删除旧修订版

最后,通过向具有要删除的修订版的 URL 发送 DELETE 请求来删除旧修订版。

请参阅以下示例请求以使用 HTTP 删除旧文档修订版:

DELETE https://$ACCOUNT.cloudant.com/products/$_ID?rev=2-61ae00e029d4f5edd2981841243ded13

请参阅以下示例请求以删除旧文档修订版:

curl -H "Authorization: Bearer $API_BEARER_TOKEN" -X DELETE "$SERVICE_URL/events/0007241142412418284?rev=2-9a0d1cd9f40472509e9aac6461837367"
import com.ibm.cloud.cloudant.v1.Cloudant;
import com.ibm.cloud.cloudant.v1.model.DeleteDocumentOptions;
import com.ibm.cloud.cloudant.v1.model.DocumentResult;

Cloudant service = Cloudant.newInstance();

DeleteDocumentOptions documentOptions =
    new DeleteDocumentOptions.Builder()
        .db("events")
        .docId("0007241142412418284")
        .rev("2-9a0d1cd9f40472509e9aac6461837367")
        .build();

DocumentResult response =
    service.deleteDocument(documentOptions).execute()
        .getResult();

System.out.println(response);
const { CloudantV1 } = require('@ibm-cloud/cloudant');
const service = CloudantV1.newInstance({});

service.deleteDocument({
  db: 'events',
  docId: '0007241142412418284',
  rev: '2-9a0d1cd9f40472509e9aac6461837367'
}).then(response => {
  console.log(response.result);
});
from ibmcloudant.cloudant_v1 import CloudantV1

service = CloudantV1.new_instance()

response = service.delete_document(
  db='events',
  doc_id='0007241142412418284',
  rev='2-9a0d1cd9f40472509e9aac6461837367'
).get_result()

print(response)
deleteDocumentOptions := service.NewDeleteDocumentOptions(
  "events",
  "0007241142412418284",
)
deleteDocumentOptions.SetRev("2-9a0d1cd9f40472509e9aac6461837367")

documentResult, response, err := service.DeleteDocument(deleteDocumentOptions)
if err != nil {
  panic(err)
}

b, _ := json.MarshalIndent(documentResult, "", "  ")
fmt.Println(string(b))

先前的 Go 示例需要以下导入块:

import (
   "encoding/json"
   "fmt"
   "github.com/IBM/cloudant-go-sdk/cloudantv1"
)

所有 Go 示例都需要初始化 service 对象。 有关更多信息,请参阅 API 文档的 认证部分 以获取示例。

现在,影响文档的冲突已解决。 您可以通过在 conflicts 参数设置为 true的情况下再次对文档运行 GET 来验证状态。