Как обойти отсутствие транзакций в MongoDB? - PullRequest
135 голосов
/ 09 июля 2011

Я знаю, что здесь есть похожие вопросы, но они либо говорят мне , чтобы переключиться обратно на обычные системы СУБД, если мне нужны транзакции, либо используют атомарные операции или двухфазные совершить . Второе решение кажется лучшим выбором. Третий, которому я не хочу следовать, потому что кажется, что многие вещи могут пойти не так, и я не могу проверить это во всех аспектах. Мне трудно рефакторинг моего проекта для выполнения атомарных операций. Я не знаю, исходит ли это от моей ограниченной точки зрения (я до сих пор работал только с базами данных SQL), или это на самом деле невозможно.

Мы бы хотели провести пилотное тестирование MongoDB в нашей компании. Мы выбрали относительно простой проект - SMS-шлюз. Это позволяет нашему программному обеспечению отправлять SMS-сообщения в сотовую сеть, и шлюз выполняет грязную работу: фактически общаясь с поставщиками через различные протоколы связи. Шлюз также управляет выставлением счетов за сообщения. Каждый клиент, который подает заявку на услугу, должен купить несколько кредитов. Система автоматически уменьшает баланс пользователя при отправке сообщения и запрещает доступ, если баланс недостаточен. Кроме того, поскольку мы являемся клиентами сторонних поставщиков SMS-сообщений, у нас также может быть свой баланс. Мы также должны следить за ними.

Я начал думать о том, как сохранить необходимые данные с MongoDB, если урезать некоторые сложности (внешний биллинг, отправка SMS в очереди). Исходя из мира SQL, я бы создал отдельную таблицу для пользователей, другую для SMS-сообщений и одну для хранения транзакций, касающихся баланса пользователей. Допустим, я создаю отдельные коллекции для всех в MongoDB.

Представьте себе задачу отправки SMS со следующими шагами в этой упрощенной системе:

  1. проверить, имеет ли пользователь достаточный баланс; запретить доступ, если недостаточно средств

  2. отправьте и сохраните сообщение в коллекции SMS с подробностями и стоимостью (в действующей системе сообщение будет иметь атрибут status, и задание получит его для доставки и установит цену SMS в соответствии с текущим состоянием)

  3. уменьшить баланс пользователей на стоимость отправленного сообщения

  4. регистрация транзакции в коллекции транзакций

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

Я пришел с двумя идеями:

  • Создайте единую коллекцию для пользователей и сохраните баланс в виде поля, связанные с пользователем транзакции и сообщения в качестве вложенных документов в документе пользователя. Поскольку мы можем обновлять документы атомарно, это фактически решает проблему транзакции. Недостатки: если пользователь отправляет много SMS-сообщений, размер документа может увеличиться и может быть достигнут лимит в 4 МБ. Может быть, я могу создавать исторические документы в таких сценариях, но я не думаю, что это было бы хорошей идеей. Кроме того, я не знаю, как быстро будет работать система, если я добавлю все больше и больше данных в один и тот же большой документ.

  • Создайте одну коллекцию для пользователей и одну для транзакций. Может быть два вида транзакций: покупка в кредит с положительным изменением баланса и отправка сообщений с отрицательным изменением баланса. Транзакция может иметь поддокумент; например, в отправленных сообщениях детали SMS могут быть встроены в транзакцию. Недостатки: я не сохраняю текущий баланс пользователя, поэтому мне приходится рассчитывать его каждый раз, когда пользователь пытается отправить сообщение, чтобы узнать, может ли сообщение пройти или нет. Боюсь, что этот расчет может стать медленным по мере роста количества хранимых транзакций.

Я немного запутался в том, какой метод выбрать. Есть ли другие решения? Я не мог найти лучшие практики онлайн о том, как обойти подобные проблемы. Я предполагаю, что многие программисты, которые пытаются познакомиться с миром NoSQL, сталкиваются с подобными проблемами в начале.

Ответы [ 10 ]

81 голосов
/ 29 августа 2016

Жизнь без транзакций

Транзакции поддерживают ACID свойства, но хотя в MongoDB нет транзакций, у нас есть атомарные операции.Ну, атомарные операции означают, что когда вы работаете с одним документом, эта работа будет завершена до того, как кто-либо еще увидит документ.Они увидят все изменения, которые мы внесли, или ни одного из них.А используя атомарные операции, вы часто можете выполнить то же самое, что мы сделали бы, используя транзакции в реляционной базе данных.И причина в том, что в реляционной базе данных нам нужно вносить изменения в несколько таблиц.Обычно таблицы, которые необходимо объединить, и поэтому мы хотим сделать это все сразу.И чтобы сделать это, поскольку существует несколько таблиц, нам нужно начать транзакцию и выполнить все эти обновления, а затем завершить транзакцию.Но с MongoDB мы собираемся встраивать данные, так как мы собираемся предварительно объединить их в документах, и они являются этими богатыми документами, которые имеют иерархию.Мы часто можем сделать то же самое.Например, в примере с блогом, если мы хотим убедиться, что мы обновили пост в блоге атомарно, мы можем сделать это, потому что мы можем обновить весь пост в блоге сразу.Где, как если бы это была группа реляционных таблиц, нам, вероятно, пришлось бы открыть транзакцию, чтобы мы могли обновить коллекцию записей и комментариев.

Итак, каковы наши подходы, которые мы можем использовать в MongoDB чтобы преодолеть недостаток транзакций?

  • реструктурировать - реструктурировать код, чтобы мы работали в одном документе и использовали элементарные операции, которые мы предлагаем в рамкахэтот документ.И если мы это сделаем, то, как правило, мы все готовы.
  • реализовать в программном обеспечении - мы можем реализовать блокировку в программном обеспечении, создав критическую секцию.Мы можем построить тест, протестировать и установить, используя поиск и изменение.Мы можем построить семафоры, если это необходимо.И в некотором смысле, так устроен большой мир.Если подумать, если одному банку нужно перевести деньги в другой, они не живут в одной и той же реляционной системе.И каждый из них часто имеет свои собственные реляционные базы данных.И они должны иметь возможность координировать эту операцию, даже если мы не можем начать транзакцию и завершить транзакцию в этих системах баз данных, только в рамках одной системы в одном банке.Так что в программном обеспечении, безусловно, есть способы обойти эту проблему.
  • допуск - последний подход, который часто работает в современных веб-приложениях и других приложениях, использующих огромное количество данных, - просто допустить небольшую несогласованность.Например, если мы говорим о фиде друзей в Facebook, не имеет значения, все ли увидят обновление вашей стены одновременно.Если все в порядке, если один человек несколько раз отстает на несколько секунд, и они догоняют.Часто во многих системных конструкциях не критично, чтобы все было идеально согласованным и чтобы у всех было абсолютно согласованное и одинаковое представление о базе данных.Таким образом, мы могли бы просто допустить небольшую несогласованность, которая является несколько временной.атомарно в пределах одного документа.
24 голосов
/ 14 октября 2013

Проверьте это , Tokutek. Они разработали плагин для Mongo, который обещает не только транзакции, но и повышение производительности.

16 голосов
/ 15 февраля 2018

Начиная с 4.0, MongoDB будет иметь многодокументные транзакции ACID. План состоит в том, чтобы сначала включить тех, кто находится в развертываниях наборов реплик, а затем сегментированные кластеры. Транзакции в MongoDB будут выглядеть так же, как и разработчики транзакций, знакомые по реляционным базам данных - они будут состоять из нескольких операторов с похожей семантикой и синтаксисом (например, start_transaction и commit_transaction). Важно отметить, что изменения в MongoDB, которые разрешают транзакции, не влияют на производительность для рабочих нагрузок, которым они не требуются.

Подробнее см. здесь .

11 голосов
/ 10 июля 2011

Суть в том, что: если целостность транзакции должна , то не используйте MongoDB, а используйте только компоненты в системе, поддерживающей транзакции. Чрезвычайно сложно создать что-то поверх компонента, чтобы обеспечить аналогичные функции ACID для компонентов, не совместимых с ACID. В зависимости от индивидуальных вариантов использования может иметь смысл разделять действия на транзакционные и нетранзакционные действия каким-либо образом ...

7 голосов
/ 10 июля 2011

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

Этоэто не проблемаУпомянутая вами ошибка является либо логической (ошибка), либо ошибкой ввода-вывода (сеть, сбой диска).Такой тип ошибки может оставить как транзакционные, так и транзакционные хранилища в несогласованном состоянии.Например, если он уже отправил SMS, но при сохранении сообщения произошла ошибка - он не может откатить отправку SMS, что означает, что он не будет зарегистрирован, баланс пользователя не будет уменьшен и т. Д.

РеальноеПроблема здесь в том, что пользователь может воспользоваться состоянием гонки и отправить больше сообщений, чем позволяет его баланс.Это также относится к СУБД, если только вы не отправляете SMS внутри транзакции с блокировкой поля баланса (что было бы большим узким местом).В качестве возможного решения для MongoDB можно было бы сначала использовать findAndModify, чтобы уменьшить баланс и проверить его, если он отрицательный, запретить отправку и возместить сумму (атомный прирост).Если получено положительное значение, продолжите отправку, и в случае неудачи верните сумму.Также можно вести коллекцию истории сальдо, чтобы помочь исправить / проверить поле сальдо.

6 голосов
/ 27 января 2018

Это, пожалуй, лучший блог, который я нашел относительно реализации функции, подобной транзакции, для mongodb.!

Флаг синхронизации: лучше всего копировать данные из основного документа

Job Queue: очень общего назначения, решает 95% случаев. В большинстве систем в любом случае должна быть хотя бы одна очередь заданий!

Двухфазная фиксация: этот метод гарантирует, что каждый объект всегда имеет всю информацию, необходимую для перехода в согласованное состояние

Сверка журналов: самый надежный метод, идеальный для финансовых систем

Управление версиями: обеспечивает изоляцию и поддерживает сложные структуры

Прочитайте это для получения дополнительной информации: https://dzone.com/articles/how-implement-robust-and

6 голосов
/ 10 июля 2011

Транзакции отсутствуют в MongoDB по уважительным причинам. Это одна из тех вещей, которые делают MongoDB быстрее.

В вашем случае, если транзакция является обязательной, монго не подходит.

Может быть RDMBS + MongoDB, но это добавит сложностей и усложнит управление и поддержку приложения.

6 голосов
/ 09 июля 2011

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

Итак, если вам действительно нужен пилотный проект для MongoDB, выберите тот, который прост в , что в отношении.

4 голосов
/ 05 апреля 2018

Уже поздно, но думаю, что это поможет в будущем.Я использую Redis для создания очереди для решения этой проблемы.

  • Требование:
    Изображение ниже показывает, что 2 действия должны выполняться одновременно, но фаза 2 и фаза 3 действия 1 должны быть завершены до начала фазы 2 действия 2 илинапротив (фаза может быть запросом REST API, запросом к базе данных или выполнением кода JavaScript ...).enter image description here

  • Как вам поможет очередь
    В очереди убедитесь, что каждый код блока между lock() и release()во многих функция не будет работать одновременно, сделайте их изолированными.

    function action1() {
      phase1();
      queue.lock("action_domain");
      phase2();
      phase3();
      queue.release("action_domain");
    }
    
    function action2() {
      phase1();
      queue.lock("action_domain");
      phase2();
      queue.release("action_domain");
    }
    
  • Как построить очередь
    Я сосредоточусь только на том, как избежать участия race conditon при создании очереди на бэкэнд-сайте.Если вы не знаете основную идею очереди, приходите сюда .
    Приведенный ниже код показывает только концепцию, вам нужно правильно ее реализовать.

    function lock() {
      if(isRunning()) {
        addIsolateCodeToQueue(); //use callback, delegate, function pointer... depend on your language
      } else {
        setStateToRunning();
        pickOneAndExecute();
      }
    }
    
    function release() {
      setStateToRelease();
      pickOneAndExecute();
    }
    

Но вам нужно isRunning() setStateToRelease() setStateToRunning() изолировать себя, иначе вы снова столкнетесь с состоянием гонки.Для этого я выбираю Redis для ACID цели и масштабируемости.
Redis документ поговорим о его транзакции:

Все команды в транзакции являютсясериализуется и выполняется последовательно.Никогда не может случиться, что запрос, выданный другим клиентом, будет обработан в середине выполнения транзакции Redis.Это гарантирует, что команды выполняются как одна изолированная операция.

P / s:
Я использую Redis, потому что мой сервис уже использует его, вы можете использовать любую другуюспособ поддержки изоляции, чтобы сделать это.
action_domain в моем коде выше для того случая, когда вам нужен только вызов действия 1 для пользователя A блокировать действие 2 для пользователя A, не блокируйте другого пользователя.По идее ставится уникальный ключ для блокировки каждого пользователя.

3 голосов
/ 20 июля 2018

Транзакции теперь доступны в MongoDB 4.0.Образец здесь

// Runs the txnFunc and retries if TransientTransactionError encountered

function runTransactionWithRetry(txnFunc, session) {
    while (true) {
        try {
            txnFunc(session);  // performs transaction
            break;
        } catch (error) {
            // If transient error, retry the whole transaction
            if ( error.hasOwnProperty("errorLabels") && error.errorLabels.includes("TransientTransactionError")  ) {
                print("TransientTransactionError, retrying transaction ...");
                continue;
            } else {
                throw error;
            }
        }
    }
}

// Retries commit if UnknownTransactionCommitResult encountered

function commitWithRetry(session) {
    while (true) {
        try {
            session.commitTransaction(); // Uses write concern set at transaction start.
            print("Transaction committed.");
            break;
        } catch (error) {
            // Can retry commit
            if (error.hasOwnProperty("errorLabels") && error.errorLabels.includes("UnknownTransactionCommitResult") ) {
                print("UnknownTransactionCommitResult, retrying commit operation ...");
                continue;
            } else {
                print("Error during commit ...");
                throw error;
            }
       }
    }
}

// Updates two collections in a transactions

function updateEmployeeInfo(session) {
    employeesCollection = session.getDatabase("hr").employees;
    eventsCollection = session.getDatabase("reporting").events;

    session.startTransaction( { readConcern: { level: "snapshot" }, writeConcern: { w: "majority" } } );

    try{
        employeesCollection.updateOne( { employee: 3 }, { $set: { status: "Inactive" } } );
        eventsCollection.insertOne( { employee: 3, status: { new: "Inactive", old: "Active" } } );
    } catch (error) {
        print("Caught exception during transaction, aborting.");
        session.abortTransaction();
        throw error;
    }

    commitWithRetry(session);
}

// Start a session.
session = db.getMongo().startSession( { mode: "primary" } );

try{
   runTransactionWithRetry(updateEmployeeInfo, session);
} catch (error) {
   // Do something with error
} finally {
   session.endSession();
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...