此 承前 数据结构,启后 数据类型
前面我们介绍了 各种数据结构 ( sds
、dict
、skiplist
、intset
、ziplist
等 ),作为 Redis 对外提供的 各种数据类型的 底层组成部分;但是 各种数据类型的 键值对 并不是直接使用 这些数据结构,而是基于这些 数据结构 构建了一个 对象系统 ( 字符串对象
、列表对象
、哈希对象
、集合对象
、有序集合对象
),每种对象都 至少使用了一种 我们之前提到的 数据结构
对象的有如下特性
- Redis 在执行命令之前,根据对象的类型就可以判断 命令是否可以执行
- 依托对象,可以针对不同的使用场景,为对象设置不同的数据结构实现,从而优化对象在不同场景下的使用效率
- Redis 对象系统实现了基于引用计数技术的内存回收机制,当程序不再使用某个对象的时候,这个对象所占用的内存就会被自动释放
- Redis 使用引用计数技术实现了对象共享机制,一定场景下,可以通过让多个数据库键共享同一个对象来节约内存
- Redis 对象带有访问时间记录,可以用来计算数据库键的空转时间,在服务器启用 maxmemory 的情况下,空转时长较大的键可能被优先删除
对象结构
Redis 每个对象都是由一个 redisObject
结构表示,如下:
对象类型 type
对象的类型主要有如下常量值,分别对应不同对象类型 字符串对象
、列表对象
、集合对象
、有序集合对象
、哈希对象
作为 Redis 键值对 来说,键总是 字符串对象,而值是 各种对象,比如 常说的 列表键,实际就是 列表键对象,键为字符串对象,值是 列表对象
Redis 中命令 TYPE
可以查看数据的对象类型
编码类型 encoding
对象的 ptr 指针指向对象的底层实现,就是 我们之前 提到的那些 数据结构,而 区分哪些 数据结构 的属性就是 encoding,encoding 有如下常量值
Redis 的每一种对象类型可以对应不同的编码方式,这就极大地提升了 Redis 的灵活性和效率,可以根据不同的使用场景,来选择合适的编码方式,五种对象类型对应的底层编码方式如下
LRU 访问
lru表示该对象最后一次被访问的时间,其占用 24 个bit位;保存该值的目的是为了计算该对象的空转时长,便于后续根据空转时长来决定是否释放该键,回收内存,通过 命令 OBJECT IDLETIME
查看对象的 空转时间(当前时间 - lru 时间)
我们可以看到设置完 foo1 键值对以后停留了一下,lru 值为 14,然后 get 访问后,lru 值立即缩短了,不过 OBJECT IDLETIME
这个命令不会修改 lru 的值,否则 我们怎么调试 ^-^
当然 lru 的设计并不是仅仅用来查看的,如果 服务器打开了 maxmemory 选项,在一定情况下 会 启动 lru 算法进行淘汰:
volatile-lru
根据LRU算法删除设置了超时属性(expire)的键,直到腾出足够空间为止;如果没有可删除的键对象,回退到noeviction策略。allkeys-lru
根据LRU算法删除键,不管数据有没有设置超时属性,直到腾出足够空间为止
引用计数 refcount
实际 C 是不具备内存回收功能的,所以 Redis 在自己的对象系统中构建了一个引用计数的实现的回收机制,通过这一机制,程序可以通过跟踪对象的引用计数信息,在适当的时候自动释放对象并内存回收
对象的引用计数信息的变化规律如下:
- 在创建一个对象的时候,引用计数的值会初始化为 1
- 当对象被一个新程序使用时,它的引用计数值会增加 1
- 当对象不再被一个程序使用时,它的引用计数值会减去 1
- 当对象的引用计数值为 0 的时候,对象占用的内存会被释放
获取键值的 refcount 属性值也可以通过命令进行
对象共享
除了 回收机制 功能外,对象的 引用计数 还带有 对象共享功能。Redis 默认会建立 0 ~ 9999 ( 通过 server.h: line 92 的常量 OBJ_SHARED_INTEGERS
控制 ) 每个数的对象,也就是说 Redis 在初始化的时候已经把这 10000个 数字对象建立了,只要后续使用到的 这个范围内的数字都会 直接引用这些对象,我们可以通过 refcount
属性来查看一下使用 对象共享功能情况,比如:
我们可以看到
- foo1 和 foo2 的键值都为 hello 字符串,但是 引用计数都是 1
- 而设置 foo3 键值为 101 这个数字 ,但是 引用计数设置为了 INT_MAX(2147483647) 值
- 反观设置了 foo4 键值大于默认的 10000,引用计数为 1,没有使用 共享对象
当然,并不只是 字符串对象 才能使用这些 共享对象,那些在数据结构中嵌套了字符串对象都会用到( 双向链表、哈希、有序集合 等)
那么为什么共享对象只是使用了 数字字符串呢? 因为 服务器考虑将一个 对象设置为键 的值对象时,需要先检查 给定共享对象 跟 想要创建的对象 是否完全相同,只有在 共享对象 和 目标对象 完全相同的时候,程序才会使用 共享对象,所以 共享对象 保存的值越复杂,那么这个 对比过程 CPU 消耗率就越高
- 数字字符串的对比验证复杂度为 O(1)
- 字符串对比验证复杂度为 O(n)
- 列表对象或者哈希对象的验证复杂度为 O(n2)*
所以,尽管共享对象更为节约内存,但是受 CPU 运算的限制,Redis 只包含整数值字符串的共享对象
源码解析
由于有些方法里的代码量比较大,我们这里按照 典型的代码片段进行解析,同志们可以根据文章提示的代码位置 和 代码里面的关键词 在源码中搜素,可能数据结构一些元素 看不太懂什么意思,没关系,先混个脸熟,后面看完回头再看过来就明白了
由于 对象 object 的 API 大多数都跟 各个数据类型 有关,我们后续在 各个数据类型模块会进行 附带讲解
此次我们主要讲解以下三个交互指令的源码
object refcount
返回对象的引用计数object encoding
返回对象中存放的数据的编码方式object idletime
返回对象的空转时长object freq
返回对象的访问频率,这个地方相关的主要是利用 LFU 原理获取频率值后进行排序,获取热点 key
|
|
本文作者: wettper
本文链接: http://www.web-lovers.com/redis-source-object.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!