Redis最为常用的数据类型主要有以下五种:
- String 字符串
- Hash 哈希
- List 列表
- Set 集合
- Sorted set 有序集合
丰富的类型是 Redis 相对于 Memcached 等的一大优势
在了解上文的基础上,进一步了解Redis 的内存模型,对 Redis 的使用有很大帮助。如:
- 估算 Redis 内存使用量,选择合理的机器配置
- 优化内存占用,选择最合适的数据结构和编码
- 快速定位问题,当 Redis 出现阻塞、内存占用等问题时
本文介绍在redis3.0的版本下, Redis 占用内存的情况及如何查询、不同的对象类型在内存中的编码方式、内存分配器(jemalloc)、简单动态字符串(SDS)、RedisObject 等
通过 redis-cli 连接服务器 。使用 info memory
返回参数解释如下:
Redis 分配器分配的内存总量(单位是字节),包括使用的虚拟内存(即 swap)。而used_memory_human是友好显示。
used_memory_human = used_memory/1024/1024
Redis 进程占据操作系统的内存(单位是字节),与 top 及 ps 命令看到的值是一致的。包括进程运行本身需要的内存、内存碎片等,但不包括虚拟内存。
used_memory 和 used_memory_rss,前者是从 Redis 角度得到的量,后者是从操作系统角度得到的量。
二者之所以有所不同,一方面是因为内存碎片和 Redis 进程运行需要占用内存,使得前者可能比后者小,另一方面虚拟内存的存在,使得前者可能比后者大。
因此 used_memory_rss 和 used_memory 的比例,便成了衡量 Redis 内存碎片率的参数;这个参数就是 mem_fragmentation_ratio。
内存碎片比率,该值是 used_memory_rss / used_memory 的比值。
mem_fragmentation_ratio 一般大于 1,且该值越大,内存碎片比例越大;mem_fragmentation_ratio<1,说明 Redis 使用了虚拟内存,由于虚拟内存的媒介是磁盘,比内存速度要慢很多。
当小于1时,应该及时排查,如果内存不足应该及时处理,如增加 Redis 节点、增加 Redis 服务器的内存、优化应用等。
一般来说,mem_fragmentation_ratio 在 1.03 左右是比较健康的状态
Redis 使用的内存分配器,在编译时指定;可以是 libc 、jemalloc 或者 tcmalloc,默认是 jemalloc;截图中使用的便是默认的 jemalloc。
Redis作为一个内存数据库,内存只要划分为如下几个部分:
作为key-value被存储当数据,这部分占用的内存会统计在 used_memory 中。其中value 包括前文所述的5种类型。
这 5 种类型是 Redis 对外提供的,实际上,在 Redis 内部,每种类型可能有 2 种或更多的内部编码实现。
此外,Redis 在存储对象时,并不是直接将数据扔进内存,而是会对对象进行各种包装:如 RedisObject、SDS 等。
如代码、常量池、执行 AOF、RDB 重写时创建的子进程等等;这部分内存大约几兆,在生产环境中可忽略。这部分内存不是由 jemalloc 分配,也不会在 used_memory 中。
缓冲内存包括客户端缓冲区、复制积压缓冲区、AOF 缓冲区等;其中,客户端缓冲区存储客户端连接的输入输出缓冲;复制积压缓冲区用于部分复制功能;AOF 缓冲区用于在进行 AOF 重写时,保存最近的写入命令。
这部分内存由 jemalloc 分配,因此会统计在 used_memory 中。
内存碎片是 Redis 在分配、回收物理内存过程中产生的。
如果对数据的更改频繁,而且数据之间的大小相差很大,可能导致 Redis 释放的空间在物理内存中并没有释放。
但 Redis 又无法有效利用,这就形成了内存碎片,内存碎片不会统计在 used_memory 中。
内存碎片的产生与对数据进行的操作、数据的特点等都有关; jemalloc 内存分配器在控制内存碎片方面做的比较好。
如果 Redis 服务器中的内存碎片已经很大,可以通过安全重启的方式减小内存碎片:重启后,Redis 重新从备份文件中读取数据,在内存中进行重排,为每个数据重新选择合适的内存单元,减小内存碎片。
redis中的数据在底层存储,涉及到内存分配器(如 jemalloc)、简单动态字符串(SDS)、5 种对象类型及内部编码、RedisObject。
Redis 是 Key-Value 数据库,因此对每个键值对都会有一个 dictEntry,里面存储了指向 Key 和 Value 的指针;next 指向下一个 dictEntry,与本 Key-Value 无关。dictEntry是个链表结构。
key 并不是直接已string进行存储的,而是存储在SDS结构中的。
无论value是5中类型中的哪一种,都是存储在 RedisObject 中。而 RedisObject 中的 type 字段指明了 value 对象的类型,ptr 字段则指向对象所在的地址。
因此,字符串对象虽然经过了 RedisObject 的包装,但仍然需要通过 SDS 存储。
Redis 在编译时便会指定内存分配器。内存分配器可以是 libc 、jemalloc 或者 tcmalloc,默认是 jemalloc。
无论是 DictEntry 对象,还是 RedisObject、SDS 对象,都需要内存分配器分配内存进行存储。
jemalloc 作为 Redis 的默认内存分配器,在减小内存碎片方面做的相对比较好。
jemalloc 在 64 位系统中,将内存空间划分为小、大、巨大三个范围;每个范围内又划分了许多小的内存块单位;当 Redis 存储数据时,会选择大小最合适的内存块进行存储。
5种数据结构在存储的时候都是通过RedisObject来存储,RedisObject 对象非常重要,Redis 对象的类型、内部编码、内存回收、共享对象等功能,都需要 RedisObject 支持。
// FROM https://github.com/antirez/redis/blob/unstable/src/server.h
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru: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;
字段说明:
type 字段表示对象的类型,占 4 个比特;目前包括 REDIS_STRING(字符串)、REDIS_LIST (列表)、REDIS_HASH(哈希)、REDIS_SET(集合)、REDIS_ZSET(有序集合)。
当我们执行 type xxx 命令时,便是通过读取 RedisObject 的 type 字段获得对象的类型。
encoding 表示对象的内部编码,占 4 个比特。对于 Redis 支持的每种类型,都有至少两种内部编码,例如对于字符串,有 int、embstr、raw 三种编码。
Redis 可以根据不同的使用场景来为对象设置不同的编码,大大提高了 Redis 的灵活性和效率。
以列表对象为例,有压缩列表和双端链表两种编码方式;如果列表中的元素较少,Redis 倾向于使用压缩列表进行存储,因为压缩列表占用内存更少,而且比双端链表可以更快载入。
当列表对象元素较多时,压缩列表就会转化为更适合存储大量元素的双端链表。
通过 object encoding 命令,可以查看对象采用的编码方式
后续会列出Redis5 种对象类型对应的编码方式以及使用条件
lru 记录的是对象最后一次被命令程序访问的时间,占据的比特数不同的版本有所不同。通过对比 lru 时间与当前时间,可以计算某个对象的空转时间;
object idletime 命令可以显示该空转时间(单位是秒)。
object idletime 命令的一个特殊之处在于它不改变对象的 lru 值。
lru 值除了通过 object idletime 命令打印之外,还与 Redis 的内存回收有关系。
如果 Redis 打开了 maxmemory 选项,且内存回收算法选择的是 volatile-lru 或 allkeys—lru,那么当 Redis 内存占用超过 maxmemory 指定的值时,Redis 会优先选择空转时间最长的对象进行释放。
refcount 与共享对象:refcount 记录的是该对象被引用的次数,类型为整型。refcount 的作用,主要在于对象的引用计数和内存回收。
当创建新对象时,refcount 初始化为 1;当有新程序使用该对象时,refcount 加 1;当对象不再被一个新程序使用时,refcount 减 1;当 refcount 变为 0 时,对象占用的内存会被释放。
Redis 中被多次使用的对象(refcount>1),称为共享对象。这是为了节约内存。目前仅支持整数值的字符串对象。
Redis 的共享对象目前只支持整数值的字符串对象。之所以如此,实际上是对内存和 CPU(时间)的平衡:共享对象虽然会降低内存消耗,但是判断两个对象是否相等却需要消耗额外的时间。
对于整数值,判断操作复杂度为 O(1);对于普通字符串,判断复杂度为 O(n);而对于哈希、列表、集合和有序集合,判断的复杂度为 O(n^2)。
虽然共享对象只能是整数值的字符串对象,但是5种类型都可能使用共享对象(如哈希、列表等的元素可以使用)。
Redis在初始化时,会创建 10000 个字符串对象,值分别是 0~9999 的整数值;当 Redis 需要使用值为 0~9999 的字符串对象时,可以直接使用这些共享对象。
共享对象的引用次数可以通过 object refcount 命令查看
ptr 指针指向具体的数据
一个 RedisObject 对象的大小为 16 字节:4bit+4bit+24bit+4Byte+8Byte=16Byte。
SDS 是简单动态字符串(Simple Dynamic String)的缩写。redis中所有的key都是SDS
//FROM https://github.com/antirez/redis/blob/fc0c9c8097a5b2bc8728bec9cfee26817a702f09/src/sds.h
/* Note: sdshdr5 is never used, we just access the flags byte directly.
* However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
不难看出,sds中包含总共申请的内存空间(排除头和空终止符),字节数组和当前使用的字节数组的长度。
SDS获取字符串的复杂度为O(1),而C字符串是 O(n)。
Redis的五种value对象类型及其内部编码。下文介绍