Redis 从入门到实战:一篇搞定核心数据结构与 NestJS 集成
从"内存缓存"到"多轮对话上下文",彻底掌握 Redis 的核心玩法
前言
在接触 Redis 之前,我一直把它简单地理解为"一个放在服务器上的超大型 JavaScript Map"。直到我在构建 AI 多轮对话应用时,遇到了 Redis + Zustand 的组合,才真正意识到------Redis 远不止是缓存,它是一把能解决无数后端难题的"瑞士军刀"。
本文将从零开始,带你彻底搞懂 Redis 的核心数据结构、过期策略,以及如何在 NestJS 中优雅地集成 Redis,并用它来实现多轮对话上下文管理。无论你是刚接触 Redis 的新手,还是想在实际项目中落地的开发者,这篇文章都能帮你少走弯路。
一、Redis 是什么?为什么它这么快?
Redis(Remote Dictionary Server)是一个基于内存的键值对数据库。它之所以被广泛使用,核心就三点:
- 基于内存:数据全在 RAM 里,读写速度是毫秒甚至微秒级。
- 丰富的数据结构 :不只是
String,还有Hash、List、Set、Sorted Set等。 - 单线程 + 异步 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 类似于编程语言里的 Map 或 Object,特别适合存储对象,因为你可以单独修改某个字段,而不需要序列化整个对象。
常用命令:
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?
- 节省内存:Redis 数据全在内存里,不设过期时间,内存迟早爆掉。
- 保证数据时效性:比如验证码 5 分钟过期、会话 30 分钟无操作自动失效。
- 兜底安全:即使用户没有主动退出,数据也会自动销毁,保护隐私。
如何设置?
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 多轮对话的历史记忆。
数据结构设计
- Key :
chat:{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 的三大核心:
- 数据结构:String、Hash、List 各自的特点和适用场景。
- 过期策略:TTL 的作用、设置方式以及 Redis 的清理机制。
- NestJS 集成 :如何用
ioredis在 NestJS 中优雅地操作 Redis。
更重要的是,你看到了一个真实的生产级场景------用 Redis 的 List 结构存储多轮对话上下文,实现了高性能、低成本、自动过期的会话管理。
Redis 的世界远不止这些,还有 Set、Sorted Set、Stream、Bitmap 等更多数据结构等着你去探索。但掌握了本文的内容,你已经可以应对 80% 的后端缓存和会话管理场景了。
下一步建议:
- 试着在你的 NestJS 项目中跑通上面的代码
- 用
redis-cli命令行工具实时观察数据变化 - 思考如何用 Redis 实现分布式限流或分布式锁
如果这篇文章帮到了你,欢迎点赞、收藏、转发!有任何问题,评论区见 👇
本文首发于 稀土掘金,转载请注明出处。