【Redis】 对象

Metadata

title: 【Redis】 对象
date: 2023-07-09 08:35
tags:
  - 行动阶段/完成
  - 主题场景/数据存储
  - 笔记空间/KnowladgeSpace/ProgramSpace/BasicsSpace
  - 细化主题/数据存储/Redis
categories:
  - 数据存储
keywords:
  - 数据存储/Redis
description: 【Redis】 对象

概述

Redis 没有直接使用前文的数据结构来实现键值对数据库,而是基于这些数据结构构建了一个对象系统,通过对象组织数据结构,包括字符串对象,列表对象,哈希对象,集合对象有序集合对象这 5 种对象。

使用对象的一个好处是可以针对不同的使用场景,为对象设置多种不同的数据结构实现,从而优化对象在不同场景下的使用效率。

Redis 使用对象来表示数据库的键和值。每个对象都是一个 redisObject 结构,是一个按照位段存储的结构,节约内存:

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

对象的结构

Redis 使用对象来表示数据库的键和值。每个对象都是一个 redisObject 结构,是一个按照位段存储的结构,节约内存:

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

其中,type 是类型常量,记录对象的类型:

类型常量 对象的名字
REDIS_ String 字符串对象
REDIS_ List 列表对象
REDIS_ HASH 哈希对象
REDIS_ SET 集合对象
REDIS_ ZSET 有序集合对象

encoding 记录对象使用的编码,即对象底层使用的具体数据结构:

编码常量 编码所对应的底层数据结构
REDIS_ ENCODING_ INT long类型的整数
REDIS ENCODING EMBSTR embstr编码的简单动态字符串
REDIS_ ENCODING RAW 简单动态字符串
REDIS_ ENCODING_ HT 字典
REDIS_ ENCODING LINKEDLIST 双端链表
REDIS ENCODING ZIPLIST 压缩列表
REDIS_ ENCODING_ INTSET 整数集合
REDIS ENCODING SKIPLIST 一秒乎 跳跃表和字典

Redis 对象采用 encoding 属性来设置编码,从而决定底层数据结构,而不是为特定类型的对象关联一种固定编码。这种方式极大地提高了灵活性和效率。

字符串对象

字符串对象可以是 int,rawembstr

  • 如果字符串对象保存的是整数值,且这个数值可用 long 表示,底层就会以**REDIS_ENCODING_INT**编码来实现。
  • 如果字符串对象是一个字符串值,且这个字符串长度 >39 字节,字符串将使用一个 SDS 保存,底层编码为**REDIS_ENCODING_RAW**。
  • 如果字符串对象保存的是字符串,且这个字符串长度 <=39 字节,底层编码就是**REDIS_ENCODING_EMBSTR**,使用 embstr 编码的方式保存字符串。

embstr 编码

专门用于保存短字符串的一种优化编码方式,与 raw 的效果相同,都使用 redisObject 和 sdshdr 结构来表示字符串对象,但是 raw 会调用两次内存分配函数分别创建 redisObject 和 sdshdr 结构。embstr 编码则通过调用一次内存分配函数来分配一块连续空间,空间依次包括 redisObject 和 sdshdr 俩结构。

使用 embstr 编码保存短字符串的优点

  • 内存分配次数由两次降为 1 次
  • 释放 embstr 字符串对象只需调用 1 次内存释放函数。
  • embstr 字符串放在一块连续的内存中,能更好地利用缓存带来的优势.

注:浮点数的存储,在 Redis 底层也会以字符串的形式保存。在有需要时,程序会将字符串对象中的字符串值转为浮点数值执行运算操作,然后再将结果转为字符串值保存。

编码的转换

int->raw:对 int 编码的字符串对象执行后,保存的不再是整数值,而是字符串值时。比如整数追加字符串。

embstr->raw:Redis 没有为 embstr 编写修改程序,所以是只读的,当 embstr 编码的字符串修改后,就变成 raw 编码的字符串对象。

列表对象

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

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

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

否则使用 linkedlist 编码。

注:两条件的上限值可通过配置文件修改。

使用 ziplist 编码,执行RPUSH elements "a" "b" 1,后的数据结构:

注:SDS 对象都以 StringObject 代替

哈希对象

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

  • ziplist 的数据结构:每当有新的键值对插入哈希对象时,Redis 会先将保存键的压缩列表节点推入表尾,再将保存值的压缩列表节点推入表尾。
  • hashtable 的数据结构:字典的每个键都是一个字符串对象,保存键;字典的每个值都是字符串对象,保存值

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

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

否则使用 hashtable 编码。

注:两条件的上限值可通过配置文件修改。

使用 ziplist 编码,执行HSET student name "madongmei" age 25 career "pick up trash"后的数据结构:

使用 hashtable 编码,执行HSET student name "madongmei" age 25 career "pick up trash"后的数据结构:

集合对象

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

如果以 hashtable 编码作为集合对象底层实现,那么字典的每个键都是一个字符串对象,值都是 null

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

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

否则使用 hashtable 编码。

注:两条件的上限值可通过配置文件修改。

使用 intset 编码,执行SADDnumbers 1 3 5后的数据结构:

有序集合对象

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

使用 ziplist 编码时,每个元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,而第二个元素则保存元素的分值。

如果是 skiplist 编码,使用 zset 结构:

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

dict 字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值对都保存了一个集合元素:键保存元素,值保存分值。通过字典以 O(1) 查找给定成员的分值。有序集合元素都是字符串对象,分值都是 double 类型浮点数。zset 的跳跃表和字典通过指针来共享相同元素的成员和分值,不会浪费额外内存。

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

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

否则使用 skiplist 编码。

注:两条件的上限值可通过配置文件修改。

类型检查与命令多态

Redis 中用于操作键的命令可分为两种类型。一种是可对任何类型执行的,如 del,expire,rename 等。另一种命令只能对特定类型的键执行,如 set,get,hdel,hset,rpush 等。如果对特定类型使用其他类型的命令,那么就会报错。

类型检查的实现

为了确保只有制定类型的键可以执行某些特定命令,在执行前,Redis 会先通过 RedisObject 的 type 属性检查输入键的类型是否正确。

多态命令的实现

Redis 除了根据值对象判断键是否能够执行制定命令外,还会根据值对象的编码方式,选择正确的命令实现代码来执行。比如基于编码的多态,列表对象的编码可能是 ziplist 或 linkedlist,所以需要多态命令执行对应编码的 API。基于类型的多态是一个命令可以同时处理多种不同类型的键

内存回收

由于 C 语言没有内存回收机制,Redis 在对象系统中构建了引用计数器技术实现内存回收机制。每个对象的引用计数器信息由 redisObject 的 refcount 来记录。当对象的引用计数值为 0 时,所占用的内存会被释放

对象共享

引用计数器还有共享对象的作用。如果两个不同键的值都一样(必须是整数值的字符串对象),则将数据库键的值指针指向一个现有的值对象,然后将被共享对象的引用计数加一。如果不是整数值的对象,则需要耗费大量的时间验证共享对象和目标对象是否相同,复杂度较高,消耗 CPU 时间,所以 Redis 不会共享包含字符串的对象

Redis 在初始化服务时,会创建很多字符串对象,包含 0~9999 的整数(和 Integer 的常量池有点像),当需要时,就能直接复用。

对象的空转时长

redisObject 还包含了 lru 属性,记录对象最后一个被命令程序访问的时间。object idletime命令可打印键的空转时长,就是当前时间减去 lru 时间计算得到的。