This library helps executing critical code in concurrent situations.
Installation
Use Composer:
composer require malkusch/lock
Usage
The package is in the namespace
malkusch\lock
.
Mutex
The
Mutex
provides the API for this library.
Mutex::synchronized()
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()
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);
});
Implementations
The Mutex
is an abstract class. You will have to chose an implementation:
CASMutex
FlockMutex
MemcacheMutex
MemcachedMutex
PHPRedisMutex
PredisMutex
SemaphoreMutex
TransactionalMutex
CASMutex
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();
});
FlockMutex
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);
});
MemcacheMutex
The MemcacheMutex
is a spinlock implementation which uses the Memcache
API.
Example:
$memcache = new \Memcache();
$memcache->connect("localhost");
$mutex = new MemcacheMutex("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);
});
MemcachedMutex
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);
});
PHPRedisMutex
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);
});
PredisMutex
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);
});
SemaphoreMutex
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);
});
TransactionalMutex
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]);
});
License and authors
This project is free and under the WTFPL. Responsible for this project is Markus Malkusch markus@malkusch.de.
Donations
If you like this project and feel generous donate a few Bitcoins here: 1335STSwu9hST4vcMRppEPgENMHD2r1REK