Redis并没有直接使用SDS、双端链表、字典、压缩列表、整数集合等这些数据结构,而是基于这些数据结构创建了一个对象系统,这个对象系统包含字符串对象(String)、列表对象(List)、哈希对象(Hash)、集合对象(Set)和有序集合对象(SortedSet)这5种类型,每种对象都用到了至少一种数据结构。
这种设计的好处是将我们真实使用的对象提到了底层数据结构层级之上,让我们可以对同一个对象类型有不同的实现,然后根据不同的场景使用不同的数据结构实现,优化使用效率。
除此之外,Redis对象系统还实现了基于引用计数范式的内存回收机制(即每个对象还有一个引用计数字段,每当有其他对象引用便计数加1,当引用计数为0时,表示没有其他对象引用这个对象,即可以回收,这种访问有个明显缺点,即存在相互引用情况导致对象不能被回收),回收不再使用对象的内存;另外,Redis还通过引用计数方式实现了对象共享机制,这一机制可以在适当条件下,通过让多个数据库键共享同一个对象节约内存。
最后,Redis对象带有访问时间记录信息,可以用于计算数据库键的空转时长,在服务器启用了maxmemory功能并且回收内存的算法为volatile-lru或者allkeys-lru的情况下,当内存超过maxmemory设置的阀值,空转时长较大的那些键可能会优先被服务器回收。具体可以查看配置文件maxmemory和maxmemory-policy选项的说明介绍。
redis.h/redisObject源码
typedef struct redisObject { // 类型 unsigned type:4; // 编码 unsigned encoding:4; // 对象最后一次被访问的时间 unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */ // 引用计数 int refcount; // 指向实际值的指针 void *ptr;} robj;
- type:表示当前redis对象的类型,可以使用type命令查看某个key对应的值的redisObject的类型,包含5种可能值:
- REDIS_STRING 字符串对象,type命令输出为"string"
- REDIS_LIST 列表对象,type命令输出为"list"
- REDIS_HASH 哈希对象,type命令输出为"hash"
- REDIS_SET 集合对象,type命令输出为"set"
- REDIS_ZSET 有序集合对象,type命令输出为"zset"
- encoding:表示当前对象使用的数据结构类型,即ptr指针指向的对象对应的数据结构,可以使用obejct encoding <key>命令查出对应值的数据结构,可能值如下:
- REDIS_ENCODING_INT long类型的整数,object encoding命令返回值“int”
- REDIS_ENCODING_EMBSTR embstr编码的简单动态字符串,object encoding命令返回值“embstr”
- REDIS_ENCODING_RAW 简单动态字符串,object encoding命令返回值“raw”
- REDIS_ENCODING_HT 字典,object encoding命令返回值“hashtable”
- REDIS_ENCODING_LINKEDLIST 双端链表,object encoding命令返回值“linkedlist”
- REDIS_ENCODING_ZIPLIST 压缩链表,object encoding命令返回值“ziplist”
- REDIS_ENCODING_INTSET 整数集合,object encoding命令返回值“intset”
- REDIS_ENCODING_SKIPLIST 跳跃表,object encoding命令返回值“skiplist”
编码与类型对应关系如下:
- refcount:引用计数,当计数为0时,用于内存回收
- ptr:指向对象实际结构的指针
字符串对象
字符串对象编码可以是int、raw或者embstr,字符串对象是唯一会潜逃到其他数据结构中使用的结构,编码切换条件如下:
- 如果保存的是一个字符串值(不能转化成整数),且长度大于32字节,使用raw即SDS
- 如果保存的是一个字符串值(不能转化成整数),且长度小于等于32字节,使用embstr
- 如果保存的是一个整数,使用int即c语言的long类型
embstr与raw(SDS)结构完全一样,唯一的却别是如果使用raw(SDS)会进行两次内存分配,先分配创建SDS,然后分配创建redisObject,再使ptr指针指向SDS,即SDS和redisObject的内存地址可以不连续。而embstr的SDS结构和redisObject结构内存连续,只需要一次内存分配,embstr优势:
- 只需要一次内存分配
- 只需要一次内存回收
- 由于内存连续,更利于CPU高速缓存载入
字符串各个数据结构之间调用不同方法的转换:
列表对象
列表对象的编码可以是ziplist或者linkedlist,转换规则,当满足下面任意一条将使用linkedlist:
- 列表对象所存字符串元素长度有一个及以上大于等于64字节;
- 列表对象所存元素数量大于等于512;
上面两个条件的阀值可以通过配置文件list-max-ziplist-value和list-max-ziplist-entries两个属性进行修改。
ziplist和linkedlist方法实现差异:
哈希对象
哈希对象的编码可以是ziplist或者hashtable,ziplist在存键值对时总是两个相关联的键值对紧挨在一起,建在前值在后,每次都在表尾添加,即越后添加的键值对越靠近表尾。
编码切换条件,满足任意一个,将使用hashtable:
- 任意一个或以上键或者值长度大于等于64字节
- 键值对数量大于等于512
上限阀值可以通过配置中的hash-max-ziplist-value和hash-max-ziplist-entries属性修改。
不同数据结构方法实现差异:
集合对象
集合对象的编码可以是intset或者hashtable,切换条件如下,满足任一会使用hashtable:
- 集合所有元素有一个或以上不是整数值
- 集合元素数量大于等于512,可以通过配置set-max-intset-entries设置
方法实现差异:
有序集合对象
有序集合的编码可以是ziplist或者skiplist。ziplist编码也和集合对象使用一样,只是插入时会确保score的升序排列,有序列表的skiplist数据结构有些特殊,源代码如下:
/* * 有序集合 */typedef struct zset { // 字典,键为成员,值为分值 // 用于支持 O(1) 复杂度的按成员取分值操作 dict *dict; // 跳跃表,按分值排序成员 // 用于支持平均复杂度为 O(log N) 的按分值定位成员操作 // 以及范围操作 zskiplist *zsl;} zset;
同时使用zskiplist和dict,这样做是为了保证可以同时使用skiplist进行高效查找遍历,使用dict高效通过key获取score,由于使用了共享对象,所以内存并没有浪费。
编码转换条件,满足任一对象使用ziplist编码:
- 保存的元素数量大于等于128
- 保存的元素长度由一个或以上长度大于等于64字节
可以用配置中zset-max-ziplist-entries和zset-max-ziplist-value属性修改
方法实现区别: