Skip to content

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\).

figure 60

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).

figure 61

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).

figure 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.