2-3-4-3-Redis底层数据结构

news/2025/11/11 14:41:48/文章来源:https://www.cnblogs.com/panhua/p/19210250

1、Redis数据结构总览

Redis的数据结构是其核心竞争力之一——它并非简单的“键值对存储”,而是为不同业务场景设计了多样化的值类型,且每个类型都有高效的底层实现(用C语言优化)。理解这些数据结构的设计逻辑底层原理,是掌握Redis的关键。

一、Redis数据结构的整体设计哲学

Redis的数据结构遵循两个核心原则:

  1. 空间效率:优先使用紧凑结构(如压缩列表、整数集合)存储小数据,减少内存占用;
  2. 时间效率:大数据场景切换为高效结构(如哈希表、跳表),保证O(1)或O(logN)的操作复杂度;
  3. 灵活性:支持动态转换(如Hash从小数据转为哈希表),适应不同阶段的数据增长。

二、基础数据结构(最常用)

1. String(字符串)—— 万能的“值”类型

基本概念

Redis的String是最基础的类型,键对应的值是字符串(也可以是数字、二进制数据),支持增删改查原子计数

底层实现:SDS(Simple Dynamic String)

Redis没有用C语言的原生字符串,而是自定义了SDS(简单动态字符串),结构如下:

struct sdshdr {int len;     // 已使用的字节数(字符串长度)int alloc;   // 总分配的字节数(len + 剩余空间)char buf[];  // 字符数组(存储实际数据,末尾用'\0'结尾,兼容C字符串)
};

SDS的优势(对比C原生字符串):

  • 二进制安全:可以存储图片、音频等二进制数据(C字符串遇到'\0'会终止,SDS用len判断长度);
  • 预分配策略:追加字符串时,会额外分配1倍空间(如原len=5,alloc=10,追加3字节后len=8,alloc=10,减少扩容次数);
  • 避免内存重分配:修改字符串时,SDS只需调整len或alloc,无需重新分配内存(除非空间不足);
  • 计算长度快:直接取len属性,O(1)时间(C字符串需遍历到'\0',O(n))。

核心特性

  • 原子计数incr/decr命令是原子的(适合做点赞数、访问量);
  • 支持过期:可以给String设置TTL(如缓存过期);
  • 多语言支持:可以存储JSON、XML等序列化数据。

应用场景

  • 缓存:存储用户信息(如user:123{"name":"张三","age":25});
  • 计数器:文章点赞数(incr like:123)、接口调用次数;
  • 分布式锁:用set key value NX PX 30000实现互斥(value是客户端唯一标识)。

面试追问

  • 为什么SDS比C字符串更适合Redis?

    答:二进制安全、预分配减少扩容、计算长度快,符合Redis高性能的要求。

  • String能存储多大?

    答:默认最大512MB(可通过proto-max-bulk-len配置)。

2. Hash(哈希表)—— 对象属性存储

基本概念

Hash是键值对的集合(类似Java的HashMap),适合存储对象的属性(如用户的姓名、年龄、地址)。

底层实现:压缩列表(ziplist)→ 哈希表(hashtable)

Redis的Hash采用渐进式转换

  • 小数据(键值对≤512,或值长度≤64字节):用压缩列表(ziplist)—— 一种紧凑的线性结构,节省内存;
  • 大数据:转换为哈希表(hashtable)—— 基于链地址法解决冲突,O(1)查询效率。

压缩列表的结构(简化):

zlbytes(总字节数)→ zltail(最后一个节点偏移)→ zllen(节点数)→ [节点1→节点2→…]→ zlend(结束标志)

每个节点存储(都是SDS)。

核心特性

  • 内存紧凑:小数据用压缩列表,减少内存占用;
  • 查询高效:大数据用哈希表,O(1)时间获取属性;
  • 支持批量操作hgetall/hmset等命令可以批量读写属性。

应用场景

  • 对象存储:用户资料(user:123{name:张三, age:25, gender:男});
  • 购物车:存储商品ID和数量(cart:123{商品ID:1001:数量:2, 商品ID:1002:数量:1});
  • 配置项:存储系统的配置(如config:app1{timeout:3000, maxThreads:10})。

面试追问

  • Hash的底层为什么用压缩列表?

    答:压缩列表比哈希表更省内存,适合小数据场景。

  • Hash的键值对数量超过多少会转哈希表?

    答:默认512个键值对,或值长度超过64字节(可通过hash-max-ziplist-entrieshash-max-ziplist-value配置)。

3. List(列表)—— 有序的元素集合

基本概念

List是有序、可重复的元素集合(类似Java的LinkedList),支持头部/尾部插入删除(O(1)),适合做队列

底层实现:快速列表(quicklist)

Redis的List采用快速列表—— 是双向链表压缩列表的组合:

  • 双向链表的每个节点存储一个压缩列表(称为“ziplist node”);
  • 当压缩列表的大小超过阈值(默认8KB),会拆分成新的双向链表节点。

快速列表的结构

双向链表头 → [ziplist node1] → [ziplist node2] → … → 双向链表尾

每个ziplist node存储多个元素(如100个),兼顾了内存紧凑(压缩列表)和插入效率(双向链表)。

核心特性

  • 两端高效lpush/rpush/lpop/rpop都是O(1)时间;
  • 内存优化:压缩列表减少内存占用,双向链表避免大元素的移动;
  • 支持切片lrange命令可以获取指定范围的元素(如lrange list 0 9取前10个)。

应用场景

  • 消息队列:用rpush生产消息,lpop消费消息(FIFO);
  • 最新动态:存储用户的最新朋友圈(user:123:feed[动态1, 动态2, ..., 动态10],用ltrim保持最多10条);
  • :用lpushlpop实现后进先出(LIFO)。

面试追问

  • 为什么List用快速列表而不是纯双向链表?

    答:纯双向链表每个节点需要存储前后指针(占用更多内存),快速列表用压缩列表存储元素,减少了内存开销。

  • List能存储多大的元素?

    答:单个元素最大512MB,整个List的最大长度受内存限制。

4. Set(集合)—— 去重的无序集合

基本概念

Set是无序、唯一的元素集合(类似Java的HashSet),支持交集/并集/差集运算,适合做去重关系计算

底层实现:整数集合(intset)→ 哈希表(hashtable)

Redis的Set采用渐进式转换

  • 小数据(元素都是整数,且数量≤512):用整数集合(intset)—— 一种紧凑的整数存储结构;
  • 大数据非整数元素:转换为哈希表(hashtable)—— 键是元素,值是null(保证唯一性)。

整数集合的结构

struct intset {uint32_t encoding; // 编码方式(存储4/8/16字节整数)uint32_t length;   // 元素数量int8_t contents[]; // 元素数组(按升序存储)
};

核心特性

  • 去重:自动去重(插入重复元素会返回0);
  • 集合运算sinter(交集)、sunion(并集)、sdiff(差集);
  • 随机元素srandmember可以随机获取一个元素(适合抽奖)。

应用场景

  • 去重:存储用户的标签(如user:123:tags{美食, 旅游, 电影},不会重复);
  • 共同好友:用sinter计算两个用户的共同好友(sinter friend:user1 friend:user2);
  • 抽奖:用srandmember随机抽取中奖用户。

面试追问

  • Set的底层为什么用整数集合?

    答:整数集合比哈希表更省内存,适合存储小整数。

  • Set能存储非整数吗?

    答:可以,当元素是非整数或数量超过阈值时,会转换为哈希表。

5. Sorted Set(有序集合)—— 带分数的排序集合

基本概念

Sorted Set是有序、唯一的元素集合(类似Java的TreeSet),每个元素有一个分数(score),Redis根据分数排序(默认升序)。支持按分数范围排名查询,适合做排行榜

底层实现:跳表(skiplist)→ 哈希表

Redis的Sorted Set采用双结构组合

  • 跳表(skiplist):存储元素和分数,支持O(logN)的插入、删除、范围查询;
  • 哈希表:键是元素,值是分数,支持O(1)查找元素的分数。

跳表的结构(简化):

跳表是一种多层索引结构,每层是下一层的“快速通道”:

  • 最底层(Level 0)是所有元素的有序链表;
  • 上层(Level 1、Level 2…)是下层的子集,节点随机晋升(概率50%)。

查询流程(比如找分数≥100的元素):

从顶层开始,找到最后一个小于100的节点,然后往下层遍历,直到找到目标。

核心特性

  • 有序性:根据分数自动排序(升序或降序);
  • 范围查询zrange(正序)、zrevrange(倒序)可以获取指定分数范围的元素;
  • 快速查找:哈希表支持O(1)查找元素的分数。

应用场景

  • 排行榜:游戏积分排名(rank{用户1:1000分, 用户2:900分},用zrevrange取前10名);
  • 时间线:微博热搜(分数是时间戳,zrange按时间正序获取);
  • 延迟队列:用分数作为执行时间,zrangebyscore获取到期的任务。

面试追问

  • 为什么Sorted Set用跳表而不用红黑树?

    答:① 跳表支持范围查询(红黑树需中序遍历);② 跳表实现更简单(无需旋转节点);③ 跳表的查询/插入/删除时间复杂度都是O(logN),符合Redis的需求。

  • 跳表的层数最多是多少?

    答:默认最多32层(可通过zset-max-level配置),足够支撑千万级数据。

三、高级数据结构(解决复杂场景)

1. Stream(流)—— 持久化消息队列

基本概念

Stream是Redis 5.0引入的持久化消息队列(类似Kafka的Topic),支持消费者组消息确认,解决了传统Pub/Sub“消息易丢失”的问题。

底层实现:基数树(radix tree)

Stream的底层是基数树—— 一种紧凑的树结构,适合存储有序的键值对(消息ID是时间戳-序列号,如1678901234567-0)。

核心特性

  • 持久化:消息存储在Stream中,不会随消费者离线而丢失;
  • 消费者组:多个消费者共同消费一个Stream(负载均衡);
  • 消息确认:消费者处理完消息后需ACK,避免重复消费。

应用场景

  • 订单状态变更:生产者发送订单状态(如“支付成功”),消费者组处理后续逻辑(如发货、通知用户);
  • 日志收集:多个服务发送日志到Stream,消费者组统一处理(如写入Elasticsearch)。

2. Bitmap(位图)—— 二进制位操作

基本概念

Bitmap是基于String的位操作(每个字符是8位),适合做统计(如活跃用户、签到)。

核心特性

  • 空间高效:一个位代表一个状态(0/1),比如100万用户的签到状态只需125KB(1000000/8/1024);
  • 快速统计bitcount可以统计1的数量(如统计本月活跃用户)。

应用场景

  • 签到系统:用户每天签到用setbit user:123:sign 20240501 1,统计签到次数用bitcount user:123:sign
  • 活跃用户:用Bitmap记录用户是否活跃(如active:202405用户ID:1表示活跃)。

3. HyperLogLog(基数估算)—— 统计唯一元素数量

基本概念

HyperLogLog是基数估算算法(基于概率论),适合统计大量数据的唯一数量(如UV),误差约1%。

核心特性

  • 空间极小:每个HyperLogLog只需12KB,却能估算2^64个元素;
  • 近似统计:不是精确值,但误差可控(约1%)。

应用场景

  • UV统计:统计每日新增的独立用户(pfadd uv:20240501 user1 user2 ...pfcount uv:20240501);
  • 去重统计:统计文章的独立阅读量。

四、数据结构对比总结

数据结构 底层实现 核心场景 优势
String SDS 缓存、计数器、分布式锁 二进制安全、预分配、原子计数
Hash 压缩列表→哈希表 对象属性、购物车、配置项 内存紧凑、查询高效
List 快速列表 消息队列、最新动态、栈 两端高效、内存优化
Set 整数集合→哈希表 去重、共同好友、抽奖 去重、集合运算
Sorted Set 跳表→哈希表 排行榜、时间线、延迟队列 有序性、范围查询
Stream 基数树 持久化消息队列、订单状态通知 持久化、消费者组、消息确认
Bitmap String的位操作 签到系统、活跃用户统计 空间高效、快速统计
HyperLogLog 概率算法 UV统计、独立阅读量统计 空间极小、近似统计

五、面试高频问题总结

  1. Redis的String为什么用SDS而不是C字符串?

    答:SDS二进制安全、预分配减少扩容、计算长度快,符合Redis高性能要求。

  2. Hash的底层什么时候转哈希表?

    答:默认键值对≤512或值长度≤64字节时用压缩列表,超过则转哈希表。

  3. Sorted Set为什么用跳表?

    答:支持范围查询、实现简单、查询/插入/删除O(logN)。

  4. Bitmap和HyperLogLog的区别?

    答:Bitmap统计精确的唯一数量(如签到次数),HyperLogLog估算基数(如UV)。

六、总结

Redis的数据结构是“为场景而生”——每个类型都针对不同的业务需求做了优化,从基础的String到高级的Stream,覆盖了缓存、对象存储、消息队列、统计等几乎所有常见场景。掌握这些数据结构的底层实现应用场景,不仅能应对面试,更能在工程中选对数据结构,提升系统性能。

若有具体场景(如秒杀系统中的库存存储),可以进一步深入探讨如何选择数据结构。

2、数据结构底层一览表

以下是Redis数据类型与底层数据结构、变更触发条件的结构化梳理表格,包含核心逻辑、配置参数及实际场景说明:

一、Redis数据类型底层结构与变更触发条件总表

Redis数据类型 底层数据结构 升级触发条件(从紧凑结构→高效结构) 降级触发条件 结构特点与设计逻辑
String 整数/浮点数/SDS(简单动态字符串) - 存整数:值在LONG_MIN~LONG_MAX之间(如SET key 123) - 存浮点数:值为浮点型(如SET key 45.6) - 存字符串/超范围数值:其他所有情况(如SET key "hello" 不支持(数值转字符串后无法自动转回) 整数/浮点数极致省内存;SDS支持字符串操作(如追加、截断),无降级机制
Hash 压缩列表(ziplist)/哈希表(hashtable) 满足任一条件: 1. 字段数>hash-max-ziplist-entries(默认512) 2. 字段值长度>hash-max-ziplist-value(默认64字节) 不支持(Redis4.0+) ziplist内存紧凑(无额外指针开销);哈希表O(1)操作;超过阈值后永久转哈希表
List 压缩列表(ziplist)/快速列表(quicklist) 满足任一条件: 1. 元素个数>list-max-ziplist-entries(默认512) 2. 单元素大小>list-max-ziplist-size(默认-2,即每个节点≤64字节) 不支持 quicklist是ziplist的链表(将多个ziplist节点串起来),兼顾内存与性能;转后不可逆
Set 整数集合(intset)/哈希表(hashtable) 满足任一条件: 1. 含非整数元素(如字符串) 2. 元素个数>set-max-intset-entries(默认512) 不支持 intset仅存整数(极致省内存);哈希表支持任意类型;转换后不可逆
Sorted Set 压缩列表(ziplist)/跳跃表+哈希表 满足任一条件: 1. 元素个数>zset-max-ziplist-entries(默认128) 2. 成员/分值长度>zset-max-ziplist-value(默认64字节) 不支持 ziplist紧凑但查询慢(O(N));跳跃表O(logN)排序+哈希表O(1)查分值;转后不可逆

二、关键概念与细节补充

1. 核心设计逻辑:空间换时间 vs 时间换空间

Redis的底层结构转换本质是平衡内存占用与操作性能

  • 紧凑结构(ziplist/intset):无额外指针开销,内存利用率高,但操作复杂度高(如ziplist查找需遍历O(N));
  • 高效结构(hashtable/quicklist/skiplist):引入指针或分层结构,内存占用大,但操作效率高(如hashtable的O(1)查询)。

2. 配置参数说明(可通过CONFIG GET查看)

参数名 含义
hash-max-ziplist-entries Hash中字段数的最大阈值(超过则转哈希表)
hash-max-ziplist-value Hash中字段值长度的最大阈值(超过则转哈希表)
list-max-ziplist-entries List中元素个数的最大阈值(超过则转快速列表)
list-max-ziplist-size List中单个ziplist节点的大小(默认-2表示≤64字节)
set-max-intset-entries Set中元素个数的最大阈值(超过则转哈希表)
zset-max-ziplist-entries Sorted Set中元素个数的最大阈值(超过则转跳跃表+哈希表)
zset-max-ziplist-value Sorted Set中成员/分值长度的最大阈值(超过则转跳跃表+哈希表)

3. 实际场景举例

  • Hash升级:假设hash-max-ziplist-entries=512,当HSET user:1000添加第513个字段时,Hash从ziplist转成hashtable。即使后续删除字段回到500个,也不会转回ziplist(转换成本高于保持哈希表)。
  • Sorted Set升级ZADD rank 100 "very_long_member_which_exceeds_64_bytes",因成员长度超过zset-max-ziplist-value,Sorted Set从ziplist转成跳跃表+哈希表。
  • String类型切换SET key 123(整数)→ SET key "hello"(SDS)→ SET key 45.6(浮点数)→ SET key "world"(SDS),无降级。

三、面试延伸问题(苏格拉底式追问)

  1. 为什么Redis选择快速列表而非直接用哈希表存储List?

    (提示:快速列表是ziplist的链表,既避免了哈希表的指针开销,又解决了ziplist长链表的操作效率问题,兼顾内存与性能。)

  2. 如果你希望Hash尽可能省内存,应该调整哪个配置?会有什么风险?

    (提示:增大hash-max-ziplist-entrieshash-max-ziplist-value,让更多Hash用ziplist存储;风险是Hash操作(如HGETALL)的时间复杂度会上升,可能导致延迟。)

  3. Sorted Set的跳跃表+哈希表组合,解决了什么问题?

    (提示:跳跃表支持O(logN)的排序操作(如ZRANGE),哈希表支持O(1)的成员分值查询(如ZSCORE),两者结合平衡了排序与点查的需求。)

四、总结建议

  • 记住核心转换条件:字段数/元素数/长度超过配置阈值→从紧凑结构转高效结构;
  • 理解不可逆性:大部分转换是单向的(避免反复转换的开销);
  • 调整配置需结合场景:内存紧张则增大紧凑结构的阈值,性能优先则减小阈值。

如果需要深入某个数据类型的源码细节(如ziplist的编码方式),或扩展到分布式场景下的Redis使用(如集群中的数据结构一致性),可以随时告诉我~

3、压缩列表(ZipList)

要理解Redis压缩列表(ZipList),我们需要从设计目标内存模型核心字段解析Entry结构连锁更新问题实际应用场景六个维度展开,并通过具体例子还原其内存布局。

一、压缩列表的设计目标

Redis作为内存数据库,内存利用率是核心指标之一。压缩列表是Redis为小批量数据设计的极致紧凑的线性数据结构,旨在替代传统的链表或数组,解决以下痛点:

  • 链表的每个节点需要额外的指针(prev/next),内存开销大;
  • 数组的扩容会导致内存碎片和拷贝成本;
  • 小数据的存储不需要复杂的结构,用紧凑格式能大幅减少内存占用。

压缩列表的核心取舍:牺牲部分读写性能(遍历/修改需线性扫描),换取极高的内存利用率。

二、压缩列表的整体内存模型

压缩列表是一个连续的内存块,由以下5部分组成(按顺序排列):

字段名 字节数 描述
zlbytes 4 整个压缩列表的总内存字节数(包括自身),用于快速计算结束位置
zltail 4 最后一个Entry的起始偏移量(相对于压缩列表首地址),用于快速定位尾部
zllen 2 Entry的数量;若数量≥65535,则值为0xFFFF,需遍历Entry计算
EntryX 可变 存储实际数据的Entry序列
zlend 1 结束标志,固定值0xFF(255)

三、Entry的核心结构:变长设计是内存紧凑的关键

每个Entry存储一个数据元素(字符串/整数),由3部分组成,通过变长编码最小化开销

字段名 描述
prevlen 前一个Entry的长度(用于反向遍历);变长:<254字节用1字节,≥254用5字节(首字节0xFE+4字节长度)
encoding 数据类型和长度的编码(关键!通过位运算区分字符串/整数及长度)
content 实际数据内容(字符串的二进制数据/整数的二进制表示)

关键:encoding字段的编码规则

encoding高位比特标识数据类型,低位比特标识长度。以下是常见编码(以二进制为例):

1. 字符串类型(前两位00

  • 长度范围:1~63字节(6位能表示的最大值)
  • 编码格式:00xxxxxxxxxxxx是长度的低6位)
  • 示例:字符串长度3→编码000000110x03

2. 整数类型(前两位11

  • 直接存储整数的二进制值,无需额外content字段(content就是整数本身)
  • 常见编码:
    • 8位整数:111111100xFE)→后续1字节存整数
    • 16位整数:110000000xC0)→后续2字节存整数
    • 32位整数:110100000xD0)→后续4字节存整数
    • 64位整数:111000000xE0)→后续8字节存整数

3. 特殊短整数(前四位1111

  • 长度1~15的整数:1111xxxxxxxx直接是整数值)
  • 示例:整数5→编码111101010xF5

四、实战例子:还原压缩列表的内存布局

我们用一个具体的Redis列表场景演示压缩列表的内存模型:

假设列表存储3个短字符串:["a", "bb", "ccc"],且Redis配置该列表用ZipList存储。

步骤1:计算每个Entry的字段值

我们从头字段Entry再到结束标志逐步计算:

1. 头字段(固定格式)

  • zlbytes:总内存字节数(后续计算总和)→先留空
  • zltail:最后一个Entry的起始偏移→先留空
  • zllen:Entry数量=3→0x0003(2字节)

2. Entry1:字符串"a"(长度1)

  • prevlen:第一个Entry,前无节点→0x00(1字节)
  • encoding:字符串长度1→000000010x01,1字节)
  • content"a"的二进制→0x61(1字节)
  • Entry1总长度:1+1+1=3字节

3. Entry2:字符串"bb"(长度2)

  • prevlen:Entry1的长度=3→0x03(1字节,<254)
  • encoding:字符串长度2→000000100x02,1字节)
  • content"bb"的二进制→0x62, 0x62(2字节)
  • Entry2总长度:1+1+2=4字节

4. Entry3:字符串"ccc"(长度3)

  • prevlen:Entry1+Entry2的长度=3+4=7→0x07(1字节,<254)
  • encoding:字符串长度3→000000110x03,1字节)
  • content"ccc"的二进制→0x63, 0x63, 0x63(3字节)
  • Entry3总长度:1+1+3=5字节

5. 结束标志zlend

  • 固定值0xFF(1字节)

步骤2:计算头字段的最终值

  • zlbytes:头字段(4+4+2=10字节) + Entry1(3) + Entry2(4) + Entry3(5) + zlend(1)= 23字节0x00000017(4字节)
  • zltail:最后一个Entry(Entry3)的起始偏移→头字段10字节 + Entry1的3字节 + Entry2的4字节 = 17字节0x00000011(4字节)

步骤3:完整内存布局(按字节偏移,从0开始)

偏移范围 字段 十六进制值 描述
0-3 zlbytes 00 00 00 17 总内存23字节
4-7 zltail 00 00 00 11 最后一个Entry起始于偏移17(字节)
8-9 zllen 00 03 3个Entry
10 Entry1.prevlen 00 前无节点,长度0
11 Entry1.encoding 01 字符串长度1
12 Entry1.content 61 "a"的二进制(ASCII码)
13 Entry2.prevlen 03 前一个Entry长度3
14 Entry2.encoding 02 字符串长度2
15-16 Entry2.content 62 62 "bb"的二进制
17 Entry3.prevlen 07 前两个Entry总长度7
18 Entry3.encoding 03 字符串长度3
19-21 Entry3.content 63 63 63 "ccc"的二进制
22 zlend FF 结束标志

五、压缩列表的优缺点与连锁更新问题

1. 优点

  • 内存极致紧凑:通过变长prevlenencoding,几乎无冗余开销;
  • 连续内存:CPU缓存友好(遍历时缓存命中率高);
  • 实现简单:无需复杂的指针操作。

2. 缺点与限制

  • 连锁更新风险:若某个Entry的prevlen从1字节扩容到5字节(比如前一个Entry长度增加导致当前Entry的prevlen需要更多字节),会触发后续Entry的prevlen连锁扩容,导致内存重新分配和数据拷贝,性能急剧下降;
  • 查找效率低:需线性遍历Entry(O(n)时间复杂度),不适合大量数据;
  • 修改成本高:插入/删除Entry可能需要重新分配内存(比如Entry长度变化导致后续所有Entry偏移改变)。

3. Redis的规避策略

压缩列表仅用于小批量、长度稳定的数据:

  • 列表(List):当元素数量≤list-max-ziplist-size(默认512)且每个元素长度≤64字节时,用ZipList;否则转成QuickList(ZipList的链表封装);
  • 哈希(Hash):当字段数量≤hash-max-ziplist-entries(默认512)且每个值长度≤64字节时,用ZipList;否则转成Dict(哈希表);
  • 有序集合(ZSet):当元素数量≤zset-max-ziplist-entries(默认128)且每个元素长度≤64字节时,用ZipList存储成员和分数;否则转成SkipList+Dict

六、总结:压缩列表的本质

压缩列表是Redis“用时间换空间”的典型设计:

  • 放弃了链表的快速插入/删除和数组的随机访问;
  • 换来了极高的内存利用率,适合存储小而固定的数据;
  • 核心是通过变长编码prevlen/encoding)最小化每个字段的开销。

理解压缩列表,不仅能帮你掌握Redis的底层实现,更能让你学会如何在内存受限场景下设计紧凑的数据结构——这是架构师必备的“内存优化思维”。

七、互动提问(帮你适应面试节奏)

  1. 如果压缩列表的zllen0xFFFF,Redis怎么获取真实的Entry数量?
  2. 为什么连锁更新是压缩列表的致命缺陷?如何避免?
  3. 假设你要存储1000个长度为10的字符串,为什么Redis不会用压缩列表?

4、LIst结构中的快速列表(QuickList)

要理解Redis的QuickList,需要从设计背景核心结构关键特性操作逻辑配置调优场景价值六个维度展开,最终明确它为何是Redis List类型的“最优解”。

Redis QuickList是通用QuickList思想在特定场景下的最优实践——它的“特殊”恰恰是因为“适配Redis List的需求”。关键优化是将通用QuickList的思想与Redis的具体需求深度结合

  • ZipList替代普通数组块,获得更紧凑的内存存储
  • 双向链表解决ZipList的连锁更新
  • 压缩机制进一步优化内存,适配List的高频场景。

一、设计背景:解决ZipList和纯双向链表的痛点

Redis早期用ZipList(压缩列表)实现List,但遇到两个致命问题:

  1. 连锁更新风险:ZipList是连续内存块,每个元素记录前一个元素的长度。当插入/删除元素导致前一个元素长度变化时,会触发后续所有元素的长度字段更新(“连锁反应”),极端情况下性能暴跌。
  2. 大列表内存浪费:ZipList为了紧凑存储,牺牲了随机访问效率,且当列表极大时,单一连续内存块的管理成本高。

纯双向链表(如Java的LinkedList)虽然避免了连锁更新,但每个节点需要额外的prev/next指针(每个指针占8字节,64位系统),内存开销大(比如存100万个元素,光指针就占16MB)。

QuickList的出现,将两者结合:用双向链表组织多个ZipList节点,既避免连锁更新,又保留紧凑存储。

二、核心结构:双向链表 + ZipList节点

QuickList的本质是一个轻量级双向链表,每个节点存储一个ZipList(或压缩后的ZipList)。我们通过Redis源码的简化结构理解:

1. QuickList整体结构(quicklist

typedef struct quicklist {quicklistNode *head;    // 链表头节点quicklistNode *tail;    // 链表尾节点unsigned long len;      // QuickList的总元素数(所有节点的count之和)unsigned long count;    // 所有节点的ZipList总内存占用?非必须,辅助统计int fill : 16;          // 每个ZipList节点的最大元素数(由list-max-ziplist-size决定)unsigned int compress : 16; // 压缩深度(由list-compress-depth决定)
} quicklist;

2. QuickList节点(quicklistNode

每个节点对应一个ZipList,结构如下:

typedef struct quicklistNode {struct quicklistNode *prev;  // 前驱节点struct quicklistNode *next;  // 后继节点unsigned char *zl;           // 指向该节点的ZipList内存块unsigned int sz;             // 该ZipList的内存大小(字节)unsigned int count;          // 该ZipList中的元素个数int compress;                // 是否被压缩(LZF算法)
} quicklistNode;

3. 关键关系图

QuickList (quicklist)
│
├─ head → quicklistNode1 (ZipList1: [a,b,c])
│        │
│        ├─ prev: NULL
│        ├─ next: quicklistNode2
│        └─ zl: 指向ZipList1的内存
│
├─ tail → quicklistNodeN (ZipListN: [x,y,z])
│        │
│        ├─ prev: quicklistNodeN-1
│        ├─ next: NULL
│        └─ zl: 指向ZipListN的内存
│
└─ len: N (总元素数=所有ZipList的count之和)

三、关键特性:平衡空间与时间的艺术

QuickList的核心优势在于四个折中

1. 避免连锁更新:每个节点独立ZipList

每个quicklistNode存储独立的ZipList,插入/删除元素仅影响当前节点的ZipList,不会触发“连锁反应”。

2. 内存紧凑:ZipList的压缩存储

每个节点的ZipList是连续内存,元素以变长编码存储(比如整数用4字节而非对象指针),比纯双向链表节省30%-50%内存。

3. 支持两端快速操作:O(1)头插/尾插

通过双向链表的headtail指针,直接定位到首尾节点,在节点的ZipList尾部/头部插入元素(ZipList的尾插/头插是O(1))。

4. 可选的节点压缩:LZF算法节省内存

Redis支持对非首尾的中间节点进行LZF压缩(配置list-compress-depth),压缩比可达5-7倍(比如100字节压缩到20字节),但会增加CPU开销(访问时需解压)。

四、操作逻辑:时间复杂度与实际效率

QuickList的操作复杂度取决于操作的 position(头/尾/中间):

操作 时间复杂度 实际效率说明
lpush/rpush O(1) 直接操作头/尾节点的ZipList,无需遍历。
lpop/rpop O(1) 同上,直接弹出头/尾节点的ZipList元素。
linsert(中间) O(n/m + m) n是总元素数,m是节点的元素数(由fill决定)。先遍历双向链表找到节点(O(n/m)),再在节点的ZipList插入(O(m))。
lindex(中间) O(n/m + m) 先找到节点,再在节点的ZipList中查找元素(ZipList的随机访问是O(m))。

关键结论

由于fill(每个节点的最大元素数)通常配置为5-10,因此中间操作的实际效率接近O(n),但远优于纯ZipList的连锁更新。

五、配置调优:两个核心参数

QuickList的行为由Redis的两个配置参数控制:

1. list-max-ziplist-size:每个节点的最大容量

  • 正数:表示每个节点最多存多少个元素(比如5→每个节点最多5个元素)。
  • 负数:表示每个节点的最大内存大小(Redis的映射规则:-1→4KB,-2→8KB,-3→16KB,-4→32KB,-5→64KB)。

调优建议

  • 若元素是小整数(如ID),可设为10(每个节点存10个整数,内存紧凑);
  • 若元素是大字符串(如1KB的文本),可设为1(避免单个ZipList过大,降低遍历成本)。

2. list-compress-depth:压缩深度

  • 0:不压缩(默认);
  • 1:头尾第1个节点不压缩,中间节点压缩;
  • 2:头尾前2个节点不压缩,中间节点压缩;
  • 以此类推。

调优建议

  • 频繁访问中间元素(如取第1000个元素),设为1(减少解压开销);
  • 很少访问中间元素(如仅操作头尾),设为3或更高(最大化节省内存)。

六、场景价值:为什么Redis选QuickList?

QuickList完美匹配Redis List的典型使用场景

  1. 消息队列:用rpush+lpop实现FIFO队列,QuickList的两端O(1)操作保证效率;
  2. 最新评论列表:用lpush+lrange 0 9获取最新10条评论,QuickList的头插+头遍历高效;
  3. 大列表存储:比如存储10万条用户行为日志,QuickList的节点压缩节省内存,同时避免连锁更新。

七、常见坑点与避坑指南

  1. fill设置过小:若fill=1,QuickList会有10万个节点,双向链表的指针占用大量内存(10万×16字节=1.6MB),且遍历变慢。建议fill≥5
  2. 过度压缩:若compress-depth=5,大部分节点被压缩,访问中间元素时CPU开销激增。根据访问模式调整:频繁访问中间→压缩浅,否则压缩深。
  3. 元素类型过大:若List存的是1MB的大对象,QuickList的节点ZipList会变得很大,失去紧凑优势。建议大对象用Hash或String存储

总结:QuickList的本质

QuickList是Redis对“空间效率”与“时间效率”的经典权衡:

  • 双向链表解决ZipList的连锁更新问题;
  • ZipList解决双向链表的内存浪费问题;
  • 可选压缩进一步优化内存,适配不同场景。

它不是“完美的数据结构”,但绝对是最适合Redis List场景的结构——这也是Redis“用尽可能少的内存,换尽可能高的性能”的设计哲学体现。

互动提问

  1. 如果让你设计一个实时更新的排行榜(需要频繁头插、尾删、随机访问中间元素),你会选QuickList吗?为什么?
  2. Redis的Hash类型底层也用了类似“分块存储”的结构(ZipList→Dict),你能对比一下Hash和List的分块策略差异吗?

5、哈希表(Hashtable)

(1)Hashtable的核心定义与本质

要回答这个问题,我们需要先明确Hashtable的核心定义与本质,再分析跨语言实现的通用性与差异——本质上,Hashtable是哈希表的经典实现,是几乎所有编程语言都会提供的“基础数据结构工具”。

一、Hashtable是什么?——从定义到核心机制

Hashtable(哈希表,注意与HashMap的差异)是一种基于哈希函数实现的键值对(Key-Value)存储数据结构,核心目标是通过哈希计算将键映射到内存地址,实现O(1)时间复杂度的查找、插入、删除

1. 核心结构与关键机制

Hashtable的本质是一个数组+哈希函数+冲突解决策略的组合体:

  • 数组:存储数据的底层容器(称为“桶”,Bucket);
  • 哈希函数:将任意类型的Key转换为数组的下标(hash(key) % array_length);
  • 冲突解决:当两个不同Key的哈希值映射到同一个数组下标时(哈希碰撞),需要解决冲突的策略——最常用的是链地址法(Separate Chaining),即每个数组元素指向一个链表(或红黑树,如Java 8+的HashMap),将碰撞的Key-Value存在链表中。

2. 核心操作的时间复杂度

  • 理想情况(无冲突):Put/Get/Remove均为O(1);
  • 最坏情况(所有Key都碰撞):退化为链表,时间复杂度O(n);
  • 平均情况:取决于哈希函数的均匀性和冲突解决策略,通常接近O(1)。

3. 与HashMap的关键差异(以Java为例)

很多语言中“哈希表”的实现类会命名为HashMap,而Hashtable线程安全的 legacy 实现

  • 线程安全:Hashtable的所有方法都用synchronized修饰(类级锁),而HashMap是非线程安全的;
  • Null支持:Hashtable不允许Key或Value为null(会抛NullPointerException),HashMap允许1个nullKey和多个nullValue;
  • 迭代器:Hashtable的迭代器是“fail-safe”(迭代时修改不会抛异常,但可能反映修改后的状态),HashMap的迭代器是“fail-fast”(迭代时修改会抛ConcurrentModificationException);
  • 继承体系:Hashtable继承自Dictionary(已过时),HashMap继承自AbstractMap

二、各种语言都能实现Hashtable吗?——通用性与语言特性

结论所有现代编程语言都可以实现Hashtable,因为它是基础数据结构的普适实现。不同语言的实现细节(命名、线程安全、冲突策略)可能不同,但核心原理一致。

1. 主流语言的Hashtable实现示例

以下是常见语言中“哈希表”的具体实现及特点:

语言 实现类/结构 核心特点
Java Hashtable 线程安全(类级锁)、不允许null、继承Dictionary
Java HashMap 非线程安全、允许null、继承AbstractMap(更常用,底层是数组+链表+红黑树)
Python dict 底层是开放寻址法的哈希表(不是链地址法)、不允许null Key、动态扩容
Go map 内置类型、非线程安全(需用sync.Map实现线程安全)、自动扩容
C++ std::unordered_map 基于哈希表、非线程安全、允许自定义哈希函数和相等比较器
JavaScript Object/Map Object是特殊的哈希表(键只能是字符串/Symbol)、Map支持任意类型键、非线程安全

2. 跨语言实现的共性

无论语言如何实现,Hashtable的核心逻辑一致:

  • 哈希函数:将Key映射到数组下标(如Python的hash()函数、Java的hashCode());
  • 冲突解决:要么链地址法(Java HashMap),要么开放寻址法(Python dict);
  • 动态扩容:当元素数量超过“负载因子×数组长度”时,自动扩容(如Java HashMap的负载因子默认0.75,扩容为原数组的2倍)。

3. 语言特有的优化或差异

  • Python的dict:采用开放寻址法(而非链地址法),当冲突时,会找下一个空闲的数组位置(线性探测),优点是缓存友好(连续内存访问),缺点是扩容成本更高;
  • Go的map:支持“增量式扩容”(避免一次性扩容导致性能骤降),且内置了“哈希种子”(防止哈希碰撞攻击);
  • JavaScript的Map:键可以是任意类型(包括对象),而Object的键只能是字符串——这是因为Map的底层是标准的哈希表实现,而Object是针对字符串键优化的。

三、Hashtable的应用场景与面试高频问题

Hashtable是计算机科学中最常用的数据结构之一,应用场景包括:

  • 缓存:如Redis的String类型底层用SDS,但缓存系统(如Guava Cache)常用哈希表存储键值对;
  • 索引:数据库的哈希索引(如MySQL的Memory引擎);
  • 去重:快速判断元素是否存在(如Python的in操作符底层用dict的哈希表);
  • 路由:API网关的路由规则映射(如Spring Cloud Gateway的路由表)。

面试高频问题(延伸思考)

  1. 为什么Hashtable是线程安全的,但HashMap不是?

    (提示:Hashtable的方法用synchronized修饰,锁住整个对象;HashMap没有同步机制,多线程并发修改会导致数据不一致或死循环。)

  2. 哈希冲突严重时,如何优化Hashtable的性能?

    (提示:1. 换更好的哈希函数(减少碰撞);2. 调整负载因子(提前扩容);3. 将链地址法的链表转为红黑树(如Java HashMap的TREEIFY_THRESHOLD=8)。)

  3. Python的dict为什么用开放寻址法而不是链地址法?

    (提示:开放寻址法的内存布局更紧凑,缓存命中率更高——链表的每个节点需要额外存储指针,而开放寻址法的元素直接存在数组中,访问速度更快。)

四、总结建议

  • Hashtable的核心是哈希函数+冲突解决:不管语言如何实现,这两个机制决定了性能;
  • 线程安全是可选特性:需要线程安全选Hashtable(Java)或sync.Map(Go),否则选更高效的HashMap/dict/map
  • 理解底层实现才能用好:比如Java中HashMap的扩容机制、Python中dict的探测策略,这些细节能帮你解决线上性能问题(如哈希碰撞导致的延迟飙升)。

如果需要深入某个语言的Hashtable源码(如Java HashMap的putVal()方法),或扩展到分布式场景下的哈希表一致性(如Redis Cluster的哈希槽),可以随时问我~

(2)Redis中的哈希表

hash类型在Redis中的数据结构定义:

![image-20251014144121801](/Users/panhua/Library/Application Support/typora-user-images/image-20251014144121801.png)

![image-20251014144128952](/Users/panhua/Library/Application Support/typora-user-images/image-20251014144128952.png)

![image-20251014144138092](/Users/panhua/Library/Application Support/typora-user-images/image-20251014144138092.png)

![image-20251014144145294](/Users/panhua/Library/Application Support/typora-user-images/image-20251014144145294.png)

当 Redis 的 Hash 类型因数据量较大(字段数超过hash-max-ziplist-entries或字段 / 值长度超过hash-max-ziplist-value)触发编码转换,使用hashtable作为底层存储时,其内存数据模型是一个多层嵌套的结构,从上层的数据库字典到最底层的字段 / 值存储,涉及多个核心结构体的关联。

一、整体层次结构概览

Hash 类型(使用 hashtable 编码)的内存模型可分为 5 层,从上层到下层依次为:

Redis数据库(redisDb)→ 全局字典(dict)→ Hash键的redisObject → Hash值的底层字典(dict)→ 哈希表(dictht)→ 哈希节点(dictEntry)→ 字段/值的SDS

每层的核心作用:

  • 数据库层:管理所有键值对(包括 Hash 类型的键);
  • 全局字典层:关联 Hash 键(用户可见的键,如user:1001)与对应的 Hash 值;
  • redisObject 层:标识 Hash 类型及底层编码(hashtable);
  • 底层字典层:存储 Hash 的 field-value 对,是 Hash 类型的核心数据容器;
  • 哈希表及节点层:实现 field-value 对的高效存储与查找;
  • SDS 层:存储 field 和 value 的实际字符串内容。

二、逐层拆解与实例说明

以一个具体例子展开:假设执行HSET user:1001 name "张三" age 25 address "中国北京市朝阳区建国路88号某小区某号楼某单元某室..." email "zhangsan@example.com" ...(字段数超过 512,触发 hashtable 编码)。

1. 第一层:Redis 数据库(redisDb)

Redis 服务器的每个数据库由redisDb结构体表示,核心字段是dict *dict(存储该数据库的所有键值对)。

typedef struct redisDb {dict *dict;         // 数据库的键值对字典(全局字典)// 其他字段(如过期键字典、阻塞键等)
} redisDb;
  • 作用redisDb.dict是一个全局字典,管理当前数据库中所有用户可见的键(包括 String、Hash、List 等类型),其中就包含我们的 Hash 键user:1001

2. 第二层:全局字典(dict)与 Hash 键的 dictEntry

全局字典(redisDb.dict)是一个dict结构体,其底层通过dictht存储键值对,每个键值对对应一个dictEntry(哈希节点)。

对于 Hash 键user:1001,全局字典中存在一个dictEntry,结构如下:

typedef struct dictEntry {void *key;          // 指向Hash键的SDS(即"user:1001")union {void *val;      // 指向Hash值的redisObject// 其他值类型(整数等,此处不涉及)} v;struct dictEntry *next; // 哈希冲突时的下一个节点(此处假设无冲突)
} dictEntry;
  • 关键字段

    • key:指向"user:1001"的 SDS(字符串结构,见下文 SDS 层);
    • v.val:指向 Hash 值对应的redisObject(见第三层);
    • next:为NULL(假设无哈希冲突)。

3. 第三层:Hash 值的 redisObject

Hash 类型的值被redisObject封装,用于标识数据类型和底层编码。

typedef struct redisObject {unsigned type:4;    // 类型:REDIS_HASH(2)unsigned encoding:4;// 编码:REDIS_ENCODING_HT(2,即hashtable)unsigned lru:LRU_BITS; // LRU淘汰时间戳int refcount;       // 引用计数(默认1)void *ptr;          // 指向底层存储Hash的dict(见第四层)
} redisObject;
  • 关键字段

    • type=REDIS_HASH:标识这是 Hash 类型;
    • encoding=REDIS_ENCODING_HT:标识底层用 hashtable 存储;
    • ptr:指向一个dict结构体(即 Hash 的底层字典,存储 field-value 对)。

4. 第四层:Hash 的底层字典(dict)

redisObject.ptr指向的dict是 Hash 类型的核心数据容器,专门用于存储 field-value 对。其结构与全局字典一致,但dictType(操作函数集)针对 Hash 的 field 和 value 设计。

typedef struct dict {dictType *type;     // Hash专用的操作函数集void *privdata;     // 私有数据(传递给操作函数的参数)dictht ht[2];       // 哈希表数组(ht[0]为当前使用的表)long rehashidx;     // rehash索引(-1表示未进行rehash)unsigned long iterators; // 迭代器数量
} dict;
  • 关键字段

    • type
      

      :指向 Hash 专用的

      dictType
      

      ,其中:

      • hashFunction:计算 field 的哈希值(基于 SDS 的哈希函数);
      • keyCompare:比较两个 field 是否相等(基于 SDS 的比较函数);
      • keyDestructor/valDestructor:销毁 field 和 value 的 SDS(释放内存)。
    • ht[0]:当前存储 field-value 对的哈希表(dictht结构体)。

5. 第五层:哈希表(dictht)与 field-value 的 dictEntry

dict.ht[0]是一个dictht结构体,包含桶数组(table),每个桶存储一个dictEntry链表(解决哈希冲突),每个dictEntry对应一个 field-value 对。

typedef struct dictht {dictEntry **table;  // 桶数组(每个元素是dictEntry链表头)unsigned long size; // 桶数量(2的幂,如1024)unsigned long sizemask; // 掩码(size-1,用于计算桶索引)unsigned long used; // field-value对总数(即dictEntry数量)
} dictht;

对于我们的例子,ht[0].table是一个大小为 1024 的桶数组,假设field="address"的哈希值计算后索引为i,则table[i]指向该 field 对应的dictEntry

struct dictEntry {void *key;          // 指向field的SDS(即"address")union {void *val;      // 指向value的SDS(即长地址字符串)// 其他值类型(此处为字符串,用val指针)} v;struct dictEntry *next; // 哈希冲突时的下一个节点(假设无冲突则为NULL)
};
  • 关键字段

    • key:指向"address"的 SDS(field 的字符串内容);
    • v.val:指向长地址字符串的 SDS(因长度超过 64 字节,触发 hashtable 编码);
    • next:为NULL(假设无冲突)。

6. 第六层:field 和 value 的 SDS 存储

Hash 的 field 和 value(非整数)均通过 SDS 存储,以field="address"value="中国北京市朝阳区建国路88号..."为例:

  • field 的 SDS("address")

    长度为 7 字节,使用SDS8类型:

    struct sdshdr8 {uint8_t len;      // 7(字符串长度)uint8_t alloc;    // 7(无预分配,因长度固定)unsigned char flags; // 0x08(SDS8标识)char buf[];       // 存储"address"的ASCII字节:0x61 64 64 72 65 73 73 00(最后00是兼容C的终止符)
    };
    
  • value 的 SDS(长地址字符串)

    假设长度为 100 字节,使用SDS8(因≤255):

    struct sdshdr8 {uint8_t len;      // 100(字符串长度)uint8_t alloc;    // 100(无预分配)unsigned char flags; // 0x08(SDS8标识)char buf[];       // 存储地址的UTF-8字节(如"中国..."的二进制数据,最后加00终止符)
    };
    

三、层次关系图(实例对应)

redisDb
└── dict(全局字典)└── dictEntry(全局键值对节点)├── key: SDS("user:1001")  // Hash键└── v.val: redisObject     // Hash值的封装├── type=REDIS_HASH, encoding=REDIS_ENCODING_HT└── ptr: dict(Hash底层字典)├── type: Hash专用dictType(field操作函数)└── ht[0](当前哈希表)├── size=1024, sizemask=1023└── table(桶数组)├── ...(其他桶)├── table[i](索引i的桶)│   └── dictEntry(field-value节点)│       ├── key: SDS("address")  // field│       ├── v.val: SDS("中国北京市朝阳区...")  // value│       └── next: NULL├── table[j](索引j的桶)│   └── dictEntry(field="name")│       ├── key: SDS("name")│       ├── v.val: SDS("张三")│       └── next: NULL└── ...(其他field-value节点)

四、核心特点总结

  1. 双层 dict 嵌套:全局 dict 管理 Hash 键与 redisObject 的映射,Hash 底层 dict 管理 field 与 value 的映射,两者都是dict结构但作用不同;
  2. SDS 无处不在:Hash 键、field、value 均通过 SDS 存储,保证字符串操作的安全性和效率;
  3. 哈希冲突处理:底层 dict 的dictht.table通过链表(dictEntry.next)解决哈希冲突,保证 O (1) 平均复杂度;
  4. 动态扩缩容:底层 dict 会根据used/size(负载因子)自动触发 rehash,通过ht[1]rehashidx实现渐进式迁移,避免性能抖动。

这种模型既保证了大数据量下 Hash 类型的操作效率(O (1) 增删改查),又通过 SDS 和 dict 的设计兼顾了内存安全性和灵活性

6、整数集合(intset)

要理解Redis的intset(整数集合),我们需要从定义定位核心结构编码与升级机制操作实现内存模型五个维度展开,并结合实际场景说明其设计意图与优缺点。

一、intset的定义与定位

intset是Redis用于存储纯整数集合的底层数据结构,核心目标是在存储小范围整数时最大化内存利用率

触发场景:当Set集合中的元素全部是整数数量未超过配置阈值intset-max-intset-entries,默认512)时,Redis会优先使用intset而非Hashtag或链表。

核心优势:相比Hashtable(需存储键值对指针),intset紧凑的连续内存数组,无额外指针开销,内存利用率可提升30%-50%(视整数大小而定)。

二、intset的核心结构(源码级解析)

intset的结构体定义(来自Redis 7.0源码)非常简洁,核心字段如下:

typedef struct intset {uint32_t encoding;  // 编码类型:决定每个元素的位数(2/4/8字节)uint32_t length;    // 当前元素个数int8_t contents[];  // 柔性数组:存储整数的连续内存块(实际长度=length*(encoding/8))
} intset;

关键字段说明:

  1. encoding(编码)

    取值由存储的整数范围决定,共3种类型:

    编码值(宏定义) 整数范围 单元素字节数
    INTSET_ENC_INT16 -32768 ~ 32767 2字节
    INTSET_ENC_INT32 -2^31 ~ 2^31-1 4字节
    INTSET_ENC_INT64 -2^63 ~ 2^63-1 8字节
  2. contents(柔性数组)

    intset核心存储区,以升序存储整数(便于二分查找)。柔性数组的特性是:结构体本身不包含数组空间,需动态分配内存时与结构体绑定(如malloc(sizeof(intset) + length*element_size))。

三、intset的编码升级机制(核心特性)

intset动态编码升级是其核心设计,确保能存储更大范围的整数,同时避免提前占用过多内存。

1. 升级触发条件

插入的新元素超出当前编码的范围时,必须升级到更高阶的编码(不可逆,升级后无法降级)。

例如:当前intsetINTSET_ENC_INT16存储[1,2,3],插入40000(超过INT16最大值32767),触发升级到INTSET_ENC_INT32

2. 升级步骤(以INT16→INT32为例)

升级是内存重分配+元素迁移的过程,具体步骤:

  1. 计算新编码:根据新元素的大小选择最小的适配编码(如40000INT32)。
  2. 分配新内存:计算新intset的总大小(sizeof(intset) + length*new_element_size),并分配内存。
  3. 迁移旧元素:将旧contents中的元素按新编码重新解析(如INT16INT32),复制到新contents中。
  4. 插入新元素:将新元素添加到正确位置(保持升序)。
  5. 替换旧结构:释放旧intset内存,用新intset替代。

3. 升级的代价

升级会导致O(n)的时间复杂度(n为当前元素个数),因为需复制所有旧元素。但这是用“偶尔的高开销”换取“长期的内存高效”,对于小整数集合来说是合理的权衡。

四、intset的操作实现

intset的核心操作(查找、插入)均围绕升序数组设计,保证高效性。

1. 查找操作(O(log n))

利用数组的有序性,采用二分查找定位元素。源码简化如下:

static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {int low = 0, high = is->length - 1;while (low <= high) {uint32_t mid = low + (high - low)/2;int64_t midVal = getIntSetValue(is, mid); // 根据encoding获取中间值if (midVal < value) low = mid + 1;else if (midVal > value) high = mid - 1;else { *pos = mid; return 1; } // 找到元素}*pos = low; // 未找到,返回应插入的位置return 0;
}

其中getIntSetValue函数根据encoding解析contents中的值:

  • INT16*(int16_t*)(is->contents + mid*2)
  • INT32*(int32_t*)(is->contents + mid*4)
  • INT64*(int64_t*)(is->contents + mid*8)

2. 插入操作(O(n) 最坏情况)

插入流程:

  1. 查找位置:用二分查找确定元素应插入的位置pos
  2. 检查重复:若元素已存在,直接返回。
  3. 升级编码:若新元素超出当前编码范围,先升级。
  4. 移动元素:将pos之后的所有元素向后移动一位(腾出空间)。
  5. 插入元素:将新元素写入pos位置,更新length

示例:向intset [1,3,5]INT16)插入2

  • 查找得pos=1(应在1和3之间);
  • 无需升级(2在INT16范围内);
  • 移动5到位置2;
  • 插入2到位置1,结果为[1,2,3,5]

五、intset的内存模型(精准计算)

intset的内存占用由结构体本身contents数组两部分组成:

1. 结构体内存

intset结构体包含两个uint32_t字段(encodinglength),固定占用8字节4+4)。

2. contents数组内存

数组内存 = 元素个数 × 单元素字节数(由encoding决定)。

3. 总内存计算公式

总内存 = 结构体大小 + contents数组大小 + 内存分配器对齐开销

(注:Redis默认用jemalloc分配内存,会对齐到2/4/8/16MB等,对齐开销通常很小,可忽略)

4. 示例计算

  • 场景:存储100个INT16整数(如标签ID);
  • 结构体:8字节;
  • contents:100 × 2 = 200字节;
  • 总内存:8 + 200 = 208字节(实际jemalloc可能分配256字节)。

若升级到INT32

  • contents:100 × 4 = 400字节;
  • 总内存:8 + 400 = 408字节(实际约416字节)。

六、intset的优缺点与适用场景

1. 优点

  • 内存高效:连续数组无指针开销,比Hashtable节省30%-50%内存;
  • 查找高效:二分查找O(log n),对于小集合来说足够快;
  • 纯整数友好:针对整数做了极致优化,避免类型转换开销。

2. 缺点

  • 插入/删除代价高:升级时需复制所有元素,最坏O(n);
  • 类型限制:仅能存储整数,无法处理字符串或其他类型;
  • 降级不灵活:升级后无法自动降级(需手动删除元素至阈值以下才会转换回intset?不,Redis会自动降级:当Hashtable中的元素全为整数且数量低于intset-max-intset-entries时,会转换为intset)。

3. 适用场景

  • 小数量整数集合:如商品标签ID、用户权限位(用整数表示);
  • 内存敏感场景:如缓存系统、嵌入式设备中的Redis实例;
  • 读多写少场景:查找频繁但插入删除少的场景(如配置中心的全局开关集合)。

七、实战案例与常见陷阱

1. 案例:电商标签系统

某电商系统中,商品的“风格标签”用整数ID表示(如1=简约、2=复古),每个商品最多有200个标签。此时Redis用intset存储,每个商品的标签集合仅需约8 + 200×2 = 408字节,若用Hashtable则需约200×(8+8) = 3200字节(每个键值对需存储整数键和指针),内存节省87%!

2. 常见陷阱

  • 陷阱1:插入大整数导致频繁升级

    若初始插入的整数都很小(如INT16),后续突然插入大量大整数(如INT64),会导致多次升级,性能下降。解决方案:提前预估整数范围,初始化时指定编码(但Redis不支持手动指定,需通过配置intset-max-intset-entries间接控制)。

  • 陷阱2:误判intset的查找性能

    虽然intset的查找是O(log n),但当元素数量达到1000时,log2(1000)≈10次比较,而Hashtable是O(1)平均。解决方案:通过intset-max-intset-entries配置阈值(默认512),超过后自动转换为Hashtable,平衡内存与性能。

八、总结

intset是Redis针对纯整数小集合的极致内存优化方案,通过动态编码升级有序数组+二分查找实现了内存与性能的平衡。其设计思想可迁移到其他场景:针对特定数据类型做定制化存储优化(如用数组存储有序小整数,而非用链表)。

若你想进一步了解:

  • Redis如何判断何时用intset还是Hashtable?
  • intsetziplist(另一种紧凑结构)的区别?
  • 如何通过配置调优intset的性能?

欢迎随时提出,我们可以继续深入!

7、intsetziplist的区别

要理解Redis中intset(整数集合)ziplist(压缩列表)的区别,需要从设计目标、存储模型、编码逻辑、操作性能、适用场景五大核心维度展开,并结合Redis的实际配置与线上案例说明两者的边界。

一、基础定义与设计目标

先明确两个结构的核心定位——这是理解差异的起点:

结构 设计目标 核心特点
intset 针对纯整数集合内存极致优化,用连续内存存储小范围整数 专用、整数-only、无嵌套
ziplist 通用型紧凑存储结构,支持存储任意类型(整数/字符串)的小批量数据 通用、可嵌套、内存高度压缩

二、核心差异对比(结构化拆解)

我们从6个关键维度对比两者的不同,并结合源码/配置说明细节:

1. 存储内容与类型限制

  • intset

    仅能存储整数(有符号整型,支持int16/int32/int64),元素必须是纯数字,无法处理字符串、哈希或列表。

    例如:存储商品标签ID([1,2,3])可以用intset,但存储标签名称(["简约","复古"])则不行。

  • ziplist

    支持任意Redis数据类型的紧凑存储,包括:

    • 字符串(如"hello");

    • 整数(如123,自动转成变长整数编码);

    • 甚至可以嵌套其他结构(如哈希表的field-value、列表的元素,均以ziplist entry形式存储)。

      例如:用户的简要信息哈希({"name":"张三","age":18})可以用ziplist编码存储。

2. 内存布局与编码逻辑

两者的内存组织方式完全不同,直接影响内存利用率和操作效率:

(1)intset:连续整数数组

intset的核心是柔性数组contents,存储升序排列的整数,无任何额外元数据(仅encodinglength标记类型和数量)。

  • 内存布局:结构体头(8字节) + 连续整数块(length × 单元素字节数)
  • 编码规则:根据整数范围自动选择INT16/INT32/INT64(见上一篇分析),升级不可逆。

(2)ziplist:连续Entry序列

ziplist紧凑的Entry链表,每个Entry包含前缀长度、类型标记、值三部分,支持反向遍历(通过prevlen字段)。

  • 内存布局(简化版):

    zlbytes(4字节,总长度) → zltail(4字节,尾Entry偏移) → zllen(2字节,Entry数量) → [Entry1][Entry2]... → zlend(1字节,结束标记)
    
  • Entry结构(每个Entry的格式):

    prevlen(1/5字节,前一个Entry的长度,用于反向遍历) → encoding(1/2/5字节,类型+长度) → value(变长,存储实际数据)
    
  • 编码规则:

    • 字符串:用raw(原始字节)或LZF压缩(当字符串较长时);
    • 整数:用变长整数编码(如123用1字节,1<<30用4字节),自动选择最小空间。

3. 操作性能对比

两者的操作复杂度差异主要体现在查找、插入、删除上:

操作 intset ziplist
查找 O(log n)(二分查找,因元素升序) O(n)(需遍历Entry,无有序性)
插入 O(n)(升级时需复制所有元素) O(n)(需移动后续Entry,且可能触发prevlen扩容)
删除 O(n)(删除后需移动元素) O(n)(同理,需移动Entry)
反向遍历 支持(升序数组反向遍历即可) 高效(prevlen字段直接跳转)

4. 升级/扩容机制

两者均有内存优化机制,但逻辑完全不同:

  • intset

    仅当插入的整数超出当前编码范围时升级(如INT16→INT32),升级是内存重分配+元素迁移,不可逆。

  • ziplist

    无“升级”概念,但有连锁更新风险

    当某个Entry的prevlen字段从1字节扩展到5字节(比如前一个Entry长度超过253字节),会导致后续所有Entry的prevlen都需要扩容,引发O(n²)的时间复杂度。

    例如:一个ziplist中有1000个Entry,每个Entry长度254字节,修改第一个Entry会导致后面999个Entry都要更新prevlen,性能急剧下降。

5. Redis配置与自动转换

Redis通过阈值配置决定何时用intset/ziplist,以及何时转换为更高效的结构(如hashtable/list):

结构 触发使用的配置 触发转换的配置
intset Set集合元素全为整数,且数量≤intset-max-intset-entries(默认512) 数量超过intset-max-intset-entries,或插入非整数 → 转hashtable
ziplist Hash列表元素≤hash-max-ziplist-entries(默认512)且每个value≤hash-max-ziplist-value(默认64字节); List元素≤list-max-ziplist-size(默认8192字节); ZSet元素≤zset-max-ziplist-entries(默认128)且每个value≤zset-max-ziplist-value(默认64字节) 元素数量/大小超过配置 → 转hashtable/linkedlist/skiplist

6. 适用场景对比

结合两者的特点,典型适用场景如下:

(1)intset的典型场景

  • 小整数集合:如商品标签ID、用户权限位(用整数表示)、订单状态枚举值;
  • 内存敏感场景:如缓存系统、嵌入式Redis实例,需要最大化内存利用率;
  • 读多写少场景:查找频繁但插入删除少(如配置中心的全局开关集合)。

(2)ziplist的典型场景

  • 小哈希:如用户的简要信息({"name":"张三","age":18});
  • 小列表:如商品的标签列表(["简约","复古"],字符串类型);
  • 小有序集合:如热门商品的评分排名(zset,元素少且分数小);
  • 需要嵌套结构的场景:如Hash的field-value本身是小整数/字符串。

三、关键差异总结(一句话记牢)

  • intset:纯整数、无嵌套、内存连续、升级明确,适合小整数集合
  • ziplist:通用、可嵌套、内存压缩、有连锁更新风险,适合小批量任意类型数据

四、实战案例与常见陷阱

1. 案例:用户信息存储

某社交APP中,用户的“基本信息”(name/age/gender)用ziplist编码的Hash存储:

  • name:"张三"(字符串,raw编码,占4字节);

  • age:18(整数,变长编码,占1字节);

  • gender:"男"(字符串,raw编码,占3字节)。

    总内存仅约zlbytes+zltail+zllen+Entry1+Entry2+Entry3+zlend4+4+2+(1+1+4)+(1+1+1)+(1+1+3)+1= 约22字节,远小于普通Hash的存储开销。

2. 常见陷阱

  • 陷阱1:ziplist的连锁更新

    ziplist中有一个Entry长度超过253字节,修改前面的Entry会触发prevlen扩容,导致性能暴跌。解决方案:通过list-max-ziplist-size/hash-max-ziplist-value等配置限制单个Entry的大小,避免触发连锁更新。

  • 陷阱2:误用intset存字符串

    若尝试将字符串存入intset,Redis会直接拒绝(或转换为hashtable),因为intset的编码逻辑仅支持整数。解决方案:字符串集合用ziplist编码的List,或直接用普通List。

  • 陷阱3:intset的升级代价

    若初始插入INT16整数,后续突然插入大量INT64整数,会导致多次升级,性能下降。解决方案:提前预估整数范围,或通过intset-max-intset-entries配置阈值,让Redis尽早转换为hashtable

五、延伸思考:为什么Redis需要两个紧凑结构?

Redis的设计哲学是“用最合适的结构解决最合适的问题”

  • intset专用优化,针对纯整数集合做到内存极致;

  • ziplist通用优化,覆盖任意小批量数据的紧凑存储需求。

    两者互补,共同实现Redis在内存利用率性能之间的平衡。

总结

维度 intset ziplist
设计目标 纯整数集合的内存优化 通用小数据的紧凑存储
存储内容 仅整数 任意类型(整数/字符串/嵌套)
内存布局 连续整数数组 连续Entry序列(含prevlen)
查找性能 O(log n) O(n)
升级机制 整数范围升级(不可逆) 无升级,有连锁更新风险
适用场景 小整数集合 小哈希/小列表/小有序集合

若你想进一步了解:

  • ziplist的连锁更新具体如何触发?能否避免?
  • intsetziplist在Redis源码中的具体实现细节?
  • 如何通过配置优化这两个结构的生产环境性能?

欢迎随时提出,我们继续深入!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/962404.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Docker部署Tomcat9.0

1、查看华为云镜像中心Tomcat版本 华为云容器镜像中心官网地址:https://console.huaweicloud.com/swr/?region=cn-north-4#/swr/mirror 2、拉取tomcat镜像docker pull tomcat:9.0.263、查看镜像是否下载成功docker i…

2025年11月学习机品牌对比榜:十强机型资质认证与用户反馈一览

开学季刚过,后台留言里“到底该给孩子买哪台学习机”的提问量陡增。家长画像很集中:工作节奏快、辅导时间碎片化、对“新课标”变化心里没底,同时预算有限却希望一次到位。教育部2024年10月发布的《2025新课标实施要…

wireshark的用法

以上仅供参考,如有疑问,留言联系

2-3-3-1-Dubbo

1、理解“Dubbo是RPC调用层面的服务治理工具” 要理解“Dubbo是RPC调用层面的服务治理工具”,需要从分布式系统的核心痛点、RPC的本质以及Dubbo的定位边界三个维度拆解;而“Dubbo解决什么问题”“没有它会怎样”则是…

2025年11月学习机品牌榜单:从早教到高中全场景覆盖排行解析

期中考试刚过,家长群里又开始新一轮“选机焦虑”:孩子成绩波动大,校内进度快,课外时间被切割成碎片,传统辅导班往返耗时,线上网课又缺互动。教育部“双减”后,学科类培训被严格限时段,家长把希望转向能同步校内…

2025年评价高的无菌室净化门TOP实力厂家推荐榜

2025年评价高的无菌室净化门TOP实力厂家推荐榜 行业背景与市场趋势 随着生物医药、电子制造、食品加工等行业对生产环境洁净度要求的不断提高,无菌室净化门作为洁净室系统的关键组成部分,其市场需求持续增长。据《…

2025年11月国际机票预定平台推荐榜:IATA认证服务商全面排行

2025年11月,全球商旅复苏叠加留学生返校高峰,国际机票需求进入年内最后一轮集中释放期。中国民航局10月发布的《国际航空运输市场白皮书》显示,2025年冬春航季国际客运航班量已恢复至2019年同期的92%,但平均票价仍…

2025年包装设计行业十大品牌权威推荐榜单

摘要 随着消费升级和品牌竞争加剧,包装设计行业在2025年迎来新一轮发展机遇。根据市场调研数据显示,2025年全球包装设计市场规模预计达到5820亿美元,年复合增长率达5.3%。中国作为全球最大的包装消费市场,2025年包…

2025年靠谱的精密冲床品牌厂家排行榜

2025年靠谱的精密冲床品牌厂家排行榜行业背景与市场趋势精密冲床作为现代制造业的核心设备之一,在汽车零部件、电子电器、五金制品等领域发挥着不可替代的作用。根据中国机床工具工业协会最新发布的《2024-2025年中国…

2025年陕西省搜索优化服务商综合实力排行榜Top10

摘要 随着数字化转型加速,陕西省搜索优化服务行业迎来快速发展期。本文基于权威数据分析和行业调研,为您呈现2025年陕西省搜索优化服务商综合排名,为用地审批技术服务需求方提供参考依据。文末附专业咨询表单,供进…

视频融合平台EasyCVR:构建智慧化城市/乡镇污水处理厂综合安防与运营监管方案

视频融合平台EasyCVR:构建智慧化城市/乡镇污水处理厂综合安防与运营监管方案随着我国城镇化进程的加速和环保政策的日趋严格,城市及乡镇污水处理厂的稳定运行与高效监管变得至关重要。传统的污水处理厂在安全管理与生…

2025年靠谱的组合式恒温 振荡培养箱厂家推荐及采购参考

2025年靠谱的组合式恒温振荡培养箱厂家推荐及采购参考 行业背景与市场趋势 随着生物医药、食品检测、环境监测等领域的快速发展,实验室设备市场需求持续增长。根据《2024年中国实验室仪器行业分析报告》,全球实验室…

2-3-1-1-ZooKeeper

ZooKeeper 是一个开源的分布式协调服务,为分布式应用提供一致性保障。它通过基于 Paxos 算法的 ZAB(ZooKeeper Atomic Broadcast)协议实现数据一致性,并以类似文件系统的树形结构(ZNode 树) 存储数据。其核心特点…

2025年口碑好的钩针纸布厂家最新实力排行

2025年口碑好的钩针纸布厂家最新实力排行行业背景与市场趋势钩针纸布作为一种环保、轻便且具有独特质感的材料,近年来在工艺品、包装、装饰等领域应用日益广泛。根据中国纺织品商业协会最新发布的《2025年中国特种纺织…

2025年口碑好的ZDSA-4500清淤机器人优质厂家推荐榜单

2025年口碑好的ZDSA-4500清淤机器人优质厂家推荐榜单行业背景与市场趋势随着城市化进程加速和环保要求日益严格,清淤机器人市场迎来了爆发式增长。据《2024-2029年中国清淤机器人行业市场调研与投资战略研究报告》显示…

2025年质量好的冷冻离心浓缩干燥器最新TOP厂家排名

2025年质量好的冷冻离心浓缩干燥器最新TOP厂家排名行业背景与市场趋势冷冻离心浓缩干燥器作为现代生物医药、化学分析等实验室的核心设备,近年来随着科研投入的持续增加和检测标准的不断提高,市场需求呈现稳定增长态…

2025年11月显微镜品牌推荐:科研工业用户必看榜与对比评测

进入2025年末,国内科研、医疗、工业检测三大场景对显微镜的需求同步升温:高校年底采购窗口打开,医院病理科更新设备,半导体厂为来年扩产做仪器储备。预算从三万元的基础教学机到百万元级的科研定制平台不等,用户却…

2025年安徽猪肉批发厂家综合实力排行榜TOP5

摘要 2025年安徽猪肉批发行业迎来品质升级新阶段,随着食品安全要求不断提高,具备完善质量管控体系的厂家更受市场青睐。本文基于权威数据分析和市场调研,为您呈现安徽地区猪肉批发厂家综合实力排名,并提供详细的企…

2025年口碑好的自动叠皮机厂家最新热销排行

2025年口碑好的自动叠皮机厂家最新热销排行行业背景与市场趋势随着食品工业自动化水平的不断提升,自动叠皮机作为面食加工领域的关键设备,近年来市场需求呈现稳定增长态势。据中国食品机械设备行业协会2024年发布的《…