Fork me on GitHub

六、Redis-对象

前言

之前我们说了 Redis 的基本数据结构,但是 Redis 并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统。

除此之外,Redis还引入了基于引用计数技术的内存回收机制和键的空转时长

对象的类型和编码

Redis 使用对象来表示数据库中的键和值,当我们在 Redis 数据库中创建一个键值对时,键默认是字符串对象,值可以是字符串、链表、集合、哈希、有序集合。

Redis中每个对象都是由一个 RedisObject 结构表示 ,改结构中有三个和和保存的数据相关的属性,type、encoding、ptr

1
2
3
4
5
6
7
8
9
10
11
12
struct redisObject{
//类型
unsigned type:4;

//编码
unsigned encoding:4;

//指向底层实现数据结构的指针
void *ptr;

//...
}
  • type:属性记录了对象的类型,其值可以为下面几种
类型常量 对象的名称
REDIS_STRING 字符串对象
REDIS_LIST 列表对象
REDIS_HASH 哈希对象
REDIS_SET 集合对象
REDIS_ZSET 有序集合对象

我们可以通过 TYPE 命令来查看数据库键对应的值对象的类型

  • ptr:指针指向对象的底层实现的数据结构,而这些数据结构由对象的 encoding 属性决定
  • encoding:记录了对象所使用的编码,也即这个对象使用了什么数据结构作为对象的底层实现,其值可以为下面几种
编码常量 编码对应的底层数据结构
REDIS_ENCODING_INT long 类型的编码
REDIS_ENCODING_EMBSTR embstr编码的简单动态字符串
REDIS_ENCODING_RAM 简单动态字符串
REDIS_ENCODING_HT 字典
REDIS_ENCODING_LINKEDLIST 双端链表
REDIS_ENCODING_ZIPLIST 压缩列表
REDIS_ENCODING_INTSET 整数集合
REDIS_ENCODING_SKIPLIST 跳跃表和字典

我们可以使用 OBJECT ENCODING 命令可以查看一个数据库键

字符串对象

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

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

如果字符串对象保存的字符串值的长度大于39字节,那么字符串对象会使用简单动态字符串(SDS)来保存这个字符串值,并将对象的编码设置为 raw

如果字符串对象保存的字符串值长度小于39字节,那么字符串对象将使用 embstr 编码的方式来保存这个字符串值

raw和embstr的区别

  • embstr编码将创建字符串对象所需的内存分配次数从 raw 的两次降低为一次
  • 释放 embstr 编码字符串对象只需要调用一次内存释放函数,而 raw 编码的字符串需要两次
  • embstr 编码的字符串对象的所有数据都保存在一块连续的内存里面,所以这比 raw 编码的字符串对象能更好的利用缓存带来的优势

int编码的字符串对象和 embstr 编码的字符串对象在条件满足的情况下,会被转换成 raw 编码的字符串对象。因为Redis没有为embstr编码的字符串对象编写任何相应的修改程序,所以embstr编码的字符串对象实际上是只读的。

列表对象

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

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

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

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

  • 列表对象保存的所有字符串元素的长度都小于64字节
  • 列表对象保存的元素数量小于512个;不能满足这两个条件的列表对象需要使用 linkedlist 编码

哈希对象

哈希对象的编码可以是 ziplist(REDIS_ENCODING_ZIPLIST) 和 hashtable(REDIS_ENCODING_HT)

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

  • 保存了同一键值对的两个节点总是挨在一起,保存键的节点在前,保存了值的节点在后
  • 先添加到哈希对象中的键值对会被放在压缩列表的表头方向,而后来添加到的会被放在压缩列表的表尾方向

hashtable 编码的哈希对象使用字典作为底层实现,哈希对象中的每个键值对都使用一个字典键值对来保存

  • 字典的每个键都是一个字符串对象,对象中保存了键值对的键;
  • 字典的每个值都是一个字符串对象,对象中保存了键值对的值

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

  • 哈希对象保存的所有键值对的键和值的字符串长度都小于 64 字节
  • 哈希对象保存的键值对数量小于 512 个;不能满足这两个条件的哈希对象需要使用 hashtable

集合对象

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

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

hashtable 编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而字典的值则全部被设置为null

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

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

有序集合对象

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

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

集合元素按照分值由大到小排序,分值较小的元素被放置在靠近表头的位置,而分值较大的被放置在靠近表尾的位置

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

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

内存回收

C语言并没有具备自动回收的功能,所以在Redis内部,Redis自己构建了一个基于引用计数的内存回收标准。程序可以通过跟踪对象的引用技术信息,在适当的时候自动释放对象并进行内存回收。

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

对象共享

对象的引用计数属性除了可以用来进行内存回收之外,也可以用来进行对象的共享。Redis在初始化服务器的时候,会创建1万个字符串对象,包含了从0到9999的所有整数值,当服务器需要用到0到9999的字符串对象时,服务器就会使用这些共享对象,而不再去创建新对象。

Redis只对包含整数值的字符串对象进行共享

对象的空转时长

前面介绍了 RedisObject 结构包含的四个属性,type、encoding、ptr、refcount;除了这些最后再介绍一个属性 lru 属性,改属性记录了对象最后一次被命令程序访问的时间。

键的空转时长可以用于回收内存,当服务器打开了 maxmemory 选项,并且服务器用于回收内存的算法为 volatile-lru 或者 allkeys-lru ,那么当服务器占用的内存数超过了 maxmemory 选项设置的上限,空转时长较高的那部分键会优先被服务器释放,从而回收内存。