Category Archives: Грабли

MongoDB и изоляция

В последних проектах в качестве основного хранилища данных мы используем MongoDB. В целом, нас устраивает этот выбор. MongoDB позволяет быстро и просто хранить не слишком структурированные данные и поддерживает шардинг из коробки. Для большинства веб-проектов этого достаточно. Однако, как и все, относящееся к NoSQL, MongoDB решая одни проблемы (производительности, простоты, масштабирования), добавляет другие. Причем, иногда достаточно неожиданные. Попробую рассказать про последний такой случай. Заметьте, это реальный, а не выдуманный абстрактный пример.

Допустим, у нас есть некая коллекция и нам захотелось пронумеровать определенным образом записи в ней. В качестве языка запросов в MongoDB используется Javascript, поэтому вполне естественная мысль – написать простой скрипт. Запускаем mongo shell:

> db.coll.count()
1000
> var counter = 0
> db.coll.find().sort({ “_id”: 1 }).forEach(function(x) { 
    counter++; 
    db.coll.update({ _id: x._id }, { $set: { num: counter } }); 
  })
> counter
1010

Но ведь у нас в коллекции только 1000 записей! Почему наш счетчик дошел до 1010? Ответ прост, но не сразу очевиден. MongoDB не дает гарантию, что курсор не вернет одну и ту же запись дважды. В случае, если размеры записей меняются, может произойти сдвиг и некоторые записи переместятся в конец. Курсор просто идет по коллекции и возвращает все подряд, соответственно те записи, которые переместились в конец будут возвращены дважды. Что и приводит к ошибке в таком, казалось бы простом скрипте.
Оказывается, в MongoDB есть специальный костыль для таких случаев: cursor.snapshot():

> var counter = 0
> db.coll.find().snapshot().forEach(function(x) { 
    counter++; 
    db.coll.update({ _id: x._id }, { $set: { num: counter } }); 
  })
> counter
1000

И как у любого костыля есть ограничения: нельзя использовать сортировку. В нашем случае это подходит, snapshot сам сортирует как раз по _id. Если нужна какая-то другая сортировка, придется заходить с другой стороны:

> var counter = 0
> var ids = db.coll.find({}, { _id: 1 }).sort({ date: 1 }).toArray()
> ids.forEach(function(x) { 
    counter++; 
    db.coll.update({ _id: x._id }, { $set: { num: counter } }); 
  })
> counter
1000

Для повторения описанного выше на одной и той же коллекции несколько раз, нужно либо каждый раз удалять ее, либо удалять новое поле и делать compact чтобы восстановился первоначальный порядок записей.

Вот такие неочевидные грабли в таком, казалось бы, простом случае. Выкидывая SQL, мы выкидываем и ACID и должны уже сами думать об изоляции и транзакциях. Вообще вокруг NoSQL и MongoDB в частности сложился этакий модно-пионерский образ: “без схемы – это круто”, “SQL теперь не нужен совсем” и т.д. На самом деле схема ведь обычно никуда не девается, просто забота о ней перекладывается на плечи самих программистов. Причем вспомнить об этом придется в самый неподходящий момент. Тут еще нужно отметить, что в современных реляционных базах тоже хватает средств для хранения данных с динамической структурой (xml/xpath, json, различные key-value расширения).

К слову, в текущем проекте мы используем MongoDB скорее по историческим причинам, а не потому что у нас высокие нагрузки, требующие горизонтального масштабирования или потому что это сейчас модно. Просто мы выбрали ее на одном из предыдущих проектов, где она была действительно обоснована. И, поскольку, основные проблемы уже изучены, используем ее до сих пор и на других проектах. Но, как видим, бывает и новые проблемы появляются.

P.S. Уже после написания этого поста увидел практически аналогичный, местами более подробный, но с несколько другими акцентами.