[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 */
}
);