You are browsing the Symfony 4 documentation, which changes significantly from Symfony 3.x. If your app doesn't use Symfony 4 yet, browse the Symfony 3.4 documentation.

The Lock Component

4.0 version

The Lock Component

The Lock Component creates and manages locks, a mechanism to provide exclusive access to a shared resource.

Installation

You can install the component in 2 different ways:

Then, require the vendor/autoload.php file to enable the autoloading mechanism provided by Composer. Otherwise, your application won't be able to find the classes of this Symfony component.

Usage

Locks are used to guarantee exclusive access to some shared resource. In Symfony applications, you can use locks for example to ensure that a command is not executed more than once at the same time (on the same or different servers).

In order to manage the state of locks, a Store needs to be created first and then use the Factory class to actually create the lock for some resource:

1
2
3
4
5
use Symfony\Component\Lock\Factory;
use Symfony\Component\Lock\Store\SemaphoreStore;

$store = new SemaphoreStore();
$factory = new Factory($store);

Then, a call to the acquire() method will try to acquire the lock. Its first argument is an arbitrary string that represents the locked resource:

1
2
3
4
5
6
7
8
9
// ...
$lock = $factory->createLock('pdf-invoice-generation');

if ($lock->acquire()) {
    // The resource "pdf-invoice-generation" is locked.
    // You can compute and generate invoice safely here.

    $lock->release();
}

If the lock can not be acquired, the method returns false. The acquire() method can be safely called repeatedly, even if the lock is already acquired.

Note

Unlike other implementations, the Lock Component distinguishes locks instances even when they are created for the same resource. If a lock has to be used by several services, they should share the same Lock instance returned by the Factory::createLock method.

Tip

If you don't release the lock explicitly, it will be released automatically on instance destruction. In some cases, it can be useful to lock a resource across several requests. To disable the automatic release behavior, set the third argument of the createLock() method to false.

Blocking Locks

By default, when a lock cannot be acquired, the acquire method returns false immediately. To wait (indefinitely) until the lock can be created, pass true as the argument of the acquire() method. This is called a blocking lock because the execution of your application stops until the lock is acquired.

Some of the built-in Store classes support this feature. When they don't, they can be decorated with the RetryTillSaveStore class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use Symfony\Component\Lock\Factory;
use Symfony\Component\Lock\Store\RedisStore;
use Symfony\Component\Lock\Store\RetryTillSaveStore;

$store = new RedisStore(new \Predis\Client('tcp://localhost:6379'));
$store = new RetryTillSaveStore($store);
$factory = new Factory($store);

$lock = $factory->createLock('notification-flush');
$lock->acquire(true);

Expiring Locks

Locks created remotely are difficult to manage because there is no way for the remote Store to know if the locker process is still alive. Due to bugs, fatal errors or segmentation faults, it cannot be guaranteed that release() method will be called, which would cause the resource to be locked infinitely.

The best solution in those cases is to create expiring locks, which are released automatically after some amount of time has passed (called TTL for Time To Live). This time, in seconds, is configured as the second argument of the createLock() method. If needed, these locks can also be released early with the release() method.

The trickiest part when working with expiring locks is choosing the right TTL. If it's too short, other processes could acquire the lock before finishing the job; it it's too long and the process crashes before calling the release() method, the resource will stay locked until the timeout:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// ...
// create an expiring lock that lasts 30 seconds
$lock = $factory->createLock('charts-generation', 30);

$lock->acquire();
try {
    // perform a job during less than 30 seconds
} finally {
    $lock->release();
}

Tip

To avoid letting the lock in a locking state, it's recommended to wrap the job in a try/catch/finally block to always try to release the expiring lock.

In case of long-running tasks, it's better to start with a not too long TTL and then use the refresh() method to reset the TTL to its original value:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// ...
$lock = $factory->createLock('charts-generation', 30);

$lock->acquire();
try {
    while (!$finished) {
        // perform a small part of the job.

        // renew the lock for 30 more seconds.
        $lock->refresh();
    }
} finally {
    $lock->release();
}

Available Stores

Locks are created and managed in Stores, which are classes that implement StoreInterface. The component includes the following built-in store types:

Store Scope Blocking Expiring
FlockStore local yes no
MemcachedStore remote no yes
RedisStore remote no yes
SemaphoreStore local yes no

FlockStore

The FlockStore uses the file system on the local computer to create the locks. It does not support expiration, but the lock is automatically released when the PHP process is terminated:

use Symfony\Component\Lock\Store\FlockStore;

// the argument is the path of the directory where the locks are created
$store = new FlockStore(sys_get_temp_dir());

Caution

Beware that some file systems (such as some types of NFS) do not support locking. In those cases, it's better to use a directory on a local disk drive or a remote store based on Redis or Memcached.

MemcachedStore

The MemcachedStore saves locks on a Memcached server, it requires a Memcached connection implementing the \Memcached class. This store does not support blocking, and expects a TTL to avoid stalled locks:

1
2
3
4
5
6
use Symfony\Component\Lock\Store\MemcachedStore;

$memcached = new \Memcached();
$memcached->addServer('localhost', 11211);

$store = new MemcachedStore($memcached);

Note

Memcached does not support TTL lower than 1 second.

RedisStore

The RedisStore saves locks on a Redis server, it requires a Redis connection implementing the \Redis, \RedisArray, \RedisCluster or \Predis classes. This store does not support blocking, and expects a TTL to avoid stalled locks:

1
2
3
4
5
6
use Symfony\Component\Lock\Store\RedisStore;

$redis = new \Redis();
$redis->connect('localhost');

$store = new RedisStore($redis);

SemaphoreStore

The SemaphoreStore uses the PHP semaphore functions to create the locks:

use Symfony\Component\Lock\Store\SemaphoreStore;

$store = new SemaphoreStore();

CombinedStore

The CombinedStore is designed for High Availability applications because it manages several stores in sync (for example, several Redis servers). When a lock is being acquired, it forwards the call to all the managed stores, and it collects their responses. If a simple majority of stores have acquired the lock, then the lock is considered as acquired; otherwise as not acquired:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use Symfony\Component\Lock\Strategy\ConsensusStrategy;
use Symfony\Component\Lock\Store\CombinedStore;
use Symfony\Component\Lock\Store\RedisStore;

$stores = [];
foreach (array('server1', 'server2', 'server3') as $server) {
    $redis= new \Redis();
    $redis->connect($server);

    $stores[] = new RedisStore($redis);
}

$store = new CombinedStore($stores, new ConsensusStrategy());

Instead of the simple majority strategy (ConsensusStrategy) an UnanimousStrategy can be used to require the lock to be acquired in all the stores.

This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.