En esta página, se explican las transacciones en Cloud Spanner y se incluye un código de muestra para ejecutar transacciones.
Introducción
Una transacción en Spanner es un conjunto de lecturas y escrituras que se ejecutan de forma atómica en un único momento lógico a través de columnas, filas y tablas en una base de datos.
Spanner admite los siguientes modos de transacción:
Bloqueo de lectura y escritura. Estas transacciones dependen del bloqueo pesimista y, si es necesario, la confirmación en dos fases. El bloqueo de transacciones de lectura y escritura puede anular, lo que requiere que la aplicación vuelva a intentarlo.
Solo lectura. Este tipo de transacción proporciona coherencia garantizada en varias operaciones de lectura, pero no permite operaciones de escritura. De forma predeterminada, las transacciones de solo lectura se ejecutan en una marca de tiempo elegida por el sistema que garantiza la coherencia externa, pero también se pueden configurar para que lean en una marca de tiempo anterior. Las transacciones de solo lectura no necesitan confirmarse ni aceptan bloqueos. Además, las transacciones de solo lectura pueden esperar a que se completen las escrituras en curso antes de ejecutarse.
DML particionado. Este tipo de transacción ejecuta una declaración de lenguaje de manipulación de datos (DML) como DML particionado. El DML particionado está diseñado para actualizaciones y eliminaciones masivas, en especial, la limpieza y el reabastecimiento periódicos. Si necesitas confirmar una gran cantidad de escrituras ciegas, pero no necesitas una transacción atómica, puedes modificar de forma masiva las tablas de Spanner con la escritura por lotes. Para obtener más información, consulta Modifica datos con escrituras por lotes.
En esta página, se describen las propiedades generales y la semántica de las transacciones en Spanner y se presentan las interfaces de lectura y escritura, solo lectura y de DML particionado en Spanner.
Transacciones de lectura y escritura
Estas son situaciones en las que deberías usar una transacción de bloqueo de lectura y escritura:
- Si realizas una escritura que depende del resultado de una o más operaciones de lectura, debes realizar esa escritura y las operaciones de lectura en la misma transacción de lectura y escritura.
- Ejemplo: duplica el saldo de la cuenta bancaria A. La lectura del saldo de la cuenta A debe estar en la misma transacción que la escritura para reemplazar el saldo con el valor duplicado.
- Si realizas una o más operaciones de escritura que deben confirmarse de manera atómica, debes realizarlas en la misma transacción de lectura y escritura.
- Ejemplo: transfiere $200 de la cuenta A a la cuenta B. Las dos operaciones de escritura (una para disminuir A por $200 y otra para aumentar B por $200) y las operaciones de lectura de los saldos de cuenta iniciales deben estar en la misma transacción.
- Si puedes realizar una o más operaciones de escritura, según los resultados de una o más operaciones de lectura, debes hacer esas operaciones de escritura y lectura en la misma transacción de lectura y escritura, incluso si las escrituras no se ejecutan.
- Ejemplo: transfiere $200 de la cuenta bancaria A a la cuenta bancaria B si el saldo actual de A es superior a $500. Tu transacción debe contener una lectura del saldo de A y una declaración condicional que contenga las escrituras.
Esta es una situación en la que no deberías usar una transacción de bloqueo de lectura y escritura:
- Si solo realizas lecturas y puedes expresarlas con un método de lectura única, debes usar ese método o una transacción de solo lectura. Las lecturas únicas no se bloquean, a diferencia de las transacciones de lectura y escritura.
Propiedades
Una transacción de lectura y escritura en Spanner ejecuta de forma atómica un conjunto de lecturas y escrituras en un único momento lógico. Además, la marca de tiempo en la que se ejecutan las transacciones de lectura y escritura coincide con el tiempo real, y el orden de serialización coincide con el orden de la marca de tiempo.
¿Por qué usar una transacción de lectura y escritura? Las transacciones de lectura y escritura proporcionan las propiedades ACID de las bases de datos relacionales (de hecho, las transacciones de lectura y escritura de Spanner ofrecen garantías aún más sólidas que el ACID tradicional; consulta la sección Semántica a continuación).
Aislamiento
Las siguientes son propiedades de aislamiento para transacciones de lectura y escritura y de solo lectura.
Transacciones que leen y escriben
Estas son las propiedades de aislamiento que obtienes después de confirmar de forma correcta una transacción que contiene una serie de lecturas (o consultas) y escrituras:
- Todas las lecturas dentro de la transacción mostraron valores que reflejan una instantánea coherente tomada en la marca de tiempo de confirmación de la transacción.
- Las filas o rangos vacíos permanecieron así en el momento de la confirmación.
- Todas las escrituras dentro de la transacción se confirmaron en la marca de tiempo de confirmación de la transacción.
- Las escrituras no eran visibles para ninguna transacción hasta después de que se confirmaba la transacción.
Ciertos controladores de cliente de Spanner contienen una lógica de reintento de transacción para enmascarar los errores transitorios. Para ello, vuelven a ejecutar la transacción y validan los datos observados por el cliente.
El efecto es que todas las operaciones de lectura y escritura parecen haber ocurrido en un único momento, tanto desde la perspectiva de la transacción en sí como desde la de otros lectores y escritores de la base de datos de Spanner. En otras palabras, las operaciones de lectura y las escritura ocurren en la misma marca de tiempo (consulta una ilustración de esto en la sección Serialización y coherencia externa que se encuentra a continuación).
Transacciones que solo leen
Las garantías para una transacción de lectura y escritura que solo lee son similares: todas las operaciones de lectura dentro de esa transacción muestran datos de la misma marca de tiempo, incluso para la inexistencia de filas. Una diferencia es que, si lees datos y, luego, confirmas la transacción de lectura y escritura sin ninguna escritura, no hay garantía de que los datos no hayan cambiado en la base de datos después de la lectura y antes de la confirmación. Si deseas saber si los datos cambiaron desde que los leíste por última vez, el mejor enfoque es volver a leerlos (ya sea en una transacción de lectura y escritura o con una lectura sólida). Además, para obtener mayor eficiencia, si ya sabes que solo leerás y no escribirás, deberías usar una transacción de solo lectura en lugar de una de lectura y escritura.
Atomicidad, coherencia y durabilidad
Además de la propiedad de aislamiento, Spanner proporciona atomicity (si se confirma alguna de las escrituras en la transacción, todas se confirman), coherencia (la base de datos permanece en un estado coherente después de la transacción) y durabilidad (los datos confirmados permanecen confirmados).
Beneficios de estas propiedades
Debido a estas propiedades, como desarrollador de aplicaciones, puedes enfocarte en la precisión de cada transacción, sin preocuparte por proteger su ejecución de otras transacciones que podrían ejecutarse al mismo tiempo.
Interfaz
Las bibliotecas cliente de Spanner proporcionan una interfaz a fin de ejecutar un cuerpo de trabajo en el contexto de una transacción de lectura y escritura, con reintentos para anulaciones de transacciones. Aquí hay un poco de contexto para explicar este punto: es posible que se deba probar una transacción de Spanner varias veces antes de que se confirme. Por ejemplo, si dos transacciones intentan trabajar con datos al mismo tiempo de una manera que podría causar un interbloqueo, Spanner anula una de ellas para que la otra transacción pueda progresar. (En pocas ocasiones, los eventos transitorios en Spanner pueden provocar la anulación de algunas transacciones). Dado que las transacciones son atómicas, una transacción anulada no tiene efecto visible en la base de datos. Por lo tanto, se deben reintentar las ejecuciones de las transacciones hasta que tengan éxito.
Cuando usas una transacción en una biblioteca cliente de Spanner, debes definir el cuerpo de la transacción (es decir, las operaciones de lectura y escritura que se realizarán en una o más tablas de una base de datos) en la forma de un objeto de función. De forma interna, la biblioteca cliente de Spanner ejecuta la función de forma repetida hasta que la transacción se confirma o se encuentra un error irreintentable.
Ejemplo
Supongamos que agregaste una columna MarketingBudget
a la tabla Albums
que se muestra en la página Modelo de datos y esquema:
CREATE TABLE Albums ( SingerId INT64 NOT NULL, AlbumId INT64 NOT NULL, AlbumTitle STRING(MAX), MarketingBudget INT64 ) PRIMARY KEY (SingerId, AlbumId);
El departamento de marketing decide lanzar una campaña para el álbum con clave Albums (1, 1)
y te pide que transfieras $200,000 del presupuesto de Albums
(2, 2)
, pero solo si el dinero está disponible en el presupuesto de ese álbum. Debes usar una transacción de bloqueo de lectura y escritura para esta operación, ya que la transacción puede realizar operaciones de escritura según el resultado de una lectura.
A continuación, se muestra cómo ejecutar una transacción de lectura y escritura:
C++
C#
Go
Java
Node.js
PHP
Python
Ruby
Semántica
Serialización y coherencia externa
Spanner proporciona “serialización”, lo que significa que todas las transacciones aparecen como si se ejecutaran en serie, incluso si algunas de las lecturas, escrituras y otras operaciones de transacciones distintas ocurrieron en paralelo. Spanner asigna marcas de tiempo de confirmación que reflejan el orden de las transacciones confirmadas para implementar esta propiedad. De hecho, Spanner ofrece una garantía más sólida que la serialización llamada coherencia externa: las transacciones se confirman en un orden que se refleja en las marcas de tiempo de confirmación, que reflejan el tiempo real para que puedas compararlas con el reloj. Las operaciones de lectura en una transacción ven todo lo que se confirmó antes de la confirmación de la transacción, y todo lo que se inicie después de confirmar la transacción puede visualizar las operaciones de escritura.
Por ejemplo, considera la ejecución de dos transacciones como las que aparecen en el siguiente diagrama:
La transacción Txn1
en azul lee algunos datos A
, almacena en búfer una escritura en A
y, luego, la confirma de forma correcta. La transacción Txn2
en verde comienza después de Txn1
, lee algunos datos B
y, luego, lee los datos A
. Dado que Txn2
lee el valor de A
después de que Txn1
confirmó su escritura en A
, Txn2
ve el efecto de la escritura de Txn1
en A
, aunque Txn2
comenzó antes de que Txn1
se complete.
A pesar de que hay una superposición en el momento en el que Txn1
y Txn2
están en ejecución, sus marcas de tiempo de confirmación c1
y c2
respetan un orden de transacción lineal. Esto significa que todos los efectos de las operaciones de lectura y escritura de Txn1
parecen haber ocurrido en un momento determinado (c1
) y todos los efectos de las operaciones de lectura y escritura de Txn2
también parecen haber ocurrido en un momento determinado (c2
). Además, sucede esto c1 < c2
(está garantizado porque Txn1
y Txn2
confirmaron las lecturas; esto es así incluso si las escrituras se realizaron en máquinas diferentes), lo que respeta el orden de que Txn1
se haya producido antes que Txn2
.
(Sin embargo, si Txn2
solo realizó operaciones de lectura en la transacción, entonces sucede esto c1 <= c2
).
Las lecturas observan un prefijo del historial de confirmaciones; si una lectura ve el efecto de Txn2
, también ve el de Txn1
. Todas las transacciones que se confirman de forma correcta tienen esta propiedad.
Garantías de lectura y escritura
Si una llamada para ejecutar una transacción falla, las garantías de lectura y escritura que tendrás dependerán del error con el que falló la llamada de confirmación subyacente.
Por ejemplo, un error como “No se encontró la fila” o “La fila ya existe” significa que la escritura de las mutaciones almacenadas en búfer encontró algún error; p. ej., no existe una fila que el cliente intenta actualizar. En ese caso, las lecturas tienen coherencia garantizada, las escrituras no se aplican y se garantiza que la inexistencia de la fila también será coherente con las lecturas.
Cancelación de operaciones de transacción
El usuario puede cancelar operaciones de lectura asíncronas en cualquier momento (p. ej., cuando se cancela una operación de nivel superior o si decides detener una lectura en función de los resultados iniciales recibidos de la lectura) sin afectar otras operaciones existentes dentro de la transacción.
Sin embargo, incluso si intentaste cancelar la lectura, Spanner no garantiza que esta se cancele. Después de solicitar la cancelación de una lectura, esta puede completarse de forma correcta o fallar por algún otro motivo (p. ej., anulación). Además, esa lectura cancelada podría mostrarte algunos resultados, y esos resultados posiblemente incompletos se validarán como parte de la confirmación de la transacción.
Ten en cuenta que, a diferencia de las lecturas, cancelar una operación de confirmación de transacción dará como resultado la anulación de la transacción (a menos que la transacción ya se haya confirmado o falle por otro motivo).
Rendimiento
Bloqueo
Spanner permite que varios clientes interactúen de forma simultánea con la misma base de datos. Para garantizar la coherencia de varias transacciones simultáneas, Spanner usa una combinación de bloqueos compartidos y exclusivos a fin de controlar el acceso a los datos. Cuando realizas una lectura como parte de una transacción, Spanner adquiere bloqueos de lectura compartidos, lo que permite que otras lecturas accedan a los datos hasta que la transacción esté lista para confirmarse. Cuando se confirma la transacción, y se aplican las escrituras, la transacción intenta actualizarse a un bloqueo exclusivo. Bloquea nuevos bloqueos de lectura compartidos en los datos, espera a que se borren los bloqueos de lectura compartidos existentes y, luego, establece un bloqueo exclusivo para el acceso exclusivo a los datos.
Notas sobre los bloqueos:
- Los bloqueos se aplican según el nivel de detalle de la fila y la columna. Si la transacción T1 bloqueó la columna “A” de la fila “foo”, y la transacción T2 desea escribir la columna “B” de la fila “foo”, no se producen problemas.
- Las operaciones de escritura en un elemento de datos que tampoco leen los datos escritos (también conocidas como “escrituras ocultas”) no entran en conflicto con otras escrituras ocultas del mismo elemento (la marca de tiempo de confirmación de cada escritura determina el orden en el que se aplica a la base de datos). Una consecuencia de esto es que Spanner solo necesita actualizarse a un bloqueo exclusivo si leíste los datos que escribes. De lo contrario, Spanner usa un bloqueo compartido llamado bloqueo compartido de escritura.
- Cuando realices búsquedas de filas dentro de una transacción de lectura y escritura, usa índices secundarios para limitar las filas analizadas a un rango más pequeño. Esto hace que Spanner bloquee una menor cantidad de filas en la tabla, lo que permite modificar de forma simultánea las filas fuera del rango.
Los bloqueos no deben usarse para garantizar el acceso exclusivo a un recurso fuera de Spanner. Spanner puede anular las transacciones por varios motivos, por ejemplo, cuando se permite que los datos se muevan por los recursos de procesamiento de la instancia. Si se reintenta una transacción, ya sea de forma explícita mediante el código de la aplicación o de forma implícita por el código de cliente, como el controlador JDBC de Spanner, solo se garantiza que los bloqueos se retuvieron durante el intento que se realizó en realidad.
Puedes usar la herramienta de introspección Estadísticas de bloqueo para investigar los conflictos de bloqueo en tu base de datos.
Detección de interbloqueo
Spanner detecta el momento en el que varias transacciones pueden estar interbloqueadas y fuerza la anulación de todas, excepto una. Por ejemplo, considera la siguiente situación: la transacción Txn1
mantiene un bloqueo en el registro A
y espera un bloqueo en el registro B
, y Txn2
mantiene un bloqueo en el registro B
y espera un bloqueo en el registro A
. La única manera de progresar en esta situación es anular una de las transacciones para que quite su bloqueo y permita que la otra continúe.
Spanner usa el algoritmo estándar de “esperar” para controlar la detección de interbloqueo. De forma interna, Spanner realiza un seguimiento de la antigüedad de cada transacción que solicita bloqueos en conflicto. También permite que las transacciones más antiguas anulen las transacciones más recientes (“más antiguas” significa que la lectura, la consulta o la confirmación ocurrieron antes).
Cuando se da prioridad a las transacciones más antiguas, Spanner garantiza que todas las transacciones tengan la posibilidad de adquirir bloqueos en algún momento, una vez que tengan la antigüedad suficiente para tener una prioridad más alta que otras transacciones. Por ejemplo, una transacción más antigua que necesita un bloqueo compartido de escritura puede anular una transacción que adquirió un bloqueo compartido de lectura.
Ejecución distribuida
Spanner puede ejecutar transacciones en datos que abarcan varios servidores. Esta capacidad tiene un costo de rendimiento en comparación con las transacciones de un solo servidor.
¿Qué tipos de transacciones se pueden distribuir? De forma interna, Spanner puede dividir la responsabilidad de las filas de la base de datos en muchos servidores. Las mismas transacciones suelen entregar una fila y las filas correspondientes de las tablas intercaladas en las filas de servidores diferentes; sin embargo, como regla general, las transacciones que afectan a muchas filas que comparten ubicación son más rápidas y económicas que las que afectan muchas filas distribuidas en la base de datos o en una tabla grande.
Las transacciones más eficientes en Spanner incluyen solo las operaciones de lectura y escritura que se deben aplicar de forma atómica. Las transacciones son más rápidas cuando todas las operaciones de lectura y escritura acceden a los datos en la misma parte del espacio de claves.
Transacciones de solo lectura
Además de bloquear las transacciones de lectura y escritura, Spanner ofrece transacciones de solo lectura.
Usa una transacción de solo lectura cuando necesites ejecutar más de una lectura en la misma marca de tiempo. Si puedes expresar tu lectura con uno de los métodos de lectura única de Spanner, debes usar ese método. El rendimiento del uso de una llamada de lectura única debe ser similar al rendimiento de una lectura única realizada en una transacción de solo lectura.
Si lees una gran cantidad de datos, considera usar particiones para leer los datos en paralelo.
Debido a que las transacciones de solo lectura no escriben, no mantienen bloqueos y no bloquean otras transacciones. Las transacciones de solo lectura observan un prefijo coherente del historial de confirmación de transacciones, por lo que la aplicación siempre obtiene datos coherentes.
Propiedades
Una transacción de solo lectura de Spanner ejecuta un conjunto de lecturas en un único punto lógico en el tiempo, tanto desde la perspectiva de la transacción de solo lectura como desde la de otros lectores y escritores de la base de datos de Spanner. Esto significa que las transacciones de solo lectura siempre observan un estado coherente de la base de datos en un punto determinado del historial de transacciones.
Interfaz
Spanner proporciona una interfaz a fin de ejecutar un cuerpo de trabajo en el contexto de una transacción de solo lectura, con reintentos para anulaciones de transacciones.
Ejemplo
A continuación, se muestra cómo usar una transacción de solo lectura con el fin de obtener datos coherentes para dos lecturas en la misma marca de tiempo:
C++
C#
Go
Java
Node.js
PHP
Python
Ruby
Transacciones de DML particionado
Con el lenguaje de manipulación de datos particionado (DML particionado), puedes ejecutar declaraciones UPDATE
y DELETE
a gran escala sin lidiar con límites de transacciones ni bloquear una tabla completa.
Spanner particiona el espacio de claves y ejecuta las declaraciones DML en cada partición en una transacción de lectura y escritura independiente.
Ejecuta declaraciones DML en las transacciones de lectura y escritura que creas de forma explícita en tu código. Para obtener más información, consulta la sección sobre cómo usar DML.
Propiedades
Solo puedes ejecutar una declaración DML particionada a la vez, ya sea que uses un método de biblioteca cliente o Google Cloud CLI.
Las transacciones particionadas no admiten confirmación ni reversión. Spanner ejecuta y aplica la declaración DML de inmediato. Si cancelas la operación o la operación falla, Spanner cancela todas las particiones en ejecución y no inicia ninguna de las particiones restantes. Spanner no revierte ninguna partición que ya se haya ejecutado.
Interfaz
Spanner proporciona una interfaz para ejecutar una sola declaración DML particionada.
Ejemplos
En el siguiente ejemplo de código, se actualiza la columna MarketingBudget
de la tabla Albums
.
C++
Usa la función ExecutePartitionedDml()
para ejecutar una declaración DML particionada.
C#
Usa el método ExecutePartitionedUpdateAsync()
para ejecutar una declaración DML particionada.
Go
Usa el método PartitionedUpdate()
para ejecutar una declaración DML particionada.
Java
Usa el método executePartitionedUpdate()
para ejecutar una declaración DML particionada.
Node.js
Usa el método runPartitionedUpdate()
para ejecutar una declaración DML particionada.
PHP
Usa el método executePartitionedUpdate()
para ejecutar una declaración DML particionada.
Python
Usa el método execute_partitioned_dml()
para ejecutar una declaración DML particionada.
Ruby
Usa el método execute_partitioned_update()
para ejecutar una declaración DML particionada.
En el siguiente ejemplo de código, se borran las filas de la tabla Singers
según la columna SingerId
.