一、简单动态字符串SDS
无论是Redis中的key还是value,其基础数据类型都是字符串。如,Hash型value的field与value的类型,List型,Set型,ZSet型value的元素的类型等都是字符串。redis没有使用传统C中的字符串而是自定义了一种字符串结构类型,这种字符串本身的结构比较简单,但是功能强大,称为简单动态字符串 Simple Dynamic String ,简称为SDS。
SDS结构
SDS不同于C中的字符串,C中的字符串是以一个双引号括起来,以空字符'\0'结尾的字符序列。而SDS是重新定义的一个结构体,在redis的安装目录src/sds.h
这个结构体有三个部分组成:
struct sdshdr{
// 字节数组,用来保存字符串
char buf[];
// SDS的长度,在buf[]中使用的字节数量
int len;
// buf[]中没有使用的字节数量
int free;
}
注意:虽然redis当中定义了结构体sds,但是并不是所有的字符串都使用这种方式存储,比如我们执行一个set命令,返回了一个OK,这个OK就是C语言中的字符串。get命令获取的值返回的也是C语言的字符串。C 字符串只会出现在字符串“字面常量”中,并且该字符串不可能发生变更!!
我们看下面的命令我们来了解一下类型与实际内存中的存储结构的区别:
set name zhangsan # 写入key为name的值为zhangsan,这里写入的是zhangsan应该是SDS结构类型的
type name # 这里返回的会是string 表示是字符串
object encoding name # 这里会返回embstr,它就表示是SDS,是内存中的存储类型
set age 23 # 写入key为age的值为23
type age # 这里返回的是string,表示是字符串
object encoding age # 这里返回的是int,表示在内存中是以整型存储的
SDS优势
C字符串底层是一个数组,字符串最后以’\0‘来结束。
优势一:防止字符串长度获取性能瓶颈
C字符串的长度获取必须要通过遍历整个字符串(直到\0结束)才可以获得,这样的话对于超长的字符串遍历,还是会产生性能瓶颈的。
SDS结构体中我们可以看到直接存放着字符串长度的数据,所以不管字符串有多长,获取字符串的长度所要消耗的系统性能都是一样的,不会成为redis的性能瓶颈。
优势二:保障二进制安全
C字符串对于字符串中的字符是有要求的,遇到\0就表示字符串结束了,但是我们在redis中会用字符串存储二进制数据(图片,视频,压缩文件...),在这些文件中使用\0在中间作为分隔符的情况是很常见的。所以redis中的SDS不以\0作为字符串结束标志,而是通过len属性来判断字符串是否结束,所以对于程序处理SDS中的字符串数据,不需要对数据做任何的限制、过滤等处理,直接读取即可。写入的是什么读出来的就是什么。
优势三:减少内存再分配
SDS中采用了空间预分配策略与惰性空间释放策略来避免内存再分配的问题。
这两个策略就是以空间换时间的做法!
如果我们要释放SDS的未使用空间,可以通过sdsRemoveFreeSpace()函数来释放。
优势四:兼容C函数
redis中提供了很多的SDS的api,方便用户对redis进行二次开发,为了兼容C函数,SDS的底层数组buf[]中的字符串仍以空字符'\0'结尾。
SDS常用操作函数
- 空间预分配策略,每次SDS进行空间扩展的时候,程序不但为其分配所需的空间,还会为其分配额外的未使用空间,从而减少内存再分配次数,客户分配的未使用空间大小取决于空间扩展后SDS的len属性值。
- 如果len属性值小于1M,那么会分配未使用空间保持与len属性相当
- 如果len属性值大于等于1M,那么会固定分配未使用空间1M
-
惰性空间释放策略,如果SDS字符串长度如果缩短,那么多出的未使用空间会暂时不释放,而是增加到free当中去,从而保证后期扩展SDS时减少再分配的次数
函数 | 功能 |
---|---|
sdsnew() | 使用指定的C字符串创建一个SDS |
sdsempty() | 创建一个不包含任何字符串数据的SDS |
sdsdup() | 创建一个指定SDS的副本 |
sdsfree() | 释放是定的SDS |
sdsclear() | 清空指定SDS的字符串内容 |
sdslen() | 获取指定SDS的已使用空间len值 |
sdsavail() | 获取指定SDS的未使用空间free值 |
sdsMakeRoomFor() | 使指定的SDS的free空间增加指定的大小 |
sdsRemoveFreeSpace() | 释放指定SDS的free空间 |
sdscat() | 把指定的C字符串拼接到指定SDS的字符串末尾 |
sdscatsds() | 把指定的SDS字符串拼接到另一个指定的SDS字符串末尾 |
sdscpy() | 把指定C字符串复制到指定的SDS中,覆盖原SDS字符串内容 |
sdsgrouzero() | 扩展SDS字符串到指定长度,这个扩展使用空字符'\0'填充 |
sdsrange() | 截取指定SDS中指定范围内的字符串 |
sdstrim() | 在指定SDS中删除报有指定C字符串中出现的所有字符 |
sdsemp() | 对比给定的两个SDS字符串是否相同 |
sdstolow() | 把指定SDS字符串中的所有字母变为小写 |
sdstoupper() | 把指定SDS字符串中所有字母变为大写 |
这些函数在需要对redis二次开发时可能会用到,正常使用redis时用不到!
集合底层实现原理
Set集合
对于set,我们看它底层的实现可以如下:
sadd cities bj sh gz sz # 添加一个set集合
type cities # 返回的类型是set
object encoding cities # 返回的是hashtable
从上面可以看到set底层是使用hashtable存储的!
Hash与ZSet
这两种它们的底层实际上有两种:压缩列表zipList;跳跃列表skipList
至于Redis什么时候用zipList,什么时候用skipList对于我们用户来说是透明的,用户写入不同的数据,系统会自动判断使用不同的实现。
只要我们的数据量不超过我们设定的阈值则会使用zipList,一旦超过这个阈值则什么变为skipList。
关于这个阈值的查看,可以使用命令进行查看:
config get zset-*-ziplist-*
# 运行后得到的结果类似如下:
1) "zset-max-ziplist-entries"
2) "128"
3) "zset-max-ziplist-value"
4) "64"
上面的信息表示,zset中包含的元素不超过128,并且每一个元素的大小不超过64字节就会使用ziplist。这两个任意一个不满足就会变为skipList。
同样hash的查询方式也类似,其查询命令如下:
config get hash-*-ziplist-*
# 运行这个命令后的结果如下:
1) "hash-max-ziplist-value"
2) "64"
3) "hash-max-ziplist-entries"
4) "512"
关于zipList的底层结构
首先,什么是zipList?
通常称为压缩列表,是一个经过特殊编码(体现了zip压缩的)的用于存储字符串或整数的双向链表。其底层数据结构由三部分构成:head(基础信息)、entries(元素)、end(结束标记),这三部分在内存上是连续存放的。
zipList的三部分在底层如何存储?
说明 | |
---|---|
head | 它包含总括性信息,由三部分组成 |