redis那点事10: 数据结构篇

redis大纲: 

    传送

 

我们先来回顾下redis的基本类型:

    string  list  hash  set  zset

    

    一共有这5种基本类型, 那我们来聊以聊他们是数据结构

 

set一个数据的时候它的数据模型是什么?

    当执行set hello world 的时候, 他的数据模型为:

    

    dictEntry: redis 给每个键值对都会分配一个 dictEntry, 里面有 key val 指针, 还有指向下一个 dictEntry 的

        next 指针这个指针可以把多个hash值相同的键值对连接到一起, 形成一个链表,

        从而解决 hash 冲突的问题 (链地址法)

 

    sds: (Simple Dynamic String) 简单动态字符串, 长度可变的字符串

    

    redisObject: val 的值是存在 redisObject 中的, redis 的5中基本类型都是以 redisObject 形式存储的, type 字段

        表明这个value 是什么类型的, ptr 字段指向对象的存储地址, 值的数据结构是由 encoding 属性决定的

       redisObject 是 redis 的核心, redis 的对象编码, 内部编码, 内存回收, 共享对象等功能都是需要 redisObject

        的支持 这样的好处是: 对5种基本类型设置不同是数据结构, 从而优化在不同场景下的使用效率

    

    内存分配器: dictEntry, sds, redisObject 都是需要内存分配器 (如: jemalloc) 分配内存的

           jemalloc 是 redis 默认的内存分配器, 它在减小内存碎片方面做得比较好

           在 64位的系统中, jemalloc 将内存空间分配为 小, 大, 巨大 三个范围, 每个范围又分配了很多的内存块单位, 

           当 redis 进行存储的操作时, 会选择最合适的内存块进行数据的存储

    

    数据类型所对应的的编码格式:

        

 

    type 对应的 encoding编码图:

       

 

redis中的string数据结构:

    字符串对象底层实现可以是 int、ember、raw      

    ember 编码是通过调用一次内存分配器函数来分配一块连续的空间, raw 需要调用两次

    int 编码和 ember 编码在一定情况下会转换为 raw 编码

   

    ember:  <= 39个字节

    int : 8个字节的长整形

    raw:  大于39个字节的字符串

 

    测试编码:

    /**
     * 测试 string 格式的编码
     */
    private static void testString() {
        // key
        String key = "hello";

        jedis.set(key, "1");
        // int
        System.out.println(jedis.objectEncoding(key));

        jedis.set(key, "world");
        // embstr
        System.out.println(jedis.objectEncoding(key));

        jedis.set(key, "worldworldworldworldworldworldworldworldworld");
        // raw
        System.out.println(jedis.objectEncoding(key));
    }

 

动态简单字符串 (SDS) :

    这种结构像 Java 中的 ArrayList<Character> 长度是可变的

    sds 源码: 

        

 

    常数复杂度获取字符串长度SDS对象中有len属性, 所以获得SDS对象的长度时间复杂度是 O(1)

    

    预分配空间如果对SDS进行修改可能有两种情况 

        1. SDS的长度(len) 小于1MB, 那么程序分配和len属性一样大小的空间, 这时 free 属性和 len 属性值相同

            举个例子: 如SDS将len属性改为15字节, 程序也会分配15个字节给free, 这时buf数组的实际长度变为

            15+15+1 = 31字节 这里的1是 ‘\0的空字符的字节

        2. SDS的长度(len) 大于1MB, 程序会分配1MB的可用空间

            举个例子: 如SDS将len属性改为10MB, 程序会分配1MB的空间给 free, 这时 buf 数组的实际长度变为

           10MB+1MB+1byte

    

惰性释放空间: 当执行截取字符串操作 (sdstrim) 后, SDS不会立马释放多出来的空间, 如果下次再进行拼接字符串操作,

        且拼接没有刚才释放的空间大, 那些未释放的空间就派上了用场,

        惰性释放空间避免了特定情况下字符串重新分配内存的的操作

 

杜绝缓冲区溢出: 使用C语言字符串操作时, 如果字符串长度增加 (strcat操作), 而忘记重新分配内存,

        则很容易造成缓冲区溢出, 由于SDS记录了长度, 相应的操作在可能造成缓冲区溢出时会

        自动重新分配内存, 从而杜绝缓冲区溢出

 

redis中的list数据结构:

    list对象底层实现是 quicklist (快速队列, 是 ziplist 压缩队列 和 linkedlist 双向列表的组合)

    list支持两边插入和弹出, 可以获得指定位置(范围)的元素, 可以充当数组, 队列, 栈

 

    编码测试:

    /**
     * 测试 list 格式的编码
     */
    private static void testList() {
        jedis.lpush("list", "a", "b");

        // quicklist
        System.out.println(jedis.objectEncoding("list"));
    }

 

    quickList 源码:

        

 

    linkedList (双端列表): 结构比较像Java中linkedList

        

    

        从 linkedList 图中可以看到 redis 双端列表 list 的节点带有 prev指针、next指针、head指针和 tail 指针

        获取前节点、后节点、头部节点、尾部节点他们的复杂度都是O(1), len的复杂度也是O(1)

 

    ziplist (压缩列表): 

        当一个列表包含少量元素时, 并且是小数整数或比较短的字符串时, 那么redis就用 ziplist 作为 list 的底层实现

        ziplist是为了节约内存而开发的, 是由一系列特殊编码的连续内存块 (而不像双端列表每个节点都是指针)

        组成的顺序型数据结构, 具体结构比较复杂, 在新版本的redis中 使用quicklist替代了linkedList以及ziplist

 

    双端列表与压缩列表对比: 

        压缩列表可以节省空间, 但是增删操作的复杂度太高,

        当节点较少时可以使用压缩列表, 当节点很多时, 还是使用双端列表好

 

    quickList (快速列表):

        

        quickList是 linkedList和zipList的混合体, 它将 linkedList 按断切分, 每一段使用zipList来紧凑储存,

        多个 zipList 之间使用双向指针串起来, 因为链表的附加空间太高, 每一个 prev 和 next 指针都要

        占16个字节(64位系统8个字节), 另外每个节点的内存都是独立分配的, 这样会加剧内存的碎片化,

        影响内存的管理效率

    

        quickList的默认压缩深度为0, 也就是说不压缩, 为了支持快速的 pop/push 操作,

        quickList的首尾两个 zipList 不压缩, 此时深度为1, 为了进一步的节省空间,

        redis 还会对 zipList 进行压缩储存, 使用LZF压缩算法

       

redis中的hash数据结构:

    hash 的底层对象可以是 zipList (压缩列表), 也可以是 hashTable (哈希表)

    hashTable哈希表可以实现O(1)复杂度的读写操作, 一次效率非常高

    当满足这两个条件时, 才会使用 zipList:

        1. 哈希表中的数据小于512个

        2. 哈希表中的每一个键值对的长都小于64个字节

 

    编码测试:

    /**
     * 测试 hash 格式的编码
     */
    private static void testHash() {
        for (int i = 0; i < 100; i++) {
            jedis.hset("hash", "key" + String.valueOf(i), String.valueOf(i));
        }

        // ziplist
        System.out.println(jedis.objectEncoding("hash"));

        for (int i = 0; i < 513; i++) {
            jedis.hset("hash1", "key" + String.valueOf(i), String.valueOf(i));
        }

        // hashtable
        System.out.println(jedis.objectEncoding("hash1"));
    }

 

    hashTable 源码: 

        

    

    hashTable 结构图: 

        

        

        hash的结构类似于JDK7之前的 hashMap, 当有两个或两个以上的键被分到哈希数组的同一个索引上,

        会产生 hash 冲突, redis用链地址法来解决键冲突 ,每个哈希表节点都有一个next指针, 多个 hash 表节点

        使用 next 指针构成单项链表, 链地址法就是将哈希冲突的对象组织成一个单项链表, 放在hash值对应的槽上

 

        redis 的 hash 使用 hashTable 作为底层实现的话, 每个 hash 都会有两个hash表, 一个平时使用,

        另一个仅在rehash (从新散列) 时使用, 随着对hash表的操作, key会逐渐的增多或者减少,

        为了让hash表的负载因子维持在一个合理的范围内, redis会对hash表的大小进行扩张或收缩(rehash),

        也就是将 ht [0] 的所有的键值进行多次的, 渐进式的rehash 到 ht [1] 里

 

redis中的set数据结构:

    set集合的底层实现可以是 intset (整数集合) 或 hashTable (哈希表)

    intset (整数集合) 当一个集合只包含整数的时候, 并且元素不多的时候会使用 intset, 作为 set 集合的底层实现

    

    编码测试: 

    /**
     * 测试 set 格式的编码
     */
    private static void testSet() {
        for (int i = 0; i < 10; i++) {
            jedis.sadd("set", String.valueOf(i));
        }
        // intset
        System.out.println(jedis.objectEncoding("set"));

        for (int i = 0; i < 10; i++) {
            jedis.sadd("set1", "s" + String.valueOf(i));
        }
        // hashtable
        System.out.println(jedis.objectEncoding("set1"));
    }

 

    intset源码: 

        

        intset底层实现为有序, 无重复数组保存元素, intset这个结构里的整数数组的类型可以是16位, 32位, 64位  

        如果这个集合里的元素都是16位长度的, 添加一个17为长度的元素, 那么这整个集合会从16位升级成32位的数组,

       升级可以提升intset的灵活性, 还可以节约内存, 但是 不可逆 (32位 可以变为64位, 但不能变成16位)

    

redis中的zset数据结构:

    zset的底层实现可以是 zipList (压缩列表) 也可以是 skipList (跳跃表)

    当一个有序集合的元素比较多或者成员是比较长的字符串时, redis就会使用skipList (跳跃表) 作为zset的底层实现

 

    编码测试:

    /**
     * 测试 zset 格式的编码
     */
    private static void testZSet() {
        jedis.zadd("zset", 1, "a");
        // ziplist
        System.out.println(jedis.objectEncoding("zset"));

        jedis.zadd("zset1", 1, "aaaaaaaaaaaaaaaaaaaaaaaaa");
        // skiplist
        System.out.println(jedis.objectEncoding("zset1"));
    }

 

    skipList源码: 

        

 

skipList 结构图:

    

    

    skipList的查找复杂度是logN, 可以和平衡二叉树相当, 实现起来比较简单

    skipList是一种有序数据结构, 他通过在某个节点中维护多个指向其他节点的指针, 从而达到快速访问的目的

 

结束

  这就是我对redis数据结构篇的总结  感觉有用就点个赞吧 如果有错误或更好的方法评论区请多多指出  相互学习共同进步

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值