When a program runs concurrently, some part of code which modify shared resources should not be accessed by multiple processes at the same time. Symfony's :doc:`Lock component </components/lock>` provides a locking mechanism to ensure that only one process is running the critical section of code at any point of time to prevent race conditions from happening.
The following example shows a typical usage of the lock:
$lock = $lockFactory->createLock('pdf-creation'); if (!$lock->acquire()) { return; } // critical section of code $service->method(); $lock->release();
In applications using :ref:`Symfony Flex <symfony-flex>`, run this command to install the Lock component:
$ composer require symfony/lock
By default, Symfony provides a :ref:`Semaphore <lock-store-semaphore>`
when available, or a :ref:`Flock <lock-store-flock>` otherwise. You can configure
this behavior by using the lock
key like:
.. configuration-block:: .. code-block:: yaml # config/packages/lock.yaml framework: lock: ~ lock: 'flock' lock: 'flock:///path/to/file' lock: 'semaphore' lock: 'memcached://m1.docker' lock: ['memcached://m1.docker', 'memcached://m2.docker'] lock: 'redis://r1.docker' lock: ['redis://r1.docker', 'redis://r2.docker'] lock: 'rediss://r1.docker?ssl[verify_peer]=1&ssl[cafile]=...' lock: 'zookeeper://z1.docker' lock: 'zookeeper://z1.docker,z2.docker' lock: 'zookeeper://localhost01,localhost02:2181' lock: 'sqlite:///%kernel.project_dir%/var/lock.db' lock: 'mysql:host=127.0.0.1;dbname=app' lock: 'pgsql:host=127.0.0.1;dbname=app' lock: 'pgsql+advisory:host=127.0.0.1;dbname=app' lock: 'sqlsrv:server=127.0.0.1;Database=app' lock: 'oci:host=127.0.0.1;dbname=app' lock: 'mongodb://127.0.0.1/app?collection=lock' lock: '%env(LOCK_DSN)%' # named locks lock: invoice: ['semaphore', 'redis://r2.docker'] report: 'semaphore' .. code-block:: xml <!-- config/packages/lock.xml --> <?xml version="1.0" encoding="UTF-8" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:framework="http://symfony.com/schema/dic/symfony" xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> <framework:config> <framework:lock> <framework:resource>flock</framework:resource> <framework:resource>flock:///path/to/file</framework:resource> <framework:resource>semaphore</framework:resource> <framework:resource>memcached://m1.docker</framework:resource> <framework:resource>memcached://m1.docker</framework:resource> <framework:resource>memcached://m2.docker</framework:resource> <framework:resource>redis://r1.docker</framework:resource> <framework:resource>redis://r1.docker</framework:resource> <framework:resource>redis://r2.docker</framework:resource> <framework:resource>zookeeper://z1.docker</framework:resource> <framework:resource>zookeeper://z1.docker,z2.docker</framework:resource> <framework:resource>zookeeper://localhost01,localhost02:2181</framework:resource> <framework:resource>sqlite:///%kernel.project_dir%/var/lock.db</framework:resource> <framework:resource>mysql:host=127.0.0.1;dbname=app</framework:resource> <framework:resource>pgsql:host=127.0.0.1;dbname=app</framework:resource> <framework:resource>pgsql+advisory:host=127.0.0.1;dbname=app</framework:resource> <framework:resource>sqlsrv:server=127.0.0.1;Database=app</framework:resource> <framework:resource>oci:host=127.0.0.1;dbname=app</framework:resource> <framework:resource>mongodb://127.0.0.1/app?collection=lock</framework:resource> <framework:resource>%env(LOCK_DSN)%</framework:resource> <!-- named locks --> <framework:resource name="invoice">semaphore</framework:resource> <framework:resource name="invoice">redis://r2.docker</framework:resource> <framework:resource name="report">semaphore</framework:resource> </framework:lock> </framework:config> </container> .. code-block:: php // config/packages/lock.php use Symfony\Config\FrameworkConfig; return static function (FrameworkConfig $framework): void { $framework->lock() ->resource('default', ['flock']) ->resource('default', ['flock:///path/to/file']) ->resource('default', ['semaphore']) ->resource('default', ['memcached://m1.docker']) ->resource('default', ['memcached://m1.docker', 'memcached://m2.docker']) ->resource('default', ['redis://r1.docker']) ->resource('default', ['redis://r1.docker', 'redis://r2.docker']) ->resource('default', ['zookeeper://z1.docker']) ->resource('default', ['zookeeper://z1.docker,z2.docker']) ->resource('default', ['zookeeper://localhost01,localhost02:2181']) ->resource('default', ['sqlite:///%kernel.project_dir%/var/lock.db']) ->resource('default', ['mysql:host=127.0.0.1;dbname=app']) ->resource('default', ['pgsql:host=127.0.0.1;dbname=app']) ->resource('default', ['pgsql+advisory:host=127.0.0.1;dbname=app']) ->resource('default', ['sqlsrv:server=127.0.0.1;Database=app']) ->resource('default', ['oci:host=127.0.0.1;dbname=app']) ->resource('default', ['mongodb://127.0.0.1/app?collection=lock']) ->resource('default', [env('LOCK_DSN')]) // named locks ->resource('invoice', ['semaphore', 'redis://r2.docker']) ->resource('report', ['semaphore']) ; };
To lock the default resource, autowire the lock factory using :class:`Symfony\\Component\\Lock\\LockFactory`:
// src/Controller/PdfController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Lock\LockFactory; class PdfController extends AbstractController { #[Route('/download/terms-of-use.pdf')] public function downloadPdf(LockFactory $factory, MyPdfGeneratorService $pdf): Response { $lock = $factory->createLock('pdf-creation'); $lock->acquire(true); // heavy computation $myPdf = $pdf->getOrCreatePdf(); $lock->release(); // ... } }
Caution!
The same instance of LockInterface
won't block when calling acquire
multiple times inside the same process. When several services use the
same lock, inject the LockFactory
instead to create a separate lock
instance for each service.
Sometimes the application is able to cut the resource into small pieces in order
to lock a small subset of processes and let others through. The previous example
showed how to lock the $pdf->getOrCreatePdf()
call for everybody,
now let's see how to lock a $pdf->getOrCreatePdf($version)
call only for
processes asking for the same $version
:
// src/Controller/PdfController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Lock\LockFactory; class PdfController extends AbstractController { #[Route('/download/{version}/terms-of-use.pdf')] public function downloadPdf($version, LockFactory $lockFactory, MyPdfGeneratorService $pdf): Response { $lock = $lockFactory->createLock('pdf-creation-'.$version); $lock->acquire(true); // heavy computation $myPdf = $pdf->getOrCreatePdf($version); $lock->release(); // ... } }
If the application needs different kind of Stores alongside each other, Symfony provides :ref:`named lock <reference-lock-resources-name>`:
.. configuration-block:: .. code-block:: yaml # config/packages/lock.yaml framework: lock: invoice: ['semaphore', 'redis://r2.docker'] report: 'semaphore' .. code-block:: xml <!-- config/packages/lock.xml --> <?xml version="1.0" encoding="UTF-8" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:framework="http://symfony.com/schema/dic/symfony" xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> <framework:config> <framework:lock> <framework:resource name="invoice">semaphore</framework:resource> <framework:resource name="invoice">redis://r2.docker</framework:resource> <framework:resource name="report">semaphore</framework:resource> </framework:lock> </framework:config> </container> .. code-block:: php // config/packages/lock.php use Symfony\Config\FrameworkConfig; return static function (FrameworkConfig $framework): void { $framework->lock() ->resource('invoice', ['semaphore', 'redis://r2.docker']) ->resource('report', ['semaphore']); ; };
An autowiring alias is created for each named lock with a name using the camel
case version of its name suffixed by LockFactory
.
For instance, the invoice
lock can be injected by naming the argument
$invoiceLockFactory
and type-hinting it with
:class:`Symfony\\Component\\Lock\\LockFactory`:
// src/Controller/PdfController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Lock\LockFactory; class PdfController extends AbstractController { #[Route('/download/terms-of-use.pdf')] public function downloadPdf(LockFactory $invoiceLockFactory, MyPdfGeneratorService $pdf): Response { // ... } }