Дата обновления перевода: 2020-12-25

When a program runs concurrently, some part of code which modify shared resources should not be accessed by multiple processes at the same time.

Многопоточная работа с блокировками

Когда выполнение программы распараллелена, часть кода, изменяющая общие ресурсы не должна быть доступна одновременно нескольким процессам. Компонент Symfony Lock предоставляет механизм блокировок, чтобы убедиться, что только один процесс запускает критический участок кода в любой момент времени для предотвращения состояния гонки (race condition).

Пример, показывающий типичное использование блокировки:

$lock = $lockFactory->createLock('pdf-invoice-generation');
if (!$lock->acquire()) {
    return;
}

// критический участок кода
$service->method();

$lock->release();

Установка

В приложениях с Symfony Flex, запустите эту команду для установки компонента Lock:

1
$ composer require symfony/lock

Настройка Lock с FrameworkBundle

По умолчанию Symfony предоставляет Semaphore, когда возможно, или Flock в других случаях. Вы можете настроить это поведение используя ключ lock так:

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    # 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: 'zookeeper://z1.docker'
        lock: 'zookeeper://z1.docker,z2.docker'
        lock: 'sqlite:///%kernel.project_dir%/var/lock.db'
        lock: 'mysql:host=127.0.0.1;dbname=lock'
        lock: 'pgsql:host=127.0.0.1;dbname=lock'
        lock: 'sqlsrv:server=localhost;Database=test'
        lock: 'oci:host=localhost;dbname=test'
        lock: '%env(LOCK_DSN)%'
    
        # named locks
        lock:
            invoice: ['semaphore', 'redis://r2.docker']
            report: 'semaphore'
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    <!-- config/packages/lock.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:framework="http://symfony.com/schema/dic/symfony"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            https://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    
        <framework:config>
            <framework:lock>
                <framework:resource>flock</framework:resource>
    
                <framework:resource>flock:///path/to/file</framework:resource>
    
                <framework:resource>semaphore</framework:resource>
    
                <framework:resource>memcached://m1.docker</framework:resource>
    
                <framework:resource>memcached://m1.docker</framework:resource>
                <framework:resource>memcached://m2.docker</framework:resource>
    
                <framework:resource>redis://r1.docker</framework:resource>
    
                <framework:resource>redis://r1.docker</framework:resource>
                <framework:resource>redis://r2.docker</framework:resource>
    
                <framework:resource>zookeeper://z1.docker</framework:resource>
    
                <framework:resource>zookeeper://z1.docker,z2.docker</framework:resource>
    
                <framework:resource>sqlite:///%kernel.project_dir%/var/lock.db</framework:resource>
    
                <framework:resource>mysql:host=127.0.0.1;dbname=lock</framework:resource>
    
                <framework:resource>pgsql:host=127.0.0.1;dbname=lock</framework:resource>
    
                <framework:resource>sqlsrv:server=localhost;Database=test</framework:resource>
    
                <framework:resource>oci:host=localhost;dbname=test</framework:resource>
    
                <framework:resource>%env(LOCK_DSN)%</framework:resource>
    
                <!-- named locks -->
                <framework:resource name="invoice">semaphore</framework:resource>
                <framework:resource name="invoice">redis://r2.docker</framework:resource>
                <framework:resource name="report">semaphore</framework:resource>
            </framework:lock>
        </framework:config>
    </container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // config/packages/lock.php
    $container->loadFromExtension('framework', [
        'lock' => null,
        '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' => 'zookeeper://z1.docker',
        'lock' => 'zookeeper://z1.docker,z2.docker',
        'lock' => 'sqlite:///%kernel.project_dir%/var/lock.db',
        'lock' => 'mysql:host=127.0.0.1;dbname=lock',
        'lock' => 'pgsql:host=127.0.0.1;dbname=lock',
        'lock' => 'sqlsrv:server=localhost;Database=test',
        'lock' => 'oci:host=localhost;dbname=test',
        'lock' => '%env(LOCK_DSN)%',
    
        // named locks
        'lock' => [
            'invoice' => ['semaphore', 'redis://r2.docker'],
            'report' => 'semaphore',
        ],
    ]);
    

Блокировка ресурса

Для блокировки ресурса по умолчанию, автоподключите Lock используя LockInterface (id сервиса lock):

// src/Controller/PdfController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Lock\LockInterface;

class PdfController extends AbstractController
{
    /**
     * @Route("/download/terms-of-use.pdf")
     */
    public function downloadPdf(LockInterface $lock, MyPdfGeneratorService $pdf)
    {
        $lock->acquire(true);

        // сложные расчёты
        $myPdf = $pdf->getOrCreatePdf();

        $lock->release();

        // ...
    }
}

Caution

Тот же экземпляр LockInterface не будет блокирован, если вызвать acquire несколько раз внутри того же процесса. Когда несколько сервисов используют тот же lock, внедрите LockFactory, чтобы создавать новый экземпляр блокировщика для каждого сервиса.

Блокирование динамического ресурса

Иногда приложение может разделить ресурс на небольшие части, чтобы блокировать только часть и позволять остальным обрабатываться. В предудущем примере где блокировалось $pdf->getOrCreatePdf('terms-of-use') для всех, давайте посмотрим как заблокировать $pdf->getOrCreatePdf($version) только для процессов, которым нужен доступ к той же $version:

// src/Controller/PdfController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Lock\LockInterface;

class PdfController extends AbstractController
{
    /**
     * @Route("/download/{version}/terms-of-use.pdf")
     */
    public function downloadPdf($version, LockFactory $lockFactory, MyPdfGeneratorService $pdf)
    {
        $lock = $lockFactory->createLock($version);
        $lock->acquire(true);

        // сложные расчёты
        $myPdf = $pdf->getOrCreatePdf($version);

        $lock->release();

        // ...
    }
}

Именованая блокировка

Если приложению нужен другой тип хранилища для разных блокировок, Symfony предоставляет именованную блокировку:

.. configuration-block::
1
2
3
4
5
# config/packages/lock.yaml
framework:
    lock:
        invoice: ['semaphore', 'redis://r2.docker']
        report: 'semaphore'
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<!-- config/packages/lock.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:framework="http://symfony.com/schema/dic/symfony"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        https://symfony.com/schema/dic/services/services-1.0.xsd
        http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">

    <framework:config>
        <framework:lock>
            <framework:resource name="invoice">semaphore</framework:resource>
            <framework:resource name="invoice">redis://r2.docker</framework:resource>
            <framework:resource name="report">semaphore</framework:resource>
        </framework:lock>
    </framework:config>
</container>
1
2
3
4
5
6
7
// config/packages/lock.php
$container->loadFromExtension('framework', [
    'lock' => [
        'invoice' => ['semaphore', 'redis://r2.docker'],
        'report' => 'semaphore',
    ],
]);

Каждое название становится сервисом, где к id сервиса добавляется суффикс в виде имени блокировщика (например, lock.invoice). Также создаётся алиас для автоподключения каждого блокировщика используя camel case версию его имени с добавлением Lock - например, invoice может быть автовнедрён с помощью названия аргумента $invoiceLock и с добавлением типа к нему LockInterface.

Symfony также предоставляет соответствующую фабрику и хранилице с аналогичными правилами (например invoice генерирует фабрику lock.invoice.factory и хранилище lock.invoice.store, оба могут быть автовнедрены с названиями соответственно $invoiceLockFactory и $invoiceLockStore с указанием типов LockFactory и PersistingStoreInterface)

Блокирующее хранилище

Если вы хотите использовать RetryTillSaveStore для неблокирующих блокировщиков, вы можете сделать это декорировав сервис хранилища:

1
2
3
4
lock.default.retry_till_save.store:
    class: Symfony\Component\Lock\Store\RetryTillSaveStore
    decorates: lock.default.store
    arguments: ['@lock.default.retry_till_save.store.inner', 100, 50]

Эта документация является переводом официальной документации Symfony и предоставляется по свободной лицензии CC BY-SA 3.0.