Reputation: 177
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
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 theupdate()
method atomically update the document.
Upvotes: 8