目录
Redis对象概述
Redis对象的类型type和编码encoding
类型type
编码encoding
ptr属性
字符串对象
编码类型
int编码
embstr编码
raw编码
字符串对象编码转换
字符串对象底层数据结构
简单动态字符串SDS
SDS和C的字符串的区别
列表对象
编码类型
列表对象编码转换
列表对象底层数据结构
链表
压缩列表
哈希对象
ziplist编码
hashtable编码
哈希对象编码转换
哈希对象的底层数据结构
压缩列表
字典(字典实现包括哈希表和哈希表节点)
字典实现
集合对象
编码类型
编码转换
集合对象的底层数据结构
整数集合
字典
有序集合对象
编码类型
ziplist编码
skiplist编码
编码转换
有序集合对象底层数据结构
压缩列表
跳跃表
总结:
其他拓展
内存回收
对象共享
对象空转时长
Redis 是一个基于内存的键值对(key-value)的分布式存储系统,Redis 数据库里面的每个键值对(key-value pair) 底层都是由对象(redisObject)组成的,其中
Redis使用对象来表示key和value,在Redis数据库中,新创建一个键值对时,至少会创建两个对象,一个键对象,一个值对象
Redis中的每个对象都由redisObject结构表示。redisObject结构与保存数据相关的三个属性分别是:type属性、encoding属性和ptr属性。
typedef struct redisObject {
// 类型
unsigned type:4;
// 编码
unsigned encoding:4;
// 底层数据结构的指针
void *ptr;
//...
} robj;
redisObject对象的type属性记录了不同的对象的类型,这个属性值可以是如下的其中之一:
类型常量(type) | 对象类型名称 |
---|---|
REDIS_STRING | 字符串对象 |
REDIS_LIST | 列表对象 |
REDIS_HASH | 哈希对象 |
REDIS_SET | 集合对象 |
REDIS_ZSET | 有序集合对象 |
Redis数据库中,key总是字符串对象,而value可以是五种对象类型的其中之一,一般我们称Redis的数据类型指的是key对应的value的值数据类型。同样,通过type命令返回的也是数据库key对应的value对象的类型,type命令如下:
TYPE key
返回 key 所储存的值的类型。
时间复杂度:
O(1)
返回值:
none (key不存在)
string (字符串)
list (列表)
set (集合)
zset (有序集)
hash (哈希表)
redis> SET weather "sunny"
OK
redis> TYPE weather
string
redis> LPUSH book_list "programming in scala"
(integer) 1
redis> TYPE book_list
list
对象的ptr指针指向对象的底层数据实现数据结构,数据结构由encoding属性决定。
encoding属性记录对象使用的编码,即对象使用什么数据结构作为对象的底层实现,这个属性值可以是如下的其中之一:
编码常量encoding | 编码对应的底层数据结构 |
---|---|
REDIS_ENCODING_INT | long类型的整数 |
REDIS_ENCODING_EMBSTR | emstr编码的简单动态字符串 |
REDIS_ENCODING_RAW | 简单动态字符串 |
REDIS_ENCODING_HT | 字典 |
REDIS_ENCODING_LINKEDLIST | 双端链表 |
REDIS_ENCODING_ZIPLIST | 压缩列表 |
REDIS_ENCODING_INTSET | 整数集合 |
REDIS_ENCODING_SKIPLIST | 跳跃表和字典 |
可以看出每种类型的对象都至少使用了两种不同的编码,如下:
类型type | 编码encoding | 对象 |
REDIS_STRING | REDIS_ENCODING_INT | 使用整数值实现的字符串对象 |
REDIS_STRING | REDIS_ENCODING_EMBSTR | 使用embstr编码的简单动态字符串实现的字符串对象 |
REDIS_STRING | REDIS_ENCODING_RAW | 使用简单动态字符串实现的字符串对象 |
REDIS_LIST | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的列表对象 |
REDIS_LIST | REDIS_ENCODING LINKEDLIST | 使用双端链表实现的列表对象 |
REDIS_HASH | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的哈希对象 |
REDIS_HASH | REDIS_ENCODING_HT | 使用字典实现的哈希对象 |
REDIS_SET | REDIS_ENCODING_INTSET | 使用整数集合实现的集合对象 |
REDIS_SET | REDIS_ENCODING_HT | 使用字典实现的集合对象 |
REDIS_ZSET | REDIS ENCODING ZIPLIST | 使用压缩列表实现的有序集合对象 |
REDIS_ZSET | REDIS_ENCODING_SKIPLIST | 使用跳跃表和字典实现的有序集合对象 |
使用OBJECT ENCODING 命令可以查看一个数据库键的值对象的编码
redis> SET game "lol" # 设置一个字符串
OK
redis> OBJECT ENCODING game # 字符串的编码方式
"embstr"
redis> SET story "Long long ago there lived a king..." # 设置一个长字符串
OK
redis> OBJECT ENCODING game # 字符串的编码方式
"raw"
redis> SET phone 15820123123 # 大的数字也被编码为字符串
OK
redis> OBJECT ENCODING phone
"raw"
redis> SET age 20 # 短数字被编码为 int
OK
redis> OBJECT ENCODING age
"int"
对象可以以多种方式编码:
通过encoding属性设定对象使用的编码,而不是为特定类型的对象关联固定的编码,这样极大提高Redis的效率和灵活度,Redis可以根据不同场景为一个对象设置不同的编码,进而提高效率。比如:在列表对象包含的元素比较少时,Redis使用压缩列表作为列表对象的底层实现:
对象的ptr指针指向对象的底层数据实现数据结构,Redis的底层数据结构主要有以下几种:
字符串对象的编码可以是int、embstr或者raw
如果一个字符串对象保存的是整数值并且这个整数值可以用long类型表示,那么该字符串对象会将整数值保存在字符串对象结构的ptr属性中(将void*转换成long),并将字符串对象编码设置成int
如下set number 10086,就会创建int编码的字符串对象作为number键的值
如果字符串对象保存的是长度小于等于39个字节,编码类型则为embstr ,底层数据结构就是embstr编码的SDS。
embstr编码是专门用于保存短字符串的一种优化编码方式,embstr编码和raw编码一样都是使用redisObject结构和sdshdr结构,执行命令产生的效果是相同的,区别在于:
embstr编码的好处在于:创建时少分配一次空间,删除时少释放一次空间,因为内存是连续的,所以寻找也方便。坏处是长度增加要重新分配内存,因此一般是只读的。
如果字符串对象保存的是长度大于39字节的字符串,此时编码类型即为raw,其底层数据结构是简单动态字符串(SDS);
int编码和embstr编码的字符串对象在条件满足的情况下会自动转换为raw编码的字符串对象
redis> SET number 10086
OK
redis> OBJECT ENCODING number
"int"
redis> APPEND number "is a good number"
(integer)23
redis> GET number
"10086 is good number"
redis> OBJECT ENCODING number
"raw"
redis> SET msg "hello world"
OK
redis> OBJECT ENCODING msg
"embstr"
redis> APPEND msg "again"
(integer)18
redis> OBJECT ENCODING msg
"raw"
拓展:为什么raw和embstr的区分长度是39个字节
redisObject的长度是16,sds的长度是 9 + 字符串长度。因此embstr的长度正好是: 16 + 9 + 39 = 64字节
使用long double类型保存浮点型
可以用long double类型表示浮点数,在Redis中也是作为字符串值保存的,程序会将浮点数转换成字符串值,保存转换后的字符串值
redis> SET pi 3.14
OK
redis> OBJECT ENCODING pi
"embstr"
redis> INCRBYFLOAT pi 2.0
"5.14
redis> OBJECT ENCODING pi
"embstr"
总结:
值 | 编码 |
可以用long类型保存的整数 | int |
可以用long double类型保存的浮点数 | embstr或者raw |
字符串值,或者长度太大没法用long类型表示的整数,或者长度太大没法用long double类型表示的浮点数 | embstr或者raw |
Redis 是用 C 语言写的,但是对于Redis的字符串,却不是 C 语言中的字符串(以空字符结尾的字符数组),而是自己构建了一种名为简单动态字符串(simple dynamic string SDS)的抽象类型,并将SDS用作Redis 的默认字符串表示,使用sdshdr数据结构表示SDS:
struct sdshdr {
// 字符串长度
int len;
// buf数组中未使用的字节数
int free;
// 字节数组,用于保存字符串
char buf[];
};
SDS遵循了C字符串以空字符结尾的惯例,保存空字符的1字节不会计算在len属性里面。例如,Redis这个字符串在SDS里面的数据可能是如下形式:
这个1个字节的空字符串对于SDS使用者来说完全是透明的,之所以遵循空串结尾的好处SDS是可以直接重用一部分C字符串函数库的函数
C语言使用长度为N+1的字符数组来表示长度为N的字符串,并且字符串的最后一个元素是空字符\0。Redis采用SDS相对于C字符串有如下几个优势:
常数复杂度获取字符串长度
因为C字符串并不记录自身的长度信息,所以为了获取字符串的长度,必须遍历整个字符串,时间复杂度是O(N);而SDS使用len属性记录了字符串的长度,因此获取SDS字符串长度的时间复杂度是O(1)
C字符串不记录自身长度带来的另一个问题是很容易造成缓存区溢出。比如使用字符串拼接函数(stract)的时候,很容易覆盖掉字符数组原有的数据。与C字符串不同,SDS的空间分配策略完全杜绝了发生缓存区溢出的可能性。当SDS进行字符串扩充时,首先会检查当前的字节数组的长度是否足够,如果不够的话,会先进行自动扩容,然后再进行字符串操作。
C字符串不记录自身长度,底层是一个N+1字符长的数组,C字符串的长度和底层数据是紧密关联的,所以每次增长或者缩短一个字符串,程序都要对这个数组进行一次内存重分配:
如果是增长字符串操作,需要先通过内存重分配来扩展底层数组空间大小,不这么做就导致缓存区溢出。
如果是缩短字符串操作,需要先通过内存重分配来来回收不再使用的空间,不这么做就导致内存泄漏。
因为内存重分配涉及复杂的算法,并且可能需要执行系统调用,所以通常是个比较耗时的操作。对于Redis来说,字符串修改是一个十分频繁的操作,如果每次都像C字符串那样进行内存重分配,对性能影响太大了,显然是无法接受的。
SDS通过空闲空间解除了字符串长度和底层数据之间的关联。在SDS中,数组中可以包含未使用的字节,这些字节数量由free属性记录。通过空闲空间,SDS实现了空间预分配和惰性空间释放两种优化策略。
1.空间预分配:空间预分配是用于优化SDS字符串增长操作的,简单来说就是当字节数组空间不足触发重分配的时候,总是会预留一部分空闲空间。这样的话,就能减少连续执行字符串增长操作时的内存重分配次数。有两种预分配的策略:
len小于1MB时:每次重分配时会多分配同样大小的空闲空间;
len大于等于1MB时:每次重分配时会多分配1MB大小的空闲空间。
2.惰性空间释放:惰性空间释放是用于优化SDS字符串缩短操作的,简单来说就是当字符串缩短时,并不立即使用内存重分配来回收多出来的字节,而是用free属性记录,等待将来使用。SDS也提供直接释放未使用空间的API,在需要的时候,也能真正的释放掉多余的空间。
C字符串中的字符必须符合某种编码,并且除了字符串末尾之外,其它位置不允许出现空字符,否则被程序读入的空字符串将被误认为是字符串结尾,这些限制使得C字符串只能保存文本数据。而无法保存像图片、音频、视频这样的二进制数据,但是对于Redis来说,不仅仅需要保存文本,还要支持保存二进制数据。因为SDS使用len属性的值来判断字符串是否结束而非空字符。同样SDS的API全部做到了二进制安全(binary-safe)。
总结:
C 字符串 | SDS |
获取字符串长度的复杂度为O(N) | 获取字符串长度的复杂度为O(1) |
API 是不安全的,可能会造成缓冲区溢出 | API 是安全的,不会造成缓冲区溢出 |
修改字符串长度N次必然需要执行N次内存重分配 | 修改字符串长度N次最多执行N次内存重分配 |
只能保存文本数据 | 可以保存二进制数据和文本文数据 |
可以使用所有 |
可以使用一部分 |
列表对象的编码可以是linkedlist编码或者ziplist编码,对应的底层数据结构是链表和压缩列表。
当列表对象可以同时满足以下两个条件时,列表对象使用ziplist编码
不能满足这两个条件的列表对象都是用linkedlisted编码,当然这两个上限值可以通过配置文件hash-max-ziplist-value选项和hash-max-ziplist-entry选项修改
链表是一种非常常见的数据结构,提供了高效的节点重排能力以及顺序性的节点访问方式。在Redis中,每个链表节点使用listNode结构表示:
typedef struct listNode {
// 前置节点
struct listNode *prev;
// 后置节点
struct listNode *next;
// 节点值
void *value;
} listNode
多个listNode通过prev和next指针组成双端链表,如下图所示:
为了操作起来比较方便,Redis使用了list结构持有链表。list的结构如下:
typedef struct list {
// 表头节点
listNode *head;
// 表尾节点
listNode *tail;
// 链表包含的节点数量
unsigned long len;
// 节点复制函数
void *(*dup)(void *ptr);
// 节点释放函数
void (*free)(void *ptr);
// 节点对比函数
int (*match)(void *ptr, void *key);
} list;
list结构为链表提供了表头指针head、表尾指针tail,以及链表长度计数器len,而dup、free和match成员则是实现多态链表所需类型的特定函数。其中:
以下是一个由list结构和三个listNode结构组成的链表
Redis链表实现的特性
压缩列表不仅是列表键的底层实现之一,同样也是哈希键的底层实现之一,当列表项中只包含少量列表项并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现,如下:
redis> LPUSH lst 1,3,4,12306,"hello", "world"
(integer) 6
redis> OBJECT ENCODING lst
"ziplist"
压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构,一个压缩列表可以包含任意多个entry,每个节点可以保存一个字节数组或者一个整数值,如下图
压缩列表各个组成部分详细说明
属性 | 类型 | 长度 | 用途 |
zlbytes | uint_32_t | 4字节 | 记录整个压缩列表占用的内存字节数 |
zltail | uint_32_t | 4字节 | 记录压缩列表表尾节点距离起始地址有多少字节,通过这个偏移量,程序无需遍历整个压缩列表就能确定表尾节点地址 |
zlen | uint_16_t | 2字节 | 记录压缩列表包含的节点数量 |
entryX | 列表节点 | 不定 | 压缩列表的各个节点,节点长度由保存的内容决定 |
zlend | uint_8_t | 1字节 | 特殊值(0xFFF),用于标记压缩列表末端 |
如下图:
压缩列表节点组成
每个压缩列表的节点可以保存一个字节数组或者一个整数值,其中字节数组可以是以下三种长度其中一:
而整数值则可以是以下六种长度的其中一种:
每个压缩列表节点都由previous_entry_length、encoding、content三个部分组成
previous_entry_length
节点的previous_entry_length属性以字节为单位,记录了压缩列表中前一个节点的长度。长度可以是1字节或者5字节
previoust_entry_length属性记录前一个节点长度,所以程序可以通过指针根据当前节点地址计算出前一个节点的起始长度
encoding
记录了节点的content属性所保存数据的类型以及长度,字节数组编码如下表:
编码 | 编码长度 | content属性保存的值 |
00bbbbbb | 1字节 | 长度小于等于63(2^6 -1)字节的字节数组 |
01bbbbbb xxxxxxxx | 2字节 | 长度小于等于16383(2^14 -1)的字节数组 |
10_ _ _ _ __ aaaaaaaa bbbbbbbb cccccccc dddddddd | 5字节 | 长度小于等于4294967295(2^32 -1)的字节数组 |
整数编码如下:
编码 | 编码长度 | content属性保存的值 |
11000000 | 1字节 | int16_t类型整数 |
11010000 | 1字节 | int32_t类型整数 |
11100000 | 1字节 | int64_t类型整数 |
11110000 | 1字节 | 24位有符号证书 |
11111110 | 1字节 | 8位有符号证书 |
1111xxxx | 1字节 | 这一编码没有响应的content属性,因为编码本身的xxxx四位已经保存了一个介意0-12之间的值,无需在保存到content |
content
节点content属性负责保存节点的值,可以是一个字节数组或者证书,值的类型和长度由节点的encoding属性决定,如下:
哈希对象的编码可以是ziplist或者hashtable。对应的底层数据结构是压缩列表和哈希表
ziplist编码的哈希对象使用也是使用压缩列表的编码形式,每当有新得键值对要加入到哈希对象时候,先将保存了键的节点推入到压缩列表表尾,然后再将保存了值的节点推入到压缩列表表尾,比如执行如下三条HSET命令:
HSET profile name "tom"
HSET profile age 25
HSET profile career "Programmer"
如果此时使用profile键的值使用的是ziplist编码,那么该对象在内存中的结构如下:
可以看出第一个添加的哈希对象对靠近压缩列表表头,后来添加的哈希对象放到压缩列表的表尾,同时,同一哈希对象中,保存键的节点再前,保存值的节点在后。
hashtable编码的哈希对象使用字典作为底层实现,哈希对象中每个键值对都使用一个字典键值来保存,当一个哈希键包含的键值对比较多,或者键值对重的元素都是比较长的字符串,Redis就会使用字典作为哈希键的底层实现
当哈希对象可以同时满足以下两个条件时,哈希对象使用ziplist编码
无法同时满足上述两个条件的哈希对象都需要使用hashtable编码,当然这两个上限值可以通过配置文件hash-max-ziplist-value选项和hash-max-ziplist-entry选项修改
同列表对象底层数据结构,元素在压缩列表的位置如上图
哈希表
Redis中字典使用哈希表作为底层实现,哈希表由dictht结构表示
typedef struct dictht{
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 总是等于 size-1
unsigned long sizemask;
// 该哈希表已有节点数量
unsigned long used;
} dictht
table属性是一个数组,数组中的每个元素都是一个指向dictEntry结构的指针,每个dictEntry结构保存着一个键值对。size属性记录了哈希表的大小,即table数组的大小。used属性记录了哈希表目前已有节点数量。sizemask总是等于size-1,这个值主要用于数组索引。比如下图展示了一个大小为4的空哈希表。
哈希表节点
哈希表节点 哈希表节点使用dictEntry结构表示,每个dictEntry结构都保存着一个键值对:
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
unit64_t u64;
nit64_t s64;
} v;
// 指向下一个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
key属性保存着键值对中的键,而v属性则保存了键值对中的值。值可以是一个指针,一个uint64_t整数或者是int64_t整数。next属性指向了另一个dictEntry节点,在数组桶位相同的情况下,将多个dictEntry节点串联成一个链表,以此来解决键冲突问题。(链地址法)
Redis中的字典由dict结构表示:
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
//rehash索引
// 当rehash不在进行时,值为-1
int rehashidx;
}
其中type属性和privdata属性是针对不同类型的键值对,为创建多态字典而设置的,ht是大小为2,且每个元素都指向dictht哈希表。一般情况下,字典只会使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]哈希表进行rehash时使用。rehashidx记录了rehash的进度,如果目前没有进行rehash,值为-1。
哈希算法和键冲突
Redis中新的键值添加到字典时,会根据字典设置的哈希函数计算key哈希值,同时根据哈希表的sizemask属性和哈希值计算出索引值,最后将哈希表节点放到哈希表数组的指定索引上面。
当有两个或以上的键被分配到同一个哈希表数组的同一索引上,Redis使用链地址法来解决冲突,每个哈希表节点都有一个next指针,多个哈希表节点通过next指针构成一个单向链表
Rehash
随着不断操作,哈希表保存的键值对会逐渐的增多或者减少,为了让哈希表的负载因子维持在一个合理区间,需要对哈希表进行响应的扩展或者收缩,即需要rehash操作,步骤如下:
1.为ht[1]哈希表分配空间
1.如果是扩展操作,那么ht[1]的大小为第一个大于等于ht[0].used*2的下一个2的n次幂。比如 ht[0].used=5,那么此时ht[1]的大小就为16
2.如果是收缩操作,那么ht[1]的大小为第一个大于ht[0].used的下一个2的n次幂。比如 ht[0].used=5,那么此时ht[1]的大小就为8。
2.将保存在ht[0]中的所有键值对rehash到ht[1]中。
3.迁移完成之后,释放掉ht[0],并将现在的ht[1]设置为ht[0],在ht[1]新创建一个空白哈希表,为下一次rehash做准备
哈希表的扩展和收缩时机:
渐进式rehash
上面提到的扩展或者收缩需要将ht[0]里面的元素全部rehash到ht[1]中,但是这个rehash动作不是一次性集中式完成,而是可以分多次和渐进式的完成,避免在大量键值对全部rehash时造成的性能影响,具体步骤如下:
渐进式rehash一次迁移一个桶上所有的数据,设计上采用分而治之的思想,将原本集中式的操作分散到每个添加、删除、查找和更新操作上,从而避免集中式rehash带来的庞大计算。 因为在渐进式rehash时,字典会同时使用ht[0]和ht[1]两张表,所以此时对字典的删除、查找和更新操作都可能会在两个哈希表进行。比如,如果要查找某个键时,先在ht[0]中查找,如果没找到,则继续到ht[1]中查找。
集合对象的编码可以是intset或者hashtable
当集合对象可以同时满足以下两个条件时,对象使用intset编码
不能满足上述条件的集合对象需要使用hashtable编码,同样第二个上限值可以通过配置文件中的set-max-intsetentries选项修改
inset 编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合里面
redis> SADD numbers 1,3,5
(integer) 3
redis> OBJECT ENCODING numbers
"intset"
整数集合(intset)是Redis用于保存整数值的集合抽象数据结构,它可以保存类型为int16_t、int32_t或者int64_t的整数值,并且保证集合中的数据不会重复。Redis使用intset结构表示一个整数集合,如下:
typedef struct intset {
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
contents数组是整数集合的底层实现:整数集合的每个元素都是contents数组的一个数组项,各个项在数组中按值大小从小到大有序排列,并且数组中不包含重复项。
length属性记录了证书集合包含的元素数量,即contents数组的长度
虽然intset将contents属性声明为int8_t类型的数组,但实际上,contents数组不保存任何int8_t类型的值,数组中真正保存的值类型取决于encoding。
如果encoding属性值为INTSET_ENC_INT16,那么contents数组就是int16_t类型的数组,数组里面每个项都是一个int16_t类型的整数值(最小值是-32768,最大值是32767),依次类推INTSET_ENC_INT32和INTSET_ENC_INT64也是同理
升级
当新插入元素的类型比整数集合现有类型元素的类型大时,整数集合必须先升级,然后才能将新元素添加进来。这个过程分以下三步进行:
如下:有一个INTSET_ENC_INT16编码的整数集合,集合中包含三个int16_t类型的元素
因为每个元素占用16位空间(4个字节16位),所以整数集合的底层数组的大小为3*16=48,位置如下:
假设此时将类型为int32_t的整数值65535添加到证书集合,此时就需要先对整数集合进行升级,按照整数集合米钱的三个元素加上65535这个元素,整数集合需要分配四个元素的空间,每个int42_t整数值需要占用32位空间,底层数组的大小为4*32=128,然后转换之前的三个元素类型为int32_t类型,并将转换的元素保证有序的情况下放置在正确的位置即可。
此外,整数集合不支持降级,一旦对数组进行了升级,编码就会一直保持升级后的状态。
hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符换对象,每个字符串对象包含一个集合元素,而字典的值则全部被设置为null,当我们执行SADD fruits "apple" "banana" "cherry"向集合对象插入数据时,该集合对象在内存的结构如下:
有序集合的编码可以是ziplist或者skiplist
ziplist编码的有序集合对象使用压缩列表作为底层实现,每个集合元素使用两个紧邻在一起的压缩列表节点来保存,第一个节点保存元素的成员member,第二个元素保存元素的分数score
压缩列表内的集合元素按分值从小到大进行排序,分值较小的靠近表头方向,分值较大的靠近表尾方向,如果我们执行ZADD price 8.5 apple 5.0 banana 6.0 cherry命令,向有序集合插入元素,该有序集合在内存中的结构如下:
skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表。
typedef struct zset {
zskiplist *zs1;
dict *dict;
}
zset结构中包括字典和跳跃表,其中:
跳跃表zsl
按分值从小到大保存了所有集合元素,每个跳跃表节点都保存一个集合元素:跳跃表节点的object属性保存了元素的成员,而跳跃表的score属性则保存了元素的分值,通过跳跃表,可以对有序集合进行范围性操作,比如ZRANK、ZRANGE等命令
字典dict
dict中字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值对都保存一个集合元素:字典的键保存了元素的成员,字典的值保存了元素的分数,通过字典可以用O(1)的复杂度查找给定成员的分值,ZSOCRE命令就是基于这一特性实现。
有序集合的每个元素的成员都是一个字符串对象,而每个元素的分值都是一个double类型的浮点数,虽然zset结构同时使用跳跃表和字典来保存有序集合元素,但是这两种数据结构都会通过指针来共享相同元素的成员和分数,不会产生任何重复的成员或分值并且浪费内存
当有序集合对象可以同时满足以下两个条件时候,对象使用ziplist
不能同时满足上述两个条件的有序集合使用skiplist编码,可以通过zset-max-ziplist-entries选项和zset-max-ziplist-value选项的说明
数据结构同列表对象介绍,插入元素的顺序如上图
Redis使用跳跃表作为有序集合建的底层实现之一,当有序集合元素数量比较多,又或者元素成员member是比较长的字符串时候使用跳跃表作为有序集合底层实现
跳跃表(skiplist)是一种有序的数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的,支持平均O(logN)、最坏O(N)的复杂度的节点查找,还可以通过顺序化操作批量处理节点。
skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表。其中Redis的跳跃表由zskiplist和zskiplistNode两个结构定义,
其中:zskiplist保存跳跃表节点相关信息,比如节点的数量,以及指向表头和表尾节点的指针等
typedef struct zskiplist {
// 表头节点和表尾节点
struct skiplistNode *header, *tail;
// 节点数量
unsigned long length;
// 最大层数
int level;
} zskiplist;
跳跃表节点zskiplistNode结构定义如下:
typedef struct zskiplistNode {
// 后退指针
struct zskiplistNode *backward;
// 分值
double score;
// 成员对象
robj *obj;
// 层
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];
} zskiplistNode;
其中主要包括以下几个属性:
需要注意的是,表头节点不存储真实数据,并且层高固定为32,从表头节点第一个不为NULL
最高层开始,就能实现快速查找。
上述是一个左边zskiplist结构,包括以下属性
上述是一个左边zskiplistNode结构,包括以下属性
当然表头节点也和其他节点构造一样,有后退指针、分值、成员对象等,但是这些属性不会被用到,故省略
skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表。 假如还是执行ZADD price 8.5 apple 5.0 banana 6.0 cherry命令向zset保存数据,如果采用skiplist编码方式的话,该有序集合在内存中的结构如下:
总的来说,Redis底层数据结构主要包括:简单动态字符串(SDS)、链表、字典、跳跃表、整数集合和压缩列表六种类型,并且基于这些基础数据结构实现了字符串对象、列表对象、哈希对象、集合对象以及有序集合对象五种常见的对象类型。每一种对象类型都至少采用了2种数据编码,不同的编码使用的底层数据结构也不同。
C语言不具备自动内存回收功能,Redis构建引用计数器来实现内存回收,每个对象的引用计数信息由redisObeject结构的refcount属性记录
typedef struct redisObejct{
// ...
int refcount;
// ...
} robj;
对象引用计数信息随着对象使用状态而不断变化
对象的引用计数属性除了用于内存回收机制之外,还带有对象共享的作用。比如:键A创建了一个包含整数值100的字符串对象作为值对象,如果这时键B也要创建一个同样保存了整数值100的字符串对象作为值对象,那么服务器有以下两种做法:
很明显是第二种方法更节约内存。在Redis中,让多个键共享同一个值对象需要执行以下两个步骤:
可以看到,除了对象的引用计数从之前的1变成了2之外,其他属性都没有变化。共享对象机制对于节约内存非常有帮助,数据库中保存的相同值对象越多,对象共享机制就能节约越多的内存,目前来说Redis会在初始化服务时,创建一万个字符串对象,这些对象包含0-9999的所有整数值,当需要使用这些字符串对象时候,使用的是共享对象。
可以通过redis.h/REDIS_SHARED_INTEGERS常量来修改
redisObject对象中除了type、encoding、ptr、refcount四个属性外,还有一个lru属性,记录对象最后一次被命令程序访问的时间,
typedef struct redisObejct{
// ...
unsigned lru:22;
// ...
} robj;
可以通过OBJECT IDLETIME命令打印给定键的空转时长,空转时长通过当前时间减去键的值对象的lru时间计算得到,当键处于活跃状态,空转时长为0,
键的空转时长的作用:
在服务器打开maxmemory选项,并且服务器用于回收内存的算法为volatile-lru或者allkeys-lru,那么当服务器占用内存超过maxmemory选项所设置的上限值时,空转时长较高的那部分键优先被服务器释放,从而回收内存