跳转至

11-Redis深度专题

Redis深度专题

🎯 学习目标

通过本章学习,你将能够:

  • 深入理解 Redis五大基础数据结构及其底层实现(SDS、ziplist、quicklist、skiplist、intset)
  • 掌握 Redis持久化机制(RDB、AOF、混合持久化)的原理与选型
  • 理解 Redis高可用架构(主从复制、哨兵、Cluster集群)的设计与运维
  • 精通 Redis缓存设计模式与缓存穿透/击穿/雪崩的解决方案
  • 实现 分布式锁、限流、排行榜、秒杀系统等高频实战场景
  • 掌握 Redis性能优化与大Key/热Key问题的排查处理
  • 应对 大厂Redis相关面试题,达到高薪后端/AI工程师面试水平

⏱️ 预计学习时间:8-10小时 📋 前置要求:06-NoSQL数据库 基础了解,熟悉基本的Redis操作


目录


第一部分:Redis基础与数据结构

1.1 Redis特点与使用场景总览

Redis(Remote Dictionary Server)是一个基于内存的高性能键值存储系统,具有以下核心特点:

特点 说明
高性能 基于内存操作,单线程模型(Redis 6.0之前),读写速度极快(10万+ QPS)
丰富的数据结构 String、List、Hash、Set、ZSet 五大基础类型 + Bitmap、HyperLogLog、GeoSpatial、Stream
持久化 支持 RDB 快照和 AOF 日志两种持久化方式
高可用 支持主从复制、哨兵、Cluster 集群
原子操作 单个命令原子执行,支持 Lua 脚本保证多命令原子性
丰富的特性 发布订阅、事务、Pipeline、慢查询日志等

为什么Redis是单线程还这么快?

  1. 纯内存操作:数据存储在内存中,读写耗时在纳秒级
  2. IO多路复用:使用 epoll/kqueue 等多路复用技术,单线程处理大量并发连接
  3. 单线程避免上下文切换:无锁竞争,无线程切换开销
  4. 高效数据结构:专门优化的数据结构(如 SDS、ziplist、skiplist)

⚠️ 注意:Redis 6.0 引入了多线程 I/O,但命令执行仍然是单线程。多线程仅用于网络 I/O 读写,解决网络 I/O 瓶颈。

常见使用场景

Text Only
┌──────────────────────────────────────────────────────────────────┐
│                     Redis 典型使用场景                            │
├──────────────┬───────────────────────────────────────────────────┤
│ 缓存         │ 数据库查询缓存、页面缓存、Session 缓存              │
│ 计数器       │ 文章阅读量、点赞数、在线人数                        │
│ 排行榜       │ 游戏排行、热搜排名(ZSet)                         │
│ 分布式锁     │ 秒杀、库存扣减、幂等性控制                          │
│ 消息队列     │ 异步任务处理、事件通知                               │
│ 限流         │ API限流、防刷、滑动窗口限流                         │
│ 社交关系     │ 共同好友、关注列表(Set交集)                        │
│ 地理位置     │ 附近的人、距离计算(GeoSpatial)                    │
│ 位图统计     │ 用户签到、在线状态(Bitmap)                        │
│ UV统计       │ 独立访客统计(HyperLogLog)                         │
│ AI/ML缓存    │ 特征缓存、模型推理结果缓存、embedding向量缓存        │
└──────────────┴───────────────────────────────────────────────────┘

1.2 五大基础数据类型深入

1.2.1 String(字符串)

底层实现:SDS(Simple Dynamic String)

Redis没有直接使用C语言的字符串(以\0结尾的字符数组),而是自己实现了SDS:

C
// Redis 3.2+ 使用多种 sdshdr 类型以节省内存
// 示例:sdshdr8(适用于长度 <256 的字符串)
struct __attribute__ ((__packed__)) sdshdr8 {  // struct结构体:自定义复合数据类型
    uint8_t len;      // 已使用长度
    uint8_t alloc;    // 分配的总空间(不含头和\0)
    unsigned char flags; // 低3位表示类型(sdshdr8/16/32/64)
    char buf[];       // 字节数组,保存实际字符串
};
// 还有 sdshdr16, sdshdr32, sdshdr64 用于更长的字符串

SDS相比C字符串的优势

特性 C字符串 SDS
获取长度 O(n) 遍历 O(1) 直接读 len
缓冲区溢出 可能发生 自动扩容,杜绝溢出
内存分配 每次修改都分配 空间预分配 + 惰性释放
二进制安全 不支持(\0截断) 支持(按 len 判断结尾)

空间预分配策略: - 修改后 SDS 长度 < 1MB:分配同等大小的 free 空间(len == free) - 修改后 SDS 长度 ≥ 1MB:分配 1MB free 空间

惰性空间释放:缩短 SDS 时不立即回收内存,而是记录到 free 中,供后续使用。

编码方式: - int:存储整数值(如 SET counter 100) - embstr:字符串长度 ≤ 44 字节,SDS 和 redisObject 在一块连续内存中 - raw:字符串长度 > 44 字节,SDS 和 redisObject 分开存储

常用命令

Text Only
# 基本操作
SET name "redis"              # 设置值
GET name                      # 获取值 → "redis"
SETNX lock:order "1"          # 不存在时才设置(分布式锁基础)
SETEX session:token 3600 "abc" # 设置带过期时间的值

# 计数器
INCR article:1001:views       # 自增 1
INCRBY article:1001:views 10  # 自增 10
DECR stock:sku123             # 自减 1

# 批量操作
MSET k1 v1 k2 v2 k3 v3       # 批量设置
MGET k1 k2 k3                # 批量获取

# 位操作(String底层也支持位操作)
SETBIT sign:user:1001:202601 5 1  # 用户1001在2026年1月第6天签到
GETBIT sign:user:1001:202601 5    # 查询是否签到 → 1
BITCOUNT sign:user:1001:202601    # 统计本月签到天数

📋 面试要点:String 底层用 SDS 实现,核心优势是空间预分配和惰性释放减少内存分配次数,O(1) 获取长度,二进制安全。注意 embstr 和 raw 编码的 44 字节分界线。


1.2.2 List(列表)

底层实现演变

Text Only
Redis 3.2 之前:ziplist(压缩列表) + linkedlist(双向链表)
Redis 3.2 之后:quicklist(快速列表)= 双向链表 + ziplist 的组合
Redis 7.0 之后:quicklist = 双向链表 + listpack(替代 ziplist)

ziplist(压缩列表): - 连续内存块存储,类似数组 - 省内存,但修改时可能触发连锁更新(cascade update) - 适合少量小元素

quicklist(快速列表): - 双向链表,每个节点是一个 ziplist - 兼顾了链表插入快和 ziplist 省内存的优势 - 通过 list-max-ziplist-size 控制每个 ziplist 节点的大小

Text Only
quicklist 结构示意:

head ←→ [ziplist1] ←→ [ziplist2] ←→ [ziplist3] ←→ tail
         ↑ entry     ↑ entry      ↑ entry
         ↑ entry     ↑ entry      ↑ entry
         ↑ entry

常用命令

Text Only
# 队列操作(FIFO)
LPUSH queue:task "task1" "task2" "task3"  # 左端入队
RPOP queue:task                           # 右端出队 → "task1"

# 栈操作(LIFO)
LPUSH stack:undo "action1" "action2"
LPOP stack:undo                           # → "action2"

# 阻塞弹出(消费者等待)
BRPOP queue:task 30                       # 阻塞30秒等待元素

# 范围查询
LRANGE mylist 0 -1                        # 获取所有元素
LRANGE mylist 0 9                         # 获取前10个元素
LLEN mylist                               # 获取长度

# 固定长度列表(最新N条消息)
LPUSH latest:news "news1"
LTRIM latest:news 0 99                    # 只保留最新100条

1.2.3 Hash(哈希)

底层实现: - ziplist/listpack:field 数量少(默认 < 512)且每个 value 小(默认 < 64 bytes)时使用 - hashtable(哈希表):超过上述阈值后转换

渐进式 rehash

当哈希表需要扩容或缩容时,Redis 不会一次性完成所有键的迁移(避免长时间阻塞),而是采用渐进式 rehash:

Text Only
步骤:
1. 为 ht[1] 分配空间(扩容时为 ht[0].used * 2 的最小 2^n)
2. 设置 rehashidx = 0,标记 rehash 开始
3. 在每次对字典执行 CRUD 操作时,顺带将 ht[0] 中 rehashidx 索引上的所有键值对迁移到 ht[1]
4. rehashidx++
5. 当 ht[0] 全部迁移完成,将 ht[1] 设为 ht[0],释放旧表

期间:
- 查找/删除/更新:先查 ht[0],再查 ht[1]
- 新增:只写入 ht[1](保证 ht[0] 只减不增)

常用命令

Text Only
# 存储对象
HSET user:1001 name "张三" age 25 city "北京"
HGET user:1001 name              # → "张三"
HMGET user:1001 name age city    # 批量获取
HGETALL user:1001                # 获取所有字段

# 计数(适合统计场景)
HINCRBY user:1001 login_count 1  # 登录次数+1

# 判断字段是否存在
HEXISTS user:1001 email          # → 0(不存在)

# 获取所有字段名/值
HKEYS user:1001                  # → ["name", "age", "city"]
HVALS user:1001                  # → ["张三", "25", "北京"]

📋 面试要点:Hash 的渐进式 rehash 是高频考点。面试官会问:为什么不一次性 rehash?(阻塞主线程)rehash 期间数据怎么查?(两个哈希表都查)新数据写到哪?(ht[1])


1.2.4 Set(集合)

底层实现: - intset(整数集合):当所有元素都是整数且数量少(默认 < 512)时使用,内存紧凑,升级机制(int16 → int32 → int64) - hashtable:超过阈值或包含非整数元素时使用

intset升级机制: 当新插入的整数类型比现有编码更长(如插入 int32 到 int16 集合),整个集合会升级编码,但不支持降级

常用命令

Text Only
# 基本操作
SADD tags:article:1001 "Redis" "数据库" "缓存"
SMEMBERS tags:article:1001      # 获取所有成员
SISMEMBER tags:article:1001 "Redis"  # 判断是否存在 → 1
SCARD tags:article:1001         # 获取成员数 → 3

# 社交关系:共同关注
SADD follow:user:A "user1" "user2" "user3"
SADD follow:user:B "user2" "user3" "user4"
SINTER follow:user:A follow:user:B    # 交集 → "user2" "user3"
SUNION follow:user:A follow:user:B    # 并集
SDIFF follow:user:A follow:user:B     # A有B没有的 → "user1"

# 随机元素(抽奖场景)
SRANDMEMBER lottery:pool 3      # 随机抽3个(不删除)
SPOP lottery:pool 1             # 随机弹出1个(删除)

1.2.5 ZSet(有序集合)

底层实现: - ziplist/listpack:元素少(默认 < 128)且每个元素小(默认 < 64 bytes) - skiplist(跳表)+ hashtable:超过阈值后使用

跳表(skiplist)原理

跳表是一种可以替代平衡树的数据结构,通过多层索引实现 O(log n) 的查找效率:

Text Only
Level 3:  1 ──────────────────────────────→ 9
Level 2:  1 ──────────→ 5 ────────────────→ 9
Level 1:  1 ───→ 3 ───→ 5 ───→ 7 ────────→ 9
Level 0:  1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9  (原始链表)

为什么用跳表而不是红黑树/B+树? - 实现简单,易理解维护 - 范围查询效率高(在找到起点后顺序遍历即可) - 插入/删除无需复杂的旋转/分裂操作 - 通过随机层数实现概率平衡,不需要严格平衡

常用命令

Text Only
# 排行榜
ZADD leaderboard 98.5 "player:A" 95.0 "player:B" 99.0 "player:C"

# 获取排名(从高到低)
ZREVRANK leaderboard "player:C"        # → 0(第1名)
ZREVRANGE leaderboard 0 9 WITHSCORES   # Top 10 及分数

# 获取分数
ZSCORE leaderboard "player:A"          # → 98.5

# 分数范围查询
ZRANGEBYSCORE leaderboard 90 100       # 90-100分的成员
ZCOUNT leaderboard 90 100              # 90-100分的成员数

# 增加分数
ZINCRBY leaderboard 2.5 "player:B"     # B加2.5分

# 集合运算(加权排行汇总)
ZUNIONSTORE total:rank 2 rank:math rank:english WEIGHTS 0.6 0.4

📋 面试要点:ZSet 底层跳表是面试高频题。必须掌握:跳表查找/插入/删除的时间复杂度 O(log n),空间复杂度 O(n);为什么 Redis 选跳表而不选红黑树(范围查询友好、实现简单)。


1.3 高级数据类型

1.3.1 Bitmap(位图)

Bitmap 本质上是 String 类型,但提供了位操作命令,适合大规模布尔值存储场景。

典型场景:用户签到

Text Only
# 用户1001在2026年1月的签到记录
# 第1天签到
SETBIT sign:1001:202601 0 1
# 第3天签到
SETBIT sign:1001:202601 2 1
# 第5天签到
SETBIT sign:1001:202601 4 1

# 查询第3天是否签到
GETBIT sign:1001:202601 2     # → 1

# 统计本月签到天数
BITCOUNT sign:1001:202601     # → 3

# 查询本月首次签到位置
BITPOS sign:1001:202601 1     # → 0(第1天)

# 多天签到统计(AND运算:连续签到)
BITOP AND result sign:1001:202601 sign:1001:202602

内存优势:存储1亿用户的签到状态仅需 12MB(1亿 bit ≈ 12MB),远小于用一个 key-value 存储。

1.3.2 HyperLogLog(基数估算)

用于不精确的去重计数,误差率约 0.81%,但每个 HyperLogLog 仅占 12KB 内存。

典型场景:UV(独立访客)统计

Text Only
# 记录页面访问
PFADD page:index:uv "user1" "user2" "user3" "user1"  # user1重复
PFCOUNT page:index:uv                                  # → 3(去重后)

# 合并多个页面的UV
PFADD page:about:uv "user2" "user4"
PFMERGE total:uv page:index:uv page:about:uv
PFCOUNT total:uv                                       # → 4

适用场景:当不需要精确计数、数据量巨大时使用。如日活用户数、页面 UV、搜索关键词去重统计。

1.3.3 GeoSpatial(地理位置)

底层使用 ZSet 实现,将经纬度编码为 GeoHash 作为 score。

Text Only
# 添加地理位置
GEOADD shops 116.403963 39.915119 "北京烤鸭店"
GEOADD shops 116.413413 39.908692 "火锅店"
GEOADD shops 121.472644 31.231706 "上海小笼包"

# 查看位置
GEOPOS shops "北京烤鸭店"

# 计算两点距离
GEODIST shops "北京烤鸭店" "火锅店" km   # → ~1.2 km

# 搜索附近门店(以某坐标为圆心,5km半径)
GEOSEARCH shops FROMLONLAT 116.403963 39.915119 BYRADIUS 5 km ASC COUNT 10

1.3.4 Stream(消息队列)

Redis 5.0 引入的专业消息队列数据类型,支持消费者组,可替代简单的消息中间件。

Text Only
# 生产者:发送消息
XADD stream:orders * user_id 1001 product_id "SKU123" amount 2

# 消费者:读取消息
XREAD COUNT 10 BLOCK 5000 STREAMS stream:orders 0
# BLOCK 5000: 阻塞5秒等待新消息

# 创建消费者组
XGROUP CREATE stream:orders group1 0

# 消费者组消费(不同消费者读取不同消息)
XREADGROUP GROUP group1 consumer1 COUNT 5 BLOCK 2000 STREAMS stream:orders >
XREADGROUP GROUP group1 consumer2 COUNT 5 BLOCK 2000 STREAMS stream:orders >

# 确认消息已处理
XACK stream:orders group1 "1624000000000-0"

# 查看待处理消息(消费者crash后可重新处理)
XPENDING stream:orders group1 - + 10

Stream vs List 做消息队列

特性 List Stream
消费者组
消息确认(ACK)
消息持久化
消息回溯 ✅(可按ID回溯)
阻塞读取 ✅ BRPOP ✅ XREAD BLOCK

第二部分:Redis持久化

Redis是内存数据库,服务器重启数据会丢失,因此需要持久化机制将数据保存到磁盘。

2.1 RDB快照(Redis Database)

RDB 将某个时间点的所有数据生成快照(snapshot),保存为一个紧凑的二进制文件(dump.rdb)。

触发时机

Text Only
# 方式1:手动触发(阻塞主线程,生产环境禁用)
SAVE

# 方式2:手动触发(fork子进程执行,推荐)
BGSAVE

# 方式3:自动触发(redis.conf 配置)
# save 900 1      # 900秒内至少1次修改
# save 300 10     # 300秒内至少10次修改
# save 60 10000   # 60秒内至少10000次修改

BGSAVE 执行流程(fork + COW)

Text Only
1. 主进程调用 fork() 创建子进程
2. fork 使用 Copy-On-Write(写时复制)机制:
   - 子进程与父进程共享内存页
   - 只有当父进程修改某个内存页时,才会复制该页
3. 子进程遍历所有数据,写入 RDB 文件
4. 子进程完成后,用新 RDB 文件替换旧文件

                 ┌──────────────────────┐
                 │      主进程           │
                 │  继续处理客户端请求    │
                 └──────┬───────────────┘
                        │ fork()
                 ┌──────▼───────────────┐
                 │      子进程           │
                 │  遍历内存 → dump.rdb  │
                 │  (Copy-On-Write)    │
                 └──────────────────────┘

RDB优缺点

优点 缺点
紧凑的二进制文件,备份方便 两次快照之间的数据可能丢失
恢复速度快(直接加载) fork 大内存实例时可能阻塞主线程
子进程执行,不影响主进程 不适合实时持久化

2.2 AOF日志(Append Only File)

AOF 记录每一条写命令,以日志追加的方式保存(类似 MySQL 的 binlog)。

写后日志:Redis 先执行命令,再写 AOF 日志(避免语法检查开销,不阻塞当前命令执行)。

三种同步策略

Text Only
┌──────────────┬──────────────────────────────────┬────────────────────┐
│ 策略          │ 说明                              │ 数据安全性          │
├──────────────┼──────────────────────────────────┼────────────────────┤
│ always       │ 每条命令都 fsync 到磁盘             │ 最安全,最多丢1条    │
│ everysec     │ 每秒 fsync 一次(推荐)             │ 最多丢1秒数据        │
│ no           │ 由操作系统决定何时 fsync            │ 性能最好,可能丢较多  │
└──────────────┴──────────────────────────────────┴────────────────────┘

AOF重写(bgrewriteaof)

随着时间推移,AOF 文件会越来越大。AOF 重写将多条命令合并为等效的最少命令:

Text Only
# 重写前(AOF文件记录了多次操作):
SET name "A"
SET name "B"
SET name "C"
INCR counter
INCR counter
INCR counter

# 重写后(等效的最少命令):
SET name "C"
SET counter 3

重写过程: 1. 主进程 fork 子进程 2. 子进程根据内存中的当前数据生成新 AOF 3. 重写期间,主进程的新命令同时写入旧 AOF 和 AOF 重写缓冲区 4. 子进程完成后,主进程将重写缓冲区的内容追加到新 AOF 5. 用新 AOF 替换旧 AOF


2.3 混合持久化(Redis 4.0+)

混合持久化结合了 RDB 和 AOF 的优点:

Text Only
开启方式:
aof-use-rdb-preamble yes

原理:
AOF 重写时,文件前半部分是 RDB 格式(全量数据快照),
后半部分是 AOF 格式(重写期间的增量命令)。

┌─────────────────────────────────────┐
│          混合持久化 AOF 文件          │
├─────────────────────┬───────────────┤
│   RDB 格式数据      │  AOF 增量命令  │
│  (快速加载全量)    │ (追加增量)    │
└─────────────────────┴───────────────┘

优势:
- 加载速度接近纯 RDB(快)
- 数据安全性接近纯 AOF(丢失少)

2.4 持久化策略选型建议

场景 推荐方案 原因
纯缓存,数据丢失可接受 关闭持久化 最高性能
数据重要,允许分钟级丢失 RDB 性能好,恢复快
数据重要,最多秒级丢失 AOF (everysec) 安全性高
既要安全又要恢复快 混合持久化(推荐) 两者优点结合
AI 场景特征缓存 RDB 或关闭 特征可从数据库重建

📋 面试要点:务必掌握 RDB 和 AOF 的区别、各自的优缺点、fork + COW 原理、AOF 重写流程。混合持久化是 Redis 4.0 后推荐方案。


第三部分:Redis高可用架构

3.1 主从复制

主从复制是 Redis 高可用的基础,实现读写分离数据备份

Text Only
写请求 → [Master] ──同步──→ [Slave 1] ← 读请求
                  ──同步──→ [Slave 2] ← 读请求
                  ──同步──→ [Slave 3] ← 读请求

全量同步(PSYNC,首次连接或 repl_backlog 溢出时)

Text Only
1. Slave 发送 PSYNC ? -1(首次请求全量同步)
2. Master 执行 BGSAVE 生成 RDB
3. Master 将 RDB 发送给 Slave
4. 生成 RDB 期间的写命令存入 repl_backlog_buffer
5. Slave 加载 RDB
6. Master 将 repl_backlog_buffer 中的命令发送给 Slave
7. 后续进入增量同步

增量同步

Text Only
- Master 的写命令实时传播给 Slave
- 使用 repl_backlog_buffer(环形缓冲区)记录最近的写命令
- Slave 断线重连后,通过 offset 判断是否可以增量同步
- 如果 offset 还在 buffer 内 → 增量同步(发送差量数据)
- 如果 offset 已超出 buffer → 全量同步(重新 RDB)

建议:适当调大 repl-backlog-size(默认1MB → 建议64MB-256MB)

无盘复制(diskless replication): Master 直接将 RDB 通过 socket 发给 Slave,不落盘,适合磁盘 IO 慢但网络好的场景。 配置:repl-diskless-sync yes


3.2 哨兵 Sentinel

哨兵是 Redis 官方的高可用方案,自动完成故障检测故障转移

Text Only
           ┌────────────┐
           │ Sentinel 1 │
           └─────┬──────┘
                 │ 监控
 ┌───────────────┼───────────────┐
 │               │               │
 ▼               ▼               ▼
[Master]     [Slave 1]      [Slave 2]
          自动故障转移
          [New Master]

故障检测

  1. 主观下线(SDOWN):某个 Sentinel 认为 Master 不可达(超时未响应 PING)
  2. 客观下线(ODOWN)quorum 个 Sentinel 都认为 Master 不可达才触发故障转移

Leader 选举(Raft 协议简化版)

Text Only
1. 发现 Master 客观下线的 Sentinel 发起选举
2. 向其他 Sentinel 请求投票(RequestVote)
3. 每个 Sentinel 在每个 epoch 只投一票(先到先得)
4. 获得多数票(> N/2 + 1)的 Sentinel 成为 Leader
5. Leader 执行故障转移

故障转移流程

Text Only
1. Leader Sentinel 从所有 Slave 中选择新 Master:
   - 排除已下线的 Slave
   - 选择 slave-priority 最小的(优先级最高)
   - 选择 offset 最大的(数据最新)
   - 选择 run_id 最小的(启动最早)
2. 向选出的 Slave 发送 SLAVEOF NO ONE(升级为 Master)
3. 向其他 Slave 发送 SLAVEOF 新Master(切换复制目标)
4. 继续监控旧 Master,恢复后设为 Slave

最小配置示例

Bash
# sentinel.conf(至少3个Sentinel实例)
port 26379
sentinel monitor mymaster 127.0.0.1 6379 2   # quorum=2
sentinel down-after-milliseconds mymaster 5000 # 超时5秒判定主观下线
sentinel failover-timeout mymaster 60000       # 故障转移超时60秒
sentinel parallel-syncs mymaster 1             # 故障转移后同时同步的Slave数

3.3 Redis Cluster 集群

Redis Cluster 是 Redis 的分布式解决方案,支持数据分片和自动故障转移。

哈希槽(Hash Slot)

Text Only
Redis Cluster 将整个数据空间划分为 16384 个哈希槽(slot)。
每个 key 通过 CRC16(key) % 16384 确定所属槽。
每个节点负责一部分槽。

示例(3主3从):
┌──────────────────────────────────────────────────────────────┐
│  Node A (Master)     Node B (Master)     Node C (Master)    │
│  Slot: 0-5460        Slot: 5461-10922    Slot: 10923-16383  │
│       │                    │                    │           │
│  Node A' (Slave)     Node B' (Slave)     Node C' (Slave)    │
└──────────────────────────────────────────────────────────────┘

为什么是 16384?
- 16384 = 2^14,CRC16 结果为 16 位,取模 16384 等价于取低14位
- 心跳包中 bitmap 大小为 16384/8 = 2KB,网络带宽友好
- 集群规模最多约1000个节点,16384个槽够用

节点通信:Gossip 协议

Text Only
- 每个节点维护集群状态(节点信息、槽分配)
- 节点间通过 Gossip 协议交换状态(PING/PONG消息)
- 每次 PING/PONG 携带自身信息 + 随机几个其他节点信息
- 优点:去中心化,容错性强
- 缺点:状态收敛需要时间(最终一致性)

ASK / MOVED 重定向

Text Only
客户端请求的 key 不在当前节点时:

MOVED 重定向(永久转移):
  → 说明该槽已经永久迁移到目标节点
  → 客户端更新本地槽映射表,后续直接请求目标节点

ASK 重定向(临时转移,迁移中):
  → 说明该槽正在从源节点迁移到目标节点
  → 客户端本次请求到目标节点(先发 ASKING 命令),但不更新映射表
  → 下次请求仍发到源节点

集群扩缩容

Bash
# 添加节点
redis-cli --cluster add-node 新节点IP:PORT 集群中任意节点IP:PORT

# 分配槽位(从现有节点迁移部分槽到新节点)
redis-cli --cluster reshard 集群IP:PORT

# 删除节点(先迁移槽位,再移除空节点)
redis-cli --cluster del-node 集群IP:PORT 节点ID

3.4 各架构方案对比与选型

方案 数据容量 高可用 水平扩展 复杂度 适用场景
单机 受内存限制 最低 开发测试
主从复制 受单机限制 手动切换 读扩展 读多写少,手动运维
哨兵 受单机限制 自动切换 读扩展 中小规模,自动HA
Cluster 分片扩展 自动切换 读写扩展 大数据量,高并发

📋 面试要点:Redis Cluster 的 16384 个哈希槽如何计算(CRC16 % 16384)、Gossip 协议的优缺点、MOVED 和 ASK 重定向区别、集群扩缩容流程。


第四部分:Redis高级特性

4.1 内存管理与淘汰策略

当 Redis 内存达到 maxmemory 限制时,需要通过淘汰策略决定删除哪些 key。

8种淘汰策略

Text Only
┌──────────────────────┬───────────────────────────────────────────────┐
│ 策略                  │ 说明                                         │
├──────────────────────┼───────────────────────────────────────────────┤
│ noeviction           │ 不删除,内存满时写操作返回错误(默认)           │
│ allkeys-lru          │ 从所有key中淘汰最近最少使用的(推荐)           │
│ volatile-lru         │ 从设置了过期时间的key中淘汰LRU                 │
│ allkeys-lfu          │ 从所有key中淘汰最不经常使用的(Redis 4.0+)    │
│ volatile-lfu         │ 从设置了过期时间的key中淘汰LFU                 │
│ allkeys-random       │ 从所有key中随机淘汰                           │
│ volatile-random      │ 从设置了过期时间的key中随机淘汰                │
│ volatile-ttl         │ 淘汰剩余TTL最短的key                          │
└──────────────────────┴───────────────────────────────────────────────┘

LRU 近似算法:Redis 的 LRU 不是精确 LRU,而是采样近似。默认采样5个 key(maxmemory-samples 5),淘汰其中最久未使用的。增大采样数可提高精确度但增加 CPU 开销。

LFU(Least Frequently Used)原理:Redis 4.0 引入。使用 Morris 计数器记录访问频率,结合衰减因子使频率随时间递减,避免老旧热点 key 长期占据内存。

Text Only
# 查看 key 的 LRU/LFU 信息
OBJECT FREQ mykey           # LFU频率
OBJECT IDLETIME mykey       # LRU空闲时间(秒)

# 配置淘汰策略
CONFIG SET maxmemory 4gb
CONFIG SET maxmemory-policy allkeys-lfu

4.2 事务与Lua脚本

Redis事务

Text Only
# 基本事务
MULTI                       # 开启事务
SET account:A 1000
SET account:B 2000
EXEC                        # 执行事务(所有命令原子执行)

# 放弃事务
MULTI
SET key1 "value1"
DISCARD                     # 取消事务

# WATCH 乐观锁(CAS)
WATCH account:A             # 监视 key
balance = GET account:A     # 获取当前值
MULTI
SET account:A (balance - 100)
EXEC                        # 如果 account:A 在 WATCH 后被其他客户端修改,EXEC 返回 nil

⚠️ Redis 事务不支持回滚。如果 EXEC 中某条命令执行失败,其他命令仍会执行。这与关系型数据库的事务有本质区别。

Lua脚本

Lua 脚本在 Redis 中原子执行,适合实现复杂的原子操作(如分布式锁、限流)。

Text Only
# EVAL 执行 Lua 脚本
# 语法:EVAL script numkeys key1 key2 ... arg1 arg2 ...
EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 mykey myvalue

# 库存扣减(原子操作)
EVAL "
    local stock = tonumber(redis.call('GET', KEYS[1]))
    if stock > 0 then
        redis.call('DECR', KEYS[1])
        return 1
    end
    return 0
" 1 stock:sku123

# EVALSHA(缓存脚本,减少网络传输)
# 先用 SCRIPT LOAD 缓存脚本,获得 SHA1
SCRIPT LOAD "return redis.call('GET', KEYS[1])"
# → "a42059b356c875f0717db19a51f6aaa9161571a2"
EVALSHA "a42059b356c875f0717db19a51f6aaa9161571a2" 1 mykey

4.3 发布订阅 Pub/Sub

Text Only
# 订阅者(终端1)
SUBSCRIBE channel:news        # 订阅频道
PSUBSCRIBE channel:*          # 模式订阅(通配符)

# 发布者(终端2)
PUBLISH channel:news "Breaking: Redis 8.0 released!"

# 查看活跃频道
PUBSUB CHANNELS
PUBSUB NUMSUB channel:news   # 查看频道订阅数

⚠️ Pub/Sub 消息不持久化,如果订阅者不在线,消息会丢失。如需可靠消息,使用 Stream。


4.4 Pipeline 管道

Pipeline 将多个命令打包一次性发送,减少网络往返(RTT),大幅提升批量操作性能。

Python
# Python 示例:Pipeline批量操作
import redis

r = redis.Redis(host='localhost', port=6379)

# 不用Pipeline:1000次网络往返
for i in range(1000):
    r.set(f'key:{i}', f'value:{i}')    # 每次一个RTT

# 使用Pipeline:1次网络往返
pipe = r.pipeline()
for i in range(1000):
    pipe.set(f'key:{i}', f'value:{i}')  # 命令缓存在客户端
results = pipe.execute()                 # 一次性发送,一次RTT

# 性能对比:Pipeline 快 10-100 倍(取决于网络延迟)

⚠️ Pipeline 不是原子操作(中间可能穿插其他客户端命令)。需要原子性请用事务或 Lua 脚本。


4.5 慢查询日志分析

Text Only
# 配置慢查询
CONFIG SET slowlog-log-slower-than 10000  # 超过10ms记录(单位微秒)
CONFIG SET slowlog-max-len 128             # 最多保存128条

# 查看慢查询记录
SLOWLOG GET 10        # 获取最近10条慢查询
SLOWLOG LEN           # 当前慢查询条数
SLOWLOG RESET         # 清空慢查询日志

# 慢查询记录包含:
# 1) 日志ID  2) 发生时间戳  3) 耗时(微秒)  4) 命令及参数

常见慢查询原因: - KEYS *:全量扫描(应改用 SCAN 命令增量遍历) - 大 Key 操作:HGETALL 大 Hash、SMEMBERS 大 Set - SORT 复杂排序 - 不合理的 Lua 脚本


第五部分:Redis实战场景

5.1 分布式锁

基础方案:SETNX + EXPIRE

Text Only
# 错误:两条命令不是原子操作(SETNX成功后崩溃,EXPIRE未执行 → 死锁)
SETNX lock:order 1
EXPIRE lock:order 30

# 正确:使用 SET 命令的 NX 和 EX 选项(原子操作)
SET lock:order "request_id_123" NX EX 30
# NX: 不存在时才设置
# EX 30: 过期时间30秒

删除锁时的问题:必须验证锁是自己的(防止误删其他客户端的锁):

Text Only
# Lua 脚本原子地判断并删除
EVAL "
    if redis.call('GET', KEYS[1]) == ARGV[1] then
        return redis.call('DEL', KEYS[1])
    end
    return 0
" 1 lock:order "request_id_123"

Redisson 看门狗机制

Redisson 是 Java 生态最强大的 Redis 客户端,内置了完善的分布式锁实现。

Java
// Java Redisson 分布式锁
RLock lock = redissonClient.getLock("lock:order:1001");
try {  // try/catch捕获异常
    // 尝试获取锁,最多等待10秒,锁自动过期30秒
    boolean acquired = lock.tryLock(10, 30, TimeUnit.SECONDS);
    if (acquired) {
        // 执行业务逻辑
        processOrder();
    }
} finally {
    lock.unlock();
}

看门狗(Watchdog)机制: - 如果获取锁时没有指定过期时间,Redisson 默认设置30秒 - 后台有一个定时任务(看门狗),每隔 leaseTime / 3(10秒)自动续期 - 只要客户端还持有锁(线程存活),锁就不会过期 - 客户端崩溃 → 看门狗停止续期 → 锁超时自动释放

RedLock 算法

RedLock 用于在多个独立 Redis 实例上实现更可靠的分布式锁:

Text Only
步骤:
1. 获取当前时间戳 T1
2. 依次向 N 个独立 Redis 实例请求加锁(SET NX EX)
3. 如果在超过 N/2+1 个实例上加锁成功,且总耗时 < 锁过期时间
   → 加锁成功,实际有效时间 = 过期时间 - 加锁耗时
4. 否则,向所有实例释放锁

争议(Martin Kleppmann vs Antirez):
- 反对:依赖系统时钟同步、存在分布式系统time jump问题
- 支持:在大多数实际场景下足够可靠
- 结论:如果需要强一致性,建议使用 ZooKeeper 或 etcd

分布式锁方案对比

方案 性能 可靠性 实现复杂度 适用场景
Redis SETNX 极高 中(单点故障) 一般场景
Redisson 中高(看门狗) 低(封装好) Java 生态推荐
RedLock 中高(存在争议) 多实例部署
ZooKeeper 最高(CP) 金融等强一致场景
etcd 最高(CP) 云原生场景

5.2 缓存设计模式

Cache Aside(旁路缓存,最常用)

Text Only
读流程:
1. 先读缓存 → 命中则返回
2. 缓存未命中 → 读数据库 → 写入缓存 → 返回

写流程:
1. 先更新数据库
2. 再删除缓存(而不是更新缓存!)

为什么是"删除"缓存而不是"更新"?
- 避免并发写导致的缓存与DB不一致
- 缓存值可能是复杂计算结果,每次更新代价高
- 懒加载思想:下次读取时再重建缓存

延迟双删

Python
# 解决 先删缓存→再更新DB 期间的并发读问题
def update_data(key, value):
    # 1. 先删缓存
    redis.delete(key)
    # 2. 更新数据库
    db.update(key, value)
    # 3. 延迟一段时间(略大于一次读请求的耗时)
    time.sleep(0.5)
    # 4. 再次删缓存(删除并发读请求可能写入的旧缓存)
    redis.delete(key)

Read/Write Through

Text Only
应用程序只与缓存交互,缓存层负责与数据库同步。

Read Through:
  应用 → 缓存(未命中 → 缓存自动从DB加载)→ 返回数据

Write Through:
  应用 → 缓存(同步写入DB,确认后返回)→ 写入完成

优点:应用逻辑简单
缺点:缓存层实现复杂,写延迟高(同步写DB)

Write Behind / Write Back

Text Only
写操作只更新缓存,异步批量写入数据库。

应用 → 缓存(立即返回)
         ↓ 异步/批量
        数据库

优点:写性能极高
缺点:数据一致性差,缓存崩溃可能丢数据
适用:写密集且允许短暂不一致的场景(如日志、计数)

5.3 缓存问题三连

缓存穿透(查不存在的数据)

问题:大量请求查询数据库中不存在的数据,每次都穿透缓存打到数据库。

方案一:缓存空对象

Text Only
# 查询 DB,结果为空
# → 缓存空值,设置较短过期时间
SET user:99999 "" EX 300    # 缓存空值5分钟

缺点:缓存大量空值浪费内存、可能造成短暂不一致。

方案二:布隆过滤器(BloomFilter)

Text Only
原理:
1. 用一个位数组(bitmap)+ 多个哈希函数
2. 添加元素时,用多个哈希函数计算位置,将对应位设为1
3. 查询时,如果所有哈希位置都为1 → 可能存在(误判率约1%)
4. 如果任何位置为0 → 一定不存在

请求 → BloomFilter 判断 key 是否存在
  → 不存在:直接返回(拦截)
  → 可能存在:查缓存 → 查数据库
Python
# Python 使用 redis-py + RedisBloom
from redis.commands.bf import BFBloom

r = redis.Redis()
bf = r.bf()

# 创建布隆过滤器(误差率0.01,预计容量100万)
bf.create('user:filter', 0.01, 1000000)

# 添加已有的用户ID
bf.madd('user:filter', 'user:1', 'user:2', 'user:3')

# 查询
bf.exists('user:filter', 'user:99999')  # → False(一定不存在)
bf.exists('user:filter', 'user:1')      # → True(可能存在)

缓存击穿(热点Key过期)

问题:某个热点 key 过期瞬间,大量并发请求同时打到数据库。

方案一:互斥锁(Mutex Lock)

Python
def get_data(key):
    value = redis.get(key)
    if value is not None:
        return value

    # 缓存未命中,尝试获取互斥锁
    lock_key = f"lock:{key}"
    if redis.set(lock_key, "1", nx=True, ex=10):
        try:  # try/except捕获异常
            # 获取锁成功,查询数据库
            value = db.query(key)
            redis.set(key, value, ex=3600)  # 重建缓存
            return value
        finally:
            redis.delete(lock_key)
    else:
        # 获取锁失败,短暂等待后重试
        time.sleep(0.05)
        return get_data(key)  # 递归重试

方案二:逻辑过期

Python
# 缓存永不过期,但存储逻辑过期时间
cache_data = {
    "data": actual_data,
    "expire_time": time.time() + 3600  # 逻辑过期时间
}
redis.set(key, json.dumps(cache_data))  # 不设置 Redis TTL

def get_data(key):
    cache = json.loads(redis.get(key))  # json.loads将JSON字符串转为Python对象
    if cache['expire_time'] > time.time():
        return cache['data']  # 未逻辑过期

    # 逻辑过期,异步重建缓存
    if redis.set(f"lock:{key}", "1", nx=True, ex=10):
        # 获取锁成功,开启异步线程重建
        threading.Thread(target=rebuild_cache, args=(key,)).start()  # 线程池/多线程:并发执行任务

    return cache['data']  # 返回旧数据(短暂不一致)

缓存雪崩(大量Key同时过期)

问题:大量缓存 key 在同一时间过期,导致请求全部打到数据库。

解决方案

Python
import random

# 方案1:随机过期时间(打散过期时间点)
base_ttl = 3600  # 基础1小时
random_ttl = base_ttl + random.randint(0, 600)  # 加0-10分钟随机值
redis.set(key, value, ex=random_ttl)

# 方案2:多级缓存(本地缓存 + Redis + DB)
# L1: 本地缓存(Caffeine/Guava, 容量小,速度极快)
# L2: Redis(容量大,速度快)
# L3: 数据库
def get_data(key):
    # L1 本地缓存
    value = local_cache.get(key)
    if value: return value

    # L2 Redis
    value = redis.get(key)
    if value:
        local_cache.set(key, value, ttl=60)  # 本地缓存1分钟
        return value

    # L3 数据库
    value = db.query(key)
    redis.set(key, value, ex=3600)
    local_cache.set(key, value, ttl=60)
    return value

# 方案3:降级策略
# 当检测到数据库压力过大时,返回兜底数据/限流/熔断

📋 面试要点:缓存穿透/击穿/雪崩是必考题。务必掌握每种问题的定义、区别、解决方案。布隆过滤器原理和互斥锁方案要能默写。


5.4 限流方案

固定窗口计数器

Text Only
# 每分钟限制100次请求
SET rate:api:user1001 0 EX 60 NX   # 不存在时初始化
INCR rate:api:user1001              # 每次请求+1
# 如果结果 > 100,拒绝请求

缺点:临界值问题(窗口切换瞬间可能承受 2 倍流量)。

滑动窗口(ZSet实现)

Python
import time

def is_allowed(user_id, max_count, window_seconds):
    key = f"rate:{user_id}"
    now = time.time()
    window_start = now - window_seconds

    pipe = redis.pipeline()
    # 1. 移除窗口外的记录
    pipe.zremrangebyscore(key, 0, window_start)
    # 2. 统计窗口内的请求数
    pipe.zcard(key)
    # 3. 添加当前请求
    pipe.zadd(key, {str(now): now})
    # 4. 设置过期时间(防止冷用户数据堆积)
    pipe.expire(key, window_seconds)
    results = pipe.execute()

    current_count = results[1]
    return current_count < max_count

令牌桶算法(Lua脚本实现)

Text Only
-- 令牌桶限流 Lua 脚本
-- KEYS[1]: 令牌桶key
-- ARGV[1]: 桶容量(burst)
-- ARGV[2]: 令牌生成速率(每秒)
-- ARGV[3]: 当前时间戳

local key = KEYS[1]
local capacity = tonumber(ARGV[1])     -- 桶容量
local rate = tonumber(ARGV[2])          -- 令牌/秒
local now = tonumber(ARGV[3])

local last_time = tonumber(redis.call('hget', key, 'last_time') or now)
local tokens = tonumber(redis.call('hget', key, 'tokens') or capacity)

-- 计算新增令牌
local elapsed = math.max(0, now - last_time)
tokens = math.min(capacity, tokens + elapsed * rate)

if tokens >= 1 then
    tokens = tokens - 1
    redis.call('hset', key, 'tokens', tokens)
    redis.call('hset', key, 'last_time', now)
    redis.call('expire', key, capacity / rate * 2)
    return 1  -- 允许
else
    redis.call('hset', key, 'last_time', now)
    return 0  -- 拒绝
end

5.5 消息队列方案

方案 实现 优点 缺点 适用场景
List LPUSH + BRPOP 简单 无ACK、无消费者组 简单异步任务
Stream XADD + XREADGROUP 完整消息队列特性 Redis 5.0+ 中等规模消息队列
Kafka 独立中间件 超高吞吐、持久化、分区 重量级、运维复杂 大数据流处理
RabbitMQ 独立中间件 丰富路由、协议完善 吞吐量相对较低 企业级消息路由

建议:轻量消息队列用 Redis Stream,大规模数据流用 Kafka,复杂路由用 RabbitMQ。


5.6 排行榜(ZSet实现)

Text Only
# 游戏排行榜
ZADD game:leaderboard 1500 "player:A"
ZADD game:leaderboard 2300 "player:B"
ZADD game:leaderboard 1800 "player:C"
ZADD game:leaderboard 2100 "player:D"

# Top 3(分数从高到低)
ZREVRANGE game:leaderboard 0 2 WITHSCORES
# → "player:B" 2300, "player:D" 2100, "player:C" 1800

# 查看某玩家排名
ZREVRANK game:leaderboard "player:A"   # → 3(第4名,0-based)

# 增加分数
ZINCRBY game:leaderboard 500 "player:A"  # A加500分

# 获取某分数段的玩家
ZRANGEBYSCORE game:leaderboard 2000 3000

# 分页查询(第2页,每页10个)
ZREVRANGE game:leaderboard 10 19 WITHSCORES

5.7 秒杀系统中的Redis应用

Text Only
秒杀架构:

用户请求 → Nginx限流 → 网关层 → 秒杀服务 → Redis预扣库存 → MQ异步下单 → 数据库

Redis在秒杀中的角色:
1. 商品库存预热:提前将库存加载到Redis
2. 原子扣减库存:Lua脚本保证原子性
3. 用户去重:Set防止重复下单
4. 限流控制:滑动窗口限流
Text Only
-- 秒杀扣库存 Lua 脚本(原子操作)
-- KEYS[1]: 库存key  KEYS[2]: 订单集合key
-- ARGV[1]: 用户ID

-- 1. 检查用户是否已下单
if redis.call('sismember', KEYS[2], ARGV[1]) == 1 then
    return -1  -- 重复下单
end

-- 2. 检查库存
local stock = tonumber(redis.call('get', KEYS[1]))
if stock <= 0 then
    return 0  -- 库存不足
end

-- 3. 扣减库存
redis.call('decr', KEYS[1])
-- 4. 记录已下单用户
redis.call('sadd', KEYS[2], ARGV[1])
return 1  -- 下单成功,后续异步处理
Java
// Java 调用秒杀Lua脚本
String luaScript = "..."; // 上述Lua脚本
Long result = redisTemplate.execute(
    new DefaultRedisScript<>(luaScript, Long.class),
    Arrays.asList("seckill:stock:1001", "seckill:orders:1001"),
    userId
);

if (result == 1) {
    // 发送消息到MQ,异步创建订单
    rabbitTemplate.convertAndSend("seckill.exchange", "seckill.order",
        new OrderMessage(userId, productId));
} else if (result == 0) {
    throw new RuntimeException("库存不足");
} else {
    throw new RuntimeException("重复下单");
}

第六部分:Redis性能优化与运维

6.1 大Key问题

定义:Value 特别大的 Key。如 String > 10KB、Hash/Set/ZSet > 5000个元素。

危害: - 读写耗时大,阻塞其他请求 - 内存不均(Cluster中某节点内存远超其他节点) - 删除大Key时阻塞主线程(DEL 命令是同步的) - 网络带宽占用高

检测方法

Bash
# 方式1:redis-cli --bigkeys(只统计每种类型最大的key)
redis-cli --bigkeys

# 方式2:SCAN + TYPE + SIZE遍历(推荐生产环境)
redis-cli --scan --pattern '*' | while read key; do
    type=$(redis-cli type "$key")  # $()命令替换:执行命令并获取输出
    case $type in
        string) size=$(redis-cli strlen "$key") ;;
        list)   size=$(redis-cli llen "$key") ;;
        hash)   size=$(redis-cli hlen "$key") ;;
        set)    size=$(redis-cli scard "$key") ;;
        zset)   size=$(redis-cli zcard "$key") ;;
    esac
    echo "$key $type $size"
done

# 方式3:MEMORY USAGE(Redis 4.0+,精确字节数)
MEMORY USAGE mykey

删除方案

Text Only
# ❌ 错误:DEL(同步阻塞,大Key可能阻塞数秒)
DEL big:hash:key

# ✅ 正确:UNLINK(异步删除,Redis 4.0+)
UNLINK big:hash:key
# 后台线程异步释放内存,不阻塞主线程

# 对于 Hash/Set 等:分批删除
# HSCAN + HDEL 分批删除大Hash
HSCAN big:hash:key 0 COUNT 100
HDEL big:hash:key field1 field2 ... field100
# 循环直到删完

拆分方案

Text Only
# 大Hash拆分:按field分片
# 原始:user:profile → 包含100个field
# 拆分:user:profile:0, user:profile:1, ...
# 分片规则:field_hash = CRC32(field) % N

# 大List拆分:按范围分段
# 原始:timeline:user1 → 100万条
# 拆分:timeline:user1:0, timeline:user1:1, ...(每段5000条)

6.2 热Key问题

定义:被高频访问的 Key,单个 Key 的 QPS 极高。

危害: - 单节点 CPU/网络压力过大 - Cluster 集群中热点数据所在节点成为瓶颈

检测方法

Bash
# 方式1:redis-cli --hotkeys(需要先开启 LFU 淘汰策略)
redis-cli --hotkeys

# 方式2:redis-cli MONITOR(实时监控命令,注意性能影响)
redis-cli MONITOR | head -1000  # |管道:将前一命令的输出作为后一命令的输入

# 方式3:代理层统计(如 Twemproxy、Codis 的监控)

解决方案

Text Only
方案1:本地缓存(JVM缓存 / Python dict)
  → 热点数据在应用层缓存,减少 Redis 请求
  → 使用 Caffeine/Guava(Java)或 cachetools(Python)
  → 设置较短的本地缓存TTL(如10秒)

方案2:读写分离 + 读副本
  → 多个只读从节点分担读压力
  → 客户端随机选择从节点读取

方案3:Key 分片
  → 将 hot:key 拆分为 hot:key:0, hot:key:1, ..., hot:key:N
  → 读取时随机选择一个分片
  → 写入时更新所有分片(或通过消息队列异步同步)

6.3 Redis内存优化

Text Only
# 1. 合理使用数据类型(利用小数据类型的压缩编码)
# Hash 元素少时用 ziplist(内存比hashtable省很多)
CONFIG SET hash-max-ziplist-entries 128
CONFIG SET hash-max-ziplist-value 64

# 2. 控制 Key 的过期时间(避免大量永不过期的Key堆积)
SET session:user:1001 "data" EX 1800  # 30分钟过期

# 3. 内存碎片整理(Redis 4.0+)
CONFIG SET activedefrag yes        # 开启自动碎片整理
# 查看碎片率
INFO memory
# mem_fragmentation_ratio > 1.5 时建议整理

# 4. 使用更紧凑的编码
# 用 Hash 替代多个 String 存储对象属性(一个 Hash ziplist 比多个 String 省内存)

# 5. 监控内存使用
INFO memory                        # 查看整体内存信息
MEMORY DOCTOR                      # Redis内存诊断
MEMORY STATS                       # 详细内存统计

6.4 Redis 8.6 新特性简介

特性 说明
Search 增强 RediSearch 性能大幅提升,支持更复杂的向量搜索和混合查询
Vector Search 原生支持向量相似度搜索,AI/ML 应用场景优化
Function 替代 Lua 脚本的新方案,支持库管理(FUNCTION LOAD),可持久化函数定义
Sharded Pub/Sub Pub/Sub 消息按 slot 分片,Cluster 模式下不再广播到所有节点
Multi-part AOF AOF 文件拆分为多个文件(base + incremental),重写更高效,避免单一大文件
listpack 替代 ziplist 所有压缩列表场景统一使用 listpack,解决 ziplist 连锁更新问题
Client eviction 可设置客户端连接的内存上限,超限时断开连接,防止客户端缓冲区 OOM
多线程 I/O 增强 I/O 线程池优化,网络吞吐量进一步提升

第七部分:面试精选

📋 Redis经典面试题(15题)


Q1:Redis 为什么这么快?

: 1. 纯内存操作:数据存储在内存中,读写速度快 2. 单线程模型:避免上下文切换和锁竞争(Redis 6.0 后 I/O 多线程,命令执行仍单线程) 3. IO多路复用:使用 epoll/kqueue 处理大量并发连接 4. 高效数据结构:SDS、ziplist、quicklist、skiplist 等专门优化的数据结构 5. 通信协议简单:RESP 协议解析效率高


Q2:Redis 的 String 底层是怎么实现的?三种编码分别是什么?

:底层使用 SDS(Simple Dynamic String),支持 O(1) 获取长度、空间预分配、惰性释放、二进制安全。三种编码: - int:存储整数值 - embstr:≤ 44 字节的字符串,redisObject 和 SDS 在连续内存 - raw:> 44 字节的字符串,redisObject 和 SDS 分开存储


Q3:ZSet 底层的跳表是怎么工作的?为什么用跳表而不用红黑树?

:跳表是多层有序链表,通过逐层建立索引实现 O(log n) 的查找。选择跳表的原因: 1. 范围查询高效:找到起点后直接遍历链表,红黑树需要中序遍历 2. 实现简单:红黑树的旋转逻辑复杂 3. 插入删除简单:无需旋转/分裂 4. 内存灵活:通过随机层数实现概率平衡


Q4:Hash 的渐进式 rehash 是怎么实现的?

: 1. 分配新哈希表 ht[1],大小为 ht[0].used 的最小 2^n 2. 维护 rehashidx 标记当前迁移进度 3. 每次 CRUD 操作时,顺带迁移 rehashidx 桶中的所有键值对到 ht[1] 4. 查询时先查 ht[0] 再查 ht[1],新增只写 ht[1] 5. 迁移完成后释放 ht[0],将 ht[1] 设为 ht[0]

目的:避免一次性大规模数据迁移导致主线程长时间阻塞。


Q5:RDB 和 AOF 的区别?如何选择?

对比项 RDB AOF
存储内容 二进制快照 写命令日志
文件大小 小(压缩) 大(命令文本)
恢复速度 慢(重放命令)
数据安全 可能丢最后一次快照后的数据 最多丢1秒(everysec)
性能影响 fork时可能阻塞 每条命令追加,IO开销

选择:推荐 Redis 4.0+ 混合持久化(RDB 快速加载 + AOF 增量安全)。


Q6:主从复制的 PSYNC 全量同步和增量同步分别在什么时候触发?

: - 全量同步:Slave 首次连接 Master、Slave 的 offset 不在 repl_backlog_buffer 范围内 - 增量同步:Slave 正常运行中实时接收 Master 的写命令传播、Slave 短暂断线重连且 offset 仍在 repl_backlog_buffer 范围内


Q7:Redis Sentinel 的故障转移流程?

: 1. Sentinel 通过 PING 检测 Master 不可达 → 主观下线(SDOWN) 2. 询问其他 Sentinel,达到 quorum → 客观下线(ODOWN) 3. Sentinel 间通过 Raft 选举 Leader Sentinel 4. Leader 从 Slave 中选出新 Master(优先级 > offset > runid) 5. 向新 Master 发 SLAVEOF NO ONE 6. 向其他 Slave 发 SLAVEOF 新Master 7. 监控旧 Master,恢复后设为 Slave


Q8:Redis Cluster 为什么是 16384 个哈希槽?

: 1. CRC16 算法输出 16 位,取模 16384(2^14)是高效的位运算 2. 心跳包中需要携带节点负责的槽位 bitmap,16384/8 = 2KB,节省网络带宽 3. Redis 作者建议集群最多 ≈ 1000 个节点,16384 个槽分配已足够 4. 如果用 65536(2^16),bitmap 需 8KB,心跳包过大


Q9:缓存穿透、缓存击穿、缓存雪崩的区别和解决方案?

问题 原因 解决方案
穿透 查询不存在的数据 缓存空值 + 布隆过滤器
击穿 热点Key过期 互斥锁 + 逻辑过期
雪崩 大量Key同时过期 随机过期时间 + 多级缓存 + 降级

Q10:分布式锁怎么实现?有什么注意事项?

: - 基础方案:SET lock_key request_id NX EX 30(原子设置 + 过期时间) - 释放锁:Lua 脚本判断 request_id 后删除(防误删) - 续期:Redisson 看门狗机制自动续期 - 多节点:RedLock 算法(在 N/2+1 个节点上加锁) - 注意:锁必须设置过期时间(防死锁)、必须用唯一标识防误删、考虑锁续期


Q11:Redis 的淘汰策略有哪些?LRU 和 LFU 的区别?

: 8种策略:noeviction、allkeys-lru、volatile-lru、allkeys-lfu、volatile-lfu、allkeys-random、volatile-random、volatile-ttl。

  • LRU(Least Recently Used):淘汰最久未使用的。Redis 用近似 LRU(采样淘汰),非精确 LRU。
  • LFU(Least Frequently Used):淘汰使用频率最低的。使用 Morris 计数器 + 衰减因子。
  • LFU 更适合热点数据场景,能区分偶尔访问和频繁访问。

Q12:Redis 的事务能保证原子性吗?和数据库事务有什么区别?

: - Redis 事务保证命令批量执行(EXEC 后全部执行),但不支持回滚 - 如果某条命令执行错误,其他命令仍会执行 - 与关系型数据库的 ACID 事务本质不同:Redis 事务不保证原子性(A) - 需要原子性时用 Lua 脚本(脚本中所有命令原子执行,中间不会被其他命令打断)


Q13:大 Key 和热 Key 问题如何排查和解决?

大 Key: - 检测:redis-cli --bigkeysMEMORY USAGESCAN 遍历 - 删除:UNLINK(异步删除)替代 DEL - 优化:拆分大 Key(Hash 分片、List 分段)

热 Key: - 检测:redis-cli --hotkeys(需 LFU 策略)、MONITOR、代理层统计 - 解决:本地缓存(L1 Cache)、读写分离多副本、Key 分片


Q14:Cache Aside 模式中为什么是先更新数据库再删缓存,而不是先删缓存再更新数据库?

先删缓存再更新数据库会导致不一致: 1. 线程A 删除缓存 2. 线程B 读取缓存未命中 3. 线程B 从数据库读取旧值并写入缓存 4. 线程A 更新数据库为新值 → 缓存中是旧值,数据库是新值,不一致!

先更新数据库再删缓存也有小概率不一致(读线程回填旧缓存),但概率极低(数据库写操作通常慢于缓存写)。

如果要更可靠 → 延迟双删Canal 监听 binlog 删缓存


Q15:Redis 在 AI/ML 场景中有哪些应用?

: 1. 特征缓存:将频繁访问的特征向量缓存在 Redis(Hash 或 String),加速模型推理 2. 模型推理结果缓存:相同输入的推理结果缓存,避免重复计算 3. Embedding 缓存:LLM 应用中缓存 embedding 向量,减少调用向量数据库 4. 限流保护:AI API 调用限流,防止模型服务过载 5. 异步任务队列:Stream/List 实现推理任务队列 6. 实时特征存储:和 Feast 等特征平台配合,缓存实时特征 7. 会话管理:LLM 对话历史缓存 8. A/B 测试:存储实验分组配置和实时指标


✏️ 练习

基础练习

  1. 数据结构操作:使用 Redis CLI 完成以下操作:
  2. 用 Hash 存储一个用户信息(包含 name、age、city、login_count 字段)
  3. 用 ZSet 创建一个包含 10 个成员的排行榜,获取 Top 3
  4. 用 Set 模拟两个用户的关注列表,计算共同关注

  5. 编码验证:使用 OBJECT ENCODING 命令观察:

  6. 当 String 存储数字 vs 长字符串时的编码差异
  7. Hash 元素从 ziplist 转为 hashtable 的临界点

  8. 持久化配置:在本地 Redis 实例上分别配置 RDB 和 AOF,验证数据恢复。

进阶练习

  1. 分布式锁实现:用 Python/Java 实现一个完整的分布式锁方案,包括:
  2. 获取锁(SET NX EX)
  3. 释放锁(Lua 脚本验证后删除)
  4. 超时重试机制
  5. 编写测试模拟并发场景

  6. 缓存设计:设计一个 Cache Aside 模式的缓存方案,处理缓存穿透(布隆过滤器)和缓存击穿(互斥锁)。

  7. 限流器:用 Redis + Lua 脚本实现一个滑动窗口限流器,支持每分钟 N 次请求限制。

高阶练习

  1. 秒杀系统:设计一个完整的秒杀系统,使用 Redis 实现:
  2. 库存预热加载
  3. 原子扣减库存(Lua 脚本)
  4. 用户去重(Set)
  5. 限流控制(令牌桶)

  6. Redis Cluster 搭建:在本地使用 Docker 搭建一个 3主3从 的 Redis Cluster,测试:

  7. 数据分片(观察不同 key 分配到不同节点)
  8. 故障转移(手动停止一个 Master,观察 Slave 提升过程)
  9. 集群扩容(添加新节点,迁移槽位)

📚 推荐资源

书籍: - 《Redis 设计与实现》— 黄健宏(深入底层实现,面试必备) - 《Redis 实战》— Josiah Carlson(实战场景丰富) - 《Redis 深度历险》— 钱文品(深入浅出讲解原理)

官方资源: - Redis 官方文档 - Redis 命令参考 - Redis University(免费官方课程)

B站推荐

💡 以下为推荐的搜索关键词,请在B站直接搜索获取最新内容。

推荐搜索关键词: - "Redis 6/7 深度剖析"、"Redis 数据结构 源码" - "Redis 持久化 AOF RDB"、"Redis 集群 Sentinel" - "Redis 缓存穿透/雪崩/击穿"

源码学习: - Redis 源码(GitHub) - Redis 源码注释版


📌 学习建议:Redis 知识体系庞大,建议按以下优先级学习:数据结构 → 持久化 → 高可用架构 → 缓存设计(穿透/击穿/雪崩)→ 分布式锁 → 性能优化。面试中,缓存三连问(穿透/击穿/雪崩)和分布式锁几乎是必考题,务必反复练习到能脱口而出。


⬅️ 上一章:实战项目案例 | 📖 返回目录