乐趣区

关于redis:换一种存储方式居然能节约这么多内存

前言

提到缓存,就会想到 redis, 提到 Redis,咱们的脑子里马上就会呈现一个词:快。那么咱们也晓得,redis 之所以这么快,因为数据是放在内存中的,然而内存是十分低廉的,怎么来设计咱们的利用的存储构造,让利用满足失常的业务的前提下来节约内存呢?首先咱们从 Redis 的数据类型开始看起。

Redis 的数据类型及底层实现

说到 redis 的数据类型,大家必定会说:不就是 String(字符串)、List(列表)、Hash(哈希)、Set(汇合)和 Sorted Set(有序汇合)吗?”

其实,这些只是 Redis 键值对中值的数据类型,也就是数据的保留模式。

而这里,咱们说的数据结构,是要去看看它们的底层实现。简略说底层数据结构一共有 6 种:

  • 简略动静字符串
  • 双向链表
  • 压缩列表
  • 哈希表
  • 跳表和整数数组。

Redis 存储构造总览

其实在 Redis 中,并不是单纯将 key 与 value 保留到内存中就能够的。它须要依赖下面讲的数据结构对其进行治理。

因为这个哈希表保留了所有的键值对,所以,我也把它称为全局哈希表。哈希表的最大益处很显著,就是让咱们能够用 O(1) 的工夫复杂度来疾速查找到键值对——咱们只须要计算键的哈希值,就能够晓得它所对应的哈希桶地位,而后就能够拜访相应的 entry 元素。每个 entry 都会有一个数据类型。

Redis 不同数据类型的编码

而且每个类型又对应了多种编码格局,不同的编码格局对应的底层数据结构是不同的。能够先做个理解,前面会具体用到。

编码 编码对应的底层数据结构
INT long 类型的整数
EMBSTR embstr 编码的简略动静字符串
RAW 简略动静字符串
HT 字典 HASH\_TABLE
LINKEDLIST 双端链表
ZIPLIST 压缩列表
INTSET 整数汇合
SKIPLIST 跳跃表和字典

类型与编码映射

类型 编码 编码对应的底层数据结构
STRING INT long 类型的整数
STRING EMBSTR embstr 编码的简略动静字符串
STRING RAW 简略动静字符串
LIST ZIPLIST 压缩列表
LIST QUICKLIST 疾速列表
LIST LINKEDLIST 双端链表
HASH ZIPLIST 压缩列表
HASH HT 字典
SET INTSET 整数汇合
SET HT 字典
ZSET ZIPLIST 压缩列表
ZSET SKIPLIST 跳跃表和字典

具体的映射关系

1. 字符串类型(STRING)对象

编码方式 阐明
编码方式阐明 OBJ\_ENCODING\_RAW 长度超过 44 个字节的字符串以这种形式编码
OBJ\_ENCODING\_INT 能够被转化为 long 类型整数的字符串以这种形式编码
OBJ\_ENCODING\_EMBSTR 不能被转化为 long 类型整数且长度不超过 44 个字节的字符串

|

2. 汇合类型(SET)对象

编码方式 阐明
编码方式阐明 OBJ\_ENCODING\_INTSET 增加的元素是整数,且保留的整数个数不超过配置项 set\_max\_intset\_entries (默认 512)
OBJ\_ENCODING\_HT 增加的元素不是整数或者整数的个数超过配置项 set\_max\_intset\_entries (默认 512)

|

3. 有序汇合类型(ZSET)对象

有序汇合类型的对象有两种编码方式:OBJ\_ENCODING\_SKIPLIST、OBJ\_ENCODING\_ZIPLIST。Redis 对于有序汇合类型的编码有两个配置项:

  • zset\_max\_ziplist\_entries,默认值为 128,示意有序汇合中压缩列表节点的最大数量。
  • zset\_max\_ziplist\_value,默认值为 64,示意有序汇合中压缩列表节点的最大长度。
编码方式 阐明
OBJ\_ENCODING\_ZIPLIST 增加的元素长度不超过 zset\_max\_ziplist\_value,且压缩列表节点数量不超过 zset\_max\_ziplist\_entries
OBJ\_ENCODING\_SKIPLIST OBJ\_ENCODING\_SKIPLIST 增加的元素长度超过 zset\_max\_ziplist\_value,或压缩列表节点数量超过 zset\_max\_ziplist\_entries,会将压缩列表转化为跳表

注:当删除或更新元素,使得满足以上两个配置项时,编码方式是不会主动从 OBJ\_ENCODING\_SKIPLIST 转化为 OBJ\_ENCODING\_ZIPLIST 的,但 Redis 提供了函数 zsetConvertToZiplistIfNeeded 反对。

4. 哈希类型(HASH)对象

哈希类型对象有两种编码方式:OBJ\_ENCODING\_ZIPLIST、OBJ\_ENCODING\_HT。Redis 对于哈希类型对象的编码有两个配置项:

  • hash\_max\_ziplist\_entries,默认值 512,示意哈希类型对象中压缩列表节点的最大数量。
  • hash\_max\_ziplist\_value,默认值 64,示意哈希类型对象中压缩列表节点的最大长度。
编码方式 阐明
OBJ\_ENCODING\_ZIPLIST 增加的元素长度不超过 hash\_max\_ziplist\_value,且压缩列表节点数量不超过 hash\_max\_ziplist\_entries
OBJ\_ENCODING\_HT 增加的元素长度超过 hash\_max\_ziplist\_value,或压缩列表节点数量超过 hash\_max\_ziplist\_entries,会将压缩列表转化为字典

注:当删除或更新元素,使得满足以上两个配置项时,编码方式是不会主动从 OBJ\_ENCODING\_HT 向 OBJ\_ENCODING\_ZIPLIST 转化。

对于压缩链表

因为这个和咱们前面的优化有关系,咱们先来看看什么是压缩链表。

压缩列表实际上相似于一个数组,数组中的每一个元素都对应保留一个数据。和数组不同的是,压缩列表在表头有三个字段 zlbytes、zltail 和 zllen,别离示意列表长度、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,示意列表完结。

  • 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:保留理论数据。

压缩链表的查问

压缩列表的设计不是为了查问的,而是为了缩小内存的应用和内存的碎片化。比方一个列表中的只保留 int,构造上还须要两个额定的指针 prev 和 next,每增加一个结点都这样。而压缩列表是将这些数据集合起来只须要一个 prev 和 next。

在压缩列表中,如果咱们要查找定位第一个元素和最初一个元素,能够通过表头三个字段的长度间接定位,复杂度是 O(1)。而查找其余元素时,就没有这么高效了,只能一一查找,此时的复杂度就是 O(N) 了。

为什么 String 类型内存开销大?

对于 kv 类型的缓存数据,咱们常常会用 redis string 类型。比方日常工作中常常对合作方客户一周内是否营销进行缓存,key 为 32 位的 hash(用户编码),value 为是否(0 或者 1)营销。很合乎 string 的数据类型。

然而随着营销数据量的 一直减少,咱们的 Redis 内存使用量也在减少,后果就遇到了大内存 Redis 实例因为生成 RDB 而响应变慢的问题。很显然,String 类型并不是一种好的抉择,须要进一步寻找能节俭内存开销的数据类型计划。

通过下面的文章,咱们对 string 类型进行钻研,会发现:

当你保留 64 位有符号整数时,String 类型会把它保留为一个 8 字节的 Long 类型整数,这种保留形式通常也叫作 int 编码方式。然而,当你保留的数据中蕴含字符时,String 类型就会用简略动静字符串(Simple Dynamic String,SDS)构造体来保留,

另一方面,当保留的是字符串数据,并且字符串小于等于 44 字节时,RedisObject 中的元数据、指针和 SDS 是一块间断的内存区域,这样就能够防止内存碎片。这种布局形式也被称为 embstr 编码方式。

int、embstr 和 raw 这三种编码模式, 如下所示:

依据文章结尾的示意图,redis 全局 hash 表中的 entry 元素 其实是 dictEntry,dictEntry 构造中有三个 8 字节的指针,别离指向 key、value 以及下一个 dictEntry,三个指针共 24 字节,如下图所示:

而且 redis 模式应用 jemalloc 进行内存治理,jemalloc 在分配内存时,会依据咱们申请的字节数 N,找一个比 N 大,然而最靠近 N 的 2 的幂次数作为调配的空间,这样能够缩小频繁调配的次数。所以,咱们用 string 这种存储简略数据的形式比拟节约内存!!

用什么数据结构能够节俭内存?

那么,用什么数据结构能够节约内存呢?就是咱们下面讲的压缩列表(ziplist),这是一种十分节俭内存的构造。

通过前文编码对应的底层数据结构咱们理解到,应用压缩链表实现的数据结构有 List、Hash 和 Sorted Set 这样的汇合类型。

基于压缩列表最大益处就是节俭了 dictEntry 的开销。当你用 String 类型时,一个键值对就有一个 dictEntry。但采纳汇合类型时,一个 key 就对应一个汇合的数据,能保留的数据多了很多,但也只用了一个 dictEntry,这样就节俭了内存。

通过钻研 hash 数据结构。我发现,hash 类型有十分节俭内存空间的底层实现构造,然而,hash 类型保留的数据模式,是一个键对应一系列值,并不适宜间接保留单值的键值对。所以就应用 二级编码【二级编码:把 32 位前 3 位作为 key,后 29 位作为 field】的办法,实现了用汇合类型保留单值键值对,Redis 实例的内存空间耗费显著降落了。

试验数据

咱们模仿 20w 数据的写入,看看 string 类型和 hash 类型别离对内存的占用状况。

string 类型:

hash 类型

只是改了一个存储构造,内存节约了大略 2 /3.

二级编码应用注意事项

因为 Redis Hash 类型的两种底层实现构造,别离是压缩列表和哈希表。

那么,Hash 类型底层构造什么时候应用压缩列表,什么时候应用哈希表呢?依据下面表格的内容,咱们晓得有两个阈值:

  • hash-max-ziplist-entries:示意用压缩列表保留时哈希汇合中的最大元素个数。
  • hash-max-ziplist-value:示意用压缩列表保留时哈希汇合中单个元素的最大长度。

如果咱们往 Hash 汇合中写入的元素个数超过了 hash-max-ziplist-entries,或者写入的单个元素大小超过了 hash-max-ziplist-value,Redis 就会主动把 Hash 类型的实现构造由压缩列表转为哈希表。一旦从压缩列表转为了哈希表,Hash 类型就会始终用哈希表进行保留,而不会再转回压缩列表了。在节俭内存空间方面,哈希表就没有压缩列表那么高效了。

所以要依据理论状况调整二级编码的实现形式, 节约内存,进步 redis 的响应速度!

通过 config get 命令 咱们能够查看 阀值的设置:

退出移动版