redis底层数据结构

1 字典

:dict是一个用于维护key和value映射关系的数据结构。类似于java中的map,通过dictentry保存节点,其中dict同时保存两个entry数组;

typedef struct dict{
    dictType *type; //dictType结构中包含自定义的函数,这些函数使得key和value能够存储任何类型的数据
    void *privdata; //私有数据,保存着dictType结构中函数的 参数
    dictht ht[2]; //只有在rehash的过程中,ht[0]和ht[1]才都有效。而在平常情况下,只有ht[0]有效,ht[1]里面没有任何数据。上图表示的就是重哈希进行到中间某一步时的情况。
    long rehashidx; //rehash的标记,rehashidx == -1,表示没有进行 rehash
    int itreators;  //正在迭代的迭代器数量
  }dict;
typedef struct dictht
{
         //哈希表数组
         dictEntry **table;
         //哈希表大小
         unsigned long size;
         //哈希表大小掩码,用于计算索引值
         unsigned long sizemask;
         //该哈希已有节点的数量
         unsigned long used;
}dictht;
//哈希表节点定义dictEntry结构表示,每个dictEntry结构都保存着一个键值对。
typedef struct dictEntry
{
         //键
         void *key;
         //值
         union{
           void *val;
            uint64_tu64;
            int64_ts64;
            }v;
         // 指向下个哈希表节点,形成链表
         struct dictEntry *next;
}dictEntry;

扩容的条件 在没进行AOF重写的时候 负载因子》1 或者在进行AOF重写的时候负载因子》5

渐进式rehash 过程:

  1. 新创建一个哈希表ht[1],为新表分配空间,此时字典同时持有ht[0],ht[1]两个哈希表
  2. 在字典中维持一个计数器变量rehashidx,初始值为0,表示开始rehash工作
  3. 在rehash期间,对字典的所有增删改查操作都会同时操作两个哈希表,目的是将ht[0]中的健值对重新hash到ht[1]对应位置上,操作完成后rehashidx加1
  4. 迁移完毕后,rehashidx设置成-1,表示rehash完成。释放ht[0],将ht[1]设置成ht[0],并创建一个空哈希表,为下次rehash做准备。

为什么使用渐进式
redis的key 有很多,并且核心计算是单线程的,一次性散列那么多key会造成服务不可用。

2 sds(简单动态字符串)

redis是C语言实现,C语言中没有String 类型,而是用char数组进行拼接,所以redis设计了一种简单动态字符串(SDS[Simple Dynamic String])作为底实现

struct sdshdr {
    //用于记录buf数组中使用的字节的数目,和SDS存储的字符串的长度相等 
    int len;
    //用于记录buf数组中没有使用的字节的数目 
    int free;
    //字节数组,用于储存字符串
    char buf[]; //buf的大小等于len+free+1,其中多余的1个字节是用来存储’\0’的
};

img

DSD好处:在获取字符串长度的时候 可以直接获取len字段 时间复杂度为O(1);

在操作字符串的时候不用扩展char数组的空间大小 而sds有预分配和惰性释放

预决策:(还可以解决缓冲区溢出)

如果对SDS进行修改之后,SDS的长度(也即是len属性的值)将小于1MB,那么程序分配和len属性同样大小的未使用空间,这时SDS len属性的值将和free属性的值相同。
如果对SDS进行修改之后,SDS的长度(也即是len属性的值)将大于等于1MB,那么程序会分配1MB的未使用空间。

惰性释放

用于优化 SDS 的字符串缩短操作: 当 SDS 的 API 需要缩短 SDS 保存的字符串时, 程序并不立即使用内存重分配来回收缩短后多出来的字节, 而是使用 free 属性将这些字节的数量记录起来, 并等待将来使用。

还是二进制安全的,可以存储任意数据。

3.intset(整数集合)

当存储整数集合并且数据量较小的情况下Redis会使用intset作为set的底层实现,当数据量较大或者集合元素为字符串时则会使用dict实现set。

int8_t 的意思是 bit为 8位, 取值范围位-128—127

uint8_t 的意思是 bit 位 8位, 取值范围位0-255

其他一样

typedef struct intset {
    uint32_t encoding; //intset的类型编码
    uint32_t length; //集合包含的元素数量
    int8_t contents[]; //保存元素的数组
}

img

inset数据集合具有以下特点:

intset将整数元素按顺序存储在数组里,并通过二分法降低查找元素的时间复杂度。

当新增的元素类型比原集合元素类型的长度要大时,可以进行元素升级,

元素升级的具体步骤:

  • 1、根据新元素类型,扩展整数集合底层数组的大小,并为新元素分配空间。
  • 2、将现有的元素都转成与新元素相同类型的元素
  • 3、将新元素添加到整数集合中(保证有序)

4.skiplist(跳表)

跳跃表 是对有序链表的扩展,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问队尾目的。

skiplist查找效率很高,和红黑树相比,查找key的时间复杂度都为O(log n)但是红黑树的插入删除可能会引发旋转或者变色等操作 而跳表只需要改变相邻节点的指针。

一般来说 层级越多,访问节点的速度越快,但是跳表的层级完全是随机的。如果完全按照二分的思想层级固定,那么插入的时候就会打乱2:1的关系,维持2:1的关系的话 时间复杂度会降低到O(n);

5.ziplist(压缩表)

ziplist是一块连续的内存空间,元素之间紧挨着存储,没有任何冗余空隙的双向链表 它的设计目标就是为了提高存储效率。

struct ziplist<T> {
  int32 zlbytes;   
  int32 zltail_offset; 
  int16 zllength; 
  T[] entries; 
  int8 zlend; 
}
//zlbytes:ziplist的长度
//zltail_offset:最后一个元素距离压缩列表起始位置的偏移量,快速定位到最后一个节点,从而可以在ziplist尾部快速的执行push,pop操作
//zllength:元素个数 注意 zllength 只有16位,小于2的16次方-1的时候该值表示长度,如果超过需要遍历表得到长度 
 //entries:元素内容,挨个挨个紧凑存储
//zlend:标志压缩列表的结束,值恒为 0xFF(255)

每个entries节点由三部分组成:prevlength、encoding、data

  • prevlengh: 记录上一个节点的长度,为了方便反向遍历ziplist
  • encoding: 当前节点的编码规则
  • data: 存储的数据

ziplist特点:

(1)内存空间连续:ziplist为了提高存储效率,从存储结构上看ziplist更像是一个表(list),但不是一个链表(linkedlist)。ziplist将每一项数据存放在前后连续的地址空间内,一个ziplist整体占用一大块内存。而普通的双向链表每一项都占用独立的一块内存,各项之间用指针连接,这样会带来大量内存碎片,而且指针也会占用额外内存。

(2)查询元素:查找指定的数据项就会性能变得很低,需要进行遍历整个zipList。

(3)插入和修改:每次插入或修改引发的重新分配内存(realloc)操作会有更大的概率造成内存拷贝,从而降低性能。跟list一样,一旦发生内存拷贝,内存拷贝的成本也相应增加,因为要拷贝更大的一块数据。

6.quicklist(快速列表)

quicklist双向链表是由多个节点(Node)组成,而quicklist的每个节点又是一个ziplist。

typedef struct quicklist {
    quicklistNode *head;        /* 头结点 */
    quicklistNode *tail;        /* 尾结点 */
    unsigned long count;        /* 在所有的ziplist中的entry总数 */
    unsigned long len;          /* quicklist节点总数 */
    int fill : QL_FILL_BITS;              /* 16位,每个节点的最大容量 */
    unsigned int compress : QL_COMP_BITS; /* 16位,quicklist的压缩深度,0表示所有节点都不压缩,否则就表示从两端开始有多少个节点不压缩 */
    unsigned int bookmark_count: QL_BM_BITS;  /*4位,bookmarks数组的大小,bookmarks是一个可选字段,用来quicklist重新分配内存空间时使用,不使用时不占用空间*/
    quicklistBookmark bookmarks[];
}

img

quicklist将 双向链表插入和修改元素不需要移动节点的优点 和 ziplist的存储效率很高优点(一整块连续内存)结合在一起,同时将各自的缺点进行一个折中的处理。

1 双向链表便于在表的进行插入和删除节点操作,但是它的内存开销比较大。首先,它在每个节点上除了要保存数据之外,还要额外保存两个指针;其次,双向链表的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片。(内存开销大,额外的指针,单独的内存块,地址不连续,容易产生内存碎片)

2 ziplist由于是一整块连续内存,所以存储效率很高。但是,它不利于修改操作,每次数据变动都会引发一次内存的内存重新分配(realloc)。特别是当ziplist长度很长的时候,一次realloc可能会导致大批量的数据拷贝,进一步降低性能。

Redis还提供了一个配置参数list-max-ziplist-size,就是为了让使用者可以调整ziplist的长度。

list-max-ziplist-size -2
即可以取正值也可以取负值
//当取正值的时候,表示按照数据项个数来限定每个quicklist节点上的ziplist长度
//当取负值的时候,表示按照占用字节数来限定每个quicklist节点上的ziplist长度 此时表示每个quicklist节点上的ziplist大小不能超过8Kb。

当列表很长的时候,最容易被访问的很可能是两端的数据,中间的数据被访问的频率比较低 redis提供list-compress-depth来将中间的数据节点进行压缩,从而进一步节省空间

list-compress-depth 0
//0: 是个特殊值,表示都不压缩。这是Redis的默认值。
//1表示两端各有1个节点不压缩。
//2 、3 ....

参考 https://blog.csdn.net/Seky_fei/article/details/106968173

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值