Caching ist eine der effektivsten Methoden zur Verbesserung der Performance von Webanwendungen. NestJS bietet umfassende Unterstützung für verschiedene Caching-Strategien, von einfachem In-Memory-Caching bis hin zu komplexen verteilten Cache-Systemen. Eine durchdachte Caching-Strategie kann die Antwortzeiten drastisch reduzieren und die Serverlast minimieren.
In-Memory Caching speichert Daten direkt im Arbeitsspeicher der Anwendung und bietet die schnellsten Zugriffzeiten für häufig verwendete Daten.
import { Module, CacheModule } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
CacheModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
ttl: configService.get('CACHE_TTL', 300), // 5 Minuten Standard-TTL
max: configService.get('CACHE_MAX_ITEMS', 1000), // Maximale Anzahl Items
isGlobal: true, // Cache global verfügbar machen
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}import { Injectable, Inject, CACHE_MANAGER } from '@nestjs/common';
import { Cache } from 'cache-manager';
@Injectable()
export class CacheService {
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
// Basis-Cache-Operationen
async get<T>(key: string): Promise<T | undefined> {
return await this.cacheManager.get<T>(key);
}
async set<T>(key: string, value: T, ttl?: number): Promise<void> {
await this.cacheManager.set(key, value, ttl);
}
async del(key: string): Promise<void> {
await this.cacheManager.del(key);
}
async reset(): Promise<void> {
await this.cacheManager.reset();
}
// Erweiterte Cache-Operationen
async getOrSet<T>(
key: string,
factory: () => Promise<T>,
ttl?: number
): Promise<T> {
let value = await this.get<T>(key);
if (value === undefined) {
value = await factory();
await this.set(key, value, ttl);
}
return value;
}
// Cache mit automatischer Serialisierung
async setObject(key: string, value: any, ttl?: number): Promise<void> {
const serialized = JSON.stringify(value);
await this.set(key, serialized, ttl);
}
async getObject<T>(key: string): Promise<T | undefined> {
const serialized = await this.get<string>(key);
return serialized ? JSON.parse(serialized) : undefined;
}
// Batch-Operationen
async mget<T>(keys: string[]): Promise<(T | undefined)[]> {
const promises = keys.map(key => this.get<T>(key));
return Promise.all(promises);
}
async mset(keyValuePairs: Array<{ key: string; value: any; ttl?: number }>): Promise<void> {
const promises = keyValuePairs.map(({ key, value, ttl }) =>
this.set(key, value, ttl)
);
await Promise.all(promises);
}
// Cache-Statistiken
async getStats(): Promise<{
hits: number;
misses: number;
keys: number;
hitRate: number;
}> {
// Implementation abhängig vom verwendeten Cache-Store
const store = this.cacheManager.store as any;
if (store.getStats) {
return store.getStats();
}
// Fallback für Stores ohne Statistiken
return {
hits: 0,
misses: 0,
keys: 0,
hitRate: 0,
};
}
}import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { CacheService } from './cache.service';
@Injectable()
export class AutoCacheInterceptor implements NestInterceptor {
constructor(private cacheService: CacheService) {}
async intercept(
context: ExecutionContext,
next: CallHandler,
): Promise<Observable<any>> {
const request = context.switchToHttp().getRequest();
const cacheKey = this.generateCacheKey(request);
// Cache-Lookup
const cachedResult = await this.cacheService.get(cacheKey);
if (cachedResult !== undefined) {
return new Observable(observer => {
observer.next(cachedResult);
observer.complete();
});
}
// Bei Cache-Miss: Ausführen und Ergebnis cachen
return next.handle().pipe(
tap(async (result) => {
if (result !== undefined && result !== null) {
await this.cacheService.set(cacheKey, result, 300); // 5 Minuten TTL
}
}),
);
}
private generateCacheKey(request: any): string {
const { method, url, query, body } = request;
const keyParts = [method, url];
if (Object.keys(query).length > 0) {
keyParts.push(JSON.stringify(query));
}
if (body && Object.keys(body).length > 0) {
keyParts.push(JSON.stringify(body));
}
return keyParts.join(':');
}
}
// Verwendung des Interceptors
@Controller('users')
@UseInterceptors(AutoCacheInterceptor)
export class UsersController {
constructor(private usersService: UsersService) {}
@Get()
async getUsers(@Query() query: any) {
return this.usersService.findAll(query);
}
}import { SetMetadata } from '@nestjs/common';
export const CACHE_CONFIG_KEY = 'cache_config';
export interface CacheConfig {
ttl?: number;
key?: string;
condition?: (context: ExecutionContext) => boolean;
}
export const Cacheable = (config: CacheConfig = {}) =>
SetMetadata(CACHE_CONFIG_KEY, config);
// Smart Cache Interceptor mit Konfiguration
@Injectable()
export class SmartCacheInterceptor implements NestInterceptor {
constructor(
private cacheService: CacheService,
private reflector: Reflector,
) {}
async intercept(
context: ExecutionContext,
next: CallHandler,
): Promise<Observable<any>> {
const cacheConfig = this.reflector.get<CacheConfig>(
CACHE_CONFIG_KEY,
context.getHandler(),
);
if (!cacheConfig) {
return next.handle();
}
// Conditional Caching
if (cacheConfig.condition && !cacheConfig.condition(context)) {
return next.handle();
}
const cacheKey = cacheConfig.key || this.generateCacheKey(context);
const cachedResult = await this.cacheService.get(cacheKey);
if (cachedResult !== undefined) {
return new Observable(observer => {
observer.next(cachedResult);
observer.complete();
});
}
return next.handle().pipe(
tap(async (result) => {
if (result !== undefined && result !== null) {
await this.cacheService.set(cacheKey, result, cacheConfig.ttl);
}
}),
);
}
private generateCacheKey(context: ExecutionContext): string {
const request = context.switchToHttp().getRequest();
const handler = context.getHandler().name;
const className = context.getClass().name;
return `${className}:${handler}:${this.hashRequest(request)}`;
}
private hashRequest(request: any): string {
const { url, query, body } = request;
const data = { url, query, body };
return Buffer.from(JSON.stringify(data)).toString('base64');
}
}
// Verwendung mit Decorator
@Controller('products')
export class ProductsController {
@Get(':id')
@Cacheable({
ttl: 600, // 10 Minuten
condition: (context) => {
const request = context.switchToHttp().getRequest();
return !request.query.nocache; // Caching wenn nocache nicht gesetzt
}
})
async getProduct(@Param('id') id: string) {
return this.productsService.findById(id);
}
@Get()
@Cacheable({
ttl: 300,
key: 'products:list' // Statischer Cache-Key
})
async getProducts() {
return this.productsService.findAll();
}
}Redis bietet persistentes, hochperformantes Caching mit erweiterten Features wie Pub/Sub und komplexen Datenstrukturen.
import { Module, CacheModule } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import * as redisStore from 'cache-manager-redis-store';
@Module({
imports: [
CacheModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => {
const isRedisEnabled = configService.get('REDIS_ENABLED', 'false') === 'true';
if (isRedisEnabled) {
return {
store: redisStore,
host: configService.get('REDIS_HOST', 'localhost'),
port: configService.get('REDIS_PORT', 6379),
password: configService.get('REDIS_PASSWORD'),
db: configService.get('REDIS_DB', 0),
ttl: configService.get('CACHE_TTL', 300),
max: configService.get('CACHE_MAX_ITEMS', 1000),
retry_strategy: (options: any) => {
if (options.error && options.error.code === 'ECONNREFUSED') {
return new Error('Redis server connection refused');
}
if (options.total_retry_time > 1000 * 60 * 60) {
return new Error('Retry time exhausted');
}
if (options.attempt > 10) {
return undefined;
}
return Math.min(options.attempt * 100, 3000);
},
};
} else {
// Fallback zu In-Memory Cache
return {
ttl: configService.get('CACHE_TTL', 300),
max: configService.get('CACHE_MAX_ITEMS', 1000),
};
}
},
inject: [ConfigService],
isGlobal: true,
}),
],
})
export class CacheConfigModule {}import { Injectable, Inject, CACHE_MANAGER } from '@nestjs/common';
import { Cache } from 'cache-manager';
import Redis from 'ioredis';
@Injectable()
export class RedisService {
private redis: Redis;
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {
// Direkte Redis-Verbindung für erweiterte Features
this.redis = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379'),
password: process.env.REDIS_PASSWORD,
retryDelayOnFailover: 100,
maxRetriesPerRequest: 3,
});
}
// Basis Cache-Operationen über Cache Manager
async get<T>(key: string): Promise<T | undefined> {
return await this.cacheManager.get<T>(key);
}
async set<T>(key: string, value: T, ttl?: number): Promise<void> {
await this.cacheManager.set(key, value, ttl);
}
// Redis-spezifische Operationen
async setWithExpiry(key: string, value: any, seconds: number): Promise<void> {
await this.redis.setex(key, seconds, JSON.stringify(value));
}
async increment(key: string, amount: number = 1): Promise<number> {
return await this.redis.incrby(key, amount);
}
async decrement(key: string, amount: number = 1): Promise<number> {
return await this.redis.decrby(key, amount);
}
// Hash-Operationen
async hset(key: string, field: string, value: any): Promise<void> {
await this.redis.hset(key, field, JSON.stringify(value));
}
async hget<T>(key: string, field: string): Promise<T | undefined> {
const value = await this.redis.hget(key, field);
return value ? JSON.parse(value) : undefined;
}
async hgetall<T>(key: string): Promise<Record<string, T>> {
const hash = await this.redis.hgetall(key);
const result: Record<string, T> = {};
for (const [field, value] of Object.entries(hash)) {
result[field] = JSON.parse(value);
}
return result;
}
// Set-Operationen
async sadd(key: string, ...members: string[]): Promise<number> {
return await this.redis.sadd(key, ...members);
}
async smembers(key: string): Promise<string[]> {
return await this.redis.smembers(key);
}
async sismember(key: string, member: string): Promise<boolean> {
return (await this.redis.sismember(key, member)) === 1;
}
// Pub/Sub für Cache Invalidation
async publish(channel: string, message: any): Promise<void> {
await this.redis.publish(channel, JSON.stringify(message));
}
subscribe(channel: string, callback: (message: any) => void): void {
const subscriber = this.redis.duplicate();
subscriber.subscribe(channel);
subscriber.on('message', (receivedChannel, message) => {
if (receivedChannel === channel) {
try {
const parsedMessage = JSON.parse(message);
callback(parsedMessage);
} catch (error) {
console.error('Error parsing Redis message:', error);
}
}
});
}
// Lua Scripts für atomare Operationen
async getAndSetIfNotExists(key: string, value: any, ttl: number): Promise<any> {
const script = `
local current = redis.call('GET', KEYS[1])
if current == false then
redis.call('SETEX', KEYS[1], ARGV[2], ARGV[1])
return nil
else
return current
end
`;
const result = await this.redis.eval(
script,
1,
key,
JSON.stringify(value),
ttl.toString()
);
return result ? JSON.parse(result as string) : undefined;
}
// Pipeline für Batch-Operationen
async batchSet(operations: Array<{ key: string; value: any; ttl?: number }>): Promise<void> {
const pipeline = this.redis.pipeline();
operations.forEach(({ key, value, ttl }) => {
if (ttl) {
pipeline.setex(key, ttl, JSON.stringify(value));
} else {
pipeline.set(key, JSON.stringify(value));
}
});
await pipeline.exec();
}
// Memory-effiziente Schlüssel-Suche
async findKeys(pattern: string, count: number = 100): Promise<string[]> {
const keys: string[] = [];
let cursor = '0';
do {
const result = await this.redis.scan(cursor, 'MATCH', pattern, 'COUNT', count);
cursor = result[0];
keys.push(...result[1]);
} while (cursor !== '0' && keys.length < count);
return keys.slice(0, count);
}
// Cache-Statistiken
async getRedisInfo(): Promise<any> {
const info = await this.redis.info('memory');
const stats = await this.redis.info('stats');
return {
memory: this.parseRedisInfo(info),
stats: this.parseRedisInfo(stats),
};
}
private parseRedisInfo(info: string): Record<string, any> {
const result: Record<string, any> = {};
info.split('\r\n').forEach(line => {
if (line && !line.startsWith('#')) {
const [key, value] = line.split(':');
if (key && value) {
result[key] = isNaN(Number(value)) ? value : Number(value);
}
}
});
return result;
}
}import { Injectable } from '@nestjs/common';
import Redis, { Cluster } from 'ioredis';
@Injectable()
export class RedisClusterService {
private cluster: Cluster;
constructor() {
this.cluster = new Redis.Cluster([
{ host: process.env.REDIS_NODE1_HOST, port: 6379 },
{ host: process.env.REDIS_NODE2_HOST, port: 6379 },
{ host: process.env.REDIS_NODE3_HOST, port: 6379 },
], {
redisOptions: {
password: process.env.REDIS_PASSWORD,
},
enableOfflineQueue: false,
retryDelayOnFailover: 100,
maxRetriesPerRequest: 3,
});
}
async distributedSet(key: string, value: any, ttl: number): Promise<void> {
// Cluster-aware Caching mit Konsistenz-Checks
const serializedValue = JSON.stringify({
value,
timestamp: Date.now(),
ttl,
});
await this.cluster.setex(key, ttl, serializedValue);
}
async distributedGet<T>(key: string): Promise<T | undefined> {
const result = await this.cluster.get(key);
if (!result) {
return undefined;
}
try {
const parsed = JSON.parse(result);
// TTL-Check für zusätzliche Sicherheit
if (Date.now() - parsed.timestamp > parsed.ttl * 1000) {
await this.cluster.del(key);
return undefined;
}
return parsed.value;
} catch (error) {
console.error('Error parsing cached value:', error);
return undefined;
}
}
// Consistent Hashing für gleichmäßige Verteilung
async setWithConsistentHashing(key: string, value: any, ttl: number): Promise<void> {
const hashedKey = this.hashKey(key);
await this.distributedSet(hashedKey, value, ttl);
}
async getWithConsistentHashing<T>(key: string): Promise<T | undefined> {
const hashedKey = this.hashKey(key);
return await this.distributedGet<T>(hashedKey);
}
private hashKey(key: string): string {
// Einfacher Hash für Consistent Hashing
let hash = 0;
for (let i = 0; i < key.length; i++) {
const char = key.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // 32-bit Integer
}
return `${key}:${Math.abs(hash)}`;
}
}Das Cache-Aside Pattern ist eine bewährte Strategie, bei der die Anwendung den Cache direkt verwaltet und bei Cache-Misses die Daten aus der primären Datenquelle lädt.
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { CacheService } from '../cache/cache.service';
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
private cacheService: CacheService,
) {}
async findById(id: string): Promise<User | null> {
const cacheKey = `user:${id}`;
// 1. Cache-Lookup
const cachedUser = await this.cacheService.get<User>(cacheKey);
if (cachedUser) {
return cachedUser;
}
// 2. Datenbank-Lookup bei Cache-Miss
const user = await this.userRepository.findOne({ where: { id } });
// 3. Ergebnis in Cache speichern (auch null-Werte für negative Caching)
if (user) {
await this.cacheService.set(cacheKey, user, 600); // 10 Minuten
} else {
// Negative Caching mit kürzerer TTL
await this.cacheService.set(cacheKey, null, 60); // 1 Minute
}
return user;
}
async findAll(options: { page?: number; limit?: number } = {}): Promise<{
users: User[];
total: number;
page: number;
}> {
const { page = 1, limit = 10 } = options;
const cacheKey = `users:page:${page}:limit:${limit}`;
// Cache-Lookup
const cached = await this.cacheService.get<{
users: User[];
total: number;
page: number;
}>(cacheKey);
if (cached) {
return cached;
}
// Datenbank-Query
const [users, total] = await this.userRepository.findAndCount({
skip: (page - 1) * limit,
take: limit,
order: { createdAt: 'DESC' },
});
const result = { users, total, page };
// Cache mit kürzerer TTL für Listen
await this.cacheService.set(cacheKey, result, 300); // 5 Minuten
return result;
}
async create(userData: Partial<User>): Promise<User> {
const user = await this.userRepository.save(userData);
// Cache invalidieren und neuen Benutzer cachen
await this.invalidateUserListCache();
await this.cacheService.set(`user:${user.id}`, user, 600);
return user;
}
async update(id: string, userData: Partial<User>): Promise<User | null> {
const result = await this.userRepository.update(id, userData);
if (result.affected === 0) {
return null;
}
const user = await this.userRepository.findOne({ where: { id } });
if (user) {
// Cache aktualisieren
await this.cacheService.set(`user:${id}`, user, 600);
await this.invalidateUserListCache();
}
return user;
}
async delete(id: string): Promise<boolean> {
const result = await this.userRepository.delete(id);
if (result.affected && result.affected > 0) {
// Cache-Eintrag entfernen
await this.cacheService.del(`user:${id}`);
await this.invalidateUserListCache();
return true;
}
return false;
}
// Erweiterte Cache-Operationen
async warmupCache(userIds: string[]): Promise<void> {
const users = await this.userRepository.findByIds(userIds);
const cacheOperations = users.map(user => ({
key: `user:${user.id}`,
value: user,
ttl: 600,
}));
await this.cacheService.mset(cacheOperations);
}
async refreshUserCache(id: string): Promise<User | null> {
// Cache-Eintrag löschen und neu laden
await this.cacheService.del(`user:${id}`);
return await this.findById(id);
}
private async invalidateUserListCache(): Promise<void> {
// Alle Listenseiten-Caches invalidieren
const listKeys = await this.cacheService.findKeys('users:page:*');
await Promise.all(listKeys.map(key => this.cacheService.del(key)));
}
// Conditional Cache Update
async updateWithCacheCheck(id: string, userData: Partial<User>): Promise<User | null> {
const cacheKey = `user:${id}`;
const cachedUser = await this.cacheService.get<User>(cacheKey);
if (cachedUser) {
// Prüfen ob Update notwendig ist
const hasChanges = Object.keys(userData).some(
key => cachedUser[key] !== userData[key]
);
if (!hasChanges) {
return cachedUser; // Keine Änderungen, Cache-Wert zurückgeben
}
}
// Update durchführen
return await this.update(id, userData);
}
}import { Injectable } from '@nestjs/common';
import { Repository, DeepPartial, FindManyOptions } from 'typeorm';
import { CacheService } from '../cache/cache.service';
export abstract class CachedRepository<T extends { id: string | number }> {
protected abstract readonly entityName: string;
protected abstract readonly repository: Repository<T>;
protected abstract readonly cacheService: CacheService;
protected readonly defaultTtl: number = 600; // 10 Minuten
async findById(id: string | number): Promise<T | null> {
const cacheKey = this.getCacheKey('id', id);
const cached = await this.cacheService.get<T>(cacheKey);
if (cached !== undefined) {
return cached;
}
const entity = await this.repository.findOne({ where: { id } as any });
if (entity) {
await this.cacheService.set(cacheKey, entity, this.defaultTtl);
} else {
await this.cacheService.set(cacheKey, null, 60); // Negative Caching
}
return entity;
}
async findMany(options: FindManyOptions<T>): Promise<T[]> {
const cacheKey = this.getCacheKey('query', this.hashOptions(options));
const cached = await this.cacheService.get<T[]>(cacheKey);
if (cached) {
return cached;
}
const entities = await this.repository.find(options);
await this.cacheService.set(cacheKey, entities, this.defaultTtl / 2);
return entities;
}
async save(entity: DeepPartial<T>): Promise<T> {
const savedEntity = await this.repository.save(entity);
// Cache aktualisieren
const cacheKey = this.getCacheKey('id', savedEntity.id);
await this.cacheService.set(cacheKey, savedEntity, this.defaultTtl);
// Query-Caches invalidieren
await this.invalidateQueryCaches();
return savedEntity;
}
async remove(id: string | number): Promise<boolean> {
const result = await this.repository.delete(id);
if (result.affected && result.affected > 0) {
await this.cacheService.del(this.getCacheKey('id', id));
await this.invalidateQueryCaches();
return true;
}
return false;
}
protected getCacheKey(type: string, identifier: any): string {
return `${this.entityName}:${type}:${identifier}`;
}
protected hashOptions(options: FindManyOptions<T>): string {
return Buffer.from(JSON.stringify(options)).toString('base64');
}
protected async invalidateQueryCaches(): Promise<void> {
const queryKeys = await this.cacheService.findKeys(`${this.entityName}:query:*`);
await Promise.all(queryKeys.map(key => this.cacheService.del(key)));
}
}
// Konkrete Implementierung
@Injectable()
export class UserCachedRepository extends CachedRepository<User> {
protected readonly entityName = 'user';
constructor(
@InjectRepository(User)
protected readonly repository: Repository<User>,
protected readonly cacheService: CacheService,
) {
super();
}
// Spezifische Cache-Methoden
async findByEmail(email: string): Promise<User | null> {
const cacheKey = this.getCacheKey('email', email);
const cached = await this.cacheService.get<User>(cacheKey);
if (cached !== undefined) {
return cached;
}
const user = await this.repository.findOne({ where: { email } });
if (user) {
// Doppeltes Caching: by ID und by Email
await this.cacheService.set(cacheKey, user, this.defaultTtl);
await this.cacheService.set(this.getCacheKey('id', user.id), user, this.defaultTtl);
} else {
await this.cacheService.set(cacheKey, null, 60);
}
return user;
}
}Cache Invalidation ist eine der schwierigsten Aufgaben im Caching und erfordert durchdachte Strategien zur Aufrechterhaltung der Datenkonsistenz.
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { CacheService } from './cache.service';
@Injectable()
export class CacheInvalidationService {
constructor(private cacheService: CacheService) {}
// TTL-basierte Invalidation (automatisch durch Cache-Store)
async setWithCustomTtl(key: string, value: any, ttlSeconds: number): Promise<void> {
await this.cacheService.set(key, value, ttlSeconds);
}
// Scheduled Invalidation
@Cron(CronExpression.EVERY_HOUR)
async invalidateHourlyCache(): Promise<void> {
const hourlyKeys = await this.cacheService.findKeys('hourly:*');
await Promise.all(hourlyKeys.map(key => this.cacheService.del(key)));
console.log(`Invalidated ${hourlyKeys.length} hourly cache entries`);
}
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
async invalidateDailyCache(): Promise<void> {
const dailyKeys = await this.cacheService.findKeys('daily:*');
await Promise.all(dailyKeys.map(key => this.cacheService.del(key)));
console.log(`Invalidated ${dailyKeys.length} daily cache entries`);
}
// Conditional Invalidation basierend auf Datenalter
async conditionalInvalidation(): Promise<void> {
const allKeys = await this.cacheService.findKeys('conditional:*');
for (const key of allKeys) {
const data = await this.cacheService.get<{
value: any;
timestamp: number;
conditions: { maxAge?: number; maxAccess?: number };
}>(key);
if (data && this.shouldInvalidate(data)) {
await this.cacheService.del(key);
}
}
}
private shouldInvalidate(data: any): boolean {
const now = Date.now();
const age = now - data.timestamp;
// Altersbasierte Invalidation
if (data.conditions.maxAge && age > data.conditions.maxAge) {
return true;
}
// Weitere Bedingungen können hier hinzugefügt werden
return false;
}
// LRU-ähnliche Invalidation für Memory Management
async performLruInvalidation(maxCacheSize: number): Promise<void> {
const stats = await this.cacheService.getStats();
if (stats.keys > maxCacheSize) {
const excessKeys = stats.keys - maxCacheSize;
console.log(`Performing LRU invalidation for ${excessKeys} keys`);
// Implementation würde echte LRU-Logik benötigen
// Hier vereinfachte Version mit Pattern-based Cleanup
await this.invalidateOldestEntries(excessKeys);
}
}
private async invalidateOldestEntries(count: number): Promise<void> {
// Vereinfachte Implementierung - in der Praxis würde man
// Access-Timestamps tracken und echte LRU implementieren
const temporaryKeys = await this.cacheService.findKeys('temp:*');
const keysToDelete = temporaryKeys.slice(0, count);
await Promise.all(keysToDelete.map(key => this.cacheService.del(key)));
}
}import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { CacheService } from './cache.service';
// Events für Cache Invalidation
export class CacheInvalidationEvent {
constructor(
public readonly pattern: string,
public readonly reason: string,
) {}
}
export class EntityChangedEvent {
constructor(
public readonly entityType: string,
public readonly entityId: string | number,
public readonly operation: 'create' | 'update' | 'delete',
) {}
}
@Injectable()
export class EventDrivenCacheInvalidation {
constructor(private cacheService: CacheService) {}
@OnEvent('cache.invalidate')
async handleCacheInvalidation(event: CacheInvalidationEvent): Promise<void> {
const keys = await this.cacheService.findKeys(event.pattern);
await Promise.all(keys.map(key => this.cacheService.del(key)));
console.log(`Invalidated ${keys.length} cache entries for pattern: ${event.pattern}, reason: ${event.reason}`);
}
@OnEvent('entity.changed')
async handleEntityChanged(event: EntityChangedEvent): Promise<void> {
const { entityType, entityId, operation } = event;
// Direkte Entity-Caches invalidieren
await this.cacheService.del(`${entityType}:${entityId}`);
// Verwandte Caches invalidieren
await this.invalidateRelatedCaches(entityType, entityId, operation);
// Listen-Caches invalidieren
const listKeys = await this.cacheService.findKeys(`${entityType}:list:*`);
await Promise.all(listKeys.map(key => this.cacheService.del(key)));
}
private async invalidateRelatedCaches(
entityType: string,
entityId: string | number,
operation: string,
): Promise<void> {
// Entity-spezifische Invalidation-Logik
switch (entityType) {
case 'user':
await this.invalidateUserRelatedCaches(entityId);
break;
case 'product':
await this.invalidateProductRelatedCaches(entityId);
break;
case 'order':
await this.invalidateOrderRelatedCaches(entityId);
break;
}
}
private async invalidateUserRelatedCaches(userId: string | number): Promise<void> {
// User-spezifische Caches
const userPatterns = [
`user:profile:${userId}`,
`user:permissions:${userId}`,
`user:orders:${userId}:*`,
`user:favorites:${userId}:*`,
];
for (const pattern of userPatterns) {
const keys = await this.cacheService.findKeys(pattern);
await Promise.all(keys.map(key => this.cacheService.del(key)));
}
}
private async invalidateProductRelatedCaches(productId: string | number): Promise<void> {
const productPatterns = [
`product:details:${productId}`,
`product:reviews:${productId}:*`,
`product:recommendations:*:${productId}`,
`category:products:*`, // Alle Kategorie-Listen da sich Produkt geändert hat
];
for (const pattern of productPatterns) {
const keys = await this.cacheService.findKeys(pattern);
await Promise.all(keys.map(key => this.cacheService.del(key)));
}
}
private async invalidateOrderRelatedCaches(orderId: string | number): Promise<void> {
// Order-bezogene Caches würden hier invalidiert werden
const orderPatterns = [
`order:${orderId}`,
`order:status:*`,
`analytics:orders:*`, // Analytics-Caches
];
for (const pattern of orderPatterns) {
const keys = await this.cacheService.findKeys(pattern);
await Promise.all(keys.map(key => this.cacheService.del(key)));
}
}
}
// Service für Event-Emission
@Injectable()
export class CacheAwareService {
constructor(private eventEmitter: EventEmitter2) {}
async updateUser(userId: string, userData: any): Promise<void> {
// Business Logic...
// Event für Cache Invalidation emittieren
this.eventEmitter.emit('entity.changed', new EntityChangedEvent('user', userId, 'update'));
}
async deleteProduct(productId: string): Promise<void> {
// Business Logic...
this.eventEmitter.emit('entity.changed', new EntityChangedEvent('product', productId, 'delete'));
}
async clearCachePattern(pattern: string, reason: string): Promise<void> {
this.eventEmitter.emit('cache.invalidate', new CacheInvalidationEvent(pattern, reason));
}
}import { Injectable } from '@nestjs/common';
import { CacheService } from './cache.service';
interface TaggedCacheEntry {
value: any;
tags: string[];
timestamp: number;
ttl?: number;
}
@Injectable()
export class TaggedCacheService {
constructor(private cacheService: CacheService) {}
async setWithTags(
key: string,
value: any,
tags: string[],
ttl?: number,
): Promise<void> {
const taggedEntry: TaggedCacheEntry = {
value,
tags,
timestamp: Date.now(),
ttl,
};
// Cache-Eintrag mit Tags speichern
await this.cacheService.set(key, taggedEntry, ttl);
// Tag-Mappings aktualisieren
await this.updateTagMappings(key, tags);
}
async getByKey<T>(key: string): Promise<T | undefined> {
const entry = await this.cacheService.get<TaggedCacheEntry>(key);
return entry?.value;
}
async invalidateByTags(tags: string[]): Promise<number> {
let invalidatedCount = 0;
for (const tag of tags) {
const tagKey = this.getTagKey(tag);
const keySet = await this.cacheService.get<string[]>(tagKey) || [];
// Alle Keys mit diesem Tag invalidieren
await Promise.all(keySet.map(key => this.cacheService.del(key)));
// Tag-Mapping entfernen
await this.cacheService.del(tagKey);
invalidatedCount += keySet.length;
}
return invalidatedCount;
}
async findByTag<T>(tag: string): Promise<Array<{ key: string; value: T }>> {
const tagKey = this.getTagKey(tag);
const keySet = await this.cacheService.get<string[]>(tagKey) || [];
const results: Array<{ key: string; value: T }> = [];
for (const key of keySet) {
const entry = await this.cacheService.get<TaggedCacheEntry>(key);
if (entry) {
results.push({ key, value: entry.value });
}
}
return results;
}
async invalidateByPattern(pattern: string): Promise<number> {
const keys = await this.cacheService.findKeys(pattern);
// Alle gefundenen Keys invalidieren
await Promise.all(keys.map(key => this.cacheService.del(key)));
// Tag-Mappings für invalidierte Keys aufräumen
await this.cleanupTagMappings(keys);
return keys.length;
}
private async updateTagMappings(key: string, tags: string[]): Promise<void> {
for (const tag of tags) {
const tagKey = this.getTagKey(tag);
const existingKeys = await this.cacheService.get<string[]>(tagKey) || [];
if (!existingKeys.includes(key)) {
existingKeys.push(key);
await this.cacheService.set(tagKey, existingKeys, 3600); // 1 Stunde TTL für Tag-Mappings
}
}
}
private async cleanupTagMappings(invalidatedKeys: string[]): Promise<void> {
// Alle Tag-Mappings durchsuchen und invalidierte Keys entfernen
const tagKeys = await this.cacheService.findKeys('tag:*');
for (const tagKey of tagKeys) {
const keySet = await this.cacheService.get<string[]>(tagKey) || [];
const updatedKeySet = keySet.filter(key => !invalidatedKeys.includes(key));
if (updatedKeySet.length === 0) {
await this.cacheService.del(tagKey);
} else if (updatedKeySet.length !== keySet.length) {
await this.cacheService.set(tagKey, updatedKeySet, 3600);
}
}
}
private getTagKey(tag: string): string {
return `tag:${tag}`;
}
// Convenience-Methoden für häufige Use Cases
async cacheUserData(userId: string, data: any, ttl?: number): Promise<void> {
await this.setWithTags(
`user:${userId}`,
data,
['user', `user:${userId}`, 'profile'],
ttl,
);
}
async cacheProductData(productId: string, data: any, categoryId: string, ttl?: number): Promise<void> {
await this.setWithTags(
`product:${productId}`,
data,
['product', `product:${productId}`, `category:${categoryId}`, 'catalog'],
ttl,
);
}
async invalidateUserCaches(userId: string): Promise<number> {
return await this.invalidateByTags([`user:${userId}`]);
}
async invalidateCategoryCaches(categoryId: string): Promise<number> {
return await this.invalidateByTags([`category:${categoryId}`]);
}
async invalidateAllUserCaches(): Promise<number> {
return await this.invalidateByTags(['user']);
}
}
// Usage Example
@Injectable()
export class ProductService {
constructor(
private taggedCache: TaggedCacheService,
@InjectRepository(Product) private productRepo: Repository<Product>,
) {}
async getProduct(id: string): Promise<Product | null> {
// Cache-Lookup
const cached = await this.taggedCache.getByKey<Product>(`product:${id}`);
if (cached) {
return cached;
}
// Database-Lookup
const product = await this.productRepo.findOne({ where: { id } });
if (product) {
// Mit relevanten Tags cachen
await this.taggedCache.setWithTags(
`product:${id}`,
product,
['product', `product:${id}`, `category:${product.categoryId}`],
600,
);
}
return product;
}
async updateProduct(id: string, data: Partial<Product>): Promise<Product> {
const product = await this.productRepo.save({ id, ...data });
// Cache für dieses Produkt invalidieren
await this.taggedCache.invalidateByTags([`product:${id}`]);
// Falls Kategorie geändert wurde, auch Kategorie-Caches invalidieren
if (data.categoryId) {
await this.taggedCache.invalidateByTags([`category:${data.categoryId}`]);
}
return product;
}
}Distributed Caching ermöglicht es, Cache-Daten über mehrere Server-Instanzen hinweg zu teilen und zu synchronisieren.
import { Injectable } from '@nestjs/common';
import { RedisService } from './redis.service';
@Injectable()
export class DistributedCacheService {
private readonly instanceId: string;
private readonly syncChannel = 'cache:sync';
constructor(private redisService: RedisService) {
this.instanceId = `instance:${process.pid}:${Date.now()}`;
this.setupSynchronization();
}
private setupSynchronization(): void {
// Cache-Synchronization zwischen Instanzen
this.redisService.subscribe(this.syncChannel, (message) => {
this.handleSyncMessage(message);
});
}
async set(key: string, value: any, ttl?: number): Promise<void> {
// Lokaler Cache und Redis
await Promise.all([
this.setLocal(key, value, ttl),
this.redisService.set(key, value, ttl),
]);
// Andere Instanzen über Änderung informieren
await this.broadcastChange('set', key, this.instanceId);
}
async get<T>(key: string): Promise<T | undefined> {
// Lokaler Cache zuerst
let value = await this.getLocal<T>(key);
if (value === undefined) {
// Redis fallback
value = await this.redisService.get<T>(key);
if (value !== undefined) {
// Lokalen Cache aktualisieren
await this.setLocal(key, value);
}
}
return value;
}
async del(key: string): Promise<void> {
await Promise.all([
this.deleteLocal(key),
this.redisService.del(key),
]);
await this.broadcastChange('delete', key, this.instanceId);
}
async invalidatePattern(pattern: string): Promise<void> {
// Lokale Keys finden und löschen
const localKeys = await this.findLocalKeys(pattern);
await Promise.all(localKeys.map(key => this.deleteLocal(key)));
// Redis Keys finden und löschen
const redisKeys = await this.redisService.findKeys(pattern);
await Promise.all(redisKeys.map(key => this.redisService.del(key)));
await this.broadcastChange('invalidatePattern', pattern, this.instanceId);
}
private async broadcastChange(
operation: string,
key: string,
sourceInstance: string,
): Promise<void> {
const message = {
operation,
key,
sourceInstance,
timestamp: Date.now(),
};
await this.redisService.publish(this.syncChannel, message);
}
private async handleSyncMessage(message: any): Promise<void> {
const { operation, key, sourceInstance, timestamp } = message;
// Eigene Änderungen ignorieren
if (sourceInstance === this.instanceId) {
return;
}
// Veraltete Nachrichten ignorieren (älter als 10 Sekunden)
if (Date.now() - timestamp > 10000) {
return;
}
switch (operation) {
case 'set':
// Wert aus Redis laden und lokal setzen
const value = await this.redisService.get(key);
if (value !== undefined) {
await this.setLocal(key, value);
}
break;
case 'delete':
await this.deleteLocal(key);
break;
case 'invalidatePattern':
const localKeys = await this.findLocalKeys(key); // key ist hier das Pattern
await Promise.all(localKeys.map(k => this.deleteLocal(k)));
break;
}
}
// Lokaler Cache (In-Memory)
private localCache = new Map<string, { value: any; expiry?: number }>();
private async setLocal(key: string, value: any, ttl?: number): Promise<void> {
const expiry = ttl ? Date.now() + (ttl * 1000) : undefined;
this.localCache.set(key, { value, expiry });
}
private async getLocal<T>(key: string): Promise<T | undefined> {
const entry = this.localCache.get(key);
if (!entry) {
return undefined;
}
// TTL prüfen
if (entry.expiry && Date.now() > entry.expiry) {
this.localCache.delete(key);
return undefined;
}
return entry.value;
}
private async deleteLocal(key: string): Promise<void> {
this.localCache.delete(key);
}
private async findLocalKeys(pattern: string): Promise<string[]> {
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
return Array.from(this.localCache.keys()).filter(key => regex.test(key));
}
// Cache-Statistiken für Monitoring
async getDistributedStats(): Promise<{
local: { size: number; keys: string[] };
redis: any;
instance: string;
}> {
return {
local: {
size: this.localCache.size,
keys: Array.from(this.localCache.keys()),
},
redis: await this.redisService.getRedisInfo(),
instance: this.instanceId,
};
}
}import { Injectable } from '@nestjs/common';
export enum ConsistencyLevel {
EVENTUAL = 'eventual',
STRONG = 'strong',
WEAK = 'weak',
}
@Injectable()
export class ConsistentCacheService {
constructor(private distributedCache: DistributedCacheService) {}
async set(
key: string,
value: any,
ttl?: number,
consistency: ConsistencyLevel = ConsistencyLevel.EVENTUAL,
): Promise<void> {
switch (consistency) {
case ConsistencyLevel.STRONG:
await this.setWithStrongConsistency(key, value, ttl);
break;
case ConsistencyLevel.EVENTUAL:
await this.setWithEventualConsistency(key, value, ttl);
break;
case ConsistencyLevel.WEAK:
await this.setWithWeakConsistency(key, value, ttl);
break;
}
}
private async setWithStrongConsistency(
key: string,
value: any,
ttl?: number,
): Promise<void> {
// Alle Instanzen müssen bestätigen
const timestamp = Date.now();
const versionedValue = {
value,
version: timestamp,
checksum: this.calculateChecksum(value),
};
// Distributed Lock für starke Konsistenz
const lockKey = `lock:${key}`;
const lockAcquired = await this.acquireDistributedLock(lockKey, 5000);
if (!lockAcquired) {
throw new Error(`Failed to acquire lock for key: ${key}`);
}
try {
await this.distributedCache.set(key, versionedValue, ttl);
await this.waitForConsistency(key, timestamp);
} finally {
await this.releaseDistributedLock(lockKey);
}
}
private async setWithEventualConsistency(
key: string,
value: any,
ttl?: number,
): Promise<void> {
// Standard-Verhalten: Write-through ohne Wartezeit
await this.distributedCache.set(key, value, ttl);
}
private async setWithWeakConsistency(
key: string,
value: any,
ttl?: number,
): Promise<void> {
// Fire-and-forget: Keine Garantien
this.distributedCache.set(key, value, ttl).catch(error => {
console.warn('Weak consistency cache write failed:', error);
});
}
async get<T>(
key: string,
consistency: ConsistencyLevel = ConsistencyLevel.EVENTUAL,
): Promise<T | undefined> {
switch (consistency) {
case ConsistencyLevel.STRONG:
return await this.getWithStrongConsistency<T>(key);
case ConsistencyLevel.EVENTUAL:
case ConsistencyLevel.WEAK:
return await this.distributedCache.get<T>(key);
}
}
private async getWithStrongConsistency<T>(key: string): Promise<T | undefined> {
// Read-through mit Konsistenz-Validierung
const value = await this.distributedCache.get<{
value: T;
version: number;
checksum: string;
}>(key);
if (!value) {
return undefined;
}
// Checksum validieren
const expectedChecksum = this.calculateChecksum(value.value);
if (value.checksum !== expectedChecksum) {
console.warn(`Checksum mismatch for key ${key}, invalidating cache`);
await this.distributedCache.del(key);
return undefined;
}
return value.value;
}
private async acquireDistributedLock(
lockKey: string,
timeoutMs: number,
): Promise<boolean> {
const lockValue = `${this.getInstanceId()}:${Date.now()}`;
const acquired = await this.redisService.getAndSetIfNotExists(
lockKey,
lockValue,
Math.ceil(timeoutMs / 1000),
);
return acquired === undefined; // Lock acquired if key didn't exist
}
private async releaseDistributedLock(lockKey: string): Promise<void> {
await this.distributedCache.del(lockKey);
}
private async waitForConsistency(key: string, version: number): Promise<void> {
const maxWaitTime = 1000; // 1 Sekunde
const startTime = Date.now();
while (Date.now() - startTime < maxWaitTime) {
const value = await this.distributedCache.get<{ version: number }>(key);
if (value && value.version >= version) {
return; // Konsistenz erreicht
}
await new Promise(resolve => setTimeout(resolve, 50)); // 50ms warten
}
console.warn(`Consistency timeout for key: ${key}`);
}
private calculateChecksum(value: any): string {
const crypto = require('crypto');
const data = JSON.stringify(value);
return crypto.createHash('md5').update(data).digest('hex');
}
private getInstanceId(): string {
return `instance:${process.pid}`;
}
// Conflict Resolution für konkurrierende Updates
async setWithConflictResolution(
key: string,
value: any,
resolver: (current: any, new: any) => any,
): Promise<void> {
let attempts = 0;
const maxAttempts = 3;
while (attempts < maxAttempts) {
const current = await this.get(key, ConsistencyLevel.STRONG);
const resolved = current ? resolver(current, value) : value;
try {
await this.setWithStrongConsistency(key, resolved);
return; // Erfolgreich
} catch (error) {
attempts++;
if (attempts >= maxAttempts) {
throw new Error(`Failed to resolve conflict for key ${key} after ${maxAttempts} attempts`);
}
// Exponential backoff
await new Promise(resolve =>
setTimeout(resolve, Math.pow(2, attempts) * 100)
);
}
}
}
}HTTP Response Caching nutzt Browser-Caches und Proxy-Server für optimale Performance.
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
export interface CacheHeaders {
maxAge?: number; // Cache-Control: max-age
sMaxAge?: number; // Cache-Control: s-maxage
mustRevalidate?: boolean; // Cache-Control: must-revalidate
noCache?: boolean; // Cache-Control: no-cache
noStore?: boolean; // Cache-Control: no-store
private?: boolean; // Cache-Control: private
public?: boolean; // Cache-Control: public
etag?: string; // ETag header
lastModified?: Date; // Last-Modified header
vary?: string[]; // Vary header
}
@Injectable()
export class HttpCacheInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const response = context.switchToHttp().getResponse();
return next.handle().pipe(
tap((data) => {
if (data && data._cacheHeaders) {
this.setCacheHeaders(response, data._cacheHeaders);
delete data._cacheHeaders; // Clean up metadata
}
}),
);
}
private setCacheHeaders(response: any, headers: CacheHeaders): void {
// Cache-Control Header
const cacheControl: string[] = [];
if (headers.maxAge !== undefined) {
cacheControl.push(`max-age=${headers.maxAge}`);
}
if (headers.sMaxAge !== undefined) {
cacheControl.push(`s-maxage=${headers.sMaxAge}`);
}
if (headers.mustRevalidate) {
cacheControl.push('must-revalidate');
}
if (headers.noCache) {
cacheControl.push('no-cache');
}
if (headers.noStore) {
cacheControl.push('no-store');
}
if (headers.private) {
cacheControl.push('private');
}
if (headers.public) {
cacheControl.push('public');
}
if (cacheControl.length > 0) {
response.header('Cache-Control', cacheControl.join(', '));
}
// ETag Header
if (headers.etag) {
response.header('ETag', `"${headers.etag}"`);
}
// Last-Modified Header
if (headers.lastModified) {
response.header('Last-Modified', headers.lastModified.toUTCString());
}
// Vary Header
if (headers.vary && headers.vary.length > 0) {
response.header('Vary', headers.vary.join(', '));
}
}
}
// Decorator für einfache HTTP-Cache-Konfiguration
export const HttpCache = (headers: CacheHeaders) => {
return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const result = originalMethod.apply(this, args);
// Cache-Headers zu Antwort hinzufügen
if (result && typeof result === 'object') {
result._cacheHeaders = headers;
}
return result;
};
return descriptor;
};
};import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import * as crypto from 'crypto';
@Injectable()
export class ConditionalCacheInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();
return next.handle().pipe(
map((data) => {
if (!data) {
return data;
}
// ETag für Response generieren
const etag = this.generateETag(data);
const lastModified = this.extractLastModified(data);
// Client-seitige Cache-Headers prüfen
const clientETag = request.headers['if-none-match'];
const clientLastModified = request.headers['if-modified-since'];
// Conditional GET: Prüfen ob Daten sich geändert haben
if (this.isNotModified(etag, lastModified, clientETag, clientLastModified)) {
response.status(304); // Not Modified
response.header('ETag', `"${etag}"`);
if (lastModified) {
response.header('Last-Modified', lastModified.toUTCString());
}
return null; // Leere Response für 304
}
// Response-Headers für zukünftige Conditional Requests setzen
response.header('ETag', `"${etag}"`);
if (lastModified) {
response.header('Last-Modified', lastModified.toUTCString());
}
return data;
}),
);
}
private generateETag(data: any): string {
const content = JSON.stringify(data);
return crypto.createHash('md5').update(content).digest('hex');
}
private extractLastModified(data: any): Date | null {
// Versuche Last-Modified aus Daten zu extrahieren
if (data.updatedAt) {
return new Date(data.updatedAt);
}
if (data.lastModified) {
return new Date(data.lastModified);
}
if (Array.isArray(data) && data.length > 0) {
// Bei Arrays: neuestes Update-Datum finden
const dates = data
.map(item => item.updatedAt || item.lastModified)
.filter(date => date)
.map(date => new Date(date));
return dates.length > 0 ? new Date(Math.max(...dates.map(d => d.getTime()))) : null;
}
return null;
}
private isNotModified(
etag: string,
lastModified: Date | null,
clientETag?: string,
clientLastModified?: string,
): boolean {
// ETag-Vergleich
if (clientETag) {
const cleanClientETag = clientETag.replace(/"/g, '');
if (cleanClientETag === etag) {
return true;
}
}
// Last-Modified-Vergleich
if (clientLastModified && lastModified) {
const clientDate = new Date(clientLastModified);
if (clientDate.getTime() >= lastModified.getTime()) {
return true;
}
}
return false;
}
}
// Usage in Controller
@Controller('api/products')
@UseInterceptors(ConditionalCacheInterceptor)
export class ProductsController {
constructor(private productsService: ProductsService) {}
@Get(':id')
@HttpCache({
maxAge: 300, // 5 Minuten
public: true,
mustRevalidate: true
})
async getProduct(@Param('id') id: string) {
return await this.productsService.findById(id);
}
@Get()
@HttpCache({
maxAge: 60, // 1 Minute
sMaxAge: 300, // 5 Minuten für Proxies
vary: ['Accept-Encoding', 'Authorization']
})
async getProducts(@Query() query: any) {
return await this.productsService.findAll(query);
}
}import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import * as zlib from 'zlib';
@Injectable()
export class CompressionCacheInterceptor implements NestInterceptor {
private readonly compressionThreshold = 1024; // 1KB
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();
return next.handle().pipe(
map(async (data) => {
if (!data) {
return data;
}
const acceptEncoding = request.headers['accept-encoding'] || '';
const jsonData = JSON.stringify(data);
// Nur komprimieren wenn Daten groß genug sind
if (jsonData.length < this.compressionThreshold) {
return data;
}
// Komprimierung basierend auf Client-Unterstützung
if (acceptEncoding.includes('gzip')) {
const compressed = await this.gzipCompress(jsonData);
response.header('Content-Encoding', 'gzip');
response.header('Content-Length', compressed.length.toString());
response.header('Vary', 'Accept-Encoding');
// Cache-freundliche Headers
response.header('Cache-Control', 'public, max-age=300');
return compressed;
} else if (acceptEncoding.includes('deflate')) {
const compressed = await this.deflateCompress(jsonData);
response.header('Content-Encoding', 'deflate');
response.header('Content-Length', compressed.length.toString());
response.header('Vary', 'Accept-Encoding');
response.header('Cache-Control', 'public, max-age=300');
return compressed;
}
return data;
}),
);
}
private async gzipCompress(data: string): Promise<Buffer> {
return new Promise((resolve, reject) => {
zlib.gzip(data, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
}
private async deflateCompress(data: string): Promise<Buffer> {
return new Promise((resolve, reject) => {
zlib.deflate(data, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
}
}
// CDN-freundliches Caching
@Injectable()
export class CdnCacheService {
async setCdnHeaders(response: any, data: any): Promise<void> {
const cacheKey = this.generateCacheKey(data);
// CDN-spezifische Headers
response.header('X-Cache-Key', cacheKey);
response.header('X-Cache-TTL', '300');
response.header('X-Cache-Tags', this.generateCacheTags(data).join(','));
// Standard HTTP Cache Headers
response.header('Cache-Control', 'public, max-age=300, s-maxage=3600');
response.header('Vary', 'Accept-Encoding, Authorization');
// Edge-Side Includes Support
if (this.supportsESI(data)) {
response.header('X-ESI-Enabled', 'true');
}
}
private generateCacheKey(data: any): string {
// Cache-Key basierend auf Dateninhalt
const crypto = require('crypto');
const content = JSON.stringify(data);
return crypto.createHash('sha256').update(content).digest('hex').substring(0, 16);
}
private generateCacheTags(data: any): string[] {
const tags: string[] = [];
if (data.id) {
tags.push(`id:${data.id}`);
}
if (data.type) {
tags.push(`type:${data.type}`);
}
if (data.category) {
tags.push(`category:${data.category}`);
}
return tags;
}
private supportsESI(data: any): boolean {
// Prüfen ob Daten für ESI geeignet sind
return data && typeof data === 'object' && !Array.isArray(data);
}
}Caching-Strategien sind entscheidend für die Performance moderner Webanwendungen. NestJS bietet mit seinem flexiblen Cache-System die Möglichkeit, von einfachem In-Memory-Caching bis hin zu komplexen verteilten Cache-Architekturen alles zu implementieren. Die Wahl der richtigen Strategie hängt von den spezifischen Anforderungen wie Konsistenz, Performance und Skalierbarkeit ab. Durch die Kombination verschiedener Caching-Ebenen - von HTTP-Response-Caching über Application-Level-Caching bis hin zu Database-Caching - lassen sich erhebliche Performance-Verbesserungen erzielen.