Skip to content
Michael Caldera edited this page Apr 30, 2015 · 16 revisions

Distributed locks with Redis

This library is an implementation of the [Redlock] 1 algorithm.

It is worth it to take a look at the wikipedia page for [distributed locking] 2.

Requirements

  • PHP >= 5.4
  • Redis >= 2.8 as using the [SCAN] 6 command

Installation

You can find the library in packagist here.

{
  "require": {
    "everlution/redlock": "dev-master"
  }
}

Testing

If you want to run the tests you need to change the tests/config.yml config file in order to specify the valid and invalid Redis instances that the testing suite should use for the integration tests.

In the tests/vagrant folder you can find the vagrant files that can spin up 5 different virtual machines with the Redis service. This is required by the default tests/config.yml unless you change it (knowing what you are actually doing).

In order to run the tests execute:

./vendor/bin/phpunit tests

Some of the tests are actually using sleep() in order to make some locks expire, so please be patient.

Locks

A lock is an object composed by:

  • Resource name is the unique name given to a specific resource that we want to lock
  • Lock type specifies what kind of lock we are asking for (see below)
  • Token is a random value used for security reasons (for example you can release a lock only if you provide the same token)
  • Validity time is the time in seconds that we want the lock to live for, after that the lock would be released
use Everlution\Redlock\Model\Lock;
use Everlution\Redlock\Model\LockType;

$resourceName = 'printer';
$type = LockType::EX; // Exclusive lock
$token = 'thisIsTheRandomToken';
$validityTime = 60; // 60 seconds

$lock = new Lock($resourceName, $type, $token, $validityTime);

Lock types

The library allows to acquire different types of locks.

Null (NL)

Indicates interest in the resource, but does not prevent other processes from locking it. It has the advantage that the resource and its lock value block are preserved, even when no processes are locking it.

Protected Read (PR)

This is the traditional share lock, which indicates a desire to read the resource but prevents other from updating it. Others can however also read the resource.

Protected Write (PW)

This is the traditional update lock, which indicates a desire to read and update the resource and prevents others from updating it. Others with Concurrent Read access can however read the resource.

Concurrent Read (CR)

Indicates a desire to read (but not update) the resource. It allows other processes to read or update the resource, but prevents others from having exclusive access to it. This is usually employed on high-level resources, in order that more restrictive locks can be obtained on subordinate resources.

Concurrent Write (CW)

Indicates a desire to read and update the resource. It also allows other processes to read or update the resource, but prevents others from having exclusive access to it. This is also usually employed on high-level resources, in order that more restrictive locks can be obtained on subordinate resources.

Exclusive (EX)

This is the traditional exclusive lock which allows read and update access to the resource, and prevents others from having any access to it.

Lock Types Truth Table

Mode NL CR CW PR PW EX
NL Y Y Y Y Y Y
CR Y Y Y Y Y N
CW Y Y Y N N N
PR Y Y N Y N N
PW Y Y N N N N
EX Y N N N N N

Key Generator

A lock actually consists in a unique key saved in Redis.

You can find the default key generator class \Everlution\Redlock\KeyGenerator\DefaultKeyGenerator that defines the keys in the following way:

{ResourceName}:{LockType}:{token}

Quorum

The quorum defines the rule in order to decide how many Redis instances storing the lock are required in order to consider the lock acquired.

The provided quorum is \Everlution\Redlock\Quorum\HalfPlusOneQuorum which (guess what) considers the lock acquired only when half of the instances plus one are storing the lock.

Adapters

An adapter is a class that maps a specific Redis master instance which is supposed to store the lock.

The current available adapters are:

  • [Predis] 3 - \Everlution\Redlock\Adapter\PredisAdapter

If you want to develop your own adapter (using another package such as PHPRedis) it must implement the interface \Everlution\Redlock\Adapter\AdapterInterface.

Theoretically it is possible to mix different adapters even using different technologies. For example we can have a cluster of 5 adapters with 3 of them using Redis (of which 2 are using the Predis adapter and 1 the PHP Redis extension adapter) and 2 of them using MySQL, however this is not a good idea.

Here is an example with the Predis adapter:

use Everlution\Redlock\Adapter\PredisAdapter;

$adapter = new PredisAdapter(
    new \Predis\Client(array(
        'host'    => '127.0.0.1',
        'port'    => 6379,
        'timeout' => 0,
        'async'   => false,
    )
));

The Lock Type Manager

This class (\Everlution\Redlock\Manager\LockTypeManager) implements the logic of the Lock Types Truth Table mentioned above in order to define whether a lock can be acquired or not depending by the current locks.

The Lock Manager

This is the class that actually allows you to acquire/release a lock distributing it on all the instances.

For the suggested number of adapters and other info on the best configuration please read this [article] 1 carefully.

use Everlution\Redlock\Manager\LockManager;
use Everlution\Redlock\Manager\LockTypeManager;
use Everlution\Redlock\Quorum\HalfPlusOneQuorum;
use Everlution\Redlock\KeyGenerator\DefaultKeyGenerator;

$lockManager = new \Everlution\Redlock\Manager\LockManager(
    new HalfPlusOneQuorum(),
    new DefaultKeyGenerator(),
    new LockTypeManager(),
    $defaultLockValidityTime, // if the lock validity time has not been specified in the lock object itself
    $retryCount, // the number of times we want to try to acquire the lock on failure
    $retryMaxDelay // the max delay before a retry
);

$lockManager
    ->addAdapter($adapter1)
    ->addAdapter($adapter2)
    ->addAdapter($adapter3)
    ->addAdapter($adapter4)
    ->addAdapter($adapter5)
;

$lock = new Lock('printer', LockType::EX, 'thisIsTheRandomToken', 60);

$acquired = $lockManager->acquireLock($lock);

if ($acquired) {
    echo 'The lock has been acquired successfully';
}

$released = $lockManager->releaseLock($lock);

if ($released) {
    echo 'The lock has been released successfully';
}

Examples

Please take a look at the examples folder.

To do

  • Add a new adapter for [PHP Redis extension] 4

Contributors

  • [Michael Caldera] 5