Redis学习(1)内存模型
一、Redis内存统计
客户端通过redis-cli命令连接服务器后,可通过info memory命令查看内存使用情况:
1 | # Memory |
重要参数介绍:
used_memory:Redis分配器分配的内存总量(单位字节),包括使用的虚拟内存。used_memory_human方便人查看。
used_memory_rss:Redis进程占用操作系统大的内存(单位字节)。除了包含分配器分配的内存外,还包括进程运行本身需要的内存,但不包含虚拟内存。
used_memory_rss:内存碎片比率。计算方式:used_memory_rss / used_memory。
mem_allocator:Redis使用的内存分配器,编译时指定,存在三种libc 、jemalloc或者tcmalloc,默认是jemalloc。
二、Redis内存划分
1. 数据
Redis作为数据库,数据是最主要的部分;这部分占用的内存会统计在used_memory中。
Redis使用键值对存储数据,其中的值包括5种类型,即字符串、哈希、列表、集合、有序集合。在Redis内部,每种类型可能有2种或更多的内部编码实现;此外,Redis在存储对象时,并不是直接将数据扔进内存,而是会对对象进行包装:例如redisObject、SDS等。
2. 进程本身运行需要的内存
Redis主进程本身运行肯定需要占用内存,如代码、常量池等等。这部分内存大约几兆,在大多数生产环境中与Redis数据占用的内存相比可以忽略。这部分内存不是由jemalloc分配,因此不会统计在used_memory中。
除主进程外,Redis创建的子进程也会占用内存,如Redis执行AOF、RDB重写时创建的子进程。同样这部分占用的内存也不会统计在used_memory和used_memory_rss中。
3. 缓冲内存
缓冲内存包括客户端缓冲区、复制积压缓冲区、AOF缓冲区等,这部分内存由jemalloc分配,因此会统计在used_memory中。
- 客户端缓冲区:每个客户端连接在 Redis 服务器端对应的输入缓冲区和输出缓冲区。
- 复制挤压缓冲区:主节点专门为从节点准备的写命令历史记录缓存。主节点执行写命令时,不仅应用到自身数据,也会把这些写操作序列化成复制流,并写入这块缓冲区。
- AOF缓冲区:Redis 在开启AOF持久化时,用来暂存最近写命令的内存缓冲区。
4. 内存碎片
内存碎片是在 Redis 分配和回收内存过程中产生的。当数据频繁修改、且对象大小差异较大时,Redis 释放的内存可能仍保留在内存分配器的内存池中,未立即归还给操作系统,也可能无法被后续对象有效利用,从而形成内存碎片。
三、Redis数据存储的细节
1. 概述
执行set hello world命令时涉及到的数据模型:

dictEntry:是Redis字典中存储键值对的基本单元。
- key:指向键的指针,键的类型是sds(redis自定义的字符串结构)。
- val:指向值的指针,这里值的类型是redisObject(redis所有数据类型的统一封装对象)。
- next:指向另一个dictEntry的指针,用于处理哈希冲突。
redisObject:Redis中所有数据(字符串、列表、哈希等)都通过redisObejct封装。
- type:标识对象的类型,这里是string。
- ptr:指向时机数据的指针。
Sds:Redis自定义的字符串实现,相比C原生字符串更高效(支持动态扩容、记录长度等)。
2. Jemalloc
Redis在编译时会指定内存分配器;内存分配器可以是 libc 、jemalloc或者tcmalloc,默认是jemalloc。
jemalloc作为Redis的默认内存分配器,在减小内存碎片方面做的相对比较好。jemalloc在64位系统中,将内存空间划分为小、大、巨大三个范围;每个范围内又划分了许多小的内存块单位;当Redis存储数据时,会选择大小最合适的内存块进行存储。
jemalloc划分的内存单元如下图所示:

因此,如果需要存储的对象大小为130字节,则会将其放入160字节的内存单元中。
3. RedisObject
前面有提到Redis中所有数据(字符串、列表、哈希等)都通过redisObejct封装。
redisObject的定义如下:
1 | typedef struct redisObject { |
- unsigned type:4:用于标识当前对象的数据类型,Redis支持的核心类型包括:REDIS_STRING(字符串)、REDIS_LIST(列表)、REDIS_HASH(哈希)、REDIS_SET(集合)。4位二进制可表示16种类型,预留了拓展空间。
- unsigned encoding:4:标识对象的底层存储结构(Redis 对同一种数据类型可能采用多种编码以优化性能)。例如:
- 字符串类型(type=REDIS_STRING)的编码可能是:
- REDIS_ENCODING_RAW(原生 SDS,存储长字符串)
- REDIS_ENCODING_INT(直接存储整数,避免 SDS 开销)
- 列表类型(type=REDIS_LIST)的编码可能是:
- REDIS_ENCODING_ZIPLIST(压缩列表,适合小数据)
- REDIS_ENCODING_LINKEDLIST(双向链表,适合大数据)。
- 字符串类型(type=REDIS_STRING)的编码可能是:
- unsigned lru:REDIS_LRU_BITS:用于记录对象的最近访问时间,配合 Redis 的内存淘汰策略。
- int refcount:引用计数,用于内存管理的垃圾回收机制。
- void *ptr:指向对象的实际存储数据,具体指向的结构由 encoding 决定:
- 若 encoding=REDIS_ENCODING_INT,ptr 直接指向整数(通过指针强转存储,节省空间)。
- 若 encoding=REDIS_ENCODING_RAW,ptr 指向 sds 结构体(字符串)。
- 若 encoding=REDIS_ENCODING_ZIPLIST,ptr 指向压缩列表的起始地址。
因此,每个redisObject所占大小是4bit+4bit+24bit+4Byte+8Byte=16Byte。
4. SDS
sds用于存储字符串,结构如下:
1 | struct sdshdr { |
参数说明:len表示buf已使用的长度、free表示buf未使用的长度、buf表示字节数组。
因此,每个sds所占的大小是4+4+len+free+1(Byte)
对比C字符串:
- 获取字符串长度:时间复杂度SDS是O(1),C字符串是O(n)。
- 缓冲区溢出:对于C字符串,用strcat方法往字符串中添加字符,如原先分配给字符串的空间不够,会导致缓冲区溢出;对于SDS会检测空间是否足够,如空间不足会自动扩容。
- 字符串修改内存重分配:对于C字符串,如果字符串变长,那么需要重新分配内存,字符串变短,不重新分配内存造成内存浪费;对于SDS,在扩容时会分配更多的空间,以减少realloc的次数,并且遵循惰性空间释放策略,减少缩容的频率,以提高性能。
- 存取二进制数据:对于C字符串,通常以空字符作为字符串结束的标志,对于一些二进制文件,内容可能包含空字符,因而C字符串无法正确读取;对于SDS则是根据len作为字符串结束标志,很好的避免了这个问题。
四、Redis的对象类型与内部编码
前面已经提到,Redis支持5种对象类型,每种结构都有至少两种编码。好处是对于开发者来说,使用Redis只需要面对统一的命令和数据类型(如字符串、列表、哈希),不用关心其底层采用哪种编码;另一方面,可以根据不同的应用场景选用不同的内部编码,以提高效率。
1. 字符串
1.1 概况
字符串是最基础的类型,所有的键都是字符串类型,其长度不能超过512MB。
1.2 内部编码
字符串类型的内部编码有3种:
- int:8个字节的长整型(注意不是4字节)。字符串值是整型时,这个值使用long整型表示。
- embstr:<=39字节的字符串。
- raw:>39字节的字符串。
embstr与raw都使用redisObject和sds保存数据,区别在于,embstr只分配一次内存空间,只读,而raw需要分配两次内存空间,但可修改。
注意:如果采用embstr编码,Redis会一次性申请一块连续空间,例如:
1 | | redisObject | sds 头部 | "Tom\0" | |
如果采用raw编码,则会分别申请redisObject与sds两块内存,例如:
1 | redisObject ---> 指向 ---> | sds头部 | "This is a very very long string ..." | |
1.3 编码转换
- 当int数据不再是整数或者大小超过long的范围时,则自动转换为raw。
- 对于embstr,由于其是只读的,因此在修改时,需要先将其转换为raw再修改。
2. 列表
2.1 概况
列表用来存储多个有序的字符串,每个字符串称为元素;一个列表可以存储2^32-1个元素。Redis中的列表支持两端插入与删除。
2.2 内部编码
列表内部编码有两种:
- 双端链表。
- 压缩列表,是Redis为了节约内存而开发的,由一系列特殊编码的连续内存块组成的顺序型数据结构,因而修改与增删操作的效率低,常用于节点数量少的情况。
2.3 编码转换
使用压缩列表的条件(需同时满足):
- 列表中所有字符串对象都不超过64字节。
- 列表中元素数量小于512个。
如果不满足则采用双端链表,编码只可能由压缩列表转换为双端链表,反方向则不可能。
3. 哈希
3.1 概况
哈希不仅是Redis对外提供的5种对象类型的一种,也是Redis作为Key-Value数据库所使用的数据结构。为方便区分,“内层的哈希”表示redis对外提供的5种对象类型的一种,“外层的哈希”表示Redis作为Key-Value数据库使用的数据结构。
3.2 内部编码
内层的哈希使用的内部编码可以是压缩列表和哈希表两种,外层的哈希只使用了哈希表。
压缩列表相较于哈希表,适用于元素个数少,元素长度小的场景,优势在于集中存储,节省空间;当元素较少时,与哈希表相比没有明显劣势。
哈希表:一个哈希表由一个dict结构,两个dictht结构,一个dictEntry指针数组和多个dictEntry结构组成,可见下图:

dictEntry
dictEntry用于保存键值对,结构定义如下:
1 | typedef struct dictEntry{ |
- key:键。
- val:值,基于union实现,存储的内容可能是:一个指向值的指针,64位整型,无符号64位整型。
- next:指向的下一个dictEntry,主要用于解决哈希冲突。
在64位的操作系统中,一个dictEntry对象占24字节。
bucket
bucket是一个数组,存储这dictEntry结构的指针。bucket大小按照大于dictEntry的、最小的2^n的规则计算。
dictht
dictht结构如下:
1 | typedef struct dictht{ |
- table:指针,指向bucket。
- size:记录bucket的大小。
- sizemask:size-1。
- used:记录已使用的dictEntry数量。
dict
一般来说,使用dictht和dictEntry已经足够实现哈希表的功能,但在Redis实现中,dictht上层还存在dict结构,dict结构定义如下:
1 | typedef struct dict{ |
- type:dictType定义了一簇用于操作特定类型键值对的函数。
- privdata:
- ht[2]:字典的核心,包含两个dictht结构的数组。使用两个哈希表的目的是支持渐进式rehash。
- ht[0]:平时使用的哈希表。
- ht[1]:仅在rehash过程中使用。
- trehashidx:用于指示当前是否在进行rehash。-1表示没有进行rehash操作,非-1表示正在进行,其值表示下一个需要从ht[0]迁移到ht[1]的哈希表数组索引。
当ht[0]的负载因子超过某个阈值时,字典需要扩容以避免哈希冲突过于频繁,这时Redis会分配一个更大的ht[1](通常是ht[0]的两倍),之后将 rehashidx 设置为 0,表示开始 rehash。在后续的每次字典操作(如 dictAdd, dictFind, dictDelete 等)中,除了执行指定的操作外,还会顺带将 ht[0] 中 rehashidx 索引上的所有 dictEntry 迁移到 ht[1]。迁移完成后,rehashidx 自增。当所有 ht[0] 的数据都迁移到 ht[1] 后,ht[0] 和 ht[1] 互换角色,ht[1] 变为新的空哈希表,rehashidx 设为 -1,表示 rehash 结束。
3.3 编码转换
内层的哈希可使用压缩列表也可使用哈希表。
使用压缩列表需满足的条件如下:
- 哈希中元素数量小于512个;
- 哈希中所有键值对的键和值字符串长度都小于64字节。
任何一项不满足则使用哈希表,编码只可能从压缩列表转换为哈希表。
4. 集合
4.1 概况
集合(set)与列表类似,都是用来保存多个字符串,但集合与列表有两点不同:集合中的元素是无序的,因此不能通过索引来操作元素;集合中的元素不能重复。
一个集合中最多可以存储2^32-1个元素;除了支持常规的增删改查,Redis还支持多个集合取交集、并集、差集。
4.2 内部编码
集合的内部编码可以是整数集合(intset)或哈希表(hashtable)。哈希表前面已经提到,下面介绍整数集合。
整数集合的结构定义如下:
1 | typedef struct intset{ |
- encoding:记录contents数组中元素的编码方式,它决定了每个元素占用的字节数。Redis支持三种编码:
- INTSET_ENC_INT16
- 每个元素是一个 16 位有符号整数(int16_t)。
- 当集合中所有元素都在这个范围内时,使用此编码。
- INTSET_ENC_INT32
- 每个元素是一个 32 位有符号整数(int32_t)。
- 当集合中出现超出int16_t范围,但仍在int32_t范围内的元素时,会触发 “升级”,将所有元素都转换为int32_t编码。
- INTSET_ENC_INT64
- 每个元素是一个 64 位有符号整数(int64_t)。
- 当集合中出现超出 int32_t 范围的元素时,会再次触发升级,将所有元素转换为int64_t编码。
- INTSET_ENC_INT16
- length:表示元素个数。
- contents[]:实际存储的类型是int16_t、int32_t、int64_t,由encoding决定。
4.3 编码转换
使用整数集合需同时满足以下条件:
- 集合中元素数量小于512。
- 集合中所有元素都是整数值。
任何一个条件不满足则使用哈希表,只可能能从整数集合转换为哈希表,反方向则不可能。
5. 有序集合
5.1 概况
有序集合与集合一样,元素不能重复,但元素是有序的,其排序方式取决于每个元素的分数。
5.2 内部编码
有序集合的内部编码可以是压缩列表(ziplist)或跳跃表(skiplist)。ziplist前面已经提到过,这里主要介绍跳跃表。
跳跃表是一种有序数据结构,通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。除了跳跃表,实现有序数据结构的另一种典型实现是平衡树,但大多数情况下,跳跃表的效率可以和平衡树媲美,且跳跃表实现比平衡树简单很多,因此redis中选用跳跃表代替平衡树。跳跃表支持平均O(logN)、最坏O(N)的复杂点进行节点查找,并支持顺序操作。Redis的跳跃表实现由zskiplist和zskiplistNode两个结构组成:前者用于保存跳跃表信息(如头结点、尾节点、长度等),后者用于表示跳跃表节点。
5.3 编码转换
使用压缩列表需同时满足以下条件:
- 有序集合中元素数量小于128个。
- 有序集合中所有成员长度都不足64字节。
如果有一个条件不满足,则使用跳跃表,且编码只可能由压缩列表转化为跳跃表,反方向则不可能。