Redis-简单动态字符串

news2024/11/26 0:44:58

Redis中字符串应该是我们使用最多的一种数据类型了,但是有没有想过Redis是如何存储字符串的呢?Redis并没有用C语言传统的字符串(C语言中的字符串一般末尾采用空字符结尾,'\0'),而是采用它们自己实现的一种简单动态字符串(SDS)实现的;

比如说我们在Redis客户端执行如下命令:

set msg "hello world"

那么Redis将会在数据库中创建一个新的键值对,键msg是一个字符串对象,底层就是保存着字符串"msg"的SDS;值也一样,底层也是一个SDS;

SDS的定义

在源代码sds.h的头文件中,可以看到SDS的定义:

==========================stdint.h==================================
/* Unsigned.  */
typedef unsigned char        uint8_t;
typedef unsigned short int    uint16_t;
#ifndef __uint32_t_defined
typedef unsigned int        uint32_t;
# define __uint32_t_defined
#endif
#if __WORDSIZE == 64
typedef unsigned long int    uint64_t;
==========================分割线(sds.h)===============================
typedef char *sds;
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[];
};
=====================================================================

可以看到有sdshdr5(没有使用)、sdshdr8、sdshdr16、sdshdr32、sdshdr64;它们有什么区别呢?其实是针对不同长度的字符串采用不同的数据类型去存储对应的值,可以看到sdshdr8,它的len和alloc都是使用的uint8_t,而在stdint.h中可以看到uint8_t使用的是char类型,而其它不同长度对应的类型也不同;不同长度的字符串用不同的数据类型存储,这样就可以节省空间。

sds存储结构:

其中len表示已经使用了的长度,alloc表示实际分配的长度,buf是真正存储字符串的数组。

为什么要自己定义SDS来存储字符串

(1)SDS获取字符串长度的时间复杂度为O(1),而C语言本身提供的字符串获取长度时间复杂度为O(N)

从上面存储结构可以看出来,在结构中有个len属性,如果我们=想获取字符串的长度直接访问这个len属性即可拿到;而使用C语言本身提供的字符串,要获取长度的话需要遍历整个字符串计算出这个字符串的长度,时间复杂度为O(N);所以在Redis获取一个字符串的长度是很快的。对Redis服务器几乎不会造成任何性能上的影响。

(2)在对字符串进行拼接的时候可以动态扩容并且不会导致缓冲区溢出

在C语言有一个函数,可以把拼接两个字符串:char *strcat(char *dest,const char *src);当dest后面内存如果紧跟的有其它字符串的话,这个函数可能会产生内存溢出;而sds的字符串拼接因为记录了字符串的长度,所以在使用前会先判断,如果不够会再分配足够的内存之后在进行拼接,sds的拼接函数:sds sdscat(sds s,char * t) 。

sds字符串拼接 源代码:

//可以看注释,知道大概流程
sds sdscat(sds s, const char *t) {
    return sdscatlen(s, t, strlen(t));
}
sds sdscatlen(sds s, const void *t, size_t len) {
    size_t curlen = sdslen(s); //当前字符的长度
    s = sdsMakeRoomFor(s,len); //为s扩容,腾出足够的空间,len是拼接字符串的长度
    if (s == NULL) return NULL;
    memcpy(s+curlen, t, len); //拼接字符串
    sdssetlen(s, curlen+len); //将字符串的长度更新为 目前的长度+分配的长度
    s[curlen+len] = '\0';  //结尾字符 sds并没有算上它的长度,对用户来说是不可见的
    return s;  
}

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;
    
    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)  //SDS_MAX_PREALLOC = 1024*1024 = 1M
        newlen *= 2; //扩容 新长度的2倍
    else
        newlen += SDS_MAX_PREALLOC;  //大于1M时 直接多分配1M的空间

    type = sdsReqType(newlen);

    if (type == SDS_TYPE_5) type = SDS_TYPE_8; //如果是sdshdr5 直接向上转换为 sdshdr8

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

说实话,上面这段源代码的 :sds sdsMakeRoomFor(sds s, size_t addlen) 方法我没有完全看懂,我只知道大概意思,因为我本身是搞Java开发的,C语言是在大学学的,加上很久没有用过了所以导致看起来比较困难,如果你看懂了的话,请留言给我讲解一下。阿里嘎多 /(ㄒoㄒ)/~~

(3)减少了修改字符串时带来的内存重新分配次数

在C语言字符串底层实现使用数组实现的,在每次对字符串进行修改的时候都会对存储字符串的数组进行一次内存重新分配操作(Java可能就是基于这个原因,才把字符串底层的数组设置为final的把把;比如说对字符串进行拼接,那么就需要内存重新分配来扩展底层数组的大小,必然就会像上面说的那样产生内存溢出。因为每次内存重新都会涉及复杂的算法,还有可能会执行系统调用,所以对于Redis这种使用字符串频率很高的系统,会严重影响性能。所以Redis通过空间预分配和惰性空间释放两种优化方案来大大提升了效率。

空间预分配:

可以简单的理解为 在对字符串进行增长操作的时候,可以给这个字符数组多分配一些预留空间(java集合动态扩容也用到了这种思想),它的扩容方式你看过上面的代码应该有印象了,当SDS的长度小于1M时,直接扩容二倍,当超过1M时就直接多1M。

 if (newlen < SDS_MAX_PREALLOC)  //SDS_MAX_PREALLOC = 1024*1024 = 1M
        newlen *= 2; //扩容 新长度的2倍
    else
        newlen += SDS_MAX_PREALLOC;  //大于1M时 直接多分配1M的空间

惰性空间释放:

惰性空间释放就是当对字符串进行截取的时候(简单理解为缩短字符串的长度),并不会立即释放已经没有使用的空间,而是预留,以免在后面的时候又使用;我们知道SDS有len和alloc这两个属性,它们会记录记录当前字符数组使用情况;

可以看到这时候字符数组已经满了,假设我们对它进行截取操作,只要he了,那么这时候会变成什么样子呢?

剩余未使用的空间依然保留了,下次在进行增加的时候可以通过len属性知道还有剩余足够空间,不用进行扩容,直接使用就可以了,不需要再去进行内存重新分配;提升了使用效率。

总结

我简单介绍了Redis底层字符串的存储结构,字符串在Redis中使用非常多,它们通过定义简单动态字符串(SDS)来提升了字符串的使用与分配很大程度上提高了Redis的效率;是不是发现人家的思维真的很厉害?光一个字符串都能优化到这种程度,其实去学习一些东西的时候,还是有必要学习一下底层原理,因为有一些设计思想真的很棒。

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

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

相关文章

若依ruoyi——手把手教你制作自己的管理系统【二、修改样式】

阿里图标一(&#xffe3;︶&#xffe3;*)) 图片白嫖一((*&#xffe3;3&#xffe3;)╭ ********* 专栏略长 爆肝万字 细节狂魔 请准备好一键三连 ********* 运行成功后&#xff1a; idea后台正常先挂着 我习惯用VScode操作 当然如果有两台机子 一个挂后台一个改前端就更好…

java中volatile与synchronized的区别,volatile为什么不能保证原子性

1.volatile与synchronized的区别 2.volatile为什么不能保证原子性 定义&#xff1a; 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断&#xff0c;要么就都不执行。 原子性是拒绝多线程操作的&#xff0c;不论是多核还是单核&#xff0c;具有原子性的量…

【C/C++】VS2019下C++生成DLL并且成功调用(金针菇般细)

目录 一&#xff0c;生成动态链接库 二&#xff0c;使用动态链接库 一&#xff0c;生成动态链接库 1.打开VS2019&#xff0c;创建新项目&#xff0c;选择 动态链接库(DLL) 模板后进行下一步 2.输入项目名称&#xff0c;其它默认就行(可自行选择)&#xff0c;点击创建 3 工程…

hive开窗函数

hive开窗函数 窗口函数 数据准备 1 jx 20 2 zx 24 3 yx 18 4 wz 10 5 yy 34 6 wy 25create table t (> id int,> name string,> age int> )> row format delimited fields terminated by ; load data inpath /data/data.txt into table t;ROW_NUMBER ROW_N…

网上订餐项目(含后台管理界面)

项目开发环境 项目使用IDEA 2018.3.5进行开发。Maven版本为 3.6.2。Tomcat版本为 8.5.42。数据库为mysql 5.7。JDK版本为1.8_211。项目使用SpringSpringMVCMybits框架。 点餐前台功能 登陆界面如下 登陆后可添加菜品到餐车 餐车里可查看添加的菜品 提交后可查看已派送和未…

Zookeeper配置化中心

zookeeper的基本知识 zookeeper的数据结构:zookeeper提供的命名空间非常类似于标准的文件系统&#xff0c;key-value的形式存储&#xff0c;名称key由/分割的一系列路径元素&#xff0c;zookeeper名称空间中的每个节点都是一个路径标志。 windows下的zookeeper安装&#…

使用Docker快速部署ES单机

所有的操作都是基于Docker来的&#xff0c;没有装Docker的话请参照官方文档安装单机环境部署初始化相关目录mkdir -p /usr/local/elasticsearch/{config,plugins,data}准备配置文件vim /usr/local/elasticsearch/config/elasticsearch.yml将下面的内容粘贴到elasticsearch.yml#…

训练一个中文gpt2模型

前言 这是我的github上的一个介绍&#xff0c;关于如何训练中文版本的gpt2的。链接为: https://github.com/yuanzhoulvpi2017/zero_nlp 介绍 本文&#xff0c;将介绍如何使用中文语料&#xff0c;训练一个gpt2可以使用你自己的数据训练&#xff0c;用来&#xff1a;写新闻、…

linux中top命令分析

TOP命令是 比较常用的性能分析命令&#xff0c;可以看出服务器CPU 、负载、内存、磁盘、IO等数值&#xff0c;接下来就详细解读top命令 top命令 打开服务器终端&#xff0c;直接输入top&#xff0c;top命令中的数据显示的都是当前的实时数据 直接这样输入&#xff0c;回车即…

iptables防火墙屏蔽指定ip的端口

因为需要测试客户端程序与hadoop服务器之间正常通信需要开通的端口, 所以在hadoop各服务器上使用iptables防火墙屏蔽了测试客户端程序的ip和所有端口。然后&#xff0c;根据报错信息提示的端口号来逐步放开直到能正常通信下载文件。 在服务器端屏蔽指定ip访问所有端口 #查看…

UUID的弊端以及雪花算法

目录 一、问题 为什么需要分布式全局唯一ID以及分布式ID的业务需求 ID生成规则部分硬性要求 ID号生成系统的可用性要求 二、一般通用方案 &#xff08;一&#xff09;UUID &#xff08;二&#xff09;数据库自增主键 &#xff08;三&#xff09;Redis生成全局id策略 三…

与AI相遇 | 在ChatGPT中输入“情人节”,我们会得到......?

最近ChatGPT可谓是风靡全球&#xff0c;大家彼此的问候从“你吃饭了吗”变成“你玩ChatGPT了吗”。这款当今最火爆的AI语言模型&#xff0c;是美国人工智能研究实验室OpenAI新推出的一种人工智能技术驱动的自然语言处理工具&#xff0c;使用了Transformer神经网络架构&#xff…

校园创新创业基地管理系统(java,jsp,ssh,mysql)+全套视频教程

技术栈: JAVA,SSH, MYSQL, JQUERY,HTML,CSS, JAVASCRIPT首页访问 http://localhost:8080/Struts2.3.16.1Hibernate4.3.4Spring4.0.2/index_index.action管理员admin 123456用户 user1 123456代码功能演示&#xff1a; http://woc.xin/8C64kZ功能列表:本系统包含普通用户,后台管…

LeetCode(Java)—— 加一(简单)

加一概述&#xff1a;给定一个由整数组成的非空数组所表示的非负整数&#xff0c;在该数的基础上加一。最高位数字存放在数组的首位&#xff0c; 数组中每个元素只存储单个数字。你可以假设除了整数 0 之外&#xff0c;这个整数不会以零开头。输入&#xff1a;digits [1,2,3] …

如何设计一个API接口?

在日常开发中&#xff0c;总会接触到各种接口。前后端数据传输接口&#xff0c;第三方业务平台接口。一个平台的前后端数据传输接口一般都会在内网环境下通信&#xff0c;而且会使用安全框架&#xff0c;所以安全性可以得到很好的保护。这篇文章重点讨论一下提供给第三方平台的…

Python readline()和readlines()函数:按行读取文件

如果想读取用 open() 函数打开的文件中的内容&#xff0c;除了可以使用 read() 函数&#xff0c;还可以使用 readline() 和 readlines() 函数。和 read() 函数不同&#xff0c;这 2 个函数都以“行”作为读取单位&#xff0c;即每次都读取目标文件中的一行。对于读取以文本格式…

基于JAVA+SpringBoot+VUE的心理健康测试系统的设计与实现

✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1f345; 一、项目背景介绍&#xff1a; 随着现代社会的不断发…

写给小白,Python 爬虫学习思路

爬虫是Python是一个很经典的方向&#xff0c;大多数的小伙伴看到的是Python爬虫的就业效果&#xff0c;确实Python爬虫学习成本低&#xff08;学习快&#xff09;&#xff0c;就业效果好&#xff0c;特别适合新手入门&#xff0c;但是也要关注另外一个点&#xff0c;就是Python…

iTerm2 + Oh My Zsh 打造舒适终端体验

最终效果图&#xff1a; 因为powerline以及homebrew均需要安装command line tool&#xff0c;网络条件优越的同学在执行本文下面内容之前&#xff0c;可以先安装XCode并打开运行一次&#xff08;会初始化安装components&#xff09;&#xff0c;省去以后在iterm2中的等待时间。…

LeetCode 234. 回文链表 | C语言版

LeetCode 234. 回文链表 | C语言版LeetCode 234. 回文链表题目描述解题思路思路一&#xff1a;使用快慢双指针代码实现运行结果参考文章&#xff1a;[https://leetcode.cn/problems/palindrome-linked-list/solutions/1011052/dai-ma-sui-xiang-lu-234-hui-wen-lian-bia-qs0k/?…