【Redis】 对象
【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,raw 或 embstr。
- 如果字符串对象保存的是整数值,且这个数值可用 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 时间计算得到的。