Skip to main content

Redis

Redis 是基于内存的高性能 K-V 数据库,全栈面试中「缓存三兄弟 + 分布式锁」是绝对高频。

面试题:Redis 为什么快?

  • 纯内存操作,避免磁盘 IO。
  • 单线程模型(极其关键):Redis 的命令执行和 Node.js 的 V8 引擎一样,都是单线程的!这彻底避免了多线程上下文切换的开销,也完全不需要考虑各种繁琐的“锁”机制。
  • 基于多路复用的 IO 模型:和 Node.js 底层的 epoll/kqueue 如出一辙,Redis 使用 I/O 多路复用技术并发处理海量网络连接,这也是为什么单线程能扛住高并发网络请求的核心大杀器。
  • 高效的数据结构(如跳表、压缩列表、ziplist/listpack)。

面试官追问:“Redis 和 Node.js 都是单线程 + 异步 I/O,它们的设计哲学有什么异曲同工之妙?”

  • 共同点 1:扬长避短,极致利用 CPU 缓存。无论是 Node.js 执行 JS 还是 Redis 执行命令,纯内存计算都是极快的。单线程可以避免 CPU 在不同线程间切换,从而让 CPU 缓存(L1/L2 Cache)的命中率达到最高。
  • 共同点 2:非阻塞网络 I/O 扛并发。它们都没有为每个网络连接创建一个线程,而是把“等待网络数据”这件事交给了操作系统的多路复用机制(epoll),主线程只负责“处理就绪的事件”。
  • 共同点 3:遇到耗时任务都会“外包”出去
    • Node.js 遇到耗时的文件读写或加密计算,会丢给 libuv 的后台线程池处理。
    • Redis 6.0 之后引入了多线程,但仅仅是用来处理网络数据的读写(解析请求/回写响应),因为网络 I/O 成了内存操作的瓶颈。但请注意,Redis 的核心命令执行依然是严格的单线程!此外,Redis 处理耗时的删除操作(如 UNLINK 大 Key),也会丢给后台的异步线程去回收内存。

数据结构与应用场景

类型底层典型场景
StringSDS缓存、计数器(INCR)、分布式锁、限流
Hashziplist / hashtable存对象(用户信息)
Listquicklist消息队列、最新列表
Setintset / hashtable去重、共同好友(交并差集)
ZSet(有序集合)ziplist / 跳表排行榜、延时队列、带权重排序
Bitmap / HyperLogLog / GEO-签到统计、UV 去重计数、地理位置

持久化

面试题:RDB 和 AOF 的区别?

  • RDB:定时把内存快照写入磁盘(二进制)。优点:文件小、恢复快;缺点:会丢失最后一次快照之后的数据。
  • AOF:记录每条写命令(追加)。优点:数据安全(可配置每秒 fsync);缺点:文件大、恢复慢。
  • 混合持久化(4.0+):AOF 重写时以 RDB 格式写全量 + 增量 AOF 命令,兼顾两者。生产常用 RDB + AOF。

过期与淘汰

  • 过期删除策略:惰性删除(访问时才检查过期)+ 定期删除(周期性随机抽查),二者结合。
  • 内存淘汰策略maxmemory-policy):内存满时如何淘汰。常用 allkeys-lru(所有 key 按 LRU 淘汰)、allkeys-lfu(按访问频率)、volatile-ttl(淘汰最快过期的)、noeviction(不淘汰,写报错)。

缓存三兄弟(必考)

面试题:缓存穿透、击穿、雪崩的区别和解决方案?

问题现象解决方案
穿透查询不存在的数据,每次都打到 DB(常见于恶意攻击)① 缓存空值(带短过期);② 布隆过滤器拦截不存在的 key
击穿某个热点 key 过期瞬间,大量并发同时打到 DB① 热点 key 不过期/逻辑过期;② 互斥锁,只让一个请求重建缓存
雪崩大量 key 同时过期 或 Redis 宕机,请求全打到 DB① 过期时间加随机值,分散失效;② 多级缓存;③ 限流降级 + Redis 高可用(集群/哨兵)

缓存一致性

面试题:如何保证缓存与数据库的一致性?

主流方案是 Cache Aside(旁路缓存)+ 先更新数据库,再删除缓存

  • 读:先读缓存,没有则读 DB 并写回缓存。
  • 写:先更新 DB,再删除缓存(而不是更新缓存)。
  • 为什么是「删除」不是「更新」?避免并发写导致脏数据,且延迟加载更高效。
  • 删除失败的兜底:延迟双删、消息队列重试、订阅 binlog(如 Canal)异步删缓存。

分布式锁

面试题:如何用 Redis 实现分布式锁?

// 加锁:SET key value NX PX,原子操作。value 用唯一标识(如 uuid)防止误删别人的锁
const ok = await redis.set(lockKey, requestId, "NX", "PX", 30000);

// 解锁:必须用 Lua 脚本保证「判断是自己的锁」和「删除」的原子性
const unlockScript = `
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
`;
await redis.eval(unlockScript, 1, lockKey, requestId);

要点与追问:

  • 为什么要 NX + 过期时间? NX 保证互斥,过期时间防止持锁进程崩溃导致死锁。
  • 为什么解锁要用 Lua? 防止「判断后、删除前」锁恰好过期被别人获取,导致误删。
  • 锁过期了业务还没执行完怎么办? 用「看门狗」自动续期(如 Redisson)。
  • 单点/主从切换丢锁怎么办?RedLock 算法向多个独立节点加锁(有争议,强一致场景可考虑 zk/etcd)。

Node.js 实践

const Redis = require("ioredis");
const redis = new Redis({ host: "127.0.0.1", port: 6379 });

await redis.set("user:1", JSON.stringify({ name: "a" }), "EX", 3600);
const data = JSON.parse(await redis.get("user:1"));

常用库:ioredis(功能全、支持集群)、redis(官方)。