Fortgeschritten5 Min. Lesezeit

DynamoDB-Reference-Counts

Ein Reference Count ist eine Zahl, die du auf einem Parent-Item speicherst und die trackt, wie viele Child-Items darauf zeigen — Likes auf einem Post, Members in einem Workspace, Replies auf einem Comment. Du hältst ihn, weil Children bei jedem Read zu zählen zu teuer ist.

Wie pflegst du einen Count in DynamoDB?

Speichere die laufende Summe als Number auf dem Parent-Item und update sie im selben Write, der das Child erstellt. Ein lässt beide landen oder keines, und eine Condition auf dem Child-Write stoppt Retries am Double-Counting — sodass ein einzelnes GetItem einen akkuraten Count zurückgibt.

  • Zähle Children nicht zur Read-Zeit. Eine Query, um Likes zu zählen, zahlt für jedes Like-Item, das sie scannt. Speichere die Summe auf dem Post und lies ein Item statt dessen.
  • Pflege den Count dort, wo das Child geschrieben wird, nicht danach. Bumpe ihn in derselben Operation, die das Child erstellt, damit die beiden nie driften.
  • Nutze eine , wenn Write und Bump verschiedene Items berühren. Ein Like ist ein Item, der Count lebt auf einem anderen — TransactWriteItems lässt beide landen oder keines.
  • Der Fallstrick ist Double-Counting. Ein retried oder dupliziertes Like, das das Increment neu läuft, bläht die Zahl auf. Schütze den Child-Write mit einer Condition.

Warum überhaupt zählen

Aus SQL würdest du nie einen Like-Count speichern — du würdest SELECT COUNT(*) FROM likes WHERE post_id = ? und einen Index günstig machen lassen. DynamoDB hat kein COUNT(*), das das Lesen von Items skippt.

Eine Query über die Likes eines Posts liest — und rechnet ab — jedes Like-Item in dieser Partition, selbst wenn du nur die Zahl willst. Auf einem viralen Post sind das Tausende RCUs, um „wie viele Likes?“ zu beantworten. Das ist der Read- Fallstrick, den Reference Counts töten sollen.

Also du: speichere die laufende Summe auf dem Post selbst. Den Count zu lesen wird ein einzelnes GetItem. Die Kosten: du besitzt jetzt, ihn akkurat zu halten.

Die Items modellieren

Zwei Item-Typen teilen eine Partition, sodass Post und Likes in einer Item Collection sitzen. Erfundene Keys:

Post item
PKSKattributes
POST#a91fMETAlikeTally (Number), body, authorId, createdAt
Like item
PKSKattributes
POST#a91fLIKE#USER#7c20likedAt

Das likeTally-Attribut auf dem META-Item ist der Reference Count. Jedes LIKE#-Item ist ein Child. Beide unter PK = "POST#a91f" zu legen heißt, eine einzelne Query kann Post und Likers zusammen holen, wenn du die Liste wirklich willst.

Den Count atomar bumpen

DynamoDB inkrementiert eine Number mit einer ADD- (oder SET x = x + :n) — das ist ein Atomic Counter: DynamoDB wendet das Delta serverseitig an, ohne dass du den aktuellen Wert zuerst liest, sodass concurrent Increments einander nicht clobbern. (AWS: atomic counters)

Einen Post liken sind zwei Writes auf zwei Items — das LIKE#-Item erstellen und 1 zu likeTally auf META addieren. Wenn das Like landet, der Bump aber scheitert, ist der Tally forever falsch. Du brauchst beide oder keines.

Das garantiert TransactWriteItems — all-or-nothing über mehrere Items, und es cancelt die ganze Transaction, wenn irgendein Item concurrent modifiziert wird (AWS: pessimistic locking with transactions):

{
  "TransactItems": [
    {
      "Put": {
        "TableName": "Social",
        "Item": {
          "PK": {"S": "POST#a91f"},
          "SK": {"S": "LIKE#USER#7c20"},
          "likedAt": {"N": "1750636800"}
        },
        "ConditionExpression": "attribute_not_exists(SK)"
      }
    },
    {
      "Update": {
        "TableName": "Social",
        "Key": {
          "PK": {"S": "POST#a91f"},
          "SK": {"S": "META"}
        },
        "UpdateExpression": "ADD likeTally :one",
        "ExpressionAttributeValues": {":one": {"N": "1"}}
      }
    }
  ]
}

Put und Update committen zusammen. Wenn eines scheitert, rollt DynamoDB beide zurück und gibt TransactionCanceledException zurück.

Gegen Double-Counting schützen

Der echte Bug ist kein halbgeschriebenes Like — die Transaction verhindert das. Es ist derselbe User, der zweimal liked, oder ein Client-Retry, der den Request replayed. Jeder Replay addiert ein weiteres 1, und likeTally driftet still über den echten Count.

Die ConditionExpression: attribute_not_exists(SK) auf dem Put ist der Guard. Wenn das LIKE#-Item dieses Users schon existiert, scheitert die Condition des Put, die ganze Transaction wird gecancelt und — kritisch — das ADD läuft nie. Ein Like pro User, enforced durch den Key.

Baue und kopiere diese Update- und Condition-Expressions — mit den richtigen ExpressionAttributeValues und dem attribute_not_exists-Guard — im DynamoDB Expression Builder, statt das JSON von Hand zusammenzusetzen.

Unlike, und die Kosten

Ein Like zu entfernen ist das Spiegelbild: Delete das LIKE#-Item mit ConditionExpression: attribute_exists(SK) und ADD likeTally :minusOne in derselben Transaction. Die Condition stoppt ein Double-Unlike daran, den Tally negativ zu treiben.

Kenn den Preis. Ein transactional Write kostet 2 WCUs pro Item für Items bis 1 KB — eins zum Prepare, eins zum Commit — versus 1 WCU für einen plainen Write. Ein Like sind zwei Items, also grob vier WCUs pro Like. Günstig pro Action, aber wissenswert, bevor ein Celebrity-Post einen Like-Storm nimmt.

In DynoTable ansehen

Wenn du vermutest, dass ein Tally gedriftet ist, willst du den gespeicherten likeTally gegen die tatsächliche Zahl der LIKE#-Children vergleichen — ohne eine Count-Query in Prod zu fahren.

Das Post-META-Item neben seinen LIKE#-Children in einer Item Collection — so eyeballst du den gespeicherten Tally gegen den echten Child-Count.
Das Post-META-Item neben seinen LIKE#-Children in einer Item Collection — so eyeballst du den gespeicherten Tally gegen den echten Child-Count.

Für echte Reconciliation über eine begrenzte Menge Posts — „welche Tallies matchen nicht ihre Child-Counts?“ — führt DynoTables SQL Workbench GROUP BY und Join clientseitig über die Rows, die du geladen hast, was plaines PartiQL nicht ausdrücken kann.

Fallstricke und nächste Schritte

  • Pflege den Count nicht out-of-band (eine Lambda, die nachts recountet). Das ist ein Band-Aid über einen Write-Pfad, der von Anfang an transactional hätte sein sollen.
  • Achte auf . Ein einzelner wild populärer Post konzentriert jedes Like — und jeden Tally-Bump — auf einen Partition Key. Der Count ist korrekt; die Partition kann trotzdem drosseln.
  • Reconcile selten, repariere chirurgisch. Drift sollte near-zero sein, wenn jede Mutation conditioned ist. Behandle einen Mismatch als Bug zum Finden, nicht als Zahl zum Überschreiben.

Verwandte Lektüre: Single-Table Design dafür, warum Post und Likes eine Partition teilen, und Query vs Scan dafür, warum Children zur Read-Zeit zu zählen das Pattern ist, das du vermeidest.

Dann lade DynoTable herunter, um diese Item Collections zu inspizieren und deine Tallies gegen deine eigenen Tabellen zu verifizieren.

Aktualisiert