11 | “万金油”的String,为什么不好用了?

2024-04-20 21:32
文章标签 string 不好 万金油

本文主要是介绍11 | “万金油”的String,为什么不好用了?,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • Redis核心技术与实战
    • 实践篇
      • 11 | “万金油”的String,为什么不好用了?
        • 为什么 String 类型内存开销大?
        • 用什么数据结构可以节省内存?
        • 如何用集合类型保存单值的键值对?


Redis核心技术与实战

实践篇

11 | “万金油”的String,为什么不好用了?

示例

开发一个图片存储系统,要求这个系统能快速地记录图片 ID 和图片在存储系统中保存时的 ID(图片存储对象 ID)。同时,还要能够根据图片 ID 快速查找到图片存储对象 ID。
图片数量巨大,所以用 10 位数来表示图片 ID 和图片存储对象 ID,例如,图片 ID 为 1101000051,它在存储系统中对应的 ID 号是 3301000051。

photo_id: 1101000051
photo_obj_id: 3301000051

图片 ID 和图片存储对象 ID 正好一一对应,是典型的“键 - 单值”模式。和 String 类型提供的“一个键对应一个值的数据”的保存形式刚好契合。
String 类型可以保存二进制字节流,就像“万金油”一样,只要把数据转成二进制字节数组,就可以保存。
随着图片数据量的不断增加,Redis 内存使用量也在增加,大内存 Redis 实例因为生成 RDB 而响应变慢。

String 类型并不是适用于所有场合的,它保存数据时所消耗的内存空间较多。

为什么 String 类型内存开销大?

保存了 1 亿张图片的信息,用了约 6.4GB 的内存,一个图片 ID 和图片存储对象 ID 的记录平均用了 64 字节。而一组图片 ID 及其存储对象 ID 的记录,实际只需要 16 字节。

除了记录实际数据,String 类型还需要额外的内存空间记录数据长度、空间使用等信息,这些信息也叫作元数据。

String 类型具体是怎么保存数据?

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

在这里插入图片描述

  • buf:字节数组,保存实际数据。为了表示字节数组的结束,Redis 会自动在数组最后加一个“\0”,这就会额外占用 1 个字节的开销。
  • len:占 4 个字节,表示 buf 的已用长度。
  • alloc:也占个 4 字节,表示 buf 的实际分配长度,一般大于 len。

1. 在 SDS 中,buf 保存实际数据,而 len 和 alloc 本身其实是 SDS 结构体的额外开销。

2. 对于 String 类型来说,除了 SDS 的额外开销,还有一个来自于 RedisObject 结构体的开销。

不同数据类型都有些相同的元数据要记录(比如最后一次访问的时间、被引用的次数等),所以,Redis 会用一个 RedisObject 结构体来统一记录这些元数据,同时指向实际数据。

在这里插入图片描述

为了节省内存空间,Redis 还对 Long 类型整数和 SDS 的内存布局做了专门的设计。

  • 当保存的是 Long 类型整数时,RedisObject 中的指针就直接赋值为该整数数据,这样就不用额外的指针再指向整数了,节省了指针的空间开销。这种布局方式也被称为 int 编码模式
  • 当保存的是字符串数据,并且字符串小于等于 44 字节时,RedisObject 中的元数据、指针和 SDS 是一块连续的内存区域,这样就可以避免内存碎片。这种布局方式也被称为 embstr 编码模式
  • 字符串大于 44 字节时,SDS 的数据量开始变多,Redis 不再把 SDS 和 RedisObject 布局在一起,而是会给 SDS 分配独立的空间,并用指针指向 SDS 结构。这种布局方式被称为 raw 编码模式

在这里插入图片描述

因为 10 位数的图片 ID 和图片存储对象 ID 是 Long 类型整数,所以直接用 int 编码的 RedisObject 保存。每个 int 编码的 RedisObject 元数据部分占 8 字节,指针部分被直接赋值为 8 字节的整数。此时,每个 ID 会使用 16 字节,加起来一共是 32 字节。而一个图片 ID 和图片存储对象 ID 的记录平均用了 64 字节。

疑问:还有 32 字节在哪?

3. Redis 使用一个全局哈希表保存所有键值对,哈希表的每一项是一个 dictEntry 的结构体,用来指向一个键值对。 dictEntry 结构中有三个 8 字节的指针,分别指向 key、value 以及下一个 dictEntry,三个指针共 24 字节。

在这里插入图片描述

Redis 使用的内存分配库 jemalloc 在分配内存时,会根据申请的字节数 N,找一个比 N 大,但是最接近 N 的 2 的幂次数作为分配的空间,这样可以减少频繁分配的次数。

如果申请 6 字节空间,jemalloc 实际会分配 8 字节空间;如果申请 24 字节空间,jemalloc 则会分配 32 字节。

String 不是什么场合都适用。在保存的键值对本身占用的内存空间不大时(例如图片 ID 和图片存储对象 ID的示例),String 类型的元数据开销占据主导,包括了 RedisObject 结构、SDS 结构、dictEntry 结构的内存开销。

在这里插入图片描述

用什么数据结构可以节省内存?

Redis 有一种底层数据结构,叫压缩列表(ziplist),这是一种非常节省内存的结构。

表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量,以及列表中的 entry 个数。压缩列表尾还有一个 zlend,表示列表结束。

在这里插入图片描述

每个 entry 的元数据包括下面几部分:

  • prev_len: 表示前一个 entry 的长度。prev_len 有两种取值情况:1 字节或 5 字节。取值 1 字节时,表示上一个 entry 的长度小于 254 字节。虽然 1 字节的值能表示的数值范围是 0 到 255,但是压缩列表中 zlend 的取值默认是 255,因此,就默认用 255 表示整个压缩列表的结束,其他表示长度的地方就不能再用 255 这个值了。所以,当上一个 entry 长度小于 254 字节时,prev_len 取值为 1 字节,否则,就取值为 5 字节。
  • len: 表示自身长度,4 字节;
  • encoding: 表示编码方式,1 字节;
  • content: 保存实际数据。

这些 entry 会挨个儿放置在内存中,不需要再用额外的指针进行连接,这样就可以节省指针所占用的空间。

以保存图片存储对象 ID 为例,每个 entry 保存一个图片存储对象 ID(8 字节),此时,每个 entry 的 prev_len 只需要 1 个字节就行,因为每个 entry 的前一个 entry 长度都只有 8 字节,小于 254 字节。这样一来,一个图片的存储对象 ID 所占用的内存大小是 14 字节(1+4+1+8=14),实际分配 16 字节。

Redis 基于压缩列表实现了 List、Hash 和 Sorted Set,这样做的最大好处就是节省了 dictEntry 的开销。

当用 String 类型时,一个键值对就有一个 dictEntry,要用 32 字节空间。但采用集合类型时,一个 key 就对应一个集合的数据,能保存的数据多了很多,但也只用了一个 dictEntry,这样就节省了内存。

如何用集合类型保存单值的键值对?

在保存单值的键值对时,可以采用基于 Hash 类型的二级编码方法。这里说的二级编码,就是把一个单值的数据拆分成两部分,前一部分作为 Hash 集合的 key,后一部分作为 Hash 集合的 value (类似于 Map<key,Map<key,value> > 这种结构)

以图片 ID 1101000060 和图片存储对象 ID 3302000080 为例,把图片 ID 的前 7 位(1101000)作为 Hash 类型的键,把图片 ID 的最后 3 位(060)和图片存储对象 ID 分别作为 Hash 类型值中的 key 和 value。

Hash 类型的两种底层实现结构,分别是压缩列表和哈希表。Hash 类型设置了用压缩列表保存数据时的两个阈值,一旦超过了阈值,Hash 类型就会用哈希表来保存数据。这两个阈值分别对应以下两个配置项:

  • hash-max-ziplist-entries:表示用压缩列表保存时哈希集合中的最大元素个数。
  • hash-max-ziplist-value:表示用压缩列表保存时哈希集合中单个元素的最大长度。

如果往 Hash 集合中写入的元素个数超过了 hash-max-ziplist-entries,或者写入的单个元素大小超过了 hash-max-ziplist-value,Redis 就会自动把 Hash 类型的实现结构由压缩列表转为哈希表。

一旦从压缩列表转为哈希表,Hash 类型就会一直用哈希表进行保存,而不会再转回压缩列表了。

在节省内存空间方面,哈希表没有压缩列表那么高效。

为了能充分使用压缩列表的精简内存布局,一般要控制保存在 Hash 集合中的元素个数。 所以,在上述示例的二级编码中,只用图片 ID 最后 3 位作为 Hash 集合的 key(三位数最大为999,不会超过1000),也就保证了 Hash 集合的元素个数不超过 1000,同时,把 hash-max-ziplist-entries 设置为 1000,这样一来,Hash 集合就可以一直使用压缩列表来节省内存空间了。

这篇关于11 | “万金油”的String,为什么不好用了?的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

C++中处理文本数据char与string的终极对比指南

《C++中处理文本数据char与string的终极对比指南》在C++编程中char和string是两种用于处理字符数据的类型,但它们在使用方式和功能上有显著的不同,:本文主要介绍C++中处理文本数... 目录1. 基本定义与本质2. 内存管理3. 操作与功能4. 性能特点5. 使用场景6. 相互转换核心区别

C++ STL-string类底层实现过程

《C++STL-string类底层实现过程》本文实现了一个简易的string类,涵盖动态数组存储、深拷贝机制、迭代器支持、容量调整、字符串修改、运算符重载等功能,模拟标准string核心特性,重点强... 目录实现框架一、默认成员函数1.默认构造函数2.构造函数3.拷贝构造函数(重点)4.赋值运算符重载函数

redis数据结构之String详解

《redis数据结构之String详解》Redis以String为基础类型,因C字符串效率低、非二进制安全等问题,采用SDS动态字符串实现高效存储,通过RedisObject封装,支持多种编码方式(如... 目录一、为什么Redis选String作为基础类型?二、SDS底层数据结构三、RedisObject

Java获取当前时间String类型和Date类型方式

《Java获取当前时间String类型和Date类型方式》:本文主要介绍Java获取当前时间String类型和Date类型方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,... 目录Java获取当前时间String和Date类型String类型和Date类型输出结果总结Java获取

java String.join()方法实例详解

《javaString.join()方法实例详解》String.join()是Java提供的一个实用方法,用于将多个字符串按照指定的分隔符连接成一个字符串,这一方法是Java8中引入的,极大地简化了... 目录bVARxMJava String.join() 方法详解1. 方法定义2. 基本用法2.1 拼接

Java字符串处理全解析(String、StringBuilder与StringBuffer)

《Java字符串处理全解析(String、StringBuilder与StringBuffer)》:本文主要介绍Java字符串处理全解析(String、StringBuilder与StringBu... 目录Java字符串处理全解析:String、StringBuilder与StringBuffer一、St

Java String字符串的常用使用方法

《JavaString字符串的常用使用方法》String是JDK提供的一个类,是引用类型,并不是基本的数据类型,String用于字符串操作,在之前学习c语言的时候,对于一些字符串,会初始化字符数组表... 目录一、什么是String二、如何定义一个String1. 用双引号定义2. 通过构造函数定义三、St

Java中的String.valueOf()和toString()方法区别小结

《Java中的String.valueOf()和toString()方法区别小结》字符串操作是开发者日常编程任务中不可或缺的一部分,转换为字符串是一种常见需求,其中最常见的就是String.value... 目录String.valueOf()方法方法定义方法实现使用示例使用场景toString()方法方法

C#数据结构之字符串(string)详解

《C#数据结构之字符串(string)详解》:本文主要介绍C#数据结构之字符串(string),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录转义字符序列字符串的创建字符串的声明null字符串与空字符串重复单字符字符串的构造字符串的属性和常用方法属性常用方法总结摘

如何解决mysql出现Incorrect string value for column ‘表项‘ at row 1错误问题

《如何解决mysql出现Incorrectstringvalueforcolumn‘表项‘atrow1错误问题》:本文主要介绍如何解决mysql出现Incorrectstringv... 目录mysql出现Incorrect string value for column ‘表项‘ at row 1错误报错