正在加载,请稍候…

Caching Strategies for Backend Applications: Redis Patterns

Implement effective caching strategies using Redis. Learn cache-aside, write-through, read-through patterns, cache invalidation, and distributed caching.

Caching Strategies for Backend Applications

Cache-Aside (Lazy Loading)

The application manages the cache manually. Most common pattern.

class UserService {
  constructor(
    private userRepo: UserRepository,
    private cache: RedisCache
  ) {}

  async getUser(userId: string): Promise<User> {
    const cacheKey = `user:${userId}`;

    // 1. Check cache first
    const cached = await this.cache.get<User>(cacheKey);
    if (cached) return cached;

    // 2. Cache miss - fetch from DB
    const user = await this.userRepo.findById(userId);
    if (!user) throw new UserNotFoundError(userId);

    // 3. Store in cache with TTL
    await this.cache.set(cacheKey, user, { ttl: 3600 }); // 1 hour

    return user;
  }

  async updateUser(userId: string, dto: UpdateUserDto): Promise<User> {
    const user = await this.userRepo.update(userId, dto);

    // Invalidate cache after update
    await this.cache.delete(`user:${userId}`);

    return user;
  }
}

Write-Through Cache

Write to cache and DB simultaneously.

async setUser(user: User): Promise<void> {
  const cacheKey = `user:${user.id}`;

  // Write to both simultaneously
  await Promise.all([
    this.userRepo.save(user),
    this.cache.set(cacheKey, user, { ttl: 3600 }),
  ]);
}

Redis Cache Implementation

import { createClient } from 'redis';

class RedisCache {
  private client = createClient({ url: process.env.REDIS_URL });

  async get<T>(key: string): Promise<T | null> {
    const data = await this.client.get(key);
    if (!data) return null;
    return JSON.parse(data) as T;
  }

  async set<T>(key: string, value: T, options: { ttl?: number } = {}): Promise<void> {
    const serialized = JSON.stringify(value);
    if (options.ttl) {
      await this.client.setEx(key, options.ttl, serialized);
    } else {
      await this.client.set(key, serialized);
    }
  }

  async delete(key: string): Promise<void> {
    await this.client.del(key);
  }

  async deletePattern(pattern: string): Promise<void> {
    const keys = await this.client.keys(pattern);
    if (keys.length > 0) {
      await this.client.del(keys);
    }
  }

  async getOrSet<T>(key: string, factory: () => Promise<T>, ttl = 3600): Promise<T> {
    const cached = await this.get<T>(key);
    if (cached !== null) return cached;
    const value = await factory();
    await this.set(key, value, { ttl });
    return value;
  }
}

Cache Invalidation Strategies

// Tag-based invalidation
class TaggedCache {
  async setWithTags(key: string, value: unknown, tags: string[], ttl = 3600): Promise<void> {
    await this.cache.set(key, value, { ttl });
    // Store reverse mapping: tag -> keys
    for (const tag of tags) {
      await this.client.sAdd(`tag:${tag}`, key);
      await this.client.expire(`tag:${tag}`, ttl);
    }
  }

  async invalidateByTag(tag: string): Promise<void> {
    const keys = await this.client.sMembers(`tag:${tag}`);
    if (keys.length > 0) {
      await this.client.del([...keys, `tag:${tag}`]);
    }
  }
}

// When user updates, invalidate related caches
await taggedCache.setWithTags(
  `user:${userId}`,
  user,
  [`user:${userId}`, 'users:list'],
  3600
);

await taggedCache.invalidateByTag(`user:${userId}`);

Cache Stampede Prevention

// Mutex lock to prevent multiple simultaneous rebuilds
async getWithLock<T>(key: string, factory: () => Promise<T>): Promise<T> {
  const cached = await this.cache.get<T>(key);
  if (cached !== null) return cached;

  const lockKey = `lock:${key}`;
  const lockAcquired = await this.client.set(lockKey, '1', { NX: true, EX: 5 });

  if (!lockAcquired) {
    // Another process is rebuilding, wait and retry
    await new Promise(resolve => setTimeout(resolve, 100));
    return this.getWithLock(key, factory);
  }

  try {
    const value = await factory();
    await this.cache.set(key, value, { ttl: 3600 });
    return value;
  } finally {
    await this.client.del(lockKey);
  }
}

Cache Hit Rate Monitoring

class MonitoredCache extends RedisCache {
  private hits = 0;
  private misses = 0;

  async get<T>(key: string): Promise<T | null> {
    const result = await super.get<T>(key);
    if (result !== null) {
      this.hits++;
      metrics.increment('cache.hit', { key: key.split(':')[0] });
    } else {
      this.misses++;
      metrics.increment('cache.miss', { key: key.split(':')[0] });
    }
    return result;
  }

  get hitRate(): number {
    const total = this.hits + this.misses;
    return total > 0 ? this.hits / total : 0;
  }
}

Aim for 95%+ cache hit rate on frequently accessed data. Monitor and tune TTLs accordingly.