Redis核心技术与实战
蒋德钧
中科院计算所副研究员
新⼈⾸单¥29.9
6731 人已学习
课程目录
已更新 14 讲 / 共 50 讲
0/4登录后,你可以任选4讲全文学习。
开篇词 (1讲)
开篇词 | 这样学Redis,才能技高一筹
免费
基础篇 (10讲)
01 | 基本架构:一个键值数据库包含什么?
02 | 数据结构:快速的Redis有哪些慢操作?
03 | 高性能IO模型:为什么单线程Redis能那么快?
04 | AOF日志:宕机了,Redis如何避免数据丢失?
05 | 内存快照:宕机后,Redis如何实现快速恢复?
06 | 数据同步:主从库如何实现数据一致?
07 | 哨兵机制:主库挂了,如何不间断服务?
08 | 哨兵集群:哨兵挂了,主从库还能切换吗?
09 | 切片集群:数据增多了,是该加内存还是加实例?
10 | 第1~9讲课后思考题答案及常见问题答疑
实践篇 (2讲)
11 | “万金油”的String,为什么不好用了?
12 | 有一亿个keys要统计,应该用哪种集合?
加餐篇 (1讲)
加餐(一)| 经典的Redis学习资料有哪些?
Redis核心技术与实战
15
15
1.0x
00:00/00:00
登录|注册

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

蒋德钧 2020-08-31
你好,我是蒋德钧。
从今天开始,我们就要进入“实践篇”了。接下来,我们会用 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/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《Redis核心技术与实战》,如需阅读全部文章,
请订阅文章所属专栏新⼈⾸单¥29.9
立即订阅
登录 后留言

精选留言(26)

  • 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
    1
    26
  • Geek1185
    老师能否讲解一下hash表这种redis数据结构,底层在用压缩列表的时候是如何根据二级的键找到对应的值的呢。是一个entry里会同时保存键和值吗
    2020-08-31
    5
  • Wangxi
    实测老师的例子,长度7位数,共100万条数据。使用string占用70mb,使用hash ziplist只占用9mb。效果非常明显。redis版本6.0.6

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

    2020-08-31
    3
  • 慎独明强
    看了Redis设计与实现,有讲SDS这一块,对于老师分析的内容,自己心里有印象,再结合老师今天的实践案例,前面的知识还没有吃透
    😂😂
    2020-08-31
    2
  • 伟伟哦
    老师今天讲的可以给个代码,配置了选项 如何实现把图片 ID 的最后 3 位(060)和图片存储对象 ID 分别作为 Hash 类型值中的 key 和 value。 代码操作下
    2020-08-31
    1
  • MClink
    老师,底层数据结构的转换是怎么实现的呢?是单纯的开一个新的数据结构再把数据复制过去吗?再释放之前的数据结构的内存,复制过程中有修改值的话要怎么处理,复制过程中不就两倍内存消耗了
    2020-08-31
    1
  • nickyi
    老师你好,后面会有涉及到 redis事件机制相关的讲解吗
    2020-09-01
  • 一步
    dictEntry 这个里面的 3个指针到底指向的是哪啊? key 的指针, value 指针 ,nextDictEntry 指针, 指向的是 SDS 或者 RedisObject 中的哪个位置
    2020-09-01
  • 一步
    使用 http://www.redis.cn/redis_memory/ 这个网站来计算 文章中 一亿张图片ID消耗的内存, 为什么得出来 9269.71M, 9点多个 G呢? 1亿个 string , string 的 key 和 value 分别是 8个 字节
    2020-09-01
  • zhou
    hset 1101000 060 3302000080
    这条记录只消耗 16 字节没明白,压缩列表保存一个对象需要 14 字节,060、3302000080 都需要保存,那应该至少大于 28 字节
    2020-09-01
    1
  • 小喵喵
    老师,请教下,这样拆分的话,如何重复了咋办呢?
    以图片 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
    2
  • yeek
    再一个问题:每个hash存储1000个数据,那么redis的主hash表中 dicEntry 是不是要有十万个,也会占据一部分内存空间吧?
    2020-09-01
  • 我有一个疑惑,老师,文中的案例,这么大的数据量,为什么采用redis这种内存数据库来存储数据么呢,感觉它的业务场景还是不很清楚?直接采用mysql存储会有什么问题么?

    作者回复: 这是个好问题。

    其实这个图片ID和存储对象ID对应关系的存储,就是用在分布式存储系统中的一个小的元数据服务,访问模式也比较简单,key-value的PUT、GET就行,但是要求请求响应快。Redis很轻量级,而且速度也快,所以用的Redis。

    MySQL用在这个场景中显得有些太重了,这个场景里面没有关系模型,也没有事务需求和复杂查询,上MySQL不太需要。图片数量再增加时,MySQL的表就太大了,插入效率会降低。

    2020-09-01
  • 张三
    老师最后给的网址链接打不开
    既然用压缩列表可以节省内存空间,那么另外两种底层由压缩列表实现的list和sortedset 应该都可以吧,同样应该也要用二级编码的方式来存储吧。使每一个list或sortedset的大小不要超过阈值(对应小于512个或128个),并且value的大小也不要超过阈值(64字节)。
    2020-08-31
  • 学友的情书
    第一篇基础能理解,数据结构有点顶不住了🤩
    2020-08-31
  • 老纪
    我想知道最后那个保证了 Hash 集合的元素个数不超过 1000,是怎么算出来的
    2020-08-31
    1
  • 其实我认为这就是解决实际问题中的实现方法的选择,怎么能够根据实际的业务场景,选择一个合适或者说各方面都比较均衡的方式,不能说是为了省内存,就直接抛弃字符串了
    2020-08-31
  • 一大只😴
    不懂就问,老师,以photo_id:1101000051为例,key(photo_id)是字符串,那应该按SDS算,那元数据(8)+ptr(8)+SDS(4+4+8+1)应该是33,key+value+hash表 应该是49+32=81
    2020-08-31
    2
  • 拥有两个端点是线段
    老师,在字符串长度不超过44字节时是使用embstr编码,那为何规定是44字节呢?
    2020-08-31
  • 叶子。
    我记得之前讲的是
    1)字典中保存的键和值的大小都小于64字节
    2)字典中键值对的个数小于512个
    这两个是配置的默认值吗,为什么这里又可以设置为1000呢?
    2020-08-31
收起评论
26
返回
顶部