Como estruturar dados com uma consistência forte

Observação: os desenvolvedores que criam novos aplicativos são bastante incentivados a usar a Biblioteca de cliente NDB, que oferece diversos benefícios em comparação com esta biblioteca de cliente, como armazenamento em cache automático de entidades por meio da API Memcache. Se você estiver usando a antiga biblioteca de cliente DB, leia o Guia de migração de DB para NDB.

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:

import webapp2
from google.appengine.ext import db

class Guestbook(webapp2.RequestHandler):
  def post(self):
    greeting = Greeting()
    ...

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

import webapp2
from google.appengine.ext import db

class MainPage(webapp2.RequestHandler):
  def get(self):
    self.response.out.write('<html><body>')
    greetings = db.GqlQuery("SELECT * "
                            "FROM Greeting "
                            "ORDER BY date DESC LIMIT 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:

import webapp2
from google.appengine.ext import db

class Guestbook(webapp2.RequestHandler):
  def post(self):
    guestbook_name=self.request.get('guestbook_name')
    greeting = Greeting(parent=guestbook_key(guestbook_name))
    ...

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

import webapp2
from google.appengine.ext import db

class MainPage(webapp2.RequestHandler):
  def get(self):
    self.response.out.write('<html><body>')
    guestbook_name=self.request.get('guestbook_name')

    greetings = db.GqlQuery("SELECT * "
                            "FROM Greeting "
                            "WHERE ANCESTOR IS :1 "
                            "ORDER BY date DESC LIMIT 10",
                            guestbook_key(guestbook_name))

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.