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

Aislamiento de instantáneas confirmadas de lectura

[ Ver el índice de toda la serie ]

SQL Server proporciona dos implementaciones físicas de la lectura confirmada nivel de aislamiento definido por el estándar SQL, bloqueo de lectura confirmada y aislamiento de instantáneas de lectura confirmada (RCSI ). Si bien ambas implementaciones cumplen con los requisitos establecidos en el estándar SQL para comportamientos de aislamiento de lectura confirmada, RCSI tiene comportamientos físicos bastante diferentes de la implementación de bloqueo que analizamos en la publicación anterior de esta serie.

Garantías Lógicas

El estándar SQL requiere que una transacción que opere en el nivel de aislamiento de lectura confirmada no experimente lecturas sucias. Otra forma de expresar este requisito es decir que una transacción de lectura confirmada solo debe encontrar datos confirmados .

El estándar también dice que las transacciones confirmadas de lectura podrían experimente los fenómenos de concurrencia conocidos como lecturas fantasmas y no repetibles (aunque en realidad no es necesario que lo hagan). Da la casualidad de que ambas implementaciones físicas del aislamiento de confirmación de lectura en SQL Server pueden experimentar lecturas no repetibles y filas fantasma, aunque los detalles precisos son bastante diferentes.

Una vista puntual de los datos comprometidos

Si la opción de base de datos READ_COMMITTED_SNAPSHOT en ON , SQL Server usa una implementación de versiones de filas del nivel de aislamiento de confirmación de lectura. Cuando esto está habilitado, las transacciones que solicitan aislamiento de confirmación de lectura utilizan automáticamente la implementación de RCSI; no se requieren cambios en el código T-SQL existente para usar RCSI. Sin embargo, tenga en cuenta que esto no es lo mismo como diciendo que el código se comportará igual bajo RCSI como cuando se usa la implementación de bloqueo de lectura confirmada, de hecho, generalmente este no es el caso .

No hay nada en el estándar SQL que requiera que los datos leídos por una transacción de lectura confirmada sean los más recientes datos comprometidos. La implementación de RCSI de SQL Server aprovecha esto para proporcionar transacciones con una vista de un punto en el tiempo de datos comprometidos, donde ese punto en el tiempo es el momento en que comenzó la declaración actual ejecución (no el momento en que se inició cualquier transacción contenedora).

Esto es bastante diferente del comportamiento de la implementación de bloqueo de lectura confirmada de SQL Server, donde la declaración ve los datos confirmados más recientemente a partir del momento en que cada elemento se lee físicamente. . El bloqueo de lectura confirmada libera los bloqueos compartidos lo más rápido posible, por lo que el conjunto de datos encontrados puede provenir de puntos muy diferentes en el tiempo.

Para resumir, el bloqueo de lectura confirmada ve cada fila tal como estaba en el momento en que se cerró brevemente y se leyó físicamente; RCSI ve todas las filas como estaban en el momento en que comenzó la declaración. Se garantiza que ambas implementaciones nunca verán datos no confirmados, pero los datos que encuentran pueden ser muy diferentes.

Las implicaciones de una vista puntual

Ver una vista de un punto en el tiempo de los datos comprometidos puede parecer evidentemente superior al comportamiento más complejo de la implementación de bloqueo. Está claro, por ejemplo, que una vista de un punto en el tiempo no puede sufrir los problemas de filas faltantes o encontrar la misma fila varias veces , ambos posibles con bloqueo de aislamiento de lectura confirmada.

Una segunda ventaja importante de RCSI es que no adquiere bloqueos compartidos al leer datos, porque los datos provienen del almacén de versiones de filas en lugar de acceder a ellos directamente. La falta de bloqueos compartidos puede mejorar drásticamente la concurrencia al eliminar conflictos con transacciones concurrentes que buscan adquirir bloqueos incompatibles. Esta ventaja suele resumirse diciendo que los lectores no bloquean a los escritores en RCSI y viceversa. Como consecuencia adicional de la reducción del bloqueo debido a solicitudes de bloqueo incompatibles, la posibilidad de interbloqueos generalmente se reduce mucho cuando se ejecuta bajo RCSI.

Sin embargo, estos beneficios no vienen sin costos y advertencias . Por un lado, el mantenimiento de versiones de filas confirmadas consume recursos del sistema, por lo que es importante que el entorno físico esté configurado para hacer frente a esto, principalmente en términos de tempdb requisitos de rendimiento y memoria/espacio en disco.

La segunda advertencia es un poco más sutil:RCSI proporciona una vista instantánea de los datos comprometidos tal como estaban. al comienzo de la declaración, pero no hay nada que impida que se cambien los datos reales (y esos cambios se confirmen) mientras se ejecuta la declaración RCSI. No hay candados compartidos, recuerda. Una consecuencia inmediata de este segundo punto es que el código T-SQL que se ejecuta bajo RCSI podría tomar decisiones basadas en información desactualizada. , en comparación con el estado confirmado actual de la base de datos. Hablaremos más sobre esto en breve.

Hay una última observación (específica de la implementación) que quiero hacer sobre RCSI antes de continuar. Funciones escalares y de sentencias múltiples ejecutar usando un contexto T-SQL interno diferente de la instrucción contenedora. Esto significa que la vista de un punto en el tiempo que se ve dentro de una invocación de función escalar o de varias declaraciones puede ser posterior a la vista de un punto en el tiempo que se ve en el resto de la declaración. Esto puede generar inconsistencias inesperadas, ya que diferentes partes de la misma declaración ven datos de diferentes puntos en el tiempo. . Este comportamiento extraño y confuso no se aplican a las funciones en línea, que ven la misma instantánea que la instrucción en la que aparecen.

Lecturas y fantasmas no repetibles

Dada una vista de punto en el tiempo a nivel de declaración del estado comprometido de la base de datos, es posible que no sea inmediatamente evidente cómo una transacción de lectura confirmada bajo RCSI podría experimentar los fenómenos de lectura no repetible o fila fantasma. De hecho, si limitamos nuestro pensamiento al ámbito de una sola declaración , ninguno de estos fenómenos es posible bajo RCSI.

Leer los mismos datos varias veces dentro de la misma declaración bajo RCSI siempre devolverá los mismos valores de datos, ningún dato desaparecerá entre esas lecturas y tampoco aparecerán nuevos datos. Si se pregunta qué tipo de declaración podría leer los mismos datos más de una vez, piense en las consultas que hacen referencia a la misma tabla más de una vez, tal vez en una subconsulta.

La consistencia de lectura a nivel de declaración es una consecuencia obvia de que las lecturas se emitan contra una instantánea fija de los datos. La razón por la que RCSI no proporcionar protección contra lecturas no repetibles y fantasmas es que estos fenómenos estándar de SQL se definen en el nivel de transacción. Varias declaraciones dentro de una transacción que se ejecuta en RCSI pueden ver datos diferentes, porque cada declaración tiene una vista de un punto en el tiempo a partir del momento esa declaración en particular comenzó.

Para resumir, cada afirmación dentro de una transacción RCSI ve un conjunto de datos confirmados estáticos, pero ese conjunto puede cambiar entre declaraciones dentro de la misma transacción.

Datos desactualizados

La posibilidad de que nuestro código T-SQL tome una decisión importante basada en información desactualizada es más que un poco inquietante. Considere por un momento que la instantánea de un punto en el tiempo utilizada por una sola declaración que se ejecuta bajo RCSI podría ser arbitrariamente antigua .

Una instrucción que se ejecuta durante un período de tiempo considerable seguirá viendo el estado comprometido de la base de datos tal como estaba cuando comenzó la instrucción. Mientras tanto, a la declaración le faltan todos los cambios confirmados que ocurrieron en la base de datos desde ese momento.

Esto no quiere decir que los problemas asociados con el acceso a datos obsoletos bajo RCSI se limiten a ejecución prolongada. declaraciones, pero los problemas ciertamente podrían ser más pronunciados en tales casos.

Cuestión de tiempo

Este problema de datos desactualizados se aplica a todas las declaraciones de RCSI en principio, sin importar qué tan rápido se completen. Por pequeña que sea la ventana de tiempo, siempre existe la posibilidad de que una operación concurrente pueda modificar el conjunto de datos con el que estamos trabajando, sin que nos demos cuenta de ese cambio. Veamos nuevamente uno de los ejemplos simples que usamos antes al explorar el comportamiento de bloqueo de lectura confirmada:

INSERT dbo.OverdueInvoices
SELECT I.InvoiceNumber
FROM dbo.Invoices AS I
WHERE I.TotalDue >
(
    SELECT SUM(P.Amount)
    FROM dbo.Payments AS P
    WHERE P.InvoiceNumber = I.InvoiceNumber
);

Cuando se ejecuta bajo RCSI, esta declaración no puede ver cualquier modificación de la base de datos confirmada que ocurra después de que la declaración comience a ejecutarse. Si bien no encontraremos los problemas de filas perdidas o encontradas múltiples posibles bajo la implementación de bloqueo, una transacción concurrente podría agregar un pago que debería para evitar que a un cliente se le envíe una severa carta de advertencia sobre un pago atrasado después de que la instrucción anterior comience a ejecutarse.

Probablemente pueda pensar en muchos otros problemas potenciales que podrían ocurrir en este escenario, o en otros que son conceptualmente similares. Cuanto más tiempo se ejecuta la declaración, más desactualizada se vuelve su vista de la base de datos y mayor es el alcance de posibles consecuencias no deseadas.

Por supuesto, hay muchos factores atenuantes en este ejemplo específico. El comportamiento bien podría verse como perfectamente aceptable. Después de todo, enviar una carta de recordatorio porque un pago llegó unos segundos tarde es una acción fácil de defender. Sin embargo, el principio permanece.

Fracasos de las reglas de negocio y riesgos de integridad

Pueden surgir problemas más serios del uso de información desactualizada que enviar una carta de advertencia unos segundos antes. Un buen ejemplo de esta clase de debilidad se puede ver con código de activación se utiliza para hacer cumplir una regla de integridad que quizás sea demasiado compleja para hacer cumplir con restricciones de integridad referencial declarativas. Para ilustrar, considere el siguiente código, que utiliza un activador para aplicar una variación de una restricción de clave externa, pero que aplica la relación solo para ciertas filas de tablas secundarias:

ALTER DATABASE Sandpit
SET READ_COMMITTED_SNAPSHOT ON
WITH ROLLBACK IMMEDIATE;
GO
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
GO
CREATE TABLE dbo.Parent (ParentID integer PRIMARY KEY);
GO
CREATE TABLE dbo.Child
(
    ChildID integer IDENTITY PRIMARY KEY,
    ParentID integer NOT NULL,
    CheckMe bit NOT NULL
);
GO
CREATE TRIGGER dbo.Child_AI
ON dbo.Child
AFTER INSERT
AS
BEGIN
    -- Child rows with CheckMe = true
    -- must have an associated parent row
    IF EXISTS
    (
        SELECT ins.ParentID
        FROM inserted AS ins
        WHERE ins.CheckMe = 1
        EXCEPT
        SELECT P.ParentID
        FROM dbo.Parent AS P
    )
    BEGIN
    	RAISERROR ('Integrity violation!', 16, 1);
        ROLLBACK TRANSACTION;
    END
END;
GO
-- Insert parent row #1
INSERT dbo.Parent (ParentID) VALUES (1);

Ahora considere una transacción que se ejecuta en otra sesión (use otra ventana de SSMS para esto si está siguiendo) que elimina la fila principal #1, pero aún no se confirma:

SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN TRANSACTION;
DELETE FROM dbo.Parent
WHERE ParentID = 1;

Volviendo a nuestra sesión original, intentamos insertar una fila secundaria (marcada) que haga referencia a este padre:

INSERT dbo.Child (ParentID, CheckMe)
VALUES (1, 1);

El código de activación se ejecuta, pero debido a que RCSI solo ve comprometidos datos a partir del momento en que comenzó la declaración, todavía ve la fila principal (no la eliminación no confirmada) y la inserción se realiza correctamente !

La transacción que eliminó la fila principal ahora puede confirmar su cambio con éxito, dejando la base de datos en un estado inconsistente. estado en términos de nuestra lógica de activación:

COMMIT TRANSACTION;
SELECT P.* FROM dbo.Parent AS P;
SELECT C.* FROM dbo.Child AS C;

Este es un ejemplo simplificado, por supuesto, y uno que podría eludirse fácilmente utilizando las funciones de restricción integradas. Se pueden escribir reglas comerciales mucho más complejas y restricciones de pseudointegridad dentro y fuera de los activadores. . El potencial de comportamiento incorrecto bajo RCSI debería ser obvio.

Comportamiento de bloqueo y últimos datos confirmados

Mencioné anteriormente que no se garantiza que el código T-SQL se comporte de la misma manera con la lectura confirmada de RCSI que con la implementación de bloqueo. El ejemplo de código de activación anterior es una buena ilustración de eso, pero debo enfatizar que el problema general no se limita a los activadores .

Por lo general, RCSI no es una buena opción para ningún código T-SQL cuya corrección dependa del bloqueo si existe un cambio no confirmado concurrente. RCSI también podría no ser la opción correcta si el código depende de la lectura actual datos confirmados, en lugar de los últimos datos confirmados en el momento en que se inició la declaración. Estas dos consideraciones están relacionadas, pero no son lo mismo.

Bloqueo de lectura comprometida bajo RCSI

SQL Server proporciona una forma de solicitar bloqueo lectura confirmada cuando RCSI está habilitado, utilizando la sugerencia de tabla READCOMMITTEDLOCK . Podemos modificar nuestro activador para evitar los problemas que se muestran arriba agregando esta sugerencia a la tabla que necesita un comportamiento de bloqueo para funcionar correctamente:

ALTER TRIGGER dbo.Child_AI
ON dbo.Child
AFTER INSERT
AS
BEGIN
    -- Child rows with CheckMe = true
    -- must have an associated parent row
    IF EXISTS
    (
        SELECT ins.ParentID
        FROM inserted AS ins
        WHERE ins.CheckMe = 1
        EXCEPT
        SELECT P.ParentID
        FROM dbo.Parent AS P WITH (READCOMMITTEDLOCK) -- NEW!!
    )
    BEGIN
        RAISERROR ('Integrity violation!', 16, 1);
        ROLLBACK TRANSACTION;
    END
END;

Con este cambio en su lugar, el intento de insertar los bloques de filas secundarias potencialmente huérfanas hasta que la transacción de eliminación se confirma (o cancela). Si se confirma la eliminación, el código de activación detecta la violación de integridad y genera el error esperado.

Identificar consultas que podrían no funcionar correctamente bajo RCSI es una tarea no trivial que puede requerir pruebas exhaustivas para hacerlo bien (¡y recuerde que estos problemas son bastante generales y no se limitan al código de activación!) Además, agregue el READCOMMITTEDLOCK insinuar cada tabla que lo necesite puede ser un proceso tedioso y propenso a errores. Hasta que SQL Server proporcione una opción de alcance más amplio para solicitar la implementación de bloqueo donde sea necesario, estamos obligados a usar las sugerencias de la tabla.

La próxima vez

La siguiente publicación de esta serie continúa nuestro examen del aislamiento de instantáneas confirmadas de lectura, con una mirada al comportamiento sorprendente de las instrucciones de modificación de datos en RCSI.

[ Ver el índice de toda la serie ]