Redis 设计与实现 读书笔记(简略版)

  • 写在前面
    • 第一章(内部数据结构)
      • SDS
      • List
      • Dictionary
        • Rehash
        • Rehash 与 COW
        • 渐进式Rehash
        • 字典收缩
      • Skiplist(跳跃表)

写在前面

这是我第一次尝试写读书笔记,其实也是想和大家一起交流一下学习新技术的点滴,内容也有参考别人博客的部分会在开头直接表明。

写这博客时,我正在读黄建宏老师写的《Redis 设计与实现, 第一版》,同时配合书中提供的github代码1 一起学习,并巩固C语言知识,希望能坚持下去吧!

第一章(内部数据结构)

SDS

什么是SDS?说人话:SDS是redis底层字符串的表示,用于替代char*类型
在代码中SDS长下面这个样子:

// sds 类型
typedef char *sds;// sdshdr 结构
struct sdshdr {// buf 已占用长度int len;// buf 剩余可用长度int free;// 实际保存字符串数据的地方// 利用c99(C99 specification 6.7.2.1.16)中引入的 flexible array member,通过buf来引用sdshdr后面的地址,// 详情google "flexible array member"char buf[];
};

buf是实际储存数据的地方,事实上对于的很多操作,都是以 sds为参数的。这并不难里理解,因为归根到底我们还是在对char* 类型做操作,但是当sds被操作(如增加长度甚至因为需要扩容而被copy到其他内存地址)时sdshdr也需要被做相应的更新。这又怎么办呢?参考一段代码:

sds sdsMakeRoomFor(sds s,size_t addlen   // 需要增加的空间长度
)
{struct sdshdr *sh, *newsh;size_t free = sdsavail(s);size_t len, newlen;// 剩余空间可以满足需求,无须扩展if (free >= addlen) return s;sh = (void*) (s-(sizeof(struct sdshdr)));// 目前 buf 长度len = sdslen(s);// 新 buf 长度newlen = (len+addlen);// 如果新 buf 长度小于 SDS_MAX_PREALLOC 长度// 那么将 buf 的长度设为新 buf 长度的两倍if (newlen < SDS_MAX_PREALLOC)newlen *= 2;elsenewlen += SDS_MAX_PREALLOC;// 扩展长度newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);if (newsh == NULL) return NULL;newsh->free = newlen - len;return newsh->buf;
}

sh = (void*) (s-(sizeof(struct sdshdr))); s所占用的内存位置是buf的位置,所以 s - 2*(int 的长度) 这里的sh便是sdshdr和对应对sds开始的位置,但是这可能会让不熟悉C的同学confused:这个sizeof(struct sdshdr) 难道不包括buf的大小吗?buf不是个指针吗?这个问题在comment 中也提过,这是C99中的flexible array member2,所以sizeof(struct sdshdr) 也就是起那两个int的大小。

于是它的源代码就十分容易理解了。从上面的代码我们不难看出:

  • sds能在O(1)时间算出len
  • sds有一部分的free space所以对于一些时候的append 和 concat concatenate来说是高效的,不需要重新申请内存

但是当free space不够时我们怎么办呢?redis会为为sds扩容,代码也在上面的代码块里比如当我们的sds的len是10 free是0 时,append 一个长度为5的sds,那么redis就将原本的sds先扩容为:

10+5 < SDS_MAX_PREALLOC ? (10+5)*2 : 10+5+SDS_MAX_PREALLOC

List

List 是什么感觉应该大家都知道了,具体来说Redis里面的list实现用的是double linked list, 也就是:一个 listNode 结构中既有指向上一个节点的指针也有指向下一个节点的指针。

typedef struct listNode {// 前驱节点struct listNode *prev; // 后继节点struct listNode *next; // 值 void *value;
} listNode;
  • 对应的,链表结构中必然会维护一个指向list头部节点的指针和尾部节点的指针,这使得从左边和右边的pop,push都在常数时间内完成;
  • 值得注意的是,list中还维护了链表长度len,这使得我们可以在常数时间内得到链表的长度信息而不需要遍历整个链表。
  • 还有一点追得注意的是,由于value是一个void* 类型,这个节点可以指向任何值,所以list保留了三个函数指针,这是用来处理不同类型的值的,不同的数值会指向不同的函数,如果这个这个指针不为空那么就会相应的调用函数。
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;

Dictionary

Dictionary(字典)也就是一种 map(映射)由key-value pair(键值对)组成,如果你知道redis也是key-value pair数据库那你就知道这东西确实很重要了LOL。
字典的结构如下,暂时到这大家只用知道 ht[2] 就是hash table也就是具体存储数据的地方,它为什么有俩?还有其他的值我后面再讲,菜鸟版本的读书笔记我们慢慢来……

typedef struct dict {// 特定于类型的处理函数dictType *type;// 类型处理函数的私有数据void *privdata; // 哈希表(2 个) (后面讲)dictht ht[2];// 记录 rehash 进度的标志,值为-1 表示 rehash 未进行 (后面讲)int rehashidx;// 当前正在运作的安全迭代器数量int iterators;
} dict;

dictht的代码如下:

  • size 和 used用于计算字典需不需要扩充和减小。
  • table 或者也可以叫做 bucket就是存数据的地方,它可以理解为一个dictEntry指针的动态数组,数组中每一个元素(dictEntry指针)也就指向一个dictEntry。(dictEntry是啥继续往后看)
typedef struct dictht {// 哈希表节点指针数组(俗称桶,bucket)dictEntry **table; // 指针数组的大小unsigned long size;// 指针数组的长度掩码,用于计算索引值unsigned long sizemask; // 哈希表现有的节点数量unsigned long used;
} dictht;

下面的代码一切豁然开朗,dictEntry其实也就是一个含有键值对的链表的一个节点。而做成一个链表的节点而不是一个简单的键值对结构意图也十分明显,就是为了处理hashing中的collision啦!

typedef struct dictEntry {// 键void *key;// 值 union {void *val; uint64_t u64; int64_t s64;} v;// 链往后继节点struct dictEntry *next;} dictEntry;
  • value 用union保存,也就是值可以是任何指针或是对应的数字。
  • 学会了上面这些我们就可以大概画出某一时间的dict的样子了,我们可以发现其中 key1 和 key2 的哈希值一样发生了collision所以直接把 key2 接在了 key1 这就是redis底层字典解决collision的方法:链地址法。

    聪明的小伙伴会发现,如果一直无休止的加入新的键值对,整个字典会退化成四个长链表,这会导致极低的查找效率,那么我们就需要了解扩容和rehash啦!

Rehash

上面说的情况解决办法就是rehash,也就是重新给这个dictionary分配一块更大的内存更大低bucket然后把原本的数据在hash进去,而rehash的时机一般分为两种:

  • 自然rehash:
    (used / size >= 1) and dict_can_resize == True
  • 强制rehash:
    used / size >= dict_force_resize_ratio (一般为5)
  • 可能有朋友要说了,看上去自然rehash比较科学啊,为什么dict_can_resize会等于False呢?这就和redis的持久化(duration)有关了,redis有两个的持久化命令(BGSAVE 和 BGERITEAOF)好奇的同学看下一节,不好奇的同学直接看下下节:渐进式Rehash

Rehash 与 COW

Redis快的原因是因为数据全部在内存进行操作,而要保证持久化,就不可能绕开磁盘的I/O,Redis的持久化策略是fork一个子进程(subprocess)完成备份,但是熟悉的小伙伴会发现传统的父子进程的数据段和堆栈是相互独立的,所以难以避免的会造成:

  • 分配不必要的资源
  • 将父进程的数据拷贝到子进程时暂时的延时

所以redis采用了Copy On Wirte (COW) 写时复制(COW技术介绍):父子进程共享内存空间,两者只是虚拟空间不同,但是其对应的物理空间是同一个。利用COW 时父节点会将这些数据设置为read only 当任何进程试图进行写操作时,便会激活 page fault,kernel就会把触发的异常的页复制一份,于是父子进程各自持有独立的一份数据。

回到变量dict_can_resize:因为rehash是一个有大量写操作的过程,那么后台在进行持久化命令dict_can_resize 为False,因为尽量地避免这些写操作可以利用COW最大限度的减少持久化命令的资源消耗和延迟。

渐进式Rehash

rehash的过程我放几张图来说明一下,顺便解说一下redis的渐进式rehash原理。

  • 达到rehash要求,为ht[1]->table申请足够大的内存,并依次将bucket中的键值对hash到ht[1]->table中,rehashidx标记的是已经完成到哪个键值对。

  • 继续rehash,并更新size, use 和 rehashidx。在转移全部完成后,ht[0]->table指向 ht[1]->table指向的bucket。ht[1]->table指向NULL。


那什么是渐进式的rehash呢?rehash开始以后主进程不可能一直等着这个rehash吧?所以redis采取的策略是rehash一部分数据,主进程执行一部分命令,交替执行直到结束所以主进程就不必一直等着了!!!具体来说每一轮渐进式rehash的操作的实现有两种:

  • _dictRehashStep: 将rehashidx索引后 ht[0]->table 的第一个不为空的索引内的所有键值对rehash到 ht[1]->table
  • dictRehashMilliseconds: 在指定时间内尽可能rehash键值对;
  • 注意:查找时将在ht[0]->tableht[1]->table中查找,而写入时只像ht[1]->table中写入,这样可以保证ht[0]->table的键值对只减不增

字典收缩

字典的收缩和扩容时的rehash类似,但只能手动执行

Skiplist(跳跃表)

skiplist其实没什么特别新鲜的,redis实现的skiplist有几个特点:

  • 允许score值相同但member不同的情况
  • score相同时比较member的大小(具体来说就是StringObjCmp())
  • 每个节点维护一个在第一层的指向上一个节点的指针

一个简明图例送给大家:


  1. redis 源代码 黄建宏老师comment ↩︎

  2. flexible array member 相关sunlylorn
    的blog ↩︎

Redis 设计与实现 读书笔记(菜鸟版)相关推荐

  1. Redis 设计与实现读书笔记-第三章

    引言 第三部分,属于多机数据库的实现,相较而言是很受关注的一部分,也是面试的高频考点,总体包含三个部分:主从复制.Sentinel 以及 集群.这三部分(加上之前介绍到的根据 RDB 和 AOF 实现 ...

  2. Redis 设计与实现——读书笔记

    项目中能用到的: redis 数据结构(网络数据存储数据结构,使用何种数据结构能有利于网络数据的查询,如存储拓扑结构,前端需要频繁查询拓扑结构的数据(交换机自身信息switches+交换机链接link ...

  3. Redis设计与实现-读书笔记

    一:数据结构和对象 SDS:动态字符串 包含len free buf SDS可以用作AOF缓冲区.客户端状态的输入缓冲区 len:记录buf数组中已使用字节的数量,等于SDS字符串长度 free:记录 ...

  4. Redis设计与实现 -读书笔记

    2. 简单动态字符串(SDS) 定义: struct sdshdr {int len; // buf中已使用字节数量(不包括末尾'\0')int free; // 未使用字节char buf[]; / ...

  5. 《Redis设计与实现》笔记|SDS动态字符串|链表字典跳跃表整数集合压缩列表结构|redis中的对象|数据库原理|RDB持久化|AOF持久化|事件与多路利用模型|发布订阅原理|事务原理|慢查询日志

    <Redis设计与实现>笔记 前记: 参考配套网站:http://redisbook.com 带注释的源码地址:https://github.com/huangz1990/redis-3. ...

  6. 领域驱动设计DDD之读书笔记

    查看文章   领域驱动设计DDD之读书笔记  转载原地址:http://hi.baidu.com/lijiangzj 2007-08-17 16:53 一.当前Java软件开发中几种认识误区 Hibe ...

  7. 【《Redis深度历险》读书笔记(1)】基础:万丈高楼平地起 ——Redis 5种基础数据结构

    [时间]2021.11.16 [题目][<Redis深度历险>读书笔记(1)]基础:万丈高楼平地起 --Redis 基础数据结构 本栏目是<Redis深度历险:核心原理和应用实践&g ...

  8. redis设计与实现学习笔记1

    文章目录 1.对象 1.1 类型 1.2 内存回收 1.3 对象共享 1.4 对象空转时长 2.单机数据库 2.1 RDB 2.2 AOF 2.3 事件 2.4客户端 2.5服务器 3.常用命令 参考 ...

  9. 《H5 移动营销设计指南》 读书笔记整理

    一个前端工程师最近迷上了营销类的H5页面,被五花八门的H5页面迷的眼花缭乱,兴趣使然,于是买了一本<H5 营销设计指南>,看完以后对营销类的H5页面有了更深的理解,感觉很实在,所以参考读书 ...

最新文章

  1. 2021-08-05 Ubuntu18.04安装ROS出现的一些问题
  2. MobileNetV1/V2/V3简述 | 轻量级网络
  3. 在PostgreSQL命令行psql里格式化输出json字段
  4. 如果你不曾失败,只因你从未尝试
  5. Java String 字符串
  6. 开始使用Lumen吧,3分钟搞定登陆认证
  7. JAVASCRIPT干了不下四五种工作
  8. 更新 PORTS-Tree 且升级已安装的软件[zt]
  9. Flutter入门——山寨掘金(二)
  10. yum离线安装rpm包
  11. swift开发网络篇—NSURLConnection基本使用
  12. 台式机XP系统调节屏幕亮度
  13. AMS分析 -- 启动过程
  14. MathType初级教程:怎么安装MathType
  15. python编写agent_python 自动生成useragent/User-Agent方法全解析
  16. MySQL-- 统计函数
  17. Git--Git基本使用
  18. 命令top动态监控进程所占系统资源
  19. websocket协议与实现原理
  20. php socket 服务端

热门文章

  1. Android——SVG图片转成安卓能用的vector矢量图
  2. 卧底“刷量”卖家,有关微信公众号“刷量”的五个劲爆事实
  3. 复旦大学2015--2016学年第一学期高等代数I期末考试情况分析
  4. jetson的学习资料总结
  5. jetson nano开发使用的基础详细分享
  6. 防微信聊天气泡图片实现
  7. 简述c++语言的特点(优点)
  8. 炉石传说 疯狂爆破者空场炸死2个精灵龙的概率
  9. 计算机日历算法流程图,计算机日历
  10. matlab实验报告井字棋,有偿井字棋游戏300+