Transactions

La plupart du temps, les modifications à notre base de données sont groupées, selon la logique business :

  • Plusieurs lignes sont modifiĂ©es ou supprimĂ©es

  • Plusieurs tables sont modifiĂ©es

Plus que souvent cette logique business s’effectue cotĂ© serveur (dans le cas de PHP) ou cotĂ© API.

Prenons l'exemple d'une opération de banque : Personne Y veut verser une somme à une personne X.

Exemple d'une opération multi-commande

On voit dans l'image que le transfert d'argent s'effectue par plusieurs commandes SQL. Une opération logique, mais multi-commande coté SQL.

Le problÚme est que chaque commande SQL s'effectue quelques millisecondes aprÚs la précédente. Dans l'informatique, qui fonctionne au niveau de micro-secondes, il est bien possible qu'un sinistre arrive en plein milieu de cette opération !

Il y a quelques exemples possibles :

Une coupure de courant

Une coupure de courant en plein milieu de l'opération

Dans l'exemple, il y a une coupure de courant entre l'étape 4 et 5. Le problÚme est qu'on a déjà enlevé de l'argent du compte Y. AprÚs le rétablissement du serveur, on n'a aucune notion de ce qu'il n'est pas encore terminé.

On a dĂ©truit pour toujours la somme de 100 €. Personne Y ne l'a plus, et Personne X ne l'a pas reçu !

Une condition de course

Race condition

Dans l'exemple, dans l'opĂ©ration 1, on charge le solde (disons 200 €) de la Personne Y en mĂ©moire dans notre API ou appli PHP (par exemple).

Dans les quelques millisecondes entre l'Ă©tape 1 et 4, il arrive une mise Ă  jour d'une autre appli qui change le solde Ă  50 €. On peut dire un virement automatique de paiement de loyer.

Notre processus, ensuite, procĂšde Ă  effectuer son update, en Ă©crasant la valeur actuelle (50 €), par 100 €. Notre solde n'est plus cohĂ©rente ni fiable !!

Une autre exemple :

Race condition

Dans cet exemple, on aura dĂ©jĂ  supprimĂ© la somme du compte de la Personne Y, seulement pour trouver que le compte de la personne X a Ă©tĂ© suspendu. À ce moment-lĂ , on signale une erreur, sauf la Personne Y a dĂ©jĂ  perdu sa somme !

Opération ACID

Nous cherchons alors à éviter tous ces problÚmes, avec une rÚgle simple :

Tout ou rien !

Plus formellement, on parle d'une opération ACID :

  • AtomicitĂ© : Toutes les opĂ©rations s’effectuent correctement dans la base de donnĂ©es, sinon aucun changement n’est enregistrĂ©

  • CohĂ©rence : La base de donnĂ©es commence dans un Ă©tat consistant, et se termine dans un Ă©tat consistant. En partie, implĂ©mentĂ© par les contraintes sur les donnĂ©es, mais aussi sous la responsabilitĂ© du dĂ©veloppeur.

  • Isolement : Les transactions peuvent s’effectuer en parallĂšle, mais la SGBDR fait une sorte de garantir l’isolement des donnĂ©es touchĂ©es par chaque transaction. Si les mĂȘmes donnĂ©es sont touchĂ©es par 2 transactions, faire une sorte de les effectuer dans l’ordre

  • DurabilitĂ© : AprĂšs la fin d’une transaction, tous les changements sont persistants, mĂȘme en cas de panne

Transactions

Les SGDBR implémente ce qu'on appelle une transaction, une technique qui assure les propriétés ACID sur un ensemble de commandes SQL.

La syntaxe est le suivant :

Dans les sections suivantes, on va analyser l'usage des transactions, et tester des différentes caractéristiques ACID.

Atomicité

Créons une table pour notre transaction bancaire, et insérons 2 lignes pour nos deux comptes :

Actuellement, notre table est le suivant :

client_id
balance

1

2000

2

5000

Maintenant, on va implémenter le transfert d'argent de notre exemple, à l'aide des variables SQL :

Tout se passe bien, cette fois-ci, le résultat est :

client_id
balance

1

1900

2

5100

On répÚte la procédure, mais en simulant une panne de courant :

Comment arrĂȘter votre Container docker ? Si vous avez crĂ©e votre SGBDR avec un fichier docker-compose.yml, il suffit de naviguer dans le dossier de ce fichier et taper :

Quand on relance notre Container docker, et on regarde notre table, on verra des valeurs inconsistantes :

client_id
balance

1

1800

2

5100

On a dĂ©truit 100 € ! La seule façon serait de manuellement dĂ©tecter le problĂšme, et le corriger Ă  la main avec :

Maintenant, testons avec l'outil transaction de notre SGBDR ! Ajoutons la ligne start transaction avant notre opération, et commit à la fin de notre opération.

Testez ! Commencez Ă  taper vos commandes, et arrĂȘtez votre instance en plein milieu, comme indiquĂ©.

Relancez votre Container, et analysez la table :

client_id
balance

1

1900

2

5100

SuccÚs !! La modification qu'on a apportée avant la coupure a été annulée !

Cohérence

La cohĂ©rence de nos donnĂ©es est quelque chose qui, en partie, peut ĂȘtre assurĂ© par notre SGBDR Ă  travers des contraintes et les transactions.

Imaginons l'exemple dans lequel Personne Y essaye de transférer plus d'argent de que ce qui est disponible dans son compte.

On voit que la derniĂšre ligne produit une erreur ! En effet, Ă  la crĂ©ation de cette table, nous avons ajoutĂ© une contrainte que le solde d'un compte ne peut jamais ĂȘtre infĂ©rieur Ă  zĂ©ro ! Le SGBDR nous a protĂ©gĂ©s contre une incohĂ©rence !

En revanche, à nous de décider comment réagir.

À ce moment, si on fait un select de notre table, on verra que le solde du compte 2 a Ă©tait dĂ©jĂ  fait :

client_id
balance

1

1900

2

7600

On pourrait manuellement réduire la somme (compliqué !).

Mais, la transaction peut nous aider ! Il suffit de réagir à l'erreur en émettant un rollback :

Cette commande annule toutes les opérations depuis start transaction. Vérifions, en re-visionnant la table account :

client_id
balance

1

1900

2

5100

La notion de responsabilité partagée et illustrée dans l'image suivante :

Pseudo-code : Gestion de la consistance en dehors du SGBDR

Dans cet exemple, on implémente l'opération dans du code similaire à Javascript, à travers un driver, ou librairie, qui envoie des commandes SQL à notre SGBDR. Il y a deux cas de figure :

  1. Le SGBDR retourne une erreur, qui est manifesté en tant qu'exception dans notre code. Dans l'exemple gauche, il suffit d'attraper l'exception et renvoyer la commande rollback.

  2. Dans l'exemple sur le droit, l'erreur est plutĂŽt dĂ©tectĂ©e dans notre code, et pas par le SGBDR mĂȘme. C'est l'exemple oĂč la logique business est trop compliquĂ©e pour les simples contraintes possibles avec SQL. Dans ce cas, on lance une exception manuellement, et ensuite on rĂ©agit avec un rollback.

Isolement

Aujourd’hui nos bases de donnĂ©es opĂšrent de façon parallĂšle ! Plusieurs connexions peuvent se faire en mĂȘme temps, et plusieurs commandes traitĂ©es en parallĂšle.

Et si on modifiait le solde du compte 1 (on dĂ©pose de l’argent) pendant notre transaction de transfert d'argent ?

Pour simplifier nos tests, on commence par réinitialiser nos deux comptes :

Pour simuler des connexions parallĂšles, nous allons ouvrir deux clients SQL.

Dans l'invite de commande 1 on commence à taper l'opération de transfert d'avant :

Et dans l'invite de commande 2, on tape l'opĂ©ration de dĂ©pose de 5000 € dans le compte :

Simulez des opérations simultanées dans deux invites de commandes

Vous allez noter que dans le terminal 2, aprÚs avoir omis le update, l'invite de commande fige. Le SGBDR attend que la transaction qui a déjà agi sur la ligne soit terminé avec commit avant de procéder.

De cette maniÚre, l'isolement a été assuré pas le SGBDR !

Mais, ce n'est pas suffisant ! Regardons notre table :

client_id
balance

1

7000

2

5100

On a un solde de 7000 €, c'est comme si on n'a pas transfĂ©rĂ© 100 € Ă  la personne B !

Le problÚme vient de l'implémentation des transactions :

  • Chaque mise Ă  jour au sein d'une transaction est stockĂ© dans une table Ă©phĂ©mĂšre, et donc non visible pour toutes les commandes en dehors de la transaction

  • Ces modifications sont Ă©crites officiellement Ă  la commande commit.

Dans notre exemple, dans l'invite de commande 2, on a récupéré le solde pas encore touché par l'opération update (parce qu'elle n'a pas été commit encore). Donc le solde est erroné ! Mais, on ne peut pas le savoir !

L'isolement ne garantit pas de la cohérence !

En vrai, le problÚme de base est qu'on a réussi à lire une valeur qui était en plein de modification. Il faut isoler encore plus les données pour dire au SGBDR de mettre en pause toute opération qui peut agir sur une valeur en cours de modification dans une transaction.

Heureusement, il est possible de le faire, notamment en ajoutant for update Ă  la fin de notre commande select :

La clause for update signal au SGBDR qu'on a l'intention de modifier cette ligne, et toute opération de lecture en dehors de cette transaction devrait attendre.

Réessayons !

Et dans l'invite de commande 2, on tape l'opĂ©ration de dĂ©pose de 5000 € dans le compte :

Cette fois-ci, on observe que le deuxiĂšme invite de commandes fige plutĂŽt Ă  la ligne select. En fait, il sait maintenant que la ligne concernĂ©e est en train d'ĂȘtre modifiĂ© par une autre transaction, et qu'on devrait attendre.

Est-ce que les valeurs sont consistantes ? Oui !

client_id
balance

1

6900

2

5100

Deadlocks

La clause for update à l'effet de verrouiller les lignes concernées. En revanche, il faut l'utiliser avec précaution !

On risque de figer toute notre base si on verrouille trop de lignes en mĂȘme temps, car on crĂ©era ce qu'on appelle des impasses ou deadlocks.

Impasses

Créons un deadlock pour s'amuser !

Et dans l'invite de commande 2, on tape l'opĂ©ration de dĂ©pose de 5000 € dans le compte :

Impasse : un exemple

On voit que MariaDB nous détecte tout seul l'impasse :

Certains SGBDR aura une maniÚre de détecter des impasses (simples). Ou bien, ils imposent un timeout sur les transactions si une impasse est trop complexe à détecter automatiquement (s'il n'y a aucun mouvement sur la transaction pendant X secondes, on abandonne la transaction).

La solution est de mieux gĂ©rer les opĂ©rations pour Ă©viter l'impasse. Dans notre exemple, notre bĂȘtise Ă©tait de verrouiller les lignes dans le mauvais ordre. Si, dans l'invite de commandes 2, on avait commencĂ© par rĂ©cupĂ©rer le solde 1, on ne serait immĂ©diatement bloquĂ©, et on ne serait pas tombĂ© dans l'impasse.

Je vous laisse comme exercice la résolution de l'impasse !

Transactions et le DDL

Attention, les transactions sont respectĂ©es uniquement entre les requĂȘtes type DML: update, insert et delete.

Si, pendant une transaction, on rencontre une requĂȘte type DDL (alter, create, drop etc.) :

  • commit est implicitement appelĂ©, juste avant l’instruction DDL

  • une nouvelle transaction est implicitement créée juste aprĂšs l’instruction DDL

Vous perdez alors les propriétés ACID !

À souvenir notamment pour vos migrations !

Mis Ă  jour