1 4 8 6 Option transactions
Une étude de cas : aspects transactionnels (option / pas dans le programme)¶
Supports complémentaires
Reprenons le programme transactionnel d´envoi de message. Même sur un exemple aussi simple, il est utile de se poser quelques questions sur ses propriétés dans un environnement sujet aux pannes et à la concurrence.
Une exécution de ce programme crée une transaction par message. Chaque
transaction lit un message sans date d´envoi dans le curseur, envoie le
message, puis modifie le message dans la base en affectant la date
d´envoi. La transaction se termine par un commit
. Que peut-on en
déduire, en supposant un environnement idéal sans panne, où chaque
transaction est la seule à accéder à la base quand elle s´exécute? Dans
un tel cas, il est facile de voir que chaque message serait envoyé
exactement une fois. Les choses sont moins plaisantes en pratique,
regardons-y de plus près.
Cas d´une panne¶
Imaginons (pire scénario) une panne juste avant le commit
, comme
illustré sur la Fig. 60. Cette figure montre la phase d´exécution, suivie de la
séquence des transactions au sein desquelles on a mis en valeur celle
affectant le message \(M_1\).
Fig. 60. Cas d'une panne en cours de transaction
Au moment du redémarrage après la panne, le SGBD va effectuer un
rollback
qui affecte la transaction en cours. Le message reprendra
donc son statut initial, sans date d´envoi. Il a pourtant été envoyé:
l´envoi n´étant pas une opération de base de données, le SGBD n´a
aucun moyen de l´annuler (ni même d´ailleurs de savoir quelle action
le programme client a effectuée). C´est donc un premier cas qui viole
le comportement attendu (chaque message envoyé exactement une fois).
Il faudra relancer le programme en espérant qu´il se déroule sans
panne. Cette seconde exécution ne sélectionnera pas les messages traités
par la première exécution avant \(M_1\) puisque ceux-là ont fait
l´objet d´une transaction réussie. Selon le principe de durabilité, le
commit
de ces transactions réussies n´est pas affecté par la panne.
Le curseur est-il impacté par une mise à jour?¶
Passons maintenant aux problèmes potentiels liés à la concurrence.
Supposons, dans un premier scénario, qu´une mise à jour du message
\(M_1\) soit effectuée par une autre transaction entre l´exécution de la
requête et le traitement de \(M_1\). La Fig. 61 montre
l´exécution concurrente de deux exécutions du programme d´envoi: la
première transaction (en vert) modifie le message et effectue un
commit
avant la lecture de ce message par la seconde (en orange).
Fig. 61. Cas d'une mise à jour après exécution de la requête mais avant traitement du message
Question: cette mise à jour sera-t-elle constatée par la lecture de \(M_1\)? Autrement dit, est-il possible que l´on constate, au moment de lire ce message dans la transaction orange, qu´il a déjà une date d´envoi parce qu´il a été modifié par la transaction verte?
On pourrait être tenté de dire "Oui" puisqu´au moment où la transaction orange débute, le message a été modifié et validé. Mais cela voudrait dire qu´un curseur permet d´accéder à des données qui ne correspondent pas au critère de sélection ! (En l´occurrence, on s´attend à ne recevoir que des messages sans date d´envoi). Ce serait très incohérent.
En fait, tout se passe comme si le résultat du curseur était un "cliché" pris au moment de l´exécution, et immuable durant tout la durée de vie du curseur. En d´autres termes, même si le parcours du résultat prend 1 heure, et qu´entretemps tous les messages ont été modifiés ou détruits, le système continuera à fournir via le curseur l´image de la base telle qu´elle était au moment de l´exécution.
En revanche, si on exécutait à nouveau une requête pour lire le message
juste avant la modification de ce dernier, on verrait bien la mise à
jour effectuée par la transaction verte. En résumé: une requête fournit
la version des nuplets effective, soit au moment où la requête est
exécutée (niveau d´isolation read committed
), soit au moment où la
transaction débute (niveau d´isolation repeatable read
).
Conséquence: sur le scénario illustré par la
transaction-messages-2
, on enverra le
message deux fois. Une manière d´éviter ce scénario serait de
verrouiller tous les nuplets sélectionnés au moment de l´exécution, et
d´effectuer l´ensemble des mises à jour en une seule transaction.
Transactions simultanées¶
Voici un dernier scénario, montrant un exécution simultanée ou quasi-simultanée de deux transactions concurrentes affectant le même message (Fig. 62).
Fig. 62. Exécution concurrente, avec risque de deadlock
Cette situation est très peu probable, mais pas impossible. Elle
correspond au cas-type dit "des mises à jour perdues" étudié dans le
chapitre sur les transactions. Dans tous les niveaux d´isolation sauf
serializable
, le déroulé sera le suivant:
- chaque transaction lit séparément le message
- une des transactions, disons la verte, effectue une mise à jour
- la seconde transaction (orange) tente d´effectuer la mise à jour et est mise en attente;
- la transaction verte finit par effectuer un
commit
, ce qui libère la transaction orange: le message est envoyé deux fois.
En revanche, en mode serializable
, chaque transaction va bloquer
l´autre sur le scénario de la Fig. 62. Le système va
détecter cet interblocage et rejeter une des transactions.
La bonne méthode¶
Ce qui précède mène à proposer une version plus sûre d´un programme d´envoi.
# Tous les messages non envoyés
messages = connexion.cursor()
messages.execute("SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE")
# Début de la transaction
connexion.begin()
messages.execute("select * from Message where dateEnvoi is null")
for message in messages.fetchall():
# Marquage du message
maj = connexion.cursor()
maj.execute ("Update Message set dateEnvoi='2018-12-31' "
+ "where idMessage=%s", message['idMessage'])
print ("Envoi du message ...", message['contenu'])
connexion.commit()
Tout d´abord (ligne 3) on se place en niveau d´isolation sérialisable.
Puis (ligne 5), on débute la transaction à l´extérieur de la boucle du curseur, et on la termine après la boucle (ligne 17). Cela permet de traiter la requête du curseur comme partie intégrante de la transaction.
Au moment de l´exécution du curseur, les nuplets sont réservés, et une exécution simultanée sera mise en attente si elle essaie de traiter les mêmes messages.
Avec cette nouvelle version, la seule cause d´envoi multiple d´un message et l´occurence d´un panne. Et le problème dans ce cas vient du fait que l´envoi n´est pas une opération contrôlée par le serveur de données.