我有一个使用mongoDB作为后端的分布式应用程序。该应用程序有两个集合(C1和C2)与M:1的关系,所以如果我删除C1中的文档,我需要搜索C1以查找指向C2中相同文档的任何其他文档,如果没有匹配,然后删除C2中的相关文档。
这显然存在竞争条件的问题,可能会将新文档插入C1,而搜索正在进行C2中即将删除的文档,导致数据库不一致。删除可以被延迟,以便它们可以被批量处理并且每周执行一次,比如在低负载期间,所以我正在考虑为mongo编写分布式锁定系统来解决RC问题。
问题:
更新
我把它留下来以避免混淆这个问题,但我现在需要把它包括在内。实际上有另一个资源(R)(本质上是远程磁盘上的文件)需要与C2文档和C2一起删除:R是M:1。 R完全在mongodb生态系统之外,这就是为什么我的脑子跳到锁定应用程序,所以我可以安全地删除所有这些东西。因此,下面提到的反转链接的想法不适用于这种情况。是的,系统很复杂,不,它不能改变。
UPDATE2
我试图抽象出实施细节以保持问题简洁,一直咬我。另一个细节:R通过REST调用操作到另一个服务器。
答案 0 :(得分:2)
1。 此类问题通常通过嵌入来解决。因此,基本上C1和C2可以是单个集合,而C2 doc将自己嵌入到C1中。显然,这并不总是可行或可取的,其中一个缺点是数据重复。另一个缺点是,如果不通过所有C1并且给定M:1关系,你将无法找到所有C2,这并不总是好事。所以这取决于这些缺点是否是您申请中的真正问题。
2。 处理它的另一种方法是只删除从C1到C2的链接,从而使C2文档不存在任何链接。在某些情况下,这可能会有很低的成本。
3。 使用两阶段提交,类似于此处所述:http://docs.mongodb.org/manual/tutorial/perform-two-phase-commits/。
4。
另一种选择可能是反转你的链接。 C2将有一个指向C1s的链接数组。每次从该阵列中删除C1 $pull
时,指向已删除C1的链接。在您从C2中删除条件后,链接数组为空,并且_id
是您从更新中返回的条件。如果在将新文档插入C1并尝试更新C2时发生竞争条件,并且您返回的结果是您没有更新任何内容,那么您可以使插入失败或尝试插入新的C2。这是一个例子:
// Insert first doc
db.many.insert({"name": "A"});
// Find it to get an ID to show.
db.many.find();
{ "_id" : ObjectId("52eaf9e05a07ef0270a9eccc"), "name" : "A" }
// lets add a tag to it right after
db.one.update({"tag": "favorite"}, {$addToSet: {"links": ObjectId("52eaf9e05a07ef0270a9eccc")}}, {upsert: true, multi: false});
// show that tag was created and a link was added
db.one.find();
{ "_id" : ObjectId("52eafaa77365653791085540"), "links" : [ ObjectId("52eaf9e05a07ef0270a9eccc") ], "tag" : "favorite" }
// Insert one more doc which will not be tagged just for illustration
db.many.insert({"name": "B"});
// Insert last document, show IDs of all docs and tag the last document inserted:
db.many.insert({"name": "C"});
db.many.find();
{ "_id" : ObjectId("52eaf9e05a07ef0270a9eccc"), "name" : "A" }
{ "_id" : ObjectId("52eafab95a07ef0270a9eccd"), "name" : "B" }
{ "_id" : ObjectId("52eafac85a07ef0270a9ecce"), "name" : "C" }
db.one.update({"tag": "favorite"}, {$addToSet: {"links": ObjectId("52eafac85a07ef0270a9ecce")}}, {upsert: true, multi: false});
// Now we have 2 documents tagged out of 3
db.one.find();
{ "_id" : ObjectId("52eafaa77365653791085540"), "links" : [ ObjectId("52eaf9e05a07ef0270a9eccc"), ObjectId("52eafac85a07ef0270a9ecce") ], "tag" : "favorite" }
// START DELETE PROCEDURE
// Let's delete first tagged document
db.many.remove({"_id" : ObjectId("52eaf9e05a07ef0270a9eccc")});
// remove the "dead" link
db.one.update({"tag": "favorite"}, {$pull: {"links": ObjectId("52eaf9e05a07ef0270a9eccc")}});
// just to show how it looks now (link removed)
db.one.find();
{ "_id" : ObjectId("52eafaa77365653791085540"), "links" : [ ObjectId("52eafac85a07ef0270a9ecce") ], "tag" : "favorite" }
// try to delete a document that has no links - it's not the case here yet, so the doc is not deleted.
db.one.remove({"tag" : "favorite", "links": {$size: 0}});
db.one.find();
{ "_id" : ObjectId("52eafaa77365653791085540"), "links" : [ ObjectId("52eafac85a07ef0270a9ecce") ], "tag" : "favorite" }
// DELETE OF THE FIRST DOC IS COMPLETE, if any docs got added with
// links then the tag will just have more links
// DELETE LAST DOC AND DELETE UNREFERENCED LINK
db.many.remove({"_id" : ObjectId("52eafac85a07ef0270a9ecce")});
db.one.update({"tag": "favorite"}, {$pull: {"links": ObjectId("52eafac85a07ef0270a9ecce")}});
// no links are left
db.one.find();
{ "_id" : ObjectId("52eafaa77365653791085540"), "links" : [ ], "tag" : "favorite" }
db.one.remove({"tag" : "favorite", "links": {$size: 0}});
// LAST DOC WAS DELETED AND A REFERENCING DOC WAS DELETED AS WELL
// final look at remaining data
db.one.find();
// empty
db.many.find();
{ "_id" : ObjectId("52eafab95a07ef0270a9eccd"), "name" : "B" }
如果从one
删除后发生upsert,那么它只会创建一个新文档并添加一个链接。如果它发生之前,那么旧的one
doc将保留,链接将正确更新。
<强>更新强>
这是处理“删除文件”要求的一种方法。它假设您有类似于POSIX的文件系统,如ext3 / ext4,许多其他FS也具有相同的属性。对于您创建的每个C2,您应该创建一个随机命名的hard link,它指向R文件。例如,在C2文档中存储该链接的路径。最终会有多个指向单个文件的硬链接。每当您删除C2时,您都会删除此硬链接。最终当链接计数变为0时,OS将删除该文件。因此,除非删除所有硬链接,否则无法删除该文件。
反转C1-C2链接和使用FS硬链接的另一种方法是使用多阶段提交,您可以以任何方式实现。
免责声明:我描述的任何机制都应该有效,但可能包含一些我错过的案例。我自己并没有尝试过这种方法,但过去我成功地使用了类似的“事务”文件删除方案。所以我认为这样的解决方案可行,但需要进行良好的测试,并考虑所有可能的情况。
更新2
考虑到所有约束,您必须实施多阶段提交或某种锁定/事务机制。您还可以通过任务队列订购所有操作,这自然没有竞争条件(同步)。所有这些机制都会使系统慢一点,但你可以选择C2文档id的粒度级别,我认为这并不是那么糟糕。因此,您仍然可以在C2 id级别上与隔离并行运行。
一种简单实用的方法是使用消息总线/队列。
答案 1 :(得分:1)
如果您不使用分片,则可以使用TokuMX而不是MongoDB,它支持具有原子提交和回滚的多文档,多语句事务,以及其他好处。这些事务跨集合和数据库工作,因此它们似乎适用于您的应用程序而无需进行很多更改。
有一个完整的教程here。
免责声明:我是Tokutek的工程师
答案 2 :(得分:0)
阿列克,
您是否考虑过将关系移至其他收藏中。您可以拥有一个集合,将所有关系从C1映射到C2。每个文档还可以存储一个布尔值,表明它已标记为要收集。您可以编写后台任务,定期扫描此表并查找已删除的集合。该模型的优点是,当集合不同步时很容易检测到。
E.g。 { C1_ID, [C2_ID_1,C2_ID_2 ....], 真假 }