🧠 NestJS 系列教程(十五):缓存体系设计 ------ Redis、接口缓存与缓存三大问题解决方案
如果说限流是防止被打爆,
那缓存就是防止被"累死"。
✨ 本篇目标
咱们将一起学会:
-
如何在 NestJS 中接入 Redis
-
如何设计接口级缓存
-
如何优雅地封装缓存服务
-
什么是:
- 缓存穿透
- 缓存击穿
- 缓存雪崩
-
如何与限流、traceId、异常体系联动
🧠 一、为什么必须要有缓存?
假设有一个接口:
GET /products/1
没有缓存:
每个请求 → 数据库
1000 QPS → 数据库直接打爆
有缓存:
第一次查数据库
之后全部走 Redis
数据库压力几乎为 0
👉 Redis 是后端系统的"减压阀"
🧱 二、在 NestJS 中接入 Redis(推荐方式)
我们使用 ioredis
1️⃣ 安装依赖
bash
npm install ioredis
2️⃣ 创建 RedisModule
src/
└── common/
└── redis/
├── redis.module.ts
└── redis.service.ts
3️⃣ redis.module.ts
ts
import { Global, Module } from '@nestjs/common';
import { RedisService } from './redis.service';
@Global()
@Module({
providers: [RedisService],
exports: [RedisService],
})
export class RedisModule {}
@Global() → 全局可用
4️⃣ redis.service.ts
ts
import { Injectable, OnModuleDestroy } from '@nestjs/common';
import Redis from 'ioredis';
@Injectable()
export class RedisService implements OnModuleDestroy {
private client: Redis;
constructor() {
this.client = new Redis({
host: '127.0.0.1',
port: 6379,
});
}
async get(key: string) {
return this.client.get(key);
}
async set(key: string, value: any, ttl?: number) {
if (ttl) {
await this.client.set(key, JSON.stringify(value), 'EX', ttl);
} else {
await this.client.set(key, JSON.stringify(value));
}
}
async del(key: string) {
return this.client.del(key);
}
onModuleDestroy() {
this.client.disconnect();
}
}
🧠 三、实现接口级缓存(Service 层封装)
示例:商品查询接口
ts
@Injectable()
export class ProductsService {
constructor(
private redisService: RedisService,
) {}
async findOne(id: number) {
const cacheKey = `product:${id}`;
// 1️⃣ 先查缓存
const cached = await this.redisService.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// 2️⃣ 查数据库(模拟)
const product = {
id,
name: 'MacBook',
price: 19999,
};
// 3️⃣ 写入缓存(60 秒)
await this.redisService.set(cacheKey, product, 60);
return product;
}
}
🧨 四、缓存三大问题(必须理解)
① 缓存穿透
问题:
查询一个根本不存在的 id:
/products/999999
缓存没有 → 查数据库
数据库也没有 → 每次都打数据库
攻击者可直接拖垮系统
解决方案:
缓存空值
ts
if (!product) {
await this.redisService.set(cacheKey, 'null', 30);
return null;
}
② 缓存击穿
问题:
某个热点数据突然过期:
product:1
一瞬间 5000 个请求同时打数据库
解决方案:
加分布式锁(Redis setnx)
ts
const lockKey = `lock:${cacheKey}`;
真实生产推荐:
- Redlock
- 或者设置逻辑过期时间
③ 缓存雪崩
问题:
大量缓存同时过期
比如:
所有 key TTL = 60 秒
60 秒一到 → 全部打数据库
解决方案:
加随机时间
ts
const ttl = 60 + Math.floor(Math.random() * 30);
🧠 五、封装一个更优雅的 CacheService
生产中建议再封一层:
common/cache/cache.service.ts
ts
@Injectable()
export class CacheService {
constructor(private redis: RedisService) {}
async getOrSet(
key: string,
fetchFn: () => Promise<any>,
ttl = 60,
) {
const cached = await this.redis.get(key);
if (cached) {
return JSON.parse(cached);
}
const data = await fetchFn();
if (data) {
await this.redis.set(
key,
data,
ttl + Math.floor(Math.random() * 30),
);
}
return data;
}
}
使用方式:
ts
return this.cacheService.getOrSet(
`product:${id}`,
() => this.productRepo.findOneBy({ id }),
);
🔗 六、缓存 + 限流 + traceId 联动
当缓存未命中:
ts
console.log(
`[CacheMiss] traceId=${getTraceId()} key=${cacheKey}`,
);
当频繁访问:
- RateLimitGuard 会拦截
- traceId 帮助定位问题
- TimeoutInterceptor 兜底
👉 现在你已经有:
- 限流
- 超时
- 缓存
- 异常统一
- traceId
- 日志体系
这已经是一套真正可上线的 NestJS 架构。
🧠 七、真实生产缓存架构建议
推荐目录:
common/
├── redis/
├── cache/
├── logger/
├── guards/
├── interceptors/
├── filters/
└── context/
缓存策略:
- 查询接口 → 必缓存
- 写接口 → 删除缓存
- 热点数据 → 长缓存
- 用户数据 → 短缓存
✅ 本章小结
学完本篇文章后的你已经掌握:
- Redis 在 NestJS 中的接入
- 接口级缓存实现
- 缓存穿透 / 击穿 / 雪崩解决方案
- 封装 CacheService
- 与整套系统架构的联动设计
🔮 下一篇预告(第 16 篇)
第16篇:数据库事务与高并发一致性控制
跟着我咱们一起来学习:
- TypeORM 事务
- 乐观锁 / 悲观锁
- 防止超卖
- 高并发下的库存扣减设计
- 与缓存的结合策略