Redis 从入门到实战:一篇搞定核心数据结构与 NestJS 集成

Redis 从入门到实战:一篇搞定核心数据结构与 NestJS 集成

从"内存缓存"到"多轮对话上下文",彻底掌握 Redis 的核心玩法

前言

在接触 Redis 之前,我一直把它简单地理解为"一个放在服务器上的超大型 JavaScript Map"。直到我在构建 AI 多轮对话应用时,遇到了 Redis + Zustand 的组合,才真正意识到------Redis 远不止是缓存,它是一把能解决无数后端难题的"瑞士军刀"

本文将从零开始,带你彻底搞懂 Redis 的核心数据结构、过期策略,以及如何在 NestJS 中优雅地集成 Redis,并用它来实现多轮对话上下文管理。无论你是刚接触 Redis 的新手,还是想在实际项目中落地的开发者,这篇文章都能帮你少走弯路。


一、Redis 是什么?为什么它这么快?

Redis(Remote Dictionary Server)是一个基于内存的键值对数据库。它之所以被广泛使用,核心就三点:

  1. 基于内存:数据全在 RAM 里,读写速度是毫秒甚至微秒级。
  2. 丰富的数据结构 :不只是 String,还有 HashListSetSorted Set 等。
  3. 单线程 + 异步 I/O:Redis 使用单线程处理命令,避免了多线程的锁竞争,同时配合 I/O 多路复用,轻松支撑十万级 QPS。

💡 一句话总结:Redis 就是一个"快得离谱"的内存数据库,并且它内置了各种好用的数据结构,让你不用自己造轮子。


二、核心数据结构:从 String 到 Hash 再到 List

Redis 支持多种数据结构,但最常用、也最需要掌握的是以下五种。

1. String(字符串)------最基础的万能类型

String 是 Redis 的基石,二进制安全,可以存 JSON、图片、序列化对象等任何数据。

常用命令

bash 复制代码
SET key value          # 设置值
GET key                # 获取值
INCR key               # 原子加1(计数器神器)
SETEX key seconds value # 设置值并指定过期时间
SETNX key value        # 不存在才设置(分布式锁的基础)

底层原理 :Redis 没有用 C 语言的传统字符串,而是自己实现了 SDS(简单动态字符串),支持 O(1) 获取长度、空间预分配减少内存重分配。

2. Hash(哈希)------对象的天然容器

Hash 类似于编程语言里的 MapObject,特别适合存储对象,因为你可以单独修改某个字段,而不需要序列化整个对象。

常用命令

bash 复制代码
HSET user:1001 name "Tom" age 18   # 设置多个字段
HGET user:1001 name                # 获取单个字段
HGETALL user:1001                  # 获取全部字段和值
HINCRBY user:1001 age 1            # 字段原子增加
HDEL user:1001 age                 # 删除字段

典型场景:用户信息、商品详情、配置项等结构化数据。

底层原理 :字段少时用紧凑的 listpack(省内存),字段多时自动转为哈希表,查询效率 O(1)。

3. List(列表)------高性能双端队列

List 是一个有序的字符串序列,支持从两端(左/右)插入和弹出元素。

常用命令

bash 复制代码
LPUSH queue "task1"    # 从左侧插入
RPUSH queue "task2"    # 从右侧插入
LPOP queue             # 从左侧弹出
RPOP queue             # 从右侧弹出
LRANGE queue 0 -1      # 查看全部元素
LTRIM queue 0 99       # 只保留前100个(裁剪)
BLPOP queue 10         # 阻塞弹出(消息队列神器)

典型场景:消息队列、最新动态列表(如微博时间线)、任务队列。


三、Hash vs List:到底该怎么选?

这是很多初学者最纠结的问题。我们用一张表说清楚:

维度 Hash List
数据结构 键值对集合(field → value) 有序字符串序列
访问方式 通过字段名(field)访问 通过索引或范围访问
读写性能 字段级读写 O(1) 头尾操作 O(1),中间访问 O(n)
适用场景 存储对象、频繁更新部分字段 队列、栈、时间线、消息流

选型口诀

  • 要存"对象的多个属性"且经常单独改某个属性 → 选 Hash
  • 要存"有序的消息列表"且频繁追加/裁剪 → 选 List

💡 实战中,两者经常配合使用:用 Hash 存用户的元信息(systemPrompt、createdAt),用 List 存对话消息流。这就是"各取所长"的最佳实践。


四、TTL 过期时间:为什么必须设置?

TTL(Time To Live)是 Redis 的自动过期机制,可以给每个 Key 设置一个"生命倒计时"。

为什么要设置 TTL?

  1. 节省内存:Redis 数据全在内存里,不设过期时间,内存迟早爆掉。
  2. 保证数据时效性:比如验证码 5 分钟过期、会话 30 分钟无操作自动失效。
  3. 兜底安全:即使用户没有主动退出,数据也会自动销毁,保护隐私。

如何设置?

bash 复制代码
EXPIRE session:user123 1800   # 30分钟后自动删除
TTL session:user123           # 查看剩余秒数(-2 表示已消失)

Redis 的过期清理策略

  • 惰性删除:只有当你访问某个 Key 时,Redis 才检查它是否过期,过期则删除。
  • 定期删除:Redis 每隔一段时间(默认 100ms)主动扫描一批过期 Key 并清理。

两者配合,既保证了性能,又避免了内存无限膨胀。

最佳实践建议

数据类型 建议 TTL
会话数据 30 分钟 ~ 24 小时
验证码 5 ~ 10 分钟
API 缓存 根据业务需求(如 1 分钟 ~ 1 小时)

⚠️ 重要 :每次用户活跃时,记得用 EXPIRE 重新设置 TTL,实现"滑动过期"------只要用户一直在用,数据就永远存活。


五、NestJS 中集成 Redis(基于 ioredis)

在 NestJS 中操作 Redis,社区最常用的是 ioredis 客户端。

第一步:安装依赖

bash 复制代码
pnpm add ioredis
pnpm add -D @types/ioredis

第二步:创建 Redis Provider

typescript 复制代码
// redis.provider.ts
import { Provider } from '@nestjs/common';
import Redis from 'ioredis';

export type RedisClient = Redis;

export const RedisProvider: Provider = {
  provide: 'REDIS_CLIENT',
  useFactory: () => {
    return new Redis({
      host: process.env.REDIS_HOST || 'localhost',
      port: parseInt(process.env.REDIS_PORT || '6379'),
      retryStrategy: (times) => Math.min(times * 50, 2000),
    });
  },
};

第三步:注册为全局模块

typescript 复制代码
// app.module.ts
import { Module, Global } from '@nestjs/common';
import { RedisProvider } from './redis.provider';

@Global()
@Module({
  providers: [RedisProvider],
  exports: [RedisProvider],
})
export class RedisModule {}

第四步:在 Service 中使用

typescript 复制代码
import { Inject, Injectable } from '@nestjs/common';
import { RedisClient } from './redis.provider';

@Injectable()
export class ChatService {
  constructor(@Inject('REDIS_CLIENT') private readonly redis: RedisClient) {}

  // 保存消息(使用 List)
  async addMessage(userId: string, role: string, content: string) {
    const key = `chat:${userId}`;
    const msg = JSON.stringify({ role, content, timestamp: Date.now() });
    
    await this.redis
      .multi()
      .rpush(key, msg)
      .ltrim(key, -20, -1)   // 只保留最近 20 条
      .expire(key, 60 * 30)  // 30 分钟过期
      .exec();
  }

  // 获取历史消息
  async getHistory(userId: string): Promise<any[]> {
    const key = `chat:${userId}`;
    const messages = await this.redis.lrange(key, 0, -1);
    // 刷新过期时间(用户活跃,续命)
    await this.redis.expire(key, 60 * 30);
    return messages.map(msg => JSON.parse(msg));
  }
}

六、实战:用 Redis 实现多轮对话上下文

这是本文的终极落地场景------用 Redis 存储 AI 多轮对话的历史记忆。

数据结构设计

  • Keychat:{userId}(List 类型)
  • 存储内容 :最近 N 轮对话(如 10 轮),每轮存为 JSON 对象 {role, content}
  • TTL:30 分钟无操作自动过期

完整流程

typescript 复制代码
@Injectable()
export class AIChatService {
  constructor(
    @Inject('REDIS_CLIENT') private readonly redis: RedisClient,
    private readonly llmService: LLMService,
  ) {}

  async chat(userId: string, userMessage: string): Promise<string> {
    const key = `chat:${userId}`;

    // 1. 获取历史对话
    const history = await this.redis.lrange(key, 0, -1);
    const historyParsed = history.map(msg => JSON.parse(msg));

    // 2. 拼接 Prompt(最近 10 轮)
    const context = historyParsed.slice(-10).map(turn =>
      `${turn.role}: ${turn.content}`
    ).join('\n');

    const prompt = `${context}\nuser: ${userMessage}`;

    // 3. 调用大模型
    const reply = await this.llmService.generate(prompt);

    // 4. 保存本轮对话(用户消息 + AI 回复)
    const pipeline = this.redis.pipeline();
    pipeline.rpush(key, JSON.stringify({ role: 'user', content: userMessage }));
    pipeline.rpush(key, JSON.stringify({ role: 'assistant', content: reply }));
    pipeline.ltrim(key, -20, -1);  // 最多保留 20 条消息(10 轮)
    pipeline.expire(key, 60 * 30); // 刷新过期时间
    await pipeline.exec();

    return reply;
  }
}

为什么这个设计是高效的?

  • 追加操作 O(1)RPUSH 直接插末尾,不涉及历史数据的读写。
  • 自动裁剪LTRIM 保证内存占用恒定。
  • 原子操作pipeline 保证多条命令一次性执行,避免并发问题。
  • 自动过期:30 分钟无活动自动清理,不占多余内存。

七、总结

通过这篇文章,你应该已经掌握了 Redis 的三大核心:

  1. 数据结构:String、Hash、List 各自的特点和适用场景。
  2. 过期策略:TTL 的作用、设置方式以及 Redis 的清理机制。
  3. NestJS 集成 :如何用 ioredis 在 NestJS 中优雅地操作 Redis。

更重要的是,你看到了一个真实的生产级场景------用 Redis 的 List 结构存储多轮对话上下文,实现了高性能、低成本、自动过期的会话管理。

Redis 的世界远不止这些,还有 Set、Sorted Set、Stream、Bitmap 等更多数据结构等着你去探索。但掌握了本文的内容,你已经可以应对 80% 的后端缓存和会话管理场景了。


下一步建议

  • 试着在你的 NestJS 项目中跑通上面的代码
  • redis-cli 命令行工具实时观察数据变化
  • 思考如何用 Redis 实现分布式限流或分布式锁

如果这篇文章帮到了你,欢迎点赞、收藏、转发!有任何问题,评论区见 👇


本文首发于 稀土掘金,转载请注明出处。

相关推荐
海兰12 小时前
【数据库】tdsql(mysql8.0)优化思考三
数据库·redis
千河殇 多想和她15 小时前
线上 Redis 突然“爆”了,怎么办?
数据库·redis·bootstrap
Bug收容所1 天前
12305项目学习day5
java·spring boot·redis·mysql·spring·rocketmq
智码看视界1 天前
Day33-数据层 × 中间件AI化篇:Redis缓存经典问题-击穿、穿透、雪崩的终极解决方案
数据库·redis·缓存·中间件·穿透·雪崩·击穿
后端观测站2 天前
第九章:Redis 为什么会发生缓存穿透、击穿、雪崩?
redis
海兰2 天前
【高速缓存】RedisVL 存储类型选择指南:Hash 与 JSON
人工智能·redis·算法·缓存·json·哈希算法
Devin~Y2 天前
互联网大厂 Java 面试实录:Spring Boot、MyBatis、Redis、Kafka、Spring Security、RAG 与 MCP 全链路问答
java·redis·kafka·mybatis·spring security·spring mvc·sprint boot
海兰2 天前
【高速缓存】RedisVL 索引迁移指南:安全演进索引结构
前端·数据库·redis·安全·缓存·bootstrap
海兰2 天前
【高速缓存】 RedisVL MCP 运行指南(下)
人工智能·redis·哈希算法·高速缓存