MongoDB: 解决竞争条件的策略

4

我们有一个需要在站点模型下存储多个数据源的场景,具体如下:

{
  id: site_id
  name: site_name
  feeds: [
    {
      url: feed_url_1
      date: feed_update_date_1
    },
    {
      url: feed_url_2
      date: feed_update_date_2
    },
    ...
  ]
}

由于feeds是一个数组,因此我们可以使用$set$push$addToSet来更新它。

当我们的并发应用程序(队列)尝试更新相同的站点模型时,可能会发生2种不同的竞争条件(写偏斜)。

如果我们选择$set,并在客户端上保护重复项,则如果2个队列正在写入同一站点,则一个feed可能会丢失,具体如下:

Given a wordpress site, extract 2 feeds (RSS and ATOM), dispatch to Q1 and Q2.
Q1: load existing feed, check RSS feed is new
Q2: load existing feed, check ATOM feed is new
Q1: $set feeds => [RSS]
Q2: $set feeds => [ATOM]

现在RSS源已经丢失。

如果我们选择$push$addToSet,则可能会发生以下情况。

User A added a site, putting RSS feed to Q1
User B added the same site, putting the same RSS feed to Q2
Q1: load existing feed, check RSS feed is new
Q2: load existing feed, check RSS feed is new
Q1: $push RSS
Q2: $push RSS

现在RSS提要已经被复制了。
如果我们的数据模型只是{url},那么$addToSet将保护免受重复提要的影响。但不幸的是,情况并非如此,date属性可能不同。所以$addToSet并没有比$push更安全。
我们考虑了一些可能的解决方法,但是考虑到我们紧张的时间表,都不是很好。
1.将提要从网站中分离出来成为一个独立的集合,仅用url进行保护,并相应地更改我们的模型和存储库。
2.首先向网站模型插入部分{url},然后使用其他信息更新它们,这应该可以使$addToSet可用,但可能会破坏需要始终存在日期的其他队列(需要测试)。
3.让竞争条件发生,先$push提要,然后使用后台队列检测重复项并稍后删除它们。
(如果我所知道的MongoDB v2.4还没有位置查询的upsert,那么可能有第4个解决方案)
因此,我想知道是否有更好的替代方案来解决这种竞争条件。或者是否有一些最佳实践。

你很可能在错误地使用存储。对于基于文档的数据库,您必须将唯一性信息作为值的组成部分存储,以避免重复发生。因此,在您的示例中,您可以为RSS和ATOM各保留一个子文档。您必须跳出思维定势,将您认为是竞争条件的问题转化为处理不一致和最终重复数据的标准工作。 - hakre
@hakre 谢谢,如果是这样的话,我们需要为每个供稿URL生成某种缩写,因为可能会有多个RSS/ATOM供稿。不幸的是,这确实需要对模型进行相当大的改变,但我同意这种方法是有道理的。 - bitinn
@hakre 其实我希望可以再次确认您是否建议将“feeds”从数组转换为对象(或拆分为单独的属性)。这种方法需要放宽我们的验证,它通过列名进行保护,就像传统的关系型数据库验证一样(此外,我们可能需要处理多个ATOM和1个RSS源)。否则,我看不到如何避免使用数组时出现写入偏差。 - bitinn
如果你像传统的关系型数据库一样保护某些东西,那么你正在使用错误的工具。Mongo不是关系型数据库(也不是特别好的文档数据库)。考虑选择正确的工具来完成工作,否则你最终会得到一连串的问题,这些问题可能无法以你需要的详细程度得到解答。 - hakre
@hakre 鉴于我们实际上将订阅项存储为文档,在单独的集合中,我认为这个工具非常适合我们的需求。在这种情况下,一个站点只能有几个订阅源,如果我们正确地阅读了mongodb文档,嵌入式是这种元数据的正确选项。唯一的问题是我们希望避免元数据的重复。 - bitinn
2个回答

4

您可能想要查看Tokumx,这是MongoDB的一个分支,支持事务(以及其他一些有用的功能)。


2
由于MongoDB的整个目的是在分片分布式设置中使用,因此我认为使用toku事务有点违背了使用MongoDB的初衷。 - Sammaye

3
您可以在更新选择器上使用一个“gard”:
alice(mongod-2.4.8) test> db.foo.save({_id: 12 })
Updated 1 new record(s) in 1ms
alice(mongod-2.4.8) test> db.foo.update({ _id: 12, "feeds.url" : {$ne: "baz"} }, 
    { $push :     { feeds : { url: "baz" } } } )
Updated 1 existing record(s) in 1ms
alice(mongod-2.4.8) test> db.foo.update({ _id: 12, "feeds.url" : {$ne: "baz"} },
    { $push : { feeds : { url: "baz" } } } )
Updated 0 record(s) in 1ms
alice(mongod-2.4.8) test> db.foo.find({_id: 12 })
{
    "_id": 12,
    "feeds": [
        {
            "url": "baz"
        }
    ]
}
Fetched 1 record(s) in 1ms -- Index[_id_]

看到这个,我就像:我们怎么没想到呢?这肯定是一个有效的解决方案。 - bitinn
这绝对是最好的答案,适用于MongoDB环境,一个分布式的分片数据集。 - Sammaye

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接