Redis数据结构与对象(三)


前言

前文:
Redis数据结构与对象(一)
Redis数据结构与对象(二)

学习《Redis设计与实现》过程中摘要的笔记(三),本文主要覆盖内容:对象


Redis对象系统包含字符串对象,列表对象,哈希对象,集合对象和有序集合对象五种类型的对象,每种对象都用到了至少一种前文中介绍的数据结构;且其还实现了基于引用计数器的内存回收机制:,当程序不再使用某个对象的时候,这个对象的内存则会被自动释放。

一. 对象的类型和编码

Redis使用对象来表示数据库中的键和值,当新创建一个键值对的时候,Redis至少会创建两个对象,即一个键对象和一个值对象。例如创建以下键值对对象时,则包含键对象“msg”和字符串值对象“hello world”:

redis>SET msg "hello world"
OK

Redis对象的数据结构如下,与保存数据有关的三个属性分别是type,encoding和ptr属性:

typedef struct redisObject{
	// 类型
	unsigned type:4;
    
	// 编码
	unsigned encoding:4;
    
	// 指向底层数据结构的指针
	void *ptr;
    
	// 引用计数
	int refcount;
    
	// 记录最后一次被程序访问的时间
	unsigned lru:22;
 
}robj;

1. 对象类型(type属性)

type属性记录了对象的类型,对于Redis数据库来说,键总是一个字符串对象,而值则可以是字符串对象,列表对象,哈希对象,集合对象或者有序集合对象中的一种。

类型常量对象的名称TYPE命令的输出
REDIS_STRING字符串对象“string”
REDIS_LIST列表对象“list”
REDIS_HASH哈希对象“hash”
REDIS_SET集合对象“set”
REDIS_ZSET有序集合对象“zset”

2. 编码和底层实现

ptr属性指向对象的底层实现数据结构,而具体的数据结构则由encoding属性决定,每种类型的对象都至少使用了两种不同的编码,如下表:

类型编码对象
REDIS_STRINGREDIS_ENCODING_INT使用整数值实现的字符串对象
REDIS_STRINGREDIS_ENCODING_EMBSTR使用embstr编码的简单动态字符串实现的字符串对象
REDIS_STRINGREDIS_ENCODING_RAW使用简单动态字符串实现的字符串对象
REDIS_LISTREDIS_ENCODING_ZIPLIST使用压缩列表实现的列表对象
REDIS_LISTREDIS_ENCODING_LINKEDLIST使用双端链表实现的列表对象
REDIS_HASHREDIS_ENCODING_ZIPLIST使用压缩列表实现的哈希对象
REDIS_HASHREDIS_ENCODING_HT使用字典实现的哈希对象
REDIS_SETREDIS_ENCODING_INTSET使用整数集合实现的集合对象
REDIS_SETREDIS_ENCODING_HT使用字典实现的集合对象
REDIS_ZSETREDIS_ENCODING_ZIPLIST使用压缩列表实现的有序集合对象
REDIS_ZSETREDIS_ENCODING_SKIPLIST使用跳跃表和字典实现的有序集合对象

利用encoding属性来设定对象所使用的编码方式,而不是为特定类型的对象关联一种固定的编码,使得Redis可以根据不同的使用场景来为一个对象设置不同的编码方式,提高灵活性和效率。

二. Redis的五种对象

1. 字符串对象

字符串对象的编码包括int,raw和embstr,其中浮点数使用raw或者embstr来保存。

  • int类型:如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面(将void*转换成long),并将字符串对象的编码设置为int。

  • raw类型:如果字符串对象保存的是一个字符串值,并且这个字符串值的长度大于39字节,那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值,并将对象的编码设置为raw。
    在这里插入图片描述

  • embstr类型:如果字符串对象保存的是一个字符串值,并且这个字符串值的长度小于等于39字节,那么字符串对象将使用embstr编码的方式来保存这个字符串值。
    在这里插入图片描述

raw类型和embstr类型都是基于SDS实现的,但两者具有一定的差异:

  • embstr编码创建的字符串只需要调用一次内存的分配和释放函数,raw需要两次(分别创建和释放redisObject和sdshdr内存)
  • 因为embstr编码的字符串对象的所有数据都保存在一块连续的内存里面,所以这种编码的字符串对象比起raw编码的字符串对象能够更好地利用缓存带来的优势。

编码的转换

int编码的字符串对象和embstr编码的字符串对象会在条件满足的情况下,转换为raw编码的字符串对象。

  • 对于int编码,如果通过APPENND命令追加一个字符串值,则Redis会先将之前保存的整数值转换成字符串值,再执行追加操作,最终的结果就是raw编码的字符串对象。
127.0.0.1:6379> SET number 10086
OK
127.0.0.1:6379> OBJECT ENCODING number
"int"
127.0.0.1:6379> APPEND number " is a good number!"
(integer) 23
127.0.0.1:6379> GET number
"10086 is a good number!"
127.0.0.1:6379> OBJECT ENCODING number
"raw"
  • 对于embstr编码的字符串对象,由于其不具备修改操作(创建后无法改动对象保存的值)。因此当需要对其进行任何修改命令时,则Redis会首先将其转换成raw,然后再执行修改命令,最终的结果就是raw编码的字符串对象。
127.0.0.1:6379> SET msg "hello world"
OK
127.0.0.1:6379> OBJECT ENCODING msg
"embstr"
127.0.0.1:6379> APPEND msg " again!"
(integer) 18
127.0.0.1:6379> OBJECT ENCODING msg
"raw"

2. 列表对象

列表对象的编码可以是zipliist或者linkedlist。

  • ziplist编码
    ziplist编码的列表对象使用压缩列表作为底层实现,每个压缩列表节点保存了一个列表元素。
127.0.0.1:6379> RPUSH numbers 1 "three" 5
(integer) 3

在这里插入图片描述

  • linkedlist编码
    linkedlist编码的列表对象使用双端链表作为底层实现,每个双端链表节点都保存了一个字符串对象,而每个字符串对象都保存了一个列表元素。
    在这里插入图片描述
    linklist编码的列表对象再底层的双端列表中包含了多个字符串对象,这是因为字符串对象是Redis五种类型的对象中唯一一个会被其他四种对象嵌套的对象。

编码的转换

当列表对象可以同时满足以下两个条件时,列表对象使用ziplist编码:

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

不能满足这两个条件的列表对象需要使用linkedlist编码。
以上两个条件的上限值都是可以修改的,参考list-max-ziplist-value和list-max-ziplist-entries选项配置说明。

3. 哈希对象

哈希对象的编码可以是ziplist或者hashtable。

  • ziplist编码
    ziplist编码的哈希对象使用压缩列表作为底层实现,每当有新的键值对要加入到哈希对象时,程序会先将保存了键的压缩列表节点推入到压缩列表表尾,然后再将保存了值的压缩列表节点推入到压缩列表表尾。
    在这里插入图片描述
    其具有以下特点:
    A. 保存了同一键值对的两个节点总是紧挨在一起,保存键的节点在前,保存值的节点在后。
    B. 先添加到哈希对象中的键值对会被放在压缩列表的表头方向,而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向。

在这里插入图片描述

  • hashtable编码
    hashtable编码的哈希对象使用字典作为底层实现,哈希对象中的每个键值对都使用一个字典键值对来保存,字典的每个键和值都是一个字符串对象,用于保存具体的键和值。
    在这里插入图片描述

编码的转换

当哈希对象可以同时满足以下两个条件时,哈希对象使用ziplist编码:

  • 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节。
  • 哈希对象保存的键值对数量小于512个。

不能满足这两个条件的哈希对象需要使用hashtable编码。
以上两个条件的上限值都是可以修改的,参考hash-max-ziplist-value和hash-max-ziplist-entries选项配置说明。

4. 集合对象

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

  • intset编码
    intset编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合里面。
    在这里插入图片描述
  • hashtable编码
    hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而字典的值则全部被设置为NULL。
    在这里插入图片描述

编码的转换

当集合对象可以同时满足以下两个条件时,对象使用intset编码:

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

不能满足这两个条件的集合对象需要使用hashtable编码。
其中第二个条件的上限值是可以i修改的,参考配置文件中关于set-max-intset-entries选项配置说明。

5. 有序集合对象

有序集合对象的编码可以是ziplist或者skiplist。

  • ziplist编码
    ziplist编码的有序集合对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,而第二个元素则保存元素的分值。其中压缩列表内的集合元素按分值从小到大进行排序,分值较小的元素被放置在靠近表头的位置,而分值较大的元素则被放置在靠近表尾的位置。
    在这里插入图片描述
    在这里插入图片描述
  • skiplist编码
    skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表:
typedef struct zset{
	// 跳跃表
	zskiplist *zsl;
     
	// 字典
	dict *dice;
}zset;

zset结构中的zsl跳跃表按分值从小到大保存了所有集合元素,每个跳跃表节点都保存了一个集合元素:跳跃表节点的object属性保存了元素的成员,而跳跃表节点的score属性则保存了元素的分值。

zset结构中的dict字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值对都保存了一个集合元素:字典的键保存了元素的成员,而字典的值则保存了元素的分值。通过这个字典,程序可以用O(1)复杂度查找给定成员的分值。

有序集合每个元素的成员都是一个字符串对象,而每个元素的分值都是一个double型的浮点数,虽然zset结构同时使用跳跃表和字典来保存有序集合元素,但这两种数据结构都会通过指针来共享相同元素的成员和分值,所以同时使用跳跃表和字典来保存集合对象不会产生任何重复成员或者分值,也不会因此而浪费额外的内存。
在这里插入图片描述
在这里插入图片描述

为什么有序集合需要同时使用跳跃表和字典来实现?
简而言之,为了提高查找和范围操作的效率。
假如单独使用字典,虽然能以O(1) 的时间复杂度查找成员的分值,但是因为字典是以无序的方式来保存集合元素,所以每次进行范围操作的时候都要进行排序,复杂度至少为O(NlogN) ,且还需要额外的O(N) 空间来保存排序后的元素;
假如单独使用跳跃表来实现,虽然能执行范围操作,但是查找操作由O(1) 的复杂度变为了O(logN)

编码的转换

当有序集合对象可以同时满足以下两个条件时,对象使用ziplist编码:

  • 有序集合保存的元素数量小于128个。
  • 有序集合保存的所有元素成员的长度都小于64字节。

不能满足以下两个条件的有序集合对象将使用skiplist编码。
以上两个条件的上限值都是可以修改的,参考zset-max-ziplist-entries和zset-max-ziplist-value选项配置说明。

三. 类型检查与命令多态

Redis中使用操作键的命令基本上可以分为两种类型:一种命令可以对任何类型的键执行,另一种命令只能对特定类型的键执行。

类型检查的实现

为了确保只有指定类型的键可以执行某些特定的命令,在执行一个类型特定的命令之前,Redis会先检查输入键的类型是否正确,然后再决定是否执行给定的命令,类型特定命令所进行的类型检查是通过redisObject结构的type属性来实现的:

  • 在执行一个类型特定命令之前,服务器会先检查输入数据库键的值对象是否为执行命令所需要的类型,如果是的话,服务器就对键执行指定的命令
  • 否则,服务器将拒绝执行命令,并向客户端返回一个类型错误
    在这里插入图片描述

多态命令的实现

Redis除了会根据值对象的类型来判断键是否能够执行指定命令之外,还会根据对象的编码方式,选择正确的命令实现代码来执行命令

如果对一个键执行LLEN命令,那么服务器除了要确保执行命令的是列表建之外,还需要根据键的值对象所使用的编码来选择正确的LLEN命令实现:

  • 如果列表对象的编码为ziplist,那么说明列表对象的实现为压缩列表,程序将使用ziplistLen函数来返回列表的长度
  • 如果列表对象的编码为linkedlist,那么说明列表对象的实现为双端链表,程序将使用listLength函数来返回双端链表的长度
    在这里插入图片描述
    DEL、EXPIRE等命令和LLEN等命令的区别在于,前者是基于类型的多态,即一个命令可以同时用于处理多种不同类型的键,而后者是基于编码的多态,即一个命令可以同时用于处理多种不同编码

四. 内存回收

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

typedef struct redisObject{
	// 引用计数
	int refcount;
    
	// ...
 
}robj;

对象的引用计数信息会随着对象的使用状态而不断变化:

  • 创建一个新对象,属性refcount初始化为1
  • 对象被一个新程序使用,属性refcount加1
  • 对象不再被一个程序使用,属性refcount减1
  • 当对象的引用计数值变为0时,对象所占用的内存就会被释放

五. 对象共享

对象的引用计数属性还带有对象共享的作用,共享的目的是节省内存开销,在Redis中,让多个键共享同一个值对象需要执行以下两个步骤:

  • 将数据库键的值指针指向一个现有的值对象
  • 将被共享的值对象的引用计数增一
    在这里插入图片描述

Redis会在初始化服务器时,创建一万个字符串对象,这些对象包含了从0到9999的所有整数值,当服务器需要用到值0到9999的字符串对象时,服务器就会使用这些共享对象,而不是新创建对象。创建共享字符串对象的数量可以通过修改redis.h/REDIS_SHARED_INTEGEERS常量来操作。

六. 对象的空转时长

redisObject的lru属性记录最后一次被程序访问的时间

typedef struct redisObject{
	// 记录最后一次被程序访问的时间
	unsigned lru:22;
    
	// ...
 
}robj;

如果Redis打开了maxmemory选项,且内存回收算法选择的是volatile-lru或allkeys—lru,那么当Redis内存占用超过maxmemory指定的值时,Redis会优先选择空转时间最长的对象进行释放。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值