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,
« Niveaux d'isolation des transactions SQL ».
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
|
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.
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
,
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 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 (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 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
,
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 le processus de mise à jour est
validé (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 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.
12.2.2.1. Isolation
sérialisable contre vraie sérialisation
La signification intuitive (et la définition mathématique) de
l'exécution « sérialisable »
est que toute paire de transactions concurrentes validées 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 12.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 garantie pas une exécution en série dans ce
sens
. Comme exemple, considérez une 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. En concurrence,
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. [] 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 représenter 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 comprennent plus de discussion sur ce sujet.