Handling Multiple Requests Seamlessly with Symfony Lock

Jakub Skowron (skowron.dev)
4 min readAug 31, 2023

--

Photo by Bruno Aguirre on Unsplash

Concurrency in programming is not just a challenge but also an art. Imagine being a developer in an e-commerce company handling thousands of orders daily. Everything goes smoothly until one day you notice that when multiple customers try to place orders simultaneously, the system starts behaving erratically. That’s when you begin to appreciate the beauty of locking mechanisms, such as Symfony Lock.

The Lock Component in Symfony

When starting with Symfony, one of the components that might catch our attention is Lock. Introduced in Symfony 3.4, it has become an integral part of many developers. With it, we can create locks that ensure that only one process at a time has access to critical code sections.

Basic example:

$lock = lockFactory->createLock('pdf-creation');

if (!acquire()) {
return;
}

service->method();
release();

Installation and Configuration

To start using the Lock component in Symfony, you first need to install it using Composer:

composer require symfony/lock

After installation, you can configure the component in the config/packages/lock.yaml file. You can choose the store where locks will be stored and other store-specific options.

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

Serializing Locks

Serializing locks allows storing the lock’s state and later recreating it. This way, we can start work in one process and then continue in another, using the same lock.

$key = $lock->getKey();
$serializedKey = serialize($key);
$unserializedKey = unserialize($serializedKey);
$lock->setKey($unserializedKey);


class ProductTaxonomy {
public function __construct(private object $product, private Key $key) {}
public function getProduct(): object { return $this->product; }
public function getKey(): Key { return $this->key; }
}

Blocking Locks

Sometimes we want our code to wait until it can acquire a lock. In Symfony, we can achieve this using blocking locks.

$lock = $factory->createLock('order-12345');

if (!$lock->acquire(true)) {
throw new \RuntimeException('Cannot acquire the lock!');
}

$lock->release();

Expiring Locks

Locks are not eternal. In certain situations, we want a lock to expire after some time. Symfony allows setting a lock’s lifespan.

$lock = $factory->createLock('order-12345', 30); // 30 seconds TTL

if ($lock->acquire()) {
// Processing the order...
$lock->release();
}

Automatically Releasing the Lock

One of the biggest challenges when working with locks is remembering to release them. Symfony has a solution for this — locks are automatically released when the Lock object is destroyed.

Shared Locks

Not all locks have to be exclusive. Sometimes we want to allow multiple processes to read data but only one to write them. In Symfony, we can achieve this using shared locks.

$lock = $factory->createLock('product-56789');

if ($lock->acquireRead()) {
// Reading product information...
}

$lock->release();

The Owner of the Lock

When we acquire a lock, we become its “owner”. Symfony allows checking if a given process is the owner of the lock.

if ($lock->isAcquired()) {
// Processing the order...
}

Semaphore vs Flock:

  • Semaphore: This is a locking mechanism based on system semaphores. It's fast and efficient but only works on systems that support IPC semaphores.
  • Flock: This is a locking mechanism based on file locks. It's more universal than semaphores but might be less efficient in some situations.

Example of using Flock:

$store = new FlockStore('/path/to/dir');
$factory = new LockFactory($store);
$lock = $factory->createLock('product-56789');

if ($lock->acquire()) {
// Updating product information...
$lock->release();
}

Quick question: is this story of any value to you? Please support my work by leaving a clap as a token of appreciation. Thank you.

Available Stores

In Symfony, locks are stored in special stores. There are many different types of stores that we can use depending on our needs, from simple file stores to advanced database stores.

Locks are created and managed in stores (Stores), which are classes implementing the PersistingStoreInterface. The component includes the following built-in store types: FlockStore, MemcachedStore, MongoDbStore, PdoStore, DoctrineDbalStore, PostgreSqlStore, DoctrineDbalPostgreSqlStore, RedisStore, ZookeeperStore, and a special InMemoryStore used for storing locks in memory during the process, which can be useful for testing.

Conclusion

The Lock component in Symfony is a powerful tool that allows for effective management of concurrency in web applications. With it, we can write code that is not only more reliable but also easier to maintain. If you haven’t used this component yet, it’s worth giving it a shot.

--

--

Jakub Skowron (skowron.dev)

Poland based PHP/Python Web Backend dev. Love to work with Symfony and FastAPI frameworks. In spare time totally gearhead.