一、字符串 SDS
Redis的底层的字符串并不是使用C语言字符串(C字符串),而是自己定义了动态字符串五种数据类型对应的实现:String
记录长度
C字符串由于没有记录字符串长度,每次执行计算长度时都会每个字符进行计数,时间复杂度是O(N);在SDS由于记录了必要的空间长度,所以redis就算反复执行计算字符串长度时时间复杂度都是O(1)
防止缓存溢出
在C字符串中由于不记录自己的字符串长度,如果在执行修改字符串时没有提前分配空间就会造成长度溢出,而SDS记录了必要的空间长度,所以每次进行字符串修改操作时,会检查自身的空间是否足够容纳字符串的需求,检查如果空间不足时就会先进行内存重新分配才会执行操作,由于记录了必要的空间长度,所以这个检查的性能消耗是几乎可以忽略的。
减少内存重新分配
SDS通过减少了空间的重新分配,所以有效的提升了性能,SDS通过以下手段减少空间的重新分配:
- 1、空间预分配
在修改字符串时,SDS检查空间不足进行空间拓展时,如果拓展的空间小于1MB,就会拓展到字符串长度的两倍,例如一个字符串长度是13字节,SDS就会拓展到13+13+1(1字节是保存'\0',SDS对此不作记录),这样SDS就额外多了13字节,当再有修改操作时就会检查额外空间,记录在free字段中,当要在进行修改时SDS就会检查额外空间加现有是否足够,足够就不需要进行内存重新分配,如果拓展的空间大于1MB时,就会直接在所需空间的基础上再多分配1MB的内存空间。
- 2、惰性空间释放
当修改字符串是一个缩短操作时,SDS并不会把空间释放掉,而是使用free记录额外空间,以备以后使用,这样就又减少了内存重新分配的次数
二进制安全
C字符串只能存储文本数据不能存储二进制数据,而Redis提供了API处理二进制数据使SDS能够存储图片、视频、压缩文件等二进制数据
兼容C字符串函数
SDS有自己的API也能使用C字符串的函数
总结:
二、双向链表(linkedlist)
Redis使用的C语言中并没有链表结构,Redis构建了自己的双向链表作为list的数据结构之一,因为双向链表占用的内存比压缩列表要多, 所以当创建新的列表键时, 列表会优先考虑使用压缩列表, 当list的元素比较多或者元素的长度都比较长的时候, 才从压缩列表实现转换到双向链表。
五种数据类型对应的实现:List
Redis中定义的双向链表listNode:三、字典(map)
C语言中没有内置字典的数据结构,Redis构建了自己的字典结构,Redis中的字典采用哈希表作为底层实现,一个哈希表有多个节点,每个节点保存一个键值对。Redis的数据库底层就是字典
字典是Hash的底层实现的数据结构之一,Hash默认使用压缩列表,当Hash包含的键值对比较多,或者键值对中的元素都是比较长的字符串时,Redis就会使用字典作为Hash的底层实现。
hash使用字典的条件:
- hash对象保存的键和值字符串长度都大于64字节
- hash对象保存的键值对数量大于512
五种数据类型对应的实现:Hash
下面是redis字典的哈希表结构定义如下:,其中type保存了一组操作键值对的函数,privdata是保存了传给函数的可选参数,ht数组中ht[0]是哈希表,h[1]是用于扩容时使用的。
rehash的步骤:
哈希表扩展的条件
在bgsave或者bgwriteaof是数据异步备份复制的操作,异步操作会创建子进程来完成,子进程会按照写时复制来优化,所以为了避免不必要的内存写入,最大限度的节约内存,在子进程运行期间尽量减少哈希表扩展操作
当哈希表的加载因子小于0.1时会自动进入缩容操作
渐进式rehash
扩容或者缩容是将h[0]键值rehash转移到h[1],这个操作不是一次性的转移,而是渐进式的链地址法解决哈希冲突
哈希冲突时形成一个单向链表解决哈希冲突可以看到,Redis解决哈希冲突是使用next连接相同键值形成一个链表,也就是类似java中hashmap的链地址法
总结:
四、跳跃表(skiplist)
跳跃表是有序的数据结构,平均复杂度是O(logN),最坏复杂度是O(N),跳跃表是sort set数据类型的底层数据结构实现之一
zskiplistNode是跳跃表的节点结构。zskiplist是保存跳跃表节点信息,比如节点数量,指向头尾节点的指针等信息。
五种数据类型对应的实现:SortSet
zskiplistNode的结构定义:zskiplist的结构定义
链表的检索效率非常低,而跳表改善解决了链表的检索效率低的问题
下面是查询23的例子,从头结点开始找,先跳到7,然后跳到19,然后到22,再到23,中间跳过了3和11总结:
五、整数集合(intset)
整数集合(intset)是实现set数据类型的底层数据结构之一,其底层有两种实现方式,当value是整数值时,且数据量不大时使用inset来存储,其他情况都是用字典dict来存储。整数集合可以保证不会出现重复数据。
contents数组是整数集合的底层实现,整数集合的每一个元素都是contents数组的一个数组项,各个项按照数值的从小到大排序,并且不包含重复项
五种数据类型对应的实现:Set
整数集合数据结构定义如下:encoding编码方式:共有三种,INTSET_ENC_INT16、INSET_ENC_INT32和INSET_ENC_INT64三种,分别对应不同的范围。Redis为了尽可能地节省内存,会根据插入数据的大小选择不一样的类型来进行存储。默认是INTSET_ENC_INT16
intset的结构图如下:升级
降级
不支持降级,一旦升级就会保持升级后的状态或许再次升级的升级的状态
总结
六、压缩列表(ziplist)
压缩列表是Redis为了节约内存而开发的,是list和hash的底层实现之一。一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或者一个整数值。
五种数据类型对应的实现:List,Hash
- List
当一个list只包含少量列表项,并且每个列表项要么就是小整数,要么就是长度比较短的字符串,redis就会使用压缩列表来做列表键的底层实现 - Hash
当一个hash只包含少量键值对,并且每个键值对的键和值要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做hash的底层实现。
五种数据类型
五种数据类型会根据不同的数据量大小和数据长度的不同切换不同的编码类型,不同的编码类型对应不一样的数据结构,下面是各个数据结构对应的编码使用OBJECT ENCODING命令可以查看编码类型
String
String类型对应了三种不一样的编码,int、embstr、raw
- int:如果String存储的是整数,就会使用int编码
- embstr:embstr是SDS编码的一种优化,当存储的字符串长度小于等于32字节时会使用embstr,其有以下好处:
- raw:raw是SDS的编码,当储存的字符串长度大于32字节时,使用raw编码
编码转化:
List
List对应的实现数据结构有两种:ziplist和linkedlist,对应的编码分别是ziplist和linkedlist(由于使用ziplist编码时,比其他的编码类型更加节省内存,由于元素数量少,连续内存块方式的ziplist载入内存中会更快,而当元素变多或者元素的长度过长时ziplist的优势就会消失,就会转化为其他功能更多的编码)
下面是List编码转化的条件:Hash
Hash对应的实现数据结构有两种:ziplist和hashtable,对应的编码分别为ziplist和hashtable。hashtable是编码底层是由字典实现的。
下面是Hash编码转化的条件:Set
Set对应的实现数据结构有两种:intset和hashtable,对应的编码分别为intset和hashtable。
下面是Set编码转化的条件:SortSet
SortSet对应的实现数据结构有两种:ziplist和skiplist,对应的编码分别为ziplist和skiplist。
下面是SortSet编码转化的条件: