Intermediário8 min de leitura

Por que um GSI do DynamoDB É Eventualmente Consistente

Você escreve um item, imediatamente consulta um Índice Secundário Global por ele, e recebe nada de volta — mesmo que a escrita tenha sucedido e um GetItem na tabela base retorne o item normalmente.

Nada está quebrado. Você bateu na propriedade mais surpreendente dos GSIs: toda leitura de um GSI é eventualmente consistente. Há uma breve janela após uma escrita em que o índice ainda não alcançou.

Os GSIs do DynamoDB são eventualmente consistentes?

Sim — toda leitura de um Índice Secundário Global é eventualmente consistente, sem como desativar. Sua escrita commita primeiro na tabela base, depois propaga de forma assíncrona para o índice, então uma consulta feita logo após uma escrita pode retornar linhas obsoletas ou ausentes. O DynamoDB não oferece flag ConsistentRead para um GSI.

  • Um GSI é uma tabela separada, replicada de forma assíncrona — sua escrita commita na tabela base primeiro, depois propaga para o índice.
  • Não existe flag ConsistentRead para um GSI. Diferente da tabela base, você não pode forçar uma leitura forte para fechar a lacuna.
  • Leia suas próprias escritas a partir da tabela base, não do GSI. Você já tem a chave primária em mãos logo após uma escrita.
  • Imponha unicidade com uma escrita condicional, não com uma consulta a GSI. A lacuna de propagação transforma uma verificação "isto está tomado?" em uma corrida.

O sintoma: um cadastro que "não consegue achar a si mesmo"

Pegue uma tabela Members para um serviço de contas de usuário. A tabela base é chaveada por um id interno, mas os usuários fazem login por e-mail, então há um GSI de lookup por e-mail:

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

O fluxo de cadastro faz duas coisas seguidas: PutItem no novo membro, depois Query EmailIndex WHERE GSI1PK = "ada@northwind.test" para checar que ninguém mais reivindicou aquele endereço e para carregar o perfil.

Rode essas duas chamadas a poucos milissegundos de distância e o Query pode retornar zero itens. Faça de novo um segundo depois e a linha está lá. A escrita não falhou — o índice só ainda não tinha sido atualizado.

Por que isso acontece: GSIs são replicados de forma assíncrona

Um GSI é uma tabela separada, gerenciada internamente com suas próprias partições e seu próprio esquema de chaves. Ele não é mantido dentro da mesma transação que sua escrita na tabela base.

Quando você faz PutItem, o DynamoDB commita de forma durável na tabela base, confirma sua escrita, e depois propaga de forma assíncrona a mudança para cada GSI. A documentação de GSI da AWS afirma claramente: GSIs suportam apenas leituras eventualmente consistentes.

O atraso de propagação entre uma escrita na tabela base e a atualização do índice geralmente é uma fração de segundo — mas não é garantido e não é limitado sob carga. Projetar como se fosse limitado é a cilada.

Isso não é um bug; é o trade-off de design original do Dynamo. O artigo Dynamo da Amazon de 2007 escolheu disponibilidade e tolerância a partição em vez de consistência forte.

GSIs herdam essa linhagem. O acoplamento frouxo é o que deixa o índice escalar e permanecer gravável de forma independente da tabela base.

EmailIndexTabela baseAppEmailIndexTabela baseApppropagação assíncronaPutItem (novo membro)200 OKQuery por e-mail0 itens (obsoleto)replicar mudançaQuery por e-mail1 item (alcançou)

A lacuna entre o 200 OK e "replicar mudança" é a janela em que sua leitura de índice está obsoleta. Não há flag de leitura consistente que a feche.

Diferente da tabela base — onde você passa ConsistentRead = true para forçar um GetItem/Query fortemente consistente — um GSI rejeita categoricamente essa opção.

Um LSI pode ser lido de forma forte porque compartilha as partições da tabela base; veja GSI vs LSI para entender por que essa distinção existe.

Uma cilada mais sutil: valores antigos obsoletos, não só novos faltando

O caso da linha faltando é o óbvio. O bug mais silencioso é ler um valor anterior obsoleto.

Digamos que Ada troque o e-mail de ada@northwind.test para ada.l@northwind.test. A tabela base atualiza atomicamente, mas por um momento o GSI ainda pode retornar a antiga entrada de índice.

Um lookup contra o novo valor falha, enquanto o valor abandonado ainda resolve.

Pior: se você consulta o GSI e escreve de volta com base no que leu, você pode agir sobre um valor que não existe mais. Trate qualquer leitura de GSI como um snapshot que pode atrasar em relação à realidade.

Projete contornando — não lute contra

A janela de propagação é real, então a solução é arquitetural, não um botão de retry que você ajusta. Quatro padrões, mais ou menos em ordem de preferência:

  1. Leia suas próprias escritas a partir da tabela base. Logo após uma escrita você já tem a chave primária (ACC#a1f9c), então faça um GetItem fortemente consistente na tabela base em vez de consultar o GSI.

    O GSI é para o outro padrão de acesso — "tenho um e-mail, ache a conta" — não para confirmar a escrita que você acabou de fazer.

  2. Imponha unicidade com um item de guarda, não com o GSI. Nunca confie em uma consulta a GSI para provar que um e-mail está livre — a lacuna de propagação faz disso uma corrida que dois cadastros simultâneos podem ambos perder.

    Em vez disso, escreva um item de unicidade dedicado chaveado pelo próprio e-mail (PK = "EMAIL#ada@northwind.test") dentro de um TransactWriteItems com uma ConditionExpression de attribute_not_exists(PK).

    Condições fortemente consistentes na tabela base, aplicadas atomicamente, são o que de fato impõe unicidade.

    TransactWriteItems:
      - Put member item    (PK = ACC#a1f9c, SK = PROFILE)
      - Put uniqueness item (PK = EMAIL#ada@northwind.test)
          ConditionExpression: attribute_not_exists(PK)

    Se um segundo cadastro corre pelo mesmo endereço, sua condição falha e a transação inteira é rejeitada — sem GSI, sem atraso de propagação, sem dupla reivindicação.

    Construa e visualize essa condição attribute_not_exists com o DynamoDB Expression Builder antes de fiá-la no código.

  3. Tolere o atraso na UX. Quando a leitura de GSI genuinamente é a ferramenta certa (login por e-mail de um usuário existente), a janela é sub-segundo e inofensiva — uma conta estabelecida propagou há muito tempo.

    Reserve o caminho fortemente consistente da tabela base apenas para o momento de leitura-após-escrita.

  4. Re-consulte, não assuma. Se um fluxo precisa observar um item recém-criado pelo GSI, trate um resultado vazio como "ainda não visível", não "não existe", e re-consulte após um backoff curto.

    Mas prefira os padrões 1 e 2, que removem o chute por completo.

Veja a lacuna de propagação você mesmo

A forma mais rápida de construir intuição é vê-la acontecer. No DynoTable você coloca um item na tabela base e imediatamente consulta o GSI em uma segunda aba.

Em uma tabela sob carga, você ocasionalmente pega o índice atrasado em relação aos dados base, depois o vê convergir na próxima atualização.

Ver o atraso com seus próprios dados faz a regra "leia suas próprias escritas a partir da tabela base" grudar muito melhor que qualquer diagrama.

Armadilhas e próximos passos

  • Não baseie lógica em uma leitura-após-escrita de GSI. Verificações de unicidade, confirmações de "minha escrita entrou", e loops de read-modify-write pertencem à tabela base fortemente consistente.
  • Não recorra a ConsistentRead em um GSI — não é permitido e dará erro.
  • Não modele um padrão de acesso como GSI quando a chave base já o responde. Sirva uma leitura pela chave primária e você pula a janela de propagação por completo.

Escolher a forma de chave certa é o jogo inteiro no single-table design; saber quando um Query bate um Scan te mantém fora do índice em primeiro lugar (Query vs Scan).

Construa e teste sua ConditionExpression de unicidade no DynamoDB Expression Builder. Depois experimente o DynoTable para observar escritas na tabela base propagarem para um GSI em tempo real, e projete suas chaves para que a janela de consistência eventual nunca morda você.

Atualizado