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.