「Redis源码解读」—数据结构(六)对象

知识点

  • redis数据库中的每一个键值对的键和值都是一个对象
  • redis共有字符串、列表、哈希、集合、有序集合五种类型的对象,每种类型的对象至少都有两种或以上的编码方式,不同编码可以在不同的使用场景上优化对象的使用效率
  • redis在执行命令之前,会先检查给定键的类型是否能执行指定命令,而检查一个键的类型就是检查键的值对象的类型
  • redis的对象系统带有引用计数实现的内存回收机制,当一个对象不再被使用时,该对象所占用的内存就会被自动释放
  • redis会共享值为0-9999的字符串对象
  • 对象会记录自己的最后一次被访问的时间,这个时间可以用于计算对象的空转时间。

结构体

typedef struct redisObject {  
    // 类型  
    unsigned type:4;          
    // 不使用(对齐位)  
    unsigned notused:2;  
    // 编码方式  
    unsigned encoding:4;  
    // LRU 时间(相对于 server.lruclock)  
    unsigned lru:22;  
    // 引用计数  
    int refcount;  
    // 指向对象的值  
    void *ptr;  
} robj;
  • 编码常量 编码所对应的底层数据结构
    REDIS_ENCODING_INT —— long 类型的整数
    REDIS_ENCODING_EMBSTR —— embstr 编码的简单动态字符串(SDS)
    REDIS_ENCODING_RAW ——简单动态字符串
    REDIS_ENCODING_HT ——字典
    REDIS_ENCODING_LINKEDLIST ——双端链表
    REDIS_ENCODING_ZIPLIST ——压缩列表
    REDIS_ENCODING_INTSET—— 整数集合
    REDIS_ENCODING_SKIPLIST ——跳跃表

字符串对象

字符串对象的编码可以是int、raw或者embstr。
如果一个字符串的内容可以转换为long,那么该字符串就会被转换成为long类型,对象的ptr就会指向该long,并且编码类型也用int类型表示。
普通的字符串有两种,embstr和raw。embstr编码是由redIsObject和sdshdr组成,sdshdr结构体如下

struct sdshdr {
    unsigned int len;
    unsigned int free;
    char buf[];
}

redIsObject占用16字节,如果buf长度是39个字节,那么sdshdr就是8+39+1=48个字节,那么embstr就是64字节,而redis采用的是jemalloc内存分配器,可以分配8,16,32,64字节等大小的内存,而sdshdr最小分配8+8+1=17字节,那么embstr最小就是33字节,需要分配64字节。所以对于redis来说小于等于39字节的字符串采用embstr编码,大于则用raw编码。

  • embstr好处:
    1.embstr的创建只需分配一次内存,而raw为两次(一次为sds分配对象,另一次为objet分配对象,embstr省去了第一次,直接分配一块连续的内存)。
    相对地,释放内存的次数也由两次变为一次。
    2.embstr的objet和sds放在一起,更好地利用缓存带来的优势。
    3.释放embstr编码的字符串对象只需要调用一次内存释放函数,而释放raw编码的字符串对象需要条用两次内存释放函数
    4.因为embstr编码的字符串对象的所有数据都保存在一块连续的内存里面,所有这种编码的字符串对象比起raw编码的字符串对象能够更好的利用缓存带来的优势。
    需要注意的是,redis并未提供任何修改embstr的方式,即embstr是只读的形式。对embstr的修改实际上是先转换为raw再进行修改。

列表对象

  • 列表对象的编码可以是ziplist或者linkedlist。

ziplist是一种压缩链表,它的好处是更能节省内存空间,因为它所存储的内容都是在连续的内存区域当中的。当列表对象元素不大,每个元素也不大的时候,就采用ziplist存储。但当数据量过大时就ziplist就不是那么好用了。因为为了保证他存储内容在内存中的连续性,插入的复杂度是O(N),即每次插入都会重新进行realloc。

另一方面,linkedlist编码的列表对象使用双端链表作为底层实现,每个双端链表节点Node都保存了一个字符串对象,而每个字符串对象都保存了一个列表元素

编码转换

当列表对象可以同时满足以下两个条件时,列表对象使用ziplist编码
1.列表对象保存的所有字符串元素的长度都小于64字节
2.列表对象保存的元素数量小于512个,不能满足这两个条件的列表对象需要使用linkedlist编码

哈希对象

  • 哈希对象的底层实现可以是ziplist或者hashtable。

ziplist中的哈希对象是按照key1,value1,key2,value2这样的顺序存放来存储的。当对象数目不多且内容不大时,这种方式效率是很高的。

编码转换

当哈希对象可以同时满足以下两个条件时,哈希对象使用ziplist编码
1.哈希对象保存的所有键值对的键和值的字符串长度都小于64字节
2.哈希对象保存的键值对数量小于512个,不能满足这两个条件的哈希对象需要使用hashtable编码

集合对象

  • 集合对象的编码可以是intset或者hashtable。
    intset是一个整数集合,里面存的为某种同一类型的整数,支持如下三种长度的整数:
#define INTSET_ENC_INT16 (sizeof(int16_t))  #define INTSET_ENC_INT32 (sizeof(int32_t))  #define INTSET_ENC_INT64 (sizeof(int64_t))  

intset是一个有序集合,查找元素的复杂度为O(logN),但插入时不一定为O(logN),因为有可能涉及到升级操作。比如当集合里全是int16_t型的整数,这时要插入一个int32_t,那么为了维持集合中数据类型的一致,那么所有的数据都会被转换成int32_t类型,涉及到内存的重新分配,这时插入的复杂度就为O(N)了。是intset不支持降级操作。

编码转换

当集合对象可以同时满足以下两个条件时,集合对象使用intset编码
1.集合对象保存的所有元素都是整数值
2.集合对象保存的元素不超过512,不能满足这两个条件的哈希对象需要使用hashtable编码

有序集合对象

  • 有序集合的编码可能两种,一种是ziplist,另一种是skiplist
    ziplist作为集合和作为哈希对象是一样的,member和score顺序存放。按照score从小到大顺序排列。它的结构不再复述。
    skiplist是一种跳跃表,它实现了有序集合中的快速查找,在大多数情况下它的速度都可以和平衡树差不多。但它的实现比较简单,可以作为平衡树的替代品。它的结构比较特殊。
编码转换

1.有序集合保存的元素数量小于128个
2.有序集合保存的所有元素成员的长度都小于64字节
不能满足以上两个条件的有序集合对象将使用skiplist编码

内存回收

redis在自己的对象系统中构建一个引用计数(reference counting)技术实现的内存回收机制,通过这一机制,程序可以通过跟踪对象的引用计数信息,在适当的时候自动释放对象并进行内存回收。

  • 对象的引用计数信息会随着对象的使用状态而不断变化
    1.在创建一个新对象时,引用计数的值会被初始化为-1;
    2.当对象被一个新程序使用时,它的引用计数器会被+1;
    3.当对象不再被一个程序使用时,它的引用计数值会被-1;
    4.当对象的引用计数值变为0时,对象所占用的内存会被释放;

对象共享

假设键A于键B都创建了一个包含整数100的字符串对象作为值对象时,redis将会通过让键A和键B共享同一个字符串对象节约内存。

  • 在redis中让多个键共享一个值对象需要执行以下两个步骤:
    1.将数据库键的值指针指向一个现有对象;
    2.将被共享的值对象的引用计数器增1;

对象空转时长

redisObject结构包含的最后一个属性为lru属性,该属性记录了对象最后一次被命令程序访问的时间;
如果redis打开了maxmemory选项,那么当redis占用的内存数超过了maxmemory选项所设置的上限时,空转时长较高的那部分键会被优先被释放,从而回收内存

你可能感兴趣的:(「Redis源码解读」—数据结构(六)对象)