上一篇中我们讲到了redis的4种底层数据结构支撑,这一篇学习存储数据结构的实现。

  redis的值value支持5种类型数据的存储。分别是1. 字符串 2.列表 3.有序集合 4.哈希表 5.集合,千万不要觉得这些数据结构和上一篇中的有什么关系,数据结构只是一种通用的抽象化数据的理解方法 ,在哪都可以独立出来。相同的地方只是某个具体实现用同一种数据结构比较好,就像字符串,作键也可以,作值也可以。

redis制定数据存储路径 redis如何存储数据_内存

  由上图可知,5种数据的底层实现都不是唯一的。可以看出来,它们的底层实现是什么东西呢?不就是咱们上一篇所提到的4中数据结构么?至于还有一个整数集合和压缩列表,这是为了节省内存的存储简单数据类型的策略,当占用内存变大的时候,就升级为字典。下一篇专门说明一下这两个数据结构。

  要实现上面这张图的数据关联关系,必须得应用到多态的概念。而C中是如何应用多态这个概念的呢?

  通过结构体来模仿:


typedef struct redisObject {

    // 类型
    unsigned type:4;

    // 对齐位
    unsigned notused:2;

    // 编码方式
    unsigned encoding:4;

    // LRU 时间(相对于 server.lruclock)
    unsigned lru:22;

    // 引用计数
    int refcount;

    // 指向对象的值
    void *ptr;

} robj;

  其中有这些成员:

     type:redisObject类型(Type):5种类型

     encoding:底层实现结构通过此项确定,上一节的4种基本类型,加上整数集合和压缩列表大约6种

     ptr:初始化一个空的指针,根据以上两者的判断之后再将其指向确切的物理地址


   知道了它的基本结构,所有的操作都是基于上面的结构来执行的:

     1.对数据库执行一个操作时,首先根据key,查看数据库中是否存在对应的可供操作的redisObject,没有就返回null

        2.查看redisObject的type是否支持要执行的操作(如集合有集合的操作命令,字典有字典的操作命令,这里判断是不是一一对应的)

     3.根据redisObject的encoding属性选择对应的数据结构(上面这一步正确,操作命令对同一种数据结构(任何的底层实现)操作应该都是起效果的(可能物理上的更改不一样,但不影响客户端得到的结果),所以找到对应的数据执行对应操作)

     4.返回处理结果


内存共享的理解

redis制定数据存储路径 redis如何存储数据_数据库_02

    针对一些(1)常用的操作返回值大多都不大的情况,(2)还有一些常用的值的对象(像一些整数)

    对此,redis并不严格区分不同数据结构,而让它们使用同一块内存,相当于把这些对象放入到一块共用的内存

    常要使用的对象直接在其中调用,避免了重复分配,节省了CPU处理时间     


字符串:

    毫无疑问是redis使用得最多的数据结构,可以作key,这里可以用来作为 set / get 的操作对象。

    它的底层有两种实现:

      1.REDIS_ENCODING_INT   使用long类型保存long的值

                  2.REDIS_ENCODING_ROW    使用sdshdr保存sds,long long,double,long  double...

          第一种是保存long类型的存储,而且只有保存long时用它

    除此之外其他字符串的保存都是使用第二种方法,(但是在保存数据到数据库中时,会自动尝试将其转换为第一种类型long式保存,为了节省空间)


哈希表:

    它的底层实现也是两种:

      1.REDIS_ENCODING_ZIPLIST  压缩列表

      2.REDIS_ENCODING_HT    字典

    创建新的哈希表时,默认先使用压缩列表作为底层实现数据结构,好处是基本不会产生多余的内存,要多少就申请多少,追加到压缩列表中。

    只有当触发了某些条件才会转换成字典。

      1.哈希表中某个键或者值的长度大于server.hash_max_ziplist_value(默认为64)
      2.压缩列表中的节点数量大于server.hash_max_ziplist_entries(默认为512)

列表:

    队列(链表实现)。2种底层实现:

      1.REDIS_ENCODING_ZIPLIST

      2.REDIS_ENCODING_LINKEDLIST

         和哈希表类似,默认是压缩列表,之后也是触发某些条件才会变成双端链表

      1.试图往列表中插入一个字符串值,长度大于server..list_max_ziplist_value(默认是64)

         2.ziplist包含的节点超过server.list_max_ziplist_entries(默认值为512)

    

    对于列表,还需要提到的一个是阻塞命令:

      阻塞原语:BLPOP / BRPOP / BRPOPLPUSH,这些命令都可能造成客户端的阻塞。

        当这些命令作用于空列表,造成阻塞(上面的命令根据字面意思就是删除(pop)一个节点,当没有节点的时候就会阻塞到有节点进来,让你删除,之后再释放)

        如果被处理的列表不为空,它们就执行无阻塞版本的LPOP / RPOP / RPOPLPUSH   (少了Blocked)

    要解除阻塞:

        1. 添加节点以供删除 -----  被动脱离

        2.超过最长阻塞时间 -----   主动脱离

        3.关机          ------            强制脱离


集合:

    set。底层也是2种实现:

      1.REDIS_ENCODING_INTSET  整数集合

      2.REDIS_ENCODING_HT    

    与之前不一样的是:

      如果第一个进入到集合的元素是long  long类型,那么就使用上面第一种编码方式

      否则,就使用字典

    转化依旧是有两个任意触发条件:

      1.intset保存的整数值个数超过server.set_max_intset_entries(默认值为512)

      2.从第二个元素开始,如果插入的元素类型不是long long的,就要转化成第二种


有序集:

    有序集中的每一个元素都有一个score,根据score的大小进行排列(如果两个元素的 score 相同, 那么按字典序对 member 进行对比,决定排列顺序)

    结果得到的是一个排序过的map。

   

redis制定数据存储路径 redis如何存储数据_内存_03

 

    ziplist的节点指针只能线性地移动,即使我们已经排好了序,在ziplist中查找某个元素也是O(N)的复杂度,每次执行删除或

添加修改都必须经过一次查找,那必然在O(N)之上。这时候为了除了效率低的问题,有序集采用了字典的结构:键---值。这里的键

值是以member作为key,score作为value。

    通过检查给的member是否存在于有序集中

    有就取出member对应的score值     

    这样在O(1)内就查出了两个需要的值。


  还有一个快被忘了的数据结构:跳跃表.....

      通过使用它,可以让有序集支持以下两种操作:

        1.在 O(logN) 期望时间、O(N) 最坏时间内根据 score 对 member 进行定位(被很多底层函数使用)

        2.范围性查找和处理操作,这是(高效地)实现 ZRANGE 、ZRANK 和 ZINTERSTORE等命令的关键

    通过同时使用字典和跳跃表,有序集可以高效地实现按成员查找和按顺序查找两种操作

总结:

    好好学习数据结构