Компонент Lock

Дата обновления перевода: 2024-06-05

Компонент Lock

Компонент Lock создаёт и управляет блокировками - механизмом, который предоставляет эксклюзивный доступ к общему источнику.

Если вы используете фреймворк Symfony, прочтите документацию блокировки фреймворка Symfony.

Установка

1
$ composer require symfony/lock

Note

Если вы устанавливаете этот компонент вне приложения Symfony, вам нужно подключить файл vendor/autoload.php в вашем коде для включения механизма автозагрузки классов, предоставляемых Composer. Детальнее читайте в этой статье.

Использование

Блокировки используются для гарантии эксклюзивного доступа к некоторыми общим источникам. В приложениях Symfony вы можете использовать блокировки, к примеру, чтобы гарантировать, что одномоментно команда не будет выполнена больше, чем единожды (на одном или разных серверах).

Блокировки создаются классом LockFactory, который в свою очередь подключает другой класс для управление хранением блокировок:

1
2
3
4
5
use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Lock\Store\SemaphoreStore;

$store = new SemaphoreStore();
$factory = new LockFactory($store);

Блокировка создаётся вызовом метода createLock(). Её первый аргумент - произвольная строка, которая представляет заблокированный ресурс. Потом вызов метода acquire() попытается получить блокировку:

1
2
3
4
5
6
7
8
9
// ...
$lock = $factory->createLock('pdf-invoice-generation');

if ($lock->acquire()) {
    // Источник "pdf-invoice-generation" заблокирован.
    // Здесь вы можете безопасно вычислять и генерировать счёт.

    $lock->release();
}

Если блокировку нельзя получить, метод возвращает false. Метод acquire() может быть безопасно вызван несколько раз, даже если блокировка уже вычислена.

Note

В отличие от других реализаций, Компонент Lock различает экземпляры блокировок даже когда они создаются для одного и того же источника. Это означает, что для заданного поля и источника, один экземпляр блокировки можно получить множество раз.Если блокировка должны быть использована несколькими сервисами, они должны иметь одинаковый экземпляр Lock, возвращённый методом Factory::createLock.

Tip

Если вы не выпустите блокировку ясно, то он будет автоматически выпущен при разрушении экземпляра. В некоторых случаях, может быть полезно заблокировать источник по нескольким запросам. Чтобы отключить поведение автоматичекого релиза, установите третий аргумент метода createLock(), как false.

Сериализация блокировок

Key содержит состояние Lock и может быть сериализован. Это позволяет пользователю начать долгую работу в процессе, получив блокировку, и продолжить работу в другом процессе, используя ту же блокировку:

Для начала вы можете создать сериализуемый класс, содержащий источник и ключ
блокировки:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/Lock/RefreshTaxonomy.php
namespace App\Lock;

use Symfony\Component\Lock\Key;

class RefreshTaxonomy
{
    public function __construct(
        private object $article,
        private Key $key,
    ) {
    }

    public function getArticle(): object
    {
        return $this->article;
    }

    public function getKey(): Key
    {
        return $this->key;
    }
}

Затем вы можете использовать этот класс, чтобы развернуть всё необходимо для того, чтобы другой процесс проделал остаток работы:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use App\Lock\RefreshTaxonomy;
use Symfony\Component\Lock\Key;
use Symfony\Component\Lock\Lock;

$key = new Key('article.'.$article->getId());
$lock = new Lock(
    $key,
    $this->store,
    300,  // ttl
    false // autoRelease
);
$lock->acquire(true);

$this->bus->dispatch(new RefreshTaxonomy($article, $key));

Note

Не забудьте установить аргумент autoRelease как false в конструкторе Lock, чтобы избежать выпуска блокировки, когда вызывается деструктор.

Не все хранилища совместимы с сериализацией и межпроцессной блокировкой: например, явдро автоматически выпустит семафоры, полученные хранилищем SemaphoreStore . Если вы используете несовместимое хранилище (see lock stores for supported stores), будет вызвано исключение, когда приложение попробует сериализовать ключ.

Блокирующие блокировки

По умолчанию, когда блокировку нельзя извлечь, метод acquire немедленно возвращает false. Чтобы (бесконечно) ждать, пока будет создана блокировка, передайте true в качестве аргумента метода acquire(). Это называется блокирующая блокировка, так как выполнение вашего приложения останавливается, пока не будет приобретена блокировка:

1
2
3
4
5
6
7
8
use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Lock\Store\RedisStore;

$store = new RedisStore(new \Predis\Client('tcp://localhost:6379'));
$factory = new LockFactory($store);

$lock = $factory->createLock('pdf-creation');
$lock->acquire(true);

Если предоставленное хранилище не реализует интерфейс BlockingStoreInterface (см. lock stores , чтобы увидеть поддерживаемые хранилища), класс Lock снова попробует получить блокировку неблокирующим образом, до тех пор, пока блокировка не будет получена.

Блокировки с истечением срока действия

Блокировками, созданными удалённо, тяжело управлять, так как удалённому Store невозможно знать, жив ли ещё процесс блокировки. В связи с багами, неустранимыми ошибками или ошибками сегментации, невозможно гарантировать, что метод release() будет вызван, что приведёт к неограниченной блокировке источника.

Лучшим решением в этом случае будет создание блокировок с истечением срока действия, которые выпускаются автоматически после того, как прошло некоторое время (называемые ВЖ - Время Жизни). Это время (в секундах) конфигурируется в качестве второго аргумента метода createLock(). Если необходимо, эти блокировки также могут быт выпущены раньше, с помощью метода release().

Самая сложная часть при работе с блокировками со сроком действия - это выбор правильного ВЖ. Если оно слишком короткое, другие процессы могут получить блокировку до окончания работы; если оно слишком длинное и процесс вызовет сбой до вызова метода release(), источник останется заблокирован до тайм-аута:

1
2
3
4
5
6
7
8
9
10
11
12
// ...
// создать блокировку с истечением срока через 30 секунд
$lock = $factory->createLock('charts-generation', 30);

if (!$lock->acquire()) {
    return;
}
try {
    // выполнить работу меньше, чем за 30 секунд
} finally {
    $lock->release();
}

Tip

Чтобы не оставлять блокировку в заблокированном состоянии, рекомендуется обернуть задачу в блок try/catch/finally, чтобы всегда пытаться выпускать блокировку с истекающим сроком действия.

В случае долгосрочных задач, лучше начинать с не очень долгого ВЖ и потом использовать метод refresh(), чтобы переустановить ВЖ в его изначальное значение:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ...
$lock = $factory->createLock('charts-generation', 30);

if (!$lock->acquire()) {
    return;
}
try {
    while (!$finished) {
        // выполнить маленькую часть задачи.

        // обновить блокировку ещё на 30 секунд.
        $lock->refresh();
    }
} finally {
    $lock->release();
}

Tip

Другая полезная техника для долгоработающих задач - передача своего TTL как аргумента метода refresh() для изменения TTL блокировки по умолчанию:

1
2
3
4
5
6
7
$lock = $factory->createLock('charts-generation', 30);
// ...
// обновить блокировку на 30 секунд
$lock->refresh();
// ...
// обновить блокировку на 600 секунд (следующий вызов refresh() будет опять на 30 секунд)
$lock->refresh(600);

Данный компонент также предоставляет два полезных метода, связанных с блокировками с истечением срока действия: getRemainingLifetime() (который возвращает null или float в качестве второго), и isExpired() (который возвращает булево значение).

Автоматический выпуск блокировки

Блокировки автоматически выпускаются, когда разрушаются их объекты блокировки. Это деталь реализация, которая будет важна при использовании общих блокировок между процессами. В примере ниже, pcntl_fork() создает два процесса, а блокировка будет автоматически выпущена, как только будет закончен один из процессов:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ...
$lock = $factory->createLock('report-generation', 3600);
if (!$lock->acquire()) {
    return;
}

$pid = pcntl_fork();
if (-1 === $pid) {
    // Невозможно выполнить ветвление
    exit(1);
} elseif ($pid) {
    // Родительский процесс
    sleep(30);
} else {
    // Дочерний процесс
    echo 'The lock will be released now.';
    exit(0);
}
// ...

Note

Для того чтобы приведенный выше пример работал, должно быть установлено расширение PCNTL .

Чтобы отключить это поведение, установите false в третьем аргументе LockFactory::createLock(). Это заставит получать блокировку на 3600 секунд, или пока не будет вызван Lock::release():

1
2
3
4
5
$lock = $factory->createLock(
    'pdf-creation',
    3600, // ttl
    false // autoRelease
);

Общие блокировки

Общая блокировка или блокировка чтения-записи - это примитивная синхронизация, которая позволяет одновременный доступ к операциям только для чтения, в то время, как операция записи требуют эксклюзивного доступа. Это означает, что множество тем могут читать данные параллельно, но для записи или изменения данных необходима эксклюзивная блокировка. Они используются, к примеру, для структур данных, которые не могут быть обновлены автоматически, и невалидны до завершения обновления.

Используйте метод acquireRead(), чтобы получить блокировку только для чтения, и существующий метод acquire(), чтобы получить блокировку для записи:

1
2
3
4
$lock = $factory->createLock('user'.$user->id);
if (!$lock->acquireRead()) {
    return;
}

Схоже с методом acquire(), передайте true в качестве аргумента acquireRead(), чтобы получить блокировку в режиме блокирования:

1
2
$lock = $factory->createLock('user'.$user->id);
$lock->acquireRead(true);

Note

Политика приоритетов общих блокировок Symfony зависит от низлежащего хранилища (например, хранилище Redis приоритизирует чтение над записью).

Когда получена блокировка только для чтения, методом acquireRead(), возможно продвигать блокировку, и изменять ее на блокировку записи, вызвав метод acquire():

1
2
3
4
5
6
7
8
9
$lock = $factory->createLock('user'.$userId);
$lock->acquireRead(true);

if (!$this->shouldUpdate($userId)) {
    return;
}

$lock->acquire(true); // Продвигать блокировку до блокировки записи
$this->update($userId);

Таким же образом можно понизить блокировку записи, и изменить ее на блокировку только для чтения, вызвав метод acquireRead().

Если предоставленное хранилище не реализует интерфейс SharedLockStoreInterface (см. хранилища блокировок , чтобы увидеть поддерживаемые хранилища), класс Lock резервно откатится до блокировки записи, вызвав метод acquire().

Владелец блокировки

Блокировки, которые получены впервые, принадлежат экземпляру Lock, который получил их. Если вам нужно проверить, являеся из экземпляр Lock (все еще) владельцем блокировки, вы можете использовать метод isAcquired():

1
2
3
if ($lock->isAcquired()) {
    // Мы (всё ещё) владеем блокировкой
}

Так как некоторые хранилища блокировок имеют блокировки с истечением срока (как мы видели и разъясняли выше), экземпляр может потерять блокировку, которую он получил автоматически:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Если мы не можем получить сами, это означает, что некоторые другие процессы уже работают над этим
if (!$lock->acquire()) {
    return;
}

$this->beginTransaction();

// Выполнить очень долгий процесс, который может выйти за рамки TTL блокировки

if ($lock->isAcquired()) {
    // Всё ещё хорошо, ни один другой экземпляр не получил блокировку за это время, мы в безопасности
    $this->commit();
} else {
    // Чёрт! У нашей блокировки похоже истёк срок, и в это время начался другой процесс,
    // поэтому нам небезопасно коммитить.
    $this->rollback();
    throw new \Exception('Process failed');
}

Caution

Распространённая ошибка - использовать метод isAcquired() для проверки того, была ли блокировка уже получена каким-то процессом. Как вы можете увидеть в этом примере, для этого вам нужно использовать acquire(). Метод isAcquired() используется для проверки того, была ли блокировка получена только текущим процессом!

Note

Технически, настоящими владельцами блокировки являются те, кто имеют общий экземпляр Key, а не Lock. Но с точки зрения пользователя, Key является внутренним, и вы скорее всего будете работать только с экземпляром Lock, поэтому проще думать об экземпляре Lock, как о том, который владеет блокировкой.

Доступные хранилища

Блокировки создаются и управляются в Stores, которые являются классами, реализующими StoreInterface, и, опционально, BlockingStoreInterface

Компонент включает в себя следующие встроенные типы хранилищ:

????????? ??????? ?????????? ????????? ????? ?????
FlockStore ???????? ?? ??? ??
MemcachedStore ???????? ??? ?? ???
MongoDbStore ???????? ??? ?? ???
PdoStore ???????? ??? ?? ???
DoctrineDbalStore ???????? ??? ?? ???
PostgreSqlStore ???????? ?? ??? ??
DoctrineDbalPostgreSqlStore ???????? ?? ??? ??
RedisStore ???????? ??? ?? ??
SemaphoreStore ???????? ?? ??? ???
ZookeeperStore ???????? ??? ??? ???

Tip

Доступно специальное InMemoryStore для хранения блокировок в памяти во время процесс, и может быть полезным для тестирования.

FlockStore

FlockStore использует файловую систему на локальном компьютере для создания блокировок. Он не поддерживает истечение срока, но блокировка автоматически выпускается, когда объект блокировки выходит из области, и освобождается сборщиком мусора (например, когда заканчивается PHP процесс):

1
2
3
4
5
use Symfony\Component\Lock\Store\FlockStore;

// аргумент - это путь каталога, где создаются блокировки
// если он не задан, внутренне используется sys_get_temp_dir().
$store = new FlockStore('/var/stores');

Caution

Имейте в виду, что некоторые файловые системы (например, некоторые типы NFS), не поддерживают блокировку. В таких случаях, лучше использовать каталог на локальном диске или удалённом хранилище, основанном на Redis или Memcached.

MemcachedStore

MemcachedStore сохраняет блокировки на сервере Memcached, он требует подключения Memcached, реализующего класс \Memcached. Это хранилище не поддерживает блокировку, и ожидает TTL (Time To Live - время жизни), чтобы избежать затянутых блокировок:

1
2
3
4
5
6
use Symfony\Component\Lock\Store\MemcachedStore;

$memcached = new \Memcached();
$memcached->addServer('localhost', 11211);

$store = new MemcachedStore($memcached);

Note

Memcached не поддерживает TTL менее 1 секунды.

MongoDbStore

MongoDbStore сохраняет блокировки на сервере MongoDB >=2.2, оно требует \MongoDB\Collection или \MongoDB\Client из mongodb/mongodb или строки соединения MongoDB. Это хранилище не поддерживает блокировку и ожидает срока истечения, чтобы избежать устаревших блокировок:

1
2
3
4
5
6
7
8
9
10
11
use Symfony\Component\Lock\Store\MongoDbStore;

$mongo = 'mongodb://localhost/database?collection=lock';
$options = [
    'gcProbability' => 0.001,
    'database' => 'myapp',
    'collection' => 'lock',
    'uriOptions' => [],
    'driverOptions' => [],
];
$store = new MongoDbStore($mongo, $options);

MongoDbStore приниает следующие $options (в зависимости от типа первого параметра):

????? ????????
gcProbability ??? ???????? ??????? ????????? ?????, ?? ?????? ???? ??????? ???????????? ?? 0.0 ?? 1.0 (?? ????????? 0.001)
database ??? ???? ??????
collection ??? ?????????
uriOptions ????? ????? uri ??? MongoDBClient::__construct
driverOptions ?????? ????? ???????? ??? MongoDBClient::__construct

Когда первый параметр:

MongoDB\Collection:

  • $options['database'] игнорируется
  • $options['collection'] игнорируется

MongoDB\Client:

  • $options['database'] обязательна
  • $options['collection'] обязательна

Строка соединения MongoDB:

  • используется $options['database'], иначе - /path из DSN, как минимум одна - обязательна
  • используется $options['collection'], иначе - ?collection= из DSN, как минимум одна - обязательна

Note

Параметр строки запроса collection не является частью определения строки соединения MongoDB. Он используется для разрешения создания MongoDbStore, используя Имя источника данных (DSN) без $options.

PdoStore

PdoStore сохраняет блокировки в базе данных SQL. Оно требует соединения PDO, соединения Doctrine DBAL, или Имя источника данных (DSN). Это хранилище не поддерживает блокировку, и ожидает срока действия, чтобы избежать устаревших блокировок:

1
2
3
4
5
use Symfony\Component\Lock\Store\PdoStore;

// PDO, соединение Doctrine DBAL или DSN для ленивого соединения через PDO
$databaseConnectionOrDSN = 'mysql:host=127.0.0.1;dbname=app';
$store = new PdoStore($databaseConnectionOrDSN, ['db_username' => 'myuser', 'db_password' => 'mypassword']);

Note

Это хранилище не поддерживает срок действия меньше 1 секунды.

Таблица, где хранятся значения, создается автоматически при первом вызове к методу save(). Вы также можете создать эту таблицу ясно, вызвав метод createTable() в вашем коде.

DoctrineDbalStore

DoctrineDbalStore сохраняет блокировки в базе данных SQL. Оно идентично PdoStore, но требует соединения Doctrine DBAL или Doctrine DBAL URL. Это хранилище не поддерживает блокировку и ожидает TTL, чтобы избежать заглохших блокировок:

1
2
3
4
5
use Symfony\Component\Lock\Store\DoctrineDbalStore;

// соединение Doctrine DBAL или DSN
$connectionOrURL = 'mysql://myuser:mypassword@127.0.0.1/app';
$store = new DoctrineDbalStore($connectionOrURL);

Note

Это хранилище не поддерживает TTL меньше 1 секунды.

Таблица, где хранятся значения, будет автоматически сгенерирована, когда вы запустите команду:

1
$ php bin/console make:migration

Если вы предпочитаете создавать таблицу самостоятельно и она ещё не была создана, вы можете создать эту таблицу явным образом, вызвав createTable(). Вы также можете добавить эту таблицу в свою схему, вызвав configureSchema() в вашем коде.

Если таблица не была создана ранее, то она будет создана автоматически при первом вызове метода save().

PostgreSqlStore

PostgreSqlStore использует консультативные блокировки, предоставленные PostgreSQL. Оно требует соединения PDO или Имя источника данных (DSN). Оно поддерживает нативную блокировку, а также общие блокировки:

1
2
3
4
5
use Symfony\Component\Lock\Store\PostgreSqlStore;

// PDO или экземпляр DSN для ленивого соединения через PDO
$databaseConnectionOrDSN = 'postgresql://myuser:mypassword@localhost:5634/lock';
$store = new PostgreSqlStore($databaseConnectionOrDSN);

В отличие от PdoStore, PostgreSqlStore не нуждается в таблице для хранения блокировок, и не имеет срока истечения действия.

DoctrineDbalPostgreSqlStore

DoctrineDbalPostgreSqlStore использует консультативные блокировки, предоставленные PostgreSQL. Оно идентично PostgreSqlStore, но требует соединения Doctrine DBAL или Doctrine DBAL URL. Поддерживает нативные блокировки, а также общие блокировки:

1
2
3
4
5
use Symfony\Component\Lock\Store\DoctrineDbalPostgreSqlStore;

// соединение Doctrine или DSN
$databaseConnectionOrDSN = 'postgresql+advisory://myuser:mypassword@127.0.0.1:5634/lock';
$store = new DoctrineDbalPostgreSqlStore($databaseConnectionOrDSN);

В отличие от DoctrineDbalStore, DoctrineDbalPostgreSqlStore не нуждается в таблице, чтобы хранить блокировки, и не имеет срока окончания действия.

RedisStore

RedisStore сохраняет блокировки на сервере Redis, он требует подключения Redis, реализующего классы \Redis, \RedisArray, \RedisCluster, \Relay\Relay или \Predis. Это хранилище не поддерживает блокировку, и ожидает TTL, чтобы избежать затянутых блокировок:

1
2
3
4
5
6
use Symfony\Component\Lock\Store\RedisStore;

$redis = new \Redis();
$redis->connect('localhost');

$store = new RedisStore($redis);

SemaphoreStore

SemaphoreStore использует функции PHP semaphore для создания блокировок:

1
2
3
use Symfony\Component\Lock\Store\SemaphoreStore;

$store = new SemaphoreStore();

CombinedStore

CombinedStore создан для приложений Высокой Доступности, так как он синхронно управляет несколькими хранилищами (например, несколькими серверами Redis). Когда блокировка обнаружена, он перенаправляет вызов ко всем управляемым хранилищам, и собирает их ответы. Если простое большинство хранилищ обнаружили блокировку, то она считается обнаруженной; иначе - нет:

1
2
3
4
5
6
7
8
9
10
11
12
13
use Symfony\Component\Lock\Strategy\ConsensusStrategy;
use Symfony\Component\Lock\Store\CombinedStore;
use Symfony\Component\Lock\Strategy\ConsensusStrategy;

$stores = [];
foreach (['server1', 'server2', 'server3'] as $server) {
    $redis= new \Redis();
    $redis->connect($server);

    $stores[] = new RedisStore($redis);
}

$store = new CombinedStore($stores, new ConsensusStrategy());

Вместо простой стратегии простого большинства (ConsensusStrategy) можно использовать UnanimousStrategy для запроса обнаружения блокировки во всех хранилищах:

1
2
3
4
use Symfony\Component\Lock\Store\CombinedStore;
use Symfony\Component\Lock\Strategy\UnanimousStrategy;

$store = new CombinedStore($stores, new UnanimousStrategy());

Caution

Для того, чтобы получить высокую доступность при использовании ConsensusStrategy, минимальный размер кластера должен быть на трех серверах. Это позволяет кластеру продолжать работу, когда сбоит один из серверов (так как стратегия требует того, чтобы блокировка была получена на более, чем половине серверов).

ZookeeperStore

ZookeeperStore сохраняет блокировки на сервере ZooKeeper. Оно требует соединения ZooKeeper, релизующего класс \Zookeeper. Это хранилище не поддерживает блокировку и истечение действия, но блокировка автоматически выпускается, когда заканчивается PHP процесс:

1
2
3
4
5
6
7
use Symfony\Component\Lock\Store\ZookeeperStore;

$zookeeper = new \Zookeeper('localhost:2181');
// использовать следующее, чтобы определить кластер высокой доступности:
// $zookeeper = new \Zookeeper('localhost1:2181,localhost2:2181,localhost3:2181');

$store = new ZookeeperStore($zookeeper);

Note

Zookeeper не требует срока действия, так как узлы, используемые для блокирования, являются эфемерными, и умирают, когда заканчивается PHP-процесс.

Надёжность

Компонент гарантирует, что один и тот же источник не может быть заблокировн дважды, если компонент используется следующим образом.

Удалённые хранилища

Удалённые хранилища (MemcachedStore , MongoDbStore , PdoStore , PostgreSqlStore , RedisStore и ZookeeperStore ) используют уникальный токен, чтобы распознаьт настоящего владельца блокировки. Этот токен хранится в объекте Key и используется внутренне Lock.

Каждый одновременный процесс должен хранить Lock на одном и том же сервере. Иначе, разные машину могут позволять двум разным процессам получать один и тот же Lock.

Caution

Чтобы гарантировать, что один и тот же сервер всегда будет безопасным, не используйте Memcached за LoadBalancer, кластер или карусельные DNS. Даже если основной сервер ляжет, вызовы не должны перенаправляться на резервный сервер.

Хранилища с истечением срока

Хранилища с истечением срока (MemcachedStore , MongoDbStore , PdoStore и RedisStore ) гарантируют, что блокировка будет получена только в течение определенного временного промежутка. Если выполнение задачи занимает больше времени, то блокировка может быть выпущена хранилищем и получена кем-то другим.

Lock предоставляет несколько методов для проверки здоровья. Метод isExpired() проверяет, закончилось ли время существования, а метод getRemainingLifetime() возвращает оставшееся время существования в секундах.

Используя методы выше, более обширный код будет:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ...
$lock = $factory->createLock('invoice-publication', 30);

if (!$lock->acquire()) {
    return;
}
while (!$finished) {
    if ($lock->getRemainingLifetime() <= 5) {
        if ($lock->isExpired()) {
            // блокировка была утеряна, выполнить откат или отправить уведомление
            throw new \RuntimeException('Lock lost during the overall process');
        }

        $lock->refresh();
    }

    // Выполнить задачу, длительность которой ДОЛЖНА быть менее 5 минут
}

Caution

Разумно выбирайте время существования Lock и проверяйте, достаточно ли оставшегося времени существования для выполнения задачи.

Caution

Сохранение Lock обычно занимает пару миллискуенд, но условия сети могут сильно увеличить это время (до нескольких секунд). Примите это во внимание, выбирая правильный срок окончания действия.

По проекту, блокировки хранятся на серверах с определенным временем существования. Если дата или время машины изменяются, блокировка может быть выпущена ранее, чем ожидается.

Caution

Чтобы гарантировать, что дата не изенится, сервис NTP должен быть отключен, а дата должна быть обнволена, когда сервер будет остановлен.

FlockStore

Используя файловую систему, это Store надежно, если одновременные процессы использует один и тот же физический каталог для хранения блокировок.

Процессы должны выполняться на одной машине, виртуальной машине или контейнере. Будьте осторожны при обновлении сервиса Kubernetes или Swarm, так как на короткий период времени, два контейнера могут работать параллельно.

Абсолютный путь к каталогу должен оставаться одним и тем же. Будьте осторожны с символьными ссылками, которые могут измениться в любое время: Capistrano и зеленый/синий запуск часто используют этот фокус. Будьте осторожны, когда путь к этому каталогу изменяется между двумя запусками.

Некоторые файловые системы (вроде некоторых типов NFS) не поддерживают блокировку.

Caution

Все одновременные процессы должны использовать одну и ту же физическую файловую систему, путем выполнения на одной и той же машине, используя один и тот же аболсютный путь к каталогу блокировок.

По определению, использование FlockStore в HTTP-контексте несовместимо со множеством фронт-сервером, разве что не гарантировать, что один и тот же источник будет всегда заблокирован на одной и той же машине, или не использовать хорошо сконфигурированную общую файловую систему.

Файлы в файловой системе могут быть удалены во время операции по техническому обслуживанию. Например, чтобы очистить каталог /tmp, или после перезагрузки машины, когда каталог использует tmpfs. Это не проблема, если блокировка выпущена, когда процесс закончен, но это проблема, если Lock используется повторно между запросами.

Danger

Не храните блокировки в изменчивых файловых системах, если они должны быть повторно использованы по нескольким запросам.

MemcachedStore

Memcached работает путем сохранения объектов в памяти. Это означает, что используя MemcachedStore , блокировки не сохраняются, и могут по ошибке исчезнуть в любое время.

Если сервис Memcached или хостинг машины перезагружается, каждая блокировка будет утеряна без уведомления текущих процессов.

Caution

Чтобы избежать заполучения кем-то блокировки после перезагрузки, рекомендуется откладывать запуск сервисов, и ждать столько, сколько длится срок действия самой долгой блокировки.

По умолчанию Memcached использует механизм LRU для удаления старых записей, когда сервису необходимо пространство для добавления новых объектов.

Caution

Количество объектов, сохраненных в Memcached должно быть под контролем. Если это невозможно, LRU должен быть отключен, а блокировка должна быть сохранена в соответствующем сервисе Memcached подальше от Кеша.

Когда сервис Memcached общий и используется множеством образом, блокировки могут быть удалены по ошибке. Например, некоторая реализация метода PSR-6 clear() использует метод Memcached flush(), который сбрасывает и удаляет все.

Danger

Метод flush() не должен вызываться, или блокировки должны храниться в соответствующем сервисе Memcached подальше от Кеша.

MongoDbStore

Caution

Имя заблокированного источника индексируется в поле _id коллекции блокировок. Имейте в виду, что в MongoDB значение индексированного поля может быть максимум 1024 байтов в длину, включая структурную нагрузку.

Индекс срока действия должен быть использован, чтобы автоматически очищать устаревшие блокировки. Такой индекс может быть создан вручную:

1
2
3
4
db.lock.createIndex(
    { "expires_at": 1 },
    { "expireAfterSeconds": 0 }
)

Как вариант, можно единождя вызвать метод MongoDbStore::createTtlIndex(int $expireAfterSeconds = 0), чтобы создать индекс срока действия во время настройки базы данных. Прочтите больше о Дате истечения срока из коллекций, путем установки TTL в MongoDB.

Tip

MongoDbStore будет пробовать автоматически создать индекс TTL. Рекомендуется установить опцию конструктора gcProbability как 0.0, чтобы отключить это поведение, если вы уже вручную разобрались с созданием индекса TTL.

Caution

Это хранилище полагается на все узлы PHP-приложения и базы данных, чтобы сихнронизировать часы для истечения срока блокировок в заданное время. Чтобы гарантировать, что блокировки не устареют заранее, блокировка TTL должна быть установлена с достаточным количеством дополнительного времени в expireAfterSeconds, чтобы учесть все смещения между узлами.

writeConcern и readConcern не указаны MongoDbStore, означая, что будут действовать настройки коллекции. readPreference является primary для всех запросов. Прочтите больше о Семантике набора реплик для чтения и записи в MongoDB.

PdoStore

PdoStore полагается на свойства ACID движка SQL.

Caution

В кластере, сконфигурированным со множеством первичных, гарантируйте, что записи синхронно размножаются по всем узлам, или всегда используйте один и тот же узел.

Caution

Некоторые движки SQL вроде MySQL позволяют отключать проверку уникального ограничения. Убедитесь, что это не так для SET unique_checks=1;.

Для того, чтобы сбросить старые блокировки, это хранилище использует текущие дату и время, чтобы определить справочник даты истечения срока. Этот механизм полагается на все узлы сервера, чтобы иметь синхронизированные часы.

Caution

Чтобы гарантировать, что блокировки не устареют заранее, TTL должны быть установлены с достаточным количеством дополнительного времени, учитывая все смещения часов между узлами.

PostgreSqlStore

PdoStore полагается на свойства Консультативных болкировок базы данных PostgreSQL. Это означает, что используя PostgreSqlStore , блокировки будут автоматически выпущены в конце сессии в случае, если клиент не сможет произвести разблокирование по каким-то причинам.

Если сервис PostgreSQL, или машина на которой он размещен, перезапускается, каждая блокировка будет утеряна, без уведомления текущих процессов.

Если TCP-соединение утеряно, PostgreSQL может выпустить блокировки без уведомления об этом приложения.

RedisStore

Redis работает, сохраняя объекты в памяти. Это означает, что используя RedisStore , болкировки не сохраняются и могут по ошибке изчезнуть в любое время.

Если сервис Redis, или машина на которой он размещен, перезапускается, каждая блокировка будет утеряна, без уведомления текущих процессов.

Caution

Чтобы избежать заполучения блокировки кем-то после перезагрузки, рекомендуется откладывать начало сервиса и ждать как минимум столько времени, сколько действует самый долгий TTL.

Tip

Redis может быть сконфигурирован, чтобы сохранять объекты на диске, но эта опция замедлит запись в сервисе. Это может противоречить другим использованиям сервера.

Когда сервис Redis общий и спользуется для нескольких применений, блокировки могут быть удалены по ошибке.

Danger

Команда FLUSHDB не должна вызываться, или блокировки должны быть сохранены в соответствующем сервисе Redis подальше от Кеша.

CombinedStore

Combined хранилище позволяет хранить блокировки по нескольким бэкендам. Распространенная ошибка - думать, что механизм блокировок будет более надежным. Это не так. CombinedStore в лучшем случае будет настолько надежен, насколько наименее надежный из всех управляемых хранилищ. Как только одно из управляемых хранилищ вернет ошибочную информацию, CombinedStore не будет надежным.

Caution

Все одновременные процессы должны использовать одну и ту же конфигурацию, с одним и тем же управляемым хранилищем и одинаковой конечной точкой.

Tip

Вместо использования кластера серверов Redis или Memcached, лучше использовать CombinedStore с одним сервером на управляемое хранилище.

SemaphoreStore

Семафоры обрабатываются уровнем Ядра. Для того, чтобы быть надежными, процессы должны работать на одной и той же машине, виртуальной машине или контейнером. Будьте осторожны обновляя сервисы Kubernetes или Swarm, так как на короткий период времени, два контейнера могут работать параллельно.

Caution

Все одновременные процессы должны использовать одну и ту же машину. Перед запуском одновременных процессов на новой машине, проверьте, чтобы все другие процессы были остановлены на старой.

Caution

При запуске на systemd с несистемным пользователем и опцией RemoveIPC=yes (значение по умолчанию), блокировки удаляются systemd, когда пользователь выходит из системы. Убедитесь, что процесс запущен с системным пользователем (UID <= SYS_UID_MAX) с SYS_UID_MAX, определенным в /etc/login.defs, или установите опцию RemoveIPC=off в /etc/systemd/logind.conf.

ZookeeperStore

ZookeeperStore работает путем содержания блокировок в виде эфемерных узлов на сервере. Это означает, что используя ZookeeperStore , блокировки будут автоматически выпущены в конце сессии, если клиент не сможет произвести разблокировку по каким-то причинам.

Если сервис ZooKeeper, или машина на которой он размещен, перезапускается, каждая блокировка будет утеряна, без уведомления текущих процессов.

Tip

Чтобы использовать функцию высокой доступности ZooKeeper, вы можете настроить кластер множества серверов, чтобы если один из серверов ляжет, большинство все еще будет работать и обрабатывать запросы. Все доступные серверы в класетер будут видеть одинаковые данные.

Note

Так как это хранилище не поддерживает многоуровненвые блокировки узлов, и так как очистка промежуточных узлов создает излишнюю нагрузку, все узлы содержатся на корневом уровне.

Итог

Изменение конфигурации хранилищ должно производиться очень осторожно. К примеру, во время запуска новой версии. Процессы с новой конфигурацией не должны начинаться в то время, как старые процессы со старой конфигурацией все еще идут.