Caching com Redis e Predis
1. Introdução ao Redis como Sistema de Cache
Redis é um armazenamento de estrutura de dados em memória, de código aberto, que pode ser usado como banco de dados, cache e message broker. Quando falamos de caching em PHP, Redis oferece vantagens significativas sobre cache em arquivo ou banco de dados relacional:
- Velocidade: opera em memória RAM, com latência de microssegundos
- Estruturas de dados ricas: strings, hashes, lists, sets, sorted sets
- Persistência opcional: pode salvar dados em disco sem perder performance
- Escalabilidade: suporte a clustering e replicação
- TTL nativo: expiração automática de chaves
Para instalar o Redis no Ubuntu/Debian:
sudo apt update
sudo apt install redis-server
sudo systemctl enable redis-server
sudo systemctl start redis-server
Verifique a instalação:
redis-cli ping
# Resposta esperada: PONG
2. Configurando o Predis no Projeto PHP
Predis é um cliente Redis completo escrito em PHP. Instale via Composer:
composer require predis/predis
Configuração básica de conexão:
<?php
require 'vendor/autoload.php';
use Predis\Client;
// Configuração simples
$redis = new Client([
'scheme' => 'tcp',
'host' => '127.0.0.1',
'port' => 6379,
]);
// Com timeout e reconexão automática
$redis = new Client([
'scheme' => 'tcp',
'host' => '127.0.0.1',
'port' => 6379,
'timeout' => 5.0,
'read_write_timeout' => 5.0,
'replication' => 'sentinel',
], [
'prefix' => 'myapp:',
'exceptions' => true,
]);
// Tratamento de erros
try {
$redis->ping();
} catch (Predis\Connection\ConnectionException $e) {
error_log("Redis indisponível: " . $e->getMessage());
// Fallback para cache alternativo
}
3. Operações Básicas de Cache com Predis
Set e Get com TTL
<?php
// Armazenar valor por 3600 segundos (1 hora)
$redis->setex('user:1001:profile', 3600, json_encode([
'id' => 1001,
'name' => 'João Silva',
'email' => 'joao@example.com'
]));
// Recuperar valor
$profile = $redis->get('user:1001:profile');
if ($profile) {
$data = json_decode($profile, true);
echo $data['name']; // João Silva
}
// Verificar se chave existe
if ($redis->exists('user:1001:profile')) {
echo "Cache hit!";
}
// Remover chave
$redis->del('user:1001:profile');
Operações em Massa
<?php
// MSET - definir múltiplas chaves
$redis->mset([
'product:1:price' => 29.90,
'product:2:price' => 49.90,
'product:3:price' => 99.90,
]);
// MGET - recuperar múltiplas chaves
$prices = $redis->mget(['product:1:price', 'product:2:price', 'product:3:price']);
// ['29.90', '49.90', '99.90']
// DEL em massa
$redis->del(['product:1:price', 'product:2:price']);
4. Estratégias de Cache para Aplicações PHP
Cache de Consultas ao Banco de Dados
<?php
function getRecentPosts(PDO $pdo, Client $redis, int $limit = 10): array {
$cacheKey = "posts:recent:{$limit}";
// Tentar cache primeiro
$cached = $redis->get($cacheKey);
if ($cached !== null) {
return json_decode($cached, true);
}
// Cache miss - buscar do banco
$stmt = $pdo->prepare("SELECT * FROM posts ORDER BY created_at DESC LIMIT ?");
$stmt->execute([$limit]);
$posts = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Armazenar em cache por 5 minutos
$redis->setex($cacheKey, 300, json_encode($posts));
return $posts;
}
Cache de Resultados de APIs Externas
<?php
function getWeatherData(string $city): array {
$redis = new Client();
$cacheKey = "weather:{$city}";
// Verificar cache
$cached = $redis->get($cacheKey);
if ($cached) {
return json_decode($cached, true);
}
// API externa
$apiKey = getenv('WEATHER_API_KEY');
$url = "https://api.weather.com/v1/{$city}?apikey={$apiKey}";
$response = file_get_contents($url);
$data = json_decode($response, true);
// Cache por 30 minutos (dados climáticos mudam com frequência)
$redis->setex($cacheKey, 1800, json_encode($data));
return $data;
}
Cache de Sessões de Usuário
<?php
// Configurar sessão com Redis
session_set_save_handler(
function ($savePath, $sessionName) use ($redis) {
return true;
},
function () {
return true;
},
function ($sessionId) use ($redis) {
return $redis->get("session:{$sessionId}") ?: '';
},
function ($sessionId, $data) use ($redis) {
$redis->setex("session:{$sessionId}", 3600, $data);
return true;
},
function ($sessionId) use ($redis) {
$redis->del("session:{$sessionId}");
return true;
},
function ($maxLifetime) use ($redis) {
// Redis gerencia TTL automaticamente
return true;
}
);
session_start();
5. Tipos de Dados do Redis para Caching Avançado
Hash: Armazenar Objetos Estruturados
<?php
// Armazenar usuário como hash
$redis->hmset('user:500', [
'name' => 'Maria Santos',
'email' => 'maria@example.com',
'age' => 28,
'city' => 'São Paulo'
]);
// Acessar campos específicos
echo $redis->hget('user:500', 'name'); // Maria Santos
// Incrementar campo numérico
$redis->hincrby('user:500', 'login_count', 1);
List: Filas e Cache de Listagens Paginadas
<?php
// Cache de últimas notícias (mantém apenas 100 itens)
$newsKey = 'news:latest';
// Adicionar nova notícia ao início
$redis->lpush($newsKey, json_encode([
'id' => 1500,
'title' => 'Nova atualização do PHP 8.3',
'date' => '2024-01-15'
]));
// Manter apenas 100 itens mais recentes
$redis->ltrim($newsKey, 0, 99);
// Obter página 2 (itens 10-19)
$page = $redis->lrange($newsKey, 10, 19);
Sorted Sets: Ranking e Cache Ordenado
<?php
// Ranking de jogadores
$redis->zadd('game:leaderboard', [
'player:100' => 1500,
'player:200' => 2300,
'player:300' => 1800,
]);
// Top 10 jogadores
$topPlayers = $redis->zrevrange('game:leaderboard', 0, 9, 'WITHSCORES');
// Atualizar pontuação
$redis->zincrby('game:leaderboard', 100, 'player:100');
// Obter rank de um jogador
$rank = $redis->zrevrank('game:leaderboard', 'player:100');
6. Padrões de Cache e Invalidação
Cache-Aside (Lazy Loading)
<?php
function getProduct(int $productId): array {
$redis = new Client();
$cacheKey = "product:{$productId}";
// Tentar cache primeiro
$cached = $redis->get($cacheKey);
if ($cached !== null) {
return json_decode($cached, true);
}
// Buscar do banco de dados
$product = $db->query("SELECT * FROM products WHERE id = ?", [$productId]);
if ($product) {
// Armazenar em cache com TTL
$redis->setex($cacheKey, 3600, json_encode($product));
}
return $product;
}
Invalidação com Chaves Versionadas
<?php
$version = $redis->get('product:version') ?: 1;
// Chave inclui versão para invalidação forçada
$cacheKey = "product:{$productId}:v{$version}";
// Quando produto é atualizado
function invalidateProductCache(int $productId): void {
$redis = new Client();
$redis->incr('product:version'); // Nova versão invalida todos os caches
}
7. Monitoramento e Otimização do Cache
Monitoramento
<?php
// Informações do servidor Redis
$info = $redis->info();
echo "Memória usada: " . $info['used_memory_human'];
echo "Total de chaves: " . $info['db0']['keys'];
echo "Hit ratio: " . ($info['keyspace_hits'] / ($info['keyspace_hits'] + $info['keyspace_misses'])) * 100 . "%";
// Escanear chaves por padrão
$iterator = null;
$keys = $redis->scan($iterator, 'user:*', 100);
Cache Warming
<?php
function warmupProductCache(array $productIds): void {
$redis = new Client();
foreach ($productIds as $id) {
$cacheKey = "product:{$id}";
if (!$redis->exists($cacheKey)) {
$product = getProductFromDatabase($id);
$redis->setex($cacheKey, 3600, json_encode($product));
}
}
}
// Pré-carregar produtos mais populares
warmupProductCache([100, 200, 300, 400, 500]);
Compressão de Dados
<?php
// Comprimir dados grandes antes de armazenar
function cacheSetCompressed(Client $redis, string $key, $data, int $ttl = 3600): void {
$serialized = serialize($data);
$compressed = gzcompress($serialized, 9);
$redis->setex($key, $ttl, base64_encode($compressed));
}
function cacheGetCompressed(Client $redis, string $key) {
$cached = $redis->get($key);
if ($cached === null) return null;
$compressed = base64_decode($cached);
$serialized = gzuncompress($compressed);
return unserialize($serialized);
}
8. Boas Práticas e Armadilhas Comuns
Evitando Cache Stampede (Thundering Herd)
<?php
function getExpensiveData(Client $redis, string $cacheKey): array {
// Usar lock para evitar múltiplas requisições simultâneas
$lockKey = "lock:{$cacheKey}";
// Tentar adquirir lock com timeout de 5 segundos
if ($redis->set($lockKey, 1, 'NX', 'EX', 5)) {
try {
// Apenas esta requisição buscará os dados
$data = fetchExpensiveDataFromDatabase();
$redis->setex($cacheKey, 300, json_encode($data));
return $data;
} finally {
$redis->del($lockKey);
}
}
// Outras requisições esperam e tentam novamente
usleep(100000); // 100ms
$cached = $redis->get($cacheKey);
if ($cached) {
return json_decode($cached, true);
}
// Fallback: buscar dados mesmo sem lock
return fetchExpensiveDataFromDatabase();
}
Segurança: Protegendo Chaves e Conexões
<?php
// Usar prefixo para evitar colisão de chaves
$redis = new Client(null, ['prefix' => 'app:prod:']);
// Conexão com autenticação
$redis = new Client([
'scheme' => 'tcp',
'host' => '127.0.0.1',
'port' => 6379,
'password' => getenv('REDIS_PASSWORD'),
]);
// Sanitizar chaves que vêm de input do usuário
function sanitizeCacheKey(string $input): string {
return preg_replace('/[^a-zA-Z0-9_:.-]/', '_', $input);
}
$userId = sanitizeCacheKey($_GET['user_id']);
$redis->get("user:{$userId}:profile");
Testes Unitários com Mock do Predis
<?php
use PHPUnit\Framework\TestCase;
use Predis\Client;
class CacheServiceTest extends TestCase
{
private $redis;
private $cacheService;
protected function setUp(): void
{
// Mock do Predis
$this->redis = $this->createMock(Client::class);
$this->cacheService = new CacheService($this->redis);
}
public function testGetCachedData(): void
{
$this->redis
->expects($this->once())
->method('get')
->with('test:key')
->willReturn(json_encode(['name' => 'Test']));
$result = $this->cacheService->get('test:key');
$this->assertEquals(['name' => 'Test'], $result);
}
public function testSetCachedDataWithTTL(): void
{
$this->redis
->expects($this->once())
->method('setex')
->with('test:key', 3600, json_encode(['data' => 'value']));
$this->cacheService->set('test:key', ['data' => 'value'], 3600);
}
}
Referências
- Documentação Oficial do Redis — Guia completo sobre comandos, tipos de dados e configuração do Redis
- Predis no GitHub — Repositório oficial com exemplos, issues e documentação do cliente PHP
- Redis Caching Patterns — Padrões de cache no Redis Book oficial
- PHP Redis Session Handler — Documentação PHP sobre armazenamento de sessões com Redis
- Cache Stampede Prevention — Estratégias para evitar cache stampede com Redis
- Redis Memory Optimization — Técnicas de otimização de memória no Redis
- Laravel Redis Cache Documentation — Implementação de cache Redis no framework Laravel (referência prática)