redisObject结构
实际上每一个redis都是一个redisObject对象。redis对象的类型检查,内存回收,对象共享都是基于redisObject完成的,下面来看一下redisObject的结构。
typedef struct redisObject { // 类型 unsigned type:4; // 对齐位 unsigned notused:2; // 编码方式 unsigned encoding:4; // LRU 时间(相对于 server.lruclock) unsigned lru:22; // 引用计数 int refcount; // 指向对象的值 void *ptr;} robj;复制代码
- type:对象类型。
- encoding: 对象编码方式。
- lru: 对象的最近使用时间,配合内存回收使用。
- refcount: 引用次数。配合内存回收使用。
- ptr: 指向具体底层数据结构的指针。
redisObject是这样的结构主要是为了方便redis灵活的切换对象的编码及实时的回收redis内失效的内存,防止内存泄漏。
五大类型对象
redis的五大数据对象分别是字符串、列表、集合、有序集合、哈希表,下面来分别介绍。
字符串
字符串对象一共有三种实现——整数、SDS(简单动态字符串)和embstr编码的SDS。 当字符串的长度小于44时,会使用embstr编码的SDS,大于44时会使用SDS。这里主要有两个问题,一是什么是embstr编码,二是为什么偏偏要选择44为临界点。 <1> embstr编码 首先需要了解redisObject对象,如下图所示,一般的string的redisObject在内存中是以这样的形式存在的,需要分配两块空间,且要分配两次。
而embstr编码在内存中的分布是这样的,直接和redisObject的头部连在一起,只有一块空间,且只用分配一次内存。所以相较于分开两块内存保存,embstr编码更能够发挥缓存的优势。<2> 44字节 从2.4版本开始,redis开始使用jemalloc内存分配器,jemalloc会分配8,16,32,64等字节的内存。embstr由redisObject和sds组成,其中redisObject有16个字节,如果embstr有44个字节,则sds的长度为44+3+1=48,加起来刚好为64字节。
列表
当列表对象可以同时满足以下两个条件时, 列表对象使用 ziplist 编码:
- 列表对象保存的所有字符串元素的长度都小于 64 字节;
- 列表对象保存的元素数量小于 512 个;
当这两个条件中的一个不满足时,将使用双端列表,而且这种变化是动态的。
哈希表
当哈希表对象满足ziplist编码的条件时,会使用ziplist作为底层数据结构,当不满足条件时,会使用字典作为底层数据结构。
集合
当集合对象可以同时满足以下两个条件时, 对象使用 intset 编码:
- 集合对象保存的所有元素都是整数值;
- 集合对象保存的元素数量不超过 512 个; 当这两个条件中的一个不满足时,将使用hashtable,而且这种变化是动态的。
有序集合
当集合对象可以同时满足以下两个条件时, 对象使用 ziplist编码:
- 列表对象保存的所有字符串元素的长度都小于 64 字节;
- 列表对象保存的元素数量小于 512 个; 当这两个条件中的一个不满足时,将使用跳跃表和hashtable两种数据结构。 为什么要将数据多冗余出一份呢?其实单凭跳跃表或哈希表中的一个数据结构都可以完成有序集合的需求,但是只有使用哈希表时才能以O(1)的复杂度查找成员的分值,但是在使用zrange等范围性操作时,相比于使用跳跃表,会使得复杂度从O(N),上升到O(NLogN),而光使用跳跃表时,原本的以O(1)的复杂度查找成员的分值,会变成O(N),为了这两种类型的操作都能有良好的表现,redis使用了两种数据结构来实现有序集合。
类型检查和命令多态
redis会对命令进行检查,以确保命令被正确的运用到正确的对象之上,当命令和对象不匹配时,会向客户端返回一个错误,例如当要对一个字符串执行自增操作时,就会返回一个错误。下表显示了这种匹配关系。
SET 、 GET 、 APPEND 、 STRLEN 等命令只能对字符串键执行; HDEL 、 HSET 、 HGET 、 HLEN 等命令只能对哈希键执行; RPUSH 、 LPOP 、 LINSERT 、 LLEN 等命令只能对列表键执行; SADD 、 SPOP 、 SINTER 、 SCARD 等命令只能对集合键执行; ZADD 、 ZCARD 、 ZRANK 、 ZSCORE 等命令只能对有序集合键执行;
上面说过,redis的对象都是redisObject,类型检查就是通过redisObject的type属性完成的。同时依赖于type属性,redis还可以实现命令的多态,即相同的命令作用的不同的对象上,执行不同的操作。例如当对整数型字符串执行append操作时,redis会首先将值转化为sds,之后执行append操作,而对sds型的字符串执行append操作时,则直接执行append操作。
内存回收
redis采用了引用计数法来回收内存,在redisObject结构中有一个refCount属性便是为此服务,当创建对象时,refCount置为1,当对象被引用则refCount+1,不被引用则refCount-1,当refCount为0时,对象所占用的内存就会被回收。
内存共享
redis 会在初始化服务器时, 创建一万个字符串对象, 这些对象包含了从 0 到 9999 的所有整数值, 当服务器需要用到值为 0到 9999 的字符串对象时, 服务器就会使用这些共享对象, 而不是新创建对象。
对象空转时间
redisObject的lru属性记录了对象最后一次被命令程序访问的时间,此属性主要是为了配合redis的回收内存算法,例如volatile-lru 或者 allkeys-lru,当内存超过设置的maxmemory时,会配合回收算法计算要被回收的内存。