Дата обновления перевода: 2021-12-24
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 26 27 28
// config/packages/lock.php use Symfony\Config\FrameworkConfig; return static function (FrameworkConfig $framework) { $framework->lock() ->resource('default', ['flock']) ->resource('default', ['flock:///path/to/file']) ->resource('default', ['semaphore']) ->resource('default', ['memcached://m1.docker']) ->resource('default', ['memcached://m1.docker', 'memcached://m2.docker']) ->resource('default', ['redis://r1.docker']) ->resource('default', ['redis://r1.docker', 'redis://r2.docker']) ->resource('default', ['zookeeper://z1.docker']) ->resource('default', ['zookeeper://z1.docker,z2.docker']) ->resource('default', ['sqlite:///%kernel.project_dir%/var/lock.db']) ->resource('default', ['mysql:host=127.0.0.1;dbname=app']) ->resource('default', ['pgsql:host=127.0.0.1;dbname=app']) ->resource('default', ['pgsql+advisory:host=127.0.0.1;dbname=lock']) ->resource('default', ['sqlsrv:server=127.0.0.1;Database=app']) ->resource('default', ['oci:host=127.0.0.1;dbname=app']) ->resource('default', ['mongodb://127.0.0.1/app?collection=lock']) ->resource('default', ['%env(LOCK_DSN)%']) // named locks ->resource('invoice', ['semaphore', 'redis://r2.docker']) ->resource('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 8 9 // config/packages/lock.php use Symfony\Config\FrameworkConfig; return static function (FrameworkConfig $framework) { $framework->lock() ->resource('invoice', ['semaphore', 'redis://r2.docker']) ->resource('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: ['@.inner', 100, 50]
|
Эта документация является переводом официальной документации Symfony и предоставляется по свободной лицензии CC BY-SA 3.0.