NestJS 系列教程(十五):缓存体系设计 —— Redis、接口缓存与缓存三大问题解决方案

🧠 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 事务
  • 乐观锁 / 悲观锁
  • 防止超卖
  • 高并发下的库存扣减设计
  • 与缓存的结合策略
相关推荐
筱顾大牛2 小时前
缓存更新策略
java·redis·缓存
難釋懷2 小时前
Redis主从-repl_backlog原理
数据库·redis·缓存
随便写个昵称2 小时前
Django中的httpresponse返回类型
数据库·django·sqlite
银河麒麟操作系统2 小时前
服务器通用(全架构)【深入解析文件操作中的常见问题:空洞、传输与大小差异】技术文章
运维·服务器·数据库
爬山算法2 小时前
MongoDB(32)如何查看集合中的索引?
数据库·mongodb
艾莉丝努力练剑2 小时前
【MYSQL】MYSQL学习的一大重点:MYSQL库的操作
android·linux·运维·数据库·人工智能·学习·mysql
JuneXcy2 小时前
第5讲 MySql数据操纵语句--复杂查询
数据库·sql·mysql
小鸡吃米…2 小时前
Python 中的并发 —— 简介
服务器·数据库·python
听雪楼主.2 小时前
某客户系统Oracle数据运行慢分析
数据库·oracle