导图社区 Cpp面试知识合集
这是一篇关于Cpp面试知识合集的思维导图,包含C++基础,STL源码,C++对象模型等内容。
编辑于2022-05-14 09:59:44面试知识合集
C++
C++ 基础
深浅拷贝
什么是深拷贝?什么是浅拷贝? 1.浅拷贝: 对于内置类型,或者类的数据成员中不包含指针的这些类型,使用系统提供的默认拷贝构造函数就可以正常运行出结果了。 Object A; Object B = A; // Object B(A); 浅拷贝就是将要对象A在内存中的数据按位复制给对象B所在的内存,因为默认的拷贝构造函数一般就够用了,所以就没必要显示地定义拷贝构造函数。 2.深拷贝: 对于数据成员含有指针的类,如果在拷贝的时候依然使用浅拷贝,那么就会出现问题:使用浅拷贝,实际上是直接把对象A的指针值复制给对象B对应的指针值,那么这两个对象的指针都会指向同一块内存,虽然在编译的时候会通过,但在程序运行时会出现一些问题:(1)可能之前B的指针指向World,A的指针指向Hello,但是将A的指针值复制给B对应的指针,那么B之前指向的World这块内存就没有指针指向了,会出现“内存泄露”;(2)由于两个指针指向同一块内存,当A的指针把内存释放后,B的指针就成为了野指针; 所以,为了避免这些现象,对于那些数据成员含有指针的类就应该使用深拷贝,就是实际的在计算机中开辟一块内存用于存放复制的对象数据。 一般就是在类中显示地定义拷贝构造函数,重载赋值运算符,析构函数,而不是使用默认的
4 种强制类型转换
4种类型转换运算符
语法格式: xxx_cast<newType>(data) 例如: double scores = 95.5; int n = static_cast<int>(scores);
static_cast
静态转换: 在编译期间进行转换,如果转换失败,会抛出一个编译错误。它主要有如下几种用法: (1)用于基本数据类型之间的转换,如把 int 转换成 char。但是,不能把两个具体类型指针之间进行转换,例如:int*转double* (2)把 void 指针转换成目标类型的指针(不安全!!) (3) 把任何类型的表达式转换成void类型; (4)不能进行int和指针之间的转换,例如:int 和 int* (5)向上转型:只要待转换的类型之间存在继承关系并且基类有虚函数,就可以将子类的指针或引用转换为父类的类型;
dynamic_cast
动态转换: 用于在类的继承层次之间进行向下类型的转换向下转型。 因为dynamic_cast在RTTI操作时是在程序运行期间进行类型转换,所以要求基类必须包含虚函数。 转换的类型和被转换的类型都必须是指针类型或引用类型。如果转换失败,对于指针,返回NULL;对于引用,将抛出std::bad_cast异常 向下转型:用运行时信息(RTTI)来进行类型安全检查,,并且必须含有虚函数,因为dynamic_cast使用了存储在虚表中的信息来判断实际的类型。 继承顺序为:A --> B --> C --> D A *pa = new A(); B *pb; C *pc; pb = dynamic_cast<B*>(pa); //向下转型失败 pa = new D(); //向上转型都是允许的 pb = dynamic_cast<B*>(pa); //向下转型成功 dynamic_cast 会在程序运行过程中遍历继承链,如果途中遇到了要转换的目标类型,那么就能够转换成功,如果直到继承链的顶点(最顶层的基类)还没有遇到要转换的目标类型,那么就转换失败。
reinterpret_cast
重新解释转换: 就是对二进制数据重新解释,不会对数据进行调整,非常简单除暴,所以风险很大。 算是对static_cast的一种补充,可以完成static_cast不能完成的转换,例如:两个不同类型指针之间的转换,int和指针之间的转换 【示例】 class A{ public: A(int a = 0, int b = 0): m_a(a), m_b(b){} private: int m_a; int m_b; }; void main(){ // 1. 不同类型指针之间的转换:将 char* 转换为 float* char str[] = "http://c.biancheng.net"; float *p1 = reinterpret_cast<float*>(str); cout<<*p1<<endl; // 2. 将 int 和指针之间的转换:将 int 转换为 int* int *p = reinterpret_cast<int*>(100); //将 A* 转换为 int* p = reinterpret_cast<int*>(new A(25, 96)); cout<<*p<<endl; } 【结果】 3.0262e+29 25 // 直接访问了类的私有数据,类的封装性遭到破坏 这样的转换方式不到万不得已的时候不要使用
const_cast
用来去掉表达式的 const 修饰或 volatile 修饰,就是用来将 const / volatile 类型转换为非 const / volatile 类型。 例如: const int n = 100; int *p = const_cast<int*>(&n); // 获取 n 的地址,类型为const int*,然后进行类型转换 *p = 234; cout<<"n = "<<n<<endl; // n = 100 cout<<"*p = "<<*p<<endl; // *p = 234 结果不同的原因: 因为 C++ 对常量的处理更像是编译时期的#define,是一个值替换的过程,代码中所有使用 n 的地方在编译期间就被替换成了 100。即使程序在运行期间修改 n 的值,也不会影响 cout 语句了。
常见关键字
static & const
static
参考链接: https://blog.csdn.net/majianfei1023/article/details/45290467 https://blog.csdn.net/ypshowm/article/details/89030194
静态 变量
静态 全局变量
static int i = 1; // note:3 // int i = 1; // note:4 int foo( ){ i += 1; return i; } 1. 静态全局变量: 定义在函数体外,用于修饰全局变量,表示该变量只在本文件可见,用用就是实现了 文件隔离 2. note:3 和 note:4 的差异: 无论调用函数几次,两种结果都是一样的。也就是说在本文件内调用他们是完全相同的。那么他们的区别是什么呢? 文件隔离!本文件使用 static 修饰的变量不能被其他文件使用(其他文件中定义了相同的名字也不影响) 假设有另一个文件: // 文件1:file a.c // static int n = 15; // note:5 int n = 15; // note:6 // 文件2:file b.c extern int n; void fn( ){ n++; printf("after: %d\n",n); } void main(){ printf("before: %d\n",n); fn( ); } 如果使用非静态全局变量 note:6,输出为: before: 15 after: 16 文件2 通过extern使用了 a.c 定义的全局变量。 那么我们改成使用 note:5,也就是使用静态全局变量呢? gcc a.c b.c -o output.out 会出现类似undeference to "n"的报错,它是找不到n的,因为static进行了文件隔离,你是没办法访问a.c定义的静态全局变量的,当然你用 #include "a.c",那就不一样了。 3. 静态全局变量的特点: 静态全局变量不能被其它文件所用(全局变量可以); 其它文件中可以定义相同名字的变量,不会发生冲突(自然了,因为static隔离了文件,其它文件使用相同的名字的变量,也跟它没关系了);
静态 局部变量
int foo( ){ static int i = 1; // note:1 // int i = 1; // note:2 i += 1; return i; } (建议先弄清楚最下面的C++内存分布) 1. 修饰局部变量 特点:(用于修饰函数体内修饰变量): (1)这种变量保存在内存的全局数据区 (2)这种变量的生存期长于函数,变量的值不会因为函数终止而丢失,随着第一次函数的调用而初始化,且之初始化一次,却不随着函数的调用结束而销毁(栈区数据就是随着函数的结束而销毁),第二次调用这个函数就会直接跳过 (3)一般在声明处初始化,如果没有显示初始化,会被程序自动初始化为 0(在栈内的局部变量就不会被初始化) (4)始终在全局数据区,直到程序运行结束,但作用域仍为局部作用域,不能在函数外部使用 2. 和局部变量的区别: 那么它跟定义一个全局变量有什么区别呢,同样是初始化一次,连续调用foo( )的结果是一样的? 使用全局变量:变量不属于函数本身,不受函数的控制,给程序的维护带来不便; 使用静态局部变量:可以解决这个问题,静态局部变量保存在全局数据区,而不是保存在栈中,每次的值保持到下一次调用,直到下次赋新值,变量是受到函数控制的。 总结:一个不被函数控制,一个能被函数控制 C++内存分布: 栈区:由编译器自动分配释放,像局部变量,函数参数,都是在栈区。会随着作用域退出而释放空间。 堆区:程序员分配并释放的区域,像malloc(c),new(c++) 全局数据区(静态区): 全局变量 和 静态变量 在一块区域 初始化的 全局变量 和 静态变量 在一块区域 未初始化的 全局变量 和 静态变量 在相邻的另一块区域 程序结束释放。 代码区
静态 函数
修饰函数:(和静态变量的作用类似) 被static修饰的函数只在本文件中调用,不能被其他文件调用 其他文件中可以定义相同名字的函数,不会发生冲突 (下面看不看都行) //file a.c #include <stdio.h> void fn() { printf("this is non-static func in a"); } //file b.c #include <stdio.h> extern void fn(); //我们用extern声明其他文件的fn(),供本文件使用。 void main() { fn(); } 可以正常输出:this is non-static func in a。 当给void fn()加上static的关键字之后呢? undefined reference to "fn".
静态 类的成员
类的 静态 数据成员
修饰类的数据成员: 该数据成员不属于类的任何一个对象,而是属于类本身,供全体对象使用。 静态数据成员只分配一次内存,存储在全局数据区,在定义时就要分配空间,不能在类声明中定义。
类的 静态 成员函数
修饰类的成员函数: 用 static 修饰的类成员函数不能访问类的非静态成员,所以,类的静态函数只能访问它的参数,类的静态成员 和 全局变量。 因为对象知道你,而你不认识对象,所以对象的函数可以调用你,但你不能调用对象的东西。 静态成员之间可以相互访问,包括静态成员函数访问静态数据成员和访问静态成员函数; 非静态成员函数可以任意地访问静态成员函数和静态数据成员; 静态成员函数不能访问非静态成员函数和非静态数据成员; (如何调用)调用静态成员函数,可以用成员访问操作符(.)和(->)为一个类的对象或指向类对象的指针调用静态成员函数,也可以用类名::函数名调用(因为他本来就是属于类的,用类名调用很正常) (解释前三条)因为静态是属于类的,它是不知道你创建了10个还是100个对象,所以它对你对象的函数或者数据是一无所知的,所以它没办法调用,而反过来,你创建的对象是对类一清二楚的(不然你怎么从它那里实例化呢),所以你是可以调用类函数和类成员的
const
const 为常量修饰符,用来告诉编译器该变量或对象的值是不可修改的。 (const 对象创建时就应该初始化,并且之后值不会发生改变)
修饰 变量
1. 修饰一般常量及数组 对于基本数据类型,const 的位置放在类型前后的作用都是一样的 const int a=10; int const a=10; const int arr[3]={1,2,3}; int const arr[3]={1,2,3}; 2. 修饰 指针 及 引用 (1)修饰指针 const 位于 星号* 左侧,指针指向的对象是常量 const 位于 星号* 右侧,指针本身是常量 // 指针指向的 对象是常量(就是不同通过指针改变变量) const int* p = &a; int const *p = &a; // 指针本身是常量 int* const p = &a; // 指针指向对象和指针本身都是常量 const int* const p = &a; (2)修饰引用 const 关键字放在引用符号前或者后,效果都是一样的,不能通过引用对引用的值进行修改 int const &a = x; const int &a = x; int &const a = x; 最后一条是不合法的,因为把一个const 对象被一个非const 引用绑定,那么就可以通过引用修改了,这样是不合法的。 const int a = 1; cosnt int &ra = a; int &ra2 = a; // 错误 int b = 2; int &rb = b; const int &rb2 = b;
修饰 函数
你可以把一个非const 的东西赋值给 const 对象,但是不能把 const 对象赋值给 非const 对象,因为这个非const 可以修改变量,这样就对 const 对象不合法了。 const int& fun ( int& a ); // 修饰返回值 int& fun ( const int& a ); // 修饰形参 int& fun ( int& a ) const{ } // const 成员函数
修饰 类的成员
const 对象
const 对象的数据成员不可以修改 const 对象只能访问 const 成员函数 普通成员函数可以访问 非const对象 的普通数据成员,const数据成员,但不可以访问 const对象的任意数据成员
成员函数
1. const 函数 访问成员变量:const 成员函数可以访问类中的所有成员变量,但不能修改这些变量的值,主要作用是:保护数据而设置的。(检查发生在编译时) 访问成员函数:const 成员函数不能调用 非const成员函数,因为 非const成员函数可以修改成员变量 const 成员函数需要在声明和定义时都在函数头部的结尾加上 const 关键字,否则,就是两个不同的函数(函数签名不同)。 class Student{ public: // ... int getAge( ) const ; // const 函数 private: int age; }; (如果成员函数同时具有 const 和 non-const 两个版本的话, const 对象只能调用const成员函数, non-const 对象只能调用 non-const 成员函数) 2. const 参数
成员函数能同时用 static 和 const 进行修饰?
不可以, static 修饰的函数表示该函数是属于类的,而不是属于某一个对象的,没有this指针 const 修饰的函数表示该函数不能改变this中的内容,会有一个隐含的const this指针 两者是冲突矛盾的。
C++ 三大特性
封装
继承
多态
概念
静态多态 与 动态多态
多态分为静态多态和动态多态: 静态多态:通过 重载 实现,在程序编译时就可以确定调用哪个函数,编译器根据函数名、参数类型以及个数来判断重载函数 动态多态:通过 虚函数 实现,在程序运行时才能决定操作的对象,也就是动态绑定,可以用基类指针指向派生类对象,执行虚函数时根据动态类型确定调用哪一个函数。
动态多态:虚函数
使用虚函数,就是用基类的指针指向派生类的实例,然后通过基类的指针调用派生类类的成员函数。这种技术可以让基类的指针有“多种形态”,是一种泛型技术,就是试图使用不变的代码来实现可变的算法。
虚函数的实现
虚函数是通过虚函数表实现的 一个类只要有虚函数,那么,就会有一个虚函数表,虚函数是一个存储虚成员函数指针的数据结构,但是类不会显示的存放这个虚函数,而是通过保存指向这个虚函数表的指针来调用虚函数的。 当派生类继承基类时,这个虚函数表其实也会被派生类继承,但是,派生类和基类的虚函数表并不是在同一张表中,派生类继承的是基类的虚函数表指针,所以,派生类就有了两个虚函数表的指针。 并且派生类还应该对基类的虚函数进行重写覆盖
析构函数 虚函数
(1)为什么父类的析构函数必须是虚函数? 当我们动态申请一个子类对象时,使用基类指针指向该对象,如果不用虚函数,子类的析构函数不能得到调用,那么,子类申请的内存就不会被释放,造成了内存泄露。所以,为了在释放基类指针时可以释放掉子类的空间,防止内存泄漏. (2)为什么C++默认的析构函数不是虚函数? 因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。
构造函数 虚函数
为什么构造函数不能是虚函数? (1)从存储角度:如果构造函数是虚函数,那么需要虚表来调用,那么对象就需要使用内存空间存放指向这个虚表的指针,但是由于构造函数没有运行,所以对象也还没实例化,对象的内存空间还没分配,对象中也就没有这个虚表指针,也就找不到虚表,所以也就没法调用构造函数,造成了死循环。 构造函数 要用 虚表,虚表指针 需要 内存存放,内存 需要 构造函数来分配 https://blog.csdn.net/salmonwilliam/article/details/114259314
内联函数 inline 虚函数
我在网上看的,内联函数可不可以是虚函数,两种观点都有,但是主流观点说: 虚函数不可以内联的,因为虚函数是在运行期确定要调用的函数,而内联呢,是在编译期的时候进行代码展开,两者冲突,所以没有一起使用的做法。 但是另一种观点说: 虚函数是可以内联的,函数内联只是对编译器的一种请求,但是否真正要进行内联要看编译器的处理,当把虚函数定义成内联时,编译器可以不去响应这种内联请求,直接把这个函数当做普通虚函数处理。 我感觉这两种说法都有些道理,不知道您怎么看这个问题。
静态成员函数 static 虚函数
为何static成员函数不能为virtual 1. static成员不属于任何类对象或类实例,所以即使给此函数加上virutal也是没有任何意义的。 2. 静态与非静态成员函数之间有一个主要的区别。那就是静态成员函数没有this指针。 虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它,因为它是类的一个成员,并且vptr指向保存虚函数地址的vtable. 对于静态成员函数,它没有this指针,所以无法访问vptr. 这就是为何static函数不能为virtual. 虚函数的调用关系:this -> vptr -> vtable ->virtual function
纯虚函数 虚函数
(1)声明和实现不同: 纯虚函数没有函数体,所以不能够实现;但是虚函数必须要实现 virtual void funtion() { 函数体 } virtual void funtion()=0; (2)有纯虚函数的类为抽象类,不可以实例化对象,需要派生类实现; 但是虚函数可以实例化对象,而且,虚函数只在派生类中有同名函数时才会被覆盖 纯虚函数引入原因: (1)为了方便使用多态特性 (2)在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔 雀等子类,但动物本身生成对象明显不合常理。 为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;)。若要使派生类为非抽象类,则编译器要求在派生类中,必须对纯虚函数予以重载以实现多态性。同时含有纯虚函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。 例如,绘画程序中,shape作为一个基类可以派生出圆形、矩形、正方形、梯形等, 如果我要求面积总和的话,那么会可以使用一个 shape * 的数组,只要依次调用派生类的area()函数了。如果不用接口就没法定义成数组,因为既可以是circle ,也可以是square ,而且以后还可能加上rectangle,等等.
构造函数和析构函数可以调用虚构函数吗?
可以,但不提倡。 如果调用了,也就没有了动态绑定的效果,父类构造函数中调用的仍然是父类版本的函数,子类中调用的仍然是子类版本的函数。 原因: (1)不要在构造函数中调用虚函数的原因:因为父类对象会在子类之前进行构造,此时子类部分的数据成员还未初始化, 因此调用子类的虚函数是不安全的,故而C++不会进行动态绑定。 (2)不要在析构函数中调用虚函数的原因:析构函数是用来销毁一个对象的,在销毁一个对象时,先调用子类的析构函数,然后再调用基类的析构函数。所以在调用基类的析构函数时,派生类对象的数据成员已经“销毁”,这个时再调用子类的虚函数已经没有意义了。 (effictive c++第九条,参考链接:https://www.cnblogs.com/sylar5/p/11523992.html)
词类辨析
重载 & 重写
函数的重写 / 重载(Overriding)和重载(Overloading0是多态性的不同表现。 重写Overriding是父类与子类之间多态性的一种表现,重载Overloading是一个类中多态性的一种表现。 如果在子类中定义某方法与其父类有相同的名称和参数,我们说该方法被重写 (Overriding)。子类的对象使用这个方法时,将调用子类中的定义,对它而言,父类中的定义如同被“屏蔽”了,而且如果子类的方法名和参数类型和个数都和父类相同,那么子类的返回值类型必须和父类的相同;如果在一个类中定义了多个同名的方法,它们或有不同的参数个数或有不同的参数类型,则称为方法的重载(Overloading)。Overloading的方法是可以改变返回值的类型。也就是说,重载的返回值类型可以相同也可以不同。
STL 源码
容器
有序容器
vector
介绍下 vector vector 是有序容器,是在堆中分配一段连续的内存空间来存放元素,支持随机存取和顺序存取。简单说,vector 其实就是一个动态增长的数组,随着元素的加入,vector 的内部机制会自动进行空间的扩充以容纳新元素。 vector 的实现原理 vector 内部有三个指针变量: start:指向第一个元素 finish:指向最后一个元素的下一位置 end_of_storage:分配空间的最后一个位置 通过这三个指针就可以得到 vector 空间的使用情况 finish - start:目前已使用的内存空间 end_of_storage - start:给 vector 分配的空间 end_of_storage - finish:目前 vector 中空闲的空间 finish == start:vector 为空 扩容机制 vector 动态增加时,并不是在原空间之后继续提供新空间(因为无法保证原空间之后还有可供配置的空间),而是以原大小空间的两倍在另一个地方重新开辟空间,之后再把原空间的元素拷贝过来,然后再构造新的元素至于尾部,并释放原空间。 为什么是进行 2倍 空间增长? 其实不同编译器,空间的扩容机制是不同的,Visual Stdio 下是 1.5 倍,但是在 GCC 下是 2 倍。 扩容的倍数也是根据空间和时间进行权衡的: 空间分配多,平均时间复杂度就低(因为分配的空间可能足够用,之后就不用在分配了),但是可能导致浪费的空间也就多了些;
list
forward_list
deque & stack
无序容器
set
map
关联容器
unordered_map
unordered_set
算法
适配器
C++11 新特性
auto 和 decltype
1.auto:根据表达式推导出其数据类型并用表达式的值初始化变量 auto x = 5; // 5 是 int 类型,所以 x 是 int 类型 auto item = val1 + val2; // 由val1 和 val2 相加的结果推导出 item 的类型,并确定 item 的初始值 错误使用: auto a; // 没有初始值,编译器无法为a匹配数据类型,导致编译错误。 auro i = 2, j = 3.14; // i 和 j 推断出的类型不一致,编译器并不知道该如何对 i, j 匹配数据类型 (1)auto 一般会忽略掉 引用 和 顶层const 例如: int i = 0, &r = i; auto a = r; // a 是一个整数 const int j = i, &rj = j; auto b = j; // b 是一个整数(j 的顶层 const 特性被忽略掉了) auto v = rj; // 才是一个整数(引用 和 顶层const 都被忽略掉了) (2)底层 const 会保留下来 int i = 1; const int j = 5; auto d = &i; // d 是一个整型指针 auto e = &j; // e 是一个指向整数常量的指针(底层const特性被保留) (3)其他情况 const int j = 5; auto &r = j; // r 是一个整型常量引用,绑定到 j 上 auto *p = &j; // p 是指向整型常量的指针 2.decltype:只是推导出数据类型,但不想用表达式的值初始化变量 (1)可以推导出表达式的类型 int i = 1, j = 2; decltype( i ) k = 5; decltype( i + j ) t; (2)可以推导出函数返回值类型 decltype( f( ) ) sum = x; // sum 的类型就是函数 f 的返回值类型 (3)处理 顶层const 和 引用 与 auto 不同,会进行保留 const int ci = 0, &cj = ci; decltype( ci ) x = 0; // x 的类型是 const int decltype( cj ) y = 0; // y 的类型是 const int& decltype( cj ) z ; // 错误,z 的类型是 const int&,必须初始化 3.auto 与 decltype 对数组的处理 当使用数组名作为一个auto 变量的初始值时,得到的类型是指针而不是数组 decltype(数组名)返回的是数组类型:大小和元素类型 例: int ia[ ] = {1, 2, 3, 4, 5}; auto ia2(ia); // ia2 是一个指针,指向ia 的第一个元素 decltype( ia ) ia3 = {6, 7, 8, 9, 10}; // ia3 是一个包含5 个元素的int 数组; ia3[2] = 5; //正确:可以给数组元素用int 赋值
nullptr
在C++11 之前,C++是把 NULL 和 0 看成同一个东西,但是看起来不是特别合理,一个是指针,一个是数值,所以在C++11,引入 nullptr 关键字来区分 空指针 和 0,并代替 NULL. nullptr 的类型是 nullptr_t ,能够转换为 任何指针 或 成员指针 的类型,也可以进行相等或不等的比较。 例如,传入头节点 TreeNode* root 会把头节点与nullptr进行比较,看是否是空树 if(root == nullptr) { ... } // 转换为 节点指针类型,并且进行了比较
智能指针
智能指针的作用
引入原因: C++ 没有内存回收机制,在C++ 11 之前,程序员都是通过 new 和 delete 对内存进行管理,但是程序员 new 出来的内存可能会忘记 delete 而导致内存泄漏,所以在 C++ 11 种引入新的 3种智能指针 对内存进行高效安全地管理。 智能指针的实质: 智能指针把一个普通的指针将封装成一个类,使用类实例化出一个栈对象,利用对象的生成周期控制资源,当这个对象的生命周期结束后,对象就会在自动在析构函数中释放掉在构造函数中申请的内存,防止内存泄露。 使用到的思想: RAII(Resource Acquisition Is Initialization):资源获取即初始化,就是在构造函数中申请分配资源,在析构函数中释放资源,利用对象生命周期控制资源。 shared_ptr:多个指针指向同一个对象,引用计数 unique_ptr:独占,一个指针指向同一个对象 weak_ptr:只引用,不计数,用于弥补 shared_ptr 的环形引用问题
auto_ptr
是C++11之前的一种智能指针,也能够对资源进行自动管理,但是当管理的对象拷贝或者赋值后,旧对象就没有对这份资源的管理权了。 当然可以添加一个成员变量来控制资源管理权归属问题,但是这种解决方法会导致野指针的出现。 所以实际使用不会使用auto_ptr,C++11也移除了这种智能指针。
shared_ptr
底层实现
原理
shared_ptr: 多个指针对象可以指向同一个资源。 运算符重载 + 引用计数 要想表现的像是一个指针,就要有指针那样的操作,因为智能指针是一个类,所以需要对指针的 *(解引用运算符) 和 ->(成员访问运算符)进行重载 还需要对三大函数进行重载: 拷贝构造函数:使引用计数加1 赋值运算符 = := 操作符右侧对象已经有对象,那么对其引用计数减1,并判断是否为0,再将左侧对象的引用计数加1 析构函数:使引用计数减1,斌判断是否为0,为0,调用delete释放资源 数据成员: 一个指针,一个引用计数 成员函数: operator * 和 operator -> 拷贝构造函数 赋值运算符 = 析构函数
代码
缺点及解决
线程不安全
线程不安全问题: shared_ptr 智能指针对象中的引用计数是多个智能指针对象共享的,若多个线程中智能指针在未加锁的情况下,同时对引用计数进行操作,会导致引用计数混乱,最终可能会造成资源泄漏或者程序奔溃的问题; 智能指针管理的对象保存在堆上,两个线程同时去访问,也会导致线程安全问题。 解决: 为了保证线程安全,需要在访问临界资源的地方(临界区),例如:引用计数,管理的对象前 加锁;访问完解锁。
管理资源单一
管理资源能力单一问题: shared_ptr只能处理用new申请出来的对象资源,那么当使用者用shared_ptr管理不是new申请出来的对象时,程序运行到释放资源的这一步就会出现错误。 解决: 为了解决这个问题C++又为shared_ptr设计了特殊的删除器; 这个特殊的删除器本质上就是仿函数,利用仿函数的特点来将shared_ptr智能指针管理资源能力单一的缺陷,即将释放管理资源的任务交给定制出来的删除器来完成。
概要
可能会产生循环引用的问题
使用 weak_ptr 解决
线程安全问题
多线程对引用计数: shared_ptr内部的引用计数是原子的操作,所以是线程安全的。 多线程读写shared_ptr: (1)同⼀个shared_ptr被多个线程“读”是安全的; (2)同⼀个shared_ptr被多个线程“写”是不安全的; (3)共享引⽤计数的不同的shared_ptr被多个线程”写“ 是安全的。
weak_ptr
weak_ptr 出现原因 和 问题解决
weak_ptr 的设计初衷: 用来解决 shared_ptr (共享指针)循环引用问题,weak_ptr 不独立的管理资源,即不能单独使用,必须配合 sherad_ptr 来使用。(举例见下) 本质: weak_ptr 本质上是一个计数器。 循环引用: 使用多个智能指针 shared_ptr 时,出现了指针之间的相互指向,从而形成环的情况,类似于死锁现象,在应该结束生命周期时,对象内部的引用计数由于没减为0,所以不能调用析构函数释放资源,造成内存泄露。 循环引用的解决: 使用弱指针 weak_ptr,weak_ptr 和 shared_ptr类似,但是 weak_ptr 不添加引用计数,只是获取 原理: 通过引用计数检测所管理的对象是否已经被释放, 从而避免访问非法内存; 问题提出: class B; class A{ public: A ( ) { cout << "A()" << std::endl; } ~A ( ) { cout << "~A()" << std::endl; } public: Shared_Ptr<B> spa; }; class B{ public: B ( ) { cout << "B()" << std::endl; } ~B() { cout << "~B()" << std::endl; } public: Shared_Ptr<A> spb; }; int main( ){ Shared_Ptr<A> pa(new A()); Shared_Ptr<B> pb(new B()); pa->spa = pb; pb->spb = pa; return 0; } 结果:  分析: 析构函数没有运行,new 出来的资源没有释放,出现了内存泄露(引用计数没有减为0,就认为生命周期没有结束,所以不会调用析构函数) 
unique_ptr
unique_ptr
unique_ptr是在C++11中为了解决auto_ptr的问题而提出来的一种智能指针;同样的unique_ptr也并不是非常完美的,它也有问题:为了解决auto_ptr的缺陷,而将unqiue_ptr的拷贝和赋值函数私有化,这样导致对象之间不可以共享资源;
tuple 元组
列表初始化
范围 for 语句
lambda 表达式
constexpr
修饰普通变量
定义变量是,可以用 constexpr 修饰,该变量会的在编译阶段即可计算出结果的能力。 使用 constexpr 修改普通变量时,变量必须经过初始化且初始值必须是一个常量表达式 // 如果将 constexpr 删除,就会报错,因为数组的长度必须是常量 constexpr int num = 1 + 2 + 3; int url[num] = {1,2,3,4,5,6}; couts<< url[1] << endl; 将上述的 constexpr 变成 const 也可以正常运行,编译器会认定 num 是一个常量表达式
修饰函数:常量表达式函数
常量表达式函数的写法: constexpr int display(int x) { //可以添加 using 执行、typedef 语句以及 static_assert 断言 return 1 + 2 + x; } 常量表达式函数的作用: 用于修饰函数的返回值 成为常量表达式函数的条件: (1)整个函数的函数体中,只能包含一条 return 语句(除了 using 语句, typedef 语句,static_assert 断言) (2)返回类型不能为 void,必须要有返回值 (3)使用之前必须定义 (4)return 返回的表达式必须是常量表达式 因为程序编译阶段就可以通过这个函数的返回值获得常量,不能推迟到运行时再获得
修饰构造函数
constexpr不能修饰自定义类型,如果想要修饰自定义类型对象,需要在该类型内部添加一个常量构造函数,并且该构造函数的函数体必须为空,且采用初始化列表的方式为各个成员赋值时,必须使用常量表达式: (不支持修饰 虚函数) //自定义类型的定义 struct myType { constexpr myType(char *name,int age):name(name),age(age){}; const char* name; int age; //其它结构体成员 }; void main(){ constexpr struct myType mt { "zhangsan", 10 }; cout << mt.name << " " << mt.age << endl; }
右值引用
move 函数
使用 using 定义别名
模板
C++ 对象模型
C++ 类 5 种成员
数据成员
static(静态数据成员)
nonstatic(非静态数据成员)
成员函数
static(静态成员函数)
nonstatic(非静态成员函数)
virtual(虚函数)
补充:面试题
内存泄漏、溢出
unordered_map实现
class和struct的区别
用模板写一个vector,实现添加,删除,自动扩容,下标访问等功能
迭代器应该实现那两个函数
override 关键字的作用
如果派生类在虚函数声明时使用了override描述符,那么该函数必须重写其基类中的同名函数,否则代码将无法通过编译。
编译,运行,链接
内存
内存泄漏
什么是内存泄露?
内存泄漏(memory leak)是指由于疏忽或错误造成了程序未能释放掉不再使⽤的内存的情况。内存泄漏并⾮指内存在物理上的消失,⽽是应⽤程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因⽽造成了内存的浪费。
哪些情况会发生内存泄露?
(1)本来使用深拷贝却使用了浅拷贝 (2)
计算机网络
键入网址到网页显示,期间发生了生么?
浏览器解析 URL
在客户端在浏览器中键入URL之后,浏览器就开始对这个 URL 进行解析,从而生成发送给 服务端 的请求信息。 URL 元素组成: 是由使用的协议(一般是HTTP),后跟两个斜杠 + 服务端的域名或者直接 IP 地址,在后面可以跟数据在服务器上的存放路径名,如果省略访问数据的路径名,会直接访问服务器分目录下的默认文件 ( / 表示服务器的根目录 )  生成 HTTP 请求信息: 对 URL 进⾏解析之后,浏览器确定了 服务器 和 ⽂件名,接下来就是根据这些信息来⽣成 HTTP 请求消息了。
真实地址查询
浏览器解析 URL 并生成 HTTP 消息后,就需要通过网络把消息报文发送给服务器端。但是这是需要 服务器 的真实地址,也就是 IP 地址是多少,当然,可以在 URL 中直接使用IP地址,但是一般不这样做,因为IP地址很难记,通常时使用服务器域名代替IP地址。 那么就需要将服务器域名转化为IP地址,使用到的就是DNS: 查找 DNS 缓存 浏览器 查找 浏览器 缓存 -- 查找 操作系统 缓存 -- 查找与之相连的路由器缓存 -- 检查本地通信服务商 ISP 缓存,如果上面四步都没查找目标网络的 IP 地址,会发起 DNS 查询。 发起 DNS 查询(递归) 客户端会向 本地 DNS 服务器(客户端TCP/IP设置中填写的DNS服务器地址) 发送一个 DNS 请求,问:请问这个域名的IP地址是多少,本地DNS服务器知道就直接进行返回,否则,会进行后续操作; 本地域名服务器会问根域名服务器:大哥,这个域名的IP地址是多少,但是根域名服务器不直接进行域名解析,但会给它知名一条道路,说这个域名归某个顶级域名服务器管,你去问问他; 本地域名服务器又去问顶级域名服务器:2哥,这个域名的IP地址是多少,顶级域名服务器会把相应的权威DNS服务器告诉它,因为权威DNS域名服务器就是域名解析结果的原出处; 之后,本地域名服务器会把这个域名相应的IP地址返回给本地域名服务器,本地域名服务器又会把这个 IP 地址返回给客户端。 这样,客户端就知道了服务端的 IP 是多少了。 
封装 TCP 数据包
拿到 IP 地址后,浏览器也会通过ARP协议得到目标服务器地MAC地址,接下来就开始准备尝试建立 HTTP 连接,(三次握手)建立一条 TCP 连接,之后就开始正常传输数据、
应用层
HTTP
介绍下 HTTP 协议
HTTP ,超⽂本传输协议,HyperText Transfer Protocol HTTP 是⼀个在计算机世界⾥专⻔在「两点」之间「传输」⽂字、图⽚、⾳频、视频等「超⽂本」数据的「约定和 规范」 具有: 简单、灵活、易于扩展、应⽤⼴泛和跨平台 的特性 Web 上的通信就是建⽴在 HTTP 协议上的 1. 客户端发起 HTTP 请求; 2. 服务器做出响应处理后,返回 HTTP 响应报⽂
HTTP 五大类状态码
1xx:提示信息,表示目前是协议处理中的⼀种中间状态,还需要后续操作 2xx:成功,表示服务器成功处理了客户端的请求 200 OK:成功状态码,表示⼀切正常。如果是⾮ HEAD 请求,服务器返回的响应头都会有 body 数据 204 No Content:成功状态码,与 200 OK 类似,但响应头没有 body 数据 3xx:重定向,资源位置发生了变动,需要客户端⽤新的 URL 重新发送请求获取资源 301 Moved Permanently:表示永久重定向,说明请求的资源已经不存在了,需改⽤新的 URL 再次访问 302 Found:表示临时重定向,说明请求的资源还在,但暂时需要⽤另⼀个 URL 来访问 4xx:客户端错误,请求报⽂有误,服务器⽆法处理 400 Bad Request:客户端请求报⽂有错误 404 Not Found:请求的资源在服务器上不存在或未找到,所以⽆法提供给客户端。 5xx:服务器错误,客户端请求报⽂正确,但服务器处理时内部发⽣了错误 500 Internal Server Error:服务器发⽣了什么错误 503 Service Unavailable:服务器当前很忙,暂时⽆法响应
头部常见字段
1. Host:在请求报文中,⽤来指定服务器的域名 2.Content-Length:服务器在返回数据时,会有 Content-Length 字段,表明本次回应的数据⻓度。 3.Connection:常⽤于客户端要求服务器使⽤ TCP 持久连接,以便其他请求复⽤。 HTTP/1.1 版本的默认连接都是持久连接,但为了兼容⽼版本的 HTTP,需要指定 Connection 的值为 Keep-Alive(Connection: keep-alive),这样⼀个可以复⽤的 TCP 连接就建⽴了,直到客户端或服务器主动关闭连接。 4.Content-Type:⽤于服务器回应时,告诉客户端,本次数据是什么格式,是UTF-8还是ASCII格式 5.Accept:客户端请求的时候,可以使⽤ Accept 字段声明⾃⼰可以接受哪些数据格式。 Accept : */* 可以接受任何格式的数据 6.Content-Encoding:说明数据的压缩⽅法。表示服务器返回的数据使⽤了什么压缩格式 7.Accept-Encoding:客户端在请求时,指定⾃⼰可以接受哪些压缩⽅法。
Get 和 Post
GET和POST都是HTTP请求⽅法 GET 申请获取资源,只是查询,读取,不会修改服务器上的数据 POST 客户端向服务器提交数据或修改数据,会影响服务器 Get 方式把查询字符串参数通过键值对形式存放在 URL 中进行发送,而 Post 是将查询字符串存放到Http 消息主体中发送的。所以,Get 请求对 URL 有长度限制,而 Post 请求无长度限制。 区别: Get 和 Post 本质上就是 TCP 链接,没有太大的本质区别,是因为浏览器和服务器的限制,导致在应用过程中可能有所不同。 两者之间的一个主要区别是:Get 产生一个 TCP 数据包;Post 产生两个 TCP 数据报 Get:浏览器回把 Http 的 Header 和 Data 一并发送出去,服务器返回 200 OK(返回数据); Post:浏览器先发送 Header,服务器响应 100 Continue,浏览器再发送 Data,服务器响应 200 OK
优点(特性),缺点
优点: 简单、灵活和易于扩展、应⽤⼴泛和跨平台 简单:报⽂格式 header + body ,头部信息是 键值对 简单⽂本形式,易于理解,降低了学习和使⽤的⻔槛 灵活和易于扩展:HTTP协议包含各类请求⽅法、URL、状态码、头字段等组成要素,而且可以⾃定义和扩充 应⽤⼴泛和跨平台:可以在各种端系统的各种各样的应用使用 缺点: ⽆状态、明⽂传输,不安全 ⽆状态 好处:服务器不记录 HTTP 的状态,就避免了额外的资源,减轻了服务器的负担,能够把更多的 CPU 和内存⽤来对外提供服务 坏处:在有关联性的操作时会很麻饭,例如在购物网站,如果服务器不记录状态,那么在登录页面,下单页面,⽀付页面都需要验证⽤户身份,这样会造成用户体验极差。 解法⽅案:常用方式是 Cookie 技术。Cookie 在客户端第⼀次请求后,服务器会下发⼀个装有客户信息的⼩贴纸,后续客户端请求服务器的时候, 带上⼩贴纸,服务器就能认得了了, 明⽂传输 优点:传输过程中的信息可以⽅便阅读 缺点:正是这样,HTTP 的所有信息被暴露,在传输过程中,信息内容容易被窃取 不安全 使⽤不加密的明⽂传输,内容可能会被窃听会被修改等情况, 但是 HTTP 的安全问题,可以使用 HTTPS 的⽅式解决,即通过引⼊ SSL/TLS 层增加安全性
HTTPS
参考链接: https://segmentfault.com/a/1190000021494676 https://segmentfault.com/a/1190000021559557
HTTP 与 HTTPS 有哪些区别?
HTTP 是超⽂本传输协议,信息是明⽂传输,存在安全⻛险的问题。HTTPS 使用加密传输,就是在 TCP 和 HTTP ⽹络层之间加⼊了 SSL/TLS 安全协议,安全性提高 HTTP 在 TCP 三次握⼿之后即可进⾏ HTTP 的报⽂传输;⽽ HTTPS 在 TCP 三次握⼿之 后,还需进⾏ SSL/TLS 的握⼿过程,才可进⼊加密报⽂传输。 HTTP 的端⼝号是 80,HTTPS 的端⼝号是 443。 HTTPS 协议需要向 CA(证书权威机构)申请数字证书,来保证服务器的身份是可信的。
工作原理
HTTPS 在 HTTP 与 TCP 层之间加⼊了 SSL/TLS 协议,可以解决HTTP 存在的⻛险,使用到的技术有 “信息加密”,“校验机制”,“数字证书”。 1.信息加密: 信息加密是通过 混合加密 的⽅式可以保证信息的机密性,解决了窃听的⻛险。 混合加密是使用 对称加密 和 ⾮对称加密 结合的⽅式: 只在通信建⽴前采⽤⾮对称加密的⽅式交换会话秘钥 后续全部的通信过程都使⽤对称加密的「会话秘钥」的⽅式加密明⽂数据。 采⽤「混合加密」的⽅式的原因: 对称加密只使⽤⼀个密钥,运算速度快,密钥必须保密,⽆法做到安全的密钥交换。 ⾮对称加密使⽤两个密钥:公钥和私钥,公钥可以任意分发⽽私钥保密,解决了密钥交换问题但速度慢。 2.摘要算法 ⽤来实现完整性,能够为数据⽣成独有的信息,⽤于校验数据的完整性,解决了篡改的⻛险。 3.数字证书 第三⽅权威机构 CA (数字证书认证机构),能确保数字证书中的服务器公钥是可信的,并能保证不被篡改。
加密算法
混合加密算法: HTTPS 解决数据传输安全问题的方案就是使用混合加密算法(对称加密和非对称加密混合使用) 1. 对称加密 对数据加密和解密都是使用同一个密钥 2. 非对称加密 对数据加密和机密使用两个不同的密钥(公钥和私钥),公钥用来加密,对应的私钥用来解密 对称加密效率高,但安全性不高;相反,非对称加密安全性高,但效率不高。上面两种算法都各有优缺点,HTTPS 综合这两种算法的优点,不仅保证通信安全,还保证数据传输效率。
SSL / TLS
SSL/TLS: SSL 和 TLS 协议能够为通信双方提供识别和认证通道,从而保证通信的机密性和数据完整性; 这两种协议都算是安全层,但是并不兼容,SSL 已经逐渐被 TLS 取代现在 SSL/TLS 都指代 TLS。 TLS 握手: TLS 握手是启动 HTTPS 通信的过程,是 HTTPS 通信的基础部分,类似于 TCP 建立连接时的三次握手。 在 TLS 握手的过程中,通信双方交换消息以相互验证,相互确认,并确立它们所要使用的加密算法以及会话密钥 (用于对称加密的密钥)。
HTTPS 原理
HTTPS = HTTP + SSL/TLS HTTPS 连接建立阶段: HTTPS 的整个通信过程可以分为两大阶段:证书验证和数据传输阶段,数据传输阶段又可以分为非对称加密和对称加密两个阶段。 原理解析:(建立过程简要分析,看着图) 1. 客户端使用 HTTPS 网址 进行请求,然后连接到 server 的 443 端口 (HTTPS 默认端口,类似于 HTTP 的80端口)。 2. 采用 HTTPS 协议的服务器必须要有一套数字 CA (Certification Authority)证书,证书是需要申请的,并由专门的数字证书认证机构(CA)通过非常严格的审核之后颁发的电子证书 (当然了是要钱的,安全级别越高价格越贵)。颁发证书的同时会产生一个私钥和公钥。私钥由服务端自己保存,不可泄漏。公钥则是附带在证书的信息中,可以公开的。证书本身也附带一个证书电子签名,这个签名用来验证证书的完整性和真实性,可以防止证书被篡改。 3. 服务器响应客户端请求,将证书传递给客户端,证书包含公钥和大量其他信息,比如证书颁发机构信息,公司信息和证书有效期等。Chrome 浏览器点击地址栏的锁标志再点击证书就可以看到证书详细信息。 4. 客户端解析证书并对其进行验证。如果证书不是可信机构颁布,或者证书中的域名与实际域名不一致,或者证书已经过期,就会向访问者显示一个警告,由其选择是否还要继续通信。 如果证书没有问题,客户端就会从服务器证书中取出服务器的公钥A。然后客户端还会生成一个随机码 KEY,并使用公钥A将其加密。 5. 客户端把加密后的随机码 KEY 发送给服务器,作为后面对称加密的密钥。 6. 服务器在收到随机码 KEY 之后会使用私钥B将其解密。经过以上这些步骤,客户端和服务器终于建立了安全连接,完美解决了对称加密的密钥泄露问题,接下来就可以用对称加密愉快地进行通信了。 7. 服务器使用密钥 (随机码 KEY)对数据进行对称加密并发送给客户端,客户端使用相同的密钥 (随机码 KEY)解密数据。 8. 双方使用对称加密愉快地传输所有数据。
HTTPS 建立连接过程(详细)
和HTTPS 原理的流程类似,但是更细致了: 1. "client hello"消息:客户端通过发送"client hello"消息向服务器发起握手请求,该消息包含了客户端所支持的 TLS 版本和密码组合以供服务器进行选择,还有一个"client random"随机字符串。 2. "server hello"消息:服务器发送"server hello"消息对客户端进行回应,该消息包含了数字证书,服务器选择的密码组合和"server random"随机字符串。 3. 验证:客户端对服务器发来的证书进行验证,确保对方的合法身份,验证过程可以细化为以下几个步骤: (1)检查数字签名 (2)验证证书链 (这个概念下面会进行说明) (3)检查证书的有效期 (4)检查证书的撤回状态 (撤回代表证书已失效) 4. "premaster secret"字符串:客户端向服务器发送另一个随机字符串"premaster secret (预主密钥)",这个字符串是经过服务器的公钥加密过的,只有对应的私钥才能解密。 5. 使用私钥:服务器使用私钥解密"premaster secret"。 6. 生成共享密钥:客户端和服务器均使用 client random,server random 和 premaster secret,并通过相同的算法生成相同的共享密钥 KEY。 7. 客户端就绪:客户端发送经过共享密钥 KEY加密过的"finished"信号。 8. 服务器就绪:服务器发送经过共享密钥 KEY加密过的"finished"信号。 9. 达成安全通信:握手完成,双方使用对称加密进行安全通信。
HTTP 版本
HTTP 1.1
HTTP/2
HTTP/1.1 的缺陷
延迟难以下降,虽然现在⽹络的「带宽」相⽐以前变多了,但是延迟降到⼀定幅度后,就很难再下降了,说 ⽩了就是到达了延迟的下限; 并发连接有限,⾕歌浏览器最⼤并发连接数是 6 个,⽽且每⼀个连接都要经过 TCP 和 TLS 握⼿耗时,以及 TCP 慢启动过程给流ᰁ带来的影响; 队头阻塞问题,同⼀连接只能在完成⼀个 HTTP 事务(请求和响应)后,才能处理下⼀个事务; HTTP 头部巨⼤且᯿复,由于 HTTP 协议是⽆状态的,每⼀个请求都得携带 HTTP 头部,特别是对于有携带 cookie 的头部,⽽ cookie 的⼤⼩通常很⼤; 不⽀持服务器推送消息,因此当客户端需要获取通知时,只能通过定时器不断地拉取消息,这⽆疑浪费⼤ᰁ 了带宽和服务器资源。
改进方面
为了能够兼容 1.1 版本协议,2 代版本没有引入新的协议名,依然是用HTTP表示明文协议,HTTPS表示加密协议。 只在应用层做了改变,还是基于TCP协议传输(3代版本应用层就做了改变,使用的是UDP协议); 应用层把HTTP分成了 语义 和 语法 两部分。 语义方面不改动,例如:请求方法,状态码,头部字段等规则 语法方面进行改动,基本改变了HTTP报文的传输格式
1. 头部压缩
HTTP 协议报⽂由「Header + Body」构成的, 对于 Body 部分,1.1 版本可在 ContentEncoding 字段指定 Body 的压缩⽅式,如 gzip 压缩,可节约带宽 但 1.1 版本 Header 部分没有优化⼿段,主要问题有: 头部字段加起来占用大量字节,所以也有必要进行压缩; 并且当发送大量报⽂时,报文中这些字段值大部分是重复的,资源被浪费了,所以有必须要避免重复性; 字段是 ASCII 编码的,虽然易读,但效率低,所以可以改成⼆进制编码; 2 代使用 HPACK 算法 技术进行压缩,而不是使用平常的压缩方式(例如gzip),HPACK 算法主要包含 3 部分: 静态字典 动态字典 Huffman 编码(压缩算法) 1.静态字典: 将 HTTP/2 出现的高频字符串或字段使用 Huffman编码 方式写进一张 静态表 中,并且写进 HTTP/2 框架中,不会变化,并且这些编码都是基于二进制编码的。 2.动态字典: 静态表中包含的字符串有限,不在静态表中的头部字符串就要自行构建一张 动态表,以索引 62 起步(因为静态表中包含61种),会在编码解码的时候随时更新。 例如:发送端第一次发送一个新的头部字段,经过 Huffman编码 发送端和客户端都会更新自己的动态表,将这个新的字段加进去。 使用静态表有一个前提:必须在同一个连接上,重复传输完全相同的HTTP头部。 随着发送的数据信息增加,动态表也在增加,所以在动态表占用内存过大时,就应该连接上的请求数量,并且请求数量到达极限时,应该断开连接释放内存。 3.Huffman 编码 贯穿在静态表和动态表的建立和更新阶段
2. 二进制帧
2 代将 1.1版本 的⽂本格式改成⼆进制格式传输数据,提⾼了 HTTP 传输效率,⽽且使⽤位运算解析⼆进制数据也很快。
3. 并发传输
1.1 版本的实现是基于 请求-响应模型 的。同⼀个连接中,HTTP 只有完成上⼀个事务(请求与响应),才能处理下⼀个事务,导致传输效率很低,可能会造成后续的请求是⽆法发送的,造成了队头阻塞的问题。 而 2 代版本,通过 Stream 设计,使得多个 Stream 复⽤⼀条 TCP 连接,达到并发的效果,解决了 HTTP/1.1 队头阻塞的问题。 2 代并发是通过 Stream、Message、Frame 3种消息结构实现的 1 个 TCP 连接包含若干个 Stream 一个 Stream 包含 若干个 Message,一个 Message 对应 的是 HTTP/1 中的请求或响应报文 一个 Message 包含若干个 Frame,一个 Frame 是 HTTP/2 最⼩单位,以⼆进制压缩格式存放 HTTP/1 中的内容(头部和包体); HTTP 消息可以由多个 Frame 构成,以及 1 个 Frame 可以由多个 TCP 报⽂构成 在 HTTP/2 连接上,不同 Stream 的帧是可以乱序发送的(因此可以并发不同的 Stream ),因为每个帧的头部会 携带 Stream ID 信息,所以接收端可以通过 Stream ID 有序组装成 HTTP 消息,⽽同⼀ Stream 内部的帧必须是严格有序的
HTTP/3
HTTP/2 的缺陷
队头阻塞 TCP 与 TLS 的握⼿时延迟 ⽹络迁移需要᯿新连接 1. 队头阻塞 HTTP/2 多个请求是跑在⼀个 TCP 连接中的,那么当 TCP 丢包时,整个 TCP 都要等待重传,那么就会阻塞该TCP 连接中的所有请求。 因为 TCP 是字节流协议,TCP 层必须保证收到的字节数据是完整且有序的,如果序列号较低的 TCP 段在⽹络传输中丢失了,即使序列号较⾼的 TCP 段已经被接收了,应⽤层也⽆法从内核中读取到这部分数据,从 HTTP 视⻆看,就是请求被阻塞了。 2. TCP 与 TLS 的握手时延迟 双方发送数据前需要经过 TCP 三次挥手和 TLS 四次挥手建立一条TCP 连接,因此,需要经过3到4个RTT时延才能发送请求数据。 3. 网络迁移需要重新连接 ⼀个 TCP 连接是由四元组(源 IP 地址,源端⼝,⽬标 IP 地址,⽬标端⼝)确定的,这意味着如果 IP 地址或者端⼝变动了,就会导致需要 TCP 与 TLS ᯿新握⼿,这不利于移动设备切换⽹络的场景,⽐如 4G ⽹络环境切换成WIFI。 以上的问题是因为使用 TCP 必定会存在的情况,只要使用 TCP 协议,那么 HTTP/2 就无法逃脱这些问题,所以,HTTP/3 将 TCP 协议换成了 UDP 协议。 
QUIC 协议
QUIC 协议的引入: UDP 是⼀个简单、不可靠的传输协议,数据包之间也是⽆序的,没有依赖关系; UDP 不需要连接的,也就不需要握⼿和挥⼿的过程,所以⽐ TCP 快; HTTP/3 不仅将下层的 TCP 换成 UDP,还在 UDP 协议和HTTP协议之间加入了 QUIC 协议 QUIC 协议: 具有类似 TCP 的连接管理、拥塞窗⼝、流量控制特性,相当于将不可靠传输的 UDP 协议变成“可靠”的了,所以不会有数据包丢失的问题,并且也能够解决 HTTP/2 的问题,实现了 无队头阻塞,更快的连接建立,连接迁移。 (1)无队头阻塞: 因为 QUIC 使⽤的传输协议是 UDP,所以 UDP 不关⼼数据包的顺序,如果数据包丢失,UDP 也不关⼼; 但是 QUIC 协议会保证数据包的可靠性,每个数据包都有一个唯⼀标识。当某个流中的⼀个数据包丢失了,即使该流的其他数据包到达了,数据也⽆法被 HTTP/3 读取,直到 QUIC 重传丢失的报⽂,数据才会交给 HTTP/3。 ⽽其他流的数据报⽂只要被完整接收,HTTP/3 就可以读取到数据。这与 HTTP/2 不同,HTTP/2 只要某个流中的数据包丢失了,其他流也会因此受影响。 所以,QUIC 连接上的多个 Stream 之间并没有依赖,都是独⽴的,某个流发⽣丢包了,只会影响该流,其他流不受影响  (2)更快的连接建立: HTTP/2 协议的连接建立阶段要经过 TCP 的三次握手 和 TLS 的四次握手,需要3-4个RTT时延才能建立; HTTP/3虽然在传输数据前也需要 QUIC 协议握手,但是只需要 1个RTT 时延,握手的目的是:为了确认双方的 连接ID,这个 连接ID 是为了实现连接迁移。 QUIC 内部包含了 TLS,仅需 1 个 RTT 就可以同时完成建⽴连接与密钥协商,甚⾄在第⼆次连接的时候,应⽤数据包可以和 QUIC 握⼿信息(连接信息 + TLS 信息)⼀起发送,达到 0-RTT 的效果。 (3)连接迁移: 在前⾯我们提到,HTTP/2的IP地址或者端口发生变化,那么就必须要断开连接,然后重新使用TCP三次握手和TLS四次握手建⽴连接,因此连接的迁移成本是很⾼的。 QUIC 协议不⽤四元组⽅式“绑定”连接,⽽是通过 连接 ID 来标记通信的两个端点,客户端和服务器可以各⾃选择⼀组 ID 来标记⾃⼰,因此即使移动设备的⽹络变化后,导致 IP 地址变化了,只要仍保有上下⽂信息(⽐如连接 ID、TLS 密钥等),就可以“⽆缝”地复⽤原连接,消除重连的成本,没有丝毫卡顿感,达到了连接迁移的功能。
HTTP/3 协议
1. 帧结构发生变化: HTTP/3 帧头只有两个字段:帧类型、帧长度 (1)帧类型: 数据帧:HEADERS 帧(HTTP 头部 )和 DATA 帧(HTTP 包体)都属于数据帧 控制帧 (2)帧长度  2. 头部压缩算法升级 使用 QPACK ,与HTTP/2使用的HPACK类似,也是用Huffman编码 静态表进行了扩充 动态表编码方式发生了变化 
DNS
传输层
TCP
TCP 介绍
TCP 是传输层的一个协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议。
1. 面向连接的:TCP 连接的建立域断开
三次握手
作用
三次握手的作用: 创建一条通信双方的一条 TCP 连接 为了确认双方的接收与发送能力是否正常; 确定双方的初始化序列号,为后面的可靠传送做准备; 如果使用的是 https 协议,在三次握手过程,还会进行数字证书的验证以及加密密钥的生成。
为什么是三次握手,不是两次,四次?
(1)原因一: 从上面解释知道,当在第二次握手后,只有客户端知道双方都具有发送和接收能力,但是服务器只知道服务器自己有接受能力以及客户端具有发送能力; 服务器要知道i及本身是否有发送能力以及客户端是否有接收能力要在第三次握手(客户端发送完数据并被客户端收到)后才知道。 所以,在三次握手后,服务器和客户端都知道通信双方都具有了接收和发送能力,再进行多次握手也没有太大的必要,会浪费系统资源。 (2)原因二: 为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。 例如:“已失效的连接请求报文段” 的产生在这样一种情况下:client 发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达 server。 本来这是一个早已失效的报文段。但 server 收到此失效的连接请求报文段后,就误认为是 client 再次发出的一个新的连接请求。于是就向 client 发出确认报文段,同意建立连接。假设不采用 “三次握手”,那么只要 server 发出确认,新的连接就建立了。由于现在 client 并没有发出建立连接的请求,因此不会理睬 server 的确认,也不会向 server 发送数据。但 server 却以为新的运输连接已经建立,并一直等待 client 发来数据。这样,server 的很多资源就白白浪费掉了。 采用 “三次握手” 的办法可以防止上述现象发生。例如刚才那种情况,client 不会向 server 的确认发出确认。server 由于收不到确认,就知道 client 并没有要求建立连接。”
过程
初始状态:客户端处于 closed 状态,服务端处于 listen 状态。 第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN(c)。此时客户端处于 SYN_Send 状态。 第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s),同时会把客户端的 ISN + 1 作为 ACK 的值,表示自己已经收到了客户端的 SYN。此时服务器处于 SYN_REVD 的状态。 第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文。此时客户端处于 establised 状态。 服务器收到 ACK 报文之后,也处于 establised 状态,此时,双方以建立起了链接。 
TCP 半连接 和 全连接队列
在 TCP 三次握⼿的时候,Linux 内核会维护两个队列,分别是: 半连接队列,也称 SYN 队列; 全连接队列,也称 accepet 队列; 服务端收到客户端发起的 SYN 请求后,内核会把该连接存储到半连接队列,并向客户端响应 SYN+ACK,接着客 户端会返回 ACK,服务端收到第三次握⼿的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的连 接,并将其添加到 accept 队列,等待进程调⽤ accept 函数时把连接取出来。 什么是半连接队列? 服务器第一次收到客户端的 SYN 之后,就会处于 SYN_RCVD 状态,此时双方还没有完全建立其连接,服务器会把此种状态下请求连接放在一个队列里,我们把这种队列称之为半连接队列。 当然还有一个全连接队列,就是已经完成三次握手,建立起连接的就会放在全连接队列中。如果队列满了就有可能会出现丢包现象。
SYN 洪泛攻击
SYN 洪范攻击:(利用 TCP 连接机制的漏洞) 服务器端的资源分配是在二次握手时分配的 客户端的资源是在完成三次握手时分配的,所以服务器容易受到 SYN 洪泛攻击,这是一种典型的 Dos/DDos 攻击。 攻击者在短时间内伪造大量不存在的 IP 地址,向服务器不断发送 SYN 包,此时服务器会不断向这些不存在的 IP 地址建立半连接并放进半连接队列,并且回复确认包,并等待 Client 确认,由于源地址不存在,因此 Server 需要不断重发直至超时,并长时间占用半连接队列,导致正常的 SYN 请求因为队列满而被丢弃,从而引起网络拥塞甚至系统瘫痪。(半连接队列满后会产生丢包现象) 检测 SYN 攻击: 在服务器上看到大量的半连接状态时,特别是源 IP 地址是随机的,基本上可以断定这是一次 SYN 攻击。 解决方法: 缩短超时(SYN Timeout)时间 增加最大半连接数 过滤网关防护 SYN cookies 技术: 在服务器收到客户端的第一次请求之后,不立马分配资源,打开半连接队列了,而是根据报文段中的重要信息(源、目的 IP 地址,端口号)使用特定的散列函数生成一个 cookies 值,将 cookie 作为初始序列号,返回 SYN + ACk 报文段给客户端; 如果客户端真实存在,则客户端将会带着 cookie+1 作为 ack 响应。而服务器可以再次执行上述过程,比较与 ack-1 是否相同。如果相同,则建立连接。如果客户端不存在,则服务器没有为其消耗任何资源。
其他问题
(1)三次握手过程中可以携带数据吗? SYN 报文不携带数据,但会消耗一些序号。所以在前两次握手中是不携带数据的。为什么不携带呢? 假如第一次握手可以携带数据的话,服务器就要花费时间以及系统内存资源来存放这些报文数据。 如果有人想要恶意攻击服务器,并在第一次握手中的 SYN 报文中放入大量的数据,以及疯狂地向服务器重复发这种 SYN 报文的话,这会让服务器花费很多时间、内存空间来接收这些报文。 所以,第一次握手可以放数据的话,其中一个简单的原因就是会让服务器更加容易受到攻击了。第二次握手同理,可能会攻击客户端主机。 而对于第三次的话,此时客户端已经处于 established 状态,也就是说,客户端已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据页没啥毛病。(第二次握手,客户端就知道了双方都具有接收、发送能力) (2)初始序列号 ISN 是固定的吗? 三次握手的一个重要功能是客户端和服务端交换 ISN,以便让对方知道接下来接收数据的时候如何按序列号组装数据。 如果ISN是固定的,攻击者很容易猜出后续的确认号,因此 ISN 是动态生成的。 (3)SYN-ACK 重传次数的问题——截断二进制指数退避算法: 服务器发送完SYN-ACK包,如果未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传,如果重传次数超 过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。 注意,每次重传等待的时间不一定相同,一般会是指数增长,例如间隔时间为 1s, 2s, 4s, 8s, …3、
四次挥手
介绍
建立一个连接需要三次握手,而终止一个连接要经过 4次挥手。这由 TCP 的半关闭( half-close) 造成的。 既然一个 TCP 连接是全双工 (即数据在两个方向上能同时传递), 因此每个方向必须单独地进行关闭。 这原则就是当一方完成它的数据发送任务后就能发送一个 FIN 来终止这个方向连接。当一端收到一个 FIN,它必须通知应用层另一端已经终止了数据传送。理论上客户端和服务器都可以发起主动关闭,但是更多的情况下是客户端主动发起。
过程
第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于FIN_WAIT1状态; 第二次握手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 + 1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT状态; 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态; 第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 + 1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态5、服务端收到 ACK 报文之后,就处于关闭连接了,此时服务端处于 CLOSED 状态。 服务器还会通知上层的应用程序对方已经释放连接,此时 TCP 处于半关闭状态,也就是说客户端已经没有数据要发送了,但是服务器还可以发送数据,客户端也还能够接收。 
2MSL 的作用
2MSL: 两倍的报文段最大存活时间:这是任何报文段在被丢弃前能在网络中存在的最大存活时间。 客户端为什么需要在 TIME-WAIT 状态等待 2MSL 时间才能进入 CLOSED 状态? 按照常理,在网络正常的情况下,四个报文段发送完后,双方就可以关闭连接进入 CLOSED 状态了,但是网络并不总是可靠的,如果客户端发送的 ACK 报文段丢失,服务器在接收不到 ACK 的情况下会一直重发 FIN 报文段。因此客户端为了确保服务器收到了 ACK,会设置一个定时器,并在 TIME-WAIT 状态等待 2MSL 的时间,如果在此期间又收到了来自服务器的 FIN 报文段,那么客户端会重新设置计时器并再次等待 2MSL 的时间,如果在这段时间内没有收到来自服务器的 FIN 报文,那就说明服务器已经成功收到了 ACK 报文,此时客户端就可以进入 CLOSED 状态了。
为什么 TCP 关闭连接为什么要四次而不是三次?
服务器在收到客户端的 FIN 报文段后,可能还有一些数据要传输,所以不能马上关闭连接,但是会做出应答,返回 ACK 报文段,接下来可能会继续发送数据,在数据发送完后,服务器会向客户单发送 FIN 报文,表示数据已经发送完毕,请求关闭连接,然后客户端再做出应答,因此一共需要四次挥手。
2. 基于字节流的
数据交换格式: TCP 连接双方的数据交换格式是以字节构成的一种有序但无结构的字节流。 字节流服务: TCP 连接双方都有缓冲区,发送方和接收方会把要发送或接收的数据存在缓冲区中,等到合适的时机就会把数据发送或接收。 并且 TCP 不在字节流中插入记录标识符,所以接受方也不会直到发送方每一次发送多少字节的数据; TCP 对字节流中的内容不做任何解释,所以 TCP 不知道传输的数据字节流是二进制数据还是 ASCII 字符等形式。
TCP 报文段结构
 1. TCP 报文: TCP 首部的固定数据有20字节,加上选项部分最大可达60字节,而有效数据部分则是被打包的应用层数据。 端口号:用于确定发送端和接收端应用进程。源端口和 IP 地址 与 目的端口和 IP 地址可以确定一个唯一的 TCP 连接; 序号:(这两个序号都是对字节进行编号) 确认序号:(序号和确认序号可以实现 TCP 的可靠性) 首部长度:首部长度由于选项的存在是可变的,该值的4倍就是首部长度 保留字段:用于未来的使用,目前默认值为0 控制位:6位标志位,各有其含义,其值位1时生效 URG:紧急指针生效 ACK:确认序号生效 PSH:接收方应尽快将这个报文段交给应用层 RST:发送端遇到问题,想要重新建立连接 SYN:用于发起连接 FIN:用于关闭连接 窗口大小:用于流量控制 检验和:用于验证数据完整性,确保数据未被修改 紧急指针:是一个正的偏移量,和序号字段中的值相加表示紧急数据最后一个字节的序号,是发送端向另一端发送紧急数据的一种方式。 选项: 有效数据部分: 2. TCP 报文传输过程: 在 TCP 连接建立之后,客户端的应用层协议向 TCP 发送无特殊格式的字节流,TCP 将这些字节打包成报文段,报文段大小视情况而定,这些报文段会被网络层的 IP 封装成 IP 数据报,然后经过网络传输给服务器,而接下来服务器的操作相当于客户端的逆操作,先从 IP 数据报中拆分出 TCP 报文段,再把 TCP 报文段还原成字节流并发送给上层的应用层协议。
3. 可靠的:可靠性机制
重传机制
背景
重传机制是TCP实现可靠传输的方式之一,是通过序列号与确认应答。 发送端的数据到达目的主机时,接收端主机会返回一个确认应答消息,表示已经收到消息。但是在这个过程中,可能会发生数据包丢失的现象。 TCP针对数据包丢失的情况,会用重传机制解决。
超时重传
工作方式
在发送数据时,发送端会设定一个定时器,当超过这个指定的时间后,没有收到对方的ACK确认应答报文,就会重发这个数据。 触发: (1)发送端发送的数据包丢失 (2)接收端发送的确认应答报文丢失 D:\学习文件夹\面试\XMind笔记图片\重传机制1.jpg
往返时延(RTT,Round-Trip Time)
数据从网络一端传到另一端所需要的时间,也就是包的往返时延。 D:\学习文件夹\面试\XMind笔记图片\超时重传2.jpg
超时重传时间(RTO,Re-transaction Time out)
往返时间和超时重传时间不是同一个概念,但超时重传时间会参考往返时间。 超时重传时间: 较大:重发就慢,丢了老半天才开始重发,效率慢,性能差 较小:可能导致数据包还没有到达另一端或者丢失,就开始重发,发增加网络拥塞,导致更多的超时重传,产生恶行循环 设置: 超时重传时间 RTO 的值应该略大于报文往返时间 RTT 的值 因为网络是动态变化的,所以 RTT 也是会变化的,所以RTO 也是一个动态变化的值。
优缺点
优点: 缺点: 超时周期可能相对较长
快速重传
工作方式
不以时间为驱动,而是以数据进行驱动重传。 触发: 当收到三个相同的 ACK 报文时,会在定时器过期之前,重传丢失的报文段。 (快速重传和超时重传可以同时工作) 例子: 当发送端发送了1,2,3,4,5个数据包,但第2个数据包丢失了,接收端正常接收 1 这个数据包,想要接受 2 这个数据包,但是却接收到了3,4,5,发送端就会发送 3 次对于数据包 2 的确认报文。 发送端收到了单个 ACK = 2 的确认,就知道接收端 2 这个报文没收到,就会重传丢失的 2 数据报文
缺点
重传的时候,是只重传2这个数据报文,还是重传2,3,4,5 这 4 个。(根据 TCP 不同的实现,会有不同的处理方式)
SACK
工作方式
选择性确认(Selective Acknowledgment) 在 TCP头部选项字段中,加一个SACK的信息,它可以将缓存的地图发送给发送方,这样发送方就知道哪些数据收到了,哪些数据没有收到。 而重传时只需要重传丢失的数据即可。 就是ACK标志的信息是接收端想要的数据序列号 而SACK标志的信息是接受到的信息序列号范围 例如: ACK = 200 (下一个想要接受200开始的数据报文) SACK = 300 - 599 (接收端已经接受到了300到599序列号的数据)
例子
D:\学习文件夹\面试\XMind笔记图片\重传机制3.jpg
D-SACK
工作方式
Duplicate SACK,主要使用 SACK 告诉发送方有哪些数据被重复接收了。
例子
例1:ACK丢失 假设发送端发送两个数据包,300-399,400-499,接收端也都正常接收了,并且发送了这两个数据包的确认报文; 但是,两个确认报文都丢失了,没有到达发送端。发送端经过了超时时间,开始重传300-399的数据报文。 但是接收方收到后就发现数据是重复收到的,于是就返回一个带有SACK = 300-499的信息(之前的确认报文没这个信息),告诉发送方这个范围的数据都收到了。 发送端接收到这个报文后,发现ACK都到了499,意味着之前的数据都收到了。 此时,这个SACK就是D-SACK D:\学习文件夹\面试\XMind笔记图片\重传机制4.jpg 例2:网络延迟 发送端发送的一个数据包100-199由于网络延迟,导致在网络中停留了很久,导致这个数据包重传到了接收端。 之后某个时间点,网络好了,这个数据包又到了接收端,接收端发现已经接受过了,并且ACK都到达了300,所以就会返回一个带有SACK = 100-199,此时这个SACK就是D-ACK,表示收到了重复的包。 D:\学习文件夹\面试\XMind笔记图片\重传机制5.jpg
滑动窗口
引入原因
引入原因: 在没有滑动窗口之前,TCP是没发送一个数据都要进行一次确认应答。上一个数据包收到硬打了,在发送下一个。效率较低,并且数据包的往返时间越长,通信的效率就越低。 为解决这种问题,TCP引入了窗口,有了窗口,无序等待确认应答,也可以继续发送后续的数据,即使在往返时间较长的情况下,也不会降低网络通信的效率。 窗口实质: 是操作系统开辟的一个缓存空间,发送端在收到确认应答之前,数据就在缓冲区保留,收到确认应答,就从缓冲区移除。 接收端拥有窗口,可以进行累计确认,即使前面的确认报文丢失,也可以通过下一个确认应答进行确认,发送端收到确认应答后,就以为了接收端在这个确认应答号之前的数据都接收到了。
窗口大小的决定
TCP 头部字段的 Window ,即为窗⼝⼤⼩。 这个字段是接收端告诉发送端⾃⼰还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能⼒来发送数据,⽽不会导致接收端处理不过来。 所以,通常窗⼝的⼤⼩是由接收⽅的窗⼝⼤⼩来决定的。 发送⽅发送的数据⼤⼩不能超过接收⽅的窗⼝⼤⼩,否则接收⽅就⽆法正常接收到数据。
流量控制
背景
发送方如果一直无脑地发送数据给接收方,接收方可能会处理不过来,导致发送方重传,从而导致网络流量的浪费。 流量控制: TCP提供一种机制让 发送方 根据 接收方 的实际接收能力控制发送方发送的数据量,这就是流量控制。
拥塞控制
背景
在⽹络出现拥堵时,如果继续发送⼤量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是重传就会导致⽹络的负担更重,并且会导致更⼤的延迟以及更多的丢包,如果不进行解决,就会进入恶性循环。 所以,TCP 也应该考虑网络的情况,当⽹络发送拥塞时,TCP 应该要降低发送的数据量,减小网络的压力。 拥塞控制: 控制的⽬的就是避免 发送⽅ 的数据填满整个⽹络,调节 发送方 要发送的数据量,这个数据量就引出了 拥塞窗口 的概念。
拥塞窗口
拥塞窗口 cwnd 是发送方维护的一个状态变量,会根据网络的用色成都动态变化 网络中出现拥塞,cwnd 变小 网络中没有拥塞,cwnd 增大
拥塞检测
只要 发送方 没有在规定时间内接收到 ACK 确认报文,就是发生了 超时重传,就认为网络中出现了拥塞。 检测到了拥塞,该怎么办呢? 就需要使用拥塞控制算法来进行处理。
拥塞控制算法
慢启动算法
慢启动就是一点一点地提高发送数据包地数量,每当发送方收到一个 ACK,拥塞窗口 cwnd 的大小就会加 1。 例如: (1)链接建立后,一开始初始化 cwnd = 1,表示可以传一个 MSS 大小的数据; (2)发送端收到一个 ACK 确认应答后,cwnd 加 1,接下来发送端就可以发送 2 个 MSS 大小的数据; (3)之后,就是发送2个,收到2个;然后可以发送4个,就这样,发送的MSS大小按照2,4,8,16,... 这种按2的指数型增长; (4)但是这种增长不能一直增长下去,而是有一个限度,这个限度就是 慢启动门限(ssthresh) 慢启动门限 ssthresh: 当 cwnd < ssthresh 时,使用 慢启动算法 当 cwnd >= ssthresh 时,使用 拥塞避免算法
拥塞避免算法
触发条件: 当拥塞窗口 cwnd 超过 慢启动门限 ssthresh 就会进入 拥塞避免算法。 拥塞避免算法: 每当收到一个 ACK 时,cwnd 增加 1 / cwnd,就是没经过一个往返时间 RTT 就把发送方的拥塞窗口 cwnd 加 1,而不是加倍,实现了从 慢启动的指数增长 转变为 线性增长,都是增长阶段,只不过增长的速度缓慢了 。 但这种增长也不是无限增长的,在之后的某个时间,网络就会慢慢进入拥塞的状态,就会发现丢包现象,然后对丢失的数据包进行重传。当触发了 重传机制,也就进入了 拥塞发生算法。
拥塞发生算法
触发条件: 当出现超时重传,就会使用拥塞发生算法,开始重新进行慢启动,减慢增长速度。 将慢启动门限 ssthresh 设为 此时的窗口大小的一半 cwnd/2 cwnd 重置为 1 使用过拥塞发生算法后,就重新进入了慢启动阶段,这时,由于重新开始慢启动,网络中的数据会突然的减少。 这种一旦发生 超时重传,就直接回到解放前的方式太激进,可能会造成网络卡顿现象。 改进: 快速重传
快速回复算法
因为一旦发生超时重传,就会进入慢启动阶段,对整体效率有影响,所以在发生超时重传前,可以先进入快速恢复阶段,真发生了超时重传,再使用拥塞发生算法。 触发条件: 当收到3个重复ACK,就会发生快速重传,使用快速重传算法进行处理。 因为既然能收到3个同样的ACK,说明网络也没有特别糟糕,所以也没有必要向超时重传那样进行处理。 处理方法: 慢启动门限变为此时拥塞窗口的一半 拥塞窗口也减半,然后线性增加
TCP 粘包 和 拆包
TCP 为提高性能,发送端会将需要发送的数据发送到发送缓存,等待缓存满了之后,再将缓存中的数据发送到接收方。同理,接收方也有接收缓存这样的机制,来接收数据。 1. 发生 TCP 粘包和拆包的原因: (1)要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包 (2)待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包 (3)应用程序写入数据小于剩余缓存大小,网卡将应用多次写入的数据先缓存起来,然后一起发送到网络上,这将会发生粘包 (4)接收数据端的应用层没有及时读取接收缓存中的数据,将发生粘包 2. TCP 粘包和拆包的解决 (1)设置定长消息,服务端每次读取既定长度的内容作为一条完整消息 (2)设置消息边界,数据结尾尾增加特殊字符分割 (3)使用带消息头的协议,消息头存储消息开始标识及消息长度信息,接收方获取消息头的时候解析出消息长度,然后向后读取该长度的内容
UDP
TCP 和 UDP 的区别

网络层
IP
操作系统
用户态 与 内核态
讲一下用户态 与 内核态
内核 与 应用程序: (1)内核:一种特殊的应用程序,用于控制计算机的硬件资源,例如:协调CPU资源,分配内存资源,读写IO等; (2)应用程序:用户的程序代码 系统调用: 内存也分为用户空间和内核空间,用户态空间是应用用程序运行的空间,同时,应用程序是不能直接使用计算机的底层资源和内核空间的,而为了让应用程序能够访问到内核管理的资源,例如CPU,内存,I/O,内核必须提供一组访问接口,这些接口就叫系统调用。  库函数 与 shell: 库函数对系统调用进行封装,提供接口给用户,增强了程序的灵活性,减轻了程序员的负担,并且可以直接使用系统调用访问资源; shell是一种特殊的应用程序,即命令行。能方便用户和系统交互,呈现给用户交互窗口 线程运行应用程序时就是处于用户态的,当需要使用一些计算机资源时,线程需要执行系统调用,然后线程就陷入到内核程序去使用这些资源,此时的线程是处于内核态的。 用户态到内核态的转换: 一个进程可以运行在用户态也可以运行在内核态,那它们之间肯定存在用户态和内核态切换的过程。打一个比方:C库接口malloc申请动态内存,malloc的实现内部最终还是会调用brk()或者mmap()系统调用来分配内存。 那为问题又来了,从用户态到内核态到底怎么进入?只能通过系统调用吗?还有其他方式吗? 从用户态到内核态切换可以通过三种方式: 系统调用:本质是软中断 异常:如果当前进程运行在用户态,如果这个时候发生了异常事件,就会触发切换。例如:缺页异常。 中断:当外设完成用户的请求时,会向CPU发送中断信号。
用户态 与 内核态 的切换
进程 与 线程
进程
进程通信方式
每个进程的⽤户地址空间都是独⽴的,⼀般⽽⾔是不能互相访问的,但内核空间是每个进程都共享的,所以进程之间要通信必须通过内核。
无名/命名管道
管道传输数据是单向的,想要相互通信,需要创建两个管道。 匿名管道,没有名字,用完就销毁;是种特殊文件,只存在内存,不存在文件系统 命名管道,使用前先创建命名 管道通信方式效率低,不适合进程间频繁地交换数据。 匿名管道创建: 使用 pipe 函数创建管道,返回两个文件描述符,一个管道有两个文件描述符,一个写端,一个读端。 匿名管道:通信范围是存在⽗⼦关系的进程。子进程只能通过 fork 来复制⽗进程 fd ⽂件描述符,来达到通信的⽬的; 命名管道:可以在不相关的进程间相互通信。因为命令管道,提前创建了⼀个类型为管道的设备⽂件,在进程⾥只要使⽤这个设备⽂件,就可以相互通信
消息队列
管道的通信效率低,不适合进程间频繁地交换数据。 可以使用消息队列解决这个问题。 例如,A 进程把要发送地消息数据放在对应的消息队列后就正常返回,然后 B 进程在需要的时候去读取数据即可。 消息队列是保存在内核中的消息链表,消息体之间相互独立,并且包含了发送方和接收方约定好的自定义消息数据类型。 消息队列的缺点: 通信不及时 不适合大数据的传输 通信过程中,存在用户态与内核态之间的数据拷贝开销(运行在用户态的进程访问消息队列要进入内核态)
共享内存
消息队列的读取和写⼊的过程,都会有发⽣⽤户态与内核态之间的消息拷⻉过程。 而共享内存的机制是:两个进程访问自己的虚拟内存空间,映射到物理内存其实是同一块区域,这样,就避免了数据之间的拷贝搓成,大大提高了进程间通信的速度。
信号量
信号量机制是为了防⽌多进程竞争共享资源,⽽造成的数据错乱的一种保护机制,使得共享的资源,在任意时刻只能被⼀个进程访问 信号量其实是⼀个整型的计数器,主要⽤于实现进程间的互斥与同步,⽽不是⽤于缓存进程间通信的数据。 信号量表示资源的数量,控制信号量的⽅式有两种原⼦操作 P、V操作
信号
以上的通信方式都是进程间通信正常工作下的常规状态。 对于异常情况下的⼯作模式,就需要⽤「信号」的⽅式来通知进程。 信号是进程间通信机制中唯⼀的异步通信机制,因为可以在任何时候发送信号给某⼀进程,⽤户进程对信号的处理⽅式: 执⾏默认操作:Linux 的每种信号都有默认操作 捕捉信号:为信号定义⼀个信号处理函数,当信号发⽣时,就执⾏相应的信号处理函数。 忽略信号:忽略信号,不做任何处理。
同一主机通信方式
socket
不同主机通信方式
线程
线程通信方式
线程间的通信⽬的主要是⽤于线程同步。所以线程没有像进程通信中的⽤于数据交换的通信机制。 同⼀进程的不同线程共享同⼀份内存区域,所以线程之间可以⽅便、快速地共享信息。只需要将数据复制到共享 (全局或堆)变量中即可。但是需要避免出现多个线程试图同时修改同⼀份信息。
死锁
死锁概念
多线程之间竞争共享资源,导致线程之间都想要对方手中一获取到的资源,在没有外力的作用下,线程会一直等下去,等待对方释放资源,就导致后续操作没办法运行,这种情况就是 死锁。
发生的4个必要条件
互斥条件 持有并等待条件 不可剥夺条件 环路等待条件 1.互斥 多个线程不能同时访问同⼀个共享资源 2.拥有并等待 线程 A 已持有了资源 1,想申请资源 2,但资源 2 已被线程 B 持有,这时线程 A 就会处于等待状态,等待资源 2 并同时持有资源 1 不会释放。 3.不可剥夺 当线程已经持有了资源 ,在⾃⼰使⽤完之前不能被其他线程获取 4.环路等待 在死锁发⽣的时候,多个线程获取资源的顺序构成了环形链
死锁的解决
只要破坏 4 个必要条件中的一个就可以解决死锁。 1.死锁预防: (1)互斥: 不能破坏,因为这是共享资源的特性 (2)拥有并等待 一次性地分配所有线程所需要的资源(会降低资源的利用率) (3)不可剥夺 让线程释放已经申请的资源(实现复杂,代价较大) (4)环路等待 使用资源有序分配法来分配资源(给资源编号,并且只能申请比当前资源序号大的资源) 2.避免死锁 使用银行家算法,在使用前进行判断,只允许不会产生死锁的进程申请资源 3.死锁解除 当发生死锁,需要对系统采取相应措施,例如:抢占死锁线程的资源,终止或撤销线程等
协程
进程与线程的区别
Linux
常见重要命令
find
grep
使用
1. grep 是搜索包含特定字符串的行,常在日志中找信息 (--color 选项:高亮查找的字符串)  2. grep 反查技能:(-v 选项) 把不包含指定字符串的行显示出来  3. 展示行号和统计行号 (1)展示行号:-n 选项  (2)统计行号:-c 选项  4. 不区分大小写(-i 选项)  5. 看看前/后 n 行 (1)看前 n 行(-B 选项,Before)  (2)看后 n 行(-A 选项,After)  (3)前后一起看 n 行(-C 选项,不能直接写 -AB,可以写 -A n -B m)  6. 支持正则表达式
awk
sed
sed,stream editor,流编辑器 以”行“为处理单位,每次处理一行内容,处理时,sed 会把要处理的行 存储在缓冲区中,接着用 sed 命令处理缓冲区中的内容,处理完成后,把缓冲区的内容送往屏幕,接着处理下一行,就这样不断重复,知道文件末尾。(这个缓冲区被称为”模式空间“) 处理过程总,sed 命令不会对文件本身进行任何更改。
netstat
netstat 查看网络状态命令,既可以查看到本机开启的端口,也可以查看有哪些客户端连接。 命令格式: netstat [选项] 选项: -a:列出所有网络状态,包括 socket 程序 -c:单位,秒,指定每 c 秒刷新一次网络状态 -n:使用 IP 地址和 端口号 显示,不使用域名与服务名 -p:显示 PID 和程序名 -t:显示使用 TCP 协议端口的连接状况 -u:显示使用 UDP 协议端口的连接状况 -l:仅显示 监听状态(Listen) 的连接 -r:显示路由表 【例1】查看本机开启的端口:-tuln 因为使用了 -l 选项,所以智能看到处于 监听状态Listen 的连接,而不能看到 已连接状态established 的连接  加上 -p 选项,最后还能显示程序的进程号PID和程序名  【例2】查看所有连接,并以IP地址和端口号显示 
ps
在不同的 Linux 发行版本上,ps (pthread state)命令的语法是不太相同的,所以就记了几个固定重要的选项: 格式: pa [选项] 选项:(有些选项不带 - ) a:显示一个终端的所有进程 u:显示进程的归属用户及内存的使用情况 x:显示没有控制终端的进程 -l:长格式显示进程的详细信息 -e:显示所有进程 记住下面2,3个用法即可 【例1】显示所有进程的信息 ps aux ps -le 上面两个命令都可以,只不过 ps -le 显示的更详细些(还能看到夫进行的信息),一般我用的是 ps aux 显示的信息有(11): USER:该进程是属于那个用户的 PID:进程的ID %CPU:进程占用 CPU 资源的百分比 %MEM:进程占用 内存 资源的百分比 VSZ:占用虚拟内存的大小(KB) RSS:占用实际内存的大小(KB) TTY:该进程在哪个终端运行的,tty1-tty7代表本地控制台终端(tty1~tty6 是本地的字符界面终端,tty7 是图形终端),pts/0 ~ 255 代表虚拟终端,一般是远程连接的终端 STAT:进程状态 START:进程的启动时间 TIME:进程占用 CPU 的运算时间 COMMAND:产生这个进程的命令名 【例2】不想看到所有进程,指向查看当前登录产生了哪些进程 ps -l
数据库
数据库原理(MySQL)
事务
事务 4 大特性
事务 4 大特性:ACID 1. 原子性(Atomicity) 事务是最小的执行单位,不允许分割,事务中的所有操作要么全部完成,要么都没完成,不能结束在中间环节。如果事务在执行过程中发生错误,会被回滚到事务开始前的状态。 2. 一致性(Consistency) 事务在执行前后,数据都保持一致。在一致性状态下,所有事务对同一个数据的读取结果都是相同的。 3. 隔离性(Isolation) 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的; 4. 持久性(Durability) 一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃或故障也不会对其有任何影响。
数据库事务的实现原理
事务的原子性: 使用 undo log(回滚日志) :回滚可以用回滚日志来实现,回滚日志记录着事务所执行的修改操作,在回滚时反向执行这些修改操作即可 事务的持久性: 使用 redo log(重做日志):系统发生崩溃可以用重做日志进行恢复,从而实现持久性。与回滚日志记录数据的逻辑修改不同,重做日志记录的是数据页的物理修改。 事务的隔离性: 通过 锁机制、MVCC 等手段( 默认支持的隔离级别是 REPEATABLE-READ )。 事务的一致性: 保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。
4 大特性之间的关系
事务的 ACID 不是一种平级关系: 持久化是为了能应对系统崩溃的情况 一致性是为了让事务的执行结果是正确的,但要满足一致性,首先要满足原子性和隔离性: 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时只要能满足原子性,就一定能满足一致性; 在并发的情况下,多个事务并行执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性; 
数据库的 并发一致性 问题
由于事务之间可以并发执行,所以在并发环境下,事务的隔离性很难保证(可能多个用户对同一数据进行操作),所以会出现很多并发一致性问题。 1. 并发一致性问题: (1)脏读(Dirty read): 一个事务访问了另一个数据修改但还没提交的数据,这个还没提交的数据就是“脏数据”,对其进行操作可能会产生错误的结果; (2)丢失修改(Lost to modify): 事务 A 读取一个数据时,事务 B 也访问了这个数据 如果事务 A 修改了这个数据后,事务 B 也修改了这个数据。那么事务 A 对这个数据的修改结果就丢失了 (3)不可重复读(Unrepeatable read): 事务 A 需要多次读取同一数据,在多次读取的过程中,事务 B 也访问了这个数据并对这个数据进行了修改,导致事务 A 多次读取该数据的结果不一样,就是不可重复读 (4)幻读(Phantom read): 属于不可重复读的情况,在事务 A 读取了几行数据(某个范围的数据),另一个并发事务 B 在这个范围内插入了一些数据时。 在之后 A 再次访问时,就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。 (也属于前后读取的结果不同) 不可重复读和幻读区别: 不可重复读的重点是修改,幻读的重点在于新增或者删除。 2. 产生并发不一致性的主要原因: 破坏了事务的隔离性 3. 解决方法: (1)并发控制(用户实现): 通过并发控制来保证隔离性。并发控制可以通过封锁来实现,但是封锁操作需要用户自己控制,相当复杂 (2)隔离级别(数据库实现): 数据库管理系统提供了事务的隔离级别,让用户以一种更轻松的方式处理并发一致性问题
数据库的隔离级别
数据库的隔离级别:(解决并发一致性的隔离级别) RU 读不提交(Read Uncommited) 事务中数据的修改,即使没有提交,其他事务也可以访问;最低的隔离级别 RC 读提交(Read Commited) 一个事务所做的修改在提交之前对其他事务是不可见的;但在同⼀个事务中,前后读取的数据结果可能不同 RR 可重复读(Repeatable Read) 保证在同一事务中多次读取同一数据的结果是一样的 serializable串⾏化 使用锁机制实现,强制事务串行执行,这样多个事务之间就互不干扰,不会出现并发一致性问题 
锁机制
1. 数据库的锁: 当数据库的事务并发执行时,保证数据的访问顺序的机制称为锁机制 应该尽量只锁定需要修改的那部分数据,而不是所有的资源。锁定的数据量越少,发生锁争用的可能就越小,系统的并发程度就越高。 但是加锁需要消耗资源,锁的各种操作(包括获取锁、释放锁、以及检查锁状态)都会增加系统开销。因此封锁粒度(锁的数据量)越小,系统开销就越大。 2. 数据库锁的类型: MySQL 中提供了两种封锁粒度:行级锁以及表级锁。 (1)行级锁:锁粒度大,对当前操作的整张表加锁 优点:实现简单,资源消耗也比较少,加锁快,不会出现死锁 缺点:锁定粒度最大,触发锁冲突的概率最高,并发度最低 (2)表级锁:锁粒度小,只针对当前操作的行加锁 优点:行级锁能大大减少数据库操作的冲突 缺点:其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁
索引
Redis
redis 介绍
redis为什么那么快?
基于内存:Redis是内存数据库,所有操作都在内存上进行,没有磁盘IO上的开销; 高效的数据结构:Redis 每种数据类型底层都做了优化,在对数据进行增删改查操作时能更高效地进行处理; 单线程实现( Redis 6.0以前):Redis使用单个线程处理请求,避免了多个线程之间线程切换和锁资源争用的开销。 IO多路复用模型:Redis 采用 IO 多路复用技术。Redis 使用单线程来轮询描述符,将数据库的操作都转换成了事件,不在网络I/O上浪费过多的时间。
redis 数据结构
SDS(简单动态字符串)
为什么不用C语言的字符串?
1. C 语言字符串的不足之处:(也是SDS与C字符串的区别所在) (1)获取字符串长度时间复杂度 C 字符串:O(n),必须遍历整个字符串,直到遇到结束标识符结束 SDS :O(1),有一个记录字符串长度的数据成员 (2)不能包含结束符 '\0' C 字符串:以结束符 '\0' 字符结尾,字符串内不能包含有 '\0' 字符,因此不能保存像图片,音频,视频这样的二进制数据; SDS:使用len属性来判断字符串是否结束,所以部队对其中的数据做限制 (3)缓冲区溢出 C 字符串:不记录自身的缓冲区大小,所以字符串操作函数不高效且不安全,操作时可能会导致缓冲区溢出,进而造成程序运行终止; SDS:SDS 的空间分配策略完全杜绝了发生缓冲区溢出的可能性:当 SDS API 需要对SDS进行修改时,API会先检查SDS的空间是否满足修改所需的要求,如果不满足的话,API会自动将SDS的空间扩展至所需的大小,然后才执行实际的修改操作,所以使用SDS既不需要手动修改SDS的空间大小,也不会出现前面所说的缓冲区溢出问题 同时,SDS 在底层仍会使用 C 字符串,也就可以兼容部分 C 字符串函数,从而避免了不必要的代码重复 2. C 字符串和 SDS 之间的区别: 
SDS 的底层实现与结构设计
1. 底层实现: struct sdshdr { // 1. 字节数组,用于保存字符串 char buf[]; // 2. 字符串的长度,即 buf 数组中已使用字节的数量 int len; // 3. 剩余空间,即 buf 数组中未使用字节的数量 int free; };   2. 结构设计: 
空间分配策略
1. C 语言和 SDS 的空间分配策略区别: (1)C 语言的不足之处:C语言在处理字符串的空间长度变化操作时,可能会进行内存的重分配,然后将新字符串的值保存在新的内存中。 因为 Redis 常被用于速度要求严苛、数据被频繁修改的场合,若使用 C 语言字符串格式,如果频繁地修改字符串的长度造成内存重分配,那么就会对数据库性能造成影响 (2)SDS 的改进:在 C 语言中,字符串长度和底层数组长度几乎是同一种东西(相差1); 但是,在 SDS 中,引入了一个 未使用空间free 属性 解除了字符串长度和底层数组长度之间的关系,而是底层数组长度包含字符串长度和未使用空间长度(加1) 2. SDS 空间分配策略 SDS 通过 未使用空间,是实现了 空间预分配策略 和 惰性空间策略释放 两种优化策略。 (1)空间预分配策略:用于优化SDS的字符串增长操作 当 SDS 的 API 对一个 SDS 进行修改,并且需要对 SDS 进行空间扩展的时候,程序不仅会为 SDS 分配修改所必须要的空间,还会为 SDS 分配额外的未使用空间 额外分配的未使用空间数量由下规则决定: 若 SDS 修改之后,len < 1MB,那么程序分配 和 len 大小相同的未使用空间,即 len == free 【例】如果 SDS 进行修改之后,SDS 的 len==13B < 1MB,那么程序会额外分配 13B 的未使用空间,SDS 的 buf 数组的实际长度将变成 13+13+1=27B(额外的 1B 用于保存空字符)。 若 SDS 修改之后,len >= 1MB,那么程序会分配 1MB 的未使用空间,即 free == 1MB 【例】如果 SDS 进行修改之后,SDS的 len == 30MB,那么程序会分配 1MB 的未使用空间,SDS 的 buf 数组的实际长度将为 30MB+1MB+1B 通过空间预分配策略,Redis 可以减少连续执行字符串增长操作所需的内存重分配次数,将连续增长N次字符串所需的内存重分配次数从 必定N次 降低为 最多N次 【示例】对下面的 SDS 执行 sdscat(s,"Cluster")  那么 sdscat 将执行一次内存重分配操作,将SDS的长度修改为13字节,并将SDS的未使用空间同样修改为13字节,如下图所示  如果这时,我们再次对 s 执行 sdscat(s,"Tutorial")。那么这次 sdscat 将不需要执行内存重分配,因为未使用空间里面的 13 字节足以保存9字节 的"Tutorial",执行sdscat之后的SDS如下图所示  (2)惰性空间释放策略:用于优化SDS的字符串缩短操作 当 SDS 的 API 需要缩短 SDS 保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用 free 属性将这些字节的数量记录起来,并等待将来使用 通过惰性空间释放策略,SDS避免了缩短字符串时所需的内存重分配操作,并为将来可能有的增长操作提供了优化。并且,SDS 也提供了相应的 API 在合适的时机真正地释放 SDS 的未使用空间,也避免了内存的浪费。 【示例】sdstrim 函数接受一个 SDS 和一个 C 字符串作为参数,移除 SDS 中所有在 C 字符 串中出现过的字符 对下面的 SDS 执行 sdstrim(s, "XY") 操作,将移除SDS字符串中所有的 'X' 和 'Y'  会将 SDS 修改成下图所示的样子。注意执行 sdstrim 之后的 SDS 并没有释放多出来的 8 字节空间,而是将这 8 字节空间作为未 使用空间保留在了 SDS 里面,如果将来要对 SDS 进行增长操作的话,这些未使用空间就可能会派上用场 
链表
链表 介绍
C 语言本身没有链表这个数据结构的,所以 Redis 自己设计了一个链表数据结构。 链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活地调整链表的长度。 链表在 redis 中的应用场景: ①链表在Redis中的应用非常广泛,比如列表键的底层实现之一就是链表。当一个列表键包 含了数量比较多的元素,又或者列表中包含的元素都是比较长的字符串时,Redis就会使用链表作为列表键的底层实现 【示例】以下展示的integers列表键包含了从1到1024共一千零二十四个整数(integers列表键的底层实现就是一个链表,链表中的每个节点都保存了一个整数值):  ②除了链表键之外,发布与订阅、慢查询、监视器等功能也用到了链表 ③Redis服务器本身还使用链表来保存多个客户端的状态信息 ④使用链表来构建客户端输出缓冲区(output buffer)
链表的底层实现与结构设计
1. 底层实现 (1)链表节点 从节点设计可以看出,redis 的链表设计是双向链表 typedef struct listNode { // 1. 前置节点 struct listNode * prev; // 2. 后置节点 struct listNode * next; // 3. 节点的值 (注意,这里的链表值是 void* 类型,表明可以存放任意类型的元素值) void * value; } listNode;  (2)链表 typedef struct list { // 1. 表头节点 listNode * head; // 2. 表尾节点 listNode * tail; // 3. 链表所包含的节点数量 unsigned long len; // 4. 节点值复制函数:用于复制链表节点所保存的值 void *(*dup)(void *ptr); // 5. 节点值释放函数:用于释放链表节点所保存的值 void (*free)(void *ptr); // 6. 节点值对比函数:用于对比链表节点所保存的值和另一个输入值是否相等 int (*match)(void *ptr,void *key); } list; 
链表的特性
双端:获取某个节点的 前置节点 和 后置节点 的复杂度:O(1) 表头指针和表尾指针:获取链表的 表头节点 和 表尾节点 的复杂度:O(1) 链表长度计数器:获取链表中节点数量的复杂度:O(1) 多态:链表节点使用 void* 指针来保存节点值,所以链表可以用于保存各种不同类型的值 无环:表头节点的prev指针和表尾节点的next指针都指向NULL,对链表的访问以 NULL为终点,所以链表是无环的
字典(哈希表实现)
字典 介绍
字典(哈希表)是一种用于保存键值对(key-value pair)的抽象数据结构,字典中的每个键都是独一无二的,可以在字典中根据键查找与之关联的值,或者通过键来更新值,又或者根据键来删除整个键值对,等等 字典 在 redis 中的应用场景: 1. 用来实现数据库: Redis 数据库就是使用字典来作为底层实现的, 对数据库的增、删、查、改操作也是构建在对字典的操作之上的 【示例】执行下面命令——在数据库中创建一个键为"msg",值为"hello world"的键值对时,这个键值对就是保存在代表数据库的字典里面的: redis> SET msg "hello world" OK 2. 用来实现哈希键: 字典还是哈希键的底层实现之一,当一个哈希键包含的键值对比较多,又或者键值对中的元素都是比较长的字符串时,Redis就会使用字典作为哈希键的底层实现 【示例】website是一个包含10086个键值对的哈希键,这个哈希键的键都是一些数据 库的名字,而键的值就是数据库的主页网址:  website键的底层实现就是一个字典,字典中包含了10086个键值对 键值对的键为"Redis",值为"Redis.io" 键值对的键为"MariaDB",值为"MariaDB.org" 键值对的键为"MongoDB",值为"MongoDB.org" ...
字典 的底层实现与结构设计
1. 底层实现: Redis 的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对  (1)哈希表节点 struct dictEntry 实现 typedef struct dictEntry { // 1. 键 void *key; // 2. 值:键值对的值可以是一个指针,或是一个 uint64_t 整数,或是一个 int64_t 整数 union{ void *val; uint64_tu64; int64_ts64; } v; // 3. 指向下个哈希表节点,形成链表 struct dictEntry *next; } dictEntry; (2)哈希表 struct dictht 实现 typedef struct dictht { // 1. 哈希表数组 dictEntry **table; // 2. 哈希表大小:记录了哈希表的大小,也即是table数组的大小 unsigned long size; // 3. 哈希表大小掩码,用于计算索引值。值总是等于 size - 1 unsigned long sizemask; // 4. 该哈希表已有节点的数量 unsigned long used; } dictht; (3)字典 struct dict 实现  typedef struct dict { // 1. 类型特定函数(每个dictType结构保存了一簇用于操作特定类型键值对的函数,Redis会为用途不同的字典设置不同的类型特定函数) dictType *type; // 2. 私有数据:保存了需要传给那些类型特定函数的可选参数 void *privdata; // 3. 哈希表:包含两个哈希表的哈希表,一般情况下, 字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]哈希表进行rehash时使用 dictht ht[2]; // 4. rehash 索引。当rehash 不在进行时,值为-1 in trehashidx; // 记录了rehash目前的进度,如果目前没有在进行 rehash,那么其值为 -1 } dict; typedef struct dictType { // 1. 计算哈希值的函数 unsigned int (*hashFunction)(const void *key); // 2. 复制键的函数 void *(*keyDup)(void *privdata, const void *key); // 3. 复制值的函数 void *(*valDup)(void *privdata, const void *obj); // 4. 对比键的函数 int (*keyCompare) (void *privdata, const void *key1, const void *key2); // 5. 销毁键的函数 void (*keyDestructor)(void *privdata, void *key); // 6. 销毁值的函数 void (*valDestructor)(void *privdata, void *obj); } dictType;
哈希算法
哈希冲突 的解决
整数集合
整数集合 介绍
整数集合是 Set 对象的底层实现之一。 当一个 Set 对象只包含整数值元素,并且元素数量不大时,就会使用整数集这个数据结构作为底层实现。 【示例】创建一个只包含 5 个元素的集合键,并且集合中的所有元素都是整数值,那么这个集合键的底层实现就会是整数集合: redis> SADD numbers 1 3 5 7 9 (integer) 5 redis> OBJECT ENCODING numbers "intset"
整数集合 的底层实现与结构设计
1. 底层实现: 本质上是一块连续内存空间,结构如下: typedef struct intset { // 1. 编码方式 uint32_t encoding; // 2. 集合包含的元素数量 uint32_t length; // 3. 保存元素的数组:存放的真正类型取决于 编码方式 enconding 属性的值 int8_t contents[]; } intset; 2. 编码方式 encoding: 如果 encoding 属性值为 INTSET_ENC_INT16,那么 contents 就是一个 int16_t 类型的数组,数组中每一个元素的类型都是 int16_t; 如果 encoding 属性值为 INTSET_ENC_INT32,那么 contents 就是一个 int32_t 类型的数组,数组中每一个元素的类型都是 int32_t; 如果 encoding 属性值为 INTSET_ENC_INT64,那么 contents 就是一个 int64_t 类型的数组,数组中每一个元素的类型都是 int64_t;
整数集合 的升级操作
整数集合中的数组可以存放类型为 int16_t、int32_t 或 int64_t 的整数值,并且保证集合中不会出现重复元素 当要存放的新元素比当前整数集合中的元素类型所占位数长时,就需要进行整数集合的升级操作,然后才能将新元素加入到集合里面 1. 升级整数集合并添加新元素共分为三步进行: (1)空间扩容: 根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间 (2)原元素类型转换及位置移动: 将底层数组现有的所有元素都转换成与新元素相同的类型,并将类型转换后的元素放置到正确的位上,而且在放置元素的过程中,需要继续维持底层数组的有序性质不变 (3)新元素添加: 将新元素添加到底层数组里面 2. 【示例】 假设有一个整数集合里有 3 个类型为 int16_t 的元素,往这个整数集合中加入一个新元素 65535(需用int32_t 类型来保存)  (1)空间扩容: 整数集合要进行升级操作,首先需要为 contents 数组扩容,在原本空间的大小之上再扩容多 80 位(4x32-3x16=80),这样就能保存下 4 个类型为 int32_t 的元素。  (2)原元素类型转换及位置移动: 空间扩容后,将之前的 3 个 int16_t 元素转换为 int32_t 类型,并将转换后的元素放置到正确的位上面,并且需要维持底层数组的有序性不变,整个转换过程如下:  (3)新元素添加: 上图的第四步 3. 整数集合支持降级操作吗? 不支持降级操作,一旦对数组进行了升级,就会一直保持升级后的状态。比如前面的升级操作的例子,如果删除了 65535 元素,整数集合的数组还是 int32_t 类型的,并不会因此降级为 int16_t 类型
整数集合 的优点
整数集合升级的好处是节省内存资源 & 提高灵活度: (1)节约内存: 如果要让一个数组同时保存 int16_t、int32_t、int64_t 类型的元素,最简单做法就是直接使用 int64_t 类型的数组。不过这样的话,当如果元素都是 int16_t 类型的,就会造成内存浪费的情况; (2)提高灵活度: 整数集合升级就能避免这种情况,如果一直向整数集合添加 int16_t 类型的元素,那么整数集合的底层实现就一直是用 int16_t 类型的数组,只有在我们要将 int32_t 类型或 int64_t 类型的元素添加到集合时,才会对数组进行升级操作。
跳表
跳表 介绍
链表在查找元素的时候,因为需要逐一查找,所以查询效率非常低,时间复杂度是O(N),于是就出现了跳表。 跳表是在链表基础上改进过来的,实现了一种「多层」的有序链表,这样的好处是能快读定位数据,查找数据的平均时间复杂度为 O(logN) (最坏时间复杂度为O(N),变成了链表) 跳表应用场景:redis 只在两个地方用到了跳跃表 (1)实现有序集合键 (2)在集群节点中用作内部数据结构 【示例】fruit-price 是一个有序集合键,这个有序集合中每个成员“以水果名为成员,以水果价格为分值”,该有序集合中有130个元素  fruit-price 有序集合中的所有元素都保存在一个跳跃表中,其中每个跳跃节点(node)都保存了一款水果的价钱信息,所有水果按价钱的高低从低到高在跳跃表里面排序 跳跃表的第一个元素的成员为“banana”,分值为5 跳跃表的第二个元素的成员为“cherry”,分值为6.5 跳跃表的第三个元素的成员为“apple”,分值为8
跳表 的底层实现与结构设计
底层实现:  (1)跳跃表节点 struct zskiplistNode(蓝色节点) typedef struct zskiplistNode { // 1. 层:不同层的下一个节点可能是不一样的 struct zskiplistLevel { // 1.1 前进指针:用于从表头向表尾方向访问节点 struct zskiplistNode *forward; // 1.2 跨度:两个节点之间的距离 unsigned int span; } level[ ]; // 2. 后退指针:用于从表尾向表头方向访问节点 struct zskiplistNode *backward; // 3. 分值:节点的分值是一个double类型的浮点数,跳跃表中的所有节点都按分值从小到大来排序 double score; // 4. 成员对象:节点的成员对象是一个指针,它指向一个字符串对象,而字符串对象则保存着一个SDS值 robj *obj; } zskiplistNode; 跳跃表节点的level数组可以包含多个元素,每个元素都包含一个指向其他节点的指针, 程序可以通过这些层来加快访问其他节点的速度,一般来说,层的数量越多,访问其他节点 的速度就越快; 每次创建一个新跳跃表节点的时候,程序都根据幂次定律(power law,越大的数出现的 概率越小)随机生成一个介于1和32之间的值作为level数组的大小,这个大小就是层的“高 度” 因为C语言的数组索引总是从0开始 的,所以节点的第一层是level[0],而第二层是level[1],以此类推 初看上去,很容易以为跨度和遍历操作有关,但实际上并不是这样,遍历操作只使用前 进指针就可以完成了,跨度实际上是用来计算排位(rank)的:在查找某个节点的过程中, 将沿途访问过的所有层的跨度累计起来,得到的结果就是目标节点在跳跃表中的排位。 两个节点之间的跨度越大,它们相距得就越远。指向NULL的所有前进指针的跨度都为0,因为它们没有连向任何节点。 后退指针每次只能后退一个节点,而前进指针一次可以跳过多个节点; 每个节点的成员对象都是唯一的,但是节点间的分值可以相同。相同分值的节点按照成员对象在字典序中的大小进行排序。(分值小的,字典序小的节点靠近表头)  (2)跳跃表 实现(头节点) typedef struct zskiplist { // 1. 表头节点和表尾节点:通过这两个指针,定位表头节点和表尾节点的复杂度为O(1) structz skiplistNode *header, *tail; // 2. 表中节点的数量:记录节点的数量,可在O(1)复杂度内返回跳跃表的长 度 unsigned long length; // 3. 表中层数最大的节点的层数:在O(1)复杂度内获取跳跃表中层高最大的那个节点的层数量,表头节点的层高不计在内 int level; } zskiplist; 结构设计: a. 仅靠多个跳跃表节点就可以组成一个跳跃表  b. 通过使用一个zskiplist结构来持有这些节点,更方便地对整个跳跃表进行处理,如快速访问跳跃表的表头节点和表尾节点,或快速地获取跳跃表节点的数量(即跳跃表的长度)等信息 
跳表 节点查询过程
跳表节点查询过程 查找一个跳表节点的过程时,跳表会从头节点的最高层开始,逐一遍历每一层。在遍历某一层的跳表节点时,会用跳表节点中的 SDS 类型的元素和元素的权重来进行判断,共有两个判断条件: 如果当前节点的权重「小于」要查找的权重时,跳表就会访问该层上的下一个节点。 如果当前节点的权重「等于」要查找的权重时,并且当前节点的 SDS 类型数据「小于」要查找的数据时,跳表就会访问该层上的下一个节点。 如果上面两个条件都不满足,或者下一个节点为空时,跳表就会使用目前遍历到的节点的 level 数组里的下一层指针,然后沿着下一层指针继续查找,这就相当于跳到了下一层接着查找。 举个例子,下图有个 3 层级的跳表。  如果要查找「元素:abcd,权重:4」的节点,查找的过程是这样的: 先从头节点的最高层开始,L2 指向了「元素:abc,权重:3」节点,这个节点的权重比要查找节点的小,所以要访问该层上的下一个节点; 但是该层上的下一个节点是空节点,于是就会跳到「元素:abc,权重:3」节点的下一层去找,也就是 leve[1]; 「元素:abc,权重:3」节点的 leve[1] 的下一个指针指向了「元素:abcde,权重:4」的节点,然后将其和要查找的节点比较。虽然「元素:abcde,权重:4」的节点的权重和要查找的权重相同,但是当前节点的 SDS 类型数据「大于」要查找的数据,所以会继续跳到「元素:abc,权重:3」节点的下一层去找,也就是 leve[0]; 「元素:abc,权重:3」节点的 leve[0] 的下一个指针指向了「元素:abcd,权重:4」的节点,该节点正是要查找的节点,查询结束。 #跳表节点层数设置 跳表的相邻两层的节点数量的比例会影响跳表的查询性能。 举个例子,下图的跳表,第二层的节点数量只有 1 个,而第一层的节点数量有 6 个。  这时,如果想要查询节点 6,那基本就跟链表的查询复杂度一样,就需要在第一层的节点中依次顺序查找,复杂度就是 O(N) 了。所以,为了降低查询复杂度,我们就需要维持相邻层结点数间的关系。 跳表的相邻两层的节点数量最理想的比例是 2:1,查找复杂度可以降低到 O(logN)。 下图的跳表就是,相邻两层的节点数量的比例是 2 : 1。  那怎样才能维持相邻两层的节点数量的比例为 2 : 1 呢? 如果采用新增节点或者删除节点时,来调整跳表节点以维持比例的方法的话,会带来额外的开销。 Redis 则采用一种巧妙的方法是,跳表在创建节点的时候,随机生成每个节点的层数,并没有严格维持相邻两层的节点数量比例为 2 : 1 的情况。 具体的做法是,跳表在创建节点时候,会生成范围为[0-1]的一个随机数,如果这个随机数小于 0.25(相当于概率 25%),那么层数就增加 1 层,然后继续生成下一个随机数,直到随机数的结果大于 0.25 结束,最终确定该节点的层数。 这样的做法,相当于每增加一层的概率不超过 25%,层数越高,概率越低,层高最大限制是 64
redis 持久化
redis 持久化介绍
1. redis 持久化机制 (1)redis 为什么需要持久化机制? 因为Redis是内存数据库,它将自己的数据库状态储存在内存里面,所以如果不想办法将储存在内存中的数据库状态保存到磁盘里面,那么一旦服务器进程退出,服务器中的数据库状态也会消失不见 (2)为了解决这个问题,redis 提供了两种持久化机制,将redis 的数据通过 AOF 或 RDB 方式保存在磁盘里里面,避免数据意外丢失。 2. 两种持久化机制: redis 是内存数据库,但是它为数据的持久化提供了两个技术: AOF 日志 和 RDB 快照 这两种技术都会用各用一个日志文件来记录信息,但是记录的内容是不同的。 AOF 文件的内容:写操作命令,记录的不是实际数据 因为恢复时还要额外执行操作命令,所以恢复效率没有 RDB 方式快 RDB 文件的内容:数据库一瞬间的内存数据,记录的是实际数据 因为记录的是数据库的实际数据,所以直接将 RDB 文件读入内存就可以,数据恢复效率快
AOF 持久化
AOF 持久化介绍
两种持久化机制的不同: RDB 持久化通过保存数据库中的键值对来记录数据库状态不同 AOF 持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的 AOF redis 默认是开启RDB 持久化机制,如果想要开启 AOF 持久化机制,可以通过修改 redis.conf 配置文件中的参数开启: // redis.conf appendonly yes // 表示是否开启 AOF 持久化(默认 no,关闭) appendfilename "appendonly.aof" // AOF 持久化文件的名称  1. AOF 持久化介绍: redis 每执行一条写操作命令,就把该命令以追加的方式写入到一个 AOF 文件里,然后重启 Redis 的时候,就读取 AOF 文件里的命令,并且执行它,就相当于恢复缓存数据。(并且只会记录写操作命令,读操作命令是不会被记录的) 所以,AOF 持久化是通过保存 redis 服务器所执行的写命令来记录数据库状态的。 2. AOF 文件 AOF 日志文件就是一种普通的文本文件,只不过在存放内容时,有一定的规则 【示例】对空白的数据库执行以下写命令,那么数据库中将包含三个键值对:  RDB 持久化保存数据库状态的方法:将msg、fruits、numbers 三个键的键值对保存到RDB文件中 AOF 持久化保存数据库状态的方法:将服务器执行的 SET、SADD、 RPUSH 三个命令保存到AOF文件中  (\r 和 \n 是制表符和换行符,其实就是下面形式) *3 // 表示当前命令有三个部分,后面紧跟具体的命令、键或值(用 $ + 数字 表示这部分的长度) $3 SET $3 msg $6 hello ... 开启AOF持久化功能之后,Redis会将你本次在数据库中所操作的写记录追加到一个文件中(系统会自动在第一行添加一个SELECT命令用来指定数据库) 之后,服务器在启动时,可以通过载入和执行AOF文件中保存的命令来还原服务器关闭之前的数据库状态,以下就是服务器载入AOF文件并还原数据库状态时打印的日志: 
redis 写入 AOF 日志的过程
  redis 先执行写操作命令,之后将该命令记录到 AOF 日志文件(AOF 缓冲)中,这么做既有优点也有缺点: (1)优点: 避免额外的检查开销:如果在这条命令执行前就将命令写到 AOF 文件中,如果执行命令出错,redis 在使用 AOF 日志恢复数据时就可能会出错,所以要进行检查,但检查这条命令会消耗额外的检查开销;但是等到命令成功执行后再把这条命令写到AOF日志中,就避免了这种开销; 不会阻塞当前写操作命令的执行:因为是等到命令执行完成后才进行写入,所以肯定不会阻塞命令的执行 (2)缺点: 数据有丢失的风险:因为执行命令和记录日志是两个过程,如果再执行的命令还没写入到硬盘,服务器发生故障,这个数据就可能会丢失 可能会阻塞下一条命令:因为执行命令后并把命令写入到日志都是在主进程中完成的,所以这两个操作是同步的(要前后完成),但是如果再写入到硬盘时,硬盘的IO压力比较大,会导致将日志写入到硬盘时速度很慢,进而造成阻塞,也就导致了后续的命令无法进行 解决方法——三种写回策略: 写操作命令是先写到 AOF 日志文件中的,等到某一时刻,再将 AOF 中的数据写到硬盘中 (参看右上图的过程) 1. redis 写入 AOF 日志的过程 redis 执行完写操作命令后,将写命令追加到 server.aof_buf 缓冲区; 然后通过 write() 系统调用,将 aof_buf 缓冲区的数据写入到 AOF 文件(在内核缓冲区),此时数据并没有写入到硬盘,而是拷贝到了内核缓冲区 page cache,等待内核将数据写入硬盘; 具体内核缓冲区的数据什么时候写入到硬盘,由内核决定。 2. 三种写回策略: (1)Always 策略,总是,每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘 优点:安全性高,保证最大程度数据不丢失 缺点:由于它每执行一条写操作命令就同步将 AOF 内容写回硬盘,所以是不可避免会影响主进程的性能; (2)Everysec 策略,每秒,每次写操作命令执行后,先将命令写到 AOF 文件,然后每隔一秒将缓冲区里的内容写回到硬盘(这个内容的同步的过程专门有一个线程负责执行的) (always 和 no 策略的这种方案) (3)No 策略,不由 Redis 决定,而是将 写回权 交给 操作系统,就是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,再由操作系统决定何时将缓冲区内容写回硬盘。 优点:交由操作系统来决定何时将 AOF 日志内容写回硬盘,相比于 Always 策略性能较好 缺点:操作系统写回硬盘的时机是随机的,如果 AOF 日志内容没有写回硬盘,一旦服务器宕机,就会丢失不定数量的数据 如果要高性能,就选择 No 策略;(效率高,安全性低) 如果要高可靠,就选择 Always 策略;(安全性高,效率低) 如果允许数据丢失一点,但又想性能高,就选择 Everysec 策略。 3. 三种策略的实现: 对于这三种策略是怎么实现,其实是通过控制 一个函数 fsync() 的调用时机 (这个函数的作用就是将 AOF 的内容写入到硬盘) 其实当应用程序向文件写入数据时,内核通常先将数据复制到内核缓冲区中,然后排入队列,然后由内核决定何时写入硬盘。(AOF 文件也是如此) 如果想要应用程序向文件写入数据后,能立马将数据同步到硬盘,就可以调用 fsync() 函数,这样内核就会将内核缓冲区的数据直接写入到硬盘,等到硬盘写操作完成后,该函数才会返回。 Always 策略:每次向 AOF 文件写入数据后,就执行 fsync() 函数; Everysec 策略:创建一个异步任务来执行 fsync() 函数; No 策略:永不执行 fsync() 函数; 
AOF 文件重写
1. 为什么需要 AOF 重写? AOF 日志是一个文件,随着执行的写操作命令越来越多,文件的大小会越来越大。 如果当 AOF 日志文件过大就会带来性能问题,在重启 Redis 后,需要读 AOF 文件的内容以恢复数据,如果文件过大,整个恢复的过程就会很慢。 所以,Redis 为了避免 AOF 文件越写越大,提供了 AOF 重写机制,当 AOF 文件的大过大时,Redis 就会启用 AOF 重写机制。 2. AOF 文件重写 的原理: 通过 AOF 文件重写功能,Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件,新旧两个AOF文件所保存的数据库状态相同,但新AOF文件不会包含任何浪费空间的冗余命令,所以新AOF文件的体积通常会比旧AOF文件的体积要小得多(相当于对 AOF 文件进行压缩) 3. 新 AOF 文件怎么节省占用空间: AOF 重写机制是在重写时,读取当前数据库中的所有键值对,然后将每一个键值对用一条命令记录到「新的 AOF 文件」,等到全部记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件。(注意,依然是读取当前数据库的状态,而不是从旧 AOF 文件中读取) 例如:在没有执行重写机制前,前后执行了 set name yang set name li 这两条命令,但是最终name的结果的键值对肯定是name : li,而在旧AOF文件中,会把这两条命令全部记录下来,但其实第一条命令是没有意义的,因为最终结果由最新的那条决定。 所以,在执行 AOF 重写机制时,只是读取当前数据库的最新状态中的左右键值对,对于一个键值,在重写日志中只用一条命令即可,而不需要记录之前的多条命令。 重写机制的妙处在于:尽管某个键值被多条命令修改多次,最终也只是记录这个键值对的最新状态,然后只需要在重写日志中用一条命令记录键值对。 4. 为什么不直接复用现有的 AOF 文件,而是先写到新的 AOF 文件,然后再覆盖过去? 因为 AOF 重写过程可能会失败,如果失败后,旧的 AOF 文件会被污染,也就没法用于恢复数据。 所以,AOF 重写过程,先重写到新的 AOF 文件,重写失败的话,就直接删除这个文件就好,不会对现有的 AOF 文件造成影响。
AOF 后台重写
1. 为什么有 AOF 后台重写? 在进行 AOF 重写操作(aof_rewrite函数)创建一个新AOF文件时,需要进行大量的写入操作,因为 Redis 服务器是使用单个线程来处理命令请求的,所以如果主线程(服务器线程)执行这个操作(直接调用aof_rewrite函数)时,服务期将无法处理客户端发来的命令请求 所以,为了解决这种问题,redis 使用 AOF 后台重写机制 进行解决,就是Redis决定将AOF重写程序放到子进程里执行。 2. AOF 后台重写 (1)优点: 子进程进行AOF重写期间,服务器进程(父进程)可以继续处理命令请求 子进程带有服务器进程的数据副本,使用子进程而不是线程,原因见下。 不使用线程:因为如果是使用线程,多线程之间会共享内存,那么在修改共享内存数据的时候,需要通过加锁来保证数据的安全,而这样就会降低性能) 使用子进程:创建子进程时,父子进程是共享内存数据的,不过这个共享的内存只能以只读的方式,而当父子进程任意一方修改了该共享内存,就会发生「写时复制」,于是父子进程就有了独立的数据副本,就不用加锁来保证数据安全。) (2)子进程怎么拥有主进程一样的数据副本的? 主进程在通过 fork 系统调用生成 bgrewriteaof 子进程时,操作系统会把主进程的「页表」复制一份给子进程,这个页表记录着虚拟地址和物理地址映射关系,而不会复制物理内存,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。这样,子进程就能共享父进程的物理内存数据了,并且能节省物理内存资源。(页表项的属性会标记该物理内存的权限为只读) 当父进程或者子进程在向这个内存发起写操作时,CPU 就会触发缺页中断,这个缺页中断是由于违反权限导致的,然后操作系统会在「缺页异常处理函数」里进行物理内存的复制,并重新设置其内存映射关系,将父子进程的内存读写权限设置为可读写,最后才会对内存进行写操作,这个过程被称为「写时复制(Copy On Write)」。   (3)写时复制: 只有在发生写操作时,操作系统才复制这块物理内存,这样可以避免在创建子进程就进行物理内存的复制时间过长而导致父进程长时间阻塞。 但是,在创建子进程对页表进行复制的时候依然会阻塞,但是页表的大小相对较小,复制过程也是比较快的。 (4)写时复制过程会出现的问题: 子进程重写过程中,主进程依然可以正常处理命令。 若在重写 AOF 日志过程中,主进程修改了已经存在 key-value,并且在子进程的内存数据和主进程的内存数据也不一致了,这时要怎么办呢? 为了解决这种数据不一致问题,Redis 设置了一个 AOF 重写缓冲区,这个缓冲区在创建 bgrewriteaof 子进程之后开始使用。在重写 AOF 期间,当 Redis 执行完一个写命令之后,它会同时将这个写命令写入到 「AOF 缓冲区」(AOF 日志)和 「AOF 重写缓冲区」 在 bgrewriteaof 子进程执行 AOF 重写期间,主进程需要执行以下三个工作: 执行客户端发来的命令; 将执行后的写命令追加到 「AOF 缓冲区」; 将执行后的写命令追加到 「AOF 重写缓冲区」;  当子进程完成 AOF 重写工作(扫描数据库中所有数据,逐一把内存数据的键值对转换成一条命令,再将命令记录到重写日志)后,会向主进程发送一条信号,信号是进程间通讯的一种方式,且是异步的。 主进程收到该信号后,会调用一个信号处理函数,该函数主要做以下工作: 将 AOF 重写缓冲区中的所有内容追加到新的 AOF 的文件中,使得新旧两个 AOF 文件所保存的数据库状态一致; 新的 AOF 的文件进行改名,覆盖现有的 AOF 文件。 信号函数执行完后,主进程就可以继续像往常一样处理命令了。 在整个 AOF 后台重写过程中,除了发生写时复制会对主进程造成阻塞,还有信号处理函数执行时也会对主进程造成阻塞,在其他时候,AOF 后台重写都不会阻塞主进程。
RDB 持久化
RDB 持久化介绍
redis 的 RDB 方式中的快照是全量快照,即每次执行快照,都是把内存中的所有数据都记录到磁盘中,所以,如果频率太频繁,redis 性能会下降;如果频率太低,当服务器故障时,丢失的数据就越多。 而 redis 通常可能设置至少 5 分钟保存一次快照,如果reids 出现故障,就意味着丢失 5 分钟的数据。
RDB 文件的创建与加载
1. RDB 文件的创建: redis 生成 RDB 文件的两个命令:save 和 bgsave (1)save: 执行 SAVE 命令的主进程会生成 RDB 文件,因此处理客户端请求和生成 RDB 文件的是一个进程。所以,在执行 save 命令的同时,会阻塞 redis 服务器进程(主进程),也就不能处理客户端发送的任何命令,直到RDB文件创建完毕为止 (2)bgsave: 执行 bgsave 命令,父进程会创建出一个子进程用来负责创建 RDB 文件,服务器进程(父进程)继续处理命令请求,可以避免主进程的阻塞 (3)rdbSave 函数: 创建RDB文件的实际工作由 rdb.c/rdbSave 函数 完成,save 命令 和 bgsave 命令会以不同的方式调用这个函数:  2. RDB 文件的加载: RDB 文件的载入工作是在服务器启动时自动执行的,所以 Redis 并没有专门用于载入 RDB 文件的命令,只要 Redis 服务器在启动时检测到 RDB 文件存在,它就会自动载入 RDB 文件 (1)RDB 文件载入时,服务器进程的状态 服务器在载入 RDB 文件期间,会一直处于阻塞状态,直到载入工作完成为止 (2)AOF持久化对RDB持久化的影响: 如果服务器开启了 AOF 持久化功能,那么服务器会优先使用 AOF 文件来还原数据库状 态,那么就不会使用 RDB 文件了 只有在 AOF 持久化功能处于关闭状态时,服务器才会使用 RDB 文件来还原数据库状态  (3)rdbSave 函数: 载入RDB文件的实际工作由rdb.c/rdbLoad函数完成,这个函数和rdbSave函数之间的关系可以如下图所示: 
自动间隔保存
save 和 bgsave 命令实现方面的主要区别: save 命令有服务器进程执行保存工作,所以 save 命令会阻塞服务器进程 bgsave 命令由服务器进程创建的子进程执行保存工作,所以不会阻塞服务器进程 因为 bgsave 命令在执行时,不阻塞服务器进程,所以 redis 允许当 redis 服务器启动时: 用户可以通过设置指定服务器的配置文件或者传入启动参数的方式设置 save 选项,让服务器每隔一段时间自动执行一次 bgsave 命令; 如果用户没有主动设置 save 选项,那么服务器会为 save 选项设置默认条件 save 选项: (1)用户可以通过save选项设置多个保存条件,但只要其中任意一个条件被满足,服务器就会执行 bgsave 命令 (2)如果用户没有主动设置save选项,那么服务器会为save选项设置默认条件: (会有一个函数专门用来检查 save 选项是否满足) save 900 1 // 服务器在 900 秒之内,对数据库进行了至少 1 次修改 save 300 10 // 服务器在 300 秒之内,对数据库进行了至少 10 次修改 save 60 10000 // 服务器在 60 秒之内,对数据库进行了至少 10000 次修改 只要满足以上三个条件中的任意一个,bgsave 命令就会被执行
RDB 文件
RDB 文件为二进制格式保存,下面使用字符串形式代替二进制形式演示 1. 总体结构:  (1)REDIS:常量,程序可以在载入文件时,快速检查所载入的文件是否 RDB 文件; (2)db_version:变量,使用 4 字节的字符串表示整数,记录 RDB 文件的版本号,如“0006”为第六版 (3)database:包含 0 个或多个数据库,如果包含 0 个,此部分长度为 0 字节;如果至少包含 1 个,根据数据库保存的键值对的数量,类型和内容不同,长度也会有所不同; (4)EOF:常量,1 字节,当读入程序遇到这个值的时候,标志 RDB 文件正文内容的结束; (5)check_sum:变量,8 字节的无符号整数,校验和(通过前 4 部分计算得出),用来检查 RDB 文件是否异常 2. database 部分结构: 一个 RDB 文件的 databases 部分可以保存任意多个非空数据库。例如,如果服务器的 0 号数据库和 3 号数据库非空,那么服务器将创建一个如下图所示的 RDB 文件:  (1)SELECTDB:常量,1 字节,用来表示接下来要读入的将是一个数据库号码 (2)db_number:服务器会调用SELECT命令,根据此字段的数据库号码进行数据库切换,使得之后读入的键值对可以载入到正确的数据库中 (3)key_value_pairs:保存了数据库中的所有键值对数据  3. key_value_pairs部分结构  (1)TYPE:1 字节,记录 value 的类型 (2)key:总为一个字符串对象,长度不一样,根据存放的内容,长度会有所不同 (3)value:根据 TYPE 类型的不同,以及保存内容长度的不同,保存value的结构和长度也会有所不同
执行快照时,数据能被修改吗?
执行 bgsava 命令时,服务器进程创建子进程来创建 RDB 文件,然后服务器进程继续工作,那么之后服务器进程可以修改数据吗? 可以!怎么做到的呢?使用 写时复制技术(COW,copy-on-write) 执行 bgsava 命令的时候,服务器进程 会通过 fork() 创建子进程,此时子进程和父进程是共享同一片内存数据的,因为创建子进程的时候,会复制父进程的页表,但是页表指向的物理内存还是一个。  若父进程对共享的内存数据也只是只读操作,那么主进程 和 子进程相互不影响; 若父进程要修改共享内存中的数据,那么会使用 写时复制技术。 写时复制技术: 只有在发生修改内存数据的情况时,物理内存会被复制一份。 然后父进程在这个 数据副本 进行修改操作,同时,bgsave 子进程可以继续把原来的数据写入到 RDB 文件。而主进程刚修改的数据,是被办法在这一时间写入 RDB 文件的,只能交由下一次的 bgsave 快照。 这样的目的是为了减少创建子进程时的性能损耗,从而加快创建子进程的速度,毕竟创建子进程的过程中,是会阻塞主进程的。 所以 Redis 在使用 bgsave 快照过程中,如果主进程修改了内存数据,不管是否是共享的内存数据,RDB 快照都无法写入主线程刚修改的数据,因为此时主线程的内存数据和子线程的内存数据已经分离了,子线程写入到 RDB 文件的内存数据只能是原本的内存数据。
redis 事件
文件事件
1. 文件事件: Redis服务器通过套接字与客户端或其他 Redis 服务器进行连接,而文件事件就是服务器对套接字操作的抽象。 服务器与客户端(或者其他服务器) 的通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来完成一系列网络通信 操作 2. 文件事件类型: IO 多路复用程序 监听的多个套接字上的事件分为以下两种: (1)AE_READABLE 事件(客户端套接字处于可读状态) 当套接字上产生 AE_READABLE 事件时,表示套接字变得可读,即服务端可以读取套接字里面的内容,即用户端那边进行了write操作、close操作或者connect操作。 (我写了write一些东西,现在变得可读,快来读我) (2)AE_WRITABLE 事件(客户端套接字处于可写状态) 当套接字产生 AE_WRITEABLE 事件时,表示套接字变得可写,即服务端可以往套接字写入内容,给用户端看,即用户端那边进行了Read操作。 (我请求read一些东西,现在变得可写,快来写我) (3)【注意】 若一个套接字上同时产生这两种事件,即客户端套接字变得可读又可写,那么服务器将优先处理 AE_READABLE事件(先读套接字,后写套接字) [ 可能是 AE_READABLE 事件对应的客户端操作更多 ] 3. I/O 多路复用程序的实现: Redis的 I/O 多路复用程序的所有功能都是通过包装常见的select、epoll 等 I/O多路复用函数库来实现的,每个 I/O 多路复用函数库在Redis源码中都对应一个单独的文件,如 ae_select.c、ae_epoll.c 等 因为 Redis 为每个 I/O 多路复用函数库都实现了相同的 API ,所以 I/O 多路复用程序的底层实现是可以互换的  4. 文件事件处理器: Redis 为文件事件编写了多个处理器,这些事件处理器分别用于实现不同的网络通信需求,(其实处理器都是用函数实现的)最常用的处理器有以下三种: (1)连接应答处理器 用于对连接服务器监听套接字的客户端进行应答,其实就是对 socket.h/accept函数 的包装 当 Redis 服务器进行初始化时,程序会将这个连接应答处理器和服务器监听套接字的 AE_READABLE事件关联起来;(注意是将服务器监听套接字与连接应答处理器进行关联) 当客户端用 socket.h/connect函数 连接服务器监听套接字时,服务器监听套接字产生AE_READABLE事件,并交由连接应答处理器执行,并执行相应的套接字应答操作(注意,是服务器监听套接字产生这个事件)  (2)命令请求处理器 用于从套接字中读入客户端发送的命令请求内容,其实就是对 unistd.h/read函数 的包装 当一个客户端通过连接应答处理器成功连接到服务器之后,服务器会将这个客户端套接字的 AE_READABLE事件 和命令请求处理器关联起来,当客户端向服务器发送命令请求的时候, 套接字就会产生AE_READABLE事件,引发命令请求处理器执行,并执行相应的套接字读入操作。  (3)命令回复处理器 用于将服务器执行命令后得到的命令回复通过套接字返回给客户端,其实就是对 unistd.h/write函数 的包装 当服务器有命令回复需要传送给客户端的时候,服务器会将客户端套接字的 AE_WRITABLE事件和命令回复处理器关联起来,当客户端准备好接收服务器传回的命令回复时,就会产生AE_WRITABLE事件,引发命令回复处理器执行,并执行相应的套接字写入操作 当命令回复发送完毕之后,服务器就会解除命令回复处理器与客户端套接字的 AE_WRITABLE事件之间的关联 
文件线程模型 / 文件事件处理模型
1. 文件处理器的结构: 多个 socket 每当一个套接字准备好执行连接应答(accept)、写 入、读取、关闭等操作时,就会产生一个文件事件。 IO 多路复用程序 负责监听多个套接字,并向文件事件分派器传送那些产生了事件的套接字 尽管多个文件事件可能会并发地出现,但 I/O 多路复用程序总是会将所有产生事件的套接字都放到一个队列里面 这个有序队列可以保证事件以 有序、同步、每次一个套接字的方式向文件事件分派器传送套接字。 当上一个套接字产生的事件被处理完毕之后(该套接字为事件所关联的事件处理器执行完毕),I/O多路复用程序才会继续向文件事件分派器传送下一个套接字  文件事件分派器 接收 I/O 多路复用程序传来的套接字,并根据套接字产生的事件的类型, 调用相应的事件处理器 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器) 服务器会为执行不同任务的套接字关联不同的事件处理器,这些处理器是一个个函数, 它们定义了某个事件发生时,服务器应该执行的动作 (可能同一个套接字在不同的执行阶段关联的事件处理器也是不同的) 2. 线程模型: 多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO多路复用程序会监听多个 socket,会将产生事件的 socket 放入队列中排队,事件分派器每次从队列中取出一个 socket,根据 socket 的事件类型交给对应的事件处理器进行处理。  3. 一次客户端与 redis 的完整通信过程:  服务器套接字 server socket 记为 套接字 ss 客户端套接字 client socket 01 记为 客户端套接字 s1 服务器套接字 server socket 01 记为 套接字 s1 (1)建立连接 首先,redis 服务端进程初始化的时候,会将 服务器套接字 ss 的 AE_READABLE 事件与连接应答处理器关联; 客户端套接字 s1 向 redis 进程的 套接字 ss 请求建立连接,此时 套接字 ss 会产生一个 AE_READABLE 事件,IO 多路复用程序监听到 套接字 ss 产生的事件后,将该 socket 压入队列中。 文件事件分派器从队列中获取 socket,交给连接应答处理器。 连接应答处理器会创建一个能与客户端通信的 套接字 s1,并将该 套接字 s1 的 AE_READABLE 事件与命令请求处理器关联。 (2)执行一个 set 请求 客户端发送了一个命令请求 set key value(相当于执行write函数),此时 套接字 s1 产生 AE_READABLE 事件,IO 多路复用程序将套接字 s1 压入队列, 此时事件分派器从队列中获取到 套接字 s1 产生的 AE_READABLE 事件,由于套接字 s1 在连接建立后就与命令请求处理器关联了,所以这个 AE_READABLE 事件会被事件分派器交给命令请求处理器来处理。 命令请求处理器读取 套接字 s1 的 键值对 key value,并在内存中完成对 key value 的设置。操作完成后,命令请求处理器会将 套接字 s1 的 AE_WRITABLE 事件与命令回复处理器关联。(因为对于set命令来说,已经操作完成了,如果是其他命令,可能会关联不同的处理器) 当客户端准备好接收返回结果时,套接字 s1 产生一个 AE_WRITABLE 事件,再压入队列中,事件分派器找到相关联的命令回复处理器,由命令回复处理器对 socket01 输入本次操作的一个结果,比如 ok,之后命令回复处理器再解除套接字 s1 的 AE_WRITABLE 事件与命令回复处理器的关联。
时间事件
1. 时间事件: Redis服务器中的一些操作(比如serverCron函数)需要在给定的时间点执行,而时间事件就是服务器对这类定时操作的抽象  2. 时间事件的 3 个属性 id:服务器为时间事件创建的全局唯一ID(标识号)。ID号按从小到大的顺序递增,新事件的ID号比旧事件的ID号要大 when:毫秒精度的UNIX时间戳,记录了时间事件的到达(arrive)时间,如when为20,就表示下次这个时间事件会在20毫秒后发生 timeProc:时间事件处理器,一个函数。当时间事件到达时,服务器就会根据这个属性调用相应的处理器来处理事件 3. 时间事件的分类: (1)定时事件 让一段程序在指定的时间之后执行一次 (2)周期性事件 让一段程序每隔指定时间就执行一次 (3)时间事件类别的判定: 一个时间事件是定时事件还是周期性事件取决于时间事件处理器的返回值 定时事件:事件处理器返回 AE_NOMORE,该事件在达到一次之后就会被删除,之后不再到达 周期性事件:事件处理器返回一个 非AE_NOMORE的整数值,当一个时间事件到达之后,服务器会根据事件处理器返回的值,对时间事件的when属性进行更新,让这个事件在一段时间之后再次到达,并以这种方式一直更新并运行下去。 如果一个时间事件的处理器返回整数值 30,那么服务器应该对这个时间事件的属性when进行更新,让这个事件在30毫秒之后再次到达 4. 时间事件的实现 由上图可以看出,服务器将所有时间事件都放在一个无序链表中,每当时间事件执行器运行时,它就遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器。 无序链表是指不按照属性 when 的值进行排序,所以当时间事件执行器运行的时候,它必须遍历链表中的所有时间事件,这样才能确保服务器中所有已到达的时间事件都会被处理。 遍历无序链表并不影响时间处理器的性能: 因为目前版本,时间事件是比较少的,只有几个,使用无需链表来保存时间事件也并不会影响事件执行的性能。
服务器事件的调度与执行
1. 服务器事件的调度与执行: 因为服务器中同时存在文件事件和时间事件两种事件类型,所以服务器必须对这两种事件进行调度,决定何时应该处理文件事件,何时又应该处理时间事件,以及花多少时间来处理它们等等 事件的调度与执行由一个函数 ae.c/aeProcessEvents 负责 2. aeProcessEvents函数 时间事件在文件事件之后执行,事件之间不会出现抢占,所以时间事件的实际处理时间通常比时间事件设定的时间要晚一点 文件事件是随机出现的,如果等待并处理完一次文件事件之后,仍未有任何时间事件到达,那么服务器将再次等待并处理文件事件。等到时间事件到达后服务器开始处理到达的时间事件。(所以时间事件不会出现饥饿现象) 对文件事件和时间事件的处理都是同步有序原子地执行,服务器不会中途中断事件处理,也不会对事件进行抢占,因此,事件处理器尽可能地减少程序的阻塞时间,并在有需要时主动让出执行权,从而降低造成事件饥饿的可能性。   aeApiPoll函数的最大阻塞时间由到达时间最接近当前时间的时间事件决定,这个方法既可以避免服务器对时间事件进行频繁的轮询(忙等待),也可以确保aeApiPoll函数不会 阻塞过长时间 3. redis服务器的主函数: 初始化服务器 然后一直循环调用 aeProcessEvents处理事件,直到服务器关闭为止, 服务器关闭了进行清理功能 
redis 集群的三种模式
主从复制 模式
复制 介绍
复制: 主服务器,从服务器 一个服务器可以复制另一个服务器,而被复制的服务器是主服务器,想要复制别人的服务器是从服务器。 通过复制,主从服务器双方的数据库将保存相同的数据,即数据库状态一致。 
复制 功能
1. 复制功能的两个操作: (1)同步操作 用于将从服务器的数据库状态更新至主服务器当前所处的数据库状态 (2)命令传播操作 主服务器的数据库被些命令修改,会导致主从服务器的数据库状态出现不一致,主服务器通过将些命令发送给从服务器,进而让主从服务器的数据库重新回到一致状态 2. 同步操作 PSYNC 当客户端向从服务器发送 SLAVEOF 命令,要求从服务器复制主服务器时,从服务器首先需要执行同步操作,将从服务器的数据库状态更新至主服务器当前所处的数据库状态; 从服务器对主服务器的同步操作是通过向主服务器发送 PSYNC 命令完成的。 PSYNC 命令具有两种模式: 完整重同步: 主要用于初次复制情况,通过让主服务器创建并发送RDB文件,以及向从服务器发送保存在缓冲区里面的写命令来进行同步;当然,断线后也可能会进行完成重同步 部分重同步: 用于处理断线后重复制情况,当从服务器在短线后重新连接主服务器时,如果条件允许,主服务器可以将主从服务器连接断开期间执行的写命令发送给从服务 器,从服务器只要接收并执行这些写命令,就可以将数据库更新至主服务器当前所处的状态
1. 同步操作
完整重同步
完整重同步 通过让主服务器创建并发送 RDB 文件,以及向从服务器发送保存在缓冲区里面的写命令来进行同步 1. PSYNC 命令的执行步骤: 从服务器向主服务器发送 PSYNC 命令 收到 PSYNC 命令的主服务器执行 BGSAVE 命令,在后台生成一个 RDB 文件,并使用一个缓冲区记录从现在开始执行的所有写命令 当主服务器的 BGSAVE 命令执行完毕时,主服务器会将 BGSAVE 命令生成的 RDB 文件发送给从服务器,从服务器接收并载入这个 RDB 文件,将自己的数据库状态更新至主服务器 执行 BGSAVE 命令时的数据库状态 主服务器将记录在缓冲区里面的所有写命令发送给从服务器,从服务器执行这些写 命令,将自己的数据库状态更新至主服务器数据库当前所处的状态  2. 完整重同步的缺陷: 处于命令传播阶段的主从服务器因为网络原因而中断了复制,但从服务器通过自动重连接重新连上了主服务器,并继续复制主服务器,这时若直接使用完整重同步虽然也能让主从服务器重新回到一致状态,但是由于完整重同步需要创建主服务器整个数据库的 RDB 文件,效率是非常低的。 为什么网络断开之后重新复制效率低,以上面的演示案例为例: 主从服务器在执行命令 K0 至 K10086 的过程中一直处于一致状态,这两个服务器保存的数据大部分都是相同的 在执行 K10086 命令后主从服务器由于网络原因断开了,从服务器在主服务器执行 K10089 命令后重新连接上,从服务器想要将自己更新至主服务器当前所处的状态,其实只需要执行在连接中断期间主服务器执行的写命令即可 但是,如果直接使用完整重同步,从服务器会让主服务器生成整个数据库的RDB文件,但其实是没必要的 上面给出的例子可能有一点理想化,因为在主从服务器断线期间,主服务器执行的写命令可能会有成百上千个之多,而不仅仅是两三个写命令。但总的来说,主从服务器断开的时 间越短,主服务器在断线期间执行的写命令就越少,而执行少量写命令所产生的数据量通常比整个数据库的数据量要少得多,在这种情况下,为了让从服务器补足一小部分缺失的数 据,却要让主从服务器重新执行一次SYNC命令,这种做法无疑是非常低效的 
部分重同步
1. 部分重同步: 解决了旧版复制功能在处理断线后重复制时出现的低效情况 执行部分重同步所需的资源比执行完整重同步所需的资源要少得多,完成同步的速度也快得多: 完整重同步:需要生成、传送和载入整个 RDB 文件 部分重同步:只需要将从服务器缺少的写命令发送给从服务器执行就可以了  (断线后重新建立连接,使用 PSYNC 命令进行同步操作,上图是根据主从服务器之间的信息状态选择了部分重同步) 2. 部分重同步的实现细节: 同步功能由以下三个部分构成: 主服务器 和 从服务器 分别保存的复制偏移量 主服务器的复制积压缓冲区 服务器运行 ID (1)复制偏移量: 执行复制的双方——主服务器和从服务器会分别维护一个复制偏移量 offset : 主服务器:每次向从服务器传播N个字节的数据时,主服务器 offset 就 +N 从服务器:每次收到主服务器传来的N个字节的数据时,从服务器 offset 就 +N 复制偏移量的作用: 通过对比主从服务器的复制偏移量,判断主从服务器是否处于一致状态 若偏移量相同,那么主从服务器处于一致状态; 否则,主从服务器并未处于一致状态, 【示例】主从服务器的复制偏移量的值都为 10086,主服务器向三个从服务器传播长度为 33 字节的数据,那么主服务器的复制偏移量将更新为 10086+33=10119,而三个从服务器在接收到主服务器传播的数据之后,也会将复制偏移量更新为10119   网络断开重连后的复制偏移量: 主服务器在发送 33 字节的数据时,服务器 A 断线了,导致主服务器和从服务器 A 的复制偏移量 / 数据库状态不一致  从服务器 A 在断线之后就立即重新连接主服务器,并且成功,那么接下来,从服务器将向主服务器发送 PSYNC 命令要求进行数据库同步,并报告自己当前的复制偏移量为 10086,那么这时, 主服务器应该对从服务器执行完整重同步还是部分重同步呢? 如果执行部分重同步的话,主服务器又如何补偿从服务器A在断线期间丢失的那部分数据呢? 以上问题的答案都和复制积压缓冲区有关。(问题:完整重同步还是部分重同步?怎么补偿丢失的那部分数据?) (2)复制积压缓冲区 复制积压缓冲区是由主服务器维护的一个固定大小空间的先进先出(FIFO)队列,队尾进元素,队头出元素。(复制积压缓冲区的大小可以根据公式来估算) 当主服务器进行命令传播时,它不仅会将写命令发送给所有从服务器,还会将写命令入队到复制积压缓冲区里面  但由于复制积压缓冲区的大小是一定的,所以主服务器的复制积压缓冲区只会保存一部分最近发送给从服务器的些命令,并且复制积压缓冲区也会为队列中的每个字节记录相应的复制偏移量  【问题】是选择部分重同步?还是选择完整重同步? 如果从服务器发送的 PAYNC 命令的复制偏移量参数 offest 值在主服务器的复制积压缓冲区范围内,那么主服务器就对从服务器执行部分重同步;否则,offset 偏移量之后的数据已经不再复制积压缓冲区内,就要执行完整重同步操作 【示例】   从服务器 A 断线并重新连接主服务器后,向主服务器发送 PSYNC 命令,并报告自己的复制偏移量为 10086 主服务器收到从服务器发来的 PSYNC 命令以及偏移量 10086 之后,根据偏移量 10086 之后的数据是否存在于复制积压缓冲区里面,结果发现这些数据仍然存在,于是主服务器向从服务器发送 +CONTINUE 回复,表示数据同步将以部分重同步模式来进行 接着主服务器会将复制积压缓冲区 10086 偏移量之后的所有数据(偏移量为10087至 10119)都发送给从服务器 从服务器只要接收这 33 字节的缺失数据,就可以回到与主服务器一致的状态 (3)服务器运行 ID 每个Redis服务器,都有唯一的运行 ID,运行 ID 在服务器启动时自动生成,由 40 个随机的十六进制字符组成,例如 53b9b28df8042fdc9ab5e3fcbbbabff1d5dce2b3 运行 ID 的作用: 通过对运行 ID 的比较,用来判断再次连接的那个主服务器是否为上一次的主服务器 服务器运行 ID 起作用的过程: 从服务器对主服务器进行初次复制时,主服务器将自己的运行 ID 传送给从服务器, 而从服务器将这个运行 ID 保存起来 若从服务器断线并重新连上一个主服务器时,从服务器将向当前连接的主服务器发送之前保存的运行 ID,主服务器检查从服务器发送来的运行 ID 和自己的运行 ID 作比较 如果从服务器保存的运行 ID 和当前连接的主服务器的运行 ID 相同,那么说明从服务器断线之前复制的就是当前连接的这个主服务器,主服务器可以继续尝试执行部分重同步操作 相反地,如果从服务器保存的运行ID和当前连接的主服务器的运行 ID 并不相同,那么说明从服务器断线之前复制的主服务器并不是当前连接的这个主服务器,主服务器将对从服务器执行完整重同步操作
PSYNC 命令的实现
1. PSYNC 命令的调用方法 PSYNC 命令的调用方法有两种: (1)从服务器 没有复制过任何主服务器,或之前执行过SLAVEOF no one命令 从服务器在开始一次新的复制时将向主服务器发送 PSYNC ? -1 命令,主动请求主服务器进行完整重同步(因为这时不可能执行部分重同步) (2)从服务器 已经复制过某个主服务器: 从服务器在开始一次新的复制时将向主服务器发送 PSYNC <runid> <offset> 命令: runid :上一次复制的主服务器的运行 ID offset:从服务器当前的复制偏移量 接收到这个命令的主服务器会通过这两个参数来判断应该对从服务器执行哪种同步操作 2. 主服务器对 PSYNC 命令的回复 (1)返回 +FULLRESYNC <runid> <offset>: 表示主服务器将与从服务器执行完整重同步操作 runid:这个主服务器的运行 ID,从服务器会将这个 ID 保存起 来,在下一次发送PSYNC命令时使用 offset:主服务器当前的复制偏移量,从服务器会将这个值作为自己的初始化偏移量 (2)返回 +CONTINUE: 表示主服务器将与从服务器执行部分重同步操作,从服务器只要等着主服务器将自己缺少的那部分数据发送过来就可以了 (3)返回 -ERR : 表示主服务器的版本低于Redis 2.8,它识别不了PSYNC命令,从服务器将向主服务器发送SYNC命令,并与主服务器执行完整同步操作 
2. 命令传播操作
命令传播操作 在同步操作执行完毕之后,主从服务器两者的数据库将达到一致状态,当主服务器执行客户端发送的写命令时,主服务器的数据库就有可能会被修改,并导致主从服务器状态不再一致 为了让主从服务器再次回到一致状态,主服务器需要对从服务器执行命令传播操作:主服务器会将这条写命令,发送给从服务器执行,当从服务器执行了相同的写命令之后,主从服务器将再次回到一致状态 例如,主服务器执行了DEL k3的命令,那么主服务器将向从服务器发送相同的命令DEL k3,当从服务器执行完这个命令之后,主从服务器将再次回到一致状态,现在主从服务器两者的数据库都不再包含键k3了 
复制 过程
SLAVEOF——slave of——...的奴隶 1. 步骤一:设置主服务器的地址和端口 客户端 向 从服务器 发送 SLAVEOF 命令 从服务器 就会根据客户端发送的命令里面的信息设置主服务器的地址和端口进行保存(保存到从服务器的两个属性中) (1)客户端向从服务器发送 SLAVEOF 命令: SLAVEOF 命令是一个异步命令,从服务器在完成对主服务器的属性设置后,再向客户端返回 OK,表示复制指令已经被接收,而实际的复制工作将在 OK 返回之后才真正开始执行  (2)从服务器中保存的主服务器的地址和端口信息  2. 步骤二:建立套接字连接 在 SLAVEOF 命令执行之后,从服务器将根据命令所设置的 IP 地址和端口,创建连向主服务器的套接字连接 (accept)(connect) (1)主服务器接受连接: 主服务器在接受(accept)从服务器的套接字连接后,为该套接字创建相应的客户端状态,并将从服务器是一个客户端,这样,从服务器就可以以客户端身份向主服务器发送命令请求,然后主服务器就可以向从服务器返回命令回复  (2)从服务器设置文件事件处理器: 从服务器创建的套接字成功连接(connect)到主服务器后,从服务器为这个套接字关联一个专门用于处理复制工作的文件事件处理器,这个处理器将负责执行后续的复制工作,如: 接收RDB文件 接收主服务器传播来的写命令 3. 步骤三:发送 PING 命令 从服务器成为主服务器的客户端之后,做的第一件事就是向主服务器发送一个PING命令  (1)PING 命令的作用: 用于检查套接字的读写状态是否正常 用于检查主服务器能否正常处理命令请求 (2)PING 命令的几种返回结果: 主服务器返回“PONG”:表示主从服务器之间的网络连接状态正常,且能正常处理从服务器发送的命令请求,从服务器可继续执行复制工作的下个步骤 主服务器返回一个命令回复,从服务器不能再规定时间内读取命令回复的内容:主从服务器之间的网络连接状态不佳,从服务器不能继续后续复制工作步骤,还要断开连接并重新创建与主服务器连接的套接字 主服务器返回一个错误:主服务器暂时没办法处理从服务器的命令请求,从服务器要断开并重新创建连接主服务器的套接字  4. 步骤四:身份验证 (1)主服务器 设置 requirepass 选项,决定是否让从服务器提供密码来进行身份验证 设置了该选项,需要进行身份验证 没有设置该选项,不需进行身份验证 (2)从服务器 设置 masterauth 选项,决定下一步决定是否向主服务器进行身份验证 (3)身份验证的几种情况: 主—无,从—无:从服务器无需发送 AUTH 命令,可继续接下来复制工作 主—有,从—无:从服务器发送其他命令时,主服务器会返回一个 NOAUTH 错误 在需要进行身份验证的情况下,从服务器将向主服务器发送一条 AUTH 命令,命令的参数是从服务器 masterauth 选项的值,如:masterauth = 10086,那么会发送命令 AUTH 10086  主—无,从—有:主服务器将返回一个no password is set错误 主—有,从—有: 从服务器通过 AUTH 发送的密码 与 主服务器的 requirepass 选项设置的密码相同,主服务器会继续接下来的复制工作; 若不相同,主服务器返回 invalid password 错误  所有错误情况都会令从服务器中止目前的复制工作,并从创建套接字开始重新执行复制,直到身份验证通过,或者从服务器放弃执行复制为止。 5. 步骤五:发送端口信息 在身份验证步骤之后,从服务器将执行命令 REPLCONF listening-port 向主服务器发送自己的监听端口号  主服务器在接收到这个命令之后,会将端口号记录在从服务器所对应的客户端状态的slave_listening_port属性中(该属性目前唯一的作用就是在主服务器执行 INFO replication命令时打印出从服务器的端口号) 6. 步骤六:同步 从服务器将向主服务器发送 PSYNC 命令,执行同步操作,并将自己的数据库更新至主服务器数据库当前所处的状态(从服务器根据自己的状态会执行完整重同步操作 / 部分重同步操作) (1)在同步操作执行之前,只有从服务器是主服务器的客户端 (2)在同步操作执行之后,主服务器也会成为从服务器的客户端,即主从服务器双方都是对方的客户端,它们可以互相向对方发送命令请求,或者互相向对方返回命令回复 因为,主服务器必要要成为从服务器的客户端,主服务器才可以通过发送些命令的方式去改变从服务器的状态,不仅同步操作需要这一点,命令传播操作也是需要的,都会改变从服务器的状态  7. 步骤七:命令传播操作 当完成了同步之后,主从服务器就会进入命令传播阶段,这时主服务器只要一直将自己执行的写命令发送给从服务器,而从服务器只要一直接收并执行主服务器发来的写命令,就可以保证主从服务器一直保持一致了
心跳检测
1. 心跳检测: 在命令传播阶段,从服务器默认会以每秒一次的频率,向主服务器发送 REPLCONF 命令,用于实现心跳检测 (1)心跳检测对于主服务器的作用 检测主从服务器的网络连接状态 辅助实现 min-slaves 选项 检测命令丢失 (2)REPLCONF 命令 // replication_offset :从服务器当前的复制偏移量 REPLCONF ACK <replication_offset> 2. 心跳检测的作用: (1)检测主从服务器的网络连接状态(lag标志) 如果主服务器超过一秒钟没有收到从服务器发来的 REPLCONF ACK 命令,那么主服务器就知道主从服务器之间的连接出现问题了 通过向主服务器发送 INFO replication 命令,在列出的从服务器列表的 lag 一栏中,可以看到相应从服务器最后一次向主服务器发送 REPLCONF ACK 命令距离现在过了多少秒(大于1秒,连接就出现了问题) (2)辅助实现min-slaves配置选项 Redis 的 min-slaves-to-write 和 min-slaves-max-lag 两个选项可以防止主服务器在不安全的情况下执行写命令 不安全状况: 在从服务器的数量少于3个,或者3个从服务器的延迟(lag)值都大于或等于10秒时,主服务器将拒绝执行写命令(延迟值:INFO replication 命令的 lag 值)  (3)检测命令丢失 因为网络故障,从服务器没有收到主服务器发来的写命令(可能丢失或者阻塞了),那么从服务器的复制偏移量就不会发生变化,但是主服务器的复制偏移量是发生变化了的 当从服务器再向主服务器发送 REPLCONF ACK <offset> 命令时,主服务器会发现从服务器发来的命令中的复制偏移量少于自己的复制偏移量 然后主服务器就会根据从服务器提交的复制偏移量,在复制积压缓冲区里面找到从服务器缺少的数据,并将这些数据重新发送给从服务器进行数据同步
哨兵(Sentinel)模式
哨兵是什么?为什么需要哨兵?
1. 主从模式的引入: 在主从模式下,主从复制机制使得 slave 成为与 master 完全一致的副本,一旦 master 宕机,我们可以选择一个正常的slave 成为新的主节点,实现手动的故障恢复。 手动选择新的主节点弊端 及 解决: 人工干预效率低、易出错,并且故障感知滞后,不具备生产实用性。 所以为了能够让 redis 能够自动感知系统故障、并进行自动故障转移,reids 官方提供一个 Redis 的高可用方案——哨兵(Sentinel),使用它可以搭建一个即使无人干预也能抵抗某些类型失败的高可用的 Redis 分布式系统。 2. 什么是哨兵? 哨兵 Sentinel 是一种运行模式,是 Redis 的高可用性解决方案,哨兵模式是天然的分布式系统,它被设计为基于一套配置,并在多个哨兵实例的配合下工作。 哨兵系统有一个或多个 Sentinel 实例组成,能够对 Redis 实例(主节点、从节点)运行状态进行监控,并能够在主节点发生故障时通过一系列的机制实现 选主 及 主从切换,实现故障转移,确保整个 Redis 系统的可用性。 (1)多个哨兵实例共同协作的优势: 主节点的系统故障是在多个实例共同认可的情况下完成的,大大降低了误报的概率 即使不是所有的哨兵实例都正常运行哨兵集群也能正常工作,这大大增加了系统的鲁棒性 (2)哨兵的功能: 监控(Monitoring): 持续监控 主节点、从节点 是否处于预期的工作状态。 通知(Notification): 被监控的服务器出现问题时,这个哨兵应该向其他哨兵、客户端发送通知 自动故障恢复(Automatic failover): 当主节点运行故障时,哨兵会启动自动故障恢复流程:某个从节点会升级为主节点,其他从节点会使用新的主节点进行主从复制,通知客户端使用新的主节点进行。 配置中心(Configuration provider): 哨兵可以作为客户端服务发现的授权源,客户端连接到哨兵请求给定服务的Redis主节点地址。如果发生故障转移,哨兵会通知新的地址。这里要注意:哨兵并不是Redis代理,只是为客户端提供了Redis主从节点的地址信息。
哨兵工作原理
https://www.bilibili.com/video/BV1VJ411B7Kr?p=3 哨兵在进行主从切换过程中经历三个阶段: 监控 通知 故障转移 1. 监控阶段
Cluster 模式
redis主要有哪些功能?
作者:一只小飞猪 链接:https://www.nowcoder.com/discuss/930412?type=all&order=recall&pos=&page=0&ncTraceId=&channel=-1&source_id=search_all_nctrack&gio_id=7D1C6049F9E9C6A63C84CE1BEB2CC152-1649816841897 来源:牛客网 1.哨兵(Sentinel)和复制(Replication) Redis服务器毫无征兆的罢工是个麻烦事,如何保证备份的机器是原始服务器的完整备份呢?这时候就需要哨兵和复制。 哨兵Sentinel可以管理多个Redis服务器,它提供了监控,提醒以及自动的故障转移的功能,Replication则是负责让一个Redis服务器可以配备多个备份的服务器。 Redis也是利用这两个功能来保证Redis的高可用的。 2.事务 很多情况下我们需要一次执行不止一个命令,而且需要其同时成功或者失败。redis对事务的支持也是源自于这部分需求,即支持一次性按顺序执行多个命令的能力,并保证其原子性。 3.LUA脚本 在事务的基础上,如果我们需要在服务端一次性的执行更复杂的操作(包含一些逻辑判断),则lua就可以排上用场了。 4.持久化 redis的持久化指的是redis会把内存的中的数据写入到硬盘中,在redis重新启动的时候加载这些数据,从而最大限度的降低缓存丢失带来的影响。 5.集群(Cluster) 单台服务器资源的总是有上限的,CPU资源和IO资源我们可以通过主从复制,进行读写分离,把一部分CPU和IO的压力转移到从服务器上,这也有点类似mysql数据库的主从同步。 在Redis官方的分布式方案出来之前,有twemproxy和codis两种方案,这两个方案总体上来说都是依赖proxy来进行分布式的,下面的内容有具体集群方案详解。
使用redis的优势
作者:一只小飞猪 链接:https://www.nowcoder.com/discuss/930412?type=all&order=recall&pos=&page=0&ncTraceId=&channel=-1&source_id=search_all_nctrack&gio_id=7D1C6049F9E9C6A63C84CE1BEB2CC152-1649816841897 来源:牛客网 1.速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1) 支持丰富数据类型,支持string,list,set,sorted set,hash 3.支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行 丰富的特性:可用于缓存,消息,按key设置过期时间,过期后将会自动删除 Redis单点吞吐量 单点TPS达到8万/秒,QPS达到10万/秒,补充下TPS和QPS的概念 1.QPS: 应用系统每秒钟最大能接受的用户访问量 每秒钟处理完请求的次数,注意这里是处理完,具体是指发出请求到服务器处理完成功返回结果。可以理解在server中有个counter,每处理一个请求加1,1秒后counter=QPS。 2.TPS: 每秒钟最大能处理的请求数 每秒钟处理完的事务次数,一个应用系统1s能完成多少事务处理,一个事务在分布式处理中,可能会对应多个请求,对于衡量单个接口服务的处理能力,用QPS比较合理。
redis有哪几种数据淘汰策略?
作者:一只小飞猪 链接:https://www.nowcoder.com/discuss/930412?type=all&order=recall&pos=&page=0&ncTraceId=&channel=-1&source_id=search_all_nctrack&gio_id=7D1C6049F9E9C6A63C84CE1BEB2CC152-1649816841897 来源:牛客网 在Redis中,允许用户设置最大使用内存大小server.maxmemory,当Redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。 1.volatile-lru:从已设置过期的数据集中挑选最近最少使用的淘汰 2.volatile-ttr:从已设置过期的数据集中挑选将要过期的数据淘汰 3.volatile-random:从已设置过期的数据集中任意挑选数据淘汰 4.allkeys-lru:从数据集中挑选最近最少使用的数据淘汰 5.allkeys-random:从数据集中任意挑选数据淘汰 6.noenviction:禁止淘汰数据 redis淘汰数据时还会同步到aof
redis相比memcached有哪些优势?
作者:一只小飞猪 链接:https://www.nowcoder.com/discuss/930412?type=all&order=recall&pos=&page=0&ncTraceId=&channel=-1&source_id=search_all_nctrack&gio_id=7D1C6049F9E9C6A63C84CE1BEB2CC152-1649816841897 来源:牛客网 1.memcached所有的值均是简单的字符串,Redis作为其替代者,支持更为丰富的数据类型 2.Redis的速度比memcached快很多 3.Redis可以持久化其数据 4.Redis支持数据的备份,即master-slave模式的数据备份。
redis集群方案怎么做?都有哪些方案?
作者:一只小飞猪 链接:https://www.nowcoder.com/discuss/930412?type=all&order=recall&pos=&page=0&ncTraceId=&channel=-1&source_id=search_all_nctrack&gio_id=7D1C6049F9E9C6A63C84CE1BEB2CC152-1649816841897 来源:牛客网 1.twemproxy 2.codis,目前用的最多的集群方案,基本和twemproxy一致的效果,但它支持在 节点数量改变情况下,旧节点数据可恢复到新hash节点。 3.Redis cluster3.0自带的集,特点在于他的分布式算法不是一致性hash,而是hash槽的概念,以及自身支持节点设置从节点。 具体请查看:高并发架构系列:详解Redis的存储类型、集群架构、以及应用场景
redis读写分离模型
作者:一只小飞猪 链接:https://www.nowcoder.com/discuss/930412?type=all&order=recall&pos=&page=0&ncTraceId=&channel=-1&source_id=search_all_nctrack&gio_id=7D1C6049F9E9C6A63C84CE1BEB2CC152-1649816841897 来源:牛客网 Redis读写分离模型 通过增加Slave DB的数量,读的性能可以线性增长。为了避免Master DB的单点故障,集群一般都会采用两台Master DB做双机热备,所以整个集群的读和写的可用性都非常高。 读写分离架构的缺陷在于,不管是Master还是Slave,每个节点都必须保存完整的数据,如果在数据量很大的情况下,集群的扩展能力还是受限于单个节点的存储能力,而且对于Write-intensive类型的应用,读写分离架构并不适合。
redis数据分片模型
作者:一只小飞猪 链接:https://www.nowcoder.com/discuss/930412?type=all&order=recall&pos=&page=0&ncTraceId=&channel=-1&source_id=search_all_nctrack&gio_id=7D1C6049F9E9C6A63C84CE1BEB2CC152-1649816841897 来源:牛客网 Redis数据分片模型 为了解决读写分离模型的缺陷,可以将数据分片模型应用进来。 可以将每个节点看成都是独立的master,然后通过业务实现数据分片。 结合上面两种模型,可以将每个master设计成由一个master和多个slave组成的模型。
Redis常见性能问题和解决方案?
作者:一只小飞猪 链接:https://www.nowcoder.com/discuss/930412?type=all&order=recall&pos=&page=0&ncTraceId=&channel=-1&source_id=search_all_nctrack&gio_id=7D1C6049F9E9C6A63C84CE1BEB2CC152-1649816841897 来源:牛客网 (1) Master最好不要做任何持久化工作,如RDB内存快照和AOF日志文件 (2) 如果数据比较重要,某个Slave开启AOF备份数据,策略设置为每秒同步一次 (3) 为了主从复制的速度和连接的稳定性,Master和Slave最好在同一个局域网内 (4) 尽量避免在压力很大的主库上增加从库 (5) 主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <- Slave3… 这样的结构方便解决单点故障问题,实现Slave对Master的替换。如果Master挂了,可以立刻启用Slave1做Master,其他不变。
Redis哈希槽的概念?
作者:一只小飞猪 链接:https://www.nowcoder.com/discuss/930412?type=all&order=recall&pos=&page=0&ncTraceId=&channel=-1&source_id=search_all_nctrack&gio_id=7D1C6049F9E9C6A63C84CE1BEB2CC152-1649816841897 来源:牛客网 Redis集群没有使用一致性hash,而是引入了哈希槽的概念,当需要在 Redis 集群中放置一个 key-value 时,根据 CRC16(key) mod 16384的值,决定将一个key放到哪个桶中。
Redis集群最大节点个数是多少?
Redis集群预分好16384个桶(哈希槽)
Redis集群的主从复制模型是怎样的?
为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所以集群使用了主从复制模型,每个节点都会有N-1个复制品.
Redis集群会有写操作丢失吗?为什么?
Redis并不能保证数据的强一致性,这意味这在实际中集群在特定的条件下可能会丢失写操作。
Redis集群之间是如何复制的?
异步复制
Redis如何做内存优化?
作者:一只小飞猪 链接:https://www.nowcoder.com/discuss/930412?type=all&order=recall&pos=&page=0&ncTraceId=&channel=-1&source_id=search_all_nctrack&gio_id=7D1C6049F9E9C6A63C84CE1BEB2CC152-1649816841897 来源:牛客网 尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的web系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里面.
Redis回收进程如何工作的?
一个客户端运行了新的命令,添加了新的数据。 Redi检查内存使用情况,如果大于maxmemory的限制, 则根据设定好的策略进行回收。
Redis回收使用的是什么算法?
作者:一只小飞猪 链接:https://www.nowcoder.com/discuss/930412?type=all&order=recall&pos=&page=0&ncTraceId=&channel=-1&source_id=search_all_nctrack&gio_id=7D1C6049F9E9C6A63C84CE1BEB2CC152-1649816841897 来源:牛客网 LRU算法 LRU全称是Least Recently Used,即最近最久未使用的意思。 LRU算法的设计原则是:如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。
Redis有哪些适合的场景?
1)Session共享(单点登录) 2)页面缓存 3)队列 4)排行榜/计数器 5)发布/订阅
项目
网络编程
多多总结以上步骤
事件处理模式
同步模式
异步模式
半同步半异步模式
半同步半反应堆模式
设计模式
map 与 unordered_map 的区别
map 和 unordered_map 都是 C++ STL 中的容器,它们的区别主要体现在头文件、实现原理、优缺点以及适用场景的不同。 头文件 map:#include < map > unordered_map:#include < unordered_map > 实现原理 map map 底层采用红黑树实现,红黑树具有自动排序的功能,因此 map 内部的所有元素都是有序的,红黑树的每一个节点都代表着 map 的一个元素。对于 map 进行的查找、删除、添加等一系列的操作都相当于是对红黑树进行的操作。 unordered_map unordered_map 内部实现了一个哈希表,其元素的排列顺序是无序的。 优缺点及适用场景 map 优点:有序性,其元素的有序性在很多应用中都会简化很多的操作;底层适用红黑树实现,因此效率非常的高 缺点: 空间占用率高,因为 map 内部实现了红黑树,虽然提高了运行效率,但是因为每一个节点都需要额外保存父节点、孩子节点和红/黑性质,使得每一个节点都占用大量的空间 适用场景:对于那些有顺序要求的问题,用 map 会更高效一些 unordered_map 优点: 内部实现了哈希表,因此其查找速度非常的快 缺点: 哈希表的建立比较耗费时间 适用处:对于查找问题,unordered_map 会更加高效一些