Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Transactions et Concurrence

Dans un environnement web, plusieurs utilisateurs peuvent faire des requêtes simultanément. Pire, un même utilisateur peut envoyer la même requête plusieurs fois très rapidement (double-clic, script, lag).

Cela peut causer des problèmes graves d’intégrité des données, appelés Race Conditions (Conditions de concurrence).

Le problème

Imaginons la fonction “Acheter un objet” :

  1. Lire le solde de l’utilisateur (ex: 100 pièces).
  2. Vérifier si le solde >= prix (ex: 50 pièces).
  3. Déduire le prix (Nouveau solde = 50).
  4. Sauvegarder le solde.
  5. Ajouter l’objet à l’inventaire.

Si deux requêtes arrivent exactement en même temps :

  • Requête A lit le solde : 100.
  • Requête B lit le solde : 100 (car A n’a pas encore sauvegardé !).
  • Requête A déduit 50 et sauvegarde -> Solde 50.
  • Requête B déduit 50 et sauvegarde -> Solde 50.
  • Résultat : L’utilisateur a payé 50 pièces au total mais a reçu l’objet deux fois ! Il aurait dû payer 100.

Solution 1 : Les Transactions

Une transaction garantit que plusieurs opérations de base de données sont traitées comme une seule unité indivisible (Atomicité).

Si une erreur survient au milieu, tout est annulé (Rollback).

Avec Entity Framework Core :

using var transaction = _context.Database.BeginTransactionAsync();

try
{
    // Opération 1 : Débit
    var user = _context.Users.Find(userId);
    user.Money -= 50;
    _context.SaveChangesAsync();

    // Opération 2 : Ajout Item
    _context.Inventory.Add(newItem);
    _context.SaveChangesAsync();

    // Si on arrive ici sans erreur, on valide tout
    await transaction.CommitAsync();
}
catch (Exception)
{
    // En cas d'erreur, tout est annulé automatiquement
    // (Le Rollback est implicite si on ne commit pas, mais on peut le forcer)
    await transaction.RollbackAsync();
    throw;
}

Cependant, les transactions seules ne résolvent pas toujours le problème de lecture concurrente (Requête B qui lit 100 alors que A est en train de modifier). Il faut souvent changer le Niveau d’Isolation de la transaction (ex: Serializable), ce qui peut ralentir la base de données.

Solution 2 : Concurrence Optimiste

La concurrence optimiste part du principe que les conflits sont rares. On laisse tout le monde lire, mais au moment d’écrire, on vérifie si la donnée a changé entre temps.

On ajoute souvent un jeton de concurrence (ex: RowVersion ou Timestamp) dans la table.

public class User 
{
    public int Id { get; set; }
    public int Money { get; set; }
    
    [Timestamp]
    public byte[] RowVersion { get; set; }
}

Lors de l’update :

UPDATE Users SET Money = 50 WHERE Id = 1 AND RowVersion = [AncienneValeur]

Si quelqu’un d’autre a modifié la ligne entre temps, le RowVersion a changé. La clause WHERE ne trouve aucune ligne, et EF Core lance une DbUpdateConcurrencyException.

Il suffit d’attraper cette exception et de dire à l’utilisateur : “Oups, réessayez svp”.