Czy mogę dokonywać transakcji i blokować w CouchDB?

81

Muszę wykonać transakcje (rozpocząć, zatwierdzić lub wycofać), blokady (wybrać do aktualizacji). Jak mogę to zrobić w bazie danych modelu dokumentu?

Edytować:

Sprawa jest taka:

  • Chcę prowadzić serwis aukcyjny.
  • I myślę też, jak skierować zakup.
  • W przypadku zakupu bezpośredniego muszę zmniejszyć pole ilości w rekordzie towaru, ale tylko wtedy, gdy ilość jest większa od zera. Dlatego potrzebuję zamków i transakcji.
  • Nie wiem, jak sobie z tym poradzić bez blokad i / lub transakcji.

Czy mogę rozwiązać ten problem za pomocą CouchDB?

user2427
źródło

Odpowiedzi:

145

Nie. CouchDB używa modelu „optymistycznej współbieżności”. Mówiąc najprościej, oznacza to po prostu, że wysyłasz wersję dokumentu wraz z aktualizacją, a CouchDB odrzuca zmianę, jeśli aktualna wersja dokumentu nie odpowiada temu, co wysłałeś.

To naprawdę zwodniczo proste. Możesz zmienić ramy wielu normalnych scenariuszy opartych na transakcjach dla CouchDB. Jednak podczas nauki CouchDB musisz wyrzucić swoją wiedzę o domenie RDBMS. Pomocne jest podejście do problemów z wyższego poziomu, zamiast próbować dopasować Couch do świata opartego na SQL.

Śledzenie zapasów

Problem, który nakreśliłeś, to przede wszystkim problem z zapasami. Jeśli masz dokument opisujący towar, który zawiera pole „Dostępna ilość”, możesz rozwiązać następujące problemy związane ze współbieżnością:

  1. Pobierz dokument, zanotuj _revwłaściwość, którą przesyła CouchDB
  2. Zmniejsz pole ilości, jeśli jest większe od zera
  3. Odeślij zaktualizowany dokument, korzystając z _revwłaściwości
  4. Jeśli numer _revpasuje do aktualnie zapisanego numeru, gotowe!
  5. Jeśli występuje konflikt (kiedy _revnie pasuje), pobierz najnowszą wersję dokumentu

W tym przypadku należy przemyśleć dwa możliwe scenariusze awarii. Jeśli najnowsza wersja dokumentu zawiera liczbę 0, postępujesz z nią tak, jak w przypadku systemu RDBMS i ostrzegasz użytkownika, że ​​nie może faktycznie kupić tego, co chciał kupić. Jeśli najnowsza wersja dokumentu zawiera liczbę większą niż 0, po prostu powtórz operację ze zaktualizowanymi danymi i zacznij od początku. To zmusza cię do wykonania nieco więcej pracy niż RDBMS i może być trochę irytujące, jeśli występują częste, sprzeczne aktualizacje.

Odpowiedź, której właśnie udzieliłem, zakłada, że ​​zamierzasz robić rzeczy w CouchDB w taki sam sposób, jak w RDBMS. Mogę podejść do tego problemu nieco inaczej:

Zacząłbym od dokumentu „produktu głównego”, który zawiera wszystkie dane deskryptora (nazwa, zdjęcie, opis, cena itp.). Następnie dodałbym dokument „kwitu inwentaryzacyjnego” dla każdej konkretnej instancji z polami product_keyi claimed_by. Jeśli sprzedajesz model młota i masz ich 20 do sprzedania, możesz mieć dokumenty z kluczami, takimi jak hammer-1:hammer-2 itd, aby reprezentować każdą dostępną młotek.

Następnie utworzyłbym widok, który daje mi listę dostępnych młotków z funkcją zmniejszania, która pozwala mi zobaczyć „sumę”. Są one całkowicie poza mankietem, ale powinny dać ci wyobrażenie o tym, jak wyglądałby widok roboczy.

Mapa

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

To daje mi listę dostępnych „biletów” według klucza produktu. Mógłbym złapać ich grupę, gdy ktoś chce kupić młotek, a następnie iterować, wysyłając aktualizacje (używając idi_rev ), aż z powodzeniem zdobędę jeden (wcześniej zgłoszone bilety spowodują błąd aktualizacji).

Zmniejszyć

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

Ta funkcja redukcji po prostu zwraca całkowitą liczbę nieodebranych inventory_ticket elementów, dzięki czemu można określić, ile „młotków” jest dostępnych do zakupu.

Ostrzeżenia

To rozwiązanie reprezentuje około 3,5 minuty całkowitego myślenia nad konkretnym problemem, który przedstawiłeś. Mogą istnieć lepsze sposoby na zrobienie tego! To powiedziawszy, znacznie zmniejsza liczbę konfliktów aktualizacji i ogranicza potrzebę reagowania na konflikt za pomocą nowej aktualizacji. W tym modelu wielu użytkowników nie będzie próbowało zmieniać danych w głównym wpisie produktu. W najgorszym przypadku wielu użytkowników będzie próbowało odebrać jeden bilet, a jeśli złapałeś kilka z nich z widoku, po prostu przejdź do następnego biletu i spróbuj ponownie.

Źródła: https://wiki.apache.org/couchdb/Frequently_asked_questions#How_do_I_use_transactions_with_CouchDB.3F

MrKurt
źródło
4
Nie jest dla mnie jasne, w jaki sposób posiadanie `` biletów '', które próbujesz przejąć po kolei, jest znaczącym ulepszeniem w porównaniu z prostą ponowną próbą odczytu / modyfikacji / zapisu w celu zaktualizowania jednostki głównej. Z pewnością nie wydaje się to warte dodatkowych kosztów ogólnych, zwłaszcza jeśli masz duże ilości zapasów.
Nick Johnson,
4
Z mojej perspektywy konwencja biletów jest „prostsza” do zbudowania. Nieudane aktualizacje wpisu głównego wymagają ponownego załadowania dokumentu, ponownego wykonania operacji, a następnie zapisania. Bilet umożliwia ci próbę „odebrania” czegoś bez konieczności żądania dodatkowych danych.
MrKurt,
Zależy to również od rodzaju kosztów ogólnych, o które się martwisz. Albo będziesz walczyć ze zwiększoną rywalizacją, albo będziesz mieć dodatkowe wymagania dotyczące przechowywania. Biorąc pod uwagę, że bilet może być również dowodem zakupu, nie wiem, czy byłby taki problem z przechowywaniem, jak myślisz.
MrKurt,
2
Edytuję pole ilości na dokumencie produktu. Następnie muszę utworzyć tysiące „biletów”, jeśli na przykład ilość = 2K. Następnie zmniejszam ilość, muszę usunąć niektóre bilety. Dla mnie brzmi to zupełnie niefrasobliwie. Dużo bólu głowy w podstawowych przypadkach użycia. Może czegoś mi brakuje, ale dlaczego nie przywrócić wcześniej usuniętego zachowania transakcji, po prostu uczynić go opcjonalnym za pomocą czegoś takiego jak _bulk_docs? Całkiem przydatne w konfiguracjach z jednym wzorcem.
Sam
3
@mehaase: Przeczytaj to: guide.couchdb.org/draft/recipes.html , odpowiedź sprowadza się do wewnętrznej struktury danych couchdb „nigdy nie zmieniasz danych, po prostu dodajesz nowe”. W twoim scenariuszu oznacza to utworzenie jednej (niepodzielnej) transakcji z konta na konto w tranzycie dla obciążenia i drugiej (niepodzielnej) transakcji z konta w tranzycie w przód (lub w tył). Tak robią prawdziwe banki. Każdy krok jest zawsze dokumentowany.
Fabian Zeindl,
26

Poszerzenie odpowiedzi MrKurt. W przypadku wielu scenariuszy nie musisz zamawiać w kolejności biletów giełdowych. Zamiast wybierać pierwszy bilet, możesz wybrać losowo spośród pozostałych biletów. Biorąc pod uwagę dużą liczbę biletów i dużą liczbę jednoczesnych żądań, uzyskasz znacznie mniejszą rywalizację o te bilety w porównaniu do wszystkich, którzy próbują zdobyć pierwszy bilet.

Kerr
źródło
21

Wzorzec projektowy dla niespełnionych transakcji polega na stworzeniu „napięcia” w systemie. W popularnym przykładzie użycia transakcji na koncie bankowym musisz zaktualizować sumę dla obu zaangażowanych kont:

  • Utwórz dokument transakcji „prześlij 10 USD z konta 11223 na konto 88733”. Stwarza to napięcie w systemie.
  • Aby rozwiązać problem skanowania napięcia dla wszystkich dokumentów transakcji i
    • Jeśli konto źródłowe nie zostało jeszcze zaktualizowane, zaktualizuj konto źródłowe (-10 USD)
    • Jeśli rachunek źródłowy został zaktualizowany, ale dokument transakcji tego nie pokazuje, zaktualizuj dokument transakcji (np. Ustaw flagę „sourcedone” w dokumencie)
    • Jeśli konto docelowe nie zostało jeszcze zaktualizowane, zaktualizuj konto docelowe (+10 USD)
    • Jeśli rachunek docelowy został zaktualizowany, ale dokument transakcji tego nie pokazuje, zaktualizuj dokument transakcji
    • Jeśli zaktualizowano oba konta, możesz usunąć dokument transakcji lub zachować go do audytu.

Skanowanie w poszukiwaniu napięcia powinno być wykonywane w procesie zaplecza dla wszystkich „dokumentów dotyczących napięcia”, aby skrócić czasy napięć w systemie. W powyższym przykładzie przewidywana niespójność w krótkim czasie wystąpi, gdy pierwsze konto zostanie zaktualizowane, a drugie nie zostanie jeszcze zaktualizowane. Należy to wziąć pod uwagę w ten sam sposób, w jaki będziesz radzić sobie z ostateczną konsekwencją, jeśli Twoja Couchdb jest dystrybuowana.

Inna możliwa implementacja pozwala całkowicie uniknąć transakcji: po prostu przechowuj dokumenty dotyczące napięcia i oceń stan swojego systemu, oceniając każdy dokument dotyczący napięcia. W powyższym przykładzie oznaczałoby to, że suma dla rachunku jest określana tylko jako suma wartości w dokumentach transakcji, w których ten rachunek jest zaangażowany. W Couchdb można to bardzo ładnie modelować jako widok mapy / zmniejszania.

ordnungswidrig
źródło
5
Ale co z przypadkami, w których konto jest obciążane, ale dokument napięcia nie jest zmieniany? Każdy scenariusz awarii między tymi dwoma punktami, jeśli nie są atomowe, spowoduje trwałą niespójność, prawda? Coś w tym procesie musi być atomowe, o to chodzi w transakcji.
Ian Varley
Tak, masz rację, w tym przypadku - dopóki napięcie nie zostanie rozwiązane - pojawi się niespójność. Jednak niespójność jest tylko tymczasowa, dopóki nie wykryje tego następny skan dokumentów dotyczących napięcia. Na tym polega handel w tym przypadku, rodzaj ostatecznej konsekwencji w odniesieniu do czasu. Dopóki najpierw dekretujesz konto źródłowe, a później zwiększasz konto docelowe, jest to dopuszczalne. Ale uwaga: dokumenty dotyczące napięć nie dają transakcji ACID oprócz REST. Ale mogą być dobrym kompromisem między czystym REST a ACID.
ordnungswidrig
4
Wyobraź sobie, że każdy dokument dotyczący napięcia ma znacznik czasu, a dokumenty konta mają pole „ostatnio zastosowane napięcie” - lub listę zastosowanych napięć. Obciążając konto źródłowe, aktualizujesz również pole „ostatnio zastosowane napięcie”. Te dwie operacje są niepodzielne, ponieważ dotyczą tego samego dokumentu. Konto docelowe również ma podobne pole. W ten sposób system zawsze może stwierdzić, które dokumenty dotyczące napięcia zostały zastosowane do których kont.
Jesse Hallett
1
Jak wykryć, czy dokument źródłowy / docelowy został już zaktualizowany? Co się stanie, jeśli zakończy się niepowodzeniem po kroku 1, a następnie zostanie ponownie wykonany i ponownie się nie powiedzie, i tak dalej, będziesz nadal odliczać konto źródłowe?
wump
1
@wump: będziesz musiał odnotować, że dokument dotyczący napięcia został zastosowany na koncie. np. przez dołączenie identyfikatora dokumentu napięcia do właściwości listy dowolnego konta. kiedy wszystkie konta, których dotyczył dokument napięcia, zostały zaktualizowane, zaznacz ten dokument jako „zakończony” lub usuń go. Następnie identyfikator dokumentu można usunąć z listy dla wszystkich rachunków.
ordnungswidrig
6

Nie, CouchDB generalnie nie nadaje się do aplikacji transakcyjnych, ponieważ nie obsługuje operacji atomowych w środowisku klastrowym / replikowanym.

CouchDB poświęcił możliwości transakcyjne na rzecz skalowalności. Aby mieć atomowe operacje, potrzebujesz centralnego systemu koordynacji, który ogranicza twoją skalowalność.

Jeśli możesz zagwarantować, że masz tylko jedną instancję CouchDB lub że wszyscy modyfikujący określony dokument łączą się z tą samą instancją CouchDB, możesz użyć systemu wykrywania konfliktów do stworzenia pewnego rodzaju atomowości przy użyciu metod opisanych powyżej, ale jeśli później skalujesz w górę do klastra lub skorzystaj z usługi hostowanej, takiej jak Cloudant, zepsuje się i będziesz musiał przerobić tę część systemu.

Tak więc moja sugestia byłaby taka, aby użyć czegoś innego niż CouchDB do sald konta, w ten sposób będzie o wiele łatwiej.

Dobes Vandermeer
źródło
5

W odpowiedzi na problem OP Couch prawdopodobnie nie jest tutaj najlepszym wyborem. Korzystanie z widoków to świetny sposób na śledzenie zapasów, ale ograniczenie do 0 jest mniej więcej niemożliwe. Problemem jest stan wyścigu, kiedy czytasz wynik widoku, decydujesz, że możesz użyć elementu „młotek-1”, a następnie napisz dokument, aby go użyć. Problem polega na tym, że nie ma atomowego sposobu, aby napisać dokument tylko po to, aby użyć młotka, jeśli wynikiem widoku jest> 0 młotków-1. Jeśli 100 użytkowników jednocześnie zapyta widok i zobaczy 1 młotek-1, wszyscy mogą napisać dokument, aby użyć młotka 1, co daje -99 młotków-1. W praktyce stan wyścigu będzie dość mały - naprawdę mały, jeśli twoja baza danych działa na serwerze lokalnym. Ale po skalowaniu i posiadaniu zewnętrznego serwera bazy danych lub klastra problem stanie się znacznie bardziej zauważalny.

Aktualizacja odpowiedzi MrKurt (może być po prostu datowana lub mógł nie wiedzieć o niektórych funkcjach CouchDB)

Widok to dobry sposób na obsługę takich rzeczy, jak salda / zapasy w CouchDB.

Nie musisz emitować dokumentu docid i rev w widoku. Przy pobieraniu wyników widoku oba te elementy są bezpłatne. Emitowanie ich - zwłaszcza w formacie rozwlekłym, takim jak słownik - spowoduje niepotrzebne powiększenie widoku.

Prosty widok do śledzenia sald zapasów powinien wyglądać bardziej tak (również z góry mojej głowy)

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

Funkcja redukcji jest jeszcze prostsza

_sum

Wykorzystuje wbudowaną funkcję redukcji która po prostu sumuje wartości wszystkich wierszy z pasującymi kluczami.

W tym widoku każdy dokument może mieć element „InventoryChange”, który odwzorowuje klucz produktu na zmianę w jego całkowitym stanie magazynowym. to znaczy.

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

Dodałby 10 hammer_1234's i 25 saw_4321's.

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

Spaliłby 5 młotów z inwentarza.

W tym modelu nigdy nie aktualizujesz żadnych danych, a jedynie dodajesz. Oznacza to, że nie ma możliwości wystąpienia konfliktów aktualizacji. Wszystkie problemy transakcyjne związane z aktualizacją danych odchodzą :)

Kolejną fajną rzeczą w tym modelu jest to, że KAŻDY dokument w bazie danych może zarówno dodawać, jak i odejmować pozycje z inwentarza. Dokumenty te mogą zawierać różnego rodzaju inne dane. Możesz mieć dokument „Wysyłka” zawierający zbiór danych o dacie i godzinie odbioru, magazynie, odbierającym pracowniku itp. I tak długo, jak ten dokument określa InventoryChange, zaktualizuje on stan zapasów. Podobnie jak dokument „Sale”, dokument „DamagedItem” itp. Patrząc na każdy dokument, czytają bardzo wyraźnie. A widok radzi sobie z całą ciężką pracą.

Wallacer
źródło
Ciekawa strategia. Jako nowość w CouchDB wydawałoby się, że aby obliczyć bieżącą liczbę młotów, musisz wykonać mapę / zmniejszyć całą historię zmian zapasów młotów w firmie . Może to zająć lata zmian. Czy jest jakaś wbudowana funkcja CouchDB, która sprawi, że będzie to wydajne?
chadrik
Tak, widoki w CouchDB są jak ciągła, trwała mapa / redukcja. Masz rację, że zrobienie tego od zera na dużym zestawie danych zajęłoby wieki, ale kiedy nowe dokumenty są dodawane, aktualizują tylko istniejący widok, nie trzeba przeliczać całego widoku. Należy pamiętać, że widoki wymagają zarówno miejsca, jak i procesora. Poza tym, przynajmniej kiedy profesjonalnie pracowałem z CouchDB (minęło kilka lat), bardzo ważne było, aby używać tylko wbudowanych funkcji redukujących, tj. _suma. Niestandardowe funkcje redukujące JavaScript działały bardzo wolno
wallacer
3

Właściwie w pewnym sensie możesz. Przyjrzyj się interfejsowi API dokumentów HTTP i przewiń w dół do nagłówka „Modyfikuj wiele dokumentów za pomocą jednego żądania”.

Zasadniczo możesz utworzyć / zaktualizować / usunąć kilka dokumentów w jednym żądaniu postu do URI / {dbname} / _ bulk_docs i albo wszystkie się powiodą, albo wszystkie nie. Dokument ostrzega jednak, że to zachowanie może ulec zmianie w przyszłości.

EDYCJA: Zgodnie z przewidywaniami, od wersji 0.9 dokumenty zbiorcze nie działają już w ten sposób.

Evan
źródło
To nie pomogłoby w omawianej sytuacji, tj. Sporze o pojedyncze dokumenty od wielu użytkowników.
Kerr,
3
Począwszy od CouchDB 0.9, zmieniła się semantyka aktualizacji zbiorczych.
Barry Wark
0

Po prostu użyj lekkiego rozwiązania SQlite do transakcji, a gdy transakcja zostanie zakończona pomyślnie, zreplikuj ją i oznacz jako replikowaną w SQLite

Tabela SQLite

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

Możesz także usunąć transakcje, które zostały pomyślnie zreplikowane.

OldGaurd01
źródło