Skip to content

Dealing with Concurrency with Locks

Warning: You are browsing the documentation for Symfony 5.x, which is no longer maintained.

Read the updated version of this page for Symfony 7.2 (the current stable version).

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
# 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: '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'

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
24
// src/Controller/PdfController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Lock\LockFactory;

class PdfController extends AbstractController
{
    /**
     * @Route("/download/terms-of-use.pdf")
     */
    public function downloadPdf(LockFactory $factory, MyPdfGeneratorService $pdf)
    {
        $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
24
// src/Controller/PdfController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Lock\LockFactory;

class PdfController extends AbstractController
{
    /**
     * @Route("/download/{version}/terms-of-use.pdf")
     */
    public function downloadPdf($version, LockFactory $lockFactory, MyPdfGeneratorService $pdf)
    {
        $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
// src/Controller/PdfController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Lock\LockFactory;

class PdfController extends AbstractController
{
    #[Route('/download/terms-of-use.pdf')]
    public function downloadPdf(LockFactory $invoiceLockFactory, MyPdfGeneratorService $pdf)
    {
        // ...
    }
}

Blocking Store

If you want to use the RetryTillSaveStore for non-blocking locks, you can do it by decorating the store service:

1
2
3
4
lock.default.retry_till_save.store:
    class: Symfony\Component\Lock\Store\RetryTillSaveStore
    decorates: lock.default.store
    arguments: ['@.inner', 100, 50]
This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.
TOC
    Version