redis 知识补充
@@ -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指针然后找到相应的虚函数。
|
||||
* 当然这样子的过程相比静态多态而言,时间和空间上的开销都多了(这也是为什么内联函数为什么不能声明为虚函数,因为这和内联函数加快执行速度的初衷相矛盾)。
|
||||
|
||||
@@ -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编程)
|
||||
@@ -3,10 +3,11 @@
|
||||
> 完成昨天的任务。
|
||||
|
||||
- [x] 学习、复习图算法,动手实现所有的图算法。
|
||||
- [ ] 看完数据结构与算法的三本书!!!对相关的原理进行复习和总结。
|
||||
- [ ] ~~看完数据结构与算法的三本书!!!对相关的原理进行复习和总结。~~
|
||||
- [x] 学习机器学习的实现方案。毕设计划真正的开始执行。
|
||||
- [x] 关于字符串分割。字符串格式化方法的总结。转换成流,作为流对象处理。转换为容器。作为容器对象处理,使用泛型算法。
|
||||
|
||||
|
||||
## 收获
|
||||
|
||||
* 使用递归不能解决动态规划问题。适应为动态规划的子问题有重复。使用递归的方法。会导致重复计算的问题。
|
||||
|
||||
@@ -127,3 +127,9 @@
|
||||
5. 第八周:论文的阅读和复现工作。包括联邦学习和恶意软件机器学习。
|
||||
|
||||
## 收获
|
||||
|
||||
* 第六周(4.5-4.11)
|
||||
* 第七周(4.12-4.18)
|
||||
* 第八周(4.19-4.25)
|
||||
* 第九周(4.25-4.30)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## 计划
|
||||
|
||||
* 执行之前的计划
|
||||
> 执行之前的计划
|
||||
- [ ] 整理会议记录发到群里。
|
||||
|
||||
## 收获
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
* 项目概述:单人项目开发。
|
||||
* 主要工作:MFC开发界面、windows网络通信。
|
||||
* 主要成果:课设。
|
||||
|
||||
### ~~补充项目——TensorFlowIO优化~~
|
||||
|
||||
* 项目概述:分析TensorFlow源代码,对源代码进行修改重新编译。使用mmap方法,优化TensorFlow数据加载过程中的IO操作。
|
||||
@@ -108,3 +109,7 @@
|
||||
|
||||
## 5 个人能力和性格
|
||||
|
||||
1. C++后端开发、Java后端开发、Mysql数据库
|
||||
2. 学习能力较强。
|
||||
3. 乐观积极。热衷于开发
|
||||
|
||||
|
||||
96
工作日志/2021年4月8日-简历投递记录.md
Normal 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中至少一种编程语言;
|
||||
* 熟练使用MySQL,PostgreSQL、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/IP,HTTP等网络协议有很深的理解;
|
||||
* 或许,你享受底层技术,在kernel的源代码中纵横驰骋;
|
||||
* 或许,你并不熟悉C,C++,但是你不畏挑战,喜欢钻研,能够用你亮眼的成果证明自己超强的学习能力;
|
||||
* 或许,你参加过大学生数学建模竞赛,“挑战杯”,机器人足球比赛等;
|
||||
* 投递
|
||||
- [x] 简历投递
|
||||
- [x] 素质测评
|
||||
- [ ] 4.9号笔试
|
||||
|
||||
> 二期简历
|
||||
|
||||
-------------
|
||||
|
||||
## 快手
|
||||
|
||||
* 岗位
|
||||
* 技术要求
|
||||
* 投递
|
||||
## 华为实习
|
||||
|
||||
## 美团
|
||||
|
||||
|
||||
需要补充的能力:
|
||||
* Redis nosql非关系型数据库。
|
||||
* 分布式开发
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
14
工作日志/2021年4月9日-实习复习计划.md
Normal file
@@ -0,0 +1,14 @@
|
||||
## 计划
|
||||
|
||||
### **腾讯面试前复习**
|
||||
- 基础知识复习
|
||||
- [ ] 数据库
|
||||
- [ ] 计算机网络
|
||||
- [ ] 操作系统(最后一遍搞清楚同步通信、IO过程相关的问题提)
|
||||
- 腾讯要求复习
|
||||
- [x] nosql/redis非关系型数据库
|
||||
- [ ] Hadoop/spark 分布式数据处理(没时间了,等下一轮吧)
|
||||
- 腾讯面经复习
|
||||
- [ ] 针对腾讯的面试笔试问题进行复习
|
||||
|
||||
## 收获
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
单个用户线程:对于十万个用户同时访问服务器,有两种方式处理并发。
|
||||
|
||||
1. 为每个用户开一个新的用户现场,每个线程内部采用阻塞通信的方式,即同步通信,从数据库中取数据、与服务器通信等,直到得到结果,返回给用户。其中涉及多个用户线程。
|
||||
1. 为每个用户开一个新的用户线程,每个线程内部采用阻塞通信的方式,即同步通信,从数据库中取数据、与服务器通信等,直到得到结果,返回给用户。其中涉及多个用户线程。
|
||||
2. 只有一个用户线程,采用非阻塞通信的方式,即异步通信,通过事件驱动的方式实现并发。从数据库中取数据、与服务器通信或与其他进程通信,并不会阻塞线程的执行,每次数据获取完毕,通过事件的方式,调用用户进程,处理得到的数据,返回给用户。其中,只有一个用户进程。
|
||||
3. 对于事件驱动的方法:会存在一个事件队列,唯一的用户进程会不断地依次处理队列中的事件。所以不会存在冲突。有两种处理事件的方法:基于监听器的事件处理机制和基于回调的事件处理机制。
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
## 2.3 进程同步
|
||||
|
||||
## 1 进程同步的基本概念
|
||||
|
||||
> 进程同步指的是同步和互斥两种行为。
|
||||
### 两种制约关系
|
||||
> 针对两种制约关系,合作制约关系和互斥制约关系,需要通过同步机制实现。
|
||||
|
||||
- 直接制约关系(合作)。由于多个进程相互合作产生,使得进程有一定的先后执行关系。
|
||||
- 直接制约关系(合作/同步)。由于多个进程相互合作产生,使得进程有一定的先后执行关系。
|
||||
- 间接制约关系(互斥)。由于多个进程资源共享产生,多个进程在同一时刻只有一个进程能进入临界区。
|
||||
|
||||
### 临界资源和临界区
|
||||
|
||||
@@ -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 文件,基于文件的通信机制。
|
||||
* 向管道(共享文件)提供输入的发送进程(即写进程),以字符流形式将大量的数据送入管道;而接受管道输出的接收进程(即读进程),则从管道中接收(读)数据。由于发送进程和接收进程是利用管道进行通信的,故又称为管道通信。
|
||||
* 有部分博客说。管道通信也属于消息通信中的直接通信的一种。
|
||||
### 管道实现
|
||||
* 它具有以下限制:
|
||||
|
||||
- 只支持半双工通信(单向交替传输);
|
||||
- 只能在父子进程或者兄弟进程中使用。
|
||||

|
||||
|
||||
|
||||
### 函数接口
|
||||
* 管道是通过调用 pipe 函数创建的,fd[0] 用于读,fd[1] 用于写。
|
||||
|
||||
```c
|
||||
@@ -369,36 +372,31 @@ int main(int argc, char *argv[])
|
||||
int pipe(int fd[2]);
|
||||
```
|
||||
|
||||
* 它具有以下限制:
|
||||
|
||||
- 只支持半双工通信(单向交替传输);
|
||||
- 只能在父子进程或者兄弟进程中使用。
|
||||
|
||||

|
||||
|
||||
### 编程实现
|
||||
```C
|
||||
```
|
||||
|
||||
## 5 管道通信机制的实例——FIFO命名管道通信
|
||||
|
||||
### 通信原理
|
||||
* 也称为命名管道,去除了管道只能在父子进程中使用的限制。
|
||||
* FIFO 常用于客户-服务器应用程序中,FIFO 用作汇聚点,在客户进程和服务器进程之间传递数据。
|
||||

|
||||
|
||||
### 函数接口
|
||||
|
||||
```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 用作汇聚点,在客户进程和服务器进程之间传递数据。
|
||||
|
||||

|
||||
|
||||
### 编程实现
|
||||
```C
|
||||
```
|
||||
## 6 高级管道通信
|
||||
|
||||
### 通信原理
|
||||
### 函数接口
|
||||
### 编程实现
|
||||
```C
|
||||
```
|
||||
|
||||
231
操作系统/附录3 互斥量、信号量、条件变量.md
Normal 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, 出错则返回错误编号.
|
||||
* 这两个函数用于通知线程条件已经满足. 调用这两个函数, 也称向线程或条件发送信号. 必须注意, 一定要在改变条件状态以后再给线程发送信号.
|
||||
|
||||
|
||||
@@ -117,3 +117,11 @@
|
||||
|
||||
## 15 五种IO模型和epoll机制
|
||||
|
||||
|
||||
## 16 终极三连问
|
||||
1. 进程/线程的同步和通信原理、
|
||||
2. 进程/线程的同步和通信代码实现(并行编程)、
|
||||
3. 设备IO的实现原理、
|
||||
4. 设备IO的代码实现、
|
||||
5. 网络通信的基本原理、
|
||||
6. 网络通信的代码实现(socket编程)(网络编程)、
|
||||
795
数据库/Redis/0redis.md
Normal 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
@@ -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)
|
||||
|
||||
77
数据库/Redis/Redis持久化.md
Normal 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持久化),
|
||||
* 另外一种是AOF(append only file)持久化(原理是将Reids的操作日志以追加的方式写入文件)。
|
||||
* 那么这两种持久化方式有什么区别呢,改如何选择呢?网上看了大多数都是介绍这两种方式怎么配置,怎么使用,就是没有介绍二者的区别,在什么应用场景下使用。
|
||||
|
||||
## 2 二者的区别
|
||||
* RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
|
||||

|
||||
|
||||
* AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。
|
||||

|
||||
|
||||
## 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
@@ -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文件:
|
||||
|
||||

|
||||
|
||||
Redis自带的文件检查工具是redis-check-dump。
|
||||
|
||||
# 导航
|
||||
|
||||
[目录](README.md)
|
||||
|
||||
上一章:[9. 数据库](ch9.md)
|
||||
|
||||
下一章:[11. AOF持久化](ch11.md)
|
||||
|
||||
55
数据库/Redis/ch11.md
Normal file
@@ -0,0 +1,55 @@
|
||||
AOF(Append 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
@@ -0,0 +1,152 @@
|
||||
Redis服务器是一个事件驱动程序,需要处理以下两类事件:
|
||||
|
||||
- 文件事件(file event):Redis服务器通过socket与客户端连接,文件事件就是对套接字操作的对象。服务器与客户端的通信会产生相应的文件事件,服务器监听并处理这些事件来完成一系列的网络通信操作。
|
||||
- 时间事件(time event):Redis服务器的一些操作(如`serverCron`函数)需要在特定时间点执行,时间事件就是对这类定时任务的抽象。
|
||||
|
||||
# 12. 1 文件事件
|
||||
|
||||
Redis基于Reactor模式开发了自己的网络事件处理器,称为『文件事件处理器』,文件事件处理器以单线程方式运行。
|
||||
|
||||

|
||||
|
||||
文件事件处理器的四个组成部分:
|
||||
|
||||
- 套接字。
|
||||
|
||||
当被监听的套接字准备好执行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
@@ -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
@@ -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,表明服务器没有再做持久化操作,则:
|
||||
|
||||

|
||||
|
||||
## 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
@@ -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文件发送给slave,slave接收并载入这个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)
|
||||
- 服务器的运行ID(run ID)
|
||||
|
||||
## 复制偏移量
|
||||
|
||||
master和slave分别维护一个复制偏移量:
|
||||
|
||||
- master每次向slave传播N个字节的数据时,就将自己的复制偏移量+N。
|
||||
- slave每次收到master的N个字节数据时,就将自己的复制偏移量+N。
|
||||
|
||||
对比两者的复制偏移量,就知道它们是否处于一致状态。
|
||||
|
||||
## 复制积压缓冲区
|
||||
|
||||
复制积压缓冲区是master维护的一个固定长度的FIFO队列,默认大小为1MB。当服务器进行命令传播时,不仅会将命令发送给所有slave,还会入队到积压缓冲区。因此,积压缓冲区保存了最近被传播的写命令,且为队列中的每个字节记录相应的复制偏移量。
|
||||
|
||||
slave重连上master时,slave通过`PSYNC`将自己的复制偏移量offset发送给master,master会根据这个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`来判断执行哪种同步。
|
||||
|
||||

|
||||
|
||||
# 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`命令,那么:
|
||||
|
||||

|
||||
|
||||
## 步骤4:身份验证
|
||||
|
||||
收到master的“PONG”回复后,slave要检查自己的`masterauth`选项决定是否进行身份验证。如果需要验证,slave会向master发送一条`AUTH`命令,参数为`masterauth`选项的值,接下来:
|
||||

|
||||
|
||||
## 步骤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将自己执行写命令发送给slave,slave接到后就执行,这样两者的状态就一直保持一致了。
|
||||
|
||||
# 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
@@ -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
@@ -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`之后,双方握手完成。
|
||||
|
||||

|
||||
|
||||
# 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指派给目标节点,这一指派信息通过消息传送至整个集群。
|
||||
|
||||

|
||||
|
||||
# 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标记为已下线(FAIL)时,A将集群广播关于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
@@ -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
@@ -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
@@ -0,0 +1,99 @@
|
||||
Redis里,C字符串只会作为字符串字面量用在一些无需对字符串值进行修改的地方,比如打印日志。Redis构建了 简单动态字符串(simple dynamic string,SDS)来表示字符串值。
|
||||
|
||||
在Redis里,包含字符串值的键值对在底层都是由SDS实现的。除此之外,SDS还被用作缓冲区:AOF缓冲区,客户端状态中的输入缓冲区。
|
||||
|
||||
# 2.1 SDS的定义
|
||||
|
||||
每个sds.h/sdshdr结构表示一个SDS值:
|
||||
|
||||
```c
|
||||
struct sdshdr {
|
||||
// 记录buf数组中已使用字节的数量
|
||||
// 等于SDS所保存字符串的长度
|
||||
int len;
|
||||
|
||||
// 记录buf数组中未使用字节的数量
|
||||
int free;
|
||||
|
||||
// 字节数组,用于保存字符串
|
||||
char buf[];
|
||||
}
|
||||
```
|
||||
|
||||
示例:
|
||||
|
||||

|
||||
|
||||
SDS遵循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
@@ -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`函数将结果作为函数返回值返回给脚本的调用者。
|
||||
|
||||

|
||||
|
||||
## `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
@@ -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`结构。
|
||||
|
||||

|
||||
|
||||
2. 遍历数组,将每个元素的`obj`指针指向numbers列表的各个项,构成一一对应关系。
|
||||
|
||||

|
||||
|
||||
3. 遍历数组,将各个`obj`指针所指向的列表项转换为一个`double`类型的浮点数,并将这个浮点数保存在相应数组项的`u.score`属性中。
|
||||
|
||||

|
||||
|
||||
4. 根据数组项`u.score`的值,对数组进行数字值排序。
|
||||
|
||||

|
||||
|
||||
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
@@ -0,0 +1,107 @@
|
||||
Redis提供了`SETBIT`、`GETBIT`、`BITCOUNT`、`BITOP`四个命令用于处理二进制位数组。
|
||||
|
||||
- `SETBIT`,为位数组指定偏移量上的二进制位设置值0或1。
|
||||
- `GETBIT`,获取位数组指定偏移量上的二进制位的值。
|
||||
- `BITCOUNT`,统计位数组中1的个数。
|
||||
- `BITOP`,既可以对多个位数组进行按位与、按位或、按位异或运算,也可以对给定位数组取反。
|
||||
|
||||
# 22.1 位数组的表示
|
||||
|
||||
Redis使用字符串来表示位数组,并使用SDS结构的操作函数来处理位数组。
|
||||
|
||||

|
||||
|
||||
- `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]的值 2,2 就是 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
@@ -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
@@ -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
@@ -0,0 +1,67 @@
|
||||
Redis构建了自己的链表实现。列表键的底层实现之一就是链表。发布、订阅、慢查询、监视器都用到了链表。Redis服务器还用链表保存多个客户端的状态信息,以及构建客户端输出缓冲区。
|
||||
|
||||
# 3.1 链表和链表节点的实现
|
||||
|
||||
链表节点用adlist.h/listNode结构来表示
|
||||
|
||||
```c
|
||||
typedef struct listNode {
|
||||
struct listNode *prev;
|
||||
struct listNode *next;
|
||||
void *value;
|
||||
} listNode;
|
||||
```
|
||||
|
||||

|
||||
|
||||
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;
|
||||
```
|
||||
|
||||

|
||||
|
||||
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
@@ -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;
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
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指针。
|
||||
|
||||

|
||||
|
||||
# 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
@@ -0,0 +1,85 @@
|
||||
跳跃表是一种**有序数据结构**,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问的目的。跳跃表支持平均*O(logN)*、最坏*O(N)*的查找,还可以通过顺序性操作来批量处理节点。
|
||||
|
||||
Redis使用跳跃表作为有序集合键的底层实现之一,如果有序集合包含的元素数量较多,或者有序集合中元素的成员是比较长的字符串时,Redis使用跳跃表来实现有序集合键。
|
||||
|
||||
在集群节点中,跳跃表也被Redis用作内部数据结构。
|
||||
|
||||
# 5.1 跳跃表的实现
|
||||
|
||||
Redis的跳跃表由redis.h/zskiplistNode和redis.h/zskiplist两个结构定义,其中zskiplistNode代表跳跃表节点,zskiplist保存跳跃表节点的相关信息,比如节点数量、以及指向表头/表尾结点的指针等。
|
||||
|
||||

|
||||
|
||||
```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
@@ -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;
|
||||
```
|
||||
|
||||

|
||||
|
||||
上图中,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
@@ -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\_MAX(65535)时,这个属性的值就是节点的数量。如果等于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
@@ -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. 先添加的键值对会被放在表头,后添加的在表尾。
|
||||
|
||||

|
||||
|
||||
使用hashtable时,哈希对象中的每个键值对都使用一个字典键值对来保存:
|
||||
|
||||
- 字典的每个键都是一个字符串对象,对象中保存了键值对的键。
|
||||
- 字典的每个值都是一个字符串独显,对象中保存了键值对的值。
|
||||
|
||||

|
||||
|
||||
## 编码转换
|
||||
|
||||
当哈希对象同时满足以下两个条件时,使用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 | 遍历ziplist,ziplistGet返回所有的键和值 | 遍历字典,dictGetKey返回键,dictGetVal返回值 |
|
||||
|
||||
# 8.5 集合对象
|
||||
|
||||
集合对象的编码可以使intset或hashtable。
|
||||
|
||||
1. inset编码,集合对象的所有元素都被保存在整数集合中。
|
||||
|
||||

|
||||
|
||||
2. hashtable编码,字典的每个键都是一个字符串对象,每个字符串对象都包含了一个集合元素,字典的值全部为NULL。
|
||||
|
||||

|
||||
|
||||
## 编码的转换
|
||||
|
||||
当集合对象同时满足一下两个条件时,使用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命令的执行过程:
|
||||
|
||||

|
||||
|
||||
# 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
@@ -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;
|
||||
```
|
||||
|
||||
键空间和用户所见的数据库是直接对应的:
|
||||
|
||||
- 键空间的键也就是数据库的键。每个键都是一个字符串对象。
|
||||
- 键空间的值也是数据库的值。每个值可以使字符串对象、列表对象、哈希表对象、集合对象、有序集合对象。
|
||||
|
||||

|
||||
|
||||
所有针对数据库的操作,实际上都是通过键空间字典来实现。
|
||||
|
||||
## 添加新键
|
||||
|
||||
添加一个新键值对到数据库,就是将新键值对添加到键空间字典中。
|
||||
|
||||
## 删除键
|
||||
|
||||
删除数据库中的一个键,就是在键空间中删除键所对应的键值对对象。
|
||||
|
||||
## 更新键
|
||||
|
||||
更新数据库的一个键,就是对键空间里键所对应的值对象进行更新。根据值对象类型的不同,更新的具体方法也不同。
|
||||
|
||||
## 对键取值
|
||||
|
||||
对一个数据库键取值,就是在键空间中取出键所对应的值对象。
|
||||
|
||||
## 读写键空间时的维护操作
|
||||
|
||||
当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;
|
||||
```
|
||||
|
||||

|
||||
|
||||
`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命令都会在执行前调用该函数。
|
||||
|
||||

|
||||
|
||||
## 定期删除的实现
|
||||
|
||||
定期删除的策略由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)
|
||||
BIN
数据库/Redis/image/2021-04-07-23-24-02.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
数据库/Redis/image/2021-04-07-23-24-24.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
数据库/Redis/img/chap10/img0.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
数据库/Redis/img/chap12/img0.png
Normal file
|
After Width: | Height: | Size: 152 KiB |
BIN
数据库/Redis/img/chap14/img0.png
Normal file
|
After Width: | Height: | Size: 173 KiB |
BIN
数据库/Redis/img/chap15/img0.png
Normal file
|
After Width: | Height: | Size: 157 KiB |
BIN
数据库/Redis/img/chap15/img1.png
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
数据库/Redis/img/chap15/img2.png
Normal file
|
After Width: | Height: | Size: 167 KiB |
BIN
数据库/Redis/img/chap17/img0.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
数据库/Redis/img/chap17/img1.png
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
数据库/Redis/img/chap2/sds-example.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
数据库/Redis/img/chap20/img0.png
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
数据库/Redis/img/chap21/img0.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
数据库/Redis/img/chap21/img1.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
数据库/Redis/img/chap21/img2.png
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
数据库/Redis/img/chap21/img3.png
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
数据库/Redis/img/chap22/img0.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
数据库/Redis/img/chap3/list.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
数据库/Redis/img/chap3/listNode.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
数据库/Redis/img/chap4/collision.png
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
数据库/Redis/img/chap4/k1-k0.png
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
数据库/Redis/img/chap5/skiplist.png
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
数据库/Redis/img/chap6/five-int16.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
数据库/Redis/img/chap8/img0.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
数据库/Redis/img/chap8/img1.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
数据库/Redis/img/chap8/img2.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
数据库/Redis/img/chap8/img3.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
数据库/Redis/img/chap8/img4.png
Normal file
|
After Width: | Height: | Size: 135 KiB |
BIN
数据库/Redis/img/chap9/img0.png
Normal file
|
After Width: | Height: | Size: 209 KiB |
BIN
数据库/Redis/img/chap9/img1.png
Normal file
|
After Width: | Height: | Size: 278 KiB |
BIN
数据库/Redis/img/chap9/img2.png
Normal file
|
After Width: | Height: | Size: 80 KiB |