Transacciones

Cloud Datastore admite transacciones. Una transacción es una operación (o un conjunto de operaciones) atómica: se producen todas las operaciones en la transacción o no se produce ninguna. Una aplicación puede realizar varias operaciones y cálculos en una única transacción.

Usa transacciones

Una transacción es un conjunto de operaciones de Cloud Datastore en una o más entidades. Se garantiza que toda transacción sea atómica, es decir, que nunca se aplica parcialmente. Se aplican todas las operaciones de la transacción o ninguna de ellas. Las transacciones tienen una duración máxima de 60 segundos, con un tiempo de caducidad por inactividad de 10 segundos una vez transcurridos 30 segundos.

Las operaciones pueden fallar cuando ocurren las siguientes situaciones:

  • Se intentan aplicar demasiadas modificaciones simultáneas en el mismo grupo de entidad.
  • La transacción supera el límite de recursos.
  • Ocurre un error interno en Cloud Datastore.

En todos estos casos, la API de Cloud Datastore muestra un error.

Las transacciones son una función opcional de Cloud Datastore. No es obligatorio que las utilices para realizar operaciones del servicio.

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

package counter

import (
	"fmt"
	"net/http"

	"golang.org/x/net/context"

	"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 muestra nil, RunInTransaction intenta confirmar la transacción y muestra nil si esa confirmación se realiza de manera correcta. Si la función muestra un valor de error que no es nil, no se aplican los cambios de Cloud Datastore y RunInTransaction muestra el mismo error.

Si RunInTransaction no puede confirmar la transacción debido a un conflicto, lo intenta de nuevo y deja de hacerlo después de tres intentos. Esto significa que la función de transacción debe ser idempotente, lo que significa que tiene el mismo resultado cuando se ejecuta varias veces. Ten en cuenta que datastore.Get no es idempotente cuando se deserializan los campos de segmentos.

¿Qué se puede hacer en una transacción?

Cloud Datastore impone restricciones sobre qué puede hacerse dentro de una transacción única.

Todas las operaciones de Cloud Datastore en una transacción deben operar en entidades del mismo grupo de entidad si la transacción es de un solo grupo, o en entidades de veinticinco grupos de entidad como máximo si la transacción es entre grupos. Esto incluye las consultas de entidades por entidad principal, la recuperación de entidades por clave, y la actualización y eliminación de entidades. Ten en cuenta que cada entidad raíz pertenece a un grupo de entidades separado, de modo que una única transacción no puede crearse 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 entidades de forma simultánea en uno o más grupos de entidad comunes, solo la primera transacción que confirme los cambios se realizará correctamente, y la confirmación de las demás fallará. Debido a este diseño, el uso de grupos de entidad limita el número de escrituras simultáneas que puedes realizar en cualquier entidad de los grupos. Cuando se inicia una transacción, Cloud Datastore usa un control de simultaneidad optimista. Para ello, se verifica la hora de la última actualización de los grupos de entidad que se usaron en la transacción. Cuando se confirma una transacción en los grupos de entidad, Cloud Datastore vuelve a verificar la hora de la última actualización de los grupos de entidad que se usaron en la transacción. Si la hora se cambió desde la verificación inicial, se muestra un error. Para ver una explicación de los grupos de entidad, consulta la página Descripción general de Cloud Datastore.

Aislamiento y coherencia

Fuera de las transacciones, el nivel de aislamiento de Cloud Datastore es el más cercano a las lecturas confirmadas. Dentro de las transacciones, se aplica el aislamiento de serialización. Esto significa que una transacción no puede modificar de forma simultánea los datos que lee o modifica esta transacción. Lee el artículo de la wiki sobre el aislamiento de serialización y el artículo sobre el aislamiento de transacción para obtener más información sobre los niveles de aislamiento.

En una transacción, todas las lecturas reflejan el estado actual y coherente de Cloud Datastore al momento en el que se inició la transacción. Se garantiza que las consultas y las operaciones get dentro de una transacción visualicen una instantánea única y coherente de Cloud Datastore al comienzo de la transacción. Las filas de las entidades y los índices de los grupos de entidades de la transacción se actualizan de forma completa para que las consultas muestren el conjunto completo y correcto de las entidades del resultado, sin los falsos positivos o falsos negativos que se describen en el artículo sobre el aislamiento de transacciones y que pueden generarse en las consultas realizadas fuera de las transacciones.

Esta instantánea coherente también se extiende 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 get que ingresan a una transacción de Cloud Datastore no visualizan los resultados de las escrituras previas dentro de la transacción. En concreto, si se modifica o borra una entidad dentro de una transacción, una consulta o una operación get muestra la versión original de la entidad según como estaba al principio de la transacción, o no devuelve nada si la entidad no existía en ese entonces.

Uso de las transacciones

Este ejemplo demuestra que uno de los usos de las transacciones es la actualización de una entidad con un valor de propiedad nuevo 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)
}

Esta técnica requiere una transacción porque otro usuario puede actualizar el valor luego de que este código recupere el objeto, pero guarda el objeto modificado antes. Sin una transacción, la solicitud del usuario utiliza el valor de count antes de la actualización de otro usuario y, cuando se guarda, se reemplaza el valor nuevo. Mediante una transacción, la aplicación se entera de la actualización del otro usuario. Si se actualiza la entidad durante la transacción, entonces, la transacción se reintenta hasta que todos los pasos se hayan completado sin interrupción.

Otro uso común de las transacciones es recuperar 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)
}

Como antes, la transacción es necesaria para abordar el caso en que otro usuario intente crear o actualizar una entidad con el mismo ID de string. Sin una transacción, si la entidad no existe y dos usuarios intentan crearla, el segundo reemplazará al primero sin saberlo.

Cuando falla una transacción, puedes hacer que tu aplicación vuelva a intentarla hasta que tenga éxito, o bien puedes dejar que tus usuarios manejen el error. Para ello, deberás propagarlo al nivel de la interfaz de usuario de la aplicación. No es necesario crear un bucle de reintento en cada transacción.

Por último, puedes usar la transacción para visualizar una instantánea coherente de Cloud Datastore. Esto puede ser útil cuando se necesitan varias lecturas para procesar una página o exportar datos que deben ser coherentes. Estos tipos de transacciones suelen llamarse de solo lectura, ya que no realizan ninguna operación de escritura. Las transacciones de solo lectura que se realizan en un solo grupo no fallan nunca debido a modificaciones simultáneas, por lo que no debes implementar reintentos en caso de fallas. Sin embargo, las transacciones que se realizan entre grupos pueden fallar debido a modificaciones simultáneas, por lo que deben tener reintentos. Tanto confirmar como revertir transacciones de solo lectura constituyen operaciones autónomas.

Tareas transaccionales en cola

Puedes poner una tarea en cola como parte de una transacción de Cloud Datastore para que la tarea solo esté en cola si la transacción se confirma correctamente. Una vez en cola, la tarea no se ejecuta de inmediato; por lo tanto, la tarea no es atómica con la transacción. Sin embargo, una vez en cola, la tarea no se volverá a intentar hasta que tenga éxito. Esto se aplica a cualquier tarea en cola durante una función RunInTransaction.

Las tareas transaccionales son útiles, porque te permiten combinar acciones que no son de Cloud Datastore con una transacción que depende del éxito de esta (por ejemplo, enviar un correo electrónico para confirmar una compra). También puedes vincular las acciones de Cloud Datastore con la transacción, como la confirmación de los cambios en los grupos de entidad fuera de la transacción únicamente si se logra realizar la transacción.

Una aplicación no puede insertar más de cinco tareas transaccionales en las listas de tareas en cola 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)
¿Te ha resultado útil esta página? Enviar comentarios:

Enviar comentarios sobre...

Entorno estándar de App Engine para Go