把string作为储存的对象,,除了记录实际数据,String 类型还需要额外的内存空间记录数据长度、空间使用等信息,这些信息也叫作元数据。
当你保存 64 位有符号整数时,String 类型会把它保存为一个 8 字节的 Long 类型整数,这种保存方式通常也叫作 int 编码方式。但是当保存的数据中包含字符的时候,,String类型就会用简单动态字符串(Simple Dynamic String,SDS)结构来保存,包括:
- buf:字节数组,保存实际数据。为了表示字节数组的结束,Redis 会自动在数组最后加一个“\0”,这就会额外占用 1 个字节的开销。
- len:占 4 个字节,表示 buf 的已用长度。
- alloc:也占个 4 字节,表示 buf 的实际分配长度,一般大于 len。
除了SDS基本的额外字段外,因为 Redis 的数据类型有很多,而且,不同数据类型都有些相同的元数据要记录,所以,Redis 会用一个 RedisObject 结构体来统一记录这些元数据,同时指向实际数据。RedisObject包括:
- 元数据:比如最后一次访问的时间、被引用的次数等。
- 指针:指向具体的数据
其中对于数据来说
- 当保存的是 Long 类型整数时,RedisObject 中的指针就直接赋值为整数数据了,这样就不用额外的指针再指向整数了,节省了指针的空间开销。这种布局方式被称为 int 编码模式。
- 当保存的是字符串数据,并且字符串小于等于 44 字节时,RedisObject 中的元数据、指针和 SDS 是一块连续的内存区域,这样就可以避免内存碎片。
- 当字符串大于 44 字节时,SDS 的数据量就开始变多了,Redis 就不再把 SDS 和RedisObject 布局在一起了,而是会给 SDS 分配独立的空间,并用指针指向 SDS 结构。这种布局方式被称为 raw 编码模式。
现在有一个key值是10位数的整数,value是log类型,可以直接用int编码模式,那么RedisObject的指针就会直接改成8字节的整数,加上元数据的8字节,再加上long的16字节,加起来是32字节。
此外,还有一个指针指向全局哈希表中的key、value以及下一个键值对,各占用8字节,此时就是56字节。而实际上占用了64字节,因为jemalloc 在分配内存时,会根据我们申请的字节数 N,找一个比 N 大,但是最接近 N 的2 的幂次数作为分配的空间,这样可以减少频繁分配的次数,这样就分配的2的6次方字节。
压缩列表
压缩列表表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量,以及列表中的 entry 个数。压缩列表尾还有一个 zlend,表示列表结束。压缩列表之所以能节省内存,就在于它是用一系列连续的 entry 保存数据。每个 entry 的元数据包括下面几部分。
- prev_len,表示前一个 entry 的长度。prev_len 有两种取值情况:1 字节或 5 字节。取值 1 字节时,表示上一个 entry 的长度小于 254 字节。虽然 1 字节的值能表示的数值范围是 0 到 255,但是压缩列表中 zlend 的取值默认是 255,因此,就默认用 255表示整个压缩列表的结束,其他表示长度的地方就不能再用 255 这个值了。所以,当上一个 entry 长度小于 254 字节时,prev_len 取值为 1 字节,否则,就取值为 5 字节。
- len:表示自身长度,4 字节;
- encoding:表示编码方式,1 字节;
- content:保存实际数据。
这些 entry 会挨个儿放置在内存中,不需要再用额外的指针进行连接,这样就可以节省指针所占用的空间。
现在我们看看上面的例子, prev_len一个字节,len占4字节,编码方式1字节,数据占8字节,总共14字节。
Hash 类型的二级编码方法
以上是单个键对应一个集合,如果是单键值对,那么可以用hash类型的二级编码方式。就是把一个单值的数据拆分成两部分,前一部分作为 Hash 集合的 key,后一部分作为Hash 集合的 value,这样一来,我们就可以把单值数据保存到 Hash 集合中了。
以key 1101000060 和value 3302000080 为例,我们可以把key 的前7 位(1101000)作为 Hash 类型的键,把key 的最后 3 位(060)和value 分别作为 Hash 类型值中的 key 和 value。这样子储存的占用内存只有16个字节。
二级编码中截取的ID长度
Redis Hash 类型的两种底层实现结构,分别是压缩列表和哈希表。Hash 类型设置了用压缩列表保存数据时的两个阈值,一旦超过了阈值,Hash 类型就会用哈希表来保存数据了。
这两个阈值分别对应以下两个配置项:
- hash-max-ziplist-entries:表示用压缩列表保存时哈希集合中的最大元素个数。
- hash-max-ziplist-value:表示用压缩列表保存时哈希集合中单个元素的最大长度。
如果我们往 Hash 集合中写入的元素个数超过了 hash-max-ziplist-entries,或者写入的单个元素大小超过了 hash-max-ziplist-value,Redis 就会自动把 Hash 类型的实现结构由压缩列表转为哈希表。
一般压缩列表会比哈希更节省空间,所以按照刚刚的例子,最后 3 位作为 Hash 集合的 key,也就保证了 Hash 集合的元素个数不超过 1000。如果我们把hash-max-ziplist-entries设置为1000,那么就会用压缩列表来储存。
验证hash的两种底层数据结构
- hash-max-ziplist-entries = 3
- hash-max-ziplist-value = 10
表示:属性个数 <= 3 && 每个属性字符长度 <= 10 时使用 ziplist 编码格式存储,反之,使用 hashtable 编码格式进行存储。
两种配置修改的方式
方式一:在命令行使用 config set xxxx 的方式进行设置
修改后立即生效,不过修改的配置仅在当前实例进程中生效,重启失效,
1) "hash-max-ziplist-entries"
2) "3"
3) "hash-max-ziplist-value"
4) "64"
方式二:通过修改配置文件 redis.conf 中的配置参数
修改后,需要重启才会生效,不过配置持久化到配置文件中后,重启配置仍然生效
验证属性字符长度 > 10
插入一个用户,给他一个名称,字符长度不超过 10字节
127.0.0.1:6379> hset user:1 username hh
(integer) 1
127.0.0.1:6379> object encoding user:2
"ziplist
将 user:1 中的 username 变更为长度为10字节的字符串,如下
127.0.0.1:6379> hset user:1 username hhhhhhhhhh
(integer) 1
127.0.0.1:6379> HSTRLEN user:1 username
(integer) 10
127.0.0.1:6379> object encoding user:2
"ziplist"
将 user:1 中的 username 变更为长度为 11字节的字符串,如下
127.0.0.1:6379> hset user:1 username hhhhhhhhhhh
(integer) 1
127.0.0.1:6379> HSTRLEN user:1 username
(integer) 11
127.0.0.1:6379> object encoding user:2
"hashtable"
结论:当属性个数 <=3 但是存在一个属性字符长度大于 10 字节时,hash类型的编码格式为 hashtable
2.3、验证 ziplist -> hashtable 不可逆
在上面的试验中,得到的 user:1 只存在一个属性 username,且 username 是一个长度为 11 的字符串,所以此时 user:1 的编码格式为 hashtable。
此时,尝试将 username 改为长度小于等于 10 的字符串,来看看其编码格式:
127.0.0.1:6379> hset user:1 username hh
(integer) 0
127.0.0.1:6379> HSTRLEN user:1 username
(integer) 2
127.0.0.1:6379> OBJECT encoding user:1
"hashtable"
结论:ziplist->hashtable 的转化过程是不可逆的。
2.4、验证:属性值都是 <= 10 字节的字符串,属性个数 >3
插入用户 user:2 ,拥有 age,username,sex 且是哪个属性的字符长度 <= 10 ,如下
127.0.0.1:6379> hmset user:2 username yy age 18 sex unknown
OK
127.0.0.1:6379> OBJECT encoding user:2
"ziplist"
新增一个属性:weight=18
127.0.0.1:6379> hset user:2 weight 18
(integer) 1
127.0.0.1:6379> OBJECT encoding user:2
"hashtable"
结论:当属性个数大于 3 时,编码格式为 hashtable