Как обойти отсутствие транзакций в MongoDB?

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

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

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

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

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

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

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

  • зарегистрировать транзакцию в коллекции транзакций

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

Я придумал две идеи:

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

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

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

Ответ 1

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

Для более подробной информации смотрите здесь.

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

Ответ 2

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

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

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

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

Update, findAndModify, $addToSet (в рамках обновления) и $push (в рамках обновления) операции работают атомарно внутри одного документа.

Ответ 3

Отметьте этот из Tokutek. Они разрабатывают плагин для Mongo, что promises не только транзакции, но и повышение производительности.

Ответ 4

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

Ответ 5

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

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

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

Ответ 6

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

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

Ответ 7

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

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

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

Ответ 8

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

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

Очередь вакансий: очень общего назначения, решает 95% случаев. Большинству систем необходимо иметь хотя бы одну очередь заданий!

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

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

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

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

Ответ 9

Уже поздно, но думаю, что это поможет в будущем. Я использую 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");
    }
    
  • Как построить очередь
    Я сосредоточусь только на том, как избежать участия в гоночных условиях при создании очереди на бэкэнд-сайте. Если вы не знаете основную идею очереди, иди сюда.
    Приведенный ниже код только показывает концепцию, которую нужно правильно реализовать.

    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 в моем коде приведен выше для action_domain когда вам нужен только вызов action 1 пользователем A block action 2 пользователя A, не блокируйте другого пользователя. По идее ставится уникальный ключ для блокировки каждого пользователя.

Ответ 10

Транзакции теперь доступны в 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();
}