Redis08:redis中的对象与存储形式

对象

redis中所有数据都是以对象的形式存在的,而不是直接使用之前提到过的各种数据结构,使用对象的好处主要有几点:

1、可以很方便的判断对象的类型,进而判断是否可以执行给定的命令。

2、一个类型可以使用不同的底层结构,切换起来很灵活,优化对象在不同场景下的使用效率。

3、基于对象系统,引入了内存回收机制和对象共享机制。

对象的类型和编码

redis中的对象定义如下:

typedef struct redisObject{
	//类型
	unsigned type:4;
	//编码
	unsigned encoding:4;
	//指向底层数据结构的指针
	void *ptr;
	
	...
}

当我们在redis中定义了一个键值对时,我们此时是定义了两个对象,分别是键对象和值对象,在redis中,键对象中是字符串类型的。

type属性记录了对象的类型,如下表所示:

当我们对一个键执行type命令时,实际上返回的是值对象的类型:

type msg

type命令的返回值如下:

encoding属性记录了对象所使用的编码,也就是对象使用的底层数据结构是什么,这个概念要和数据类型区分开来,因为一个类型的对象可能会有多种不同的编码方式。编码方式如下:

不同类型和编码的关系如下,每种类型的对象都至少使用了两种不同的编码:

使用object encoding命令可以查看一个值对象的编码类型:

object encoding msg

该命令的返回值和编码类型的对应关系如下:

字符串对象

字符串对象不仅会单独存在,而且会在其他对象中被引用,字符串对象是redis五种类型的对象中唯一一个会被其他4种类型对象嵌套的对象。

编码方式

字符串对象的编码可以是int、embstr和raw。

当一个字符串对象保存的是整数值,且这个值可以用long类型来表示时,此时ptr就直接指向一个long类型的整数,此时的编码方式就是int。

如果一个字符串对象保存的是一个字符串值,且长度大于32字节,那么字符串对象将使用SDS来保存该值,此时的编码方式就是raw。

如果一个字符串对象保存的是一个字符串值,且长度小于等于32字节,那么字符串对象将使用embstr的方式来保存这个字符串值。embstr编码是一种用于保存短字符串的优化方式,它和SDS一样使用sdshdr类来表示字符串,但是和raw不同的是,embstr将对象和字符串放在一块连续的空间表示,而不是将其分开:

这种方式使得内存分配和释放变得更方便,而且速度更快。

值得注意的是,redis在存储小数时会先将其转换为字符串值,然后再保存,使用时也是先取出字符串,然后将其转换为小数。

编码的转换

对于int编码的字符串对象,如果我们执行了一些命令将其修改为非整数值,那么它就会转换编码为raw。

而redis没有设置对embstr的修改程序,embstr编码的字符串是只读的,当我们对这种编码的字符串做出任何修改命令时,都会导致其编码变为raw,然后再执行对应的修改命令。

字符串命令的实现

字符串命令在不同编码下的实现如下:

列表list对象

编码方式

对于list类型来说,redis可能使用压缩列表或者双端链表来实现,当元素较少时,使用紧凑的压缩列表速度较快且节约内存;当元素较多时,压缩列表的优势逐渐消失,redis转而使用更适合存储大量数据的双端链表来实现list类型。

使用压缩列表表示list时,压缩列表的每个entry表示list的一个元素:

使用链表表示list时,链表的每个节点都表示list的一个元素:

编码的转换

当list对象同时满足以下两个条件时就使用ziplist编码,否则就使用linkedlist:

1、列表对象保存的所有字符串元素的长度都小于64字节

2、列表对象保存的元素数量小于512个

这两个限值可以在配置文件中修改。

列表命令的实现

列表命令在不同编码下的实现如下:

哈希对象

编码方式

hash对象的编码方式有ziplist和hashtable两种。

当hash对象采用压缩列表的形式表示时,插入一个键值对实际上是往压缩列表的表尾方向插入两个entry,保存键的entry在前,保存值的entry在后,具体实现如下图所示:

当hash对象采用hashtable的形式表示时,类似下图:

编码的转换

当哈希对象同时满足以下两个条件时,就会使用ziplist编码,否则使用hashtable:

1、哈希对象保存的键值对的键和值的字符串长度都小于64字节。

2、哈希对象保存的键值对数量小于512个。

这两个限值可以在配置文件中修改。

哈希命令的实现

哈希命令在不同编码下的实现如下:

集合set对象

编码方式

set对象的编码方式有两种:intset和hashtable。

当set对象使用intset表示时,存储set中元素的就是intset的底层数组:

当set对象使用hashtable表示时,字典的每个哈希节点的键就是set中元素的值,哈希节点的值全部置为null:

编码的转换

当同时满足以下两个条件时,set对象使用intset存储,否则就使用hashtable:

1、set中保存的元素都是整数值。

2、set保存的元素数量不超过512个。(这个参数可以在配置文件中调整)

集合命令的实现

集合命令在不同编码下的实现:

有序集合sorted set对象

编码方式

sorted set对象的编码方式有两种,分别是ziplist和skiplist。

当有序集合用ziplist来实现时,压缩列表用两个相邻的entry来表示成员member和分值score,且列表内的结合元素按分值从小到大进行排序,分值较小的被放置到靠近表头的方向:

当有序集合用skiplist来实现时,底层同时包含一个跳表和一个字典:

typedef struct zset{
	zskiplist *zsl;
	dict *dict;
} zset;

跳表中的每个节点都保存了数据,其中节点的object属性保存了成员member,节点的score属性保存了分值,跳表主要用来完成范围型操作。字典的作用是建立成员member到分值score的映射,字典节点的键保存了元素的成员,值保存了元素的分值。

有序集合的skiplist编码之所以同时采取了两种底层表示方法,原因是redis要保持高效的操作,采取两种表示方法可以兼顾两者的优点,跳表适合执行范围型操作,而字典则用来支持根据成员查找分值,两种结构缺一不可,表示方法大致如下:

这里的底层虽然采取了两种结构,但是字典和跳表会共享元素的成员的分值,不会造成内存浪费。

编码的转换

当满足以下两个条件时,有序集合会采用ziplist编码,否则使用skiplist。

1、有序集合保存的元素数量小于128个。

2、有序集合保存的所有元素成员的长度都小于64字节。

有序集合命令的实现

有序集合命令在不同编码下的实现:

类型检查和多态命令

redis中的操作键的基本命令分为两种,第一种是可以对任何键都执行的命令,如del、rename等;第二种是只能对某一特定类型的键执行的命令,如set、get、hset、hget等。

在执行特定命令时需要进行类型检查,类型检查是通过redisObject类的type属性来实现的,如果检查合格则执行,如果不合格则报错。

在执行命令时涉及多态的概念,这里的多态有两个层次,第一个层次是命令可以处理不同类型的键,第二个层次是对于一个类型,命令可以根据编码的不同调整执行方式,类型和编码分别根据redisObject类的type和encoding属性来检查,如llen命令的执行:

内存回收和对象共享

redis建立了一个引用计数机制来完成内存回收。在redisObject类中有一个int属性refcount,它代表了引用当前对象的计数值,当创建一个新对象时,该值被置为1,当该值变为0时,对象所占用的内存会被释放。

redis通过refcount属性来完成对象共享功能,例如,如果同时让键A和键B都指向值为100的字符串对象,那么该字符串对象的refcount就是2,这样只要值一样,redis中只需要保存一份对象内存,只需要改变对象的refcount属性就行了。我们可以用object refcount命令来查看引用计数:

object refcount A

如果我们在检查引用计数之前设置A的值:

set A 100

那么引用计数则会返回2,这是因为除了键A以外,服务器程序也持有了该值对象。

目前redis会在初始化服务器时直接创建了0到9999的1万个字符串对象,当需要用到的时候直接使用这些共享对象,而不需要创建。(初始化创建共享字符串对象的数量可以通过配置文件来修改)

要注意的是redis目前只对字符串类型的对象实行共享操作,一些更复杂的对象因为检查是否相同消耗的时间太长,所以redis放弃了其他类型的对象共享。

对象的空转时长

redisObject有一个unsigned类型的属性lru,该属性记录了对象最后一次被命令程序访问的时间,用object idletime这个命令可以返回某个键的空转时长,空转时长等于当前时间减去键对象的lru的值。(object idletime命令不会刷新lru)

空转时长的重要应用是在redis的内存淘汰策略中被使用,有时空转时间较高的那部分键会优先被服务器释放。

原文地址:https://www.cnblogs.com/yinyunmoyi/p/11522118.html