Fortgeschritten7 Min. Lesezeit

Warum ein DynamoDB-GSI letztendlich konsistent ist

Du schreibst ein Item, fragst sofort einen Global Secondary Index danach ab und bekommst nichts zurück — obwohl der Write gelang und ein Basistabellen-GetItem das Item problemlos zurückgibt.

Nichts ist kaputt. Du bist auf die überraschendste Eigenschaft von GSIs gestoßen: jeder Read eines GSI ist letztendlich konsistent. Es gibt ein kurzes Fenster nach einem Write, in dem der Index noch nicht aufgeholt hat.

Sind DynamoDB-GSIs letztendlich konsistent?

Ja — jeder Read eines Global Secondary Index ist letztendlich konsistent, ohne Möglichkeit, das abzuschalten. Dein Write committet zuerst auf die Basistabelle und propagiert dann asynchron zum Index, sodass eine Query direkt nach einem Write veraltete oder fehlende Zeilen zurückgeben kann. DynamoDB bietet für einen GSI kein ConsistentRead-Flag.

  • Ein GSI ist eine separate, asynchron replizierte Tabelle — dein Write committet zuerst auf die Basistabelle, dann propagiert er zum Index.
  • Für einen GSI existiert kein ConsistentRead-Flag. Anders als bei der Basistabelle kannst du keinen stark konsistenten Read erzwingen, um die Lücke zu schließen.
  • Read-your-own-writes aus der Basistabelle, nicht dem GSI. Du hältst den Primary Key direkt nach einem Write ohnehin schon.
  • Erzwinge Eindeutigkeit mit einem Conditional Write, nicht einer GSI-Query. Die Propagierungslücke verwandelt eine "ist das vergeben?"-Prüfung in ein Race.

Das Symptom: eine Anmeldung, die "sich selbst nicht findet"

Nimm eine Members-Tabelle für einen User-Account-Service. Die Basistabelle ist auf eine interne id gekeyt, aber User loggen sich per E-Mail ein, also gibt es einen E-Mail-Lookup-GSI:

Members (base table)
PKSKemaildisplayName
ACC#a1f9cPROFILEada@northwind.testAda L.
EmailIndex (GSI)
GSI1PKGSI1SK
ada@northwind.testACC#a1f9c

Der Anmelde-Flow macht zwei Dinge hintereinander: PutItem des neuen Members, dann Query EmailIndex WHERE GSI1PK = "ada@northwind.test", um zu prüfen, dass niemand sonst diese Adresse beansprucht hat, und um das Profil zu laden.

Führe diese beiden Aufrufe ein paar Millisekunden auseinander aus, und die Query kann null Items zurückgeben. Tu es eine Sekunde später erneut, und die Zeile ist da. Der Write scheiterte nicht — der Index war nur noch nicht aktualisiert.

Warum das passiert: GSIs werden asynchron repliziert

Ein GSI ist eine separate, intern verwaltete Tabelle mit eigenen Partitionen und eigenem Key-Schema. Er wird nicht innerhalb derselben Transaktion wie dein Basistabellen-Write gepflegt.

Wenn du PutItemst, committet DynamoDB dauerhaft auf die Basistabelle, bestätigt deinen Write und propagiert die Änderung dann asynchron zu jedem GSI. Die GSI-Dokumentation von AWS sagt es klar: GSIs unterstützen nur letztendlich konsistente Reads.

Die Propagierungsverzögerung zwischen einem Basistabellen-Write und der Index-Aktualisierung ist üblicherweise der Bruchteil einer Sekunde — aber sie ist nicht garantiert und unter Last nicht begrenzt. Zu entwerfen, als wäre sie begrenzt, ist die Falle.

Das ist kein Bug; es ist der ursprüngliche Dynamo-Design-Trade-off. Das Amazon-Dynamo-Paper von 2007 wählte Verfügbarkeit und Partitionstoleranz über starke Konsistenz.

GSIs erben diese Linie. Lose Kopplung ist es, was den Index unabhängig von der Basistabelle skalieren und schreibbar bleiben lässt.

EmailIndexBasistabelleAppEmailIndexBasistabelleAppasynchrone PropagierungPutItem (neuer Member)200 OKQuery nach E-Mail0 Items (veraltet)Änderung replizierenQuery nach E-Mail1 Item (aufgeholt)

Die Lücke zwischen dem 200 OK und "Änderung replizieren" ist das Fenster, in dem dein Index-Read veraltet ist. Es gibt kein Consistent-Read-Flag, das sie schließt.

Anders als bei der Basistabelle — wo du ConsistentRead = true übergibst, um ein stark konsistentes GetItem/Query zu erzwingen — lehnt ein GSI diese Option rundweg ab.

Ein LSI kann stark gelesen werden, weil er sich die Partitionen der Basistabelle teilt; siehe GSI vs. LSI dafür, warum diese Unterscheidung existiert.

Eine subtilere Falle: veraltete alte Werte, nicht nur fehlende neue

Der Fall der fehlenden Zeile ist der offensichtliche. Der leisere Bug ist das Lesen eines veralteten vorherigen Werts.

Sagen wir, Ada ändert ihre E-Mail von ada@northwind.test zu ada.l@northwind.test. Die Basistabelle aktualisiert atomar, aber für einen Moment kann der GSI noch den alten Index-Eintrag zurückgeben.

Ein Lookup gegen den neuen Wert verfehlt, während der aufgegebene Wert noch auflöst.

Schlimmer: wenn du den GSI abfragst und basierend auf dem Gelesenen zurückschreibst, kannst du auf einem Wert handeln, der nicht mehr existiert. Behandle jeden GSI-Read als Snapshot, der der Realität hinterherhinken kann.

Entwirf darum herum — bekämpf es nicht

Das Propagierungsfenster ist real, also ist der Fix architektonisch, kein Retry-Regler, den du umlegst. Vier Muster, grob in Reihenfolge der Präferenz:

  1. Read-your-own-writes aus der Basistabelle. Direkt nach einem Write hältst du den Primary Key (ACC#a1f9c) ohnehin schon, also mach ein stark konsistentes GetItem auf die Basistabelle, statt den GSI abzufragen.

    Der GSI ist für das andere Zugriffsmuster — "ich habe eine E-Mail, finde das Konto" — nicht zum Bestätigen des Writes, den du gerade gemacht hast.

  2. Erzwinge Eindeutigkeit mit einem Guard-Item, nicht dem GSI. Vertrau nie einer GSI-Query, um zu beweisen, dass eine E-Mail unbeansprucht ist — die Propagierungslücke macht das zu einem Race, das zwei gleichzeitige Anmeldungen beide verlieren können.

    Schreib stattdessen ein dediziertes Eindeutigkeits-Item, das auf die E-Mail selbst gekeyt ist (PK = "EMAIL#ada@northwind.test"), innerhalb eines TransactWriteItems mit einer ConditionExpression von attribute_not_exists(PK).

    Stark konsistente Basistabellen-Bedingungen, atomar angewandt, sind es, was Eindeutigkeit tatsächlich erzwingt.

    TransactWriteItems:
      - Put member item    (PK = ACC#a1f9c, SK = PROFILE)
      - Put uniqueness item (PK = EMAIL#ada@northwind.test)
          ConditionExpression: attribute_not_exists(PK)

    Wenn eine zweite Anmeldung um dieselbe Adresse rennt, scheitert ihre Bedingung und die ganze Transaktion wird abgelehnt — kein GSI, keine Propagierungsverzögerung, kein Doppel-Anspruch.

    Baue und sieh dir diese attribute_not_exists-Bedingung mit dem DynamoDB Expression Builder an, bevor du sie in Code verdrahtest.

  3. Toleriere die Verzögerung in der UX. Wenn der GSI-Read wirklich das richtige Werkzeug ist (Login per E-Mail für einen bestehenden User), ist das Fenster im Sub-Sekunden-Bereich und harmlos — ein etabliertes Konto propagierte längst.

    Reserviere den stark konsistenten Basistabellen-Pfad nur für den Read-after-Write-Moment.

  4. Frag erneut ab, nimm nichts an. Wenn ein Workflow ein brandneues Item durch den GSI beobachten muss, behandle ein leeres Ergebnis als "noch nicht sichtbar", nicht "existiert nicht", und frag nach einem kurzen Backoff erneut ab.

    Aber bevorzuge die Muster 1 und 2, die das Rätselraten ganz beseitigen.

Sieh die Propagierungslücke selbst

Der schnellste Weg, Intuition aufzubauen, ist, es geschehen zu sehen. In DynoTable legst du ein Item in die Basistabelle und fragst sofort den GSI in einem zweiten Tab ab.

Auf einer belasteten Tabelle erwischst du gelegentlich den Index, wie er den Basisdaten hinterherhinkt, und siehst dann zu, wie er beim nächsten Refresh konvergiert.

Die Verzögerung mit deinen eigenen Daten zu sehen, lässt die Regel "Read-your-own-writes aus der Basistabelle" weit besser haften als jedes Diagramm.

Fallstricke und nächste Schritte

  • Mach keine Logik von einem GSI-Read-after-Write abhängig. Eindeutigkeitsprüfungen, "ist mein Write gelandet"-Bestätigungen und Read-modify-write-Loops gehören auf die stark konsistente Basistabelle.
  • Greif nicht zu ConsistentRead auf einem GSI — es ist nicht erlaubt und wird einen Fehler werfen.
  • Modelliere kein Zugriffsmuster als GSI, wenn der Basis-Key es schon beantwortet. Bediene einen Read aus dem Primary Key, und du überspringst das Propagierungsfenster ganz.

Die richtige Key-Form zu wählen ist das ganze Spiel im Single-Table Design; zu wissen, wann eine Query einen Scan schlägt, hält dich von vornherein vom Index fern (Query vs. Scan).

Baue und teste deine Eindeutigkeits-ConditionExpression im DynamoDB Expression Builder. Dann probier DynoTable, um Basistabellen-Writes in Echtzeit zu einem GSI propagieren zu sehen, und entwirf deine Keys so, dass das Fenster letztendlicher Konsistenz nie beißt.

Aktualisiert