第 188 章:缓存(Cache)与性能优化
学习目标
- 掌握 NestJS 内置缓存模块
- 集成 Redis 缓存
- 学会自定义缓存策略
- 实现查询缓存与自动失效
一、内置缓存模块
1.1 安装
bash
pnpm add @nestjs/cache-manager cache-manager
# 内存版无需其他包,Redis 版需要:
pnpm add cache-manager-redis-store redis1.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-store | Redis 存储 |
| 双写 / 失效 | 一致性策略 |
动手练习
- 注册 CacheModule,用内存缓存
- 集成 Redis,实现用户列表缓存
- 写一个自动失效的 CRUD 缓存策略
- 实现 Cache-Aside + 失效的 user 接口