Fortgeschritten7 Min. Lesezeit

Warum ein DynamoDB-GSI eventually consistent ist

Du schreibst ein Item, queryst sofort einen Global Secondary Index danach und bekommst nichts zurück — obwohl der Write succeeded und ein Base-Table- GetItem das Item fine zurückgibt.

Nichts ist broken. Du hast die überraschendste Eigenschaft von GSIs getroffen: jeder Read eines GSI ist eventually consistent. Es gibt ein kurzes Fenster nach einem Write, in dem der Index noch nicht aufgeholt hat.

Sind DynamoDB-GSIs eventually consistent?

Ja — jeder Read eines Global Secondary Index ist eventually consistent, ohne Opt- out. Dein Write committed zuerst zur Base Table und propagiert dann asynchron zum Index, sodass eine direkt nach einem Write stale oder fehlende Rows zurückgeben kann. DynamoDB bietet kein ConsistentRead-Flag für einen GSI.

  • Ein GSI ist eine separate, asynchron replizierte Tabelle — dein Write committed zuerst zur Base Table und propagiert dann zum Index.
  • Es gibt kein ConsistentRead-Flag für einen GSI. Anders als bei der Base Table kannst du keinen strong Read erzwingen, um die Lücke zu schließen.
  • Read-your-own-writes von der Base Table, nicht vom GSI. Du hältst den direkt nach einem Write bereits.
  • Erzwinge Uniqueness mit einem conditional Write, nicht mit einer GSI-Query. Die Propagation-Lücke macht aus einem „ist das genommen?“-Check eine Race.

Das Symptom: ein Sign-up, der „sich selbst nicht finden kann“

Nimm eine Members-Tabelle für einen User-Accounts-Service. Die Base Table ist nach einer internen Id gekeyed, aber Users loggen per Email ein, also gibt es einen Email-Lookup-GSI:

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

Der Sign-up-Flow tut zwei Dinge hintereinander: PutItem den neuen Member, dann Query EmailIndex WHERE GSI1PK = "ada@northwind.test", um zu checken, dass niemand sonst die Adresse claimed hat, und um das Profil zu laden.

Fahre diese zwei Calls ein paar Millisekunden auseinander und die Query kann zero Items zurückgeben. Tu es eine Sekunde später wieder und die Row ist da. Der Write ist nicht gefailt — der Index war einfach noch nicht updated.

Warum das passiert: GSIs werden asynchron repliziert

Ein GSI ist eine separate, intern gemanagte Tabelle mit eigenen Partitionen und eigenem Key Schema. Er wird nicht innerhalb derselben Transaction wie dein Base- Table-Write maintained.

Wenn du PutItemst, committed DynamoDB durable zur Base Table, acknowledged deinen Write und propagiert dann asynchron die Änderung zu jedem GSI. Die AWS- GSI-Dokumentation sagt es klar: GSIs unterstützen nur eventually consistent Reads.

Die Propagation-Verzögerung zwischen Base-Table-Write und Index-Update ist meist ein Bruchteil einer Sekunde — aber sie ist nicht guaranteed und nicht bounded unter Load. So zu designen, als wäre sie bounded, ist die Falle.

Diese Verzögerung ist der originale Dynamo-Design-Trade-off. Das 2007 Amazon Dynamo Paper wählte Availability und Partition Tolerance über Strong Consistency.

GSIs erben diese Lineage. Loose Coupling ist, was den Index skalieren und unabhängig von der Base Table writable bleiben lässt.

EmailIndexBase TableAppEmailIndexBase TableAppasync PropagationPutItem (neuer Member)200 OKQuery per Email0 Items (stale)Änderung replizierenQuery per Email1 Item (aufgeholt)

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

Anders als bei der Base Table — wo du ConsistentRead = true übergibst, um ein GetItem/Query zu erzwingen — rejectet ein GSI diese Option flat.

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

Read-Kosten auf einem GSI

GSI-Queries rechnen die On-Demand-RCU des Index in us-east-1 wie jeden anderen Read ab — 0,5 RCU pro 4 KB eventually-consistent Block — und du kannst nicht doppelt für Strong Consistency zahlen, weil ConsistentRead rejected wird. Der Propagation- Lag ist gratis; der Index-Read nicht. Vergleiche Base-Table- vs GSI-Read-Rates im Pricing Calculator.

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

Der Missing-Row-Fall ist der offensichtliche. Der leisere Bug ist, einen stale vorherigen Wert zu lesen.

Angenommen, Ada ändert ihre Email von ada@northwind.test zu ada.l@northwind.test. Die Base Table updated atomar, aber für einen Moment kann der GSI immer noch den alten Index-Eintrag zurückgeben.

Ein Lookup gegen den neuen Wert missest, während der aufgegebene Wert noch resolved.

Schlimmer: wenn du den GSI queryst und basierend auf dem, was du liest, zurückschreibst, kannst du auf einen Wert handeln, der nicht mehr existiert. Behandle jeden GSI-Read als Snapshot, der Reality nachhinken kann.

Darum herum designen — nicht dagegen kämpfen

Das Propagation-Fenster ist real, also ist der Fix architektonisch, kein Retry-Knob zum Togglen. Vier Patterns, grob in Präferenzreihenfolge:

  1. Read your own writes von der Base Table. Direkt nach einem Write hältst du den Primary Key (ACC#a1f9c) bereits, also tu ein strongly consistent GetItem auf der Base Table statt den GSI zu querien.

    Der GSI ist für das andere Access Pattern — „ich habe eine Email, finde den Account“ — nicht dafür, den Write zu bestätigen, den du gerade gemacht hast.

  2. Erzwinge Uniqueness mit einem Guard-Item, nicht dem GSI. Vertrau nie einer GSI-Query, um zu beweisen, dass eine Email unclaimed ist — die Propagation-Lücke macht das zu einer Race, die zwei simultane Sign-ups beide verlieren können.

    Schreibe stattdessen ein dediziertes Uniqueness-Item keyed on der Email selbst (PK = "EMAIL#ada@northwind.test") in einem TransactWriteItems mit einer ConditionExpression von attribute_not_exists(PK).

    Strongly consistent Base-Table-Conditions, atomar angewandt, sind, was Uniqueness wirklich erzwingt.

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

    Wenn ein zweiter Sign-up um dieselbe Adresse racet, failt seine Condition und die ganze wird rejected — kein GSI, kein Propagation-Delay, kein Double-Claim.

    Baue und preview diese attribute_not_exists-Condition mit dem DynamoDB Expression Builder, bevor du sie in Code verdrahtest.

  3. Tolriere den Lag in der UX. Wenn der GSI-Read wirklich das richtige Tool ist (Login per Email für einen existierenden User), ist das Fenster sub-second und harmlos — ein etablierter Account hat längst propagiert.

    Reserve den strongly consistent Base-Table-Pfad nur für den Read-after-Write- Moment.

  4. Re-query, nimm nicht an. Wenn ein Workflow ein brandneues Item durch den GSI beobachten muss, behandle ein leeres Result als „noch nicht sichtbar“, nicht als „existiert nicht“, und re-query nach kurzem Backoff.

    Aber bevorzuge Patterns 1 und 2, die die Guesswork komplett entfernen.

Die Propagation-Lücke selbst sehen

Der schnellste Weg, Intuition zu bauen, ist, es passieren zu sehen. In DynoTable puttest du ein Item in die Base Table und queryst den GSI sofort in einem zweiten Tab.

Auf einer geladenen Tabelle fängst du gelegentlich den Index, wie er Base-Daten nachhinkt, und siehst ihn beim nächsten Refresh konvergieren.

Den Lag mit eigenen Daten zu sehen, lässt die Regel „read your own writes von der Base Table“ weit besser hängen als jedes Diagramm.

Fallstricke und nächste Schritte

  • Gate Logic nicht auf einem GSI-Read-after-Write. Uniqueness-Checks, „hat mein Write gelandet“-Bestätigungen und Read-Modify-Write-Loops gehören auf die strongly consistent Base Table.
  • Greif nicht nach ConsistentRead auf einem GSI — es ist nicht erlaubt und errort.
  • Modelliere kein Access Pattern als GSI, wenn der Base-Key es schon beantwortet. Bediene einen Read vom Primary Key und du skippst das Propagation-Fenster komplett.

Die richtige Key-Form zu wählen ist das ganze Spiel in Single-Table Design; zu wissen, wann eine Query einen Scan schlägt, hält dich vom Index fern, bevor er überhaupt nötig ist (Query vs Scan).

Baue und teste deine Uniqueness-ConditionExpression im DynamoDB Expression Builder. Dann probier DynoTable, um Base-Table-Writes zu einem GSI in Echtzeit propagieren zu sehen, und designe deine Keys so, dass das Eventual-Consistency- Fenster dich nie beißt.

Aktualisiert