Оптимистичный контроль параллелизма mongoDB для обновления

Я моделирую multiple concurrent request для «обновления» MongoDB.

Вот в чем дело: я вставляю данные amount=1000 в mongoDB, и каждый раз, когда я запускаю API, он обновляет сумму на amount += 50 и сохраняет ее обратно в базу данных. По сути, это find and update операция над одним документом.

    err := globalDB.C("bank").Find(bson.M{"account": account}).One(&entry)

    if err != nil {
        panic(err)
    }

    wait := Random(1, 100)
    time.Sleep(time.Duration(wait) * time.Millisecond)

    //step 3: add current balance and update back to database
    entry.Amount = entry.Amount + 50.000
    err = globalDB.C("bank").UpdateId(entry.ID, &entry)

Вот исходный код проект.

Я имитирую запросы с помощью Vegeta:

Если я установлю -rate=10 (что означает запуск API 10 раз в секунду, поэтому 1000 + 50 * 10 = 1500), данные будут правильными.

echo "GET http://localhost:8000" | \
vegeta attack -rate=10 -connections=1 -duration=1s | \
tee results.bin | \
vegeta report

введите здесь описание изображения

Но с -rate=100 (что означает запуск API 100 раз в секунду, поэтому 1000 + 50 * 100 = 6000) получается очень запутанный результат.

echo "GET http://localhost:8000" | \
vegeta attack -rate=100 -connections=1 -duration=1s | \
tee results.bin | \
vegeta report

введите здесь описание изображения

Короче говоря, я хочу знать следующее: я думал, что MongoDB использует optimistic concurrency control, что означает, что если есть write conflict, он должен повторить попытку, чтобы увеличить задержку, но данные должны быть гарантированно правильными.

Почему результат выглядит так, будто правильность данных в MongoDB полностью не гарантируется?

Я знаю, что некоторые из вас, ребята, могут заметить спящий режим в строках 41 и 42, но даже несмотря на то, что я закомментировал это, при тестировании с -rate=500 результат все еще неверен.

Любые подсказки, почему это происходит?


person Llewellyn    schedule 23.04.2020    source источник
comment
Какие операции не дают ожидаемых результатов? Извлеките их из источника в тело вопроса.   -  person D. SM    schedule 23.04.2020


Ответы (1)


Как правило, вы должны извлечь соответствующий сегмент кода в вопрос. Неразумно просить людей найти 5 соответствующих строк в вашей 76-строчной программе.

Ваш тест выполняет параллельные операции поиска и изменения. Предположим, что есть два параллельных процесса A и B, каждый из которых увеличивает баланс счета на 50. Начальный баланс равен 0. Порядок операций может быть следующим:

A: what is the current balance for account 1234?
B: what is the current balance for account 1234?
DB -> A: balance for account 1234 is 0
DB -> B: balance for account 1234 is 0
A: new balance is 0+50 = 50
A: set balance for account 1234 to 50
DB -> A: ok, new balance for account 1234 is 50
B: new balance is 0+50 = 50
B: set balance for account 1234 to 50
DB -> B: ok, new balance for account 1234 is 50

С точки зрения базы данных здесь нет «конфликтов записи». Вы дважды просили установить баланс на 50 для данной учетной записи.

Существуют разные пути решения этого вопроса. Один из них — использовать условные обновления, чтобы процесс выглядел следующим образом:

A: what is the current balance for account 1234?
B: what is the current balance for account 1234?
DB -> A: balance for account 1234 is 0
DB -> B: balance for account 1234 is 0
A: new balance is 0+50 = 50
A: if balance in account 1234 is 0, set balance to 50
DB -> A: ok, new balance for account 1234 is 50
B: new balance is 0+50 = 50
B: if balance in account 1234 is 0, set balance to 50
DB -> B: balance is not 0, no update was performed
B: err, let's start over
B: what is the current balance for account 1234?
DB -> B: balance for account 1234 is 50
B: new balance is 50+50 = 100
B: if balance in account 1234 is 50, set balance to 100
DB -> B: ok, new balance for account 1234 is 100

Как видите, база данных должна поддерживать условное обновление, а приложение должно обрабатывать возможность одновременных обновлений и повторять операцию.

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

Существуют способы создания «конфликта записи» на стороне базы данных, например, с помощью транзакций, поддерживаемых MongoDB 4.0+. В принципе, это работает так же, но «версия» называется «идентификатором транзакции» и хранится в другом месте (не встроена в обрабатываемый документ). Но принцип тот же. В этом случае база данных сообщит вам о конфликте записи, вам все равно придется повторить операции.

Обновлять:

Я думаю, вам также нужно различать «оптимистичный валютный контроль» как понятие, его реализацию и то, к чему относится реализация. https://docs.mongodb.com/manual/faq/concurrency/#how-granular-are-locks-in-mongodb например говорит:

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

При внимательном прочтении этого утверждения видно, что оно применяется к операциям записи на уровне механизма хранения. Я предполагаю, что когда MongoDB выполняет что-то вроде $set или других атомарных операций записи, это применимо. Но это не относится к последовательностям операций на уровне приложения, как вы привели в своем примере.

Если вы попробуете свой пример кода с вашей любимой реляционной СУБД, я думаю, вы обнаружите, что он дает примерно тот же результат, что и с MongoDB, если вы запускаете транзакцию для каждого отдельного чтения и записи (таким образом, баланс чтения и записи находится в разных транзакциях) по той же причине - РСУБД блокируют данные (или используют такие методы, как MVCC) на время существования транзакции, но не между транзакциями.

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

Наконец, API, который MongoDB реализует для транзакций (с повторными попытками), описан здесь. Если вы посмотрите на него внимательно, вы обнаружите, что он ожидает, что приложение повторит не только команду фиксации транзакции, но и повторит всю операцию транзакции. Это связано с тем, что, как правило, при наличии «конфликта записи» начальные данные изменились, и просто повторной попытки окончательной записи недостаточно — потенциально необходимо переделать вычисления в приложениях, возможно, даже побочные эффекты этого процесса изменяются, поскольку результат.

person D. SM    schedule 24.04.2020
comment
Олег, спасибо за подробное объяснение. Однако меня немного смущают следующие моменты. 1. Насколько я знаю, транзакция должна использоваться для обнаружения чтения и записи нескольких документов. В данном случае, поскольку это один документ для чтения и записи. Я думал, что транзакция не будет работать для этого примера. Версионирование, однако, я считаю, что это сработает. 2. Так что же это означает, что mongoDB использует оптимистичный валютный контроль? Похоже, нам нужно обрабатывать все конфликты записи со стороны приложения с помощью транзакции, управления версиями или двухфазной фиксации? В базе данных он сам не проверяет? - person Llewellyn; 25.04.2020
comment
Я думаю, что теперь я знаю ответ для второго, потому что mongoDB не применяет оптимистичный контроль параллелизма на уровне документа. - person Llewellyn; 25.04.2020