Redis 核心技术 #

高性能内存数据库,缓存系统核心知识点,面试高频


📋 目录 #


数据类型 #

五种基础数据类型 #

类型 数据结构 使用场景
String 简单动态字符串 SDS 缓存、计数器、分布式ID、Session
List 快速链表 消息队列、最新列表、排行榜
Hash 字典 + 压缩列表 对象缓存(用户信息、商品信息)
Set 哈希表 去重、共同好友、标签
ZSet 跳跃表 + 哈希表 排行榜、带权重队列、分页

特殊数据类型 #

类型 作用 使用场景
Bitmap 位图统计 用户签到、活跃用户统计、布隆过滤器
HyperLogLog 基数估计 独立IP统计,不保存元素
Geo 地理位置 附近的人、距离计算
Stream 消息队列 多生产者消费者消息流
JSON JSON文档 Redis Stack原生支持JSON
Search 全文搜索 Redis Stack全文搜索

编码优化 #

Redis 根据数据大小自动选择不同编码节省内存:

场景 编码
小整数 int 编码直接存储
短字符串 embstr 连续内存分配
长字符串 raw 分离分配
Hash/ZSet 元素少 压缩列表 ziplist
Hash/ZSet 元素多 字典/跳跃表

跳跃表(SkipList) #

跳表是一种有序链表+分层索引的数据结构,支持 O(log n) 时间复杂度的查找、插入和删除操作,是 Redis SortedSet (ZSet) 的核心数据结构之一。

跳表结构示意图:

Level 3

10

30

60

Level 2

20

50

Level 1

40

70

Level 0

最底层(Level 0)包含所有元素,上层是下层的稀疏索引。通过从顶层开始跳跃查找,快速缩小搜索范围。

查找过程 #

  1. 从最高层的头节点开始查找
  2. 当前节点值 小于 目标值,继续在当前层向右走
  3. 当前节点值 大于等于 目标值,下降到下一层继续查找
  4. 重复直到找到目标或到达最底层末尾

这种方法类似二分查找,但用空间换时间通过索引跳跃前进。

插入过程 #

  1. 从顶层开始查找,找到插入位置的前驱节点
  2. 随机化决定新节点的层数(关键:概率性分层,不保证严格平衡)
  3. 在每一层插入新节点到前驱节点和后继节点之间

随机分层概率: Redis 中每层晋升概率为 p = 0.25,即每提升一层概率是 1/4。期望总层数为 log_{1/p} n

删除过程 #

  1. 查找找到目标节点
  2. 在每一层中删除该节点,然后释放内存
  3. 如果删除后最高层没有节点,减少跳表层数

复杂度分析 #

操作 平均时间 最坏时间 空间
查找 O(log n) O(n) O(n)
插入 O(log n) O(n) O(n)
删除 O(log n) O(n) O(n)

为什么最坏是 O(n) 但实际很好用?因为随机分层概率保证了最坏情况极少出现,实际运行表现非常稳定。

空间复杂度: 期望存储 n/(1-p) = 4n/3 个节点指针。对于 Redis 场景内存开销完全可以接受。

为什么 ZSet 用跳表不用平衡树? #

对比项 跳跃表 平衡树(AVL/红黑树)
实现复杂度 简单递归/迭代,代码短 旋转操作复杂,代码长
范围查询 天然支持,找到起点后遍历链表 需要中序遍历,更麻烦
插入删除 只需修改相邻节点指针,不需要旋转重构 需要旋转调整树结构
CPU 开销 概率平衡,不需要频繁调整 严格平衡,旋转占用更多 CPU

Redis 使用跳表+哈希表组合实现 ZSet:

  • 哈希表:O(1) 根据成员查找分值
  • 跳跃表:按分值范围排序和范围查询

两者互补,兼顾单点查找和范围操作性能。


高性能原理 #

纯内存访问 #

  • 所有数据在内存,读写速度 10万+ QPS
  • 内存访问纳秒级,比磁盘快几个数量级

单线程模型 #

为什么单线程还这么快?

  1. 纯内存操作,瓶颈在内存不是CPU
  2. 避免多线程上下文切换开销
  3. 避免锁竞争,代码简单
  4. IO多路复用模型,非阻塞

Redis单线程

主线程

处理命令

Persist异步

内存读写

返回结果

IO多路复用 #

Redis 使用 epoll/kqueue/select 多路复用模型:

  • 单线程监听多个Socket
  • 事件驱动方式处理命令
  • 避免阻塞等待

网络模型 #

  • Redis 6 之前:单线程处理命令
  • Redis 6+:多线程IO(IO读/写多线程,命令执行还是单线程)
  • 提升网络IO处理能力,充分利用多核CPU

高可用方案 #

主从复制 (Master-Slave) #

Master 主节点

Slave 从节点1

Slave 从节点2

Slave 从节点3

作用:

  • 读写分离:主写从读,分担压力
  • 数据备份:防止单点丢失数据
  • 扩容:增加读吞吐量

同步过程:

  1. 从节点连接主节点,发送 SYNC 命令
  2. 主节点执行 BGSAVE 生成RDB文件,发送给从节点
  3. 从节点加载RDB,完成初始化
  4. 增量同步:主节点把后续命令发送给从节点

哨兵 (Sentinel) #

自动故障发现和故障转移 ⭐⭐⭐⭐⭐

作用:

  • 监控:不断检查主从节点是否健康
  • 通知:提醒管理员节点故障
  • 故障转移:主节点下线后,选举从节点升为主节点
  • 配置提供者:客户端获取当前主节点地址

哨兵集群:

故障down

Sentinel 1

Master

Sentinel 2

Sentinel 3

选举新Master

投票选举:

哨兵发现Master下线

哨兵间投票 Raft算法

多数同意?

继续监控

选出Leader哨兵

选择最优Slave

升为新Master

其他Slave同步新Master

  • 奇数个哨兵节点,Raft算法选举
  • 多数原则,防止脑裂

读写分离配置 #

操作 节点
写操作 主节点
读操作 从节点
过期键删除 惰性删除 + 定期删除

持久化 #

RDB (Redis Database) #

原理: 定时把内存快照保存到磁盘

过程:

bgsave命令

fork子进程

子进程写RDB文件

替换旧RDB文件

  1. bgsave 命令调用 fork() 创建子进程
  2. 子进程把内存数据写入RDB文件
  3. 替换旧的RDB文件完成

优点:

  • 文件紧凑,体积小,恢复快
  • 适合备份

缺点:

  • 会丢最后一次持久化后的数据
  • fork() 子进程,大数据量会阻塞

AOF (Append Only File) #

原理: 追加写命令到日志文件

过程:

always

everysec

no

命令执行

追加到AOF缓冲区

刷盘策略

每命令刷盘

每秒刷盘

OS决定

写入AOF文件

  1. 命令执行成功后,追加到AOF缓冲区
  2. 根据刷盘策略定期刷盘到磁盘
  3. 重启时,重放AOF命令恢复数据

刷盘策略:

策略 说明 安全性 性能
always 每个命令都刷盘 最安全 最慢
everysec 每秒刷盘一次 丢1秒数据 平衡
no 操作系统决定 依赖OS 最快

AOF重写:

  • AOF越来越大,自动重写压缩
  • 保留最小命令集合,缩小文件体积
  • bgrewriteaof 命令,fork子进程执行

优点:

  • 安全性高,最多丢1秒数据
  • 文件可读性好,可以手动修改

缺点:

  • 文件比RDB大
  • 恢复速度比RDB慢

混合持久化 (推荐) #

  • Redis 4.0+ 支持
  • RDB快照 + AOF增量
  • 兼顾恢复速度和安全性

配置:

aof-use-rdb-preamble yes

集群方案 #

哨兵模式 #

  • 解决高可用问题,自动故障转移
  • 数据还是全量存储在主节点
  • 容量受限于单机内存

集群分片 (Redis Cluster) #

分片存储,水平扩展 ⭐⭐⭐⭐⭐

原理:

  • 总共 16384 个槽 (slot)
  • 每个节点负责一部分槽
  • key 根据 CRC16(key) % 16384 计算槽位置

key1

CRC16 % 16384

Slot 1-5461

Node 1

Slot 5462-10922

Node 2

Slot 10923-16383

Node 3

特点:

  • 无中心节点
  • 自动分片,自动重平衡
  • 主节点写,从节点读
  • 客户端直接连接节点,不需要proxy

节点:

  • 每个主节点处理一部分槽
  • 每个主节点可以有多个从节点
  • 主节点故障,从节点升级为主节点

客户端分片 #

  • 客户端自己做分片,一致性哈希
  • 优点:简单,没有中心
  • 缺点:扩容麻烦,不好重平衡

代理分片 (Twemproxy/Codis) #

  • 代理层做分片
  • 优点:客户端透明
  • 缺点:多一层转发,性能损耗

分布式锁 #

基于Redis实现分布式锁 ⭐⭐⭐⭐⭐


一、核心需求 #

需求 说明
互斥性 同一时刻只有一个客户端持有锁
防死锁 客户端崩溃后锁能自动释放
安全性 只能释放自己持有的锁
容错性 部分Redis节点故障仍能工作
可重入 同一客户端可以多次获取同一把锁
高性能 获取/释放锁开销小

二、方案演进 #

方案1:基础版(SET NX PX) #

# 获取锁
SET lock:resource random_value NX PX 30000

问题:

  • ❌ 业务执行时间超过30秒,锁过期了,其他客户端获取到锁
  • ❌ 无法续期
  • ❌ 不可重入

方案2:基础版 + 释放锁(Lua脚本) #

-- 获取锁
SET lock:resource uuid NX PX 30000

-- 释放锁(Lua脚本保证原子性)
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end

改进:

  • ✅ 防止误删别人的锁

仍有问题:

  • ❌ 锁过期业务未完成
  • ❌ 不可重入

方案3:完整版(看门狗续期 + 可重入) #

数据结构:

  • Key: lock:{resource}
  • Value: Hash 结构 {owner: uuid, holdCount: 1}
  • 过期时间: 30秒

获取锁流程:

尝试获取锁

锁存在?

设置锁, holdCount=1, PX 30000

owner==自己?

holdCount+1, 重置过期时间

返回失败

启动看门狗

每10秒续期一次

Lua脚本 - 获取锁:

local key = KEYS[1]
local uuid = ARGV[1]
local ttl = ARGV[2]

-- 锁不存在,直接获取
if redis.call('exists', key) == 0 then
    redis.call('hset', key, 'owner', uuid, 'holdCount', 1)
    redis.call('pexpire', key, ttl)
    return 1
end

-- 锁存在,检查是否是自己的
if redis.call('hexists', key, 'owner') == 1 then
    if redis.call('hget', key, 'owner') == uuid then
        redis.call('hincrby', key, 'holdCount', 1)
        redis.call('pexpire', key, ttl)
        return 1
    end
end

return 0

Lua脚本 - 释放锁:

local key = KEYS[1]
local uuid = ARGV[1]

-- 锁不存在
if redis.call('exists', key) == 0 then
    return 0
end

-- 不是自己的锁
if redis.call('hget', key, 'owner') ~= uuid then
    return -1
end

-- holdCount减1
local count = redis.call('hincrby', key, 'holdCount', -1)

-- 如果count > 0,还有重入次数
if count > 0 then
    return count
end

-- count <= 0,删除锁
redis.call('del', key)
return 1

看门狗续期:

local key = KEYS[1]
local uuid = ARGV[1]
local ttl = ARGV[2]

if redis.call('hexists', key, 'owner') == 1 then
    if redis.call('hget', key, 'owner') == uuid then
        redis.call('pexpire', key, ttl)
        return 1
    end
end

return 0

方案4:RedLock 红锁(多节点一致性) #

适用场景: 对一致性要求极高,不能容忍单节点故障

算法:

开始

获取当前时间

依次向N个节点请求锁

统计成功数量

成功 >= 半数+1?

向所有节点释放锁

总耗时 < 锁超时?

获取锁成功

启动看门狗续期

N个节点配置: 建议5个(奇数,防止脑裂)


三、最佳实践 #

1. 锁粒度控制 #

场景 推荐做法
粗粒度锁 lock:order → 整个订单模块
细粒度锁 lock:order:123 → 只锁特定订单

推荐:细粒度锁,减少锁竞争


2. 超时时间设置 #

业务执行时间 锁超时时间 看门狗续期间隔
1秒 10-30秒 锁超时/3
5秒 30-60秒 锁超时/3
30秒 60-120秒 锁超时/3

3. 重试策略 #

// 指数退避重试
int retryCount = 0;
int maxRetries = 3;
long baseDelayMs = 100;

while (retryCount < maxRetries) {
    if (tryLock()) {
        return true;
    }
    long delay = baseDelayMs * (1 << retryCount); // 100ms, 200ms, 400ms
    Thread.sleep(delay + random(0, 100)); // 加随机防止惊群
    retryCount++;
}

4. 生产级实现对比 #

特性 自己实现 Redisson Curator
可重入 ❌/✅
看门狗续期 ❌/✅
RedLock ❌/✅
联锁
红锁
读写锁
信号量
闭锁

推荐:生产环境直接用 Redisson


5. Redisson 使用示例 #

// 1. 配置
Config config = new Config();
config.useSingleServer()
    .setAddress("redis://127.0.0.1:6379");

RedissonClient client = Redisson.create(config);

// 2. 获取可重入锁
RLock lock = client.getLock("lock:order:123");

// 3. 加锁(自动续期)
lock.lock();
try {
    // 业务逻辑
} finally {
    lock.unlock();
}

// 4. 尝试加锁,带超时
boolean acquired = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (acquired) {
    try {
        // 业务逻辑
    } finally {
        lock.unlock();
    }
}

// 5. 红锁
RLock lock1 = client.getLock("lock1");
RLock lock2 = client.getLock("lock2");
RLock lock3 = client.getLock("lock3");
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
redLock.lock();

四、常见问题及解决方案 #

问题 原因 解决方案
锁过期业务未完成 业务执行时间超过锁超时 看门狗自动续期
释放别人的锁 没判断锁owner 用uuid标识owner,Lua脚本原子释放
Redis主从切换丢锁 主节点set成功,还没同步到从,主挂了 RedLock多节点
惊群效应 锁释放,大量客户端同时争抢 随机退避重试
锁竞争激烈 锁粒度太粗 细粒度锁 + 分段锁

五、面试回答结构 #

面试官:用Redis实现分布式锁,讲一下设计方案

回答思路:

1. 先讲基础版:SET NX PX + Lua释放
   - NX保证互斥,PX防死锁
   - 为什么用random_value防止误删

2. 再讲进阶问题和解决方案:
   - 锁过期了业务没执行完 → 看门狗续期
   - 同一客户端需要多次获取 → 可重入(Hash结构)
   - 主从切换丢锁 → RedLock

3. 最后讲生产实践:
   - 推荐直接用Redisson,不要自己造轮子
   - 锁粒度、超时时间、重试策略

七、Redis 分布式锁的瓶颈与局限 #

面试高频追问:Redis 分布式锁有什么瓶颈/缺陷?⭐⭐⭐⭐⭐

1. 主从切换丢锁(一致性缺陷) #

核心问题: Redis 主从复制是异步的,锁写入 Master 后还没同步到 Slave,Master 就挂了,Slave 升为 Master 后锁丢失。

Client BSlaveMasterClient AClient BSlaveMasterClient AMaster 崩溃!锁还没同步到 Slave没有锁数据!两个客户端同时持有锁!互斥性被破坏!SET lock:123 uuid NX PX 30sOK(获取成功)同步中断升级为 MasterSET lock:123 uuid2 NX PX 30sOK(获取成功)

严重程度: ⭐⭐⭐⭐⭐ 这是 Redis 分布式锁最大的硬伤

缓解方案: RedLock(但 RedLock 本身也有争议,见下文)


2. RedLock 算法的争议 #

Martin Kleppmann(《DERTA》作者)2016 年发表著名文章质疑 RedLock:

问题 说明
时钟跳跃 5 个 Redis 节点的时钟不可能完全同步,NTP 同步可能产生时间跳跃
GC 停顿 客户端 JVM 发生 Full GC(STW),锁到期了但客户端还以为持有锁
网络延迟 获取锁的网络请求延迟不确定,计算锁有效时间不可靠
没有 fencing token 无法检测锁是否已经失效,无法阻止过期锁继续操作共享资源

Antirez(Redis 作者)的回应:

  • 假设时钟漂移有上限(通常 <1%)
  • 自动续期可以解决 GC 停顿问题
  • 但在极端场景下(长时间 GC + 网络分区),仍然可能出问题

结论: RedLock 提高了可靠性,但无法达到真正的强一致性锁,本质是因为 Redis 是 AP 系统,不是 CP 系统。


3. AP vs CP 的根本矛盾 #

CAP理论

牺牲部分

Consistency 一致性

Availability 可用性

Partition 分区容错性

Redis

ZooKeeper/etcd

维度 Redis(AP) ZooKeeper/etcd(CP)
一致性 最终一致,可能丢锁 强一致,不会丢锁
可用性 高,节点故障快速切换 牺牲部分可用性来保证一致
性能 极高(10万+ QPS) 较低(几千 QPS)
适用场景 对一致性要求不极端 对一致性要求极高

4. 性能瓶颈 #

瓶颈 说明
网络 RTT 每次加锁/释放至少一次网络往返(~1ms),高并发下成为瓶颈
单 Key 热点 所有客户端竞争同一个 key,在 Cluster 中集中到一个节点
串行化 Redis 单线程执行 Lua 脚本,锁竞争激烈时吞吐量下降
看门狗开销 续期需要定时任务,增加 Redis 负担

5. 锁公平性问题 #

问题 说明
无 FIFO 保证 先请求的客户端不一定先拿到锁
饥饿风险 某个客户端可能反复抢不到锁
重试风暴 大量客户端同时重试,浪费 CPU 和网络资源

缓解: Redisson 的 RedissonLock 通过 Pub/Sub 实现公平锁(监听锁释放事件,按序通知),但增加了复杂度。


6. 长时间 GC 导致的锁失效 #

Client B业务线程RedisClient(JVM)Client B业务线程RedisClient(JVM)⚠️ Full GC 触发STW 10秒锁 30s 到期,自动释放GC 结束,线程恢复两个客户端同时操作!数据不一致!获取锁成功执行业务逻辑锁过期删除获取锁成功继续执行业务(以为还持有锁)

缓解:

  • 使用 fencing token(每次获取锁附带递增序号,服务端拒绝过期序号的写入)
  • 但 Redis 本身不提供 fencing token,需要额外实现

7. 集群模式下的限制 #

限制 说明
Lua 脚本不支持跨 slot 多 key 操作(如联锁)必须在同一个 slot
RedLock 配置复杂 需要 5 个独立 Redis 实例,运维成本高
不能跨集群加锁 锁只能在一个 Redis 实例/集群内生效

8. 面试回答模板 #

面试官:Redis 分布式锁有什么瓶颈?

回答思路(由浅入深):

1. 主从切换丢锁(最直接的瓶颈)
   - 异步复制导致主挂从升,锁丢失
   - 两个客户端可能同时持有同一把锁

2. RedLock 的局限
   - 虽然多节点投票,但 Martin Kleppmann 证明了
     时钟漂移、GC 停顿、网络延迟都可能导致失效
   - 没有 fencing token 机制,无法真正防止过期锁继续写数据

3. AP vs CP 本质矛盾
   - Redis 是 AP 系统,设计目标是高可用不是强一致
   - 对一致性要求极高的场景(如金融扣款),应该用 ZK/etcd 这种 CP 系统

4. 性能瓶颈
   - 网络 RTT、单 Key 热点、串行化执行
   - 高竞争场景下吞吐量受限

5. 公平性和工程问题
   - 无 FIFO 保证,可能饥饿
   - 长时间 GC 导致锁失效
   - 集群模式下 Lua 脚本跨 slot 限制

6. 实际选择
   - 大多数业务场景(如防止重复提交)→ Redis 锁够用,性能最好
   - 对一致性要求极高 → ZooKeeper/etcd
   - 两者结合 → Redis 做快速锁,ZK 做兜底确认

9. Redis 锁 vs ZooKeeper 锁 vs etcd 锁 #

维度 Redis ZooKeeper etcd
一致性 弱(AP) 强(CP) 强(CP)
性能 最高 较低 中等
锁丢失风险 极低 极低
公平锁 需额外实现 天然支持(临时有序节点) 支持
会话超时 靠 TTL + 看门狗 靠 Session(心跳自动续期) 靠 Lease(心跳续期)
运维成本 中等
适用场景 高性能、容忍小概率丢锁 强一致、金融级 云原生、K8s 生态

八、完整流程图 #

WatchDogRedisClientWatchDogRedisClientloop[每10秒]SET lock:123 uuid NX PX 30sOK (获取成功)启动看门狗PEXPIRE lock:123 30s执行业务停止看门狗EVAL Lua脚本 1 lock:123 uuid1 (释放成功)

缓存最佳实践 #

缓存穿透 #

问题: 查询不存在的数据,每次都穿透缓存打到数据库

不存在

可能存在

命中

未命中

不存在

存在

请求

布隆过滤器

直接拒绝

查缓存

返回缓存数据

查数据库

缓存空值, 短TTL

写入缓存

返回空

返回数据

解决方案:

  1. 缓存空值:不存在也缓存一个空值,设置短过期时间
  2. 布隆过滤器:把所有存在的key放布隆过滤器,不存在直接过滤

缓存击穿 #

问题: 热点key过期,大量并发直接打到数据库

未过期

已过期

成功

失败

大量并发请求

缓存是否过期

返回缓存数据

获取互斥锁

查询数据库

等待重试

写入缓存

返回数据

解决方案:

  1. 互斥锁:只有一个线程去查数据库,重建缓存
  2. 永不过期:热点key不设置过期,异步后台更新
  3. 提前预热:上线前提前把热点数据加载进缓存

缓存雪崩 #

问题: 大量key同时过期,或者缓存宕机,大量请求打到数据库

解决方案:

  1. 随机过期时间:不让key同时过期
  2. 多级缓存:本地缓存 + 分布式缓存
  3. 限流降级:缓存宕机后限流,或者返回默认值
  4. 缓存高可用:哨兵或集群部署

Key 设计最佳实践 #

  • 前缀清晰product:100:info业务:id:含义
  • 避免太长:太长浪费内存,影响效率
  • 避免过多前缀:统一风格

热点key发现 #

  • Redis 4.0+ INFO statskeyspace_hits
  • redis-cli --hotkeys 找出热点key
  • 热点key分片打散,缓解单key流量倾斜

常见问题 #

缓存和数据库一致性问题 #

推荐方案流程:

更新请求

更新数据库

删除缓存

延迟双删?

等待几百ms

再次删除缓存

完成

方案对比:

方案三 最佳

更DB

binlog

Canal消费

异步更新缓存

方案二 推荐

更DB

删缓存

延迟双删

方案一 不推荐

删缓存

更DB

并发不一致

  1. 先删缓存,再更数据库 → 容易不一致,并发问题
  2. 先更数据库,再删缓存 → 推荐方案,延迟双删解决最终一致性
  3. 订阅binlog异步更新缓存 → 阿里 Canal方案,最可靠

Redis 过期键删除策略 #

  • 惰性删除:访问key的时候检查是否过期,过期删除
  • 定期删除:每隔一段时间随机抽查一批key,删除过期
  • 不会一次性删除所有过期key,平衡CPU和内存

Redis 内存淘汰策略 #

策略 说明
volatile-lru 淘汰设置了过期的LRU
allkeys-lru 淘汰所有key中的LRU(推荐)
volatile-random 随机淘汰设置了过期
allkeys-random 随机淘汰所有
volatile-ttl 淘汰更早过期的
noeviction 不淘汰,写请求返回错误

生产推荐: allkeys-lru

为什么Redis 是单线程还能比Memcached快? #

  • 数据结构更优(跳跃表 vs 链表)
  • 简洁的代码逻辑,更少锁
  • 多路复用IO模型高效
  • 字符串SDS优化,减少内存分配

面试题汇总 #

基础 #

  1. Redis 有哪些数据类型?各有什么使用场景?
  2. Redis 为什么这么快?
  3. RDB和AOF区别?各自优缺点?
  4. 什么是缓存穿透/击穿/雪崩?怎么解决?

架构 #

  1. 主从复制过程?
  2. 哨兵原理?怎么选举新主节点?
  3. Redis Cluster 槽为什么是16384个?
  4. Redis Cluster 怎么路由key?

实践 #

  1. 基于Redis实现分布式锁?原子性怎么保证?
  2. 缓存和数据库一致性怎么保证?
  3. Redis 内存淘汰策略有哪些?生产选哪个?
  4. 过期键怎么删除?

进阶 #

  1. Redis 6 多线程是什么意思?为什么要用多线程?
  2. 什么是RedLock?解决什么问题?
  3. 跳跃表结构是什么?为什么ZSet用跳跃表不用平衡树?
  4. 如何解决Redis热点key问题?

面试题答案详解 #

基础 #

  1. Redis 有哪些数据类型?各有什么使用场景?

答案:

类型 数据结构 使用场景
String 简单动态字符串SDS 缓存、计数器、分布式ID、Session
List 快速链表 消息队列、最新列表、排行榜
Hash 字典+压缩列表 对象缓存(用户信息、商品信息)
Set 哈希表 去重、共同好友、标签
ZSet 跳跃表+哈希表 排行榜、带权重队列、分页
Bitmap 位图 用户签到、活跃用户、布隆过滤器
HyperLogLog 基数估计 独立IP统计
Geo 地理位置 附近的人、距离计算
Stream 消息队列 多生产者消费者

  1. Redis 为什么这么快?

答案:

原因 说明
纯内存 数据在内存,纳秒级访问
单线程 避免上下文切换和锁竞争
IO多路复用 epoll/kqueue模型,非阻塞
高效数据结构 跳跃表、SDS等优化
Redis 6+ 多线程IO,提升网络处理

性能: 10万+ QPS


  1. RDB和AOF区别?各自优缺点?

答案:

对比项 RDB AOF
原理 内存快照 追加写命令
文件大小 小,紧凑 大,可重写压缩
恢复速度
数据安全 丢最后一次快照 最多丢1秒(everysec)
适用场景 备份、灾难恢复 数据持久化

推荐方案: 混合持久化(Redis 4.0+)

aof-use-rdb-preamble yes

  1. 什么是缓存穿透/击穿/雪崩?怎么解决?

答案:

问题 描述 解决方案
穿透 查询不存在的数据,直接打DB 缓存空值、布隆过滤器
击穿 热点key过期,大量并发打DB 互斥锁、永不过期、预热
雪崩 大量key同时过期/缓存宕机 随机过期、多级缓存、限流

架构 #

  1. 主从复制过程?

答案:

全量同步:

Slave发SYNC

Master执行BGSAVE

生成RDB文件

发送RDB给Slave

Slave加载RDB

初始化完成

  1. Slave发送SYNC命令
  2. Master执行BGSAVE生成RDB
  3. Master发送RDB给Slave
  4. Slave加载RDB初始化

增量同步:

  • Master把后续命令发给Slave
  • 命令传播,保持同步

  1. 哨兵原理?怎么选举新主节点?

答案:

哨兵作用:

  • 监控:检查节点健康
  • 通知:告警
  • 故障转移:自动选主
  • 配置提供:客户端发现Master

选举流程:

发现Master下线

Raft投票选举

选出Leader哨兵

选择最优Slave

升为新Master

其他Slave重定向到新Master

  1. 哨兵发现Master下线
  2. 哨兵投票(Raft算法)
  3. 选出Leader哨兵
  4. 选择最优Slave升为Master
  5. 其他Slave同步新Master

关键点: 奇数个哨兵,多数原则


  1. Redis Cluster 槽为什么是16384个?

答案:

原因:

  • 16384 = 2^14,方便哈希计算
  • 心跳包中用bitmap存储槽信息,16384 bits = 2KB,适中
  • 太多浪费,太少不够分片
  • 官方测试16384是最佳实践

  1. Redis Cluster 怎么路由key?

答案:

槽计算:

slot = CRC16(key) % 16384

路由:

  • 客户端直接连接对应节点
  • 重定向:MOVED/ASK响应
  • Smart客户端:缓存槽映射

实践 #

  1. 基于Redis实现分布式锁?原子性怎么保证?

答案:

获取锁:

SET lock:order:123 random_value NX EX 30

释放锁(Lua脚本保证原子):

if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end

关键点:

  • NX:不存在才设置
  • EX:过期防死锁
  • Lua:比较+删除原子操作
  • Redisson:看门狗自动续期

  1. 缓存和数据库一致性怎么保证?

答案:

方案对比:

方案 说明 推荐度
先删缓存,再更DB 并发问题多
先更DB,再删缓存 延迟双删 ⭐⭐⭐
订阅binlog更新 Canal,最可靠 ⭐⭐⭐⭐⭐

延迟双删:

1. 先删缓存
2. 更数据库
3. 睡一会(几百ms)
4. 再删缓存(二次删除)

  1. Redis 内存淘汰策略有哪些?生产选哪个?

答案:

策略 说明
allkeys-lru 淘汰所有key的LRU(推荐)
volatile-lru 淘汰过期key的LRU
allkeys-random 随机淘汰所有
volatile-random 随机淘汰过期
volatile-ttl 淘汰更早过期
noeviction 不淘汰,报错

生产推荐: allkeys-lru


  1. 过期键怎么删除?

答案:

两种策略:

  1. 惰性删除: 访问时检查,过期则删除
  2. 定期删除: 每隔一段时间随机抽查一批

平衡: 避免CPU消耗太多,也避免内存浪费


进阶 #

  1. Redis 6 多线程是什么意思?为什么要用多线程?

答案:

架构:

  • IO线程多线程: 网络读写用多线程
  • 命令执行单线程: 核心命令执行还是单线程

原因:

  • 网络IO成为瓶颈
  • 利用多核CPU处理网络
  • 命令执行单线程避免锁

  1. 什么是RedLock?解决什么问题?

答案:

RedLock红锁: 多Redis节点投票

算法:

  1. 在N个独立节点依次获取锁
  2. 超过半数成功,且总耗时<超时,才算成功
  3. 释放锁:所有节点都释放

解决: 单节点故障导致锁失效(高一致性场景)


  1. 跳跃表结构是什么?为什么ZSet用跳跃表不用平衡树?

答案:

跳跃表: 有序链表+分层索引,O(log n)

对比平衡树:

对比项 跳跃表 平衡树
实现 简单 复杂
范围查询 天然支持 需中序遍历
插入删除 只需改指针 需旋转
CPU开销

ZSet: 跳跃表+哈希表,兼顾范围查询和单点查找


  1. 如何解决Redis热点key问题?

答案:

方案:

  1. key分片: key1, key2, key3...分散到不同节点
  2. 本地缓存: JVM本地缓存热点数据
  3. 永不过期: 异步后台更新
  4. 互斥锁: 单线程重建缓存
  5. 发现热点: redis-cli --hotkeys

🔗 相关笔记 #


最后更新: 2026-05-13