Redis的字符串是如何实现的

目录
  • 前言
  • 为什么不用char*
  • 传统设计操作复杂度高
  • SDS的设计
  • SDS的高效操作
    • 创建sds
    • 字符数组拼接
    • 长度获取
    • 预分配内存空间
  • 节省内存的设计

前言

字符串在日常开发中应用得比较普遍,对于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"结束字符对字符串长度的影响。

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

表示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[];
};

用个图来表示一下

代码中定义了一个别名

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字符串实现内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • redis字符串类型_动力节点Java学院整理

    我们都知道redis是采用C语言开发,那么在C语言中表示string都是采用char[]数组的,然后你可能会想,那还不简单,当我执行如下命令,肯定是直接塞给char[]数组的. 如果你真的这么想的话,会有几个问题就要过来砍你了,先我们来找一个redis手册,http://doc.redisfans.com/ 第一:如果你每次都执行Append函数,那是不是redis的char[]每次都需要再次扩容,这样是不是每次都是耗时操作呢? 第二:如果你每次执行String中的StrLen,那redis底层

  • Redis字符串对象实用笔记

    字符串对象 字符串数据类型是Redis里最常用的类型了,它的键和值都是字符串,使用起来非常的方便.虽然字符串数据类型的值都统称为字符串了,但是在实际存储时会根据值的不同自动选择合适的编码.字符串对象的编码一共有三种:int.raw.embstr. Redis对象 Redis用统一的数据结构来表示一个对象,具体定义如下: typedef struct redisObject { unsigned type:4; unsigned encoding:4; // 当内存超限时采用LRU算法清除内存中的

  • Redis字符串原理的深入理解

    前言 来掘进都有两年多了一直当个小透明,今天终于发一次文章了. 最近在看 Redis,感觉收获很多,写篇博客记录一下. Redis 有五种基础数据结构:string,list,set,zset,hash.其中 string是最最最简单的也是最常用的.这个数据类型虽然简单但是内部的结构设计却很是精致. 基本介绍 相比于 Java,在 Redis 中 string 是可以修改的,是动态字符串(Simple Dynamic String 简称 SDS)他的内部结构更像是一个 ArrayList,维护一

  • Redis源码阅读:Redis字符串SDS详解

    SDS 基本概念 简单动态字符串(Simple Dynamic String)SDS,用作Redis 的默认字符串. C语言中的字符串:以空字符结尾的字符数组 SDS实现举例 redis > SET msg "hello world" OK 我们通过 SET 在 Redis 数据库中创建了一个数据键对象为 "msg" 和 数据值对象为 "hello world" 的键值对,其中数据键和数据值对象底层的字符串实现都是 SDS .同时, SDS

  • Redis字符串类型的常用命令小结

    Redis字符串类型 字符串类型是Redis中最为基础的数据存储类型,它在Redis中是二进制安全的,这便意味着该类型可以接受任何格式的数据,如JPEG图像数据或Json对象描述信息等.在Redis中字符串类型的Value最多可以容纳的数据长度是512M. 一.最简单的命令 1.获得符合规则的键名列表 keys * 这里的*号,是指列出所有的键,同时*号也可以替换成其他支持glob风格通配符格式,具体规则如下: ?:匹配一个字符 *:匹配任意个(包括0个)字符 []:匹配括号间多大任一个字符,可

  • 压缩Redis里的字符串大对象操作

    背景 Redis缓存的字符串过大时会有问题.不超过10KB最好,最大不能超过1MB. 有几个配置缓存,上千个flink任务调用,每个任务5分钟命中一次,大小在5KB到6MB不等,因此需要压缩. 第一种,使用gzip /** * 使用gzip压缩字符串 */ public static String compress(String str) { if (str == null || str.length() == 0) { return str; } ByteArrayOutputStream o

  • Redis缓存,泛型集合与json字符串的相互转换实例

    难点是泛型如何转换 一.arrayList<Map<String, Object>>转化json字符串,存入redis缓存 ArrayList<Map<String, Object>> listProfit //将ArrayList<Map<String, Object>>类型数据转换成json字符串 String listProfitPctJsonStr = JSON.toJSONString(listProfit); //然后将j

  • Redis中的动态字符串学习教程

    sds 的用途 Sds 在 Redis 中的主要作用有以下两个: 实现字符串对象(StringObject): 在 Redis 程序内部用作 char* 类型的替代品: 以下两个小节分别对这两种用途进行介绍. 实现字符串对象 Redis 是一个键值对数据库(key-value DB), 数据库的值可以是字符串.集合.列表等多种类型的对象, 而数据库的键则总是字符串对象. 对于那些包含字符串值的字符串对象来说, 每个字符串对象都包含一个 sds 值. "包含字符串值的字符串对象",这种说

  • Redis的字符串是如何实现的

    目录 前言 为什么不用char* 传统设计操作复杂度高 SDS的设计 SDS的高效操作 创建sds 字符数组拼接 长度获取 预分配内存空间 节省内存的设计 前言 字符串在日常开发中应用得比较普遍,对于Redis来说,键值对中的键是字符串,值也是字符串.比如在Redis中写入一条客户信息记录姓名.性别.爱好等. 在Redis这种内存数据库中,由于字符串被广泛的应用,在设计字符串时基于以下几点来设计: 1.支持丰富高效的字符串操作,比如追加.拷贝.比较等操作 2.能保存二进制数据 3.能尽可能的节省

  • redis内部数据结构之SDS简单动态字符串详解

    前言 reids 没有直接使用C语言传统的字符串表示(以空字符结尾的字符数组)而是构建了一种名为简单动态字符串的抽象类型,并为redis的默认字符串表示,因为C字符串不能满足redis对字符串的安全性.效率以及功能方面的需求 1.SDS 定义 在C语言中,字符串是以'\0'字符结尾(NULL结束符)的字符数组来存储的,通常表达为字符指针的形式(char *).它不允许字节0出现在字符串中间,因此,它不能用来存储任意的二进制数据. sds的类型定义 typedef char *sds; 每个sds

  • 详解Redis 键和字符串常用命令

    目录 Redis 相关知识 Redis中的数据类型 redis 键(key) Redis字符串(String) 常用命令 String的数据结构 Redis 相关知识 Redis的默认端口号为6379 默认16个数据库,类似数组下标从0开始,初始默认使用0号库.使用命令select <dbid>来切换数据库. 如: select 8.统一密码管理,所有库同样密码. dbsize查看当前数据库的key的数量.flushdb清空当前库.flushall通杀全部库. Redis是单线程+多路IO复用

  • redis数据类型_动力节点Java学院整理

    Redis支持5种数据类型,它们描述如下: Strings - 字符串 Redis的字符串是字节序列.在Redis中字符串是二进制安全的,这意味着他们有一个已知的长度,是没有任何特殊字符终止决定的,所以可以存储任何东西,最大长度可达512兆. 例子 redis 127.0.0.1:6379> SET name "yiibai" OK redis 127.0.0.1:6379> GET name "yiibai" 在上面的例子使用Redis命令set和ge

  • redis简介_动力节点Java学院整理

    Redis是一个开源的,先进的 key-value 存储可用于构建高性能,可扩展的 Web 应用程序的解决方案.Redis官方网网站是:http://www.redis.io/,如下: Redis 有三个主要使其有别于其它很多竞争对手的特点: Redis是完全在内存中保存数据的数据库,使用磁盘只是为了持久性目的: Redis相比许多键值数据存储系统有相对丰富的数据类型: Redis可以将数据复制到任意数量的从服务器中: Redis优点 异常快速 : Redis是非常快的,每秒可以执行大约110

  • 详解Redis命令和键_动力节点Java学院整理

    Redis命令用于在redis服务器上执行某些操作. 要在Redis服务器上运行的命令,需要一个Redis客户端. Redis客户端在Redis的包,这已经我们前面安装使用过了. 语法 Redis客户端的基本语法如下: $redis-cli 例子 下面举例说明如何使用Redis客户端. 要启动redis客户端,打开终端,输入命令Redis命令行:redis-cli.这将连接到本地服务器,现在就可以运行各种命令了. $redis-cli redis 127.0.0.1:6379> redis 12

随机推荐