1.介绍文章内容
在上一篇文章中,我们介绍了redis,以及redis的String,Set的数据结构。
这篇文章,我将讲解剩下的三种数据类型(Zset,Hash,List) 对每种数据类型从介绍是什么有哪些功能,应用场景,底层的数据结构是什么三个维度去分析
2.Zset 有序集合
2.1是什么,有哪些功能
相比于set,zset维护了一个score字段,使得zset中的元素有序,也可以根据score这个字段进行范围查询元素的列表,或者查询某个元素的位置
# 添加三个成员
ZADD leaderboard 100 "Alice" 200 "Bob" 150 "Charlie"
# 获取排名
ZRANGE leaderboard 0 -1 WITHSCORES
# 输出: Alice(100), Charlie(150), Bob(200)
# 获取Bob的排名
ZRANK leaderboard "Bob" # 返回2(0-based)
# 查询分数在120-180之间的成员
ZRANGEBYSCORE leaderboard 120 180
# 输出: Charlie
2.2应用场景
- 排行榜类应用
- 游戏积分榜:玩家分数作为score
- 直播打赏榜:打赏金额作为score
2.优先队列(带权重处理)
- VIP客户优先处理
- 紧急工单优先处理
- 重要消息优先推送
3.范围筛选(高效查询)
- 价格区间商品筛选(100-500元)
- 学生成绩查询(80-100分)
- 地理位置附近的人(GeoHash转换)
- 滑动窗口统计(去重计数)
- 最近1小时UV统计
- 用户最近搜索记录
2.3底层的数据结构
zst的底层是由ZipList、SkipList组合实现的。
(1) ziplist (压缩列表)
使用条件(可配置):
- 元素数量 <
zset-max-ziplist-entries(默认128) - 每个元素大小 <
zset-max-ziplist-value(默认64字节)
[header][entry1][entry2]...[entryN][end]
- 每个entry存储member和score,按score排序
- member和score作为两个连续节点存储
- 内存连续分配,节省内存但修改效率低
优点:
- 内存利用率高(没有指针开销)
- 数据局部性好(CPU缓存友好)
缺点:
- 插入/删除需要内存重分配
- 查询效率O(n)
(2) skiplist + dict (跳跃表+哈希表)
zset的数据结构
typedef struct zset {
dict *dict; // 哈希表:存储member->score的映射
zskiplist *zsl; // 跳跃表:按score排序存储所有元素
} zset;
跳表的数据结构
typedef struct zskiplistNode {
robj *member; // 成员对象
double score; // 分数
struct zskiplistNode *backward; // 后退指针
struct zskiplistLevel {
struct zskiplistNode *forward; // 前进指针
unsigned int span; // 跨度
} level[]; // 层级数组
} zskiplistNode;
关键特性
-
多层结构:每个节点有1-32层随机高度
-
排序依据:
- 首先按score排序
- score相同则按member字典序排序
-
平均时间复杂度:
- 查询:O(logN)
- 插入:O(logN)
- 删除:O(logN)
为什么使用跳跃表?
-
相比平衡树:
- 实现更简单
- 范围查询更方便
- 并发环境下更容易扩展
-
相比普通链表:
- 查询效率从O(n)提升到O(logN)
-
哈希表(dict)的作用
虽然跳跃表已经可以满足所有操作需求,但额外维护一个哈希表是为了:
-
O(1)时间复杂度获取member的score
ZSCORE命令直接通过哈希表实现
-
检查member是否存在
- 添加新元素时快速判断是否已存在
-
内存布局示例
增加数据
ZADD scores 85 "LiLei" 92 "HanMei" 78 "Jim" 88 "Lucy" 95 "Ann"
哈希表结构:
"Jim" => 78
"LiLei" => 85
"Lucy" => 88
"HanMei" => 92
"Ann" => 95
跳跃表结构:
Level3: head ---------------------------> Ann -->null
Level2: head -------> LiLei -----------> HanMei ------> Ann --->null
Level1: head -> Jim -> LiLei -> Lucy -> HanMei -> Ann --> null
Level0: head -> Jim -> LiLei -> Lucy -> HanMei -> Ann --> null
范围查询 ZRANGEBYSCORE scores 85 92 的执行过程
步骤1:查找下限边界(85)
-
从最高层(L3)开始:
- head->Ann(95)>85 → 降层
-
到L2层:
- head->LiLei(85)>=85 → 找到起点
- 记录当前位置:LiLei(L2)
步骤2:开始范围遍历
从找到的LiLei节点开始:
- LiLei(L0): 85 → 符合范围,输出"LiLei"
- 沿L0前进到Lucy:88 → 符合,输出"Lucy"
- 前进到HanMei:92 → 符合,输出"HanMei"
- 前进到Ann:95 >92 → 停止
步骤3:返回结果
最终返回:["LiLei", "Lucy", "HanMei"]
所以在拿某个元素的时候可以直接用hash表拿数据
- 数据转换过程
当以下任一条件满足时,ziplist会转换为skiplist+dict:
- 元素数量超过zset-max-ziplist-entries
- 任一member长度超过zset-max-ziplist-value
转换过程:
-
创建空的skiplist和dict
-
遍历ziplist所有元素
-
逐个插入到新结构中
-
释放ziplist内存
-
性能特点总结
| 操作 | ziplist时间复杂度 | skiplist时间复杂度 |
|---|---|---|
| 插入 | O(n) | O(logN) |
| 删除 | O(n) | O(logN) |
| 按score查询 | O(n) | O(logN) |
| 按member查score | O(n) | O(1) |
| 范围查询 | O(n) | O(logN + M) |
| 内存占用 | 低 | 较高 |
3.Hash 哈希
2.1是什么,有哪些功能
redis的hash类型是用来存键值对的,有点像Java里面的hashmap,存储对象的属性经常变或者k-v的value值经常变的时候
2.2应用场景
1.字段值经常变的对象(购物车,商品信息,用户信息)
2.3底层的数据结构
redis的底层数据结构有2种,分别是ziplist和hashtable (1) ziplist (压缩列表)
使用条件(可通过配置修改):
- 所有字段和值的字符串长度都小于
hash-max-ziplist-value(默认64字节) - 字段数量小于
hash-max-ziplist-entries(默认512个)
[zlbytes][zltail][zllen]|[field1][value1][field2][value2]...[fieldN][valueN][zlend]
-
连续内存存储,没有指针开销
-
每个entry格式:
prevlen:前一个entry的长度encoding:当前entry的编码content:实际存储的数据
操作特点:
- 查询需要遍历:O(n)时间复杂度
- 插入/删除需要内存重分配
- 但内存利用率极高
(2) hableTable (哈希表)
typedef struct dictht {
dictEntry **table; // 哈希表数组
unsigned long size; // 表大小
unsigned long sizemask; // 大小掩码,用于计算索引值
unsigned long used; // 已有节点数量
} dictht;
详细展示
dictht (哈希表)
│
├── table: 指针数组
│ │
│ ├── [0] → dictEntry → dictEntry → ... → NULL (哈希冲突链)
│ ├── [1] → NULL
│ ├── [2] → dictEntry → NULL
│ └── ...
│
├── size: 哈希表大小(总是2的幂次方,如4,8,16...)
├── sizemask: size-1(用于快速计算索引)
└── used: 已存储键值对数量
typedef struct dictEntry {
void *key; // 键(指向SDS字符串)
union { // 值(联合体,节省空间)
void *val; // 可以指向任意Redis对象
uint64_t u64; // 存储无符号整数
int64_t s64; // 存储有符号整数
double d; // 存储双精度浮点数
} v;
struct dictEntry *next; // 指向下一个entry(解决哈希冲突)
} dictEntry;
4. 自动转换过程
当以下任一条件满足时,ziplist会转换为hashtable:
- 新增字段后字段数量超过hash-max-ziplist-entries
- 插入的字段或值长度超过hash-max-ziplist-value
转换步骤:
- 创建空的字典结构
- 遍历ziplist所有field-value对
- 逐个插入到新字典中
- 释放ziplist内存
5. 两种结构的对比
| 特性 | ziplist | hashtable |
|---|---|---|
| 内存占用 | 极小(无指针和元数据开销) | 较大(需存储指针和结构信息) |
| 查询效率 | O(n) | 平均O(1) |
| 插入/删除效率 | O(n),可能触发内存重分配 | 平均O(1) |
| 适合场景 | 小规模、字段值短的Hash | 大规模Hash |
| 是否支持大字段 | 否(单个值≤64字节) | 是 |
4.List 集合
2.1是什么,有哪些功能
Redis 的 List 是一种有序的字符串列表,它是基于链表实现的线性数据结构,支持从头部或尾部高效地进行插入和删除操作。
2.2应用场景
1.最新文章、最新动态。 2.简单的消息队列
2.3底层的数据结构
底层的数据结构由ziplist和linkedlist双向列表实现 (1) zipList
<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
- zlbytes (4字节):整个 ziplist 占用的内存字节数
- zltail (4字节):到达最后一个 entry 的偏移量
- zllen (2字节):entry 数量(当超过 2¹⁶-1 时需遍历计算)
- entry:列表元素,长度可变
- zlend (1字节):结束标记 0xFF
- entry 结构 每个 entry 包含三部分:
<prevlen> <encoding> <content>
-
prevlen:前一个 entry 的长度(1或5字节)
- 前一个长度 < 254 字节:用 1 字节存储
- 前一个长度 ≥ 254 字节:用 5 字节(首字节固定 0xFE,后4字节存储实际长度)
-
encoding:内容编码(1/2/5字节)
- 字符串:最高2位表示类型,其余位表示长度
- 整数:特殊编码(如 11000000 表示 int16)
-
content:实际存储的数据
(2)linkedlist(双向链表)实现
typedef struct listNode {
struct listNode *prev; // 前驱指针(8字节)
struct listNode *next; // 后继指针(8字节)
void *value; // 值指针(8字节)
} listNode;
typedef struct list {
listNode *head; // 头节点指针
listNode *tail; // 尾节点指针
unsigned long len; // 节点计数
void *(*dup)(void *ptr); // 值复制函数
void (*free)(void *ptr); // 值释放函数
int (*match)(void *ptr, void *key); // 值比较函数
} list;
存储 ["one", "two", "three"]:
list结构体
│
├── head → node1
├── tail → node3
└── len = 3
node1:
├── prev → NULL
├── next → node2
└── value → "one"
node2:
├── prev → node1
├── next → node3
└── value → "two"
node3:
├── prev → node2
├── next → NULL
└── value → "three"
-
优点:
- 插入/删除操作 O(1) 时间复杂度
- 支持大元素(不受 64字节限制)
- 节点数量无硬性限制
-
缺点:
- 每个元素额外 24 字节开销(prev+next+value指针)
- 内存不连续,缓存不友好
Redis 在以下情况会将 ziplist 转换为 linkedlist:
- 插入新元素后,元素数量 >
list-max-ziplist-entries(默认512) - 插入的字符串元素长度 >
list-max-ziplist-value(默认64字节)
转换过程
- 创建新的 list 结构
- 遍历 ziplist 所有元素
- 为每个元素创建 listNode
- 构建双向链表关系
- 释放原 ziplist 内存
| 特性 | ziplist | linkedlist |
|---|---|---|
| 内存占用 | 极低(无指针开销) | 每个元素+24字节 |
| 插入性能 | 平均O(n),需重分配 | O(1) |
| 索引访问 | O(n) | O(n) |
| 头部/尾部操作 | O(n) | O(1) |
| 最大元素长度 | ≤64字节 | 无限制 |
| 最大元素数量 | ≤512个 | 无限制(内存允许) |
| 内存连续性 | 连续 | 不连续 |