Подтвердить что ты не робот

Атомные транзакции в хранилищах с ключом

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

Существует несколько постоянных хранилищ ключей, в том числе CouchDB и Cassandra, а также множество других проектов.

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

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

Один очевидный подход состоит в том, чтобы иметь отдельную таблицу, которая описывает транзакции. Затем перенос денег с одного банковского счета на другой состоит в простом вводе новой строки в эту таблицу. Мы не сохраняем текущие балансы двух банковских счетов и вместо этого рассчитываем суммирование всех соответствующих строк в таблице транзакций. Легко представить, что это было бы слишком большой работой; банк может иметь миллионы транзакций в день, а индивидуальный банковский счет может быстро иметь несколько тысяч "транзакций", связанных с ним.

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

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

4b9b3361

Ответ 1

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

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

Ответ 2

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

Чтобы завершить банковский перевод, вы должны сделать несколько вещей:

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

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

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

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

EDIT: Возможно, стоит отметить, что реальные банки в реальном мире используют в конечном итоге последовательность. Если вы достаточно долго просматриваете свой банковский счет, вы получаете комиссию за овердрафт. Если бы вы были очень хороши, вы могли бы даже снять деньги из двух разных банкоматов почти одновременно и переустановить свою учетную запись, потому что есть условие гонки, чтобы проверить баланс, выпустить деньги и записать транзакцию. Когда вы депозируете чек в свою учетную запись, они балансируют баланс, но на самом деле удерживают эти средства в течение определенного периода времени "на всякий случай" у исходной учетной записи действительно недостаточно денег.

Ответ 3

Чтобы привести конкретный пример (потому что в Интернете есть неожиданное отсутствие правильных примеров): здесь, как реализовать " перенос баланса атомного банка" в CouchDB (в основном скопирован из моего сообщения в блоге по тому же вопросу: http://blog.codekills.net/2014/03/13/atomic-bank-balance-transfer-with-couchdb/)

Во-первых, краткий обзор проблемы: как может банковская система, которая позволяет деньги, которые должны быть переведены между счетами, должны быть сконструированы таким образом, чтобы не было расы условия, которые могут оставить недействительные или бессмысленные балансы?

Есть несколько причин для этой проблемы:

Во-первых: журнал транзакций. Вместо хранения баланса счета в одном запись или документ - {"account": "Dave", "balance": 100} - учетная запись баланс рассчитывается путем суммирования всех кредитов и дебетов на этот счет. Эти кредиты и дебетования хранятся в журнале транзакций, который может выглядеть что-то вроде этого:

{"from": "Dave", "to": "Alex", "amount": 50}
{"from": "Alex", "to": "Jane", "amount": 25}

И функции CouchDB для уменьшения количества карт для вычисления баланса могут выглядеть что-то вроде этого:

POST /transactions/balances
{
    "map": function(txn) {
        emit(txn.from, txn.amount * -1);
        emit(txn.to, txn.amount);
    },
    "reduce": function(keys, values) {
        return sum(values);
    }
}

Для полноты, вот список балансов:

GET /transactions/balances
{
    "rows": [
        {
            "key" : "Alex",
            "value" : 25
        },
        {
            "key" : "Dave",
            "value" : -50
        },
        {
            "key" : "Jane",
            "value" : 25
        }
    ],
    ...
}

Но это оставляет очевидный вопрос: как обрабатываются ошибки? Что произойдет, если кто-то пытается сделать перевод больше, чем их баланс?

С CouchDB (и аналогичными базами данных) такая бизнес-логика и ошибка обработка должна выполняться на уровне приложения. Наивно, такая функция может выглядеть так:

def transfer(from_acct, to_acct, amount):
    txn_id = db.post("transactions", {"from": from_acct, "to": to_acct, "amount": amount})
    if db.get("transactions/balances") < 0:
        db.delete("transactions/" + txn_id)
        raise InsufficientFunds()

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

// Initial balances: Alex: 25, Jane: 25
db.post("transactions", {"from": "Alex", "To": "Jane", "amount": 50}
// Current balances: Alex: -25, Jane: 75

Как это можно зафиксировать?

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

  • Время создания транзакции (чтобы убедиться, что существует strict полное упорядочение транзакций) и

  • Состояние - успешная транзакция.

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

POST /transactions/balance-available
{
    "map": function(txn) {
        if (txn.status == "successful") {
            emit(txn.from, txn.amount * -1);
            emit(txn.to, txn.amount);
        }
    },
    "reduce": function(keys, values) {
        return sum(values);
    }
}

POST /transactions/oldest-pending
{
    "map": function(txn) {
        if (txn.status == "pending") {
            emit(txn._id, txn);
        }
    },
    "reduce": function(keys, values) {
        var oldest = values[0];
        values.forEach(function(txn) {
            if (txn.timestamp < oldest) {
                oldest = txn;
            }
        });
        return oldest;
    }

}

Список передач теперь может выглядеть примерно так:

{"from": "Alex", "to": "Dave", "amount": 100, "timestamp": 50, "status": "successful"}
{"from": "Dave", "to": "Jane", "amount": 200, "timestamp": 60, "status": "pending"}

Далее, приложение должно будет иметь функцию, которая может разрешить транзакций, проверяя каждую ожидающую транзакцию, чтобы убедиться, что она действителен, а затем обновляет свой статус с "ожидающего" до "успешного" или "Отклонен":

def resolve_transactions(target_timestamp):
    """ Resolves all transactions up to and including the transaction
        with timestamp `target_timestamp`. """
    while True:
        # Get the oldest transaction which is still pending
        txn = db.get("transactions/oldest-pending")
        if txn.timestamp > target_timestamp:
            # Stop once all of the transactions up until the one we're
            # interested in have been resolved.
            break

        # Then check to see if that transaction is valid
        if db.get("transactions/available-balance", id=txn.from) >= txn.amount:
            status = "successful"
        else:
            status = "rejected"

        # Then update the status of that transaction. Note that CouchDB
        # will check the "_rev" field, only performing the update if the
        # transaction hasn't already been updated.
        txn.status = status
        couch.put(txn)

Наконец, код приложения для правильной передачи:

def transfer(from_acct, to_acct, amount):
    timestamp = time.time()
    txn = db.post("transactions", {
        "from": from_acct,
        "to": to_acct,
        "amount": amount,
        "status": "pending",
        "timestamp": timestamp,
    })
    resolve_transactions(timestamp)
    txn = couch.get("transactions/" + txn._id)
    if txn_status == "rejected":
        raise InsufficientFunds()

Несколько примечаний:

  • Для краткости эта конкретная реализация предполагает некоторое количество атомарность в CouchDB map-reduce. Обновление кода, чтобы он не полагался на это предположение остается в качестве упражнения для читателя.

  • Репликация мастера/мастера или синхронизация документа CouchDB не были приняты рассмотрение. Репликация мастера и мастера и синхронизация делают эту проблему значительно сложнее.

  • В реальной системе использование time() может привести к конфликтам, поэтому использование что-то с немного большей энтропией может быть хорошей идеей; возможно, "%s-%s" %(time(), uuid()), или используя документ _id в заказе. Включение времени не является строго необходимым, но оно помогает поддерживать логическое если несколько запросов поступают примерно в одно и то же время.

Ответ 4

BerkeleyDB и LMDB являются хранилищами с ключевыми значениями с поддержкой транзакций ACID. В BDB txns являются необязательными, тогда как LMDB работает только транзакционно.

Ответ 5

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

Многие современные хранилища данных не поддерживают атомарные множественные обновления (транзакции) из коробки, но большинство из них предоставляют примитивы, которые позволяют вам создавать транзакции на стороне клиента ACID.

Если хранилище данных поддерживает каждую линеаризуемость ключа и операцию сравнения и замены или тестирования и установки, то этого достаточно для реализации сериализуемых транзакций. Например, этот подход используется в Google Percolator и в CockroachDB.

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

Среди хранилищ данных, которые поддерживают ключевую линеаризуемость и CAS:

  • Кассандра с облегченными транзакциями
  • Riak с согласованными ведрами
  • RethinkDB
  • Zookeeper
  • Etdc
  • HBase
  • DynamoDB
  • MongoDB

Кстати, если вы в порядке с уровнем изоляции Read Committed, тогда имеет смысл взглянуть на транзакции RAMP Питера Бейлиса, Они также могут быть реализованы для одного и того же набора хранилищ данных.