前言
这个博客项目一路写下来,到了数据层:笔记从哪来?我选了 Redis——一个只用了 20 行代码就接进来的"数据库"。但它和之前接触的 MySQL 完全不同:没有表、没有 SQL、数据还在内存里。这一篇把 Redis 讲透:它是什么、为什么快、用在项目的哪个位置、以及那个最容易踩的坑——装了依赖并不等于装好了数据库。
一、Redis 是什么
Redis = REmote DIctionary Server(远程字典服务器)
一句话:一个跑在内存里的 key-value 数据库。没有表结构、不用写 SQL,直接 key:value 存取。
把它和你熟悉的东西放在一起对比,位置一下就清楚了:
| localStorage | MySQL | Redis | |
|---|---|---|---|
| 数据在哪 | 浏览器里 | 硬盘 | 内存 |
| 结构 | key:value | 表格 + SQL | key:value(多种类型) |
| 访问方 | 只有你的浏览器 | 所有连服务器的程序 | 所有连服务器的程序 |
| 速度 | — | 毫秒级 | 微秒级 |
| 存活 | 关浏览器还在 | 重启还在 | 重启就没了(默认) |
关键认知:Redis 不是"替代 MySQL"的数据库,而是站在 MySQL 前面的加速层。 它存"一段时间内不变的热数据",MySQL 存权威数据。
二、为什么它这么快
三个原因,面试也常问:
- 数据在内存——读写不碰硬盘,这是根本原因。
- 单线程 + I/O 多路复用——同一时刻只处理一个命令,没有线程切换开销,配合 epoll 同时监听海量连接。反而因此天然"线程安全",多个客户端不会互相干扰。
- 数据模型简单——直接操作内存里的数据结构,没有 SQL 解析、没有索引维护。
代价是内存贵、容量小,所以它从来不是主数据库,而是配合主数据库做读写加速(这正是我 README 里记的那个 I/O 瓶颈场景)。
三、5 种核心数据类型
Redis 的灵魂是类型。同一个 key,用不同方法存,底层结构完全不同,存之前先想好用哪种类型:
| 类型 | 底层结构 | 典型命令 | 典型场景 |
|---|---|---|---|
| String 字符串 | 二进制安全字符串 | set / get / incr | 缓存、计数器、验证码 |
| Hash 哈希 | 一个 key 里塞多个 field:value | hset / hget / hgetall | 对象/记录、用户信息 |
| List 列表 | 双向链表 | lpush / rpop | 消息队列、最近列表 |
| Set 集合 | 无序去重 | sadd / sismember | 点赞用户、标签 |
| ZSet 有序集合 | 每个元素带分数 | zadd / zrange | 排行榜、延迟队列 |
这个项目用的是 Hash:一个 key 叫 notes,里面每个 field 是一条笔记的 ID,value 是笔记的 JSON。这就是"用 Hash 存对象集合"的标准姿势,下面第五节拆给你看。
四、最容易踩的坑:ioredis 只是客户端
项目 package.json 里装的是 ioredis,它是 Redis 的 Node.js 客户端驱动(类似 mysql2 之于 MySQL、mongoose 之于 MongoDB)。
它本身不包含 Redis 服务端。
你写的代码
│
▼
ioredis 驱动 ──TCP:6379──▶ Redis 服务(单独的程序!)
│
▼
服务器进程 / Docker 容器 / WSL 里的 redis-server
所以 lib/redis.js 里这一行:
import Redis from "ioredis";
const redis = new Redis(); // 无参 = 默认连 localhost:6379
如果本机没有 Redis 服务在 6379 端口监听,一执行 hgetall 就会 ECONNREFUSED——驱动找不到服务器。这和装 mysql2 却不开 MySQL 是一个道理。
本地怎么启动(Windows)
Redis 官方不支持 Windows 原生运行,三条路:
| 方式 | 做法 |
|---|---|
| Docker(最省事) | docker run -d -p 6379:6379 redis |
| WSL | 在 WSL 里 sudo apt install redis-server && redis-server |
| 社区移植版 | 下载 tporadowski/redis(Windows 原生二进制) |
启动后验证:
redis-cli ping # 返回 PONG 就说明服务起来了
五、项目里的实际用法:20 行代码接进来
看 lib/redis.js 全貌,它就是"数据业务逻辑"层(Next.js 里放在 lib/ 目录):
// node redis 客户端, 驱动
import Redis from "ioredis";
const redis = new Redis();
// 哈希:key 是字符串 ID,值 note 的序列化字符串
const initialData = {
1702459181837:
'{"title":"sunt aut","content":"quia et suscipit suscipit recusandae","updateTime":"2023-12-13T09:19:48.837Z"}',
1702459182837:
'{"title":"qui est","content":"est rerum tempore vitae sequi sint","updateTime":"2023-12-13T09:19:48.837Z"}',
1702459188837:
'{"title":"ea molestias","content":"et iusto sed quo iure","updateTime":"2023-12-13T09:19:48.837Z"}',
};
export async function getAllNotes() {
const data = await redis.hgetall("notes");
if (Object.keys(data).length == 0) {
await redis.hset("notes", initialData);
}
return await redis.hgetall("notes");
}
三个设计点逐个讲:
1. 哈希结构:对象集合的存法
notes 在 Redis 里长这样:
notes (一个哈希)
├─ field "1702459181837" → "{\"title\":\"sunt aut\",...}"
├─ field "1702459182837" → "{\"title\":\"qui est\",...}"
└─ field "1702459188837" → "{\"title\":\"ea molestias\",...}"
hgetall("notes")→ 一次取回哈希里所有 field-value;hset("notes", data)→ 传入一个对象,对象键就是 field、值就是 value,和 Redis 哈希数据结构逐一对上。
2. 时间戳当 ID,JSON 当值
- field 用 Unix 毫秒时间戳当唯一 ID:不需要自增计数器,毫秒级基本不重复,还能看出创建先后。
- value 是序列化后的 JSON 字符串:对象不能直接存,先
JSON.stringify成字符串,取回来再JSON.parse。
3. 惰性初始化:读不到就自动写入种子数据
const data = await redis.hgetall("notes");
if (Object.keys(data).length == 0) {
await redis.hset("notes", initialData); // 首次为空,自动播种
}
Redis 是独立的外部服务,不跟着项目走——换台机器、清掉数据,notes 就空了。所以代码做了一层兜底:读不到就写入初始数据(种子数据),保证任何环境第一次跑都有数据显示。
4. 谁在消费它
components/Sidebar.js 里,服务端组件直接拿:
import { getAllNotes } from "@/lib/redis";
export default async function Sidebar() {
const notes = await getAllNotes(); // 服务器上直连 Redis,不走 HTTP
return <SidebarNoteList notes={notes} />;
}
链路是:服务端组件 → lib/redis.js → ioredis → Redis 服务 → 返回数据 → 渲染成 HTML 发给浏览器。Redis 全程只跟 Node 进程打交道,浏览器接触不到——这就是"后端服务",和前几篇讲的 SSR 是同一件事。
六、真实项目里的分工:Redis 站在 MySQL 前面
个人练习 20 行够了,真实项目 Redis 的姿势是这样的:
用户请求文章列表
↓
① 先读 Redis:GET notes
├─ 有 → 直接返回(命中缓存,不动 MySQL)
└─ 没有 ↓
② 查 MySQL(权威数据)
③ 写进 Redis,并设过期时间 EXPIRE 300(5 分钟)
④ 返回给用户
要点:
- 过期时间(TTL):Redis 数据默认永不过期,缓存一定要
expire,否则旧数据永远不刷新。 - 缓存穿透兜底:Redis 崩了,顶多回源查一次 MySQL,不会丢数据——所以缓存要能"容忍丢失"。
- 持久化:默认数据在内存、重启就丢。要持久化用 RDB(定期快照,性能好)或 AOF(记录每条写命令,更可靠)。缓存场景一般不需要。
小结
- Redis 是内存型 key-value 数据库,是 MySQL 前面的加速层,不是替代品
- 快的原因:数据在内存 + 单线程 I/O 多路复用 + 模型简单
- 5 种类型各有各的用法,对象集合用 Hash,命令
hset/hgetall - ioredis 只是客户端驱动,本机必须另有 Redis 服务在 6379 端口跑,否则
ECONNREFUSED lib/redis.js的核心模式:哈希存对象集合 + 时间戳当 ID + JSON 当值 + 惰性初始化种子数据- 真实项目套路:读 Redis → 没命中查 MySQL → 写回 Redis 并设过期时间