概览,Redis中的对象
redis中有五种不同类型的对象,分别是
对于保存在Redis中的键值对,键总是一个字符串对象,而值可以是上面五种类型中的一种。所以,当称呼一个键为列表键时,指的是这个键所对应的值是列表对象。
对于每个对象,都是由redisObject结构表示,结构中包含
深入,对象的实现方式
字符串对象
字符串对象的编码可以是int,embstr,raw类型。
int类型即为整型
如果一个字符串对象保存的是整数值, 并且这个整数值可以用 long 类型来表示,那么字符串对象会将整数值保存在字符串对象结构的 ptr 属性里面(将 void* 转换成 long ), 并将字符串对象的编码设置为 int 。
embstr,raw即为简单动态字符串SDS
SDS为redis自定义类型
要注意的是sds的最后一个字符是'\0',但这个字符是redis内部处理的,其长度也不算在len属性中,这么做是为了可以兼容C语言部分字符串函数。
embstr,raw都使用sds来表示字符串对象,两者的区别在于,embstr使用通过调用一次内存分配函数来分配一块连续的空间, 空间中依次包含 redisObject 和 sdshdr 两个结构;而raw则会调用两次内存分配函数来分别创建 redisObject 结构和 sdshdr 结构。
对于embstr来说,所有数据都保存在一块连续的内存里面, 这种编码的字符串对象比起 raw 编码的字符串对象能够更好地利用缓存带来的优势。
但是,任意使用这种方式必然导致内存碎片的不断增加,所以,embstr与raw的使用场景如下:如果字符串对象保存的是一个字符串值, 并且这个字符串值的长度小于等于 39 字节, 那么字符串对象将使用 embstr 编码的方式来保存这个字符串值。否则使用raw方式。
编码转换
但是,字符串对象的值不会是只读的,因此,对该类对象的修改将导致编码之间的转换。转换只会发生在int与embstr被转换为raw这个方向上。
对于int,如果被append了字符串,或是long类型不再能表达,就被转为raw
对于embstr,好吧,这个是只读的。如果要改,程序会先将对象的编码从 embstr 转换成 raw , 然后再执行修改命令; 因为这个原因, embstr 编码的字符串对象在执行修改命令之后, 总会变成一个 raw 编码的字符串对象。
列表对象
列表对象的编码可以是ziplist或是linkedlist类型
ziplist即为压缩列表
此数据结构是redis为了节约内存而开发的,由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构。
一个压缩列表可以包含任意多个节点(entry), 每个节点可以保存一个字节数组或者一个整数值。
每个压缩列表节点都由
使用压缩链表可能存在的问题是连锁更新。
由于previous_entry_length在前一个节点长度小于 254 字节, 那么 previous_entry_length 属性用 1 字节长的空间来保存这个长度值。如果前一节点的长度大于等于 254 字节, 那么 previous_entry_length 属性需要用 5 字节长的空间来保存这个长度值。
当我们修改某个节点使得其长度大于254,那么程序将对压缩列表执行空间重分配操作,将本节点的所占长度增加4个字节,若恰好本节点长度加上这4字节后大于了254,程序又将对压缩列表执行空间重分配操作,令下一个节点发生变化。这就是连锁更新。
虽然有连锁更新的潜在性能影响问题,但真正造成性能问题的几率是很低的,因为问题发生的前置条件出现的可能性比较低。
linkedlist即为双端链表
双端链表中每个节点都保存了一个字符串对象,不像ziplist一样,而是存的上述字符串对象,字符串对象中保存列表的元素。
编码转换
当列表对象可以同时满足以下两个条件时, 列表对象使用 ziplist 编码:
1、列表对象保存的所有字符串元素的长度都小于 64 字节;
2、列表对象保存的元素数量小于 512 个;
原本保存在压缩列表里的所有列表元素都会被转移并保存到双端链表里面, 对象的编码也会从 ziplist 变为 linkedlist 。
哈希对象
哈希对象的编码可以是ziplist或是hashtable
ziplist即为压缩列表
ziplist 编码的哈希对象使用压缩列表作为底层实现, 每当有新的键值对要加入到哈希对象时, 程序会先将保存了键的压缩列表节点推入到压缩列表表尾,然后再将保存了值的压缩列表节点推入到压缩列表表尾
因此:
1、保存了同一键值对的两个节点总是紧挨在一起, 保存键的节点在前, 保存值的节点在后;
2、先添加到哈希对象中的键值对会被放在压缩列表的表头方向,而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向。
hashtable即为哈希表
哈希算法使用的是MurmurHash
解决冲突的办法是链式
存在ziplist中的依旧是采用encoding与content的方式,存在hashtable中的键与值则都是采用字符串对象来存储的。
编码转换
当哈希对象可以同时满足以下两个条件时, 哈希对象使用 ziplist 编码:
1、哈希对象保存的所有键值对的键和值的字符串长度都小于 64 字节;
2、哈希对象保存的键值对数量小于 512 个;
不能满足这两个条件的哈希对象需要使用 hashtable 编码。
集合对象
集合对象的编码可以是intset或是hashtable
intset即为整数集合
intset会根据包含元素的位数进行编码调整。比如新插入的元素大于当前采用的编码最大长度,那么整个intset就会进行升级操作。
编码转换
当集合对象可以同时满足以下两个条件时, 对象使用 intset 编码:
1、集合对象保存的所有元素都是整数值;
2、集合对象保存的元素数量不超过 512 个;
不能满足这两个条件的集合对象需要使用 hashtable 编码。
有序集合对象
有序集合的编码可以是 ziplist 或者 skiplist 。
skiplist即位跳跃表
Redis 的跳跃表实现由 zskiplist 和 zskiplistNode 两个结构组成, 其中 zskiplist 用于保存跳跃表信息(比如表头节点、表尾节点、长度), 而 zskiplistNode 则用于表示跳跃表节点。
每个跳跃表节点的层高都是 1 至 32 之间的随机数。
在同一个跳跃表中, 多个节点可以包含相同的分值, 但每个节点的成员对象必须是唯一的。
跳跃表中的节点按照分值大小进行排序, 当分值相同时, 节点按照成员对象的大小进行排序。
skiplist 编码的有序集合对象使用 zset 结构作为底层实现, 一个 zset 结构同时包含一个字典和一个跳跃表:
zset结构中的dict字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值对都保存了一个集合元素: 字典的键保存了元素的成员, 而字典的值则保存了元素的分值。
虽然 zset 结构同时使用跳跃表和字典来保存有序集合元素,但这两种数据结构都会通过指针来共享相同元素的成员和分值,所以同时使用跳跃表和字典来保存集合元素不会产生任何重复成员或者分值,也不会因此而浪费额外的内存。
这样一来,通过元素查找分值,或是通过分值进行范围查找元素都会快速的执行。
编码转换:
当有序集合对象可以同时满足以下两个条件时, 对象使用 ziplist 编码:
1、有序集合保存的元素数量小于 128 个;
2、有序集合保存的所有元素成员的长度都小于 64 字节;
不能满足以上两个条件的有序集合对象将使用 skiplist 编码。
redis中有五种不同类型的对象,分别是
- 字符串对象
- 列表对象
- 哈希对象集合对象
- 有序集合对象
对于保存在Redis中的键值对,键总是一个字符串对象,而值可以是上面五种类型中的一种。所以,当称呼一个键为列表键时,指的是这个键所对应的值是列表对象。
对于每个对象,都是由redisObject结构表示,结构中包含
- type属性:记录对象的类型,当我们对一个键使用命令type时,得到的即是该键对应的值的类型。
- encoding属性:记录对象使用的编码,即此对象使用了什么数据结构作为对象的底层实现。当我们对一个键使用OBJECT ENCODING时,得到的即是该键对应的值对象的编码。
- ptr属性:该属性为指针,指向底层实现数据结构的指针。
深入,对象的实现方式
字符串对象
字符串对象的编码可以是int,embstr,raw类型。
int类型即为整型
如果一个字符串对象保存的是整数值, 并且这个整数值可以用 long 类型来表示,那么字符串对象会将整数值保存在字符串对象结构的 ptr 属性里面(将 void* 转换成 long ), 并将字符串对象的编码设置为 int 。
embstr,raw即为简单动态字符串SDS
SDS为redis自定义类型
struct sdshdr {
// 记录 buf 数组中已使用字节的数量
// 等于 SDS 所保存字符串的长度
int len;
// 记录 buf 数组中未使用字节的数量
int free;
// 字节数组,用于保存字符串
char buf[];
};
通过结构体定义可以直观认识该结构,快速得到长度,已知长度避免缓冲区溢出,free可使结构包含暂不使用的内容避免内存频繁分配,不会与传统c字符串一样遇到'\0'就认为字符串结束等。
要注意的是sds的最后一个字符是'\0',但这个字符是redis内部处理的,其长度也不算在len属性中,这么做是为了可以兼容C语言部分字符串函数。
embstr,raw都使用sds来表示字符串对象,两者的区别在于,embstr使用通过调用一次内存分配函数来分配一块连续的空间, 空间中依次包含 redisObject 和 sdshdr 两个结构;而raw则会调用两次内存分配函数来分别创建 redisObject 结构和 sdshdr 结构。
对于embstr来说,所有数据都保存在一块连续的内存里面, 这种编码的字符串对象比起 raw 编码的字符串对象能够更好地利用缓存带来的优势。
但是,任意使用这种方式必然导致内存碎片的不断增加,所以,embstr与raw的使用场景如下:如果字符串对象保存的是一个字符串值, 并且这个字符串值的长度小于等于 39 字节, 那么字符串对象将使用 embstr 编码的方式来保存这个字符串值。否则使用raw方式。
编码转换
但是,字符串对象的值不会是只读的,因此,对该类对象的修改将导致编码之间的转换。转换只会发生在int与embstr被转换为raw这个方向上。
对于int,如果被append了字符串,或是long类型不再能表达,就被转为raw
对于embstr,好吧,这个是只读的。如果要改,程序会先将对象的编码从 embstr 转换成 raw , 然后再执行修改命令; 因为这个原因, embstr 编码的字符串对象在执行修改命令之后, 总会变成一个 raw 编码的字符串对象。
列表对象
列表对象的编码可以是ziplist或是linkedlist类型
ziplist即为压缩列表
此数据结构是redis为了节约内存而开发的,由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构。
一个压缩列表可以包含任意多个节点(entry), 每个节点可以保存一个字节数组或者一个整数值。
每个压缩列表节点都由
- previous_entry_length:以字节为单位,记录压缩列表前一个节点的长度,有了这个属性,就可以实现反向遍历等操作。
- encoding:记录节点的content属性所保存的数据类型以及长度
- content:保存节点的值,可以是节点数组或是整数。
使用压缩链表可能存在的问题是连锁更新。
由于previous_entry_length在前一个节点长度小于 254 字节, 那么 previous_entry_length 属性用 1 字节长的空间来保存这个长度值。如果前一节点的长度大于等于 254 字节, 那么 previous_entry_length 属性需要用 5 字节长的空间来保存这个长度值。
当我们修改某个节点使得其长度大于254,那么程序将对压缩列表执行空间重分配操作,将本节点的所占长度增加4个字节,若恰好本节点长度加上这4字节后大于了254,程序又将对压缩列表执行空间重分配操作,令下一个节点发生变化。这就是连锁更新。
虽然有连锁更新的潜在性能影响问题,但真正造成性能问题的几率是很低的,因为问题发生的前置条件出现的可能性比较低。
linkedlist即为双端链表
双端链表中每个节点都保存了一个字符串对象,不像ziplist一样,而是存的上述字符串对象,字符串对象中保存列表的元素。
编码转换
当列表对象可以同时满足以下两个条件时, 列表对象使用 ziplist 编码:
1、列表对象保存的所有字符串元素的长度都小于 64 字节;
2、列表对象保存的元素数量小于 512 个;
原本保存在压缩列表里的所有列表元素都会被转移并保存到双端链表里面, 对象的编码也会从 ziplist 变为 linkedlist 。
哈希对象
哈希对象的编码可以是ziplist或是hashtable
ziplist即为压缩列表
ziplist 编码的哈希对象使用压缩列表作为底层实现, 每当有新的键值对要加入到哈希对象时, 程序会先将保存了键的压缩列表节点推入到压缩列表表尾,然后再将保存了值的压缩列表节点推入到压缩列表表尾
因此:
1、保存了同一键值对的两个节点总是紧挨在一起, 保存键的节点在前, 保存值的节点在后;
2、先添加到哈希对象中的键值对会被放在压缩列表的表头方向,而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向。
hashtable即为哈希表
哈希算法使用的是MurmurHash
解决冲突的办法是链式
存在ziplist中的依旧是采用encoding与content的方式,存在hashtable中的键与值则都是采用字符串对象来存储的。
编码转换
当哈希对象可以同时满足以下两个条件时, 哈希对象使用 ziplist 编码:
1、哈希对象保存的所有键值对的键和值的字符串长度都小于 64 字节;
2、哈希对象保存的键值对数量小于 512 个;
不能满足这两个条件的哈希对象需要使用 hashtable 编码。
集合对象
集合对象的编码可以是intset或是hashtable
intset即为整数集合
typedef struct intset {
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
编码方式分为16位整数,32位,64位
intset会根据包含元素的位数进行编码调整。比如新插入的元素大于当前采用的编码最大长度,那么整个intset就会进行升级操作。
编码转换
当集合对象可以同时满足以下两个条件时, 对象使用 intset 编码:
1、集合对象保存的所有元素都是整数值;
2、集合对象保存的元素数量不超过 512 个;
不能满足这两个条件的集合对象需要使用 hashtable 编码。
有序集合对象
有序集合的编码可以是 ziplist 或者 skiplist 。
skiplist即位跳跃表
Redis 的跳跃表实现由 zskiplist 和 zskiplistNode 两个结构组成, 其中 zskiplist 用于保存跳跃表信息(比如表头节点、表尾节点、长度), 而 zskiplistNode 则用于表示跳跃表节点。
每个跳跃表节点的层高都是 1 至 32 之间的随机数。
在同一个跳跃表中, 多个节点可以包含相同的分值, 但每个节点的成员对象必须是唯一的。
跳跃表中的节点按照分值大小进行排序, 当分值相同时, 节点按照成员对象的大小进行排序。
skiplist 编码的有序集合对象使用 zset 结构作为底层实现, 一个 zset 结构同时包含一个字典和一个跳跃表:
typedef struct zset {
// 一个跳表
zskiplist *zsl;
// 一个字典
dict *dict;
} zset;
zset 结构中的 zsl 跳跃表按分值从小到大保存了所有集合元素, 每个跳跃表节点都保存了一个集合元素: 跳跃表节点的 object 属性保存了元素的成员, 而跳跃表节点的 score 属性则保存了元素的分值
zset结构中的dict字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值对都保存了一个集合元素: 字典的键保存了元素的成员, 而字典的值则保存了元素的分值。
虽然 zset 结构同时使用跳跃表和字典来保存有序集合元素,但这两种数据结构都会通过指针来共享相同元素的成员和分值,所以同时使用跳跃表和字典来保存集合元素不会产生任何重复成员或者分值,也不会因此而浪费额外的内存。
这样一来,通过元素查找分值,或是通过分值进行范围查找元素都会快速的执行。
编码转换:
当有序集合对象可以同时满足以下两个条件时, 对象使用 ziplist 编码:
1、有序集合保存的元素数量小于 128 个;
2、有序集合保存的所有元素成员的长度都小于 64 字节;
不能满足以上两个条件的有序集合对象将使用 skiplist 编码。