redis从入门到熟练 (二) Zset,Hash,List类型详解

157 阅读9分钟

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应用场景

  1. 排行榜类应用
  • 游戏积分榜:玩家分数作为score
  • 直播打赏榜:打赏金额作为score

2.优先队列(带权重处理)

  • VIP客户优先处理
  • 紧急工单优先处理
  • 重要消息优先推送

3.范围筛选(高效查询)

  • 价格区间商品筛选(100-500元)
  • 学生成绩查询(80-100分)
  • 地理位置附近的人(GeoHash转换)
  1. 滑动窗口统计(去重计数)
  • 最近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. 多层结构:每个节点有1-32层随机高度

  2. 排序依据

    • 首先按score排序
    • score相同则按member字典序排序
  3. 平均时间复杂度

    • 查询:O(logN)
    • 插入:O(logN)
    • 删除:O(logN)

为什么使用跳跃表?

  1. 相比平衡树:

    • 实现更简单
    • 范围查询更方便
    • 并发环境下更容易扩展
  2. 相比普通链表:

    • 查询效率从O(n)提升到O(logN)
  3. 哈希表(dict)的作用

虽然跳跃表已经可以满足所有操作需求,但额外维护一个哈希表是为了:

  1. O(1)时间复杂度获取member的score

    • ZSCORE命令直接通过哈希表实现
  2. 检查member是否存在

    • 添加新元素时快速判断是否已存在
  3. 内存布局示例

增加数据

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)

  1. 从最高层(L3)开始:

    • head->Ann(95)>85 → 降层
  2. 到L2层:

    • head->LiLei(85)>=85 → 找到起点
    • 记录当前位置:LiLei(L2)

步骤2:开始范围遍历

从找到的LiLei节点开始:

  1. LiLei(L0): 85 → 符合范围,输出"LiLei"
  2. 沿L0前进到Lucy:88 → 符合,输出"Lucy"
  3. 前进到HanMei:92 → 符合,输出"HanMei"
  4. 前进到Ann:95 >92 → 停止

步骤3:返回结果

最终返回:["LiLei", "Lucy", "HanMei"]

所以在拿某个元素的时候可以直接用hash表拿数据

  1. 数据转换过程

当以下任一条件满足时,ziplist会转换为skiplist+dict:

  1. 元素数量超过zset-max-ziplist-entries
  2. 任一member长度超过zset-max-ziplist-value

转换过程:

  1. 创建空的skiplist和dict

  2. 遍历ziplist所有元素

  3. 逐个插入到新结构中

  4. 释放ziplist内存

  5. 性能特点总结

操作ziplist时间复杂度skiplist时间复杂度
插入O(n)O(logN)
删除O(n)O(logN)
按score查询O(n)O(logN)
按member查scoreO(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:

  1. 新增字段后字段数量超过hash-max-ziplist-entries
  2. 插入的字段或值长度超过hash-max-ziplist-value

转换步骤

  1. 创建空的字典结构
  2. 遍历ziplist所有field-value对
  3. 逐个插入到新字典中
  4. 释放ziplist内存

5. 两种结构的对比

特性ziplisthashtable
内存占用极小(无指针和元数据开销)较大(需存储指针和结构信息)
查询效率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
  1. 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:

  1. 插入新元素后,元素数量 > list-max-ziplist-entries(默认512)
  2. 插入的字符串元素长度 > list-max-ziplist-value(默认64字节)

转换过程

  1. 创建新的 list 结构
  2. 遍历 ziplist 所有元素
  3. 为每个元素创建 listNode
  4. 构建双向链表关系
  5. 释放原 ziplist 内存
特性ziplistlinkedlist
内存占用极低(无指针开销)每个元素+24字节
插入性能平均O(n),需重分配O(1)
索引访问O(n)O(n)
头部/尾部操作O(n)O(1)
最大元素长度≤64字节无限制
最大元素数量≤512个无限制(内存允许)
内存连续性连续不连续