DynamoDB-Reference-Counts
Ein Reference-Count ist eine Zahl, die du auf einem Parent-Item speicherst und die nachverfolgt, wie viele Child-Items auf es zeigen — Likes auf einem Post, Mitglieder in einem Workspace, Antworten auf einem Kommentar. Du hältst sie, weil das Zählen der Children bei jedem Read zu teuer ist.
Wie pflegt man einen Count in DynamoDB?
Speichere die laufende Summe als Zahl auf dem Parent-Item und aktualisiere sie in demselben Write, der das Child erstellt. Ein sorgt dafür, dass beide landen oder keiner, und eine Bedingung auf dem Child-Write verhindert, dass Wiederholungen doppelt zählen — sodass ein einzelnes GetItem eine akkurate Zählung 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 stattdessen ein Item. - Pflege den Count dort, wo das Child geschrieben wird, nicht danach. Bump ihn in derselben Operation, die das Child erstellt, sodass die beiden nie auseinanderdriften.
- Verwende eine Transaktion, wenn der Write und der Bump verschiedene Items
berühren. Ein Like ist ein Item, der Count lebt auf einem anderen —
TransactWriteItemssorgt dafür, dass beide landen oder keiner. - Das Footgun ist Doppelzählung. Ein wiederholter oder duplizierter Like, der das Inkrement erneut ausführt, bläht die Zahl auf. Sichere den Child-Write mit einer Bedingung ab.
Warum überhaupt zählen
Von SQL kommend würdest du nie eine Like-Anzahl speichern — du würdest
SELECT COUNT(*) FROM likes WHERE post_id = ? machen und einen Index das billig
machen lassen. DynamoDB hat kein COUNT(*), das das Lesen von Items überspringt.
Eine Query über die Likes eines Posts liest — und berechnet — jedes Like-Item in
dieser Partition, selbst wenn du nur die Zahl willst. Bei einem viralen Post sind
das tausende RCUs, um „wie viele Likes?" zu beantworten. Das ist das Read-Footgun,
für das Reference-Counts existieren, um es zu töten.
Also denormalisierst du: Du speicherst die laufende Summe auf dem Post selbst.
Den Count zu lesen wird ein einzelnes GetItem. Der Preis ist, dass du jetzt
dafür verantwortlich bist, ihn akkurat zu halten.
Die Items modellieren
Zwei Item-Typen teilen sich eine Partition, sodass der Post und seine Likes in einer Item-Collection liegen. Erfundene Keys:
| PK | SK | attributes |
|---|---|---|
| POST#a91f | META | likeTally (Number), body, authorId, createdAt |
| PK | SK | attributes |
|---|---|---|
| POST#a91f | LIKE#USER#7c20 | likedAt |
Das likeTally-Attribut auf dem META-Item ist der Reference-Count. Jedes
LIKE#-Item ist ein Child. Beide unter PK = "POST#a91f" zu legen bedeutet, dass
eine einzelne Query den Post und seine Liker zusammen abrufen kann, wenn du die
Liste haben willst.
Den Count atomar bumpen
DynamoDB inkrementiert eine Zahl mit einer ADD- (oder SET x = x + :n-)
Update-Expression — das ist ein Atomic Counter: DynamoDB wendet das Delta
server-seitig an, ohne dass du zuvor den aktuellen Wert liest, sodass nebenläufige
Inkremente sich nicht gegenseitig überschreiben.
(AWS: Atomic Counters)
Das Problem: Einen Post zu liken sind zwei Writes auf zwei Items — das
LIKE#-Item erstellen und 1 zu likeTally auf META addieren. Wenn der Like
landet, aber der Bump scheitert, ist die Zählung für immer falsch. Du brauchst
beide oder keinen.
Genau das garantiert TransactWriteItems — Alles-oder-nichts über mehrere Items,
und es bricht die ganze Transaktion ab, wenn irgendein Item nebenläufig geändert
wird
(AWS: Pessimistic Locking mit Transaktionen):
{
"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"}}
}
}
]
}Das Put und das Update committen zusammen. Wenn eines scheitert, rollt
DynamoDB beide zurück und gibt eine TransactionCanceledException zurück.
Gegen Doppelzählung absichern
Der eigentliche Bug ist kein halb geschriebener Like — die Transaktion verhindert
das. Es ist derselbe Nutzer, der zweimal liked, oder ein Client-Retry, der den
Request erneut abspielt. Jede Wiederholung addiert eine weitere 1, und
likeTally driftet still über die wahre Zählung hinaus.
Die ConditionExpression: attribute_not_exists(SK) auf dem Put ist die
Absicherung. Wenn das LIKE#-Item dieses Nutzers bereits existiert, scheitert die
Bedingung des Put, die ganze Transaktion wird abgebrochen und — entscheidend —
das ADD läuft nie. Ein Like pro Nutzer, durch den Key erzwungen.
Baue und kopiere diese Update- und Condition-Expressions — mit den richtigen
ExpressionAttributeValues und der attribute_not_exists-Absicherung — im
DynamoDB Expression Builder, statt das JSON
von Hand zusammenzusetzen.
Unlike, und die Kosten
Einen Like zu entfernen ist das Spiegelbild: Delete das LIKE#-Item mit
ConditionExpression: attribute_exists(SK) und ADD likeTally :minusOne in
derselben Transaktion. Die Bedingung stoppt, dass ein doppeltes Unlike die
Zählung ins Negative treibt.
Kenne den Preis. Ein transaktionaler Write kostet 2 WCUs pro Item für Items bis 1 KB — eine zum Vorbereiten, eine zum Committen — gegenüber 1 WCU für einen einfachen Write. Ein Like sind zwei Items, also ist jeder Like ungefähr vier WCUs. Günstig pro Aktion, aber gut zu wissen, bevor ein Promi-Post einen Like-Sturm abbekommt.
In DynoTable ansehen
Wenn du vermutest, dass eine Zählung gedriftet ist, willst du das gespeicherte
likeTally gegen die tatsächliche Anzahl der LIKE#-Children vergleichen — ohne
eine Zähl-Query in Prod laufen zu lassen.

Für eine echte Abstimmung über eine begrenzte Menge von Posts — „welche Zählungen
stimmen nicht mit ihren Child-Anzahlen überein?" — führt die SQL-Workbench von
DynoTable das GROUP BY und den Join client-seitig über die geladenen Zeilen aus,
was reines PartiQL nicht ausdrücken kann.
Fallstricke und nächste Schritte
- Pflege den Count nicht out-of-band (ein Lambda, der nächtlich neu zählt). Das ist ein Pflaster über einem Schreibpfad, der von Anfang an transaktional hätte sein sollen.
- Achte auf Hot Partitions. Ein einzelner wild populärer Post konzentriert jeden Like — und jeden Zähl-Bump — auf einen Partition Key. Die Zählung ist korrekt; die Partition kann trotzdem throtteln.
- Stimme selten ab, repariere chirurgisch. Drift sollte nahe null sein, wenn jede Mutation konditioniert ist. Behandle eine Abweichung als Bug zum Finden, nicht als Zahl zum Überschreiben.
Verwandte Lektüre: Single-Table-Design dafür, warum sich der Post und die Likes eine Partition teilen, und Query vs Scan dafür, warum das Zählen von Children zur Read-Zeit das Muster ist, das du vermeidest.
Dann lade DynoTable herunter, um diese Item-Collections zu inspizieren und deine Zählungen gegen deine eigenen Tabellen zu verifizieren.


