Skip to content

第 8 篇:缓存策略 —— 快,但别错

在前面的文章中,我们探讨了事务管理等重要话题。现在,让我们关注另一个关键的横切关注点:缓存策略。

缓存是提升系统性能的重要手段,但错误的缓存策略可能导致数据不一致、缓存穿透、缓存雪崩等问题。正确实现缓存策略需要在性能和一致性之间找到平衡。

缓存穿透/雪崩/击穿防护

让我们首先了解缓存的三种常见问题及其解决方案:

typescript
// 缓存服务
@Injectable()
export class CacheService {
  constructor(
    @Inject('CACHE_MANAGER') private readonly cacheManager: Cache,
  ) {}

  // 基础缓存获取方法,包含穿透防护
  async get<T>(key: string, loader: () => Promise<T>, ttl?: number): Promise<T> {
    // 尝试从缓存获取
    let value = await this.cacheManager.get<T>(key);
    
    if (value !== undefined && value !== null) {
      return value;
    }

    // 缓存未命中,从数据源加载
    try {
      value = await loader();
      
      // 加载成功后写入缓存
      if (value !== undefined && value !== null) {
        await this.cacheManager.set(key, value, ttl);
      }
      
      return value;
    } catch (error) {
      // 数据加载失败,返回默认值或抛出异常
      throw error;
    }
  }

  // 带空值缓存的获取方法(防止缓存穿透)
  async getWithNullProtection<T>(
    key: string, 
    loader: () => Promise<T | null>, 
    ttl?: number,
    nullTtl: number = 60, // 空值缓存时间较短
  ): Promise<T | null> {
    // 尝试从缓存获取
    const cached = await this.cacheManager.get<{value: T | null, isEmpty: boolean}>(key);
    
    if (cached !== undefined) {
      // 如果是空值标记,返回null
      if (cached.isEmpty) {
        return null;
      }
      return cached.value;
    }

    // 缓存未命中,从数据源加载
    try {
      const value = await loader();
      
      if (value !== undefined && value !== null) {
        // 正常值缓存
        await this.cacheManager.set(key, {value, isEmpty: false}, ttl);
      } else {
        // 空值也缓存,但时间较短
        await this.cacheManager.set(key, {value: null, isEmpty: true}, nullTtl);
      }
      
      return value;
    } catch (error) {
      throw error;
    }
  }

  // 带分布式锁的获取方法(防止缓存击穿)
  async getWithLock<T>(
    key: string,
    loader: () => Promise<T>,
    ttl?: number,
    lockTimeout: number = 5000, // 锁超时时间
  ): Promise<T> {
    // 尝试从缓存获取
    const value = await this.cacheManager.get<T>(key);
    if (value !== undefined && value !== null) {
      return value;
    }

    // 获取分布式锁
    const lockKey = `lock:${key}`;
    const lock = await this.acquireLock(lockKey, lockTimeout);
    
    if (!lock) {
      // 获取锁失败,可能是热点数据,短暂等待后重试
      await this.delay(100);
      return this.get(key, loader, ttl);
    }

    try {
      // 再次检查缓存(双重检查)
      const cachedValue = await this.cacheManager.get<T>(key);
      if (cachedValue !== undefined && cachedValue !== null) {
        return cachedValue;
      }

      // 从数据源加载
      const loadedValue = await loader();
      
      if (loadedValue !== undefined && loadedValue !== null) {
        await this.cacheManager.set(key, loadedValue, ttl);
      }
      
      return loadedValue;
    } finally {
      // 释放锁
      await this.releaseLock(lockKey);
    }
  }

  // 简单的分布式锁实现
  private async acquireLock(key: string, timeout: number): Promise<boolean> {
    try {
      // 使用 Redis 的 SET 命令实现分布式锁
      const result = await this.cacheManager.store.getClient().set(
        key, 
        '1', 
        'PX', 
        timeout, 
        'NX'
      );
      return result === 'OK';
    } catch (error) {
      return false;
    }
  }

  private async releaseLock(key: string): Promise<void> {
    try {
      // 使用 Lua 脚本安全释放锁
      const script = `
        if redis.call("get", KEYS[1]) == ARGV[1] then
          return redis.call("del", KEYS[1])
        else
          return 0
        end
      `;
      await this.cacheManager.store.getClient().eval(script, 1, key, '1');
    } catch (error) {
      // 忽略释放锁的错误
    }
  }

  private delay(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

多租户下的缓存 key 设计:order:{tenantId}:{id}

在多租户系统中,缓存 key 的设计需要考虑租户隔离:

typescript
// 多租户缓存键生成器
@Injectable()
export class TenantCacheKeyGenerator {
  generateKey(
    tenantId: string,
    resourceType: string,
    resourceId?: string | number,
    ...additionalParts: (string | number)[]
  ): string {
    const parts = [resourceType, tenantId];
    
    if (resourceId !== undefined) {
      parts.push(String(resourceId));
    }
    
    if (additionalParts.length > 0) {
      parts.push(...additionalParts.map(String));
    }
    
    return parts.join(':');
  }

  // 生成用户相关缓存键
  userKey(tenantId: string, userId: string): string {
    return this.generateKey(tenantId, 'user', userId);
  }

  // 生成订单相关缓存键
  orderKey(tenantId: string, orderId?: string): string {
    return this.generateKey(tenantId, 'order', orderId);
  }

  // 生成订单列表缓存键
  orderListKey(tenantId: string, userId: string, status?: string): string {
    const parts = ['order-list', tenantId, userId];
    if (status) {
      parts.push(status);
    }
    return parts.join(':');
  }

  // 生成统计数据缓存键
  statsKey(tenantId: string, type: string, period: string): string {
    return this.generateKey(tenantId, 'stats', type, period);
  }
}

// 在服务中使用缓存键生成器
@Injectable()
export class OrderService {
  constructor(
    @InjectRepository(Order)
    private readonly orderRepository: Repository<Order>,
    private readonly cacheService: CacheService,
    private readonly cacheKeyGenerator: TenantCacheKeyGenerator,
  ) {}

  async getOrder(tenantId: string, orderId: string): Promise<Order> {
    const cacheKey = this.cacheKeyGenerator.orderKey(tenantId, orderId);
    
    return this.cacheService.getWithLock(
      cacheKey,
      () => this.orderRepository.findOne({ 
        where: { id: orderId, tenantId } 
      }),
      300, // 5分钟缓存
    );
  }

  async getOrdersByUser(tenantId: string, userId: string, status?: string): Promise<Order[]> {
    const cacheKey = this.cacheKeyGenerator.orderListKey(tenantId, userId, status);
    
    return this.cacheService.getWithNullProtection(
      cacheKey,
      () => this.orderRepository.find({ 
        where: { 
          tenantId, 
          userId,
          ...(status ? { status } : {})
        } 
      }),
      120, // 2分钟缓存
    );
  }

  // 清除相关缓存
  async invalidateOrderCache(tenantId: string, orderId: string): Promise<void> {
    const orderKey = this.cacheKeyGenerator.orderKey(tenantId, orderId);
    const orderListKeyPattern = this.cacheKeyGenerator.generateKey(tenantId, 'order-list', '*');
    
    await this.cacheService.del(orderKey);
    // 注意:通配符删除需要 Redis 支持
    // await this.cacheService.delPattern(orderListKeyPattern);
  }
}

缓存与数据库一致性(Cache-Aside vs Write-Through)

不同的缓存策略适用于不同的场景:

typescript
// Cache-Aside 模式(旁路缓存)
@Injectable()
export class CacheAsideService {
  constructor(
    private readonly cacheService: CacheService,
    private readonly tenantCacheKeyGenerator: TenantCacheKeyGenerator,
  ) {}

  async getCachedData<T>(
    tenantId: string,
    key: string,
    loader: () => Promise<T>,
    ttl?: number,
  ): Promise<T> {
    return this.cacheService.getWithLock(
      `${tenantId}:${key}`,
      loader,
      ttl,
    );
  }

  async invalidateCache(tenantId: string, key: string): Promise<void> {
    await this.cacheService.del(`${tenantId}:${key}`);
  }
}

// Write-Through 模式(直写缓存)
@Injectable()
export class WriteThroughService {
  constructor(
    private readonly cacheService: CacheService,
    private readonly tenantCacheKeyGenerator: TenantCacheKeyGenerator,
  ) {}

  async saveData<T>(
    tenantId: string,
    key: string,
    data: T,
    saver: () => Promise<void>,
    ttl?: number,
  ): Promise<void> {
    // 先写入数据库
    await saver();
    
    // 再写入缓存
    await this.cacheService.set(`${tenantId}:${key}`, data, ttl);
  }
}

// Write-Behind 模式(回写缓存)
@Injectable()
export class WriteBehindService {
  private readonly writeQueue: Array<{
    tenantId: string;
    key: string;
    data: any;
    saver: () => Promise<void>;
    ttl?: number;
  }> = [];

  constructor(
    private readonly cacheService: CacheService,
    private readonly tenantCacheKeyGenerator: TenantCacheKeyGenerator,
  ) {
    // 定期处理写队列
    setInterval(() => this.processWriteQueue(), 1000);
  }

  async updateData<T>(
    tenantId: string,
    key: string,
    data: T,
    saver: () => Promise<void>,
    ttl?: number,
  ): Promise<void> {
    // 立即更新缓存
    await this.cacheService.set(`${tenantId}:${key}`, data, ttl);
    
    // 加入写队列,异步写入数据库
    this.writeQueue.push({ tenantId, key, data, saver, ttl });
  }

  private async processWriteQueue(): Promise<void> {
    while (this.writeQueue.length > 0) {
      const item = this.writeQueue.shift();
      if (item) {
        try {
          await item.saver();
        } catch (error) {
          console.error('Failed to write to database:', error);
          // 可以重新加入队列或记录错误
        }
      }
    }
  }
}

// 实际应用示例
@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
    private readonly cacheAsideService: CacheAsideService,
    private readonly writeThroughService: WriteThroughService,
    private readonly tenantCacheKeyGenerator: TenantCacheKeyGenerator,
  ) {}

  // 读多写少的场景使用 Cache-Aside
  async getUser(tenantId: string, userId: string): Promise<User> {
    const cacheKey = this.tenantCacheKeyGenerator.userKey(tenantId, userId);
    
    return this.cacheAsideService.getCachedData(
      tenantId,
      cacheKey,
      () => this.userRepository.findOne({ where: { id: userId, tenantId } }),
      300, // 5分钟缓存
    );
  }

  // 写操作使用 Write-Through 确保一致性
  async updateUser(tenantId: string, userId: string, updateData: Partial<User>): Promise<User> {
    const cacheKey = this.tenantCacheKeyGenerator.userKey(tenantId, userId);
    const user = await this.userRepository.findOne({ where: { id: userId, tenantId } });
    
    if (!user) {
      throw new ResourceNotFoundException('User', userId);
    }

    Object.assign(user, updateData);
    
    return this.writeThroughService.saveData(
      tenantId,
      cacheKey,
      user,
      () => this.userRepository.save(user),
      300,
    );
  }

  // 批量更新使用 Write-Behind 提升性能
  async batchUpdateUsers(
    tenantId: string,
    updates: Array<{ userId: string; data: Partial<User> }>,
  ): Promise<void> {
    for (const update of updates) {
      const cacheKey = this.tenantCacheKeyGenerator.userKey(tenantId, update.userId);
      const user = await this.userRepository.findOne({ where: { id: update.userId, tenantId } });
      
      if (user) {
        Object.assign(user, update.data);
        
        await this.writeBehindService.updateData(
          tenantId,
          cacheKey,
          user,
          () => this.userRepository.save(user),
          300,
        );
      }
    }
  }
}

Redis 命名空间隔离(防测试污染生产)

在多环境部署中,需要确保不同环境的缓存数据隔离:

typescript
// 环境感知的缓存服务
@Injectable()
export class EnvironmentAwareCacheService {
  private readonly namespace: string;

  constructor(
    @Inject('CACHE_MANAGER') private readonly cacheManager: Cache,
    private readonly configService: ConfigService,
  ) {
    // 根据环境生成命名空间
    const env = this.configService.get<string>('NODE_ENV', 'development');
    const serviceName = this.configService.get<string>('SERVICE_NAME', 'saas-app');
    this.namespace = `${serviceName}:${env}`;
  }

  private getNamespacedKey(key: string): string {
    return `${this.namespace}:${key}`;
  }

  async get<T>(key: string): Promise<T> {
    return this.cacheManager.get(this.getNamespacedKey(key));
  }

  async set(key: string, value: any, ttl?: number): Promise<void> {
    await this.cacheManager.set(this.getNamespacedKey(key), value, ttl);
  }

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

  async delPattern(pattern: string): Promise<void> {
    // 注意:这个方法需要 Redis 客户端支持
    const namespacedPattern = this.getNamespacedKey(pattern);
    const keys = await this.cacheManager.store.getClient().keys(namespacedPattern);
    if (keys.length > 0) {
      await this.cacheManager.store.getClient().del(...keys);
    }
  }

  async clearAll(): Promise<void> {
    // 清除当前环境的所有缓存
    const pattern = this.getNamespacedKey('*');
    await this.delPattern(pattern);
  }
}

// 缓存预热服务
@Injectable()
export class CacheWarmupService {
  constructor(
    private readonly environmentAwareCacheService: EnvironmentAwareCacheService,
    private readonly userService: UserService,
    private readonly orderService: OrderService,
  ) {}

  @Cron('0 2 * * *') // 每天凌晨2点执行
  async warmupCache(): Promise<void> {
    try {
      // 预热热点数据
      console.log('Starting cache warmup...');
      
      // 预热用户统计数据
      await this.warmupUserStats();
      
      // 预热订单统计数据
      await this.warmupOrderStats();
      
      console.log('Cache warmup completed');
    } catch (error) {
      console.error('Cache warmup failed:', error);
    }
  }

  private async warmupUserStats(): Promise<void> {
    // 实现用户统计数据预热逻辑
  }

  private async warmupOrderStats(): Promise<void> {
    // 实现订单统计数据预热逻辑
  }
}

// 缓存监控服务
@Injectable()
export class CacheMonitoringService {
  constructor(
    @Inject('CACHE_MANAGER') private readonly cacheManager: Cache,
  ) {}

  async getCacheStats(): Promise<any> {
    try {
      // 获取 Redis 统计信息
      const info = await this.cacheManager.store.getClient().info();
      return this.parseRedisInfo(info);
    } catch (error) {
      return { error: error.message };
    }
  }

  private parseRedisInfo(info: string): any {
    const stats: any = {};
    const lines = info.split('\n');
    
    for (const line of lines) {
      if (line.includes(':')) {
        const [key, value] = line.split(':');
        stats[key.trim()] = value.trim();
      }
    }
    
    return stats;
  }

  @Cron('*/30 * * * *') // 每30分钟执行一次
  async reportCacheStats(): Promise<void> {
    const stats = await this.getCacheStats();
    console.log('Cache Stats:', stats);
    // 可以发送到监控系统
  }
}

创业团队行动清单

  1. 立即行动

    • 实现基础的缓存服务,包含穿透、击穿防护
    • 设计多租户环境下的缓存键命名规范
    • 选择合适的缓存策略(Cache-Aside 为主)
  2. 一周内完成

    • 实现缓存失效机制
    • 添加环境隔离的缓存命名空间
    • 建立缓存监控和统计
  3. 一月内完善

    • 实现缓存预热机制
    • 添加复杂的缓存策略(Write-Through、Write-Behind)
    • 建立缓存相关的性能测试和调优

总结

缓存策略是提升系统性能的关键,但需要谨慎设计:

  1. 防护机制:实现缓存穿透、击穿、雪崩的防护
  2. 多租户隔离:设计合理的缓存键命名规范
  3. 一致性策略:根据业务场景选择合适的缓存策略
  4. 环境隔离:确保不同环境的缓存数据不会相互污染
  5. 监控运维:建立缓存监控和维护机制

在下一篇文章中,我们将探讨可观测性,这是保障系统稳定运行的重要手段。