Redis的字符串是如何实现的


Posted in Redis onOctober 24, 2021

前言

字符串在日常开发中应用得比较普遍,对于Redis来说,键值对中的键是字符串,值也是字符串。比如在Redis中写入一条客户信息记录姓名、性别、爱好等。

Redis的字符串是如何实现的

在Redis这种内存数据库中,由于字符串被广泛的应用,在设计字符串时基于以下几点来设计:
1.支持丰富高效的字符串操作,比如追加、拷贝、比较等操作
2.能保存二进制数据
3.能尽可能的节省内存开销

可能会有人问了,既然C语言库提供了char*这样的字符数组来字符串操作。比如strcmp,strcat。感觉完全可以考虑直接使用C库提供的啊。C库字符串运用是很普遍,但是也不是没有问题的。它需要频繁的创建和检查空间,这在实际项目中其实很花时间的。所以,Redis设计了简单字符串(SDS,Simple Data )来表示字符串。同原来的C语言相比提升了字符串的操作效率,而且还支持二进制格式。下面我们就来介绍下Redis的字符串是如何实现的。

为什么不用char*

先来看看char*字符数组的结构,其实很简单就是开辟一块连续的内存空间来依次存放每一个字符,最后一个字符是"\0"表示字符串结束。C库中的字符串操作函数就是通过检查"\0"来判断字符串结束。比如strlen函数就是遍历字符数组中的每一个字符并计数,直到遇到"\0"结束计数,然后返回计数结果。下面我们通过一个代码来看看"\0"结束字符对字符串长度的影响。

Redis的字符串是如何实现的

这段代码的执行结果如下:

Redis的字符串是如何实现的

表示a1的字符长度是2个字符。这是因为在he后面有了"\0",所以字符串以"\0"表示结束,这就会产生一个问题,如果字符串内部本身就有"\0",那么数据就会被"\0"截断,而这就不能保存任意二进制数据了。

传统设计操作复杂度高

除了上面提到的不能保存任意二进制数据以外,操作复杂度也挺大。比如C语言中用得比较普遍的strlen函数,它要遍历字符数组中的每一个字符才能得到字符串长度。所以,时间复杂度是O(n)。另外再说一个常用函数strcat,它同strlen函数一样先遍历字符串才能得到目标字符串的末尾,而且它把源字符串追加到目标字符串末尾的时,还得确认目标字符串是否具有足够的空间。所以在调用的时候,开发人员还要人为保证目标字符串有足够的可用空间,不然就需要动态地申请空间。这样不仅时间复杂度高,操作复杂度也高了。

SDS的设计

Redis在设计的时候还是尽量保证复用C标准的字符串操作函数的。Redis在保留了使用字符数组来保存实际数据基础上,专门设计了一种SDS数据结构。
首先,SDS结构里面包含了一个字符数组buf[],同时SDS结构里面还包含了三个元数据。分别是字符数组现有长度len,分配给字符数组的空间长度alloc以及SDS类型flags。其中len和alloc这两个元数据定义了不同类型的SDS。SDS定义代码如下所示:

typedef char *sds;

/* Note: sdshdr5 is never used, we just access the flags byte directly.
 * However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

用个图来表示一下

Redis的字符串是如何实现的

代码中定义了一个别名

typedef char *sds;

所以SDS本质还是字符数组,只是在字符数组基础上增加了额外的元数据,Redis在使用字符数组时直接使用sds这个别名。

SDS的高效操作

创建sds

Redis调用sdsnewlen函数创建sds。我们以sedsnewlen举例,代码如下:

hisds sdsnewlen(const void *init, size_t initlen) {
   //指向SDS结构的指针
    void *sh;
    //sds类型变量,就是char*的别名
    sds s;
    //根据大小获取SDS的类型
    char type = hi_sdsReqType(initlen);
    /* Empty strings are usually created in order to append. Use type 8
     * since type 5 is not good at this. */
    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
    int hdrlen = sdsHdrSize(type);
    unsigned char *fp; /* flags pointer. */

     //为新创建的sds结构分配内存
    sh = s_malloc(hdrlen+initlen+1);
    if (sh == NULL) return NULL;
    if (!init)
        memset(sh, 0, hdrlen+initlen+1);
    
    //指向SDS结构体中的buf数组,sh指向SDS结构的起始位置,hdrlen表示元数据的长度    
    s = (char*)sh+hdrlen;
    fp = ((unsigned char*)s)-1;
    //根据类型初始化len,alloc
    switch(type) {
        case SDS_TYPE_5: {
            *fp = type | (initlen << HI_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)
    //将字符串拷贝给sds变量s
        memcpy(s, init, initlen);
     //字符串变量末尾添加"\0"表示字符串结束  
    s[initlen] = '\0';
    return s;
}

该函数主要执行过程如下:
1.根据初始化长度获取SDS类型。如果初始化长度initlen为0,一般被认为是要执行append操作,设置SDS类型为SDS_TYPE_8
2.为新创建的SDS结构分配内存,内存空间为元数据长度+buf长度+字符串最后的结束符"\0"。
3.根据SDS类型去初始化元数据len和alloc
4.将字符串拷贝给sds

字符数组拼接

由于sds结构中记录了占用的空间和被分配的空间,所以它比传统C语言的字符串效率更高。下面我们通过Redis的字符串追加函数sdscatlen来看一看。代码如下:

sds sdscatlen(sds s, const void *t, size_t len) {
//获取目标字符串的长度 
 size_t curlen = sdslen(s);
//根据追加长度和目标字符串长度判断是否需要增加新的空间
  s = sdsMakeRoomFor(s,len);
  if (s == NULL) return NULL;
  //将源字符串t中len长度的数据拷贝到目标字符串尾部
  memcpy(s+curlen, t, len);
  //设置目标字符串的最新长度
  sdssetlen(s, curlen+len);
  //拷贝完成后,在字符串结尾加上"\0"
  s[curlen+len] = '\0';
  return s;
}

这个函数有三个参数分别是目标字符串s,源字符串t和要追加的长度len。这个代码执行过程如下:
1.首先获取目标字符串的长度,然后调用sdsMakeRoomFor函数判断是否要给目标字符串添加新的空间,这样就可以保证目标字符串有足够的空间追加字符串
2.在保证了有足够空间可以追加字符串后,将源字符串中指定长度len的数据追加到目标字符串
3.设置目标字符串的最新长度

长度获取

代码中,函数sdslen记录了字符数组的使用长度,不用同C库一样遍历字符串了,这样可以大大降低了操作使用字符串的开销。该函数的代码如下所示:

static inline size_t sdslen(const sds s) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            return SDS_TYPE_5_LEN(flags);
        case SDS_TYPE_8:
            return SDS_HDR(8,s)->len;
        case SDS_TYPE_16:
            return SDS_HDR(16,s)->len;
        case SDS_TYPE_32:
            return SDS_HDR(32,s)->len;
        case SDS_TYPE_64:
            return SDS_HDR(64,s)->len;
    }
    return 0;
}

这样时间复杂度直接降到了O(1)。这个函数有一个骚操作,通过s[-1]获取到flags,然后调用SDS_HDR宏函数。我们来看下这个宏函数定义

#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))

其中##用来将两个token连接为一个token,所以加上参数将在预编译阶段将被替换如下

SDS_HDR(8,s);
((struct sdshdr8 *)((s)-(sizeof(struct sdshdr8))))

字符数组地址减去结构体的大小,就能获取到结构体的首地址,然后直接访问len属性。

预分配内存空间

此外,在代码中还使用了sdsMakeRoomFor函数,它在拼接字符串之前会检查是否需要扩容,如果需要扩容则会预分配空间。这一设计的好处就是避免了开发中忘记给目标字符串扩容而导致操作失败。比如strcpy(char* dst, const char* dst),如果src长度大于了dst的长度,又没有做检查就会遭成内存溢出。代码如下所示:

sds sdsMakeRoomFor(sds s, size_t addlen) {
    void *sh, *newsh;
    //获取SDS目前可用的空间
    size_t avail = sdsavail(s);
    size_t len, newlen;
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    int hdrlen;
    size_t usable;

    //空余空间足够,无需扩展
    if (avail >= addlen) return s;

    len = sdslen(s);
    sh = (char*)s-sdsHdrSize(oldtype);
    newlen = (len+addlen);
    assert(newlen > len);   /* Catch size_t overflow */
    //如果新的字符数组长度小于SDS_MAX_PREALLOC
    //分配2倍所需长度
    if (newlen < SDS_MAX_PREALLOC)
        newlen *= 2;
    //否则分配新长度加上SDS_MAX_PREALLOC的长度
    else
        newlen += SDS_MAX_PREALLOC;
   
   //重新获取SDS类型
    type = sdsReqType(newlen);

    /* Don't use type 5: the user is appending to the string and type 5 is
     * not able to remember empty space, so sdsMakeRoomFor() must be called
     * at every appending operation. */
    if (type == SDS_TYPE_5) type = SDS_TYPE_8;

    hdrlen = sdsHdrSize(type);
    assert(hdrlen + newlen + 1 > len);  /* Catch size_t overflow */
    if (oldtype==type) {
        newsh = s_realloc_usable(sh, hdrlen+newlen+1, &usable);
        if (newsh == NULL) return NULL;
        s = (char*)newsh+hdrlen;
    } else {
        / /如果头部大小发生变化只需要将字符数组向前移,不使用realloc
        newsh = s_malloc_usable(hdrlen+newlen+1, &usable);
        if (newsh == NULL) return NULL;
        memcpy((char*)newsh+hdrlen, s, len+1);
        s_free(sh);
        s = (char*)newsh+hdrlen;
        s[-1] = type;
        sdssetlen(s, len);
    }
    usable = usable-hdrlen-1;
    if (usable > sdsTypeMaxSize(type))
        usable = sdsTypeMaxSize(type);
     //更新SDS容量  
    sdssetalloc(s, usable);
    return s;
}

其中SDS_MAX_PREALLOC的长度为1024*1024

#define SDS_MAX_PREALLOC (1024*1024)

节省内存的设计

前面讲SDS结构的时候提到过它有一个元数据flag,表示字符串类型。SDS一共有5中类型,它们分别是sdshdr5,sdshdr8,sdshdr16,sdshdr32和sdshdr64。这五种的主要区别是它们字符数组的现有长度len和分配空间alloc的不同。
那么我们就以sdshdr16为例,它的定义如下

struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

我们可以看到现有长度len和已分配空间alloc都是uint16_t类型,uint16_t是16位无符号整型,会占用2个字节。当字符串类型是sdshdr16的时候它包含的字符数组长度最大为2^16-1字节。而对于其它三种sdshdr8,sdshdr32和sdshdr64,以此类推它们的类型就分别是uin8_t,uin32_t和uint64_t,len和alloc这两个元数据占用的空间分别是1字节、4字节和8字节。
实际上,设计不同的结构头的目的是为了灵活保存不同大小的字符串,从而有效地节省内存空间。在保存不同大小的字符串时,结构头占用的内存空间也不一样,这样一来保存小字符串时,占用的空间也会比较小。
除了设计不同类型的结构头以外,Redis还使用编译优化来节省内存空间。比如上面sdshdr16的代码中就有__attribute__ ((packed)),它的目的是告诉编译器采用紧凑的方式分配内存,默认情况下编译器会按照16字节的对齐方式给变量分配内存。也就是说一个变量没有到16个字节,编译器也会给它分配16个字节。
我们来举个例子

#include <string.h>
#include <iostream>

using namespace std;

typedef struct MyStruct
{
 char  a;
 int b;   
} MyStruct;


int
main()
{
    cout << sizeof(MyStruct) << endl;
    
    return 0;
}

虽然char占用1个字节,int占用4个字节,但是打印出来是8,这样多出来的3个字节白白浪费掉了。现在我们运用__attribute__ ((packed))属性定义结构体,就可以实际占用多少字节,编译器就分配多少空间。我们把刚才代码修改一下加上这个属性。代码如下

#include <string.h>
#include <iostream>

using namespace std;

typedef struct MyStruct
{
 char  a;
 int b;   
} __attribute__ ((__packed__))MyStruct;


int
main()
{
    cout << sizeof(MyStruct) << endl;
    
    return 0;
}

运行这段代码,结果就变为5了,表示编译器用了紧凑型的内存分配。在开发过程中,为了节省内存开销就可以考虑把__attribute__ ((packed))这个属性运用起来。

到此这篇关于Redis的字符串是如何实现的的文章就介绍到这了,更多相关Redis字符串实现内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Redis 相关文章推荐
Redis IP地址的绑定的实现
May 08 Redis
基于Redis实现分布式锁的方法(lua脚本版)
May 12 Redis
深入浅析Redis 集群伸缩原理
May 15 Redis
Java Socket实现Redis客户端的详细说明
May 26 Redis
Redis可视化客户端小结
Jun 10 Redis
Redisson实现Redis分布式锁的几种方式
Aug 07 Redis
Redis+Lua脚本实现计数器接口防刷功能(升级版)
Feb 12 Redis
解决 redis 无法远程连接
May 15 Redis
Redis特殊数据类型bitmap位图
Jun 01 Redis
Redis实现主从复制方式(Master&Slave)
Jun 21 Redis
利用Redis实现点赞功能的示例代码
Jun 28 Redis
Redis唯一ID生成器的实现
Jul 07 Redis
SpringBoot集成Redis的思路详解
详解redis在微服务领域的贡献
详解Redis在SpringBoot工程中的综合应用
Oct 16 #Redis
Redis三种集群模式详解
浅谈Redis的keys命令到底有多慢
基于Redis结合SpringBoot的秒杀案例详解
Jedis操作Redis实现模拟验证码发送功能
Sep 25 #Redis
You might like
PHP取整函数:ceil,floor,round,intval的区别详细解析
2013/08/31 PHP
PHP生成图片验证码功能示例
2017/01/12 PHP
PHP实现的策略模式示例
2019/03/20 PHP
使用laravel指定日志文件记录任意日志
2019/10/17 PHP
PHP Swoole异步MySQL客户端实现方法示例
2019/10/24 PHP
写得不错的jquery table鼠标经过变色代码
2013/09/27 Javascript
getAsDataURL在Firefox7.0下无法预览本地图片的解决方法
2013/11/15 Javascript
Javascript selection的兼容性写法介绍
2013/12/20 Javascript
jquery ajax传递中文参数乱码问题及解决方法说明
2014/02/07 Javascript
jquery 扑捉回车键事件代码
2014/04/24 Javascript
js实现简单鼠标跟随效果的方法
2015/04/10 Javascript
js实现图片无缝滚动特效
2020/03/19 Javascript
iscroll.js的上拉下拉刷新时无法回弹的解决方法
2016/02/18 Javascript
AngularJS中过滤器的使用与自定义实例代码
2016/09/17 Javascript
vue中的计算属性实例详解
2018/09/19 Javascript
在LayUI图片上传中,解决由跨域问题引起的请求接口错误的方法
2019/09/24 Javascript
详解ES6新增字符串扩张方法includes()、startsWith()、endsWith()
2020/05/12 Javascript
Javascript实现鼠标移入方向感知
2020/06/24 Javascript
JavaScript 常见的继承方式汇总
2020/09/17 Javascript
js实现三角形粒子运动
2020/09/22 Javascript
基于Python数据可视化利器Matplotlib,绘图入门篇,Pyplot详解
2017/10/13 Python
python爬虫爬取淘宝商品信息(selenum+phontomjs)
2018/02/24 Python
Python统计单词出现的次数
2018/04/04 Python
win7下 python3.6 安装opencv 和 opencv-contrib-python解决 cv2.xfeatures2d.SIFT_create() 的问题
2019/10/24 Python
python3.6连接mysql数据库及增删改查操作详解
2020/02/10 Python
Python如何优雅删除字符列表空字符及None元素
2020/06/25 Python
全球最大最受欢迎的旅游社区:Tripadvisor
2017/11/03 全球购物
Infababy英国:婴儿推车、Travel System婴儿车和婴儿汽车座椅销售
2018/05/23 全球购物
医学生个人求职信范文
2013/09/24 职场文书
自我鉴定的范文
2013/10/03 职场文书
个人实用简单的自我评价
2013/10/19 职场文书
优秀班主任推荐材料
2014/12/17 职场文书
小学教师教育随笔
2015/08/14 职场文书
Java实现给Word文件添加文字水印
2022/02/15 Java/Android
面试中老生常谈的MySQL问答集锦夯实基础
2022/03/13 MySQL
Win11 22H2 2022怎么更新? 获得Win1122H22022版本升级技巧
2022/09/23 数码科技