Sometimes, execution of some code in parallel can lead to troubles: this is known as race-conditions, where parallel execution of some specific operations leads to corruption and/or unexpected results.
In these cases, the solution is to use locking techniques to force sections of code to execute sequentially rather than in parallel.
Note: for those not familiar with locking, it is important to understand that the same locking algorithm should be implemented by each party that will be executing a critical section! Locking is useless if only one party implements it.
Here I’ll show two techniques in PHP (one for local locking, and one for cross-server locking).
In PHP you can use flock() or IPC semaphores to achieve this kind of serialization. Here is an example with flock():
$lockfile = 'foo.lock'; $lock = fopen($lockfile, 'a'); if ($lock === false) { throw new Exception(sprintf("Could not open lockfile '%s'", 'lock')); } /* Get exclusive lock: this will block until we get the lock */ $ret = flock($lock, LOCK_EX); if ($ret === false) { throw new Exception(sprintf("Could not get lock on lockfile '%s'.", $lockfile)); } /* * Critical section which should not be executed * in parallel by another process */ [...] /* End of critical section */ /* Release the lock */ $ret = flock($lock, LOCK_UN); if ($ret === false) { throw new Exception(sprintf("Could not release lock on lockfile '%s'.", $lockfile)); } fclose($lock);
This might not be the fastest locking mechanism, but it works. But it also only works when your PHP code runs on a single server, as the lock is done on a local file. IPC semaphores should be faster, but they also only work locally.
What if you need to lock PHP code running on different servers?
To achieve this kind of global locking, you’ll still need a shared object accessible by all the PHP servers.
The flock() technique is still possible if the lock file is located on an NFS mounted volume for example. Sometimes flock() over NFS can be problematic, but if everyone is using nfslock daemons, then you should be fine.
Anyway, your PHP application might already be using a shared database? and more precisely a Postgresql database? So here is another technique that rely on the « LOCK TABLE » command with Postgresql database to provide cross-server locking.
We’ll be using the « LOCK TABLE » command on a dedicated blank table. The « LOCK TABLE » command must be used in a transaction, and once the lock is acquired, it will last until the transaction ends (either with a « COMMIT » or » ROLLBACK »).
We’ll lock on an empty table, that will contain no data, and whose purpose is only for locking.
First, we need to create a blank table we’ll call « global_lock »:
CREATE TABLE global_lock (id int);
Now, we can issue « LOCK TABLE » commands on this table:
$conn = pg_connect($dsn); if ($conn === false) { throw new Exception(sprintf("Error connecting to database with DSN '%s': %s", $dsn, pg_last_error()); } /* Start a transaction */ $res = pg_query($conn, 'BEGIN'); if ($res === false) { throw new Exception(sprintf("Error starting transaction: %s", pg_last_error($conn)); } /* Try to acquire a lock: this will block until we get the lock */ $res = pg_query($conn, 'LOCK TABLE global_lock'); if ($res === false) { throw new Exception(sprintf("Could not get lock on 'global_lock': %s", pg_last_error($conn)); } /* * Critical section which should not be executed * in parallel by another process */ [...] /* End of critical section */ /* Release the lock */ $res = pg_query($conn, 'ROLLBACK'); if ($res === false) { throw new Exception(sprintf("Could not release lock on 'global_lock': %s", pg_last_error($conn)); }
Regarding performances, I’m not sure this is faster than flock(), and it might also add pressure on your Postgresql’s max_connection limit if lot of PHP requires locking.
Services that heavily rely on locks (like distributed filesystems) uses dedicated services (like a Distributed Lock Manager), and that might be the route to go if you need that kind of performance/scalability.