17

I'm in the process of porting my application from an App Engine Datastore to a MongoDB backend and have a question regarding the consistency of "document updates." I understand that the updates on one document are all atomic and isolated, but is there a way to guarantee that they're "consistent" across different replica sets?

In our application, many users can (and will) be trying to update one document at the same time by inserting a few embedded documents (objects) into it during one single update. We need to ensure these updates occur in a logically consistent manner across all replicas, i.e. when one user "puts" a few embedded documents into the parent document, no other users can put their embedded documents in the parent document until we ensure they've read and received the first user's updates.

So what I mean by consistency is that we need a way to ensure that if two users attempt to perform an update on one document at exactly the same time, MongoDB only allows one of those updates to go through, and discards the other one (or at least prevents both from occuring). We can't use a standard "sharding" solution here, because a single update consists of more than just an increment or decrement.

What's the best way of guaranteeing the consistency of one particular document?

Eternal Rubyist
  • 3,445
  • 4
  • 34
  • 51
  • 3
    I think that you have given credit to the wrong answer. The trick here is atomic operations + `findAndModify`. In your case, you want `findAndModify` with a timestamp so that subsequent writes fail until the reader refreshes. – Gates VP Nov 13 '11 at 08:39
  • @GatesVP Both answers are good and I encourage everyone to read both to form a more complete picture of MongoDB consistency. I selected mnemosyn's response because it explained the core concept of MongoDB's "write concern" policies and safe vs. unsafe reads. I had already seen examples like dcrosta's, but needed to know precisely what one could and couldn't guarantee with "unsafe" reads. – Eternal Rubyist Oct 17 '13 at 18:15
  • 1
    In real distributed database world, you cannot rely on timestamps to determine order. Different nodes may (and will) have inconsistent clocks. We wouldn't need to mess with consensus protocols like Paxos if we could just use timestamps. But since MongoDB is essentially a single-master database, feel free to use timestamps. Just don't ask in which way then it's better than good old *SQL. – Dzmitry Lazerka May 12 '14 at 23:07

2 Answers2

19

There may be other ways to accomplish this, but one approach is to version your documents, and issue updates against only the version that the user had previously read (i.e., ensure that no one else has updated the document since it was last read). Here's a brief example of this technique using pymongo:

>>> db.foo.save({'_id': 'a', 'version': 1, 'things': []}, safe=True)
'a'
>>> db.foo.update({'_id': 'a', 'version': 1}, {'$push': {'things': 'thing1'}, '$inc': {'version': 1}}, safe=True)
{'updatedExisting': True, 'connectionId': 112, 'ok': 1.0, 'err': None, 'n': 1}

note in the above, key "n" is 1, indicating that the document was updated

>>> db.foo.update({'_id': 'a', 'version': 1}, {'$push': {'things': 'thing2'}, '$inc': {'version': 1}}, safe=True)
{'updatedExisting': False, 'connectionId': 112, 'ok': 1.0, 'err': None, 'n': 0}

here where we tried to update against the wrong version, key "n" is 0

>>> db.foo.update({'_id': 'a', 'version': 2}, {'$push': {'things': 'thing2'}, '$inc': {'version': 1}}, safe=True)
{'updatedExisting': True, 'connectionId': 112, 'ok': 1.0, 'err': None, 'n': 1}
>>> db.foo.find_one()
{'things': ['thing1', 'thing2'], '_id': 'a', 'version': 3}

Note that this technique relies on using safe writes, otherwise we don't get an acknowledgement indicating the number of documents updated. A variation on this would use the findAndModify command, which will either return the document, or None (in Python) if no document matching the query was found. findAndModify allows you to return either the new (i.e. after updates are applied) or old version of the document.

dcrosta
  • 26,009
  • 8
  • 71
  • 83
  • 1
    +1 Good answer but worth noting that this approach puts the responsibility on the client (your application code) to check to see what is the result of the findAndModify command and re-issue the update if the findAndModify fails (as an indication that the document has already been updated by someone else). I think this is a fair trade off to make. – Bryan Migliorisi Nov 10 '11 at 15:45
  • 1
    @dcrosta That makes sense... so basically as long as we use the highest level of "safe write" (WriteConcern.REPLICAS_SAFE for us Java drinkers) there's virtually no chance of two clients accessing different replicas at the exact same time and both (successfuly) writing conflicting "version: 1" updates? – Eternal Rubyist Nov 10 '11 at 15:57
  • @nomizzz See also: http://www.mongodb.org/display/DOCS/Atomic+Operations#AtomicOperations-%22UpdateifCurrent%22 – pingw33n Nov 11 '11 at 09:00
  • @nomizzz since secondaries do not accept writes, even with replication this will work. if you read from secondaries, there is a slightly higher chance (depending on load and replication lag) that you will have "conflicting" writes -- i.e. having read an "old" version from the secondary before accepting user changes and then writing the new version to the primary. `REPLICAS_SAFE` doesn't have transactional semantics, so the write happens on the primary first anyway -- it just makes the client doing the writing wait until the new data is replicated to at least 2 secondaries before returning. – dcrosta Nov 13 '11 at 19:41
3

MongoDB does not offer master-master replication or multi-version concurrency. In other words, writes always go to the same server in a replica set. By default, even reads from secondaries are disabled so the default behavior is that you communicate only with one server at a time. Therefore, you do not need to worry about inconsistent results in safe mode if you use atomic modifiers (like $inc, $push, etc.).

If you don't want to restrict yourself to these atomic modifiers, compare and swap as recommended by dcrosta (and the mongo docs) looks like a good idea. All this is not related to replica sets or sharding, however - it would be the same in a single-server scenario.

If you need to ensure read consistency also in case of a database/node failure, you should make sure you're writing to the majority of servers in safe mode.

The two approaches behave different if you allow unsafe reads: the atomic update operations would still work (but may give unexpected results), while the compare-and-swap approach would fail.

mnemosyn
  • 45,391
  • 6
  • 76
  • 82
  • I don't see why the compare-and-swap as I've proposed would fail under replication but not in single-server. The key point in my suggestion is that you only write if the version hasn't changed from when you began editing it (i.e. in a web form), and let the user decide what to do if it has been modified (or otherwise manage the conflict). In single-server, you can still have multiple users, so conflicts can happen in both scenarios. – dcrosta Nov 13 '11 at 19:44
  • If you allow *unsafe reads* (i.e. reads from secondaries), the version read might not be up-to-date. In that scenario, the client believes the current version is, say 12, but the master has already seen a write, increasing the version to 13. Now the update would fail because we read from a secondary. This can't happen in single-server, and it can't happen when reading only from the master. The modifier operation would still execute, but might not give the expected result, because the older version is different from what was displayed to the user. – mnemosyn Nov 13 '11 at 20:21
  • It can happen in single-server scenarios (or when not reading from secondaries) if another user updates the document between when a user reads a version (12 in your example) and when they try to write (i.e. on web page submission). In other words, build the code to expect conflicts and handle them appropriately (which is an application specific concern; you're right that MongoDB doesn't handle this for you in any way). – dcrosta Nov 14 '11 at 04:33
  • @dcrosta: what you describe is what the algorithm is *expected* to do. If someone writes in the mean time, there obviously is a conflict, and it's the algorithm's job to detect that. The algorithm *fails* (= does not do what is expected) if the read operation does not read the current state. That is unexpected behavior, not the write conflict which is part of the algorithm. – mnemosyn Nov 14 '11 at 09:06
  • 1
    I suppose that depends on what you determine to be "expected" in this situation. From my point of view, it does exactly what's expected, given that you're allowing (potentially) stale reads if reading from a secondary. But, I don't want to get into an argument, and I grant that under narrow circumstances it will at least behave differently, so point taken. – dcrosta Nov 14 '11 at 13:12
  • @mnemosyn "If you need to ensure read consistency also in case of a database/node failure, you should make sure you're writing to the majority of servers in safe mode." Does this mean that there is a chance that read consistency is not guaranteed? – Eternal Rubyist Jan 26 '12 at 22:21
  • If the primary fails, some changes might not have propagated to the secondaries yet. In that case, read consistency would be violated, yes. It's unlikely, but possible. At the moment, I'm not even sure if the majority would *guarantee* consistent reads - maybe you'd have to write to all servers for such strong guarantees. – mnemosyn Jan 27 '12 at 08:17