Redis从入门到精通【进阶篇】之对象机制详解


在这里插入图片描述

0. 前言

Redis 之所以是一款高性能和受大家欢迎的的内存数据库,不仅是它支持多种数据类型,包括字符串、列表、哈希、集合、有序集合等数据结构。而且这些数据类型都是由对象结构(redisObject) 和对应编码的数据结构组合而成。在 Redis 中,对象结构是所有数据类型的底层实现,它包含了数据对象的类型、引用计数、编码方式、值等信息。使得Redis在性能和内存利用方面有着无可比拟的地位。奠定了它的江湖地位。

本文将重点介绍 Redis 对象结构(redisObject) 的实现原理。我们将从对象结构的基本原理入手,分析对象结构的组成部分,以及对象结构的内存管理和引用计数等重要概念。同时,我们也会探讨 Redis 对象结构的应用场景和实际应用案例,帮助大家更好地理解 Redis 的内部实现机制。

今天我们换一种分享方式,先抛出一些问题,这些问题可能是面试经常被问到的

1. 什么是redisObject对象?
2. redisObject数据结构解析?
4. redisObject对象如何实现数据共享和对象池技术?
5. redisObject对象的大小是否会随着数据类型的不同而变化?
6. redisObject对象的序列化和反序列化有哪些常用的方法?
7. Redis中还有哪些技术可以提高系统的性能和稳定性?
8. Redis中的对象池技术如何管理内存?
9. Redis中的共享池如何管理共享字符串对象?

1. 详解

Redis 中的 redisObject 对象是 Redis 中的基本数据结构,它是 Redis 实现高效存储和处理数据的关键之一。redisObject 对象可以表示各种不同类型的数据,如字符串、列表、哈希表等,它们在 Redis 中被广泛使用。

1.1 redisObject 对象设计目的

增强数据结构的灵活性:Redis 中支持多种不同类型的数据结构,如字符串、列表、哈希表、集合和有序集合等,这些数据结构在 Redis 中都被表示为 redisObject 对象。通过将不同类型的数据统一封装成 redisObject 对象,Redis 可以更加灵活地处理不同类型的数据,从而支持多种不同的应用场景。

简化内存管理:Redis 中所有数据都存储在内存中,因此需要对内存进行有效的管理。通过使用 redisObject 对象可以简化内存管理的复杂性,因为 redisObject 对象可以自己管理内部的内存空间,从而避免了手动管理内存的问题。

提高数据存储和访问的效率:Redis 中的 redisObject 对象是一个轻量级的对象,它的大小通常只有几个字节,因此可以快速地存储和访问。此外,Redis 还通过对象共享和对象池等技术来优化内存使用效率,从而提高了数据存储和访问的效率。

方便数据序列化和反序列化:Redis 中的 redisObject 对象可以方便地进行序列化和反序列化,因为 redisObject 对象是一种结构化的数据类型,它可以被序列化成二进制格式或其他格式,方便数据的传输和存储。

1.2 redisObject数据结构

redisObject 是 Redis 中的一个基本数据结构,它用于表示 Redis 中的各种数据类型,如字符串、列表、哈希表、集合和有序集合等。redisObject 结构体定义如下:
源码地址 https://github.com/redis/redis/blob/6.0/src/server.h
在这里插入图片描述
我把源码复制出来解析一下

typedef struct redisObject {
    
    
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;
    void *ptr;
} robj;

字段含义:

  • type:表示 redisObject 对象的类型,它是一个 4 位的整数,用于区分不同的数据类型。Redis 中支持多种不同的数据类型,如字符串、列表、哈希表、集合和有序集合等,每种数据类型都有一个对应的 type 值。
  • encoding:表示 redisObject 对象的编码方式,它是一个 4 位的整数,用于区分不同的编码方式。由于 Redis 中的每种数据类型可以用多种不同的编码方式来表示,因此需要使用 encoding 字段来区分不同的编码方式。
  • lru:表示 redisObject 对象的 LRU 时间,它是一个整数,用于记录对象最后一次被访问的时间。LRU(Least Recently Used)算法用于判断哪些对象最近最少使用,从而进行淘汰。
  • refcount:表示 redisObject 对象的引用计数,它是一个整数,用于记录当前对象被引用的次数。当引用计数为 0 时,表示当前对象可以被释放。
  • ptr:表示 redisObject 对象的实际数据,它是一个指针,指向对象的实际数据。不同类型的对象的实际数据类型不同,如字符串对象的实际数据是一个 char 数组,列表对象的实际数据是一个链表等。

1.2 Redis 是如何使用redisObject

Redis 在使用 redisObject 时,主要涉及以下几个方面:

1.2.1. 对象创建

Redis 使用 redisObjectCreate 函数来创建数据对象,并初始化对象的类型、编码方式和实际值等属性。在创建对象时,Redis 会根据实际值的类型来选择适当的编码方式,以提高内存使用效率。
redisObjectCreate 函数的定义和实现位于 Redis 源码的 src/object.c 文件中。
源码位置https://github.com/redis/redis/blob/6.0/src/object.c
还是老办法我们把源码复制出来解析一下

robj *createObject(int type, void *ptr) {
    robj *o = zmalloc(sizeof(*o));
    o->type = type;
    o->encoding = REDIS_ENCODING_RAW;
    o->ptr = ptr;
    o->refcount = 1;
    return o;
}

我们在源码中可以看到,createObject 函数接受两个参数:type 和 ptr,分别表示对象的类型和实际值。在函数内部,它首先使用 zmalloc 函数分配一块内存,大小为 redisObject 结构体的大小。然后,它初始化 redisObject 结构体的成员变量,包括对象类型(type)、编码方式(encoding)、实际值指针(ptr) 和引用计数(refcount)。最后,它返回创建的 redisObject 对象。

声明一下Redis 中还有其他的 redisObjectCreate 函数,用于创建不同类型的数据对象。例如,createStringObject 函数用于创建字符串对象,createListObject 函数用于创建列表对象,createSetObject 函数用于创建集合对象等。这些函数都是在 src/object.c 源码 文件中定义和实现的。
我就不仔细讲了方便大家理解记忆点击上面的链接进去大概扫一遍源码有个基本认知

1.2.2. 对象引用计数

Redis 使用对象引用计数来管理数据对象的生命周期,确保对象在使用过程中不会被意外释放。每个对象都包含一个引用计数(refcount)属性,表示对象当前被引用的次数。在对象被创建时,引用计数初始化为1,当对象被引用时,引用计数加1,当对象被释放时,引用计数减1。当引用计数为0时,对象被释放。所以这个也是JVM 的GC 机制中的一种策略。引用计数法。思想和目的都是一样的,大家可以类比学习。

1.2.3. 对象共享

Redis 中有些数据对象是共享的,例如字符串常量、空列表等。为了节省内存,Redis 使用共享对象来表示这些数据对象,多个变量可以共享同一个对象。Redis 通过将共享对象的引用计数设置为负数来区分共享对象和普通对象。其实如果是java同学看到这块可能会联想到java语言中的一个特性,叫做常量池(Constant Pool)。最常见的字符串常量池(String Pool)专门用于存储字符串常量对象。
Java 中的字符串常量池是一种特殊的内存区域,用于存储字符串常量对象。这些字符串常量对象是在编译期间或运行期间通过字面量(Literal)创建的,例如字符串直接量、字符直接量等。当 Java 程序中出现字符串直接量时,Java 编译器会自动将其添加到字符串常量池中,并在运行时共享这些字符串常量对象,以提高内存使用效率。
那么Redis其实也是这个思想和java的常量池思想类似,以提高内存使用效率 。所以我们可以看到其实设计思想和语言无关,只要在恰当的场景中,能够有效的解决某种问题,就是最优秀的设计思想。其实C++,python,javascript 都使用这种思想,以提高内存使用效率和性能。 所以大家理解和对比记忆很重要,会发现计算机的世界里有的一套思想横行几十年还都是最优解。好了不扯了,咱们继续。

redis预分配的值对象如下:各种命令的返回值,比如成功时返回的OK,错误时返回的ERROR,命令入队事务时返回的QUEUE,等等包括0在内,小于REDIS_SHARED_INTEGERS的所有整数(REDIS_SHARED_INTEGERS的默认值是10000)

Redis预分配的整数对象和字符串值对象的源码位置在Redis的src目录下的以下文件中:

  • redis.h:定义了Redis的整数对象结构体redisObject和字符串值对象结构体robj,以及REDIS_SHARED_INTEGERS宏定义。

  • object.c:定义了Redis的对象操作函数,包括创建、释放、增加和减少引用计数等操作,同时也包括预分配整数对象和字符串值对象的函数initServerSharingObjects()。
    在这里插入图片描述

在initServerSharingObjects()函数中,Redis会预分配一定数量的整数对象和字符串值对象,并将它们缓存在全局共享对象池中。这些对象的数量可以通过在redis.conf配置文件中设置sharedobjects-pool-size选项来调整。默认情况下,REDIS_SHARED_INTEGERS宏定义的值为10000,表示预分配10000个整数对象,而字符串值对象则根据需要预分配一些常见的字符串值对象,如"OK"、"ERROR"和"QUEUE"等。

1.2.4. 对象的编码方式

Redis 支持多种编码方式来表示不同类型的数据对象,例如字符串可以使用 int、embstr 或 raw 编码方式。Redis 会根据实际值的类型和大小来选择适当的编码方式,以提高内存使用效率。在对象创建时,Redis 会自动选择适当的编码方式,并将编码方式保存在对象的编码(encoding)属性中。
对象的 ptr 指针指向对象的底层实现数据结构, 而这些数据结构由对象的 encoding 属性决定。encoding 属性记录了对象所使用的编码, 也即是说这个对象使用了什么数据结构作为对象的底层实现。

/* Objects encoding. Some kind of objects like Strings and Hashes can be
 * internally represented in multiple ways. The 'encoding' field of the object
 * is set to one of this fields for this object. */
// encoding 的10种类型
#define OBJ_ENCODING_RAW 0     /* Raw representation */     //原始表示方式,字符串对象是简单动态字符串
#define OBJ_ENCODING_INT 1     /* Encoded as integer */         //long类型的整数
#define OBJ_ENCODING_HT 2      /* Encoded as hash table */      //字典
#define OBJ_ENCODING_ZIPMAP 3  /* Encoded as zipmap */          //不在使用
#define OBJ_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */  //双端链表,不在使用
#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */         //压缩列表
#define OBJ_ENCODING_INTSET 6  /* Encoded as intset */          //整数集合
#define OBJ_ENCODING_SKIPLIST 7  /* Encoded as skiplist */      //跳跃表和字典
#define OBJ_ENCODING_EMBSTR 8  /* Embedded sds string encoding */   //embstr编码的简单动态字符串
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */   //由压缩列表组成的双向列表-->快速列表

每种类型的对象都至少使用了两种不同的编码,下面列出了每种对象可使用的编码方式。
来自网上
在这里插入图片描述

1.2.4. 对象的值

Redis 使用 redisObject 中的 ptr 属性来指向实际的数据对象,实际数据对象的类型和内容取决于对象的类型和编码方式。例如,对于字符串对象,ptr 指向字符串的字符数组,对于列表对象,ptr 指向列表的头节点。

所以这样来看,我们之前讲过的所有Redis的基本数据类型,其实底层创建的时候基本上都是。Redis 通过使用 redisObject 来统一表示不同类型的数据对象,并使用对象引用计数、共享对象、编码方式和实际值等机制来管理和优化数据对象的使用。而且使用 redisObject 可以帮助 Redis 实现高效的内存管理和数据存储。所以如果面试的花,能简略的答出来这一段内容的关键点,也算是有一定理解了。

2. 总结

redis使用自己实现的对象机制(redisObject)来实现类型判断、命令多态和基于引用次数的垃圾回收;redis会预分配一些常用的数据对象,并通过共享这些对象来减少内存占用,和避免频繁的为小对象分配内存。
最后我们来回答一下上面抛出的问题

2.1. redisObject对象如何实现数据共享和对象池技术?

数据共享技术:Redis 中的字符串对象是可以共享的,如果多个字符串对象的值相同,那么它们可以共享同一个 redisObject 对象。Redis 通过使用共享池来实现字符串对象的共享,共享池中存储了所有的共享字符串对象,每个共享字符串对象都有一个引用计数,当字符串对象不再被使用时,可以将它从共享池中删除。

对象池技术:Redis 中的对象池是用于存储 redisObject 对象的内存池,它可以有效地管理对象的内存,减少内存碎片和内存分配的开销。Redis 中的对象池使用两个栈来管理空闲的 redisObject 对象,一个是大对象栈,用于管理较大的对象,另一个是小对象栈,用于管理较小的对象。当需要分配新的 redisObject 对象时,Redis 会先从对象池中查找是否有空闲对象,如果有,则直接分配给新的对象,如果没有,则从系统中分配新的内存空间。

2.2. redisObject对象的大小是否会随着数据类型的不同而变化

Redis 中的 redisObject 对象的大小会随着数据类型的不同而变化。不同类型的 redisObject 对象所需要的内存空间是不同的,因此它们的大小也会不同。
以字符串对象为例,Redis 中的字符串对象包含一个 len 字段和一个 buf 字段,其中 len 字段表示字符串的长度,buf 字段表示字符串的实际内容。因此,字符串对象的大小等于 len 字段的大小加上 buf 字段的大小。而对于列表对象、哈希表对象、集合对象和有序集合对象等,它们的大小也会随着数据结构的复杂度和元素数量的不同而变化。
需要注意的是,Redis 中的 redisObject 对象还包含一些额外的字段,如 type、encoding、lru 和 refcount 等,这些字段也会占用一定的内存空间。因此,在计算 redisObject 对象的大小时,还需要考虑这些额外的字段所占用的内存空间。

2.3. Redis中的对象池技术如何管理内存?

Redis 中的对象池技术是一种用于管理内存的技术,它可以有效地减少内存碎片和内存分配的开销,提高 Redis 的内存使用效率。对象池技术可以分为两个部分:内存池和空闲对象管理。

内存池:Redis 中的对象池使用一块大的内存空间来存储 redisObject 对象。这个内存空间被称为内存池,它可以分为两个部分:大对象池和小对象池。大对象池用于管理较大的 redisObject 对象,而小对象池用于管理较小的 redisObject 对象。内存池中的每个 redisObject 对象都有一个固定的大小,这样就可以避免内存碎片的产生。

空闲对象管理:Redis 中的对象池使用两个栈来管理空闲的 redisObject 对象,一个是大对象栈,另一个是小对象栈。当需要分配新的 redisObject 对象时,Redis 会先从对象池中查找是否有空闲对象,如果有,则直接分配给新的对象,如果没有,则从系统中分配新的内存空间。

当 redisObject 对象不再被使用时,对象池会将这个对象放回对象池中,以便下次再次使用。这个过程会先将对象的实际数据清空,然后将对象放入对应的空闲对象栈中,同时更新对象的 refcount 字段,将其设置为 0。当空闲对象栈中的对象数量达到一定的阈值时,对象池会释放一部分内存,以便回收内存空间。
这种设计是大多数编程语言都喜欢使用的一种思想。

2.4.Redis中的共享池如何管理共享字符串对象?

共享池是用于管理共享字符串对象的内存池。共享字符串对象是指多个字符串对象的值相同,可以共享同一个 redisObject 对象。Redis通过使用共享池来实现字符串对象的共享,从而提高 Redis 的内存使用效率。 共享池中存储了所有的共享字符串对象,每个共享字符串对象都有一个引用计数。当创建新的字符串对象时,Redis 会先在共享池中查找是否存在相同值的共享字符串对象。如果存在,则将新的字符串对象的指针指向共享字符串对象的地址,并将共享字符串对象的引用计数加 1。如果不存在,则创建新的字符串对象,并将其加入共享池中。 当某个字符串对象不再被使用时,Redis 会将其从共享池中删除,并将其引用计数减 1。如果引用计数变为 0,则表示该字符串对象不再被任何其他对象所引用,可以将其释放并回收其所占用的内存。

这块有个坑 共享池中的每个共享字符串对象都是只读的,就类似于JAVA语言的String对象。如果需要修改一个共享字符串对象的值,那么需要先将其从共享池中拷贝出来,创建一个新的字符串对象,并将其值修改为新的值。这样就可以避免修改共享字符串对象的值对其他对象造成影响。

2.5. 如何判断一个字符串对象是否在共享池中?

可以通过 Redis 的一些命令,如 object encodingobject idletimedebug object 等,来判断一个字符串对象是否在共享池中。如果字符串对象是共享字符串对象,那么 Redis 会返回特定的结果,可以通过这些结果来判断字符串对象是否在共享池中。

3. Redis从入门到精通系列文章

《Redis从入门到精通【进阶篇】之消息传递发布订阅模式详解》
《Redis从入门到精通【进阶篇】之持久化 AOF详解》
《Redis从入门到精通【进阶篇】之持久化RDB详解》
《Redis从入门到精通【高阶篇】之底层数据结构字典(Dictionary)详解》
《Redis从入门到精通【高阶篇】之底层数据结构快表QuickList详解》
《Redis从入门到精通【高阶篇】之底层数据结构简单动态字符串(SDS)详解》
《Redis从入门到精通【高阶篇】之底层数据结构压缩列表(ZipList)详解》
《Redis从入门到精通【进阶篇】之数据类型Stream详解和使用示例》
在这里插入图片描述
大家好,我是冰点,今天的Redis从入门到精通【进阶篇】之对象机制详解,全部内容就是这些。如果你有疑问或见解可以在评论区留言。

猜你喜欢

转载自blog.csdn.net/wangshuai6707/article/details/131556935