「这是我参与2022首次更文挑战的第 14 天,活动详情查看:2022首次更文挑战」。
此篇为自己在准备面试中写的 redis 笔记。
面向的是准备面试的同学,不是面试的,请还是去看看源码以及相关详细的文章。同时本文总结的很多也是来源于其他很多文章,只是为了准备,作者将其收录起来,面试而用。所以,本文几乎没有原创。谢谢各位的观看和评论。
本系列 redis 面试文章针对的是 redis5.x 的版本。
基本数据结构
这也是 redis 快的原因,优秀的数据结构设计以及省内存的编码格式。
SDS
- 通过记录内部字符数组的使用长度和分配大小,避免了字符串的遍历 (比如创建、追加、复制、比较)
- 内部把空间检查和扩容都封装在:
sdsMakeRoomFor(),避免开发忘记扩容 - 设计了不同的结构头来灵活保存不同大小的字符串,有效节省内存空间
- 使用专门的编译优化来节省内存空间,不使用字节对齐的方式,直接采用紧凑编码格式
__attribute__((__packed__))标记
- 改善C语言中的二进制不安全字符串
- 由于有长度统计变量len的存在,读写buf不依赖 "\0" end
- buf依然是柔性数组,通过对数组指针作“减一”(buf[-1])操作,能方便地定位到flags,标识当前结构体的类型
dict
先说它的底层实现数据结构的实现原理:
- 使用「链式hash」存储;链式冲突严重,会开辟一个新的hashtable,翻倍扩容,并采取「渐进式rehash」迁移数据
- 「渐进式rehash」是把迁移数据的开销,平摊多次迁移操作,目的是降低主线程响应性能
- 作为上层数据结构Hash/Set/Sorted Set底层数据结构
- redis server每个DB都存放「全局数据键值对以及标志过期的key」都是使用dict类型
- 存储数据字段:
dictEntry **table→ 二维数组- dictEntry → 存储键值对,next指向hash冲突的下一个键值对
- used:全部存储元素个数
- size:控制
table一维数组的数量
所以在整个 dict 类型存储的是两个 dictht(为了rehash) 的包转类型:
rehash
- 初始化时,ht[1] 是没有size的;但扩容主体是在 ht[1] 中完成的
- 给 ht[1] 申请当前容量*2的空间 → 双倍扩容;并将 rehashidx 置为0
- 重新计算每一个key的hash和index,依次添加到 ht[1] 中,并把 ht[0] 中该键值对珊瑚,rehashidx 表示当前 ht[0] 正在进行rehash的节点的index (表示当前的进度)
- 操作完毕,清空 ht[0],ht[1] → ht[0],引用转移
- 在执行插入/删除/查找/修改操作之前,都会判断当前是否在 rehash,如果在则进行一次rehash,不断分摊每一个rehash的成本
- 在rehash中,ht[0] & ht[1] 共同承担查找工作
ziplist
这里只说上层数据结构和 ziplist 的关系:
hash
- field 较少而且 value 值比较小的时候,使用 ziplist 做压缩处理
- 随着field增多和value值增大,改为dict实现。存储效率就没办法和序列化string相比 (这个和string效率)
- 原因:
- ziplist在元素增多的过程中,插入/修改引发的realloc操作会有大概率的内存拷贝,降低性能
- 查找只能遍历,优化了一下反向遍历 (加上了前一个entry的占用字节数)
sorted set
- 数据较少时,采用一个ziplist存储
- 数据多时,采用zset存储:
- 由dict+skiplist存储
- dict:查询 member → score 的映射
- skiplist:范围查询,只需要通过指针的跳转操作
- 具体的存储则交给 dict 做实际数据存储和编码
如果使用 zset 存储,就涉及到 skiplist 的存储:
skiplist
-
每一个元素存储:zadd key score value
- 数据本身:value
- 数据对应的分数:score
- 根据分数大小和数据本身进行 rank:
zrank key member
typedef struct zskiplistNode { //Sorted Set中的元素 sds ele; //元素权重值 double score; //后向指针 -> 便于反向遍历 struct zskiplistNode *backward; //节点的level数组,保存每层上的前向指针和跨度 struct zskiplistLevel { struct zskiplistNode *forward; unsigned long span; } level[]; } zskiplistNode; -
如何比较:
- cur→score < target→score, cur = cur→forward (比target权重小,查看链表的下一个节点)
- cur→score = target→score, cur→val < target→val, cur = cur→forward (权重相等,元素值小查看链表的下一个节点)
- 都不满足 ⇒ 开始 forrange level[],查看下一层的指针
//获取跳表的表头 x = zsl->header; //从最大层数开始逐一遍历 for (i = zsl->level-1; i >= 0; i--) { ... // 当前层不满足,则开始下一轮循环,找下一层的指针,往后找 while (x->level[i].forward && (x->level[i].forward->score < score || (x->level[i].forward->score == score && sdscmp(x->level[i].forward->ele,ele) < 0))) { ... x = x->level[i].forward; } ... } -
插入:
- 需要额外两个空间:
- update[]: 记录查询过程中每一层的前一个节点 (链表插入最重要的就是找到前一个节点)
- rank[]: 记录header走到 update[i] 所经历的步长。更新 update[i].span 以及插入newNode.span,做一个缓存
- 通过算法计算新插入节点的 level
- 循环 level,将 update[i].forward 和 newNode.level[i] 对应起来
- 更新 update[i].forward 指向
- 更新每一层的 span
- 插入 skiplist 和 dict 是分开操作的
- 需要额外两个空间: