参考书:《Redis设计与实现》黄建宏 著
第二章:简单动态字符串
SDS:simple dynamic string 简单动态字符串
redis把一切都看成字符串,SDS是底层最基础的字符串结构,它不像c语言以\0为结束符,而是记录了长度等信息(见下),这样我们就可以将任意的数据(比如图片、音频)当成字符串来存而不会有任何失真。所以是二进制安全的
1.数据结构:
struct sdshdr{
int len; //记录了buf中已经使用的字节数量
int free;//记录了buf中未使用字节数
// len+free 就是buff最大承载量。
char buff[];//字节数组,用来保存数据。大小为buff+len+1 ,最后的+1是为了额外保存一个\0
};
注意:redis中字符串末尾也以\0结束,但是\0不算入len和free属性,redis会自动为buff多分配一个字节的空间。这样做的好处是兼容部分c字符串函数,比如printf,但是最好不要用strlen。
相比于c中简单的字符串,额外保存的信息可以降低strlen的复杂度,杜绝缓冲区溢出。
2.内存分配方式:
空间预分配:对SDS进行操作导致需要给buff扩容的时候,会分配额外的空闲空间来优化复杂度,公式如下:
1.如果len<1MB,则free=len,意思是分配实际数据大小二倍的空间。
2.如果len>=1MB,则free=1MB,意思是多分配1MB的空间。
这样做可以大大减少内存分配次数提高效率。(空间换时间)
惰性空间释放:当SDS有效数据减少时(len变小,比如删掉一节数据),只会改变len和free不会释放空间(还是空间换时间)。
3.相关函数调用:
函数 | 作用 | 时间复杂度 |
---|---|---|
sdsnew | 创建一个SDS | O(len) |
sdsempty | 创建空的SDS | O(1) |
sdsfree | 释放一个SDS | O(len) |
sdslen | 获取sds保存数据的长度(len属性) | O(1) |
sdsavail | 获取sds未使用空间大小(free属性) | O(1) |
sdsdup | 创建一个给定sds的副本(copy) | O(len) |
sdsclear | 清空sds保存的字符串内容 | O(1) |
sdscat | 将给定的c字符串拼接到sds末尾 | O(n),n为c字符串长度 |
sdscatsds | 将给定的sds拼接到sds末尾 | O(len) |
sdscpy | 用给定的c字符串覆盖掉sds | O(n) |
sdsgrowzero | 用空字符将sds扩展至给定长度 | O(n) |
sdsrange | 保留sds给定区间内的数据,不在区间的数据将会被覆盖或清除 | O(n),n为保留数据字节数(这里不太清楚函数具体作用) |
sdstrim | 接受一个sds和一个c字符串作为参数,从sds左右两端分别移除所有在c字符串中出现过的字符(遇到第一个不存在的字符就停止) | O(M*N) |
sdscmp | 对比两个sds是否相同 | O(n) |
第三章:链表
链表就是我们平时说的很常规的链表,就不仔细讲了。
1.数据结构:
链表结构:
typedef struct list{
linkNode *head;
linkNode *tail;
unsigned long len;//节点数量
void *(*dup)(void *ptr); //节点复制函数
void (*free)(void *ptr); //节点释放函数
int (*match)(void *ptr,void *key) //节点对比函数
}list;
节点结构:
typede struct listNode{
//见名字,识功能
struct listNode *prev;
struct listNode *next;
void *value;
}listNode;
2.相关API
(见《Redis设计与实现》P21~22)
第四章:字典
字典是一种保存键值对(key-value pair)的抽象数据结构。
性质:
- 每个键都是独一无二的。
- 可以通过键来独一无二地确定一个值。
数据结构中最常见的字典大概有 树型、哈希型 两种结构。
redis 采用哈希表来实现词典。
1.数据结构
哈希表:
typedef struct dictht{
dictEntry **table; //哈希表数组,一维,每个都指向一个哈希表节点
unsigned long size; //哈希表大小
unsigned long sizemask;
//哈希表大小掩码,用于计算索引值,总是等于size-1(既然这样为什么还要保存它?)
unsigned long used; //哈希表已经有的节点数量。
}dictht;
哈希表节点:
typedef struct dictEntry{
void *key; //键
union{
void *val;
uint64_t u64;
int 64_t s64;
}v; //值 , 有三种可选项。
struct dictEntry *next; //指向下一个哈希表节点,形成链表。从这里可以看出哈希表处理冲突的方式。
}dictEntry;
字典:(略)
2.哈希算法
哈希算法:MurmurHash2
3.键冲突
从哈希表节点的数据结构可以看出:采用链表的形式处理键冲突。头插法。此处链表不是第三章讲的那种链表,而是最简单的只有首地址的单链表。
4.渐进式rehash
随着操作不断执行,哈希表中的节点会增多或减少。为了让负载因子(load factor=used/size)维持在一个合理的范围内,需要对表的大小进行相应的扩展或者收缩(rehash)
处理rehash不是一次性完成的(集中式rehash会大幅影响服务器性能),而是分批次地进行。
要进行rehash时,打一个rehash标记,之后每次进行增删改查操作时除了完成指定的操作外,还会顺带将一个节点进行搬运。
5.字典API
函数 | 作用 | 时间复杂度 |
---|---|---|
dictCreate | 创建一个字典 | O(1) |
dictAdd | 将给定的键值对添加到字典中 | O(1) |
dictReplace | 将给定的键值对添加到字典中,如果该键已经存在,则覆盖。 | O(1) |
dictFetchValue | 返回给定键的值 | O(1) |
dictGetRandomKey | 从字典中随机返回一个键值对 | O(1) |
dictDelete | 从字典中删除给定键所对应的键值对(全删除还是只删除一个?) | O(1) |
dictRelease | 释放给定字典,以及字典中包含的所有键值对 | O(N) |
第五章:跳跃表
功能相当于平衡树,大部分情况效率可以和平衡树媲美,但是跳跃表实现简单,所以不少程序都采用跳跃表来代替平衡树。
Redis只在两个地方用到了跳跃表,一个是有序集合键,另一个是在集群节点中作为内部数据结构。
具体实现和理论就不再赘述了。
第六章:整数集合
整数集合是保存数值的集合抽象数据结构,可以保存:int_16_t int32_t int_64_t 的数值,并保证集合中没有重复元素,元素在set内部从小到大排列。
1.数据结构
typedef struct intset {
uint32_t encoding; //编码方式
uint32_t length; //元素数量(并非contents大小)
int8_t contents[]; //保存元素的数组
}intset;
注意:int8_t只是缓冲区类型,并不代表元素类型。元素类型要看encoding的规定(会有相应的解码方式)。
比如存放int64_t类型的数据,则每8个int8_t空间表示一个元素。
2.升级
当我们要将一个新元素添加到整数集合中,且新元素的类型比encoding规定类型要长时,整数集合要先进行升级。
分为三步:
- 扩展空间
- 将原有数据放到新内存合适的位置
- 将新加入的元素放到合适的位置(保持集合的有序性)
注意:因为引发升级的新元素的长度总是比整数集合现有的所有元素的长度都大,所以这个新元素的值要么就大于所有元素,要么小于所有元素。所以新元素放到集合中后的索引要么是0,要么是length-1。
从这也可以看出,redis总是为整数自动分配恰好可以容纳它的最小数据类型,否则这里就会出错。
注意:没有没有降级的操作,也就是只升不降
这种策略的优点灵活节约什么的就不再提了,相信大家想想就能明白。
3.整数集合API
函数 | 作用 | 时间复杂度 |
---|---|---|
insetNew | 创建 | O(1) |
intsetAdd | 添加元素 | 如果引发升级则为O(n),其他O(1) |
intsetRemove | 移除元素 | O(n) |
intsetFind | 查找元素是否存在于集合中 | O(logn) |
intsetRandom | 随机返回一个元素 | O(1) |
intsetGet | 取出索引对应的元素 | O(1) |
intsetLen | 返回元素个数 | O(1) |
intsetBlobLen | 返回集合占用内存字节数 | O(1) |
第七章:压缩列表
压缩列表(ziplist)是redis为了节约内存而开发的。是由一系列特殊编码的连续内存块组成的顺序数据结构。
是列表键和哈希键的底层实现之一。
当一个列表键只包含少量列表项,并且每个列表项要么是小整数值,要么是长度比较短的字符串,那么redis就会使用ziplist来做列表键的底层实现。
1.压缩列表整体结构
zlbytes | zltail | zllen | entry1 | entry2 | ... | entryN | zlend |
属性 | 类型 | 长度 | 用途 |
---|---|---|---|
zlbytes | uint32_t | 4字节 | 记录整个压缩列表占用的内存字节数:在对压缩列表进行内存重分配,或者计算zlend的位置时使用。 |
zltail | uint32_t | 4字节 | 记录压缩列表节点距离压缩列表的起始地址的偏移:可以直接计算出表尾节点的地址。 |
zllen | uint16_t | 2字节 | 记录节点数量:当zllen<UINT16_MAX(65535)时,zllen就是节点数量;当zllen==UINT16_MAX时,则zllen数据无效,需要遍历列表计算节点数。 |
entryX | 列表节点 | 不定 | 压缩列表的节点类型,特殊编码。 |
zlend | uint8_t | 1字节 | 特殊值0xFF(十进制255)用于标记压缩列表的末端。 |
2.压缩列表节点结构
节点分为两种:整数类型、字节数组
A.整数类型有:
- 4位长(0~12)
- 1字节signed
- 3字节signed
- int16_t
- int32_t
- int64_t
B.字节数组类型有:
- 长度<=2^ 6 - 1 (63)
- 长度<=2^14-1 (16383)
- 长度<=2^32-1 (4 294 967 295)
每个节点的结构如下图:
previous_entry_length | encoding | content |
其中:
属性 | 长度、编码描述 | 用途 |
---|---|---|
previous_entry_length | 如果前一个节点的长度<254字节,则本属性为1字节;若>=254字节,本属性为5字节,且第一个字节置为0xFE,后4个字节保存前一个节点的长度。 | 记录了前一个节点的长度, |
encoding | 1/2/5字节,详见下文描述 | 记录了数据的类型和长度 |
content | 跟encoding有关,详见下文描述 | 存放数据 |
利用previous_entry_length属性,程序可以通过指针运算根据当前节点的起始地址来计算出前一个节点的起始地址。
3.节点encoding编码方式
长度有1/2/5字节三种编码。
A.最高两位为00、01、10的是字节数组编码。
- 高两位是00 : encoding长度一共是8位,剩下6位表示content保存的数据长度
- 高两位是01 : encoding长度一共16位,剩下14位表示content保存的数据长度
- 高两位是10 : encoding长度一共是40位(5字节),剩下38位表示content保存的数据长度
B.最高两位为11的是整数编码。
- 一字节长,剩余6位的编码决定了content保存的是哪一种整数。
(详见《redis设计与实现》P56)
4.连锁更新
由于previous_entry_length属性记录了前一个节点的长度,而此属性的长度跟具体值有关(以254为分界线,1字节或5字节)。
所以将一个节点插入到某个位置后,要相应地改变它后面那个节点的previous_entry_length属性。
相应地,如果因此引起后面节点previous_entry_length属性长度由1字节变为5字节,则需要进一步向后更新,如果又恰好引起后面的后面节点长度的改变,则需要再往后更新。以此类推。直到列表末尾或者无法继续连锁反应为止。
(详见《redis设计与实现》P57)
注意:添加节点、删除节点都可能引起连锁更新。但是这种情况发生的概率比较低,且一般连锁长度不会很长,所以造成性能问题的概率很低。我们可以放心使用而不需要过于担心性能问题。
5.压缩列表API
函数 | 作用 | 算法复杂度 |
---|---|---|
ziplistNew | 创建一个新的压缩列表。 | O(1) |
ziplistPush | 创建一个包含给定值的新节点, 并将这个新节点添加到压缩列表的表头或者表尾。 | 平均 O(N) ,最坏 O(N^2) 。 |
ziplistInsert | 将包含给定值的新节点插入到给定节点之后。 | 平均 O(N) ,最坏 O(N^2) 。 |
ziplistIndex | 返回压缩列表给定索引上的节点。 | O(N) |
ziplistFind | 在压缩列表中查找并返回包含了给定值的节点。 | 因为节点的值可能是一个字节数组, 所以检查节点值和给定值是否相同的复杂度为 O(N) , 而查找整个列表的复杂度则为 O(N^2) 。 |
ziplistNext | 返回给定节点的下一个节点。 | O(1) |
ziplistPrev | 返回给定节点的前一个节点。 | O(1) |
ziplistGet | 获取给定节点所保存的值。 | O(1) |
ziplistDelete | 从压缩列表中删除给定的节点。 | 平均 O(N) ,最坏 O(N^2) 。 |
ziplistDeleteRange | 删除压缩列表在给定索引上的连续多个节点。 | 平均 O(N) ,最坏 O(N^2) 。 |
ziplistBlobLen | 返回压缩列表目前占用的内存字节数。 | O(1) |
ziplistLen | 返回压缩列表目前包含的节点数量。 | 节点数量小于 65535 时 O(1) , 大于 65535 时 O(N) 。 |
因为 ziplistPush
、 ziplistInsert
、 ziplistDelete
和 ziplistDeleteRange
四个函数都有可能会引发连锁更新, 所以它们的最坏复杂度都是 O(N^2) 。
第八章:对象
redis五大对象:字符串对象(string)、列表对象(list)、哈希对象(hash)、集合对象(set)、有序集合对象(zset)。
1.对象的类型与编码
当我们创建一个键值对时,至少要创建两个对象:键和值。键总是一个字符串对象,因此使用TYPE命令时返回的总是键对应值的类型,而不是键的类型。
对象包含几个基本属性:
//位域
typedef struct redisObject{
unsigned type:4; //类型
unsigned encoding:4 //编码
void* ptr; //指向底层实现数据结构的指针
int refcount; //引用计数
unsigned lru:22; //记录了对象最后一次被命令访问的时间。22位长。
...
}robj;
对象的type记录了对象的类型。如下表:
类型常量 | 对象的名称 |
---|---|
REDIS_STRING | 字符串对象 |
REDIS_LIST | 列表对象 |
REDIS_HASH | 哈希对象 |
REDIS_SET | 集合对象 |
REDIS_ZSET | 有序集合对象 |
encoding属性记录了对象使用的编码,也就是底层数据结构。可以是下表中的其中一个:‘
编码常量 | 编码所对应的底层数据结构 |
---|---|
REDIS_ENCODING_INT | long类型的整数 |
REDIS_ENCODING_EMBSTR | embstr编码的简单动态字符串 |
REDIS_ENCODING_RAW | 简单动态字符串 |
REDIS_ENCODING_HT | 字典 |
REDIS_ENCODING_LINKEDLIST | 双端链表 |
REDIS_ENCODING_ZIPLIST | 压缩列表 |
REDIS_ENCODING_INTSET | 整数集合 |
REDIS_ENCODING_SKIPLIST | 跳跃表和字典 |
每种类型的对象都至少使用了两种不同的编码。
使用 object encoding 命令可以查看一个数据库键的值对象的编码。
对象所使用的底层数据结构 | 编码常量 | object encoding 命令输出 |
---|---|---|
整数 | REDIS_ENCODING_INT | "int" |
embstr编码的简单动态字符串(SDS) | REDIS_ENCODING_EMBSTR | “embstr” |
简单动态字符串(好像也是SDS) | REDIS_ENCODING_RAW | “raw” |
字典 | REDIS_ENCODING_HT | "hashtable" |
双端链表 | REDIS_ENCODING_LINKEDLIST | “linkedlist” |
压缩列表 | REDIS_ENCODING_ZIPLIST | "ziplist" |
整数集合 | REDIS_ENCODING_INTSET | "intset" |
跳跃表和字典 | REDIS_ENCODING_SKIPLIST | "skiplist" |
2.字符串对象
字符串对象是唯一一种会被其他四种对象嵌套的对象
2.1.编码
字符串对象的编码可以是:int、raw、embstr 。
- 如果一个字符串对象保存的是整数值,且可以用long表示,则字符串对象就可以直接保存在ptr属性里面(将void*转换成long),并将对象的编码设置为int。
- 如果一个字符串对象保存的是一个字符串值,且长度>39字节,则使用一个简单动态字符串SDS来保存。并将编码设置为raw。
- 如果一个字符串对象保存的是一个字符串值,且长度<=39字节则使用embstr编码。
- 浮点数也是以字符串类型保存的
embstr编码是专门用来保存短字符串的一种优化编码方式。
embstr和raw的区别是:
embstr | raw | |
---|---|---|
适用的字符串长度 | 短 | 长 |
创建时的内存分配次数 | 一次内存分配来为redisObject和sdshdr结构分配空间,这两个结构存在于一块连续空间上。 | 两次内存分配来分别为redisObject和sdshdr结构分配空间。两个结构存在于两块不连续空间上。 |
释放时内存释放次数 | 一次 | 两次 |
redisObject和sdshdr结构空间是否连续 | 是 | 否 |
性能 | 由于内存连续,可以很好地利用缓存带来的优势。内存分配和释放次数少。 | 与embstr相反 |
修改 | 修改时会自动转为raw编码 | 不发生转码 |
embs和raw的共同点是:
都使用redisObject+sdshdr结构来表示字符串对象。
执行命令时效果是相同的(但是修改操作会使embstr转码)
2.2.编码转换
对于int类型的字符串对象来说,如果我们进行的一些操作使得这个对象保存的不再是整数值,而是一个字符串值,那么编码就会从int转为raw。
例如使用APPEND向一个int类型的字符串对象追加一个值。由于追加只能对字符串值执行,所以需要先将int转为字符串值,再执行追加操作。执行的结果就是一个raw编码的字符串对象。(博主实验过了,如果向一个int编码的字符串对象"1"追加一个整数"2",最终还是会变为raw编码)。
2.3.字符串命令的实现
命令 | int 编码的实现方法 | embstr 编码的实现方法 | raw 编码的实现方法 |
---|---|---|---|
SET | 使用 int 编码保存值。 | 使用 embstr 编码保存值。 | 使用 raw 编码保存值。 |
GET | 拷贝对象所保存的整数值, 将这个拷贝转换成字符串值, 然后向客户端返回这个字符串值。 | 直接向客户端返回字符串值。 | 直接向客户端返回字符串值。 |
APPEND | 将对象转换成 raw 编码, 然后按 raw编码的方式执行此操作。 | 将对象转换成 raw 编码, 然后按 raw编码的方式执行此操作。 | 调用 sdscatlen 函数, 将给定字符串追加到现有字符串的末尾。 |
INCRBYFLOAT | 取出整数值并将其转换成 longdouble 类型的浮点数, 对这个浮点数进行加法计算, 然后将得出的浮点数结果保存起来。 | 取出字符串值并尝试将其转换成long double 类型的浮点数, 对这个浮点数进行加法计算, 然后将得出的浮点数结果保存起来。 如果字符串值不能被转换成浮点数, 那么向客户端返回一个错误。 | 取出字符串值并尝试将其转换成 longdouble 类型的浮点数, 对这个浮点数进行加法计算, 然后将得出的浮点数结果保存起来。 如果字符串值不能被转换成浮点数, 那么向客户端返回一个错误。 |
INCRBY | 对整数值进行加法计算, 得出的计算结果会作为整数被保存起来。 | embstr 编码不能执行此命令, 向客户端返回一个错误。 | raw 编码不能执行此命令, 向客户端返回一个错误。 |
DECRBY | 对整数值进行减法计算, 得出的计算结果会作为整数被保存起来。 | embstr 编码不能执行此命令, 向客户端返回一个错误。 | raw 编码不能执行此命令, 向客户端返回一个错误。 |
STRLEN | 拷贝对象所保存的整数值, 将这个拷贝转换成字符串值, 计算并返回这个字符串值的长度。 | 调用 sdslen 函数, 返回字符串的长度。 | 调用 sdslen 函数, 返回字符串的长度。 |
SETRANGE | 将对象转换成 raw 编码, 然后按 raw编码的方式执行此命令。 | 将对象转换成 raw 编码, 然后按 raw编码的方式执行此命令。 | 将字符串特定索引上的值设置为给定的字符。 |
GETRANGE | 拷贝对象所保存的整数值, 将这个拷贝转换成字符串值, 然后取出并返回字符串指定索引上的字符。 | 直接取出并返回字符串指定索引上的字符。 | 直接取出并返回字符串指定索引上的字符。 |
3.列表对象
3.1.编码
列表对象的编码可以是ziplist或者linkedlist,redisObject结构中的ptr属性指向一个ziplist或linkedlist。
ziplist使用压缩列表来实现。每个压缩列表节点(entry)保存了一个列表元素(元素是简单字符串而不是对象,也不是sds)。
linkedlist使用双端链表作为底层实现。每个节点都保存了一个字符串对象。(请注意保存的是对象)
(这里字符串对象被嵌套了)
3.2.编码转换
当列表对象同时满足下列两个条件时,列表对象使用ziplist编码:
- 所有字符串元素长度均<64字节
- 元素数量<512个
不能够同时满足上述两个条件的列表对象使用linkedlist编码。
注意:上述两个条件的上限值可以修改,具体看配置文件中关于list-max-ziplist-value选项和list-max-ziplist-entries选项的说明。
3.3列表命令的实现
命令 | 功能 |
---|---|
LPUSH | 将元素压入表头 |
RPUSH | 将元素压到表尾 |
LPOP | 返回表头,并删除表头节点 |
RPOP | 返回表尾,并删除表尾节点 |
LINDEX | 返回索引指定的节点 |
LLEN | 返回表长 |
LINSERT | 插入一个新节点到指定位置。(ziplist编码下如果位置为头或尾,则跟push命令相同) |
LREM | 遍历列表,删除包含给定元素的节点 |
LTRIM | 删除列表中所有不在索引范围内的节点 |
LSET | 更新节点值 |
4.哈希对象
4.1.编码
编码可以是ziplist或者hashtable。
ziplist编码的哈希对象使用压缩列表作为底层实现。列表中键值对的保存方式是:先存一个键,再存一个值,两个节点位置一前一后:
键 | 值 |
4.2编码转换
当哈希对象同时满足下列两个条件时,哈希对象使用ziplist编码:
- 键和值都<64字节。
- 键值对数量<512个。
不能同时满足的则使用hashtable编码。
这两个条件的上限值也是可以修改的,具体看配置文件中的hash-max-ziplist-value选项和hash-max-ziplist-entries选项的说明。
4.3哈希命令的实现
命令 | 功能 |
---|---|
HSET | 将一个键值对加入到哈希对象中 |
HGET | 查找给定键对应的值 |
HEXISTS | 查询键是否存在 |
HDEL | 将给定键对应的键值对删掉 |
HLEN | 返回键值对的数量 |
HGETALL | 返回所有键和值 |
5.集合对象
5.1.编码
集合对象的编码可以是intset或者hashtable。
intset编码的集合使用整数集合作为底层实现。
hashtable编码的结合使用字典作为底层实现。字典中每一个键都是一个字符串对象,每个字符串对象包含了一个元素。字典的值全被设置为NULL。
两种编码的结构如图:
5.2编码的转换
当集合对象可以同时满足以下两个条件时,对象使用intset编码:
- 所有元素都是整数值
- 元素数量<=512个
不能同时满足这两个条件的集合对象需要使用hashtable编码。
注意:第二个条件的上限值是可以更改的。具体请看配置文件中关于set-max-intset-entries选项的说明。
5.3集合命令的实现
命令 | 功能 |
---|---|
SADD | 将键值对添加到集合中 |
SCARD | 返回元素数目 |
SISMEMBER | 在字典的键中查找给定元素,判断是否存在 |
SMEMBERS | 遍历整个集合,返回所有元素 |
SRANDMEMBER | 随机返回一个元素 |
SPOP | 随机返回一个元素并将其从集合中删掉。 |
SREM | 删除指定元素 |
6.有序集合对象
6.1编码
有序集合的编码可以是ziplist或者skiplist。
(1) ziplist编码
ziplist编码的有序集合对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素成员(member),第二个元素则保存元素的分值(score)。
压缩列表内的集合元素按分值从小到大排序。
(2) skiplist编码
skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表:
typedef struct zset{
zskiplist *zsl; //跳跃表
dict *dict; //字典
}
zsl跳跃表按照分值从小到大保存了所有元素{*object,*score}。
dict字典创建了一个从成员到分值的映射,允许以O(1)的复杂度查询任意成员对应的分值。其中键是成员,值是分值。
注意:有序集合每个成员都是一个字符串对象,分值是double类型的浮点数。zsl和dict这两个结构会通过指针来共享相同元素的成员和分值。所以不会产生任何重复成员或分值。也就不会浪费内存。
为什么同时采用这两种数据结构?
- zsl跳跃表可以快速地完成范围性操作。但是查分值这个操作需要O(logN)的复杂度。
- dict字典可以O(1)完成查找分值,但是范围性操作需要先排序,至少额外花费O(NlogN)的时间复杂度和O(N)的空间复杂度(因为要额外保存一个有序数组)。
所以为了同时保留两个结构的优点,就如此设计了。
(其实我暂时想不明白为什么要单独查询分值,分值不只是在比较的时候才有意义吗?)
6.2编码的转换
当有序集合同时满足下列两个条件时,对象采用ziplist编码:
- 元素数量<128。
- 所有元素成员长度<64字节。
不能同时满足这两个条件的使用skiplist编码。
注意:以上两个条件的上限值是可以修改的,具体请看配置文件中关于zset-max-ziplist-entries选项和zset-max-ziplist-value选项的说明。
6.3有序集合命令的实现
命令 | 功能 |
---|---|
ZADD | 将键值对添加到有序集合中 |
ZCARD | 获取元素数目 |
ZCOUNT | 遍历有序列表,统计分值在给定范围内的节点数量 |
ZRANGE | 遍历列表,返回给定索引范围内的所有元素(从表头开始遍历) |
ZREVRANGE | 遍历列表,返回给定索引范围内的所有元素(从表尾开始遍历) |
ZRANK | 查找给定成员对应的排名。(从小到大的排名) |
ZREVRANK | 查找给定成员对应的排名。(从大到小的排名) |
ZREM | 删除给定成员 |
ZSCORE | 返回成员对应的分值 |
7.类型检查与命令多态
redis用于操作键的命令基本可以分为两类:
- 可以对任何类型的键执行的命令,比如DEL、EXPIRE、RENAME、TYPE、OBJECT。
- 只能对待特定类型的键执行的命令。
命令检查是依靠对象的type属性来检查的。
命令的多态:命令还会根据对象的编码方式自动选择正确的操作。
8.内存回收
依靠对象的refcount属性(引用计数)。初始化对象时使refcount=1。当refcount为0时就会被释放。
9.对象共享
对象共享也跟引用计数有关,共享一次refcount就加一。
目前来说,redis会在初始化服务器的时候自动创建一万个字符串对象,包含了0~9999的所有整数值用于共享。
对于字符串对象来说,redis只共享整数类型的字符串对象,这是出于时间复杂度考虑。
10.对象空转时长
对象属性 lru 记录了最后一次被命令访问的时间。用于计算空转时间。
OBJECT IDLETIME命令可以查看空转时长(=当前时间-上次访问的时间)。注意:这个命令比较特殊,不修改lru属性。
空转时长可以用来为内存回收算法提供依据。当内存回收算法为volatile-lru或者allkeys-lru时,当服务器占用的内存数超过了maxmemory选项所设置的上限值时,空转时长较高的那部分键会被优先释放,从而回收内存