|
| 1 | +.. index:: |
| 2 | + single: Lock |
| 3 | + single: Components; Lock |
| 4 | + |
| 5 | +The Lock Component |
| 6 | +================== |
| 7 | + |
| 8 | + The Lock Component creates and manages `locks`_, a mechanism to provide |
| 9 | + exclusive access to a shared resource. |
| 10 | + |
| 11 | +.. versionadded:: 3.4 |
| 12 | + The Lock component was introduced in Symfony 3.4. |
| 13 | + |
| 14 | +Installation |
| 15 | +------------ |
| 16 | + |
| 17 | +You can install the component in 2 different ways: |
| 18 | + |
| 19 | +* :doc:`Install it via Composer </components/using_components>` (``symfony/lock`` on `Packagist`_); |
| 20 | +* Use the official Git repository (https://github.com/symfony/lock). |
| 21 | + |
| 22 | +.. include:: /components/require_autoload.rst.inc |
| 23 | + |
| 24 | +Usage |
| 25 | +----- |
| 26 | + |
| 27 | +Locks are used to guarantee exclusive access to some shared resource. In |
| 28 | +Symfony applications, you can use locks for example to ensure that a command is |
| 29 | +not executed more than once at the same time (on the same or different servers). |
| 30 | + |
| 31 | +In order to manage the state of locks, a ``Store`` needs to be created first |
| 32 | +and then use the :class:`Symfony\\Component\\Lock\\Factory` class to actually |
| 33 | +create the lock for some resource:: |
| 34 | + |
| 35 | + use Symfony\Component\Lock\Factory; |
| 36 | + use Symfony\Component\Lock\Store\SemaphoreStore; |
| 37 | + |
| 38 | + $store = new SemaphoreStore(); |
| 39 | + $factory = new Factory($store); |
| 40 | + |
| 41 | +Then, a call to the :method:`Symfony\\Component\\Lock\\LockInterface::acquire` |
| 42 | +method will try to acquire the lock. Its first argument is an arbitrary string |
| 43 | +that represents the locked resource:: |
| 44 | + |
| 45 | + // ... |
| 46 | + $lock = $factory->createLock('pdf-invoice-generation'); |
| 47 | + |
| 48 | + if ($lock->acquire()) { |
| 49 | + // The resource "pdf-invoice-generation" is locked. |
| 50 | + // You can compute and generate invoice safely here. |
| 51 | + |
| 52 | + $lock->release(); |
| 53 | + } |
| 54 | + |
| 55 | +If the lock can not be acquired, the method returns ``false``. The ``acquire()`` |
| 56 | +method can be safely called repeatedly, even if the lock is already acquired. |
| 57 | + |
| 58 | +.. note:: |
| 59 | + |
| 60 | + Unlike other implementations, the Lock Component distinguishes locks |
| 61 | + instances even when they are created for the same resource. If a lock has |
| 62 | + to be used by several services, they should share the same ``Lock`` instance |
| 63 | + returned by the ``Factory::createLock`` method. |
| 64 | + |
| 65 | +Blocking Locks |
| 66 | +-------------- |
| 67 | + |
| 68 | +By default, when a lock cannot be acquired, the ``acquire`` method returns |
| 69 | +``false`` immediately. To wait (indefinitely) until the lock |
| 70 | +can be created, pass ``true`` as the argument of the ``acquire()`` method. This |
| 71 | +is called a **blocking lock** because the execution of your application stops |
| 72 | +until the lock is acquired. |
| 73 | + |
| 74 | +Some of the built-in ``Store`` classes support this feature. When they don't, |
| 75 | +they can be decorated with the ``RetryTillSaveStore`` class:: |
| 76 | + |
| 77 | + use Symfony\Component\Lock\Factory; |
| 78 | + use Symfony\Component\Lock\Store\RedisStore; |
| 79 | + use Symfony\Component\Lock\Store\RetryTillSaveStore; |
| 80 | + |
| 81 | + $store = new RedisStore(new \Predis\Client('tcp://localhost:6379')); |
| 82 | + $store = new RetryTillSaveStore($store); |
| 83 | + $factory = new Factory($store); |
| 84 | + |
| 85 | + $lock = $factory->createLock('notification-flush'); |
| 86 | + $lock->acquire(true); |
| 87 | + |
| 88 | +Expiring Locks |
| 89 | +-------------- |
| 90 | + |
| 91 | +Locks created remotely are difficult to manage because there is no way for the |
| 92 | +remote ``Store`` to know if the locker process is still alive. Due to bugs, |
| 93 | +fatal errors or segmentation faults, it cannot be guaranteed that ``release()`` |
| 94 | +method will be called, which would cause the resource to be locked infinitely. |
| 95 | + |
| 96 | +The best solution in those cases is to create **expiring locks**, which are |
| 97 | +released automatically after some amount of time has passed (called TTL for |
| 98 | +*Time To Live*). This time, in seconds, is configured as the second argument of |
| 99 | +the ``createLock()`` method. If needed, these locks can also be released early |
| 100 | +with the ``release()`` method. |
| 101 | + |
| 102 | +The trickiest part when working with expiring locks is choosing the right TTL. |
| 103 | +If it's too short, other processes could acquire the lock before finishing the |
| 104 | +job; it it's too long and the process crashes before calling the ``release()`` |
| 105 | +method, the resource will stay locked until the timeout:: |
| 106 | + |
| 107 | + // ... |
| 108 | + // create an expiring lock that lasts 30 seconds |
| 109 | + $lock = $factory->createLock('charts-generation', 30); |
| 110 | + |
| 111 | + $lock->acquire(); |
| 112 | + try { |
| 113 | + // perform a job during less than 30 seconds |
| 114 | + } finally { |
| 115 | + $lock->release(); |
| 116 | + } |
| 117 | + |
| 118 | +.. tip:: |
| 119 | + |
| 120 | + To avoid letting the lock in a locking state, it's recommended to wrap the |
| 121 | + job in a try/catch/finally block to always try to release the expiring lock. |
| 122 | + |
| 123 | +In case of long-running tasks, it's better to start with a not too long TTL and |
| 124 | +then use the :method:`Symfony\\Component\\Lock\\LockInterface::refresh` method |
| 125 | +to reset the TTL to its original value:: |
| 126 | + |
| 127 | + // ... |
| 128 | + $lock = $factory->createLock('charts-generation', 30); |
| 129 | + |
| 130 | + $lock->acquire(); |
| 131 | + try { |
| 132 | + while (!$finished) { |
| 133 | + // perform a small part of the job. |
| 134 | + |
| 135 | + // renew the lock for 30 more seconds. |
| 136 | + $lock->refresh(); |
| 137 | + } |
| 138 | + } finally { |
| 139 | + $lock->release(); |
| 140 | + } |
| 141 | + |
| 142 | +Available Stores |
| 143 | +---------------- |
| 144 | + |
| 145 | +Locks are created and managed in ``Stores``, which are classes that implement |
| 146 | +:class:`Symfony\\Component\\Lock\\StoreInterface`. The component includes the |
| 147 | +following built-in store types: |
| 148 | + |
| 149 | + |
| 150 | +============================================ ====== ======== ======== |
| 151 | +Store Scope Blocking Expiring |
| 152 | +============================================ ====== ======== ======== |
| 153 | +:ref:`FlockStore <lock-store-flock>` local yes no |
| 154 | +:ref:`MemcachedStore <lock-store-memcached>` remote no yes |
| 155 | +:ref:`RedisStore <lock-store-redis>` remote no yes |
| 156 | +:ref:`SemaphoreStore <lock-store-semaphore>` local yes no |
| 157 | +============================================ ====== ======== ======== |
| 158 | + |
| 159 | +.. _lock-store-flock: |
| 160 | + |
| 161 | +FlockStore |
| 162 | +~~~~~~~~~~ |
| 163 | + |
| 164 | +The FlockStore uses the file system on the local computer to create the locks. |
| 165 | +It does not support expiration, but the lock is automatically released when the |
| 166 | +PHP process is terminated:: |
| 167 | + |
| 168 | + use Symfony\Component\Lock\Store\FlockStore; |
| 169 | + |
| 170 | + // the argument is the path of the directory where the locks are created |
| 171 | + $store = new FlockStore(sys_get_temp_dir()); |
| 172 | + |
| 173 | +.. caution:: |
| 174 | + |
| 175 | + Beware that some file systems (such as some types of NFS) do not support |
| 176 | + locking. In those cases, it's better to use a directory on a local disk |
| 177 | + drive or a remote store based on Redis or Memcached. |
| 178 | + |
| 179 | +.. _lock-store-memcached: |
| 180 | + |
| 181 | +MemcachedStore |
| 182 | +~~~~~~~~~~~~~~ |
| 183 | + |
| 184 | +The MemcachedStore saves locks on a Memcached server, it requires a Memcached |
| 185 | +connection implementing the ``\Memcached`` class. This store does not |
| 186 | +support blocking, and expects a TTL to avoid stalled locks:: |
| 187 | + |
| 188 | + use Symfony\Component\Lock\Store\MemcachedStore; |
| 189 | + |
| 190 | + $memcached = new \Memcached(); |
| 191 | + $memcached->addServer('localhost', 11211); |
| 192 | + |
| 193 | + $store = new MemcachedStore($memcached); |
| 194 | + |
| 195 | +.. note:: |
| 196 | + |
| 197 | + Memcached does not support TTL lower than 1 second. |
| 198 | + |
| 199 | +.. _lock-store-redis: |
| 200 | + |
| 201 | +RedisStore |
| 202 | +~~~~~~~~~~ |
| 203 | + |
| 204 | +The RedisStore saves locks on a Redis server, it requires a Redis connection |
| 205 | +implementing the ``\Redis``, ``\RedisArray``, ``\RedisCluster`` or |
| 206 | +``\Predis`` classes. This store does not support blocking, and expects a TTL to |
| 207 | +avoid stalled locks:: |
| 208 | + |
| 209 | + use Symfony\Component\Lock\Store\RedisStore; |
| 210 | + |
| 211 | + $redis = new \Redis(); |
| 212 | + $redis->connect('localhost'); |
| 213 | + |
| 214 | + $store = new RedisStore($redis); |
| 215 | + |
| 216 | +.. _lock-store-semaphore: |
| 217 | + |
| 218 | +SemaphoreStore |
| 219 | +~~~~~~~~~~~~~~ |
| 220 | + |
| 221 | +The SemaphoreStore uses the `PHP semaphore functions`_ to create the locks:: |
| 222 | + |
| 223 | + use Symfony\Component\Lock\Store\SemaphoreStore; |
| 224 | + |
| 225 | + $store = new SemaphoreStore(); |
| 226 | + |
| 227 | +.. _lock-store-combined: |
| 228 | + |
| 229 | +CombinedStore |
| 230 | +~~~~~~~~~~~~~ |
| 231 | + |
| 232 | +The CombinedStore is designed for High Availability applications because it |
| 233 | +manages several stores in sync (for example, several Redis servers). When a lock |
| 234 | +is being acquired, it forwards the call to all the managed stores, and it |
| 235 | +collects their responses. If a simple majority of stores have acquired the lock, |
| 236 | +then the lock is considered as acquired; otherwise as not acquired:: |
| 237 | + |
| 238 | + use Symfony\Component\Lock\Strategy\ConsensusStrategy; |
| 239 | + use Symfony\Component\Lock\Store\CombinedStore; |
| 240 | + use Symfony\Component\Lock\Store\RedisStore; |
| 241 | + |
| 242 | + $stores = []; |
| 243 | + foreach (array('server1', 'server2', 'server3') as $server) { |
| 244 | + $redis= new \Redis(); |
| 245 | + $redis->connect($server); |
| 246 | + |
| 247 | + $stores[] = new RedisStore($redis); |
| 248 | + } |
| 249 | + |
| 250 | + $store = new CombinedStore($stores, new ConsensusStrategy()); |
| 251 | + |
| 252 | +Instead of the simple majority strategy (``ConsensusStrategy``) an |
| 253 | +``UnanimousStrategy`` can be used to require the lock to be acquired in all |
| 254 | +the stores. |
| 255 | + |
| 256 | +.. _`locks`: https://en.wikipedia.org/wiki/Lock_(computer_science) |
| 257 | +.. _Packagist: https://packagist.org/packages/symfony/lock |
| 258 | +.. _`PHP semaphore functions`: http://php.net/manual/en/book.sem.php |
0 commit comments