Redis-7. 对象

对象

Redis没有直接使用SDS等主要的数据结构来实现数据库,而是根据这些数据结构构建了一个对象系统

这个系统包含5种不同类型的对象:

  • 字符串对象
  • 列表对象
  • 哈希对象
  • 集合对象
  • 有序集合对象

Redis可以在执行命令前根据对象类型判断是否可以执行给定命令,同时可以针对不同使用场景,为对象设置多种不同的数据结构实现,优化对象在不同场景下的使用效率

Redis的对象系统实现了基于引用计数技术的内存回收机制
同样的,Redis也通过饮用计数技术实现了对象共享机制,可以在适当的条件下让多个数据库键共享一个对象来节约内存

Redis的对象带有访问时间记录信息,可以用于计算数据库键的空转市场。在服务器启用了maxmemory功能的情况下,空转时长较大的键可能被服务器优先删除

对象的类型与编码

数据库的键与值都是用对象表示的。每当创建一个键值对时,至少会创建两个对象

Redis的每个对象都由一个redisObject结构表示,其中与保存数据有关的3个属性是typeencodingptr

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

类型

type可以有以下几种类型

类型常量对象名称
REDIS_STRING字符串对象
REDIS_LIST列表对象
REDIS_HASH哈希对象
REDIS_SET集合对象
REDIS_ZSET有序集合对象

对于Redis数据库保存的键值对,键总是一个字符串对象,而值可以是以上五种对象种的任意一种

对一个数据库键执行TYPE命令时,返回的是值对象的类型
不同类型值对象的TYPE命令输出

编码和底层实现

对象的ptr指向对象的底层数据结构,而对象的encoding决定这些数据结构,它记录了对象所使用的编码,其属性可以是以下常量中的一个
对象的编码

每种类型的对象都至少使用了两种不同的编码,以下是每种类型的对象可以使用的编码
不同类型和编码的对象

使用OBJECT ENCODING命令可以查看一个数据库键的值对象的编码
OBJECT ENCODING对不同编码的输出

通过encoding属性来设定对象所使用的编码,而不是为特定类型的对象关联一种固定的编码,使得Redis的灵活性和效率非常高,Redis可以根据不同的场景为一个对象设置不同的编码,从而优化对象的效率

字符串对象

字符串对象的编码可以是intrawembstr

如果一个字符串对象保存的是整数值且该整数值可以用long类型来表示,那么该对象会把该值保存在对象的ptr里面,并将编码设置为int

如果保存的是字符串值且长度大于32字节,那么就会用一个SDS来保存这个字符串值,且将编码设置为raw

如果字符串小于等于32字节,那么字符串对象使用embstr编码来保存该值

embstr是专门用于保存短字符串的一种优化编码方式,和raw一样,使用redisObjectsdshdr来表示字符串对象,但是raw会调用两次内存分配函数来分别创建两个结构,而embstr则是通过一次内存分配函数来分配一块连续的空间,其中依次包含两个结构

使用embstr保存短字符串有以下优点

  • 创建时内存分配次数只需要1次,内存释放时也只需要1次
  • 所有数据都在一块连续的内存中,可以更好地利用缓存带来的优势

浮点数是被当作字符串值来保存在字符串对象里的

编码的转换

int编码的对象在保存的不再是整数值,而是一个字符串值的时候,将被转换成raw编码

embstr编码的字符串对象没有任何相应的修改程序,相当于是只读的,因此在修改embstr编码的对象时,其编码将会转换成raw编码

字符串命令的实现

字符串命令的实现

列表对象

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

ziplist编码使用压缩列表作为底层实现,每个压缩列表节点保存了一个列表元素

linkedlist编码使用双端链表作为底层实现,每个双端链表的节点保存了一个字符串对象,每个字符串对象保存了一个列表元素

编码转换

列表对象在同时满足以下条件时使用ziplist编码:

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

不能同时满足这两个条件的列表对象则使用linkedlist编码
这两个条件的上限值是可以修改的

列表命令的实现

列表命令的实现

哈希对象

编码可以是ziplist或者hashtable

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

  • 同一键值对的两个节点总是紧挨在一起,保存键的节点在前,值的在后
  • 先添加到哈希对象的键值对会在表头,后来的会在表尾

使用hashtable时使用字典作为底层实现,每个键值对都用一个字典键值对来保存,字典的每个键都是一个字符串对象,其保存了键值对的键,而字典的每个值也是一个字符串对象,其保存了键值对的值

编码转换

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

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

不能满足这两个条件的哈希对象需要使用hashtable编码

这两个条件的上限值也可以更改

哈希命令的实现

哈希命令的实现

集合对象

集合对象的编码可以是intset或者hashtable
使用intset编码时采用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合里

使用hashtable编码时采用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含一个集合元素,字典的值则为NULL

编码的转换

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

  • 所有元素都是整数值
  • 元素数量不超过512个
    否则使用hashtable编码

第二个条件的上限值可以更改

集合命令的实现

集合命令的实现方法-1
集合命令的实现方法-2

有序集合对象

有序集合对象的编码可以是ziplist或者skiplist
使用ziplist编码时采用压缩列表作为底层实现,每个元素使用两个紧挨在一起的压缩列表节点来存储,第一个节点保存元素的成员,第二个元素则保存元素的分值。压缩列表内的集合元素按分值从小到大排序

使用skiplist编码时采用zset结构作为底层实现,一个zset同时包含一个字典和一个跳跃表

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

zset中的zsl按分值从小到大保存了所有集合元素,每个跳跃表节点保存了一个集合元素:object属性保存元素的成员,score属性保存分值
通过这个跳跃表,程序可以对有序集合进行范围型操作
通过dict属性,程序可以方便地查找给定成员的分值

有序集合每个元素的成员都是一个字符串对象,每个元素的分值都是一个double类型的浮点数。
此外,zset中同时使用跳跃表和字典来保存有序集合元素,但是二者是通过指针来共享相同元素的成员和分值的,因此不会产生重复成员和分值,也不会因此浪费额外的内存

编码的转换

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

  • 保存的元素数量小于128个
  • 所有元素成员的长度小于64字节
    不能满足则使用skiplist编码

这两个条件的上限值也是可以修改的

有序集合命令的实现

有序集合键的所有命令都是针对哈希对象构建的(因为有序集合键的值为哈希对象)
有序集合命令的实现

类型检查与命令多态

Redis用于操作键的命令可以分为两种类型。

①可以对任何类型的键执行的命令:

  • DEL
  • EXPIRE
  • RENAME
  • TYPE
  • OBJECT
    等等

②只能对特定类型的键执行的命令:

  • SET、GET、APPEND、STRLEN等命令只能对字符串键执行
  • HDEL、HSET、HGET、HLEN等命令只能对哈希键执行
  • RPUSH、LPOP、LINSERT、LLEN等命令只能对列表键执行
  • SADD、SPOP、SINTER、SCARD等命令只能对集合键执行
  • ZADD、ZCARD、ZRANK、ZSCORE等命令只能对有序集合键执行

类型检查的实现

执行一个类型特定的命令之前,Redis会先检查输入键的类型是否正确,然后再决定是否执行给定的命令
类型检查是通过redisObjecttype属性实现的

  • 执行类型特定命令之前,服务器先检查输入数据库键的值对象是否为执行命令所需的类型,是则执行命令
  • 否则拒绝执行,然后返回一个类型错误

多态命令的实现

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

例子:LLEN命令执行的过程
LLEN命令执行的过程
这里是基于编码的多态,诸如DEL等可以对任意类型执行的命令是基于类型的多态

内存回收

Redis在对象系统中构建了一个引用计数技术实现的内存回收机制,通过该机制,程序可以跟踪对象的引用计数信息,并在适当的时候自动释放对象并进行内存回收

每个对象的引用计数信息由redisObject结构的refcount属性记录

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

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

  • 创建新对象时,引用计数的值初始化为1
  • 对象被一个新程序使用时,引用计数+1
  • 对象不再被一个程序使用时,引用计数-1
  • 引用计数为0时,释放对象内存

修改对象引用计数的API:修改引用计数的API

对象的整个生命周期可以划分为三个阶段:

  1. 创建对象
  2. 操作对象
  3. 释放对象

对象共享

对象的引用计数属性还带有对象共享的作用。
Redis中,让多个键共享同一个值对象需要执行以下两个操作:

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

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

这些共享对象不单只有字符串键可以使用,在数据结构中嵌套了字符串对象的对象都可以使用这些共享对象

注意:Redis只共享包含整数值(而非字符串)的对象,这是因为验证两个字符串是否相等的复杂度较高,会带来较大开销,尤其是共享对象是包含了多个值(或对象)的对象

对象的空转时长

redisObject包含的最后一个属性为lru属性,该属性记录了对象最后一次被命令程序访问的时间

typedef struct redisObject {
	// ...
	unsigned lru:22;
	// ...
} robj;

OBJECT IDLETIME命令可以打印给定键的空转时长。这个数值是通过当前时间减去目标的lru时间计算得出的。这个命令不会修改对象的lru属性

如果服务器打开了maxmemory属性,且服务器用于回收内存的算法为volatile-lru或者allkeys-lru,那么当服务器占用超过maxmemory时,服务器会优先释放空转时长较高的对象,从而回收内存

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值