Sessões e Rate Limiting
Aula 4 de 6
Session Storage
Redis + express-session (Node.js)
const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
const redisClient = createClient({ url: 'redis://localhost:6379' });
redisClient.connect();
const app = express();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: 'seu-segredo-aqui',
resave: false,
saveUninitialized: false,
name: 'sid', // nome do cookie
cookie: {
httpOnly: true, // não acessível via JS
secure: true, // apenas HTTPS
maxAge: 1800000, // 30 minutos
sameSite: 'strict'
},
rolling: true // renovar TTL a cada request
}));
Sessão no Redis
# Estrutura real no Redis
# session:{sid} = { dados da sessão em JSON }
# Criar sessão manualmente
SET session:abc123 '{"userId":1001,"role":"admin","loginAt":"2024-01-15T10:00:00Z"}' EX 1800
# Sliding expiration (renovar a cada acesso)
EXPIRE session:abc123 1800
# Ver sessão
GET session:abc123
TTL session:abc123
# Remover sessão (logout)
DEL session:abc123
# Listar sessões ativas (SCAN para produção)
SCAN 0 MATCH session:* COUNT 100
Cookie-based vs Server-side
# Cookie-based (JWT): sessão no cliente
# - Prós: sem armazenamento server-side, escalável horizontalmente
# - Contras: difícil invalidar, payload visível (assinado, não criptografado)
# - Uso: APIs REST stateless
# Server-side (Redis): sessão no servidor
# - Prós: fácil invalidar, controle total, mais seguro
# - Contras: dependência de Redis, latência adicional
# - Uso: aplicações web tradicionais, sessões com estado
# Session Rotation: trocar session ID periodicamente
# Previne session fixation attacks
# Após login, gerar novo SID e deletar o antigo
Rate Limiting
Sliding Window (ZREMRANGEBYSCORE + ZCARD)
# Sliding window com Sorted Set
# Janela de tempo deslizante, precisa, consistente
# Função Lua para rate limit
cat << 'EOF' > sliding-window.lua
-- KEYS[1] = rate limit key
-- ARGV[1] = window size (ms)
-- ARGV[2] = max requests
-- ARGV[3] = current timestamp
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local max = tonumber(ARGV[3])
local window_start = now - window
-- Remover entradas fora da janela
redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, window_start)
-- Contar requisições na janela
local count = redis.call('ZCARD', KEYS[1])
if count >= max then
return 0 -- limit exceeded
end
-- Adicionar requisição atual
redis.call('ZADD', KEYS[1], now, now)
redis.call('EXPIRE', KEYS[1], window / 1000)
return max - count -- remaining
EOF
# Testar sliding window
echo "=== SLIDING WINDOW ==="
redis-cli << 'REDIS'
-- Limpar
DEL rate:user:1001
-- Simular 5 requisições
EVAL "
local now = tonumber(ARGV[1])
local window = 60000
local max = 5
local window_start = now - window
redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, window_start)
local count = redis.call('ZCARD', KEYS[1])
if count >= max then return 0 end
redis.call('ZADD', KEYS[1], now, now)
redis.call('EXPIRE', KEYS[1], 60)
return max - count
" 1 rate:user:1001 1000 60000 5
-- Verificar (deve retornar 4, 3, 2, 1, 0)
REDIS
Token Bucket (Lua Script)
# Token Bucket: tokens são adicionados a uma taxa constante
# Permite bursts até o bucket capacity
cat << 'EOF' > token-bucket.lua
-- KEYS[1] = bucket key
-- ARGV[1] = capacity (max tokens)
-- ARGV[2] = refill rate (tokens per second)
-- ARGV[3] = request cost (default 1)
-- ARGV[4] = current timestamp
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local cost = tonumber(ARGV[3]) or 1
local now = tonumber(ARGV[4])
-- Buscar estado atual do bucket
local bucket = redis.call('HMGET', KEYS[1], 'tokens', 'last_refill')
local tokens = tonumber(bucket[1]) or capacity
local last_refill = tonumber(bucket[2]) or now
-- Refill baseado no tempo passado
local elapsed = math.max(0, now - last_refill)
local refill = math.floor(elapsed * rate / 1000)
if refill > 0 then
tokens = math.min(capacity, tokens + refill)
last_refill = now
end
if tokens < cost then
-- Taxa excedida
return -1
end
-- Consumir tokens
tokens = tokens - cost
redis.call('HMSET', KEYS[1], 'tokens', tokens, 'last_refill', last_refill)
redis.call('EXPIRE', KEYS[1], math.ceil(capacity / rate * 2))
return tokens -- tokens restantes
EOF
Fixed Window (INCR + EXPIRE)
# Fixed Window: contador que reseta a cada janela fixa
# Simples, mas permite picos no final de uma janela + início da próxima
# Implementação
cat << 'EOF' > fixed-window.sh
#!/bin/bash
# Uso: ./fixed-window.sh <key> <limit> <window_seconds>
KEY="$1"
LIMIT="${2:-100}"
WINDOW="${3:-60}"
# INCR + EXPIRE usando Redis
# Se chave não existir, setar EXPIRE junto com INCR
current=$(redis-cli INCR "$KEY")
if [[ $current -eq 1 ]]; then
redis-cli EXPIRE "$KEY" "$WINDOW"
fi
if [[ $current -gt $LIMIT ]]; then
echo "RATE_LIMITED (${current}/${LIMIT})"
exit 1
fi
echo "OK (${current}/${LIMIT})"
EOF
chmod +x fixed-window.sh
# Testar fixed window
echo "=== FIXED WINDOW ==="
for i in $(seq 1 5); do
./fixed-window.sh test:api:1001 3 60
sleep 0.5
done
Generic Cell Rate Algorithm (GCRA)
# GCRA: algoritmo mais preciso, usado pelo Redis Rate Limiter do Laravel
# Implementa taxa via "time until next allowed"
# Implementação Lua
cat << 'EOF' > gcra.lua
-- KEYS[1] = key
-- ARGV[1] = rate (requests per second)
-- ARGV[2] = burst size
-- ARGV[3] = now (ms)
local rate = tonumber(ARGV[1])
local burst = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local interval = 1000000 / rate -- microssegundos entre requests
local increment = interval * burst
local emission_time = redis.call('GET', KEYS[1])
if not emission_time then
emission_time = now * 1000 - increment
redis.call('SET', KEYS[1], emission_time, 'PX', math.ceil(increment / 1000))
return {1, 0}
end
emission_time = tonumber(emission_time)
local new_emission = math.max(emission_time + interval, now * 1000)
local delay = new_emission - now * 1000
if delay > increment then
return {0, math.ceil(delay / 1000)}
end
redis.call('SET', KEYS[1], new_emission, 'PX', math.ceil((new_emission - emission_time + increment) / 1000))
return {1, math.ceil(delay / 1000)}
EOF
Distributed Locks
Redlock Algorithm
# Redlock: lock distribuído para múltiplos nós Redis
# Implementação: SET NX EX em N nós master independentes
# Lock básico (SET NX EX)
SET lock:resource:1 "server:A" NX EX 10
# Retorna OK se adquiriu, nil se já existe
# Unlock (Lua: verificar se é o owner)
cat << 'EOF' > unlock.lua
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
EOF
# Lock com extensão (auto-extension)
# Claude de lock: enquanto processa, estende o TTL periodicamente
# Implementação Node.js
cat << 'EOF' > distributed-lock.js
const redis = require('ioredis');
const client = new Redis();
async function acquireLock(key, ttl = 10000) {
const lockValue = `${process.pid}-${Date.now()}`;
const acquired = await client.set(key, lockValue, 'NX', 'PX', ttl);
if (!acquired) return null;
// Auto-extensão (watchdog)
const interval = setInterval(async () => {
await client.pexpire(key, ttl);
}, ttl / 2);
return {
value: lockValue,
release: async () => {
clearInterval(interval);
await client.eval(`
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
end
`, 1, key, lockValue);
}
};
}
// Uso
const lock = await acquireLock('resource:process-order');
if (lock) {
try {
await processOrder();
} finally {
await lock.release();
}
}
EOF
Redlock Controversy
# Redlock é controverso (Martin Kleppmann vs Antirez)
# Críticas principais:
# - Depende de timing (clock drift pode quebrar garantias)
# - F Jepsen encontrou problemas com GC pause
# - Para a maioria dos casos, SET NX EX simples é suficiente
# - Alternativas:
# - Redisson (Redis-based, mature)
# - etcd / ZooKeeper (strong consistency)
# - PostgreSQL advisory locks
# - SET NX EX (single node, suficiente para 99% dos casos)
# ⚠️ Redlock é útil apenas quando:
# - Múltiplos nós Redis independentes
# - Garantia de exclusão mútua crítica
# - Não pode usar sistemas externos (etcd, ZK)
Counters
Contadores Simples
# INCR / DECR atômicos
INCR page:views:home # 1
INCR page:views:home # 2
GET page:views:home # 2
# INCRBY / DECRBY
INCRBY user:1001:points 50 # +50
INCRBY user:1001:points -10 # -10
# Com TTL (cache counter)
INCR api:calls:2024-01-15
EXPIRE api:calls:2024-01-15 86400
# Reset periódico
SET daily:signups "0" EX 86400
INCR daily:signups
Leaderboard com ZINCRBY
# Sorted Set para ranking em tempo real
ZADD leaderboard:global 0 "player:1001"
ZADD leaderboard:global 0 "player:1002"
# Incrementar score
ZINCRBY leaderboard:global 100 "player:1001" # +100 pontos
ZINCRBY leaderboard:global 50 "player:1002"
# Top 10
ZREVRANGE leaderboard:global 0 9 WITHSCORES
# Posição do jogador
ZREVRANK leaderboard:global "player:1001"
# Score do jogador
ZSCORE leaderboard:global "player:1001"
# Rangos em volta
ZREVRANGEBYSCORE leaderboard:global +inf -inf WITHSCORES LIMIT 0 10
Daily/Weekly/Monthly Counters
# Estratégia de partição temporal
# Chaves diárias
INCR stats:daily:2024-01-15:signups
INCR stats:daily:2024-01-15:orders
# Chaves semanais (ISO week)
INCR stats:weekly:2024-W03:revenue
INCR stats:weekly:2024-W03:active_users
# Chaves mensais
INCR stats:monthly:2024-01:page_views
# Buscar agregado (MGET)
MGET stats:daily:2024-01-15:signups stats:daily:2024-01-16:signups
# Somar com Lua
EVAL "
local total = 0
for _, key in ipairs(KEYS) do
total = total + (redis.call('GET', key) or 0)
end
return total
" 3 stats:daily:2024-01-13:signups stats:daily:2024-01-14:signups stats:daily:2024-01-15:signups
Lab: API Rate Limiter Completo
cat > rate-limiter.sh << 'SCRIPT'
#!/bin/bash
set -euo pipefail
echo "=== API RATE LIMITER ==="
LIMIT=5
WINDOW=60
API_KEY="12345"
check_rate() {
local key="rate:api:${API_KEY}"
local current
current=$(redis-cli INCR "$key")
[[ $current -eq 1 ]] && redis-cli EXPIRE "$key" "$WINDOW" > /dev/null
if [[ $current -gt $LIMIT ]]; then
local ttl
ttl=$(redis-cli TTL "$key")
echo "429 Too Many Requests - Reset in ${ttl}s"
return 1
fi
local remaining=$((LIMIT - current))
echo "200 OK - ${remaining}/${LIMIT} requests remaining"
return 0
}
echo ""
echo "--- Teste: ${LIMIT} requests em ${WINDOW}s ---"
for i in $(seq 1 7); do
echo "Request $i: $(check_rate)"
sleep 0.3
done
echo ""
echo "--- Contador temporal + scoreboard ---"
redis-cli << 'REDIS'
-- Scoreboard diário
INCR stats:api:calls:today
INCR stats:api:users:today
-- Top API keys por uso
ZINCRBY stats:api:topkeys 1 "api_key:12345"
ZINCRBY stats:api:topkeys 1 "api_key:67890"
ZINCRBY stats:api:topkeys 1 "api_key:12345"
-- Leaderboard
ZREVRANGE stats:api:topkeys 0 -1 WITHSCORES
REDIS
echo ""
echo "--- Sliding Window (Lua) ---"
redis-cli --eval sliding-window.lua rate:sliding:test , 60000 5 "$(date +%s%N | cut -b1-13)"
redis-cli --eval sliding-window.lua rate:sliding:test , 60000 5 "$(date +%s%N | cut -b1-13)"
echo ""
echo "--- Limpeza ---"
redis-cli EVAL "return redis.call('DEL', unpack(redis.call('KEYS', 'rate:*')))" 0 2>/dev/null || true
redis-cli EVAL "return redis.call('DEL', unpack(redis.call('KEYS', 'stats:*')))" 0 2>/dev/null || true
echo "Dados limpos."
SCRIPT
chmod +x rate-limiter.sh
Sessões no Redis com sliding TTL (rolling). Rate limiting: fixed window (INCR+EXPIRE) é simples, sliding window (Sorted Set) é preciso, token bucket permite bursts. Locks com SET NX EX e Lua garantem atomicidade. Counters com INCR e ZINCRBY são atômicos e performáticos.