In a multi-threaded environment, it is essential to ensure that a code block can only be accessed by one thread at a time. Today, distributed architectures are popular within companies, so how do we ensure thread synchronization across different nodes in a distributed environment?
In distributed scenarios, we can use a distributed lock, which is a method to control mutual exclusion access to shared resources across a distributed system. For each lock, it’s crucial to have a unique ID to prevent erroneous unlocking (e.g., if locks A and B have the same key, and B unlocks at the exact moment A expires, without validating the lock ID, A could mistakenly be unlocked).

Redis offers the SETNX (Set if Not Exists) command, which inserts a value only if the key does not exist. Starting with Redis version 2.6.12, the SET command was overloaded to support inserting a value with an expiration time if the key does not exist. Although Redis doesn't provide a command to delete a key only when the value matches, version 2.6.0 introduced EXAL, which can be used to write scripts that enable this functionality.
Introducing dependencies
using ServiceStack.Redis;
Lock implementation process
private static readonly string ScriptSetIfAbsent = "return redis.call('SET',KEYS[1],ARGV[1],'EX',ARGV[2],'NX')";
private static readonly string ScriptDeleteIfEqualValue = @"if redis.call('GET',KEYS[1]) == ARGV[1] then
return redis.call('DEL',KEYS[1])
else
return 'FALSE'
end";/// <summary>
/// Acquire a lock.
/// </summary>
/// <param name="key">Lock key</param>
/// <param name="lockToken">Lock token, used for releasing the lock</param>
/// <param name="lockExpirySeconds">Lock expiration time (in seconds)</param>
/// <param name="waitLockSeconds">Time to wait for the lock (in seconds)</param>
/// <returns>True if lock acquired successfully</returns>
public bool Lock(string key, out string lockToken, int lockExpirySeconds = 10, double waitLockSeconds = 0)
{
int waitIntervalMs = 1000;
string lockKey = GetLockKey(key);
DateTime begin = DateTime.Now;
string uuid = Guid.NewGuid().ToString();
// Loop to acquire the lock
while (true)
{
string result;
using (var client = GetNativeClient())
{
// Return result of the SET operation, "OK" indicates success
result = client.EvalStr(ScriptSetIfAbsent, 1,
System.Text.Encoding.UTF8.GetBytes(lockKey),
System.Text.Encoding.UTF8.GetBytes(uuid),
System.Text.Encoding.UTF8.GetBytes(lockExpirySeconds.ToString()));
}
if (result == "OK")
{
lockToken = uuid;
return true;
}
// Stop waiting if the wait time is exceeded
if ((DateTime.Now - begin).TotalSeconds >= waitLockSeconds) break;
Thread.Sleep(waitIntervalMs);
}
lockToken = null;
return false;
}/// <summary>
/// Release the lock after executing the code
/// </summary>
/// <param name="key">Lock key</param>
/// <param name="lockToken">Lock token</param>
/// <returns>True if lock is successfully released</returns>
public bool DelLock(string key, string lockToken)
{
if (string.IsNullOrWhiteSpace(lockToken))
{
throw new Exception("The parameter lockToken cannot be null");
}
string lockKey = GetLockKey(key);
using (var client = GetNativeClient())
{
// Return number of deleted rows, 1 indicates success
string result = client.EvalStr(ScriptDeleteIfEqualValue, 1,
System.Text.Encoding.UTF8.GetBytes(lockKey),
System.Text.Encoding.UTF8.GetBytes(lockToken));
return result == "1";
}
}Lock usage process
if (RedisManager.Lock(key, out tokenLock))
{
try
{
IRedisClient rdsclient = null;
try
{
// Execute your business logic here
}
finally
{
rdsclient?.Dispose();
}
}
finally
{
RedisManager.DelLock(key, tokenLock);
}
}