- 前面几章介绍的都是Redis基于C优化的基础数据结构,这一章具体介绍五种对象类型的底层结构实现,并且在特定情况下结构会发生转化。
8.1 对象的类型与编码
- Redis每创建一次键值对时至少要生成两个对象,键对象和值对象,每个对象都是用redisObject结构表示。
typedef struct redisObject {
//类型
unsigned type:4;
//编码
unsigned encoding:4;
//指向底层实现数据结构的指针
void *ptr;
//...
} redisObject;
8.1.1 类型(TYPE)
- 键的类型:键总是一个字符串。
- 值的类型:值类型也就是最常见的5中基本类型,分别是字符串(REDIS_STRING)、列表(REDIS_LIST)、哈希(REDIS_HASH)、集合(REDIS_SET)、有序集合(REDIS_ZSET)。
- TYPE命令:TYPE + key可以查看这个对象的值类型。
8.1.2 编码和底层实现(encoding或缩写ENC)
- 编码一共有8种,对应的数据结构分别是:
- REDIS_ENCODING_INT:long类型整数
- REDIS_ENCODING_EMBSTR:embstr编码的简单动态字符串
- REDIS_ENCODING_RAW:简单动态字符串
- REDIS_ENCODING_HT:字典
- REDIS_ENCODING_LINKEDLIST:双端链表
- REDIS_ENCODING_ZIPLIST:压缩列表
- REDIS_ENCODING_INTSET:整数集合
- REDIS_ENCODING_SKIPLIST:跳跃表和字典
-
每种基本类型至少使用了两种不同的编码,不同场景下有不同的解决方案,例如Redis会把数字和字符都算做基本类型里的REDIS_STRING,但底层存储时又会有不同的编码,从而提升效率,下面列举所有基本类型可能会使用到的底层编码。
- OBJECT ENCODING命令:OBJECT ENCODING + key可以查看这个对象值的编码。
8.2 字符串对象
- 字符串编码可以是REDIS_ENCODING_INT、REDIS_ENCODING_RAW或者REDIS_ENCODING_EMBSTR(这章后续用缩写int、raw、embstr缩写表示)。
- 编码判定条件:
- 浮点或整形数字不超过32位,用int
- 超长数字、字符串并且小于等于39字节,用raw
- 超长数字、字符串并且大于39字节,用embstr
- raw和embstr的共同点和区别:
- raw和embstr都是使用SDS结构保存数据,但embstr对SDS做了一些优化。
- raw调用两次内存分配,分别创建了redisObject和sdshdr两个空间,embstr只调用一次,并且内存空间连续。
- 释放时raw也是调用两次释放,embstr调用一次。
- embstr本身就是短字符串时使用,内存连续所以效率更高。
- embstr是只读的,创建后如果需要修改,直接会转化成raw。
- 保存浮点类型时存的是embstr结构,执行一些数字操作时再转成浮点型。
-
浮点型和长整型在数据过长的情况下也会扩展成embstr甚至raw
8.2.1 编码的转换
- 之前提到了在不同条件下string的底层数据结构实现不同,int和embstr会升级成raw,最常见的场景就是APPEND命令。
- 例如初始化一个数字底层是int,拼接一个字符串后就变成了raw。
- embstr同理,但embstr创建后是只读的,任何的修改操作都会直接升级成raw,int和embstr之间不会互相升级。
8.2.1 字符串命令的实现
8.3 列表
- 列表对象编码可以是ziplist或linkedlist。
- ziplist和linkedlist的区别:
- ziplist地址连续,而且很少浪费空间,大概就能猜到肯定是节点数少时候会用到ziplist
-
linkedlist是节点间引用连接,每个节点又是一个独立的字符串对象,是上一个小结介绍的字符串对象,字符串对象是唯一被其他对象嵌套的对象。
8.3.1 编码的转换
- 所有元素长度小于64字节,并且元素数量小于512时用ziplist,否则使用linkedlist
8.3.2 列表指令的实现
8.4 哈希对象
- 哈希对象编码可以是ziplist或hashtable。
-
ziplist:使用ziplist时因为内存连续,每次添加节点时都会按key、value的顺序添加到列表尾部。
-
hashtable:使用字典实现,字典结构的Key和value都各自是一个字符串对象。
8.4.1 编码转换
- 所有键和值都小于64字节,并且节点数量小于512个时使用ziplist,否则使用hashtable。
8.4.1 哈希命令的实现
8.5 对象集合
- 对象集合编码可以是intset或hashtable。
-
intset:
-
hashtable:对象集合的hashtable所有的value都是Null
8.5.1 编码的转换
- 集合都是整数并且元素小于512时,是intset否则用hashtable
8.5.2 对象集合命令的实现
8.6 有序集合对象
- 有序集合对象编码可以是ziplist或skiplist。
-
ziplist:
- skiplist:当zset使用skiplist时,会同时包含一个字典和一个跳跃表。字典的key是数据,value是分值,跳跃表的object是数据,score是分值。两种数据结构通过指针共享数据和分值。
- 为什么同时使用跳跃表和字典:还是性能问题。
-
字典可以最快的找到对应数据,但zset还有一些范围操作命令,所以引入了跳跃表,只有跳跃表数据查找效率又会低,所以最后的解决方案就是同时引入跳跃表和字典。
-
8.6.1 编码的转换
- 数量小于128并且元素数量小于64字节用ziplist否则用skiplist。
8.6.2 有序集合命令的实现
8.7 类型检查与命令多态
- Redis操作类型分为两种:
- 可以对任何类型的键执行:DEL、EXPIRE、RENAME、TYPE等等
- 对特定类型使用:set、get是字符串,HSET、HGET是哈希等等
8.7.1 类型检查的实现
- 在执行特定类型的命令前,Redis会先检查redisObject结构的type属性是否符合要求,否则报错。
8.7.2 多态命令的实现
- 命令根据不同的底层数据结构实现,执行不同的操作。
8.8 内存回收
- Redis的回收使用引用计数的方式实现。
- 创建时计数+1
- 对象被新程序使用时+1
- 对象不被程序使用时-1
- 对象计数为0时释放内存
8.9 对象共享
- 对象共享和内存回收相关,共享的对象有新引用时计数+1。
- Redis初始化服务器时会创建0-9999的字符串共享变量供程序使用。
- Redis只对包含整数的字符串对象进行共享。
8.10 对象的空转时长
- redisObject属性除了上面提到的三个还有一个属性,lru记录了对象最后一次被访问的时间,OBJECT IDLETIME属性可以打印出来。
- 服务器可以开启maxmemory选项,空转时间较长的键会被释放回收。