Lesson 62 · 数据库与中间件实战
Redis 五大核心数据结构与应用场景
Redis 五大核心数据结构与应用场景
String/List/Hash/Set/ZSet 的底层编码(SDS、ziplist、skiplist、hashtable、intset)。每种结构的典型应用场景。
String——SDS 动态字符串
Redis 的 String 不是 C 语言的原生字符串,而是自定义的 SDS(Simple Dynamic String)。SDS 在 C 字符串基础上增加了长度记录、预分配空间等特性。
SDS 的预分配策略是什么?
当新长度 < 1MB 时,分配 2 倍空间;当新长度 >= 1MB 时,多分配 1MB。这种策略保证了连续追加 N 次,最多触发 O(logN) 次内存分配。
String 底层有三种编码: int: 当值是整数时(如 SET counter 100) 直接存储在 redisObject.val 中,无 SDS 开销 embstr: 当字符串长度 <= 44 字节时 redisObject 和 SDS 分配在一次 malloc 中(连续内存) 优点: 一次分配一次释放,减少碎片,缓存友好 缺点: 不可修改(追加操作会转为 raw) raw: 当字符串长度 > 44 字节时 redisObject 和 SDS 分别分配内存 支持修改操作(APPEND、SET 新值等) OBJECT ENCODING mykey → 返回 "int" / "embstr" / "raw"
1. 缓存对象 SET user:1001 '{"name":"Alice","age":25}' EX 3600 2. 分布式计数器 INCR article:views:1001 -- 原子递增 INCRBY stock:1001 -1 -- 原子递减(扣库存) 3. 分布式锁 SET lock:order uuid NX EX 30 -- 不存在则设置,30 秒过期 4. 分布式 Session 共享 SET session:token123 '{"userId":1001}' EX 1800
List——quicklist = ziplist + 双向链表
Redis 3.2+ 的 List 底层使用 quicklist,它是 ziplist 和双向链表的混合体:双向链表的每个节点是一个 ziplist。
ziplist 是一块连续内存: zlbytes | zltail | zllen | entry1 | entry2 | ... | zlend 每个 entry 的结构: prevlen // 前一个 entry 的长度(用于反向遍历) encoding // 编码方式(字符串 or 整数) data // 实际数据 优点:连续内存 → CPU cache friendly,无指针开销 缺点:插入/删除可能引发连锁更新(prevlen 字段从 1 字节变 5 字节)
List 底层是 quicklist(Redis 3.2+)。quicklist = 双向链表,每个节点是一个 ziplist。LPUSH/RPUSH 是 O(1),LINDEX 是 O(N)。ziplist 用连续内存存储,对 CPU 缓存友好。
Hash——ziplist vs hashtable
Hash 类型有两种底层编码:ziplist(数据少时)和 hashtable(数据多时)。
ziplist → hashtable 转换条件(满足任一即转换): 1. Hash 中 field 数量 > hash-max-ziplist-entries(默认 512) 2. 任一 field 或 value 长度 > hash-max-ziplist-value(默认 64 字节) 注意:转换是单向的,hashtable 不会退化为 ziplist
Set——intset vs hashtable
Set(集合)的底层编码也有两种:intset(全是整数且数量少时)和 hashtable。
intset 支持三种整数编码: INTSET_ENC_INT16 → 每个元素 2 字节 INTSET_ENC_INT32 → 每个元素 4 字节 INTSET_ENC_INT64 → 每个元素 8 字节 当插入一个超出当前编码范围的整数时,intset 会"升级": 例如从 INT16 升级到 INT32 → 所有已有元素都要重新编码 升级是单向的,不会降级 intset → hashtable 转换条件(满足任一): 1. 元素个数 > set-max-intset-entries(默认 512) 2. 插入了非整数元素(如字符串)
Set 全整数且 < 512 个时用 intset(有序数组,紧凑高效),否则用 hashtable(value 为 NULL 的 dict,O(1) 查找)。SISMEMBER 在 intset 中是 O(logN) 二分,在 hashtable 中是 O(1)。
ZSet——ziplist vs skiplist + hashtable
有序集合(ZSet)是最复杂的数据结构,有两种编码:ziplist(数据少时)和 skiplist + hashtable。
为什么 ZSet 用 skiplist 而不用红黑树?
三个原因:① skiplist 实现简单(随机层级,不用旋转);② skiplist 天然支持范围查询(ZRANGEBYSCORE),从高层快速定位起点后沿 level 0 遍历即可,红黑树做范围查询需要中序遍历;③ skiplist 的并发友好性更好(虽然 Redis 单线程不关心这个)。
ZSet 底层:少量数据用 ziplist,大量数据用 skiplist(排序 + 范围查询)+ hashtable(按 member 查 score,O(1))。skiplist 每个节点随机生成 1~64 层,平均 O(logN) 查找。
编码转换——Redis 什么时候"升级"底层结构?
Redis 会根据数据量和元素大小,自动将底层编码从"紧凑"升级到"高效"。转换是单向的,不会降级。
| 数据类型 | 紧凑编码 | 高效编码 | 升级触发条件 |
|---|---|---|---|
| String | int (embstr) | raw (SDS) | 值长度 > 44 字节 / 执行追加操作 |
| List | ziplist | quicklist | Redis 3.2+ 默认就用 quicklist |
| Hash | ziplist | hashtable | field > 512 或 value > 64 字节 |
| Set | intset | hashtable | 元素 > 512 或含非整数 |
| ZSet | ziplist | skiplist + hashtable | 元素 > 128 或 value > 64 字节 |
-- 查看 key 的底层编码 OBJECT ENCODING myhash -- 返回 "ziplist" 或 "hashtable" -- 调优阈值(redis.conf) hash-max-ziplist-entries 512 hash-max-ziplist-value 64 zset-max-ziplist-entries 128 zset-max-ziplist-value 64 set-max-intset-entries 512 -- 如果业务中 Hash 经常只有少量 field,可以调大阈值来节省内存
"Redis 会自动根据数据量选择最优编码。小数据用连续内存(ziplist/intset)节省空间,大数据用哈希表/跳表保证性能。OBJECT ENCODING 命令可以查看当前编码。"——这段话能展示你对底层的理解。
应用场景总结
| 数据类型 | 典型应用场景 | 核心命令 |
|---|---|---|
| String | 缓存、计数器、分布式锁、Session 共享、验证码 | SET/GET/INCR/EXPIRE |
| List | 消息队列(LPUSH + BRPOP)、最新列表、朋友圈时间线 | LPUSH/RPUSH/LPOP/RPOP/LRANGE |
| Hash | 用户信息(field-value)、商品属性、配置项 | HSET/HGET/HMSET/HGETALL |
| Set | 标签系统、共同好友、去重、抽奖(SRANDMEMBER) | SADD/SMEMBERS/SINTER/SUNION |
| ZSet | 排行榜、延迟队列、带权重的任务调度 | ZADD/ZRANGE/ZRANGEBYSCORE/ZRANK |
全篇回顾
- String:底层 SDS,O(1) 获取长度,二进制安全,预分配空间
- List:底层 quicklist = 双向链表 + ziplist 节点,兼顾内存效率和操作性能
- Hash:少量用 ziplist,多用 hashtable。field-value 存储对象属性首选
- Set:全整数少量用 intset(有序数组),否则 hashtable。去重/集合运算
- ZSet:少量用 ziplist,多用 skiplist + hashtable。排行榜/延迟队列
- 编码转换:自动单向升级,可通过 redis.conf 调阈值优化内存
- 选型:按业务语义选类型,按数据量调编码