Redis设计核心-1-Redis底层数据结构总结1

前言

最近看了相关的Redis设计核心相关的书籍,对Redis有了一些小的认识,然后自己也做一些产出加深映象,我会从几个方面去总结Redis设计的核心内容:Redis底层数据结构总结、Redis高性能由哪些基础支撑、Redis应用场景、那些有趣的功能。

概述 

本篇主要内容是Redis底层数据结构总结。Redis供用户直接使用的数据结构有String、List、Set、Zset、Hash等结构,而这些结构下层又基于一些数据结构,这些数据结构被设计的非常优美,来提供了Redis高效的性能、低内存占用、多样的功能等,他们有Dict、RedisObject、SDS、ZipList、QuickList、Listpak、Skiplist,我将从是什么、为什么使用、设计思想、特点、实现结构几个方面去总结。


目录

前言

概述 

Dict

是什么

为什么使用

设计思想

特点

实现结构

RedisObject

是什么

为什么使用

设计思想

特点

实现结构

SDS

是什么

为什么使用SDS

设计思想

特点

实现结构

使用emb的好处

Ziplist

是什么

为什么使用

设计思想

特点

实现结构

为什么previous_entry_length要小于254?


 

Dict

是什么

Dict是一种查找结构,用来解决查询问题,是一种哈希散列表结构,采用拉链法解决哈希冲突,类似于JDK中HashTable但其特点是渐进式扩容,将单次指令触发扩容的时间分散到多次指令渐进扩容,避免了单次响应时间的剧烈波动,符合其响应效率的特点。

为什么使用

一般查找结构有常用的两种结构:1.各种平衡树,内部对数据排序,时间复杂度趋近于O(logn),但是需要维护其平衡结构,较耗费性能并且实现复杂,可执行标识查找和范围查询;2.哈希散列表结构,不对元素排序,通过hash()定位元素在数组上的位置,利用了数组的索引O(1)查找特点,快速定位元素,其实现相对简单但只提供标识查询,时间复杂度为O(1)。

设计思想

Dict是字典结构,采用了散列表结构,利用数组索引定位O(1)的特点来实现快速操作,但区别于JDK的散列表实现,Dict使用数组存储元素,拉链法解决hash冲突,并且最突出的是使用渐进式扩容。HashMap扩容是一次性同步扩容,既单次add指令导致元素数量达到临界点,触发同步全量扩容,导致单次的响应时间剧烈波动。而Dict是将一次指令的同步扩容分散到多次指令上,每次执行指令时都会触发一部分扩容,避免了单次响应时间的剧烈波动,这也是体现其设计核心--响应效率优先。

特点

  1. 响应效率->采用散列表结构,其操作时间复杂度为O(1)。
  2. 响应效率->渐进式扩容。非一次性同步扩容,Dict采用了渐进式扩容,将扩容的时间分散到每次操作上,避免单次请求的响应时间剧烈波动。
  3. 存储效率-->Dict的key使用SDSHDR5结构存储,降低内存占用率和内存碎片率。
  4. 补充:Dict的渐进式扩容内部有两种策略,上面讲的是惰性扩容,只有执行指令时才会触发扩容,但如果某个key不执行时,就会导致扩容一致未完成,所以Redis还提供了定时扩容,即每隔一段时间自动执行未完成的扩容操作,使用Dict存储整个database时就采用了这两种策略。

实现结构

这张图来源于http://zhangtielei.com/posts/blog-redis-dict.html

这是Redis使用Dict存储Database的结构图。

数据结构:dictEntry --> dictht --> dict
其中dict内部维护了ht[2],ht[0]用来存储数据,ht[1]用于扩容,rehashidx记录当前扩容位置,为-1则未执行扩容 
dictht维护了存储数组的信息,以及数组长度、元素个数
dictEntry是真正存储元素的结构,记录了key、value、next下一个元素。

更多关于该数据结构内容请从我的Redis专栏中获取。


RedisObject

是什么

Redis的最外层的数据结构是一个Dict结构,Key使用SDS存储,而Value因为其功能的不同所以底层数据结构也不同,RedisObject被用来设计包装Value结构,为多种数据结构提供统一的表现方式。

为什么使用

因为Redis为了节省内存,Redis对value的结构根据应用场景提供了多种实现。每种数据结构大部分都有2种数据结构来实现,这样就需要一个类型来包住百变的value,Redis设计了RedisObject来封装value,并且还可以提供全局value所需的基础字段。

设计思想

RedisObject(roj)是封装了value对象,可以通过roj获取到value的基础信息,如数据结构、编码类型、lru、value引用等,让底层数据结构无需关联全局字段,顶层数据结构信息和底层数据结构解耦,底层数据结构只需要关注于如何高效的存储数据,而顶层数据结构关注于全局功能,如数据的基础信息、lru驱逐时所需的lru最后访问时间、refcount等。

特点

  • 1.为多种数据类型提供一种统一的表示方式。
  • 2.解耦。将Redis顶层数据结构与底层存储数据结构解耦,底层数据结构只需要关系如何更高效的存储数据,而底层结构用来实现全局功能,如LRU驱逐策略、recount引用失效等,便于后续扩展功能。
  • 3.提供全局基础字段,来维护其基础功能。        
  • 4.支持对象共享和引用计数。当对象被共享的时候,只占用一份内存拷贝,进一步节省内存。        
  • 5.允许同一类型的数据采用不同的内部表示,从而在某些情况下尽量节省内存。
  • 6.占用内存少。roj只占用16byte,一个最小的字符串只需要19个byte。

实现结构

typedef struct redisObject {
    unsigned type:4; 代表value的数据类型,如OBJ_STRING, OBJ_LIST, OBJ_SET, OBJ_ZSET, OBJ_HASH,分别代表5种数据类型。
    unsigned encoding:4; 对象的内部的编码方式,对于同一个type可能会存在不同值,可能的取值有10种,如:emb、raw、int、ht、ziplist、skiplist、linkedlist等
    unsigned lru:LRU_BITS;用于LRU或LFU驱逐策略
    int refcount; 引用计数,当refcount为0时会释放roj对象。它允许robj对象在某些情况下被共享
    void *ptr;  数据指针。指向真正的数据
} robj;

roj所表示的就是Redis对外暴露的第一层面的数据结构:string, list, hash, set, sorted set,而每一种数据结构的底层实现所对应的是哪个(或哪些)第二层面的数据结构(dict, sds, ziplist, quicklist, skiplist, 等),则通过不同的encoding来区分。

更多关于该数据结构内容请从我的Redis专栏中获取。


SDS

是什么

Redis的字符串是可以修改的,底层实现是SDS数据结构-底层使用字节数组存储元素,可以存储任意类型的数组,Redis为了节省内存空间,将SDS分为5种数据结构和内存使用压缩编码方式来节省内存空间。并且提供冗余扩容来避免每次修改时都触发内存重分配.

为什么使用SDS

Redis是用C写的,为什么不直接采用C语言提供的字符串呢?C语言的字符串结构使用\0字符结尾的字符数组实现,它不能用来存储任意的二进制数据,并且获取元素长度时需要遍历获取。而SDS使用字节数组实现,并且兼容C语言字符串,常数复杂度获取字符串长度,可存储任意数据、高效利用内存。

设计思想

Redis的底层数据结构核心有三个,而SDS主要专注于两点:1.提高存储效率 2.提高响应效率。比如根据字符串长度提供多种SDS结构:节省内存、使用紧凑数组存储:节省内存,降低内存碎片率、提供多种编码提高效率。
冗余的设计思想是:使N次append操作最多导致N次扩容操作,避免频繁的空间分配和数据拷贝工作。

特点

  •     0.内部使用字节数组紧凑存储元素
  •     1.可动态扩展内存。sds表示的字符串内容可以修改和追加,
  •     2.二进制安全。使用字节数组存储数据,可存储任意类型的数据
  •     3.与传统C语言字符串类型兼容。
  •     4.SDS结构支持位操作
  •     5.内部根据字符串长度提供多种数据结构,目的是节省内存空间
  •     6.压缩编码,节省内存空间
  •     7.SDS有三种编码格式: emb 、raw、 int(用于存储整形)
  •     8.冗余设计,避免每次操作都执行扩容操作。
  •     9.结构不下沉。reids的内存回收策略如下:当SDS中存储的字符串长度变少时,结构不会改变
  •     10.最大支持存储512M的value。

实现结构

struct SDS<T> {
  T capacity; // 数组容量,提供冗余空间而避免频繁的数组复制工作,比如append操作,根据SDS的Header结构类型分为多个长度类型。
  T len; // 数组长度,表示数组实际长度,根据SDS的Header结构类型分为多个长度类型。
  byte flags; // 固定一个字节,其中的最低3个bit用来表示header的类型。header的类型共有5种。
  byte[] content; // 数组内容,未满部分用\0填空(\0是ASCLL码,真实代表NULL)
}

所以知道为什么上面说一个字符串最少需要19个字节,因为roj需要16字节,而capacity+len+flags最少需要3个字节,所以最少需要19字节存储一个字符串。capacity是用来存储数组容量,初始化时capacity与len相等,进行append操作时会对数组冗余扩容--double和1M的选择,而flags是标识SDS结构的,SDS内部为了节省内存有5种结构,SDSHDR5、SDSHDR8、SDSHDR16、SDSHDR32、SDSHDR64,每种结构的内部结构有部分区别(capacity和len的值范围不同)。

除了这些Redis为String还设计多种编码类型,int、emb、raw,其中int是存储整形数据,emb是存储等于小于44字符的字符串,raw是大于44字符的字符串,emb和raw内部效率不同。

使用emb的好处

  •     1.使用emb只需要一次malloc分配,而raw需要两次,相对的释放内存也是两次。
  •     2.emb的内容是连续的,更好的利用缓存优势顺序读取。
  •     3.Redis并未提供对emb的修改,emb是只读模式,对emb的修改实际上是先转为raw然后在修改的。
  •     4.emb只需要一次定位即可获取元素,因为roj和emb内存连续。而raw内存不连续需要两次定位,也降低了执行效率

更多关于该数据结构内容请从我的Redis专栏中获取。


Ziplist

是什么

ziplist是一个经过特殊编码的双向链表,底层使用字节数组实现,可以存储字符串和整形。其中hash和zset非标准结构底层都直接使用了ziplist,list间接使用了ziplist。最大存储2^32-1个字节。其核心思想是提升存储效率,内部数据使用了压缩编码+紧凑字节数组(代表内存连续),并且根据元素长度使用合适的变长编码。

为什么使用

传统的双向链表每个元素都需要维护多个引用指针,占用大量内存空间,比如存储的数据可能比维护元素所需的基础结构占用空间还小;传统双向链表每个元素都独立申请一块内存,造成了内存碎片,不利于内存管理。ziplist是一个紧凑的字节数组,内存是连续的,且无需维护内部指针,并且也提供双向遍历以及首尾操作O(1)时间复杂度。

设计思想

ziplist充分体现了Redis对于存储效率的追求。ziplist将数据存放在前后连续的字节数组内,并且元素之间紧凑存储,另外,ziplist为了节省内存,对值的存储采用了变长编码方式,既大值用大空间存储小值用小空间存储,但是这种结构并不擅长做修改操作。一旦数据发生改动,就会引发内存realloc,可能导致内存拷贝。最后还对部分数据结构做了压缩编码,使一个属性能存储多个数据。

特点

  • 1.使用字节数组紧凑存储,内存连续、内存碎片率低    
  • 2.存储效率高。ziplist使用压缩编码、根据数据类型选择合适的编码结构、当数据在0-12之间时直接存储在encoding上,内置了9种结构
  • 3.提供双向遍历、首尾操作时间复杂度为O(1)
  • 4.无需维护内部指针,根据指针偏移量来获取元素
  • 5.通过encoding可以获取数据类型和占用长度
  • 6.元素与元素有相关性,所以存在级联影响
  • 7.因为内部对数据经过了压缩编码,所以每次使用时都需要先解码再使用
  • 8.只适合存储少量元素或小字节元素,因为每一次操作都会触发realloc,可能导致内存空间重分配和数据拷贝工作
  • 9.对中间数组更改时,后向数据有可能都跟着变动,比如插入元素时后向元素都需要向后移动,比如级联更新。
  • 10.因为len占用2个字节,所以最大能表示2^16-2个元素,当大于这个临界点时需要通过遍历方式获取元素数量
  • 11.因为zlbytes占用4个字节,则元素最多存储2^32-1个字节。

实现结构

 

struct ziplist<T> {
    int32 zlbytes;   //表示整个压缩列表占用内存空间,占4个字节,因此压缩列表最长(2^32)-1字节;
可以对压缩列表进行内存重分配或计算zlend位置
    int32 zltail_offset; //压缩列表尾元素距离首地址的偏移量,用于快速定位尾部元素,占4个字节。zlbytes+zltail_offset就是尾元素的地址。从而实现了快速的lrpop lrpush
    int16 zllength; // 压缩列表的元素个数,占两个字节;那么当压缩列表的元素数目超过(2^16)-1怎么
处理呢?此时通过zllen字段无法获得压缩列表的元素数目,必须遍历整个压缩列表才能获取到元素数目;一般
不会出现,会提前转为标准结构。
    entryX //元素内容的引用,该类型可以为字节数组(字符串)或者整数,内部数组采用紧凑存储结构。
    int8 zlend; // 标志压缩列表的结束位,占一个字节,值恒为 0xFF(255),这是为什么pre_e_l最多
表示254,因为这里使用了255标示。
}


struct entry {
    int<var> previous_entry_length; // 前一个 entry 的字节占用空间,占1个或者5个字节,为了实
现反向遍历,这个字段使元素之间有了联系,可能导致极联更新。
    int<var> encoding; // 元素类型编码,即content字段存储的数据类型(整数或者字节数组)和元素
长度,为了节约内存,encoding字段同样是可变长度,表示了9种长度范围的字符串或整数。当元素极小时会
直接存在encoding中。
    optional byte[] content; // 存储元素内容,值的类型和长度由encoding决定
}

previous_entry_length :是一个变长整数如果前一个entry小于254个字节时,那么pre_e_l占一个字节,如果大于254个字节时占用5个字节表示,5个字节时其中属性的第一字节会被设置为 0xFE(十进制值 254), 而之后的四个字节则用于保存前一节点的长度,其实是用0XFE来标识pre_e_l占用5个字节,实际用4个字节存储元素,代表前一个字节空间最大为2^32-1个字节,默认是64字节转为标准结构。

encoding 同样是可变长度,决定了content的类型、最大存储长度、占用空间。Redis通过这个字段的前缀位来识别元素的类型(00,01,10代表数组,11代表整数。),后缀位来识别元素的存储长度.encoding总共有9种类型,其中3种字符串类型,最大存储2^32-1字节元素,6种整形类型,最多8字节的整数

 

为什么previous_entry_length要小于254?

因为1个字节最大表示数值为255,而255标示已被lend结束符占用,在ziplist的很多操作中会读取第一个字节,判断是否为255来代表ziplist已达到结尾,所以previous_entry_length最大能使用254,而previous_entry_length小于254时能代表该长度时前一个元素的占用空间,当等于时则代表previous_entry_length占用5个字节。所以ziplist在读取元素第一个字节可以得出previous_entry_length的占用空间。

更多关于该数据结构内容请从我的Redis专栏中获取。

未完,请关注Redis底层数据结构总结2..

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值