基础数据结构
string实现:SDS(Simple Dynamic String) 简单动态字符串
结构:
- int len; 字符串长度
- int free; 空闲长度
- char[] buf 存储字符串的字符数组,采取c语言风格。
buf特点:
- 采用c语言存储字符串的风格,在字符数组后多开辟一个字节空间,用来存储空字符串
\0
, 表示结束符【原本拥有free和len的他是不需要的,但是为了统一还是设了] - 好处是可以直接使用c语言关于字符串的函数。
采用SDS的优点:
- o(1)时间获取长度,而无需遍历数组。
- 避免内存溢出。c语言动态数组添加内容都会默认空间是足够的,若是空间不足就会溢出,因此需要手动添加内存。sds会在添加之前先检查内存是否足够,如不足会首先通过api自动添加内存。
- 避免大量的空间分配行为。c数组在添加新元素会首先进行内存扩充,这是一个非常耗时的工作,需要调用os来进行。而数组库数据是进行需要进行修改的,因此添加操作很常见,sds可以对其进行优化。
sds会在空间判断不足做这样的行为:
(1)若所需空间不足1m, 会首先扩充到需要的大小【如需要13byte,就给13byte】,在多分配一倍+1byte空间【1byte还是作为结束位置】
(2)若所需空间超过1m,会多分配1m + 1byte
若空间充足,sds不会进行扩充
因此,对于sds,他将原本扩容n次就需要分配n次空间降低到了最多只需要分配n次空间。
- 避免了内存泄漏。c数组若某些单元不再使用需要手动释放,否则会内存溢出。sds会自动对这些无用空间进行处理
sds会将这些垃圾空间变成free的一部分,下次扩容可以直接使用这些空间【这样也减少了空间分配的次数】
- 二进制安全。c字符串不能存储
\0
,因此则会导致之后的字符失效【这是字符数组
的缺陷】,sds由于要存储不仅仅是字符,还有大量的字节数据【如图片等等】,因此必须要保证空字符不被过滤,sds对字节流可以进行原样的保存,因此不是字符数组,而是字节数组
- 复用c字符串函数。由于\0的存在可以透明的使用c语言的某些字符串库函数而不用另写。
list实现:LinkedList
typed struct listNode {
void *vlaue; //void指针存储值
listNode *prev;
listNode *next;
}
typed struct list {
listNode *head; //头结点
listNode *tail; //尾结点
unsinged long len; //链表长度
void dup(); //复制节点
int match(); //比对两个节点
void free(); //释放节点
}
特点:
- 双端:拥有prev与next两个指针获取前后节点;
- 无环:tail的下个节点和head的上一个节点都为空;
- 头尾指针:通过head和tail可以在o(1)时间获取头尾节点;
- 多态:
void*
的值类型可以保证存储任意类型的值; - o(1)时间获取长度。
set实现:dict(哈希表/字典)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GsTrn8lG-1615539216854)(https://i.loli.net/2021/03/08/8lv1b9aEBQYPZnA.png)]
set由三个结构共同组合成:
- dictEntry; //哈希表节点
每个桶位上的链表节点,拥有key, value, next三个值,指向同hash的下个节点。
需要注意的是:没有tail指针,因此为了保证插入效率只能采取头插法
- dictht; //哈希表结构
typed struct dictht { int size; //哈希表最大容量; int sizemask; //计算哈希时候的掩码 int use; //当前使用长度 dictEntry[] table; //哈希节点数组,存储每个初始桶位 }
-
dict; //字典结构
typedef struct dictType { dictType type; //指定类型的函数; void *privdata; // 私有数据 int trehashidx; //rehash索引,进行rehash到达的位置,不rehash就为-1,为0表示rehash开始。 dictht ht[2]; //哈希表;之所以两个位置,0是普通存储位,1是rehash时候的复制位置; }
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UPitiGgt-1615539216856)(https://i.loli.net/2021/03/08/FxRKgeTD4c6vrYj.png)]
-
哈希算法:
-
hash:
-
hash算法是为了计算dictEntry的插入数组的位置,并采取头插法插入。
-
过程:
-
计算hash:通过key计算hashcode hashFuntion(key)。计算hashCode采用
Murmur2算法
-
通过hashCode与sizemask得到插入下标:
index = hashCode & sizemask;
这是和java的hashMap同样的方式:
只要将table长度设为2的整数倍,就可以通过hashcode & (len - 1)映射到表的下标上,与的运算速度远高于取余。
-
-
-
rehash:
-
rehash是对运行过程中hashEntry数量增减而适时调整哈希表容量以稳定负载因子的行为,分为扩容和缩减。
-
扩容:
- 当不在执行
BGSAVE与BREWRITEDAOF
命令时,负载因子达到1.0就进行扩容; - 当在执行这两个命令时,负载因子达到5.0开始扩容
扩容过程:
- 初始化ht[1], 表长初始化为
大于ht[0].use * 2 的最小的那个二进制整数幂
【如,现在有3个HashEntry,那ht[1]就会初始化长度为8】 - 对每个ht[0]上的dictEntry进行rehash【根据新的表长生成的sizemask进行重新计算下标】,并将所有entry的引用移动到ht[1]的下标桶位处;
- 将ht[1]修改为新的ht[0],将原本ht[0]释放。
- 当不在执行
-
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aPI3ZBnQ-1615539216859)(https://i.loli.net/2021/03/08/eCyzbTj4KJX6ZsE.png)]
-
缩减:
- 当负载因子低到0.1以下,会进行缩减,容量依然为
大于ht[0].use * 2 的最小的那个二进制整数幂
- 当负载因子低到0.1以下,会进行缩减,容量依然为
-
load_factor = ht[0].used / ht[0].size;
关于
BGSAVE
与BGREWRITEAOF
命令时提高loadfactor的上限:这是为了避免在写时复制时不必要的内存写入。
-
-
渐进式rehash:
- 当哈希表数据量很大时,采用一次移动所有entry的方式会导致服务器暂停,非常影响体验。redis采取渐进式rehash的方式,一次转移一部分entry,在一段时间之后才会完成转移;
- 当达到rehash条件时,开始进行rehash,将rehashidx赋值为0,表示开始;
- 当出现对ht[0]的delete, find, update操作时,会执行一次rehashidx上的rehash,将这个桶位上的entry全部复制到ht[1]上重写经过hash计算的桶位上,并将rehashidx + 1;
- 当新插入entry时,只会插入到ht[1]上,因此ht[0]只减不增,最终会为空。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5FfOfCjh-1615539216862)(https://i.loli.net/2021/03/08/NlvwazbXSEf124g.png)]
- 当哈希表数据量很大时,采用一次移动所有entry的方式会导致服务器暂停,非常影响体验。redis采取渐进式rehash的方式,一次转移一部分entry,在一段时间之后才会完成转移;
sortedSet底层实现:skiplist(跳跃表)
跳跃表是一种有序的链状结构,用来代替平衡树实现平均查找长度o(logn)最差查找长度o(n)的数据查找。
跳跃表底层通过两个结构:跳跃表节点zskiplistNode和跳跃表zskiplist实现。
-
zskiplistNode有如下属性:
- 分数score:用来进行排序的数据;【可重复,重复后按照obj字典序排列】
- 对象obj:存储实际数据的指针【不可重复】
- level[]:层级数组,每个数组单元都指向存储的其他层【如下标2就指向层数为2的节点】, level具有两个指针:
- forward:前进指针,指向下个位置;
- backward:后退指针
跨度
:配合level的forward指针使用,表示level指向的指针离当前节点在排序序列之间的距离,用于统计排列位置。
-
zskiplist有如下属性:
- head, tail :头尾节点
- level:层数最高的节点的层数【此“层数”是说他是level几】;
- length: 节点数目,不包括头结点
-
注:
- 头结点没有level[]以外其他属性赋值,但是也存在,只是没有值。
- 头结点不参与排序与计数。
- 跨度不是用来遍历的,而是用来计算排序位置的:
- 如,从level1开始,查找level4的指针,若level1的节点有level4的指针,则该指针上跨度为3,则level4的排序位置为 1 + 3 = 4; 若每次跨度只为1,那么只能一次增长一个,从level1到l2,l3, l4, 也是4.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vst9CmSv-1615539216864)(https://i.loli.net/2021/03/08/xfiHGtXCU9enO4D.png)]
L5节点位于头结点跨度为3的位置,而头结点计数视为0,因此L5位于第三层。
-
分值可以相等,之后按照对象字典序排序。
-
level[]的size称为节点的层数,或者层的高度,与节点数量无关,是随机生成的一个
1~32
的数【创建节点随机生成】 -
幂次定律:
- level[]存储的层数越高,同样的,遍历速度越快。
总结:
简单说,跳跃表就是一种存储多个序列其他节点地址的数据结构,其快读访问由其level的指针及跨度实现。
集合 实现方式的一种:整数集合
当集合存储元素全为整数时,采用。
特点:
- 存储元素有序;
- 具有16, 32, 64三种类型,每次只会使用一种。
结构:
- encoding:编码方式【uin16_t, …]
- length:集合元素个数
- contents[] :存储元素的数组,有序排列。初始为uint8_t[], 随着插入元素的类型进行
升级
。
升级:
当新插入的元素的范围大于当前数组的单元范围时,会将整个数组的存储范围提升。
如在16位整形中插入65536,会引发其向着32位升级。
升级过程:
- 重新划分空间:按照encoding * length 来划分空间。
- 按照从右向左的顺序移动元素【如下图移动3的过程】
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dPZ9lL8T-1615539216866)(https://i.loli.net/2021/03/08/4LlTtyzZ2J1SCAM.png)]
好处:
- 节省空间,若没有大的数据,可以用小范围控制,只在合适时候升级。
- 避免出错,c语言是静态语言,本身不允许高类型数据进入低类型空间,升级避免了这种问题。
注意:
- 只有全部是整数才能使用整形数组;
- 只能升级不能降级;
- 引起升级的元素要么在最开头,要么在最结尾;
- 集合不允许重复,存储数据有序。
其他结构
压缩列表
为了减少空间的浪费,节省内存
,对某些简单的键使用压缩列表存储。
主要用于list和hash结构,用来存储较短的整形以及字节数组。
压缩列表是一个杂表
,作为列表结构,其中存储的元素类型可以是各式各样的,他通过开头的标志位已经标志位后面的数值来标识这块节点的类型以及长度。
属性;
- zlbytes:整个压缩列表所占内存空间;
- zltail:列表尾节点首地址。
- zllen: 节点数量;
- entry。。。 主体数据部分,由各种类型的数据节点组成;
- zlend:标志位,标识压缩列表的结束位置。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mKgFcIgK-1615539216868)(https://i.loli.net/2021/03/08/kcbXIf9MxtEVzyr.png)]
节点entry属性:
-
previous_entry_length
标记上个节点的长度【用于从后往前遍历:当前节点首地址 - 这个
就是上个节点首地址】;previous_entry_length的长度在前节点小于254字节时为1字节,大于则为5字节。
-
encoding:标记当前节点的类型、大小信息。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p6MN68L8-1615539216869)(https://i.loli.net/2021/03/08/xUuZLHynIsGq53O.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-autE2p5i-1615539216870)(https://i.loli.net/2021/03/08/JbsvUEhAHjp6eWO.png)]
连锁更新:
- 由于一个previous_entry_length在前节点小于254字节存储一个字节内容,一旦前面插入了大于254的单元,就需要将自己的previous_entry_length + 1,因此现在需要5字节,同样,后面也会因为前面长度突然增加到254以上导致更新previous_entry_length,如果大家都是254,就会一直连锁下去,但这种可能性很低。
- 这种情况下,导致插入复杂度为o(N^2).
对象
上面提到了大部分redis存储采用的数据结构。但是实际上redis并不直接采用这些结构,而是通过组合成对象
的方式间接的使用。
组成的对象有五种:
- string
- set
- sortedset
- list
- hash
可以通过 type 键
来获取键的类型。
使用对象的好处:
- 可以使用
引用计数法
, 对使用不到的对象进行回收; - 实现了
对象共享机制
,多个数据库可以共用同一个对象数据。
redis的对象采用结构redisObject描述:
typedef struct redisObject {
type; // 表示对象类的类型【即五种之一】
encoding; // 实现这种结构采用的编码方式【每种结构都至少有两种编码,各有优势】
*ptr; // 指向存储这种编码的数据结构的位置。【如采用sds的raw方式实现的字符串,会指向raw的数据结构】
}
- 通常说的“xx键”指的是一个值为xx类型键值对,因为redis的键都是字符串
对象编码类型如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nwrkjBvw-1615539216871)(https://i.loli.net/2021/03/08/4Gun78vqNgidbEf.png)]
任何对象的实现方式都无外乎这8种的组合。
五种类型采用的编码方式列举:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6g5EVN1N-1615539216873)(https://i.loli.net/2021/03/08/vhVBKs76HPm5oES.png)]
如图,除了string有三种,其他都是两种。
redis可以根据不同的应用场景以及操作需求进行不同实现方式的切换。
string
string采用三种方式:
int, 存储一个long以下的整形时使用;
embstr, sds的一种,申请和释放内存都只进行一次,效率高。但是只读
。long double以下的浮点数以及32位以下的字符串采用
raw,sds一种,申请和释放内存都需要进行两次,可以存储任何类型字符串,支持读写。默认用来存储long double存储不了的浮点数以及32位以上的字符串。
-
int只能存储数字字符,并且对这个数字字符的append(修改)会首先转换为raw,之后也不会转回来。
-
float类型使用embstr和raw,在进行浮点运算时会转回浮点型,操作完成后转回字符串【如 incrybyfloat pi 2.0, 将pi + 2.0】【他不会直接转为raw,而是之前用embstr就用embstr】
-
由于embstr只读,因此对embstr做任何修改都会导致转换为raw【浮点操作不属于字符串操作】
字符串命令的流程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ai7eqrHR-1615539216874)(https://i.loli.net/2021/03/08/UXlp4A1v6PMBi5R.png)]
list
list有两种编码方式:ziplist与linklist
ziplist适合存储整形和短数组,可以节省空间。
linklist只有o(n)的存取速度,但是存储数量可以无限延伸。
两者的头尾节点存取速度都是O(1).
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vBnLqT7s-1615539216875)(https://i.loli.net/2021/03/09/P9mcSJr3Qnsup1g.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s0KgA63h-1615539216876)(https://i.loli.net/2021/03/09/1cO4zPpVwWURICo.png)]
StringObject是对String存储结构的缩写,其实是:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jWkyCfKo-1615539216876)(https://i.loli.net/2021/03/09/wG5dIJYNztciMUE.png)]
上面只有存储键值对的键的时候使用了这种结构。
存储时会优先使用zipList,但是不满足下面两个条件之一就会引发转化为linklist,即编码转化
(1)每个字符串的值不超过64字节。
(2)数组的字节量
大于512byte;
hash
hash有两种编码:ziplist以及dict
ziplist采用线性存储,每次插入键值对都会插入到表尾节点,同样,查找一个元素也只能采用o(N)遍历,但是由于存储量小,也很快。
dict是字典的数据结构,采取hashCode映射数组下标的方式,存取速度很快。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uwEnHT2U-1615539216878)(https://i.loli.net/2021/03/09/Eirxl2mZOC6FRPs.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pO0pzoKR-1615539216879)(https://i.loli.net/2021/03/09/1S5wsZTqu98Yhki.png)]
在以下条件都满足时,使用ziplist存储:
(1)存储的每个键值对的键和值的字符串都小于64bytes;
(2)存储数组长度小于等于512byte。
set
set采用两种存储方式:intset与hashtable
intset只能存储全为整形、元素数量较小的集合。
hashtable可以存储任意类型的集合。
使用hashtable存储时,key用来存储值,而value全部设为null,保证存取速度又不浪费空间。
当满足以下两个条件,可以使用intset:
(1)全为整形;
(2)数量小于等于512个
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jVYvgPlx-1615539216880)(https://i.loli.net/2021/03/09/QTRMUEgSyrtH6A3.png)]
sortedSet
有序集合采用两种方式编码:ziplist与zset【内置skiplist与hashtable】
当存储的值-分
对很少时,采用ziplist。ziplist会使用顺序存储,排序按照score。【可以想象插入和删除、查找都很慢,都需要o(N),但是由于节点很少,也很快】
以下是ziplist的存储:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0Z3QnOvh-1615539216881)(https://i.loli.net/2021/03/09/gQ4HG6ailFtLsDS.png)]
采用zset存储时,zset会同时使用两种结构:skiplist和字典
两种数据结构所引用的是同一个数据
,因此不会耗费额外的空间存储数据。
两种存储结构的值-分
对存储方式:
(1)skiplist采用object属性存储对象,score存储分数;
(2)dict采用key存储字符串对象,value存储分数
为什么同时使用skiplist和dict?
dict用来实现o(1)的
单个节点分数
的取值;(zscore)skiplist用来实现范围操作和排序(zrange, zrank)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B4XXu7MG-1615539216882)(https://i.loli.net/2021/03/09/OpCwAW2baNhP8xz.png)]
上图有些问题,其实两种结构的数据区域共用同一个数据单元;
编码转化:
当不满足任一个条件,ziplist转化为zset:
(1)集合元素数量小于128个;
(2)每个元素长度小于64byte
注:
以上关于编码转化的阈值都可以调节。
redisObject其他机制
命令多态
redis有一些共享的命令,可以被好几种数据结构甚至所有的5种结构使用,如del expire type
当执行这些命令时,需要首先判断redis对象的类型,再访问其结构中的数据。
也有一些私有的命令,其他类型不能使用,如llen只有list才能使用。
类型的检查与多态调用命令基于redisObject的type属性。
在执行命令前,会首先检查type属性是否满足,再根据encoding调用对应数据结构的编码。
del对不同type调用不同的方法,list的llen根据不同的encoding也会调用不同的方法,按照oo的角度,都称为多态。
引用计数与内存回收
redis通过引用计数法决定对象生命周期。
引用的计数体现在redisObject的refcount
属性。
机制:
- 创建一个redis对象,refcount初始化为1;
- 每多一个程序引用这个redis对象,refcount + 1;
- 每少… refcount - 1;
- refcount降为0时,对象回收。
这里的程序一般指的是
服务器程序
,即使用redis作为缓存的服务器项目。
通过object refcount obj
可以查看对象的引用计数。
不仅程序引用会导致引用计数的修改,
被其他redis对象引用
也会修改。
共享对象
refcount也会因为其他redisObject的引用而修改。
当不同的redisObject的引用值相等时,不会创建新的数据单元,而是共同引用同一个对象。
引用不会做其他的事情,只会吧refcount + 1;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3GP8GkDE-1615539216883)(https://i.loli.net/2021/03/09/fCwc7JomjWVBu6q.png)]
注意:redis只会共享
整形
数据,任何包含字符串的数据类型都不会共享
空转时长
redisObject还拥有最后一个参数:lru,表示这个对象上一次被访问的时间,通过当前时间 - lru
可以得到对象的空转时间,这个数据可以通过object idletime obj
命令得到【这个命令不会修改lru值,虽然他也访问了对象】
除了查询空转时长,lru还有一个作用:
创建一个redis对象,refcount初始化为1;
- 每多一个程序引用这个redis对象,refcount + 1;
- 每少… refcount - 1;
- refcount降为0时,对象回收。
这里的程序一般指的是
服务器程序
,即使用redis作为缓存的服务器项目。
通过object refcount obj
可以查看对象的引用计数。
不仅程序引用会导致引用计数的修改,
被其他redis对象引用
也会修改。
共享对象
refcount也会因为其他redisObject的引用而修改。
当不同的redisObject的引用值相等时,不会创建新的数据单元,而是共同引用同一个对象。
引用不会做其他的事情,只会吧refcount + 1;
[外链图片转存中…(img-3GP8GkDE-1615539216883)]
注意:redis只会共享
整形
数据,任何包含字符串的数据类型都不会共享
空转时长
redisObject还拥有最后一个参数:lru,表示这个对象上一次被访问的时间,通过当前时间 - lru
可以得到对象的空转时间,这个数据可以通过object idletime obj
命令得到【这个命令不会修改lru值,虽然他也访问了对象】
除了查询空转时长,lru还有一个作用:
服务器在设置了maxMemory
后,且回收策略为volatile-lru
或者allkeys-lru
,一旦内存超过maxMemory,会引发idletime最大的对象被标记释放,等待回收。