1.举个栗子
-
需求
- 记录图片 ID(10 位数) 与 图片在存储系统中保存时的 ID(10 位数)
-
分析
-
String 类型提供的“一个键对应一个值的数据”的保存形式刚好契合\
-
String 类型可以保存二进制字节流,就像“万金油”一样,只要把数据转成二进制字节数组,就可以保存了\
-
-
使用string
- 保存了 1 亿张图片,大约用了 6.4GB 的内存
-
出现问题
- 大内存 Redis 实例因为生成 RDB 而响应变慢的问题\
-
分析原因
- string保存数据时所消耗的内存空间较多
-
解决办法
- 不使用string存储
- 改用存储效率更高的list,集合类型有非常节省内存空间的底层实现结构,
-
新的问题
- 集合类型保存的数据模式,是一个键对应一系列值,并不适合直接保存单值的键值对
2.为什么 String 类型内存开销大?
-
分析 为什么1 亿张图片,大约用了 6.4GB 的内存 6.4GB/1亿约等于64字节
-
一组图片 ID 及其存储对象 ID 的记录,实际只需要 16 字节(2个long类型 8+8=16)就可以了
-
String 类型还需要额外的内存空间记录数据长度、空间使用等信息,这些信息也叫作元数据\
- 当数据较小时,元数据显得占用空间大
-
string的存储方式
-
String 类型会把它保存为一个 8 字节的 Long 类型整数,这种保存方式通常也叫作 int 编码方式
-
数据中包含字符时,String 类型就会用简单动态字符串 SDS\
-
buf 字节数组,保存实际数据\
-
len 占 4 个字节,表示 buf 的已用长度(额外开销)\
-
alloc 也占个 4 字节,表示 buf 的实际分配长度,一般大于 len(额外开销)\
-
-
还有一个来自于 RedisObject 结构体的开销\
-
8 字节的元数据\
-
8 字节指针 指向实际数据\
-
-
long\
- 如果是Long 类型整数时,RedisObject 中的指针就直接赋值为整数数据了,这样就不用额外的指针再指向整数了,节省了指针的空间开销
-
embstr\
- 另一方面,当保存的是字符串数据,并且字符串小于等于 44 字节时,RedisObject 中的元数据、指针和 SDS 是一块连续的内存区域,这样就可以避免内存碎片\
-
raw
- 当字符串大于 44 字节时,SDS 的数据量就开始变多了,Redis 就不再把 SDS 和 RedisObject 布局在一起了,而是会给 SDS 分配独立的空间,并用指针指向 SDS 结构。这种布局方式被称为 raw 编码模式
-
-
因为 10 位数的图片 ID 和图片存储对象 ID 是 Long 类型整数,所以可以直接用 int 编码的 RedisObject 保存
-
每个 int 编码的 RedisObject 元数据部分占 8 字节,指针部分被直接赋值为 8 字节的整数了(8+8)*2=32字节\
-
哈希表的每一项是一个 dictEntry 的结构体,用来指向一个键值对\
-
dictEntry 结构中有三个 8 字节的指针,分别指向 key、value 以及下一个 dictEntry,三个指针共 24 字节\
-
最后的8字节是因为jemalloc分配会分配2的整数倍
-
-
有效的信息只有16字节,最后却存储了64字节
\
3.用什么数据结构可以节省内存?
-
Redis 有一种底层数据结构,叫压缩列表(ziplist),这是一种非常节省内存的结构\
-
压缩列表\
-
zlbytes 列表长度\
-
zltail 列表尾的偏移量\
-
zllen 列表中的 entry 个数\
-
-
每个 entry 的元数据\
-
prev_len 表示前一个 entry 的长度(1 字节或 5 字节)\
-
len:表示自身长度,4 字节;\
-
encoding:表示编码方式,1 字节;\
-
content:保存实际数据\
-
entry 会挨个儿放置在内存中,不需要再用额外的指针进行连接,这样就可以节省指针所占用的空间(指针也会占据空间)\
-
-
分析
-
每个 entry 保存一个图片存储对象 ID(8 字节),此时,每个 entry 的 prev_len 只需要 1 个字节就行\
-
Redis 基于压缩列表实现了 List、Hash 和 Sorted Set 这样的集合类型,这样做的最大好处就是节省了 dictEntry 的开销\
-
但采用集合类型时,一个 key 就对应一个集合的数据,能保存的数据多了很多,但也只用了一个 dictEntry,这样就节省了内存\
-
-
存在的问题
- \
\
4.如何用集合类型保存单值的键值对?
-
在保存单值的键值对时,可以采用基于 Hash 类型的二级编码方法
-
把一个单值的数据拆分成两部分,前一部分作为 Hash 集合的 key,后一部分作为 Hash 集合的 value,这样一来,我们就可以把单值数据保存到 Hash 集合中了\
-
以图片 ID 1101000060 和图片存储对象 ID 3302000080 为例,我们可以把图片 ID 的前 7 位(1101000)作为 Hash 类型的键,把图片 ID 的最后 3 位(060)和图片存储对象 ID 分别作为 Hash 类型值中的 key 和 value\
-
相同前7位的图片就保存在一起了,并且通过hash key有效区分了别的数据(最后还控制在1000个,压缩了存储,不过查找效率就很低了)
-
-
增加一条记录后,内存占用只增加了 16 字节\
-
Redis Hash 类型的两种底层实现结构,分别是压缩列表和哈希表\
-
Hash 类型设置了用压缩列表保存数据时的两个阈值,一旦超过了阈值,Hash 类型就会用哈希表来保存数据了\
-
hash-max-ziplist-entries:表示用压缩列表保存时哈希集合中的最大元素个数**
**- 写入的元素个数超过了 hash-max-ziplist-entries\
-
hash-max-ziplist-value:表示用压缩列表保存时哈希集合中单个元素的最大长度**
**- 写入的单个元素大小超过了 hash-max-ziplist-value\
-
-
我们只用图片 ID 最后 3 位作为 Hash 集合的 key,也就保证了 Hash 集合的元素个数不超过 1000\
5.总结
-
但是,在保存的键值对本身占用的内存空间不大时(例如这节课里提到的的图片 ID 和图片存储对象 ID),String 类型的元数据开销就占据主导了
-
RedisObject 结构\
-
SDS 结构\
-
dictEntry 结构
-