主要参考文章
Redis深度历险
和Redis设计与实现
Redis底层数据结构
即redis的底层数据结构,包含其组成,扩展等原理
主要的几种数据结构为sds简单动态字符串
,链表
,字典
,跳跃表
,整数集合
,压缩列表
,对象
等,此外还有hyperLoglog
,bitMap位图
等
sds
1 | 面试问题:https://juejin.im/post/5ca9d8ae6fb9a05e5c05c4e8?utm_source=gold_browser_extension |
结构
sds是redis基于C语言的字符串进一步优化的1
2
3
4
5struct sdshdr {
int len;// 已使用的字符串长度
int free;// 未使用的字符串长度
char buf[];// 字节数组,用于保存字符串本数据
};
如上,redis的sds结构
与C字符串的对比
- 时间复杂度
c字符串查询字符串长度的时间复杂度是O(N),需要对每个数进行计数
sds的查询长度的时间复杂度是O(1),其维护了字段len来计算其长度 - 缓冲区溢出问题
c语言不记录本身数据长度,在扩容时候,非常容易造成缓冲区溢出(即分配过多内存)
而sds因为记录了本身的长度,在空间分配策略/扩容时能杜绝溢出的可能,在低于512M大小时,不会溢出(sds是先扩容,再修改) - 修改的性能
c语言在每次修改数据时(增删改),会从新分配内存,容易出现扩容内存溢出,截断内存泄漏等问题
sds不需要反复的进行内存分配,因为sds采用的是空间预分配+懒性空间释放机制,(参见下方的扩容机制)
扩容机制
采用的是空间预分配和懒性空间释放策略
- 预分配
在字符串长度小于1M的时候,其扩容是加倍扩容,即会保留100%的冗余空间
在字符串长度超过1M之后,每次扩容只会多分配1M的冗余空间 - 懒性释放
上述的冗余空间的意思是,不仅会分配新数据所需空间,还会分配free的空间备用
而懒性空间释放,不是直接释放,而是将空出的空间记录在free上,以待后续使用
总结
链表LinkedList
在redis中链表使用非常频繁,在其他数据结构也使用了链表,在基本数据类型中列表的本质就是链表,而非数组
结构
1 | typedef struct listNode { |
1 | typedef struct list { |
多个链表节点互联组成双向链表
特性
1.redis的链表特性,其一就是双端,如此获取前后节点的时间复杂度都是O(1)
2.无环,即表头或者表尾,总有一个prev或者next指向NULL,不会造成环状
3.链表自带计数器,其自带len属性,获取其长度的时间复杂度为O(1)
字典dirt
字段是redis使用最多的数据结构,字典的本质就是hash,结构类似于java的hashMap
全局所有的key,value也组成了一个字段,过期的key也组成了一个字典
zset的score和value的关系映射也用到了字典
字典的底层是由哈希表hash实现的,一个哈希表由多个哈希节点组成,一个哈希节点存放一个字典的键值对
结构
1 | typedef struct dictht { |
1 | typedef struct dictEntry { |
1 | typedef struct dict { |
如上
1.分别为哈希表,哈希节点和字典,可以看出字典对象的主体就是哈希表对象,此外还维护了一些诸如函数和rehash索引的数据
2.可以看出,字典的组成与java的hashmap组成基本一致,同属于数组+链表的结构
其中数组默认维护了两份哈希表,其一为常用存储,另外一个是在rehash的时候使用的
链表冲突
在多个键分配到同一数组的哈希表的同一索引上时,会发生冲突的,而哈希表对于这个冲突,采取的是链地址法来解决冲突的,没个哈希表节点维护了一个next指针,多个节点通过这个指针组成链表
渐进式rehash
redis中比较考究的一个知识点,就是渐进式rehash
在扩容/收缩的时候,需要重新计算hash值,并重新分配,非常耗时,所以redis便采取渐进式rehash策略
rehash使用到了字典的两个哈希表
redis的扩容条件:当hash中元素等于链表中元素时就会扩容
redis的缩容条件:只占有10%的时候
简单的过程如下:
1.在扩容时,会通过计算,得出新的需要的内存大小,并将此大小分配给h1表
2.然后将h0的已有键值对数据,同步到h1的表中去,此同步过程,需要重新计算hash值并分配位置
3.此时,释放h0表,然后将h1表置为h0表作为使用,并新开辟一个h1表做待用,如此完成渐进式rehash过程
上述是hash表的rehash过程,但需要知道的是,rehash过程,不是一蹴而就的,是分多次,渐进式的把数据同步过去的
具体的过程如下:
1.为字典的h1分配计算后的空间,让字典同时持有两块hash表
2.将字段持有的rehashidx标识置为0(默认为-1),标识rehash开始
3.在rehash的数据同步阶段,任何对h0表的在索引rehashidx上的键的修改,都会同步rehash到h1表中,并将rehashidx的值加1(rehashidx记录进度)
4.rehash过程中,任何查询/删除/修改,都会同时作用两张表去查询的,而新增会直接作用在h1表中,保证h0只减不增,随着rehash变成空表
5.rehash完成后,将rehashinx置为-1,标识rehash结束
跳跃表
- TODO