Skip to content
第 188 / 250 章Node⏱ 12 分钟阅读

第 188 章:缓存(Cache)与性能优化

学习目标

  • 掌握 NestJS 内置缓存模块
  • 集成 Redis 缓存
  • 学会自定义缓存策略
  • 实现查询缓存与自动失效

一、内置缓存模块

1.1 安装

bash
pnpm add @nestjs/cache-manager cache-manager
# 内存版无需其他包,Redis 版需要:
pnpm add cache-manager-redis-store redis

1.2 注册

typescript
// app.module.ts
import { CacheModule } from '@nestjs/cache-manager';

@Module({
  imports: [
    CacheModule.register({
      isGlobal: true,
      ttl: 5000,             // 默认 5 秒
    }),
  ],
})
export class AppModule {}

1.3 基础用法

typescript
@Injectable()
export class UserService {
  constructor(
    @Inject(CACHE_MANAGER) private cache: Cache,
    private userRepo: UserRepository,
  ) {}

  async findAll() {
    // 1. 查缓存
    const cached = await this.cache.get<User[]>('users:all');
    if (cached) return cached;

    // 2. 查 DB
    const users = await this.userRepo.find();

    // 3. 写缓存
    await this.cache.set('users:all', users, 60_000);   // 60 秒

    return users;
  }
}

二、Redis 集成

2.1 配置

typescript
import { redisStore } from 'cache-manager-redis-store';

CacheModule.registerAsync({
  isGlobal: true,
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: async (config: ConfigService) => ({
    store: await redisStore({
      socket: {
        host: config.get('REDIS_HOST'),
        port: config.get('REDIS_PORT'),
      },
      password: config.get('REDIS_PASS'),
      database: 0,
    }),
    ttl: 60 * 1000,
  }),
});

2.2 使用

typescript
@Injectable()
export class CacheService {
  constructor(@Inject(CACHE_MANAGER) private cache: Cache) {}

  async get<T>(key: string): Promise<T | null> {
    return await this.cache.get<T>(key);
  }

  async set(key: string, value: any, ttl?: number) {
    await this.cache.set(key, value, ttl);
  }

  async del(key: string) {
    await this.cache.del(key);
  }

  async reset() {
    await this.cache.reset();
  }
}

三、自动缓存装饰器

3.1 @CacheInterceptor

typescript
import { CacheInterceptor, CacheKey, CacheTTL } from '@nestjs/cache-manager';

@Controller('users')
@UseInterceptors(CacheInterceptor)
export class UserController {
  @Get()
  findAll() {
    return this.userService.findAll();   // 自动缓存
  }

  @Get(':id')
  @CacheKey('user:detail')           // 自定义 key
  @CacheTTL(60_000)                  // 60 秒
  findOne(@Param('id') id: string) {
    return this.userService.findOne(+id);
  }
}

3.2 自定义 Key

typescript
@Get(':id')
@CacheKey((req) => `user:${req.params.id}`)
@CacheTTL(120_000)
findOne(@Param('id') id: string) {}

四、缓存模式

4.1 Cache-Aside(旁路缓存)

typescript
async findOne(id: number) {
  const key = `user:${id}`;
  let user = await this.cache.get<User>(key);
  if (!user) {
    user = await this.userRepo.findOne({ where: { id } });
    if (user) await this.cache.set(key, user, 60_000);
  }
  return user;
}

4.2 Write-Through

typescript
async update(id: number, dto: UpdateUserDto) {
  const user = await this.userRepo.save({ id, ...dto });
  await this.cache.set(`user:${id}`, user, 60_000);   // 同步写缓存
  return user;
}

4.3 Cache-Aside with Invalidate

typescript
async create(dto: CreateUserDto) {
  const user = await this.userRepo.save(dto);
  // 失效列表缓存
  await this.cache.del('users:all');
  return user;
}

async update(id: number, dto: UpdateUserDto) {
  const user = await this.userRepo.save({ id, ...dto });
  await this.cache.del(`user:${id}`);
  await this.cache.del('users:all');
  return user;
}

async remove(id: number) {
  await this.userRepo.delete(id);
  await this.cache.del(`user:${id}`);
  await this.cache.del('users:all');
}

五、缓存粒度

5.1 整体缓存

typescript
const users = await this.cache.get('users:all');

5.2 单条缓存

typescript
const user = await this.cache.get(`user:${id}`);

5.3 部分字段缓存

typescript
const stats = await this.cache.get(`user:${id}:stats`);

六、缓存击穿 / 雪崩 / 穿透

6.1 击穿(热点 key 过期)

typescript
async findOne(id: number) {
  const key = `user:${id}`;
  let user = await this.cache.get<User>(key);

  if (!user) {
    // 双重检查 + 锁
    const lockKey = `lock:user:${id}`;
    const acquired = await this.cache.get(lockKey);
    if (acquired) {
      // 别人在重建,短暂等待重试
      await sleep(50);
      return this.findOne(id);
    }

    // 加锁
    await this.cache.set(lockKey, '1', 5_000);

    user = await this.userRepo.findOne({ where: { id } });
    if (user) await this.cache.set(key, user, 60_000);

    await this.cache.del(lockKey);
  }

  return user;
}

6.2 雪崩(大量 key 同时过期)

typescript
// 加随机偏移
const ttl = 60_000 + Math.random() * 30_000;
await this.cache.set(key, value, ttl);

6.3 穿透(查不存在的数据)

typescript
async findOne(id: number) {
  const key = `user:${id}`;
  let user = await this.cache.get<User>(key);

  if (user === null) {     // 缓存了"不存在"
    return null;
  }

  if (!user) {
    user = await this.userRepo.findOne({ where: { id } });
    if (!user) {
      await this.cache.set(key, null, 30_000);   // 短 TTL 缓存不存在
    } else {
      await this.cache.set(key, user, 60_000);
    }
  }

  return user;
}

七、Interceptor 缓存

typescript
import { CacheInterceptor, ExecutionContext, Injectable } from '@nestjs/common';
import { Cache } from 'cache-manager';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class HttpCacheInterceptor implements CacheInterceptor['intercept'] {
  constructor(private cache: Cache) {}

  async intercept(context: ExecutionContext, next: any): Promise<Observable<any>> {
    const req = context.switchToHttp().getRequest();
    const key = `${req.method}:${req.url}`;
    const cached = await this.cache.get(key);
    if (cached) return of(cached);

    return next.handle().pipe(
      tap(data => this.cache.set(key, data, 60_000)),
    );
  }
}

八、缓存统计

typescript
@Injectable()
export class UserService {
  private hitCount = 0;
  private missCount = 0;

  async findOne(id: number) {
    const key = `user:${id}`;
    const cached = await this.cache.get<User>(key);

    if (cached) {
      this.hitCount++;
      return cached;
    }

    this.missCount++;
    const user = await this.userRepo.findOne({ where: { id } });
    if (user) await this.cache.set(key, user, 60_000);
    return user;
  }

  getStats() {
    const total = this.hitCount + this.missCount;
    return {
      hitRate: total ? this.hitCount / total : 0,
      hits: this.hitCount,
      misses: this.missCount,
    };
  }
}

九、TTL 策略

数据TTL
用户信息5-10 分钟
列表1-5 分钟
配置1 小时
字典1 天
排行榜实时

十、Redis 高级用法

typescript
@Injectable()
export class RedisService {
  private client: Redis;

  constructor() {
    this.client = new Redis({
      host: 'localhost',
      port: 6379,
    });
  }

  // 分布式锁
  async lock(key: string, ttl = 10_000) {
    const token = uuidv4();
    const ok = await this.client.set(key, token, 'PX', ttl, 'NX');
    return ok === 'OK' ? token : null;
  }

  async unlock(key: string, token: string) {
    const lua = `
      if redis.call("get", KEYS[1]) == ARGV[1] then
        return redis.call("del", KEYS[1])
      else
        return 0
      end
    `;
    await this.client.eval(lua, 1, key, token);
  }

  // 排行榜
  async addScore(key: string, member: string, score: number) {
    await this.client.zadd(key, score, member);
  }

  async topN(key: string, n = 10) {
    return this.client.zrevrange(key, 0, n - 1, 'WITHSCORES');
  }
}

十一、本章小结

概念用途
@nestjs/cache-manager内置缓存
CacheManager注入缓存
CacheInterceptor自动缓存
@CacheKey / @CacheTTL自定义
redis-storeRedis 存储
双写 / 失效一致性策略

动手练习

  1. 注册 CacheModule,用内存缓存
  2. 集成 Redis,实现用户列表缓存
  3. 写一个自动失效的 CRUD 缓存策略
  4. 实现 Cache-Aside + 失效的 user 接口

推荐阅读


下一章:第 189 章:任务调度(Schedule)与队列

本站基于 VitePress 构建 · 由 Codebook 团队维护