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 事务
  • 乐观锁 / 悲观锁
  • 防止超卖
  • 高并发下的库存扣减设计
  • 与缓存的结合策略
相关推荐
arronKler12 分钟前
大数据量高并发的数据库优化
服务器·数据库·oracle
星辰_mya25 分钟前
OSI 七层模型之“跨国诈骗集团”深度讲解
运维·服务器·后端·面试·架构师
IT_陈寒31 分钟前
SpringBoot自动配置这破玩意儿又坑我一次
前端·人工智能·后端
祖传F8736 分钟前
SQL DATE()函数会抹去时间戳
数据库·sql
untE EADO37 分钟前
在 MySQL 中使用 `REPLACE` 函数
android·数据库·mysql
Absurd58740 分钟前
Redis如何限制列表最大长度_利用LTRIM指令截断List保留最新记录
jvm·数据库·python
2401_8822737240 分钟前
SQL函数面试题解析_函数性能与设计考点
jvm·数据库·python
l1t43 分钟前
DeepSeek总结的DuckDB internals 的 设计与实现 (DiDi)
数据库·duckdb
a95114164244 分钟前
mysql查询分析中如何快速识别全表扫描_通过EXPLAIN中的type列检查
jvm·数据库·python
coNh OOSI44 分钟前
Redis——Windows安装
数据库·windows·redis