Динамическое атомарное изменение в Mongodb, позволяющее избежать состояний гонки

У нас есть объекты, для которых мы хотим сохранить заданный пользователем порядок. Например:

class Book:
    id: ObjectId
    order: int


class ClubBookRanking:
    books: list[Book]

Прежде чем вы предложите: полагаться на порядок списка здесь не работает. Это просто меняет проблему.

Мы храним эти объекты в монго. Здесь нам нужно выполнить два типа операций.

  1. Добавьте новую книгу: она должна быть добавлена ​​в конец списка (при включении нового элемента порядок должен быть равен длине списка)
  2. Изменение порядка пользователя: книга должна изменить свой порядок, а порядок всех последующих книг также должен быть обновлен.

Обе эти ситуации уязвимы к условиям гонки. Я инстинктивно пытаюсь сделать это в сложных атомарных транзакциях. Я предполагаю, что mongo будет выполнять атомарные транзакции последовательно, и поэтому можно будет избежать условий гонки (ala redis).

Кто-нибудь знает лучший способ справиться с этим? Мы используем Atlas, поэтому можем использовать их функциональность.

Редактировать: В книгах есть uuid

🤔 А знаете ли вы, что...
MongoDB может использоваться для хранения данных схем свойственных JSON, что удобно для веб-разработки...


100
1

Ответ:

Решено

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

ClubBookRanking — это единый документ, и все книги упорядочены внутри массива «books».

Все, что вам нужно, это обеспечить отсутствие одновременных обновлений этого документа, что легко исправить с помощью изменений, например. посмотрите, как mongoose реализовал это с помощью versionKey https://github.com/Automattic/mongoose/blob/e359b99e0d1a15669143363855207660aa508fb9/docs/guide.md#option-versionkey

По сути, вы добавляете в ClubBookRanking монотонно увеличивающийся номер версии, который увеличивается при каждом обновлении. В псевдокоде:

const cbr = db.ClubBookRanking.findOne(); //the document
const __v = cbr.__v;  //the revision number
reorder(cbr.book); // reshuffle the books
const result = db.ClubBookRanking.updateOne(
    {
        _id: cbr._id,   // filter by unique id
        __v: __v        // and only when revision didn't change since we read the document
    }, {
        $set: {books: cbr.books}, // the new order
        $inc:{__v:1}              // increment the revision
    })
    if (result.nModified < 1 ) {
         throw new Error("Concurrent update") 
    }
);

Здесь мы пробуем оптимистичное обновление, предполагая, что одновременных обновлений нет, так что updateOne обновляет не более 1 документа. Если было одновременное обновление, __v не будет соответствовать фильтру, и обновлений не произойдет — result.nModified будет равен 0.

Теперь вам решать, как разрешить конфликт — вы можете сделать что-то умное, выбрав новую ревизию и попытавшись повторно применить переупорядочение к последней ревизии программно, или через нее обратно пользователю, сказав, что кто-то другой обновил порядок быстрее. чем они, и попросить просмотреть и изменить заказ.