数据结构与对象
- 简单动态字符串——Simple Dynamic String(SDS)
- 链表——list
- 字典——dict
- 跳跃表——skiplist
- 整数集合——intset
- 压缩列表——ziplist
1.简单动态字符串
Redis没有直接使用C语言传统的字符串表示,而是自己构建了一种名为简单动态字符串(simple dynamic String)的抽象类型
1.1 SDS的定义
sds的结构:
struct sdshdr {
// 记录buf字符数组中已使用的字节的数量
// 等于sds的当前长度
int len;
// 记录buf字符数组中未使用的字节数量
int free;
// 字符数组,用于保存字符串
char[] buf;
}
free属性为0,表示这个sds没有剩余空间
len属性为5,表示这个sds的长度是5
buf属性是一个char类型的数组,数组中保存了’R’,’e’,’d’,’i’,’s’ 5个字符,最后一个字节保存了空字符’\0’.
sds遵循C字符串以空字符‘\0’结尾的惯例,保存空字符的一个字符空间不计算在sds的len属性里;为空字符分配额外的1字节空间,以及添加到字符串尾部的操作,都是由sds函数自动完成的,对用户来说是透明的。
这样做的好处是可以重用C字符串的部分函数(如printf函数)
1.2 SDS与C字符串的区别
- 常数复杂度获取字符串长度
- SDS有len属性,时间复杂度:O(1)
- C字符串遍历,时间复杂度:O(n)
- 杜绝缓冲区溢出
- C字符串不记录自身长度,易造成缓冲区溢出(buffer overflow),如使用string.h的strcat函数时,strcat函数假定用户已经为目标字符串分配了足够空间,一旦这个假定不成立,就会导致缓冲区溢出。
- 与C 字符串不同,SDS 的空间分配策略杜绝了缓冲区溢出的可能性:修改sds字符串时,会先检查空间是否足够(不够就扩展至足够空间),再做修改操作。
- C字符串不记录自身长度,易造成缓冲区溢出(buffer overflow),如使用string.h的strcat函数时,strcat函数假定用户已经为目标字符串分配了足够空间,一旦这个假定不成立,就会导致缓冲区溢出。
- 减少修改字符串时导致的内存重分配次数
- 因为C 字符串不记录字符串长度,所以每次修改字符串都会进行内存重分配操作;如果是扩展字符串(拼接操作,如果忘了会导致缓冲器溢出),如果是缩短字符串(截断操作,如果忘了会导致内存泄漏)
- 而SDS 修改字符串前都会进行检查剩余空间以判断是否需要内存重分配,因为redis在性能方面要求严苛,所以SDS 通过空间预分配和惰性空间释放两个策略满足需求
- 二进制安全
- C 字符串中的字符必须符合某种编码(ASCII),除字符串末尾外不能出现空字符;这些限制导致C 字符串只能保存文本数据,不能保存像图片,音频, 视频,压缩文件这样的二进制数据
- SDS 的API 都是二进制安全的,API函数都会以处理二进制的方式来处理放在buf中的数据。因为sds是使用len属性值来判断字符串是否结束的。
- 可以使用部分string.h库的函数
- 因为sds遵循C字符串以‘\0’字符结尾的惯例,所以sds可以使用string.h函数库的部分函数,避免了不必要的重复代码。
2.链表
链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活地调整链表长度。
2.1链表和链表节点的实现
每个链表节点使用一个listNode的结构表示
typedef struct listNode {
//前置节点
struct listNode *prev;
//后置节点
struct listNode *next;
//节点的值
void *value;
}listNode
多个listNode通过prev和next指针组成双端链表
多个listNode结构组成链表lsit,list结构定义:
typedef struct list {
//表头节点
listNode *head;
//表尾节点
lsitNode *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成员则是用于实现多态链表所需的类型特定函数:
- dup函数用于复制链表节点所保存的值;
- free函数用于释放链表节点所保存的值;
- match函数用于对比链表节点所保存的值和另一个输入值是否相等。
Redis的链表实现的特性:
- 双端:链表节点带有prev和tail指针,获取某个节点的前置节点和后置节点的复杂的都是O(1);
- 无环:表头节点的prev指针和表尾节点的tail指针都指向null,对链表的访问以null为终点;
- 带表头节点和表尾节点:通过list结构的head指针和tail指针,获取链表的头节点和尾节点的复杂的O(1);
- 带链表长度计数器:通过list结构的len属性来对list节点进行技术,获取list中节点数量的复杂的O(1);
- 多态:链表节点使用value*指针来保存节点值,并且可以通过list结构的dup、free、match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。
3.字典
字典,又称为符号表(symbol table)、关联数组(associative array)或映射(map),是一种用于保存键值对的抽象数据结构。在字典中,每个键都是独一无二的。
3.1 字典的实现
Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,每个节点保存了字典的一个键值对。
3.1.1 哈希表
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,这个属性和哈希值一起决定一个键应该放到table数组的那个索引上。
3.1.2 哈希表节点
哈希表节点使用dictEntry结构表示,每个dictEntry结构都保存着一个键值对:
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
}v;
//指向下一个哈希表节点,形成链表
struct dictEntry *next;
}dictEntry;
key属性保存着键值对的键,而v属性保存值,其中值可以是一个指针,或者是一个uint64_t整数,又或者是int64_t整数。next属性是指向另一个节点的指针,这个指针可以将多个哈希值的键值对连接在一起,以此来解决键冲突(collision)的问题。
3.1.3 字典
Redis中的字典dict 的结构表示:
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash索引
// 当rehash没有进行时,值为-1
int rehashidx;
}dict;
type 属性和privdata属性是针对不同类型的键值对,为创建多态字典而设置的;
ht属性是一个包含两个项的数组,数组中的每个想都是一个dictht哈希表,一般情况下,字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]哈希表进行rehash时使用。
除了ht[1]之外,rehashidx属性也和rehash有关,它记录了rehash的当前进度,如果当前没有在进行rehash,值为-1.
3.2 哈希算法
当要将一个新的键值对添加到字典里面时,需要根据键值对的键计算出哈希值和索引值,然后再根据索引值,将包含新键值对的哈希表节点放到哈希表数组的指定索引上。
3.2.1 解决键冲突
当两个或两个以上的键被分配到了哈希表数组的同一个索引上时,我们称这些键发生了冲突。
Redis 的哈希表使用链地址法来解决键冲突:每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用next连接成单向链表,这就解决了键冲突问题。
因为dictEntry节点组成的链表没有指向链表尾部的指针,所以为了速度考虑,程序总是将新节点添加到链表头部位置(复杂度O(1))。
3.2.2 rehash与渐进式rehash
4. 跳跃表
跳跃表(skiplist),是一种有序的数据结构,它通过在每个节点里维护多个指向其他节点的指针,从而达到快速访问节点的目的;跳跃表支持平均O(logN),最坏O(N)复杂度的节点查找,还可以通过顺序性操作来处理节点。
4.1 跳跃表的实现
Redis的跳跃表有zskiplistNode 和 zskiplist 两个结构定义,其中zskiplistNode结构用于表示跳跃表节点,而skiplist结构则用于保存跳跃表节点的相关信息,比如节点的数量,节点的表头指针,表尾指针等。
zskiplistNode结构的定义
typedef struct zskiplistNode{
//层
struct zskiplistLevel {
//前进指针
struct zskiplistNode *forward;
//跨度
unsigned int span;
} level[];
//后退指针
struct zskiplistNode *backward;
//分值
double score;
//成员变量
robj *obj;
}zskiplistNode;
1.层
节点的level数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点的速度。
前进指针:每个层都有一个指向表尾方向的前进指针,用于从表头向表尾方向访问节点
跨度:层的跨度用于记录两个节点之间的距离;跨度越大,相距越远,指向null的前进节点的跨度为 0
2.后退节点
后退节点用于从表尾向表头方向访问节点
3.分值和成员
节点的分值(score属性)是一个double类型的浮点数,跳跃表中所有节点都按分值从小到大排序
节点的成员变量是一个指针,它指向一个字符串对象
zskiplist结构的定义
typedef struct zskiplist {
//表头节点和表尾节点
struct zskipliztNode *head,*tail;
//表中节点的数量
unsigned long length;
//表中层数最大的节点层数
int level;
}zskiplist;
head,tail指针分别指向跳跃表的表头节点和表尾节点,获取表头,表尾节点的复杂的是O(1)
length属性,用于记录节点的数量
level属性,用于获取跳跃表中层数最大的那个节点的层数量,注意表头节点的层高不计算在内。
5. 整合集合
整数集合(intset)是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。
5.1整数集合的实现
整数集合intset 是Redis用于保存整数值的集合抽象数据结构,它可以保存类型为int_16、int_32或者int_64的整数值,并且保证集合中不会出现重复元素
typedef struct intset {
//编码方式
uint32_t encoding;
//集合包含的元素个数
uint32_t length;
//保存元素的数组
int8_t content[];
}intset;
contents数组是整数集合的底层实现:整数集合的每个元素都是contents数组的一个数组项,各个项在数组中按值的大小从小到大有序排序,并且不包含重复项。
length属性记录了整数集合包含的元素数量,也即是contents数组的长度。
虽然intset结构将contents属性声明为int8_t类型的数组,但实际上contents数组并不保存任何int8_t类型的值,contents的真正类型取决于encoding属性的值。
1. 如果encoding=INTSET_ENC_INT16,那么contents就是一个int16_t类型,[-32768,32767]
2. 如果encoding=INTSET_ENC_INT32,那么contents就是一个int32_t类型,[-2^32,2^32-1]
3. 如果encoding=INTSET_ENC_INT64,那么contents就是一个int64_t类型,[-2^64,2^64-1]
5.2 升级
当要将一个新元素添加到整数集合里,并且新元素的类型比整数集合现在的类型要长时,整数集合就会先进行升级(upgrade),然后才能将新元素添加到整数集合里
升级分三步进行:
1. 根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间
2. 将数组现有的所有元素都转换成新元素的类型,并将转换后的元素放置到正确的位置上(移动元素从后面的元素开始),并且要保持数组元素的有序性
3. 将新元素添加到数组中
因为每次向整数集合添加新元素都可能会引起升级,而每次升级都需要对底层数组的所有元素进行类型转换,所以添加新元素的时间复杂度为O(N)。
升级的好处:
1. 提高了活性:可以任意添加int16_t,int32_t,int64_t类型的值,不用担心类型错误
2. 节约内存:会用int16_t保存整数,在有需要的时候可以自动升级,节约内存。
降级
整数集合不支持降级操作
6. 压缩列表
压缩列表(ziplist)是列表键和哈希键的底层实现之一,当列表键只包含少量列表项,并且每个列表项要么是小整数值,要么是长度较短的字符串,你们Redis将会使用压缩列表键的底层实现。
6.1 压缩列表的构成
压缩列表是由一系列特殊编码的连续内存块组成的顺序型数据结构。一个压缩列表可以包含任意个节点,每个节点保存一个字节数组或者一个整数值。
属性 | 类型 | 长度 | 用途 |
---|---|---|---|
zlbytes | uint32_t | 4字节 | 记录压缩列表占用的内存字节数,在对压缩列表进行内重重分配,或计算zlend的位置时使用 |
zltail | uint32_t | 4字节 | 记录压缩列表表尾节点距离其实节点地址的字节数,通过这个偏移量,可直接获取表尾节点的地址 |
zllen | uint16_t | 2字节 | 记录了列表节点的数量;当这个值小于UINT16_MAX(65535)时,这个值就是列表节点的数量;当等于65535时,需要遍历计算 |
entryX | 链表节点 | 不定 | 压缩列表包含的节点 |
zlend | uint8_t | 1字节 | 特殊值0xff(255),用于标记压缩列表的末端 |
6.2 压缩列表节点的构成
每个压缩列表列表节点可以保持一个字节数组或者一个整数值,其中,字节数组可以是以下三种长度的其中一种:
1. 长度小于等于63(2^6-1)字节的字节数组;
2. 长度小于等于16383(2^14-1)字节的字节数组;
3. 长度小于等于xxxx(2^32-1)字节的字节数组;而整数值则可以是以下6种长度的其中一只:
1. 4位长,介于0-12之间的无符号整数
2. 1字节长的有符号整数
3. 3字节长的有符号整数
4. int16_t类型整数
5. int32_t类型整数
6. int64_t类型整数
每个压缩列表节点都由previous_entry_length、encoding、content三个属性组成。
previous_entry_length属性以字节为单位,记录了前一个节点的长度,previous_entry_length属性的长度可以是1字节或者5字节
如果前一个节点的长度小于254字节,那么previous_entry_length属性的长度为1字节
若果前一个节点的长度大于等于254字节,那么previous_entry_length属性的长度为5字节encoding属性记录了节点的content属性所保存数据的类型及长度:
1字节,2字节,5字节,值的最高位为00,01或者10的是字节数组编码,这种编码表示节点的content属性保存着字节数组,数组的长度由编码去除最高2位之后的其他位记录
1字节,值的最高位以11开头的整数编码,这种编码表示节点的content属性保存着整数值,整数值的类型和长度由编码去除最高2位之后的其他位记录content属性负责保存节点的值,节点的值可以是一个字节数组或者整数,值的类型和长度由节点的encoding属性决定
6.3 连锁更新
前面说到,previous_entry_length属性保存了前一个节点的长度(if(length(前一个节点)<254字节){previous_entry_length需要1字节存储} else{previous_entry_length需要5字节存储})
因此,添加新节点可能会产生连续多次空间扩展操作,称之为连锁更新;另外,删除节点也可能引发连续多次空间扩展操作。
因为连锁更新在最坏的情况下需要对压缩列表执行N次空间次分配操作,而每次空间次分配的最坏复杂的为O(N),使用连锁更新的最坏复杂度为O(N^2)。
尽管连锁更新的复杂的较高,但发生的概率很低,所以ziplistPush等命令的平均复杂度为O(N)。
7 对象
上面介绍了redis用到的所有数据结构:简单动态字符串(sds),双端链表(list),字典(dist),跳跃表(skiplist),整数集合(intset)和压缩列表(ziplist)。
Redis没有直接使用这些数据结构构建键值对数据库,而是基于这些数据结构构建了一套对象系统,这个系统包括了字符串对象,列表对象,哈希对象,集合对象,有序集合对象这五种类型的对象,每种对象都至少使用了前面两种数据机构作为实现。使用对象的好处:
1. 可以根据对象的类型,判断这个对象是否可以执行给定的命令
2. 可以根据不同的使用场景,为对象设置不同的数据结构实现,从而优化对象在 不同场景下的使用效率。
除此之外,redis的对象系统基于引用计数实现了下面两个功能:
1.redis通过引用计数*实现了内存回收机制,当程序不在使用某个对象时,这个对象占用的内存就会被自动释放;
2.redis通过引用计数实现了对象共享机制,这一机制在适当的条件下,通过让多个数据库键共享同一个对象来节约内存
最后,redis对象还记录了访问时间信息,该信息可以用于计算数据库键的空转时长,当数据库服务器启用了maxmemory功能时,空转时长较大的数据库键可能会优先被服务器删除。
7.1 对象的类型与编码
Redis使用对象来表示数据库键和值,每当我们数据库中保存一个键值对时,都至少创建两个对象,键对象和值对象;键对象是字符串对象,值对象可以是上面提到的五种对象。
另外,我们常叫“字符串键”“列表键”都是指值对象的类型。字符串,列表
Redis中的每个对象都油一个redisObject结构表示,该结构有三个属性:type、encoding和ptr
typedef struct redisObject{
//类型
unsigned type:4;
//编码
unsigned encoding:4;
//指向底层实现数据结构的指针
void *ptr;
....
}robj;
7.1.1 类型
对象的type字段记录对象的类型,当我们对一个数据库键执行TYPE命令时,命令返回结果是数据库键对应的值对象的类型。
type命令的输出:
7.1.2 编码和底层实现
对象的ptr指针指向对象的底层实现数据结构,而这些数据结构有encoding属性决定。
对象的encoding属性记录了对象所使用的编码,也即是这个对象使用了什么数据结构作为底层实现。
每种类型的对象至少使用了两种不同的编码:
使用object encoding 命令可以查看一个数据库键的值对象的编码
7.2 字符串对象
字符串对象的编码可以是int、raw或者embstr。
如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在对象结构(redisObject)的ptr属性里(将void*装换成long),并将字符串的编码encoding设置为int。
如果字符串对象保存的是一个字符串值,并且字符串值长度大于32字节,那么将用简单动态字符串(sds)保存,并将编码设置为raw。
如果字符串长度小于等于32字节,那么字符串对象将使用embstr编码方式保存这个字符串值
embstr编码是一种专门用来保存短字符串的一种优化编码方式,这种编码和raw编码一样,都是用redisObject机构和sdshdr结构,但raw会调用两次内存分配函数分别创建redisObject和sdshdr,而embstr则通过调用一次,分配一块连续的内存空间。
embstr相对于raw的优点:
1. 内存分配次数从两次降为一次
2. 内存回收次数也从两次降为一次(调用内存释放函数)
3. embstr编码的字符串对象将所有我的数据都保存在一块连续的内存里,所以相对于raw编码的字符串对象,他能更好地利用缓存带来的优势。
对于浮点数,redis保存的类型是字符串类型的值;在需要取浮点数进行数学运算时,会将字符串类型的浮点数值取出来转回浮点数值,执行完计算后,再转换成字符串保存。
7.2.1编码的转换
int编码和embstr编码的字符串对象在合适的条件下会转换成raw编码的字符串对象。
当int编码的字符串对象进行某些操作后不在是整数值时,会转换为raw编码的字符串
embstr编码的字符串是只读的,他没有任何修改的方法函数,所以放对embstr编码的字符串对象执行修改命令后,都会转换为raw编码的字符串
7.2.2 字符串命令的实现
7.3 列表对象
列表对象的编码可以是ziplist或者linkedlist。
zip编码的列表对象使用压缩列表最为底层实现,每个压缩列表节点(entry)保存一个列表元素
linkedlist编码的列表对象使用双端链表作为底层实现,每个双端链表节点(node)都保存一个字符串对象(redisObject),每个字符串对象都保存一个列表元素。
注意,linkedlist编码的列表对象在底层的双端链表结构中包含了多个字符串对象。字符串对象是redis五种类型中唯一一种会被其他类型对象嵌套的对象。
7.3.1 编码转换
当列表对象可以满足以下两个条件时,列表对象使用ziplist:
1. 列表对象保存的每个字符串对象元素的长度都小于64字节
2. 列表对象保存的元素个数小于512个。
当不能满足这两个条件时,列表对象需要使用linkedlist编码
以上两个条件的上限是可以自定义调整的,具体在配置文件中关于list-max-ziplist-value和list-max-ziplist-entries选项的说明。
对于使用ziplist编码的列表对象来说,当不能满足ziplist所需的两个条件的任意一个时,对象编码的转换操作就会被执行,原本保存在压缩列表中的所有列表元素都会被转移并保存到双端链表中,对象的编码也会从ziplist变为linkedlist。
7.3.2 列表命令的实现
7.4 哈希对象
哈希对象的编码可以是ziplist和hashtable
ziplist编码的哈希对象使用压缩列表作为底层实现,每当有新的键值对要加入到哈希对象中,程序会将保存了键的压缩列表节点推入压缩列表表尾,然后再将值节点推入表尾。哈希对象中键值对两个节点紧挨在一起,键在前,值在后。
hashtable编码的哈希对象使用字典作为底层实现,哈希对象的每个键值对都是用一个字典的键值对保存
7.4.1 编码转换
当哈希对象可以同时满足下面两个条件时,哈希对象使用ziplist编码:
1. 哈希对象保存的键值对的键和值的字符串长度都小于64字节
2. 哈希对象保存的键值对数量小于512个
不能满足上面任意一个条件,都会使用hashtable编码
注意,上限可以自定义调整,具体参照配置文件中hash-max-ziplist-value和hash-max-ziplsit-entriescan选项的说明。
当不能满足上面任何一个条件时,转换操作被执行,原本保存在压缩列表中的所有键值对都会被转移到字典中,对象的编码也会从ziplist转换为hashtable。
7.4.2 哈希命令的实现
7.5 集合对象
集合对象的编码可以是intset和hashtable。
intset编码的集合对象使用整数集合作为底层实现,集合包含的所有元素都被保存在整数集合里面。
hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象都包含一个集合元素,而字典的值则全被设置为null。
7.5.1 集合的转换
当集合对象可以同时满足以下两个条件时,对象使用intset编码
1. 集合对象所保存的所有元素都是整数值;
2 .集合对象所保存的所有元素数量不超过512个。
不能满足上面两个条件时,对象使用编码
注意,上面的第二个条件是可以自定义调整的,具体参照配置文件的set-max-intset-entries选项的说明。对于使用intset编码的集合对象,当上面任意一个条件不能满足时,就会执行对象的编码转换操作,原本保存在整数集合中的所有元素被转换并保存到字典中,并且对象的编码从intset变为hashtable。
7.5.2 集合命令的实现
7.6 有序集合对象
有序集合对象的编码可以是ziplist和skiplist。
ziplist编码的有序集合对象使用压缩列表作为底层实现,每个集合对象使用两个紧挨在一起的压缩列表节点保存,第一个节点保存集合元素的成员(member),第二个保存元素的分值(score)。
压缩列表内的集合元素按分值(score)大小从小到大排序。分值小的靠近表头。
skiplist编码的有序集合对象使用zset结构作以为底层实现,一个zset结构包含一个字典和一个跳跃表:
typedef struct zset{
zskiplist *zsl;
dict *dict;
} zset;
zset结构中的zsl按集合分值从小到大保存了所有集合元素,每个跳跃表节点保存一个集合元素:跳跃表节点的object属性保存了元素的成员(member),跳跃表节点的score属性保存元素的分值。通过这个跳跃表,程序可以对有序集合进行范围型的操作,如zrange,zrank等命令就是基于跳跃表的api实现的。
zset结构中的dict字典为有序集合创建了一个从成员到分值的映射关系,字典中的每个键值对保存一个集合元素:字典的键保存元素的成员(member),字典的值保存元素的分值(score)。通过这个字典,程序可以用O(1)复杂度查找给定成员的分值,zscore命令就是基于这个实现的。
有序集合每个元素的成员都是一个字符串对象,每个分值都是一个double类型的浮点数。zset结构中跳跃表和字典都是通过指针共享每个元素的成员(member)和分值(score),并不会造成内存浪费。
7.6.1 编码的转换
当有序集合对象同时满足以下两个条件时,使用ziplist编码:
1. 有序集合每个元素的成员长度都小于64字节;
2. 有序集合对象保存的元素数量小于128和。
不能同时满足上面两个条件,使用skiplist编码 。
注意,以上两个条件可以自定义调整,具体参照配置文件中关于zset-max-ziplist-value和zset-max-ziplist-entries选项的说明。
7.6.2 有序集合命令的实现
7.7 类型检查和命令多态
Redis中操作键的命令可以分为两种:
其中一种命令可以对任何类型进行操作,比如del,expire,rename,type,object等。
而另一种只能针对特定类型的键执行,比如:
set,get,append,strlen等命令只能对字符串键执行;
hdel,hset,hget,hlen等命令只能对哈希键执行;
rpush,lpop,linsert,llen等命令只能对列表键执行;
sadd,spop,sinsert,scard等命令只能对集合键执行;
zadd,zcard,zrank,zscore等命令只能对有序集合执行。
否则会报类型错误
7.7.1 类型检查和实现
redis在执行命令前,会先检查输入键的类型,判断该类型是否可以执行给定的命令;类型检查是通过redisObject对象的type属性实现的:
在执行一个类型特定命令前,服务器会先检查输入数据库键的值对象是否为给定命令所需的类型,如果是则执行
否则。服务器向客户端返回一个类型错误。
7.7.2 多态命令的实现
redis会根据值对象的编码,选择正确的命令 实现代码来执行命令;
这种根据对象的不同编码来选择不同命令实现来执行命令,称之为多态
举个例子,llen命令会根据数据库键的值对象的编码(ziplist或linkedlist),选择不同的命令实现来执行命令。
ziplist编码,选择压缩列表的api来实现命令
linkedlist编码,选择双端链表的api来实现命令实际上,可以将del,expire,rename,type,object命令也称为多态命令。
del,expire命令相对于llen命令的区别在于,前者是基于类型的多态——一个命令可以用来处理多种不同类型的键;后者是基于编码的多态—— 一个命令可以同时用于处理多种不同编码的键。
7.8 内存回收
C语言不具备自动内存回收功能,所以redis在自己的对象系统中构建了一个基于引用计数技术实现的内存回收机制,通过这一机制,程序可以通过跟踪对象的引用计数信息,在适当的时候自动释放对象并进行内存回收。
对象的引用计数信息有redisObject结构中的refcount属性记录:
typedef struct redisObject{
//...
//引用计数
int refcount;
//...
}robj;
对象的引用计数信息会随着对象的使用状态而不断变化:
1. 在创建一个新对象时,引用计数的值被初始化为1;
2. 当对象被一个新程序使用时,引用计数的值 +1;
3. 当对象不再被一个程序使用时,引用计数的值 -1;
4. 当对象的引用计数值变为0时,对象所占用的内存会被释放。
对象的整个生命周期都可以划分为创建对象,操作对象和释放对象三个阶段。
7.9 对象的共享
对象的引用计数属性处理用于内存回收机制外,还可以用于对象共享。
在Redis中,让多个键共享同一个值对象需要下面两个步骤:
1. 将数据库键的值指针指向一个现有的值对象;
2. 将被共享的之对象的引用计数+1
Redis会共享值为0到9999的字符串对象
Redis在初始化服务器时,创建0到9999这一万个对象;当用到这些对象时,不在创建,而是共享。
Redis不共享包含字符串的对象,虽然共享能节约内存,但对象对比时会耗费时间:
1. 整数值的字符串对象:验证目标对象和更新对象是否完全相同的时间复杂度为O(1);
2. 字符串值的字符串对象:验证目标对象和更新对象是否完全相同的时间复杂度为O(N);
3. 共享对象包含了多个值(或对象)的对象,比如列表对象或者哈希对象,那么验证目标对象和更新对象是否完全相同的时间复杂度为O(N^2);
因此尽管共享复杂对象可以节约内存,但受到执行时间的限制,
Redis只对包含整数值的字符串对象进行共享
7.10 对象的空转时长
redisObject结构除了上面介绍的type,encoding,ptr和refcount四个属性外,还包含的最后一个属性是lru属性,该属性记录了对象最后一次被命令程序访问的时间:
typedef struct redisObject{
//...
unsigned lru:22;
//...
}robj;
object idletime 命令可以打印出给定键的空转时长,这个空转时长就是通过当前时间减去键的值对象的lru时间计算得出的。
注意:object idletime命令的实现是特殊的,这个命令在访问键的值对象时,不会修改值对象的lru属性除了可以被object idletime命令打印出来之外,键的空转时长还有另外一项作用:
如果服务器开启了maxmemory选项,并且服务器用于回收内存的算法为volatile-lru或者allkeys-lru,那么当服务器占用的内存超过了maxmemory选项设置的上限值时,空转时长较高的那部分键会优先被服务器释放,从而回收内存。