文章

Redis基础深度解析

后端性能优化的第一把钥匙:五种核心数据类型怎么选、缓存穿透/击穿/雪崩怎么防、RDB 与 AOF 怎么取舍,以及分布式锁的正确姿势。

Redis基础深度解析

一句话概括

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 不是”多了个缓存”,而是给后端装了台”内存加速器”——选对数据类型让它好用,防住三大缓存坑让它稳,用对持久化和分布式锁让它不掉链子,这三点吃透,性能优化面试你就有了硬通货。

本文由作者按照 CC BY 4.0 进行授权