---
title: "Base de données avancées : Mise en œuvre des transactions"
date: 2022-03-29
tags: ["transaction"]
categories: ["Base de données avancées", "Cours"]
mathjax: true
---

Une transaction est une unité d'un programme qui accède à des données d'un SGBDR
en lecture, écriture ou les deux. Une transaction accède à un **état cohérent**
de la base. Puis lors de son exécution l'état de la base **peut ne plus être
cohérent**. Lors de sa validation (on parle de *commit*), l'état de la base
**doit redevenir cohérent**.

Deux problèmes types peuvent se poser :

 * problème systèmes: récupérabilité
 * exécution concurente de plusieurs transactions: sériabilité.

Le système doit garantir les propriété ACID afin de preserver la cohérence des
données :

 * Atomicité: soit **toutes** les opérations sont validées soit aucune.
 * Cohérence: l'exécution d'une transaction jusqu'à sa validation doit laisser
   la base dans un **état cohérent**.
 * Isoliation: les transactions qui s'exécutent en concurence sont isolées les
   unes des autres. Les modification effectuée par l'une ne doivent pas être
   prise en comptes par une autre.
 * Durabilité: si une transaction est validée, les modification qu'elle a
   apportée à la base sont **persitantes**, même après le redémarrage du
   service, une coupure de courant ou un crash

## L'exemple de la banque

Prenons un exemple de virement de 1000 euros d'un compte A vers un compte B:

 1. Lire(A)
 2. A = A - 1000
 3. Ecrire(A)
 4. Lire B
 5. B - B + 1000
 6. Ecrire(B)

Une fois la transaction terminée et validée, elle est réputée **durable**. Si
elle échoue à partir de l'etape 3, il faut s'assurer que les modifications ne
soient pas persistantes.

Les données sont alors **cohérente** : la somme de A est B est toujours la même.
                 
Si une autre transaction accède à la base entre les étapes 3 et 6, elle trouvera
la base dans un état incohérent (A + B est inférieur et 1000 euros se
balladent...). **L'isolation** n'est pas assurée, la solution la plus simple
consisterai alors à **sérialiser** les transactions.

## État d'une transaction

Une transaction peut avoir 5 états:

 * Active: c'est une transaction en cours d'exécution
 * Partiellement validée: entre la dernière action et la validation
 * Échec: lorque l'exécution normale de la transaction ne peut avoir lieu
 * Avortée: après que toutes les modifications faite par la transaction soient
   annulées (rollback). Il est alors possible de réexécuter la transaction ou de
   la supprimer.
 * Validée: après l'exécution réussie de la dernière opération.

![États d'une transation](./images/etat_transaction.svg)

## Implémentation de l'atomicité

### Approche naïve

Nous pouvons commencer par une approche naïve : **copie intégrale de la base
lors d'une opération** avec gestion d'un **pointeur** qui mème vers la dernière
version cohérente. Le pointeur est modifié seulement si la transaction réussie.

En plus de ne permettre l'exécution d'une seule transaction (pas de gestion de
l'exécution concurrente), cette approche est lourde et coûteuse surtout si la
base est volumineuse.

### L'exécution concurrente

Dans ce cas de figure, plusieurs transaction peuvent s'exécuter en même temps.
Cette approche apporte une **meilleure utilisation des ressources** ( voir les 
[cours de système]({{< ref "/systemes_exploitation/1-introduction/index.md" >}}),
ainsi on réduit le temps de réponse: une longue transaction ne bloque plus les
autres.

**Le contrôle de la concurence** est le mécanisme permettant l'interaction entre
les transaction tout en garantissant l'intégrité de la base.

## Ordonnancement

Ici encore on peut se référer au cours de *système d'exploitation*. 
L'ordonnancement défini la manière d'exécuter les transactions concurrentes.

### Ordonnancement en série

Soit \\(T_1\\) et \\(T_2\\) deux transactions ordonnancées par \\(O_1\\), en
série \\(T_2\\) sera exécutée lorsque \\(T_1\\) sera terminée.

### Ordonnancement entrelacé

Ici les actions réalisées par deux transaction s'exécutent de manières
entrelacées. Soit deux transactions \((T_2\)) et \((T_3\)), voici le plan
d'ordonnancement \\(O_2\\):


\\( \begin{array}{l|l}
T_1 & T_2 \\\
lire(A) & \\\
A := A + 1000 &\\\
ecrire(A) &\\\
 & lire(A) \\\
 & tmp := A * 0.2 \\\
 & A := A - tmp \\\
 & ecrire(A) \\\
lire(B) & \\\
B := B + 1000 & \\\
ecrire(B $ \\\
 & lire(B) \\\
 & B = B + tmp \\\
 & ecrire(B) \\\
\end{array}
\\)

Dans l'exemple ci-dessus, l'entrelassement ne préserve pas les valeurs de A et
B. Les valeurs finales de notre version entrelacée ne correxpondent plus aux
valeurs de la version en série. **Nous sommes donc face à un problème**

### Notion de sériabilité

Dans le cas d'opération de lecture / écriture, nous partons du principe que
chaque transaction prise à part conserve la coherence de la base. Ainsi
l'ordonnancement en série préserve la cohérence.

Un ordonnancement entrelace est dit *sérialisable* si son résultat est
**exactement le même** que s'il est exécuté en série. 

L'ordonnancement \\(O_f\\) composé de n transactions \\(T_1, T_2, ... T_n\\) est
sérialisable si toutes les permutations possibles ne changent pas le résultat.
Il faut donc tester \\(n!\\) possibilités de permutations.

Pour tester et trouver le bon ordonnancement, nous avons à notre disposition la
c-sériabilité.

### c-seriabilité

Les instructions notées \\(t_x\\) et \\(t_y\\) des transactions \\(T_x\\) et
\\(T_y\\) ordonnancées par \\(O_n\\) accèdent au même objet \\(Q\\):

 * ne sont **pas en conflit** si elles accèdent toutes des deux à \\(Q\\) en
   lecture  
 * sont **en conflit** si l'une des deux (ou les deux) accède à \\(Q\\)en 
   écriture

Si notre ordonnancement \\(O_n\\) peut-être transformé en \\(O_n^\text{'}\\) par
une série de remplacement d'instructions non conflictuelle alors \\(O_n\\) et
\\(O_n^\text{'}\\) sont dit **c-équivalent**. \\(O_n\\) est c-sérialisable s'il
est c-équivalent à un ordonnancement en série.

## Reprise sur panne, récupérabilité

un Ordonnancement \\(O\\) est récupérable si à la suite de l'annulation d'une de
ses transactions, nous pouvons revenir à un état cohérent sans pour autant
annuler les transaction validées.

Lors d'un **ordonnancement récupérable**, si \\(T_j\\) lit un objet précédement
écrit par \\(T_i\\), la validation de \\(T_i\\) a lieu avant celle de de
\\(T_j\\). Dans le cadre d'un *ordonnancement en série* le problème se se pose
pas, mais comme nous l'avons vu c'est une méthode inefficace, surtout sans le
cas d'achitecture multi-cœurs / multi-proceseurs.

C'est au SGBD de s'assurer de la récupérabilité des ordonnancements, mais cette
tâche parfois difficile, prenons l'exemple simple suivant:


\\( \begin{array}{l|l}
t_1 & t_2 \\\
lire(a) & \\\
a := a + 1000 &\\\
ecrire(a) &\\\
 & lire(a) \\\
ecrire(b) & \\\
... & ... \\\\
\end{array}
\\)

Si \\(T_1\\) doit être annulée, alors la valeur de \\(A\\) pourrait-être dans un
état incohérent lorsque \\(T_1\\) y accède. Le SGBDR doit alors positionner le
curseur entre **ordonnancement** (réactivité, utilisation des ressources) et
**récupérabilité**.

Pour deux transactions \\(T_i\\), \\(T_j\\),si \\(T_j\\) accède  une donnée
écrite précédement par \\(T_i\\) alors cette dernière doit être validée en 
premier.

L'échec d'une transaction peut entrainer des annulations en cascade, dans
l'exemple suivant \\(T_2\\) et \\(T_3\\) doivent être annulé:

\\( \begin{array}{l|l}
t_1 & t_2 & t_3\\\
lire(a) & & \\\
a := a + 1000 & &\\\
ecrire(a) & &\\\
 & lire(a) & \\\
 & & lire(a) \\\
ecrire(b) & & \\\
... & ... & ... \\\
\end{array}
\\)

Un ordonnancement sans cascade est récupérable, il est donc préférable de
limiter les ordonnancement entrelacé à ceux sans cascade.

### tester la c-sériabilité

Considérons un ordonnancement \\(O\\) avec les transactions \\(T_1, T_2, ...
T_n\\) :

 * \\(n\\) est l'ensemble des transactions
 * il y a un arc \\((T_i, T_j)\\) s'il y a conflit entre ces deux transactions
   sur un object \\(Q\\), \\(T_i\\) accède à \\(Q\\) avant \\(T_j\\).

Alors \\(O\\) est *c-serialisable* si son graphe de précédence est acyclique.

\\( \begin{array}{l|l|l|l|l}
t_1 & t_2 & t_3 & t_4 & t_5\\\
 & lire(x) & & & \\\
lire(y) & & & & \\\
lire(z) & & & & \\\
 & & & & lire(v) \\\
 & & & & lire(w) \\\
 & & & & ecrire(w) \\\
 & lire(y) & & & \\\
 & ecrire(y) & & & \\\
 & & ecrire(z) & & \\\
lire(u) & & & & \\\
 & & & lire(y) & \\\
 & & & ecrire(y) & \\\
 & & & lire(z) & \\\
 & & & ecrire(z) & \\\
lire(u) & & & & \\\
ecrire(u) & & & \\\
\end{array}
\\)


![Graphe de précédence](./images/graphe_precedence.svg)

Tester si l'ordonnancement est sérialisable après son exécution
ou sur son graphe de précédence est inefficace. Le but ici est donc de
développer des statégies de contrôle de la concurence qui puissent garantir la
c-sériabilité. Il ne sera pas question de faire des tests sur le grahe de
précédence mais plutôt les mécanisme de verrouillage que nous verrons un peu
plus bas.

## contrôle de la concurrence

### notion de verrouillage

Lorsque une transaction accède à une donnée, elle pose un verrou dessus, il
existe deux type de verrous:

 * **exclusif**: la transaction qui pose se verrou peux *lire* et *écrire* la
   donnée. Le verrou `X` est alors attribué à l'exécution via la commande
   `Lock_X(data)`
 * **partagé**: la transaction ne peut que *lire* la donnée. Le verrou `P` est
   attribué via la commande `Lock_P(data)`

C'est le rôle du **gestionnaire de concurrence** de gérer l'attribution des
verrous à la demande des transaction. Il défini le protocole de verrouillage : 
commend demander et libérer les verrous. Cette dernière ne peut avancer tant 
que le verrou demandé ne lui a pas été attribué.

Une transaction ne peut poser un verrou sur une donnée qui si celui-ci est
compatible avec le verrou déjà en place (s'il y en a un bien entendu).

Une transaction peut libérer certain verrou avec la commande `Unlock()`.

### Gestion de l'interblocage (deadlock)

Comme nous l'avons vu pour les [Mecanisme de synchronisation]({{< ref
"/systemes_exploitation/3-synchronisation/index.md" >}}), dès qu'il y a verrou 
il y a risque d'interblocage. Les transaction n'échappent pas à la règle.

### Protocole de verrou à deux phases

Ce protocole permet de garantir la *c-sériabilité*

 * **phase 1**: pose de verrous, il est interdit d'en libérer.
 * **phase 2**: libération des verrou, il est interdit d'en poser.

### protocole avec estampille

C'est un protocole avec la gestion d'une file d'attente en fonction de l'heure
d'arrivée: l'ordre chronologique des transactions.

### granularité des verrouillage

Le verroullage peut se faire à plusieurs niveau organisés hiérachiquement:

 1. la base
 2. une table
 3. une page
 4. un tuple

Une transaction verrouiller n'importe quel niveau en fonction de ses besoins.

### gestion des blocages

Le SGBDR peut intervenië  sur deux niveaux : **prévenir** et *guérir*

#### prévenir

Il est question ici d'éviter les situations de blocage. L'estampille joue un
rôle crutial : la gestion des transaction se fait en fonction de son horodatage
(privilegier les plus anciennes)

#### guérir

Pour guérir mous pouvons

 1. détecter les blocages à l'aide de graphe précédence
 2. choisir d'ue transaction à annuler, en général celle la plus loin de son
    état final. 
