redis 知识补充

This commit is contained in:
yinkanglong_lab
2021-04-07 23:29:52 +08:00
parent 30f583d779
commit 0d6737d404
70 changed files with 4889 additions and 33 deletions

View File

@@ -13,10 +13,11 @@
### 实现方式
* 重载是在同一作用域内(不管是模块内还是类内,只要是在同一作用域内),具有相同函数名,不同的形参个数或者形参类型。返回值可以相同也可以不同(在函数名、形参个数、形参类型都相同而返回值类型不同的情况下无法构成重载,编译器报错。这个道理很简单,在函数调用的时候是不看返回值类型的)。
* C++模板机制也能实现重载。基于模板能够实现C++的静态多态。
### 实现原理
* 重载是一种静态多态即在编译的时候确定的。C++实现重载的方式是跟编译器有关编译过后C++的函数名会发生改变,会带有形参个数、类型以及返回值类型的信息(虽然带有返回值类型但是返回值类型不能区分这个函数),所以编译器能够区分具有不同形参个数或者类型以及相同函数名的函数。
* 重载是一种**静态多态**,即在**编译的时候**确定的。C++实现重载的方式是跟编译器有关编译过后C++的函数名会发生改变,会带有形参个数、类型以及返回值类型的信息(虽然带有返回值类型但是返回值类型不能区分这个函数),所以编译器能够区分具有不同形参个数或者类型以及相同函数名的函数。
* 插一句在C语言中编译器编译过后函数名中不会带有形参个数以及类型的信息因此C语言没有重载的特性。由此带来麻烦的一点是如果想要在C++中调用C语言的库需要特殊的操作extern “C”{}。库中的函数经过C编译器编译的话会生成不带有形参信息的函数名而用C++的编译器编译过后会生成带有形参信息的函数名因此将会找不到这个函数。extern “C”{}的作用是使在这个作用域中的语句用C编译器编译这样就不会出错。这也是一种语言兼容性的问题。
## 2 重写
@@ -27,7 +28,7 @@
### 实现原理
* 重写是一种动态多态即在运行时确定的。C++实现重写的方式也跟编译器有关编译器在实例化一个具有虚函数的类时会生成一个vptr指针这就是为什么静态函数、友元函数不能声明为虚函数因为它们不实例化也可以调用而虚函数必须要实例化这也是为什么构造函数不能声明为虚函数因为你要调用虚函数必须得要有vptr指针而构造函数此时还没有被调用内存中还不存在vptr指针逻辑上矛盾了
* 重写是一种**动态多态**,即在**运行时确定**的。C++实现重写的方式也跟编译器有关编译器在实例化一个具有虚函数的类时会生成一个vptr指针这就是为什么静态函数、友元函数不能声明为虚函数因为它们不实例化也可以调用而虚函数必须要实例化这也是为什么构造函数不能声明为虚函数因为你要调用虚函数必须得要有vptr指针而构造函数此时还没有被调用内存中还不存在vptr指针逻辑上矛盾了
* vptr指针在类的内存空间中占最低地址的四字节。vptr指针指向的空间称为虚函数表vptr指针指向其表头在虚函数表里面按声明顺序存放了虚函数的函数指针如果在子类中重写了在子类的内存空间中也会产生一个vptr指针同时会把父类的虚函数表copy一下当做自己的然后如果在子类中重新声明了虚函数会按声明顺序接在父类的虚函数函数指针下。而子类中重写的虚函数则会替换掉虚函数表中原先父类的虚函数函数指针。
* 重点来了,在调用虚函数时,不管调用他的是父类的指针、引用还是子类的指针、引用,他都不管,只看他所指向或者引用的对象的类型(这也称为**动态联编**如果是父类的对象那就调用父类里面的vptr指针然后找到相应的虚函数如果是子类的对象那就调用子类里面的vptr指针然后找到相应的虚函数。
* 当然这样子的过程相比静态多态而言,时间和空间上的开销都多了(这也是为什么内联函数为什么不能声明为虚函数,因为这和内联函数加快执行速度的初衷相矛盾)。

View File

@@ -104,4 +104,4 @@
* 第二周时间完成了数据结构的复习。用两天时间复习了基础的数据结构。然后开始刷题。上周二、周三。一边刷题。一边总结了数据结构相关的代码。一边对算法基础、递归迭代、深度广度搜索进行了总结。对具体的算法的总结还么有开始。
* 第三周继续刷算法题。并对算法进行深入的总结。现在那些较难的算法(动态规划和图算法)还没有完成总结。还包括一些特殊的数据结构,例如单调栈的特性的总结。
* 第四周与第三周的事情交叉进行主要画一天时间复习了Python的相关内容。熟悉了Python、numpy、matplotlib。然后用剩下的时间学习了sklearn机器学习和pytorch深度学习。准备了寒假和开学后做的东西进行组会。感觉剩下的东西有点多。想想这几天做的事情也没有那么多。还需要完成第三周交叉没有完成的算法的总结。
* 第五周主要对基础知识进行总结包括算法的基础知识和理论知识。截止到4.5号。总共五周的时间。复习的内容包括计算机操作系统、数据库、计算机网络。包括算法的枚举法、分治法、动态规划、贪心、回溯法、分支限界、图算法。手写了Dijkstra、floyd、prim、kruscal等算法bellmanford等一系列的算法等以后再学习。基础理论知识包括数据库、计算机网络和操作系统
* 第五周主要对基础知识进行总结包括算法的基础知识和理论知识。截止到4.5号。总共五周的时间。复习的内容包括计算机操作系统、数据库、计算机网络。包括算法的枚举法、分治法、动态规划、贪心、回溯法、分支限界、图算法。手写了Dijkstra、floyd、prim、kruscal等算法bellmanford等一系列的算法等以后再学习,手写了六个排序算法。基础理论知识包括数据库、计算机网络和操作系统。其中操作系统部分需要记忆和搞明白的东西比较多,主要包括三个点:进程/线程的同步和通信原理、进程/线程的同步和通信代码实现、设备IO的实现原理、设备IO的代码实现、网络通信的基本原理、网络通信的代码实现socket编程

View File

@@ -3,10 +3,11 @@
> 完成昨天的任务。
- [x] 学习、复习图算法,动手实现所有的图算法。
- [ ] 看完数据结构与算法的三本书!!!对相关的原理进行复习和总结。
- [ ] ~~看完数据结构与算法的三本书!!!对相关的原理进行复习和总结。~~
- [x] 学习机器学习的实现方案。毕设计划真正的开始执行。
- [x] 关于字符串分割。字符串格式化方法的总结。转换成流,作为流对象处理。转换为容器。作为容器对象处理,使用泛型算法。
## 收获
* 使用递归不能解决动态规划问题。适应为动态规划的子问题有重复。使用递归的方法。会导致重复计算的问题。

View File

@@ -127,3 +127,9 @@
5. 第八周:论文的阅读和复现工作。包括联邦学习和恶意软件机器学习。
## 收获
* 第六周4.5-4.11
* 第七周4.12-4.18
* 第八周4.19-4.25
* 第九周4.25-4.30

View File

@@ -2,7 +2,7 @@
## 计划
* 执行之前的计划
> 执行之前的计划
- [ ] 整理会议记录发到群里。
## 收获

View File

@@ -82,6 +82,7 @@
* 项目概述:单人项目开发。
* 主要工作MFC开发界面、windows网络通信。
* 主要成果:课设。
### ~~补充项目——TensorFlowIO优化~~
* 项目概述分析TensorFlow源代码对源代码进行修改重新编译。使用mmap方法优化TensorFlow数据加载过程中的IO操作。
@@ -108,3 +109,7 @@
## 5 个人能力和性格
1. C++后端开发、Java后端开发、Mysql数据库
2. 学习能力较强。
3. 乐观积极。热衷于开发

View File

@@ -0,0 +1,96 @@
> 一期简历
## 腾讯实习
* 岗位:后端开发——微信事业群
* 技术要求:
* C/C++/Java开发语言
* TCP/UDP网络协议及相关编程、进程间通讯编程
* 专业软件知识,包括算法、操作系统、软件工程、设计模式、数据结构、数据库系统、网络安全等。
* Python、Shell、Perl等脚本语言
* MySQL及SQL语言、编程
* NoSQL, Key-value存储原理。
* (加分项)分布式系统设计与开发、负载均衡技术,系统容灾设计,高可用系统等知识。
* 流程
- [x] 简历投递 join.qq.com
- [ ] 2021年4月8日16:00 1面。准备以上内容。
> 但是TMD之前的面试进度还在。没办法参加第二次面试了早知道直接换个事业群换一波人说不定还好说话。妈卖批。别是上一个boss
## 商汤科技
* 岗位:研究院-后端研发工程师
* 技术要求:
* 熟练使用C/C++、Python、Go中至少一种编程语言
* 熟练使用MySQLPostgreSQL、Redis等主流的关系型、非关系型数据库
* 具有扎实的计算机科学素养,对计算机组成,数据结构和算法,操作系统和编译原理有良好的理解;
* 具备阅读英文文档和开源源码的能力和习惯,能基于需求对开源组件进行快速选型和运用;
* 具有优秀的分析问题和解决问题的能力,以及良好的沟通能力和团队合作能力。
* 具备使用Vue/Angular/React等前端框架进行简单前端开发的能力
* 有机器学习基础含深度学习掌握PyTorch、TensorFlow、MXNet、Caffe等深度学习框架之一
* ~~对UE4/Unity/Blender/Maya等3D动画、游戏工具有过使用经验~~
* 流程
- [x] 简历投递https://hr.sensetime.com/SU604c56f9bef57c3d1a752c60/pb/account.html#/myDeliver
- [ ]
## ~~拼多多~~
> 只有上海的岗位
## 深睿医疗
* 岗位:后端研发
* 技术要求:
* 流程:
- [x] 发送简历到邮箱gongjiayi@deepwise.com
- [ ]
## 字节跳动
* 岗位1后端开发工程师-基础架构(实习)
* 技术要求
* 热爱计算机科学和互联网技术精通至少一门编程语言包括但不仅限于Java、C、C++、PHP、 Python、Golang等
* 掌握扎实的计算机基础知识,深入理解数据结构、算法和操作系统知识;
* 有云计算、分布式存储、研发平台类项目经历优先。
* 岗位2后端开发实习生-产品研发
* 技术要求:
* 掌握PHP、Go、Java、Python、C/C++等任意一门编程语言Go/C++语言优先;
* 熟悉MySQL的使用与优化熟悉Redis/Mongodb/Memcache等NoSQL技术的优先
* 流程
- [x] 简历投递https://jobs.bytedance.com/campus/position/application
- [ ]
## 阿里巴巴
* 岗位研发工程师C++
* 技术要求:
* 或许你熟悉Unix/Linux/Win32环境下编程并有相关开发经验熟练使用调试工具并熟悉某种脚本语言
* 或许你熟悉网络编程和多线程编程对TCP/IPHTTP等网络协议有很深的理解
* 或许你享受底层技术在kernel的源代码中纵横驰骋
* 或许你并不熟悉CC++,但是你不畏挑战,喜欢钻研,能够用你亮眼的成果证明自己超强的学习能力;
* 或许,你参加过大学生数学建模竞赛,“挑战杯”,机器人足球比赛等;
* 投递
- [x] 简历投递
- [x] 素质测评
- [ ] 4.9号笔试
> 二期简历
-------------
## 快手
* 岗位
* 技术要求
* 投递
## 华为实习
## 美团
需要补充的能力:
* Redis nosql非关系型数据库。
* 分布式开发

View File

@@ -0,0 +1,14 @@
## 计划
### **腾讯面试前复习**
- 基础知识复习
- [ ] 数据库
- [ ] 计算机网络
- [ ] 操作系统最后一遍搞清楚同步通信、IO过程相关的问题提
- 腾讯要求复习
- [x] nosql/redis非关系型数据库
- [ ] Hadoop/spark 分布式数据处理(没时间了,等下一轮吧)
- 腾讯面经复习
- [ ] 针对腾讯的面试笔试问题进行复习
## 收获

View File

@@ -5,7 +5,7 @@
单个用户线程:对于十万个用户同时访问服务器,有两种方式处理并发。
1. 为每个用户开一个新的用户现场,每个线程内部采用阻塞通信的方式,即同步通信,从数据库中取数据、与服务器通信等,直到得到结果,返回给用户。其中涉及多个用户线程。
1. 为每个用户开一个新的用户线程,每个线程内部采用阻塞通信的方式,即同步通信,从数据库中取数据、与服务器通信等,直到得到结果,返回给用户。其中涉及多个用户线程。
2. 只有一个用户线程,采用非阻塞通信的方式,即异步通信,通过事件驱动的方式实现并发。从数据库中取数据、与服务器通信或与其他进程通信,并不会阻塞线程的执行,每次数据获取完毕,通过事件的方式,调用用户进程,处理得到的数据,返回给用户。其中,只有一个用户进程。
3. 对于事件驱动的方法:会存在一个事件队列,唯一的用户进程会不断地依次处理队列中的事件。所以不会存在冲突。有两种处理事件的方法:基于监听器的事件处理机制和基于回调的事件处理机制。

View File

@@ -1,11 +1,11 @@
## 2.3 进程同步
## 1 进程同步的基本概念
> 进程同步指的是同步和互斥两种行为。
### 两种制约关系
> 针对两种制约关系,合作制约关系和互斥制约关系,需要通过同步机制实现。
- 直接制约关系(合作)。由于多个进程相互合作产生,使得进程有一定的先后执行关系。
- 直接制约关系(合作/同步)。由于多个进程相互合作产生,使得进程有一定的先后执行关系。
- 间接制约关系(互斥)。由于多个进程资源共享产生,多个进程在同一时刻只有一个进程能进入临界区。
### 临界资源和临界区

View File

@@ -48,6 +48,8 @@
## 1 信号量通信
> 参考文献
> * [https://blog.csdn.net/csdn_kou/article/details/81240666](https://blog.csdn.net/csdn_kou/article/details/81240666)
### 通信原理
### 函数接口
* linux进程中的信号量函数
```C++
@@ -62,14 +64,7 @@ pthread_mutex_lock
pthread_mutex_unlock
pthread_mutex_destroy
```
* 跨平台C库中的信号量(window/linux都可以使用)
```C++
// 标准库中的进程同步和通信
mutex/recursive_mutex;
guard_lock;
unique_lock;
condition_variable;
```
### 编程实现
```C
#include <time.h>
@@ -178,7 +173,7 @@ int main(int argc, char *argv[])
```
## 2 信号通信
### 通信原理
### 函数接口
* linux下的信号机制
```
@@ -191,7 +186,7 @@ int main(int argc, char *argv[])
## 3 共享内存通信
> 参考文献
> * [https://blog.csdn.net/csdn_kou/article/details/82908922](https://blog.csdn.net/csdn_kou/article/details/82908922)
### 类型
### 通信原理
* **共享内存** 相互通信的进程共享某些数据结构或共享存储区,进程之间能够通过这些空间进行通信。据此,又可把它们分成以下两种类型:
* **基于共享数据结构的通信方式**。在这种通信方式中,要求诸进程公用某些数据结构。借以实现诸进程间的信息交换。如在生产者—消费者问题中,就是用有界缓冲区这种数据结构来实现通信的。
@@ -357,11 +352,19 @@ int main(int argc, char *argv[])
```
## 4 管道通信机制的实例——PIPE匿名管道通信
### 管道定义
### 通信原理
* **“管道”** 是指用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,又名 pipe 文件,基于文件的通信机制。向管道(共享文件)提供输入的发送进程(即写进程),以字符流形式将大量的数据送入管道;而接受管道输出的接收进程(即读进程),则从管道中接收(读)数据。由于发送进程和接收进程是利用管道进行通信的,故又称为管道通信。
* **“管道”** 是指用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,又名 pipe 文件,基于文件的通信机制。
* 向管道(共享文件)提供输入的发送进程(即写进程),以字符流形式将大量的数据送入管道;而接受管道输出的接收进程(即读进程),则从管道中接收(读)数据。由于发送进程和接收进程是利用管道进行通信的,故又称为管道通信。
* 有部分博客说。管道通信也属于消息通信中的直接通信的一种。
### 管道实现
* 它具有以下限制:
- 只支持半双工通信(单向交替传输);
- 只能在父子进程或者兄弟进程中使用。
![](image/2021-03-30-08-58-55.png)
### 函数接口
* 管道是通过调用 pipe 函数创建的fd[0] 用于读fd[1] 用于写。
```c
@@ -369,36 +372,31 @@ int main(int argc, char *argv[])
int pipe(int fd[2]);
```
* 它具有以下限制:
- 只支持半双工通信(单向交替传输);
- 只能在父子进程或者兄弟进程中使用。
![](image/2021-03-30-08-58-55.png)
### 编程实现
```C
```
## 5 管道通信机制的实例——FIFO命名管道通信
### 通信原理
* 也称为命名管道,去除了管道只能在父子进程中使用的限制。
* FIFO 常用于客户-服务器应用程序中FIFO 用作汇聚点,在客户进程和服务器进程之间传递数据。
![](image/2021-03-30-08-59-57.png)
### 函数接口
```c
#include <sys/stat.h>
int mkfifo(const char *path, mode_t mode);
int mkfifoat(int fd, const char *path, mode_t mode);
```
* FIFO 常用于客户-服务器应用程序中FIFO 用作汇聚点,在客户进程和服务器进程之间传递数据。
![](image/2021-03-30-08-59-57.png)
### 编程实现
```C
```
## 6 高级管道通信
### 通信原理
### 函数接口
### 编程实现
```C
```

View File

@@ -0,0 +1,231 @@
# 互斥朗、信号量、条件变量.md
> 参考文献
> * [http://blog.chinaunix.net/uid-20205875-id-4865684.html](http://blog.chinaunix.net/uid-20205875-id-4865684.html)
## 1 信号量
### 概念
* 信号量强调的是线程或进程间的同步“信号量用在多线程多任务同步的一个线程完成了某一个动作就通过信号量告诉别的线程别的线程再进行某些动作大家都在sem_wait的时候就阻塞在那里。当信号量为单值信号量是也可以完成一个资源的互斥访问。
### 实现——有名信号量
> 可以用于不同进程间或多线程间的互斥与同步
```C
//创建打开有名信号量
sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
```
* 成功返回信号量指针失败返回SEM_FAILED设置errno
* name是文件路径名,但不能写成/tmp/a.sem这样的形式,因为在linux下,sem都是在/dev/shm目录下,可写成"/mysem"或"mysem",创建出来的文件都是"/dev/shm/sem.mysem",
* mode设置为0666
* value设置为信号量的初始值.所需信号灯等已存在条件下指定
* O_CREAT|O_EXCL是个错误。
```C
//关闭信号量,进程终止时,会自动调用它
int sem_close(sem_t *sem);
```
* 成功返回0失败返回-1设置errno
```C
//删除信号量,立即删除信号量名字,当其他进程都关闭它时,销毁它
int sem_unlink(const char *name);
```
```C
//等待信号量测试信号量的值如果其值小于或等于0那么就等待阻塞一旦其值变为大于0就将它减1并返回
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
```
* 成功返回0失败返回-1设置errno
* 当信号量的值为0时sem_trywait立即返回设置errno为EAGAIN。如果被某个信号中断sem_wait会过早地返回设置errno为EINTR
```C
//发出信号量给它的值加1然后唤醒正在等待该信号量的进程或线程
int sem_post(sem_t *sem);
```
* 成功返回0失败返回-1不会改变它的值设置errno该函数是异步信号安全的可以在信号处理程序里调用它
### 实现——无名信号量
> 用于进程体内各线程间的互斥和同步,使用如下API(无名信号量,基于内存的信号量)
1. sem_init
* 功能:用于创建一个信号量,并初始化信号量的值。
* 函数原型: int sem_init (sem_t* sem, int pshared, unsigned int value);
* 函数传入值: sem:信号量。pshared:决定信号量能否在几个进程间共享。由于目前LINUX还没有实现进程间共享信息量所以这个值只能取0。
2. 其他函数。
* int sem_wait (sem_t* sem);
* int sem_trywait (sem_t* sem);
* int sem_post (sem_t* sem);
* int sem_getvalue (sem_t* sem);
* int sem_destroy (sem_t* sem);
3. 功能:
* sem_wait和sem_trywait相当于P操作它们都能将信号量的值减一两者的区别在于若信号量的值小于零时
* sem_wait将会阻塞进程而sem_trywait则会立即返回。
* sem_post相当于V操作它将信号量的值加一同时发出唤醒的信号给等待的进程或线程
* sem_getvalue 得到信号量的值。
* sem_destroy 摧毁信号量。
> 如果某个基于内存的信号灯是在不同进程间同步的,该信号灯必须存放在共享内存区中,这要只要该共享内存区存在,该信号灯就存在。
## 2 互斥量(又名互斥锁)
### 概念
* 强调的是资源的访问互斥互斥锁是用在多线程多任务互斥的一个线程占用了某一个资源那么别的线程就无法访问直到这个线程unlock其他的线程才开始可以利用这个资源。比如对全局变量的访问有时要加锁操作完了在解锁。有的时候锁和信号量会同时使用的”
* 也就是说信号量不一定是锁定某一个资源而是流程上的概念比如有A,B两个线程B线程要等A线程完成某一任务以后再进行自己下面的步骤这个任务并不一定是锁定某一资源还可以是进行一些计算或者数据处理之类。而线程互斥量则是“锁住某一资源”的概念在锁定期间内其他线程无法对被保护的数据进行操作。
* 在有些情况下两者可以互换。
### 实现——独占互斥锁
* 在linux下, 线程的互斥量数据类型是pthread_mutex_t. 在使用前, 要对它进行初始化:
* 对于静态分配的互斥量, 可以把它设置为PTHREAD_MUTEX_INITIALIZER, 或者调用pthread_mutex_init.
* 对于动态分配的互斥量, 在申请内存(malloc)之后, 通过pthread_mutex_init进行初始化, 并且在释放内存(free)前需要调用pthread_mutex_destroy.
> 互斥量定义
```C
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restric attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
```
* 返回值: 成功则返回0, 出错则返回错误编号.
* 说明: 如果使用默认的属性初始化互斥量, 只需把attr设为NULL. 其他值在以后讲解.
> 加锁函数:
```C
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
```
* 返回值: 成功则返回0, 出错则返回错误编号.
* 说 明: 具体说一下trylock函数, 这个函数是非阻塞调用模式, 也就是说, 如果互斥量没被锁住, trylock函数将把互斥量加锁, 并获得对共享资源的访问权限; 如果互斥量被锁住了, trylock函数将不会阻塞等待而直接返回EBUSY, 表示共享资源处于忙状态.
> 解锁函数:
```C
int pthread_mutex_unlock(pthread_mutex_t *mutex);
```
* 返回值: 成功则返回0, 出错则返回错误编号.
### 互斥锁、信号量、条件变量对比
1. 互斥锁要么被锁住,要么被解开,和二值信号量类似
2. 互斥锁必须是谁上锁就由谁来解锁而信号量的wait和post操作不必由同一个线程执行。
3. sem_post是各种同步技巧中唯一一个能在信号处理程序中安全调用的函数
4. 互斥锁是为上锁而优化的;条件变量是为等待而优化的;信号量既可用于上锁,也可用于等待,因此会有更多的开销和更高的复杂性
5. 互斥锁,条件变量都只用于同一个进程的各线程间,而信号量(有名信号量)可用于不同进程间的同步。当信号量用于进程间同步时,要求信号量建立在共享内存区。
6. 信号量有计数值每次信号量post操作都会被记录而条件变量在发送信号时如果没有线程在等待该条件变量那么信号将丢失。
## 3 读写锁
### 概念
* 读写锁与互斥量类似,不过读写锁允许更高的并行性。互斥量要么是锁住状态要么是不加锁状态,而且一次只有一个线程可以对其加锁。
* 读写锁可以由三种状态:读模式下加锁状态、写模式下加锁状态、不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。
* 在读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是如果线程希望以写模式对此锁进行加锁,它必须阻塞直到所有的线程释放读锁。虽然读写锁的实现各不相同,但当读写锁处于读模式锁住状态时,如果有另外的线程试图以写模式加锁,读写锁通常会阻塞随后的读模式锁请求。这样可以避免读模式锁长期占用,而等待的写模式锁请求一直得不到满足。
* 读写锁非常适合于对数据结构读的次数远大于写的情况。当读写锁在写模式下时,它所保护的数据结构就可以被安全地修改,因为当前只有一个线程可以在写模式下拥有这个锁。当读写锁在读状态下时,只要线程获取了读模式下的读写锁,该锁所保护的数据结构可以被多个获得读模式锁的线程读取。
* 读写锁也叫做共享-独占锁,当读写锁以读模式锁住时,它是以共享模式锁住的;当他以写模式锁住时,它是以独占模式锁住的。
### 实现——读写锁
> 初始化和销毁
```C
#include<>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
```
* 成功则返回0, 出错则返回错误编号.
* 同互斥量以上, 在释放读写锁占用的内存之前, 需要先通过pthread_rwlock_destroy对读写锁进行清理工作, 释放由init分配的资源.
> 读和写:
```
#include<>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
```
* 成功则返回0, 出错则返回错误编号.
* 这3个函数分别实现获取读锁, 获取写锁和释放锁的操作. 获取锁的两个函数是阻塞操作,
> 非阻塞的函数为:
```C
#include<>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
```
* 成功则返回0, 出错则返回错误编号.
* 非阻塞的获取锁操作, 如果可以获取则返回0, 否则返回错误的EBUSY.
### 读写锁性能
* 虽然读写锁提高了并行性,但是就速度而言并不比互斥量快.可能这也是即使有读写锁存在还会使用互斥量的原因,因为他在速度方面略胜一筹。这就需要我们在写程序的时候
* 综合考虑速度和并行性并找到一个折中。
* 比如假设使用互斥量需要0.5秒使用读写锁需要0.8秒。在类似学生管理系统这类软件中可能百分之九十的时间都是查询操作那么假如现在突然来个个20个请求如果使用的是互斥量那么最后的那个查询请求被满足需要10后。这样估计没人能受得了。而使用读写锁应为读锁能够多次获得。所以所有的20个请求每个请求都能在1秒左右得到满足。也就是说在一些写操作比较多或是本身需要同步的地方并不多的程序中我们应该使用互斥量而在读操作远大于写操作的一些程序中我们应该使用读写锁来进行同步
## 4 条件变量(condition)
### 概念
* 条件变量常与互斥锁同时使用达到线程同步的目的条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足。在发送信号时如果没有线程等待在该条件变量上那么信号将丢失而信号量有计数值每次信号量post操作都会被记录。
* 条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。
* 条件本身是由互斥量保护的。线程在改变条件状态前必须首先锁住互斥量,其它线程在获得互斥量之前不会察觉到这种改变,因此必须锁定互斥量以后才能计算条件。
* 条件的检测是在互斥锁的保护下进行的。如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步。
### 实现——条件变量
> 初始化
* 条件变量采用的数据类型是pthread_cond_t, 在使用之前必须要进行初始化, 这包括两种方式:
* 静态: 可以把常量PTHREAD_COND_INITIALIZER给静态分配的条件变量.
* 动态: pthread_cond_init函数, 是释放动态条件变量的内存空间之前, 要用pthread_cond_destroy对其进行清理.
```c
#include<>
int pthread_cond_init(pthread_cond_t *restrict cond, pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
```
* 成功则返回0, 出错则返回错误编号.
* 注意:条件变量占用的空间并未被释放。
* 当pthread_cond_init的attr参数为NULL时, 会创建一个默认属性的条件变量; 非默认情况以后讨论.
> 等待条件
```C
#include
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restric mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict timeout);
```
* 成功则返回0, 出错则返回错误编号。
* 这两个函数分别是阻塞等待和超时等待。等待条件函数等待条件变为真, 传递给pthread_cond_wait的互斥量对条件进行保护, 调用者把锁住的互斥量传递给函数. 函数把调用线程放到等待条件的线程列表上, 然后对互斥量解锁, 这两个操作是原子的. 这样 便关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道, 这样线程就不会错过条件的任何变化.
* 当pthread_cond_wait返回时, 互斥量再次被锁住.
* pthread_cond_wait函数的返回并不意味着条件的值一定发生了变化必须重新检查条件的值。
* pthread_cond_wait函数返回时相应的互斥锁将被当前线程锁定即使是函数出错返回。
* 阻塞在条件变量上的线程被唤醒以后直到pthread_cond_wait()函数返回之前条件的值都有可能发生变化。所以函数返回以后,在锁定相应的互斥锁之前,必须重新测试条件值。最好的测试方法是循环调
* pthread_cond_wait函数并把满足条件的表达式置为循环的终止条件。
```c
pthread_mutex_lock();
while (condition_is_false)
pthread_cond_wait();
pthread_mutex_unlock();
```
* 阻塞在同一个条件变量上的不同线程被释放的次序是不一定的。
* 注意pthread_cond_wait()函数是退出点,如果在调用这个函数时,已有一个挂起的退出请求,且线程允许退出,这个线程将被终止并开始执行善后处理函数,而这时和条件变量相关的互斥锁仍将处在锁定状态。
* pthread_cond_timedwait函数到了一定的时间即使条件未发生也会解除阻塞。这个时间由参数abstime指定。函数返回时相应的互斥锁往往是锁定的即使是函数出错返回。
* 注意pthread_cond_timedwait函数也是退出点。
* 超时时间参数是指一天中的某个时刻。使用举例:
```
pthread_timestruc_t to;
to.tv_sec = time(NULL) + TIMEOUT;
to.tv_nsec = 0;
```
* 超时返回的错误码是ETIMEDOUT。
> 通知条件:
```
#include
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
```
* 成功则返回0, 出错则返回错误编号.
* 这两个函数用于通知线程条件已经满足. 调用这两个函数, 也称向线程或条件发送信号. 必须注意, 一定要在改变条件状态以后再给线程发送信号.

View File

@@ -117,3 +117,11 @@
## 15 五种IO模型和epoll机制
## 16 终极三连问
1. 进程/线程的同步和通信原理、
2. 进程/线程的同步和通信代码实现(并行编程)、
3. 设备IO的实现原理、
4. 设备IO的代码实现、
5. 网络通信的基本原理、
6. 网络通信的代码实现socket编程网络编程

795
数据库/Redis/0redis.md Normal file
View File

@@ -0,0 +1,795 @@
# redis笔记
<!-- TOC -->
- [redis笔记](#redis笔记)
- [零、redis是什么](#零redis是什么)
- [一、redis与memcached比较](#一redis与memcached比较)
- [二、安装](#二安装)
- [三、配置](#三配置)
- [四、通用key操作](#四通用key操作)
- [五、redis中的5中数据结构](#五redis中的5中数据结构)
- [1. 字符串string](#1-字符串string)
- [2. 列表list链表支持 有序 可重复](#2-列表list链表支持-有序-可重复)
- [3. 集合set无序 不可重复](#3-集合set无序-不可重复)
- [4. 哈希hash键值对 key => value](#4-哈希hash键值对--key--value)
- [5. 有序集合zset键值对 成员 => 分值 成员必须唯一](#5-有序集合zset键值对--成员--分值-成员必须唯一)
- [六、Redis详细数据类型](#六redis详细数据类型)
- [1、string 字符串](#1string-字符串)
- [2、list 列表](#2list-列表)
- [3、set 集合](#3set-集合)
- [4、sorted set 有序集合](#4sorted-set-有序集合)
- [5、hash 哈希](#5hash-哈希)
- [6、bitmap 位图](#6bitmap-位图)
- [7、geo 地理位置类型](#7geo 地理位置类型)
- [8、hyperLogLog 基数统计](#8hyperloglog-基数统计)
- [七、redis事务](#七redis事务)
- [1、mysql事务与redis事务比较](#1mysql事务与redis事务比较)
- [2、悲观锁与乐观锁](#2悲观锁与乐观锁)
- [八、发布订阅](#八发布订阅)
- [九、持久化](#九持久化)
- [1、redis 快照rdb](#1redis-快照rdb)
- [2、redis 日志aof](#2redis-日志aof)
- [十、redis主从复制](#十redis主从复制)
- [十一、redis表设计](#十一redis表设计)
- [十二、面试](#十二面试)
- [1、缓存雪崩](#1缓存雪崩)
- [2、缓存穿透](#2缓存穿透)
- [3、缓存与数据库读写一致](#3缓存与数据库读写一致)
- [十三、docker实现redis主从](#十三docker实现redis主从)
- [1、命令行模式](#1命令行模式)
- [2、docker-compose模式 推荐](#2docker-compose模式-推荐)
- [十四、参考资料](#十四参考资料)
<!-- /TOC -->
## 零、redis是什么
redis是什么是一种非关系型数据库统称nosql。
## 一、redis与memcached比较
- 1、redis受益于“持久化”可以做存储(storge)memcached只能做缓存(cache)
- 2、redis有多种数据结构memcached只有一种类型`字符串(string)`
## 二、安装
安装最新稳定版
```sh
# 源码安装redis-4.0
# 下载
wget http://download.redis.io/releases/redis-4.0.1.tar.gz
# 解压
tar zxvf redis-4.0.1.tar.gz
cd redis-4.0.1
# 编译
make && make install
/usr/local/bin/redis-server -v
```
## 三、配置
- redis-benchmark redis性能测试工具
- redis-check-aof 检查aof日志的工具
- redis-check-rdb 检查rdb日志的工具
- redis-cli 连接用的客户端
- redis-server 服务进程
```sh
# 地址
bind 0.0.0.0
# 保护模式
protected-mode no
# 端口
port 6380
tcp-backlog 511
timeout 0
tcp-keepalive 300
# 守护进程模式
daemonize yes
supervised no
# 进程id文件
pidfile /usr/local/redis/run/redis.pid
# 日志等级
loglevel notice
# 日志位置
logfile /usr/local/redis/logs/redis.log
# 数据个数
databases 16
always-show-logo yes
# after 900 sec (15 min) if at least 1 key changed
# after 300 sec (5 min) if at least 10 keys changed
# after 60 sec if at least 10000 keys changed
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
# rdb开启
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
rdb-del-sync-files no
dir ./
# 主从
# +------------------+ +---------------+
# | Master | ---> | Replica |
# | (receive writes) | | (exact copy) |
# +------------------+ +---------------+
acllog-max-len 128
# 密码
requirepass omgzui
lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del no
replica-lazy-flush no
lazyfree-lazy-user-del no
oom-score-adj no
oom-score-adj-values 0 200 800
# aof
appendonly yes
appendfilename "appendonly.aof"
# appendfsync always
appendfsync everysec
# appendfsync no
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
aof-use-rdb-preamble yes
lua-time-limit 5000
# 从服务器
# cluster-announce-ip 10.1.1.5
# cluster-announce-port 6379
# cluster-announce-bus-port 6380
slowlog-log-slower-than 10000
slowlog-max-len 128
latency-monitor-threshold 0
notify-keyspace-events ""
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-size -2
list-compress-depth 0
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
hll-sparse-max-bytes 3000
stream-node-max-bytes 4096
stream-node-max-entries 100
activerehashing yes
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit replica 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
hz 10
dynamic-hz yes
aof-rewrite-incremental-fsync yes
rdb-save-incremental-fsync yes
jemalloc-bg-thread yes
```
## 四、通用key操作
1. keys 查询
```sh
在redis里,允许模糊查询key
有3个通配符 - ? []
-: 通配任意多个字符
?: 通配单个字符
[]: 通配括号内的某1个字符
```
1. keys 查询
2. del 删除
3. rename 重命名
4. move 移到另外一个库
5. randomkey 随机
6. exists 存在
7. type 类型
8. ttl 剩余生命周期
9. expire 设置生命周期
10. persist 永久有效
11. flushdb 清空
## 五、redis中的5中数据结构
### 1. 字符串string
- set
- set name shengj -> OK
- get
- get name -> "shengj"
- del
- del name -> (integer) 1
- get name -> (nil)
- mset
- mset name shengj age 23 sex male -> OK
- mget
- mget age sex
```sh
1) "23"
2) "male"
```
- setrange
- setrange sex 2 1 将sex的第3个字符改成1 -> (integer) 4
- get sex -> "ma1e"
- append
- append name GG -> (integer) 8
- get name -> "shengjGG"
- getrange
- getrange name 1 2 -> "he"
- incr 自增
- incrby 自增一个量级
- incrbyfloat 自增一个浮点数
- decr 递减
- decrby 递减一个量级
- decrbyfloat 递减一个浮点数
- setbit 设置二进制位数
- getbit 获取二进制表示
- bitop 位操作
---
### 2. 列表list链表支持 有序 可重复
- rpush 右边插入
- rpush list item1 -> (integer) 1
- rpush list item2 -> (integer) 2
- rpush list item3 -> (integer) 3
- lrange 列出链表值
- lrange list 0 -1
```sh
1) "item1"
2) "item2"
3) "item3"
```
- lindex
- lindex list 1 -> "item2"
- lpop
- lpop list -> "item1"
- lrange list 0 -1
```sh
1) "item2"
2) "item3"
```
- ltrim
- ltrim list 3 0 -> OK
- lrange list 0 -1 -> (empty list or set)
- lpush 左边插入
- rpop 右边删除
- lrem
---
### 3. 集合set无序 不可重复
- sadd 增加
- sadd set item1 -> (integer) 1
- sadd set item2 -> (integer) 1
- sadd set item3 -> (integer) 1
- sadd set item1 -> (integer) 0 已存在
- smembers 所有集合元素
- smembers set
```sh
1) "item3"
2) "item2"
3) "item1"
```
- sismember 存不存在
- sismember set item1 -> (integer) 1
- sismember set item -> (integer) 0 不存在
- srem 移除元素
- srem set item1 -> (integer) 1
- smembers set
```sh
1) "item3"
2) "item2"
```
- spop 随机删除一个元素
- srandmember 随机获取一个元素 -> 抽奖
- scard 多少个元素
- smove 移动
- sinter 交集
- sinterstore 交集并赋值
- suion 并集
- sdiff 差集
---
### 4. 哈希hash键值对 key => value
- hset 设置一个
- hset hash key1 value1 -> (integer) 1
- hset hash key2 value2 -> (integer) 1
- hset hash key3 value3 -> (integer) 1
- hset hash key1 value1 -> (integer) 0 已存在
- hgetall 获取全部
- hgetall hash
```sh
1) "key1"
2) "value1"
3) "key2"
4) "value2"
5) "key3"
6) "value3"
```
- hget 获取一个
- hget hash key1 -> "value1"
- hdel 删除
- hdel hash key1 -> (integer) 1
- hgetall hash
```sh
1) "key2"
2) "value2"
3) "key3"
4) "value3"
```
- hmset 设置多个
- hmget 获取多个
- hlen 个数
- hexists 是否存在增长
- hinrby 增长
- hkeys 所有的key
- hvals 所有的值
---
### 5. 有序集合zset键值对 成员 => 分值 成员必须唯一
- zadd 增加
- zadd zset 100 item1 -> (integer) 1
- zadd zset 200 item2 -> (integer) 1
- zadd zset 300 item3 -> (integer) 1
- zadd zset 100 item1 -> (integer) 0 已存在
- zrange 按分值排序
- zrange zset 0 -1 withscores
```sh
1) "item1"
2) "100"
3) "item2"
4) "200"
5) "item3"
6) "300"
```
- zrangebyscore 按分值的一部分排序
- zrangebyscore zset 0 200 withscores
```sh
1) "item1"
2) "100"
3) "item2"
4) "200"
```
- zrem 删除
- zrem zset item1 -> (integer) 1
- zrange zset 0 -1 withscores
```sh
1) "item2"
2) "200"
3) "item3"
4) "300"
```
- zrank 排名升序
- zremrangebyscore 按分值删除一部分
- zremrangebyrank 按排名删除一部分
- zcard 个数
## 六、Redis详细数据类型
首先来看一下 Redis 的核心数据类型。Redis  8 种核心数据类型分别是 
- string 字符串类型
- list 列表类型
- set 集合类型
- sorted set 有序集合类型
- hash 类型
- bitmap 位图类型
- geo 地理位置类型
- HyperLogLog 基数统计类型。
### 1、string 字符串
string  Redis 的最基本数据类型。可以把它理解为 Mc  key 对应的 value 类型。string 类型是二进制安全的 string 中可以包含任何数据。
Redis 中的普通 string 采用 raw encoding 即原始编码方式该编码方式会动态扩容并通过提前预分配冗余空间来减少内存频繁分配的开销。
在字符串长度小于 1MB 按所需长度的 2 倍来分配超过 1MB则按照每次额外增加 1MB 的容量来预分配。
Redis 中的数字也存为 string 类型但编码方式跟普通 string 不同数字采用整型编码字符串内容直接设为整数值的二进制字节序列。
在存储普通字符串序列化对象以及计数器等场景时都可以使用 Redis 的字符串类型字符串数据类型对应使用的指令包括 set、get、mset、incr、decr 等。
### 2、list 列表
Redis  list 列表是一个快速双向链表存储了一系列的 string 类型的字串值。list 中的元素按照插入顺序排列。插入元素的方式可以通过 lpush 将一个或多个元素插入到列表的头部也可以通过 rpush 将一个或多个元素插入到队列尾部还可以通过 lset、linsert 将元素插入到指定位置或指定元素的前后。
list 列表的获取可以通过 lpop、rpop 从对头或队尾弹出元素如果队列为空则返回 nil。还可以通过 Blpop、Brpop 从队头/队尾阻塞式弹出元素如果 list 列表为空没有元素可供弹出则持续阻塞直到有其他 client 插入新的元素。这里阻塞弹出元素可以设置过期时间避免无限期等待。最后list 列表还可以通过 LrangeR 获取队列内指定范围内的所有元素。Redis list 列表的偏移位置都是基于 0 的下标即列表第一个元素的下标是 0第二个是 1。偏移量也可以是负数倒数第一个是 -1倒数第二个是 -2依次类推。
list 列表对于常规的 pop、push 元素性能很高时间复杂度为 O(1),因为是列表直接追加或弹出。但对于通过随机插入、随机删除,以及随机范围获取,需要轮询列表确定位置,性能就比较低下了。
feed timeline 存储时由于 feed id 一般是递增的可以直接存为 list用户发表新 feed就直接追加到队尾。另外消息队列、热门 feed 等业务场景都可以使用 list 数据结构。
操作 list 列表时可以用 lpush、lpop、rpush、rpop、lrange 来进行常规的队列进出及范围获取操作在某些特殊场景下也可以用 lset、linsert 进行随机插入操作 lrem 进行指定元素删除操作最后在消息列表的消费时还可以用 Blpop、Brpop 进行阻塞式获取从而在列表暂时没有元素时可以安静的等待新元素的插入而不需要额外持续的查询。
### 3、set 集合
set  string 类型的无序集合set 中的元素是唯一的 set 中不会出现重复的元素。Redis 中的集合一般是通过 dict 哈希表实现的所以插入、删除以及查询元素可以根据元素 hash 值直接定位时间复杂度为 O(1)。
 set 类型数据的操作除了常规的添加、删除、查找元素外还可以用以下指令对 set 进行操作。
sismember 指令判断该 key 对应的 set 数据结构中是否存在某个元素如果存在返回 1否则返回 0
sdiff 指令来对多个 set 集合执行差集
sinter 指令对多个集合执行交集
sunion 指令对多个集合执行并集
spop 指令弹出一个随机元素
srandmember 指令返回一个或多个随机元素。
set 集合的特点是查找、插入、删除特别高效时间复杂度为 O(1)所以在社交系统中可以用于存储关注的好友列表用来判断是否关注还可以用来做好友推荐使用。另外还可以利用 set 的唯一性来对服务的来源业务、来源 IP 进行精确统计。
### 4、sorted set 有序集合
Redis 中的 sorted set 有序集合也称为 zset有序集合同 set 集合类似也是 string 类型元素的集合且所有元素不允许重复。
但有序集合中每个元素都会关联一个 double 类型的 score 分数值。有序集合通过这个 score 值进行由小到大的排序。有序集合中元素不允许重复 score 分数值却允许重复。
有序集合除了常规的添加、删除、查找元素外,还可以通过以下指令对 sorted set 进行操作。
zscan 指令按顺序获取有序集合中的元素
zscore 指令获取元素的 score 
zrange指令通过指定 score 返回指定 score 范围内的元素;
在某个元素的 score 值发生变更时还可以通过 zincrby 指令对该元素的 score 值进行加减。
通过 zinterstore、zunionstore 指令对多个有序集合进行取交集和并集然后将新的有序集合存到一个新的 key 如果有重复元素重复元素的 score 进行相加然后作为新集合中该元素的 score 值。
sorted set 有序集合的特点是
所有元素按 score 排序而且不重复
查找、插入、删除非常高效时间复杂度为 O(1)。
因此可以用有序集合来统计排行榜实时刷新榜单还可以用来记录学生成绩从而轻松获取某个成绩范围内的学生名单还可以用来对系统统计增加权重值从而在 dashboard 实时展示。
### 5、hash 哈希
Redis 中的哈希实际是 field  value 的一个映射表。
hash 数据结构的特点是在单个 key 对应的哈希结构内部可以记录多个键值对 field  value value 可以是任何字符串。而且这些键值对查询和修改很高效。
所以可以用 hash 来存储具有多个元素的复杂对象然后分别修改或获取这些元素。hash 结构中的一些重要指令包括hmset、hmget、hexists、hgetall、hincrby 等。
hmset 指令批量插入多个 field、value 映射;
hmget 指令获取多个 field 对应的 value 值;
hexists 指令判断某个 field 是否存在;
如果 field 对应的 value 是整数,还可以用 hincrby 来对该 value 进行修改。
### 6、bitmap 位图
Redis 中的 bitmap 位图是一串连续的二进制数字底层实际是基于 string 进行封装存储的 bit 位进行指令操作的。bitmap 中每一 bit 位所在的位置就是 offset 偏移可以用 setbit、bitfield 对 bitmap 中每个 bit 进行置 0 或置 1 操作也可以用 bitcount 来统计 bitmap 中的被置 1  bit 还可以用 bitop 来对多个 bitmap 进行求与、或、异或等操作。
bitmap 位图的特点是按位设置、求与、求或等操作很高效而且存储成本非常低用来存对象标签属性的话一个 bit 即可存一个标签。可以用 bitmap存用户最近 N 天的登录情况每天用 1 bit登录则置 1。个性推荐在社交应用中非常重要可以对新闻、feed 设置一系列标签如军事、娱乐、视频、图片、文字等 bitmap 来存储这些标签在对应标签 bit 位上置 1。对用户也可以采用类似方式记录用户的多种属性并可以很方便的根据标签来进行多维度统计。bitmap 位图的重要指令包括setbit、 getbit、bitcount、bitfield、 bitop、bitpos 等。
### 7、geo 地理位置类型
在移动社交时代LBS 应用越来越多比如微信、陌陌中附近的人美团、大众点评中附近的美食、电影院滴滴、优步中附近的专车等。要实现这些功能就得使用地理位置信息进行搜索。地球的地理位置是使用二维的经纬度进行表示的我们只要确定一个点的经纬度就可以确认它在地球的位置。
Redis  3.2 版本之后增加了对 GEO 地理位置的处理功能。Redis  GEO 地理位置本质上是基于 sorted set 封装实现的。在存储分类 key 下的地理位置信息时需要对该分类 key 构建一个 sorted set 作为内部存储结构用于存储一系列位置点。
在存储某个位置点时首先利用 Geohash 算法将该位置二维的经纬度映射编码成一维的 52 位整数值将位置名称、经纬度编码 score 作为键值对存储到分类 key 对应的 sorted set 中。
需要计算某个位置点 A 附近的人时首先以指定位置 A 为中心点以距离作为半径算出 GEO 哈希 8 个方位的范围 然后依次轮询方位范围内的所有位置点只要这些位置点到中心位置 A 的距离在要求距离范围内就是目标位置点。轮询完所有范围内的位置点后重新排序即得到位置点 A 附近的所有目标。
使用 geoadd将位置名称如人、车辆、店名与对应的地理位置信息添加到指定的位置分类 key 
使用 geopos 方便地查询某个名称所在的位置信息
使用 georadius 获取指定位置附近不超过指定距离的所有元素
使用 geodist 来获取指定的两个位置之间的距离。
这样,是不是就可以实现,找到附近的餐厅,算出当前位置到对应餐厅的距离,这样的功能了?
Redis GEO 地理位置利用 Geohash 将大量的二维经纬度转一维的整数值这样可以方便的对地理位置进行查询、距离测量、范围搜索。但由于地理位置点非常多一个地理分类 key 下可能会有大量元素 GEO 设计时需要提前进行规划避免单 key 过度膨胀。
Redis  GEO 地理位置数据结构应用场景很多比如查询某个地方的具体位置查当前位置到目的地的距离查附近的人、餐厅、电影院等。GEO 地理位置数据结构中重要指令包括 geoadd、geopos、geodist、georadius、georadiusbymember 等。
### 8、hyperLogLog 基数统计
Redis  hyperLogLog 是用来做基数统计的数据类型当输入巨大数量的元素做统计时只需要很小的内存即可完成。HyperLogLog 不保存元数据只记录待统计元素的估算数量这个估算数量是一个带有 0.81% 标准差的近似值在大多数业务场景对海量数据不足 1% 的误差是可以接受的。
Redis  HyperLogLog 在统计时如果计数数量不大采用稀疏矩阵存储随着计数的增加稀疏矩阵占用的空间也会逐渐增加当超过阀值后则改为稠密矩阵稠密矩阵占用的空间是固定的约为12KB字节。
通过 hyperLoglog 数据类型你可以利用 pfadd 向基数统计中增加新的元素可以用 pfcount 获得 hyperLogLog 结构中存储的近似基数数量还可以用 hypermerge 将多个 hyperLogLog 合并为一个 hyperLogLog 结构从而可以方便的获取合并后的基数数量。
hyperLogLog 的特点是统计过程不记录独立元素占用内存非常少非常适合统计海量数据。在大中型系统中统计每日、每月的 UV 即独立访客数或者统计海量用户搜索的独立词条数都可以用 hyperLogLog 数据类型来进行处理。
## 七、redis事务
### 1、mysql事务与redis事务比较
|比较|mysql|redis|
|---|---|---|
|开启|start transaction|multi|
|语句|普通sql语句|普通redis命令|
|失败|rollback|discard|
|成功|commit|exec|
如果已经成功执行了2条语句, 第3条语句出错.
rollback后,前2条的语句影响消失.
discard只是结束本次事务,前2条语句造成的影响仍然还在
### 2、悲观锁与乐观锁
我正在买票`ticket -1 , money -100`而票只有1张, 如果在我multi之后,和exec之前, 票被别人买了,即ticket变成0了.我该如何观察这种情景,并不再提交
悲观的想法:
世界充满危险,肯定有人和我抢, 给 ticket上锁, 只有我能操作. [悲观锁]
乐观的想法:
没有那么人和我抢,因此,我只需要注意,
--有没有人更改ticket的值就可以了 [乐观锁]
Redis的事务中,启用的是乐观锁,只负责监测key没有被改动
```sh
具体的命令---- watch命令
redis 127.0.0.1:6379> watch ticket
OK
redis 127.0.0.1:6379> multi
OK
redis 127.0.0.1:6379> decr ticket
QUEUED
redis 127.0.0.1:6379> decrby money 100
QUEUED
redis 127.0.0.1:6379> exec
(nil) // 返回nil,说明监视的ticket已经改变了,事务就取消了.
redis 127.0.0.1:6379> get ticket
"0"
redis 127.0.0.1:6379> get money
"200"
watch key1 key2 ... keyN
作用:监听key1 key2..keyN有没有变化,如果有变, 则事务取消
unwatch
作用: 取消所有watch监听
```
## 八、发布订阅
订阅端: subscribe 频道名称
发布端: publish 频道名称 发布内容
## 九、持久化
### 1、redis 快照rdb
有限制,还是容易数据丢失,恢复快
```sh
save 900 1 # 900内,有1条写入,则产生快照
save 300 1000 # 如果300秒内有1000次写入,则产生快照
save 60 10000 # 如果60秒内有10000次写入,则产生快照
(这3个选项都屏蔽,则rdb禁用)
stop-writes-on-bgsave-error yes # 后台备份进程出错时,主进程停不停止写入?
rdbcompression yes # 导出的rdb文件是否压缩
Rdbchecksum yes # 导入rbd恢复时数据时,要不要检验rdb的完整性
dbfilename dump.rdb # 导出来的rdb文件名
dir ./ //rdb的放置路径
```
### 2、redis 日志aof
```sh
appendonly no # 是否打开 aof日志功能
appendfsync always # 每1个命令,都立即同步到aof. 安全,速度慢
appendfsync everysec # 折衷方案,每秒写1次
appendfsync no # 写入工作交给操作系统,由操作系统判断缓冲区大小,统一写入到aof. 同步频率低,速度快,
no-appendfsync-on-rewrite yes: # 正在导出rdb快照的过程中,要不要停止同步aof
auto-aof-rewrite-percentage 100 #aof文件大小比起上次重写时的大小,增长率100%时,重写
auto-aof-rewrite-min-size 64mb #aof文件,至少超过64M时,重写
```
注: 在dump rdb过程中,aof如果停止同步,会不会丢失?
答: 不会,所有的操作缓存在内存的队列里, dump完成后,统一操作.
注: aof重写是指什么?
答: aof重写是指把内存中的数据,逆化成命令,写入到.aof日志里.以解决 aof日志过大的问题.
问: 如果rdb文件,和aof文件都存在,优先用谁来恢复数据?
答: aof
问: 2种是否可以同时用?
答: 可以,而且推荐这么做
问: 恢复时rdb和aof哪个恢复的快
答: rdb快,因为其是数据的内存映射,直 接载入到内存,而aof是命令,需要逐条执行
## 十、redis主从复制
```sh
Master配置:
1:关闭rdb快照(备份工作交给slave)
2:可以开启aof
slave配置:
1: 声明slave-of
2: 配置密码[如果master有密码]
3: [某1个]slave打开 rdb快照功能
4: 配置是否只读[slave-read-only]
```
## 十一、redis表设计
主键表
|列名|操作|备注|
|--|--|--|
|global:user_id|incr|全局user_id|
|global:post_id|incr|全局post_id|
---
mysql用户表
|列名|操作|备注||
|--|--|--|--|
|user_id|user_name|password|authsecret|
|1|shengj|123456|,./!@#|
redis用户表
|列名|操作|备注||
|--|--|--|--|
|user:user_id|user:user_id:*:user_name|user:user_id:*:password|user:user_id:*:authsecret|
|1|shengj|123456|,./!@#|
---
mysql发送表
|列名|操作|备注|||
|--|--|--|--|--|
|post_id|user_id|user_name|time|content|
|1|1|shengj|1370987654|测试内容|
redis发送表
|列名|操作|备注|||
|--|--|--|--|--|
|post:post_id|post:post_id:*:user_id|post:post_id:*:user_name|post:post_id:*:time|post:post_id:*:content|
|1|1|shengj|1370987654|测试内容|
---
关注表following -> set user_id
粉丝表follower -> set user_id
推送表receivepost -> list user_ids
拉取表pullpost -> zset user_ids
## 十二、面试
### 1、缓存雪崩
问题当我们的缓存失效或者redis挂了那么这个时候的请求都会直接走数据库就会给数据库造成极大的压力导致数据库也挂了
解决:
1. 对缓存设置不同的过期时间,这样就不会导致缓存同时失效
2. 建立redis集群保证服务的可靠性
### 2、缓存穿透
问题:当有大量用户不走我们设置的键值,就会直接走数据库,就会给数据库造成极大的压力,导致数据库也挂了
解决:
1. 参数过滤和提醒,引导用户走我们的设置的键值
2. 对不合法的参数进行空对象缓存,并设置较短的过期时间
### 3、缓存与数据库读写一致
问题:如果一直是读的话,是没问题的,但是更新操作会导致数据库已经更新了,缓存还是旧的数据
解决:
并发下解决数据库与缓存不一致的思路:将删除缓存、修改数据库、读取缓存等的操作积压到队列里边,实现串行化。
- 先删除缓存,再更新数据库
在高并发下表现不如意,在原子性被破坏时表现优异
- 先更新数据库,再删除缓存(Cache Aside Pattern设计模式)
在高并发下表现优异,在原子性被破坏时表现不如意
## 十三、docker实现redis主从
[docker实现redis主从](https://github.com/OMGZui/redis_m_s)
### 1、命令行模式
```bash
# 拉取redis
docker pull redis
# 主
docker run -v $(pwd)/master/redis.conf:/usr/local/etc/redis/redis.conf --name redis-master redis redis-server /usr/local/etc/redis/redis.conf
# 从1 --link redis-master:master master是别名
docker run -v $(pwd)/slave1/redis.conf:/usr/local/etc/redis/redis.conf --name redis-slave1 --link redis-master:master redis redis-server /usr/local/etc/redis/redis.conf
# 从2
docker run -v $(pwd)/slave2/redis.conf:/usr/local/etc/redis/redis.conf --name redis-slave2 --link redis-master:master redis redis-server /usr/local/etc/redis/redis.conf
```
### 2、docker-compose模式 推荐
```bash
# 拉取redis
docker pull redis
# 目录
├── docker-compose.yml
├── master
│   ├── Dockerfile
│   └── redis.conf
├── redis.conf
├── slave1
│   ├── Dockerfile
│   └── redis.conf
└── slave2
├── Dockerfile
└── redis.conf
# 启动
docker-compose up -d master slave1 slave2
# 查看主容器
docker-compose exec master bash
root@cab5db8d544b:/data# redis-cli
127.0.0.1:6379> info Replication
# Replication
role:master
connected_slaves:2
slave0:ip=172.23.0.3,port=6379,state=online,offset=1043,lag=0
slave1:ip=172.23.0.4,port=6379,state=online,offset=1043,lag=0
master_replid:995257c6b5ac62f7908cc2c7bb770f2f17b60401
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1043
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:1043
```
## 十四、参考资料
- [redis](https://redis.io/)
- [Docker创建Redis集群](https://lw900925.github.io/docker/docker-redis-cluster.html)

58
数据库/Redis/README.md Normal file
View File

@@ -0,0 +1,58 @@
[《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,77 @@
# Redis持久化
> 参考文献
> * [https://www.cnblogs.com/shizhengwen/p/9283824.html](https://www.cnblogs.com/shizhengwen/p/9283824.html)
## 概述
* Redis是一种高级key-value数据库。它跟memcached类似不过数据可以持久化而且支持的数据类型很丰富。有字符串链表集 合和有序集合。支持在服务器端计算集合的并,交和补集(difference)等还支持多种排序功能。所以Redis也可以被看成是一个数据结构服务 器。
* Redis的所有数据都是保存在内存中然后不定期的通过异步方式保存到磁盘上(这称为“半持久化模式”)也可以把每一次数据变化都写入到一个append only file(aof)里面(这称为“全持久化模式”)。
* 由于Redis的数据都存放在内存中如果没有配置持久化redis重启后数据就全丢失了于是需要开启redis的持久化功能将数据保存到磁盘上当redis重启后可以从磁盘中恢复数据。redis提供两种方式进行持久化
* 一种是RDB持久化原理是将Reids在内存中的数据库记录定时dump到磁盘上的RDB持久化
* 另外一种是AOFappend only file持久化原理是将Reids的操作日志以追加的方式写入文件
* 那么这两种持久化方式有什么区别呢,改如何选择呢?网上看了大多数都是介绍这两种方式怎么配置,怎么使用,就是没有介绍二者的区别,在什么应用场景下使用。
## 2 二者的区别
* RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘实际操作过程是fork一个子进程先将数据集写入临时文件写入成功后再替换之前的文件用二进制压缩存储。
![](image/2021-04-07-23-24-02.png)
* AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作查询操作不会记录以文本的方式记录可以打开文件看到详细的操作记录。
![](image/2021-04-07-23-24-24.png)
## 3 二者优缺点
### RDB存在哪些优势呢
1. 一旦采用该方式那么你的整个Redis数据库将只包含一个文件这对于文件备份而言是非常完美的。比如你可能打算每个小时归档一次最近24小时的数据同时还要每天归档一次最近30天的数据。通过这样的备份策略一旦系统出现灾难性故障我们可以非常容易的进行恢复。
2. 对于灾难恢复而言RDB是非常不错的选择。因为我们可以非常轻松的将一个单独的文件压缩后再转移到其它存储介质上。
3. 性能最大化。对于Redis的服务进程而言在开始持久化时它唯一需要做的只是fork出子进程之后再由子进程完成这些持久化的工作这样就可以极大的避免服务进程执行IO操作了。
4. 相比于AOF机制如果数据集很大RDB的启动效率会更高。
### RDB又存在哪些劣势呢
1. 如果你想保证数据的高可用性即最大限度的避免数据丢失那么RDB将不是一个很好的选择。因为系统一旦在定时持久化之前出现宕机现象此前没有来得及写入磁盘的数据都将丢失。
2. 由于RDB是通过fork子进程来协助完成数据持久化工作的因此如果当数据集较大时可能会导致整个服务器停止服务几百毫秒甚至是1秒钟。
### AOF的优势有哪些呢
1. 该机制可以带来更高的数据安全性即数据持久性。Redis中提供了3中同步策略即每秒同步、每修改同步和不同步。事实上每秒同步也是异步完成的其效率也是非常高的所差的是一旦系统出现宕机现象那么这一秒钟之内修改的数据将会丢失。而每修改同步我们可以将其视为同步持久化即每次发生的数据变化都会被立即记录到磁盘中。可以预见这种方式在效率上是最低的。至于无同步无需多言我想大家都能正确的理解它。
2. 由于该机制对日志文件的写入操作采用的是append模式因此在写入过程中即使出现宕机现象也不会破坏日志文件中已经存在的内容。然而如果我们本次操作只是写入了一半数据就出现了系统崩溃问题不用担心在Redis下一次启动之前我们可以通过redis-check-aof工具来帮助我们解决数据一致性的问题。
3. 如果日志过大Redis可以自动启用rewrite机制。即Redis以append模式不断的将修改数据写入到老的磁盘文件中同时Redis还会创建一个新的文件用于记录此期间有哪些修改命令被执行。因此在进行rewrite切换时可以更好的保证数据安全性。
4. AOF包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作。事实上我们也可以通过该文件完成数据的重建。
### AOF的劣势有哪些呢
1. 对于相同数量的数据集而言AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
2. 根据同步策略的不同AOF在运行效率上往往会慢于RDB。总之每秒同步策略的效率是比较高的同步禁用策略的效率和RDB一样高效。
### 二者选择的标准
就是看系统是愿意牺牲一些性能换取更高的缓存一致性aof还是愿意写操作频繁的时候不启用备份来换取更高的性能待手动运行save的时候再做备份rdb。rdb这个就更有些 eventually consistent的意思了。
## 4 常用配置
### RDB持久化配置
* Redis会将数据集的快照dump到dump.rdb文件中。此外我们也可以通过配置文件来修改Redis服务器dump快照的频率在打开6379.conf文件之后我们搜索save可以看到下面的配置信息
```
save 900 1 #在900秒(15分钟)之后如果至少有1个key发生变化则dump内存快照。
save 300 10 #在300秒(5分钟)之后如果至少有10个key发生变化则dump内存快照。
save 60 10000 #在60秒(1分钟)之后如果至少有10000个key发生变化则dump内存快照
```
### AOF持久化配置
* 在Redis的配置文件中存在三种同步方式它们分别是
```
appendfsync always #每次有数据修改发生时都会写入AOF文件。
appendfsync everysec #每秒钟同步一次该策略为AOF的缺省策略。
appendfsync no #从不同步。高效但是数据不会被持久化。
```

118
数据库/Redis/ch10.md Normal file
View File

@@ -0,0 +1,118 @@
RDB持久化可将内存中的数据库状态保存到磁盘上避免数据丢失。持久化可以手动也可以根据服务器配置选项定期执行。
RDB持久化生成的RDB文件是一个压缩过的二进制文件通过该文件可以还原生成RDB文件时的数据库状态。
# 10.1 RDB文件的创建于载入
有两个命令可以生成RDB文件
1. SAVE。该命令会阻塞Redis服务器进程直到RDB文件创建完毕期间拒绝任何命令请求。
2. BGSAVE。派生出一个子进程来创建RDB文件服务器进程父进程继续处理命令请求。
> 在BGSAVE命令执行期间服务器处理SAVE、GBSAVE、BGREWRITEAOF命令会被拒绝执行。
创建RDB文件的操作由`rdb.c/rdbSave`函数完成。
RDB文件的载入工作在服务器启动时自动执行。
另外AOF文件的更新频率比RDB文件要高所以
- 如果服务器开启了AOF那么优先用AOF来还原数据库。
- 只有在AOF关闭时服务器才会用RDB来还原数据库。
载入RDB文件的工作由`rdb.c/rdbLoad`函数完成。载入RDB文件期间服务器一直处于阻塞状态。
# 10.2 自动间隔性保存
Redis允许用户通过设置服务器配置的save选项每隔一段时间执行一次BGSAVE命令。配置如下
> save 900 1
>
> save 300 10
>
> save 60 10000
那么上述三个条件只要满足任意一个BGSAVE命令就会被执行
1. 服务器在900秒内对服务器进行了至少1次修改。
2. 服务器在300秒内对服务器进行了至少10次修改。
3. 服务器在60秒内对服务器进行了至少10000次修改。
当Redis服务器启动时用户可以指定配置文件或者传入启动参数的方式设置save选项。如果没有主动设置服务器默认使用上述三个条件。接着服务器会根据save的条件设置`redisServer`结构的`saveParams`属性。
```objective-c
struct redisServer {
// ...
struct saveparam *saveparams; // 保存条件的数组
long long dirty;
time_t lastsave;
//...
}
struct saveparam {
time_t seconds; // 秒数
int changes; // 修改数
}
```
除此之外服务器还维持着一个dirty计数器以及一个lastsave属性。
- dirty记录上一次成功`SAVE`或`BGSAVE`之后,服务器对数据库状态进行了多少次修改。
- lastsave是一个UNIX时间戳记录了服务器上一次成功`SAVE`或`BGSAVE`的时间。
## 检查保存条件是否满足
服务器的周期性操作函数`serverCron`默认每个100毫秒就会执行一次其中一项工作是检查save选项所设置的保存条件是否满足。
# 10.3 RDB文件结构
RDB文件的各个部分包括
> REDIS | db_version | databases | EOF | check_sum
## REDIS
开头是REDIS部分长度为5。保存了五个字符以便载入时确认是否为RDB文件。
## db_version
db\_version长4字节是一个字符串表示的整数记录了RDB文件的版本号。
## databases
databases部分包含了0个或多个数据库以及各个数据库中的键值对数据。一个保存了0号和3号数据库的RDB文件如下
> REDIS | db_version | database 0 | databse 3 | EOF | check_sum
每个非空数据库在RDB文件中都可保存为以下三部分
> SELECTDB | db_number | key_value_pairs
- SELECTEDB。1字节。但程序遇到这个值的时候它就知道接下来要读入的将是一个数据库号码。
- db\_number。读取号码之后服务器会调用`SELECT`命令切换数据库。
- key_value_pairs。不带过期时间的键值对在RDB文件中包括TYPE、key、value。TYPE的值决定了如何读入和解释value的数据。带过期时间的键值对增加了EXPIRETIME_MS和ms。前者告知程序接下来要读入一个UNIX时间戳。
## EOF
长度为1字节标识RDB文件结束。
## check_sum
8字节的无符号整数保存着一个前面四个部分的校验和。
# 10.4 分析RDB文件
od命令分析RDB文件。-c参数可以以ASCII编码打印文件。比如一个数据库状态为空的RDB文件
![](img/chap10/img0.png)
Redis自带的文件检查工具是redis-check-dump。
# 导航
[目录](README.md)
上一章:[9. 数据库](ch9.md)
下一章:[11. AOF持久化](ch11.md)

55
数据库/Redis/ch11.md Normal file
View File

@@ -0,0 +1,55 @@
AOFAppend Only File持久化与RDB持久化通过保存数据库中的键值对来记录数据库状态不同AOF保存Redis所执行的写命令来记录数据库状态。被写入AOF文件的命令都是以Redis的命令请求协议格式保存的纯文本格式打开即可查看。
# 11.1 AOF持久化的实现
AOF持久化功能的实现可分为命令追加append、文件写入、文件同步sync三个步骤。
## 命令追加
如果打开AOF功能服务器在执行完一个写命令后会以协议格式将被执行的命令追加到服务器状态的`aof_buf`缓冲区的末尾。
```c
struct redisServer {
// ...
sds aof_buf;
// ...
};
```
## AOF文件的写入与同步
Redis的服务器进程就是一个事件循环loop这个循环中的文件事件负责接受客户端的请求并向客户端发送回复而时间事件则负责执行像`serverCron`函数这样的定时任务。
服务器在处理文件任务时可能会执行写命令,追加内容到`aof_buf`缓冲区,所以服务器在每次结束一个事件循环前,都会调用`flushAppendOnlyFile`考虑是否将缓冲区的内容写入到AOF文件中。
> flushAppendOnlyFile函数的行为由服务器配置的`appendfsync`选项的值来决定always、everysec默认、no。
# 11.2 AOF文件的载入与数据还原
服务器只要读入并重新执行一遍AOF文件中的写命令就可以还原服务器关闭之前的数据库状态
1. 创建一个不带连接的**伪客户端**。
2. 从AOF文件中分析并读取一条写命令。
3. 使用伪客户端执行被读出的命令
4. 一直执行步骤2和3知道AOF文件中的所有命令都被处理完位置。
# 11.3 AOF重写
为了解决AOF文件体积膨胀的问题Redis提供了AOF重写功能。通过该功能Redis可以创建一个新的AOF文件来替代现有的AOF文件新文件不会包含荣誉命令体积也会小很多。
## 实现
AOF文件重写不需要对现有AOF文件做任何读取、分析或写入操作而是通过读取服务器当前的数据库状态实现的。首先从数据库中读取现在的键然后用一条命令去记录键值对代替之前记录这个键值对的多条命令。这就是AOF重写的实现原理。
Redis服务器采用单个线程来处理命令请求所以将AOF重写程序放到子进程中这样父进程可以继续处理请求。父子进程会出现数据不一致的问题Redis服务器设置了一个AOF重写缓冲区这个缓冲区在创建子进程之后开始使用但Redis服务器执行完一个写命令后会通知将写命令发送给AOF缓冲区和AOF重写缓冲区。子进程完成AOF重写操作后向父进程发送一个信号父进程将执行以下操作
1. 将AOF重写缓冲区的内容写入新AOF文件。
2. 对新的AOF文件改名覆盖现有的AOF文件。
# 导航
[目录](README.md)
上一章:[10. RDB持久化](ch10.md)
下一章:[12. 事件](ch12.md)

152
数据库/Redis/ch12.md Normal file
View File

@@ -0,0 +1,152 @@
Redis服务器是一个事件驱动程序需要处理以下两类事件
- 文件事件file eventRedis服务器通过socket与客户端连接文件事件就是对套接字操作的对象。服务器与客户端的通信会产生相应的文件事件服务器监听并处理这些事件来完成一系列的网络通信操作。
- 时间事件time eventRedis服务器的一些操作`serverCron`函数)需要在特定时间点执行,时间事件就是对这类定时任务的抽象。
# 12. 1 文件事件
Redis基于Reactor模式开发了自己的网络事件处理器称为『文件事件处理器』文件事件处理器以单线程方式运行。
![](img/chap12/img0.png)
文件事件处理器的四个组成部分:
- 套接字。
当被监听的套接字准备好执行accept、read、write、close等操作时与操作相对应的文件事件就会产生。
- I/O多路复用程序。
使用I/O多路复用程序同时监听多个套接字并向文件分派器传送那些产生了事件的套接字使用队列
- 文件事件分派器
根据套接字的事件类型,调用相应的事件处理器。
- 事件处理器
## I/O多路复用程序的实现
Redis的I/O多路复用包装了常见的select、poll、evport和kqueue等函数库来实现的每个函数库的在Redis源码中都有一个独立的文件。
## 事件的类型
I/O多路复用程序可以监听多个套接字的ae.h/AE_READABLE和ae.h/AE_WRITABLE事件。两种事件可以同时监听但会优先处理AE_READABLE事件。
## API
| 函数 | 参数 | 作用 |
| ---------------------- | ----------------- | ---------------------------------------- |
| ae.c/aeCreateFileEvent | 套接字描述符、事件类型、事件处理器 | 将给定套接字的给定事件加入到I/O多路复用程序的监听范围之内并对事件和事件处理器进行关联 |
| ae.c/aeDeleteFileEvent | 套接字描述符、监听事件类型 | 让I/O多路复用程序取消套接字的事件监听并取消事件与事件处理器的关联 |
| ae.c/aeGetFileEvent | 套接字描述符 | 返回套接字正在被监听的事件类型none、readable、writable |
| ae.c/aeWait | 套接字描述符、事件类型、毫秒数 | 在给定时间内阻塞并等待套接字的给定类型的事件发生 |
| ae.c/aeApiPoll | timeval结构 | 在给定事件内阻塞并等待所有被acCreateFileEvent函数设置为监听状态的套接字产生文件事件 |
| ae.c/aeProcessEvents | | 先调用aeApiPoll来等待事件然后遍历所有事件调用相应的事件处理器 |
| ae.c/aeGetApiName | | 返回I/O多路复用程序底层使用的函数库名称epoll、select等 |
## 文件事件的处理器
Redis为文件事件编写了多个处理器分别用于实现不同的网络通信需求
- 连接应答处理器:监听客户端的套接字,并应答。
networking.c/acceptTcpHandler函数具体实现为sys/socket.h/accept函数的包装。服务器初始化时会将这个处理器与套接字的AE\_READABLE事件关联起来。
- 命令请求处理器:接受来自客户端的命令请求。
networking.c/readQueryFromClient函数具体实现为unistd.h/read函数的包装。
- 命令回复处理器:向客户端返回命令的执行结果。
networking.c/sendReplyToClient函数具体实现为unistd.h/write函数的包装。
- 复制处理器:主从服务器的复制操作。
# 12.2 时间事件
Redis的时间事件分为两类
- 定时事件:在指定一段时间后执行一次。
- 周期性事件:每隔一段时间就执行一次。
时间事件主要有三个属性:
- id
- when毫秒进度的UNIX时间戳事件的到达时间。
- timeProc时间事件处理器事件到达时负责处理事件。
一个事件是定时事件还是周期性事件,取决于时间事件处理器的返回值:
- 返回ae.h/AE\_NOMORE就是定时事件到达一次后就删除
- 返回非AE\_NOMORE的整数值就是周期性事件事件到达后根据返回值对when属性进行更新。
## 实现
服务器的所有时间事件存放在一个无序链表(*不按when属性排序*)中,每当时间事件处理器运行时,遍历整个链表,找到已到达的事件,调用相应的事件处理器。
## API
| 函数 | 参数 | 作用 |
| ------------------------- | ----------- | ---------------- |
| ae.c/aeCreateTimeEvent | 毫秒数,时间事件处理器 | 将新的时间事件添加到服务器 |
| ae.c/aeDeleteFileEvent | 事件ID | 删除时间事件 |
| ae.c/aeSearchNearestTimer | | 返回到达时间最近的事件 |
| ae.c/processTimeEvents | | 时间事件的执行器,遍历并调用事件 |
## serverCron函数
serverCron函数的工作包括
- 更新服务器的统计信息,如时间、内存占用、数据库占用
- 清理过期的键值对
- 关闭和清理失效的连接
- 尝试AOF或RDB持久化
- 如果是主服务器,对从服务器定期同步
- 如果是集群模式,对集群进行同步和测试连接
# 12.3 事件的调度与执行
调度和执行由ae.c/aeProcessEvents函数负责。
```python
def aeProcessEvents():
# 获取最近的事件
time_event = aeSearchNearestTimer()
# 计算最近的事件还有多少毫秒
remaind_ms = time_event.when - unix_ts_now()
# 如果事件已到达
if remaind_ms < 0:
remaind_ms = 0
# 根据remaind_ms的值创建timeval结构
timeval = create_timeval_with_ms(remaind_ms)
# 阻塞并等待文件事件
# 如果remaind_ms为0那么aeApiPoll调用之后马上返回不阻塞
aeApiPoll(timeval)
# 处理所有已产生的文件事件
processFileEvents()
# 处理所有已到达的时间事件
processTimeEvents()
```
调度和执行的规则如下:
- aeApiPoll函数的最大阻塞时间由到达时间最接近当前时间的事件决定避免服务器的频繁轮询。
- 如果处理完一次文件事件后,未有时间事件到达,则再次处理文件事件。
- 对事件的处理都是同步、有序、原子地执行。不会中断、抢占事件处理。
- 时间事件的处理时间,通常比其设定的到达时间晚一些。
# 导航
[目录](README.md)
上一章:[11. AOF持久化](ch11.md)
下一章:[13. 客户端](ch13.md)

103
数据库/Redis/ch13.md Normal file
View File

@@ -0,0 +1,103 @@
Redis服务器为客户端建立了相应的redis.h/redisClient结构保存了客户端的当前信息以及执行相关功能需要的数据结构
- 客户端的套接字描述符
- 客户端的名字
- 客户端的标志值flag
- 客户端正在使用的数据库的指针及号码
- 客户端当前要执行的命令、参数
- 客户端的输入输出缓冲区
- 客户端的复制状态信息
- 客户端的事务状态
- 客户端执行发布与订阅功能用到的数据结构
- 客户端的身份验证标识
- 客户端的统计信息,如创建时间、最后一次通行时间、缓冲区大小超出限制的时间
`redisServer`结构保存了一个`clients`链表,保存了所有连接的客户端的状态信息。
```c
struct redisServer {
// ...
list *clients;
redisClient *lua_client; // Lua伪客户端服务器运行时一直存在
// ...
}
```
# 13.1 客户端属性
```c
typedef struct redisClient {
/*
fd记录客户端正在使用的套接字描述符
伪客户端的fd为-1不需要套接字连接目前用于 1. AOF还原 2. 执行Lua脚本的Redis命令
普通客户端为大于-1的整数。CLIENT list命令可以查看当前正在使用的套接字描述符
*/
int fd;
// 连接到服务器的客户端默认没有名字CLIENT setname可以设置一个名字。
robj *name;
/*
flags记录了客户端的role以及目前所处的状态
所以flags可以是多个二进制或所有标志在redis.h中定义
*/
int flags;
// 输入缓冲区用于保存客户端发送的命令请求
sds querybuf;
// 解析querybuf的请求得出命令参数及命令个数
// argv是个数组每个元素都是一个字符串对象其中argv[0]是要执行的命令
robj **argv;
int argc;
// redisCommand保存了命令的实现函数标识、参数个数、总执行次数等统计信息
struct redisCommand *cmd;
// 输出缓冲区保存命令的回复,其中
// 1. buf是固定缓冲区用于保存长度较小的回复
// 2. reply可变缓冲区保存长度较大的回复
char bug[REDIS_REPLY_CHUNK_BYTES];
int bufpos;
list *reply;
// 记录客户端是否通过了验证
int authenticated;
time_t ctime;
time lastinteraction;
time_t obuf_soft_limit_reached_time;
// ...
} redisClient;
```
# 13.2 客户端的创建于关闭
## 创建客户端
客户端使用connect函数连接到服务器服务器就会调用连接事件处理器为客户端创建相应的客户端状态并添加到链表的末尾。
## 关闭客户端
一个普通客户端可因为多种原因关闭:
- 客户端进程被杀死
- 发送的协议不符合格式
- 客户端成了`CLIENT KILL`命令的目标
- 服务器配置了timeout选项客户端空转被断开
- 超出输入/输出缓冲区限制
> 输出缓冲区的限制包括:硬性限制、弱性限制。超过软性限制一段时间,客户端也会被关闭。
# 导航
[目录](README.md)
上一章:[12. 事件](ch12.md)
下一章:[14. 服务器](ch14.md)

259
数据库/Redis/ch14.md Normal file
View File

@@ -0,0 +1,259 @@
Redis服务器负责与多个客户端建立连接处理客户端的命令请求在数据库中保存命令产生的数据并通过资源管理来维持服务器自身的运转。
# 14.1 命令请求的执行过程
`SET KEY VALUE`命令的执行过程:
1. 客户端向服务器发送命令请求`SET KEY VALUE`
2. 服务器接收并处理命令请求,在数据库中设置操作,并产生命令回复`OK`
3. 服务器将`OK`发送给客户端。
4. 客户端接收服务器返回的命令`OK`,并打印给用户。
## 发送命令请求
用户:键入命令请求
客户端:将命令请求转换为协议格式然后发送给服务器
## 读取命令请求
当连接套接字因为客户端的写入而变得可读时,服务器将调用命令请求处理器执行以下操作:
1. 读取套接字协议格式中的命令请求,并将其保存在客户端状态的输入缓冲区里。
2. 对输入缓冲区的命令请求进行分析提取命令参数及其个数保存到客户端状态的argv和argc属性。
3. 调用命令执行器,执行指定的命令。
## 命令执行器1查找命令实现
命令执行器要做的第一件事是根据客户端状态的`argv[0]`参数在命令表command table中查找参数指定的命令并将其保存到客户端状态的`cmd`属性里。
命令表是一个字典,键是命令名字,值是一个`redisCommand`结构。命令表使用的是**大小写无关**的查找算法。
## 命令执行器2执行预备操作
有了执行命令所需的命令实现函数、参数、参数个数,但程序还需要一些预备操作:
- 检查客户端状态的`cmd`指针是否为`NULL`
- 根据`cmd`属性指向`redisCommand`结构的`arity`属性,检查命令请求的参数个数是否正确。
- 检查客户端是否通过了身份验证,未通过必须使用`AUTH`命令。
- 如果服务器打开了`maxmemory`功能,检查内存占用情况,有需要时进行内存回收。
- 如果上一次`BGSAVE`出错,且服务器打开了`stop-writes-on-bgsave-error`功能,且服务器要执行一个写命令,拒绝执行。
- 如果客户端正在用`SUBSCRIBE`订阅频道,服务器只会执行订阅相关的命令。
- 如果服务器正在进行输入载入那么客户端发送的命令必须带有1标识才能被执行。
- 如果服务器因为Lua脚本而超时阻塞那么服务器只会执行客户端发来的`SHUTDOWN nosave``SCRIPT KILL`命令。
- 如果客户端正在执行事务,那么服务器只会执行客户端发来的`EXEC``DISCARD``MULTI``WATCH`命令,其余命令进入事务队列。
- 如果服务器打开监视器功能,要将执行的命令和参数等信息发给监视器,其后才真正执行命令。
## 命令执行器3调用命令的实现函数
> client->cmd->proc(client);
相当于执行语句:
> sendCommand(client);
命令回复会保存在输出缓冲区,之后实现函数还会为套接字关联命令回复处理器,将回复返回给客户端。
## 命令执行器5执行后续工作
- 如果开启了慢查询,添加新的日志。
- `redisCommand`结构的`calls`计数器+1。
- 写入AOF缓冲区。
- 同步从服务器。
## 将命令回复发送给客户端
当客户端套接字变为可写时,服务器将输出缓冲区的命令发送给客户端。发送完毕后,清空输出缓冲区。
## 客户端接收并打印命令回复
服务器:回复处理器将协议格式的命令返回给客户端。
客户端:将回复格式化成人类可读的格式,打印。
# 14.2 serverCron函数
## 更新服务器时间缓存
每次获取系统的当前时间都要执行一次系统调用,为了减少系统调用,服务器状态中保存了当前时间的缓存:
```c
struct redisServer {
// 秒级的系统当前UNIX时间戳
time_t unixtime;
// 毫秒级的系统当前UNIX时间戳
long long mstime;
};
```
`serverCron`默认会100毫秒更新一次这两个属性所以它们的精确度并不高。对于一些高精度要求的操作还是会再次执行系统调用。
## 更新LRU时钟
```objective-c
struct redisServer {
// 默认10秒更新一次的时钟缓存用于计算键的空转时长
// INFO server可查看
unsigned lruclock:22;
};
// 每个Redis对象都有一个lru属性计算键的空转时长就是用服务器的lruclock减去对象的lru时间
typedef struct redisObject {
unsigned lru:22;
} robj;
```
## 更新服务器每秒执行命令次数
`serverCron`函数中的`trackOperationPerSecond`函数以每100毫秒一次的频率执行该函数以抽样计算的方式估算并记录服务器在最近一秒内处理的命令请求数量这个值可以用过`INFO status`命令查看。
```c
struct redisServer {
// 上一次抽样的时间
long long ops_sec_last_sample_time;
// 上一次抽样时,服务器已执行命令的数量
long long ops_sec_last_sample_ops;
// REDIS_OPS_SEC_SAMPLES 大小默认16
// 环形数组中的每个项记录了一次抽样结果
long long ops_sec_samples[REDIS_OPS_SEC_SAMPLES];
// ops_sec_samples 数组的索引值,每次抽样后自动+1
// 让 ops_sec_samples 数组构成一个环形数组
int ops_sec_idx;
};
```
客户端执行`INFO`命令,服务器会调用`getOperationsPerSecond`函数,根据`ops_sec_samples`中的抽样结果,计算出`instantaneous_ops_per_sec`属性的值。
## 更新服务器内存峰值记录
```c
struct redisServer {
// 已使用内存峰值
size_t stat_peak_memory;
};
```
每次`serverCron`执行,程序都会查看当前的内存数量,更新`stat_peak_memory`。`INFO memory`可查看。
## 处理SIGTERM信号
启动时Redis会为服务器进程的`SIGTERM`信号关联处理器`sigtermHandler`函数。它在接到该信号后,打开服务器状态的`shutdown_asap`标识。每次`serverCron`执行,程序都会检查该标识,并决定是否关闭服务器。
```objective-c
struct redisServer {
// 关闭服务器的标识1关闭2不做操作。
int shutdown_asap;
};
```
## 管理客户端资源
`serverCron`每次都会调用`clientsCron`函数,后者会对一定数量的客户端作如下检查:
- 连接是否超时
- 输入缓冲区是否超过长度,如果是,新建缓冲区
## 管理数据库资源
`serverCron`每次都会调用`databasesCron`函数,检查一部分的数据库,删除过期键,对字典进行收缩等。
## 执行被延迟的BGREWRITEAOF
服务器执行`BGSAVE`期间,会阻塞`BGREWRITEAOF`命令。
```c
struct redisServer {
// 记录是否有BGREWRITEAOF被延迟
int aof_rewrite_scheduled;
};
```
## 检查持久化操作的运行状态
```c
struct redisServer {
// 记录执行BGSAVE命令的子进程ID
// 如果服务器没有执行BGSAVE值为-1
pid_t rdb_child_pid;
// 记录执行BGREWRITEAOF命令的子进程ID
pid_t aof_child_pid;
};
```
`serverCron`执行时,只要两个属性有一个为-1则执行wait3函数检查是否有信号发来服务器进程
- 如果有信号达到表明新的RDB文件生成完毕或AOF文件重写完毕服务器需要执行相应命令的后续操作
- 没有信号就不做操作
如果两个属性都不为-1表明服务器没有再做持久化操作
![](img/chap14/img0.png)
## serverCron的其他操作
- 将AOF缓冲区的内容写入AOF文件
- 关闭异步客户端(超出输入缓冲区限制)
- 增加cronloops计数器它的唯一作用就是复制模块中实现『每执行`serverCron`函数N次就执行一次指定代码』的功能”
# 14.3 初始化服务器
## 初始化服务器状态结构
初始化服务器的第一步就是创建一个`redisServer`类型的实例变量`server`,并为结构中的各个属性设置默认值。这个工作由`redis.c/initServerConfig`函数完成:
- 设置服务器运行id
- 为id加上结尾字符
- 设置默认的配置文件路径
- 设置默认服务器频率
- 设置服务器的运行架构64位 or 32位
- 设置服务器的默认端口
- 设置服务器的默认RDB和AOF条件
- 初始化服务器的LRU时钟
- 创建命令表
## 载入配置选项
启动服务器时,用户可以通过配置参数或者配置文件来修改服务器的默认配置。
`redis.c/initServerConfig`函数初始化完`server`变量后,开始载入用户给定的配置。
## 初始化服务器数据结构
载入用户的配置选项之后,才能正确地初始化数据结构,由`initServer`函数负责:
- `server.clients`链表
- `server.db`数组
- `server.pubsub_channels`字典
- `server.lua`Lua环境
- `server.slowlog`
除此之外,`initServer`还:
- 为服务器设置进程信号处理器
- 创建共享对象
- 打开服务器的监听端口,并为套接字关联应答事件处理器
- 为`serverCron`函数创建时间事件
- 打开或创建的AOF文件
- 初始化后台I/O模块
## 还原数据库状态
初始化完`server`后服务器要载入RDB或AOF文件还原数据库状态
## 执行事件循环
开始执行服务器的loop。
# 导航
[目录](README.md)
上一章:[13. 客户端](ch13.md)
下一章:[15. 复制](ch15.md)

155
数据库/Redis/ch15.md Normal file
View File

@@ -0,0 +1,155 @@
Redis中用户可以执行`SAVEOF`命令或设置`saveof`选项让一个服务器去复制replicate另一个服务器。被复制的服务器叫做master对master进行复制的服务器叫做slave。
进行复制中的master和slave应该保存相同的数据这称作“数据库状态一致”。
## 15.1 旧版复制功能的实现
Redis的复制功能分为同步sync和命令传播command propagate两个操作
- 同步用于将slave的数据库状态更新至master当前所处的数据库状态。
- 命令传播用于master的数据块状态被修改导致和lsave的数据库状态不一致时让两者的数据库重回一致状态。
## 同步
复制开始时slave会先执行同步操作步骤如下
- slave对master发送`SYNC`命令
- master收到`SYNC`执行`BGSAVE`在后台生成一个RDB文件并使用一个缓冲区记录从现在开始执行的所有写命令。
- master的`BGSAVE`执行完毕后将生成的RDB文件发送给slaveslave接收并载入这个RDB更新自己的数据库状态
- master将记录在缓冲区中的所有写命令发送给slave后者执行这些操作再次更新自己的数据库状态
## 命令传播
同步完成后主从服务器的一致状态仍有可能改变每当master执行写命令时主从服务器的状态就会不一致。为此master执行写命令并将其发送给slave一并执行。
# 15.2 旧版复制功能的缺陷
Redis的复制可以分为两种情况
- 初次复制slave没有复制过或者slave要复制的master和上一次复制的master不同。
- 断线后重复制处于命令传播阶段的master和slave中断了复制但重连后slave继续复制master。
对于初次复制,旧版复制功能可以很好完成。但是断线后复制,效率却很低,因为重连后会浪费一次`SYNC`操作。
# 15.3 新版复制功能的实现
为了解决旧版复制功能在断线后的低效问题Redis从2.8之后,使用`PSYNC`代替`SYNC`执行复制时的同步操作。`PSYNC`具有完整重同步full resynchronization)和部分重同步partial resynchronization两种模式
- 完整重同步用于处理初次复制,执行步骤和`SYNC`命令基本一样。
- 部分重同步用于处理断线后重复制重连后如果条件允许master可以将断开期间的谢明令发送给slave执行。
# 15.4 部分重同步的实现
部分重同步功能有三个部分组成:
- master和slave的复制偏移量replication offset
- master的复制积压缓冲区replication backlog
- 服务器的运行IDrun ID
## 复制偏移量
master和slave分别维护一个复制偏移量
- master每次向slave传播N个字节的数据时就将自己的复制偏移量+N。
- slave每次收到master的N个字节数据时就将自己的复制偏移量+N。
对比两者的复制偏移量,就知道它们是否处于一致状态。
## 复制积压缓冲区
复制积压缓冲区是master维护的一个固定长度的FIFO队列默认大小为1MB。当服务器进行命令传播时不仅会将命令发送给所有slave还会入队到积压缓冲区。因此积压缓冲区保存了最近被传播的写命令且为队列中的每个字节记录相应的复制偏移量。
slave重连上master时slave通过`PSYNC`将自己的复制偏移量offset发送给mastermaster会根据这个offset决定slave执行何种同步操作
- 如果offset之后的数据仍在复制积压缓冲区中执行部分重同步操作。
- 否则,执行完整重同步操作。
## 服务器运行ID
部分重同步还要用到服务器运行ID主从服务器都有自己的ID。初次复制时master将自己的ID传给slave后者将其保存。
断线重连后slave向当前连接的master发送之前保存的ID
- master发现接收的ID和自己的相同那么说明断线之前复制的就是自己继续执行部分重同步。
- 如果不同,完整重同步啦!
# 15.5 PSYNC命令的实现
`PSYNC`的调用方式有两种:
- slave没有复制过任何master则在开始一个新的复制时向master发送`PSYNC ? -1`命令,请求完整重同步。
- slave复制过某个master则发送`PSYNC <runid> <offset>`命令接收到这个命令的master会根据`runid``offset`来判断执行哪种同步。
![](img/chap15/img0.png)
# 15.6 复制的实现
通过向slave发送`SLAVEOF`命令可以让slave复制master
## 步骤1设置master的地址和端口
命令`slave 127.0.0.1 6379`会设置服务器状态的以下两个属性:
```c
struct redisServer {
char *masterhost;
int masterport;
};
```
## 步骤2建立套接字连接
如果slave的套接字能成功连接到master那么slave会为这个套接字关联一个专门用于处理复制工作的文件事件处理器它将负责处理后续的复制工作。
master接收到客户端的套接字连接之后为其创建相应的客户端状态这时slave同时有server和client两个身份。
## 步骤3发送PING命令
slave成为master的客户端之后紧接着就向其发送`PING`命令,那么:
![](img/chap15/img1.png)
## 步骤4身份验证
收到master的“PONG”回复后slave要检查自己的`masterauth`选项决定是否进行身份验证。如果需要验证slave会向master发送一条`AUTH`命令,参数为`masterauth`选项的值,接下来:
![](img/chap15/img2.png)
## 步骤5发送端口信息
身份验证之后slave将执行`REPLCONF listening-port <port-number>`向master发送slave的监听端口号。master收到后会将端口号放到客户端状态的`slave_listening_por`t属性中该属性的唯一作用就是master执行`INFO replication`命令时打印slave的端口号。
```c
typdef struct redisClient {
int slave_listening_port;
} redisClient;
```
## 步骤6同步
这一步slave发送`PSYNC`执行同步操作。执行同步之后master也成了slave的客户端master发送写命令来改变slave的数据库状态。
## 步骤7命令传播
完成同步之后主从服务器就进入命令传播阶段master将自己执行写命令发送给slaveslave接到后就执行这样两者的状态就一直保持一致了。
# 15.7 心跳检测
命令传播阶段slave默认每秒给master发送一次命令`REPLCONF ACK <replication_offset>`其中replication_offset对应当前slave的复制偏移量。该命令有三个作用
- 检测网络连接状态
- 辅助实现min-slaves选项
该选项防止master在不安全的情况下执行写命令比如slave数量小于3的时候。
- 检测命令丢失
这个根据复制偏移量来判断如果两者不一致master就会把复制积压缓冲区的命令重新发送。
# 导航
[目录](README.md)
上一章:[14. 服务器](ch14.md)
下一章:[16. Sentinel](ch16.md)

244
数据库/Redis/ch16.md Normal file
View File

@@ -0,0 +1,244 @@
Sentinel哨兵是Redis的高可用性解决方案由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个master以及属下的所有slave。Sentinel在被监视的master下线后自动将其属下的某个slave升级为新的master然后由新的master继续处理命令请求。
# 16.1 启动并初始化Sentinel
启动一个Sentinel可以使用命令
> redis-sentinel sentinel.conf
或者
> redis-server sentnel.conf —sentinel
当一个Sentinel启动时会执行以下几步
1. 初始化服务器
2. 将普通Redis服务器使用的代码替换成Sentinel专用代码
3. 初始化Sentinel状态
4. 根据配置文件初始化监视的master列表
5. 创建与master的网络连接
## 初始化服务器
Sentinel本质上是一个运行在特殊模式下的Redis服务器它的初始化过程与普通Redis服务器并不相同
| 功能 | Sentinel使用情况 |
| ------------------------------------ | ---------------------------------------- |
| 数据库和键值对方面的命令:`SET`, `DEL`, `FLUSHDB` | 不使用 |
| 事务命令 | 不使用 |
| 脚本命令 | 不使用 |
| RDB和AOF持久化 | 不使用 |
| 复制命令 | Sentinel内部使用客户端不可用 |
| 发布、订阅命令 | 订阅命令可在Sentinel内部和客户端使用发布命令只能在Sentinel内部使用 |
| 文件事件处理器(发送命令请求,处理命令回复) | Sentinel内部使用 |
| 时间事件处理器 | Sentinel内部使用`serverCron`会用`sentinel.c/sentinelTimer`函数 |
## 使用Sentinel专用代码
将一部分普通Redis服务器的代码替换为Sentinel专用代码比如端口号命令表。
## 初始化Sentinel状态
接下来,服务器会初始化一个`sentinel.c/sentinelState`结构它保存了服务器有关Sentinel的状态
```c
struct sentinelState {
// 当前纪元,用于实现故障转移
uint64_t current_epoch;
// 保存了所有被监视的master键是master名字值是指向 sentinelRedisInstance 结构的指针
dict *masters;
// 是否进入TILT模式
int tilt;
// 目前正在执行的脚本数量
int runing_scripts;
// 进入TILT模式的时间
mstime_t tilt_start_time;
// 最后一次执行时间处理器的时间
mstime_t previous_time;
// FIFO队列包含所有需要执行的用户脚本
list *scripts_queue;
} sentinel;
```
## 初始化Sentinel状态的masters属性
sentinelRedisInstance结构代表一个被监视的Redis服务器实例可以是master、slave、或者另一个Sentinel。
```c
typedef struct sentinelRedisInstance {
// 标识符,记录了实例的类型,及其当前状态
int flags;
// 实例的名字master的名字由用户配置slave和Sentinel的名字自动配置
// 格式为 ip: port
char *name;
// 实例的运行ID
char *runid;
// 配置计院,用于实现故障转移
uint64_t config_epoch;
// 实例的地址
sentinelAddr *addr;
// SENTINEL down-after-milliseconds 选项设定的值
// 实例无响应多少毫秒后才会判断为主观下线(subjectively down)
mstime_t down_after_periods;
// SENTINEL monitor <master-name> <IP> <port> <quorum> 选项的quorum参数
// 判断这个实例是否为客观下线(objectively down)所需的支持投票数量
int quorum;
// SENTINEL parallel-sycs <master-name> <number>选项的值
// 在执行故障转移时可以同时对新的master进行同步的slave数量
int parallel_syncs;
// SENTINEL failover-timeout <master-name> <ms>选项的值
// 判断故障转移状态的最大时限
mstime_t failover_timeout;
} sentinelRedisInstance;
```
`sentinelRedisInstance.addr`指向一个`sentinel.c/sentinelAddr`结构它保存着实例的IP地址和端口号
```c
typedef struct sentinelAddr {
char *ip;
int port;
} sentinelAddr;
```
## 创建与master的网络连接
连接建立后Sentinel将成为master的客户端可以向其发送命令。对于被监视的master来说Sentinel会创建两个异步网络连接
- 命令连接,用于发送和接收命令。
- 订阅连接。用于订阅master的`__sentinel__:hello`频道。
# 16.2 获取master信息
Sentinel以默认10秒一次的频率向master发送`INFO`命令,获取其当前信息:
- master本身的信息包括运行ID、role等。据此Sentinel更新master实例的结构。
- master的slave信息。据此Sentinel更新master实例的slaves字典。
# 16.3 获取slave信息
Sentinel发现master有新的slave时除了会为这个slave创建相应的实例结构外还会创建到它的命令连接和订阅连接。
通过命令连接Sentinel会向slave每10秒发送一次`INFO`命令根据回复更新slave的实例结构
- slave的运行ID
- slave的角色role
- master的地址和端口
- 主从的连接状态
- slave的优先级
- slave的复制偏移量
# 16.4 向master和slave发送信息
默认情况下Sentinel会以两秒一次的频率通过命令连接向所有被监视的master和slave发送
> PUBLISH \_\_sentinel\_\_:hello "<s_ip>, <s_port>, <s_runid>, <s_epoch>, <m_name>, <m_ip>, <m_port>, <m_epoch>"
其中以s\_开头的参数表示Sentinel本身的信息m\_开头的参数是master的信息。如果Sentinel正在监视的是slave那就是slave正在复制的master信息。
# 16.5 接收来自master和slave的频道信息
当Sentinel与一个master或slave建立订阅连接后会向服务器发送以下命令
> SUBSCRIBE \_\_sentinel\_\_:hello
Sentinel对\_\_sentinel\_\_:hello频道的订阅会持续到两者的连接断开为止。也就是说Sentinel既可以向服务器的\_\_sentinel\_\_:hello频道发送信息又通过订阅连接从\_\_sentinel\_\_:hello频道接收信息。
对于监视同一个server的多个Sentinel来说一个Sentinel发送的信息会被其他Sentinel收到。这些信息用于更新其他Sentinel队发送信息Sentinel和被监视Server的认知。
## 更新sentinels字典
Sentinel为master创建的实力结构中有sentinels字典保存了其他监视这个master的Sentinel
- 键是Sentinel名字格式为ip: port。
- 值是Sentinel实例的结构。
当一个Sentinel收到其他Sentinel发来的信息时目标Sentinel会从信息中提取出
- 与Sentinel有关的参数源Sentinel的IP、端口、运行ID、配置纪元。
- 与master有关的参数master的名字、IP、端口、配置纪元。
根据提取的参数目标Sentinel会在自己的Sentinel状态中更新sentinels和masters字典。
## 创建连向其他Sentinel的命令连接
Sentinel通过频道信息发现一个新的Sentinel时不仅会为其创建新的实例结构还会创建一个连向新Sentinel的命令连接新的Sentinel也会创建连向这个Sentinel的命令连接最终监视同一master的多个Sentinel成为相互连接的网络。各个Sentinel可以通过发送命令请求来交换信息。
# 16.6 检测主观下线状态
默认情况下Sentinel会每秒一次地向所有与它创建了嘛命令连接的实例master、slave、其他sentinel发送`PING`命令,并通过回复来判断其是否在线。只有+PONG/-LOADING/-MASERDOWN三种有效回复。
Sentinel的配置文件中`down-after-milliseconds`选项指定了判断实例主观下线所需的时间长度。在`down-after-milliseconds`毫秒内如果连续返回无效回复那么Sentinel会修改这个实例对应的实例结构`flags`属性中打开`SRI_S_DOWN`标识,标识主观下线。
注意多个Sentinel设置的`down-after-milliseconds`可能不同。
# 16.7 检查客观下线时长
当Sentinel将一个master判断为主观下线后为了确认是真的下线会向监视这一master的其他Sentinel询问。有足够数量quorum的已下线判断后Sentinel会将master判定为客观下线并对master执行故障转移。
# 16.8 选举领头Sentinel
master被判定为客观下线后监视这个master的所有Sentinel会进行协商选举一个领头Sentinel并由其对该master执行故障转移。选举的规则如下
- 所有Sentinel都可以成为领头。
- 每次进行领头Sentinel选举后不论选举是否成功所有Sentinel的配置纪元都会+1。这个配置纪元就是一个计数器。
- 一个配置纪元里所有Sentinel都有一次将某个Sentinel设置为局部领头Sentinel的机会且局部领头一旦设定在这个配置纪元内就不可修改。
- 每个发现master进入客观下线的Sentinel都会要求其他Sentinel将自己设为局部领头Sentinel。
- 当一个Sentinel向另一个Sentinel发送`SENTINEL is-master-down-by-addr`且命令中的runid参数是自己的运行ID这表明源Sentinel要求目标Sentinel将他设置为局部领头。
- Sentinel设置局部领头的规则是先到先得。
- 目标Sentinel收到`SENTINEL is-master-down-by-addr`后,会返回一条命令回复,恢复中的`leader_runid``leader_epoch`参数分别记录了目标Sentinel的局部领头Sentinel的运行ID和配置纪元。
- 源Sentinel收到目标Sentinel的回复后检查回复中的`leader_runid``leader_epoch`是否和自己相同。
- 如果某个Sentinel被半数以上的Sentinel设置为局部领头那么这个Sentinel就成为领头Sentinel。
- 因为领头Sentinel需要半数以上的支持且每个Sentinel在每个配置纪元里只设置一次局部领头所以一个配置纪元里只能有一个领头。
- 如果给定时限内没有产生领头Sentinel那么各个Sentinel过段时间再次选举知道选出领头为止。
# 16.8 故障转移
领头Sentinel会对已下线的master执行故障转移包括以下三个步骤
- 从已下线master属下的所有slave选出一个新的master。
- 让已下线master属下的所有slave改为新复制新的master。
- 让已下线master成为新master的slave重新上线后就是新slave。
## 选出新的master
新master的挑选规则
- 在线
- 五秒内回复过领头Sentinel的`INFO`命令
- 与已下线master在`down-after-milliseconds`毫秒内有过通信。
- salve的自身有优先级
- 复制偏移量最大
Sentinel向salve发送`SLAVEOF no one`命令将其转换为master。
## 修改salve的复制目标
同样通过`SLAVEOF`命令实现。
## 将旧的master变为slave
同样通过`SLAVEOF`命令实现。
# 导航
[目录](README.md)
上一章:[15. 复制](ch15.md)
下一章:[17.集群](ch17.md)

516
数据库/Redis/ch17.md Normal file
View File

@@ -0,0 +1,516 @@
Redis集群是分布式的数据库方案通过分片sharing来进行数据共享并提供复制或故障转移功能。
# 17.1 节点
一个Redis集群通常由多个节点node组成。开始时每个node都是独立的要将其连接起来
> CLUSTER MEET <ip> <port>
## 启动节点
一个节点就是运行在集群模式下的Redis服务器根据`cluster-endabled`配置选项是否为yes来决定是否开启集群模式。
节点在集群模式下会继续使用单机模式的组件,如:
- 文件事件处理器
- 时间事件处理器
- 使用数据库来保存键值对数据
- RDB和AOF持久化
- 发布与订阅
- 复制模块
- Lua脚本
节点会继续使用`redisServer`结构保存服务器的状态,`redisClient`结构保存客户端的状态,集群模式下的数据,保存在`cluster.h/clusterNode``cluster.h/clusterLink``cluster.h/clusterState`结构中。
## 集群数据结构
`cluster.h/clusterNode`保存了一个节点的当前状态如节点的创建时间、名字、配置纪元、IP和端口等。每个节点都有一个自己的`clusterNode`结构,并为集群中的其它节点创建一个相应的`clusterNode`结构。`clusterNode`结构的`link`属性是一个`clusterLink`结构,保存了连接节点所需的有关信息,如套接字、缓冲区。
每个节点都有一个`clusterState`,记录了当前节点所在集群的状态。
```c
struct clusterNode {
// 创建节点的时间
mstime_t ctime;
// 节点的名字40个十六进制字符串
char name[REDIS_CLUSTER_NAMELEN];
// 节点标识,记录节点的角色(主从)、状态(在线或下线)
int flags;
// 当前的配置纪元
uint64_t configEpoch;
char ip[REDIS_IP_STR_LEN];
int port;
// 保存连接节点所需的有关信息
clusterLink *link;
};
typedef struct clusterLink {
// 连接的创立时间
mstime_t ctime;
// TCP 套接字描述符
itn fd;
// 输出缓冲区
sds sndbuf;
// 输入缓冲区
sds recvbuf;
// 与这个连接相关联的节点没有就为NULL
struct clusterNode *node;
} clusterLink;
typedef struct clusterState {
// 指向当前节点的指针
clusterNode *myself;
// 集群当前的配置纪元,用于故障转移
uint64_t currentEpoch;
// 集群当前的状态:在线还是下线
int state;
// 集群中至少处理着一个槽的节点的数量
int size;
// 集群节点的名单包括myself键为节点的名字值为节点对应的clusterNode结构
dict *nodes;
} clusterState;
```
## CLUSTER MEET命令的实现
通过向节点A发送`CLUSTER MEET`命令客户端可以让接受命令的节点A将另一个节点B接入到A所在的集群中。
收到`CLUSTER MEET`命令的节点A会进行以下操作
1. 为节点B创建一个`clusterNode`结构,并将该结构添加到自己的`clusterState.nodes`字典。
2. 节点A根据`CLUSTER MEET`命令的IP和端口先节点B发送`MEET`消息。
3. 节点B收到`MEET`消息为节点A创建一个`clusterNode`结构,并加入字典。
4. 节点B回给节点A一条`PONG`消息。
5. 节点A收到`PONG`知道节点B已经接收了自己的`MEET`消息。
6. 节点A向节点B返回一条`PING`消息。
7. 节点B收到`PING`之后,双方握手完成。
![](img/chap17/img0.png)
# 17.2 槽指派
Redis集群通过分片的方式保存数据库中的键值对集群中的整个数据库被分为16384个槽slot数据库中的每个键都属于其中的一个集群中的每个节点可以处理0个或最多16384个槽。
当数据库中的16384个槽都有节点在处理时集群处于上线状态ok如果任何一个槽都没有得到处理就处于下线状态fail
`CLUSTER MEET`只是将节点连接起来,集群仍处于下线状态,通过向节点发送`CLUSTER ADDSLOTS`可以为一个或多个槽指派assign给节点负责。
> CLUSTER ADDSLOTS <slot> [slot ...]
## 记录节点的槽指派信息
```c
struct clusterNode {
unsigned char slots[16384/8];
int numslots;
};
```
`slots`数组中的索引`i`上的二进制位的值来判断节点是否负责处理槽`i``numslots`记录节点负责处理的槽的数量,即`slots`数组中二进制1的数量。
## 传播节点的槽指派信息
一个节点除了会将自己处理的槽记录在`clusterNode`结构中的`slots``numslots`属性之外,还会将自己的`slots`数组通过消息发送给集群中的其它节点。
节点A通过消息从节点B接收到节点B的`slots`数组会,会在自己的`clusterState.nodes`字典中查找节点B对应的`clusterNode`结构,并对结构中的`slots`数组进行更新。
最终集群中的每个节点都知道数据库中的16384个槽分别被指派给了哪些节点。
## 记录集群所有槽的指派信息
`clusterState`结构中的`slots`数组记录了所有16384个槽的指派信息
```c
typedef struct clusterState {
clusterNode *slots[16384];
} clusterState;
```
如果槽指派信息只保存在各个节点的`clusterNode.slots`数组中,那么检查某个槽被指派给哪个节点,就需要遍历`clusterState.nodes`字典中的所有`clusterNode`结构。`clusterState.slots`数组就解决了这个问题。
反过来,纵然有了`clusterState.slots``clusterNode.slots`仍有必要:
- 传播节点的槽指派信息时,只需要发送`clusterNode.slots`即可。
## CLUSTER ADDSLOTS命令的实现
`CLUSTER ADDSLOTS`命令接受一个或多个槽作为参数,并将所有输入的槽指派给接收该命令的节点负责:
```python
def CLUSTER_ADDSLOTS(*all_input_slots):
# 遍历所有输入槽,检查它们是否都是未指派
for i in all_input_slots:
# 如果有一个槽已指派,那么返回错误
if clusterState.slots[i] != NULL:
reply_error()
return
# 再次遍历
for i in all_input_slots:
# 设置clusterState结构的slots数组
clusterState.slots[i] = clusterState.myself
# 访问代表当前节点的clusterNode结构的slots数组
setSlotBit(clusterState.myself.slots, i)
```
# 17.3 在集群中执行命令
客户端向节点发送与数据库键有关的命令时,接收命令的节点会计算出命令要处理的键属于哪个槽,并检查这个槽是否被指派给了自己:
- 如果指派给了自己,节点直接执行命令。
- 否则,节点向客户端返回一个`MOVED`错误指引客户端转向redirect到正确的节点再次发送命令。
## 计算键属于哪个槽
```python
def slot_number(key):
return CRC16(key) & 16383
```
使用`CLUSTER KEYSLOT <key>`能查看键属于哪个槽。
## 判断槽是否由当前节点负责处理
节点计算出键所属的槽`i`之后,会检查自己在`clusterState.slots`数组中的第`i`项,判断键所在的槽是不是自己负责。
## MOVED错误
`MOVED`错误的格式为:
> MOVED <slot> <ip>:<port>
客户端通常会与集群中的多个节点创建套接字连接,所谓的节点转向就是换一个套接字来发送命令。
## 节点数据库的实现
节点与单击服务器的一个区别是节点只能使用0号数据库。
另外,除了将键值对保存在数据库里之外,节点会用`clusterState`结构中的`slots_to_keys`跳跃表来保存槽与键之间的关系:
```c
typdef struct clusterState {
zskiplist *slots_to_keys;
} clusterState;
```
`slots_to_keys`的每个分值score都是一个槽号每个节点的成员member都是一个数据库键
- 每当节点往数据库中添加新的键值对时,节点会将键与槽号关联到`slots_to_keys`
- 删除键值对时,节点也会接触`slots_to_keys`中键与槽号的关联。
通过在`slots_to_keys`中记录各个数据库键所属的槽,节点可以很方便地对属于某个槽的键进行批量操作,如`CLUSTER GETKEYINSLOT <slot> <count>`
# 17.4 重新分片
Redis集群的重新分片指的是将任意数量已经指派给某个节点的槽改为指派给另一个节点且相关槽所属的键也从源节点移动到目标节点。重新分片可以在线online进行分片过程中集群不需要下线且源节点和目标节点都可以继续处理命令请求。
重新分片是由Redis的集群管理软件`redis-trib`负责的Redis提供了重新分片所需的所有命令`redis-trib`则通过向源节点和目标节点发送命令来实现重新分片:
1. 向目标节点发送`CLUSTER SETSLOT <slot> IMPORTING <source_id>`命令让目标节点准备好导入源节点中属于槽slot的键值对。
2. 向源节点发送`CLUSTER SETSLOT <slot> MIGRATING <target_id>`命令,让源节点准备好迁移键值对。
3. 向源节点发送`CLUSTER GETKEYINSLOT <slot> <count>`命令获得最多count个属于槽slot的键值对的键名。
4. 对于步骤3获得的每个键名向源节点发送一个`MIGRATE <target_ip> <target_port> <key_name> 0 <timeout>`命令,将选中的键原子地从原籍诶单迁移到目标节点。
5. 充分执行步骤3和4知道所有键值对都被迁移至目标及诶单
6. 向集群中的任一节点发送`CLUSTER SETSLOT <slot> NODE <target_id>`命令将槽slot指派给目标节点这一指派信息通过消息传送至整个集群。
![](img/chap17/img1.png)
# 17.5 ASK 错误
在重新分片期间,源节点向目标节点迁移一个槽的过程中,可能会出现:属于被迁移槽的一部分键值对保存在源节点中,而另一部分保存在目标节点中。
当客户端向源节点发送一个与数据库键有关的命令,且要处理的键恰好就属于正在被迁移的槽时:
- 源节点现在自己的数据库中查找键,如果找到,直接执行命令。
- 否则,源节点向客户端返回`ASK`错误,指引客户端转向正在导入槽的目标节点,再次发送命令。
## CLUSTER SETSLOT IMPORTING 命令的实现
`clusterState`结构的`importing_slots_from`数组记录了当前节点正在从其它节点导入的槽:
```c
typedef struct clusterState {
clusterNode *importing_slots_from[16384;
} clusterState;
```
如果`importing_slots_from[i]`指向一个`clusterNode`结构,表示当前节点正在从`clusterNode`所代表的节点导入槽`i`
`CLUSTER SETSLOT <i> IMPORTING <source_id>` 命令,可以将目标节点的`importing_slots_from[i]`置为`source_id`所代表节点的`clusterNode`结构。
## CLSUTER SETSLOT MIGRATING 命令的实现
`clusterState`结构的`migrating_slots_to`数组记录了当前节点正在迁移至其它节点的槽:
```c
typedef struct clusterState {
clusterNode *migrating_slots_to[16384;
} clusterState;
```
如果`migrating_slots_to[i]`指向一个`clusterNode`结构,表示当前节点正在将槽`i`迁移至`clusterNode`所代表的节点。
`CLUSTER SETSLOT <i> MIGRATING <target_id>` 命令,可以将源节点的`migrating_slots_to[i]`置为`target_id`所代表节点的`clusterNode`结构。
## ASK 错误
节点收到一个关于键`key`的命令请求,先查找`key`所属的槽`i`是否自爱自己的数据库里,如果在,直接执行命令。
如果不在,节点会检查自己的`clusterState.migrating_slots_to[i]`,看槽`i`是否正在被迁移。如果是,返回客户端一个`ASK`错误。
接到`ASK`错误的客户端根据错误提供的IP地址和端口转向目标节点先向其发送一个`ASKING`命令,之后再重新发送原来要执行的命令。如果不先发送一个`ASKING`命令那么会被节点拒绝执行并返回MOVED错误。
## ASKING 命令
`ASKING`命令唯一要做的就是打开发送该命令的客户端的`REDIS_ASKING`标识。该标识是一次性标识,节点执行了一个带有该标识的客户端发来的命令后,标识就被移除。
## ASK 错误和MOVED 错误的区别
- `MOVED`错误代表槽的负责权已经转移。
- `ASK`错误是迁移槽过程中的临时措施。接收`ASK`指引的转向,不会对客户端今后发送关于槽`i`的命令请求有任何影响,客户端仍会将请求发送至目前负责处理槽`i`的节点,除非`ASK`错误再次出现。
# 17.6 复制与故障转移
Redis集群中的master用于处理槽slave用于复制某个master并在被复制的master下线时代替master继续处理命令请求。
## 设置slave
向一个节点发送命令:
> CLUSTER REPLICATE <node_id>
可以让接受命令的节点成为`node_id`所指定节点的slave并开始对master进行复制
1. 接收命令的节点先在自己的`clusterState.nodes`字典中找到`node_id`对应节点的`clusterNode`结构,并将自己的`clusterState.myself.slaveof`指针指向这个结构以此来记录正在复制的master。
2. 节点修改自己在`clusterState.myself.flags`中的属性,打开`REDIS_NODE_SLAVE`标识。
3. 节点调用复制代码,并根据`clusterState.myself.slaveof`指向的`clusterNode`结构保存的IP地址和端口号对主节点进行复制。
一个节点成为master并开始复制某个master这一信息会通过消息发送给集群中的其它节点。集群中的所有节点都会在代表主节点的`clusterNode`结构的`slaves``numslaves`属性中记录正在复制这个master的slave名单
```c
struct clusterNode {
// 正在复制这个master的slave数量
int numslaves;
// 正在复制这个master的slave的clusterNode结构
struct clusterNode **slaves;
};
```
## 故障检测
集群中的每个节点都会定期向其它节点发送`PING`消息,检测对方是否在线。各个节点都会通过消息来交换其它节点的状态信息。
当一个master A通过消息得知master B认为master C进入疑似下线状态A会在自己的`clusterState.nodes`字典中找到C对应的`clusterNode`结构并将B的下线报告添加到`clusterNode`结构的`fail_reposts`链表中:
```c
struct clusterNode {
// 一个链表,记录了所有其它节点对该节点的下线报告
list *fail_reports;
};
```
每个下线报告由一个`clusterNodeFailReport`结构表示:
```c
struct clusterNodeFailReport {
// 报告目标节点已经下线的节点
struct clusterNode *node;
// 最后一次从node节点收到下线报告的时间用这个来检查报告是否过期过期则删除
mstime_t time;
} typedef clusterNodeFailReports;
```
如果在一个集群里半数以上负责处理槽的master都将某个master X报告为疑似下线那么X就被标记为下线。将X标记为下线的节点向集群广播关于X的`FAIL`消息收到消息的节点会立即将X标记为已下线。
## 故障转移
当一个slave发现自己正在复制的master已下线会开始对其进行故障转移
1. 复制master的所有从节点里会有一个slave被选中。
2. 被选中的slave执行`SALVEOF no one`命令成为新的master。
3. 新master会撤销所有对已下线master的槽指派并指派给自己。
4. 新master向集群广播一条`PONG`消息宣布自己成为master。
5. 新master开始接收和处理自己负责的槽有关的命令请求。
## 选举新的master
新的master是选举产生的
1. 集群中的配置纪元是一个自增计数器初始值为0。
2. 集群中的某个节点开始一次故障转移操作时,集群配置纪元的值+1。
3. 对于每个配置纪元集群中每个负责处理槽的master都有一次投票机会而第一个向master要求投票的slave将获得投票权。
4. 当slave发现自己正在复制的master已下线会广播一条`CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST`要求收到消息的master给自己投票。
5. 如果一个master有投票权正在处理槽且未投票给其它slave那么master会向要求投票的slave返回一条`CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK`消息表示支持它成为新master。
6. 每个参与选举的slave都会接收到`CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK`消息,根据消息的个数来统计自己获得几票。
7. 一个slave收集到大于N/2+1的支持票后会当选新master。
8. 因为每个配置纪元里拥有投票权的master只有一票因此新的master只会有一个。
9. 如果一个配置纪元中没有选举出新master那么集群进入一个新的配置纪元继续选举。
# 17.7 消息
集群中的节点通过消息来通信消息主要分为以下5种
- `MEET`消息:加入当前集群
- `PING`消息:检测在线
- `PONG`消息:回复`MEET``PING`
- `FAIL`消息:进入`FAIL`状态
- `PUBLISH`消息:节点接收到`PUBLISH`消息,会执行这个命令,并向集群广播一条`PUBLISH`消息,所有接收到这条`PUBLISH`消息的节点都会执行相同的`PUBLISH`命令。
一个消息由消息头header和消息正文body组成。
## 消息头
每个消息头都由一个`cluster.h/clusterMsg`结构表示:
```c
typedef struct {
// 消息的长度,包括消息头和消息正文
uint32_t totlen;
// 消息的类型
uint16_t type;
// 消息正文包含的节点信息数量
// 只在发送MEET、PING、PONG这三种Gossip协议的消息时使用
uint16_t count;
// 发送者所处的配置纪元
uint64_t currentEpoch;
// 如果发送者是一个master那么这里记录的是发送者的配置纪元
// 如果发送者是一个slave那么这里记录的是发送者正在复制的master的配置纪元
uint64_t configEpoch;
// 发送者的名字(ID)
char sender[REDIS_CLUSTER_NAMELEN];
// 发送者目前的槽指派信息
unsigned char myslots[REDIS_CLUSTER_SLOTS/8];
// 如果发送者是一个slave那么这里记录的是它正在复制的master的名字
// 如果发送者是一个master那么这里记录的是REDIS_NODE_NULL_NAME
char slaveof[REDIS_CLUSTER_NAMELEN];
// 发送者的端口号
uint16_t port;
// 发送者的标识值
uint16_t flags;
// 发送者所处集群的状态
unsigned char state;
// 消息的正文
union clusterMsgData data;
} cllusterMsg;
union clusterMsgData {
struct {
// 每条 MEET、PING、PONG 消息都包含两个 clusterMsgDataGossip 结构
clusterMsgDataGossip[1];
} ping;
// FAIL 消息的正文
struct {
clusterMsgDataFail about;
} fail;
// PUBLISH 消息的正文
struct {
clusterMsgDataPublish msg;
} publish;
};
```
`clusterMsg`结构的`currentEpoch``sender``myslots`等属性记录了发送者的节点信息,接收者可以根据这些信息,在自己的`clusterState.nodes`字典中找到发送者对应的`clusterNode`结构进行更新。
## MEET、PING、PONG 消息的实现
Redis集群中的各个节点通过Gossip协议来交换节点的状态信息其中Gossip协议由`MEET``PING``PONG`三种消息实现,这三种消息的正文都是由两个`cluster.h/clusterMsgDataGossip`结构组成。
每次发送`MEET``PING``PONG`消息时,发送者从自己的已知节点中随机选出两个,将它们的信息保存到两个`cluster.h/clusterMsgDataGossip`结构中。
```c
typedef struct {
// 节点的名字
char nodename[REDIS_CLUSTER_NAMELEN];
// 最后一次向该节点发送 PING 消息的时间戳
uint32_t ping_sent;
// 最后一次从该节点接收到 PONG 消息的时间戳
uint32_t pong_received;
// 节点的IP
char ip[16];
// 节点的端口
uint16_t port;
// 节点的标识符
uint16_t flags;
} clusterMsgDataGossip;
```
接收者收到信息,访问正文中的两个`clusterMsgDataGossip`结构,根据自己是否认识其中的被选中节点来选择操作:
- 被选中节点不存在于接收者的已知节点列表根据IP和端口跟其握手。
- 被选中节点存在于接收者的已知节点列表:根据`clusterMsgDataGossip`记录的信息,更新被选中节点的`clusterNode`结构。
## FAIL 信息的实现
当集群里的master A将master B标记为已下线FAILA将集群广播关于B的`FAIL`消息接收到消息的节点都将B标记为已下线。为了避免Gossip协议的延迟`FAIL`消息正文采用`cluster.h/clusterMsgDataFail`结构表示:
```c
typedef struct {
char nodename[REDIS_CLUSTER_NAMELEN];
} clusterMsgDataFail;
```
## PUBLISH 消息的实现
向某个节点发送:
> PUBLISH <channel> <message>
会导致集群中的所有及诶单都向`channel`发送`message`消息。
`PUBLISH`消息的正文由`cluster.h/clusterMsgDataPublish`结构表示:
```c
typedef struct {
uint32_t channel_len;
uint32_t message_len;
// 8字节是为了对齐其他消息结构实际长度由保存的内容决定
// bulk_data 保存了channel参数和message参数
unsigned char bulk_data[8];
} clusterMsgDataPublish;
```
# 导航
[目录](README.md)
上一章:[16. Sentinel](ch16.md)
下一章:[18. 发布与订阅](ch18.md)

102
数据库/Redis/ch18.md Normal file
View File

@@ -0,0 +1,102 @@
Redis的发布与订阅功能由`PUBLISH``SUBSCRIBE``PSUBSCRIBE`等命令组成。
通过执行`SUBSCRIBE`命令客户端可以订阅一个或多个频道成为这些频道的订阅者subscriber每当有其他客户端向被订阅的频道发送消息时频道的所有订阅者都会收到这条消息。
客户端还可以通过`PSUBSCRIBE`订阅一个或多个模式:每当有其他客户端向某个频道发送消息,消息不仅会发送给这个频道的订阅者,还会发送给与这个频道相匹配的模式的订阅者。
# 18.1 频道的订阅与退订
Redis将所有频道的订阅关系都保存在服务器状态的`pubsub_channles`字典中,键是某个被订阅的频道,值是一个链表,里面记录了所有订阅这个频道的客户端:
```c
struct redisServer {
dict *pubsub_channels;
};
```
## 订阅频道
每当客户端执行`SUBSCRIBE`命令时,服务器都会将客户端与被订阅的频道在`pubsub_channles`字典中关联:
- 如果频道已有其他订阅者,将当前客户端添加到订阅者链表的末尾。
- 如果频道未有订阅者,则在`pubsub_channles`字典中创建一个键,并将客户端添加至链表。
## 退订频道
`UNSUBSCRIBE`命令让客户端退订某个频道,服务器从`pubsub_channles`字典中解除关联:
- 根据被退订频道的名字,在`pubsub_channles`字典中找到订阅者链表,移除退订客户端的信息。
- 如果链表变成了空,则从`pubsub_channles`字典中删除频道对应的键。
# 18.2 模式的订阅与退订
服务器将所有模式订阅关系保存在`pubsub_patterns`属性中:
```c
struct redisServer {
list *pubsub_patterns;
};
```
`pubsub_patterns`属性是个链表,每个节点都包含一个`pubsubPattern`结构:
```c
typedef struct pubsubPattern {
// 订阅模式的客户端
redisClient *client;
// 被订阅的模式
robj *pattern;
} pabsubPattern;
```
## 订阅模式
客户端执行`PSUBSCRIBE`订阅某个模式时,服务器会对被订阅的模式执行以下操作:
1. 新建一个`pubsubPattern`结构,初始化`pattern``client`值。
2.`pubsubPattern`结构添加到`pubsub_patterns`链表末尾。
## 退订模式
客户端执行`PUNSUBSCRIBE`退订某些模式的时候,服务器在`pubsub_patterns`链表中查找并删除那些`pattern`属性为被退订模式,且`client`属性为执行退订命令的客户端的节点。
# 18.3 发送消息
Redis客户端执行`PUBLISH <channel> <message>`命令,将消息发送给频道时,服务器执行以下两个操作:
1.`message`消息发送给`channel`频道的所有订阅者。
2. 如果一个或多个模式`pattern`与频道`channel`匹配,那么将消息`message`发送给`pattern`模式的订阅者。
## 将消息发送给频道订阅者
`pubsub_channles`字典中找到频道`channel`的订阅者名单,然后将消息发送给名单中的所有客户端。
## 将消息发送给模式订阅者
遍历整个`pubsub_patterns`链表,查找那些与`channel`频道相匹配的模式,然后将消息发送给订阅了这些模式的客户端。
# 18.4 查看订阅消息
`PUBSUB`命令可以查看频道或模式的相关信息。
## PUBSUB CHANNELS
`PUBSUB CHANNELS [pattern]`用于返回服务器当前被订阅的频道,其中`pattern`参数可选。这个命令遍历`pubsub_channles`字典的所有键,然后记录并返回符合条件的频道。
## PUBSUB NUMSUB
`PUBSUB NUMSUB [channel-1 channel-2 … channel-N]`返回这些频道的订阅者的数量,也是在`pubsub_channles`字典中找到频道对应的订阅者链表,然后返回链表的长度。
## PUBSUB NUMPAT
`PUBSUB NUMPAT`返回服务器当前被订阅的模式的数量,返回`pubsub_patterns`链表的长度。
# 导航
[目录](README.md)
上一章:[17. 集群](ch17.md)
下一章:[19. 事务](ch19.md)

159
数据库/Redis/ch19.md Normal file
View File

@@ -0,0 +1,159 @@
Redis通过`MULTI``EXEC``WATCH`等命令实现事务transaction功能。事务提供一种将多个命令请求打包然后一次性、按顺序地执行多个命令的机制。在事务执行期间服务器不会中断事务去执行其他客户端的命令请求。
事务以`MULTI`开始,接着是多个命令放入事务之中,最后由`EXEC`将这个事务提交commit到服务器执行。
# 19.1 事务的实现
一个事务从开始到结束经历三个阶段:
1. 事务开始
2. 命令入队
3. 事务执行
## 事务开始
`MULTI`命令标志着事务的开始,它将客户端从非事务状态切换到事务状态,即打开客户端状态的`flags`属性的`REDIS_MULTI`标识:
```python
def MULTI():
client.flags |= REDIS_MULTI
replyOK()
```
## 命令入队
客户端切换到事务状态后,服务器会根据不同的命令执行不同的操作:
- `EXEC``DISCARD``WATCH``MULTI`其中一个,服务器立即执行该命令。
- 否则,服务器将命令放入一个事务队列,然后向客户端返回`QUEUED`回复。
## 事务队列
每个Redis客户端都有自己的事务状态保存在客户端状态的`mstate`属性中:
```c
typedef struct redisClient {
multiState mstate;
} redisClient;
typedef struct multiState {
// 事务队列FIFO顺序
multiCmd *commands;
// 已入队命令计数
int count;
} multiState;
typedef struct multiCmd {
// 参数
robj **argv;
// 参数数量
int argc;
// 命令指正
struct redisCommand *cmd;
} multiCmd;
```
## 执行事务
服务器收到`EXEC`命令后,会遍历客户端的事务列表,执行其中的所有命令。最后将执行所得的结果返回给客户端。
```python
def EXEC():
# 创建空白的回复队列
reply_queue = []
# 遍历事务列表中的每个项
for argv, argc, cmd in client.mstate.commands:
# 执行命令
reply = execute_command(cmd, argv, argc)
reply_quque.append(reply)
# 移除 REDIS_MULTI 标识
client.flags &= ~REDIS_MULTI
# 清空客户端的事务状态,清零计数器,释放事务队列
client.mstate.count = 0
release_transaction_queue(client.mstate.commands)
send_reply_to_client(client, reply_queue)
```
# 19.2 WATCH 命令的实现
`WATCH`命令是个乐观锁,它可以再`EXEC`执行之前,监视任意数量的数据库键,并在`EXEC`执行时,检查被监视的键是否至少有一个已经被修改过了。如果是,服务器将拒绝执行事务,并返回客户端事务执行失败的空回复。
## 使用 WATCH 命令监视数据库键
每个Redis数据库都保存了一个`watched_keys`字典,键是某个被`WATCH`的数据库键,值是一个链表,记录了所有监视该键的客户端:
```c
typedef struct redisDb {
dict *watched_keys;
} redisDb;
```
## 监视机制的触发
所有对数据库进行修改的命令,执行之后都会调用`multi.h/touchWatchKey`函数对`watched_keys`字典进行检查。如果被监视的键被修改,那么打开监视该键的客户端的`REDIS_DIRTY_CAS`标识,表示该客户端的事务安全性已遭破坏。
## 判断事务是否安全
服务器收到`EXEC`命令后,根据这个客户端是否打开了`REDIS_DIRTY_CAS`标识来决定是否执行事务。
# 19.3 事务的ACID性质
Redis的事务总是具有原子性atomicity、一致性consistency、隔离性isolation且当Redis运行在某种特定的持久化模式下事务也具有耐久性durability
## 原子性
事务的原子性是指,事务中的多个操作当做一个整体来执行,要么执行所有,要么一个也不执行。
Redis的事务与传统关系型数据库事务的区别在于Redis不支持事务的回滚机制rollback即使事务队列中的某个命令执行出现错误整个事务也会继续执行下去直到所有命令执行完毕。
## 一致性
事务的一致性是指,如果数据库在事务执行前是一致的,那么执行后,无论事务是否执行成功,数据库也应该是一致的。「一致」是数据符合数据库本身的定义和要求,没有包含非法或无效的错误数据。
Redis通过谨慎的错误检测和简单的设计来保证事务的一致性。
1. 入队错误
如果事务在入队命令的过程中出现了命令不存在或者命令格式不正确等情况Redis会拒绝执行该事务。
2. 执行错误
执行过程中的错误是不能再入队时被服务器发现的,这些错误只会在命令实际执行时被触发。事务的执行过程中出现错误,服务器也不会中断事务的执行,而是继续执行其他命令,一致性的命令不会被出错的命令影响。
3. 服务器停机
执行事务的过程中停机不管服务器使用的何种持久化模式Redis总能保持重启后的数据库一致性。
## 隔离性
事务的隔离性是指,即使数据库中有多个事务并发执行,各个事务之间不会相互影响,且与串行执行的结果相同。
Redis采用单线程执行事务所以事务总是以串行的方式执行也当然具有隔离性。
## 持久性
事务的持久性是指,一个事务执行完毕后,结果已经被保存到永久性存储介质中。即使服务器停机,执行事务所得的结果也不会丢失。
Redis没有为事务提供额外的持久化功能事务的持久化由Redis使用的持久化模式决定的
- 无持久化:事务不具持久性,一旦停机,所有服务器的数据都将丢失。
- RDB持久化只有执行`BGSAVE`才会对数据库进行保存,且异步执行的`BGSAVE`不能保证事务数据在第一时间被保存。因此RDB持久化也不能保证事务的持久性。
- AOF持久化`appendfsync`选项为`always`时:程序执行命令后会调用同步操作,将命令数据保存到硬盘。这时事务是有持久性的。
- AOF持久化`appendfsync`选项为`everysec`时:每秒一次同步命令数据到硬盘,事务也不具有持久性。
- AOF持久化`appendfsync`选项为`no`时:程序交由操作系统来决定何时同步到硬盘,事务也不具有持久性。
# 导航
[目录](README.md)
上一章:[18. 发布与订阅](ch18.md)
下一章:[20. Lua脚本](ch20.md)

99
数据库/Redis/ch2.md Normal file
View File

@@ -0,0 +1,99 @@
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)

225
数据库/Redis/ch20.md Normal file
View File

@@ -0,0 +1,225 @@
Redis从2.6版本开始引入对Lua脚本的支持通过在服务器中嵌入Lua环境Redis客户端可以使用Lua脚本直接在服务器原子地执行多个Redis命令。
`EVAL`命令可以直接对输入的脚本进行求值:
> EVAL "return 'hello world'" 0
>
> "hello world"
`EVALSHA`命令可以根据脚本的SHA1校验和来对脚本进行求值但这个命令要求校验和对应的脚本至少被`EVAL`命令执行过一次,或者被`SCRIPT LOAD`命令载入过。
# 20.1 创建并修改Lua环境
Redis服务器创建并修改Lua环境的整个过程有以下步骤
1. 创建一个基础的Lua环境
2. 载入多个函数库到Lua环境
3. 创建全局表格`redis`表格包含了对Redis进行操作的函数`redis.call`
4. 使用Redis自制的随机函数来替换Lua原有的带有副作用的随机函数
5. 创建排序辅助函数
6. 创建`redis.pcall`函数的错误报告辅助函数,这个函数可以提供更详细的出错信息
7. 对Lua环境中的全局变量进行保护防止用户在执行Lua脚本时添加额外的全局变量
8. 将完成修改的Lua环境保存到服务器状态的`lua`属性中等待服务器传来的Lua脚本
## 创建Lua环境
服务器调用Lua的C API函数`lua_open`创建一个新的Lua环境。
## 载入函数库
- 基础库base library包含Lua的核心函数`assert``error`等。为了防止用户从外部文件引入不安全的代码,`loadfile`函数被删除。
- 表格库table library
- 字符串库string library
- 数学库math library
- 调试库debug library
- Lua CJSON库
- Struct库用于Lua值和C结构的转换
- Lua cmsgpack库处理MessagePack格式的数据
## 创建redis全局表格
`redis`表格包含以下函数:
- 用于执行Redis命令的`redis.call``redis.pcall`
- 用于记录日志的`redis.log`
- 用于计算SHA1校验和的`redis.sha1hex`
- 用于返回错误信息的`redis.error_repyl``redis.status_reply`
## 使用Redis自制的随机函数来替换Lua原有的随机函数
Redis服务器要求传入的Lua脚本都是纯函数pure function
Redis用自制的随机函数替换了原有的`math.random``math.randomseed`函数,自制函数有如下特征:
- 对于相同的seed`math.random`总是相同的随机数序列。
- 对除非在脚本中使用`math.randsomseed`显式修改seed否则每次运行脚本时Lua环境都是用固定的`math.randomseed(0)`来初始化seed。
## 创建排序辅助函数
当Lua脚本执行完一个不确定性的命令后会使用`__redis__compare_helper`函数作为对比函数,自动调用`table.sort`函数对命令的返回值排序,以保证相同的数据集总是产生相同是输出。
## 创建 redis.pcall 函数的错误报告辅助函数
Redis服务器为Lua环境创建一个名为`__redis__err__handler`的错误处理函数。当脚本调用`redis.pcall`执行Redis命令且执行的命令出现错误`__redis__err__handler`函数会打印出错代码的来源和行数。
## 保护Lua的全局环境
确保传入服务器的脚本不会因为忘记使用`local`关键字而将额外的全局变量添加到Lua环境。
## 将Lua环境保存到服务器状态的`lua`属性中
这一步服务器将Lua环境与服务器状态的`lua`属性关联起来。
因为Redis使用串行化的方式来执行命令所以任意时刻最多只会有一个脚本能够被放入Lua环境执行。因此整个Redis服务器只需要一个Lua环境即可。
# 20.2 Lua环境协作组件
除了创建并修改Lua环境之外Redis服务器还创建了两个用于与Lua环境写作的组件
- 负责执行Lua脚本中的Redis命令的伪客户端。
- 用于保存Lua脚本的`lua_scripts`字典。
## 伪客户端
Lua脚本使用`redis.call``redis.pcall`执行命令,需要以下几个步骤:
- Lua环境将`redis.call``redis.pcall`函数想要执行的命令传送给伪客户端。
- 伪客户端将命令传送给命令执行器。
- 命令执行器执行命令,并将结果返回给伪客户端。
- 伪客户端接收到结果将结果返回Lua环境。
- Lua环境接收到命令结果后将结果返回给`redis.call``redis.pcall`函数。
- 接收到结果的`redis.call``redis.pcall`函数将结果作为函数返回值返回给脚本的调用者。
![](img/chap20/img0.png)
## `lua_sripts`字典
`lua_scripts`字典的键为某个Lua脚本的SHA1校验和值是SHA1校验和对应的Lua脚本。
```c
struct redisServer {
dict *lua_scripts;
};
```
Redis服务器会将所有被`EVAL`执行过的Lua脚本和所有被`SCRIPT LOAD`再如果的Lua脚本都保存到`lua_scripts`字典中。
# 20.3 `EVAL` 命令的实现
`EVAL`命令执行过程分为三分步骤:
1. 根据客户端给定的Lua脚本在Lua环境中定义一个Lua函数。
2. 将客户端给定的脚本保存到`lua_scripts`字典中。
3. 执行刚刚在Lua环境中定义的函数。
## 定义脚本函数
函数名字由`f_`前缀加上脚本的SHA1校验和组成函数体则是脚本本身。使用函数来保存客户端传入的脚本有以下好处
- 执行脚本的步骤很简单,只要调用与脚本对应的函数即可。
- 通过函数的局部性让Lua环境保持清洁减少垃圾回收避免使用全局变量。
- 如果某脚本使用的函数在Lua环境中被定义过一次那么只要记得这个脚本的校验和服务器就可以直接调用Lua函数来执行脚本。这就是`EVALSHA`的实现原理。
## 将脚本保存到`lua_scripts`字典
服务器在`lua_scripts`字典中新添加一个键值对。
## 执行脚本函数
`lua_scripts`字典中保存脚本之后,服务器还需要一些准备工作,才能开始执行脚本:
1.`EVAL`命令传入的键名参数和脚本参数分别保存到`KEYS`数组和`ARGV`数组然后将这两个数组作为全局变量传入Lua环境。
2. 为Lua环境装载超时处理钩子hook在脚本出现超时后hook可以让客户端执行`SCRIPT SKILL`函数停止脚本,或`SHUTDOWN`命令关闭服务器。
3. 执行脚本函数。
4. 移除之前装载的超时钩子。
5. 将执行脚本函数的结果保存到客户端状态的输入缓冲区。
6. 对Lua环境执行垃圾回收操作。
# 20.4 `EVALSHA`命令的实现
```python
def EVALSHA(sha1):
# 拼接函数的名字
func_name = "f_" + sha1
# 查看这个函数是否在Lua环境中
if function_exits_in_lua_env(func_name):
execute_lua_funciton(func_name)
else:
send_script_error("SCRIPT NOT FOUND")
```
# 20.5 脚本管理命令的实现
## `SCRIPT FLUSH`
`SCRIPT FLUSH`命令用于清除服务器中所有与Lua脚本有关的信息它会释放并重建`lua_scripts`字典关闭现有的Lua环境并重建一个新的Lua环境。
## `SCRIPT EXISTS`
`SCRIPT EXISTS`命令根据输入的SHA1校验和检查其对应的脚本是否存在于服务器中。它是通过检查`lua_scripts`字典实现的。
## `SCRIPT LOAD`
`SCRIPT LOAD`命令所做的事情和`EVAL`的前两步一样:
- 在Lua环境中为脚本创建相应的函数
- 将脚本保存到`lua_scripts`字典中。
## `SCRIPT KILL`
如果服务器设置了`lua-time-limit`选项那么每次执行Lua脚本前服务器都会在Lua环境中设置一个超时钩子。
一旦钩子发现脚本的运行超时,那么将会定期在脚本执行期间的间隙,检查是否有`SCRIPT KILL``SHUTDOWN`命令到达服务器。
如果超时的脚本从未执行过写入操作,那么客户端可以通过`SCRIPT KILL`命令来停止执行脚本,并向客户端返回一个错误回复。
如果超时的脚本执行过写入操作,那么客户单只能用`SHUTDOWN nosave`命令来停止服务器,防止被不合法的数据写入。
# 20.6 脚本复制
服务器运行在复制模式下具有写性质的脚本也会被复制到slave`EVAL``EVALSHA``SCRIPT FLUSH``SCRIPT LOAD`
## 复制`EVAL`、`SCRIPT FLUSH`、`SCRIPT LOAD`
Redis复制`EVAL``SCRIPT FLUSH``SCRIPT LOAD`的方法和其他普通命令一样。master执行完上述命令后会将其传播到所有slave。
## 复制`EVALSHA`
因为主从服务器载入Lua脚本的情况不同`EVALSHA`命令不能直接传播给slave。
Redis要求master在传播`EVALSHA`命令的时候,必须确保`EVALSHA`要执行的脚本已经在slave中载入过。如果不能保证那么master会将`EVALSHA`替换为等价的`EVAL`命令传播给slave。
### 1. 判断`EVALSHA`命令是否安全
master使用服务器状态的`repl_scriptcache_dict`字典记录自己已经将哪些脚本传播给了所有slave。
```c
struct redisServer {
dict *repl_scriptcache_dict;
};
```
`repl_scriptcache_dict`的键是一个Lua脚本的SHA1校验和值全部是NULL。
如果一个脚本的SHA1出现在`lua_scripts`字典,却没有出现在`repl_scriptcache_dict`字典说明对应的的Lua脚本已被master载入却没有传播给所有slave。
### 2. 清空`repl_scriptcache_dict`字典
每当master添加一个新的slave时都会清空自己的`repl_scriptcache_dict`字典。
### 3. `EVALSHA`命令换成`EVAL`
通过`EVALSHA`指定的SHA1校验和以及`lua_scripts`字典保存的Lua脚本服务器总可以将 `EVALSHA`命令换成`EVAL`命令。
### 4. 传播`EVALSHA`命令
当master在本机执行完一个`EVALSHA`命令后根据其SHA1校验和是否存在于`repl_scriptcache_dict`字典决定是向所有slave传播`EVALSHA`还是`EVAL`命令。
# 导航
[目录](README.md)
上一章:[19. 事务](ch19.md)
下一章:[21. 排序](ch21.md)

117
数据库/Redis/ch21.md Normal file
View File

@@ -0,0 +1,117 @@
Redis的`SORT`命令可以对列表键、集合键或者有序集合键的值进行排序。
# 21.1 `SORT <key>`命令的实现
`SORT <key>` 可以对一个包含数字值的键key进行排序假设
> PRUSH numbers 3 1 2
>
> SORT numbers
1. 创建一个和numbers长度相同的数组每个元素都是一个`redis.h/redisSortObject`结构。
![](img/chap21/img0.png)
2. 遍历数组,将每个元素的`obj`指针指向numbers列表的各个项构成一一对应关系。
![](img/chap21/img1.png)
3. 遍历数组,将各个`obj`指针所指向的列表项转换为一个`double`类型的浮点数,并将这个浮点数保存在相应数组项的`u.score`属性中。
![](img/chap21/img2.png)
4. 根据数组项`u.score`的值,对数组进行数字值排序。
![](img/chap21/img3.png)
5. 遍历数组,将各个数组项的`obj`指针所指向的列表项作为排序结果返回给客户端。
```c
typedef struct _redisSortObject {
// 被排序的值
robj *obj;
// 权重
union {
// 排序数字值时使用
double score;
// 排序带有BY选项的字符串值使用
robj *cmpobj;
} u;
} redisSortObject;
```
# 21.2 `ALPHA`选项的实现
> SORT <key> ALPHA
可以对包含字符串值的键进行排序,例如:
> SADD fruits apple banana cherry
>
> SORT fruits ALPHA
1. 创建一个`redisSortObject`数组长度等于fruits集合。
2. 遍历数组,将各个元素的`obj`指针指向fruits集合的各个元素。
3. 根据`obj`指针所指向的集合元素,对数组进行字符串排序。
4. 遍历数组,一次将数组项的`obj`指针指向的元素返回给客户端。
# 21.3 `ASC`和`DESC`选项的实现
`SORT`默认是升序排序,所以`SORT <key>``SORT <key> ASC`是等价的。`DESC`可以降序排序。
升序和降序都是使用**快速排序**完成的。
# 21.4 `BY`选项的实现
默认情况下,`SORT`命令使用被排序键包含的元素作为排序的权重,元素本身决定了元素排序后的位置。
通过`BY`选项,`SORT`可以指定某些字符串键或某个哈希键所包含的某些域field作为元素的权重。
不同的是,排序用到的`redisSortObject`数组元素指向权重键。
# 21.5 带有`ALPHA`和`BY`选项的实现
`BY`选项默认权重键保存的是数字值,针对字符串值还是要配合`ALPHA`选型。
# 21.6 `LIMIT`选项的实现
默认情况下,`SORT`返回排序后的所有元素。`LIMIT`选项可以只返回一部分已排序的元素:
> SORT <key> ALPHA LIMIT <offset> <count>
其中:
- `offset`表示要跳过的已排序元素数量。
- `count`表示跳过给定数量的已排序元素后,要返回的元素数量。
`LIMIT`生效,还是要排序伸个数组,最后返回元素的时候,根据`offset``count`的索引。
# 21.7 `GET`选项的实现
默认情况下,`SORT`排序之后,总是返回被排序键所包含的元素。`GET`可以返回指定模式的键的值。
# 21.8 `STORE`选项的实现
默认情况下,`SORT`只向客户端返回结果,要保存结果,使用`SORTE`选项。
# 21.9 多个选项的执行顺序
如果按照选项来划分,`SORT`命令可以分为四步:
1. 排序:使用`ALPHA``ASC``DESC``BY`选项。
2. 限制结果集的长度:使用`LIMIT`选项。
3. 获取外部键:使用`GET`选项。
4. 保存结果集:使用`STORE`选项。
5. 先客户端返回结果集。
调用`SORT`命令时,除了`GET`选项之外,改变选项的位置不会影响`SORT`的顺序。
# 导航
[目录](README.md)
上一章:[20. Lua脚本](ch20.md)
下一章:[22. 二进制位数组](ch22.md)

107
数据库/Redis/ch22.md Normal file
View File

@@ -0,0 +1,107 @@
Redis提供了`SETBIT``GETBIT``BITCOUNT``BITOP`四个命令用于处理二进制位数组。
- `SETBIT`为位数组指定偏移量上的二进制位设置值0或1。
- `GETBIT`,获取位数组指定偏移量上的二进制位的值。
- `BITCOUNT`统计位数组中1的个数。
- `BITOP`,既可以对多个位数组进行按位与、按位或、按位异或运算,也可以对给定位数组取反。
# 22.1 位数组的表示
Redis使用字符串来表示位数组并使用SDS结构的操作函数来处理位数组。
![](img/chap22/img0.png)
- `redisObject.type`的值为`REDIS_STRING`,表示字符串对象。
- `sdshdr.len`值为1表示这个SDS保存了一个一字节长的位数组。
- `buf`数组的`buf[0]`字节保存了一个一字节长的位数组。
- `buf`数组的`buf[1]`字节保存了SDS程序自动追加到值的末尾的'\0'。
# 22.2 `GETBIT`命令的实现
> GETBIT <bitarray> <offset>
用于返回位数组`bitarray``offset`偏移量上的二进制位的值:
1. 计算 `byte = (offset / 8)``byte`记录了`offset`偏移量指定的二进制保存在位数组的哪个字节。
2. 计算 `bit = (offset mode 8) + 1``bit`记录`offset`指定的二进制位是`byte`字节的第几个二进制位。
3. 根据 `byte``bit` 值,在位数组 `bitarray`中定位`offset`指定的二进制位,并返回这个位的值。
# 22.3 `SETBIT`命令的实现
> SETBIT <bitarray> <offset> <value>
用于将位数组`bitarray``offset`偏移量上的二进制位设置为`value`
1. 计算`len = (offset / 8) + 1``len`记录了`offset`指定的二进制位至少需要多少个字节。
2. 检查`bitarray`键保存的位数组长度是否小于`len`。如果是,扩展,并将新空间的二进制位置为`0`
3. 计算 `byte = (offset / 8)``byte`记录了`offset`偏移量指定的二进制保存在位数组的哪个字节。
4. 计算 `bit = (offset mode 8) + 1``bit`记录`offset`指定的二进制位是`byte`字节的第几个二进制位。
5. 根据 `byte``bit` 值,在位数组 `bitarray`中定位`offset`指定的二进制位,首先将现在的值保存在`oldvalue`变量,然后将`value`设置为新值。
6. 向客户端返回`oldvalue`的值。
# 22.4 `BITCOUNT`命令的实现
`BITCOUNT`用于统计给定位数组中,值为`1`的二进制位的个数。它的实现用到了查表和variable-precision SWAR两种算法
- 查表算法使用键长为8的表记录了从`0000 0000``1111 1111`在内的汉明重量。
- variable-precision SWAR算法方面`BITCOUNT`在每次循环时载入128个二进制调用四次32位variable-precision SWAR算法来计算这个128个二进制位的汉明重量。
根据二进制位的长度是否大于128来决定使用哪种算法。
```python
# 一个表记录了所有8位长位数组的汉明重量
# 程序将8位长的位数组转换为无符号整数并在表中进行索引
# 例如对于输入0000 0011程序将二进制转换为无符号整数 3
# 然后取出 weight_in_byte[3]的值 22 就是 0000 0011 的汉明重量
weight_in_byte = [0, 1, 1, 2, 1, 2, 2, ..., 7, 7, 8]
def BITCOUNT(bits):
# 计算位数组中包含了多少个二进制位
count = count_bit(bits)
# 初始汉明重量为0
weight = 0
# 如果未处理的二进制位大于等于 128 位
# 那么使用 variable-precision SWAR 算法
while count >= 128:
# 4个swar调用每个调用计算32位二进制位的汉明重量
# 注意bits[i:j]中的索引j是不包含在取值范围之内的
weight += swar (bits[0:32])
weight += swar (bits[32:64])
weight += swar (bits[64:96])
weight += swar (bits[96:128])
# 移动指针,略过已处理的位
bits = bits[128:]
# 减少未处理位的长度
count -= 128
# 如果执行到这里说明未处理的位数量不足128那么使用查表法
while count:
index = bits_to_unsigned_int(bits[0:8])
weight += weight_in_byte[index]
# 移动指正,略过未处理的位
bits = bits[8:]
# 减少未处理位的长度
count -= 8
return weight
```
# 22.5 `BITOP`命令的实现
`BITOP`命令直接使用C语言的逻辑运算。
# 导航
[目录](README.md)
上一章:[21. 排序](ch21.md)
下一章:[23. 慢查询日志](ch23.md)

79
数据库/Redis/ch23.md Normal file
View File

@@ -0,0 +1,79 @@
Redis的慢查询日志用于记录执行时间超过给定时长的命令请求用户可以通过这个日志来监视和优化查询速度。
服务器有两个选项和慢查询有关:
- `slowlog-log-slower-than`,指定执行时间超过多少微妙的命令请求会被记录到日志上。
- `slowlog-max-len`,指定服务器上最多保存多少条慢查询日志。数量超过,则先入先出。
`SLOWLOG GET`可以查看服务器保存的慢查询日志。
# 23.1 慢查询日志的保存
```c
struct redisServer {
// 下一条日志的ID
long long slowlog_entry_id;
// 保存了所有日志的链表
lisg *slowlog;
long long slowlog_log_slower_than;
unsigned long slowlog_max_len;
};
// slowlog链表保存了所有慢查询日志每个节点都保存了一个slowlogEntry结构代表一条日志
typedef struct slowlogEntry {
long long id;
// 命令执行时的时间
time_t time;
// 执行命令的消耗时间,微妙级
long long duration;
// 命令与命令参数
robj **argv;
// 命令与命令参数的个数
int argc;
} slowlogEntry;
```
# 23.2 慢查询日志的阅览与删除
```python
def SLOTLOG_GET(number=None):
# 用户没有给定number惨呼那么打印全部日志
if number is None:
number = SLOWLOG_LEN()
# 遍历所有日志
for log in redisServer.slowlog:
if number <= 0:
break;
else:
number -= 1
printLog(log)
def SLOTLOG_LEN():
return len(redisServer.slowlog)
def SLOWLOG_RESET():
for log in redisServer.slowlog:
deleteLog(log)
```
# 23.3 添加新日志
每次命令执行前后,程序都会记录时间戳,两者之差就是命令执行的耗时。服务器会把这个时长传递给函数`slowlogPushEntryIfNeeded`,它负责检查是否需要创建慢查询日志:
1. 如果执行时长超过`slowlog-log-slower-than`选项,为其创建新日志,添加到`slowlog`链表的表头。
2. 如果慢查询日志的长度超过了`slowlog-max-len`的限制,那么将多余的日志从`slowlog`链表删除。
# 导航
[目录](README.md)
上一章:[22. 二进制位数组](ch22.md)
下一章:[24. 监视器](ch24.md)

38
数据库/Redis/ch24.md Normal file
View File

@@ -0,0 +1,38 @@
通过执行`MONITOR`命令,客户端可以将自己变成一个监视器,实时接收并打印出服务器正在处理的命令请求的相关信息。
# 24.1 成为监视器
```python
def MONITOR():
# 打开客户端的监视器标识
client.flags != REDIS_MONITOR
# 将客户端添加到服务器状态的monitors链表的末尾
server.monitors.append(client)
# 向客户端返回OK
send+reply("OK")
```
# 24.2 向监视器发送命令信息
服务器每次处理命令请求前,会调用`replicationFeedMonitors`函数,由它将被处理的命令的请求的相关信息发送给各个监视器。
```python
def replicationFeedMonitors(client, monitors, dbid, argv, argc):
# 创建要发送的消息
msg = create_msg(client, dbid, argv, argc)
# 遍历所有监视器
for monitor in monitors:
send_msg(monitor, msg)
```
# 导航
[目录](README.md)
上一章:[23. 慢查询日志](ch23.md)
**End**

67
数据库/Redis/ch3.md Normal file
View File

@@ -0,0 +1,67 @@
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)

164
数据库/Redis/ch4.md Normal file
View File

@@ -0,0 +1,164 @@
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) |
# 导航
[目录](README.md)
上一章:[3. 链表](ch3.md)
下一章:[5. 跳跃表](ch5.md)

85
数据库/Redis/ch5.md Normal file
View File

@@ -0,0 +1,85 @@
跳跃表是一种**有序数据结构**,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问的目的。跳跃表支持平均*O(logN)*、最坏*O(N)*的查找,还可以通过顺序性操作来批量处理节点。
Redis使用跳跃表作为有序集合键的底层实现之一如果有序集合包含的元素数量较多或者有序集合中元素的成员是比较长的字符串时Redis使用跳跃表来实现有序集合键。
在集群节点中跳跃表也被Redis用作内部数据结构。
# 5.1 跳跃表的实现
Redis的跳跃表由redis.h/zskiplistNode和redis.h/zskiplist两个结构定义其中zskiplistNode代表跳跃表节点zskiplist保存跳跃表节点的相关信息比如节点数量、以及指向表头/表尾结点的指针等。
![skiplist](img/chap5/skiplist.png)
```c
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int leve;
} zskiplist;
```
zskiplist结构包含
- header指向跳跃表的表头结点
- tail指向跳跃表的表尾节点
- level记录跳跃表内层数最大的那个节点的层数表头结点不计入
- length记录跳跃表的长度 即跳跃表目前包含节点的数量(表头结点不计入)
```c
typedef struct zskiplistNode {
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned int span; // 跨度
} level[];
struct zskiplistNode *backward;
double score;
robj *obj;
} zskiplistNode;
```
zskiplistNode包含
- level节点中用L1、L2、L3来标记节点的各个层每个层都有两个属性前进指针和跨度。前进指针用来访问表尾方向的其他节点跨度记录了前进指针所指向节点和当前节点的距离图中曲线上的数字
level数组可以包含多个元素每个元素都有一个指向其他节点的指针程序可以通过这些层来加快访问其他节点。层数越多访问速度就越快。没创建一个新节点的时候根据幂次定律越大的数出现的概率越小随机生成一个介于1-32之间的值作为level数组的大小。这个大小就是层的高度。
跨度用来计算排位rank在查找某个节点的过程中将沿途访问过的所有层的跨度累计起来得到就是目标节点的排位。
- 后退指针BW指向位于当前节点的前一个节点。只能回退到前一个节点不可跳跃。
- 分值score节点中的1.0/2.0/3.0保存的分值,节点按照各自保存的分值从小到大排列。节点的分值可以相同。
- 成员对象obj节点中的o1/o2/o3。它指向一个字符串对象字符串对象保存着一个SDS值。
注:表头结点也有后退指针、分值和成员对象,只是不被用到。
遍历所有节点的路径:
1. 访问跳跃表的表头,然后从第四层的前景指正到表的第二个节点。
2. 在第二个节点时,沿着第二层的前进指针到表中的第三个节点。
3. 在第三个节点时,沿着第二层的前进指针到表中的第四个节点。
4. 但程序沿着第四个程序的前进指针移动时遇到NULL。结束遍历。
# 5.2 跳跃表API
| 函数 | 作用 | 时间复杂度 |
| ------------------------------- | ------------------------------- | ------------------ |
| zslCreate | 创建一个跳跃表 | O(1) |
| zslFree | 释放跳跃表,以及表中的所有节点 | O(N) |
| zslInsert | 添加给定成员和分值的新节点 | 平均O(logN)最坏O(N) |
| zslDelete | 删除节点 | 平均O(logN)最坏O(N) |
| zslGetRank | 返回包含给定成员和分值的节点在跳跃表中的排位 | 平均O(logN)最坏O(N) |
| zslGetElementByRank | 返回给定排位上的节点 | 平均O(logN)最坏O(N) |
| zslIsInRange | 给定一个range跳跃表中如果有节点位于该range返回1 | O(1),通过表头结点和表尾节点完成 |
| zslFirstInRange zslLastInRange | 返回第一个/最后一个符合范围的节点 | 平均O(logN)最坏O(N) |
| zslDeleteRangeByScore | 删除所有分值在给定范围内的节点 | O(N) |
| zslDeleteRangeByRank | 删除所有排位在给定范围内的节点 | O(N) |
# 导航
[目录](README.md)
上一章:[4. 字典](ch4.md)
下一章:[6. 整数集合](ch6.md)

66
数据库/Redis/ch6.md Normal file
View File

@@ -0,0 +1,66 @@
整数集合intset是集合键的底层实现之一当一个集合只包含整数值元素并且数量不多时Redis采用整数集合作为集合键的底层实现。
# 6.1 整数集合的实现
整数集合可以保存int16\_t、int32\_t或者int64\_t的整数值且元素不重复intset.h/intset结构表示一个整数集合
```c
typedef struct intset {
uint32_t encoding; // 决定contents保存的真正类型
uint32_t length;
int8_t contents[]; // 各项从小到大排序
} inset;
```
![five-int16](img/chap6/five-int16.png)
上图中contents数组的大小为sizeof(int16\_t) * 5 = 80位。
# 6.2 升级
每当添加一个新元素到整数集合中且新元素的类型比现有所有元素的类型都要长时整数集合需要先升级update然后才能添加新元素
1. 根据新元素的类型,扩展底层数组的空间大小,并未新元素分配空间。
2. 将底层数组现有元素转换成与新元素相同的类型,并放置在正确的位置上(从后向前遍历)。放置过程中,维持底层数组的有序性质不变。
3. 将新元素添加到底层数组里。
因为每次升级都可能对所有元素进行类型转换,所以复杂度为*O(N)*。
PS. 因为引发升级的新元素长度比当前元素都大,所以它的值要么大于当前所有元素,要么就小于。前种情况放置在底层数组的末尾,后种情况放置在头部。
# 6.3 升级的好处
升级有两个好处
1. 提升整数集合的灵活性
我们可以随意地将int16\_t、int32\_t添加到集合中不必担心出现类型错误毕竟C是个静态语言。
2. 尽可能解约内存
避免用一个int64\_t的数组包含所有元素
# 6.4 降级
**整数集合不支持降级**
# 6.5 整数集合API
| 函数 | 作用 | 时间复杂度 |
| ------------- | ---------- | ------------------ |
| intsetNew | 创建一个新的整数集合 | O(1) |
| intsetAdd | 添加指定元素 | O(N) |
| intsetRemove | 移除指定元素 | O(N) |
| intsetFind | 检查给定值是否存在 | 因为底层数组有序所以O(logN) |
| insetRandom | 随机返回一个元素 | O(1) |
| intsetGet | 返回给定索引上的元素 | O(1) |
| intsetLen | 返回元素个数 | O(1) |
| intsetBlobLen | 返回占用的内存字节数 | O(1) |
# 导航
[目录](README.md)
上一章:[5. 跳跃表](ch5.md)
下一章:[7. 压缩列表](ch7.md)

93
数据库/Redis/ch7.md Normal file
View File

@@ -0,0 +1,93 @@
压缩列表ziplist是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表现并且每个列表项要么就是小整数值要么就是长度较短的字符串那么Redis就会使用压缩列表来实现列表键。
当一个哈希键只包含少量键值对并且每个键值对要么是小整数值要么是长度较短的字符串Redis就会使用压缩列表来实现哈希键。
# 7.1 压缩列表的构成
压缩列表是Redis为了节约内存而开发的由一系列特殊编码的连续内存块组成的顺序型sequential数据结构。一个压缩列表可以包含多个节点entry每个节点可以保存一个字节数组或者一个整数值。
压缩列表的各组成部分:
> zlbytes | zltail | zllen | entry1 | entry2 | … | entryN | zlend
其中,
| 属性 | 类型 | 长度 | 用途 |
| ------- | --------- | ---- | ---------------------------------------- |
| zlbytes | uint32\_t | 4字节 | 记录压缩列表占用的内存字节数在内存重分配或计算zlend的位置时使用 |
| zltail | uint32\_t | 4字节 | 记录表尾结点距离起始地址的字节数:通过这个偏移量,程序可以直接确定表尾结点的地址 |
| zllen | uint16\_t | 2字节 | 记录节点数量但这个属性小于UINT16\_MAX65535这个属性的值就是节点的数量。如果等于UINT16\_MAX节点的真实数量要遍历整个压缩列表才能得到 |
| entryX | 列表节点 | 不定 | 各个节点,节点的长度由保存的内容决定 |
| zlend | uint8\_t | 1字节 | 特殊值0xFF标记压缩列表的尾端 |
# 7.2 压缩列表节点的构成
压缩列表的节点可以保存一个字节数组或者一个整数值。压缩节点的各个组成部分:
> previous_entry_length | encoding | content
## previous_entry_length
previous_entry_length以字节为单位记录前一个节点的长度。previous_entry_length属性的长度可以是1字节或5字节
1. 若前一节点的长度小于254字节那么previous_entry_length属性的长度就是1字节。前一节点的长度保存在其中。
2. 若前一节点的长度大于254字节那么previous_entry_length属性的长度就是5字节其中属性的第一个字节被设置为0xFE十进制254而之后的四个字节则用于保存前一节点的长度。
程序可以通过指针运算,根据当前节点的起始地址来计算出前一个结点的起始地址。压缩列表的从尾向头遍历就是据此实现的。
## encoding
节点的encoding记录了节点的content属性所保存的数据的类型和长度
- 1字节、2字节或者5字节长值的最高位为00、01或10的是字节数组编码这种编码表示节点的content保存的是字节数组数组的长度由编码除去最高两位置后的其他位记录。
- 1字节长。值的最高位以11开头的是整数编码表示content保存着整数值整数值的类型和长度由编码除去最高两位之后的其他位记录。
## content
content保存节点的值可以使字节数组或整数值的类型和长度由encoding属性决定。
保存字节数组“hello world”的节点
| previoid_entry_length | encoding | content |
| --------------------- | -------- | ------------- |
| ... | 00001011 | "hello world" |
保存整数10086的节点
| previoid_entry_length | encoding | content |
| --------------------- | -------- | ------- |
| ... | 11000000 | 10086 |
# 7.3 连锁更新
因为previoid_entry_length的长度限制添加或删除节点都有可能引发「连锁更新」。在最坏的情况下需要执行*N*次重分配操作,而每次空间重分配的最坏复杂度是*O(N)*,合起来就是*O(N^2)*。
尽管如此,连锁更新造成性能问题的概率还是比较低的:
1. 压缩列表里有多个连续的、长度介于250和253字节之间的节点连锁更新才有可能触发。
2. 即使出现连锁更新,只要需要更新的节点数量不多,性能也不会受影响。
# 7.4 压缩列表API
| 函数 | 作用 | 复杂度 |
| ------------------ | ---------------------- | ---------------------------------------- |
| ziplistNew | 创建新的压缩列表 | O(1) |
| ziplistPush | 创建一个包含给定值的新节点,并添加到表头或尾 | 平均O(N)最坏O(N^2) |
| ziplistInsert | 将包含给定值的新节点插入到给定节点之后 | 平均O(N)最坏O(N^2) |
| ziplistIndex | 返回给定索引上的节点 | O(N) |
| ziplistFind | 查找并返回给定值的节点 | 因为节点的值可能是一个数组所以检查节点值和给定值是否相同的复杂度为O(N)查找整个列表的复杂度为O(N^2) |
| ziplistNext | 返回给定节点的下一个节点 | O(1) |
| ziplistPrev | 返回给定节点的前一个节点 | O(1) |
| ziplistGet | 获取给定节点所保存的值 | O(1) |
| ziplistDelete | 删除给定节点 | 平均O(N)最坏O(N^2) |
| ziplistDeleteRange | 删除在给定索引上的连续多个节点 | 平均O(N)最坏O(N^2) |
| ziplistBlobLen | 返回压缩列表占用的内存字节数 | O(1) |
| ziplistLen | 返回包含的节点数量 | 节点数量小于65535时为O(1)否则为O(N) |
# 导航
[目录](README.md)
上一章:[6. 整数集合](ch6.md)
下一章:[8. 对象](ch8.md)

286
数据库/Redis/ch8.md Normal file
View File

@@ -0,0 +1,286 @@
Redis并没有使用SDS、双端链表、字典、压缩列表、整数集合来实现键值对数据库而是基于这些数据结构创建了一个对象系统。这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象。
通过这五种类型的对象Redis可以在执行命令之前根据对象的类型判断一个对象是否执行给定的命令。使用对象的好处是可以针对不同的场景为对象设置多种不同的数据结构的实现从而优化使用效率。
除此之外Redis还实现了引用计数的内存回收机制。当程序不再需要某个对象的时候它所占用的内存会被自动释放。另外Redis还用引用计数实现了对象共享让多个数据库键共享同一个对象来节约内存。
最后Redis的对象带有访问时间记录信息空转时长较大的键可能被优先删除。
# 8.1 对象的类型和编码
Redis使用对象来表示数据库中的键和值。创建一个新键值对时至少会创建两个对象一个对象用作键一个对象用作值。每个对象都由一个redisObject结构表示
```c
typedef struct redisObject {
unsigned type: 4; // 类型
unsigned encoding: 4; // 编码
void *ptr; // 指向底层实现数据结构的指针
// ...
} robj;
```
## 类型
对象的type记录了对象的类型它的值可以使
| type常量 | 对象的名称 |
| ------------- | ------ |
| REDIS\_STRING | 字符串对象 |
| REDIS\_LIST | 列表对象 |
| REDIS\_HASH | 哈希对象 |
| REDIS\_SET | 集合对象 |
| REDIS\_ZSET | 有序集合对象 |
键总是一个字符串对象,值可以是字符串对象、列表对象、哈希对象、集合对象、有序集合对象。
但数据库执行TYPE命令时返回的结果为数据库键对应的值对象的类型而不是键对象的类型。
## 编码和底层实现
对象的ptr指向对象的底层实现数据结构而这些数据结构由对象的encoding决定它可以是
| encoding常量 | 对应的底层数据结构 |
| --------------------------- | ------------ |
| REDIS\_ENCODING\_INT | long类型的整数 |
| REDIS\_ENCODING\_EMBSTR | embstr编码的SDS |
| REDIS\_ENCODING\_RAW | SDS |
| REDIS\_ENCODING\_HT | 字典 |
| REDIS\_ENCODING\_LINKEDLIST | 双端链表 |
| REDIS\_ENCODING\_ZIPLIST | 压缩列表 |
| REDIS\_ENCODING\_INTSET | 整数集合 |
| REDIS\_ENCODING\_SKIPLIST | 跳跃表和字典 |
每种类型的对象至少使用了两种编码。
使用OBJECT ENCODING命令可以查看一个数据库键的值对象的编码。
# 8.2 字符串对象
字符串对象的编码可以使int、raw或embstr。
1. 如果字符串对象保存的是整数值且可以用long类型表示那么字符串对象会将整数值保存在ptr中将void* 转换成 long并将编码设置为int。
2. 如果字符串对象保存到是一个字符串值且长度大于32字节那么字符串对象使用SDS来保存这个字符串值并将编码设置为raw。
3. 如果字符串对象保存到是一个字符串值且长度小于等于32字节那么字符串对象使用embstr编码的方式来存储这个字符串值。
embstr编码是专门用来保存短字符串的优化方式。和raw编码一样都是用redisObject结构和sdshdr结构来表示字符串对象但raw会调用两次内存分配函数分别创建redisObject结构和sdshdr结构而embstr则通过一次内存分配一块连续空间依次包含两个结构
| redisObject | sdshdr |
| ------------------------------ | ------------------ |
| type \| encoding \| ptr \| ... | free \| len \| buf |
embstr的好处
1. 内存分配次数降为一次。
2. 释放字符串对象只要一次内存释放函数。
3. 因为内存连续,可以更好地利用缓存。
PS. 用`long double`类型表示的浮点数在Redis中也是作为字符串值存储的。程序会先将浮点数转成字符串值然后再保存转换的字符串值。
## 编码的转换
int编码和embstr编码的字符串对象可以被转换为raw编码的字符串对象。
1. 对int编码的字符串对象执行一些命令可使其不再是整数值而是字符串值那么编码也就变为raw了。如APPEND。
2. 对embstr编码的字符串执行修改命令也会变成raw对象。如APPEND。
## 字符串命令的实现
用于字符串键的所有命令都是针对字符串对象来构建的。
| 命令 | int编码的实现方法 | embstr编码的实现方法 | raw编码的实现方法 |
| ---------- | ---------------------------------------- | ---------------------------------------- | ---------------------------------------- |
| SET | int编码保存值 | embstr编码保存值 | raw编码保存值 |
| GET | 拷贝对象所保存的整数值,将这个拷贝转换为字符串值,然后向客户端返回这个字符串值 | 直接向客户端返回字符串值 | 直接向客户端返回字符串值 |
| APPEND | 将对象转换为raw编码然后按raw方式执行此操作 | 将对象转换为raw编码然后按raw方式执行此操作 | 调用sdscatlen函数将给定字符串追加到现有字符串的末尾 |
| INCBYFLOAT | 取出整数值并将其转换为long double的浮点数对这个浮点数进行加法计算然后将结果保存起来 | 取出整数值并将其转换为long double的浮点数对这个浮点数进行加法计算然后将结果保存起来。如果字符串值不能被转换为浮点数那么客户端会报错 | 取出整数值并将其转换为long double的浮点数对这个浮点数进行加法计算然后将结果保存起来。如果字符串值不能被转换为浮点数那么客户端会报错 |
| INCBY | 对整数值进行加法计算,得出的结果作为整数被保存起来 | 不能执行此命令,客户端报错 | 不能执行此命令,客户端报错 |
| DECBY | 对整数值进行减法计算,得出的结果作为整数被保存起来 | 不能执行此命令,客户端报错 | 不能执行此命令,客户端报错 |
| STRLEN | 拷贝对象保存的整数值,将这个拷贝转换为字符串值,计算并返回这个字符串值的长度 | 调用sdslen函数返回字符串的长度 | 调用sdslen函数返回字符串的长度 |
| SETRANGE | 将对象转换为raw编码然后按raw方式执行此命令 | 将对象转换为raw编码然后按raw方式执行此命令 | 将字符串特定索引上的值设置为给定的字符 |
| GETRANGE | 拷贝对象保存的整数值,将这个拷贝转换为字符串,然后取出返回字符串指定索引上的字符 | 直接取出并返回给定索引上的字符 | 直接取出并返回给定索引上的字符 |
# 8.3 列表对象
列表对象的编码是ziplist或linkedlist。
使用ziplist时每个压缩列表的节点保存了一个列表元素。使用linkedlist时每个链表节点保存了一个字符串对象而每个字符串对象都保存了一个列表元素。字符串对象是Redis五种类型的对象中唯一一种会被嵌套的对象。
## 编码转换
当列表对象同时满足以下两个条件时使用ziplist编码
1. 保存的字符串对象的长度都小于64字节。
2. 保存的元素数量小于512个。
否则就是用linkedlist编码。
> 以上两个条件的上限可以修改使用list-max-ziplist-value选项和list-max-ziplist-entries选项。
## 列表命令的实现
| 命令 | ziplist编码的实现 | linkedlist编码的实现 |
| ------- | ---------------------------------------- | ---------------------------------------- |
| LPUSH | 调用ziplistPush函数将新元素压入表头 | 调用listAddNodeHead函数将新元素压入表头 |
| RPUSH | 调用ziplistPush函数将新元素压入表尾 | 调用listAddNodeTail函数将新元素压入表尾 |
| LPOP | 调用ziplistIndex定位表头节点返回节点保存的元素后调用ziplistDelete删除表头结点 | 调用lsitFrist定位表头节点返回节点保存的元素后调用listDelNode删除表头结点 |
| RPOP | 调用ziplistIndex定位表尾节点返回节点保存的元素后调用ziplistDelete删除表尾结点 | 调用listLast定位表尾节点返回节点保存的元素后调用listDelNode删除表尾结点 |
| LINDEX | 调用ziplistIndex | 调用listIndex |
| LLEN | 调用ziplistLen | 调用listLength |
| LINSERT | 插入新节点到表头或表尾时使用ziplistPush其他位置使用ziplistInsert | 调用listInsertNode |
| LREM | 遍历节点调用ziplistDelete删除包含给定元素的节点 | 遍历节点调用listDelNode删除包含给定元素的节点 |
| LTRIM | 调用ziplistDeleteRange函数删除不再指定索引范围内的节点 | 遍历节点调用listDelNode |
| LSET | 调用ziplistDelete先删除给定索引上的节点然后调用ziplistInsert插入新节点 | 调用listIndex函数定位给定索引上的节点然后通过赋值操作更新节点的值 |
# 8.4 哈希对象
哈希对象的编码可以是ziplist或hashtable。
使用ziplist时每当有新的键值对要加入哈希对象时程序先保将存了**键**的压缩列表对象推入到表尾,然后再将保存了**值**的节点推入到表尾。因此:
1. 保存了同一键值对的两个节点总是挨在一起。
2. 先添加的键值对会被放在表头,后添加的在表尾。
![](img/chap8/img0.png)
使用hashtable时哈希对象中的每个键值对都使用一个字典键值对来保存
- 字典的每个键都是一个字符串对象,对象中保存了键值对的键。
- 字典的每个值都是一个字符串独显,对象中保存了键值对的值。
![](img/chap8/img1.png)
## 编码转换
当哈希对象同时满足以下两个条件时使用ziplist编码
1. 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节。
2. 哈希对象保存的键值对数量小于512个。
否则就使用hashtable编码。
> 以上两个条件的上限可以修改使用hash-max-ziplist-value选项和hah-max-ziplist-entries选项。
## 哈希命令的实现
| 命令 | ziplist编码的实现 | hashtable编码的实现 |
| ------- | ---------------------------------------- | -------------------------------- |
| HSET | ziplistPush将元素压入表尾然后再ziplistPush将值压入表尾 | dictAdd添加新节点 |
| HGET | ziplistFind查找指定键对应的节点再ziplistNext将指针移动到键节点旁边的值节点返回直值节点 | dictFind查找给定键然后dictGetVal返回对应的值 |
| HEXISTS | ziplistFind查找指定键对应的节点 | dictFind |
| HDEL | ziplistFind然后删除键节点和值节点 | dictDelete |
| HLEN | ziplistLen然后除以2 | dictSize |
| HGETALL | 遍历ziplistziplistGet返回所有的键和值 | 遍历字典dictGetKey返回键dictGetVal返回值 |
# 8.5 集合对象
集合对象的编码可以使intset或hashtable。
1. inset编码集合对象的所有元素都被保存在整数集合中。
![](img/chap8/img2.png)
2. hashtable编码字典的每个键都是一个字符串对象每个字符串对象都包含了一个集合元素字典的值全部为NULL。
![](img/chap8/img3.png)
## 编码的转换
当集合对象同时满足一下两个条件时使用inset编码
1. 所有元素都是整数值。
2. 元素数量不超过512个。
> 第二个的上限修改查看set-max-intset-entries选项。
## 集合命令的实现
| 命令 | intset编码的实现 | hashtable编码的实现 |
| ----------- | --------------------------- | ----------------------------- |
| SADD | intsetAdd | dictAdd |
| SCARD | intsetLen | dictSize |
| SISMEMBER | intsetFind | dictFind |
| SMEMBERS | 遍历集合使用intsetGet返回元素 | 遍历字典使用dictGetKey返回元素 |
| SRANDMEMBER | intsetRandom随机返回一个元素 | dictGetRandomKey |
| SPOP | intsetRandom然后intsetRemove | dictGetRandomKey然后dictDelete |
| SREM | intsetRemove | dictDelete |
# 8.6 有序集合的对象
有序集合的编码是ziplist或skiplist。
1. ziplist编码每个集合元素使用两个紧挨在一起的ziplist节点来存储。第一个节点保存元素的成员member第二元素保存元素的分值score。元素按分值的从小到大排序。
2. skiplist编码一个zset结构同时包含一个字典和一个跳跃表。跳跃表按分值从小到大保存了所有集合元素每个跳跃表节点都保存了一个集合元素节点的object保存了元素的成员score保存了元素的分值。字典为有序集合创建了一个从成员到分值的映射字典中的每个键值对都保存了一个集合元素键保存了元素的成员值保存了元素的分值。
## 编码的转换
有序集合满足以下两个条件时使用ziplist编码
1. 元素数量小于128。
2. 元素成员的长度小于64个字节。
> 两个条件的上限参考zset-max-ziplist-entries和zset-max-ziplist-value选项。
## 有序集合命令的实现
| 命令 | ziplist编码的实现 | zset编码的实现 |
| --------- | --------------------------- | -------------------------------------- |
| ZADD | ziplistInsert将成员和分值两个节点分别插入 | zslInsert将新元素插入跳跃表然后dictAdd将新元素关联到字典 |
| ZCARD | ziplistLen然后除以2 | 访问跳跃表的length |
| ZCOUNT | 遍历列表,统计分值在给定范围内的节点的数量 | 遍历跳跃表 |
| ZRANGE | 从头到尾遍历 | 从头到尾遍历跳跃表 |
| ZREVRANGE | 从尾向头遍历 | 从尾向头遍历 |
| ZRANK | 从头到尾遍历,查找给定成员,并记录经过节点的数量 | 从头到尾遍历,查找给定成员,并记录经过节点的数量 |
| ZREVRANK | 从尾向头遍历,查找给定成员,并记录经过节点的数量 | 从尾向头遍历,查找给定成员,并记录经过节点的数量 |
| ZREM | 遍历,删除包含给定成员的节点及旁边的分值节点 | 遍历跳跃表,删除节点,并在字典中解除被删除元素的成员和分值的关联 |
| ZSCORE | 遍历查找成员节点,返回旁边的分值节点 | 从字典中取出给定成员的分值 |
# 8.7 类型检查与命令多态
在执行一个类型特定的命令之前Redis会先检查输入键的类型是否正确然后再决定是否执行。类型检查是通过redisObject的type属性来的。
除此之外Redis还会根据值对象的编码方式选择正确的实现命令来执行。这就是多态。
LLEN命令的执行过程
![](img/chap8/img4.png)
# 8.8 内存回收
Redis为对象系统构建了一个引用计数垃圾回收。每个对象的引用计数由redisObject结构的refcount保存。
| 操作 | 引用计数的变化 |
| ----------- | ------- |
| 创建一个新对象 | 初始化为1 |
| 对象被一个新程序使用 | +1 |
| 对象不再被一个程序使用 | -1 |
当计数变为0时对象占用的内存就会被释放。
# 8.9 对象共享
refcount还可用于对象共享
1. 将数据库键的值指向现有的值对象。
2. refcount++。
Redis在初始化服务器时创建了10000个字符串对象包含0 ~ 9999的所有整数值用于共享。
> 数量通过redis.h/REDIS\_SHARED\_INTSETGERS常量控制。
使用OBJECT REFCOUNT可查看值对象的引用计数。
**但Redis只对包含整数值的字符串对象共享**。即只有共享对象和目标对象完全相同的情况下。一个共享对象保存的值越复杂,验证共享对象和目标对象是否相同的操作也就越复杂。
# 8.10 对象的空转时长
redisObject最后一个属性lru记录了对象最后一次被访问的时间用OBJECT IDLETIME可查看。
如果服务器打开了maxmemory属性lru对象可用于回收内存。
# 导航
[目录](README.md)
上一章:[7. 压缩列表](ch7.md)
下一章:[9. 数据库](ch9.md)

277
数据库/Redis/ch9.md Normal file
View File

@@ -0,0 +1,277 @@
# 9.1 服务器中的数据库
Redis服务器将所有数据库都保存在服务器状态redis.h/redisServer结构的db数组中数组的每个项都是一个redis.h/redisDb结构每个redisDb结构代表一个数据库
```c
struct redisServer {
// ...
redisDb *db;
int dbnum; // 数据库的数量
// ...
};
```
其中dbnum的值有服务器配置的database选项决定默认为16。
# 9.2 切换数据库
默认情况下Redis客户端的目标数据库是0号数据库客户端可以执行`SELECT`命令来切换。
服务器内部,客户端状态`redisClient`结构的db属性记录了客户端当前的目标数据库
```c
typedef struct redisClient {
redisDb *db; // 指向redisServer.db数组中的一个元素
} redusClient;
```
# 9.3 数据库键空间
Redis是一个键值对key-value pair数据库服务器。redisDb结构的dict字典保存了数据库的所有键值对这个字典就是键空间
```c
typedef struct redisDb {
// ...
dict *dict;
// ...
} redisDb;
```
键空间和用户所见的数据库是直接对应的:
- 键空间的键也就是数据库的键。每个键都是一个字符串对象。
- 键空间的值也是数据库的值。每个值可以使字符串对象、列表对象、哈希表对象、集合对象、有序集合对象。
![](img/chap9/img0.png)
所有针对数据库的操作,实际上都是通过键空间字典来实现。
## 添加新键
添加一个新键值对到数据库,就是将新键值对添加到键空间字典中。
## 删除键
删除数据库中的一个键,就是在键空间中删除键所对应的键值对对象。
## 更新键
更新数据库的一个键,就是对键空间里键所对应的值对象进行更新。根据值对象类型的不同,更新的具体方法也不同。
## 对键取值
对一个数据库键取值,就是在键空间中取出键所对应的值对象。
## 读写键空间时的维护操作
当Redis对数据库读写时不仅对键空间执行指定的操作还会执行一些额外的维护
1. 读取一个键后,更新服务器的键命中次数或不命中次数。这两个值可通过`INFO stats`命令查看。
2. 读取一个键后更新LRU时间。`OBJECT idletime <key>`查看。
3. 读取键时发现已过期,删除。
4. 如果有客户端`WATCH`了某个键修改后将键标记为dirty从而让事物程序注意到它。
5. 每次修改一个键后将dirty键计数器的值+1这个计数器会触发服务器的持久化和赋值操作。
6. 如果服务器开启了通知功能,键修改后,服务器会按照配置发送通知。
# 9.4 设置键的生存时间或过期时间
`EXPIRE``PEXPIRE`命令让客户端可以以秒或者毫秒进度为某个键设置生存时间。经过指定的时间后服务器会自动删除生存时间为0的键。
`EXPIREAT``PEXPIREAT`命令以秒或毫秒精度为某个键设置过期时间过期时间是一个UNIX时间戳。
`TTL``PTTL`命令可查看某个键的剩余生存时间。
实际上,`EXPIRE``PEXPIRE``EXPIREAT`三个命令都是使用`PEXPIREAT`来实现的。
## 保存过期时间
redisDb结构的expires字典保存了所有键的过期时间
- 过期字典的键是一个指针,指向键空间中的某个键对象。
- 过期字典的值是一个`long long`类型的整数保存了一个UNIX时间戳。
```c
typedef struct redisDb {
// ...
dict *expires;
// ...
} redisDb;
```
![](img/chap9/img1.png)
`PEXPIREAT`的伪代码定义:
```python
def PEXPIREAT(key, expire_time_in_ms):
# 如果键不存在于键空间,那么不能设置过期时间
if key not in redisDb.dict:
return 0
# 在过期字典中关联键和过期时间
redisDb.expires[key] = expire_time_in_ms
# 设置成功过
return 1
```
## 移除过期时间
`PERSIST`可以移除一个键的过期时间,它在过期字典中找到给定的键,解除键和值(过期时间)的关联。
```python
def PERSIST(key):
# 如果键不存在或者没有设置过期时间
if key not in redisBb.expires:
return 0
redisDb.expires.remove(key)
return 1
```
## 计算并返回剩余生存时间
`TTL``PTTL`都是通过计算键的过期时间和当前时间的差来实现的:
```python
def PTTL(key):
if key not in redisDb.dict:
return -2
expire_time_in_ms = redisDb.expires.get(key)
if expire_time_in_ms is None:
return -1
now_ms = get_current_unix_timestamp_in_ms()
return expire_time_in_ms - now_ms
```
## 过期键的判定
通过过期字典,程序可通过以下步骤来判定键是否过期:
1. 检查给定键是否存在于过期字典,如果存在,取得其过期时间
2. 检查当前UNIX时间戳是否大于其过期时间
# 9.5 过期键的删除策略
有三种不同的键删除策略:
| 策略 | 操作 | 优点 | 缺点 |
| ---- | ---------------------------------------- | ---------------- | -------------- |
| 定时删除 | 设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时立即执行删除操作。 | 对内存最友好,保证会尽快释放内存 | 对CPU时间不友好 |
| 惰性删除 | 每次从键空间获取键时,检查其是否过期,过期则删除;否则就返回该键。 | 对CPU时间最友好 | 对内存不友好 |
| 定期删除 | 每隔一段时间,对数据库进行一次检查,删除所有的过期键。 | 上述两种策略的整合和折中 | 难点在于确定删除的时长和频率 |
# 9.6 Redis的过期键删除策略
Redis服务器使用的是惰性删除和定期删除两种策略。
## 惰性删除的实现
惰性删除的策略由db.c/exipireIfNeeded函数实现所有读写数据库的Redis命令都会在执行前调用该函数。
![](img/chap9/img2.png)
## 定期删除的实现
定期删除的策略由redis.c/activeExpireCycle函数实现每当Redis服务器周期性操作redis.c/serverCron函数执行时该函数会被调用。它在规定时间内分多次遍历各个数据库检查过期时间并删除过期键。
```python
DEFAULT_DB_NUMBERS = 16
DEFAULT_KEY_NUMBERS = 20
current_db = 0
def activeExpireCycle():
if server.dbnum < DEFAUKT_DB_NUMBERS:
db_numbers = server.dbnum
else:
db_numbers = DEFAULT_DB_NUMBERS
for i in range(db_numbers):
if current_db == server.dbnum:
current_db = 0
redisDb = server.db[current_db]
current_db += 1
for j in range(DEFAULT_KEY_NUMBERS):
if redisDb.expires.size() == 0:
break
key_with_ttl = redisBb.expires.get_random_key()
if is_expired(key_with_ttl):
delete_key(key_with_ttl)
if reach_time_limit():
return
```
activeExpireCycle的工作模式总结如下
- 函数运行时,会从一定数量的数据库中取出一定数量的随机键检查并删除。
- 全局变量current\_db记录当前检查的进度并在下一次调用时接着处理上一次的进度。
- 随着activeExpireCycle的不断执行所有数据库都会被检查一遍这是current\_db重置为0再次开始新一轮动机检查。
# 9.7 AOF、RDB和复制功能对过期键的处理
## RDB文件生成和载入
执行SAVE或BGSAVE命令时会创建一个新的RDB文件已过期的键不会保存到RDB中。
在启动服务器时如果开启了RDB功能服务器会载入RDB文件
- 如果服务器以主服务器模式运行那么载入RDB时会检查文件中的键过期键会被忽略。
- 如果服务器以从服务器模式运行那么载入RDB时不管键是否过期一律载入。其后在主从服务器同步时从服务器的数据库就会被清空。
## AOF文件写入和重写
服务器以AOF持久化模式运行时如果某个键已过期但还没有被删除那么AOF文件不会因为这个过期键而产生任何影响。但过期键被删除后程序会向AOF文件追加一条DEL命令显式记录该键已被删除。
AOF重写过程中程序会对键进行检查已过期的键不会被保存到重写后的AOF文件中。
## 复制
当服务器处于复制模式下时,过期键删除动作由主服务器控制,这就保证了一致性:
- 主服务器删除一个过期键后显式向从服务器发送DEL命令
- 从服务器执行客户端发送的杜明令时,即时碰到过期键也不会删除,而是像初期未过期的键一样
- 从服务器接到主服务器的DEL命令后才会删除过期键
# 9.8 数据库通知
数据库通知是Redis 2.8新增加的功能,让客户端通过订阅可给定的频道或模式,来获取数据库中键的变化,以及数据库命令的执行情况。
“某个键执行了什么命令”的通知成为「键空间通知」。“某个命令被什么键执行了”是「键时间通知」。服务器配置的notify-keyspace-events选项决定了服务器发送通知的类型。
发送通知的功能由notify.h/notifyKeyspaceEvent函数实现的
```c
void notifyKeyspaceEvent(int type, char *event, int dbid);
```
伪代码如下:
```python
def notifyKeyspaceEvent(type, event, key, bdid):
if not (server.notify_keyspace_events & type):
return
# 发送键空间通知
if server.notify_keyspace_events & REDIS_NOTIFY_KEYSPACE:
# 将通知发送给频道 __keyspace@<dbid>__:<key>
chan = "_keyspace@{bdid}__:{key}".format(dbid_dbid, key=key)
pubsubPublishMessage(chan, event)
# 发送键时间通知
if server.notify_keyspace_events & REDIS_NOTIFY_KEYEVENT:
chan = "_keyspace@{bdid}__:{event}".format(dbid_dbid, event=event)
pubsubPublishMessage(chan, event)
pubsubPublishMessage(chan, key)
```
# 导航
[目录](README.md)
上一章:[8. 对象](ch8.md)
下一章:[10. RDB持久化](ch10.md)

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB