Distributed Locks Manager (C# and Redis)

Matt Ghafouri
CodeBoulevard
Published in
7 min readFeb 28, 2022

--

Distributed Locks Manager(C# and Redis)

Preface

In the previous article, I explained what is the concept of DLM and how it can be helpful in microservice architectures and scalable applications to control concurrency on shared resources. If you have not read the article yet, I highly recommend you to read that first.

Control concurrency for shared resources in distributed systems with DLM

Here, I’ve attempted to implement a Redis DLM with dotnet 6.0 and C# with the help of RedLock.net.

Distributed locks are a very useful primitive in many environments where different processes must operate with shared resources in a mutually exclusive way.

There are a number of libraries and blog posts describing how to implement a DLM (Distributed Lock Manager) with Redis, but every library uses a different approach, and many use a simple approach with lower guarantees compared to what can be achieved with slightly more complex designs.

As you may know, behind the scene Redis uses the Redlock algorithm to provide a DLM for us. But you don’t need to worry about the complexity of the implementation Because Redlock.net provides a reliable solution to implement that. Of course, there are alternatives for Redlock.net which are specific for Dotnet developers, but in case you use one of the other famous languages like Java, PHP, Go, Python, etc you can also find your specific language implementation here.

As this article tries to elaborate implementation of a DLM with C#, from now on, we will talk just about dotnet libraries.

Whenever you want to use a library as an external resource, it’s better to check some preconditions, to be acceptable for inclusion in projects.

First of all, find the library page on Github and check these parameters:

1- Number of Github stars.
2- Number of forked projects.
3- Frequency of updates.
4- Number of Contributors.

One of the primary factors which convinced me to use Redlock.net was having a retry pattern that is internally implemented and also an expiry time for each lock. It means in case a requested lock key could not be acquired by a request, the request will be queued for a specified time and also will be retried to acquire the lock periodically.

Good. Let’s talk about the implementation of the DLM.

distributed lock manager with c# and Redis and dotnet 6 and RedLock
Distributed lock manager with Redis

In this implementation that is written in C# language and dotnet 6.0, there are three major parts:

1- Client scope

2- Application scope

a. Controller: Responsible to receive and route the requests from clients to the application service.b. Application Service: contains all the business logic and also application services are the consumer of the DLMc. Cache service: An abstraction on StackExchange.Redis library to encapsulate functionalities that we need to interact with Redis Cache.

3- Infrastructure (Clustered Redis cache)

Solution Structure

distributed lock manager with c# and Redis and dotnet 6 and RedLock

So as to achieve the simplicity of understanding the implementation step by step, first of all, create a new web API project ( Here we are using VS 2022 and C#)

1- Install these Nuget packages

RedLock.net
StackExchange.Redis

2- Create an extension for IserviceCollection for config our RedLock on Redis.

3- Create an extension for IAppliationBuilder aimed to register the LockFactory object to the disposal list of the application(the created RedLockFactory which is static should be disposed (Redis’ open connections and release the acquired locks)whenever the application stops).

4- Now in the infrastructure layer, a RedLockProvider needs to be added, which is responsible to create a static instance of RedLockFactory.

Here you can add all your Redis Endpoints, however, It also works with one endpoint as well. (The best practice as Redis's official document suggests is to have at least three endpoints).

You can also set a LoggerFactory for the Create method if you want to see verbosely all the interaction with DLM in the logger. ( It’s better to send a ConsoleLogger to this provider, in case you have some issue with acquiring the lock. Somehow you can monitor your DLM with this logger).

5- Now, It’s time to implement a cache service which is an abstraction on the StackExchange.Redis library

There is DoActionWithLockAsync method in this service which can be used to control concurrency for the processor function.

There are two overloads for this method.

1- One accepts a generic type as the input for the processor.
2- The other does accept no input.

The return value for both of them is Task. (But in your scenario you may need to return some value after processing, you are able to add your own overload to this service.)

The output type of DoActionWithLockAsync is LockProcessResult. There is nothing special in this type. Just in case the execution face an exception or in case the lock cannot be acquired the exception will be set by the proper value, in order to check the execution result in the application services.

It’s time to elaborate on the code inside the Cache Service.

First of all, a new Lock object should be created.

await using var redLock = await distributedLockFactory.CreateLockAsync()

This method accepts four parameters:

1- LockKey : a specific lock key (string) that should be unique for all applications which are needed to be controlled by their access to this block of code.
If you have a distributed application(like an application with microservice architecture or any other type of distributed system).you may keep this key in a Nuget package that is accessible between all the applications that they want to have access to this block of code. And the execution of this block of code should be serialized regarding concurrency control.
2- ExpiryTime: Determine an expiration time for the lock, to avoid any deadlock, because if the lock does not have an expiry time, maybe the process does not release the lock.3- WaitTime: It says, in case a process couldn't acquire a lock, how long it should wait to acquire the lock. (If you want to have a retry pattern)4- RetryTime: In case a process could not get the lock, It will wait for WaitTime, and during this waiting period, how many times it should retry to acquire the lock.

6- All these configurations are added in the appSettings.json. To bind them with an object and use the Options pattern, a class with these properties needs to be added.

7- So far implementation of the DLM is finished, now it’s time to write some code to test the lock functionality., for this purpose, add a new application service and name it with ContributionService.

Inside the service, there are two methods.

1- AddContributionWithDLM
2- AddContributionWihtoutDLM

Both methods call a private method (AddContributionToCache) which adds value to the Redis cache, but in the second method (AddContributionWihtoutDLM), we use DLM to control concurrency and race conditions.

8- Finally, in the Presentation layer create a new Controller (ContributionController) and add these codes to it.

In this controller, there are two endpoints.

1- AddContributionsWithDLM
2- AddContributionsWithoutDLM

Both the endpoints create fifty tasks and each task calls the related method in the application service. Then all the tasks will be awaited. It means fifty concurrent requests will be sent to the application service, in order to check the performance of the DLM.

Check The Results

For the With_DLM endpoint, we always get the same value (50), because, concurrency and race conditions can be controlled via the DLM.

distributed lock manager with c# and Redis and dotnet 6 and RedLock

But, look at the second endpoint(Without_DLM)

distributed lock manager with c# and Redis and dotnet 6 and RedLock

This endpoint each time returns a different value because the concurrent requests will face race conditions and requests values will be overridden with each other, so the final value is not valid.

Source Code

You can find the implementation source code on my Github page Here

Final Thought

For concurrency management in an application, first, you must understand the problem, after that determine your critical points, and in the last step according to the collected data and consulting with a domain expert, you can choose your approach. Distributed lock managers in most cases can handle your problems. But sometimes you have to choose another approach like Actor systems.

That’s all, Hope you’ve enjoyed the article, feel free to contact me and send me your comments.

--

--