├── .gitignore ├── .vscode ├── configurationCache.log ├── dryrun.log ├── settings.json ├── targets.log └── tasks.json ├── C++ ├── C++11.md ├── C++语法.md ├── STL.md ├── socket.md └── 类.md ├── Distributed_System ├── CAP.md ├── Distributed_Lock.md ├── Distributed_Transaction.md ├── ETCD.md ├── ETCD.png ├── Raft脑裂.png └── log.md ├── GCC └── README.md ├── Large-Scale-Data └── README.md ├── Linux └── README.md ├── OS ├── README.md └── img │ ├── 1.png │ └── 2.png ├── README.md ├── Structure ├── README.md └── img │ └── 单体架构.png ├── cache ├── README.md └── img │ ├── cache_before_db.png │ ├── db_before_cache.png │ └── db_remove_cache.png ├── data_structure ├── 排序算法.md └── 排序算法时间复杂度.png ├── database ├── RDMS.md ├── img │ ├── index.png │ ├── next_key_lock.png │ ├── rr-phantom-read-example.png │ └── transaction.png ├── index.md └── transaction.md ├── docker └── README.md ├── gc ├── README.md ├── algorithm.md ├── g1.md └── img │ ├── cms_gc.png │ └── gc_roots.png ├── golang ├── GMP.md ├── ReadMe.md ├── context.md ├── escape_analysis.md ├── go_channel.md ├── goroutine.md ├── go协程交互 │ ├── chan实现互斥锁.go │ ├── mutex实现互斥锁.go │ ├── 交叉打印100个abc.go │ ├── 交替打印奇数与偶数.go │ └── 交替打印数字和字母.go ├── img │ ├── GMP.png │ └── context.png ├── map.md ├── mutex.md ├── new与make区别.md ├── range.md └── slice.md ├── messageQueue ├── Kafka.md ├── README.md └── img │ ├── ack_timeout.jpeg │ ├── decouple1.jpeg │ ├── dedouple2.jpeg │ ├── delay_third_directly.jpeg │ ├── delay_third_using_mq.jpeg │ ├── kafka_available_performance.png │ ├── mq_overview.jpeg │ ├── overview.jpeg │ └── too_many_msg.jpeg ├── microservice ├── availability.md └── general.md ├── network ├── README.md └── img │ ├── 1.png │ └── 2.png ├── pattern ├── README.md ├── 懒汉模式.cpp └── 线程安全单例模式-懒汉.cpp ├── redis ├── availability.md ├── data_structure.md ├── expired.md ├── img │ ├── availability.png │ ├── data_structure.png │ ├── expired.png │ ├── io_model.png │ ├── persistence.png │ ├── pipeline.png │ └── value_object.png ├── io_model.md ├── persistent.md └── pipeline.md └── 项目 ├── jwt-go └── README.md └── 抖音项目 └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | .idea 18 | 19 | .DS_Store -------------------------------------------------------------------------------- /.vscode/configurationCache.log: -------------------------------------------------------------------------------- 1 | {"buildTargets":[],"launchTargets":[],"customConfigurationProvider":{"workspaceBrowse":{"browsePath":[],"compilerArgs":[]},"fileIndex":[]}} -------------------------------------------------------------------------------- /.vscode/dryrun.log: -------------------------------------------------------------------------------- 1 | make.exe --dry-run --always-make --keep-going --print-directory 2 | 'make.exe' �����ڲ����ⲿ���Ҳ���ǿ����еij��� 3 | ���������ļ��� 4 | 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.ignoreLimitWarning": true 3 | } -------------------------------------------------------------------------------- /.vscode/targets.log: -------------------------------------------------------------------------------- 1 | make.exe all --print-data-base --no-builtin-variables --no-builtin-rules --question 2 | 'make.exe' �����ڲ����ⲿ���Ҳ���ǿ����еij��� 3 | ���������ļ��� 4 | 5 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": [ 3 | { 4 | "type": "cppbuild", 5 | "label": "C/C++: g++.exe 生成活动文件", 6 | "command": "C:\\TDM-GCC-64\\bin\\g++.exe", 7 | "args": [ 8 | "-fdiagnostics-color=always", 9 | "-g", 10 | "${file}", 11 | "-o", 12 | "${fileDirname}\\${fileBasenameNoExtension}.exe" 13 | ], 14 | "options": { 15 | "cwd": "${fileDirname}" 16 | }, 17 | "problemMatcher": [ 18 | "$gcc" 19 | ], 20 | "group": { 21 | "kind": "build", 22 | "isDefault": true 23 | }, 24 | "detail": "调试器生成的任务。" 25 | } 26 | ], 27 | "version": "2.0.0" 28 | } -------------------------------------------------------------------------------- /C++/C++11.md: -------------------------------------------------------------------------------- 1 | # C++ 11新特性 2 | 3 | ## 新增特性 4 | - nullptr替代 NULL 5 | - 引入了 auto 和 decltype 这两个关键字实现了类型推导 6 | - 基于范围的 for 循环for(auto& i : res){} 7 | - 类和结构体的中初始化列表 8 | - 右值引用和move语义 9 | - override、final 10 | - 3种智能指针 11 | - atomic实现原子操作 12 | 13 | ## atomic 14 | 15 | 对int、char等进行原子封装,确保同一时刻只有一个线程对其访问,效率比互斥锁更高,实现数据结构的无锁设计 16 | ### 类型自动推导auto 17 | 1. auto 类型说明符让编译器分析表达式所属的类型 18 | 2. 使用auto必需一个初始化值 19 | 20 | ## ThreadLocal 21 | 22 | 想让多个函数内共享一个变量。即一个变量要跨越多个函数的生命周期,并且不同线程需要不同的存储空间,那么需要thread local 23 | 该关键词修饰的变量具有线程周期,线程开始的时候被生成,在线程结束的时候被销毁,每一个线程都拥有一个独立的变量实例 24 | 25 | 26 | **可用于多线程无锁编程** 27 | 28 | 29 | ### 自动化推导decltype 30 | decltype让你从一个变量(或表达式)中得到类型 31 | 32 | 33 | - 如果表达式e是一个变量,那么就是这个变量的类型。 34 | - 如果表达式e是一个函数,那么就是这个函数返回值的类型。 35 | - 如果不符合1和2,如果e是左值,类型为T,那么decltype(e)是T&;如果是右值,则是T。 36 | 37 | ### nullptr 38 | nullptr是为了解决原来C++中NULL的二义性问题而引进的一种新的类型,因为NULL实际上代表的是0 39 | f 40 | ### lambda匿名函数 41 | ## 智能指针 42 | #### 原理 43 | 智能指针是一个类,用来存储指向动态分配对象的指针,负责自动释放动态分配的对象,防止堆内存泄漏。动态分配的资源,交给一个类对象去管理,当类对象声明周期结束时,自动调用析构函数释放资源 44 | #### 分为以下几类 45 | - unique_ptr: 如果内存资源的所有权不需要共享,就应当使用这个(它没有拷贝构造函数),但是它可以转让给另一个unique_ptr(存在move构造函数)。 46 | - 关联的原始指针的唯一所有者 47 | - shared_ptr 48 | - 允许多个智能指针指向同一个对象,每当多一个指针指向该对象时,指向该对象的所有智能指针内部的引用计数加1,每当减少一个智能指针指向对象时,引用计数会减1,当计数为0的时候会自动的释放动态分配的资源 49 | - 对一个对象进行赋值时,赋值操作符**减少**左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并**增加**右操作数所指对象的引用计数 50 | - 引用计数是原子操作,线程安全 51 | - 代码实现(很大概率会考) 52 | 53 | ```c++ 54 | template 55 | class Ref_count{ 56 | private: 57 | T* ptr; //数据对象指针 58 | int* count; //引用计数器指针 59 | public: 60 | /* 61 | 普通指针构造共享指针,注意这样有问题,造成二龙治水 62 | 因为同一块内存的普通指针构建的共享指针也指的是同一块内存,所以不应该是1,应该++ 63 | 比如shared_ptr s_ptr(p); s_ptr指向了这块地址,pCount = 1 64 | shared_ptr s_ptr1 = s_ptr; s_ptr1也指向了这块地址,pCount = 2 65 | shared_ptr s_ptr2(p); s_ptr2也指向了这块地址,不过重新创建了引用计数,pCount1 = 1,这样显然不行*/ 66 | //所以要避免一个原生指针多次使用这个函数 67 | Ref_count(T* t):ptr(t),count(new int(1)){} 68 | 69 | 70 | 71 | ~Ref_count(){ 72 | decrease(); 73 | } 74 | 75 | //拷贝构造 76 | Ref_count(const Ref_count& tmp){ 77 | count = tmp->count; 78 | ptr = tmp->ptr 79 | increase(); 80 | } 81 | 82 | //注意=在指针里面是指向的意思,因此说明=左边的共享指针指向了=右边的 83 | //因此=左边的共享指针-1,=右边的共享指针+1 84 | Ref_count& operator=(const Ref_count& tmp){ 85 | if(tmp != this){ 86 | decrease(); 87 | ptr = tmp->ptr; 88 | count = tmp->count; 89 | increase(); 90 | } 91 | return *this 92 | } 93 | 94 | T* operator ->() const{ 95 | return ptr; 96 | } 97 | 98 | T& operator *() const{ 99 | return *ptr; 100 | } 101 | 102 | void increase(){ 103 | if(count){ 104 | *(count)++; 105 | } 106 | } 107 | 108 | void decrease(){ 109 | if(count){ 110 | *(count)--; 111 | if(*count == 0){ 112 | //引用计数为0的时候就删除数据对象指针和引用对象指针 113 | delete ptr; 114 | ptr = nullptr; 115 | delete count; 116 | count = nullptr; 117 | } 118 | } 119 | } 120 | 121 | T* get() const{ 122 | return ptr; 123 | } 124 | 125 | int get_count() const{ 126 | if(!count){ 127 | return 0; 128 | } 129 | return *count; 130 | } 131 | }; 132 | ``` 133 | 134 | 引出循环引用问题:两个类对象中各自有一个 shared_ptr 指向对方时,会造成循环引用,使引用计数失效,从而导致内存泄露 135 | 136 | - weak_ptr: 137 | - 不控制对象生命周期的智能指针 138 | - 持有被shared_ptr所管理对象的引用,但是不会改变引用计数值。 139 | - 将循环引用的一方修改为弱引用,可以避免内存泄露 140 | - 成员函数 141 | - expired 用于检测所管理的对象是否已经释放, 如果已经释放, 返回 true; 否则返回 false 142 | - 使用之前使用函数lock()检查weak_ptr是否为空指针 143 | - weak_ptr 支持拷贝或赋值, 但不会影响对应的 shared_ptr 内部对象的计数 144 | - reset 将 weak_ptr 置空 145 | - 另一方面,auto_ptr已经被废弃,不会再使用了。 -------------------------------------------------------------------------------- /C++/C++语法.md: -------------------------------------------------------------------------------- 1 | # C++基础语法 2 | 3 | ## 面向对象和面向过程语言区别 4 | 5 | 面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用 6 | 7 | 面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为 8 | 9 | ## 指针与引用区别 10 | 11 | 1. 指针是一个变量,存储的是一个地址,引用是原变量的别名 12 | 2. 指针可以有多级,引用只有一级 13 | 3. 指针可以为空,引用不能为NULL且在定义时必须初始化 14 | 4. 指针在初始化后可以改变指向,而引用在初始化之后不可再改变 15 | 5. sizeof指针得到的是本指针的大小,sizeof引用得到的是引用所指向变量的大小 16 | 17 | 指针类型所占字节数:32位系统中4字节,64位系统中8字节 18 | 19 | 20 | <引导出什么时候用指针或引用> 21 | 22 | ### 什么时候指针,什么时候引用,什么时候值传递 23 | 24 | 仅用传递的值,不对值修改 25 | 26 | (1)如果数据对象很小,如内置数据类型或小型结构,使用按值传递。 27 | (2)如果数据对象是数组,则使用指向const的指针。 28 | (3)如果数据对象是类对象,则使用const引用。 29 | 30 | 修改传递过来的值 31 | 32 | (1)如果数据对象是内置数据类型,则使用指针/引用。 33 | (2)如果数据对象是数组,则只能使用指针。 34 | (3)如果数据对象是类对象,则使用引用。 35 | 36 | 子问题1: 37 | #### 常量指针与指针常量的区别 38 | - 指针常量是一个指针,读成常量的指针,指向一个只读变量 39 | - 常量指针是一个不能给改变指向的指针:指针是个常量,必须初始化,一旦初始化完成,它的值(也就是存放在指针中的地址)就不能在改变了 40 | 41 | 子问题2: 42 | #### 数组指针与指针数组的区别 43 | - 数组指针:是一个指针变量,指向了一个一维数组,`int (*p)[233]` 44 | - 指针数组:是一个数组,只不过数组的元素存储的是指针变量,`int *p[233]` 45 | 46 | ## malloc/free与new/delete区别 47 | 48 | 相同: 49 | 50 | - 都可用于内存的动态申请和释放 51 | 52 | 不同: 53 | 54 | - 后者是C++运算符,前者是C/C++语言标准库函数 55 | - new自动计算要分配的空间大小,malloc需要指定空间大小 56 | - new调用名为**operator new**的标准库函数分配足够空间并调用相关对象的构造函数,返回的是对象类型的指针 57 | - malloc内存分配成功则返回void* 58 | - delete对指针所指对象运行适当的析构函数;然后通过调用名为**operator delete**的标准库函数释放该对象所用内存。 59 | - malloc失败返回空指针,new失败抛出异常 60 | 61 | 子问题1: 62 | 63 | ### 为什么加入new/delete: 64 | 65 | - 在对非基本数据类型的对象使用的时候,对象创建的时候还需要执行构造函数,销毁的时候要执行析构函数 66 | 67 | 子问题2: 68 | 69 | ### 被free的内存会直接返回函数? 70 | 71 | 不是的,被free回收的内存会首先一个双链表保存起来,当用户下一次申请内存的时候,会尝试从这些内存中寻找合适的返回。这样就避免了频繁的系统调用,占用过多的系统资源。同时该双链表也会尝试对小块内存进行合并,避免过多的内存碎片。 72 | 73 | 74 | 75 | ## 变量声明与定义区别 76 | 77 | - 声明仅提供变量的声明的位置及类型提供给编译器,并不分配内存空间; 78 | - 定义要在定义的地方为其分配存储空间 79 | - 相同变量可以在多处声明(外部变量extern),但只能在一处定义 80 | 81 | 82 | 83 | ## strlen与sizeof区别 84 | 85 | - sizeof是运算符,并不是函数,结果在编译时得到而非运行中获得;strlen是字符处理的库函数 86 | - sizeof参数可以是任何数据的类型或者数据(sizeof参数不退化);strlen的参数只能是字符指针且结尾是'\0'的字符串。 87 | 88 | ## C++与其他语言的比较(优点) 89 | ### C++与Python区别 90 | 91 | - Python是一种脚本语言,是解释执行的,而C++是编译语言,是需要编译后在特定平台运行的 92 | - 二者都可跨平台,python效率没有C++高 93 | - C++中需要事先定义变量的类型,而Python不需要,Python的基本数据类型只有数字,布尔值,字符串,列表,元组等等 94 | 95 | 96 | 97 | ### C++与C语言区别 98 | 99 | - 在C++中,允许有相同的函数名,不过它们的参数类型不能完全相同,这样这些函数就可以相互区别开来。而这在C语言中是不允许的,一个面向过程,另一个面向对象 100 | - C++语言中,允许变量定义语句在程序中的任何地方,只要在是使用它之前就可以;而C语言中,必须要在函数开头部分 101 | - 在C++中,除了值和指针之外,新增了引用。引用型变量是其他变量的一个别名,我们可以认为他们只是名字不相同,其他都是相同的 102 | - static:C中仅用于定义局部静态变量和全局静态变量,C++还会定义静态成员变量和静态成员函数,与类绑定 103 | - 结构体:C中结构体没有函数成员,数据成员也没有private、protected等访问限制,此外也没有继承关系 104 | 105 | 106 | 107 | 108 | ## define宏定义与const的区别 109 | 从三方面回答: 110 | 1. 发挥作用时间不同 111 | 2. 占用空间不同 112 | 3. 有无类型检查 113 | 114 | 具体解释: 115 | 116 | - define是在编译的**预处理**阶段起作用,而const是在**编译、运行**的时候起作用 117 | - define只做替换,不做类型检查和计算,容易产生错误 118 | - const常量有数据类型,编译器可以对其进行类型安全检查 119 | - define只是将宏名称进行替换,在内存中会产生多分相同的备份。const在程序运行中只有一份备份,且可以执行常量折叠 120 | 121 | 同样还有define与typedef区别 122 | 1. typedef在编译阶段有效,类型检查;define是宏定义,仅替换 123 | 2. define没有作用域的限制,而typedef有自己的作用域 124 | 125 | 126 | ## define宏加括号和不加括号的区别 127 | 当宏中包含运算符时要在最外层加括号,不然可能会出错 128 | 129 | ```c++ 130 | #define A(x) x+x 131 | #define B(x) (x+x) 132 | 133 | ``` 134 | 执行`A(x)*A(x)`与`B(x)*B(x)`结果不同 135 | 136 | ## C++中static作用 137 | 138 | - 不考虑类 139 | 140 | - static 修饰全局变量或函数的时候,这个全局变量只能在本文件中访问,不能在其它文件中访问,即便是 extern 外部声明也不可以 141 | 142 | - 默认初始化为0,都在全局初始化区data段 143 | 144 | - 在修饰变量的时候,static 修饰的静态局部变量只执行初始化一次,而且延长了局部变量的生命周期,直到程序运行结束以后才释放 145 | 146 | - 类 147 | 148 | - static成员变量:只与类关联。定义时要分配空间,不能在类声明中初始化,必须在**类定义体外部初始化**,初始化时不需要标示为static;可以被非static成员函数任意访问 149 | - static成员函数:不具有this指针,无法访问类对象的非static成员变量和非static成员函数;**不能被声明**为const、虚函数和**volatile**;可以被非static成员函数任意访问 150 | 151 | 152 | 153 | ## C++的顶层const与底层const 154 | 155 | - **顶层**const:指的是const修饰的变量**本身**是一个常量,无法修改,指的是指针 156 | - **底层**const:指的是const修饰的变量**所指向的对象**是一个常量,指的是所指变量 157 | 158 | 159 | 160 | ## extern 161 | 162 | extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用 163 | 164 | 与extern对应的关键字是static,被它修饰的全局变量和函数只能在本模块中使用 165 | 166 | extern C用法 167 | 168 | - **指示编译器这部分代码按C语言的进行编译,而不是C++的** 169 | - 够**正确的在C++代码中调用**C语言 170 | 171 | 172 | 173 | ## 野指针与悬空指针 174 | 175 | - 野指针, 176 | - 指的是**没有被初始化过**的指针 177 | - 指针释放后未置空 178 | - 指针操作超出了变量的作用域 179 | - 解决:为了防止出错,对于指针初始化时都是赋值为 nullptr,这样在使用时编译器就会直接报错,产生非法内存访问。 180 | 181 | - 悬空指针,指针最初指向的**内存已经被释放了的一种指针** 182 | - 解决:引入智能指针,避免悬空指针 183 | 184 | 185 | 186 | ## 类型安全 187 | 188 | - 类型安全的代码**不会试图访问自己没被授权的内存区域** 189 | - C++如何保障 190 | - 操作符new返回的指针类型严格与对象匹配,而不是void* 191 | - C中很多以void*为参数的函数可以改写为C++模板函数,而模板是支持类型检查的 192 | - **dynamic_cast**关键字,使得转换过程更加安全 193 | 194 | 195 | ## 浅拷贝与深拷贝区别 196 | 197 | - 浅拷贝 198 | - 将源对象的值拷贝到目标对象中,如果对象中有某个成员是指针类型数据,并且是在堆区创建,则使用浅拷贝仅仅拷贝的是这个指针变量的值,没有新开辟一个地址,拷贝的指针和原来的指针指向同一块地址,浅复制仅仅是指向被复制的内存地址,如果原地址中对象被改变了,那么浅复制出来的对象也会相应改变 199 | 200 | - 深拷贝不仅拷贝值,还开辟出一块新的空间用来存放新的值,即使原先的对象被析构掉,释放内存了也不会影响到深拷贝得到的值 201 | 202 | 203 | 204 | ## 内联函数与宏定义区别 205 | 206 | - 宏只做简单字符串替换(预处理)。而内联函数**可以进行参数类型检查**(编译时),且具有返回值 207 | 208 | 209 | 210 | ## 大小端存储 211 | 212 | - 大端存储:字数据的高字节存储在低地址中 213 | - 小端存储:字数据的低字节存储在低地址中 214 | - 在Socket编程中,往往需要将操作系统所用的**小端存储的IP地址转换为大端存储**,这样才能进行网络传输 215 | 216 | 217 | 218 | ## volatile、mutable与explicit关键字用法 219 | volatile 220 | 221 | - volatile 关键字是一种类型修饰符,**用它声明的类型变量表示可以被某些编译器未知的因素更改**,编译器不要对该变量访问擅自优化。 222 | - 要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据 223 | 224 | - **volatile定义变量的值是易变的,每次用到这个变量的值的时候都要去重新读取这个变量的值,而不是读寄存器内的备份。多线程中被几个任务共享的变量需要定义为volatile类型**。volatitle是防止编辑器对该代码优化, 225 | 226 | const 227 | 228 | - 要在const函数里面修改一些跟类状态无关的数据成员,那么这个数据成员就应该被mutable来修饰,并且放在函数后后面关键字位置 229 | 230 | explict 231 | 232 | - explicit关键字用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换,只能以**显示的方式进行类型转换** 233 | - 因为仅含一个参数的构造函数和除了第一个参数外其余参数都有默认值的多参构造函数承担了两个角色。 第一个是成为带参数的构造函数,第二个是一个默认且隐含的类型转换操作符 234 | 235 | 236 | 237 | ## C++中几个new 238 | 239 | - **plain new**,是普通的new,就是我们常用的new 240 | - **nothrow new**:在空间分配失败的情况下是不抛出异常,而是返回NULL 241 | - **placement new** : `operator new`重载版本, 在一块已经分配成功的内存上重新构造对象或对象数组,做的唯一一件事情就是**调用对象的构造函数** 242 | - 创建的对象使用完毕,必须**显式的调用他们的析构函数**来销毁,但不会释放内容 243 | - 如何释放内存 244 | - 缓冲区在堆,`delete []buf` 245 | - 在栈,跳出作用域,内存自动释放 246 | - 构造起来的对象或数组大小并不一定等于原分配的内存大小 247 | 248 | ## malloc、realloc、calloc区别 249 | 250 | - malloc:申请的空间的值是随机初始化的 251 | - calloc:申请的空间的值是**初始化为0**的 252 | - realloc:给动态分配的空间**分配额外的空间**,用于扩充容量 253 | 254 | 255 | 256 | ## 什么是内存泄漏?如何检测与避免 257 | 258 | - 由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况 259 | - 没有将父类的析构函数定义为虚函数,当父类的指针指向子类对象时,delete该对象不会调用子类的析构函数 260 | - 后果: 261 | - 性能下降到内存逐渐用完,导致另一个程序失败 262 | 263 | 解决办法: 264 | - 智能指针 265 | - 把new和delete全部都封装到构造函数和析构函数中,保证任何资源的释放都在析构函数中进行 266 | 267 | 如何定位 268 | - 检查内存使用情况,Linux下是`ps -aux` 269 | 270 | - Linux下可以使用**Valgrind**工具 271 | 272 | 273 | 274 | 275 | ## 面向对象的三大特性 276 | 277 | 1. 继承:**让某种类型对象获得另一个类型对象的属性和方法** 278 | 2. 封装:**把客观事物封装成抽象的类**,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏 279 | 3. 多态:基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,或者说有多种表现方式。**重载实现编译时多态,虚函数实现运行时多态** 280 | 1. 静态多态与动态多态 281 | - 静态多态:对于相关的对象类型,直接实现它们各自的定义,不需要共有基类,甚至可以没有任何关系。只需要各个具体类的实现中要求相同的接口声明 282 | - 动态多态 283 | 对于相关的对象类型,确定它们之间的一个共同功能集,然后在基类中,把这些共同的功能声明为多个公共的虚函数接口。各个子类重写这些虚函数,以完成具体的功能。具体实现就是c++的虚函数 284 | 2. 实现动态多态条件: 285 | 1. 要有继承关系 286 | 2. 要有虚函数重写(被 virtual 声明的函数叫虚函数) 287 | 3. 父类指针(父类引用)指向子类对象 288 | 3. 动态多态原理 289 | 1. 当类中声明虚函数时,编译器会在类中生成一个虚函数表,虚函数表是一个存储类虚函数指针的数据结构, 虚函数表是由编译器自动生成与维护的。virtual 成员函数会被编译器放入虚函数表中,存在虚函数时,每个对象中都有一个指向虚函数表的指针(vptr 指针)。在多态调用时, vptr 指针就会根据这个对象在对应类的虚函数表中查找被调用的函数,从而找到函数的入口地址。 290 | 291 | 292 | 293 | ## C++的四种强制转换 294 | 295 | - `reinterpret_cast (expression)`:完成任意指针类型向任意指针类型的转换 296 | 297 | - 既不会有指向内容的检查,也不会有指针本身类型的检查 298 | 299 | - `const_cast (expression)`:设置或移除指针所指向对象的const 300 | - **常量**指针被转化成**非常量**的指针,并且仍然指向原来的对象 301 | - **常量**引用被转换成**非常量**的引用,并且仍然指向原来的对象 302 | - const_cast一般用于修改底指针 303 | 304 | - `static_cast (expression)`:将expression转为type-id 305 | - 用于类层次结构中基类(父类)和派生类(子类)之间指针或引用引用的转换 306 | - 派生类指针/引用转基类是安全 307 | - 基类指针/引用转派生类,因无动态类型检查,不安全 308 | - 用于基本数据类型之间的转换,如把int转换成char,把int转换成enum 309 | - 空指针转换成目标类型的空指针 310 | - 不能转换掉expression的const、volatile 311 | 312 | - `dynamic_cast (expression)`: 313 | - 只能用于含虚函数的类的指针或引用 314 | 315 | - 运行时转换,需要知道类对象的信息 316 | 317 | - 通过虚函数表获取该信息 318 | - 通过虚函数表指针,获得该类对象的所有虚函数,用来判断对象有无继承关系 319 | 320 | - 确保目标指针类型指向一个有效且完整的对象 321 | 322 | - 允许派生类向基类转换 323 | 324 | - 基类向派生类转换,仅当转过去的指针指向的目标对象有效且完整 325 | 326 | - 返回空指针表示转换失败 327 | 328 | - 若是无法完成的引用转换,抛出bad_cast 329 | 330 | **RTTI(Run-Time Type Identification)**;为了让程序在运行时能根据基类的指针或引用来获得该指针或引用所指的对象的实际类型 331 | 332 | - 运行时类型信息,指在运行时确定对象类型 333 | 334 | 335 | 336 | ## 是否遇到coredump,如何解决 337 | 338 | - 程序由于异常或者bug在运行时异常退出或者终止 339 | 340 | 341 | 342 | ## strcpy与memcpy区别 343 | 344 | - 前者仅复制字符串,后者复制任意类型 345 | - 前者遇到'\0'停下,后者根据第三个参数决定复制的长度 346 | 347 | 348 | 349 | 350 | ## 静态绑定与动态绑定 351 | 352 | - 静态类型:声明时的类型,编译时确定,无法更改 353 | - 动态类型:运行期间决定,可以更改 354 | - 静态绑定:绑定的是对象的静态类型 355 | - 动态绑定:绑定动态类型 356 | 357 | 358 | 359 | 360 | ## 回调函数 361 | 362 | - 相当于一个中断处理函数,由系统在符合你设定的条件时自动调用 363 | - 1,声明;2,定义;3,设置触发条件,就是在你的函数中把你的回调函数名称转化为地址作为一个参数,以便于系统调用 364 | - 函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数 365 | 366 | 367 | 368 | ## 一致性哈希,负载均衡相关 369 | - 哈希算法存在的问题 370 | - 哈希算法虽然能建立数据和节点的映射关系,但是每次在节点数量发生变化的时候,最坏情况下所有数据都需要迁移,这样太麻烦了,所以不适用节点数量变化的场景 371 | - 一致性哈希算法 372 | - 概念 373 | - 指将「存储节点」和「数据」都映射到一个首尾相连的哈希环上,如果增加或者移除一个节点,仅影响该节点在哈希环上顺时针相邻的后继节点,其它数据也不会受到影响 374 | - 应该满足的条件 375 | 1. 均衡性:哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用 376 | 2. 单调性:当缓冲区大小变化时,一致性哈希(Consistent hashing)尽量保护已分配的内容不会被重新映射到新缓冲区 377 | 3. 分散性:避免相同的内容被不同的终端映射到不同的缓冲区的情况 378 | 4. 负载:降低缓冲的负荷 379 | - 环形hash空间 380 | - 将各个服务器使用Hash进行一个哈希,具体可以选择服务器的ip或唯一主机名作为关键字进行哈希,这样每台机器就能确定其在哈希环上的位置 381 | - 将objectA、objectB、objectC、objectD四个对象通过特定的Hash函数计算出对应的key值,然后散列到Hash环上,然后从数据所在位置沿环顺时针“行走”,第一台遇到的服务器就是其应该定位到的服务器 382 | - 如果此时NodeC宕机了,此时Object A、B、D不会受到影响,只有Object C会重新分配到Node D上面去,而其他数据对象不会发生变化 383 | - 环境中新增一台服务器Node X,通过hash算法将Node X映射到环中,通过按顺时针迁移的规则,那么Object C被迁移到了Node X中,其它对象还保持这原有的存储位置 384 | - 为了解决数据倾斜问题,一致性哈希算法引入了虚拟节点机制,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点 385 | 386 | 可有数据库解决缓存一致性问题引导而来,一致性哈希+singleflight 387 | ## 动态编译与静态编译 388 | 389 | - 静态编译:在编译时,把所有模块都编译进可执行文件里,当启动这个可执行文件时,所有模块都被加载进来 390 | - 静态库优点:代码的装载速度快,执行速度也比较快 391 | - 缺点: 392 | - 程序体积会相对大一些 393 | - 如果静态库需要更新,程序需要重新编译 394 | - 如果多个应用程序使用的话,会被装载多次,浪费内存 395 | 396 | - 动态编译:是将应用程序需要的模块都编译成动态链接库,启动程序(初始化)时,这些模块不会被加载,运行时用到哪个模块就调用哪个 397 | - 缺点: 398 | - 只用到了链接库的一两条命令,也需要附带一个相对庞大的链接库 399 | - 如果没有安装对应的运行库,则用动态编译的可执行文件就不能运行 400 | - 优点: 401 | - 缩小了执行文件本身的体积 402 | - 是加快了编译速度,节省了系统资源 403 | 404 | 405 | 406 | ## 为什么不把所有函数写成内联 407 | 408 | - 函数体内的代码比较长,将导致内存消耗代价 409 | - 函数体内有循环,函数执行时间要比函数调用开销大 410 | 411 | ## 内联函数的作用 412 | 413 | - 函数调用时候需要创建时间、参数传入传递等操作,造成了时间和空间的额外开销。 414 | - 通过编译器预处理,在调用内联函数的地方将内联函数内的语句复制到调用函数的地方,也就是直接展开代码执行,从而提高了效率,减少了一些不必要的开销。同时内联函数还能解决宏定义的问题 415 | 416 | 417 | 418 | ## C++的程序内存分区 419 | 高地址->低地址 420 | 421 | - 栈区:在执行函数时,**函数内局部变量**的存储单元都可以在栈上创建。函数执行结束时这些存储单元自动被释放 422 | 423 | - 堆区:由 new分配的内存块。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收 424 | - 堆区与栈区之间有一个共享区,也就是文件映射区 425 | - BSS段:程序中**未初始化的全局变量和静态变量**的内存区域 426 | - data段:**已初始化的全局变量和静态变量**的内存区域 427 | 428 | - 代码区:存放函数体的二进制代码,只读,**不允许修改**。头部还会包括一些**只读常量** 429 | 430 | 这里会牵扯到堆与栈的区别,详见OS文件夹 431 | 432 | ## 内存错误检测,GDB的使用 433 | 434 | `Address Sanitizer(ASan)`是一个快速的内存错误检测工具,从gcc 4.9开始支持 435 | 436 | 使用步骤: 437 | 438 | 1. 用`-fsanitize=address`选项编译和链接你的程序。 439 | 440 | 2. 用`-fno-omit-frame-pointer`编译,以得到更容易理解stack trace。 441 | 442 | 3. 可选择-O1或者更高的优化级别编译 443 | 444 | 4. ``` 445 | gcc -fsanitize=address -fno-omit-frame-pointer -O1 -g use-after-free.c -o use-after-free 446 | ``` 447 | 448 | 449 | 450 | 451 | ## RAII 452 | 453 | - 直译过来是“资源获取即初始化”,也就是说在构造函数中申请分配资源,在析构函数中释放资源 454 | 455 | 456 | 457 | ## 2.左值引用和右值引用 458 | 459 | - 取地址的、有名字的就是左值;反之,不能取地址的、没有名字、临时值,就是右值,诸如表达式中间结果/函数返回值(可能拥有变量名,也可能没有) 460 | 461 | - 右值值引用通常不能绑定到任何的左值,要想绑定一个左值到右值引用,通常需要std::move()将左值强制转换为右值 462 | - 右值引用的意义:移动语义,实现各种情形下对象的资源所有权转移 463 | 464 | - 右值引用特点 465 | - 通过右值引用的声明,右值又“重获新生”,其生命周期与右值引用类型变量的生命周期一样长,只要该变量还活着,该右值临时量将会一直存活下去 466 | 467 | - 右值引用独立于左值和右值。意思是右值引用类型的变量可能是左值也可能是右值 468 | 469 | - T&& t在发生自动类型推断的时候,它是左值还是右值取决于它的初始化。 470 | 471 | - ```c++ 472 | int a = 10; 473 | int& b = a; //b是左值引用 474 | int& c = 10; //错误,c是左值不能使用右值初始化 475 | int&& d = 10; //正确,右值引用用右值初始化 476 | int&& e = a; //错误,e是右值引用不能使用左值初始化 477 | const int& f = a; //正确,左值常引用相当于是万能型,可以用左值或者右值初始化 478 | const int& g = 10;//正确,左值常引用相当于是万能型,可以用左值或者右值初始化 479 | const int&& h = 10; //正确,右值常引用 480 | const int& aa = h;//正确 481 | int& i = getInt(); //错误,i是左值引用不能使用临时变量(右值)初始化 482 | int&& j = getInt(); //正确,函数返回值是右值 483 | fun(10); //此时fun函数的参数t是右值 484 | fun(a); //此时fun函数的参数t是左值 485 | return 0; 486 | ``` 487 | 488 | - 应用场景 489 | 490 | - **实现移动语义,避免拷贝,从而提升程序性能** 491 | - 移动语义:使用std::move可以强制将左值引用转为右值引用。而对于右值引用,程序可以调用移动构造函数进行对象的构造,减少了原来调用拷贝构造函数的时候很大的开销 492 | - 都实现了以**右值引用为参数**的`移动构造函数`和`移动赋值重载函数`,或者其他函数,最常见的如std::vector的`push_back`和`emplace_back`。参数为左值引用意味着拷贝,为右值引用意味着移动。 493 | - 模板函数按照参数实际类型转发,所谓“完美转发” 494 | 495 | 496 | ## C++多态如何实现 497 | 498 | 1. 在基类的函数前加上**virtual**关键字,在派生类中重写该函数,运行时将会根据所指对象的实际类型来调用相应的函数,如果对象类型是派生类,就调用派生类的函数,如果对象类型是基类,就调用基类的函数 499 | 2. 多态原理 500 | 1. 虚表:虚函数表的缩写,类中含有virtual关键字修饰的方法时,编译器会自动生成虚表 501 | 2. 虚表指针:在含有虚函数的类实例化对象时,对象地址的前四个字节存储的指向虚表的指针 502 | 3. 实现多态过程: 503 | - 编译器会自动为每个含有虚函数的类生成一份虚表,该表是一个一维数组,虚表里保存了虚函数的入口地址 504 | - 会在每个对象的前四个字节中保存一个虚表指针,即**vptr**,指向对象所属类的虚表。在构造时,根据对象的类型去初始化虚指针vptr,从而让vptr指向正确的虚表,从而在调用虚函数时,能找到正确的函数 505 | - 在派生类定义对象时,程序运行会自动调用构造函数,在构造函数中创建虚表并对虚表初始化。在构造子类对象时,会先调用父类的构造函数 506 | - 当调用子类的构造函数时,为子类对象初始化虚表指针,令它指向子类的虚表 507 | - 当派生类对基类的虚函数没有重写时,派生类的虚表指针指向的是基类的虚表;当派生类对基类的虚函数重写时,派生类的虚表指针指向的是自身的虚表;当派生类中有自己的虚函数时,在自己的虚表中将此虚函数地址添加在后面 508 | 3. 虚函数表带来的成本问题 509 | - 使用虚函数时,对于内存和执行速度方面会有一定的成本: 510 | - 1. 每个对象都会变大,变大的量为存储虚函数表指针; 511 | 2. 对于每个类,编译器都会创建一个虚函数表 512 | 3. 对于每次调用虚函数,都需要额外执行一个操作,就是到表中查找虚函数地址 513 | 514 | 515 | 516 | ## C++程序优化方法 517 | 空间足够时,可以将经常需要读取的资源,缓存在内存中 518 | 尽量减少大内存对象的构造与析构 519 | 尽量使用C++11的右值语义,减少临时对象的构造 520 | 优化线程或进程的同步方式,能用原子操作的就不用锁 521 | 优化堆内存的使用,如果有内存频繁的申请与释放,可以考虑内存池】 522 | 523 | 524 | ## 内存问题 525 | 1. 指针悬挂/野指针,用shared_ptr和weak_ptr就可以解决 526 | 2. 重复释放,用boost的scoped_ptr或者仔细检查,只在对象析构的时候释放一次 527 | 3. 内存泄漏,也可以用scoped_ptr 528 | 4. new[]和delete不配对,把new[]统统换成vector就行 529 | 530 | 531 | ## 模板和继承,这个区别是什么? 532 | 533 | 第一点: 534 | 535 | 模板可以生成一组类或者函数,这些类或函数的实现都是一样的 536 | 537 | 继承是事物之间的一种关系,从父类到子类实际上就是从普遍到特殊、从共性到特性 538 | 539 | 第二点: 540 | 541 | 模板和继承都是多态性的体现,继承是运行时的多态性,模板是编译时的多态性。 542 | 543 | 第三点: 544 | 545 | 继承是数据的复制、模版是代码的复制。 546 | 547 | 模板函数在编译完成之后,会生成对应参数数类型的函数; 548 | 549 | 继承是对虚表、数据的复制 550 | 551 | 552 | 553 | ## C++如何保证线程安全 554 | 555 | 什么是线程安全: 556 | 557 | 在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染 558 | 559 | 1. 给共享的资源加把锁,保证每个资源变量每时每刻至多被一个线程占用 560 | 2. 让线程也拥有资源,不用去共享进程中的资源。如: 使用threadlocal可以为每个线程的维护一个私有的本地变量 -------------------------------------------------------------------------------- /C++/STL.md: -------------------------------------------------------------------------------- 1 | # STL 2 | ## RAII 3 | 直译过来是“资源获取即初始化”,也就是说在构造函数中申请分配资源,在析构函数中释放资源 4 | 5 | ## 迭代器与指针区别 6 | 迭代器实际上是对“遍历容器”这一操作进行了封装,迭代器实际上是对“遍历容器”这一操作进行了封装 7 | 8 | ## 容器中迭代器失效 9 | 10 | 1. 当容器调用erase()方法后,当前位置到容器末尾元素的所有迭代器全部失效。 11 | 2. 当容器调用insert()方法后,当前位置到容器末尾元素的所有迭代器全部失效。 12 | 3. 如果容器扩容,在其他地方重新又开辟了一块内存。原来容器底层的内存上所保存的迭代器全都失效了。 13 | 14 | - 对于序列式容器(如vector,deque,list等),删除当前的iterator会使后面所有元素的iterator都失效 15 | 1. 原因:vector,deque使用了连续分配的内存,删除一个元素导致后面所有的元素会向前移动一个位置。不过erase方法可以返回下一个有效的iterator 16 | 2. 当插入一个元素到vector中,由于引起了内存重新分配,所以指向原内存的迭代器全部失效 17 | - 对于链表式容器和关联式容器 18 | 1. 原因:链表的插入和删除节点不会对其他节点造成影响,因此只会使得当前的iterator失效 19 | 2. 解决办法:利用erase可以返回下一个有效的iteratord的特性,或者直接iterator++ 20 | 21 | 22 | ## 为何关联容器的插入删除效率一般比用其他序列容器高 23 | 24 | 关联容器一般指map,multimap,set,multiset这四种底层实现都是红黑树。对于关联容器来说,存储的只是节点。插入删除只是节点指针的换来换去,不需要做内存拷贝和内存移动 25 | ## 哈希表实现,如何解决哈希值冲突 26 | - 解决方法 27 | - 线性探测 28 | - 使用hash函数计算出的位置如果已经有元素占用了,则向后依次寻找,找到表尾则回到表头,直到找到一个空位 29 | - 开链 30 | - 每个表格维护一个list,如果hash函数计算出的格子相同,则按顺序存在这个list中 31 | - 再散列 32 | - 发生冲突时使用另一种hash函数再计算一个地址,直到不冲突 33 | 34 | ## vector扩容为什么2倍 35 | - 采用成倍方式扩容,可以保证常数的时间复杂度 36 | - 增加指定大小容量,仅达到O(n)时间复杂度 37 | ## map、set如何实现,红黑树如何同时实现两种容器,为什么使用红黑树 38 | - map与set底层都是以红黑树的结构实现,因此插入删除等操作都在O(logn)时间内完成,因此可以完成高效的插入删除 39 | 40 | - map和set要求是自动排序的,红黑树能够实现这一功能,而且时间复杂度比较低 41 | - 树高度越小越好,BST这种有特殊情况,比如只有左子树有值,导致O(n)复杂度 42 | ## unordered_map与map区别 43 | - unordered_map不会根据key的大小进行排序,后者按照键值排序 44 | - map中的元素是按照二叉搜索树存储,进行中序遍历会得到有序遍历 45 | - unordered_map的底层实现是hash_table,后者红黑树 46 | - map适用于有序数据的应用场景,unordered_map适用于高效查询的应用场景 47 | ## STL的heap实现 48 | - binary heap本质是一种complete binary tree(完全二叉树),整棵binary tree除了最底层的叶节点之外,都是填满的,但是叶节点从左到右不会出现空隙 49 | 50 | 大端堆建立 51 | ```c++ 52 | class Solution { 53 | public: 54 | void maxTrep(vector& nums,int l,int treapsize){ //将最大元素调整到堆顶 55 | int largest = l; 56 | if(l*2+1 < treapsize && nums[l*2+1] > nums[l]){ //完全二叉树性质 57 | largest = l*2+1; 58 | } 59 | if(l*2+2 < treapsize && nums[l*2+2] > nums[l]){ 60 | largest = l*2+2; 61 | } 62 | 63 | if(largest != l){ 64 | swap(nums[largest],nums[l]); 65 | maxTrep(nums,largest,treapsize); 66 | } 67 | } 68 | 69 | void buildTreap(vector& nums,int treapsize){ 70 | for(int i=treapsize/2;i>=0;i--){ 71 | maxTrep(nums,i,treapsize); 72 | } 73 | } 74 | 75 | int findKthLargest(vector& nums, int k) { 76 | int treapsize = nums.size(); 77 | buildTreap(nums,treapsize);//建堆 78 | ``` 79 | ## 红黑树概念 80 | - 首先是一个二叉排序树 81 | - 若左子树不空,则左子树上所有结点的值均小于或等于它的根结点的值 82 | - 若右子树不空,则右子树上所有结点的值均大于或等于它的根结点的值。 83 | - 左、右子树也分别为二叉排序树 84 | - 满足以下请求 85 | - 树中所有节点非红即黑 86 | - 根节点必为黑节点 87 | - 红节点的子节点必为黑(黑节点子节点可为黑) 88 | - 从根到NULL的任何路径上黑结点数相同 89 | - 查找时间一定可以控制在O(logn) 90 | ## 常见容器 91 | - **顺序容器**:vector、deque、list 92 | - vector 采用一维数组实现,元素在内存连续存放,不同操作的时间复杂度为: 插入: O(N) 查看: O(1) 删除: O(N) 93 | - deque 采用双向队列实现,元素在内存连续存放,不同操作的时间复杂度为: 插入: O(N) 查看: O(1) 删除: O(N) 94 | - list 采用双向链表实现,元素存放在堆中,不同操作的时间复杂度为: 插入: O(1) 查看: O(N) 删除: O(1) 95 | - **关联式容器**:set、multiset、map、multimap 96 | - 均采用红黑树实现,红黑树是平衡二叉树的一种。不同操作的时间复杂度近似为: 插入: O(logN) 查看: O(logN) 删除: O(logN) 97 | - unordered_map、unordered_set、unordered_multimap、 unordered_multiset 98 | - 上述四种容器采用哈希表实现,不同操作的时间复杂度为: 插入: O(1),最坏情况O(N) 查看: O(1),最坏情况O(N) 删除: O(1) 99 | - **容器适配器**:栈、队列、优先级队列 100 | 101 | ## deque底层存储 102 | 103 | 存储数据的空间是由一段一段等长的连续空间构成,各段空间之间并不一定是连续的,可以位于在内存的不同区域。 104 | 105 | 当 deque 容器需要在头部或尾部增加存储空间时,它会申请一段新的连续空间,同时在 map 数组的开头或结尾添加指向该空间的指针,由此该空间就串接到了 deque 容器的头部或尾部。 -------------------------------------------------------------------------------- /C++/socket.md: -------------------------------------------------------------------------------- 1 | # Linux下的socket编程 2 | 3 | ## 什么是Socket? 4 | 5 | **对于 UNIX 系的操作系统,是利用 Socket 文件系统,Socket 是一种特殊的文件——每个都是一个双向的管道。一端是应用,一端是缓冲区(服务器)** 6 | 7 | 8 | 有客户端连接服务端时,服务端 Socket 文件中会写入这个客户端 Socket 的文件描述符。进程可以通过 accept() 方法,从服务端 Socket 文件中读出客户端的 Socket 文件描述符,从而拿到客户端的 Socket 文件。 9 | 10 | ## 进程如何监听关注集合的状态变化,比如说在有数据进来,如何通知到这个进程? 11 | **所有关注的 Socket 状态发生了变化,都由一个线程去处理,构成了 I/O 的多路复用问题** 12 | 13 | Linux提供三种多路复用问题的API:select、poll、epoll 14 | 15 | - select 16 | ```c++ 17 | fd_set read_fd_set, write_fd_set, error_fd_set; 18 | 19 | while(true) { 20 | 21 | select(..., &read_fd_set, &write_fd_set, &error_fd_set); 22 | 23 | } 24 | ``` 25 | 26 | 27 | - 具体操作 28 | - 每次 select 操作会阻塞当前线程,在阻塞期间所有操作系统产生的每个消息,都会通过遍历的手段查看是否在 3 个集合当中 29 | - 上面程序read_fd_set中放入的是当数据可以读取时进程关心的 Socket;write_fd_set是当数据可以写入时进程关心的 Socket;error_fd_set是当发生异常时进程关心的 Socket 30 | 31 | - **用户程序可以根据不同集合中是否有某个 Socket 判断发生的消息类型**:FD_ISSET 32 | - FD_SETSIZE 是一个系统的默认设置,通常是 1024. 33 | - FD_SETSIZE 是一个系统的默认设置,通常是 1024 34 | 35 | - poll 36 | - 阻塞调用,它将某段时间内操作系统内发生的且进程关注的消息告知用户程序 37 | - poll 函数的第一个参数告知内核 poll 关注哪些 Socket 及消息类型 38 | - poll 调用后,经过一段时间的等待(阻塞),就拿到了是一个消息的数组 39 | - 遍历这个数组中的消息,能够知道关联的文件描述符和消息的类型 40 | - 消息类型判断接下来该进行读取还是写入操作 41 | - 通过文件描述符,可以进行实际地读、写、错误处理 42 | 43 | - 内核在产生一个消息之后,依然需要遍历 poll 关注的所有文件描述符来确定这条消息是否跟用户程序相关 44 | 45 | - epoll 46 | - epoll 将进程关注的文件描述符存入一棵二叉搜索树,通常是红黑树的实现.当内核发送一个事件,能够立马找到进程是否关注该事件 47 | - 当有关注的事件发生时,epoll 会先放到一个队列当中.一个双链表,存储就绪的文件描述符列表 48 | - 调用epoll_wait,测此链表中是否有数据,有的话直接返回 49 | 50 | - 优势 51 | - 内部使用红黑树减少了内核的比较操作 52 | - 非阻塞的模型更容易处理各种各样的情况。程序员习惯了写出每一条语句就可以马上得到结果,这样不容易出 Bug 53 | 54 | - 两种触发模式 55 | - level (水平)模式:只要文件描述符还有数据可读,每次epoll_wait就会返回它的事件(只要有数据就触发) 56 | - edge (边缘)模式:只有数据流到来的时候才触发,不管缓冲区是否还有数据(只有数据流到来才会触发) 57 | 58 | 59 | **select/poll/epoll 三者都是同步调用**,可以确定程序执行的顺序的调用 -------------------------------------------------------------------------------- /C++/类.md: -------------------------------------------------------------------------------- 1 | # 类 2 | 3 | 4 | 5 | ## 类结构体内存对齐 6 | 7 | 8 | CPU只能在特定的地址处读取数据,所以在访问一些数据时,*对于访问未对齐的内存,处理器可能需要进行多次访问;而对于对齐的内存,只需要访问一次就可以* 9 | 10 | 第一个数据成员存放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小的整数倍开始 11 | 12 | 如果一个结构体里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储 13 | 14 | 结构体的总大小,即sizeof的结果,必须是其内部最大成员长度的整数倍,不足的要补齐 15 | 16 | 17 | 计算公式: 18 | - 前面的地址必须是后面的地址正数倍,不是就补齐 19 | - 整个Struct的地址必须是最大字节的整数倍 20 | 21 | 22 | ## 类的继承 23 | 24 | - 一个类继承了另一个类的属性和方法,这个新的类包含了上一个类的属性和方法,被称为子类或者派生类,被继承的类称为父类或者基类 25 | 26 | 引出问题,子类继承父类的类型,以及不同的权限、多种继承方式 27 | ### 继承和成员函数中,public、protected、private区别 28 | 29 | - 访问受限 30 | - public的变量和函数在类的内部外部都可以访问 31 | - protected的变量和函数只能在类的内部和其派生类中访问 32 | - private修饰的元素只能在类内访问 33 | - 继承权限 34 | - public继承:基类的公有成员和保护成员作为派生类的成员时,都保持原有的状态,而基类的私有成员任然是私有的,不能被这个派生类的子类所访问 35 | - protected继承:基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元函数访问,基类的私有成员仍然是私有的 36 | - private继承:是基类的所有公有成员和保护成员都成为派生类的私有成员,并不被它的派生类的子类所访问,基类的成员只能由自己派生类访问,无法再往下继承 37 | 38 | 39 | ### 单继承 40 | 从一个直接基类中产生派生类的能力 41 | ### 多继承 42 | 43 | 一个派生类有多个基类,对象可以调用多个基类中的接口 44 | 45 | 如果派生类所继承的多个基类有相同的基类,而派生类对象需要调用这个祖先类的接口方法,就会容易出现二义性 46 | 47 | ### 菱形继承 48 | 49 | 两个派生类继承同一个基类,同时两个派生类又作为基本继承给同一个派生类。这种继承形如菱形,故又称为菱形继承 50 | 51 | 存在问题: 52 | 53 | 1. 数据冗余(最下面的派生类会保留多份基类) 54 | 2. 二义性:不知道该以哪一个类作为中介访问基类。解决方法:作用域访问符:: 55 | 3. 56 | 如何解决:虚继承 57 | 58 | ### 虚继承 59 | 60 | 解决的问题:继承间接共同基类只保留一份成员,解决菱形继承的数据冗余问题和二义性 61 | 62 | 虚基类并不是在声明基类时声明的,而是在声明派生类时,指定继承方式(virtual)声明的。 63 | 64 | 形式:`class 派生类名:virtual public 基类名` 65 | 66 | 初始化: 67 | 1. 最后派生类不仅需要对直接基类初始化,还需要对虚基类初始化 68 | 2. 中间类按照继承类初始化 69 | 70 | 71 | ## 类中final与override关键字、default关键字 72 | 73 | - overide 74 | - 指定了子类的这个虚函数是重载的父类的虚函数 75 | - final 76 | - 不希望某个类被继承,或不希望某个虚函数被重写,可以在类名和虚函数后添加final关键字 77 | 78 | - default 79 | - 生成默认构造函数 80 | 81 | ## 类对象初始化方式?构造函数执行顺序?析构函数执行顺序?为什么用成员列表初始化更快 82 | 83 | - 两种初始化 84 | - 赋值初始化,在函数体内进行赋值初始化 85 | - 列表初始化:在冒号后使用初始化列表进行初始化 86 | - 区别: 87 | - 初始化列表会在程序刚开始运行的时候发生,而赋值是只有在程序执行到这条语句才会发生 88 | - 类成员在构造函数中执行的赋值语句之前已经被系统进 行了初始化,当执行赋值的时候就需要抹掉之前default的初始化的数据,这样就相当于多做了一次无用功,而构造函数中运行的初始化列表则不需要做这次无用功 89 | - 常量成员和引用成员只能使用初始化列表。这是c++的语法 90 | - 构造函数执行顺序 91 | - 父类构造函数 92 | - 成员变量的构造函数 93 | - 类自身的构造函数 94 | - 如果通过“父类::函数名”来在子类中访问父类的函数,此时不论该函数是否为虚函数,都会直接调用父类对应的函数 95 | - 原因:C++的赋值操作是会产生临时对象的。临时对象的出现会降低程序的效率 96 | - 析构函数执行顺序 97 | - 自身的析构函数 98 | - 成员变量的析构函数 99 | - 父类的析构函数 100 | 101 | 引导出,什么时候只能/最好用列表初始化 102 | 103 | 104 | ### 什么时候只能用到成员列表初始化? 105 | 106 | - 当初始化一个**引用成员**时 107 | - 当初始化一个**常量`const`成员**时 108 | - **没有默认构造函数**的成员对象 109 | - **基类数据成员**的初始化 110 | - 构造函数的参数名字与数据成员名字相同 111 | 112 | 113 | ## 拷贝构造函数 114 | 115 | 显式定义拷贝构造函数完成指针属性等拷贝工作,深拷贝 116 | 117 | ## 默认拷贝(赋值)构造函数 118 | 119 | 类中没有定义拷贝构造函数时,编译器会默认提供一个拷贝构造函数,进行成员变量之间的拷贝。这个拷贝操作是**浅拷贝** 120 | 导致两个指针成员指向同一地址,从而析构一个指针后,另一个指针就成了野指针 121 | 122 | 123 | 124 | ## 移动构造函数 125 | 126 | 首先说明产生移动构造函数原因。 127 | 128 | 介绍左值引用与右值引用 129 | 130 | 左值引用一般都是引用变量,右值引用则可以引用无名临时变量 131 | 132 | 133 | 134 | - 引入右值引用,避免没有必要的深拷贝操作 135 | - 移动语义:以移动而非深拷贝的方式初始化含有指针成员的类对象。因此就有一个移动构造函数 136 | 137 | - 其包含的指针成员指向的内存资源直接移给新对象所有,无需再新拷贝一份 138 | - 移动构造函数的初值是一个右值引用。 139 | 140 | 141 | 142 | ## 空类大小 143 | - 大小 144 | - 用sizeof()得到空类大小为1 145 | - 带有虚函数的空类,大小不为1;因其有一个vptr指向虚函数表,具体大小由指针大小确定 146 | - C++中要求对于类的每个实例都必须有独一无二的地址,那么编译器自动为空类分配一个字节大小,这样便保证了每个实例均有独一无二的内存地址 147 | 148 | ## 阻止一个类被实例化 149 | 150 | - 类定义为抽象基类或构造函数为private 151 | - 不允许类外部创建对象,仅在内部创建对象 152 | 153 | 154 | ## 禁止自动生成默认拷贝构造函数 155 | 156 | - 可以定一个base类,在base类中将拷贝构造函数和拷贝赋值函数设置成 private,那么派生类中编译器将不会自动生成这两个函数,且由于base类中该函数是私有的,因此,派 生类将阻止编译器执行相关的操作 157 | 158 | - 在**C++11**标准下,将这些函数声明为[删除的函数](https://link.jianshu.com/?t=https%3A%2F%2Fstackoverflow.com%2Fquestions%2F5513881%2Fmeaning-of-delete-after-function-declaration),在函数参数的后面加上=delete来指示出我们定义的删除的函数 159 | 160 | - 禁止原因 161 | - 类含有指针成员,调用默认拷贝构造函数,直接拷贝指针的值,使得两个指针指向同一地址 162 | 163 | - 析构的时候两次删除同一片区域的问题 164 | 165 | - 自定义了基类和派生类的拷贝构造函数,但派生类对象拷贝时,调用了派生类的拷贝,没有调用自定义的基类拷贝而是调用默认的基类拷贝。这样可能造成不安全 166 | 167 | 168 | ## 类对象存储空间大小 169 | 170 | - 非静态成员的数据类型大小之和 171 | - 内存对齐另外分配的空间大小 172 | - 编译器加入的额外成员变量(如指向虚函数表的指针vptr 173 | - 空类(无非静态成员)的对象size为1,作为基类时,size()为0 174 | 175 | 176 | 177 | ## 类的this指针 178 | 179 | - this 指针是一个隐含于每一个非静态成员函数中的特殊指针。它指向调用该非静态成员函数的那个对象 180 | - this指针只能在成员函数中使用,在全局函数、静态成员函数中都不能用this 181 | - this 并不是一个常规变量,而是个右值,所以不能取得 this 的地址 182 | - 在哪儿 183 | - 成员函数的其它参数正常都是存放在栈中。而this指针参数则是存放在寄存器中 184 | 185 | 186 | ### 构造函数或析构函数调用delete this的结果如何 187 | #### 构造函数调用delete this 188 | 189 | - 对象是通过new产生的,那么delete这个动作本身不会造成问题。问题是delete执行之后this就变成了野指针,对这个对象的任何操作都变成了对野指针的访问;操作系统有可能会把this指向的那块内存挪作它用,后果同样是不可预料的 190 | 191 | 具体代码可见: 192 | ```c++ 193 | class C 194 | { 195 | public: 196 | int m; 197 | C(int i=0) : m(i) { delete this; } 198 | }; 199 | int main() 200 | { 201 | C* p1 = new C(100); 202 | C* p2 = new C(200); 203 | cout << p1->m << ", " << p2->m << endl; 204 | p1->m = 100; 205 | cout << p1->m << ", " << p2->m << endl; 206 | p2->m = 200; 207 | cout << p1->m << ", " << p2->m << endl; 208 | return 0; 209 | } 210 | 211 | ``` 212 | 其输出是 213 | ``` 214 | 0, 0 215 | 100, 100 216 | 200, 200 217 | ``` 218 | p1的构造函数调用delete this后,OS回收该内存又分配p2,p2又释放,使得p1、p2指向同一内存,而OS中认为该内存已经没人使用。出现不可预料的结果。 219 | 220 | 221 | #### 析构函数调用delete this 222 | 223 | delete this先调用析构函数,析构函数再次调用delete,会再次调用析构函数 224 | 225 | - 堆栈溢出:形成无限递归 226 | 227 | 228 | ## 析构函数可否为虚函数 229 | 230 | - 当析构函数不被声明成虚函数,则编译器实施静态绑定,在删除**基类指针时(该指针指向一个派生类)**,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全,造成内存泄漏 231 | - 只有**在基类析构函数定义为虚函数**时,调用操作符delete销毁指向对象的基类指针时,才能准确调用派生类的析构函数(从该级向上按序调用虚函数),才能准确销毁数据 232 | - **析构函数可以是纯虚函数**,含有纯虚函数的类是抽象类,此时不能被实例化。但派生类中可以根据自身需求重新改写基类中的纯虚函数 233 | 234 | 235 | 236 | ## 构造函数能否为虚函数 237 | 238 | 首先回答,构造函数不能定义为虚函数 239 | 240 | - 虚函数对应一个vtable(虚函数表),类中存储一个vptr指向这个vtable。如果构造函数是虚函数,就需要通过vtable调用,可是对象没有初始化就没有vptr,无法找到vtable,所以构造函数不能是虚函数 241 | 242 | 243 | 244 | ## 虚函数表特征 245 | 246 | 1. 每个包含了虚函数的类都包含一个虚表。一个类继承了包含虚函数的基类,那么这个类也拥有自己的虚表 247 | 2. 虚表是一个指针数组,其元素是虚函数的指针,每个元素对应一个虚函数的函数指针。虚函数指针的赋值发生在编译器的编译阶段 248 | 3. **虚函数表**在Linux/Unix中位于可执行文件的**只读数据段中** 249 | 4. **虚函数**位于**代码段** 250 | 251 | 252 | ## 虚函数指针 253 | 254 | 为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表 255 | 256 | 为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*__vptr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。 257 | 258 | 259 | 260 | ## 构造函数、析构函数、虚函数可否声明为内联函数 261 | 262 | - **构造函数和析构函数声明为内联函数是没有意义的** 263 | class中的函数除虚函数外,默认是inline型的,编译器也只是有选择性的inline 264 | - 虚函数声明为inline 265 | - 内联函数是编译器选择。而虚函数是多态一种体现,多态表现在运行阶段。**故虚函数表现为多态时不可内联** 266 | - 编译器知道所调用的对象是哪个类。只有在编译器具有实际对象而不是对象的指针或引用时才会发生 267 | - **inline函数可以声明为虚函数** 268 | - 可以,不过编译器会忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去 269 | 270 | - 内联函数优缺点 271 | - 优点 272 | - 把内联函数里面的代码写在调用内联函数处。不用执行进入函数的步骤,更像是宏,但却比宏多了类型检查,真正具有函数特性 273 | - 类声明中定义的函数,除了虚函数的其他函数都会自动隐式地当成内联函数 274 | - 编译器会为所用 inline 函数中的局部变量分配内存空间 275 | - 缺点 276 | - 内联是以代码膨胀(复制)为代价,消除函数调用带来的开销 277 | - inline函数的改变需要重新编译 278 | 279 | 280 | ## 友元 281 | 需要定义一些函数,这些函数不是类的一部分,但又需要频繁地访问类的数据成员,这时可以将这些函数定义为该函数的友元函数,除此还有友类。 282 | 283 | 作用:避免了类成员函数的频繁调用,可以节约处理器开销,提高程序的效率 284 | 缺点:破坏了类的封装性和隐藏性 285 | 286 | 287 | ## 模板是什么,底层如何实现 288 | 289 | - 编译器从函数模板通过具体类型产生不同的函数;编译器会对函数模板进行两次编译: 290 | - 在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译 291 | 292 | 293 | 294 | ## 构造函数和析构函数可以调用虚函数吗 295 | 296 | **提倡不在构造函数和析构函数中调用虚函数** 297 | 1. 如果有继承,构造函数会先调用父类构造函数,而如果构造函数中有虚函数,此时子类还没有构造,所以此时的对象还是父类的,不会触发多态 298 | 2. 析构函数也是一样,子类先进行析构,这时,如果有virtual函数的话,子类的内容已经被析构了,C++会视其父类,执行父类的virtual函数 299 | 300 | 301 | 302 | 303 | ## 类什么时候析构 304 | 305 | 1. 对象生命周期结束,被销毁时 306 | 2. delete指向对象的指针时,删除指针类对象 307 | 3. 包含关系:对象Dog是对象Person的成员,Person的析构函数被调用时,对象Dog的析构函数也被调用 308 | 4. 继承关系:当Person是Student的父类,调用Student的析构函数,会调用Person的析构函数 309 | 310 | 311 | 312 | ## 构造函数的关键字有哪些 313 | 314 | - default关键字可以显式要求编译器生成合成构造函数,防止在调用时相关构造函数类型没有定义而报错 315 | - delete关键字可以删除构造函数、赋值运算符函数等 316 | - 0:将虚函数定义为纯虚函数(纯虚函数无需定义,= 0只能出现在类内部虚函数的声明语句处 317 | 318 | 319 | 320 | ## 什么时候生成类默认构造函数 321 | 322 | 1. 如果一个类没有任何构造函数,但它含有一个成员对象,而后者有默认构造函数,那么编译器就为该类合成出一个默认构造函数 323 | 2. 如果一个没有任何构造函数的派生类派生自一个带有默认构造函数基类,那么该派生类会合成一个构造函数调用上一层基类的默认构造函数 324 | 3. 带有一个虚函数的类 325 | 326 | 327 | 328 | ## 抽象类为什么不能创建对象 329 | 330 | - 引入原因:方便使用多态特性 331 | - 定义:带有纯虚函数的类为抽象类 332 | - 作用:抽象类的主要作用是将有关的操作作为接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根 333 | - 抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出 334 | - 如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类 335 | 336 | ## 类继承 337 | ### 模板与继承区别 338 | 1. 模板可以生成一组类或者函数,这些类或函数的实现都是一样;继承是事物之间的一种关系,从父类到子类实际上就是从普遍到特殊、从共性到特性 339 | 2. 模板和继承都是多态性的体现,继承是运行时的多态性,模板是编译时的多态性 340 | 3. 继承是数据的复制、模版是代码的复制; 341 | 342 | 343 | ### 哪些不能被继承 344 | 1. 构造函数:派生类使用成员列表初始化列表语法调用基类构造函数,创建基类部分;若无则调用基类默认构造函数 345 | 2. 析构函数:OS调用派生类析构函数,调用基类析构函数。若基类默认析构函数,编译器为派生类生成默认析构函数 346 | 3. 父类的赋值运算符被派生类覆盖 347 | 348 | 349 | 350 | ## 静态函数为什么不能定义为虚函数 351 | 352 | 1. static成员不属于任何类对象或类实例,所以即使给此函数加上virutal也是没有任何意义的 353 | 2. 静态与非静态成员函数之间有一个主要的区别,那就是静态成员函数没有this指针。而虚函数指针需要this指针去访问 354 | 355 | 356 | 357 | ## 移动构造函数 358 | 359 | 1. 移动构造函数,专门处理这种,用a初始化b后,就将a析构的情况 360 | 2. 移动构造函数中,对于指针,我们采用浅层复制 361 | 3. 移动实现的是对象值真实的转移(源对象到目的对象):源对象将丢失其内容,其内容将被目的对象占有 362 | 363 | 364 | 365 | ## 构造函数执行顺序 366 | 367 | 1. 在派生类构造函数中,所有的虚基类及上一层基类的构造函数调用; 368 | 2. 对象的vptr被初始化 369 | 3. 如果有成员初始化列表,将在构造函数体内扩展开来,这必须在vptr被设定之后才做 370 | 4. 执行程序员所提供的代码 371 | 372 | 373 | 374 | ## 哪些不能设为虚函数 375 | 376 | 1. 构造函数:不能。对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的,如果构造函数为虚函数,会先在对象中的虚函数查找对应虚函数,而此时虚函数表没有生成(动态多态是运行时绑定) 377 | 2. 静态函数:没有this指针,无法通过对象指针找到其虚函数表 378 | 3. 友元函数,友元函数不属于类的成员函数,不能被继承 379 | 5. 普通函数,普通函数不属于类的成员函数,不具有继承特性,因此普通函数没有虚函数 380 | 381 | 382 | 383 | ## 虚函数与纯虚函数区别 384 | 385 | 1. 。虚函数需要在基类中加上virtual修饰符修饰,因为virtual会被隐式继承,所以子类中相同函数都是虚函数。当一个成员函数被声明为虚函数之后,其派生类中同名函数自动成为虚函数,在派生类中重新定义此函数时要求函数名、返回值类型、参数个数和类型全部与基类函数相同 386 | 2. 纯虚函数只是相当于一个接口名,但含有纯虚函数的类不能够实例化 387 | 3. 纯虚函数首先是虚函数,其次它没有函数体,取而代之的是用“=0”。 388 | 4. 具有函数体的虚函数则是函数的具体地址 389 | 390 | 391 | 392 | 393 | ## struct与Union区别 394 | 395 | 1.在存储多个成员信息时,编译器会自动给struct多个成员分配存储空间,struct 可以存储多个成员信息,而Union每个成员会用同一个存储空间,分配给union的内存size 由类型最大的元素 size 来确定。 396 | 397 | 2.都是由多个不同的数据类型成员组成,但在任何同一时刻,Union只存放了一个被先选中的成员,而结构体的所有成员都存在。 398 | 399 | 3.对于Union的不同成员赋值,将会对其他成员重写,原来成员的值就不存在了,而对于struct 的不同成员赋值 是互不影响的。 400 | 401 | 注:在很多地方需要对结构体的成员变量进行修改。只是部分成员变量,那么就不能用联合体Union,因为Union的所有成员变量占一个内存。eg:在链表中对个别数值域进行赋值就必须用struct. 402 | 403 | ## 对象访问普通函数快还是虚函数更快? 404 | 405 | - 如果是普通对象,是一样快的 406 | - 如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找 407 | 408 | 409 | ## 重载、重写与隐藏区别 410 | 411 | - 重载:重载是指在同一范围定义中的同名成员函数才存在重载关系。主要特点是函数名相同,参数类型和数目有所不同 412 | 413 | - 重写指的是在派生类中覆盖基类中的同名函数,**重写就是重写函数体**,**要求基类函数必须是虚函数** 414 | 415 | - 与基类的虚函数有相同的参数个数 416 | 417 | 与基类的虚函数有相同的参数类型 418 | 419 | 与基类的虚函数有相同的返回值类型 420 | 421 | - 隐藏指的是某些情况下,派生类中的函数**屏蔽**了基类中的同名函数 422 | 423 | -------------------------------------------------------------------------------- /Distributed_System/CAP.md: -------------------------------------------------------------------------------- 1 | 一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容忍性(Partition Tolerance)这三项中的两项 2 | 3 | - 一致性 4 | - 更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致,等同于所有节点拥有数据的最新版本 5 | 6 | - 可用性 7 | - 服务一直可用,而且是正常响应时间 8 | 9 | - 分区容忍性 10 | - 当部分节点出现消息丢失或者分区故障的时候,分布式系统仍然能够继续运行 -------------------------------------------------------------------------------- /Distributed_System/Distributed_Lock.md: -------------------------------------------------------------------------------- 1 | # 分布式锁 2 | 3 | 4 | 分布式模型下,数据只有一份(或有限制),此时需要利用锁的技术控制某一时刻修改数据的进程数。与单机模式下的锁不同,分布式锁不仅需要保证进程可见,还需要考虑进程与锁之间的网络问题。 5 | 6 | 单机锁由于其能够共享堆内存,可以将内存作为标记存储位置。而分布式锁需要将标记存储在进程都能看见的地方,比如公共内存。 7 | 8 | ## 分布式锁的条件 9 | 10 | - 互斥性:在任意时刻,对于同一个锁,只有一个客户端能持有 11 | - 具备可重入特性 12 | - 锁失效机制,防止死锁 13 | - 非阻塞锁特性,即没有获取到锁将直接返回获取锁失败 14 | 15 | ## 基于 redis 的 setnx()、expire() 方法做分布式锁 16 | 17 | 首先解释函数。 18 | 19 | setnx(key,value),原子操作。如果key不存在,则设置其key,返回1;否则设置失败,返回0。 20 | 21 | expire():设置key的过期时间 22 | 23 | 具体过程: 24 | 25 | 1. setnx(lockkey, 1) 如果返回 0,则说明占位失败;如果返回 1,则说明占位成功 26 | 27 | 2. expire() 命令对 lockkey 设置超时时间,为的是避免死锁问题。 28 | 3. 执行完业务代码后,可以通过 delete 命令删除 key 29 | 30 | 可能存在的问题: 31 | 32 | 成功执行setnx()后,在expire()执行成功前,发生宕机,此时就还是死锁问题。 33 | 34 | 如何解决:setnx()、get() 和 getset() 方法来实现分布式锁 35 | 36 | 简单介绍函数 37 | 38 | setset(key,value):原子操作。对 key 设置 newValue 这个值,并且返回 key 原来的旧值。如果key之前不存在,返回的null。 39 | 40 | 过程; 41 | 42 | 1. setnx(lockkey, 当前时间+过期超时时间),如果返回 1,则获取锁成功;如果返回 0 则没有获取到锁,转向 2 43 | 2. get(lockkey) 获取值 oldExpireTime ,并将这个 value 值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向 3 44 | 3. 计算 newExpireTime = 当前时间+过期超时时间,然后 getset(lockkey, newExpireTime) 会返回当前 lockkey 的值currentExpireTime 45 | 4. 判断 currentExpireTime 与 oldExpireTime 是否相等,如果相等,说明当前 getset 设置成功,获取到了锁。如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试 46 | 5. 获取到锁之后,当前线程可以开始自己的业务处理,当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行 delete 释放锁;如果大于锁设置的超时时间,则不需要再锁进行处理 47 | 48 | 49 | 50 | ## ETCD分布式锁 51 | 52 | 1. Prefix:目录机制,etcd支持前缀查找。具体形式:/锁名称/key的UUID。查询前缀"/锁名称",返回 Key-Value 列表,同时也包含它们的 Revision,通过 Revision 大小,客户端可以判断自己是否获得锁,如果抢锁失败,则等待锁释放(对应的 Key 被删除或者租约过期),然后再判断自己是否可以获得锁 53 | 2. lease:在创建锁的时候绑定租约,并定期进行续约。如果租约到期,锁将被删除;如果获得锁期间持有者因故障不能主动释放锁,则持有的锁也会到期被自动删除,避免了死锁的产生 54 | 3. Revision:etcd内部维护了一个全局的Revision值,并会随着事务的递增而递增。可以用Revision值的大小来决定获取锁的先后顺序,实现公平锁。 55 | 4. Watch 机制支持监听某个固定的 Key,也支持监听一个范围(前缀机制),当被监听的 Key 或范围发生变化,客户端将收到通知。如果抢锁失败,可通过 Prefix 机制返回的 Key-Value 列表获得 Revision 比自己小且相差最小的 Key(称为 Pre-Key),对 Pre-Key 进行监听,因为只有它释放锁,自己才能获得锁,如果监听到 Pre-Key 的 DELETE 事件,则说明 Pre-Key 已经释放,自己已经持有 -------------------------------------------------------------------------------- /Distributed_System/Distributed_Transaction.md: -------------------------------------------------------------------------------- 1 | # 分布式事务是分布式系统中实现事务,多个本地事务组成 2 | 3 | 由数据库的事务问题,引申至分布式事务。 4 | 5 | ## 2PC 6 | 7 | 8 | 9 | 2PC 引入一个事务协调者的角色来协调管理各参与者(也可称之为各本地资源)的提交和回滚,二阶段分别指的是准备(投票)和提交两个阶段 10 | 11 | 12 | 1. 准备阶段协调者会给各参与者发送准备命令,除了提交事务之外啥事都做完了。 13 | 1. 若所有参与者都返回准备成功,则发送提交事务命令 14 | 2. 若第一阶段有一个参与者返回失败,则协调者向所有参与者发送回滚事务请求,分布式事务执行失败 15 | 2. 同步等待所有资源的响应之后就进入第二阶段即提交阶段(注意提交阶段不一定是提交事务,也可能是回滚事务 16 | 1. 第二阶段执行的是回滚事务操作,那么答案是不断重试,直到所有参与者都回滚了 17 | 2. 第二阶段执行的是提交事务操作,那么答案也是不断重试,因为有可能一些参与者的事务已经提交成功了,这个时候只有一条路,就是头铁往前冲,不断的重试,直到提交成功 18 | 19 | 20 | 实质是同步阻塞协议 21 | 22 | 23 | ## 3PC 24 | 25 | 相比于 2PC 它在参与者中也引入了超时机制,并且新增了一个阶段使得参与者可以利用这一个阶段统一各自的状态 26 | 27 | 分为三阶段:**准备阶段、预提交阶段和提交阶段**, 28 | 29 | 1. 准备阶段:先去询问此时的参与者是否有条件接这个事务,**因此不会一来就干活**直接锁资源,使得在某些资源不可用的情况下所有参与者都阻塞着 30 | 2. 预提交阶段: 与2PC 的准备阶段一样,除了事务的提交该做的都做了。起到了一个统一状态的作用 31 | 3. 提交阶段 32 | 33 | **超时机制**: 34 | 35 | 2PC 是同步阻塞的,我们已经分析了协调者挂在了提交请求还未发出去的时候是最伤的,所有参与者都已经锁定资源并且阻塞等待着 36 | 37 | 如果是等待提交命令超时,那么参与者就会提交事务了,因为都到了这一阶段了大概率是提交的,如果是等待预提交命令超时,那该干啥就干啥了,反正本来啥也没干 38 | 39 | 超时机制也会带来数据不一致的问题,比如在等待提交命令时候超时了,参与者默认执行的是提交事务操作,但是有可能执行的是回滚操作,这样一来数据就不一致了 40 | 41 | 42 | ## TCC 43 | 44 | 业务层面的分布式事务,可以跨数据库、跨不同的业务系统来实现事务 45 | 46 | TCC 指的是Try - Confirm - Cancel。 47 | 48 | - Try 指的是预留,即资源的预留和锁定,注意是预留。 49 | - Confirm 指的是确认操作,这一步其实就是真正的执行了。 50 | - Cancel 指的是撤销操作,可以理解为把预留阶段的动作撤销了。 51 | 52 | 53 | 54 | ### 本地消息表 55 | 56 | 利用了**各系统本地的事务**来实现分布式事务 57 | 58 | 有一张存放本地消息的表,一般都是放在数据库中,然后在执行业务的时候 将业务的执行和将消息放入消息表中的操作放在同一个事务中,这样就能保证消息放入本地表中业务肯定是执行成功的 59 | 60 | 紧接去调用下一个操作,如果下一个操作调用成功了好说,消息表的消息状态可以直接改成已成功。 61 | 62 | 如果调用失败也没事,会有后台任务定时去读取本地消息表,筛选出还未成功的消息再调用对应的服务,服务更新成功了再变更消息的状态 63 | 64 | 本地消息表其实实现的是**最终一致性**,**容忍了数据暂时不一致**的情况 65 | 66 | ### 分布式事务开源组件 67 | 68 | Seata:一个分布式事务拆分成一个包含了若干分支事务的全局事务。分支事务本身就是一个满足 ACID 的 本地事务,全局事务的职责是协调其下管辖的分支事务达成一致,要么一起成功提交,要么一起失败回滚 -------------------------------------------------------------------------------- /Distributed_System/ETCD.md: -------------------------------------------------------------------------------- 1 | # ETCD 2 | 3 | 首先介绍Raft算法 4 | 5 | ## Raft(分布式一致性算法) 6 | 7 | Raft 把集群中节点分为三种状态:Leader、 Follower、Candidate 8 | 9 | - Leader(领导者):负责日志的同步管理,处理来自客户端的请求,与Follower保持heartBeat的联系 10 | - Follower(追随者):响应 Leader 的日志同步请求,响应Candidate的邀票请求,以及把客户端请求到Follower的事务转发(重定向)给Leader 11 | - Candidate(候选者):负责选举投票,集群刚启动或者Leader宕机时,状态为Follower的节点将转为Candidate并发起选举,选举胜出(获得超过半数节点的投票)后,从Candidate转为Leader状态 12 | 13 | 为了解决数据一致性问题,将其划分为3个子问题: 14 | 15 | 1. 选举:当 Leader 宕机或者集群初创时,一个新的 Leader 需要被选举出来 16 | 2. 日志复制:Leader 接收来自客户端的请求并将其以日志条目的形式复制到集群中的其它节点,并且强制要求其它节点的日志和自己保持一致 17 | 3. 安全性:有任何的服务器节点已经应用了一个确定的日志条目到它的状态机中,那么其它服务器节点不能在同一个日志索引位置应用一个不同的指令 18 | 19 | ### 选举 20 | 21 | - 集群最开始都是Follower,任期为0.此时启动每个节点的选举计时器,并且每个计时器的超时时间也不同 22 | - 如果在节点选举计时器周期内没有收到心跳和投票请求,该节点转为Candidate,任期自增,向集群中所有节点发送投票请求并且重置自己选举定时器 23 | - 投票 24 | - 请求节点的 Term 大于自己的 Term,且自己尚未投票给其它节点,则接受请求,把票投给它 25 | - 请求节点的 Term 小于自己的 Term,且自己尚未投票,则拒绝请求,将票投给自己 26 | 27 | - 如果一个Candidate收到超过半数节点票,升级为Leader,定时发送心跳给其他节点,其他节点转为Follower并与Leader保持同步 28 | 29 | ### 日志复制 30 | 31 | - Leader收到客户端请求,将其作为日志条目Entry记录入本地日志,该条目是未提交状态 32 | - 随着心跳,Leader将Entry并发发送给其他Follower,让它们复制这条日志条目 33 | - 发送追加日志条目的时候,Leader 会把新的日志条目紧接着之前条目的索引位置, Leader 任期号也包含在其中;如果Follower 在它的日志中找不到包含相同索引位置和任期号的条目,那么它就会拒绝接收新的日志条目 34 | - 如何解决Leader与Follower不一致? 35 | - leader 必须找到最后两者达成一致的地方,然后删除从那个点之后的所有日志条目,发送自己的日志给 Follower。所有的这些操作都在进行附加日志的一致性检查时完成 36 | 37 | - Followers 接收到 Leader 发来的复制请求后,回应Leader。此时Entry仍是未提交 38 | - 写入本地日志中,返回 Success 39 | - 一致性检查失败,拒绝写入,返回 False 40 | 41 | - Leader收到大多数Follower回应,Entry标记为提交,把这条日志条目应用到它的状态机中 42 | - Leader向客户端回应OK 43 | - Leader回应客户端后,将随着下一个心跳通知 Followers,Followers 收到通知后也会将 Entry 标记为提交状态 44 | 45 | ### 安全性 46 | - 选举限制 47 | - Candidate 的日志至少和大多数的服务器节点一样新,那么它一定持有了所有已经提交的日志条目。投票请求的限制中请求中包含了 Candidate 的日志信息,然后投票人会拒绝那些日志没有自己新的投票请求 48 | 49 | - 提交之前任期内的日志条目 50 | - Leader 当前任期里的日志条目通过计算副本数目可以被提交;一旦当前任期的日志条目以这种方式被提交,那么由于日志匹配特性,之前的日志条目也都会被间接的提交 51 | - Leader 复制之前任期里的日志时,Raft 会为所有日志保留原始的任期号 52 | 53 | ### 脑裂问题 54 | 55 | ![脑裂问题](Raft脑裂.png) 56 | 57 | 当raft在集群中遇见网络分区的时候,集群就会因此而相隔开,在不同的网络分区里会因为无法接收到原来的leader发出的心跳而超时选主,这样就会造成多leader现象 58 | 59 | 当网络恢复的时候,集群不再是双分区,raft会有如下操作: 60 | 61 | ①: leaderD发现自己的Term小于LeaderA,会自动下台(step down)成为follower,leaderA保持不变依旧是集群中的主leader角色 62 | 63 | ②: 分区中的所有节点会回滚roll back自己的数据日志,并匹配新leader的log日志,然后实现同步提交更新自身的值。通知旧leaderA也会主动匹配主leader节点的最新值,并加入到follower中 64 | 65 | ③: 最终集群达到整体一致,集群存在唯一leader(节点A) 66 | 67 | 68 | 69 | ## ETCD 70 | 71 | 一个高可用、强一致的分布式键值(Key-Value)数据库,主要用途是共享配置和服务发现。其内部采用 Raft 算法作为分布式一致性协议 72 | 73 | ![ETCD框架图](ETCD.png) 74 | 75 | - HTTP Server:用于处理客户端发送的 API 请求以及其它 Etcd 节点的同步与心跳信息请求 76 | - Store:用于处理 Etcd 支持的各类功能的事务,包括数据索引、节点状态变更、监控与反馈、事件处理与执行等等 77 | - Raft:Raft 强一致性算法的具体实现,是 Etcd 的核心 78 | - WAL:Write Ahead Log(预写式日志),是 Etcd 的数据存储方式。除了在内存中存有所有数据的状态以及节点的索引,Etcd 还通过 WAL 进行持久化存储 79 | 80 | 81 | 一个用户的请求发送过来,会经由 HTTP Server 转发给 Store 进行具体的事务处理;如果涉及到节点的修改,则交给 Raft 模块进行状态的变更、日志的记录,然后再同步给别的 Etcd 节点以确认数据提交;最后进行数据的提交,再次同步 82 | 83 | ### ETCD应用场景 84 | 85 | 1. 服务发现 86 | 1. 存在一个高可靠、高可用的中心配置节点 87 | 2. 用户可以在 Etcd 中注册服务,并且对注册的服务配置租约,定时续约以达到维持服务的目的 88 | 3. 服务提供方在 Etcd 指定的目录(前缀机制支持)下注册服务,服务调用方在对应的目录下查询服务。通过 Watch 机制,服务调用方还可以监测服务的变化 89 | 90 | 2. 消息发布与订阅 91 | 1. 应用在启动时,主动从 Etcd 获取一次配置信息,同时,在 Etcd 节点上注册一个 Watcher 并等待,以后每次配置有更新,Etcd 都会实时通知订阅者,以此达到获取最新配置信息的目的 92 | 93 | 3. 分布式锁 94 | 95 | 96 | ### 通信方式 97 | 采用gRPC作为底层通信协议,而gRPC实现多路复用,一对多 98 | 99 | protobuf作为消息传递数据格式 100 | 101 | 问题1:为什么不用json? 102 | 103 | 回答: 104 | 1. 多余内存开销。对于int类型,内存只占2字节,转为json需要5字节 105 | 2. 重复传输字段,同样的key,work,只是因为值不同,就要传输两次work这个key值 106 | 107 | 问题2:为什么采用gRPC 108 | 109 | 回答: 110 | 1. 性能: protobuf序列化字段,负载小 111 | 2. 协议:转为HTTP/2 设计,比普通的HTTP紧凑高效,单个TCP可复用多个HTTP/2 调用 112 | 3. 代码生成:.proto文件自动生成,并且端到端生成消息和客户端代码 113 | 4. 严格规范:避免多平台的情况下出现分歧,各个平台实现一致。 114 | 5. 流式处理:支持一元,服务到客户端,客户到服务端,双向流式传输 115 | 6. 超时处理支持:支持rpc内部的timeout,并且可以取消timeout的服务 116 | 117 | 118 | ## ETCD查询步骤 119 | 1. 寻找指定的key值 120 | 2. 获取全部generation版本号 121 | 3. 根据查询的generation从存储中找到具体的Value值 122 | 4. 输出Value 123 | 124 | 125 | etcd将数据存放在一个持久化的B+树 126 | etcd会维护一个字段序的B树索引,是为了加速针对key的范围扫描 127 | 每个B树索引项中,都存储了一个key值,这也是为了快速定位指定的key或者进行范围扫描 128 | etcd的每个key有多个版本,在每个revision的tree里,有多个对应的keys -------------------------------------------------------------------------------- /Distributed_System/ETCD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/Distributed_System/ETCD.png -------------------------------------------------------------------------------- /Distributed_System/Raft脑裂.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/Distributed_System/Raft脑裂.png -------------------------------------------------------------------------------- /Distributed_System/log.md: -------------------------------------------------------------------------------- 1 | # 分布式下如何实现统一日志系统 2 | 3 | ## 传统日志查看 4 | 5 | Linux查看日志的命令: 6 | 7 | - tail 和 head 命令 8 | - tail是我最常用的一种查看方式,典型的应用是查看日志尾部最后几行的日志 9 | - head是查看日志文件的前几行日志 10 | - more 命令可以按照分页的方式现实日志内容,并且可以进行快速地翻页操作 11 | 12 | 存在问题: 13 | 14 | 1. 基于文件的,搜索效率比较低,并且很难对日志文件之间的关系进行聚合,无法从全局上梳理日志,也很难进行调用链路的分析 15 | 2. 有上千个节点的时候,日志是分布在许多机器上的,如果要获取这些日志的文件,不可能一台一台地去查看 16 | 3. 日志文件分布在不同的服务器上,因此进行相关的分析和聚合非常困难,也不利于展开整体的数据分析工作 17 | 18 | 19 | ## ELK同一日治系统 -------------------------------------------------------------------------------- /GCC/README.md: -------------------------------------------------------------------------------- 1 | # GCC相关 2 | 3 | ## gcc常用参数 4 | 1. -v/--version:查看gcc的版本 5 | 2. -I:编译的时候指定头文件路径,不然头文件找不到 6 | 3. -c:将汇编文件转换成二进制文件,得到.o文件 7 | 4. -g:gdb调试的时候需要加 8 | 5. -D:编译的时候指定一个宏(调试代码的时候需要使用例如printf函数,但是这种函数太多了对程序性能有影响,因此如果没有宏,则#ifdefine的内容不起作用) 9 | 6. -wall:添加警告信息 10 | 7. -On:-O是优化代码,n是优化级别:1,2,3 11 | 12 | ## 静态库制作 13 | - 原材料:源代码(.c或.cpp文件) 14 | - 将.c文件生成.o文件(ex:g++ a.c -c) 15 | - 将.o文件打包 16 | - ar rcs 静态库名称 原材料(ex: ar rcs libtest.a a.0) 17 | 18 | 19 | ## 动态库制作 20 | 21 | - 生成位置无关的目标文件.o,此外加编译器选项-fpic 22 | - `g++ -fPIC -c unite_time.cpp` 23 | - 生成动态库,加链接器选项-shared 24 | - g++ -shared -o libunite_time.so unite_time.o 25 | 26 | -------------------------------------------------------------------------------- /Large-Scale-Data/README.md: -------------------------------------------------------------------------------- 1 | # 海量数据处理 2 | 3 | 什么是海量? 4 | - 超大量的数据,分为两种情况 5 | - 空间:无法一次性装入内存 6 | - 时间:较短时间内无法迅速解决 7 | 8 | 9 | 解决方法: 10 | 1. 分而治之:hash映射 + hash统计 + 排序(堆/快速/归并排序) 11 | 2. 多层划分 12 | 3. Bitmap 13 | 14 | 15 | 分别介绍方法及对应的问题: 16 | 17 | ## 分治法 18 | 19 | **问题关键词:求最多、前K、文件很大、内存受限** 20 | 21 | ### 通解 22 | 1. Hash映射:利用哈希函数均匀分布的特点,对数据进行哈希计算,然后对数据进行 %num 取余,这样数据就被映射到了 num 个区间(桶)内(取模映射)。 即:把大文件化成(取模映射)小文件,大而化小,各个击破,分而治之,逐个解决。 23 | 2. Hash_map进行统计:转换成 num 个小文件(桶)后,可以采用常规的哈希表 hash_map( id, value ) 来统计每个桶中的最大次数;其中每个桶的内存大小是: 所给内存 / num ; 24 | 3. 快速排序/堆排序/归并排序:统计完成后用相应的排序算法就可以得到你所需要的数据,然后就可能代码题让你写快排。 25 | 26 | 例:一个文本文件,大约有一万行,每行一个词,要求统计出其中最频繁出现的前10个词,请给出思想,给出时间复杂度分析 27 | 28 | 答: 29 | - 如果文件比较大,无法一次性读入内存,可以采用hash取模的方法,将大文件分解为多个小文件,对于单个小文件利用hash_map统计出每个小文件中10个最常出现的词,然后再进行归并处理,找出最终的10个最常出现的词 30 | - 通过hash取模将大文件分解为多个小文件后,除了可以用hash_map统计出每个小文件中10个最常出现的词,也可以用trie树统计每个词出现的次数,时间复杂度是`O(n*le)`(le表示单词的平准长度),最终同样找出出现最频繁的前10个词(可用堆来实现),时间复杂度是`O(n*lg10)` 31 | 32 | 33 | ## 多层划分 34 | 35 | **问题关键词:第N大的数、中位数、不重复或重复的数字** 36 | 37 | 原理分析:通过hash取模将大文件分解为多个小文件后,除了可以用hash_map统计出每个小文件中10个最常出现的词,也可以用trie树统计每个词出现的次数,时间复杂度是O(n*le)(le表示单词的平准长度),最终同样找出出现最频繁的前10个词(可用堆来实现),时间复杂度是O(n*lg10) 38 | 39 | 例1:5亿个数据类型为 int 的数据,请求出他们的中位数。 40 | 41 | 答: 42 | 43 | 1. 讲 int 划分为 2^16 个区域(因为 int 的数据范围是 [ - 2^15~2^15-1 ] ),第一次扫描,我们遍历5亿个数据,统计落到每个区域里的个数; 44 | 2. 根据统计到的个数,就可以知道中位数落到了哪个区域,同时这个区域里的第几个数是中位数; 45 | 3. 第二次扫描,我们统计落在那个区域里的数据就可以啦; 46 | 实际上,如果不是int是int64,我们可以经过3次这样的划分即可降低到可以接受的程度。即可以先将int64分成2^24个区域,然后确定区域的第几大数,在将该区域分成2^20个子区域,然后确定是子区域的第几大数,然后子区域里的数的个数只有2^20,就可以直接利用direct addr table进行统计了 47 | 48 | ### Bitmap 49 | 50 | **问题关键词:快速判断海量数据是否存在所求数据** 51 | 52 | 例1:服务器收集了 40亿个不重复并且没有排过序的无符号的int整数, 要求使用的内存大小不超过2G;给出一个整数,问如果快速地判断这个整数是否在文件40亿个数据当中? 53 | 54 | 答: 55 | - 申请一个int数组长度为 int tmp[1+N/32];其中N代表要进行查找的总数 56 | - tmp中的每个元素在内存在占32位可以对应表示十进制数0~31,所以可得到BitMap表: 57 | - tmp[0]:可表示0~31 58 | - tmp[1]:可表示32~63 59 | - tmp[2]可表示64~95 60 | - 如何判断int数字放在哪一个tmp数组中:将数字直接除以32取整数部分(x/32),例如:整数8除以32取整等于0,那么8就在tmp[0]上; 61 | - 如何确定数字放在32个位中的哪个位:将数字mod32(x%32)。上例中我们如何确定8在tmp[0]中的32个位中的哪个位,这种情况直接mod上32就ok,又如整数8,在tmp[0]中的第8 mod上32等于8,那么整数8就在tmp[0]中的第八个bit位(从右边数起 62 | -------------------------------------------------------------------------------- /Linux/README.md: -------------------------------------------------------------------------------- 1 | ## Linux相关命令 2 | 3 | 1. 查看当前进程?怎么执行退出?怎么查看当前路径? 4 | 5 | 查看当前进程:ps 6 | 7 | 执行退出:exit 8 | 9 | 查看当前路径:pwd 10 | 11 | 2. ls命令执行什么功能? 可以带哪些参数,有什么区别? 12 | 13 | 列出指定目录中的目录,以及文件哪些参数以及区别:a所有文件l详细信息,包括大小字节数,可读可写可执行的权限等 14 | 15 | 3. 查看文件命令 16 | 17 | - vi/vim :编辑方式查看,可修改 18 | - cat:显示全部文件内容 19 | - more:分页显示文件内容 20 | - less:功能类似more,可以往前翻页 21 | - tail:仅查看尾部,还可以指定行数 22 | - head:仅查看头部,还可以指定行数 23 | - awk:打印符合条件的文本内容 24 | 25 | 4. 文档编辑 26 | - 创建目录和移除目录:mkdir rmdir 27 | - tar:打包 28 | - grep:在当前目录中中查找包含指定字符串的文件 29 | - ag:在目录中搜索相应关键字的文件 30 | 31 | 5. 文件管理 32 | - which:查找命令在哪个目录下 33 | - cp:复制文件或目录 34 | - chmod:控制文件权限 35 | - scp:不同linux主机之间复制文件 36 | - tree:列出目录内容 37 | - whereis:用于程序名的搜索,而且只搜索二进制文件 38 | - find:找到指定文件名的文件/目录 39 | 40 | 6. 磁盘管理 41 | - df:Linux系统上的文件系统的磁盘空间使用情况统计 42 | - mount:挂载Linux系统外的文件 43 | 44 | 7. 系统管理 45 | - groupadd:创建一个新的工作组,新工作组的信息将被添加到系统文件 46 | - ps:当前进程 (process) 的状态 47 | - 查看进程CPU占用率和内存占用率:ps -aux 48 | - 查看进程的父进程id:ps -ef 49 | - ifconfig:IP 地址配置 50 | - uname:显示电脑以及操作系统的相关信息。 51 | - who:当前用户 52 | - free:内存使用情况 53 | - top:显示进程的资源占用情况 54 | - %CPU、%MEM 、load average(任务队列的平均长度) 55 | - service:打印/启动/停止指定服务 56 | - chkconfig:检查,设置系统的各种服务 57 | - kill:杀掉指定进程 58 | - fuser:查询文件、目录、socket端口和文件系统的使用进程 59 | - vmstat:查看CPU性能指令 60 | - 查看所有CPU核信息:mpstat 61 | - 每个进程使用CPU的用量分解信息:pidstat 62 | 63 | 8. 网络 64 | - lsof:列出当前系统打开文件的信息 65 | - lsof -i:端口号 66 | - netstat -npl 67 | - ifstat:查看网络IO情况的指令 68 | - netstat: 查看机器已建立的TCP连接的指令 69 | - ping:测试网络连通性 70 | 71 | 72 | ## 功能实现题: 73 | 74 | 1. 如何利用linux的指令来查询一个文件的行数/字节数/字符数 75 | 76 | wc -l 77 | 78 | 2. linux下统计一个文件中每个id的出现次数 79 | 80 | grep -o "xxx" a | wc -l 81 | 82 | 3. 如何查看占用cpu最多进程 83 | 84 | ps aux|grep -v PID|sort -rn -k +3|head 85 | 86 | 87 | ### `sar` 88 | 89 | 从多方面对系统的活动进行报告,包括:文件的读写情况、系统调用的使用情况、磁盘 I/O、CPU 效率、内存使用状况、进程活动及 IPC 有关的活动等 90 | 91 | 92 | ### 如何查看主机 CPU 核数?如何查看内存还剩多少 93 | 94 | cat /proc/cpuinfo 95 | 96 | cat /proc/meminfo 97 | 98 | ## 查看进程上下文切换 99 | 100 | pidstat 101 | 102 | -------------------------------------------------------------------------------- /OS/img/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/OS/img/1.png -------------------------------------------------------------------------------- /OS/img/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/OS/img/2.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 面试八股文 2 | 3 | -------------------------------------------------------------------------------- /Structure/README.md: -------------------------------------------------------------------------------- 1 | # 架构介绍 2 | ## 单机架构 3 | 将所有功能都实现在一个进程里,部署在一台机器上 4 | - 优点 5 | - 简单 6 | 7 | - 缺点 8 | - C10M、C10B等问题 9 | - 运维需要停服 10 | 11 | ## 单体架构 12 | 进程部署在多个机器上,引入负载均衡. 13 | 将不同应用代码拆分,来到垂直架构 14 | ![](img/单体架构.png) 15 | 16 | - 优点 17 | - 水平扩容 18 | - 运维不需要停服 19 | 20 | - 缺点 21 | - 职责太多,开发效率低 22 | - 单个业务的上线、变更会影响其他不涉及的场景 23 | 24 | 25 | ## SOA、微服务 26 | 概念: 27 | 1. 服务:根据功能抽象出来 28 | 2. 通信标准:服务之间通信的基石 29 | 30 | 中心化 31 | 去中心化:微服务架构 -------------------------------------------------------------------------------- /Structure/img/单体架构.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/Structure/img/单体架构.png -------------------------------------------------------------------------------- /cache/README.md: -------------------------------------------------------------------------------- 1 | # 缓存 2 | 3 | 缓存,其目的就是能够处理大量需求。 4 | 5 | 解析:缓存的面试其实分成两大块: 6 | - 缓存的基本理论 7 | - 缓存中间件的应用 8 | 9 | 这里我们讨论缓存的一些基本理论,缓存中间件 Redis 等,在对应的中间件章节里面看里面查看。 10 | 11 | 将数据缓存在Redis中,也就是存在了内存中。内存天然支持高并发访问。可以瞬间处理大量请求 12 | 13 | 缓存的基本理论,目前来说考察比较多的是: 14 | - 缓存和 DB 一致性的问题 15 | - 缓存模式 16 | - 缓存穿透、缓存击穿、缓存雪崩 17 | 18 | ### 缓存和 DB 一致性问题 19 | 20 | 21 | 22 | 缓存一致性的问题根源于两个原因: 23 | - 不同线程并发更新 DB 和数据库; 24 | - 即便是同一个线程,更新 DB 和更新缓存是两个操作,容易出现一个成功一个失败的情况; 25 | 26 | 缓存和 DB 一致性的问题可以说是无最优解的。无论选择哪个方案,总是会有一些缺点。 27 | 28 | 最常用的是三种必然会引起不一致的方案。 29 | 30 | 1. 先更新 DB,再更新缓存。不一致的情况: 31 | 1. A 更新 DB,DB中数据被更新为1 32 | 2. B 更新 DB,DB中数据被更新为2 33 | 3. B 更新缓存,缓存中数据被更新为2 34 | 4. A 更新缓存,缓存中数据被更新为1 35 | 5. 此时缓存中数据为1,而DB中数据为2。这种不一致会一直持续到缓存过期,或者缓存和DB再次被更新,并且被修改正确; 36 | ![](img/db_before_cache.png) 37 | 1. 先更新缓存,再更新 DB。不一致的情况; 38 | 1. A 更新缓存,缓存中数据被更新为1 39 | 2. B 更新缓存,缓存中数据被更新为2 40 | 3. B 更新 DB,DB中数据被更新为2 41 | 4. A 更新 DB,DB中数据被更新为1 42 | 5. 此时缓存中数据为2,但是DB 中数据为1。这种不一致会一直持续到缓存过期,或者缓存和DB再次被更新,并且被修改正确; 43 | ![](img/cache_before_db.png) 44 | 1. 先更新 DB,再删除缓存。不一致的情况; 45 | 1. A 从数据库中读取数据1 46 | 2. B 更新数据库为2 47 | 3. B 删除缓存 48 | 4. A 更新缓存为1 49 | 5. 此时缓存中数据为1,数据库中数据为2 50 | ![](img/db_remove_cache.png) 51 | 52 | 所以本质上,没有完美的解决方案,或者说仅仅考虑这种更新顺序,是不足以解决缓存一致性问题的。 53 | 54 | 与这三个类似的一个方案是利用 CDC 接口(**即变化数据捕获接口**),异步更新缓存。但是本质上,也是要忍受一段时间的不一致性。比如说典型的,应用只更新 MySQL,然后监听 MySQL 的 binlog,更新缓存。 55 | 56 | 而如果需求强一致性的话,那么比较好的方案就是: 57 | - 第一个是负载均衡算法结合singleflight 58 | - 第二个是分布式锁。 59 | 60 | 第一个方案。我们可以考虑对 key 采用哈希一致性算法来作为负载均衡算法,那么我们可以确保,同一个 key 的请求,永远会落到同一台实例上。然后结合单机 singleflight,那么可以确保永远只有一个线程更新缓存或者 DB,自然就不存在一致性问题了。 61 | 62 | 这个方案要注意的是在哈希一致性算法因为扩容,或者缩容,或者重新部署,导致 key 迁移到别的机器上的时候,会出现问题。假设请求1、2都操作同一个 key: 63 | - 请求1被路由到机器 C 上 64 | - 扩容,加入了 C1 节点 65 | - 请求2被路由到了 C1 节点上 66 | - (以先写DB为例)请求1更新DB 67 | - 请求2更新DB,请求2更新缓存 68 | - 请求1更新缓存 69 | 70 | 在这种情况下。那么可能的解决方案就是: 71 | - 要么在部署 C1 之前,在 C 上禁用缓存 72 | - 要么在部署 C1 之后,先不使用缓存,在等待一段时间之后,确保 C 上的迁移key的请求都被处理完了,C1 再启用缓存 73 | 74 | 75 | 这里,也就是只有三个选项: 76 | - 追求强一致性,选用分布式事务; 77 | - 追求最终一致性,可以引入重试机制; 78 | - 同步重试:在上次请求失败或超时,程序再次发起同步调用请求 79 | - 异步重试:通过异步系统(消息队列或调度中间件)对失败或超时请求再次发起调用 80 | - 如果可以使用本地事务,那么应该是:开启本地事务-更新DB-更新缓存-提交事务 81 | 82 | 83 | 84 | ### 缓存模式 85 | 86 | 缓存模式主要是所谓的 cache-aside, read-through, write-through, write-back, refresh ahead 以及更新缓存使用到的 singleflight 模式。 87 | - cache-aside:应用直接去缓存中找数据,命中缓存则直接返回,如果未命中缓存,则需要先去数据库中查询数据,并将查询到的数据存储到缓存中 88 | - read-through:通过应用程序来更新缓存中的数据,存在缓存中数据与数据库中数据不一致的情况;需要和write-through搭配解决 89 | - write-through:先将数据写入到缓存中,然后由缓存将数据存入到数据库中 90 | - write-back:标准的 write-back 是在缓存过期的时候,然后再将缓存刷新到 DB 里面。因此,它的**弊端**就是,在缓存刷新到 DB 之前,如果缓存宕机了,比如说 Redis 集群崩溃了,那么数据就永久丢失了;但是**好处**就在于,因为过期才把数据刷新到 DB 里面,因为读写都操作的是缓存。如果缓存是 Redis 这种集中式的,那么意味着大家读写的都是同一份数据,也就没有一致性的问题。但是,如果你设置了过期时间,那么缓存过期之后重新从数据库里面加载的同时,又有一个线程更新缓存,那么两者就会冲突,出现不一致的问题; 91 | - refresh ahead:在缓存过期之前,自动刷新(重新加载)最近访问过的条目。甚至可以通过预加载来减少延迟,但如果预测不准反而会导致性能下降 92 | 93 | ### 缓存塞满解决方法 94 | 95 | - LRU:将最近没有用到的数据剔除出去 96 | - LFU:根据使用频率,将最不常用的数据剔除出去 97 | 98 | 99 | ### 缓存异常场景 100 | 101 | 缓存穿透、击穿和雪崩,其实,这三个就是描述了三种场景: 102 | - 你数据库本来就没数据 103 | - 你数据库有,但是缓存里面没有 104 | - 你缓存本来有,但是突然一大批缓存集体过期了 105 | 106 | - 缓存穿透:数据库本来就没数据,所以请求来的时候,肯定是查询数据库的。但是因为数据库里面没有数据,所以不会刷新回去,也就是说,缓存里面会一直没有。因此,如果有一些黑客,一直发一些请求,这些请求都无法命中缓存,那么数据库就会崩溃。 107 | 108 | - 缓存击穿:如果数据库有,但是缓存里面没有。理论上来说,只要有人请求数据,就会刷新到缓存里面。问题就在于,如果突然来了一百万个请求,一百万个线程都尝试从数据库捞数据,然后刷新到缓存,那么数据库也会崩溃。 109 | 110 | - 缓存雪崩:缓存本来都有,但是过期了。一般情况下都不会有问题,但是如果突然之间几百万个 key 都过期了,那么接下来的请求也几乎全部命中数据库,也会导致数据库崩溃。 111 | 112 | ### 缓存解决方案 113 | - 缓存预热:系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题。用户会直接查询事先被预热的缓存数据 114 | - 缓存更新:定时去清理过期的缓存;有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存 115 | - 缓存降级:访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级 116 | ## 面试题 117 | 118 | ### 如何解决缓存和 DB 的一致性问题 119 | 120 | 缓存和 DB 的一致性问题,没有什么特别好的解决方案,主要就是一个取舍的问题。 121 | 122 | 如果能够忍受短时间的不一致,那么可以考虑只更新 DB,等缓存自然过期。大多数场景其实没有那么强的一致性需求,这样做就够了。 123 | 124 | 进一步也可以考虑先更新 DB 再更新缓存,或者先更新缓存再更新 DB,或者更新 DB 之后删除缓存,都会有不一致的可能,但是至少不会比只更新 DB 更差。 125 | 126 | 另外一种思路是利用 CDC 接口,比如说监听 MySQL 的binlog,然后更新缓存。应用是只更新 MySQL,丝毫不关心缓存更新的问题。(引导面试官问 CDC 问题,或者 MySQL binlog,或者说这种模式和别的思路比起来有什么优缺点) 127 | 128 | - CDC捕获到数据库的变更之后,会将变更事件发布到消息队列中供消费者消费,诸如Kafka。只需要订阅消息队列中内容,便实现功能。 129 | 130 | 131 | 如果追求强一致性,那么可行的方案有两个: 132 | - 利用分布式锁。在读上没必要加锁,在写的时候加锁。(在同一个时刻,有人更新数据,有人读数据,那么读的人,读到哪个数据都是可以的。如果写已经完成,那么读到的肯定是新数据,如果写没有完成,读到的肯定是老数据)。(刷亮点)一种可行的优化方案,是在单机上引入 singleflight。那么更新某个 key 的时候,同一个实例上的线程自己竞争一下,决出一个线程去参与抢全局分布式锁。在写频繁的时候,这种优化能够有些减轻分布式锁的压力。 133 | - 另外一个方案是利用负载均衡算法和 singleflight。可以选择一种负载均衡算法,即一个 key 只会被路由到同一个实例上。比如说使用一致性哈希算法。结合 singleflight,那么可以确保全局只有一个线程去更新数据,那么自然就不存在一致性的问题了。(在扩容缩容,或者重启的时候,会有问题。要是面试官水平不高,他是意识不到的) 134 | 135 | - 这种方案的问题在于,在扩容、缩容、或者重启的时候,因为会引起 key 迁移到别的实例上,所以可能出现不一致的问题。在这种情况下,可以考虑两种方案。第一种方案是扩容或者缩容的时候在原本实例上禁用这些迁移 key 的缓存;另外一种方案是目标实例先不开启读这些迁移 key 的缓存,等一小段时间,确保原本实例上的这些迁移 key 的请求都被处理完了,然后再开启缓存。 136 | 137 | #### 类似问题 138 | - 在使用了缓存的时候,你先更新缓存还是先更新 DB 139 | - 要是先更新缓存成功,再更新 DB 失败会怎样 140 | - 要是先更新DB成功,但是更新缓存失败会怎样 141 | - 更新 DB 之后,删除缓存的做法有什么弊端?怎么解决这种弊端? 142 | 143 | ### singleflight 是什么 144 | singleflight 是一种设计模式,使用这种设计模式,可以在我们更新缓存的时候,控制住只有一个线程去更新缓存。 145 | 146 | 不过,这里面强调的一个线程只是指同一个 key 只会有一个线程。因为我们并不会说更新不同的 key 也共享一个更新线程。 147 | 148 | (亮点,要解释 singleflight 只在单机层面上,而不是在全局上)另外一个是,在分布式环境下,我们只做单机层面上的控制。也就是说,如果有多台机器,我们会保证一个机器只有一个线程去更新特定一个 key 的缓存。比如说,针对 key1,如果有三台机器,那么最多会有三个线程去更新缓存。 149 | 150 | 不做全局的原因很简单,在分布式环境下,数据库至少要能撑住这种多台机器同时发起请求的负载。而做全局的 singleflight 本质上就是利用分布式锁,这个东西非常消耗性能。 151 | 152 | (如果是 Go 语言)在 Go 上,标准库直接提供了 singleflight 的支持。(这里要防面试官让你手写一个) 153 | 154 | #### 类似问题 155 | - 为什么用 singleflight 156 | - 为什么 singleflight 只在单机层面上应用 157 | - 如果要在全局层面上应用 singleflight,怎么搞?其实就是加一个分布式锁,没什么花头 158 | -------------------------------------------------------------------------------- /cache/img/cache_before_db.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/cache/img/cache_before_db.png -------------------------------------------------------------------------------- /cache/img/db_before_cache.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/cache/img/db_before_cache.png -------------------------------------------------------------------------------- /cache/img/db_remove_cache.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/cache/img/db_remove_cache.png -------------------------------------------------------------------------------- /data_structure/排序算法.md: -------------------------------------------------------------------------------- 1 | # 排序算法 2 | 3 | 4 | - 比较排序,时间复杂度`O(nlogn) ~ O(n^2)`,主要有:冒泡排序,选择排序,插入排序,归并排序,堆排序,快速排序等。 5 | - 非比较排序,时间复杂度可以达到O(n),主要有:计数排序,基数排序,桶排序等。 6 | 7 | 时间复杂度 8 | 9 | ![](排序算法时间复杂度.png) 10 | 11 | 所谓稳定排序:排序前后两个相等数的相对顺序不变 12 | 13 | ## 冒泡排序 14 | 15 | 从数组中第一个数开始,依次遍历数组中的每一个数,通过相邻比较交换,每一轮循环下来找出剩余未排序数的中的最大数并"冒泡"至数列的顶端 16 | 17 | - 最好的情况:如果待排序数据序列为正序,则一趟冒泡就可完成排序,排序码的比较次数为 n-1 次,且没有移动,时间复杂度为O(n) 18 | 19 | - 最坏的情况:如果待排序数据序列为逆序,则冒泡排序需要 n-1 次趟起泡,每趟进行n-i次排序码的比较和移动,即比较和移动次数均达到最大值 20 | 21 | 22 | ## 选择排序 23 | 24 | 从所有记录中选出最小的一个数据元素与第一个位置的记录交换;然后在剩下的记录当中再找最小的与第二个位置的记录交换,循环到只剩下最后一个数据元素为止。 25 | 26 | 27 | ## 插入排序 28 | 29 | ### 直接插入 30 | 31 | 原理: 从待排序的n个记录中的第二个记录开始,依次与前面的记录比较并寻找插入的位置,每次外循环结束后,将当前的数插入到合适的位置。 32 | 33 | 最好情况:当待排序记录已经有序 34 | 35 | 最坏情况:如果待排序记录为逆序 36 | 37 | ## 希尔排序 38 | 39 | Shell排序法是对相邻指定距离(称为增量)的元素进行比较,并不断把增量缩小至1,完成排序。 40 | 41 | 在直接插入排序的基础上,将直接插入排序中的1全部改变成增量d即可,因为Shell排序最后一轮的增量d就为1。 42 | 43 | ## 快速排序 44 | 45 | 本质是分治 46 | 47 | **算法原理** 48 | 49 | (1)从待排序的 n 个记录中任意选取一个记录(通常选取第一个记录)为分界值; 50 | (2)把所有小于分界值的记录移动到左边,把所有大于分界值的记录移动到右边,中间位置填分界值,称之为第一趟排序; 51 | (3)然后对前后两个子序列分别重复上述过程,直到所有记录都排好序。 52 | 53 | 最好的情况:是每趟排序结束后,每次划分使两个子文件的长度大致相等,时间复杂度为O(nlog2n) 54 | 55 | 最坏的情况:是待排序记录已经排好序,第一趟经过n-1次比较后第一个记录保持位置不变,并得到一个n-1个元素的子记录;第二趟经过n-2次比较,将第二个记录定位在原来的位置上,并得到一个包括n-2个记录的子文件 56 | 57 | ### 优化 58 | 59 | 1. 固定位置:取序列的第一个或最后一个元素作为基准,如果数组已经有序时,此时为最坏情况,时间复杂度O(n^2) 60 | 2. 取待排序列中任意一个元素作为基准 61 | 1. 在整个数组数字全相等时,仍然是最坏情况,时间复杂度是O(n^2)。实际上,随机化快速排序得到理论最坏情况的可能性仅为1/(2^n)。所以随机化快速排序可以对于绝大多数输入数据达到O(nlogn)的期望时间复杂度。 62 | 63 | 3. 三数取中:一组序列的中值(中位数)是枢纽元最好的选择(因为可以将序列均分为两个子序列,归并排序告诉我们,这时候是O(NlogN);但要计算一组数组的中位数就比较耗时,会减慢快排的效率。但可以通过计算数组的第一个,中间位置,最后一个元素的中值来代替。 64 | 65 | 66 | ## 堆排序 67 | 68 | 堆是完全二叉树 69 | 70 | 最坏情况:如果待排序数组是有序的,仍然需要 O(N * logN) 复杂度的比较操作,只是少了移动的操作 71 | 72 | 最好情况:如果待排序数组是逆序的,不仅需要O(N * logN)复杂度的比较操作,而且需要O(N * logN)复杂度的交换操作。总的时间复杂度还是O(N * logN)。 73 | 74 | 堆排序一般优于快速排序的重要一点是,数据的初始分布情况对堆排序的效率没有大的影响 75 | 76 | ## 归并排序 77 | 78 | 堆排序一般优于快速排序的重要一点是,数据的初始分布情况对堆排序的效率没有大的影响 79 | 80 | 最坏、最好和平均时间复杂度都是 O(nlgn) 81 | 82 | 83 | ## 计数排序 84 | 85 | 算法步骤: 86 | (1)找出待排序的数组中最大的元素 k,申请一个长度为 k + 1 的中间数组 C。 87 | (2)遍历待排序数列,统计每个值为 i 的元素出现的次数,存入数组 C 的第 i 项。 88 | (3)对所有的计数累加(从 C 中的第一个元素开始,每一项和前一项相加)。 89 | (4)反向填充目标数组:将每个元素i放在新数组的第 C(i) 项,每放一个元素就将 C(i) 减去 1。 90 | 91 | 92 | 要求: 待排序数中最大数值不能太大。 93 | 94 | ## 基数排序 95 | 96 | 透过键值的部分信息,将要排序的元素分配至某些“桶”中,藉以达到排序的作用。 97 | 98 | ## 桶排序 99 | 100 | 首先要假设待排序的元素输入符合某种均匀分布,例如数据均匀分布在[ 0,1)区间上,则可将此区间划分为10个小区间,称为桶,对散布到同一个桶中的元素再排序。 101 | 102 | 排序过程: 103 | (1)设置一个定量的数组当作空桶子; 104 | (2)寻访序列,并且把记录一个一个放到对应的桶子去; 105 | (3)对每个不是空的桶子进行排序。 106 | (4)从不是空的桶子里把项目再放回原来的序列中。 -------------------------------------------------------------------------------- /data_structure/排序算法时间复杂度.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/data_structure/排序算法时间复杂度.png -------------------------------------------------------------------------------- /database/RDMS.md: -------------------------------------------------------------------------------- 1 | # 关系型数据库 2 | 3 | ## 数据库三大范式 4 | 5 | 如果数据库表中的所有字段值都是不可分解的原子值,就说明该数据库表满足了第一范式 6 | 7 | 第二范式需要确保数据库表中的每一列都和主键相关,而不能只与主键的某一部分相关。也就是说在一个数据库表中,一个表中只能保存一种数据,不可以把多种数据保存在同一张数据库表中。 8 | 9 | 确保数据表中的每一列数据都和主键直接相关,而不能间接相关 10 | 11 | ## 主键、超键、候选键、外键是什么 12 | 13 | - **超键**:在关系中能唯一标识**元组的属性集**称为关系模式的超键 14 | - **候选键**:不含有**多余属性的超键**称为候选键。也就是在候选键中,若再删除属性,就不是键了 15 | - **主键**:**用户选作元组标识的一个候选键程序主键** 16 | - **外键**:如果关系模式**R中属性K是其它模式的主键**,那么**k在模式R中称为外键** 17 | - 主键与索引的区别 18 | - 主键一定是唯一性索引,唯一性索引并不一定是主键 19 | - 主键可以唯一的标识表中的某一行的属性或者属性组 20 | - 主键列不允许为空,但是索引可以为空 21 | ## RDMS与非关系型数据库区别 22 | - 关系型数据库 23 | - 采用了关系模型来组织数据 24 | - 保持数据的一致性 25 | - 数据更新的开销比较小 26 | - 支持复杂查询 27 | - 非关系型数据库 28 | - 不需要经过SQL层的解析,读写效率高 29 | - 基于键值对,数据的扩展性很好 30 | - 支持多种类型数据的存储 31 | - 适用场景: 32 | - 数据量大、高可用、日志系统等 33 | 34 | ## RDMS中SQL执行过程 35 | - 连接器 36 | - 查询缓存,此前是否执行过语句 37 | - SQL引擎 38 | - Paser:词法分析与语法分析生成语法树,并对语法树进行合法性检验 39 | - Optimizer:根据生成的语法树,按照规则或代价产生执行计划树 40 | - Executor:根据计划树执行,常用模型为**火山模型** 41 | - 存储引擎 42 | - Buffer Pool:缓存数据,减少I/O开销 43 | - Page:数据存储基本单位 44 | - B+ Tree:InnoDB常用索引结构 45 | - 事务引擎 46 | - ACID 47 | 48 | ## RDMS企业实践的问题 49 | ### 请求流量非常大 50 | - 问题 51 | - 单节点数据容量有限 52 | - 单节点写容易成为瓶颈 53 | 54 | - 解决方法:Sharding 55 | - 业务数据水平拆分:数据拆分并存储在多台服务器 56 | - 代理层分片路由:增加服务器数量 57 | 58 | ### 流量突增 59 | - 问题 60 | - 活动期间流量上涨 61 | - 集群性能不满足要求 62 | 63 | - 解决方法1:扩容 64 | - 扩容DB物理节点数量 65 | - 解决方法2:代理连接池 66 | - 业务侧预热连接池 67 | - 代理侧预热连接池 68 | - 代理侧支持连接队列 69 | 70 | ### 可靠性与稳定性 71 | - 问题 72 | - DB所在机器宕机 73 | 74 | - 解决方法:HA管理 75 | - ha服务监管,切换宕机节点 76 | - 代理支持配置热加载 77 | - 代理自动屏蔽宕机读节点 78 | 79 | 80 | ## MySQL主从复制原理 81 | 类似于Kafka的主从复制 82 | 83 | 主从复制是指一台服务器充当主数据库服务器,另一台或多台服务器充当从数据库服务器,主服务器中的数据自动复制到从服务器之中 84 | 85 | MySQL主从复制的基础是主服务器对数据库修改记录二进制日志,从服务器通过主服务器的二进制日志自动执行更新 86 | 87 | 主要有3个线程参与 88 | - 主节点log dump:发送和读取bin-log的内容。读取时会加锁,读完,在发送给从节点之前,释放锁 89 | - 从节点I/O线程:请求主库中更新的bin-log,获得更新后保存在本地relay-log 90 | - 从节点SQL线程:读取relay-log中的内容,解析成具体的操作并执行,最终保证主从数据的一致性 91 | 92 | 1. 从节点的I/O进程连接主节点,请求指定日志的指定位置后的内容 93 | 2. 主节点收到请求,通过负责复制的I/O进程(log dump现场)读取指定信息,返回给从节点。此外包含bin-log和big-log中下一个指定更新位置 94 | 3. 从节点I/O进程收到日志内容、文件及位置点,将内容更新到本地relay-log中,将bin-log和位置保存在master-info文件中,以便下一次告诉master从某个bin-log的哪个位置后内容 95 | 4. 从节点SQL现场检测到relay-log中新增内容,解析为主节点实际执行的MySQL,在本数据库中按照解析出的顺序执行 96 | 97 | 主从同步分为三种: 98 | 99 | - 异步模式:主库在执行完客户端提交的事务后会立即将结果返给给客户端,并不关心从库是否已经接收并处理 100 | - 半同步模式:主库在执行完客户端提交的事务后不是立刻返回给客户端,而是等待至少一个从库接收到并写到relay-log中才返回成功信息给客户端;一旦等待超过超时时间,切换异步模式 101 | - 全同步模式:当主库执行完一个事务,然后所有的从库都复制了该事务并成功执行完才返回成功信息给客户端 102 | 103 | 104 | ## innodb两阶段锁 105 | 106 | 发生在记录更新操作或是(select for update、lock in share model)时,对记录加锁 107 | 108 | 1. 扩张阶段:不断上锁,没有锁被释放 109 | 2. 收缩阶段:锁被陆续释放,没有心加锁 110 | 111 | 两段锁协议并不要求事务必须一次将所有要使用的数据全部加锁,因此遵守两段锁协议的事务可能发生死锁 -------------------------------------------------------------------------------- /database/img/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/database/img/index.png -------------------------------------------------------------------------------- /database/img/next_key_lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/database/img/next_key_lock.png -------------------------------------------------------------------------------- /database/img/rr-phantom-read-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/database/img/rr-phantom-read-example.png -------------------------------------------------------------------------------- /database/img/transaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/database/img/transaction.png -------------------------------------------------------------------------------- /database/index.md: -------------------------------------------------------------------------------- 1 | # 数据库索引 2 | 3 | 基本回答: 4 | 5 | 索引是为了加快数据查询的一种数据结构。 6 | 7 | 从**数据结构**角度出发,索引分为**B树索引**,**B+树索引**,**哈希索引**和**位图索引**。 8 | 在MySQL上,主要是采用B+树索引,B树索引在NoSQL上使用较多,哈希索引在KV数据库上较为常见。 9 | 10 | 从**形态**上来说,可以分成覆盖索引,前缀索引,全文索引,联合索引,唯一索引和主键索引。(下面的定义,可以直接一股脑说出来,也可以等面试官问) 11 | 1. 覆盖索引指索引的叶子结点已经包含了需要查询的数据,这样就没必要根据主键进行二次回表查询 12 | 2. 前缀索引是指只利用了数据前几个字符的索引,如果前面几个字符区分度不好的话,不建议使用前缀索引; 13 | 3. 全文索引现在比较少用,一般推荐使用别的中间件来完成,例如ES(小心这一步,可能咔嚓把话题引过去了ES上); 14 | 4. 联合索引是指多个列组成一个索引。创建的时候我们会考虑把区分度好的索引放在前面,因为MySQL遵循最左前缀匹配原则;(这里可能会问你啥是最左前缀匹配原则) 15 | 5. 唯一索引是指数据库里面要求该索引值必须要唯一,我们一般用于业务唯一性保证; 16 | 6. 主键索引是比较特殊的索引,一般它的叶子节点要么存储了数据,要么存储了指向数据的指针。MySQL的innodb引擎存储的是数据,MyISAM放的是数据的地址;(这里也会引过去聚簇索引与非聚簇索引) 17 | 18 | 从是否存储数据的角度,又可以分为聚簇索引和非聚簇索引,MySQL的主键就是聚簇索引,每张表唯一一个,非聚簇索引的数据本质上存储的是主键。(面试官可能从这里被引导过去聚簇索引与非聚簇索引) 19 | 20 | 而对于MySQL的innodb来说,它的行锁是利用索引来实现的,所以如果查询的时候没有索引,那么会导致表锁。(这一句可能引导面试官问你锁和事务的问题,如果不熟悉锁和事务,请不要回答)。 21 | 22 | ![知识点](./img/index.png) 23 | 24 | ## 扩展点 25 | 26 | ### 为什么使用索引 27 | 1. 通过过创建唯一性索引,可以保证数据库表中**每一行数据的唯一性** 28 | 2. 可以大大加快数据的检索速度 29 | 3. 帮助服务器避免排序和临时表 30 | 4. 将随机IO变为顺序IO 31 | 5. 可以加速表和表之间的连接 32 | 33 | ### 创建索引的原则 34 | 1. **最左匹配原则** 35 | 2. 较为频繁的作为查询条件去创建索引;更新比较频繁字段以及数据值很少的列 不适合创建索引 36 | 3. 在经常需要范围查询搜索的的列上建立索引;以及在需要排序的列上建立索引,因为索引已经排序;在使用 where 语句的列上建立索引 37 | 4. 尽量的扩展索引,不要新建索引 38 | 5. 定义有外键的数据列一定要建立索引 39 | 40 | ### 什么是最左匹配原则 41 | 42 | 43 | 答案:最左前缀匹配原则是指,MySQL会按照联合索引创建的顺序,从左至右开始匹配。例如创建了一个联合索引(A,B,C),那么本质上来说,是创建了A,(A,B),(A,B,C)三个索引。之所以如此,因为MySQL在使用索引的时候,类似于多重循环,一个列就是一个循环。在这种原则下,我们会优先考虑把区分度最好的放在最左边,而区分度可以简单使用不同值的数量除以总行数来计算(distinct(a, b, c)/count(*))。 44 | 45 | ### 数据库支持哈希索引吗 46 | 数据库项目中采用的哈希索引,介绍项目可引导至此 47 | 48 | 哈希索引是利用哈希表来实现的,适用于等值查询,如等于,不等于,IN等,对范围查询是不支持的。我们惯常用的innodb引擎是不支持用户自定义哈希索引的,但是innodb有一个优化会建立自适应哈希索引。 49 | 所谓的自适应哈希索引,是指innodb引擎,如果发现二级索引(除了主键以外的别的索引)被经常使用,那么innodb会给这个索引建立一个哈希索引,加快查询。所以从本质上来说,innodb的自适应哈希索引是一个对索引的哈希索引。 50 | 51 | 关键:等值查询,对索引的哈希索引 52 | 53 | #### 如何引导 54 | 1. 在前面回答了哈希索引之后,就直接跳过来这里,例如“哈希索引在KV数据库上比较常见,不过innodb引擎支持自适应哈希索引,它是..." 55 | 56 | ### 聚簇索引和非聚簇索引的区别 57 | 58 | 聚簇索引是指叶子节点存储了**数据的索引**。MySQL整张表可以看做是一个聚簇索引。因为非聚簇索引没有存储数据,所以一般是存储了主键。于是会导致一个**回表**的问题。即如果我们查询的列包含不在索引上的列,这会引起数据库先根据非聚簇索引找出主键,而后拿着主键去聚簇索引里边捞出来数据。而根据主键找数据会引起磁盘I/O,性能大幅度下降。这就是我们推荐使用覆盖索引的原因。 59 | 60 | 关键点:聚簇索引存了数据,非聚簇索引要回表 61 | 62 | #### 如何引导过来这里? 63 | 1. 聊到了覆盖索引与回表的问题,话术可以是”一般用覆盖索引,在不使用覆盖索引的时候,会引起回表查询,这是因为MySQL的非聚簇索引...“; 64 | 2. 聊到如何计算一次查询的开销。这个比较少见,因为一般的面试官也讲不清楚一次MySQL查询时间开销会在哪里; 65 | 3. 前面基本回答,回答了聚簇索引之后直接回答这部分 66 | 4. 聊到了B+树的叶子节点可以存放什么,或者聊到了索引的叶子节点可以存放什么 67 | 5. 是不是查询一定会引起回表?这其实是考察覆盖索引,所以在谈及了覆盖索引之后可以聊这个聚簇索引和非聚簇索引的点 68 | 69 | 70 | ### B树与B+树区别 71 | 72 | - B+树内节点不存储数据,所有 data 存储在叶节点导致查询时间复杂度固定为 log n。而B-树查询时间复杂度不固定,与 key 在树中的位置有关,最好为O(1) 73 | - B+树叶节点两两相连可大大增加区间访问性,可使用在范围查询等,而B-树每个节点 key 和 data 在一起,则无法区间查找 74 | - B+树更适合外部存储。由于内节点无 data 域,每个节点能索引的范围更大更精确 75 | 76 | 77 | ### MySQL为什么使用B+树索引 78 | 79 | MySQL使用B+树主要就是考虑三个角度: 80 | 1. 和二叉树,如平衡二叉树,红黑树比起来,B+树是多叉树,比如MySQL默认是1200叉树,同样数据量,高度要比二叉树低; 81 | 2. 和B树比起来,B+树的叶子节点被连接起来,形成了一个链表,这意味着,当我们执行范围查询的时候,MySQL可以利用这个特性,沿着叶子节点前进。而之所以NoSQL数据库会使用B树作为索引,也是因为它们不像关系型数据库那般大量查询都是范围查询; 82 | 3. B+树只在叶子节点存放数据,因此和B树比起来,查询时间稳定可预测。(注:这是一个高级观点,就是在工程实践中,我们可能倾向于追求一种稳定可预测,而不是某些数据贼快,某些数据唰一下贼慢) 83 | 4. B+树和跳表比起来,MySQL将B+树节点大小设置为磁盘页大小,这样可以充分利用MySQL的预加载机制,减少磁盘IO 84 | 85 | 关键点:高度低,叶子节点是链表,查询时间可预测性,节点大小等于页大小 86 | 87 | ### MongoDB为什么采用B树作为索引 88 | 89 | MongoDB认为**查询单个数据记录**远比遍历数据更加常见,同时对单记录查询和遍历查询都需要有较好的支持 90 | 1. 由于 B 树的非叶结点也可以存储数据,所以查询一条数据所需要的平均随机 IO 次数会比 B+ 树少 91 | 2. 如果采用哈希,有单条记录查询的复杂度都会是 O(1),但是遍历数据的复杂度就是 O(n),又对遍历数据没有较好的性能支持 92 | 93 | 94 | #### 如何引导过来这里? 95 | 1. 面试官直接问起来; 96 | 2. 你们聊起了树结构,聊到了B树和B+树,话术一般是“因为B+树和B树比起来,有...的优点,索引MySQL索引主要是使用B+树的; 97 | 3. 聊到了范围查询或者全表扫描,你可以从B+树的角度来说,这种扫描利用到了B+树叶子节点是链表的特征; 98 | 99 | ### 为什么使用自增主键 100 | 101 | 答案:MySQL的主键是一个**聚簇索引**,即它的叶子节点存放了数据。 102 | 在使用自增主键的情况下,会保证树的分裂照着单方向分裂的,这会大概率导致物理页的分裂也是朝着单方向进行的,即连续的。 103 | 在不使用自增主键的情况下,如果在已经满的页里面插入,会导致MySQL页分裂,虽然逻辑上页依旧是连续的,但是物理页已经不连续了。 104 | 如果在使用机械硬盘的情况下,会导致范围查询经常导致机械硬盘重新定位,性能差。 105 | 106 | 关键点:单方向增长,物理页连续 107 | 108 | #### 如何引导过来这里? 109 | 1. 面试官可能直接问你 110 | 2. 抖音项目涉及自增主键 111 | 3. 你在基本回答那里,回答到"聚簇索引"的时候,主动说起,为什么我们要使用自增索引。话术可能是"MySQL的主键索引是聚簇索引,每张表一个,所以我们一般推荐使用自增主键,因为自增主键会保证树单方向分裂..." 112 | 4. 聊到树结构的特征。比如说面试官其实面你的是数据结构,而不是数据库,但是你们聊到了树,就可以主动提起。因为大部分树,比如说红黑树,二叉平衡树,B树,B+树都有一个调整树结构的过程,所以可以强行引过来; 113 | 5. 聊起分库分表设计,主键生成的时候,可以提起生成的主键为什么最好是单调递增的。这个问题其实和为什么使用自增主键,是同一个问题; 114 | 115 | ### 索引有什么缺点 116 | 117 | 索引的维护是有开销的。在增改数据的时候,数据库都要对应修改索引;而如果索引过多,以至于内存没法装下全部索引,那么会导致访问索引本身都会触发IO。所以索引不是越多越好。比如为了避免数据量过大,某些时候我们会使用前缀索引。 118 | 119 | #### 如何引导过来这里 120 | 1. 面试官直接问了 121 | 2. 你在基本回答那里回答了前缀索引,之后可以说”使用前缀索引是为了节省空间,因为索引本身的维护是有开销的,除了空间开销,在数据更新的时候..." 122 | 3. 在回答完什么时候索引之后可以直接说 123 | 124 | ### 什么是索引下推 125 | 126 | 答案:索引下推是指将于索引有关的条件由MySQL服务器下推到引擎。例如按照名字存取姓张的,like "张%"。在原来没有索引下推的时候,即便在用户名字上建立了索引,但是还是不能利用这个索引。而在支持**索引下推的引擎上**,引擎就可以利用名字索引,将数据提前过滤,避免回表。目前innodb引擎和MyISAM都支持索引下推。索引下推和覆盖索引的理念都是一致的,尽量避免回表。 127 | 128 | ### 使用索引了为什么还是很慢? 129 | 130 | 索引只能帮助定位数据,但是**从索引定位到数据,到返回结果,或者更新数据,都需要时间**。尤其是在事务中,索引定位到数据之后,可能一直在等待锁。如果别的事务执行时间缓慢,那么即便你用了索引,这一次的查询还是很慢。本质上是因为,MySQL 的执行速度是受到很多因素影响的,准确来说,索引只是大概率能够加速这个过程而已。 131 | 132 | 另外要考虑,数据库是否使用错了索引。如果我们的表上面创建了多个索引,那么就会导致 MySQL 选择使用了不那么恰当的索引。在这种时候,我们可以通过数据库的 Hint 机制提示数据库走某个索引。 133 | 134 | 135 | #### 类似问题 136 | - 为什么我定义了索引,查询还是很慢?这个问题有一个陷阱,即他没说我用到了索引,也就是说,你定义了索引,但是可能MySQL没用;也可能用了,但是卡在锁竞争那里了 137 | 138 | #### 如何引导 139 | - 在前面聊到使用索引来优化的时候,可以提一嘴这个,即并不是说使用了索引就肯定很快 140 | 141 | ### 什么时候索引会失效? 142 | 143 | 没有使用索引主要有两大类原因,一种是自己 SQL 没写好,例如: 144 | 1. 索引列上做了计算 145 | 2. like 关键字用了前缀匹配,例如”%abc“。注意的是,后缀匹配是可以用索引; 146 | 3. 隐式转换——字符串列与数字比较 147 | 4. 如果是OR操作,只有一个字段没有索引,该语句不走索引 148 | 5. 使用!= 或者 <> 判断,不走索引 149 | 6. where中使用 is null,is not null也无法使用索引,不走索引 150 | 7. 不符合最左匹配原则的符合索引写法,不走索引 151 | 1. **in 和 = 都可以乱序**,比如有索引(a,b,c),语句 select * from t where c =1 and a=1 and b=1,这样的语句也可以用到最左匹配 152 | 2. 如果是select * from t where b=1 and c=1;(没匹配到a,碰到了b和c 结束匹配) 153 | 154 | 另一种,则是 MySQL 判断到使用索引的代价很高,比如说要全索引扫描并且回表,那么就会退化成为**全表扫描**。数据库数据量的大小和数据分布,会影响MySQL的决策。 155 | 156 | #### 类似问题 157 | - 为什么我定义了索引,查询还是很慢?没用 or 锁竞争 158 | - 为什么我定义了索引,MySQL 却不用? -------------------------------------------------------------------------------- /database/transaction.md: -------------------------------------------------------------------------------- 1 | # 数据库事务 2 | 3 | 事务是指多个数据库操作组成一个逻辑执行单元,满足 ACID 四个条件。 4 | 5 | - A是指原子性,即这些操作要么全部成功,要么全部不成功,不存在中间状态; 6 | - C是指一致性,数据库从一个状态转移到另外一个状态,数据完整性约束不变。在分布式语境下,这个很多时候是指数据如果存储了多份,那么每一份都应该是一样的。(后面分布式语境,要小心一点,因为这一步,可能会让面试官准备考察分布式事务) 7 | - I是指隔离性,一个事务的执行不会影响另外一个事务; 8 | - D是指持久性,已提交对数据库的修改,应该永久保留在数据库中。而实际上MySQL的事务,如果设置不当,可能出现事务已经提交,但是并没有被持久化。(这一点是为了加分的,你需要记住后面的《事务提交了但是数据没有保存》) 9 | 10 | 在MySQL上,innodb引擎支持事务,但是MyISAM不支持事务。(这个是为了引导面试官问两个引擎的区别) 11 | 12 | innodb 引擎是通过MVCC来支持事务的。(到这一步,停下来,接下来,面试官极大概率问你什么是MVCC) 13 | 14 | 关键点:ACID,innodb 通过 MVCC 支持事务 15 | 16 | ![数据库事务](img/transaction.png) 17 | 18 | ## 扩展点 19 | 20 | ### 什么是 MVCC 21 | 22 | MVCC,多版本并发控制。`innodb`引擎主要是通过`undo log` 和事务版本号来实现多版本,利用锁机制来实现并发控制。 23 | 24 | (接下来仔细解释`undo log`和版本号的运作机制,其中`undo log`是为了引导面试官继续问相关的问题,如`redo log`,`bin log`。) 25 | 26 | `innodb`引擎会给每张表加一个隐含的列,存储的是事务版本号。当修改数据的时候,会生成一条对应的`undo log`,`undo log`一般用于事务回滚,里面含有版本信息。简单来说可以认为`undo log`存储了历史版本数据。每当发起查询的时候,`MySQL` 依据隔离级别的设置生成`Read View`,来判断当前查询可以读取哪个版本的数据。例如,在已提交读的隔离级别下,可以从`undo log`中读取到已经提交的最新数据,而不会读取到当前正在修改尚未提交的事务的数据。 27 | 28 | 而锁机制,对于 innodb 来说,有多个维度: 29 | 1. 从独占性来说,有排他锁和共享锁; 30 | 2. 从锁粒度来说,有行锁和表锁; 31 | 3. 从意向来说,有排他意向锁和共享意向锁; 32 | 4. 从场景来说,还可以分为记录锁,间隙锁和临键锁; 33 | 34 | 分析:到这里停下来,上面这一番回答,基本上什么都点到了,接下来就是等提问了。这一堆回答,涉及到了很多知识点,可以考察的非常多: 35 | 1. `undo log`, `redo log`, `binlog` 36 | 2. 隔离级别 37 | 3. 各种锁,其中又以记录锁、间隙锁和临键锁比较有亮点 38 | 4. Read View 39 | 40 | 关键点:多版本 = undo log + 事务版本号,并发控制=各种锁 41 | 42 | 43 | #### 如何引导 44 | 1. 讨论数据库事务隔离级别 45 | 46 | ### 能够解释一下MySQL的隔离级别吗? 47 | 48 | 分析:考察基本的知识点。如果只是背出来各种隔离级别和对应存在的问题,那么就达标了。刷亮点如何刷呢?一个是结合 MVCC 来阐述MySQL是如何支持的;一个是讨论 snapshot isolation。前者比较中规中矩,后者比较多是秀知识面。我们分成这两个思路,前面都类似,就是总结各种隔离级别。 49 | 50 | 数据库的隔离级别有四种: 51 | 1. 未提交读:事务可以读取另外一个事务没有提交的数据。 问题:脏读,不可重复读,幻读 52 | 2. 提交读:事务只能读取到另外一个已经提交的事务数据。 问题: 不可重复读,幻读 53 | 3. 重复读:事务执行过程查询结果都是一致的,innodb 默认级别。 问题: 幻读 54 | 4. 串行化:读写都会相互阻塞 问题: 55 | 56 | 脏读: 57 | 58 | A事务读取B事务尚未提交的数据,此时如果B事务发生错误并执行回滚操作,那么A事务读取到的数据就是脏数据 59 | 60 | 不可重复读:(前后多次读取,数据内容不一致) 61 | 62 | 在事务A第一次读取数据,比如此时读取了小明的年龄为20岁,事务B执行更改操作,将小明的年龄更改为30岁,此时事务A第二次读取到小明的年龄时,发现其年龄是30岁,和之前的数据不一样了,也就是数据不重复了,系统不可以读取到重复的数据,成为不可重复读 63 | 64 | 幻读: 65 | 66 | 事务A在执行读取操作,需要两次统计数据的总量,前一次查询数据总量后,此时事务B执行了新增数据的操作并提交后,这个时候事务A读取的数据总量和之前统计的不一样,就像产生了幻觉一样,平白无故的多了几条数据,成为幻读。 67 | 68 | 69 | MVCC 方向: 70 | innodb 引擎利用了 `Read View` 来支持提交读和重复读。`Read View`里面维护这三个变量: 71 | 1. up_limit_id:已提交事务ID + 1 72 | 2. low_limit_id:最大事务ID + 1 73 | 3. txn_ids:当前执行的事务ID 74 | 75 | 提交读这个级别,默认读取是不加锁的,只有修改才会加锁。简单来说,已提交读,是每次查询都生成一个新的`Read View`,所以永远都能看到已经提交的事务。 76 | 77 | 可重复读则是在第一次查询生成`Read View`之后,后面的查询都是使用这个`Read View`。 78 | 79 | snapshot isolation 方向: 80 | 81 | innodb 引擎的可重复读隔离级别,要比定义的隔离级别更加严苛一点。一般的可重复读,无法解决幻读的问题。比如说原本你事务里面查询订单信息,这个时候又插入了一个新的订单,那么这种时候,幻读就会导致我们下一个查询就会查询到这条记录。但是 innodb 引擎的隔离级别并不会出现这个问题。 82 | 83 | 因为 innodb 引擎使用了临键锁,在“当前读”,也就是写的时候,锁住了记录之间的空档,防止插入数据。(这里面,不需要解释临键锁,等面试官提问) 84 | 85 | ### InnoDB的表锁与行锁 86 | - 表锁 87 | 1. 不会出现死锁,发生锁冲突几率高,并发低 88 | 2. 表级锁有两种模式:表共享读锁和表独占写锁 89 | 3. 读锁会阻塞写,写锁会阻塞读和写 90 | - 行锁 91 | 1. 会出现死锁,发生锁冲突几率低,并发高 92 | 2. 注意 93 | 1. 行锁必须有索引才能实现,否则会自动锁全表,那么就不是行锁了 94 | 2. 两个事务不能锁同一个索引 95 | 3. insert,delete,update在事务中都会自动默认加上排它锁 96 | 3. 适用常见 97 | 1. 用户消费 98 | 99 | ### 什么是意向锁 100 | 101 | 意向锁是表级锁的一种,它是由数据库引擎自行维护的,用户自己无需也无法操作意向锁 102 | 103 | 一个用户在表上添加一个共享锁或排他锁,需要如下检查: 104 | 1. 检查这张表的排他锁有没有被其他事务占用,如果有,那么加锁失败 105 | 2. 检查这张表中的行锁有没有被其他事务占用,如果有,那么加锁失败 106 | 107 | 对于第二点,如果一张表数据量太大,我们在其之上添加一个表锁,就得一行一行遍历该表数据有没有被锁住,效率低下。意向锁就算解决这个问题。 108 | 109 | 当一个事务想要获取表中某一行的(共享/排他)锁的时候,它会自动尝试给当前表的加上意向(共享/排他)锁。然后,表锁和行锁之间的兼容互斥性就变成了表锁和意向锁之间的竞争关系。 110 | 111 | ### 什么是共享锁,排它锁 112 | 113 | 分析:概念题,答完顺便回答意向排他锁,意向共享锁,刷一波 114 | 115 | 答案:共享锁指别的事务可以读,但是不可以写。排他锁,是指别的事务既不可以读也不可以写。与之非常类似的是,意向共享锁和意向排他锁,事务在获取共享锁或者排他锁之前,要先获得对应的意向锁。意向锁是数据库自己加的,不需要干预。 116 | 117 | (下面这段可能比较绕,记不住就算) 118 | 排它锁和其它三种都互斥;(X排斥一切) 119 | 意向排它锁和意向锁兼容;(IX 兼容 I) 120 | 共享锁和共享锁、意向共享锁兼容;(S 兼容 S) 121 | 122 | ![lock and i lock](https://pic4.zhimg.com/80/v2-37761612ead11ddc3762a4c20ddab3f3_720w.jpg) 123 | 124 | ### 什么是记录锁,临键锁,间隙锁 125 | 126 | 分析:概念题,可以点出来记录锁和行锁的关系,并且指明一下行锁是在索引项上加的。 127 | 128 | 答案: 129 | 1. 记录锁:锁住一行,所以叫做记录锁,也是行锁; 130 | 2. 间隙锁:锁住记录之间的间隔,或者索引之前的范围,或者所以之后的范围。只在重复读级别产生,(可以在前面隔离级别的地方提) 131 | 3. 临键锁(Next key lock):记录锁和间隙锁的组合,即锁住记录,又锁住了间隙 132 | 133 | ![记录锁和间隙锁](img/next_key_lock.png) 134 | 135 | 136 | #### 如何引导 137 | 1. 前面聊到了MVCC提到隔离级别,机会合适就可以主动发起进攻 138 | 139 | ### InnoDB的checkpoint技术 140 | - 数据库宕机,只需要在checkpoints后的重做日志恢复,缩短数据库恢复时间 141 | - 缓冲池不够用,将脏页(缓冲池页版本比磁盘更新)刷新到磁盘 142 | - 重做日志需要使用,强制产生checkpoints,缓冲池中页至少刷新到当前重做日志位置 143 | - 脏页数量太多,导致强制checkpoints 144 | 145 | #### 如何引导 146 | 1.提到InnoDB与MySQL的undo log,提出checkpoint技术 147 | 148 | ### InnoDB 的 Repeatable Read 隔离级别有没有解决幻读 149 | 150 | 先说答案:解决了(在官方文档的暧昧中),但是又没有完全解决(在头脑清醒的开发者眼中)。 151 | 152 | 如上文所言,官方文档中表述 InnoDB 用临键锁 (next-key lock) 解决了幻读的问题,临键锁工作在 RR 隔离级别下,设置隔离级别为 RC 会导致 GAP 锁失效,继而导致没有临键锁。这是 InnoDB 自我定义其 RC 存在幻读,而 RR 可以避免幻读的描述。 153 | 154 | InnoDB 作为一个优等生,在[隔离级别定义](https://en.wikipedia.org/wiki/Isolation_(database_systems)#Repeatable_reads)要求 RR 不需要避免幻读的情况下,宣称自己实现了这个功能。但实际上受到了限制: 155 | 156 | 157 | - 对于仅包含连续相同快照读语句的事务,MVCC 避免了幻读,但是这种场景临键锁没有用武之地,而官方文档重点强调是临键锁的实际避免了幻读,所以 InnoDB 肯定觉得自己做到了更多。 158 | - 对于仅包含连续相同当前读语句的事务,第一个当前读会加临键锁,会阻塞别的事物的修改,也避免了幻读。 159 | - 但是对于快照都和当前读语句交错的事务,第一个快照读后其它事务仍可以修改并提交内容,当前事务的后续当前读就会读到其他事务带来的变更。导致可以造出一些印证 InnoDB 没有解决幻读问题的例子。 160 | 161 | ![](img/rr-phantom-read-example.png) 162 | 163 | 快照读:当某个数据正在被修改的时候,也可以进行读取该数据,保证读写不冲突。第一次执行select的时候生成。 164 | 165 | #### 参考资料 166 | 167 | - 官方文档-[InnoDB 宣称使用临键锁解决幻读](https://dev.mysql.com/doc/refman/8.0/en/innodb-next-key-locking.html) 168 | - To prevent phantoms, InnoDB uses an algorithm called next-key locking that combines index-row locking with gap locking 169 | - 官方文档-[InnoDB 定义临键锁为 Record lock plus gap lock](https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html#innodb-next-key-locks) 170 | - A next-key lock is a combination of a record lock on the index record and a gap lock on the gap before the index record. 171 | - 官方文档-[InnoDB 临键锁工作在 RR 下](https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html#innodb-next-key-locks) 172 | - By default, InnoDB operates in REPEATABLE READ transaction isolation level 173 | - 官方文档-[InnoDB 可设置隔离级别为 RC 以关闭 Gap lock](https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html#innodb-gap-locks) 174 | - Gap locking can be disabled explicitly. This occurs if you change the transaction isolation level to READ COMMITTED 175 | - 官方文档-[幻读(幻影行)定义](https://dev.mysql.com/doc/refman/8.0/en/innodb-next-key-locking.html) 176 | - The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. 177 | - Wikipedia [幻读定义](https://en.wikipedia.org/wiki/Isolation_(database_systems)#Repeatable_reads) 178 | - A phantom read occurs when, in the course of a transaction, new rows are added or removed by another transaction to the records being read. 179 | - Wikipedia [Serializable 隔离级别可以避免幻读](https://en.wikipedia.org/wiki/Isolation_(database_systems)#Phantom_reads) 180 | - However, at the lesser isolation levels (than Serializable), a different set of rows may be returned the second time (phantom read happens) 181 | - MySQL [BUG #63870](https://bugs.mysql.com/bug.php?id=63870) 关于当前读附带一点幻读的讨论 182 | - 正经讨论 - [Innodb 中 RR 隔离级别能否防止幻读](https://github.com/Yhzhtk/note/issues/42) 183 | 184 | 185 | 186 | ### MySQL的死锁如何产生,如何解决 187 | 188 | MySQL出现死锁的要素为: 189 | 1. 两个或者两个以上事务 190 | 2. 每个事务都已经持有锁并且申请新的锁 191 | 3. 锁资源同时只能被同一个事务持有或者不兼容 192 | 4. 事务之间因为持有锁和申请锁导致彼此循环等待 193 | 194 | 如何解决: 195 | 1. 从死锁日志分析 196 | 2. 避免死锁 197 | 1. 调整业务逻辑 SQL 执行顺序, 避免 update/delete 长时间持有锁的 SQL 在事务前面 198 | 2. 以固定的顺序访问表和行 199 | 3. 合理的设计索引,区分度高的列放到组合索引前面,使业务 SQL 尽可能通过索引定位更少的行,减少锁竞争 200 | 4. 尽量控制事务的大小,减少一次事务锁定的资源数量,缩短锁定资源的时间 201 | 5. 尽可能使用低级别的事务隔离机制。 202 | 6. 在并发比较高的系统中,不要显式加锁,特别是是在事务里显式加锁。如 select … for update 语句,如果是在事务里(运行了 start transaction 或设置了autocommit 等于0),那么就会锁定所查找到的记录 203 | 204 | 205 | ### innodb 引擎和 MyISAM 引擎的区别 206 | 207 | innodb 引擎和 MyISAM 最大的区别是事务、索引、锁支持。 208 | 1. innodb 引擎支持事务,而 MyISAM 不支持; 209 | 2. innodb 引擎的主键索引的叶子节点存放的是数据本身,而MyISAM存储的是数据的地址,需要再一次寻址; 210 | 3. innodb 支持行锁,而MyISAM 只支持表锁,因此`innodb`支持的并发粒度更细更高; 211 | 212 | 一般来说,在不使用事务,数据修改少而读多的时候,又或者机器比较差的时候,用MyISAM比较合适。 213 | 214 | ### 为什么事务提交了但是数据没有保存 215 | 216 | 在MySQL的innodb引擎中,事务提交后,必须将数据刷盘到磁盘上,如果在事务提交之后,没来得及刷到磁盘,就会出现事务已经提交,但是数据丢失了。(回到这一步你要开始判断,如果你是主动聊的,那就停下来,等面试官追问;如果这是面试官问的,那就接着答细节)MySQL的innodb引擎,事务提交的关键是将`redo log`写入到`Log buffer`,而后MySQL调用`write`写入到`OS cache`,只有到最后操作系统调用`fsync`的时候,才会落到磁盘上。 217 | 218 | (为了方便记忆,记住这个过程:`commit` -> `log buffer` -> `OS cache` -> `fsync`) 219 | (下面这一段是可选) 220 | 数据库有一个参数 `innodb_flush_log_at_trx_commit` 可以控制刷盘的时机: 221 | 1. 0,写到`log buffer`, 每秒刷新; 222 | 2. 1,实时刷新; 223 | 3. 2,写到`OS cache`, 每秒刷新 224 | 225 | (接下来步入终极装逼环节,为了表达我们对这个问题的深刻理解,对OS的一般理解,我们得扩充一下回答面,慎用) 226 | 227 | Redis的`AOF`机制也面临类似的问题,即`AOF`也不是立刻刷盘,而是写入到了`OS cache`,等到缓冲区填满,或者`Redis`决定刷盘才会刷到磁盘。而`redis`有三种策略控制,`always` 永远, `everysec` 每秒, `no` 不主动。默认情况下`everysec`,即有一秒钟的数据可能丢失。 228 | 229 | (最后升华一下主题) 230 | 对于大多数要和磁盘打交道的系统来说,都会面临类似的问题,要么选择性能,要么选择强持久性。 231 | 232 | 关键字:提交不等于落盘了,`fsync` 233 | 234 | #### 如何引导 235 | 1. 从`Redis` AOF 引过来,两边讨论的都是同一个主题; 236 | 2. 回答`ACID`的时候引导过来; 237 | 3. 讨论磁盘 IO 的时候看情况; 238 | 4. 讨论操作系统文件系统的时候,看情况; 239 | 240 | 核心就是,涉及到了`OS cache`,`fsync`等点,就可以引导来这边。 241 | 242 | 243 | ### Drop、Delete与Truncate的共同点与区别 244 | - 共同点:都表示删除 245 | - 区别: 246 | - **Delete**用来删除表的全部或者一部分数据行,执行delete之后,用户需要提交(commmit)或者回滚(rollback)来执行删除或者撤销删除,会触发这个表上所有的delete触发器 247 | - 删除操作作为事务记录在日志中保存以便回滚操作 248 | - 日志太大,从checkpoin开始回滚 249 | - **Truncate**删除表中的所有数据,这个操作不能回滚,也不会触发这个表上的触发器,TRUNCATE比delete更快,占用的空间更小,保留表 250 | - **Drop**命令从数据库中删除表,所有的数据行,索引和权限也会被删除,所有的DML触发器也不会被触发,这个命令也不能回滚 251 | 252 | ### 什么是redo log, undo log 和 binlog 253 | 254 | 255 | 1. `redo log` 是`innodb`引擎产生的,主要用于支持MySQL事务,MySQL会先写`redo log`,而后在写`binlog`。`redo log`可以保证即使**数据库异常重启,数据也不会丢失**,只记录事务对数据页做了哪些修改 256 | - 采用循环写的方式记录,当写到结尾时,会回到开头循环写日志 257 | 2. `undo log` 是`innodb`引擎产生的,主要时候用于解决事务回滚和MVCC。数据修改的时候,不仅记录`redo log`,也会记录`undo log`。在事务执行失败的时候,会使用`undo log`进行回滚; 258 | 3. `binlog` 主要用于主从复制和数据恢复,记录了写入性的操作。`binlog`分成基于语句,基于行和混合模式三种复制模式。 259 | - 基于语句:将修改数据的sql语句记录到binlog中 260 | - 基于行:仅记录哪条数据被修改 261 | - 混合:使用基于语句模式,语句模式无法复制操作用基于行模式保存 262 | 263 | (扩展点1,阐述两阶段提交) 264 | 因为`redo log`生成到`binlog`写入之间有一个时间差,所以为了保证两者的一致性,MySQL引入了两阶段提交: 265 | 1. Prepare阶段,写入`redo log`; 266 | 2. Commit阶段,写入`binlog`,提交事务; 267 | 268 | (扩展点2,阐述一下的刷盘时机) 269 | 1. `binlog` 刷盘可以通过`sync_binlog`参数来控制。0-系统自由判断,1-commit刷盘,N-每N个事务刷盘 270 | 2. `redo log`刷盘可以通过参数`innodb_flush_log_at_trx_commit`控制。0-写入`log buffer`,每秒刷新到盘;1-每次提交;2-写入到`OS cache`,每秒刷盘; 271 | 272 | 273 | ## Reference 274 | [一文理解MySQL MVCC](https://zhuanlan.zhihu.com/p/29150809) 275 | [innodb中的事务隔离级别和锁的关系](https://tech.meituan.com/2014/08/20/innodb-lock.html) 276 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | # docker常用命令 2 | 3 | ## 介绍一下docker与虚拟机区别 4 | 5 | 二者目的不同 6 | 7 | VMware 为应用提供虚拟的计算机(虚拟机);Docker 为应用提供虚拟的空间,被称作容器(Container) 8 | 9 | 对于虚拟机来说 10 | 1. 用起来像一台真的机器那样,包括开机、关机,以及各种各样的硬件设备,需要安装操作系统。同时未来多个操作系统高效率同时进行,虚拟机很依赖底层硬件架构提供的虚拟化能力 11 | 1. Hypter-V:代表WSL2 12 | 2. Type-2:代表VMware 13 | 2. 一台实体机上的所有的虚拟机实例不会互相影响 14 | 15 | 而对于容器来说, 16 | 1. 容器是直接跑在操作系统之上的,容器内部是应用,应用执行起来就是进程 17 | 18 | ## 什么是docker 19 | 20 | Docker对进程进行封装隔离,属于操作系统层面的虚拟化技术。 由于隔离的进程独立于宿主和其它的隔离的进 程,因此也称其为容器。 21 | 22 | 1. 镜像 23 | - 特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等) 24 | 2. 容器 25 | - 容器是镜像运行时的实体 26 | 3. 仓库 27 | - 镜像仓库是Docker用来集中存放镜像文件的地方类似于我们之前常用的代码仓库 28 | ## images镜像 29 | 30 | - docker pull ubuntu:20.04:拉取一个镜像 31 | - docker images:列出本地所有镜像 32 | - docker image rm ubuntu:20.04 或 docker rmi ubuntu:20.04:删除镜像ubuntu:20.04 33 | - docker [container] commit CONTAINER IMAGE_NAME:TAG:创建某个container的镜像 34 | - docker save -o ubuntu_20_04.tar ubuntu:20.04:将镜像ubuntu:20.04导出到本地文件ubuntu_20_04.tar中 35 | - docker load -i ubuntu_20_04.tar:将镜像ubuntu:20.04从本地文件ubuntu_20_04.tar中加载出来 36 | 37 | ## 容器 38 | 39 | - docker [container] create -it ubuntu:20.04:利用镜像ubuntu:20.04创建一个容器。 40 | - docker ps -a:查看本地的所有容器 41 | - docker [container] start CONTAINER:启动容器 42 | - docker [container] stop CONTAINER:停止容器 43 | - docker [container] restart CONTAINER:重启容器 44 | - docker [contaienr] run -itd ubuntu:20.04:创建并启动一个容器 45 | 46 | 47 | images和container是 类和对象 的关系 -------------------------------------------------------------------------------- /gc/README.md: -------------------------------------------------------------------------------- 1 | # GC (垃圾回收) 2 | 3 | ## GC 的考察点 4 | GC 在特定的语言里面,是一个极其重要的面试知识点,比如说 Golang 和 Java。 5 | 6 | GC 的面试,主要是从三个维度进行考察: 7 | 1. GC 的算法:例如引用计数,标记清扫,标记整理等,纯粹从算法的层面考察大家; 8 | 2. GC 的实现:例如 JVM 中 HotSpot 实现的 CMS,G1等,重点考察这些实现的具体步骤,部分情况下会涉及细节; 9 | 3. 实践:集中在,DEBUG 和调优。DEBUG是指,实践中是否遇到多 GC 相关的问题,如果遇到了,怎么解决的;调优则是发现实际中的 GC 的效果不理想,如何优化的问题; 10 | 11 | 因此 GC 要如何复习,才能面试顺利呢? 12 | 1. 背熟算法; 13 | 2. 背熟具体实现的步骤,部分重点实现要深挖细节。要熟记影响这些实现的参数,同时可以结合自己公司内部配置来记忆参数和理解; 14 | 3. 准备案例,包括各种奇诡问题,优化案例。这里要注意的是,**如果你亲自遇到过,那么就用你亲自遇到过**,如果没有,用你同事遇到的问题;依旧没有,就用公司出过的问题;完全没遇到,就准备网上的案例; 15 | 16 | 那么常见的面试失误在哪里呢? 17 | 1. 算法和实现不能区分清楚; 18 | 2. 并行和并发不能区分清楚; 19 | 3. 实现之间混淆; 20 | 4. 遗漏了不同实现的参数配置及其影响; 21 | 5. 未提前准备好各种案例; 22 | 23 | 那怎么样才能在 GC 面试里面刷出来亮点?—— 与众不同 24 | 1. 别人不知道的,我知道; 25 | 2. 别人知道的,我知道更多细节; 26 | 3. 结合实际 27 | 4. 结合内存分配器 28 | 5. 横向比较 29 | 30 | 前两条很好理解,后一条如何理解呢?要知道,面试官面 GC,不管是面算法,面实现还是面调优,他就是想确认你能不能解决 GC 的问题。所以结合实际能够让他知道,你确实是知道如何解决 GC 问题的。 31 | 32 | 第四点则是一个盲点。就是大部分的人只关注过GC,但是没有关注过不同垃圾回收器,其内存是如何被分配的。比如说 CMS 采用了空闲链表法来管理空闲内存,就是一个很有特色的点。 33 | 34 | 35 | -------------------------------------------------------------------------------- /gc/algorithm.md: -------------------------------------------------------------------------------- 1 | # GC 算法 2 | 分析:算法可以从多个维度进行分析。 3 | 4 | ## 总览 5 | 6 | ### 触发条件 7 | 8 | - 主动触发(手动触发),通过调用 runtime.GC 来触发 GC,此调用阻塞式地等待当前 GC 运行完毕。 9 | 10 | - 被动触发,分为两种方式: 11 | 12 | - 使用系统监控,当超过两分钟没有产生任何 GC 时,强制触发 GC。 13 | 14 | - 使用步调(Pacing)算法,其核心思想是控制内存增长的比例,每次内存分配时检查当前内存分配量是否已达到阈值(环境变量 GOGC):默认 100%,即当内存扩大一倍时启用 GC。 15 | 16 | ### 收集器与回收器 17 | 18 | 首先,算法可以分成两个大问题 19 | 1. 如何找到存活对象:基本上就是两类,引用计数和标记(也叫做跟踪式,可达性分析); 20 | 2. 怎么回收空间:复制,整理或者清扫。复制是指直接将存活对象拷贝到另外一块内存区域;压缩是指,将存活对象挪到一起;清扫实际上,大多数时候,就是做一些标记,标记内存可用; 21 | 22 | 这里要强调一下清扫。清扫明面上是指,我将垃圾扫掉,实际上是指将内存返回给内存分配器。那么问题就来了,内存分配器怎么维护这些空闲内存?基本上这里又是两种选择,一种是位图,一种是空闲链表法。相比之下,采用复制的算法,基本上就只需要维护寥寥几个地址。 23 | 24 | 如果笛卡尔积一下,就有了: 25 | 1. 引用计数-复制,引用计数-整理,引用计数-清扫; 26 | 2. 标记-复制,标记-整理,标记-清扫; 27 | 28 | 不过引用计数我们面试比较少遇到,它在实际中用得也不多,标记类用得比较多。 29 | 30 | ### 并发与并行 31 | 32 | 算法又有并发和并行之分。这里提到的并发和并行,在GC这个特定的语义下,含义稍微有点区别。 33 | 34 | 这里我们说的并发,其实是指 GC 线程和应用线程,一起运行。就是一边GC,一边对外服务。并行则是指多个GC线程一起干活。 35 | 36 | 那么,要记住,多核 CPU 之下,并发往往意味着并行。即,GC 线程和应用线程是并行的,GC线程和GC线程也是并行的,应用线程和应用线程也是并行的。 37 | 38 | 但是并行不一定意味着并发。例如 Java 里面的 Parallel New,就是GC线程并行收集,这个就不是并发的,因为没有应用线程此时也在运转。 39 | 40 | 典型的并发 GC: 41 | 1. Java 上 HotSpot 实现的 CMS,G1,ZGC 42 | 2. Golang GC; 43 | 44 | 典型的并行 GC:HotSpot 带 Parallel 关键字的,Old Parallel, Parallel New 45 | 46 | #### 类似问题 47 | - 并发 GC 是并行 GC吗?多核 CPU 上就是,否则就不是 48 | - 并行 GC 一定是并发 GC吗?不是 49 | 50 | ### 分代 51 | 52 | 不是所有 GC 都有分代的!! 53 | 54 | 分代主要是基于分代假说,大部分刚分配的对象会在短时间内死掉,越是生存时间长的对象,越不容易死掉 55 | 56 | (类比“**幼儿夭折**”和“**老不死**”) 57 | 58 | ![HotSpot分代的效果](https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/img/jsgct_dt_003_alc_vs_srvng.png) 59 | 60 | 典型的分代 GC:HotSpot 的大部分GC都是 61 | 62 | ### 增量回收 63 | 64 | 增量回收核心在于,回收的时候并不是将整个堆,或者整个分代回收掉,而是只回收部分。实施增量回收核心就在于避免在一次GC中消耗太多资源,典型的就是 G1 采用了增量回收来避免停顿时间超长。 65 | 66 | 典型的增量回收GC:HotSpot G1 67 | 68 | ## 面试题 69 | 70 | ### 你了解 XXX 算法吗? 71 | 72 | 记住步骤: 73 | 1. 基本流程 74 | 2. 优缺点 75 | 76 | 下面我们一个个算法说过去。 77 | 78 | #### 标记-复制 79 | 80 | (首先回答基本流程)标记-复制算法,在GC开始的时候,会从 GC root 出发标记,沿着对象的引用链,标记存活的对象。在标记完成之后,将存活的对象复制到另外一块内存。Java的Serial New, Parallel New 和 G1 都是采用了标记复制算法。 81 | 82 | (讨论优缺点)该算法的优点是,复制会保证我们能够得到一块连续的内存,可以采用高效率的内存分配方案(叫做bump-the-pointer,其实就是指针移动)。缺点则是内存利用率不高,极端情况下,我们只能利用一半内存,另外一半内存要作为复制的目标内存。而且复制也是一个消耗极大的过程。 83 | 84 | ![指针碰撞](https://img-blog.csdnimg.cn/20201130155644621.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3JkX3dfY3Nkbg==,size_16,color_FFFFFF,t_70#pic_center) 85 | 86 | ##### 类似问题 87 | - 复制算法,只能使用一半内存吗?这个问题是说,我们在使用复制算法的时候,要留出一部分空间来装复制的对象。比如说,我们有1G内存,复制算法是不是只能使用 500 M,剩下的500M作为装存活对象的空间。并不是,假如说我们存活对象占比10%,例如我100M对象,存活的有10M。那么我就只需要10M来装存活对象。回到这个 1G 的例子,这意味着我可以用 800 M,第一次回收,有 80M 存活,我丢到一个 100M 的块里边;第二次它是 (800+100) * 10% = 90 M 存活对象,还剩下 100 M,非常完美放下。这就是 JVM 里面为啥是两个 Survivor。因此用复制算法,内存利用率也可以超过50%。 88 | ![注意一个Eden两个](https://img-blog.csdn.net/20170518171044703?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvc3RlZF96eHo=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) 89 | 90 | #### 标记-整理 91 | 92 | (首先回答基本流程)标记-整理算法,在GC开始的时候,会从 GC root 出发标记,沿着对象的引用链,标记存活的对象。在标记完成之后,将存活的对象全部挪到一侧。这个过程类似于标记-复制。比较有名的采用了这个算法的就是 HotSpot 里面的 Serial Old 和 Parallel Old。 93 | 94 | (讨论优缺点)该算法的优点是,整理复制会保证我们能够得到一块连续的内存,可以采用高效率的指针碰撞技术来分配内存。缺点则是整理过程非常耗时,涉及到了大量对象移动。比如 CMS 在启用压缩之后,这个过程是 STW 的,导致 GC 停顿时间特别长。 95 | 96 | (讨论改进方案,亮点)有一种改进思路,是不进行全量整理,而是部分整理,即每次 GC 只会整理一部分,作为 GC 停顿时间和内存碎片的一种权衡。 97 | 98 | #### 标记-清扫 99 | 100 | (首先回答基本流程)标记-清扫算法,在GC开始的时候,会从 GC root 出发标记,沿着对象的引用链,标记存活的对象。在标记完成之后,垃圾回收器会把空闲内存交回内存分配器。最有名的标记清扫垃圾回收器是 CMS 回收器。 101 | 102 | (讨论优缺点)该算法的优点是,清扫的过程比复制和整理要快很多。但是带来的缺点是,内存分配要更加复杂,并且空闲内存不再是一个连续的块。比如说 CMS 就采用了空闲链表来管理空闲内存(这里可能引导过去聊CMS 空闲内存管理,不熟悉就忽略)。带来严重的内存碎片问题。 103 | 104 | ##### 如何引导 105 | - 从操作系统内存管理聊过来 106 | 107 | #### 引用计数 108 | 109 | 引用计数是在对象里面维护一个计数,标记有多少对象使用到了该对象。如果计数为0,就表明该对象可以被回收了。 110 | 111 | 该算法的优点是实现简单,GC过程很快。缺点则是,循环引用难以解决。所谓的循环引用,是指多个对象之间互相引用,最终行成一个环,因此它们的计数永远都不会为0。Swift 的垃圾回收就是采用了引用计数(赫赫有名的ARC),还有很多智能指针也是用引用计数来实现的。 112 | 113 | 还有一个缺点,则是整个开销和引用变更次数成正比。 114 | 115 | (注意,如果记得住,可以接着回答**如何解决循环引用**) 116 | 117 | ### 如何解决引用计数中的循环引用问题? 118 | 分析:难题。其实大多数情况下,面试官问出来这个问题,也没指望我们能回答出来,就是抱着万一你知道的心态。当然,如果你面的语言,就是用了引用计数来做GC的话,那么这会比较重要。对于 Java,golang 开发来说,稍微知道一点就可以。 119 | 120 | 答案:一般是有三种策略: 121 | 1. 采用特殊引用。例如使用 weak reference,弱引用。这一类的做法是用户需要自己显式管理自己的引用,在出现循环引用的地方,将一部分引用修改为 weak reference,从而所谓的 strong reference 就不再组成环; 122 | 2. 采用后备的追踪式收集器。一般来说,是把可能出现环的对象单独处理,用追踪式的收集器标记一遍,这些就是存活对象。(Python就是这种策略,不了解算法细节) 123 | 3. 采用试探删除策略。该方法类似于图里面去除环的算法,尝试把某些引用删掉。如果删掉之后别的对象的计数变为0,那么说明这些对象只有环内的引用,因此是可回收对象。 124 | 125 | #### 如何引导 126 | - 在聊到了引用计数的时候。这个是一个比较安全的亮点,就是认真研究过循环引用处理方案的面试官不多 127 | - 在聊到特殊引用的时候,可以讲一下特殊引用在引用计数里面的应用。 128 | 129 | #### 类似问题 130 | - 追踪式垃圾回收器如何解决循环引用问题?这个问题其实是吓人的,因为追踪式的一般都是使用三色法来追踪对象,天然就解决了,可以参考后面的三色标记法面试题 131 | 132 | ### 引用计数和可达性分析的优缺点? 133 | 分析:根据两者的基本特征来回答就可以。这个问题比较罕见。 134 | 135 | 答案:引用计数最大的优点就是实现简单、GC 很快,整体开销被平摊到了整个应用生命周期内,对并发 GC 支持比较好。缺点则是循环引用难以解决,整体开销和引用变更次数成正比,比较大。 136 | 137 | 可达性分析则是会在 GC 过程中引入 STW,难以实现,并发 GC 的实现特别困难。优点则是可达性分析只和存活对象数量有关,开销较小。并且可达性分析解决循环引用的问题非常容易 138 | 139 | #### 如何引导 140 | - 无论是讨论了引用计数还是可达性分析,都可以做一个总结 141 | 142 | #### 类似问题 143 | - XXX 为什么用引用计数/可达性分析?总结它们的优缺点,指出就是不同人在不同场景下的权衡。 144 | 145 | 146 | ### 什么是三色标记法 147 | 分析:考察基本算法。答出一般步骤就可以。如果要刷亮点,就要回答并发标记流程,并发的情况下,可能误把回收对象标记为存活。 148 | 149 | 答案:三色标记法是指在标记过程中将对象标记为黑色、灰色或者白色。黑色代表存活对象,灰色代表正在标记中,白色表示死亡对象; 150 | 1. 最开始的时候,所有的对象都是白色的; 151 | 2. 而后从GC root 出发,首先将对象标记为灰色,其次将其引用对象标记为灰色,再把自己从灰色变为黑色; 152 | 3. 重复步骤2,直到灰色对象均为黑色 153 | 4. 通过写屏障检测对象有变化,重复以上操作 154 | 5. 收集所有白色对象 155 | 156 | ![三色标记法](https://pic2.zhimg.com/v2-5fe8ea45e2518ca19cfeb31558160fb1_b.webp) 157 | 158 | (引出误标记的话题)这个标记过程可以和应用线程并发运行,不过这个时候可能存在一个问题,就是可能一个对象被标记为黑色(即存活),但是随后应用线程更新了指向它的引用,它变成了死对象。这个时候,标记结束之后,该对象依旧会被认为还存活着(活死人,假阴性)。 159 | 160 | (这里我们补充一下如何解决循环引用)使用三色标记法能够天然解决循环引用的问题,因为循环引用的一端,必然会被先染成了黑色,这时候就直接跳过,而不会重复染色,导致循环。 161 | 162 | #### 类似问题 163 | - 三色标记法怎么解决循环引用问题?其实这个问题有点鸡肋,一般了解一点三色标记法的人都不会问这个。因为三色标记法里面,循环引用就不是一个问题。 164 | - 标记为黑色的对象一定是存活对象吗?并发下就不是,非并发下就是 165 | 166 | #### 补充:写屏障 167 | 为了避免 GC 的过程中新修改的引用关系到 GC 的结果发生错误,我们需要进行 STW)。但是 STW 会影响程序的性能,所以我们要通过写屏障技术尽可能地缩短 STW 的时间 168 | - 引用对象丢失的条件: 169 | - 一个黑色的节点 A 新增了指向白色节点 C 的引用,并且白色节点 C 没有除了 A 之外的其他灰色节点的引用,或者存在但是在 GC 过程中被删除了。 170 | - 以上两个条件需要同时满足: 171 | - 满足条件 1 时说明节点 A 已扫描完毕,A 指向 C 的引用无法再被扫描到; 172 | - 满足条件 2 时说明白色节点 C 无其他灰色节点的引用了,即扫描结束后会被忽略 。 173 | 174 | - 破坏条件1:Dijistra 写屏障 175 | - 黑色节点不允许引用白色节点,一旦其新增了白色节点的引用时,将对应的白色节点改为灰色 176 | 177 | - 破坏条件2:Yuasa 写屏障 178 | - 黑色节点允许引用白色节点,但是该白色节点有其他灰色节点间接的引用 179 | - 当白色节点被删除了一个引用时,悲观地认为它一定会被一个黑色节点新增引用,所以将它置为灰色 180 | 181 | ### 补充:STW 182 | 为了避免在 GC 的过程中,对象之间的引用关系发生新的变更,使得 GC 的结果发生错误(如 GC 过程中新增了一个引用,但是由于未扫描到该引用导致将被引用的对象清除了),停止所有正在运行的协程。 183 | 184 | ### 为什么使用并发 GC? 185 | 186 | 分析:并发 GC 意味着应用线程不停,减少停顿时间。 187 | 188 | 答案:并发 GC 有很显著的优势,即在整个回收过程中,大部分情况下,应用依旧可以对外服务,仅仅需要在特定的时间节点上 STW,整体停顿时间很多。 189 | 190 | 不过并发 GC 一般实现复杂,而且吞吐量不如并行 GC。(这里尝试引导面试官,进一步问,为什么并发 GC 吞吐量不如并行 GC) 191 | 192 | ### 为什么并发 GC 的吞吐量一般比并行 GC 要低? 193 | 194 | 分析:考察并发 GC 实现上的难点。核心就在于并发 GC 要额外引入别的数据结构和步骤来处理,在并发过程中,引用的变更。例如回收过程中,应用创建了新的对象,修改了原本的对象。我们这里使用具体的例子来总结。 195 | 196 | 答案:主要在于,并发 GC需要引入额外的数据结构和步骤来处理并发过程中,应用线程修改过的对象。(如果自己不熟悉后面的这些,就不要说)例如在 Java CMS 回收器,就引入了预清理和再标记步骤,G1 引入 SATB (snapshot at the beginning)了。这些都会占据更多的 CPU 资源。 197 | 198 | ### 并发 GC 和并行 GC 比起来有什么优缺点? 199 | 200 | 分析:考察的是这两大类 GC 的设计初衷。并发 GC 设计初衷是为了不停下应用线程,也是为了降低停顿时间。而并行 GC 则是纯粹为了加快 GC 速度。 201 | 202 | 因此,并发 GC的优点是应用不停,停顿时间短;并行 GC则是吞吐量大。 203 | 204 | 答案:并发 GC 优点在于整个 GC 过程中,大多数时候应用不需要停下来,因此应用能够平稳运行,整个STW的时间也短。 205 | 206 | 并行 GC 则专注在吞吐量,停顿时间会比并发GC长。 207 | 208 | (给出选择建议)对于互联网应用这种强调停顿时间的应用来说,一般选择并发 GC;而对于批处理之类的应用,则可以使用并行 GC。 209 | 210 | #### 类似问题 211 | - 并发 GC 性能比并行 GC 好?错 212 | - 并行 GC 性能比并发 GC 好?错 213 | - 什么时候用并发 GC? 214 | - 什么时候用并行 GC? 215 | 216 | ### 什么是安全点? 217 | 218 | 分析:这个问题其实并没有非常标准的回答,理论上也应该很少有人关注,不过我被问过几次,姑且放这里。一般来说,只有JVM 才会面这个问题。别的语言应该虽然有类似的概念——比如说 golang,但是面试没遇到过。 219 | 220 | 答案:安全点是指在这个时间点上,引用关系不会被改变,常见的安全点有方法调用和循环。GC一般要从安全点开始,当准备 GC 的时候,会等待所有的线程都到达安全点,这就是 STW 的实现方式。但是也有别的问题,需要利用到安全点,例如我们尝试 dump 整个堆栈。 221 | 222 | #### 如何引导 223 | - 在谈到 STW 的时候,可以聊起是怎么进入 STW 状态的,就是依靠安全点 224 | 225 | ### 如何判断一个对象存活? 226 | 分析:一般来说,面试官问出这个问题,其实是希望你回答什么标记过程啥的。但是呢,这个问题的答案,准确来说,是问的引用计数和可达性分析,所以先回答可达性分析,直接命中面试官的下怀,然后再补充引用计数。 227 | 228 | 答案:这主要有两种手段,可达性分析和引用计数。 229 | 230 | 可达性分析目前主流是采用三色标记法,三色标记法巴拉巴拉(接上面什么是三色标记法),标记结束之后白色的对象就是死掉的对象。在并发标记的时候,黑色的对象是可能存活对象,但是并不能确保一定存活(亮点也在这里,就是并发三色标记的活死人问题); 231 | 232 | 引用计数,则要简单很多。计数为 0 就是死掉了,不过考虑到循环引用的问题,应该说,除了循环引用之外的引用数量是0,代表已经死了(亮点在要解释循环引用的特殊之处,它们虽然计数不为0,但是已经死掉了)。 233 | 234 | #### 类似问题 235 | - 如何知道一个对象可以被回收了? 236 | - 引用计数不为0,对象一定活着吗? 237 | 238 | ### GC root 是什么? 239 | 分析:本来这个问题,应该和具体的实现结合在一起来考察的。比如说,准确的问法是,我用 CMS + Parallel New,那么在 Full GC 的时候,GC root 包含哪些?不过很多时候面试官都不严谨,所以我们可以从一般原则上回答,然后举个例子。这个问题,在具体语言的 GC 上还会进一步分析。 240 | 241 | 答案:GC root,顾名思义,是指在GC启动的时候必然存活的一组对象。一般来说,GC root 包含: 242 | 1. 栈上对象,于 Java 来说,还包含本地方法栈; 243 | 2. 全局对象,如常量池; 244 | 3. 非收集部分指向收集部分的引用。常见于分代 GC 和增量式GC中 245 | 246 | ### Minor GC 是什么?Major GC 是什么? 247 | 分析:茴香豆的茴字有几种写法的问题。 248 | 249 | 答案:Minor GC 是指年轻代的垃圾回收,Major GC 是指 Full GC。 250 | 251 | ### 为什么要分代? 252 | 分析:其实分代不是最开始就有的,而是大家观察到了分代假说的两个现象之后,才有了分代的设计。那么分代究竟是为什么引入呢?很简单,就是既然新对象很容易就死掉,老对象很难死掉,那我们就分开着两个,然后新对象朝生夕死,这样就可以每次都回收大量的空间。而老对象待着的地方,我就可以少回收,反正也回收不到东西,只在确实没空间了我再回收。所以分代,核心就是为了提高 GC 效率。 253 | 254 | 但是还有一个难点,就是为啥有的 GC 实现是不分代的。这个问题的答案可以作为我们回答的亮点。 255 | 256 | 答:(首先回答分代假说,这是从理论上直接回答了这个问题)分代是基于分代假说,即新对象很容易死,老对象不容易死。(下面点出核心,就是为了效率)因此如果我们采用分代,依据存活时间来将对象放到不同的内存区域,那么在回收的时候,就可以只回收年轻代,或者一起回收。这样一来,回收效率高,(效率高的两个方面)一方面是停顿时间短(也可以说是资源消耗低),一方面是能够回收更多的内存。 257 | 258 | (下面我们指出并不是所有的 GC 实现都是分代的,作为一个亮点)但是并不是所有的 GC 都是分代,例如Java 的 ZGC,golang GC 都不是分代的。绝大部分情况下,分代都要比不分代效率高,但是分代带来的了额外的问题: 259 | 1. 实现难度高,分代 GC 的实现难度,要比非分代高一个量级; 260 | 2. 较难配置和优化,所有的分代 GC 都面临一个问题,就是各个分代的大小该如何确定; 261 | 262 | #### 类似问题 263 | - 为什么有些 GC 没有采用分代 264 | 265 | ### 对象死了就立刻会被回收吗? 266 | 267 | 答案:并不是。只有触发了 GC 才会被回收。 268 | 269 | ### 对象死了一定会被回收吗? 270 | 271 | 答案:并不是,有些语言设计了复活机制,那么它可以在被回收之前,重新活过来(也就是又有了新的引用指向它)。比如说 Java。 -------------------------------------------------------------------------------- /gc/g1.md: -------------------------------------------------------------------------------- 1 | # G1 垃圾回收器 2 | 3 | 分析:CMS 和 G1 都可以被认为是近年面试考察的高频考点。G1 的复习也类似于 CMS 的复习,重点在于捋清楚其中的步骤。而后为了刷出亮点,可以尝试在部分细节上下功夫。 4 | 5 | G1 的几个基本概念要捋清楚: 6 | 1. Region。这个可以说是和 CMS 根源上不同设计理念的体现。总体来说,虽然 CMS 曾经也是支持增量式回收的,但是做得不如 G1 彻底。G1是彻底的增量式回收,原因就在于,它不是每次都回收全部的内存,而是挑一部分 Region 出来。之所以只挑选一部分出来,核心也就是为了控制停顿时间。 7 | 2. Garbage First:也就是 G1 名字的由来。是指,每次回收的时候,回收器会从 Region 里面挑出一些比较脏的来回收。注意这里面有两个,**挑出一些** 和 **比较脏**。这揭示了两个问题:第一个,G1是增量式回收的;第二,G1 优先挑选垃圾最多的。 8 | 9 | 这里给出一个理解 G1 算法的思路: 10 | 11 | G1 的目标是控制住停顿时间。那么我们怎么控制停顿时间?一种比较好的思路就是,我每次回收只回收一小部分内存。例如说我有一个 32G 的堆,我每次只回收 4 个G。那么如果原来你停顿时间是32秒,回收 4G 就只需要5秒。 12 | 13 | 进一步你就会想,如果是我来设计这个 G1,我要想做到这一步,我该怎么搞?我能不能先把堆分成四部分,每次回收其中的一部分? 14 | 15 | 答案是可以的。然后你就又会遇到问题,有些人可能想回收三分之一的堆,那你怎么办?加个参数控制?比如说启动的时候让用户指定把堆分成多少分? 16 | 17 | 那么问题又来了,用户也不知道该分成多少份才能恰好满足自己希望的停顿时间。 18 | 19 | 这个时候你就会考虑,能不能让用户把他希望的停顿时间告诉你,你自己来决定回收多大的一块。 20 | 21 | 到了这一步,你又会发现一个问题,即便用户告诉你期望停顿时间要控制在一秒内,于是你提前把堆分成了三十二份,但是因为应用的负载不是静态的,导致你每次回收一份,也会经常超出期望。 22 | 23 | 这个时候,你就会想,我这提前划分好感觉不太靠谱,能不能动态划分呢? 24 | 25 | 所以问题的根源就是怎么做到动态划分,比如说一会分成三十二份,一会分成六十四份。这个问题难在哪里?难在怎么知道不回收的部分,有哪些引用指向了被回收部分。如果直接动态划分,就没法子维护整个信息。 26 | 27 | 那么,你就会想到,我能不能先把堆划分得很细碎,比如说,我直接把堆分成1024份,每一份自己维护一下别人引用自己内部对象的信息?然后当回收的时候,我就从里面挑。比如说这次回收,预计一秒内只能回收128份,那我就挑128份出来回收掉;下一次能更惨,只能回收64份,所以我就挑64份来回收。 28 | 29 | 这就是 G1 的基本思想。 30 | 31 | 这就是抓住 G1 的核心。G1 的后面的一切,都是因为分成了那么多小份,然后每一次要挑出来一部分回收。 32 | 33 | 然后我们从这一点出发,看一下 G1 的各种奇技淫巧。 34 | 35 | 首先 Region 我们已经说过了,就是为了能够保证 GC 期间灵活选择而不得不划分的。 36 | 37 | 那么 RSet(记忆集)又是拿来干啥?用来记录别的 Region 引用本 Region 内部对象的结构体。为什么要记录?不记录的话不知道这个 Region 内部的对象是不是活着。 38 | 39 | 那么怎么理解 G1 的两种模式? 40 | 41 | 我们再考虑一下,我想要挑出来一部分 Region 来回收,我是随机挑吗?当然不是,我希望尽可能回收脏的 Region。那么什么 Region 比较脏? 42 | 43 | 显然是放着年轻代对象的 Region 比较脏。因为对象朝生夕死,所以想当然的我们会说我们优先挑年轻代的 Region 就可以了。 44 | 45 | 那么问题来了,你不能一直挑年轻代,你总要挑老年代的,不然老年代岂不是永远不回收了? 46 | 47 | 所以我们会想到,启动一个后台线程,扫描这些老年代的 Region,看看脏不脏。要是很多已经很脏了,我们就把这部分老年代的 Region 回收掉。 48 | 49 | 这就是 G1 的 Young GC、Mixed GC 和全局并发标记循环的来源了。 50 | 51 | 这里面还有几个细节,我们沿着思路继续思考下去。 52 | 53 | 首先一个,并发标记循环,意味着应用也在运行,如果我在标记过程中,引用被修改了,怎么办?这就是引入了 SATB( snapshot at the beginning)。这个名字就有点让人误解,会以为 G1 真的记了一个快照,其实不是的。简单来说,可以理解为这个机制记录了在并发期间引用发生过修改的对象。在最终标记阶段,会处理这些变更。 54 | 55 | 其次,如果我要是 Mixed GC 太慢,还没来得及回收老年代也满了,怎么办?这个问题和 CMS 是一样。那么 CMS 是怎么解决的?退化为 Serial Old GC。很显然,G1 也是一样处理的。(从这个角度来说,可以理解 Serial Old 是一个后备回收器,只要你 CMS 或者 G1 崩了,那就是它顶上) 56 | 57 | 前面我们还提到,就是要挑出脏的,那么什么才是脏的,那就是要算一下,里边活着的对象还有多少。要是一个活着的对象都没了,这个 Region 是不是可以直接回收了?都不用复制存活对象了。这就是并发循环标记最后一步,把发现的一个存活对象都没了的 Region,脏得彻底的 Region 直接收回。 58 | 59 | 还有一个点,其实算是优化,而不算是本质。就是并发标记循环会复用 Young GC 的结果。在 Young GC 的初始标记完成后,一边是 Young GC 继续下去,一边是并发循环标记。 60 | 61 | 接下来我们想,每次挑出来多少个才是合适呢?之前我们已经揭露了,静态划分是不行的,因为要根据程序动态运行来决定挑多大一块内存来回收。因此我们肯定不能用参数或者直接写死,而是要实时计算。 62 | 63 | 那么怎么计算呢?这个细节,面试基本不会考。大概的原理是考察最近的几次 G1 GC 的情况,大概推断这一次 G1 至多回收多少块。有点像是根据最近几次 GC 的情况,来猜测这一次 GC 回收每一块 Region 需要多长时间,然后算出来。核心在于,根据最近几次情况来推断。 64 | 65 | G1 的面试总体上来说不如 CMS 常见。原因很简单,对于大多数应用来说,4G 的堆就足够了。在这个规模上,G1 是并不比 CMS 优秀的。而且 CMS 因为应用得多,所以懂得原理调优的人比 G1 多。 66 | 67 | ## 面试问题 68 | 69 | ### 什么是 Region? 70 | 71 | 分析:基本概念题,可以从为什么需要 Region 的角度来作为亮点。 72 | 73 | 答案:G1 将整个内存划分成了一个个块,通过这种块,可以控制每次回收的时候只回收一定数量的块,来控制整个停顿时间(这就是引入Region的目标)。 74 | 75 | 有三类 Region: 76 | 1. 年轻代 Region; 77 | 2. 老年代 Region; 78 | 3. Humongous Region,用于存放大对象(这是一个不同于 CMS 的地方。CMS 是使用老年代来存放大对象的); 79 | 80 | ![Region](https://upload-images.jianshu.io/upload_images/2579123-b5f52615c38aa31b.png?imageMogr2/auto-orient/strip|imageView2/2/w/478/format/webp) 81 | 82 | 每一个 Region 归属哪一类并不是固定不变的(这是一个很容易让人误解的地方),也就是说,在某一个时间点,一个 Region 可能是放着年轻代对象,另一个时间点,可能放着老年代对象。 83 | 84 | (我们稍微提及 Region 内部内存是如何分配的)为对象分配内存就比较简单了,Region 内部通过指针碰撞分配内存。为了减少并发,每一个线程,会从 Region 里面申请一大块内存,这块内存叫做 TLAB(thread local allocation buffer),每一个线程自己内部再分配。 85 | 86 | #### 类似问题 87 | - 年轻代的 Region 能不能给老年代用?能,在回收清空了这个 Region之后,就可以分配给老年代了 88 | - Region 有哪几类? 89 | - Region 怎么分配内存? 90 | - 什么是 TLAB?有些面试官好像会把这个东西记成 TLB(thread local buffer) 91 | 92 | ### 什么是 CSet?Collection Set 93 | 分析:基本概念题。刷亮点落在一个很容易误解的地方 94 | 答案:在每一次 GC 的时候,G1 会挑选一部分的 Region 来回收,这部分 Region 就被称为 CSet。不过要注意的是,在 Young GC的时候,是选择全部年轻代的 Region 的,G1 是通过控制所能允许的 年轻代 Region 数量来控制Young GC 的停顿时间。 95 | 96 | (后边这一点很容易让人误解,总以为是分配了一大堆年轻代 Region,然后 Young GC 只回收其中一部分,其实并不是,而是说,当 G1 觉得我只能一次性回收 50 个年轻代的 Region,那么当分配了 50 个年轻代 Region 之后,就会触发 Young GC) 97 | 98 | ### G1 的具体步骤 99 | 100 | 分析:基本考察。如果直接问步骤,那么大概率是问 MIXED GC。但是从回答的角度,要交代清楚 Young GC 和 Mixed GC。既然谈及了 Mixed GC,就要谈到并发标记循环。最后以 Mixed GC 失败,退化为Serial Old结束。 101 | 102 | 我们会把亮点放在与 CMS 横向比较上。G1 的很多步骤,都和 CMS 是类似的。通过这种比较,我们能够看到一些这一类并发 GC 在处理一些问题上的共同点。 103 | 104 | 所以接下来的回答,但凡是涉及到了和 CMS 的部分,都可以成为亮点。 105 | 106 | 答案:G1 的具体来说,可以分成 Young GC, Mixed GC 两个部分。 107 | 108 | 1. 初始标记,该步骤标记 GC root;(什么是 GC root 可以看 [GC 算法](./algorithm.md) 109 | 2. 根区间扫描阶段:该阶段简单理解,就要扫描 Young GC 之后的 Survivor Regions 的引用,和第一步骤的 GC root,合在一起作为扫描老年代 Regions 的根,这一个步骤,在 CMS 里面是没有的; 110 | 3. 并发标记阶段 111 | 4. 重新标记阶段 112 | 5. 清扫阶段:该阶段有一个很重要的地方,是会把完全没有存活对象的 Region 放回去,后边可以继续使用。清扫阶段也有一个及其短暂的 STW,而 CMS 这个步骤是完全并发的; 113 | 114 | 在标记阶段结束之后,G1 步入评估阶段,就是利用前面标记的结果,看看回收哪些 Region。G1 会根据近期的 GC 情况来判定要回收多少个 Region,在达到期望停顿时间的情况下,尽可能回收多的 Region。 115 | 116 | 而 G1 会优先挑选脏的 Region 来回收。 117 | 118 | #### 类似问题 119 | - 并发标记循环步骤 120 | - Mixed GC 步骤 121 | 122 | ### G1 什么时候会触发 Full GC 123 | 分析:其实和 CMS 类似,核心都是在老年代尝试分配内存的时候,找不到足够的空间,就会退化为 Full GC。那么问题来了,什么时候会尝试分配对象到老年代?—— 年轻代对象晋升。这和 CMS 又不同,CMS 中还有可能是大对象直接分配到老年代。那么 G1 的大对象分配到哪里?分配到了 Huge Regions。那么万一 G1 也没有 Region 来容纳这个大对象,会不会也开始 Full GC?答案是会的。所以总结下来就是两个: 124 | 1. 分配对象到老年代的时候,老年代没有足够的内存。这基本上就是对象晋升失败; 125 | 2. 分配大对象失败; 126 | 127 | 答:主要是两个时机: 128 | 1. 对象晋升到老年代的时候,没有足够的空间来容纳,也就是并发模式失败,要进行 Full GC 129 | 2. 分配大对象的时候,没有足够的空间来容纳,也会触发 Full GC 130 | 131 | (尝试回答如何解决 Full GC,作为一个亮点)对于前者来说,要避免这种情况, 就是要确保 Mixed GC 执行得足够快足够频繁。因此可以通过调整堆大小,提前启动 Mixed GC,或者调整并发线程数来加快 Mixed GC。至于后者,则没什么好办法,只能是加大堆,或者加大 Region 大小来避免。 132 | 133 | (总结和 CMS 的相同点)基本上,G1 触发 Full GC 和 CMS 触发 Full GC 是类似的,核心都在于并发模式失败,老年代找不到空间。所不同的是 G1 有专门的存放大对象的 Region,所以这一点会稍微有点不同。 134 | 135 | ### CMS 和 G1 的区别 136 | 137 | 分析:这个问题就很宽泛,可以从多个角度去回答。 138 | 1. 从两者内存管理的角度去回答 139 | 2. 从适用场景去回答 140 | 3. 回收模式 141 | 142 | 也可以聊具体步骤上的差异。但是一般来说问这种区别,更加多是希望讨论一些特征上的差异。步骤上的差异虽然也算是差异,不过可能不太符合期望而已。 143 | 144 | 答案:CMS 和 G1 都是并发垃圾回收器,但是它们在内存管理,适用场景上都有很大的不同。 145 | 1. CMS 的内存整体上是分代的,使用了空闲链表来管理空闲内存;而 G1 也用了分代,但是内存还被划分成了一个个 Region,在 Region 内部,使用了指针碰撞来分配内存; 146 | 2. 在适用场景下,G1 在大堆的表现下比 CMS 更好。G1 采用的是启发式算法,能够有效控制 GC 的停顿时间; 147 | 3. 回收模式上,G1 有 Young GC 和 Mixed GC。Mixed GC 是同时回收老年代和年轻代; 148 | 149 | #### 类似问题 150 | 151 | - 为什么 G1 会比 CMS 更适合大堆?启发式算法能够比较准确控制停顿时间 152 | 153 | ### 在并发标记期间,G1 是怎么处理这阶段发生变化的引用? 154 | 155 | 分析:考察并发的固有问题,就是如果这个过程应用的引用发生了变化,G1 是如何处理的。在 CMS 里面,我们说了,CMS 是用卡表,也就是卡表 + 预清理 + 重标记来完成的,核心是利用写屏障来重新把卡表标记为脏,在预清理和重标记阶段重新处理脏卡。 156 | 157 | G1 里面则不同,它用的是 SATB,但是也利用了写屏障。它的处理机制,可以归结为,当引用变更的时候,会把这个变更作为一条 log 写到一个 buffer 里面。在重标记阶段重新这些 log。 158 | 159 | 这个机制,亮点可以横向对比 CMS,也有一个比较出其不意的角度,就是横向对比 Redis 的 BG save。基本上都是一样的。 160 | 161 | 先是产生一个快照,然后再把并发期间的修改丢到日志里面,在最后重新处理一下日志。 162 | 163 | 答案:G1 采用了所谓的 SATB。G1 利用写屏障,会把并发标记期间被修改的引用都记录到一个 log buffer 里面,再重标记阶段重新处理这个 log。 164 | 165 | (和 CMS 对比)这个机制和 CMS 是比较像的,差别在于 CMS 是把内存对应的卡表标记为脏,并且引入预清理阶段,在预清理和重标记阶段处理这些脏卡。 166 | 167 | (和 Redis BG save 对比,抽象一下) 这种 snapshot + change log 的机制,在别的场景下也能看到。比如说在 Redis 的 BG Save 里面,Redis 在子进程处理快照的时候,主进程也会记录这期间的变更,存放在一个日志里面。后面再统一处理这些日志。 168 | 169 | -------------------------------------------------------------------------------- /gc/img/cms_gc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/gc/img/cms_gc.png -------------------------------------------------------------------------------- /gc/img/gc_roots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/gc/img/gc_roots.png -------------------------------------------------------------------------------- /golang/GMP.md: -------------------------------------------------------------------------------- 1 | # GMP 2 | 3 | 4 | 具体流程 5 | ![](img/GMP.png) 6 | P-逻辑处理器: 7 | 8 | G-Goroutine 9 | 10 | M-thread线程:是执行P中的实体 11 | 12 | 1. **全局队列**(Global Queue):存放等待运行的G,其来源主要有从系统调用中恢复的G 13 | 2. **P的本地队列**:同全局队列类似,存放的也是等待运行的G,存的数量有限,不超过256个。新建G’时,G’优先加入到P的本地队列,如果队列满了,则会把本地队列中一半的G移动到全局队列。 14 | 3. **P列表**:所有的P都在程序启动时创建,并保存在数组中,最多有`GOMAXPROCS`(可配置)个。 15 | 4. **M**:线程想运行任务就得获取P,从P的本地队列获取G,P队列为空时,M也会尝试从全局队列**拿**一批G放到P的本地队列,或从其他P的本地队列**偷**一半放到自己P的本地队列。M运行G,G执行之后,M会从P获取下一个G,不断重复下去。 16 | 17 | 18 | ## Goroutine调度 19 | 1. 队列轮转: 20 | 1. 不考虑G进入系统调用或IO操作的情况下,P周期性的将G调度到M中执行,执行一小段时间,将上下文保存下来,然后将G放到队列尾部,然后从队列中重新取出一个G进行调度。每个P会周期性地查看全局队列中是否有G待运行并将其调度到M中执行 21 | 2. 系统调用: 22 | 1. M0运行的某G0进入系统调用,M0释放P,进而某个空闲的M1获取P,继续执行P队列中剩下的G。而M0由于陷入系统调用而进被阻塞,M1接替M0的工作,只要P不空闲,就可以保证充分利用CPU 23 | 24 | 3. 工作量窃取 25 | 1. 多个P中维护的G队列有可能是不均衡的。此时空闲P将其他P中G偷取一部分过来 26 | 27 | ## 高效的策略 28 | 29 | - M是可以复用的,不需要反复创建与销毁,当没有可执行的Goroutine时候就处于自旋状态,等待唤醒 30 | - 内存分配状态(mcache)位于P,G可以跨M调度,不再存在跨M调度局部性差的问题 31 | - M从关联的P中获取G,不需要使用锁,是lock free的 -------------------------------------------------------------------------------- /golang/ReadMe.md: -------------------------------------------------------------------------------- 1 | # GO 2 | 3 | ## 竞态 4 | 5 | 同一块内存同时被多个 goroutine 访问。我们使用 go build、go run、go test 命令时,添加 -race 标识可以检查代码中是否存在资源竞争 6 | 7 | 解决方法: 8 | ```go 9 | sync.Mutex 10 | sync.RWMutex 11 | ``` 12 | 13 | ## 值接收者和指针接收者的区别? 14 | 1. 方法的接收者: 15 | 16 | - 值类型,既可以调用值接收者的方法,也可以调用指针接收者的方法; 17 | 18 | - 指针类型,既可以调用指针接收者的方法,也可以调用值接收者的方法。 19 | 20 | 2. 接口的接收者 21 | 22 | - 以值类型接收者实现接口,类型本身和该类型的指针类型,都实现了该接口; 23 | 24 | - 以指针类型接收者实现接口,只有对应的指针类型才被认为实现了接口。 25 | 26 | 27 | 3. 通常我们使用指针作为方法的接收者的理由: 28 | 29 | 1. 使用指针方法能够修改接收者指向的值。 30 | 31 | 2. 可以避免在每次调用方法时复制该值,在值的类型为大型结构体时,这样做会更加高效。 32 | 33 | 34 | ## Go有什么优势或特点 35 | - Go 允许跨平台编译,编译出来的是二进制的可执行文件,直接部署在对应系统上即可运行。 36 | - Go 在语言层次上天生支持高并发,通过 goroutine 和 channel 实现。channel 的理论依据是 CSP 并发模型, 即所谓的通过通信来共享内存;Go 在 runtime 运行时里实现了属于自己的调度机制:GMP,降低了内核态和用户态的切换成本。 37 | - Go 的代码风格是强制性的统一,如果没有按照规定来,会编译不通过。 38 | 39 | ## Go的内存分配 40 | 41 | 内存池 + 多级对象管理。内存池主要是预先分配内存,减少向系统申请的频率;多级对象有:mheap、mspan、arenas、mcentral、mcache。它们以 mspan 作为基本分配单位 42 | 43 | - 当要分配大于 32K 的对象时,从 mheap 分配。 44 | - 当要分配的对象小于等于 32K 大于 16B 时,从 P 上的 mcache 分配,如果 mcache 没有内存,则从 mcentral 获取,如果 mcentral 也没有,则向 mheap 申请,如果 mheap 也没有,则从操作系统申请内存。 45 | - 当要分配的对象小于等于 16B 时,从 mcache 上的微型分配器上分配。 46 | 47 | ## defer、panic、recover用法 48 | defer 函数调用的顺序是后进先出,当产生 panic 的时候,会先执行 panic 前面的 defer 函数后才真的抛出异常。一般的,recover 会在 defer 函数里执行并捕获异常,防止程序崩溃。 49 | 50 | ## Go中对nil的Slice和空Slice的处理是一致的吗 51 | 52 | JSON 标准库对 nil slice 和 空 slice 的处理是不一致 53 | - slice := make([]int,0):slice不为nil,但是slice没有值,slice的底层的空间是空的 54 | - slice := []int{} :slice的值是nil,可用于需要返回slice的函数,当函数出现异常的时候,保证函数依然会有nil的返回值 55 | 56 | ## Go主协程如何等其余协程完再操作? 57 | 58 | 使用sync.WaitGroup。WaitGroup,就是用来等待一组操作完成的。 59 | 60 | WaitGroup内部实现了一个计数器,用来记录未完成的操作个数。 61 | - Add()用来添加计数; 62 | - Done()用来在操作结束时调用,使计数减一; 63 | - Wait()用来等待所有的操作结束,即计数变为0,该函数会在计数不为0时等待,在计数为0时立即返回。 64 | 65 | ## Go中为什么内存泄漏 66 | 67 | 如果一个程序持续不断地产生新的 goroutine,且不结束已经创建的 goroutine 并复用这部分内存,就会造成内存泄漏的现象 68 | 69 | 解决: 70 | 71 | Go自带的工具pprof或者使用Gops去检测诊断当前在系统上运行的Go进程的占用的资源 72 | 73 | 74 | ## Go中rune类型 75 | 76 | 77 | uint8 类型,或者叫 byte 型,代表了 ASCII 码的一个字符。 78 | 79 | 80 | rune 类型,代表一个 UTF-8 字符,当需要处理中文、日文或者其他复合字符时,则需要用到 rune 类型。rune 类型等价于 int32 类型。 81 | 82 | 83 | ## GO的空结构 84 | 85 | - 不占据任何内存空间 86 | - 作用 87 | - map 作为集合(Set)使用时,可以将值类型定义为空结构体,仅作为占位符使用 88 | - 不需要发送任何的数据的channel ,只用来通知子协程(goroutine)执行任务,或只用来控制协程并发度 89 | 90 | 91 | ## defer 92 | 93 | - 延迟函数的参数在defer语句出现时就已经确定 94 | - 延迟函数执行按 「先进后出」 顺序执行,即先出现的 defer 最后执行 95 | - 延迟函数可能操作主函数的具名返回值 96 | 97 | ## select 98 | 99 | GO 语言中用来提供 IO 复用的机制,它可以检测多个 chan 是否 ready(可读/可写) 100 | 101 | - select 语句中除 default 外,每个 case 操作一个channel,要么读要么写 102 | - select语句中除 default 外,各 case 执行顺序是随机的 103 | - select 语句中如果没有 default 语句,则会阻塞等待任一 case 104 | - select 语句中读操作要判断是否成功读取,关闭的 channel 也可以读取 105 | 106 | 107 | ## Go中的分布式锁 108 | 109 | 多个进程,操作同一份资源,此时需要分布式锁 110 | 111 | 要解决的问题: 112 | 113 | 1. 对共享资源操作时候,首先需要加锁,在加锁时候,抢到锁的进程可以直接返回,进而操作共享资源,而没有抢到锁的进程需要等待锁的释放,对于同一个锁,同一时刻只能有一个进程来持有 114 | 2. 假如抢到锁的进程突然宕机,需要能够有释放锁的机制,避免后面的进程一直阻塞导致死锁。提供锁的组件也应该具备高可用性,在某个节点宕机后能够继续提供服务 115 | 3. 对资源的操作结束之后,需要及时释放锁,但是不能释放其他进程的锁,后面没有抢到锁的进程可以获得锁。如果抢锁的进程过多,可能会导致惊群效应,提供锁的组件应在一定程度上避免该现象 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /golang/context.md: -------------------------------------------------------------------------------- 1 | # Context 2 | 3 | ## 什么是go的context 4 | 5 | context 主要用来在 goroutine 之间传递上下文信息,包括:取消信号、超时时间、截止时间、k-v 等,主要用于**超时控制**和**多Goroutine间的数据传递** 6 | 7 | 8 | ## context使用场景 9 | 10 | **HTTP/RPC Server** 11 | 12 | ![](img/context.png) 13 | Go 的 server 里,通常每来一个请求都会启动若干个 goroutine 同时工作:有些去数据库拿数据,有些调用下游接口获取相关数据 14 | 15 | 而客户端一般不会无限制的等待,都会被请求设定超时时间,比如100ms。 16 | 17 | 比如这里GoroutineA消耗80ms,GoroutineB3消耗30ms,已经超时了,那么后续的GoroutineCDEF都没必要执行了,客户端已经超时返回了,服务端就算计算出结果也没有任何意义了。 18 | 19 | 所以这里就可以使用 Context 来在多个 Goroutine 之间进行超时信号传递 20 | 21 | 引入超时好处: 22 | 23 | 1. 客户端可以快速返回,提升用户体验 24 | 2. 服务端减少无效计算 25 | 26 | 27 | ## 注意事项 28 | - context 的 Done() 方法往往需要配合 select {} 使用,以监听退出。 29 | - 尽量通过函数参数来暴露 context,不要在自定义结构体里包含它。 30 | - 直接将 Context 类型作为函数的第一参数,而且一般都命名为 ctx 31 | - context 存储的应该是一些共同的数据。例如:登陆的 session、cookie 等 32 | - WithValue 类型的 context 应该尽量存储一些全局的 data,而不要存储一些可有可无的局部 data。 33 | 34 | - 一旦 context 执行取消动作,所有派生的 context 都会触发取消。 -------------------------------------------------------------------------------- /golang/escape_analysis.md: -------------------------------------------------------------------------------- 1 | # Go逃逸现象 2 | 3 | 局部对象不论是否动态new出来,编译器会做**逃逸分析(escape analysis)**,**当发现变量的作用域没有跑出函数范围,就可以在栈上,反之则必须分配在堆**。 4 | 5 | 逃逸场景: 6 | 7 | - 指针逃逸 8 | - 栈空间不足逃逸 9 | - 动态类型逃逸 10 | - 闭包引用对象逃逸 -------------------------------------------------------------------------------- /golang/go_channel.md: -------------------------------------------------------------------------------- 1 | # GoChannel 2 | 3 | ### chan的内部存储 4 | 1. 环形队列,存储数据 5 | 2. 记录发送数据位置 6 | 3. 记录接收数据位置 7 | 4. 锁,实现Goroutine safe 8 | 9 | 创建chan时,**默认分在堆上**,make返回的只是一个指向该对象指针 10 | 11 | 12 | ### 调度 13 | 14 | 使用 sendq 和 recvq来暂存由于发送或接收而被阻塞的goroutine。 15 | 16 | send/recv的时候都会判断recvq/sendq是否有goroutine正在等待,有则优先处理。 17 | 18 | 19 | ### 发送、接收 20 | 21 | 发送过程: 22 | - 先判断是否有等待接收数据的groutine,如果有,直接将数据发给Groutine,唤醒groutine,就不放入队列中了。 23 | - 队列如果满了,那就只能放到队列中等待,直到有数据被取走才能发送 24 | - 省去了两次内存拷贝和加锁的开销 25 | 26 | 接收过程: 27 | - 从channel读取数据时,不是直接去环形队列中取数据,而是先判断是否有等待发送数据的groutine。如果有,直接将groutine出队列,取出数据返回,并唤醒groutine。如果没有等待发送数据的groutine,再从环形队列中取数据 28 | 29 | ### CSP模型 30 | CSP 模型是“以通信的方式来共享内存”,不同于传统的多线程通过共享内存来通信。用于描述两个独立的并发实体通过共享的通讯 channel (管道)进行通信的并发模型 31 | 32 | ### 从已关闭的channel发送/读取数据,结果如何 33 | - 给一个已经关闭的 channel 发送数据,引起 panic 34 | - 从一个已经关闭的 channel 接收数据,如果缓冲区中为空,则返回一个零值 35 | 36 | ### 无缓冲的 channel 和 有缓冲的 channel 的区别? 37 | - 对于无缓冲的 channel,发送方将阻塞该信道,直到接收方从该信道接收到数据为止,而接收方也将阻塞该信道,直到发送方将数据发送到该信道中为止。 38 | 39 | - 对于有缓存的 channel,发送方在没有空插槽(缓冲区使用完)的情况下阻塞,而接收方在信道为空的情况下阻塞。 40 | 41 | 42 | 43 | ### 协程泄漏 44 | 协程泄露是指协程创建后,长时间得不到释放,并且还在不断地创建新的协程,最终导致内存耗尽,程序崩溃。常见的导致协程泄露的场景有以下几种: 45 | - 缺少接收器,导致发送阻塞 46 | - 缺少发送器,导致接收阻塞 47 | - 死锁(dead lock) 48 | - 无限循环(infinite loops) 49 | - 为了避免网络等问题,采用了无限重试的方式,发送 HTTP 请求,直到获取到数据。那如果 HTTP 服务宕机,永远不可达,导致协程不能退出,发生泄漏 -------------------------------------------------------------------------------- /golang/goroutine.md: -------------------------------------------------------------------------------- 1 | # goroutine —— 协程 2 | 3 | ## 面试题 4 | 5 | ### 进程、线程和协程的不同? 6 | 7 | 分析:这种很明显的是一种逐步演化的路径。也就是`进程-线程-协程`总体而言可以看做是一种演化路线。 8 | 9 | 审视这个演化,就会发现它是朝着更轻量的方向演进的。于是结合需求和计算机的发展,就会发现:业务越来越复杂,计算机越来越强大,但是我们需要的确是越来越细粒度的资源分配。 10 | 11 | 进程演进到线程,共享了内存,但是线程可以被CPU单独调度;线程到协程,内存使用量更少了,多个协程绑定到一个线程,相当于大家平分了这个线程的 CPU。 12 | 13 | 以上这一段吹牛,如果面试记得,可以跟面试官聊。不记得就算了。 14 | 15 | **答案**:(首先是标准答案) 16 | 1. 进程是资源分配的最小单位,而线程是 CPU 调度的单位,一个进程可以有多个线程。因为同一个进程内的线程共享了堆内存,所以在经常会引起并发编程问题; 17 | 2. 协程比线程更轻量级。线程的创建和销毁、调度还需要陷入到内核中,而协程可以认为完全是依赖于用户空间创建、销毁和调度的。同时协程相比线程,占据的资源更加小。 18 | 19 | (其次,开始引申)目前来说,很多语言都开始尝试支持协程,主要是因为现在的很多业务都是短平快,或者是 IO 密集的,相比之下,线程也过于重了。 20 | 21 | 最有名的就是`goroutine`(实际上也不是最有名吧,不过都面试 go 了,就说最有名了),此外还有`kotlin`协程,`python`的协程。 22 | 23 | ### 类似问题 24 | - 为什么要引入协程?看后面**为什么引入协程**,其实这两个问题基本上一样; 25 | 26 | 27 | ### 为什么要引入协程? 28 | 29 | 分析:`goroutine` 的引入,本质上还是为了规避一个问题:我又想有并发,但是我又不想陷入到内核里面去。于是就有了这个 `goroutine` 的东西。 30 | 31 | 该面试题的核心,就是协程“轻量”。这种轻量体现在两方面: 32 | 1. 所需要的资源更少 33 | 2. 创建销毁和调度更轻量,并不需要陷入内核 34 | 35 | 36 | 37 | 答案:因为我们需要一个更轻量的东西来取代线程。(开始聊自己理解的起源)当前绝大多数系统处理的任务都是非常短平快的,或者是 IO 密集这种频繁触发上下文切换的任务。这导致我们如果使用线程,就不得不面临线程频繁切换,陷入内核的问题——这是一个极大的开销。 38 | 39 | 因此我们需要一个比线程更加轻量的东西。这个东西要具备两个特征: 40 | 1. 占有的资源小——我们都是小任务,不需要那么多资源; 41 | 2. 创建销毁和调度消耗少——小任务,还时常阻塞,所以调度一定要快要轻; 42 | 43 | 结合在一起就是`goroutine`了。 44 | 45 | #### 类似问题 46 | - 为什么有了线程池还是要有`goroutine`:线程池只是减轻了创建和销毁的开销,但是线程本身还是占有很多资源,上下文调度依然很重 47 | 48 | ### 怎么避免`goroutine`泄露? 49 | 50 | 分析:如果你不知道`goroutine`什么时候会结束,就不要使用`goroutine`。这是核心原则。讲完这个原则之后,可以讲一些如何做到“知道goroutine”何时结束。 51 | 52 | 53 | 然后刷两点可以回答”如何发现`goroutine`泄露“。 54 | 55 | 答案:避免`goroutine`泄露的核心原则是"Never start a goroutine without knowning when it will stop"(用英文会显得你比较专业,记不住可以替换为对应的中文)。 56 | 57 | 归根结底,就是要有办法控制住结束掉自己开启的`goroutine`。大体上有两类做法: 58 | 1. 超时控制,主要利用`context.Timeout`的特性; 59 | 2. 主动发信号给`goroutine`关闭。一般是要利用到`channel`的特性; 60 | 61 | 两种做法基本都要配合`select`特性来。要么是业务正常结束,退出`goroutine`,要么是超时,或者收到关闭信号,异常退出。 62 | 63 | (开始讨论如何发现`goroutine`泄露)如果`goroutine`都不是自己开启的,那肯定是没得办法了。只能通过`runtime.NumGoroutine()`方法监控`goroutine`的数量来判断有没有泄露。如果`goroutine`一直在上涨,而且数量也很多,说明泄露很严重。而如果只是轻微泄露,比如说一万个`goroutine`里面泄露了十个,是很难看出来的。 64 | 65 | (后面可以进一步引申,跳到**`goroutine`泄露的典型场景**) 66 | (这个问题也可以针对`goroutine`泄露的典型场景来回答,比如说小心使用`channel`,正确使用`mutex`,防止业务一直阻塞等,不过略等于啥也没说) 67 | 68 | #### 类似问题 69 | - 如何发现`goroutine`泄露了 70 | 71 | ### `goroutine`泄露的典型场景 72 | 73 | 分析:这个问题答案来自煎鱼大佬的文章[跟读者聊 Goroutine 泄露的 N 种方法,真刺激!](https://blog.csdn.net/EDDYCJY/article/details/115535237) 74 | 75 | PS:煎鱼大佬的文章都很浅显易懂,即便是难题也能说得很容易理解,大家可以多读读,他有一个公众号《脑子进煎鱼了》,可以关注。 76 | 77 | 记住,至少背下来里面的一个例子,防着面试官让你手写一个`goroutine`泄露的例子。 78 | 79 | 然后面试官让你看一段代码,如果有锁,就要怀疑死锁;如果有`channel`就要怀疑`goroutine`泄露; 80 | 81 | 其实`channel`的代码坑极多,在`channel`里面进一步讨论。 82 | 83 | 答:有: 84 | - `channel`发送不接收 85 | - `channel`接收不发送 86 | - `nil channel` 87 | - 慢等待 88 | - 互斥锁忘记解锁 89 | - 同步锁使用不当 90 | 91 | (同样聊排查作为亮点)排查主要用`runtime.NumGoroutine`或者`pprof`工具。`pprof`会返回所有带有堆栈跟踪的`goroutine`列表。 92 | 93 | #### 类似问题 94 | - 可以写一个 `goroutine`泄露的例子吗? 95 | - 或者面试官给你一段代码,让你看有什么问题 96 | 97 | ## 单核CPU,开两个Goroutine,其中一个死循环,会如何? 98 | 99 | Goroutine 里跑着死循环,也就是时时刻刻在运行着 “业务逻辑”。这块需要与单核 CPU 关联起来,考虑是否会一直阻塞住,把整个 Go 进程运行给 hang 住了? 100 | 101 | 在不同的 Go 语言版本中,结果可能会是不一样的。 102 | 103 | 1.14版本之前,没有输出。 104 | 105 | 没有涉及执行权变更的行为,子协程一直循环中,此时主协程无法被调度,整个程序阻塞在子协程中 106 | 107 | 1.14版本后,有输出 108 | 109 | 引入基于信号的抢占式调度,能够处理以下情况 110 | - 抢占阻塞在系统调用上的 P 111 | - 抢占运行时间过长的 G 112 | 113 | 一旦检测到以上场景之一,发信号给M,M收到信号后休眠当前阻塞的Goroutine,调用绑定信号方法,重新调度 114 | 115 | ## Reference 116 | [跟读者聊 Goroutine 泄露的 N 种方法,真刺激!](https://blog.csdn.net/EDDYCJY/article/details/115535237) 117 | -------------------------------------------------------------------------------- /golang/go协程交互/chan实现互斥锁.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | var num int 9 | 10 | func add(h chan int, wg *sync.WaitGroup) { 11 | defer wg.Done() 12 | 13 | h <- 1 14 | num += 1 15 | <-h 16 | 17 | } 18 | 19 | func main() { 20 | ch := make(chan int, 1) 21 | var wg sync.WaitGroup 22 | 23 | for i := 0; i < 100; i++ { 24 | wg.Add(1) 25 | go add(ch, &wg) 26 | } 27 | wg.Wait() 28 | 29 | fmt.Println(num) 30 | } 31 | -------------------------------------------------------------------------------- /golang/go协程交互/mutex实现互斥锁.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | var num int 9 | var wg sync.WaitGroup 10 | var mtx sync.Mutex 11 | 12 | func add() { 13 | mtx.Lock() 14 | 15 | defer mtx.Unlock() 16 | defer wg.Done() 17 | 18 | num += 1 19 | } 20 | 21 | func main() { 22 | for i := 0; i < 100; i++ { 23 | wg.Add(1) 24 | go add() 25 | } 26 | 27 | wg.Wait() 28 | 29 | fmt.Println(num) 30 | } 31 | -------------------------------------------------------------------------------- /golang/go协程交互/交叉打印100个abc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | var wg sync.WaitGroup 9 | 10 | func main(){ 11 | Achan := make(chan int,1) 12 | Bchan := make(chan int) 13 | Cchan := make(chan int) 14 | 15 | wg.Add(3) 16 | 17 | go A(Achan,Bchan) 18 | go B(Bchan,Cchan) 19 | go C(Cchan,Achan) 20 | 21 | wg.Wait() 22 | } 23 | 24 | func A(Achan chan int,Bchan chan int){ 25 | def wg.Done() 26 | 27 | for i:=0;i<100;i++{ 28 | Achan <- 1 29 | fmt.Println("a") 30 | <- Bchan 31 | } 32 | return 33 | } 34 | 35 | 36 | 37 | func B(Bchan chan int,Cchan chan int){ 38 | def wg.Done() 39 | 40 | for i:=0;i<100;i++{ 41 | Bchan <- 1 42 | fmt.Println("b") 43 | <- Cchan 44 | } 45 | return 46 | } 47 | 48 | func C(Cchan chan int,Achan chan int){ 49 | def wg.Done() 50 | 51 | for i:=0;i<100;i++{ 52 | Cchan <- 1 53 | fmt.Println("c") 54 | <- Achan 55 | } 56 | return 57 | } -------------------------------------------------------------------------------- /golang/go协程交互/交替打印奇数与偶数.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | func main() { 9 | ch := make(chan bool) 10 | var wg sync.WaitGroup 11 | 12 | wg.Add(2) 13 | 14 | go func() { 15 | defer wg.Done() 16 | for i := 1; i <= 100; i++ { 17 | ch <- true 18 | if i%2 == 1 { 19 | fmt.Printf("routine 1: %d\n", i) 20 | } 21 | } 22 | }() 23 | 24 | go func() { 25 | defer wg.Done() 26 | for i := 1; i < 101; i++ { 27 | <-ch 28 | if i%2 == 0 { 29 | fmt.Printf("routine 2: %d\n", i) 30 | } 31 | 32 | } 33 | }() 34 | wg.Wait() 35 | } 36 | -------------------------------------------------------------------------------- /golang/go协程交互/交替打印数字和字母.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "unicode/utf8" 7 | ) 8 | 9 | func main() { 10 | number, letter := make(chan bool), make(chan bool) 11 | 12 | wg := sync.WaitGroup{} 13 | 14 | go func() { 15 | i := 1 16 | for { 17 | <-number //从number获取数字消息 18 | fmt.Printf("%d%d", i, i+1) 19 | i += 2 20 | letter <- true 21 | } 22 | }() 23 | 24 | wg.Add(1) 25 | 26 | go func(wg *sync.WaitGroup) { 27 | str := "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 28 | i := 0 29 | for { 30 | <-letter //接收管道消息 31 | if i >= utf8.RuneCountInString(str) { //走到字符串结尾 32 | wg.Done() 33 | return 34 | } 35 | fmt.Print(str[i : i+2]) 36 | i += 2 37 | number <- true 38 | } 39 | }(&wg) 40 | 41 | number <- true 42 | wg.Wait() 43 | } 44 | -------------------------------------------------------------------------------- /golang/img/GMP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/golang/img/GMP.png -------------------------------------------------------------------------------- /golang/img/context.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/golang/img/context.png -------------------------------------------------------------------------------- /golang/map.md: -------------------------------------------------------------------------------- 1 | # MAP 2 | 3 | 使用哈希表作为底层实现,一个哈希表里可以有多个哈希表节点,也即bucket,而每个bucket就保存了map中的一个或一组键值对 4 | 5 | ## 如何实现顺序读取 6 | 7 | Go中map如果要实现顺序读取的话,可以先把map中的key,通过sort包排序 8 | ## 哈希冲突 9 | 10 | 当有两个或以上数量的键被哈希到了同一个bucket时,我们称这些键发生了冲突。Go使用链地址法来解决键冲突 11 | 12 | 由于每个bucket可以存放8个键值对,所以同一个bucket存放超过8个键值对时就会再创建一个键值对,用类似链表的方式将bucket连接起来。 13 | 14 | - 为什么是8个:bucket的数据结构中,仅能存储哈希值的高8位 15 | 16 | ## 哈希扩容 17 | 18 | 扩容条件: 19 | 20 | 1. 负载因子>6.5 21 | 22 | ```go 23 | 负载因子 = 键数量/bucket数量 24 | ``` 25 | 26 | 2. overflow数量 > `2^15` 27 | 28 | bucket数据结构指示下一个bucket的指针称为overflow bucket,意为当前bucket盛不下而溢出的部分 29 | 30 | ### 增量扩容 31 | 32 | 当负载因子过大时,就新建一个bucket,新的bucket长度是原来的2倍,然后旧bucket数据搬迁到新的bucket。 33 | 34 | 考虑到如果map存储了数以亿计的key-value,一次性搬迁将会造成比较大的延时,Go采用逐步搬迁策略,即每次访问map时都会触发一次搬迁,每次搬迁2个键值对 35 | 36 | ### 等量扩容 37 | 38 | buckets数量不变,重新做一遍类似增量扩容的搬迁动作,把松散的键值对重新排列一次,以使bucket的使用率更高,进而保证更快的存取。 39 | 40 | 41 | ### 为什么并发不安全 42 | map 在扩缩容时,需要进行数据迁移,迁移的过程并没有采用锁机制防止并发操作,而是会对某个标识位标记为 1,表示此时正在迁移数据。如果有其他 goroutine 对 map 也进行写操作,当它检测到标识位为 1 时,将会直接 panic 43 | 44 | 45 | 并发安全则使用`sync.map` 46 | 47 | 如何实现并发安全: 48 | 49 | sync.map底层数据结构: 50 | ```go 51 | type Map struct { 52 | mu Mutex //互斥锁 53 | read atomic.Value // readOnly 54 | dirty map[interface{}]*entry //读写数据,原生map,需要加锁保证数据安全 55 | misses int //多少次读取read未命中 56 | } 57 | ``` 58 | 59 | 当读数据: 60 | 1. 先查看 read 中是否包含所需的元素 61 | 1. 有,则通过 atomic 原子操作读取数据并返回 62 | 2. 无,则会判断 read.readOnly 中的 amended 属性,他会告诉程序 dirty 是否包含 read.readOnly.m 中没有的数据。如果true,从dirty读取 63 | 64 | 65 | 因此读操作性能高 66 | 67 | 当写入: 68 | 1. 查 read:存在且未被标记删除状态,尝试存储;read 上没有,或者已标记删除状态,紧接下面 69 | 2. 上互斥锁 70 | 3. 操作dirty -------------------------------------------------------------------------------- /golang/mutex.md: -------------------------------------------------------------------------------- 1 | # Mutex 2 | 3 | ## 面试题 4 | 5 | ### `mutex`是如何加锁的 6 | 7 | 分析:也是考察`mutex`的实现原理。基本上就是围绕`自旋-FIFO`来说的。简单理解就是,`mutex`先尝试自旋,自旋不行就所有`goroutine`步入`FIFO`,先到先得。 8 | 9 | 加锁的这个步骤,讲得非常详细。 10 | 11 | 答案:`mutex`加锁大概分成两种模式: 12 | 1. 在正常模式下,`goroutine`通过自旋来获得锁; 13 | 2. 但是如果存在一个`goroutine`等待锁超过了`1ms`,那么`mutex`就会进入饥饿模式,在饥饿模式下,互斥锁会直接交给等待队列最前面的 Goroutine。也就是从等待队列里面唤醒第一个等待者。新的 Goroutine 在该状态下不能获取锁、也不会进入自旋状态,它们只会在队列的末尾等待。如果一个 Goroutine 获得了互斥锁并且它在队列的末尾或者它等待的时间少于 1ms,那么当前的互斥锁就会切换回正常模式 14 | 15 | (讨论一下公平性的问题)所以从严格意义上来说,它并不是公平锁,因为在正常状态下,一个新的请求锁的`goroutine`和等待的`goroutine`一起竞争锁。而严格意义的公平应该是永远遵循 `FIFO`。 16 | 17 | 18 | 1. 判断当前 Goroutine 能否进入自旋; 19 | 20 | 自旋是一种多线程同步机制,当前的进程在进入自旋的过程中会一直保持 CPU 的占用,持续检查某个条件是否为真。在多核的 CPU 上,自旋可以避免 Goroutine 的切换,使用恰当会对性能带来很大的增益,但是使用的不恰当就会拖慢整个程序,所以 Goroutine 进入自旋的条件非常苛刻: 21 | 22 | 1)互斥锁只有在普通模式才能进入自旋; 23 | 24 | 2)runtime.sync_runtime_canSpin需要返回 true 25 | 26 | - 运行在多 CPU 的机器上; 27 | 28 | - 当前 Goroutine 为了获取该锁进入自旋的次数小于四次; 29 | 30 | - 当前机器上至少存在一个正在运行的处理器 P 并且处理的运行队列为空 31 | 32 | 2. 通过自旋等待互斥锁的释放 33 | 34 | 一旦当前 Goroutine 能够进入自旋就会调用runtime.sync_runtime_doSpin和runtime.procyield执行 30 次的 PAUSE 指令,该指令只会占用 CPU 并消耗 CPU 时间 35 | 36 | 3. 计算互斥锁的最新状态 37 | 38 | 处理了自旋相关的特殊逻辑之后,互斥锁会根据上下文计算当前互斥锁最新的状态 39 | 40 | 4. 更新互斥锁的状态并获取锁 41 | 42 | 计算了新的互斥锁状态之后,会使用 CAS 函数更新状态 43 | 44 | 如果没有通过 CAS 获得锁,会调用 runtime.sync_runtime_SemacquireMutex 45 | 46 | runtime.sync_runtime_SemacquireMutex 会在方法中不断尝试获取锁并陷入休眠等待信号量的释放,一旦当前 Goroutine 可以获取信号量,它就会立刻返回,sync.Mutex.Lock的剩余代码也会继续执行 47 | #### 类似问题 48 | - 什么是饥饿状态 49 | - 是不是公平锁 50 | 51 | 52 | ## Reference 53 | [mutex is more fair](https://news.ycombinator.com/item?id=15096463) 54 | [mutex fairness]() 55 | [这可能是最容易理解的 Go Mutex 源码剖析](https://segmentfault.com/a/1190000039855697) 56 | -------------------------------------------------------------------------------- /golang/new与make区别.md: -------------------------------------------------------------------------------- 1 | ## 不同 2 | 3 | new只接受一个参数类型,分配好内存后,返回一个指向该类型内存地址的指针。同时把**分配的内存置为零**,也就是类型的零值 4 | 5 | make仅用于chan、map、slice的内存分配,且它返回的类型就是这三个类型本身,而不是他们的指针类型 6 | 7 | ## 相同 8 | 9 | 堆空间分配 -------------------------------------------------------------------------------- /golang/range.md: -------------------------------------------------------------------------------- 1 | # Range 2 | 3 | range 是 Go 语言用来遍历的一种方式,它可以操作数组、切片、map、channel 等 4 | 5 | ## 实现原理 6 | ### range for slice 7 | 遍历 slice 前会先获取 slice 的长度 len_temp 来作为循环次数 8 | 9 | 每次循环会先获取元素值,如果接收index和value,对其赋值,发生一次数据拷贝 10 | 11 | - 循环过程中新添加的元素无法遍历到 12 | 13 | ### range for map 14 | 遍历 map 时没有指定循环次数,循环体与遍历 slice 类似 15 | 16 | - 由于map插入元素随机,故新加入的元素不能保证遍历到 17 | 18 | ### range for channel 19 | 20 | channel 遍历是依次从 channel 中读取数据 21 | - 没有元素,阻塞等待 22 | - channel关闭,则会解除阻塞并退出循环 -------------------------------------------------------------------------------- /golang/slice.md: -------------------------------------------------------------------------------- 1 | # SLICE 2 | 3 | ## slice概念 4 | 5 | - 每个切片都指向一个底层数组 6 | - 每个切片都保存了当前切片的长度、底层数组可用容量 7 | - 使用len()计算切片长度时间复杂度为O(1),不需要遍历切片 8 | - 使用cap()计算切片容量时间复杂度为O(1),不需要遍历切片 9 | - 通过函数传递切片时,不会拷贝整个切片,因为切片本身只是个结构体而已 10 | - 使用append()向切片追加元素时有可能触发扩容,扩容后将会生成新的切片 11 | 12 | ## slice扩容 13 | 14 | 扩容遵顼以下原则: 15 | 16 | 1. 如果原Slice容量**小于1024**,则新Slice容量将**扩大为原来的2倍**; 17 | 2. 如果原Slice容量**大于等于1024**,则新Slice容量将扩大为**原来的1.25倍**; 18 | 19 | append向slice添加一个元素的实现步骤如下 20 | 21 | 1. 假如Slice容量够用,则将新元素追加进去,Slice.len++,返回原Slice 22 | 2. 原Slice容量不够,则将Slice先扩容,扩容后得到新Slice 23 | 3. 将新元素追加进新Slice,Slice.len++,返回新的Slice 24 | 25 | ## slice copy 26 | 27 | 使用copy()内置函数拷贝两个切片时,会将源切片的数据逐个拷贝到目的切片指向的数组中,拷贝数量取两个切片长度的**最小值** 28 | 29 | ## Go的string 30 | 31 | 32 | Go 中,string 不包含内存空间,它只有一个内存指针,所以 string 非常轻量,很方便进行传递且不用担心内存拷贝。string 通常指向字符串字面量,字面量存储的位置是只读段,并不是堆或栈上,所以 string 不能被修改 33 | -------------------------------------------------------------------------------- /messageQueue/Kafka.md: -------------------------------------------------------------------------------- 1 | # 你是否了解 Kafka 2 | 3 | 介绍一下 Kafka 的基本原理,几个主要概念。 4 | 5 | ![Kafka知识点](img/kafka_available_performance.png) 6 | 7 | 答:Kafka 是一个基于发布订阅模式的消息队列中间件。它由 Producer, Consumer, Broker 和 Partition 几个组成。 8 | 9 | Kafka 里面的每一个消息都属于一个主题,每一个主题都有多个 Partition。Partition 又可以使用主从复制模式,即 Partition 之间组成主从模式。这些 Partition 均匀分布在 Broker 上,以保证高可用。(这里点到了高可用,引导面试官探讨 Kafka 高可用)。每一个 Partition 内消息是有序的,即分区顺序性。(这一句是为了引出后面如何保证消息有序性) 10 | 11 | Producer 依据负载均衡设置,将消息发送到 Topic 的特定 Partition 下;(后面面试官可能会问负载均衡策略) 12 | 13 | Consumer 之间组成了 Consumer Group,可以有多个 Consumer Group 消费同一个 Topic,互相之间不会有影响。Kafka 强制要求每个 Partition 只能有一个 Consumer,并且 Consumer 采取拉模式,消费完一批消息之后再拉取一批(尝试引出来后面的拉模型的讨论); 14 | 15 | 一个 Kafka 集群由多个 Broker 组成,每个 Broker 上存放着不同 Topic 的 Partition; 16 | 17 | ![Topic,Broker 和 Partition](https://pic2.zhimg.com/80/v2-17a2d36445a764081b45e012397291bd_720w.jpg) 18 | 19 | ## 扩展点 20 | 21 | ### Kafka 的高性能是如何保证的? 22 | 23 | 24 | 答:Kafka 高性能依赖于非常多的手段: 25 | 1. 零拷贝。在 Linux 上 Kafka 使用了两种手段,mmap (内存映射) 和 sendfile,前者用于解决 Producer 写入数据,后者用于 Consumer 读取数据; 26 | 2. 顺序写:Kafka 的数据,可以看做是 AOF (append only file),它只允许追加数据,而不允许修改已有的数据。(后面是亮点)该手段也在数据库如 MySQL,Redis上很常见,这也是为什么我们一般说 Kafka 用机械硬盘就可以了。有人做过实验(的确有,你们可以找找,我已经找不到链接了),机械磁盘 Kafka 和 SSD Kafka 在性能上差距不大; 27 | 3. Page Cache:Kafka 允许落盘的时候,是写到 Page Cache的时候就返回,还是一定要刷新到磁盘(主要就是mmap之后要不要强制刷新磁盘),类似的机制在 MySQL, Redis上也是常见,(简要评价一下两种方式的区别)如果写到 Page Cache 就返回,那么会存在数据丢失的可能。 28 | 4. 批量操作:包括 Producer 批量发送,也包括 Broker 批量落盘。批量能够放大顺序写的优势,比如说 Producer 还没攒够一批数据发送就宕机,就会导致数据丢失; 29 | 5. 数据压缩:Kafka 提供了数据压缩选项,采用数据压缩能减少数据传输量,提高效率;**Producer 端压缩、Broker 端保持、Consumer 端解压缩** 30 | 6. 日志分段存储:Kafka 将日志分成不同的段,只有最新的段可以写,别的段都只能读。同时为每一个段保存了偏移量索引文件和时间戳索引文件,采用二分法查找数据,效率极高。同时 Kafka 会确保索引文件能够全部装入内存,以避免读取索引引发磁盘 IO。(这里有一点很有意思,就是在 MySQL 上,我们也会尽量说把索引大小控制住,能够在内存装下,在讨论数据库磁盘 IO 的时候,我们很少会计算索引无法装入内存引发的磁盘 IO,而是只计算读取数据的磁盘 IO) 31 | 32 | (批量操作+压缩的亮点)批量发送和数据压缩,在处理大数据的中间件中比较常见。比如说分布式追踪系统 CAT 和 skywalking 都有类似的技术。代价就是存在数据丢失的风险; 33 | (数据压缩的亮点)数据压缩虽然能够减少数据传输,但是会消耗更过 CPU。不过在 IO 密集型的应用里面,这不会有什么问题; 34 | 35 | (下面是零拷贝详解) 36 | 一般的数据从网络到磁盘,或者从磁盘到网络,都需要经过四次拷贝。比如说磁盘到网络,要经过: 37 | 38 | ![四次拷贝](https://miro.medium.com/max/840/0*Q6eoQ-19bq-qkm_Y) 39 | 40 | 1. 磁盘到内核缓冲区 41 | 2. 内核缓冲区到应用缓冲区 42 | 3. 应用缓冲区到内核缓冲区 43 | 4. 内核缓冲区到网络缓冲 44 | 45 | 零拷贝则是去掉了第二和第三。(之所以叫零拷贝,并不是说完全没有拷贝,而是指没有CPU参与的拷贝,DMA的还在)。 46 | 47 | ![零拷贝](https://miro.medium.com/max/700/0*es45Nv-ea2WDtI0n) 48 | 49 | (这一段可选,因为比较冷僻)如果在 Linux 高版本下,而且支持 DMA gather copy,那么内核缓冲区到 50 | 51 | ![零拷贝](https://miro.medium.com/max/700/0*XJNUTI5QoiCzSbxE) 52 | 53 | Kafka 利用了两项零拷贝技术,mmap 和 sendfile。前者是用于解决网络数据落盘的,Kafka 直接利用内存映射,完成了“写入操作”,对于 Kafka 来说,完成了网络缓冲区到磁盘缓冲区的“写入”,之后强制调用`flush`或者等操作系统(有参数控制)。(继续补充细节,如果自己是JAVA开发并且记得的话)Java 提供了`FileChannel`和`MappedByteBuffer`两项技术来实现 mmap。 54 | 55 | `sendfile`是另外一种零拷贝实现,主要解决磁盘到网络的数据传输。操作系统读取磁盘数据到内存缓冲,直接丢过去`socket buffer`,而后发送出去。很多中间件,例如 `Nignx`, `tomcat` 都采用了类似的技术。 56 | 57 | 关键字:零拷贝,顺序写,缓冲区,批量,压缩,分段存储 58 | 59 | ### KafKa高可用性 60 | 61 | 1. 备份机制:Kafka允许同一个Partition存在多个消息副本,每个Partition的副本通常由1个Leader及0个以上的Follower组成,生产者将消息直接发往对应Partition的Leader,Follower会周期地向Leader发送同步请求。所有的Partition以及各Partition的副本均匀地分配到整个集群的各个Broker上 62 | 2. ISR机制 63 | 3. 故障恢复机制。 64 | 65 | 66 | #### 类似问题 67 | - 什么是零拷贝 68 | - 为什么顺序写那么快? 69 | - Kafka 为什么那么快? 70 | - 71 | 72 | #### 如何引导 73 | - 讨论到了零拷贝技术 74 | - 讨论到了顺序写技术 75 | 76 | ### Kafka 的 ISR 是如何工作的? 77 | 78 | 答:ISR 是分区同步的概念。Kafka 为每个分区的leader维护了一个 ISR,处于 ISR 的follower意味着与leader保持了同步(所以主分区也在 ISR 里面)。 79 | 80 | 当 Producer 写入消息的时候,需要等 ISR 里面分区的确认,当 ISR 确认之后,就被认为消息已经提交成功了。ISR 里面的分区会定时从主分区里面拉取数据,如果长时间未拉取,或者数据落后太多,分区会被移出 ISR。ISR 里面分区已经同步的偏移量被称为 LEO(Log End Offset),最小的 LEO 称为 HW(高水位,high water,这个用木桶来比喻就很生动,ISR 里面的分区已同步消息就是木板,高水位就取决于最短的那个木板,也就是同步最落后的),也就是消费者可以消费的最新消息。 81 | 82 | 所谓的一致性,就是Consumer始终读HW以上的消息。对于那些没有被足够多的Follower复制的消息,视为不安全。等消息都复制完毕,Consumer才使用消息。 83 | 84 | ![LEO 和 HW](https://img-blog.csdnimg.cn/20200706235345430.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2FqaWFueWluZ3hpYW9xaW5naGFu,size_16,color_FFFFFF,t_70) 85 | 86 | 当主分区挂掉的时候,会从 ISR 里面选举一个新的主分区出来。 87 | 88 | (下面我们进一步解释一下 Producer 写入消息) 89 | 我们在 Producer 里面可以控制 ACK 机制。Producer 可以配置成三种: 90 | 1. Producer 发出去就算成功; 91 | 2. Producer 发出去,主分区写入本地磁盘就算成功; 92 | 3. Producer 发出去,ISR 所有的分区都写入磁盘,就算成功; 93 | 94 | 其性能依次下降,但是可靠性依次上升。 95 | 96 | (如果记得,可以补上这个说明)因为 ISR 里面包含了主分区,也就是说,如果整个 ISR 只有主分区,那么全部写入就退化为主分区写入。所以在可靠性要求非常高的情况下,我们要求 ISR 中分区不能少于三个。该参数可以在 Broker 中配置(min.insync.replicas) 97 | 98 | (回答到这里,我们基本上就说清楚了 ISR 的基本机制。下面我们横向对比一下 ISR 机制与别的主从同步机制。很明显的,就是 Producer 这种发送策略,是否等待同步完成,在很多中间件上都能看到,随便挑一个出来就可以。我这里总结一下: 99 | 100 | ISR 的同步机制和其它中间件机制也是类似的,在涉及主从同步的时候都要在性能和可靠性之间做取舍。通常的选项都是: 101 | 1. 主写入就认为成功 102 | 2. 主写入,至少一个从写入就认为成功; 103 | 3. 主写入,大部分从库写入就认为成功(一般“大部分”是可以配置的,从这个意义上来说,2和3可以合并为一点); 104 | 4. 主写入,所有从库写入就认为成功; 105 | 106 | 而“写入”也会有不同语义: 107 | 1. 中间件写到日志缓存就认为写入了; 108 | 2. 中间件写入到系统缓存(page cache)就认为写入了; 109 | 3. 中间件强制刷新到磁盘(发起了 fsync)就认为写入了; 110 | 111 | 都是性能到可靠性的取舍。) 112 | 113 | (在面试的时候,可以考虑回答完 ISR 之后,将上面的总结说出来,可以借用 MySQL,Redis, ZK 来说明,这算是一般规律) 114 | 115 | 116 | #### 类似问题 117 | - Kafka GC 时间过长会导致什么问题?可能导致分区被踢出去 ISR。 118 | - Kafka 是如何保证可靠性的?(除了 ISR 以外,还要强调一下 Partition 是分布在不同 Broker 上,以避免 Broker 宕机导致 Topic 不可用 119 | - 如何提高 Kafka 的可靠性 120 | - 如何提高 Kafka 吞吐量?可靠性和吞吐量在这里就是互斥的,调整参数只能提高一个,降低另外一个。 121 | 122 | ### 什么时候分区会被移出 ISR? 123 | 124 | 125 | 126 | 答案:当分区触发两个条件中的任何一个时,都会被移除出 ISR。 127 | 1. 消息落后太多,这个是参数`replica.lag.max.messages` [0.9.0后被移除](http://kafka.apache.org/documentation/#upgrade_9_breaking) 128 | 2. 分区长时间没有发起`fetch`请求,由参数`replica.lag.time.max.ms`控制。 129 | 130 | (刷亮点,点出影响因素,后面面试官跟你探讨这些因素怎么影响的)基本上,除非是新的 Broker,否则几乎都是由网络、磁盘IO和GC引起的,大多数情况下,是负载过高导致的。 131 | 132 | (点出过大过小的影响)这两个参数,过小会倒是 ISR 频繁变化,过大会导致可靠性降低,存在数据丢失的风险。 133 | 134 | (如果你知道你们公司的配置)我们公司的配置是 XXX 和 XXX。 135 | 136 | 关键字:落后多少消息,多久没同步 137 | 138 | #### 如何引导 139 | - 谈到 Kafka 可靠性可用性 140 | 141 | >补充: 为什么kafka要将`replica.lag.max.messages`删除? 142 | > 143 | > 因为这个参数本身很难给出一个合适的值。以默认的值4000为例,对于消息流入速度很低的主题(比如TPS为10),这个参数就没什么用;对于消息流入速度很高的主题(比如TPS为2000),这个参数的取值又会引入ISR的频繁变动(ISR 需要在Zookeeper中维护)。所以从0.9x版本开始,Kafka就彻底移除了这一个参数。 144 | 145 | 146 | ### Kafka的一致性和可靠性如何保证 147 | 148 | 1. 生产者数据不丢失 149 | 150 | Producer向 kafka 发送数据的时候,每次发送消息都会有一个确认反馈机制,确保消息正常的能够被收到。 151 | 152 | 反馈分为3种 153 | 154 | 2. Topic分区副本 155 | 156 | 每个Partition有多个副本,一个副本是Leader,其余位Follower。所有的读写操作都是经过 Leader 进行的,同时 follower 会定期地去 leader 上复制数据。当 Leader 挂掉之后,其中一个 follower 会重新成为新的 Leader。通过分区副本,引入了数据冗余,同时也提供了 Kafka 的数据可靠性 157 | 158 | **把消息写入多个副本可以使 Kafka 在发生崩溃时仍能保证消息的持久性** 159 | 160 | 3. leader选举 161 | 162 | leader挂掉后,会从 ISR 列表中选择第一个 follower 作为新的 Leader,因为这个分区拥有最新的已经 committed 的消息。通过这个可以保证已经 committed 的消息的数据可靠性 163 | 164 | 165 | 166 | ### Kafka 的负载均衡策略有哪些? 167 | 168 | 169 | 170 | 答案:一般来说有两种,一种是轮询,即 Producer 轮流挑选不同的 Partition;另外一种是 Hash 取余,这要求我们提供 Key。 171 | 172 | **轮询策略有非常优秀的负载均衡表现,它总是能保证消息最大限度地被平均分配到所有分区上,故默认情况下它是最合理的分区策略,也是我们最常用的分区策略之一** 173 | 174 | (接下来,我们讨论 Key 的选择对 Partition 负载的影响,主要是为了体现自己用 Kafka 解决不同问题的思路) 175 | 176 | Key 的选取,大原则上是采用业务特征 ID,或者业务特征的某些字段拼接而成。比如说,我们可以考虑按照用 Order ID(可以替换成自己项目里面的某些业务的ID)作为 Key,这意味某个订单的消息肯定落在特定的某个 Partition 上,这就保证了针对该订单的消息是有序的(这里面间接提到了有序性的问题,体现了自己对于 Partition 的理解)。 177 | 178 | (说一下 Hash 策略的风险)但是 Hash 策略下,如果 Key 设置不当,可能会导致某些 Partition 承载了大多数的流量。比如说按照商家 ID 来作为 Key,那么可能某些热点商家,大卖家,其消息就集中在某个 Partition 上,导致负载不均衡。 179 | 180 | (我们升华一下这个问题,就是这些负载策略实际上都只考虑 Partition 的负载,而没有考虑 Consumer 的负载,为了进一步凸显自己对负载均衡的理解) 181 | 无论是轮询,还是 Hash,都无法解决一个问题:它们没有考虑 Consumer 的负载。例如,我们可以用 Hash 策略均匀分布了消息,但是可能某些消息消费得慢,有些消息消费得快。假如说非常不幸我们消费得慢的消息都落在某个 Partition,那么该 Partition 的消费者和别的消费者比起来,消费起来就很慢,带来很大的延迟,甚至出现消息堆积。 182 | 183 | 关键字:轮询,Hash,Key 的选取 184 | 185 | #### 类似的问题 186 | - 如何选取 Hash Key 187 | - 你们是如何设置 Producer 推送消息到哪个 Partition 的? 188 | 189 | #### 如何引导 190 | - 在介绍 Kafka 的时候 191 | - 讨论到 Hash Key 选择的时候。其实不仅仅是 Kafka,所有基于 Hash 的负载均衡算法,都会有类似的问题。所谓的 Hash 冲突,也就是这个问题。 192 | - 聊到了消息有序性的时候 193 | 194 | ### 为什么 Kafka 的从 Partition 不能读取? 195 | 196 | 197 | 198 | 答:首先是 Kafka 自身的限制,即 Kafka 强制要求一个 Partition 只能有一个 Consumer,因此 Consumer 天然只需要消费主 Partition 就可以。 199 | 200 | 那么假如说 Kafka 放开这种限制,比如说有多个 Consumer,分别从主 Partition 和从 Partition 上读取数据,那么会出现一个问题:即偏移量如何同步的问题。例如一个 Consumer 从 Partition A 读取了 0- 100 的消息,那么另外一个 Consumer 从 Partition B 上读取,就只能读取 100 之后的数据。那么 Kafka 就需要在不同的 Partition 之间协调这个已读取偏移量。而这是分布式一致性的问题,难以解决。 201 | 202 | MySQL 的主从模式比起来,并没有这种问题,即 MySQL 不需要进行类似偏移量的协商。 203 | 204 | 而从另外一个角度来说,Kafka 的读取压力是远小于 MySQL 的,毕竟一个 Topic,是不会有特别多的消费者的。并且 Kafka 也不需要支持复杂查询,所以完全没必要读取从 Partition 的数据。 205 | 206 | 关键字:偏移量 207 | 208 | ### 为什么 Kafka 在消费者端采用了拉(PULL)模型? 209 | 210 | 211 | 答:采用拉模型的核心原因在于,消费者的消费速率不同。在拉模型之下,消费者自己消费完毕就自己再去拉去一批,那么这种速率是由消费者自己控制的,所需要的控制信息也是由消费者自己保存的。而采用推模型,就意味着中间件要和消费者就速率问题进行协商,否则容易导致要么推送过快,要么推送过慢的问题。 212 | 213 | 推模型的一个极大的好处是避免竞争,例如在多个消费者拉同一主题的消息的时候,就需要保证,不同消费者不会引起并发问题。而 Kafka 不会有类似的问题,因为 Kafka 限制了一个 Partition 只能有一个消费者,所以拉模型反而更加合适。 214 | 215 | 关键字:谁控制,并发竞争 216 | 217 | ### 分区过多会引起什么问题? 218 | 219 | 220 | 221 | 答:对于 Producer 来说,它采用的是批量发送的机制,那么分区数量多的话,就需要消耗大量的内存来维护这些缓存的消息。同时,也增大了数据丢失的风险。 222 | 223 | 对于 Consumer 来说,分区数量多意味着要么部署非常多的实例,要么开启非常多的线程,无论是哪一种方案,都是开销巨大。 224 | 225 | 对于 Broker 来说,分区特别多而对应的 Broker 数量又不足的话,那么意味着一个 Broker 上分布着大量的分区,那么一次宕机就会引起 Kafka 延时猛增。同时,每一个分区都要求 Broker 开启三个句柄,那么会引起 Broker 上的文件句柄被急速消耗,可能导致程序崩溃。还要考虑到,Kafka 虽然采用了顺序写,但是这是指在一个分区内部顺序写,在多个分区之间,是无法做到顺序写的。 226 | 227 | (注意,对于 Broker 来说,如果你的集群规模非常大,以至于虽然有一万个分区,但是每个 Broker 上只有寥寥几个分区,那么分区数量对 Broker 来说是没影响的。我们这里的讨论,都是建立在我一个 Broker 上放了很多分区的基础上) 228 | 229 | (分区数量和性能的关系类似一个二次函数,随着分区增长会慢慢变好,但是到达一个临界点之后,就会开始衰退) 230 | #### 类似问题 231 | - 分区数量是不是越多越好?显然不是; 232 | - 分区数量越多,是不是吞吐量越高?显然不是; 233 | - 能不能通过增加分区数量来提高 Kafka 性能?注意,这个是可以的,但是要注意把握度,就是不能无限增加; 234 | - Topic 过多会引起什么问题?其实差不多是同一个问题,Topic 多意味着分区多,而且通常伴随的是每个 Topic 的数据量都不大; 235 | 236 | ### 如何解决 Topic 的分区数量过多的问题? 237 | 238 | 239 | 240 | 答:增加 Broker,确保 Broker 上不会存在很多的分区。这可以避免 Broker 上文件句柄数量过多,顺序写退化为随机写,以及宕机影响范围太大的问题。 241 | 242 | 其次可以考虑拆分 Topic 并且部署到不同的集群。(这里要注意,Topic 如果拆了但是没有增加 Broker,也没有部署额外的 Kafka 集群,那么其实还是没啥用) 243 | 244 | 当然,如果分区的写入负载其实并不大,那么可以考虑削减分区的。(尝试引出削减分区的话题,这是一个鱼钩,因为kafka本身不支持削减分区) 245 | 246 | #### 类似问题 247 | - 如何解决 Topic 太多的问题?这个问题稍微有点不同,我们考虑的就不是拆分 Topic 而是合并 Topic了。增加 Broker 有点效果,但是没有分区数量多那么有效。核心就在于, Topic 多伴随的都是每个 Topic 数据不多。 248 | 249 | #### 如何引导 250 | - 聊到了分区数量是不是越多越好 251 | 252 | ### 如何确定合适的分区数量? 253 | 254 | 分析:典型的计算容量题。所不同的是,分区数量会影响两端,因此要同时考虑 Producer 的效率和 Consumer 的效率。 255 | 256 | 答:使用 Kafka 提供的压测工具来测试。一般来说,我们对于某个特定的 Topic,其消息大小是能够从业务上推断出来的,也就是我们不存在说一个 Topic,某些消息特别长,某些消息特别短。大部分的消息长度都在相差不多的范围内。 257 | 258 | 因此我们可以控制写入一个分区的 TPS,观察同步延时和消息是否积压(消费端的消费数据,例如99线等也可以)。 259 | 260 | #### 类似问题 261 | - 如何确定消费者数量?要注意,消费者最多最多就是和分区数量一样,其它就是压测了。 262 | 263 | #### 如何引导 264 | - 聊到了分区数量过多的问题 265 | 266 | ### 如何保证消息有序性?方案有什么缺点? 267 | 268 | 答:Kafka 要做到消息有序,只需要将消息都投递到同一个分区里面。因为 Kafka 的设计确保了一个分区内部的消息是有序的。但是,这并不是说,我们只能拥有一个分区,而是我们可以从业务上,将相关的消息都扔到了一个分区。例如按照用户 ID 来选择分区,确保用户相关的某些消息都在同一个分区内部。(点出缺点)类似的方案都要注意分区负载,例如热点用户产生了大量的消息,都被积压在该分区。 269 | 270 | #### 类似问题 271 | - Topic 为了保证消息有序性,我们会考虑只使用一个 Partition,你有什么改进方案? 272 | 273 | 274 | ### 什么情况导致重复消费? 275 | 276 | 277 | 278 | 答案: 279 | 280 | 1. 从生产者到消息中间件之间,生产者可能重复发送。例如生产者发送过程中出现超时,因此生产者不确定自己是否发出去了,重试; 281 | 2. 消息中间件到消费者,也可能超时。即消息中间件不知道消费者消费消息了没有,那么重试就会引起重复消费。消息中间件不知道消费者消费消息了没有,又有两种子情况: 282 | 283 | 2.1 消息传输到消费者的时候超时; 284 | 285 | 2.2 消费者确认的时候超时; 286 | 287 | 关键字:**超时** 288 | 289 | 290 | ### Kafka能不能重复消费? 291 | 292 | 293 | 答:能。Kafka 的分区用 offset 来记录消费者消费到哪里了。因此我们可以考虑指定 offset 来消费,比如指定一个很久之前的 offset。 294 | 295 | 一些场景之下,我们会更加倾向于指定时间节点,那么可以先根据时间戳找到 offset,然后再从 offset 消费。 296 | 297 | 不过要注意的是,有些时候,这些消息可能已经被归档(删除——一般都不会直接删除,而是丢到一个别的地方放起来,以防万一)了,那么这一类的消息,就确实是没法子重复消费了。 298 | 299 | ### Kafka避免重复消费 300 | 301 | - 避免Rebalance 302 | - 引入消息去重机制。例如:生成消息时,在消息中加入唯一标识符如消息id等 303 | 304 | ### Rebalance 发生时机 305 | 306 | 答: 307 | 1. Topic 或者分区的数量变化(苹果数量变化,例如增加新的分区) 308 | 2. 消费者数量变化(加入或者退出)。这个又可以细分为两个:一个是消费超时(max.poll.interval.ms),一个是心跳超时(session.timeout.ms) 309 | 310 | 类似问题 311 | - 什么时候会 Rebalance? 312 | - 消费者加入或者退出会有啥影响? 313 | - 扩容(指增加分区)会有什么影响?这两个都是引起 rebalance 314 | 315 | ### rebalance 的过程 316 | 317 | 318 | 答:以新的消费者加入为例,这个步骤可以分成以下几步: 319 | 1. 新的消费者向协调者上报自己的订阅信息; 320 | 2. 协调者强制别的消费者发起一轮 rebalance,上报自己的订阅信息; 321 | 3. 协调者从消费者中挑选一个 leader,注意这里是挑选了消费者中的 leader; 322 | 4. 协调者将订阅信息发给 leader,让 leader 来制作分配方案; 323 | 5. leader 上报自己的方案; 324 | 6. 协调者同步方案给别的消费者 325 | 326 | (更加简单的记忆方式是:挑选 leader -> leader 出方案 -> 同步方案,就很像一堆同事说我来搞负责解决这个问题,然后老板挑了一个卷王,说你出个方案,老板看了方案很满意,交代其它同事说按照这个方案执行) 327 | 328 | ### rebalance 有啥影响 329 | 330 | 331 | 答: 332 | 1. 重复消费:如果在消费者已经消费了,但是还没提交,这个时候发生了 rebalance,那么别的消费者可能会再一次消费; 333 | 2. 影响性能:rebalance 的过程,一般是在几十毫秒到上百毫秒。这个过程会导致集群处于一种不稳定状态中,影响消费者的吞吐量; 334 | 335 | #### 如何引导 336 | - 在前面聊到过程,就可以主动聊有什么影响 337 | 338 | ### 如何避免 rebalance? 339 | 340 | 341 | 答:首先,Topic 或者分区变化,引起 rebalance 是无法避免的,因为一般都是因为业务变化引起的。比如说,随着流量增加,我们要增加分区。 342 | 343 | 能够避免的就是防止消费者出现消费超时或者心跳超时。消费超时可以增大`max.poll.interval.ms` 参数,避免被协调者踢掉。或者优化消费逻辑,使得消费者能够快速消费,拉取下一批消息。 344 | 345 | 而心跳超时,也可以通过增大`session.timeout.ms`来缓解。 346 | 347 | (可以进一步分析这两个参数增大的弊端) 348 | 但是这两个参数增大,都可能导致,消费者真的出了问题,但是协调者却迟迟没有感知到的问题。 349 | 350 | #### 如何引导 351 | - 聊到 rebalance 的影响 352 | 353 | ### 为什么 Kafka 不支持减少分区? 354 | 355 | 356 | 答:主要还是在于,难以处理分区上的数据。假如说,我们要支持 Kafka 支持减少分区,那么我们就要考虑第一个问题,这个分区上的数据,该怎么办?大多数情况下,我们不能直接丢掉,那么只能考虑重新分配给其它的分区。于是就涉及到,如何分配,以及对其余分区的影响的问题了。 357 | 358 | 总体来说,减少分区的复杂度,远比增加分区的复杂度大,但是收益是小的。一方面,有别的手段来解决类似的问题,另一方面,大多数的场景,都是增加分区,而不是减少分区。 359 | 360 | 假如我们要实现类似的功能,可以考虑两种方案: 361 | 1. 创建一个完全一样的 Topic,然后分区数量少一点,等老的 Topic 消费完就直接下线,只留下这个新的 Topic; 362 | 2. 考虑在写入分区的时候,不再写入特定的分区,可以通过业务来控制,也可以通过负载均衡机制来控制;其缺点是,这个没用的分区会长期存在,并没有在事实上删除它; 363 | 364 | 365 | ## References 366 | [图解Kafka高可用机制](https://zhuanlan.zhihu.com/p/56440807) 367 | 368 | [Kafka高性能原理](https://zhuanlan.zhihu.com/p/105509080) 369 | 370 | [Why Kafka is fast](https://preparingforcodinginterview.wordpress.com/2019/10/04/kafka-3-why-is-kafka-so-fast/) -------------------------------------------------------------------------------- /messageQueue/README.md: -------------------------------------------------------------------------------- 1 | # 消息队列 2 | 3 | 4 | 支持高吞吐、高并发、高可用的队列 5 | 6 | ![消息队列概览](img/mq_overview.jpeg) 7 | 8 | 9 | 10 | 要理解这些问题,我们要先理解分布式调用语义: 11 | 12 | 1. 至少一次语义:是指消费者至少消费消息一次。这意味着存在重复消费的可能,解决思路就是幂等; 13 | 2. 至多一次语义:是指消费者至多消费消息一次。这意味着存在消息没有被消费的可能,基本上实际中不会考虑采用这种语义,只有在日志采集之类的,数据可以部分缺失的场景,才可能考虑这种语义; 14 | 3. 恰好一次语义:最严苛的语义,指消息不多不少恰好被消费一次; 15 | 16 | 绝大多数情况下,我们追求的都是**至少一次**语义,即生产者至少发送一次,可能重复发送;消费者至少消费一次,可能重复消费(虽然去重了,但是我们也认为消费了,只不过这个消费啥也没干)。结合之下,就能发现,只要解决了消费者重复消费的问题的,那么生产者发送多次,就不再是问题了。 17 | 18 | 有时候面试官也会将这种去重之后的做法称为“恰好一次”,所以面试的时候要注意一下,是可以用重试+去重来达成恰好一次语义的。 19 | 20 | 理解了这些,我们就解决了数据一致性的问题,即生产者一定发出去了消息,或者消费者一定消费了消息。 21 | 22 | 还有一些问题,我们会在具体的消息队列中间件上讨论,例如如何保证高可用,如何保证高性能。这些都是具体消息中间件相关的。 23 | 24 | 关键字:**一致性**,**幂等**,**顺序** 25 | 26 | ![大概模式](img/overview.jpeg) 27 | 28 | ## 问题 29 | 30 | ### 你用消息队列做什么? 31 | 32 | 33 | 34 | 1. 解耦:将不同的系统之间解耦开来。尤其是当你在不希望感知到下游的情况。例如我们退款,会用消息队列来暴露我们的退款信息,比如说退款成功与否。很多下游关心,但是实际上,我们退款部门根本不关心有谁关心。在不使用消息队列的时候,我们就需要一个个循环调用过去; 35 | 36 | 2. 异步:是指将一个同步流程,利用消息队列来拆成多个步骤。这个特性被广泛应用在事件驱动之中。比如说在退款的时候,退款需要接入风控,多个款项资金转移,这些步骤都是利用消息队列解耦,上一个步骤完成,发出事件来驱动执行下一步; 37 | 38 | 3. 削峰:削峰主要是为了应对突如其来的流量。一般来说,数据库只能承受每秒上千的写请求,如果在这个时候,突然来了几十万的请求,那么数据库可能就会崩掉。消息队列这时候就起到一个缓冲的效果,服务器可以根据自己的处理能力,一批一批从消息队列里面拉取请求并进行处理。 39 | 40 | 关键字:**解耦**,**异步**,**削峰** 41 | 42 | ![直接调用](img/decouple1.jpeg) 43 | 44 | ![依赖中间件解耦](img/decouple2.jpeg) 45 | 46 | #### 类似问题 47 | 48 | - 消息队列有什么作用 49 | - 你为什么用消息队列 50 | - 为什么不直接调用下游,而要使用消息中间件?这个基本上就是回答解耦,异步也勉强说得上,不过要点在解耦。 51 | - 或者,面试官直接问道三个特性的某个,你是如何使用的 52 | 53 | #### 如何引导 54 | 55 | - 讨论到秒杀 56 | - 讨论到事件驱动 57 | 58 | ### 消息队列有什么缺点? 59 | 60 | 61 | 答: 62 | 63 | 1. 可用性降低:引入任何一个中间件,或者多任何一个模块,都会导致你的可用性降低。(所以这个其实不是MQ的特性,而是所有中间件的特性) 64 | 2. 一致性难保证:引入消息队列往往意味着本地事务不可用,那么就容易出现数据一致性的问题。例如业务成功了,但是消息没发出去; 65 | 3. 复杂性上升:复杂性分两方面,一方面是消息队列集群维护的复杂性,一方面是代码的复杂性 66 | 67 | (升华主题)几乎所有的中间件的引入,都会引起类似的问题。 68 | 69 | 关键字:**可用性**,**一致性**,**复杂性** 70 | 71 | #### 如何引导 72 | - 前面说完消息队列的好处之后,直接就可以接缺点 73 | 74 | 75 | 76 | ### 如何保证消息消费的幂等性? 77 | 78 | 79 | ![ACK 机制无法确保幂等性](img/ack_timeout.jpeg) 80 | 81 | 答案:保证幂等性,主要依赖消费者自己去完成。一般来说,一条消息里面都会带上标记这一次业务的某些特征字段。核心就是利用这些字段来去重。比如说,最常见的是利用数据库的唯一索引来去重,要小心的就是,采用 `check - doSomething` 模式可能会有并发问题。 82 | 83 | 另外一种就是利用 Redis。因为你只需要处理一次,所以不必采用分布式锁的模式,只需要将超时时间设置得非常非常长。带来的不利影响就是Redis会有非常多的无用数据,而且万一真有消息在 Redis 过期之后又发过来,那还是会有问题。 84 | 85 | #### 类似问题 86 | 87 | - 如何保证消息只会被消费一次? 88 | - 如何保证消息消费恰好一次语义? 89 | 90 | 关键字:**去重**,**Redis Set**, **唯一索引** 91 | 92 | ### 什么情况导致重复消费? 93 | 94 | 95 | 96 | 答案: 97 | 98 | 1. 从生产者到消息中间件之间,生产者可能重复发送。例如生产者发送过程中出现超时,因此生产者不确定自己是否发出去了,重试; 99 | 2. 消息中间件到消费者,也可能超时。即消息中间件不知道消费者消费消息了没有,那么重试就会引起重复消费。消息中间件不知道消费者消费消息了没有,又有两种子情况: 100 | 101 | 2.1 消息传输到消费者的时候超时; 102 | 103 | 2.2 消费者确认的时候超时; 104 | 105 | 关键字:**超时** 106 | 107 | ### 如何保证生产者只会发送消息一次? 108 | 109 | 110 | 111 | 答案:这个问题,可以拆成两个问题:如何保证消息一定发出去了?其次是如何保证只发了一次? 112 | 113 | 对于第一个问题来说,可以考虑分布式事务,或者重试机制。 114 | 115 | 开启分布式事务需要消息中间件的支持。 116 | 117 | 超时机制,核心就是超时处理 + 查询。如果在消息发送明确得到了失败的响应,那么可以考虑重试,超过重试次数就需要考虑人手工介入。 118 | 119 | 另外一种是超时处理,即你也不知道究竟成功了没。为了防止这种问题,可以考虑在发送消息之前,插入一条数据库待发送消息记录,这个插入要和前面的业务逻辑绑在一起作为一个本地事务。在发送成功、失败或者超时都标记对应的记录。带来的问题就是增加数据库的负担,并且后面更新记录的时候,依旧可能失败,最终还是无法保证生产一次。 120 | 121 | 而后开启一个定时任务,扫描超时的记录,然后重新发送。如果消息中间件支持查询,那么可以考虑查询一下上一次的究竟成功没有,否则就只能直接重试。 122 | 123 | 第二个问题:如何确保只发送一次? 124 | 125 | 从前面来看,分布式事务天然就能保证只发送一次。而超时机制,则完全无法保证。 126 | 127 | (升华主题)其实我们追求的并不是消息恰好发送一次,而是消息至少发送一次,依赖于消费端的幂等性来做到恰好一次语义。 128 | 129 | #### 类似问题 130 | 131 | - 如何保证消息一定发出去了? 132 | - 如何保证消息的可靠性传输? 133 | - 如何保证消息的数据一致性?分生产者和消费者两方来回答 134 | - 能不能依赖于消息ID来做重复消费的去重? 135 | 136 | #### 如何引导 137 | - 聊到如何确保消费一次。 138 | 139 | ### 如何保证消息顺序 140 | 141 | 可能指: 142 | 143 | 1. AB两台机器,A机器在实际上先于B机器发出来的消息,那么消费者一定先消费 144 | 2. 同一个业务(例如下单),先发出来的消息(例如创建订单消息)一定比后面发的消息(例如支付消息)先被消费 145 | 3. 不同业务,先发出来的消息的(例如支付消息)一定比后面发的消息(例如退款)先被消费。(实际中,支付和退款差不多都是分属两个部门) 146 | 147 | 第一个问题,涉及的是时钟问题,可以忽略,除非面到了中间件设计中时序处理的问题。 148 | 149 | 第二个问题,可以理解为同一个主题(topic),前后两条消息。这是一般意义上的消息顺序。 150 | 151 | 第三个问题,可以理解为不同主题(topic),前后两条消息。这是业务上的消息顺序。 152 | 153 | 所以,我们的回答就围绕:同一个业务的消息,如何保证顺序。 154 | 155 | 156 | 157 | 回答:一般原则上,保证消息顺序需要保证生产者投递过来的顺序是对的,消费者消费的顺序也是对的。大多数情况下,我们可以让相关消息都发送到同一个队列里面。例如,对于`Kafka`而言,我们可以让要求顺序的消息,都丢到同一个`Partition`里面。重要的是,`Kafka`的机制保证了,一个`Partition`只会有一个线程来消费,从而保证了顺序。 158 | 159 | (要补充说明,`Partition`可以替换为队列等你所用的中间件的类似的概念)这里指的同一个`Partition`是指,相关的消息都到一个`Partition`,而不是指整个`topic`只有一个`Partition`。 160 | 161 | (补充缺点)带来的问题是要慎重考虑负载均衡的问题,否则容易出现一个`Partition`拥挤的问题。而且它还限制了,同一个`Partition`只能被一个线程消费,而不能让一个线程取数据而后提交给线程池消费。 162 | 163 | 有些时候,我们需要自己重新排序收到的消息,比如说消息中间件不支持指定发送目的地(队列),或者消息属于不同的主题。(属于不同主题,就只能用这个方法) 164 | 165 | 在收到乱序之后的消息,可以选择两种做法: 166 | 167 | 1. 将消息转储到某个地方,例如扔过去数据库,放到`Redis`或者直接放在内存里面; 168 | 2. 让消息中间件过一段时间再发过来,期望再发过来的时候,前置消息已经被处理过了; 169 | 170 | 每次收到消息,都要检查是不是可以处理这个消息了,可以的话才处理。 171 | 172 | (要考虑周详,容错问题)这种问题下,可能出现的是,某个前置消息一直没来,那么应该有一个告警机制。还要考虑到,这种已经被处理过的消息,是可以被清理掉的,以节省资源。 173 | 174 | 而如果我们要求不同主题的消息有顺序,例如某个业务会依次产生 A,B 两个消息,它们属于不同主题。那么这种情况下,第二种方法才能达到保证消息顺序的目的。 175 | 176 | #### 类似问题 177 | - 如何保证业务消息的顺序?要根据消息是否属于同一主题来回答,两个方面都要回答 178 | - 如何保证管理消息的依赖性? 179 | - 如何确保某个消息一定会先于别的消息执行? 180 | 181 | #### 如何引导 182 | 183 | - 在谈论到`Kafka`的`Partition`的时候(或者讨论到类似的东西的时候,可以简单提及可用这种性质来解决顺序问题); 184 | 185 | ### 消息积压怎么办? 186 | 187 | 188 | ![消息积压解决思路](img/too_many_msg.jpeg) 189 | 190 | 答案:整体上有两个思路: 191 | 192 | 1. 增加集群规模。不过这个只能治标,缓解问题,但是不能解决问题; 193 | 2. 加快单个消息的消费速率。例如原本同步消费的,可以变成异步消费。把耗时的操作从消费的同步过程里面摘出去; 194 | 3. 增加消费者。例如`Kafka`中,增加`Partition`,或者启用线程池来消费同一个`Partition`。 195 | 196 | (刷亮点)其实消息积压要看是突然的积压,即偶然的,那么只需要扩大集群规模,确保突然起来的消息都能在消息中间件上保存起来,就可以了。因为后续生产者的速率回归正常,消费者可以逐步消费完积压的消息。如果是常态化的生产者速率大于消费者,那么说明容量预估就不对。这时候就要调整集群规模,并且增加消费者。典型的就是,`Kafka`增加新的`Partition`。 197 | 198 | #### 类似问题 199 | 200 | - 生产者发送消息太快怎么办? 201 | - 消费者消费太慢怎么办? 202 | - 怎么加快消费者消费速率? 203 | 204 | ### 如何实现延时消息? 205 | 206 | 207 | 208 | 答案:在消息中间件本身不支持延时消息的情况下,大体上有两种思路: 209 | 210 | 第一种思路是消息延时发送。生产者知道自己的消息要延时发送,可以考虑先存进去代发消息列表,而后定时任务扫描,到达时间就发送;也可以生产者直接发到一个特殊的主题,该主题的消费者会存储下来。等到时间到了,消费者再投递到准确的主题; 211 | 212 | 第二种思路是消息延时消费。消费者直接收到一个延时消息,发现时间点还没到,就自己存着。定时任务扫描,到时间就消费。 213 | 214 | 如果是消费者和生产者自己存储延时消息,那么意味着每个人都需要写类似的代码来处理延时消息。所以比较好的是借助一个第三方,而第三方的位置也有两种模式: 215 | 1. 第三方位于消息队列之前,第三方临时存储一下,后面再投递; 216 | 2. 第三方位于某个特殊主题之后,生产者统一发到该特殊主题。第三方消费该主题,临时存储,而后到点发送到准确主题; 217 | 218 | 219 | -------------------------------------------------------------------------------- /messageQueue/img/ack_timeout.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/messageQueue/img/ack_timeout.jpeg -------------------------------------------------------------------------------- /messageQueue/img/decouple1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/messageQueue/img/decouple1.jpeg -------------------------------------------------------------------------------- /messageQueue/img/dedouple2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/messageQueue/img/dedouple2.jpeg -------------------------------------------------------------------------------- /messageQueue/img/delay_third_directly.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/messageQueue/img/delay_third_directly.jpeg -------------------------------------------------------------------------------- /messageQueue/img/delay_third_using_mq.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/messageQueue/img/delay_third_using_mq.jpeg -------------------------------------------------------------------------------- /messageQueue/img/kafka_available_performance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/messageQueue/img/kafka_available_performance.png -------------------------------------------------------------------------------- /messageQueue/img/mq_overview.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/messageQueue/img/mq_overview.jpeg -------------------------------------------------------------------------------- /messageQueue/img/overview.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/messageQueue/img/overview.jpeg -------------------------------------------------------------------------------- /messageQueue/img/too_many_msg.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/messageQueue/img/too_many_msg.jpeg -------------------------------------------------------------------------------- /microservice/availability.md: -------------------------------------------------------------------------------- 1 | # 微服务可用性 2 | 3 | ## 面试题目 4 | 5 | ### 怎么保证微服务的可用性? 6 | 7 | 微服务稳定性 8 | - 过载保护:服务B压力较大,直接拒绝部分流量 9 | - 熔断:服务B负载大,服务A无法连,此时服务A间断尝试连接 10 | - 限流:限制服务处理的最大 QPS,拒绝过多请求 11 | - 降级:重要服务运作,不重要拒绝 12 | 13 | ### 微服务的服务治理 14 | 15 | 服务发布 16 | - 难点 17 | - 服务不可用 18 | - 服务抖动 19 | - 服务回滚 20 | 21 | - 解决办法 22 | - 灰度发布:先发布少部分实例,逐步增加实例 23 | - 蓝绿发布:服务分成两部分,分别前后发布 24 | 25 | 26 | #### 类似问题 27 | - 你怎么保证 XXX 服务的可用性? 28 | - 你做了一些什么?or 你们公司做了一些什么? 29 | 30 | ### 31 | -------------------------------------------------------------------------------- /microservice/general.md: -------------------------------------------------------------------------------- 1 | # 微服务概览 2 | 3 | > 微服务细碎的问题就放在这里讨论 4 | 5 | 微服务的面试题,大体上可以分成: 6 | 1. 微服务本身的面试题。包括微服务的概念,为什么要微服务之类的; 7 | 2. 微服务治理的面试题。主要就是服务注册与发现,熔断限流之类的; 8 | 3. 微服务架构的面试题。微服务有什么部分,每个部分有什么用处,以及怎么设计; 9 | 4. 微服务选型的面试题。包括选择微服务框架,以及选择微服务依赖的第三方中间件的问题; 10 | 11 | ## 面试题目 12 | ### 微服务和 RESTful 的区别 13 | 14 | 微服务是一种架构,而 RESTful API 是符合**REST**设计风格的 Web API。所以从这个角度来说,这两者根本不具备可比性。那么为什么面试官要问这个问题呢?大概率是他们公司内部有并存的微服务应用,和 RESTful 应用。 15 | 16 | 我们先来分析 RESTful,它是一种遵守了**REST**设计风格的 Web API,显然也有不遵守 REST 的。这部分我们一般就是简单描述为 Web 服务。 17 | 18 | REST设计风格:互联网应用程序的API设计理念:URL定位资源,用HTTP描述操作 19 | 20 | 21 | 而微服务,一般是指以 RPC 为通信,结合了整个微服务治理的架构模式。实际上,微服务作为一种架构模式,其落地的选择是有很多的,除了这里说的 RPC,还有一种很重要的实现手段就是基于 Web API 的实现方式。 22 | 23 | 我们可以总结来说,微服务落地,最基本的通信的角度来说,可以是 RPC,也可以是 HTTP。而在 HTTP 之下,有一个子分类就是 RESTful。因此,微服务和 RESTful 的区别,核心就是: 24 | 1. 微服务是架构模式; 25 | 2. RESTful 是指符合 REST 规范的 Web API; 26 | 3. 微服务可以用 RESTful 来实现; 27 | 28 | 这里就基本上讨论清楚了,不过我们还可以刷一个亮点:RPC 本身也是可以用 RESTful 来实现的。于是我们可以加上第四点:有些微服务虽然是基于 RPC 来构建的,但是 RPC 本身又可以是用 RESTful 来实现的。 29 | 30 | 沿着这个思路: 31 | 1. 微服务用什么实现?RESTful 或者 RPC 都可以; 32 | 2. RPC 用什么实现?直接基于 TCP 或者基于 HTTP 都可以。 33 | 34 | 答案: 35 | 1. 微服务是架构模式; 36 | 2. RESTful 是指符合 REST 规范的 Web API; 37 | 3. 微服务可以用 RESTful 来实现; 38 | 4. (亮点)微服务的另外一种实现,是利用 RPC 来实现;而 RPC 可以直接基于 TCP 来实现,也可以基于 HTTP 来实现,所以也可以用 RESTful 来实现;(这里可能会引起面试官的兴趣,就是问你 RPC 有哪些实现思路) 39 | 40 | (最后总结)微服务和 RESTful 总体来说是两种维度的东西。 41 | 42 | (这里还有一个可能,就是面试官问你,RPC 和 RESTful 的区别,以为第四点我们聊到了微服务可以是 RPC 也可以是 RESTful) 43 | 44 | ### RPC 和 RESTful 的区别 45 | 46 | 分析:RPC 和 RESTful 的区别,前面的问题**微服务和 RESTful 的区别**我们已经提到了。 RPC 名字叫做远程过程调用,是一种远程通信协议。但凡是协议,就会有落地。那么 RPC 的落地就很百花争鸣了,不过主流就是两个流派:基于 HTTP 的和基于 TCP 的。前者的代表是 gRPC,后者的代表是 Dubbo。基于 HTTP 的,如果要是它的 API 设计也符合 REST 设计风格,那么就可以说,它是基于 RESTful 的。 47 | 48 | 后面我们可以稍微聊一下这两种实现方式的优劣对比,作为一个亮点。 49 | 50 | 答案:RPC 是远程通信协议,它的实现可以是基于 HTTP 的,也可以是基于 TCP 的。而 RESTful 是符合 REST 设计风格的 Web API。因此,如果一个 RPC 是基于 HTTP 的,并且 HTTP 的 API 设计是符合 REST 设计风格的,那么就可以说这个 RPC 是基于 RESTful 的。 51 | 52 | 它们也是两个不同维度的东西。一般来说,基于 TCP 的 RPC 实现更加复杂,但是可以从 TCP 层面上优化,因此性能会更好。而基于 HTTP 的则是实现非常简单,目前,基于 HTTP2 协议实现的 RPC 在性能上也很优秀,对于大部分应用来说,并不会触及它的性能上限(这里面是一个很大的误区,有很多人实际中,根本不考虑自己的实际情况,就使劲朝着高性能的角度去选型,其实大多数时候,我们都是和复杂度本身做斗争,而不是和性能作斗争)。 53 | 54 | ### RPC实现思路 55 | - 机器之间的通信传输协议 56 | - 报文的编码与解码,是json、xml或是protobuf编码 57 | - 连接超时 58 | - 是否支持异步请求和并发 59 | - 服务注册中心和服务动态添加、删除(**心跳机制**确保服务处于可用状态,引出zookeeper心跳) 60 | - 负载均衡:随机选择、轮询、加权轮询、一致性哈希 61 | 62 | ### 微服务划分的粒度该如何确定? 63 | - 各服务有清晰的责任及边界,一个服务对应一块业务,服务间多为单向依赖 64 | - 新增或变更业务上有很明确的服务对应 65 | - 技术上服务设定的核心要关注对性能的影响、是否稳定及架构是否简洁,是否要引入额外的中间件等 66 | 67 | ### 既然有了模块化,为什么还要微服务? 68 | 69 | - 团队能够独立地工作与扩张。 70 | - 微服务小巧、专一,降低了复杂度。 71 | - 服务可以在不会影响全局的情况下内部进行更改或者替换。 72 | 73 | ### 微服务和 SOA 对比 74 | 75 | SOA:面向服务架构 76 | - 将进程按照不同的功能单元进行抽象,拆分为『服务』 77 | - SOA 还为服务之间的通信定义了标准,保证各个服务之间通讯体验的一致性 78 | - 优点 79 | 各服务的职责更清晰 80 | 爆照半径可控 81 | 82 | - 缺点 83 | ESB (企业服务总线) 往往需要一整套解决方案 84 | 85 | 86 | 微服务: 87 | - SOA分布式演进的分支的最终形态 88 | - 优点 89 | 兼具 SOA 解决的问题 90 | 服务间的通信更敏捷、灵活 91 | 92 | - 缺点 93 | 运维成本 94 | 95 | 96 | 二者对比 97 | 1. 服务粒度 98 | 1. SOA粗,微服务更细 99 | 100 | 2. 服务通信 101 | 1. SOA采用 ESB 作为服务间通信的关键组件,负责服务定义、服务路由、消息转换、消息传递,一般情况下都是重量级的实现 102 | 2. 微服务则使用统一的协议和格式,例如:HTTP RESTful 协议、TCP RPC 协议,不需要 ESB 这样的重量级实现 103 | 104 | 3. 服务交付 105 | 1. SOA 更加适合于庞大、复杂、异构的企业级系统 106 | 107 | 108 | -------------------------------------------------------------------------------- /network/README.md: -------------------------------------------------------------------------------- 1 | # 计算机网络 2 | ## OSI七层模型及其作用 3 | 4 | 1. 物理层:底层数据传输,如网线;网卡标准,**比特流** 5 | 2. 数据链路层:定义数据的基本格式,如何传输,如何标识;如网卡MAC地址,**此时数据是帧** 6 | 3. 网络层:定义IP选址,定义路由功能;**数据称为包** 7 | 4. 传输层:端到端传输数据的基本功能;如 TCP、UDP,**数据为段** 8 | 5. 会话层:控制应用程序之间会话能力;如不同软件数据分发给不同软件 9 | 6. 表示层:数据格式化,基本压缩加密功能。 10 | 7. 应用层:各种应用软件,包括 Web 应用 11 | 12 | ## TCP/IP五层模型 13 | 14 | - 应用层 15 | - 传输层 16 | - 网络层 17 | - 数据链路层 18 | - 物理层 19 | 20 | ## TCP/IP协议分为5/7层原因 21 | 22 | 1. 隔层之间是独立的 23 | 2. 灵活性好 24 | 3. 结构上分隔开 25 | 4. 易于实现与维护 26 | 5. 促进标准化工作 27 | 28 | ## 一个完整HTTP请求过程/输入url显示网页的过程,包含物理设备 29 | 1. 键盘输入URL 30 | 1. 按下键盘,一个用于该键的电流回路闭合,因此电流进入键盘的逻辑电路系统。这个逻辑电路系统检测到该按键的状态变化,将电流信号转换为键盘码值 31 | 2. 键盘控制器得到码值后,将其编码,通过中断请求发送,OS的内核提供的中断处理器进行处理 32 | 3. OS得到输入具体内容 33 | 34 | 2. 解析URL 35 | 1. 按下回车:回车信号被操作系统捕获,解析URL的过程被触发 36 | 1. 判断是url还是搜索关键字 37 | 1. 首先会检测输入的URL的合法性。通过检测协议和主机名来进行合法性检测。如果不是一个合法的URL,浏览器则会将输入视为一个搜索关键字 38 | 39 | 2. 检查HSTS列表(HTTP严格传输安全),包含了只能使用HTTPS来进行访问的网站 40 | 1. 因为用户可能会发送HTTP请求,被服务器拒绝后,才会重新发送HTTPS请求。 41 | 2. 而第一次的HTTP请求并不安全。浏览器内置的HSTS就可以一定程度的解决这个问题 42 | 43 | 3. 转换非ASCII的字符,将其编码 44 | 45 | 3. 访问URL 46 | 1. DNS查询 47 | 1. 检查浏览器缓存、本地Hosts缓存、路由器缓存、本地域名服务器缓存 48 | 2. 访问根域名,依次访问对应的顶级域名,最终获得对应访问目标IP 49 | 2. ARP 50 | 1. IP地址转MAC地址 51 | 52 | 4. Socket通信 53 | 1. 找到确定位置,调用系统函数socket,请求TCP套接字,逐层处理封装 54 | 1. 传输层将TCP请求封装成TCP报文 55 | 2. 网络层主要将上一层生成的TCP报文进行处理,加入一个IP头部。封装成IP报文 56 | 3. 链路层在IP报文外封装一个frame头部,包括了本地网卡的MAC地址以及网关的MAC地址等信息 57 | 4. 物理层主要根据传输介质的不同,将报文转换为各种适于传输的格式 58 | 59 | 5. 三次握手 60 | 61 | 6. TLS/SSL握手 62 | 1. 客户端发送一个Hello消息到服务器端,消息主要用来告知服务器端,客户端使用的TLS的版本,可用的加密算法和压缩算法。 63 | 2. 服务器端返回一个Hello消息到客户端,消息用于确认通信将使用的TLS版本,加密算法和压缩算法。另外还包括服务器的公开证书以及其公钥。 64 | 3. 客户端根据自己信任的CA列表,验证服务器端的证书是否有效。 65 | 4. 如果证书有效,则客户端生成一个伪随机数,并根据伪随机数生成对称密钥,并使用使用服务器端提供的公钥加密它,并发送给服务器。至此TLS握手完成。 66 | 5. 服务器使用私钥解密得到对称密钥,由此双方可以使用该对称密钥进行安全通信 67 | 68 | 69 | 7. HTTP协议 70 | 1. 通过TLS建立安全的TCP连接后,则可以通过HTTP请求来进行数据的传输了 71 | 1. HTTP请求包 72 | 1. 请求类型 73 | 2. 头部放入响应文件类型、是否保持TCP连接、浏览器保持的cookie 74 | 75 | 2. HTTP响应包 76 | 1. 状态码、响应内容编码方式、内容长度 77 | 78 | 8. 四次挥手 79 | 80 | 9. 浏览器解析并渲染 81 | 1. 收到HTTP响应文件后,执行以下操作 82 | 1. 解析HTMPL、CSS、JS 83 | 2. 渲染布局、绘制 84 | 85 | 先后用到的知识:DNS、TCP三次握手、HTTP、TCP四次挥手 86 | 87 | 88 | ### 介绍DNS 89 | 90 | - 域名系统:通过主机名,最终得到该主机名对应的IP地址 91 | - **应用层协议**,传输层采用**UDP** 92 | - 采用UDP原因 93 | - 速度快,只要一个请求和一个应答。且DNS服务器返回的内容不超过512字节,UDP足够使用 94 | - 工作原理 95 | - 用户输入域名时,浏览器先检查自己的缓存中是否 这个域名映射的ip地址,有解析结束 96 | - 若没命中,则检查操作系统缓存(如Windows的hosts)中有没有解析过的结果,有解析结束 97 | - 若无命中,则请求本地域名服务器解析( LDNS) 98 | - 若LDNS没有命中就直接跳到根域名服务器请求解析。根域名服务器返回给LDNS一个 顶级域名服务器地址 99 | - 此时LDNS再发送请求给上一步返回的gTLD( 通用顶级域), 返回这个域名对应的Name Server的地址 100 | - Name Server根据映射关系表找到目标ip,返回给LDNS 101 | - LDNS缓存这个域名和对应的ip, 把解析的结果返回给用户,用户缓存到本地系统缓存中,域名解析过程至此结束 102 | 103 | ### HTTP长连接与短连接区别 104 | 105 | - HTTP/1.0中默认使用短连接。也就是说,客户端和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中断连接 106 | - 从HTTP/1.1起,默认使用长连接,用以保持连接特性 107 | 108 | ### HTTP的请求方法 109 | 110 | - HTTP1.0 定义了三种请求方法: GET, POST 和 HEAD方法(引出GET与POST区别 111 | - HTTP1.1 新增了六种请求方法:OPTIONS、PUT、PATCH、DELETE、TRACE 和 CONNECT 方法 112 | 113 | #### GET与POST区别 114 | 115 | - get是获取数据,post是修改数据 116 | - get把请求的数据放在url上,post把数据放在HTTP的包体内 117 | - get提交的数据最大是2k( 限制实际上取决于浏览器), post理论上没有限制 118 | - GET产生一个TCP数据包,浏览器会把http header和data一并发送出去,服务器响应200(返回数据) 119 | - POST产生两个TCP数据包,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据) 120 | - GET请求会被浏览器主动缓存,而POST不会,除非手动设置 121 | 122 | ### TCP三次握手(重点) 123 | - 图示 124 | - ![](img/1.png) 125 | 126 | 127 | #### 为什么三次握手 128 | 129 | 若使用两次握手,则出现以下: 130 | 131 | - 客户端发出连接请求,但因连接请求报文丢失而未收到确认,于是客户端再重传一次连接请求 132 | - 客户端收到了确认,建立了连接。数据传输完毕后,就释放了连接,客户端共发出了两个连接请求报文段,其中第一个丢失,第二个到达了服务端 133 | 134 | 第一个丢失的报文段只是在**某些网络结点长时**间滞留了,延误到连接释放以后的某个时间才到达服务端,此时服务端误认为客户端又发出一次新的连接请求,于是就向客户端发出确认报文段,同意建立连接,不采用三次握手,只要服务端发出确认,就建立新的连接了,此时客户端忽略服务端发来的确认,也不发送数据,则服务端一致等待客户端发送数据,浪费资源 135 | 136 | #### 半连接队列域全连接队列 137 | 138 | - 服务器第一次收到客户端的 SYN 之后,就会处于 SYN_RCVD 状态,此时双方还没有完全建立其连接,服务器会把此种状态下请求连接放在一个**队列**里,我们把这种队列称之为**半连接队列** 139 | - **全连接队列**,就是已经完成三次握手,建立起连接的就会放在全连接队列中。如果队列满了就有可能会出现丢包现象 140 | 141 | 142 | 服务端收到第三次握手的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到 accept 队列 143 | 144 | #### 三次握手可否携带数据 145 | 146 | 第三次握手的时候,是可以携带数据的。但是,**第一次、第二次握手不可以携带数据** 147 | 148 | 第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据。因为攻击者根本就不理服务器的接收、发送能力是否正常,然后疯狂着重复发 SYN 报文的话,这会让服务器花费很多时间、内存空间来接收这些报文 149 | 150 | **第一次握手不可以放数据,其中一个简单的原因就是会让服务器更加容易受到攻击了。而对于第三次的话,此时客户端已经处于ESTABLISHED** 状态。对于客户端来说,他已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据 151 | 152 | ### TCP四次挥手 153 | 图示: 154 | 155 | ![](img/2.png) 156 | 157 | 1. 第一次挥手:**告诉服务端,客户端所有数据已经全发完了**,**服务端你可以关闭接收了**,但是如果你们服务端有数据要发给客户端,客户端照样可以接收的 158 | 2. 第二次挥手:服务端接收到客户端的释放请求连接之后,**知道客户端没有数据要发给自己了**,**然后服务端发送**ACK = 1告诉客户端收到你发给我的信息**,此时服务端处于 CLOSE_WAIT 等待关闭状态 159 | 3. 第三次挥手:服务端向客户端把所有的数据发送完了,然后发送一个FIN = 1,**用于告诉客户**端,服务端的所有数据发送完毕**,**客户端你也可以关闭接收数据连接了**。此时服务端状态处于LAST_ACK状态,来等待确认客户端是否收到了自己的请求 160 | 4. 第四次挥手:如果客户端收到了服务端发送完的信息之后,就发送ACK = 1,告诉服务端,客户端已经收到了你的信息。**有一个** **2 MSL** **的延迟等待** 161 | 1. 意义:保证客户端发送的最后一个ACK报文段能够到达服务端。这个ACK报文段有可能丢失,使得处于LAST-ACK状态的B收不到对已发送的FIN+ACK报文段的确认。客户端在2MSL时间内收到这个重传的FIN+ACK报文段,接着客户端重传一次确认,重新启动2MSL计时器,最后客户端和服务端都进入到CLOSED状态 162 | 2. 使本连接持续的时间内所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现这种旧的连接请求报文段 163 | 164 | #### 为什么四次挥手 165 | 166 | 当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中**ACK报文是用来应答的,SYN报文是用来同步的**。 167 | 168 | 关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端,"你发的FIN报文我收到了"。只有等到我服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四次挥手 169 | 170 | 171 | ### TCP为什么采用随机初始序列号 172 | 173 | 这样做主要是出于网络安全的因素着想。如果不是随机产生初始序列号,黑客将会以很容易的方式获取到你与其他主机之间通信的初始化序列号,并且伪造序列号进行攻击,这已经成为一种很常见的网络攻击手段。 174 | 175 | #### 为什么Time_WAIT需要等待2MSL(报文最大生存时间)才能返回到close状态 176 | 177 | - **TIME_WAIT 状态就是用来重发可能丢失的 ACK 报文** 178 | - 原因是,担心网络不可靠而导致的丢包,最后一个回应 B 的 ACK 万一丢了怎么办,在这个时间内,A 是可以重新发包的。另一方接收到 FIN 后,会重发 ACK 给被动关闭方, 一来一去正好 2 个 MSL。 179 | 180 | 为什么不是4或8MSL? 181 | 182 | 183 | 一个丢包率达到百分之一的糟糕网络,连续两次丢包的概率只有万分之一,这个概率实在是太小了,忽略它比解决它更具性价比。 184 | #### 服务器出现大量close_wait,原因、解决方法 185 | 186 | close_wait状态是在TCP四次挥手的时候收到FIN但是没有发送自己的FIN时出现的 187 | 188 | 原因: 189 | 190 | 1. 服务器内部业务处理占用了过多时间,都没能处理完业务;或者还有数据需要发送;或者服务器的业务逻辑有问题,没有执行close()方法 191 | 2. 服务器的父进程派生出子进程,子进程继承了socket,收到FIN的时候子进程处理但父进程没有处理该信号,导致socket的引用不为0无法回收 192 | 193 | 解决方案: 194 | 195 | 1. 停止应用程序 196 | 2. 修改程序里的bug 197 | 198 | ## TCP粘包/拆包及其原因 199 | 200 | - 一个完整的业务可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这个就是TCP的拆包和粘包问题 201 | - 粘包:发送方发送数据包的长度和接收方在缓存中读取的数据包长度不一致,就会发生粘包,发送端可能堆积了两次数据,每次100字节一共在发送缓存堆积了200字节的数据,而接收方在接收缓存中一次读取120字节的数据,这时候接收端读取的数据中就包括了下一个报文段的头部,造成了粘包。 202 | - 原因 203 | - 应用程序写入数据的字节大小大于套接字发送缓冲区的大小 204 | - 进行MSS大小的TCP分段。( MSS=TCP报文段长度-TCP首部长度) 205 | - 以太网的payload大于MTU进行IP分片 206 | - 解决 207 | - 消息定长 208 | - 包尾部增加回车或者空格符等特殊字符进行分割 209 | - 将消息分为消息头和消息尾 210 | 211 | 212 | ## TCP的socket编程 213 | 214 | 1. 服务端和客户端初始化 socket,得到文件描述符; 215 | 2. 服务端调用 bind,将绑定在 IP 地址和端口; 216 | 3. 服务端调用 listen,进行监听; 217 | 4. 服务端调用 accept,等待客户端连接; 218 | 5. 客户端调用 connect,向服务器端的地址和端口发起连接请求; 219 | 6. 服务端 accept 返回用于传输的 socket 的文件描述符; 220 | 7. 客户端调用 write 写入数据;服务端调用 read 读取数据; 221 | 8. 客户端断开连接时,会调用 close,那么服务端 read 读取数据的时候,就会读取到了 EOF,待处理完数据后,服务端调用 close,表示连接关闭。 222 | 223 | 224 | ## HTTP与HTTPS区别 225 | 226 | 分析,此问题引出ssl/tls和证书等相关安全知识 227 | 228 | Http缺点 229 | 230 | 1. 使用明文进行通信,内容可能会被窃听 231 | 2. 不验证通信方的身份,通信方的身份有可能遭遇伪装 232 | 3. 无法证明报文的完整性,报文有可能遭篡改 233 | 234 | 区别: 235 | 236 | 1. HTTP协议传输的数据都是未加密的,也就是明文的; 237 | 2. HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,更加安全 238 | 3. https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用 239 | 4. http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443 240 | 241 | ### 什么是SSL/TLS 242 | 243 | - SSL代表安全套接字层,用于加密和验证应用程序(如浏览器)和Web服务器之间发送的数据的协议 244 | - 作用:认证用户和服务,加密数据,维护数据的完整性的应用层协议加密和解密需要两个不同的密钥,故被称为非对称加密;加密和解密都使用同一个密钥的 245 | 246 | 247 | ### HTTPS如何保证数据传输的安全(四次握手) 248 | 249 | - 客户端向服务器端发起SSL连接请求,包含自己支持的SSL/TLS协议版本、自己生成的随机数(作为生成会话密钥条件之一)、支持密码套机列表 250 | - 服务器响应客户端,包含确认SSL/TLS版本、服务器产生的随机数,确认的密码套机列表、数字证书 251 | - 客户端收到响应,通过浏览器或者操作系统中的 CA 公钥,确认服务器的数字证书的真实性;验证通过,从证书取出公钥,加密报文,发送以下信息 252 | - 随机数,被服务器公钥加密 253 | - 加密通信算法改变通知,随后的信息都将用「会话秘钥」加密通信 254 | - 客户端握手结束通知,表示客户端的握手阶段已经结束。这一项同时把之前所有内容的发生的数据做个摘要,用来供服务端校验 255 | - 服务器收到随机数,计算本次的会话密钥,向客户端发送最后的信息 256 | - 加密通信算法改变通知,表示随后的信息都将用「会话秘钥」加密通信 257 | - 服务器握手结束通知,表示服务器的握手阶段已经结束 258 | - 客户端与服务器进入加密通信,就完全是使用普通的 HTTP 协议,只不过用「会话秘钥」加密内容 259 | 260 | ### 如何保证公钥不被篡改 261 | 262 | 1. 公钥放在数字证书中。只要证书是可信的,公钥就是可信的。 263 | 2. 证书: 264 | 1. 服务器的运营人员向数字认证机构 CA 提出公开密钥的申请,CA 在判明提出申请者的身份之后,会对已申请的公开密钥做数字签名,然后分配这个已签名的公开密钥,并将该公开密钥放入公开密钥证书后绑定在一起 265 | 2. 进行 HTTPS 通信时,服务器会把证书发送给客户端。客户端取得其中的公开密钥之后,先使用数字签名进行验证,如果验证通过,就可以开始通信了 266 | 267 | 268 | ## TCP头部控制字段 269 | 270 | ACK:确认序号有序 271 | SYN:发起链接 272 | FIN:释放一个链接 273 | RST:重建一个链接 274 | 275 | ## HTTP请求与响应报文有哪些主要字段 276 | 277 | - 请求 278 | 1. 请求行:包含请求方法、请求目标、版本号 279 | 2. 请求头 280 | 1. Content-Type:请求体的多媒体类型 (用于POST和PUT请求中) 281 | 2. Cookie:由服务器通过 Set- Cookie(下文详述)发送的一个 超文本传输协议Cookie 282 | 3. host:服务器的域名,以及服务器所监听的传输控制协议端口号 283 | 3. 空行 284 | 4. 请求体 285 | - 响应 286 | 1. 状态行:版本号、状态码、原因 287 | 2. 响应头 288 | 3. 响应体 289 | 290 | 291 | HTTP/1.1 里唯一要求必须提供的头字段是 Host,它必须出现在请求头里,标记虚拟主机名 292 | 293 | ## cookie是什么 294 | 295 | - HTTP是无状态协议,引入cookie保存状态信息 296 | - 是**服务器发送到用户浏览器并保存在本地的一小块数据**,它会在浏览器之后向同一服务器再次发起请求时被携带上,用于告知服务端两个请求是否来自同一浏览器 297 | 298 | ## session是什么 299 | 300 | - 存储在服务器端/数据库、文件、内存等 301 | - 维护登录状态的过程 302 | - 用户进行登录时,用户提交包含用户名和密码的表单,放入 HTTP 请求报文中 303 | - 服务器验证该用户名和密码,如果正确则把用户信息存储到 Redis 中,它在 Redis 中的 Key 称为Session ID 304 | - 服务器返回的响应报文的 Set-Cookie 首部字段包含了这个 Session ID,客户端收到响应报文之后将该Cookie 值存入浏览器中 305 | - 客户端之后对同一个服务器进行请求时会包含该 Cookie 值,服务器收到之后提取出 Session ID,从Redis 中取出用户信息,继续之前的业务操作 306 | 307 | ### cookie与session区别 308 | 309 | 1. Cookie是客户端保持状态的方法;Session是服务器保持状态的方法 310 | 311 | 2. 存储数据大小限制不同 312 | 313 | 1. cookie:大小受浏览器的限制,很多是是4K的大小, 314 | 2. session:理论上受当前内存的限制 315 | 316 | 3. 使用场景 317 | 318 | 1. Cookie 只能存储 ASCII 码字符串,而 Session 则可以存储任何类型的数据,因此在考虑数据复杂性时首选 Session 319 | 2. 对于大型网站,如果用户所有的信息都存储在 Session 中,那么开销是非常大的,因此不建议将所有的用户信息都存储到 Session 中 320 | 3. Cookie 存储在浏览器中,容易被恶意查看。如果非要将一些隐私数据存在 Cookie 中,可以将 Cookie 值进行加密,然后在服务器进行解密 321 | 322 | 323 | ## ARP 324 | 325 | 实现将IP地址解析为MAC地址 326 | 327 | 过程: 328 | 329 | 1. Host_1会查找自己本地缓存的ARP表,确定是否包含Host_3对应的ARP表项。如果有,帧封装发送;如果没有,转到2 330 | 2. 先缓存该数据报文,并以广播方式发送一个ARP请求报文。包含源MAC地址与源IP地址,目的MAC为0的地址和目的IP地址 331 | 3. 被请求主机处理该报文,将ARP请求报文中的源IP地址和源MAC地址存入自己的ARP表中,以单播方式发送ARP应答报文给Host_1 332 | 4. Host_1收到应答报文,其中MAC地址加入到自己的ARP表中以用于后续报文的转发,同时将数据报文进行帧封装,并将数据报文发送给目的主机 333 | 334 | ## 什么是RARP 335 | 336 | 1. 反向地址转换协议,网络层协议,RARP与ARP工作方式相反 337 | 2. RARP发出要反向解释的物理地址并希望返回其IP地址,应答包括能够提供所需信息的RARP服务器发出的IP地址 338 | 339 | 340 | 341 | ## DDos攻击 342 | 343 | - 客户端向服务端发送请求链接数据包,服务端向客户端发送确认数据包,客户端不向服务端发送确认数据包,服务器一直等待来自客户端的确认 344 | - 无法根治,除非不使用TCP 345 | - 预防 346 | - 限制同时打开SYN半链接的数目 347 | - 缩短SYN半链接的Time out 时间 348 | 349 | ## 应用层常见协议及其端口 350 | 351 | | 协议 | 默认端口 | 底层协议 | 352 | | ----- | -------- | ----------------------------------------------- | 353 | | HTTP | 80 | TCP | 354 | | HTTPS | 443 | TCP | 355 | | DNS | 53 | 服务器进行域传输用TCP,客户端查询DNS服务器用UDP | 356 | 357 | 358 | ## 对称加密域非对称加密 359 | 360 | 1. 对称密钥加密:加密和解密使用同一密钥 361 | 1. 运算速度快 362 | 2. 无法安全地将密钥传输给通信方 363 | 2. 非对称加密:**通信发送方获得接收方的公开密钥之后,就可以使用公开密钥进行加密**,**接收方收到通信内容后使用私有密钥解密** 364 | 365 | ## 网络层协议 366 | 367 | | 协议 | 作用 | 368 | | ---------------------- | -------------------------------------------------------- | 369 | | IP | 定义数据传输基本单元与格式,定义数据报递交方法和路由选择 | 370 | | ICMP(网络控制报文协议 | 检测网络的连线情况,是ping和traceroute工作协议 | 371 | 372 | 373 | ## SYN Flood,如何解决 374 | 375 | 客户端发送三次握手的第一个 SYN 报文后收到服务器的报文却不回应,从而导致服务器的半开资源浪费直到超时释放 376 | 377 | 服务器使用SYN-ACK数据包对每个连接请求进行响应,然后从积压中删除SYN请求,从存储器中删除请求并使端口打开,准备建立新的连接。如果连接是合法请求,并且最终的ACK数据包从客户端计算机发送回服务器,则服务器将重建(有一些限制)SYN积压队列条目 378 | 379 | 380 | ## TCP四大拥塞控制算法 381 | 382 | 1. 慢启动 383 | - 连接建好的开始先初始化拥塞窗口cwnd大小为1,表明可以传一个MSS(最大分节字节)大小的数据 384 | - 每当过了一个往返延迟时间RTT(Round-Trip Time),cwnd大小直接翻倍,乘以2,呈指数让升 385 | - 阻止拥塞窗口cwind的增长引起网络拥塞,还需要另外一个变量—慢开始门限ssthresh(slow start threshold),是一个上限,当cwnd >= ssthresh时,就会进入“拥塞避免算法” 386 | 2. 拥塞避免算法 387 | - 收到一个ACK,则cwnd = cwnd + 1 / cwnd,线性增加 388 | - 每当过了一个往返延迟时间RTT,cwnd大小加一 389 | 3. 判断拥塞发送状态 390 | - 超时重传 391 | - 受到三个重复确认ACK 392 | - 两次ACK可能是因为乱序造成 393 | - 具体解释 394 | - 以A方发送,B方接收报文为例。观察其中第N-1个报文到达B方后,B方的答复。B收到第N-1报文后,就会答复1个ACK号为N的ACK报文,此时如果收不到序列号为N的报文,则会继续发送第2个ACK号为N的答复报文,此为重复ACK。如果一直收不到对方序列号为N的报文,则会出现三次冗余的ACK,则意味着很大可能出现丢包了 395 | - 三次重复ACK可能是丢包 396 | 4. 快速恢复 397 | - 在进入快速恢复之前,cwnd和ssthresh已经被更改为原有cwnd的一半 398 | - cwnd = cwnd + 3 *MSS*,加*3* MSS的原因是因为收到3个重复的ACK 399 | - 重传ACKs指定的数据包 400 | - 如果再收到ACKs,那么cwnd大小增加一 401 | - 如果收到新的ACK,表明重传的包成功了,那么退出快速恢复算法。将cwnd设置为ssthresh,然后进入拥塞避免算法 402 | 403 | ## TCP协议如何保证可靠传输 404 | 405 | - **确认和重传**:接收方收到报文就会确认,发送方发送一段时间后没有收到确认就会重传 406 | - **数据校验**:TCP报文头有校验和,用于校验报文是否损坏 407 | - **数据合理分片和排序**:tcp会按最大传输单元(MTU)1460字节合理分片,接收方会缓存未按序到达的数据,重新排序后交给应用层 408 | - **流量控制**:当接收方来不及处理发送方的数据,能通过滑动窗口,提示发送方降低发送的速率,防止包丢失 409 | - **拥塞控制**:当网络拥塞时,通过拥塞窗口,减少数据的发送,防止包丢失 410 | 411 | ## UDP协议如何保证可靠传输 412 | 413 | 应用层模仿TCP的可靠性传输 414 | 415 | - 1、添加seq/ack机制,确保数据发送到对端 416 | - 2、添加发送和接收缓冲区,主要是用户超时重传。 417 | - 3、添加超时重传机制。 418 | 419 | 目前有如下开源程序利用udp实现了可靠的数据传输。分别为 **RUDP、RTP、UDT** 420 | 421 | UDT(基于UDP的数据传输协议)建于UDP之上,并引入新的拥塞控制和数据可靠性控制机制。UDT是面向连接的双向的应用层协议。它同时支持可靠的数据流传输和部分可靠的数据报传输 422 | 423 | ## TCP封包和拆包 424 | 425 | - 封包和拆包都是基于TCP的概念。因为TCP是无边界的流传输,所以需要对TCP进行封包和拆包,确保发送和接收的数据不粘连 426 | - 封包就是在发送数据报的时候为每个TCP数据包加上一个包头 427 | - 拆包:接收方在接收到报文后提取包头中的长度信息进行截取 428 | 429 | ## TCP、UDP相关特点 430 | 431 | UDP: 432 | 433 | - UDP是**无连接的**; 434 | - UDP使用**尽最大努力交付**,即不保证可靠交付,因此主机不需要维持复杂的链接状态 435 | - UDP是**面向报文**的 436 | - UDP**没有拥塞控制** 437 | - UDP**支持一对一、一对多、多对一和多对多**的交互通信 438 | - UDP的**首部开销小**,只有8个字节,比TCP的20个字节的首部要短 439 | 440 | TCP: 441 | 442 | - **TCP是面向连接的**。 443 | - 每一条TCP连接只能有两个端点,每一条TCP连接只能是点对点的(**一对一**) 444 | - TCP**提供可靠交付的服务** 445 | - TCP**提供全双工通信** 446 | - **面向字节流** 447 | 448 | ## TCP与UDP各自对应的应用层协议 449 | 450 | - TCP 451 | 1. FTP:文件传输协议,21端口传输,20端口控制哦 452 | 2. Telnet:远程登录协议,23端口 453 | 3. SMTP:简单邮件传输协议,25端口 454 | 4. POP3:接收邮件 455 | 4. SSH:22端口 456 | - UDP 457 | 1. DNS:域名解析服务,53端口 458 | 2. SNMP:简单网络管理协议,161端口 459 | 460 | ## RTO、RTT与超时重传是什么 461 | 462 | 1. 超时重传:发送端发送报文后若长时间未收到确认的报文则需要重发该报文 463 | 1. 发送的数据没能到达接收端,所以对方没有响应 464 | 2. 接收端接收到数据,但是ACK报文在返回过程中丢失 465 | 3. 接收端拒绝或丢弃数据 466 | 2. RTO:从上一次发送数据,因为长期没有收到ACK响应,到下一次重发之间的时间。就是重传间隔 467 | 3. RTT:数据从发送到接收到对方响应之间的时间间隔,即数据报在网络中一个往返用时 468 | 469 | ## 常见HTTP状态码 470 | 471 | - 1xx:提示信息,协议处理中间状态 472 | - 2xx:服务器收到并成功处理客户端请求 473 | - 3xx:客户端请求的资源发生了变动,客户端必须用新的 URI 重新发送请求获取资源 474 | - 4xx:客户端发送的请求报文有误,服务器无法处理 475 | - 5xx:服务器在处理时内部发生了错误,无法返回应有的响应数据 476 | 477 | **100 continue**:表明到目前为止都很正常,客户端可以继续发送请求或者忽略这个响应 478 | 479 | **200 OK**:服务器如客户端所期望的那样返回了处理结果,如果是非 HEAD 请求,通常在响应头后都会有 body 数据 480 | 481 | **201 CREATED**: 已创建。成功请求并创建了新的资源 482 | 483 | **202 ACCEPTED**:已经接受请求,但未处理完成 484 | 485 | **204 No Context**:请求已经成功处理,但是返回的响应报文不包含实体的主体部分 486 | 487 | **300 Multiple Choices**:请求的资源可包括多个位置,相应可返回一个资源特征与地址的列表用于用户终端 488 | 489 | **301 Moved Permanently**:永久性重定向,改用新的 URI 再次访问 490 | 491 | **302 Found** :资源只是临时被移动。客户端应继续使用原有URI 492 | 493 | **304 Not Modified**:缓存重定向,也就是告诉客户端可以继续使用缓存资源,用于缓存控制。 494 | 495 | **400 Bad Request** :请求报文中存在语法错误 496 | 497 | **401 Unauthorized** :该状态码表示发送的请求需要有认证信息(BASIC 认证、DIGEST 认证)。如果之前已进行过一次请求,则表示用户认证失败 498 | 499 | **403 Forbidden** :请求被拒绝 500 | 501 | **404 Not Found**:资源在本服务器上未找到,所以无法提供给客户端 502 | 503 | **500 Internal Server Error** :服务器正在执行请求时发生错误 504 | 505 | **501 Not Implemented**:服务器不支持请求功能,无法完成请求 506 | 507 | **502 Bad Gateway**:作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应 508 | 509 | **503 Service Unavailable** :服务器暂时处于超负载或正在进行停机维护,现在无法处理请求 510 | 511 | 512 | ## 有IP地址,为什么还用MAC地址 513 | 514 | 1.IP地址是有限的,根本就不够用,不可能为全球每台计算机都分配一个IP地址。 515 | 2.MAC地址全球固定而且唯一的,有了MAC地址就能准确的找到你的计算。 516 | 3.如果IP层抢了第二层的饭碗,你就不得不考虑第二层的很多东西了,这就让IP层的实现变得十分困难 517 | 518 | ## 有MAC地址,还需要IP地址吗 519 | 520 | Mac地址是物理层的地址,但它是以太网的物理地址。互联网是由很多异构的物理网络通过路由器联接起来的,不同的物理网络,寻址方式很可能是不同的,可能根本不使用MAC地址。如果只用MAC地址,不同的物理网络进行寻址时会非常困难,因为彼此的数据帧格式不一样相互不兼容。所以,我们需要一个公用的标准去遵循,这个标准就是IP地址 521 | 522 | ## SYN攻击 523 | 524 | SYN攻击就是Client在短时间内伪造大量不存在的IP地址,并向Server不断地发送SYN包,Server则回复确认包,并等待Client确认,由于源地址不存在,因此Server需要不断重发直至超时,这些伪造的SYN包将长时间占用未连接队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网络拥塞甚至系统瘫痪 525 | 526 | netstats -n -p TCP | grep SYN_RECV检测 527 | 528 | 防御“ 529 | 530 | - 缩短超时时间 531 | - 增加最大半连接数 532 | - 过滤网关防护 533 | - SYN cookies技术 534 | 535 | 536 | ## 一个TCP连接中,HTTP请求发送可以一起发送吗 537 | 538 | - HTTP/1.0 存在一个问题,单个 TCP 连接在同一时刻只能处理一个请求,任意两个 HTTP 请求从开始到结束的时间在同一个 TCP 连接里不能重叠 539 | - HTTP/1.1 存在 Pipelining 技术可以完成这个多个请求同时发送,但是由于浏览器默认关闭,所以可以认为这是不可行的 540 | - 通过以下提高页面加载效率 541 | - 维持和服务器已经建立的 TCP 连接,在同一连接上顺序处理多个请求 542 | - 和服务器建立多个 TCP 连接 543 | - 在 HTTP2 中由于 Multiplexing 特点的存在,多个 HTTP 请求可以在同一个 TCP 连接中并行进行 544 | 545 | ## HTTP 1.0/1.1/2.0/3.0区别 546 | 547 | - HTTP 1.0 548 | 549 | - 无状态,无连接的应用层协议。 HTTP1.0规定浏览器和服务器保持短暂的链接 550 | - 每次请求都需要与服务器建立一个TCP连接,服务器处理完成以后立即断开TCP连接(无连接),服务器不跟踪也每个客户单,也不记录过去的请求(无状态) 551 | - 问题: 552 | - 无法复用连接 553 | - TCP 连接的新建成本很高,因为需要客户端和服务器三次握手,并且开始时发送速率较慢(slow start) 554 | 555 | - HTTP 1.1 556 | 557 | - 保持HTTP连接不断 558 | - 可以使用管道传输,多个请求可以同时发送,但是服务器还是按照顺序,先回应 A 请求,完成后再回应 B 请求。要是 前面的回应特别慢,后面就会有许多请求排队等着。这称为**「HTTP层队头堵塞」** 559 | 560 | - HTTP 2.0 561 | 562 | - 多路复用(链接共享)— 真并行传输 563 | - 同域名下所有通信在单个连接(流)上完成,该连接可用承载任意数量的双向数据流 564 | - 所有HTTP2.0通信都在一个TCP链接上完成,这个链接可以承载任意流量的双向数据流 565 | - **数据流**以消息发送,消息由多个帧组成 566 | - 多路复用(连接共享)可能会导致关键字被阻塞,HTTP2.0里每个数据流都可以设置优先级和依赖,优先级高的数据流会被服务器优先处理和返回客户端,数据流还可以依赖其他的子数据流 567 | - 头部压缩 568 | - 服务器和客户端之间建立哈希表,将用到的字段存放在这张表中,那么在传输的时候对于之前出现过的值,只需要把**索引**(比如0,1,2,...)传给对方即可,对方拿到索引查表 569 | - 于整数和字符串进行**哈夫曼编码**,哈夫曼编码的原理就是先将所有出现的字符建立一张索引表,然后让出现次数多的字符对应的索引尽可能短,传输的时候也是传输这样的**索引序列**,可以达到非常高的压缩率 570 | 571 | - 一旦发送丢包,阻塞所有HTTP请求,TCP层队头阻塞 572 | 573 | - HTTP 3.0 574 | - 基于QUIC(传输层协议)实现 575 | - 引入了类似 HTTP/2 的“流”和“多路复用”,单个“流”是有序的,可能会因为丢包而阻塞,但其他“流”不会受到影响 576 | - 基于udp实现的 577 | - 初始的数据包建立连接之后,连接发起者会马上发一个加密的帧以开始安全层握手。安全层使用TLS 1.3协议 578 | - 内含了 TLS1.3,只能加密通信,支持 0-RTT 快速建连 579 | - 先前已连接过一个服务器的客户端可能缓存来自该连接的某些参数,并在之后与该服务器建立一个无需等待握手完成就可以立即传输信息的0-RTT连接,从而减少建立新连接所必需的时间 580 | - 连接使用“不透明”的连接 ID,不绑定在“IP 地址 + 端口”上,支持“连接迁移” 581 | 582 | - 当某个流发生丢包时,只会阻塞这个流,其他流不会受到影响,因此不存在队头阻塞问题 583 | 584 | - 什么是0-RTT建连? 585 | 586 | - 传输层0-RTT就能建立连接 587 | - 加密层0-RTT就能建立加密连接 588 | 589 | - 缓存当前会话的上下文,下次恢复会话的时候,只需要将之前的缓存传递给服务器,验证通过,就可以进行传输了 590 | 591 | 减少了tcp三次握手时间,以及tls握手时间; 592 | 593 | 解决了http 2.0中前一个stream丢包导致后一个stream被阻塞的问题; 594 | 595 | 优化了重传策略,重传包和原包的编号不同,降低后续重传计算的消耗; 596 | 597 | 连接迁移,不再用tcp四元组确定一个连接,而是用一个64位随机数来确定这个连接; 598 | 599 | 更合适的流量控制。 600 | 601 | ## TCP流量控制原理(滑动窗口) 602 | 603 | - 目的是接收方通过TCP头窗口字段告知发送方本方可接收的最大数据量,用以解决发送速率过快导致接收方不能接收的问题。所以流量控制是点对点控制 604 | - TCP是双工协议,双方可以同时通信,所以发送方接收方各自维护一个发送窗和接收窗 605 | - 发送窗:用来限制发送方可以发送的数据大小,其中发送窗口的大小由接收端返回的TCP报文段中窗口字段来控制,接收方通过此字段告知发送方自己的缓冲(受系统、硬件等限制)大小 606 | - 接收窗:用来标记可以接收的数据大小 607 | - TCP是流数据,发送出去的数据流可以被分为以下四部分: 608 | - 已发送且被确认部分 | 609 | - 已发送未被确认部分 | 610 | - 未发送但可发送部分 | 611 | - 不可发送部分 612 | - 其中发送窗 = 已发送未确认部分 + 未发但可发送部分。接收到的数据流可分为:已接收 | 未接收但准备接收 | 未接收不准备接收。接收窗 = 未接收但准备接收部分 613 | - 发送窗内数据只有当接收到接收端某段发送数据的ACK响应时才移动发送窗,左边缘紧贴刚被确认的数据。 614 | - 接收窗也只有接收到数据且最左侧连续时才移动接收窗口 615 | 616 | ## 流量控制与拥塞控制区别 617 | 618 | 流量控制是**端到端**的控制,例如A通过网络给B发数据,A发送的太快导致B没法接收(B缓冲窗口过小或者处理过慢),这时候的控制就是流量控制,原理是通过滑动窗口的大小改变来实现。 619 | 拥塞控制是A与B之间的网络发生堵塞导致传输过慢或者丢包,来不及传输。防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不至于过载。拥塞控制是一个全局性的过程,**涉及到所有的主机、路由器,以及与降低网络性能有关的所有因素** 620 | 621 | 622 | 623 | ## HTTP特点及其缺点 624 | 625 | HTTP 的特点概括如下: 626 | 627 | 1. 灵活可扩展,主要体现在两个方面。一个是语义上的自由,只规定了基本格式,比如空格分隔单词,换行分隔字段,其他的各个部分都没有严格的语法限制。另一个是传输形式的多样性,不仅仅可以传输文本,还能传输图片、视频等任意数据,非常方便。 628 | 2. 可靠传输。HTTP 基于 TCP/IP,因此把这一特性继承了下来。这属于 TCP 的特性,不具体介绍了。 629 | 3. 请求-应答。也就是`一发一收`、`有来有回`, 当然这个请求方和应答方不单单指客户端和服务器之间,如果某台服务器作为代理来连接后端的服务端,那么这台服务器也会扮演**请求方**的角色。 630 | 4. 无状态。这里的状态是指**通信过程的上下文信息**,而每次 http 请求都是独立、无关的,默认不需要保留状态信息。 631 | 632 | HTTP缺点: 633 | 634 | 1. 无状态: 635 | 1. 在需要长连接的场景中,需要保存大量的上下文信息,以免传输大量重复的信息,那么这时候无状态就是 http 的缺点 636 | 2. 外一些应用仅仅只是为了获取一些数据,不需要保存连接上下文信息,无状态反而减少了网络开销,成为了 http 的优点 637 | 2. 明文传输 638 | 1. 协议里的报文(主要指的是头部)不使用二进制数据,而是文本形式 639 | 2. `WIFI陷阱`就是利用 HTTP 明文传输的缺点,诱导你连上热点,然后疯狂抓你所有的流量,从而拿到你的敏感信息 640 | 3. 队头阻塞 641 | 1. 当 http 开启长连接时,共用一个 TCP 连接,同一时刻只能处理一个请求,那么当前请求耗时过长的情况下,其它的请求只能处于阻塞状态,也就是著名的**队头阻塞**问题 642 | 643 | ## HTTP1.1如何解决HTTP的队头阻塞问题 644 | 645 | - 并发连接:允许客户端最多并发2个连接,在现在浏览器标准中,上限更高 646 | - 域名分片:一个`sanyuan.com`域名下可以分出非常多的二级域名,而它们都指向同样的一台服务器,能够并发的长连接数更多了,事实上也更好地解决了队头阻塞的问题 647 | 648 | 649 | 650 | ## 负载均衡 651 | 652 | 负载均衡:将工作负载分布到多个服务器来提高网站、应用、数据库或其他服务的性能和可靠性 653 | 654 | 在后端引入一个负载均衡器和至少一个额外的 web 服务器,可以缓解这个故障。通常情况下,所有的后端服务器会保证提供相同的内容,以便用户无论哪个服务器响应,都能收到一致的内容。 655 | 656 | ### 4层负载均衡 657 | 658 | 因其工作在OSI模型的传输层 659 | 660 | 基于IP+端口 661 | 662 | #### 调度算法 663 | 664 | 1. 所有请求平均分配到每个真实服务器 665 | 2. 给每个后端服务器一个权值比例,将请求按照比例分配 666 | 3. 把新连接请求分配到当前连接数最小的服务器 667 | 4. 一致性哈希:仅影响故障服务器上连接session 668 | 669 | ### 7层负载均衡 670 | 671 | 七层负载均衡一般是基于请求URL地址的方式进行代理转发 672 | 673 | #### 设备 674 | 675 | - Nginx 676 | 677 | 特点 678 | 679 | 1. 热部署,可以在线升级 680 | 2. 内存消耗低 681 | 3. 事件驱动:异步非阻塞模型,mmap(内存映射) 682 | 1. 一个进程/线程处理多个连接/请求异步非阻塞模型,减少OS进程切换 683 | 684 | 多个客户端给服务器发送的请求,Nginx服务器接收到之后,按照一定的规则分发给了后端的业务处理服务器进行处理了 685 | 686 | 具体哪个服务器不明确 687 | 688 | 689 | ## NAT(网络地址转换) 690 | 691 | 用以解决IPv4空间不足的问题, 692 | 693 | 私网用户访问公网的报文到达网关设备后,如果网关设备上部署了NAT功能,设备会将收到的IP数据报文头中的IP地址转换为另一个IP地址,端口号转换为另一个端口号之后转发给公网 694 | 695 | 设备可以用同一个公网地址来转换多个私网用户发过来的报文,并通过端口号来区分不同的私网用户,从而达到地址复用的目的 -------------------------------------------------------------------------------- /network/img/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/network/img/1.png -------------------------------------------------------------------------------- /network/img/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/network/img/2.png -------------------------------------------------------------------------------- /pattern/README.md: -------------------------------------------------------------------------------- 1 | # 常考设计模式 2 | 3 | 考察不多 4 | ## 单例设计模式 5 | 6 | 保证一个类只有一个实例,并且提供一个访问该全局访问点 7 | 8 | 实现时需要注意以下几点: 9 | 1. 单例类的构造函数为私有 10 | 2. 提供一个自身的静态私有成员变量 11 | 3. 提供一个公有的静态工厂方法 12 | 13 | - 使用场景: 14 | 15 | 网站计数器、应用程序的日志应用 16 | 17 | - 种类,代码见两个cpp 18 | - 懒汉式:获取该类的对象时才创建该类的实例 19 | - 饿汉式:获取该类的对象之前已经创建好该类的实例 20 | 21 | 22 | ## 工厂模式 23 | 24 | ### 简单工厂模式 25 | 26 | 可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类 27 | 28 | 角色: 29 | 1. 公共基类:负责描述所有实例所共有的公共接口 30 | 2. 具体类的实例 31 | 3. 工厂角色:负责创建所有实例内部逻辑 32 | 33 | ### 工厂方法模式 34 | 35 | 将具体按钮的创建过程交给专门的工厂子类去完成,体现C++中多态性。 36 | 37 | 工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象 38 | 39 | 40 | ### 抽象工厂 41 | 42 | 需要生产的多个位于不同产品等级结构中属于不同类型的具体产品 43 | 44 | 一个工厂等级结构可以负责多个不同产品等级结构中的产品对象的创建 45 | - 例如:海尔电器工厂生产的海尔电视机、海尔电冰箱,海尔电视机位于电视机产品等级结构中,海尔电冰箱位于电冰箱产品等级结构中 46 | 47 | 角色: 48 | 1. 抽象工厂用于声明生成抽象产品的方法 49 | 2. 具体工厂实现了抽象工厂声明的生成抽象产品的方法,生成一组具体产品,这些产品构成了一个产品族,每一个产品都位于某个产品等级结构中 50 | 3. 抽象产品接口为每种产品声明接口,在抽象产品中定义了产品的抽象业务方法 51 | 4. 具体产品定义具体工厂生产的具体产品对象,实现抽象产品接口中定义的业务方法 52 | 53 | ### 建造者模式 54 | 55 | 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。 56 | 57 | 1. 抽象建造者为创建一个产品对象的各个部件指定抽象接口 58 | 2. 具体建造者实现了抽象建造者接口,实现各个部件的构造和装配方法 59 | 3. 产品角色是被构建的复杂对象,包含多个组成部件 60 | 4. 指挥者负责安排复杂对象的建造次序 61 | 62 | 通过指挥者类调用建造者的相关方法,返回一个完整的产品对象 63 | 64 | ## 观察者模式 65 | 66 | 观察者模式的作用是:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。 67 | 68 | 1. 目标又称为主题,它是指被观察的对象 69 | 2. 具体目标是目标类的子类,通常它包含有经常发生改变的数据,当它的状态发生改变时,向它的各个观察者发出通知。即Partition中leader 70 | 3. 观察者将对观察目标的改变做出反应,ISR中Follower 71 | 4. 具体观察者中维护一个指向具体目标对象的引用,它存储具体观察者的有关状态,这些状态需要和具体目标的状态保持一致 72 | 73 | 应用场景:消息队列 74 | ## 适配器模式 75 | 76 | 一个类接口转为客户希望的另一个接口,使得原本由于接口不兼容而不能一起工作类一起工作 77 | 78 | 1. 目标抽象类定义客户要用的特定领域的接口 79 | 2. 适配器类可以调用另一个接口,作为一个转换器,对适配者和抽象目标类进行适配 80 | 3. 适配者类是被适配的角色,它定义了一个已经存在的接口,这个接口需要适配 81 | -------------------------------------------------------------------------------- /pattern/懒汉模式.cpp: -------------------------------------------------------------------------------- 1 | //方法调用前,创建好实例 2 | 3 | #include 4 | #include 5 | 6 | 7 | class Singleton 8 | { 9 | private: 10 | static Singleton* instance; 11 | 12 | Singleton(std::string values):value_(values){} 13 | ~Singleton(){} 14 | std::string value_; 15 | 16 | 17 | 18 | public: 19 | Singleton(Singleton& other) = delete; 20 | void operator=(Singleton& other) = delete; 21 | 22 | Singleton* Getinstance(std::string& value){} 23 | }; 24 | 25 | Singleton* Singleton::instance(nullptr); 26 | 27 | Singleton* Singleton::Getinstance(std::string& value){ 28 | instance = new Singleton(value); 29 | return instance; 30 | } 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /pattern/线程安全单例模式-懒汉.cpp: -------------------------------------------------------------------------------- 1 | //懒汉模式 2 | //单例的构造函数/析构函数应该总是私有,避免使用' new ' / ' delete '直接构造/销毁 3 | 4 | 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | 15 | class Singleleton 16 | { 17 | private: 18 | static Singleleton* pininstance; 19 | static std::mutex mutex_; 20 | 21 | protected: 22 | Singleleton(const std::string value):value_(value){} 23 | ~Singleleton(){} 24 | std::string value_; 25 | public: 26 | Singleleton(Singleleton& other) = delete; 27 | void operator=(const Singleleton&) = delete; 28 | 29 | static Singleleton* Getinstance(const std::string& value); 30 | 31 | void SomeBusinessLogic(){ 32 | 33 | } 34 | 35 | std::string value() const{ 36 | return value_; 37 | } 38 | }; 39 | 40 | //静态方法/成员在类外定义或初始化 41 | Singleleton* Singleleton::pininstance{nullptr}; 42 | std::mutex Singleleton::mutex_; 43 | 44 | Singleleton* Singleleton::Getinstance(const std::string& value){ 45 | std::lock_guard lock(mutex_); //构造时调用mutex_lock(),析构时调用mutex_unlock() 46 | if(pininstance==nullptr){ 47 | pininstance = new Singleleton(value); 48 | } 49 | 50 | return pininstance; 51 | } 52 | 53 | 54 | void ThreadFoo(){ 55 | // Following code emulates slow initialization. 56 | std::this_thread::sleep_for(std::chrono::milliseconds(1000)); 57 | Singleleton* singleton = Singleleton::Getinstance("FOO"); 58 | std::cout << singleton->value() << "\n"; 59 | } 60 | 61 | void ThreadBar(){ 62 | // Following code emulates slow initialization. 63 | std::this_thread::sleep_for(std::chrono::milliseconds(1000)); 64 | Singleleton* singleton = Singleleton::Getinstance("BAR"); 65 | std::cout << singleton->value() << "\n"; 66 | } 67 | 68 | int main() 69 | { 70 | std::cout <<"If you see the same value, then singleton was reused (yay!\n" << 71 | "If you see different values, then 2 singletons were created (booo!!)\n\n" << 72 | "RESULT:\n"; 73 | std::thread t1(ThreadFoo); 74 | std::thread t2(ThreadBar); 75 | t1.join(); 76 | t2.join(); 77 | 78 | return 0; 79 | } -------------------------------------------------------------------------------- /redis/availability.md: -------------------------------------------------------------------------------- 1 | # Redis 高可用 2 | 3 | 分析:Redis 高可用分成两个点,Redis Cluster 和 Redis Sentinel。这里要区别两种模式解决的痛点: 4 | - Redis Sentinel 是纯粹的高可用方案,采用的是主从复制; 5 | - Redis Cluster 部署的是对等(peer-to-peer)节点,解决了高可用问题,同时,也解决了**单机瓶颈**; 6 | 7 | 这里面有一个很容易误解的点,就是 Redis Cluster 里面的每个节点,都可以是一个是**主从**集群,在高端操作下,Redis Cluster 就是由多个主从集群构成的。 8 | 9 | 所以,Redis Cluster 解决高可用是要从两个角度来回答的,一个是对等节点,一个是节点是主从集群。另外一个考点在于,Cluster 的槽分配和迁移。迁移理解了,就理解了扩容和缩容。 10 | 11 | Redis Sentinel 模式要注意有两个集群,一个是存放了 Redis 数据的集群,一个是监控这个数据集群的哨兵集群。于是就需要理解哨兵集群之间是如何监控的,如何就某件事达成协议,以及哨兵自身的容错。 12 | 13 | ![Redis 高可用](img/availability.png) 14 | 15 | 答案:Redis 高可用有两种模式,Sentinel 和 Cluster。 16 | 17 | Sentinel 本质上是主从模式,与一般的主从模式不同的是,主节点的选举,不是从节点完成的,而是通过 Sentinel 来监控整个集群模式,发起主从选举。因此本质上 Redis Sentinel 有两个集群,一个是 Redis 数据集群,一个是哨兵集群。 18 | 19 | Redis Cluster 集成了对等模式和主从模式。Redis Cluster 由多个节点组成,每个节点都可以是一个主从集群。Redis 将 key 映射为 16384 个槽(slot),均匀分配在所有节点上。(这里有两个点,先不说,一个是怎样的均匀才是均匀,一个是客户端怎么查询,坐等面试官问) 20 | 21 | 两种模式下的主从同步都有全量同步和增量同步两种(引导面试官询问两种同步模式细节),一般情况下,我们应该尽量避免全量同步(钓鱼,面试官接着就会问为什么,或者全量同步有啥缺点,或者如何避免) 22 | 23 | (简单提及一下如何选择) 24 | 一般而言,如果数据量和复杂并不大的时候,想要保证高可用,就采用 Redis Sentinel;如果负载很大,或者说触及了 Redis 单机瓶颈,那么应该采用 Redis Cluster 模式。 25 | 26 | #### 相关问题 27 | - 你了解 Redis Sentinel 模式么 28 | - 你了解 Redis Cluster 模式么 29 | 30 | 31 | ## 扩展点 32 | 33 | ### Redis 主从之间是如何同步数据的? 34 | 35 | 分析:考察一般的数据同步模式。这里要回答出亮点,就要回到全量同步和增量同步(PSYNC)。而实际上从服务器发起的 PSYNC 既可能触发全量同步,也可能触发增量同步,所以我们不用 PSYNC 术语,以免搞混。而要理解增量复制,首先就要理解全量复制。为了方便记忆,我们记住核心点,就是如果同步的起始点命令,还在主服务器的缓冲队列上,那就是增量同步,如果不在,那就是全量同步。 36 | 37 | 答案:分成两种,全量同步和增量同步。全量同步的步骤是: 38 | 1. 从服务器发起同步,主服务器开启 BG SAVE,生成 BG SAVE 过程中的写命令也会被放入一个缓冲队列; 39 | 2. 主节点生成 RDB 文件之后,将 RDB 发给从服务器; 40 | 3. 从服务器接收文件,**清空本地数据**,再入 RDB 文件;(这个过程会忽略已经过期的 key,参考过期部分的讨论) 41 | 4. 主节点将缓冲队列命令发送给从节点,从节点执行这些命令; 42 | 5. 从节点重写 AOF; 43 | 44 | 这时候已经同步完毕,之后主节点会源源不断把命令同步给从节点。 45 | 46 | ( 主生成 RDB 和 缓冲命令, 发给从,从加载 RDB,执行缓冲命令,重写 AOF ) 47 | 48 | (先分析这种全量同步面临的问题,而后引出增量同步) 49 | 从上面的步骤可以看出来,全量同步非常重,资源消耗很大,而且,大多数情况下,从服务器上是存在大部分数据的,只是短暂失去了连接。如果这个时候又发起全量同步,那么很容易陷入到无休止的全量同步之中。 50 | 51 | 因此 Redis 引入了增量同步。增量同步的依赖于三个东西: 52 | 1. 服务器ID:用于标识 Redis 服务器ID; 53 | 2. 复制偏移量:主服务器用于标记它已经发出去多少;从服务用于标记它已经接收多少(从服务器的比较关键); 54 | 3. 复制缓冲区:主服务器维护的一个 1M 的FIFO队列,近期执行的写命令保存在这里; 55 | 56 | 从服务器将自己的复制偏移量发给主服务器,如果主服务器发现,该偏移量还在复制缓冲区,那么就执行增量复制,将偏移量后面的命令同步给从服务器;否则执行全量同步; 57 | 58 | (其实就是,从服务器记录了一下自己同步到哪里,然后找主服务器同步,主服务器一看,这个数据还在缓冲区,ok,可以增量同步) 59 | 60 | #### 如何引导 61 | - 讨论到无论是 Cluster 还是 Sentinel 的主从同步的时候 62 | 63 | #### 相关问题 64 | - Redis 全量同步是如何进行的 65 | - Redis 的增量同步是如何进行的 66 | - 你了解 Redis 的复制缓冲区吗(or 复制积压缓冲区)? 67 | 68 | ### Redis 如何决定是使用全量同步还是增量同步? 69 | 70 | 分析:考察同步的知识点。前面提到过了一点,这里系统总结一下。其实从一般的认知里面去推断,也能推出来。 71 | 72 | 答案: 73 | 1. 从服务器发现自己从来没有同步过,那么执行全量同步; 74 | 2. 从服务器发起同步命令(PSYNC),但是主服务器发现从服务器上次同步的对象不是自己,(服务器ID不匹配),于是执行全量同步; 75 | 3. 从服务器发起同步命令(PSYNC),主服务器发现偏移量太古老了,数据已经不在复制缓冲区了,全量同步; 76 | 4. 从服务器发起同步命令(PSYNC),主服务器发现偏移量对应的数据还在复制缓冲区,执行增量同步; 77 | 78 | (上面记不住,就简单记忆下面这个) 79 | 当且仅当,从服务器从相同的主服务器里面同步,偏移量对应的命令还在缓冲区,执行增量同步。 80 | 81 | #### 类似问题 82 | - PSYNC 一定发起增量同步么?NO 83 | - 增大、缩小复制缓冲区有什么影响?影响你触发全量同步的概率 84 | 85 | ### Redis 服务器重启可能引发什么问题? 86 | 87 | 分析:考察同步。前面提到过服务器ID,这个ID准确说是服务器运行时ID,它在重启后会变化。而结合主从同步的问题,我们会发现,服务器运行时ID变化会触发全量同步。 88 | 89 | 答案:服务器重启,分成主服务器重启和从服务器重启。 90 | 91 | 对于从服务器来说,因为重启会使它丢失了上一次同步的主服务器的ID,所以只能发起全量同步; 92 | 93 | 对于主服务器重启来说,因为服务器ID发生变化,所有的从服务器都需要执行全量同步; 94 | 95 | (刷亮点,论述一种不会变更服务器ID的重启方式) 96 | 针对这种情况,Redis 引入了一种安全重启机制,这种机制下重启不会变更服务器ID,可以避免全量同步 97 | 98 | #### 如何引导 99 | - 聊到服务器ID的时候就可以 100 | - 聊到 Redis 性能调优 101 | - 聊到可能触发全量同步的情况 102 | 103 | #### 类似问题 104 | - 你了解安全重启么? 105 | - 服务器ID变更会出现什么问题? 106 | - 如何避免全量同步? 107 | 108 | ### Redis 主从之间网络不稳定可能引发什么问题? 109 | 110 | 分析:考察同步。大多数和主从有关的问题,几乎都是围绕全量同步来做文章的。网络连接不稳,导致主从同步失败。亮点在于,要结合 Redis 的超时机制来回答。 111 | 112 | 答案:主从之间网络不稳定可能引起三种情况: 113 | 1. 短暂网络抖动,那么从服务器可以通过 ACK 机制重新补充丢失的数据(参考后面的心跳机制); 114 | 2. 超时,但是从服务器发过来的偏移量还在缓冲区,增量复制; 115 | 3. 超时,偏移量不在缓冲区,全量复制; 116 | 117 | (为了方便记忆,把时间轴想象成三部分:未超时,超时但是复制缓冲还在,超时没救了) 118 | 119 | #### 如何引导 120 | - 心跳机制可以谈起 121 | - 聊到全量同步 122 | - 聊到避免全量同步 123 | 124 | ### 全量同步有什么缺点 125 | 126 | 分析:考察全量同步。核心在于领悟全量同步的开销,要从非常具体的 CPU,内存,磁盘 IO,网络传输,以及潜在可能失败导致无休止的全量同步几个角度回答,最后点出因为这么多缺点,所以需要引入增量同步。 127 | 128 | 答案:全量同步是利用 BG SAVE 来完成的,所以 129 | 1. 从 CPU 和 内存的角度来说,会发起`fork`系统调用,在单机内存很大的时候,这会引起很大的延迟,并且因为 COW 的原因,引发大量的缺页中断; 130 | 2. BG SAVE 的文件写入到磁盘,会增大磁盘负载; 131 | 3. BG SAVE 在网络中传输,会导致短时间内网络负载飙升; 132 | 4. 更重要的是,因为全量同步非常复杂,这段时间可能从服务器再次和主服务器失去连接。等下次重连的时候,又触发一遍全量同步,循环往复; 133 | 134 | 也因此引入了增量同步。 135 | 136 | (如果记得,继续回答**如何避免全量同步**) 137 | 138 | ### 如何避免全量同步 139 | 140 | 分析:前面已经分析过了,这里再总结一下。先从怎么触发全量同步开始聊起,然后得出避免的方法,逻辑分明。 141 | 142 | 答案:引发全量同步的几个原因有: 143 | 1. 主服务器宕机重连 144 | 2. 主服务器没有安全启动 145 | 3. 主从同步超时,导致缓冲区溢出(就是偏移量对应的数据不再缓冲区了) 146 | 4. 从服务器重启 147 | 148 | 这些情况大部分是避免不了。能做的大概就是两件事: 149 | 1. 主服务器使用安全重启机制,避免ID变化; 150 | 2. 增大复制缓冲区 151 | 3. 调大超时 152 | 153 | 然后就是加强网络建设了(这是一句屁话,因为网络不可用,你软件是没办法的) 154 | 155 | #### 如何引导 156 | - 但凡聊到全量同步代价很高,就可以跳过来这里 157 | 158 | #### 类似问题 159 | - 什么时候会触发全量同步 160 | 161 | ### Redis 的心跳机制是怎样的? 162 | 163 | 分析:Redis 的心跳机制,关键点在于,它是一个类似于 TCP 协议的 ACK 机制。所以我们结合 TCP 的 ACK 机制来回答。 164 | 165 | 答案:Redis 的心跳机制,是两个方向的,一个是主服务器向从服务器发送心跳,用于检测网络和从服务器存活; 166 | 167 | 另外一个是从服务器像主服务器发送 REPLCONF ACK,这个 ACK 会带上自身的复制偏移量。因此,如果服务器发现从服务器的偏移量比较落后,可以将丢失的数据重新补上。 168 | 169 | (开始结合 TCP 来讨论)这就是类似于 TCP 的 ACK 机制。ACK 会告诉发送端下一次期望的数据报,而后发送端进行重发。 170 | 171 | (进一步装逼,不熟悉滑动窗口协议的请忽略)不过 TCP 引入了滑动窗口协议,因此可以简单处理失序报文,但是 Redis 的同步,是要求严格的顺序的,并且从服务器并不具备处理失序命令的能力。 172 | 173 | #### 如何引导 174 | - 聊到了 TCP 的ACK机制 175 | 176 | 177 | ### Sentinel 是如何监控主从集群的? 178 | 179 | 分析:考察 Sentinel 模式的基本特点。核心就是主观下线 -> 客观下线 -> 主节点故障转移。 180 | 181 | 答案:Sentinel 本身有三个定时任务(重要): 182 | 1. 获取主从结构信息,所以能够做到主从结构动态更新; 183 | 2. 获取其它 Sentinel 节点的看法; 184 | 3. 对主从节点的心跳检测; 185 | 186 | 整个过程可以理解为:首先 Sentinel 获取了主从结构的信息,而后向所有的节点发送心跳检测,如果这个时候发现某个节点没有回复,就把它标记为**主观下线**;如果这个节点是主节点,那么 Sentinel 就询问别的 Sentinel 节点主节点信息。如果大多数都 Sentinel 都认为主节点已经下线了,就认为主节点已经**客观下线**。 187 | 188 | 当主节点已经**客观下线**,就要步入故障转移阶段。故障转移分成两个步骤,一个是 Sentinel 要选举一个 leader,另外一个步骤是 Sentinel leader 挑一个主节点。 189 | 190 | Sentinel leader 选举是使用 raft 算法的,(这里犯不着描述具体步骤,等后面他如果有兴趣,就会问你细节。大部分情况下是没有的,因为 RAFT 算法很复杂) 191 | 选举出 leader 之后,leader 从健康从节点之中依据 <优先级, 偏移量, 服务器ID> 进行排序,挑出一个节点作为主节点。 192 | 193 | (在这里可以刷一波,是因为这个排序方式有点违背直觉,正常我们可能会认为优先选择偏移量最大的,也就是数据最新的,而不是优先级最大的,一个很奇怪的点,猜不出原因) 194 | 195 | 找出主节点之后,Sentinel 要命令其它从节点连接新的主节点,同时保持对老的主节点的关注,在它恢复过来之后把它标记为从节点,命令它去同步新的主节点。 196 | 197 | (这个过程,比较复杂的是各种参数配置和权衡,我们只讨论一个参数,`parallel-syncs`,这部分是刷亮点的部分) 198 | 199 | 因为可能存在多个从节点,因此我们需要控制同时进行控制转移的从节点的数量,也就是`paralle-syncs`参数。该参数如果设置过小,会导致故障转移时间很长;但是如果该参数设置过大,会导致多数从节点不可用。 200 | 201 | #### 如何引导 202 | - 如果聊到了 RAFT 算法,可以用 Sentinel leader 选举作为举例 203 | 204 | #### 相关问题 205 | - 主观下线是什么? 206 | - 客观下线是什么? 207 | - Sentinel 如何发现节点故障? 208 | - Sentinel 如何选举主节点? 209 | 210 | ### 为什么会发生脑裂?有什么危害?如何解决? 211 | 212 | 分析:考察脑裂。脑裂不是只有 Redis 才有的,而是所有的主从模式都会有类似的问题。比如说 Zookeeper,所以可以结合 zookeeper 来说。 213 | 214 | 答案:(首先点明)大部分主从模式都会遇到脑裂问题。它的根源在于,当我们把一个主节点标记位从节点之后,它自己认为自己还是主节点。如果这个时候客户端还是连上了这个主节点,那么就会导致在错误的主节点上执行了写命令,导致数据不一致。 215 | 216 | zookeeper 也有类似的问题。彻底解决这个问题其实不太可能(确实不太可能,分布式环境下),只能尽量缓解。在 Redis 里面有一个参数,控制主节点至少要有多少个从节点才会接受写请求,把这个值设置比较大,能够缓解问题。 217 | 218 | (吹牛逼了,这一段我也不是很有把握肯定对,但是没关系,吹出来就是加分,毕竟你深入思考了)上面的参数是无法根绝这个问题的。因为事情就可能那么凑巧,恰巧整个集群一分为二,然后两边各有一个主节点,然后都认为自己是主节点,而且从节点数也达标。这时候,如果将参数设置为超过一半,那么就可以避免这个问题。 219 | 220 | 脑裂也是现在制约主从模式的一个很大的问题,因此最近涌现出来了很多的对等集群。 221 | 222 | #### 如何引导 223 | - zookeeper 脑裂 224 | - 单纯探讨主从模式 225 | - 聊到了对等模式 226 | 227 | #### 相关问题 228 | - 主从模式有什么缺点 229 | 230 | ### Redis 为什么不直接使用普通的 Master-Slave 模式,而是要引入 Sentinel ? 231 | 232 | 分析:这个问题也有一点强行解释的意味。这个问题源于这么一种朴素的认知,就是其实从服务器完全可以自己发起选举,选出一个 leader 来,也就是主节点。Sentinel 却是引入了哨兵,由哨兵选出哨兵 leader,由哨兵 leader 选出一个主节点。但是在 Cluster 里面,主节点是直接由从节点选举出来的。强行解释,没啥好说的。 233 | 234 | 答案:(可能)是出于性能考虑。Sentinel 可以单独部署,那么 Sentinel 在选举 leader,挑选主节点的时候,并不会影响到 Redis 数据集群的性能。 235 | 236 | ### Redis Cluster 是如何运作的? 237 | 238 | 分析:详细讨论Cluster的机制。一般人只会回答到槽分配那一步,但是在面试后的时候,答得比较好的,要回答的到槽迁移。槽迁移就是发生在扩容缩容,因此不必纠结于扩容还是缩容,理解了槽迁移机制就可以。之前在数据结构部分,我们谈到Redis的渐进式 rehash, 说过Redis一个显著特征是大量运用延迟策略,而槽的迁移也是这种策略。 239 | 240 | 答案:Redis Cluster 主要是利用 key 的哈希值,将其分成 16384 个槽,而后每个槽被分配到不同的服务器上。这些服务器,本身也是一个主从模式的主服务器。 241 | 242 | (先回答,请求路由问题) 243 | Redis Cluster 是peer-to-peer,每个节点都能提供读写服务。在这种情况下,如果客户端请求的某个 key 不在该服务器上,该服务器就会返回一个`move`错误,让客户端再一次请求正确的服务器(类似于HTTP的重定向)。 244 | 245 | (记得就提及智能客户端) 246 | 可见,在这种情况下,如果我们能够在客户端维持一份槽映射表,我们的就不必经过这么一份转发,这就是所谓的智能路由。(不要继续讨论,等问) 247 | 248 | (接下里讨论槽迁移) 249 | 但是,分布式环境下,可能会扩容缩容。因此槽就会出现迁移,从一台服务器挪到另外一台服务器。 250 | 251 | Redis 提供了槽迁移的命令,主要步骤就是让目标节点准备好接收,源节点准备迁移。热后小批量迁移key。 252 | 253 | (讨论迁移过程中key的访问) 254 | 因此在迁移过程中,一个槽的部分 key 可能在源节点,一部分在目标节点。因此如果请求过来,打到源节点,源节点发现已经迁移了,就会返回一个 ASK 错误,这个错误会引导客户端直接去访问目标节点。 255 | 256 | #### 相关问题 257 | - 当槽在迁移过程中,我一个 key 过来,会如何? 258 | - Redis Cluster 是如何分片的? 259 | 260 | ### smart client 是什么? 261 | 262 | 分析:考察基本的智能客户端的概念。对于智能客户端来说,关键点在于,维护了槽映射关系,并且在收到`move`错误的时候更新,其次则是对每一个节点,创建了一个线程池。使用智能客户端性能提高很大,但是也存在映射关系落后于真实关系的问题。 263 | 264 | 答案:智能客户端是指,客户端将槽到服务器的映射关系维持在内存中,并且在收到`move`错误的时候更新信息。如果在使用连接池的情况下,它会对每一个主节点建立一个池。这种模式,极大减少了`move`错误发生的概率,并且即便真的发生了槽迁移,也很快就能修正自己的映射关系。 265 | 266 | #### 相关问题 267 | - 你用了智能客户端吗?(不管用不用,反正先把特点答了) 268 | - 有什么缺点?(稍微滞后一点) 269 | -------------------------------------------------------------------------------- /redis/data_structure.md: -------------------------------------------------------------------------------- 1 | # Redis为什么快 2 | 3 | - 完全基于内存,数据存在内存中,绝大部分请求是纯粹的内存操作,非常快速,跟传统的磁盘文件数据存储相比,避免了通过磁盘IO读取到内存这部分的开销 4 | - Redis中的数据结构是专门进行设计的,每种数据结构都有一种或多种数据结构来支持。Redis正是依赖这些灵活的数据结构,来提升读取和写入的性能 5 | - 网络请求采用单线程,省去了很多上下文切换的时间以及CPU消耗,不存在竞争条件,不用去考虑各种锁的问题,不存在加锁释放锁操作,也不会出现死锁而导致的性能消耗 6 | - 使用基于IO多路复用机制的线程模型,根据Socket上的事件来选择对应的事件处理器进行处理 7 | 8 | # Redis 数据结构 9 | 10 | 回答的是 Redis 值对象的数据结构(表象),还是底层实现。大多数情况下,你应该从表象出发,即值对象的角度出发,而后讨论每一种值对象的可能的底层实现。如果记不住全部的底层实现,可以只讨论重点的几个。先看图: 11 | 12 | ![值对象](img/value_object.png) 13 | 14 | ![底层数据结构](img/data_structure.png) 15 | 16 | 总体回答的讨论就是“某种值对象-有什么实现-某种实现的特点”。 17 | 18 | 答案:从使用的角度来说,Redis 有五种数据结构(注意,我们这里按照第一张图来回答,即从值对象的角度出发) 19 | 20 | - 字符串对象,即我们设置的值是一个简单的数字或者字符串; 21 | - 列表对象,即值是一个列表,存储多个元素。从底层实现上来说,有`ziplist`和`linkedlist`两种实现; 22 | - 字典对象,即值本身就是一个字典。从底层实现上来说,有`ziplist`和`hashtable`俩中实现; 23 | - 集合对象,即值是一个集合(Set)。底层有`intset`和`hashtable`两种实现; 24 | - 有序集合对象,即值是一个有序的集合,也就是zset。底层有`ziplist`和`skiplist`两种实现; 25 | 26 | 27 | 28 | ## 扩展点 29 | 30 | ### Redis 使用的字符串有什么特点? 31 | 32 | 分析:考察底层数据结构特点。 33 | 34 | 答案:Redis 使用的字符串,叫做 SDS。SDS 的特点是: 35 | 36 | 1. 直接存储了字符串长度,可以常量时间获得长度; 37 | 2. SDS 采用了预分配和懒回收的策略来分配内存,可以减少内存分配次数; 38 | 1. buf保存字符串,free记录buf中未使用的字节的个数 39 | 40 | #### 类似问题 41 | - SDS 有什么特点 42 | - SDS 和 C 字符串比起来有什么优点 43 | - Redis 为什么不直接使用 C 字符串 44 | 45 | #### 如何引导 46 | 可以考虑在前面回答字符串对象的时候主动谈起。 47 | 48 | 49 | ### Redis 的 hashtable 是如何实现的 50 | 51 | 分析:Redis 的渐进式 rehash 是很有特色的。要结合各自语言的 hashtable 实现来做交叉对比。例如,对于 Java 的开发者来说,可以比较 Redis 的 rehash 和 Java HashMap 的 rehash 过程;对于 golang 来说,Redis 的 rehash 过程和 map 的底层实现理念接近,也可以一并说起来。 52 | 53 | 而后要再一次点出,采用渐进式 rehash 的优缺点,即采用渐进式 rehash 会时总的开销增大,但是这种开销被平摊到了每次访问数据中,是一种取舍。 54 | 55 | 在回答完毕之后,为了万无一失,可以再一次提起,就是说字典除了可以用哈希表实现,也可以用`ziplist`来实现。 56 | 57 | 答案:(关于哈希算法这一段,可以作为候选,因为哈希算法名字很难记)Redis 采用 MurmurHash2 (么么哈希),该算法效率高,随机性好,可以减少冲突可能。 58 | 59 | Redis的哈希表是采用了链地址法来解决冲突,在冲突的时候,每个哈希表节点都有一个 next 指针, 多个哈希表节点可以用 next 指针构成一个单向链表, 被分配到同一个索引上的多个节点可以用这个单向链表连接起来 60 | 61 | Redis 的扩容比较有特色,采用的是渐进式 rehash。即 Redis 实际上维持了新旧两张表,迁移发生的时候,Redis 并不是直接把数据迁移到新的表,而是在后续删改查的时候逐步挪过去。 62 | 63 | (以下是和特定语言进行比较,看你的语言里面 rehash 是如何实现的) 64 | - (golang 版本) 65 | - go 语言的 map 实现理念很接近。golang 的 map 的扩容也是渐进式的,也是在访问数据过程中逐步完成迁移。 66 | - (C++ 版本) 67 | - 以2倍的原桶容量(2*old_bucket_count)分配一个新的桶数组 68 | - 遍历原桶数组,计算原桶数组中的每个元素在新桶数组中的位置,并插入到相应的桶中 69 | - 所有元素迁移完成之后,释放原桶数组 70 | 71 | 所以,当查找某个 key 的时候,大概是先去原表里面找,找不到就去新表里面找。如果在原表找到了,就同时执行迁移逻辑。 72 | 73 | 74 | (总结,升华主题,强调一下渐进式的改进并不是毫无缺点的。这也是回答“为什么使用渐进式 rehash ”)总体而言,渐进式 rehash 可以带来更平滑的响应时间。但是渐进式的 rehash 也会带来总体开销比一次性迁移开销大的缺点。 75 | 76 | 77 | 78 | 关键点:拉链法,MurmurHash2(么么哈希),渐进式 rehash 79 | 80 | #### 类似问题 81 | - 为什么要使用渐进式 rehash 82 | - 渐进式 rehash 有什么缺点?(总开销大,每次查询都比一次性 rehash要慢) 83 | - Redis 的哈希表扩容有什么特色 84 | - Redis 的哈希表如何扩容 85 | 86 | #### 如何引导 87 | - 讨论到你的语言的 map 的底层实现的时候,你可以用 Redis 作为横向对比 88 | 89 | ### Hash对象如何实现 90 | 91 | **ziplist**和**hashtable** 92 | 93 | 数据量小的时候使用 ziplist 节省空间,数据量大的时候用 hashtable 以降低操作复杂度 94 | 95 | ziplist;将保存了键的压缩列表节点推入到压缩列表表尾, 然后再将保存了值的压缩列表节点推入到压缩列表表尾 96 | 97 | 保证: 98 | 99 | - 保存了同一键值对的两个节点总是紧挨在一起 100 | - 先添加到哈希对象中的键值对会被放在压缩列表的表头方向, 而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向 101 | 102 | hashtable:字典作为底层实现,每个键值对都使用一个字典键值对 103 | 104 | 105 | #### 二者如何转换? 106 | 107 | 使用ziplist: 108 | - 所有键值对的键和值的字符串长度都小于 64 字节 109 | - 键值对数量小于 512 个 110 | 111 | 否则使用hashtable编码 112 | 113 | 114 | ### Redis 里面的 ziplist 是如何实现的?有什么用? 115 | 116 | 分析:考察底层数据结构。`ziplist`在 Redis 里面是一个很重要的结构,要紧紧围绕`ziplist`节省内存,结构紧凑,搜索快速的特点来回答。要想答出特色来,要先回答`ziplist`的基本特点,要把重点放在`ziplist`增删改数据时候的行为,关键点在于两个,一个是数据移动,一个是连锁更新。 117 | 118 | 答案:`ziplist`是一个很特殊的列表,它的内存类似数组那样是连续,但是每个元素的大小却不相同。`ziplist`通常用于单个数据小,并且数据量不多的情况。在 Redis 里面,`ziplist`用于组成有序集合,字典和列表三种值对象。 119 | 120 | (回答第一个要点,元素移动) 121 | `ziplist`能够在`O(1)`的时间内完成对头尾的操作(因为`ziplist`记录了首尾节点),但是一般的增删改查,都是`O(N)`的。这是因为`ziplist`是一个连续内存的结构,找到位置`i`,需要从头部开始遍历,而在**增删**的时候需要将位置`i`之后的元素移动(增往后移动,删往前移动)。 122 | 123 | (回答第二个要点,连锁更新) 124 | 尤其是,因为`ziplist`的节点存储了前一个节点的长度`prelen`,所以,当前一个节点发生变更的时候,就需要更新长度`prelen`。而 Redis 为了节约内存,`prelen`有一个字节和五个字节两种长度。举例来说,假设前一个节点,最开始的长度是254,而后更新成了256,那么当前节点原本一个字节能够放下`prelen`,不得不扩展到五个字节。假如说当前节点最开始长度也是254,那么`prelen`扩展到五个字节之后就变成了258,当前节点的后一个节点,就不得不跟着扩展。 125 | 126 | 这就是所谓的连锁更新,它使得一个增删改操作,最坏的时候是`O(N^2)`。这也是为什么`ziplist`只适合放置小数据,少数据的原因。(从这里也可以解释为什么前面那些编码,都是限制数据小于64字节,并且数量少于512) 127 | 128 | 关键点:内存连续,数据移动,连锁更新, 129 | 130 | #### 类似问题 131 | - 如何往`ziplist`里面插入或者删除一个元素?(考察数据移动和连锁更新) 132 | - 什么时候会触发连锁更新? 133 | - `ziplist`在最糟糕的情况下性能如何?(考察连锁更新) 134 | - 为什么数据量大的时候不用`ziplist`?(考察`ziplist`的特点,特别是增删改查的行为) 135 | - `ziplist`的操作效率是多少?(O(N),一般情况下,只要不是操作头尾,即PUSH,POP之类的操作,都是) 136 | - 删除会引起连锁更新么?(当然可能!增删改都可能!) 137 | - 为什么使用`ziplist`?(考察`ziplist`特点) 138 | - 什么情况下使用`ziplist`? (同上,单个数据小,数据量也少) 139 | 140 | #### 如何引导 141 | - 讨论到了有序列表的时候 142 | - 讨论到 ArrayList, LinkedList 的时候。`ziplist`的实现不同于这两种,所以可以扩展到这里 143 | 144 | ### Redis 的整数集合(intset)是什么?有什么特色 145 | 146 | 分析:考察底层数据结构,核心就在于理解`intset`的**升级不降级**的特性。 147 | 148 | 答案:`intset`是一个数组结构,用于存储整数类型,里面的元素是唯一的。它可以存放16、32、64位的整数。如果元素位数变大,那么就会触发升级过程。例如原本存储的元素都是16位整数,现在插入一个32位的整数,那么 Redis 需要按照32位重新计算内存大小,并且分配内存,迁移原本的数据,而后将新数据插入。有一点需要注意的是,Redis并不支持降级。 149 | 150 | #### 类似问题 151 | - 如果我有一个小的整数数据集想要放到 Redis,Redis会用什么结构来存储? 152 | - Redis 的`intset`是否支持降级? 153 | 154 | ### Redis中zset底层实现 155 | #### ziplist实现 156 | 1. 什么时候使用 157 | 158 | - 成员的数量小于128 个; 159 | - 每个 member (成员)的字符串长度都小于 64 个字节 160 | 161 | 2. 组成 162 | 163 | - 当前ziplist占用总字节数 164 | - 尾部元素相对起始偏移量 165 | - entry数量:需要完全遍历entry 166 | - entry:具体数据线,可以是字节数组或正数 167 | - entry 的第一个节点保存 member,第二个节点保存 score。依次类推,集合中的所有成员最终会按照 score 从小到大排列 168 | - zlend:标识ziplist内存结束点作用 169 | #### skiplist实现 170 | 171 | 插入、删除、查找的时间复杂度均为 O(logN) 172 | 173 | 174 | 175 | 性质 176 | 177 | (1)由很多层结构组成; 178 | (2)每一层都是一个有序的链表; 179 | (3)最底层(Level 1)的链表包含所有元素; 180 | (4)如果一个元素出现在 Level i 的链表中,则它在 Level i 之下的链表也都会出现; 181 | (5)每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素。 182 | 183 | Redis的跳跃表由zskiplistNode和skiplist两个结构定义,其中 zskiplistNode结构用于表示跳跃表节点,而 zskiplist结构则用于保存跳跃表节点的相关信息 184 | 185 | skiplist包含以下属性: 186 | 187 | 1. header:跳跃表头节点 188 | 2. tail:跳跃表尾节点 189 | 3. level:跳跃表内,层数最大的哪个节点的层数(不计表头节点 190 | 4. length:跳跃表长度 191 | 5. 后退指针 192 | 6. 分值 193 | 194 | ## 总结 195 | 196 | Redis 的问题大同小异,套路就是: 197 | 1. 你用过XXX结构么? 198 | 2. 如果我想存储XXX特点的数据,你会用什么? 199 | 3. 如果我想存储XXX特点的数据,Redis 会用什么结构? 200 | 201 | 而后就是前面列举的几个有很强个性的数据结构的实现原理。在复习的时候,一定要记住值对象和底层实现的关系。大概的思路就是“值对象——支持的数据结构——数据结构特点” 202 | 203 | 还要把握住不同底层实现切换的逻辑。比如说字典,底层可能是`ziplist`和`hashtable`,那么要把握住两个点: 204 | 1. 什么时候会从`ziplist`转化到`hashtable` 205 | 2. 怎么转化 206 | 207 | 第二个问题`怎么转化`其实很好回答,所有的底层实现转换,都是遍历老的实现的数据,一个个迁移过去。例如`ziplist`迁移`hashtable`,就是遍历`ziplist`,对里面每一个元素做哈希,放到对应的位置。 208 | 209 | Redis 还有一个设计理念,就是先凑合,不行再升级。最开始 Redis 总是选择能够节省内存的,紧凑的数据结构,后面发现不行了,再来升级。 210 | 211 | 212 | -------------------------------------------------------------------------------- /redis/expired.md: -------------------------------------------------------------------------------- 1 | # Redis 过期处理 2 | 3 | 分析:Redis 对过期键值对的处理,可以说是日经题。核心就是懒惰删除+定期删除。前者很容易记忆,后者很容易忽略。而要刷亮点,要从 RDB, AOF 和 主从复制三个不同处理策略上着手。 4 | 5 | ![过期策略](img/expired.png) 6 | 7 | 答案: Redis 删除过期键值对,主要依赖于两种方式,定期删除和懒惰删除。 8 | 9 | 定期删除是指 Redis 会定期遍历数据库,检查过期的 key 并且执行删除。它的特点是随机检查,点到即止。它并不会一次遍历全部过期 key,然后删除,而是在规定时间内,能删除多少就删除多少。这是为了平衡 CPU 开销和内存消耗。 10 | 11 | (扩展点一,和 JVM 的 G1 垃圾回收器做对比,它们的类似点在于部分回收)这有点类似于 JVM 的 G1 垃圾回收器,G1 也是挑选一部分 Region 来回收,不过 G1 主要是为了平衡停顿时间。 12 | 13 | Redis 的另外一个删除策略是懒惰删除,即如果在访问某个 key 的时候,会检查其过期时间,如果已经过期,则会删除该键值对。 14 | 15 | (扩展点二,结合 RDB, AOF 和 主从复制来回答) 16 | 17 | 如果 Redis 开启了持久化和主从同步,那么 Redis 的过期处理要复杂一些。 18 | 1. 在 RDB 之下,加载 RDB 会忽略已经过期的 key;(RDB 不读) 19 | 2. 在 AOF 之下,重写 AOF 会忽略已经过期的 key;(AOF 不写) 20 | 3. 主从同步之下,从服务器等待主服务器的删除命令;(从服务器啥也不干) 21 | 22 | (扩展点三,讨论不同版本从服务器对过期 key 的处理策略) 23 | 如果 Redis 开启了主从同步,那么从库对过期 key 的处理,不同版本有不同策略。对于写来说,从库都是等主库的删除命令,但是对于读来说: 24 | - 在 3.2 之前,Redis 从服务器会返回过期 key 的值,仿佛没有过期一样 25 | - 在 3.2 之后,Redis 从服务器会返回NULL,和主库行为一致 26 | 27 | (扩展点四,讨论 3.2 版本之前,读取从库过期 key 的策略) 28 | 在 3.2 之前,可以使用`TTL`命令来判断 key 究竟有没有过期。 29 | 30 | 31 | #### 类似问题 32 | - 为什么 Redis 定时删除策略不删除全部过期的 key? 33 | - Redis 的定时删除策略是怎样的? 34 | - Redis key 过期之后,还能读到数据吗? 35 | - 为什么有时候 key 已经过期了,但是还能读到数据? 36 | - 如何解决 Redis 从库 key 过期依然返回数据的问题? -------------------------------------------------------------------------------- /redis/img/availability.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/redis/img/availability.png -------------------------------------------------------------------------------- /redis/img/data_structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/redis/img/data_structure.png -------------------------------------------------------------------------------- /redis/img/expired.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/redis/img/expired.png -------------------------------------------------------------------------------- /redis/img/io_model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/redis/img/io_model.png -------------------------------------------------------------------------------- /redis/img/persistence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/redis/img/persistence.png -------------------------------------------------------------------------------- /redis/img/pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/redis/img/pipeline.png -------------------------------------------------------------------------------- /redis/img/value_object.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hizeal/baguwen-interview/b9636b4deea4ce64affed6f5ee9439f305e6b298/redis/img/value_object.png -------------------------------------------------------------------------------- /redis/io_model.md: -------------------------------------------------------------------------------- 1 | # IO 模型 2 | 3 | 分析:所有的 IO 模型,考来考去就是一句话,**IO多路复用**。因为操作系统就那么一回事,你要高性能,就没啥选择,反正别问,问就是`IO 多路复用`。那么为什么大家还问呢?因为`IO 多路复用`大体上大家都是差不多的,但是细节上就五花八门。回答 Redis 的 IO 模型,亮点可以从两个角度刷,一个是和`memcache`的比较;一个是从 6.0 支持多线程角度刷。 4 | 5 | ![IO 模型](img/io_model.png) 6 | 7 | 核心就是四个组件。 8 | 9 | 答案:Redis 采用的是 IO 多路复用模型,核心分成四个组件: 10 | 1. 多路复用程序 11 | 2. 套接字队列 12 | 3. 事件分派器 13 | 4. 事件处理器。事件处理器又可以分成三种,连接处理器,请求处理器,回复处理器。 14 | 15 | (事件处理器怎么记住这三个呢?按照“发起连接——发送请求——发回响应”三个步骤,刚好对应三个处理器) 16 | 17 | (大概描述一下各个组件是怎么配合的,大致就是生产者——消费者模式) 18 | 19 | 多路复用程序会监听不同套接字的事件,当某个事件,比如发来了一个请求,那么多路复用程序就把这个套接字丢过去套接字队列,事件分派器从队列里边找到套接字,丢给对应的事件处理器处理。 20 | 21 | ![Redis IO 多路复用](https://img-blog.csdnimg.cn/20200614190842638.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1Nla3lfZmVp,size_16,color_FFFFFF,t_70) 22 | 23 | 24 | (扩展点一,讨论 6.0 引入的多线程模型) 25 | 26 | Redis 这种模型的瓶颈在于从套接字中读写数据。因此在 6.0 中引入了异步 IO 线程,专门负责读取 IO 数据。在这种模型之下,相当于主线程监听到套接字事件,找到一个 IO 线程去读数据,之后主线程根据命令,找到对应的事件处理器,执行命令。写入响应的时候,也是交给了 IO 线程。这就是相当于,有一个线程池,只负责读写数据,主线程负责轮询和执行命令。 27 | 28 | ![Redis 多线程模型](https://pic3.zhimg.com/80/v2-4bd6569139472aaf4423540dd303e61a_1440w.jpg) 29 | 30 | (扩展点二,讨论`memcache`的 IO 模型) 31 | 32 | `memcache` 的 IO 模型本质上是 IO 多路复用。所不同的是,`memcache` 的 IO 多路复用是多线程的,并且命令的执行也是多线程的。`memcache`的`acceptor`线程监听到套接字事件之后,丢给`workers`线程,线程负责读写数据并且执行命令。 33 | 34 | ![memcache IO 模型](https://upload-images.jianshu.io/upload_images/6302559-7b933753b04ac9bb.png?imageMogr2/auto-orient/strip|imageView2/2/w/856/format/webp) 35 | 36 | (扩展点三,结合扩展点二,进一步比较 Redis 多线程和 `memecache` 多线程) 37 | 在 Redis 6.0 支持多线程 IO 之后,两者的 IO 模型看上去其实差别不是特别大了。根本的差距,在于 Redis 依旧是只有一个单线程来执行命令,但是 `memcache` 是各自线程执行各自的命令。 38 | 39 | #### 类似问题 40 | - Redis 为什么引入多线程模型? 41 | - Redis 一定是单线程的吗? 42 | - Redis 是如何保证高性能的?(这里只讨论了一个点,还有别的点) 43 | 44 | #### 如何引导 45 | - 讨论 Redis 为什么那么高效 46 | - 讨论到多路复用 47 | - 讨论到 IO 模型 48 | - 讨论到“Redis一定是单线程的吗”这种问题 49 | - 讨论到了`memcache` 和 Redis 的区别 50 | 51 | ## References 52 | [Redis 多线程解密](https://segmentfault.com/a/1190000039223696) -------------------------------------------------------------------------------- /redis/persistent.md: -------------------------------------------------------------------------------- 1 | # Redis 的持久化机制 2 | 3 | 分析:Redis 持久化机制,其实还是比较简单的,就是 RDB 和 AOF 两种,掌握各自的特点和适用场景,然后背一下 AOF 重写的流程,差不多就可以了。如果要刷出亮点,在于两个点,COW 和 `fsync`的时机。前者是和操作系统相关,`fork` 调用有关;后者是横向对比其它中间件的持久化机制,比如说MySQL 的 `redolog`, `binlog` 都有类似的机制。如果直接问起来持久化机制,可以只先回答大概,把关键知识点点到就可以,后面等面试官来挖。 4 | 5 | 进一步,我们可以拿 Redis 的 AOF 机制和 MySQL 的 `binlog` 进行对比,它们都记录的是中间执行步骤。而 MySQL 的`mysqldump`就非常接近 `RDB`,也是一种快照保存方案。 6 | 7 | ![Redis 持久化](img/persistence.png) 8 | 9 | 答案: Redis 的持久化机制分成两种,RDB 和 AOF。 10 | 11 | RDB 可以理解为是一个快照,直接把 Redis 内存中的数据以快照的形式保存下来。因为这个过程很消耗资源,所以分成 SAVE 和 BG SAVE 两种。(后面这个,是点出来 COW,如果你无法理解 COW 机制,就不要回答)BG SAVE的核心是利用`fork` 和 `COW` 机制。 12 | 13 | AOF 是将 Redis 的命令逐条保留下来,而后通过重放这些命令来复原。我们可以通过重写 AOF 来减少资源消耗。(重写AOF,这个是钓鱼,是为了引导后面的两个话题,为什么要重写,以及如何重写) 14 | 15 | #### 如何引导 16 | 17 | 这里面有一个很出人意料的引导的点,就是分布式锁。看后面**使用 Redis 来作为分布式锁,会有什么问题?** 18 | 19 | ## 扩展点 20 | 21 | ### 为什么RDB使用子进程 22 | 23 | 1. 通过 fork 创建的子进程能够获得和父进程完全相同的内存空间,父进程对内存的修改对于子进程是不可见的,两者不会相互影响 24 | 2. 通过 fork 创建子进程时不会立刻触发大量内存的拷贝,内存在被修改时会以页为单位进行拷贝,这也就避免了大量拷贝内存而带来的性能问题 25 | 26 | ### BG SAVE是如何工作的? 27 | 28 | 分析:明面上是考察BG SAVE,实际上是考察 COW。所以亮点就在于把 COW 说个大概。思路就是从`fork`系统调用谈起,谈到 COW,再谈到 COW 内部的大概步骤。另外一个方向是结合 Java 的`CopyOnWrite`数据结构一起聊。 29 | 30 | 答案:BG SAVE 是为了解决 SAVE 资源消耗过多的问题(这一句是点出目标)。BG SAVE核心是利用`fork`系统调用,复制出来一个子进程,而后子进程尝试将数据写入文件。这个时候,子进程和主进程是共享内存的,当主进程发生写操作,那么就会复制一份内存,这就是所谓的 COW。COW 的核心是利用缺页异常,操作系统在捕捉到缺页异常之后,发现他们共享内存了,就会复制出来一份。(这里,如何发现共享内存,检查的是页表项,记不住没关系,认怂就可以。关键点在于缺页异常。) 31 | 32 | (下面是结合 Java `CopyOnWrite` 来阐述,不熟悉 Java 这一类数据结构的请忽略。我猜测其它语言或者工具也会有类似的机制,可以挑一个深入描述,作为一种对比) 33 | Java 里面也有一大类数据结构,利用了 COW 这种思想,例如 `CopyOnWriteArrayList`,当里面元素变更的时候,就会复制出来一个新的。它特别适合那种大多数情况只是读,只有小部分可能是写的场景。 34 | 35 | (进一步升华,引导下去下面的**COW 缺陷**)如果 Redis 的数据也是读多写少,那么 COW 就很高效。这也是一种典型的空间换取时间策略。 36 | 37 | #### 如何引导 38 | 39 | - 操作系统里面聊到了进程,`fork`系统调用 40 | - Java 里面聊到了`CopyOnWrite`之类的数据结构 41 | 42 | #### 类似问题 43 | 44 | - 为什么引入 BG SAVE? 45 | - RDB 是如何运作的?先回答 SAVE,而后回答 BG SAVE。 46 | - COW 是如何运作的 47 | 48 | ### COW 有什么缺陷? 49 | 50 | 分析:考察 COW 的特点。其实这个问题有点故意找茬没事找事的感觉。COW 就两个问题,一个是写多的时候,缺页异常会非常多,如果物理内存紧张,会引发大量的物理页置换。另外一个就是,COW 的存在,导致 Redis 无法完全利用内存,总要留出来一部分给 COW 使用。 51 | 52 | 答案:有两个缺点: 53 | 54 | 1. 引发缺页异常。如果物理内存紧张,还会引起大量的物理页置换; 55 | 2. COW 的存在,导致我们需要预留一部分内存出来,Redis 无法全部利用服务器的内存; 56 | 57 | (这个时候我们可以进一步讨论,这一段讨论只是为了展示你对于 COW 的理解)一般来说,最极端情况是所有内存复制一遍,那么 Redis 最多利用一半的内存,考虑到操作系统本身的开销,那么一半都不到。不过如果愿意冒险的话,可以设置超过一半。例如,不考虑操作系统开销,如果自己的 Redis 读多写少,在整个 BG SAVE 过程,最多复制 10% 的内存,那么就可以给 Redis 分配 80% 的内存。这种搞法,糟糕的情况下,会引发大量的物理页置换,性能下降。所以,很少有人这么使用。 58 | 59 | #### 如何引导 60 | 61 | - 前面聊到 BGSAVE 就可以指出来 62 | 63 | #### 类似问题 64 | 65 | - COW 会引发什么问题? 66 | - 频繁写的 Redis,在使用 BG SAVE 的时候会有什么问题 67 | - BG SAVE 有什么缺陷? 68 | 69 | ### 为什么启用了 AOF 还是会丢失数据? 70 | 71 | 分析:又是一道违背一般常识的问题,考察的就是刷盘时间,和数据库“为什么事务提交了,数据却丢了”一个性质。所以两边可以交叉对比来回答。 72 | 73 | 答案:原因在于 AOF 的数据只写到了缓存,还没有写到磁盘。 AOF 有三个选项可以控制刷盘: 74 | 75 | 1. always: 每次都刷盘 76 | 2. everysec: 每秒,这意味着一般情况下会丢失一秒钟的数据。而实际上,考虑到硬盘阻塞(见后面**使用 everysec 输盘策略有什么缺点),那么可能丢失两秒的数据。 77 | 3. no: 由操作系统决定 78 | 79 | 他们的数据保障逐渐变弱,但是性能变强。 80 | 81 | (开始升华主题,横向对比)所有依赖于`fsync`系统调用落盘的中间件都会碰到类似的问题。例如`redolog`, `binlog`。而且,在`redolog`如果提交事务之后,没有及时落盘,而此时数据库崩掉,就会出现事务已经提交,但是数据依旧丢失的问题。 82 | 83 | #### 如何引导 84 | 85 | - 前面聊到 MySQL 的`redolog`,`binlog`; 86 | - 聊到`fsync`话题; 87 | 88 | ### 使用 everysec 策略刷盘有什么缺点? 89 | 90 | 分析:这是为了考察所谓的刷盘阻塞。就是当你每秒刷一次的时候,可能会出现,数据太多,或者硬盘阻塞,你无法在一秒钟内刷完数据。 91 | 92 | 答案:使用 everysec 会面临一个刷盘阻塞的问题。如果数据太多,或者硬盘阻塞,导致一秒钟内无法把所有的数据都刷新到磁盘。Redis 如果发现上一次的刷盘还没结束,就会检查,距离上一次刷盘成功多久了,如果超过两秒,那么 Redis 会停下来等待刷盘成功。 93 | 94 | 因此使用 everysec 可能导致丢失两秒数据,而且在同步等待的时候,Redis 的其它请求都被阻塞。 95 | 96 | #### 如何引导 97 | 98 | - 一般我建议在前面聊到了刷盘策略的时候说 99 | 100 | #### 类似问题 101 | 102 | - 什么是硬盘阻塞 103 | - 如果磁盘负载(或者 IO 负载)太大,会有什么问题? 104 | 105 | ### 为什么 AOF 要引入重写的机制? 106 | 107 | 分析:考察 AOF 特点。核心就是 AOF 逐条记录命令,导致 AOF 文件非常巨大,其次就是,AOF 记录的命令是可以合并的。我们用一个例子来辅助记忆后面的 AOF 命令合并这一个点。 108 | 109 | 答案:AOF 是逐条记录 Redis 执行命令的,这会导致 AOF 文件快速膨胀。在使用 AOF 恢复数据的时候,异常缓慢。从另外一个角度来说,我们也不需要真的逐条记录 Redis 的命令,一些命令是可以合并的。举例来说,假如我们 Redis 记录了用户ID到用户名字的数据,那么某个用户先更新自己的用户名为AAA,后面更新为BBB,实际上,我们只需要记录最后一条更新为BBB的。又比如说,Redis 先插入了一条数据AAA,后面又删除了AAA,这个时候我们可以两条都不记录。 110 | 111 | (刷亮点)MySQL `binlog` 类似于 AOF,但是并没有重写机制,因为 MySQL 可以混用 `mysqldump` 和 `binlog` 来恢复数据。(看后面**Redis 如何利用 RDB 和 AOF 恢复数据**) 112 | 113 | ### AOF 重写是怎么运作的? 114 | 115 | 分析:考察 AOF 重写的特点。这里面有一个误区,就是直觉上,我们以为 AOF 重写是读已有的 AOF 文件,然后尝试合并里面的记录。实际上不是的,AOF 重写非常类似于 RDB,只不过是输出格式不一样。但是 AOF 重写还要解决一边在重写,一边又有新的 AOF 的问题。面试的亮点就在于回答清楚后面的一边重写 AOF,一边又有新的 AOF 来了怎么处理。 116 | 117 | 答案:重写 AOF 整体类似于 RDB。它并不是读已经写好的 AOF 文件,然后合并。而是类似于 RDB,直接`fork`出来一个子进程,子进程按照当前内存数据生成一个 AOF 文件。在这个过程中,Redis 还在源源不断执行命令,这部分命令将会被写入一个 AOF 的缓存队列里面。当子进程写完 AOF 之后,发一个信号给主进程,主进程负责把缓冲队列里面的数据写入到新 AOF。而后用新的 AOF 替换掉老的 AOF。这里可以看出来,最后这个步骤是比较耗时的,同时 Redis 也处于一种无法执行别的命令的状态。(据我所知是这样的,就是处理缓冲队列的数据的时候,类似于 GC 的 STW 过程,无法对外服务,这也是一个亮点,很少有人会考虑最后这个缓冲队列处理,是不是会导致无法执行用户命令) 118 | 119 | (这里还有一个刷亮点的地方,但是是只适用于 Java 方向,熟悉 G1 垃圾回收期的同学。在 G1 里面也有一个类似的缓冲队列)这种机制,在别的地方也可以看到。比如说 G1 回收器,使用了SATB技术,在开始的时候记录了一个快照,而 GC 过程的引用变更都会丢到一个缓冲队列,在再标记阶段重新处理。 120 | 121 | (升华主题,点出 AOF 这种方案的固有缺陷.)类似于 AOF 这种记录变更的技术,都要面临类似的问题,也就都需要考虑合并与重写的机制。 122 | 123 | ### RDB 和 AOF 该如何选择? 124 | 125 | 分析:考察 RDB 和 AOF 的优缺点。 126 | 127 | 答案:选择的原则是: 128 | 129 | 1. 如果数据不能容忍任何丢失,或者只能容忍少量丢失,那么用 AOF; 130 | 2. 否则 RDB,即一般的数据备份和容灾,RDB就够了; 131 | 132 | 遇事不决 AOF,反正 RDB 可以的,AOF 肯定也可以。 133 | 134 | #### 类似问题 135 | 136 | - 为什么 Redis 要搞 RDB 和 AOF 两种机制? 137 | - 要想保证丢失数据最少,应该使用哪种? AOF 138 | - 只是出于数据备份和容灾,用哪种? 139 | 140 | ### Redis 如何利用 RDB 和 AOF 恢复数据? 141 | 142 | 分析:一句话的事情,非常简单,不过我们可以结合MySQL数据恢复来做比较。 143 | 144 | 答案:原则就是,有 AOF 用 AOF,没有就用 RDB。(AOF>RDB,你可以进一步解释为什么)这是因为 AOF 的数据在大概率的情况下,是要比 RDB 新的。这和 MySQL 的数据恢复有点不同。 MySQL 的 `mysqldump` 类似于 RDB,而`binlog` 类似于 AOF。MySQL 是可以用 `mysqldump` 的文件来恢复,而后从`binlog`里面找出后续变更,从而恢复数据。 145 | 146 | (进一步深化,其实我也说不清楚为毛`binlog`没有类似的机制,我觉得也可以考虑有的)我觉得这也是为什么`binlog`没有类似于 AOF 重写机制的一个原因。 147 | 148 | #### 类似问题 149 | 150 | - 为什么 Redis 恢复数据优先使用 AOF 数据 151 | 152 | ### 使用 Redis 来作为分布式锁,会有什么问题? 153 | 154 | 分析:这是一个很偏门冷僻的问题,在分布式锁里面可能会问到。我们假定你能正确使用 Redis 命令来写一个分布式锁,那么你还需要考虑这个场景:一个线程抢到了分布式锁,然后这个锁没有持久化,然后 Redis 崩了,很快又重启了,结果下一个线程立马就拿到了锁,这个时候就会出现你代码万无一失,但是分布式锁还是被多个线程拿到了的问题。我感觉很少人会考虑这个点,就暂且留着。 155 | 156 | 答案:要考虑分布式锁持久化的问题。假定我一个线程拿到了分布式锁,那么如果这个锁没有被持久化,那么如果 Redis 崩溃立刻重启,那么下一个线程立马就能拿到锁。 157 | 158 | 所以在考虑这种场景下,万无一失的方案,就是开启 AOF 持久化,并且将刷盘时机设置成`always`。 159 | -------------------------------------------------------------------------------- /redis/pipeline.md: -------------------------------------------------------------------------------- 1 | # Redis Pipeline 2 | 分析:Redis Pipeline 的面试主要停留在为什么 Pipeline 快这个核心要点,围绕着这个核心要点考察 Pipeline 的原理。同时还会结合考察如何在 Redis Cluster 里面使用 Pipeline,以及和批量命令的区别。 3 | 4 | 所以需要首先掌握 Pipeline 的大概原理。 5 | 6 | Pipeline 的原理其实不难: 7 | 8 | ![Redis Pipeline](./img/pipeline.png) 9 | 10 | 它的核心要点: 11 | 1. 应用代码会持续不断的把请求发给 Redis Client; 12 | 2. Redis Client 会缓存这些命令,等凑够了一批,就发送命令到 Redis 服务端; 13 | 3. Redis Server 收到命令之后进行处理,并且在处理完这一波命令之前,所有的响应都被缓存在内存里面; 14 | 4. Redis Server 处理完了 Pipeline 发过来的一批命令,而后返回响应给 Redis Client; 15 | 5. Redis Clinet 接收响应,并且将结果递交给应用代码; 16 | 6. 如果此时还有命令,Redis Client 会继续发送剩余命令 17 | 18 | 先来回顾一下普通的一个命令从发送出去到收到响应,要做些什么: 19 | 1. Redis Client 发起系统调用 send(),将命令发送给 Redis Server 20 | 2. 命令在网络中传输 21 | 3. Redis Server 发起系统调用 read(),从网络中读取命令 22 | 4. Redis Server 发起系统调用 send(),返回响应 23 | 5. 响应在网络中传输 24 | 6. Redis Client 发起系统调用 read(),读取响应 25 | 26 | 总结下来,整个过程就是四次系统调用,而后一个在网络中传输命令和响应——可以看做是一个 RTT。 27 | 28 | 那么假如有 N 个命令,这里就需要 N * 4 次系统调用,并且需要 N * RTT。 29 | 30 | 如果在 Pipeline 里面,如果恰好是 N 个命令发送一波,那么只需要四次系统调用加一个 RTT。但是这一个 RTT 和原本的单个 RTT 比起来是要慢一点的,因为这里要发送的数据量要多。 31 | 32 | 因此我们可以总结,使用 Pipeline 性能比较好就在于两点: 33 | - 减少系统调用,避免内核态切换 34 | - 减少了 RTT 35 | 36 | 而开销我们也能看出来:对于 Redis Client 来说,需要额外的内存缓存命令;对于 Redis Server 来说,需要额外的内存来缓存响应。 37 | 38 | 这两者的缓存,都跟 N 的大小有关。因此控制 N 的大小就比较关键了,N 同时影响着系统调用的数量和缓存所需的内存大小,两者需要权衡折中。此外,如果 N 很大,导致很难凑够 N 个命令,那么客户端的命令就会长期缓存,而没能够及时发送到服务端。 39 | 40 | 使用 Pipeline 的时候,服务端在收到命令之后会先缓存,因此 Redis Server 可能会把好几个不同客户端发过来的命令混在一起。 41 | 42 | 但是 Redis Server 保证了来自同一个 Pipeline 的命令会被顺序执行。只不过这并不意味着这 N 个命令是原子的。如果中间有任何一个命令失败,那么后续的命令将不会被执行,而已经被执行的命令,也不会回滚。 43 | 44 | 那么和批量处理命令比起来有什么不同? 45 | - 从系统调用的角度来说,没什么不同 46 | - 批量命令里面一批命令,必然是同种命令,比如说都是 Get,或者都是 Set;而 Pipeline 则不是,任何命令都可以通过 Pipeline 来发送 47 | 48 | ## 面试题 49 | ### Redis Pipeline 的原理 50 | 分析:这里基本上就是回答 Pipeline 的实现机制。在答基本步骤的过程中,可以有意识引导面试官问 N 的取值,N 对内存的影响。 51 | 52 | 最后简单总结一下 Pipeline 为什么快。 53 | 54 | 答案:Redis Pipeline 的原理是: 55 | 1. 应用代码会持续不断的把请求发给 Redis Client; 56 | 2. Redis Client 会缓存这些命令,等凑够了 N 个,就发送命令到 Redis 服务端。而 N 的取值对 Pipeline 的性能影响比较大(引导询问 N 的取值); 57 | 3. Redis Server 收到命令之后进行处理,并且在处理完这 N 个命令之前,所有的响应都被缓存在内存里面。这里也可以看到,N 如果太大也会额外消耗 Redis Server 的内存(这里引导讨论内存消耗这个弊端); 58 | 4. Redis Server 处理完了 Pipeline 发过来的一批命令,而后返回响应给 Redis Client; 59 | 5. Redis Clinet 接收响应,并且将结果递交给应用代码; 60 | 6. 如果此时还有命令,Redis Client 会继续发送剩余命令; 61 | 62 | (刷亮点,也是引导)Redis Pipeline 减少了网络 IO,也减少了 RTT,所以性能比较好。 63 | 64 | #### 类似问题 65 | - 为什么 Redis Pipeline 在实时性上要差一点?主要就是命令和响应都会被缓存,而不是及时返回。 66 | 67 | ### Redis Pipeline 有什么优势? 68 | 分析:如果直接回答性能比较好,那么就基本等于没说。这个问题本质上其实是“为什么 Redis Pipeline 性能好”。 69 | 70 | 结合之前我们的分析,可以看到无非就是两个原因:网络 IO 和 RTT。 71 | 72 | 这里可以稍微讨论一下,批处理命令如 mget 和 mset 其实也是具备这两个优点的 73 | 74 | 答:Redis Pipeline 相比普通的单个命令模式,性能要好很多。 75 | 76 | 单个命令执行的时候,需要两次 read 和 两次 send 系统调用,加上一个 RTT。如果有 N 个命令就是分别乘以 N。 77 | 78 | 但是在 Pipeline 里面,一次发送,不管 N 多大,都是两次 read 和两次 send 系统调用,和一次 RTT。因而性能很好。 79 | 80 | (刷亮点,准备引导面试官问和批处理命令的区别)实际上 mget 之类的批量命令,相比单个命令分别执行,也是只需要两次 read 和两次 send 系统调用,和一次 RTT。和 Pipeline 比起来没啥区别。 81 | 82 | ### Redis Pipeline 和 mget 的不同点 83 | 84 | 分析:虽然问的是不同点,但是一般回答都是相同点和不同点一起说。 85 | 相同点: 86 | - 减少网络 IO 和 RTT,性能好 87 | - Redis Cluster 对这两种用法都不太友好 88 | 89 | 不同点: 90 | - Redis Pipeline 可以执行任意的命令,而 mget 之类的只能是执行同种命令; 91 | - Redis Pipeline 的命令和响应都会被缓存,因此实时响应上不如 mget; 92 | - Redis Pipeline 和 mget 都会受到批次大小的影响,但是相比之下 Redis Pipeline 更加严重,因为它消耗内存更多; 93 | 94 | 这里分析完之后,可以进一步分析两者的使用场景。其中 Redis Cluster 的问题属于引导,不必全部回答出来。 95 | 96 | 答:Redis Pipeline 和 mget 之类的批量命令有很多地方都很相似,比如说: 97 | 98 | - 减少网络 IO 和 RTT,性能好(注意,这里可能面试官会问,它是如何减少 IO 和 RTT,也就是我们前面讨论优势的地方) 99 | - Redis Cluster 对这两种用法都不太友好(这个是引导,准备讨论 Redis Cluster 需要的特殊处理) 100 | 101 | 但是具体来说,Redis Pipeline 使用场景和 mget 不太一样: 102 | - Redis Pipeline 可以执行任意的命令,而 mget 之类的只能是执行同种命令; 103 | - Redis Pipeline 的命令和响应都会被缓存,因此实时响应上不如 mget; 104 | - Redis Pipeline 和 mget 都会受到批次大小的影响,但是相比之下 Redis Pipeline 更加严重,因为它需要缓存命令和响应,消耗更大; 105 | 106 | (刷亮点,讨论什么时候用 Redis Pipeline)在频繁读写的情况下,使用 Redis Pipeline 都是能受益的。但是如果是追求实时响应的话,那么就不要使用 Redis Pipeline,因为 Redis Pipeline 的机制导致请求和响应会被缓存一小段时间。这种实时场景,只能考虑批量处理命令 107 | #### 类似命令 108 | - 什么时候选择 Redis Pipeline 109 | - 什么时候选择 mget 110 | 111 | ### 如何在 Redis Cluster 上使用 Redis Pipeline 112 | 113 | 分析:首先,一般的说法都是 Redis Cluster 上无法使用批量命令和 Pipeline。这种说法其实没什么大问题,但是实际上我们可以考虑在客户端上做一些改造,使得在 Redis Cluster 上也能使用 Pipeline。 114 | 115 | 核心要点: 116 | - 知道 Redis Cluster 上槽的分布。假如说 Redis Cluster 有 ABC 三个节点,并且 0-5000 槽在 A 上,5001-10000 槽在 B 上,10001-16384 在 C 上; 117 | - 我们在每个节点上都创建一个 Pipeline 118 | - 当请求过来,比如说找 key1, key2, key3 119 | - 这时候,根据 key1, key2, key3 知道对应的槽,假说 key1 槽是 100,key2 槽是2000,key3 槽是6000 120 | - 根据槽找到对应的节点。key1 和 key2 对应 A,key3 对应 B 121 | - 找到在该节点上创建的 Pipeline,发送命令。key1 和 key2 通过 A 上的 Pipeline 发送,key3 上的通过 B 的 Pipeline 来发送 122 | 123 | 所以本质上这个过程,就是手动将 key 按照槽分布到不同节点,然后使用不同节点上的 Pipeline。 124 | 125 | 答:一般来说,Redis Pipeline 是针对节点,所以会说无法在 Redis Cluster 上使用 Pipeline。这种说法是指,我们无法创建一个 Pipeline,连上整个 Redis Cluster。 126 | 127 | 但是实际上,我们可以通过创建多个 Pipeline 分别连上每一个节点来在 Redis Cluster 上使用 Pipeline。 128 | 129 | 这种使用方式的核心在于:当我们收到一个请求时,要能计算出来它的槽。而后根据槽找到对应的节点。然后将请求发送到该节点对应的 Pipeline 上。 130 | 131 | #### 类似问题 132 | - Redis Cluster 上能不能使用 Pipeline? -------------------------------------------------------------------------------- /项目/jwt-go/README.md: -------------------------------------------------------------------------------- 1 | 传统的session问题: 2 | 3 | Session: 每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大 4 | 5 | 6 | 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力 7 | 8 | 9 | CSRF: 因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到**跨站请求伪造**的攻击 10 | 11 | 12 | 基于Token的鉴权机制: 13 | 14 | 15 | ## 流程 16 | 17 | 1. 用户使用用户名密码来请求服务器 18 | 2. 服务器进行验证用户的信息 19 | 3. 服务器通过验证发送给用户一个token 20 | 4. 客户端存储token,并在每次请求时附送上这个token值,保存在请求头 21 | 5. 服务端验证token值,并返回数据 22 | 23 | ## 优点 24 | 25 | - 无状态(也称:服务端可扩展行):Token机制在服务端不需要存储session信息,因为Token 自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息 26 | - 去耦: 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你可以进行Token生成调用即可 27 | - 性能: 一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算 的Token验证和解析要费时得多 28 | - 便于传输,jwt的构成非常简单,字节占用很小,所以它是非常便于传输 29 | - 不需要在服务端保存会话信息, 所以它易于应用的扩展 -------------------------------------------------------------------------------- /项目/抖音项目/README.md: -------------------------------------------------------------------------------- 1 | # 架构设计 2 | 3 | Middleware:Token解析和颁发,密码加密 4 | 5 | Handler:解析得到参数,开始调用下层逻辑 6 | 7 | Service: 8 | - 上层需要返回数据信息 9 | - 检查参数 10 | - 准备数据 11 | - 打包数据 12 | 13 | - 不需要返回数据信息 14 | - 检查参数,执行上层指定动作 15 | 16 | - Model层 17 | - 面向于数据库的增删改查,不需要考虑和上层的交互 18 | 19 | # 用户登录 20 | 21 | main函数->路由->调用对应中间件加密密码->调用对应Handler 22 | 23 | ## middleware 24 | 25 | 进入中间件SHAMiddleWare内的函数逻辑,得到password明文加密后再设置password。具体需要调用gin.Context的Set方法设置password。随后调用next()方法继续下层路由 26 | 27 | ## Handler层:UserLoginHandler(c *gin.Context) 28 | 29 | - 传入context,解析其中的name和password,解析失败,响应体写入错误 30 | - 调用Service层QueryUserLogin,将上文解析的name和password送入 31 | - Service调用middleware中颁布Token函数,返回包含user_id和Token的响应 32 | - Handler在响应体写入状态码、状态信息、id和Token(HS256) 33 | - 状态码1表示错误 34 | - 状态码0表示正常返回 35 | 36 | 37 | ## Service层:QueryUserLogin(username, password string) (*LoginResponse, error) 38 | 39 | - 定义结构LoginResponse,包含user_id和Token 40 | - 检测username和password是否合法 41 | - 调用models层QueryUserLogin查找是否存在对应用户 42 | - 若存在,颁发Token,打包数据写入到结构,返回给Handler 43 | 44 | ## Models层:QueryUserLogin(q.username, q.password, &login) 45 | - 定义用户登录表 46 | - 用户id:外键,UserInfo表主键 47 | - UserInfoId: 48 | - Username:登陆表主键 49 | - Password 50 | 51 | - 通过Gorm接口在MySQL中查找是否存在匹配该用户名和对应用户密码,返回错误信息 52 | 53 | 54 | # 用户注册 55 | 56 | main函数->路由->调用对应中间件加密密码->调用对应Handler 57 | 58 | ## middleware 59 | 60 | 进入中间件SHAMiddleWare内的函数逻辑,得到password明文加密后再设置password。具体需要调用gin.Context的Set方法设置password。随后调用next()方法继续下层路由 61 | 62 | ## Handles:UserRegisterHandler(c *gin.Context) 63 | 64 | - 传入context,解析username和password,解析失败,响应体写入错误 65 | - 调用Service层PostUserLogin(username, password string) 66 | - Service返回结构体LoginResponse,包含user_id和Token的响应 67 | - Handler在响应体写入状态码、状态信息、id和Token 68 | 69 | ## Service层:PostUserLogin(username, password string) (*LoginResponse, error) 70 | - 检测传入username和password是否合法 71 | - 用户名是否为空,用户名长度限制,密码是否空 72 | - 构建用户信息表 73 | - 如果用户名已存在,返回错误 74 | - 调用models层AddUserInfo,数据库中添加该用户信息表 75 | - 用户登录表 76 | - 用户信息表UserInfo 77 | - 添加成功,颁发Token,打包数据,返回给上层Handler 78 | 79 | ## Models层:AddUserInfo(&userinfo) 80 | - 传入用户信息表 81 | - 通过Gorm给MySQL添加用户信息 82 | - 若出错则返回错误,否则不返回 83 | 84 | 85 | # 用户发布视频 86 | 87 | main函数->router->调用对应中间件解析Token->调用对应Handler 88 | 89 | 此时处于登录状态,只需要传入用户id和Token,验证Token。根据用户Id生成Token 90 | 91 | ## middleware 92 | 93 | 解析token,是否超时或token不正确 94 | 95 | ## Handlers:PublishVideoHandler(c *gin.Context) 96 | - 解析user_id和视频title, 97 | - 提取文件后缀名,判断是否为视频文件格式;支持多文件上传 98 | - 根据user_id与用户发布视频数量,构建唯一文件名,与文件拓展名结合,存于static文件下 99 | - 截取一帧作为视频封面 100 | - 调用Service层PostVideo(userId int64, videoName, coverName, title string) 101 | - Handler在响应体写入状态码、状态信息 102 | 103 | ## Service:PostVideo(userId int64, videoName, coverName, title string) error 104 | - 准备参数,视频名和封面 105 | - 构建视频表 106 | - 调用models的NewVideoDAO().AddVideo(video),添加视频表 107 | 108 | ## Models层:NewVideoDAO().AddVideo(video) 109 | - 通过Gorm接口在MySQL中添加视频表 110 | 111 | 由于视频和userinfo有多对一的关系,所以传入的Video参数一定要进行id的映射处理 112 | 113 | # 获得用户发布视频列表 114 | 115 | main函数->router->调用对应中间件取出user_id->调用对应Handler 116 | 117 | ## Handler:QueryVideoListHandler(c *gin.Context) 118 | - 取出context中的user_id 119 | - 调用Service层的QueryVideoListByUserId(userId),返回该id的视频列表 120 | - 视频列表写入响应体中 121 | 122 | ## Service: 123 | - 检测数据库中UserInfo表是否存在该id 124 | - 调用Model层的QueryVideoListByUserId(q.userId, &q.videos),视频列表存于q.videos 125 | - 调用Model层的QueryUserInfoById(q.userId, &userInfo),用户信息表中对应的用户信息存于userInfo 126 | - 填充视频信息表的Auther字段,封装视频列表 127 | 128 | ## Model: 129 | - 分别获取user_id对应的视频列表和用户信息表 130 | 131 | # 用户点赞操作 132 | 133 | ## middleware 134 | 135 | 解析token,是否超时或token不正确 136 | 137 | ## Handler:PostFavorHandler(c *gin.Context) 138 | - 取出context的user_id、video_id、动作类型(点赞1或取消点赞2) 139 | - 调用Service的PostFavorState(p.userId, p.videoId, p.actionType) 140 | - 操作成功或失败的状态码和错误消息写入到响应体中 141 | 142 | ## Service:PostFavorState(userId, videoId, actionType int64) error 143 | - 传入的参数,封装为一个结构,定义其方法,用以调用Model层函数 144 | - 检查动作类型:既非1也非2,不支持该操作 145 | - 如果是点赞操作,调用Model层的PlusOneFavorByUserIdAndVideoId(p.userId, p.videoId) 146 | - 更新对应的用户点赞状态,该信息存于Redis中,key为(favor, userId),value为视频id 147 | 148 | - 如果是取消点赞操作,调用Model层的MinusOneFavorByUserIdAndVideoId(p.userId, p.videoId) 149 | - 更新对应用户点赞状态,同上 150 | 151 | - 返回错误信息 152 | 153 | ## Model层: 154 | - PlusOneFavorByUserIdAndVideoId(p.userId, p.videoId) 155 | 更新MySQL中video信息表中video_id对应的favorite_count,user_favorite_videos中间表的user_id及video_id 156 | - MinusOneFavorByUserIdAndVideoId(p.userId, p.videoId) 157 | 先判断Video信息表中video_id对应的favorite_count大于,接下来才能减1 158 | 在user_favorite_videos中间表中移除user_id和video_id 159 | 160 | 161 | ## 用户评论 162 | 163 | main函数->router->调用对应中间件解析Token->调用对应Handler 164 | 165 | ### Handler层:PostCommentHandler(c *gin.Context) 166 | 167 | - 定义结构体,存储Context外还保存Context中的videoId、userId、commentId、actionType、commentText 168 | - 从context解析参数,存储结构体 169 | - 调用Service层的PostComment(p.userId, p.videoId, p.commentId, p.actionType, p.commentText) 170 | - 若Service层处理失败,响应体写入错误代码和错误消息;否则返回操作成功代码和Comment表信息 171 | 172 | ### Service层:PostComment(userId int64, videoId int64, commentId int64, actionType int64, commentText string) (*Response, error) 173 | 174 | - 调用Model层IsUserExistById(p.userId)和IsVideoExistById(p.videoId),判断数据库是否存在对应信息 175 | - 判断action是发布评论1还是删除评论2 176 | - 如果是发布评论,调用CreateComment()函数 177 | - 该函数调用Model层的AddCommentAndUpdateCount(&comment),这里comment为Comment表 178 | - 如果是删除评论,调用DeleteComment()函数 179 | - 调用DeleteCommentAndUpdateCountById(p.commentId, p.videoId) 180 | 181 | 182 | ### Model层: 183 | #### AddCommentAndUpdateCount(comment *Comment) error 184 | 1. 执行事务,数据库中添加该信息;返回任何错误,回滚事务 185 | 2. 增加Comment表中的comment_count 186 | 3. 执行事务成功,提交事务,返回nil 187 | 188 | #### DeleteCommentAndUpdateCountById(commentId, videoId int64) 189 | 1. 执行事务,删除表中对应commentId的信息 190 | 2. 减少comment_count 191 | 3. 提交事务,返回nil 192 | 193 | 194 | ## 用户关注/取消关注 195 | 196 | ## middleware 197 | 198 | 解析token,是否超时或token不正确 199 | 200 | ## Handler:PostFollowActionHandler(c *gin.Context) 201 | - 用户id,他要关注的id,关注/取消关注的动作 202 | - 调用startAction(),startAction()调用Service层的PostFollowAction(p.userId, p.followId, p.actionType) 203 | - Service返回错误 204 | - 判断是否是未定义操作或是数据库中找不到用户,自己关注自己 205 | - 否则就是model层错误,说明是重复键值插入 206 | 207 | ## Service:PostFollowAction(p.userId, p.followId, p.actionType) 208 | - 调用Model层IsUserExistById,检查要关注的用户id是否在数据库当中 209 | - 判断actionType是否为关注或取消关注 210 | - 关注 211 | - 调用Models层的AddUserFollow(userId, userToId int64) 212 | - 更新Redis中信息 213 | - 取消关注 214 | - 调用Models层的CancelUserFollow(p.userId, p.userToId) 215 | - 更新Redis中信息 216 | 217 | ## Model层: 218 | 219 | ### AddUserFollow(p.userId, p.userToId) 220 | 221 | 更新用户信息表中对应的关注人数和关注者人数 222 | 在用户关系表中插入(用户id,关注id) 223 | 224 | 更新Redis中 225 | ### CancelUserFollow(p.userId, p.userToId) 226 | 227 | 更新用户信息表中对应的关注人数和关注者人数 228 | 在用户关系表中删除(用户id,关注id) 229 | 230 | 231 | # 用户信息表 232 | ```go 233 | type UserInfo struct { 234 | Id int64 `json:"id" gorm:"id,omitempty"` 235 | Name string `json:"name" gorm:"name,omitempty"` 236 | FollowCount int64 `json:"follow_count" gorm:"follow_count,omitempty"` 237 | FollowerCount int64 `json:"follower_count" gorm:"follower_count,omitempty"` 238 | IsFollow bool `json:"is_follow" gorm:"is_follow,omitempty"` 239 | User *UserLogin `json:"-"` //用户与账号密码之间的一对一 240 | Videos []*Video `json:"-"` //用户与投稿视频的一对多 241 | Follows []*UserInfo `json:"-" gorm:"many2many:user_relations;"` //用户之间的多对多 242 | FavorVideos []*Video `json:"-" gorm:"many2many:user_favor_videos;"` //用户与点赞视频之间的多对多 243 | Comments []*Comment `json:"-"` //用户与评论的一对多 244 | } 245 | ``` 246 | 247 | # 用户登录表,与用户信息表一对一 248 | ```go 249 | type UserLogin struct { 250 | Id int64 `gorm:"primary_key"` 251 | UserInfoId int64 252 | Username string `gorm:"primary_key"` 253 | Password string `gorm:"size:200;notnull"` 254 | } 255 | ``` 256 | # 视频表 257 | 258 | ```go 259 | type Video struct { 260 | Id int64 `json:"id,omitempty"` 261 | UserInfoId int64 `json:"-"` 262 | Author UserInfo `json:"author,omitempty" gorm:"-"` //这里应该是作者对视频的一对多的关系,而不是视频对作者,故gorm不能存他,但json需要返回它 263 | PlayUrl string `json:"play_url,omitempty"` 264 | CoverUrl string `json:"cover_url,omitempty"` 265 | FavoriteCount int64 `json:"favorite_count,omitempty"` 266 | CommentCount int64 `json:"comment_count,omitempty"` 267 | IsFavorite bool `json:"is_favorite,omitempty"` 268 | Title string `json:"title,omitempty"` 269 | Users []*UserInfo `json:"-" gorm:"many2many:user_favor_videos;"` 270 | Comments []*Comment `json:"-"` 271 | CreatedAt time.Time `json:"-"` 272 | UpdatedAt time.Time `json:"-"` 273 | } 274 | ``` 275 | 276 | # 评论表 277 | ```go 278 | type Comment struct { 279 | Id int64 `json:"id"` 280 | UserInfoId int64 `json:"-"` //用于一对多关系的id 281 | VideoId int64 `json:"-"` //一对多,视频对评论 282 | User UserInfo `json:"user" gorm:"-"` 283 | Content string `json:"content"` 284 | CreatedAt time.Time `json:"-"` 285 | CreateDate string `json:"create_date" gorm:"-"` 286 | } 287 | ``` 288 | 289 | # 中间表 290 | 291 | user_infos和videos的多对多关系,创建一张user_favor_videos中间表,然后将该表的字段均设为外键,分别存下user_infos和videos对应行的id。如id为1的用户对id为2的视频点了个赞,那么就把这个1和2存入中间表user_favor_videos即可 292 | 293 | user_relations中间表,存下用户id和该用户关注者id 294 | 295 | # Redis 296 | 297 | key:点赞:用户id 298 | value:用户点赞的视频id集合 299 | 300 | 301 | key:关注:用户id 302 | value:用户关注的id集合 303 | 304 | --------------------------------------------------------------------------------