Параллелизм в программировании — это не просто задача, но и искусство. Представьте себе, что вы разработчик в компании электронной коммерции, ежедневно обрабатывающей тысячи заказов. Все идет гладко, пока однажды вы не заметите, что когда несколько клиентов пытаются разместить заказы одновременно, система начинает вести себя хаотично. Именно тогда вы начнете ценить красоту запирающих механизмов, таких как Symfony Lock.

Компонент блокировки в Symfony

Когда мы начинаем работать с Symfony, одним из компонентов, который может привлечь наше внимание, является Lock. Представленный в Symfony 3.4, он стал неотъемлемой частью многих разработчиков. С его помощью мы можем создавать блокировки, которые гарантируют, что только один процесс одновременно имеет доступ к критическим участкам кода.

Основной пример:

$lock = lockFactory->createLock('pdf-creation');

if (!acquire()) {
    return;
}

service->method();
release();

Установка и настройка

Чтобы начать использовать компонент Lock в Symfony, сначала необходимо установить его с помощью Composer:

composer require symfony/lock

После установки вы можете настроить компонент в файле config/packages/lock.yaml. Вы можете выбрать магазин, в котором будут храниться замки, и другие параметры, специфичные для магазина.

# config/packages/lock.yaml
framework:
    lock: ~
    lock: 'flock'
    lock: 'flock:///path/to/file'
    lock: 'semaphore'
    lock: 'memcached://m1.docker'
    lock: ['memcached://m1.docker', 'memcached://m2.docker']
    lock: 'redis://r1.docker'
    lock: ['redis://r1.docker', 'redis://r2.docker']
    lock: 'rediss://r1.docker?ssl[verify_peer]=1&ssl[cafile]=...'
    lock: 'zookeeper://z1.docker'
    lock: 'zookeeper://z1.docker,z2.docker'
    lock: 'zookeeper://localhost01,localhost02:2181'
    lock: 'sqlite:///%kernel.project_dir%/var/lock.db'
    lock: 'mysql:host=127.0.0.1;dbname=app'
    lock: 'pgsql:host=127.0.0.1;dbname=app'
    lock: 'pgsql+advisory:host=127.0.0.1;dbname=app'
    lock: 'sqlsrv:server=127.0.0.1;Database=app'
    lock: 'oci:host=127.0.0.1;dbname=app'
    lock: 'mongodb://127.0.0.1/app?collection=lock'
    lock: '%env(LOCK_DSN)%'

    # named locks
    lock:
        invoice: ['semaphore', 'redis://r2.docker']
        report: 'semaphore'

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

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

$key = $lock->getKey();
$serializedKey = serialize($key);
$unserializedKey = unserialize($serializedKey);
$lock->setKey($unserializedKey);


class ProductTaxonomy {
    public function __construct(private object $product, private Key $key) {}
    public function getProduct(): object { return $this->product; }
    public function getKey(): Key { return $this->key; }
}

Блокировка блокировок

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

$lock = $factory->createLock('order-12345');

if (!$lock->acquire(true)) {
    throw new \RuntimeException('Cannot acquire the lock!');
}

$lock->release();

Блокировки с истекающим сроком действия

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

$lock = $factory->createLock('order-12345', 30); // 30 seconds TTL

if ($lock->acquire()) {
    // Processing the order...
    $lock->release();
}

Автоматическое снятие блокировки

Одна из самых больших проблем при работе с замками — не забыть их снять. У Symfony есть решение для этой проблемы — блокировки автоматически снимаются при уничтожении объекта Lock.

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

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

$lock = $factory->createLock('product-56789');

if ($lock->acquireRead()) {
    // Reading product information...
}

$lock->release();

Владелец замка

Приобретая замок, мы становимся его «владельцем». Symfony позволяет проверить, является ли данный процесс владельцем блокировки.

if ($lock->isAcquired()) {
    // Processing the order...
}

Семафор против Стаи:

  • Semaphore: Это механизм блокировки, основанный на системных семафорах. Это быстро и эффективно, но работает только в системах, поддерживающих семафоры IPC.
  • Flock: Это механизм блокировки, основанный на блокировке файлов. Он более универсален, чем семафоры, но в некоторых ситуациях может быть менее эффективным.

Пример использования Flock:

$store = new FlockStore('/path/to/dir');
$factory = new LockFactory($store);
$lock = $factory->createLock('product-56789');

if ($lock->acquire()) {
    // Updating product information...
    $lock->release();
}

Быстрый вопрос: представляет ли эта история какую-либо ценность для вас? Пожалуйста, поддержите мою работу, оставив аплодисменты в знак признательности. Спасибо.

Доступные магазины

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

Блокировки создаются и управляются в хранилищах (Store), которые представляют собой классы, реализующие PersistingStoreInterface. Компонент включает в себя следующие встроенные типы хранилищ: FlockStore, MemcachedStore, MongoDbStore, PdoStore, DoctrineDbalStore, PostgreSqlStore, DoctrineDbalPostgreSqlStore, RedisStore, ZookeeperStore, а также специальный InMemoryStore, используемый для хранения блокировок в памяти во время процесса, что может быть полезно при тестировании.

Заключение

Компонент Lock в Symfony — мощный инструмент, позволяющий эффективно управлять параллелизмом в веб-приложениях. С его помощью мы можем писать код, который не только более надежен, но и проще в обслуживании. Если вы еще не использовали этот компонент, стоит попробовать.