Documentation PostgreSQL 8.4.22 > Langage SQL > Contrôle d'accès simultané > Isolation des transactions | |
Contrôle d'accès simultané | Verrouillage explicite |
Le standard SQL définit quatre niveaux d'isolation de transaction pour empêcher trois phénomènes de se produire lors de transactions simultanées. Ces phénomènes indésirables sont :
Une transaction lit des données écrites par une transaction concurrente non validée.
Une transaction relit des données qu'elle a lu précédemment et trouve que les données ont été modifiées par une autre transaction (validée depuis la lecture initiale).
Une transaction ré-exécute une requête renvoyant un ensemble de lignes satisfaisant une condition de recherche et trouve que l'ensemble des lignes satisfaisant la condition a changé du fait d'une autre transaction récemment validée.
Les quatre niveaux d'isolation de transaction et les comportements correspondants sont décrits dans le Tableau 13.1, « Niveaux d'isolation des transactions SQL ».
Tableau 13.1. Niveaux d'isolation des transactions SQL
Niveau d'isolation | Lecture sale | Lecture non reproductible | Lecture fantôme |
---|---|---|---|
Uncommited Read (en français, « Lecture de données non validées ») | Possible | Possible | Possible |
Commited Read (en français, « Lecture de données validées ») | Impossible | Possible | Possible |
Repeatable Read (en français, « Lecture répétée ») | Impossible | Impossible | Possible |
Serializable (en français, « Sérialisable ») | Impossible | Impossible | Impossible |
Dans PostgreSQL™, vous pouvez demander un des quatre niveaux standards d'isolation de transaction. Mais, en interne, il existe seulement deux niveaux distincts d'isolation, qui correspondent aux niveaux Read Committed et Serializable. Lorsque vous sélectionnez le niveau Read Uncommitted, vous obtenez réellement Read Committed, et quand vous sélectionnez Repeatable Read, vous obtenez réellement Serializable, donc le niveau d'isolation actuel pourrait être plus strict que ce que vous sélectionnez. Ceci est permis par le standard SQL. Les quatre niveaux d'isolation définissent seulement quel phénomène ne doit pas survenir, ils ne définissent pas ce qui doit arriver. La raison pour laquelle PostgreSQL™ fournit seulement deux niveaux d'isolation est qu'il s'agit de la seule façon raisonnable de faire correspondre les niveaux d'isolation standards avec l'architecture de contrôle des accès simultanés multiversion. Le comportement des niveaux standards d'isolation est détaillé dans les sous-sections suivantes.
Pour initialiser le niveau d'isolation d'une transaction, utilisez la commande SET TRANSACTION.
Read Committed est le niveau d'isolation par défaut dans PostgreSQL™. Quand une transaction utilise ce niveau d'isolation, une requête SELECT (sans clause FOR UPDATE/SHARE) voit seulement les données validées avant le début de la requête ; il ne voit jamais les données non validées et les modifications validées pendant l'exécution de la requête par des transactions exécutées en parallèle. En effet, une requête SELECT voit une image de la base de données datant du moment où l'exécution de la requête commence. Néanmoins, SELECT voit les effets de mises à jour précédentes exécutées dans sa propre transaction, même si celles-ci n'ont pas encore été validées. De plus, notez que deux commandes SELECT successives peuvent voir des données différentes, même si elles sont exécutées dans la même transaction si d'autres transactions valident des modifications pendant l'exécution du premier SELECT.
Les commandes UPDATE, DELETE, SELECT FOR UPDATE et SELECT FOR SHARE se comportent de la même façon que SELECT en ce qui concerne la recherche des lignes cibles : elles ne trouveront que les lignes cibles qui ont été validées avant le début de la commande. Néanmoins, une telle ligne cible pourrait avoir déjà été mise à jour (ou supprimée ou verrouillée) par une autre transaction concurrente au moment où elle est découverte. Dans ce cas, le processus de mise à jour attendra que la première transaction soit validée ou annulée (si elle est toujours en cours). Si la première mise à jour est annulée, alors ses effets sont niés et le deuxième processus peut exécuter la mise à jour des lignes originellement trouvées. Si la première mise à jour est validée, la deuxième mise à jour ignorera la ligne si la première mise à jour l'a supprimée, sinon elle essaiera d'appliquer son opération à la version mise à jour de la ligne. La condition de la recherche de la commande (la clause WHERE) est ré-évaluée pour savoir si la version mise à jour de la ligne correspond toujours à la condition de recherche. Dans ce cas, la deuxième mise à jour continue son opération en utilisant la version mise à jour de la ligne. Dans le cas des commandes SELECT FOR UPDATE et SELECT FOR SHARE, cela signifie que la version mise à jour de la ligne est verrouillée et renvoyée au client.
À cause de la règle ci-dessus, une commande de mise à jour a la possibilité de voir une image non cohérente : elle peut voir les effets de commandes de mises à jour concurrentes sur les mêmes lignes que celles qu'elle essaie de mettre à jour mais elle ne voit pas les effets de ces commandes sur les autres lignes de la base de données. Ce comportement rend le mode de lecture validée non convenable pour les commandes qui impliquent des conditions de recherche complexes ; néanmoins, il est intéressant pour les cas simples. Par exemple, considérons la mise à jour de balances de banque avec des transactions comme :
BEGIN; UPDATE comptes SET balance = balance + 100.00 WHERE no_compte = 12345; UPDATE comptes SET balance = balance - 100.00 WHERE no_compte = 7534; COMMIT;
Si deux transactions comme celle-ci essaient de modifier en même temps la balance du compte 12345, nous voulons clairement que la deuxième transaction commence à partir de la version mise à jour de la ligne du compte. Comme chaque commande n'affecte qu'une ligne prédéterminée, la laisser voir la version mise à jour de la ligne ne crée pas de soucis de cohérence.
Des utilisations plus complexes peuvent produire des résultats non désirés dans le mode Read Committed. Par exemple, considérez une commande DELETE opérant sur des données qui sont à la fois ajoutées et supprimées du critère de restriction par une autre commande. Supposons que website est une table sur deux lignes avec website.hits valant 9 et 10 :
BEGIN; UPDATE website SET hits = hits + 1; -- exécuté par une autre session : DELETE FROM website WHERE hits = 10; COMMIT;
La commande DELETE n'aura pas d'effet même s'il existe une ligne website.hits = 10 avant et après la commande UPDATE. Cela survient parce que la valeur 9 de la ligne avant mise à jour est ignorée et que lorsque l'UPDATE termine et que DELETE obtient un verrou, la nouvelle valeur de la ligne n'est plus 10, mais 11, ce qui ne correspond plus au critère.
Comme le mode Read Committed commence chaque commande avec une nouvelle image qui inclut toutes les transactions validées jusqu'à cet instant, les commandes suivantes dans la même transaction verront les effets de la transaction validée en parallèle dans tous les cas. Le problème en question est de savoir si une seule commande voit une vue absolument cohérente ou non de la base de données.
L'isolation partielle des transactions fournie par le mode Read Committed est adéquate pour de nombreuses applications, et ce mode est rapide et simple à utiliser. Néanmoins, il n'est pas suffisant dans tous les cas. Les applications qui exécutent des requêtes et des mises à jour complexes pourraient avoir besoin d'une vue plus rigouresement cohérente de la base de données, une vue que le mode Read Committed ne fournit pas.
Le niveau sérialisable est le niveau d'isolation de transaction le plus strict. Ce niveau émule l'exécution de la transaction en série, comme si les transactions avaient été exécutées l'une après l'autre, en série plutôt que parallèlement. Néanmoins, les applications utilisant ce niveau doivent être préparées à tenter de nouveau les transactions suite aux échecs de la sérialisation.
Quand une transaction utilise le niveau sérialisable, une requête SELECT voit seulement les données validées avant le début de la transaction ; elle ne voit jamais les données non validées et les modifications validées lors de l'exécution de la transaction par des transactions concurrentes (néanmoins, la requête voit bien les effets des mises à jour précédentes exécutées à l'intérieur de sa propre transaction même si elles ne sont pas encore validées). C'est différent du niveau Read Commited dans la mesure où une requête, dans une transaction sérialisable, voit une image du début de la transaction et non pas du début de la requête en cours à l'intérieur de la transaction. Du coup, les commandes SELECT successives à l'intérieur d'une seule transaction voient toujours les mêmes données, c'est-à-dire qu'elles ne voient jamais les modifications faites par les autres transactions qui ont validé après le début de leur propre transaction (ce comportement peut être idéal pour les applications de création de rapports.)
Les commandes UPDATE, DELETE, SELECT FOR UPDATE et SELECT FOR SHARE se comportent de la même façon que SELECT en ce qui concerne la recherche de lignes cibles : elles trouveront seulement les lignes cibles qui ont été validées avant le début de la transaction. Néanmoins, une telle ligne cible pourrait avoir été mise à jour (ou supprimée ou verrouillée) par une autre transaction concurrente au moment où elle est utilisée. Dans ce cas, la transaction sérialisable attendra que la première transaction de mise à jour soit validée ou annulée (si celle-ci est toujours en cours). Si la première mise à jour est annulée, les effets sont inversés et la transaction sérialisable peut continuer avec la mise à jour de la ligne trouvée à l'origine. Mais si la mise à jour est validée (et que la ligne est mise à jour ou supprimée, pas simplement verrouillée), alors la transaction sérialisable sera annulée avec le message
ERROR: could not serialize access due to concurrent update
parce qu'une transaction sérialisable ne peut pas modifier ou verrouiller les lignes changées par d'autres transactions après que la transaction sérialisable ait commencé.
Quand une application reçoit ce message d'erreurs, elle devrait annuler la transaction actuelle et ré-essayer la transaction complète. La seconde fois, la transaction voit les modifications déjà validées comme faisant partie de sa vue initiale de la base de données, donc il n'y a pas de conflit logique en utilisant la nouvelle version de la ligne comme point de départ pour la mise à jour de la nouvelle transaction.
Notez que seules les transactions de modifications ont besoin d'être tentées de nouveau ; les transactions en lecture seule n'auront jamais de conflits de sérialisation.
Le mode sérialisable fournit une garantie rigoureuse que chaque transaction accède à une vue totalement cohérente de la base de données. Néanmoins, l'application doit être prête à tenter de nouvelles transactions lorsque des mises à jour concurrentes rendent impossibles de soutenir l'illusion d'une exécution en série. Comme le coût de la ré-exécution de transactions complexes pourrait être significatif, le mode sérialisable est seulement recommandé lors de mise à jour contenant une logique suffisamment complexe pour donner de mauvaises réponses dans le mode de lecture validée. Plus communément, le mode sérialisable est nécessaire quand une transaction exécute plusieurs commandes successives qui doivent avoir des vues identiques de la base de données.
La signification intuitive (et la définition mathématique) de l'exécution « sérialisable » est que toute paire de transactions concurrentes validée avec succès apparaîtra comme ayant été exécutée en série, l'une après l'autre -- bien que celle survenant en premier n'est pas prévisible. Il est important de réaliser qu'interdire les comportements indésirables listés dans le Tableau 13.1, « Niveaux d'isolation des transactions SQL » n'est pas suffisant pour garantir une vraie exécution en série et, en fait, le mode sérialisable de PostgreSQL™ ne garantit pas une exécution en série dans ce sens. Comme exemple, considérez la table ma_table, contenant initialement
classe | valeur --------+------- 1 | 10 1 | 20 2 | 100 2 | 200
Supposons que la transaction sérialisable A traite
SELECT SUM(valeur) FROM ma_table WHERE classe = 1;
puis insère le résultat (30) comme valeur dans une nouvelle ligne avec classe = 2. Simultanément, la transaction serialisable B traite
SELECT SUM(valeur) FROM ma_table WHERE classe = 2;
et obtient le résultat 300, qu'il insère dans une nouvelle ligne avec classe = 1. Donc, les deux transactions valident. Aucun des comportements indiqués comme non désirables n'est survenu, pourtant nous avons un résultat qui n'aurait pû arriver dans tout ordre sériel. Si A a été exécuté avant B, B aurait trouvé la somme 330, et non pas 300. De façon similaire, l'autre ordre aurait eu comme résultat une somme différente pour le calcul par A.
Pour garantir une vraie sérialisation mathématique, il est nécessaire que le système de bases de données force le verrouillage du prédicat, ce qui signifie qu'une transaction ne peut pas insérer ou modifier une ligne qui aurait correspondue à la condition WHERE d'une requête dans une autre transaction concurrente. Par exemple, une fois que la transaction A a exécuté la requête SELECT ... WHERE classe = 1, un système à verrouillage de prédicat aurait interdit l'insertion de toute ligne de classe 1 par la transaction B jusqu'à la validation de la transaction A. [8] Un tel système de verrous est complexe à implémenter et extrêmement coûteux à l'exécution car toutes les sessions doivent être conscientes des détails de chaque requête exécutée par chaque transaction concurrente. Et cette grande dépense est pratiquement complètement perdue car, en pratique, la plupart des applications ne posent pas ce genre de problèmes (l'exemple ci-dessus est assez petit et a peu de chance de se présenter avec de vrais logiciels). Pour ces raisons, PostgreSQL™ n'implémente pas le verrouillage de prédicat.
Dans ces cas où la possibilité d'une exécution non sérialisable est un vrai hasard, les problèmes peuvent être prévenus par l'utilisation appropriée d'un verrou explicite. Les sections suivantes contiennent plus d'informations sur ce sujet.
[8] En fait, un système de verrouillage de prédicat empêche les lectures fantômes en restreignant ce qui est écrit alors que MVCC les empêche de restreindre ce qui est lu.