Redis 核心技术
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 0)包含所有元素,上层是下层的稀疏索引。通过从顶层开始跳跃查找,快速缩小搜索范围。
查找过程 #
- 从最高层的头节点开始查找
- 当前节点值 小于 目标值,继续在当前层向右走
- 当前节点值 大于等于 目标值,下降到下一层继续查找
- 重复直到找到目标或到达最底层末尾
这种方法类似二分查找,但用空间换时间通过索引跳跃前进。
插入过程 #
- 从顶层开始查找,找到插入位置的前驱节点
- 随机化决定新节点的层数(关键:概率性分层,不保证严格平衡)
- 在每一层插入新节点到前驱节点和后继节点之间
随机分层概率: Redis 中每层晋升概率为 p = 0.25,即每提升一层概率是 1/4。期望总层数为 log_{1/p} n。
删除过程 #
- 查找找到目标节点
- 在每一层中删除该节点,然后释放内存
- 如果删除后最高层没有节点,减少跳表层数
复杂度分析 #
| 操作 | 平均时间 | 最坏时间 | 空间 |
|---|---|---|---|
| 查找 | 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
- 内存访问纳秒级,比磁盘快几个数量级
单线程模型 #
为什么单线程还这么快?
- 纯内存操作,瓶颈在内存不是CPU
- 避免多线程上下文切换开销
- 避免锁竞争,代码简单
- IO多路复用模型,非阻塞
IO多路复用 #
Redis 使用 epoll/kqueue/select 多路复用模型:
- 单线程监听多个Socket
- 事件驱动方式处理命令
- 避免阻塞等待
网络模型 #
- Redis 6 之前:单线程处理命令
- Redis 6+:多线程IO(IO读/写多线程,命令执行还是单线程)
- 提升网络IO处理能力,充分利用多核CPU
高可用方案 #
主从复制 (Master-Slave) #
作用:
- 读写分离:主写从读,分担压力
- 数据备份:防止单点丢失数据
- 扩容:增加读吞吐量
同步过程:
- 从节点连接主节点,发送
SYNC命令 - 主节点执行
BGSAVE生成RDB文件,发送给从节点 - 从节点加载RDB,完成初始化
- 增量同步:主节点把后续命令发送给从节点
哨兵 (Sentinel) #
自动故障发现和故障转移 ⭐⭐⭐⭐⭐
作用:
- 监控:不断检查主从节点是否健康
- 通知:提醒管理员节点故障
- 故障转移:主节点下线后,选举从节点升为主节点
- 配置提供者:客户端获取当前主节点地址
哨兵集群:
投票选举:
- 奇数个哨兵节点,Raft算法选举
- 多数原则,防止脑裂
读写分离配置 #
| 操作 | 节点 |
|---|---|
| 写操作 | 主节点 |
| 读操作 | 从节点 |
| 过期键删除 | 惰性删除 + 定期删除 |
持久化 #
RDB (Redis Database) #
原理: 定时把内存快照保存到磁盘
过程:
bgsave命令调用 fork() 创建子进程- 子进程把内存数据写入RDB文件
- 替换旧的RDB文件完成
优点:
- 文件紧凑,体积小,恢复快
- 适合备份
缺点:
- 会丢最后一次持久化后的数据
- fork() 子进程,大数据量会阻塞
AOF (Append Only File) #
原理: 追加写命令到日志文件
过程:
- 命令执行成功后,追加到AOF缓冲区
- 根据刷盘策略定期刷盘到磁盘
- 重启时,重放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计算槽位置
特点:
- 无中心节点
- 自动分片,自动重平衡
- 主节点写,从节点读
- 客户端直接连接节点,不需要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秒
获取锁流程:
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个节点配置: 建议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 后锁丢失。
严重程度: ⭐⭐⭐⭐⭐ 这是 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 的根本矛盾 #
| 维度 | 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 导致的锁失效 #
缓解:
- 使用 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 生态 |
八、完整流程图 #
缓存最佳实践 #
缓存穿透 #
问题: 查询不存在的数据,每次都穿透缓存打到数据库
解决方案:
- 缓存空值:不存在也缓存一个空值,设置短过期时间
- 布隆过滤器:把所有存在的key放布隆过滤器,不存在直接过滤
缓存击穿 #
问题: 热点key过期,大量并发直接打到数据库
解决方案:
- 互斥锁:只有一个线程去查数据库,重建缓存
- 永不过期:热点key不设置过期,异步后台更新
- 提前预热:上线前提前把热点数据加载进缓存
缓存雪崩 #
问题: 大量key同时过期,或者缓存宕机,大量请求打到数据库
解决方案:
- 随机过期时间:不让key同时过期
- 多级缓存:本地缓存 + 分布式缓存
- 限流降级:缓存宕机后限流,或者返回默认值
- 缓存高可用:哨兵或集群部署
Key 设计最佳实践 #
- 前缀清晰:
product:100:info→业务:id:含义 - 避免太长:太长浪费内存,影响效率
- 避免过多前缀:统一风格
热点key发现 #
- Redis 4.0+
INFO stats看keyspace_hits redis-cli --hotkeys找出热点key- 热点key分片打散,缓解单key流量倾斜
常见问题 #
缓存和数据库一致性问题 #
推荐方案流程:
方案对比:
- 先删缓存,再更数据库 → 容易不一致,并发问题
- 先更数据库,再删缓存 → 推荐方案,延迟双删解决最终一致性
- 订阅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优化,减少内存分配
面试题汇总 #
基础 #
- Redis 有哪些数据类型?各有什么使用场景?
- Redis 为什么这么快?
- RDB和AOF区别?各自优缺点?
- 什么是缓存穿透/击穿/雪崩?怎么解决?
架构 #
- 主从复制过程?
- 哨兵原理?怎么选举新主节点?
- Redis Cluster 槽为什么是16384个?
- Redis Cluster 怎么路由key?
实践 #
- 基于Redis实现分布式锁?原子性怎么保证?
- 缓存和数据库一致性怎么保证?
- Redis 内存淘汰策略有哪些?生产选哪个?
- 过期键怎么删除?
进阶 #
- Redis 6 多线程是什么意思?为什么要用多线程?
- 什么是RedLock?解决什么问题?
- 跳跃表结构是什么?为什么ZSet用跳跃表不用平衡树?
- 如何解决Redis热点key问题?
面试题答案详解 #
基础 #
- Redis 有哪些数据类型?各有什么使用场景?
答案:
| 类型 | 数据结构 | 使用场景 |
|---|---|---|
| String | 简单动态字符串SDS | 缓存、计数器、分布式ID、Session |
| List | 快速链表 | 消息队列、最新列表、排行榜 |
| Hash | 字典+压缩列表 | 对象缓存(用户信息、商品信息) |
| Set | 哈希表 | 去重、共同好友、标签 |
| ZSet | 跳跃表+哈希表 | 排行榜、带权重队列、分页 |
| Bitmap | 位图 | 用户签到、活跃用户、布隆过滤器 |
| HyperLogLog | 基数估计 | 独立IP统计 |
| Geo | 地理位置 | 附近的人、距离计算 |
| Stream | 消息队列 | 多生产者消费者 |
- Redis 为什么这么快?
答案:
| 原因 | 说明 |
|---|---|
| 纯内存 | 数据在内存,纳秒级访问 |
| 单线程 | 避免上下文切换和锁竞争 |
| IO多路复用 | epoll/kqueue模型,非阻塞 |
| 高效数据结构 | 跳跃表、SDS等优化 |
| Redis 6+ | 多线程IO,提升网络处理 |
性能: 10万+ QPS
- RDB和AOF区别?各自优缺点?
答案:
| 对比项 | RDB | AOF |
|---|---|---|
| 原理 | 内存快照 | 追加写命令 |
| 文件大小 | 小,紧凑 | 大,可重写压缩 |
| 恢复速度 | 快 | 慢 |
| 数据安全 | 丢最后一次快照 | 最多丢1秒(everysec) |
| 适用场景 | 备份、灾难恢复 | 数据持久化 |
推荐方案: 混合持久化(Redis 4.0+)
aof-use-rdb-preamble yes
- 什么是缓存穿透/击穿/雪崩?怎么解决?
答案:
| 问题 | 描述 | 解决方案 |
|---|---|---|
| 穿透 | 查询不存在的数据,直接打DB | 缓存空值、布隆过滤器 |
| 击穿 | 热点key过期,大量并发打DB | 互斥锁、永不过期、预热 |
| 雪崩 | 大量key同时过期/缓存宕机 | 随机过期、多级缓存、限流 |
架构 #
- 主从复制过程?
答案:
全量同步:
- Slave发送SYNC命令
- Master执行BGSAVE生成RDB
- Master发送RDB给Slave
- Slave加载RDB初始化
增量同步:
- Master把后续命令发给Slave
- 命令传播,保持同步
- 哨兵原理?怎么选举新主节点?
答案:
哨兵作用:
- 监控:检查节点健康
- 通知:告警
- 故障转移:自动选主
- 配置提供:客户端发现Master
选举流程:
- 哨兵发现Master下线
- 哨兵投票(Raft算法)
- 选出Leader哨兵
- 选择最优Slave升为Master
- 其他Slave同步新Master
关键点: 奇数个哨兵,多数原则
- Redis Cluster 槽为什么是16384个?
答案:
原因:
- 16384 = 2^14,方便哈希计算
- 心跳包中用bitmap存储槽信息,16384 bits = 2KB,适中
- 太多浪费,太少不够分片
- 官方测试16384是最佳实践
- Redis Cluster 怎么路由key?
答案:
槽计算:
slot = CRC16(key) % 16384
路由:
- 客户端直接连接对应节点
- 重定向:MOVED/ASK响应
- Smart客户端:缓存槽映射
实践 #
- 基于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:看门狗自动续期
- 缓存和数据库一致性怎么保证?
答案:
方案对比:
| 方案 | 说明 | 推荐度 |
|---|---|---|
| 先删缓存,再更DB | 并发问题多 | ❌ |
| 先更DB,再删缓存 | 延迟双删 | ⭐⭐⭐ |
| 订阅binlog更新 | Canal,最可靠 | ⭐⭐⭐⭐⭐ |
延迟双删:
1. 先删缓存
2. 更数据库
3. 睡一会(几百ms)
4. 再删缓存(二次删除)
- Redis 内存淘汰策略有哪些?生产选哪个?
答案:
| 策略 | 说明 |
|---|---|
allkeys-lru |
淘汰所有key的LRU(推荐) |
volatile-lru |
淘汰过期key的LRU |
allkeys-random |
随机淘汰所有 |
volatile-random |
随机淘汰过期 |
volatile-ttl |
淘汰更早过期 |
noeviction |
不淘汰,报错 |
生产推荐: allkeys-lru
- 过期键怎么删除?
答案:
两种策略:
- 惰性删除: 访问时检查,过期则删除
- 定期删除: 每隔一段时间随机抽查一批
平衡: 避免CPU消耗太多,也避免内存浪费
进阶 #
- Redis 6 多线程是什么意思?为什么要用多线程?
答案:
架构:
- IO线程多线程: 网络读写用多线程
- 命令执行单线程: 核心命令执行还是单线程
原因:
- 网络IO成为瓶颈
- 利用多核CPU处理网络
- 命令执行单线程避免锁
- 什么是RedLock?解决什么问题?
答案:
RedLock红锁: 多Redis节点投票
算法:
- 在N个独立节点依次获取锁
- 超过半数成功,且总耗时<超时,才算成功
- 释放锁:所有节点都释放
解决: 单节点故障导致锁失效(高一致性场景)
- 跳跃表结构是什么?为什么ZSet用跳跃表不用平衡树?
答案:
跳跃表: 有序链表+分层索引,O(log n)
对比平衡树:
| 对比项 | 跳跃表 | 平衡树 |
|---|---|---|
| 实现 | 简单 | 复杂 |
| 范围查询 | 天然支持 | 需中序遍历 |
| 插入删除 | 只需改指针 | 需旋转 |
| CPU开销 | 小 | 大 |
ZSet: 跳跃表+哈希表,兼顾范围查询和单点查找
- 如何解决Redis热点key问题?
答案:
方案:
- key分片: key1, key2, key3...分散到不同节点
- 本地缓存: JVM本地缓存热点数据
- 永不过期: 异步后台更新
- 互斥锁: 单线程重建缓存
- 发现热点:
redis-cli --hotkeys
🔗 相关笔记 #
最后更新: 2026-05-13