前面我们看了Redis用到的主要数据结构,如简单动态字符串(SDS)、双向链表、字典、压缩列表、整数集合等。
但是Redis并没有直接使用这些数据结构来实现键值对,而是基于这些数据结构创建了一个对象系统,这个系统包括字符串对象、列表对象、哈希对象、集合对象、有序集合对象,除此之外,redis的对象系统还实现了基于计数技术的内存回收机制,另外redis还通过引用计数技术实现了对象共享机制(适当条件下,多个数据库键共享同一个对象来节约内存)。
最后,redis的对象带有访问时间记录信息,该信息可以用于计算数据库键的空转时长,在服务器启用了maxmemory功能的情况下,空转时长较大的键会优先被服务器删除。
1、Redis中的每个结构都是由redisObject结构标识,包含ptr(指向底层实现的数据结构)、encoding(决定用那种底层数据结构)、type等属性。
2、当我们创建一个键值对时,我们至少会创建两个对象:键对象(字符串对象),值对象(物种类型)。
3、字符串对象的编码可以是整数、raw或者enbstr、sds
- enbstr(短字符串长度小于32)调用一次分配内存函数,分配一个连续的内存包含redisObject结构与sdshdr结构,不包含修改命令,执行任何修改命令会转为raw对象。
- sds(字符串长度超过32)。
- raw 会调用两次内存分配分别创建redisObject结构与sdshdr结构。
4、列表对象,列表对象的编码可以是ziplist或者linkedlist
- ziplist使用压缩列表作为底层实现。列表对象保存的所有字符串元素长度都小于64字节,元素数量小于512个时使用压缩列表做为底层实现。
- linkedlist编码列表对象使用双向链表作为底层实现,每个双向链表节点都保存一个字符串对象。
5、哈希对象
- 哈希对象的编码可以是ziplist或者hashtable
- ziplist编码的哈希对象使用压缩表作为底层实现,当由新的键值对加入到hash对象时,程序会先将保存了键的压缩列表节点推入到压缩列表的表尾,然后将保存了值的压缩列表节点推入到压缩列表的表尾。
- 使用hashtable作为编码的哈希对象使用字典作为底层实现,键使用字符串对象,值使用字符串对象。
6、集合对象
- 集合对象的编码可以是 intset或者是hashtable
- intset编码的集合对象使用整数集合作为底层实现。
- hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含一个集合元素,而字段的值则全部设置为null。
- 对象转换 。intset转hashtable条件:元素中不全是整数或者元素数量超过512.
7、有序集合对象
- 有序集合的编码可以是ziplist 或者skiplist
- ziplist编码的压缩列表对象使用压缩列表作为底层实现,每个集合元素使用两个挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(member)第二个元素则保存元素的分值(score)。
- skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表。
typedef struct zset{
zskiplist *zsl;
dict *dict;//保存从成员到分值的映射,键保存元素成员 值保存了分数。
}
- 解释下为什么同时使用字典与跳跃表来实现有序集合:虽然用两种结构的任意一种都能实现有序集合,但是当我们只是用字典来实现有序集合时,由于字典是一个无序的保存元素,当我们实行范围操作时,需要先对所有的元素进行排序,这里所使用的时间复杂度至少为O(NlogN),并且有额外的内存消费;另外如果只使用跳跃表来实现有序集合时,虽然范围操作的优势被保留,但是没有了字典根据成员查找分值时这一操作的复杂度将从O(1)提升到O(logN)。
- 编码转换,当元素的数量小于128,且每个元素的长度都小于64字节时,使用ziplist。
8、内存回收
- 每个对象的引用技术信息由redisObject结构的refcount属性记录。
- 创建对象时,计数值会被默认初始化为1,被程序使用时,计数器加一,不再被使用时,计数器减一,当计数器为0时,对象占用的内存会被释放。
9、对象空转时长
- 对象空转时长使用redisObjet结构中的lru属性记录,该属性记录对象最后一次被访问的时间。