Transações

O Datastore suporta transações. Uma transação é uma operação ou um conjunto de operações que é atómico: todas as operações na transação ocorrem ou nenhuma delas ocorre. Uma aplicação pode realizar várias operações e cálculos numa única transação.

Usar transações

Uma transação é um conjunto de operações do Datastore numa ou mais entidades. Cada transação tem a garantia de ser atómica, o que significa que as transações nunca 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 de inatividade de 10 segundos após 30 segundos.

Uma operação pode falhar quando:

  • Foram tentadas demasiadas modificações simultâneas no mesmo grupo de entidades.
  • A transação excede um limite de recursos.
  • O Datastore encontra um erro interno.

Em todos estes casos, a API Datastore devolve um erro.

As transações são uma funcionalidade opcional do Datastore. Não tem de usar transações para realizar operações do Datastore.

A função datastore.RunInTransaction executa a função fornecida numa transação.


package counter

import (
	"context"
	"fmt"
	"net/http"

	"google.golang.org/appengine"
	"google.golang.org/appengine/datastore"
	"google.golang.org/appengine/log"
	"google.golang.org/appengine/taskqueue"
)

func init() {
	http.HandleFunc("/", handler)
}

type Counter struct {
	Count int
}

func handler(w http.ResponseWriter, r *http.Request) {
	ctx := appengine.NewContext(r)

	key := datastore.NewKey(ctx, "Counter", "mycounter", 0, nil)
	count := new(Counter)
	err := datastore.RunInTransaction(ctx, func(ctx context.Context) error {
		// Note: this function's argument ctx shadows the variable ctx
		//       from the surrounding function.
		err := datastore.Get(ctx, key, count)
		if err != nil && err != datastore.ErrNoSuchEntity {
			return err
		}
		count.Count++
		_, err = datastore.Put(ctx, key, count)
		return err
	}, nil)
	if err != nil {
		log.Errorf(ctx, "Transaction failed: %v", err)
		http.Error(w, "Internal Server Error", 500)
		return
	}

	fmt.Fprintf(w, "Current count: %d", count.Count)
}

Se a função devolver nil, RunInTransaction tenta confirmar a transação, devolvendo nil se for bem-sucedida. Se a função devolver um valor de erro não nil, as alterações ao Datastore não são aplicadas e RunInTransaction devolve o mesmo erro.

Se RunInTransaction não conseguir confirmar a transação devido a um conflito, tenta novamente, desistindo após três tentativas. Isto significa que a função de transação deve ser idempotente, o que significa que tem o mesmo resultado quando é executada várias vezes. Tenha em atenção que datastore.Get não é idempotente quando deserializa campos de fatias.

O que pode ser feito numa transação

O Datastore impõe restrições sobre o que pode ser feito numa única transação.

Todas as operações do Datastore numa transação têm de operar em entidades no mesmo grupo de entidades se a transação for uma transação de grupo único, ou em entidades num máximo de vinte e cinco grupos de entidades se a transação for uma transação entre grupos. Isto inclui a consulta de entidades por ascendente, a obtenção de entidades por chave, a atualização de entidades e a eliminação de entidades. Tenha em atenção que cada entidade raiz pertence a um grupo de entidades separado. Por isso, uma única transação não pode criar nem operar em mais do que uma entidade raiz, a menos que seja uma transação entre grupos.

Quando duas ou mais transações tentam modificar simultaneamente entidades num ou mais grupos de entidades comuns, apenas a primeira transação a confirmar as respetivas alterações é bem-sucedida. Todas as outras falham na confirmação. Devido a este design, a utilização de grupos de entidades limita o número de escritas simultâneas que pode fazer em qualquer entidade nos grupos. Quando uma transação é iniciada, o Datastore usa o controlo de concorrência otimista verificando a hora da última atualização dos grupos de entidades usados na transação. Ao confirmar uma transação para os grupos de entidades, o Datastore volta a verificar a hora da última atualização dos grupos de entidades usados na transação. Se tiver sido alterado desde a verificação inicial, é devolvido um erro.

Isolamento e consistência

Fora das transações, o nível de isolamento do Datastore é o mais próximo do read committed. Dentro das transações, o isolamento serializável é aplicado. Isto significa que outra transação não pode modificar em simultâneo os dados que são lidos ou modificados por esta transação.

Numa 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 as obtenções numa transação têm a garantia de ver um único resumo consistente do Datastore a partir do início da transação. As entidades e as linhas de índice no grupo de entidades da transação são totalmente atualizadas para que as consultas devolvam o conjunto completo e correto de entidades de resultados, sem os falsos positivos ou os falsos negativos que podem ocorrer em consultas fora das transações.

Esta vista de instantâneo consistente também se aplica a leituras após escritas em transações. Ao contrário da maioria das bases de dados, as consultas e as obtenções numa transação do Datastore não veem os resultados das escritas anteriores nessa transação. Especificamente, se uma entidade for modificada ou eliminada numa transação, uma consulta ou um pedido de obtenção devolve a versão original da entidade no início da transação ou nada se a entidade não existisse nessa altura.

Usos para transações

Este exemplo demonstra uma utilização das transações: atualizar uma entidade com um novo valor de propriedade relativo ao respetivo valor atual.

func increment(ctx context.Context, key *datastore.Key) error {
	return datastore.RunInTransaction(ctx, func(ctx context.Context) error {
		count := new(Counter)
		if err := datastore.Get(ctx, key, count); err != nil {
			return err
		}
		count.Count++
		_, err := datastore.Put(ctx, key, count)
		return err
	}, nil)
}

Isto requer uma transação porque o valor pode ser atualizado por outro utilizador após este código obter o objeto, mas antes de guardar o objeto modificado. Sem uma transação, o pedido do utilizador usa o valor de count antes da atualização do outro utilizador, e a gravação substitui o novo valor. Com uma transação, a aplicação é informada sobre a atualização do outro utilizador. Se a entidade for atualizada durante a transação, a transação é repetida até que todos os passos sejam concluídos sem interrupção.

Outra utilização comum das transações é obter uma entidade com uma chave com nome ou criá-la se ainda não existir:

type Account struct {
	Address string
	Phone   string
}

func GetOrUpdate(ctx context.Context, id, addr, phone string) error {
	key := datastore.NewKey(ctx, "Account", id, 0, nil)
	return datastore.RunInTransaction(ctx, func(ctx context.Context) error {
		acct := new(Account)
		err := datastore.Get(ctx, key, acct)
		if err != nil && err != datastore.ErrNoSuchEntity {
			return err
		}
		acct.Address = addr
		acct.Phone = phone
		_, err = datastore.Put(ctx, key, acct)
		return err
	}, nil)
}

Tal como antes, é necessária uma transação para processar o caso em que outro utilizador está a tentar criar ou atualizar uma entidade com o mesmo ID de string. Sem uma transação, se a entidade não existir e dois utilizadores tentarem criá-la, o segundo substitui o primeiro sem saber que isso aconteceu.

Quando uma transação falha, pode fazer com que a sua app tente novamente a transação até ter êxito ou pode permitir que os utilizadores lidem com o erro propagando-o ao nível da interface do utilizador da sua app. Não tem de criar um ciclo de repetição em torno de cada transação.

Por último, pode usar uma transação para ler uma imagem consistente do Datastore. Isto pode ser útil quando são necessárias várias leituras para renderizar uma página ou exportar dados que têm de ser consistentes. Estes tipos de transações são frequentemente denominados transações apenas de leitura, uma vez que não realizam escritas. As transações de grupo único só de leitura nunca falham devido a modificações simultâneas, pelo que não tem de implementar novas tentativas em caso de falha. No entanto, as transações entre grupos podem falhar devido a modificações simultâneas, pelo que devem ter novas tentativas. A confirmação e a reversão de uma transação só de leitura são ambas operações nulas.

Colocação em fila de tarefas transacionais

Pode colocar uma tarefa em fila como parte de uma transação do Datastore, de modo que a tarefa só seja colocada em fila (e seja garantido que é colocada em fila) se a transação for confirmada com êxito. Depois de adicionada à fila, não é garantido que a tarefa seja executada imediatamente, pelo que não é atómica com a transação. No entanto, depois de adicionada à fila, a tarefa vai ser repetida até ter êxito. Isto aplica-se a qualquer tarefa colocada em fila durante uma função RunInTransaction.

As tarefas transacionais são úteis porque permitem combinar ações que não são do Datastore numa transação que dependem do êxito da transação (como enviar um email para confirmar uma compra). Também pode associar ações do Datastore à transação, como confirmar alterações a grupos de entidades fora da transação se e apenas se a transação for bem-sucedida.

Uma aplicação não pode inserir mais de cinco tarefas transacionais em filas de tarefas durante uma única transação. As tarefas transacionais não podem ter nomes especificados pelo utilizador.

datastore.RunInTransaction(ctx, func(ctx context.Context) error {
	t := &taskqueue.Task{Path: "/path/to/worker"}
	if _, err := taskqueue.Add(ctx, t, ""); err != nil {
		return err
	}
	// ...
	return nil
}, nil)