Redis的String类型,原来这么占内存

news2025/1/20 6:01:45

Redis的String类型,原来这么占内存

存一个 Long 类型这么占内存,Redis 的内存开销都花在哪儿了?

1、场景介绍

假设现在我们要开发一个图片存储系统,要求这个系统能够根据图片 ID 快速查找到图片存储对象 ID。图片 ID 和图片存储对象 ID 的样例数据如下:

photo_id: 1101000060
photo_obj_id: 3302000080

在这种场景下,图片 ID 和图片存储对象 ID 刚好是一对一的关系,是典型的“键 - 单值”模式,Redis 的 String 类型提供了“一个键对应一个值的数据”的保存形式,在这种场景下刚好适用。

确定使用 String 类型后,接下来我们通过实战,来看看它的内存使用情况。首先通过下面命令连接上 Redis。

本文我使用的 Redis Server 及下文源码都是 6.2.4 版本。

redis-cli -h 127.0.0.1 -p 6379

然后执行下面的命令查看 Redis 的初始内存使用情况。

127.0.0.1:6379> info memory
# Memory
used_memory:871840

接着插入 10 条数据:

10.118.32.170:0> set 1101000060 3302000080
10.118.32.170:0> set 1101000061 3302000081
10.118.32.170:0> set 1101000062 3302000082
10.118.32.170:0> set 1101000063 3302000083
10.118.32.170:0> set 1101000064 3302000084
10.118.32.170:0> set 1101000065 3302000085
10.118.32.170:0> set 1101000066 3302000086
10.118.32.170:0> set 1101000067 3302000087
10.118.32.170:0> set 1101000068 3302000088
10.118.32.170:0> set 1101000069 3302000089

再次查看内存:

127.0.0.1:6379> info memory
# Memory
used_memory:872528

可以看到,存储 10 个图片,内存使用了 688 个字节。一个图片 ID 和图片存储对象 ID 的记录平均用了 68 字节。

但问题是,一组图片 ID 及其存储对象 ID 的记录,实际只需要 16 字节就可以了。图片 ID 和图片存储对象 ID 都是 10 位数,而 8 字节的 Long 类型最大可以表示 2 的 64 次方的数值,肯定可以表示 10 位数。这样算下来只需 16 字节就可以了,为什么 String 类型却用了 68 字节呢?

为了一探究竟,我们不得不从 String 类型的底层实现扒起。

2、String 类型的底层实现

当你保存的数据中包含字符时,String 类型就会用简单动态字符串(Simple Dynamic String,SDS)结构体来保存。

2.1 SDS

SDS 的结构定义在sds.h文件中,在 Redis 3.2 版本之后,SDS 由一种数据结构变成了 5 种数据结构。

/* 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__)) hisdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) hisdshdr8 {
    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__)) hisdshdr16 {
    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__)) hisdshdr32 {
    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__)) hisdshdr64 {
    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[];
};

这 5 种数据结构依次存储不同长度的内容,Redis 会根据 SDS 存储的内容长度来选择不同的结构。

  • sdshdr5:存储大小为 32 字节(2 的 5 次方),只被应用在了 Redis 中的 key 中。
  • sdshdr8:存储大小为 256 字节(2 的 8 次方)。
  • sdshdr16:存储大小为 64KB(2 的 16 次方)。
  • sdshdr32:存储大小为 4GB(2 的 32 次方)。
  • sdshdr64:存储大小为 2 的 64 次方字节。

以 sdshdr8 为例。

  • buf:字节数组,保存实际数据。为了表示字节数组的结束,Redis 会自动在数组最后加一个'\0',这就会额外占用 1 个字节的开销。
  • len:占 4 个字节,表示 buf 的已用长度,不包括'\0'
  • alloc:也占 4 个字节,表示 buf 的实际分配长度,不包括'\0'
  • flags:占 1 个字节,标记当前字节数组的属性,是sdshdr8还是sdshdr16等。(flags 值的定义可以看下面代码)

在源码sds.h中,flags 值定义如下:

#define HI_SDS_TYPE_5  0 
#define HI_SDS_TYPE_8  1
#define HI_SDS_TYPE_16 2
#define HI_SDS_TYPE_32 3
#define HI_SDS_TYPE_64 4

2.2 RedisObject

因为 Redis 的数据类型有很多,而且,不同数据类型都有些相同的元数据要记录,所以,值对象并不是直接存储,而是被包装成redisObject对象,它的定义如下。

typedef struct redisObject {
    unsigned type:4;//对象类型(4位=0.5字节)
    unsigned encoding:4;//编码(4位=0.5字节)
    unsigned lru:LRU_BITS;//记录对象最后一次被应用程序访问的时间(24位=3字节)
    int refcount;//引用计数。等于0时表示可以被垃圾回收(32位=4字节)
    void *ptr;//指向底层实际的数据存储结构,如:sds等(8字节)
} robj;

下面可以帮助我们理解:

为了节省内存空间,Redis 还做了一些优化。

当保存的是 Long 类型整数时,RedisObject 中的指针就直接赋值为整数数据了,这样就不用额外的指针再指向整数了。这种保存方式通常也叫作 int 编码方式。

当保存的是字符串数据,并且字符串小于等于 44 字节时,RedisObject 中的元数据、指针和 SDS 是一块连续的内存区域,这样就可以避免内存碎片。这种布局方式也被称为 embstr 编码方式。

当字符串大于 44 字节时,SDS 的数据量就开始变多了,Redis 就不再把 SDS 和 RedisObject 布局在一起了,而是会给 SDS 分配独立的空间,并用指针指向 SDS 结构。这种布局方式被称为 raw 编码模式。

使用 OBJECT ENCODING 命令可以查看一个数据库键的值对象的编码:

127.0.0.1:6379> SET msg "hello world"
OK
127.0.0.1:6379> OBJECT ENCODING msg
"embstr"
127.0.0.1:6379> SET story "long long long ago..."
OK
127.0.0.1:6379> OBJECT ENCODING story
"raw"
127.0.0.1:6379> SADD numbers 1 3 5
(integer) 3
127.0.0.1:6379> OBJECT ENCODING numbers
"intset"
127.0.0.1:6379> SADD numbers "seven"
(integer) 1
127.0.0.1:6379> OBJECT ENCODING numbers
"hashtable"

注意这个命令SET story "long long long ago...",省略号指的是省略了很多字符。

知道了 SDS 和 RedisObject 额外元数据开销,现在,我们就可以计算 String 类型的内存使用量了。

图片存储对象 ID 是 Long 类型整数,所以可以直接用 int 编码的 RedisObject 保存。每个 int 编码的 RedisObject 元数据部分占 8 字节,指针部分被直接赋值为 8 字节的整数了。图片 ID 使用 sdshdr5 数据结构来保存,会为 10 位的图片 ID 分配 16 个字节,结束符 ‘\0’ 占 1 个字节。

共占用 34 个字节。与上文所说的一个图片 ID 和图片存储对象 ID 的记录平均用了 68 字节相差有点大啊,另外的开销去哪儿了?

2.3 全局哈希表

为了实现从键到值的快速访问,Redis 使用了一个哈希表来保存所有键值对。因为这个哈希表保存了所有的键值对,所以,也称为全局哈希表。哈希表的每一项是一个 dictEntry 的结构体,用来指向一个键值对。dictEntry 结构中有三个 8 字节的指针,分别指向 key、value 以及下一个 dictEntry,三个指针共 24 字节,如下图所示:

jemalloc 在分配内存时,会分配一个最接近 2 的 N 次方的数值。举个例子。如果你申请 6 字节空间,jemalloc 实际会分配 2 的 4 次方即 8 字节空间;如果你申请 24 字节空间,jemalloc 则会分配 32 字节。

最终我们分析出来的内存开销,为 66 字节,比较接近上文场景中的平均值 68 了。

最后

既然 String 类型这么占内存,那么你有好的方案来节省内存吗?

这篇文章内容我准备了一周,如果对你有帮助,可以点个「在看」吗?你的点赞会让作者兴奋得一晚上睡不着觉。

对后面的内容感兴趣,也可以关注公众号「杨同学technotes」,感谢支持!

参考资料

  • 文中的一些命令,参考菜鸟教程:https://www.runoob.com/redis/redis-tutorial.html
  • Redis 的 key 也是 SDS 类型的,参考:https://www.cnblogs.com/lonely-wolf/p/14261486.html
  • SDS 的定义,参考:https://juejin.cn/post/6844903936520880135#heading-6
  • 文章大纲,参考极客时间《Redis核心技术与实战》
  • 《Redis设计与实现》

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

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

相关文章

【Flink基础】-- 源码中的注解

1.Flink自定义注解级别在升级 Flink版本至 1.15.3时,偶然遇到了一个异常,然后就准备详细了解下源码中的注解。设计注解的初衷:为了更好地进行代码和版本管理,Flink使用了Java的注解特性自定义了注解,对代码进行增强说明…

Kubernetes安全扫描之kubescape

一 背景 Kubescape 是第一个用于测试 Kubernetes 是否按照 NSA 和 CISA 的 Kubernetes 强化指南中定义的安全部署的工具 使用 Kubescape 测试集群或扫描单个 YAML 文件并将其集成到您的流程中。 二 特性 功能:提供多云 K8s 集群检测,包括风险分析、安…

stack 中缀表达式求值

【解法一】双栈思路梳理 【解法二】利用逆波兰表达式求解(中缀转后缀) 这个有俩种方法,一是直接根据条件进行各种情况的推导直接由中缀表达式求解, 二就是将中缀表达式转化为后缀表达式,利用更容易的逆波兰表达式求解…

分享微信小程序开发详细步骤

1、梳理小程序开发功能需求自己可以用思维导图写出自己想要开发的小程序里面,需要设置哪些功能,这些功能帮助我解决什么问题。然后把想法用文字形式在思维导图中写出来。如果不知道如果梳理,也可以找专业的产品经理协助处理。如果不知道功能可…

Vector - VT System - CANCANFDLIN板卡_VT6104|6204

对于做车载开发或者测试的朋友来说,大部分对于CANoe(VN1600系列)是相当的熟悉,我们知道CANoe支持CAN&CANFD&LIN,都有对应的硬件来匹配;但是如果需要做台架测试,VN1600系列的硬件放在架子…

读书笔记:来自一个外企优秀销售的业务心法和秘籍(中)

01 普通销售能达到的三种境界第一 投其所好 (110)销售把自己的资源1奉献出去,控制不了对方的回报,新人都处在这个阶段。第二 互利互惠 (1+1=1)大家互相贡献自己的一部分,…

满足你一切需求的 MMYOLO/MMDet 可视化 (一)

可视化在深度学习时代算是核心需求,借助可视化功能,研究者可以快速定位分析模型以及排查问题。在 OpenMMLab 2.0 时代,MMEngine 对常用的可视化需求进行了设计和实现,其具备如下功能: 提供丰富的开箱即用可视化功能&a…

TP6队列与延时队列

安装在此我就不再略过TP6的项目创建过程了&#xff0c;大致就是安装composer工具&#xff0c;安装成功以后&#xff0c;再使用composer去创建项目即可。think-queue 安装composer require topthink/think-queue项目中添加驱动配置我们需要在安装好的config下找到 queue.php<…

Solon v1.12.1 发布,已累计 10000+ 次提交

一个更现代感的 Java 应用开发框架&#xff1a;更快、更小、更自由。没有 Spring&#xff0c;没有 Servlet&#xff0c;没有 JavaEE&#xff1b;独立的轻量生态。主框架仅 0.1 MB。 Controller public class App {public static void main(String[] args) {Solon.start(App.cl…

leetcode刷题精讲————17.电话号码的字母组合

力扣https://leetcode.cn/problems/letter-combinations-of-a-phone-number/description/ 这道题要用到多叉树遍历、回溯、递归、排列组合等相关知识&#xff0c;算是比较经典的例题了&#xff0c;掌握它的核心思想就可以解决这一大类问题。 首先&#xff0c;2~9的数字对应不同…

JS 安全策略 101

依赖审计 依赖审计其实就是利用 npm 或是 yarn 自带的一个 audit 命令检测 node_module 里存在的一些具有安全隐患的依赖项。我习惯用yarn audit, 所以给大家放了张自己博客的 yarn 审计结果。这里显示&#xff1a;一个叫 trim 的包太老了&#xff0c;有很高的安全风险。 有风…

【数据结构初阶】5. 栈和队列

栈 1.1 栈的概念及结构 栈&#xff1a;一种特殊的线性表&#xff0c;其只允许在固定的一端进行插入和删除元素操作。**进行数据插入和删除操作的一端 称为栈顶&#xff0c;另一端称为栈底。**栈中的数据元素遵守后进先出LIFO&#xff08;Last In First Out&#xff09;的原则…

win7安装onenote2016时碰到30094-1011(0)的 问题

安装办法用途【1】中所有的更新【1】中下载IE11的依赖IE11360软件管家解决OneNote的登录界面不显示问题微软常用运行库360软件管家kernalbase.dll等文件的缺失windowsupdateagent-7.6-x64官网下载Windows6.1-KB4474419-v3-ia64.msu控制面板的update中选择安装支持sha-2校验【3】…

【算法】差分数组

目录1.概述2.代码实现3.应用本文参考&#xff1a; LABULADONG 的算法网站 1.概述 &#xff08;1&#xff09;差分数组的思想与前缀和算法的非常近似&#xff08;有关前置和算法的具体细节可以参考前缀和算法这篇文章&#xff09;&#xff0c;其主要适用于频繁地对原始数组的某…

国产单通道直流有刷马达驱动芯片型号推荐

直流有刷马达驱动芯片是一款适应消费类、工业类的单通道直流有刷驱动IC&#xff0c;适用于各类玩具&#xff0c;智能家居&#xff0c;智能三表。小封装&#xff0c;低功耗&#xff0c;内置完善的保护机制&#xff08;过温/过流/过压&#xff09;。具有一个PWM&#xff08;INA/I…

写了2年文章的我,昨天第一次露脸直播。

作为一名不知名的技术博主&#xff0c;上周六晚上在视频号第一次做露脸直播。 勇敢的迈出视频号开播的第一步&#xff0c;并且数据不错&#xff0c;这个感觉很爽&#xff0c;和写作输出完全是两回事。 写这篇文章的目的是鼓励技术博主们&#xff0c;也尝试一下直播和做视频。 …

ClickHouse 挺快,esProc SPL 更快

开源分析数据库ClickHouse以快著称&#xff0c;真的如此吗&#xff1f;我们通过对比测试来验证一下。 ClickHouse vs Oracle 先用ClickHouse&#xff08;简称CH&#xff09;、Oracle数据库&#xff08;简称ORA&#xff09;一起在相同的软硬件环境下做对比测试。测试基准使用国…

基于ESP8266和SU-03T的离线语音红外遥控器设计

一. 系统设计及框图 之前设计了基于ESP32模块的智能红外遥控器&#xff0c;具体功能见以下CSDN链接&#xff1a; 智能红外遥控器&#xff08;一&#xff09;&#xff1a;功能简介_远望创客学堂的博客-CSDN博客 上面这款智能红外遥控器可以实现红外的远程控制&#xff0c;也可…

【从零开始学习深度学习】44. 图像增广的几种常用方式并使用图像增广训练模型【Pytorch】

大规模数据集是成功应用深度神经网络的前提&#xff0c;图像增广&#xff08;image augmentation&#xff09;技术通过对训练图像做一系列随机改变&#xff0c;来产生相似但又不同的训练样本&#xff0c;从而扩大训练数据集的规模。图像增广的另一种解释是&#xff0c;随机改变…

PCB入门学习—PCB封装的创建2

3.2 IC类PCB封装的创建注&#xff1a;PCB封装的名字一定要和原理图上填写的封装名字一样&#xff0c;不然对不上。规格书里有最大值最小值&#xff0c;就按最大值来做。快捷键EA是特殊粘贴。SOP-8:焊盘比较多时(BGA)可以利用向导去创建。做封装从规格书需要读取的数据&#xff…