Pourquoi un GSI DynamoDB est à cohérence à terme
Tu écris un item, tu interroges immédiatement un Global Secondary Index pour le
trouver, et tu n'obtiens rien — alors même que l'écriture a réussi et qu'un
GetItem sur la table de base renvoie l'item sans problème.
Rien n'est cassé. Tu as heurté la propriété la plus surprenante des GSI : chaque lecture d'un GSI est à cohérence à terme. Il y a une brève fenêtre après une écriture pendant laquelle l'index n'a pas encore rattrapé.
Les GSI DynamoDB sont-ils à cohérence à terme ?
Oui — chaque lecture d'un Global Secondary Index est à cohérence à terme, sans aucun moyen de s'y soustraire. Ton écriture est d'abord validée sur la table de base, puis propagée de façon asynchrone vers l'index, si bien qu'une requête lancée juste après une écriture peut renvoyer des lignes périmées ou manquantes. DynamoDB n'offre aucun flag ConsistentRead pour un GSI.
- Un GSI est une table séparée, répliquée de façon asynchrone — ton écriture est d'abord validée sur la table de base, puis propagée vers l'index.
- Aucun flag
ConsistentReadn'existe pour un GSI. Contrairement à la table de base, tu ne peux pas forcer une lecture forte pour combler l'écart. - Lis tes propres écritures depuis la table de base, pas depuis le GSI. Tu tiens déjà la clé primaire juste après une écriture.
- Impose l'unicité avec une écriture conditionnelle, pas une requête GSI. L'écart de propagation transforme une vérification « est-ce pris ? » en course.
Le symptôme : une inscription qui « ne se trouve pas elle-même »
Prends une table Members pour un service de comptes utilisateurs. La table de base
est clée par un id interne, mais les utilisateurs se connectent par email, donc il y a
un GSI de recherche par email :
| PK | SK | displayName | |
|---|---|---|---|
| ACC#a1f9c | PROFILE | ada@northwind.test | Ada L. |
| GSI1PK | GSI1SK |
|---|---|
| ada@northwind.test | ACC#a1f9c |
Le flux d'inscription fait deux choses coup sur coup : PutItem du nouveau membre,
puis Query EmailIndex WHERE GSI1PK = "ada@northwind.test" pour vérifier que personne
d'autre n'a réclamé cette adresse et pour charger le profil.
Lance ces deux appels à quelques millisecondes d'écart et le Query peut renvoyer
zéro item. Refais-le une seconde plus tard et la ligne est là. L'écriture n'a pas
échoué — l'index n'avait juste pas encore été mis à jour.
Pourquoi ça arrive : les GSI sont répliqués de façon asynchrone
Un GSI est une table séparée, gérée en interne avec ses propres partitions et son propre schéma de clés. Il n'est pas maintenu dans la même transaction que ton écriture sur la table de base.
Quand tu fais PutItem, DynamoDB valide durablement sur la table de base, accuse
réception de ton écriture, puis propage de façon asynchrone le changement vers
chaque GSI. La
documentation GSI
d'AWS le dit clairement : les GSI ne supportent que les lectures à cohérence à
terme.
Le délai de propagation entre une écriture sur la table de base et la mise à jour de l'index est généralement une fraction de seconde — mais il n'est ni garanti ni borné sous charge. Concevoir comme s'il l'était est le piège.
Ce n'est pas un bug ; c'est le compromis de conception original de Dynamo. Le papier Amazon Dynamo de 2007 a choisi la disponibilité et la tolérance au partitionnement plutôt que la cohérence forte.
Les GSI héritent de cette lignée. Le couplage lâche est ce qui laisse l'index monter en charge et rester accessible en écriture indépendamment de la table de base.
L'écart entre le 200 OK et « réplique le changement » est la fenêtre où ta lecture
d'index est périmée. Il n'y a aucun flag de lecture cohérente qui le comble.
Contrairement à la table de base — où tu passes ConsistentRead = true pour forcer un
GetItem/Query fortement cohérent — un GSI rejette catégoriquement cette option.
Un LSI peut être lu fortement parce qu'il partage les partitions de la table de base ; vois GSI vs LSI pour comprendre pourquoi cette distinction existe.
Un piège plus subtil : des anciennes valeurs périmées, pas juste des nouvelles manquantes
Le cas de la ligne manquante est l'évident. Le bug plus discret est de lire une valeur précédente périmée.
Disons qu'Ada change son email de ada@northwind.test à ada.l@northwind.test. La
table de base se met à jour atomiquement, mais pendant un moment le GSI peut encore
renvoyer l'ancienne entrée d'index.
Une recherche sur la nouvelle valeur échoue, tandis que la valeur abandonnée se résout encore.
Pire : si tu interroges le GSI et réécris d'après ce que tu lis, tu peux agir sur une valeur qui n'existe plus. Traite toute lecture de GSI comme un instantané qui peut être en retard sur la réalité.
Conçois autour — ne le combats pas
La fenêtre de propagation est réelle, donc le correctif est architectural, pas un bouton de réessai que tu actionnes. Quatre motifs, à peu près par ordre de préférence :
Lis tes propres écritures depuis la table de base. Juste après une écriture tu tiens déjà la clé primaire (
ACC#a1f9c), donc fais unGetItemfortement cohérent sur la table de base au lieu d'interroger le GSI.Le GSI sert l'autre motif d'accès — « j'ai un email, trouve le compte » — pas à confirmer l'écriture que tu viens de faire.
Impose l'unicité avec un item garde, pas le GSI. Ne fais jamais confiance à une requête GSI pour prouver qu'un email n'est pas réclamé — l'écart de propagation en fait une course que deux inscriptions simultanées peuvent toutes deux perdre.
À la place, écris un item d'unicité dédié clé sur l'email lui-même (
PK = "EMAIL#ada@northwind.test") à l'intérieur d'unTransactWriteItemsavec uneConditionExpressiondeattribute_not_exists(PK).Des conditions fortement cohérentes sur la table de base, appliquées atomiquement, sont ce qui impose réellement l'unicité.
TransactWriteItems: - Put member item (PK = ACC#a1f9c, SK = PROFILE) - Put uniqueness item (PK = EMAIL#ada@northwind.test) ConditionExpression: attribute_not_exists(PK)Si une seconde inscription court pour la même adresse, sa condition échoue et toute la transaction est rejetée — pas de GSI, pas de délai de propagation, pas de double revendication.
Construis et prévisualise cette condition
attribute_not_existsavec le DynamoDB Expression Builder avant de la câbler dans le code.Tolère le retard dans l'UX. Quand la lecture du GSI est véritablement le bon outil (connexion par email pour un utilisateur existant), la fenêtre est inférieure à la seconde et inoffensive — un compte établi a propagé il y a longtemps.
Réserve le chemin fortement cohérent de la table de base au seul moment lecture-après-écriture.
Réinterroge, ne suppose pas. Si un flux doit observer un item tout neuf à travers le GSI, traite un résultat vide comme « pas encore visible », pas « n'existe pas », et réinterroge après un court backoff.
Mais préfère les motifs 1 et 2, qui retirent entièrement la conjecture.
Vois l'écart de propagation toi-même
Le moyen le plus rapide de bâtir une intuition est de le regarder se produire. Dans DynoTable tu mets un item dans la table de base et interroges immédiatement le GSI dans un second onglet.
Sur une table chargée tu attraperas occasionnellement l'index en retard sur les données de base, puis tu le regarderas converger au rafraîchissement suivant.
Voir le retard avec tes propres données fait bien mieux coller la règle « lis tes propres écritures depuis la table de base » que n'importe quel diagramme.
Pièges et étapes suivantes
- Ne base pas ta logique sur une lecture-après-écriture d'un GSI. Les vérifications d'unicité, les confirmations « mon écriture est-elle arrivée », et les boucles read-modify-write appartiennent à la table de base fortement cohérente.
- Ne sors pas
ConsistentReadsur un GSI — ce n'est pas autorisé et ça lèvera une erreur. - Ne modélise pas un motif d'accès en GSI quand la clé de base y répond déjà. Sers une lecture depuis la clé primaire et tu sautes entièrement la fenêtre de propagation.
Choisir la bonne forme de clé est tout le jeu dans le
single-table design ; savoir quand un Query bat un
Scan te garde hors de l'index dès le départ
(Query vs Scan).
Construis et teste ta ConditionExpression d'unicité dans le
DynamoDB Expression Builder. Puis
essaie DynoTable pour regarder les écritures de la table de base se
propager vers un GSI en temps réel, et conçois tes clés pour que la fenêtre de
cohérence à terme ne te morde jamais.