malkusch/lock

Mutex library for exclusive code execution.

v1.3 2018-05-26 11:36 UTC

README

Build Status

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 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);
});

Implementations

The Mutex is an abstract class. You will have to chose an implementation:

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);
});

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]);
});

MySQLMutex

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);
});

PgAdvisoryLockMutex

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);
});

License and authors

This project is free and under the WTFPL. Responsible for this project is Willem Stuursma-Ruwen willem@stuursma.name.

Donations

If you like this project and feel generous donate a few Bitcoins here: 1P5FAZ4QhXCuwYPnLZdk3PJsqePbu1UDDA