Redis 高级场景速查手册

最后更新:2026-08-10

定位:Java 高级开发工程师面试 Redis 深度备战,口语化 + 真实案例 + 面试话术。 阅读建议:先通一遍建立知识体系,面试前重点背「面试怎么说」的话术。


目录

  1. Redis 数据类型深度

  2. 缓存穿透 击穿 雪崩

  3. 分布式锁

  4. Redis 持久化

  5. Redis 集群方案

  6. Redis 内存管理

  7. Redis 高可用架构

  8. Redis 7.x 新特性

  9. 面试高频问答


1. Redis 数据类型深度

1.1 八大类型全景图

Redis 远不止 String、List、Hash、Set、ZSet 这 5 种基础类型,从 3.2 开始陆续加了 Bitmap、HyperLogLog、GEO、Stream,面试时把这几样说出来,直接加分。

类型典型场景一句话总结
String缓存、计数器、分布式锁万能类型,啥都能存
Hash用户信息、对象属性存对象首选,字段可单独改
List消息队列、时间线有序但没索引,头尾快
Set标签、共同好友、抽奖去重 + 集合运算
ZSet排行榜、延迟队列带 score 排序,O(logN)
Bitmap签到、在线状态、布隆过滤器位操作,极致省内存
HyperLogLogUV 统计基数估算,误差 0.81%,只占 12KB
GEO附近的人、打车距离地理位置,底层就是 ZSet
Stream消息队列(Kafka 平替)5.0 引入,支持消费组

1.2 底层编码结构

面试必问:Redis 为了省内存,同一个数据类型在不同数据量下,底层编码是不一样的。

数据类型小数据量编码大数据量编码转换条件
Stringint → embstr → rawraw≤44字节用 embstr,否则 raw
Listquicklist(ziplist 已废弃)quicklistlist-max-ziplist-size 默认 -2(8KB)
Hashlistpack(4.0 前是 ziplist)hashtablehash-max-listpack-entries ≤128 且每个 value ≤64字节
Setintset → listpackhashtableintset:全是整数且 ≤512 个;listpack:≤128 个且 ≤64字节
ZSetlistpackskiplist + hashtablezset-max-listpack-entries ≤128 且每个 ≤64字节

⚠️ 面试高频坑点

  • 4.0 版本 ziplistlistpack 的迁移,ziplist 有连锁更新问题(一个节点变长导致后面的节点全部重新分配),listpack 通过限制 entry 最大长度解决了这个问题。

  • Redis 7.0 里 listpack 进一步取代了 ziplist 在几乎所有场景的使用。

  • 5.0 之后 List 的底层是 quicklist,本质是 ziplist(现在也换成了 listpack)的双向链表,兼顾头尾操作效率和内存利用。

底层结构口诀

String 三兄弟:int → embstr → raw
List 用 quicklist:链表套 listpack
Hash 小 listpack 大 hashtable
Set 全整数 intset,否则 listpack 或 hashtable
ZSet 小 listpack 大 skiplist + hashtable

1.3 内存编码转换阈值速查表

配置项默认值含义
hash-max-listpack-entries128Hash 的 field 数 ≤ 128 用 listpack
hash-max-listpack-value64Hash 每个 value ≤ 64 字节
zset-max-listpack-entries128ZSet 元素数 ≤ 128 用 listpack
zset-max-listpack-value64ZSet 每个 value ≤ 64 字节
set-max-intset-entries512Set 全是整数且 ≤ 512 个用 intset
set-max-listpack-entries128Set 用 listpack 的元素上限(7.0+)
list-max-ziplist-size-2quicklist 每个节点的 listpack 大小,-2 代表 8KB

💡 面试怎么说: "Redis 会根据数据量动态切换底层编码。比如 Hash 在字段少于 128 个且 value 都小于 64 字节时用 listpack,超过了就切到 hashtable。这种设计在节省内存的同时保证了大数据量下的操作性能。listpack 是 4.0 引入的,解决了 ziplist 的连锁更新问题——因为 listpack 限制了单个 entry 最大长度,不会因为一个节点的变长导致后面所有节点连锁重新分配。"

1.4 选型指南:场景 → 类型

业务场景推荐类型备注
用户会话 / TokenString简单 KV,设过期时间
用户对象(姓名+年龄+…)Hash可单独改某个字段,省序列化开销
消息队列Stream / ListStream 支持 ACK、消费组,生产首选
排行榜ZSetscore 排序 + 范围查询
延迟队列ZSetscore = 执行时间戳,轮询 ZRANGEBYSCORE
每日签到BitmapSETBIT 一位一天,一年才 46 字节
UV 统计HyperLogLog百万 UV 只占 12KB,误差可接受
附近的人GEO底层是 ZSet,GEORADIUS 查询
布隆过滤器Bitmap + 多哈希或用 RedisBloom 模块
分布式锁StringSET key value NX EX
计数器(点赞数)StringINCR 原子操作

2. 缓存穿透 击穿 雪崩

2.1 三者区别一张表

维度缓存穿透缓存击穿缓存雪崩
定义查询一个根本不存在的数据,缓存和 DB 都没有某个热点 Key 突然过期,大量并发打到 DB大量 Key 同时过期或 Redis 宕机
原因恶意请求 / 不存在的 ID热点数据过期批量设相同 TTL / Redis 故障
影响范围单个 Key单个热点 Key全局 / 大范围
DB 压力持续但量小短时间巨大瞬间巨大
核心解决思路挡住不存在的请求保护热点 Key,串行化查 DB打散过期时间 + 兜底机制

2.2 缓存穿透解决方案

方案一:空值缓存

// 查到 null 也缓存,TTL 随机化防穿透
String val = redis.get(key);
if (val != null) {
    return val.equals("NULL") ? null : val;
}
// 查 DB
val = db.query(key);
if (val == null) {
    // 缓存空值,TTL 随机 60~120 秒
    redis.set(key, "NULL", randomTTL(60, 120));
} else {
    redis.set(key, val, randomTTL(300, 600));
}

优点:简单粗暴,代码改动小 缺点:攻击者用随机 UUID 轰炸,缓存里堆满垃圾 Key,内存浪费

方案二:布隆过滤器

请求 → 布隆过滤器检查 → "不存在" → 直接返回
                      → "可能存在" → 查缓存 → 查 DB

布隆过滤器核心原理:一个超长的 bit 数组 + 多个哈希函数。插入时把多个哈希位置 1,查询时所有位都为 1 才认为"可能存在"。

特性说明
误判率可能把"不存在"判断为"存在",但绝不会把"存在"判断为"不存在"
不可删除标准布隆过滤器不支持删除(会误删其他 Key 的位)
Redis 实现RedisBloom 模块(BF.ADDBF.EXISTS
本地实现Google Guava BloomFilter

💡 面试怎么说: "我们线上用的是布隆过滤器方案。用户 ID 注册时写入布隆过滤器,查询请求先过布隆过滤器,如果判断不存在就直接返回,不会打到 DB。布隆过滤器放在 Redis 里用 RedisBloom 模块,也可以用 Guava 在应用本地做一层。布隆过滤器的缺点是不能删除、有一定误判率,但对我们的场景够用了——我们允许极少量请求穿透到 DB,但不能让大量恶意请求把 DB 打挂。"

2.3 缓存击穿解决方案

方案一:互斥锁(分布式锁)

public String getData(String key) {
    String val = redis.get(key);
    if (val != null) return val;

    // 加互斥锁,只让一个线程去查 DB
    String lockKey = "lock:" + key;
    if (redis.set(lockKey, "1", "NX", "EX", 10)) {
        try {
            // 二次检查(可能前一个线程刚写进去)
            val = redis.get(key);
            if (val != null) return val;

            val = db.query(key);
            redis.set(key, val, 300);
            return val;
        } finally {
            redis.del(lockKey);
        }
    } else {
        // 没拿到锁,休眠重试
        Thread.sleep(50);
        return getData(key);
    }
}

优点:数据一致性没问题 缺点:吞吐量下降,其他线程要等锁

方案二:逻辑过期

// 缓存里不设 Redis TTL,而是在 value 里存一个逻辑过期时间
class CacheValue {
    String data;
    long expireTime; // 逻辑过期时间戳
}

public String getData(String key) {
    CacheValue cv = redis.get(key);
    if (cv == null) {
        // 首次查询,同步加载
        return rebuildCache(key);
    }
    if (System.currentTimeMillis() < cv.expireTime) {
        return cv.data; // 未过期,直接返回
    }
    // 已过期 → 异步刷新,当前返回旧值
    executor.submit(() -> rebuildCache(key));
    return cv.data;
}

优点:不阻塞,高性能 缺点:短暂返回旧数据,一致性有妥协

方案一致性性能复杂度适用场景
互斥锁强一致较差对一致性要求高的场景
逻辑过期弱一致极好热点数据,允许短暂不一致

2.4 缓存雪崩解决方案

手段一:随机 TTL

// 基础 TTL 上加一个随机值,防止同一时间大面积过期
int baseTTL = 3600;
int randomTTL = baseTTL + random(0, 300); // 3600~3900 秒
redis.set(key, value, randomTTL);

手段二:多级缓存

请求 → Nginx 本地缓存(OpenResty) → Redis 集群 → DB

手段三:限流降级

// Sentinel 限流,DB 快扛不住时直接降级
@SentinelResource(value = "getProduct", fallback = "fallback")
public Product getProduct(String id) {
    // ...
}

public Product fallback(String id) {
    return Product.defaultProduct(); // 返回兜底数据
}

2.5 生产案例:一次缓存雪崩的排查

背景:2025 年双 11 大促前压测,某电商系统 QPS 到 8000 时 DB CPU 飙到 95%,RT 从 50ms 涨到 2 秒。

排查链路

1. 监控告警 → DB CPU 飙高,慢查询激增
2. 检查慢查询日志 → 都是简单的主键查询,不该这么慢
3. 检查 Redis → 发现大量 Key 集中过期(运维批量刷新缓存时设了相同 TTL)
4. 确认根因 → 某次配置变更把商品缓存 TTL 统一改成了 3600 秒
   → 1 小时后所有商品缓存同时失效
   → 海量请求穿透到 DB
5. 紧急处理
   → 临时扩容 DB 连接池
   → 开启 Sentinel 限流,保护 DB
   → 紧急脚本刷缓存
6. 后续优化
   → TTL 加随机偏移(±10%)
   → 引入多级缓存(本地 Caffeine + Redis)
   → 上线热点 Key 探测,提前预警
   → 增加缓存预热机制,大促前主动加载

💡 面试怎么说: "我们之前压测时遇到过一次缓存雪崩,根因是运维批量刷配置时把一批商品缓存 TTL 设成了相同值,1 小时后集中过期,请求全打到 DB 上。我们的排查思路是先看 DB 慢查询日志发现都是简单查询不该慢,再看 Redis 发现大量 Key 同时过期。紧急处理是限流保护 DB + 脚本刷缓存。后续做了三个优化:TTL 加随机偏移、引入本地 Caffeine 做多级缓存、上线热点 Key 探测。"


3. 分布式锁

3.1 演进路线

SETNX(简陋版)
  ↓ 问题:没过期时间,死锁
SET key value NX EX(能用了)
  ↓ 问题:锁过期了业务没执行完,误删别人的锁
Redisson(生产标配)
  ↓ 优势:Watch Dog 续期 + 可重入 + 各种锁
RedLock(争议方案)
  ↓ 争议:CAP 取舍

第一阶段:SETNX

if (redis.setnx("lock", "1")) {
    redis.expire("lock", 30); // 这两步不是原子的!
    // 业务...
    redis.del("lock");
}

问题setnxexpire 之间进程挂了 → 死锁。

第二阶段:SET NX EX

// 原子操作,一步到位
String result = redis.set("lock", "requestId", "NX", "EX", 30);
if ("OK".equals(result)) {
    try {
        // 业务...
    } finally {
        // 问题:锁过期了,别人拿到了锁,你删了别人的锁
        redis.del("lock");
    }
}

问题:锁 30 秒过期,但业务执行了 40 秒 → 锁自动释放 → 线程 B 拿到锁 → 线程 A 执行完把 B 的锁删了。

解决:value 存 requestId,删锁时校验 + Lua 保证原子性:

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

第三阶段:Redisson(生产标配)

RLock lock = redisson.getLock("order:lock:" + orderId);
try {
    lock.lock(); // 自动续期!
    // 业务...
} finally {
    lock.unlock();
}

3.2 Redisson 核心原理

Watch Dog 自动续期

加锁成功 → 启动 Watch Dog(后台定时任务)
         → 每 10 秒检查一次(默认锁 30 秒,每 1/3 检查)
         → 线程还活着 → 续期到 30 秒
         → 线程结束 / unlock → 停止续期

默认锁超时 30 秒(lockWatchdogTimeout),每 10 秒续期一次。只要持锁线程还活着,锁就不会过期。

Lua 脚本加锁(简化版)

-- 检查锁是否存在
if (redis.call('exists', KEYS[1]) == 0) then
    redis.call('hset', KEYS[1], ARGV[2], 1);  -- Hash 结构:线程ID → 重入次数
    redis.call('pexpire', KEYS[1], ARGV[1]);   -- 设置过期时间
    return nil;                                  -- 加锁成功
end;
-- 锁已存在,检查是否是自己(可重入)
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
    redis.call('hincrby', KEYS[1], ARGV[2], 1); -- 重入次数 +1
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;                                   -- 加锁成功
end;
return redis.call('pttl', KEYS[1]);              -- 加锁失败,返回剩余时间

注意 Redisson 的锁用的是 Hash 结构,field 是线程标识,value 是重入次数。所以 Redisson 的锁是可重入的

3.3 Redisson 各种锁

锁类型说明适用场景
可重入锁(ReentrantLock)同一个线程可以多次加锁,计数递增最常用,默认选择
公平锁(FairLock)按请求顺序排队获锁防饥饿,但有性能损耗
联锁(MultiLock)多个锁打包成一个,全部加锁成功才算成功跨资源操作
红锁(RedLock)多个独立 Redis 节点上分别加锁对一致性要求极高的场景(有争议)
读写锁(ReadWriteLock)读读共享、读写互斥读多写少场景

3.4 RedLock 争议

RedLock 做法:在 N 个(通常 5 个)独立的 Redis 节点上加锁,超过半数(N/2+1)加锁成功才算成功。

Martin Kleppmann 的质疑(经典博文 "How to do distributed locking"):

  1. 时钟跳跃问题:Redis 用的是墙钟(wall clock),GC 停顿、网络延迟都可能导致时钟不准

  2. 没有 fencing token:无法检测过期锁

  3. GC 停顿:客户端拿到锁后 GC 停顿,锁过期了都不知道

Antirez 的反驳

  1. RedLock 的设计目标就是在无可靠时钟的环境工作

  2. 如果真需要强一致分布式锁,应该用 ZooKeeper

业界共识

方案一致性性能推荐度
单节点 RedissonAP(高可用)极高⭐⭐⭐⭐⭐ 大部分场景够用
RedLock介于 AP 和 CP 之间较高⭐⭐⭐ 极端场景才考虑
ZooKeeper 锁CP(强一致)较低⭐⭐⭐⭐ 对一致性要求高时

💡 面试怎么说: "生产上我们用的是 Redisson 单节点分布式锁,配合 Watch Dog 自动续期。RedLock 虽然理论上更安全,但实际引入的复杂度很高——需要多个独立 Redis 节点,还有 Martin Kleppmann 提出的时钟问题。如果业务真的需要强一致的分布式锁,我觉得不如直接用 ZooKeeper 或者 etcd。大部分互联网场景用 Redisson 就够了,关键是理解它的原理和边界。"

3.5 生产使用建议

  1. 锁的 TTL 设合理:虽然 Redisson 有 Watch Dog,但建议业务超时时间也设好,防止异常情况

  2. 加 try-finally:确保 unlock() 一定执行

  3. 避免锁粒度太大:锁订单就别锁用户,锁行就别锁表

  4. 锁竞争监控:加 metrics 监控锁等待时间、获取失败率

  5. 降级方案:锁获取失败时有兜底逻辑,别直接报错


4. Redis 持久化

4.1 RDB(快照)

原理:某个时间点的全量数据快照,保存为 .rdb 二进制文件。

触发方式

方式说明
save阻塞主线程,生产禁用
bgsavefork 子进程,不阻塞主线程
配置自动触发save 900 1(900秒内至少1次写入就触发)
其他命令隐式触发shutdownslaveofdebug reload

fork + COW(Copy-On-Write)流程

1. 主进程 fork 子进程(此时共享同一块内存)
2. 子进程遍历内存生成 RDB 文件
3. 主进程处理写请求时,操作系统把被修改的内存页复制一份(COW)
4. 子进程拿到的是 fork 时刻的数据快照
优点缺点
文件紧凑,恢复速度快fork 时可能阻塞(大内存页表复制)
适合备份和灾备两次快照之间的数据可能丢失
对主进程性能影响小大内存实例 fork 耗时长

⚠️ 关键指标lazyfree-lazy-evictionlazyfree-lazy-expire 这些配置要打开,避免主线程做删除操作时阻塞。

4.2 AOF(追加日志)

原理:每条写命令追加到 AOF 文件末尾。

写策略(fsync 策略)

策略说明数据安全性能
always每条命令都 fsync最高,最多丢 1 条最差
everysec(默认)每秒 fsync 一次最多丢 1 秒数据很好
no由 OS 决定何时 fsync不确定最好

AOF Rewrite(重写)

目的:AOF 文件越来越大,重写压缩体积
过程:
1. fork 子进程
2. 子进程根据当前内存数据生成新的 AOF 文件
3. 重写期间主进程的新写入命令同时写入 rewrite buffer
4. 子进程完成后,把 rewrite buffer 追加到新 AOF 文件末尾
5. 用新文件替换旧文件

自动触发配置:auto-aof-rewrite-percentage 100 + auto-aof-rewrite-min-size 64mb

4.3 混合持久化(4.0+)

配置:aof-use-rdb-preamble yes

AOF 重写时:
前半段 → RDB 格式(当前全量数据)
后半段 → AOF 格式(重写期间的增量命令)
方案恢复速度数据安全文件体积
纯 RDB⭐⭐⭐⭐⭐ 极快⭐⭐ 可能丢很多
纯 AOF⭐⭐ 慢(要重放命令)⭐⭐⭐⭐⭐ 最多丢 1 秒
混合持久化⭐⭐⭐⭐ 快⭐⭐⭐⭐⭐ 最多丢 1 秒较小

💡 面试怎么说: "生产上我们用的是混合持久化,Redis 4.0 以后引入的。AOF 重写时前半段用 RDB 格式保存全量数据,后半段用 AOF 格式追加增量命令。这样恢复速度快(不用全量重放 AOF),数据也不容易丢。持久化策略用的 everysec,最多丢 1 秒数据。另外我们还会定期把 RDB 文件备份到 OSS,做灾备用。"

4.4 生产持久化配置建议

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

# AOF fsync 策略
appendfsync everysec

# 自动重写
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

# 异步删除(重要!避免主线程阻塞)
lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
lazyfree-lazy-server-del yes

# RDB 自动保存(兜底)
save 900 1
save 300 10
save 60 10000

5. Redis 集群方案

5.1 主从复制

原理

主节点 ←── 写请求
  │
  ├── 全量复制(首次同步 / 断线太久)
  │   1. 从节点发 PSYNC
  │   2. 主节点 BGSAVE 生成 RDB
  │   3. 发送 RDB 给从节点
  │   4. 发送 repl_backlog 中的增量命令
  │
  └── 增量复制(正常同步)
      主节点执行写命令 → 传播到 repl_backlog → 从节点读取执行

repl_backlog 的作用

一个固定大小的环形缓冲区(默认 1MB)
→ 从节点短暂断线后重连,只需要从 repl_backlog 中读取增量数据
→ 避免每次都全量复制
→ 配置:repl-backlog-size 1mb

5.2 哨兵模式(Sentinel)

架构:1 主 N 从 + M 个 Sentinel 节点

核心功能

  • 监控:Sentinel 定期 PING 主从节点

  • 自动故障转移:主节点挂了,Sentinel 投票选出新主

  • 通知:故障转移后通知客户端新的主节点地址

故障转移流程

1. Sentinel 每秒 PING 主节点
2. 连续 sentinel-down-after-milliseconds 毫秒无响应
   → 主观下线(SDOWN)
3. Sentinel 询问其他 Sentinel 是否也认为主节点挂了
4. 超过 quorum 个 Sentinel 同意
   → 客观下线(ODOWN)
5. 选举一个 Leader Sentinel 执行故障转移
6. Leader 从从节点中选一个提升为主节点(考虑优先级、复制偏移量、runid)
7. 通知其他从节点跟着新主
8. 客户端通过 Sentinel 获取新的主节点地址

从节点选择优先级

1. 从节点优先级(slave-priority,默认 100,越小越优先)
2. 复制偏移量最大(数据最新)
3. runid 最小

5.3 Cluster 模式

架构:N 个主节点,每个主节点有从节点做故障转移。

核心设计

16384 个哈希槽(slot)分配到各主节点
例:3 主节点
  节点 A:0~5460
  节点 B:5461~10922
  节点 C:10923~16383

客户端请求 → CRC16(key) % 16384 → 确定 slot → 找到对应节点
如果 slot 不在当前节点 → 返回 MOVED 重定向

Gossip 协议

节点间通过 Gossip 协议交换状态信息
- PING / PONG:心跳检测
- MEET:新节点加入
- FAIL:标记节点故障
- PFAIL:主观下线(一个节点认为某节点挂了)
         → 超过半数主节点认为 → 变成 FAIL(客观下线)

扩缩容

扩容:
1. 启动新节点
2. 从现有节点迁移 slot 和新节点
3. 迁移 slot 关联的数据(按 slot 逐个迁移 key)

缩容:
1. 把下线节点的 slot 迁移到其他节点
2. 移除节点

5.4 三种方案对比

维度主从复制哨兵模式Cluster
数据分布全量数据在每个节点全量数据在每个节点分片存储
高可用❌ 需要手动切换✅ 自动故障转移✅ 自动故障转移
水平扩展❌ 不能❌ 不能(单主瓶颈)✅ 天然支持
运维复杂度
客户端普通客户端需要 Sentinel 客户端需要 Cluster 客户端
最大数据量受单机内存限制受单机内存限制理论上无上限
适用场景读多写少 + 灾备中小规模高可用大规模、高并发

💡 面试怎么说: "生产环境我推荐用 Cluster 模式。主从复制只能做读写分离和灾备,没有自动故障转移能力;哨兵模式解决了高可用问题,但单主节点的写入能力有上限,数据量受单机内存限制。Cluster 模式既能水平扩展又能自动故障转移,适合大规模高并发场景。当然运维复杂度会高一些,槽位迁移、客户端路由都要处理好。我们线上是 6 主 6 从的 Cluster,每个主节点大概 4~8GB 数据。"


6. Redis 内存管理

6.1 内存淘汰策略

当 Redis 内存使用达到 maxmemory 限制时,就需要淘汰数据。

策略淘汰范围淘汰算法适用场景
noeviction不淘汰-写入报错,适合纯缓存不适合的场景
allkeys-lru所有 KeyLRU(最近最少使用)通用缓存,最常用
volatile-lru有过期时间的 KeyLRU只淘汰设了 TTL 的
allkeys-lfu所有 KeyLFU(最不经常使用)4.0+,按访问频率淘汰
volatile-lfu有过期时间的 KeyLFU按访问频率淘汰有 TTL 的
allkeys-random所有 Key随机差不多都要,随机吧
volatile-random有过期时间的 Key随机同上,但只淘汰有 TTL 的
volatile-ttl有过期时间的 KeyTTL 最短的优先优先淘汰快过期的

LRU vs LFU

  • LRU:最近没访问过的先淘汰。问题:偶尔被访问一次的大 Key 一直不被淘汰

  • LFU:按访问频率淘汰。4.0 引入,更科学。但新 Key 刚开始频率低可能被误淘汰

💡 面试怎么说: "我们生产用的是 allkeys-lfu,Redis 4.0 引入的。相比 LRU,LFU 按访问频率来淘汰更合理——一个 Key 虽然最近没被访问,但如果它历史访问频率很高,LFU 会倾向保留它。实际效果是缓存命中率比 LRU 高了大概 3~5 个百分点。"

6.2 内存碎片

产生原因:频繁的创建和删除导致内存空洞。

例:申请 100B → 释放 → 申请 50B → 释放 → 申请 80B
   → 可能产生碎片:100B 的块只用了 50B,剩下的浪费了

监控指标

info memory
mem_fragmentation_ratio = used_memory_rss / used_memory

> 1.0 正常(1.0~1.5 可接受)
> 1.5 碎片较多,需要处理
< 1.0 可能使用了 swap(更危险!)

解决方案

方案说明推荐度
activedefragRedis 4.0+ 内置在线碎片整理⭐⭐⭐⭐⭐
重启最简单粗暴,重建内存⭐⭐⭐ 有停机时间
config set activedefrag yes开启主动碎片整理⭐⭐⭐⭐⭐
# 碎片整理配置
activedefrag yes
active-defrag-enabled yes
active-defrag-threshold-lower 10    # 碎片率超过 110% 开始
active-defrag-threshold-upper 100   # 碎片率超过 200% 全力整理
active-defrag-cycle-min 1           # 最少占用 1% CPU
active-defrag-cycle-max 25          # 最多占用 25% CPU

6.3 maxmemory 设置建议

maxmemory 不要设成服务器全部内存!

推荐:maxmemory = 物理内存 × 60%~70%

原因:
1. Redis fork 子进程(RDB/AOF rewrite)需要额外内存
2. 操作系统本身需要内存
3. 预留一些给连接缓冲、客户端输出缓冲等

例:16GB 服务器 → maxmemory 10GB~12GB

💡 面试怎么说: "maxmemory 我们一般设成物理内存的 60%~70%,因为 Redis 在做 RDB 快照或 AOF 重写时需要 fork 子进程,利用 COW 机制虽然初期共享内存,但如果 fork 期间有大量写入,实际会复制大量内存页。留 30%~40% 就是为了应对这种情况。另外我们还会监控 mem_fragmentation_ratio,超过 1.5 就开启 activedefrag 做在线碎片整理。"


7. Redis 高可用架构

7.1 热 Key 问题

什么是热 Key:某个 Key 被超高频率访问(如秒杀商品、热搜话题)。

问题:单节点承受所有请求压力,可能导致节点崩溃。

解决方案

方案一:本地缓存

// Caffeine 本地缓存,挡住大部分请求
LoadingCache<String, String> localCache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(5, TimeUnit.SECONDS) // 极短过期
    .build(key -> redis.get(key));

方案二:读写分离 + 多从节点

热 Key → 通过 Cluster 的只读从节点分散读压力

方案三:热 Key 探测 + 自动复制

热 Key 探测工具(如京东的 hotkey 框架)
→ 检测到热 Key 后自动在多个节点创建副本
→ 分散请求压力

7.2 大 Key 问题

什么是大 Key

类型大 Key 标准
String> 10KB
Hash List Set / ZSet元素数 > 5000 或总大小 > 10MB

危害

  • 网络带宽:一次传输数据量大

  • 阻塞主线程:DEL 大 Key 会阻塞

  • 内存不均:大 Key 所在的 shard 内存偏高

  • 集群倾斜:数据分布不均

解决方案

拆分策略:
Hash → 拆成多个小 Hash(如 user:1:info, user:2:info → user:{shard}:info)
List → 按时间段拆分(如 message:202501, message:202502)
Set  → 按业务维度拆分

删除策略:
→ 用 UNLINK 替代 DEL(异步删除,4.0+)
→ 用 HSCAN 分批删除而非 DEL 整个 Key
// 大 Key 扫描
redis-cli --bigkeys
redis-cli --memkeys        // 按内存排序

// 大 Key 分析
redis-cli -h xxx --bigkeys --interval 0.1

// 更推荐用 RDB 分析工具
redis-rdb-tools (rdb -c memory dump.rdb)

7.3 缓存和数据库一致性

这是面试超高频题,核心是保证"缓存数据和数据库数据一致"。

方案一:延迟双删

更新流程:
1. 先删缓存
2. 更新数据库
3. 延迟 N 毫秒后再删缓存(N = 主从同步延迟 + 读请求耗时)

问题:延迟时间不好确定,还是有极小窗口不一致

方案二:Canal 订阅 Binlog(推荐)

更新流程:
1. 业务只更新数据库
2. Canal 伪装成 MySQL 从节点,订阅 binlog
3. Canal 解析 binlog → 发消息到 MQ
4. 消费者从 MQ 拿消息 → 更新 / 删除缓存
方案一致性复杂度推荐度
Cache Aside(先更新 DB,再删缓存)最终一致⭐⭐⭐⭐ 简单场景够用
延迟双删最终一致⭐⭐⭐
Canal + Binlog最终一致⭐⭐⭐⭐⭐ 生产推荐

💡 面试怎么说: "我们线上用的是 Canal 订阅 MySQL Binlog 的方案。业务流程只负责更新数据库,Canal 解析 Binlog 变更事件,发到 Kafka,消费端根据变更去更新 Redis 缓存。这样做的好处是业务代码完全解耦,缓存更新是异步的,而且有重试机制保证最终一致性。极端场景下 Canal 延迟可能在几百毫秒到几秒,但这个延迟对大部分业务是可以接受的。"

7.4 多级缓存架构设计

┌──────────┐     ┌──────────────┐     ┌─────────┐     ┌──────┐
│  Client   │────▶│ Nginx/OpenResty│────▶│  Redis   │────▶│  DB  │
│  (浏览器) │     │ (本地缓存)     │     │ (分布式) │     │      │
└──────────┘     └──────────────┘     └─────────┘     └──────┘
                        │                   │
                   OpenResty            Caffeine
                   lua_shared_dict      (JVM 本地)

各层职责

层级技术容量过期时间作用
L1:浏览器/CDN浏览器缓存、CDN用户端分钟~小时挡住静态资源和重复请求
L2:应用本地缓存Caffeine / Guava小(受 JVM 堆限制)秒级挡住热 Key,减轻 Redis 压力
L3:分布式缓存Redis Cluster分钟~小时核心缓存层
L4:数据库MySQL最大永久数据最终来源

本地缓存一致性

问题:本地缓存改了,其他机器怎么知道?
方案:
1. 本地缓存 TTL 设很短(5~10 秒),接受短暂不一致
2. 通过 Redis Pub/Sub 通知所有节点清除本地缓存
3. 通过 MQ 广播缓存变更事件

8. Redis 7.x 新特性

8.1 Redis Functions(7.0+)

替代传统 EVAL + Lua 脚本的方案。

对比EVAL + LuaRedis Functions
脚本管理每次都要传脚本内容先注册,后调用(函数名)
版本管理✅ 支持多版本
持久化❌(每次加载)✅ 持久化到 RDB / AOF
调试困难更好(有 function stats)
# 注册函数
FUNCTION LOAD "#!lua name=mylib
redis.register_function('myfunc', function(keys, args)
    return redis.call('GET', keys[1])
end)"

# 调用函数
FCALL mylib 1 mykey

8.2 Multi-part AOF(7.0+)

之前:一个巨大的 appendonly.aof 文件 + manifest

7.0+:拆分成多个文件
  appendonly.aof.manifest     # 清单文件
  appendonly.aof.1.base.rdb   # 基础数据(RDB 格式)
  appendonly.aof.1.incr.aof   # 增量命令
  appendonly.aof.2.incr.aof   # 新的增量

好处

  • Rewrite 更高效(不用重写整个文件)

  • 管理更清晰

  • 恢复更快

8.3 ACL v2(7.0+)

之前:requirepass 一个简单的密码

7.0+:完整的访问控制
  - 用户管理(ACL SETUSER / ACL GETUSER)
  - 细粒度权限(按命令、按 Key 模式、按 Channel)
  - 选择器(Selector):一个用户多组权限
# 创建一个只能读写 user:* 的用户
ACL SETUSER alice on >password ~user:* +get +set +del

8.4 Sharded Pub/Sub(7.0+)

之前:Pub/Sub 消息会广播到所有节点(Cluster 模式下)

7.0+:SSUBSCRIBE / SPUBLISH
  → 消息只发往 Key 所在的分片节点
  → 避免消息在所有节点间冗余传播
  → 更适合大规模 Cluster 场景

8.5 客户端缓存优化(7.0+)

CLIENT TRACKING 增强:
- 支持 tracking 特定前缀的 Key
- 支持 broadcast 模式(被动通知失效)
- 更好的客户端缓存失效通知机制

8.6 其他 7.x 重要特性

特性版本说明
Listpack 替代 Ziplist7.0彻底消灭 ziplist
SHARD 命令7.0查看 Cluster 槽位信息
多部分 AOF7.0AOF 文件拆分管理
FUNCTION7.0替代 EVAL
EVAL_RO / FCALL_RO7.0只读脚本,可路由到从节点
概率数据结构7.4Bloom Filter / Cuckoo Filter 原生支持

💡 面试怎么说: "Redis 7.0 有几个我觉得比较重要的改进:一是 Redis Functions 替代了 EVAL + Lua,脚本可以注册和持久化,管理更方便;二是 Multi-part AOF,AOF 文件不再是一个大文件,而是拆分成 base + incr 多个文件,rewrite 更高效;三是 Sharded Pub/Sub,解决了 Cluster 模式下 Pub/Sub 消息冗余传播的问题。另外 7.4 还新增了原生的布隆过滤器支持,不用再依赖 RedisBloom 模块了。"


9. 面试高频问答

Q1:Redis 为什么这么快?

初中级回答:基于内存操作;单线程避免上下文切换;高效数据结构。

高级回答

"Redis 快的原因有几个层面:第一,纯内存操作,避免磁盘 IO;第二,单线程模型避免了多线程的上下文切换和锁竞争;第三,高效的数据结构设计,如跳表、压缩列表、整数集合等;第四,IO 多路复用(epoll)处理大量并发连接。但要注意,Redis 6.0 之后引入了多线程,主要是为了处理网络 IO 的并行化,命令执行还是单线程的。多线程处理网络读写,单线程执行命令,这样既利用了多核 CPU 的网络处理能力,又避免了命令执行的锁竞争。"

Q2:Redis 单线程为什么能处理高并发?

高级回答

"Redis 用的是 IO 多路复用(epoll),一个线程就能同时监听大量 socket 连接。当某个连接有数据可读时,epoll 会通知 Redis,Redis 再去读取和处理。这样即使单线程也能处理数万并发连接。加上 Redis 的命令执行大多是 O(1) 或 O(logN) 的,单次执行时间极短(微秒级),所以吞吐量非常高,单实例能到 10W+ QPS。"

Q3:Redis 6.0 为什么引入多线程?

高级回答

"Redis 6.0 的多线程是为了解决网络 IO 瓶颈。当 QPS 非常高时,网络读写成为瓶颈——单线程处理网络 IO 来不及了。6.0 引入了线程池来处理网络读写和协议解析,但命令执行仍然是单线程的,所以不需要同步锁,保持了原有的简单性。本质上是用多线程加速 IO,命令执行依然是单线程保证原子性。"

Q4:Redis 的过期策略和内存淘汰有什么区别?

高级回答

"这是两个不同的机制。过期策略是处理设了 TTL 的 Key,有惰性删除(访问时检查)和定期删除(每 100ms 随机检查一批)两种。内存淘汰是 maxmemory 达到上限时的策略,和过期策略无关——淘汰策略决定哪些 Key 被踢出,可能是有 TTL 的也可能是没有的。简单说,过期是时间到了自动删,淘汰是内存不够了主动踢。"

Q5:Redis 如何实现分布式锁?有什么问题?

(详见第 3 章分布式锁部分,面试时重点说:SET NX EX + Lua 删锁 + Redisson Watch Dog + RedLock 争议)

Q6:缓存和数据库一致性怎么保证?

(详见 7.3 节,面试时重点说 Canal + Binlog 方案)

Q7:Redis Cluster 的数据分片原理?

高级回答

"Redis Cluster 用哈希分片,16384 个 slot 分配到各主节点。Key 通过 CRC16(key) % 16384 计算所属 slot,找到对应节点。如果访问了错误的节点,会返回 MOVED 重定向。在 slot 迁移过程中会返回 ASK 重定向。节点间通过 Gossip 协议交换状态,PING/PONG 做心跳,PFAIL/FAIL 做故障检测。"

Q8:什么是缓存穿透?怎么解决?

(详见第 2 章,面试时重点说布隆过滤器原理 + 空值缓存方案)

Q9:Redis 持久化怎么选?

(详见第 4 章,面试时推荐混合持久化,说明 RDB + AOF 各自优缺点)

Q10:Redis 内存淘汰策略你用的哪个?为什么?

高级回答

"我们用的是 allkeys-lfu。Redis 4.0 引入的 LFU 比 LRU 更合理——LRU 只看最近访问时间,一个偶尔被访问的 Key 可能长期不被淘汰;LFU 综合考虑了访问频率,更能反映 Key 的热度。实际测试中 LFU 的缓存命中率比 LRU 高 3~5%。当然如果 Key 都是均匀访问的,两者差别不大,但真实业务中访问频率往往符合幂律分布,LFU 优势明显。"

Q11:如何发现和处理大 Key?

高级回答

"发现方面:我们用 redis-cli --bigkeys 做初步扫描,更精确的会用 redis-rdb-tools 分析 RDB 文件,生成大 Key 报告。处理方面:首先业务层面避免大 Key 设计——Hash 超过 5000 个字段就考虑拆分;其次删除时用 UNLINK 异步删除,避免 DEL 阻塞主线程;对于已经存在的大 Key,用 HSCAN 分批删除。我们还会在监控系统里加大 Key 告警,超过阈值自动通知。"

Q12:热 Key 问题怎么解决?

高级回答

"热 Key 会导致单节点压力过大。我们的方案是多层防护:第一层是本地缓存(Caffeine),设置很短的过期时间(3~5 秒),挡住绝大部分请求;第二层是 Redis 读写分离,把读请求分散到从节点;第三层是热 Key 探测框架(如京东 hotkey),自动检测热 Key 并在多个节点创建副本。同时我们会在监控里加热点 Key 探测,超过阈值的 Key 自动告警,运维可以提前处理。"

Q13:Redis 的 ZSet 底层为什么用跳表不用红黑树?

高级回答

"Redis 作者 Antirez 的解释有几个原因:第一,跳表实现更简单,代码可读性好,红黑树的旋转操作复杂;第二,跳表范围查询更友好——找到起点后沿着链表顺序遍历即可,红黑树需要中序遍历;第三,跳表通过调整层数可以改变时间复杂度,灵活性更好;第四,跳表的内存局部性更好,对 CPU 缓存更友好。虽然红黑树查找是严格的 O(logN),跳表期望 O(logN) 但最坏 O(N),但实际表现差不多。"

Q14:布隆过滤器的原理和局限?

高级回答

"布隆过滤器本质上是一个 bit 数组 + k 个哈希函数。插入元素时,用 k 个哈希函数计算 k 个位置,全部置 1。查询时,检查 k 个位置是否全为 1——全为 1 才认为可能存在。局限:1)不支持删除(标准版),因为删除可能影响其他元素;2)有误判率,可能把不存在的判断为存在,但不会把存在的判断为不存在;3)随着元素增多,误判率上升。生产上我们用 RedisBloom 模块,支持动态扩容和计数布隆过滤器(支持删除)。"

Q15:Redis 主从复制的全量同步流程?

高级回答

"首次同步或断线太久(超过 repl-diskless-sync-max-replicas)会触发全量同步:1)从节点发送 PSYNC 命令,主节点判断需要全量同步回复 ? -1;2)主节点执行 BGSAVE fork 子进程生成 RDB 快照,期间主进程继续处理请求,新命令写入 repl_backlog;3)RDB 生成后发送给从节点,从节点加载;4)主节点把 repl_backlog 中的增量命令发给从节点执行。增量复制时走 PSYNC 的偏移量机制,从节点告诉主节点自己的偏移量,主节点从 repl_backlog 中找到对应位置发送增量数据。"

Q16:Watch Dog 续期机制如果 Redis 主节点挂了怎么办?

高级回答

"Watch Dog 续期依赖于客户端和 Redis 的连接。如果用的是单节点 + 哨兵,主节点挂了触发故障转移,客户端连接到新主节点后可以重新加锁。如果用的是 Cluster,原理类似。但如果锁还没续期成功主节点就挂了,可能出现两个客户端同时拿到锁的情况——这就是 RedLock 要解决的问题。不过 RedLock 本身也有争议,所以我们生产上更倾向于接受这种极端情况下的短暂不一致,或者在业务层做幂等性保障。"

Q17:Redis 的 Pipeline 和 Lua 脚本有什么区别?

高级回答

"Pipeline 是客户端批量发送命令,减少网络 RTT,但服务端是逐条执行的,不保证原子性——中间可能插入其他客户端的命令。Lua 脚本在服务端原子执行,执行期间不会处理其他命令,保证原子性。但 Lua 脚本如果执行时间太长会阻塞主线程(Redis 有 busy-command 超时机制 5 秒限制)。选型建议:只需要减少网络往返用 Pipeline,需要原子性用 Lua。"

Q18:如何保证 Redis 和 MySQL 的数据不丢?

高级回答

"这是两个维度的问题。Redis 不丢数据靠持久化——我们用混合持久化(RDB + AOF),最多丢 1 秒数据,再加上 RDB 定期备份到 OSS 做灾备。MySQL 不丢靠主从复制 + binlog。两者之间的一致性靠 Canal 订阅 binlog 异步更新缓存。极端场景比如 Canal 延迟期间读到的可能是旧数据,业务层通过版本号或乐观锁做最终一致性保障。"

Q19:Redis 的 IO 多路复用原理?

高级回答

"Redis 用的是 epoll(Linux)作为 IO 多路复用机制。核心思想是一个线程同时监听多个 socket 描述符,当某个 socket 有数据可读或可写时,epoll 会返回就绪的描述符列表。Redis 主循环不断调用 epoll_wait 获取就绪事件,然后依次处理。这样单线程就能高效处理数万并发连接,避免了每个连接一个线程的开销。相比 select 和 poll,epoll 的时间复杂度是 O(1)(基于红黑树 + 就绪链表),效率更高。"

Q20:Redis 集群模式下,客户端怎么知道数据在哪个节点?

高级回答

"客户端本地维护一个 slot → 节点的映射表。请求时 CRC16(key) % 16384 算出 slot,查映射表找到目标节点直接发送。如果节点信息过期,目标节点会返回 MOVED 重定向,客户端更新本地映射表。如果 slot 正在迁移中,返回 ASK 重定向到目标节点,但客户端不更新映射表(因为迁移还没完成)。Redis 的 Java 客户端如 Jedis、Lettuce 都实现了这个逻辑。我们生产用 Lettuce,它基于 Netty,性能更好,而且天然支持异步和 Cluster 模式。"


附录:Redis 面试速记口诀

数据类型五老三新四,底层编码随量变
穿透布隆挡假ID,击穿互斥锁热点
雪崩随机TTL多级缓存,持久化混合最安全
分布式锁用Redisson,WatchDog自动续期
主从全量增量repllog,哨兵自动做切换
Cluster 16384槽gossip传,淘汰策略lfu最科学
热Key本地缓存挡,大Key拆分UNLINK删
一致性Canal订阅binlog,多级缓存层层防

📌 使用建议

  1. 面试前 1 天:重点看第 2、3、7 章(缓存三兄弟、分布式锁、高可用)

  2. 面试前 3 天:通读全文,每个知识点看「面试怎么说」

  3. 面试前 1 周:系统学习,配合实际项目经验,把知识点串成故事

  4. 面试时:先给结论,再展开原理,最后说生产实践


本文档由面试备战经验整理,持续更新中。


本内容由 Coze AI 生成,请遵循相关法律法规及《人工智能生成合成内容标识办法》使用与传播。

技术面试笔记 / 04_Redis高级场景速查手册_2026 0 0 cosolar
2026-08-30T02:14:22.267711034Z 2026-08-30T02:20:18.777011224Z