Does mongodb transactions implement optimistic concurrency/locking by default?

Hi I am wondering whether mongodb transactions implement optimistic concurrency by default? For example let’s say I start a transaction then do some updates in the session and right before I commit an update was made to a document in that transaction seperately. Does that transaction still go through?

I’m thinking if it does not I should read from the database at the documents getting updated before commit but I feel there’s a flaw if somehow by some chance the document gets updated before the commit which would compromise data integrity.

I just tested this on a local instance and it seemed to work as expected with defaults:

var session = db.getMongo().startSession()
var sessionColRef = session.getDatabase('TranTest').Test
session.startTransaction()
sessionColRef.updateOne(
{_id:1},
{
    $set:{
        house:false
    }
}
)
sessionColRef.find()

So we’ve done an update to document 1 (yes, I know…it’s not an ObjectID).

If outside of that transaction I attempt to update the same document:

db.getCollection("Test").updateOne({_id:1}, {$set:{house:true}})

The update hangs, it cannot complete as there is a transaction locking that document.

If I update another field that the transaction had not changed then the update will go through

A .find in another session will not show this new property until the transaction is committed.

Upon the first transaction completing the second update can go through and writes house:true to the record.

If I wrap both updates in transactions so:

Session1 : Update house to 9
Session2 : Update house to ‘A’

If I try and commit session 2 I get a transaction aborted as the data has already been updated by another session.
I can commit session 1 which saves 9 to the house property.

Obviously you can also change the read and write isolation levels as per the documentation, but have a play and test out.

If you wrap everything in transactions it seems to work the way you want, but if the second update is run outside a transaction, it waits but then goes through overwriting the transaction update. If both were wrapped then the second update would fall over as the first transaction has already done an update on that record.

1 Like

Hey John thanks so much for the reply, it helped me a lot! When you refer to the second update outside a transaction overwriting the transaction update do you mean this happens after the commit or the period between the session actions and commit?

You wrote:

If outside of that transaction I attempt to update the same document:

`db.getCollection("Test").updateOne({_id:1}, {$set:{house:true}})`

The update hangs, it cannot complete as there is a transaction locking that document.
If I update another field that the transaction had not changed then the update will go through

So if I retrieve or update a document in the session does it lock out non-transactional updates as well until that’s session abort or commit?

Like let’s say in your example you update “name” in the non-transactional update that would go through but not be detectable when the transaction queries for the document in the session or do you mean another transactions uncommitted update would just not be visible?

Do you think checking for document versioning right at the end of my transaction is sufficient to ensure data integrity or is it not necessary?

I didnt test a read, but I suspect that just a read within a session would not affect a read outside of the session.
I did see a question on here recently about forcing a lock on a record in preparation of a later update, but I imagine that’s a pretty niche use case, you want to have a light touch generally and not lock what you’re not updating now.

I’d strongly recommend having a play as I did with a local instance, the only caveat is you’ll need to have a replicaset to use transactions, but that’s easy enough to setup:

Then you can open two shells (or query windows in your tool of choice, I was using Studio3T free edition) and play.

This is the script I was playing with:

var session = db.getMongo().startSession()
var sessionColRef = session.getDatabase('TranTest').Test
session.startTransaction()
sessionColRef.updateOne(
{_id:1},
{
    $set:{
        amazing:10
    }
}
)
sessionColRef.find()
session.commitTransaction()

and in the other one:

var session2 = db.getMongo().startSession()
var sessionColRef2 = session2.getDatabase('TranTest').Test
session2.startTransaction()
sessionColRef2.updateOne(
{_id:1},
{
    $set:{
        amazing:false
    }
}
)
sessionColRef2.find()
session2.commitTransaction()

db.getCollection("Test").updateOne({_id:1}, {$set:{ttt:7}})

Just running the parts that I needed, so in the first window run up to before the commit is run and then in the second window you can play about with different updates and sessions etc.

I’m not an expert on this, but it was something I’d not played about with enough that it was worth a play and get some hands on experience of using transactions. I have used them a while back for a migration project (node.js) where I was inserting batches of transactions and I needed to ensure that in the event of a failure, I could roll back the complete batch and re-try.

1 Like

Hey John, so I tried it for myself and it seems to work they way I want it to but it seems when I update the field that is not being updated in mongodb it still is processed after the transaction. Which I think you said that the document would be updated if the fields didn’t collide but for me it seems it was locked anyways and the non-transactional update was updated later.

db.accounts.insertOne({
  _id:1,
  email: 'canvas@hotmail.com',
  name: 'Jake',
});
const session = db.getMongo().startSession()
const sessionColRef = session.getDatabase('Testing').accounts
session.startTransaction()


sessionColRef.updateOne(
{_id:1},
{
    $set:{
        name: 'ses1Update'
    }
}
)

db.accounts.updateOne({_id:1}, {$set:{
        job: 'Interfering update'
    }
})


sessionColRef.updateOne(
{_id:1},
{
    $set:{
         job: 'ses1update'
    }
}
)
sessionColRef.find()
session.commitTransaction()

So the end result for me was “job: ‘interfering update’”. The whole document is locked it seems and mongo processes the non-transactional update after the transaction commit if the document was already going to be updated in a transaction.

Trying again, I do seem to be getting the non-transaction locked when the trasnsaction has been started and an update sent for the same document. After the transaction completes, the update continues and updates the document as expected.
In the case of a different field being updated then both updates show in the final document, in the case of them both updating the same field then the blocked transaction update shows in the final output, having taken place after the lock was released.
In the event of both being within a transaction, the second update returns an error when it picks up that you’re trying to update a document that’s locked.

So if you’re in an application using transactions, I guess the take-away is…use transactions, keep them short and have robust error handling to pick up these situations and a well defined process flow for how you deal with them!