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
was 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 chose an implementation:
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);
});
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.
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]);
});
This project is free and under the WTFPL. Responsible for this project is Markus Malkusch markus@malkusch.de.
If you like this project and feel generous donate a few Bitcoins here: 1335STSwu9hST4vcMRppEPgENMHD2r1REK