sql >> Base de Datos >  >> RDS >> Database

El nivel de aislamiento SNAPSHOT

[ Ver el índice de toda la serie ]

Los problemas de concurrencia son difíciles de la misma manera que la programación multiproceso es difícil. A menos que se utilice aislamiento serializable, puede ser difícil codificar transacciones T-SQL que siempre funcionarán correctamente cuando otros usuarios realicen cambios en la base de datos al mismo tiempo.

Los problemas potenciales pueden no ser triviales incluso si la 'transacción' en cuestión es un simple SELECT declaración. Para transacciones complejas de varios extractos que leen y escriben datos, el potencial de resultados inesperados y errores bajo alta concurrencia puede volverse abrumador rápidamente. Intentar resolver problemas de simultaneidad sutiles y difíciles de reproducir mediante la aplicación de sugerencias de bloqueo aleatorio u otros métodos de prueba y error puede ser una experiencia extremadamente frustrante.

En muchos aspectos, el nivel de aislamiento de instantáneas parece una solución perfecta para estos problemas de concurrencia. La idea básica es que cada transacción instantánea se comporte como si se ejecutara contra su propia copia privada del estado confirmado de la base de datos, tomada en el momento en que comenzó la transacción. Proporcionar a toda la transacción una vista inalterable de los datos comprometidos obviamente garantiza resultados consistentes para las operaciones de solo lectura, pero ¿qué sucede con las transacciones que modifican los datos?

El aislamiento de instantáneas maneja los cambios de datos de manera optimista, asumiendo implícitamente que los conflictos entre escritores simultáneos serán relativamente raros. Cuando se produce un conflicto de escritura, el primer confirmador gana y se revierten los cambios de la transacción perdedora. Es desafortunado para la transacción revertida, por supuesto, pero si se trata de una ocurrencia bastante rara, los beneficios del aislamiento de instantáneas pueden superar fácilmente los costos de una falla ocasional y un reintento.

La semántica relativamente simple y limpia del aislamiento de instantáneas (en comparación con las alternativas) puede ser una ventaja significativa, especialmente para las personas que no trabajan exclusivamente en el mundo de las bases de datos y, por lo tanto, no conocen bien los distintos niveles de aislamiento. Incluso para profesionales experimentados en bases de datos, un nivel de aislamiento relativamente "intuitivo" puede ser un alivio bienvenido.

Por supuesto, las cosas rara vez son tan simples como parecen y el aislamiento de instantáneas no es una excepción. La documentación oficial hace un buen trabajo al describir las principales ventajas y desventajas del aislamiento de instantáneas, por lo que la mayor parte de este artículo se concentra en explorar algunos de los problemas menos conocidos y sorprendentes que puede encontrar. Primero, sin embargo, una mirada rápida a las propiedades lógicas de este nivel de aislamiento:

Propiedades de ACID y aislamiento de instantáneas

El aislamiento de instantáneas no es uno de los niveles de aislamiento definidos en SQL Standard, pero a menudo se compara con los "fenómenos de concurrencia" definidos allí. Por ejemplo, la siguiente tabla de comparación se reproduce del artículo técnico de SQL Server, "SQL Server 2005 Row Versioning-Based Transaction Isolation" de Kimberly L. Tripp y Neal Graves:

Proporcionando una vista de un punto en el tiempo de datos comprometidos , el aislamiento de instantáneas brinda protección contra los tres fenómenos de simultaneidad que se muestran allí. Se evitan las lecturas sucias porque solo se ven los datos confirmados y la naturaleza estática de la instantánea evita que se encuentren lecturas no repetibles y fantasmas.

Sin embargo, esta comparación (y la sección resaltada en particular) solo muestra que los niveles de aislamiento de instantáneas y serializables previenen los mismos tres fenómenos específicos. No significa que sean equivalentes en todos los aspectos. Es importante destacar que el estándar SQL-92 no define el aislamiento serializable en términos de los tres fenómenos solos. La sección 4.28 de la norma da la definición completa:

Se garantiza que la ejecución de transacciones SQL simultáneas en el nivel de aislamiento SERIALIZABLE sea serializable. Una ejecución serializable se define como una ejecución de las operaciones de ejecución simultánea de transacciones SQL que produce el mismo efecto que alguna ejecución en serie de esas mismas transacciones SQL. Una ejecución en serie es aquella en la que cada transacción SQL se ejecuta hasta su finalización antes de que comience la siguiente transacción SQL.

A menudo se pasa por alto el alcance y la importancia de las garantías implícitas aquí. Para decirlo en un lenguaje sencillo:

Cualquier transacción serializable que se ejecute correctamente cuando se ejecute sola continuará ejecutándose correctamente con cualquier combinación de transacciones simultáneas, o se revertirá con un mensaje de error (normalmente un punto muerto en la implementación de SQL Server).

Los niveles de aislamiento no serializables, incluido el aislamiento de instantáneas, no ofrecen las mismas garantías sólidas de corrección.

Datos obsoletos

El aislamiento de instantáneas parece casi seductoramente simple. Las lecturas siempre provienen de datos comprometidos en un solo punto en el tiempo, y los conflictos de escritura se detectan y manejan automáticamente. ¿Cómo es que esta no es una solución perfecta para todas las dificultades relacionadas con la concurrencia?

Un problema potencial es que las lecturas de instantáneas no reflejan necesariamente el estado confirmado actual de la base de datos. Una transacción de instantánea ignora por completo cualquier cambio confirmado realizado por otras transacciones simultáneas después de que comience la transacción de instantánea. Otra forma de decirlo es decir que una transacción instantánea ve datos obsoletos y obsoletos. Si bien este comportamiento puede ser exactamente lo que se necesita para generar un informe puntual preciso, puede que no sea tan adecuado en otras circunstancias (por ejemplo, cuando se usa para hacer cumplir una regla en un disparador).

Escritura sesgada

El aislamiento de instantáneas también es vulnerable a un fenómeno algo relacionado conocido como sesgo de escritura. La lectura de datos obsoletos juega un papel en esto, pero este problema también ayuda a aclarar qué hace y qué no hace la 'detección de conflicto de escritura' de la instantánea.

El sesgo de escritura ocurre cuando dos transacciones simultáneas leen datos que la otra transacción modifica. No se produce ningún conflicto de escritura porque las dos transacciones modifican filas diferentes. Ninguna transacción ve los cambios realizados por la otra, porque ambas están leyendo desde un punto en el tiempo antes de que se realizaran esos cambios.

Un ejemplo clásico de sesgo de escritura es el problema de las canicas blancas y negras, pero quiero mostrar otro ejemplo simple aquí:

-- Create two empty tables
CREATE TABLE A (x integer NOT NULL);
CREATE TABLE B (x integer NOT NULL);
 
-- Connection 1
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
BEGIN TRANSACTION;
INSERT A (x) SELECT COUNT_BIG(*) FROM B;
 
-- Connection 2
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
BEGIN TRANSACTION;
INSERT B (x) SELECT COUNT_BIG(*) FROM A;
COMMIT TRANSACTION;
 
-- Connection 1
COMMIT TRANSACTION;

Bajo el aislamiento de instantáneas, ambas tablas en ese script terminan con una sola fila que contiene un valor cero. Este es un resultado correcto, pero no es serializable:no corresponde a ninguna posible orden de ejecución de transacciones en serie. En cualquier programación verdaderamente serial, una transacción debe completarse antes de que comience la otra, por lo que la segunda transacción contaría la fila insertada por la primera. Esto puede sonar como un tecnicismo, pero recuerde que las poderosas garantías serializables solo se aplican cuando las transacciones son verdaderamente serializables.

Una sutileza de detección de conflictos

Un conflicto de escritura de instantáneas ocurre cada vez que una transacción de instantáneas intenta modificar una fila que ha sido modificada por otra transacción que se confirmó después de que comenzó la transacción de instantáneas. Aquí hay dos sutilezas:

  1. Las transacciones en realidad no tienen que cambiar cualquier valor de datos; y
  2. Las transacciones no tienen que modificar ninguna columna común .

El siguiente script demuestra ambos puntos:

-- Test table
CREATE TABLE dbo.Conflict
(
    ID1 integer UNIQUE,
    Value1 integer NOT NULL,
    ID2 integer UNIQUE,
    Value2 integer NOT NULL
);
 
-- Insert one row
INSERT dbo.Conflict
    (ID1, ID2, Value1, Value2)
VALUES
    (1, 1, 1, 1);
 
-- Connection 1
BEGIN TRANSACTION;
 
UPDATE dbo.Conflict
SET Value1 = 1
WHERE ID1 = 1;
 
-- Connection 2
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
BEGIN TRANSACTION;
 
UPDATE dbo.Conflict
SET Value2 = 1
WHERE ID2 = 1;
 
-- Connection 1
COMMIT TRANSACTION;

Observe lo siguiente:

  • Cada transacción ubica la misma fila usando un índice diferente
  • Ninguna actualización da como resultado un cambio en los datos ya almacenados
  • Las dos transacciones 'actualizan' diferentes columnas en la fila.

A pesar de todo eso, cuando se confirma la primera transacción, la segunda termina con un error de conflicto de actualización:

Resumen:la detección de conflictos siempre opera en el nivel de una fila completa, y una 'actualización' no tiene que cambiar realmente ningún dato. (En caso de que se lo pregunte, los cambios en los datos LOB o SLOB fuera de la fila también cuentan como un cambio en la fila a efectos de detección de conflictos).

El problema de la clave externa

La detección de conflictos también se aplica a la fila principal en una relación de clave externa. Al modificar una fila secundaria con aislamiento de instantánea, un cambio en la fila principal en otra transacción puede desencadenar un conflicto. Como antes, esta lógica se aplica a toda la fila principal:la actualización principal no tiene que afectar a la columna de clave externa en sí. Cualquier operación en la tabla secundaria que requiera una verificación automática de clave externa en el plan de ejecución puede generar un conflicto inesperado.

Para demostrar esto, primero cree las siguientes tablas y datos de muestra:

CREATE TABLE dbo.Dummy
(
    x integer NULL
);
 
CREATE TABLE dbo.Parent
(
    ParentID integer PRIMARY KEY,
    ParentValue integer NOT NULL
);
 
CREATE TABLE dbo.Child 
(
    ChildID integer PRIMARY KEY,
    ChildValue integer NOT NULL,
    ParentID integer NULL FOREIGN KEY REFERENCES dbo.Parent
);
 
INSERT dbo.Parent 
    (ParentID, ParentValue) 
VALUES (1, 1);
 
INSERT dbo.Child 
    (ChildID, ChildValue, ParentID) 
VALUES (1, 1, 1);

Ahora ejecute lo siguiente desde dos conexiones separadas como se indica en los comentarios:

-- Connection 1
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
BEGIN TRANSACTION;
SELECT COUNT_BIG(*) FROM dbo.Dummy;
 
-- Connection 2 (any isolation level)
UPDATE dbo.Parent SET ParentValue = 1 WHERE ParentID = 1;
 
-- Connection 1
UPDATE dbo.Child SET ParentID = NULL WHERE ChildID = 1;
UPDATE dbo.Child SET ParentID = 1 WHERE ChildID = 1;

La lectura de la tabla ficticia está ahí para garantizar que la transacción de la instantánea haya comenzado oficialmente. Emitiendo BEGIN TRANSACTION no es suficiente hacer esto; tenemos que realizar algún tipo de acceso a datos en una tabla de usuario.

La primera actualización de la tabla secundaria no genera ningún conflicto porque la columna de referencia se establece en NULL no requiere una verificación de la tabla principal en el plan de ejecución (no hay nada que verificar). El procesador de consultas no toca la fila principal en el plan de ejecución, por lo que no surge ningún conflicto.

La segunda actualización de la tabla secundaria desencadena un conflicto porque se realiza automáticamente una verificación de clave externa. Cuando el procesador de consultas accede a la fila principal, también se comprueba si hay un conflicto de actualización. En este caso, se genera un error porque la fila principal a la que se hace referencia experimentó una modificación confirmada después de que se inició la transacción de la instantánea. Tenga en cuenta que la modificación de la tabla principal no afectó a la columna de clave externa en sí.

También puede ocurrir un conflicto inesperado si un cambio en la tabla secundaria hace referencia a una fila principal que se creó por una transacción concurrente (y esa transacción confirmada después de que comenzó la transacción instantánea).

Resumen:un plan de consulta que incluye una verificación de clave externa automática puede arrojar un error de conflicto si la fila a la que se hace referencia experimentó algún tipo de modificación (¡incluida la creación!) desde que comenzó la transacción de la instantánea.

El problema de la tabla truncada

Una transacción de instantánea fallará con un error si alguna de las tablas a las que accede se ha truncado desde que comenzó la transacción. Esto se aplica incluso si la tabla truncada no tenía filas para comenzar, como lo demuestra el siguiente script:

CREATE TABLE dbo.AccessMe
(
    x integer NULL
);
 
CREATE TABLE dbo.TruncateMe
(
    x integer NULL
);
 
-- Connection 1
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
BEGIN TRANSACTION;
SELECT COUNT_BIG(*) FROM dbo.AccessMe;
 
-- Connection 2
TRUNCATE TABLE dbo.TruncateMe;
 
-- Connection 1
SELECT COUNT_BIG(*) FROM dbo.TruncateMe;

El SELECT final falla con un error:

Este es otro efecto secundario sutil que debe verificar antes de habilitar el aislamiento de instantáneas en una base de datos existente.

La próxima vez

La siguiente (y última) publicación de esta serie hablará sobre el nivel de aislamiento de lectura no confirmada (conocido cariñosamente como "nolock").

[ Ver el índice de toda la serie ]