VOOZH about

URL: https://dev.to/unhacked/redis-incr-o-comando-atomico-que-voce-deveria-conhecer-194b

⇱ Redis INCR: O comando atômico que você deveria conhecer - DEV Community


Em sistemas distribuídos, contar algo corretamente é mais difícil do que parece. Dois processos lendo e atualizando o mesmo valor podem facilmente gerar race conditions e inconsistências.

É exatamente aqui que o comando INCR do Redis brilha.

O INCR resolve 3 problemas clássicos:

  • contadores distribuídos
  • rate limiting
  • geração de IDs

Vamos entender por que esse comando aparentemente simples é tão poderoso.

O que é o comando INCR?

O INCR incrementa o valor numérico armazenado em uma chave por 1. Se a chave não existir, ela é criada com valor 0 antes do incremento, resultando em 1.

> SET contador 10
OK
> INCR contador
(integer) 11
> INCR contador
(integer) 12

Se a chave não existe:

> INCR novo_contador
(integer) 1

A família completa de comandos de incremento inclui:

Comando Descrição
INCR Incrementa por 1
INCRBY Incrementa por um valor inteiro específico
INCRBYFLOAT Incrementa por um valor decimal
DECR Decrementa por 1
DECRBY Decrementa por um valor inteiro específico

Por que o INCR é especial?

A característica mais importante do INCR é sua atomicidade. Em sistemas distribuídos, onde múltiplas instâncias da aplicação podem tentar modificar o mesmo valor simultaneamente, a atomicidade garante que cada operação seja executada de forma isolada.

A atomicidade do INCR é possível porque o Redis executa comandos de forma sequencial em um único thread. Isso significa que não existe concorrência interna entre comandos: cada operação é processada por completo antes da próxima iniciar. Essa característica de single-thread para execução de comandos é o que torna operações como INCR naturalmente seguras em ambientes concorrentes.

Considere o cenário clássico de race condition:

Tempo Processo A Processo B Valor Real
─────────────────────────────────────────────────────────────
T1 Lê valor: 10 10
T2 Lê valor: 10 10
T3 Calcula: 10 + 1 10
T4 Calcula: 10 + 1 10
T5 Escreve: 11 11
T6 Escreve: 11 11 ← Perdemos um incremento!

Com INCR, o Redis garante que isso não acontece. A operação de leitura-incremento-escrita é executada como uma única unidade atômica.

Casos de Uso Práticos

1. Contadores de visualização

Um dos casos mais comuns é contar visualizações de páginas ou recursos:

public class ViewCounterService
{
 private readonly IDatabase _redis;
 private readonly TimeSpan _cacheExpiry = TimeSpan.FromHours(24);

 public ViewCounterService(IConnectionMultiplexer connection)
 {
 _redis = connection.GetDatabase();
 }

 public async Task<long> IncrementViewCountAsync(string resourceId)
 {
 var key = $"views:{resourceId}";
 var count = await _redis.StringIncrementAsync(key);

 // Define TTL apenas na primeira visualização
 if (count == 1)
 {
 await _redis.KeyExpireAsync(key, _cacheExpiry);
 }

 return count;
 }

 public async Task<long> GetViewCountAsync(string resourceId)
 {
 var key = $"views:{resourceId}";
 var value = await _redis.StringGetAsync(key);
 return value.HasValue ? (long)value : 0;
 }
}

2. Rate Limiting com Fixed Window

O rate limiting é essencial para proteger APIs contra abusos. O INCR combinado com TTL cria um rate limiter baseado em fixed window, simples e eficiente:

public class RateLimiter
{
 private readonly IDatabase _redis;

 public RateLimiter(IConnectionMultiplexer connection)
 {
 _redis = connection.GetDatabase();
 }

 public async Task<RateLimitResult> CheckRateLimitAsync(
 string clientId, 
 int maxRequests, 
 TimeSpan window)
 {
 var key = $"ratelimit:{clientId}:{DateTime.UtcNow.Ticks / window.Ticks}";

 var count = await _redis.StringIncrementAsync(key);

 if (count == 1)
 {
 await _redis.KeyExpireAsync(key, window);
 }

 return new RateLimitResult
 {
 IsAllowed = count <= maxRequests,
 CurrentCount = count,
 Limit = maxRequests,
 RetryAfter = count > maxRequests 
 ? await _redis.KeyTimeToLiveAsync(key) 
 : null
 };
 }
}

public record RateLimitResult
{
 public bool IsAllowed { get; init; }
 public long CurrentCount { get; init; }
 public int Limit { get; init; }
 public TimeSpan? RetryAfter { get; init; }
}

Nota: Essa abordagem usa fixed window counter, onde cada janela de tempo tem seu próprio contador. Para cenários que exigem sliding window real, considere usar Sorted Sets com ZADD/ZRANGEBYSCORE ou um algoritmo de token bucket. Além disso, vale observar que o padrão INCR seguido de EXPIRE pode sofrer uma race condition sutil: outro processo pode incrementar a chave entre o INCR e o EXPIRE. Para maior robustez, você pode encapsular ambos em um script Lua, garantindo a execução atômica de INCR + EXPIRE.

Uso em um middleware ASP.NET Core:

public class RateLimitingMiddleware
{
 private readonly RequestDelegate _next;
 private readonly RateLimiter _rateLimiter;

 public RateLimitingMiddleware(RequestDelegate next, RateLimiter rateLimiter)
 {
 _next = next;
 _rateLimiter = rateLimiter;
 }

 public async Task InvokeAsync(HttpContext context)
 {
 var clientId = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
 var result = await _rateLimiter.CheckRateLimitAsync(
 clientId, 
 maxRequests: 100, 
 window: TimeSpan.FromMinutes(1));

 context.Response.Headers["X-RateLimit-Limit"] = result.Limit.ToString();
 context.Response.Headers["X-RateLimit-Remaining"] = 
 Math.Max(0, result.Limit - result.CurrentCount).ToString();

 if (!result.IsAllowed)
 {
 context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
 if (result.RetryAfter.HasValue)
 {
 context.Response.Headers["Retry-After"] = 
 ((int)result.RetryAfter.Value.TotalSeconds).ToString();
 }
 return;
 }

 await _next(context);
 }
}

3. Geração de IDs sequenciais

Quando você precisa de IDs sequenciais em um sistema distribuído:

public class SequentialIdGenerator
{
 private readonly IDatabase _redis;
 private readonly string _prefix;

 public SequentialIdGenerator(IConnectionMultiplexer connection, string prefix = "id")
 {
 _redis = connection.GetDatabase();
 _prefix = prefix;
 }

 public async Task<long> GenerateIdAsync(string entity)
 {
 var key = $"{_prefix}:{entity}:sequence";
 return await _redis.StringIncrementAsync(key);
 }

 public async Task<string> GenerateFormattedIdAsync(string entity, string format = "{0}-{1:D8}")
 {
 var id = await GenerateIdAsync(entity);
 return string.Format(format, entity.ToUpper(), id);
 }
}

// Uso
var generator = new SequentialIdGenerator(connection);
var orderId = await generator.GenerateFormattedIdAsync("order"); 
// Resultado: "ORDER-00000001"

4. Contadores de sessões ativas

Monitorar quantos usuários estão ativos em tempo real:

public class ActiveSessionTracker
{
 private readonly IDatabase _redis;
 private const string ActiveSessionsKey = "sessions:active:count";
 private const string SessionPrefix = "session:";

 public ActiveSessionTracker(IConnectionMultiplexer connection)
 {
 _redis = connection.GetDatabase();
 }

 public async Task<string> StartSessionAsync(string userId)
 {
 var sessionId = Guid.NewGuid().ToString("N");
 var sessionKey = $"{SessionPrefix}{sessionId}";

 var transaction = _redis.CreateTransaction();

 _ = transaction.StringSetAsync(sessionKey, userId, TimeSpan.FromMinutes(30));
 _ = transaction.StringIncrementAsync(ActiveSessionsKey);

 await transaction.ExecuteAsync();

 return sessionId;
 }

 public async Task EndSessionAsync(string sessionId)
 {
 var sessionKey = $"{SessionPrefix}{sessionId}";

 var existed = await _redis.KeyDeleteAsync(sessionKey);

 if (existed)
 {
 await _redis.StringDecrementAsync(ActiveSessionsKey);
 }
 }

 public async Task<long> GetActiveSessionCountAsync()
 {
 var value = await _redis.StringGetAsync(ActiveSessionsKey);
 return value.HasValue ? (long)value : 0;
 }
}

Nota: Nessa abordagem, se uma sessão expirar automaticamente pelo TTL, o contador ActiveSessionsKey não será decrementado, o que pode gerar drift ao longo do tempo. Para maior robustez em produção, considere usar um Sorted Set (ZSET) com timestamps para rastrear sessões ativas, permitindo que você calcule o total real a qualquer momento com ZRANGEBYSCORE, ou implemente um processo periódico de reconciliação do contador.

5. Métricas por intervalo de tempo

Coletar métricas agregadas por minuto/hora/dia:

public class MetricsCollector
{
 private readonly IDatabase _redis;

 public MetricsCollector(IConnectionMultiplexer connection)
 {
 _redis = connection.GetDatabase();
 }

 public async Task RecordMetricAsync(string metricName, long value = 1)
 {
 var now = DateTime.UtcNow;

 var keys = new[]
 {
 $"metrics:{metricName}:minute:{now:yyyyMMddHHmm}",
 $"metrics:{metricName}:hour:{now:yyyyMMddHH}",
 $"metrics:{metricName}:day:{now:yyyyMMdd}"
 };

 var expiries = new[]
 {
 TimeSpan.FromHours(2),
 TimeSpan.FromDays(2),
 TimeSpan.FromDays(60)
 };

 var batch = _redis.CreateBatch();

 for (int i = 0; i < keys.Length; i++)
 {
 var key = keys[i];
 var expiry = expiries[i];

 _ = batch.StringIncrementAsync(key, value);
 _ = batch.KeyExpireAsync(key, expiry, ExpireWhen.HasNoExpiry);
 }

 batch.Execute();
 }

 public async Task<Dictionary<string, long>> GetHourlyMetricsAsync(
 string metricName, 
 DateTime date)
 {
 var results = new Dictionary<string, long>();

 for (int hour = 0; hour < 24; hour++)
 {
 var key = $"metrics:{metricName}:hour:{date:yyyyMMdd}{hour:D2}";
 var value = await _redis.StringGetAsync(key);
 results[$"{hour:D2}:00"] = value.HasValue ? (long)value : 0;
 }

 return results;
 }
}

Sistemas Distribuídos com múltiplos pods

Em ambientes Kubernetes ou qualquer arquitetura com múltiplas instâncias, o Redis INCR se torna ainda mais valioso. Cada pod opera de forma independente, sem conhecimento dos outros, mas todos precisam coordenar acesso a recursos compartilhados.

O Problema do estado compartilhado

Imagine 5 pods processando requisições simultaneamente. Sem coordenação central, cada pod manteria seu próprio contador local:

Pod 1: contador local = 47
Pod 2: contador local = 52
Pod 3: contador local = 38
Pod 4: contador local = 61
Pod 5: contador local = 44
 ───────
Total real de requisições: 242
Mas cada pod "acha" que processou apenas seu número local

Com Redis como ponto central de coordenação, todos os pods compartilham a mesma visão do estado.

E em Redis Cluster?

Em arquiteturas maiores que utilizam Redis Cluster, o INCR funciona normalmente desde que as chaves relacionadas estejam no mesmo hash slot. O Redis Cluster distribui chaves entre nós usando CRC16, então se você precisa que múltiplas chaves sejam processadas no mesmo nó, utilize hash tags. Por exemplo, {rate}:user:123 e {rate}:user:456 serão direcionadas para o mesmo slot por compartilharem a tag {rate}. Isso é especialmente relevante quando você usa transactions ou scripts Lua que operam sobre múltiplas chaves.

Considerações de Performance

O INCR é extremamente rápido, com complexidade O(1). No entanto, existem algumas práticas para otimizar ainda mais:

Batch de operações: Quando você precisa incrementar múltiplos contadores, use batching:

public async Task IncrementMultipleAsync(Dictionary<string, long> counters)
{
 var batch = _redis.CreateBatch();
 var tasks = new List<Task<long>>();

 foreach (var (key, increment) in counters)
 {
 tasks.Add(batch.StringIncrementAsync(key, increment));
 }

 batch.Execute();
 await Task.WhenAll(tasks);
}

Pipeline: Para operações que não dependem do resultado imediato:

public void RecordEventsFireAndForget(IEnumerable<string> eventKeys)
{
 foreach (var key in eventKeys)
 {
 _redis.StringIncrement(key, flags: CommandFlags.FireAndForget);
 }
}

Armadilhas Comuns

1. Não definir TTL: Contadores sem expiração podem acumular indefinidamente. Sempre considere se o contador precisa de um TTL.

2. Usar GET seguido de SET: Nunca faça isso para incrementar valores. Use sempre INCR:

// ERRADO: Race condition!
var value = await _redis.StringGetAsync(key);
await _redis.StringSetAsync(key, (long)value + 1);

// CORRETO: Atômico
await _redis.StringIncrementAsync(key);

3. Ignorar overflow: O Redis armazena inteiros como strings de até 64 bits signed. Em cenários extremos, considere verificar limites.

4. Não tratar valores não numéricos: Se uma chave contiver um valor não numérico, INCR retornará erro. Valide ou use chaves dedicadas.

Conclusão

O INCR do Redis é uma ferramenta fundamental para construir sistemas distribuídos robustos. Sua atomicidade elimina race conditions, sua velocidade permite uso em cenários de alta performance, e sua simplicidade torna a implementação direta.

Os casos de uso vão desde contadores simples até rate limiters sofisticados e sistemas de métricas em tempo real. O segredo está em entender que operações atômicas simples, quando combinadas corretamente, resolvem problemas complexos de concorrência.

O poder do Redis muitas vezes não está em comandos complexos, mas em operações simples que são atômicas, rápidas e distribuídas.

Na próxima vez que você precisar de um contador distribuído, pense no INCR antes de partir para soluções mais complexas. Frequentemente, a resposta mais simples é também a mais elegante.