Redis底层数据结构之String

文章目录

      • 1. 前提回顾
      • 2. RedisObject三大数据类型简介
      • 3. SDS字符串
      • 4. SDS字符串源码分析
      • 5. 总结

1. 前提回顾

前面我们说到redis的String数据结构在底层有多种编码方式。例如我们执行下面两条语句

set k1 v1
set age 17

我们查看类型,发现这类型都是String类型

在这里插入图片描述
我们现在查看底层的编码类型,发现编码类型一个是embstr一个是int类型,虽然都是String字符串,但redis根据value的不同类型设置了不同的编码方式

在这里插入图片描述

redisObject内部对应3大物理编码:intembstrraw

在这里插入图片描述

2. RedisObject三大数据类型简介

  • int

它保存long型(长整型)的64位(8个字节)有符号整数(9223372036854775807,最高19位)。

在这里插入图片描述
只有整数才会使用int类型去编码,如果是浮点数,redis内部其实先将浮点数转化为字符串值,然后再保存。

在这里插入图片描述

  • emstr

代表embstr格式的SDS(Simple Dynamic String 简单动态字符串),保存长度小于44字节的字符串,embstr即embedded String,表示嵌入式的String

  • raw

保存长度大于44字节的字符串

下面给演示一下各个类型的使用:

首先长度小于19位的数字,类型为int

在这里插入图片描述

浮点数底层为emstr

在这里插入图片描述

长度超过19的数字,类型为emstr

在这里插入图片描述

普通字符串是emstr

在这里插入图片描述

长度超过44的字符串,为raw类型

在这里插入图片描述

长度超过44的浮点数,为raw类型

在这里插入图片描述

3. SDS字符串

我们知道c语言底层表示字符串本质上是用的一个char数组。但是redis没有直接复用C语言的字符串的底层实现,而是新建了属于自己的结构—SDS,在redis数据库里,包含字符串值的键值对都是由SDS实现的(redis中所有的键都是由字符串对象实现的即底层是由SDS实现,Redis所有的值对象中包含的字符串对象底层也是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[];
};

可以看到redis底层工定义了五种sds字符串的结构,分别是:sdshdr5sdshdr8sdshdr16sdshdr32sdshdr64。那么各个字段是什么意思:

  • len:代表存储的字符串的长度
  • alloc:存储了为字符串分配的内存空间的长度(即实际分配的空间大小,不包括结构体头部和字符串结束符 \0)
  • flags:代表SDS字符串的类型,上面介绍类型的五种之一
  • buf[]:真正存储字符串的char数组

那上面五种类型有什么不同?

sdshdr5:表示 2 5 2^5 25即共32个字节
sdshdr8:表示 2 8 2^8 28即256个字节
sdshdr16:表示 2 16 2^{16} 216即65536个字节,64KB
sdshdr32:表示 2 32 2^{32} 232即4GB
sdshdr64:表示 2 64 2^{64} 264即17179869184G个字节

len表示SDS实际的长度,使我们在获取字符串的时候能够在O(1)时间复杂度情况下拿到,而不需要遍历一遍char数组

alloc:可以用来计算free就是字符串已经分配的未使用空间,这个值就可以引入预分配空间算法了,而不用去考虑内存分配问题

buf:字符串数组,真正存数据的

alloc 字段的作用是提供了对字符串进行动态增长的能力。当字符串需要扩容时,Redis 可以根据需要重新分配更大的内存空间,并将原有的内容复制到新的内存中,然后释放原来的内存。这样做的好处是可以减少频繁地进行内存分配和释放操作,提高了性能。举个例子,假设有一个 SDS 字符串,其实际长度为 10,但为了防止频繁的内存分配和释放操作,alloc 被设置为了 20。当这个字符串需要扩容时,Redis 可以直接在内存中分配一个长度为 20 的新空间,然后将原字符串内容复制到新的空间中,同时更新 len 和 alloc 字段。这样即使字符串长度增长,也不需要每次都重新分配内存,减少了系统的开销。

现在思考一个问题:Redis为什么不直接使用char数组,而是自己重写设计来一个SDS数据结构?

这是因为C语言没有Java里面的String类型,只能是靠自己的char[]来实现,字符串在C语言中的存储方式,如果现在我们想通过strlen命令获取value的长度,我们就需要从头开始遍历,知道遇到\0即可,这样操作的时间复杂度是O(n),所以redis没有直接使用C语言传统的字符串标识,而是自己构建了一种名为简单动态字符串类型SDS,并作为redis的默认字符串类型。

在这里插入图片描述

4. SDS字符串源码分析

当我们输入set k1 v1 底层到底发生了什么?这里我们详细分析一下

首先看t_string.c文件

/* SET key value [NX] [XX] [KEEPTTL] [GET] [EX <seconds>] [PX <milliseconds>]*     [EXAT <seconds-timestamp>][PXAT <milliseconds-timestamp>] */
void setCommand(client *c) {//key的过期时间robj *expire = NULL;//时间的单位是sint unit = UNIT_SECONDS;int flags = OBJ_NO_FLAGS;if (parseExtendedStringArgumentsOrReply(c,&flags,&unit,&expire,COMMAND_SET) != C_OK) {return;}//c->args[2]就是value,这里调用tryObjectEncoding就是尝试堆value值进行编码,编码后的结果再保存到c->args[2]中c->argv[2] = tryObjectEncoding(c->argv[2]);setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}

当我们在cli写set k1 v1时,它底层本质上调用的是上面的setCommand命令

从上面代码代码可以看出字符串的编码和tryObjectEncoding这个函数时紧密相关的,我们来分析一下底层是怎么做的

  • int

当字符串键值的内存可以用一个64位有符号整数表示时,Redis会将键值转化为long型来存储,此时即对于OBJ_ENDOCDING_INT编码类型。内存内存结构如下:

在这里插入图片描述
OBJECT_ENCODING_INT这个时Redis定义的一个常量,我们可以在object.c文件中看到

char *strEncoding(int encoding) {switch(encoding) {case OBJ_ENCODING_RAW: return "raw";case OBJ_ENCODING_INT: return "int";case OBJ_ENCODING_HT: return "hashtable";case OBJ_ENCODING_QUICKLIST: return "quicklist";case OBJ_ENCODING_LISTPACK: return "listpack";case OBJ_ENCODING_INTSET: return "intset";case OBJ_ENCODING_SKIPLIST: return "skiplist";case OBJ_ENCODING_EMBSTR: return "embstr";case OBJ_ENCODING_STREAM: return "stream";default: return "unknown";}
}

Redis启动时会预先建立10000个分别存储0~9999的redisObject变量做为共享变量,这意味着如果set字符串的键值在0~10000之间的话,就可以直接指向功效变量,而不需要再建立新对象,此时键值不占内存。(这和java中有些包装类设置缓存的原理一样)。如下面两条命令:

set k1 100
set k2 100

在这里插入图片描述

上面的缓存可以再server.h中可以看到定义:

#define OBJ_SHARED_INTEGERS 10000 //缓存去大小struct sharedObjectsStruct {robj *ok, *err, *emptybulk, *czero, *cone, *pong, *space,*queued, *null[4], *nullarray[4], *emptymap[4], *emptyset[4],*emptyarray, *wrongtypeerr, *nokeyerr, *syntaxerr, *sameobjecterr,*outofrangeerr, *noscripterr, *loadingerr,*slowevalerr, *slowscripterr, *slowmoduleerr, *bgsaveerr,*masterdownerr, *roslaveerr, *execaborterr, *noautherr, *noreplicaserr,*busykeyerr, *oomerr, *plus, *messagebulk, *pmessagebulk, *subscribebulk,*unsubscribebulk, *psubscribebulk, *punsubscribebulk, *del, *unlink,*rpop, *lpop, *lpush, *rpoplpush, *lmove, *blmove, *zpopmin, *zpopmax,*emptyscan, *multi, *exec, *left, *right, *hset, *srem, *xgroup, *xclaim,  *script, *replconf, *eval, *persist, *set, *pexpireat, *pexpire, *time, *pxat, *absttl, *retrycount, *force, *justid, *entriesread,*lastid, *ping, *setid, *keepttl, *load, *createconsumer,*getack, *special_asterick, *special_equals, *default_username, *redacted,*ssubscribebulk,*sunsubscribebulk, *smessagebulk,*select[PROTO_SHARED_SELECT_CMDS],*integers[OBJ_SHARED_INTEGERS],*mbulkhdr[OBJ_SHARED_BULKHDR_LEN], /* "*<value>\r\n" */*bulkhdr[OBJ_SHARED_BULKHDR_LEN],  /* "$<value>\r\n" */*maphdr[OBJ_SHARED_BULKHDR_LEN],   /* "%<value>\r\n" */*sethdr[OBJ_SHARED_BULKHDR_LEN];   /* "~<value>\r\n" */sds minstring, maxstring;
};

下面我们看看redis时如何利用这些缓存起来的对象的

/* Try to encode a string object in order to save space */
robj *tryObjectEncodingEx(robj *o, int try_trim) {long value;sds s = o->ptr;size_t len;serverAssertWithInfo(NULL,o,o->type == OBJ_STRING);if (!sdsEncodedObject(o)) return o;if (o->refcount > 1) return o;len = sdslen(s);if (len <= 20 && string2l(s,len,&value)) {if ((server.maxmemory == 0 ||!(server.maxmemory_policy & MAXMEMORY_FLAG_NO_SHARED_INTEGERS)) &&value >= 0 &&value < OBJ_SHARED_INTEGERS){decrRefCount(o);return shared.integers[value];} else {if (o->encoding == OBJ_ENCODING_RAW) {sdsfree(o->ptr);o->encoding = OBJ_ENCODING_INT;o->ptr = (void*) value;return o;} else if (o->encoding == OBJ_ENCODING_EMBSTR) {decrRefCount(o);return createStringObjectFromLongLongForValue(value);}}}/* If the string is small and is still RAW encoded,* try the EMBSTR encoding which is more efficient.* In this representation the object and the SDS string are allocated* in the same chunk of memory to save space and cache misses. */if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) {robj *emb;if (o->encoding == OBJ_ENCODING_EMBSTR) return o;emb = createEmbeddedStringObject(s,sdslen(s));decrRefCount(o);return emb;}/* We can't encode the object...* Do the last try, and at least optimize the SDS string inside */if (try_trim)trimStringObjectIfNeeded(o, 0);/* Return the original object. */return o;
}robj *tryObjectEncoding(robj *o) {return tryObjectEncodingEx(o, 1);
}

关键是下面这几句代码

if ((server.maxmemory == 0 ||!(server.maxmemory_policy & MAXMEMORY_FLAG_NO_SHARED_INTEGERS)) &&value >= 0 &&value < OBJ_SHARED_INTEGERS){decrRefCount(o);return shared.integers[value];} else {if (o->encoding == OBJ_ENCODING_RAW) {sdsfree(o->ptr);o->encoding = OBJ_ENCODING_INT;o->ptr = (void*) value;return o;} else if (o->encoding == OBJ_ENCODING_EMBSTR) {decrRefCount(o);return createStringObjectFromLongLongForValue(value);}}

value < OBJ_SHARED_INTEGERS判断当前值是否小于10000,如何小于shared.integers[value]中直接拿值,上面也是对INT类型编码的核心源码

  • embstr

上面我们分析了int类型是如何编码的,下面我们看看embstr类型是如何编码的

#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
robj *createStringObject(const char *ptr, size_t len) {if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)return createEmbeddedStringObject(ptr,len);elsereturn createRawStringObject(ptr,len);
}robj *createEmbeddedStringObject(const char *ptr, size_t len) {//创建redisObject对象并分配内存,使用 zmalloc 函数分配了一块内存,用于存储字符串对象。这块内存的大小包括了字符串对象结构体 robj 的大小、sdshdr8 结构体的大小以及字符串的长度 len 加上一个字节用于字符串的结束符 \0。robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr8)+len+1);//定义了一个 sdshdr8 结构体指针 sh,它指向了 robj 结构体之后的内存地址。这是因为 robj 结构体之后的内存空间被用来存储字符串数据的头信息。struct sdshdr8 *sh = (void*)(o+1);//类型表示为String类型o->type = OBJ_STRING;//编码为embstr编码o->encoding = OBJ_ENCODING_EMBSTR;//设置了 robj 结构体中的 ptr 字段为指向 sdshdr8 结构体之后的内存地址,即指向字符串的实际内容。o->ptr = sh+1;//引用计数为1o->refcount = 1;//使用次数0o->lru = 0;sh->len = len;sh->alloc = len;sh->flags = SDS_TYPE_8;if (ptr == SDS_NOINIT)sh->buf[len] = '\0';else if (ptr) {memcpy(sh->buf,ptr,len);sh->buf[len] = '\0';} else {memset(sh->buf,0,len+1);}return o;
}

首先len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT判断当前字符串的长度是否小于44,这前面也讲过为什么是44,如果小于则符合emstr类型,则调用createEmbeddedStringObject方法。我们看到ptr指针指向的位置为sh+1。我们从源码可以看出sh是robj 结构体位置,这就说明一个现象,即字符串SDS结构体和其对应的redisObject对象分配在同一连续内存空间,就像讲sds嵌入到redisObject中。

简单来说redisObject和对应的sdshdr8是在内存汇总紧挨着的,使用 embstr 编码时,字符串的内容直接存储在 robj 结构体之后的内存空间中,而不需要额外分配存储空间。这样做的好处是减少了内存分配和内存碎片,提高了内存使用效率。

  • raw

我们继续回到tryEncodingEx方法,如果我们对象的字节数大于44,我们此时就是创建raw类型

obj *createObject(int type, void *ptr) {robj *o = zmalloc(sizeof(*o));o->type = type;o->encoding = OBJ_ENCODING_RAW;o->ptr = ptr;o->refcount = 1;o->lru = 0;return o;
}

当字符串长度大于44的超长字符串时,Redis则将键值的内部编码方式改为OBJ_ENCODING_RAW格式,这与OBJ_ENCODING_EMBSTR的编码方式不同,此时动态字符串sds的内存与其依赖的redisObject内存不再连续。

在这里插入图片描述
下面看一种特殊情况:
在这里插入图片描述

5. 总结

在这里插入图片描述
在这里插入图片描述
redis底层会根据用户给的键值使用不同的编码格式,自适应地选择较优化的内存编码格式,而这一切对用户完全透明!

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

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

相关文章

docker desktop 启动失败

docker desktop 启动失败 1.如果直接启动报错&#xff0c;设置 2.如果启动报 Failed to set version to docker-desktop: exit code: -1的解决方法. netsh winsock reset

vue3+Ts项目按需引入Echarts,并封装成hooks

记录 vue3Ts 项目中&#xff0c;按需引入echarts并进行二次封装使用。 1、安装&#xff1a;npm i echarts 2、新增按需引入配置文件&#xff1a;echartsConfig.ts // 引入 echarts 核心模块&#xff0c;核心模块提供了 echarts 使用必须要的接口。 import * as echarts from …

3.Windows下安装MongoDB和Compass教程

Windows下安装MongoDB 总体体验下来&#xff0c;&#xff0c;要比MySQL的安装简单了许多&#xff0c;没有过多的配置&#xff0c;直接就上手了&#xff01; 1、下载 进入官方的下载页面https://www.mongodb.com/try/download/community&#xff0c;如下选择&#xff0c;我选…

第七节:使用SMB发布Web前端程序

一、概述 一直以来&#xff0c;多数人都使用Apache、IIS、Tomcat等开源或商业Web服务器来运行Web程序&#xff0c;各种参数太多&#xff0c;与我们简单易用逻辑相左。所以在架构设计的时候&#xff0c;我们也在考虑&#xff0c;我们公司的Web程序是否能运行在SMB中&#xff0c;…

52 硬中断的实现

前言 呵呵 中断机制 也是内核中很常见的机制了 中断机制是现代计算机系统中的基本机制之一&#xff0c;它在系统中起着通信网络的作用&#xff0c;以协调系统对各种外部事件的响应和处理&#xff0c;中断是实现多道程序设计的必要条件&#xff0c;中断是CPU 对系统发生的某个…

idea Springboot 在线考试管理系统开发mysql数据库web结构java编程计算机网页

一、源码特点 springboot 在线考试管理系统是一套完善的完整信息系统&#xff0c;结合mvc框架和bootstrap完成本系统springboot spring mybatis &#xff0c;对理解JSP java编程开发语言有帮助系统采用springboot框架&#xff08;MVC模式开发&#xff09;&#xff0c;系统具有…

关于-机器人学导论

机器人学导论&#xff0c;经典的基础教材&#xff0c;原书叫introduction to robotics mechanics and control &#xff0c;在网上看到这个下载&#xff0c;这个好像不像有些书已经免费了&#xff0c;但是太经典拿它当教材的很多&#xff0c;最好看清晰的纸质版&#xff0c;特别…

java-ssm-jsp基于java的信访管理系统的设计与实现

java-ssm-jsp基于java的信访管理系统的设计与实现 获取源码——》公主号&#xff1a;计算机专业毕设大全 获取源码——》公主号&#xff1a;计算机专业毕设大全

Gitlab CI/CD 自动化打包部署前端(vue)项目

一、虚拟机安装 1.vmware下载 2.镜像下载 3.Ubuntu 4.新建虚拟机 一直点下一步&#xff0c;直到点击完成。 5.分配镜像 二、Gitlab CI/CD 自动化部署项目 1.配置GitLab CI/CD&#xff1a; A.在你的Vue.js项目中&#xff0c;创建一个名为.gitlab-ci.yml的文件&#xff0…

windbg调试协议wireshark抓包解析插件

把目录下文件复制到如下位置,Wireshark支持版本4.0以上 C:\Program Files\Wireshark\plugins\4.0\kdnet.lua C:\Program Files\Wireshark\gcrypt.dll C:\Program Files\Wireshark\luagcrypt.dll 启动 “C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\windbg.exe” -k …

使用vscode——配置vue3用户代码片段

一、 设置/配置用户代码片段 二、点击新建全局代码片段文件输入vue.json 三、配置代码片段、 {"Print to console": {"prefix": "vue3","body": ["<template>"," <div></div>","</te…

express+mysql+vue,从零搭建一个商城管理系统14--快递查询(对接快递鸟)

提示&#xff1a;学习express&#xff0c;搭建管理系统 文章目录 前言一、安装md5&#xff0c;axios&#xff0c;qs二、新建config/logistics.js三、修改routes/order.js四、添加商品到购物车总结 前言 需求&#xff1a;主要学习express&#xff0c;所以先写service部分 快递鸟…

Linux进程补充

进程 一、进程创建 1.fork fork() //Linux创建子进程的系统调用&#xff0c;允许创建多个子进程&#xff0c;使用循环创建多个子进程&#xff1b; //需要注意的是&#xff0c;创建出来的进程与父进程在调度是由调度器决定的&#xff0c;并没有严格的先后顺序&#xff1b;二、…

组建对等网

一、概念 对等网络&#xff08;Peer-to-Peer, P2P&#xff09;是一种分布式网络架构&#xff0c;其中每个参与节点&#xff08;称为"对等体"或"节点"&#xff09;既可以作为客户端也可以作为服务器&#xff0c;直接与网络中的其他节点分享资源&#xff08…

基于Python django的人脸识别门禁系统,附源码

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、Python技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&…

Chrome的V8引擎 和操作系统交互介绍

Chrome的V8引擎是一个用C编写的开源JavaScript和WebAssembly引擎&#xff0c;它被用于Chrome浏览器中&#xff0c;以解释和执行JavaScript代码。V8引擎将JavaScript代码转换为机器代码&#xff0c;这使得JavaScript能够以接近本地代码的速度运行。 V8引擎与操作系统的交互主要体…

亚马逊国际获得AMAZON商品详情 API 返回值说明

亚马逊国际的商品详情API返回值通常包含了关于商品的详细信息&#xff0c;这些信息对于开发者来说是非常有价值的&#xff0c;因为它们可以用于构建电商应用、展示商品信息、比价购物等场景。 item_get-获得AMAZON商品详情调用链接获取 amazon.item_get 公共参数 下面是一个简…

Adobe PDF背景设置护眼模式,缓解眼部疲劳

一、背景 在用Adobe PDF看论文时&#xff0c;默认的白色背景看久了&#xff0c;眼睛会特别疲劳&#xff0c;下面介绍如何设置背景为护眼模式。 二、设置PDF为护眼模式 使用Adobe Acrobat Pro DC打开任意PDF文件&#xff0c;在上方工具栏选择“编辑”&#xff0c;在下拉菜单栏…

浏览器同源策略及跨域问题

同源策略&#xff1a;同源策略是一个重要的安全策略&#xff0c;它用于限制一个源的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档&#xff0c;减少可能被攻击的媒介。 同源策略的作用&#xff1a;保护浏览器中网站的安全&#xff0c;限制ajax只…

第五节:使用SMB开发WebSocket通信

一、概述 本节主要讲解在SMB中如何进行websocket快速开发&#xff0c;实现客户端连接、关闭、消息通讯等功能。 示例下载&#xff1a;https://download.csdn.net/download/lllllllllluoyi/88949743 二、创建WebSocket服务器 1、在csdnProject工程中新建一个消息流。 添加W…