美文网首页
Redis 的内存模型

Redis 的内存模型

作者: Minnakey | 来源:发表于2020-02-25 10:02 被阅读0次
Redis 内存统计

查看内存:

info memory
127.0.0.1:6379> info memory
# Memory
used_memory:855016
used_memory_human:834.98K
used_memory_rss:5042176
used_memory_rss_human:4.81M
mem_fragmentation_ratio:6.19   #mem_fragmentation_ratio 值很大,是因为还没有向 Redis 中存入数据,Redis 进程本身运行的内存使得 used_memory_rss 比 used_memory 大得多
mem_fragmentation_bytes:4228176
mem_allocator:jemalloc-5.1.0

used_memory : Redis 分配器分配的内存总量
used_memory_rss : Redis 进程占据操作系统的内存(单位是字节)top,ps
mem_fragmentation_ratio : 内存碎片比率,该值是 used_memory_rss / used_memory 的比值 mem_fragmentation_ratio<1,说明 Redis 使用了虚拟内存,由于虚拟内存的媒介是磁盘,比内存速度要慢很多
mem_allocator : Redis 使用的内存分配器

Redis 内存划分

Redis 的内存占用主要可以划分为以下几个部分:

  • 数据
  • 进程本身运行需要的内存
  • 缓冲内存
    缓冲内存包括客户端缓冲区、复制积压缓冲区、AOF 缓冲区等;其中,客户端缓冲区存储客户端连接的输入输出缓冲;复制积压缓冲区用于部分复制功能;AOF 缓冲区用于在进行 AOF 重写时,保存最近的写入命令。
  • 内存碎片 -- 是 Redis 在分配、回收物理内存过程中产生的。
Redis 数据存储的细节
Redis 数据存储
  • dictEntry:Redis 是 Key-Value 数据库,因此对每个键值对都会有一个 dictEntry,里面存储了指向 Key 和 Value 的指针;next 指向下一个 dictEntry,与本 Key-Value 无关。
  • Key:图中右上角可见,Key(”hello”)并不是直接以字符串存储,而是存储在 SDS 结构中。
  • RedisObject:Value(“world”)既不是直接以字符串存储,也不是像 Key 一样直接存储在 SDS 中,而是存储在 RedisObject 中。
  • jemalloc:无论是 DictEntry 对象,还是 RedisObject、SDS 对象,都需要内存分配器(如 jemalloc)分配内存进行存储

一个 RedisObject 对象的大小为 16 字节:4bit+4bit+24bit+4Byte+8Byte=16Byte。

jemalloc
Redis 在编译时便会指定内存分配器;内存分配器可以是 libc 、jemalloc 或者 tcmalloc,默认是 jemalloc。
jemalloc 作为 Redis 的默认内存分配器,在减小内存碎片方面做的相对比较好。
jemalloc 在 64 位系统中,将内存空间划分为小、大、巨大三个范围;每个范围内又划分了许多小的内存块单位;当 Redis 存储数据时,会选择大小最合适的内存块进行存储。
jemalloc 划分的内存单元如下图所示:

jemalloc 内存单元
例如,如果需要存储大小为 130 字节的对象,jemalloc 会将其放入 160 字节的内存单元中。
RedisObject
Redis 对象的类型、内部编码、内存回收、共享对象等功能,都需要 RedisObject 支持;
RedisObject 的每个字段的含义和作用如下:
type --> 字段表示对象的类型,占 4 个比特;目前包括 REDIS_STRING(字符串)、REDIS_LIST (列表)、REDIS_HASH(哈希)、REDIS_SET(集合)、REDIS_ZSET(有序集合)。当我们执行 type 命令时,便是通过读取 RedisObject 的 type 字段获得对象的类型;
encoding --> 表示对象的内部编码,占 4 个比特。对于 Redis 支持的每种类型,都有至少两种内部编码,例如对于字符串,有 int、embstr、raw 三种编码。通过 object encoding 命令,可以查看对象采用的编码方式
lru --> 记录的是对象最后一次被命令程序访问的时间,占据的比特数不同的版本有所不同.通过对比 lru 时间与当前时间,可以计算某个对象的空转时间;object idletime 命令可以显示该空转时间(单位是秒)。object idletime 命令的一个特殊之处在于它不改变对象的 lru 值。lru 值除了通过 object idletime 命令打印之外,还与 Redis 的内存回收有关系。
如果 Redis 打开了 maxmemory 选项,且内存回收算法选择的是 volatile-lru 或 allkeys—lru,那么当 Redis 内存占用超过 maxmemory 指定的值时,Redis 会优先选择空转时间最长的对象进行释放。
refcount -->refcount 与共享对象:refcount 记录的是该对象被引用的次数,类型为整型。refcount 的作用,主要在于对象的引用计数和内存回收。
当创建新对象时,refcount 初始化为 1;当有新程序使用该对象时,refcount 加 1;当对象不再被一个新程序使用时,refcount 减 1;当 refcount 变为 0 时,对象占用的内存会被释放。object recount [0-9999]
ptr ptr 指针指向具体的数据,如前面的例子中,set hello world,ptr 指向包含字符串 world 的 SDS。
SDS

Redis 没有直接使用 C 字符串(即以空字符’\0’结尾的字符数组)作为默认的字符串表示,而是使用了 SDS。SDS 是简单动态字符串(Simple Dynamic String)的缩写。

SDS 结构

其中,buf 表示字节数组,用来存储字符串;len 表示 buf 已使用的长度;free 表示 buf 未使用的长度。
一个 SDS 结构占据的空间为:free 所占长度+len 所占长度+ buf 数组的长度= 4+4+free+len+1=free+len+9。

Redis 的对象类型与内部编码
  • 字符串
    1.内部编码:
    int:8 个字节的长整型。字符串值是整型时,这个值使用 long 整型表示。
    embstr:<=39 字节的字符串。只分配一次内存空间, RedisObject 和 sds 是连续的.
    当字符串长度是 39 时,embstr 的长度正好是 16+9+39=64,jemalloc 正好可以分配 64 字节的内存单元。
    raw:大于 39 个字节的字符串。需要分配两次内存空间(分别为 RedisObject 和 sds 分配空间).
    2.编码转换
    当 int 数据不再是整数,或大小超过了 long 的范围时,自动转化为 raw。
    对于 embstr,由于其实现是只读的,因此在对 embstr 对象进行修改时,都会先转化为 raw 再进行修改。因此,只要是修改 embstr 对象,修改后的对象一定是 raw 的,无论是否达到了 39 个字节。
  • 列表
    1.内部编码:列表的内部编码可以是压缩列表(ziplist)或双端链表(linkedlist)
    双端链表:由一个 list 结构和多个 listNode 结构组成;
    压缩列表:压缩列表是 Redis 为了节约内存而开发的,是由一系列特殊编码的连续内存块(而不是像双端链表一样每个节点是指针)组成的顺序型数据结构,具体结构相对比较复杂。
    当节点数量较少时,可以使用压缩列表;但是节点数量多时,还是使用双端链表划算
    2.编码转换
    只有同时满足下面两个条件时,才会使用压缩列表:列表中元素数量小于 512 个;列表中所有字符串对象都不足 64 字节。
  • 哈希
    1.内部编码:内层的哈希使用的内部编码可以是压缩列表(ziplist)和哈希表(hashtable)2 种;Redis 的外层的哈希则只使用了 hashtable。
    hashtable:一个 hashtable 由 1 个 dict 结构、2 个 dictht 结构、1 个 dictEntry 指针数组(称为 bucket)和多个 dictEntry 结构组成。
    hashtable

    dictEntry:dictEntry 结构用于保存键值对
    key:键值对中的键。
    val:键值对中的值,使用 union(即共用体)实现,存储的内容既可能是一个指向值的指针,也可能是 64 位整型,或无符号 64 位整型。
    next:指向下一个 dictEntry,用于解决哈希冲突问题。
    在 64 位系统中,一个 dictEntry 对象占 24 字节(key/val/next 各占 8 字节)。

    dictEntry
    bucket:bucket 是一个数组,数组的每个元素都是指向 dictEntry 结构的指针。
    Redis 中 bucket 数组的大小计算规则如下:大于 dictEntry 的、最小的 2^n。
    dictht:dictht 结构如下。
    dictht
    table 属性是一个指针,指向 bucket。
    size 属性记录了哈希表的大小,即 bucket 的大小。
    used 记录了已使用的 dictEntry 的数量。
    sizemask 属性的值总是为 size-1,这个属性和哈希值一起决定一个键在 table 中存储的位置。
    dict:一般来说,通过使用 dictht 和 dictEntry 结构,便可以实现普通哈希表的功能。
    但是 Redis 的实现中,在 dictht 结构的上层,还有一个 dict 结构。下面说明 dict 结构的定义及作用。
    dict 结构
    其中,type 属性和 privdata 属性是为了适应不同类型的键值对,用于创建多态字典。
    ht 属性和 trehashidx 属性则用于 rehash,即当哈希表需要扩展或收缩时使用。
    ht 是一个包含两个项的数组,每项都指向一个 dictht 结构,这也是 Redis 的哈希会有 1 个 dict、2 个 dictht 结构的原因。

2.编码转换
只有同时满足下面两个条件时,才会使用压缩列表:哈希中元素数量小于 512 个;哈希中所有键值对的键和值字符串长度都小于 64 字节。

  • 集合
    1.内部编码:集合的内部编码可以是整数集合(intset)或哈希表(hashtable);集合在使用哈希表时,值全部被置为 null。
    整数集合的结构定义如下:
    intset
    其中,encoding 代表 contents 中存储内容的类型,虽然 contents(存储集合中的元素)是 int8_t 类型。但实际上其存储的值是 int16_t、int32_t 或 int64_t,具体的类型便是由 encoding 决定的,length 表示元素个数。
    整数集合适用于集合所有元素都是整数且集合元素数量较小的时候.
    2.编码转换
    只有同时满足下面两个条件时,集合才会使用整数集合:集合中元素数量小于 512 个,集合中所有元素都是整数值。
  • 有序集合
    1.内部编码:有序集合的内部编码可以是压缩列表(ziplist)或跳跃表(skiplist)。
    跳跃表是一种有序数据结构,通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
    除了跳跃表,实现有序数据结构的另一种典型实现是平衡树;大多数情况下,跳跃表的效率可以和平衡树媲美,且跳跃表实现比平衡树简单很多,因此 Redis 中选用跳跃表代替平衡树。
    Redis 的跳跃表实现由 zskiplist 和 zskiplistNode 两个结构组成:前者用于保存跳跃表信息(如头结点、尾节点、长度等),后者用于表示跳跃表节点,具体结构相对比较复杂。
    2.编码转换
    只有同时满足下面两个条件时,才会使用压缩列表:有序集合中元素数量小于 128 个;有序集合中所有成员长度都不足 64 字节。
应用举例

估算 Redis 内存使用量
下面以最简单的字符串类型来进行说明。

假设有 90000 个键值对,每个 key 的长度是 7 个字节,每个 value 的长度也是 7 个字节(且 key 和 value 都不是整数),下面来估算这 90000 个键值对所占用的空间。

在估算占据空间之前,首先可以判定字符串类型使用的编码方式:embstr。

90000 个键值对占据的内存空间主要可以分为两部分:
90000 个 dictEntry 占据的空间。
键值对所需要的 bucket 空间。

每个 dictEntry 占据的空间包括:
一个 dictEntry,24 字节,jemalloc 会分配 32 字节的内存块。
一个 key,7 字节,所以 SDS(key)需要 7+9=16 个字节,jemalloc 会分配 16 字节的内存块。
一个 RedisObject,16 字节,jemalloc 会分配 16 字节的内存块。
一个 value,7 字节,所以 SDS(value)需要 7+9=16 个字节,jemalloc 会分配 16 字节的内存块。
综上,一个 dictEntry 需要 32+16+16+16=80 个字节。

bucket 空间:bucket 数组的大小为大于 90000 的最小的 2^n,是 131072;每个 bucket 元素为 8 字节(因为 64 位系统中指针大小为 8 字节)。

因此,可以估算出这 90000 个键值对占据的内存大小为:9000080 + 1310728 = 8248576。
下面写个程序在 Redis 中验证一下:

运行结果:8247552
理论值与结果值误差在万分之 1.2,对于计算需要多少内存来说,这个精度已经足够了。

之所以会存在误差,是因为在我们插入 90000 条数据之前 Redis 已分配了一定的 bucket 空间,而这些 bucket 空间尚未使用。

作为对比将 key 和 value 的长度由 7 字节增加到 8 字节,则对应的 SDS 变为 17 个字节,jemalloc 会分配 32 个字节,因此每个 dictEntry 占用的字节数也由 80 字节变为 112 字节。

此时估算这 90000 个键值对占据内存大小为:90000112 + 1310728 = 11128576。

在Redis 中验证代码如下(只修改插入数据的代码):

image

运行结果:11128576,估算准确。

对于字符串类型之外的其他类型,对内存占用的估算方法是类似的,需要结合具体类型的编码方式来确定。

优化内存占用

了解 Redis 的内存模型,对优化 Redis 内存占用有很大帮助。下面介绍几种优化场景。

利用 jemalloc 特性进行优化

上一小节所讲述的 90000 个键值便是一个例子。由于 jemalloc 分配内存时数值是不连续的,因此 key/value 字符串变化一个字节,可能会引起占用内存很大的变动,在设计时可以利用这一点。

例如,如果 key 的长度是 8 个字节,则 SDS 为 17 字节,jemalloc 分配 32 字节。

此时将 key 长度缩减为 7 个字节,则 SDS 为 16 字节,jemalloc 分配 16 字节;则每个 key 所占用的空间都可以缩小一半。

使用整型/长整型

如果是整型/长整型,Redis 会使用 int 类型(8 字节)存储来代替字符串,可以节省更多空间。

因此在可以使用长整型/整型代替字符串的场景下,尽量使用长整型/整型。

共享对象

利用共享对象,可以减少对象的创建(同时减少了 RedisObject 的创建),节省内存空间。

目前 Redis 中的共享对象只包括 10000 个整数(0-9999);可以通过调整 REDIS_SHARED_INTEGERS 参数提高共享对象的个数。

例如将 REDIS_SHARED_INTEGERS 调整到 20000,则 0-19999 之间的对象都可以共享。

考虑这样一种场景:论坛网站在 Redis 中存储了每个帖子的浏览数,而这些浏览数绝大多数分布在 0-20000 之间。

这时候通过适当增大 REDIS_SHARED_INTEGERS 参数,便可以利用共享对象节省内存空间。

避免过度设计

然而需要注意的是,不论是哪种优化场景,都要考虑内存空间与设计复杂度的权衡;而设计复杂度会影响到代码的复杂度、可维护性。

如果数据量较小,那么为了节省内存而使得代码的开发、维护变得更加困难并不划算;还是以前面讲到的 90000 个键值对为例,实际上节省的内存空间只有几 MB。

但是如果数据量有几千万甚至上亿,考虑内存的优化就比较必要了。

关注内存碎片率

内存碎片率是一个重要的参数,对 Redis 内存的优化有重要意义。

如果内存碎片率过高(jemalloc 在 1.03 左右比较正常),说明内存碎片多,内存浪费严重。

这时便可以考虑重启 Redis 服务,在内存中对数据进行重排,减少内存碎片。

如果内存碎片率小于 1,说明 Redis 内存不足,部分数据使用了虚拟内存(即 swap)。

由于虚拟内存的存取速度比物理内存差很多(2-3 个数量级),此时 Redis 的访问速度可能会变得很慢。

因此必须设法增大物理内存(可以增加服务器节点数量,或提高单机内存),或减少 Redis 中的数据。

要减少 Redis 中的数据,除了选用合适的数据类型、利用共享对象等,还有一点是要设置合理的数据回收策略(maxmemory-policy),当内存达到一定量后,根据不同的优先级对内存进行回收。

整理自
http://www.cnblogs.com/kismetv/p/8654978.html
https://mp.weixin.qq.com/s/GaCpNatAII4iBIUlwVdYOA

相关文章

  • redis内存优化的探索和实践

    1,redis如何管理内存结构 redis内存模型: 【used_memory】:Redis内存占用中最主要的部分...

  • 面试题|Java|Redis

    Redis内存模型 Redis内存分配 数据 :Redis存储的数据对象 字符串、哈希、列表、集合、有序集合 进程...

  • java面试宝典 redis和分布式锁

    redis 是基于key-value的内存高速缓存数据库 redis 是单线程模型 redis 是单线程模型为什么...

  • 【问答】Redis

    Memcache和Redis的区别?Redis和memcached 的内存管理的区别? 网络IO模型:Memcac...

  • Redis内存模型

    前言 Redis是目前最火爆的内存数据库之一,通过在内存中读写数据,大大提高了读写速度,可以说Redis是实现网站...

  • Redis内存模型

    Redis是目前最火的数据库之一,通过在内存中读写数据,大大提高了读写速度,可以说Redis是实现网站高并发不可或...

  • 【redis内存模型】

    一、内存统计命令 info memory 该命令会输出redis的相关信息,包括内存使用情况,cpu等数据 use...

  • Redis内存模型

    一、Redis内存统计 工欲善其事必先利其器,在说明Redis内存之前首先说明如何统计Redis使用内存的情况。 ...

  • Redis内存模型

    一、概述 Redis有五种对象类型:String,List,Set,ZSet,Hash。进一步的理解Redis的内...

  • Redis内存模型

    ​ 1、估算Redis内存使用量。目前为止,内存的使用成本仍然相对较高,使用内存不能无所顾忌;根据需求...

网友评论

      本文标题:Redis 的内存模型

      本文链接:https://www.haomeiwen.com/subject/nbemqhtx.html