Kann ich Transaktionen und Sperren in CouchDB durchführen?

81

Ich muss Transaktionen durchführen (beginnen, festschreiben oder zurücksetzen), sperren (zum Aktualisieren auswählen). Wie kann ich das in einer Dokumentmodell-Datenbank machen?

Bearbeiten:

Der Fall ist folgender:

  • Ich möchte eine Auktionsseite betreiben.
  • Und ich denke auch, wie man direkt einkauft.
  • Bei einem Direktkauf muss ich das Mengenfeld im Artikeldatensatz dekrementieren, aber nur, wenn die Menge größer als Null ist. Deshalb brauche ich Sperren und Transaktionen.
  • Ich weiß nicht, wie ich das ohne Sperren und / oder Transaktionen angehen soll.

Kann ich das mit CouchDB lösen?

user2427
quelle

Antworten:

143

Nein. CouchDB verwendet ein "optimistisches Parallelitäts" -Modell. Im einfachsten Sinne bedeutet dies nur, dass Sie eine Dokumentversion zusammen mit Ihrem Update senden und CouchDB die Änderung ablehnt, wenn die aktuelle Dokumentversion nicht mit der von Ihnen gesendeten übereinstimmt.

Es ist wirklich täuschend einfach. Sie können viele normale transaktionsbasierte Szenarien für CouchDB neu gestalten. Sie müssen jedoch Ihr RDBMS-Domänenwissen irgendwie wegwerfen, wenn Sie CouchDB lernen. Es ist hilfreich, Probleme von einer höheren Ebene aus anzugehen, anstatt zu versuchen, Couch an eine SQL-basierte Welt anzupassen.

Inventar verfolgen

Das von Ihnen beschriebene Problem ist in erster Linie ein Inventarproblem. Wenn Sie ein Dokument haben, das einen Artikel beschreibt und ein Feld für "verfügbare Menge" enthält, können Sie Probleme mit der Parallelität wie folgt behandeln:

  1. Rufen Sie das Dokument ab und notieren Sie sich die _revEigenschaft, die CouchDB mitsendet
  2. Dekrementieren Sie das Mengenfeld, wenn es größer als Null ist
  3. Senden Sie das aktualisierte Dokument mithilfe der _revEigenschaft zurück
  4. Wenn das _revmit der aktuell gespeicherten Nummer übereinstimmt, fertig!
  5. Wenn ein Konflikt vorliegt (wenn er _revnicht übereinstimmt), rufen Sie die neueste Dokumentversion ab

In diesem Fall müssen zwei mögliche Fehlerszenarien berücksichtigt werden. Wenn die neueste Dokumentversion eine Menge von 0 hat, behandeln Sie diese wie in einem RDBMS und warnen den Benutzer, dass er nicht kaufen kann, was er kaufen möchte. Wenn die neueste Dokumentversion eine Menge größer als 0 hat, wiederholen Sie einfach den Vorgang mit den aktualisierten Daten und beginnen wieder am Anfang. Dies zwingt Sie dazu, etwas mehr Arbeit zu leisten als ein RDBMS und kann bei häufigen, widersprüchlichen Updates etwas ärgerlich werden.

Die Antwort, die ich gerade gegeben habe, setzt voraus, dass Sie die Dinge in CouchDB genauso machen wie in einem RDBMS. Ich könnte dieses Problem etwas anders angehen:

Ich würde mit einem "Master-Produkt" -Dokument beginnen, das alle Deskriptordaten (Name, Bild, Beschreibung, Preis usw.) enthält. Dann würde ich für jede spezifische Instanz ein "Inventarticket" -Dokument mit Feldern für product_keyund hinzufügen claimed_by. Wenn Sie ein Modell von Hammer verkaufen, und haben 20 von ihnen zu verkaufen, könnten Sie Dokumente mit Tasten wie hammer-1, hammer-2usw., um jeden verfügbaren Hammer darzustellen.

Dann würde ich eine Ansicht erstellen, die mir eine Liste der verfügbaren Hämmer mit einer Reduzierungsfunktion gibt, mit der ich eine "Summe" sehen kann. Diese sind völlig aus der Manschette, sollten Ihnen aber eine Vorstellung davon geben, wie eine Arbeitsansicht aussehen würde.

Karte

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

Dies gibt mir eine Liste der verfügbaren "Tickets" nach Produktschlüssel. Ich könnte mir eine Gruppe davon schnappen, wenn jemand einen Hammer kaufen möchte, und dann durch das Senden von Updates (mit idund _rev) iterieren, bis ich eines erfolgreich beanspruche (zuvor beanspruchte Tickets führen zu einem Update-Fehler).

Reduzieren

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

Diese Reduzierungsfunktion gibt einfach die Gesamtzahl der nicht beanspruchten inventory_ticketArtikel zurück, sodass Sie feststellen können, wie viele "Hämmer" zum Kauf verfügbar sind.

Vorsichtsmaßnahmen

Diese Lösung entspricht ungefähr 3,5 Minuten Gesamtdenken für das von Ihnen vorgestellte Problem. Es gibt vielleicht bessere Möglichkeiten, dies zu tun! Das heißt, es reduziert widersprüchliche Updates erheblich und verringert die Notwendigkeit, auf einen Konflikt mit einem neuen Update zu reagieren. Bei diesem Modell werden nicht mehrere Benutzer versuchen, Daten im primären Produkteintrag zu ändern. Im schlimmsten Fall versuchen mehrere Benutzer, ein einzelnes Ticket zu beanspruchen. Wenn Sie mehrere davon aus Ihrer Sicht ausgewählt haben, fahren Sie einfach mit dem nächsten Ticket fort und versuchen es erneut.

Referenz: https://wiki.apache.org/couchdb/Frequently_asked_questions#How_do_I_use_transactions_with_CouchDB.3F

MrKurt
quelle
4
Mir ist nicht klar, dass das Vorhandensein von Tickets, die Sie nacheinander beanspruchen möchten, eine erhebliche Verbesserung gegenüber dem einfachen erneuten Versuch des Lesens / Änderns / Schreibens zum Aktualisieren der Master-Entität darstellt. Sicherlich scheint es den zusätzlichen Aufwand nicht wert zu sein, besonders wenn Sie große Mengen an Lagerbeständen haben.
Nick Johnson
4
Aus meiner Sicht ist die Ticketkonvention "einfacher" zu erstellen. Bei fehlgeschlagenen Aktualisierungen des Haupteintrags müssen Sie das Dokument neu laden, den Vorgang erneut ausführen und dann speichern. Mit dem Ticket können Sie versuchen, etwas zu "beanspruchen", ohne weitere Daten anfordern zu müssen.
MrKurt
Es hängt auch davon ab, um welche Art von Overhead Sie sich Sorgen machen. Sie werden entweder mit zunehmender Konkurrenz kämpfen oder zusätzliche Speicheranforderungen haben. Angesichts der Tatsache, dass ein Ticket auch als Kaufdatensatz dienen kann, weiß ich nicht, dass es so viele Speicherprobleme geben würde, wie Sie denken.
MrKurt
2
Ich bearbeite ein Mengenfeld eines Produktdokuments. Dann muss ich Tausende von "Tickets" erstellen, wenn zum Beispiel Menge = 2K. Dann reduziere ich eine Menge, ich muss einige Tickets löschen. Klingt für mich völlig entspannt. Viel Kopfschmerz in grundlegenden Anwendungsfällen. Vielleicht fehlt mir etwas, aber warum nicht das zuvor entfernte Transaktionsverhalten zurückbringen, es einfach mit etwas wie _bulk_docs optional machen? Reject_on_conflict = true. Sehr nützlich in Single-Master-Konfigurationen.
Sam
3
@mehaase: Lesen Sie dies: guide.couchdb.org/draft/recipes.html , die Antwort läuft auf die interne Datenstruktur von couchdb hinaus: "Sie ändern niemals Daten, Sie hängen nur neue an". In Ihrem Szenario bedeutet dies, dass Sie eine (atomare) Transaktion vom Konto auf ein Transitkonto für die Lastschrift und eine zweite (atomare) Transaktion vom Transitkonto vorwärts (oder rückwärts) erstellen. So machen es echte Banken. Jeder Schritt ist immer dokumentiert.
Fabian Zeindl
26

Erweiterung der Antwort von MrKurt. Für viele Szenarien müssen Sie keine Lagertickets einlösen. Anstatt das erste Ticket auszuwählen, können Sie zufällig aus den verbleibenden Tickets auswählen. Bei einer großen Anzahl von Tickets und einer großen Anzahl von gleichzeitigen Anfragen werden Sie weniger Konkurrenz zu diesen Tickets haben als alle anderen, die versuchen, das erste Ticket zu erhalten.

Kerr
quelle
21

Ein Entwurfsmuster für erholsame Transaktionen besteht darin, eine "Spannung" im System zu erzeugen. Für den beliebten Anwendungsbeispiel einer Bankkontotransaktion müssen Sie sicherstellen, dass die Gesamtsumme für beide beteiligten Konten aktualisiert wird:

  • Erstellen Sie ein Transaktionsdokument "Überweisen Sie USD 10 von Konto 11223 auf Konto 88733". Dies erzeugt die Spannung im System.
  • So lösen Sie Spannungsspannungen für alle Transaktionsdokumente und
    • Wenn das Quellkonto noch nicht aktualisiert wurde, aktualisieren Sie das Quellkonto (-10 USD).
    • Wenn das Quellkonto aktualisiert wurde, das Transaktionsdokument dies jedoch nicht anzeigt, aktualisieren Sie das Transaktionsdokument (z. B. setzen Sie das Flag "Sourcedone" im Dokument).
    • Wenn das Zielkonto noch nicht aktualisiert wurde, aktualisieren Sie das Zielkonto (+10 USD).
    • Wenn das Zielkonto aktualisiert wurde, das Transaktionsdokument dies jedoch nicht anzeigt, aktualisieren Sie das Transaktionsdokument
    • Wenn beide Konten aktualisiert wurden, können Sie das Transaktionsdokument löschen oder zur Prüfung aufbewahren.

Das Scannen nach Spannung sollte in einem Backend-Prozess für alle "Spannungsdokumente" durchgeführt werden, um die Spannungszeiten im System kurz zu halten. Im obigen Beispiel wird eine kurze erwartete Inkonsistenz auftreten, wenn das erste Konto aktualisiert wurde, das zweite jedoch noch nicht aktualisiert wurde. Dies muss auf die gleiche Weise berücksichtigt werden, wie Sie mit eventueller Konsistenz umgehen, wenn Ihre Couchdb verteilt wird.

Eine andere mögliche Implementierung vermeidet die Notwendigkeit von Transaktionen vollständig: Speichern Sie einfach die Spannungsdokumente und bewerten Sie den Status Ihres Systems, indem Sie alle beteiligten Spannungsdokumente bewerten. Im obigen Beispiel würde dies bedeuten, dass die Gesamtsumme für ein Konto nur als die Summenwerte in den Transaktionsdokumenten bestimmt wird, an denen dieses Konto beteiligt ist. In Couchdb können Sie dies sehr gut als Karten- / Verkleinerungsansicht modellieren.

Besitzwidrig
quelle
5
Aber was ist mit Fällen, in denen das Konto belastet wird, der Spannungsdokument jedoch nicht geändert wird? Jedes Fehlerszenario zwischen diesen beiden Punkten führt zu dauerhaften Inkonsistenzen, wenn sie nicht atomar sind, oder? Etwas an dem Prozess muss atomar sein, das ist der Punkt einer Transaktion.
Ian Varley
Ja, Sie haben Recht, in diesem Fall wird es - obwohl die Spannung nicht gelöst ist - zu Inkonsistenzen kommen. Die Inkonsistenz ist jedoch nur vorübergehend, bis der nächste Scan nach Spannungsdokumenten dies erkennt. Das ist der Handel in diesem Fall, eine Art eventuelle zeitliche Konsistenz. Solange Sie das Quellkonto zuerst dekrementieren und später das Zielkonto erhöhen, kann dies akzeptabel sein. Aber Vorsicht: Spannungsdokumente geben Ihnen keine ACID-Transaktionen zusätzlich zu REST. Aber sie können ein guter Kompromiss zwischen reinem REST und ACID sein.
ordnungswidrig
4
Stellen Sie sich vor, jedes Spannungsdokument hat einen Zeitstempel, und Kontodokumente haben ein Feld "Letzte Spannung angewendet" - oder eine Liste der angewendeten Spannungen. Wenn Sie das Quellkonto belasten, aktualisieren Sie auch das Feld "Letzte Spannung angewendet". Diese beiden Operationen sind atomar, da sie sich im selben Dokument befinden. Das Zielkonto hat auch ein ähnliches Feld. Auf diese Weise kann das System immer erkennen, welche Spannungsdokumente auf welche Konten angewendet wurden.
Jesse Hallett
1
Wie kann festgestellt werden, ob das Quell- / Zieldokument bereits aktualisiert wurde? Was passiert, wenn es nach Schritt 1 fehlschlägt, dann erneut ausgeführt wird und erneut fehlschlägt usw. Sie werden das Quellkonto weiterhin abziehen?
Wump
1
@wump: Sie müssen aufzeichnen, dass das Spannungsdokument auf das Konto angewendet wurde. B. durch Anhängen der Spannungsdokument-ID an eine Listeneigenschaft eines der beiden Konten. Wenn alle vom Spannungsdokument berührten Konten aktualisiert wurden, markieren Sie das Spannungsdokument als "erledigt" oder löschen Sie es. Anschließend kann die Dokument-ID für alle Konten aus der Liste entfernt werden.
Besitzwidrig
6

Nein, CouchDB ist im Allgemeinen nicht für Transaktionsanwendungen geeignet, da es keine atomaren Operationen in einer Cluster- / Replikationsumgebung unterstützt.

CouchDB opferte die Transaktionsfähigkeit zugunsten der Skalierbarkeit. Für atomare Operationen benötigen Sie ein zentrales Koordinationssystem, das Ihre Skalierbarkeit einschränkt.

Wenn Sie garantieren können, dass Sie nur eine CouchDB-Instanz haben oder dass jeder, der ein bestimmtes Dokument ändert, eine Verbindung zu derselben CouchDB-Instanz herstellt, können Sie das Konflikterkennungssystem verwenden, um mithilfe der oben beschriebenen Methoden eine Art Atomizität zu erstellen. Wenn Sie jedoch später auf einen Cluster skalieren Wenn Sie einen gehosteten Dienst wie Cloudant verwenden, wird dieser ausfallen und Sie müssen diesen Teil des Systems wiederholen.

Mein Vorschlag wäre also, etwas anderes als CouchDB für Ihren Kontostand zu verwenden. Auf diese Weise wird es viel einfacher.

Dobes Vandermeer
quelle
5

Als Antwort auf das Problem des OP ist Couch hier wahrscheinlich nicht die beste Wahl. Die Verwendung von Ansichten ist eine gute Möglichkeit, den Überblick über das Inventar zu behalten, aber das Festklemmen auf 0 ist mehr oder weniger unmöglich. Das Problem ist die Rennbedingung, wenn Sie das Ergebnis einer Ansicht lesen, entscheiden, ob Sie in Ordnung sind, ein "Hammer-1" -Element zu verwenden, und dann ein Dokument schreiben, um es zu verwenden. Das Problem ist, dass es keine atomare Möglichkeit gibt, das Dokument nur zu schreiben, um den Hammer zu verwenden, wenn das Ergebnis der Ansicht ist, dass es> 0 Hammer-1 gibt. Wenn 100 Benutzer alle gleichzeitig die Ansicht abfragen und 1 Hammer-1 sehen, können sie alle ein Dokument schreiben, um einen Hammer 1 zu verwenden, was zu -99 Hammer-1 führt. In der Praxis ist die Rennbedingung ziemlich klein - sehr klein, wenn auf Ihrer Datenbank localhost ausgeführt wird. Sobald Sie jedoch skalieren und einen externen DB-Server oder Cluster haben, wird das Problem viel deutlicher.

Eine Aktualisierung der Antwort von MrKurt (möglicherweise nur datiert oder einige CouchDB-Funktionen sind ihm nicht bekannt)

Eine Ansicht ist ein guter Weg, um Dinge wie Salden / Vorräte in CouchDB zu handhaben.

Sie müssen die docid und rev in einer Ansicht nicht ausgeben. Sie erhalten beide kostenlos, wenn Sie die Ansichtsergebnisse abrufen. Wenn Sie sie senden - insbesondere in einem ausführlichen Format wie einem Wörterbuch - wird Ihre Ansicht nur unnötig groß.

Eine einfache Ansicht zum Verfolgen von Lagerbeständen sollte eher so aussehen (auch auf den ersten Blick).

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

Und die Reduktionsfunktion ist noch einfacher

_sum

Dies verwendet eine integrierte Reduktionsfunktion , die nur die Werte aller Zeilen mit übereinstimmenden Schlüsseln summiert.

In dieser Ansicht kann jedes Dokument ein Mitglied "InventoryChange" haben, das product_key's einer Änderung des Gesamtinventars zuordnet. dh.

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

Würde 10 hammer_1234 und 25 saw_4321 hinzufügen.

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

Würde 5 Hämmer aus dem Inventar verbrennen.

Mit diesem Modell aktualisieren Sie niemals Daten, sondern hängen sie nur an. Dies bedeutet, dass keine Möglichkeit für Aktualisierungskonflikte besteht. Alle Transaktionsprobleme beim Aktualisieren von Daten verschwinden :)

Eine weitere schöne Sache an diesem Modell ist, dass JEDES Dokument in der Datenbank Elemente zum Inventar hinzufügen und daraus entfernen kann. Diese Dokumente können alle Arten anderer Daten enthalten. Möglicherweise verfügen Sie über ein "Versand" -Dokument mit einer Reihe von Daten zu Empfangsdatum und -zeit, Lager, empfangendem Mitarbeiter usw. Solange dieses Dokument eine Inventaränderung definiert, wird das Inventar aktualisiert. Wie ein "Sale" -Dokument und ein "DamagedItem" -Dokument usw. Wenn sie jedes Dokument betrachten, lesen sie sich sehr deutlich. Und die Ansicht erledigt die ganze harte Arbeit.

Wallacer
quelle
Interessante Strategie. Als CouchDB-Neuling scheint es, dass Sie zur Berechnung der aktuellen Anzahl von Hämmern eine Karte erstellen / die gesamte Historie der Bestandsänderungen des Unternehmens für Hämmer reduzieren müssen . Dies könnte jahrelange Änderungen sein. Gibt es eine integrierte Funktion von CouchDB, die diese Leistung erbringt?
Chadrik
Ja, Ansichten in CouchDB sind wie eine kontinuierliche, dauerhafte Zuordnung / Reduzierung. Sie haben Recht, dass es ewig dauern würde, dies bei einem großen Datensatz von Grund auf neu zu tun. Wenn jedoch neue Dokumente hinzugefügt werden, wird nur die vorhandene Ansicht aktualisiert, und es muss nicht die gesamte Ansicht neu berechnet werden. Beachten Sie, dass für Ansichten sowohl Speicherplatz als auch CPU erforderlich sind. Zumindest als ich professionell mit CouchDB gearbeitet habe (es sind einige Jahre vergangen), war es ziemlich wichtig, nur die eingebauten Reduktionsfunktionen zu verwenden, d. H. _Summe. Benutzerdefinierte Javascript Reduktionsfunktionen waren extrem langsam
Wallacer
3

Eigentlich kann man das in gewisser Weise. Schauen Sie sich die HTTP-Dokument-API an und scrollen Sie nach unten zur Überschrift "Mehrere Dokumente mit einer einzigen Anforderung ändern".

Grundsätzlich können Sie eine Reihe von Dokumenten in einer einzigen Post-Anfrage an URI / {Datenbankname} / _ Bulk_Docs erstellen / aktualisieren / löschen. Diese sind entweder alle erfolgreich oder alle schlagen fehl. Das Dokument weist jedoch darauf hin, dass sich dieses Verhalten in Zukunft ändern kann.

BEARBEITEN: Wie vorhergesagt, funktionieren die Massendokumente ab Version 0.9 nicht mehr auf diese Weise.

Evan
quelle
Das würde in der diskutierten Situation nicht wirklich helfen, dh Streit um einzelne Dokumente von mehreren Benutzern.
Kerr
3
Ab CouchDB 0.9 hat sich die Semantik von Massenaktualisierungen geändert.
Barry Wark
0

Verwenden Sie einfach eine einfache SQlite-Lösung für Transaktionen. Wenn die Transaktion erfolgreich abgeschlossen wurde, replizieren Sie sie und markieren Sie sie als repliziert in SQLite

SQLite-Tabelle

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

Sie können auch die Transaktionen löschen, die erfolgreich repliziert wurden.

Ravinder Payal
quelle