Redis——简单动态字符串(Simple Dynamic Strings,SDS)

2023-12-05 01:36

本文主要是介绍Redis——简单动态字符串(Simple Dynamic Strings,SDS),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

简单动态字符串(Simple Dynamic Strings,SDS)是Redis的基本数据结构之一,用于存储字符串和整型数据。SDS兼容C语言标准字符串处理函数,且在此基础上保证了二进制安全。

1、数据结构

在了解SDS源码前,我们先思考一个问题:如何实现一个扩容方便且二进制安全的字符串呢?

注意: 什么是二进制安全?通俗地讲,C语言中,用“\0”表示字符串的结束,如果字符串中本身就有“\0”字符,字符串就会被截断,即非二进制安全;若通过某种机制,保证读写字符串时不损害其内容,则是二进制安全。

SDS既然是字符串,那么首先需要一个字符串指针;为了方便上层的接口调用,该结构还需要记录一些统计信息,如当前数据长度和剩余容量等,例如:

    struct sds {int len; // buf中已占用字节数int free; // buf中剩余可用字节数char buf[]; // 数据空间};

SDS结构示意如下图所示,在64位系统下,字段len和字段free各占4个字节,紧接着存放字符串:
在这里插入图片描述
Redis 3.2之前的SDS也是这样设计的。这样设计有以下几个优点:

  • 有单独的统计变量len和free(称为头部)。可以很方便地得到字符串长度。
  • 内容存放在柔性数组buf中,SDS对上层暴露的指针不是指向结构体SDS的指针,而是直接指向柔性数组buf的指针。上层可像读取C字符串一样读取SDS的内容,兼容C语言处理字符串的各种函数。
  • 由于有长度统计变量len的存在,读写字符串时不依赖“\0”终止符,保证了二进制安全。

注意: 上例中的buf[]是一个柔性数组。柔性数组成员(flexible array member),也叫伸缩性数组成员,只能被放在结构体的末尾。包含柔性数组成员的结构体,通过malloc函数为柔性数组动态分配内存。

之所以用柔性数组存放字符串,是因为柔性数组的地址和结构体是连续的,这样查找内存更快(因为不需要额外通过指针找到字符串的位置);可以很方便地通过柔性数组的首地址偏移得到结构体首地址,进而能很方便地获取其余变量。

到这里我们实现了一个最基本的动态字符串,但是该结构是否有改进的空间呢?我们从一个简单的问题开始思考:不同长度的字符串是否有必要占用相同大小的头部?一个int占4字节,在实际应用中,存放于Redis中的字符串往往没有这么长,每个字符串都用4字节存储未免太浪费空间了。我们考虑三种情况:短字符串,len和free的长度为1字节就够了;长字符串,用2字节或4字节;更长的字符串,用8字节。

这样确实更省内存,但依然存在以下问题:

  • 问题1:如何区分这3种情况?
  • 问题2:对于短字符串来说,头部还是太长了。以长度为1字节的字符串为例,len和free本身就占了2个字节,能不能进一步压缩呢?

对于问题1,我们考虑增加一个字段flags来标识类型,用最小的1字节来存储,且把flags加在柔性数组buf之前,这样虽然多了1字节,但通过偏移柔性数组的指针即能快速定位flags,区分类型,也可以接受;对于问题2,由于len已经是最小的1字节了,再压缩只能考虑用位来存储长度了。

结合两个问题,5种类型(长度1字节、2字节、4字节、8字节、小于1字节)的SDS至少要用3位来存储类型(2^3=8),1个字节8位,剩余的5位存储长度,可以满足长度小于32的短字符串。在Redis 5.0中,我们用如下结构来存储长度小于32的短字符串:

    struct __attribute__ ((__packed__))sdshdr5 {unsigned char flags; /* 低3位存储类型,高5位存储长度 */char buf[]; /*柔性数组,存放实际内容*/};

sdshdr5结构(下图)中,flags占1个字节,其低3位(bit)表示type,高5位(bit)表示长度,能表示的长度区间为0~31(25-1), flags后面就是字符串的内容。
在这里插入图片描述
而长度大于31的字符串,1个字节依然存不下。我们按之前的思路,将len和free单独存放。sdshdr8、sdshdr16、sdshdr32和sdshdr64的结构相同,sdshdr16结构如下图所示:
在这里插入图片描述
其中“表头”共占用了S[2(len)+2(alloc)+1(flags)]个字节。flags的内容与sdshdr5类似,依然采用3位存储类型,但剩余5位不存储长度。

在Redis的源代码中,对类型的宏定义如下:

    #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

在Redis 5.0中,sdshdr8、sdshdr16、sdshdr32和sdshdr64的数据结构如下:

    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[]; /*柔性数组,存放实际内容*/};

可以看到,这4种结构的成员变量类似,唯一的区别是len和alloc的类型不同。结构体中4个字段的具体含义分别如下:

  • len:表示buf中已占用字节数。
  • alloc:表示buf中已分配字节数,不同于free,记录的是为buf分配的总长度。
  • flags:标识当前结构体的类型,低3位用作标识位,高5位预留。
  • buf:柔性数组,真正存储字符串的数据空间。

2、基本操作

数据结构的基本操作不外乎增、删、改、查,SDS也不例外。由于Redis 3.2后的SDS涉及多种类型,修改字符串内容带来的长度变化可能会影响SDS的类型而引发扩容。

2.1、创建字符串

Redis通过sdsnewlen函数创建SDS。在函数中会根据字符串长度选择合适的类型,初始化完相应的统计值后,返回指向字符串内容的指针,根据字符串长度选择不同的类型:

    sds sdsnewlen(const void *init, size_t initlen) {void *sh;sds s;char type = sdsReqType(initlen); //根据字符串长度选择不同的类型if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8; //SDS_TYPE_5强制转化为SDS_TYPE_8int hdrlen = sdsHdrSize(type); //计算不同头部所需的长度unsigned char *fp; /* 指向flags的指针 */sh = s_malloc(hdrlen+initlen+1); //"+1"是为了结束符’\0'...s = (char)sh+hdrlen; //s是指向buf的指针fp = ((unsigned char)s)-1; //s是柔性数组buf的指针,-1即指向flags...s[initlen] = '\0'; //添加末尾的结束符return s;}

注意: Redis 3.2后的SDS结构由1种增至5种,且对于sdshdr5类型,在创建空字符串时会强制转换为sdshdr8。原因可能是创建空字符串后,其内容可能会频繁更新而引发扩容,故创建时直接创建为sdshdr8。

创建SDS的大致流程:首先计算好不同类型的头部和初始长度,然后动态分配内存。需要注意以下3点:

  • 创建空字符串时,SDS_TYPE_5被强制转换为SDS_TYPE_8。
  • 长度计算时有“+1”操作,是为了算上结束符“\0”。
  • 返回值是指向sds结构buf字段的指针。

返回值sds的类型定义如下:

    typedef char *sds;

从源码中我们可以看到,其实s就是一个字符数组的指针,即结构中的buf。这样设计的好处在于直接对上层提供了字符串内容指针,兼容了部分C函数,且通过偏移能迅速定位到SDS结构体的各处成员变量。

2.2、释放字符串

SDS提供了直接释放内存的方法——sdsfree,该方法通过对s的偏移,可定位到SDS结构体的首部,然后调用s_free释放内存:

    void sdsfree(sds s) {if (s == NULL) return;s_free((char)s-sdsHdrSize(s[-1])); //此处直接释放内存}

为了优化性能(减少申请内存的开销), SDS提供了不直接释放内存,而是通过重置统计值达到清空目的的方法——sdsclear。该方法仅将SDS的len归零,此处已存在的buf并没有真正被清除,新的数据可以覆盖写,而不用重新申请内存:

    void sdsclear(sds s) {sdssetlen(s, 0); //统计值len归零s[0] = '\0'; //清空buf}

2.3、拼接字符串

拼接字符串操作本身不复杂,可用sdscatsds来实现,代码如下:

    sds sdscatsds(sds s, const sds t) {return sdscatlen(s, t, sdslen(t));}

sdscatsds是暴露给上层的方法,其最终调用的是sdscatlen。由于其中可能涉及SDS的扩容,sdscatlen中调用sdsMakeRoomFor对带拼接的字符串s容量做检查,若无须扩容则直接返回s;若需要扩容,则返回扩容好的新字符串s。函数中的len、curlen等长度值是不含结束符的,而拼接时用memcpy将两个字符串拼接在一起,指定了相关长度,故该过程保证了二进制安全。最后需要加上结束符。

    /* 将指针t的内容和指针s的内容拼接在一起,该操作是二进制安全的*/sds sdscatlen(sds s, const void *t, size_t len) {size_t curlen = sdslen(s);s = sdsMakeRoomFor(s, len);if (s == NULL) return NULL;memcpy(s+curlen, t, len); //直接拼接,保证了二进制安全sdssetlen(s, curlen+len);s[curlen+len] = '\0'; //加上结束符return s;}

下图描述了sdsMakeRoomFor的实现过程。
在这里插入图片描述
Redis的sds中有如下扩容策略:
1)若sds中剩余空闲长度avail大于新增内容的长度addlen,直接在柔性数组buf末尾追加即可,无须扩容。代码如下:

    sds sdsMakeRoomFor(sds s, size_t addlen){void *sh, *newsh;size_t avail = sdsavail(s);size_t len, newlen;char type, oldtype = s[-1] & SDS_TYPE_MASK; //s[-1]即flagsint hdrlen;if (avail >= addlen) return s; //无须扩容,直接返回...}

2)若sds中剩余空闲长度avail小于或等于新增内容的长度addlen,则分情况讨论:新增后总长度len+addlen<1MB的,按新长度的2倍扩容;新增后总长度len+addlen>1MB的,按新长度加上1MB扩容。代码如下:

    sds sdsMakeRoomFor(sds s, size_t addlen){...newlen = (len+addlen);if (newlen < SDS_MAX_PREALLOC)// SDS_MAX_PREALLOC这个宏的值是1MBnewlen *= 2;elsenewlen += SDS_MAX_PREALLOC;...}

3)最后根据新长度重新选取存储类型,并分配空间。此处若无须更改类型,通过realloc扩大柔性数组即可;否则需要重新开辟内存,并将原字符串的buf内容移动到新位置。具体代码如下:

    sds sdsMakeRoomFor(sds s, size_t addlen){...type = sdsReqType(newlen);/* type5的结构不支持扩容,所以这里需要强制转成type8*/if (type == SDS_TYPE_5) type = SDS_TYPE_8;hdrlen = sdsHdrSize(type);if (oldtype==type) {/*无须更改类型,通过realloc扩大柔性数组即可,注意这里指向buf的指针s被更新了*/newsh = s_realloc(sh, hdrlen+newlen+1);if (newsh == NULL) return NULL;s = (char)newsh+hdrlen;} else {/* 扩容后数据类型和头部长度发生了变化,此时不再进行realloc操作,而是直接重新开辟内存,拼接完内容后,释放旧指针*/newsh = s_malloc(hdrlen+newlen+1); //按新长度重新开辟内存if (newsh == NULL) return NULL;memcpy((char)newsh+hdrlen, s, len+1); //将原buf内容移动到新位置s_free(sh); //释放旧指针s = (char)newsh+hdrlen; //偏移sds结构的起始地址,得到字符串起始地址s[-1] = type; //为falgs赋值sdssetlen(s, len); //为len属性赋值}sdssetalloc(s, newlen); //为alloc属性赋值return s;}

2.4、其余API

下表列出了其他常用的API:
在这里插入图片描述
学习时把握以下两点:

  • SDS暴露给上层的是指向柔性数组buf的指针。
  • 读操作的复杂度多为O(1),直接读取成员变量;涉及修改的写操作,则可能会触发扩容。

这篇关于Redis——简单动态字符串(Simple Dynamic Strings,SDS)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/455644

相关文章

Java中String字符串使用避坑指南

《Java中String字符串使用避坑指南》Java中的String字符串是我们日常编程中用得最多的类之一,看似简单的String使用,却隐藏着不少“坑”,如果不注意,可能会导致性能问题、意外的错误容... 目录8个避坑点如下:1. 字符串的不可变性:每次修改都创建新对象2. 使用 == 比较字符串,陷阱满

IDEA编译报错“java: 常量字符串过长”的原因及解决方法

《IDEA编译报错“java:常量字符串过长”的原因及解决方法》今天在开发过程中,由于尝试将一个文件的Base64字符串设置为常量,结果导致IDEA编译的时候出现了如下报错java:常量字符串过长,... 目录一、问题描述二、问题原因2.1 理论角度2.2 源码角度三、解决方案解决方案①:StringBui

Android 悬浮窗开发示例((动态权限请求 | 前台服务和通知 | 悬浮窗创建 )

《Android悬浮窗开发示例((动态权限请求|前台服务和通知|悬浮窗创建)》本文介绍了Android悬浮窗的实现效果,包括动态权限请求、前台服务和通知的使用,悬浮窗权限需要动态申请并引导... 目录一、悬浮窗 动态权限请求1、动态请求权限2、悬浮窗权限说明3、检查动态权限4、申请动态权限5、权限设置完毕后

C++初始化数组的几种常见方法(简单易懂)

《C++初始化数组的几种常见方法(简单易懂)》本文介绍了C++中数组的初始化方法,包括一维数组和二维数组的初始化,以及用new动态初始化数组,在C++11及以上版本中,还提供了使用std::array... 目录1、初始化一维数组1.1、使用列表初始化(推荐方式)1.2、初始化部分列表1.3、使用std::

redis群集简单部署过程

《redis群集简单部署过程》文章介绍了Redis,一个高性能的键值存储系统,其支持多种数据结构和命令,它还讨论了Redis的服务器端架构、数据存储和获取、协议和命令、高可用性方案、缓存机制以及监控和... 目录Redis介绍1. 基本概念2. 服务器端3. 存储和获取数据4. 协议和命令5. 高可用性6.

Redis的数据过期策略和数据淘汰策略

《Redis的数据过期策略和数据淘汰策略》本文主要介绍了Redis的数据过期策略和数据淘汰策略,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录一、数据过期策略1、惰性删除2、定期删除二、数据淘汰策略1、数据淘汰策略概念2、8种数据淘汰策略

JAVA调用Deepseek的api完成基本对话简单代码示例

《JAVA调用Deepseek的api完成基本对话简单代码示例》:本文主要介绍JAVA调用Deepseek的api完成基本对话的相关资料,文中详细讲解了如何获取DeepSeekAPI密钥、添加H... 获取API密钥首先,从DeepSeek平台获取API密钥,用于身份验证。添加HTTP客户端依赖使用Jav

Redis存储的列表分页和检索的实现方法

《Redis存储的列表分页和检索的实现方法》在Redis中,列表(List)是一种有序的数据结构,通常用于存储一系列元素,由于列表是有序的,可以通过索引来访问元素,因此可以很方便地实现分页和检索功能,... 目录一、Redis 列表的基本操作二、分页实现三、检索实现3.1 方法 1:客户端过滤3.2 方法

Java使用POI-TL和JFreeChart动态生成Word报告

《Java使用POI-TL和JFreeChart动态生成Word报告》本文介绍了使用POI-TL和JFreeChart生成包含动态数据和图表的Word报告的方法,并分享了实际开发中的踩坑经验,通过代码... 目录前言一、需求背景二、方案分析三、 POI-TL + JFreeChart 实现3.1 Maven

Python中操作Redis的常用方法小结

《Python中操作Redis的常用方法小结》这篇文章主要为大家详细介绍了Python中操作Redis的常用方法,文中的示例代码简洁易懂,具有一定的借鉴价值,有需要的小伙伴可以了解一下... 目录安装Redis开启、关闭Redisredis数据结构redis-cli操作安装redis-py数据库连接和释放增