Redis基础深度解析
后端性能优化的第一把钥匙:五种核心数据类型怎么选、缓存穿透/击穿/雪崩怎么防、RDB 与 AOF 怎么取舍,以及分布式锁的正确姿势。
一句话概括
Redis 是”把数据放内存里”的键值数据库,单机轻松 10 万+ QPS,延迟是微秒级,而 MySQL 是毫秒级——差着一到两个数量级。前端写 SSR、BFF、接口缓存、限流、排行榜,背后几乎都有它。
面试里 Redis 最常问的不是”有哪些命令”,而是缓存三大坑(穿透/击穿/雪崩)怎么防、为什么单线程还这么快、数据会不会丢。把这些讲清楚,你就能把”我听过 Redis”变成”我真用过 Redis”。
核心知识点
1. 五种核心数据类型:按场景选
Redis 的 value 不只是字符串,它有丰富的结构,选对类型比背命令重要:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# String:缓存值、计数器(INCR 是原子的)
SET article:1:views 1000
INCR article:1:views # 原子 +1
# Hash:存对象,可单独改某个字段(比整体 String 省流量)
HSET user:1 name "张三" age 28
HGET user:1 name
# List:有序列表,可做简单队列 / 最新消息
LPUSH notifications "msg1"
RPOP notifications
# Set:去重、交集(共同好友 / 标签)
SADD article:1:tags "js" "react"
SISMEMBER article:1:tags "js" # 是否含某标签
# Sorted Set:带分数的有序集合,排行榜首选
ZADD ranking 100 "user1"
ZINCRBY ranking 50 "user1" # 加分
ZREVRANGE ranking 0 9 WITHSCORES # Top 10
Node.js 里用 ioredis,典型缓存读取套路:
1
2
3
4
5
6
7
8
9
10
11
const Redis = require('ioredis');
const redis = new Redis();
// String 缓存 API 响应:命中直接返回,未命中查库再写回
async function getArticle(id) {
const cached = await redis.get(`article:${id}`);
if (cached) return JSON.parse(cached);
const data = await db.queryArticle(id);
await redis.setex(`article:${id}`, 3600, JSON.stringify(data)); // 1 小时过期
return data;
}
2. 缓存三大坑:穿透 / 击穿 / 雪崩
这是 Redis 面试的必考题,三个词极易混,先分清:
| 问题 | 触发 | 解法 |
|---|---|---|
| 穿透 | 查根本不存在的数据,缓存和库都没有,请求直捣数据库 | 缓存空值;布隆过滤器拦截 |
| 击穿 | 某个热点 key 刚好过期的瞬间,海量并发同时打进库 | 互斥锁(SETNX);热点 key 不设置过期 + 异步刷新 |
| 雪崩 | 大量 key 同一时刻过期(或 Redis 宕机),请求集体涌向数据库 | 过期时间加随机抖动;多级缓存;限流降级 |
1
2
3
4
5
6
7
8
9
10
11
12
// 防穿透:连"查不到"也缓存一个短 TTL 的空值
async function getData(id) {
const cached = await redis.get(`data:${id}`);
if (cached !== null) return JSON.parse(cached); // 含 'null' 空值
const data = await db.query(id);
await redis.setex(`data:${id}`, data ? 300 : 60, JSON.stringify(data));
return data;
}
// 防雪崩:基础 TTL 之上加随机抖动,避免同时失效
const ttl = baseSeconds + Math.floor(Math.random() * 60);
await redis.setex(key, ttl, value);
3. 过期与内存淘汰:缓存为什么不会撑爆内存
给 key 设了 EXPIRE 后,Redis 并不会准时删除它,而是惰性删除 + 定期抽样删除结合——访问时发现过期就删,同时后台每 100ms 随机抽一批检查。所以过期是”最终生效”,不是精确闹钟。
当内存吃到 maxmemory 上限,Redis 按淘汰策略挑 key 删:
| 策略 | 行为 | 适用 |
|---|---|---|
noeviction | 不淘汰,写满报错 | 不能丢数据 |
allkeys-lru | 所有 key 里淘汰最久未用 | 通用缓存首选 |
volatile-lru | 只淘汰设了过期的 key 里最久未用 | 缓存 + 持久化混合 |
allkeys-lfu | 淘汰访问频率最低(4.0+) | 热点数据场景 |
注意:Redis 的 LRU 是近似 LRU(随机采样一批挑最久未用),不是全量精确排序,换来的是省性能。
4. 持久化:内存数据怎么不丢
Redis 是内存库,重启会丢数据,所以有两套持久化:
- RDB(快照):按
save 900 1(900 秒内至少 1 次改动)定时 dump 成二进制文件。恢复快、文件小,但可能丢最后一次快照之后的数据。 - AOF(日志):记录每条写命令,
appendfsync everysec(每秒刷盘,默认)最多丢 1 秒。文件大、恢复慢,但更安全。 - 混合持久化(Redis 4.0+,7.0 起默认开启):AOF 重写时头部用 RDB 格式、后面追加增量命令,兼顾快恢复与少丢数据,生产推荐。
1
2
3
4
5
# redis.conf 生产推荐配置
appendonly yes
appendfsync everysec
aof-use-rdb-preamble yes # 混合持久化,Redis 7 默认就是 on
save 900 1 # 同时保留 RDB 做冷备
5. 分布式锁:别用裸 DEL 释放
用 Redis 做分布式锁,加锁要对(SET key value NX EX 一条命令原子完成,避免 SETNX 和 EXPIRE 分两步时中间崩溃导致死锁):
1
2
3
// ✅ 加锁:value 用唯一标识(如 UUID),靠它识别"是不是自己加的锁"
const token = crypto.randomUUID();
const ok = await redis.set('order:lock', token, 'NX', 'EX', 10); // 拿到 'OK' 才算抢到
1
2
3
4
5
6
7
8
9
10
11
// ❌ 错误释放:直接 DEL 可能删掉别人的锁(你的业务超时了,锁已过期被别人拿到)
await redis.del('order:lock');
// ✅ 正确释放:用 Lua 脚本保证"判断 value + 删除"原子执行
const release = `
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end`;
await redis.eval(release, 1, 'order:lock', token);
多实例高可用场景用 Redlock 算法(至少 3 个独立节点),但绝大多数业务单实例 + 上面这套”唯一 value + Lua 释放”已经足够。
其实你每天都在用
- 接口缓存:把慢查询 / 聚合结果塞进 Redis,TTL 1 小时,秒回前端
- Session 共享:多台 Node 服务共用一个 Redis 存登录态,用户无感切换机器
- 排行榜:游戏 / 直播间人气榜用 Sorted Set,
ZINCRBY加分、ZREVRANGE取 Top - 限流:
INCR+EXPIRE实现”每 IP 每分钟最多 60 次”,防刷接口 - 消息队列:
LPUSH/RPOP做轻量任务队列(注意用BRPOP阻塞取更稳) - 防缓存穿透:查库的空结果也缓存 60 秒,挡住恶意刷不存在的 ID
- 实时在线人数:
SADD/SCARD维护在线用户集合,视图实时刷新
常见误解(FAQ)
❌ 误区一:”Redis 是单线程,所以高并发扛不住”
“单线程”指命令执行单线程,避免了锁竞争和上下文切换;但网络 IO 在 6.0+ 已多线程化,且底层靠 epoll 多路复用扛住海量连接。数据在内存里,瓶颈在 CPU 而非 IO,单线程反而更快。所以 Redis 快是因为”内存 + IO 多路复用 + 无锁”,不是因为多线程。
❌ 误区二:”加了缓存就万事大吉,数据一致性无所谓”
缓存和数据库是两套存储,更新顺序很关键。常见正确姿势:写时”先更新库,再删缓存”(Cache-Aside),读时”未命中查库并回写”。别天真地认为缓存永远等于最新数据——缓存本来就是”允许的短暂不一致”,关键业务要设计失效 / 双写策略。
❌ 误区三:”分布式锁用 SETNX 抢到,用完 DEL 掉就行”
裸 DEL 是经典坑:如果你的业务执行时间超过锁的 TTL,锁已自动过期、被别人抢走,你再 DEL 删的就是别人的锁。必须用唯一 token + Lua 脚本原子释放(见上文),或上 Redlock。否则分布式锁形同虚设。
❌ 误区四:”Redis 做持久化,就能当主数据库用”
Redis 持久化是”防宕机丢数据”的兜底,不是 ACID 数据库。AOF 默认每秒刷盘仍可能丢 1 秒、RDB 丢得更多;且内存成本远高于磁盘库。它定位是缓存 / 高速读写层,核心账目、订单仍应落在 MySQL / PostgreSQL。
一句话总结
Redis 不是”多了个缓存”,而是给后端装了台”内存加速器”——选对数据类型让它好用,防住三大缓存坑让它稳,用对持久化和分布式锁让它不掉链子,这三点吃透,性能优化面试你就有了硬通货。