作者:lunnzhang,腾讯 CDG 后台开发工程师。
2007 年,他和朋友一起创建了一个网站。为了解决这个网站的负载问题,他自己定制了一个数据库。2009 年开发的,这个是 Redis。这位意大利程序员是萨尔瓦托勒·桑菲利波(Salvatore Sanfilippo),他被称为Redis之父,更广为人知的名字是Antirez。
一、Redis简介
REmote DIctionary Server(Redis) 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存、分布式、可选持久性的键值对(Key-Value)存储数据库,并提供多种语言的 API。
Redis 通常被称为数据结构服务器,因为值(value)可以是字符串(String)、哈希(Hash)、列表(list)、集合(sets)和有序集合(sorted sets)等类型。
二、内存模型
首先可以进行Redis的内存模型学习,对Redis的使用有很大帮助,例如OOM时定位、内存使用量评估等。
通过info memory命令查看内存的使用情况。
主要参数:
used_memory:从Redis角度使用了多少内存,即Redis分配器分配的内存总量(单位是字节),包括使用的虚拟内存(即swap);
used_memory_rss:从操作系统角度实际使用量,即Redis进程占据操作系统的内存(单位是字节),包括进程运行本身需要的内存、内存碎片等,不包括虚拟内存。一般情况下used_memory_rss都要比used_memory大,因为Redis频繁删除读写等操作使得内存碎片较多,而虚拟内存的使用一般是非极端情况下是不怎么使用的;
mem_fragmentation_ratio:即内存碎片比率,该值是used_memory_rss / used_memory的比值;mem_fragmentation_ratio一般大于1,且该值越大,内存碎片比例越大。如果mem_fragmentation_ratio<1,说明Redis使用了虚拟内存,由于虚拟内存的媒介是磁盘,比内存速度要慢很多,当这种情况出现时,应该及时排查,如果内存不足应该及时处理,如增加Redis节点、增加Redis服务器的内存、优化应用等。一般来说,mem_fragmentation_ratio在1.03左右是比较健康的状态(对于jemalloc来说);
mem_allocator:即Redis使用的内存分配器,一般默认是jemalloc。
Redis内存划分
数据
作为数据库,数据是最主要的部分,这部分占用的内存会统计在used_memory中。
进程本身运行需要的内存
这部分内存不是由jemalloc分配,因此不会统计在used_memory中。
缓冲内存
缓冲内存包括:
客户端缓冲区:存储客户端连接的输入输出缓冲;
复制积压缓冲区:用于部分复制功能;
aof_buf:用于在进行AOF重写时,保存最近的写入命令。
这部分内存由jemalloc分配,因此会统计在used_memory中。
内存碎片
内存碎片是Redis在数据更改频繁分配、回收物理内存过程中产生的。
内存碎片不会统计在used_memory中。
Redis数据存储的细节
下面看一张经典的图。
jemalloc
无论是DictEntry对象,还是RedisObject、SDS对象,都需要内存分配器(如jemalloc)分配内存进行存储。Redis在编译时便会指定内存分配器;内存分配器可以是 libc 、jemalloc或者tcmalloc,默认是jemalloc。当Redis存储数据时,会选择大小最合适的内存块进行存储。
jemalloc划分的内存单元如下图所示:
dictEntry
每个dictEntry都保存着一个键值对,key值保存一个sds结构体,value值保存一个redisObject结构体。
redisObject
前面说到,Redis对象有5种类型;无论是哪种类型,Redis都不会直接存储,而是通过RedisObject对象进行存储。
RedisObject的每个字段的含义和作用如下:
type
type字段表示对象的类型,占4个比特;目前包括REDIS_STRING(字符串)、REDIS_LIST (列表)、REDIS_HASH(哈希)、REDIS_SET(集合)、REDIS_ZSET(有序集合)。
encoding
encoding表示对象的内部编码,占4个比特(redis-3.0)。
lru
lru记录的是对象最后一次被命令程序访问的时间,占据的比特数不同的版本有所不同(如4.0版本占24比特,2.6版本占22比特)。
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)的值进行改变。
ptr
ptr指针指向具体的数据,如前面的例子中,set hello world,ptr指向包含字符串world的SDS。
SDS
Redis没有直接使用C字符串(即以空字符‘\0’结尾的字符数组)作为默认的字符串表示,而是使用了SDS。SDS是简单动态字符串(Simple Dynamic String)的缩写。