开发者

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.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜