Redis设计原理之五种对象(二)

版权声明:本文为博主原创文章,转载需注明出处. https://blog.csdn.net/piaoslowly/article/details/83339798

Redis设计原理之五种对象(二)

第一章介绍了redis的8种数据结构,但是redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统.字符串对象,列表对象,哈希对象,集合对象和有序集合对象.每种对象都至少用到了一种其中的数据结构.

对象

Redis使用对象来表示数据库中的键和值,我们每次在redis中创建一个键值对,就至少会包含两个对象,一个键对象,一个值对象.
redis中的每个对象都由一个redisObject结构表示.

typedef struct redisObject{

   //类型REDIS_STRING字符串对象,_LIST对象,等其他对象
	unsigned type:4;
	
	//编码
	unsigned encoding:4;
	//指向底层实现的数据结构指针
	void *ptr;
	//引用计数
	int refcount;
	
	//最后一次被命令程序访问的时间
	unsigned lru:22;
}robj

type:显示出对象的类型 > type key1 可以显示出key1的对象类型.
encoding:显示出对象底层的数据结构 > object encoding key1 可以显示出key1的对象的底层
refcount: 用于内存回收

字符串对象

字符串对象的编码可以是:int,raw 或者embstr.

int编码

如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,字符串对象的encoding=int,ptr属性里面void* 转换为long.

redis> set key1 1332

redis> object encoding key1
"int"

raw与embstr编码

如果一个字符串对象保存的是一个字符串值,并且这个字符串值的长度小于32字节,那么字符串对象将使用embstr编码的方式来保存这个字符串值.

raw与embstr编码的区别:
embstr编码是用于保存短字符串的一种优化编码,这种编码和raw编码一个.区别就是raw编码会调用两次内存分配函数来分别创建redisObject结构和sdshdr结构,而embstr编码则通过一次调用内存分配函数分配一块连续的空间,空间中一次包含redisObject和sdshdr两个结构.

注意:

  • long,double类型表示的浮点数是通过字符串值来保存的.
  • long类型保存的整数则通过int来保存.

编码转换

embstr字符串一个只读字符串,一个embstr字符串一旦被修改时,则会先转换为raw类型,然后再修改,之后也都一直是raw字符串.

列表对象

列表对象的编码可以是ziplist或者linkedlist.

redis> rpush number 1 "three" 5

压缩列表条件,以下两个条件必须同时满足:

  • 列表对象保存的所有字符串元素的长度都小于64字节;
  • 列表对象元素的数量小于512;

这两个参数是可以修改的:list-max-ziplist-value和list-max-ziplist-entries

编码转换

当两个添加其中之一不能满足时,则会发生编码转换,将ziplist转换为linkedlist

哈希对象

哈希列表的编码可以是ziplist或者hashtable.

ziplist编码的哈希对象使用列表做为底层实现,每当有新的键值对要加入到哈希对象时,程序会先将键的压缩列表节点推入到压缩列表表尾,然后再将保存了值的压缩列表节点推入到压缩列表表尾.

redis> hset key1 name "Tom"
redis> hset key1 age 25
redis> hset key1 career "programmer"


hashtable编码的哈希对象使用字典作为底层实现,哈希对象中的每个键和值都是一个字符串对象.

压缩字典条件,两个条件必须同时满足

  • 哈希对象保存的键值对字符串长度都小于64字节;
  • 哈希对象保存的键值对总数小于512个.

这两个参数是可以修改的:hash-max-ziplist-value和hash-max-ziplist-entries

编码转换

在哈希对象不满足压缩字典条件时,编码将改为hashtable编码.

集合对象

集合对象的编码可以是intset或者hashtable.

intset编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合里面.
hashtable编码的结合使用字典作为底层实现,字典的key就是集合的元素,而字典的value则为null.

redis> sadd number 1 3 5

redis> sadd key1 "apple" "banana" "cherry"

intset集合条件,必须两个同时满足

  • 集合对象保存的都是整数值
  • 集合对象保存的元素数量不超过512个.

这两个参数是可以修改的:set-max-ziplist-value和set-max-ziplist-entries

编码转换

在集合对象不满足intset条件时,编码将改为hashtable编码.

有序集合对象

有序集合的编码可以是ziplist或者skiplist(跳跃表+字典)

ziplist编码的压缩列表对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素成员,第二个节点保存元素节点的分值. 压缩列表内的集合元素按分值从小到大进行排序,分值较小的元素放在表头,较大的放在表尾.

skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表.

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

为什么有序集合需要同时使用跳跃表加字典来实现呢?
有序集合可以单独使用一个字典或者跳跃表来实现,但是单独使用其中一种性能上都会有所降低.
单独使用字典实现的有序集合,虽然查询复杂度为O(1),但是其他zrank,zrange等集合命令操作,字典需要自己先排序然后在筛选,那么复杂度就变成O(NlogN)时间复杂度+O(N)内存空间(用来保存排序后的数组元素)
如果单独使用有序集合,查询复杂度则会从O(1)上升为O(logN).所以有序集合干脆用两种数据结构来保存元素.

图中展示了一个“banana”,分值为5.0.即在跳跃表中保存了一份,又在字典中保存了一份.看似保存一个值需要存储两份的空间,但是在实际中“banana”只有一个,只不过跳跃表中的指针指向了“banana”地址,而字典中的指针也同样指向了同一个“banana”地址.所以实际还是占用一份的空间大小,只不过多了一层结构而已占用空间较小.

ziplist编码的有序集合条件,必须两个同时满足

  • 有序集合对象保存的元素的长度都小于64字节.
  • 有序集合对象保存的元素数量不超过128个.

这两个参数是可以修改的:zset-max-ziplist-value和zset-max-ziplist-entries

编码转换

在有序集合对象不满足ziplist条件时,编码将改为skiplist.

命令检查

redis > set msg 'hello work'
ok

redis> get msg
"hello work"

redis> llen msg
(error) wrongtype operation against a key holding the wrong kind of value

上面key=“msg” 它的value值的类型也是字符串.但是最下面的llen msg 却用list的命令去操作key,所以就返回了一个类型操作错误的信息.
redis在llen使用获取值的时候,服务器会先检查数据库键的值对象是否为列表类型,即检查值对象的redisObject结构的type属性,如果是的再执行llen命令,如果不是则返回一个类型错误.

很容易理解,把get msg当成两个参数就完了.(get,“msg”)传递给redis时,redis先根据“msg”做hash,然后直接找到key.getValue()的值.因为redis整个数据库是key,value,查找key的复杂度为O(1)所以很快匹配到“msg”,获取到它的值.根据值对象的type字段,判断是否是get命令,如果是则get命令,如果不是则返回类型错误.所以上面的(llen,“msg)类型匹配错误了,无法执行llen命令,所以返回错误结果.

内存回收

因为c语言并不具备自动内存回收功能,所以Redis在自己的对象系统中构建一个引用计数技术实现的内存回收机制,程序可以通过跟踪对象的引用计数信息,在适当的时候自动释放对象并进行内存回收.

  • 在创建一个新对象时:refcount=1;
  • 当被一个新程序使用时+1;
  • 不在被一个新使用时-1;
  • 当对象引用计数值变为0时,对象所占用的内存会被释放

对象共享

当键A创建了一个包含整数值为100的字符串对象作为值对象时,键B也要创建一个同样100字符串对象,那么A和B的值将共享这个100字符串对象.

redis中,让多个键共享同一个值对象需要执行两个步骤:

  • 将数据库键的值指针指向一个现有的值对象.
  • 将被共享的值对象的引用计数增1

目前来说,Redis会在初始化服务器时,创建一万个字符串对象,这些对象包含了从0到9999的所有整数值.当服务器向使用0到9999的字符串对象时,服务器直接共享这些对象,而不是新键对象.
共享对象只会包含整数值的字符串对象,而不包含字符串值的字符串对象.(字符串值的字符串对象在判断是否是同一个对象时复杂度为O(N),整数值为O(1),所以字符串值的字符串对象共享起来得不偿失).

对象的空转时长

在redisObject包含了一个lru字段,存储对象最后一次被命令程序访问的时间.

redis > object idletime key1
(integer)20

如果服务器打开了maxmemory选项,并且服务器用于回收内存的算法为volatile-lru或者allkeys-lru,那么当服务器占用的内存数超过maxmemory选项所设置的上限值时,空转时间较长的那部分键会优先被服务器释放,从而回收内存.

猜你喜欢

转载自blog.csdn.net/piaoslowly/article/details/83339798