redis 数据类型的底层实现
显示五大数据类型的底层数据结构的命令:OBJECT ENCODING key
简单动态字符串 并不是采用C语言中的字符串(以空字符'\0'结尾的字符数组),自己构建了一种名为简单动态字符串(simple dynamic string , SDS)的抽象类型,并将SDS作为redis的默认字符串表示。
SDS的定义:
struct sdshdr{
//记录buf数组中已使用字节的数量
//等于 SDS 保存字符串的长度
int len;
//记录 buf 数组中未使用字节的数量
int free;
//字节数组,用于保存字符串
char buf[];
}
使用SDS的好处:
常数复杂度获取字符串长度
杜绝缓冲区溢出,字符修改时,根据记录的len属性检查内存空间是否满足要求,不满足会进行空间扩展,内存扩展若小于1m,预分配与len相同的空间,若大于1M,则预分配1M空间。
减少修改字符串的内存重新分配次数,C语言不记录字符串长度,修改时必须重新分配(先释放再申请);而修改SDS时,实现空间预分配和惰性空间释放的两种策略。空间预分配就是扩展的内存比实际需要的多,这样减少连续执行字符串增长操作所需的内存重新分配次数;惰性空间分配会对字符串进行缩短操作时,程序不立即回收,而是free属性记录下来,等待使用。
二进制安全,C语言的字符串以空字符结尾,对于二进制文件会无法存取;SDS的API以处理二进制的方式来处理buf里面的元素,并且SDS不以空字符来判断结束,而是以len属性来判断。
兼容部分C语言的字符串函数。
2.链表,链表节点定义如下:
typedef struct listNode{
//前置节点
struct listNode *prev;
//后置节点
struct listNode *next;
//节点的值
void *value;
}listNode
通过多个listNode结构可以组成链表,双端链表
typedef struct list{
//表头节点
listNode *head;
//表尾节点
listNode *tail;
//链表所包含的节点数量
unsigned long len;
//节点值复制函数
void (*free) (void *ptr);
//节点值释放函数
void (*free) (void *ptr);
//节点值对比函数
int (*match) (void *ptr,void *key);
}list;
特性:
双端:链表具有前置节点和后置节点的引用,获取这两个节点时间复杂度都是常数级别。
无环:表头节点的前指针和表尾节点的后指针都是null结束
带链表的长度计数器,根据len获取
多态:链表节点使用void*指针来保存,可以保存不同类型的值
3.map字典类型
字典结构包含哈希表数组,哈希表数组中包含键值对的个数,索引地址是根据hash函数结果与size-1进行与运算得到。若发生冲突则通过链地址法来解决。
扩容是长度变为大于现有长度的2倍的最近2次幂,注意当节点数太多,扩容会影响程序使用,采用渐进式rehash。大致过程:在哈希表中记录一个值为rehashidx,若不扩容默认为-1.扩容开始时,变为0,即把索引为0的键值对转移到另一个哈希表中,rehashidx递增,直到rehashidx值遍历一遍后,即全部完成rehash操作,最后把扩容之后的哈希表重新命名成原数组名,扩容之后哈希表的置为空。在扩容期间执行的删除查找操作,会分两步,先从原数组找,找不到去扩容之后的数组找。注意,增加键值对的操作一定是在扩容之后的哈希表中进行。
4.有序set,采用跳表结构实现。
性质:很多层结构组成,每一层是有序的链表,排列顺序从高层到低层,至少包含两个链表节点,head和nil节点。最底层的链表包含所有的元素。链表中的每个节点都包含两个指针,一个指向同一层的下一个链表节点,另一个指向下一层的同一个链表节点。
有序的链表,设置几个书签的感觉,快速定位位置。插入的时候怎么构建呢,通过掷硬币的方式,随机生成结果,来判断是否生成书签。删除就要每个层中全部删除。
效率,查找某个节点的时间复杂度logN。
跳表结构中不仅包含多个跳表节点,还包含指向头节点和尾节点的指针,还有保存最高层的level值(保证O1时间复杂度找到最高的那层)。
跳表节点包含分值,前进指针,跨度(计算排位),后退指针,成员对象。
每个跳表节点的高度为1~32之间的随机数。可以包含相同的分值,但成员对象不同,排列时按字典序的大小进行排列。
redis采用字典和跳表作为zset底层实现,利用两者是因为只用其中一个效率较低,若只用字典不能保证有序;若只用跳表,那么取数据需要logN复杂度。所以将二者结合
5.整数集合(intset),作为集合键的底层实现之一,保证不存在重复元素。保持一个数组,按从小到大排序。数据结构中包含数组,元素的数量,元素的编码方式。升级过程是指数据类型要升级,并且保持有序,那就按最大类型的长度重新分配,并保证有序。每次升级都要内存重新分配,引起分配的新元素只会比现有元素都大,或都小于现有元素。注意,一旦升级,不会发生降级。升级操作的优势在于灵活操作,节约内存(不升级,则保持原有内存)。
6.压缩列表(ziplist)
就是由一系列特殊编码的连续内存块组成的顺序性数据结构,一个压缩列表包含多个节点,其实是将数据按照一定规则编码在一块连续的内存空间,目的是节省内存。每个节点保存一个字节数组或者整数值,还会保存前一个节点的长度(两种,1Mb或5Mb)。根据这个长度可以用当前节点的指针减去长度,得到前一个节点的指针地址,以此实现从后往前遍历。注意,可能会出现连锁更新的情况,即加入或者删除一个节点,导致保存前一个节点的地址空间不够,就会再重新分配,不过产生可能性很低。
为不同的情景设置不同的类型
7.内存回收和对象共享
引用计数计数来实现内存回收,当引用计数变为0时,回收内存。对象的引用计数用来实现对象共享。默认在启动会共享0到9999的整数值。没有保存字符串类型是因为检查是同一共享对象的时间复杂度较高。
对象的空转时长通过当前时间减去最后一次操作的时间,为内存回收的LRU算法提供依据