• 《闲扯Redis二》String数据类型之底层解析


    原文出处http://www.yund.tech/zdetail.html?type=1&id=585ee331353551a44b29a9e9a09a1570
    作者: jstarseven


    一、前言

    Redis 提供了5种数据类型:String(字符串)、Hash(哈希)、List(列表)、Set(集合)、Zset(有序集合),理解每种数据类型的特点对于redis的开发和运维非常重要。

    二、疑问与解析

    结构图上显示,String类型有三种实现方式

    • 使用整数值实现的字符串对象
    • 使用 embstr 编码的动态字符串实现的字符串对象
    • 动态字符串实现的字符串对象

    疑问embstr 是什么意思,动态字符串又是什么意思?字符串对象到底什么结构?三种实现方式有什么区别呢?

    不急,咱们一步一步的往下看:

    1、Redis中定义的对象的结构体

    /*
     * Redis 对象
     */
    typedef struct redisObject {
        // 类型 4bits
        unsigned type:4;
        // 编码方式 4bits
        unsigned encoding:4;
        // LRU 时间(相对于 server.lruclock) 24bits
        unsigned lru:22;
        // 引用计数 Redis里面的数据可以通过引用计数进行共享 32bits
        int refcount;
        // 指向对象的值 64-bit
        void *ptr;
    } robj;// 16bytes
    

    注释:type表示该对象的类型,即上面 [String,List,Hash,Set,Zset] 中的一个,但为了提高存储效率与程序执行效率,每种对象的底层数据结构实现都可能不止一种,encoding 表示对象底层所使用的编码。

    2、Redis对象底层八种数据结构

     REDIS_ENCODING_INT(long 类型的整数)
     REDIS_ENCODING_EMBSTR embstr (编码的简单动态字符串)
     REDIS_ENCODING_RAW (简单动态字符串)
     REDIS_ENCODING_HT (字典)
     REDIS_ENCODING_LINKEDLIST (双端链表)
     REDIS_ENCODING_ZIPLIST (压缩列表)
     REDIS_ENCODING_INTSET (整数集合)
     REDIS_ENCODING_SKIPLIST (跳跃表和字典)
    

    3、embstr与动态字符串

    embstr :是专门用于保存短字符串的一种优化编码方式,跟正常的字符编码相比,字符编码会调用两次内存分配函数来分别创建 redisObject 和 sdshdr 结构(动态字符串结构),而 embstr 编码则通过调用一次内存分配函数来分配一块连续的内存空间,空间中包含 redisObject 和 sdshdr(动态字符串)两个结构,两者在同一个内存块中。从 Redis 3.0 版本开始,字符串引入了 embstr 编码方式,长度小于 OBJ_ENCODING_EMBSTR_SIZE_LIMIT(39) 的字符串将以EMBSTR方式存储。

    注意: 在Redis 3.2 之后,就不是以 39 为分界线,而是以 44 为分界线,主要与 Redis 中内存分配使用的是 jemalloc 有关。( jemalloc 分配内存的时候是按照 8、16、32、64 作为 chunk 的单位进行分配的。为了保证采用这种编码方式的字符串能被 jemalloc 分配在同一个 chunk 中,该字符串长度不能超过64,故字符串长度限制

    OBJ_ENCODING_EMBSTR_SIZE_LIMIT = 64 - sizeof('0')为1 - sizeof(robj) 为16 - sizeof(struct sdshdr)为8 = 39)

    动态字符串 :Redis 自己构建的一种名为 简单动态字符串(simple dynamic string,SDS)的抽象类型,并将 SDS 作为 Redis 的默认字符串表示。先简单了解概念,后面看详细解析

    4、带着疑问来细品下面一段话

    字符串的编码可以是 int,raw 或者 embstr。如果一个字符串内容可转为 long,那么该字符串会被转化为 long 类型,对象 ptr 指向该 long,并且对象类型也用 int 类型表示。普通的字符串有两种 embstr 和 raw。如果字符串对象的长度小于 39 字节,就用 embstr,否则用 raw。

    也就是说,Redis 会根据当前值的类型和长度决定使用内部编码实现:恍然大悟

    int:8个字节的长整型
    embstr:小于等于39个字节的字符串
    raw:大于39个字节的字符串
    

    5、实践验证

    命令:object encoding key ,获取数据底层的数据结构

    1)整数类型示例如下:

    2)短字符串示例如下:

    3)长字符串示例如下:

    疑问:至此,我们知道了embstr、字符串对象, 但是动态字符串的结构还是没说清楚啊,你是不是在逗我?
    靓仔疑问,再一次出现,别急,继续往下看

    三、动态字符串

    众所周知,Redis 是用 C 语言写的,但是对于 Redis 的字符串,却不是 C 语言中的字符串(即以空字符 ’’ 结尾的字符数组),它是自己构建了一种名为 简单动态字符串(simple dynamic string,SDS)的抽象类型,并将 SDS 作为 Redis 的默认字符串表示。

    1、动态字符串结构分析

    SDS 定义

    struct sdshdr{
         //记录buf数组中已使用字节的数量
         //等于 SDS 保存字符串的长度 4byte
         int len;
         //记录 buf 数组中未使用字节的数量 4byte
         int free;
         //字节数组,用于保存字符串 字节结尾的字符串占用了1byte
         char buf[];
    }
    

    用 SDS 保存字符串 “Redis” 具体结构如下图


    对于 SDS 数据类型的定义:

    • len 保存了SDS保存字符串的长度
    • buf[] 数组用来保存字符串的每个元素
    • free 记录了 buf 数组中未使用的字节数量

    上面的定义相对于 C 语言对于字符串的定义,多出了 len 属性以及 free 属性。为什么不直接使用 C 语言字符串实现,而是要使用 SDS 呢?有什么特别的优势呢?

    2、SDS结构与C语言字符串结构比较分析
    7

    1)获取字符串长度复杂度

     sdshdr 中由于 len 属性的存在,获取 SDS 字符串的长度只需要读取 len 属性,时间复杂度为 O(1),而对于 C 语言来说,获取字符串的长度通常是遍历字符串计数来实现的,时间复杂度为 O(n)。

    2)API安全性与缓冲区溢出

      缓冲区溢出(buffer overflow):是这样的一种异常,当程序将数据写入缓冲区时,会超过缓冲区的边界,并覆盖相邻的内存位置。在 C 语言中使用 strcat 函数来进行两个字符串的拼接,一旦没有分配足够长度的内存空间,就会造成缓冲区溢出,如

    8

    s1 = 'Redis',s2 = 'MongoDB',当执行strcat(s1, " Cluster")时,未给 s1 分配足够内存空间,s1 的数据将溢出到 s2 所在的内存空间,导致 s2 保存的内容被意外地修改。

    由于 SDS 记录了自身长度,同时在修改时,API 会按照如下步骤进行:

       (1)先检查SDS的空间是否满足修改所需的要求;
    
       (2)如果不满足要求的话,API 会自动将 SDS 的空间扩展至执行修改所需的大小(realloc);
    
       (3)然后才执行实际的修改操作;
    
        所以SDS不会造成缓冲区溢出情况
    

    3)字符串的内存重分配次数

     C 语言由于不记录字符串的长度,所以如果要修改字符串,必须要重新分配内存。
     SDS 实现了空间预分配和惰性释放两种策略:
    (1)空间预分配:当 SDS 的 API 对一个 SDS 进行修改,并且需要对 SDS 进行空间扩展的时候,程序不仅会为 SDS 分配修改所必须的空间,还会为 SDS 分配额外的未使用空间,这样可以减少连续执行字符串增长操作所需的内存重分配次数。
    (2)惰性释放:当 SDS 的 API 需要对 SDS 保存的字符串进行缩短时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用 free 属性将这些字节的数量记录起来,并等待将来使用,如

    sdstrim(s, "XY"); // 移除 SDS 字符串中的所有 'X' 和 'Y' 
    

    结果

    4)二进制数据安全

     二进制安全(binary-safe):指能处理任意的二进制数据,包括非 ASCII 和 null 字节。
     C 字符串以空字符 '',作为字符串结束的标识,而对于一些二进制文件(如图片等),内容可能包括空字符串'',导致程序读入的空字符会被误认为是字符串的结尾,因此C字符串无法正确存取二进制数据;
     SDS 的 API 都是以处理二进制的方式来处理 buf 里面的元素,并且 SDS 不是以空字符串''来判断是否结束,而是以 len 属性表示的长度来判断字符串是否结束,

    因此 Redis 不仅可以保存文本数据,还可以保存任意格式的二进制数据。

    5)C字符串函数兼容

      SDS 的buf数组会以''结尾,这样可以重用 C 语言库<string.h> 中的一部分函数,避免了不必要的代码重复。

    四、要点总结

    String 类型对象三种实现方式,int,embstr,raw
    字符串内容可转为 long,采用 int 类型,否则长度<39(3.2版本前39,3.2版本后分界线44) 用 embstr,其他用 raw
    SDS 是Redis自己构建的一种简单动态字符串的抽象类型,并将 SDS 作为 Redis 的默认字符串表示
    SDS 与 C 语言字符串结构相比,具有五大优势

  • 相关阅读:
    移动应用专项测试
    MAC连接安卓手机通过adb指令安装apk
    Git GUI可视化操作教程
    nestjs中typeorm进行事物操作
    vue-element-admin 实现动态路由(从后台查询出菜单列表绑定侧边栏)
    el-form 表单校验
    vscode设置VUE eslint开发环境
    .netcore signalR 实时消息推送
    psexec局域网执行远程命令
    Asp.Net跨平台 Jexus 5.8.1 独立版
  • 原文地址:https://www.cnblogs.com/jstarseven/p/12586680.html
Copyright © 2020-2023  润新知