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安装启动及常见数据类型
Apr 14 Redis
Redis延迟队列和分布式延迟队列的简答实现
May 13 Redis
分布式锁为什么要选择Zookeeper而不是Redis?看完这篇你就明白了
May 21 Redis
Redis Cluster 字段模糊匹配及删除
May 27 Redis
详解缓存穿透击穿雪崩解决方案
May 28 Redis
Redis 哨兵集群的实现
Jun 18 Redis
浅谈redis整数集为什么不能降级
Jul 25 Redis
Redis集群新增、删除节点以及动态增加内存的方法
Sep 04 Redis
SpringBoot集成Redis的思路详解
Oct 16 Redis
关于SpringBoot 使用 Redis 分布式锁解决并发问题
Nov 17 Redis
分布式Redis Cluster集群搭建与Redis基本用法
Feb 24 Redis
Redis之RedisTemplate配置方式(序列和反序列化)
Mar 13 Redis
SpringBoot集成Redis的思路详解
详解redis在微服务领域的贡献
详解Redis在SpringBoot工程中的综合应用
Oct 16 #Redis
Redis三种集群模式详解
浅谈Redis的keys命令到底有多慢
基于Redis结合SpringBoot的秒杀案例详解
Jedis操作Redis实现模拟验证码发送功能
Sep 25 #Redis
You might like
php中大括号作用介绍
2012/03/22 PHP
PHP限制页面只能在微信自带浏览器访问的代码
2014/01/15 PHP
php+xml结合Ajax实现点赞功能完整实例
2015/01/30 PHP
PHP读取文件的常见几种方法
2016/11/03 PHP
php中10个不同等级压缩优化图片操作示例
2016/11/14 PHP
php中preg_replace正则替换用法分析【一次替换多个值】
2017/01/17 PHP
JS控制文本框textarea输入字数限制的方法
2013/06/17 Javascript
浏览器窗口大小变化时使用resize事件对框架不起作用的解决方法
2014/05/11 Javascript
jQuery的Cookie封装,与PHP交互的简单实现
2016/10/05 Javascript
js实现简单的计算器功能
2017/01/16 Javascript
Bootstrap笔记之缩略图、警告框实例详解
2017/03/09 Javascript
分享19个JavaScript 有用的简写写法
2017/07/07 Javascript
bootstrap table表格客户端分页实例
2017/08/07 Javascript
node前端模板引擎Jade之标签的基本写法
2018/05/11 Javascript
JavaScript设计模式之工厂模式简单实例教程
2018/07/03 Javascript
element-ui表格列金额显示两位小数的方法
2018/08/24 Javascript
基于javascript的拖拽类封装详解
2019/04/19 Javascript
vue+elementUI实现表单和图片上传及验证功能示例
2019/05/14 Javascript
JS实现页面跳转与刷新的方法汇总
2019/08/30 Javascript
使用webpack将ES6转化ES5的实现方法
2019/10/13 Javascript
js中的面向对象之对象常见创建方法详解
2019/12/16 Javascript
详解微信小程序动画Animation执行过程
2020/09/23 Javascript
[01:18:36]LGD vs VP Supermajor 败者组决赛 BO3 第一场 6.10
2018/07/04 DOTA
Python描述器descriptor详解
2015/02/03 Python
Django中使用group_by的方法
2015/05/26 Python
合并百度影音的离线数据( with python 2.3)
2015/08/04 Python
对python字典过滤条件的实例详解
2019/01/22 Python
python+os根据文件名自动生成文本
2019/03/21 Python
浅谈Keras的Sequential与PyTorch的Sequential的区别
2020/06/17 Python
关于前端上传文件全面基础扫盲贴(入门)
2019/08/01 HTML / CSS
中东最大的在线宠物店:Dubai Pet Food
2020/06/11 全球购物
酒店前台接待岗位职责
2013/12/03 职场文书
工作态度不端正检讨书
2014/10/04 职场文书
2014年乡镇妇联工作总结
2014/12/02 职场文书
2015年简历自我评价范文
2015/03/11 职场文书
5人制售《绝地求生》游戏外挂获利500多万元 被判刑
2022/03/31 其他游戏