Redis数据结构底层

119 阅读5分钟

1、 String

Redis为了极致利用内存,为String类型的对象,设置了三种数据结构: int、embstr和raw。

  • int : 在64位系统中,一个指针用8位字节来存储,正好是一个长整型的存储,所以为了节省空间,若一个字符串的内容可以转为long,那就将该字符串转为long类型存储,并将encoding设置为int, 可以节省空间,算是对长整型的一个优化
  • raw : 如果保存的字符串大小大于32字节 ,则会使用一个简单动态字符串SDS来保存,并将字符串编码设置为raw
  • embstr : 如果保存的字符串大小小于等于44字节 ,那么使用embstr来保存。

1.1 SDS结构

  • 属性: len字符串长度、alloc预分配空间大小、flags sds类型、buf[] 存储字符串数组

1.2 SDS与C字符串的对比

  • 长度计算: C中,字符串长度需要遍历计算,sds的话,自身便维护了len属性,可以直接读取
  • 缓冲区溢出: c中,若未提前做好内存分配,会出现内存溢出的情况,SDS的话,会根据缓冲区大小和len大小,计算预留内存是否足够分配,并重新申请内存
  • 动态扩展: SDS内存空间会在长度变化时,自动扩展计算。小于1M时翻倍,大于1M时,每次扩展1M
  • 惰性释放: 字符串删除某些内容后所占用的内存空间并不会立即释放,后续字符串变更扩展就无需再申请内存。

1.3 embstr如何存放字符串

cpu读取数据时,通常会先读取到cache line中,一个缓存杭基本占64个字节,其中RedisObject最起码得占16个字节,所以读取一个RedisObject,会发现有48个字节浪费掉,所以为了提高内存利用率,在RedisObject后面又开辟了一个48字节的连续空间,将ptr的值放入其中。48个字节对应的时SDShdr8存储结构,而它在不存放任何数据的情况下,至少需要4个字节空间('\0'),所以还剩下44个字节来存放字符串,这也是为什么embstr大小要设置在44的原因。

2、 List

2.1 ziplist(为节省内存而设计的结构)

image.png 其中,三个头部数据: zlbyte表示整体占用的字节数,用于内存重分配或计算列表尾端; zltail为到达列表最后一个节点的偏移量,方便直接找到尾部节点;zllen为列表的节点数量。最后的zlend用来标记列表尾端,占用一个字节的大小。

列表中每个节点的存储方式:

typedef struct zlentry {
    unsigned int prevrawlensize, prevrawlen;    // prevrawlen是前一个节点的长度,prevrawlensize是指prevrawlen的大小,有1字节和5字节两种
    unsigned int lensize, len;  // len为当前节点长度 lensize为编码len所需的字节大小
    unsigned int headersize;    // 当前节点的header大小\
    unsigned char encoding; // 节点的编码方式\
    unsigned char *p;   // 指向节点的指针\
} zlentry;

void zipEntry(unsigned char *p, zlentry *e) {   // 根据节点指针返回一个enrty\
    ZIP_DECODE_PREVLEN(p, e->prevrawlensize, e->prevrawlen);    // 获取prevlen的值和长度\
    ZIP_DECODE_LENGTH(p + e->prevrawlensize, e->encoding, e->lensize, e->len);  // 获取当前节点的编码方式、长度等\
    e->headersize = e->prevrawlensize + e->lensize; // 头大小\
    e->p = p;\
}
作者:小罗老师\
链接:https://juejin.cn/post/6863256540439117831\

当添加的字符串长度超过默认的64的时候,由压缩链表转化为双向链表。

  • zipList的缺点 : 连锁更新问题 在ziplist中,每个entry中都存储着前一个节点所占据的字节数,而这个数值又是一个变长编码。当前一个节点的大小发生变化的时候,后面都得跟着变化,每一次扩充都需要进行空间再分配操作。

一个压缩列表节点在保存上一个节点长度使用previous_entry_length属性,这个属性可以使用1字节或者是5字节。假设现有一个压缩列表里面保存的节点长度全部都是250-253,这时候previous_entry_length使用一字节记录就行。但是这时候添加一个新节点到头节点的位置,恰好这个节点的大小大于254字节,这时候所有后面字节都需要更新,因为他们的previous_entry_length都会变成5字节。(为什么是254??)

2.2 quickList

ziplist会引入频繁的内存申请和释放,而linkedlist由于指针也会造成内存的浪费,而且每个节点是单独存在的,会造成很多内存碎片,所以结合两个结构的特点,设计了quickList

quickList 是一个 ziplist 组成的双向链表。每个节点使用 ziplist 来保存数据。本质上来说,quicklist 里面保存着一个一个小的 ziplist。结构如下:

image.png

其中ziplist太小,内存碎片变多,若ziplist过大,则分配大块连续内存空间的难度就偏大,所以quicklist将ziplist的大小设置为可变:4、16、32、64Kb……

3、 Set

整数集合有三个特点:全是数字、内容连续、有序但不重复
set集合底层实现为hash表或intset,主要介绍intset数据结构

typedef struct intset{ 
    uint32_t encoding;   整数集合有三种编码方式: 163264
    uint32_t length;     整数集合中保存的元素个数
    int8_t contents[];   从小到大保存的整数集合中的元素
}intset;

4、 ZSet

zset 底层使用了两个数据结构:hash 和 跳跃链表。

typedef struct zskiplist{ 
    //表头结点和尾节点 
    structz skiplistNode *heade,*tail; 
    //表中节点数量 
    unsigned long length; 
    //表中层数最大的节点的层数 
    int level; 
}zskiplist;

 typedof struct zskiplistNode{ 
 //层 
     struct zskiplistNode{ 
         struct zskiplistNode *forward;  前进指针 
         unsihned int span;   跨度
     }level[];  
     struct zskiplistNode *backward;   后退指针
     double score;   分值
     robj *obj;     成员对象
 }zsikplistNode;

5、 Hash

即 :Hash对象

有一个注意的点:rehash

  • 字典的rehash当数据量过大时,扩容并不是一步完成的
  • rehash过程中,新插入的数据放在hash表1中,并将原数据重新hash,计算到1中,读操作同时操作新旧两个hash表
  • rehash过程中,使用dict中的rehashidx属性值
  • rehash采用cow写时复制技术