我们之前介绍了Redis的各种基本数据类型.
比如SDS字符串,链表,压缩链表,字典,跳跃表,整数集合
等.
但是Redis并不是直接使用它们来构建一个数据库的,而是又包装了一层,使用它们构建了对象系统,然后使用这些对象系统来建立数据库系统.
Redis中主要的对象类型有:
字符串对象
列表对象
哈希对象
集合对象
有序集合对象
相对于上一篇文章写的那些基础数据类型,这些对象类型是不是看起来更亲近了?这些是我们在使用中能够直观感受的数据类型.
文中有编码方式与基础数据类型俩种说法,要注意一个编码方式可能不止使用一个基本数据类型来实现,就比如有序集合对象的skiplist
编码方式就使用了dict
和zskiplist
俩种基本数据类型来实现
并且Redis在这个对象系统的基础之上还实现了一些额外的功能:
类型检查和命令多态
: 一个对象类型在不同的使用场景下可以使用不同的编码方式,十分灵活,而这对于客户端来说是透明的,无感知的.当执行命令之前会.并且对于客户端的相同命令,服务端可能会因为其对象的使用的编码方式的不同调用不同的函数.内存自动回收
: Redis实现了基于引用计数的对象回收系统,当服务端内一个对象没有人使用之后,会进行内存释放,避免空间的浪费.共享对象
: 我们上面说了有引用计数,那么同样,在某些情况下,Redis中的多个键可以共享一个对象来节约内存记录访问时间
: Redis会对对象的上一次的访问时间进行记录,如果一个对象长时间没有被使用,如果服务端启用了maxmemory
,在回收的时候会优先回收这些键.
我们接下来就来介绍一下Redis的对象系统.
对象的类型与编码
我们前面说过,Redis有不同的对象类型,而同一个对象类型在不同的使用场景下能够使用不同的编码方式.
这个时候我们可以来看一下Redis对象的定义:
typedef struct redisObject{
//类型,对应我们客户端能够直观感受到的对象类型
unsigned type:4;
//编码,对应对象所使用的编码方式
unsigned encoding:4;
//指向基本数据类型的对象的指针
void* ptr;
//引用计数
int refCount;
//最后一次被访问的时间
unsigned lru:22
}
//属性名:整数 (意思是该字段占用的bit位数,该数要小于该字段数据类型占用的位数,就比如int amount:n 这个n的值就需要小于32)
type属性,即一个对象的类型的取值有:
类型常量 | 描述 |
---|---|
REDIS_STRING | 字符串对象 |
REDIS_LIST | 列表对象 |
REDIS_HASH | 哈希对象 |
REDIS_SET | 集合对象 |
REDIS_ZSET | 有序集合对象 |
encoding属性,即一个对象采用的编码方式有:
编码常量 | 描述 |
---|---|
REDIS_ENCODING_INT | long类型的整数 |
REDIS_ENCODING_EMBSTR | embstr编码的简单动态字符串 |
REDIS_ENCODING_RAW | 简单动态字符串 |
REDIS_ENCODING_HT | 字典 |
REDIS_ENCODING_LINKEDLIST | 链表 |
REDIS_ENCODING_ZIPLIST | 压缩链表 |
REDIS_ENCODING_INTSET | 整数集合 |
REDIS_ENCODING_SKIPLIST | 跳跃表 |
一个对象类型至少拥有俩个基础编码方式的实现,其对应关系有:
那么我们如果想要知道某个键的值对应的数据类型该如何获取呢?
- 可以使用
OBJECT ENCODING key
命令来获取.
对象类型与对应的编码
我们前面说过一个对象类型至少有俩个编码方式的实现,在不同场景下会使用不同的编码方式.
那么在什么场景下会使用什么样的编码方式呢?在何种情况下对象会进行编码方式的切换呢?这就是我们接下来要讨论的问题
字符串对象
字符串对象的实现有三种: int,embrstr,raw
当存入的字符串值是整数,并且可以使用long来表示,字符串对象就会使用int
如果存入的字符串值长度大于32
,并且可以用一个字符串来表示,那么就会使用raw
来存储
如果存入的字符串值长度小于32
,并且可以用一个字符串来表示,那么就会使用embstr
类型来进行存储.
那么这就带来了一个问题,embstr之前我们介绍基本数据类型的时候并没有说过啊?这又是哪来的?
embstr
和raw
同样都采用redisObject结构和sdshdr结构来作为字符串的实现,而embstr是专门对于存储短字符串进行的优化,区别就在于:
当创建存储embstr的对象时,服务端只会调用一次内存分配函数来分配一个连续的内存空间,也就是redisObject结构体对象与sdshdr结构体对象在内存中相邻,这样我们在回收它的内存空间的时候也只需要调用一次内存释放函数
也因为embstr在内存中的位置是一片连续的内存空间,也就更好能利用缓存的优势.
而raw会调用俩次内存分配函数来分别分配空间,回收空间的时候也会调用俩次内存释放函数.
在Redis中的浮点数实际上也是采用embstr和raw来存储的,程序会先将浮点数转换成字符串再存入数据库,然后再取出使用的时候再将字符串转换成浮点数.
那么在何种情况下,对象的编码方式会进行转换呢?
- 如果存储的值可以用long来表示,则会使用int编码方式来存储,如果对该value更新为整数无法表示(需要使用字符串来表示)的操作,又或是只有对字符串才能做的操作,比如append,就会先将该值转换成raw类型,然后再进行操作.
如果embstr编码方式的值被更改,那么就会创建一个raw编码方式的对象,再进行操作.(因为实际上embstr的函数库是没有编写任何对字符串的修改操作的)
列表对象
列表对象的编码方式有ziplist,linkedlist
前面在介绍压缩链表ziplist的时候可能有人会想:压缩链表的结构不是也挺复杂的,为什么说它能节省了空间呢?
- 因为之前在讲解linkedlist链表的时候,只说了listNode结构体使用value属性来存储节点的值,并没有说节点是如何存储的,但是实际上,像是一个存储字符串列表的linkedlist链表,其节点的value是指向一个字符串的redisObject的:
我们可以想象一下:最外层redisObject的ptr指针指向这个adlist.h/list链表,这个链表中又有多个listNode链表节点,每一个节点中又包括一个redisObject,每一个redisObject又指向一个int或是embstr又或是raw字符串,在这其中每一层结构体都有自己的属性需要存储,这样花费空间是不是就多了很多? - 但如果是使用ziplist,其结构就会是这样:
看起来是不是比linkedlist结构节省许多空间了.
那么在何种情况下,对象的编码方式会进行转换呢?
- 当列表元素存储的字符串长度都小于64字节,并且列表的元素数量小于512,就会采用ziplist,否则采用linkedlist,当然这些数值都是可以进行配置更改的.
哈希对象
哈希对象的编码方式有ziplist,hashtable
当使用ziplist
作为实现的时候,哈希的键与值紧紧的挨在一起,当要对该哈希对象添加键值对时,会先将该键值对的键添加到队尾,然后再将该键值对的值也给添加到队尾.
我们可以想像到,当哈希对象的编码方式为ziplist时,其redisObject的ptr指针指向的对象会是这样:
当使用hashtable
作为实现的时,使用dict字典来作为哈希对象的实现.
这就和我们印象里的哈希对象使用一样了,当要对hashtable编码方式的哈希对象进行添加键值对时,会将该键值对的键的字符串对象作为键,该键值对的值的字符串对象作为键来存入到dict之中:
那么在何种情况下,对象的编码方式会进行转换呢?
- 当哈希对象存储的键和值所对应的字符串长度都小于64字节,并且哈希对象中的元素数量小于512,就会采用ziplist,否则采用hashtable,当然这些数值都是可以进行配置更改的.
集合对象
集合对象的编码方式有intset,hashtable
当使用intset
作为集合对象的编码方式时,集合对象中的整数都会被存放到整数集合中,就像这样:
当使用hashtable
作为集合对象的编码方式时,会将集合中的元素作为hashtable中的键来存储,而其键对应的值则设置为NULL:
那么在何种情况下,对象的编码方式会进行转换呢?
- 当集合对象中的元素都是整数并且集合中的元素个数小于512的时候就会采用
intset
,否则采用hashtable
有序集合
有序集合的编码方式有ziplist,skiplist
当使用ziplist
作为有序集合对象的编码方式的时候,程序会使用俩个紧挨着的位置存放有序集合的member(成员)和score(分值).当要在有序集合中添加一个新的成员时,会根据这个成员的分值的大小寻找到对应的位置,使用俩个位置来存放这个成员与其分值.
当使用skiplist
作为有序集合的编码方式的时候,这个编码方式的实现使用zset结构体
:
typeof struct zset{
zskiplist *zsl;
dict *dict;
}
我们可以看到zset结构体是由一个跳跃表和一个字典组成的
其中zsl中存放了以分值排序从小到大的成员.skiplist编码方式使用zskiplist的函数来实现ZRANGE,ZRANK等命令
而dict中存放了键为成员,值为分值的键值对.这样ZSCORE命令根据某个成员查询对应的分值就可以只使用O(1)的时间复杂度了.
看着是不是觉得使用俩个结构体更加复杂了?会不会更浪费内存空间?
但其实zskiplist和dict中指向成员和分值的指针都是指向相同的对象,并不会重复创建对象,只额外消耗了存储指针的内存空间,所以问题不大.
并且通过使用使用zskiplist能够便捷的实现ZRANGE,ZRANK等命令,不需要再花费至少O(NlogN)的时间复杂度和额外的内存空间O(N)来暂时存放排序后的成员,而使用dict又可以让获取成员分值的时间复杂度降到O(1).我们可以肯定的说这是利大于弊的.
那么在何种情况下,对象的编码方式会进行转换呢?
- 当有序集合对象中的成员长度都小于64字节并且序集合对象中的元素个数小于128的时候就会采用
ziplist
,否则采用skiplist
类型检查和命令多态
类型检查
:Redis有许多命令键对应的值对象类型是有要求的,比如APPEND,SET,STRLEN
这些命令就要求值对象的类型是字符串.Redis在执行这些有值对象类型
限制的命令时,会先对对应键的值对象type所记录的类型进行检查,如果不符合则向客户端抛出错误.
这是通过redisObject结构体的type字段来实现的.
命令多态
:为了实现在不同场景下可以使用不同的编码方式来进行优化,在Redis中对象的类型会有不同编码方式的实现,一个相同的命令,如果对象的编码方式不同,可能会选择使用不同的函数来执行.
比如LLEN
命令,获取列表的长度,如果该值对象的编码方式是ziplist,则会返回压缩列表的长度,如果是linkedlist,则会返回链表的节点数量,这也就是多态
,会根据实际编码方式选择不同的实现.
内存回收.
内存回收
: 我们知道c和c++都是不具备内存回收功能的语言,需要用户来手动进行回收内存空间,但是Redis实现了内存回收.
Redis中基于引用计数方式实现了内存回收,这是通过在redisObject结构体中refCount属性来记录该对象的引用次数来实现的,程序根据一个对象的refCount属性来判断这个对象所占用的内存空间是否应该被回收.
当一个对象刚创建的时候,这个对象的refCount字段的属性为1,每当有一个新的程序引用它的时候,它的refCount字段就会增加1
与此相反,每当有一个程序不再引用它的时候,他的refCount就会减1
当这个对象的refCount为0的时候,redis就会去回收这个对象的内存空间
我们可以通过OBJECT REFCOUNT key
命令来查看该值对象被引用的次数
对象共享
我们上面也说了,Redis通过引用计数来进行内存回收,这也就说明一个对象是可以被多个程序来引用的,这样服务端就不需要去重复的创建一个相同的对象,也就能够节省空间.
Redis在启动的时候就会初始化0~9999的字符串对象,用于服务端共享.
要注意的是Redis只对整数值做对象共享优化,这主要是因为要进行共享前,我们肯定要判断新值是否存在于共享对象池中,如果是一个整数字符串对象,比较俩个对象是否相同就只需要O(1)的时间复杂度,但是是字符串就会是O(N),如果是一个列表的对象,就会是O(N²),如果要进行整数字符串以外的对象共享对于CPU时间是不友好的.
记录对象的空转时间
有时候我们进行Redis内存回收的时候,希望那些最近没有用到的对象占用的内存空间优先被回收.
我们就可以通过redisObject中记录的lru
字段来判断哪些对象的空转时间最长,来优先回收这些对象.
我们可以使用OBJECT IDLETIME key
命令来查看这个值对象的空转时间.
标签:对象,Redis,系统,内存,使用,字符串,编码方式 From: https://www.cnblogs.com/youjunhui/p/18065666服务器需要打开maxmemory选项,并且设置内存回收策略为
volatile-lru或是allkeys-lru
,在占用内存超过maxmemory值之后,就会优先回收这些空转时间长的对象了.