Dealing with Concurrency with Locks
Warning: You are browsing the documentation for Symfony 6.2, which is no longer maintained.
Read the updated version of this page for Symfony 7.1 (the current stable version).
Dealing with Concurrency with Locks
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 Lock component 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:
1 2 3 4 5 6 7 8 9
$lock = $lockFactory->createLock('pdf-creation');
if (!$lock->acquire()) {
return;
}
// critical section of code
$service->method();
$lock->release();
Installing
In applications using Symfony Flex, run this command to install the Lock component:
1
$ composer require symfony/lock
Configuring
By default, Symfony provides a Semaphore
when available, or a Flock otherwise. You can configure
this behavior by using the lock
key like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
# 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'
6.1
The CSV support (e.g. zookeeper://localhost01,localhost02:2181
) in
ZookeeperStore DSN was introduced in Symfony 6.1.
Locking a Resource
To lock the default resource, autowire the lock factory using LockFactory:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
// 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.
Locking a Dynamic Resource
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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
// 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();
// ...
}
}
Naming Locks
If the application needs different kind of Stores alongside each other, Symfony provides named lock:
1 2 3 4 5
# config/packages/lock.yaml
framework:
lock:
invoice: ['semaphore', 'redis://r2.docker']
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
LockFactory:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// 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
{
// ...
}
}