Pessimistic locking in practice
Writing an app is already a challenge. Writing a concurrent-resistant app is twice as challenging. It involves coordinating multiple threads, handling the synchronization to avoid race conditions and deadlocks. Not a surprise that designing a concurrent program that is scalable, efficient, and maintains high levels of performance can be a complex task.
As a result, developers just tend not to do it. Avoid it until it bites you😁. However, at some point, the non-deterministic behavior of your program can lead to unexpected outcomes and bugs. No matter, how many if/else statements you write, sooner or later you will face and will be forced to deal with locks. So why not learn about it beforehand 😉
In this story, you will see when pessimistic locking can be applied. The problems it solves. Its benefits and drawbacks. Scaling issues. And most importantly, how to implement it in practice.
Don’t forget to check out the previous article in the series:
- Optimistic locking in practice
- Pessimistic locking in practice
Meanwhile, we are starting.
Have you ever seen the auction in action? There is a lot for sale, let’s say Jaconda, with a starting price. People compete for it by hiring its cost. The time is limited, so everybody is under pressure there. They rapidly have to decide whether the current item worth their money. Most of the time, those decisions are spontaneous, impulsive, thoughtless, risky, nervous, and wrong. But that is the whole point 😃!
People used to gather together, drink some whiskey, and tell jokes. These days, everything is online. 😒 Wait! Why the hell am I complaining? It is just a perfect world for me, an introvert 😃
And for you, my dear developer, since our imaginary auction system going online too. You have the chance to work on it. As always we have the lot and at least 100 people voting for it. Sounds, easy, isn’t it? Let’s see how it actually behaves.
So, a new, shiny lot appears on a screen. It happens to be an ancient flower pot. Among all those hundred users only Alice and Bob found it really attractive. If not only the price 50$! They are hesitating, but time is slowly running out. Tick-tock, tick-tock.⏰ The next few actions were so sudden, that it is better to describe them in sloooo-mo:
- At the very last moment, Alice finally makes a decision and is willing to pay 100$ for it.
- At the same time, Bob also bid 60$ on it.
- Time ran out and we saw the winner… His name is Robert Paulson.
So Bob won the lot. Imagine the frustrated face of Alice when she sees that.
Of course, the flower pot should belong to her. She was the first to vote. She put a higher price. However, somehow our system decided differently. In real life that would never be the case. Let’s see what went wrong and how it can be improved.
Finishing your morning coffee, you open the code, which seems to cause the issue.
[ApiController]
[Route("items")]
public class ItemController : ControllerBase
{
[HttpPost]
public void PlaceBid(int itemId, decimal amount)
{
var item = _dbContext.Items.Find(itemId);
// Check if the bid is higher than the current highest bid
if (amount > item.CurrentBidAmount)
{
// Update the item's bid
item.CurrentBidAmount = amount;
item.CurrentBidUserId = _userService.GetCurrentUserId();
}
_dbContext.SaveChanges();
}
}The code is neat. You would not see any flaws with the unaided eye. Sadly, it does not work 😔. Can you find the race condition here?
If you have two requests running in parallel, they can simultaneously load the same item with a bid of 50$. Update it. Then the last one, which saves the data, will override the previous bid. This is definitely wrong.
In an auction system, when a user places a bid on an item, the system needs to ensure that no other user can place a bid on the same item while the first user have not finished yet. This can be achieved using pessimistic locking.
Let’s see how it works:
- Alice starts a business transaction by getting a record №19 and locks it.
- Bob tries to get the same record №19, but since it is locked, he gets an error (or waits until it is released).
- Alice updates her changes and frees the lock. The action is done, the item is available and can be used by other users.
If Alice updates the record first, then Bob will retrieve updated data and won’t be able to place a bit since his bet is lower.
This way, the user gets an error at the beginning of a transaction, not at its end, as it is done with optimistic locking.
For example, in our case, when a user clicks the “Place Bid” button for an item, the server-side code can acquire a lock on the item using a shared object:
[ApiController]
[Route("items")]
public class ItemController : ControllerBase
{
// SyncRoot
public static object PlaceBidSyncLock = new object();
[HttpPost]
public void PlaceBid(int itemId, decimal amount)
{
lock (PlaceBidSyncLock)
{
// the code to get and update a bid goes here
}
}
}By acquiring a lock on the item, the server ensures that no other user can place a bid on the same item at the same time, preventing conflicts and ensuring the integrity of the auction process.
Just lock the entire request. That’s it! Once again, the beast is not as terrible as he is painted, isn’t it? 😏
As you can see, pessimistic locking solves the problem of parallel usage, by preventing multiple changes from being done at once. Requests won’t overlap and be handled one by one.
A bonus section for the smartest 🎁
Even though the lock operator is great, you won’t find it much in your code. A better approach would be to wrap it with a service. This way, logic is encapsulated, can be easily unit tested, and all the other beauty of the abstractions 😏
For example, the lock can be replaced with a such interface:
interface ILockService
{
void AcquireLock(string key, TimeSpan lockTimeout);
void ReleaseLock(string key);
}Notice, that when acquiring a lock, we have a timeout for it. This is done so our system does not lock resources infinitely long in case it unexpectedly goes down or simply because a developer forgets to release it. It is kind of crucial for a system to release itself from a locked state.
Get iamprovidence’s stories in your inbox
Join Medium for free to get updates from this writer.
In terms of implementation, it can vary. Here is my shitty, non-generic, without any retries, thread-unsafe implementation:
class InProcessLockService : ILockService
{
private readonly IMemoryCache _cache;
public void AcquireLock(string key, TimeSpan timeout)
{
if (_cache.Get(key) is not null)
{
throw new LockCanNotBeAcquiredException();
}
_cache.Set(key, value: key, new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = timeout,
});
}
public void ReleaseLock(string key)
{
_cache.Remove(key);
}
}Please, don’t use it on the real projects 😅. It is just an example for you, so you have some inspiration.
Keep in mind, such a service should be singleton for our system!
services.AddSingleton<ILockService, InProcessLockService>();Similarly how it was with our SyncRoot.
Another bonus for the scalest 🎁
The previous implementation works great when we have a single instance of the server. However, when you scale your application horizontally, this won’t be enough.
Say, you have two instances of the same application. Locking a request in one service won’t lock it for another. You’ll need to use a distributed locking mechanism that works across multiple nodes, meaning to have a singleton component in your architecture.
Typically Redis is used for that because it hella-fast. You can rewrite the previous implementation with just a few lines of changes by replacing IMemoryCache with IDistributedCache.
However, if you don’t want to introduce new infrastructure, you can use SQL Server or other options.
The final bonus for those who seek the sweetest 🎁 🍫😅
Let’s make the final improvement to our service. Here is the usage of it:
[ApiController]
[Route("items")]
public class ItemController : ControllerBase
{
[HttpPost]
public void PlaceBid(int itemId, decimal amount)
{
var key = $"place-bid-{itemId}";
try
{
_lockService.AcquireLock(key, TimeSpan.FromSeconds(2));
// the code to get and update a bid goes here
}
finally
{
_lockService.ReleaseLock(key);
}
}
}It causes nausea, isn’t it ?🤢 I like my code tidy and neat. However, because of the vital necessity of releasing the lock, we have to wrap it up with try/finally block. If only we could have something similar to thelock operator that can guarantee freeing resources without additional code. Wait a second! We actually have! 😃
Before going to actual implementation, let’s see what happens when the compiler sees the lock in our code. Simple lock statement get converted to that grumpy try/finally block s we have above. This is called lowering, when high-level construction during compilation gets removed and replaced with a much simpler, low-level one.
Our goal is similar, but the opposite. We want to get rid of try/finally. For that, we need to find another keyword that will be converted to it. Lucky we are, there is such a thing as the using operator! It guarantees that finally will be called regardless of how forgetful a developer is.
Therefore, we need to create a new class that will release the lock in the Dispose() method:
class LockingScope : IDisposable
{
private readonly ILockService _lockService;
private readonly string _key;
public LockingScope(ILockService lockService, string key)
{
_lockService = lockService;
_key = key;
_lockService.AcquireLock(key, TimeSpan.FromSeconds(2));
}
public void Dispose()
{
_lockService.ReleaseLock(_key);
}
}
class InProcessLockService : ILockService
{
public LockingScope CreateLockingScope(string key)
{
return new LockingScope(this, key);
}
. . .
}With that, we have accomplished our goal. Our code could not been cleaner before 🤩
[ApiController]
[Route("items")]
public class ItemController : ControllerBase
{
[HttpPost]
public void PlaceBid(int itemId, decimal amount)
{
using (_lockService.CreateLockingScope($"place-bid-{itemId}"))
{
// the code to get and update a bid goes here
}
}
}Just one last small suggestion. I know that using block can be simplified to using declaration. Do not do it for this case, even when your IDE suggests it. Having curly brackets defined makes the scope of the lock more visible and easier to handle.
Wrapping it up
Pessimistic locking prevents conflicts between transactions by avoiding them altogether. It forces a business transaction to acquire a lock on a piece of data before it starts to use it and releases it when the operation is complete.
- It’s called pessimistic because we are pessimists here and assume that the chance of conflict is very high.
- It fits best when there are few records and relatively many users. An example of such a case would be an auction system.
- It prevents multiple users from working on the same data.
- It costs more to do an operation since there is a lock to acquire.
- However, when a collision does occur, your users will be notified right away.
That is basically all you need to know, about pessimistic locking, to start applying it in your program 😉.
I highly suggest you check the article about optimistic locking. Pay attention to which conditions suit the best for which locking mechanism. How it affects your architecture. How it affects your entities. How it scales.
Check your project to see whether there is a place for it. Although, it does not mean you should apply it right away. I have seen numerous apps that require locking but work without it just fine 😃. It may be your case as well. At least now, when you have similar issues, you will know how to deal with them 😉.
That’s all for today, folks 🐷
I hope you enjoyed it and learned something new 📑
Don’t forget to clap, so I know you are interested 🎬
Follow to get more articles on concurrency ✅
You can also support me if you want to continue seeing those ☕️
Have a nice day 🌞 and keep coding 😉









