DĂ©couvrez les secrets du verrou pessimiste avec Laravel đź”’
DĂ©couvrez les secrets du verrou pessimiste avec Laravel đź”’
Dans cet article, nous allons voir comment Ă©viter certaines catastrophes lors de transactions dans une application bancaire Laravel.
Si vous ne connaissez pas encore la méthode lockForUpdate
(ou sharedLock
) alors ce qui va suivre risque de vous sauver la vie !
Cette connaissance est crucial lorsqu'on manipule des données sensibles, comme les soldes bancaires, pour éviter les erreurs ou duplications dans des situations de forte concurrence.
Problème : Les conflits de transactions simultanées
Pour illustrer cette méthode, nous allons prendre comme exemple une application bancaire tout au long de cet article.
Chaque utilisateur peut transférer des fonds à d'autres bénéficiaires. Ce transfert est géré par une classe BankService
, qui effectue le débit du solde de l'expéditeur et le crédit du bénéficiaire dans une transaction SQL :
1<?php 2 3namespace App\Services; 4 5class BankService 6{ 7 public static function transferFunds(int $from, int $to, float $amount) 8 { 9 return DB::transaction(function () use ($from, $to, $amount) {10 $from = User::query()->findOrFail($from);11 $to = User::query()->findOrFail($to);12 13 if ($from->balance < $amount) {14 return false;15 }16 17 $from->update(['balance' => $from->balance - $amount]);18 $to->update(['balance' => $to->balance + $amount]);19 20 return true;21 });22 }23}
Cependant, si plusieurs transactions sont lancées en même temps (comme dans le cas de transferts simultanés vers plusieurs bénéficiaires), cela peut entraîner des anomalies :
- Les vérifications de fonds se font presque simultanément pour chaque transaction.
- Le solde disponible peut être incorrectement partagé, car chaque transaction est exécutée sans attendre que la précédente se termine.
Conséquence : Sans une gestion stricte, des montants peuvent être débités en double, créant un effet de duplication de l’argent.
Solution : Utiliser le lockForUpdate pour bloquer les enregistrements
La méthode lockForUpdate
est un type de verrou pessimiste qui permet de s'assurer qu'un enregistrement reste inchangé durant toute la transaction SQL. En clair, si un autre processus tente d’accéder à cet enregistrement, il attendra la fin de la première transaction avant de poursuivre.
Qu'est-ce qu'un verrou pessimiste ?
Un verrou pessimiste (ou pessimistic lock) consiste Ă verrouiller une ressource (dans notre exemple, une ligne de la table users
) de manière exclusive dès qu'une transaction y accède, empêchant les autres transactions d'effectuer des modifications ou même des lectures jusqu'à ce que le verrou soit libéré.
Fonctionnement de lockForUpdate
Lorsqu'une transaction utilise lockForUpdate
, un verrou exclusif est placé sur les lignes sélectionnées. Ce verrou garantit qu'aucune autre transaction ne peut modifier ou lire ces lignes tant que la transaction initiale n'est pas terminée.
Si une autre transaction tente de modifier ou de verrouiller les mêmes lignes (récupération des users nécessaires lors du second transfert financier), elle sera mise en attente jusqu'à la fin de la première transaction. Cela évite des problèmes tels que les conditions de concurrence, où plusieurs transactions pourraient lire et modifier les mêmes données de manière incohérente.
Le verrou est ensuite automatiquement libéré à la fin de la transaction, soit par un commit (validation des modifications) soit par un rollback (annulation des modifications).
Étape 1 : Configurer lockForUpdate notre service
Pour garantir que chaque transfert s’effectue correctement, modifions la méthode transferFunds
dans BankService
en ajoutant le verrou :
1<?php 2 3namespace App\Services; 4 5class BankService 6{ 7 public static function transferFunds(int $from, int $to, float $amount) 8 { 9 return DB::transaction(function () use ($from, $to, $amount) {10 $from = User::query()->lockForUpdate()->findOrFail($from); 11 $to = User::query()->lockForUpdate()->findOrFail($to);12 13 if ($from->balance < $amount) {14 return false;15 }16 17 $from->update(['balance' => $from->balance - $amount]);18 $to->update(['balance' => $to->balance + $amount]);19 20 return true;21 });22 }23}
Étape 2 : Tester le comportement
Ici, pour simuler un fort traffic, nous allons effectivement lancer nos tâches en même temps via la Façade Concurrency.
Aussi, assurez-vous que seul un transfert réussit lorsque le montant n'est pas suffisant pour plusieurs.
Voici Ă quoi ressemblerait le code dans le ContrĂ´leur :
1<?php 2 3namespace App\Http\Controllers; 4 5class BankController extends Controller 6{ 7 // ... 8 9 public function transferFunds(Request $request) {10 $amount = $request->amount;11 $from = auth()->id();12 13 /**14 * @var array<Closure(): bool> $tasks15 */16 $tasks = [];17 18 $request->collect('beneficiary')->each(function ($id) use ($from, $amount, &$tasks) { 19 $tasks[] = fn() => BankService::transferFunds($from, $id, $amount);20 });21 22 $response = Concurrency::run($tasks);23 }24}
VĂ©rification des RĂ©sultats
Pour chaque transfert, nous attendons que la première transaction se termine avant que la suivante ne commence, empêchant ainsi le système de « dupliquer » des fonds lorsque le solde devient insuffisant.
Tutoriel vidéo
Si vous préférez les contenus vidéos pratiques, vous pouvez visionner le tutoriel publié sur ma chaîne LaravelJutsu :
Conclusion
L’utilisation de lockForUpdate
est essentielle dans des contextes de transactions concurrentes où l’intégrité des données doit être garantie, comme pour des transferts bancaires. Ce verrou pessimiste assure que seules les transactions valides sont exécutées, évitant les anomalies dues aux mises à jour simultanées. Pour des applications avec un fort trafic, ce type de verrou est indispensable.
A lire
Autres articles de la même catégorie
Les bases du CQRS en Laravel
DĂ©couvrir les bases de l'architecture CQRS en Laravel.
Mathieu De Gracia
Piloter son application avec Forge
De l’installation du serveur au déploiement automatique, découvrons ensemble l’outil Forge.
William Suppo
Tour d'horizon des façades
Présentation du fonctionnement des façades, leurs avantages et inconvénients. Pourquoi faut-il les considérer dans un écosystème Laravel ?
Marc COLLET