博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
redis之数据结构探究
阅读量:4221 次
发布时间:2019-05-26

本文共 4610 字,大约阅读时间需要 15 分钟。

  在了解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

    

     

  

你可能感兴趣的文章
Java多态性理解
查看>>
Intellij Idea 工具在java文件中怎么避免 import .*包,以及import包顺序的问题
查看>>
IDEA Properties中文unicode转码问题
查看>>
Oracle中Blob转换成Clob
查看>>
Linux如何查看so中函数名
查看>>
自动管理代码的android.mk
查看>>
cocos2dx 2.2.6编译记录(1)
查看>>
makefile学习网站
查看>>
C 编写lua模块(1)
查看>>
Lua教程:Lua调用C/C++函数(4)
查看>>
win下创建win32控制台工程,执行lua脚本
查看>>
cocos2dx android启动错误
查看>>
eclipse: android rename package name
查看>>
cocos2dx c++调用java思想
查看>>
cocos2dx lua Node节点 私有数据存取
查看>>
lua math.ceil math.ceil
查看>>
cocos2dx CCNode计算node的大小
查看>>
cocos2dx 布局记录(1)
查看>>
lua 多行注释和取消多行注释
查看>>
缩放系数计算
查看>>