This library helps executing critical code in concurrent situations.
Use Composer:
composer require malkusch/lock
The package is in the namespace
malkusch\lock
.
The
Mutex
provides the API for this library.
Mutex::synchronized()
executes code exclusively. This method guarantees that the code is only executed
by one process at once. Other processes have to wait until the mutex is available.
The critical code may throw an exception, which would release the lock as well.
Example:
$mutex->synchronized(function () use ($bankAccount, $amount) {
$balance = $bankAccount->getBalance();
$balance -= $amount;
if ($balance < 0) {
throw new \DomainException("You have no credit.");
}
$bankAccount->setBalance($balance);
});
Mutex::check()
performs a double-checked locking pattern. I.e. if the check fails, no lock
will be acquired. Else if the check was true, a lock will be acquired and the
check will be perfomed as well together with the critical code.
Example:
$mutex->check(function () use ($bankAccount, $amount) {
return $bankAccount->getBalance() >= $amount;
})->then(function () use ($bankAccount, $amount) {
$balance = $bankAccount->getBalance();
$balance -= $amount;
$bankAccount->setBalance($balance);
});
The Mutex
is an abstract class. You will have to choose an implementation:
CASMutex
FlockMutex
MemcachedMutex
PHPRedisMutex
PredisMutex
SemaphoreMutex
TransactionalMutex
MySQLMutex
PgAdvisoryLockMutex
The CASMutex
has to be used with a Compare-and-swap operation.
This mutex is lock free. It will repeat executing the code until the CAS operation was
successful. The code should therefore notify the mutex by calling
CASMutex::notify()
.
As the mutex keeps executing the critical code, it must not have any side effects as long as the CAS operation was not successful.
Example:
$mutex = new CASMutex();
$mutex->synchronized(function () use ($memcached, $mutex, $amount) {
$balance = $memcached->get("balance", null, $casToken);
$balance -= $amount;
if (!$memcached->cas($casToken, "balance", $balance)) {
return;
}
$mutex->notify();
});
The FlockMutex is a lock implementation based on flock()
.
Example:
$mutex = new FlockMutex(fopen(__FILE__, "r"));
$mutex->synchronized(function () use ($bankAccount, $amount) {
$balance = $bankAccount->getBalance();
$balance -= $amount;
if ($balance < 0) {
throw new \DomainException("You have no credit.");
}
$bankAccount->setBalance($balance);
});
Timeouts are supported as an optional second argument. This uses the pcntl extension if possible or busy waiting if not.
The MemcachedMutex
is a spinlock implementation which uses the Memcached
API.
Example:
$memcache = new \Memcached();
$memcache->addServer("localhost", 11211);
$mutex = new MemcachedMutex("balance", $memcache);
$mutex->synchronized(function () use ($bankAccount, $amount) {
$balance = $bankAccount->getBalance();
$balance -= $amount;
if ($balance < 0) {
throw new \DomainException("You have no credit.");
}
$bankAccount->setBalance($balance);
});
The PHPRedisMutex is the distributed lock implementation of RedLock
which uses the phpredis
extension.
This implementation requires at least phpredis-2.2.4
.
If used with a cluster of Redis servers, acquiring and releasing locks will continue to function as long as a majority of the servers still works.
Example:
$redis = new Redis();
$redis->connect("localhost");
$mutex = new PHPRedisMutex([$redis], "balance");
$mutex->synchronized(function () use ($bankAccount, $amount) {
$balance = $bankAccount->getBalance();
$balance -= $amount;
if ($balance < 0) {
throw new \DomainException("You have no credit.");
}
$bankAccount->setBalance($balance);
});
The PredisMutex is the distributed lock implementation of RedLock
which uses the Predis
API.
Example:
$redis = new Client("redis://localhost");
$mutex = new PredisMutex([$redis], "balance");
$mutex->synchronized(function () use ($bankAccount, $amount) {
$balance = $bankAccount->getBalance();
$balance -= $amount;
if ($balance < 0) {
throw new \DomainException("You have no credit.");
}
$bankAccount->setBalance($balance);
});
The SemaphoreMutex is a lock implementation based on Semaphore.
Example:
$semaphore = sem_get(ftok(__FILE__, "a"));
$mutex = new SemaphoreMutex($semaphore);
$mutex->synchronized(function () use ($bankAccount, $amount) {
$balance = $bankAccount->getBalance();
$balance -= $amount;
if ($balance < 0) {
throw new \DomainException("You have no credit.");
}
$bankAccount->setBalance($balance);
});
The TransactionalMutex
delegates the serialization to the DBS. The exclusive code is executed within
a transaction. It's up to you to set the correct transaction isolation level.
However if the transaction fails (i.e. a PDOException
was thrown), the code
will be executed again in a new transaction. Therefore the code must not have
any side effects besides SQL statements. Also the isolation level should be
conserved for the repeated transaction. If the code throws an exception,
the transaction is rolled back and not replayed again.
Example:
$mutex = new TransactionalMutex($pdo);
$mutex->synchronized(function () use ($pdo, $accountId, $amount) {
$select = $pdo->prepare("SELECT balance FROM account WHERE id = ? FOR UPDATE");
$select->execute([$accountId]);
$balance = $select->fetchColumn();
$balance -= $amount;
if ($balance < 0) {
throw new \DomainException("You have no credit.");
}
$pdo->prepare("UPDATE account SET balance = ? WHERE id = ?")
->execute([$balance, $accountId]);
});
The MySQLMutex uses MySQL's
GET_LOCK
function.
It supports time outs. If the connection to the database server is lost or interrupted, the lock is automatically released.
Note that before MySQL 5.7.5 you cannot use nested locks, any new lock will silently release already held locks. You should probably refrain from using this mutex on MySQL versions < 5.7.5.
$pdo = new PDO("mysql:host=localhost;dbname=test", "username");
$mutex = new MySQLMutex($pdo, "balance", 15);
$mutex->synchronized(function () use ($bankAccount, $amount) {
$balance = $bankAccount->getBalance();
$balance -= $amount;
if ($balance < 0) {
throw new \DomainException("You have no credit.");
}
$bankAccount->setBalance($balance);
});
The PgAdvisoryLockMutex uses PostgreSQL's advisory locking functions.
Named locks are offered. PostgreSQL locking functions require integers but the conversion is handled automatically.
No time outs are supported. If the connection to the database server is lost or interrupted, the lock is automatically released.
$pdo = new PDO("pgsql:host=localhost;dbname=test;", "username");
$mutex = new PgAdvisoryLockMutex($pdo, "balance");
$mutex->synchronized(function () use ($bankAccount, $amount) {
$balance = $bankAccount->getBalance();
$balance -= $amount;
if ($balance < 0) {
throw new \DomainException("You have no credit.");
}
$bankAccount->setBalance($balance);
});
This project is free and under the WTFPL. Responsible for this project is Willem Stuursma-Ruwen willem@stuursma.name.
If you like this project and feel generous donate a few Bitcoins here: 1P5FAZ4QhXCuwYPnLZdk3PJsqePbu1UDDA