Perché una GSI di DynamoDB è eventualmente coerente
Scrivi un item, interroghi subito una Global Secondary Index per cercarlo e ottieni
nulla in risposta — anche se la scrittura è andata a buon fine e un GetItem
sulla tabella base restituisce l'item senza problemi.
Niente è rotto. Hai colpito la proprietà più sorprendente delle GSI: ogni lettura di una GSI è eventualmente coerente. C'è una breve finestra dopo una scrittura in cui l'indice non si è ancora aggiornato.
Le GSI di DynamoDB sono eventualmente coerenti?
Sì — ogni lettura di una Global Secondary Index è eventualmente coerente, senza alcun modo per disattivarla. La tua scrittura viene committata prima sulla tabella base, poi propagata in modo asincrono all'indice, quindi una query lanciata subito dopo una scrittura può restituire righe obsolete o mancanti. DynamoDB non offre alcun flag ConsistentRead per una GSI.
- Una GSI è una tabella separata, replicata in modo asincrono — la tua scrittura viene committata prima sulla tabella base, poi propagata all'indice.
- Non esiste un flag
ConsistentReadper una GSI. A differenza della tabella base, non puoi forzare una lettura forte per chiudere il divario. - Leggi le tue scritture dalla tabella base, non dalla GSI. Subito dopo una scrittura possiedi già la chiave primaria.
- Imponi l'unicità con una scrittura condizionale, non con una query sulla GSI. Il divario di propagazione trasforma un controllo "è già preso?" in una gara.
Il sintomo: un sign-up che "non riesce a trovare sé stesso"
Prendi una tabella Members per un servizio di account utente. La tabella base è
indicizzata su un id interno, ma gli utenti accedono tramite email, quindi c'è una
GSI di lookup per email:
| PK | SK | displayName | |
|---|---|---|---|
| ACC#a1f9c | PROFILE | ada@northwind.test | Ada L. |
| GSI1PK | GSI1SK |
|---|---|
| ada@northwind.test | ACC#a1f9c |
Il flusso di sign-up fa due cose una dietro l'altra: PutItem del nuovo membro,
poi Query EmailIndex WHERE GSI1PK = "ada@northwind.test" per verificare che
nessun altro abbia rivendicato quell'indirizzo e per caricare il profilo.
Esegui quelle due chiamate a pochi millisecondi di distanza e la Query può
restituire zero item. Falla di nuovo un secondo dopo e la riga c'è. La
scrittura non è fallita — l'indice semplicemente non era ancora stato aggiornato.
Perché succede: le GSI sono replicate in modo asincrono
Una GSI è una tabella separata, gestita internamente con le proprie partizioni e il proprio key schema. Non è mantenuta all'interno della stessa transazione della tua scrittura sulla tabella base.
Quando fai PutItem, DynamoDB committa in modo durevole sulla tabella base,
riconosce la tua scrittura, e poi propaga in modo asincrono il cambiamento a
ogni GSI. La
documentazione GSI di AWS
lo dice chiaramente: le GSI supportano solo letture eventualmente coerenti.
Il ritardo di propagazione tra una scrittura sulla tabella base e l'aggiornamento dell'indice è di solito una frazione di secondo — ma non è garantito né limitato sotto carico. Progettare come se lo fosse è la trappola.
Non è un bug; è il compromesso di design originale di Dynamo. Il paper Amazon Dynamo del 2007 scelse disponibilità e tolleranza alle partizioni rispetto alla coerenza forte.
Le GSI ereditano quella discendenza. L'accoppiamento debole è ciò che permette all'indice di scalare e restare scrivibile indipendentemente dalla tabella base.
Il divario tra il 200 OK e "replica il cambiamento" è la finestra in cui la tua
lettura dell'indice è obsoleta. Non c'è alcun flag di lettura coerente che
lo chiuda.
A differenza della tabella base — dove passi ConsistentRead = true per forzare
un GetItem/Query fortemente coerente — una GSI rifiuta categoricamente
quell'opzione.
Una LSI può essere letta in modo forte perché condivide le partizioni della tabella base; vedi GSI vs LSI per cui esiste questa distinzione.
Una trappola più sottile: valori vecchi obsoleti, non solo nuovi mancanti
Il caso della riga mancante è quello ovvio. Il bug più silenzioso è leggere un valore precedente obsoleto.
Diciamo che Ada cambia la sua email da ada@northwind.test a
ada.l@northwind.test. La tabella base si aggiorna atomicamente, ma per un
momento la GSI può ancora restituire la vecchia voce dell'indice.
Un lookup contro il nuovo valore manca, mentre il valore abbandonato risolve ancora.
Peggio: se interroghi la GSI e riscrivi basandoti su ciò che hai letto, puoi agire su un valore che non esiste più. Tratta ogni lettura GSI come uno snapshot che può essere in ritardo rispetto alla realtà.
Progetta per aggirarla — non combatterla
La finestra di propagazione è reale, quindi la soluzione è architetturale, non una manopola di retry da regolare. Quattro pattern, all'incirca in ordine di preferenza:
Leggi le tue scritture dalla tabella base. Subito dopo una scrittura possiedi già la chiave primaria (
ACC#a1f9c), quindi fai unGetItemfortemente coerente sulla tabella base invece di interrogare la GSI.La GSI è per l'altro access pattern — "ho un'email, trova l'account" — non per confermare la scrittura che hai appena fatto.
Imponi l'unicità con un item di guardia, non con la GSI. Non fidarti mai di una query sulla GSI per dimostrare che un'email non è rivendicata — il divario di propagazione la rende una gara che due sign-up simultanei possono perdere entrambi.
Scrivi invece un item di unicità dedicato con chiave sull'email stessa (
PK = "EMAIL#ada@northwind.test") dentro unTransactWriteItemscon unaConditionExpressiondiattribute_not_exists(PK).Condizioni fortemente coerenti sulla tabella base, applicate atomicamente, sono ciò che effettivamente impone l'unicità.
TransactWriteItems: - Put item membro (PK = ACC#a1f9c, SK = PROFILE) - Put item unicità (PK = EMAIL#ada@northwind.test) ConditionExpression: attribute_not_exists(PK)Se un secondo sign-up gareggia per lo stesso indirizzo, la sua condizione fallisce e l'intera transazione è rifiutata — niente GSI, niente ritardo di propagazione, niente doppia rivendicazione.
Costruisci e visualizza in anteprima quella condizione
attribute_not_existscon il DynamoDB Expression Builder prima di cablarla nel codice.Tollera il ritardo nell'UX. Quando la lettura GSI è davvero lo strumento giusto (login tramite email per un utente esistente), la finestra è sub-secondo e innocua — un account consolidato si è propagato da tempo.
Riserva il percorso fortemente coerente sulla tabella base solo per il momento read-after-write.
Ri-interroga, non assumere. Se un workflow deve osservare un item nuovissimo attraverso la GSI, tratta un risultato vuoto come "non ancora visibile", non "non esiste", e ri-interroga dopo un breve backoff.
Ma preferisci i pattern 1 e 2, che rimuovono del tutto le congetture.
Vedi tu stesso il divario di propagazione
Il modo più rapido per costruire intuizione è guardarlo accadere. In DynoTable metti un item nella tabella base e interroghi subito la GSI in una seconda scheda.
Su una tabella sotto carico coglierai occasionalmente l'indice in ritardo rispetto ai dati base, poi lo guarderai convergere al refresh successivo.
Vedere il ritardo con i tuoi dati fa attecchire la regola "leggi le tue scritture dalla tabella base" molto meglio di qualsiasi diagramma.
Trappole e prossimi passi
- Non basare la logica su una lettura GSI subito dopo la scrittura. I controlli di unicità, le conferme "la mia scrittura è atterrata" e i loop read-modify-write appartengono alla tabella base fortemente coerente.
- Non ricorrere a
ConsistentReadsu una GSI — non è consentito e darà errore. - Non modellare un access pattern come GSI quando la chiave base risponde già. Servi una lettura dalla chiave primaria e salti del tutto la finestra di propagazione.
Scegliere la giusta forma delle chiavi è tutto il gioco nel
single-table design; sapere quando una Query
batte uno Scan ti tiene fuori dall'indice in primo luogo
(Query vs Scan).
Costruisci e testa la tua ConditionExpression di unicità nel
DynamoDB Expression Builder. Poi
prova DynoTable per guardare le scritture sulla tabella base
propagarsi a una GSI in tempo reale, e progetta le tue chiavi così che la finestra
di coerenza eventuale non morda mai.