Transactions dans le code
AvancéUne transaction doit être courte, cohérente et réessayable.
await using var conn = await db.OpenConnectionAsync(ct);
await using var tx = await conn.BeginTransactionAsync(ct);
await using (var debit = new NpgsqlCommand(
"UPDATE comptes SET solde = solde - $1 WHERE id = $2 AND solde >= $1", conn, tx))
{
debit.Parameters.AddWithValue(montant);
debit.Parameters.AddWithValue(source);
if (await debit.ExecuteNonQueryAsync(ct) == 0) throw new SoldeInsuffisantException();
}
await using (var credit = new NpgsqlCommand(
"UPDATE comptes SET solde = solde + $1 WHERE id = $2", conn, tx))
{
credit.Parameters.AddWithValue(montant);
credit.Parameters.AddWithValue(destination);
await credit.ExecuteNonQueryAsync(ct);
}
await tx.CommitAsync(ct);
Règles à retenir#
- Aucun appel externe (HTTP, e-mail, file de messages) entre
BEGINetCOMMIT: la transaction retient des verrous pendant toute l'attente. - Vérifier le nombre de lignes affectées :
UPDATE ... WHERE solde >= $1échoue silencieusement (0 ligne) si la condition est fausse. - Réessayer l'ensemble de la transaction sur
40001(sérialisation) et40P01(deadlock), avec un nombre d'essais borné et un délai croissant. - Propager le
CancellationToken: une requête annulée doit annuler la requête SQL.
Concurrence optimiste#
Pour éviter les mises à jour perdues sans verrouiller, on utilise une colonne de version :
UPDATE produits
SET prix = $1, version = version + 1
WHERE id = $2 AND version = $3; -- 0 ligne affectée : quelqu'un d'autre a modifié entre-temps
EF Core ([ConcurrencyCheck], RowVersion) et la plupart des ORM offrent ce mécanisme.
Astuce : préférez les opérations atomiques en une instruction (
UPDATE ... SET stock = stock - 1 WHERE stock > 0) aux séquences lire-calculer-écrire dans le code : elles sont plus courtes, plus rapides et sans course.