第一部分:数据结构与对象
简单动态字符串
redis没有直接使用C语言的传统字符串表示,而是自己构建了一种简单动态字符串(SDS)来实现redis默认的字符串。
SDS的定义是:
struct sdshdr{
int len;//基于已使用的字节长度,即SDS的长度
int free;//记录未使用的字节数量
char buf[];//保存字符串
}
SDS的好处是可以O(1)的复杂度求得字符串的长度;并且可以防止缓冲区溢出,因为是记录了free的,所以在修改时可以检测到是否空间足够;还能减少修改字符串带来的重新分配内存次数,因为C的字符串每次修改长度都会建立新的字符数组,并且复制过去或者删除之前的空间等等操作,而SDS包含未使用的空间,所以修改的时候不用重新分配或者回收内存,减少内存的操作次数。
SDS对未使用的空间采取了空间预分配和懒惰空间释放两种优化策略:当SDS需要空间扩展时,不仅会为SDS分配修改需要的空间,还会额外分配未使用空间(若SDS修改后小于1MB,则未分配空间等于当前字符串长度,若大一1MB,则分配1MB);当SDS需要缩短长度时,并不回收多余的字节,而是用free将这些空间记录下来等待以后使用。
链表
redis中的链表就是常规意义上的双向链表,带头尾指针和长度计数器,没什么特殊的。结点类型是void*
来实现多态。
字典
redis使用哈希表作为字典的底层实现。结构如下:
typedef struct dictht{
dictEntry **table;//存放Entry的数组
unsigned long size;//表大小
unsigned long sizemask;//表大小的掩码,用于哈希函数计算索引值
unsigned long used;//已经存储的节点数量
}
typedef struct dictEntry{
void * key; //key
union{
void *val;
uint64_tu64;
int64_ts64;
} v; //valie
dictEntry *next;//指向下一个结点,组织成链表
}
redis使用MurmurHash2算法来计算哈希值。解决键冲突使用的链地址法,每次新节点都添加到表头位置,复杂度为O(1)。
rehash:如果是扩展大小,则新大小为大于当前存储节点数量两倍最近的一个2的幂数,如果是收缩大小的话就是当前存储节点数最近的那个幂数。
redis采用的是渐进式rehash,当键数量非常多的时候,会同时持有ht[o]
和ht[1]
两个hash表,每次增删改查时,会顺带把之前表中的数据移动至新表,可以分而治之。
跳表
跳表是一种有序的数据结构,通过在每个节点中维持多个指向别的节点的指针来打到快速访问的目的。
跳表支持平均O(logn),最坏O(n)的复杂度进行查找,也能顺序批量处理结点,效率可一个平衡树媲美,但是实现比平衡树简单。
整数集合
整数集合(intset)是集合键的底层实现之一,如果一个集合只包含整数,并且数量不多的情况下,redis就会用intset作为键的底层实现。
typedef struct intset{
uint32_t encoding;
uint32_t length;
int8_t contents[];
}
压缩列表
压缩列表(ziplist)是列表键和哈希键的底层实现。当一个列表键只包含少量元素,并且要么是小整数,要么是小字符串,就会用ziplist作为列表键的底层实现。
主要是通过一系列的特殊编码的连续内存快来组成数据结构,为了节约内存。
对象
redis并不是使用上述的这些数据结构来实现键值对数据库,而是基于上述数据结构构建了5中对象:字符串对象,列表对象,haxi对象,集合对象和有序集合对象。
redis的对象系统采用基于引用计数的内存回收机制来进行垃圾的释放。还通过引用计数的技术实现了对象的共享机制,可以通过让多个数据库键引用一个对象来节约内存。
typedef struct redisObject{
unsigned type; //类型 共5种:字符串,集合,有序集合,列表,哈希
unsigned encoding; //编码 共8种,每种类型都对应至少两种不同的编码,包括整数,SDS,ziplist,intset等等
void * ptr; //实际的指针
}
类型(省略前缀) | 编码(省略前缀) | 对象 |
---|---|---|
STRING | int | 使用整数实现的字符串 |
STRING | embstr | 使用embstr编码简单动态字符串实现的字符串 |
STRING | raw | 使用SDS实现的字符串 |
LIST | ziplist | 使用压缩列表实现的列表 |
LIST | linkedlist | 使用链表实现的列表 |
HASH | ziplist | 使用压缩列表实现的哈希 |
HASH | hashtable | 使用哈希表实现的哈希 |
SET | intset | 使用整数集合实现的集合 |
SET | hashtable | 使用字典实现的集合 |
ZSET | ziplist | 使用压缩列表实现的有序集合 |
ZSET | skiplist | 使用跳表实现的有序集合 |
- 字符串对象的编码可以是int,raw和embstr。如果一个字符串对象保存的是整数值,并且这个整数可以用long来表示,则这个字符串对象会将整数值保存在结构体的ptr指针里面,并将编码设置为int;如果字符串对象保存的是字符串并且长度大于32字节,则将使用SDS保存,编码为raw;若保存的是字符串并且长度小于32字节,则使用embstr保存,embstr是保存短字符串的优化编码方法。
- 列表对象的编码可以是ziplist和linkedlist。当列表对象保存的所有字符串元素长度都小于64字节,并且保存元素的数量小于512个,此时会用ziplist编码,否则会用linkedlist编码。
- 哈希对象的编码可以是ziplist和hashtable。当哈希对象保存的所有键值对的键和值长度都小于64字节并且键值对数量小于512时会用ziplist编码,否则用hashtable。
- 集合对象的编码可以是intset或者hashtable。当用hashtable编码时,所有键值对的值都是null。当集合保存的都是整数,并且数量小于512时才用intset编码,,否则用hashtable。
- 有序集合对象的编码可以是ziplist和skiplist。当使用ziplist时,每个集合元素使用两个紧挨的压缩列表项保存,第一个结点保存成员,第二个结点保存分值,按照分值从小到大排序,分小的元素排在表头。skiplist编码的有序集合使用zset作为底层实现,包含一个字典和一个跳表,跳表按照分值从小到大保存元素。另外还使用字典创建一个从成员到分值的映射,这样可以O(1)的复杂度访问元素的分值。当有序集合保存的元素数量小于128,并且所有元素的长度小于64字节时,采用ziplist,否则用skiplist。
redis会共享0-9999的字符串对象