Ecritures cohérentes, par Tom Kyte (1ère partie)

Cet article est la traduction d’un article de Tom Kyte sur son blog (L’article original en anglais se trouve ici). L’article est en 3 parties (il s’agit ici de la partie 1) et il est extrait de son livre Expert Oracle Database Architecture.

Il peut être utile de (re)lire avant:

Nous avons étudié précédemment (en français ici) la lecture cohérente: la capacité d’Oracle à utiliser les informations d’undo pour fournir des requêtes non-bloquantes et cohérentes (c’est à dire correctes) pour une lecture. Nous savons que lorsque Oracle lit des blocs pour notre requête, dans le buffer cache, il s’assure que la version du bloc est assez ‘vieille’ pour que notre requête puisse le voir.

Mais cela soulève la question suivante: Qu’en est-il des écritures/modifications ? Qu’est-ce qui se passe lorsque vous exécutez l’instruction UPDATE suivante: UPDATE T SET X = 2 WHERE Y = 5;
et que, pendant que cette requête est en cours d’exécution, quelqu’un modifie une ligne en faisant passer la valeur de Y de 5 vers 6, et commite cette modification, avant que nôtre requête n’ait lu cette ligne ?

Autrement dit, lorsque votre mise à jour a commencé, certaines lignes ont Y=5. Et comme votre mise à jour fait des lectures cohérentes, elle voit pour cette ligne Y=5, puisque c’est la valeur qu’il y avait au moment où votre update a démarré. Cependant, la valeur actuelle de Y est maintenant 6, et non plus 5, et avant de mettre à jour la valeur de X, Oracle va vérifier si la valeur de Y est toujours 5. Maintenant que doit-il se passer ? Et comment cela impacte les update ?

De toute évidence, nous ne pouvons pas modifier une ancienne version d’un bloc: quand on va modifier une ligne, il faut modifier la version actuelle du bloc. En outre, Oracle ne peut pas simplement ignorer cette ligne, car ce serait une lecture incohérente et imprévisible. Ce que nous allons découvrir, c’est que dans ce cas, Oracle va redémarrer à zéro l’écriture des modification.

Lectures cohérentes (Consistent Reads) et lectures courantes (Current Reads)

Oracle peut faire 2 types de lectures de blocs (block gets) lorsqu’il execute un requête de mise à jour (voir aussi cette traduction de Jonathan Lewis). Il fait:

  • Des lectures cohérentes (Consistent reads): pour ‘chercher’ les lignes à modifier
  • Des lectures courantes (Current reads): pour prendre le bloc dans lequel il y a les lignes à modifier.

On peut voir cela facilement avec TKPROF. Considerons le petit exemple suivant, qui lit et modifie l’unique ligne de la table T qu’on a déjà vu:

ORA10GR1> alter session set sql_trace=true;
Session altered.

ops$tkyte@ORA10GR1> select * from t;

X
----------
10001

ops$tkyte@ORA10G> update t t1 set x = x+1;
1 row updated.

ops$tkyte@ORA10G> update t t2 set x = x+1;
1 row updated.

TKPROF donne le résultat suivant (j’ai supprimé les colonnes ELAPSED, CPU, and DISK):

select * from t

call    count query current rows
------- ----- ----- ------- ----
Parse       1     0       0    0
Execute     1     0       0    0
Fetch       2     3       0    1
------- ----- ----- ------- ----
total       4     3       0    1

update t t1 set x = x+1

call    count query current rows
------- ----- ----- ------- ----
Parse       1     0       0    0
Execute     1     3       3    1
Fetch       0     0       0    0
------- ----- ----- ------- ----
total       2     3       3    1

update t t2 set x = x+1

call    count query current rows
------- ----- ----- ------- ----
Parse       1     0       0    0
Execute     1     3       1    1
Fetch       0     0       0    0
------- ----- ----- ------- ----
total       2     3       1    1

Ainsi, pour un select normal, nous avons 3 lectures cohérentes (query ou consistent gets). Pour le premier UPDATE, nous avons les 3 mêmes lectures (car ici la partie de recherche des lignes doit trouver toutes les lignes qui sont dans la table au moment où la requête démarre), mais aussi 3 lectures courantes.
Les 3 lectures en mode courant (current mode gets) servent

  1. à accéder au bloc courant, tel qu’il existe actuellement, celui avec la ligne à modifier
  2. à accéder à un bloc de segment d’undo pour commencer notre transaction (l’enregistrer dans la table des transactions)
  3. à accéder à un bloc d’undo (pour écrire les informations d’undo)

Le deuximème UPDATE ne fait qu’une lecture en mode courant, car nous n’avons pas à répeter le travail pour les blocs d’undo. Il n’y a que le bloc avec la ligne à mettre à jour. La présence du current get nous indique qu’une modification, de quelque sorte qu’elle soit, a eu lieu. Avant de modifier un bloc avec une nouvelle donnée, il doit avoir sa version la plus récente.

Alors comment la lecture cohérente peut affecter une mise à jour ? Bien, imaginons maintenant l’UPDATE suivant:

Update t set x = x+1 where y = 5;

Nous savons que la clause WHERE Y=5 , qui est la partie qui doit être cohérente en lecture, va être faite en mode cohérent (consistent, ou query dans TKPROF). Les enregistrements que voit la requête sont ceux qui ont la valeur Y=5, commitée, au moment où la requête démarre (en supposant qu’on est en niveau d’isolation READ COMMITTED sinon, en SERIALIZABLE, ce seraient ceux qui ont la valeur Y=5, commitée, au moment où la transaction démarre). Cela signifie que si la requête dure 5 minutes, du début à la fin, et qu’entre temps quelqu’un insère et commite un nouvel enregistrement qui a la valeur Y=5, alors notre UPDATE ne verra pas cet enregistrement parce que la lecture cohérente ne le lira pas. C’est le comportement attendu et normal. Mais la question qui se pose est maintenant: que se passe-t-il si deux sessions exécutent les requêtes suivantes, dans l’ordre::

Update t set y = 10 where y = 5;
Update t Set x = x+1 Where y = 5;

Voici ce qu’il se passe en sequence:

  1. Session 1: Update t set y = 10 where y = 5;
    mise à jour de la ligne qui correspond au critère.
  2. Session 2: Update t Set x = x+1 Where y = 5;
    Du fait de la lecture cohérente, la ligne modifiée par la session 1 correspond au critère (la session 1 n’a pas encore commité) mais ne peut pas la mettre à jour car elle est vérouillée par la session 1. La session 2 est bloquée et attends.
  3. Session 1: Commit;
    Cela libère le verrou sur lequel attends la session 2, qui se débloque.
    Elle peut alors faire la lecture courante du bloc qui contient l’enregistrement pour lequel Y=5 au moment où la requête a démarré.

Mais voilà que l’enregistrement qui avait Y=5 au moment du démarrage de l’UPDATE n’a plus la valeur Y=5. La lecture cohérente nous dit qu’il faut mettre à jour cette ligne parce que Y=5 au moment où la requête a démarrée. Mais la version courante du bloc nous dit que non, ce serait une erreur, on ne peut pas mettre à jour cette ligne puisque maintenant Y n’est plus égal à 5.

Si nous décidions juste de passer cette ligne et de l’ignorer, alors l’UPDATE ne serait pas deterministe. Ce serait jeter à la poubelle la cohérence et l’intégrité des données. Le résultat de l’UPDATE (combien de lignes et lesquelles sont modifiées) dépendrait de l’ordre dans lequel on traite les lignes de la table, et des autres activités qu’il y a autour. Vous pourriez prendre le même jeu de données dans 2 bases différentes, chacune exécutant les mêmes transactions dans le même ordre, et vous auriez des résultats différentes, simplement parce que les enregistrements sont placés différemment sur le disque.

Alors dans cette situation, Oracle choisit de redémarrer l’UPDATE. Lorsque il voit que la ligne qui avait Y=5 au début de la requête a maintenant la valeur Y=10, alors Oracle, si on est en niveau d’isolation READ COMMITTED, va silencieusement faire un rollback des modifications faites et redémarrer l’UPDATE. En niveau d’isolation SERIALIZABLE, alors on aurait un message d’erreur ORA-08177 Impossible de sérialiser l'accès pour cette transaction. (can’t serialize access for this transaction). En READ COMMITTED, après avoir rollbacké les modifications, Oracle va relancer l’update (donc le moment du démarrage de la requête, pour la lecture cohérente, n’est plus le même). Mais cette fois il va faire un SELECT FOR UPDATE en essayant de verrouiller toutes les lignes pour lesquelles Y=5. Une fois que c’est fait, il va faire l’UPDATE sur ces lignes qui sont verrouillées par notre session, étant sûr maintenant de pouvoir terminer sans avoir à redémarrer.

Continuons avec les questions ‘que se passe-t-il ?’. Le SELECT FOR UPDATE utilise les mêmes lectures cohérentes (pour trouver les enregistrements) et lectures courantes (pour les verrouiller) que l’UPDATE. Alors que se passe-t-il si une ligne qui avait Y=5 au démarrage de notre update se trouve avoir maintenant la valeur Y=11 ? Alors le SELECT FOR UPDATE va lui aussi redémarrer.

Il ya deux questions qui seront abordées ici, deux questions qui m’ont beaucoup intéressé, de toute façon. La première était: Peut-on observer cela ? Peut-on s’en apercevoir lorsque çà arrive ?
Et la deuxième: Et alors ? En quoi suis-je concerné en tant que développeur ?

Nous traiterons ces questions chacune à leur tour.
A suivre…

Les 2 articles correspondants seront traduits prochainement:

Laisser un commentaire