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.

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

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

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 :

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
ACIDNous cherchons alors à éviter tous ces problÚmes, avec une rÚgle simple :

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 :
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 :
1
1900
2
5100
On répÚte la procédure, mais en simulant une panne de courant :
Quand on relance notre Container docker, et on regarde notre table, on verra des valeurs inconsistantes :
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 :
1
1900
2
5100
SuccÚs !! La modification qu'on a apportée avant la coupure a été annulée !
L'atomicité est donc assurée en utilisant start transaction et commit.
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 :
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 :
1
1900
2
5100
La cohérence est assurée par nos soins et le SGBDR :
Valider la cohérence de nos données est la responsabilité du développeur :
En mettant en place des contraintes
En faisant des tests de cohérence manuels
Parfois notre logique business et trop complexes pour les contraintes possibles avec SQL
Et, en réagissant correctement
Notamment en utilisant
rollbackpour la remise à zéro de nos données
La notion de responsabilité partagée et illustrée dans l'image suivante :

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

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 :
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 !

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

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 :

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 !
On n'est jamais à l'abri des impasses, mais on peut toujours essayer de les éviter :
un schéma bien normalisé va nous permettre de plus facilement isoler les lignes
nâacquĂ©rez que les locks strictement nĂ©cessaires
attention Ă lâordre de rĂ©cupĂ©ration de locks !
implĂ©menter un systĂšme backoff : quand une transaction Ă©choue Ă cause dâun deadlock, attendez quelques secondes, puis rĂ©essayez.
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.) :
commitest implicitement appelĂ©, juste avant lâinstruction DDLune 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