Redis7 底层数据结构解析--重点笔记

一 Redis底层数据结构代码

Redis是⼀个<k,v>型的数据库,其中key通常都是string类型的字符串对象,⽽value在底层就统⼀是redisObject对象。
struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits
frequency
* and most significant 16 bits access time).
*/
int refcount;
void *ptr;
};
这⾥⾯⼏个核⼼字段意义如下:
type:Redis的上层数据类型。⽐如string,hash,set等,可以使⽤指令type key查看。
encoding: Redis内部的数据类型。
lru:当内存超限时会采⽤LRU算法清除内存中的对象。关于LRU与LFU,在redis.conf
中有描述
# LRU means Least Recently Used
# LFU means Least Frequently Used
refcount:表示对象的引⽤次数。可以使⽤OBJECT REFCOUNT key 指令查看。
*ptr:这是⼀个指针,指向真正底层的数据结构。encoding只是⼀个类型描述。实际
数据是保存在ptr指向的具体结构⾥。

二 Redis常⻅数据类型的底层数据结构总结

image

 三 string 对应的int,embstr,raw

对于string类型的⼀系列操作,Redis内部会根据⽤户给的不同键值使⽤不同的编码⽅式,⾃适应地选择最优化的内部编码⽅式。这些逻辑,对于⽤户是完全隔离的。
 
对于string类型的数据,如果value可以转换为数字,Redis底层就会使⽤int类型。在RedisObject中的ptr指针中,会直接复制为整数数据,不再额外创建指针指向整数,节省
了指针的空间开销。并且,如果数字⽐较⼩,⼩于1000,将会直接使⽤预先创建的缓存对象,连创建对象的内存空间也节省了。
 
如果value是字符串且⻓度⼩于44字节,Redis底层就会使⽤embstr类型。embstr类型会调⽤内存分配函数,分配⼀块连续的内存空间保存对应的SDS。这样使⽤连续的内存空间,
不光可以提⾼数据的读取速度,⽽且可以避免内存碎⽚。
 
如果value是字符串类型,但是⼤于44字节,那么RedisObject和SDS就会分开申请内存。通过RedisObject的ptr指针指向新创建的SDS。
 
 

四 ziplist和listpack

ziplist

ziplist最⼤的特点,就是他被设计成⼀种内存紧凑型的数据结构,占⽤⼀块连续的内存空间,不仅可以利⽤CPU缓存,⽽且会针对不同⻓度的数据,进⾏响应的编码。这种⽅法可
以及有效的节省内存开销。
ziplist是由连续内存块组成的顺序性数据结构,整个结构有点类似于数组。可以在任意⼀端进⾏push/pop操作,时间复杂度都是O(1)。整体结构如下:

image

 

这些entry就可以认为是保存hash类型的value当中的⼀个键值对。
然后,每⼀个entry结构⼜分为三个部分。

image

previous_entry_length:记录前⼀个节点的⻓度,占1个或者5个字节。如果前⼀个节点的⻓度⼩于254字节,则采⽤⼀个字节来保存⻓度值。如果前⼀个节点的⻓度⼤于等于
254字节,则采⽤5个字节来保存这个⻓度值。第⼀个字节是0xfe,后⾯四个字节才是真实⻓度数据
为什么要这样?因为255已经⽤在了ziplist的最后⼀个zlend。
encoding:编码属性,记录content的数据类型。表明content是字符串还是整数,以及content的⻓度。
contents:负责保存节点的数据,可以是字符串或整数。
连锁更新问题
连锁更新问题的核⼼就是在enty的previous_entry_length记录⽅式。如果前⼀个节点的⻓度⼩于254字节,那么previous_entry_length只有1个字节。如果⼤于等于254字节,则
previous_entry_length需要扩展到5个字节。

这时假设我们有这样⼀个ziplist,每个entry的⻓度都是在250~253字节之间,previous_entry_length都只要⼀个字节。

image

这时,如果将⼀个⻓度⼤于等于254字节的新节点加⼊到压缩列表的表头节点,也就是e1的头部。

image

 

这时,因为e1的previous_entry_length只有1个字节,⽆法保存新节点的⻓度,此时就需要扩充previous_entry_length到5个字节。这样e1的整体⻓度就会超过254字节。⽽e1⼀旦⻓
度扩展,意味着e2的previous_entry_length也需要从1扩展到5字节。接下来,后续每⼀个entry都需要重新调整空间。
 
核⼼是entry中原本记录前⼀个entry的⻓度,现在改为记录⾃⼰的⻓度。这样,就不会再因为前⼀个entry变化⽽影响⾃⼰的⻓度。这样也就没有了连锁更新的问题

listpack

listpack整体和ziplist很相似,的整体结构如下:

image

 

五 quicklist

quicklist⼤体上可以认为是⼀个链表结构。⾥⾯的每个节点是⼀个quicklistNode。每个quicklistNode会保存前后节点的指针,这就是⼀个典型的链表结构。
在quicklistNode中,*entry实际上就是指向具体保存数据的listpack结构.

image

 

这样就形成了quicklist的整体结构。这个quicklist结构,就相当于是数组Array和链表List的结合体。这就能尽可能的结合这两种数据结构的优点。 
 

六 skiplist

skiplist跳表就是⼀种思路。skiplist的优化思路是构建多层逐级缩减的⼦索引,⽤更多的索引来提升搜索的性能。

image

 

skiplist是⼀种典型的⽤空间换时间的解决⽅案,优点是数据检索的性能⽐较⾼。时间复杂度是O(logN),空间复杂度是O(N)。但是他的缺点也很明显,就是更新链表时,维护索引
的成本相对更⾼。因此,skiplist适合那些数据量⽐较⼤,且是读多写少的应⽤场景。
Redis天⽣就是针对读多写少的应⽤场景,⽽数据量的⼤⼩通过之前看到的两个参数,从数据条⽬数和数据⼤⼩两个⽅⾯来进⾏区别。 

 

 

 

 

 

 

posted @ 2026-03-30 10:48  OMGq  阅读(18)  评论(0)    收藏  举报