数据结构
内存寸土寸金,能省则省,Redis在这一部分的设计思路便是节约
简单的C字符串无法满足Redis的需求,开发者设计了数据结构Simple Dynamic String用于存取字符串。
struct sdshdr {
unsigned int len;
unsigned int free;
char buf[];
};
比起C字符串,SDS具有以下优点:
1)常数复杂度获取字符串长度。
2)杜绝缓冲区溢出。
3)减少修改字符串长度时所需的内存重分配次数。
4)二进制安全。
5)兼容部分c字符串函数。
c语言字符串 | redis SDS |
---|---|
不记录自身长度,每次获取字符串长度时需要遍历整个字符串计数,时间复杂度为O(n) | 通过len属性记录自身长度,每次获取字符串长度时直接返回len的值,时间复杂度为O(1) |
改变字符串时需要扩展或回收空间,否则会引起缓冲区溢出或内存泄漏问题,N次修改操作一定需要对内存进行N次重分配 | 通过空间预分配和惰性空间释放两种策略优化缓冲区溢出或内存泄漏问题,N次修改操作最多需要进行N次重分配 |
二进制不安全:由于c语言保存字符串以’\0’结尾的限制,所以只能保存文本数据,如果保存的字符串中存在’\0’这种c语言中特有语义的字符时就会导致保存的字符串错误 | 二进制安全:redis中使用len属性来记录字符串长度,因此不需要以’\0’表示字符串结尾,没有特殊语义的字符限制,因此除了能保存文本外还能保存任意格式的二进制数据 |
使用所有c语言中字符串的库函数 | 使用部分c语言中字符串的库函数 |
功能实现用的数据结构
-
SDS
-
链表
-
跳表
有序集合
-
字典
功能优化用的数据结构
内存寸土寸金,能省则省
-
整数集合
用于保存整数,分为int16_t,int32_t,int64_t,默认小而短的单元存储,超过小范围时再升级。
-
压缩列表
如果要存的东西在很小的范围内波动,那么就用单字节(类似这种优化方案)的存储单元来保存。
对象
Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象,每种对象都用到了至少一种我们前面所介绍的数据结构。
对于Redis数据库保存的键值对来说,键总是一个字符串对象,而值则可以是字符串对象、列表对象、哈希对象、集合对象或者有序集合对象的其中一种。
Redis的对象系统还实现了基于引用计数技术的内存回收机制,当程序不再使用某个对象的时候,这个对象所占用的内存就会被自动释放;另外,Redis还通过引用计数技术实现了对象共享机制,这一机制可以在适当的条件下,通过让多个数据库键共享同个对象来节约内存。
Redis中的每个对象都由一个redisobject结构表示:
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
void ptr*; //指向底层实现数据结构的指针
unsigned lru:22; //对象最后一次被命令程序访问的时间
//...
}robj;
对象编码
对象的ptr指针指向对象的底层实现数据结构,而这些数据结构由对象的encoding属性决定。
encoding属性记录了对象所使用的编码,也即是说这个对象使用了什么数据结构作为对象的底层实现。
每种类型的对象都至少使用了两种不同的编码,表8-4列出了每种类型的对象可以使用的编码。
通过encoding属性来设定对象所使用的编码,而不是为特定类型的对象关联一种固定的编码,极大地提升了Redis的灵活性和效率,因为Redis可以根据不同的使用场景来为个对象设置不同的编码,从而优化对象在某一场景下的效率。
举个例子,
-
在列表对象包含的元素比较少时,Redis使用压缩列表作为列表对象的底层实现:因为压缩列表比双端链表更节约内存,并且在元素数量较少时,在内存中以连续块方式保存的压缩列表比起双端链表可以更快被载人到缓存中;
-
随着列表对象包含的元素越来越多,使用压缩列表来保存元素的优势逐渐消失时,对象就会将底层实现从压缩列表转向功能更强、也更适合保存大量元素的双端链表上面;其他类型的对象也会通过使用多种不同的编码来进行类似的优化。
字符串对象 String
分类编码符合其设计思想:节约。
-
数字
如果一个字符串对象保存的是整数值(可用long类型保存的整数),该字符串对象使用int编码
long double浮点数是使用字符串来保存的。
-
长字符串
如果一个字符串对象保存的是大于32字节的字符串,使用SDS来保存这个值,该字符串对象使用raw编码
-
短字符串
如果一个字符串对象保存的是小于等于32字节的字符串,该字符串对象使用embstr编码
embstr编码是专门用于保存短字符串的一种优化编码方式,具有如下优势
-
embstr和raw编码一样,都使用redisobject结构和sdshdr结构来表示字符申对象,但raw编码会调用两次内存分配函数来分别创建redisobject结构和sdshdr结构,而embstr编码则通过调用一次内存分配函数来分配一块连续的空间,空间中依次包含redisobject和sdshdr两个结构。
-
embstr编码将创建字符串对象所需的内存分配次数从raw编码的两次降低为一次。
-
释放embstr编码的字符申对象只需要调用一次内存释放函数,而释放raw编码的字符串对象需要调用两次内存释放函数。
-
因为embstr编码的字符申对象的所有数据都保存在一块连续的内存里面,所以这种编码的字符串对象比起raw编码的字符串对象能够更好地利用缓存
-
列表对象 List
ziplist压缩列表 linkedlist双向链表
-
在列表对象包含的元素比较少时,Redis使用压缩列表作为列表对象的底层实现:因为压缩列表比双端链表更节约内存,并且在元素数量较少时,在内存中以连续块方式保存的压缩列表比起双端链表可以更快被载人到缓存中;
-
随着列表对象包含的元素越来越多,使用压缩列表来保存元素的优势逐渐消失时,对象就会将底层实现从压缩列表转向功能更强、也更适合保存大量元素的双端链表上面;其他类型的对象也会通过使用多种不同的编码来进行类似的优化。
哈希对象 Hash
哈希对象的编码可以是ziplist或者hashtable.
当哈希对象可以同时满足以下两个条件时,哈希对象使用ziplist编码:
-
哈希对象保存的所有键值对的键和值的字符串长度都小于64字节;
-
哈希对象保存的键值对数量小于512个;
注意:这两个上限值可以在配置中进行修改。
不能满足这两个条件的哈希对象需要使用hashtable编码。
集合对象Set
集合对象的编码可以是intset或者hashtable。
当集合对象可以同时满足以下两个条件时,对象使用intset编码:
- 集合对象保存的所有元素都是整数值;
- 集合对象保存的元素数量不超过512个。(上限可改)
不能满足这两个条件的集合对象需要使用hashtable编码。
intset: 带有length属性的数组。
有序集合 Zset
有序集合的编码可以是ziplist或者skiplist.
采用skiplist存储的数据结构中,zset用两种结构保存,dict和zsl。
zset结构中的zsl跳跃表按分值从小到大保存了所有集合元素,每个跳跃表节点都保存了一个集合元素:跳跃表节点的object属性保存了元素的成员,而跳跃表节点的score属性则保存了元素的分值。通过这个跳跃表,程序可以对有序集合进行范围型操作,比如ZRANK,ZRANGE等命令就是基于跳跃表API来实现的。
除此之外,zset结构中的dict字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值对都保存了一个集合元素:字典的键保存了元素的成员,而字典的值则保存了元素的分值。通过这个字典,程序可以用O(1)复杂度查找给定成员的分值,ZSCORE命令就是根据这一特性实现的,而很多其他有序集合命令都在实现的内部用到了这一特性。
两种数据结构通过指针共享相同元素的成员和分值,不会重复浪费内存。
当有序集合对象可以同时满足以下两个条件时,对象使用ziplist编码:
- 有序集合保存的元素数量小于128个;
- 有序集合保存的所有元素成员的长度都小于64字节;
不能满足以上两个条件的有序集合对象将使用skiplist编码。
内存回收 & 对象共享
c语言下,Redis使用引用计数法实现,存在不能回收循环引用的情况(参考jvm的实现)。
对象的引用计数信息会随着对象的使用状态而不断变化:
- 在创建一个新对象时,引用计数的值会被初始化为1;
- 当对象被一个新程序使用时,它的引用计数值会被增一;
- 当对象不再被一个程序使用时,它的引用计数值会被减一;
- 当对象的引用计数值变为0时,对象所占用的内存会被释放。
引用计数法还用于实现对象共享。节约内存。
在Redis中,让多个键共享同一个值对象需要执行以下两个步骤:
1)将数据库键的值指针指向一个现有的值对象;
2)将被共享的值对象的引用计数增一。
数据库中保存的相同值对象越多,对象共享机制就能节约越多的内存。
目前来说,Redis会在初始化服务器时,创建一万个字符串对象,这些对象包含了从。到9999的所有整数值,当服务器需要用到值为0到9999的字符串对象时,服务器就会使用这些共享对象,而不是新创建对象。
lru属性内存回收机制
redisObject有一项属性lru,记录对象最后一次呗命令程序访问的时间。
除了可以被OBJECT IDLETIME命令打印出来之外,键的空转时长还有另外一项作用:如果服务器打开了maxmemory选项,并且服务器用于回收内存的算法为volatile-1ru或者allkeys-lru,那么当服务器占用的内存数超过了maxmemory选项所设置的上限值时,空转时长较高的那部分键会优先被服务器释放,从而回收内存。