O Datastore é compatível com transações. Transação é uma operação ou um conjunto de operações atômico. Todas as operações na transação ocorrem ou nenhuma delas ocorre. Um aplicativo pode realizar várias operações e cálculos em uma única transação.
Como usar transações
Transação é um conjunto de operações do Datastore que ocorrem em uma ou mais entidades. Cada transação é certamente atômica, o que significa que transações jamais são aplicadas parcialmente. Todas as operações na transação são aplicadas ou nenhuma delas é aplicada. As transações têm uma duração máxima de 60 segundos, com um tempo de expiração por inatividade de 10 segundos após 30 segundos.
Uma operação poderá falhar quando:
- muitas modificações simultâneas forem tentadas no mesmo grupo de entidades;
- a transação exceder um limite de recursos;
- o Datastore encontrar um erro interno.
Em todos esses casos, a API do Datastore retornará um erro.
As transações são um recurso opcional do Datastore. Elas não precisam ser usadas para realizar operações do Datastore.
A função datastore.RunInTransaction
executa a função fornecida em uma
transação.
Se a função retorna nil
, RunInTransaction
tenta confirmar a
transação, retornando nil
se tiver êxito. Se a função retorna um valor de erro diferente de nil
,
alterações no Datastore não são aplicadas e retorna
RunInTransaction
o mesmo erro.
Se RunInTransaction
não conseguir confirmar a transação devido a um conflito, ele
tentará novamente, desistindo depois de três tentativas. Isso significa que a função da transação
precisa ser idempotente, ou seja, ela tem o mesmo resultado quando
executada várias vezes. Observe que datastore.Get
não é idempotente
ao desmembrar campos parciais.
O que pode ser feito em uma transação
O Datastore impõe restrições sobre o que pode ser feito dentro de uma única transação.
Quando a transação é feita em um único grupo, todas as operações do Datastore precisam operar em entidades do mesmo grupo. Quando a transação é feita entre grupos, ela precisa operar em um máximo de 25 grupos. Isso inclui consultar entidades por ancestral, recuperar entidades por chave, atualizar entidades e excluir entidades. Cada entidade raiz pertence a um grupo separado de entidades. Dessa forma, uma única transação não pode criar nem operar em mais de uma entidade raiz, a menos que seja uma transação entre grupos.
Quando duas ou mais transações tentam modificar entidades simultaneamente em um ou mais grupos de entidades comuns, somente a primeira transação que faz o commit das suas alterações pode ser bem-sucedida. Todas as outras falharão no commit. Por causa desse design, usar grupos de entidades limita o número de gravações simultâneas que você pode fazer em qualquer entidade nos grupos. Quando uma transação é iniciada, o Datastore usa o controle de simultaneidade otimista verificando o horário da última atualização dos grupos de entidades usados na transação. Após a confirmação de uma transação nos grupos de entidades, o Datastore verifica novamente o horário da última atualização dos grupos de entidades usados na transação. Se ela tiver sido alterada depois da verificação inicial, um erro será retornado.
Isolamento e consistência
Fora das transações, o nível de isolamento do Datastore está mais próximo da confirmação de leitura. Dentro, por sua vez, o isolamento serializável é imposto. Isso significa que outra transação não pode modificar simultaneamente os dados lidos ou modificados por essa transação.
Em uma transação, todas as leituras refletem o estado atual e consistente do Datastore no momento em que a transação foi iniciada. As consultas e os recebimentos dentro de uma transação garantem a visualização de um snapshot único e consistente do Datastore desde o início da transação. Entidades e linhas de índice no grupo de entidades da transação são totalmente atualizadas. Dessa forma, as consultas retornam o conjunto completo e correto de entidades de resultado, sem os falsos positivos ou falsos negativos que podem ocorrer em consultas fora das transações.
A visualização desse instantâneo consistente também se estende a leituras após gravações dentro de transações. Diferentemente da maioria dos bancos de dados, as consultas e os recebimentos dentro de uma transação do Datastore não exibem os resultados de gravações anteriores dentro dessa transação. Mais especificamente, se uma entidade tiver sido modificada ou excluída dentro de uma transação, a consulta ou o recebimento retornará a versão original da entidade desde o início da transação, ou nada, caso ela não exista.
Usos em transações
Este exemplo demonstra um uso de transações: atualizar uma entidade com um novo valor de propriedade relativo ao valor atual.
Isso requer uma transação porque o valor poderá ser atualizado por outro usuário depois que esse código buscar o objeto, mas antes de ele salvar o objeto modificado.
Sem uma transação, a solicitação do usuário usa o valor de count
antes da
atualização do outro usuário, e a gravação substitui o novo valor. Com uma
transação, o aplicativo é informado sobre a atualização.
Caso a entidade seja atualizada durante a transação, esta será repetida até que todas as etapas estejam concluídas sem interrupção.
Outro uso comum para transações é buscar uma entidade com uma chave nomeada ou criá-la caso ela ainda não exista:
Como já mencionado, uma transação é necessária para processar o caso em que outro usuário está tentando criar ou atualizar uma entidade com o mesmo código de string. Sem uma transação, se a entidade não existir e dois usuários tentarem criá-la, a segunda substituirá a primeira sem saber disso.
Quando uma transação falha, você pode fazer o app repetir a transação até ser bem-sucedido, ou pode deixar que os usuários lidem com o erro propagando-o até o nível de interface do usuário do app. Não é necessário criar um loop de repetição em cada transação.
Por fim, use a transação para ler um snapshot consistente do Datastore. Isso pode ser útil quando várias leituras são necessárias para renderizar uma página ou exportar dados que precisam ser consistentes. Esses tipos de transações costumam ser chamadas de transações somente leitura, porque elas não realizam gravações. As transações de grupo único somente leitura nunca falham por causa de modificações simultâneas. Dessa forma, você não precisa implementar repetições após uma falha. Porém, as transações entre grupos podem falhar por causa de modificações simultâneas. Dessa maneira, elas devem ter novas tentativas. O commit e a reversão de uma transação somente leitura são de ambiente autônomo.
Enfileiramento de tarefa transacional
É possível enfileirar uma tarefa como parte de uma transação do Datastore, de modo
que ela seja enfileirada e garantida para ser enfileirada apenas se a transação
for confirmada. Depois de enfileirada, a tarefa não terá garantia de execução
imediata. Dessa maneira, a tarefa não é atômica com a transação. Ainda assim,
uma vez enfileirada, a tarefa tentará novamente até conseguir. Isso se aplica a qualquer tarefa
enfileirada durante uma função RunInTransaction
.
As tarefas transacionais são úteis porque permitem combinar ações que não são do Datastore com uma transação que depende do êxito da transação, como o envio de um e-mail para confirmar uma compra. É possível também vincular ações do Datastore à transação, como confirmar alterações em grupos de entidades fora dela, apenas em caso de êxito dessa transação.
Um aplicativo não pode inserir mais de cinco tarefas transacionais em filas de tarefas durante uma única transação. As tarefas transacionais não têm nomes especificados pelo usuário.