diff --git a/code_segment/xiaoyu.cpp b/code_segment/xiaoyu.cpp new file mode 100644 index 00000000..168a57b7 --- /dev/null +++ b/code_segment/xiaoyu.cpp @@ -0,0 +1,5 @@ +#include + +#include + +#include \ No newline at end of file diff --git a/code_segment/xiaoyu.py b/code_segment/xiaoyu.py new file mode 100644 index 00000000..0a18e0da --- /dev/null +++ b/code_segment/xiaoyu.py @@ -0,0 +1,58 @@ +class Solution: + def revmoveDuplicateChars(self,s:list)->None: + m = list([0 for i in range(255)]) + i=0 + while iarray[left-1]: + right = i-1 + + i =i+1 + if left==0: + return True + if right == -1: + right = len(array)-1 + + i=0 + insert_pos=0 + while i< left: + if array[i]>array[right]: + insert_pos=i + break + i=i+1 + if i==0 and insert_pos==0: + return True + + if i!=0 and insert_pos==0: + return False + + if insert_pos!=0 and array[insert_pos-1] 由于java和go的重复性。go作为新兴语言的优势。所以掌握go语言。 -* [ ] C++ - * [ ] 基础知识 - * [ ] 标准库 - * [ ] 面向对象 - * [ ] 设计模式 +* [x] C++ + * [x] 基础知识 + * [x] 标准库 + * [x] 面向对象 + * [x] 设计模式 * [ ] 并行编程 * [ ] GO * [ ] 基础知识。 @@ -22,8 +22,19 @@ * [ ] 算法 ### 知识复习——基础 * [ ] 操作系统 -* [ ] 数据库 + * [ ] 基础知识 + * [ ] Linux底层原理和常见函数 +* [x] 数据库 + * [x] 基础知识 + * [x] MySQL + * [ ] Redis + * [ ] 消息队列 * [ ] 计算机网络 + * [ ] 应用层 + * [ ] 网络层 + * [ ] 通信层 + * [ ] 链路层 + * [ ] 物理层 ### 知识复习——框架 * [ ] C++/GO框架 * [ ] 微服务框架 diff --git a/工作日志/2021年8月4日-今日计划.md b/工作日志/2021年8月4日-今日计划.md index e67bf793..2b4572d2 100644 --- a/工作日志/2021年8月4日-今日计划.md +++ b/工作日志/2021年8月4日-今日计划.md @@ -1,10 +1,6 @@ ## 任务 - - - - ## 收获 ### 关于工作周期的思考。 diff --git a/工作日志/2021年8月7日-今日计划.md b/工作日志/2021年8月7日-今日计划.md index bdd93364..c83af4df 100644 --- a/工作日志/2021年8月7日-今日计划.md +++ b/工作日志/2021年8月7日-今日计划.md @@ -1,8 +1,8 @@ ## 任务 - [x] 完成电话的PPT。能够有啥有意思的东西呢,无非就是推荐一两个科幻电影和科幻动漫。 -- [ ] 完成数据集的设置(加载其他类型的数据集、更多类型的非独立同分布划分和衡量方法) -- [ ] 完成实验的设置(需要对比的地方,需要考虑到可能的影响)基本达到周二要求的水准了。 +- [x] 完成数据集的设置(加载其他类型的数据集、更多类型的非独立同分布划分和衡量方法) +- [x] 完成实验的设置(需要对比的地方,需要考虑到可能的影响)基本达到周二要求的水准了。 ## 收获 diff --git a/数据库/Redis/00 目录.md b/数据库/Redis/00 目录.md deleted file mode 100644 index 4b8b4e67..00000000 --- a/数据库/Redis/00 目录.md +++ /dev/null @@ -1,58 +0,0 @@ -[《Redis 设计与实现》](http://redisbook.com/)的读书笔记 - -# 目录 - -## 第一部分:数据结构和对象 - -[2. 简单动态字符串](ch2.md) - -[3. 链表](ch3.md) - -[4. 字典](ch4.md) - -[5. 跳跃表](ch5.md) - -[6. 整数集合](ch6.md) - -[7. 压缩列表](ch7.md) - -[8. 对象](ch8.md) - -## 第二部分:单机数据库的实现 - -[9. 数据库](ch9.md) - -[10. RDB持久化](ch10.md) - -[11. AOF持久化](ch11.md) - -[12. 事件](ch12.md) - -[13. 客户端](ch13.md) - -[14. 服务器](ch14.md) - -## 第三部分:多机数据库的实现 - -[15. 复制](ch15.md) - -[16. Sentinel](ch16.md) - -[17. 集群](ch17.md) - -## 第四部分:独立功能的实现 - -[18. 发布与订阅](ch18.md) - -[19. 事务](ch19.md) - -[20. Lua脚本](ch20.md) - -[21. 排序](ch21.md) - -[22. 二进制位数组](ch22.md) - -[23. 慢查询日志](ch23.md) - -[24. 监视器](ch24.md) - diff --git a/数据库/Redis/01 Redis概述.md b/数据库/Redis/01 Redis概述.md new file mode 100644 index 00000000..cf98cd0a --- /dev/null +++ b/数据库/Redis/01 Redis概述.md @@ -0,0 +1,10 @@ +# Redis概述 + +## 概述 + + +Redis 是速度非常快的非关系型(NoSQL)内存键值数据库,可以存储键和五种不同类型的值之间的映射。 + +键的类型只能为字符串,值支持五种数据类型:字符串、列表、集合、散列表、有序集合。 + +Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,使用复制来扩展读性能,使用分片来扩展写性能。 \ No newline at end of file diff --git a/数据库/Redis/02 动态字符串.md b/数据库/Redis/02 动态字符串.md deleted file mode 100644 index d1648724..00000000 --- a/数据库/Redis/02 动态字符串.md +++ /dev/null @@ -1,99 +0,0 @@ -Redis里,C字符串只会作为字符串字面量用在一些无需对字符串值进行修改的地方,比如打印日志。Redis构建了 简单动态字符串(simple dynamic string,SDS)来表示字符串值。 - -在Redis里,包含字符串值的键值对在底层都是由SDS实现的。除此之外,SDS还被用作缓冲区:AOF缓冲区,客户端状态中的输入缓冲区。 - -# 2.1 SDS的定义 - -每个sds.h/sdshdr结构表示一个SDS值: - -```c -struct sdshdr { - // 记录buf数组中已使用字节的数量 - // 等于SDS所保存字符串的长度 - int len; - - // 记录buf数组中未使用字节的数量 - int free; - - // 字节数组,用于保存字符串 - char buf[]; -} -``` - -示例: - -![sds-example](img/chap2/sds-example.png) - -SDS遵循C字符串以空字符结尾的管理,空字符不计算在len属性中。这样,SDS可以重用一部分C字符串函数库,如printf。 - -# 2.2 SDS与C字符串的区别 - -- 常数复杂度获取字符串长度 - - C字符串必须遍历整个字符串才能获得长度,复杂度是O(N)。 - - SDS在len属性中记录了SDS的长度,复杂度为O(1)。 - -- 杜绝缓冲区溢出 - - C字符串不记录长度的带来的另一个问题是缓冲区溢出。假设s1和s2是紧邻的两个字符串,对s1的strcat操作,有可能污染s2的内存空间。 - - SDS的空间分配策略杜绝了缓冲区溢出的可能性:但SDS API修改SDS时,会先检查SDS的空间是否满足修改所需的要求,不满足的话,API会将SDS的空间扩展至执行修改所需的大小,然后再执行实际的修改操作。 - -- 减少修改字符串时带来的内存重分配次数 - - 每次增长或缩短一个C字符串,程序都要对保存这个C字符串的数组进行一次内存重分配操作。 - - Redis作为数据库,数据会被平凡修改,如果每次修改字符串都会执行一次内存重分配的话,会对新嗯呢该造成影响。SDS通过未使用空间接触了字符串长度和底层数组长度的关联:在SDS中,buf数组的长度不一定就是字符数量+1,数组里面可以包含未使用的字节,由free属性记录。对于未使用空间,SDS使用了空间预分配和惰性空间释放两种优化策略: - - 1. 空间预分配:当SDS的API对SDS修改并需要空间扩展时,程序不仅为SDS分配修改所需的空间,还会分配额外的未使用空间(取决于长度是否小于1MB)。 - 2. 惰性空间释放:当SDS的API需要缩短时,程序不立即触发内存重分配,而是使用free属性将这些字节的数量记录下来,并等待将来使用。与此同时,SDS API也可以让我们真正师范未使用空间,防止内存浪费。 - -- 二进制安全 - - C字符串中的字符必须复合某种编码(如ASCII),除了字符串末尾之外,字符串里不能包含空字符。这些限制使得C字符串只能保存文本,而不是不能保存二进制数据。 - - SDS API会以处理二进制的方式处理SDS存放在buf数组中的数据,写入时什么样,读取时就是什么样。 - -- 兼容部分C字符串函数 - - 遵循C字符串以空字符结尾的管理,SDS可以重用函数库。 - - ​ - -总结: - -| C字符串 | SDS | -| ------------------- | ------------------- | -| 获取长度的复杂度O(N) | O(1) | -| API不安全,缓冲区溢出 | API安全,不会缓冲区溢出 | -| 修改字符串长度必然导致内存重分配 | 修改字符串长度不一定导致内存重分配 | -| 只能保存文本数据 | 可以保存文本或二进制数据 | -| 可使用所有库的函数 | 可使用部分库的函数 | - -# 2.3 SDS API - -| 函数 | 作用 | 时间复杂度 | -| ----------- | --------------------------------- | :-------- | -| sdsnew | 创建一个包含给定C字符串的SDS | O(N) | -| sdsempty | 创建一个不包含任何内容的SDS | O(1) | -| sdsfree | 释放SDS | O(N) | -| sdslen | 返回SDS已使用的字节数 | O(1) | -| sdsavail | 返回SDS未使用的字节数 | O(1) | -| sdsdup | 创建一个给定SDS的副本 | O(N) | -| sdsclear | 清空SDS保存的字符串内容 | O(1),惰性释放 | -| sdscat | 将给定C字符串拼接到SDS字符串的末尾 | O(N) | -| sdscatsds | 将给定SDS字符串拼接到另一个SDS的末尾 | O(N) | -| sdscpy | 复制 | O(N) | -| sdsgrowzero | 用空字符将SDS扩展至给定长度 | O(N) | -| sdsrange | 保留SDS给定区间内的数据,不在区间内的数据会被覆盖或清除 | O(N) | -| sdstrim | 接受一个SDS和C字符为参数,从SDS中移除C字符串中出现过的字符 | O(N^2) | -| sdscmp | 比较 | O(N) | - -# 导航 - -[目录](README.md) - -下一章:[3. 链表](ch3.md) - - diff --git a/数据库/Redis/02 数据类型.md b/数据库/Redis/02 数据类型.md new file mode 100644 index 00000000..859da104 --- /dev/null +++ b/数据库/Redis/02 数据类型.md @@ -0,0 +1,15 @@ +# 数据类型 + +## 0 数据类型 + + +| 数据类型 | 可以存储的值 | 操作 | +| :--: | :--: | -- | +| String | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作
对整数和浮点数执行自增或者自减操作 | +| List | 列表 | 从两端压入或者弹出元素
对单个或者多个元素进行修剪,
只保留一个范围内的元素 | +| Set | 无序集合 | 添加、获取、移除单个元素
检查一个元素是否存在于集合中
计算交集、并集、差集
从集合里面随机获取元素 | +| Hash | 无序字典 | 添加、获取、移除单个键值对
获取所有键值对
检查某个键是否存在| +| Zset | 有序集合 | 添加、获取、删除元素
根据分值范围或者成员来获取元素
计算一个键的排名 | + + +![](image/2021-09-02-16-24-45.png) diff --git a/数据库/Redis/03 数据结构.md b/数据库/Redis/03 数据结构.md new file mode 100644 index 00000000..7d75960c --- /dev/null +++ b/数据库/Redis/03 数据结构.md @@ -0,0 +1,300 @@ +# 底层数据结构 +## 1 简单动态字符串SDS + + +Redis构建了 简单动态字符串(simple dynamic string,SDS)来表示字符串值。 + +SDS还被用作缓冲区:AOF缓冲区,客户端状态中的输入缓冲区。 + +### SDS 代码实现 + +每个sds.h/sdshdr结构表示一个SDS值: + +```c +struct sdshdr { + // 记录buf数组中已使用字节的数量 + // 等于SDS所保存字符串的长度 + int len; + + // 记录buf数组中未使用字节的数量 + int free; + + // 字节数组,用于保存字符串 + char buf[]; +} +``` +![sds-example](img/chap2/sds-example.png) + +SDS遵循C字符串以空字符结尾的管理,空字符不计算在len属性中。这样,SDS可以重用一部分C字符串函数库,如printf。 + +### SDS 特点(与C字符串的不同) + +- **常数复杂度获取字符串长度**。 + - C字符串必须遍历整个字符串才能获得长度,复杂度是O(N)。 + - SDS在len属性中记录了SDS的长度,复杂度为O(1)。 + +- **杜绝缓冲区溢出**。 + - C字符串不记录长度的带来的另一个问题是缓冲区溢出。假设s1和s2是紧邻的两个字符串,对s1的strcat操作,有可能污染s2的内存空间。 + - SDS的空间分配策略杜绝了缓冲区溢出的可能性:但SDS API修改SDS时,会先检查SDS的空间是否满足修改所需的要求,不满足的话,API会将SDS的空间扩展至执行修改所需的大小,然后再执行实际的修改操作。 + +- **减少修改字符串时带来的内存重分配次数**。 + - 每次增长或缩短一个C字符串,程序都要对保存这个C字符串的数组进行一次内存重分配操作。Redis作为数据库,数据会被平凡修改,如果每次修改字符串都会执行一次内存重分配的话,会对新嗯呢该造成影响。 + - SDS通过未使用空间接触了字符串长度和底层数组长度的关联:在SDS中,buf数组的长度不一定就是字符数量+1,数组里面可以包含未使用的字节,由free属性记录。对于未使用空间,SDS使用了空间预分配和惰性空间释放两种优化策略: + 1. 空间预分配:当SDS的API对SDS修改并需要空间扩展时,程序不仅为SDS分配修改所需的空间,还会分配额外的未使用空间(取决于长度是否小于1MB)。 + 2. 惰性空间释放:当SDS的API需要缩短时,程序不立即触发内存重分配,而是使用free属性将这些字节的数量记录下来,并等待将来使用。与此同时,SDS API也可以让我们真正师范未使用空间,防止内存浪费。 + +- **二进制安全**。 + - C字符串中的字符必须复合某种编码(如ASCII),除了字符串末尾之外,字符串里不能包含空字符。这些限制使得C字符串只能保存文本,而不是不能保存二进制数据。 + - SDS API会以处理二进制的方式处理SDS存放在buf数组中的数据,写入时什么样,读取时就是什么样。 + +- **兼容部分C字符串函数**。遵循C字符串以空字符结尾的管理,SDS可以重用函数库。 + + ​ + +| C字符串 | SDS | +| ------------------- | ------------------- | +| 获取长度的复杂度O(N) | O(1) | +| API不安全,缓冲区溢出 | API安全,不会缓冲区溢出 | +| 修改字符串长度必然导致内存重分配 | 修改字符串长度不一定导致内存重分配 | +| 只能保存文本数据 | 可以保存文本或二进制数据 | +| 可使用所有库的函数 | 可使用部分库的函数 | + +### SDS API + +| 函数 | 作用 | 时间复杂度 | +| ----------- | --------------------------------- | :-------- | +| sdsnew | 创建一个包含给定C字符串的SDS | O(N) | +| sdsempty | 创建一个不包含任何内容的SDS | O(1) | +| sdsfree | 释放SDS | O(N) | +| sdslen | 返回SDS已使用的字节数 | O(1) | +| sdsavail | 返回SDS未使用的字节数 | O(1) | +| sdsdup | 创建一个给定SDS的副本 | O(N) | +| sdsclear | 清空SDS保存的字符串内容 | O(1),惰性释放 | +| sdscat | 将给定C字符串拼接到SDS字符串的末尾 | O(N) | +| sdscatsds | 将给定SDS字符串拼接到另一个SDS的末尾 | O(N) | +| sdscpy | 复制 | O(N) | +| sdsgrowzero | 用空字符将SDS扩展至给定长度 | O(N) | +| sdsrange | 保留SDS给定区间内的数据,不在区间内的数据会被覆盖或清除 | O(N) | +| sdstrim | 接受一个SDS和C字符为参数,从SDS中移除C字符串中出现过的字符 | O(N^2) | +| sdscmp | 比较 | O(N) | + + +## 2 链表 + +Redis构建了自己的链表实现。列表键的底层实现之一就是链表。 + +发布、订阅、慢查询、监视器都用到了链表。Redis服务器还用链表保存多个客户端的状态信息,以及构建客户端输出缓冲区。 + +### 双向链表 代码实现 + +链表节点用adlist.h/listNode结构来表示 + +```c +typedef struct listNode { + struct listNode *prev; + struct listNode *next; + void *value; +} listNode; +``` + +![listNode](img/chap3/listNode.png) + +adlist.h/list来持有链表: + +```c +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; +``` + + ![list](img/chap3/list.png) + +### 链表 特点 + +1. 双向无环。表头结点的prev和表尾节点的next都指向NULL +2. 带表头指针和表尾指针 +3. 带链表长度计数器 +4. 多态。使用void*指针来保存节点值,并通过list结构的dup、free。match三个属性为节点值设置类型特定函数 + +### 链表 API + +| 函数 | 作用 | 复杂度 | +| ---------------------------------------- | ------------------------------------- | ------ | +| listSetDupMethod, listSetFreeMethod, listSetMatchMethod | 将给定函数设置为链表的节点值复制/释放/对比函数 | O(1) | +| listGetDupMethod, listGetFreeMethod, listGetMatchMethod | | O(1) | +| listLength | 返回链表长度 | O(1) | +| listFrist | 返回表头结点 | O(1) | +| listLast | 返回表尾结点 | O(1) | +| listPrevNode, listNextNode | 返回给定节点的前置/后置节点 | O(1) | +| listNodeValue | 返回给定节点目前正在保存的值 | O(1) | +| listCreate | 创建一个不包含任何节点的新链表 | O(1) | +| listAddNodeHead, listAddNodeTail | 将一个包含给定值的新节点添加到表头/表尾 | O(1) | +| listSearchKey | 查找并返回包含给定值的节点 | *O(N)* | +| listIndex | 返回链表在给定索引上的节点 | *O(N)* | +| listDelNote | 删除给定节点 | *O(N)* | +| listRotate | 将链表的表尾结点弹出,然后将被弹出的节点插入到链表的表头,成为新的表头结点 | O(1) | +| listDup | 复制一个给定链表的副本 | *O(N)* | +| listRelease | 释放给定链表,及所有节点 | *O(N)* | + + +## 3 字典 + +Redis的数据库就是使用字典来作为底层实现的,对数据库的增删改查都是构建在字典的操作之上。 + +字典还是哈希键的底层实现之一,但一个哈希键包含的键值对比较多,又或者键值对中的元素都是较长的字符串时,Redis就会用字典作为哈希键的底层实现。 + +### 字典的实现 + +Redis的字典使用**哈希表**作为底层实现,每个哈希表节点就保存了字典中的一个键值对。 + +Redis字典所用的**哈希表**由dict.h/dictht结构定义: + +```c +typedef struct dictht { + // 哈希表数组 + dict Entry **table; + // 哈希表大小 + unsigned long size; + // 哈希表大小掩码,用于计算索引值,总是等于size - 1 + unsigned long sizemask; + // 该哈希表已有节点的数量 + unsigned long used; +} dictht; +``` + +**哈希表节点**使用dictEntry结构表示,每个dictEntry结构都保存着一个键值对: + +```c +typedef struct dictEntry { + void *key; // 键 + + // 值 + union { + void *val; + uint64_t u64; + int64_t s64; + } v; + + // 指向下个哈希表节点,形成链表。一次解决键冲突的问题 + struct dictEntry *next; +} +``` + +![k1-k0](img/chap4/k1-k0.png) + +Redis中的**字典**由dict.h/dict结构表示: + +```c +typedef struct dict { + dictType *type; // 类型特定函数 + void *privdata; // 私有数据 + + /* + 哈希表 + 一般情况下,字典只是用ht[0]哈希表,ht[1]只会在对ht[0]哈希表进行rehash时是用 + */ + dictht ht[2]; + + // rehash索引,但rehash不在进行时,值为-1 + // 记录了rehash的进度 + int trehashidx; +} dict; +``` + +type和privdata是针对不同类型的键值对,为创建多态字典而设置的: + +- type是一个指向dictType结构的指针,每个dictType都保存了一簇用于操作特定类型键值对的函数,Redis会为用途不同的字典设置不同的类型特定函数。 +- privdata保存了需要传给那些类型特定函数的可选参数。 + +```c +typedef struct dictType { + // 计算哈希值的函数 + unsigned int (*hashFunction) (const void *key); + + // 复制键的函数 + void *(*keyDup) (void *privdata, const void *obj); + + // 对比键的函数 + void *(*keyCompare) (void *privdata, const void *key1, const void *key2); + + // 销毁键的函数 + void (*keyDestructor) (void *privdata, void *key); + + // 销毁值的函数 + void (*valDestructor) (void *privdata, void *obj); +} dictType; +``` + +### 哈希算法 + +Redis计算哈希值和索引值的方法如下: + +```python +# 使用字典设置的哈希函数,计算key的哈希值 +hash = dict.type.hashFucntion(key) +# 使用哈希表的sizemask属性和哈希值,计算出索引值 +# 根据情况的不同,ht[x]可以使ht[0]或ht[1] +index = hash & dict.ht[x].sizemask +``` + +当字典被用作数据库或哈希键的底层实现时,使用MurmurHash2算法来计算哈希值,即使输入的键是有规律的,算法人能有一个很好的随机分布性,计算速度也很快。 + +### 解决键冲突 + +Redis使用链地址法解决键冲突,每个哈希表节点都有个next指针。 + + ![collision](img/chap4/collision.png) + +### rehash + +随着操作的不断执行,哈希表保存的键值对会增加或减少。为了让哈希表的负载因子维持在合理范围,需要对哈希表的大小进行扩展或收缩,即通过执行rehash(重新散列)来完成: + +1. 为字典的ht[1]哈希表分配空间: + 1. 如果执行的是扩展操作,ht[1]的大小为第一个大于等于ht[0].used * 2 的2^n + 2. 如果执行的是收缩操作,ht[1]的大小为第一个大于等于ht[0].used的2^n + +2. 将保存在ht[0]中的所有键值对rehash到ht[1]上。rehash是重新设计的计算键的哈希值和索引值 + +3. 释放ht[0],将ht[1]设置为ht[0],并为ht[1]新建一个空白哈希表 + +## 哈希表的扩展与收缩 + +满足一下任一条件,程序会自动对哈希表执行扩展操作: + +1. 服务器目前没有执行BGSAVE或BGREWRITEAOF,且哈希表负载因子大于等于1 +2. 服务器正在执行BGSAVE或BGREWRITEAOF,且负载因子大于5 + +其中负载因子的计算公式: + +```python +# 负载因子 = 哈希表已保存节点数量 / 哈希表大小 +load_factor = ht[0].used / ht[0].size +``` + +注:执行BGSAVE或BGREWRITEAOF过程中,Redis需要创建当前服务器进程的子进程,而多数操作系统都是用写时复制来优化子进程的效率,所以在子进程存在期间,服务器会提高执行扩展操作所需的负载因子,从而尽可能地避免在子进程存在期间扩展哈希表,避免不避免的内存写入,节约内存。 + +# 4.5 渐进式rehash + +将ht[0]中的键值对rehash到ht[1]中的操作不是一次性完成的,而是分多次渐进式的: + +1. 为ht[1]分配空间 +2. 在字典中维持一个索引计数器变量rehashidx,设置为0,表示rehash工作正式开始 +3. rehash期间,**每次对字典的增删改查操作**,会顺带将ht[0]在rehashidx索引上的所有键值对rehash到ht[1],rehash完成之后,rehashidx属性的值+1 +4. 最终ht[0]会全部rehash到ht[1],这是将rehashidx设置为-1,表示rehash完成 + +渐进式rehash过程中,字典会有两个哈希表,字典的增删改查会在两个哈希表上进行。 + +# 4.6 字典API + +| 函数 | 作用 | 时间复杂度 | +| ---------------- | --------------- | ----- | +| dictCreate | 创建一个新的字典 | O(1) | +| dictAdd | 添加键值对 | O(1) | +| dictReplace | 添加键值对,如已存在,替换原有 | O(1) | +| dictFetchValue | 返回给定键的值 | O(1) | +| dictGetRandomKey | 随机返回一个键值对 | O(1) | diff --git a/数据库/Redis/03 链表.md b/数据库/Redis/03 链表.md deleted file mode 100644 index 92b176b2..00000000 --- a/数据库/Redis/03 链表.md +++ /dev/null @@ -1,67 +0,0 @@ -Redis构建了自己的链表实现。列表键的底层实现之一就是链表。发布、订阅、慢查询、监视器都用到了链表。Redis服务器还用链表保存多个客户端的状态信息,以及构建客户端输出缓冲区。 - -# 3.1 链表和链表节点的实现 - -链表节点用adlist.h/listNode结构来表示 - -```c -typedef struct listNode { - struct listNode *prev; - struct listNode *next; - void *value; -} listNode; -``` - - ![listNode](img/chap3/listNode.png) - -adlist.h/list来持有链表: - -```c -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; -``` - - ![list](img/chap3/list.png) - -Redis的链表实现可总结如下: - -1. 双向 -2. 无环。表头结点的prev和表尾节点的next都指向NULL -3. 带表头指针和表尾指针 -4. 带链表长度计数器 -5. 多态。使用void*指针来保存节点值,并通过list结构的dup、free。match三个属性为节点值设置类型特定函数 - -# 3.2 链表和链表节点的API - -| 函数 | 作用 | 复杂度 | -| ---------------------------------------- | ------------------------------------- | ------ | -| listSetDupMethod, listSetFreeMethod, listSetMatchMethod | 将给定函数设置为链表的节点值复制/释放/对比函数 | O(1) | -| listGetDupMethod, listGetFreeMethod, listGetMatchMethod | | O(1) | -| listLength | 返回链表长度 | O(1) | -| listFrist | 返回表头结点 | O(1) | -| listLast | 返回表尾结点 | O(1) | -| listPrevNode, listNextNode | 返回给定节点的前置/后置节点 | O(1) | -| listNodeValue | 返回给定节点目前正在保存的值 | O(1) | -| listCreate | 创建一个不包含任何节点的新链表 | O(1) | -| listAddNodeHead, listAddNodeTail | 将一个包含给定值的新节点添加到表头/表尾 | O(1) | -| listSearchKey | 查找并返回包含给定值的节点 | *O(N)* | -| listIndex | 返回链表在给定索引上的节点 | *O(N)* | -| listDelNote | 删除给定节点 | *O(N)* | -| listRotate | 将链表的表尾结点弹出,然后将被弹出的节点插入到链表的表头,成为新的表头结点 | O(1) | -| listDup | 复制一个给定链表的副本 | *O(N)* | -| listRelease | 释放给定链表,及所有节点 | *O(N)* | - -# 导航 - -[目录](README.md) - -上一章:[2. 简单动态字符串](ch2.md) - -下一章:[4. 字典](ch4.md) - diff --git a/数据库/Redis/04 字典.md b/数据库/Redis/04 字典.md index 9804e28a..05ab8483 100644 --- a/数据库/Redis/04 字典.md +++ b/数据库/Redis/04 字典.md @@ -1,159 +1,3 @@ -Redis的数据库就是使用字典来作为底层实现的,对数据库的增删改查都是构建在字典的操作之上。 - -字典还是哈希键的底层实现之一,但一个哈希键包含的键值对比较多,又或者键值对中的元素都是较长的字符串时,Redis就会用字典作为哈希键的底层实现。 - -# 4.1 字典的实现 - -Redis的字典使用**哈希表**作为底层实现,每个哈希表节点就保存了字典中的一个键值对。 - -Redis字典所用的**哈希表**由dict.h/dictht结构定义: - -```c -typedef struct dictht { - // 哈希表数组 - dict Entry **table; - // 哈希表大小 - unsigned long size; - // 哈希表大小掩码,用于计算索引值,总是等于size - 1 - unsigned long sizemask; - // 该哈希表已有节点的数量 - unsigned long used; -} dictht; -``` - -**哈希表节点**使用dictEntry结构表示,每个dictEntry结构都保存着一个键值对: - -```c -typedef struct dictEntry { - void *key; // 键 - - // 值 - union { - void *val; - uint64_t u64; - int64_t s64; - } v; - - // 指向下个哈希表节点,形成链表。一次解决键冲突的问题 - struct dictEntry *next; -} -``` - -![k1-k0](img/chap4/k1-k0.png) - -Redis中的**字典**由dict.h/dict结构表示: - -```c -typedef struct dict { - dictType *type; // 类型特定函数 - void *privdata; // 私有数据 - - /* - 哈希表 - 一般情况下,字典只是用ht[0]哈希表,ht[1]只会在对ht[0]哈希表进行rehash时是用 - */ - dictht ht[2]; - - // rehash索引,但rehash不在进行时,值为-1 - // 记录了rehash的进度 - int trehashidx; -} dict; -``` - -type和privdata是针对不同类型大家键值对,为创建多态字典而设置的: - -- type是一个指向dictType结构的指针,每个dictType都保存了一簇用于操作特定类型键值对的函数,Redis会为用途不同的字典设置不同的类型特定函数。 -- privdata保存了需要传给那些类型特定函数的可选参数。 - -```c -typedef struct dictType { - // 计算哈希值的函数 - unsigned int (*hashFunction) (const void *key); - - // 复制键的函数 - void *(*keyDup) (void *privdata, const void *obj); - - // 对比键的函数 - void *(*keyCompare) (void *privdata, const void *key1, const void *key2); - - // 销毁键的函数 - void (*keyDestructor) (void *privdata, void *key); - - // 销毁值的函数 - void (*valDestructor) (void *privdata, void *obj); -} dictType; -``` - -# 4.2 哈希算法 - -Redis计算哈希值和索引值的方法如下: - -```python -# 使用字典设置的哈希函数,计算key的哈希值 -hash = dict.type.hashFucntion(key) -# 使用哈希表的sizemask属性和哈希值,计算出索引值 -# 根据情况的不同,ht[x]可以使ht[0]或ht[1] -index = hash & dict.ht[x].sizemask -``` - -当字典被用作数据库或哈希键的底层实现时,使用MurmurHash2算法来计算哈希值,即使输入的键是有规律的,算法人能有一个很好的随机分布性,计算速度也很快。 - -# 4.3 解决键冲突 - -Redis使用链地址法解决键冲突,每个哈希表节点都有个next指针。 - - ![collision](img/chap4/collision.png) - -# 4.4 rehash - -随着操作的不断执行,哈希表保存的键值对会增加或减少。为了让哈希表的负载因子维持在合理范围,需要对哈希表的大小进行扩展或收缩,即通过执行rehash(重新散列)来完成: - -1. 为字典的ht[1]哈希表分配空间: - - 如果执行的是扩展操作,ht[1]的大小为第一个大于等于ht[0].used * 2 的2^n - - 如果执行的是收缩操作,ht[1]的大小为第一个大于等于ht[0].used的2^n - -2. 将保存在ht[0]中的所有键值对rehash到ht[1]上。rehash是重新设计的计算键的哈希值和索引值 - -3. 释放ht[0],将ht[1]设置为ht[0],并为ht[1]新建一个空白哈希表 - -## 哈希表的扩展与收缩 - -满足一下任一条件,程序会自动对哈希表执行扩展操作: - -1. 服务器目前没有执行BGSAVE或BGREWRITEAOF,且哈希表负载因子大于等于1 -2. 服务器正在执行BGSAVE或BGREWRITEAOF,且负载因子大于5 - -其中负载因子的计算公式: - -```python -# 负载因子 = 哈希表已保存节点数量 / 哈希表大小 -load_factor = ht[0].used / ht[0].size -``` - -注:执行BGSAVE或BGREWRITEAOF过程中,Redis需要创建当前服务器进程的子进程,而多数操作系统都是用写时复制来优化子进程的效率,所以在子进程存在期间,服务器会提高执行扩展操作所需的负载因子,从而尽可能地避免在子进程存在期间扩展哈希表,避免不避免的内存写入,节约内存。 - -# 4.5 渐进式rehash - -将ht[0]中的键值对rehash到ht[1]中的操作不是一次性完成的,而是分多次渐进式的: - -1. 为ht[1]分配空间 -2. 在字典中维持一个索引计数器变量rehashidx,设置为0,表示rehash工作正式开始 -3. rehash期间,**每次对字典的增删改查操作**,会顺带将ht[0]在rehashidx索引上的所有键值对rehash到ht[1],rehash完成之后,rehashidx属性的值+1 -4. 最终ht[0]会全部rehash到ht[1],这是将rehashidx设置为-1,表示rehash完成 - -渐进式rehash过程中,字典会有两个哈希表,字典的增删改查会在两个哈希表上进行。 - -# 4.6 字典API - -| 函数 | 作用 | 时间复杂度 | -| ---------------- | --------------- | ----- | -| dictCreate | 创建一个新的字典 | O(1) | -| dictAdd | 添加键值对 | O(1) | -| dictReplace | 添加键值对,如已存在,替换原有 | O(1) | -| dictFetchValue | 返回给定键的值 | O(1) | -| dictGetRandomKey | 随机返回一个键值对 | O(1) | # 导航 diff --git a/数据库/Redis/image/2021-09-02-16-24-45.png b/数据库/Redis/image/2021-09-02-16-24-45.png new file mode 100644 index 00000000..f808795e Binary files /dev/null and b/数据库/Redis/image/2021-09-02-16-24-45.png differ diff --git a/数据库/Redis/00 Redis详解.md b/数据库/Redis/附录1 Redis详解.md similarity index 85% rename from 数据库/Redis/00 Redis详解.md rename to 数据库/Redis/附录1 Redis详解.md index b41f5d8c..a1a53fe1 100644 --- a/数据库/Redis/00 Redis详解.md +++ b/数据库/Redis/附录1 Redis详解.md @@ -1,72 +1,53 @@ # Redis -* [Redis](#redis) - * [一、概述](#一概述) - * [二、数据类型](#二数据类型) - * [STRING](#string) - * [LIST](#list) - * [SET](#set) - * [HASH](#hash) - * [ZSET](#zset) - * [三、数据结构](#三数据结构) - * [字典](#字典) - * [跳跃表](#跳跃表) - * [四、使用场景](#四使用场景) - * [计数器](#计数器) - * [缓存](#缓存) - * [查找表](#查找表) - * [消息队列](#消息队列) - * [会话缓存](#会话缓存) - * [分布式锁实现](#分布式锁实现) - * [其它](#其它) - * [五、Redis 与 Memcached](#五redis-与-memcached) - * [数据类型](#数据类型) - * [数据持久化](#数据持久化) - * [分布式](#分布式) - * [内存管理机制](#内存管理机制) - * [六、键的过期时间](#六键的过期时间) - * [七、数据淘汰策略](#七数据淘汰策略) - * [八、持久化](#八持久化) - * [RDB 持久化](#rdb-持久化) - * [AOF 持久化](#aof-持久化) - * [九、事务](#九事务) - * [十、事件](#十事件) - * [文件事件](#文件事件) - * [时间事件](#时间事件) - * [事件的调度与执行](#事件的调度与执行) - * [十一、复制](#十一复制) - * [连接过程](#连接过程) - * [主从链](#主从链) - * [十二、Sentinel](#十二sentinel) - * [十三、分片](#十三分片) - * [十四、一个简单的论坛系统分析](#十四一个简单的论坛系统分析) - * [文章信息](#文章信息) - * [点赞功能](#点赞功能) - * [对文章进行排序](#对文章进行排序) - * [参考资料](#参考资料) +- [Redis](#redis) + - [二、数据类型](#二数据类型) + - [STRING](#string) + - [LIST](#list) + - [SET](#set) + - [HASH](#hash) + - [ZSET](#zset) + - [三、数据结构](#三数据结构) + - [字典](#字典) + - [跳跃表](#跳跃表) + - [四、使用场景](#四使用场景) + - [计数器](#计数器) + - [缓存](#缓存) + - [查找表](#查找表) + - [消息队列](#消息队列) + - [会话缓存](#会话缓存) + - [分布式锁实现](#分布式锁实现) + - [其它](#其它) + - [五、Redis 与 Memcached](#五redis-与-memcached) + - [数据类型](#数据类型) + - [数据持久化](#数据持久化) + - [分布式](#分布式) + - [内存管理机制](#内存管理机制) + - [六、键的过期时间](#六键的过期时间) + - [七、数据淘汰策略](#七数据淘汰策略) + - [八、持久化](#八持久化) + - [RDB 持久化](#rdb-持久化) + - [AOF 持久化](#aof-持久化) + - [九、事务](#九事务) + - [十、事件](#十事件) + - [文件事件](#文件事件) + - [时间事件](#时间事件) + - [事件的调度与执行](#事件的调度与执行) + - [十一、复制](#十一复制) + - [连接过程](#连接过程) + - [主从链](#主从链) + - [十二、Sentinel](#十二sentinel) + - [十三、分片](#十三分片) + - [十四、一个简单的论坛系统分析](#十四一个简单的论坛系统分析) + - [文章信息](#文章信息) + - [点赞功能](#点赞功能) + - [对文章进行排序](#对文章进行排序) + - [参考资料](#参考资料) -## 一、概述 - -Redis 是速度非常快的非关系型(NoSQL)内存键值数据库,可以存储键和五种不同类型的值之间的映射。 - -键的类型只能为字符串,值支持五种数据类型:字符串、列表、集合、散列表、有序集合。 - -Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,使用复制来扩展读性能,使用分片来扩展写性能。 - ## 二、数据类型 -| 数据类型 | 可以存储的值 | 操作 | -| :--: | :--: | :--: | -| STRING | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作\ 对整数和浮点数执行自增或者自减操作 | -| LIST | 列表 | 从两端压入或者弹出元素 \ 对单个或者多个元素进行修剪,\ 只保留一个范围内的元素 | -| SET | 无序集合 | 添加、获取、移除单个元素\ 检查一个元素是否存在于集合中\ 计算交集、并集、差集\ 从集合里面随机获取元素 | -| HASH | 包含键值对的无序散列表 | 添加、获取、移除单个键值对\ 获取所有键值对\ 检查某个键是否存在| -| ZSET | 有序集合 | 添加、获取、删除元素\ 根据分值范围或者成员来获取元素\ 计算一个键的排名 | - -> [What Redis data structures look like](https://redislabs.com/ebook/part-1-getting-started/chapter-1-getting-to-know-redis/1-2-what-redis-data-structures-look-like/) - ### STRING

@@ -559,7 +540,7 @@ Sentinel(哨兵)可以监听集群中的服务器,并在主服务器进入 假设有 4 个 Redis 实例 R0,R1,R2,R3,还有很多表示用户的键 user:1,user:2,... ,有不同的方式来选择一个指定的键存储在哪个实例中。 -- 最简单的方式是范围分片,例如用户 id 从 0\~1000 的存储到实例 R0 中,用户 id 从 1001\~2000 的存储到实例 R1 中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。 +- 最简单的方式是范围分片,例如用户 id 从 0~1000 的存储到实例 R0 中,用户 id 从 1001~2000 的存储到实例 R1 中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。 - 还有一种方式是哈希分片,使用 CRC32 哈希函数将键转换为一个数字,再对实例数量求模就能知道应该存储的实例。 根据执行分片的位置,可以分为三种分片方式: diff --git a/数据库/Redis/01 Redis笔记.md b/数据库/Redis/附录2 Redis笔记.md similarity index 100% rename from 数据库/Redis/01 Redis笔记.md rename to 数据库/Redis/附录2 Redis笔记.md diff --git a/数据库/数据库.xmind b/数据库/数据库.xmind index 821e877e..563e6e5c 100644 Binary files a/数据库/数据库.xmind and b/数据库/数据库.xmind differ