In my previous post, I talked about creating a simple scheduler service using the .NET Background service. The scheduler service would work well as long as we only have a single instance of worker deployed. However, if there are more than one worker nodes, it would cause the scheduler to run once for every worker instance, which is not desirable. We can fix this issue using a distributed lock.
All the major data store vendors, such as Oracle, SQL Server, Redis, etc., provide us with a way to create a distributed lock.
In this post, I will explain how you can create distributed lock in .NET using PostgreSQL.
Why do we need a distributed lock?
The distributed lock ensures that only one node or instance of our service performs the task. That task might be storing data in a data store, calling an API, publishing an event, and so on. With distributed lock, the task is performed only once, improving efficiency and ensuring correctness.
Distributed lock using PostgreSQL
We can create distributed lock using PostgreSQL through advisory locks. From the documentation:
These are called advisory locks, because the system does not enforce their use — it is up to the application to use them correctly. Advisory locks can be useful for locking strategies that are an awkward fit for the MVCC model. Once acquired, an advisory lock is held until explicitly released or the session ends.
You can follow this GitHub repository for the source code of the implementation.
Let us first start with creating a sample PostgreSQL DB using the docker run command.
docker run --name distributed-lock-test -e POSTGRES_USER=dbUser -e POSTGRES_DB=distributed-lock-db -e POSTGRES_PASSWORD=password -p 5432:5432 -d postgres
Next, let us create an empty class library project. We connect to PostgreSQL using a popular Nuget package, Npgsql and log through default Microsoft.Extension.Logging
Next, we create a class DistributedLock, as shown below:
Let us deep dive, into the code. We have a single public method, TryExecuteInDistributedLock, that takes lockId and exclusiveLockTask
as parameters. Within the method where we do the following:
- First, we try to acquire a session lock for the passed lock id. To acquire the lock, we call PostgreSQL in-built function pg_try_advisory_lock
- If the lock is not acquired successfully, we return false, indicating that we could not acquire the session lock and the task was not executed.
- If the lock is acquired successfully, we run the exclusiveLockTask.
- Once exclusiveLockTask is executed successfully, we release the lock by calling function pg_advisory_unlock so that it can be acquired later.
To validate our implementation, here is the test:
As you can see in the above code, we have tried to simulate a distributed scenario by creating five nodes that try to acquire a distributed lock simultaneously. However, only one node is successful in acquiring the lock and executing the task. The output of the above test run is:
This post explains how you can create a distributed lock using PostgreSQL and ensure that a task is executed only once.