Por qué un GSI de DynamoDB es de consistencia eventual
Escribes un item, haces Query de inmediato a un Global Secondary Index por él y
obtienes nada de vuelta — aunque la escritura tuvo éxito y un GetItem a la
tabla base devuelve el item bien.
Nada está roto. Has golpeado la propiedad más sorprendente de los GSI: cada lectura de un GSI es de consistencia eventual. Hay una ventana breve tras una escritura en la que el índice aún no se ha puesto al día.
¿Son los GSI de DynamoDB de consistencia eventual?
Sí — cada lectura de un Global Secondary Index es de consistencia eventual, sin
forma de optar out. Tu escritura hace commit primero a la tabla base y luego se
propaga de forma asíncrona al índice, así que un lanzado justo
después de una escritura puede devolver filas stale o que faltan. DynamoDB no
ofrece un flag ConsistentRead para un GSI.
- Un GSI es una tabla aparte, replicada de forma asíncrona — tu escritura hace commit primero a la tabla base y luego se propaga al índice.
- No existe un flag
ConsistentReadpara un GSI. A diferencia de la tabla base, no puedes forzar una lectura strong para cerrar el hueco. - Lee tus propias escrituras desde la tabla base, no desde el GSI. Ya tienes la justo después de una escritura.
- Enforce uniqueness con una escritura condicional, no con un Query al GSI. El hueco de propagación convierte un check de «¿está cogido?» en una race.
El síntoma: un sign-up que «no puede encontrarse a sí mismo»
Toma una tabla Members para un servicio de cuentas de usuario. La tabla base
está claveada por un id interno, pero los usuarios hacen login por email, así
que hay un GSI de lookup por email:
| PK | SK | displayName | |
|---|---|---|---|
| ACC#a1f9c | PROFILE | ada@northwind.test | Ada L. |
| GSI1PK | GSI1SK |
|---|---|
| ada@northwind.test | ACC#a1f9c |
El flujo de sign-up hace dos cosas seguidas: PutItem del miembro nuevo, luego
Query EmailIndex WHERE GSI1PK = "ada@northwind.test" para comprobar que nadie
más reclamó esa dirección y para cargar el perfil.
Lanza esas dos llamadas a unos pocos milisegundos de distancia y el Query
puede devolver cero items. Hazlo otra vez un segundo después y la fila está
ahí. La escritura no falló — el índice simplemente aún no se había actualizado.
Por qué pasa: los GSI se replican de forma asíncrona
Un GSI es una tabla aparte, gestionada internamente, con sus propias particiones y su propio key schema. No se mantiene dentro de la misma transacción que tu escritura a la tabla base.
Cuando haces PutItem, DynamoDB hace commit durable a la tabla base, te
reconoce la escritura y luego propaga el cambio de forma asíncrona a cada
GSI. La
documentación del GSI
de AWS lo dice claro: los GSI solo soportan lecturas de consistencia eventual.
El delay de propagación entre una escritura a la tabla base y la actualización del índice suele ser una fracción de segundo — pero no está garantizado ni acotado bajo carga. Diseñar como si estuviera acotado es la trampa.
Ese delay es el trade-off original del diseño de Dynamo. El paper de Amazon Dynamo de 2007 eligió disponibilidad y tolerancia a particiones sobre consistencia fuerte.
Los GSI heredan ese linaje. El acoplamiento suelto es lo que deja que el índice escale y se quede escribible de forma independiente de la tabla base.
El hueco entre el 200 OK y «replicar cambio» es la ventana donde tu lectura
del índice está stale. No hay flag de consistent-read que lo cierre.
A diferencia de la tabla base — donde pasas ConsistentRead = true para forzar
un GetItem/Query — un GSI
rechaza de plano esa opción.
Un LSI sí puede leerse de forma strong porque comparte las particiones de la tabla base; ver GSI vs LSI para por qué existe esa distinción.
Coste de lectura en un GSI
Los Queries a GSI facturan el RCU on-demand del índice en us-east-1 como
cualquier otra lectura — 0,5 RCU por bloque de 4 KB de consistencia eventual
— y no puedes pagar el doble por consistencia strong porque ConsistentRead se
rechaza. El lag de propagación es gratis; la lectura del índice no. Compara
tasas de lectura de tabla base vs GSI en la
calculadora de precios.
Una trampa más sutil: valores viejos stale, no solo nuevos que faltan
El caso de la fila que falta es el obvio. El bug más silencioso es leer un valor previo stale.
Digamos que Ada cambia su email de ada@northwind.test a
ada.l@northwind.test. La tabla base se actualiza de forma atómica, pero durante
un momento el GSI puede seguir devolviendo la entrada vieja del índice.
Un lookup contra el valor nuevo falla, mientras el valor abandonado sigue resolviendo.
Peor: si haces Query al GSI y escribes de vuelta basándote en lo que leíste, puedes actuar sobre un valor que ya no existe. Trata cualquier lectura de GSI como un snapshot que puede ir por detrás de la realidad.
Diseña a su alrededor — no luches contra ello
La ventana de propagación es real, así que el arreglo es arquitectónico, no un knob de retry que volteas. Cuatro patrones, más o menos en orden de preferencia:
Lee tus propias escrituras desde la tabla base. Justo después de una escritura ya tienes la primary key (
ACC#a1f9c), así que haz unGetItemstrongly consistent sobre la tabla base en lugar de hacer Query al GSI.El GSI es para el otro access pattern — «tengo un email, encuentra la cuenta» — no para confirmar la escritura que acabas de hacer.
Enforce uniqueness con un item guarda, no con el GSI. Nunca confíes en un Query al GSI para probar que un email no está reclamado — el hueco de propagación convierte eso en una race que dos sign-ups simultáneos pueden perder ambos.
En su lugar, escribe un item de uniqueness dedicado claveado en el email mismo (
PK = "EMAIL#ada@northwind.test") dentro de unTransactWriteItemscon unConditionExpressiondeattribute_not_exists(PK).Las condiciones strongly consistent de la tabla base, aplicadas de forma atómica, son lo que de verdad enforce uniqueness.
TransactWriteItems: - Put member item (PK = ACC#a1f9c, SK = PROFILE) - Put uniqueness item (PK = EMAIL#ada@northwind.test) ConditionExpression: attribute_not_exists(PK)Si un segundo sign-up hace race por la misma dirección, su condición falla y toda la se rechaza — sin GSI, sin delay de propagación, sin doble-claim.
Construye y previsualiza esa condición
attribute_not_existscon el generador de expresiones de DynamoDB antes de cablearla al código.Tolera el lag en la UX. Cuando la lectura del GSI es de verdad la herramienta correcta (login por email para un usuario existente), la ventana es sub-segundo e inofensiva — una cuenta establecida se propagó hace mucho.
Reserva el path strongly consistent de la tabla base solo para el momento de read-after-write.
Re-query, no asumas. Si un workflow debe observar un item brand-new a través del GSI, trata un resultado vacío como «aún no visible», no como «no existe», y re-query tras un backoff corto.
Pero prefiere los patrones 1 y 2, que quitan la adivinanza del todo.
Mira el hueco de propagación tú mismo
La forma más rápida de construir intuición es verlo pasar. En DynoTable pones un item en la tabla base y de inmediato haces Query al GSI en una segunda pestaña.
En una tabla cargada ocasionalmente pillas el índice yendo por detrás de los datos base, y luego lo ves converger en el siguiente refresh.
Ver el lag con tus propios datos hace que la regla «lee tus propias escrituras desde la tabla base» se pegue mucho mejor que cualquier diagrama.
Escollos y próximos pasos
- No gates lógica en un read-after-write de GSI. Los checks de uniqueness, las confirmaciones de «¿aterrizó mi escritura?» y los bucles read-modify-write pertenecen a la tabla base strongly consistent.
- No llegues a
ConsistentReaden un GSI — no está permitido y dará error. - No modeles un access pattern como GSI cuando la clave base ya lo responde. Sirve una lectura desde la primary key y te saltas la ventana de propagación del todo.
Elegir la forma de clave correcta es todo el juego en el
single-table design; saber cuándo un
Query gana a un Scan te mantiene fuera del índice de entrada
(Query vs Scan).
Construye y prueba tu ConditionExpression de uniqueness en el
DynamoDB Expression Builder. Luego
prueba DynoTable para ver cómo las escrituras de la tabla base se
propagan a un GSI en tiempo real, y diseña tus claves para que la ventana de
consistencia eventual nunca te muerda.