口语八股——Redis 面试实战指南(终篇):集群与高可用篇、性能优化篇、面试回答技巧总结

📌 一、集群与高可用篇

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个槽?

  1. 16384 = 16K = 2^14,槽位信息可以用bitmap表示,只需要2KB
  2. 集群通过心跳包交换槽位信息,16384个槽位的bitmap只需要2KB,传输快
  3. 槽位太多,心跳包太大,影响性能
  4. 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秒,所有请求超时,报警狂响。

解决:

  1. 立即kill掉那个客户端连接
  2. 在redis.conf里禁用KEYS命令: rename-command KEYS ""
  3. 培训运营同学,绝对不能在生产环境用KEYS

案例2: 大key导致的慢查询

监控发现某个Hash的HGETALL命令耗时200ms,排查发现是购物车,有个用户加购了8000个商品(刷单)!

解决:

  1. 把购物车拆分,每100个商品一个Hash
  2. 对异常用户做限制,最多加购500个商品

案例3: 过期key太多

监控发现expired_keys指标暴涨,CPU使用率高。原因是运营活动结束,大量优惠券key集中过期。

解决:

  1. 优惠券过期时间加随机值,不集中过期
  2. 调大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面试八股文,我尽量用"人话"把技术点讲清楚,并且提供了大量实际代码和项目经验。

重点回顾:

  1. 基础: Redis是内存数据库,快的原因是内存+单线程+IO多路复用
  2. 数据类型: 5种基础+4种高级,每种都有具体应用场景
  3. 持久化: RDB快照 vs AOF日志,推荐混合持久化
  4. 缓存三大问题: 穿透(布隆过滤器)、击穿(互斥锁)、雪崩(过期时间打散)
  5. 一致性: Cache Aside,先更新DB再删缓存
  6. 分布式锁: 用Redisson,注意续期和释放自己的锁
  7. 集群: 哨兵(高可用)vs Cluster(高可用+可扩展)
  8. 性能优化: 禁用KEYS,用SCAN,监控慢查询,选对淘汰策略

最后的建议:

  • 理解比背诵重要
  • 实战经验比理论重要
  • 能说出"为什么"比知道"是什么"重要

如果这篇文章对你有帮助,记得收藏起来,面试前看一遍,效果更佳!

相关推荐
a285282 小时前
最新SQL Server 2022保姆级安装教程【附安装包】
数据库·性能优化
马猴烧酒.2 小时前
【面试八股|Mysql篇】Mysql常见面试题详解笔记
笔记·mysql·面试
学到头秃的suhian3 小时前
Redis分布式锁
java·数据库·redis·分布式·缓存
钛态3 小时前
Flutter for OpenHarmony:leak_tracker 自动监测内存泄漏,精准定位未释放对象(内存性能优化) 深度解析与鸿蒙适配指南
flutter·华为·性能优化·harmonyos
番茄去哪了3 小时前
Redis零基础入门
数据库·redis·缓存
知识即是力量ol3 小时前
口语八股——Redis 面试实战指南(二):缓存篇、分布式锁篇
java·redis·缓存·面试·分布式锁·八股
三水不滴3 小时前
SpringBoot + Redis 滑动窗口计数:打造高可靠接口防刷体系
spring boot·redis·后端
予枫的编程笔记3 小时前
【Docker进阶篇】Docker Compose实战:Spring Boot与Redis服务名通信全解析
spring boot·redis·docker·docker compose·微服务部署·容器服务发现·容器通信
wei7064 小时前
Redis持久化机制详解
面试