mongoDB optimisti c управление параллелизмом для обновления - PullRequest
1 голос
/ 23 апреля 2020

Я имитирую 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

enter image description here

Но с -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

enter image description here

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

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

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

Есть какие-нибудь подсказки, почему это происходит?

1 Ответ

0 голосов
/ 24 апреля 2020

Как правило, вы должны извлечь соответствующий сегмент кода в вопрос. Неразумно просить людей найти 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

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

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

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

Обновление:

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

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

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

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

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

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

...