【redis深层次探索】数据结构和对象【七】-- 对象

一、说明

redis基于简单动态字符串( SDS) 、 双端链表、 字典、 压缩列表、 整数集合等等,这些数据结构创建了一个对象系统, 这个系统包含字符串对象、 列表对象、 哈希对象、 集合对象和有序集合对象这五种类型的对象, 每种对象都用到了至少一种我们前面所介绍的数据结构。

二、redis中的对象类型和编码

Redis使用对象来表示数据库中的键和值, 每次当我们在Redis的数据库中新创建一个键值对时, 我们至少会创建两个对象, 一个对象用作键值对的键( 键对象), 另一个对象用作键值对的值( 值对象)。

Redis中的每个对象都由一个redisObject结构表示, 该结构中和保存数据有关的三个属性分别是type属性、 encoding属性和ptr属性:

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

对于Redis数据库保存的键值对来说, 键总是一个字符串对象, 而值则可以是字符串对象、 列表对象、 哈希对象、 集合对象或者有序集合对象的其中一种;
TPYE:使用type [key]命令可以查看对象类型
在这里插入图片描述
PTR:使用object encoding [key]命令可以查看编码
在这里插入图片描述
每种类型的对象都至少使用了两种不同的编码, 表8-4列出了每种类型的对象可以使用的编码。
在这里插入图片描述
不同编码对应的输出:
在这里插入图片描述
这样做的好处:

  • 1.极大提升redis 的灵活性和效率
  • 2.动态根据数据量选取底层实现

三、对象

3.1、字符串对象

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

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

  • 如果字符串对象保存的是一个字符串值: 并且这个字符串值的长度小于等于32字节, 那么字符串对象将使用embstr编码的方式来保存这个字符串值。
    embstr编码是专门用于保存短字符串的一种优化编码方式, 这种编码和raw编一样, 都使用redisObject结构和sdshdr结构来表示字符串对象, 但raw编码会调用两次内存分配函数来分别创建redisObject结构和sdshdr结构, 而embstr编码则通过调用一次内存分配函数来分配一块连续的空间。
    embstr编码的字符串对象在执行命令时, 产生的效果和raw编码的字符串对象
    执行命令时产生的效果是相同的, 但使用embstr编码的字符串对象来保存短字符串值有以下好处:

    • 1.embstr编码将创建字符串对象所需的内存分配次数从raw编码的两次降低为一次。
    • 2.释放embstr编码的字符串对象只需要调用一次内存释放函数, 而释放raw编码的字符串对象需要调用两次内存释放函数。
    • 3.因为embstr编码的字符串对象的所有数据都保存在一块连续的内存里面, 所以这种编码的字符串对象比起raw编码的字符串对象能够更好地利用缓存带来的优势。
  • 可以用long double类型表示的浮点数在Redis中也是作为字符串值来保存的。 如果我们要保存一个浮点数到字符串对象里面, 那么程序会先将这个浮点数转换成字符串值, 然后再保存转换所得的字符串值。
    -字符串对象保存各类型值的编码方式

3.2、列表对象

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

  • ziplist编码的列表对象使用压缩列表作为底层实现, 每个压缩列表节点( entry) 保存了一个列表元素。
  • 另一方面, linkedlist编码的列表对象使用双端链表作为底层实现, 每个双端链表节点( node) 都保存了一个字符串对象, 而每个字符串对象都保存了一个列表元素。
    编码转换
    当列表对象可以同时满足以下两个条件时, 列表对象使用ziplist编码:
  • 列表对象保存的所有字符串元素的长度都小于64字节;
  • 列表对象保存的元素数量小于512个; 不能满足这两个条件的列表对象需要使用linkedlist编码。
    注意:以上两个条件的上限值是可以修改的, 具体请看配置文件中关于list-maxziplist-value选项和list-max-ziplist-entries选项的说明。

3.3、哈希对象

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

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

  • 保存了同一键值对的两个节点总是紧挨在一起, 保存键的节点在前, 保存值的节点在后;
  • 先添加到哈希对象中的键值对会被放在压缩列表的表头方向, 而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向
    另一方面, hashtable编码的哈希对象使用字典作为底层实现, 哈希对象中的每个键值对都使用一个字典键值对来保存:
  • 字典的每个键都是一个字符串对象, 对象中保存了键值对的键;
  • 字典的每个值都是一个字符串对象, 对象中保存了键值对的值。
    编码转换
    当哈希对象可以同时满足以下两个条件时, 哈希对象使用ziplist编码:
  • 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节;
  • 哈希对象保存的键值对数量小于512个; 不能满足这两个条件的哈希对象需要使用hashtable编码。
    注意:这两个条件的上限值是可以修改的, 具体请看配置文件中关于hash-maxziplist-value选项和hash-max-ziplist-entries选项的说明。

3.4、集合对象

intset编码的集合对象使用整数集合作为底层实现, 集合对象包含的所有元素都被保存在整数集合里面。
另一方面, hashtable编码的集合对象使用字典作为底层实现, 字典的每个键都是一个字符串对象, 每个字符串对象包含了一个集合元素, 而字典的值则全部被设置为NULL。
编码转换
当集合对象可以同时满足以下两个条件时, 对象使用intset编码:

  • 集合对象保存的所有元素都是整数值;
  • 集合对象保存的元素数量不超过512个。
    不能满足这两个条件的集合对象需要使用hashtable编码。
    第二个条件的上限值是可以修改的, 具体请看配置文件中关于set-maxintset-entries选项的说明。
    注意:第二个条件的上限值是可以修改的, 具体请看配置文件中关于set-maxintset-entries选项的说明。

3.5、有序集合对象

有序集合的编码可以是ziplist或者skiplist。
ziplist编码的压缩列表对象使用压缩列表作为底层实现, 每个集合元素使用两个紧挨在一起的压缩列表节点来保存, 第一个节点保存元素的成员( member) ,而第二个元素则保存元素的分值( score) 。
压缩列表内的集合元素按分值从小到大进行排序, 分值较小的元素被放置在靠近表头的方向, 而分值较大的元素则被放置在靠近表尾的方向。

为什么有序集合需要同时使用跳跃表和字典来实现?

在理论上, 有序集合可以单独使用字典或者跳跃表的其中一种数据结构来实现, 但无论单独使用字典还是跳跃表, 在性能上对比起同时使用字典和跳跃表都会有所降低。 举个例子, 如果我们只使用字典来实现有序集合, 那么虽然以O( 1) 复杂度查找成员的分值这一特性会被保留, 但是, 因为字典以无序的方式来保存集合元素, 所以每次在执行范围型操作——比如ZRANK、 ZRANGE等命令时, 程序都需要对字典保存的所有元素进行排序, 完成这种排序需要至少O( NlogN) 时间复杂度, 以及额外的O( N) 内存空间( 因为要创建一个数组来保存排序后的元素) 。
另一方面, 如果我们只使用跳跃表来实现有序集合, 那么跳跃表执行范围型操作的所有优点都会被保留, 但因为没有了字典, 所以根据成员查找分值这一操作的复杂度将从O( 1) 上升为O( logN) 。 因为以上原因, 为了让有序集合的查找和范围型操作都尽可能快地执行, Redis选择了同时使用字典和跳跃表两种数据结构来实现有序集合。
编码转换
当有序集合对象可以同时满足以下两个条件时, 对象使用ziplist编码:·有序集合保存的元素数量小于128个;
·有序集合保存的所有元素成员的长度都小于64字节;
不能满足以上两个条件的有序集合对象将使用skiplist编码。
注意:以上两个条件的上限值是可以修改的, 具体请看配置文件中关于zset-maxziplist-entries选项和zset-max-ziplist-value选项的说明。

四、类型检查与命令多态

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等命令只能对有序集合键执行;
    类型特定命令所进行的类型检查是通过redisObject结构的type属性来实现的:
  • 在执行一个类型特定命令之前, 服务器会先检查输入数据库键的值对象是否为执行命令所需的类型, 如果是的话, 服务器就对键执行指定的命令;否则, 服务器将拒绝执行命令, 并向客户端返回一个类型错误。
    Redis除了会根据值对象的类型来判断键是否能够执行指定命令之外, 还会根据
    值对象的编码方式, 选择正确的命令实现代码来执行命令。

五、内存回收

因为C语言并不具备自动内存回收功能, 所以Redis在自己的对象系统中构建了一个引用计数( reference counting) 技术实现的内存回收机制, 通过这一机制, 程序可以通过跟踪对象的引用计数信息, 在适当的时候自动释放对象并进行内存回收。
对象的引用计数信息会随着对象的使用状态而不断变化:

  • 在创建一个新对象时, 引用计数的值会被初始化为1;
  • 当对象被一个新程序使用时, 它的引用计数值会被增一;
  • 当对象不再被一个程序使用时, 它的引用计数值会被减一;
  • 当对象的引用计数值变为0时, 对象所占用的内存会被释放。

六、对象共享

除了用于实现引用计数内存回收机制之外, 对象的引用计数属性还带有对象共享的作用。
目前来说, Redis会在初始化服务器时, 创建一万个字符串对象, 这些对象包含了从0到9999的所有整数值, 当服务器需要用到值为0到9999的字符串对象时, 服务器就会使用这些共享对象, 而不是新创建对象。
为什么Redis不共享包含字符串的对象?
当服务器考虑将一个共享对象设置为键的值对象时, 程序需要先检查给定的共享对象和键想创建的目标对象是否完全相同, 只有在共享对象和目标对象完全相同的情况下, 程序才会将共享对象用作键的值对象, 而一个共享对象保存的值越复杂, 验证共享对象和目标对象是否相同所需的复杂度就会越高, 消
耗的CPU时间也会越多:
·如果共享对象是保存整数值的字符串对象, 那么验证操作的复杂度为O( 1) ;
·如果共享对象是保存字符串值的字符串对象, 那么验证操作的复杂度为O( N) ;
·如果共享对象是包含了多个值( 或者对象的) 对象, 比如列表对象或者哈希对象, 那么验证操作的复杂度将会是O( N 2) 。
因此, 尽管共享更复杂的对象可以节约更多的内存, 但受到CPU时间的限制, Redis只对包含整数值的字符串对象进行共享。

七、对象的空转时长

除了前面介绍过的type、 encoding、 ptr和refcount四个属性之外,redisObject结构包含的最后一个属性为lru属性, 该属性记录了对象最后一次被命令程序访问的时间。
OBJECT IDLETIME命令可以打印出给定键的空转时长, 这一空转时长就是通过将当前时间减去键的值对象的lru时间计算得出的。

八、总结

  • Redis数据库中的每个键值对的键和值都是一个对象。
  • Redis共有字符串、 列表、 哈希、 集合、 有序集合五种类型的对象, 每种类型的对象至少都有两种或以上的编码方式, 不同的编码可以在不同的使用场景上优化对象的使用效率。
  • 服务器在执行某些命令之前, 会先检查给定键的类型能否执行指定的命令, 而检查一个键的类型就是检查键的值对象的类型。
    -Redis的对象系统带有引用计数实现的内存回收机制, 当一个对象不再被使用时, 该对象所占用的内存就会被自动释放。
  • Redis会共享值为0到9999的字符串对象。
  • 对象会记录自己的最后一次被访问的时间, 这个时间可以用于计算对象的空转时间。

参考资料:《redis设计与实现》

上一篇:【redis深层次探索】数据结构和对象【六】-- 压缩列表( ziplist)
下一篇:【redis深层次探索】RDB持久化
                                                                                    不积跬步,无以至千里;不积小流,无以成江海!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值