一、为什么我们需要服务端缓存

想象一下,你正在开发一个电商网站,商品详情页每天被访问几百万次。每次用户打开页面,前端都会向后端请求相同的商品数据。如果每次请求都去数据库查询,数据库压力会非常大,响应速度也会变慢。这时候,服务端缓存就能派上用场了——它可以把频繁访问的数据暂时存起来,下次请求直接返回缓存结果,避免重复计算或查询。

在Angular项目中,我们通常通过HTTP请求获取数据。如果没有缓存策略,多个组件可能同时请求相同的数据,导致不必要的网络开销和服务器压力。比如:

// Angular示例:没有缓存时,多个组件重复请求相同数据
// 商品服务(无缓存)
@Injectable()
export class ProductService {
  constructor(private http: HttpClient) {}

  // 获取商品详情(每次调用都会发起新请求)
  getProduct(id: number): Observable<Product> {
    return this.http.get<Product>(`/api/products/${id}`);
  }
}

// 组件A调用
this.productService.getProduct(1).subscribe(...);

// 组件B也调用相同接口
this.productService.getProduct(1).subscribe(...);
// 结果:发送了两次完全相同的请求!

二、Angular中的缓存实现方案

1. 使用RxJS的shareReplay操作符

RxJS是Angular的强力搭档,它的shareReplay操作符可以轻松实现缓存。原理是:第一次请求后,将结果缓存下来,后续请求直接返回缓存值。

// Angular + RxJS示例:使用shareReplay缓存
@Injectable()
export class CachedProductService {
  private cache = new Map<number, Observable<Product>>();

  constructor(private http: HttpClient) {}

  getProduct(id: number): Observable<Product> {
    // 如果缓存中没有,创建新请求并缓存
    if (!this.cache.has(id)) {
      const request = this.http.get<Product>(`/api/products/${id}`).pipe(
        shareReplay(1) // 缓存最近一次结果
      );
      this.cache.set(id, request);
    }
    return this.cache.get(id)!;
  }
}

2. 结合HttpInterceptor全局缓存

如果想对所有HTTP请求统一管理,可以用HttpInterceptor拦截请求,实现全局缓存:

// Angular拦截器示例:全局缓存GET请求
@Injectable()
export class CacheInterceptor implements HttpInterceptor {
  private cache = new Map<string, any>();

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // 只缓存GET请求
    if (req.method !== 'GET') {
      return next.handle(req);
    }

    const cachedResponse = this.cache.get(req.urlWithParams);
    if (cachedResponse) {
      // 返回缓存结果
      return of(cachedResponse);
    }

    // 没有缓存,继续请求并保存结果
    return next.handle(req).pipe(
      tap(event => {
        if (event instanceof HttpResponse) {
          this.cache.set(req.urlWithParams, event);
        }
      })
    );
  }
}

三、高级缓存策略与优化

1. 缓存失效与更新

缓存不能永远有效,比如商品价格变化时,需要让旧缓存失效。可以通过以下方式实现:

// Angular示例:手动清除缓存
@Injectable()
export class ProductService {
  private cache = new Map<number, Observable<Product>>();

  // 清除单个商品缓存
  clearProductCache(id: number) {
    this.cache.delete(id);
  }

  // 强制刷新数据(跳过缓存)
  forceGetProduct(id: number): Observable<Product> {
    this.clearProductCache(id);
    return this.getProduct(id);
  }
}

2. 基于时间的自动过期

通过setTimeoutexpiry逻辑实现自动过期:

// Angular示例:带过期时间的缓存
@Injectable()
export class ExpiringCacheService {
  private cache = new Map<string, { data: any; expiry: number }>();

  getWithExpiry(key: string): Observable<any> {
    const cached = this.cache.get(key);
    if (cached && cached.expiry > Date.now()) {
      return of(cached.data); // 返回未过期的缓存
    }
    // 否则重新请求...
  }

  setWithExpiry(key: string, data: any, ttl: number) {
    this.cache.set(key, {
      data,
      expiry: Date.now() + ttl // 设置过期时间戳
    });
  }
}

四、实战场景与注意事项

1. 适用场景

  • 高频读取低频变更的数据:如商品信息、配置数据
  • 计算成本高的接口:如大数据分析报表
  • 多组件共享的数据:如用户登录信息

2. 潜在问题与解决方案

问题1:内存泄漏
长时间运行的缓存可能占用过多内存。
解决:定期清理或使用LRU(最近最少使用)策略。

// Angular示例:LRU缓存实现
export class LRUCache {
  private cache = new Map<string, any>();
  private maxSize = 100;

  get(key: string): any {
    if (!this.cache.has(key)) return null;
    const value = this.cache.get(key);
    this.cache.delete(key); // 删除后重新插入,保持最近访问
    this.cache.set(key, value);
    return value;
  }

  set(key: string, value: any) {
    if (this.cache.size >= this.maxSize) {
      // 删除最早的一个键
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
    }
    this.cache.set(key, value);
  }
}

问题2:数据一致性
缓存数据可能与数据库不同步。
解决:通过WebSocket或事件通知更新缓存。

3. 何时不该用缓存

  • 实时性要求极高的数据:如股票价格
  • 写多读少的场景:缓存命中率低反而增加复杂度
  • 安全性敏感数据:如支付凭证

五、总结

服务端缓存是提升Angular应用性能的利器,但需要根据业务场景选择合适的策略。RxJS提供了强大的工具,而HttpInterceptor可以实现无侵入式的全局缓存。记住要处理好缓存失效和内存管理,这样才能真正发挥缓存的优势。

下次当你的应用出现重复请求时,不妨试试这些方案,你会惊讶于它们带来的性能提升!