Skip to content
Aidenz
Go back

Redis 高频面试题完全指南 - 从基础到架构

22 分钟阅读 · 6404 字
Edit page

引言

Redis 是后端面试中出现频率最高的中间件之一。本文按照由浅入深的顺序,将 Redis 面试高频知识点整理为 12 个模块,每个知识点都配有简洁明了的解释,适合面试前快速复习。

一、Redis 基础

1.1 什么是 Redis?

Redis(Remote Dictionary Server)是一个开源的键值对内存数据库,具有以下特点:

  • 多数据结构:不只是简单的 Key-Value,支持 String、List、Hash、Set、ZSet 等
  • 持久化:支持 RDB 和 AOF 两种持久化方式
  • 高可用:支持主从复制、哨兵模式、Cluster 集群
  • 高性能:单机支撑 10 万+ QPS

1.2 Redis 为什么这么快?

原因说明
纯内存操作数据存储在内存中,读写速度比磁盘快 10 万倍
单线程模型避免线程竞争和上下文切换开销
IO 多路复用epoll 实现单线程同时处理大量连接
高效数据结构SDS、跳表、压缩列表等针对性优化
C 语言实现底层用 C 实现,执行效率高

1.3 Redis 真的是单线程吗?

主线程是单线程,但不是所有操作都是单线程:

操作类型线程模型
命令处理(读写)单线程
持久化(RDB/AOF)后台子线程/子进程
网络 IO(Redis 6.0+)多线程 IO

单线程的好处:无锁竞争、无上下文切换、实现简单、效率高。

1.4 应用场景

  • 热点缓存:数据库查询结果缓存
  • 分布式锁SET key NX EX
  • 计数器:文章阅读量、点赞数
  • 排行榜:ZSet 天然支持排序
  • 限流:滑动窗口计数
  • 会话共享:Session 集中存储
  • 签到:Bitmap 位操作
  • 地理位置:Geo 查找附近的人
  • 消息队列:Stream 可靠消息
  • 基数统计:HyperLogLog UV 统计

二、5 大基础数据类型 + 特殊类型

2.1 基础数据类型

类型底层实现典型应用
StringSDS(简单动态字符串)缓存、计数器、分布式锁
Listquicklist(链表 + 压缩列表)消息队列、栈
Hashziplist / hashtable存储对象、用户信息
Setintset / hashtable去重、交集并集差集
ZSetziplist / 跳表(skiplist)排行榜、延时队列

2.2 特殊数据类型

类型底层实现典型应用
Bitmap位数组用户签到、在线状态
HyperLogLog概率算法UV 去重统计(误差 0.81%)
GeoZSet + GeoHash附近的人、门店搜索
StreamRadix Tree可靠消息队列(替代 Pub/Sub)

三、底层核心数据结构

3.1 SDS(Simple Dynamic String)

Redis 没有直接使用 C 字符串,而是自己实现了 SDS:

struct sdshdr {
    int len;      // 已使用长度
    int free;     // 剩余可用空间
    char buf[];   // 字符数组
};

SDS 相比 C 字符串的优势:

特性C 字符串SDS
获取长度O(n) 遍历O(1) 直接读取 len
缓冲区溢出可能发生空间不够自动扩展
二进制安全否(遇 \0 截断)是(按 len 判断)
空间预分配减少内存分配次数

3.2 跳表(Skip List)

跳表是 ZSet 的核心实现之一,是一种多层有序链表

Level 3:  1 ─────────────────→ 9
Level 2:  1 ────→ 4 ────→ 7 ──→ 9
Level 1:  1 → 3 → 4 → 5 → 7 → 8 → 9

跳表 vs 红黑树:

特性跳表红黑树
实现难度简单复杂
范围查询O(logn + m),天然支持需要中序遍历
内存占用可控每个节点固定开销
插入/删除O(logn)O(logn) + 旋转调整

Redis 选择跳表而不是红黑树,核心原因是:实现简单、范围查询性能好、内存占用可控

3.3 RedisObject

Redis 中所有值都被封装为 RedisObject(Redis 对象):

typedef struct redisObject {
    unsigned type:4;        // 类型(String/List/Hash/Set/ZSet)
    unsigned encoding:4;    // 编码(底层数据结构)
    unsigned lru:LRU_BITS;  // LRU 淘汰信息
    int refcount;           // 引用计数(内存回收)
    void *ptr;              // 指向实际数据的指针
} robj;

一个类型可以有多种编码,Redis 会根据数据量自动选择最优编码:

类型小数据编码大数据编码
Stringint / embstrraw
Listziplistquicklist
Hashziplisthashtable
Setintsethashtable
ZSetziplistskiplist + hashtable

四、过期删除与内存淘汰

4.1 过期键删除策略

Redis 采用惰性删除 + 定期删除的组合策略:

策略原理优点缺点
惰性删除访问 Key 时才检查是否过期节省 CPU浪费内存(过期 Key 不访问就不删)
定期删除每隔一段时间随机抽查部分 Key平衡 CPU 和内存可能漏掉一些过期 Key

两种策略配合使用,既不会占用太多 CPU,也不会让过期 Key 堆积太多内存。

4.2 内存淘汰策略

当内存不足时,Redis 会根据配置的策略淘汰 Key:

策略淘汰范围淘汰依据
noeviction不淘汰内存满时写入报错
allkeys-lru所有 KeyLRU(最近最少使用)
allkeys-lfu所有 KeyLFU(最不经常使用)
allkeys-random所有 Key随机淘汰
volatile-lru设置了过期时间的 KeyLRU
volatile-lfu设置了过期时间的 KeyLFU
volatile-random设置了过期时间的 Key随机淘汰
volatile-ttl设置了过期时间的 KeyTTL 最小的优先淘汰

推荐配置:

  • 缓存场景:allkeys-lru(最常用)
  • 有冷热数据区分:allkeys-lfu
  • 不想丢失数据:noeviction

LRU vs LFU:

算法全称关注点缺点
LRULeast Recently Used最后访问时间偶尔访问的 Key 不会被淘汰
LFULeast Frequently Used访问频率新 Key 可能因频率低被误淘汰

五、持久化

5.1 RDB(Redis Database Backup)

将内存数据以二进制快照的形式写入磁盘。

# 手动触发
SAVE          # 阻塞主线程(不推荐)
BGSAVE        # 后台子进程执行(推荐)
特性说明
优点文件小、恢复速度快
缺点可能丢失最后一次快照后的数据
触发方式手动 BGSAVE、配置自动触发

5.2 AOF(Append Only File)

将每条写命令追加到日志文件中。

AOF 刷盘策略:

策略刷盘时机数据安全性性能
always每条命令执行后最高,不丢数据最慢
everysec每秒一次最多丢 1 秒数据推荐
no操作系统决定可能丢较多数据最快
特性说明
优点数据安全性高、最多丢 1 秒
缺点文件大、恢复速度慢

5.3 混合持久化(生产推荐)

Redis 4.0+ 支持 RDB + AOF 混合持久化:

AOF 文件结构:
┌─────────────┬────────────────────┐
│  RDB 快照   │  增量 AOF 命令     │
└─────────────┴────────────────────┘
  • 前半段是 RDB 格式的全量数据(恢复快)
  • 后半段是 AOF 格式的增量命令(数据安全)

六、缓存三大问题

6.1 缓存穿透

问题:查询的数据在数据库中不存在,每次请求都穿透缓存直达数据库。

恶意请求 → Redis(没有)→ 数据库(也没有)→ 返回空
恶意请求 → Redis(没有)→ 数据库(也没有)→ 返回空
...无限重复,数据库压力巨大

解决方案:

方案原理适用场景
接口参数校验拦截非法请求所有场景
空值缓存查不到时缓存空值(TTL 较短)简单有效
布隆过滤器预先判断 Key 是否可能存在大规模数据
布隆过滤器原理:
请求 → 布隆过滤器 → 不存在?→ 直接返回(拦截穿透)
                 → 可能存在 → 查缓存/数据库

6.2 缓存击穿

问题:某个热点 Key 过期的瞬间,大量并发请求同时打到数据库。

热点 Key 过期 → 100 个请求同时查缓存(miss)→ 100 个请求同时打到数据库 → 数据库压力飙升

解决方案:

方案原理
互斥锁只允许一个请求重建缓存,其他请求等待
热点 Key 永不过期不设 TTL,通过后台任务更新
定时续期后台定时任务在 Key 过期前续期

6.3 缓存雪崩

问题:大量 Key 在同一时间过期,或 Redis 宕机,导致大量请求直接打到数据库。

解决方案:

方案原理
过期时间加随机值避免同时过期:TTL = base + random(0, 300)
集群高可用哨兵 + Cluster 避免单点故障
熔断降级数据库压力过大时返回默认值
多级缓存本地缓存(Caffeine)+ Redis + 数据库

七、缓存一致性

7.1 常用方案:先更新库,再删缓存

# 更新数据
def update(key, value):
    db.update(key, value)    # 1. 先更新数据库
    redis.delete(key)        # 2. 再删除缓存
    time.sleep(0.5)          # 3. 延时(防止主从延迟导致脏数据)
    redis.delete(key)        # 4. 再删一次(延时双删)

延时双删流程:

1. 更新数据库
2. 删除缓存
3. 延时等待(如 500ms)
4. 再次删除缓存

为什么要延时双删?因为主从同步有延迟,在延迟期间其他请求可能读到旧数据并写入缓存。

7.2 强一致方案:Canal 监听 Binlog

数据库变更 → Binlog → Canal 监听 → 异步更新/删除 Redis 缓存
  • 业务代码不需要关心缓存更新
  • 由 Canal 中间件负责同步
  • 适合对一致性要求较高的场景

八、事务与 Lua 脚本

8.1 Redis 事务

MULTI                 # 开启事务
SET key1 value1       # 命令入队
SET key2 value2       # 命令入队
EXEC                  # 执行事务

Redis 事务的特点:

特性说明
原子性命令要么全部执行,要么全部放弃(但不支持回滚)
隔离性事务中的命令串行执行
不支持回滚某条命令报错,其他命令仍然执行

8.2 Watch(乐观锁)

WATCH key             # 监控 key
MULTI                 # 开启事务
SET key newValue      # 修改 key
EXEC                  # 如果 key 被其他客户端修改,事务失败

8.3 Lua 脚本

Lua 脚本可以将多条命令原子执行,是替代事务的更好选择:

# 原子操作:检查并设置
EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('set', KEYS[1], ARGV[2]) else return 0 end" 1 mykey oldval newval

Lua 脚本的优势:

  • 多条命令原子执行,不会被其他命令打断
  • 减少网络往返次数(一次发送多条命令)
  • 替代简单的事务场景

九、分布式锁

9.1 基本实现

# 加锁:SET key value NX EX 过期时间
SET lock:order:1001 "uuid" NX EX 30

# 释放锁(Lua 脚本保证原子性)
EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 lock:order:1001 "uuid"

9.2 常见问题与解决

问题原因解决方案
死锁持有锁的进程崩溃,锁未释放加过期时间(EX
锁超时误删业务执行完但锁已过期,删除了别人的锁加唯一标识(UUID),释放时校验
不可重入同一线程无法多次获取同一把锁使用 Redisson 可重入锁
主从锁失效主节点加锁后宕机,从节点未同步RedLock(红锁)

9.3 Redisson 看门狗机制

客户端加锁(默认 30s 过期)
  → 后台线程每 10s 检查一次
  → 如果锁还在使用,自动续期到 30s
  → 业务完成后释放锁,停止续期

Redisson 提供的功能:

  • 自动续期:看门狗机制,避免业务未完成锁就过期
  • 可重入锁:同一线程可多次加锁
  • 公平锁:按请求顺序获取锁
  • 读写锁:读锁共享、写锁互斥

十、高可用架构

10.1 主从复制

Master(主节点)──复制──→ Slave1(从节点)
       └────复制──→ Slave2(从节点)

同步方式:

方式触发条件过程
全量同步首次连接 / offset 不可用Master 生成 RDB 发送给 Slave
增量同步短时间断线重连Master 发送积压缓冲区中的命令

repl_backlog(积压缓冲区):一个环形缓冲区,保存最近的写命令,用于增量同步,减少全量同步的频率。

10.2 哨兵(Sentinel)

哨兵模式解决主从架构中 Master 的单点故障问题:

Sentinel1 ──监控──→ Master ←──监控── Sentinel2
                      │                    ↑
                      ↓                    │
                   Slave ──监控──── Sentinel3

哨兵的工作流程:

  1. 监控:定期检测 Master 和 Slave 是否正常运行
  2. 主观下线(SDOWN):单个 Sentinel 认为节点不可达
  3. 客观下线(ODOWN):多个 Sentinel(达到 quorum 阈值)都认为不可达
  4. 自动选主:从 Slave 中选举新的 Master
  5. 故障转移:将其他 Slave 指向新 Master,通知客户端

10.3 Cluster 集群

Redis Cluster 将数据分布在多个节点上,支持水平扩展:

Master1 (0-5460)     Master2 (5461-10922)     Master3 (10923-16383)
    ↕                      ↕                        ↕
Slave1                 Slave2                   Slave3

核心概念:

概念说明
哈希槽共 16384 个槽,分配给不同节点
CRC16 算槽slot = CRC16(key) % 16384
分片每个节点负责一部分槽位

为什么是 16384 个槽?

  1. 消息头大小:节点间通信使用 bitmap 表示槽位,16384 位 = 2KB,刚好能放进消息头
  2. 带宽考虑:Redis 集群节点数通常不超过 1000 个,16384 个槽平均分配足够
  3. 如果用 65536:bitmap 占用 8KB,心跳包太大,浪费带宽

跨槽问题:

# 不同 Key 可能在不同槽,无法批量操作
MGET key1 key2 key3    # key1 在节点 A,key2 在节点 B → 报错

# 解决:使用 HashTag 让相关 Key 分配到同一槽
SET {user:1001}:name "张三"
SET {user:1001}:age "25"     # {user:1001} 相同,分到同一槽

十一、高级特性与问题优化

11.1 Pipeline(管道)

批量发送多条命令,减少网络往返:

# 没有 Pipeline:10 条命令 = 10 次网络往返
for i in range(10):
    redis.set(f"key{i}", f"value{i}")

# 使用 Pipeline:10 条命令 = 1 次网络往返
pipe = redis.pipeline()
for i in range(10):
    pipe.set(f"key{i}", f"value{i}")
pipe.execute()

11.2 Pub/Sub vs Stream

特性Pub/SubStream
持久化无(消息发完即丢)有(消息持久存储)
消费确认有(ACK 机制)
消息回溯不支持支持(按 ID 读取历史)
推荐场景实时通知可靠消息队列

11.3 大 Key 问题

什么是大 Key?

  • String 类型:值大于 10KB
  • Hash/List/Set/ZSet:元素数量超过 5000 个

大 Key 的危害:

  • 阻塞主线程(删除大 Key 时)
  • 网络带宽占用高
  • 内存不均衡

解决方案:

方案说明
拆分结构Hash 拆成多个小 Hash
异步删除Redis 4.0+ 用 UNLINK 替代 DEL
避免遍历不用 KEYS *,用 SCAN 分批扫描

11.4 热 Key 问题

热 Key:某个 Key 的 QPS 远高于其他 Key。

解决方案:

方案说明
本地缓存热点数据缓存在应用进程内存中
集群分片将热 Key 复制多份分散到不同节点
请求限流对热 Key 的访问做限流保护

11.5 慢查询

# 查看慢查询日志
SLOWLOG GET 10

# 配置慢查询阈值
CONFIG SET slowlog-log-slower-than 10000  # 10ms

常见慢查询原因:

  • 大 Key 操作(KEYS *HGETALL
  • 复杂命令(SORTSINTERSTORE
  • 短时间内大量命令

十二、高频面试简答题

问题简要回答
Redis 单线程为什么快?纯内存操作 + IO 多路复用 + 单线程无锁 + 高效数据结构
RDB 和 AOF 的区别?RDB 快但可能丢数据,AOF 安全但恢复慢,生产推荐混合持久化
跳表为什么不用红黑树?实现简单、范围查询性能好、内存占用可控
集群为什么 16384 个槽?适配集群通信的 bitmap,带宽占用合理
怎么保证缓存和数据库一致?延时双删、Canal 订阅 Binlog
缓存穿透怎么解决?空值缓存 + 布隆过滤器
缓存击穿怎么解决?互斥锁 + 热点 Key 永不过期
缓存雪崩怎么解决?过期时间加随机值 + 多级缓存 + 集群高可用
分布式锁怎么实现?SET key NX EX + Lua 脚本释放 + Redisson 看门狗续期
LRU 和 LFU 的区别?LRU 看最后访问时间,LFU 看访问频率

学习检查清单

  • 能说清 Redis 为什么快
  • 掌握 5 种基础数据类型和底层实现
  • 理解 SDS、跳表、RedisObject 的作用
  • 能说出过期删除和内存淘汰策略
  • 理解 RDB、AOF 和混合持久化的区别
  • 能解释缓存穿透、击穿、雪崩的原因和解决方案
  • 理解延时双删和 Canal 方案
  • 掌握分布式锁的实现和常见问题
  • 理解主从、哨兵、Cluster 三种架构
  • 了解 Pipeline、大 Key、热 Key 的优化方案

Edit page