Redis五种基本数据类型
Redis 五种基本数据类型(String、List、Hash、Set、ZSet)的强大性能与其精巧的底层数据结构设计密不可分。Redis 并没有直接使用这些简单动态字符串(SDS)、链表、字典等数据结构,而是根据不同的使用场景,用一种上层建筑,下层基础的方式,为同一个数据类型提供了多种底层实现,并在合适的时机进行自动转换,以达到性能与空间的最优平衡。
String(字符串)
String 是 Redis 中最基础的数据类型,但其底层实现并非只有一种简单的方式。
底层数据结构:
RAW (SDS - Simple Dynamic String): 这是最标准的字符串表示方式。SDS 是 Redis 对 C 语言原生字符串的封装和增强。
- 结构定义:
1
2
3
4
5struct sdshdr {
int len; // 记录buf数组中已使用字节的数量,等于SDS所保存字符串的长度
int alloc; // 记录buf数组中未使用字节的数量
char buf[]; // 字节数组,用于保存实际的字符串内容
}; - 优点:
- O(1) 时间复杂度获取字符串长度: 直接访问
len属性,而 C 字符串需要遍历。 - 杜绝缓冲区溢出: SDS 的 API(如
sdscat)会先检查alloc是否足够,不足则自动扩展。 - 减少修改字符串时带来的内存重分配次数: 采用空间预分配(扩容时会多分配一些空间)和惰性空间释放(缩短时不立即释放多余空间)策略。
- 二进制安全: 因为依赖
len而不是‘\0’来判断字符串结束,可以存储任何二进制数据,包括图片、协议等。
- O(1) 时间复杂度获取字符串长度: 直接访问
- 结构定义:
EMBSTR (Embedded String): 一种嵌入式字符串,用于存储非常短(在 Redis 3.x 后,长度 ≤ 44 字节)的字符串。
- 实现原理: 它将 Redis 的
redisObject对象头 和sdshdr结构在内存上连续分配,一次性分配一块连续内存来容纳两者。 - 优点:
- 内存效率高: 只需要一次内存分配操作。
- 缓存友好: 数据连续,能更好地利用 CPU 缓存。
- 实现原理: 它将 Redis 的
INT: 当字符串值可以表示为一个长整型数字时,Redis 会直接将数据保存在
redisObject的ptr指针里(将void*强制转换为long)。- 优点:
- 节省内存: 无需额外的 SDS 结构。
- 性能极致: 无需对字符串进行解析。
- 优点:
编码转换:
int->raw: 当对一个int编码的字符串进行 append 等修改操作,使其不再是整数时,会转换为raw编码。embstr->raw: 由于embstr是只读的,任何对embstr编码的字符串的修改命令,都会先将其转换为raw编码,然后再执行修改。
List(列表)
Redis 的 List 是一个双向链表,但其底层实现经历了多次演化,现在默认使用的是 quicklist。
底层数据结构:
QUICKLIST(快速列表): 一个由 ziplist(压缩列表) 作为节点的双向链表。它是 zipList 和 linkedList 的一种混合体,取二者之长。
- ziplist (压缩列表):
- 是一块连续的内存,通过不同的编码来存储数据(整数或短字符串)。
- 优点:内存紧凑,利用率高(是一块连续内存,没有指针开销)。
- 缺点:每次插入删除都可能引发连锁更新(因为需要重新分配内存以保证连续性),性能较差。
- linkedList (双向链表):
- 由多个节点通过指针连接而成。
- 优点:插入删除效率高,只需要修改指针。
- 缺点:内存开销大(每个节点除了数据,还有前后两个指针),内存碎片化。
- quicklist 的折中方案:
quicklistNode结构包含一个指向ziplist的指针。quicklist结构包含了链表的头尾节点、长度等信息。- 通过配置
list-max-ziplist-size,可以控制每个 ziplist 节点的最大容量。如果超过这个值,就会新建一个 quicklistNode 节点。 - 通过配置
list-compress-depth,可以对链表两端的 ziplist 节点进行压缩(使用 LZF 算法),以进一步节省内存。
总结:List 的底层就是一个宏观上是链表(便于修改),微观上是压缩列表(节省内存)的 quicklist 结构。
Hash(哈希)
Hash 类型的底层实现有两种:ziplist 和 hashtable(字典)。Redis 会根据条件自动选择。
底层数据结构:
ZIPLIST(压缩列表):
- 使用条件(可通过
hash-max-ziplist-entries和hash-max-ziplist-value配置):- 哈希对象保存的所有键值对的键和值的字符串长度都小于
hash-max-ziplist-value(默认 64 字节)。 - 哈希对象保存的键值对数量小于
hash-max-ziplist-entries(默认 512 个)。
- 哈希对象保存的所有键值对的键和值的字符串长度都小于
- 存储方式: 每当有新的键值对加入到哈希时,程序会先将键压入 ziplist 的尾部,再将值压入 ziplist 的尾部。因此,同一个键值对的两个节点总是紧挨在一起,键节点在前,值节点在后。
- 使用条件(可通过
HASHTABLE(字典):
- 当上述条件不满足时,编码会自动从
ziplist转换为hashtable。 - 结构: 与 Java 的 HashMap 类似,是“数组 + 链表”的拉链法实现,但做了很多优化。
- 字典
dict结构包含两个哈希表dictht ht[2],这是为了渐进式 rehash 而设计的。 - 哈希表
dictht包含一个指针数组table,每个元素是一个指向dictEntry结构的指针。 - 键值对
dictEntry结构包含了键、值和指向下一个dictEntry的指针(形成链表)。
- 字典
- Rehash: 当哈希表保存的键值对数量过多或过少时,需要通过 rehash 来扩展或收缩数组大小。Redis 采用渐进式 rehash,在 rehash 过程中,同时使用
ht[0]和ht[1],分多次、渐进地将ht[0]中的数据迁移到ht[1]中,避免了单次 rehash 的巨大延迟。
- 当上述条件不满足时,编码会自动从
Set(集合)
Set 类型的底层实现也有两种:intset(整数集合)和 hashtable(字典)。
底层数据结构:
INTSET(整数集合):
- 使用条件:
- 集合对象保存的所有元素都是整数值。
- 集合对象保存的元素数量不超过
set-max-intset-entries(默认 512 个)。
- 结构:
intset是一个有序的、不重复的整数数组。它支持三种编码:int16_t,int32_t,int64_t。会根据存入的最大整数自动升级编码(例如,原先是int16_t的数组,存入一个int32_t的数字,整个数组都会升级为int32_t)。
- 使用条件:
HASHTABLE(字典):
- 当不满足 intset 的条件时,会转换为
hashtable。 - 在 Set 中使用字典时,字典的每个键就是集合的元素,而字典的值全部设置为
NULL。
- 当不满足 intset 的条件时,会转换为
ZSet(有序集合)
ZSet 是 Redis 中最有特色的数据结构,它既要保证元素的唯一性,又要为每个元素关联一个 score 用于排序。其底层实现同样有两种:ziplist 和 skiplist + dict(跳跃表+字典)。
底层数据结构:
ZIPLIST(压缩列表):
- 使用条件(可通过
zset-max-ziplist-entries和zset-max-ziplist-value配置):- 有序集合保存的元素数量小于
zset-max-ziplist-entries(默认 128 个)。 - 有序集合保存的所有元素成员的长度都小于
zset-max-ziplist-value(默认 64 字节)。
- 有序集合保存的元素数量小于
- 存储方式: 与 Hash 类似,每个元素使用两个紧挨在一起的 ziplist 节点来保存,第一个节点保存元素的成员(member),第二个节点保存元素的分值(score)。ziplist 内的集合元素按分值从小到大排序。
- 使用条件(可通过
SKIPLIST + DICT(跳跃表 + 字典):
- 这是最复杂的结构,由一个跳跃表
zsl和一个字典dict共同实现。 - 为什么需要两种结构?
- 跳跃表 (zskiplist): 核心结构,支持范围性操作(如
ZRANGE,ZRANK),时间复杂度为 O(logN)。跳跃表是一种多层的有序链表,通过建立“索引”来加快查找速度。 - 字典 (dict): 用于支持 O(1) 时间复杂度的按成员键取值操作(如
ZSCORE)。字典的键是成员(member),值是分值(score)。
- 跳跃表 (zskiplist): 核心结构,支持范围性操作(如
- 数据共享: 跳跃表的节点
zskiplistNode和字典的dictEntry会共享成员和分值对象,不会造成数据的重复存储,节省内存。
- 这是最复杂的结构,由一个跳跃表
总结:ZSet 通过同时使用跳跃表和字典,既高效地支持了按分值排序和范围查询,又高效地支持了按成员键访问,以空间换时间,实现了功能的极致性能。
核心思想总结
Redis 数据类型的底层实现哲学可以概括为:
- 灵活性: 同一数据类型有多种底层实现,根据数据内容和配置动态选择最合适的编码。
- 效率至上: 在时间(操作速度)和空间(内存占用)之间做精妙的权衡。
- 平滑升级: 当数据量变化导致当前编码不再高效时,会自动转换为更合适的编码,这个过程对用户透明。
- 底层优化: 对基础数据结构(如 SDS、字典的渐进式 rehash、跳跃表)进行了大量极致优化,这些是 Redis 高性能的基石。


