← Tous les cours / Symfony Lock Cours
00 — LE VERROU

Le verrou et le problème de la ressource partagée

Sans verrou, deux processus peuvent modifier la même ressource en même temps — le Lock est la garantie d'exclusivité.

Imagine un stock de billets de concert. Deux acheteurs cliquent "Acheter" au même moment. Sans protection, chacun lit stock = 1, chacun décrémente, chacun écrit stock = 0 — mais deux billets ont été vendus pour le même siège. C'est la race condition classique.

Race condition
Quand le résultat d'une opération dépend de l'ordre d'exécution non contrôlé de plusieurs processus ou threads concurrents. Le comportement devient imprévisible.

Un verrou résout ce problème en imposant un accès séquentiel à la ressource critique : seul le détenteur du lock peut agir, les autres attendent ou échouent proprement.

Mutex (MUTual EXclusion)
Un verrou d'exclusion mutuelle garantit qu'une seule unité d'exécution détient l'accès à une ressource à la fois. Symfony Lock est l'implémentation Symfony de ce principe.

Le cycle de vie d'un verrou

Processus A
acquire()
Ressource
verrouillée
Processus B
attend ou échoue

Le verrou nomme la ressource — c'est une chaîne comme "invoice_42" ou "export_users". Deux processus qui utilisent le même nom se coordonnent ; deux noms différents sont indépendants.

💡 Le nom est le contrat : LockFactory::createLock('order-42') sur deux workers différents produit deux verrous qui se coordonnent — parce qu'ils partagent le même store ET le même nom.

Essaie par toi-même

Le simulateur ci-dessous montre ce qui se passe sans et avec verrou quand deux processus accèdent à la même ressource en même temps. Clique sur les deux boutons pour comparer.

⚠️ Symfony Lock ne remplace pas les transactions DB : pour des incréments atomiques en base, une transaction avec SELECT FOR UPDATE est plus adaptée. Lock brille pour les opérations longues, multi-étapes, ou cross-services.
01 — LOCKFACTORY

LockFactory et createLock

LockFactory est le point d'entrée — elle crée les locks à partir d'un store et d'un nom de ressource.

LockFactory est le seul point d'entrée pour créer des locks. Tu ne crées jamais une instance de Lock directement — tu passes par la factory, qui connaît le store sous-jacent.

LockFactory
Prend un PersistingStoreInterface à la construction et produit des objets SharedLockInterface via createLock().
use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Lock\Store\FlockStore;

// Création manuelle (hors Symfony DI)
$store   = new FlockStore('/tmp/locks');
$factory = new LockFactory($store);

// Création d'un lock nommé
$lock = $factory->createLock('export-users');

Signature de createLock

createLock(
    string  $resource,
    ?float  $ttl         = 300.0,  // secondes, null = pas de TTL
    bool    $autoRelease = true,   // libère à la destruction de l'objet
): SharedLockInterface
ParamètreDéfautRôle
$resourceNom de la ressource à verrouiller. Doit être unique par ressource métier.
$ttl300.0Durée de vie en secondes. null = pas d'expiration (stores sans TTL seulement).
$autoReleasetrueLe destructeur PHP appelle release() automatiquement si le lock est encore tenu.

createLockFromKey — contrôle avancé

Pour les cas où tu veux partager la même Key entre plusieurs locks (par exemple logger des métadonnées sur la clé), utilise createLockFromKey() :

use Symfony\Component\Lock\Key;

$key  = new Key('traitement-facture-42');
$lock = $factory->createLockFromKey($key, ttl: 60.0);
Key
Objet final qui encapsule le nom de ressource et les métadonnées d'état (TTL restant, états custom). C'est la Key qui est persistée dans le store — pas l'objet Lock.

Dans le contexte Symfony (injection)

Symfony déclare automatiquement LockFactory comme service si tu configures framework.lock. Tu l'injectes comme n'importe quel service :

use Symfony\Component\Lock\LockFactory;

final class ExportService
{
    public function __construct(
        private readonly LockFactory $lockFactory,
    ) {}

    public function export(): void
    {
        $lock = $this->lockFactory->createLock('export-users', ttl: 120.0);
        // ...
    }
}
💡 Un lock par opération : crée un nouveau lock à chaque appel — ne réutilise pas le même objet $lock entre deux exécutions. L'objet Lock est stateful (il sait s'il est acquis ou non).
02 — ACQUIRE / RELEASE

acquire, release et auto-release

acquire() tente d'obtenir le verrou, release() le libère — autoRelease=true le libère automatiquement à la destruction.

acquire() tente d'obtenir le verrou et retourne un bool. Si la ressource est déjà verrouillée, il retourne false immédiatement (mode non-bloquant). C'est à toi de décider quoi faire.

$lock = $factory->createLock('traitement-commande-42');

if (!$lock->acquire()) {
    // Quelqu'un d'autre traite déjà cette commande
    throw new \RuntimeException('Traitement déjà en cours.');
}

try {
    // Section critique — tu es le seul à l'exécuter
    $this->traiterCommande(42);
} finally {
    $lock->release();
}
⚠️ Toujours utiliser try/finally : si ton code lève une exception, le finally garantit que release() est appelé. Sans ça, le lock reste en place jusqu'à expiration du TTL.

auto-release — le filet de sécurité

Avec autoRelease = true (valeur par défaut), le destructeur PHP appelle release() si l'objet $lock est détruit sans avoir été libéré explicitement. C'est un filet — pas un substitut au try/finally.

Avec auto-release (défaut)
$lock = $factory->createLock(
    'ma-ressource',
    autoRelease: true
);
$lock->acquire();
// Si on oublie release(),
// le destructeur le fait
Sans auto-release
$lock = $factory->createLock(
    'ma-ressource',
    autoRelease: false
);
$lock->acquire();
// Lock persiste même après
// destruction de $lock
Quand désactiver auto-release ?
Si tu crées le lock dans un worker et que tu veux qu'il survive au-delà de l'objet PHP (exemple : lock inter-requêtes géré manuellement via Redis). C'est rare — garde autoRelease = true par défaut.

L'interface complète

interface LockInterface
{
    public function acquire(bool $blocking = false): bool;
    public function refresh(?float $ttl = null): void;
    public function isAcquired(): bool;
    public function release(): void;
    public function isExpired(): bool;
    public function getRemainingLifetime(): ?float; // null si pas de TTL
}
MéthodeRôle
acquire(false)Tente immédiatement, retourne false si échoue.
acquire(true)Bloque jusqu'à obtenir le lock. Voir section suivante.
isAcquired()Vrai si ce processus détient le lock.
release()Libère le lock.
isExpired()Vrai si le TTL est dépassé.
getRemainingLifetime()Secondes restantes, null si pas de TTL.
refresh()Renouvelle le TTL (voir section TTL).
💡 isAcquired() vs isExpired() : isAcquired() retourne true si tu détiens le lock — il peut être expiré côté store sans que l'objet PHP le sache. Appelle isExpired() avant une opération longue pour vérifier que le TTL n'a pas expiré.
03 — BLOCKING

Blocage — attendre la libération

acquire(true) bloque jusqu'à la libération — le mécanisme diffère selon que le store implémente BlockingStoreInterface ou pas.

acquire(true) passe en mode bloquant : le processus attend que le verrou soit disponible. C'est utile quand tu veux être sûr de traiter la ressource, pas juste tenter.

// Bloque jusqu'à obtenir le lock — peut attendre indéfiniment
$lock->acquire(true);

try {
    $this->traiter();
} finally {
    $lock->release();
}

Comment fonctionne le blocage ?

Symfony Lock adapte son comportement selon le store utilisé :

Store avec BlockingStoreInterface

Le store implémente waitAndSave(Key $key). Le blocage est natif : PostgreSQL advisory locks, FlockStore, SemaphoreStore. Efficace, pas de boucle d'attente.

acquire(true)
waitAndSave()
bloquant natif
Store sans BlockingStoreInterface

Lock boucle avec usleep() jusqu'à succès. L'intervalle est de ~100ms ±10ms (jitter). Redis, PDO, DoctrineDbal : tous passent par cette boucle.

acquire(true)
boucle usleep
~100ms
⚠️ acquire(true) avec Redis peut CPU-spiner : la boucle usleep(100ms) est légère, mais dans un contexte haute fréquence avec beaucoup de contention, préfère un store avec blocage natif (PostgreSQL, Flock) ou implémente une queue de travail.

Exceptions possibles

use Symfony\Component\Lock\Exception\LockConflictedException;
use Symfony\Component\Lock\Exception\LockAcquiringException;

try {
    $lock->acquire(true);
} catch (LockConflictedException $e) {
    // Lock tenu par quelqu'un d'autre (ne se produit pas en bloquant pur)
} catch (LockAcquiringException $e) {
    // Erreur lors de la tentative (store indisponible, etc.)
}

Essaie le comportement blocking

Clique sur "acquire(false)" quand le lock est tenu pour voir l'échec immédiat, puis sur "acquire(true)" pour voir l'attente. Utilise "Libérer" pour remettre à zéro.

État du lock : Libre
04 — TTL ET REFRESH

TTL et refresh

Un lock sans renouvellement expire — refresh() prolonge la durée pour éviter les dead locks.

Un verrou sans TTL est un verrou potentiellement immortel : si le processus meurt sans appeler release(), personne ne peut jamais acquérir la ressource. Le TTL est la garantie anti-deadlock.

TTL (Time-To-Live)
Durée de vie du verrou en secondes. Passé ce délai, le store supprime automatiquement l'entrée — même si le détenteur n'a pas appelé release(). Valeur par défaut : 300 secondes.

Le danger du TTL trop court

Si ton traitement prend plus de temps que le TTL, le lock expire pendant l'opération. Un autre processus peut alors acquérir le même verrou, et tu te retrouves avec deux exécutions concurrentes — exactement ce que tu voulais éviter.

Processus A
acquire() t=0
TTL expire
t=300s
Processus B
acquire() t=301s

Les deux processus s'exécutent en même temps. Race condition restaurée.

refresh() — renouveler pendant le traitement

refresh() remet le TTL à zéro depuis maintenant. Appelle-le régulièrement dans les traitements longs :

$lock = $factory->createLock('import-csv', ttl: 60.0);
$lock->acquire(true);

foreach ($this->readLines($csvPath) as $i => $line) {
    $this->processLine($line);

    // Renouvelle toutes les 100 lignes
    if ($i % 100 === 0) {
        $lock->refresh(); // Remet à 60s depuis maintenant
    }
}

$lock->release();

Tu peux aussi passer un TTL différent à refresh() :

$lock->refresh(120.0); // Prolonge à 120s depuis maintenant
💡 Pattern recommandé pour les traitements longs : TTL court (30–60s) + refresh() régulier. Ainsi, si le processus meurt, le lock expire vite. Si le processus vit, il renouvelle à intervalles réguliers.

Surveiller l'expiration

if ($lock->isExpired()) {
    // Le TTL a expiré — notre lock n'est peut-être plus valide
    throw new \RuntimeException('Lock expiré pendant le traitement.');
}

$remaining = $lock->getRemainingLifetime(); // float ou null si pas de TTL
if ($remaining !== null && $remaining < 10.0) {
    $lock->refresh(); // Moins de 10s restantes — on renouvelle
}
⚠️ LockExpiredException : certaines opérations (comme refresh()) peuvent lever LockExpiredException si le TTL a déjà expiré côté store. Capte cette exception dans les boucles longues.

Stores sans TTL

FlockStore, SemaphoreStore, PostgreSqlStore n'ont pas de TTL (le lock est lié au processus ou à la connexion). Si tu passes ttl: null avec ces stores, le lock persiste tant que le processus tourne. C'est correct — mais ne pas oublier release() ou autoRelease.

05 — LOCKS PARTAGÉS

Locks partagés — lire à plusieurs, écrire seul

SharedLockInterface permet plusieurs lectures simultanées et une écriture exclusive — le pattern Reader/Writer.

Le pattern Reader/Writer permet de lire à plusieurs en même temps, mais d'écrire seul. C'est exactement ce que SharedLockInterface implémente via acquireRead().

Reader/Writer lock
Plusieurs lecteurs peuvent tenir le verrou simultanément. Un écrivain exige l'exclusivité : aucun autre lecteur ni écrivain ne peut tenir le lock pendant son écriture.
Lecture (partagée)
// Plusieurs processus peuvent
// acquérir simultanément
$lock->acquireRead();
try {
    $data = $this->lireFichier();
} finally {
    $lock->release();
}
Écriture (exclusive)
// Exige l'exclusivité totale —
// attend que tous les lecteurs
// aient relâché
$lock->acquire();
try {
    $this->ecrireFichier($data);
} finally {
    $lock->release();
}

SharedLockInterface

interface SharedLockInterface extends LockInterface
{
    public function acquireRead(bool $blocking = false): bool;
}

acquireRead() a la même signature que acquire() — il accepte un paramètre $blocking pour attendre la disponibilité.

Cas d'usage concret : cache de configuration

use Symfony\Component\Lock\LockFactory;

final class ConfigCache
{
    public function __construct(
        private readonly LockFactory $lockFactory
    ) {}

    public function get(string $key): mixed
    {
        // Lecture : partagée — plusieurs processus peuvent lire en même temps
        $lock = $this->lockFactory->createLock("config-{$key}");
        $lock->acquireRead(true);
        try {
            return $this->readFromCache($key);
        } finally {
            $lock->release();
        }
    }

    public function set(string $key, mixed $value): void
    {
        // Écriture : exclusive — personne ne lit pendant qu'on écrit
        $lock = $this->lockFactory->createLock("config-{$key}");
        $lock->acquire(true);
        try {
            $this->writeToCache($key, $value);
        } finally {
            $lock->release();
        }
    }
}

Quels stores supportent les locks partagés ?

Pas tous — seulement ceux qui implémentent SharedLockStoreInterface :

StoreLocks partagés
FlockStoreOui
RedisStoreOui
PostgreSqlStoreOui
InMemoryStoreOui
NullStoreOui
PdoStoreNon
DoctrineDbalStoreNon
SemaphoreStoreNon
MemcachedStoreNon
💡 LockFactory retourne toujours SharedLockInterface : même avec un store qui ne supporte pas les lectures partagées. Si tu appelles acquireRead() sur un store non compatible, Symfony lèvera une exception au runtime. Vérifie la compatibilité avant de choisir ton store.
06 — LES STORES

Les stores — choisir la bonne persistance

Chaque store a ses caractéristiques : TTL, blocking, shared locks, dépendances — le tableau de choix.

Le store est l'arrière-plan du système de lock : c'est lui qui persiste "qui tient quoi". Symfony propose une dizaine de stores, chacun avec ses compromis. Le bon choix dépend de ton infrastructure.

Tableau de synthèse

StoreTTLBlockingSharedDépendance
FlockStore Non Oui Oui Système de fichiers PHP stdlib
SemaphoreStore Non Oui Non ext-sysvsem
RedisStore Oui Non Oui Redis + scripts Lua
PdoStore Oui Non Non PDO, table lock_keys
DoctrineDbalStore Oui Non Non doctrine/dbal
MemcachedStore Oui Non Non ext-memcached
MongoDbStore Oui Non Non TTL index MongoDB
PostgreSqlStore Non Oui Oui Advisory locks pg_advisory_lock()
InMemoryStore Non Non Oui Tests uniquement, single-process
NullStore Non Oui Oui No-op — désactive le locking

Focus sur les stores courants

FlockStore — le plus simple
Utilise les verrous de fichiers POSIX (flock()). Fonctionne sur un seul serveur. Pas de TTL (le lock disparaît quand le process meurt). Idéal pour les workers mono-serveur.
$store = new FlockStore('/var/lock/monapp');
RedisStore — le standard distribué
Utilise des scripts Lua atomiques + sorted sets Redis. TTL natif. Supporte les locks partagés. Le choix par défaut pour les architectures multi-serveurs.
use Symfony\Component\Lock\Store\RedisStore;

$redis = new \Redis();
$redis->connect('redis', 6379);
$store = new RedisStore($redis);
PostgreSqlStore — blocking + shared sans Redis
Utilise pg_advisory_lock() / pg_advisory_unlock(). Bloquant natif, supporte les locks partagés. Excellent si tu as déjà PostgreSQL et pas Redis.
$store = new PostgreSqlStore(
    'pgsql:host=localhost;dbname=myapp',
    ['db_username' => 'app', 'db_password' => 'secret']
);
DoctrineDbalStore — si tu as déjà Doctrine
Utilise ta connexion DBAL existante. Crée une table lock_keys automatiquement (createTable()). Pas de blocking natif, pas de shared locks.
use Symfony\Component\Lock\Store\DoctrineDbalStore;

$store = new DoctrineDbalStore($connection);
$store->createTable(); // Crée lock_keys si inexistante
⚠️ InMemoryStore = single process uniquement : le lock n'existe qu'en mémoire PHP. Deux workers distincts ne se coordonnent pas. Utilise-le exclusivement en tests.
07 — COMBINEDSTORE

CombinedStore — verrous distribués

CombinedStore multiplexe sur plusieurs stores avec quorum — pour les architectures multi-nœuds.

Dans une architecture multi-nœuds sans coordination centrale fiable, un seul store peut être indisponible. CombinedStore multiplex le lock sur plusieurs stores en parallèle et applique une stratégie de quorum pour décider si le lock est acquis.

CombinedStore
Agrège plusieurs stores. L'acquisition réussit si suffisamment de stores confirment (selon la stratégie). Inspiré de l'algorithme Redlock de Redis.

Mise en place

use Symfony\Component\Lock\Store\CombinedStore;
use Symfony\Component\Lock\Store\RedisStore;
use Symfony\Component\Lock\Strategy\ConsensusStrategy;
use Symfony\Component\Lock\Strategy\UnanimousStrategy;

// Trois instances Redis indépendantes
$redis1 = new \Redis(); $redis1->connect('redis-1');
$redis2 = new \Redis(); $redis2->connect('redis-2');
$redis3 = new \Redis(); $redis3->connect('redis-3');

$store = new CombinedStore(
    [
        new RedisStore($redis1),
        new RedisStore($redis2),
        new RedisStore($redis3),
    ],
    new ConsensusStrategy() // ou UnanimousStrategy
);

$factory = new LockFactory($store);

Les deux stratégies

UnanimousStrategy

100 % des stores doivent confirmer l'acquisition. Très strict — si un store est indisponible, le lock échoue. À utiliser quand la cohérence absolue prime sur la disponibilité.

new UnanimousStrategy()
// 3/3 stores : OK
// 2/3 stores : FAIL
ConsensusStrategy

Plus de 50 % des stores doivent confirmer. Tolérant aux pannes — un store en panne n'empêche pas le lock. C'est l'équivalent de l'algorithme Redlock.

new ConsensusStrategy()
// 3/3 stores : OK
// 2/3 stores : OK
// 1/3 stores : FAIL
acquire()
Redis-1
+
Redis-2
+
Redis-3
✗ down
Consensus
2/3 ✓

Comportement en cas d'échec partiel

Si CombinedStore arrive à acquérir sur certains stores mais pas assez pour le quorum, il libère automatiquement les stores qui avaient confirmé. Pas de lock orphelin.

⚠️ CombinedStore ne supporte pas le blocking : il n'implémente pas BlockingStoreInterface. acquire(true) utilisera la boucle usleep() — acceptable pour des locks peu fréquents.

Différence avec un cluster Redis

ApprocheCohérenceDisponibilitéComplexité
Redis cluster (1 store) Moyenne Haute Faible
CombinedStore (3 Redis) Haute Haute Moyenne
UnanimousStrategy (3 Redis) Très haute Faible Moyenne
💡 Dans la plupart des cas, un seul RedisStore suffit : CombinedStore est utile uniquement si tu as des exigences de haute disponibilité strictes sur le système de lock lui-même. N'over-engineere pas.
08 — CONFIGURATION

Configuration et injection Symfony

lock.yaml, StoreFactory, injection de LockFactory par resource name.

Symfony gère la configuration des stores via le fichier config/packages/lock.yaml et expose automatiquement les LockFactory comme services injectables.

Configuration minimale

# config/packages/lock.yaml
framework:
    lock: 'flock://%kernel.project_dir%/var/lock'

Un seul store, une seule factory. Symfony déclare le service LockFactory — tu l'injectes par type.

Plusieurs stores nommés

# config/packages/lock.yaml
framework:
    lock:
        default_resource: 'default'
        resources:
            default:  'flock://%kernel.project_dir%/var/lock'
            payment:  'redis://redis:6379'
            invoice:  'pgsql+advisory://user:pass@localhost/mydb'

Chaque ressource nommée crée une LockFactory dédiée. L'injection utilise le nom de l'argument pour résoudre la bonne factory :

use Symfony\Component\Lock\LockFactory;

final class PaymentService
{
    public function __construct(
        // Injecte la factory "payment" grâce au nom de l'argument
        private readonly LockFactory $paymentLockFactory,
    ) {}
}

final class InvoiceService
{
    public function __construct(
        // Injecte la factory "invoice"
        private readonly LockFactory $invoiceLockFactory,
    ) {}
}

StoreFactory — DSN vers store

StoreFactory::createStore() transforme un DSN en store. C'est ce que Symfony utilise en interne pour interpréter le YAML. Tu peux l'utiliser directement dans du code maison :

use Symfony\Component\Lock\Store\StoreFactory;

$store = StoreFactory::createStore('redis://redis:6379');
$store = StoreFactory::createStore('flock:///var/lock');
$store = StoreFactory::createStore('pgsql+advisory://localhost/mydb');
$store = StoreFactory::createStore('semaphore');
$store = StoreFactory::createStore('in-memory'); // Tests

DSN disponibles

DSNStore
flock://cheminFlockStore
semaphoreSemaphoreStore
redis://host:portRedisStore
rediss://host:portRedisStore (TLS)
pgsql+advisory://...PostgreSqlStore
mysql://... / pgsql://...PdoStore
in-memoryInMemoryStore (tests)
nullNullStore

Configuration par environnement

# config/packages/lock.yaml
framework:
    lock: '%env(LOCK_DSN)%'
# .env
LOCK_DSN=flock://%kernel.project_dir%/var/lock

# .env.prod
LOCK_DSN=redis://redis:6379
💡 Utilise une variable d'environnement : le store en développement peut être FlockStore (simple, pas de dépendance), et Redis en production. Une seule ligne à changer.
⚠️ Installation du composant : composer require symfony/lock. Si tu utilises Symfony Flex, le fichier config/packages/lock.yaml est créé automatiquement avec une configuration FlockStore par défaut.
09 — SYNTHÈSE

Synthèse — guide de choix

Quel store, quel TTL, blocking ou non — les patterns de production.

Symfony Lock est un outil ciblé : il résout les problèmes de concurrence inter-processus pour des opérations métier à durée non négligeable. Voici comment choisir et utiliser le bon outil.

Quel store choisir ?

ContexteStore recommandéRaison
Serveur unique, dev ou prod simple FlockStore Zéro dépendance, blocking natif, shared locks
Multi-serveurs, Redis disponible RedisStore TTL, shared locks, distribué
PostgreSQL disponible, pas Redis PostgreSqlStore Blocking natif, shared locks, advisory locks
Doctrine/DBAL sans Redis ni PostgreSQL DoctrineDbalStore TTL, utilise la connexion DB existante
Haute disponibilité multi-nœuds CombinedStore Quorum sur plusieurs Redis
Tests unitaires InMemoryStore Isolation, single-process, pas de dépendance

Patterns de production

Pattern : opération longue avec refresh
Pour les imports, exports, traitements de queue : TTL court (30–60s) + refresh() régulier.
$lock = $factory->createLock('import-csv', ttl: 30.0);
$lock->acquire(true);
try {
    foreach ($lines as $i => $line) {
        $this->process($line);
        if ($i % 50 === 0) { $lock->refresh(); }
    }
} finally {
    $lock->release();
}
Pattern : idempotence dans un worker
Pour les workers de queue — s'assurer qu'un message n'est traité qu'une fois même si plusieurs workers tournent.
$lock = $factory->createLock("message-{$message->getId()}", ttl: 60.0);
if (!$lock->acquire()) {
    // Déjà en cours de traitement — ACK sans traiter
    return;
}
try {
    $this->handle($message);
} finally {
    $lock->release();
}
Pattern : Reader/Writer sur ressource partagée
Pour les caches de fichiers, rapports partagés : plusieurs lectures simultanées, une seule écriture.
// Lecture
$lock->acquireRead(true);
try { return $cache->get($key); }
finally { $lock->release(); }

// Écriture
$lock->acquire(true);
try { $cache->set($key, $value); }
finally { $lock->release(); }

Checklist avant de déployer

💡 À vérifier :
  • Le store est cohérent entre tous les workers (même Redis, même chemin Flock…)
  • TTL > durée maximale estimée du traitement
  • refresh() présent dans les boucles longues
  • try/finally autour de toute section critique
  • LockConflictedException et LockAcquiringException gérées
  • Tests avec InMemoryStore — pas de dépendances externes

Quand ne pas utiliser Symfony Lock

CasAlternative
Incrément atomique en DB SELECT FOR UPDATE / transaction DB
Rate limiting Symfony RateLimiter
Coordination de threads PHP-FPM intra-requête PHP n'est pas multi-thread — pas besoin
File de messages avec exactly-once delivery Messenger + idempotence clé DB unique