This commit is contained in:
Estom
2021-09-02 21:37:06 +08:00
parent 086e5bd69c
commit 443143670b
18 changed files with 454 additions and 458 deletions

5
code_segment/xiaoyu.cpp Normal file
View File

@@ -0,0 +1,5 @@
#include<stdio.h>
#include<string.h>
#include

58
code_segment/xiaoyu.py Normal file
View File

@@ -0,0 +1,58 @@
class Solution:
def revmoveDuplicateChars(self,s:list)->None:
m = list([0 for i in range(255)])
i=0
while i<len(s):
if m[ord(s[i])]==1:
del s[i]
#s.pop(i)
else:
m[ord(s[i])]=1
i=i+1
def isOrderedWithOneMove(self,array:list):
# 找到左右边界
i=1
left=0;
right=-1;
while i<len(array):
if array[i]<array[i-1]:
if left!=0:
return False
left = i
if left!=0 and array[i]>array[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]<array[left]:
return True
else:
return False
def main():
# list_line = ['h','e','l','l','l','b','b','o','o','o']
# Solution().revmoveDuplicateChars(list_line)
# print(123)
# print(list_line)
array = [3,4,4,5,-2,-1]
res = Solution().isOrderedWithOneMove(array)
print(res)
main()

View File

@@ -9,9 +9,9 @@
明天(周三)
* [x] 第一轮,填充内容。复制相关的资料、自己的文档,根据其他人中期报告的句式进行修改。
* [ ] 第二轮,公式、图片、算法输入完成。查阅相关的算法、技术。对报告内容进行完善。包括完成算法的描述、公式的输入输出和论证、技术的描述。查阅软件设计绘图的资料。画完所有系统图、技术图。设计好系统和技术。
* [ ] 第三轮,添加文献引用。
* [ ] 第四轮,修改格式。(将当前的问下,分解成两个自文献——中期报告和毕设论文。)
* [x] 第二轮,公式、图片、算法输入完成。查阅相关的算法、技术。对报告内容进行完善。包括完成算法的描述、公式的输入输出和论证、技术的描述。查阅软件设计绘图的资料。画完所有系统图、技术图。设计好系统和技术。
* [x] 第三轮,添加文献引用。
* [x] 第四轮,修改格式。(将当前的问下,分解成两个自文献——中期报告和毕设论文。)

View File

@@ -8,11 +8,11 @@
### 知识复习——语言
> 由于java和go的重复性。go作为新兴语言的优势。所以掌握go语言。
* [ ] C++
* [ ] 基础知识
* [ ] 标准库
* [ ] 面向对象
* [ ] 设计模式
* [x] C++
* [x] 基础知识
* [x] 标准库
* [x] 面向对象
* [x] 设计模式
* [ ] 并行编程
* [ ] GO
* [ ] 基础知识。
@@ -22,8 +22,19 @@
* [ ] 算法
### 知识复习——基础
* [ ] 操作系统
* [ ] 数据库
* [ ] 基础知识
* [ ] Linux底层原理和常见函数
* [x] 数据库
* [x] 基础知识
* [x] MySQL
* [ ] Redis
* [ ] 消息队列
* [ ] 计算机网络
* [ ] 应用层
* [ ] 网络层
* [ ] 通信层
* [ ] 链路层
* [ ] 物理层
### 知识复习——框架
* [ ] C++/GO框架
* [ ] 微服务框架

View File

@@ -1,10 +1,6 @@
## 任务
## 收获
### 关于工作周期的思考。

View File

@@ -1,8 +1,8 @@
## 任务
- [x] 完成电话的PPT。能够有啥有意思的东西呢无非就是推荐一两个科幻电影和科幻动漫。
- [ ] 完成数据集的设置(加载其他类型的数据集、更多类型的非独立同分布划分和衡量方法)
- [ ] 完成实验的设置(需要对比的地方,需要考虑到可能的影响)基本达到周二要求的水准了。
- [x] 完成数据集的设置(加载其他类型的数据集、更多类型的非独立同分布划分和衡量方法)
- [x] 完成实验的设置(需要对比的地方,需要考虑到可能的影响)基本达到周二要求的水准了。
## 收获

View File

@@ -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)

View File

@@ -0,0 +1,10 @@
# Redis概述
## 概述
Redis 是速度非常快的非关系型NoSQL内存键值数据库可以存储键和五种不同类型的值之间的映射。
键的类型只能为字符串,值支持五种数据类型:字符串、列表、集合、散列表、有序集合。
Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,使用复制来扩展读性能,使用分片来扩展写性能。

View File

@@ -1,99 +0,0 @@
Redis里C字符串只会作为字符串字面量用在一些无需对字符串值进行修改的地方比如打印日志。Redis构建了 简单动态字符串simple dynamic stringSDS来表示字符串值。
在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可以重用<string.h>函数库。
总结:
| C字符串 | SDS |
| ------------------- | ------------------- |
| 获取长度的复杂度O(N) | O(1) |
| API不安全缓冲区溢出 | API安全不会缓冲区溢出 |
| 修改字符串长度必然导致内存重分配 | 修改字符串长度不一定导致内存重分配 |
| 只能保存文本数据 | 可以保存文本或二进制数据 |
| 可使用所有<string.h>库的函数 | 可使用部分<string.h>库的函数 |
# 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)

View File

@@ -0,0 +1,15 @@
# 数据类型
## 0 数据类型
| 数据类型 | 可以存储的值 | 操作 |
| :--: | :--: | -- |
| String | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作</br> 对整数和浮点数执行自增或者自减操作 |
| List | 列表 | 从两端压入或者弹出元素 </br> 对单个或者多个元素进行修剪,</br> 只保留一个范围内的元素 |
| Set | 无序集合 | 添加、获取、移除单个元素</br> 检查一个元素是否存在于集合中</br> 计算交集、并集、差集</br> 从集合里面随机获取元素 |
| Hash | 无序字典 | 添加、获取、移除单个键值对</br> 获取所有键值对</br> 检查某个键是否存在|
| Zset | 有序集合 | 添加、获取、删除元素</br> 根据分值范围或者成员来获取元素</br> 计算一个键的排名 |
![](image/2021-09-02-16-24-45.png)

View File

@@ -0,0 +1,300 @@
# 底层数据结构
## 1 简单动态字符串SDS
Redis构建了 简单动态字符串simple dynamic stringSDS来表示字符串值。
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可以重用<string.h>函数库。
| C字符串 | SDS |
| ------------------- | ------------------- |
| 获取长度的复杂度O(N) | O(1) |
| API不安全缓冲区溢出 | API安全不会缓冲区溢出 |
| 修改字符串长度必然导致内存重分配 | 修改字符串长度不一定导致内存重分配 |
| 只能保存文本数据 | 可以保存文本或二进制数据 |
| 可使用所有<string.h>库的函数 | 可使用部分<string.h>库的函数 |
### 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) |

View File

@@ -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)

View File

@@ -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) |
# 导航

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -1,72 +1,53 @@
# Redis
<!-- GFM-TOC -->
* [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)
- [三、分片](#十三分片)
- [四、一个简单的论坛系统分析](#十四一个简单的论坛系统分析)
- [文章信息](#文章信息)
- [点赞功能](#点赞功能)
- [对文章进行排序](#对文章进行排序)
- [参考资料](#参考资料)
<!-- GFM-TOC -->
## 一、概述
Redis 是速度非常快的非关系型NoSQL内存键值数据库可以存储键和五种不同类型的值之间的映射。
键的类型只能为字符串,值支持五种数据类型:字符串、列表、集合、散列表、有序集合。
Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,使用复制来扩展读性能,使用分片来扩展写性能。
## 二、数据类型
| 数据类型 | 可以存储的值 | 操作 |
| :--: | :--: | :--: |
| STRING | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作\</br\> 对整数和浮点数执行自增或者自减操作 |
| LIST | 列表 | 从两端压入或者弹出元素 \</br\> 对单个或者多个元素进行修剪,\</br\> 只保留一个范围内的元素 |
| SET | 无序集合 | 添加、获取、移除单个元素\</br\> 检查一个元素是否存在于集合中\</br\> 计算交集、并集、差集\</br\> 从集合里面随机获取元素 |
| HASH | 包含键值对的无序散列表 | 添加、获取、移除单个键值对\</br\> 获取所有键值对\</br\> 检查某个键是否存在|
| ZSET | 有序集合 | 添加、获取、删除元素\</br\> 根据分值范围或者成员来获取元素\</br\> 计算一个键的排名 |
> [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
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/6019b2db-bc3e-4408-b6d8-96025f4481d6.png" width="400"/> </div><br>
@@ -559,7 +540,7 @@ Sentinel哨兵可以监听集群中的服务器并在主服务器进入
假设有 4 个 Redis 实例 R0R1R2R3还有很多表示用户的键 user:1user:2... ,有不同的方式来选择一个指定的键存储在哪个实例中。
- 最简单的方式是范围分片,例如用户 id 从 0\~1000 的存储到实例 R0 中,用户 id 从 1001\~2000 的存储到实例 R1 中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。
- 最简单的方式是范围分片,例如用户 id 从 0~1000 的存储到实例 R0 中,用户 id 从 1001~2000 的存储到实例 R1 中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。
- 还有一种方式是哈希分片,使用 CRC32 哈希函数将键转换为一个数字,再对实例数量求模就能知道应该存储的实例。
根据执行分片的位置,可以分为三种分片方式:

Binary file not shown.