Como estruturar dados para uma consistência forte

O Cloud Datastore oferece alta disponibilidade, escalabilidade e durabilidade na distribuição de dados em muitas máquinas e no uso de replicação síncrona sem mestre em uma ampla área geográfica. A desvantagem nesse design, no entanto, é que a capacidade de gravação de qualquer grupo de entidades está limitada a cerca de um commit por segundo. Há também limitações relacionadas às 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:

Java 8

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;
}

Java 7

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.

Java 8

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));
}

Java 7

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:

Java 8

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;
}

Java 7

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;
}

Em seguida, você poderá executar uma consulta de ancestral de consistência forte no grupo de entidades identificado pela entidade raiz comum:

Java 8

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));
}

Java 7

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. Caso o aplicativo esteja propenso a encontrar um uso de gravação intensivo, convém levar em consideração o uso de outros meios. Por exemplo, colocar postagens recentes em um memcache com uma expiração e exibir uma mistura de postagens recentes do memcache e do Cloud Datastore, ou armazená-las em um cookie, colocar algum 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 você executar o comando get, uma consulta de ancestral ou qualquer operação em uma transação, sempre verá os dados gravados mais recentemente.

Esta página foi útil? Conte sua opinião sobre:

Enviar comentários sobre…

Ambiente padrão do App Engine para Java 8