Les problèmes de concurrence sont difficiles de la même manière que la programmation multithread est difficile. À moins que l'isolation sérialisable ne soit utilisée, il peut être difficile de coder des transactions T-SQL qui fonctionneront toujours correctement lorsque d'autres utilisateurs apporteront des modifications à la base de données en même temps.
Les problèmes potentiels peuvent être non triviaux même si la 'transaction' en question est un simple SELECT
déclaration. Pour les transactions complexes à plusieurs instructions qui lisent et écrivent des données, le potentiel de résultats inattendus et d'erreurs en cas de forte simultanéité peut rapidement devenir écrasant. Tenter de résoudre des problèmes de simultanéité subtils et difficiles à reproduire en appliquant des indices de verrouillage aléatoires ou d'autres méthodes d'essais et d'erreurs peut être une expérience extrêmement frustrante.
À bien des égards, le niveau d'isolement d'instantané semble être une solution parfaite à ces problèmes de concurrence. L'idée de base est que chaque transaction d'instantané se comporte comme si elle était exécutée par rapport à sa propre copie privée de l'état validé de la base de données, prise au moment où la transaction a commencé. Fournir à l'ensemble de la transaction une vue immuable des données validées garantit évidemment des résultats cohérents pour les opérations en lecture seule, mais qu'en est-il des transactions qui modifient les données ?
L'isolement d'instantané gère les modifications de données de manière optimiste, en supposant implicitement que les conflits entre écrivains simultanés seront relativement rares. Lorsqu'un conflit d'écriture se produit, le premier committer gagne et la transaction perdante voit ses modifications annulées. C'est malheureux pour la transaction annulée, bien sûr, mais s'il s'agit d'un événement suffisamment rare, les avantages de l'isolement d'instantané peuvent facilement l'emporter sur les coûts d'un échec occasionnel et d'une nouvelle tentative.
La sémantique relativement simple et propre de l'isolement d'instantané (par rapport aux alternatives) peut être un avantage significatif, en particulier pour les personnes qui ne travaillent pas exclusivement dans le monde des bases de données et ne connaissent donc pas bien les différents niveaux d'isolement. Même pour les professionnels chevronnés des bases de données, un niveau d'isolement relativement "intuitif" peut être un soulagement bienvenu.
Bien sûr, les choses sont rarement aussi simples qu'elles le paraissent au premier abord, et l'isolement des instantanés ne fait pas exception. La documentation officielle décrit assez bien les principaux avantages et inconvénients de l'isolation des instantanés. La majeure partie de cet article se concentre donc sur l'exploration de certains des problèmes les moins connus et les moins surprenants que vous pourriez rencontrer. Mais tout d'abord, jetons un coup d'œil sur les propriétés logiques de ce niveau d'isolement :
Propriétés ACID et isolation des instantanés
L'isolement d'instantané ne fait pas partie des niveaux d'isolement définis dans la norme SQL, mais il est encore souvent comparé à l'aide des « phénomènes de concurrence » qui y sont définis. Par exemple, le tableau de comparaison suivant est reproduit à partir de l'article technique de SQL Server, "SQL Server 2005 Row Versioning-Based Transaction Isolation" de Kimberly L. Tripp et Neal Graves :
En fournissant une vue ponctuelle de données validées , l'isolement d'instantané offre une protection contre les trois phénomènes de simultanéité qui y sont présentés. Les lectures incorrectes sont évitées car seules les données validées sont visibles, et la nature statique de l'instantané empêche à la fois les lectures non répétables et les fantômes.
Cependant, cette comparaison (et la section surlignée en particulier) montre seulement que les niveaux d'isolement instantané et sérialisable empêchent les trois mêmes phénomènes spécifiques. Cela ne signifie pas qu'ils sont équivalents à tous égards. Il est important de noter que la norme SQL-92 ne définit pas l'isolation sérialisable en termes de trois phénomènes uniquement. La section 4.28 de la norme donne la définition complète :
L'exécution de transactions SQL simultanées au niveau d'isolement SERIALIZABLE est garantie d'être sérialisable. Une exécution sérialisable est définie comme une exécution des opérations d'exécution simultanée de transactions SQL qui produit le même effet qu'une exécution en série de ces mêmes transactions SQL. Une exécution en série est une exécution dans laquelle chaque transaction SQL s'exécute jusqu'à la fin avant que la prochaine transaction SQL ne commence.
L'étendue et l'importance des garanties implicites ici sont souvent omises. Pour l'énoncer en langage simple :
Toute transaction sérialisable qui s'exécute correctement lorsqu'elle est exécutée seule continuera à s'exécuter correctement avec n'importe quelle combinaison de transactions simultanées, ou elle sera annulée avec un message d'erreur (généralement un blocage dans l'implémentation de SQL Server).
Les niveaux d'isolement non sérialisables, y compris l'isolement d'instantané, ne fournissent pas les mêmes garanties solides d'exactitude.
Données obsolètes
L'isolement instantané semble presque d'une simplicité séduisante. Les lectures proviennent toujours de données validées à un moment donné, et les conflits d'écriture sont automatiquement détectés et traités. En quoi n'est-ce pas une solution parfaite pour toutes les difficultés liées à la simultanéité ?
Un problème potentiel est que les lectures d'instantanés ne reflètent pas nécessairement l'état validé actuel de la base de données. Une transaction d'instantané ignore complètement toutes les modifications validées apportées par d'autres transactions simultanées après le début de la transaction d'instantané. Une autre façon de dire cela est de dire qu'une transaction d'instantané voit des données obsolètes et obsolètes. Bien que ce comportement puisse être exactement ce qui est nécessaire pour générer un rapport ponctuel précis, il peut ne pas être aussi approprié dans d'autres circonstances (par exemple, lorsqu'il est utilisé pour appliquer une règle dans un déclencheur).
Écrire l'inclinaison
L'isolement d'instantané est également vulnérable à un phénomène quelque peu lié connu sous le nom de décalage d'écriture. La lecture de données obsolètes joue un rôle dans ce problème, mais ce problème aide également à clarifier ce que la « détection de conflit d'écriture » d'instantané fait et ne fait pas.
Le décalage d'écriture se produit lorsque deux transactions simultanées lisent chacune des données que l'autre transaction modifie. Aucun conflit d'écriture ne se produit car les deux transactions modifient des lignes différentes. Aucune transaction ne voit les modifications apportées par l'autre, car les deux lisent à partir d'un moment antérieur à ces modifications.
Un exemple classique d'inclinaison d'écriture est le problème du marbre blanc et noir, mais je veux montrer un autre exemple simple ici :
-- 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;
Sous l'isolement d'instantané, les deux tables de ce script se retrouvent avec une seule ligne contenant une valeur nulle. C'est un résultat correct, mais il n'est pas sérialisable :il ne correspond à aucun ordre d'exécution de transaction série possible. Dans tout programme véritablement en série, une transaction doit se terminer avant que l'autre ne démarre, de sorte que la deuxième transaction compte la ligne insérée par la première. Cela peut sembler un détail technique, mais rappelez-vous que les puissantes garanties de sérialisation ne s'appliquent que lorsque les transactions sont réellement sérialisables.
Une subtilité de détection des conflits
Un conflit d'écriture d'instantané se produit chaque fois qu'une transaction d'instantané tente de modifier une ligne qui a été modifiée par une autre transaction validée après le début de la transaction d'instantané. Il y a deux subtilités ici :
- Les transactions n'ont pas à changer toutes les valeurs de données ; et
- Les transactions n'ont pas à modifier les colonnes communes .
Le script suivant illustre les deux points :
-- 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;
Notez ce qui suit :
- Chaque transaction localise la même ligne en utilisant un index différent
- Aucune mise à jour n'entraîne une modification des données déjà stockées
- Les deux transactions "mettent à jour" différentes colonnes de la ligne.
Malgré tout cela, lorsque la première transaction est validée, la deuxième transaction se termine avec une erreur de conflit de mise à jour :
Résumé :La détection des conflits fonctionne toujours au niveau d'une ligne entière, et une "mise à jour" n'a pas à modifier réellement les données. (Au cas où vous vous poseriez la question, les modifications apportées aux données LOB ou SLOB hors ligne comptent également comme une modification de la ligne à des fins de détection de conflit).
Le problème de la clé étrangère
La détection de conflit s'applique également à la ligne parent dans une relation de clé étrangère. Lors de la modification d'une ligne enfant sous isolement d'instantané, une modification de la ligne parent dans une autre transaction peut déclencher un conflit. Comme précédemment, cette logique s'applique à l'ensemble de la ligne parent - la mise à jour du parent n'a pas à affecter la colonne de clé étrangère elle-même. Toute opération sur la table enfant nécessitant une vérification automatique de la clé étrangère dans le plan d'exécution peut entraîner un conflit inattendu.
Pour illustrer cela, créez d'abord les tables et exemples de données suivants :
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);
Exécutez maintenant ce qui suit à partir de deux connexions distinctes, comme indiqué dans les commentaires :
-- 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 lecture de la table factice est là pour s'assurer que la transaction d'instantané a officiellement commencé. Émission de BEGIN TRANSACTION
n'est pas suffisant pour faire cela; nous devons effectuer une sorte d'accès aux données sur une table utilisateur.
La première mise à jour de la table Child ne provoque pas de conflit car la définition de la colonne de référence sur NULL
ne nécessite pas de vérification de la table parent dans le plan d'exécution (il n'y a rien à vérifier). Le processeur de requêtes ne touche pas la ligne parente dans le plan d'exécution, donc aucun conflit ne se produit.
La deuxième mise à jour de la table Child déclenche un conflit car une vérification de clé étrangère est automatiquement effectuée. Lorsque la ligne Parent est accessible par le processeur de requêtes, elle est également vérifiée pour un conflit de mise à jour. Une erreur est générée dans ce cas car la ligne parent référencée a subi une modification validée après le démarrage de la transaction d'instantané. Notez que la modification de la table Parent n'a pas affecté la colonne de clé étrangère elle-même.
Un conflit inattendu peut également se produire si une modification de la table enfant fait référence à une ligne parent qui a été créée par une transaction simultanée (et cette transaction validée après le démarrage de la transaction d'instantané).
Résumé :Un plan de requête qui inclut une vérification automatique de clé étrangère peut générer une erreur de conflit si la ligne référencée a subi une sorte de modification (y compris la création !) Depuis le début de la transaction d'instantané.
Le problème de table tronquée
Une transaction d'instantané échouera avec une erreur si une table à laquelle elle accède a été tronquée depuis le début de la transaction. Cela s'applique même si la table tronquée n'avait pas de lignes au départ, comme le montre le script ci-dessous :
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;
Le SELECT final échoue avec l'erreur :
Il s'agit d'un autre effet secondaire subtil à vérifier avant d'activer l'isolement d'instantané sur une base de données existante.
La prochaine fois
Le prochain (et dernier) article de cette série parlera du niveau d'isolement en lecture non validée (affectueusement appelé "nolock").
[ Voir l'index pour toute la série ]