Redis设计与实现——对象(二刷)
对象介绍
Redis 底层的基本数据类型包括动态字符串、链表、字典、跳表、整数集合、压缩列表。但是 Redis并没有直接使用这些基本数据类型来构建键值对数据库,而是基于这些数据类型创建了一个对象系统,对象系统包含字符串对象、列表对象、哈希对象、集合对象、有序集合对象。
struct redisObject {
unsigned type:4; //该对象类型
unsigned encoding:4; //对象编码,表示对象的底层结构
unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */
//对象最后一次被命令访问的时间
int refcount; //引用计数,c语言不具备自动内存回收功能,用于在引用计数为0时清除对象
void *ptr; //底层实现数据结构的指针
};
- encoding:底层的具体数据结构类型的实现:
每种类型的对象至少使用了两种不同的编码(底层实现),下表列出了每种对象可以使用的编码:
- type:对象类型:
通过 encoding
属性来设定对象使用的编码,而不是使用一种固定的编码,能够在不同的场景下切换 Redis 对象的编码结构,从而进一步提高 Redis 的灵活性和效率。举个例子,在列表对象包含的元素较少时, Redis 使用压缩列表作为列表对象的底层实现:
- 压缩列表比双端链表更加节约内存,并且在元素较少时能够比双端链表更快地载入到缓存中;
- 随着存储的元素越来越多,压缩列表在保存元素时增删改不方便,因为使用功能更强大的双端链表来实现。
字符串对象
各种编码使用场景
Redis 中字符串对象使用三种编码:int、raw、embstr,各种编码有对应的使用时机如下:
int
:如果字符串对象使用的是一个整数值,并且这个整数值可以用 long 来表示,那么使用int
作为encoding
;raw
:如果字符串对象使用的是大于32字节的字符串值,那么使用 SDS 来保存该值,并将编码设置为raw
;embstr
:如果字符串使用的是小于32字节的字符串值,那么使用embstr
编码来保存字符串值。embstr
编码也使用redisObject
、sdshdr
结构来表示字符串对象,但是区别于raw
编码,raw
编码通过两次内存分配函数来分别创建redisObject
、sdshdr
结构;而embstr
编码通过一次内存分配来创建两个对象。相应的在释放内存时也只需要一次。
各种编码间的转换
- 通过
Append
命令向保存整数字符串的对象添加了一个字符串值,编码会从int
转换为raw
; embstr
编码对象是只读的,如果对embstr
对象修改,回首先转换为raw
编码,然后再执行修改命令;
列表对象
编码使用场景
Redis 中列表对象使用 ziplist
、linkedlist
两种编码,各种编码的使用时机如下:
ziplist
:当满足以下两个条件时使用压缩列表: (1)列表对象保存的所有字符串元素的长度都小于64字节;(2)列表对象保存的元素个数小于512个;linkedlist
:每个双端链表的节点都保存一个字符串对象,而每个字符串对象都保存一个列表元素。其中字符串对象可以表示为如下值:
两种编码格式示意图:
编码转换
当列表元素同时满足以下条件,使用压缩列表编码:
- (1)列表对象保存的所有字符串元素的长度都小于64字节;可以通过修改
list-max-ziplist-value
配置文件选项来修改; - (2)列表对象保存的元素个数小于512个;可以通过修改
list-max-ziplist-entries
配置文件选项来修改;
哈希对象
编码使用场景
Redis 中哈希对象可以使用 ziplist
、hashtable
两种编码格式,编码使用时机如下:
ziplist
:每当有新的键值对要加入到哈希对象时,会先保存键的压缩列表节点推入到压缩列表表尾,然后再将保存了值的压缩列表节点推入到压缩列表表尾。因此键和值的压缩列表节点总使挨在一起;hashtable
:哈希对象中每个键值对都使用一个字典键值对来保存,其中字典中的每个键和值都是一个字符串对象。
编码转换
当哈希表对象同时满足以下两个条件时,哈希表使用 ziplist
编码:
- 哈希对象保存的键值对的键和值的字符串长度都小于 64字节;可修改配置文件
hash-max-ziplist-value
- 哈希对象保存的键值对数量小于 512个。可修改配置文件
hash-max-ziplist-entries
集合对象
编码使用场景
Redis 中集合对象的编码可以是 intset
、hashtable
两种格式:
intset
:整数集合,每个对象都是一个整数;hashtable
:底层用字典来实现,每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而字典的值则全部被设置为 NULL;
编码转换
当集合对象同时满足以下两个条件时,使用 intset
编码:
- 集合对象保存的元素都是整数值;
- 集合对象保存的元素数量不超过512个;
有序集合对象
编码使用场景
有序集合对象的编码可以是 ziplist
、skiplist
两种:
ziplist
:编码底层使用压缩列表实现,每个元素使用两个紧挨在一起的压缩列表节点实现,第一个节点保存元素的成员,第二个元素则保存元素的分值;skiplist
:编码底层使用zset
数据结构作为底层实现,一个zset
结构同时使用字典和调表两种数据结构;跳表按分值从小到大保存了所有集合元素,每个跳表节点都保存了一个集合元素,跳表节点的 object 属性保存了元素的成员,跳表节点的 score 属性保存了分值,通过跳表可以很方便地实现ZRANK
、ZRANGE
命令;字典为有序集合创建了从成员到分值的映射,字典键保存了元素成员,字典值保存了元素分值,ZSCORE
命令就可以以 O(1) 的复杂度来查找给定成员的分值。- 有序集合中每个元素成员都是一个字符串对象,而每个元素的分值都是一个
double
类型的浮点数,虽然 zset结构同时使用跳表和字典来保存有序集合元素,但是这两种数据结构都会通过指针来共享相同元素的成员和分值,所以同时使用跳表和字典来保存集合元素不会产生任何重复成员或分值,不会消耗额外的内存。
Redis 多态命令的实现
Redis 除了会根据值对象来判断键是否能够执行指定命令外,还会根据值对象的编码方式,选择正确的命令实现代码来执行命令。比如列表有 ziplist
、linkedList
两种编码方式,如果执行 LLEN
命令,服务器除了确保执行命令的是列表键之外,还需要根据键的值对象所使用的编码来选择正确的 LLEN
命令实现。
Redis 对象共享
Redis 的对象共享机制有点类似于 C++的智能指针,但是 Redis 只会共享对象是整数值的字符串对象。因为服务器在考虑将一个共享对象设置为键的值对象时,程序需要检查给定的共享对象和键想创建的目标对象是否完全相同,只有在完全相同的情况下才会将共享对象用作键的值对象。在验证共享对象时应尽可能减少时间消耗:
(1)如果共享对象是保存整数值的字符串对象,验证时间复杂度为 O(1)
;
(2)如果共享对象是保存字符串值的字符串对象,验证时间复杂度为 O(N)
;
(3)如果共享对象是包含了多个值对象,如列表或哈希对象,验证时间复杂度为 O(N2)
;