Redis基础深度解析
一句话概括
Redis 是一个基于内存的键值存储系统,以极致的读写性能(单机 10万+ QPS)和丰富的数据结构成为现代 Web 应用缓存与数据层核心组件,从前端开发者视角掌握它等于掌握了后端性能优化的第一把钥匙。
背景与意义
作为一名前端工程师,你大概率在项目中遇到过这样的场景:页面加载速度慢、接口响应缓慢、高并发场景下后端被打爆。你可能尝试过加 CDN、上 SSR、做懒加载,但有没有想过——后端数据层为什么不能像前端做状态管理一样,把”热点数据”存到最快的地方?
答案是 Redis。
Redis(Remote Dictionary Server)是一个开源的内存数据结构存储系统,可以作为数据库、缓存和消息中间件使用。它的诞生背景要追溯到 2009 年,意大利开发者 Salvatore Sanfilippo 为了解决传统关系型数据库在高并发场景下的性能瓶颈而开发。当年的 Web 2.0 浪潮让动态内容成为标配,每个页面请求都要查询数据库,MySQL 撑不住了。Redis 的出现提供了一种全新的思路:把数据放到内存里,用极致的读写速度换取吞吐量。
当前端项目越来越复杂——SSR 服务、API 网关、实时推送、Session 共享——这些场景的背后几乎都有 Redis 的影子。更直接地说,当你用 Node.js 写后端或 BFF 层时,Redis 几乎是必选项。
概念与定义
Redis 的核心概念可以归纳为几个层次:
内存数据库: 数据主要存储在内存中(RAM),读写速度通常是磁盘数据库的 10-100 倍。一个简单的 SET/GET 命令延迟通常在微秒级别,而 MySQL 查询至少是毫秒级。
键值存储: 数据以 key-value 形式存储。key 是字符串,value 可以是多种数据类型——这正是 Redis 最强大的地方。
单线程模型: Redis 使用单线程处理命令(6.0 后支持多线程 I/O,但核心命令处理仍是单线程)。这意味着它天然避免并发竞争,但同时也意味着单个慢命令会阻塞后续所有命令。
持久化: 虽然是内存数据库,但 Redis 支持将数据持久化到磁盘,重启后可以恢复数据。这也是它和简单的 Memcached 的核心区别之一。
发布/订阅(Pub/Sub): Redis 内置消息队列功能,支持发布/订阅模式,可以用作轻量级消息中间件。
核心知识点拆解
1. 五大基础数据类型
Redis 最核心的能力是它的数据类型。每种类型都有独特的用途和操作命令,理解它们的选择策略是使用 Redis 的第一步。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# String - 最基础的类型
SET user:1:name "张三"
GET user:1:name
SET article:count 1000
INCR article:count # 原子自增 → 1001
# Hash - 适合存储对象
HSET user:1 name "张三" age 28 email "zhangsan@example.com"
HGET user:1 name
HGETALL user:1
# List - 有序列表,适合消息队列
LPUSH messages "msg1"
RPUSH messages "msg2"
LPOP messages
LLEN messages
# Set - 无序集合,适合去重/交集
SADD tags "javascript"
SADD tags "react" "node.js"
SMEMBERS tags
SISMEMBER tags "javascript" # 判断是否存在
# Sorted Set - 有序集合,适合排行榜
ZADD ranking 100 "user1"
ZADD ranking 200 "user2"
ZINCRBY ranking 50 "user1" # 加分
ZRANGE ranking 0 -1 WITHSCORES
Node.js 中的选择策略:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// String → 缓存简单值
const Redis = require('ioredis');
const redis = new Redis();
// 缓存 API 响应
async function getArticle(id) {
const cacheKey = `article:${id}`;
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
const data = await db.queryArticle(id);
await redis.setex(cacheKey, 3600, JSON.stringify(data)); // 1小时过期
return data;
}
// Hash → 缓存对象字段级访问
// 用户信息只需要更新某个字段时,Hash 比全量 String 更高效
async function getUser(id) {
const cacheKey = `user:${id}`;
const exists = await redis.exists(cacheKey);
if (!exists) {
const user = await db.queryUser(id);
await redis.hmset(cacheKey, user);
await redis.expire(cacheKey, 1800);
return user;
}
return await redis.hgetall(cacheKey);
}
// List → 消息队列
async function sendNotification(userId, content) {
await redis.lpush(`notify:${userId}`, JSON.stringify(content));
await redis.ltrim(`notify:${userId}`, 0, 99); // 只保留最近100条
}
// Set → 标签系统/去重
async function addTags(articleId, tags) {
await redis.sadd(`article:${articleId}:tags`, ...tags);
}
// Sorted Set → 排行榜/限流
async function recordView(articleId) {
await redis.zincrby('hot_articles', 1, articleId);
}
2. 过期策略与内存淘汰
Redis 的过期机制是缓存场景的基石。理解它才能避免”缓存雪崩”和”缓存穿透”。
1
2
3
4
5
6
7
8
// 设置过期时间
await redis.set('key', 'value', 'EX', 10); // 10秒后过期
await redis.setex('key', 10, 'value'); // 同上
await redis.expire('key', 10); // 给已有 key 设过期
// 过期策略:消极过期+定期过期
// - 消极:访问时检查是否过期,过期则删除
// - 定期:每100ms随机抽取一批 key 检查并删除过期 key
内存淘汰策略(maxmemory-policy):
| 策略 | 说明 | 适用场景 |
|---|---|---|
| noeviction | 不淘汰,写满报错 | 不允许丢数据 |
| allkeys-lru | 最近最少使用的 key 被淘汰 | 最常用,通用缓存 |
| volatile-lru | 仅淘汰设置了过期时间的 key 中的 LRU | 缓存+持久化混合 |
| allkeys-random | 随机淘汰 | 访问模式均匀 |
| volatile-ttl | 淘汰剩余 TTL 最短的 | 几乎不用 |
| volatile-lfu | 淘汰访问频率最低的(4.0+) | 热点数据场景 |
Node.js 的最佳实践:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 缓存穿透预防:空值缓存
async function getData(id) {
const key = `data:${id}`;
const cached = await redis.get(key);
if (cached !== null) return JSON.parse(cached);
// 注意:cached 可能是 'null' 字符串(我们主动缓存的空值)
if (cached === 'null') return null;
const data = await db.query(id);
// 无论有无数据都缓存,防止缓存穿透
await redis.setex(key, data ? 300 : 60, JSON.stringify(data));
return data;
}
// 缓存雪崩预防:过期时间加随机偏移
function randomExpire(baseSeconds) {
return baseSeconds + Math.floor(Math.random() * 60);
}
3. 持久化机制
Redis 的持久化有两种方式,理解它们的差异对数据安全至关重要。
RDB(快照):
- 在指定时间间隔生成数据集的快照
- 文件紧凑,适合备份/灾难恢复
- 可能丢失最后一次快照后的数据
save 900 1→ 900秒内至少1个key变更则保存
AOF(追加文件):
- 记录每个写操作命令到文件
- 有三种 fsync 策略:always(每次写)、everysec(每秒,默认)、no(由OS决定)
- 文件体积大,恢复慢,但数据更安全
- Redis 7.0+ 默认开启 AOF+RDB 混合持久化
1
2
3
4
5
6
// 在 Node.js 中通过配置决定持久化策略
// 安装时:redis-server --appendonly yes --save ""
// 生产推荐:AOF everysec + 定期 RDB 备份
// 或者直接用 Redis config
await redis.config('SET', 'save', '900 1 300 10 60 10000');
4. 事务与管道
Redis 事务(MULTI/EXEC)保证原子性但不保证回滚。管道(Pipeline)则是批量发送命令提升性能。
1
2
3
4
5
6
7
8
9
10
11
12
// 管道:批量操作,减少网络往返
const pipeline = redis.pipeline();
for (let i = 0; i < 1000; i++) {
pipeline.set(`batch:${i}`, `value${i}`);
}
await pipeline.exec(); // 一次网络往返完成1000次写入
// 事务:保证原子性
const result = await redis.multi()
.decrby('account:balance', 100)
.incrby('account:spent', 100)
.exec();
5. Node.js 中的连接管理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// ioredis 连接最佳实践
const Redis = require('ioredis');
const redis = new Redis({
host: 'localhost',
port: 6379,
password: process.env.REDIS_PASSWORD,
retryStrategy: (times) => {
// 重试策略:指数退避
const delay = Math.min(times * 50, 2000);
return delay;
},
maxRetriesPerRequest: 3,
enableReadyCheck: true,
lazyConnect: true, // 手动连接
});
redis.on('error', (err) => {
console.error('Redis 连接异常:', err.message);
});
redis.on('ready', () => {
console.log('Redis 已就绪');
});
// 连接池管理(ioredis 自带连接池)
// 对于高并发场景,复用连接实例即可,无需手动创建连接池
// 优雅退出
async function shutdown() {
await redis.quit();
process.exit(0);
}
process.on('SIGTERM', shutdown);
实战案例
场景:全栈博客系统的缓存层
假设你正在用 Next.js/Express 开发一个博客系统。我们来构建一个完整的 Redis 缓存层。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// redis-cache.js — 通用缓存工具
const Redis = require('ioredis');
class CacheService {
constructor() {
this.redis = new Redis(process.env.REDIS_URL || {
host: 'localhost',
port: 6379,
});
this.DEFAULT_TTL = 3600; // 1小时
}
// 通用缓存包装器
async getOrSet(key, fetchFn, ttl = this.DEFAULT_TTL) {
const cached = await this.redis.get(key);
if (cached !== null) {
return JSON.parse(cached);
}
const data = await fetchFn();
if (data) {
// 随机 TTL 防止缓存雪崩
const jitter = Math.floor(Math.random() * 60);
await this.redis.setex(key, ttl + jitter, JSON.stringify(data));
}
return data;
}
// 文章缓存
async getArticle(id) {
return this.getOrSet(`article:${id}`,
() => db('articles').where({ id }).first(),
7200 // 2小时
);
}
// 文章列表缓存(带分页)
async getArticleList(page = 1, pageSize = 20) {
const key = `articles:list:${page}:${pageSize}`;
return this.getOrSet(key, async () => {
const articles = await db('articles')
.select('id', 'title', 'summary', 'created_at')
.orderBy('created_at', 'desc')
.limit(pageSize)
.offset((page - 1) * pageSize);
const total = await db('articles').count('id as count').first();
return { articles, total: total.count };
}, 300); // 5分钟刷新一次
}
// 热点文章排行榜(带衰减)
async recordView(articleId) {
const today = new Date().toISOString().split('T')[0];
const multi = this.redis.multi();
// 当日 PV
multi.incr(`pv:daily:${today}:${articleId}`);
multi.expire(`pv:daily:${today}:${articleId}`, 86400 * 3);
// 全局热度(含时间衰减)
multi.zincrby('articles:hot', 1, articleId);
await multi.exec();
}
// 获取热点文章 Top N
async getHotArticles(topN = 10) {
return this.redis.zrevrange('articles:hot', 0, topN - 1, 'WITHSCORES');
}
// 缓存失效
async invalidateArticle(id) {
await this.redis.del(`article:${id}`);
// 同时让列表缓存过期(不用精确删除,设短 TTL 即可)
await this.redis.incr('articles:cache:version'); // 缓存版本化
}
}
module.exports = new CacheService();
在 Express 中的使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// routes/articles.js
const cache = require('./redis-cache');
app.get('/api/articles/:id', async (req, res) => {
try {
const article = await cache.getArticle(req.params.id);
if (!article) return res.status(404).json({ error: '文章不存在' });
// 异步记录访问(不阻塞响应)
cache.recordView(req.params.id).catch(console.error);
res.json(article);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.get('/api/articles', async (req, res) => {
const page = parseInt(req.query.page) || 1;
const data = await cache.getArticleList(page);
res.json(data);
});
app.post('/api/articles/:id/publish', async (req, res) => {
// 更新文章后使缓存失效
await db('articles').where({ id: req.params.id }).update(req.body);
await cache.invalidateArticle(req.params.id);
res.json({ success: true });
});
底层原理
单线程模型为什么这么快?
这是 Redis 被问得最多的问题。答案在于四个层面:
- 纯内存操作:所有数据在内存中,没有磁盘寻址开销
- IO 多路复用:使用 epoll/kqueue 高效处理大量并发连接
- 单线程避免竞争:没有锁和上下文切换的开销
- 数据结构优化:如 SDS(Simple Dynamic String)替代 C 字符串,减少内存分配
Redis 处理一个 GET 命令的完整路径:客户端发送命令 → epoll 事件通知 → 读取命令到输入缓冲区 → 解析命令 → 在哈希表中查找 key → 返回 value → 写入输出缓冲区 → epoll 发送给客户端。整个过程全部在微秒级完成。
哈希表渐进式 rehash
当 Redis 的哈希表负载因子超过阈值时,会进行 rehash(扩容)。但 Redis 不是一次性完成,而是采用渐进式 rehash:
- 为 ht[1] 分配两倍于 ht[0] 的空间
- 维持一个 rehashidx 计数器(初始为 0)
- 每次 CRUD 操作时,顺便将 ht[0] 中 rehashidx 位置上的所有键值对迁移到 ht[1]
- rehashidx 递增
- 迁移完成后切换 ht[0] 和 ht[1],rehashidx 设为 -1
这保证了大 key 场景也不会卡死 Redis。
高频面试题解析
Q1: Redis 为什么快?单线程如何应对高并发?
单线程指的是命令执行线程是单线程。但 Redis 通过 IO 多路复用(epoll)来处理大量并发连接。在 Linux 上,epoll 可以高效管理数十万个 socket。单线程的优势是避免了锁竞争和上下文切换的开销。对于纯内存操作,单线程的瓶颈在于 CPU 而非 IO,所以单线程完全够用。需要注意的是,Redis 6.0 引入了多线程 IO 来处理网络读写,但命令执行仍然是单线程。
Q2: 缓存穿透、缓存击穿、缓存雪崩的区别和解决方案?
缓存穿透: 查询一个根本不存在的数据,缓存和数据库都没有。大量请求直接打到数据库。
- 解决:缓存空值;布隆过滤器(Bloom Filter)
缓存击穿: 一个热点 key 在过期瞬间,大量并发请求同时查询这个 key,直接打穿到数据库。
- 解决:互斥锁(SETNX);永不过期+异步更新
缓存雪崩: 大量 key 在同一时间过期,导致大量请求打到数据库。
- 解决:过期时间加随机值;多级缓存;限流降级
Q3: Redis 的 RDB 和 AOF 有什么区别?如何选择?
| 对比项 | RDB | AOF |
|---|---|---|
| 数据完整性 | 可能丢失最后一次快照后的数据 | 根据策略,最多丢失1秒数据 |
| 文件体积 | 紧凑,小 | 大(AOF重写可缩小) |
| 恢复速度 | 快 | 慢 |
| 对性能影响 | fork 子进程生成快照,大实例有性能影响 | 写入频率高,对磁盘压力大 |
选择建议: 能接受少量数据丢失的场景用 RDB;对数据安全要求高的场景用 AOF + everysec。Redis 4.0+ 支持混合持久化(RDB + AOF),是最推荐的方式。
Q4: Redis 的淘汰策略有哪些?LRU 如何实现?
Redis 的 LRU 实现是近似 LRU,不是精确 LRU。它在淘汰时随机采样一批 key(可配置 maxmemory-samples,默认 5),淘汰其中最久未使用的。这样避免了维护全局 LRU 链表的性能开销。
Q5: 什么是 Redis 分布式锁?如何实现?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 最简单的实现:SETNX + EXPIRE
async function acquireLock(lockKey, ttl = 10) {
const result = await redis.set(lockKey, 'locked', 'NX', 'EX', ttl);
return result === 'OK';
}
async function releaseLock(lockKey) {
await redis.del(lockKey);
}
// 生产级推荐:Redlock 算法(多节点)
// 可以使用 redlock 库:npm install redlock
const Redlock = require('redlock').default;
const redlock = new Redlock([redis1, redis2, redis3]); // 至少3个节点
async function doWithLock() {
const lock = await redlock.acquire(['resource:lock'], 5000);
try {
// 执行临界区代码
} finally {
await lock.release();
}
}
总结与扩展
Redis 远不止是一个”缓存工具”。从前端开发者的角度看,掌握 Redis 意味着你可以:
- 全栈能力跃升:理解缓存策略、数据一致性、性能瓶颈分析
- Node.js 后端优化:Session 共享、API 缓存、消息队列、实时数据
- 架构设计视野:分布式锁、限流、排行榜、计数器等场景的优雅实现
深入学习路径:
- 源码阅读:
ziplist、skiplist、SDS的实现 - 进阶特性:Redis Cluster、Redis Sentinel、Redis Stream
- 替代方案:KeyDB(多线程 Redis 兼容)、Dragonfly(多线程)、Valkey(Redis 开源分支)
最后送你一句 Redis 作者的格言:”Redis is not just a cache, it’s a data structure server.” 理解数据结构的本质,比背命令重要一万倍。