6.8 Redis
Resumo
Redis — banco in-memory key-value para cache, queues, sessões e dados real-time.
Estruturas: strings, hashes, lists, sets, sorted sets. Guarda os dados na RAM (muito rápido).
Importante: No Laravel: Cache::store('redis'), SESSION_DRIVER=redis, QUEUE_CONNECTION=redis. Leaderboards com sorted sets (zadd, zrevrange).
Conteúdo
O que é
O que é: Redis — banco in-memory (key-value store). Serve para cache, queues, sessões e dados real-time.
O essencial:
- Guarda os dados na RAM (muito rápido)
- Estrutura key-value
- Estruturas: strings, lists, sets, hashes
Como funciona
Instalação e config:
# Instalar Redis
brew install redis # macOS
apt-get install redis # Ubuntu
# Iniciar
redis-server
# Conexão no Laravel (.env)
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379Operações básicas:
use Illuminate\Support\Facades\Redis;
// SET (definir o valor)
Redis::set('user:1:name', 'João Silva');
// GET (ler o valor)
$name = Redis::get('user:1:name'); // 'João Silva'
// SETEX (com TTL em segundos)
Redis::setex('temp:data', 3600, 'value'); // Expira em 1 hora
// DEL (apagar)
Redis::del('user:1:name');
// EXISTS (checar se existe)
if (Redis::exists('user:1:name')) {
// A chave existe
}
// INCR / DECR (incremento/decremento)
Redis::incr('page:views'); // +1
Redis::incrby('page:views', 10); // +10
Redis::decr('page:views'); // -1Estruturas de dados:
// HASH (array associativo)
Redis::hset('user:1', 'name', 'João');
Redis::hset('user:1', 'email', 'joao@email.com');
Redis::hget('user:1', 'name'); // 'João'
Redis::hgetall('user:1'); // ['name' => 'João', 'email' => '...']
// LIST (lista)
Redis::rpush('queue', 'job1'); // Adicionar no fim
Redis::rpush('queue', 'job2');
Redis::lpop('queue'); // Tirar do começo ('job1')
Redis::lrange('queue', 0, -1); // Todos os elementos
// SET (conjunto de elementos únicos)
Redis::sadd('tags', 'php');
Redis::sadd('tags', 'laravel');
Redis::sadd('tags', 'php'); // Duplicata não entra
Redis::smembers('tags'); // ['php', 'laravel']
Redis::sismember('tags', 'php'); // true
// SORTED SET (conjunto ordenado)
Redis::zadd('leaderboard', 100, 'user1');
Redis::zadd('leaderboard', 250, 'user2');
Redis::zadd('leaderboard', 150, 'user3');
Redis::zrevrange('leaderboard', 0, 9); // Top 10 (decrescente)
// ['user2', 'user3', 'user1']Quando usar
Use Redis para:
- Cache (dados lidos com frequência)
- Sessões (acesso rápido)
- Queues (Jobs)
- Rate limiting
- Dados real-time (leaderboards, counters)
- Pub/Sub (Broadcasting)
Não use para:
- Persistência permanente (Redis é in-memory; sem persistence, os dados somem no restart)
- Volumes grandes (limitado pela RAM)
- Queries complexas (não tem SQL)
Exemplo prático
Cache de dados:
// Cachear o resultado da query
use Illuminate\Support\Facades\Cache;
$users = Cache::remember('users.all', 3600, function () {
return User::all();
});
// Com o driver Redis (.env: CACHE_DRIVER=redis)
$users = Cache::store('redis')->remember('users.all', 3600, function () {
return User::all();
});
// Cache com tags
$posts = Cache::tags(['posts', 'published'])->remember('posts.published', 3600, function () {
return Post::where('published', true)->get();
});
// Limpar a tag
Cache::tags(['posts'])->flush();Rate Limiting:
use Illuminate\Support\Facades\RateLimiter;
// Limitar tentativas de login
if (RateLimiter::tooManyAttempts('login:' . $email, 5)) {
$seconds = RateLimiter::availableIn('login:' . $email);
throw new TooManyRequestsException("Tente de novo em {$seconds} segundos");
}
RateLimiter::hit('login:' . $email, 60); // +1 tentativa, TTL 60 segundos
// Depois do login com sucesso
RateLimiter::clear('login:' . $email);
// API rate limiting
Route::middleware('throttle:60,1')->group(function () {
// 60 requests por minuto
});Session Storage:
// .env
SESSION_DRIVER=redis
// Sessões vão automaticamente para o Redis
session(['key' => 'value']);
$value = session('key');Queues (filas):
// .env
QUEUE_CONNECTION=redis
// Job vai automaticamente para a queue no Redis
SendEmail::dispatch($user);
// Iniciar o worker
php artisan queue:work redisCounters (contadores):
// Contador de views
class PostController extends Controller
{
public function show(Post $post)
{
// Incremento no Redis
Redis::incr("post:{$post->id}:views");
// Sincronizar com o banco de vez em quando (em background)
dispatch(new SyncPostViews($post));
return view('posts.show', compact('post'));
}
public function getViews(Post $post): int
{
// Leitura rápida no Redis
return (int) Redis::get("post:{$post->id}:views") ?: 0;
}
}
// Job de sincronização
class SyncPostViews implements ShouldQueue
{
public function handle(): void
{
$postIds = Post::pluck('id');
foreach ($postIds as $postId) {
$views = Redis::get("post:{$postId}:views");
if ($views) {
Post::where('id', $postId)->update(['views' => $views]);
}
}
}
}Leaderboard (top de jogadores):
class LeaderboardService
{
public function addScore(User $user, int $score): void
{
// Adicionar no sorted set
Redis::zadd('leaderboard', $score, $user->id);
}
public function getTop(int $limit = 10): Collection
{
// Top N em ordem decrescente
$userIds = Redis::zrevrange('leaderboard', 0, $limit - 1);
return User::whereIn('id', $userIds)
->get()
->sortBy(function ($user) use ($userIds) {
return array_search($user->id, $userIds);
});
}
public function getUserRank(User $user): int
{
// Posição do usuário (do maior para o menor)
$rank = Redis::zrevrank('leaderboard', $user->id);
return $rank !== null ? $rank + 1 : 0;
}
}Lock (bloqueio):
use Illuminate\Support\Facades\Cache;
// Pegar o lock
$lock = Cache::lock('process-orders', 10); // 10 segundos
if ($lock->get()) {
try {
// Seção crítica (só um processo)
processOrders();
} finally {
$lock->release();
}
} else {
// Não conseguiu o lock
Log::info('Outro processo já está rodando');
}
// Ou com release automático
Cache::lock('process-orders', 10)->block(5, function () {
// Espera até 5 segundos, depois executa
processOrders();
});Pub/Sub (Broadcasting):
// Publisher
Redis::publish('notifications', json_encode([
'message' => 'Novo pedido criado',
'order_id' => $order->id,
]));
// Subscriber (em processo separado)
Redis::subscribe(['notifications'], function (string $message) {
$data = json_decode($message, true);
Log::info('Notificação recebida', $data);
});Cache aside pattern:
class UserRepository
{
public function find(int $id): ?User
{
// 1. Checar o cache
$cached = Redis::get("user:{$id}");
if ($cached) {
return unserialize($cached);
}
// 2. Carregar do banco
$user = User::find($id);
if ($user) {
// 3. Guardar no cache
Redis::setex("user:{$id}", 3600, serialize($user));
}
return $user;
}
public function update(User $user): void
{
// 1. Atualizar o banco
$user->save();
// 2. Invalidar o cache
Redis::del("user:{$user->id}");
// Ou atualizar o cache
Redis::setex("user:{$user->id}", 3600, serialize($user));
}
}Na entrevista
"Redis é um banco in-memory key-value para cache, queues e sessões. Estruturas: strings, hashes, lists, sets, sorted sets. No Laravel: Cache::store('redis'), SESSION_DRIVER=redis, QUEUE_CONNECTION=redis. Rate limiting com RateLimiter. Leaderboards com sorted sets (zadd, zrevrange). Lock com Cache::lock() para seção crítica. Pub/Sub para real-time. Cache aside: checa o cache → carrega do banco → guarda no cache."
Exercícios práticos
Exercício 1: Rate Limiting na API
Limite o endpoint da API: no máximo 10 requests por minuto por IP. Se passar, devolva 429.
Solução
// Middleware de rate limiting
class RateLimitMiddleware
{
public function handle(Request $request, Closure $next, int $maxAttempts = 10, int $decayMinutes = 1)
{
$key = $this->resolveRequestSignature($request);
if (RateLimiter::tooManyAttempts($key, $maxAttempts)) {
$seconds = RateLimiter::availableIn($key);
return response()->json([
'message' => 'Muitas requisições. Tente de novo mais tarde.',
'retry_after' => $seconds,
], 429)->header('Retry-After', $seconds);
}
RateLimiter::hit($key, $decayMinutes * 60);
$response = $next($request);
return $this->addHeaders(
$response,
$maxAttempts,
RateLimiter::remaining($key, $maxAttempts)
);
}
protected function resolveRequestSignature(Request $request): string
{
return 'api:' . $request->ip();
}
protected function addHeaders($response, int $maxAttempts, int $remainingAttempts)
{
return $response->withHeaders([
'X-RateLimit-Limit' => $maxAttempts,
'X-RateLimit-Remaining' => max(0, $remainingAttempts),
]);
}
}
// Uso nas routes
Route::middleware(['rate.limit:10,1'])->group(function () {
Route::get('/api/posts', [PostController::class, 'index']);
});
// Ou o throttle middleware nativo
Route::middleware('throttle:10,1')->group(function () {
Route::get('/api/posts', [PostController::class, 'index']);
});
// Para usuários autenticados (por user_id)
Route::middleware('auth:sanctum', 'throttle:60,1')->group(function () {
Route::post('/api/posts', [PostController::class, 'store']);
});
// No RouteServiceProvider para limite customizado
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
// Limites diferentes por tipo de usuário
RateLimiter::for('api', function (Request $request) {
return $request->user()?->is_premium
? Limit::perMinute(1000)->by($request->user()->id)
: Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});Exercício 2: Leaderboard (top de jogadores)
Crie um ranking de jogadores. Precisa devolver o top 100 e a posição de um jogador específico.
Solução
class LeaderboardService
{
protected string $key = 'game:leaderboard';
// Adicionar/atualizar o score do jogador
public function updateScore(User $user, int $score): void
{
// zadd adiciona ou atualiza o score
Redis::zadd($this->key, $score, $user->id);
}
// Incrementar o score
public function incrementScore(User $user, int $points): int
{
return Redis::zincrby($this->key, $points, $user->id);
}
// Pegar o top N
public function getTop(int $limit = 100): Collection
{
// zrevrange devolve do maior para o menor
$userIds = Redis::zrevrange($this->key, 0, $limit - 1, 'WITHSCORES');
// $userIds = ['user1' => 1000, 'user2' => 950, ...]
$users = User::whereIn('id', array_keys($userIds))->get()->keyBy('id');
return collect($userIds)->map(function ($score, $userId) use ($users) {
return [
'user' => $users[$userId] ?? null,
'score' => (int) $score,
'rank' => Redis::zrevrank($this->key, $userId) + 1,
];
})->values();
}
// Pegar a posição do jogador
public function getUserRank(User $user): ?int
{
$rank = Redis::zrevrank($this->key, $user->id);
return $rank !== null ? $rank + 1 : null;
}
// Pegar o score do jogador
public function getUserScore(User $user): int
{
return (int) Redis::zscore($this->key, $user->id) ?: 0;
}
// Pegar os jogadores em volta do usuário
public function getSurroundingPlayers(User $user, int $range = 5): Collection
{
$userRank = $this->getUserRank($user);
if (!$userRank) {
return collect();
}
$start = max(0, $userRank - $range - 1);
$end = $userRank + $range - 1;
$userIds = Redis::zrevrange($this->key, $start, $end, 'WITHSCORES');
$users = User::whereIn('id', array_keys($userIds))->get()->keyBy('id');
return collect($userIds)->map(function ($score, $userId) use ($users) {
return [
'user' => $users[$userId] ?? null,
'score' => (int) $score,
'rank' => Redis::zrevrank($this->key, $userId) + 1,
];
})->values();
}
// Limpar o leaderboard
public function clear(): void
{
Redis::del($this->key);
}
}
// Uso
$leaderboard = new LeaderboardService();
// Atualizar o score
$leaderboard->updateScore($user, 1500);
$leaderboard->incrementScore($user, 100);
// Top 10
$top10 = $leaderboard->getTop(10);
// Posição do jogador
$rank = $leaderboard->getUserRank($user);
// Jogadores em volta
$surrounding = $leaderboard->getSurroundingPlayers($user, 3);
// API endpoint
public function leaderboard()
{
$leaderboard = new LeaderboardService();
return response()->json([
'top_100' => $leaderboard->getTop(100),
'current_user' => [
'rank' => $leaderboard->getUserRank(auth()->user()),
'score' => $leaderboard->getUserScore(auth()->user()),
],
]);
}Exercício 3: Distributed Lock
Crie um sistema de locks para impedir que vários processos executem a mesma tarefa ao mesmo tempo.
Solução
// Uso básico do Cache::lock()
class OrderProcessor
{
public function processOrder(Order $order): void
{
$lock = Cache::lock("order:{$order->id}:processing", 10);
if ($lock->get()) {
try {
// Só um processo executa este código
$this->doProcessing($order);
} finally {
$lock->release();
}
} else {
Log::info("Pedido {$order->id} já está em processamento");
}
}
}
// Com espera automática
class EmailSender
{
public function sendBulkEmails(Collection $users): void
{
Cache::lock('send-bulk-emails', 60)->block(5, function () use ($users) {
// Espera até 5 segundos pelo lock
// Se pegou — executa; senão throw LockTimeoutException
foreach ($users as $user) {
Mail::to($user)->send(new NewsletterEmail());
}
});
}
}
// Renovar o lock (operações longas)
class DataImporter
{
public function import(string $file): void
{
$lock = Cache::lock('data-import', 120); // 2 minutos
if ($lock->get()) {
try {
$lines = file($file);
foreach ($lines as $line) {
$this->processLine($line);
// Renovar o lock a cada 100 linhas
if ($line % 100 === 0) {
$lock->get(); // Atualizar o TTL
}
}
} finally {
$lock->release();
}
}
}
}
// Implementação custom com Redis
class CustomLock
{
protected string $key;
protected int $timeout;
protected ?string $owner = null;
public function __construct(string $key, int $timeout = 10)
{
$this->key = "lock:{$key}";
$this->timeout = $timeout;
}
public function acquire(): bool
{
$this->owner = Str::random(20);
// SET NX EX — seta se não existir, com TTL
$acquired = Redis::set(
$this->key,
$this->owner,
'EX',
$this->timeout,
'NX'
);
return $acquired === true;
}
public function release(): bool
{
// Apagar só se formos o dono (Lua script para atomicidade)
$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;
}
public function forceRelease(): void
{
Redis::del($this->key);
}
}
// Uso do lock custom
$lock = new CustomLock('critical-section', 30);
if ($lock->acquire()) {
try {
// Seção crítica
processCriticalTask();
} finally {
$lock->release();
}
} else {
Log::info('Não conseguiu o lock');
}
// Distributed lock para scheduled tasks
class ScheduledTaskLock
{
public function handle(): void
{
$lock = Cache::lock('scheduled:daily-report', 3600);
if ($lock->get()) {
// Só um servidor executa esta tarefa
try {
$this->generateDailyReport();
} finally {
$lock->release();
}
}
}
}
// No Kernel.php
protected function schedule(Schedule $schedule): void
{
$schedule->call(function () {
Cache::lock('scheduled:cleanup', 3600)->get(function () {
// Lógica de cleanup
});
})->daily();
}Parte do PHP/Laravel Interview Handbook | Feito com ❤️ pela equipe CodeMate