字符串
demo
set msg demo
demo 在底层创建的就是一个sds(simple dynamic string)字符串。
sds 除了存储字符串之外,还会被用作各类缓冲区。
实现
最新版本的sdshdr的代码如下
redis设计与实现的代码
与c语言字符串的区别
- 与c字符串相同用’\0’来做字符串的结尾。也就是说整个字符串的长度为 alloc + 1 (len + free + 1)
- c字符串没有len字段,每次获取len需要对整个的字符串遍历,是O(N)的时间复杂度。
- c strcat(char dest,char src),需要预分配内存,如果内存不够会引起报错,默认不会检查。sds因为知道字符串长度,会预检查,不会引发缓冲区溢出的问题。
- 字符串增减引起的问题
- c字符串在拼接和缩短的时候需要重新分配内存,否则会产生内存溢出或者内存泄露的相关问题。
- 而重新分配空间涉及到系统调用,是个相对耗时的操作。
- 普通程序字符串不太可能频繁更改长度。所以重新分配内存是可以接受的。
- redis数据库value会被相当频繁的修改,会产生性能问题。
- 字符串增减解决方案(其实就是空间换时间)
- 空间预分配
- 预分配sds所需要的额外的空间
- 当sds的长度小于1m,在扩展时会将free和len设置成等长(即字符串长度若为10,则free也为10,整个所占用空间为10+10+1(末尾的标志位))。
- 若大于1m,则free=1m,总长度为原始长度+1m+1
- 惰性空间释放
- 若缩短长度,则缩短的长度将会加入free之中
- sds会在必要时刻真正释放未使用空间(书中没讲,代码找不到,可能是类似gc的机制?),调用的sdsfree方法。
- 空间预分配
- sds二进制安全
- c字符串用’\0’来标识字符串的结尾,所以字符串中如果含有’\0’就会出问题了。
- sds 是一个buf来保存,不依赖’\0’,所以是二进制安全的。
- 兼容部分c字符串函数(’\0’结尾的目的)
链表
demo
lpush list_demo 1 2 3 4
llen list_demo
源代码
|
|
总结
- 带头尾指针,为了满足lpush & rpush
- 双向链表,无环
字典(dict)
demo
set msg “hello world”
hmset hash field1 value1 field2 value2 field3 value3
dict是一个用于维护key和value映射关系的数据结构
源代码
|
|
|
|
采用的是增量式rehash
- java使用的是一次性rehash
- redis为了单次插入的时间,决定将rehash分布到多个操作之中,每次只rehash10个,同时增删改查都会启动rehash操作
- rehash会导致数据分布在两个不同的table里,查询的时候也会轮训两个table,只有在table[0]找不到的情况下才回去找table[1]
- 插入数据时候会直接插入到table[1]
哈希表的扩展与收缩
- 服务器没有执行BGSAVE OR BGREWRITEOF 命令,而且哈希表的负载因子大于等于1.
- 服务器目前正在执行BGSAVE OR BGREWRITEOF 命令,且哈希表的负载因子大于等于5.(copy on write 的时候尽量避免扩展,节省内存)
- 当负载因子小于0.1时,程序自动开始对哈希表进行收缩操作。
robj
demo
redis 会根据不同的输入值选择不同的数据结构
key 和 value 都是使用的robj(reference count 最开始不明白value会在什么时候复用,后面发现blpop会复用key)
set int_key 1
set str_key str
底层是有robj来控制的数据结构类型
源码
|
|
这段代码执行的操作比较复杂,我们有必要仔细看一下每一步的操作:
- 第1步检查,检查type。确保只对string类型的对象进行操作。
- 第2步检查,检查encoding。sdsEncodedObject是定义在server.h中的一个宏,确保只对OBJ_ENCODING_RAW和OBJ_ENCODING_EMBSTR编码的string对象进行操作。这两种编码的string都采用sds来存储,可以尝试进一步编码处理。
#define sdsEncodedObject(objptr) (objptr->encoding == OBJ_ENCODING_RAW || objptr->encoding == OBJ_ENCODING_EMBSTR)
- 第3步检查,检查refcount。引用计数大于1的共享对象,在多处被引用。由于编码过程结束后robj的对象指针可能会变化(我们在前一篇介绍sdscatlen函数的时候提到过类似这种接口使用模式),这样对于引用计数大于1的对象,就需要更新所有地方的引用,这不容易做到。因此,对于计数大于1的对象不做编码处理。
试图将字符串转成64位的long。64位的long所能表达的数据范围是-2^63到2^63-1,用十进制表达出来最长是20位数(包括负号)。这里判断小于等于21,似乎是写多了,实际判断小于等于20就够了(如果我算错了请一定告诉我哦)。string2l如果将字符串转成long转成功了,那么会返回1并且将转好的long存到value变量里。
在转成long成功时,又分为两种情况。
第一种情况:如果Redis的配置不要求运行LRU替换算法,且转成的long型数字的值又比较小(小于OBJ_SHARED_INTEGERS,在目前的实现中这个值是10000),那么会使用共享数字对象来表示。之所以这里的判断跟LRU有关,是因为LRU算法要求每个robj有不同的lru字段值,所以用了LRU就不能共享robj。shared.integers是一个长度为10000的数组,里面预存了10000个小的数字对象。这些小数字对象都是encoding = OBJ_ENCODING_INT的string robj对象。
第二种情况:如果前一步不能使用共享小对象来表示,那么将原来的robj编码成encoding = OBJ_ENCODING_INT,这时ptr字段直接存成这个long型的值。注意ptr字段本来是一个void *指针(即存储的是内存地址),因此在64位机器上有64位宽度,正好能存储一个64位的long型值。这样,除了robj本身之外,它就不再需要额外的内存空间来存储字符串值。
接下来是对于那些不能转成64位long的字符串进行处理。最后再做两步处理:
如果字符串长度足够小(小于等于OBJ_ENCODING_EMBSTR_SIZE_LIMIT,定义为44),那么调用createEmbeddedStringObject编码成encoding = OBJ_ENCODING_EMBSTR;
如果前面所有的编码尝试都没有成功(仍然是OBJ_ENCODING_RAW),且sds里空余字节过多,那么做最后一次努力,调用sds的sdsRemoveFreeSpace接口来释放空余字节。
append和setbit命令的实现中,.c中的dbUnshareStringValue函数,将string对象的内部编码转成OBJ_ENCODING_RAW的(只有这种编码的robj对象,其内部的sds 才能在后面自由追加新的内容),并解除可能存在的对象共享状态。这里面调用了前面提到的getDecodedObject。
关于embstr
- 如果字符串长度小于39字节(详细解释),则会使用embstr模式
- embstr和raw一样都会使用robj和sdshdr结构来表示字符串对象,但是raw会调用两次来创建robj和sdshdr结构,embstr则通过一次内存调用分配一块连续的空间。
跳跃表
demo
zadd key 1 member1 2 member2
- 当数据较少时,sorted set是由一个ziplist来实现的。
- 当数据多的时候,sorted set是由一个dict + 一个skiplist来实现的。简单来讲,dict用来查询数据到分数的对应关系,而skiplist用来根据分数查询数据(可能是范围查找)。12345typedef struct zset {// 用于score的o1操作dict *dict;zskiplist *zsl;} zset;
|
|
源代码
|
|
疑问
关于span在更新的时候需要遍历整个数组去整个遍历span的值,这样会不会有损效率
update[i]是同一层的前驱节点
转换
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
当sorted set中的元素个数,即(数据, score)对的数目超过128的时候,也就是ziplist数据项超过256的时候。
当sorted set中插入的任意一个数据的长度超过了64的时候。
整数集合
demo
sadd numbers 1 3 5 7
object encoding numbers
intset :整数集合
源代码
|
|
- encoding分为 8 16 32 64,目的用于节约内存
- 假如原来的encoding不能容纳新的数据,将会进行一次升级,encoding会升级成新数据的大小,由于空间是2的指数倍扩展的,挪动相对容易
- 整个数组不支持降级操作。
压缩列表(ziplist)
demo
rpush 1st 1 3 5 10086 “hello” “world”
object encoding 1st
quicklist
默认配置下是quicklist(以上是redis的设计与实现的例子)
127.0.0.1:6379> hmset ziplist key1 value1 key2 value2
OK
127.0.0.1:6379> object encoding ziplist
“ziplist”
而hash的表现很正常
问题在哪
看了一下2.8的源代码
2.8版本
3.2版本
初始化的时候直接生成了quicklist,而直接去除了2.8的ziplist的设计。
|
|
上述配置看起来是遗留问题(当时是那么认为的,后面发现了quicklist,2.8代码里统一用的ziplist,quicklist是优化方案)
理解
ziplist是一个经过特殊编码的双向链表,它的设计目标就是为了提高存储效率。ziplist可以用于存储字符串或整数,其中整数是按真正的二进制表示进行编码的,而不是编码成字符串序列。它能以O(1)的时间复杂度在表的两端提供push和pop操作。
ziplist充分体现了Redis对于存储效率的追求。一个普通的双向链表,链表中每一项都占用独立的一块内存,各项之间用地址指针(或引用)连接起来。这种方式会带来大量的内存碎片,而且地址指针也会占用额外的内存。而ziplist却是将表中每一项存放在前后连续的地址空间内,一个ziplist整体占用一大块内存。它是一个表(list),但其实不是一个链表(linked list)。
数据结构
…
各个部分在内存上是前后相邻的,它们分别的含义如下:
entry数据结构
源代码
|
|
这个函数是在指定的位置p插入一段新的数据,待插入数据的地址指针是s,长度为slen。插入后形成一个新的数据项,占据原来p的配置,原来位于p位置的数据项以及后面的所有数据项,需要统一向后移动,给新插入的数据项留出空间。参数p指向的是ziplist中某一个数据项的起始位置,或者在向尾端插入的时候,它指向ziplist的结束标记
每次插入需要计算数据长度,如果超过长度可能会引发拷贝,重新调用内存的分配。
运用
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
随着数据的变大,ziplist会转换成dict(hash结构)
- 每次插入或者修改可能会引发内存拷贝性能很差。
- 一旦发生内存拷贝,内存拷贝的成本也相应增加,因为要拷贝更大的一块数据。
- 当ziplist数据项过多的时候,在它上面查找指定的数据项就会性能变得很低,因为ziplist上的查找需要进行遍历。
quicklist(为了更好的支持list,优化版本的ziplist)
demo
demo 和 配置 在ziplist中有
理解
list具有这样的一些特点:它是一个能维持数据项先后顺序的列表(各个数据项的先后顺序由插入位置决定),便于在表的两端追加和删除数据,而对于中间位置的存取具有O(N)的时间复杂度。
为了解决
源代码
|
|
解决的问题
- 双向链表便于在表的两端进行push和pop操作,但是它的内存开销比较大。首先,它在每个节点上除了要保存数据之外,还要额外保存两个指针;其次,双向链表的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片。
- ziplist由于是一整块连续内存,所以存储效率很高。但是,它不利于修改操作,每次数据变动都会引发一次内存的realloc。特别是当ziplist长度很长的时候,一次realloc可能会导致大批量的数据拷贝,进一步降低性能。
存储结构的优化
- 每个quicklist节点上的ziplist越短,则内存碎片越多。内存碎片多了,有可能在内存中产生很多无法被利用的小碎片,从而降低存储效率。这种情况的极端是每个quicklist节点上的ziplist只包含一个数据项,这就蜕化成一个普通的双向链表了。
- 每个quicklist节点上的ziplist越长,则为ziplist分配大块连续内存空间的难度就越大。有可能出现内存里有很多小块的空闲空间(它们加起来很多),但却找不到一块足够大的空闲空间分配给ziplist的情况。这同样会降低存储效率。这种情况的极端是整个quicklist只有一个节点,所有的数据项都分配在这仅有的一个节点的ziplist里面。这其实蜕化成一个ziplist了。
push逻辑
- 当插入位置所在的ziplist大小没有超过限制时,直接插入到ziplist中就好了;
- 当插入位置所在的ziplist大小超过了限制,但插入的位置位于ziplist两端,并且相邻的quicklist链表节点的ziplist大小没有超过限制,那么就转而插入到相邻的那个quicklist链表节点的ziplist中;
- 当插入位置所在的ziplist大小超过了限制,但插入的位置位于ziplist两端,并且相邻的quicklist链表节点的ziplist大小也超过限制,这时需要新创建一个quicklist链表节点插入。
- 对于插入位置所在的ziplist大小超过了限制的其它情况(主要对应于在ziplist中间插入数据的情况),则需要把当前ziplist分裂为两个节点,然后再其中一个节点上插入数据。