前言:Reids并没有直接使用这些数据结构来实现数据库的键值对,而是基于这些数据结构创建了一个对象系统,这个对象系统包括字符串对象、列表对象、哈希对象、集合对象、有序集合对象,每种对象起码用到了两种上述的数据结构。这样的好处是:
1、根据不同的对象类型,可以在redis命令执行之前判断这个对象是否可以执行相关的命令。
2、针对不同的使用场景为对象设置不同的数据结构实现,从而优化对象在不同使用场景下的效率,可以兼顾内存和时间复杂度。
一、对象的实现
redis使用对象来表示数据库中的键值对,当创建一个键值对的时候,一定会创建两个对象,键一定是字符串对象,值会是五种对象的其中之一。
struct redisObject {
unsigned type:4; // 类型
unsigned encoding:4; // 编码
void *ptr; //执行底层实现数据结构的指针
int refcount; //引用计数,用于内存回收
unsigned lru:22; // 记录最近一次访问这个对象的时间
}
1.1、 type类型
type属性记录了对象的类型,可以使用TYPE命令返回数据库键的类型(ps:如果键值对中值是列表对象,一般称之为列表键,TYPE命令返回list)。
type属性包括:
1.2 、encoding编码
encoding属性记录了对象使用了怎样的编码,也就是说这个对象使用了什么数据结构作为对象的底层实现。使用OBJECT ENCODING命令可返回这个对象的编码方式。
encoding属性包括:
#define REDIS_ENCODING_INT // 编码为long类型整数
#define REDIS_ENCODING_EMBSTR // 编码为embstr编码的SDS
#define REDIS_ENCODING_RAW // 编码为SDS
#define REDIS_ENCODING_HT // 编码为字典
#define REDIS_ENCODING_LINKEDLIST // 编码为双端链表
#define REDIS_ENCODING_ZIPLIST // 编码为压缩列表
#define REDIS_ENCODING_INTSET // 编码为整数集合
#define REDIS_ENCODING_SKIPLIST // 编码为跳跃表和字典
通过encoding属性来设定对象所使用的编码,而不是为特定类型的对象关联只一种编码,极大的提高了redis的灵活性和效率,因为redis可以根据不同的使用场景来为对象设置不同的编码,从而优化对象在某一场景下的使用效率。比如:
redis使用压缩列表作为列表键的底层实现之一。
1、压缩列表比双端链表更节约内存,在元素数量较少的时候,在内存中一连续内存块方式保存的压缩列表会比双端链表更快被载入到内存中。
2、随着列表对象包含的数量越来越多,压缩列表的优势越来越低,从而将列表对象的实现从压缩列表变为功能更强、更适合保存大量元素的双端链表。
1.3、type的不同编码
二、字符串对象
字符串对象的编码可以是int、raw或者embstr。
2.1、int
如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面(将void*转换成long),并将字符串对象的编码设置为int 。
相对于SDS优势在于 :1、节省内存 ;2、对于整数值的字符串对象可能会被执行INCR操作,SDS需要先将字符串转成整形,再执行加减操作,再将结果转成字符串保存,如果底层保存一个整形变量就不需要做类型转换了。
2.2、embstr
如果字符串对象保存的是一个字符串值, 并且这个字符串值的长度小于等于39字节, 那么字符串对象将使用embstr编码的方式来保存这个字符串值
2.3、SDS
如果字符串对象保存的是一个字符串值, 并且这个字符串值的长度大于39字节, 那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值, 并将对象的编码设置为raw。
2.4、embstr与sdshdr区别
embstr编码是专门用于保存短字符串的一种优化编码方式, 这种编码和raw编码一样, 都使用redisObject结构和sdshdr结构来表示字符串对象。
raw编码会调用两次内存分配函数来分别创建redisObject结构和sdshdr结构, 而embstr编码则通过调用一次内存分配函数来分配一块连续的空间, 空间中依次包含redisObject和sdshdr两个结构、
redisObject | SDS 连续内存块
embstr 有以下好处:
1、embstr 编码将创建字符串对象所需的内存分配次数从 raw 编码的两次降低为一次
2、释放 embstr 编码的字符串对象只需要调用一次内存释放函数, 而释放 raw 编码的字符串对象需要调用两次内存释放函数
因为 embstr 编码的字符串对象的所有数据都保存在一块连续的内存里面, 所以这种编码的字符串对象比起 raw 编码的字符串对象能够更好地利用缓存带来的优势
番位:为什么是39?
三、列表对象
列表的编码encoding可以是压缩列表或者双向链表。
3.1、编码转换
当同时满足以下两个条件的时候,列表对象使用压缩列表来编码,否则使用双端链表。
1、所有字符串对象的元素的长度都小于64字节;
2、元素数量不超过512个。
ps:其实可以通过配置文件修改。list-max-ziplist-value、list-max-ziplist-entries。
四、哈希对象
哈希对象可以使用压缩列表和字典来实现。
4.1、压缩列表的实现
每当有新的键值对加入到哈希对象的时候,程序会先将键的压缩列表节点推入到压缩列表的表尾,然后再将值的压缩列表节点保存到要锁列表的表尾。所以:
1、同一个键值对一定是紧靠在一起的;
2、先添加到哈希对象的键值对会放在表头位置,后来添加的会被放在表尾位置。
4.2、字典的实现
另外,使用字典的时候,键值对的键和值都是字符串对象。说明,redis的哈希对象只会使用字符串,只保存字符串。
4.3、编码转换
当同时满足以下两个条件的时候,哈希对象使用压缩列表来编码,否则使用字典。
1、所有键值对的键和值的字符串长度都小于64字节;
2、键值对数量不超过512个。
ps:其实可以通过配置文件修改。hash-max-ziplist-value、hash-max-ziplist-entries。
五、集合对象
集合对象的编码可以是整数集合,也可以是字典。
当使用字典的时候,字典的每个键是一个字符串对象(这个字符串对象就是一个集合元素),字典的值被设置为null。
5.1、编码转换
当同时满足以下两个条件的时候,集合对象使用整数集合来编码,否则使用字典。
1、所有元素都是整数值;
2、元素数量不超过512个。
ps:第二个选项其实是可以修改的,set-max-intset-entries。
六、有序集合
有序集合的实现有两种,一种是压缩列表,一种是字典+跳跃表。
6.1、压缩列表
使用压缩列表的时候,每个集合元素都使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(member),第二个节点保存元素的分值(score)。
压缩列表的集合元素按分值的大小从小到大排列。
6.2、skipList实现
skipList编码的有序集合使用zset作为底层实现,一个zset包括一个字典和一个跳跃表。
struct zset {
zskiplist *zsl;
dict *dict;
}
zset中的zsl跳跃表,按分值的从小到大保存了所有的集合元素,每个跳跃表节点都保存了一个集合元素。通过跳跃表ZRANGE、ZRANK都是基于跳跃表API实现的。
zset中的dict字典为有序集合创建了,一个从成员到分值的映射,字典的每个键值对都保存了一个集合元素,字典的键是member,字典的值是score。通过字典,程序可以O(1)查找元素的分值,ZSCORE就是基于这个实现的。
另外,有序集合的每一个元素的member都是字符串对象,每一个元素的score都是一个double类型的浮点数。
值得一提的是,这些字符串对象和浮点数,字典和跳跃表都会通过指针来共享,不会因此而浪费内存。
番外:为什么需要同时使用字典和跳跃表?
原因:按道理来说,只要使用一种数据结构就足以了,但是redis使用了两种。
因为如果仅使用字典,字典是无序保存集合元素的,那么ZRANGE等操作,就要对所有元素进行排序,时间复杂度O(NlogN),额外的空间复杂度O(N),因为要创建一个额外的数组来保存排序好的元素。
如果仅使用跳跃表,ZSCORE就从O(1)上升到跳跃表的查询时间复杂度O(logN)了。
为了尽量快速执行,redis同时使用了这两种数据结构。
6.3、编码转换
当同时满足以下两个条件的时候,有序集合对象使用压缩列表来编码,否则使用skiplist。
1、所有元素的成员member的字符串长度都小于64字节;
2、元素数量不超过128个。
ps:其实可以通过配置文件修改。zset-max-ziplist-value、zset-max-ziplist-entries。
七、类型检查和命令多态
redis的命令大致分为两种,一种是可以对任何类型的键执行,一种只能对指定类型的键执行。
任意类型的键可执行的命令包括DEL、EXPIRE等等。
而有些命令只能对指令类型的键使用,比如SET、HSET、RPUSH、SADD、ZADD。
7.1、类型检查
执行命令之前,redis会先查看这个对象type,然后决定是否执行,如果不能执行则返回一个错误
7.2、多态命令
redis还会根据对象的encoding,选择正确的命令实现来执行命令。
比如列表对象的实现有压缩列表和双端链表。redis会根据encoding选择不同的函数来实现指定的命令。
7.3、举例
八、内存回收
C语言并没有自动内存回收功能,所以redis是使用引用计数的方法构建自己的内存回收机制。对应的字段是refcount。
OBJECT REFCOUNT (键) 可以返回这个就这个键的值对象,这个值对象一共被引用了多少次。
九、内存共享
refcount除了可以用于内存回收,还用于内存共享。
在redis中,为了节约内存,可能会共享一些对象。包括两个步骤:
1、数据库见的值指针都指向同一个对象
2、这个对象的计数器加1
十、对象的空转时常
lru属性记录了最近一次被命令程序访问的时间,是个时间戳。
OBJECT IDLETIME可以打印出这个键的空转时常,就是当前时间减去键的值对象的lru时间计算出来的。
lru和redis的内存淘汰机制关联很大,allkeys-lru策略等等。