Sistema de cache de respostas HTTP com arquitetura plugável para suportar diferentes backends (Memória, Redis, etc.).
Response Caching armazena respostas HTTP para reutilizá-las em requisições subsequentes, reduzindo:
- Latência: Respostas instantâneas do cache
- Carga no Servidor: Menos processamento
- Uso de Banco de Dados: Menos queries
- Custos: Menos recursos computacionais
- ✅ Interface Plugável (
ICacheStore) para diferentes backends - ✅ In-Memory Cache (padrão) - Zero configuração
- ✅ Redis Support (futuro) - Cache distribuído
- ✅ Builder Fluente para configuração elegante
- ✅ Vary By - Query, Headers
- ✅ Headers Padrão - Cache-Control, X-Cache
- ✅ Thread-Safe com
TCriticalSection - ✅ Auto-Cleanup de entradas expiradas
ICacheStore = interface
function TryGet(const Key: string; out Value: string): Boolean;
procedure SetValue(const Key, Value: string; DurationSeconds: Integer);
procedure Remove(const Key: string);
procedure Clear;
end;| Store | Status | Uso | Escalabilidade |
|---|---|---|---|
| TMemoryCacheStore | ✅ Disponível | Single instance | Limitado pela RAM |
| TRedisCacheStore | 🔜 Futuro | Multi-instance | Distribuído |
| Custom | ✅ Você implementa | Qualquer | Depende da impl. |
uses
Dext.Caching;
var
App: IWebApplication;
begin
App := TDextApplication.Create;
var Builder := App.GetApplicationBuilder;
// ✅ 60 segundos de cache (padrão)
TApplicationBuilderCacheExtensions.UseResponseCache(Builder);
// ... configurar rotas ...
App.Run(8080);
end;// ✅ 30 segundos de cache
TApplicationBuilderCacheExtensions.UseResponseCache(Builder, 30);
// ✅ Com builder fluente
TApplicationBuilderCacheExtensions.UseResponseCache(Builder,
procedure(Cache: TResponseCacheBuilder)
begin
Cache
.DefaultDuration(60)
.MaxSize(1000)
.VaryByQueryString
.ForMethods(['GET', 'HEAD']);
end);uses
Dext.Caching.Redis;
var
RedisStore: ICacheStore;
begin
// Criar store Redis
RedisStore := TRedisCacheStore.Create('localhost', 6379);
// Usar Redis como backend
TApplicationBuilderCacheExtensions.UseResponseCache(Builder,
procedure(Cache: TResponseCacheBuilder)
begin
Cache
.DefaultDuration(300)
.Store(RedisStore); // ← Cache distribuído!
end);
end;// Cache longo para dados que mudam raramente
TApplicationBuilderCacheExtensions.UseResponseCache(Builder,
procedure(Cache: TResponseCacheBuilder)
begin
Cache
.DefaultDuration(3600) // 1 hora
.MaxSize(500);
end);// Vary by query para cachear cada página separadamente
TApplicationBuilderCacheExtensions.UseResponseCache(Builder,
procedure(Cache: TResponseCacheBuilder)
begin
Cache
.DefaultDuration(300) // 5 minutos
.VaryByQueryString; // Cada ?page=X tem seu cache
end);// Vary by Accept-Language header
TApplicationBuilderCacheExtensions.UseResponseCache(Builder,
procedure(Cache: TResponseCacheBuilder)
begin
Cache
.DefaultDuration(600)
.VaryByHeader(['Accept-Language', 'Accept-Encoding']);
end);// Apenas GET e HEAD (padrão é seguro)
TApplicationBuilderCacheExtensions.UseResponseCache(Builder,
procedure(Cache: TResponseCacheBuilder)
begin
Cache
.DefaultDuration(120)
.ForMethods(['GET', 'HEAD']); // Nunca cachear POST/PUT/DELETE
end);// Implementar seu próprio backend
type
TMyCustomCacheStore = class(TInterfacedObject, ICacheStore)
public
function TryGet(const Key: string; out Value: string): Boolean;
procedure SetValue(const Key, Value: string; DurationSeconds: Integer);
procedure Remove(const Key: string);
procedure Clear;
end;
// Usar
var CustomStore := TMyCustomCacheStore.Create;
Cache.Store(CustomStore);| Método | Descrição | Exemplo |
|---|---|---|
DefaultDuration(seconds) |
Define duração padrão do cache | .DefaultDuration(60) |
MaxSize(size) |
Máximo de entradas (memory store) | .MaxSize(1000) |
VaryByQueryString |
Cachear por query string | .VaryByQueryString |
VaryByHeader(headers) |
Cachear por headers específicos | .VaryByHeader(['Accept-Language']) |
ForMethods(methods) |
Métodos HTTP cacheáveis | .ForMethods(['GET', 'HEAD']) |
Store(store) |
Define backend customizado | .Store(RedisStore) |
Build |
Retorna TResponseCacheOptions |
.Build |
Cache HIT (do cache):
X-Cache: HIT
Cache-Control: public, max-age=60
Cache MISS (gerado):
X-Cache: MISS
Cache-Control: public, max-age=60
// Geração da chave de cache
Key = Method + ':' + Path + '?' + QueryString + '|' + Headers
// Exemplos:
'GET:/api/users'
'GET:/api/users?page=1'
'GET:/api/users|Accept-Language=pt-BR'┌─────────────────────────────────────────────┐
│ 1. Request chega │
│ ↓ │
│ 2. É cacheável? (GET/HEAD) │
│ ├─ Não: Continue pipeline │
│ └─ Sim: ↓ │
│ 3. Gerar cache key │
│ (Method + Path + Query + Headers) │
│ ↓ │
│ 4. TryGet(key) do ICacheStore │
│ ├─ HIT: Retornar resposta cached │
│ └─ MISS: ↓ │
│ 5. Executar handler │
│ ↓ │
│ 6. Armazenar resposta no cache │
│ ↓ │
│ 7. Retornar resposta │
└─────────────────────────────────────────────┘
TMemoryCacheStore = class
private
FEntries: TDictionary<string, TCacheEntry>;
FLock: TCriticalSection;
FMaxSize: Integer;
end;
TCacheEntry = record
Value: string;
ExpiresAt: TDateTime;
end;Características:
- Thread-safe com
TCriticalSection - Cleanup automático de expirados
- Limite de tamanho (LRU-like)
TRedisCacheStore = class
function TryGet: Boolean;
// Redis GET command
procedure SetValue;
// Redis SETEX command
end;Vantagens:
- Cache compartilhado entre instâncias
- Persistência opcional
- Escalabilidade horizontal
# Primeira requisição (MISS)
curl -I http://localhost:8080/api/data
# X-Cache: MISS
# Segunda requisição (HIT)
curl -I http://localhost:8080/api/data
# X-Cache: HIT
# Após expiração (MISS novamente)
sleep 61
curl -I http://localhost:8080/api/data
# X-Cache: MISS# Cada query string é cacheada separadamente
curl http://localhost:8080/api/users?page=1 # MISS
curl http://localhost:8080/api/users?page=1 # HIT
curl http://localhost:8080/api/users?page=2 # MISS (diferente)
curl http://localhost:8080/api/users?page=2 # HITcurl -v http://localhost:8080/api/data 2>&1 | grep -i cache
# Saída:
< X-Cache: HIT
< Cache-Control: public, max-age=60| Cenário | Latência sem Cache | Latência com Cache | Melhoria |
|---|---|---|---|
| Query simples | 50ms | < 1ms | 50x |
| Query complexa | 500ms | < 1ms | 500x |
| Com joins | 2000ms | < 1ms | 2000x |
| Métrica | In-Memory | Redis |
|---|---|---|
| Latência | < 1ms | 1-5ms |
| Throughput | 100k req/s | 50k req/s |
| Escalabilidade | Single instance | Multi-instance |
| Persistência | ❌ | ✅ (opcional) |
-
Nunca cachear dados sensíveis
// ❌ NÃO cachear GET /api/users/{id}/password GET /api/admin/secrets // ✅ OK para cachear GET /api/products GET /api/public/news
-
Apenas métodos seguros
// ✅ Padrão seguro Cache.ForMethods(['GET', 'HEAD']); // ❌ NUNCA cachear POST, PUT, DELETE, PATCH
-
Vary by usuário para dados privados
// Se cachear dados por usuário Cache.VaryByHeader(['Authorization']);
-
TTL apropriado
// Dados que mudam frequentemente Cache.DefaultDuration(30); // 30 segundos // Dados estáticos Cache.DefaultDuration(3600); // 1 hora
⚠️ Cache apenas de respostas GET/HEAD⚠️ In-memory não persiste entre restarts⚠️ Não compartilha entre múltiplas instâncias (use Redis)
- Interface
ICacheStoreplugável -
TMemoryCacheStore(in-memory) - Builder fluente
- Vary by query/headers
- Headers Cache-Control e X-Cache
- Thread-safety
- Auto-cleanup
-
TRedisCacheStorecompleto - Cache invalidation API
- Cache tags/groups
- Conditional requests (ETag, If-Modified-Since)
- Compression (gzip)
- Cache warming
- Metrics/statistics
type
TDatabaseCacheStore = class(TInterfacedObject, ICacheStore)
private
FConnection: TFDConnection;
public
constructor Create(AConnection: TFDConnection);
function TryGet(const Key: string; out Value: string): Boolean;
begin
// SELECT value FROM cache WHERE key = :key AND expires_at > NOW()
end;
procedure SetValue(const Key, Value: string; DurationSeconds: Integer);
begin
// INSERT INTO cache (key, value, expires_at) VALUES (...)
// ON CONFLICT (key) DO UPDATE ...
end;
procedure Remove(const Key: string);
begin
// DELETE FROM cache WHERE key = :key
end;
procedure Clear;
begin
// DELETE FROM cache
end;
end;type
TFileCacheStore = class(TInterfacedObject, ICacheStore)
private
FCacheDir: string;
function GetFilePath(const Key: string): string;
public
constructor Create(const ACacheDir: string);
function TryGet(const Key: string; out Value: string): Boolean;
begin
var FilePath := GetFilePath(Key);
if FileExists(FilePath) then
begin
// Check expiration from file timestamp
// Read file content
end;
end;
// ... implementar outros métodos
end;Desenvolvido com 💾 para o Dext Framework