Redis设计与实现 读后总结
- 数据结构与对象
- 简单动态字符串(SDS,simple dynamic string)
- 定义
- 简单动态字符串(SDS,simple dynamic string)
struct sdshdr{
int len; 保存的字符串长度。
int free; 记录buf数组中未使用字节的数量。
char buf[];用于保存字符串,最后一个字节保存空字符串’\0’。
}
-
-
- SDS与C字符串的区别:
- 常数复杂度获取字符串长度。
- 杜绝缓冲区溢出。当SDS API需要对SDS进行修改时,API会先检查SDS的空间是否满足修改的所需的要求,如果不满足的话,API就会自动将SDS的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以使用SDS既不需要手动修改SDS的空间大小,也不会出现缓冲区溢出的问题。
- 减少了修改字符串时带来的内存重分配次数。
- 空间预分配:
- 如果对SDS进行修改之后,SDS的长度将小于1MB,程序分配和len相同大小的未使用空间(free)。
- 如果对SDS进行修改之后,SDS的长度大于1MB,那么程序会分配1MB的未使用空间(free)。
- 惰性空间释放:
- 空间预分配:
- SDS与C字符串的区别:
-
当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用。
SDS也提供了相应的API,让我们可以在有需要时,真正地释放SDS的未使用空间,所以不用担心惰性空间释放策略会造成内存浪费。
-
-
- 二进制安全
-
使用Redis不仅可以保存文本数据,还可以保存任意格式的二进制数据。
-
-
- 兼容部分C字符串函数
- SDS API
-
|
函数 |
作用 |
时间复杂度 |
|
sdnew |
创建一个包含给定C字符串的SDS |
O(N),N为给定的C字符串长度。 |
|
sdsempty |
创建一个不包含任何内容的空SDS |
O(1) |
|
sdsfree |
释放给定的SDS |
O(N),N为被释放SDS的长度。 |
|
sdslen |
返回给定的SDS |
O(1) |
|
sdsavail |
返回SDS的未使用空间字节数 |
O(1) |
|
sdsdup |
创建一个给定SDS的副本(copy) |
O(N),N为给定SDS的长度。 |
|
sdsclear |
清空SDS保存的字符串内容 |
O(1),因为惰性空间释放策略 |
|
sdscat |
将给定C字符串拼接到SDS字符串的末尾 |
O(N) |
|
sdscatsds |
将给定SDS字符串拼接到另一个SDS字符串的末尾 |
O(N) |
|
sdscpy |
将给定的C字符串复制到SDS里面,覆盖SDS原有的字符串。 |
O(N) |
|
sdsgrowzero |
用空字符串将SDS扩展至给定长度。 |
O(N),N为扩展新增的字节数。 |
|
sdsrange |
保留SDS给定区间内的数据,不在区间内的数据会被覆盖或清除。 |
O(N) |
|
sdstrim |
接收一个SDS和一个C字符串作为参数,从SDS左右两端分别移除所有在C字符串中出现过的字符。 |
O(M*N) |
|
sdscmp |
对比两个SDS字符串是否相同。 |
O(N),N为两个SDS中较短的那个SDS长度 |
- 链表
链表提供了高效的节点重排序能力,以及顺序性的节点访问方式,并且通过增删节点来灵活地调整链表的长度。
- 实现
链表节点
typeof struct listNode{
struct listNode *prev; 前置节点
struct listNode *next; 后置节点
void *value;节点的值
}
链表
typeof struct list{
listNode *head;表头节点
listNode *tail;表尾节点
unsigned long len;链表所包含的节点数
void *(*dup)(void *ptr);节点值复制值函数
void *(*free)(void *ptr);节点值释放函数
int (*match)(void *ptr,void *key);节点值对比函数
}
- 实现特性
- 双端:链表节点带有prev和next指针,获取某个节点的前置节点和后置节点的复杂度都是O(1)。
- 无欢:表头节点的prev指针和表尾节点的next指针都指向NULL,对链表的访问以NULL为终点。
- 带表头指针和表尾指针:通过list结构的head指针和tail指针,程序获取链表的表头节点和表尾节点的复杂度为O(1)。
- 带链表长度计数器:程序使用list结构的len属性来对list持有的链表节点进行计数,程序获取链表中节点数量的复杂程度为O(1)。
- 多态:链表节点使用void*指针来保存节点值,并且可以通过list结构的dup、free、match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。
- Api
|
函数 |
作用 |
时间复杂度 |
|
listSetDupMethod |
将给定的函数设置为链表的节点值复制函数 |
O(1) |
|
listGetDupMethod |
返回链表当前正在使用的节点值释放函数 |
复制函数可以通过链表的dup属性直接获得, O(1) |
|
listSetFreeMethod |
将给定的函数设置为链表的节点释放函数 |
O(1) |
|
listGetFree |
返回链表当前正在使用的节点值释放函数 |
释放函数可以通过链表的free属性直接获得, O(1) |
|
listSetMatchMethod |
将给定的函数设置为链表的节点值对比函数 |
O(1) |
|
listGetMatchMethod |
返回链表当前正在使用的节点值对比函数 |
对比函数可以通过链表的match属性直接获得,O(1) |
|
listLength |
返回链表的长度(包含了多少个节点) |
链表长度可以通过链表的len属性直接获得, O(1) |
|
listFirst |
返回链表的表头节点 |
表头节点可以通过链表的head属性直接获得, O(1) |
|
listLast |
返回链表的表尾节点 |
表尾节点可以通过链表的tail属性直接获得, O(1) |
|
listPrevNode |
返回给定节点的前置节点 |
前置节点可以通过节点的prev属性直接获得, O(1) |
|
listNextNode |
返回给定节点的后置节点 |
后置节点可以同链表的next属性直接获得, O(1) |
|
listNodeValue |
返回给定节点目前正在保存的值 |
节点值可以通过节点的value属性直接获得, O(1) |
|
listCreate |
创建一个不包含任何节点的新链表 |
O(1) |
|
listAddNodeHead |
将一个包含给定值的新节点添加到给定链表的表头 |
O(1) |
|
listAddNodeTail |
将一个包含给定值的新节点添加到给定链表的表尾 |
O(1) |
|
listInsertNode |
将一个包含给定值的新节点添加到给定节点的之前或之后 |
O(1) |
|
listSearchKey |
查找并返回链表中包含的给定值的节点 |
O(N),N为链表长度 |
|
listIndex |
返回链表在给定索引上的节点 |
O(N),N为链表长度 |
|
listDelNode |
从链表中删除给定的节点 |
O(N),N为链表长度 |
|
listRotate |
将链表的表尾节点弹出,然后将被弹出的节点插入到链表的表头,成为新的 表头节点 |
O(1) |
|
listDup |
复制一个给定链表的副本 |
O(N),N为链表长度 |
|
listRelease |
释放给定链表,以及链表中的所有节点 |
O(N),N为链表长度 |
- 字典
字典,又称为符号表(symbol table)、关联数组(associative array)或映射(map)、是一种用于保存键值对(key-value pair)的抽象数据结构。
- 实现
字典使用哈希表作为底层实现。
-
-
- 哈希表
-
Typedef struct dictht{
dictEntry *table;哈希表数组
unsigned long size;哈希表大小
unsigned long sizemask;哈希表 大小掩码用于计算索引值,总等于size-1
unsigned long used;该哈希表已有节点数量
}
-
-
- 哈希表节点
-
typedef struct dictEntry{
void *key;键
union{
void *val;
uint64_t u64;
int64_t s64;
}v;
struct dictEntry *next;指向下个哈希表节点,形成链表
}
next属性指向另一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对连接在一起,以此来解决键冲突问题。
-
-
- 字典
-
typedef struct dict{
dictType *type;类型特定函数
void *privdata;私有数据
dictht ht[2];哈希表
}
type保存了一簇用于操作特定类型键值对的函数。
typedef struct dicType{
unsigned int (*hashFuction) (const void *key);计算哈希值的函数
void *(keyDup)(void *privdata,const void *key);复制键的函数
void *(keyDup)(void *privdata,const void *obj);复制值的函数
int (*keyCompare) (void *privdata,const void *key1,const *key2); 对比键的函数
void (*keyDestructor) (void *privdata, void *key);销毁键的函数
void (*valDestructor) (void *privdata,void *obj);销毁值的函数
}
一般字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]哈希表进行rehash时使用。
-
-
- 哈希算法
-
当一个新的键值对添加到字典里面时,程序需要现根据键值对的键计算出哈希值和索引值,然后根据索引值,将包含新键值对的哈希表节点放到哈希表数组的索引上面。
使用字典设置的哈希函数,计算键key的哈希值:
hash = dict->type->hashFunction(key);
使用hash表的sizemask属性和哈希值,计算出索引值,根据情况的不同,ht[x]可以是ht[0]或者ht[1]:
index = hash & dict->ht[x].sizemask
-
-
- 解决键冲突
-
当两个或者两个以上的键被分配到了哈希表数组的同一个索引上面时,我们就称这些键发生了冲突(collision)。
Redis的哈希表使用链地址法(separate chaining)来解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向的链表,被分配到同一索引上的多个节点可以用这个单向链表连接起来,这就解决了键冲突问题。
-
-
- Rehash
-
为了让哈希表的负载因子(load factor)维持在一个合理的范围内,当哈希表保存的键值对数量太多或者太少时,程序需要对哈希表的大小进行相应的扩展和收缩。
Rehash步骤:
- 为字典的ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及ht[0]当前包含的键值对数量(ht[0].used属性)
- 如果执行的是扩展操作,那么ht[1]的大小为第一个大于等于ht[0].used*2的2n 。
- 如果执行的是收缩操作,那么ht[1]的大小为第一个大于等于ht[0].used的2n 。
- 当ht[0]包含的所有键值对都迁移到了ht[1]之后(ht[0]变为空表),释放ht[0],将ht[1],设置为ht[0],并在ht[1]新创建一个空白哈希表,为下一次rehash做准备。
哈希表的扩展和收缩:
当以下任意条件被满足时,程序会自动对哈希表进行扩展操作。
- 服务器目前没有在执行BGSAVE命令或者 BGREWRITEAOF命令,并且哈希表的负载因子大于等于1。
- 服务器目前正在执行BGSAVE命令或者 BGREWRITEAOF命令,并且哈希表的负载因子大于等于5。
- 当哈希表的负载因子小于0.1时,程序自动开始对哈希表执行收缩操作。
负载因子公式:
负载因子 = 哈希表已保存节点数量/哈希表大小
Load_ffactor = ht[0].used / ht[0].size
大多数操作系统采用写时复制(copy-on-write)技术来优化子进程的使用效率,所以在子进程存在期间,服务器会提高执行扩展操作所需的负载因子,从而尽可能地避免在子进行存在期间进行哈希表的扩展操作,这可以避免不必要的内存写入操作,最大限度地节约内存。
渐进式rehash:
步骤:
- 为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表。
- 在字典中维持一个索引计数器变量rehashidx,并将它设置为0,表示rehash工作正式开始。
- 在rehash进行期间,每次对字典添加、删除、查找或者更新操作时,程序除了执行指定的操作外,还会将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当rehash工作完成之后,程序将rehashidx属性值增1。
- 随着字典操作的不断执行,最终在某个时间点上,ht[0]的所有键值对都会被rehash到ht[1],这时候程序将rehashidx属性设置为-1,表示rehash操作已经完成。
渐进式rehash执行期间的哈希表操作:
- 查找操作:先在ht[1]中进行查找,若没有,则在ht[0]中进行查找。
- 渐进式rehash期间,新添加的到字典的键值对一律会保存到ht[1]。
-
- API
-
|
函数 |
作用 |
时间复杂度 |
|
dictCreate |
创建一个新字典 |
O(1) |
|
dictAdd |
将给定的键值对添加到字典里面 |
O(1) |
|
dictReplace |
将给定的键值对添加到字典里面,如果键已经存在于字典,那么用新值取代原值 |
O(1) |
|
dictFetchValue |
返回给定键的值 |
O(1) |
|
dictGetRandomKey |
从字典里随机返回一个键值对 |
O(1) |
|
dictDelete |
从字典中删除给定键所对应的键值对 |
O(1) |
|
dictRelease |
释放给定字典,以及字典中包含的所有键值对 |
O(N),N为字典中包含的键值对数量 |
- 跳跃表
跳跃表(skiplist)是一种有序数据结构,他通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
-
- 跳跃表节点
Typedef struct zskiplistNode{
struct zskiplistNode *backward;后退指针
double score;分值
robj *obj;成员对象
struct zskiplistLevel{
struct zskiplistNode *forward;前进指针
unsigned int span;跨度
}level[];层
}
-
- API
|
函数 |
作用 |
时间复杂度 |
|
zslCreate |
创建一个新的跳跃表 |
O(1) |
|
zslFree |
释放给定跳跃表,以及表中包含的所有节点 |
O(N),N为跳跃表的长度 |
|
zslInsert |
将包含给定成员和分值的新节点添加到跳跃表中 |
平均O(logN),最坏O(N),N为跳跃表长度 |
|
zslDelete |
删除跳跃表中包含给定成员和分值的节点 |
平均O(logN),最坏O(N),N为跳跃表长度 |
|
zslGetRank |
返回包含给定成员和分值的节点在跳跃表中的排位 |
平均O(logN),最坏O(N),N为跳跃表长度 |
|
zslGetElementByRank |
返回跳跃表在给定排位上的节点 |
平均O(logN),最坏O(N),N为跳跃表长度 |
|
zslIsInRange |
给定一个分值范围(range),如果给定的分值范围包含在跳跃表的分值范围内,那么返回1,否则返回0 |
通过跳跃表的表头节点和表尾节点,这个检测可以用O(1)复杂度完成 |
|
zslFirstInRange |
给定一个分值范围,返回跳跃表中第一个符合这个范围的节点 |
平均O(logN),最坏O(N),N为跳跃表长度 |
|
zslLastInRange |
给定一个分值范围,返回跳跃表中最后一个符合这个范围的节点 |
平均O(logN),最坏O(N),N为跳跃表长度 |
|
zslDeleteRangeByScore |
给定一个分值范围,删除跳跃表中所有在这个范围内的节点 |
O(N),N为被删除节点的长度 |
|
zslDeleteRangeByRank |
给定一个排位范围,删除跳跃表中所有在这个范围内的节点 |
O(N),N为被删除节点的长度 |
- 整数集合
整数集合(intset)是集合键底层实现之一,当一个结合包含整数值元素,并且这个集合的元素数量不多时,redis就会使用整数集合作为集合键的底层实现。
可以保存类型为int16_t、int32_t、int64_t的整数值。
- 实现
typedef struct intset{
uint32_t encoding;编码方式
uint32_t length;集合包含的元素数量
int8_t contents[];保存元素的数组
}
- 升级
每当将一个新元素添加到整数集合中,并且新元素的类型比整数集合现有所有元素的类型都长时,整数集合需要先进行升级(update),然后才能将新元素添加到整数集合中。
步骤:
-
- 根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间。
- 将底层数组现有的所有元素都转化成与新元素相同的类型,并将类型转化后的元素放置到正确的位上,而且在放置元素的过程中,需要继续维持底层数组的有序性质不变。
- 将新元素添加到底层数组里面。
升级之后新元素的摆放位置:
因为引发升级的新元素的长度总是比整数集合现有所有元素的长度都大,所以这个新元素的值要么就大于所有现有元素,要么就小于所有现有元素:
- 在不在新元素小于所有现有元素的情况下,新元素会被放置在底层数组的最开头(索引0)。
- 在新元素大于所有现有元素的情况下,新元素会被放置在底层数组的最末尾(索引length-1)。
升级的好处:
- 提升灵活性。
- 节约内存。
- 降级
整数集合不支持降级操作,一旦对数组进行了升级,编码就会一直保持升级后的状态。
- API
|
函数 |
作用 |
时间复杂度 |
|
intsetNew |
创建一个新的整数集合 |
O(1) |
|
intsetAdd |
将给定元素添加到整数集合里面 |
O(N) |
|
intsetRemove |
从整数集合中移除给定元素 |
O(N) |
|
intsetFind |
检查给定值是否存在于集合 |
因为底层数组有序,查找可以通过二分法查找法来进行,所以时间复杂度为O(logN) |
|
intsetRandom |
从整数集合中随机返回一个元素 |
O(1) |
|
intsetGet |
取出底层数组在给定索引上的元素 |
O(1) |
|
intsetLen |
返回整数集合包含的元素个数 |
O(1) |
|
intsetBlobLen |
返回整数集合占用的内存字节数 |
O(1) |
- 压缩列表
压缩列表(ziplist)是列表键和哈希键的底层实现之一。
当一个列表键只包含少量列表项,并且每个列表项要么是小整数值,要么为长度比较短的字符串,那么redis就会使用压缩列表来做列表键的底层实现。
当一个哈希键只包含少量键值对,并且每个键值对的键和值要么是小整数值,要么为长度比较短的字符串,那么redis就会使用压缩列表来做哈希键的底层实现。
-
- 压缩列表的构成
压缩列表是redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构。
压缩列表各个组成部分:
|
zlbytes |
zltail |
zllen |
entry1 |
entry2 |
... |
entryN |
zlend |
各组成部分详细说明:
|
属性 |
类型 |
长度 |
用途 |
|
zlbytes |
uint32_t |
4字节 |
记录整个压缩列表占用的内存字节数:在对压缩列表进行内存重分配,或者计算zlend的位置时使用。 |
|
zltail |
uint32_t |
4字节 |
记录压缩列表表尾节点距离压缩列表的起始地址有多少字节:通过这个偏移量,程序无须遍历整个压缩列表就可以确定表尾节点的地址 |
|
zllen |
uint16_t |
2字节 |
记录了压缩列表包含的节点数量:当这个属性的值小于UINT16_MAX(65535)时,这个属性的值就是压缩列表包含节点的数量;当这个值等于UINT16_MAX时,节点的真是数量需要遍历整个压缩列表才能计算出来。 |
|
entryX |
列表节点 |
不定 |
压缩列表包含的各个节点,节点的长度由节点保存的内容决定。 |
|
zlend |
uint8_t |
1字节 |
特殊值0xFF(十进制255),英语标记压缩列表的末端。 |
-
- 压缩列表节点的构成
各个组成部分:
|
previous_entry_length |
encoding |
content |
压缩列表节点可以保存一个字节数组或者一个整数值。
字节数组可以是以下三种长度之一:
- 长度小于等于63(26-1)字节的字节数组。
- 长度小于等于16383(214-1)字节的字节数组。
- 长度小于等于4294967295(232-1)字节的字节数组。
整数值为以下六种之一:
(1)4位长,介于0至12之间的无符号整数。
(2)1字节长的有符号整数。
(3)3字节长的有符号整数。
(4)int16_t类型整数。
(5)int32_t类型整数。
(4)int64_t类型整数。
previous_entry_length:
节点的previous_entry_length属性以字节为单位,记录了压缩列表中前一个节点的长度。
- 如果前一个节点的长度小于254字节,那么previous_entry_length属性的长度为1字节:前一节点的长度就保存在这一个字节里面。
- 如果前一个节点的长度大于等于254字节,那么previous_entry_length属性的长度为5字节:其中属性的第一字节会被设置为0xFE(十进制254),之后的四个字节则用于保存前一个节点的长度。
因为节点的previous_entry_length属性记录了前一个节点 的长度,所有程序可以通过指针运算,根据当前节点的起始地址来计算出前一个节点的起始地址。
encoding:
节点的encoding属性记录了节点的content属性所保存数据的类型以及长度:
- 一字节、两字节或者五个字节长,值的最高位为00、01或者10的是字节数组编码:这种编码表示节点的content属性保存着字节数组,数组的长度由编码除去最高两位之后的其他位记录。
- 一字节长,值的最高位以11开头是整数编码:这种编码表示节点的content属性保存着整数值,整数值的类型和长度由编码除去最高两位之后的其他位记录。
content:
节点的content属性负责保存节点的值,节点值可以是一各字节数组或者整数,值的类型和长度由节点的encoding属性决定。
-
- 连锁更新
因为连锁更新在最坏的情况下需要对压缩列表执行N次空间重分配操作,而每次空间重分配的最坏复杂度为O(N),所以连锁更新的最坏时间复杂度为O(N2)。
尽管连锁更新的时间复杂度较高,但是真正造成性能问题的几率很低:
- 首先,压缩列表里要恰好有多个连续的、长度介于250字节至253字节之间的节点,连锁更新才有可能被引发,在实际中,这种情况并不多见。
- 其次,即使出现连锁更新,但只要更新的节点数量不多,就不会对性能造成任何影响。
- API
|
函数 |
作用 |
算法复杂度 |
|
ziplistNew |
创建一个新的压缩列表 |
O(1) |
|
ziplistPush |
创建一个包含给定值的新节点,并将这个新节点添加到压缩列表的表头或者表尾 |
平均O(N),最坏O(N2) |
|
ziplistInsert |
将包含给定值的新节点插入到给定节点之后 |
平均O(N),最坏O(N2) |
|
ziplistIndex |
返回压缩列表给定索引上的节点 |
O(N) |
|
ziplistFind |
在压缩列表中查找并返回包含了给定值的节点 |
因为节点的值可能是一个字节数组,所以检查节点值和给定值是否相同的复杂度为O(N),而查找整个列表的复杂度则为O(N2) |
|
ziplistNext |
返回给定节点的下一个节点 |
O(1) |
|
ziplistPrev |
返回给定节点的前一个节点 |
O(1) |
|
ziplistGet |
获取给点节点所保存的值 |
O(1) |
|
ziplistDelete |
从压缩列表中删除给定的 节点 |
平均O(N),最坏O(N2) |
|
ziplistDeleteRange |
删除压缩列表在给定索引上的连续多个节点 |
平均O(N),最坏O(N2) |
|
ziplistBlobLen |
返回压缩列表目前占用的内存字节数 |
O(1) |
|
ziplistLen |
返回压缩列表目前包含的节点数量 |
节点数量小于65535时为O(1),大于65535时为 O(N) |
- 对象
- 结构
typedef struct redisObject{
unsigned type:4;类型
unsigned encoding:4;编码
void *ptr;指向底层实现数据结构的指针
...
}
类型:
|
类型常量 |
对象的名称 |
|
REDIS_STRING |
字符串对象 |
|
REDIS_LIST |
列表对象 |
|
REDIS_HASH |
哈希对象 |
|
REDIS_SET |
集合对象 |
|
REDIS_ZSET |
有序集合对象 |
对象的编码:
|
编码常量 |
编码所对应的底层数据结构 |
|
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 |
跳跃表和字典 |
不同类型和编码对象:
|
类型 |
编码 |
对象 |
|
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 |
使用跳跃表和字典实现的有序集合对象 |
-
- 字符串对象
- 如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型表示,那么该字符串对象被保存为int类型。
- 如果字符串对象保存的是一个字符串值,并且这个字符串值的长度小于等于39(3.2版本为44),那么该字符串保存为embstr类型,否则为raw类型。
- 字符串对象
embstr相对于raw的好处:
- embstr编码将创建字符串对象所需的内存分配次数从raw的两次降为1次(raw编码调用两次内存分配创建redisObject结构和sdshdr结构,而embstr编码则通过调用一次内存分配函数来分配一块连续的空间,空间中依次包含redisObject和sdshdr)。
- 释放embstr编码的字符串对象只需要调用一次内存释放函数,而释放raw编码的字符串对象需要调用两次内存释放函数。
- 因为embstr编码的字符串对象的所有数据都保存在一块连续的内存里面,所以这种编码的字符串对象比raw编码的字符串对象能够更好的利用缓存带来的优势。
编码的转换:
- int编码,向对象执行了一些命令,使得这个对象保存的不再是整数值,而是一个字符串值,那么字符串对象的编码将从int变为raw。
- Redis没有为embstr编码的字符串对象编写任何相应的修改程序,所以embstr实际上是只读的。
- 当修改embstr编码的字符串时,程序会先将对象编码从embstr转化为raw,然后再执行修改命令。
API:
|
命令 |
int编码的实现方式 |
embstr的实现方式 |
raw的实现方式 |
|
SET |
使用int编码保存值 |
使用embstr编码保存值 |
使用raw编码保存值 |
|
GET |
拷贝对象所保存的整数值,将这个拷贝转化成字符串值,然后向客户端返回这个字符串值 |
直接向客户端返回字符串值 |
直接向客户端返回字符串值 |
|
APPEND |
将对象转化成raw编码,然后按raw编码的方式执行此操作 |
将对象转化成raw编码,然后按raw编码的方式执行此操作 |
调用sdscatlen函数,将给定字符串追加到现有字符串末尾 |
|
INCRBYLOAT |
取出整数值并将其转化成long double类型的浮点数,对这个浮点数进行加法计算,然后将得出的浮点数结果保存起来 |
取出字符串值并尝试将其转化为long double类型的浮点数,对这个浮点数进行加法计算,然后将得出的浮点数结果保存起来。如果字符串值不能被转化成浮点数,那么向客户返回一个错误 |
取出字符串值并尝试将其转化为long double类型的浮点数,对这个浮点数进行加法计算,然后将得出的浮点数结果保存起来。如果字符串值不能被转化成浮点数,那么向客户返回一个错误 |
|
INCRBY |
对整数值进行加法计算,得出的计算结果会作为整数被保存起来 |
embstr编码不能执行此命令,向客户端返回一个错误 |
raw编码不能执行此命令,向客户端返回一个错误 |
|
DECRBY |
对整数值进行减法计算,得出的计算结果会作为整数被保存起来 |
embstr编码不能执行此命令,向客户端返回一个错误 |
raw编码不能执行此命令,向客户端返回一个错误 |
|
STRLEN |
拷贝对象所保存的整数值,将这个拷贝转化成字符串值,计算并返回这个字符串值的长度 |
调用sdslen函数,返回字符串长度 |
调用sdslen函数,返回字符串长度 |
|
SETRANGE |
将对象转化成raw编码,然后按raw编码的方式执行此命令 |
将对象转化成raw编码,然后按raw编码的方式执行此命令 |
将字符串特定索引上的值设置为给定字符 |
|
GETRANGE |
拷贝对象所保存的整数值,将这个拷贝转化成字符串值,然后取出并返回字符串指定索引上分字符 |
直接取出并返回字符串指定索引上的字符 |
直接取出并返回字符串指定索引上的字符 |
-
- 列表对象
列表对象的编码可以是ziplist或者linkedlist。
- ziplist编码的列表对象使用压缩列表作为底层实现,每个压缩列表节点(entry)保存了一个列表元素。
- linkedlist编码的列表对象使用双端链表作为底层实现,每个双端链表节点(node)都保存了一个字符串对象,而每个字符串对象都保存了一个列表元素。
编码转换:
- 列表对象保存的所有字符串元素的长度都下小于64字节。
- 列表对象保存的元素数量小于512个;不能满足这两个条件的列表对象需要使用linkedlist编码。
列表命令的实现:
|
命令 |
ziplist编码的实现方法 |
linkedlist编码的实现方法 |
|
LPUSH |
调用ziplistPush函数,将新元素推入到压缩列表表头 |
调用listAddNodeHead函数,将新元素推入到双端链表的表头 |
|
RPUSH |
调用ziplistPush函数,将新元素推入到压缩列表表尾 |
调用listAddNodeTail函数,将新元素推入到双端链表的表尾 |
|
LPOP |
调用ziplistIndex函数定位压缩列表的表头节点,在向用户返回节点所保存的元素之后,调用ziplistDelete函数删除表头节点 |
调用listFirst函数定位双端链表的表头节点,在向用户返回节点所保存的元素之后,调用listDelNode函数删除表头节点 |
|
RPOP |
调用ziplistIndex函数定位压缩列表的表尾节点,在向用户返回节点所保存的元素之后,调用ziplistDelete函数删除表尾节点 |
调用listLast函数定位双端链表的表尾节点,在向用户返回节点所保存的元素之后,调用listDelNode函数删除表尾节点 |
|
LINDEX |
调用ziplistIndex函数定位压缩列表中的指定节点,然后返回节点所保存的元素 |
调用listindex函数定位双端链表中的指定节点,然后返回节点所保存的元素 |
|
LLEN |
调用ziplistLen函数返回压缩列表的长度 |
调用listLength函数返回双端链表的长度 |
|
LINSERT |
插入新节点到压缩列表的表头或者表尾时,使用ziplistPush函数;插入新节点到压缩列表的其他位置时,使用ziplistInsert函数 |
调用listinsertNode函数,将新节点插入到双端链表的指定位置 |
|
LREM |
遍历压缩列表节点,并调用ziplistDelete函数删除包含了给定元素的节点 |
遍历双端链表节点,并调用listDelNode函数删除包含给定元素的节点 |
|
LTRIM |
调用ziplistDeleteRange函数,删除压缩列表中所有不在指定索引范围内的节点 |
遍历双端链表节点,并调用listDelNode函数删除链表中所有不在指定索引范围内的节点 |
|
LSET |
调用ziplistDelete函数,先删除压缩列表指定索引上的现有节点,然后调用ziplistInsert函数,将一个包含给定元素的新节点插入到相同索引上面 |
调用listIndex函数,定位到双端链表指定索引上的节点,然后通过赋值操作更新节点的值 |
-
- 哈希对象
哈希对象的编码可以使ziplist或者hashtable。
- ziplist编码的哈希对象使用压缩列表作为底层实现,每当有新的键值对要加入到哈希对象时,程序会先将保存了键的压缩列表节点推入到压缩列表表尾,然后再将保存了值的压缩列表节点推入到研所列表表尾。
- 保存了同一键值对的两个节点总是紧挨在一起,保存键的节点在前,保存值的节点在后;
- 先添加到哈希对象中的键值对会被放在压缩列表的表头方向,而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向。
- hashtable编码的哈希对象使用字典作为底层实现,哈希独享中的每个键值对都使用一个字典键值对来保存。
- 字典的每个键都是一个字符串对象,对象中保存了键值对键。
- 字典的每个值都是一个字符串对象,对象中保存了键值对的值。
编码转换:
当哈希对象可以同时满足一下两个条件时,哈希对象使用ziplist编码:
- 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节。
- 哈希对象保存的键值对的数量小于512个;不能满足这两个条件的哈希对象需要使用hashtable编码。
注:这两个上限值是可以修改的,具体看配置文件中hash-max-ziplist-value选项和hash-max-ziplist-entries选项。
- 哈希命令实现
|
命令 |
ziplist编码实现方法 |
Hashtable编码的实现方法 |
|
HSET |
首先调用ziplistPush函数,将键推入到压缩列表的表尾,然后再次调用ziplistPush函数,将值推入到压缩列表的表尾 |
调用dictAdd函数,将新节点添加到字典里面 |
|
HGET |
首先调用ziplistFind函数,在压缩列表中查找指定键所对应的节点,然后调用ziplistNext函数,将指针移动到键节点旁边的值节点,最后返回值节点 |
调用dictFind函数,在字典中查找给定键,然后调用dictGetVal函数返回该键所对应的值 |
|
HEXIST |
调用ziplistFind函数,在压缩列表中查找给定键所对应的节点,如果找到的话说明键值对存在,没找到的话就说明键值对不存在 |
调用dictFind函数,在字典中查找给定键,如果找到的话说明键值对存在,没找到的话就说明键值对不存在 |
|
HDEL |
调用ziplistFind函数,在压缩列表中查找给定键所对应的节点,然后将相应的键节点,以及键节点旁边的值节点都删除掉 |
调用dictDelete函数,将给定键所对应的键值对从字典中删除掉 |
|
HLEN |
调用ziplistLen函数,取得压缩列表包含节点的总数量,将这个数量除以2,得出的结果就是压缩列表保存的键值对数量 |
调用dictSize函数,返回字典包含的键值对数量,这个数量就是哈希对象包含的键值对数量 |
|
HGETALL |
遍历整个压缩列表,用ziplistGet函数返回所有键和值(都是节点) |
遍历整个字典,用dictGetKey函数返回字典的键,用dictGetVal函数返回字典的值 |
-
- 集合对象
集合对象的编码可以是intset或者hashtable。
- intset编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合里面。
- Hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而字典的值则全部设置为NULL。
编码转换:
当集合对象可以同时满足以下两个条件时,对象使用intset编码:
- 集合对象保存的所有元素都是整数值。
- 集合对象保存的元素数量不超过512个;不能满足以上两个条件的集合对象需要使用hashtable编码
注:第二个条件的上限可以修改,具体查看配置文件中关于set-max-intset-entries选项说明。
集合命令的实现:
|
命令 |
intset编码的实现方法 |
hashtable编码的实现方法 |
|
SADD |
调用intsetAdd函数,将所有鲜元素添加到整数集合里面 |
调用dictAdd函数,以新元素为键,NULL为值,将键值对添加到字典里面 |
|
SCARD |
调用intsetLen函数,返回整数集合所包含的元素数量,这个数量就是集合对象所包含的元素数量 |
调用dictSize函数,返回字典所包含的键值对数量,这个数量就是集合对象所包含的元素数量 |
|
SISMEMBER |
调用intsetFind函数,在整数集合中查找给定的元素,如果找到了说明元素存在于集合,没找到则说明 元素不存在于集合 |
调用dictFind函数在,在字典的键中查找给定的元素,如果找到了说明元素存在于集合,没找到则说明元素不存在与集合 |
|
SMEMBERS |
遍历整个整数集合,使用intsetGet函数返回集合元素 |
遍历整个字典,使用dictGetKey函数返回字典的键作为集合元素 |
|
SRANDMEMBER |
调用intsetRandom函数,从整数集合中随机返回一个元素 |
调用dictGetRandomKey函数,从字典中随机返回一个字典键 |
|
SPOP |
调用intsetRandom函数,从整数集合中随机取出一个元素,将这个随机元素返回给客户端之后,调用intsetRemove函数,将随机元素从整数集合中删除掉 |
调用dictGetRandomKey函数,从字典中随机取出一个字典键,在将这个随机字典键的值返回给客户端之后,调用dictDelete函数,从字典中删除随机字典键所对应的键值对 |
|
SREM |
调用intsetRemove函数,从整数集合中删除所有给定的元素 |
调用dictDelete函数,从字典中删除所有键为给定元素的键值对 |
-
- 有序集合对象
有序集合的编码可以是ziplist或者skiplist。
- ziplist编码的有序集合对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(member),而第二个元素则保存元素的分值(score)。压缩列表内的集合元素按分值从小到大进行排序,分值较小的元素被放置在靠近过表头的位置,而分值较大的元素则被放置在靠近尾部的位置。
- skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表:
typedef struct zset{
zskiplist *zsl;
dict *dict;
}
zsl跳跃表按分值从小到大保存了所有集合元素,每个跳跃表节点都保存了一个集合元素:跳跃表节点的object属性保存了元素的成员。而跳跃表节点的score属性则保存了元素的分值。
dict字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值对都保存了一个集合元素:字典的键保存了元素成员,而字典的值则保存了元素的分值。
注:虽然zset结构同时使用了跳跃表和字典保存有序集合元素,但这两种数据结构都会通过指针来共享相同元素的成员和分值,所以同时使用跳跃表和字典来保存集合元素不会产生任何重复成员或者分值,也不会因此而浪费额外的内存。
编码转换:
当有序集合对象可以同时满足以下两个条件时,对象会使用ziplist编码:
- 有序集合保存的元素数量小于128个;
- 有序集合保存的所有元素成员的长度小于64字节;
注:配置文件中zset-max-xiplist-entries选项和zset-max-ziplist-value选项可以设置。
有序集合命令的实现:
|
命令 |
ziplist编码的实现方法 |
zset编码的实现方法 |
|
ZADD |
调用ziplistInsert函数,将成员和分值作为两个节点分别插入到压缩列表 |
先调用zslInsert函数,将新元素添加到跳跃表,然后调用dictAdd函数,将新元素关联到字典 |
|
ZCARD |
调用ziplistLen函数,获得压缩列表包含节点的数量,将这个数量除以2得出集合元素的数量 |
访问跳跃表数据结构的length属性,直接返回结合元素数量 |
|
ZCOUNT |
遍历压缩列表,统计分值在给定范围内的节点的数量 |
遍历跳跃表,统计分值在给定范围内的节点数量 |
|
ZRANGE |
从表头向表尾遍历压缩列表,返回给定索引范围内的所有元素 |
从表头向表尾遍历跳跃表,返回给定索引范围内所有元素 |
|
ZREVRANGE |
从表尾向表头遍历压缩列表,返回给定索引范围内的所有元素 |
从表尾向表头遍历跳跃表,返回给定索引范围内所有元素 |
|
ZRANK |
从表头向表尾遍历压缩列表,查找给定的成员,沿途记录经过节点的数量,当找到给定成员之后,途径节点的数量就是该成员所对应元素的排名 |
从表头向表尾遍历跳跃表,查找给定的成员,沿途记录经过节点的数量,当找到给定成员之后,途径节点的数量就是该成员所对应元素的排名 |
|
ZREVRANK |
从表尾向表头遍历压缩列表,查找给定的成员,沿途记录经过节点的数量,当找到给定成员之后,途径节点的数量就是该成员所对应元素的排名 |
从表尾向表头遍历跳跃表,查找给定的成员,沿途记录经过节点的数量,当找到给定成员之后,途径节点的数量就是该成员所对应元素的排名 |
|
ZREM |
遍历压缩列表,删除所有包含给定成员的节点,以及被删除成员节点旁边的分值节点 |
遍历跳跃表,删除所有包含了给定成员的跳跃表节点。并在字典中解除被删除元素的成员和分值的关联 |
|
ZSCORE |
遍历压缩列表,查找包含了给定成员的节点,然后取出成员节点旁边的分直接点保存的元素分值 |
直接从字典中取出给定成员的分值 |
类型检查与命令多态:
- 可以对任何类型的键执行:DEL命令、EXPIRE命令、RENAME命令、TYPE命令、OBJECT命令
- 只能对特定类型的键执行:
- SET、GET、APPEND、STRLEN等命令只能对字符串键执行
- HDEL、HSET、HGET、HLEN等命令只能对哈希键执行
- RPUSH、LPOP、LINSERT、LLEN等命令只能对列表键执行
- SADD、SPOP、SINTER、SCARD等命令只能对集合键执行
- ZADD、ZCARD、ZRANK、SCORE等命令只能对有序集合键执行
类型检查的实现:
类型特定命令所进行的类型检查是通过redisObject结构的type属性来实现的。
多态命令的实现:
Redis会根据值对象的类型判断键是否能执行命令外,还会根据值对象的编码方式。
内存回收:
redis在自己的对象系统中构建了一个引用计数(reference counting)计数来实现内存的回收机制。
- 在创建一个新对象时,引用计数器的值会被初始化为1.
- 当对象被一个新程序使用时,他的引用计数值会被增1.
- 当对象不再被一个程序使用时,他的引用计数值会被减1.
- 当对象的引用计数值变为0时,对象所占内存会被释放。
对象共享:
对象的引用计数属性还带有对象共享的作用。
在redis中,让多个键共享同一个值对象需要执行以下两个步骤:
- 将数据库键的值指针指向一个现有的值对象。
- 将被共享的值对象的引用计数增1。
目前来说,redis会在初始化服务器时,创建一万个字符串对象,这些对象包含了从0-9999的所有整数值。
对象空转时长:
redisObject结构的lru属性记录了对象最后一次被命令程序访问的时间。
OBJECT IDLETIME命令可以打印出给定键的空转时长,空转时长通过当前时间减去键的值对象的lru时间计算的出来的。
本文深入探讨Redis中使用的各种高效数据结构,如简单动态字符串(SDS)、链表、字典、跳跃表、整数集合及压缩列表,以及它们在不同场景下的应用。文章还解析了Redis对象的结构,包括其类型、编码和底层实现,揭示了Redis如何根据数据类型选择最优的数据结构。

被折叠的 条评论
为什么被折叠?



