mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
2720 字
7 分钟
Redis 面试题
2024-04-27

一、Redis 基础#

1.1 Redis 是什么?有什么特点?#

Redis(Remote Dictionary Server)是一个开源的、基于内存的高性能键值对数据库。主要特点:

特性说明
高性能基于内存操作,读写速度可达 10 万次/秒以上
数据结构丰富支持 String、Hash、List、Set、Sorted Set 等类型
持久化支持 RDB 快照和 AOF 日志两种持久化方式
单线程核心操作使用单线程,避免锁竞争
支持事务提供事务机制,保证命令的原子性执行
主从复制支持主从架构,实现读写分离和高可用

1.2 为什么 Redis 这么快?#

Redis 的高性能源于多个方面:

  1. 基于内存操作:数据存储在内存中,避免了磁盘 I/O 的开销

  2. 单线程模型:避免了多线程的上下文切换和锁竞争

  3. I/O 多路复用:使用 epoll(Linux)实现高并发连接处理

  4. 高效的数据结构:针对不同场景优化了底层实现

传统数据库:客户端 → 网络 → 线程池 → 磁盘 I/O → 返回
Redis:客户端 → 网络 → 单线程事件循环 → 内存操作 → 返回

1.3 Redis 为什么采用单线程?#

Redis 的单线程指的是网络请求处理使用单线程,而非整个 Redis 进程只有一个线程。实际上 Redis 还有后台线程处理持久化、异步删除等任务。

单线程的优势:

  1. 避免锁竞争:不需要加锁解锁,简化实现
  2. 避免上下文切换:CPU 不用在多线程间切换
  3. 代码简洁:更容易维护和调试

Redis 6.0 引入了多线程来处理网络 I/O,但核心命令执行仍是单线程。

二、数据结构#

2.1 Redis 有哪些数据类型?各自的应用场景是什么?#

类型底层实现典型场景示例命令
StringSDS(简单动态字符串)缓存、计数器、分布式锁SETGETINCR
HashDict + listpack对象存储、购物车HSETHGETHMGET
ListQuickList消息队列、最新列表LPUSHLRANGEBRPOP
SetDict + intset标签、共同好友、抽奖SADDSINTERSRANDMEMBER
Sorted SetDict + SkipList排行榜、延迟队列ZADDZRANGEZSCORE

2.2 String 类型的底层实现是什么?#

Redis 3.2 之后,String 类型根据存储内容使用不同的编码:

// SDS(Simple Dynamic String)结构
struct sdshdr {
uint8_t len; // 已使用长度
uint8_t alloc; // 分配的总空间
uint8_t flags; // 类型标识
char buf[]; // 字节数组
};

编码类型

编码类型条件说明
int整数值,范围在 long 内直接存储整数
embstr字符串长度 ≤ 44 字节连续内存,一次分配
raw字符串长度 > 44 字节SDS 结构

SDS 相比 C 字符串的优势:

  1. O(1) 获取长度:直接读取 len 属性
  2. 防止缓冲区溢出:自动扩展空间
  3. 减少内存分配:空间预分配和惰性释放
  4. 二进制安全:可以存储任意二进制数据

2.3 Hash 类型的底层实现?#

Hash 类型根据数据量使用不同编码:

listpack 编码(小数据量):

  • 元素数量 < 512,所有键值长度 < 64 字节
  • 连续内存存储,紧凑高效

Dict 编码(大数据量):

  • 超过阈值时转换为哈希表
  • O(1) 时间复杂度的读写
# Hash 应用示例:购物车
HSET cart:user:1001 item:001 2 # 商品 001,数量 2
HSET cart:user:1001 item:002 1 # 商品 002,数量 1
HGET cart:user:1001 item:001 # 获取商品 001 数量
HGETALL cart:user:1001 # 获取整个购物车

2.4 List 类型的底层实现?#

Redis 3.2 之后,List 使用 QuickList(快速列表)作为底层实现:

QuickList 结构:
┌─────────────────────────────────────────────┐
│ Node 1 │ Node 2 │ Node 3 │ ... │
│ (listpack)│ (listpack)│ (listpack)│ │
└─────────────────────────────────────────────┘
↕ ↕ ↕
双向链表连接各节点

QuickList 是双向链表 + listpack的组合:

  • 双向链表:便于两端操作(LPUSH/RPUSH/LPOP/RPOP)
  • listpack:每个节点存储多个元素,减少内存碎片
# List 应用示例:消息队列
LPUSH queue:orders "order:001" # 生产者:左边推入
LPUSH queue:orders "order:002"
BRPOP queue:orders 5 # 消费者:右边阻塞弹出

2.5 Set 和 Sorted Set 的底层实现?#

Set 类型

编码条件说明
intset全是整数,数量 < 512有序整数数组
Dict其他情况哈希表

Sorted Set 类型

使用 Dict + SkipList(跳表) 的组合结构:

SkipList 结构示意:
Level 4: 1 ──────────────────────────→ 100
Level 3: 1 ────────────→ 50 ────────→ 100
Level 2: 1 ────→ 25 ───→ 50 ────→ 75 → 100
Level 1: 1 → 10 → 25 → 50 → 60 → 75 → 100

跳表的优势:

  1. O(logN) 查找:类似二分查找的效率
  2. 范围查询高效:底层双向链表便于遍历
  3. 实现简单:相比平衡树更容易实现

Dict 存储 member→score 映射,SkipList 按 score 排序存储:

# Sorted Set 应用示例:排行榜
ZADD leaderboard 100 player:001
ZADD leaderboard 150 player:002
ZADD leaderboard 120 player:003
ZREVRANGE leaderboard 0 9 WITHSCORES # 获取 Top 10

三、持久化机制#

3.1 Redis 持久化有哪些方式?#

Redis 提供三种持久化方式:

方式原理优点缺点
RDB定时快照恢复快、文件小可能丢失数据
AOF追加写命令日志数据完整性好文件大、恢复慢
混合RDB + AOF兼具两者优点需要 Redis 4.0+

3.2 RDB 持久化是如何工作的?#

RDB(Redis Database)是 Redis 的快照持久化方式:

触发方式

# 配置自动触发
save 900 1 # 900 秒内有 1 次写操作
save 300 10 # 300 秒内有 10 次写操作
save 60 10000 # 60 秒内有 10000 次写操作
# 手动触发
SAVE # 阻塞主进程(不推荐)
BGSAVE # 后台 fork 子进程执行

执行流程

1. Redis 主进程 fork 子进程
2. 子进程将内存数据写入临时 RDB 文件
3. 写入完成后,原子替换旧 RDB 文件
4. 子进程退出

特点

  • 适合备份:二进制格式,压缩率高
  • 恢复快速:直接加载到内存
  • 可能丢数据:两次快照之间的数据会丢失

3.3 AOF 持久化是如何工作的?#

AOF(Append Only File)记录所有写命令:

工作流程

写命令 → 写入 AOF 缓冲区 → 根据策略同步到磁盘

同步策略

策略说明性能安全性
always每次写命令都同步最高
everysec每秒同步一次(默认)
no由操作系统决定

AOF 重写

随着写操作增加,AOF 文件会越来越大。Redis 提供重写机制:

# 手动触发重写
BGREWRITEAOF
# 自动触发配置
auto-aof-rewrite-percentage 100 # 文件比上次重写后增长 100%
auto-aof-rewrite-min-size 64mb # 文件至少 64MB

重写原理:fork 子进程,遍历当前内存数据,生成最简命令序列。

3.4 混合持久化是什么?#

Redis 4.0 引入混合持久化,结合 RDB 和 AOF 的优点:

# 开启混合持久化
aof-use-rdb-preamble yes

AOF 文件结构

┌─────────────────────────────────────────┐
│ RDB 格式数据(快照) │
├─────────────────────────────────────────┤
│ AOF 格式数据(增量命令) │
└─────────────────────────────────────────┘

优势

  1. 快速恢复:先加载 RDB 快照
  2. 数据完整:追加 AOF 增量命令
  3. 文件紧凑:RDB 压缩率高

四、集群与高可用#

4.1 Redis 主从复制是如何工作的?#

主从复制实现数据冗余和读写分离:

复制流程

1. 从节点发送 PSYNC 命令
2. 主节点执行 BGSAVE 生成 RDB
3. 主节点发送 RDB 给从节点
4. 从节点加载 RDB
5. 主节点发送增量写命令(replication buffer)

复制类型

类型说明
全量复制首次同步,传输完整 RDB
部分复制网络断开后重连,传输缺失的命令

关键配置

replicaof <master-ip> <master-port> # 从节点配置
masterauth <password> # 主节点密码
replica-read-only yes # 从节点只读

4.2 Redis Sentinel 是什么?#

Sentinel(哨兵)是 Redis 的高可用解决方案:

核心功能

  1. 监控:检测主从节点是否正常
  2. 通知:通知管理员或其他应用
  3. 自动故障转移:主节点故障时选举新主节点

工作原理

┌─────────────┐
│ Sentinel 1 │──┐
├─────────────┤ │ ┌─────────┐
│ Sentinel 2 │──┼───→│ Master │
├─────────────┤ │ └─────────┘
│ Sentinel 3 │──┘ │
└─────────────┘ ┌────┴────┐
↓ ↓
┌─────────┐ ┌─────────┐
│ Slave 1 │ │ Slave 2 │
└─────────┘ └─────────┘

故障转移流程

  1. 多数 Sentinel 认为主观下线 → 客观下线
  2. Sentinel 集群选举领导者
  3. 从节点中选举新主节点
  4. 其他从节点复制新主节点
  5. 客户端更新连接

4.3 Redis Cluster 是什么?#

Redis Cluster 是 Redis 的分布式解决方案,支持数据分片:

架构特点

16384 个槽位均匀分布在多个节点
Node A: 0-5460 槽位
Node B: 5461-10922 槽位
Node C: 10923-16383 槽位
每个节点负责一部分槽位

数据分片

# 客户端请求
SET user:1001 "data"
CRC16("user:1001") % 16384 = 槽位号
路由到对应节点

特点

  1. 去中心化:无中心节点,每个节点都知道完整拓扑
  2. 自动分片:数据自动分布到多个节点
  3. 高可用:每个主节点可以有多个从节点
  4. 自动故障转移:主节点故障时从节点升级

4.4 主从复制、Sentinel、Cluster 如何选择?#

方案数据量特点适用场景
主从复制单机可承载数据冗余、读写分离读多写少
Sentinel单机可承载高可用、自动故障转移需要高可用的场景
Cluster超出单机分布式、自动分片大数据量、高并发

五、缓存问题#

5.1 什么是缓存穿透?如何解决?#

缓存穿透:查询不存在的数据,请求穿过缓存直接打到数据库。

客户端 → Redis(不存在)→ 数据库(也不存在)→ 返回空
每次都穿透

解决方案

1. 布隆过滤器(Bloom Filter)

// 初始化时预热所有存在的 key
BloomFilter<String> filter = BloomFilter.create(Funnels.stringFunnel(), 1000000);
for (String key : allKeys) {
filter.put(key);
}
// 查询时先判断
if (!filter.mightContain(key)) {
return null; // 一定不存在,直接返回
}
// 可能存在,继续查询缓存和数据库

2. 缓存空值

# 查询数据库为空时,也缓存一个空值
SET nonexistent_key "" EX 60 # 设置较短过期时间

5.2 什么是缓存击穿?如何解决?#

缓存击穿:热点 key 在某一时刻过期,大量请求瞬间打到数据库。

热点 key 过期
并发请求 → 缓存未命中 → 全部打到数据库

解决方案

1. 互斥锁

public String getWithLock(String key) {
String value = redis.get(key);
if (value != null) {
return value;
}
// 尝试获取锁
String lockKey = "lock:" + key;
if (redis.setnx(lockKey, "1", 10)) { // 10 秒超时
try {
// 再次检查缓存(双检)
value = redis.get(key);
if (value == null) {
value = db.query(key); // 查数据库
redis.set(key, value, 3600); // 写入缓存
}
} finally {
redis.del(lockKey); // 释放锁
}
} else {
// 等待后重试
Thread.sleep(50);
return getWithLock(key);
}
return value;
}

2. 永不过期(逻辑过期)

# 物理上不设置过期时间
SET hot_key "data"
# 数据中包含逻辑过期时间
{
"data": "...",
"expireTime": 1710000000 # 逻辑过期时间
}
# 后台线程定期检查并异步更新

5.3 什么是缓存雪崩?如何解决?#

缓存雪崩:大量 key 同时过期,或 Redis 宕机导致大量请求打到数据库。

Redis 宕机或大量 key 同时过期
请求 → 缓存失效 → 全部打到数据库 → 数据库崩溃

解决方案

1. 过期时间随机化

// 基础过期时间 + 随机偏移
int baseExpire = 3600;
int randomExpire = baseExpire + new Random().nextInt(300); // 1 小时 + 0~5 分钟
redis.set(key, value, randomExpire);

2. 构建高可用集群

Redis Cluster 或 Sentinel 模式
主节点故障 → 自动切换到从节点

3. 限流降级

// 使用 Sentinel 或 Hystrix 进行熔断
@SentinelResource(value = "getData", fallback = "getDataFallback")
public String getData(String key) {
return cacheService.get(key);
}
public String getDataFallback(String key) {
return "系统繁忙,请稍后重试"; // 降级返回
}

4. 缓存预热

// 系统启动时预热热点数据
@PostConstruct
public void warmUp() {
List<String> hotKeys = getHotKeys();
for (String key : hotKeys) {
String value = db.query(key);
redis.set(key, value, getRandomExpire());
}
}

六、分布式锁#

6.1 Redis 如何实现分布式锁?#

基本实现

# 加锁
SET lock:resource unique_value NX PX 30000 # NX 不存在才设置,PX 过期时间
# 解锁(Lua 脚本保证原子性)
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end

要点

  1. NX:只有 key 不存在时才设置,保证互斥
  2. PX:设置过期时间,防止死锁
  3. 唯一值:使用 UUID 或线程 ID,防止误删其他客户端的锁
  4. Lua 解锁:检查和删除必须是原子操作

问题

单节点 Redis 的锁在主从切换时可能不安全。如果客户端 A 加锁后,主节点宕机,从节点还没同步到锁信息就被提升为主节点,客户端 B 也能加锁成功。

6.2 什么是 RedLock?#

RedLock(Redlock)是 Redis 作者提出的分布式锁算法,解决单节点故障问题:

算法步骤

  1. 获取当前时间戳
  2. 按顺序向 N 个 Redis 节点请求加锁
  3. 计算加锁耗时,只有大多数节点加锁成功且耗时小于锁过期时间才算成功
  4. 加锁失败则向所有节点发送解锁请求
┌─────────┐
│ Redis 1 │ ← 加锁成功
├─────────┤
│ Redis 2 │ ← 加锁成功
├─────────┤
│ Redis 3 │ ← 加锁失败
├─────────┤
│ Redis 4 │ ← 加锁成功
├─────────┤
│ Redis 5 │ ← 加锁成功
└─────────┘
5 个节点,4 个成功 → 获得锁

代码示例

RedissonClient client1 = Redisson.create(config1);
RedissonClient client2 = Redisson.create(config2);
RedissonClient client3 = Redisson.create(config3);
RedissonMultiLock lock = new RedissonMultiLock(client1, client2, client3);
lock.lock();
try {
// 执行业务逻辑
} finally {
lock.unlock();
}

6.3 Redisson 是什么?#

Redisson 是 Redis 的 Java 客户端,提供了丰富的分布式对象和服务:

分布式锁实现

// 可重入锁
RLock lock = redisson.getLock("myLock");
lock.lock();
try {
// 业务逻辑
} finally {
lock.unlock();
}
// 公平锁
RLock fairLock = redisson.getFairLock("myFairLock");
// 读写锁
RReadWriteLock rwLock = redisson.getReadWriteLock("myRWLock");
rwLock.readLock().lock();
rwLock.writeLock().lock();
// 联锁(多锁组合)
RLock lock1 = redisson.getLock("lock1");
RLock lock2 = redisson.getLock("lock2");
RedissonMultiLock multiLock = new RedissonMultiLock(lock1, lock2);

看门狗机制

Redisson 提供自动续期功能,防止业务执行时间超过锁过期时间:

// 默认 30 秒过期,看门狗每 10 秒续期
RLock lock = redisson.getLock("myLock");
lock.lock(); // 自动续期
// 指定过期时间(不启用看门狗)
lock.lock(10, TimeUnit.SECONDS);

七、内存管理#

7.1 Redis 的内存淘汰策略有哪些?#

当内存使用达到上限(maxmemory)时,Redis 提供多种淘汰策略:

策略说明
noeviction不淘汰,写入报错(默认)
allkeys-lru从所有 key 中淘汰最近最少使用
allkeys-lfu从所有 key 中淘汰最不常用
allkeys-random从所有 key 中随机淘汰
volatile-lru从设置了过期时间的 key 中淘汰 LRU
volatile-lfu从设置了过期时间的 key 中淘汰 LFU
volatile-random从设置了过期时间的 key 中随机淘汰
volatile-ttl淘汰即将过期的 key

配置方式

maxmemory 4gb # 设置最大内存
maxmemory-policy allkeys-lru # 设置淘汰策略

7.2 LRU 和 LFU 有什么区别?#

LRU(Least Recently Used)

淘汰最近最少使用的数据。基于时间局部性原理。

访问顺序:A → B → C → A → D
内存满时淘汰:B(最久未被访问)
LRU 队列:
[最近访问] D → A → C → B [最久访问]

LFU(Least Frequently Used)

淘汰访问频率最低的数据。基于频率统计。

访问统计:A(3次) B(1次) C(2次) D(2次)
内存满时淘汰:B(访问次数最少)

Redis 实现

Redis 4.0 引入 LFU,使用近似算法:

# 配置 LFU 相关参数
lfu-log-factor 10 # 计数器增长因子
lfu-decay-time 1 # 衰减时间(分钟)

7.3 如何优化 Redis 内存使用?#

1. 选择合适的数据结构

# 小对象使用 Hash 优化
# 原本:多个 String
SET user:1001:name "Alice"
SET user:1001:age "25"
# 优化:单个 Hash
HSET user:1001 name "Alice" age "25"

2. 使用压缩编码

# 配置压缩阈值
hash-max-listpack-entries 512
hash-max-listpack-value 64
zset-max-listpack-entries 128
zset-max-listpack-value 64

3. 设置合理的过期时间

# 避免数据堆积
SET key value EX 3600 # 1 小时过期

4. 避免大 Key

# 检查大 Key
redis-cli --bigkeys
# 拆分大 Key
# 原:一个大 Hash
HSET large:hash field1 value1 field2 value2 ... field1000 value1000
# 拆分:多个小 Hash
HSET large:hash:1 field1 value1
HSET large:hash:2 field2 value2

八、参考#


参考#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

Redis 面试题
https://blog.souloss.com/posts/interview/redis/
作者
Souloss
发布于
2024-04-27
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时