Creating Distributed Lock With Redis In .NET Core

In this article, we will discuss how to create distributed lock with redis in .NET Core.

Introduction

In this article, we will discuss how to create a distributed lock with Redis in .NET Core.
 
When we building distributed systems, we will face that multiple processes handle a shared resource together, it will cause some unexpected problems due to the fact that only one of them can utilize the shared resource at a time!
 
We can use distributed lock to solve this problem.

Why Distributed Lock?

As usual, we will use lock to handle this problem.
 
The following shows some sample code demonstrating the use of lock.
  1. public void SomeMethod()  
  2. {  
  3.     //do something...  
  4.     lock(obj)  
  5.     {  
  6.         //do ....  
  7.     }  
  8.     //do something...  

However, this type of lock cannot help us to work the problem well! This is a in-process lock which can only solve one process with shared resource.
 
This is also the main reason why we need a distributed lock!
 
I will use Redis to create a simple distributed lock here.
 
And why do I use Redis to do this job? Because of Redis's single-threaded nature and its ability to perform atomic operations.

How To Create A Lock ?

I will create a .NET Core Console application to show you.
 
Before the next step, we should run up the Redis server!
 
 
 
StackExchange.Redis is the most popular Reids client in .NET, and there is no doubt that we will use it to do the following jobs.
 
Creating the connnection with Redis at first.
  1. /// <summary>  
  2. /// The lazy connection.  
  3. /// </summary>  
  4. private static Lazy<ConnectionMultiplexer> lazyConnection = new Lazy<ConnectionMultiplexer>(() =>  
  5. {  
  6.     ConfigurationOptions configuration = new ConfigurationOptions  
  7.     {  
  8.         AbortOnConnectFail = false,  
  9.         ConnectTimeout = 5000,  
  10.     };  
  11.   
  12.     configuration.EndPoints.Add("localhost", 6379);  
  13.   
  14.     return ConnectionMultiplexer.Connect(configuration.ToString());  
  15. });  
  16.   
  17. /// <summary>  
  18. /// Gets the connection.  
  19. /// </summary>  
  20. /// <value>The connection.</value>  
  21. public static ConnectionMultiplexer Connection => lazyConnection.Value; 
In order to request a lock on a shared resource, we do the following:
  1. SET resource_name unique_value NX PX duration  
The resource_name is a value that all instances of your application would share.
 
The unique_value is something that must be unique to each instance of your application. And the purpose of this unique value is to remove the lock (unlock).
 
Finally we also provide a duration (in milliseconds), after which the lock will be automatically removed by Redis.
 
Here is the implementation in C# code.
  1. /// <summary>  
  2. /// Acquires the lock.  
  3. /// </summary>  
  4. /// <returns><c>true</c>, if lock was acquired, <c>false</c> otherwise.</returns>  
  5. /// <param name="key">Key.</param>  
  6. /// <param name="value">Value.</param>  
  7. /// <param name="expiration">Expiration.</param>  
  8. static bool AcquireLock(string key, string value, TimeSpan expiration)  
  9. {  
  10.     bool flag = false;  
  11.   
  12.     try  
  13.     {  
  14.         flag = Connection.GetDatabase().StringSet(key, value, expiration, When.NotExists);  
  15.     }  
  16.     catch (Exception ex)  
  17.     {  
  18.         Console.WriteLine($"Acquire lock fail...{ex.Message}");  
  19.         flag = true;  
  20.     }  
  21.   
  22.     return flag;  

 Here is the code to test the acquire lock.
  1. static void Main(string[] args)  
  2. {  
  3.     string lockKey = "lock:eat";  
  4.     TimeSpan expiration = TimeSpan.FromSeconds(5);  
  5.     //5 person eat something...  
  6.     Parallel.For(0, 5, x =>  
  7.     {  
  8.         string person = $"person:{x}";  
  9.         bool isLocked = AcquireLock(lockKey, person, expiration);  
  10.   
  11.         if (isLocked)  
  12.         {  
  13.             Console.WriteLine($"{person} begin eat food(with lock) at {DateTimeOffset.Now.ToUnixTimeMilliseconds()}.");  
  14.         }  
  15.         else  
  16.         {  
  17.             Console.WriteLine($"{person} can not eat food due to don't get the lock.");  
  18.         }  
  19.     });  
  20.   
  21.     Console.WriteLine("end");  
  22.     Console.Read();  

 After running the code, we may get the following result.
 
 
 
Only one person can get the lock! Others are waiting.
 
Although the lock will be automatically removed by Redis, it also does not make good use of the shared resource!
 
Because when a process finishes its job, it should let others use the resource, not wait endlessly!
 
So we also need to release the lock as well.

How To Release A Lock ?

For releasing the lock, we just remove the item in Redis!
 
As what we take in creating a lock, we need to match the unique value for the resource, this will be more safe to release a right lock.
 
When matching, we will delete the lock which means that unlock was successful. Otherwise, unlock was unsuccessful.
 
And we need to execute get and del command at a time, so we will use a lua script to do this!
  1. /// <summary>  
  2. /// Releases the lock.  
  3. /// </summary>  
  4. /// <returns><c>true</c>, if lock was released, <c>false</c> otherwise.</returns>  
  5. /// <param name="key">Key.</param>  
  6. /// <param name="value">Value.</param>  
  7. static bool ReleaseLock(string key, string value)  
  8. {  
  9.     string lua_script = @"  
  10.     if (redis.call('GET', KEYS[1]) == ARGV[1]) then  
  11.         redis.call('DEL', KEYS[1])  
  12.         return true  
  13.     else  
  14.         return false  
  15.     end  
  16.     ";  
  17.   
  18.     try  
  19.     {  
  20.         var res = Connection.GetDatabase().ScriptEvaluate(lua_script,  
  21.                                                    new RedisKey[] { key },  
  22.                                                    new RedisValue[] { value });  
  23.         return (bool)res;  
  24.     }  
  25.     catch (Exception ex)  
  26.     {  
  27.         Console.WriteLine($"ReleaseLock lock fail...{ex.Message}");  
  28.         return false;  
  29.     }  

We should call this method when a process has finished.
 
When a process gets the lock and does not release the lock due to some reasons, other processes cannot wait until it released. At this time, other processes should go ahead.
 
Here is a sample to deal with this scene.
  1. Parallel.For(0, 5, x =>  
  2. {  
  3.     string person = $"person:{x}";  
  4.     var val = 0;  
  5.     bool isLocked = AcquireLock(lockKey, person, expiration);  
  6.     while (!isLocked && val <= 5000)  
  7.     {  
  8.         val += 250;  
  9.         System.Threading.Thread.Sleep(250);  
  10.         isLocked = AcquireLock(lockKey, person, expiration);  
  11.     }  
  12.   
  13.     if (isLocked)  
  14.     {  
  15.         Console.WriteLine($"{person} begin eat food(with lock) at {DateTimeOffset.Now.ToUnixTimeMilliseconds()}.");  
  16.         if (new Random().NextDouble() < 0.6)  
  17.         {  
  18.             Console.WriteLine($"{person} release lock {ReleaseLock(lockKey, person)}  {DateTimeOffset.Now.ToUnixTimeMilliseconds()}");  
  19.         }  
  20.         else  
  21.         {  
  22.             Console.WriteLine($"{person} do not release lock ....");  
  23.         }  
  24.     }  
  25.     else  
  26.     {  
  27.         Console.WriteLine($"{person} begin eat food(without lock) at {DateTimeOffset.Now.ToUnixTimeMilliseconds()}.");  
  28.     }  
  29. }); 
 After running the sample, you may get the following result.
 
 
As you can see, person 3 and 4 will go ahead without lock.
 
Here is the source code you can find in my github page .
Summary

This article introduced how to create distributed lock with Redis in .NET Core. And it's a basic version, you can impove based on your business.
 
I hope this helps you.