When it comes to concurrency control, there are specifically two mechanisms around it - pessimistic and optimistic locking.
The optimistic locking model is a concurrency control technique in which multiple users are allowed to update the same record without informing the users that others are also attempting to update the record. The record changes are validated only when the record is committed. If one user successfully updates the record, the other users attempting to commit their concurrent updates are informed that a conflict exists.
An advantage of the optimistic locking model is that it avoids the overhead of locking a record for the duration of the action. If there are no simultaneous updates, then this model provides fast updates.
The pessimistic locking model prevents simultaneous updates to records. As soon as one user starts to update a record, a lock is placed on it. Other users who attempt to update this record are informed that another user has an update in progress. The other users must wait until the first user has finished committing their changes, thereby releasing the record lock. Only then can another user make changes based on the previous user’s changes.
An advantage of the pessimistic locking model is that it avoids the issue of conflict resolution by preventing conflicts. Updates are serialized and each subsequent update starts with the committed record changes from the previous user.
provides support for row-level locking using the
lock! method wrapped inside a transaction for example:-
If two users press the like button for an article,
at the same time then instead of the like_count of that article going up to 2,
it will only increment to 1, because both users pressed increment
from 0 to 1 at the same time.
To fix this we can use
lock! wrapped inside a transaction.
What the code does here is first, it starts a database transaction. Second, it acquires a pessimistic database lock. Once the lock is acquired, the record is reloaded in memory, so that the values on the record match those in the locked database row. The lock will prevent others from reading or writing to that row and anyone else trying to acquire a lock will have to wait for the lock to be released.
Also, we can pass various
lock! method as supported by the underlying Database
for example, we used
FOR UPDATE NOWAIT on Postgres DB
what it means is other transactions that attempt UPDATE, DELETE,
or SELECT FOR UPDATE on this row will be blocked
until the current transaction ends
and suppose another transaction tries to acquire a lock on the same record
then it will result in the below error.
We can make it more concise using
with_lock does the same thing it creates a transaction
and applies a lock on the record under the hood.
But, before Rails 7
there was no way to specify transaction arguments like
so, for example before Rails 7
if we had to create the nested transactions
we had to use multiple transaction blocks with
After Rails 7 we can use
with_lock to create nested transactions
and can specify transaction optional arguments like:-
requires_new: If this is set to
truethen the block will be wrapped in a database savepoint acting as a sub-transaction.
isolation: We can specify the Isolation Levels to avoid dirty reads.
joinable: This can be set to
falseto avoid surprises while dealing with custom nested transactions
Please refer to this PR.