zoukankan      html  css  js  c++  java
  • 蜻蜓点水说说Redis的String的奥秘

    本篇博客参考:掘金Redis小册 敖丙

    如果面试官问你,单线程的Redis为什么那么快,你可能脱口而出,因为单线程,避免上下文切换;因为基于内存,比硬盘读写快很多;因为采用的是多路复用网络模型。不管你是否真的理解了,这个回答足以应付一半以上的面试官了,但是如果可以再进行补充就更好了:因为Redis对各种数据结构进行了精心的设计,比如String采用的是SDS,比如list采用的是ziplist,quicklist等等,可能这样的回答就比较出彩了,至少可以说出部分面试者不太清楚的事情。今天我们就来看看Redis中最常用的String数据结构的奥秘。

    从位操作说起

    bitmap的应用场景很多,比如大名鼎鼎的布隆过滤器(之前的博客有介绍过:《大白话布隆过滤器》),比如统计指定用户在一年内任意日期内的登录情况,统计任意日期内,所有用户的登录情况等等,都可以用bitmap来实现(之前的博客也有介绍过:《有点长的博客:Redis不是只有get set那么简单》),所以好好看看bitmap还是很有必要的,不过本篇博客不打算详细介绍bitmap,只是通过bitmap引出我们今天的话题,而bitmap的核心就是位操作。

    如果我们要往Redis塞入一个value为“hello”的key,这个所有人都会:

    test:1>set key hello
    "OK"
    test:1>get key
    "hello"
    

    如果我们要利用位操作实现这个需求呢?什么,我没听错把,位操作也可以实现这个需求吗?当然可以,因为在Redis中,String就是用byte数组来存储的。

    什么,你不信?那请继续看下去。

    要用位操作实现这个需求,我们要获得“hello”的ascii码,接着计算出二进制:

    比如,“h”的ascii码是104,二进制是1101000:
    image.png

    "e"的ascii码是65,二进制是101,二进制是1100101:
    image.png

    然后形成如下的位图:
    image.png

    下面就需要利用位操作来进行设置:

    test:1>setbit s 1 1
    "0"
    test:1>setbit s 2 1
    "0"
    test:1>setbit s 4 1
    "0"
    test:1>setbit s 10 1
    "0"
    test:1>setbit s 13 1
    "0"
    test:1>setbit s 9 1
    "0"
    test:1>setbit s 15 1
    "0"
    

    setbit的顺序可以随意调整,只要最终得到的位图是如上形式的就OK了。(我这里就调整了下seitbit的顺序,好吧,我承认其实我是打错了,又懒得再去打一遍,反正最终形成的位图是一样的)。

    然后我们get一下:

    test:1>get s
    "he"
    

    很神奇,有木有,这也说明了在Redis的底层,String就是一个数组。

    SDS

    不管在什么编程语言、存储引擎中,String都是应用最广泛的,而在不同的编程语言、存储引擎中,String可能有不同的实现,在Redis中,String的底层就是SDS,它的全称是Simple Dynamic String。

    Redis是C语言开发的,C语言是没有现成的字符串类型的,而是用字符数组来表示字符串,Redis为什么不直接这么做呢,而要“别出心裁”的自己构建SDS数据结构来实现字符串呢?

    我们先来这个SDS是个什么鬼:

    struct sdshdr {
        int len;
        int free;
        char buf[];
    };
    

    SDS的定义比较简单,只有3个字段,而且从字面上就可以看出是什么意思:

    • len:存储字符串的实际长度
    • free:存储剩余(空闲)的空间
    • buf[]:存储实际数据

    我们可以看到,其实SDS结构也包含了字节数组,但是不同的是,新增了两个字段。

    为了方便起见,下面的文章把直接使用字节数组来表示字符串称为C语言的字符串,但是大家要明白,C语言是没有现成的字符串类型的。

    下面我们来看下SDS和C语言的字符串有什么区别:

    • 求字符串长度
      C语言的字符串,求字符串的长度只能遍历,时间复杂度是O(N),单线程的Redis表示鸭梨山大,但是现在引入了一个字段来存储字符串的实际长度,时间复杂度瞬间降低成了O(1)。
    • 二进制安全
      在C语言中,读取字符串遵循的是“遇零则止”,即,读取字符串,当读取到“”,就认为已经读到了结尾,哪怕后面还有字符串也不会读取了,像图片、音频等二进制数据,经常会穿插“”在其中,好端端的图片、音频就毁了...但是现在有了一个字段来存储字符串的实际长度,读取字符串的时候,先看下这个字符串的长度是多少,然后往后读多少位就可以了。
    • 缓冲区溢出
      字符串拼接是开发中常见的操作,C语言的字符串是不记录字符串长度的,一旦我们调用了拼接函数,而没有提前计算好内存,就会产生缓冲区溢出的情况,但是现在引入了free字段,来记录剩余的空间,做拼接操作之前,先去看下还有多少剩余空间,如果够,那就放心的做拼接操作,不够,就进行扩容。
    • 减少内存重分配次数
    1. 空间预分配:当对字符串进行拼接操作的时候,Redis会很贴心的分配一定的剩余空间,这块剩余空间现在看起来是有点浪费,但是我们如果继续拼接,这块剩余空间的作用就出来了。
    2. 惰性空间释放:当我们做了字符串缩减的操作,Redis并不会马上回收空间,因为你可能即将又要做字符串的拼接操作,如果你再次操作,还是没有用到这部分空间,Redis也会去回收这部分空间。

    扩容策略

    字符串小于1M,采用的是加倍扩容的策略,也就是多分配100%的剩余空间,当大于1M,每次扩容,只会多分配1M的剩余空间。

    最大长度

    Redis 规定字符串的长度不得超过 512M 字节。

    embstr raw

    Redis的字符串有两种存储方式,一种是embstr,一种是raw,当长度<=44,采用embstr 来存储:

    set codebear abcdefghijklmnopqrstuvwxyz012345678912345678
    "OK"
    debug object codebear
    "Value at:0x7f4050476880 refcount:1 encoding:embstr serializedlength:45 lru:1999016 lru_seconds_idle:36"
    

    当长度>44,改用raw来存储:

    set codebear abcdefghijklmnopqrstuvwxyz0123456789123456781
    "OK"
    debug object codebear
    "Value at:0x7f404ac30100 refcount:1 encoding:raw serializedlength:46 lru:1999188 lru_seconds_idle:3"
    

    网上也有一些博客说是以39为分界线,为什么会有两种答案呢?继续看下去就明白了。

    我们先来看看Redis的对象头,查看

    #define LRU_BITS 24
    typedef struct redisObject {
        unsigned type:4;
        unsigned encoding:4;
        unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                                * LFU data (least significant 8 bits frequency
                                * and most significant 16 bits access time). */
        int refcount;
        void *ptr;
    } robj;
    

    Redis对象头占 4bit+4bit+24bit+4byte+8byte(指针,在64 bit system下,占8byte)=32bit+12byte=4byte+12byte=16byte。

    再来看看这两种存储形式有什么区别:
    image.png
    embstr的存储形式比较紧凑,Redis的对象头和SDS对象存在一起(连续)。

    image.png
    一般来说,在raw的存储形式下,Redis的对象头和SDS对象不存在一起(不连续)。

    我们可以简单的理解为,一块内存的大小为64byte。

    好了,前置内容介绍完毕了,我们来看看Redis3.0版本的SDS的定义,查看

    struct sdshdr {
        unsigned int len;
        unsigned int free;
        char buf[];
    };
    

    Redis对象头占了16byte,SDS对象的len和free又占了8byte,64-16-8=40,同时保存的字符串会以结尾,又占用了1byte,所以实际存储的字符串只能<=39位,所以在低版本的Redis下,embstr、raw的分界线为39。

    再来看看Redis5.0版本的SDS的定义,查看
    image.png
    可以看到变化很大,为什么要做那么大的改变?更节省内存,当字符串长度比较小的时候,会用
    sdshdr8来存储,len和alloc共占用2byte,flags占用1byte,结尾占用1byte,一共是4byte,64byte-16byte(对象头)-4byte=44byte,所以在高版本的Redis下,embstr、raw的分界线为44。

    怎么样,没想到吧,我们Redis经常使用的String竟然牵扯到那么多东西,而这些东西就可以区分平庸开发和优秀开发,成为一个优秀的开发,要学习的东西还有很多很多。

  • 相关阅读:
    计算欧拉函数值
    矩阵快速幂
    约瑟夫环数学公式
    整型输出输入优化
    计算机设计第三章
    计算机设计第二章
    计算机设计
    阿里巴巴秋招2017客户端附加题
    程序设计基本概念
    c++面试题
  • 原文地址:https://www.cnblogs.com/CodeBear/p/13385727.html
Copyright © 2011-2022 走看看