跳转至

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 线程池优化,网络吞吐量进一步提升

6.5 Valkey:Redis 开源分叉(2024+)

⚠️ 重要背景:2024年3月,Redis Labs 将 Redis 许可证从 BSD 改为 SSPL(Server Side Public License),限制云服务商直接提供 Redis 托管服务。这一变更引发开源社区担忧。

Valkey 诞生:2024年4月,Linux基金会宣布托管 Valkey——一个基于 Redis 7.2.4 的纯 BSD-3 许可开源分叉,由 AWS、Google Cloud、Oracle、Ericsson、Snap 等公司联合支持。

对比 Redis(SSPL后) Valkey
许可证 SSPL + RSAL(双许可) BSD-3-Clause
维护方 Redis Ltd. Linux Foundation
云厂商友好 受限(需商业许可) 完全开源,可自由托管
API 兼容性 完全兼容 Redis 7.2 协议
当前版本 8.6+ 8.x(2025年后持续更新)

技术选型建议: 1. 企业自建:若需完全开源合规,可选择 Valkey 2. 云托管:AWS ElastiCache、阿里云 Tair 等已支持 Valkey 兼容模式 3. 功能需求:Redis Stack(Search、JSON、TimeSeries)仍需 Redis 商业版或 RedisInsight

Bash
# Valkey Docker 快速启动
docker run -d --name valkey -p 6379:6379 valkey/valkey:8

# 连接测试(与Redis CLI完全兼容)
redis-cli -h localhost -p 6379 PING
# 返回: PONG

第七部分:面试精选

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


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