Redis 设计与实现 读书笔记(菜鸟版)
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]->table
和ht[1]->table
中查找,而写入时只像ht[1]->table
中写入,这样可以保证ht[0]->table
的键值对只减不增。
字典收缩
字典的收缩和扩容时的rehash类似,但只能手动执行。
Skiplist(跳跃表)
skiplist其实没什么特别新鲜的,redis实现的skiplist有几个特点:
- 允许score值相同但member不同的情况
- score相同时比较member的大小(具体来说就是
StringObjCmp()
) - 每个节点维护一个在第一层的指向上一个节点的指针
一个简明图例送给大家:
redis 源代码 黄建宏老师comment ↩︎
flexible array member 相关sunlylorn
的blog ↩︎
Redis 设计与实现 读书笔记(菜鸟版)相关推荐
- Redis 设计与实现读书笔记-第三章
引言 第三部分,属于多机数据库的实现,相较而言是很受关注的一部分,也是面试的高频考点,总体包含三个部分:主从复制.Sentinel 以及 集群.这三部分(加上之前介绍到的根据 RDB 和 AOF 实现 ...
- Redis 设计与实现——读书笔记
项目中能用到的: redis 数据结构(网络数据存储数据结构,使用何种数据结构能有利于网络数据的查询,如存储拓扑结构,前端需要频繁查询拓扑结构的数据(交换机自身信息switches+交换机链接link ...
- Redis设计与实现-读书笔记
一:数据结构和对象 SDS:动态字符串 包含len free buf SDS可以用作AOF缓冲区.客户端状态的输入缓冲区 len:记录buf数组中已使用字节的数量,等于SDS字符串长度 free:记录 ...
- Redis设计与实现 -读书笔记
2. 简单动态字符串(SDS) 定义: struct sdshdr {int len; // buf中已使用字节数量(不包括末尾'\0')int free; // 未使用字节char buf[]; / ...
- 《Redis设计与实现》笔记|SDS动态字符串|链表字典跳跃表整数集合压缩列表结构|redis中的对象|数据库原理|RDB持久化|AOF持久化|事件与多路利用模型|发布订阅原理|事务原理|慢查询日志
<Redis设计与实现>笔记 前记: 参考配套网站:http://redisbook.com 带注释的源码地址:https://github.com/huangz1990/redis-3. ...
- 领域驱动设计DDD之读书笔记
查看文章 领域驱动设计DDD之读书笔记 转载原地址:http://hi.baidu.com/lijiangzj 2007-08-17 16:53 一.当前Java软件开发中几种认识误区 Hibe ...
- 【《Redis深度历险》读书笔记(1)】基础:万丈高楼平地起 ——Redis 5种基础数据结构
[时间]2021.11.16 [题目][<Redis深度历险>读书笔记(1)]基础:万丈高楼平地起 --Redis 基础数据结构 本栏目是<Redis深度历险:核心原理和应用实践&g ...
- 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.常用命令 参考 ...
- 《H5 移动营销设计指南》 读书笔记整理
一个前端工程师最近迷上了营销类的H5页面,被五花八门的H5页面迷的眼花缭乱,兴趣使然,于是买了一本<H5 营销设计指南>,看完以后对营销类的H5页面有了更深的理解,感觉很实在,所以参考读书 ...
最新文章
- 2021-08-05 Ubuntu18.04安装ROS出现的一些问题
- MobileNetV1/V2/V3简述 | 轻量级网络
- 在PostgreSQL命令行psql里格式化输出json字段
- 如果你不曾失败,只因你从未尝试
- Java String 字符串
- 开始使用Lumen吧,3分钟搞定登陆认证
- JAVASCRIPT干了不下四五种工作
- 更新 PORTS-Tree 且升级已安装的软件[zt]
- Flutter入门——山寨掘金(二)
- yum离线安装rpm包
- swift开发网络篇—NSURLConnection基本使用
- 台式机XP系统调节屏幕亮度
- AMS分析 -- 启动过程
- MathType初级教程:怎么安装MathType
- python编写agent_python 自动生成useragent/User-Agent方法全解析
- MySQL-- 统计函数
- Git--Git基本使用
- 命令top动态监控进程所占系统资源
- websocket协议与实现原理
- php socket 服务端