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/ZRANGEBYSCOREou um algoritmo de token bucket. Além disso, vale observar que o padrãoINCRseguido deEXPIREpode sofrer uma race condition sutil: outro processo pode incrementar a chave entre oINCRe oEXPIRE. Para maior robustez, você pode encapsular ambos em um script Lua, garantindo a execução atômica deINCR+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
ActiveSessionsKeynã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 comZRANGEBYSCORE, 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.
Top comments (0)