我是靠谱客的博主 体贴马里奥,最近开发中收集的这篇文章主要介绍三、Redis数据存储的细节(重点)jemallocredisObjectSDS k-v,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

        Redis是一个K-V NOSQL

        五种类型 都是针对K-V中的V的

        下图是执行set hello world时,所涉及到的数据模型。

(1)dictEntry:Redis是Key-Value数据库,因此对每个键值对都会有一个dictEntry,里面存储了指向Key和Value的指针;next指向下一个dictEntry,与本Key-Value无关。

(2)Key:图中右上角可见,key(hello)并不是直接以字符串存储,而是存储在SDS结构中。

(3)redisObject:Value("world")既不是直接字符串存储,也不像Key一样直接存储在SDS中,而是存储在redisObject中。实际上,不论Value是五种类型的哪一种,都是通过redisObject来存储的;而redisObject中的type字段指明了Value对象的类型,ptr字段则指向对象所在的地址。不过可以看出,字符串对象虽然经过了redisObject的包装,但仍需要通过SDS存储。

(4)jemalloc:无论是DictEntry对象,还是redisObject、SDS对象,都需要内存分配器(如jemlloc)分配内存进行存储。

jemalloc

        Redis在编译时便会指定内存分配器;内存分配器可以是libc、jemalloc或者tcmalloc,默认是jemalloc。

        jemalloc作为Redis的默认内存分配器,在减小内存碎片方面做的相对比较好。jemalloc在64位系统中,将内存空间划分为小、大、巨大三个范围;每个范围内又划分了许多小的内存块单位;当Redis存储数据时,会选择大小最合适的内存块进行存储。

        jemalloc划分的内存单元如下图所示:

9a2ac7ba1173bc5c5df96ab11e805402.png

        需要9 16 16-9=7

        这样划分:既能保证充分利用内存空间,又能保证我们性能使用

        例如,如果需要存储大小为130字节的对象,jemalloc会将其放入160字节的内存单元中。

redisObject

        Redis对象有五种类型;无论是那种类型,Redis都不会直接存储,而是通过redisObject对象进行存储。redisObject对象非常重要,Redis对象的类型、内部编码、内存回收、共享对象等功能,都需要Redis中的每个对象都是由如下结构表示(列出了与保存数据有关的三个属性)

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

(1)type

        type字段表示对象的类型,占4个bit;目前包括REDIS STRING(字符串)、REDIS LIST(列表)、REDIS HASH(哈希)、REDIS SET(集合)、REDIS ZSET(有序集合)。

        当我们执行type命令时,便是通过读取RedisObject的type字段获得对象的类型;如下图所示:

(2)encoding

        encoding表示对象的内部编码,占4个bit。

        对于Redis支持的每种类型,都有至少两种内部编码,例如对于字符串,有int、embstr、raw三种编码。通过encoding属性,Redis可以根据不同的使用场景来为对象设置不同的编码,大大提高了Redis的灵活性和效率。以列表对象为例,有压缩列表和双端链表两种编码方式;如果列表中的元素较少,Redis倾向于使用压缩列表进行存储,因为压缩列表占用内存更少,而且比双端链表可以更快载入;当通过object encoding命令,可以查看对象采用的编码格式,如下图所示:

 569521999c357c76a6fc6cc822e98c9e.png

        五种对象类型对应的编码方式以及使用条件,将在后面介绍。

(3)ptr

        ptr指针指向具体的数据,如前面的例子中,set hello world,ptr指向包含字符串world的SDS。

(4)refcount

        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命令查看,如下图所示。命令执行的结果页佐证了只有 0~9999之间的整数会作为共享对象。

(5)lru

        l ru记录的是对象最后一次被命令程序访问的时间,占据的比特数不同的版本有所不同(2.6版本占22比特,4.0版本占24比特)。

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

         lru值除了通过object idletime命令打印之外,还与Redis的内存回收有关系:如果Redis打开了maxmemory选项,且内存回收算法选择的是volatile-lru或allkeys—lru,那么当Redis内存占用超过maxmemory指定的值时,Redis会优先选择空转时间最长的对象进行释放。

(6)总结

        综上所述,redisObject的结构与对象类型、编码、内存回收、共享对象都有关系;一个redisObject对象的大小为16字节:

        4bit+4bit+24bit+4Byte+8Byte=16Byte

SDS k-v

        Redis没有直接使用C字符串(即以空字符’’结尾的字符数组)作为默认的字符串表示,而是使用了SDS。SDS是简单动态字符串(Simple Dynamic String)的缩写。

        ArrayList

        3.2之前

struct sdshdr{ 
    //记录buf数组中已使用字节的数量 
    //等于 SDS 保存字符串的长度 
    int len; 
    //记录 buf 数组中未使用字节的数量 
    int free; 
    //字节数组,用于保存字符串 
    char buf[]; 
}

        其中,buf表示字节数组,用来存储字符串;len表示buf已使用的长度,free表示buf未使用的长度。下面是两个例子。

        通过SDS的结构可以看出,[buf数组的长度=free+len+1(其中1表示字符串结尾的空字符);所以,一个SDS结构占据的空间为:[free所占长度+len所占长度+ buf数组的长度+1=4+4+字符串长度+1=字符串长度+9]()。

        3.2之后

typedef char *sds; 
struct __attribute__ ((__packed__)) sdshdr5 { 
    // 对应的字符串长度小于 1<<5 32字节
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length int embstr*/ 
    char buf[]; 
};
struct __attribute__ ((__packed__)) sdshdr8 { 
    // 对应的字符串长度小于 1<<8 256 
    uint8_t len; /* used */ //目前字符创的长度 用1字节存储 
    uint8_t alloc; //已经分配的总长度 用1字节存储 
    unsigned char flags; //flag用3bit来标明类型,类型 后续解释,其余5bit目前没有使用 embstr raw 
    char buf[]; //柔性数组,以''结尾 };
    struct __attribute__ ((__packed__)) sdshdr16 { // 对应的字符串长度小于 1<<16 
        uint16_t len; /*已使用长度,用2字节存储*/ 
        uint16_t alloc; /* 总长度,用2字节存储*/ 
        unsigned char flags; /* 3 lsb of type, 5 
        unused bits */ 
        char buf[]; 
    };
    struct __attribute__ ((__packed__)) sdshdr32 { // 对应的字符串长度小于 1<<32 
        uint32_t len; /*已使用长度,用4字节存储*/ 
        uint32_t alloc; /* 总长度,用4字节存储*/ 
        unsigned char flags;/* 低3位存储类型, 高5位预留 */ 
        char buf[];/*柔性数组,存放实际内容*/ 
    };
    struct __attribute__ ((__packed__)) sdshdr64 { // 对应的字符串长度小于 1<<64 
        uint64_t len; /*已使用长度,用8字节存储*/ 
        uint64_t alloc; /* 总长度,用8字节存储*/ 
        unsigned char flags; /* 低3位存储类型, 高5位预留 */ 
        char buf[];/*柔性数组,存放实际内容*/ 
    }; 

87458bae48314f53355c95d3d99ed722.png

SDS与c字符串的比较

  • 获取字符串长度:SDS是O(1),C字符串是O(n)
  • 缓冲区溢出:使用C字符串的API时,如果字符串长度增加(如strcat操作)而忘记重新分配内存,很容易造成缓冲区的溢出;而SDS由于记录了长度,相应的API在可能造成缓冲区溢出时会自动重新分配内存,杜绝了缓冲区溢出。
  • 修改字符串时内存的重分配:对于C字符串,如果要修改字符串,必须要重新分配内存(先释放再申请),因为如果没有重新分配,字符串长度增大时会造成内存缓冲区溢出,字符串长度减小时会造成内存泄露。而对于SDS,由于可以记录len和free,因此解除了字符串长度和空间数组长度之间的关联,可以在此基础上进行优化:空间预分配策略(即分配内存时比实际需要的多)使得字符串长度增大时重新分配内存的概率大大减小;惰性空间释放策略使得字符串长度减小时重新分配内存的概率大大减小。
  • 存取二进制数据:SDS可以,C字符串不可以。因为C字符串以空字符作为字符串结束的标识,而对于一些二进制文件(如图片等),内容可能包括空字符串,因此C字符串无法正确存取;而SDS以字符串长度len来作为字符串结束标识,因此没有这个问题。此外,由于SDS中的buf仍然使用了C字符串(即以’’结尾),因此SDS可以使用C字符串库中的部分函数;但是需要注意的是,只有当SDS用来存储文本数据时才可以这样使用,在存储二进制数据时则不行(’’不一定是结尾)。

        ArrayList length = alloc size= len

最后

以上就是体贴马里奥为你收集整理的三、Redis数据存储的细节(重点)jemallocredisObjectSDS k-v的全部内容,希望文章能够帮你解决三、Redis数据存储的细节(重点)jemallocredisObjectSDS k-v所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(78)

评论列表共有 0 条评论

立即
投稿
返回
顶部