Redis学习笔记(六):对象系统

本文深入解析Redis3.0的对象系统,包括基础数据结构、对象类型与编码,如字符串、列表、哈希、集合和有序集合的内部实现。介绍了编码转换的条件,内存回收和对象共享机制,以及对象空转时长在内存管理中的作用。内容涵盖Redis优化内存使用的关键策略。
摘要由CSDN通过智能技术生成

前言

Redis没有直接使用基础数据结构来实现数据库,而是基于这些数据结构创建了一个对象系统。这个系统包含字符串对象、列表对象、哈希对象、集合对象、有序集合对象。

1. Redis3.0中的6种基础数据结构

数据结构应用场景
简单动态字符串保存字符串值
链表列表键,发布与订阅,慢查询,监视器
字典哈希键,数据库键空间
跳跃表有序集合键
整数集合集合键
压缩列表列表键、哈希键

2. 对象的类型和编码

Redis使用对象表示数据库中的键和值,每次在Redis新建一个键值对都会创建两个对象,键对象和值对象

Redis中的每个对象由一个struct redisObject结构表示:

// src/redis.h
typedef struct redisObject {
    unsigned type:4;			    // 类型
    unsigned encoding:4;			// 编码
    unsigned lru:24;				// 记录对象最后一次被命令程序访问的时间
    int refcount;					// 引用计数
    void *ptr;						// 指向底层实现数据结构的指针
} robj;

其中type属性表示对象类型, encoding属性表示对象编码。以下介绍对象类型和对象编码的概念。

2.1 对象类型

Redis 3.0中,定义如下5种对象类型:

类型常量对象名称TYPE命令的输出
REDIS_STRING字符串“string”
REDIS_LIST列表“list”
REDIS_SET集合“set”
REDIS_ZSET有序集合“zset”
REDIS_HASH哈希“hash”

注意,对于Redis中的任一键值对,键总是一个字符串对象,而值可以是上面5种对象的任意一种。比如说,列表键的意思是指,这个键对应的值是列表对象,其他类型对象以此类推。

TYPE命令用于返回对象类型。

2.2 对象编码

redisObject中的ptr指针指向对象使用的基础数据结构,而具体使用何种数据结构是由对象编码决定的。

encoding属性用于表示对象编码,说明这个对象使用的是何种数据结构。Redis 3.0中,encoding值可以是如下的任何一种:

encoding使用的底层数据结构
REDIS_ENCODING_RAWSDS
REDIS_ENCODING_INTlong类型数
REDIS_ENCODING_HT字典
REDIS_ENCODING_LINKEDLIST双端链表
REDIS_ENCODING_ZIPLIST压缩列表
REDIS_ENCODING_INTSET整数集合
REDIS_ENCODING_SKIPLIST跳表
REDIS_ENCODING_EMBSTREMBSTR编码的SDS

OBJECT ENCODING命令用于查看某个数据库键的值对象的编码。

每种类型的对象都使用了至少两种不同的编码,这使得Redis可以优化对象在不同场景下的效率和空间占用。

2.3 不同类型和编码的对象
对象类型可能的编码类型
字符串long, embstr,raw
列表压缩列表、双端链表
哈希压缩列表、字典
集合整数集合、字典
有序集合压缩列表、跳表

2. Redis中的五种对象类型

2.1 字符串对象

字符串对象有三种编码,可以是int, embstr, raw。使用何种编码是通过字符串对象保存的值类型和字符串长度决定,规则如下:

  • 如果保存的是整数值,且整数不超过long范围,使用int编码
  • 如果保存的是字符串值,且字符串长度 > 39, 使用raw编码
  • 如果保存的是字符串值,且字符串长度 <= 39,使用embstr编码

对应redis源码中CreateStringObject的实现:

#define REDIS_ENCODING_EMBSTR_SIZE_LIMIT 39
robj *createStringObject(char *ptr, size_t len) {
    if (len <= REDIS_ENCODING_EMBSTR_SIZE_LIMIT)
        return createEmbeddedStringObject(ptr,len);
    else
        return createRawStringObject(ptr,len);
}
2.1.1 为什么需要embstr编码?

embstr编码是专门为保存短字符串的一种优化编码方式。和raw编码区别在于,embstr编码仅调用一次内存分配函数同时创建redisObject和sdshdr结构,优势如下:

  • 内存分配、内存回收次数由两次降为一次。
  • redisObject结构和sdshdr结构的内存是连续的、因此能更好地利用缓存。

以下分别给出raw编码和embstr编码的字符串对象示意图:

raw编码字符串:

在这里插入图片描述

embstr编码字符串:
在这里插入图片描述

2.1.2 编码的转换

如果对int , embstr编码的字符串执行append操作,它们总是会转换为raw编码的字符串, 理由如下:

  • 对int编码字符串做了append操作后,这个对象保存的就不再是整数值,所以编码必须发生转换。

  • 对于embstr编码,Redis没有为embstr编码的字符串对象编写任何修改其内容的代码,所以append操作后embstr编码总转换成raw。也就是说,embstr编码的字符串为只读对象。

2.2 列表对象

列表对象编码可以是ziplist或linkedlist,创建列表对象代码如下:

robj *createListObject(void);
robj *createZiplistObject(void);
2.2.1 编码的转换

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

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

以上条件可以通过配置文件修改,相关配置项:list-max-ziplist-value, list-max-ziplist-entries

2.3 哈希对象

哈希对象编码可以是ziplist或hashtable,创建对象代码如下:

robj *createHashObject(void);
2.3.1 编码的转换

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

  • 哈希对象保存的所有键值对的键和值长度都小于64字节
  • 哈希对象保存的键值对数少于512个

以上条件可以通过配置文件修改,相关配置项:hash-max-ziplist-value, hash-max-ziplist-entries

2.4 集合对象

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

2.4.1 编码的转换

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

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

以上条件可以通过配置文件修改, 相关配置项:set-max-intset-entries

2.5 有序集合对象

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

基于skiplist编码的数据结构如下,可以看到有序集合对象同时使用了跳表和字典数据结构来实现。

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

有序集合元素同时保存在跳表和字典,示意图如下:

在这里插入图片描述
(以上图片来源:http://redisbook.com/preview/object/sorted_set.html)

图中字典和跳表重复列出了各元素的成员和分数,而源码实现中,字典和跳表会共享元素的成员和分数,不会导致内存浪费。

2.5.1 为什么有序集合需要同时使用跳跃表和字典来实现?
  • 如只使用字典,因为字典是无序的,执行范围操作时须先排序,复杂度O(NlogN),以及额外O(N)空间。

  • 如只使用跳表,那么根据成员查找分数的操作效率较低,其复杂度为O(logN)。

2.5.2 编码的转换

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

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

以上条件可以通过配置文件修改,相关配置项:zset-max-ziplist-entries,zset-max-ziplist-value

3. 内存回收机制

Redis是通过C语言实现的,C语言没有内存自动回收机制,因此Redis在对象系统中通过引用计数方式实现内存回收机制。

每个对象的引用计数信息通过redisObject结构中的refcount属性表示:

  • 创建新对象时,引用计数初始为1
  • 对象被新程序使用时,引用计数加1,参考 object.c/incrRefCount
  • 对象不再被程序使用时,引用计数减1,当引用计数减为0时回收内存,参考object.c/decrRefCount

OBJECT REFCOUNT命令可以查看对象的引用计数。

4. 对象共享机制

对象的refcount属性除了用于实现内存回收机制外,还具有共享内存的作用。

比如,Redis服务器启动时会预先创建0 ~ 9999这1万个字符串对象。当redis需用到0 ~ 9999的字符串对象时,直接使用共享对象,不必每次都创建新对象,从而节约内存。源码参考object.c/createStringObjectFromLongLong

// object.c
robj *createStringObjectFromLongLong(long long value) {
    robj *o;
    // 如果value的大小符合 REDIS 共享整数的范围,直接使用共享对象,不用创建新对象
    if (value >= 0 && value < REDIS_SHARED_INTEGERS) {
        incrRefCount(shared.integers[value]);
        o = shared.integers[value];
    } else {
    	// 不在0 ~ 9999范围内,创建新的整数对象,以下代码省略 
        // .......
    }
    return o;
};
4.1 为什么Redis只共享包含整数的对象?
  • redis只共享包含数字的对象,是因为数字的复用场景最多。
  • 出于CPU限制。在使用一个共享对象前,需要判断共享对象和目前对象是否相等。一个共享对象的值越复杂,这个判断的时间复杂度越高,越消耗CPU性能,注意到:
    • 数字比较复杂度仅为O(1)
    • 字符串比较复杂度为O(n)
    • 对于更复杂的列表和哈希对象,比较复杂度上升到O(n ^ 2)。

5. 对象的空转时长

RedisObject的lru属性记录对象最后一次被命令程序访问的时间,可用于计算某个数据库键的空转时长。

OBJECT LDLETIME命令用于获取对象的空转时长,空转时长是通过当前时间减去对象的lru值计算得出。

空转时长可用于Redis服务器回收内存算法,原则是内存不足时,空转时长高的键优先被服务器释放,回收内存。

总结

  • Redis的对象系统包含五种基本对象:字符串对象、列表对象、哈希对象、集合对象、有序集合对象。
  • 每种类型的对象都使用了至少两种不同的编码,这使得Redis可以针对不同的场景,为对象设置多种不同的数据结构,优化对象在不同场景下的效率。
  • Redis对象实现了基于引用计数的内存回收机制,对象共享机制,从而节约内存。
  • Redis的对象带有访问事件记录信息,用于计算数据库键的空转时长,空转时长可以用于服务器的内存回收算法。

参考资料

《Redis设计与实现》 —— 第8章 对象

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

pcj_888

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值