Могу ли я выполнять транзакции и блокировки в CouchDB?

81

Мне нужно выполнить транзакции (начать, зафиксировать или откатить), заблокировать (выбрать для обновления). Как я могу сделать это в базе данных модели документа?

Редактировать:

Дело вот в чем:

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

Могу ли я решить эту проблему с помощью CouchDB?

user2427
источник

Ответы:

145

Нет. CouchDB использует модель «оптимистичного параллелизма». Проще говоря, это просто означает, что вы отправляете версию документа вместе со своим обновлением, и CouchDB отклоняет изменение, если текущая версия документа не соответствует отправленной вами.

На самом деле это обманчиво просто. Вы можете переосмыслить многие обычные сценарии на основе транзакций для CouchDB. Тем не менее, при изучении CouchDB вам нужно как бы выбросить свои знания в области СУБД. Полезно подходить к проблемам с более высокого уровня, а не пытаться приспособить Couch к миру, основанному на SQL.

Отслеживание инвентаря

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

  1. Получите документ, обратите внимание на _revсвойство, которое CouchDB отправляет вместе
  2. Уменьшите значение поля количества, если оно больше нуля
  3. Отправьте обновленный документ обратно, используя _revсвойство
  4. Если _revсовпадает с текущим сохраненным номером, готово!
  5. Если есть конфликт (когда _revне совпадает), получите самую новую версию документа

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

Итак, ответ, который я только что дал, предполагает, что вы собираетесь делать что-то в CouchDB почти так же, как в РСУБД. Я мог бы подойти к этой проблеме несколько иначе:

Я бы начал с основного документа продукта, который включает все данные дескриптора (имя, изображение, описание, цену и т. Д.). Затем я бы добавил документ «инвентарный билет» для каждого конкретного экземпляра с полями для product_keyи claimed_by. Если вы продаете модель молотка, и у вас есть 20 штук на продажу, у вас могут быть документы с ключами, такими как hammer-1, hammer-2и т.д., для представления каждого доступного молотка.

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

карта

function(doc) 
{ 
    if (doc.type == 'inventory_ticket' && doc.claimed_by == null ) { 
        emit(doc.product_key, { 'inventory_ticket' :doc.id, '_rev' : doc._rev }); 
    } 
}

Это дает мне список доступных «билетов» по ​​ключам продукта. Я мог бы взять группу из них, когда кто-то хочет купить молоток, а затем итеративно отправлять обновления (используя idи _rev), пока я не востребую один (ранее заявленные билеты приведут к ошибке обновления).

Уменьшить

function (keys, values, combine) {
    return values.length;
}

Эта функция сокращения просто возвращает общее количество невостребованных inventory_ticketпредметов, поэтому вы можете определить, сколько «молотков» доступно для покупки.

Предостережения

Это решение представляет собой примерно 3,5 минуты на обдумывание конкретной проблемы, которую вы представили. Могут быть способы сделать это лучше! Тем не менее, это значительно снижает количество конфликтующих обновлений и снижает необходимость реагировать на конфликт с помощью нового обновления. В соответствии с этой моделью у вас не будет нескольких пользователей, пытающихся изменить данные в первичной записи продукта. В самом худшем случае у вас будет несколько пользователей, пытающихся потребовать один билет, и если вы захватили несколько из них, вы просто переходите к следующему билету и повторите попытку.

Ссылка: https://wiki.apache.org/couchdb/Frequent_asked_questions#How_do_I_use_transactions_with_CouchDB.3F

MrKurt
источник
4
Мне непонятно, как наличие «билетов», которые вы пытаетесь запросить последовательно, является значительным улучшением по сравнению с простой повторной попыткой чтения / изменения / записи для обновления основного объекта. Конечно, это не кажется стоящим дополнительных накладных расходов, особенно если у вас есть большие запасы.
Ник Джонсон,
4
С моей точки зрения, билетную конвенцию «проще» построить. Неудачные обновления основной записи требуют перезагрузки документа, повторного выполнения операции и сохранения. Билет позволяет вам попробовать что-то «потребовать», не запрашивая дополнительных данных.
MrKurt
Кроме того, это зависит от того, какие накладные расходы вас беспокоят. Вы либо будете бороться с повышенной конкуренцией, либо будете иметь дополнительные требования к хранилищу. Учитывая, что билет также может использоваться как запись о покупке, я не знаю, возникнет ли такая проблема с хранением, как вы думаете.
MrKurt
2
Я редактирую поле количества в товарном документе. Затем я должен создать тысячи «билетов», например, если количество = 2К. Затем я уменьшаю количество, я должен удалить несколько билетов. Для меня это совершенно не расслабляет. В базовых сценариях использования много головной боли. Может быть, мне чего-то не хватает, но почему бы не вернуть ранее удаленное поведение транзакции, просто сделав его необязательным с помощью чего-то вроде _bulk_docs? Reject_on_conflict = true. Очень полезно в конфигурациях с одним мастером.
Сэм
3
@mehaase: Прочтите это: guide.couchdb.org/draft/recipes.html , ответ сводится к внутренней структуре данных couchdb «вы никогда не меняете данные, вы просто добавляете новые». В вашем сценарии это означает создание одной (атомарной) транзакции от учетной записи к промежуточной учетной записи для дебетования и второй (атомарной) транзакции с промежуточной учетной записи вперед (или назад). Вот как это делают настоящие банки. Каждый шаг всегда документируется.
Fabian Zeindl
26

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

Керр
источник
21

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

  • Создайте документ транзакции «Переведите 10 долларов США со счета 11223 на счет 88733». Это создает напряжение в системе.
  • Чтобы разрешить любое сканирование напряжения для всех документов транзакции
    • Если исходная учетная запись еще не обновлена, обновите исходную учетную запись (-10 долларов США)
    • Если исходная учетная запись была обновлена, но документ транзакции этого не показывает, обновите документ транзакции (например, установите в документе флаг «sourcedone»).
    • Если целевая учетная запись еще не обновлена, обновите целевую учетную запись (+10 долларов США)
    • Если целевая учетная запись была обновлена, но документ транзакции не показывает этого, обновите документ транзакции
    • Если оба счета были обновлены, вы можете удалить документ транзакции или оставить его для аудита.

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

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

ordnungswidrig
источник
5
Но как быть в случаях, когда счет списывается, но документ напряжения не изменяется? Любой сценарий отказа между этими двумя точками, если они не являются атомарными, вызовет постоянное несоответствие, верно? Что-то в процессе должно быть атомарным, в этом суть транзакции.
Ян Варлей
Да, вы правы, в этом случае - пока напряжение не снято - будет непоследовательность. Однако несоответствие носит временный характер, пока это не будет обнаружено при следующем сканировании документов с напряжением. В данном случае это торговля некой возможной согласованности во времени. Если вы сначала уменьшите исходную учетную запись, а затем увеличите целевую учетную запись, это может быть приемлемо. Но будьте осторожны: документы о напряжении не будут предоставлять вам транзакции ACID поверх REST. Но они могут быть хорошим компромиссом между чистым REST и ACID.
ordnungswidrig 02
4
Представьте, что у каждого документа напряжения есть отметка времени, а у учетных документов есть поле «последнее приложенное напряжение» или список примененных напряжений. При дебетовании исходного счета вы также обновляете поле «Последнее натяжение». Эти две операции являются атомарными, потому что они находятся в одном документе. Целевая учетная запись также имеет аналогичное поле. Таким образом, система всегда может определить, какие документы напряжения были применены к каким учетным записям.
Джесси Халлетт,
1
Как определить, был ли уже обновлен исходный / целевой документ? Что, если после шага 1 произойдет сбой, затем произойдет повторное выполнение и снова произойдет сбой, и так далее, вы продолжите вычитать исходную учетную запись?
wump
1
@wump: вам нужно будет записать, что документ напряжения был применен к учетной записи. например, добавив идентификатор документа напряжения в свойство списка любой учетной записи. когда все учетные записи, затронутые документом натяжения, будут обновлены, отметьте документ натяжения как «готово» или удалите его. После этого идентификатор документа можно будет удалить из списка для всех учетных записей.
ordnungswidrig
6

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

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

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

Итак, я предлагаю использовать что-то другое, кроме CouchDB для остатков на вашем счете, так будет намного проще.

Добс Вандермейер
источник
5

Как ответ на проблему OP, Couch, вероятно, здесь не лучший выбор. Использование представлений - отличный способ отслеживать инвентарь, но ограничение до 0 более или менее невозможно. Проблема заключается в состоянии гонки, когда вы читаете результат представления, решаете, что можете использовать элемент «молоток-1», а затем пишете документ для его использования. Проблема в том, что нет атомарного способа написать документ только для использования молотка, если в результате просмотра имеется> 0 молотков-1. Если все 100 пользователей одновременно запрашивают представление и видят 1 молоток-1, все они могут написать документ, чтобы использовать молоток 1, в результате чего получится -99 молотков-1. На практике состояние гонки будет довольно небольшим - действительно небольшим, если ваша БД работает под управлением localhost. Но после масштабирования и создания внешнего сервера или кластера БД проблема станет гораздо более заметной.

Обновление ответа MrKurt (возможно, оно просто датировано, или он мог не знать о некоторых функциях CouchDB)

Представление - это хороший способ управлять такими вещами, как балансы / инвентарь в CouchDB.

Вам не нужно использовать docid и rev в представлении. Вы получаете и то, и другое бесплатно, когда получаете результаты просмотра. Их использование - особенно в подробном формате, таком как словарь, - просто увеличит ваш обзор излишне большим.

Простое представление для отслеживания остатков инвентаря должно выглядеть примерно так (тоже не в моей голове)

function( doc )
{
    if( doc.InventoryChange != undefined ) {
        for( product_key in doc.InventoryChange ) {
            emit( product_key, 1 );
        }
    }
}

А функция уменьшения еще проще

_sum

При этом используется встроенная функция сокращения, которая просто суммирует значения всех строк с соответствующими ключами.

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

{
    "_id": "abc123",
    "InventoryChange": {
         "hammer_1234": 10,
         "saw_4321": 25
     }
}

Добавил бы 10 hammer_1234 и 25 saw_4321.

{
    "_id": "def456",
    "InventoryChange": {
        "hammer_1234": -5
    }
}

Сжечь 5 молотков из инвентаря.

В этой модели вы никогда не обновляете данные, а только добавляете. Это означает, что нет возможности для конфликтов обновлений. Уходят все транзакционные проблемы обновления данных :)

Еще одна приятная особенность этой модели заключается в том, что ЛЮБОЙ документ в БД может как добавлять, так и вычитать элементы из инвентаря. Эти документы могут содержать любые другие данные. У вас может быть документ «Отгрузка» с кучей данных о дате и времени получения, складе, принимающем сотруднике и т. Д., И пока этот документ определяет InventoryChange, он будет обновлять инвентарь. Как и документ "Продажа", документ "DamagedItem" и т. Д. Глядя на каждый документ, они читаются очень четко. А вид выполняет всю тяжелую работу.

Wallacer
источник
Интересная стратегия. Как новичок в CouchDB, может показаться, что для расчета текущего количества молотов вам необходимо выполнить сопоставление / сокращение всей истории изменений запасов молотов компании. На это могут потребоваться годы изменений. Есть ли какая-то встроенная функция CouchDB, которая сделает эту работу производительной?
chadrik 05
Да, представления в CouchDB похожи на непрерывное, постоянное отображение / сокращение. Вы правы, чтобы сделать это с нуля на большом наборе данных, потребуется много времени, но когда добавляются новые документы, они обновляют только существующее представление, не нужно пересчитывать все представление. Имейте в виду, что для представлений требуется как пространство, так и ЦП. Кроме того, по крайней мере, когда я профессионально работал с CouchDB (это было несколько лет), было очень важно использовать только встроенные функции сокращения, т.е. _sum. Пользовательские функции сокращения Javascript были чрезвычайно медленными
wallacer
3

На самом деле, вы можете в некотором роде. Взгляните на HTTP Document API и прокрутите вниз до заголовка «Изменить несколько документов с помощью одного запроса».

По сути, вы можете создать / обновить / удалить кучу документов в одном почтовом запросе к URI / {dbname} / _ bulk_docs, и все они либо будут успешными, либо все не удастся. Однако документ предупреждает, что это поведение может измениться в будущем.

РЕДАКТИРОВАТЬ: как и ожидалось, начиная с версии 0.9 объемная документация больше не работает таким образом.

Эван
источник
Это не очень поможет в обсуждаемой ситуации, например, в разногласиях по отдельным документам от нескольких пользователей.
Керр,
3
Начиная с CouchDB 0.9, семантика массовых обновлений изменилась.
Barry Wark,
0

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

Таблица SQLite

txn_id    , txn_attribute1, txn_attribute2,......,txn_status
dhwdhwu$sg1   x                    y               added/replicated

Вы также можете удалить транзакции, которые успешно реплицируются.

СтарыйGaurd01
источник