Redis中zset内存变形记

一、内存变形记:两种形态自由切换

1.1 压缩列表(ziplist)------内存界的"紧身衣"

适用场景:当同时满足:

  • 元素数量 < zset-max-ziplist-entries(默认128)
  • 每个元素大小 < zset-max-ziplist-value(默认64字节)

存储结构

python 复制代码
# 伪代码示意
ziplist = [
    [元素1内容, 分数1], 
    [元素2内容, 分数2],
    ...
]
  • 内存连续:像军训队列般紧密排列
  • 双人舞存储:每个元素与分数成对存储(类似[member1, score1, member2, score2])
  • 插入代价:中间插入元素需要"集体向右平移"(时间复杂度O(n))

特点

  • ✅ 极致内存利用率(省内存冠军)
  • ❌ 随机插入性能差(像早高峰挤地铁)

1.2 跳表(skiplist)------查询界的"闪电侠"

当突破压缩列表阈值时,ZSet会"华丽变身"为:

c 复制代码
// Redis源码结构定义
typedef struct zset {
    dict *dict;          // 哈希表:用于O(1)查询元素是否存在
    zskiplist *zsl;      // 跳表:用于范围查询和排序
} zset;

双剑合璧架构

  • 哈希表:快速判断元素是否存在(像快递柜取件码)
  • 跳表:维护有序结构(像图书馆索引系统)

二、跳表演示:Redis版的"地铁网络"

2.1 跳表节点结构

c 复制代码
typedef struct zskiplistNode {
    sds ele;                     // 元素值(如"雪王奶茶")
    double score;                // 分数(如999)
    struct zskiplistNode *backward; // 后退指针(单链表)
    struct zskiplistLevel {
        struct zskiplistNode *forward;  // 前进指针
        unsigned long span;             // 跨度(快速计算排名)
    } level[];                    // 柔性数组,层级随机生成
} zskiplistNode;

2.2 跳表层级生成:随机的艺术

插入新节点时

  1. 初始层高为1
  2. 抛硬币升级:每次有50%概率增加层高(最大32层)
  3. 最终层高确定后,建立各级连接

2.3 查询过程:从"特快列车"到"慢车"

假设查找score=800的元素:

  1. 从最高层(L3)开始快速移动
  2. 遇到score>800时,下降一层(L2)
  3. 继续移动,再次遇到score>800时,下降到最底层(L1)
  4. 最终找到目标节点
bash 复制代码
L3: head → 300 → 900(超了!下降)
         ↓
L2: head → 300 → 500 → 900(超了!下降)
             ↓
L1: head → 300 → 500 → 800(命中!)

三、深度解剖:为什么选择这样的设计?

3.1 时间复杂度对比

操作 跳表 红黑树 压缩列表
插入 O(logN) O(logN) O(N)
删除 O(logN) O(logN) O(N)
范围查询 O(logN + M) O(N) O(N)
单元素查询 O(logN) O(logN) O(N)

(M为范围内元素数量,跳表在范围查询时表现惊艳)

3.2 跳表 VS 红黑树:程序员的选择困难症

维度 跳表 红黑树
实现难度 50行代码搞定(简单) 500行代码(复杂)
范围查询 双向链表天然支持 需要额外处理
内存局部性 链表节点分散(较差) 树节点紧凑(较好)
并发控制 只需锁局部节点 可能锁整棵树
可视化调试 层级清晰易理解 旋转操作让人眼花

四、冷知识:那些你可能不知道的细节

4.1 分数存储的精度问题

  • Redis使用双精度浮点数存储score
  • 精度陷阱
    ZADD key 9007199254740992 "大数"(2^53)之后的整数会丢失精度
  • 实战忠告 :超过16位有效数字时建议存字符串(比如用<timestamp>.<seq>格式)

4.2 元素排名计算奥秘

  • 跳表节点中的span字段记录每层的跨度(相当于高速公路的里程牌)
  • 计算排名时累加跨度值,实现O(logN)时间复杂度排名查询

4.3 内存回收机制

  • 删除节点时采用延迟回收策略
  • 被删除节点的内存不会立即释放,而是等待后续插入操作复用

五、性能调优:给ZSet装上涡轮增压

5.1 参数调优指南

redis.conf 复制代码
zset-max-ziplist-entries 256   # 适当调大可提升内存利用率
zset-max-ziplist-value 128     # 根据元素平均大小调整

5.2 读写策略优化

  • 写多读少场景 :适当增加zset-max-ziplist-entries减少跳表转换
  • 读多写少场景:降低阈值提前使用跳表提升查询性能
  • 混合场景:保持默认值,让Redis自动选择最佳结构

六、灵魂拷问:如果我来设计ZSet...

假设你是Redis作者antirez,面临这些选择:

  1. 为什么不用B+树
    → 跳表实现更简单,且范围查询性能相当
  2. 为什么不用数组
    → 插入删除成本太高,无法承受高频写操作
  3. 为什么需要哈希表
    → 快速判断元素是否存在(O(1)时间复杂度)

(antirez:小孩子才做选择,成年人全都要!哈希表+跳表的组合才是王道)


七、终极思考题

  1. 当ZSet中所有元素的score相同时,查询效率会下降吗? → 答:不影响,跳表依然保持O(logN)效率,但范围查询会退化成链表遍历

  2. 如何实现score相同的元素按插入时间排序? → 答:将时间戳作为score小数部分,如score = 主分数 + 时间戳*1e-13

  3. ZSet的cardinality很大时,ZCARD命令还是O(1)吗? → 答:是的!跳表头节点存储了长度信息,直接读取即可


总结:ZSet的底层哲学

ZSet的底层设计完美诠释了**"没有银弹,只有权衡"**的工程智慧:

  • 小数据时穿"紧身衣"省内存
  • 大数据时换"多层铠甲"保性能
  • 用空间换时间(哈希表+跳表双结构)
  • 用概率换复杂度(随机层高生成)

理解这些设计原理后,下次使用ZSet时,你仿佛能听到内存中跳表节点们在高歌:"我们不是完美主义者,我们是实用主义战士!" 🛡️

相关推荐
风象南6 分钟前
SpringBoot 控制器的动态注册与卸载
java·spring boot·后端
我是一只代码狗32 分钟前
springboot中使用线程池
java·spring boot·后端
hello早上好1 小时前
JDK 代理原理
java·spring boot·spring
PanZonghui1 小时前
Centos项目部署之Java安装与配置
java·linux
沉着的码农1 小时前
【设计模式】基于责任链模式的参数校验
java·spring boot·分布式
Mr_Xuhhh2 小时前
信号与槽的总结
java·开发语言·数据库·c++·qt·系统架构
Fireworkitte2 小时前
Redis 源码 tar 包安装 Redis 哨兵模式(Sentinel)
数据库·redis·sentinel
纳兰青华2 小时前
bean注入的过程中,Property of ‘java.util.ArrayList‘ type cannot be injected by ‘List‘
java·开发语言·spring·list
coding and coffee2 小时前
狂神说 - Mybatis 学习笔记 --下
java·后端·mybatis
千楼2 小时前
阿里巴巴Java开发手册(1.3.0)
java·代码规范