Vinicius Quinafelex Alves

🌐Ler em português

[C#] Implementing Redis Redlock for distributed locking

Redis can be used on systems with parallel processing, whether it's a multi-thread or multi-instance system. However, there's always the possibility that two or more threads have to cache the same value, and end up executing redundant functions. To avoid this, the system can use a lock mechanism, so a specific data is only processed by one thread at a time, and all other threads can make use of the result.

For that, Redis recommends using the Redlock algorithm. In summary, it's a semaphore shared between processes, that can be implemented using Redis itself.

Redlock uses the value timeout as a safe-keep, to avoid infinite locks in cases when a consumer crashes before releasing the lock. As refresher about some basic concepts, this article is an introduction about Redis.

RedLock.net is one of a few C# libraries endorsed by the Redis company that can help implementing the algorithm. The code uses double-check locking to avoid reprocessing when the waiting threads acquire the lock.

Regarding the usage

When using the Redlock algorithm, there are a few scenarios that need attention. For example, the lock can timeout before the current consumer unlocks it. So it's possible that parallel processing end up happening.

Another thing is that Redlock.net is not notified when the lock becomes available. It manages itself by checking the lock status from time to time, up to a time limit. As there's a time limit, it's also possible that the time limit is reached before the lock becomes available, and the code has to handle it in some way.

Package install

dotnet add package RedLock.net

Redlock in repository

using RedLockNet.SERedis;
using RedLockNet.SERedis.Configuration;

public class RedisLockRepository
{
    // ConnectionMultiplexer should be used as a Singleton
    // Recommended to inject as IConnectionMultiplexer and dispose when the application shuts down
    private readonly static ConnectionMultiplexer Connection = ConnectionMultiplexer.Connect("localhost");

    // RedlockFactory should be used as a Singleton as well
    // Recommended to inject and dispose when the application shuts down
    private readonly static RedLockFactory LockFactory = RedLockFactory.Create(new List<RedLockMultiplexer>(){ Connection });

    private string GetLockKey(string key)
    {
        return $"lock.{key}";
    }

    private async Task<IRedLock> CreateLockAsync(string lockKey)
    {
        return await LockFactory.CreateLockAsync
        (
            lockKey,
            expiryTime: TimeSpan.FromSeconds(30), // Maximum time a process can hold the lock
            waitTime: TimeSpan.FromSeconds(10), // How much time this thread can wait for the lock until giving up
            retryTime: TimeSpan.FromSeconds(1) // How much time between rechecking if the lock is available
        );
    }

    public async Task<int> GetValueOrLoadAsync(string key, Func<Task<int>> loadAsync)
    {
        var db = Connection.GetDatabase();

        var value = await db.StringGetAsync(key);
        if(value.IsNull)
        {
            var lockKey = GetLockKey(key);

            using(var acquiredLock = await CreateLockAsync(lockKey))
            {
                if(acquiredLock.IsAcquired)
                {
                    value = await db.StringGetAsync(key);
                    if(value.IsNull)
                    {
                        value = await loadAsync();
                        await db.StringSetAsync(key, value, expiry: TimeSpan.FromSeconds(30));
                    }
                }
                else
                {
                    /* Lock not acquired, decide what to do */
                }
            }
        }
        
        return (int)value;
    }
}

Using the repository

var repository = new RedisLockRepository();

await repositoryLock.GetValueOrLoadAsync
(
    key: valueKey, 
    loadAsync: async () => 
    { 
        /* Execute value generation */ 
    }
);