关于Redis数据存储的细节,涉及到内存分配器(如jemalloc)、简单动态字符串(SDS)、5种对象类型及内部编码、redisObject
这里将说明这几个概念之间的关系。
下图是执行set hello world时,所涉及到的数据模型:
struct sdsstr{
// buf已使用的长度
int len;
// 示buf未使用的长度
int free;
// 字节数组,用来存储字符串
char buf[];
};
通过SDS的结构可以看出,buf数组的长度=free+len+1(其中1表示字符串结尾的空字符)
所以,一个SDS结构占据的空间为:free所占长度+len所占长度+ buf数组的长度=4+4+free+len+1=free+len+9
此外,由于SDS中的buf仍然使用了C字符串(即以’\0’结尾),因此SDS可以使用C字符串库中的部分函数;但是需要注意的是,只有当SDS用来存储文本数据时才可以这样使用,在存储二进制数据时则不行(’\0’不一定是结尾)
例如set hello world命令,hello和world都是以SDS的形式存储的。
而sadd myset member1 member2 member3命令,不论是键(”myset”),还是集合中的元素 (”member1”、 ”member2”和”member3”),都是以SDS的形式存储。 除了存储对象,SDS还用于存储各种缓冲区。
只有在字符串不会改变的情况下,如打印日志时,才会使用C字符串。
Redis在编译时便会指定内存分配器;内存分配器可以是 libc 、jemalloc或者tcmalloc,默认是 jemalloc。
jemalloc作为Redis的默认内存分配器,在减小内存碎片方面做的相对比较好。jemalloc在64位系统中,将内存空间划分为小、大、巨大三个范围;每个范围内又划分了许多小的内存块单位;当Redis存储数据时,会选择大小最合适的内存块进行存储。
例如,如果需要存储大小为130
这里是引用字节的对象,jemalloc会将其放入160字节的内存单元中
Redis对象有
5种类型;无论是哪种类型,Redis都不会直接存储,而是通过redisObject对象进行存储。
redisObject对象非常重要,Redis对象的类型、内部编码、内存回收、共享对象等功能,都需要redisObject支持,下面将通过redisObject的结构来说明它是如何起作用的。
redisObject的定义如下(列出了与保存数据有关的三个属性):
typedef struct redisObject{
unsigned type: 4;
unsigned encoding: 4;
unsigned lru: REDIS_LRU_BITS;
int refcount;
void *ptr;
} robj
127.0.0.1:6379> set test hello_redis
OK
127.0.0.1:6379> type test
string
127.0.0.1:6379> sadd myset member1 member2 member3
(integer) 3
127.0.0.1:6379> type myset
set
以列表对象为例,有压缩列表和双端链表两种编码方式;如果列表中的元素较少,Redis倾向于使用压缩列表进行存储,因为压缩列表占用内存更少,而且比双端链表可以更快载入;当列表对象元素较多时,压缩列表就会转化为更适合存储大量元素的双端链表。
通过object encoding命令,可以查看对象采用的编码方式:
127.0.0.1:6379> set key1 123
OK
127.0.0.1:6379> object encoding key1
"int"
127.0.0.1:6379> set key1 helloredis
OK
127.0.0.1:6379> object encoding key1
"embstr"
127.0.0.1:6379> set key1 helloredis
OK
127.0.0.1:6379> object idletime key1
(integer) 13
127.0.0.1:6379> object idletime key1
(integer) 37
127.0.0.1:6379> object idletime key1
(integer) 40
127.0.0.1:6379> object idletime key1
(integer) 43
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为了节省内存,当有一些对象重复出现时,新的程序不会创建新的对象,而是仍然使用原来的对象。这个被重复使用的对象,就是共享对象。目前共享对象仅支持整数值的字符串对象。
共享对象的具体实现
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命令查看,如下图所示。命令执行的结果页佐证了只有0~9999之间的整数会作为共享对象。
127.0.0.1:6379> set k1 9999
OK
127.0.0.1:6379> set k2 9999
OK
127.0.0.1:6379> set k3 9999
OK
127.0.0.1:6379> object refcount k1
(integer) 2147483647
127.0.0.1:6379> set k1 10000
OK
127.0.0.1:6379> set k2 10000
OK
127.0.0.1:6379> set k3 10000
OK
127.0.0.1:6379> object refcount k1
(integer) 1
127.0.0.1:6379> set k1 hello
OK
127.0.0.1:6379> set k2 hello
OK
127.0.0.1:6379> set k3 hello
OK
127.0.0.1:6379> object refcount k1
(integer) 1
ptr指针指向具体的数据,如前面的例子中,set hello world,ptr指向包含字符串world的SDS。
综上所述,redisObject的结构与对象类型、编码、内存回收、共享对象都有关系;一个redisObject对象的大小为16字节:
4bit+4bit+24bit+4Byte+8Byte=16Byte