Practical PHP Patterns: Pessimistic Offline Lock
Join the DZone community and get the full member experience.Join For Free
In this series, we are analyzing concurrency patterns, a way to allow multiple users to access application data without accepting conflicting changes which derive from stale data (like two people submitting the same editing form).
We also introduced the dichotomy between database transactions, which are natively supported by the database systems, and business transactions, which span over multiple database transactions and should be modelled by us. The Optimistic Offline Lock was a way of solving the problem when the chances of conflict are low, by simply detecting it and rolling back.
The Pessimistic Offline Lock is a more powerful and complex solution, which prescribes that only one business transaction at the time is allowed to check out a particular item.
How it works
While the optimistic lock uses the source control system metaphor, this pattern is adopted for example by desktop programs when only one instance of them should be in execution at any time.
The first transaction acquires a lock on the item of interest. It accomplishes its work, via user interaction, and finally after it is submitted, the lock is released so that other transactions on the same data may begin.
Fowler defines three steps to implement a pessimistic lock system on your data.
1. Decide what type of lock you want
The standard types of lock are:
- exclusive write: you should acquire a lock when you have the need to edit data.
- exclusive read: you have to acquire a lock to read a record, which is very restrictive. However, it means no one has stale data.
- read/write lock: mutually exclusive read and write lock; no one can read if I'm writing, and no one can write if I'm reading. However concurrent read locks are acceptable: there is no problem since it does not modify the state of the application and it provides greater concurrency. Viewing operations are much more common than editing ones.
2. Lock manager
The second step is to built a little lock manager, which may be as simple as a two-column table.
The lock manager is a Facade that totally encapsulates the data source. An unique identifier for every transaction is used to distinguish between them; is is usually associated with the session since in every session the user is commonly performing only one transaction at the time: it can be a username, a numerical id, a session id...
The data contained in the lock manager is only an associative array which maps locks to owners. However, these data it must be shared between PHP scripts: it can be kept in a database table or in other storages for application-wide state.
The last step is to define the contract that must be respected by transactions. This comprehends decisions dependent on the business domain. For example you should decide:
- what to lock;
- at what point of the business transaction;
- when to release a lock;
- what to do in case of errors.
The usual implementation of the pattern prescribes some best practices like checking that the data is not stale (current) after having issued a lock, and using the unique identifier/primary key for identifying what you're locking. The lock is commonly released after the completion of the transaction, which it is aborted in case of error.
Keep an eye on concurrency issues on the lock shared state (this is kind of a recursive problem). Use database mechanisms like unique constraints on locked objects id columns in case of simple lock types.
This pattern prevents multiple rollbacks and conflicts when the chance of them happening is high. When merging changes it is not viable, it is the only choice; this also apply when the cost of conflicts is high (much work has been completed on stale data and should be thrown away.)
This pattern eliminates concurrency altogether, and may raise data contention problems. It is also rather complex to implement with regard to the optimistic lock.
Other issues that arise with pessimistic locks are deadlocks: avoid them by simply raise an error when a lock isn't available, or by allowing only one lock per user.
Moreover, there is the problem of lost sessions: you should set up timeouts so that locks from dead clients can be removed after a waiting period. The issue is particularly perceived in web applications since they are abandoned by users without an explicit command. These days you can set up some sort of Ajax "keepalive" ping from the client to the server, that when cease to happen is the sign of a ended session.
Doctrine 2 as an ORM supports pessimistic locking at the low level: SQL commands are used to acquire locks on single rows. As so, no additional metadata on your entities is necessary.
This implementation can be used only when the transaction takes place in only one PHP script, but it prevents other scripts from tampering with your rows while you're in between SELECT and UPDATE queries.
However, you have to get the underlying connection (EntityManager::getConnection()) and start a transaction around the work you are performing.
The types of lock supported are:
- Pessimistic Write (Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE), which locks the underlying database rows for concurrent read and write Operations.
- Pessimistic Read (Doctrine\DBAL\LockMode::PESSIMISTIC_READ), which locks other concurrent requests that attempt to update or lock rows in write mode.
EntityManager::find() and EntityManager::lock() or Query::setLockMode() are the methods that accept parameters option that influence pessimistic locking.
Opinions expressed by DZone contributors are their own.