Intermedio8 min de lectura

Por qué un GSI de DynamoDB es eventualmente consistente

Escribes un elemento, consultas inmediatamente un índice secundario global para buscarlo, y no obtienes nada de vuelta — aunque la escritura tuvo éxito y un GetItem sobre la tabla base devuelve el elemento sin problemas.

Nada está roto. Has topado con la propiedad más sorprendente de los GSI: toda lectura de un GSI es eventualmente consistente. Hay una breve ventana tras una escritura en la que el índice todavía no se ha puesto al día.

¿Los GSI de DynamoDB son eventualmente consistentes?

Sí — toda lectura de un índice secundario global es eventualmente consistente, sin forma de desactivarlo. Tu escritura se confirma primero en la tabla base y luego se propaga de forma asíncrona al índice, así que una consulta lanzada justo después de una escritura puede devolver filas obsoletas o ausentes. DynamoDB no ofrece ningún flag ConsistentRead para un GSI.

  • Un GSI es una tabla separada y replicada de forma asíncrona — tu escritura se confirma primero en la tabla base, luego se propaga al índice.
  • No existe ningún flag ConsistentRead para un GSI. A diferencia de la tabla base, no puedes forzar una lectura fuerte para cerrar la brecha.
  • Lee tus propias escrituras desde la tabla base, no desde el GSI. Ya tienes la clave primaria justo después de una escritura.
  • Impón la unicidad con una escritura condicional, no con una consulta a un GSI. La brecha de propagación convierte una comprobación de "¿está cogido?" en una carrera.

El síntoma: un registro que "no puede encontrarse a sí mismo"

Toma una tabla Members para un servicio de cuentas de usuario. La tabla base está indexada por un id interno, pero los usuarios inician sesión por correo electrónico, así que hay un GSI de búsqueda por correo:

Members (base table)
PKSKemaildisplayName
ACC#a1f9cPROFILEada@northwind.testAda L.
EmailIndex (GSI)
GSI1PKGSI1SK
ada@northwind.testACC#a1f9c

El flujo de registro hace dos cosas seguidas: PutItem del nuevo miembro, luego Query EmailIndex WHERE GSI1PK = "ada@northwind.test" para comprobar que nadie más reclamó esa dirección y para cargar el perfil.

Ejecuta esas dos llamadas con unos pocos milisegundos de diferencia y la Query puede devolver cero elementos. Hazlo de nuevo un segundo después y la fila está ahí. La escritura no falló — el índice simplemente todavía no se había actualizado.

Por qué ocurre esto: los GSI se replican de forma asíncrona

Un GSI es una tabla separada y gestionada internamente con sus propias particiones y su propio esquema de claves. No se mantiene dentro de la misma transacción que tu escritura en la tabla base.

Cuando haces PutItem, DynamoDB confirma de forma durable en la tabla base, reconoce tu escritura, y luego propaga el cambio de forma asíncrona a cada GSI. La documentación de GSI de AWS lo afirma sin rodeos: los GSI solo soportan lecturas eventualmente consistentes.

El retraso de propagación entre una escritura en 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 lo estuviera es la trampa.

Esto no es un bug; es el compromiso original del diseño de Dynamo. El artículo Amazon Dynamo de 2007 eligió disponibilidad y tolerancia a particiones por encima de la consistencia fuerte.

Los GSI heredan ese linaje. El acoplamiento débil es lo que permite que el índice escale y siga siendo escribible de forma independiente de la tabla base.

EmailIndexTabla baseAppEmailIndexTabla baseApppropagación asíncronaPutItem (nuevo miembro)200 OKQuery por correo0 elementos (obsoleto)replicar cambioQuery por correo1 elemento (al día)

La brecha entre el 200 OK y "replicar cambio" es la ventana en la que tu lectura del índice está obsoleta. No hay ningún flag de lectura consistente que la cierre.

A diferencia de la tabla base — donde pasas ConsistentRead = true para forzar un GetItem/Query fuertemente consistente — un GSI rechaza rotundamente esa opción.

Un LSI puede leerse de forma fuerte porque comparte las particiones de la tabla base; consulta GSI vs LSI para entender por qué existe esa distinción.

Una trampa más sutil: valores antiguos obsoletos, no solo nuevos faltantes

El caso de la fila faltante es el obvio. El bug más silencioso es leer un valor anterior obsoleto.

Digamos que Ada cambia su correo de ada@northwind.test a ada.l@northwind.test. La tabla base se actualiza atómicamente, pero por un momento el GSI todavía puede devolver la entrada de índice antigua.

Una búsqueda contra el valor nuevo no encuentra nada, mientras que el valor abandonado todavía resuelve.

Peor: si consultas el GSI y reescribes basándote en lo que leíste, puedes actuar sobre un valor que ya no existe. Trata cualquier lectura de un GSI como una instantánea que puede ir con retraso respecto a la realidad.

Diséñalo para sortearlo — no luches contra ello

La ventana de propagación es real, así que el arreglo es arquitectónico, no una perilla de reintentos que activas. Cuatro patrones, aproximadamente en orden de preferencia:

  1. Lee tus propias escrituras desde la tabla base. Justo después de una escritura ya tienes la clave primaria (ACC#a1f9c), así que haz un GetItem fuertemente consistente sobre la tabla base en lugar de consultar el GSI.

    El GSI es para el otro patrón de acceso — "tengo un correo, encuentra la cuenta" — no para confirmar la escritura que acabas de hacer.

  2. Impón la unicidad con un elemento guarda, no con el GSI. Nunca confíes en una consulta a un GSI para probar que un correo no está reclamado — la brecha de propagación convierte eso en una carrera que dos registros simultáneos pueden perder ambos.

    En su lugar, escribe un elemento de unicidad dedicado indexado por el propio correo (PK = "EMAIL#ada@northwind.test") dentro de un TransactWriteItems con una ConditionExpression de attribute_not_exists(PK).

    Las condiciones fuertemente consistentes de la tabla base, aplicadas atómicamente, son lo que realmente impone la unicidad.

    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 registro compite por la misma dirección, su condición falla y toda la transacción se rechaza — sin GSI, sin retraso de propagación, sin doble reclamación.

    Construye y previsualiza esa condición attribute_not_exists con el Constructor de expresiones de DynamoDB antes de conectarla al código.

  3. Tolera el retraso en la UX. Cuando la lectura del GSI es genuinamente la herramienta correcta (inicio de sesión por correo de un usuario existente), la ventana es de menos de un segundo e inofensiva — una cuenta establecida se propagó hace mucho.

    Reserva la ruta fuertemente consistente de la tabla base solo para el momento de lectura-tras-escritura.

  4. Vuelve a consultar, no asumas. Si un flujo de trabajo debe observar un elemento recién-creado a través del GSI, trata un resultado vacío como "todavía no visible", no como "no existe", y vuelve a consultar tras un breve retroceso.

    Pero prefiere los patrones 1 y 2, que eliminan las suposiciones por completo.

Ve tú mismo la brecha de propagación

La forma más rápida de construir intuición es verlo suceder. En DynoTable colocas un elemento en la tabla base e inmediatamente consultas el GSI en una segunda pestaña.

En una tabla con carga ocasionalmente pillarás el índice yendo por detrás de los datos base, y luego lo verás converger en la siguiente actualización.

Ver el retraso con tus propios datos hace que la regla "lee tus propias escrituras desde la tabla base" se quede mucho mejor que cualquier diagrama.

Trampas y próximos pasos

  • No condiciones la lógica a una lectura-tras-escritura de un GSI. Las comprobaciones de unicidad, las confirmaciones de "¿aterrizó mi escritura?" y los bucles de lectura-modificación-escritura pertenecen a la tabla base fuertemente consistente.
  • No recurras a ConsistentRead en un GSI — no está permitido y dará error.
  • No modeles un patrón de acceso como un GSI cuando la clave base ya lo responde. Sirve una lectura desde la clave primaria y te saltas la ventana de propagación por completo.

Elegir la forma de clave correcta es todo el juego en el diseño de tabla única; saber cuándo una Query gana a un Scan te mantiene fuera del índice en primer lugar (Query vs Scan).

Construye y prueba tu ConditionExpression de unicidad en el Constructor de expresiones de DynamoDB. Luego prueba DynoTable para ver las escrituras de la tabla base propagarse a un GSI en tiempo real, y diseña tus claves para que la ventana de consistencia eventual nunca te muerda.

Actualizado