Redis字符串的表示

news2024/11/25 4:40:35

字符串的表示

Redis 是由 c 语言开发的,但是 Redis 使用字符串的类型却没有采用 c 语言的字符串类型,接下来我们看看为什么要采用这样的设计

c 语言表示字符串用字符数组,用’\0’这样的字符结尾

一、Redis 字符串的表示——SDS

Redis 自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型,并将 SDS 作为 Redis 的默认字符串表示。

struct sdshdr {
    //len 保存了SDS保存字符串的长度    
    int len;
    //free 记录了buf数组中未使用的字节数量     
    int free;
    //buf[] 数组用来保存字符串的每个元素     
    char buf[];
}

1. 二进制安全

因为 C 字符串以空字符作为字符串结束的标识,而对于一些二进制文件(如图片等),内容可能包括空字符串,因此 C 字符串无法正确存取;

而所有 SDS 的 API 都是以处理二进制的方式来处理 buf 里面的元素,并且 SDS 不是以空字符串来判断是否结束,而是以len 属性表示的长度来判断字符串是否结束

2. 减少修改字符串的内存重新分配次数

C 语言中如果要修改字符串,必须要重新分配内存(先释放再申请),因为如果没有重新分配,字符串长度增大时会造成内存缓冲区溢出,字符串长度减小时会造成内存泄露

而对于 SDS,由于 len 属性和 free 属性的存在,对于修改字符串 SDS 实现了空间预分配和惰性空间释放两种策略:

1、空间预分配:对字符串进行空间扩展的时候,扩展的内存比实际需要的多,这样可以减少连续执行字符串增长操作所需的内存重分配次数。

redis储存字符串的大小小于1MB 的时候 , 存储任意的字符串, 其 free大小永远与自身的大小相同;当字符串大小大于1MB时,其就分配free大小固定为1MB

2、惰性空间释放:对字符串进行缩短操作时,程序不立即使用内存重新分配来回收缩短后多余的字节,而是使用 free 属性将这些字节的数量记录下来,等待后续使用。(当然 SDS 也提供了相应的 API,当我们有需要时,也可以手动释放这些未使用的空间。)

3.兼容部分 C 字符串函数

虽然 SDS 是二进制安全的,但是一样遵从每个字符串都是以空字符串结尾的惯例,这样可以重用 C 语言库 中的一部分函数

4.杜绝缓冲区溢出

我们知道在 C 语言中使用 strcat 函数来进行两个字符串的拼接,一旦没有分配足够长度的内存空间,就会造成缓冲区溢出。而对于 SDS 数据类型,在进行字符修改的时候,会首先根据记录的 len 属性检查内存空间是否满足需求,如果不满足,会进行相应的空间扩展,然后在进行修改操作,所以不会出现缓冲区溢出

5.字符串长度 O(1)

由于 len 属性的存在,我们获取 SDS 字符串的长度只需要读取 len 属性,时间复杂度为 O(1)。而对于 C 语言,获取字符串的长度通常是经过遍历计数来实现的,时间复杂度为 O(n)。通过strlen key命令可以获取 key 的字符串长度

二、Redis3.2版本之后进一步设计了5中结构类型

在 redis3.2 分支出现之前字符串只用 sdshdr 一个类型(上文说到的 SDS),这种结构存在一个弊端,比如每次创建一个字符串,由于 len+free(int 类型,一般操作系统占 4 个字节),最少占用 8 个字节,所以是不管字符串有多长,都要最少占用 8 个字节,比较浪费。

3.2 分支引入了五种 sdshdr 类型,每次在创建一个 sds 时根据 sds 的实际长度判断应该选择什么类型的 sdshdr,不同类型的 sdshdr 占用的内存空间不同。这种细分可以极大的节省空间,下面是 3.2 版本的 sdshdr 定义:

1. sdshdr5

实际上这个类型redis不会被使用,因为没有剩余空间的字段,不方便扩容。【可忽略】

struct __attribute__ ((__packed__)) sdshdr5 {
    //实际上这个类型redis不会被使用,因为没有剩余空间的字段,不方便扩容。他的内部结构也与其他sdshdr不同,直接看sdshdr8就好。
    unsigned char flags;
    //一共8位,低3位用来存放真实的flags(类型),高5位用来存放len(长度)。
    char buf[];
    //sds实际存放的位置
};

img

根据上图看到,flags是char类型1个字节,利用字符串第一个字节表示,由于sds有5中类型,所以flags的前三位表示sds类型,后5位表示存储数据的长度,所以该类型只能存小于2^5大小字节的数据。

2. sdshdr8

struct __attribute__ ((__packed__)) sdshdr8 {
  uint8_t len;//表示当前sds的长度(单位是字节)
  uint8_t alloc; //表示已为sds分配的内存大小(单位是字节)
  //用一个字节表示当前sdshdr的类型,因为有sdshdr有五种类型,所以至少需要3位来表示
  //000:sdshdr5,001:sdshdr8,010:sdshdr16,011:sdshdr32,100:sdshdr64。高5位用不到所以都为0。
  unsigned char flags;
  char buf[];//sds实际存放的位置
};

img

  • len:表示当前 sds 的长度,不包括’/0’终止符,可直接获取获取长度,注意单位是字节,字符串的第一个字节
  • alloc:当前已分配的大小(3.2 以前的版本用的 free 是表示还剩 free 字节可用空间),不包括’/0’终止符,注意单位是字节,字符串的第二个字节
  • flags 表示当前 sdshdr 的类型,声明为 char ,则表示一共有 1 个字节(8 位),仅用低三位就可以表示所有 5 种 sdshdr 类型(参考上图表示),高5位用不到所以都为0。,字符串的第三个字节。

低三位表示:000:sdshdr5,001:sdshdr8,010:sdshdr16,011:sdshdr32,100:sdshdr64

3. sdshdr16、sdshdr32、sdshdr64

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

上述结构可类比sdshdr8

4. 如何选择使用哪种结构

redis 在创建一个 sds 之前,会调用 「sdsReqType(size_t string_size)来判断用哪个 sdshdr」。该函数传递一个 sds 的长度作为参数,返回应该选用的 sdshdr 类型

#define SDS_TYPE_5  0 //00000000
#define SDS_TYPE_8  1 //00000001
#define SDS_TYPE_16 2 //00000010
#define SDS_TYPE_32 3 //00000011
#define SDS_TYPE_64 4 //00000100

#define SDS_TYPE_MASK 7 //00000111,作为取flags低3位的掩码

static inline char sdsReqType(size_t string_size) {
    if (string_size < 1<<5) //小于2^5,flags成员的高5位即可表示
        return SDS_TYPE_5;
    if (string_size < 1<<8) //小于2^8,8位整数(sdshdr8里的uint8_t)即可表示string_size
        return SDS_TYPE_8;
    if (string_size < 1<<16) //小于2^16,16位整数(sdshdr16里的uint16_t)即可表示string_size
        return SDS_TYPE_16;
    //小于2^32,32位整数(sdshrd32里的uint32_t)即可表示string_size,
    //1ll是指1long long(至少64位)的意思,如果没有ll,1就是一个int,假设int为4字节32位,
    //1<<32就会导致undefined behavior.
    if (string_size < 1ll<<32) 
        return SDS_TYPE_32;
    return SDS_TYPE_64; //若sds的长度超过2^64,则所有类型都不法表示这个sds的len
}

所以涉及到一些关于字符串相关的函数,都存放在sds.h 文件中,比如求字符串长度的函数,只需要将sds作为参数,通过比较 flags&SDS_TYPE_MASK 和 SDS_TYPE_n 来判断该 sds 属于哪种类型 sdshdr,再按照指定的 sdshdr 类型取出 sds 的相关信息。 例如 sdslen 函数(获取字符串长度)

注意这里面其实我们判断使用sdshrd用那个类型,只需要flags&SDS_TYPE_MASK 和 SDS_TYPE_n 比较即可(之所以需要 SDS_TYPE_MASK 是因为有 sdshdr5 这个特例,它的高 5 位不一定为 0)

//返回一个类型为T包含s字符串的sdshdr的指针
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T)))) 
//用sdshdr5的flags成员变量做参数返回sds的长度,这其实是一个没办法的hack  
#define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS)  
#define SDS_TYPE_BITS 3
static inline size_t sdslen(const sds s) {
    //通过 s[-1]我们可以获得 sds 所属的 sdshdr 的成员变量 flags
    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;//取出sdshdr的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;

类似 sdslen 这样利用 sds 找到 sdshdr 类型的还有如下几个函数,就不一一分析了:

static inline size_t sdsavail(const sds s)
static inline void sdssetlen(sds s, size_t newlen)
static inline void sdsinclen(sds s, size_t inc)
static inline size_t sdsalloc(const sds s)
static inline void sdssetalloc(sds s, size_t newlen)

三、总结

本节内容主要讲解了Redis对字符串的表示方法,之所以不采用c语言中的字符串表示,主要基于安全性、内存的分配及提高字符长度的获取时间复杂度等,而且在3.2之后采用的5中sdshdr结构来表示不同的字符串更加极致的节省了内存的空间。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1164409.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

leetCode 746. 使用最小花费爬楼梯 + 记忆化搜索 + 递推 + 动态规划 + 空间优化

关于此题我的往期文章&#xff1a; leetCode 746. 使用最小花费爬楼梯 动态规划-CSDN博客https://heheda.blog.csdn.net/article/details/133325840 dfs(i-1) 跳到 dfs(i) 需要花费 dfs(i-1) cost[i-1]dfs(i-2) 跳到 dfs(i) 需要花费 dfs(i-2) cost[i-2] &#xff08;1&…

node教程(四)Mongodb+mongoose

文章目录 一、mongodb1.简介1.1Mongodb是什么&#xff1f;1.2数据库是什么&#xff1f;1.3数据库的作用1.4数据库管理数据的特点 2.核心概念3.下载安装与启动4.命令行交互4.1数据库命令4.3文档命令 二、Mongoose1.介绍2.作用3.使用流程4.插入文档5.mongoose字段类型 一、mongod…

【服务器】Redis的安装及使用命令(Linux、Windows版)

目录 一、Redis简介 二、Redis安装 1、Linux版 1.1、下载 1.2、导入 1.3、解压 1.4、安装 1.5、修改文件 1.6、启动redis 1.7、测试 1.8、结束进程 1.9、修改密码访问 1.10、安装客户端工具&连接 2、Windows版 2.1、下载 2.2、安装 2.3、修改 2.4、连接 …

【Qt控件之QMovie】详解

Qt控件之QMovies 概述公共类型属性公共函数公共槽函数信号静态公共成员示例使用场景 概述 QMovie类是一个方便的类&#xff0c;用于播放具有QImageReader的动画。此类用于显示没有声音的简单动画。如果您想显示视频和媒体内容&#xff0c;请改用Qt多媒体框架Qt Multimedia mul…

整理笔记——0欧电阻、电感、磁珠

设计电路时&#xff0c;经常用到0欧电阻、电感、磁珠&#xff0c;这三个基础电子原件万用表量都是“短路”&#xff0c;这三者之间有什么区别&#xff1f;什么情况下用什么原件&#xff1f; 一、0欧电阻 0欧电阻&#xff0c;并不是指元件的电阻值为0&#xff0c;而是电阻值很小…

SQL面试

#(1)请写出要查询员工J开头的名字其工号(EMPNO)及部门名称(DEPTNA)的 SQL语句SELECT e.emp,e.name,d.deptna FROM emp e left join dept d on d.deptno e.deptno where e.name like J%#(2)请写出要查询 Kevin 所在部门的部门代号(DEPTNO)及部门名称(DEPTNA)的 SQL 语句SELECT e…

手持创新疫苗“国际名片”,康希诺叩开全球市场大门

消灭病痛&#xff0c;重在防患于未然&#xff0c;消灭病源和阻断传播渠道。疫苗&#xff0c;因此成为了全人类“防未病”的重要手段。而当着眼于有全球性风险的疾病&#xff0c;疫苗创新的国际化就显得尤为重要。 刚刚过去不久的10月24日&#xff0c;世界脊髓灰质炎日&#xf…

【kafka】记一次kafka基于linux的原生命令的使用

环境是linux&#xff0c;4台机器&#xff0c;版本3.6&#xff0c;kafka安装在node 1 2 3 上&#xff0c;zookeeper安装在node2 3 4上。 安装好kafka&#xff0c;进入bin目录&#xff0c;可以看到有很多sh文件&#xff0c;是我们执行命令的基础。 启动kafka&#xff0c;下面的…

蓝桥白皮书16.0版——2、蓝桥等考介绍及代报名方式、报名时间

等级考试综述 蓝桥等考全称为“蓝桥青少年信息技术等级考试” 。等级考试聚焦学生学习过程的跟 踪评价 &#xff0c;以考促学 &#xff0c;标准化中小学校教学、校外机构培训和家长学生自学的学习目标及学习进程。 等级考试命题原则 等级考试各组别考试范围是掌握该组别编程知识…

js字符串支持多个分隔符分割

js字符串支持多个分隔符分割 场景代码 场景 用户输入内容后&#xff0c;支持多个分隔符&#xff08;比如&#xff1a;中英文逗号&#xff0c;分号以及换号&#xff09;对字符串进行分割&#xff0c;之后提交给后台同学解析。 代码 function splitString(inputString, separat…

封装taro的api请求工具request.ts,并发送网络请求

使用taro的网络请求工具时&#xff0c;并没有统一的请求封装工具&#xff0c;这里我来分享一下我的请求工具&#xff0c;可以设置base_url和超时时间&#xff0c;还有响应数据格式处理。 在utils/request.ts中封装请求&#xff1a; import Taro from tarojs/taro;const TIME_…

Java——StringBuffer与StringBuilder的区别

Java——StringBuffer与StringBuilder的区别 StringBuffer和StringBuilder是Java中用于处理字符串的两个类&#xff0c;它们之间的主要区别在于线程安全性和性能方面。 1. 线程安全性&#xff1a; StringBuffer&#xff1a;StringBuffer 是线程安全的&#xff0c;所有的公共方…

知心早安问候语,愿你享受美好的时光,幸福快乐每一天

人生万里路&#xff0c;走好每一步&#xff0c;身体是本钱&#xff0c;平安是财富&#xff0c;开心就是护身符&#xff0c;健康才是摇钱树。新的一天&#xff0c;事事顺意&#xff01; 晨起福门开&#xff0c;快乐安康在&#xff0c;愉悦心态好&#xff0c;生活充满爱&#xf…

Blazor 虚拟滚动/瀑布流加载Table数据

page "/virtualScrolling" using BlazorApp.Data<h3>Table 虚拟滚动行</h3> <h4>Table 组件显示大数据时通常采用分页加载数据&#xff0c;还有一种虚拟行的技术类似手机滚动到底部时后台自动加载数据</h4><p>快速滚动时显示行占位&am…

【Linux系统学习】系统编程开发工具编译器gcc/g++使用

个人主页点击直达&#xff1a;小白不是程序媛 Linux专栏&#xff1a;Linux系统学习 目录 前言 Linux系统下安装gcc和g gcc和g的不同 gcc/g的使用 gcc/g选项 预处理 头文件的展开 宏替换 注释的删除 条件的编译 编译 汇编 链接 系统库 库的分类 库的安装 库的…

RT-Thread 9. VS2012下仿真RT-Thread 和LVGL

1. 在ENV中添加组件 2. 下载组件 3. 生成代码 4. 打开代码 双击project.vcxproj 编译 5. 运行

偶数矩阵判断【C语言作业】

题目 若一个布尔矩阵所有行和所有列的和都是偶数&#xff0c;则称为偶数矩阵。请编写一个程序&#xff0c;判断一个布尔矩阵是否是偶数矩阵。 要求&#xff1a; &#xff08;1&#xff09;输入:首先输入一个正整数n(n<100),代表该矩阵的大小&#xff0c;接下来是n行n列的矩…

Vue实现消费清单明细饼图展示

功能 可以进行消费项增删消费额大于500会标红消费金额合计饼图展示消费项 代码 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initial-…

代码随想录算法训练营第四十一天 | LeetCode 416. 分割等和子集

代码随想录算法训练营第四十一天 | LeetCode 416. 分割等和子集 文章链接&#xff1a;01背包理论基础 01背包理论基础&#xff08;滚动数组&#xff09; 分割等和子集 视频链接&#xff1a;01背包理论基础 01背包理论基础&#xff08;滚动数组&#xff09; 分割等和子集 1. 01 …

腾讯云双11活动时间、活动入口、优惠政策详细解读

2023年腾讯云双11大促活动已开启&#xff0c;作为年终最大的一次优惠促销活动&#xff0c;腾讯云的优惠力度还是不错的&#xff0c;爆款云服务器首年88元&#xff0c;还有9999元大额代金券免费领取&#xff01; 一、腾讯云双11活动时间 即日起至2023-11-30 23:59:59&#xff0…