Transacciones

Datastore admite transacciones. Una transacción es una operación o un conjunto de operaciones que es atómico: o se realizan todas las operaciones de la transacción o no se realiza ninguna. Una aplicación puede realizar varias operaciones y cálculos en una sola transacción.

Usar transacciones

Una transacción es un conjunto de operaciones de Datastore en una o varias entidades. Se garantiza que cada transacción es atómica, lo que significa que las transacciones nunca se aplican parcialmente. Se aplican todas las operaciones de la transacción o no se aplica ninguna. Las transacciones tienen una duración máxima de 60 segundos y un tiempo de caducidad por inactividad de 10 segundos después de 30 segundos.

Una operación puede fallar en los siguientes casos:

  • Se han intentado realizar demasiadas modificaciones simultáneas en el mismo grupo de entidades.
  • La transacción supera un límite de recursos.
  • Datastore detecta un error interno.

En todos estos casos, la API Datastore devuelve un error.

Las transacciones son una función opcional de Datastore. No es obligatorio usarlas para realizar operaciones de Datastore.

La función datastore.RunInTransaction ejecuta la función proporcionada en una transacción.


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

Si la función devuelve nil, RunInTransaction intenta confirmar la transacción y devuelve nil si se realiza correctamente. Si la función devuelve un valor que no es un error nil RunInTransaction, no se aplican los cambios en Datastore y devuelve el mismo error.

Si RunInTransaction no puede confirmar la transacción debido a un conflicto, lo intenta de nuevo y se da por vencido después de tres intentos. Esto significa que la función de transacción debe ser idempotente, es decir, que debe dar el mismo resultado cuando se ejecute varias veces. Ten en cuenta que datastore.Get no es idempotente al deserializar campos de slice.

Qué se puede hacer en una transacción

Datastore impone restricciones sobre lo que se puede hacer dentro de una sola transacción.

Todas las operaciones de Datastore de una transacción deben operar en entidades del mismo grupo de entidades si la transacción es de un solo grupo, o en entidades de un máximo de 25 grupos de entidades si la transacción es entre grupos. Esto incluye consultar entidades por ancestro, obtener entidades por clave, actualizar entidades y eliminar entidades. Ten en cuenta que cada entidad raíz pertenece a un grupo de entidades independiente, por lo que una sola transacción no puede crear ni operar en más de una entidad raíz, a menos que sea una transacción entre grupos.

Cuando dos o más transacciones intentan modificar simultáneamente entidades en uno o más grupos de entidades comunes, solo la primera transacción que confirme sus cambios puede completarse correctamente. El resto fallarán al confirmar. Debido a este diseño, el uso de grupos de entidades limita el número de escrituras simultáneas que puedes realizar en cualquier entidad de los grupos. Cuando se inicia una transacción, Datastore usa el control de concurrencia optimista comprobando la hora de la última actualización de los grupos de entidades usados en la transacción. Cuando se confirma una transacción de los grupos de entidades, Datastore vuelve a comprobar la hora de la última actualización de los grupos de entidades utilizados en la transacción. Si ha cambiado desde la comprobación inicial, se devuelve un error.

Aislamiento y coherencia

Fuera de las transacciones, el nivel de aislamiento de Datastore es el más parecido al de lectura confirmada. Dentro de las transacciones, se aplica el aislamiento serializable. Esto significa que otra transacción no puede modificar simultáneamente los datos que lee o modifica esta transacción.

En una transacción, todas las lecturas reflejan el estado actual y coherente de Datastore en el momento en que se inició la transacción. Se garantiza que las consultas y las lecturas dentro de una transacción verán una única instantánea coherente de Datastore a partir del inicio de la transacción. Las entidades y las filas de índice del grupo de entidades de la transacción se actualizan por completo para que las consultas devuelvan el conjunto completo y correcto de entidades de resultados, sin los falsos positivos ni los falsos negativos que pueden producirse en las consultas fuera de las transacciones.

Esta vista de la captura coherente también se aplica a las lecturas posteriores a las escrituras dentro de las transacciones. A diferencia de la mayoría de las bases de datos, las consultas y las operaciones de obtención dentro de una transacción de Datastore no ven los resultados de las escrituras anteriores dentro de esa transacción. En concreto, si se modifica o elimina una entidad en una transacción, una consulta o una obtención devuelve la versión original de la entidad al principio de la transacción o nada si la entidad no existía entonces.

Usos de las transacciones

En este ejemplo se muestra un uso de las transacciones: actualizar una entidad con un nuevo valor de propiedad relativo a su valor actual.

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

Esto requiere una transacción porque otro usuario podría actualizar el valor después de que este código obtenga el objeto, pero antes de que guarde el objeto modificado. Si no hay ninguna transacción, la solicitud del usuario usará el valor de count anterior a la actualización del otro usuario y la acción de guardar sobrescribirá el nuevo valor. Con una transacción, la aplicación recibe información sobre la actualización del otro usuario. Si la entidad se actualiza durante la transacción, se vuelve a intentar la transacción hasta que se completen todos los pasos sin interrupciones.

Otro uso habitual de las transacciones es obtener una entidad con una clave con nombre o crearla si aún no existe:

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

Al igual que antes, es necesaria una transacción para gestionar el caso en el que otro usuario intente crear o actualizar una entidad con el mismo ID de cadena. Sin una transacción, si la entidad no existe y dos usuarios intentan crearla, el segundo sobrescribe al primero sin saber que ha ocurrido.

Cuando falla una transacción, puedes hacer que tu aplicación vuelva a intentarla hasta que se complete correctamente o puedes dejar que tus usuarios gestionen el error propagándolo al nivel de la interfaz de usuario de tu aplicación. No es necesario que crees un bucle de reintentos en torno a cada transacción.

Por último, puedes usar una transacción para leer una instantánea coherente de Datastore. Esto puede ser útil cuando se necesitan varias lecturas para renderizar una página o exportar datos que deben ser coherentes. Este tipo de transacciones se suelen denominar transacciones de solo lectura, ya que no realizan ninguna escritura. Las transacciones de un solo grupo de solo lectura nunca fallan debido a modificaciones simultáneas, por lo que no tienes que implementar reintentos en caso de fallo. Sin embargo, las transacciones entre grupos pueden fallar debido a modificaciones simultáneas, por lo que deberían tener reintentos. Confirmar y revertir una transacción de solo lectura son operaciones nulas.

Puesta en cola de tareas transaccionales

Puedes poner en cola una tarea como parte de una transacción de Datastore, de forma que la tarea solo se ponga en cola (y se garantice que se ponga en cola) si la transacción se confirma correctamente. Una vez puesta en cola, no se garantiza que la tarea se ejecute inmediatamente, por lo que no es atómica con la transacción. Sin embargo, una vez que se haya puesto en cola, la tarea se volverá a intentar hasta que se complete correctamente. Esto se aplica a cualquier tarea puesta en cola durante una función RunInTransaction.

Las tareas transaccionales son útiles porque te permiten combinar acciones que no son de Datastore en una transacción que depende de que la transacción se realice correctamente (por ejemplo, enviar un correo para confirmar una compra). También puedes vincular acciones de Datastore a la transacción, como confirmar los cambios en los grupos de entidades fuera de la transacción solo si esta se completa correctamente.

Una aplicación no puede insertar más de cinco tareas transaccionales en colas de tareas durante una sola transacción. Las tareas transaccionales no deben tener nombres especificados por el usuario.

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)