Redis 核心技术与实战
蒋德钧
中科院计算所副研究员
81696 人已学习
新⼈⾸单¥68
登录后,你可以任选4讲全文学习
课程目录
已完结/共 53 讲
开篇词 (1讲)
实践篇 (28讲)
Redis 核心技术与实战
15
15
1.0x
00:00/00:00
登录|注册

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

压缩列表和哈希表
构成和节省内存的原因
RedisObject结构体
Simple Dynamic String(SDS)结构体
int编码方式
Hash类型底层结构的阈值配置
基于Hash类型的二级编码方法
控制保存在Hash集合中的元素个数
集合类型的底层实现结构
压缩列表(ziplist)
jemalloc的内存分配
String类型的底层结构
内存使用量增加的问题
使用String类型保存数据
保存图片ID和图片存储对象ID的需求
其他合适的类型可以应用在保存图片的例子中
如何用集合类型保存单值的键值对?
用什么数据结构可以节省内存?
String类型内存开销的原因
String类型的内存消耗问题
每课一问
为什么String类型内存开销大?
参考文章

该思维导图由 AI 生成,仅供参考

你好,我是蒋德钧。
从今天开始,我们就要进入“实践篇”了。接下来,我们会用 5 节课的时间学习“数据结构”。我会介绍节省内存开销以及保存和统计海量数据的数据类型及其底层数据结构,还会围绕典型的应用场景(例如地址位置查询、时间序列数据库读写和消息队列存取),跟你分享使用 Redis 的数据类型和 module 扩展功能来满足需求的具体方案。
今天,我们先了解下 String 类型的内存空间消耗问题,以及选择节省内存开销的数据类型的解决方案。
先跟你分享一个我曾经遇到的需求。
当时,我们要开发一个图片存储系统,要求这个系统能快速地记录图片 ID 和图片在存储系统中保存时的 ID(可以直接叫作图片存储对象 ID)。同时,还要能够根据图片 ID 快速查找到图片存储对象 ID。
因为图片数量巨大,所以我们就用 10 位数来表示图片 ID 和图片存储对象 ID,例如,图片 ID 为 1101000051,它在存储系统中对应的 ID 号是 3301000051。
photo_id: 1101000051
photo_obj_id: 3301000051
可以看到,图片 ID 和图片存储对象 ID 正好一一对应,是典型的“键 - 单值”模式。所谓的“单值”,就是指键值对中的值就是一个值,而不是一个集合,这和 String 类型提供的“一个键对应一个值的数据”的保存形式刚好契合。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

Redis中String类型数据保存存在较大内存开销,作者通过图片存储系统案例阐述了这一问题,并提出了使用集合类型来节省内存空间的解决方案。文章深入分析了String类型的内存布局和元数据开销,以及RedisObject结构和内存分配库jemalloc的影响。作者发现了String类型内存开销大的原因,并提出了使用集合类型来保存单值键值对以节省内存空间的方法。具体介绍了压缩列表的构成和如何用集合类型保存单值的键值对。作者还提到了Hash类型底层结构的两种实现方式,以及使用压缩列表和哈希表的条件。总结指出,String类型在保存键值对占用内存空间不大时,元数据开销占主导地位,建议使用压缩列表保存数据。同时,使用Hash类型保存单值键值对时,建议将单值数据拆分成两部分,分别作为Hash集合的键和值。文章还提供了一个小工具,可以帮助读者了解不同类型保存键值对时的内存开销。整体而言,本文对于理解Redis中数据类型的内存消耗问题以及解决方案具有一定的参考价值。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Redis 核心技术与实战》
新⼈⾸单¥68
立即购买
登录 后留言

全部留言(149)

  • 最新
  • 精选
  • link
    实测老师的例子,长度7位数,共100万条数据。使用string占用70mb,使用hash ziplist只占用9mb。效果非常明显。redis版本6.0.6

    作者回复: 赞 实践的态度!

    2020-08-31
    11
    181
  • super BB💨🐷
    老师,我之前看到《redis设计与实现》中提出SDS 的结构体的中没有alloc字段,书中的提到的是free,用来表示buf数组未使用的字节长度

    作者回复: 学习的很仔细! 《Redis设计与实现》这本书分析的代码是Redis3.0的源码,在Redis3.0.4源码中,SDS结构体里还是用的free表示未使用空间。 但是应该差不多是Redis3.2.0开始,SDS结构体开始使用alloc字段了。

    2020-09-11
    4
    67
  • 我有一个疑惑,老师,文中的案例,这么大的数据量,为什么采用redis这种内存数据库来存储数据么呢,感觉它的业务场景还是不很清楚?直接采用mysql存储会有什么问题么?

    作者回复: 这是个好问题。 其实这个图片ID和存储对象ID对应关系的存储,就是用在分布式存储系统中的一个小的元数据服务,访问模式也比较简单,key-value的PUT、GET就行,但是要求请求响应快。Redis很轻量级,而且速度也快,所以用的Redis。 MySQL用在这个场景中显得有些太重了,这个场景里面没有关系模型,也没有事务需求和复杂查询,上MySQL不太需要。图片数量再增加时,MySQL的表就太大了,插入效率会降低。

    2020-09-01
    5
    51
  • Hm_
    老师有个地方不是很理解,文中讲到String需要dictEntry来保存键值关系,那么hash结构最外层的那个key没有dictEntry来维护吗?因为我记得之前讲得Redis是用一个大的hash来维护所有的键值对的,所以感觉这和dictEntry所占用的内存是一样的吧

    作者回复: dictEntry是Redis全局哈希表中的表项,包含了key和value的指针,这里的value可以是String,List,Hash等数据类型。 你说的hash结构最外层的key,如果是指全局哈希表中的key的话,指向key的指针是已经包含在dictEntry这个结构中了,同时key本身的数据结构是RedisObject。

    2020-12-14
    5
    12
  • 小喵喵
    老师,请教下,这样拆分的话,如何重复了咋办呢? 以图片 ID 1101000060 和图片存储对象 ID 3302000080 为例,我们可以把图片 ID 的前 7 位(1101000)作为 Hash 类型的键,把图片 ID 的最后 3 位(060)和图片存储对象 ID 分别作为 Hash 类型值中的 key 和 value。 比如:两张图片分别为:图片 ID 1101000060 图片存储对象 ID 3302000080; 图片 ID 1101001060 图片存储对象 ID 3302000081 这个时候最后 3 位(060)的key是冲突的的,但是它的图片存储对象 ID不同。

    作者回复: 我们是会把图片 ID 的前7位作为键值对的key,Hash集合是键值对的value,在你举的例子中,图片ID 1101000060和1101001060。它们的前7位分别是1101000和1101001,对应了两个键值对。所以,它们图片ID的后3位虽然相同,都是060,也是在两个Hash集合中的,不会冲突的。 你看看是不是呢?

    2020-09-01
    11
    12
  • yyl
    “在节省内存空间方面,哈希表就没有压缩列表那么高效了” 在内存空间的开销上,也许哈希表没有压缩列表高效 但是哈希表的查询效率,要比压缩列表高。 在对查询效率高的场景中,可以考虑空间换时间

    作者回复: 其实,在Redis的设计和使用上,是一个典型的“系统”思维,也就是权衡(trade-off),根据自己的业务场景、数据量、访问特征,来进行选择。 我们自己做系统研发,这是个核心思想 :)

    2020-09-03
    3
    11
  • Front
    如果你刚好读过Database System Implemenation, 这篇正解释了NoSQL Database越来越像RDBMS

    作者回复: 一个数据库可以粗略分成访问接口和底层存储,从访问接口来看,NoSQL和RDBMS还是有区别的,RDBMS是SQL接口,NoSQL是PUT/GET/DELETE/SCAN。但是从底层存储来看,一些NoSQL的存储机制开始被RDBMS采用,例如MySQL就使用RocksDB作为底层的存储引擎,叫做MyRocks。

    2020-12-28
    3
  • 蜗牛
    有大佬能解释下 “prev_len 有两种取值情况:1 字节或 5 字节” 这一句吗?取值的话不应该是具体的某一个值吗?这里取值为1字节或5字节 是啥意思呢?小菜鸟想不太明白。

    作者回复: 这里的prev_len取值情况是指用几个字节来表示prev_len。 prev_len是表示前一个entry的长度,如果前一个entry的长度小于254字节,那么prev_len就用1个字节来表示。否则的话,prev_len就使用5个字节表示。

    2020-11-27
    3
    2
  • Kaito
    保存图片的例子,除了用String和Hash存储之外,还可以用Sorted Set存储(勉强)。 Sorted Set与Hash类似,当元素数量少于zset-max-ziplist-entries,并且每个元素内存占用小于zset-max-ziplist-value时,默认也采用ziplist结构存储。我们可以把zset-max-ziplist-entries参数设置为1000,这样Sorted Set默认就会使用ziplist存储了,member和score也会紧凑排列存储,可以节省内存空间。 使用zadd 1101000 3302000080 060命令存储图片ID和对象ID的映射关系,查询时使用zscore 1101000 060获取结果。 但是Sorted Set使用ziplist存储时的缺点是,这个ziplist是需要按照score排序的(为了方便zrange和zrevrange命令的使用),所以在插入一个元素时,需要先根据score找到对应的位置,然后把member和score插入进去,这也意味着Sorted Set插入元素的性能没有Hash高(这也是前面说勉强能用Sorte Set存储的原因)。而Hash在插入元素时,只需要将新的元素插入到ziplist的尾部即可,不需要定位到指定位置。 不管是使用Hash还是Sorted Set,当采用ziplist方式存储时,虽然可以节省内存空间,但是在查询指定元素时,都要遍历整个ziplist,找到指定的元素。所以使用ziplist方式存储时,虽然可以利用CPU高速缓存,但也不适合存储过多的数据(hash-max-ziplist-entries和zset-max-ziplist-entries不宜设置过大),否则查询性能就会下降比较厉害。整体来说,这样的方案就是时间换空间,我们需要权衡使用。 当使用ziplist存储时,我们尽量存储int数据,ziplist在设计时每个entry都进行了优化,针对要存储的数据,会尽量选择占用内存小的方式存储(整数比字符串在存储时占用内存更小),这也有利于我们节省Redis的内存。还有,因为ziplist是每个元素紧凑排列,而且每个元素存储了上一个元素的长度,所以当修改其中一个元素超过一定大小时,会引发多个元素的级联调整(前面一个元素发生大的变动,后面的元素都要重新排列位置,重新分配内存),这也会引发性能问题,需要注意。 另外,使用Hash和Sorted Set存储时,虽然节省了内存空间,但是设置过期变得困难(无法控制每个元素的过期,只能整个key设置过期,或者业务层单独维护每个元素过期删除的逻辑,但比较复杂)。而使用String虽然占用内存多,但是每个key都可以单独设置过期时间,还可以设置maxmemory和淘汰策略,以这种方式控制整个实例的内存上限。 所以在选用Hash和Sorted Set存储时,意味着把Redis当做数据库使用,这样就需要务必保证Redis的可靠性(做好备份、主从副本),防止实例宕机引发数据丢失的风险。而采用String存储时,可以把Redis当做缓存使用,每个key设置过期时间,同时设置maxmemory和淘汰策略,控制整个实例的内存上限,这种方案需要在数据库层(例如MySQL)也存储一份映射关系,当Redis中的缓存过期或被淘汰时,需要从数据库中重新查询重建缓存,同时需要保证数据库和缓存的一致性,这些逻辑也需要编写业务代码实现。 总之,各有利弊,我们需要根据实际场景进行选择。
    2020-08-31
    50
    541
  • 注定非凡
    一,作者讲了什么? Redis的String类型数据结构,及其底层实现 二,作者是怎么把这事给说明白的? 1,通过一个图片存储的案例,讲通过合理利用Redis的数据结构,降低资源消耗 三,为了讲明白,作者讲了哪些要点?有哪些亮点? 1,亮点1:String类型的数据占用内存,分别是被谁占用了 2,亮点2:可以巧妙的利用Redis的底层数据结构特性,降低资源消耗 3,要点1: Simple Dynamic String结构体( buf:字节数组,为了表示字节结束,会在结尾增加“\0” len: 占4个字节,表示buf的已用长度 alloc:占4个字节,表示buf实际分配的长度,一般大于len) 4,要点2: RedisObject 结构体( 元数据:8字节(用于记录最后一次访问时间,被引用次数。。。) 指针:8字节,指向具体数据类型的实际数据所在 ) 5,要点3:dicEntry 结构体( key:8个字节指针,指向key value:8个字节指针,指向value next:指向下一个dicEntry) 6,要点4:ziplist(压缩列表)( zlbytes:在表头,表示列表长度 zltail:在表头,表示列尾偏移量 zllen:在表头,表示列表中 entry:保存数据对象模型 zlend:在表尾,表示列表结束) entry:( prev_len:表示一个entry的长度,有两种取值方式:1字节或5字节。 1字节表示一个entry小于254字节,255是zlend的默认值,所以不使用。 len:表示自身长度,4字节 encodeing:表示编码方式,1字节 content:保存实际数据) 5,要点4:String类型的内存空间消耗 ①,保存Long类型时,指针直接保存整数数据值,可以节省空间开销(被称为:int编码) ②,保存字符串,且不大于44字节时,RedisObject的元数据,指针和SDS是连续的,可以避免内存碎片(被称为:embstr编码) ③,当保存的字符串大于44字节时,SDS的数据量变多,Redis会给SDS分配独立的空间,并用指针指向SDS结构(被称为:raw编码) ④,Redis使用一个全局哈希表保存所以键值对,哈希表的每一项都是一个dicEntry,每个dicEntry占用32字节空间 ⑤,dicEntry自身24字节,但会占用32字节空间,是因为Redis使用了内存分配库jemalloc。 jemalloc在分配内存时,会根据申请的字节数N,找一个比N大,但最接近N的2的幂次数作为分配空间,这样可以减少频繁分配内存的次数 4,要点5:使用什么数据结构可以节省内存? ①, 压缩列表,是一种非常节省内存的数据结构,因为他使用连续的内存空间保存数据,不需要额外的指针进行连接 ②,Redis基于压缩列表实现List,Hash,Sorted Set集合类型,最大的好处是节省了dicEntry开销 5,要点6:如何使用集合类型保存键值对? ①,Hash类型设置了用压缩列表保存数据时的两个阀值,一旦超过就会将压缩列表转为哈希表,且不可回退 ②,hash-max-ziplist-entries:表示用压缩列表保存哈希集合中的最大元素个数 ③,hash-max-ziplist-value:表示用压缩列表保存时,哈希集合中单个元素的最大长度 四,对于作者所讲,我有哪些发散性思考? 看了老师讲解,做了笔记,又看了黄建宏写的《Redis 设计与实现》 有这样的讲解: 当哈希对象可以同时满足以下两个条件时, 哈希对象使用 ziplist 编码: 1. 哈希对象保存的所有键值对的键和值的字符串长度都小于 64 字节; 2. 哈希对象保存的键值对数量小于 512 个; 五,在将来的哪些场景中,我能够使用它? 这次学习Redis数据结构特性有了更多了解,在以后可以更加有信心根据业务需要,选取特定的数据结构
    2020-09-09
    4
    103
收起评论
显示
设置
留言
99+
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部