Redis 5种数据类型的内部数据结构(一)

Redis对象 RedisObject
Redis 并没有直接使用以上的数据结构来实现键值对的数据库,而是在这些数据结构上又包装了一层RedisObject(对象),RedisObject 有5中对象 字符串对象,列表对象,哈希对象 集合对象和有序集合合对象。
优点主要有2个,分别是:
1 通过不同类型的对象,Redis 可以在执行命令之前,根据对象的类型来判断一个对象是否可以执行给定的命令。
2 我们可以针对不同的使用场景,为对象设置不同的实现,从而优化内存或查询速度。
来看看它长什么样?
typedef struct redisObject {
// 类型
unsigned type:4;
// 编码
unsigned encoding:4;
// 对象最后一次被访问的时间 /*
lru time ( relative to server.lruclock)
unsigned lru:REDIS_LRU_BITS;
// 引用计数
int refcount;
// 指向实际值的指针
void *ptr;
} robj
解释说明:
A type 记录了对象的类型 一共5种即
  1. REDIS_STRING 字符串对象
  2. REDIS_LIST 列表对象
  3. REDIS_HASH 哈希对象
  4. REDIS_SET 集合对象
  5. REDIS_ZSET 有序结合对象
tips:对于 Redis 数据库保存的键值对来说,键一定是1个字符串对象,而值则可以使5种对象的其中1种。
B encoding 表示ptr 指向具体的数据结构,即这个对象使用了什么数据结构来作为底层实现。
encoding 有以下几种取值:
编码常量
编码所对应的底层数据结构
REDIS_ENCODING_INT
long类型的整数
REDIS_ENCODING_EMBSTR
embstr 编码的简单动态字符串(sds)
REDIS_ENCODING_RAW
简单动态字符串
REDIS_ENCODING_HT
hashtable 字典
REDIS_ENCODING_LINKEDLIST
双端链表
REDIS_ENCODING_ZIPLIST
压缩列表
REDIS_ENCODING_INTSET
整数集合
REDIS_ENCODING_SKIPLIST
跳跃表和字典
每种类型的对象都至少使用了两种不同的编码,对象和编码的对应关系如下
类型
编码
对象
REDIS_STRING
REDIS_ENCODING_INT
使用整数值实现的字符串对象
REDIS_STRING
REDIS_ENCODING_EMBSTR
embstr 编码的简单动态字符串,sds <=44字节
REDIS_STRING
REDIS_ENCODING_RAW
使用简单动态字符串实现的字符串对象 >44字节
REDIS_LIST
REDIS_ENCODING_ZIPLIST
使用压缩列表实现的列表对象
REDIS_LIST
REDIS_ENCODING_LINKEDLIST
使用双端链列表实现的列表对象
REDIS_HASH
REDIS_ENCODING_ZIPLIST
使用压缩列表实现的哈希对象
REDIS_HASH
REDIS_ENCODING_HT
使用字典实现的哈希对象
REDIS_SET
REDIS_ENCODING_INTSET
使用整数集合实现的集合对象
REDIS_SET
REDIS_ENCODING_HT
使用字典实现的集合对象
REDIS_ZSET
REDIS_ENCODING_ZIPLIST
使用压缩列表实现有序集合对象
REDIS_ZSET
REDIS_ENCODING_SKIPLIST
使用跳跃表和字典实现的有序集合对象
tips: sds 长度问题跟redis版本有关 3,2之前为39字节,3.2之后变成44字节。
C ptr: 指针 指向对象的底层实现数据结构;
D refcount
refcount 表示引用计数,由于 C 语言并不具备内存回收功能,所以 Redis 在自己的对象系统中添加了这个属性,当一个对象的引用计数为0时,则表示该对象已经不被任何对象引用,则可以进行垃圾回收了
refcount 与共享对象: refcount 记录的是该对象被引用的次数,类型为整型。refcount 的作用,主要在于对象的引用计数和内存回收。
当创建新对象时,refcount 初始化为 1;
当有新程序使用该对象时,refcount 加 1;
当对象不再被一个新程序使用时,refcount 减 1;
当 refcount 变为 0 时,对象占用的内存会被释放。
Redis 中被多次使用的对象(refcount>1),称为共享对象。Redis 为了节省内存,当有一些对象重复出现时,新的程序不会创建新的对象,而是仍然使用原来的对象。
这个被重复使用的对象,就是共享对象。目前共享对象仅支持整数值的字符串对象。
共享对象的具体实现: Redis 的共享对象目前只支持整数值的字符串对象。之所以如此,实际上是对内存和 CPU(时间)的平衡:共享对象虽然会降低内存消耗,但是判断两个对象是否相等却需要消耗额外的时间。
对于整数值,判断操作复杂度为 O(1);
对于普通字符串,判断复杂度为 O(n);
而对于哈希、列表、集合和有序集合,判断的复杂度为 O(n^2)。
虽然共享对象只能是整数值的字符串对象,但是5种类型都可能使用共享对象(如哈希、列表等的元素可以使用)。
就目前的实现来说,Redis 服务器在初始化时,会创建 10000 个字符串对象,值分别是 0~9999 的整数值;当 Redis 需要使用值为 0~9999 的字符串对象时,可以直接使用这些共享对象。
10000 这个数字可以通过调整参数 REDIS_SHARED_INTEGERS(4.0 中是 OBJ_SHARED_INTEGERS)的值进行改变。
共享对象的引用次数可以通过 object refcount 命令查看。

E   lru:表示对象最后一次被命令程序访问的时间。可以通过指令 object idletime key 来查看。

通过对比 lru 时间与当前时间,可以计算某个对象的空转时间;object idletime 命令可以显示该空转时间(单位是秒)。object idletime 命令的一个特殊之处在于它不改变对象的 lru 值。

分别介绍一下5种对应的RedisObject
1 字符串对象(string 类型) 内部数据结构
字符串对象的encoding有3钟,分别是int,raw,embstr
1. 如果一个字符串的对象保存的是整数值,并且这个整数值可以用long类型标识,那么字符串对象会将整数保存在ptr属性中,并将encoding 设置为int。例如 set jack 10086

2. 如果字符串对象保存的是1个字符串值,并且这个字符串的长度大于44 字节 那么字符串对象将使用一个简单动态字符串(SDS)来保存着个字符串值,并将对象的编码设置为raw。
栗子: set jack veryhandsome
 设置一个key= jack ,value = veryhandsome 的新键值对,他们底层是数据结构将会是:
     键(key)是一个字符串对象,对象的底层实现是一个保存着字符串 jack 的SDS;
     值(value)也是一个字符串对象,对象的底层实现是一个保存着字符串veryhandsome 的SDS
tips: SDS还被用作缓冲区(buffer)AOF模块中的AOF缓冲区。
3 如果字符串对象保存的是一个字符串值,并且这个字符串的长度小于等于32字节,那么字符串对象将使用embstr编码的方式来保存这个字符串。
对于每个sds都有一个sdshdr,来看下这个神奇的sdshdr,新版的redis 有不同的sdshdr,
sdshdr5,sdshdr8,sdshdr16,sdshdr32,sdshdr64 这样的内存优化,更方便小sds的内存使用不会造成内存浪费。
struct sdshdr{ //记录buf数组中已使用字节的数量 //等于 SDS 保存字符串的长度 int len; //记录 buf 数组中未使用字节的数量 int free; //字节数组,用于保存字符串 char buf[]; };
来说一下为什么会有 encoding不同的这种现象
struct RedisObject { int4 type; // 4bits int4 encoding; // 4bits int24 lru; // 24bits int32 refcount; // 4bytes void *ptr; // 8bytes,64-bit system } robj;
不同的对象具有不同的类型 type(4bit),同一个类型的 type 会有不同的存储形式 encoding(4bit),为了记录对象的 LRU 信息,使用了 24 个 bit 来记录 LRU 信息。每个对象都有个引用计数,当引用计数为零时,对象就会被销毁,内存被回收。ptr 指针将指向对象内容 (body) 的具体存储位置。这样一个 RedisObject 对象头需要占据 16 字节的存储空间
接着我们再看 SDS 结构体的大小,在字符串比较小时,SDS 对象头的大小是capacity+3,至少是 3。意味着分配一个字符串的最小空间占用为 19 字节 (16+3)
struct SDS {
int8 capacity; // 1byte
int8 len; // 1byte
int8 flags; // 1byte
byte[] content; // 内联数组,长度为 capacity
}

如图所示,embstr 存储形式是这样一种存储形式,它将 RedisObject 对象头和 SDS 对象连续存在一起,使用 malloc 方法一次分配。而 raw 存储形式不一样,它需要两次 malloc,两个对象头在内存地址上一般是不连续的。
而内存分配器 jemalloc/tcmalloc 等分配内存大小的单位都是 2、4、8、16、32、64 等等,为了能容纳一个完整的 embstr 对象,jemalloc 最少会分配 32 字节的空间,如果字符串再稍微长一点,那就是 64 字节的空间。如果总体超出了 64 字节,Redis 认为它是一个大字符串,不再使用 emdstr 形式存储,而该用 raw 形式
当内存分配器分配了 64 空间时,那这个字符串的长度最大可以是多少呢?这个长度就是 44。那为什么是 44 呢
前面我们提到 SDS 结构体中的 content 中的字符串是以字节\0结尾的字符串,之所以多出这样一个字节,是为了便于直接使用 glibc 的字符串处理函数,以及为了便于字符串的调试打印输出

看上面这张图可以算出,留给 content 的长度最多只有 45(64-19) 字节了。字符串又是以\0结尾,所以 embstr 最大能容纳的字符串长度就是 44。所以长度超出了44之后就用raw来实现了
字符串的扩容策略:字符串在长度小于 1M 之前,扩容空间采用加倍策略,也就是保留 100% 的冗余空间。当长度超过 1M 之后,为了避免加倍后的冗余空间过大而导致浪费,每次扩容只会多分配 1M 大小的冗余空间。
因为既生瑜何生亮呢?
embstr 的编码方式有一些优点,如下:
  • embstr 编码将创建字符串对象所需的内存分配次数从 raw 编码的两次降低为一次。
  • 释放 embstr 编码的字符串对象只需要调用一次内存释放函数,而释放 raw 编码的字符串对象需要调用两次内存释放函数。
  • 因为 embstr 编码的字符串对象的所有数据都保存在一块连续的内存里面,所以这种编码的字符串对象比起 raw ,编码的字符串对象能够更好地利用缓存带来的优势。
参考链接:

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注