Como estruturar dados para uma forte consistência

O Datastore fornece alta disponibilidade, escalonabilidade e durabilidade ao distribuir dados em muitas máquinas e usar replicação síncrona sem mestre 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 cerca de uma confirmação por segundo. Há também limitações relacionadas às consultas ou transações que abrangem diversos grupos. 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 se 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 confirmações 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 de 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;
    }

Você conseguirá executar uma consulta de ancestral de consistência forte dentro do 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));
    }

Essa abordagem alcança consistência forte ao gravar em um único grupo de entidades por livro de visitas, mas também limita as alterações no livro de visitas a até uma gravação por segundo, que é o limite permitido para grupos de entidades. Se seu 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 Datastore, ou 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 para o 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.