MongoDB,原子级操作

3
我希望你能翻译与MongoDB中的findAndModify相关的一些信息。据我所知,查询是“由文档隔离”的。
这意味着如果我运行两个类似于以下方式的findAndModify:
{a:1},{set:{status:"processing", engine:1}}
{a:1},{set:{status:"processing", engine:2}}

如果这个查询可能会影响到2,000个文档,因为有两个查询(2个引擎),那么可能会有一些文档将有"engine:1"和其他一些文档将有"engine:2"。
我不认为findAndModify会隔离“第一个查询”。 为了隔离第一个查询,我需要使用$isolated。
我写的一切都对吗?
更新-场景
想要编写一个邻近引擎。 集合User有1000-2000-3000个用户,或者有数百万个。
1-按最接近点“lng,lat”排序 2-在NodeJS中进行一些计算,这是在MongoDB中无法完成的 3-现在我将把用户分组为“UserGroup”,并编写批量更新
当我有2000-3000个用户时,这个过程(从1到3)需要时间。 所以我想要有多个线程并行处理。

并行线程意味着并行查询。这可能是一个问题,因为查询3可能会占用查询1的一些用户。如果发生这种情况,在点(2)我就不会得到最近的用户,而是得到了最近的“针对此查询”的用户,因为另一个查询可能已经占用了其余的用户。这可能导致纽约的一些用户与洛杉矶的用户分组。

更新2-场景

我有一个像这样的集合:

{location:[lng,lat], name:"1",gender:"m", status:'undone'}
{location:[lng,lat], name:"2",gender:"m", status:'undone'}
{location:[lng,lat], name:"3",gender:"f", status:'undone'}
{location:[lng,lat], name:"4",gender:"f", status:'done'}

我应该能够做的是,通过最接近的用户分组来创建用户“组”。每个组都有1男性+1女性。在上面的例子中,我期望只有一个组(user1+user3),因为它们是男女搭配且非常接近(user-2也是男性,但离User-3很远,而user-4也是女性,但已经处理完毕)。
现在组已经创建(仅有1个组),因此这两个用户被标记为“完成”,另一个用户-2被标记为“未完成”以供将来操作。
我希望能够快速管理1000-2000-3000个用户。

更新3:社区反馈 好的,现在让我来总结一下您的情况。根据您的数据,您想要将男性和女性条目配对,基于它们彼此之间的接近程度。假设您不想进行每个可能的匹配,而只是设置一份普通的“推荐”列表,每个用户最多10个,按照最近的位置排序。现在我必须很傻才看不出这个问题的全面方向,但这是否概括了基本的初始问题陈述。处理每个用户,找到他们的“配对”,一旦配对标记为“完成”,则通过组合将它们从其他配对中排除?


实际上,这听起来像是您想要“有序”的批量更新。但是您的实际结构(假设是循环)在这里并不是非常清晰。 - Blakes Seven
1
你为什么要在这两个操作之间紧密地运行它们?你基本上是在快速连续地运行一个查询来设置引擎1,然后又运行另一个查询来设置引擎2,不确定为什么这样做。 - Sammaye
Sammaye,我有许多名为“engine”的Node.js进程,数量为“n”。每个引擎将负责查询。如果两个引擎同时进行相同的查询,我不知道会发生什么,但这是可能的。此外,我的更新将包含GeoNear,因此我需要避免2个引擎分割GeoNear文档。 - Daniele Tassone
@Dada。看到那里的@了吗?这在这里被称为标记。如果你想和某人交谈,就像我所做的那样。你能否编辑你的问题,因为它没有任何意义。你需要在你的问题中清楚地陈述你的情况,而不是在你的评论中。停止考虑$isolated,开始说出你真正需要做的事情。在这里不能更清楚,$isolated 不是 你想要的。 - Blakes Seven
1
@BlakesSeven 我不知道怎么按照你的要求写。但是我很感激你的时间。 - Daniele Tassone
显示剩余12条评论
1个回答

2

这是一个非常棘手的问题,不容易解决。

首先,迭代方法(我最初使用的方法)可能会导致错误的结果。

假设我们有以下文档:

{
   _id: "A",
   gender: "m",
   location: { longitude: 0, latitude: 1 }
 }

 {
   _id: "B",
   gender: "f",
   location: { longitude: 0, latitude: 3 }
 }

 {
   _id: "C",
   gender: "m",
   location: { longitude: 0, latitude: 4 }
 }

 {
   _id: "D",
   gender: "f",
   location: { longitude: 0, latitude: 9 }
 }

采用迭代方法,我们现在从“A”开始计算最接近的女性,当然是距离为2的“B”。但事实上,男性和女性之间最接近的距离应该是1(从“B”到“C”的距离)。但即使我们找到这个距离,另一个匹配对“A”和“D”之间的距离仍然是8,而以前的解决方案中,“A”只需与“B”保持距离2。
因此,我们需要决定采取哪种方式:
  1. 天真地迭代文档
  2. 查找匹配个体之间距离之和最小的方法(本身并不容易解决),以便所有参与者一起旅行的距离最短。
  3. 仅匹配距离可接受的参与者
  4. 采用某种分治策略,在共同地标(例如城市)半径范围内匹配参与者

解决方案1:天真地迭代文档

var users = db.collection.find(yourQueryToFindThe1000users);

// We can safely use an unordered op here,
// which has greater performance.
// Since we use the "done" array do keep track of
// the processed members, there is no drawback.
var pairs = db.pairs.initializeUnorderedBulkOp();

var done = new Array();

users.forEach(
  function(currentUser){

     if( done.indexOf(currentUser._id) == -1 ) { return; }

     var genderToLookFor = ( currentUser.gender === "m" ) ? "f" : "m";

     // using the $near operator,
     // the returned documents automatically are sorted from nearest
     // to farest, and since findAndModify returns only one document
     // we get the closest matching partner.
     var nearPartner = db.collection.findAndModify(
       query: {
         status: "undone",
         gender: genderToLookFor,
         $near: {
           $geometry: {
             type: "Point" ,
             coordinates: currentUser.location
           }
         }
       },
       update: { $set: { "status":"done" } },
       fields: { _id: 1}
     );

     // Obviously, the current use already is processed.
     // However, we store it for simplifying the process of
     // setting the processed users to done.
     done.push(currentUser._id, nearPartner._id);

     // We have a pair, so we store it in a bulk operation
     pairs.insert({
       _id:{
         a: currentUser._id,
         b: nearPartner._id
       }
     });

  }
)

// Write the found pairs
pairs.execute();

// Mark all that are unmarked by now as done
db.collection.update(
  {
    _id: { $in: done },
    status: "undone"
  },
  {
    $set: { status: "done" }
  },
  { multi: true }
)

解决方案2:寻找比赛之间距离最小的总和

这将是理想的解决方案,但解决起来非常复杂。我们需要计算一个性别的所有成员与另一个性别的所有成员之间的所有距离,并迭代所有可能的匹配集。在我们的示例中,由于给定性别只有4种组合,因此这相当简单。再想一想,这可能至少是旅行推销员问题的一个变体(MTSP?)。如果我对此正确,那么组合数应该为

number of combinations 对于所有n>2,其中n是可能的配对数量。

因此

combinations for n=10 对于n=10

以及令人惊讶的

combinations for n=25 对于n=25

这是7.755千亿(长量级)或7.755万亿(短量级)。 虽然有解决这种问题的方法,但世界纪录在使用大量硬件和相当棘手的算法时在25000个节点范围内。我认为对于所有实际目的,这个“解决方案”可以被排除。

解决方案3

为了防止人们之间的距离不可接受并根据您的用例,您可能希望根据他们到公共地标的距离(例如他们要见面的下一个大城市)来匹配人员。

对于我们的示例,假设我们有位于[0,2]和[0,7]的城市。因此,城市之间的距离(5)必须是我们接受的匹配范围。因此,我们为每个城市进行一次查询。

db.collection.find({
 $near: {
   $geometry: {
     type: "Point" ,
     coordinates: [ 2 , 0 ]
   },
   $maxDistance: 5
 }, status: "done"
})

直接遍历结果可能会导致问题。由于"A"和"B"将是结果集中的第一项,它们将匹配并完成。对于"C"来说很不幸,没有女孩留给他。但是当我们对第二个城市进行相同的查询时,他就有了第二次机会。好吧,他的旅行变得有点长,但是嘿,他和"D"约会了!

为了找到相应的距离,请选择一组固定的城市(城镇、大都市区或任何您所在地区的规模),按位置排序,并将每个城市的半径设置为其直接邻居之间的两个距离中较大的那个。这样,您就可以获得重叠的区域。因此,即使在一个地方找不到匹配项,也可能在其他地方找到。

我记得,Google Maps允许根据其大小获取国家的城市。更简单的方法是让人们选择他们所在的城市。

注释

  1. 所示代码不适用于生产环境,需要进行改进。
  2. 建议使用1和0代替“m”和“f”表示性别:仍然可以轻松映射,但需要更少的空间来保存。
  3. 状态也是如此。
  4. 我认为最后的解决方案是最佳的,可以优化距离,并保持匹配的机会高。

1
我不想使用线程,而是为了简单起见而使用作业队列,并在查找匹配的合作伙伴时使用MongoDB的findAndModify将状态设置为“完成”,并返回新文档,实现一个原子操作。问题解决了。 - Markus W Mahlberg
这样,我也可以使用具有多个虚拟机的作业队列。我只需要使用一些“machineId”来“设置”JobQueue,当它完成后,下一个作业队列就会被触发(可能是相同的机器或其他机器)。你认为呢? - Daniele Tassone
@Dada 在我看来,KISS 应该是你的首要关注点。 - Markus W Mahlberg
你说的KISS是什么意思? - Daniele Tassone
我在思考使用“KISS.js”框架,所以我要求您更清楚地解释一下。与保持简单愚蠢有关,我不知道如何受益,因为我的问题非常具体,关于作业队列是否也可以是同步更多虚拟机的解决方案,我认为可以。 - Daniele Tassone
显示剩余2条评论

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