How to atomically pop random element?
Is there a way to atomically pop (remove and retrieve) a random element with MongoDB - like Redis's SPOP?
I've read the RandomAttribute tutorial but now I need to make sure that the element is also removed when fetched, and this must be done atomically.
I guess as an alternative I could push the data into an array field pre-sorted, but I'd really prefer to have it fetch a random record.
开发者_StackOverflowLooking at $pop
's documentation, it seems it can't take arguments, so it either removes the first or the last element of an array.
Updates within collections in Mongo are atomic, so combining findAndModify
with the RandomAttribute approach will do the trick:
var rand = Math.rand();
db.collection.findAndModify({query: {random: {$gte: rand}}, remove: true});
Just make sure to also query for random: {$lt: rand}
when the above returns null
.
Protip: some drivers have findAndRemove
, which is the same as findAndModify
with remove: true
.
There basically isn't currently a nice way to do what you want to do. The big problem is that there isn't an atomic way to delete an element of an array by its position.
See this question for more info on this.
The best - nearest to atomic - way I can think of to work around this is the following. It relies on you being able to handle null elements in the array for a short time (this is the bit that isn't atomic).
Step One
Generate a random number on the client side. Use this to specify the key of the array element you want to remove in dot notation (ie. "my_array.4") as AFAIK this can't be done dynamically in the query.
Step Two
Use the random number to atomically unset the selected element of the array while retrieving it as it was before the update using findAndModify
with new
set to false. Assuming the random number you generated was 4
, you'd do something like the following:
db.mycollection.findAndModify({
query: {_id: 1},
update: {$unset: {"my_array.4" : true }},
fields: {my_array : {$slice : [4, 1]}, _id : false},
new: false
});
(NB. findAndModify
currently returns the document pre-update by default, but I read somewhere that that might change, so it's a good idea always to specify which you want using the new
option.)
This will leave the array element that was selected as a null
value in the array.
Step Three
Remove the null element from the array with another update, using $pull:
db.mycollection.update({_id: 1}, {$pull: {my_array: null}})
Phew! Seems like a lot of work for what you want to do. I don't know Redis but SPOP sounds a lot easier. I hope this helps a bit though, anyway. It's not a single atomic operation, but (as long as you can handle those nulls) I think the most important parts of it are. You're never going to get into a situation where two different threads pop the same element at nearly the same time, which, I'm guessing, is what you're trying to avoid.
精彩评论