Isolamento de transações no App Engine

Max Ross

De acordo com a Wikipédia, o nível de isolamento de um sistema de gerenciamento de banco de dados "define como/quando as alterações feitas por uma operação se tornam visíveis para outras operações simultâneas". O presente artigo tem como objetivo explicar o isolamento de consultas e transações no Cloud Datastore usado pelo App Engine. Após sua leitura, será possível compreender melhor como as leituras e as gravações simultâneas funcionam, tanto dentro quanto fora das transações.

Transações internas: Serializable

Ordenados do mais forte para o mais fraco, os quatro níveis de isolamento são: Serializable (Serializável), Repeatable Read (Leitura repetível), Read Committed (Leitura confirmada) e Read Uncommitted (Leitura não confirmada). As transações do Datastore satisfazem o nível de isolamento Serializable. Cada transação é completamente isolada das demais transações e operações do armazenamento de dados. Transações em um determinado grupo de entidades são executadas de forma serial, uma após a outra.

Para saber mais, consulte a seção Isolamento e consistência da documentação de transações, bem como o artigo da Wikipédia sobre isolamento de snapshots.

Transações externas: Read Committed

Transações externas de operações do Datastore devem ter grande semelhança com o nível de isolamento Read Committed. Entidades recuperadas do armazenamento de dados por consultas ou gets só terão acesso a dados confirmados. Uma entidade recuperada jamais apresentará dados parcialmente confirmados, ou seja, alguns anteriores à confirmação e outros posteriores. No entanto, a interação entre consultas e transações é um pouco mais sutil e, para entendê-la, é necessário analisar mais a fundo o processo de confirmação.

O processo de confirmação

Quando uma confirmação retorna com sucesso, a transação certamente será aplicada, mas isso não significa que o resultado de sua gravação ficará imediatamente visível para os leitores. Aplicar uma transação consiste em dois eventos:

  • Evento A: ponto em que as alterações em uma entidade foram aplicadas
  • Evento B: ponto em que as alterações nos índices da entidade foram aplicadas

Mostra setas de progresso da transação de confirmação para alterações da entidade em
          índices de mudança e entidades visíveis.

No Cloud Datastore, a transação costuma ser completamente aplicada em algumas centenas de milissegundos após a confirmação retornar. No entanto, mesmo que não seja completamente aplicada, leituras subsequentes, gravações e consultas de ancestral sempre refletirão os resultados da confirmação, porque essas operações aplicam quaisquer modificações pendentes antes da execução. Porém, as consultas que abrangem vários grupos de entidades não podem determinar se existem modificações pendentes antes da execução e podem retornar resultados desatualizados ou parcialmente aplicados.

Uma solicitação que pesquisa uma entidade atualizada por sua chave em um momento após um evento A garante a visualização da versão mais recente dessa entidade. Entretanto, se uma solicitação simultânea executar uma consulta cujo predicado (a cláusula WHERE, para os fãs de SQL/GQL) não for atendido pela entidade pré-atualização, mas sim pela entidade pós-atualização, a entidade fará parte do conjunto de resultados somente se a consulta for executada depois que a operação de aplicação tiver atingido o evento B.

Em outras palavras, durante breves janelas, é possível que um conjunto de resultados não inclua uma entidade cujas propriedades, de acordo com o resultado de uma pesquisa por chave, satisfaçam o predicado da consulta. Também é possível que um conjunto de resultados inclua uma entidade cujas propriedades, novamente de acordo com o resultado de uma pesquisa por chave, falhe em satisfazer o predicado da consulta. Uma consulta não pode considerar as transações que estão entre o evento A e o evento B ao decidir quais entidades retornar. Ela será realizada com dados desatualizados, mas executar uma operação get() nas chaves retornadas sempre conseguirá a versão mais recente dessa entidade. Isso significa que você pode estar perdendo resultados que correspondam à sua consulta ou recebendo resultados que não correspondam quando você tiver a entidade correspondente.

Existem situações em que qualquer modificação pendente tem a garantia de ser completamente aplicada antes da execução da consulta, como todas as consultas de ancestral no Cloud Datastore. Nesse caso, os resultados da consulta sempre serão atuais e consistentes.

Exemplos

Fornecemos uma explicação geral sobre como as atualizações e as consultas simultâneas interagem. No entanto, se você for como eu, esses conceitos podem ser mais fáceis de entender através de exemplos práticos. Vejamos alguns deles. Começaremos com alguns exemplos simples. Os mais interessantes deixaremos para o final.

Vamos supor que temos um aplicativo que armazena entidades Pessoa. Uma Pessoa possui as seguintes propriedades:

  • Nome
  • Altura

Esse aplicativo suporta as seguintes operações:

  • updatePerson()
  • getTallPeople(), que retorna todas as pessoas com mais de 1,83 m de altura.

Temos 2 entidades Pessoa no armazenamento de dados:

  • Adam, que tem 1,73 m de altura.
  • Bob, que tem 1,85 m de altura.

Exemplo 1: como deixar o Adam mais alto

Suponha que um aplicativo receba duas solicitações basicamente ao mesmo tempo. A primeira solicitação atualiza a altura de Adam de 1,73 m para 1,88 m. Um crescimento instantâneo! A segunda solicitação chama getTallPeople(). O que getTallPeople() retorna?

A resposta depende da relação entre os dois eventos de confirmação acionados pela Solicitação 1 e a consulta getTallPeople() executada pela Solicitação 2. Vamos supor que essa relação seja semelhante a:

  • Solicitação 1, put()
  • Solicitação 2, getTallPeople()
  • Solicitação 1, put()-->commit()
  • Solicitação 1, put()-->commit()-->evento A
  • Solicitação 1, put()-->commit()-->evento B

Nessa situação, getTallPeople() retornará apenas Bob. Por quê? Porque a atualização que aumenta a altura de Adam ainda não foi confirmada e, por isso, a alteração ainda não está visível para a consulta emitida na Solicitação 2.

Agora, vamos supor que a relação seja parecida com:

  • Solicitação 1, put()
  • Solicitação 1, put()-->commit()
  • Solicitação 1, put()-->commit()-->evento A
  • Solicitação 2, getTallPeople()
  • Solicitação 1, put()-->commit()-->evento B

Nessa situação, a consulta é executada antes que a Solicitação 1 atinja o evento B; por isso, as atualizações nos índices de Pessoa ainda não foram aplicadas. Como resultado, getTallPeople() retorna apenas Bob. Este é um exemplo de um conjunto de resultados que exclui uma entidade cujas propriedades satisfazem o predicado da consulta.

Exemplo 2: como deixar o Bob mais baixo (desculpe, Bob)

Neste exemplo, a Solicitação 1 fará uma coisa diferente. Em vez de aumentar a altura do Adam de 1,73 m para 1,88 m, diminuirá a altura do Bob de 1,85 m para 1,65 m. Mais uma vez, o que getTallPeople()

retorna?
  • Solicitação 1, put()
  • Solicitação 2, getTallPeople()
  • Solicitação 1, put()-->commit()
  • Solicitação 1, put()-->commit()-->evento A
  • Solicitação 1, put()-->commit()-->evento B

Nessa situação, getTallPeople() retorna apenas Bob. Por quê? Porque a atualização que diminui a altura do Bob ainda não foi confirmada e, por isso, a alteração ainda não está visível para a consulta emitida na Solicitação 2.

Agora, vamos supor que a relação seja parecida com:

  • Solicitação 1, put()
  • Solicitação 1, put()-->commit()
  • Solicitação 1, put()-->commit()-->evento A
  • Solicitação 1, put()-->commit()-->evento B
  • Solicitação 2, getTallPeople()

Nessa situação, getTallPeople() não retornará ninguém. Por quê? Porque a atualização que diminui a altura do Bob já estava confirmada no momento em que nossa consulta na Solicitação 2 foi emitida.

Agora, vamos supor que a relação seja parecida com:

  • Solicitação 1, put()
  • Solicitação 1, put()-->commit()
  • Solicitação 1, put()-->commit()-->evento A
  • Solicitação 2, getTallPeople()
  • Solicitação 1, put()-->commit()-->evento B

Nessa situação, a consulta é executada antes do evento B e, por isso, as atualizações nos índices de Pessoa ainda não foram aplicadas. Como resultado, getTallPeople() ainda retorna Bob, mas a propriedade de altura da entidade Pessoa que é retornada é o valor atualizado: 1,65. Este é um exemplo de um conjunto de resultados que inclui uma entidade cujas propriedades não satisfazem o predicado da consulta.

Conclusão

Como você pode ver nos exemplos acima, o nível de isolamento da transação do Cloud Datastore está muito próximo de Read Committed. Há, obviamente, diferenças significativas, mas agora que você compreende essas diferenças e as razões por trás delas, terá mais facilidade para tomar melhores decisões em projetos relacionados ao armazenamento de dados dos seus aplicativos.