Redis(二)内存中的存储细节

概述

关于Redis数据存储的细节,涉及到内存分配器(如jemalloc)、简单动态字符串(SDS)、5种对象类型及内部编码、redisObject。在讲述具体内容之前,先说明一下这几个概念之间的关系。
例如执行set hello world时,所涉及到的数据模型。

dictEntry

首先redis作为K-V数据库,K-V存储结构都为dictEntry。

typedef struct dictEntry{
    //键
    void *key;
    //值
    union{
        void *val;
        uint64_tu64;
        int64_ts64;
    }v;
    //指向下一个哈希表节点,形成链表
    struct dictEntry *next;
} dictEntry

每个键值对都会有一个dictEntry,里面存储了指向Key和Value的指针;next指向下一个dictEntry,与本Key-Value无关

key

Key(”hello”)并不是直接以字符串存储,而是存储在SDS结构中

SDS

Redis 没有直接使用 C 字符串(即以空字符’\0’结尾的字符数组)作为默认的字符串表示,而是使用了SDS。SDS 是简单动态字符串(Simple Dynamic String)的缩写。
它是自己构建了一种名为 简单动态字符串(simple dynamic string,SDS)的抽象类型,并将 SDS 作为Redis的默认字符串表示。

struct sdshdr{
	//记录buf数组中已使用字节的数量
	//等于 SDS 保存字符串的长度
	int len;
	//记录 buf 数组中未使用字节的数量
	int free;
	//字节数组,用于保存字符串
	char buf[];
}

我们看上面对于 SDS 数据类型的定义:

  1. len 保存了SDS保存字符串的长度
  2. buf[] 数组用来保存字符串的每个元素
  3. free j记录了 buf 数组中未使用的字节数量

val

Value(“world”)既不是直接以字符串存储,也不是像Key一样直接存储在SDS中,而是存储在redisObject中。实际上,不论Value是5种类型的哪一种,都是通过redisObject来存储的;而redisObject中的type字段指明了Value对象的类型,ptr字段则指向对象所在的地址。不过可以看出,字符串对象虽然经过了redisObject的包装,但仍然需要通过SDS存储。

typedef struct redisObject {
	unsigned type:4;//类型 五种对象类型
	unsigned encoding:4;//编码
	void *ptr;//指向底层实现数据结构的指针
	//...
	int refcount;//引用计数
	//...
	unsigned lru:22;//记录最后一次被命令程序访问的时间
	//...
}robj;
type

type 字段表示对象的类型,占 4 个比特;目前包括 REDIS_STRING(字符串)、REDIS_LIST (列表)、REDIS_HASH(哈希)、REDIS_SET(集合)、REDIS_ZSET(有序集合)。
当我们执行 type 命令时,便是通过读取 RedisObject 的 type 字段获得对象的类型,如下所示:
127.0.0.1:6379> type a1
string

encoding

encoding 表示对象的内部编码,占 4 个比特。对于 Redis 支持的每种类型,都有至少两种内部编码,例如对于字符串,有 int、embstr、raw 三种编码。
通过 encoding 属性,Redis 可以根据不同的使用场景来为对象设置不同的编码,大大提高了 Redis 的灵活性和效率。
以列表对象为例,有压缩列表和双端链表两种编码方式;如果列表中的元素较少,Redis 倾向于使用压缩列表进行存储,因为压缩列表占用内存更少,而且比双端链表可以更快载入。
当列表对象元素较多时,压缩列表就会转化为更适合存储大量元素的双端链表。
通过 object encoding 命令,可以查看对象采用的编码方式,如下所示:
127.0.0.1:6379> object encoding a1
“int”

lru

lru 记录的是对象最后一次被命令程序访问的时间,占据的比特数不同的版本有所不同(如 4.0 版本占24 比特,2.6 版本占 22 比特)。
通过对比 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 时,对象占用的内存会被释放。
Redis 中被多次使用的对象(refcount>1),称为共享对象。Redis 为了节省内存,当有一些对象重复出现时,新的程序不会创建新的对象,而是仍然使用原来的对象。
这个被重复使用的对象,就是共享对象。目前共享对象仅支持整数值的字符串对象。
共享对象的引用次数可以通过 object refcount 命令查看,如下所示。命令执行的结果页佐证了只有
0~9999 之间的整数会作为共享对象。
127.0.0.1:6379> object refcount a1
(integer) 2147483647

ptr

ptr 指针指向具体的数据,比如:set hello world,ptr 指向包含字符串 world 的 SDS。
综上所述,RedisObject 的结构与对象类型、编码、内存回收、共享对象都有关系。

jemalloc

jemalloc作为Redis的默认内存分配器,在减小内存碎片方面做的相对比较好。jemalloc在64位系统中,将内存空间划分为小、大、巨大三个范围;每个范围内又划分了许多小的内存块单位;当Redis存储数据时,会选择大小最合适的内存块进行存储。
jemalloc划分的内存单元如下图所示:
在这里插入图片描述

例如,如果需要存储大小为130字节的对象,jemalloc会将其放入160字节的内存单元中。

Redis的对象类型与内部编码

Redis支持5种对象类型,而每种结构都有至少两种编码;
这样做的好处在于:

  1. 接口与实现分离,当需要增加或改变内部编码时,用户使用不受影响;
  2. 可以根据不同的应用场景切换内部编码,提高效率。

Redis各种对象类型支持的内部编码如下图所示(只列出重点的):
在这里插入图片描述
关于Redis内部编码的转换,都符合以下规律:编码转换在Redis写入数据时完成,且转换过程不可逆,只能从小内存编码向大内存编码转换。

字符串

字符串是最基础的类型,因为所有的键都是字符串类型,且字符串之外的其他几种复杂类型的元素也是字符串。
字符串长度不能超过512MB。

内部编码

int

8个字节的长整型。字符串值是整型时,这个值使用long整型表示。

embstr

小于44字节的字符串。embstr与raw都使用redisObject和sds保存数据,区别在于,embstr的使用只分配一次内存空间(因此redisObject和sds是连续),而raw需要分配两次内存空间(分别为redisObject和sds分配空间)。因此与raw相比,embstr的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便。而embstr的坏处也很明显,如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间,因此redis中的embstr实现为只读

raw

大于44字节的字符串。

embstr和raw进行区分的长度,是39;是因为redisObject的长度是16字节,sds的长度是9+字符串长度;因此当字符串长度是39时,embstr的长度正好是16+9+39=64,jemalloc正好可以分配64字节的内存单元。

编码转换

当int数据不再是整数,或大小超过了long的范围时,自动转化为raw。
而对于embstr,由于其实现是只读的,因此在对embstr对象进行修改时,都会先转化为raw再进行修改,因此,只要是修改embstr对象,修改后的对象一定是raw的,无论是否达到了39个字节。
示例如下图所示:
在这里插入图片描述

List

概况

列表(list)用来存储多个有序的字符串,每个字符串称为元素。
1 linkedlist和arraylist的区别一个列表可以存储2^32-1个元素。
Redis中的列表支持两端插入和弹出,并可以获得指定位置(或范围)的元素,可以充当数组、队列、栈等。

内部编码

压缩列表(ziplist)

压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块(而不是像双端链表一样每个节点是指针)组成的顺序型数据结构。

双端链表(linkedlist)

双端链表:由一个list结构和多个listNode结构组成。
双端链表同时保存了表头指针和表尾指针,并且每个节点都有指向前和指向后的指针;链表中保存了列表的长度;dup、free和match为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。而链表中每个节点指向的是type为字符串的redisObject。

对比

与双端链表相比,压缩列表可以节省内存空间,但是进行修改或增删操作时,复杂度较高;因此当节点数量较少时,可以使用压缩列表;但是节点数量多时,还是使用双端链表划算。

编码转换

只有同时满足下面两个条件时,才会使用压缩列表:

  • 列表中元素数量小于512个;
  • 列表中所有字符串对象都不足64字节。

如果有一个条件不满足,则使用双端列表;且编码只可能由压缩列表转化为双端链表,反方向则不可能。

针对Redis个数据类型的内存优化

优化内存占用

利用jemalloc特性进行优化

由于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参数,便可以利用共享对象节省内存空
间。

关注内存碎片率

内存碎片率是一个重要的参数,对redis 内存的优化有重要意义。
如果内存碎片率过高(jemalloc在1.03左右比较正常),说明内存碎片多,内存浪费严重;这时便可以考虑重启redis服务,在内存中对数据进行重排,减少内存碎片。
如果内存碎片率小于1,说明redis内存不足,部分数据使用了虚拟内存(即swap);由于虚拟内存的存取速度比物理内存差很多(2-3个数量级),此时redis的访问速度可能会变得很慢。因此必须设法增大物理内存(可以增加服务器节点数量,或提高单机内存),或减少redis中的数据。
要减少redis中的数据,除了选用合适的数据类型、利用共享对象等,还有一点是要设置合理的数据回收策略(maxmemory-policy),当内存达到一定量后,根据不同的优先级对内存进行回收。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

加班狂魔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值