11 | “万金油”的String,为什么不好用了?
该思维导图由 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-3111181 - super BB💨🐷老师,我之前看到《redis设计与实现》中提出SDS 的结构体的中没有alloc字段,书中的提到的是free,用来表示buf数组未使用的字节长度
作者回复: 学习的很仔细! 《Redis设计与实现》这本书分析的代码是Redis3.0的源码,在Redis3.0.4源码中,SDS结构体里还是用的free表示未使用空间。 但是应该差不多是Redis3.2.0开始,SDS结构体开始使用alloc字段了。
2020-09-11467 - 吕我有一个疑惑,老师,文中的案例,这么大的数据量,为什么采用redis这种内存数据库来存储数据么呢,感觉它的业务场景还是不很清楚?直接采用mysql存储会有什么问题么?
作者回复: 这是个好问题。 其实这个图片ID和存储对象ID对应关系的存储,就是用在分布式存储系统中的一个小的元数据服务,访问模式也比较简单,key-value的PUT、GET就行,但是要求请求响应快。Redis很轻量级,而且速度也快,所以用的Redis。 MySQL用在这个场景中显得有些太重了,这个场景里面没有关系模型,也没有事务需求和复杂查询,上MySQL不太需要。图片数量再增加时,MySQL的表就太大了,插入效率会降低。
2020-09-01551 - 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-14512 - 小喵喵老师,请教下,这样拆分的话,如何重复了咋办呢? 以图片 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-011112 - yyl“在节省内存空间方面,哈希表就没有压缩列表那么高效了” 在内存空间的开销上,也许哈希表没有压缩列表高效 但是哈希表的查询效率,要比压缩列表高。 在对查询效率高的场景中,可以考虑空间换时间
作者回复: 其实,在Redis的设计和使用上,是一个典型的“系统”思维,也就是权衡(trade-off),根据自己的业务场景、数据量、访问特征,来进行选择。 我们自己做系统研发,这是个核心思想 :)
2020-09-03311 - Front如果你刚好读过Database System Implemenation, 这篇正解释了NoSQL Database越来越像RDBMS
作者回复: 一个数据库可以粗略分成访问接口和底层存储,从访问接口来看,NoSQL和RDBMS还是有区别的,RDBMS是SQL接口,NoSQL是PUT/GET/DELETE/SCAN。但是从底层存储来看,一些NoSQL的存储机制开始被RDBMS采用,例如MySQL就使用RocksDB作为底层的存储引擎,叫做MyRocks。
2020-12-283 - 蜗牛有大佬能解释下 “prev_len 有两种取值情况:1 字节或 5 字节” 这一句吗?取值的话不应该是具体的某一个值吗?这里取值为1字节或5字节 是啥意思呢?小菜鸟想不太明白。
作者回复: 这里的prev_len取值情况是指用几个字节来表示prev_len。 prev_len是表示前一个entry的长度,如果前一个entry的长度小于254字节,那么prev_len就用1个字节来表示。否则的话,prev_len就使用5个字节表示。
2020-11-2732 - 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-3150541
- 注定非凡一,作者讲了什么? 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-094103