Descripción general de los niveles de aislamiento

En esta página, se presentan los diferentes niveles de aislamiento y se explica cómo funcionan en Spanner.

El nivel de aislamiento es una propiedad de la base de datos que define qué datos son visibles para las transacciones simultáneas. Spanner admite dos de los niveles de aislamiento definidos en el estándar ANSI/ISO SQL: serializable y lectura repetible. Cuando creas una transacción, debes elegir el nivel de aislamiento más adecuado para ella. El nivel de aislamiento elegido permite que las transacciones individuales prioricen varios factores, como la latencia, la tasa de anulación y si la aplicación es susceptible a los efectos de las anomalías de datos. La mejor opción depende de las demandas específicas de la carga de trabajo.

Aislamiento serializable

El aislamiento serializable es el nivel de aislamiento predeterminado en Spanner. Con el aislamiento serializable, Spanner te proporciona las garantías de control de simultaneidad más estrictas para las transacciones, lo que se denomina coherencia externa. Spanner se comporta como si todas las transacciones se ejecutaran de forma secuencial, aunque Spanner las ejecuta en varios servidores (y posiblemente en varios centros de datos) para mejorar el rendimiento y la disponibilidad en comparación con las bases de datos de un solo servidor. Además, si una transacción se completa antes de que otra comience a confirmarse, Spanner garantiza que los clientes siempre vean los resultados de las transacciones en orden secuencial. De forma intuitiva, Spanner es similar a una base de datos de una sola máquina.

La desventaja es que Spanner podría anular las transacciones si una carga de trabajo tiene una alta disputa de lectura y escritura, en la que muchas transacciones leen datos que otra transacción está actualizando, debido a la naturaleza fundamental de las transacciones serializables. Sin embargo, este es un buen valor predeterminado para una base de datos operativa. Te ayuda a evitar problemas de sincronización complicados que suelen surgir solo con una alta simultaneidad. Estos problemas son difíciles de reproducir y solucionar. Por lo tanto, el aislamiento serializable proporciona la protección más sólida contra las anomalías de datos. Si es necesario volver a intentar una transacción, es posible que aumente la latencia debido a los reintentos de transacción.

Aislamiento de lectura repetible

En Spanner, el aislamiento de lectura repetible se implementa con una técnica conocida como aislamiento de instantáneas. El aislamiento de lectura repetible en Spanner garantiza que todas las operaciones de lectura dentro de una transacción vean una instantánea coherente o sólida de la base de datos tal como existía al comienzo de la transacción. También garantiza que las escrituras simultáneas en los mismos datos solo se realicen correctamente si no hay conflictos. Este enfoque es beneficioso en situaciones de conflicto de lectura y escritura altas en las que numerosas transacciones leen datos que otras transacciones podrían estar modificando. Mediante el uso de una instantánea fija, la lectura repetible evita los impactos en el rendimiento del nivel de aislamiento serializable más restrictivo.

Con su simultaneidad optimista predeterminada, las lecturas se ejecutan sin adquirir bloqueos y sin bloquear las escrituras simultáneas, lo que genera menos transacciones anuladas que podrían necesitar reintentarse debido a posibles conflictos de serialización. Con simultaneidad pesimista, las operaciones de lectura usan instantáneas, pero los bloqueos exclusivos se aplican a los datos leídos de las FOR UPDATE consultas o las lock_scanned_ranges=exclusive sugerencias, y a los datos escritos con consultas DML.

Para las cargas de trabajo que migran desde otras bases de datos, te recomendamos que configures tu aplicación para que use el aislamiento de lectura repetible en Spanner. La semántica de la transacción de lectura repetible, específicamente el bloqueo para las lecturas, coincide con los niveles de aislamiento predeterminados en la mayoría de las otras bases de datos (por ejemplo, MySQL y PostgreSQL). Esto ayuda a reducir la necesidad de rediseñar tu aplicación para que funcione con el nivel de aislamiento serializable predeterminado de Spanner.

A diferencia del aislamiento serializable, la lectura repetible puede generar anomalías de datos si tu aplicación depende de relaciones o restricciones de datos específicas que no aplica el esquema de la base de datos, en especial cuando el orden de las operaciones es importante. En esos casos, una transacción puede leer datos, tomar decisiones basadas en esos datos y, luego, escribir cambios que infrinjan esas restricciones específicas de la aplicación, incluso si aún se cumplen las restricciones del esquema de la base de datos. Esto sucede porque el aislamiento de lectura repetible permite que las transacciones simultáneas continúen sin una serialización estricta. Una posible anomalía se conoce como sesgo de escritura, que surge de un tipo particular de actualización simultánea, en la que cada actualización se acepta de forma independiente, pero su efecto combinado infringe la integridad de los datos de la aplicación. Por ejemplo, imagina que hay un sistema hospitalario en el que al menos un médico debe estar de guardia en todo momento, y los médicos pueden solicitar que se los quite de la guardia para un turno. Con el aislamiento de lectura repetible, si el Dr. Richards y la Dra. Smith están programados para estar de guardia en el mismo turno y, de forma simultánea, intentan solicitar que se los quite de la guardia, cada solicitud se realiza en paralelo. Esto se debe a que ambas transacciones leen que hay al menos otro médico programado para estar de guardia al comienzo de la transacción, lo que causa una anomalía de datos si las transacciones se realizan correctamente. Por otro lado, el uso del aislamiento serializable impide que estas transacciones infrinjan la restricción, ya que las transacciones serializables detectarán posibles anomalías de datos y anularán la transacción. De esta manera, se garantiza la coherencia de la aplicación mediante la aceptación de tasas de anulación más altas.

En el ejemplo anterior, puedes usar la SELECT FOR UPDATE cláusula en el aislamiento de lectura repetible. La cláusula SELECT ... FOR UPDATE verifica si los datos que leyó en la instantánea elegida permanecen sin cambios en el momento de la confirmación. Del mismo modo, las instrucciones DML y las mutaciones, que leen datos de forma interna para garantizar la integridad de las escrituras, también verifican que los datos permanezcan sin cambios en el momento de la confirmación. Además, con la simultaneidad pesimista, los datos leídos por SELECT ... FOR UPDATE y los datos escritos por las instrucciones DML adquieren bloqueos exclusivos para evitar que las transacciones futuras confirmen modificaciones conflictivas antes de que se confirme la transacción actual.

Para las cargas de trabajo que migran desde otras bases de datos que usan consultas FOR UPDATE, te recomendamos que configures tu aplicación para que use el aislamiento de lectura repetible con simultaneidad pesimista en Spanner. La aplicación continúa adquiriendo bloqueos para los datos leídos por SELECT ... FOR UPDATE, que es el comportamiento predeterminado en otras bases de datos.

Para obtener más información, consulta Usa el aislamiento de lectura repetible.

Ejemplo de caso de uso

En el siguiente ejemplo, se muestra el beneficio de usar el aislamiento de lectura repetible para eliminar la sobrecarga de bloqueo. Transaction 1 y Transaction 2 se ejecutan en aislamiento de lectura repetible.

Transaction 1 establece una marca de tiempo de instantánea cuando se ejecuta la instrucción SELECT.

GoogleSQL

-- Transaction 1
BEGIN;

-- Snapshot established at T1
SELECT AlbumId, MarketingBudget
FROM Albums
WHERE SingerId = 1;

/*-----------+------------------*
| AlbumId    | MarketingBudget  |
+------------+------------------+
| 1          | 50000            |
| 2          | 100000           |
| 3          | 70000            |
| 4          | 80000            |
*------------+------------------*/

PostgreSQL

-- Transaction 1
BEGIN;

-- Snapshot established at T1
SELECT albumid, marketingbudget
FROM albums
WHERE singerid = 1;

/*-----------+------------------*
| albumid    | marketingbudget  |
+------------+------------------+
| 1          | 50000            |
| 2          | 100000           |
| 3          | 70000            |
| 4          | 80000            |
*------------+------------------*/

Luego, Transaction 2 establece una marca de tiempo de instantánea después de que comienza Transaction 1, pero antes de que se confirme. Como Transaction 1 no actualizó los datos, la consulta SELECT en Transaction 2 lee los mismos datos que Transaction 1.

GoogleSQL

-- Transaction 2
BEGIN;

-- Snapshot established at T2 > T1
SELECT AlbumId, MarketingBudget
FROM Albums
WHERE SingerId = 1;

INSERT INTO Albums (SingerId, AlbumId, MarketingBudget) VALUES (1, 5, 50000);

COMMIT;

PostgreSQL

-- Transaction 2
BEGIN;

-- Snapshot established at T2 > T1
SELECT albumid, marketingbudget
FROM albums
WHERE singerid = 1;

INSERT INTO albums (singerid, albumid, marketingbudget) VALUES (1, 5, 50000);

COMMIT;

Transaction 1 continúa después de que se confirma Transaction 2.

GoogleSQL

-- Transaction 1 continues
SELECT SUM(MarketingBudget) as UsedBudget
FROM Albums
WHERE SingerId = 1;

/*-----------*
| UsedBudget |
+------------+
| 300000     |
*------------*/

PostgreSQL

-- Transaction 1 continues
SELECT SUM(marketingbudget) AS usedbudget
FROM albums
WHERE singerid = 1;

/*-----------*
| usedbudget |
+------------+
| 300000     |
*------------*/

El valor UsedBudget que muestra Spanner es la suma del presupuesto leído por Transaction 1. Esta suma refleja solo los datos presentes en la instantánea T1. No incluye el presupuesto que agregó Transaction 2, porque Transaction 2 se confirmó después de que Transaction 1 estableció la instantánea T1. El uso de la lectura repetible significa que Transaction 1 no tuvo que anularse, aunque Transaction 2 modificó los datos leídos por Transaction 1. Sin embargo, el resultado que muestra Spanner puede ser o no el resultado deseado.

Conflictos de lectura y escritura y corrección

En el ejemplo anterior, si los datos consultados por las instrucciones SELECT en Transaction 1 se usaran para tomar decisiones posteriores sobre el presupuesto de marketing, podría haber problemas de corrección.

Por ejemplo, supongamos que hay un presupuesto total de 400,000. Según el resultado de la instrucción SELECT en Transaction 1, podríamos pensar que quedan 100,000 en el presupuesto y decidir asignarlo todo a AlbumId = 4.

GoogleSQL

-- Transaction 1 continues..
UPDATE Albums
SET MarketingBudget = MarketingBudget + 100000
WHERE SingerId = 1 AND AlbumId = 4;

COMMIT;

PostgreSQL

-- Transaction 1 continues..
UPDATE albums
SET marketingbudget = marketingbudget + 100000
WHERE singerid = 1 AND albumid = 4;

COMMIT;

Transaction 1 se confirma correctamente, aunque Transaction 2 ya asignó 50,000 del presupuesto restante de 100,000 a un álbum nuevo AlbumId = 5.

Puedes usar la sintaxis SELECT...FOR UPDATE para validar que ciertas lecturas de una transacción no cambien durante la vida útil de la transacción para garantizar su corrección. En el siguiente ejemplo con SELECT...FOR UPDATE, Transaction 1 se anula en el momento de la confirmación.

GoogleSQL

-- Transaction 1 continues..
SELECT SUM(MarketingBudget) AS TotalBudget
FROM Albums
WHERE SingerId = 1
FOR UPDATE;

/*-----------*
| TotalBudget |
+------------+
| 300000     |
*------------*/

COMMIT;

PostgreSQL

-- Transaction 1 continues..
SELECT SUM(marketingbudget) AS totalbudget
FROM albums
WHERE singerid = 1
FOR UPDATE;

/*-------------*
 | totalbudget |
 +-------------+
 | 300000      |
 *-------------*/

COMMIT;

Para obtener más información, consulta Usa SELECT FOR UPDATE en el aislamiento de lectura repetible.

También puedes usar la simultaneidad pesimista, que adquiere bloqueos exclusivos en los datos leídos por la instrucción SELECT...FOR UPDATE. Por ejemplo, Transaction 1 se anula en el momento de la confirmación porque Transaction 2 confirmó sus modificaciones antes de que Transaction 1 adquiriera bloqueos, lo que genera un conflicto. Sin embargo, si el orden de las transacciones hace que Transaction 2 intente actualizar el presupuesto de marketing después de que Transaction 1 adquiera bloqueos, Transaction 2 espera a que Transaction 1 confirme y libere los bloqueos antes de que pueda continuar. La opción de simultaneidad pesimista serializa el acceso a los datos.

Para obtener más información, consulta Control de simultaneidad.

Conflictos de escritura y escritura y corrección

Si usas el nivel de aislamiento de lectura repetible, las escrituras simultáneas en los mismos datos solo se realizan correctamente si no hay conflictos.

En el siguiente ejemplo, Transaction 1 establece una marca de tiempo de instantánea en la primera instrucción SELECT.

GoogleSQL

-- Transaction 1
BEGIN;

-- Snapshot established at T1
SELECT AlbumId, MarketingBudget
FROM Albums
WHERE SingerId = 1;

PostgreSQL

-- Transaction 1
BEGIN;

-- Snapshot established at T1
SELECT albumid, marketingbudget
FROM albums
WHERE singerid = 1;

La siguiente Transaction 2 lee los mismos datos que Transaction 1 y, luego, inserta un elemento nuevo. Transaction 2 se confirma correctamente sin esperar ni anularse.

GoogleSQL

-- Transaction 2
BEGIN;

-- Snapshot established at T2 (> T1)
SELECT AlbumId, MarketingBudget
FROM Albums
WHERE SingerId = 1;

INSERT INTO Albums (SingerId, AlbumId, MarketingBudget) VALUES (1, 5, 50000);

COMMIT;

PostgreSQL

-- Transaction 2
BEGIN;

-- Snapshot established at T2 (> T1)
SELECT albumid, marketingbudget
FROM albums
WHERE singerid = 1;

INSERT INTO albums (singerid, albumid, marketingbudget) VALUES (1, 5, 50000);

COMMIT;

Transaction 1 continúa después de que se confirma Transaction 2.

GoogleSQL

-- Transaction 1 continues
INSERT INTO Albums (SingerId, AlbumId, MarketingBudget) VALUES (1, 5, 30000);
-- Transaction aborts
COMMIT;

PostgreSQL

-- Transaction 1 continues
INSERT INTO albums (singerid, albumid, marketingbudget) VALUES (1, 5, 30000);
-- Transaction aborts
COMMIT;

Transaction 1 se anula, ya que Transaction 2 ya confirmó una inserción en la fila AlbumId = 5.

¿Qué sigue?