En esta página, se explican las transacciones en Spanner y se incluye código de muestra para ejecutarlas.
Introducción
Una transacción en Spanner es un conjunto de operaciones de lectura y escritura que se ejecutan atómicamente en un único punto en el tiempo lógico en 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, de la confirmación en dos fases. Es posible que las transacciones de bloqueo de lectura y escritura se anulen, 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 ejecutar en una marca de tiempo elegida por el sistema que garantice la coherencia externa, sino que también se pueden configurar para que lo lean en una marca de tiempo del pasado. Solo lectura las transacciones no necesitan confirmarse ni aceptan bloqueos. Además, las transacciones de solo lectura podrían esperar a que se completen las escrituras en curso antes de en ejecución.
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 operaciones de escritura ciegas, pero no requieres una transacción atómica, puedes modificar de forma masiva tus tablas de Spanner con la operación de escritura en lotes. Para obtener más información, consulta Cómo modificar datos con operaciones de escritura por lotes.
En esta página, se describen la semántica y las propiedades generales de las transacciones en Spanner y se presentan las interfaces de transacciones de lectura y escritura, solo lectura y 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 operación de escritura que depende del resultado de una o más operaciones de lectura, debes leer y escribir 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, deberías hacer esas escrituras y lecturas 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 un conjunto de operaciones de lectura y escritura atómicamente 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 el valor de las bases de datos relacionales (de hecho, las operaciones de lectura y escritura ofrecen garantías aún más sólidas que los ACID tradicionales; consulta la Semántica a continuación).
Aislamiento
Las siguientes son propiedades de aislamiento para lectura y escritura y solo lectura transacciones de contenedores.
Transacciones que leen y escriben
Estas son las propiedades de aislamiento que obtienes después de confirmar correctamente una transacción que contiene una serie de operaciones de lectura (o consultas) y escritura:
- Todas las operaciones de lectura 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.
- En el momento de la confirmación, se mantuvieron las filas o los rangos vacíos.
- Todas las escrituras dentro de la transacción se confirmaron en el y una marca de tiempo de confirmación.
- Las operaciones de escritura no eran visibles para ninguna transacción hasta después de que se confirmaba la transacción.
Algunos controladores cliente de Spanner contienen una lógica de reintento de transacción para enmascarar errores transitorios, lo que hacen volviendo a ejecutar la transacción y validando el datos observados por el cliente.
El efecto es que todas las operaciones de lectura y escritura parecen haber ocurrido en un momento determinado, desde la perspectiva de la transacción y desde la perspectiva de otros lectores y escritores en 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 Isolation, Spanner proporciona Atomicidad (si alguna de las escrituras en la transacción se confirma, todas se confirman), coherencia (la que la base de datos permanezca coherente después de la transacción) y la 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 para ejecutar un cuerpo de trabajo en el contexto de una transacción de lectura y escritura, con reintentos para la transacción se anula. A continuación, veremos un poco de contexto para explicar este punto: una instancia la transacción se deba probar 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á que se anulen 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, cuerpo de una transacción (es decir, las operaciones de lectura y escritura que se realizarán en uno o más tablas en 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 manera reiterada hasta que la transacción se confirma o se produce un error irreproducible.
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 aparecer 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 confirmaciones 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 y estas, a su vez, reflejan la hora 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 se haya cancelado realmente. 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 simultáneamente con el mismo en la base de datos. Para garantizar la coherencia de varias transacciones simultáneas, Spanner usa una combinación de bloqueos compartidos y exclusivos con el fin de controlar el acceso a los datos. Cuando realizas una operación de lectura como parte de una transacción, Spanner adquiere bloqueos de lectura compartidos, lo que permite que otras operaciones de lectura 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 actualizar a un bloqueo exclusivo si leíste los datos que estás escribiendo. De lo contrario, Spanner usa un bloqueo compartido llamado bloqueo compartido de escritor.
- 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 cantidad menor. de filas en la tabla, lo que permite la modificación simultánea de filas fuera del del rango de destino de la ruta.
Los bloqueos no deben usarse para garantizar el acceso exclusivo a un recurso fuera de Spanner Spanner puede anular las transacciones por varios motivos, como permitir que los datos se trasladen entre 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 implícitamente por código de cliente, como el controlador JDBC de Spanner, solo garantizaba que los bloqueos se retuvieran durante el intento que efectivamente se cometía.
Puedes usar las Estadísticas de bloqueo. Herramienta de introspección para investigar los conflictos de bloqueo en tu base de datos.
Detección de interbloqueo
Spanner detecta cuándo varias transacciones podrían estar interbloqueadas.
obliga a anular todas las transacciones,
salvo 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 “de prevención” estándar para controlar la detección de interbloqueo. De forma interna, Spanner realiza un seguimiento de la antigüedad de cada transacción que solicite 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).
Debido a que Spanner da prioridad a las transacciones más antiguas, 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 mayor prioridad 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. El mismo servidor suele entregar una fila y las filas correspondientes de las tablas intercaladas; lo mismo sucede con dos filas de la misma tabla con claves cercanas. Spanner puede realizar transacciones entre filas en servidores diferentes. Sin embargo, como regla general, las transacciones que afectan muchas filas en la misma ubicación son más rápidas y económicas que las que afectan muchas filas dispersas en la base de datos o en una tabla grande.
Las transacciones más eficaces en Spanner incluyen solo las operaciones de lectura y escritura que se deben aplicar de manera 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 las transacciones de bloqueo 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 solo punto en el tiempo lógico, ambos desde la perspectiva de la transacción de solo lectura y desde la perspectiva de otros lectores y escritores 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 para ejecutar todo el trabajo en el 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 uno
en una transacción de lectura y escritura separada.
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 con 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 si la operación falla, Spanner cancela todas las tareas particiones y no inicia ninguna de las restantes. Spanner no revierte ninguna partición que ya se haya ejecutado.
Interfaz
Spanner proporciona una interfaz para ejecutar una sola declaración de 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
.