12.2. Isolation des transactions

Le standard SQL définit quatre niveaux d'isolation de transaction pour empêcher trois phénomènes de se produire lors de transactions concurrentes. Ces phénomènes indésirables sont :

lecture sale

Une transaction lit des données écrites par une transaction concurrente non validée.

lecture non reproductible

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

lecture fantôme

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 12-1.

Tableau 12-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

PostgreSQL offre les niveaux d'isolation de lecture validée et sérialisable.

12.2.1. Niveau d'isolation Read committed (lecture des seules données validées)

Read Commited est le niveau d'isolation par défaut de PostgreSQL. Lorsqu'une transaction fonctionne avec ce niveau d'isolation, une requête SELECT voit uniquement les données validées avant que la requête soit lancée ; elle ne voit jamais, lors de l'exécution de la requête, ni les données non validées ni les modifications validées par les transactions concurrentes (néanmoins, le SELECT voit les effets des précédentes mises à jour dans sa propre transaction, et ce même si elles ne sont pas validées). Dans les faits, une requête SELECT voit une image de la base de données à l'instant où la requête a été lancée. Notez que des commandes SELECT successives peuvent voir des données différentes, y compris si elles font partie de la même transaction, si d'autres transactions valident des modifications de données pendant l'exécution du premier SELECT.

Les commandes UPDATE, DELETE et SELECT FOR UPDATE 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 marquée pour mise à jour) 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 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 commençant par la version mise à jour de la ligne.

À 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 affectant 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 correct 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.

Comme dans le mode de lecture validée, chaque nouvelle commande commence avec une nouvelle image incluant toutes les transactions validées jusque là, les commandes suivantes dans la transaction verront les effets de la transaction concurrente validée dans tous les cas. La problématique ici est de savoir si à l'intérieur d'une même commande, nous avons ou non une vision totalement cohérente de la base de données.

L'isolation partielle de la transaction fournie par le mode de lecture validée est adéquate pour de nombreuses applications et ce mode est rapide et simple à utiliser. Néanmoins, pour les applications réalisant des requêtes et mises à jour complexes, il pourrait être nécessaire de garantir une vue plus cohérente de la base de données que ce que fournit le mode Read Commited.

12.2.2. Niveau d'isolation sérialisable

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 est dans 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, le SELECT 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ù SELECT 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 même transaction voit toujours les mêmes données.

Les commandes UPDATE, DELETE et SELECT FOR UPDATE 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 marquée pour mise à jour) 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 le processus de mise à jour est validé (et que la ligne est mise à jour ou supprimée, pas simplement sélectionnée en vue d'une mise à jour), 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 les lignes changées par d'autres transactions après que la transaction sérialisable ait commencé.

Quand l'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 re-lancement de transactions complexes pourrait être significatif, ce mode 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.