📌 一、集群与高可用篇
1.1 Redis的集群方案有哪些?各有什么优缺点?
✅ 正确回答思路:
Redis有三种主要的集群方案:主从复制、哨兵模式、Cluster集群。我详细说明:
一、主从复制(Master-Slave)
1. 架构
Master(读写)
↓ 数据同步
Slave1(只读) Slave2(只读) Slave3(只读)
2. 配置
conf
# Master配置
bind 0.0.0.0
port 6379
# Slave配置
bind 0.0.0.0
port 6380
replicaof 192.168.1.100 6379 # 指定Master
replica-read-only yes # 从节点只读
3. 复制原理
全量复制(第一次连接):
1. Slave发送PSYNC命令给Master
2. Master执行BGSAVE生成RDB快照
3. Master发送RDB文件给Slave
4. Slave清空自己的数据,加载RDB
5. Master发送期间的增量命令给Slave
增量复制(后续同步):
1. Master把写命令发送到复制缓冲区
2. 异步发送给Slave
3. Slave执行命令
4. 优缺点
优点:
- 实现简单
- 读写分离,提高并发能力
- Slave可以做数据备份
缺点:
- 不支持自动故障转移,Master挂了需要手动切换
- 写操作只能在Master,写能力受限
- 主从同步有延迟
二、哨兵模式(Sentinel)
1. 架构
Sentinel1 Sentinel2 Sentinel3(哨兵集群,奇数个)
↓ 监控
Master
↓ 复制
Slave1 Slave2
2. 配置
conf
# sentinel.conf
port 26379
sentinel monitor mymaster 192.168.1.100 6379 2 # 监控Master,2个哨兵认为下线才算下线
sentinel down-after-milliseconds mymaster 5000 # 5秒ping不通就认为主观下线
sentinel parallel-syncs mymaster 1 # 故障转移时,同时向新Master同步的Slave数量
sentinel failover-timeout mymaster 60000 # 故障转移超时时间
3. 工作原理
监控:
- 哨兵每秒ping Master和Slave
- 如果down-after-milliseconds内没响应,标记为主观下线(SDOWN)
- 如果超过quorum(配置的数量)个哨兵认为下线,标记为客观下线(ODOWN)
故障转移:
1. Master被判定为客观下线
2. 哨兵之间选举出Leader(Raft算法)
3. Leader从Slave中选一个提升为Master(选择策略:优先级、复制偏移量、runid)
4. 让其他Slave复制新Master
5. 通知客户端新Master的地址
6. 旧Master恢复后变成Slave
4. Java客户端配置
java
@Configuration
public class RedisSentinelConfig {
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
.master("mymaster")
.sentinel("192.168.1.101", 26379)
.sentinel("192.168.1.102", 26379)
.sentinel("192.168.1.103", 26379);
return new LettuceConnectionFactory(sentinelConfig);
}
}
5. 优缺点
优点:
- 自动故障转移,高可用
- 哨兵集群自身也是高可用的
- 读写分离
缺点:
- 写能力还是受Master限制,不能水平扩展
- 数据量大了,单个Master内存不够
- 故障转移有几秒到几十秒的延迟
三、Cluster集群(分片集群)
1. 架构
Master1(0-5460) Master2(5461-10922) Master3(10923-16383)
↓ ↓ ↓
Slave1 Slave2 Slave3
槽位(Slot):
- Redis Cluster把16384个槽位分配给各个Master
- 每个key通过CRC16算法计算出一个槽位:
HASH_SLOT = CRC16(key) % 16384 - 根据槽位找到对应的Master
2. 配置
conf
# 每个节点的redis.conf
port 7000
cluster-enabled yes # 开启集群模式
cluster-config-file nodes-7000.conf # 集群配置文件
cluster-node-timeout 5000 # 节点超时时间
启动集群:
bash
# 创建集群
redis-cli --cluster create \
192.168.1.101:7000 192.168.1.102:7001 192.168.1.103:7002 \
192.168.1.101:7003 192.168.1.102:7004 192.168.1.103:7005 \
--cluster-replicas 1 # 每个Master 1个Slave
3. 工作原理
路由查询:
1. 客户端计算key的槽位
2. 查询本地缓存的槽位映射表
3. 直接连接对应的节点
4. 如果节点不对,节点返回MOVED或ASK重定向
5. 客户端更新缓存,重新请求
故障转移:
1. 某个Master挂了
2. 集群中超过半数Master认为它下线
3. 它的Slave自动提升为Master
4. 集群重新分配槽位
扩容缩容:
bash
# 添加节点
redis-cli --cluster add-node 新节点IP:端口 现有节点IP:端口
# 分配槽位
redis-cli --cluster reshard 集群节点IP:端口
4. Java客户端配置
java
@Configuration
public class RedisClusterConfig {
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration(
Arrays.asList(
"192.168.1.101:7000",
"192.168.1.102:7001",
"192.168.1.103:7002",
"192.168.1.101:7003",
"192.168.1.102:7004",
"192.168.1.103:7005"
)
);
return new LettuceConnectionFactory(clusterConfig);
}
}
5. 优缺点
优点:
- 真正的分布式,可以水平扩展
- 写能力和存储能力都可扩展
- 高可用,部分节点挂了不影响整体
缺点:
- 运维复杂
- 客户端实现复杂
- 不支持跨节点的事务和多key操作(除非用hash tag)
- 迁移槽位时有性能影响
6. Hash Tag(解决跨节点问题)
java
// ❌ 不同的key可能在不同节点,MGET不支持
redisTemplate.opsForValue().multiGet(Arrays.asList("user:1001", "user:1002"));
// ✅ 用Hash Tag,保证在同一个节点
// {user}会作为计算槽位的部分
redisTemplate.opsForValue().multiGet(Arrays.asList("{user}:1001", "{user}:1002"));
四、三种方案对比表
| 特性 | 主从复制 | 哨兵模式 | Cluster集群 |
|---|---|---|---|
| 高可用 | ❌ 需手动切换 | ✅ 自动故障转移 | ✅ 自动故障转移 |
| 读写分离 | ✅ | ✅ | ✅ |
| 写扩展 | ❌ 单Master | ❌ 单Master | ✅ 多Master |
| 存储扩展 | ❌ | ❌ | ✅ 分片存储 |
| 运维复杂度 | 低 | 中 | 高 |
| 客户端复杂度 | 低 | 中 | 高 |
五、实际项目选择
1. 数据量小(<10GB),QPS低(<10万)
- 用主从 + 哨兵
- 我的项目:1主2从3哨兵
2. 数据量大(>100GB),QPS高(>50万)
- 用Cluster集群
- 我之前的电商项目:6个节点(3主3从)
3. 对一致性要求极高
- 不用Redis,用MySQL或者Zookeeper
💡 记忆口诀:
- 主从: 简单但不高可用
- 哨兵: 高可用但不能扩展
- Cluster: 高可用又能扩展,但复杂
1.2 Redis Cluster的槽位分配和数据迁移原理是什么?
✅ 正确回答思路:
一、槽位(Slot)的概念
Redis Cluster把整个数据库分成16384个槽位(为什么是16384?后面说)。
槽位分配:
假设3个Master:
Master1: 0-5460 (5461个槽)
Master2: 5461-10922 (5462个槽)
Master3: 10923-16383(5461个槽)
Key如何找到槽位:
HASH_SLOT = CRC16(key) mod 16384
举例:
key = "user:1001"
CRC16("user:1001") = 50018
50018 % 16384 = 1266
→ 槽位1266,在Master1上
为什么是16384个槽?
- 16384 = 16K = 2^14,槽位信息可以用bitmap表示,只需要2KB
- 集群通过心跳包交换槽位信息,16384个槽位的bitmap只需要2KB,传输快
- 槽位太多,心跳包太大,影响性能
- Redis Cluster一般不会有超过1000个节点,16384个槽位足够了
二、数据迁移原理
场景1: 扩容(添加新节点)
bash
# 1. 添加新Master节点
redis-cli --cluster add-node 192.168.1.104:7006 192.168.1.101:7000
# 2. 分配槽位给新节点(重新分片)
redis-cli --cluster reshard 192.168.1.101:7000
迁移流程:
假设Master1有0-5460槽,现在要迁移1000个槽(0-999)给新Master4
1. 在Master4上执行: CLUSTER SETSLOT 0 IMPORTING Master1_ID
→ Master4准备接收槽0的数据
2. 在Master1上执行: CLUSTER SETSLOT 0 MIGRATING Master4_ID
→ Master1准备迁移槽0的数据
3. 获取槽0的所有key: CLUSTER GETKEYSINSLOT 0 100
→ 每次获取100个key
4. 迁移这些key: MIGRATE 目标IP 目标端口 key 0 5000
→ 把key迁移到Master4
5. 重复3-4,直到槽0的所有key都迁移完
6. 通知集群: CLUSTER SETSLOT 0 NODE Master4_ID
→ 槽0现在属于Master4了
7. 重复1-6,迁移槽1-999
关键点:
- 迁移过程中,集群还在正常服务
- 如果访问正在迁移的key:
- 如果key还在旧节点,旧节点处理
- 如果key已迁移,旧节点返回ASK重定向
- 客户端收到ASK,去新节点请求
场景2: 缩容(删除节点)
bash
# 1. 先把这个节点的槽位分配给其他节点
redis-cli --cluster reshard 192.168.1.101:7000
# 选择要删除的节点,把它的槽位分配给其他节点
# 2. 删除节点
redis-cli --cluster del-node 192.168.1.104:7006 节点ID
三、实际迁移案例
我们的电商项目,双11前扩容:
原架构 : 3主3从(6节点) 扩容后: 6主6从(12节点)
操作步骤:
1. 凌晨2点(流量低谷)开始扩容
2. 添加3个新Master节点
3. 把每个旧Master的1/4槽位迁移给新Master
4. 添加3个Slave节点
5. 观察24小时,没问题后移除旧节点的Slave,降低成本
结果:
- QPS从30万提升到80万
- 单节点内存从32GB降到16GB
- 迁移过程中,P99延迟增加了10ms,可以接受
💡 总结:
- 16384个槽位,CRC16(key) % 16384计算槽位
- 迁移是槽位粒度的,渐进式迁移,不影响服务
- 扩容缩容都要重新分配槽位
📌 二、性能优化篇
2.1 如何排查Redis慢查询?
✅ 正确回答思路:
一、慢查询日志
Redis有个慢查询日志功能,记录执行时间超过阈值的命令。
配置:
conf
# redis.conf
slowlog-log-slower-than 10000 # 超过10毫秒的命令记录到慢查询日志(单位:微秒)
slowlog-max-len 128 # 慢查询日志最多保存128条
查看慢查询:
bash
# 查看慢查询日志
127.0.0.1:6379> SLOWLOG GET 10
1) 1) (integer) 6 # 日志ID
2) (integer) 1709012345 # 时间戳
3) (integer) 12000 # 执行耗时(微秒),12毫秒
4) 1) "KEYS" # 命令
2) "user:*"
5) "127.0.0.1:54321" # 客户端地址
6) "user-service" # 客户端名称
二、常见慢查询原因和解决方案
1. KEYS命令
bash
# ❌ 绝对不能在生产环境用!
KEYS user:*
问题: KEYS会遍历所有key,Redis是单线程,会阻塞其他命令!
解决: 用SCAN命令
java
// ✅ 用SCAN,增量迭代,不阻塞
public Set<String> scanKeys(String pattern) {
Set<String> keys = new HashSet<>();
ScanOptions options = ScanOptions.scanOptions()
.match(pattern)
.count(100) // 每次返回约100个
.build();
Cursor<byte[]> cursor = redisTemplate.executeWithStickyConnection(
connection -> connection.scan(options)
);
while (cursor.hasNext()) {
keys.add(new String(cursor.next()));
}
return keys;
}
2. HGETALL大Hash
bash
# ❌ Hash有10万个field,一次性获取,很慢!
HGETALL user:1001:shopping_cart
解决:
- 用HSCAN增量获取
- 或者拆分成多个小Hash
java
// 拆分方案
// 原来: user:1001:cart → {product:1001: 1, product:1002: 2, ...}
// 拆分后:
// user:1001:cart:1 → {product:1001: 1, product:1002: 2, ...}
// user:1001:cart:2 → {product:1101: 1, product:1102: 2, ...}
3. SMEMBERS大Set
bash
# ❌ Set有几十万元素,一次性返回,很慢!
SMEMBERS tags:all
解决: 用SSCAN
4. ZRANGE大ZSet
bash
# ❌ 获取所有元素
ZRANGE rank:game 0 -1 WITHSCORES
解决: 分页获取
bash
# ✅ 每次只获取100个
ZRANGE rank:game 0 99 WITHSCORES
5. DEL大key
bash
# ❌ 删除一个有100万元素的List,会阻塞!
DEL mylist
解决: 用UNLINK(异步删除)
java
// ✅ 异步删除,不阻塞
redisTemplate.unlink("mylist");
6. 大value
bash
# ❌ 一个key的value有10MB
SET config:app '{"data": "10MB的JSON"}'
解决:
- 压缩后再存储
- 或者拆分成多个key
三、监控工具
1. Redis自带的INFO命令
bash
127.0.0.1:6379> INFO stats
# 关注这些指标:
instantaneous_ops_per_sec:10542 # 当前QPS
total_commands_processed:1000000 # 总命令数
rejected_connections:0 # 拒绝的连接数
expired_keys:1234 # 过期key数量
evicted_keys:0 # 被淘汰的key数量
keyspace_hits:9500 # 命中次数
keyspace_misses:500 # 未命中次数
# 命中率 = keyspace_hits / (keyspace_hits + keyspace_misses) = 95%
2. redis-cli --bigkeys
bash
# 找出占用内存最大的key
redis-cli --bigkeys
# 输出:
[00.00%] Biggest string found so far 'config:app' with 10485760 bytes
[00.00%] Biggest list found so far 'mylist' with 1000000 items
[00.00%] Biggest hash found so far 'user:1001:cart' with 50000 fields
3. RedisInsight(官方GUI工具)
- 可视化监控
- 慢查询分析
- 内存分析
4. Prometheus + Grafana
yaml
# prometheus.yml
scrape_configs:
- job_name: 'redis'
static_configs:
- targets: ['localhost:9121'] # redis_exporter端口
四、实际案例
案例1: KEYS导致的故障
我们之前有个运营同学,在生产环境用了KEYS user:*,结果Redis阻塞了5秒,所有请求超时,报警狂响。
解决:
- 立即kill掉那个客户端连接
- 在redis.conf里禁用KEYS命令:
rename-command KEYS "" - 培训运营同学,绝对不能在生产环境用KEYS
案例2: 大key导致的慢查询
监控发现某个Hash的HGETALL命令耗时200ms,排查发现是购物车,有个用户加购了8000个商品(刷单)!
解决:
- 把购物车拆分,每100个商品一个Hash
- 对异常用户做限制,最多加购500个商品
案例3: 过期key太多
监控发现expired_keys指标暴涨,CPU使用率高。原因是运营活动结束,大量优惠券key集中过期。
解决:
- 优惠券过期时间加随机值,不集中过期
- 调大redis.conf的hz参数(默认10,调到100),加快过期key清理
💡 总结:
- 用SLOWLOG排查慢查询
- 禁用KEYS、HGETALL等危险命令
- 用SCAN代替KEYS
- 用UNLINK代替DEL
- 监控QPS、命中率、慢查询
2.2 Redis的内存淘汰策略有哪些?
✅ 正确回答思路:
当Redis内存不够时,需要淘汰一些key腾出空间。Redis提供了8种淘汰策略。
一、8种淘汰策略
配置:
conf
maxmemory 2gb # 最大内存2GB
maxmemory-policy allkeys-lru # 淘汰策略
策略分类:
1. noeviction(默认)
- 不淘汰,内存满了直接报错
- 新的写命令返回错误:
(error) OOM command not allowed when used memory > 'maxmemory' - 适合场景: 缓存命中率要求极高,不允许丢数据
2. allkeys-lru
- 从所有key 中,淘汰最近最少使用(LRU, Least Recently Used)的key
- 适合场景: 通用缓存(推荐!)
3. allkeys-lfu (Redis 4.0+)
- 从所有key中,淘汰使用频率最低(LFU, Least Frequently Used)的key
- 适合场景: 有明显的热点数据
4. allkeys-random
- 从所有key中,随机淘汰
- 适合场景: 所有key访问概率差不多
5. volatile-lru
- 从设置了过期时间的key中,淘汰LRU的key
- 如果没有设置过期时间的key,行为同noeviction
- 适合场景: 部分数据是缓存(可淘汰),部分是持久数据(不可淘汰)
6. volatile-lfu (Redis 4.0+)
- 从设置了过期时间的key中,淘汰LFU的key
7. volatile-random
- 从设置了过期时间的key中,随机淘汰
8. volatile-ttl
- 从设置了过期时间的key中,淘汰TTL最小(即将过期)的key
二、LRU vs LFU
LRU(最近最少使用):
访问记录: A A A B B C A
淘汰顺序: C(最久没用) → B → A
问题: 如果有个key突然被访问了一次,就不会被淘汰,即使它不是热点数据。
LFU(最不经常使用):
访问频率: A(100次) B(50次) C(1次)
淘汰顺序: C(最少用) → B → A
优点: 更准确地识别热点数据
三、LRU实现原理(重要!)
Redis的LRU不是严格的LRU,而是近似LRU。
为什么不用严格LRU?
- 严格LRU需要维护一个双向链表,每次访问都要移动节点,开销大
- Redis使用了采样的方式,随机采样N个key,淘汰其中LRU值最小的
配置:
conf
maxmemory-samples 5 # 采样数量,默认5,越大越精确,但越慢
实现:
Redis给每个key维护一个24bit的lru字段,记录最后访问时间(秒级)
淘汰时:
1. 随机采样5个key
2. 比较这5个key的lru字段
3. 淘汰lru最小(最久没访问)的那个
4. 如果内存还不够,重复1-3
四、实际项目选择
我的电商项目:
conf
maxmemory 16gb
maxmemory-policy allkeys-lru
maxmemory-samples 10 # 提高精确度
为什么选allkeys-lru?
- 商品信息、用户信息都是缓存,可以淘汰
- 淘汰后从DB重新加载就行
- LRU能保留热点数据(热门商品)
Session缓存用volatile-lru:
java
// Session设置了30分钟过期
redisTemplate.opsForValue().set("session:" + sessionId, userInfo, 30, TimeUnit.MINUTES);
配置volatile-lru,只淘汰快过期的Session,保留活跃用户的Session。
五、监控淘汰情况
bash
127.0.0.1:6379> INFO stats
evicted_keys:12345 # 被淘汰的key数量
如果evicted_keys一直增长,说明内存不够,要么加内存,要么优化代码减少缓存。
💡 总结:
- 通用缓存用allkeys-lru(推荐)
- 有明显热点用allkeys-lfu
- 混合场景(部分可淘汰,部分不可淘汰)用volatile-*
- 监控evicted_keys,如果持续增长,扩容!
📌 三、面试回答技巧总结
1. 分层次回答
- 先说"是什么",再说"为什么",最后说"怎么用"
- 比如问Redis为什么快: 内存操作→单线程→IO多路复用→数据结构优化→协议简单
2. 结合实际项目
- 不要只说理论,一定要说"我在项目中..."
- 说具体的数据: QPS从3万提升到10万,响应时间从200ms降到10ms
3. 对比说明
- 比如说持久化: RDB vs AOF,各有优缺点
- 说集群: 主从 vs 哨兵 vs Cluster
4. 画图辅助(如果可以)
- 主从复制的架构图
- 缓存击穿的场景图
- 能让面试官更直观理解
5. 适当展示深度
- 可以说"底层是用epoll实现的IO多路复用"
- 但不要主动挖坑,说了就要能讲清楚
6. 诚实应对不会的
- 不会就说不会,但可以说"我的理解是..."
- 或者说"这个问题我回去研究一下"
7. 控制时间
- 每个问题2-3分钟
- 不要太简短(显得不懂),也不要太啰嗦
8. 高频必考题 一定要准备的:
- Redis为什么快?
- 持久化RDB vs AOF
- 缓存穿透/击穿/雪崩
- 缓存一致性
- 分布式锁
- 集群方案
- 内存淘汰策略
📌 四、总结
这篇Redis面试八股文,我尽量用"人话"把技术点讲清楚,并且提供了大量实际代码和项目经验。
重点回顾:
- 基础: Redis是内存数据库,快的原因是内存+单线程+IO多路复用
- 数据类型: 5种基础+4种高级,每种都有具体应用场景
- 持久化: RDB快照 vs AOF日志,推荐混合持久化
- 缓存三大问题: 穿透(布隆过滤器)、击穿(互斥锁)、雪崩(过期时间打散)
- 一致性: Cache Aside,先更新DB再删缓存
- 分布式锁: 用Redisson,注意续期和释放自己的锁
- 集群: 哨兵(高可用)vs Cluster(高可用+可扩展)
- 性能优化: 禁用KEYS,用SCAN,监控慢查询,选对淘汰策略
最后的建议:
- 理解比背诵重要
- 实战经验比理论重要
- 能说出"为什么"比知道"是什么"重要
如果这篇文章对你有帮助,记得收藏起来,面试前看一遍,效果更佳!