11-Redis深度专题¶
🎯 学习目标¶
通过本章学习,你将能够:
- ✅ 深入理解 Redis五大基础数据结构及其底层实现(SDS、ziplist、quicklist、skiplist、intset)
- ✅ 掌握 Redis持久化机制(RDB、AOF、混合持久化)的原理与选型
- ✅ 理解 Redis高可用架构(主从复制、哨兵、Cluster集群)的设计与运维
- ✅ 精通 Redis缓存设计模式与缓存穿透/击穿/雪崩的解决方案
- ✅ 实现 分布式锁、限流、排行榜、秒杀系统等高频实战场景
- ✅ 掌握 Redis性能优化与大Key/热Key问题的排查处理
- ✅ 应对 大厂Redis相关面试题,达到高薪后端/AI工程师面试水平
⏱️ 预计学习时间:8-10小时 📋 前置要求:06-NoSQL数据库 基础了解,熟悉基本的Redis操作
目录¶
- 第一部分:Redis基础与数据结构
- 第二部分:Redis持久化
- 第三部分:Redis高可用架构
- 第四部分:Redis高级特性
- 第五部分:Redis实战场景
- 第六部分: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是单线程还这么快?
- 纯内存操作:数据存储在内存中,读写耗时在纳秒级
- IO多路复用:使用 epoll/kqueue 等多路复用技术,单线程处理大量并发连接
- 单线程避免上下文切换:无锁竞争,无线程切换开销
- 高效数据结构:专门优化的数据结构(如 SDS、ziplist、skiplist)
⚠️ 注意:Redis 6.0 引入了多线程 I/O,但命令执行仍然是单线程。多线程仅用于网络 I/O 读写,解决网络 I/O 瓶颈。
常见使用场景:
┌──────────────────────────────────────────────────────────────────┐
│ 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:
// 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 分开存储
常用命令:
# 基本操作
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(列表)¶
底层实现演变:
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 节点的大小
quicklist 结构示意:
head ←→ [ziplist1] ←→ [ziplist2] ←→ [ziplist3] ←→ tail
↑ entry ↑ entry ↑ entry
↑ entry ↑ entry ↑ entry
↑ entry
常用命令:
# 队列操作(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:
步骤:
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] 只减不增)
常用命令:
# 存储对象
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 集合),整个集合会升级编码,但不支持降级。
常用命令:
# 基本操作
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) 的查找效率:
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+树? - 实现简单,易理解维护 - 范围查询效率高(在找到起点后顺序遍历即可) - 插入/删除无需复杂的旋转/分裂操作 - 通过随机层数实现概率平衡,不需要严格平衡
常用命令:
# 排行榜
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 类型,但提供了位操作命令,适合大规模布尔值存储场景。
典型场景:用户签到
# 用户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(独立访客)统计
# 记录页面访问
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。
# 添加地理位置
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 引入的专业消息队列数据类型,支持消费者组,可替代简单的消息中间件。
# 生产者:发送消息
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)。
触发时机:
# 方式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):
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 日志(避免语法检查开销,不阻塞当前命令执行)。
三种同步策略:
┌──────────────┬──────────────────────────────────┬────────────────────┐
│ 策略 │ 说明 │ 数据安全性 │
├──────────────┼──────────────────────────────────┼────────────────────┤
│ always │ 每条命令都 fsync 到磁盘 │ 最安全,最多丢1条 │
│ everysec │ 每秒 fsync 一次(推荐) │ 最多丢1秒数据 │
│ no │ 由操作系统决定何时 fsync │ 性能最好,可能丢较多 │
└──────────────┴──────────────────────────────────┴────────────────────┘
AOF重写(bgrewriteaof):
随着时间推移,AOF 文件会越来越大。AOF 重写将多条命令合并为等效的最少命令:
# 重写前(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 的优点:
开启方式:
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 高可用的基础,实现读写分离和数据备份。
全量同步(PSYNC,首次连接或 repl_backlog 溢出时):
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. 后续进入增量同步
增量同步:
- 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 官方的高可用方案,自动完成故障检测和故障转移。
┌────────────┐
│ Sentinel 1 │
└─────┬──────┘
│ 监控
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
[Master] [Slave 1] [Slave 2]
│
自动故障转移
│
▼
[New Master]
故障检测:
- 主观下线(SDOWN):某个 Sentinel 认为 Master 不可达(超时未响应 PING)
- 客观下线(ODOWN):
quorum个 Sentinel 都认为 Master 不可达才触发故障转移
Leader 选举(Raft 协议简化版):
1. 发现 Master 客观下线的 Sentinel 发起选举
2. 向其他 Sentinel 请求投票(RequestVote)
3. 每个 Sentinel 在每个 epoch 只投一票(先到先得)
4. 获得多数票(> N/2 + 1)的 Sentinel 成为 Leader
5. Leader 执行故障转移
故障转移流程:
1. Leader Sentinel 从所有 Slave 中选择新 Master:
- 排除已下线的 Slave
- 选择 slave-priority 最小的(优先级最高)
- 选择 offset 最大的(数据最新)
- 选择 run_id 最小的(启动最早)
2. 向选出的 Slave 发送 SLAVEOF NO ONE(升级为 Master)
3. 向其他 Slave 发送 SLAVEOF 新Master(切换复制目标)
4. 继续监控旧 Master,恢复后设为 Slave
最小配置示例:
# 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):
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 协议
- 每个节点维护集群状态(节点信息、槽分配)
- 节点间通过 Gossip 协议交换状态(PING/PONG消息)
- 每次 PING/PONG 携带自身信息 + 随机几个其他节点信息
- 优点:去中心化,容错性强
- 缺点:状态收敛需要时间(最终一致性)
ASK / MOVED 重定向:
客户端请求的 key 不在当前节点时:
MOVED 重定向(永久转移):
→ 说明该槽已经永久迁移到目标节点
→ 客户端更新本地槽映射表,后续直接请求目标节点
ASK 重定向(临时转移,迁移中):
→ 说明该槽正在从源节点迁移到目标节点
→ 客户端本次请求到目标节点(先发 ASKING 命令),但不更新映射表
→ 下次请求仍发到源节点
集群扩缩容:
# 添加节点
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种淘汰策略:
┌──────────────────────┬───────────────────────────────────────────────┐
│ 策略 │ 说明 │
├──────────────────────┼───────────────────────────────────────────────┤
│ 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 长期占据内存。
# 查看 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事务¶
# 基本事务
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 中原子执行,适合实现复杂的原子操作(如分布式锁、限流)。
# 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¶
# 订阅者(终端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 示例: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 慢查询日志分析¶
# 配置慢查询
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¶
# 错误:两条命令不是原子操作(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秒
删除锁时的问题:必须验证锁是自己的(防止误删其他客户端的锁):
# 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 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 实例上实现更可靠的分布式锁:
步骤:
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(旁路缓存,最常用)¶
读流程:
1. 先读缓存 → 命中则返回
2. 缓存未命中 → 读数据库 → 写入缓存 → 返回
写流程:
1. 先更新数据库
2. 再删除缓存(而不是更新缓存!)
为什么是"删除"缓存而不是"更新"?
- 避免并发写导致的缓存与DB不一致
- 缓存值可能是复杂计算结果,每次更新代价高
- 懒加载思想:下次读取时再重建缓存
延迟双删:
# 解决 先删缓存→再更新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¶
应用程序只与缓存交互,缓存层负责与数据库同步。
Read Through:
应用 → 缓存(未命中 → 缓存自动从DB加载)→ 返回数据
Write Through:
应用 → 缓存(同步写入DB,确认后返回)→ 写入完成
优点:应用逻辑简单
缺点:缓存层实现复杂,写延迟高(同步写DB)
Write Behind / Write Back¶
写操作只更新缓存,异步批量写入数据库。
应用 → 缓存(立即返回)
↓ 异步/批量
数据库
优点:写性能极高
缺点:数据一致性差,缓存崩溃可能丢数据
适用:写密集且允许短暂不一致的场景(如日志、计数)
5.3 缓存问题三连¶
缓存穿透(查不存在的数据)¶
问题:大量请求查询数据库中不存在的数据,每次都穿透缓存打到数据库。
方案一:缓存空对象
缺点:缓存大量空值浪费内存、可能造成短暂不一致。
方案二:布隆过滤器(BloomFilter)
原理:
1. 用一个位数组(bitmap)+ 多个哈希函数
2. 添加元素时,用多个哈希函数计算位置,将对应位设为1
3. 查询时,如果所有哈希位置都为1 → 可能存在(误判率约1%)
4. 如果任何位置为0 → 一定不存在
请求 → BloomFilter 判断 key 是否存在
→ 不存在:直接返回(拦截)
→ 可能存在:查缓存 → 查数据库
# 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)
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) # 递归重试
方案二:逻辑过期
# 缓存永不过期,但存储逻辑过期时间
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 在同一时间过期,导致请求全部打到数据库。
解决方案:
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 限流方案¶
固定窗口计数器¶
# 每分钟限制100次请求
SET rate:api:user1001 0 EX 60 NX # 不存在时初始化
INCR rate:api:user1001 # 每次请求+1
# 如果结果 > 100,拒绝请求
缺点:临界值问题(窗口切换瞬间可能承受 2 倍流量)。
滑动窗口(ZSet实现)¶
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脚本实现)¶
-- 令牌桶限流 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实现)¶
# 游戏排行榜
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应用¶
秒杀架构:
用户请求 → Nginx限流 → 网关层 → 秒杀服务 → Redis预扣库存 → MQ异步下单 → 数据库
Redis在秒杀中的角色:
1. 商品库存预热:提前将库存加载到Redis
2. 原子扣减库存:Lua脚本保证原子性
3. 用户去重:Set防止重复下单
4. 限流控制:滑动窗口限流
-- 秒杀扣库存 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 调用秒杀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 命令是同步的) - 网络带宽占用高
检测方法:
# 方式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
删除方案:
# ❌ 错误: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
# 循环直到删完
拆分方案:
# 大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 集群中热点数据所在节点成为瓶颈
检测方法:
# 方式1:redis-cli --hotkeys(需要先开启 LFU 淘汰策略)
redis-cli --hotkeys
# 方式2:redis-cli MONITOR(实时监控命令,注意性能影响)
redis-cli MONITOR | head -1000 # |管道:将前一命令的输出作为后一命令的输入
# 方式3:代理层统计(如 Twemproxy、Codis 的监控)
解决方案:
方案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内存优化¶
# 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 --bigkeys、MEMORY USAGE、SCAN 遍历 - 删除: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 测试:存储实验分组配置和实时指标
✏️ 练习¶
基础练习¶
- 数据结构操作:使用 Redis CLI 完成以下操作:
- 用 Hash 存储一个用户信息(包含 name、age、city、login_count 字段)
- 用 ZSet 创建一个包含 10 个成员的排行榜,获取 Top 3
-
用 Set 模拟两个用户的关注列表,计算共同关注
-
编码验证:使用
OBJECT ENCODING命令观察: - 当 String 存储数字 vs 长字符串时的编码差异
-
Hash 元素从 ziplist 转为 hashtable 的临界点
-
持久化配置:在本地 Redis 实例上分别配置 RDB 和 AOF,验证数据恢复。
进阶练习¶
- 分布式锁实现:用 Python/Java 实现一个完整的分布式锁方案,包括:
- 获取锁(SET NX EX)
- 释放锁(Lua 脚本验证后删除)
- 超时重试机制
-
编写测试模拟并发场景
-
缓存设计:设计一个 Cache Aside 模式的缓存方案,处理缓存穿透(布隆过滤器)和缓存击穿(互斥锁)。
-
限流器:用 Redis + Lua 脚本实现一个滑动窗口限流器,支持每分钟 N 次请求限制。
高阶练习¶
- 秒杀系统:设计一个完整的秒杀系统,使用 Redis 实现:
- 库存预热加载
- 原子扣减库存(Lua 脚本)
- 用户去重(Set)
-
限流控制(令牌桶)
-
Redis Cluster 搭建:在本地使用 Docker 搭建一个 3主3从 的 Redis Cluster,测试:
- 数据分片(观察不同 key 分配到不同节点)
- 故障转移(手动停止一个 Master,观察 Slave 提升过程)
- 集群扩容(添加新节点,迁移槽位)
📚 推荐资源¶
书籍: - 《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 知识体系庞大,建议按以下优先级学习:数据结构 → 持久化 → 高可用架构 → 缓存设计(穿透/击穿/雪崩)→ 分布式锁 → 性能优化。面试中,缓存三连问(穿透/击穿/雪崩)和分布式锁几乎是必考题,务必反复练习到能脱口而出。