Компонент Lock

Дата оновлення перекладу 2024-05-03

Компонент 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 . Якщо ви використовуєте несумісне сховище (див. сховища блокувань , щоб побачити підтримувані сховища), буде викликано виключення, коли додаток спробує серіалізувати ключ.

Блокування блокувань

За замовчуванням, коли блокування не можна обчислити, метод 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 повторно спробує обчислити блокування у неблокуючий спосіб, поки не обчислить блокування.

Блокування із закінченням строку дії

Блокуваннями, створеними віддалено, важко керувати, так як віддаленому 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);

Цей компонент також надає 2 корисних метода, пов'язаних з блокуваннями з закінченням строку дії: 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('pdf-creation');
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().

$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().

Власник блокування

Блокування, отримані вперше, належать [1]_ екземпляру 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, зʼєднання Doctrine DBAL, або Імʼя джерела даних (DSN). Воно підтримує нативне блокування, а також загальні блокування:

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

// PDO, зʼєднання Doctrine DBAL або 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-семафора для створення блокувань:

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\Store\CombinedStore;
use Symfony\Component\Lock\Store\RedisStore;
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('pdf-creation', 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 використовується повторно між запитами.

Caution

Не зберігайте блокування у мінливих файлових системах, якщо вони повинні бути повторно використані за декількома запитами.

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

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

Заключення

Зміна конфігурації сховищ повинна проводитися дуже обережно. Наприклад, під час запуску нової версії. Процеси з новою конфігурацією не повинні починатися в той час, як старі процеси зі старою конфігурацією все ще тривають.