redis设计与实现——1

Redis的底层结构:

String,list,set,hash,sortedset。这五种仅仅是redis的对象类型,redis是一个对象系统,并没有直接使用底层的数据结构,而是以对象的形式来存储的。

 

Redis的底层数据结构,一共有7种,

 

  1. String:
  1. redis是c编写的,c中的字符串是以空格结尾的,redis并没有使用c中的字符串,而是自己实现了一种字符串sds(simple dynamic string),c中的字符串只会用在日志时,真正的存储操作还是sds。
  2. Sds的结构:

Sdshdr{

Int len;记录了buf中已使用的字节数量,是实际长度

Int free;buf数组中未使用的字节数量

Char buf[];保存字符串

}

  1. sds遵循C语言中的字符串,也是以空格结尾,但是这一个字节的空字符,并不计算在内,这个空间会被额外分配,这就表明,可以使用大部分的c中关于字符串操作的函数
  2. Sds的长度可以直接使用len属性获得,但是c中字符串必须通过遍历获得。
  3. 为什么不使用c中字符串,而要自己实现呢?因为c中的字符串容易造成缓存区溢出,
  4. 空间预分配(优化sds字符串增长),如果已使用的长度<1mb,则free=length

                                       已使用长度>1mb,则free=1mb

7.惰性空间释放:当sds空间需要缩短时,free就会记录下这些长度,实际空间并不会变小

 

 

  1. 链表

1.c中并没有内置的链表,所有redis自己实现了链表。

2.链表是由多个链表节点组成的,双向的

Listnode{

Listnode *preev 前直节点

Listnode *next  后直接点

Void *value 节点值

}

  1. 链表结构

List{

Listnode *head 表头结点

Listnode *tail   表未节点

Int len 链表中节点数量

Dup() 复制节点

Free() 释放节点

Match() 判断是否相等

}

  1. 链表特点:

  双向,无环,带表头表尾,长度 计数器,多态

 

  1. 字典map(散列表,数组+链表)
  1. 每个键和唯一的值关联,键是独一无二的
  2. Redis数据库的增删改查就是通过字典实现的,并没有采用b+数进行处理,采用的是hash方式
  3. 字典是hash键的底层实现之一(hash中的元素都是比较长的字符串是,使用字典作为底层)
  4. 字典使用hash表作为底层,所以看一下hash表中的每个节点

dictEntry{

Key:key

V:value

Next:下一个节点

}

  1. 哈希表结构

Dictht{

dictEntry table[];

Long size;哈希表大小

Long  Sizemask;用于计算索引值Sizemask=use-1;

Long  use;已使用的节点数量

}

  1. 字典

Dict{

Dictht ht[2]

}

  1. 一个字典里面有两个哈希表,一般只用第一个ht[0],但是当ht[0]数量过多时,需要rehash重新散列时,就会使用第二个哈希表,将ht[1]进行空间分配,将ht[0]中的数据复制到第二个哈希表中,将第二个哈希表设置为第一个。
  2. 索引值:将key经过hash算法得到一个hash值,hash值&Sizemask
  3. Redis哈希表添加元素时,总是将新节点放到hash表的表头。
  4. rehash ht[1]要分配的空间大小为 第一个>=use*2的(2的n次方)

收缩:第一个大于等于use的(2的n次方)

  1. 负载因子 use/size ,没有执行bgsave或者bgrewriteaof,负载因子大于等于1

                          执行bgsave或者bgrewriteaof,负载因子大于等于5

    负载因子<0.1,自动收缩

12.渐进rehash,字典的扩展并不是一次性完成的,而是渐进的。

 

 

4.跳跃表

1.跳跃表(skiplist)是一种有序的数据结构,它通过每个节点中维持多个指向其他节点的指针,从而实现快速访问。跳跃表平均O(logN),最坏O(N),支持顺序遍历查找。在redis中,有序集合(sortedset)的其中一种实现方式就是跳跃表。

2.当有序集合的元素较多,或者集合中的元素是比较常的字符串,则会使用跳跃表来实现。

3.跳跃表节点

zskiplistNode{

Robj obj;成员对象

Double score 分值

zskiplistNode backword;后退指针

 

zskiplistLevel{

   zskiplistNode  forword;前进指针

   Int span;跨度,记录前进指针所指的节点和当前节点的距离,用于计算排位在查找某个节点的过程中,将沿途访问过的层的跨度累计,得到的结果就是跳跃表的排位

}

}

  1. 跳跃表

Zskiplist{

   zskiplistNode  header,tail;

   Int length;

   Level;表中最大层数

}

5.跳跃表

6.层,每个层都有两个属性,前进指针和跨度,每个节点的层数是1--32的随机数

7.分值和成员:

   按分值从小到大排列,成员指向一个对象分值相同时,成员对象较小的回排在靠近标题的方向

 

 

  1. 整数集合intset

1.整数集合(intset)是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。

2.它可以保存类型为int16_t、int32_t或者int64_t的整数值,并且保证集合中不会出现重复元素

3.

intset {

// 编码方式

uint32_t encoding;

// 集合包含的元素数量

uint32_t length;

// 保存元素的数组

int8_t contents[];

}

4.contents数组是整数集合的底层实现:整数集合的每个元素都是contents数组的一个数组项,各个项在数组中按值的大小从小到大有序地排列,并且数组中不包含任何重复项

5.升级

每当我们要将一个新元素添加到整数集合里面,并且新元素的类型比整数集合现有所有元素的的类型都要长时,整数集合需要先进行升级,然后才能将新元素添加到整数集合里面。

  根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间。

  将底层数组现有的所有元素都转换成与新元素相同的类型,并将类型转换后的元素放置到正确的位上(从后往前),而且在放置元素的过程中,需要继续位置底层数组的有序性质不变。

  将新元素添加到底层数组里面。

  将encoding属性更改。

  整数集合添加新元素的时间复杂度为O(N)。

  因为引发升级的元素要么最大要么最小,所有它的位置要么是0要么是length-1。

 升级的好处:

  提升整数集合的灵活性,可以随意将int16,int32,int64的值放入集合。

  尽可能地节约内存

降级:

  整数集合不支持降级操作

 

  1. 压缩列表

1.压缩列表(ziplist)是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。

2.

 

3.每个压缩列表节点可以保存一个字节数组或者一个整数值,而每个节点都由previous_entry_length、encoding、content三个部分组成。

4.previous_entry_length:节点的previous_entry_length属性以字节为单位,记录了压缩列表中前一个节点的长度。因为有了这个长度,所以程序可以通过指针运算,根据当前节点的起始地址来计算出前一个节点的起始地址。压缩列表的从表尾向表头遍历操作就是使用这一原理实现的。

 5.encoding:记录了节点的content属性报错数据的类型和长度

6.content:负责保存节点的值,节点值可以是一个字节数组或者整数

7.连锁更新:因为previous_entry_length属性可能是一个或者是五个字节,插入删除都会引起压缩列表空间的重分配

 

 

  1. 对象
  1. redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统中包含着五大对象系统,String,List,Hash,set,sortedset,每种对象,底层都至少使用了两种不同的数据结构。通过这五种不同的对象类型,redis可以在执行命令之前,根据对象的类型判断一个对象是否可以执行给定的命令。
  2. 使用对象的另一个好处是,可以针对不同的场景,使用不同的数据结构,从而优化对象在不同场景下的使用效率。
  3. Redis还实现了基于引用计数的内存回收机制。

4.

Redisobjetc{

Type:4;五种对象类型

Encoding:4;编码

*ptr;执行底层实现数据结构的指针,有encoding决定

Int refcount;引用计数器

}

 

其中type可以指向下表中的任何一个类型

 

Encoding属性记录了对象所使用的编码,也就是说,这个对象使用了什么数据结构作为对象的底层实现

 

 

 

 

 

 

  1. 字符串对象,字符串对象可是int,raw,embstr

Int:如果保存的是整数类型,并且这个整数可以用long表示

Raw:如果字符串长度大于39个字节,就会使用raw

Emstr:是专门用于保存段字符串的一种优化编码方式。

 

因为raw保存的是长字符串,所需内存较大,所以创建对象时,需要分配两次内存,一次分配redisobjetc对象所需内存,一次分配字符串对象所需内存。

Emstr只需要分配一次内存,redisobject和字符串所需内存分配一块连续内存

Emstr是只读的,redis并未提供修改emstr的方式,对emstr的修改实际上是先转换raw,在进行修改

Int和emstr在满足条件时,会自动转化为raw

 

Long和double也是作为字符串存储的

 

  1. 列表对象,列表对象的编码可以是ziplist和linkedlist

当列表对象同时满足下面两个条件时,则使用ziplist:

    所有字符串元素的长度都小于64字节

    元素数量小于512

优缺点:ziplist是一种压缩列表,它的好处是更能节省内存空间,因为它所存储的内容都是在连续的内存区域当中的。当列表对象元素不大,每个元素也不大的时候,就采用ziplist存储。但当数据量过大时就ziplist就不是那么好用了。因为为了保证他存储内容在内存中的连续性,插入的复杂度是O(N),即每次插入都会重新进行realloc。如下图所示,对象结构中ptr所指向的就是一个ziplist。整个ziplist只需要malloc一次,它们在内存中是一块连续的区域。

  linkedlist是一种双向链表。它的结构比较简单,节点中存放pre和next两个指针,还有节点相关的信息。当每增加一个node的时候,就需要重新malloc一块内存。

  1. 哈希对象,哈希对象底层可以是ziplist或者hashtable

Ziplist:每当添加一个新值,都会添加到尾部,key尾,后value也被添加到尾,所有,key-value是紧挨着的。

当列表对象同时满足下面两个条件时,则使用ziplist:

    所有键值对的键和值的字符串度都小于64字节

    键值对数量小于512

  1. 集合对象,可以是intset或者是ahshtable

满足下面两个条件,使用intset:

    所以有元素都是整数值

    元素数量不超过512个

  1. 有序集合对象,有序集合的编码可能两种,一种是ziplist,另一种是skiplist与dict的结合。

dict字典为有序集合创建了一个成员到分值的映射。给一用O(1)的时间查到分值。

 当有序集合对象同时满足下面两个条件时,则使用ziplist:

所有元素的字符串度都小于64字节

元素数量小于128

  1. 类型检查

类型检查通过redisobject的type属性实现的

  1. 内存回收,就是引用计数器,对象被引用,+1,对象不再被一个程序使用,-1,计数器为0,回收
  2. 对象共享,对象的引用计数器还带有对象共享作用,指向现有对象,计数器+1

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值