注:所有这部分内容来自于黄建宏前辈的《Redis设计与实现》一书
第一部分 数据结构与对象
Redis数据库里面的每个键值对(key-value pair)都是由对象(object)组成的,其中:
- 数据库键总是一个字符串对象(string object)
- 数据库的值可以是字符串对象、列表对象、哈希对象、集合对象、有序集合对象这五种之中的一种。
第2章 简单动态字符串
Redis自己创建了一种名为简单动态字符串(simple dynamic string, SDS)的抽象类型,并将SDS用作Redis的默认字符串表示。在Redis里面,C字符串只会作为字符串字面量用在一些无须对字符串值进行修改的地方,比如打印日志什么。但是如果用在一个字符串变量,Redis就会使用SDS来表示字符串值,比如在Redis的数据库里面,包含字符串值的键值对在底层都是由SDS来实现的。
127.0.0.1:6379> set k1 hello
OK
127.0.0.1:6379> get k1
"hello"
那么Redis将在数据库中创建一个新的键值对,其中键值对的键是一个字符串对象,对象的底层实现是一个保存着字符串"k1"的SDS。键值对的值也是一个字符串对象,对象的底层实现是一个保持着字符串"hello"的SDS。
如果客户端执行命令
127.0.0.1:6379> RPUSH fruits apple banana cherry
(integer) 3
那么Redis将在数据库中创建一个新的键值对,其中键值对的键是一个字符串对象,对象底层实现是一个保存了字符串"fruits"的SDS。键值对的值是一个列表对象,列表对象包含三个字符串对象,这三个字符串对象分别由三个SDS实现。
除了用来保存数据库中的字符串值外,SDS还被用作缓冲区(buffer):AOF模块中的AOF缓冲区,以及客户端状态中的输入缓冲区,都是由SDS实现的。
2.1 SDS的定义
在sds.h中定义了sdshdr结构
/* Note: sdshdr5 is never used, we just access the flags byte directly.
* However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
在这五个结构体中,len表示字符串的长度,alloc表示buf指针分配空间的大小(不包括头和空终止符),flags表示该字符串的类型(sdshdr5,sdshdr8,sdshdr16,sdshdr32,sdshdr64),是由flags的左侧三位表示的。
2.2 SDS与C字符串的区别
根据传统,C语言使用长度为N+1的字符数组来表示长度为N的字符串,并且字符数组的最后一个元素总是空字符’\0’。
2.2.1 常数复杂度获取字符串长度
因为C字符并不记录自身的长度信息,所以为了获取一个C字符串的长度,程序必须遍历整个字符串,对遇到的每个字符进行计数,直到遇到代表字符串结尾的空字符为止,这个操作的时间复杂度为O(N)。但SDS与C语言中的字符串不同,因为SDS中的len属性记录SDS本身的长度,所以获取一个SDS长度的复杂度为O(1),通过使用SDS而不是C字符串,Redis将获取字符串长度所需的复杂度从O(N)降低到了O(1),这确保了获取字符串长度的工作不会成为Redis的性能瓶颈。
2.2.2 杜绝缓冲区溢出
C字符串不记录自身长度带来的另一个问题是容易造成缓冲区溢出。比如C语言中<string.h>/strcat操作可以将src字符串中的内容拼接到dest字符串的末尾。
char* strcat(char* dest, const char *src)
如果用户在执行这个函数的时候为dest分配了足够多的内存,则可以容纳src字符串中的所有内容,而一旦这个假设不成立,就会产生缓冲区溢出。
而与C字符串不同,当SDS API需要对SDS进行修改时,API会先检查SDS的空间是否满足修改所需要的要求,如果不满足的话,API会自动将SDS的空间扩展至执行修改所需要的大小,然后才执行实际的修改操作。
2.2.3 减少修改字符串时带来的内存重新分配次数
正如前面提到了,因为C字符并不记录自身的长度,所以对一个包含了N个字符的C字符串来说,这个C字符串的底层实现总是一个N+1个字符长的数组。因为C字符串的长度和底层数组的长度之间存在着这种关联性,所以每次增长或者缩短一个C字符串,程序都总要对保存这个C字符串的数组进行一次内存重分配操作。
比如我们持有一个值为"Redis"的C字符串s,那么为了将s的值改为"Redis Cluster",在执行:strcat(s," Cluster");
之前,我们需要使用内存重分配操作,扩展s的空间。而因为内存重分配设计复杂的算法,并且可能需要执行系统调用,所以它是一个比较耗时间的操作,而且Redis作为数据库,平凡的修改会对性能造成影响,所以为了避免C字符串的这种缺陷,SDS通过未使用空间解除了字符串长度和底层数组长度之间的关联:在SDS中,buf数组的长度包括未使用的字节。通过未使用空间,SDS实现了空间预分配和惰性空间释放两种优化策略。
**空间预分配:**空间预分配用于优化SDS的字符串增长操作,当SDS的API对一个SDS进行修改,程序不仅会为SDS分配修改所必须要的空间,还会为SDS分配额外的未使用空间。其中,额外分配的未使用空间数量由以下公式决定:
如果对SDS进行修改之后,SDS的长度(也就是len属性的值)将小于1MB,那么程序分配和len属性同样大小的未使用空间,比如如果进行修改之后SDS的len将要变成13字节,那么程序也会分配13字节的未使用空间。
如果对SDS进行修改之后,SDS的长度将大于等于1MB,那么程序会分配1MB的未使用空间。
通过空间预分配策略,Redis可以减少连续执行字符串增长操作所需的内存重分配次数。
**惰性空间释放:**惰性空间释放用于优化SDS的字符串缩短操作,当SDS的API需要缩短SDS保存的字符串时,程序并不是立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用。sdstrim
函数接受一个SDS和一个C字符串作为参数,移除SDS中所有在C字符串中出现过的字符。
2.2.4 二进制安全
C字符串中的字符必须符合某种编码(比如ASCII),并且除了字符串的末尾之外,字符串里面不能包含空字符,这些限制使得C字符串只能保存文本数据,而不能保存像图片、音频、视频、压缩文件这样的二进制数据。
C字符串所用的函数只会识别其中的"Redis",而忽略之后的"Cluster"。为了确保Redis可以适用各种不同的使用场景,SDS的API都是二进制安全的,所有SDS API都会以处理二进制的方式来处理SDS存放在buf数组中的数据,这也是我们将SDS中的buf属性称为字节数组的原因,Redis用这个数组来保存一系列二进制数据。所以使用SDS来保存之前提到的特殊格式就没有任何问题,因为SDS使用len属性的值而不是空字符来判断字符串是否结束。
2.2.5 兼容部分C字符串函数
虽然SDS的API都是二进制安全的,但它们一样遵循C字符串以空字符结尾的惯例:这些API总会将SDS保存的数据的末尾设置为空字符,并且总会在为buf数组分配空间时多分配一个字节来容纳这个空字符,这是为了让那些保存文本数据的SDS可以重用一部分<string.h>库定义的函数,从而避免了不必要的代码重复。
2.3 SDS API
下面列出SDS的主要操作API。
函数 | 作用 | 时间复杂度 |
---|---|---|
sds sdsnew(const char *init); | 创建一个包含给定C字符串的SDS | O(N),N是给定C字符串的长度 |
sds sdsempty(void); | 创建一个不包含任何内容的空SDS | O(1) |
void sdsfree(sds s); | 释放给定的SDS | O(N),N为释放SDS的长度 |
size_t sdslen(const sds s); | 返回SDS的已经使用字节数 | O(1) |
size_t sdsavail(const sds s) | 返回SDS的未使用空间字节数 | O(1) |
sds sdsdup(const sds s); | 创建一个给定SDS的副本 | O(N) |
void sdsclear(sds s); | 清空SDS保存的字符串内容 | 因为惰性空间释放策略,复杂度为O(1) |
sds sdscat(sds s, const char *t); | 将给定C字符串拼接到SDS字符串的末尾 | O(N),N为被拼接C字符串的长度 |
sds sdscatsds(sds s, const sds t); | 将给定SDS字符串拼接到另一个SDS字符串的末尾 | O(N),N为被拼接SDS字符串的长度 |
sds sdscpy(sds s, const char *t); | 将给定的C字符串复制到SDS里面,覆盖SDS原来的字符串 | O(N) |
sds sdsgrowzero(sds s, size_t len); | 用空字符将SDS扩展至给定长度 | O(N) |
void sdsrange(sds s, ssize_t start, ssize_t end); | 保留SDS给定区间内的数据,不在区间内的数据将被覆盖或清除 | O(N) |
sds sdstrim(sds s, const char *cset); | 接受一个SDS和一个C字符串作为参数,从SDS中移除所有在C字符串中出现过的字符 | O(N2) |
int sdscmp(const sds s1, const sds s2); | 对比两个SDS字符串是否相等 | O(N) |
第3章 链表
链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活地调整链表的长度。链表在Redis中的应用非常广泛,比如列表键的底层实现之一就是链表。当一个列表键包含了数量比较多的元素,又或者列表包含的元素都是比较长的字符串时,Redis就会使用链表作为列表键的底层实现。
除了链表键之外,发布和订阅、慢查询、监视器等功能也用到了链表,Redis服务器本身还使用链表来保存多个客户端的状态信息,以及使用链表来创建客户端输出缓冲区。
3.1 链表和链表节点的实现
每个链表节点使用一个adlist.h/listNode结构表示:
typedef struct listNode {
struct listNode *prev;
struct listNode *next;
void *value;
} listNode;
我们可以看到listNode是用一个双端链表实现的。多个listNode可以通过prev和next指针组成双端链表,如图所示
此外,Redis中使用adlist.h/list来持有链表。
typedef struct list {
listNode *head; //表头节点
listNode *tail; //表尾节点
void *(*dup)(void *ptr); //节点值复制函数
void (*free)(void *ptr); //节点释放函数
int (*match)(void *ptr, void *key); //节点值对比函数
unsigned long len; //链表中包含的节点数
} list;
Redis的链表实现可以总结如下:
- 双端:链表节点带有prev和next指针
- 无环:表头节点的prev指针和表尾节点的next指针都指向NULL
- 带表头指针和表尾指针
- 带链表长度计数器
- 多态,链表节点使用void*指针来保存节点值,所以链表可以用于保存各种不同类型的值
3.2 链表和链表节点的API
函数 | 作用 | 时间复杂度 |
---|---|---|
listSetDupMethod(l,m) ((l)->dup = (m)) | 将给定的函数设置成链表的节点值复制函数 | 直接调用链表的dup属性,O(1) |
listGetDupMethod(l) ((l)->dup) | 返回链表当前正在使用的节点复制函数 | O(1) |
listSetFreeMethod(l,m) ((l)->free = (m)) | 设置链表的节点值释放函数 | O(1) |
listGetFree(l) ((l)->free) | 返回链表当前正在使用的节点值释放函数 | O(1) |
listSetMatchMethod(l,m) ((l)->match = (m)) | 设置链表的节点值对比函数 | O(1) |
listGetMatchMethod(l) ((l)->match) | 返回链表使用的节点值对比函数 | O(1) |
list | ||
listLength(l) ((l)->len) | 长度 | O(1) |
listFirst(l) ((l)->head) | 链表的表头节点 | O(1) |
listLast(l) ((l)->tail) | 链表的表尾节点 | O(1) |
listPrevNode(n) ((n)->prev) | 返回给定节点的前置节点 | O(1) |
listNode *listSearchKey(list *list, void *key); | 查找并返回链表中给定值的节点 | O(N) |
listNode *listIndex(list *list, long index); | 返回链表在给定索引上的节点 | O(N) |
void listRotate(list *list); | 将链表的表尾节点弹出,然后将被弹出的节点插入到链表的表头,成为新的表头节点 | O(1) |
list *listDup(list *orig); | 复制一个给定链表的副本 | O(1) |
void listRelease(list *list); | 释放给定链表,以及链表中的所有节点 | O(N) |
第4章 字典
字典,又称符号表、关联数组或映射,是一种保存键值对的抽象数据结构。在字典中,一个键可以和一个值进行关联;字典中每个键都是独一无二的。
字典在Redis中的应用相当广泛,比如Redis的数据库就是使用字典来作为底层实现的,对于数据库的增、删、查、改操作也是构建对字典的操作之上的。除了表示数据库之外,字典还是哈希键的底层实现之一,当一个哈希键包含的键值对比较多,又或者键值对中的元素都是比较长的字符串时,Redis就会使用字典作为哈希键的底层实现。
4.1 字典的实现
Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。
4.1.1 哈希表
typedef struct dictht {
//哈希表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,值总是等于size-1。这个属性和哈希值一起决定一个键应该被放到table数组的哪个索引上面
unsigned long sizemask;
//哈希表已有节点的数量
unsigned long used;
} dictht;
4.1.2 哈希表节点
typedef struct dictEntry {
void *key; //键
union { //值
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next; //指向下个哈希表节点,形成链表
} dictEntry;
4.1.3 字典
typedef struct dict {
dictType *type; //类型特定函数
void *privdata; //私有数据
dictht ht[2]; //哈希表
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
unsigned long iterators; /* number of iterators currently running */
} dict;
type属性和private属性是针对不同类型的键值对,为创建多态字典而设置的:
- type类型是一个指向dictType结构的指针,每个dictType结构保存了一簇用于操作特定类型键值对的函数,Redis会为用途不同的字典设置不同的类型特定函数。
- 而private属性则保存了需要传给那些类型特定函数的可选参数。
typedef struct dictType {
uint64_t (*hashFunction)(const void *key); //计算哈希值的函数
void *(*keyDup)(void *privdata, const void *key); //复制键的函数
void *(*valDup)(void *privdata, const void *obj); //复制值的函数
int (*keyCompare)(void *privdata, const void *key1, const void *key2); //对比键的函数
void (*keyDestructor)(void *privdata, void *key); //销毁键的函数
void (*valDestructor)(void *privdata, void *obj); //销毁值的函数
} dictType;
ht属性是一个包含两个项的数组,数组中的每个项都是一个dictht哈希表,一般情况下,字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]哈希表进行rehash时使用。除了ht[1]之外,另外一个和rehash有关的属性就是rehashidx,它记录了rehash目前的进度,如果目前没有进行rehash,那么它的值为-1。
4.2 哈希算法
当要将一个新的键值对添加到字典里面时,程序需要先根据键值对的键计算哈希值和索引值,然后再根据索引值,将包含新键值对的哈希表节点放到哈希表数组的指定索引上面。Redis计算哈希值和索引值的方法如下:
#使用字典设置的哈希函数,计算键key的哈希值
hash = dict->type->hashFunction(key);
#使用哈希表的sizemask属性和哈希值,计算出索引值
#根据情况不同,ht[x]可以是ht[0]或者ht[1]
index = hash & dict->ht[x].sizemask;
4.3 解决键冲突
当两个或以上数量的键被分配到了哈希表数组的同一个索引上面时,我们称这些键发生了冲突。而Redis的哈希表使用链地址法来解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来,这样就解决了键冲突的问题。
为了速度考虑,Redis程序总是将新节点添加到链表的表头位置(复杂度为O(1)),排在其他已有节点的前面。
4.4 rehash
为保持哈希表中的负载因子(load factor)维持在一个合理的范围之内,比如当哈希表中的键值对数量太多时,为了解决冲突,Redis中使用链地址法,这样很可能在某个节点上生成一个常常的单链表,反而会使得速度降低。而当键值的数量太少时,又会造成空间的浪费。所以要维持哈希表的负载因子在一个合理的范围之内。
扩展和收缩哈希表的工作可以通过执行rehash
操作来完成,Redis对字典的哈希表执行rehash的步骤如下:
1)为字典的ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及ht[0]当前包含的键值对数量(也就是ht[0].used属性的值)
- 如果执行的是扩展操作,那么ht[1]的大小为第一个大于等于ht[0].used*2的2n
- 如果执行的是收缩操作,那么ht[1]的大小为第一个大于等于ht[0].used的2n
2)将保存在ht[0]中的所有键值对rehash到ht[1]上面:rehash指的是重新计算键的哈希值和索引值,然后将键值对放置到ht[1]哈希表的指定位置上。
3)当ht[0]包含的所有键值对都迁移到ht[1]之后(ht[0]变为空表),释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白哈希表,为下一次rehash做准备。
举一个例子,我们要对如下字典中的ht[0]进行扩展,那么程序执行如下步骤:
1)ht[0].used当前的值为4,42=8,而8(2的次方)恰好是第一个大于等于42的n次方,所有程序会将ht[1]哈希表的大小设置为8。下图展示ht[1]在分配空间之后,字典的样子。
2)将ht[0]包含的四个键值对都rehash到ht[1],如下图所示。
3)释放ht[0],并将ht[1]设置为ht[0],然后为ht[1]分配一个空白哈希表,至此,对哈希表的扩展操作执行完毕。
哈希表的扩展和收缩
当以下条件中的任意一个被满足时,程序会自动开始对哈希表执行扩展操作:
1)服务器目前没有在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于1。
2)服务器目前正在执行BGSAVE命令或者BGREWRITEAOF,并且哈希表的负载因子大于等于5。
其中哈希表的负载因子可以通过公式:
# 负载因子=哈希表已保存节点数量/哈希表大小
load_factor = ht[0].used/ht[0].size
# 比如对于一个大小为4,包含4个键值对的哈希表来说,这个哈希表的额负载因子是:
load_factor = 4 / 4 = 1
# 又例如,对于一个大小为512,包含256个键值对的哈希表来说,这个哈希表的负载因子为:
load_factor = 256 / 512 = 0.5
当哈希表的负载因子小于0.1时,程序自动开始对哈希表执行搜索操作,原理与扩展类似。
4.5 渐进式rehash
上面说过,扩展或收缩哈希表需要将ht[0]里面的所有键值对rehash到ht[1]里面,但是,这个rehash动作并不是一次性、集中式地完成,而是分多次完成。原因是如果之前的哈希表中存在数以亿计的节点话,一次性将这些节点rehash需要巨大的计算量。因此服务器是渐进地将所有键值对全部rehash到ht[1]。渐进式步骤如下:
1)为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表
2)在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作正式开始。
3)在rehash期间,每次对字典执行添加、删除、查找或者更新操作,程序除了执行指定的操作以外,还会顺道将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当rehash工作完成之后,程序将rehashidx属性的值增一。
4)当操作完成后,将rehashidx属性的值设为-1,表示rehash操作已完成。
在进行渐进式rehash的过程中,字典会同时使用ht[0]和ht[1]两个哈希表,所以在渐进式rehash进行期间,字典的删除、查找和更新等操作会在两个哈希表上进行。比如,要查找一个键的话,程序会先在ht[0]里面进行查找,如果没有找到的话,就会继续到ht[1]里面进行查找。而在新键值的添加会被添加到ht[1]里面。
4.6 字典API
函数 | 作用 | 时间复杂度 |
---|---|---|
dictCreate | 创建一个新的字典 | O(1) |
dictAdd | 将给定的键值对添加到字典里面 | O(1) |
dictReplace | 将给定的键值对添加到字典里面,如果键已经存在于字典,那么用新值取代育有的值 | O(1) |
dictFetchValue | 返回给定键的值 | O(1) |
dictGetRandomKey | 从字典中随机返回一个键值对 | O(1) |
dictDelete | 从字典中删除给定键所对应的键值对 | O(1) |
dictRelease | 释放给定字典,以及字典中包含的所有键值对 | O(1) |
第5章 跳跃表
跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。跳跃表支持平均O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。
Redis使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员是比较长的字符串,Redis就会使用跳跃表来作为有序集合键的底层实现。
下面使用到命令zadd和zrange。
# 添加成员及其score值到有序集key中。
ZADD key [NX|XX] [CH] [INCR] score member [score member ...]
summary: Add one or more members to a sorted set, or update its score if it already exists
since: 1.2.0
group: sorted_set
# 返回有序集合中给定区间的元素,start从0开始,stop结束下标
ZRANGE key start stop [WITHSCORES]
summary: Return a range of members in a sorted set, by index
since: 1.2.0
group: sorted_set
第8章 对象
8.2 字符串对象
字符串对象的编码可以是int、raw或者embstr。
如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面(将void*转换成long),并将字符串对象的编码设置为int。
如果字符串对象保存的是一个字符串值,并且这个字符串值得长度小于等于44字节(redis版本5.0.7下),那么字符串对象将使用embstr编码得方式来保存这个字符串值。
127.0.0.1:6379> set good "123456789012345678901234567890123456789012345"
OK
127.0.0.1:6379> STRLEN good
(integer) 45
127.0.0.1:6379> OBJECT ENCODING good
"raw"
127.0.0.1:6379> set good "12345678901234567890123456789012345678901234"
OK
127.0.0.1:6379> STRLEN good
(integer) 44
127.0.0.1:6379> OBJECT ENCODING good
"embstr"
embstr编码是专门用于保存短字符串得一种优化编码方式,这种编码方式是专门用于保存短字符串得一种优化编码方式,这种编码和raw编码一样,都使用redisObject结构和sdshdr结构来表示字符串对象,但raw编码会调用两次内存分配函数来分别创建redisObject结构和sdshdr结构,而embstr编码则通过调用一次内存分配函数来分配一块连续得空间。
embstr编码的字符串对象在执行命令时,产生的效果和raw编码的字符串对象执行命令时产生的效果是相同的,但使用embstr编码的字符串对象来保存短字符串值有以下好处:
❑embstr编码将创建字符串对象所需的内存分配次数从raw编码的两次降低为一次。
❑释放embstr编码的字符串对象只需要调用一次内存释放函数,而释放raw编码的字符串对象需要调用两次内存释放函数。
❑因为embstr编码的字符串对象的所有数据都保存在一块连续的内存里面,所以这种编码的字符串对象比起raw编码的字符串对象能够更好地利用缓存带来的优势。
127.0.0.1:6379> set msg "hello"
OK
127.0.0.1:6379> OBJECT ENCODING msg
"embstr"
127.0.0.1:6379>
可以用long double类型表示的浮点数在Redis中也是作为字符串值来保存的。如果我们要保存一个浮点数到字符串对象里面,那么程序会先将这个浮点数转换成为字符串值,然后再保存转换所得的字符串值。
127.0.0.1:6379> set pi 3.14
OK
127.0.0.1:6379> OBJECT ENCODING pi
"embstr"
127.0.0.1:6379>
在有需要的时候,程序会将保存在字符串对象里面的字符串值转换为浮点数值,执行某些操作,然后再将执行操作所得的浮点数转换回字符串值,并继续保存在字符串对象里面。
127.0.0.1:6379> INCRBYFLOAT pi 2.0
"5.14"
127.0.0.1:6379> OBJECT ENCODING pi
"embstr"
127.0.0.1:6379>
8.2.1 编码的转换
int编码的字符串对象和embstr编码的字符串对象在条件满足的情况下,会被转换为raw编码的字符串对象。
对于int编码的字符串对象来说,如果我们向对象执行了一些命令,使得这个对象保存的不再是整数值,而是一个字符串值,那么字符串对象的编码将从int变为raw。
127.0.0.1:6379> set number 10086
OK
127.0.0.1:6379> OBJECT ENCODING number
"int"
127.0.0.1:6379> APPEND number " is a good number!"
(integer) 23
127.0.0.1:6379> GET number
"10086 is a good number!"
127.0.0.1:6379> OBJECT ENCODING number
"raw"
127.0.0.1:6379>
此外,因为Redis没有为embstr编码的字符串对象编写任何相应的修改程序(只有int编码的字符串对象和raw编码的字符串对象才有这些程序),所以当我们对embstr编码的字符串对象执行任何修改命令时,程序会将对象的编码从embstr转换成raw,然后再执行修改命令,程序会先将对象的编码从embstr转换成raw,然后再执行修改命令。
127.0.0.1:6379> SET msg "hello world"
OK
127.0.0.1:6379> OBJECT ENCODING msg
"embstr"
127.0.0.1:6379> APPEND msg " again!"
(integer) 18
127.0.0.1:6379> OBJECT ENCODING msg
"raw"
127.0.0.1:6379>
8.2.2 字符串命令的实现
命令 | int编码的实现方式 | embstr编码的实现方法 | raw编码的实现方法 |
---|---|---|---|
SET | 使用int编码保存值 | 使用embstr编码保存值 | 使用raw编码保存值 |
GET | 拷贝对象所保存的整数值,将这个拷贝转换成字符串值,然后向客户端返回这个字符串值 | 直接向客户端返回字符串值 | 直接向客户端返回字符串值 |
APPEND | 将对象转换成raw编码,然后按raw编码的方式执行此操作 | 将对象转换成raw编码,然后按raw编码的方式执行此操作 | 调用sdscatlen函数,将给定字符串追加到现有字符串的末尾 |
INCRBYFLOAT | 取出整数值并转换成long double类型的浮点数,然后对这个浮点数进行加法计算,然后将得出的浮点数结果保存起来 | 取出字符串值并尝试将其转换成long double类型的浮点数,对这个浮点数进行加法运算;如果不能被转换成浮点数,返回一个错误 | 取出字符串值并尝试将其转换成long double类型的浮点数,对这个浮点数进行加法运算;如果不能被转换成浮点数,返回一个错误 |
INCRBY | 对整数值进行加法计算,得出来的计算结果作为整数被保存起来 | embstr编码不能执行此命令,向客户端返回一个错误 | raw编码不能执行此命令,向客户端返回一个错误 |
DECRBY | 对整数值进行减法计算,得出来的计算结果作为整数被保存起来 | embstr编码不能执行此命令,向客户端返回一个错误 | raw编码不能执行此命令,向客户端返回一个错误 |
STRLEN | 拷贝对象所保存的整数值,将这个拷贝转换字符串值,计算并返回这个字符串值得长度 | 调用sdslen函数,返回字符串长度 | 调用sdslen函数,返回字符串长度 |
SETRANGE | 将对象转换成raw编码,然后按raw编码的方式执行此命令 | 将对象转换成raw编码,然后按raw编码的方式执行此命令 | 将字符串特定索引上的值设置为给定字符 |
GETRAGNE | 拷贝对象所保存的整数值,将这个拷贝转换成字符串值,然后取出并返回字符串指定索引上的字符 | 直接取出并返回字符串指定索引上的字符 | 直接取出并返回字符串指定索引上的字符 |
8.3 列表对象
列表对象的编码可以是ziplist或者linkedlist。
ziplist编码的列表对象使用压缩列表作为底层实现,每个压缩列表节点(entry)保存了一个列表元素。
127.0.0.1:6379> RPUSH numbers5 1 "three" 5
(integer) 3
127.0.0.1:6379>
如果numbers键的值对象使用的是ziplist编码,将会是下面这个样子
而linkedlist编码的列表对象使用双端链表作为底层实现,每个双端链表节点都保存了一个字符串对象,而每个字符串对象都保存了一个列表元素。如果前面的numbers键创建的列表对象使用的不是ziplist编码。而是linkedlist编码。
8.3.1 编码转换
当列表对象同时满足以下两个条件时,列表对象使用ziplist编码:
- 列表对象保存的所有字符串元素的长度都小于64字节
- 列表对象保存的元素数量小于512个;不能满足这两个条件的列表对象需要使用linkedlist编码。