Redis简单动态字符串SDS

news/2024/10/30 13:38:36/

目录

前言

一.SDS定义

二.SDS与C字符串的区别

        2.1 常数复杂度获取字符串的长度

        2.2 杜绝缓冲区溢出

        2.3 减少修改字符串时带来的内存重分配次数

        2.3.1 空间预分配

        2.3.2 惰性空间释放

        2.4 二进制安全

        2.5 兼容部分C字符串函数

        2.6 总结

三.SDS缺点


前言

        Redis没有直接使用C语言形式的字符串来表示,而是自己构建了一种名为简单动态字符串的抽象类型SDS(simple dynamic string)。并将SDS作为默认字符串。

        在Redis中对于不会进行改变的字符串会使用C字符串,C字符串只会作为字符串字面量来使用。比如使用在打印日志。

        举个例子:

        在客户端输入命令 SET msg "hello world"

        在Redis中会新建一个键值对,其中,键值对的键和值都是字符串类型,底层实现都是用的是SDS。因为键和值都可以进行改变。

        处理字用来保存数据库的字符串值之外,SDS还可以被用作缓冲区,AOF模块中的AOF缓冲区,以及客户端状态的输入缓冲区。

一.SDS定义

        在redis源码中,在sds.h/sdshdrxx表示不同大小的sds:sdshdrxx会根据字符串的实际长度,选取合适的结构,最大化节省内存空间。

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

__attribute__ ((__packed__)):结构体对象在大小实际不是变量字节的累加,会存在内存对齐的情况。而__attribute__ ((__packed__))是告诉编译器,不需要进行内存对齐。

变量: 

  •  len:记录buf数组中已使用字节数,等于SDS保存字符串长度,不包含'\0'。
  • alloc:记录buf数组总共分配的内存大小,不包含'\0'。
  • flags:记录当前字节数组的属性,使用的哪一个结构。前3位记录使用的SDS结构,后5位在sdshdr5中记录字符串长度。在其他结构中没有使用。
//flag定义
#define SDS_TYPE_5  0
#define SDS_TYPE_8  1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4#define SDS_TYPE_MASK 7 //按位与,得到flags的低3位
#define SDS_TYPE_BITS 3 //用于右移动flags得到高5位。//用来定义对应T结构SDS变量,s一般是buf的起始地址
#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
//用来得到对应T结构SDS变量,s一般是buf的起始地址
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
//得到sdshdr5字符串长度
#define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS)
  • buf:保存真正的字符串的值,以及最后一个'\0'。

        SDS遵循C字符串以空字符结尾,保存空字符的一字节空间不计算在SDS的len里面,并且为空字符分配1字节的额外空间,以及添加空字符到字符串结尾等操作都是SDS自动完成的。

        在SDS以空字符串结尾的好处是,可以使用C字符串里面的函数。

二.SDS与C字符串的区别

        2.1 常数复杂度获取字符串的长度

        因为C字符串并不记录自身的长度信息,所以获取一个C字符串的长度需要遍历整个字符串,知对遇到的每一个字符进行计数,知道遇到空字符串为止。时间复杂度为O(N)。

        由于SDS在len字段记录了本身的长度,所以获取SDS字符串的长度复杂度为O(1)。

        2.2 杜绝缓冲区溢出

        C字符串使用C字符串函数进行拼接字符串时,当字符串的空间不够保存拼接的字符串,容易造成缓冲区溢出,还可能会修改其他字符串。

        所以在进行C字符串拼接时,需要程序员判断空间是否够保存拼接的字符串。不够需要开辟足够的空间。

        而SDS杜绝了缓冲区溢出的可能。在使用SDS API进行字符串拼接操作时。API会先检查SDS的剩余空间(alloc - len)是否满足修改的需求,通常会调用 sds.c/sdsMakeRoomFor 方法对 SDS 的剩余容量进行检查。不满足,API会自动将SDS的空间扩展到需要的大小,最后再进行修改操作。判断空间是否满足和修改SDS空间的大小都是SDS API自动完成的

static inline size_t sdsavail(const sds s) {unsigned char flags = s[-1];switch(flags&SDS_TYPE_MASK) {case SDS_TYPE_5: {return 0;}case SDS_TYPE_8: {SDS_HDR_VAR(8,s);return sh->alloc - sh->len;}case SDS_TYPE_16: {SDS_HDR_VAR(16,s);return sh->alloc - sh->len;}case SDS_TYPE_32: {SDS_HDR_VAR(32,s);return sh->alloc - sh->len;}case SDS_TYPE_64: {SDS_HDR_VAR(64,s);return sh->alloc - sh->len;}}return 0;
}sds sdsMakeRoomFor(sds s, size_t addlen) {void *sh, *newsh;size_t avail = sdsavail(s);size_t len, newlen, reqlen;char type, oldtype = s[-1] & SDS_TYPE_MASK;int hdrlen;size_t usable;/* Return ASAP if there is enough space left. */if (avail >= addlen) return s;len = sdslen(s);sh = (char*)s-sdsHdrSize(oldtype);reqlen = newlen = (len+addlen);assert(newlen > len);   /* Catch size_t overflow */if (newlen < SDS_MAX_PREALLOC)newlen *= 2;elsenewlen += SDS_MAX_PREALLOC;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 > reqlen);  /* 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 {/* Since the header size changes, need to move the string forward,* and can't use 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);sdssetalloc(s, usable);return s;
}

        2.3 减少修改字符串时带来的内存重分配次数

        由于C字符串并不记录自身长度。在每次进行增长和缩短C字符串,程序总需要对这个C字符串进行一次内存重分配:

  • 如果程序执行的是增长操作,那么在执行前,需要先进行内存重分配来扩展底层数组空间,否则会造成缓冲区溢出。
  • 如果程序执行的是截断的操作,操作之后需要释放原来不使用的空间,否则会造成内存泄漏。

        因为内存分配涉及复杂的算法,并且需要执行系统调用,所以它通常是一个比较耗时的操作。但是Redis作为数据库,经常被用于速度要求严苛,数据会被频繁修改的场合。如果每次修改字符串都需要重新分配内存,会对Redis的性能照成影响。

        为了避免,Redis记录了字符串的长度和底层数组的长度。

        2.3.1 空间预分配

        空间预分配用于优化SDS的字符串增长操作。当SDS的API需要对底层数组进行空间扩展时,程序不仅会为SDS分配必须要的空间,还会分配额外未使用的空间。这样如果下一次增长字符串长度在剩余空间内,就不需要重新分配空间了。有效减少了空间分配次数。

        分配策略:

  • 当进行修改后,SDS的长度,也就是len的值小于1MB,那么程序会为SDS分配与拼接后的len值同样大小的未使用空间。即alloc-len = len。比如:进行拼接后SDS的len变成了13字节,那么程序也会分配13字节的未使用空间。SDS数组长度变成了13+13+1=27字节,最后一个字节保存空字符。
  • 当进行修改后,SDS的长度,也就是len的值大于等于1MB,那么程序会分配1MB的未使用空间。比如:进行拼接后SDS的len变成了30MB,实际SDS的长度变成了30MB+1MB+1byte大小。

        2.3.2 惰性空间释放

        惰性空间释放用于优化SDS的字符串缩短操作。当SDS缩短保存的字符串时,程序并不立即使用内存重新分配回收缩短多出来的字节,而是修改len值,剩余的空间可以为之后拼接字符串使用。

        通过惰性空间释放,即能避免缩短字符串的内存重新分配,还可以为将来的增长操作提供优化。

        于此同时SDS也提供了API,让我们在有需要的时候,释放SDS未使用空间。

        2.4 二进制安全

        C字符串的字符必须符合某种编码,并且由于C字符串是以空字符串结尾的,中间如果出现空字符,会被当做字符串结尾。导致后面的字符被省略。使得C字符串只能保存文本数据,而不能保存图像,音频,视频,压缩文件等二进制文件。

        而Redis的SDS的API是二进制安全的,所以API都会以处理二进制的方式来处理buf中的数据,程序不会对其中的数据做任何限制,过滤或者假设。数据在写入时什么样,读取时就什么样。

        由于SDS使用的时len来保存字符串长度,支持了二进制安全的实现。Redis不仅可以保存文本数据,还可以保存任何格式的二进制数据。

        2.5 兼容部分C字符串函数

        虽然SDS是二进制安全的,但是它仍然遵循C字符串以空字符串结尾的管理。即API会自动在分配空间时,为空字符串分配一个空间,并且会在SDS保存的数据末尾设置空字符串。

        这是为了让那些保存文本数据的SDS可以重用部分<string.h>库定义的函数。

        2.6 总结

C字符串SDS
获取字符串长度时间复杂度O(N)获取字符串长度时间复杂度O(1)
API不安全,可能造成缓冲区溢出API安全,不会造成缓冲区溢出
修改字符串长度N此必然需要进行N次内存重新分配修改字符串长度N此最多需要进行N次内存重新分配
只能保存文本数据可以保存文本和二进制数据
可以使用所有<string.h>库定义的函数可以使用部分<string.h>库定义的函数

三.SDS缺点

        从SDS结构可以看出,SDS除了保存字符串,还需要保存其他的数据入长度,空间大小,标记等。所以SDS的一个缺点是占内存。典型的一个用空间来换时间的结构。


http://www.ppmy.cn/news/503652.html

相关文章

获取bing壁纸php,php获取bing每日壁纸示例分享

php获取bing每日壁纸示例分享 复制代码 代码如下: $strfile_get_contents(http://cn.bing.com/HPImageArchive.aspx?idx0&n1); if(preg_match("/(.?)/ies",$str,$matches)){ $imgurlhttp://cn.bing.com.$matches[1]; } if($imgurl){ header(Content-Type: imag…

python 日历壁纸_极客编程日历2018桌面壁纸

图灵社区出版了一本极客编程日历“Happy Hacking 2018”&#xff0c;实体已经售罄&#xff0c;但是提供了PDF电子版下载。我们可以编写脚本&#xff0c;把日历和桌面壁纸结合在一起。最终效果如图&#xff1a; 编程日历桌面壁纸 Python源代码&#xff1a; import datetime from…

Mybatis源码分析_日志模块 (1)

不得不承认&#xff0c;学习MyBatis的时间成本要比学习Spring低很多&#xff0c;Mybatis是我所了解过的代码量最小、整体架构最简单&#xff0c;但是又是最具有学习价值的一个框架。如果&#xff0c;你想快速的阅读一个框架的源码&#xff0c;并且掌握这个框架的精髓&#xff0…

python修改桌面壁纸_轻松有趣的Python小案例,让电脑自动更换壁纸

周末好&#xff01;难得放假&#xff0c;今天的你是不是一觉睡到大中午了&#xff1f;不要有愧疚感&#xff0c;现在的年轻人压力实在太大&#xff0c;好好睡上一觉已经是一件很奢侈的事情了&#xff0c;就给自己好好放个假吧&#xff01; 在这难得的休闲时光&#xff0c;我们…

什么是版本控制系统?怎么学习? - 易智编译EaseEditing

版本控制系统&#xff08;Version Control System&#xff0c;简称VCS&#xff09;是一种用于管理和跟踪文件版本的工具或系统。它可以追踪文件的变更历史&#xff0c;记录每个版本的修改内容&#xff0c;以及支持多人协作开发。 学习版本控制系统可以帮助你更好地管理和控制你…

每一次Http请求,Java线程是如何处理的?

每一次Http请求&#xff0c;Java线程是如何处理的&#xff1f; 文章目录 每一次Http请求&#xff0c;Java线程是如何处理的&#xff1f;前言一、Http请求处理二、两种服务器模型及处理方式1、两种服务&#xff1a;2.更好的处理方式 总结 前言 当我们写好一个项目时&#xff0c…

在 Navicat Premium 中管理 MySQL 用户 | 第 2 部分:创建新用户

第 2 部分&#xff1a;创建新用户 第 2 部分&#xff1a;创建新用户 第 1 部分 中&#xff0c;我们学习了如何使用 Navicat Premium 用户管理工具保护 MySQL 的 root 帐号。本篇文章将集中介绍如何设置新用户的帐号详细信息、帐号限制和 SSL 设置。如果你想边学边用&#xff1…

typeScript(枚举篇)

枚举 使用枚举我们可以定义一些带名字的常量。 使用枚举可以清晰地表达意图或创建一组有区别的用例。 TypeScript支持数字的和基于字符串的枚举。 数字枚举 首先我们看看数字枚举&#xff0c;如果你使用过其它编程语言应该会很熟悉。 enum Direction {Up 1,Down,Left,Righ…