一.Redis中那些你不知道的秘密-五大基本结构String的实现原理

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: Redis已经成为解决高并发的必备利器,你可能已经把它玩的很熟了,但是对于它的底层,你又了解几分呢?本篇文章我们来聊聊Redis中那些你不知道的秘密之Redis五大基本结构的实现原理。你也许知道Redis常用的存储结构有 String,List,Set,ZSet,Hash,但还远远不够,本文章将带你一步一步走进Redis更深处的奥秘。

学习使用,老鸟飞过,欢迎交流

前言

Redis已经成为解决高并发的必备利器,你可能已经把它玩的很熟了,但是对于它的底层,你又了解几分呢?

本篇文章我们来聊聊Redis中那些你不知道的秘密之Redis五大基本结构的实现原理。你也许知道Redis常用的存储结构有 String,List,Set,ZSet,Hash,但还远远不够,本文章将带你一步一步走进Redis更深处的奥秘。

简单动态字符串SDS

SDS(simple dynamic string, SDS)译为简单动态字符串,是Redis自己封装的字符串结构,如下:

struct sdshdr{
   
   
    //字符串长度,即buf已用字节的数量
    int len;
    // 记录 buf 数组中未使用字节的数量
    int free;
    // 字节数组,用于保存字符串
    char buf[];
}

SDS内存分配图
在这里插入图片描述

String是Redis最常用的一种存储结构了,我们都知道Redis是使用C语言开发的高性能Nosql数据库,那为什么Redis没有直接使用C的String结构,而是使用的是自己开发的String结构SDS呢?基于两个原因:①取字符串长度的时间复杂度 ②二进制安全问题

为什么要定义len

这里不得不说一下C语言的String,C语言的字符串和其他语言都差不多,底层使用char[] 实现,但是在C语言中要获取字符串的长度需要遍历整个char[] 来完成,时间复杂度是 O(N) ,性能不是最优不说,它还有一个二进制安全问题。

在 C 中使用 ‘\0’ 来标志一个字符串的结束,比如:['h','e','l','l','o','\0'], 如果有一个特殊数据正好内容中有一个空字符,那么在C中这个数据只能被识别到空字符前面部分的数据导致数据不完整,所以在C中String这种结构也是没法保存除文本数据以外的图片、音频等数据。

而Redis不一样,它的SDS结构定义了len来维护char[]的已有数据的长度,要取数据长度直接取len,复杂度为 O(1); 读取字符串不依赖终止符‘\0’,保证了二进制安全,同时可以存储图片,音频等内容。

为什么要定义free

在C语言中每次字符串增长都需要重新分配一次内存空间,这个是非常耗性能的工作,在Redis中字符串是最常用的存储类型,当然会被频繁的修改,如果每次都重新分配内存对性能的损耗极大。

那么在Redis的SDS中,存储数据的char buf[];的实际大小是 len + free + 1

  • len表示字符串的长度,也表示buf中已经被使用的空间的大小。
  • free表示buf中没有被使用的空间的大小。
  • 其中多余的1个字节是用来存储’\0’的

即:char buf[]分配的内存空间是大于实际存储的字符串的长度的。那它为什么要多分配出 free 这一部分空间呢?①是预分配空间,防止每次修改都重新分配空间 ②防止缓冲区溢出

在C语言中,根据字符串处理特性,对于字符串的拼接、复制等操作,C语言开发者必须确保目标字符串的空间足够大,不然字符串增长时空间不够就会出现每次溢出的情况。

而在Redis中它是怎么处理的呢?每次在拼接字符串的时候,SDS的API内部会先对 free未使用的部分空间做判断看是否装得下最新的内容,如果装得下就不用开辟内存空间了,如果装不下再开辟内存空间,当然新开辟空间也不是开辟刚好放得下的空间大小,而是会额外申请len长度的空间作为 free,以便下一次使用,这是不是又是一次性能的提升呢?

SDS扩容图:
在这里插入图片描述

Redis的SDS如何释放内存

Redis采用的是惰性空间释放,如果Redis的String长度缩减它不会重新分配内存空间,而是通过较少len,增加free的方式来空大free的内存空间,这样的好处是减少内存重新分配,同时free可以用来进行下一次字符串拼接时使用这又是一次优化。当然也可以通过调用SDS提供的API直接释放内存,在需要的时候也能真正的释放掉多余的空间
在这里插入图片描述

Redis的极致优化

到此,Redis的SDS已经做了很好的优化了,但是为了把性能做到极致Redis还针对不同长度的String定制了不同的存储结构sdshdr5,sdshdr8,sdshdr32,sdshdr64。为什么这么做呢?例如:一个int占4个字节,在实际的应用中我们往往不会存储那么长的字符串,每个字符串都4个字节就太浪费空间了。

如果我们这样分配内存:短字符串len+free的长度为1个字节就够了,长字符串使用2个字节或者4个字节,更惨的字符串使用8个字节,这样会不会跟省内存?但是这样的话应该如何来区分这几种类型呢?在Redis 5.0版本中做了这样的改变:

sdshdr5{
   
   
    //flags一个字节8位,低3位存储类型,高5位存储长度
    unsigned char flags;
    //buf存放实际的内容
    char buf[];
}

Redis增加了flags占1字节(8bit),低三位用来表示String的type(短字符串,长字符串...),高5位存储长度len。

sdshdr5结构如下:
在这里插入图片描述
这样的话能够表示的长度区间为 0 到 31(2的5次方-1),那么如果字符串长度大于31呢?1个字节就存储不下了,按照之前的思路,把len和free单独存放,SDS几种存储结构如下:

/* sdshdr5和其他几种结构不一样 */
struct __attribute__ ((__packed__)) sdshdr5 {
   
   
    unsigned char flags; /* 低三位表示类型, 高五位表示字符串长度 */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
   
   
    uint8_t len; /* 字符串已用长度,1个字节存储*/
    uint8_t alloc; /* 分配总长度,1个字节存储 */
    unsigned char flags; /* 低3位表示类型,高5位未使用作为预留空间 */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
   
   
    uint16_t len; /* 字符串已用长度,2字节*/
    uint16_t alloc; /* 分配总长度,2字节 */
    unsigned char flags; /* 低3位表示类型,高5位未使用作为预留 */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
   
   
    uint32_t len; /* 字符串已用长度,4字节*/
    uint32_t alloc; /* 分配总长度,4字节 */
    unsigned char flags; /* 低3位表示类型,高5位未使用作为预留 */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
   
   
    uint64_t len; /* 字符串已用长度,8字节*/
    uint64_t alloc; /* 分配总长度,8字节 */
    unsigned char flags; /* 低3位表示类型,高5位未使用作为预留 */
    char buf[];
};
  • len:表示buf已经占用的字节数
  • alloc:表示buf分配的总数量,和len相减就是free的字节数
  • flags:表示SDS类型,第3位表示类型,高5位预留
  • buf:真正存储字符串数据的空间,是一个char[]数组。

可以看到,sdshdr5和其他几种不一太样。sdshdr5是把len放在flags中,而后面几种是把len和总长度alloc单独提出来,flags前3位表示类型,后5位作为预留。

这样一来那Redis该如何区分字符串长度来使用不同的存储类型呢?简单,做一个字符串长度判断即可,如下:

sds sdsnewlen(const void *init, size_t initlen) {
   
   
    void *sh;
    sds s;
    /* 计算出字符串长度,选择不同的SDS的类型 */
    char type = sdsReqType(initlen);
    /*SDS_TYPE_5  转换成SDS_TYPE_8 */
    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
   /* 计算不同的头部需要长度 */
    int hdrlen = sdsHdrSize(type);
    /* 指向flags */
    unsigned char *fp; 

    /* 分配内存,+1是用来存储\0,兼容C语言的 */
    sh = s_malloc(hdrlen+initlen+1);
    if (!init)
        memset(sh, 0, hdrlen+initlen+1);
    if (sh == NULL) return NULL;

    s = (char*)sh+hdrlen;

    fp = ((unsigned char*)s)-1;

    /* 根据不同的类型,初始化sdsHeader */
    switch(type) {
   
   
        case SDS_TYPE_5: {
   
   
            *fp = type | (initlen << SDS_TYPE_BITS);
            break;
        }
        case SDS_TYPE_8: {
   
   
            SDS_HDR_VAR(8,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
        case SDS_TYPE_16: {
   
   
            SDS_HDR_VAR(16,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
        case SDS_TYPE_32: {
   
   
            SDS_HDR_VAR(32,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
        case SDS_TYPE_64: {
   
   
            SDS_HDR_VAR(64,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
    }
    /* 字符串赋值 */
    if (initlen && init)
        memcpy(s, init, initlen);
    s[initlen] = '\0';
    return s;
}

好了文章就讲到这把,那么Redis对于String到底做了什么样的处理呢,这里总结一下:

  • Redis定义了自己的String结构即:SDS
  • 为了解决二进制安全问题SDS定义了len来表示已有字符串长度
  • 为了防止缓冲区溢出SDS在分配内存的时候做了预留空间即:free部分
  • 内存惰性释放,多余的内存加入free做预留,也是优化了内存频繁分配
  • 针对不同的String长度定制了不同的SDS结构

如果面试官问你:Redis为什么那么快,你怎么答?

  • Redis是单线程,没有锁的竞争,没有线程切换的性能开销
  • Redis基于内存读写并发能力强
  • Redis结构简单key-value存储
  • Redis采用多路IO复用模型
  • Redis对String做了极致优化SDS

是不是可以把Redis底层的优化加上?
Redis源码地址:https://github.com/antirez/sds/blob/master/sds.c

文章结束,希望对你有所帮助

相关文章
|
6月前
|
存储 NoSQL Redis
阿里面试:Redis 为啥那么快?怎么实现的100W并发?说出了6大架构,面试官跪地: 纯内存 + 尖端结构 + 无锁架构 + EDA架构 + 异步日志 + 集群架构
阿里面试:Redis 为啥那么快?怎么实现的100W并发?说出了6大架构,面试官跪地: 纯内存 + 尖端结构 + 无锁架构 + EDA架构 + 异步日志 + 集群架构
阿里面试:Redis 为啥那么快?怎么实现的100W并发?说出了6大架构,面试官跪地: 纯内存 + 尖端结构 +  无锁架构 +  EDA架构  + 异步日志 + 集群架构
|
8月前
|
消息中间件 缓存 NoSQL
Redis原理—5.性能和使用总结
本文详细探讨了Redis的阻塞原因、性能优化、缓存相关问题及数据库与缓存的一致性问题。同时还列举了不同缓存操作方案下的并发情况,帮助读者理解并选择合适的缓存管理策略。最终得出结论,在实际应用中应尽量采用“先更新数据库再删除缓存”的方案,并结合异步重试机制来保证数据的一致性和系统的高性能。
Redis原理—5.性能和使用总结
|
8月前
|
NoSQL 算法 安全
Redis原理—1.Redis数据结构
本文介绍了Redis 的主要数据结构及应用。
Redis原理—1.Redis数据结构
|
8月前
|
缓存 NoSQL Redis
Redis原理—2.单机数据库的实现
本文概述了Redis数据库的核心结构和操作机制。
Redis原理—2.单机数据库的实现
|
8月前
|
存储 缓存 NoSQL
Redis原理—4.核心原理摘要
Redis 是一个基于内存的高性能NoSQL数据库,支持分布式集群和持久化。其网络通信模型采用多路复用监听与文件事件机制,通过单线程串行化处理大量并发请求,确保高效运行。本文主要简单介绍了 Redis 的核心特性。
|
8月前
|
存储 缓存 NoSQL
Redis哈希结构在提升数据检索速度中的实践应用
本文详细介绍了 Redis 哈希结构的特点、常见使用场景以及如何在实际应用中利用哈希结构提升数据检索速度。通过合理使用 Redis 哈希结构,可以显著提高系统的性能和响应速度。在实际开发中,结合具体业务需求,灵活运用 Redis 提供的多种数据结构,构建高效的缓存和数据存储解决方案。希望本文能帮助您更好地理解和应用 Redis 哈希结构,提升数据检索速度。
211 18
|
8月前
|
缓存 NoSQL Redis
Redis原理—3.复制、哨兵和集群
详细介绍了Redis的复制原理、哨兵原理和集群原理。
|
12月前
|
NoSQL Redis
Redis 字符串(String)
10月更文挑战第16天
131 4
|
存储 分布式计算 NoSQL
大数据-40 Redis 类型集合 string list set sorted hash 指令列表 执行结果 附截图
大数据-40 Redis 类型集合 string list set sorted hash 指令列表 执行结果 附截图
108 3
|
设计模式 NoSQL 网络协议
大数据-48 Redis 通信协议原理RESP 事件处理机制原理 文件事件 时间事件 Reactor多路复用
大数据-48 Redis 通信协议原理RESP 事件处理机制原理 文件事件 时间事件 Reactor多路复用
183 2