🧱 数据结构
五大类型与应用场景 · 底层实现(SDS/跳表/压缩列表)· 渐进式 rehash
1. Redis 五大基础类型及典型应用场景?
| 类型 | 底层 | 经典应用 |
|---|---|---|
| String | SDS 动态字符串 | 缓存、计数器(INCR)、分布式 ID(INCRBY)、SETNX 锁 |
| Hash | 哈希表 + 压缩列表 | 对象/购物车存储(field 级操作,省序列化) |
| List | 双向链表 + 压缩列表 | 消息队列(LPUSH/BRPOP)、最新列表、简单流 |
| Set | 哈希表 + 整数集合 | 去重、共同关注(SINTER)、抽奖(SRANDMEMBER) |
| ZSet(有序集合) | 跳表 + 哈希表 | 排行榜(按 score 排序)、延时队列(score=时间戳)、限流(滑动窗口) |
进阶类型:Bitmap(签到、布隆过滤器思路)、HyperLogLog(UV 统计,~12KB 记 2^64 基数)、GEO(地理位置)、Stream(正式消息队列,支持消费者组)。
🎯 面试要点
- String 的 SETNX + EXPIRE 是分布式锁雏形;ZSet 的 score 即"权重",排行榜秒出
- HyperLogLog 是概率结构,有误差(0.81%);要精确去重用 Set 或布隆
- Stream 是 Redis 5.0 引入的"正经"MQ,替代 List 消息队列(有 ACK 机制)
2. 底层实现:SDS、跳表、压缩列表?
- SDS(简单动态字符串):String 的底层。结构:len(长度)+ free(剩余)+ buf(字节数组)。优点:O(1) 取长度、杜绝缓冲区溢出(自动扩容)、二进制安全(可存 \0)、减少内存分配次数(预分配 + 惰性释放)。比 C 字符串更适合做缓存。
- 跳表(SkipList):ZSet 的底层。多层有序链表,上层是下层的"索引",查找 O(log n)。为什么不用红黑树:实现简单、区间查询方便(从 head 顺序走)、内存可控。Redis 选择跳表而不是平衡树
- 紧凑编码(listpack / quicklist):元素少时 Hash/ZSet/List 用连续内存的紧凑结构省内存;元素多时转哈希表/跳表。Redis 7.0 起默认紧凑编码是 listpack(替代 ziplist);quicklist 是多个 listpack 组成的链表(List 底层)
- 整数集合(IntSet):Set 全为整数且少时用,有序数组
编码转换条件(object encoding 命令可查)
# Redis 7.x 紧凑编码阈值(超过就转哈希表/跳表):
# Hash: 元素 ≤ 512 且单个值 ≤ 64 字节 → listpack
# ZSet: 元素 ≤ 128 且单个值 ≤ 64 字节 → listpack
# 超过 → hashtable / skiplist
# Set 全整数且 ≤ 512 个 → intset
# 检查编码
127.0.0.1:6379> OBJECT ENCODING key
# "ziplist" / "hashtable" / "skiplist" / "intset" / "embstr"
🎯 面试要点
- Redis 7.0 用 listpack 替代 ziplist(修复连锁更新问题),原理类似:紧凑数组
- 跳表每层概率 p=1/4 提升(ZSKIPLIST_P),最高 32 层
- 记忆点:Redis 用内存换速度,一切底层选择(跳表/压缩列表/SDS)都是为了"快 + 省内存"
3. 什么是渐进式 rehash?为什么要渐进?
rehash:哈希表扩容/缩容时,把数据从 ht[0] 迁移到新表 ht[1],之后交换指针。若一次性迁移,大 key 场景会阻塞单线程 Redis 数十毫秒甚至秒级。
渐进式:迁移分摊到每次增删改查——每操作一个桶就顺带搬移一小批(桶 0..index),期间:
- 查询:先查 ht[0] 再查 ht[1];新增:只写 ht[1](保证 ht[0] 只减不增,最终搬完)
- 期间内存翻倍(两表共存),要关注大实例的瞬时内存
触发:负载因子(元素数/桶数)≥ 1 且允许扩容时扩容;有子进程做 BGSAVE/AOF rewrite 时会「避免扩容」,只有比值 > 5 才强制扩容(控制内存);< 0.1 时缩容。
🎯 面试要点
- 渐进式 rehash 是"大任务切小步"的典型:单线程框架里一切长操作都要分片(类似 JVM 的并发标记)
- 大 key 是 Redis 性能杀手:rehash、删除、迁移都会卡主线程 → 大 key 要拆分/异步删除(UNLINK)