Redis 底层学习-对象

redis 中的对象就是我们常说的 redis 数据类型,字符串对象、列表对象、哈希对象、集合对象、有序集合对象这 5 种数据类型。

这 5 种数据类型在实际使用的时候使用了上一章中的 2 种以上的数据结构,来满足不同场景下的使用效率。

在我们创建一个键值对的时候。实际上创建了 2 个对象,一个为键的字符串对象,一个为值的具体对象。

数据结构

image-20200820144943258

type:对象的类型,只能是 5 种数据类型的一种。可以通过命令type key获取对象的类型

image-20200820145041785

encoding:编码,记录了对象使用什么数据结构来作为对象的底层实现。可以通过object encoding key来获取对象的编码。

image-20200820145456016

image-20200820145543523

字符串对象

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

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

image-20200820150657821

ptr 指针变成存放具体指的对象。

  1. 如果字符串对象保存的是一个字符串值,并且这个字符值的长度大于 32 字节,那么字符串对象将使用一个简单动态字符串-sds 来保存这个字符串值,并将对象的编码设置为 raw。

    image-20200820150925203

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

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

    image-20200820151350387

    embstr 编码的字符串有一下好处

    • embstr 编码将创建字符串对象所需的内存分配次数从 2 次变成一次
    • 释放内存的时候也减少一次内存释放函数
    • 获取数据的以后,embstr 编码的字符串对象在一块连续的内存里面,所以速度更快。

编码的转化

int 编码的字符串对象,如果对对象是用 append 等修改函数,增加字符的时候,当前字符串的编码会从 int 变成 raw(没有达到 32 个字节,仍然是 raw 编码)

由于 redis 没有为 embstr 编码的字符串对象编写任何相应的修改程序,所以 embstr 编码的字符串对象实际上是只读的,只要修改就会变成 raw 编码的字符串,哪怕没有满足 32 个字节。

列表对象

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

image-20200820152738724

image-20200820152749982

编码转换

当列表对象同时满足一下两个条件的时候,列表对象是用 ziplist 编码

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

以上 2 个条件的上限值是可以通过修改配置 list-max-ziplist-value 和 list-max-ziplist-entries 参数来修改。

哈希对象

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

ziplist

  1. ziplist 编码的哈希对象是用压缩列表作为底层实现,每当有新的键值对要加入到哈希对象时,程序会先将保存了键的压缩列表节点推入压缩列表表尾,然后将保存了值的压缩列表节点推入到压缩列表末尾。因此:
  • 同一个键值对的节点总是紧挨在一起,保存键的节点在前,保存值的节点在后。
  • 先添加的哈希对象中的键值对在压缩列表的前面,后添加的哈希对象中的键值对在压缩列表的后面。

image-20200820162832741

image-20200820162843157

hashtable

哈希对象中的键值对是用字典中的键值对来保存,键是一个字符串对象,值也是一个字符串对象。

image-20200821104949899

编码转换

当哈希对象同时满足 2 个条件的时候 ,使用 ziplist 保存

  1. 哈希对象所有键值对中的键和值的字符串都小于 64 个字节
  2. 哈希对象保存的键值对少于 512 个。

这 2 个值的上限值可以通过修改配置 hash-max-ziplist-value 和 hash-max-ziplist-entries 来调整

集合对象

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

inset 编码的集合对象使用整数集合作为底层实现,集合对象的所有元素都被保存在整数集合中。

hashtable 编码的集合对象,字典中的每一个键都是一个字符串对象,每个字符串包含了一个元素,而字典的元素则全部被设置为 null。

image-20200821110236269

编码转换

当集合对象满足一下 2 个条件时,对象使用 intset 编码

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

第 2 个值的上限值可以通过修改配置 set-max-intset-entries 来调整。

有序集合对象

有序集合对象的编码可以是 ziplist 和 skiplist

ziplist

ziplist 编码的有序结合对象使用压缩列表作为底层实现,每个集合元素使用 2 个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个元素则保存元素的分值。

压缩列表内的集合元素按分值从小到大进行排序,分值较小的元素被放置在靠近表头的方向,分值较大的元素被放置在靠近表尾的方向

image-20200821112008261

image-20200821112023637

skiplist/hashtable

在跳跃表部分,按照分值从大到小存储了所有集合元素,每个跳跃表节点都保存了一个集合元素,跳跃表的 object 属性保存了元素的成员,score 属性保存了元素的分值。通过这个跳跃表,程序可以通过有序集合进行范围性操作。

除此之外,zset 还创建了一个 dict 字典保存了从成员到分值的映射,保证能够在 O(1)的复杂度查找给定成员的分值。

image-20200821115314259

单独使用 skiplist 虽然可以保证范围查询,但是不能直接通过成员获取分值,单独使用字典能保证直击诶从成员获取分值,但是由于字典是无序的,所以每次获取数据都要排序。综合之后,2 种数据结构都是用了。由于可以通过指针共享相同的成员和分值,不会出现额外的内存消耗。

编码转换

当有序集合对象可以同时满足一下 2 个条件的时候,对象是用 ziplist 编码

  1. 有序集合保存的元素小于 126 个。

  2. 有序集合保存的元素的大小都小于 64 字节。

    以上 2 个参数可以通过修改配置 zset-max-ziplist-entries 和 zet-max-ziplist-value 来调整。

对象类型检查和命令多态

redis 的不同对象可以使用不同的指令,当对一个 key 使用命令的时候,会先检查当前对象是否拥有该命令,否则会抛出类型异常错误。

由于 redis 的每个类型的 encoding 都有 2 个以上的实现,在执行命令的时候会根据编码多态选择对应具体实现来完成具体的操作。

对象内存回收

redis 在自己的对象系统中构建了一个引用计数法实现的内存机制。程序可以跟踪对象的引用计数信息,在适当的时候自动释放对象并进行回收。–refcount 属性

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

对象共享

redis 对于常用的 0-9999 整数类型的字符串对象进行了预先创建,后续如果有新的程序需要访问,可以直接指向预先创建的对象,对象的引用计数+1,这样能更节省内存。考虑到字符串内容比较的时候时 O(n)的复杂度,所以只缓存了 0-9999 整数类型的字符串。类似于 java 中的享元模式。

对象的空转时长

redisObject 对象的属性中还有一个 lru 属性,用来记录对象被程序最后一次访问的时间。

可以通过命令object ldletime key 来获取键就的空转时长,== 当前时间-最后被访问的时间。

如果服务器打开了 maxmemory 选项,并且服务器用于回收内存的算法为 volatile-lru 或者 allkeys-lru 时,那么当服务器被占用的内存超过了 maxmemory 选项,空转时长越大的那部分键会优先被服务器回收。