1. 简单动态字符串SDS
free 表示未使用的空间; len表示已保存的字符串长度;
buf是一个char类型的数组,图片中保存了5个字符,最后一个字节空字符'\0'不算入已保存的长度。
最后保留一个空字符,是自动的,方便重用一部分C字符串函数
SDS与C字符串区别
1. SDS获取字符串长度时间复杂度为O(1),而C字符串需要遍历字符串
2.SDS不会缓冲区溢出
比如有紧邻着的C字符串s1和s2
执行 strcat(s1, " Cluster"); 造成了s2的意外修改
SDS空间分配策略可以杜绝溢出:先检查空间,不足就分配,然后再拼接
3. 减少修改字符串带来的内存重分配次数
C字符串增长或者缩短,都要对保存C字符串的数组进行一次内存重分配操作。
SDS通过free和len属性,有两种分配优化策略:
①空间预分配:->用于优化增长
如果SDS修改后len长度小于1MB,则分配与len相同的大小的未使用空间,
那么SDS的buf数组实际长度为len+free+1 (len==free,1为保存空字符 )。
如果SDS修改后len大于等于1MB,那么会额外分配1MB未使用空间,
那么这时的buf实际长度为len+1MB+1byte
②惰性空间释放:->用于优化缩短
缩短字符串不会立即释放buf多余内存,而是修改len和free值,多余的空间在增长时可能会有用
4. 二进制安全
C字符串会识别空字符作为结束符,不能保存含有空字符的数据。(如:图片、音频、视频这样的二进制数据)
SDS有len属性,可以保存任意个数二进制数据。
5.兼容部分C字符串函数
2.链表
integers列表键包含了1到1024的整数,底层就是链表。
还有发布与订阅、慢查询、监视器、Redis服务器本身等都会用到链表
dup函数用于复制链表节点所保存的值
free函数用于释放链表节点所保存的值
match函数用于对比链表节点所保存的值和另一个输入值是否相等
Redis链表特性:
双端:有prev和next指针
无环:表头prev和表尾next都指向null
表头指针和表尾指针
带链表长度属性len
多态:dup、free、match设置类型特定函数,用于保存不同类型的值
3.字典
website键的底层就是一个字典,该字典包含了10086个键值对
哈希表结构图:
size哈希表大小;sizemask哈希表大小掩码,用于计算索引值,总是等于size-1;used已有
字典结构图:
例: 如果将键值对k0和v0添加到字典里面,程序会先使用语句:
hash = dict -> type -> hashFunction(k0);
计算键k0的哈希值
假设哈希值为8,那么继续计算索引值
index = hash&dict->ht[0].sizemask = 8 & 3 = 0;
那么会将这个键值对放在dictEntry中索引为0的位置
解决冲突
使用链地址法
因为没有链尾指针,所以冲突插入的节点都是在链头插入的
rehash
① 为字典ht[1]哈希表分配空间
如果是扩展,ht[1]大小为第一个大于等于ht[0].used * 2 的2^n
如果是搜索,ht[1]大小为第一个大于等于ht[0].used 的2^n
②将ht[0]上的所有键值对rehash到ht[1]上面
③ 迁移完后,释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白哈希表,为下次rehash作准备
扩展和收缩
当服务器没有执行BGSAVE或者BGREWRITEROF时,并且负载因子大于等于1;
或者,正在执行上述命令,并且负载因子大于等于5。进行扩展
当负载因子小于0.1时,自动开始收缩操作。
渐进式rehash
当普通情况下rehashidx值为-1,当rehash过程中,随着进展改变,其实是根据ditEntry数组顺序的
在rehash中,会同时使用ht[0]和ht[1],查找键现在ht[0]找,再从ht[1]找。
新添加的键值对直接保存到ht[1]
4.跳跃表
在redis中实现有序集合键或者在集群节点中用作内部数据结构,最快logN,最慢N
header指向表头结点
tail指向表尾
level层数最大的那个节点的层数
length包含节点数量,表头节点不计入
表头节点其实和其他节点一样,省略了BW、score和obj,因为用不到
5.整数集合
encoding表示着整型最大可表示的范围
contents按从小到大保存
升级
当当前的encoding不能存放新的元素,就会升级,扩展分配空间。
往往新元素放在开头或者最后,
这样可以提升灵活性、节约内存。
不支持降级,如把刚刚造成升级的元素删除,编码也不会回复原来的状态
6.压缩列表
列表键和哈希键实现之一,都包含短字符串和小整数值才会使用。
zlbytes压缩列表总长
zltail指向尾部偏移量,指向列表起始地址指针p加上该偏移量讲就是entryN的地址
zllen实体节点数量
entry节点结构:
previous_entry_length 属性长度是1字节或者5字节
如果前一节点小于254字节,那么属性为1字节
如果前一节点大于等于254节点,属性第一个字节设置成0xFE,后面4字节保存长度
这样就可以通过偏移计算前一个节点的起始位置,可以从尾向头遍历
encoding记录了content的类型和长度
开头00、01、10分别表示1字节、2字节、5字节的数组编码,后面几位表示长度
开头11表示整数编码
content,例:
连锁更新
当插入一个254以上的节点,而其他节点都在250-253之间,那么会引起一系列内存重分配
删除也会导致这样,但是总的来说这样的概率很低,所以放心。。
7.对象
Redis真正实现的是对象,是基于前面至少一种数据结构
一般会有键和值两个对象用redisObject表示
使用TYPE命令时返回的是键对应的值对象类型
使用OBJECT ENCODING命令返回键对应的值对象的编码
encoding属性极大提高了效率,不同场景下用不同编码
字符串对象
编码可以是int、raw(SDS)、embstr(SDS)
如果保存的字符串对象值长度小于等于44字节,会用embstr(针对短字符串有优化)
raw会调用两次内存分配来形成redisObject和sdshdr,释放需两次
embstr只需调用一次,释放也只需一次
long double类型也会用字符串来保存,执行操作才会转换成浮点型,然后把结果转换成字符串保存
编码转换
int类型的数据经过APPEND命令加入字符串,会转成raw
embstr是只读的,经过修改命令后会变更成raw编码
列表对象
ziplist和linkedlist
满足以下两个条件会用ziplist编码:
所有字符串元素长度都小于64字节
元素数量小于512个
哈希对象
ziplist或者hashtable
满足一下条件会使用ziplist
键和值的字符串长度都小于64字节
键值对数量小于512
集合对象
intset或者hashtable
满足以下两个条件,会使用intset编码
所有元素都是整数值
元素数量不超过512个
有序集合对象
ziplist或者skiplist
满足以下条件,会使用ziplist编码
元素数量小于128个
所有元素长度小于64字节
类型检查与命令多态
执行命令前,通过检查type属性来判断是否执行
然后检查encoding来确定具体用哪个对应的API来执行
内存回收
引用计数(跟jvm垃圾回收类似)
创建新对象时,引用计数值初始化为1
随着被程序引用和完成,计数增减,到0后对象内存会被释放
对象共享
和上面的内存回收搭配使用,
在初始化服务器时,会创建0-9999所有整数值的一万个字符串对象
当服务器需要这些字符串对象时,会直接引用,计数+1,而不会新建
由于验证复杂的对象耗cpu,所以redis只对整数值的字符串对象进行共享
对象空转时长
redisObject中有个lru属性记录了对象最后一次被访问的时间
由命令OBJECT IDLETIME命令返回,他会访问对象,但不会修改对象lru属性
如果内存超过maxmemory上限,会优先释放lru属性大的对象