Redis数据库中的键值对都是由对象构成,而对象是由数据结构构成;其中键值对的键可以是个字符串对象;值对象可以五选一(字符串、列表、哈希、集合、有序集合);
这块所说的字符串与平常使用的java中的字符串有些区别,当然主场还是redis本着尊重开发者的原则咱还是说说C字符串比较好;redis中的值对象虽然提供了比较丰富的数据结构,但是学起来也是有一定的难度,尽量先对某一语言的数据结构与算法有一定了解学起来就会比较快,博主之前有些基础,大概一个多星期就学完了;还是推荐先看看java的数据结构,壮哉我大Java红黑树无敌;虽然有序集合这块算是redis里面比较不好理解的但是跟那棵树比基本上就一幼儿园小朋友;
redis虽然是由C语言实现的但是他只是将C字符串作为字面量使用,这里我们用SDS(Simple Dynamic String 简单动态字符串)来表示字符串。
由sds.h/sdshdr结构表示,共三个属性
free:用于记录分配到的额外的未使用空间(下文中的惰性空间释放策略与空间预分配策略中会用到)
len:记录字符串长度(有的时候知道自己有多长也是有优势的)
buf:一个字节数组,前面存字符串,最后一个字节存/0,代表空白字符(个人觉得是向C语言致敬,后文会提到用处)
刚才我们介绍了SDS,现在说说有什么优势,其实无论是redis和MC还是rdb和aof,都是要通过比对来判强弱;个人以为还是Java牛逼(我StringBuilder/StringBuffer不服);--(JVM说了 你SDS再牛我字符串常量池也是你爸爸);
这块我们以C字符串来和SDS作对比:
redis的链表应用还是比较广泛的(列表对象(高级实现)、发布与订阅、慢查询、监视器等)
redis中的链表节点(listNode结构)由一个前驱指针、后继指针以及自身节点值组成,算是个双向链表
list结构:包含一个表头节点指针和一个表尾节点指针(header/tail);可以以O(1)的时间复杂度定位到表头表尾;其中表头节点的前驱指针和表尾节点的后继指针指向Null,也就是说redis中的链表是一个无环的双向链表
redis为不同类型的数据提供了不同类型的特定函数,所以链表可以支持多种丰富的数据类型
Redis的底层由字典实现,对于redis的CRUD就是基于对字典的操作
字典的底层是由哈希表构成 ,哈希表又由多个哈希表节点构成每个哈希表节点都存了一个字典的键值对
哈希表由dict.h/dictht结构表示 有4个属性
table:是一个数组,数组中的每个数组项都是一个指向一个dictEntry结构的指针,每个dictEntry结构都保存着一个键值对
size:记录哈希表的长度即数组长度
used:记录哈希表中哈希表节点的个数(有多少个键值对)
sizemask:总是size-1,与索引值共同确定一个键应该被放到table数组的哪个索引上
哈希表节点由dict.h/dictEntry结构表示 有3个属性
key:键值对的键
v:键值对的值(值可以是个指针,也可以是uint64_t/int64_t整数)
next:指向下一个节点的指针
字典由dict.h/dict结构来表示 有几个属性
type和privdata属性是为了不同类型的键值对,为实现多态字典而设置的属性
type:是一个指向一个dictType结构的指针,每个dictType结构都包含一组用于处理多种不同类型键值对的函数;Redis为多态字典设置提供了不同类型的特定函数
privdata:用于存放传给那些不同类型特定函数的可选参数
ht[]:字典中最重要的属性还是得提到ht,ht是一个数组项但是只有两个项(两个哈希表),ht[0],ht[1];正常情况下字典只会对ht[0]进行CRUD操作,ht[1]只会在对ht[0]rehash时使用;
rehashidx:该属性也是与rehash有关,可以说是rehash的进度条;俗称索引计数器变量;非rehash时值为-1(下文还有)
负载因子:used/size 哈希表中元素个数/哈希表的长度
作用:对哈希表进行扩容缩容
为了维持负载因子在一个比较正常的范围对哈希表进行扩容缩容操作,也就是说在哈希表中的哈希表节点个数过多过少时进行rehash,当然不止这一个因素,下文会具体讲解
rehash一步肯定完不了大概需要三步:
分多次的渐进的将ht[0]中的所有键值对rehash到ht[1]中
为什么要有渐进式rehash呢,因为在哈希表中键值对数量极大时比如几亿个,一次性rehash到ht[1],会造成庞大的计算量,可能会导致服务一段时间内停止
渐进式rehash运行时四个步骤:
优势:与一次性rehash相比,将rehash分配在各个CRUD操作中,可以分多次rehash,避免对服务器造成大量计算负担
渐进式rehash时对哈希表的操作:
新增操作和别的不太一样:只会在ht[1]中新增元素,保证ht[0]中的used值有减不增
查询等操作都是先去ht[0]中查找,要是没有就去ht[1]中操作
满足以下条件时,对哈希表进行扩容操作
- 当服务器未执行BGSAVE和BGREWITEAOF命令时,并且负载因子大于等于1;
- 当服务器执行BGSAVE和BGREWITEAOF命令时,并且负载因子大于等于5;
为什么这两种情况下的负载因子的要求不一样呢,因为服务器进程在执行BGSAVE和BGREWITEAOF命令时,会创建一条子进程来执行创建RDB文件/AOF后台重写,会执行大量对磁盘的写入操作,将负载因子上限调大也是为了避免此时发生扩容给服务器带来更大的压力,所以服务器希望在未执行该命令时完成扩容
当负载因子小于0.1时,对哈希表进行缩容操作
跳表在redis中引用的并不算十分广泛,只用在有序集合的实现中作为组件出现;再有就是集群中子节点的数据结构;
跳表又称跳跃表;它是从单链表中抽出索引层,索引层越多效率越高,也就是具备了多级索引层的链表,与实现二分查找算法的数组较为相似;
跳表支持最好情况下O(logN) 最坏情况下O(n) 的查找时间复杂度
跳表节点由redis.h/zskiplistNode结构定义 有以下属性
level(层):每个层都是一个数组,包含多个元素比如(前进指针和跨度值),层越多,跳表查询的速度越快
forword:前进指针,从表头方向指向表尾方向的一个指针,可以用于表头向表尾方向的访问
span:跨度值,用于记录前进指针所指向的节点与当前节点的距离
backword :后退指针,从表尾方向指向表头的指针,与前进指针不同的是,后退指针每次只能退一个节点,因为每层只有一个后退指针,却可能包含多个前进指针
score:分值;一个double类型的浮点数值。用于记录元素的分值,跳表中按照分值大小从小到大排序
obj:成员对象;一个指向了sds字符串的指针
在跳表中,每个节点的obj必须是唯一的,但是节点的分值可以相同,分值相同时,按照成员对象在字典序中的大小排列,小的在前(接近表头方向),大的靠后(靠近表尾方向)
跳表由redis.h/zskiplist结构定义
其实由多个跳表节点也可以组成跳表,但是比不上zskiplist结构的跳表;
zskiplist中提供了几个属性:
header/tail:表头节点指针与表尾节点指针,支持O(1)的时间复杂度查找定位表头表尾节点
length:跳表中节点个数
level:跳表中具备最大层级的节点(O(1)时间复杂度定位)
整数集合可以作为集合对象的底层实现;有所限制集合中元素只能是整数值,并且不能有任何重复元素存在
由intset.h/intset结构定义:
contents:是一个保存元素的数组,集合中的每个元素都是contents数组的一个数组项(item),各个项在数组中按值从小到大有序的排列,并且数组中不包含任何重复项
length:记录了集合的元素数量(contents数组的长度)
encoding:
升级:
压缩列表可以作为列表对象、哈希对象、有序集合对象的底层实现,他是redis为了节省内存而实现的,由一系列特殊编码组成的内存块构成的顺序型数据结构(可以当做是个数组)
压缩列表 ziplist
zlbytes:记录压缩列表占用的内存字节数
zllen:记录压缩列表包含的节点数量
entryX:记录压缩列表中所有的节点
zltail:记录表尾节点到起始地址有多少字节
zlend:用于标记压缩列表的末端
压缩列表节点 ziplistNode
previous_entry_length属性:以字节为单位,记录了压缩列表中前一个节点的长度
encoding属性:记录了节点的类型以及长度
content属性:负责保存节点的值,节点的值可以是个字节数组(字符串)或者整数
连锁更新:添加新节点到压缩列表,或者从压缩列表中删除节点,可能会引发连锁更新操作,但这种操作出现的几率并不高
下一篇再见!感谢阅读者阅读并提出建议!骂都可以就是别带脏字!下一篇咱们接着说对象,今儿就先歇了