美文网首页面试精选Redis系列教程
2. 浅析Redis底层数据结构

2. 浅析Redis底层数据结构

作者: Vander1991 | 来源:发表于2021-03-16 17:11 被阅读0次

    概要
    1)Redis中的字符串-sds
    2)Redis中的HashMap-dict
    3)dict的渐进式rehash
    4)Redis的5种对象底层剖析

    2.1 Redis中的字符串-sds

    众所周知,Redis是使用C语言实现的,C语言底层存放string是使用char数组,并且在最后会加上'\0'作为string的结束标志。

    C语言存放"Vander" - 》 char[] str = "Vander\0"(此时字符串的length为6byte,但是占据7byte的空间,‘\0’不算在字符串长度里)

    问题一:为什么Redis不直接跟C语言一样存放string?

    答:因为如果我想存放'Vander\0Jason'就会产生误判,提前结束,这样只会读取到“Vander”,后面的“Jason”就被截取掉了。

    由于Redis的Key都是字符串,所以先查看Redis底层的数据结构sds

    问题二:如何减少C语言中的string修改字符串时带来的内存重分配

    C字符串的内存重分配.png

    由于C字符串不记录长度,所以每次增减字符都要对保存的C字符串的数组进行内存重分配

    Redis为了解决增加字符数,频繁重分配的问题,会进行内存预分配,来减少由于再次增加字符而导致的内存重分配的问题。即,假设原有sds存放char[] = 'V','a','n','d','e','r','\0',需要存放"Vander&Jason",由于此字符串占12byte,所以会预分配25byte(12*2+1,'\0'占1byte)。但是在大于1MB之后,每次新增不会按照两倍扩展了,只能会再分配多1MB。

    惰性空间释放.png

    为了解决缩少字符数,导致的内存泄漏的问题,通过调整free来释放惰性空间,通过free来说明当前数组还有剩余空间可以使用。

    从Redis设计的sds来看,有三个特点:
    1)二进制安全的数据结构(不直接用'\0'作为判断结束的标志,而是加上len的标识)
    2)提供内存预分配机制,避免了频繁的内存分配
    3)兼容C语言的函数库(数组结尾还是使用'\0'兼容原有的C语言规范)

    2.2 Redis中的HashMap-dict

    由于C语言中没有实现字典类型的数据结构,所以Redis提供了它的实现,字典可以理解成Java中的HashMap,功能与之类似。当一个hash类型的key包含的键值对较多,或者键值对中的元素都是较长字符串时,底层就会使用字典来作为底层实现

    127.0.0.1:6379> hset user:001 bigvalue ababababaabababaababbababaabbaababababababababababababababababababababbabaabababababababababaababababababaababababababaab
    (integer) 1
    127.0.0.1:6379> object encoding user:001
    "hashtable"
    

    dictht(dict.h)-哈希表

    typedef struct dictht {// 为了实现渐进式的rehash
        dictEntry **table;// 存放key value的entry
        unsigned long size; // hashtable的容量
        unsigned long sizemask; // size - 1,用于取模运算相当于2^(size - 1)
        unsigned long used;// hashtable的元素个数
    } dictht;
    

    负载因子:use/size

    Hash冲突及解决:与JDK的实现类似,刚开始申请的数组不够用,需要进行扩容,就需要重新申请一块内存区域,然后将数据搬移到新的内存区域上,在出现Hash冲突时,会将冲突的元素用链表链起来,这种方式叫“链地址法”

    举例说明,假设存储的是<key1, value1>,<key2, value2>, <key3, value3>,默认情况下hashtable的初始大小为4,所以用长度为4的数组来存放,上图橙色方块为slot(哈希槽),hash(key2) %4 =2且hash(key3) %4 =2,都会放入槽位2中,这种情况就是Hash碰撞,在JDK的HashMap中在碰撞较少时会使用链表存放,此处也是类似的,采用链表将碰撞的key连起来,当碰撞到一定长度后会进行重新扩容,这里扩容比较粗暴直接*2即可,所以翻阅源码会看到dict里会有两个dictht(dict hash table的意思)的结构体,就是为了扩容做准备的。假设当前的槽位是4个,扩容时会向操作系统申请8个槽位的内存空间,再将另一个dictht的指针指向这块区域,然后将原有的dictht逐渐迁移到新的dictht

    注意:取模运算的优化:num % 2^n = num & (2^n - 1)

    2.2.1 Hash碰撞问题

    hash碰撞.png

    为了让链表不要太长,所以负载因子需要维持在一个合理的范围内,

    以下情况会进行hash表的扩展(即上述橙色格子加多一倍的数量,并对数据重新rehash放入新的数组中)
    1)服务端没有在执行BGSAVE或BGREWRITEAOF命令,并且负载因子大于等于1
    2)服务器正在执行BGSAVE或BGREWRITEAOF命令,并且负载因子大于等于5

    以下情况会进行hash表的收缩
    1)负载因子小于0.1

    2.2.2 渐进式rehash

    这里还需要特别说明地是,为了避免rehash对服务性能造成影响,Redis服务端不是一次性将ht[0]的所有键值对全部rehash到ht[1],而是分多次、渐进式的进行迁移数据,每次对字典执行增删改查时执行对应的key会顺带做迁移,当然在Redis空闲时也会有一个事件轮询的机制进行数据迁移,顺带一提的是,在进行查找的时候,会对这两个dictht的map都进行搜索,直到搬迁完毕,搬迁的过程实在主线程中执行的,用后台线程就要考虑边搬迁边有数据写入,由于是渐进式的所以搬迁数据速度也非常快,所以不会阻塞太长

    2.3 Redis的5种对象底层剖析

    在Redis中不管是Key还是Value都是用realObject来表示的

    typedef struct redisObject {// string/list/set/hash/zset...
        unsigned type:4; // 通过type标识value的类型,这个type就可以限定不同的redisObject只能执行相对应的指令 通过type key进行查看 占4bit
        unsigned encoding:4; // 通过object encoding key进行查看,类型有:raw,embstr,int,ziplist... 占4bit
        unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or 占3byte(24bit)
                                * LFU data (least significant 8 bits  内存淘汰策略相关frequency
                                * and most significant 16 bits access time). */ 
        int refcount; // 4byte 使用引用计数管理内存 
        void *ptr;    // 8byte 指向底层实现的数据结构的指针
    } robj; // 共占16byte
    

    2.3.1 string

    Redis中的string,key是sds类型,而value类型也是sds类型(所以通过type查看都是string),但sds底层实现(通过命令encoding object [key])查看)则有int、raw、embstr,表示的是Redis底层的真实数据结构,在Redis中称为encoding

    redisObject对应ptr.png

    1)int

    试想一下存储整型的数字,而redisObject中的指针就占了8byte,但是long类型在64bit OS中也是占用8byte,若存储整型还要向内存开辟多8byte的空间,然后再用redisObject的ptr来指向是不是一种浪费,又要申请内存,还要CPU多操作去寻址找到对应存储long类型的区域。

    Redis基于以上的原因对可以用8byte存储的长整型做了优化,直接使用ptr存放长整型,在计算机中64bit的空间能存放的长整形的范围为(-263)~(263)*即-9,223,372,036,854,775,808~9,223,372,036,854,775,807

    所以当字符串小于等于20byte时,Redis会尝试转成长整型来存储,若可以,直接用ptr指针来存储此长整型,这样存储能减少一次CPU IO也减少了另外开辟空间,时间空间上都能节省

    # 存入64bit长整型的右边界
    127.0.0.1:6379:0>set strInt 9223372036854775807
    "OK"
    # 观察到确实是int
    127.0.0.1:6379:0>debug object strInt 
    "Value at:00007FCD875B56A0 refcount:1 encoding:int serializedlength:20 lru:5249254 lru_seconds_idle:2"
    # 存入64bit长整型的右边界+1
    127.0.0.1:6379:0>set strInt 9223372036854775808
    "OK"
    # 观察到已经转换成了embstr
    127.0.0.1:6379:0>debug object strInt 
    "Value at:00007FCD875B5640 refcount:1 encoding:embstr serializedlength:20 lru:5249266 lru_seconds_idle:2"
    # 同理左边界
    127.0.0.1:6379:0>set strInt -9223372036854775808
    "OK"
    127.0.0.1:6379:0>object encoding strInt
    "int"
    

    2)embstr

    redisObject结构体总共占16byte(type:4bit, encoding:4bit, lru:3byte, refcount:4byte, *ptr:8byte)

    typedef struct redisObject {// string/list/set/hash/zset...
        unsigned type:4; // 通过type标识value的类型 占4bit
        unsigned encoding:4; // 通过object encoding key进行查看,类型有:raw,embstr,int,ziplist... 占4bit
        unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or 占3byte(24bit)
                                * LFU data (least significant 8 bits  内存淘汰策略相关frequency
                                * and most significant 16 bits access time). */ 
        int refcount; // 4byte 使用引用计数管理内存 
        void *ptr;    // 8byte 指向sds
    } robj; // 共占16byte
    

    CPU通过内存地址获取数据通过cpu cache line(一次取64byte)来取数据,由于取RedisObject的时候会获取64byte,Redis将redisObject后的48byte利用起来,而sdshdr8占3byte,字符结束符'\0'占1byte,所以只剩下44byte能够放数据

    embstr存储优化.png
    # 44位的str
    127.0.0.1:6379:0>set strInt 12345678901234567890123456789012345678901234
    "OK"
    127.0.0.1:6379:0>object encoding strInt
    "embstr"
    # 45位的str
    127.0.0.1:6379:0>set strInt 123456789012345678901234567890123456789012345
    "OK"
    127.0.0.1:6379:0>object encoding strInt
    "raw"
    

    3)raw

    当总体超过64byte,Redis认为是大字符串,则直接使用raw形式

    string类型的应用:亿级用户日活统计BitMap实战

    假设当前有一个场景需要统计上亿级别的用户的上线情况,如近三天的上线用户个数,或者是连续三天上线的用户个数,可以使用BitMap进行实现,BitMap类型实际上是sds,底层存储方式是以char[]实现的

    bitmap场景-1.png

    上图表示用bitmap的offset作为用户id,第2列表示用户0上过线,第3列表示用户1未上过线,第4列表示用户2上过线

    bitmap场景-2.png

    上图表示第2列表示20200520这天用户0上线了,20200521这天用户0也上线了,20200522这天用户0未上线

    # 设置用户0在20200520当天有上线
    127.0.0.1:6379> setbit login_20200520 0 1
    (integer) 0
    127.0.0.1:6379> setbit login_20200520 5 1
    (integer) 0
    # 0x84 -> 1000 0100
    127.0.0.1:6379> get login_20200520
    "\x84"
    127.0.0.1:6379> setbit login_20200521 0 1
    (integer) 0
    127.0.0.1:6379> setbit login_20200521 2 1
    (integer) 0
    127.0.0.1:6379> setbit login_20200521 5 1
    (integer) 0
    127.0.0.1:6379> setbit login_20200522 3 1
    (integer) 0
    # 设置用户5在20200522当天有上线
    127.0.0.1:6379> setbit login_20200522 5 1
    (integer) 0
    # 由于是5bit,所以只用1byte就能存储起来
    127.0.0.1:6379> strlen login_20200520
    (integer) 1
    # 统计20200520的用户上线人数
    127.0.0.1:6379> bitcount login_20200520
    (integer) 2
    # 指定20200521,id从3-5的用户上线人数
    127.0.0.1:6379> bitcount login_20200521 3 5
    (integer) 0
    
    ##连续三天都登录的用户个数
    127.0.0.1:6379> bitop and login_20200520-20200522 login_20200520 login_20200521 login_20200522
    (integer) 1
    # 说明只有一个用户连续三天都登录了
    127.0.0.1:6379> bitcount login_20200520-20200522
    (integer) 1
    
    ##近三天有登录过的用户个数(bitop中的或运算)
    127.0.0.1:6379> bitop or active_20200520-20200522 login_20200520 login_20200521 login_20200522
    (integer) 1
    127.0.0.1:6379> get active_20200520-20200522
    "\xb4"
    127.0.0.1:6379> get active_20200520-20200522
    "\xb4"
    127.0.0.1:6379> bitcount active_20200520-20200522
    (integer) 4
    

    说明:bitmap最多能存储 2^32-1 bit(即512M),BitCount的计算速度快是由于用了“汉明重量”的算法

    2.3.2 list

    在翻阅源码前,想自己思考一下,list会如何实现?

    由于操作时有lpush rpush lpop rpop,一般人都会想着通过双向链表实现。

    使用双向链表的实现会出现以下问题,

    问题1:pre、next两个指针,一个指针要占用8byte,两个即16byte,但是可能存放的数据就小于8byte,这种情况就叫胖指针。意思是指针占据了绝大多数空间而不是被数据占据。

    问题2:链表创建可能会产生大量的内存碎片

    为了解决胖指针的问题,在Redis 3.2之前是使用ziplist和linkedlist来进行存储的,压缩列表相对于双向链表更节省内存,所以在创建list时,会先考虑压缩列表,并在一定条件下才转化为双向链表,在说明转化条件之前,我们先了解一下什么是压缩列表。

    1)ziplist

    ziplist&demo.png

    压缩列表是Redis为了节省内存而设计的,是由连续内存空间组成的顺序型数据,每个entry可以是字节数组或整数值

    ziplist节省内存的原因

    因为节点的prerawlen属性记录了前一个节点的长度,所以程序可以通过指针运算,根据当前节点的初始地址来计算出前一个节点的起始地址。例如,假设当前指向entry2的指针为p_entry2,则 p_entry1=p_entry2-prerawlen,通过这样的计算方式就能不保留前一个节点的指针从而节约了内存。

    创建新列表时 redis 默认使用 redis_encoding_ziplist 编码, 当以下任意一个条件被满足时, 列表会被转换成 redis_encoding_linkedlist 编码:

    • 试图往列表新添加一个字符串值,且这个字符串的长度超过 server.list_max_ziplist_value (默认值为 64 )。
    • ziplist 包含的节点超过 server.list_max_ziplist_entries (默认值为 512 )。

    注意:这两个条件是可以修改的,在 redis.conf 中:

    list-max-ziplist-value 64 
    list-max-ziplist-entries 512 
    

    ziplist连锁更新问题

    因为在ziplist中,每个zlentry都存储着前一个节点所占的字节数,而这个数值又是变长编码的。假设存在一个压缩列表,其包含e1、e2、e3、e4…..,e1节点的大小为253字节,那么e2.prevrawlen的大小为1字节,如果此时在e2与e1之间插入了一个新节点e_new,e_new编码后的整体长度(包含e1的长度)为254字节,此时e2.prevrawlen就需要扩充为5字节;如果e2的整体长度变化又引起了e3.prevrawlen的存储长度变化,那么e3也需要扩…….如此递归直到表尾节点或者某一个节点的prevrawlen本身长度可以容纳前一个节点的变化。其中每一次扩充都需要进行空间再分配操作。删除节点亦是如此,只要引起了操作节点之后的节点的prevrawlen的变化,都可能引起连锁更新。

    连锁更新在最坏情况下需要进行N次空间再分配,而每次空间再分配的最坏时间复杂度为O(N),因此连锁更新的总体时间复杂度是O(N^2)。
    即使涉及连锁更新的时间复杂度这么高,但它能引起的性能问题的概率是极低的:需要列表中存在大量的节点长度接近254个entry。

    所以Redis3.2后为了解决这个问题有引入了quicklist

    2)quicklist

    可以认为quickList,是ziplist和linkedlist二者的结合;quickList将二者的优点结合起来。

    quicklist数据结构.png

    quickList就是一个标准的双向链表的配置,有head 有tail;
    每一个节点是一个quicklistNode,包含prev和next指针。
    每一个quicklistNode 包含 一个ziplist,*zp 压缩链表里存储键值。
    所以quicklist是对ziplist进行一次封装,使用小块的ziplist来既保证了少使用内存,也保证了性能。

    # Lists are also encoded in a special way to save a lot of space.
    # The number of entries allowed per internal list node can be specified
    # as a fixed maximum size or a maximum number of elements.
    # For a fixed maximum size, use -5 through -1, meaning:
    # -5: max size: 64 Kb  <-- not recommended for normal workloads
    # -4: max size: 32 Kb  <-- not recommended
    # -3: max size: 16 Kb  <-- probably not recommended
    # -2: max size: 8 Kb   <-- good
    # -1: max size: 4 Kb   <-- good
    # Positive numbers mean store up to _exactly_ that number of elements
    # per list node.
    # The highest performing option is usually -2 (8 Kb size) or -1 (4 Kb size),
    # but if your use case is unique, adjust the settings as necessary.
    # 设置单个ziplist节点最大存储8kb,超过则进行分裂,将数据存储到新的ziplist节点
    list-max-ziplist-size -2
    # 0代表所有节点,都不进行压缩,1代表前面两个和尾部两个不进行压缩,其它均压缩,2,3,4...以此类推
    list-compress-depth 1
    

    2.3.3 hash

    1)ziplist

    Hash对象的编码可以是ziplist或者hashtable,当数据量比较小,或单个元素较小时,底层用ziplist存储。数据大小和元素数量阈值可以通过以下参数进行设置

    # ziplist元素个数超过512,将改为hashtable编码
    hash-max-ziplist-entries 512
    # 单个元素大小超过64byte,将改为hashtable编码
    hash-max-ziplist-value 64
    
    ## 测试hash的object encoding
    127.0.0.1:6379> hmset user:001 name Vander age 18
    OK
    127.0.0.1:6379> type user:001
    hash
    127.0.0.1:6379> object encoding user:001
    "ziplist"
    #写入超过64byte的value,会转换成hashtable
    127.0.0.1:6379> hset user:001 bigvalue ababababaabababaababbababaabbaababababababababababababababababababababbabaabababababababababaababababababaababababababaab
    (integer) 1
    127.0.0.1:6379> object encoding user:001
    "hashtable"
    

    当有新的键值对要加入到hash对象时,程序会先将保存了键的压缩列表节点推入到压缩列表尾部,然后再将保存了值的压缩列表节点推入到压缩列表尾部,如下图所示,hset name Vander在ziplist中是这么存储的:

    hash-ziplist.png

    使用这种方式保存时,并不需要申请多余的内存空间,而且每个Key都要存储一些关联的系统信息(如过期时间、LRU等),因此和String类型的Key/Value相比,Hash类型极大的减少了Key的数量(大部分的Key都以Hash字段的形式表示并存储了),从而进一步优化了存储空间的使用效率。

    在这篇 redis memory optimization 官方文章中,作者强烈推荐使用hash存储数据

    hash vs string

    若存放user对象,string会这么存储,而hash会这么存储

    hash vs string.png

    2)hashtable

    hashtable 编码的哈希对象使用字典作为底层实现, 哈希对象中的每个键值对都使用一个字典键值对来保存:

    • 字典的每个键都是一个字符串对象, 对象中保存了键值对的键;
    • 字典的每个值都是一个字符串对象, 对象中保存了键值对的值。
    hash-hashtable.png

    具体使用哪种数据结构,其实是需要看你要存储的数据以及使用场景。

    如果存储的都是比较结构化的数据,比如用户数据缓存,或者经常需要操作数据的一个或者几个,特别是如果一个数据中如果filed比较多,但是每次只需要使用其中的一个或者少数的几个,使用hash是一个好的选择,因为它提供了hget 和 hmget,而无需取出所有数据再在代码中处理。

    反之,如果数据差异较大,操作时常常需要把所有数据都读取出来再处理,使用string 是一个好的选择。

    还有一种场景:如果一个hash中有大量的field(成千上万个),需要考虑是不是使用string来分开存储是不是更好的选择。

    2.3.4 set

    Set为无序的,自动去重的集合数据类型,Set的encoding可以是intset或者是value为null的hashtable,当数据可以用整型表示时,set集合将被编码为intset数据结构

    当满足以下两个条件时,set将用hashtable存储数据:
    1)元素个数大于set-max-intset-entries
    2)元素无法用整型表示

    # intset能存储的最大元素个数,超过则用hashtable编码
    set-max-intset-entries 512
    
    ## 测试set类型的object encoding
    127.0.0.1:6379> sadd group_nums 1 2 3 5 4 0
    (integer) 6
    # 当元素量少,且可用整型表示时,会用intset,并且排好序,方便查找
    127.0.0.1:6379> smembers group_nums
    1) "0"
    2) "1"
    3) "2"
    4) "3"
    5) "4"
    6) "5"
    127.0.0.1:6379> type group_nums
    set
    127.0.0.1:6379> object encoding group_nums
    "intset"
    # 当添加了非整型类型后,转成hashtable
    127.0.0.1:6379> sadd group_nums a
    (integer) 1
    127.0.0.1:6379> object encoding group_nums
    "hashtable"
    

    1)intset

    整数集合是一个有序的,存储整型数据的结构。整型集合在Redis中可以保存int16_t,int32_t,int64_t类型的整型数据,并且可以保证集合中不会出现重复数据

    typedef struct intset {
        uint32_t  encoding;// 编码类型 可以是INTSET_ENC_INT16,INTSET_ENC_INT32,INTSET_ENC_INT64
        uint32_t  length;  // 元素个数
        int8_t    contents[]; // 存储元素的数组,可以是int16_t,int32_t,int64_t
    } intset; 
    #define INTSET_ENC_INT16 (sizeof(int16_t))
    #define INTSET_ENC_INT32 (sizeof(int32_t))
    #define INTSET_ENC_INT64 (sizeof(int64_t))
    

    升级

    每当将新元素添加到集合,并且新元素的类型比整数集合现有所有元素的类型都要长时,整数集合需要进行升级,然后才能将新元素添加到整数集合中,并且升级之后不会进行降级

    分三步进行:
    1)根据新元素的类型,扩展整数集合底层数组的空间大小并为新元素分配空间
    2)将底层数组现有的所有元素都转换成新元素相同的类型,并将类型转换后的元素放在正确的位置(即要排好序放)
    3)将新元素放入

    2)hashtable

    set-hashtable.png

    2.3.5 zset

    有序集合的encoding可以是ziplist或者skiplist

    1)ziplist

    压缩列表内的集合元素按score从小到大进行排序,分值较小的元素被放置在靠近表头的位置,分值较大的则放置在靠近表尾的位置

    以下是“zadd accomplishment 100 math",表示存储数学成绩100分

    zset-ziplist.png

    2)dict+skiplist

    encoding为skiplist的zset对象使用zset结构体作为底层实现,一个zset的结构体同时有字典和跳表

    typedef struct zset {
        dict *dict;
        zskiplist *zsl;
    } zset;
    

    先简单展示一下,跳表查找72的过程

    跳表的简单演示.png

    红色箭头为查找路径,通过索引层可以实现类似于二分查找的结果,所以跳表的查找时间复杂度为log(n)

    Redis中的跳表谁能成为索引,是由随机函数随机决定的,越底层的元素能成为索引的可能性越大

    下面展示"zadd accomplishment 100 math 99 chinese 95 English"

    zset-skiplist.png

    从上图可以看出skiplist主要是用于通过score来给内容进行排序,若只是需要单独取出某个内容的score,可以直接查询dict结构,这种方式使用了空间换取了时间,但是占得并不多,以上重复展示了个元素的成员和分值,其实字典和跳表是会共享元素的成员和分值,不会造成数据重复存储的。(注意上述只是为了举例,实际上上述指令encoding并不是skiplist,因为元素数量和元素长度都不满足条件,所以上述图示实际上是用ziplist存储的)

    同时满足以下两个条件时,对象使用ziplist编码:
    1)有序集合保存的元素数量小于128个
    2)有序集合保存的所有元素成员的长度都小于64byte

    127.0.0.1:6379> zadd accomplishment 100 math 99 chinese 95 English
    (integer) 3
    127.0.0.1:6379> object encoding accomplishment
    "ziplist"
    127.0.0.1:6379> zadd accomplishment 50 abcdaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
    (integer) 1
    127.0.0.1:6379> object encoding accomplishment
    "skiplist"
    127.0.0.1:6379>
    

    注意:GEO类型是GeoHash算法+zset来实现的。

    扩展知识

    Redis 3.2前后sds的结构

    Redis 3.2后sds的定义(sds.h)

    struct sdshdr {
        int len;// 字符串的长度,不会计算'\0'
        int free;
        char buf[];
    }
    

    Redis 3.2后sts的定义(sds.h)

    typedef char *sds;
    
    /* Note: sdshdr5 is never used, we just access the flags byte directly.
     * However is here to document the layout of type 5 SDS strings. */
    struct __attribute__ ((__packed__)) sdshdr5 {
        unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
        char buf[];
    };
    struct __attribute__ ((__packed__)) sdshdr8 {
        uint8_t len; /* used */
        uint8_t alloc; /* excluding the header and null terminator */
        unsigned char flags; /* 3 lsb of type, 5 unused bits */
        char buf[];// 末尾的结束符'\0'会占用一个字节
    };// 所以这个结构占用了4byte
    struct __attribute__ ((__packed__)) sdshdr16 {
        uint16_t len; /* 2byte used */
        uint16_t alloc; /* 2byte excluding the header and null terminator */
        unsigned char flags; /* 1byte 3 lsb of type, 5 unused bits */
        char buf[];// 1byte
    };
    struct __attribute__ ((__packed__)) sdshdr32 {
        uint32_t len; /* used */
        uint32_t alloc; /* excluding the header and null terminator */
        unsigned char flags; /* 3 lsb of type, 5 unused bits */
        char buf[];
    };
    struct __attribute__ ((__packed__)) sdshdr64 {
        uint64_t len; /* 已用长度 used */
        uint64_t alloc; /* 申请内存空间大小 excluding the header and null terminator */
        unsigned char flags; /* 3 lsb of type, 5 unused bits */
        char buf[]; /* 真正存放数据的地方 char类型在C语言中占用1byte */ 
    };
    

    可以看出Redis的sts类型存放字符串也是通过char数组,其中还有len标记这个字符串的长度,还有alloc表示申请的总空间的大小

    Redis 3.2之后对底层的sts类型进行了优化,当len小于5bit时,即存放的数据范围为0~(25-1)时,直接使用sdshdr5进行存储即可。当存放的数据范围超过25-1,后面的5bit就会闲置无用。

    sdshdr5-flags示意图.png

    在64位系统c语言int占用4byte(32 bit),能表示20位字符的长度(由于232次方是42亿),则使用sdshdr5,若数组长度为232~2^64,则使用sdshdr64,以此类推。

    Redis的数据结构关联关系

    redis的数据结构.png

    Redis中db类型:redisDb(server.h)

    typedef struct redisDb {
        dict *dict;                 /* 存放<k, value> The keyspace for this DB */
        dict *expires;              /* 存放<key timeout>Timeout of keys with a timeout set */
        dict *blocking_keys;        /* 阻塞队列的处理 Keys with clients waiting for data (BLPOP)*/
        dict *ready_keys;           /* 客户端维护 Blocked keys that received a PUSH */
        dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
        int id;                     /* 数据库id Database ID */
        long long avg_ttl;          /* Average TTL, just for stats */
        unsigned long expires_cursor; /* Cursor of the active expire cycle. */
        list *defrag_later;         /* List of key names to attempt to defrag one by one, gradually. */
    } redisDb;
    

    dict(dict.h)

    typedef struct dict {
        dictType *type; // 类型
        void *privdata; //
        dictht ht[2];// 需要有两个dictht 为了实现渐进式的rehash,在没有进行rehash的阶段h[1]=null
        long rehashidx; /* rehashing not in progress if rehashidx == -1 */
        int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */
    } dict;
    

    dictht(dict.h)-哈希表

    typedef struct dictht {// 为了实现渐进式的rehash
        dictEntry **table;// 存放key value的entry
        unsigned long size; // hashtable的容量
        unsigned long sizemask; // size - 1,用于取模运算相当于2^(size - 1)
        unsigned long used;// hashtable的元素个数
    } dictht;
    

    dictEntry(dict.h)

    typedef struct dictEntry {
        void *key; // 实际上就是sds的结构体,由于key都是用string存储的
        union {
            void *val;// value的类型则比较多样,string/list/hash等,对应的结构体是redisObject
            uint64_t u64;
            int64_t s64;
            double d;
        } v;
        struct dictEntry *next; // 当hash冲突时,指向下个entry
    } dictEntry;
    

    dictEntry中的value是通过redisObject来进行存放的(server.h)

    typedef struct redisObject {// string/list/set/hash/zset...
        unsigned type:4; // 通过type标识value的类型,这个type就可以限定不同的redisObject只能执行相对应的指令 通过type key进行查看 占4bit
        unsigned encoding:4; // 通过object encoding key进行查看,类型有:raw,embstr,int,ziplist... 占4bit
        unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or 占3byte(24bit)
                                * LFU data (least significant 8 bits  内存淘汰策略相关frequency
                                * and most significant 16 bits access time). */ 
        int refcount; // 4byte 使用引用计数管理内存 
        void *ptr;    // 8byte 指向sds
    } robj; // 共占16byte
    

    set key value源码解析

    源码说明当redisObject的字节长度小于等于20时,会尝试使用长整型进行存储

    setCommand(t_string.c)

    void setCommand(client *c) {
        robj *expire = NULL;
        int unit = UNIT_SECONDS;
        int flags = OBJ_NO_FLAGS;
    
        if (parseExtendedStringArgumentsOrReply(c,&flags,&unit,&expire,COMMAND_SET) != C_OK) {
            return;
        }
    
        c->argv[2] = tryObjectEncoding(c->argv[2]);// 完成编码
        setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
    }
    

    tryObjectEncoding(object.c)

    robj *tryObjectEncoding(robj *o) {
        long value;
        sds s = o->ptr;
        size_t len;
    
        /* Make sure this is a string object, the only type we encode
         * in this function. Other types use encoded memory efficient
         * representations but are handled by the commands implementing
         * the type. */
        serverAssertWithInfo(NULL,o,o->type == OBJ_STRING);
    
        /* We try some specialized encoding only for objects that are
         * RAW or EMBSTR encoded, in other words objects that are still
         * in represented by an actually array of chars. */
        if (!sdsEncodedObject(o)) return o;
    
        /* It's not safe to encode shared objects: shared objects can be shared
         * everywhere in the "object space" of Redis and may end in places where
         * they are not handled. We handle them only as values in the keyspace. */
         if (o->refcount > 1) return o;
    
        /* Check if we can represent this string as a long integer.
         * Note that we are sure that a string larger than 20 chars is not
         * representable as a 32 nor 64 bit integer. */
        len = sdslen(s);// 获取字符串的字节长度
        if (len <= 20 && string2l(s,len,&value)) {// 字符串长度小于等于20byte 尝试转成长整形存储
            /* This object is encodable as a long. Try to use a shared object.
             * Note that we avoid using shared integers when maxmemory is used
             * because every object needs to have a private LRU field for the LRU
             * algorithm to work well. */
            if ((server.maxmemory == 0 ||
                !(server.maxmemory_policy & MAXMEMORY_FLAG_NO_SHARED_INTEGERS)) &&
                value >= 0 &&
                value < OBJ_SHARED_INTEGERS)
            {
                decrRefCount(o);
                incrRefCount(shared.integers[value]);
                return shared.integers[value];
            } else {
                if (o->encoding == OBJ_ENCODING_RAW) {
                    sdsfree(o->ptr);
                    o->encoding = OBJ_ENCODING_INT;// encoding为int
                    o->ptr = (void*) value;// 使用长整型存储
                    return o;
                } else if (o->encoding == OBJ_ENCODING_EMBSTR) {
                    decrRefCount(o);
                    return createStringObjectFromLongLongForValue(value);
                }
            }
        }
    
        /* If the string is small and is still RAW encoded,
         * try the EMBSTR encoding which is more efficient.
         * In this representation the object and the SDS string are allocated
         * in the same chunk of memory to save space and cache misses. */
        if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) {
            robj *emb;
    
            if (o->encoding == OBJ_ENCODING_EMBSTR) return o;
            emb = createEmbeddedStringObject(s,sdslen(s));
            decrRefCount(o);
            return emb;
        }
    
        /* We can't encode the object...
         *
         * Do the last try, and at least optimize the SDS string inside
         * the string object to require little space, in case there
         * is more than 10% of free space at the end of the SDS string.
         *
         * We do that only for relatively large strings as this branch
         * is only entered if the length of the string is greater than
         * OBJ_ENCODING_EMBSTR_SIZE_LIMIT. */
        trimStringObjectIfNeeded(o);
    
        /* Return the original object. */
        return o;
    }
    

    Redis 6.0新特性

    1)多线程
    2)服务端协助的客户端缓存(仅支持单机,lettuce 6.0版本已经支持)
    3)权限管理(进行更加细致的权限管理)

    相关文章

      网友评论

        本文标题:2. 浅析Redis底层数据结构

        本文链接:https://www.haomeiwen.com/subject/ozokcltx.html