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
ConsistentReadpara 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:
| PK | SK | displayName | |
|---|---|---|---|
| ACC#a1f9c | PROFILE | ada@northwind.test | Ada L. |
| GSI1PK | GSI1SK |
|---|---|
| ada@northwind.test | ACC#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.
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:
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 umGetItemfortemente 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.
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 umTransactWriteItemscom umaConditionExpressiondeattribute_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_existscom o DynamoDB Expression Builder antes de fiá-la no código.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.
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
ConsistentReadem 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ê.