Database
 sql >> Base de données >  >> RDS >> Database

Modifications de données sous l'isolement d'instantané validé en lecture

[ Voir l'index pour toute la série ]

L'article précédent de cette série a montré comment une instruction T-SQL s'exécutant sous l'isolement d'instantané validé en lecture (RCSI ) voit normalement une vue instantanée de l'état validé de la base de données tel qu'il était au début de l'exécution de l'instruction. C'est une bonne description du fonctionnement des instructions qui lisent des données, mais il existe des différences importantes pour les instructions exécutées sous RCSI qui modifient les lignes existantes .

J'insiste sur la modification des lignes existantes ci-dessus, car les considérations suivantes s'appliquent uniquement à UPDATE et DELETE opérations (et les actions correspondantes d'un MERGE déclaration). Pour être clair, INSERT les déclarations sont spécifiquement exclues du comportement que je suis sur le point de décrire car les insertions ne modifient pas l'existant données.

Mettre à jour les verrous et les versions de lignes

La première différence est que les instructions de mise à jour et de suppression ne lisent pas les versions de ligne sous RCSI lors de la recherche des lignes sources à modifier. Mettre à jour et supprimer des instructions sous RCSI à la place acquiert des verrous de mise à jour lors de la recherche de lignes éligibles. L'utilisation de verrous de mise à jour garantit que l'opération de recherche trouve les lignes à modifier à l'aide des données validées les plus récentes .

Sans les verrous de mise à jour, la recherche serait basée sur une version éventuellement obsolète de l'ensemble de données (données validées telles qu'elles étaient au début de l'instruction de modification des données). Cela pourrait vous rappeler l'exemple de déclencheur que nous avons vu la dernière fois, où un READCOMMITTEDLOCK indice a été utilisé pour revenir de RCSI à l'implémentation de verrouillage de l'isolation validée en lecture. Cet indice était nécessaire dans cet exemple pour éviter de fonder une action importante sur des informations obsolètes. Le même type de raisonnement est utilisé ici. Une différence est que le READCOMMITTEDLOCK hint acquiert des verrous partagés au lieu de mettre à jour les verrous. De plus, SQL Server acquiert automatiquement des verrous de mise à jour pour protéger les modifications de données sous RCSI sans nous obliger à ajouter un indice explicite.

La prise de verrous de mise à jour garantit également que l'instruction de mise à jour ou de suppression sera bloquée s'il rencontre un verrou incompatible, par exemple un verrou exclusif protégeant une modification de données en cours effectuée par une autre transaction concurrente.

Une complication supplémentaire est que le comportement modifié s'applique uniquement à la table qui est la cible de l'opération de mise à jour ou de suppression. Autres tableaux dans le même supprimer ou mettre à jour la déclaration, y compris les références supplémentaires à la table cible, continuez à utiliser les versions de ligne .

Quelques exemples sont probablement nécessaires pour rendre ces comportements déroutants un peu plus clairs…

Configuration du test

Le script suivant garantit que nous sommes tous configurés pour utiliser RCSI, crée un tableau simple et y ajoute deux exemples de lignes :

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);

L'étape suivante doit s'exécuter dans une session distincte . Il démarre une transaction et supprime les deux lignes de la table de test (cela semble étrange, mais tout cela aura un sens sous peu) :

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

Notez que la transaction est délibérément laissée ouverte . Cela maintient les verrous exclusifs sur les deux lignes en cours de suppression (ainsi que les verrous exclusifs d'intention habituels sur la page contenante et le tableau lui-même) comme la requête ci-dessous peut être utilisée pour le montrer :

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

Le test de sélection

Revenir à la session d'origine , la première chose que je veux montrer est que les instructions de sélection régulières utilisant RCSI voient toujours les deux lignes supprimées. La requête de sélection ci-dessous utilise des versions de ligne pour renvoyer les dernières données validées au début de l'instruction :

SELECT *
FROM dbo.Test;

Au cas où cela semble surprenant, rappelez-vous que l'affichage des lignes comme supprimées signifierait l'affichage d'une vue non validée des données, ce qui n'est pas autorisé lors de l'isolement validé en lecture.

Le test de suppression

Malgré le succès du test de sélection, une tentative de suppression ces mêmes lignes de la session en cours seront bloquées. Vous pouvez imaginer que ce blocage se produit lorsque l'opération tente d'acquérir des informations exclusives se verrouille, mais ce n'est pas le cas.

La suppression n'utilise pas la gestion des versions de lignes pour localiser les lignes à supprimer ; il essaie d'acquérir des verrous de mise à jour à la place. Les verrous de mise à jour sont incompatibles avec les verrous de ligne exclusifs détenus par la session avec la transaction ouverte, donc la requête bloque :

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

Le plan de requête estimé pour cette instruction montre que les lignes à supprimer sont identifiées par une opération de recherche régulière avant qu'un opérateur distinct n'effectue la suppression réelle :

Nous pouvons voir les verrous détenus à ce stade en exécutant la même requête de verrouillage qu'auparavant (à partir d'une autre session) en n'oubliant pas de changer la référence SPID à celle utilisée par la requête bloquée. Les résultats ressemblent à ceci :

Notre requête de suppression est bloquée au niveau de l'opérateur Clustered Index Seek, qui attend d'acquérir un verrou de mise à jour pour lire Les données. Cela montre que la localisation des lignes à supprimer sous RCSI acquiert des verrous de mise à jour plutôt que de lire des données versionnées potentiellement obsolètes. Cela montre également que le blocage n'est pas dû à la partie suppression de l'opération en attente d'acquérir un verrou exclusif.

Le test de mise à jour

Annulez la requête bloquée et essayez plutôt la mise à jour suivante :

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

Le plan d'exécution estimé est similaire à celui vu dans le test de suppression :

Le Compute Scalar est là pour déterminer le résultat de l'ajout de 1000 à la valeur actuelle de la colonne Data dans chaque ligne, qui est lue par le Clustered Index Seek. Cette déclaration va également bloquer lorsqu'il est exécuté, en raison du verrou de mise à jour demandé par l'opération de lecture. La capture d'écran ci-dessous montre les verrous détenus lorsque la requête est bloquée :

Comme précédemment, la requête est bloquée lors de la recherche, en attendant que le verrou exclusif incompatible soit libéré afin qu'un verrou de mise à jour puisse être acquis.

Le test d'insertion

Le test suivant comporte une instruction qui insère une nouvelle ligne dans notre tableau de test, en utilisant la valeur de la colonne de données de la ligne existante avec ID 1 dans le tableau. Rappelons que cette ligne est toujours exclusivement verrouillée par session avec la transaction ouverte :

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

Le plan d'exécution est à nouveau similaire aux tests précédents :

Cette fois, la requête n'est pas bloquée . Cela montre que les verrous de mise à jour n'ont pas été acquis lors de la lecture données pour l'encart. Cette requête a plutôt utilisé la gestion des versions de ligne pour acquérir la valeur de la colonne de données pour la ligne nouvellement insérée. Les verrous de mise à jour n'ont pas été acquis car cette instruction n'a trouvé aucune ligne à modifier , il lit simplement les données à utiliser dans l'insertion.

Nous pouvons voir cette nouvelle ligne dans le tableau en utilisant la requête de test de sélection d'avant :

Notez que nous sommes capable de mettre à jour et de supprimer la nouvelle ligne (ce qui nécessitera des verrous de mise à jour) car il n'y a pas de verrou exclusif en conflit. La session avec la transaction ouverte n'a que des verrous exclusifs sur les lignes 1 et 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;

Ce test confirme que les instructions insert n'acquièrent pas de verrous de mise à jour lors de la lecture , car contrairement aux mises à jour et aux suppressions, elles ne modifient pas une rangée existante. La partie lecture d'un insert utilise le comportement normal de contrôle de version des lignes RCSI.

Test de références multiples

J'ai mentionné précédemment que seule la référence de table unique utilisée pour localiser les lignes à modifier acquiert des verrous de mise à jour ; d'autres tables dans la même instruction de mise à jour ou de suppression lisent toujours les versions de ligne. Comme cas particulier de ce principe général, une déclaration de modification de données avec plusieurs références à la même table applique uniquement les verrous de mise à jour sur une instance utilisé pour localiser les lignes à modifier. Ce test final illustre ce comportement plus complexe, étape par étape.

La première chose dont nous aurons besoin est une nouvelle troisième ligne pour notre tableau de test, cette fois avec un zéro dans la colonne Données :

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

Comme prévu, cette insertion se déroule sans blocage, ce qui donne un tableau qui ressemble à ceci :

N'oubliez pas que la deuxième session est toujours exclusive verrouille les lignes 1 et 2 à ce stade. Nous sommes libres d'acquérir des verrous sur la ligne 3 si nous en avons besoin. La requête suivante est celle que nous utiliserons pour afficher le comportement avec plusieurs références à la table cible :

-- 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;

Il s'agit d'une requête plus complexe, mais son fonctionnement est relativement simple. Il existe deux références à la table de test, l'une que j'ai alias ReadRef et l'autre WriteRef. L'idée est de lire à partir de la ligne 1 (en utilisant une version de ligne) via ReadRef, et pour mettre à jour la troisième ligne (qui nécessitera un verrou de mise à jour) en utilisant WriteRef.

La requête spécifie explicitement la ligne 1 dans la clause where pour la référence de table de lecture. Il se joint à la référence d'écriture à la même table en ajoutant 2 à ce RowID (identifiant ainsi la ligne 3). L'instruction de mise à jour utilise également une clause de sortie pour renvoyer un ensemble de résultats indiquant les valeurs lues à partir de la table source et les modifications résultantes apportées à la ligne 3.

Le plan de requête estimé pour cette instruction est le suivant :

Les propriétés de la recherche étiquetées (1) montrer que cette recherche est sur le ReadRef alias, lecture des données de la ligne avec RowID 1 :

Cette opération de recherche ne localise pas une ligne qui sera mise à jour, donc les verrous de mise à jour ne sont pas pris; la lecture est effectuée à l'aide de données versionnées. La lecture n'est pas bloquée par les verrous exclusifs détenus par l'autre session.

Le scalaire de calcul étiqueté (2) définit une expression étiquetée 1004 qui calcule la valeur mise à jour de la colonne de données. L'expression 1009 calcule l'ID de ligne à mettre à jour (1 + 2 =ID de ligne 3) :

La deuxième recherche est une référence à la même table (3). Cette recherche localise la ligne qui sera mise à jour (ligne 3) à l'aide de l'expression 1009 :

Étant donné que cette recherche localise une ligne à modifier, un verrou de mise à jour est pris au lieu d'utiliser des versions de ligne. Il n'y a pas de verrou exclusif en conflit sur la ligne ID 3, la demande de verrou est donc accordée immédiatement.

Le dernier opérateur en surbrillance (4) est l'opération de mise à jour elle-même. Le verrou de mise à jour sur la ligne 3 est mis à niveau vers un exclusif verrouiller à ce stade, juste avant que la modification ne soit réellement effectuée. Cet opérateur renvoie également les données spécifiées dans la clause de sortie de la déclaration de mise à jour :

Le résultat de l'instruction de mise à jour (générée par la clause de sortie) est présenté ci-dessous :

L'état final de la table est comme indiqué ci-dessous :

Nous pouvons confirmer les verrous pris lors de l'exécution à l'aide d'une trace du profileur :

Cela montre qu'une seule mise à jour le verrouillage des touches de rangée est acquis. Lorsque cette ligne atteint l'opérateur de mise à jour, le verrou est converti en un exclusif fermer à clé. A la fin de l'instruction, le verrou est relâché.

Vous pourrez peut-être voir à partir de la sortie de trace que la valeur de hachage de verrouillage pour la ligne verrouillée pour la mise à jour est (98ec012aa510) dans ma base de données de test. La requête suivante montre que ce hachage de verrou est bien associé au RowID 3 dans l'index cluster :

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

Notez que les verrous de mise à jour pris dans ces exemples ont une durée de vie plus courte que les verrous de mise à jour pris si nous spécifions un UPDLOCK indice. Ces verrous de mise à jour internes sont libérés à la fin de l'instruction, alors que UPDLOCK les verrous sont maintenus jusqu'à la fin de la transaction.

Ceci conclut la démonstration des cas où RCSI acquiert des verrous de mise à jour pour lire les données validées actuelles au lieu d'utiliser la gestion des versions de ligne.

Serrures partagées et à plage de clés sous RCSI

Il existe un certain nombre d'autres scénarios dans lesquels le moteur de base de données peut encore acquérir des verrous sous RCSI. Ces situations sont toutes liées à la nécessité de préserver l'exactitude qui serait menacée en s'appuyant sur des données versionnées potentiellement obsolètes.

Verrous partagés pris pour la validation de clé étrangère

Pour deux tables dans une relation de clé étrangère simple, le moteur de base de données doit prendre des mesures pour s'assurer que les contraintes ne sont pas violées en s'appuyant sur des lectures versionnées potentiellement obsolètes. L'implémentation actuelle le fait en passant au verrouillage de la lecture validée lors de l'accès aux données dans le cadre d'une vérification automatique de clé étrangère.

La prise de verrous partagés garantit que le contrôle d'intégrité lit les toutes dernières données validées (pas une ancienne version) ou les blocs en raison d'une modification simultanée en cours. Le passage au verrouillage de la lecture validée s'applique uniquement à la méthode d'accès particulière utilisée pour vérifier les données de la clé étrangère ; les autres accès aux données dans la même instruction continuent d'utiliser les versions de ligne.

Ce comportement s'applique uniquement aux instructions qui modifient les données, où la modification affecte directement une relation de clé étrangère. Pour les modifications apportées à la table référencée (parente), cela signifie des mises à jour qui affectent la valeur référencée (sauf si elle est définie sur NULL ) et toutes les suppressions. Pour la table de référence (enfant), cela signifie toutes les insertions et mises à jour (encore une fois, sauf si la référence de clé est NULL ). Les mêmes considérations s'appliquent aux effets de composant d'un MERGE .

Un exemple de plan d'exécution montrant une recherche de clé étrangère qui prend des verrous partagés est illustré ci-dessous :

Sérialisable pour les clés étrangères en cascade

Lorsque la relation de clé étrangère a une action en cascade, l'exactitude nécessite une escalade locale vers une sémantique d'isolement sérialisable. Cela signifie que vous verrez des verrous de plage de clés pris pour une action référentielle en cascade. Comme c'était le cas pour les verrous de mise à jour vus précédemment, ces verrous de plage de clés sont limités à l'instruction, pas à la transaction. Un exemple de plan d'exécution montrant où les verrous sérialisables internes sont pris sous RCSI est présenté ci-dessous :

Autres scénarios

Il existe de nombreux autres cas spécifiques où le moteur prolonge automatiquement la durée de vie des verrous ou augmente localement le niveau d'isolement pour garantir l'exactitude. Ceux-ci incluent la sémantique sérialisable utilisée lors de la maintenance d'une vue indexée associée, ou lors de la maintenance d'un index qui a le IGNORE_DUP_KEY jeu d'options.

Le message à retenir est que RCSI réduit la quantité de verrouillage, mais ne peut pas toujours l'éliminer complètement.

La prochaine fois

Le prochain article de cette série porte sur le niveau d'isolement des instantanés.

[ Voir l'index pour toute la série ]