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

Modificaciones de datos bajo el aislamiento de instantáneas de lectura confirmada

[ Ver el índice de toda la serie ]

La publicación anterior de esta serie mostró cómo una instrucción T-SQL que se ejecuta con aislamiento de instantánea de lectura confirmada (RCSI ) normalmente ve una vista instantánea del estado comprometido de la base de datos tal como estaba cuando la declaración comenzó a ejecutarse. Esa es una buena descripción de cómo funcionan las cosas para declaraciones que leen datos, pero hay diferencias importantes para declaraciones que se ejecutan bajo RCSI que modifican filas existentes .

Destaco la modificación de filas existentes anterior, porque las siguientes consideraciones se aplican solo a UPDATE y DELETE operaciones (y las acciones correspondientes de un MERGE declaración). Para ser claro, INSERT las declaraciones están específicamente excluidas del comportamiento que estoy a punto de describir porque las inserciones no modifican existentes datos.

Actualizar bloqueos y versiones de fila

La primera diferencia es que las declaraciones de actualización y eliminación no leen versiones de fila en RCSI al buscar las filas de origen para modificar. Actualice y elimine declaraciones bajo RCSI en lugar de adquirir bloqueos de actualización al buscar filas de calificación. El uso de bloqueos de actualización garantiza que la operación de búsqueda encuentre filas para modificar utilizando los datos confirmados más recientes .

Sin bloqueos de actualización, la búsqueda se basaría en una versión posiblemente desactualizada del conjunto de datos (datos confirmados tal como estaban cuando comenzó la instrucción de modificación de datos). Esto podría recordarle el ejemplo de disparador que vimos la última vez, donde un READCOMMITTEDLOCK La sugerencia se usó para volver de RCSI a la implementación de bloqueo del aislamiento de confirmación de lectura. Esa sugerencia fue necesaria en ese ejemplo para evitar basar una acción importante en información desactualizada. Aquí se utiliza el mismo tipo de razonamiento. Una diferencia es que el READCOMMITTEDLOCK La sugerencia adquiere bloqueos compartidos en lugar de bloqueos de actualización. Además, SQL Server adquiere automáticamente bloqueos de actualización para proteger las modificaciones de datos bajo RCSI sin requerir que agreguemos una sugerencia explícita.

Tomar bloqueos de actualización también asegura que la declaración de actualización o eliminación bloqueará si encuentra un bloqueo incompatible, por ejemplo, un bloqueo exclusivo que protege una modificación de datos en curso realizada por otra transacción simultánea.

Una complicación adicional es que el comportamiento modificado solo se aplica a la mesa que es el objetivo de la operación de actualización o eliminación. Otras mesas en el mismo declaración de eliminación o actualización, incluidas referencias adicionales a la tabla de destino, siga usando versiones de fila .

Probablemente se requieran algunos ejemplos para aclarar un poco más estos comportamientos confusos...

Configuración de prueba

La siguiente secuencia de comandos garantiza que todos estemos configurados para usar RCSI, crea una tabla simple y le agrega dos filas de ejemplo:

ALTER DATABASE Sandpit
SET READ_COMMITTED_SNAPSHOT ON
WITH ROLLBACK IMMEDIATE;
GO
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
GO
CREATE TABLE dbo.Test
(
    RowID integer PRIMARY KEY,
    Data integer NOT NULL
);
GO
INSERT dbo.Test
    (RowID, Data)
VALUES 
    (1, 1234),
    (2, 2345);

El siguiente paso debe ejecutarse en una sesión separada . Inicia una transacción y elimina ambas filas de la tabla de prueba (parece extraño, pero todo esto tendrá sentido en breve):

BEGIN TRANSACTION;
DELETE dbo.Test 
WHERE RowID IN (1, 2);

Tenga en cuenta que la transacción se deja abierta deliberadamente . Esto mantiene los bloqueos exclusivos en ambas filas que se eliminan (junto con los bloqueos exclusivos de intención habituales en la página contenedora y la tabla en sí), como se puede usar para mostrar la siguiente consulta:

SELECT
    resource_type,
    resource_description,
    resource_associated_entity_id,
    request_mode,
    request_status
FROM sys.dm_tran_locks
WHERE 
    request_session_id = @@SPID;

La Prueba Selecta

Volver a la sesión original , lo primero que quiero mostrar es que las declaraciones de selección regulares que usan RCSI aún ven las dos filas que se eliminan. La siguiente consulta de selección utiliza versiones de fila para devolver los últimos datos confirmados en el momento en que comienza la instrucción:

SELECT *
FROM dbo.Test;

En caso de que parezca sorprendente, recuerde que mostrar las filas como eliminadas significaría mostrar una vista no confirmada de los datos, lo que no está permitido en el aislamiento de lectura confirmada.

La prueba de eliminación

A pesar del éxito de la prueba de selección, un intento de eliminar estas mismas filas de la sesión actual se bloquearán. Puede imaginar que este bloqueo ocurre cuando la operación intenta adquirir exclusividad cerraduras, pero ese no es el caso.

La eliminación no utiliza el control de versiones de fila para ubicar las filas a eliminar; en su lugar, intenta adquirir bloqueos de actualización. Los bloqueos de actualización son incompatibles con los bloqueos de fila exclusivos que mantiene la sesión con la transacción abierta, por lo que la consulta bloquea:

DELETE dbo.Test 
WHERE RowID IN (1, 2);

El plan de consulta estimado para esta declaración muestra que las filas que se eliminarán se identifican mediante una operación de búsqueda regular antes de que un operador independiente realice la eliminación real:

Podemos ver los bloqueos retenidos en esta etapa ejecutando la misma consulta de bloqueo que antes (desde otra sesión) recordando cambiar la referencia SPID a la utilizada por la consulta bloqueada. Los resultados se ven así:

Nuestra consulta de eliminación está bloqueada en el operador Búsqueda de índice agrupado, que está esperando adquirir un bloqueo de actualización para leer datos. Esto muestra que ubicar las filas para eliminar en RCSI adquiere bloqueos de actualización en lugar de leer datos versionados potencialmente obsoletos. También muestra que el bloqueo no se debe a la parte de eliminación de la operación que espera adquirir un bloqueo exclusivo.

La prueba de actualización

Cancele la consulta bloqueada e intente la siguiente actualización en su lugar:

UPDATE dbo.Test
SET Data = Data + 1000
WHERE RowID IN (1, 2);

El plan de ejecución estimado es similar al que se ve en la prueba de eliminación:

Compute Scalar está ahí para determinar el resultado de sumar 1000 al valor actual de la columna de datos en cada fila, que lee la búsqueda de índice agrupado. Esta declaración también bloqueará cuando se ejecuta, debido al bloqueo de actualización solicitado por la operación de lectura. La siguiente captura de pantalla muestra los bloqueos retenidos cuando la consulta se bloquea:

Como antes, la consulta se bloquea en la búsqueda, a la espera de que se libere el bloqueo exclusivo incompatible para poder adquirir un bloqueo de actualización.

La prueba de inserción

La siguiente prueba presenta una declaración que inserta una nueva fila en nuestra tabla de prueba, usando el valor de la columna Datos de la fila existente con ID 1 en la tabla. Recuerde que esta fila todavía está bloqueada exclusivamente por sesión con la transacción abierta:

INSERT dbo.Test
    (RowID, Data)
SELECT 3, Data
FROM dbo.Test
WHERE RowID = 1;

El plan de ejecución vuelve a ser similar a las pruebas anteriores:

Esta vez, la consulta no está bloqueada . Esto muestra que los bloqueos de actualización no se adquirieron al leer datos para el inserto. En su lugar, esta consulta utilizó el control de versiones de filas para adquirir el valor de la columna de datos para la fila recién insertada. No se adquirieron bloqueos de actualización porque esta declaración no encontró ninguna fila para modificar , simplemente lee datos para usar en la inserción.

Podemos ver esta nueva fila en la tabla usando la consulta de prueba de selección anterior:

Tenga en cuenta que somos capaz de actualizar y eliminar la nueva fila (lo que requerirá bloqueos de actualización) porque no hay un bloqueo exclusivo en conflicto. La sesión con la transacción abierta solo tiene bloqueos exclusivos en las filas 1 y 2:

-- Update the new row
UPDATE dbo.Test
SET Data = 9999
WHERE RowID = 3;
-- Show the data
SELECT * FROM dbo.Test;
-- Delete the new row
DELETE dbo.Test
WHERE RowID = 3;

Esta prueba confirma que las instrucciones de inserción no adquieren bloqueos de actualización al leer , porque a diferencia de las actualizaciones y eliminaciones, no modifican una fila existente. La parte de lectura de un inserto utiliza el comportamiento normal de control de versiones de filas de RCSI.

Prueba de referencia múltiple

Mencioné antes que solo la referencia de tabla única utilizada para ubicar filas para modificar adquiere bloqueos de actualización; otras tablas en la misma declaración de actualización o eliminación todavía leen versiones de fila. Como caso especial de ese principio general, una declaración de modificación de datos con múltiples referencias a la misma tabla solo aplica bloqueos de actualización en una instancia se utiliza para ubicar filas para modificar. Esta prueba final ilustra este comportamiento más complejo, paso a paso.

Lo primero que necesitaremos es una nueva tercera fila para nuestra tabla de prueba, esta vez con un cero en la columna Datos:

INSERT dbo.Test
    (RowID, Data)
VALUES
    (3, 0);

Como era de esperar, esta inserción procede sin bloqueos, lo que da como resultado una tabla que se ve así:

Recuerde, la segunda sesión aún tiene exclusividad bloquea las filas 1 y 2 en este punto. Somos libres de adquirir bloqueos en la fila 3 si es necesario. La siguiente consulta es la que usaremos para mostrar el comportamiento con múltiples referencias a la tabla de destino:

-- Multi-reference update test
UPDATE WriteRef
SET Data = ReadRef.Data * 2
OUTPUT 
    ReadRef.RowID, 
    ReadRef.Data,
    INSERTED.RowID AS UpdatedRowID,
    INSERTED.Data AS NewDataValue
FROM dbo.Test AS ReadRef
JOIN dbo.Test AS WriteRef
    ON WriteRef.RowID = ReadRef.RowID + 2
WHERE 
    ReadRef.RowID = 1;

Esta es una consulta más compleja, pero su funcionamiento es relativamente simple. Hay dos referencias a la tabla de prueba, una a la que he alias ReadRef y la otra WriteRef. La idea es leer desde la fila 1 (usando una versión de fila) a través de ReadRef y para actualizar la tercera fila (que necesitará un bloqueo de actualización) usando WriteRef.

La consulta especifica la fila 1 explícitamente en la cláusula where para la referencia de la tabla de lectura. Se une a la escritura referencia a la misma tabla agregando 2 a ese RowID (identificando así la fila 3). La declaración de actualización también usa una cláusula de salida para devolver un conjunto de resultados que muestra los valores leídos de la tabla de origen y los cambios resultantes realizados en la fila 3.

El plan de consulta estimado para esta declaración es el siguiente:

Las propiedades de la búsqueda etiquetadas como (1) mostrar que esta búsqueda está en ReadRef alias, leyendo datos de la fila con RowID 1:

Esta operación de búsqueda no localiza una fila que se actualizará, por lo que los bloqueos de actualización no tomado; la lectura se realiza utilizando datos versionados. La lectura no está bloqueada por los bloqueos exclusivos de la otra sesión.

El escalar de cómputo etiquetado como (2) define una expresión etiquetada como 1004 que calcula el valor actualizado de la columna de datos. La expresión 1009 calcula el Id. de fila que se actualizará (1 + 2 =Id. de fila 3):

La segunda búsqueda es una referencia a la misma tabla (3). Esta búsqueda localiza la fila que se actualizará (fila 3) usando la expresión 1009:

Debido a que esta búsqueda localiza una fila para cambiar, un bloqueo de actualización se toma en lugar de usar versiones de fila. No hay un bloqueo exclusivo conflictivo en la fila ID 3, por lo que la solicitud de bloqueo se otorga de inmediato.

El último operador resaltado (4) es la operación de actualización en sí. El bloqueo de actualización en la fila 3 se actualiza a un exclusivo bloquear en este punto, justo antes de que se realice la modificación. Este operador también devuelve los datos especificados en la cláusula de salida de la declaración de actualización:

El resultado de la declaración de actualización (generada por la cláusula de salida) se muestra a continuación:

El estado final de la tabla es como se muestra a continuación:

Podemos confirmar los bloqueos tomados durante la ejecución utilizando un seguimiento de Profiler:

Esto muestra que solo una actualización se adquiere el bloqueo de teclas de fila. Cuando esta fila llega al operador de actualización, el bloqueo se convierte en un exclusivo cerrar con llave. Al final de la instrucción, se libera el bloqueo.

Es posible que pueda ver en el resultado del seguimiento que el valor hash de bloqueo para la fila bloqueada por actualización es (98ec012aa510) en mi base de datos de prueba. La siguiente consulta muestra que este hash de bloqueo está asociado con RowID 3 en el índice agrupado:

SELECT RowID, %%LockRes%%
FROM dbo.Test;

Tenga en cuenta que los bloqueos de actualización tomados en estos ejemplos tienen una duración más corta que los bloqueos de actualización tomados si especificamos un UPDLOCK insinuación. Estos bloqueos de actualización internos se liberan al final de la instrucción, mientras que UPDLOCK los bloqueos se mantienen hasta el final de la transacción.

Esto concluye la demostración de casos en los que RCSI adquiere bloqueos de actualización para leer los datos confirmados actuales en lugar de utilizar el control de versiones de filas.

Bloqueos compartidos y de rango de claves bajo RCSI

Hay una serie de otros escenarios en los que el motor de la base de datos aún puede adquirir bloqueos en RCSI. Todas estas situaciones se relacionan con la necesidad de preservar la corrección que se vería amenazada al depender de datos versionados potencialmente desactualizados.

Bloqueos compartidos tomados para la validación de clave externa

Para dos tablas en una relación directa de clave externa, el motor de la base de datos debe tomar medidas para garantizar que no se violen las restricciones al depender de lecturas con versiones potencialmente obsoletas. La implementación actual hace esto cambiando a bloqueo de lectura confirmada al acceder a los datos como parte de una verificación automática de clave externa.

Tomar bloqueos compartidos garantiza que la verificación de integridad lea los últimos datos comprometidos (no una versión anterior) o bloques debido a una modificación simultánea en curso. El cambio a bloqueo de lectura confirmada solo se aplica al método de acceso particular utilizado para verificar los datos de la clave externa; otro acceso a datos en la misma declaración continúa usando versiones de fila.

Este comportamiento solo se aplica a declaraciones que cambian datos, donde el cambio afecta directamente una relación de clave externa. Para modificaciones a la tabla referenciada (principal), esto significa actualizaciones que afectan el valor referenciado (a menos que esté establecido en NULL ) y todas las eliminaciones. Para la tabla de referencia (secundaria), esto significa todas las inserciones y actualizaciones (de nuevo, a menos que la referencia clave sea NULL ). Las mismas consideraciones se aplican a los efectos de componente de un MERGE .

A continuación, se muestra un plan de ejecución de ejemplo que muestra una búsqueda de clave externa que toma bloqueos compartidos:

Serializable para claves foráneas en cascada

Cuando la relación de clave externa tiene una acción en cascada, la corrección requiere una escalada local a la semántica de aislamiento serializable. Esto significa que verá bloqueos de rango de teclas tomados para una acción referencial en cascada. Como fue el caso de los bloqueos de actualización que se vieron anteriormente, estos bloqueos de rango de claves se limitan a la declaración, no a la transacción. A continuación se muestra un plan de ejecución de ejemplo que muestra dónde se toman los bloqueos serializables internos bajo RCSI:

Otros escenarios

Hay muchos otros casos específicos en los que el motor extiende automáticamente la vida útil de las cerraduras o aumenta localmente el nivel de aislamiento para garantizar la corrección. Estos incluyen la semántica serializable utilizada cuando se mantiene una vista indexada relacionada o cuando se mantiene un índice que tiene la IGNORE_DUP_KEY conjunto de opciones.

El mensaje para llevar es que RCSI reduce la cantidad de bloqueo, pero no siempre puede eliminarlo por completo.

La próxima vez

La siguiente publicación de esta serie analiza el nivel de aislamiento de instantáneas.

[ Ver el índice de toda la serie ]