从今天开始,我们就要进入“实践篇”了。接下来,我们会用 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 类型提供的“一个键对应一个值的数据”的保存形式刚好契合。
而且,String 类型可以保存二进制字节流,就像“万金油”一样,只要把数据转成二进制字节数组,就可以保存了。
所以,我们的第一个方案就是用 String 保存数据。我们把图片 ID 和图片存储对象 ID 分别作为键值对的 key 和 value 来保存,其中,图片存储对象 ID 用了 String 类型。
刚开始,我们保存了 1 亿张图片,大约用了 6.4GB 的内存。但是,随着图片数据量的不断增加,我们的 Redis 内存使用量也在增加,结果就遇到了大内存 Redis 实例因为生成 RDB 而响应变慢的问题。很显然,String 类型并不是一种好的选择,我们还需要进一步寻找能节省内存开销的数据类型方案。
在这个过程中,我深入地研究了 String 类型的底层结构,找到了它内存开销大的原因,对“万金油”的 String 类型有了全新的认知:String 类型并不是适用于所有场合的,它有一个明显的短板,就是它保存数据时所消耗的内存空间较多。
同时,我还仔细研究了集合类型的数据结构。我发现,集合类型有非常节省内存空间的底层实现结构,但是,集合类型保存的数据模式,是一个键对应一系列值,并不适合直接保存单值的键值对。所以,我们就使用二级编码的方法,实现了用集合类型保存单值键值对,Redis 实例的内存空间消耗明显下降了。
这节课,我就把在解决这个问题时学到的经验和方法分享给你,包括 String 类型的内存空间消耗在哪儿了、用什么数据结构可以节省内存,以及如何用集合类型保存单值键值对。如果你在使用 String 类型时也遇到了内存空间消耗较多的问题,就可以尝试下今天的解决方案了。
接下来,我们先来看看 String 类型的内存都消耗在哪里了。
为什么 String 类型内存开销大?
在刚才的案例中,我们保存了 1 亿张图片的信息,用了约 6.4GB 的内存,一个图片 ID 和图片存储对象 ID 的记录平均用了 64 字节。
但问题是,一组图片 ID 及其存储对象 ID 的记录,实际只需要 16 字节就可以了。
我们来分析一下。图片 ID 和图片存储对象 ID 都是 10 位数,我们可以用两个 8 字节的 Long 类型表示这两个 ID。因为 8 字节的 Long 类型最大可以表示 2 的 64 次方的数值,所以肯定可以表示 10 位数。但是,为什么 String 类型却用了 64 字节呢?
其实,除了记录实际数据,String 类型还需要额外的内存空间记录数据长度、空间使用等信息,这些信息也叫作元数据。当实际保存的数据较小时,元数据的空间开销就显得比较大了,有点“喧宾夺主”的意思。
那么,String 类型具体是怎么保存数据的呢?我来解释一下。
当你保存 64 位有符号整数时,Str