How to Implement Distributed Locks Using Redis

Time: Column:Backend & Servers views:286

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).

How to Implement Distributed Locks Using Redis

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);
    }
}