Como estruturar dados com uma consistência forte

O Datastore fornece alta disponibilidade, escalonabilidade e durabilidade ao distribuir dados em muitas máquinas e usar a replicação síncrona em uma ampla área geográfica. No entanto, a desvantagem desse design é que a capacidade de gravação de qualquer grupo de entidades está limitada a uma confirmação por segundo, aproximadamente. Há também limitações relacionadas a consultas ou transações que abrangem diversos grupos de entidades. Nesta página, mostramos essas limitações com detalhes e discutimos as práticas recomendadas para que os dados sejam compatíveis com uma consistência forte e ainda atendam aos requisitos de capacidade de gravação do aplicativo.

As leituras de consistência forte sempre retornam dados atuais e quando realizadas dentro de uma transação, surgem provenientes de um instantâneo único e consistente. No entanto, as consultas precisam especificar um filtro ancestral de consistência forte ou participar de uma transação e estas envolvem no máximo 25 grupos de entidades. As leituras de consistência eventual não têm essas limitações e são adequadas em muitos casos. O uso de leituras de consistência eventual permite a distribuição dos dados entre um número maior de grupos de entidades e faz com que você receba maior capacidade de gravação, executando commits em paralelo nos diferentes grupos de entidades. No entanto, é preciso entender as características das leituras de consistência eventual para determinar se elas são adequadas ao aplicativo:

  • Os resultados dessas leituras talvez não reflitam as transações mais recentes. Isso pode ocorrer porque essas leituras não garantem que a réplica na qual elas estão sendo executadas está atualizada. Em vez disso, elas usam os dados disponíveis na réplica no momento da execução da consulta. A latência da replicação é quase sempre inferior a alguns segundos.
  • Uma transação commit que abrange diversas entidades talvez tenha sido aplicada em algumas entidades e não em outras. Observe, no entanto, que uma transação nunca parecerá ter sido parcialmente aplicada em uma única entidade.
  • Os resultados da consulta incluem entidades que não atendem aos critérios de filtro e excluem entidades que atendem a esses mesmos critérios. Isso ocorre porque os índices podem ser lidos em uma versão diferente da que a própria entidade é lida.

Para compreender como estruturar os dados para consistência forte, compare duas abordagens diferentes de um aplicativo simples de livro de visitas. A primeira abordagem cria uma nova entidade raiz para cada entidade criada:

protected Entity createGreeting(
    DatastoreService datastore, User user, Date date, String content) {
  // No parent key specified, so Greeting is a root entity.
  Entity greeting = new Entity("Greeting");
  greeting.setProperty("user", user);
  greeting.setProperty("date", date);
  greeting.setProperty("content", content);

  datastore.put(greeting);
  return greeting;
}

Em seguida, consulta o tipo de entidade Greeting para as dez saudações mais recentes.

protected List<Entity> listGreetingEntities(DatastoreService datastore) {
  Query query = new Query("Greeting").addSort("date", Query.SortDirection.DESCENDING);
  return datastore.prepare(query).asList(FetchOptions.Builder.withLimit(10));
}

No entanto, como você usa uma consulta que não é de ancestral, a réplica usada para executá-la nesse esquema talvez não tenha visto a nova saudação no momento da execução. No entanto, quase todas as gravações estarão disponíveis para consultas não ancestrais dentro de alguns segundos do commit. Para muitos aplicativos, normalmente uma solução que fornece os resultados de uma consulta não ancestral no contexto das próprias alterações do usuário atual será suficiente para tornar essas latências de replicação completamente aceitáveis.

Caso a consistência forte seja importante para o aplicativo, uma abordagem alternativa é gravar entidades com um caminho ancestral que identifique a mesma entidade raiz em todas as entidades a serem lidas em uma única consulta de ancestral de consistência forte:

protected Entity createGreeting(
    DatastoreService datastore, User user, Date date, String content) {
  // String guestbookName = "my guestbook"; -- Set elsewhere (injected to the constructor).
  Key guestbookKey = KeyFactory.createKey("Guestbook", guestbookName);

  // Place greeting in the same entity group as guestbook.
  Entity greeting = new Entity("Greeting", guestbookKey);
  greeting.setProperty("user", user);
  greeting.setProperty("date", date);
  greeting.setProperty("content", content);

  datastore.put(greeting);
  return greeting;
}

Será possível executar uma consulta de ancestral de consistência forte no grupo de entidades identificado pela entidade raiz comum:

protected List<Entity> listGreetingEntities(DatastoreService datastore) {
  Key guestbookKey = KeyFactory.createKey("Guestbook", guestbookName);
  Query query =
      new Query("Greeting", guestbookKey)
          .setAncestor(guestbookKey)
          .addSort("date", Query.SortDirection.DESCENDING);
  return datastore.prepare(query).asList(FetchOptions.Builder.withLimit(10));
}

Esta abordagem atinge uma consistência forte na gravação para um único grupo de entidades por livro de visitas, mas também limita as alterações nele para não mais do que 1 gravação por segundo, limite suportado para grupos de entidades. Se o aplicativo tiver maior probabilidade de uso de gravação, talvez seja necessário usar outros meios: por exemplo, é possível colocar postagens recentes em um memcache com uma expiração e exibir uma combinação de postagens recentes do memcache e do Datastore, ou então armazená-los em um cookie, colocar um pouco de estado no URL ou algo totalmente diferente. O objetivo é encontrar uma solução de armazenamento em cache que forneça os dados ao usuário atual no período de tempo em que o usuário estiver postando no aplicativo. Se executar o comando "get", uma consulta de ancestral ou qualquer operação em uma transação, você sempre verá os dados gravados mais recentemente.