14.2 Redis Cache
Resumo
Redis — in-memory key-value store com estruturas ricas (strings, hashes, lists, sets, sorted sets).
Laravel:
Cache::store('redis')->remember(). Estruturas: hashes (objetos), lists (queues), sets (tags, usuários online), sorted sets (leaderboards). Pipeline para batch, Transactions (MULTI/EXEC), Lua scripts para operações atômicas.Casos de uso: cache, sessões, rate limiting, distributed locks, leaderboards, pub/sub, queues. Persistência: RDB (snapshots), AOF (logs).
Conteúdo
- O que é
- Instalação
- Uso básico
- Estruturas de dados
- Pipeline
- Transações
- Padrões de cache
- Boas práticas
- Na entrevista
- Exercícios práticos
O que é
Redis: In-memory key-value store. Serve como cache, message broker, session store, leaderboard e muito mais.
Para quê:
- Muito rápido (in-memory)
- Estruturas ricas (strings, lists, sets, hashes, sorted sets)
- Persistência (opcional)
- Pub/Sub
- Transactions
- Lua scripting
Instalação
Docker:
docker run -d --name redis -p 6379:6379 redis:alpineLaravel config:
// config/database.php
'redis' => [
'client' => 'phpredis', // ou 'predis'
'default' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', 6379),
'database' => 0,
],
'cache' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', 6379),
'database' => 1, // Database separado para cache
],
],Uso básico
Laravel Cache:
// Put (set com TTL)
Cache::store('redis')->put('key', 'value', 3600);
// Get
$value = Cache::store('redis')->get('key');
// Remember (get ou set)
$users = Cache::store('redis')->remember('users', 3600, function () {
return User::all();
});
// Forget (apagar)
Cache::store('redis')->forget('key');
// Flush (apagar tudo)
Cache::store('redis')->flush();Estruturas de dados
1. Strings
Operações básicas:
// Set
Redis::set('user:1:name', 'João');
// Get
$name = Redis::get('user:1:name'); // "João"
// Set com expiration
Redis::setex('session:abc', 3600, json_encode($data));
// Increment
Redis::incr('page_views'); // 1
Redis::incr('page_views'); // 2
// Decrement
Redis::decr('stock');2. Hashes (para objetos)
// Set hash fields
Redis::hset('user:1', 'name', 'João');
Redis::hset('user:1', 'email', 'joao@email.com');
// Ou em batch
Redis::hmset('user:1', [
'name' => 'João',
'email' => 'joao@email.com',
'age' => 30,
]);
// Get field
$name = Redis::hget('user:1', 'name'); // "João"
// Get all
$user = Redis::hgetall('user:1');
// ['name' => 'João', 'email' => 'joao@email.com', 'age' => '30']
// Increment field
Redis::hincrby('user:1', 'age', 1); // 313. Lists (para queues)
// Push na list
Redis::lpush('queue', 'task1');
Redis::lpush('queue', 'task2'); // ['task2', 'task1']
// Pop da list
$task = Redis::lpop('queue'); // 'task2'
// Blocking pop (espera o item)
$task = Redis::blpop('queue', 5); // Espera 5 segundos
// Pegar o range
$tasks = Redis::lrange('queue', 0, -1); // Todos os itens
// Tamanho da list
$count = Redis::llen('queue');Caso de uso: Job Queue
// Producer
Redis::rpush('jobs:default', json_encode([
'class' => SendEmailJob::class,
'data' => ['user_id' => 1],
]));
// Consumer
while (true) {
$job = Redis::blpop('jobs:default', 5);
if ($job) {
$this->process(json_decode($job[1], true));
}
}4. Sets (elementos únicos)
// Adicionar no set
Redis::sadd('online_users', 1);
Redis::sadd('online_users', 2);
Redis::sadd('online_users', 1); // Duplicata é ignorada
// Pegar os membros
$users = Redis::smembers('online_users'); // [1, 2]
// Checar se está no set
$isOnline = Redis::sismember('online_users', 1); // true
// Remover
Redis::srem('online_users', 1);
// Contar
$count = Redis::scard('online_users');
// Operações de set
Redis::sadd('set1', 1, 2, 3);
Redis::sadd('set2', 2, 3, 4);
$intersection = Redis::sinter('set1', 'set2'); // [2, 3]
$union = Redis::sunion('set1', 'set2'); // [1, 2, 3, 4]
$diff = Redis::sdiff('set1', 'set2'); // [1]Caso de uso: Tags
// Adicionar tags no post
Redis::sadd('post:1:tags', 'php', 'laravel', 'redis');
// Pegar posts pela tag
Redis::sadd('tag:php:posts', 1, 2, 3);
Redis::sadd('tag:laravel:posts', 1, 4);
$posts = Redis::sinter('tag:php:posts', 'tag:laravel:posts'); // [1]5. Sorted Sets (para leaderboards)
// Adicionar com score
Redis::zadd('leaderboard', 100, 'player1');
Redis::zadd('leaderboard', 200, 'player2');
Redis::zadd('leaderboard', 150, 'player3');
// Pegar o top N (maiores scores)
$top = Redis::zrevrange('leaderboard', 0, 9); // Top 10
// ['player2', 'player3', 'player1']
// Pegar o rank (0-based)
$rank = Redis::zrevrank('leaderboard', 'player2'); // 0 (primeiro)
// Pegar o score
$score = Redis::zscore('leaderboard', 'player1'); // 100
// Incrementar o score
Redis::zincrby('leaderboard', 10, 'player1'); // 110
// Contar
$count = Redis::zcard('leaderboard');
// Pegar o range por score
$players = Redis::zrangebyscore('leaderboard', 100, 200);Caso de uso: Leaderboard
class LeaderboardService
{
public function addScore(int $userId, int $score): void
{
Redis::zincrby('leaderboard', $score, $userId);
}
public function getTop(int $limit = 10): array
{
return Redis::zrevrange('leaderboard', 0, $limit - 1, 'WITHSCORES');
}
public function getUserRank(int $userId): ?int
{
$rank = Redis::zrevrank('leaderboard', $userId);
return $rank !== false ? $rank + 1 : null; // 1-based
}
public function getUserScore(int $userId): int
{
return (int) Redis::zscore('leaderboard', $userId);
}
}Expiração (TTL)
// Set com TTL
Redis::setex('key', 60, 'value'); // 60 segundos
// Setar TTL numa chave existente
Redis::expire('key', 60);
// Pegar o TTL
$ttl = Redis::ttl('key'); // segundos restantes
// Remover o TTL (fica para sempre)
Redis::persist('key');Pipeline (operações em batch)
Problema: N network round-trips
// Lento: 100 round-trips
for ($i = 0; $i < 100; $i++) {
Redis::set("key:{$i}", $i);
}Solução: Pipeline
// Rápido: 1 round-trip
Redis::pipeline(function ($pipe) {
for ($i = 0; $i < 100; $i++) {
$pipe->set("key:{$i}", $i);
}
});Transações
Redis::multi();
Redis::set('key1', 'value1');
Redis::set('key2', 'value2');
Redis::incr('counter');
Redis::exec();
// Todos os comandos rodam de forma atômicaWatch (lock otimista):
Redis::watch('balance');
$balance = Redis::get('balance');
if ($balance >= 100) {
Redis::multi();
Redis::decrby('balance', 100);
Redis::incrby('points', 10);
$result = Redis::exec();
if ($result === null) {
// Transaction falhou (balance mudou)
}
} else {
Redis::unwatch();
}Lua Scripts (operações atômicas)
$script = <<<'LUA'
local current = redis.call('get', KEYS[1])
if tonumber(current) >= tonumber(ARGV[1]) then
redis.call('decrby', KEYS[1], ARGV[1])
return 1
else
return 0
end
LUA;
$result = Redis::eval($script, 1, 'balance', 100);
if ($result === 1) {
// Sucesso
} else {
// Saldo insuficiente
}Padrões de cache
1. Cache de dados do usuário
class UserRepository
{
public function find(int $id): ?User
{
return Redis::remember("user:{$id}", 3600, function () use ($id) {
return User::find($id);
});
}
public function save(User $user): void
{
$user->save();
// Invalidar o cache
Redis::forget("user:{$user->id}");
}
}2. Rate Limiting
class RateLimiter
{
public function attempt(string $key, int $maxAttempts, int $decaySeconds): bool
{
$attempts = Redis::get($key) ?? 0;
if ($attempts >= $maxAttempts) {
return false; // Rate limited
}
if ($attempts === 0) {
Redis::setex($key, $decaySeconds, 1);
} else {
Redis::incr($key);
}
return true;
}
}
// Uso
if (!$rateLimiter->attempt("login:{$ip}", 5, 60)) {
return response('Muitas tentativas', 429);
}3. Session Storage
// config/session.php
'driver' => 'redis',
'connection' => 'default',4. Distributed Lock
$lock = Cache::lock('process_orders', 10); // 10 segundos
if ($lock->get()) {
try {
// Seção crítica
$this->processOrders();
} finally {
$lock->release();
}
} else {
// Não conseguiu o lock
}Persistência
RDB (snapshot):
# redis.conf
save 900 1 # Depois de 900s se 1 chave mudou
save 300 10 # Depois de 300s se 10 chaves mudaram
save 60 10000 # Depois de 60s se 10000 chaves mudaramAOF (append-only file):
# redis.conf
appendonly yes
appendfsync everysec # ou always/noMonitoramento
Redis CLI:
# Info
redis-cli info
# Uso de memória
redis-cli info memory
# Quantidade de keys
redis-cli dbsize
# Monitorar comandos
redis-cli monitor
# Slow log
redis-cli slowlog get 10Laravel:
// Pegar info
$info = Redis::info();
// Pegar memória
$memory = Redis::info('memory');
// Contar as keys
$count = Redis::dbsize();Boas práticas
✓ Databases Redis separados para cada propósito (cache, sessions, queues)
✓ Namespace nas keys (user:1:name)
✓ TTL em todas as cache keys
✓ Pipeline para batch
✓ Lua scripts para operações atômicas
✓ Monitorar o uso de memória (eviction policy)
✓ Persistência para dados críticos
✓ Redis Sentinel/Cluster para HA
✓ NÃO guardar values enormes (< 1MB)Na entrevista
"Redis é um in-memory key-value store, muito rápido. Estruturas: strings (counters), hashes (objetos), lists (queues), sets (tags, usuários online), sorted sets (leaderboards). No Laravel, Cache::remember para cache. Pipeline para batch (menos round-trips). Transactions para operações atômicas. Lua scripts para lógica atômica mais complexa. Casos de uso: cache, sessões, rate limiting, distributed locks, leaderboards, queues. Persistência: RDB snapshots, AOF logs. Boas práticas: namespace nas keys, TTL, monitorar memória, eviction policy."
Exercícios práticos
Exercício 1: Leaderboard com Sorted Sets
Implemente um serviço de leaderboard de jogo com Redis Sorted Sets. Métodos: adicionar pontos, pegar o top 10, pegar o rank do jogador.
Solução
namespace App\Services;
use Illuminate\Support\Facades\Redis;
class LeaderboardService
{
private const LEADERBOARD_KEY = 'game:leaderboard';
/**
* Adicionar pontos ao jogador
*/
public function addScore(int $userId, int $score): void
{
// Incrementar o score no sorted set
Redis::zincrby(self::LEADERBOARD_KEY, $score, $userId);
}
/**
* Definir o valor absoluto dos pontos
*/
public function setScore(int $userId, int $score): void
{
Redis::zadd(self::LEADERBOARD_KEY, $score, $userId);
}
/**
* Pegar o top N de jogadores
*/
public function getTop(int $limit = 10): array
{
// ZREVRANGE — do maior para o menor, WITHSCORES — incluir os scores
$data = Redis::zrevrange(self::LEADERBOARD_KEY, 0, $limit - 1, 'WITHSCORES');
$result = [];
$rank = 1;
foreach ($data as $userId => $score) {
$result[] = [
'rank' => $rank++,
'user_id' => (int) $userId,
'score' => (int) $score,
];
}
return $result;
}
/**
* Pegar o rank do jogador (1-based)
*/
public function getUserRank(int $userId): ?int
{
// ZREVRANK — rank 0-based
$rank = Redis::zrevrank(self::LEADERBOARD_KEY, $userId);
return $rank !== false ? $rank + 1 : null; // 1-based
}
/**
* Pegar os pontos do jogador
*/
public function getUserScore(int $userId): int
{
return (int) Redis::zscore(self::LEADERBOARD_KEY, $userId) ?? 0;
}
/**
* Pegar as informações do jogador (rank + pontos)
*/
public function getUserInfo(int $userId): ?array
{
$score = $this->getUserScore($userId);
if ($score === 0) {
return null;
}
return [
'user_id' => $userId,
'rank' => $this->getUserRank($userId),
'score' => $score,
];
}
/**
* Pegar jogadores num intervalo de ranks
*/
public function getRange(int $start, int $end): array
{
// 0-based
$data = Redis::zrevrange(
self::LEADERBOARD_KEY,
$start - 1,
$end - 1,
'WITHSCORES'
);
$result = [];
$rank = $start;
foreach ($data as $userId => $score) {
$result[] = [
'rank' => $rank++,
'user_id' => (int) $userId,
'score' => (int) $score,
];
}
return $result;
}
/**
* Remover o jogador do leaderboard
*/
public function removeUser(int $userId): void
{
Redis::zrem(self::LEADERBOARD_KEY, $userId);
}
/**
* Resetar o leaderboard inteiro
*/
public function reset(): void
{
Redis::del(self::LEADERBOARD_KEY);
}
}
// Uso no controller
class LeaderboardController extends Controller
{
public function addScore(Request $request, LeaderboardService $leaderboard)
{
$validated = $request->validate([
'score' => 'required|integer|min:0',
]);
$leaderboard->addScore($request->user()->id, $validated['score']);
return response()->json([
'message' => 'Pontos adicionados',
'user_info' => $leaderboard->getUserInfo($request->user()->id),
]);
}
public function top(LeaderboardService $leaderboard)
{
return response()->json([
'leaderboard' => $leaderboard->getTop(10),
]);
}
public function myRank(Request $request, LeaderboardService $leaderboard)
{
return response()->json(
$leaderboard->getUserInfo($request->user()->id)
);
}
}Exercício 2: Rate Limiting com Redis
Crie um middleware de rate limiting com Redis. Limite: 60 requests por minuto por IP.
Solução
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redis;
class RateLimitMiddleware
{
private const MAX_ATTEMPTS = 60;
private const DECAY_SECONDS = 60;
public function handle(Request $request, Closure $next)
{
$key = $this->resolveRequestSignature($request);
if ($this->tooManyAttempts($key)) {
$retryAfter = $this->availableIn($key);
return response()->json([
'message' => 'Muitas requisições',
'retry_after' => $retryAfter,
], 429)->header('Retry-After', $retryAfter);
}
$this->hit($key);
$response = $next($request);
return $this->addHeaders($response, $key);
}
/**
* Montar a chave única do request
*/
protected function resolveRequestSignature(Request $request): string
{
$route = $request->route() ? $request->route()->getName() : $request->path();
return sprintf(
'rate_limit:%s:%s',
$request->ip(),
$route
);
}
/**
* Checar se o limite foi atingido
*/
protected function tooManyAttempts(string $key): bool
{
return $this->attempts($key) >= self::MAX_ATTEMPTS;
}
/**
* Pegar a quantidade de tentativas
*/
protected function attempts(string $key): int
{
return (int) Redis::get($key) ?? 0;
}
/**
* Incrementar o contador
*/
protected function hit(string $key): void
{
$attempts = Redis::get($key);
if ($attempts === null) {
// Primeiro request — setar o TTL
Redis::setex($key, self::DECAY_SECONDS, 1);
} else {
// Incrementar
Redis::incr($key);
}
}
/**
* Em quantos segundos pode tentar de novo
*/
protected function availableIn(string $key): int
{
return Redis::ttl($key);
}
/**
* Adicionar headers com a info do limite
*/
protected function addHeaders($response, string $key)
{
$attempts = $this->attempts($key);
$remaining = max(0, self::MAX_ATTEMPTS - $attempts);
return $response
->header('X-RateLimit-Limit', self::MAX_ATTEMPTS)
->header('X-RateLimit-Remaining', $remaining)
->header('X-RateLimit-Reset', now()->addSeconds($this->availableIn($key))->timestamp);
}
}
// Registro no Kernel.php
protected $middlewareAliases = [
'throttle.custom' => \App\Http\Middleware\RateLimitMiddleware::class,
];
// Uso nas routes
Route::middleware('throttle.custom')->group(function () {
Route::get('/api/posts', [PostController::class, 'index']);
});
// Versão avançada com limites diferentes
class FlexibleRateLimiter
{
public function handle(Request $request, Closure $next, int $maxAttempts = 60, int $decaySeconds = 60)
{
$key = $this->resolveRequestSignature($request);
// Sliding window algorithm
$now = time();
$windowStart = $now - $decaySeconds;
// Remover registros antigos
Redis::zremrangebyscore($key, 0, $windowStart);
// Contar os requests na janela
$currentAttempts = Redis::zcard($key);
if ($currentAttempts >= $maxAttempts) {
return response()->json([
'message' => 'Muitas requisições',
], 429);
}
// Adicionar o request atual
Redis::zadd($key, $now, $now . ':' . uniqid());
// Setar TTL na chave
Redis::expire($key, $decaySeconds);
return $next($request);
}
}Exercício 3: Distributed Lock para seção crítica
Implemente um serviço de pagamento com distributed lock para evitar cobrança em dobro.
Solução
namespace App\Services;
use App\Models\Order;
use App\Models\Payment;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class PaymentService
{
/**
* Processar o pagamento com distributed lock
*/
public function processPayment(Order $order, array $paymentData): Payment
{
$lockKey = "payment:order:{$order->id}";
// Pegar o lock por 10 segundos, esperar até 5 segundos
$lock = Cache::lock($lockKey, 10);
if (!$lock->get()) {
throw new \Exception('O pagamento já está em processamento');
}
try {
// Seção crítica
$payment = $this->processPaymentInternal($order, $paymentData);
return $payment;
} finally {
// Sempre liberar o lock
$lock->release();
}
}
/**
* Alternativa com block() — espera o lock automaticamente
*/
public function processPaymentWithBlock(Order $order, array $paymentData): Payment
{
$lockKey = "payment:order:{$order->id}";
return Cache::lock($lockKey, 10)->block(5, function () use ($order, $paymentData) {
return $this->processPaymentInternal($order, $paymentData);
});
}
private function processPaymentInternal(Order $order, array $paymentData): Payment
{
// Checar se o pedido ainda não foi pago
if ($order->status === 'paid') {
throw new \Exception('O pedido já está pago');
}
return DB::transaction(function () use ($order, $paymentData) {
// Criar o pagamento
$payment = Payment::create([
'order_id' => $order->id,
'amount' => $order->total,
'method' => $paymentData['method'],
'status' => 'processing',
]);
// Chamar o gateway de pagamento
$result = $this->chargePaymentGateway($paymentData);
if ($result['success']) {
$payment->update([
'status' => 'completed',
'transaction_id' => $result['transaction_id'],
]);
$order->update(['status' => 'paid']);
} else {
$payment->update(['status' => 'failed']);
throw new \Exception('Pagamento falhou: ' . $result['error']);
}
return $payment;
});
}
private function chargePaymentGateway(array $paymentData): array
{
// Simulação da chamada ao gateway
sleep(1);
return [
'success' => true,
'transaction_id' => 'txn_' . uniqid(),
];
}
}
// Uso no controller
class PaymentController extends Controller
{
public function process(Request $request, Order $order, PaymentService $paymentService)
{
$validated = $request->validate([
'method' => 'required|in:card,paypal',
'card_number' => 'required_if:method,card',
'cvv' => 'required_if:method,card',
]);
try {
$payment = $paymentService->processPayment($order, $validated);
return response()->json([
'message' => 'Pagamento processado com sucesso',
'payment' => $payment,
]);
} catch (\Exception $e) {
return response()->json([
'message' => $e->getMessage(),
], 400);
}
}
}
// Implementação custom de Lock
class RedisLock
{
private string $key;
private int $seconds;
private ?string $owner = null;
public function __construct(string $key, int $seconds)
{
$this->key = $key;
$this->seconds = $seconds;
}
public function acquire(): bool
{
$this->owner = uniqid();
// SET key owner NX EX seconds
// NX — só se a chave não existir
// EX — expiration
$result = Redis::set(
$this->key,
$this->owner,
'EX',
$this->seconds,
'NX'
);
return $result === true;
}
public function release(): bool
{
// Lua script para check-and-delete atômico
$script = <<<'LUA'
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
LUA;
return Redis::eval($script, 1, $this->key, $this->owner) === 1;
}
}Parte do PHP/Laravel Interview Handbook | Feito com ❤️ pela equipe CodeMate