Je suis Charlie

Autres trucs

Accueil

Seulement les RFC

Seulement les fiches de lecture

Ève

Isolation des transactions, oui, mais à quel niveau ?

Première rédaction de cet article le 28 juillet 2010


Un des services les plus importants que rendent les SGBD est d'isoler les transactions les unes des autres, c'est-à-dire de faire en sorte que, pendant qu'une transaction joue avec les données, les autres pourront continuer à travailler tranquillement, avec des données stables. Mais certains débutants en SGBD ignorent qu'il existe plusieurs niveaux d'isolation dans SQL et que tous ne garantissent pas une isolation parfaite, loin de là.

Prenons l'exemple de PostgreSQL, qui documente très bien ces différents niveaux d'isolation. On va créer une table pour essayer :

essais=> CREATE TABLE Foo (id SERIAL, name TEXT, number INTEGER);

Maintenant, si on se connecte et qu'on lance une transaction, par défaut, celle-ci aura le niveau d'isolation read committed. Cela vaut dire qu'on verra les COMMIT des autres (ce que la littérature SQL appelle un phantom read)  :

essais=> BEGIN;
BEGIN
essais=> SELECT * FROM foo;
 id | name | number 
----+------+--------
(0 rows)

[Ici, une autre transaction fait un 
"INSERT INTO Foo (name, number) VALUES ('durand', 15);"]

essais=> SELECT * FROM foo;
 id |  name  | number 
----+--------+--------
  1 | durand |     15
(1 rows)

On n'a donc pas été complètement isolé des autres transactions. Si on veut le faire, il faut indiquer explicitement, après le début de la transaction mais avant la première requête, un niveau plus isolant :

essais=> BEGIN;
BEGIN
essais=> SET TRANSACTION isolation level serializable;
SET
essais=> SELECT * FROM foo;
 id |  name  | number 
----+--------+--------
  1 | durand |     15
(1 rows)

[Cette fois, même si une autre transaction fait, par exemple, "INSERT
INTO Foo (name, number) VALUES ('dupont', 1)", nous ne le verrons pas
tant que notre propre transaction est en cours.]

essais=> SELECT * FROM foo;
 id |  name  | number 
----+--------+--------
  1 | durand |     15
(1 rows)

[Lorsque notre transaction se termine, on voit les changements faits
entre temps.]

essais=> COMMIT;
COMMIT
essais=> SELECT * FROM foo;
 id |  name  | number 
----+--------+--------
  1 | durand |     15
  2 | dupont |      1
(2 rows)

Pourquoi tout le monde n'utilise pas ce niveau maximal d'isolation ? Pourquoi n'est-il pas la valeur par défaut ? Parce qu'isoler a un coût, oblige le SGBD à plus de travail et à utiliser davantage de verrous. Pire, cela peut conduire à des cas où on a une erreur dans l'une des transactions. Prenons le cas où, pendant qu'on lit gentiment la table, une autre transaction la modifie avec UPDATE Foo SET number=6 WHERE id=2;. Par défaut, on verra ce changement dans notre propre transaction. Mais si on cherche la bagarre et qu'on demande à être complètement isolé :

essais=> BEGIN;
BEGIN
essais=> SET transaction ISOLATION LEVEL  serializable;
SET
essais=> SELECT * FROM foo;
...
  1 | durand |     15
...

[Ici, une autre transaction modifie cette ligne. On ne voit pas le
changement, comme prévu.]

essais=> SELECT * FROM foo;
...
  1 | durand |     15
...

[Mais si on demande à changer cette ligne ?]

essais=> DELETE FROM foo WHERE id=1;
ERROR:  could not serialize access due to concurrent update

Cette fois, on a eu un conflit. PostgreSQL ne pouvait pas faire les deux opérations demandées. L'une d'elles se termine donc en erreur.

Si l'autre transaction ne faisait pas de COMMIT immédiatement, PostgreSQL est dans l'incertitude, il ne sait pas encore laquelle faire échouer car il ne sait pas s'il n'y aura pas plutôt un ROLLBACK :

essais=> BEGIN;
BEGIN
essais=> SET transaction ISOLATION LEVEL  serializable;
SET
essais=> SELECT * FROM foo WHERE id=6;
 id |  name  | number 
----+--------+--------
  6 | armand |      8
(1 row)

[Ici, une autre transaction modifie cette ligne. On ne voit pas le
changement, comme prévu. Mais :]

essais=> DELETE FROM foo WHERE id=6;

[Ici, la transaction est bloquée. DELETE ne rendra pas la main avant
que l'autre transaction n'aie décidée d'un COMMIT ou d'un ROLLBACK. On
voit que cela peut être gênant pour certaines applications.]

Si l'autre transaction fait un COMMIT, on est ramené au cas précédent : le DELETE se termine en erreur. Si l'autre transaction fait un ROLLBACK (renonce à son opération de mise à jour), alors le DELETE peut se terminer normalement.

Attention pour le comportement par défaut si on utilise une bibliothèque d'accès à PostgreSQL. Celle-ci peut mettre automatiquement dans un niveau d'isolation différent. Pour Python, la norme d'interface avec les SGBD n'en parle pas. Avec psycopg, ce n'est pas le cas, le niveau par défaut est le read committed. Ce petit programme Python permet de tester. Il ouvre une transaction et affiche régulièrement la valeur d'une des lignes. Par défaut, il montre les changements que fait une autre transaction pendant ce temps.

PostgreSQL, pour réaliser cette isolation, utilise un mécanisme nommé MVCC. Son principal slogan est que « les écritures ne bloquent jamais les lectures et les lectures ne bloquent jamais les écritures ». (Dans l'exemple plus haut où DELETE était bloqué, c'est parce que l'autre opération était également une écriture, un UPDATE.)

Les essais ci-dessus étaient faits avec PostgreSQL. Mais cela serait à peu près pareil avec tous les SGBD sérieux. Par exemple, pour Oracle, on peut lire « On Transaction Isolation Levels » ou « How Serializable Transactions Interact ». Pour les autres modèles de SGBD, voir la bonne liste de Wikipédia.

Ceux et celles qui veulent lire davantage peuvent regarder un bon cours en français ou bien une bonne discussion sur Stack Overflow, qui explique bien pourquoi un haut niveau d'isolation n'est pas forcément souhaitable (il diminue le parallélisme).

Version PDF de cette page (mais vous pouvez aussi imprimer depuis votre navigateur, il y a une feuille de style prévue pour cela)

Source XML de cette page (cette page est distribuée sous les termes de la licence GFDL)