2.3. 复制和冲突模型¶
让我们用下面的例子来说明复制和冲突处理。
- 爱丽丝有一份文件,里面有鲍勃的名片;
- 她在台式电脑和笔记本电脑之间同步;
- 在台式电脑上,她更新了鲍勃的电子邮件地址;不再同步,她在笔记本电脑上更新了鲍勃的手机号码;
- 然后她又把两者复制到了一起。
因此,在桌面上,文档中有Bob的新电子邮件地址和旧手机号码,而在笔记本电脑上则有他的旧电子邮件地址和新手机号码。
问题是,这些相互冲突的更新文档会发生什么?
2.3.1. CouchDB复制¶
CouchDB处理数据库中的JSON文档。数据库的复制通过HTTP进行,可以是“拉”或“推”,但是单向的。因此,执行完全同步的最简单方法是先“推”然后“拉”(反之亦然)。
所以,Alice创建v1并同步它。她一边更新到v2a,另一边更新到v2b,然后复制。会发生什么?
答案很简单:两个版本都存在于两边!
DESKTOP LAPTOP
+---------+
| /db/bob | INITIAL
| v1 | CREATION
+---------+
+---------+ +---------+
| /db/bob | -----------------> | /db/bob | PUSH
| v1 | | v1 |
+---------+ +---------+
+---------+ +---------+ INDEPENDENT
| /db/bob | | /db/bob | LOCAL
| v2a | | v2b | EDITS
+---------+ +---------+
+---------+ +---------+
| /db/bob | -----------------> | /db/bob | PUSH
| v2a | | v2a |
+---------+ | v2b |
+---------+
+---------+ +---------+
| /db/bob | <----------------- | /db/bob | PULL
| v2a | | v2a |
| v2b | | v2b |
+---------+ +---------+
毕竟,这不是一个文件系统,因此没有限制只能存在一个名为/db/bob的文档。这些只是同名的“冲突”修订。
因为更改总是被复制的,所以数据是安全的。两台机器都有两个文档的相同副本,因此任何一侧的硬盘故障都不会丢失任何更改。
另一件要注意的事情是,对等点不必配置或跟踪。您可以对对等机执行定期复制,也可以执行一次性的临时推送或拉取操作。在复制发生后,没有记录任何特定文档或修订来自哪个对等方。
所以现在的问题是:当你试图读/db/bob时会发生什么?默认情况下,CouchDB选择一个任意的修订作为“赢家”,使用一个确定的算法,以便在所有对等节点上都能做出相同的选择。同样的情况也发生在视图上:决定性地选择的赢家是唯一输入到地图函数中的修订。
假设赢家是v2a,在桌面上,如果Alice阅读文档,她会看到v2a,这是她保存在那里的。但在笔记本电脑上,复制之后,她也只能看到v2a,看起来她在那里所做的更改似乎丢失了——但当然没有,它们只是被隐藏起来,作为一个相互冲突的修订版。但最终她需要将这些更改合并到Bob的名片中,否则这些更改将实际上丢失。
任何明智的名片应用程序,至少,必须提出冲突的版本爱丽丝,并允许她创建一个新的版本,其中包括所有的信息。理想情况下,它会合并更新本身。
2.3.2. 避免冲突¶
当处理单个节点时,CouchDB将通过返回一个 409 Conflict 错误。这是因为,当您放入文档的新版本时,必须将 _rev
上一版本的。如果那样的话 _rev
已被替换,更新被拒绝 409 Conflict 反应。
因此,假设同一节点上的两个用户正在获取Bob的名片,同时对其进行更新,然后将其写回:
USER1 -----------> GET /db/bob
<----------- {"_rev":"1-aaa", ...}
USER2 -----------> GET /db/bob
<----------- {"_rev":"1-aaa", ...}
USER1 -----------> PUT /db/bob?rev=1-aaa
<----------- {"_rev":"2-bbb", ...}
USER2 -----------> PUT /db/bob?rev=1-aaa
<----------- 409 Conflict (not saved)
User2的更改被拒绝,因此由应用程序再次获取/db/bob,或者:
- 应用与先前版本相同的更改,并提交新的PUT
- 重新显示文档,以便用户必须再次编辑它
- 用之前保存的文档覆盖它(这是不可取的,因为user1的更改将自动丢失)
因此,在这种模式下工作时,应用程序仍然必须能够处理这些冲突,并有一个合适的重试策略,但是这些冲突永远不会在数据库本身内部结束。
2.3.3. 修订树¶
当您在CouchDB中更新文档时,它会保留以前修订的列表。在这种情况下,在本文档的树中引入了一个冲突的树状结构的更新,其中有冲突的分支:
,--> r2a
r1 --> r2b
`--> r2c
然后,每个分支都可以扩展其历史记录-例如,如果您阅读了修订版r2b,然后使用?rev=r2b,然后您将沿着该特定分支进行新的修订。
,--> r2a -> r3a -> r4a
r1 --> r2b -> r3b
`--> r2c -> r3c
这里,(r4a,r3b,r3c)是一组相互冲突的修订。解决冲突的方法是沿其他分支删除叶节点。因此,当您将(r4a+r3b+r3c)组合成一个合并文档时,您将替换r4a并删除r3b和r3c。
,--> r2a -> r3a -> r4a -> r5a
r1 --> r2b -> r3b -> (r4b deleted)
`--> r2c -> r3c -> (r4c deleted)
注意,r4b和r4c仍然作为叶节点存在于历史树中,但作为已删除的文档。您可以检索它们,但它们将被标记 "_deleted":true
.
压缩数据库时,所有非叶文档的主体都将被丢弃。但是,将保留历史_rev的列表,以便以后解决冲突,以防将来遇到数据库的任何旧副本。有“修订删减”来阻止它变得任意大。
2.3.4. 处理冲突文档¶
基本 :get:`/{{doc}}/{{docid}}` 操作不会向您显示任何有关冲突的信息。您只看到确定的获胜者,而不知道是否存在其他冲突修订:
{
"_id":"test",
"_rev":"2-b91bb807b4685080c6a651115ff558f5",
"hello":"bar"
}
如果你这么做的话 GET /db/test?conflicts=true
,并且文档处于冲突状态,则您将获得赢家加上一个包含其他冲突修订的rev数组的u conflicts成员。然后可以使用 GET /db/test?rev=xxxx
操作:
{
"_id":"test",
"_rev":"2-b91bb807b4685080c6a651115ff558f5",
"hello":"bar",
"_conflicts":[
"2-65db2a11b5172bf928e3bcf59f728970",
"2-5bc3c6319edf62d4c624277fdd0ae191"
]
}
如果你这么做的话 GET /db/test?open_revs=all
然后您将获得修订树的所有叶节点。这将提供所有当前冲突,但也将提供已删除的叶节点(即冲突历史中已解决的部分)。您可以通过使用筛选出文档来删除这些 "_deleted":true
:
[
{"ok":{"_id":"test","_rev":"2-5bc3c6319edf62d4c624277fdd0ae191","hello":"foo"}},
{"ok":{"_id":"test","_rev":"2-65db2a11b5172bf928e3bcf59f728970","hello":"baz"}},
{"ok":{"_id":"test","_rev":"2-b91bb807b4685080c6a651115ff558f5","hello":"bar"}}
]
这个 "ok"
标签是 open_revs
,它还允许您将显式修订作为JSON数组列出,例如。 open_revs=[rev1,rev2,rev3]
. 在这种形式下,可以请求一个现在丢失的修订,因为数据库已经被压缩了。
注解
返回的修订顺序 open_revs=all
是 NOT 与“获胜”相关的“确定性算法”。在上面的例子中,获胜的版本是2-b91b。。。最后返回,但在其他情况下,它可以在不同的位置返回。
一旦检索到所有冲突的修订,应用程序就可以选择将它们全部显示给用户。或者它可以尝试合并它们,写回合并的版本,并删除冲突的版本,也就是说,永久地解决冲突。
如上所述,您需要更新一个修订并显式删除所有冲突的修订。这可以用一个 POST to _ 批量文档,设置 ``"_deleted":true
你想删除的那些修订。
2.3.5. 多文档API¶
2.3.5.1. 查找与芒果冲突的文档¶
2.2.0 新版功能.
CouchDB的 Mango system 允许轻松查询有冲突的文档,并返回每个文档的完整正文。
以下是如何使用它查找数据库中的所有冲突:
$ curl -X POST http://127.0.0.1/dbname/_find \
-d '{"selector": {"_conflicts": { "$exists": true}}, "conflicts": true}' \
-Hcontent-type:application/json
{"docs": [
{"_id":"doc","_rev":"1-3975759ccff3842adf690a5c10caee42","a":2,"_conflicts":["1-23202479633c2b380f79507a776743d5"]}
],
"bookmark": "g1AAAABheJzLYWBgYMpgSmHgKy5JLCrJTq2MT8lPzkzJBYozA1kgKQ6YVA5QkBFMgKSVDHWNjI0MjEzMLc2MjZONkowtDNLMLU0NzBPNzc3MTYxTTLOysgCY2ReV"}
这个 bookmark
值可用于在结果的其他页面中导航(如有必要)。默认情况下,每个请求只返回25个结果。
如果您希望经常运行此查询,请确保创建芒果辅助索引以加快查询速度:
$ curl -X POST http://127.0.0.1/dbname/_index \
-d '{"index":{"fields": ["_conflicts"]}}' \
-Hcontent-type:application/json
当然,选择器可以被增强以在文档中的其他键上过滤文档。请确保将这些键也添加到辅助索引中,否则将触发完整的数据库扫描。
2.3.5.2. 使用查找冲突文档 _all_docs
指数¶
使用可以同时获取多个文档 include_docs=true
看风景。但是,一个 conflicts=true
请求被忽略;值的“doc”部分从不包含 _conflicts
会员。因此,您需要执行另一个查询来确定每个文档是否处于冲突状态:
$ curl 'http://127.0.0.1:5984/conflict_test/_all_docs?include_docs=true&conflicts=true'
{
"total_rows":1,
"offset":0,
"rows":[
{
"id":"test",
"key":"test",
"value":{"rev":"2-b91bb807b4685080c6a651115ff558f5"},
"doc":{
"_id":"test",
"_rev":"2-b91bb807b4685080c6a651115ff558f5",
"hello":"bar"
}
}
]
}
$ curl 'http://127.0.0.1:5984/conflict_test/test?conflicts=true'
{
"_id":"test",
"_rev":"2-b91bb807b4685080c6a651115ff558f5",
"hello":"bar",
"_conflicts":[
"2-65db2a11b5172bf928e3bcf59f728970",
"2-5bc3c6319edf62d4c624277fdd0ae191"
]
}
2.3.6. 查看地图功能¶
视图只能获得文档的获胜修订。但是他们也得到了 _conflicts
成员,如果有任何冲突的修订。这意味着您可以编写一个视图,该视图的任务是专门查找有冲突的文档。下面是一个实现此功能的简单映射函数:
function(doc) {
if (doc._conflicts) {
emit(null, [doc._rev].concat(doc._conflicts));
}
}
其输出如下:
{
"total_rows":1,
"offset":0,
"rows":[
{
"id":"test",
"key":null,
"value":[
"2-b91bb807b4685080c6a651115ff558f5",
"2-65db2a11b5172bf928e3bcf59f728970",
"2-5bc3c6319edf62d4c624277fdd0ae191"
]
}
]
}
“如果你有一个单独的文档扫描程序,你可以定期扫描这些文档,这样可以解决冲突。”。
这是一个简单的解决窗口和窗口之间的冲突的方法。从用户的角度来看,这可能是他们刚刚成功保存的文档可能会突然丢失其更改,只是过了一段时间后才会恢复。这可能是可以接受的,也可能是不可接受的。
此外,很容易忘记启动清扫器,或没有正确执行,这将引入难以追踪的奇怪行为。
CouchDB的“获胜”修正算法可能意味着信息会从视图中消失,直到冲突得到解决。再考虑一下Bob的名片;假设Alice有一个显示手机号码的视图,这样她的电话应用程序就可以根据来电者ID显示来电者的姓名。如果与Bob的新旧手机号码有冲突的文档,并且这些文档碰巧以Bob的旧号码为准得到了解决,则视图将无法识别他的新的。在这种特殊情况下,应用程序可能更倾向于将两个冲突文档中的信息放入视图中,但目前不可能这样做。
获取具有冲突解决方案的文档的建议算法:
- 通过获取文档
GET docid?conflicts=true
请求 - 中的每个成员
_conflicts
数组调用GET docid?rev=xxx
. 如果在此阶段出现任何错误,请从步骤1重新启动。(可能有一场比赛中,其他人已经解决了这场冲突,并删除了该版本) - 执行特定于应用程序的合并
- 写
_bulk_docs
更新了第一个版本,删除了其他版本。
这可以在每次读取时完成(在这种情况下,您可以用对执行上述操作的库的调用替换所有要在应用程序中获取的调用),也可以作为清除程序代码的一部分。
下面是一个在Ruby中使用低级 RestClient :
require 'rubygems'
require 'rest_client'
require 'json'
DB="http://127.0.0.1:5984/conflict_test"
# Write multiple documents
def writem(docs)
JSON.parse(RestClient.post("#{DB}/_bulk_docs", {
"docs" => docs,
}.to_json))
end
# Write one document, return the rev
def write1(doc, id=nil, rev=nil)
doc['_id'] = id if id
doc['_rev'] = rev if rev
writem([doc]).first['rev']
end
# Read a document, return *all* revs
def read1(id)
retries = 0
loop do
# FIXME: escape id
res = [JSON.parse(RestClient.get("#{DB}/#{id}?conflicts=true"))]
if revs = res.first.delete('_conflicts')
begin
revs.each do |rev|
res << JSON.parse(RestClient.get("#{DB}/#{id}?rev=#{rev}"))
end
rescue
retries += 1
raise if retries >= 5
next
end
end
return res
end
end
# Create DB
RestClient.delete DB rescue nil
RestClient.put DB, {}.to_json
# Write a document
rev1 = write1({"hello"=>"xxx"},"test")
p read1("test")
# Make three conflicting versions
write1({"hello"=>"foo"},"test",rev1)
write1({"hello"=>"bar"},"test",rev1)
write1({"hello"=>"baz"},"test",rev1)
res = read1("test")
p res
# Now let's replace these three with one
res.first['hello'] = "foo+bar+baz"
res.each_with_index do |r,i|
unless i == 0
r.replace({'_id'=>r['_id'], '_rev'=>r['_rev'], '_deleted'=>true})
end
end
writem(res)
p read1("test")
以这种方式编写的应用程序从不需要处理 PUT 409
,并自动支持多主机。
当你知道自己在做什么的时候,你会发现这很简单。只是CouchDB目前还没有为“获取所有冲突的修订”提供方便的httpapi,也没有“放置以取代这些N个修订”,所以您需要自己包装这些修订。在撰写本文时,还没有已知的客户端库提供支持。
2.3.7. 合并和修订历史¶
实际执行合并是一个特定于应用程序的功能。这取决于数据的结构。有时这很简单:例如,如果一个文档包含一个只被附加到后面的列表,那么您可以执行两个列表版本的联合。
一些合并策略会查看对对象所做的更改,与以前的版本相比。这就是Git的merge函数的工作原理。
例如,要合并Bob的名片版本v2a和v2b,可以查看v1和v2b之间的差异,然后将这些更改应用到v2a。
使用CouchDB,您有时可以获得文档的旧版本。例如,如果 /db/bob?rev=v2b&revs_info=true
您将得到一个以前的修订ID的列表,这些修订ID以修订版v2b结束。但是,如果数据库被压缩,该文件修订版的内容将丢失。 revs_info
仍将显示v1是祖先,但报告为“缺少”:
BEFORE COMPACTION AFTER COMPACTION
,-> v2a v2a
v1
`-> v2b v2b
因此,如果您想使用diff,建议的方法是将这些diff存储在新修订本身中。也就是说:当你用v2a替换v1时,在v2a中包含一个额外的字段或附件,说明哪些字段从v1更改为v2a。不幸的是,这意味着您的应用程序需要额外的簿记。
2.3.8. 与其他复制数据存储的比较¶
其他复制系统也会出现同样的问题,因此研究一下这些系统,看看它们与CouchDB的比较是有启发性的。请随时添加其他示例。
2.3.8.1. 统一¶
Unison 是一个双向文件同步工具。在这种情况下,名片就是一个文件,比如说 bob.vcf .
运行unison时,更改会双向传播。如果文件的一侧发生了更改,而另一侧没有更改,则新文件将替换旧文件。Unison维护一个本地状态文件,以便知道自上次成功复制后文件是否已更改。
在我们的例子中,两边都发生了变化。只调用了一个文件 bob.vcf 可以存在于文件系统中。Unison通过简单地回避来解决这个问题:用户可以选择用本地版本替换远程版本,或者反之亦然(两者都会丢失数据),但默认操作是保持双方不变。
在爱丽丝看来,至少这是一个简单的解决办法。每当她在桌面上时,她都会在桌面上看到她上次编辑的版本,每当她在笔记本电脑上时,她都会看到她上次编辑的版本。
但由于实际上没有进行复制,所以数据没有得到保护。如果她的笔记本电脑硬盘坏了,她会丢失她在笔记本电脑上所做的所有更改;如果她的台式机硬盘坏了,也是一样。
由她自己手动复制其中一个版本(使用不同的文件名),合并两个版本,最后将合并后的版本推到另一个版本。
还请注意,原始文件(版本v1)此时已丢失。不管是最新版本还是最新版本的最新版本。爱丽丝必须记住她最后进的是哪一个。
2.3.8.2. 吉特¶
Git 是一个著名的分布式源代码管理系统。和Unison一样,Git处理文件。然而,Git将整个文件集的状态视为单个对象,即“树”。每当您保存更新时,您都会创建一个“提交”,它既指向更新的树,也指向上一个提交,后者又指向上一个树。因此,您拥有所有文件状态的完整历史记录。这个历史记录形成了一个分支,并且有一个指针指向分支的顶端,您可以从该分支向后工作到任何以前的状态。“指针”是tip提交的SHA1散列。
如果要使用一个或多个对等方进行复制,则会为每个对等方创建一个单独的分支。例如,您可能有:
main -- my local branch
remotes/foo/main -- branch on peer 'foo'
remotes/bar/main -- branch on peer 'bar'
在常规工作流中,复制是一种“拉动”,将更改从远程对等方导入到本地存储库中。“拉”有两个作用:首先将对等方的状态“获取”到该对等方的远程跟踪分支中;然后尝试将这些更改“合并”到本地分支中。
现在让我们考虑一下名片。Alice创建了一个Git回购,其中包含 bob.vcf
,并将其复制到另一台计算机上。树枝看起来像这样,哪里 AAAAAAAA
是提交的SHA1::
---------- desktop ---------- ---------- laptop ----------
main: AAAAAAAA main: AAAAAAAA
remotes/laptop/main: AAAAAAAA remotes/desktop/main: AAAAAAAA
现在她在桌面上做了一个更改,并将其提交到桌面回购;然后她在笔记本电脑上进行了一个不同的更改,并将其提交到笔记本电脑回购:
---------- desktop ---------- ---------- laptop ----------
main: BBBBBBBB main: CCCCCCCC
remotes/laptop/main: AAAAAAAA remotes/desktop/main: AAAAAAAA
现在在桌面上她做了 git pull laptop
. 首先,将远程对象复制到本地repo中,并更新远程跟踪分支:
---------- desktop ---------- ---------- laptop ----------
main: BBBBBBBB main: CCCCCCCC
remotes/laptop/main: CCCCCCCC remotes/desktop/main: AAAAAAAA
注解
回购仍然包含AAAAAAAA,因为commits BBBBBBBB和CCCCCCCC指向它。
然后Git将尝试合并中的更改。知道父母承诺 CCCCCCCC
是 AAAAAAAA
,这需要一个区别 AAAAAAAA
和 CCCCCCCC
并试图将其应用于 BBBBBBBB
.
如果成功,那么您将获得一个带有合并提交的新版本:
---------- desktop ---------- ---------- laptop ----------
main: DDDDDDDD main: CCCCCCCC
remotes/laptop/main: CCCCCCCC remotes/desktop/main: AAAAAAAA
然后爱丽丝必须登录到笔记本电脑上运行 git pull desktop
. 类似的过程也会发生。远程跟踪分支已更新:
---------- desktop ---------- ---------- laptop ----------
main: DDDDDDDD main: CCCCCCCC
remotes/laptop/main: CCCCCCCC remotes/desktop/main: DDDDDDDD
然后合并发生。这是一个特例: CCCCCCCC
是的父提交之一 DDDDDDDD
,所以笔记本电脑可以 fast forward 更新自 CCCCCCCC
到 DDDDDDDD
不需要进行任何复杂的合并。最终状态为:
---------- desktop ---------- ---------- laptop ----------
main: DDDDDDDD main: DDDDDDDD
remotes/laptop/main: CCCCCCCC remotes/desktop/main: DDDDDDDD
现在这一切都很好,但是在考虑CouchDB时,您可能会想知道这是如何相关的。
首先,注意在合并算法失败的情况下会发生什么。这些更改仍然会从远程repo传播到本地repo,并在远程跟踪分支中可用。因此,与Unison不同,您知道数据是受保护的。只是本地工作副本可能无法更新,或者可能与远程版本不同。由您自己创建和提交组合版本,但您可以保证拥有完成此操作所需的所有历史记录。
请注意,虽然可以在Git中构建新的合并算法,但标准算法的重点是对源代码进行基于行的更改。如果XML或JSON没有任何换行符,它们就不能很好地用于XML或JSON。
另一个有趣的考虑是多个对等点。在这种情况下,您有多个远程跟踪分支,其中一些可能与您的本地分支匹配,有些可能在您后面,有些可能在您前面(即,包含尚未合并的更改)::
main: AAAAAAAA
remotes/foo/main: BBBBBBBB
remotes/bar/main: CCCCCCCC
remotes/baz/main: AAAAAAAA
请注意,每个对等点都是显式跟踪的,因此必须显式地创建。如果对等节点过时或不再需要,则由您从配置中删除它并删除远程跟踪分支。这与CouchDB不同,CouchDB不在数据库中保留任何对等状态。
CouchDB和Git之间的另一个区别是,它将所有的历史记录保持为零——Git压缩保持所有版本之间的差异,以减小大小,但是CouchDB会丢弃它们。如果您不断更新文档,Git回购的规模将永远增长。使用“历史重写”使Git忘记比特定提交更早的提交是可能的(需要一些努力)。
2.3.8.2.1. CouchDB复制协议是什么?像吉特吗?¶
作者: | 杰森-史密斯 |
---|---|
日期: | 2011-01-29 |
来源: | StackOverflow |
关键点
如果你了解Git,那么你就知道Couch复制是如何工作的。 复制是 very 类似于用Git这样的分布式源代码管理器推或拉。
CouchDB复制没有自己的协议。 复制器只是作为客户机连接到两个数据库,然后从一个数据库读取数据,然后向另一个数据库写入数据。推式复制读取本地数据并更新远程数据库;拉式复制则相反。
- 有趣的事实1 :replicator实际上是一个独立的Erlang应用程序,在它自己的进程中。它连接到两个沙发上,然后从一个沙发读取记录并将它们写入另一个。
- 有趣的事实2 :CouchDB无法知道谁是普通客户机,谁是复制器(更不用说复制是push还是pull)。看起来都像是客户端连接。他们中的一些人读记录。有些人写记录。
一切都来自数据模型
复制算法是琐碎的,无趣的。训练有素的猴子可以设计它。这很简单,因为聪明的数据模型具有以下有用的特性:
- CouchDB中的每个记录都完全独立于所有其他记录。如果你想做一个连接或者一个事务,这很糟糕,但是如果你想写一个复制器,那就太棒了。只需找出如何复制一个记录,然后对每个记录重复该操作。
- 和Git一样,记录也有一个链表修订历史记录。记录的修订ID是其自身数据的校验和。后续修订ID是以下内容的校验和:新数据加上上上一个修订ID。
- 除了应用程序数据 (
{{"name": "Jason", "awesome": true}}
),每个记录都存储了所有之前的修订ID的进化时间线。- 练习:静下心来思考一下。考虑任何两个不同的记录,A和B。如果A的修订ID出现在B的时间线上,那么B肯定是从A演变而来的。现在考虑Git的快进合并。你听到了吗?那是你心灵被吹的声音。
- Git并不是一个真正的线性列表。当一个父对象有多个子对象时,它有fork。CouchDB也有。
- 练习:比较A和B两个不同的记录。A的修订ID不会出现在B的时间线上;但是,A和B的时间线上都有一个修订ID C。因此,A不是从B演化而来的,B也不是从A进化而来的,而是A和B有一个共同的祖先C。在Git中,这是一个“分叉”。在CouchDB中,这是一个“冲突”
- 在Git中,如果两个孩子继续独立地发展他们的时间线,那就很酷了。福克斯完全支持这一点。
- 在CouchDB中,如果两个孩子继续独立地发展他们的时间线,那也很酷。冲突完全支持这一点。
- 有趣的事实3 :CouchDB“conflicts”与Git“conflicts”不对应。CouchDB冲突是一个不同的修订历史,Git称之为“fork”。因此CouchDB社区以沉默的方式宣布“conflict” n :“合影。”
- Git也有合并,当一个子代有多个父代时。库奇达 sort 也有。
- 在数据模型中,没有合并。 客户机只需将一条时间线标记为已删除,并继续使用唯一现存的时间线。
- 在应用程序中,感觉像是合并。 通常,客户端合并 data 以特定于应用程序的方式。然后它将新数据写入时间线。在Git中,这类似于将更改从分支A复制粘贴到分支B,然后提交到分支B并删除分支A git merge .
- 这些行为是不同的,因为在Git中,时间线本身很重要;但是在CouchDB中,数据很重要,时间线是偶然的——它只是用来支持复制的。这就是为什么CouchDB的内置修订不适合像wiki页面那样存储修订数据的原因之一。
最后的注释
在这篇文章中至少有一个句子是完整的BS。