MongoDB findAndModify。它真的是原子的吗?帮助编写封闭的更新解决方案

时间:2014-10-14 07:49:51

标签: mongodb

我有Event个文档,包含嵌入式Snapshots

如果符合以下情况,我想向Snapshot添加Event A

  • 该事件在快照A的5分钟内开始
  • 事件的最新快照不超过快照A的一分钟。

否则....创建一个新的Event

以下是我的findAndUpdate查询可能更有意义:

Event.findAndModify(
  query: { 
    start_timestamp: { $gte: newSnapshot.timestamp - 5min },
    last_snapshot_timestamp: { $gte: newSnapshot.timestamp - 1min }
  },
  update: { 
    snapshots[newSnapshot.timestamp]: newSnapshot,
    $max: { last_snapshot_timestamp: newSnapshot.timestamp },
    $min: { start_timestamp: newSnapshot.timestamp }
  },
  upsert: true,
  $setOnInsert: { ALL OUR NEW EVENT FIELDS } }
)

编辑:不幸的是,我无法在start_timestamp上创建唯一索引。快照带有不同的时间戳,我想将它们分组到一个事件中。即快照A在12:00:00进入,快照B在12:00:59进入。它们应该在同一个事件中,但它们可以在不同的时间写入DB,因为编写它们的工作者同时执行。假设另一个快照进入,在12:00:30,应该写入与上述两个相同的事件。最后,应将12:02:00的快照写入新事件。

我的问题是......这将在并发环境中正常工作。 findAndUpdate是原子的吗?是否有可能创建两个事件,我应该创建一个事件,并将快照添加到它?

编辑:所以上述方法不能保证不会创建两个事件,正如@chainh所指出的那样。

所以我尝试了一种新的基于锁定的方法 - 您认为这会有效吗?

var acquireLock = function() {
  var query = { "locked": false}
  var update = { $set: { "locked": true } }
  return Lock.findAndModify({
    query: query, 
    update: update,
    upsert: true
  })
};

var releaseLock = function() {
  var query = { "locked": true }
  var update = { $set: { "locked": false } }
  return Lock.findAndModify({
    query: query, 
    update: update
  })
};

var insertSnapshot = function(newSnapshot, upsert) {
  Event.findAndModify(
    query: { 
      start_timestamp: { $gte: newSnapshot.timestamp - 5min },
      last_snapshot_timestamp: { $gte: newSnapshot.timestamp - 1min }
    },
    update: { 
      snapshots[newSnapshot.timestamp]: newSnapshot,
      $max: { last_snapshot_timestamp: newSnapshot.timestamp },
      $min: { start_timestamp: newSnapshot.timestamp }
    },
    upsert: upsert,
    $setOnInsert: { ALL OUR NEW EVENT FIELDS } }
  )
};

var safelyInsertEvent = function(snapshot) {
  return insertSnapshot(snapshot, false)
  .then(function(modifyRes) {
    if (!modifyRes.succeeded) {
      return acquireLock()
    }
  })
  .then(function(lockRes) {
    if (lockRes.succeeded) {
      return insertSnapshot(snapshot, true)
    } else {
      throw new AcquiringLockError("Didn't acquire lock. Try again")
    }
  })
  .then(function() {
    return releaseLock()
  })
  .catch(AcquiringLockError, function(err) {
    return safelyInsertEvent(snapshot)
  })
};

锁定文档只包含一个字段(已锁定)。基本上上面的代码试图找到一个现有的事件并更新它。如果它有效,那么我们可以拯救。如果我们没有更新,我们知道我们没有现成的事件来保留快照。因此我们以原子方式获取锁定,如果成功,我们可以安全地插入新事件。如果获取该锁失败,我们只需再次尝试整个过程,并希望到那时我们有一个现有的事件可以坚持下去。

2 个答案:

答案 0 :(得分:1)

findAndModify可能会在并发环境下发起多个事件。除非您的事件文档包含具有唯一索引的字段,否则只有一个findAndModify成功插入新事件,而其他findAndModify将失败并重试将快照添加到新事件。有关详细信息,请参阅此jira票证:https://jira.mongodb.org/browse/DOCS-861

答案 1 :(得分:1)

根据您的密码:

Event.findAndModify(
  query: { 
    start_timestamp: { $gte: newSnapshot.timestamp - 5min },
    last_snapshot_timestamp: { $gte: newSnapshot.timestamp - 1min }
  },
  update: { 
    snapshots[newSnapshot.timestamp]: newSnapshot,
    $max: { last_snapshot_timestamp: newSnapshot.timestamp },
    $min: { start_timestamp: newSnapshot.timestamp }
  },
  upsert: true,
  $setOnInsert: { ALL OUR NEW EVENT FIELDS } }
)

当成功将第一个事件文档插入数据库时​​,此事件文档的字段具有以下关系:
start_timestamp == last_snapshot_timestamp

在后续更新后,关系变为:
start_timestamp< last_snapshot_timestamp< last_snapshot_timestamp + 1min< start_timestamp + 5min
OR
start_timestamp< last_snapshot_timestamp< start_timestamp + 5min< last_snapshot_timestamp + 1min

因此,如果新快照想要连续插入此事件文档,则必须符合:
    newSnapshot.timestamp< Math.min(last_snapshot_timestamp + 1,start_timestamp + 5)

假设数据库中有两个事件文档随时间推移:
Event1(start_timestamp1,last_snapshot_timestamp1),
Event2(start_timestamp2,last_snapshot_timestamp2)
通常,start_timestamp2> last_snapshot_timestamp1

现在,如果有新的快照出现,其时间戳小于start_timestamp1 (只是假设可以通过延迟或伪造),然后可以插入此快照 进入任一事件文件。所以,我怀疑你是否需要添加其他条件 查询部分,以确保last_snapshot_timestamp和start_timestamp之间的距离始终小于某个值(例如5分钟)?例如,我将查询更改为

  query: { 
        start_timestamp: { $gte: newSnapshot.timestamp - 5min },
        last_snapshot_timestamp: { $gte: newSnapshot.timestamp - 1min , $lte : newSnapshot.timestamp + 5}
      }

好的,让我们继续......
如果我尝试解决此问题,我仍尝试在字段 start_timestamp 上构建唯一索引。 根据MongoDB手册,使用 findAndModify 更新即可完成工作 原子。但令人头疼的是,当出现重复值时我应该如何处理因为 newSnapshot.timestamp失控,它可能会修改 start_timestamp 运营商 $ min

方法是:

  1. 多个线程创建(upsert)一个新的Event文档,因为没有文档可以满足查询条件;
  2. 一个线程成功创建具有特定newSnapshot.timestamp值的新事件文档, 其他人因字段 start_timestamp 上的唯一索引限制而失败;
  3. 其他线程重试(现在是更新而不是upsert)并将成功更新(使用现有的事件文档);
  4. 如果更新(非upsert)导致 $ min 运算符修改 start_timestamp ,巧合的是newSnapshot.tiemstamp 等于现有事件文档中 start_timestamp 的值,更新将因唯一约束而失败 指数。但是我们可以得到消息,并且我们知道事件文档已经存在,其 start_timestamp 值恰好等于 newSnapshot.timestamp。现在,我们可以简单地将newSnapshot插入到此事件文档中,因为它肯定符合条件。
  5. 由于它不需要返回事件文档,我使用更新而不是 findAndModify ,因为两者都是原子操作 在这种情况下,更新的写作更简单 我使用简单的JavaScript(在mongo shell上运行)来表达步骤(我不熟悉您使用的代码语法。:D),我认为您可以轻松理解。

    var gap5 = 5 * 60 * 1000;   // just suppose, you should change accordingly if the value is not true. 
    var gap1 = 1 * 60 * 1000;
    var initialFields = {};     // ALL OUR NEW EVENT FIELDS
    
    function insertSnapshotIfStartTimeStampNotExisted() {
        var query = { 
                start_timestamp: { $gte: newSnapshot.timestamp - gap5 },
                last_snapshot_timestamp: { $gte: newSnapshot.timestamp - gap1 }
        };
        var update = { 
                $push : {snapshots: newSnapshot}, // suppose snapshots is an array 
                $max: { last_snapshot_timestamp: newSnapshot.timestamp },
                $min: { start_timestamp: newSnapshot.timestamp },
                $setOnInsert : initialFields
        },
    
        var result = db.Event.update(query, update, {upsert : true});
        if (result.nUpserted == 0 && result.nModified == 0) {
            insertSnapshotIfStartTimeStampExisted();            // Event document existed with that start_timestamp
        }
    }
    
    function insertSnapshotIfStartTimeStampExisted() {
        var query = { 
                start_timestamp: newSnapshot.timestamp,
        };
        var update = { 
                $push : {snapshots: newSnapshot}
        },
    
        var result = db.Event.update(query, update, {upsert : false});
        if (result.nModified == 0) {
            insertSnapshotIfStartTimeStampNotExisted();         // If start_timestamp just gets modified; it's possible.
        }
    }
    
    // entry
    db.Event.ensureIndex({start_timestamp:1},{unique:true});
    insertSnapshotIfStartTimeStampNotExisted();