Sobre nós Guias Projetos Contactos
Админка
please wait

Introdução

A cache é uma das formas mais eficazes de melhorar o desempenho de uma API e reduzir a carga do servidor. Ao armazenar dados acedidos com frequência mais perto do consumidor, a cache reduz a latência, a carga na base de dados e os custos de largura de banda. Este guia aborda estratégias práticas de cache para APIs web.

Camadas de Cache

Client ──> CDN ──> API Gateway ──> Application ──> Database
↓ ↓ ↓ ↓
Browser Edge In-Memory Query Cache
Storage Cache (Redis)

Cada camada serve propósitos diferentes:

  • Do lado do cliente: Reduz totalmente os pedidos
  • CDN/Edge: Faz cache em pontos de distribuição geográfica
  • Aplicação: Faz cache de resultados calculados
  • Base de dados: Faz cache de resultados de queries

Cabeçalhos HTTP de Cache

Cache-Control

// Resposta do Laravel com cabeçalhos de cache
return response()
->json($data)
->header('Cache-Control', 'public, max-age=3600') // Cache durante 1 hora
->header('ETag', md5(json_encode($data)));
// Diretivas do Cache-Control
// public: a CDN e o browser podem fazer cache
// private: apenas o browser pode fazer cache
// no-cache: tem de revalidar antes de usar a cache
// no-store: não fazer cache de todo
// max-age: segundos até ficar stale
// s-maxage: idade máxima para caches partilhadas (CDN)

ETags e Pedidos Condicionais

// Middleware do Laravel para suporte de ETag
class ETagMiddleware
{
public function handle($request, Closure $next)
{
$response = $next($request);
if ($request->isMethod('GET')) {
$etag = md5($response->getContent());
$response->header('ETag', '"' . $etag . '"');
// Verificar o cabeçalho If-None-Match
$requestEtag = $request->header('If-None-Match');
if ($requestEtag && $requestEtag === '"' . $etag . '"') {
return response()->noContent(304);
}
}
return $response;
}
}

Last-Modified

public function show(Product $product)
{
$lastModified = $product->updated_at;
// Verificar o cabeçalho If-Modified-Since
$ifModifiedSince = request()->header('If-Modified-Since');
if ($ifModifiedSince) {
$ifModifiedSince = Carbon::parse($ifModifiedSince);
if ($lastModified <= $ifModifiedSince) {
return response()->noContent(304);
}
}
return response()
->json($product)
->header('Last-Modified', $lastModified->toRfc7231String())
->header('Cache-Control', 'private, max-age=60');
}

Cache ao Nível da Aplicação

Cache Redis com Laravel

// Cache básica
$products = Cache::remember('products.all', 3600, function () {
return Product::with('category')->get();
});
// Cache com tags (para invalidação)
$product = Cache::tags(['products'])->remember(
"products.{$id}",
3600,
function () use ($id) {
return Product::with('reviews')->find($id);
}
);
// Invalidar por tag
Cache::tags(['products'])->flush();
// Cache com lock (evitar stampede)
$value = Cache::lock('processing')->get(function () {
return expensiveOperation();
});

Padrões de Cache

Cache-Aside (Lazy Loading)

class ProductRepository
{
public function find(int $id): ?Product
{
$cacheKey = "product:{$id}";
// Tentar a cache primeiro
$product = Cache::get($cacheKey);
if ($product === null) {
// Cache miss — carregar da base de dados
$product = Product::find($id);
if ($product) {
Cache::put($cacheKey, $product, 3600);
}
}
return $product;
}
public function update(int $id, array $data): Product
{
$product = Product::findOrFail($id);
$product->update($data);
// Invalidar a cache
Cache::forget("product:{$id}");
return $product;
}
}

Write-Through

public function update(int $id, array $data): Product
{
$product = Product::findOrFail($id);
$product->update($data);
// Atualizar a cache imediatamente
Cache::put("product:{$id}", $product, 3600);
return $product;
}

Cache-Aside com Stale-While-Revalidate

public function getProducts(): array
{
$cacheKey = 'products.list';
$staleKey = 'products.list.stale';
// Verificar cache fresca
$fresh = Cache::get($cacheKey);
if ($fresh !== null) {
return $fresh;
}
// Verificar cache stale e acionar atualização em background
$stale = Cache::get($staleKey);
if ($stale !== null) {
// Despachar job para atualizar a cache
RefreshProductsCacheJob::dispatch();
return $stale;
}
// Sem cache — obter e fazer cache
$products = $this->fetchProducts();
Cache::put($cacheKey, $products, 60); // Fresco durante 1 minuto
Cache::put($staleKey, $products, 3600); // Stale durante 1 hora
return $products;
}

Cache de Resultados de Queries

Cache de Queries da Base de Dados

// Cache de queries no Laravel
$products = Product::where('active', true)
->orderBy('name')
->cacheFor(now()->addMinutes(30))
->cacheTags(['products'])
->get();

Cache de Valores Calculados

class DashboardService
{
public function getStatistics(): array
{
return Cache::remember('dashboard.stats', 300, function () {
return [
'total_orders' => Order::count(),
'revenue' => Order::sum('total'),
'avg_order_value' => Order::avg('total'),
'top_products' => $this->getTopProducts(),
];
});
}
private function getTopProducts(): Collection
{
return Product::withCount('orderItems')
->orderByDesc('order_items_count')
->limit(10)
->get();
}
}

Invalidação de Cache

Invalidação Baseada em Eventos

// Listener de eventos
class InvalidateProductCache
{
public function handle(ProductUpdated $event): void
{
$product = $event->product;
// Limpar produto específico
Cache::forget("product:{$product->id}");
// Limpar caches relacionadas
Cache::tags(['products', 'categories'])->flush();
// Limpar caches de listagem
Cache::forget('products.list');
Cache::forget("category:{$product->category_id}.products");
}
}
// Observer do model
class ProductObserver
{
public function saved(Product $product): void
{
Cache::forget("product:{$product->id}");
}
public function deleted(Product $product): void
{
Cache::forget("product:{$product->id}");
}
}

Invalidação Baseada no Tempo

// TTL curto para dados que mudam frequentemente
Cache::put('active_users', $count, 60); // 1 minuto
// TTL mais longo para dados estáveis
Cache::put('site_settings', $settings, 86400); // 1 dia
// TTL progressivo com base na idade
$ttl = min(86400, $product->created_at->diffInSeconds(now()) / 10);
Cache::put("product:{$id}", $product, $ttl);

Cache em CDN

Cabeçalhos de Cache do Cloudflare

// Fazer cache na CDN durante 1 hora
return response()
->json($data)
->header('Cache-Control', 'public, s-maxage=3600, max-age=60');
// Variar por cabeçalho Accept (JSON vs. HTML)
return response()
->json($data)
->header('Cache-Control', 'public, max-age=300')
->header('Vary', 'Accept, Accept-Encoding');

Limpeza da Cache da CDN

// API do Cloudflare
use Illuminate\Support\Facades\Http;
class CloudflareService
{
public function purgeUrls(array $urls): void
{
Http::withHeaders([
'Authorization' => 'Bearer ' . config('services.cloudflare.token'),
'Content-Type' => 'application/json',
])->delete(
"https://api.cloudflare.com/client/v4/zones/{$this->zoneId}/purge_cache",
['files' => $urls]
);
}
public function purgeByTags(array $tags): void
{
Http::withHeaders([
'Authorization' => 'Bearer ' . config('services.cloudflare.token'),
])->delete(
"https://api.cloudflare.com/client/v4/zones/{$this->zoneId}/purge_cache",
['tags' => $tags]
);
}
}

Compressão de Respostas

// Configuração do Nginx
gzip on;
gzip_types application/json text/plain application/xml;
gzip_min_length 1000;
// Middleware do Laravel para compressão de JSON
class CompressResponse
{
public function handle($request, Closure $next)
{
$response = $next($request);
if ($request->header('Accept-Encoding') &&
str_contains($request->header('Accept-Encoding'), 'gzip')) {
$content = gzencode($response->getContent(), 6);
$response->setContent($content);
$response->header('Content-Encoding', 'gzip');
$response->header('Content-Length', strlen($content));
}
return $response;
}
}

Cache do Lado do Cliente

Controlo de Cache do Browser

// Service Worker para cache offline
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/api/')) {
event.respondWith(
caches.open('api-cache').then((cache) => {
return fetch(event.request)
.then((response) => {
cache.put(event.request, response.clone());
return response;
})
.catch(() => cache.match(event.request));
})
);
}
});

Cache do React Query

import { useQuery, QueryClient } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutos
cacheTime: 30 * 60 * 1000, // 30 minutos
},
},
});
function useProducts() {
return useQuery({
queryKey: ['products'],
queryFn: () => fetch('/api/products').then(r => r.json()),
staleTime: 60000, // Considerar fresco durante 1 minuto
});
}

Monitorização e Métricas

// Acompanhar o desempenho da cache
class CacheMetrics
{
public function get(string $key)
{
$start = microtime(true);
$value = Cache::get($key);
$duration = microtime(true) - $start;
if ($value !== null) {
Metrics::increment('cache.hits', ['key' => $key]);
} else {
Metrics::increment('cache.misses', ['key' => $key]);
}
Metrics::timing('cache.latency', $duration);
return $value;
}
}

O Problema do «Thundering Herd»

Cenário: Uma key popular (por exemplo, homepage_news) expira às 12:00:00. Resultado: 10.000 utilizadores pedem-na às 12:00:01. Todos os 10.000 pedidos atingem a base de dados em simultâneo. A base de dados falha.

Solução 1: Expiração Antecipada Probabilística (Jitter)

Em vez de uma expiração rígida, armazene um valor delta. Se (now + delta) > expiration, recalcule o valor antes de ele expirar efetivamente:

public function getCachedValue(string $key, int $ttl, callable $compute)
{
$cached = Cache::get($key);
if ($cached) {
// Verificar se devemos atualizar proativamente
$expiresAt = Cache::get("{$key}:expires_at");
$delta = rand(0, 60); // Jitter aleatório
if ((time() + $delta) > $expiresAt) {
// Atualização proativa em background
dispatch(new RefreshCacheJob($key, $compute));
}
return $cached;
}
$value = $compute();
Cache::put($key, $value, $ttl);
Cache::put("{$key}:expires_at", time() + $ttl, $ttl + 60);
return $value;
}

Solução 2: Bloqueio de Cache

Quando ocorre um cache miss: 1. Adquirir um Redis Lock (SETNX lock:key 1) 2. Se estiver bloqueado por outro processo, dormir 50 ms e voltar a tentar no Redis 3. Se for adquirido, calcular o valor, atualizar o Redis, libertar o lock

public function getCachedValueWithLock(string $key, callable $compute)
{
$value = Cache::get($key);
if ($value !== null) {
return $value;
}
// Tentar adquirir o lock
$lock = Cache::lock("lock:{$key}", 10);
if ($lock->get()) {
try {
// Verificação dupla após adquirir o lock
$value = Cache::get($key);
if ($value === null) {
$value = $compute();
Cache::put($key, $value, 3600);
}
} finally {
$lock->release();
}
} else {
// Aguardar que outro processo preencha a cache
usleep(50000); // 50 ms
$value = Cache::get($key);
}
return $value;
}

Políticas de Evicção do Redis

Quando o Redis está cheio, o que acontece depende da sua política de evicção:

  • noeviction: Devolve erro ao escrever. (Mau para caches)
  • allkeys-lru: Remove as keys menos recentemente usadas. (Padrão para caching)
  • volatile-lru: Remove keys LRU com um expire definido. (Bom se usar o Redis tanto para dados persistentes como para cache)

Defina a política adequada na configuração do Redis:

maxmemory-policy allkeys-lru

Estruturas Avançadas do Redis

Para além de uma cache simples de key-value:

  • Sorted Sets (ZSET): Ótimo para leaderboards. ZADD scores 100 "user1"
  • HyperLogLog: Conta visitantes únicos com 99% de precisão, usando apenas 12 KB de RAM (vs. megabytes para um Set)

Boas Práticas

  1. Fazer cache ao nível certo - mais perto do cliente é mais rápido
  2. Usar TTLs apropriados - equilibrar frescura com desempenho
  3. Implementar invalidação de cache - baseada em eventos é a mais fiável
  4. Monitorizar taxas de cache hit - apontar para >90%
  5. Evitar fazer cache de dados específicos do utilizador em camadas partilhadas
  6. Usar cache tags para invalidação em grupo
  7. Implementar fallbacks para falhas de cache
  8. Considerar cache warming para dados críticos
  9. Adicionar TTL jitter para evitar expirações sincronizadas
  10. Tratar o Redis como volátil - a sua aplicação tem de funcionar se o Redis for limpo

Conclusão

Uma cache eficaz melhora drasticamente o desempenho de uma API. Aplique cache em camadas de forma adequada — cabeçalhos HTTP para CDN e browser, Redis para dados da aplicação e cache de queries para resultados da base de dados. Implemente estratégias de invalidação corretas, lide com «thundering herds» com locking ou jitter e monitorize as taxas de hit para garantir que a cache é eficaz. Lembre-se: o Redis é uma camada de otimização, não uma fonte de verdade.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
ZK Interactive