redis之数据结构探究

  在了解redis之前,先要知道一个概念:分布式内存对象缓存系统。起什么作用呢?用于将需要即时处理的一些数据放入到内存中,通过在快速的内存中进行访问,减少了链接数据库需要的时间和空间开销,也大大提高了访问效率现在常用的有memchahed,还有就是这里要说的redis。redis相对memcached支持更多的数据结构,支持数据的备份(内部有一个主从机制),支持数据的持久化(持久化有AOF和RDB两种模式,以后会提到)

  那咱们这里来讲讲redis所谓的多种数据结构和底层数据结构支持。

  Redis是一个key-value数据库,键值对,它的键是string类型(它底层是用C写的,大家也都知道C哪来的sttring,说到底这里应该是C风格的字符数组,但是如果直接使用C风格的字符串,那会带来很多问题,这里redis的作者封装了一下C风格字符串(char *),使得它有了许多其他操作),而它的值支持5种数据结构:1.字符串 2.哈希表(map映射) 3.列表(list) 4.集合(set) 5.有序集

  这一节我们只需要知道redis的值支持这5中上层数据的存储,过后会再研究,这里我们来看它的底层数据结构支持,也就是内部实现:

  一:首先来将上面特别提到的封装char *(C语言中是以'\0'为结尾标识的char *)。

    怎么封装的呢?redis中管它叫做SDS---简单动态字符串。   

     内部结构:(内部实现不仅仅下面这简单的几行,这只是sdshdr的结构,后面的方法中根据传入的sds,redis重写了一个内存申请的模块叫做zmalloc,对这个char*重新操作,分配内存和维护len以及sdsfree操作(针对free的操作))

typedef  char*  sds;
struct   sdshdr{
            unsigned   int   len;
            unsigned   int   free;
            char   buf[];
};
       如上所示,一个len表示sds的长度(在插入的时候直接定位到要插入的位置),一个free用于以后的append(当数据超过某个阀值,进行翻倍叠加,和vector容器类似申请两倍的内存)

     至于为什么要这么做呢?之前也提到过直接使用char *会带来很多的问题:

      1.char *的求长度操作是O(N),append操作会引起N次remalloc(动态内存调整,增加分配内存)。而我们实际应用中需要大量的对key值进行操作,这样一来,那基本分分钟死机。使用len来操作则会使上面两种操作均为O(1)。这样效率就可观了。

          2.redis进程之间存在通信,client和server端,需要遵守一些传输协议(RESP),所有数据必须是二进制的,保证安全(与字符串操作函数有关系,因为对于二进制数据,程序不会对其进行特殊字符解释操作,得到的数据都是最原始最安全的)。

      放到redis中是因为重新定义的sds,封装了char *之后,我们有了len,意味着我们所有的操作都以这个len为核心。字符串不会以"\0"作为结束标识。这里的安全就是输入任何字节都能正确处理,就算包含\0。

   二:双端链表。

     这个就很好理解了,学过数据结构就差不多了。

     它拥有两个指针,一个指向链表头,一个指向链表尾。每个节点带有前驱和后继指针,所以它是双向的。对头或者尾的操作均是O(1),一步完成。其余就是链表的性质了。

     当然,内部也有一个len保存节点的数目,通过len又会使很多操作优化,我们只需要维护这个len。

   三:字典(重头戏)

     定义:由键值对构成的抽象数据结构

     redis中的数据库(看看这可是数据库)和哈希键值对都基于字典的实现--->>而字典的底层实现是啥呢?哈希表,所以这个字典数据结构也被叫做Map。

     下面看两张图片,能很快的了解这层层递进的关系:



    从上面可以很清楚地看出来,redis数据库的主体是基于其结构中的dict,从第一张图可以看出它包含了所有的键值对数据

(也就是整个redis的真实数据全在这)。第一张图其他的一些过期时间,watched_keys,阻塞命令都是和客户端操作键值对的动作

有关,这里不讨论,主要看其数据结构dict。

    从第二张图,我们能看出来一个包含2张hashtable表:ht[0]  //  ht[1],2张哈希表的目的是实现动态的伸缩数据库大小

(rehash),优化它的效率.。


哈希表的实现如下:

typedef   struct   dictht{
              dictEntry   **table;     
               //hash表节点指针数组
              unsigned   long   size;   
               //指针数组长度
              unsigned   long   sizemask;  
               //长度掩码,用于计算索引值,快速定位查找
              unsigned   long   used;    
               //现有的节点数量
}dictht;

    字典是基于hashtable实现的,那么ht的实现了?这时候就到了最底层的东西,哈希表中包含了一大堆的指针,指向物理上

的数据地址,也就是哈希表节点:

typedef   struct   dictEntry{
               void*   key;
               union{
                     void*  val;
                     uint64_t   u64;
                     int64_t     s64;
                 }v;
struct   dictEntry  *next;
}dictEntry;
       现在看上面的代码是不是很简明易懂呢?注意其中的next指针是为了解决多个不同键拥有相同哈希值的时候的处理方法,将

后来的键放在next位置。数据结构有两种解决哈希冲突的方法,这就是其中一种,拉链法


    但是如果节点数量比哈希表大大小大很多的话,那么哈希表就会退化成多个链表,这样一来哈希表本身的7优势便不再那么


突出,就成了类似链表的效率,这怎么可以叫哈希呢?所以才有了对应的rehash。

    

    针对上面的rehash,我们都知道,一个hash表的比率(used/size==1)的时候,效率是最好的,一般都是O(1),当链表变


长之后那就不知道是啥样了.....

      

    我们假设比率为radio:

       

     1.radio>=1 ,并且dict_can_resize为true,自然rehash
       

     

     2.radio > dict_force_resize_radio(一般为5),强制rehash
      

      

       rehash的过程在网站上也能找到很多,这里简要说一下 :
        

        1.创建一个比ht[0]->table更大的ht[1]->table(根据情况,但是起码两倍,和vector类似)
        

        2.将ht[0]->table中的所有键值对都迁移到ht[1]->table
         

        3.将原有的ht[0]数据清空,这时候将ht[1]标识为新的ht[0],也就是正在使用的hashtable,原ht[0]变成了ht[1].

     为了不影响用户使用和服务器阻塞,采用的方法叫:渐进式rehash方式(将rehash分散到多个步骤中进行,从而避免了

集中式的计算)

      

      下面的两个函数说明了这种渐进式的rehash方法的的执行规则:
        

        1.dictRehashStep:当字典处于rehash状态时(也就是字典被告知了要rehash了,撑不住了,radio太大了),这


时用户进行增删改查的时候会触发dictRehashStep:这个函数的作用是将第一个不为空的索引的全部节点迁移到ht[1]中(因为radio


一般不会大于5,节点的数目也最多不超过5),将最多5个节点元素迁移到ht[1]上,不会影响用户操作的响应时间
        

        2.dictRehashMilliseconds:这个相当与时间片轮转(我想到了银行家算法),定时来进行redis cron job来进行


rehash。会在指定时间内对dict的 rehashidx不为-1的字典进行rehash
     

     还要注意一点,当用户在增删查改的时候,redis采用的策略是在rehash过程中ht[0]只会减少不会增加,增加肯定是在ht[1]


进行,查找,删除,修改根据你要找的数据处在哪儿,才发生在哪儿,ht[0]和ht[1]均会进行。
     

    还有一点,不仅仅是rehash,还有redis还能压缩,减少存储空间(当radio<10%时),过程同上可知。

    

    最后要说的就是它内部的哈希算法到底是啥?
      

      Redis现在使用两种不同的hash算法:

        

        1.MurmurHash2 32 bit算法(更加广泛:例如数据库,集群,哈希键,阻塞操作等功能均使用到了)

        

        2.基于djb算法实现的一个大小写无关散列算法(命令表以及Lua缓存脚本)


   四:跳跃表

     

       定义:一种随机化数据结构(层是随机产生的),查找,添加,删除操作都是O(logN)级别。

     

     在redis中的应用是有序集类型的底层数据结构之一(另外一个是字典)

     

     简要的说:链表+二分查找 的结合 



总结:

  

  好了,以上就是redis的4种底层数据支撑了,按照我的理解:以上4中数据结构是描述redis的内部结构,而我们


真正要存储的数据(redis支持存储的数据类型)会在下一节讲到。学习以上的内容可以帮助我们了解redis的内部数据


走向,比如一个具体的键值是最终是存储在哪一部分,中间会经过那些数据结构,键值是怎么实现的,最需要记住的


是字典dict,还有sds(简单动态字符串)

 




很多图片都是网上拿的,参考资料:


http://github.thinkingbar.com/redisbook_chapter01/


http://origin.redisbook.com/internal-datastruct/dict.html


    

     













  



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值