Redis学习之对象系统源码分析

背景知识:

Redis并没有直接使用sds,双端链表,字典,压缩列表,跳表等这些数据结构来直接实现键值对数据库,而是基于这些对象创建了一个对象系统,这个对象系统包含5个对象:字符串对象,列表对象,哈希对象,集合对象和有序集合对象,字符串对象是唯一会被其他四种对象嵌套的对象

1.我们可以针对不同的使用场景,为对象设置多种不同的数据结构,从而优化对象在不同场景下的使用效率

2.Redis的对象系统实现了基于引用计数的内存回收机制

3.Redis的对象系统还实现了对象共享机制,这个机制在适当条件下,通过让多个数据库键共享同一个对象来节约内存

4.Redis的对象带有访问时间记录信息,该信息可以用来计算数据库键的空转时长,在服务器启用了maxmemory功能的情况下,空转时长较大的那些键可能会被服务器优先删除

5.当我们在redis中创建一个新的键值对时,我们至少会创建两个对象,一个对象用作键,另外一个对象用作值

 

一.Redis对象系统

typedef struct redisObject {

    // 类型,键值对中值的类型
    unsigned type:4;

    // 编码,决定采用什么类型的数据结构实现
    unsigned encoding:4;

    // 对象最后一次被访问的时间
    unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */

    // 引用计数
    int refcount;

    // 指向实际值的指针
    void *ptr;

} robj;

通过encodeing属性来设定对象所使用的编码,而不是为特定类型的对象关联一种固定的编码,极大的提升了Redis的灵活性和效率,因为Redis可以更具不同的使用场景来为对象设置一个不同的编码,从而优化对象在某一场景下的效率

比如下面这种情况:

在列表对象包含较少元素时,Redis使用压缩列表作为列表对象的底层实现,因为压缩列表比起双端列表更节约内存,并且在元素数量较少时,在内存中以连续块的方式保存压缩列表比起双端链表可以更快的载入到缓存中,但是!随着元素的增加,使用压缩列表带来的优势越来越小,对象系统就会将列表的底层实现转向功能更强,也更适合保存大佬元素的双端列表上,其他类型对象也会通过使用多种不同的编码来进行类似的优化

 

下面是 不同对象类型对应的不同编码:

                  |-- 1.使用整数值实现的字符串对象
                  |
字符串对象: |-- 2.使用embstr编码的SDS简单动态字符串实现的字符串对象
                  |
                  |-- 3.使用SDS简单动态字符串实现的字符串对象
           
               |--1.使用ziplist压缩列表实现的列表对象
列表对象: |
               |--2.使用list双端链表实现的列表对象        
           
               |--1.使用ziplist压缩列表实现的哈希对象
哈希对象: |
               |--2.使用dict字典实现的哈希对象


               |--1.使用intset整数集合实现的集合对象
集合对象: |
               |--2.使用dict字典实现的集合对象
           
                    |--1.使用ziplist压缩列表实现的有序集合对象
有序集合对象:|
                    |--2.使用跳跃表和字典实现的有序集合对象

 

 

1.字符串对象

字符串对象的编码可以是int,raw,embstr

1)当字符串对象保存的是整数值时,字符串对象的编码是int

2)当字符串对象保存的是字符串时,并且这个字符串的长度大于39字节,那么字符串对象的编码是raw

3)当字符串对象保存的是字符串时,并且这个字符串的长度小于等于39字节,那么字符串对象的编码是embstr

graphviz-c0ba08ec03934562687cc3cb79580e76edef81e3

graphviz-8ab5e59accdfa496f966cf90b45b146fb4959335graphviz-900c13b23ce79372939259603be936c955ccaa62

说明:

embstr编码是专门用于保存短字符串的一种优化编码方式

embstr编码和raw编码的区别:

raw编码会调用两次内存分配函数来分别创建redisobject和sdsstr结构,而embstr编码则通过调用一次内存分配函数来分配一块连续的空间,空间中依次包含redisobject和sdsstr两个结构

使用embstr编码来保存短字符串值有以下两个好处:

1)调用内存分配函数的次数减少了一次,并且调用内存释放函数的次数也减少了一次

2)embstr编码的字符串对象的所有数据都保存在一块连续的内存中,这样可以更好的利用缓存带来的优势、

ps:浮点类型的数据也是通过字符串值来保存的

编码可以进行特定的相互转换

 

2.列表对象

1.列表对象的编码可以是ziplist压缩列表或者linkedlist链表

graphviz-a8d31075b4c0537f4eb6d84aaba1df928c67c953

graphviz-84c0d231f30c740a431407c7aaf3851b96399590

问题:列表对象什么时候采用ziplist压缩列表编码,什么时候采用linkedlist链表编码?

当列表对象满足下面两个条件的时候,采用ziplist压缩列表进行编码

1)列表对象保存的所有字符串元素长度小于64字节

2)列表保存的元素数量小于512个

若不能通过满足上面两个条件的话列表将采用双端链表编码

 

3.哈希对象

哈希对象的编码可以是ziplist压缩列表或hashtable哈希表

 

当哈希对象采用ziplist压缩列表进行编码的时候:

保存了同一键值对的两个结点总是紧紧挨在一起,保存键的结点在前,保存值的结点在后

graphviz-d2524c9fe90fb5d91b5875107b257e0053794a2a

graphviz-7ba8b1f3af17e2e62cdf43608914333bf14d8e91

当哈希对象满足下面两个条件时,哈希对象采用ziplist压缩列表进行编码:

1)哈希对象保存的键值对的字符串长度都小于64字节

2)噶厦镀锡保存的键值对数量小于512个

当不满足上面两个条件时会进行编码转换采用hashtable进行编码

graphviz-68cb863d265a1cd1ccfb038d44ce6b856ebbbe3a

 

4.集合对象

集合对象的编码可以是intset整数集合或者hashtable哈希表

graphviz-fbd8f0e1aaad0bdef314af55d01212f83cba8b59

graphviz-3f77c5cca338422f418d6d11bc02109fc945e790
当集合对象满足下面两个条件时,集合对象采用intset编码:

1)集合对象保存的所有元素都是整数值

2)集合对象保存的元素数量不超过512个

如果不满足上面两个条件,集合对象将采用hashtable哈希表进行编码

 

5.有序集合对象

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

ziplist编码:ziplist结构

skiplist编码:skiplist结构和dict结构

graphviz-61b04c9bb72915ec0374125ba9455bc6783db4ff

graphviz-243db42f3ae9ad5bb64108c999f7ce7144f166a6

graphviz-122e7ebdcd23e888fae17c21813be048c2d3f0a8

graphviz-75ee561bcc63f8ea960d0339768aec97b1f570f0

当有序集合对象满足下面两个条件时,有序集合对象采用ziplist编码

1)有序集合保存的元素数量小于128个

2)有序集合保存的所有元素成员长度小于64字节

否则的话,有序集合对象将采取skiplist编码,底层用skiplist和dict数据结构实现

 

关于有序集合对象采用skiplist编码的说明:

在采用skiplist编码的时候,其底层采用的是skiplist跳表和dict字典

1.通过跳跃表,有序集合可以高效的进行范围性操作

2.通过字典,有序集合可以高效的查找成员到分值的映射

值得一提的是,虽然有序集合对象在采用skiplist编码的时候,同时采用了跳跃表和字典来保存原始,但是着两种数据结构都会通过指针来共享相同原始的成员和分值,不会因此而浪费额外的内存来保存相同元素

 

为什么有序集合在采用skiplist编码的时候,需要通过采用跳表和字典来进行底层的实现?

1)采用字典,不采用跳表:虽然查找成员到分值的映射的复杂度为O(1),但是执行比如ZRANK命令时,程序需要对字典保存的所有元素进行排序,至少需要O(log N),以及额外的O(N)内存空间

2)采用跳表,不采用字典:虽然执行范围性操作的时间复杂度是O(log N),但是根据成员查找映射的分值的操作的复杂度将从O(1)上升到O(log N)

所以为了让有序集合的查找和范围性操作都尽可能的快速执行,Redis选择同时使用字典和跳跃表两种数据结构来实现有序集合,同时有序集合中的字典和跳跃表会共享元素的成员和分值,所以并不会造成数据重复,也不会因此浪费任何内存

 

Redis的内存回收

1.Redis采用引用计数法进行内存回收,该方法最大的问题就是不能解决互相引用的问题

2.对象的空转时长:当前时间减去该对象最后一次被访问的时间就是该对象的空转时长,空转时长较长的对象在内存不够时会被回收

 

Redis的对象共享

采用对象共享机制是为了节约内存,数据库保存的相同值的对象越多,对象共享机制就节约越多的内存

目前来说,Redis会在初始化服务器时,创建一万个字符串对象,这些对象包含了从0到9999的所有整数值,当服务器需要用到值为0到9999的字符串对象时,服务器就会共享这些对象,而不是创建新对象

需要注意的是

尽管共享复杂的对象可以节约更多的内存,但是复杂的对象比较起来也比较耗时间,比如如果共享的对象包含了多个值的对象,那么比较两个对象的复杂度为O(N*N),这个比较操作非常耗费CPU时间,所以收到CPU时间的现在,Redis只多包含整数值的字符串对象进行共享

 

总结:

1)Redis数据节中每个键值对的键和值都是一个对象

2)Redis共有字符串对象,列表对象,哈希对象,集合对象,有序集合对象,这五种对象,每种类型的对象至少都有两种以上的编码方式,不同的编码方式可以在不同的使用场景上使用,可以优化对象的使用效率

3)服务器在执行某些命令前,需要先检查给定的键是否可以执行指定的命令,而检查一个键的类型就是检查键对应的值的对象类型

 

 

posted @ 2019-07-28 13:40  西*风  阅读(329)  评论(0编辑  收藏  举报