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



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



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



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



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




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


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


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


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


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

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




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


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



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

891   10  

10 ответов:

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

Жизнь Без Сделок

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • требования:
    Изображение ниже показывает, что 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 for кислоты цель и масштабируемость.
Редис документ говорить об этом по сделке:

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

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

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

Comments

    Ничего не найдено.