Controle de Concorrencia com Redis em .NET 6 no Windows

Em sistemas baseados em web, problemas de concorrência, como a alocação simultânea do mesmo recurso por múltiplas requisições, são comuns. Utilizar um mecanismo de travamento distribuído baseado em um armazenamento como o Redis é uma abordagem eficaz para resolver esses cenários, especialmente em ambientes de servidor único no Windows.

Para implementar esta solução, primeiro instale o Redis no Windows e configure-o como um serviço. Em seguida, enclua os pacotes NuGet necessários no seu projeto .NET 6.0. As principais dependências são o cliente Redis CSRedisCore e uma abstração de bloqueio, como MyStack.DistributedLocking. O Microsoft.Extensions.Configuration é usado para gerenciar configurações.

Após a instalação dos pacotes, configure a cadeia de conexão do Redis no arquivo appsettings.json:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information"
    }
  },
  "RedisConnection": "127.0.0.1:6379,abortConnect=false"
}

No ponto de entrada da aplicação (Program.cs), registre os serviços necessários para o cache distribuído e o bloqueio. A abstração do cache utiliza o CSRedisCache, e a lógica de bloqueio será tratada por uma classe de serviço customizada.

using CSRedis;
using Microsoft.Extensions.Caching.Distributed;
using StackExchange.Redis;

var builder = WebApplication.CreateBuilder(args);

// Configura e registra o cliente Redis e o cache distribuído
var redisConnection = builder.Configuration.GetValue<string>("RedisConnection")!;
var redisClient = new CSRedisClient(redisConnection);
RedisHelper.Initialization(redisClient);

builder.Services.AddSingleton<IDistributedCache>(provider =>
    new CSRedisCache(redisClient));

// Registra o serviço de bloqueio distribuído customizado
builder.Services.AddSingleton<IDistributedLockProvider, RedisDistributedLockProvider>();

builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();

A implementação do provedor de bloqueio distribuído encapsula a lógica de obtenção e liberação de chaves de bloqueio no Redis. Ela utiliza operações atômicas para garantir a segurança dos bloqueios.

using CSRedis;
using StackExchange.Redis;

public interface IDistributedLockProvider
{
    Task<bool> AcquireLockAsync(string resourceKey, TimeSpan lockTimeout, TimeSpan waitTimeout);
    Task ReleaseLockAsync(string resourceKey);
}

public class RedisDistributedLockProvider : IDistributedLockProvider
{
    private readonly CSRedisClient _redis;
    private const string LockPrefix = "distributed_lock:";

    public RedisDistributedLockProvider(CSRedisClient redisClient)
    {
        _redis = redisClient;
    }

    public async Task<bool> AcquireLockAsync(string resourceKey, TimeSpan lockTimeout, TimeSpan waitTimeout)
    {
        if (string.IsNullOrWhiteSpace(resourceKey))
            throw new ArgumentNullException(nameof(resourceKey));

        var lockKey = $"{LockPrefix}{resourceKey}";
        var lockValue = Guid.NewGuid().ToString();
        var expirySeconds = (int)lockTimeout.TotalSeconds;
        var endTime = DateTime.UtcNow.Add(waitTimeout);

        while (DateTime.UtcNow < endTime)
        {
            // Tenta adquirir o bloqueio de forma atômica (SET com NX e PX)
            var acquired = await _redis.SetAsync(lockKey, lockValue, expirySeconds, RedisExistence.Nx);
            if (acquired)
                return true;

            await Task.Delay(100); // Pequeno atraso antes de tentar novamente
        }
        return false;
    }

    public async Task ReleaseLockAsync(string resourceKey)
    {
        var lockKey = $"{LockPrefix}{resourceKey}";
        // Idealmente, a remoção deveria verificar o valor (lockValue) usando um script Lua
        // para evitar a remoção de um bloqueio adquirido por outro processo.
        // Para simplicidade, esta implementação apenas remove a chave.
        await _redis.DelAsync(lockKey);
    }
}

No código da aplicação, onde ocorre a alocação de um recurso (como uma posição ou vaga), utilize o serviço de bloqueio para garantir a execução exclusiva da operação. Primeiro, tente adquirir o bloqueio. Se bem-sucedido, prossiga com a lógica de negócio. Caso contrário, retorne uma repsosta de conflito.

// No serviço ou controlador
private readonly IDistributedLockProvider _lockProvider;

public async Task<IActionResult> AllocateSlotAsync(string slotId)
{
    var lockResourceKey = $"allocate_{slotId}";
    var acquired = await _lockProvider.AcquireLockAsync(lockResourceKey, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(5));

    if (!acquired)
    {
        return Conflict(new { message = "Recurso ocupado. Por favor, tente novamente em alguns instantes." });
    }

    try
    {
        // Lógica para verificar disponibilidade e alocar a vaga no banco de dados
        await _slotRepository.ReserveSlotAsync(slotId);
        return Ok();
    }
    finally
    {
        await _lockProvider.ReleaseLockAsync(lockResourceKey);
    }
}

Este padrão assegura que apenas uma solicitação possa executar o bloco crítico para um determinado recurso por vez, prevenindo condições de corrida e garantindo a integridade dos dados durante operações de alocação.

Tags: Redis .NET Core Distributed Locking Concurrency Control CSharp

Publicado em 5-31 08:59 por Thomas