Redis 核心
概念
Redis(Remote Dictionary Server)是一个开源的、基于内存的键值数据库,支持多种数据结构,常用于缓存、消息队列、分布式锁等场景。
核心特点:
- 纯内存操作:数据存储在内存中,读写速度极快(读约 10 万 QPS,写约 8 万 QPS)
- 单线程命令执行:避免竞态条件,无需加锁
- 丰富的数据结构:String、List、Hash、Set、Sorted Set 等
- 持久化支持:RDB 快照 + AOF 日志
- 高可用:主从复制、哨兵、集群
核心原理
1. 五种基本数据类型与底层数据结构
| 类型 | 底层结构 | 适用场景 |
|---|---|---|
| String | SDS(简单动态字符串) | 缓存对象、计数器、分布式锁 |
| List | quicklist(ziplist + 双向链表) | 消息队列、最新列表、分页 |
| Hash | ziplist(小数据)/ hashtable(大数据) | 存储对象字段、用户信息 |
| Set | intset(纯整数)/ hashtable(其他) | 去重、交集并集差集、标签系统 |
| Sorted Set | ziplist(小数据)/ skiplist + hashtable(大数据) | 排行榜、带权重队列、范围查询 |
各结构详解:
String — SDS(Simple Dynamic String)
struct sdshdr {
int len; // 已使用长度
int free; // 剩余空间
char buf[]; // 字节数组
};- 相比 C 字符串,SDS 记录长度,
strlen复杂度为 O(1) - 预分配空间,减少内存重分配次数
- 二进制安全,可存储任意二进制数据
List — quicklist
- Redis 3.2 之后,List 统一用 quicklist 实现
- quicklist = 多个 ziplist 组成的双向链表
- ziplist 节点数量由
list-max-ziplist-size控制,平衡内存与性能
Hash — ziplist / hashtable
- 元素数量 ≤
hash-max-ziplist-entries(默认 128)且值长度 ≤hash-max-ziplist-value(默认 64 字节)时使用 ziplist - 超出阈值自动转为 hashtable
Set — intset / hashtable
- 全为整数且数量 ≤
set-max-intset-entries(默认 512)时使用 intset - intset 有序存储,查找用二分,内存紧凑
Sorted Set — ziplist / skiplist + hashtable
- 元素数量 ≤
zset-max-ziplist-entries(默认 128)时使用 ziplist - 超出阈值转为 skiplist(跳表)+ hashtable 的组合
- skiplist 支持范围查询(
ZRANGE、ZRANGEBYSCORE) - hashtable 支持 O(1) 单点查找(
ZSCORE)
- skiplist 支持范围查询(
2. 持久化机制
RDB(Redis Database Snapshot)
RDB 是将某一时刻内存中的数据以二进制快照的形式写入磁盘。
触发方式:
save:阻塞主进程,同步写入(生产环境不推荐)bgsave:fork 一个子进程异步写入,主进程继续处理请求- 配置自动触发:
save 900 1(900 秒内有 1 次写操作则触发)
bgsave 流程:
主进程 --fork--> 子进程
|
| 遍历内存数据,写入临时 RDB 文件
|
v
替换旧 RDB 文件- fork 使用 Copy-On-Write(COW) 机制,fork 时不复制内存,只有写操作时才复制对应页面
- 优点:文件紧凑,恢复速度快,适合灾难恢复和备份
- 缺点:两次快照之间的数据可能丢失
AOF(Append Only File)
AOF 将每条写命令以文本形式追加到日志文件,重启时重放命令恢复数据。
三种 fsync 策略:
| 策略 | 行为 | 性能 | 数据安全性 |
|---|---|---|---|
always | 每条命令写入后立即 fsync | 最慢 | 最高,最多丢失 1 条命令 |
everysec(默认) | 每秒执行一次 fsync | 适中 | 较高,最多丢失 1 秒数据 |
no | 由操作系统决定 fsync 时机 | 最快 | 最低,可能丢失较多数据 |
AOF 重写(Rewrite):
- AOF 文件会随时间增大,通过
bgrewriteaof命令压缩 - 子进程基于当前内存状态重写,期间新写命令同时追加到缓冲区
- 重写完成后将缓冲区内容追加到新 AOF 文件,然后替换旧文件
混合持久化(Redis 4.0+)
[ RDB 格式的全量数据 ][ AOF 格式的增量命令 ]
RDB 头 AOF 尾- 配置:
aof-use-rdb-preamble yes - 重启时先加载 RDB 部分(速度快),再重放 AOF 部分(数据完整)
- 兼顾恢复速度与数据安全性
RDB vs AOF 对比
| 对比维度 | RDB | AOF |
|---|---|---|
| 文件大小 | 小(二进制压缩) | 大(文本命令) |
| 恢复速度 | 快 | 慢(需重放命令) |
| 数据完整性 | 低(可能丢失几分钟数据) | 高(最多丢失 1 秒) |
| 写性能影响 | 低(异步 fork) | 低~中(取决于 fsync 策略) |
| 适用场景 | 备份、容灾 | 数据安全要求高的场景 |
如何选择:
- 对数据安全要求高 → AOF(
everysec) - 对恢复速度要求高 → RDB
- 生产推荐 → 混合持久化(两者兼顾)
3. 内存淘汰策略(8 种)
当内存达到 maxmemory 上限时,Redis 根据配置的策略淘汰键。
| 策略 | 淘汰范围 | 淘汰算法 | 说明 |
|---|---|---|---|
noeviction | — | — | 不淘汰,写操作直接报错(默认) |
allkeys-lru | 所有键 | LRU | 淘汰最近最少使用的键 |
volatile-lru | 设有过期时间的键 | LRU | 对有 TTL 的键做 LRU 淘汰 |
allkeys-random | 所有键 | 随机 | 随机淘汰任意键 |
volatile-random | 设有过期时间的键 | 随机 | 随机淘汰有 TTL 的键 |
volatile-ttl | 设有过期时间的键 | TTL | 优先淘汰剩余 TTL 最短的键 |
allkeys-lfu | 所有键 | LFU | 淘汰使用频率最低的键(4.0+) |
volatile-lfu | 设有过期时间的键 | LFU | 对有 TTL 的键做 LFU 淘汰(4.0+) |
LRU vs LFU:
| 维度 | LRU(最近最少使用) | LFU(最不常使用) |
|---|---|---|
| 核心思路 | 按最近访问时间排序 | 按访问频率排序 |
| 优点 | 实现简单,适合访问时间局部性强的场景 | 能识别"冷门但最近访问一次"的键 |
| 缺点 | 偶发访问的冷数据可能挤占热数据 | 新键初始频率低,可能被过早淘汰 |
| 适用场景 | 通用缓存 | 热点数据分布稳定的场景 |
Redis 的 LRU/LFU 是近似实现,通过采样(默认 5 个键)选出最优淘汰对象,而非严格维护全局排序,以节省内存。
4. 缓存三大问题
缓存穿透(Cache Penetration)
问题: 查询一个数据库中也不存在的数据,缓存永远没有,每次请求都打到数据库。
请求 id=-1
→ 缓存未命中
→ 查数据库 → 无结果
→ 不写缓存
→ 下次同样请求继续穿透恶意攻击场景:大量不存在的 key 请求,导致数据库压力激增。
解决方案:
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 缓存空值 | 查到空结果也写入缓存,设置短 TTL | 简单易实现 | 浪费缓存空间,不适合大量不同 key |
| 布隆过滤器 | 预先将合法 key 加入过滤器,请求先过滤 | 内存占用小,拦截效果好 | 有误判率,不支持删除(Counting BF 可以) |
缓存击穿(Cache Breakdown)
问题: 某个热点 key 恰好过期的瞬间,大量并发请求同时穿透到数据库。
热点 key 过期
→ 大量并发请求同时缓存未命中
→ 同时查数据库 → 数据库压力激增解决方案:
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 互斥锁(分布式锁) | 只允许一个请求查数据库并回写缓存,其他请求等待 | 数据强一致 | 等待期间有延迟,锁超时处理复杂 |
| 逻辑过期 | key 永不设 TTL,在 value 中存逻辑过期时间,过期时异步更新 | 无等待,性能好 | 可能短暂返回旧数据(最终一致) |
缓存雪崩(Cache Avalanche)
问题: 大量 key 在同一时刻过期(或 Redis 服务宕机),请求全部打到数据库,导致数据库崩溃。
解决方案:
| 方案 | 说明 |
|---|---|
| 随机过期时间 | 在基础 TTL 上加随机偏移,避免集中过期 |
| 热点 key 永不过期 | 逻辑过期 + 后台异步刷新 |
| Redis 集群高可用 | 哨兵或 Cluster 模式,避免单点故障导致雪崩 |
| 服务降级 + 限流 | 数据库层面做熔断,保护数据库 |
| 多级缓存 | 本地缓存(Caffeine)+ Redis,Redis 宕机时本地缓存兜底 |
5. 单线程模型与 I/O 多路复用
为什么单线程还快?
Redis 命令执行是单线程的,但性能极高,原因如下:
| 原因 | 说明 |
|---|---|
| 纯内存操作 | 内存访问速度远快于磁盘,无 I/O 等待 |
| 非阻塞 I/O | 使用 epoll/kqueue 等 I/O 多路复用,单线程处理大量连接 |
| 避免上下文切换 | 无多线程切换开销,无锁竞争 |
| 简单高效的数据结构 | 底层数据结构针对内存操作优化 |
I/O 多路复用模型(以 epoll 为例):
多个客户端连接
↓
epoll 监听
↓
就绪事件队列
↓
单线程依次处理
(读取命令 → 执行 → 返回响应)- 单线程处理命令,不存在竞态条件,无需加锁
- 瓶颈在网络 I/O,而非 CPU
Redis 6.0 多线程 I/O
| 版本 | 线程模型 |
|---|---|
| Redis 6.0 之前 | 网络 I/O + 命令执行 均为单线程 |
| Redis 6.0+ | 网络 I/O(读写)多线程 + 命令执行仍为单线程 |
- 多线程只负责网络数据的读取和写回,提升网络吞吐量
- 命令的实际执行依然是单线程串行,保证原子性和线程安全
- 默认关闭,需手动开启:
io-threads 4
6. 集群方案
主从复制(Master-Replica Replication)
全量同步(初次同步):
Replica 发送 PSYNC replicationid offset
Master 执行 bgsave 生成 RDB → 发送给 Replica
Replica 加载 RDB
Master 将 RDB 生成期间的写命令发送给 Replica(replication buffer)增量同步(断线重连):
- Replica 重连后,发送上次同步的 offset
- Master 从 repl_backlog_buffer 中找到缺失的命令发送
- 若 offset 已不在 backlog 中(缓冲区满),则触发全量同步
特点:
- 读写分离:Master 负责写,Replica 负责读
- 无自动故障转移,Master 宕机需手动切换
哨兵模式(Sentinel)
哨兵是独立进程,监控 Redis 节点健康状态,实现自动故障转移。
Sentinel 集群(建议 3 个节点)
↓ 监控
Master + Replica(s)核心功能:
| 功能 | 说明 |
|---|---|
| 监控(Monitoring) | 定期向 Master/Replica 发送 PING,检测是否存活 |
| 通知(Notification) | 故障时通知客户端或管理员 |
| 自动故障转移(Failover) | Master 宕机时,从 Replica 中选出新 Master |
| 配置中心 | 客户端通过 Sentinel 获取当前 Master 地址 |
主观下线 vs 客观下线:
- 主观下线(SDOWN):单个 Sentinel 认为节点不可达
- 客观下线(ODOWN):超过
quorum数量的 Sentinel 都认为节点不可达,才触发故障转移
Redis Cluster(分片集群)
Redis Cluster 通过数据分片实现水平扩展,支持 PB 级数据。
核心概念:
| 概念 | 说明 |
|---|---|
| Slot(槽) | 共 16384 个 slot,每个 key 通过 CRC16(key) % 16384 映射到 slot |
| 节点分配 | 每个 Master 节点负责一部分 slot,各 Master 有对应 Replica |
| Gossip 协议 | 节点间通过 Gossip 协议交换状态信息,去中心化 |
| 重定向 | 客户端请求错误节点时收到 MOVED/ASK 响应,重定向到正确节点 |
为什么是 16384 个 slot?
- 16384 = 2^14,心跳包中用 bitmap 表示 slot 分配,16384 个 slot 只需 2KB
- 节点数通常不超过 1000,16384 个 slot 粒度足够,不需要更多
三种集群方案对比:
| 方案 | 高可用 | 水平扩展 | 自动故障转移 | 复杂度 |
|---|---|---|---|---|
| 主从复制 | 中(手动切换) | 否 | 否 | 低 |
| 哨兵模式 | 高 | 否(单 Master 写) | 是 | 中 |
| Redis Cluster | 高 | 是 | 是 | 高 |
面试常问 & 怎么答
Q1: Redis 为什么这么快?
答题思路: 从存储、I/O 模型、线程模型三个维度展开。
参考回答:
Redis 快的原因主要有以下几点:
- 纯内存操作:所有数据存储在内存中,读写不涉及磁盘 I/O,内存访问延迟在纳秒级别
- 高效的数据结构:底层使用 SDS、skiplist、ziplist 等针对内存优化的数据结构,操作复杂度低
- I/O 多路复用:通过 epoll 等机制,单线程可以同时监听大量连接,非阻塞处理网络事件
- 单线程命令执行:避免了多线程的上下文切换开销和锁竞争,逻辑简单,执行效率高
- Redis 6.0 网络 I/O 多线程:网络读写阶段引入多线程,进一步提升网络吞吐量,命令执行仍为单线程保证原子性
补充:Redis 的性能瓶颈通常在网络带宽,而非 CPU 或内存。
Q2: 缓存穿透、击穿、雪崩分别是什么?怎么解决?
答题思路: 三者都是缓存失效导致请求打到数据库,但原因和场景不同,解决方案也不同。
| 问题 | 触发场景 | 核心解决方案 |
|---|---|---|
| 缓存穿透 | 查询数据库中不存在的数据 | 布隆过滤器拦截非法 key;或缓存空值 |
| 缓存击穿 | 单个热点 key 突然过期,大量并发打入 | 互斥锁(强一致);逻辑过期(高性能) |
| 缓存雪崩 | 大量 key 同时过期,或 Redis 宕机 | TTL 加随机偏移;集群高可用;多级缓存兜底 |
参考回答要点:
- 穿透强调"数据压根不存在",用布隆过滤器在请求入口拦截
- 击穿强调"一个热点 key 的过期瞬间",用互斥锁保证只有一个请求重建缓存
- 雪崩强调"大批量 key 同时失效",用随机 TTL 打散过期时间,用集群避免单点故障
Q3: RDB 和 AOF 有什么区别?怎么选?
答题思路: 从文件格式、数据安全、恢复速度、性能影响四个维度对比,再给出选择建议。
参考回答:
| 维度 | RDB | AOF |
|---|---|---|
| 文件格式 | 二进制快照,文件小 | 文本命令日志,文件大 |
| 数据安全 | 低,两次快照间数据可能丢失 | 高,最多丢失 1 秒(everysec) |
| 恢复速度 | 快(直接加载快照) | 慢(需重放所有命令) |
| 写性能 | 低影响(fork 子进程异步) | 中等影响(everysec 策略) |
选择建议:
- 对数据安全要求高(金融、支付)→ AOF(
everysec) - 对启动恢复速度要求高,允许少量数据丢失 → RDB
- 生产环境推荐混合持久化(
aof-use-rdb-preamble yes),兼顾恢复速度和数据安全
Q4: Redis 集群的三种方案分别是什么?
答题思路: 按演进顺序介绍,每种方案说清楚解决了什么问题、有什么局限。
参考回答:
主从复制:基础方案,Master 写、Replica 读,实现读写分离和数据备份。缺点是 Master 宕机需手动切换,不支持自动故障转移。
哨兵模式(Sentinel):在主从基础上引入哨兵进程,实现自动故障检测和故障转移。多个哨兵通过投票(quorum)决策,防止误判。缺点是写操作仍集中在单个 Master,无法水平扩展写能力。
Redis Cluster:将数据分片到 16384 个 slot,分布在多个 Master 节点上,实现水平扩展。每个 Master 有对应 Replica,节点间用 Gossip 协议同步状态,支持自动故障转移。缺点是架构复杂,跨 slot 的多 key 操作受限。
如何选择:
- 数据量小、并发不高 → 主从复制
- 需要高可用但数据量可控 → 哨兵模式
- 数据量大、需要水平扩展写能力 → Redis Cluster
看到什么就先想到这类
| 关键词 / 场景 | 首先想到 |
|---|---|
| 排行榜、积分榜 | Sorted Set(ZADD / ZRANGE) |
| 去重、共同好友 | Set(交集 SINTER、并集 SUNION) |
| 用户会话、Token 缓存 | String + TTL |
| 购物车、用户信息 | Hash |
| 消息队列、最新动态 | List(LPUSH / RPOP) |
| 分布式锁 | String + SET key value NX EX(或 Redisson) |
| 防止缓存穿透 | 布隆过滤器 |
| 热点 key 过期保护 | 逻辑过期 + 互斥锁 |
| 大量 key 同时过期 | TTL 随机偏移 + 多级缓存 |
| 数据安全要求高的持久化 | AOF everysec + 混合持久化 |
| Redis 自动故障切换 | 哨兵(Sentinel) |
| 海量数据 + 高并发写 | Redis Cluster |
| 计数器(点赞数、访问量) | String INCR / INCRBY |
| 限流 | String INCR + TTL,或 Sorted Set 滑动窗口 |