快捷搜索:  汽车  科技

内存数据是怎样存储的(换一种存储方式)

内存数据是怎样存储的(换一种存储方式)因为这个哈希表保存了所有的键值对,所以,我也把它称为全局哈希表。哈希表的最大好处很明显,就是让我们可以用 O(1) 的时间复杂度来快速查找到键值对——我们只需要计算键的哈希值,就可以知道它所对应的哈希桶位置,然后就可以访问相应的 entry 元素。每个entry 都会有一个数据类型。其实在Redis中,并不是单纯将key 与value保存到内存中就可以的。它需要依赖上面讲的数据结构对其进行管理。其实,这些只是 Redis 键值对中值的数据类型,也就是数据的保存形式。而这里,我们说的数据结构,是要去看看它们的底层实现。简单说底层数据结构一共有 6 种:Redis 存储结构总览

前言

提到缓存,就会想到redis 提到 Redis,我们的脑子里马上就会出现一个词:快。那么我们也知道,redis 之所以这么快,因为数据是放在内存中的,但是内存是非常昂贵的,怎么来设计我们的应用的存储结构,让应用满足正常的业务的前提下来节约内存呢?首先我们从Redis的数据类型开始看起。

Redis 的数据类型及底层实现

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

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

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

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

Redis 存储结构总览

其实在Redis中,并不是单纯将key 与value保存到内存中就可以的。它需要依赖上面讲的数据结构对其进行管理。

内存数据是怎样存储的(换一种存储方式)(1)

因为这个哈希表保存了所有的键值对,所以,我也把它称为全局哈希表。哈希表的最大好处很明显,就是让我们可以用 O(1) 的时间复杂度来快速查找到键值对——我们只需要计算键的哈希值,就可以知道它所对应的哈希桶位置,然后就可以访问相应的 entry 元素。每个entry 都会有一个数据类型。

Redis 不同数据类型的编码

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

编码编码对应的底层数据结构INTlong 类型的整数EMBSTRembstr 编码的简单动态字符串RAW简单动态字符串HT字典 HASH_TABLELINKEDLIST双端链表ZIPLIST压缩列表INTSET整数集合SKIPLIST跳跃表和字典

类型与编码映射

类型编码编码对应的底层数据结构STRINGINTlong 类型的整数STRINGEMBSTRembstr 编码的简单动态字符串STRINGRAW简单动态字符串LISTZIPLIST压缩列表LISTQUICKLIST快速列表LISTLINKEDLIST双端链表HASHZIPLIST压缩列表HASHHT字典SETINTSET整数集合SETHT字典ZSETZIPLIST压缩列表ZSETSKIPLIST跳跃表和字典


具体的映射关系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,表示列表结束。

内存数据是怎样存储的(换一种存储方式)(2)

  • 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 这三种编码模式 如下所示:

内存数据是怎样存储的(换一种存储方式)(3)

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

内存数据是怎样存储的(换一种存储方式)(4)

而且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类型:

Long id = 10000000000l; for (Long i=0l;i<200000l;i ){ id =i; System.out.println(i); try { String encode = MD5.encode(id ""); jCacheClient.set(encode "1"); sleeptime(1);//防止qps 过高 } catch (Exception e) { e.printStackTrace(); sleeptime(1000); } }

flushdb OK info memory $1165 # Memory used_memory:135261368 info memory $1166 # Memory used_memory:156517632 使用 20.27m

hash类型

Long id = 10000000000l; for (Long i=0l;i<200000l;i ){ id =i; try { String encode = MD5.encode(id ""); String prex = encode.substring(0 3); String key = encode.substring(3 32); jCacheClient.hset(prex key "1"); sleeptime(1); } catch (Exception e) { e.printStackTrace(); sleeptime(1000); } }

flushdb OK info memory $1165 # Memory used_memory:135220400 info memory $1166 # Memory used_memory:142697280 内存使用 7.13M

只是改了一个存储结构,内存节约了大概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 命令 我们可以查看 阀值的设置:

内存数据是怎样存储的(换一种存储方式)(5)

猜您喜欢: