|
| 1 | +.. index:: |
| 2 | + single: Lock |
| 3 | + single: Components; Lock |
| 4 | + |
| 5 | +The Lock Component |
| 6 | +==================== |
| 7 | + |
| 8 | + The Lock Component provides a mechanism to garentee an exclusive access into |
| 9 | + a critical section. The component ships with ready to use stores for the |
| 10 | + most common backends. |
| 11 | + |
| 12 | +.. versionadded:: 3.3 |
| 13 | + The Lock component was introduced in Symfony 3.3. |
| 14 | + |
| 15 | +Installation |
| 16 | +------------ |
| 17 | + |
| 18 | +You can install the component in 2 different ways: |
| 19 | + |
| 20 | +* :doc:`Install it via Composer </components/using_components>` (``symfony/lock`` on `Packagist`_); |
| 21 | +* Use the official Git repository (https://github.com/symfony/lock). |
| 22 | + |
| 23 | +.. include:: /components/require_autoload.rst.inc |
| 24 | + |
| 25 | + |
| 26 | +Usage |
| 27 | +----- |
| 28 | + |
| 29 | +In order to centralize state of locks, you first need to create a ``Store``. |
| 30 | +Then, you can ask to this store to create a Lock for your ``resource``. |
| 31 | + |
| 32 | +The :method:`Symfony\\Component\\Lock\\LockInterface::acquire` method tries to |
| 33 | +acquire the lock. If the lock is can not be acquired, the method throws a |
| 34 | +:class:`Symfony\\Component\\Lock\\Exception\\LockConflictedException`. You can |
| 35 | +safly call the ``acquire()`` method several time, even if you already acquired |
| 36 | +it. |
| 37 | + |
| 38 | +.. code-block:: php |
| 39 | +
|
| 40 | + use Symfony\Component\Lock\Store\SemaphoreStore; |
| 41 | + use Symfony\Component\Lock\Exception\LockConflictedException; |
| 42 | +
|
| 43 | + $store = new SemaphoreStore(); |
| 44 | + $lock = $store->createLock('hello'); |
| 45 | +
|
| 46 | + try { |
| 47 | + $lock->acquire(); |
| 48 | + // the resource "hello" is locked. You can perform your task safely. |
| 49 | +
|
| 50 | + // do whatever you want. |
| 51 | +
|
| 52 | + $lock->release(); |
| 53 | + } catch (LockConflictedException $e) { |
| 54 | + // the resource "hello" is already locked by another process |
| 55 | + } |
| 56 | +
|
| 57 | +The first argument of `createLock` is a string representation of the |
| 58 | +``resource`` to lock. |
| 59 | + |
| 60 | +.. note:: |
| 61 | + |
| 62 | + In opposition to some other implementations, the Lock Component distinguish |
| 63 | + locks instances, even when they are created from the same ``resource``. |
| 64 | + If you want to share a lock in several services. You have to share the |
| 65 | + instance of Lock returned by the ``Store::createLock`` method. |
| 66 | + |
| 67 | + |
| 68 | +Blocking locks |
| 69 | +-------------- |
| 70 | + |
| 71 | +You can pass an optional blocking argument as the first argument to the |
| 72 | +:method:`Symfony\\Component\\Lock\\LockInterface::acquire` method, which |
| 73 | +defaults to ``false``. If this is set to ``true``, your PHP code will wait |
| 74 | +infinitely until the lock is released by another process. |
| 75 | + |
| 76 | +Some ``Store`` (but not all) natively supports this features. When they don't, |
| 77 | +you can decorate it with the ``RetryTillSaveStore``. |
| 78 | + |
| 79 | +.. code-block:: php |
| 80 | +
|
| 81 | + use Symfony\Component\Lock\Store\RedisStore; |
| 82 | + use Symfony\Component\Lock\Store\RetryTillSaveStore; |
| 83 | +
|
| 84 | + $store = new RedisStore(new \Predis\Client('tcp://localhost:6379')); |
| 85 | + $store = new RetryTillSaveStore($store); |
| 86 | +
|
| 87 | + $lock = $store->createLock('hello'); |
| 88 | +
|
| 89 | + $lock->acquire(true); |
| 90 | +
|
| 91 | +
|
| 92 | +
|
| 93 | +Expirable Locks |
| 94 | +--------------- |
| 95 | + |
| 96 | +Working with a remote ``Store`` is hard. In oposition to local ``Stores`` |
| 97 | +(like :ref:`FlockStore <lock-store-flock>` or :ref:`SemaphoreStore <lock-store-semaphore>`) there is now way for the remote |
| 98 | +``Store`` to know whether or not the locker process is till alive. Due tu bugs, |
| 99 | +fatal errors or segmentation fault, we can't garentee that the ``release()`` |
| 100 | +function will be called, which would cause a ``resource`` to be locked |
| 101 | +infinitely. |
| 102 | + |
| 103 | +To fill this gap, the remote ``Stores`` provide an expirable mechanism: The lock |
| 104 | +is acquired for a defined amount of time (named TTL for Time To Live). |
| 105 | +When the timeout occured, the lock is automatically released even if the locker |
| 106 | +don't call the ``release()`` method. |
| 107 | + |
| 108 | +That's why, when you create a lock on an expirable ``Store``. You have to choose |
| 109 | +carrefully the correct TTL. When too low, you take the risk to "loose" the lock |
| 110 | +(and someone else acquire it) wheras you don't finish your task. |
| 111 | +When too hight and the process crash before you call the ``release()`` method, |
| 112 | +the ``resource`` will stay lock till the timeout. |
| 113 | + |
| 114 | + |
| 115 | +.. code-block:: php |
| 116 | +
|
| 117 | + use Symfony\Component\Lock\Store\RedisStore; |
| 118 | +
|
| 119 | + $store = new RedisStore(new \Predis\Client('tcp://localhost:6379')); |
| 120 | +
|
| 121 | + $lock = $store->createLock('hello', 30); |
| 122 | +
|
| 123 | + $lock->acquire(); |
| 124 | + try { |
| 125 | + // perfom a job during less than 30 seconds |
| 126 | + } finally { |
| 127 | + $lock->release() |
| 128 | + } |
| 129 | +
|
| 130 | +.. tip:: |
| 131 | + |
| 132 | + To avoid to let the Lock in a locking state, try to always release an |
| 133 | + expirable lock by wraping the job in a try/catch block for instance. |
| 134 | + |
| 135 | + |
| 136 | +When you have to work on a really long task, you should not set the TTL to |
| 137 | +overlaps the duration of this task. Instead, the Lock Component expose a |
| 138 | +:method:`Symfony\\Component\\Lock\\LockInterface::refresh` method in order to |
| 139 | +put off the TTL of the Lock. Thereby you can choose a small initial TTL, and |
| 140 | +regulary refresh the lock |
| 141 | + |
| 142 | +.. code-block:: php |
| 143 | +
|
| 144 | + use Symfony\Component\Lock\Store\RedisStore; |
| 145 | +
|
| 146 | + $store = new RedisStore(new \Predis\Client('tcp://localhost:6379')); |
| 147 | +
|
| 148 | + $lock = $store->createLock('hello', 30); |
| 149 | +
|
| 150 | + $lock->acquire(); |
| 151 | + try { |
| 152 | + while (!$finished) { |
| 153 | + // perfom a small part of the job. |
| 154 | +
|
| 155 | + $lock->refresh(); |
| 156 | + // resource is locked for 30 more seconds. |
| 157 | + } |
| 158 | + } finally { |
| 159 | + $lock->release() |
| 160 | + } |
| 161 | +
|
| 162 | +
|
| 163 | +Available Stores |
| 164 | +---------------- |
| 165 | + |
| 166 | +``Stores`` are classes that implement :class:`Symfony\\Component\\Lock\\StoreInterface`. |
| 167 | +This component provides several adapters ready to use in your applications. |
| 168 | + |
| 169 | +Here is a small summary of availanble ``Stores`` and theire capabilities. |
| 170 | + |
| 171 | ++----------------------------------------------+--------+----------+-----------+ |
| 172 | +| Store | Scope | Blocking | Expirable | |
| 173 | ++==============================================+========+==========+===========+ |
| 174 | +| :ref:`FlockStore <lock-store-flock>` | local | yes | no | |
| 175 | ++----------------------------------------------+--------+----------+-----------+ |
| 176 | +| :ref:`MemcachedStore <lock-store-memcached>` | remote | no | yes | |
| 177 | ++----------------------------------------------+--------+----------+-----------+ |
| 178 | +| :ref:`RedisStore <lock-store-redis>` | remote | no | yes | |
| 179 | ++----------------------------------------------+--------+----------+-----------+ |
| 180 | +| :ref:`SemaphoreStore <lock-store-semaphore>` | local | yes | no | |
| 181 | ++----------------------------------------------+--------+----------+-----------+ |
| 182 | + |
| 183 | +.. tip:: |
| 184 | + |
| 185 | + Calling the :method:`Symfony\\Component\\Lock\\LockInterface::refresh` |
| 186 | + method on a Lock created from a non expirable ``Store`` like |
| 187 | + :ref:`FlockStore <lock-store-flock>` will do nothing. |
| 188 | + |
| 189 | +.. _lock-store-flock: |
| 190 | + |
| 191 | +FlockStore |
| 192 | +~~~~~~~~~~ |
| 193 | + |
| 194 | +The FlockStore use the fileSystem on the local computer to lock and store the |
| 195 | +``resource``. |
| 196 | +It does not supports expiration, but the lock is automaticaly released when the |
| 197 | +PHP process is terminated. |
| 198 | + |
| 199 | + |
| 200 | +.. code-block:: php |
| 201 | +
|
| 202 | + use Symfony\Component\Lock\Store\FlockStore; |
| 203 | +
|
| 204 | + $store = new FlockStore(sys_get_temp_dir()); |
| 205 | +
|
| 206 | +The first argument of the constructor is the path to the directory where the |
| 207 | +file will be created. |
| 208 | + |
| 209 | +.. caution:: |
| 210 | + |
| 211 | + Beware, some filesystem (like some version of NFS) does not support locking. |
| 212 | + We suggest to use local file, or to use a Store dedicated to remote usage |
| 213 | + like Redis or Memcached. |
| 214 | + |
| 215 | + |
| 216 | +.. _Packagist: https://packagist.org/packages/symfony/lock |
| 217 | + |
| 218 | +.. _lock-store-memcached: |
| 219 | + |
| 220 | +MemcachedStore |
| 221 | +~~~~~~~~~~~~~~ |
| 222 | + |
| 223 | +The MemcachedStore stores state of ``resource`` in a Memcached server. This |
| 224 | +``Store`` does not support blocking, and expect a TLL to avoid infinity locks. |
| 225 | + |
| 226 | +.. note:: |
| 227 | + |
| 228 | + Memcached does not supports TTL lower than 1 seconds. |
| 229 | + |
| 230 | + |
| 231 | +It requires to have installed Memcached and have created a connection that |
| 232 | +implements the ``\Memcached`` classes:: |
| 233 | + |
| 234 | +.. code-block:: php |
| 235 | +
|
| 236 | + use Symfony\Component\Lock\Store\RedisStore; |
| 237 | +
|
| 238 | + $memcached = new \Memcached(); |
| 239 | + $memcached->addServer('localhost', 11211); |
| 240 | +
|
| 241 | + $store = new MemcachedStore($memcached); |
| 242 | +
|
| 243 | +.. _lock-store-redis: |
| 244 | + |
| 245 | +RedisStore |
| 246 | +~~~~~~~~~~ |
| 247 | + |
| 248 | +The RedisStore uses an instance of Redis to store the state of the ``resource``. |
| 249 | +This ``Store`` does not support blocking, and expect a TLL to avoid infinity |
| 250 | +locks. |
| 251 | + |
| 252 | +It requires to have installed Redis and have created a connection that |
| 253 | +implements the ``\Redis``, ``\RedisArray``, ``\RedisCluster`` or ``\Predis`` |
| 254 | +classes:: |
| 255 | + |
| 256 | +.. code-block:: php |
| 257 | +
|
| 258 | + use Symfony\Component\Lock\Store\RedisStore; |
| 259 | +
|
| 260 | + $redis = new \Redis(); |
| 261 | + $redis->connect('localhost'); |
| 262 | +
|
| 263 | + $store = new RedisStore($redis); |
| 264 | +
|
| 265 | +.. _lock-store-semaphore: |
| 266 | + |
| 267 | +SemaphoreStore |
| 268 | +~~~~~~~~~~~~~~ |
| 269 | + |
| 270 | +The SemaphoreStore uses the PHP semaphore function to lock a ``resources``. |
| 271 | + |
| 272 | + |
| 273 | +.. code-block:: php |
| 274 | +
|
| 275 | + use Symfony\Component\Lock\Store\SemaphoreStore; |
| 276 | +
|
| 277 | + $store = new SemaphoreStore($redis); |
| 278 | +
|
| 279 | +.. _lock-store-combined: |
| 280 | + |
| 281 | +CombinedStore |
| 282 | +~~~~~~~~~~~~~ |
| 283 | + |
| 284 | +The CombinedStore synchronize several ``Stores`` together. When it's used to |
| 285 | +acquired a Lock, it forward the call to the managed ``Stores``, and regarding the |
| 286 | +result, uses a quorum to decide whether or not the lock is acquired. |
| 287 | + |
| 288 | +.. note:: |
| 289 | + |
| 290 | + This ``Store`` is usefull for High availability application. You can provide |
| 291 | + several Redis Server, and use theses server to manage the Lock. A |
| 292 | + MajorityQuorum is enougth to safly acquire a lock while it allow some Redis |
| 293 | + server failure. |
| 294 | + |
| 295 | +.. code-block:: php |
| 296 | +
|
| 297 | + use Symfony\Component\Lock\Quorum\MajorityQuorum; |
| 298 | + use Symfony\Component\Lock\Store\CombinedStore; |
| 299 | + use Symfony\Component\Lock\Store\RedisStore; |
| 300 | +
|
| 301 | + $stores = []; |
| 302 | + foreach (['server1', 'server2', 'server3'] as $server) { |
| 303 | + $redis= new \Redis(); |
| 304 | + $redis->connect($server); |
| 305 | +
|
| 306 | + $stores[] = new RedisStore($redis); |
| 307 | + } |
| 308 | +
|
| 309 | + $store = new CombinedStore($stores, new MajorityQuorum()); |
| 310 | +
|
| 311 | +
|
| 312 | +.. tip:: |
| 313 | + |
| 314 | + You can use the CombinedStore with the UnanimousQuorum to implement chained |
| 315 | + ``Stores``. It'll allow you to acquire easy local locks before asking for a |
| 316 | + remote lock |
| 317 | + |
| 318 | + |
| 319 | +.. _Packagist: https://packagist.org/packages/symfony/lock |
0 commit comments