StevenDaGee
StevenDaGee

Reputation: 177

Locks in Mongo and Nodejs

I'm trying to build a really simple shopping app, where users can buy items with virtual currency.

I'm using multiple NodeJS processes for this, so I'm scared about the async part of things.

This is what I do:

app.post('/buy', function(req, res){

    User.findOne({_id: req.userId}, function(err, user){

        if(user.balance >= ITEM_PRICE){

            User.decrementBalance({_id: req.userId}, function(err){

                //Do transaction, give item to user, etc

            });

        } else {
            //Not enough money
        }
    });
});

The problem with this approach is that users can submit multiple /buy requests in a very short time frame. This might lead to a race condition where the second request checked the user's balance before the first one can decrement it. This results in the user having a negative value and taking out much more items than what his balance allowed him to.

Is there a way to solve this? I'm thinking of doing an User.update() and verifying if the user was modified or not. Could that work?

Upvotes: 3

Views: 3059

Answers (1)

robertklep
robertklep

Reputation: 203439

You can use Model.findOneAndUpdate() for that, which will combine the query and the adjustment of the balance in one atomic operation:

User.findOneAndUpdate({
  _id     : req.userId,
  balance : { $gte : ITEM_PRICE }
}, {
  $inc : { balance : -ITEM_PRICE } // there is no $dec
}, {
  new : true
}, function(err, user) {
  ...
});

If the conditions of the query fails (either there's no user with that id, or they don't have enough balance), user will be null (or undefined, not sure). Since it looks like you're only dealing with logged-in users, the id will probably always be valid, so if user isn't defined it'll mean that their balance wasn't high enough.

Because of the new : true, if a user document is returned, it'll reflect the new balance (by default, it would return the old document).

EDIT: some more clarification: you're correct in assessing that there's a race condition between executing .findOne and issuing an update (which is what User.decrementBalance() will be doing).

However, findOneAndUpdate is special, in that it will use a specific MongoDB command (findAndModify) that is guaranteed to be atomic, meaning that both the find and the update will be performed without the chance of another operation interfering between those steps.

An excerpt of the documentation:

When modifying a single document, both findAndModify and the update() method atomically update the document.

Upvotes: 8

Related Questions