百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

「Redis基础篇」内存模型及实现(redis内存机制)

mhr18 2024-10-24 11:15 26 浏览 0 评论

我们都知道Redis是基于内存的一个存储系统,那么Redis是怎么划分内存的?怎么分配、管理内存的呢?今天这篇文章主要分享一下Redis的内存分配、计算redis分配的内存和简单地查看Redis内存使用情况。

序、查看redis内存

redis:0>info memory
"# Memory
used_memory:10935768
used_memory_human:10.43M
used_memory_rss:8794112
used_memory_peak:54483304
used_memory_peak_human:51.96M
used_memory_lua:36864
mem_fragmentation_ratio:0.80
mem_allocator:jemalloc-3.6.0

需要注意的是其中 used_memory 表示Redis内部存储的所有数据占用的内存,也就是对象内存,而 used_memory_rss 表示操作系统为Redis进程分配的物理内存总量,也就是used_memory、redis自身、缓冲内存、内存碎片这四种内存的总和。
当used_memory < used_memory_rss时,说明used_memory_rss中多余出来的内存没有被用来存储对象。如果两个的值差值很大,说明碎片率很高。当used_memory > used_memory_rss时,说明操作系统采用了虚拟内存,把Redis内存交换(swap)到硬盘。内存交换(swap)对于Redis来说是致命的,Redis能保证高性能的一个重要前提就是读写都基于内存。如果操作系统把Redis使用的部分内存交换到硬盘,由于硬盘的读写效率比内存低上百倍,会导致Redis性能急剧下降,特别容易引起Redis阻塞。当这种情况出现时,应该及时排查,如果内存不足应该及时处理,如增加Redis节点、增加Redis服务器的内存、优化应用等。
mem_fragmentation_ratio一般大于1,且该值越大,内存碎片比例越大。如果mem_fragmentation_ratio<1,说明Redis使用了虚拟内存。一般来说,mem_fragmentation_ratio在1.03左右是比较健康的状态(对于jemalloc来说);

一、redis内存的划分

Redis作为内存数据库,在内存中存储的内容主要是数据(键值对)。除了数据以外,Redis的其它部分也会占用内存。Redis进程的内存消耗主要包括:自身内存 + 对象内存 (数据)+ 缓冲内存 + 内存碎片。下面就对各部分做一下详细介绍:

1.1、自身内存

Redis主进程本身运行肯定需要占用内存,如代码、常量池等等。这部分内存大约几兆,在大多数生产环境中与Redis数据占用的内存相比可以忽略。这部分内存不是由jemalloc分配,因此不会统计在used_memory中。

补充说明:除了主进程外,Redis创建的子进程运行也会占用内存,如Redis执行AOF、RDB重写时创建的子进程。当然,这部分内存不属于Redis进程,也不会统计在used_memory和used_memory_rss中。

1.2、对象内存(数据)

作为数据库,数据是最主要的部分,这部分占用的内存会统计在used_memory中。
Redis使用键值对存储数据,其中的值(对象)包括5种类型:字符串、哈希、列表、集合、有序集合。
这5种类型是Redis对外提供的。实际上,在Redis内部,每种类型可能有2种或更多的内部编码实现。此外,Redis在存储对象时,并不是直接将数据扔进内存,而是会对对象进行各种包装:如RedisObject、SDS等。我们后面将重点介绍Redis中数据存储的细节。

1.3、缓冲内存

缓冲内存包括:

  • 客户端缓冲区:存储客户端连接的输入输出缓冲;
  • 复制积压缓冲区:用于部分复制功能;
  • AOF缓冲区:用于在进行AOF重写时,保存最近的写入命令。

1.3.1 客户端缓冲

客户端缓冲指的是所有连到Redis服务器的TCP连接的输入缓冲和输出缓冲。
Redis为每个客户端分配了输入缓冲区,用于临时保存客户端发过来的命令,同时Redis服务器会从输入缓冲区中拉取命令执行,输入缓冲区为客户端向服务端发送的命 令提供了缓冲的功能。输入缓冲区大小无法控制,每个客户端的输入缓冲区最大空间为1G,如果超出将断开连接。
同样的Redis为每个客户端分配了输出缓冲区,用于临时保存服务端执行的命令结果,同时Redis服务器会从输出缓冲区中拉取结果返回到客户端,输出缓冲区为服务端向客户端返回结果提供了缓冲。输出缓冲区根据客户端的类型又划分为三种:普通客户端、发布订阅客户端、从客户端(Redis复制的slave客户端)。每种客户端的输出规则也不一样,这里就不细说了。

1.3.2 复制积压缓冲

Redis在2.8版本之后提供了一个可重用的固定大小的缓冲区用于实现增量复制的功能,根据repl-backlog-size参数来控制,默认大小1MB。
对于复制积压缓冲区主节点只有一个,所有从节点共享一个,这个缓冲区可以有效地避免全量复制。

1.3.3 AOF重写缓冲区

AOF重写缓冲区用于在AOF重写期间保存写命令,所以AOF重写缓冲区的大小取决于AOF的时间以及AOF重写期间的写命令数量。
等到AOF重写完成之后,会将AOF重写缓冲区中的数据写到AOF文件,从而清空AOF重写缓冲区。

1.4、内存碎片(数据)

内存碎片是Redis在分配、回收物理内存过程中产生的。例如,如果对数据更改频繁,而且数据之间的大小相差很大,可能导致Redis释放的空间在物理内存中并没有释放,但Redis又无法有效利用,这就形成了内存碎片。内存碎片不会统计在used_memory中。
内存碎片的产生与对数据进行的操作、数据的特点等都有关。此外,与使用的内存分配器也有关系——如果内存分配器设计合理,可以尽可能
减少内存碎片的产生。后面将要说到的jemalloc便在控制内存碎片方面做的很好。Redis默认内存分配器采用jemalloc,可选的分配器还有:glibe、tcmalloc。内存分配器是为了更好管理和重复利用内存,分配内存策略一般采用固定范围的内存块进行内存分配。简单地说jemalloc将内存空间划分为三个部分:Small class、Large class、Huge class,每个部分又划分为很多小的内存块单位,就是采用不同大小的内存块来存储不同大小的内存对象,比如说一个5kb的对象就会存到8kb的内存块中,那么剩下的3kb内存就变成了内存碎片,不能再被分配给其他对象。
通常在对key做频繁更新操作和大量过期key被删除的时候会导致碎片率上升,如果Redis服务器中的内存碎片已经很大,可以通过安全重启的方式减小内存碎片。因为重启之后,Redis重新从备份文件中读取数据,在内存中进行重排,为每个数据重新选择合适的内存单元,减小内存碎片。

二、redis内存存储数据结构

2.1、dictEntry

typedef struct dictEntry{
    void *key;
    union{
        void *val;
        uint64_tu64;
        int64_ts64;
    }v;
    struct dictEntry *next;
}dictEntry;

dictEntry:Redis是Key-Value数据库,因此对每个键值对都会有一个dictEntry,里面存储了指向Key和Value的指针;next指向下一个dictEntry,与本Key-Value无关。

下图是执行set hello world时,所涉及到的数据模型。

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

2.2、RedisObject

衔接底层数据结构,与五种Value Type之间的桥梁就是redisObject这个结构.。

typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:REDIS_LRU_BITS;  /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;
    void *ptr;
} robj;

前面说到,Redis对象有5种类型;无论是哪种类型,Redis都不会直接存储,而是通过RedisObject对象进行存储。RedisObject对象非常重要,Redis对象的类型、内部编码、内存回收、共享对象等功能,都需要RedisObject支持,下面将通过RedisObject的结构来说明它是如何起作用的。

2.2.1 type

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

> set str1 "test"
OK
> type str1
"string"
> sadd myset ms1 ms2 ms3
(integer) 3
> type myset
"set"

2.2.2 encoding

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

> object encoding str1
"embstr"
> object encoding myset
"hashtable"

2.2.3 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会优先选择空转时间最长的对象进行释放。

2.2.4 refcount

refcount与共享对象

refcount记录的是该对象被引用的次数,类型为整型。refcount的作用,主要在于对象的引用计数和内存回收:

  • 当创建新对象时,refcount初始化为1;
  • 当有新程序使用该对象时,refcount加1;
  • 当对象不再被一个新程序使用时,refcount减1;
  • 当refcount变为0时,对象占用的内存会被释放。

Redis中被多次使用的对象(refcount>1)称为共享对象。Redis为了节省内存,当有一些对象重复出现时,新的程序不会创建新的对象,而是仍然使用原来的对象。这个被重复使用的对象,就是共享对象。目前共享对象仅支持整数值的字符串对象。

共享对象的具体实现

  • Redis的共享对象目前只支持整数值的字符串对象。之所以如此,实际上是对内存和CPU(时间)的平衡:共享对象虽然会降低内存消耗,但是判断两个对象是否相等却需要消耗额外的时间。
  • 对于整数值,判断操作复杂度为O(1);
  • 对于普通字符串,判断复杂度为O(n);

而对于哈希、列表、集合和有序集合,判断的复杂度为O(n^2)。
虽然共享对象只能是整数值的字符串对象,但是5种类型都可能使用共享对象(如哈希、列表等的元素可以使用)。
就目前的实现来说,Redis服务器在初始化时,会创建10000个字符串对象,值分别是0~9999的整数值;当Redis需要使用值为0~9999的字符串对象时,可以直接使用这些共享对象。10000这个数字可以通过调整参数REDIS_SHARED_INTEGERS(4.0中是OBJ_SHARED_INTEGERS)的值进行改变。
共享对象的引用次数可以通过
object refcount命令查看。

2.2.5 ptr

ptr指针指向具体的数据,如前面的例子中,set hello world,ptr指向包含字符串world的SDS。

2.2.6 总结

综上所述,redisObject的结构与对象类型、编码、内存回收、共享对象都有关系;一个redisObject对象的大小为16字节:
4bit+4bit+24bit+4Byte+8Byte=16Byte。

2.3、SDS-simple dynamic string

struct sdshdr {
    unsigned int len;
    unsigned int free;
    char buf[];
};

其中,buf表示字节数组,用来存储字符串;len表示buf已使用的长度,free表示buf未使用的长度。

通过SDS的结构可以看出,buf数组的长度=free+len+1(其中1表示字符串结尾的空字符);所以,一个SDS结构占据的空间为:free所占长度+len所占长度+ buf数组的长度=4+4+free+len+1=free+len+9。

2.3.1 SDS与C字符串的比较

SDS在C字符串的基础上加入了free和len字段,带来了很多好处:

  • 获取字符串长度:SDS是O(1),C字符串是O(n)。
  • 缓冲区溢出:使用C字符串的API时,如果字符串长度增加(如strcat操作)而忘记重新分配内存,很容易造成缓冲区的溢出;而SDS由于记录了长度,相应的API在可能造成缓冲区溢出时会自动重新分配内存,杜绝了缓冲区溢出。
  • 修改字符串时内存的重分配:对于C字符串,如果要修改字符串,必须要重新分配内存(先释放再申请),因为如果没有重新分配,字符串长度增大时会造成内存缓冲区溢出,字符串长度减小时会造成内存泄露。而对于SDS,由于可以记录len和free,因此解除了字符串长度和空间数组长度之间的关联,可以在此基础上进行优化——空间预分配策略(即分配内存时比实际需要的多)使得字符串长度增大时重新分配内存的概率大大减小;惰性空间释放策略使得字符串长度减小时重新分配内存的概率大大减小。
  • 存取二进制数据:SDS可以,C字符串不可以。因为C字符串以空字符作为字符串结束的标识,而对于一些二进制文件(如图片等),内容可能包括空字符串,因此C字符串无法正确存取;而SDS以字符串长度len来作为字符串结束标识,因此没有这个问题。

此外,由于SDS中的buf仍然使用了C字符串(即以‘\0’结尾),因此SDS可以使用C字符串库中的部分函数。但是需要注意的是,只有当SDS用来存储文本数据时才可以这样使用,在存储二进制数据时则不行(‘\0’不一定是结尾)。

2.3.2 SDS与C字符串的应用

Redis在存储对象时,一律使用SDS代替C字符串。例如set hello world命令,hello和world都是以SDS的形式存储的。而sadd myset member1 member2 member3命令,不论是键“myset”,还是集合中的元素member1、 member2和member3,都是以SDS的形式存储。除了存储对象,SDS还用于存储各种缓冲区。
只有在字符串不会改变的情况下,如打印日志时,才会使用C字符串。

相关推荐

【预警通报】关于WebLogic存在远程代码执行高危漏洞的预警通报

近日,Oracle官方发布了2021年1月关键补丁更新公告CPU(CriticalPatchUpdate),共修复了包括CVE-2021-2109(WeblogicServer远程代码执行漏洞)...

医院信息系统突发应急演练记录(医院信息化应急演练)

信息系统突发事件应急预案演练记录演练内容信息系统突发事件应急预案演练参与人员信息科参与科室:全院各部门日期xxxx-xx-xx时间20:00至24:00地点信息科记录:xxx1、...

一文掌握怎么利用Shell+Python实现完美版的多数据源备份程序

简介:在当今数字化时代,无论是企业还是个人,数据的安全性和业务的连续性都是至关重要的。数据一旦丢失,可能会造成无法估量的损失。因此,如何有效地对分布在不同位置的数据进行备份,尤其是异地备份,成为了一个...

docker搭建系统环境(docker搭建centos)

Docker安装(CentOS7)1.卸载旧版Docker#检查已安装版本yumlistinstalled|grepdocker#卸载旧版本yumremove-ydocker.x...

基础篇:数据库 SQL 入门教程(sql数据库入门书籍推荐)

SQL介绍什么是SQLSQL指结构化查询语言,是用于访问和处理数据库的标准的计算机语言。它使我们有能力访问数据库,可与多种数据库程序协同工作,如MSAccess、DB2、Informix、M...

Java21杀手级新特性!3行代码性能翻倍

导语某券商系统用这招,交易延迟从12ms降到0.8ms!本文揭秘Oracle官方未公开的Record模式匹配+虚拟线程深度优化+向量API神操作,代码量直降70%!一、Record模式匹配(代码量↓8...

一文读懂JDK21的虚拟线程(java虚拟线程)

概述JDK21已于2023年9月19日发布,作为Oracle标准Java实现的一个LTS版本发布,发布了15想新特性,其中虚拟线程呼声较高。虚拟线程是JDK21中引入的一项重要特性,它是一种轻量级的...

效率!MacOS下超级好用的Linux虚拟工具:Lima

对于MacOS用户来说,搭建Linux虚拟环境一直是件让人头疼的事。无论是VirtualBox还是商业的VMware,都显得过于笨重且配置复杂。今天,我们要介绍一个轻巧方便的纯命令行Linux虚拟工具...

所谓SaaS(所谓三维目标一般都应包括)

2010年前后,一个科技媒体的主编写一些关于云计算的概念性问题,就可以作为头版头条了。那时候的云计算,更多的还停留在一些概念性的问题上。而基于云计算而生的SaaS更是“养在深闺人未识”,一度成为被IT...

ORA-00600 「25027」 「x」报错(报错0xc0000001)

问题现象:在用到LOB大对象的业务中,进行数据的插入,失败了,在报警文件中报错:ORA-00600:内部错误代码,参数:[25027],[10],[0],[],[],[],[],[...

安卓7源码编译(安卓源码编译环境lunch失败,uname命令找不到)

前面已经下载好源码了,接下来是下载手机对应的二进制驱动执行编译源码命令下载厂商驱动https://developers.google.com/android/drivers?hl=zh-cn搜索NGI...

编译安卓源码(编译安卓源码 电脑配置)

前面已经下载好源码了,接下来是下载手机对应的二进制驱动执行编译源码命令下载厂商驱动https://developers.google.com/android/drivers?hl=zh-cn搜索NGI...

360 Vulcan Team首战告捷 以17.5万美金强势领跑2019“天府杯“

2019年11月16日,由360集团、百度、腾讯、阿里巴巴、清华大学与中科院等多家企业和研究机构在成都联合主办了2019“天府杯”国际网络安全大赛暨2019天府国际网络安全高峰论坛。而开幕当日最激荡人...

Syslog 日志分析与异常检测技巧(syslog发送日志配置)

系统日志包含有助于分析网络设备整体运行状况的重要信息。然而,理解并从中提取有效数据往往颇具挑战。本文将详解从基础命令行工具到专业日志管理软件的全流程分析技巧,助你高效挖掘Syslog日志价值。Gr...

从Oracle演进看数据库技术的发展(从oracle演进看数据库技术的发展的过程)

数据库技术发展本质上是应用需求驱动与基础架构演进的双向奔赴,如何分析其技术发展的脉络和方向?考虑到oracle数据库仍然是这个领域的王者,以其为例,管中窥豹,对其从Oracle8i到23ai版本的核...

取消回复欢迎 发表评论: