2009-07-07 3 views
17

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

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

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

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

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

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

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

+0

CouchDB не является ключом, это хранилище документов. – OrangeDog

ответ

10

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

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

+0

Спасибо, что объяснили это. Вы говорите, что «все еще очень эффективно» делать группу. Можете ли вы немного разобраться в этом? Для реляционных баз данных с высоким трафиком обычной практикой является денормализация столбца. Я могу себе представить, что CouchDB и другие хранилища существенно отличаются друг от друга, и это означает, что группировка транзакций может быть более эффективной. Но можете ли вы сделать это с 10 транзакциями для группировки? 100? 100000? – ChrisInEdmonton

+0

CouchDB использует парадокс Map/Reduce для просмотра документов в базе данных. Поскольку карта применяется только к измененным документам, ее (время) эффективность по существу равна O (1) в общем количестве документов, но O (n) в количестве измененных документов. Уменьшенные значения вычисляются и сохраняются в b-дереве. Очевидно, что все узлы, у которых есть дочерний документ, который изменился, должны быть пересчитаны. Таким образом, может потребоваться больше времени для сокращения. CouchDB был продемонстрирован в производстве со многими миллионами документов, поэтому я не думаю, что это будет проблемой в этом случае. –

+0

Спасибо. Кстати, я работаю на сайте социальной сети. Мы не планируем переходить на постоянный хранилище ключевых значений в среднесрочном будущем. Конечно, мы используем оверенные серверы баз данных MySQL и memcache. Похоже, наши занятые столы видели сотни миллионов строк, но ничего кроме этого. По вашим ответам, похоже, что CouchDB, по крайней мере, был специально разработан для обработки тех видов, которые, как я ожидал, появятся для такого сайта, как наш. Не слишком удивительно, но все же приятно слышать. Я уверен, что CouchDB и другие будут делать что-то лучше, а иногда - хуже. – ChrisInEdmonton

5

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

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

  1. Validate сделки, обеспечивая достаточное количество средств в исходном счете, что оба счета открыты, не заперта, и в хорошо стоя, и так далее
  2. Уменьшить баланс исходного счета
  3. Увеличение баланса счета получателя

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

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

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

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

+1

Это явно неверно: https://gist.github.com/wolever/1940301d4f7f530c0791 - он просто использует * другую * (хотя и значительно более сложную) модель транзакции. В то время как приложение типа «банковский перевод» требует атомных операций (на котором на кушетке есть: обновление документа с проверкой версии), блокировка не требуется. –

2

В качестве конкретного примера (потому что есть удивительное отсутствие правильных примеров онлайн): вот как реализовать «atomic bank balance transfer» в 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 
     } 
    ], 
    ... 
} 

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

С 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() 

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

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

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

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

  1. Время создания транзакции (чтобы убедиться, что существует strict total ordering транзакций) и

  2. Статус: была ли транзакция успешной.

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

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. Обновляя код, чтобы он не полагался на , это предположение остается в качестве упражнения для читателя.

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

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

1

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

1

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

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

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

В моем блоге я создал step-by-step visualization of serializable cross shard client-side transactions, описал основные варианты использования и предоставил ссылки на варианты алгоритма. Надеюсь, это поможет вам понять, как реализовать их для хранения данных.

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

  • Кассандру с легкими сделками
  • Riak с последовательными ведрами
  • RethinkDB
  • Zookeeper
  • Etdc
  • HBase
  • DynamoDB
  • MongoDB

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

Смежные вопросы