Redis的数据结构与对象
- redis数据库里的键值对都是由object组成的,数据库建总是一个字符串对象。
- redis数据库里的值可以是字符串对象,列表对象,哈希对象,集合对象,有序集合对象中的一种。
Redis的SDS(simple dynamic string ,简单动态字符串)
比如我们可以在redis客户端敲
redis> set msg "helloRedis"
那么就会在Redis数据库中创建一个新的键值对,keg是msg,value是helloRedis,两个都是SDS。
又比如在redis客户端敲
redis>rpush number 1 2 3
这也就是一个list对象(列表对象),numbe为键,1,2,3为值。经过这几个小的例子你会发现Redis的用法很简单,SDS还可以被用来作缓冲区(buffer):AOF模块的AOF缓冲区和客户端状态中的输入缓冲区。
说了这么多的SDS,那么到底什么是SDS,SDS又是怎么实现的呢,那它又和我们传统的字符串有什么区别呢?
我们可以用c的结构体来表示SDS的定义信息
struct sds{
int len;
int free;
char buf[];
}
那这些都是代表什么意思呢?
len:用来记录字符串的长度,也就是buf已使用的字节数量
free:用来记录buf中剩余的数量
buf:是一个char数组,用来保存字符串的每个字符,字符串最后必须是一个空字符’\0’,不占用空间但是占用位置。这些添加字符串后缀都是由redis的内部函数来实现。
不止这一点redis的设计和c很像,redis还有很多和c相同的地方,因为这样就可以直接重用一些c写好的函数。
现在是不是对SDS有一些了解了,从结构可以看出它比c中的字符串多了len和free,这写参数可以决定怎样的性能呢?
- SDS有len来记录字节数,获取字符串长度操作时间复杂的理所当然的为O(1)
- 普通字符串它要遍历计数,他要找到最后一个字符,因此它的时间复杂度为O(n),因此SDS的strlen函数不会对性能造成任何影响。
这个原因的实质还是因为没有len,我们假设在计算机内存中按顺序排列着S1,S2字符串,S2紧跟着S1字符串。如果是普通字符串进行拼接操作
- Reids如果使用拼接函数(sdscut),计算S1拼接后的字符串长度很简单并调用函数为S1分配足够的空间,然后进行字符串拼接操作就不会造成由于S1内存不够而溢出到S2上去。
- 普通字符串要使用拼接函数(strcut)还必须要计算所占空间数,并且一不小心忘记这步操作就可能会使S1拼接后的字符串由于没有位置而挤到S2的空间去,造成S1的缓存溢出。
为什么这么说呢?相信大家以前也做过字符串修改的函数,我们当时函数底层是怎样的呢?在我们每次修改字符串时系统都会执行内存重分配算法,如果不经常修改的话这都不是问题(前提是不粗心,不然增的时候可能会造成内存溢出,删的时候可能会造成内存泄漏),但是数据库就不一样,因为数据库的修改操作很频繁,因此redis采用的缓存策略。
- redis的预分配策略(主要优化字符个数增加问题):如果字符串小于1mb,那就给它再分配同样大小的空间作为预分配空间。比如本来是10个字节(len的值)在reids的策略后就是20个字节+一个空字符=21。如果大于1mb的话,那么就会给它分配1mb的内存,比如本来是10mb+1byte,在分配策略后就20mb+1byte,这样在频繁的修改过程中不会经常去调用内存分配,因此在性能上就占优一点。
- 惰性空间释放(主要优化字符个数减少问题):如果是缩短了字符串,那redis并不会立即把它多余的空间释放掉,而是将它加入到free中可以再一度的优化字符串增长操作。但是如果在空间紧张或者有需要的时候redis也有相应的API来释放未使用的空间,因此也就不用担心内存浪费的问题了。
这个也是c中的字符串中比较难受的事情了,比如我们输入一串数字再加上空格再加上一串数字,c就只会把它的前一段写进去,因为c判断结束的标识是空字符。但是Redis就不会出现这种问题,输入进去什么样子,输出出来就是什么样子,为什么呢?归根结底还是因为有len,因为它可以记住个数而不是一味的根据空字符来判断是不是结束了,因此它上边就可以存任何类型的东西。比如传一串二进制字符串,c可能得需要很复杂的处理,但是Redis根据SDS的定义就可以很轻松的解决这个问题。那Redis可以不给末尾加\0也可以实现为什么要加,还是因为它可以重用一些c的库函数。
总结:
SDS相对于普通字符串的优点(归根结都还是结构体的设计问题)
- 获取长度复杂度小
- 杜绝缓冲的溢出
- 减少再修改字符串时内存重分配的次数
- 二进制安全
Redis中的链表
因为c里没有链表库,因此Redis就自己实现了链表,那链表在Redis中扮演一个什么的角色呢?它在Redis中的应用非常广泛,比如链表键,发布与订阅,慢查询,监视器等功能,Redis服务器本身还使用链表来保存客户端的状态信息。
链表的操作就很简单了,因为它就和糖葫芦一样一个一个穿成一串。没有写过或者了解链表的可以去一些数据结构或者算法导论等一些书上去参考更详细的,我这就讲个中心意思。
设计数据结构模型,给Node模型加上前驱和后记和节点的值。
typedef struct listNode{
struct lsitNode *preb;
struct listNode *next;
void *value;
}listNode;
然后就通过指针操作头插或尾插进行连接成一个双向链表。
但是Redis是以高性能著称的,按照上述设计那不就是一个普通的单链表么,因此个数据结构仍有改进的空间。Redis后来就自己设计了如下结构的节点结构。
typedef struct list{
listNode *head;//表头节点
listNode *tail;//表尾
unsigned long len;//链表所包含的字节数
void *( * dup)(void *ptr);//节点复制函数
void (*free)(void *ptr);//节点释放函数
int (*match)(void *ptr,void key);//节点值对比函数,对比链表节点所保存的值和另一个输入值是否相等
}list;
这种相对于上一种的优势所在是带有长度计数器,和节点的特定函数。
可以总的来说一下Redis的链表实现的特性:
1.双向链表获取前驱和后继的时间复杂度都是O(1)
2.是一个没有头节点的链表,最小是空
3.带有表头和表尾,获取头和尾时间复杂度为O(1)
4.多种功能:几个特殊设定的函数,可用于保存各种不同类型的值
虽然感觉上述这些并不是很难懂,但是它都设计的很巧妙,对查询和修改都优化了不少。