Vinicius Quinafelex Alves

🌐English version

[C#] Implementando Redlock do Redis para lock distribuido

É possível usar uma única instância Redis em sistemas com processamento paralelo, seja pelo sistema ser multi-thread ou por executar várias instâncias. Porém, sempre existe a possibilidade de duas ou mais threads precisarem de um mesmo valor ao mesmo tempo, e acabarem executando funções em redundância. Neste cenário, é o sistema pode utilizar um mecanismo de lock, para que cada tipo de dado seja executado por apenas uma thread por vez, e as demais threads poderão aproveitar o resultado.

Pra isso, o Redis recomenda utilizar Redlock. Em resumo, ele é um semáforo compartilhado entre processos, que pode ser implementado usando o Redis.

Redlock usa o mecanismo de timeout de valores como segurança, para evitar locks intermináveis em situações que algum sistema sofra crash antes de liberar um lock. Para relembrar dos conceitos básicos, este artigo é uma introdução ao Redis.

RedLock.net é uma das bibliotecas C# apoiadas pela empresa do Redis que nos auxiliam com a implementação desse algoritmo. O código usa a técnica de double-check locking para evitar que threads enfileiradas executem um reprocessamento indevido.

Considerações sobre o uso

No algoritmo Redlock, alguns cenários precisam de atenção especial. Por exemplo, o lock pode dar timeout e ser liberado antes do processo ser concluído. Então é possível que mais de uma thread acabe executando o mesmo processo.

O Redlock.net também não é notificado quando um lock fica disponível. A biblioteca adquire o lock fazendo checagens periódicas no Redis, até um tempo limite. É possível que a thread chegue no tempo limite sem que o lock tenha ficado disponível, então o código deve prever esse tipo de cenário.

Instalação do package

dotnet add package RedLock.net

Redlock em repositório

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

Usando o repositório

var repository = new RedisLockRepository();

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