Genau einmalige Verarbeitung und höchstens einmalige Bestätigung

Das Erstellen zuverlässiger Message Queues und Microservices in verteilten Cloud-Datenbanken wie Spanner ist mit besonderen Herausforderungen verbunden. In diesem Dokument werden die wichtigsten Konzepte und Designmuster beschrieben, die zum Ausführen der Exactly-Once-Verarbeitung und der At-Most-Once-Bestätigung in Spanner-Warteschlangen verwendet werden.

Wichtige Konzepte und das Idempotenz-Dilemma

„Idempotenz“ bedeutet, dass das Ergebnis immer dasselbe ist, unabhängig davon, wie oft ein Vorgang ausgeführt wird. Beim Erstellen von Nachrichtenwarteschlangen oder Systemen zur Arbeitsverteilung gibt es drei primäre Zustellungsmethoden, die sich mit der Idempotenz befassen:

  • At-least-once::Nachrichten werden garantiert zugestellt und verarbeitet. Bei Netzwerkfehlern kann es passieren, dass eine Nachricht mehrmals zugestellt und verarbeitet wird.
  • At-most-once::Nachrichten werden höchstens einmal zugestellt. Eine doppelte Verarbeitung wird verhindert. Wenn jedoch ein Fehler auftritt, kann die Nachricht verloren gehen oder verworfen werden.
  • Genau einmal:Jede Nachricht wird genau einmal verarbeitet – weder verloren noch dupliziert.

Spanner-Warteschlangen bieten automatisch mindestens einmalige Zustellung und höchstens einmalige Bestätigung. Sie können jedoch die in den folgenden Abschnitten beschriebenen Designmuster und Beispiele verwenden, um die doppelte Verarbeitung und erneute Zustellung programmatisch zu reduzieren.

Das Problem mit dem unbekannten Commit-Status

In einer Datenbank auf einem einzelnen Computer sind Transaktionen entweder erfolgreich oder schlagen fehl. In einer verteilten Cloud-Datenbank können Transaktionen verloren gehen, wenn Daten in mehreren physischen Rechenzentren repliziert werden.

Wenn Ihre Anwendung eine Nachricht verarbeitet und eine Bestätigung wie DELETE FROM MessageQueue WHERE message_id = '123' ASSERT_ROWS_MODIFIED 1 sendet, durchläuft die Anfrage mehrere Netzwerk-Hops:

[Your App] <--(gRPC)--> [Spanner API] <--(Google network)--> [Spanner Storage]

Wenn der Spanner-Speicher-Leader den Commit empfängt, wird er mit den Speicherreplikaten synchronisiert. Nachdem ein Konsens erzielt und Daten auf die Festplatte geschrieben wurden, wird die Transaktion erfolgreich in der Datenbank gespeichert.

Ein vorübergehender Netzwerkausfall (z. B. ein Proxy-Neustart oder eine Netzwerkunterbrechung) kann jedoch auftreten, nachdem die Daten auf dem Laufwerk gespeichert wurden, aber bevor die Erfolgsbestätigung Ihre Anwendung erreicht. In diesem Fall erhält Ihre Anwendung einen Netzwerk-Zeitüberschreitungsfehler (DEADLINE_EXCEEDED oder UNAVAILABLE).

Da die Verbindung out-of-band unterbrochen wurde, kann Ihre Anwendung nicht feststellen, ob die Verbindung vor oder nach dem erfolgreichen Commit unterbrochen wurde. Dies wird als „Problem mit unbekanntem Commit-Status“ bezeichnet.

Warum automatische Wiederholungsversuche gefährlich sein können

Wenn Ihre Anwendung einen Netzwerkzeitüberschreitungsfehler abfängt und die genau gleiche Transaktion ohne Deduplizierung wiederholt, besteht das Risiko, dass die Geschäftslogik zweimal ausgeführt wird. Wenn bei der Transaktion beispielsweise die Kreditkarte eines Kunden belastet oder ein Artikel versendet wurde, führt das Wiederholen einer erfolgreichen (aber nicht bestätigten) Transaktion zu einer doppelten Belastung oder Lieferung.

Zum Schutz Ihrer Nutzer werden Transaktionen, die mit unbekannten Commit-Fehlern fehlschlagen, in offiziellen Google Cloud Clientbibliotheken nie automatisch wiederholt. Sie geben einen expliziten Fehler aus, der Sie darüber informiert, dass das Ergebnis der Transaktion unbekannt ist. Es liegt an Ihrem Anwendungscode, den Wiederholungsversuch sicher mithilfe von Idempotenzmustern zu verarbeiten.

Strategien für die genau einmalige Verarbeitung in der Praxis

Implementieren Sie eines der folgenden primären Designmuster, um Transaktionen sicher noch einmal zu versuchen und die Verarbeitung von Duplikaten und die erneute Zustellung von Nachrichten zu reduzieren.

Geänderte Zeilen bestätigen

Wenn Ihre gesamte Geschäftslogik (z. B. das Aktualisieren anderer Tabellen) und die Bestätigung der Warteschlangennachricht in einer einzigen Spanner-Transaktion erfolgen, können Sie ASSERT_ROWS_MODIFIED in Ihrer DELETE-Anweisung verwenden. Dies ist die einfachste Strategie für die Verarbeitung einmaliger Nachrichten, da keine separate Deduplizierungstabelle erforderlich ist.

Wenn Sie Ihrer Anweisung ASSERT_ROWS_MODIFIED 1 hinzufügen, ist die Bestätigung nur erfolgreich, wenn genau eine Nachricht gelöscht wurde. Wenn ein Netzwerkfehler auftritt und Ihre Anwendung die Transaktion noch einmal versucht, ist die Nachricht nicht mehr in der Warteschlange vorhanden. Beim Wiederholungsversuch werden 0 Zeilen gelöscht und die Anweisung schlägt mit OUT_OF_RANGE fehl.

Der Fehler OUT_OF_RANGE ist dauerhaft: Die Zeile ist bereits gelöscht. Wenn Sie die Anweisung noch einmal ausführen, werden immer 0 Zeilen geändert und es tritt wieder ein Fehler auf. Außerdem ist es auf Anweisungsebene: Nur die DELETE schlägt fehl. Die Transaktion bleibt offen und alle Schreibvorgänge, die Sie bereits darin ausgeführt haben, werden weiterhin gepuffert und bei Fortsetzung übernommen. Spanner bricht die Transaktion nicht ab und führt auch kein Rollback durch. Damit der beabsichtigte genau einmalige Schutz erreicht wird, müssen Sie dafür sorgen, dass die Transaktion zurückgesetzt wird:

  • Fehler weiterleiten:Wenn Sie eine runnerbasierte API wie ReadWriteTransaction in Go verwenden, lassen Sie den OUT_OF_RANGE-Fehler aus Ihrer Transaktionsfunktion weiterleiten. Die Clientbibliothek bricht die Transaktion ab und macht sie rückgängig. So wird sichergestellt, dass keine Schreibvorgänge der Geschäftslogik übernommen werden.
  • Nicht abfangen und fortfahren: Fangen Sie den Zusicherungsfehler niemals in der Transaktionsfunktion ab und fahren Sie fort. In diesem Fall werden die anderen Anweisungen von Spanner weiterhin übernommen, was zu dem exakten doppelten Update führt, das durch dieses Muster verhindert werden soll.
  • Manuelle Rollbacks:Wenn Sie Transaktionen manuell verwalten (z. B. mit ReadWriteStmtBasedTransaction in Go oder den REST-/gRPC-APIs), müssen Sie die entsprechende Rollback-Methode explizit aufrufen, wenn die Zusicherung fehlschlägt.

Transaktionsausgang

Wenn Ihre Geschäftslogik nicht innerhalb einer einzelnen Spanner-Transaktion ausgeführt werden kann (z. B. bei mehrstufigen Workflows oder Updates, die mehrere Systeme umfassen) oder wenn für die Nachrichtenverarbeitung externe Dienste aufgerufen werden müssen (z. B. zum Senden einer SMS oder zum Verarbeiten einer Zahlung), können Sie die Geschäftslogik nicht atomar mit der Warteschlangenbestätigung verknüpfen.

In diesen Szenarien dürfen Sie niemals externe APIs aufrufen oder nicht transaktionale Vorgänge direkt in einem Spanner-Transaktionsblock ausführen. Wenn Spanner die Transaktion wiederholt oder abbricht, wird die Geschäftslogik in Ihrer Anwendung möglicherweise mehrmals ausgeführt.

Ihre Anwendung sollte eine eigene Idempotenzstrategie implementieren. Sie können jedoch die folgenden Muster verwenden, um doppelte Arbeit zu vermeiden:

  • Schnelle Vorgänge (werden innerhalb des standardmäßigen 10-Sekunden-Leases abgeschlossen): Führen Sie die Geschäftslogik oder den externen API-Aufruf aus, wenn die Nachricht empfangen wird. Bestätigen Sie die Nachricht erst, wenn der Vorgang erfolgreich war. Wenn der Vorgang fehlschlägt oder der Worker abstürzt, bevor er die Nachricht bestätigt, bestätigen Sie die Nachricht nicht. Die Lease läuft ab und Spanner liefert die Nachricht automatisch für einen erneuten Versuch aus.

  • Lang andauernde Vorgänge (dauern länger als 10 Sekunden): Bestätigen Sie den Empfang der Nachricht und senden Sie in derselben Transaktion eine neue Nachricht, die für die zukünftige Zustellung geplant ist (mit einer Zustellungsverzögerung, die länger als die geschätzte Ausführungszeit ist). Alternativ können Sie die Nachricht mithilfe des RENEWLEASE_QUEUE_NAME()-TVF regelmäßig verlängern. Wenn die Geschäftslogik oder der externe Aufruf erfolgreich ist, bestätigen Sie die neu in die Warteschlange eingereihte oder verlängerte Nachricht.

Idempotenz von gemultiplexten Sitzungen

Spanner-Sitzungen können gemultiplext werden. Bei gemultiplexten Sitzungen werden Transaktionsstatus im Arbeitsspeicher über gemeinsam genutzte Sitzungen hinweg verfolgt. Dies schützt vor clientseitigen Netzwerkfehlern, da Clientbibliotheken die Verbindung wiederherstellen und unbekannte Commit-Status zuverlässiger auflösen können.

Durch Multiplexing-Sitzungen können jedoch keine unbekannten Commit-Ergebnisse vermieden werden, wenn ein Frontend-Server abstürzt. Wenn der Server, auf dem die Transaktionstabelle Ihrer Sitzung gehostet wird, unmittelbar nach einem Commit neu gestartet wird, geht der In-Memory-Status verloren. Bei der erneuten Verbindung wird dann ein Fehler wegen eines unbekannten Transaktionsergebnisses zurückgegeben. Um dieses Risiko zu minimieren, implementieren Sie die Idempotenzmuster auf dieser Seite.

Nächste Schritte