├── C++ ├── STL.md ├── 内存管理.md ├── 多线程.md ├── 新特性.md ├── 模板.md ├── 移动语义.md ├── 编译链接.md ├── 语法.md └── 面向对象.md ├── Golang ├── GRPC.md ├── unsafe.md ├── 优化.md ├── 内存管理.md ├── 基础.md ├── 并发编程.md ├── 标准库.md ├── 测试.md ├── 脑图.md └── 项目构建.md ├── HR 面.md ├── JAVA ├── JAVA │ ├── IO.md │ ├── JVM.md │ ├── SPI.md │ ├── 反射.md │ ├── 基本概念.md │ ├── 并发编程.md │ ├── 序列化与反序列化.md │ ├── 异常.md │ ├── 新特性.md │ ├── 泛型.md │ ├── 注解.md │ ├── 集合.md │ └── 面向对象.md └── Spring │ ├── AOP.md │ ├── 事务.md │ ├── 基础.md │ ├── 数据层.md │ └── 核心容器.md ├── LICENSE ├── README.md ├── 云原生 ├── Docker.md └── k8s.md ├── 分布式系统 ├── 共识算法.md ├── 分布式kv.md ├── 存储.md ├── 理论.md └── 计算.md ├── 前端 ├── ECMAScript │ ├── ES6.md │ ├── ESlint.md │ └── 异步.md ├── Vue │ ├── Vue3.0.md │ ├── Vuex.md │ ├── Vue基础.md │ ├── 生命周期.md │ ├── 组件通信.md │ ├── 虚拟 DOM.md │ └── 路由.md └── 构建打包 │ ├── Babel.md │ ├── Vite.md │ ├── Webpack.md │ └── 基本概念.md ├── 大数据 ├── Hadoop │ └── HDFS.md └── Spark │ ├── RDD.md │ ├── SparkStream.md │ ├── 内存.md │ ├── 划分.md │ ├── 基本概念.md │ ├── 宽窄依赖.md │ └── 常见算子.md ├── 安全 └── 架构安全性.md ├── 工程 ├── GDB.md ├── GIT.md └── SSH.md ├── 开源实习.md ├── 操作系统 ├── Linux 命令.md ├── 其他.md ├── 内存.md ├── 同步互斥.md ├── 常见机制.md ├── 文件系统.md ├── 硬件设备.md ├── 线程.md ├── 网络系统.md └── 进程.md ├── 数据库 ├── MySQL │ ├── SQL 基本概念.md │ ├── SQL 练习.md │ ├── 主从同步.md │ ├── 事务.md │ ├── 优化.md │ ├── 内存.md │ ├── 分库分表.md │ ├── 存储引擎.md │ ├── 数据库表设计.md │ ├── 日志.md │ ├── 索引.md │ ├── 读写分离.md │ └── 锁.md ├── Redis │ ├── Redis.md │ ├── 主从复制.md │ ├── 发布订阅.md │ ├── 基本概念.md │ ├── 应用问题.md │ ├── 持久化.md │ ├── 数据类型.md │ ├── 线程模型.md │ ├── 配置文件.md │ └── 集群.md └── leveldb&rocksdb │ ├── leveldb.md │ └── rocksdb.md ├── 数据结构算法 ├── TopK.md ├── 其他.md ├── 图论.md ├── 手撕.md ├── 排序.md ├── 查找.md ├── 树.md ├── 海量数据.md └── 笔试板子.md ├── 智力题场景题 ├── 场景题.md └── 智力题.md ├── 测试 ├── 案例.md ├── 流量回放.md ├── 理论.md └── 自动化测试与测试工具.md ├── 计算机网络 ├── API 范式.md ├── DNS.md ├── HTTP 演化.png ├── HTTP 演化.xmind ├── HTTP.md ├── HTTPS 优化.png ├── HTTPS 优化.xmind ├── HTTPS.md ├── Socket 编程.md ├── 传输层.md ├── 基础.md ├── 工具命令.md ├── 数据链路层.md ├── 物理层.md ├── 网络安全.md ├── 网络层.md └── 设备.md ├── 设计模式 ├── 单例模式.md ├── 基本概念.md ├── 工厂模式.md ├── 观察者模式.md └── 适配器模式.md └── 资源.md /C++/多线程.md: -------------------------------------------------------------------------------- 1 | # 多线程 2 | 3 | ## 问题 4 | 5 | ### 线程 6 | 7 | C++11 创建线程方法 8 | 9 | ### 锁 10 | 11 | C++ 11 互斥锁 12 | 13 | C++11用过哪些锁,lockGurd和uniqueLock有什么不同 14 | 15 | ### 原子操作 16 | 17 | atomic,它的非原子情况 18 | 19 | atomic 怎么实现的 20 | 21 | CAS 怎么保证原子性 22 | 23 | volatile 关键字的作用 24 | 25 | 请简述一下atomoic内存顺序。 26 | 27 | i++是否原子操作?并解释为什么? 28 | 29 | 原子操作是什么保证的,应该是计算机的指令级别 30 | 31 | ## 回答 32 | 33 | ### 锁 34 | 35 | ### 原子操作 36 | 37 | #### 谈谈 atomic、volatile、register 的作用 38 | 39 | 对于volatile这个单词,权威词典有三个意思: 40 | 41 | 1. likely:可能的。这意味着被 volatile 形容的对象「有可能也有可能不」发生改变,因此我们不能对这样的对象的状态做出任何假设。 42 | 2. suddenly:突然地。这意味着被 volatile 形容的对象可能发生瞬时改变。 43 | 3. unexpectedly:不可预期地。这与 likely 相互呼应,意味着被 volatile 形容的对象可能以各种不可预期的方式和时间发生更改。 44 | 45 | 因此,volatile 其实就是告诉我们,被它修饰的对象出现任何情况都不要奇怪,我们不能对它们做任何假设。 46 | 47 | volatile是一种类型修饰符 48 | 49 | **在常规程序中的volatile** 50 | 51 | ```c++ 52 | volatile int *p = /* ... */; 53 | int a, b; 54 | a = *p; 55 | b = *p; 56 | ``` 57 | 58 | 如果忽略volatile关键字,上述代码只需要从内存读取一次就够了。因为从内存中读取一次之后,CPU 的寄存器中就已经有了这个值;把这个值直接复用就可以了。这样一来,编译器就会做优化,把两次访存的操作优化成一次。但是如果加上 volatile 关键字后,编译器对访问该变量的代码就不再进行优化,从而可以提供稳定访问, 59 | 60 | 总结: 当读取一个变量时,为提高存取速度,编译器优化时有时会先把变量读取到一个寄存器中;以后,再取变量值时,就直接从寄存器中取值。这个时候在寄存器和内存中都有我们的值,按道理来说应该是一致的,但有几种情况: 61 | 62 | 1. 当变量值在本线程里改变时,会同时把变量的新值copy到该寄存器中,以便保持一致 63 | 2. 当变量在因别的线程而改变了值,该寄存器的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致 64 | 3. 当该寄存器在因别的线程而改变了值,原变量的值不会改变,从而造成应用程序读取的值和实际的变量值不一致 65 | 66 | volatile的意思是让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值 67 | 68 | 但是要知道volatile并不能真正解决多线程之间的问题。对于临界区的资源我们可以用锁机制来保护,对于非临界区的资源,如果使用 `volatile` 会禁止编译器优化相关变量,从而降低性能,所以也不建议依赖 `volatile` 在这种情况下做线程同步。 69 | 70 | 使用`std::atomic` 操作是原子的,同时构建了良好的内存屏障 71 | 72 | **多线程编程中什么情况下需要加 volatile?** 73 | 74 | C/C++多线程编程中不要使用volatile。C++11标准中明确指出解决多线程的数据竞争问题应该使用原子操作或者互斥锁。 75 | 76 | 因为C和C++中的volatile并不是用来解决多线程竞争问题的,而是用来修饰一些因为程序不可控因素导致变化的变量,比如访问底层硬件设备的变量,以提醒编译器不要对该变量的访问擅自进行优化。 77 | 78 | **是否可以和const一起使用?** 79 | 80 | 没问题,这俩又不冲突 81 | 82 | C++ 中的 `volatile` 关键字,`std::atomic` 变量及手动插入内存屏障指令(Memory Barrier)均是为了避免内存访问过程中出现一些不符合预期的行为。这三者的作用有些相似之处,不过显然它们并不相同,本文就将对这三者的应用场景做一总结。 83 | 84 | 这三者应用场景的区别可以用一张表来概括: 85 | 86 | | | `volatile` | Memory Barrier | `atomic` | 87 | | -------------- | ---------- | -------------- | -------- | 88 | | 抑制编译器重排 | Yes | Yes | Yes | 89 | | 抑制编译器优化 | Yes | No | Yes | 90 | | 抑制 CPU 乱序 | No | Yes | Yes | 91 | | 保证访问原子性 | No | No | Yes | 92 | 93 | 下面来具体看一下每一条。 94 | 95 | ### 抑制编译器重排 96 | 97 | 所谓编译器重排,这里是指编译器在生成目标代码的过程中交换没有依赖关系的内存访问顺序的行为。 98 | 99 | 比如以下代码: 100 | 101 | ``` 102 | *p_a = a;b = *p_b; 103 | ``` 104 | 105 | 编译器**不保证**在最终生成的汇编代码中对 `p_a` 内存的写入在对 `p_b` 内存的读取之前。 106 | 107 | 如果这个顺序是有意义的,就需要用一些手段来保证编译器不会进行错误的优化。具体来说可以通过以下三种方式来实现: 108 | 109 | - 把对应的变量声明为 `volatile` 的,C++ 标准保证对 `volatile` 变量间的访问编译器不会进行重排,不过仅仅是 `volatile` 变量之间, `volatile` 变量和其他变量间还是有可能会重排的; 110 | - 在需要的地方手动添加合适的 Memory Barrier 指令,Memory Barrier 指令的语义保证了编译器不会进行错误的重排操作; 111 | - 把对应变量声明为 `atomic` 的, 与 `volatile` 类似,C++ 标准也保证 `atomic` 变量间的访问编译器不会进行重排。不过 C++ 中不存在所谓的 “atomic pointer” 这种东西,如果需要对某个确定的地址进行 atomic 操作,需要靠一些技巧性的手段来实现,比如在那个地址上进行 placement new 操作强制生成一个 `atomic` 等; 112 | 113 | **抑制编译器优化** 114 | 115 | 此处的编译器优化特指编译器不生成其认为无意义的内存访问代码的优化行为,比如如下代码: 116 | 117 | ``` 118 | void f() { int a = 0; for (int i = 0; i < 1000; ++i) { a += i; }} 119 | ``` 120 | 121 | 在较高优化级别下对变量 `a` 的内存访问基本都会被优化掉,`f()` 生成的汇编代码和一个空函数基本差不多。然而如果对 `a` 循环若干次的内存访问是有意义的,则需要做一些修改来抑制编译器的此优化行为。可以把对应变量声明为 `volatile` 或 `atomic` 的来实现此目的,C++ 标准保证对 `volatile` 或 `atomic` 内存的访问肯定会发生,不会被优化掉。 122 | 123 | 不过需要注意的是,这时候手动添加内存屏障指令是没有意义的,在上述代码的 `for` 循环中加入 `mfence` 指令后,仅仅是让循环没有被优化掉,然而每次循环中对变量 `a` 的赋值依然会被优化掉,结果就是连续执行了 1000 次 `mfence`。 124 | 125 | **抑制 CPU 乱序** 126 | 127 | 上面说到了编译器重排,那没有了编译器重排内存访问就会严格按照我们代码中的顺序执行了么?非也!现代 CPU 中的诸多特性均会影响这一行为。对于不同架构的 CPU 来说,其保证的内存存储模型是不一样的,比如 x86_64 就是所谓的 TSO(完全存储定序)模型,而很多 ARM 则是 RMO(宽松存储模型)。再加上多核间 Cache 一致性问题,多线程编程时会面临更多的挑战。 128 | 129 | 为了解决这些问题,从根本上来说只有通过插入所谓的 Memory Barrier 内存屏障指令来解决,这些指令会使得 CPU 保证特定的内存访问序及内存写入操作在多核间的可见性。然而由于不同处理器架构间的内存模型和具体 Memory Barrier 指令均不相同,需要在什么位置添加哪条指令并不具有通用性,因此 C++ 11 在此基础上做了一层抽象,引入了 `atomic` 类型及 Memory Order 的概念,有助于写出更通用的代码。从本质上看就是靠编译器来根据代码中指定的高层次 Memory Order 来自动选择是否需要插入特定处理器架构上低层次的内存屏障指令。 130 | 131 | 关于 Memory Order,内存模型,内存屏障等东西的原理和具体使用方法网上已经有很多写得不错的文章了,可以参考文末的几篇参考资料。 132 | 133 | **保证访问原子性** 134 | 135 | 所谓访问原子性就是 Read,Write 操作是否存在中间状态,具体如何实现原子性的访问与处理器指令集有很大关系,如果处理器本身就支持某些原子操作指令,如 Atomic Store, Atomic Load,Atomic Fetch Add,Atomic Compare And Swap(CAS)等,那只需要在代码生成时选择合适的指令即可,否则需要依赖锁来实现。C++ 中提供的可移植通用方法就是 `std::atomic`,`volatile` 及 Memory Barrier 均与此完全无关。 136 | 137 | **总结** 138 | 139 | 从上面的比较中可以看出,`volatile`,`atomic` 及 Memory Barrier 的适用范围还是比较好区分的。 140 | 141 | - 如果需要原子性的访问支持,只能选择 `atomic`; 142 | - 如果仅仅只是需要保证内存访问不会被编译器优化掉,优先考虑 `volatile`; 143 | - 如果需要保证 Memory Order,也优先考虑 `atomic`,只有当不需要保证原子性,而且很明确要在哪插入内存屏障时才考虑手动插入 Memory Barrier。 144 | 145 | volatile是作用在单线程下,针对特定内存使用的。 146 | 147 | atomic是c++11提供的原子类操作 148 | 149 | volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。**遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。**声明时语法:int volatile vInt; 当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。 150 | 151 | volatile用在如下的几个地方: 152 | 153 | - 中断服务程序中修改的供其它程序检测的变量需要加volatile; 154 | - 多任务环境下各任务间共享的标志应该加volatile; 155 | - 存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义; 156 | 157 | - atomic可用在多线程中,对小数据进行原子操作,是安全的 158 | - volatile用于特殊内存防止编译器级别的优化,atomic用于普通内存,优化没有副作用,如果是特殊内存就用volatile,如果并且是多线程,可以叠加atomic使用 159 | - 可以搭配一起使用,atomic并不能取代volatile的作用,volatile也没有atomic的功能 160 | 161 | register关键字:这个关键字请求编译器尽可能的将变量存在CPU内部寄存器中,而不是通过内存寻址访问,以提高效率 162 | 163 | #### **请简述一下atomoic内存顺序** 164 | 165 | 有六个内存顺序选项可应用于对原子类型的操作: 166 | 167 | 1. memory_order_relaxed:在原子类型上的操作以自由序列执行,没有任何同步关系,仅对此操作要求原子性。 168 | 2. memory_order_consume:memory_order_consume只会对其标识的对象保证该对象存储先行于那些需要加载该对象的操作。 169 | 3. memory_order_acquire:使用memory_order_acquire的原子操作,当前线程的读写操作都不能重排到此操作之前。 170 | 4. memory_order_release:使用memory_order_release的原子操作,当前线程的读写操作都不能重排到此操作后。 171 | 5. memory_order_acq_rel:memory_order_acq_rel在此内存顺序的读-改-写操作既是获得加载又是释放操作。没有操作能够从此操作之后被重排到此操作之前,也没有操作能够从此操作之前被重排到此操作之后。 172 | 6. memory_order_seq_cst:memory_order_seq_cst比std::memory_order_acq_rel更为严格。memory_order_seq_cst不仅是一个"获取释放"内存顺序,它还会对所有拥有此标签的内存操作建立一个单独全序。 173 | 174 | 除非你为特定的操作指定一个顺序选项,否则内存顺序选项对于所有原子类型默认都是memory_order_seq_cst。 175 | -------------------------------------------------------------------------------- /C++/新特性.md: -------------------------------------------------------------------------------- 1 | # 新特性 2 | 3 | ## 问题 4 | 5 | ### 语言特点 6 | 7 | * C++ 语言的特点 8 | * C++11 新特性 9 | * C++ 与 C 的区别 10 | * 谈谈 extern “C” 11 | * C++ 与 JAVA 的区别 12 | * C++ 与 PYTHON 的区别 13 | 14 | ### 新特性 15 | 16 | * lambda 怎么实现的 17 | 18 | * auto 和 decltype、长度 decltype(auto) 19 | 20 | * auto 类型推断原理,拷贝还是引用 21 | 22 | ## 回答 23 | 24 | ### 语言特点 25 | 26 | #### C++ 语言的特点 27 | 28 | * C++在C语言基础上引入了**面对对象**的机制,同时也**兼容C语言**。 29 | * C++有三大特性(1)封装。(2)继承。(3)多态; 30 | * C++语言编写出的程序结构清晰、易于扩充,程序**可读性好**。 31 | * C++生成的代码**质量高**,运行**效率高**,仅比汇编语言慢10%~20%; 32 | * C++更加安全,增加了const常量、引用、四类cast转换(static_cast、dynamic_cast、const_cast、reinterpret_cast)、智能指针、try—catch等等; 33 | * C++**可复用性**高,C++引入了**模板**的概念,后面在此基础上,实现了方便开发的标准模板库STL(Standard Template Library) 34 | * 同时,C++是**不断在发展**的语言。C++后续版本更是发展了不少新特性,如C++11中引入了nullptr、auto变量、Lambda匿名函数、右值引用、智能指针。 35 | 36 | #### C++11 新特性 37 | 38 | * nullptr替代 NULL 39 | * 引入了 auto 和 decltype 这两个关键字实现了类型推导 40 | * 基于范围的 for 循环for(auto& i : res){} 41 | * 类和结构体的中初始化列表 42 | * Lambda 表达式(匿名函数) 43 | * std::forward_list(单向链表) 44 | * 右值引用和move语义 45 | 46 | #### C 与 C++ 的区别 47 | 48 | * C++中new和delete是对内存分配的运算符,取代了C中的malloc和free。 49 | * 标准C++中的字符串类取代了标准C函数库头文件中的字符数组处理函数(C中没有字符串类型)。 50 | * C++中用来做控制态输入输出的iostream类库替代了标准C中的stdio函数库。 51 | * C++中的try/catch/throw异常处理机制取代了标准C中的setjmp()和longjmp()函数。 52 | * 在C++中,允许有相同的函数名,不过它们的参数类型不能完全相同,这样这些函数就可以相互区别开来。而这在C语言中是不允许的。也就是C++可以重载,C语言不允许。 53 | * C++语言中,允许变量定义语句在程序中的任何地方,只要在是使用它之前就可以;而C语言中,必须要在函数开头部分。而且C++不允许重复定义变量,C语言也是做不到这一点的 54 | * 在C++中,除了值和指针之外,新增了引用。引用型变量是其他变量的一个别名,我们可以认为他们只是名字不相同,其他都是相同的。 55 | * C++相对与C增加了一些关键字,如:bool、using、dynamic_cast、namespace等等 56 | * C++是**面对对象**的编程语言;C语言是**面对过程**的编程语言。 57 | * C语言有一些不安全的语言特性,如指针使用的潜在危险、强制转换的不确定性、内存泄露等。而C++对此增加了不少新特性来**改善安全性**,如const常量、引用、cast转换、智能指针、try—catch等等; 58 | * C++**可复用性**高,C++引入了**模板**的概念,后面在此基础上,实现了方便开发的标准模板库STL。C++的STL库相对于C语言的函数库**更灵活、更通用**。 59 | 60 | #### 谈谈 extern “C” 61 | 62 | 为了能够**正确的在C++代码中调用C语言**的代码:在程序中加上extern "C"后,相当于告诉编译器这部分代码是C语言写的,因此要按照C语言进行编译,而不是C++; 63 | 64 | **编译区别:**由于C++支持函数重载,因此编译器编译函数的过程中会将函数的**参数类型**也加到编译后的代码中,而不仅仅是**函数名**;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括**函数名**。 65 | 66 | 哪些情况下使用extern "C": 67 | 68 | (1)C++代码中调用C语言代码; 69 | 70 | (2)在C++中的头文件中使用; 71 | 72 | (3)在多个人协同开发时,可能有人擅长C语言,而有人擅长C++; 73 | 74 | 举个例子,C++中调用C代码: 75 | 76 | ```d 77 | #ifndef __MY_HANDLE_H__ 78 | #define __MY_HANDLE_H__ 79 | 80 | extern "C"{ 81 | typedef unsigned int result_t; 82 | typedef void* my_handle_t; 83 | 84 | my_handle_t create_handle(const char* name); 85 | result_t operate_on_handle(my_handle_t handle); 86 | void close_handle(my_handle_t handle); 87 | } 88 | ``` 89 | 90 | 综上,总结出使用方法**,在C语言的头文件中,对其外部函数只能指定为extern类型,C语言中不支持extern "C"声明,在.c文件中包含了extern "C"时会出现编译语法错误。**所以使用extern "C"全部都放在于cpp程序相关文件或其头文件中。 91 | 92 | ```c 93 | //extern示例 94 | //在C++程序里边声明该函数,会指示编译器这部分代码按C语言的进行编译 95 | extern "C" int strcmp(const char *s1, const char *s2); 96 | //在C++程序里边声明该函数 97 | extern "C"{ 98 | #include //string.h里边包含了要调用的C函数的声明 99 | } 100 | //两种不同的语言,有着不同的编译规则,比如一个函数fun,可能C语言编译的时候为_fun,而C++则是__fun__ 101 | ``` 102 | 103 | #### C++ 与 JAVA 的区别 104 | 105 | **语言特性** 106 | 107 | * Java语言给开发人员提供了更为简洁的语法;完全面向对象,由于JVM可以安装到任何的操作系统上,所以说它的可移植性强 108 | * Java语言中没有指针的概念,引入了真正的数组。不同于C++中利用指针实现的“伪数组”,Java引入了真正的数组,同时将容易造成麻烦的指针从语言中去掉,这将有利于防止在C++程序中常见的因为数组操作越界等指针操作而对系统数据进行非法读写带来的不安全问题 109 | * C++也可以在其他系统运行,但是需要不同的编码(这一点不如Java,只编写一次代码,到处运行),例如对一个数字,在windows下是大端存储,在unix中则为小端存储。Java程序一般都是生成字节码,在JVM里面运行得到结果 110 | * Java用接口(Interface)技术取代C++程序中的抽象类。接口与抽象类有同样的功能,但是省却了在实现和维护上的复杂性 111 | 112 | **垃圾回收** 113 | 114 | * C++用析构函数回收垃圾,写C和C++程序时一定要注意内存的申请和释放 115 | * Java语言不使用指针,内存的分配和回收都是自动进行的,程序员无须考虑内存碎片的问题 116 | 117 | **应用场景** 118 | 119 | * Java在桌面程序上不如C++实用,C++可以直接编译成exe文件,指针是c++的优势,可以直接对内存的操作,但同时具有危险性 。(操作内存的确是一项非常危险的事情,一旦指针指向的位置发生错误,或者误删除了内存中某个地址单元存放的重要数据,后果是可想而知的) 120 | * Java在Web 应用上具有C++ 无可比拟的优势,具有丰富多样的框架 121 | * 对于底层程序的编程以及控制方面的编程,C++很灵活,因为有句柄的存在 122 | 123 | #### C++ 与 PYTHON 的区别 124 | 125 | * Python是一种脚本语言,是解释执行的,而C++是编译语言,是需要编译后在特定平台运行的。python可以很方便的跨平台,但是效率没有C++高。 126 | * Python使用缩进来区分不同的代码块,C++使用花括号来区分 127 | * C++中需要事先定义变量的类型,而Python不需要,Python的基本数据类型只有数字,布尔值,字符串,列表,元组等等 128 | * Python的库函数比C++的多,调用起来很方便 129 | 130 | ### 新特性 131 | 132 | #### lambda 怎么实现的 133 | 134 | **为什么要 lambda** 135 | 136 | C++11 之前 sort 排序想要自定义比较函数的话 137 | 138 | * 普通函数(函数指针作为参数),有最小的语法开销但不能限定作用域 139 | * 函数对象(类对象作为参数),可以在作用域内定义,但需要类定义的语法开销 140 | 141 | lambda 可以兼具两者的优点 142 | 143 | 144 | 145 | 概念:lambda 是一个可调用的代码单元,可以理解为一个匿名的内联函数 146 | 147 | 148 | 149 | https://www.zhihu.com/question/24962794/answer/2440279134 150 | 151 | 可以用 cppinsights 来看编译器视角下的 lambda 152 | 153 | ```c++ 154 | int main() { 155 | int x = 5; 156 | auto fun = [x]() { printf("%d\n", x); }; 157 | fun(); 158 | return 0; 159 | } 160 | ``` 161 | 162 | cppinsights 163 | 164 | ```c++ 165 | int main() 166 | { 167 | int x = 5; 168 | 169 | class __lambda_8_14 170 | { 171 | public: 172 | inline /*constexpr */ void operator()() const 173 | { 174 | printf("%d\n", x); 175 | } 176 | 177 | private: 178 | int x; 179 | 180 | public: 181 | __lambda_8_14(int & _x) 182 | : x{_x} 183 | {} 184 | 185 | }; 186 | 187 | __lambda_8_14 fun = __lambda_8_14{x}; 188 | fun.operator()(); 189 | return 0; 190 | } 191 | ``` 192 | 193 | 编译器会给 lambda 生成一个匿名类,重载了 operator(),跟仿函数cha'bu'd 194 | 195 | * lambda表达式中的**捕获列表**,对应lambda_xxxx类的**private 成员** 196 | * lambda表达式中的**形参列表**,对应lambda_xxxx类成员函数 **operator()的形参列表** 197 | * lambda表达式中的**mutable**,对应lambda_xxxx类[成员函数](https://www.zhihu.com/search?q=成员函数&search_source=Entity&hybrid_search_source=Entity&hybrid_search_extra={"sourceType"%3A"answer"%2C"sourceId"%3A"2440279134"}) **operator() 的常属性 const**,即是否是**常成员函数** 198 | * lambda表达式中的**返回类型**,对应lambda_xxxx类成员函数 **operator() 的返回类型** 199 | * lambda表达式中的**函数体**,对应lambda_xxxx类成员函数 **operator() 的函数体** 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | #### auto 和 decltype 208 | 209 | auto和decltype都是c++11新增的关键字,都用于自动类型推到,但是语法格式有区别。一句话概括,当你需要某个表达式的返回值类型而又不想实际执行它时用decltype。 210 | 211 | * **auto** 212 | 213 | auto的出现有对我的代码来说两个作用: 214 | 215 | 1. 为了避免太长的类型描述影响代码可读性。比如`std::vector::iterator it = myArray.begin();` 216 | 2. 这个类型不是我们所关注的,也会用。比如遍历容器中的值,用到 217 | 218 | auto自动帮我们获得定义变量的类型所以auto定义的变量必须有初始值。 219 | 220 | auto可以推断基本类型,也可以推断引用类型,当推断引用类型时候,将引用对象的类型作为推断类型(重要) 221 | 222 | auto的自动类型推断发生在编译期,所以使用auto并不会造成程序运行时效率的降低。 223 | 224 | auto仅仅保存底层const,顶层const会被忽略(又叫顶层修饰符) 225 | 226 | 编译器可以根据初始值自动推导出类型。但是不能用于函数传参以及数组类型的推导 227 | 228 | * **decltype** 229 | 230 | 一句话概括,当你需要某个表达式的返回值类型而又不想实际执行它时用decltype。 231 | 232 | 有时候我们想从表达式中推断出要定义的类型,不会真的求表达式的值。 233 | 234 | `decltype`是为了解决复杂的类型声明而使用的关键字 235 | 236 | 1. auto忽略顶层const,decltype保留顶层const; 237 | 2. 对引用操作,auto推断出原有类型,decltype推断出引用;(重要) 238 | 3. 对解引用操作,auto推断出原有类型,decltype推断出引用; 239 | 4. auto推断时会实际执行,decltype不会执行,只做分析。 240 | 241 | * **有了auto为什么还需要decltype?** 242 | 243 | `decltype` 可以让你获得编译期的类型。 `auto`不能。所以**当你需要某个表达式的返回值类型而又不想实际执行它时用decltype**。 244 | 245 | int a=8, b=3; 246 | auto c=a+b; //运行时需要实际执行a+b,哪怕编译时就能推导出类型 247 | decltype(a+b) d; //编译期类型推导 248 | //不可以用auto c; 直接声明变量,必须同时初始化。 249 | 250 | **(1)auto** 251 | 252 | C++11新标准引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型。和原来那些只对应某种特定的类型说明符(例如 int)不同, 253 | 254 | **auto 让编译器通过初始值来进行类型推演。从而获得定义变量的类型,所以说 auto 定义的变量必须有初始值。** 255 | 256 | 举个例子: 257 | 258 | ```plaintext 259 | //普通;类型 260 | int a = 1, b = 3; 261 | auto c = a + b;// c为int型 262 | 263 | //const类型 264 | const int i = 5; 265 | auto j = i; // 变量i是顶层const, 会被忽略, 所以j的类型是int 266 | auto k = &i; // 变量i是一个常量, 对常量取地址是一种底层const, 所以b的类型是const int* 267 | const auto l = i; //如果希望推断出的类型是顶层const的, 那么就需要在auto前面加上cosnt 268 | 269 | //引用和指针类型 270 | int x = 2; 271 | int& y = x; 272 | auto z = y; //z是int型不是int& 型 273 | auto& p1 = y; //p1是int&型 274 | auto p2 = &x; //p2是指针类型int* 275 | ``` 276 | 277 | **(2)decltype** 278 | 279 | 有的时候我们还会遇到这种情况,**我们希望从表达式中推断出要定义变量的类型,但却不想用表达式的值去初始化变量。**还有可能是函数的返回类型为某表达式的值类型。在这些时候auto显得就无力了,所以C++11又引入了第二种类型说明符decltype,**它的作用是选择并返回操作数的数据类型。在此过程中,编译器只是分析表达式并得到它的类型,却不进行实际的计算表达式的值。** 280 | 281 | ```plaintext 282 | int func() {return 0}; 283 | 284 | //普通类型 285 | decltype(func()) sum = 5; // sum的类型是函数func()的返回值的类型int, 但是这时不会实际调用函数func() 286 | int a = 0; 287 | decltype(a) b = 4; // a的类型是int, 所以b的类型也是int 288 | 289 | //不论是顶层const还是底层const, decltype都会保留 290 | const int c = 3; 291 | decltype(c) d = c; // d的类型和c是一样的, 都是顶层const 292 | int e = 4; 293 | const int* f = &e; // f是底层const 294 | decltype(f) g = f; // g也是底层const 295 | 296 | //引用与指针类型 297 | //1. 如果表达式是引用类型, 那么decltype的类型也是引用 298 | const int i = 3, &j = i; 299 | decltype(j) k = 5; // k的类型是 const int& 300 | 301 | //2. 如果表达式是引用类型, 但是想要得到这个引用所指向的类型, 需要修改表达式: 302 | int i = 3, &r = i; 303 | decltype(r + 0) t = 5; // 此时是int类型 304 | 305 | //3. 对指针的解引用操作返回的是引用类型 306 | int i = 3, j = 6, *p = &i; 307 | decltype(*p) c = j; // c是int&类型, c和j绑定在一起 308 | 309 | //4. 如果一个表达式的类型不是引用, 但是我们需要推断出引用, 那么可以加上一对括号, 就变成了引用类型了 310 | int i = 3; 311 | decltype((i)) j = i; // 此时j的类型是int&类型, j和i绑定在了一起 312 | ``` 313 | 314 | **(3)decltype(auto)** 315 | 316 | decltype(auto)是C++14新增的类型指示符,可以用来声明变量以及指示函数返回类型。在使用时,会将“=”号左边的表达式替换掉auto,再根据decltype的语法规则来确定类型。举个例子: 317 | 318 | ```plaintext 319 | int e = 4; 320 | const int* f = &e; // f是底层const 321 | decltype(auto) j = f;//j的类型是const int* 并且指向的是e 322 | ``` 323 | 324 | #### auto 类型推断原理 325 | 326 | auto使用的是**模板实参推断**(Template Argument Deduction)的机制。 327 | 328 | 从函数实参来确定模板实参的过程被称为模板实参推断(template argument deduction); 329 | 330 | 在使用类模板创建对象时,程序员需要显式的指明实参(也就是具体的类型),而对于函数模板,调用函数时可以不显式地指明实参,编译器会根据传入的实参类型来自动推导出模板实参类型。 331 | 332 | 看代码: 333 | 334 | ```c++ 335 | template 336 | void useContainer(const Container& container) 337 | { 338 | auto pos = container.begin(); 339 | while (pos != container.end()) 340 | { 341 | auto& element = *pos++; 342 | … // 对元素进行操作 343 | } 344 | } 345 | 346 | // auto pos = container.begin()的推断等价于如下调用模板的推断 347 | template 348 | void deducePos(T pos); 349 | deducePos(container.begin()); 350 | ``` 351 | 352 | auto被一个虚构的模板类型参数T替代,然后进行推断,即相当于把变量设为一个函数参数,将其传递给模板并推断为实参,auto相当于利用了其中进行的实参推断,承担了模板参数T的作用 353 | 354 | #### enum class 、enum struct 和 enum 355 | 356 | 枚举类型(enumeration)使我们可以将一组整型常量组织在一起。每个枚举类型定义了一种新的类型。枚举属于字面值常量类型。 357 | 358 | * 旧版本enum存在的问题 359 | 360 | 1. 没有非常完全的类型安全。即不同枚举之间不能赋值,同时整形不能向枚举类型转换,但是枚举可以向整形转。 361 | 362 | 2. 无法确定数据的类型,导致无法明确枚举类型所占用的内存大小。 363 | 3. 枚举中的成员可以在括号外部直接访问,而不需要使用域运算符。 364 | 365 | * enum class 、enum struct 366 | 367 | 有更好的类型安全和封装的特性。 368 | 369 | 1. 与整形之间不会发生隐式类型转换,除非用static_cast强制转换 370 | 2. 可以指定底层的数据类型,默认是int 371 | 3. 需要通过域运算符来访问枚举成员 372 | -------------------------------------------------------------------------------- /C++/模板.md: -------------------------------------------------------------------------------- 1 | # 模板 2 | 3 | ## 问题 4 | 5 | ### 基本认识 6 | 7 | C++ 模板底层实现(编译过程) 8 | 9 | 模板和实现可不可以不写在一个文件里面?为什么? 10 | 11 | 为什么模板类一般都是放在一个h文件中 12 | 13 | ### 类模板和模板函数 14 | 15 | * 解释下 C++ 中类模板和模板类的区别? 16 | * 模板类是什么时候实现的 17 | * 类模板和模板函数的区别 18 | * 类模板中成员函数创建时机 19 | 20 | ### 可变参数模板 21 | 22 | * va_list 原理 23 | * 递归和非递归的参数包展开 24 | * 可变参数宏和参数包如何实现 25 | * 折叠表达式 26 | * 参数包怎么展开 27 | * 为什么可变参数模板至关重要 28 | 29 | 模板类是编译时确定还是运行时确定 30 | 31 | 模板实例化发生在哪个阶段?实例化之后存在几份? 32 | 33 | 一个模板类在不同特化之后,得到的类还是不是同一个类? 34 | 35 | ## 技巧 36 | 37 | 谈谈 SFINAE 38 | 39 | 谈谈 CRTP 40 | 41 | ## 回答 42 | 43 | ### 基本认识 44 | 45 | #### C++ 模板底层实现(编译过程) 46 | 47 | 编译器并不是把函数模板处理成能够处理任意类的函数;编译器从函数模板通过具体类型产生不同的函数;编译器会对函数模板进行两次编译:在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译。 48 | 49 | 50 | 51 | 这是因为函数模板要被实例化后才能成为真正的函数,在使用函数模板的源文件中包含函数模板的头文件,如果该头文件中只有声明,没有定义,那编译器无法实例化该模板,最终导致链接错误。 52 | 53 | 54 | 55 | #### 为什么模板类一般都是放在一个h文件中 56 | 57 | 模板定义很特殊。由template<…>处理的任何东西都意味着编译器在当时不为它分配存储空间,它一直处于等待状态直到被一个模板实例告知。在编译器和连接器的某一处,有一机制能去掉指定模板的多重定义。 58 | 59 | 所以为了容易使用,几乎总是在头文件中放置全部的模板声明和定义。 60 | 61 | 在分离式编译的环境下,编译器编译某一个.cpp文件时并不知道另一个.cpp文件的存在,也不会去查找(当遇到未决符号时它会寄希望于连接器)。这种模式在没有模板的情况下运行良好,但遇到模板时就傻眼了,因为模板仅在需要的时候才会实例化出来。 62 | 63 | 所以,当编译器只看到模板的声明时,它不能实例化该模板,只能创建一个具有外部连接的符号并期待连接器能够将符号的地址决议出来。 64 | 65 | 然而当实现该模板的.cpp文件中没有用到模板的实例时,编译器懒得去实例化,所以,整个工程的.obj中就找不到一行模板实例的二进制代码,于是连接器也黔驴技穷了。 66 | 67 | ### 类模板和模板函数 68 | 69 | #### 解释下 C++ 中类模板和模板类的区别? 70 | 71 | 类模板是模板的定义,不是一个实实在在的类,定义中用到通用类型参数 72 | 73 | 模板类是实实在在的类定义,是类模板的实例化。类定义中参数被实际类型所代替。 74 | 75 | 类模板的类型参数可以有一个或多个,每个类型前面都必须加class,如template class someclass{…};在定义对象时分别代入实际的类型名,如 someclass obj; 76 | 77 | 和使用类一样,使用类模板时要注意其作用域,只能在其有效作用域内用它定义对象。 78 | 79 | 模板可以有层次,一个类模板可以作为基类,派生出派生模板类。 80 | 81 | #### 类模板和模板函数的区别 82 | 83 | 函数模板的实例化是由编译程序在处理函数调用时自动完成的,而类模板的实例化必须由程序员在程序中显式地指定。即函数模板允许隐式调用和显式调用而类模板只能显示调用。在使用时类模板必须加,而函数模板不必 84 | 85 | 类模板与函数模板区别主要有两点: 86 | 87 | 1. 类模板没有自动类型推导的使用方式 88 | 2. 类模板在模板参数列表中可以有默认参数(只有类模板可以) 89 | 90 | 类模板使用只能用显示指定类型方式 91 | 92 | 类模板中的模板参数列表可以有默认参数 93 | 94 | #### 类模板中成员函数创建时机 95 | 96 | 类模板中成员函数和普通类中成员函数创建时机是有区别的: 97 | 98 | * 普通类中的成员函数一开始就可以创建 99 | * **类模板中的成员函数在调用时才创建** 100 | 101 | 102 | 103 | ### 可变参数模板 104 | 105 | C++ 可变参数模板(Variadic Templates)是 C++11 新增的特性,它允许我们定义一个函数或类模板,使其可以接受任意数量和类型的参数。 106 | 107 | 在函数模板中,我们可以使用 `...` 语法来表示可变参数 108 | 109 | ``` 110 | template 111 | void foo(Args... args) { 112 | // ... 113 | } 114 | ``` 115 | 116 | 原理:可变参数模板的实现原理可以简单地理解为递归展开参数包。 117 | 118 | 在函数模板中,函数参数包可以通过递归展开来访问其中的每个参数 119 | -------------------------------------------------------------------------------- /Golang/GRPC.md: -------------------------------------------------------------------------------- 1 | # GRPC 2 | 3 | ## ProtoBuf 4 | 5 | ## 拦截器 6 | 7 | 8 | 9 | 17. grpc怎么用的 10 | 18. 为什么grpc速度快 11 | 19. 怎么测的速度 12 | 13 | 10. 你项目gprc用的什么协议规定 14 | 15 | 用的protobuf 16 | 17 | \-- 18 | 19 | 11.有了解过protobuf是用什么进行序列化的吗 20 | 21 | 不了解 22 | 23 | \-- 24 | 25 | 2. grpc有几种通信方式 26 | 27 | (不懂 , 提示我有流 ,还有其他什么没,没有答上来) 28 | \-- 29 | 30 | 3. gprc使用什么进行暴露接口 31 | 32 | gate-way 33 | \-- -------------------------------------------------------------------------------- /Golang/unsafe.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PokIsemaine/interview/bd5da05b557897a71361f8dbb9a6a6d25cf28d46/Golang/unsafe.md -------------------------------------------------------------------------------- /Golang/优化.md: -------------------------------------------------------------------------------- 1 | # 优化 2 | 3 | [golang 性能优化分析工具 pprof (上) - 基础使用介绍 - 九卷 - 博客园 (cnblogs.com)](https://www.cnblogs.com/jiujuan/p/14588185.html) 4 | 5 | ## 调优分析理论 6 | 7 | ### 调优内容 8 | 9 | 一般常规内容: 10 | 11 | 1. **cpu**:程序对cpu的使用情况 - 使用时长,占比等 12 | 2. **内存**:程序对cpu的使用情况 - 使用时长,占比,内存泄露等。如果在往里分,程序堆、栈使用情况 13 | 3. **I/O**:IO的使用情况 - 哪个程序IO占用时间比较长 14 | 15 | golang 程序中: 16 | 17 | 1. **goroutine**:go的协程使用情况,调用链的情况 18 | 2. **goroutine leak**:goroutine泄露检查 19 | 3. **go dead lock**:死锁的检测分析 20 | 4. **data race detector**:数据竞争分析,其实也与死锁分析有关 21 | 22 | 上面是在 golang 程序中,性能调优的一些内容。 23 | 24 | ### 调优方法 25 | 26 | 比如 linux 中 cpu 性能调试,工具有 top,dstat,perf 等。 27 | 28 | 那么在 golang 中,有哪些分析方法? 29 | 30 | **golang 性能调试优化方法:** 31 | 32 | - **Benchmark**:**基准测试**,对特定代码的运行时间和内存信息等进行测试 33 | - **Profiling**:**程序分析**,程序的运行画像,在程序执行期间,通过采样收集的数据对程序进行分析 34 | - **Trace**:**跟踪**,在程序执行期间,通过采集发生的事件数据对程序进行分析 35 | 36 | > profiling 和 trace 有啥区别? 37 | > profiling 分析没有时间线,trace 分析有时间线。 38 | 39 | ## 调优工具 40 | 41 | ### pprof 42 | 43 | 它的最原始程序是 [gperftools](https://github.com/gperftools/gperftools) - https://github.com/gperftools/gperftools,golang 的 pprof 是从它而来的。 44 | 45 | 46 | 47 | ## 常用数据结构 48 | 49 | ### 切片(slice) 性能及陷阱 50 | 51 | ### 使用空结构体节省内存 52 | 53 | 空结构体不占据任何的内存空间,常用来作为占位符适用 54 | 55 | * 节省资源 56 | * 空结构体本身就具备很强的语义 57 | 58 | 59 | #### 实现集合(Set) 60 | 61 | ```golang 62 | type Set map[string]struct{} 63 | 64 | func (s Set) Has(key string) bool { 65 | _, ok := s[key] 66 | return ok 67 | } 68 | 69 | func (s Set) Add(key string) { 70 | s[key] = struct{}{} 71 | } 72 | 73 | func (s Set) Delete(key string) { 74 | delete(s, key) 75 | } 76 | 77 | func main() { 78 | s := make(Set) 79 | s.Add("Tom") 80 | s.Add("Sam") 81 | fmt.Println(s.Has("Tom")) 82 | fmt.Println(s.Has("Jack")) 83 | } 84 | ``` 85 | 86 | 87 | 88 | #### 不发送数据的信道(channel) 89 | 90 | ```golang 91 | func worker(ch chan struct{}) { 92 | <-ch 93 | fmt.Println("do something") 94 | close(ch) 95 | } 96 | 97 | func main() { 98 | ch := make(chan struct{}) 99 | go worker(ch) 100 | ch <- struct{}{} 101 | } 102 | ``` 103 | 104 | 有时候使用 channel 不需要发送任何的数据,只用来通知子协程(goroutine)执行任务,或只用来控制协程并发度。这种情况下,使用空结构体作为占位符就非常合适了。 105 | 106 | #### 仅包含方法的结构体 107 | 108 | ```golang 109 | type Door struct{} 110 | 111 | func (d Door) Open() { 112 | fmt.Println("Open the door") 113 | } 114 | 115 | func (d Door) Close() { 116 | fmt.Println("Close the door") 117 | } 118 | ``` 119 | 120 | 在部分场景下,结构体只包含方法,不包含任何的字段。无论是 `int` 还是 `bool` 都会浪费额外的内存,因此呢,这种情况下,声明为空结构体是最合适的。 121 | 122 | 123 | 124 | ## 并发编程 125 | 126 | 127 | 128 | ### 池化 sync.Pool 129 | 130 | [瞬间提升 Go 程序性能:深入解析 Sync.Pool - 掘金 (juejin.cn)](https://juejin.cn/post/7228861445657051194) 131 | 132 | [Go 并发编程 — 深入浅出 sync.Pool ,最全的使用姿势,最深刻的原理-腾讯云开发者社区-腾讯云 (tencent.com)](https://cloud.tencent.com/developer/article/1847917) 133 | 134 | [sync.Pool:提高Go语言程序性能的关键一步 - 掘金 (juejin.cn)](https://juejin.cn/post/7219249005904445477#heading-0) 135 | 136 | [《深入Go语言之旅》之图解sync.Pool设计与实现 - 掘金 (juejin.cn)](https://juejin.cn/post/6978688329864708126) 137 | 138 | https://geektutu.com/post/hpg-sync-pool.html 139 | 140 | [深度解密 Go 语言之 sync.Pool - Stefno - 博客园 (cnblogs.com)](https://www.cnblogs.com/qcrao-2018/p/12736031.html) 141 | 142 | [ golang sync.Pool在1.14中的优化_惜暮的博客-CSDN博客](https://blog.csdn.net/u010853261/article/details/106156091) 143 | 144 | #### sync.Pool 是什么 145 | 146 | * 是 Go 语言的一个同步对象池 147 | * 是 Go 语言提供的一个用于管理临时对象的机制 148 | 149 | * 作用:用于存储和复用临时对象,尽可能的避免频繁创建和销毁对象的开销,以达到提高程序性能和减少垃圾回收负担的目的。 150 | 151 | #### 基本使用 152 | 153 | * 在创建 Sync.Pool 对象时,我们需要提供一个 New 函数作为初始化函数,该函数用于创建一个新的对象。 154 | * 在获取对象时,首先从 Sync.Pool 中查找是否有可用对象,如果有,则直接返回可用对象,如果没有,则调用 New 函数创建一个新的对象并返回。 155 | * 当我们使用完对象后,可以通过将对象放回 Sync.Pool 中来避免它被销毁,以便下次可以重复使用。但是需要注意的是,当**对象被放回到 Sync.Pool 中后,它并不保证立即可用,因为对象池的策略是在池中保留一定数量的对象,超出这个数量的对象会被销毁。** 156 | * Sync.Pool 是并发安全的,所以多个 goroutine 可以同时访问同一个 Sync.Pool 对象,从而共享池中的对象 157 | 158 | ```golang 159 |  package main 160 |   161 |  import ( 162 |      "fmt" 163 |      "sync" 164 |  ) 165 |   166 |  var pool *sync.Pool 167 |   168 |  func init() { 169 |      pool = &sync.Pool{ 170 |          New: func() interface{} { 171 |   fmt.Println("Creating new object") 172 |   return "Hello, World!" 173 |   }, 174 |   } 175 |  } 176 |   177 |  func main() { 178 |      // 从池中获取对象 179 |      obj := pool.Get().(string) 180 |      fmt.Println(obj) 181 |      // 归还对象到池中 182 |   pool.Put(obj) 183 |   184 |      // 再次获取对象,此时应该从池中获取 185 |      obj = pool.Get().(string) 186 |      fmt.Println(obj) 187 |  } 188 | 189 | ``` 190 | 191 | 192 | 193 | #### 实现原理 194 | 195 | Sync.Pool 的实现原理是基于一个简单的算法:**对象池**。对象池中存放了一些可重用的对象,当程序需要使用对象时,首先从对象池中查找是否有可用的对象,如果有,则直接返回可用对象,如果没有,则创建一个新的对象。当程序使用完对象后,将对象放回到对象池中,以便下次可以重复使用。 196 | 197 | 在 Sync.Pool 中,对象池是使用 sync.Pool 结构体来实现的。sync.Pool 中有两个字段:new 和 pool。 198 | 199 | * new 字段是一个函数类型,用于创建一个新的对象。 200 | * pool 字段是 sync.Pool 结构体的实际存储对象池的地方。sync.Pool 中使用了一个锁来保证并发安全,避免多个 goroutine 同时对 pool 进行操作。 201 | 202 | 当程序从 Sync.Pool 中获取对象时,首先尝试从 pool 中获取可用对象。如果 pool 中有可用对象,则直接返回可用对象。如果 pool 中没有可用对象,则调用 new 函数创建一个新的对象,并返回该对象。 203 | 204 | 当程序使用完对象后,可以将对象放回到 pool 中。但是需要注意的是,当对象被放回到 pool 中后,它并不保证立即可用,因为 pool 的策略是在池中保留一定数量的对象,超出这个数量的对象会被销毁。 205 | 206 | 207 | 208 | #### 应用场景 209 | 210 | ##### 不适用的情况/注意的 211 | 212 | * 对象的创建和销毁开销非常小 213 | * 对象的生命周期非常长 214 | * 需要注意放入对象的大小 215 | * 特别是会自动扩容的,有可能内存泄漏,扩容的部分一直占着 216 | * 可以考虑先判断放入对象的大小,如果太大,此时就不重新放入 sync.Pool 当中,而是直接丢弃 217 | * 不要往sync.Pool中放入数据库连接/TCP连接(如果没有显式地关闭连接对象。如果连接对象的数量很大,那么这些未关闭的连接对象就会占用大量的内存资源,导致内存泄漏等问题) 218 | 219 | 使用 Sync.Pool 可能会带来更多的负面影响,比如内存浪费和性能下降。 220 | 221 | 注意点 222 | 223 | 1. 不能对 Pool.Get 出来的对象做预判,有可能是新的(新分配的),有可能是旧的(之前人用过,然后 Put 进去的); 224 | 2. 不能对 Pool 池里的元素个数做假定,你不能够; 225 | 3. sync.Pool 本身的 Get, Put 调用是并发安全的,`sync.New` 指向的初始化函数会并发调用,里面安不安全只有自己知道; 226 | 4. 当用完一个从 Pool 取出的实例时候,一定要记得调用 Put,否则 Pool 无法复用这个实例,通常这个用 defer 完成; 227 | 228 | 229 | 230 | ##### 适用的情况 231 | 232 | **对象复用** 233 | 234 | 当程序频繁创建和销毁对象时,Sync.Pool 可以帮助我们减少创建和销毁的开销,提高程序性能。比如,在 HTTP 服务器中,每个请求都需要创建一个 Request 和 Response 对象,如果使用 Sync.Pool 来管理这些对象,可以减少对象的创建和销毁次数,提高服务器的性能。 235 | 236 | **减少内存分配** 237 | 238 | 当程序需要大量的内存分配时,Sync.Pool 可以帮助我们减少内存分配的次数,从而减少内存碎片和 GC 压力。比如,在数据库连接池中,每个连接对象都需要占用一定的内存空间,如果使用 Sync.Pool 来管理连接对象,可以避免大量的内存分配和回收操作,减少 GC 压力。 239 | 240 | **避免竞争条件** 241 | 242 | 在并发编程中,访问共享资源时需要加锁,而锁的开销是很大的。如果可以使用 Sync.Pool 来避免频繁的加锁和解锁操作,可以提高程序的性能。比如,在使用 bufio.Scanner 对大文件进行读取时,每次读取都需要创建一个缓冲区,如果使用 Sync.Pool 来管理缓冲区对象,可以避免频繁的锁操作,减少程序的性能开销。 243 | 244 | 245 | 246 | ##### 在标准库中的应用 247 | 248 | 249 | 250 | ## 内存分配模型 251 | 252 | -------------------------------------------------------------------------------- /Golang/内存管理.md: -------------------------------------------------------------------------------- 1 | # 内存管理 2 | 3 | [‍‌‍⁢⁢⁤⁡⁤‌⁤⁣⁣‬⁤⁢‍⁣⁤‬‌‍‬⁣‍‌‬‍‌⁡⁣‬‍‌Golang 内存分配 - 飞书云文档 (feishu.cn)](https://hardcore.feishu.cn/docs/doccnnKvHn4iFPXgk8ZpYmLArte) 4 | 5 | [Go 语言内存分配器的实现原理 | Go 语言设计与实现 (draveness.me)](https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-memory-allocator/#71-内存分配器) 6 | 7 | [Go 语言垃圾收集器的实现原理 | Go 语言设计与实现 (draveness.me)](https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-garbage-collector/) 8 | 9 | [Go 语言的栈内存和逃逸分析 | Go 语言设计与实现 (draveness.me)](https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-stack-management/) 10 | 11 | 12 | 13 | 14 | 15 | ## 内存分配器 16 | 17 | 18 | 19 | ## 垃圾回收 20 | 21 | ### 设计原理 22 | 23 | #### 标记清除 24 | 25 | 标记清除(Mark-Sweep)算法是最常见的垃圾收集算法,标记清除收集器是跟踪式垃圾收集器,其执行过程可以分成标记(Mark)和清除(Sweep)两个阶段: 26 | 27 | 1. 标记阶段 — 从根对象出发查找并标记堆中所有存活的对象; 28 | 2. 清除阶段 — 遍历堆中的全部对象,回收未被标记的垃圾对象并将回收的内存加入空闲链表; 29 | 30 | 如下图所示,内存空间中包含多个对象,我们从根对象出发依次遍历对象的子对象并将从根节点可达的对象都标记成存活状态,即 A、C 和 D 三个对象,剩余的 B、E 和 F 三个对象因为从根节点不可达,所以会被当做垃圾: 31 | 32 | ![mark-sweep-mark-phase](https://img.draveness.me/2020-03-16-15843705141797-mark-sweep-mark-phase.png) 33 | 34 | **图 7-22 标记清除的标记阶段** 35 | 36 | 标记阶段结束后会进入清除阶段,在该阶段中收集器会依次遍历堆中的所有对象,释放其中没有被标记的 B、E 和 F 三个对象并将新的空闲内存空间以链表的结构串联起来,方便内存分配器的使用。 37 | 38 | ![mark-sweep-sweep-phase](https://img.draveness.me/2020-03-16-15843705141803-mark-sweep-sweep-phase.png) 39 | 40 | **图 7-23 标记清除的清除阶段** 41 | 42 | 这里介绍的是最传统的标记清除算法,垃圾收集器从垃圾收集的根对象出发,递归遍历这些对象指向的子对象并将所有可达的对象标记成存活;标记阶段结束后,垃圾收集器会依次遍历堆中的对象并清除其中的垃圾,整个过程需要标记对象的存活状态,用户程序在垃圾收集的过程中也不能执行,我们需要用到更复杂的机制来解决 STW 的问题。 43 | 44 | #### 三色抽象 45 | 46 | 为了解决原始标记清除算法带来的长时间 STW,多数现代的追踪式垃圾收集器都会实现三色标记算法的变种以缩短 STW 的时间。三色标记算法将程序中的对象分成白色、黑色和灰色三类: 47 | 48 | - 白色对象 — 潜在的垃圾,其内存可能会被垃圾收集器回收; 49 | - 黑色对象 — 活跃的对象,包括不存在任何引用外部指针的对象以及从根对象可达的对象; 50 | - 灰色对象 — 活跃的对象,因为存在指向白色对象的外部指针,垃圾收集器会扫描这些对象的子对象; 51 | 52 | ![tri-color-objects](https://img.draveness.me/2020-03-16-15843705141808-tri-color-objects.png) 53 | 54 | **图 7-24 三色的对象** 55 | 56 | 在垃圾收集器开始工作时,程序中不存在任何的黑色对象,垃圾收集的根对象会被标记成灰色,垃圾收集器只会从灰色对象集合中取出对象开始扫描,当灰色集合中不存在任何对象时,标记阶段就会结束。 57 | 58 | ![tri-color-mark-sweep](https://img.draveness.me/2020-03-16-15843705141814-tri-color-mark-sweep.png) 59 | 60 | **图 7-25 三色标记垃圾收集器的执行过程** 61 | 62 | 三色标记垃圾收集器的工作原理很简单,我们可以将其归纳成以下几个步骤: 63 | 64 | 1. 从灰色对象的集合中选择一个灰色对象并将其标记成黑色; 65 | 2. 将黑色对象指向的所有对象都标记成灰色,保证该对象和被该对象引用的对象都不会被回收; 66 | 3. 重复上述两个步骤直到对象图中不存在灰色对象; 67 | 68 | 当三色的标记清除的标记阶段结束之后,应用程序的堆中就不存在任何的灰色对象,我们只能看到黑色的存活对象以及白色的垃圾对象,垃圾收集器可以回收这些白色的垃圾,下面是使用三色标记垃圾收集器执行标记后的堆内存,堆中只有对象 D 为待回收的垃圾: 69 | 70 | ![tri-color-mark-sweep-after-mark-phase](https://img.draveness.me/2020-03-16-15843705141821-tri-color-mark-sweep-after-mark-phase.png) 71 | 72 | **图 7-26 三色标记后的堆** 73 | 74 | 因为用户程序可能在标记执行的过程中修改对象的指针,所以三色标记清除算法本身是不可以并发或者增量执行的,它仍然需要 STW,在如下所示的三色标记过程中,用户程序建立了从 A 对象到 D 对象的引用,但是因为程序中已经不存在灰色对象了,所以 D 对象会被垃圾收集器错误地回收。 75 | 76 | ![tri-color-mark-sweep-and-mutator](https://img.draveness.me/2020-03-16-15843705141828-tri-color-mark-sweep-and-mutator.png) 77 | 78 | **图 7-27 三色标记与用户程序** 79 | 80 | 本来不应该被回收的对象却被回收了,这在内存管理中是非常严重的错误,我们将这种错误称为悬挂指针,即指针没有指向特定类型的合法对象,影响了内存的安全性[5](https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-garbage-collector/#fn:5),想要并发或者增量地标记对象还是需要使用屏障技术。 81 | 82 | 83 | 84 | 85 | 86 | ### 为什么小对象多了会造成GC压力? 87 | 88 | 通常小对象过多会导致GC三色法消耗过多的CPU。优化思路是,减少对象分配。 89 | 90 | 91 | 92 | ### GC 的触发条件? 93 | 94 | - 主动触发(手动触发),通过调用 runtime.GC 来触发GC,此调用阻塞式地等待当前GC运行完毕。 95 | - 被动触发,分为两种方式: 96 | - 使用系统监控,当超过两分钟没有产生任何GC时,强制触发 GC。 97 | - 使用步调(Pacing)算法,其核心思想是控制内存增长的比例,每次内存分配时检查当前内存分配量是否已达到阈值(环境变量GOGC):默认100%,即当内存扩大一倍时启用GC。 98 | 99 | 100 | 101 | ## 逃逸分析 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | ## 栈内存管理 110 | 111 | ### Go语言中的内存对齐了解吗? 112 | 113 | CPU 访问内存时,并不是逐个字节访问,而是以字长(word size)为单位访问。比如 32 位的 CPU ,字长为 4 字节,那么 CPU 访问内存的单位也是 4 字节。 114 | 115 | CPU 始终以字长访问内存,如果不进行内存对齐,很可能增加 CPU 访问内存的次数,例如: 116 | 117 | ![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6980893261734ba383e1e702c9723100~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp) 118 | 119 | 变量 a、b 各占据 3 字节的空间,内存对齐后,a、b 占据 4 字节空间,CPU 读取 b 变量的值只需要进行一次内存访问。如果不进行内存对齐,CPU 读取 b 变量的值需要进行 2 次内存访问。第一次访问得到 b 变量的第 1 个字节,第二次访问得到 b 变量的后两个字节。 120 | 121 | 也可以看到,内存对齐对实现变量的原子性操作也是有好处的,每次内存访问是原子的,如果变量的大小不超过字长,那么内存对齐后,对该变量的访问就是原子的,这个特性在并发场景下至关重要。 122 | 123 | 简言之:合理的内存对齐可以提高内存读写的性能,并且便于实现变量操作的原子性。 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | ## 内存泄漏 132 | 133 | #### 在Go函数中为什么会发生内存泄露? 134 | 135 | Goroutine 需要维护执行用户代码的上下文信息,在运行过程中需要消耗一定的内存来保存这类信息,如果一个程序持续不断地产生新的 goroutine,且不结束已经创建的 goroutine 并复用这部分内存,就会造成内存泄漏的现象。 136 | 137 | #### Goroutine发生了泄漏如何检测? 138 | 139 | 可以通过Go自带的工具pprof或者使用Gops去检测诊断当前在系统上运行的Go进程的占用的资源。 -------------------------------------------------------------------------------- /Golang/标准库.md: -------------------------------------------------------------------------------- 1 | # 常用包 2 | 3 | ## HTTP 4 | 5 | 为什么json包不能 导出私有变量的tag? https://mp.weixin.qq.com/s/P7TEx2mInwEktXTEE6JDWQ 6 | 7 | ## JSON 8 | 9 | golang⾯试题:json包变量不加tag会怎么样? https://mp.weixin.qq.com/s/bZlKV_BWSqc-qCa4DrsCbg 10 | 11 | ## 反射 12 | 13 | golang⾯试题:reflect(反射包)如何获取字段tag? https://mp.weixin.qq.com/s/P7TEx2mInwEktXTEE6JDWQ 14 | 15 | ## 数据库 -------------------------------------------------------------------------------- /Golang/测试.md: -------------------------------------------------------------------------------- 1 | # Golang 测试 2 | 3 | ## 单元测试 4 | 5 | 6 | 7 | ## 基准测试 8 | 9 | ### 什么是 BenchMark 10 | 11 | 12 | 13 | ### 怎么写 BenchMark 14 | 15 | ```golang 16 | import ( 17 | "fmt" 18 | "testing" 19 | ) 20 | func BenchmarkSprint(b *testing.B) { 21 | b.ResetTimer() 22 | for i := 0; i < b.N; i++ { 23 | fmt.Sprint(i) 24 | } 25 | } 26 | ``` 27 | 28 | 对以上代码做如下说明: 29 | 30 | 1. 基准测试代码文件必须是_test.go结尾,和单元测试一样; 31 | 2. 基准测试的函数以Benchmark开头; 32 | 3. 参数须为 *testing.B; 33 | 4. 基准测试函数不能有返回值; 34 | 5. b.ResetTimer是重置计时器,这样可以避免for循环之前的初始化代码的干扰; 35 | 6. b.N是基准测试框架提供的,Go会根据系统情况生成,不用用户设定,表示循环的次数,因为需要反复调用测试的代码,才可以评估性能; 36 | 37 | ### 并发环境 38 | 39 | 40 | 41 | ## testing 包源码解析 42 | 43 | 44 | 45 | ## 覆盖率 46 | 47 | [聊聊Go代码覆盖率技术与最佳实践 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/279177026) 48 | 49 | [Go的测试覆盖率 | Brantou的日常](https://brantou.github.io/2017/05/24/go-cover-story/) 50 | 51 | 52 | 53 | ### go test 54 | 55 | * `-covermode` 56 | * `set` :语句是否执行(默认) 57 | * `count`:每个语句执行次数 58 | * `atomic`:类似 count,表示并发程序中精准计数 59 | * `-coverprofile=XXX.out` 测试结果 60 | 61 | ### go tool cover 62 | 63 | * `-func=xxx.out` 64 | * `-html=xxx.out` 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /Golang/脑图.md: -------------------------------------------------------------------------------- 1 | [GO成长路线-ProcessOn](https://www.processon.com/view/link/5ff500aa1e08531de81e1288) -------------------------------------------------------------------------------- /Golang/项目构建.md: -------------------------------------------------------------------------------- 1 | # 项目构建 2 | 3 | 4 | 5 | ## Go Mod 6 | 7 | [Go Modules Reference - The Go Programming Language](https://go.dev/ref/mod) 8 | 9 | 10 | 11 | ### Go Mod Tidy 12 | 13 | 14 | 15 | 16 | 17 | ### Go Mod Vendor 18 | 19 | [Go:go mod vendor 使用_test1280的博客-CSDN博客](https://blog.csdn.net/test1280/article/details/120855865) 20 | 21 | [Go Modules Reference - The Go Programming Language go-mod-vendor](https://go.dev/ref/mod#go-mod-vendor) 22 | 23 | [Go Modules Reference - The Go Programming Language vendoring](https://go.dev/ref/mod#vendoring) 24 | 25 | 26 | 27 | 28 | 29 | ### 同步 Go 模块 30 | 31 | 32 | 33 | ## pprof 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /HR 面.md: -------------------------------------------------------------------------------- 1 | # HR 面 2 | 3 | * 自我介绍 4 | * 优点 5 | * 缺点 6 | * 最有成就感的事情 7 | * 学习情况 8 | * 介绍项目:为什么做这个,是投入使用的吗,项目的难点,亮点,有什么问题,提升点 9 | * 讲讲参加的比赛 10 | * 讲讲自己当前的情况,概述当前的规划 11 | * 第一份工作考虑的点 12 | * 最想做的工作岗位 13 | * 为什么选择这个岗位 14 | * 谈谈对这个岗位的理解 15 | * 对于 ToB 和 ToC 的理解 16 | * 期望薪资 17 | * 在校成绩 18 | * 压力大的时候如何缓解 -------------------------------------------------------------------------------- /JAVA/JAVA/IO.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PokIsemaine/interview/bd5da05b557897a71361f8dbb9a6a6d25cf28d46/JAVA/JAVA/IO.md -------------------------------------------------------------------------------- /JAVA/JAVA/JVM.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PokIsemaine/interview/bd5da05b557897a71361f8dbb9a6a6d25cf28d46/JAVA/JAVA/JVM.md -------------------------------------------------------------------------------- /JAVA/JAVA/SPI.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PokIsemaine/interview/bd5da05b557897a71361f8dbb9a6a6d25cf28d46/JAVA/JAVA/SPI.md -------------------------------------------------------------------------------- /JAVA/JAVA/反射.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PokIsemaine/interview/bd5da05b557897a71361f8dbb9a6a6d25cf28d46/JAVA/JAVA/反射.md -------------------------------------------------------------------------------- /JAVA/JAVA/基本概念.md: -------------------------------------------------------------------------------- 1 | # 基本概念 2 | 3 | ## 问题 4 | 5 | ### 基本概念与尝试 6 | 7 | * JVM vs JDK vs JRE 8 | * 什么是字节码?采用字节的好处是什么? 9 | * 为什么不全部使用 AOT 呢? 10 | * 为什么说 Java 语言编译与解释并存 11 | * Oracle JDK vs Open JDK 12 | * Java 和 C++ 区别 13 | 14 | ### 基本语法 15 | 16 | * 注释有哪几种形式? 17 | * 标识符和关键字区别是什么? 18 | * Java 语言关键字有哪些? 19 | * 自增自减运算符 20 | * 移位运算符 21 | * continue、break 和 return 的区别 22 | 23 | ### 基本数据类型 24 | 25 | * Java 中的几种基本数据类型了解吗? 26 | * 基本类型和包装类型的区别? 27 | * 包装类型的缓存机制了解吗? 28 | * 自动装箱与拆箱了解吗?原理是什么? 29 | * 为什么浮点数运算的时候会有精度丢失的风险? 30 | * 如何解决浮点数运算的精度丢失问题? 31 | * 超过 long 整型的数据应该如何表示? 32 | 33 | ### 变量 34 | 35 | * 成员变量与局部变量的区别? 36 | * 静态变量有什么作用? 37 | * 字符型常量和字符串常量的区别? 38 | 39 | ### 方法 40 | 41 | * 什么是方法的返回值?方法有哪几种类型? 42 | * 静态方法为什么不能调用非静态成员? 43 | * 静态方法和实例方法有何不同? 44 | * 重载和重写有什么区别? 45 | * 什么是可变长参数? 46 | 47 | ## 回答 -------------------------------------------------------------------------------- /JAVA/JAVA/并发编程.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PokIsemaine/interview/bd5da05b557897a71361f8dbb9a6a6d25cf28d46/JAVA/JAVA/并发编程.md -------------------------------------------------------------------------------- /JAVA/JAVA/序列化与反序列化.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PokIsemaine/interview/bd5da05b557897a71361f8dbb9a6a6d25cf28d46/JAVA/JAVA/序列化与反序列化.md -------------------------------------------------------------------------------- /JAVA/JAVA/异常.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PokIsemaine/interview/bd5da05b557897a71361f8dbb9a6a6d25cf28d46/JAVA/JAVA/异常.md -------------------------------------------------------------------------------- /JAVA/JAVA/新特性.md: -------------------------------------------------------------------------------- 1 | # 新特性 2 | 3 | ## Java8 4 | 5 | ### Interface 6 | 7 | 8 | 9 | ### Lambda 表达式 10 | 11 | 12 | 13 | ### Stream 14 | 15 | 16 | 17 | ### Optional 18 | 19 | 20 | 21 | ### Date-Time API 22 | 23 | 24 | 25 | ## -------------------------------------------------------------------------------- /JAVA/JAVA/泛型.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PokIsemaine/interview/bd5da05b557897a71361f8dbb9a6a6d25cf28d46/JAVA/JAVA/泛型.md -------------------------------------------------------------------------------- /JAVA/JAVA/注解.md: -------------------------------------------------------------------------------- 1 | # 注解 2 | 3 | ## 问题 4 | 5 | * 何谓注解 6 | * 注解的解析方法有哪几种 7 | 8 | ## 回答 9 | 10 | ### 何谓注解? 11 | 12 | `Annotation` (注解) 是 Java5 开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。 13 | 14 | 注解本质是一个继承了`Annotation` 的特殊接口: 15 | 16 | ```java 17 | @Target(ElementType.METHOD) 18 | @Retention(RetentionPolicy.SOURCE) 19 | public @interface Override { 20 | 21 | } 22 | 23 | public interface Override extends Annotation{ 24 | 25 | } 26 | ``` 27 | 28 | JDK 提供了很多内置的注解(比如 `@Override` 、`@Deprecated`),同时,我们还可以自定义注解。 29 | 30 | ### 注解的解析方法有哪几种? 31 | 32 | 注解只有被解析之后才会生效,常见的解析方法有两种: 33 | 34 | - **编译期直接扫描** :编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用`@Override` 注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。 35 | - **运行期通过反射处理** :像框架中自带的注解(比如 Spring 框架的 `@Value` 、`@Component`)都是通过反射来进行处理的。 -------------------------------------------------------------------------------- /JAVA/JAVA/集合.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PokIsemaine/interview/bd5da05b557897a71361f8dbb9a6a6d25cf28d46/JAVA/JAVA/集合.md -------------------------------------------------------------------------------- /JAVA/JAVA/面向对象.md: -------------------------------------------------------------------------------- 1 | # 面向对象 2 | 3 | ## 问题 4 | 5 | ### 面向对象基础 6 | 7 | * 面向对象和面向过程的区别 8 | * 创建一个对象用什么运算符?对象实体与对象引用有何不同? 9 | * 对象的相等和引用相等的区别 10 | * 如果一个类没有声明构造方法,该程序能正确执行吗? 11 | * 构造方法有哪些特点?是否可被 override? 12 | * 面向对象三大特征 13 | * 接口和抽象类有什么共同点和区别? 14 | * 深拷贝和浅拷贝区别了解吗?什么是引用拷贝? 15 | 16 | ### Object 17 | 18 | * Object 类的常见方法有哪些? 19 | * == 和 equals() 的区别 20 | * hashCode() 有什么用? 21 | * 为什么要有 hashCode? 22 | * 为什么重写 equals() 时必须重写 hashCode() 方法 23 | 24 | ### String 25 | 26 | * String、StringBuffer、StringBuilder 的区别 27 | * String 为什么时不可变的 28 | * 字符串拼接用 + 还是 StringBuilder 29 | * String equals 和 Object equals 有何区别? 30 | * 字符串常量池的作用了解吗? 31 | * String s1 = new String("abc");这句话创建了几个字符串对象? 32 | * String intern 方法有什么作用 33 | * String 类型的变量和常量做“+”运算时发生了什么? 34 | 35 | ## 回答 -------------------------------------------------------------------------------- /JAVA/Spring/AOP.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PokIsemaine/interview/bd5da05b557897a71361f8dbb9a6a6d25cf28d46/JAVA/Spring/AOP.md -------------------------------------------------------------------------------- /JAVA/Spring/事务.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PokIsemaine/interview/bd5da05b557897a71361f8dbb9a6a6d25cf28d46/JAVA/Spring/事务.md -------------------------------------------------------------------------------- /JAVA/Spring/基础.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PokIsemaine/interview/bd5da05b557897a71361f8dbb9a6a6d25cf28d46/JAVA/Spring/基础.md -------------------------------------------------------------------------------- /JAVA/Spring/数据层.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PokIsemaine/interview/bd5da05b557897a71361f8dbb9a6a6d25cf28d46/JAVA/Spring/数据层.md -------------------------------------------------------------------------------- /JAVA/Spring/核心容器.md: -------------------------------------------------------------------------------- 1 | # 核心容器 2 | 3 | ## IoC 控制反转 4 | 5 | ### 谈谈自己对于 Spring IoC 的了解 6 | 7 | ![image.png](https://s2.loli.net/2023/04/28/CLUIaGEy69ruPfe.png) 8 | 9 | **IoC(Inversion of Control:控制反转)** 是一种设计思想,而不是一个具体的技术实现。IoC 的思想就是将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理。不过, IoC 并非 Spring 特有,在其他语言中也有应用。 10 | 11 | **为什么叫控制反转?** 12 | 13 | - **控制** :指的是对象创建(实例化、管理)的权力 14 | - **反转** :控制权交给外部环境(Spring 框架、IoC 容器) 15 | 16 | ![img](https://oss.javaguide.cn/java-guide-blog/frc-365faceb5697f04f31399937c059c162.png) 17 | 18 | 将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。 IoC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。 19 | 20 | 在实际项目中一个 Service 类可能依赖了很多其他的类,假如我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。如果利用 IoC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。 21 | 22 | 在 Spring 中, IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个 Map(key,value),Map 中存放的是各种对象。 23 | 24 | Spring 时代我们一般通过 XML 文件来配置 Bean,后来开发人员觉得 XML 文件来配置不太好,于是 SpringBoot 注解配置就慢慢开始流行起来。 25 | 26 | ### 什么是 Spring Bean? 27 | 28 | 简单来说,Bean 代指的就是那些被 IoC 容器所管理的对象。 29 | 30 | 我们需要告诉 IoC 容器帮助我们管理哪些对象,这个是通过配置元数据来定义的。配置元数据可以是 XML 文件、注解或者 Java 配置类。 31 | 32 | ```xml 33 | 34 | 35 | 36 | 37 | ``` 38 | 39 | 下图简单地展示了 IoC 容器如何使用配置元数据来管理对象。 40 | 41 | ![img](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/062b422bd7ac4d53afd28fb74b2bc94d.png) 42 | 43 | `org.springframework.beans`和 `org.springframework.context` 这两个包是 IoC 实现的基础,如果想要研究 IoC 相关的源码的话,可以去看看 44 | 45 | ### 什么叫 DI(Dependency Injection) 依赖注入 46 | 47 | 在容器中建立 bean 与 bean 依赖关系的过程称为依赖注入 48 | 49 | ### 将一个类声明为 Bean 的注解有哪些? 50 | 51 | - `@Component` :通用的注解,可标注任意类为 `Spring` 组件。如果一个 Bean 不知道属于哪个层,可以使用`@Component` 注解标注。 52 | - `@Repository` : 对应持久层即 Dao 层,主要用于数据库相关操作。 53 | - `@Service` : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。 54 | - `@Controller` : 对应 Spring MVC 控制层,主要用于接受用户请求并调用 `Service` 层返回数据给前端页面。 55 | 56 | ### @Component 和 @Bean 的区别是什么? 57 | 58 | - `@Component` 注解作用于类,而`@Bean`注解作用于方法。 59 | - `@Component`通常是通过类路径扫描来自动侦测以及自动装配到 Spring 容器中(我们可以使用 `@ComponentScan` 注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中)。`@Bean` 注解通常是我们在标有该注解的方法中定义产生这个 bean,`@Bean`告诉了 Spring 这是某个类的实例,当我需要用它的时候还给我。 60 | - `@Bean` 注解比 `@Component` 注解的自定义性更强,而且很多地方我们只能通过 `@Bean` 注解来注册 bean。比如当我们引用第三方库中的类需要装配到 `Spring`容器时,则只能通过 `@Bean`来实现。 61 | 62 | `@Bean`注解使用示例: 63 | 64 | ```java 65 | @Configuration 66 | public class AppConfig { 67 | @Bean 68 | public TransferService transferService() { 69 | return new TransferServiceImpl(); 70 | } 71 | 72 | } 73 | ``` 74 | 75 | 上面的代码相当于下面的 xml 配置 76 | 77 | ```xml 78 | 79 | 80 | 81 | ``` 82 | 83 | 下面这个例子是通过 `@Component` 无法实现的。 84 | 85 | ```java 86 | @Bean 87 | public OneService getService(status) { 88 | case (status) { 89 | when 1: 90 | return new serviceImpl1(); 91 | when 2: 92 | return new serviceImpl2(); 93 | when 3: 94 | return new serviceImpl3(); 95 | } 96 | } 97 | ``` 98 | 99 | ### 注入 Bean 的注解有哪些? 100 | 101 | Spring 内置的 `@Autowired` 以及 JDK 内置的 `@Resource` 和 `@Inject` 都可以用于注入 Bean。 102 | 103 | | Annotaion | Package | Source | 104 | | ------------ | ---------------------------------- | ------------ | 105 | | `@Autowired` | `org.springframework.bean.factory` | Spring 2.5+ | 106 | | `@Resource` | `javax.annotation` | Java JSR-250 | 107 | | `@Inject` | `javax.inject` | Java JSR-330 | 108 | 109 | `@Autowired` 和`@Resource`使用的比较多一些。 110 | 111 | ### @Autowired 和 @Resource 的区别是什么? 112 | 113 | `Autowired` 属于 Spring 内置的注解,默认的注入方式为`byType`(根据类型进行匹配),也就是说会优先根据接口类型去匹配并注入 Bean (接口的实现类)。 114 | 115 | **这会有什么问题呢?** 当一个接口存在多个实现类的话,`byType`这种方式就无法正确注入对象了,因为这个时候 Spring 会同时找到多个满足条件的选择,默认情况下它自己不知道选择哪一个。 116 | 117 | 这种情况下,注入方式会变为 `byName`(根据名称进行匹配),这个名称通常就是类名(首字母小写)。就比如说下面代码中的 `smsService` 就是我这里所说的名称,这样应该比较好理解了吧。 118 | 119 | ```java 120 | // smsService 就是我们上面所说的名称 121 | @Autowired 122 | private SmsService smsService; 123 | ``` 124 | 125 | 举个例子,`SmsService` 接口有两个实现类: `SmsServiceImpl1`和 `SmsServiceImpl2`,且它们都已经被 Spring 容器所管理。 126 | 127 | ```java 128 | // 报错,byName 和 byType 都无法匹配到 bean 129 | @Autowired 130 | private SmsService smsService; 131 | // 正确注入 SmsServiceImpl1 对象对应的 bean 132 | @Autowired 133 | private SmsService smsServiceImpl1; 134 | // 正确注入 SmsServiceImpl1 对象对应的 bean 135 | // smsServiceImpl1 就是我们上面所说的名称 136 | @Autowired 137 | @Qualifier(value = "smsServiceImpl1") 138 | private SmsService smsService; 139 | ``` 140 | 141 | 我们还是建议通过 `@Qualifier` 注解来显式指定名称而不是依赖变量的名称。 142 | 143 | `@Resource`属于 JDK 提供的注解,默认注入方式为 `byName`。如果无法通过名称匹配到对应的 Bean 的话,注入方式会变为`byType`。 144 | 145 | `@Resource` 有两个比较重要且日常开发常用的属性:`name`(名称)、`type`(类型)。 146 | 147 | ```java 148 | public @interface Resource { 149 | String name() default ""; 150 | Class type() default Object.class; 151 | } 152 | ``` 153 | 154 | 如果仅指定 `name` 属性则注入方式为`byName`,如果仅指定`type`属性则注入方式为`byType`,如果同时指定`name` 和`type`属性(不建议这么做)则注入方式为`byType`+`byName`。 155 | 156 | ```java 157 | // 报错,byName 和 byType 都无法匹配到 bean 158 | @Resource 159 | private SmsService smsService; 160 | // 正确注入 SmsServiceImpl1 对象对应的 bean 161 | @Resource 162 | private SmsService smsServiceImpl1; 163 | // 正确注入 SmsServiceImpl1 对象对应的 bean(比较推荐这种方式) 164 | @Resource(name = "smsServiceImpl1") 165 | private SmsService smsService; 166 | ``` 167 | 168 | 简单总结一下: 169 | 170 | - `@Autowired` 是 Spring 提供的注解,`@Resource` 是 JDK 提供的注解。 171 | - `Autowired` 默认的注入方式为`byType`(根据类型进行匹配),`@Resource`默认注入方式为 `byName`(根据名称进行匹配)。 172 | - 当一个接口存在多个实现类的情况下,`@Autowired` 和`@Resource`都需要通过名称才能正确匹配到对应的 Bean。`Autowired` 可以通过 `@Qualifier` 注解来显式指定名称,`@Resource`可以通过 `name` 属性来显式指定名称。 173 | 174 | ### bean-的作用域有哪些) Bean 的作用域有哪些? 175 | 176 | Spring 中 Bean 的作用域通常有下面几种: 177 | 178 | - **singleton** : IoC 容器中只有唯一的 bean 实例。Spring 中的 bean 默认都是单例的,是对单例设计模式的应用。 179 | - **prototype** : 每次获取都会创建一个新的 bean 实例。也就是说,连续 `getBean()` 两次,得到的是不同的 Bean 实例。 180 | - **request** (仅 Web 应用可用): 每一次 HTTP 请求都会产生一个新的 bean(请求 bean),该 bean 仅在当前 HTTP request 内有效。 181 | - **session** (仅 Web 应用可用) : 每一次来自新 session 的 HTTP 请求都会产生一个新的 bean(会话 bean),该 bean 仅在当前 HTTP session 内有效。 182 | - **application/global-session** (仅 Web 应用可用): 每个 Web 应用在启动时创建一个 Bean(应用 Bean),该 bean 仅在当前应用启动时间内有效。 183 | - **websocket** (仅 Web 应用可用):每一次 WebSocket 会话产生一个新的 bean。 184 | 185 | **如何配置 bean 的作用域呢?** 186 | 187 | xml 方式: 188 | 189 | ```xml 190 | 191 | ``` 192 | 193 | 注解方式: 194 | 195 | ```java 196 | @Bean 197 | @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) 198 | public Person personPrototype() { 199 | return new Person(); 200 | } 201 | ``` 202 | 203 | ### 单例 Bean 的线程安全问题了解吗? 204 | 205 | 大部分时候我们并没有在项目中使用多线程,所以很少有人会关注这个问题。单例 Bean 存在线程问题,主要是因为当多个线程操作同一个对象的时候是存在资源竞争的。 206 | 207 | 常见的有两种解决办法: 208 | 209 | 1. 在 Bean 中尽量避免定义可变的成员变量。 210 | 2. 在类中定义一个 `ThreadLocal` 成员变量,将需要的可变成员变量保存在 `ThreadLocal` 中(推荐的一种方式)。 211 | 212 | 不过,大部分 Bean 实际都是无状态(没有实例变量)的(比如 Dao、Service),这种情况下, Bean 是线程安全的。 213 | 214 | ### Bean 的生命周期了解么? 215 | 216 | > 下面的内容整理自:[https://yemengying.com/2016/07/14/spring-bean-life-cycle/open in new window](https://yemengying.com/2016/07/14/spring-bean-life-cycle/) ,除了这篇文章,再推荐一篇很不错的文章 :[https://www.cnblogs.com/zrtqsk/p/3735273.htmlopen in new window](https://www.cnblogs.com/zrtqsk/p/3735273.html) 。 217 | 218 | - Bean 容器找到配置文件中 Spring Bean 的定义。 219 | - Bean 容器利用 Java Reflection API 创建一个 Bean 的实例。 220 | - 如果涉及到一些属性值 利用 `set()`方法设置一些属性值。 221 | - 如果 Bean 实现了 `BeanNameAware` 接口,调用 `setBeanName()`方法,传入 Bean 的名字。 222 | - 如果 Bean 实现了 `BeanClassLoaderAware` 接口,调用 `setBeanClassLoader()`方法,传入 `ClassLoader`对象的实例。 223 | - 如果 Bean 实现了 `BeanFactoryAware` 接口,调用 `setBeanFactory()`方法,传入 `BeanFactory`对象的实例。 224 | - 与上面的类似,如果实现了其他 `*.Aware`接口,就调用相应的方法。 225 | - 如果有和加载这个 Bean 的 Spring 容器相关的 `BeanPostProcessor` 对象,执行`postProcessBeforeInitialization()` 方法 226 | - 如果 Bean 实现了`InitializingBean`接口,执行`afterPropertiesSet()`方法。 227 | - 如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法。 228 | - 如果有和加载这个 Bean 的 Spring 容器相关的 `BeanPostProcessor` 对象,执行`postProcessAfterInitialization()` 方法 229 | - 当要销毁 Bean 的时候,如果 Bean 实现了 `DisposableBean` 接口,执行 `destroy()` 方法。 230 | - 当要销毁 Bean 的时候,如果 Bean 在配置文件中的定义包含 destroy-method 属性,执行指定的方法。 231 | 232 | 图示: 233 | 234 | ![Spring Bean 生命周期](https://images.xiaozhuanlan.com/photo/2019/24bc2bad3ce28144d60d9e0a2edf6c7f.jpg)Spring Bean 生命周期 235 | 236 | 与之比较类似的中文版本: 237 | 238 | ![Spring Bean 生命周期](https://images.xiaozhuanlan.com/photo/2019/b5d264565657a5395c2781081a7483e1.jpg)Spring Bean 生命周期 239 | 240 | ------ 241 | 242 | 著作权归Guide所有 原文链接:https://javaguide.cn/system-design/framework/spring/spring-knowledge-and-questions-summary.html#bean-%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E4%BA%86%E8%A7%A3%E4%B9%88 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 NEFU-AK-MY-OFFER 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 面试八股文 2 | 3 | ## 大纲 4 | 5 | 状态 6 | * 计划中:还没开始 7 | * 执行中:正在整理,少部分可用 8 | * 优化维护:基本完工,大部分可用,优化、维护为主 9 | 10 | 内容 11 | * 操作系统(优化阶段) 12 | * 分布式系统(计划中) 13 | * 工程(优化阶段) 14 | * 计算机网络(优化阶段) 15 | * 设计模式(执行中) 16 | * 数据结构算法(执行中) 17 | * 数据库(执行中) 18 | * 云原生(计划中) 19 | * 智力题场景题(优化阶段) 20 | * C++(优化阶段) 21 | * Golang(计划中) 22 | * JAVA(计划中) 23 | * 前端(计划中) 24 | * 开源实习(执行中) 25 | * 资源(参考的资料) 26 | 27 | ## 贡献 28 | 29 | 本仓库为文档类的仓库,没有太多规矩,你可以通过以下方式做贡献 30 | 31 | ### Issues 32 | 33 | * 各种勘误,小勘误可以直接提 PR 34 | * 知识点添加 35 | * 写好的面经,维护人员会把面经内容归纳整理 36 | * 单个题目描述 37 | * 新增内容建议(不局限于现有内容) 38 | 39 | ### PR 40 | 下列情况可以直接提交 PR 41 | * 错别字问题 42 | * 格式问题 43 | * 资源链接问题 44 | 45 | ## 致谢 46 | 47 | * 感谢相关贡献人员 48 | * 感谢资源里的参考资料作者 -------------------------------------------------------------------------------- /云原生/k8s.md: -------------------------------------------------------------------------------- 1 | https://www.nowcoder.com/discuss/490850490599608320 -------------------------------------------------------------------------------- /分布式系统/共识算法.md: -------------------------------------------------------------------------------- 1 | # 问题 2 | 3 | 基本概念 4 | 5 | * Raft 作用是什么? 6 | * 什么是任期? 7 | * raft kv读写流程 8 | * raft的优势在哪里 9 | * raft中有哪些角色,怎么转换的(领导,候选人,跟随者) 10 | * Raft协议,任期是什么,任期存在哪,日志是什么,日志存在哪,日志下标是什么,存在哪里 11 | * raft 流程 12 | * raft算法要求的最少节点数 13 | * raft 有哪些模块 14 | * raft 解决什么问题 15 | 16 | * raft 幽灵复现的问题 + 解决方法 17 | 18 | * 一个任期内 follow 给 candidate 投票后然后宕机了,起来后还会再投票吗 19 | 20 | * 基于raft 的 kv引擎如何做? 21 | 22 | * raft选举leader需要至少几个节点同意 23 | * raft在集群规模3个、4个节点的情况下,分别需要几个节点同意,才能选出leader 24 | 25 | * paxos中节点宕机后如何去恢复数据 26 | * 有多个 candidate 发起选举怎么解决? 27 | * raft 和 paxos 选举流程有什么区别? 28 | * 选举超时的设置一般多久,为什么这么设置? 29 | * raft term id 有什么用 30 | 31 | * 元数据服务器能做分片吗?怎么做 32 | * 高可用怎么做? 33 | * 分布式kv怎么实现? 34 | * 新 leader 上线怎么继续去日志复制 35 | * raft 这种协议要满足奇数节点吗?面试官觉得是奇数,因为出现 2 2 的网络分区就不可用了,但其实 Quorum 不等于 majority,可以修改协议,实现偶数节点也能正常应对网络分区 36 | 37 | 38 | * 说一下raft的集群变更 39 | 40 | * raft 中大多数节点提交某条日志后,未包含该日志的某个节点能成为 leader 吗? 41 | * raft 论文中 peer 启动的时候是 follower,可以是 candidate 吗? 42 | 43 | * 多副本如何选主? 44 | * 多个副本写入如何保证一致性和可靠性? 45 | 46 | * 如果用同步复制呢,有什么问题 47 | * Quorum 读写这种方法相比 raft 的问题? 48 | * 如果 Quorum 读写也去设置一个 leader ,然后先写 leader,这种情况会有什么问题 49 | * Raft成员变更 50 | * 为什么raft安全 51 | 52 | 问了项目的append操作是怎么做的,kv用什么存储 53 | * 然后问了raft的分区容错,脑裂怎么解决的 54 | * 聊聊raft,单点负荷你怎么解决呢 55 | * 介绍一下raft特点,什么是强一致性,一致性有哪些 56 | 57 | * 脑裂的读取你怎么办 58 | 59 | * raft是如何保证日志一致性的? 60 | * raft中follower发现日志有缺损后如何恢复? 61 | 62 | Raft 优化/改造 63 | 64 | * Raft 有哪些不足,怎么优化 65 | * raft有什么缺点(答了读请求也要经过follower) 66 | * Prevote机制 67 | * raft 协议选举冲突的优化 68 | * 说一下 raft,有什么方式减少选举次数。 69 | * 怎么优化读请求 70 | * raft leader 接受读写请求,如何优化? 71 | * raft 如何避免惊群效应?raft 惊群怎么优化 72 | * 实现的raft跟工业的raft有什么区别吗(懵了,讲了个prevote机制,刚好看过) 73 | * raft 如何实现从 follower 读取? 74 | 75 | * 写操作一定是写 Leader 吗? 76 | * 读操一定从 Leader 吗? 77 | * 能保证读到最新的吗? 78 | * 如何解决,有没有优化方式? 79 | * 主节点一直收不到大部分从节点的心跳回复会怎样。 80 | * 存不存在 Paxos 写虽然写到了大部分,读却没有读到? 81 | * 写只能在 Leader,那性能会不会有瓶颈,怎么解决? 82 | * 怎么保证数据均匀分散到多台机器 83 | * 那怎么让 Leader 尽量均匀分布? 84 | * 怎么确定集群的数量比较合适? 85 | * 怎么测试的性能? 86 | * 解释一下 WAL,必须在事务前落盘吗? 87 | * 如果缓存的日志丢失了怎么办?事务会有问题吗? 88 | * 为什么并发日志性能会好?(是真的疑问) 89 | * 为什么你们可以并发日志? 90 | * 对比 redis 的日志。 91 | 92 | Raft 选举 93 | 94 | * raft怎么保证只有一个leader 95 | * RAFT如何解决网络分片 96 | * Raft leader 选举过程 97 | * RAFT是否会出现多个leader的情况,如果出现了怎么保证一致性的 98 | * RAFT的消息是怎样的 99 | * 脑裂是什么,什么情况下会出现脑裂(数据分区/Split-brain),项目中对于脑裂的情况是怎么处理的 100 | * 怎么保证网络分区恢复后leader无故下任的情况 101 | * Split-Vote 怎么处理的 102 | * 选举投票的条件 103 | * Raft选举哪些限制 104 | * Raft投票分裂怎么处理 105 | * Raft如何防止脑裂 106 | * 讲一下 raft 的选举机制,为什么要初始化为 follower,为什么要随机超时? 107 | * 讲一下新 leader 上任为什么要 append 一条空 entry? 108 | * Raft 集群发生网络分区会怎么样 109 | * raft选举过程,节点怎么判断落后的,分区问题 110 | * 选举会出现活锁问题吗 111 | * 出现网络分区对 raft 有什么影响?会脑裂吗,描述下场景,怎么解决? 112 | * raft的心跳是什么?raft选举竞争问题怎么解决(心跳时间加上随机值) 113 | 114 | Raft 与 CAP 115 | 116 | * Raft 实现了 CAP 中的哪两个原则 117 | * Raft 如何高可用 118 | * 采用什么策略保证的强一致性 119 | * 基于raft的kv存储如何实现线性一致性? 120 | * raft协议的线性一致性是推理得出的还是实验证明出来的?(那肯定是推理得到的)你对于线性一致性怎么理解?怎么保证线性一致性? 121 | * 讲一下 raft 为啥能保证一致性 122 | * raft 的可用性 123 | * raft本身能保证线性一致吗?raft本身保证了什么?关于读可以有什么优化,前提是强一致? 124 | * 讲一下raft共识算法,raft实现了什么一致性?客户端总能读到最新的吗? 125 | * raft 容灾 126 | * Raft中如果部分节点落后,如何保证一致性 127 | * get put线性一致性怎么实现的 128 | 129 | raft 延时提交的场景分析 130 | 131 | 132 | 客户端 133 | 134 | * 客户端网络故障怎么办?如何防止重复请求 135 | * 客户端交互的具体实现 136 | 137 | 138 | 集群配置 139 | 140 | * 异步 schema 变更原理 141 | * 异步 schema 变更的实现,真正可用吗? 142 | * 如何处理脑裂,同步配置时出现网络分区怎么处理 143 | 144 | 如果 Raft 集群超过半数节点挂了会怎么样 145 | multi-Raft 原理(用哈希分片好吗?) 146 | 147 | Leader Down 了怎么处理,怎么保障一致性 148 | 149 | raft no-op理解 150 | 151 | 152 | raft在选举时不能正常对外提供服务,这影响大吗? 153 | 154 | 你怎么看待Raft在拜占庭问题中出现的问题,如果让你改进怎么改进(我说加固leader安全面试官直接否认,又汗流浃背了) 155 | 如果单机出现网络分区会出现什么后果,你应该怎样避免这些 156 | 157 | Raft协议中出现网络故障导致分区时,如何解决任期号一直增加的问题 158 | 159 | raft如何确定一个修改是否能够读了 160 | raft如何commit一个log 161 | 如果要进行存储和读取,写请求和读请求需要设计哪些字段 162 | 给了个raft的log场景的term,一号节点是1 2,二号节点是1,三号节点是1 3。问这个时候,三号节点挂了,一号节点成为了leader,是否能够把一号节点的log同步到二和三 163 | 164 | 发生网络故障时raft协议如何工作的 165 | 166 | RAFT算法,PBFT算法你是怎么结合的(项目提到的) 167 | 如何仿真? 168 | Raft哪些情况下会发生脑裂?怎么解决脑裂?面试官说主要是靠任期,但是我提出疑问,因为分区故障恢复后由于小分区中一直存在选举,所以有的节点任期会很大,会扰乱主分区的正常工作,然后说了解决办法是Raft的prevote机制。 169 | 170 | 然后拷打MIT6.824的项目,raft底层选举如何实现的,leader和follower方的逻辑分别如何实现。详细介绍下服务端接收到客户端的数据后进行的存储和处理操作流程、日志快照存储和持久化存储的方式、什么时候更新快照以及同步快照,谁来判断是否更新快照以及如何判断。 171 | 172 | raft 日志 173 | 174 | * 复制日志怎么实现的 175 | * raft的日志需要有什么特点 176 | * 新 leader 上线怎么继续去日志复制 177 | * 日志压缩如何实现的,什么时候做日志压缩 178 | * Raft日志覆盖和回溯的关键点(term ) 179 | * raft怎么进行日志同步 180 | * Raft会存在follower比leader日志多的情况吗,如何解决 181 | * follower收到一个日志,有冲突怎么办 182 | * 如果一个Follower缺了一些日志怎么发现 183 | * leader 和 follower 的日志 有一个index 上的 日志 term 相同,那么之前的日志也是相同的吗? 184 | 185 | raft kv数据库是怎么实现的; 186 | raft能保证kv数据库不读到脏数据吗?(不能,有可能没apply到状态机上) 187 | 那和mysql那些主从模式有什么区别(我答了主从模式只关注数据一致性,没有关注分布式事务的一致性)。 188 | 189 | raft中snapshot怎么实现的,snapshot的作用是什么 190 | 191 | raft 中网络延迟/网络分区的情况 192 | 193 | 工程应用 194 | 195 | * etcd如何用raft,etcd底层是如何存储的 196 | * 基于Raft的开源项目或中间件有什么? 197 | * Redis里面使用到了Raft协议,你有了解么? 啊???? 198 | * 为什么zk不用raft? 199 | * 实现paxos算法的工程还有哪个? 200 | * 上层业务如何使用 Raft?(面试官想的是和 etcd 那样单独作为一个存储服务 201 | 202 | 项目相关6.824 lab 203 | 分片怎么迁移 204 | 重复请求怎么解决 205 | rpc协议简单介绍一下 继续问rpc流程和序列化 206 | Rpc的Roundbin怎么回事的。 207 | 讲讲Rpc的主要工作流程。 208 | 209 | 210 | 如何使用Raft构建一个分布式数据库 211 | 212 | 假如我用raft实现一个分布式数据库,和我用mysql实现一个分布式数据库,二者有什么区别吗?有什么场景上的区分吗?哪个更好一点? 213 | 214 | Raft prevote防止集群被vote 干扰 215 | Raft如何在leader 变化的瞬间也能保持一致性 216 | 217 | 218 | 那如果leader磁盘坏了,又收到另一个follower的OK,但是这个follower还没有成功apply,怎么办? 219 | 220 | Leader异常下线会发生什么 221 | 222 | 一共三个节点Leader挂了描述一下之后发生的事 223 | 224 | raft网络分区以后怎么同步日志,以及新老leader节点问题。 225 | 226 | 227 | 弱一致性协议有哪些。(答Paxos,Raft,面试官说不对,这是强一致性协议) 228 | 229 | raft通过单机协程模拟和多进程的区别 230 | 231 | 如何选举leader,判定的条件是什么(term,日志,日志index) 232 | 233 | raft和分布式事务的区别 234 | raft、paxos,zab 各解决的什么问题,有什么区别,有什么缺点、性能对比、选举流程区别都讲一下 235 | 236 | 讲raft和分布式系统,问的挺详细的,raft选主过程,append entries过程,为什么选主要随机选举超时,怎么判断log是最新的,不随机选举超时会有什么问题,raft的term有什么作用,raft现在有什么应用,raft强一致性的应用场景,抛开应用场景强一致性和弱一致性应该怎么选择。 237 | 238 | Paxos项目做了什么 239 | 240 | Raft能当注册中心吗(当然可以 241 | 242 | 讲一下paxos(因为聊的很开心,我直接说我就开始吟唱了😼 243 | 244 | 从CAP--》PACELC 245 | 246 | 应该再讲NWR的 247 | 248 | 然后Paxos 249 | 250 | 活锁 251 | 252 | mutil-paxos 253 | 254 | 你觉得raft和paxos他们有什么本质的区别吗?它为了工程实现丢弃了什么东西? 255 | 256 | 讲讲raft的子功能过程 257 | 258 | -----场景题----- 259 | 260 | 如果有个节点落后其他节点很多日志,raft怎么处理? 261 | 262 | 有个读请求到了follower,raft怎么操作 263 | 264 | 读请求正好打到了刚才那个落后了很多日志的节点上怎么办? 265 | 266 | 如果实现了线性一致性读,是可以从follower读的 267 | 268 | 三节点,有一个节点(可能是leader可能是follower宕机),说下此时集群内部是什么情况 269 | 270 | 宕机节点重新加入集群后发生什么? 271 | 272 | 网络分区时,候选者的term不停+1后,另外两个节点正常服务,网络恢复后,集群发生什么? 273 | 274 | 最后说到raft,面试官问raft怎么应对脑裂,我说根据Leader Election和Log Replication,Raft不会有实质的脑裂现象,面试官还举了一些场景,不过我觉得那些场景上述两个机制都覆盖到了,然后就没有然后了 275 | 276 | raft分布式集群的项目, 277 | raft算法整体介绍,为什么节点个数是奇数(节约成本),3个结点,其中两个无法通讯会怎样(Leader和可通讯的结点保持连接),如何扩容,增加结点如何数据迁移(没做)。最后推荐看一下商用的zookeeper和redis怎么做的 278 | LSM-tree 存储引擎和 Raft 的关系 279 | 280 | 出现网络分区,会脑裂吗 281 | 282 | raft 这种协议要满足奇数节点吗?面试官觉得是奇数,因为出现 2 2 的网络分区就不可用了,但其实 Quorum 不等于 majority,可以修改协议,实现偶数节点也能正常应对网络分区 283 | 284 | 285 | 项目6.824; 286 | 287 | * raft强一致,什么叫强一致;向客户端保证了什么问题; 288 | * 从leader读比较强,从follower读的时候怎么办(讲了zk的FIFO队列和readIndex) 289 | * leader失效了解过吗,怎么保证你读的时候leader还是真正的leader?(说了读之前先发个心跳) 290 | * 这个太浪费资源了,还有其他办法吗,提示一下和任期相关(没想到,后来突然想起了lease机制,但是不太会说,然后面试官可能赶时间,就帮我从头捋了一遍,我就在那“啊对对对我就想说这个”) 291 | 292 | 分布式一致性怎么保证,mysql的XA和tcc 293 | -------------------------------------------------------------------------------- /分布式系统/分布式kv.md: -------------------------------------------------------------------------------- 1 | # 问题 2 | 3 | b站会有一些数据会设置 ttl,定期自动删除,怎么去改造rocksdb呢? b站的分布式kv 是基于 raft + rocksdb,当时答的是需要改造下 rocksdb 的 compaction filter。但会引入一个问题,就是三个 replica 各自去 compaction可能会出现有的 replica 对超过ttl的数据gc了,有的没有,导致不一致,问怎么解决? 4 | 对一组事务,每个事务读写 N 个kv对,怎么去做事务并发(不能用锁)? 5 | 6 | 印象比较深的一个场景题:比如给 kv master 上 raft,然后client 会去向master拿数据,如果采用 round-robin机制,会存在一个问题,就是向副本 A 拿数据后,可能后面会向副本 B 拿数据,但如果 B 的进度比 A 慢,可能会引起 client 的缓存发生回退,怎么解决这种case? 7 | 8 | -------------------------------------------------------------------------------- /分布式系统/存储.md: -------------------------------------------------------------------------------- 1 | # 问题 2 | 3 | * 问了下分布式存储的设计要考虑什么? 4 | * 问了下数据库和分布式存储的区别? 5 | * GFS 如何保证数据的高可用?遇到错误如何进行重试? 6 | * 了解GFS吗?介绍一下 7 | * 你刚才提到GFS谷歌已经不用了,你能介绍一下GFS的缺点吗?以及你改进的思路(属实是给自己挖坑了) -------------------------------------------------------------------------------- /分布式系统/理论.md: -------------------------------------------------------------------------------- 1 | # 问题 2 | 3 | * CAP是什么 4 | 5 | CAP,它其实是提出了一种理论思想,也就是说在一个分布式系统里的话,同时达成 CAP 这三个特性是基本上不可能完成的。 c 的话就是一致性,系统节点他们所处的状态和记录需要达成一致的, a 的话是可用,提供的服务是可以对外是一直可用的。然后 p 的话就是partition,也就是说进行相关的分区管理来提高一定的性能。然后现在的分布式系统的话,基本上都会在这三个中间进行相关的权衡来实践,嗯,可能是 CA 或者是 AP 相关的系统,然后基于此的话,业界其实还有一个基于 CAP 理论的实践的一个方式,其实是BASE,也就是说,可以实现一个基本可用。然后 s 的话是一个软状态, e 对应最终一致性来达成这个实际上分布式系统的实践。 6 | 7 | * 有哪些CA的系统,HBase属于什么? 8 | Hbase 的话它肯定是首先满足了一致性的要求,然后,嗯,到底是 available 还是 partiition 的话,我这个不太清楚。 9 | 10 | * 分布式系统怎么保证系统的一致性? 11 | 执行层:乐观执行,保存历史版本信息,冲突之后,重做。 12 | 13 | 存储层:日志文件,进行恢复 14 | 15 | 16 | * 分布式理论了解是吗?能讲讲Base吗?(我问能不能先从CAP开始,讲AP和CP的时候延申到Base) 17 | * 讲一下你对CAP的理解 18 | 解释一下为什么保证P的情况下C和A不能同时满足 19 | A的具体含义是什么 20 | 21 | 讲一下分布式CAP理论,有哪些一致性,最终一致性的实现方法,举个场景 22 | CAP任何时候都只能保证两个吗,可不可以三个都要 -------------------------------------------------------------------------------- /分布式系统/计算.md: -------------------------------------------------------------------------------- 1 | # 问题 2 | 3 | * 讲讲 MapReduce 4 | * MapReduce中怎么确保可用性和容错性 5 | * 使用多线程如何实现类似于mapreduce计算1-100w的总和 6 | * Mapreduce排序,优化,算复杂度和耗时 7 | * 讲一下map-reduce计算框架,map和reduce的结果存储在本地还是远端?有什么区别?工业实现做了哪些优化,猜测一下 -------------------------------------------------------------------------------- /前端/ECMAScript/ES6.md: -------------------------------------------------------------------------------- 1 | # ES6 2 | 3 | [ECMAScript 6 入门 - 《阮一峰 ECMAScript 6 (ES6) 标准入门教程 第三版》 - 书栈网 · BookStack](https://www.bookstack.cn/read/es6-3rd/sidebar.md) 4 | 5 | ## let 和 const 6 | 7 | 8 | 9 | ## Proxy 10 | 11 | 12 | 13 | ## Module -------------------------------------------------------------------------------- /前端/ECMAScript/ESlint.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PokIsemaine/interview/bd5da05b557897a71361f8dbb9a6a6d25cf28d46/前端/ECMAScript/ESlint.md -------------------------------------------------------------------------------- /前端/ECMAScript/异步.md: -------------------------------------------------------------------------------- 1 | # 异步 2 | 3 | [JS 异步编程六种方案 - 掘金 (juejin.cn)](https://juejin.cn/post/6844903760280420366) 4 | 5 | 6 | 7 | ## 同步与异步 8 | 9 | 10 | 11 | ## 异步发展 12 | 13 | ### 回调函数 14 | 15 | * 优点:简单,容易理解和实现 16 | * 缺点:不利于代码阅读和维护,回调地狱;不能用 try catch 捕获错误,不能直接 return 17 | 18 | ### 事件监听 19 | 20 | * 这种方式下,异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生 21 | * 优点:比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"去耦合",有利于实现模块化。 22 | * 缺点:整个程序都要变成事件驱动型,运行流程会变得很不清晰。阅读代码的时候,很难看出主流程。 23 | 24 | ### 发布订阅 25 | 26 | * 存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行 27 | * 可以通过查看“消息中心”,了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。 28 | 29 | ### Promise/A+ 30 | 31 | * Promise 的三种状态 32 | * Pending----Promise对象实例创建时候的初始状态 33 | * Fulfilled----可以理解为成功的状态 34 | * Rejected----可以理解为失败的状态 35 | * **这个承诺一旦从等待状态变成为其他状态就永远不能更改状态了**,比如说一旦状态变为 resolved 后,就不能再次改变为Fulfilled 36 | * 当我们在构造 Promise 的时候,构造函数内部的代码是立即执行的 37 | * promise的链式调用 38 | * 每次调用返回的都是一个新的Promise实例(这就是then可用链式调用的原因) 39 | * 如果then中返回的是一个结果的话会把这个结果传递下一次then中的成功回调 40 | * 如果then中出现异常,会走下一个then的失败回调 41 | * 在 then中使用了return,那么 return 的值会被Promise.resolve() 包装(见例1,2) 42 | * then中可以不传递参数,如果不传递会透到下一个then中(见例3) 43 | * catch 会捕获到没有捕获的异常 44 | * 优点:不仅能够捕获错误,而且也很好地解决了回调地狱的问题 45 | * 缺点:无法取消 Promise,错误需要通过回调函数捕获 46 | 47 | ### 生成器Generators/ yield 48 | 49 | * Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同,Generator 最大的特点就是可以控制函数的执行。 50 | * 语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。 51 | * **Generator 函数除了状态机,还是一个遍历器对象生成函数**。 52 | * **可暂停函数, yield可暂停,next方法可启动,每次返回的是yield后的表达式结果**。 53 | * yield表达式本身没有返回值,或者说总是返回undefined。**next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值**。 54 | 55 | ### Async/Await 56 | 57 | * 使用async/await,你可以轻松地达成之前使用生成器和co函数所做到的工作,它有如下特点: 58 | * async/await是基于Promise实现的,它不能用于普通的回调函数。 59 | * async/await与Promise一样,是非阻塞的。 60 | * async/await使得异步代码看起来像同步代码,这正是它的魔力所在。 61 | * **一个函数如果加上 async ,那么该函数就会返回一个 Promise** 62 | * Async/Await并发请求 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /前端/Vue/Vue3.0.md: -------------------------------------------------------------------------------- 1 | # Vue3.0 2 | 3 | ## Vue3.0 有什么更新 4 | 5 | (1)监测机制的改变 6 | 7 | 3.0 将带来基于代理 Proxy的 observer 实现,提供全语⾔覆盖的反应性跟踪。 消除了 Vue 2 当中基于 Object.defineProperty 的实现所存在的很多限制: 8 | 9 | (2)只能监测属性,不能监测对象 10 | 11 | 检测属性的添加和删除; 检测数组索引和⻓度的变更; ⽀持 Map、Set、WeakMap 和 WeakSet。 12 | 13 | (3)模板 14 | 15 | * 作⽤域插槽,2.x 的机制导致作⽤域插槽变了,⽗组件会重新渲染,⽽ 3.0 把作⽤域插槽改成了函 数的⽅式,这样只会影响⼦组件的重新渲染,提升了渲染的性能。 16 | * 同时,对于 render 函数的⽅⾯,vue3.0 也会进⾏⼀系列更改来⽅便习惯直接使⽤ api 来⽣成 vdom 。 17 | 18 | (4)对象式的组件声明⽅式 19 | 20 | * vue2.x 中的组件是通过声明的⽅式传⼊⼀系列 option,和 TypeScript 的结合需要通过⼀些装饰器 的⽅式来做,虽然能实现功能,但是⽐较麻烦。 21 | * 3.0 修改了组件的声明⽅式,改成了类式的写法,这样使得和 TypeScript 的结合变得很容易 22 | 23 | (5)其它⽅⾯的更改 24 | 25 | * ⽀持⾃定义渲染器,从⽽使得 weex 可以通过⾃定义渲染器的⽅式来扩展,⽽不是直接 fork 源码来 改的⽅式。 ⽀持 26 | * Fragment(多个根节点)和 Protal(在 dom 其他部分渲染组建内容)组件,针对⼀些特殊的 场景做了处理。 27 | * 基于 tree shaking 优化,提供了更多的内置功能。 28 | 29 | 30 | 31 | ## defineProperty 和 proxy 的区别 32 | 33 | Vue 在实例初始化时遍历 data 中的所有属性,并使⽤ Object.defineProperty 把这些属性全部转为 getter/setter。这样当追踪数据发⽣变化时,setter 会被⾃动调⽤。 34 | 35 | Object.defineProperty 是 ES5 中⼀个⽆法 shim 的特性,这也就是 Vue 不⽀持 IE8 以及更低版本浏 览器的原因。 36 | 37 | 38 | 39 | 但是这样做有以下问题: 40 | 41 | 1. 添加或删除对象的属性时,Vue 检测不到。因为添加或删除的对象没有在初始化进⾏响应式处理, 只能通过 $set 来调⽤ Object.defineProperty() 处理。 42 | 2. ⽆法监控到数组下标和⻓度的变化。 43 | 44 | 45 | 46 | Vue3 使⽤ Proxy 来监控数据的变化。Proxy 是 ES6 中提供的功能,其作⽤为:⽤于定义基本操作的 ⾃定义⾏为(如属性查找,赋值,枚举,函数调⽤等)。相对于 Object.defineProperty() ,其有 以下特点: 47 | 48 | 1. Proxy 直接代理整个对象⽽⾮对象属性,这样只需做⼀层代理就可以监听同级结构下的所有属性变 化,包括新增属性和删除属性。 49 | 2. Proxy 可以监听数组的变化。 50 | 51 | 52 | 53 | ## Vue3.0 为什么要用 proxy 54 | 55 | 在 Vue2 中, Object.defineProperty 会改变原始数据,⽽ Proxy 是创建对象的虚拟表示,并提供 set 、get 和 deleteProperty 等处理器,这些处理器可在访问或修改原始对象上的属性时进⾏拦截,有以下 特点∶ 56 | 57 | * 不需⽤使⽤` Vue.$set` 或 `Vue.$delete` 触发响应式。 58 | * 全⽅位的数组变化检测,消除了Vue2 ⽆效的边界情况。 59 | * ⽀持 Map,Set,WeakMap 和 WeakSet。 60 | 61 | Proxy 实现的响应式原理与 Vue2的实现原理相同,实现⽅式⼤同⼩异∶ 62 | 63 | * get 收集依赖 64 | * Set、delete 等触发依赖 65 | * 对于集合类型,就是对集合对象的⽅法做⼀层包装:原⽅法执⾏后执⾏依赖相关的收集或触发逻辑。 66 | 67 | 68 | 69 | ## Vue3.0 中的 Vue Composition API 70 | 71 | 在 Vue2 中,代码是 Options API ⻛格的,也就是通过填充 (option) data、methods、computed 等 属性来完成⼀个 Vue 组件。这种⻛格使得 Vue 相对于 React极为容易上⼿,同时也造成了⼏个问题: 72 | 73 | 1. 由于 Options API 不够灵活的开发⽅式,使得Vue开发缺乏优雅的⽅法来在组件间共⽤代码。 74 | 2. Vue 组件过于依赖 this 上下⽂,Vue 背后的⼀些⼩技巧使得 Vue 组件的开发看起来与 JavaScript 的开发原则相悖,⽐如在 methods 中的 this 竟然指向组件实例来不指向 methods 所在的对象。这也使得 TypeScript 在Vue2 中很不好⽤。 75 | 76 | 于是在 Vue3 中,舍弃了 Options API,转⽽投向 Composition API。Composition API本质上是将 Options API 背后的机制暴露给⽤户直接使⽤,这样⽤户就拥有了更多的灵活性,也使得 Vue3 更适合 于 TypeScript 结合。 77 | 78 | 如下,是⼀个使⽤了 Vue Composition API 的 Vue3 组件: 79 | 80 | ```vue 81 | 84 | 104 | 105 | ``` 106 | 107 | 显⽽易⻅,Vue Composition API 使得 Vue3 的开发⻛格更接近于原⽣ JavaScript,带给开发者更多 地灵活性 108 | 109 | 110 | 111 | ### Composition API 与 React Hook 很像,区别是什么 112 | 113 | 从React Hook的实现⻆度看,React Hook是根据useState调⽤的顺序来确定下⼀次重渲染时的state是 来源于哪个useState,所以出现了以下限制 114 | 115 | * 不能在循环、条件、嵌套函数中调⽤Hook 116 | * 必须确保总是在你的React函数的顶层调⽤Hook 117 | * useEffect、useMemo等函数必须⼿动确定依赖关系 118 | 119 | ⽽Composition API是基于Vue的响应式系统实现的,与React Hook的相⽐ 120 | 121 | * 声明在setup函数内,⼀次组件实例化只调⽤⼀次setup,⽽React Hook每次重渲染都需要调⽤ Hook,使得React的GC⽐Vue更有压⼒,性能也相对于Vue来说也较慢 122 | * Compositon API的调⽤不需要顾虑调⽤顺序,也可以在循环、条件、嵌套函数中使⽤ 123 | * 响应式系统⾃动实现了依赖收集,进⽽组件的部分的性能优化由Vue内部⾃⼰完成,⽽React Hook 需要⼿动传⼊依赖,⽽且必须必须保证依赖的顺序,让useEffect、useMemo等函数正确的捕获依 赖变量,否则会由于依赖不正确使得组件性能下降。 124 | 125 | 虽然Compositon API看起来⽐React Hook好⽤,但是其设计思想也是借鉴React Hook的。 -------------------------------------------------------------------------------- /前端/Vue/Vuex.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PokIsemaine/interview/bd5da05b557897a71361f8dbb9a6a6d25cf28d46/前端/Vue/Vuex.md -------------------------------------------------------------------------------- /前端/Vue/Vue基础.md: -------------------------------------------------------------------------------- 1 | # Vue 基础 2 | 3 | ## Vue的基本原理 4 | 5 | 当⼀个Vue实例创建时,Vue会遍历data中的属性,⽤ Object.defineProperty(vue3.0使⽤proxy )将 它们转为 getter/setter,并且在内部追踪相关依赖,在属性被访问和修改时通知变化。 每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter被 调⽤时,会通知watcher重新计算,从⽽致使它关联的组件得以更新。 6 | 7 | ![image.png](https://s2.loli.net/2023/04/27/AscaCNv7VSDdewJ.png) 8 | 9 | ## 双向数据绑定的原理 10 | 11 | Vue.js 是采⽤数据劫持结合发布者-订阅者模式的⽅式,通过Object.defineProperty()来劫持各个属性 的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几个步骤: 12 | 13 | 1. 需要observe的数据对象进⾏递归遍历,包括⼦属性对象的属性,都加上setter和getter这样的话, 给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化 14 | 2. compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染⻚⾯视图,并将每个指令对应 的节点绑定更新函数,添加监听数据的订阅者,⼀旦数据有变动,收到通知,更新视图 15 | 3. Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是: ①在⾃身实例化时往属性 订阅器(dep)⾥⾯添加⾃⼰ ②⾃身必须有⼀个update()⽅法 ③待属性变动dep.notice()通知时,能调 ⽤⾃身的update()⽅法,并触发Compile中绑定的回调,则功成身退。 16 | 4. MVVM作为数据绑定的⼊⼝,整合Observer、Compile和Watcher三者,通过Observer来监听⾃⼰ 的model数据变化,通过Compile来解析编译模板指令,最终利⽤Watcher搭起Observer和Compile 之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定 效果。 17 | 18 | ![image.png](https://s2.loli.net/2023/04/27/E5UDI2JjpobVxqw.png) 19 | 20 | 21 | 22 | ## 使⽤ Object.defineProperty() 来进⾏数据劫持有什么缺点? 23 | 24 | 在对⼀些属性进⾏操作时,使⽤这种⽅法⽆法拦截,⽐如通过下标⽅式修改数组数据或者给对象新增属 性,这都不能触发组件的重新渲染,因为 Object.defineProperty 不能拦截到这些操作。更精确的来 说,对于数组⽽⾔,⼤部分操作都是拦截不到的,只是 Vue 内部通过重写函数的⽅式解决了这个问题。 在 Vue3.0 中已经不使⽤这种⽅式了,⽽是通过使⽤ Proxy 对对象进⾏代理,从⽽实现数据劫持。使⽤ Proxy 的好处是它可以完美的监听到任何⽅式的数据改变,唯⼀的缺点是兼容性的问题,因为 Proxy 是 ES6 的语法。 25 | 26 | 27 | 28 | ## MVVM、MVC、MVP的区别 29 | 30 | MVC、MVP 和 MVVM 是三种常见的软件架构设计模式,主要通过分离关注点的⽅式来组织代码结构, 优化开发效率。 在开发单⻚⾯应⽤时,往往⼀个路由⻚⾯对应了⼀个脚本⽂件,所有的⻚⾯逻辑都在⼀个脚本⽂件⾥。 ⻚⾯的渲染、数据的获取,对⽤户事件的响应所有的应⽤逻辑都混合在⼀起,这样在开发简单项⽬时,可能看不出什么问题,如果项⽬变得复杂,那么整个⽂件就会变得冗⻓、混乱,这样对项⽬开发和后期 的项⽬维护是⾮常不利的。 31 | 32 | (1)MVC 33 | 34 | MVC 通过分离 Model、View 和 Controller 的⽅式来组织代码结构。其中 View 负责⻚⾯的显示逻 辑,Model 负责存储⻚⾯的业务数据,以及对相应数据的操作。并且 View 和 Model 应⽤了观察者模 式,当 Model 层发⽣改变的时候它会通知有关 View 层更新⻚⾯。Controller 层是 View 层和 Model 层的纽带,它主要负责⽤户与应⽤的响应操作,当⽤户与⻚⾯产⽣交互的时候,Controller 中的事件触 发器就开始⼯作了,通过调⽤ Model 层,来完成对 Model 的修改,然后 Model 层再去通知 View 层 更新。 35 | 36 | ![image.png](https://s2.loli.net/2023/04/27/ENGeSKktlFXngDr.png) 37 | 38 | (2)MVVM 39 | 40 | MVVM 分为 Model、View、ViewModel: 41 | 42 | * Model代表数据模型,数据和业务逻辑都在Model层中定义; 43 | * View代表UI视图,负责数据的展示; ViewModel负责监听Model中数据的改变并且控制视图的更新,处理⽤户交互操作; 44 | * Model和View并⽆直接关联,⽽是通过ViewModel来进⾏联系的,Model和ViewModel之间有着双向数 据绑定的联系。 45 | 46 | 因此当Model中的数据改变时会触发View层的刷新,View中由于⽤户交互操作⽽改变的 数据也会在Model中同步。 这种模式实现了 Model和View的数据⾃动同步,因此开发者只需要专注于数据的维护操作即可,⽽不需 要⾃⼰操作DOM。 47 | 48 | ![image.png](https://s2.loli.net/2023/04/27/1b8faOhPVICL7SG.png) 49 | 50 | (3)MVP 51 | 52 | MVP 模式与 MVC 唯⼀不同的在于 Presenter 和 Controller。在 MVC 模式中使⽤观察者模式,来实现 当 Model 层数据发⽣变化的时候,通知 View 层的更新。这样 View 层和 Model 层耦合在⼀起,当项 ⽬逻辑变得复杂的时候,可能会造成代码的混乱,并且可能会对代码的复⽤性造成⼀些问题。MVP 的 模式通过使⽤ Presenter 来实现对 View 层和 Model 层的解耦。MVC 中的Controller 只知道 Model 的接⼝,因此它没有办法控制 View 层的更新,MVP 模式中,View 层的接⼝暴露给了 Presenter 因此 可以在 Presenter 中将 Model 的变化和 View 的变化绑定在⼀起,以此来实现 View 和 Model 的同步 更新。这样就实现了对 View 和 Model 的解耦,Presenter 还包含了其他的响应逻辑。 53 | 54 | 55 | 56 | ## Computed 和 Watch 的区别 57 | 58 | 对于Computed: 59 | 60 | * 它⽀持缓存,只有依赖的数据发⽣了变化,才会重新计算 61 | * 不⽀持异步,当Computed中有异步操作时,⽆法监听数据的变化 62 | * computed的值会默认⾛缓存,计算属性是基于它们的响应式依赖进⾏缓存的,也就是基于data声 明过,或者⽗组件传递过来的props中的数据进⾏计算的。 63 | * 如果⼀个属性是由其他属性计算⽽来的,这个属性依赖其他的属性,⼀般会使⽤computed 64 | * 如果computed属性的属性值是函数,那么默认使⽤get⽅法,函数的返回值就是属性的属性值; 65 | * 在 computed中,属性有⼀个get⽅法和⼀个set⽅法,当数据发⽣变化时,会调⽤set⽅法。 66 | 67 | 68 | 69 | 对于Watch: 70 | 71 | * 它不⽀持缓存,数据变化时,它就会触发相应的操作 72 | * ⽀持异步监听 73 | * 监听的函数接收两个参数,第⼀个参数是最新的值,第⼆个是变化之前的值 74 | * 当⼀个属性发⽣变化时,就需要执⾏相应的操作 75 | * 监听数据必须是data中声明的或者⽗组件传递过来的props中的数据,当发⽣变化时,会触发其他 操作,函数有两个的参数: 76 | * immediate:组件加载⽴即触发回调函数 77 | * deep:深度监听,发现数据内部的变化,在复杂数据类型中使⽤,例如数组中的对象发⽣变 化。需要注意的是,deep⽆法监听到数组和对象内部的变化。 78 | 79 | 当想要执⾏异步或者昂贵的操作以响应不断的变化时,就需要使⽤watch。 80 | 81 | 82 | 83 | 总结: 84 | 85 | * computed 计算属性 : 依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值 发⽣改变,下⼀次获取 computed 的值时才会重新计算 computed 的值。 86 | * watch 侦听器 : 更多的是观察的作⽤,⽆缓存性,类似于某些数据的监听回调,每当监听的数 据变化时都会执⾏回调进⾏后续操作。 87 | 88 | 运⽤场景: 89 | 90 | * 当需要进⾏数值计算,并且依赖于其它数据时,应该使⽤ computed,因为可以利⽤ computed 的缓存特性,避免每次获取值时都要重新计算。 91 | * 当需要在数据变化时执⾏异步或开销较⼤的操作时,应该使⽤ watch,使⽤ watch 选项允许 执⾏异步操作 ( 访问⼀个 API ),限制执⾏该操作的频率,并在得到最终结果前,设置中间状 态。这些都是计算属性⽆法做到的。 -------------------------------------------------------------------------------- /前端/Vue/生命周期.md: -------------------------------------------------------------------------------- 1 | # 生命周期 -------------------------------------------------------------------------------- /前端/Vue/组件通信.md: -------------------------------------------------------------------------------- 1 | # 组件通信 -------------------------------------------------------------------------------- /前端/Vue/虚拟 DOM.md: -------------------------------------------------------------------------------- 1 | # 虚拟 DOM 2 | 3 | ## 对虚拟 DOM 的理解 4 | 5 | 从本质上来说,Virtual Dom是⼀个JavaScript对象,通过对象的⽅式来表示DOM结构。将⻚⾯的状态 抽象为JS对象的形式,配合不同的渲染⼯具,使跨平台渲染成为可能。通过事务处理机制,将多次 DOM修改的结果⼀次性的更新到⻚⾯上,从⽽有效的减少⻚⾯渲染的次数,减少修改DOM的重绘重排 次数,提⾼渲染性能。 6 | 7 | 虚拟DOM是对DOM的抽象,这个对象是更加轻量级的对 DOM的描述。它设计的最初⽬的,就是更好的 跨平台,⽐如Node.js就没有DOM,如果想实现SSR,那么⼀个⽅式就是借助虚拟DOM,因为虚拟 DOM本身是js对象。 在代码渲染到⻚⾯之前,vue会把代码转换成⼀个对象(虚拟 DOM)。以对象的 形式来描述真实DOM结构,最终渲染到⻚⾯。在每次数据发⽣变化前,虚拟DOM都会缓存⼀份,变化 之时,现在的虚拟DOM会与缓存的虚拟DOM进⾏⽐较。在vue内部封装了diff算法,通过这个算法来进 ⾏⽐较,渲染时修改改变的变化,原先没有发⽣改变的通过原先的数据进⾏渲染。 8 | 9 | 10 | 11 | 另外现代前端框架的⼀个基本要求就是⽆须⼿动操作DOM,⼀⽅⾯是因为⼿动操作DOM⽆法保证程序 性能,多⼈协作的项⽬中如果review不严格,可能会有开发者写出性能较低的代码,另⼀⽅⾯更重要的 是省略⼿动DOM操作可以⼤⼤提⾼开发效率。 12 | 13 | ## 虚拟 DOM 的解析过程 14 | 15 | 虚拟DOM的解析过程: 16 | 17 | * ⾸先对将要插⼊到⽂档中的 DOM 树结构进⾏分析,使⽤ js 对象将其表示出来,⽐如⼀个元素对 象,包含 TagName、props 和 Children 这些属性。然后将这个 js 对象树给保存下来,最后再将 DOM ⽚段插⼊到⽂档中。 18 | * 当⻚⾯的状态发⽣改变,需要对⻚⾯的 DOM 的结构进⾏调整的时候,⾸先根据变更的状态,重新 构建起⼀棵对象树,然后将这棵新的对象树和旧的对象树进⾏⽐较,记录下两棵树的的差异。 19 | * 最后将记录的有差异的地⽅应⽤到真正的 DOM 树中去,这样视图就更新了。 20 | 21 | 22 | 23 | ## 为什么要虚拟 DOM 24 | 25 | (1)保证性能下限,在不进⾏⼿动优化的情况下,提供过得去的性能 26 | 27 | 看⼀下⻚⾯渲染的流程:解析HTML -> ⽣成DOM -> ⽣成 CSSOM -> Layout -> Paint -> Compiler 28 | 29 | 下⾯对⽐⼀下修改DOM时真实DOM操作和Virtual DOM的过程,来看⼀下它们重排重绘的性能消耗∶ 30 | 31 | * 真实DOM∶ ⽣成HTML字符串+重建所有的DOM元素 32 | * 虚拟DOM∶ ⽣成vNode+ DOMDiff+必要的dom更新 33 | 34 | Virtual DOM的更新DOM的准备⼯作耗费更多的时间,也就是JS层⾯,相⽐于更多的DOM操作它的消 费是极其便宜的。尤⾬溪在社区论坛中说道∶ 框架给你的保证是,你不需要⼿动优化的情况下,依然可 以给你提供过得去的性能。 35 | 36 | 37 | 38 | (2)跨平台 39 | 40 | Virtual DOM本质上是JavaScript的对象,它可以很⽅便的跨平台操作,⽐如服务端渲染、uniapp等。 41 | 42 | ## 虚拟 DOM 真的比真实 DOM 性能好吗 43 | 44 | * ⾸次渲染⼤量DOM时,由于多了⼀层虚拟DOM的计算,会⽐innerHTML插⼊慢。 45 | * 正如它能保证性能下限,在真实DOM操作的时候进⾏针对性的优化时,还是更快的。 46 | 47 | 48 | 49 | ## DIFF 算法的原理 50 | 51 | 在新⽼虚拟DOM对⽐时: 52 | 53 | * ⾸先,对⽐节点本身,判断是否为同⼀节点,如果不为相同节点,则删除该节点重新创建节点进⾏ 替换 54 | 55 | * 如果为相同节点,进⾏patchVnode,判断如何对该节点的⼦节点进⾏处理,先判断⼀⽅有⼦节点⼀ ⽅没有⼦节点的情况(如果新的children没有⼦节点,将旧的⼦节点移除) 56 | 57 | * ⽐较如果都有⼦节点,则进⾏updateChildren,判断如何对这些新⽼节点的⼦节点进⾏操作(diff 核⼼)。 58 | 59 | * 匹配时,找到相同的⼦节点,递归⽐较⼦节点 60 | 61 | 62 | 63 | 在diff中,只对同层的⼦节点进⾏⽐较,放弃跨级的节点⽐较,使得时间复杂从$O(n^3 )$降低值 $O(n)$,也就 是说,只有当新旧children都为多个⼦节点时才需要⽤核⼼的Diff算法进⾏同层级⽐较。 64 | 65 | 66 | 67 | ## Vue 中 key 的作用 68 | 69 | vue 中 key 值的作⽤可以分为两种情况来考虑: 70 | 71 | * 第⼀种情况是 v-if 中使⽤ key。由于 Vue 会尽可能⾼效地渲染元素,通常会复⽤已有元素⽽不是 从头开始渲染。因此当使⽤ v-if 来实现元素切换的时候,如果切换前后含有相同类型的元素,那么 这个元素就会被复⽤。如果是相同的 input 元素,那么切换前后⽤户的输⼊不会被清除掉,这样是 不符合需求的。因此可以通过使⽤ key 来唯⼀的标识⼀个元素,这个情况下,使⽤ key 的元素不会 被复⽤。这个时候 key 的作⽤是⽤来标识⼀个独⽴的元素。 72 | * 第⼆种情况是 v-for 中使⽤ key。⽤ v-for 更新已渲染过的元素列表时,它默认使⽤“就地复⽤”的 策略。如果数据项的顺序发⽣了改变,Vue 不会移动 DOM 元素来匹配数据项的顺序,⽽是简单复 ⽤此处的每个元素。因此通过为每个列表项提供⼀个 key 值,来以便 Vue 跟踪元素的身份,从⽽ ⾼效的实现复⽤。这个时候 key 的作⽤是为了⾼效的更新渲染虚拟 DOM。 73 | 74 | 75 | 76 | key 是为 Vue 中 vnode 的唯⼀标记,通过这个 key,diff 操作可以更准确、更快速 更准确: 77 | 78 | * 因为带 key 就不是就地复⽤了,在 sameNode 函数a.key === b.key对⽐中可以避免就地 复⽤的情况。所以会更加准确。 79 | * 更快速:利⽤ key 的唯⼀性⽣成 map 对象来获取对应节点,⽐遍历⽅式更快 80 | 81 | ## 为什么不建议用 index 作为 key? 82 | 83 | 使⽤index 作为 key和没写基本上没区别,因为不管数组的顺序怎么颠倒,index 都是 0, 1, 2...这样排 列,导致 Vue 会复⽤错误的旧⼦节点,做很多额外的⼯作。 -------------------------------------------------------------------------------- /前端/Vue/路由.md: -------------------------------------------------------------------------------- 1 | # 路由 -------------------------------------------------------------------------------- /前端/构建打包/Babel.md: -------------------------------------------------------------------------------- 1 | # Babel 2 | 3 | ## Babel 原理 4 | 5 | babel 的转译过程也分为三个阶段,这三步具体是: 6 | 7 | * 解析 Parse: 将代码解析⽣成抽象语法树(AST),即词法分析与语法分析的过程; 8 | * 转换 Transform: 对于 AST 进⾏变换⼀系列的操作,babel 接受得到 AST 并通过 babel-traverse 对其进⾏遍历,在此过程中进⾏添加、更新及移除等操作; 9 | * ⽣成 Generate: 将变换后的 AST 再转换为 JS 代码, 使⽤到的模块是 babel-generator。 10 | 11 | ![image.png](https://s2.loli.net/2023/04/27/jaEuBywiIODkZCb.png) -------------------------------------------------------------------------------- /前端/构建打包/Vite.md: -------------------------------------------------------------------------------- 1 | # Vite 2 | 3 | -------------------------------------------------------------------------------- /前端/构建打包/Webpack.md: -------------------------------------------------------------------------------- 1 | # Webpack 2 | 3 | ## 说说你对 webpack 的理解 4 | 5 | webpack 是一个静态模块的打包工具。它会在内部从一个或多个入口点构建一个依赖图,然后将项目中所需的每一个模块组合成一个或多个 bundles 进行输出,它们均为静态资源。输出的文件已经编译好了,可以在浏览器运行。 webpack 具有打包压缩、编译兼容、能力扩展等功能。其最初的目标是实现前端项目的模块化,也就是如何更高效地管理和维护项目中的每一个资源。 ![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/946c5dafab804ac39cf12b1e1dd8bb2e~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?) 6 | 7 | webpack 有五大核心概念: 8 | 9 | - 入口(entry) 10 | - 输出(output) 11 | - 解析器(loader) 12 | - 插件(plugin) 13 | - 模式(mode) 14 | 15 | 16 | 17 | ## webpack 的作用 18 | 19 | * 模块打包。可以将不同模块的文件打包整合在一起,并保证它们之间的引用正确,执行有序。 20 | * 编译兼容。通过 webpack 的 Loader 机制,可以编译转换诸如 .less, .vue, .jsx 这类在浏览器无法识别的文件,让我们在开发的时候可以使用新特性和新语法,提高开发效率。 21 | * 能力扩展。通过 webpack 的 Plugin 机制,可以进一步实现诸如按需加载,代码压缩等功能,帮助我们提高工程效率以及打包输出的质量。 22 | 23 | 24 | 25 | ## webpack与grunt、gulp的不同? 26 | 27 | Grunt、Gulp是基于任务运⾏的⼯具: 它们会⾃动执⾏指定的任务,就像流⽔线,把资源放上去然后通 过不同插件进⾏加⼯,它们包含活跃的社区,丰富的插件,能⽅便的打造各种⼯作流。 28 | 29 | 30 | 31 | **Webpack是基于模块化打包的⼯具**: ⾃动化处理模块,webpack把⼀切当成模块,当 webpack 处理应 ⽤程序时,它会递归地构建⼀个依赖关系图 (dependency graph),其中包含应⽤程序需要的每个模 块,然后将所有这些模块打包成⼀个或多个 bundle。 32 | 33 | 34 | 35 | 因此这是完全不同的两类⼯具,⽽现在主流的⽅式是⽤npm script代替Grunt、Gulp,npm script同样可 以打造任务流。 36 | 37 | 38 | 39 | ## webpack、rollup、parcel优劣? 40 | 41 | * webpack适⽤于⼤型复杂的前端站点构建: webpack有强⼤的loader和插件⽣态,打包后的⽂件实际 上就是⼀个⽴即执⾏函数,这个⽴即执⾏函数接收⼀个参数,这个参数是模块对象,键为各个模块 的路径,值为模块内容。⽴即执⾏函数内部则处理模块之间的引⽤,执⾏模块等,这种情况更适合⽂ 件依赖复杂的应⽤开发。 42 | * rollup适⽤于基础库的打包,如vue、d3等: Rollup 就是将各个模块打包进⼀个⽂件中,并且通过 Tree-shaking 来删除⽆⽤的代码,可以最⼤程度上降低代码体积,但是rollup没有webpack如此多的 的如代码分割、按需加载等⾼级功能,其更聚焦于库的打包,因此更适合库的开发。 43 | * parcel适⽤于简单的实验性项⽬: 他可以满⾜低⻔槛的快速看到效果,但是⽣态差、报错信息不够全 ⾯都是他的硬伤,除了⼀些玩具项⽬或者实验项⽬不建议使⽤。 44 | 45 | 46 | 47 | ## 有哪些常⻅的Loader? 48 | 49 | * file-loader:把⽂件输出到⼀个⽂件夹中,在代码中通过相对 URL 去引⽤输出的⽂件 50 | 51 | * url-loader:和 file-loader 类似,但是能在⽂件很⼩的情况下以 base64 的⽅式把⽂件内容注⼊到 代码中去 52 | 53 | * source-map-loader:加载额外的 Source Map ⽂件,以⽅便断点调试 54 | 55 | * image-loader:加载并且压缩图⽚⽂件 babel-loader:把 ES6 转换成 ES5 56 | 57 | * css-loader:加载 CSS,⽀持模块化、压缩、⽂件导⼊等特性 58 | 59 | * style-loader:把 CSS 代码注⼊到 JavaScript 中,通过 DOM 操作去加载 CSS。 60 | 61 | * eslint-loader:通过 ESLint 检查 JavaScript 代码 62 | 63 | 64 | 65 | 注意:在Webpack中,loader的执⾏顺序是从右向左执⾏的。因为webpack选择了compose这样 的函数式编程⽅式,这种⽅式的表达式执⾏是从右向左的。 66 | 67 | 68 | 69 | ## 有哪些常⻅的Plugin? 70 | 71 | * define-plugin:定义环境变量 72 | * html-webpack-plugin:简化html⽂件创建 73 | * uglifyjs-webpack-plugin:通过 UglifyES 压缩 ES6 代码 74 | * webpack-parallel-uglify-plugin: 多核压缩,提⾼压缩速度 75 | * webpack-bundle-analyzer: 可视化webpack输出⽂件的体积 76 | * mini-css-extract-plugin: CSS提取到单独的⽂件中,⽀持按需加载 77 | 78 | 79 | 80 | ## bundle,chunk,module是什么? 81 | 82 | * bundle:是由webpack打包出来的⽂件; 83 | * chunk:代码块,⼀个chunk由多个模块组合⽽成,⽤于代码的合并和分割; 84 | * module:是开发中的单个模块,在webpack的世界,⼀切皆模块,⼀个模块对应⼀个⽂件, webpack会从配置的 entry中递归开始找出所有依赖的模块。 85 | 86 | 87 | 88 | ## Loader和Plugin的不同? 89 | 90 | 不同的作⽤: 91 | 92 | * Loader直译为"加载器"。Webpack将⼀切⽂件视为模块,但是webpack原⽣是只能解析js⽂件,如 果想将其他⽂件也打包的话,就会⽤到 loader 。 所以Loader的作⽤是让webpack拥有了加载和解 析⾮JavaScript⽂件的能⼒。 Plugin直译为"插件"。 93 | * Plugin可以扩展webpack的功能,让webpack具有更多的灵活性。 在 Webpack 运⾏的⽣命周期中会⼴播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。 94 | 95 | 不同的⽤法: 96 | 97 | * Loader在 module.rules 中配置,也就是说他作为模块的解析规则⽽存在。 类型为数组,每⼀项都 是⼀个 Object ,⾥⾯描述了对于什么类型的⽂件( test ),使⽤什么加载( loader )和使⽤的参数 ( options ) 98 | * Plugin在 plugins 中单独配置。 类型为数组,每⼀项是⼀个 plugin 的实例,参数都通过构造函数 传⼊。 99 | 100 | 101 | 102 | ## webpack 的构建流程 103 | 104 | Webpack 的运⾏流程是⼀个串⾏的过程,从启动到结束会依次执⾏以下流程: 105 | 106 | 1. 初始化参数:从配置⽂件和 Shell 语句中读取与合并参数,得出最终的参数; 107 | 2. 开始编译:⽤上⼀步得到的参数初始化 Compiler 对象,加载所有配置的插件,执⾏对象的 run ⽅ 法开始执⾏编译; 108 | 3. 确定⼊⼝:根据配置中的 entry 找出所有的⼊⼝⽂件; 109 | 4. 编译模块:从⼊⼝⽂件出发,调⽤所有配置的 Loader 对模块进⾏翻译,再找出该模块依赖的模 块,再递归本步骤直到所有⼊⼝依赖的⽂件都经过了本步骤的处理; 110 | 5. 完成模块编译:在经过第4步使⽤ Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容 以及它们之间的依赖关系; 111 | 6. 输出资源:根据⼊⼝和模块之间的依赖关系,组装成⼀个个包含多个模块的 Chunk,再把每个 Chunk 转换成⼀个单独的⽂件加⼊到输出列表,这步是可以修改输出内容的最后机会; 112 | 7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和⽂件名,把⽂件内容写⼊到⽂件系 统。 113 | 114 | 在以上过程中,Webpack 会在特定的时间点⼴播出特定的事件,插件在监听到感兴趣的事件后会执⾏ 特定的逻辑,并且插件可以调⽤ Webpack 提供的 API 改变 Webpack 的运⾏结果。 115 | 116 | 117 | 118 | ## 编写loader或plugin的思路? 119 | 120 | Loader像⼀个"翻译官"把读到的源⽂件内容转义成新的⽂件内容,并且每个Loader通过链式操作,将源 ⽂件⼀步步翻译成想要的样⼦。 121 | 122 | 编写Loader时要遵循单⼀原则,每个Loader只做⼀种"转义"⼯作。 每个Loader的拿到的是源⽂件内容 (source),可以通过返回值的⽅式将处理后的内容输出,也可以调⽤ this.callback() ⽅法,将内容返 回给webpack。 还可以通过this.async() ⽣成⼀个 callback 函数,再⽤这个callback将处理后的内容输 出出去。 此外 webpack 还为开发者准备了开发loader的⼯具函数集——loader-utils 。 123 | 124 | 相对于Loader⽽⾔,Plugin的编写就灵活了许多。 webpack在运⾏的⽣命周期中会⼴播出许多事件, Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。 125 | 126 | 127 | 128 | ## webpack 热更新的实现原理? 129 | 130 | webpack的热更新⼜称热替换(Hot Module Replacement),缩写为HMR。 这个机制可以做到不⽤ 刷新浏览器⽽将新变更的模块替换掉旧的模块。 131 | 132 | 原理: 133 | 134 | ![image.png](https://s2.loli.net/2023/04/27/jaEuBywiIODkZCb.png) 135 | 136 | ⾸先要知道server端和client端都做了处理⼯作: 137 | 138 | 1. 第⼀步,在 webpack 的 watch 模式下,⽂件系统中某⼀个⽂件发⽣修改,webpack 监听到⽂件 变化,根据配置⽂ 件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中。 139 | 2. 第⼆步是 webpack-dev-server 和 webpack 之间的接⼝交互,⽽在这⼀步,主要是 dev-server 的中间件 webpack- dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调 ⽤ webpack 暴露的 API对代码变化进⾏监 控,并且告诉 webpack,将代码打包到内存中。 140 | 3. 第三步是 webpack-dev-server 对⽂件变化的⼀个监控,这⼀步不同于第⼀步,并不是监控代码变 化重新打包。当我们在配置⽂件中配置了devServer.watchContentBase 为 true 的时候,Server 会监听这些配置⽂件夹中静态⽂件的变化,变化后会通知浏览器端对应⽤进⾏ live reload。注意, 这⼉是浏览器刷新,和 HMR 是两个概念。 141 | 4. 第四步也是 webpack-dev-server 代码的⼯作,该步骤主要是通过 sockjs(webpack-devserver 的依赖)在浏览器端和服务端之间建⽴⼀个 websocket ⻓连接,将 webpack 编译打包的 各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态⽂件变化的信息。浏览 器端根据这些 socket 消息进⾏不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值, 后⾯的步骤根据这⼀ hash 值来进⾏模块热替换。 142 | 5. webpack-dev-server/client 端并不能够请求更新的代码,也不会执⾏热更模块操作,⽽把这些⼯ 作⼜交回给了webpack,webpack/hot/dev-server 的⼯作就是根据 webpack-devserver/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进⾏模块热更新。当 然如果仅仅是刷新浏览器,也就没有后⾯那些步骤了。 143 | 6. HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上⼀步传递给他的新模块的 hash 值,它通过JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回⼀个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请 求,获取到最新的模块代码。这就是上图中 7、8、9 步骤。 144 | 7. ⽽第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进 ⾏对⽐,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新 模块间的依赖引⽤。 145 | 8. 最后⼀步,当 HMR 失败后,回退到 live reload 操作,也就是进⾏浏览器刷新来获取最新打包代 码。 146 | 147 | 148 | 149 | ## 如何⽤webpack来优化前端性能? 150 | 151 | ⽤webpack优化前端性能是指优化webpack的输出结果,让打包的最终结果在浏览器运⾏快速⾼效。 152 | 153 | * 压缩代码:删除多余的代码、注释、简化代码的写法等等⽅式。可以利⽤webpack的 UglifyJsPlugin 和 ParallelUglifyPlugin 来压缩JS⽂件, 利⽤ cssnano (css-loader?minimize) 来压缩css 154 | * 利⽤CDN加速: 在构建过程中,将引⽤的静态资源路径修改为CDN上对应的路径。可以利⽤ webpack对于 output 参数和各loader的 publicPath 参数来修改资源路径 155 | * Tree Shaking: 将代码中永远不会⾛到的⽚段删除掉。可以通过在启动webpack时追加参数 -- optimize-minimize 来实现 156 | * Code Splitting: 将代码按路由维度或者组件分块(chunk),这样做到按需加载,同时可以充分利⽤浏览 器缓存 157 | * 提取公共第三⽅库: SplitChunksPlugin插件来进⾏公共模块抽取,利⽤浏览器缓存可以⻓期缓存这些 ⽆需频繁变动的公共代码 158 | 159 | 160 | 161 | ## 如何提⾼webpack的打包速度 162 | 163 | * happypack: 利⽤进程并⾏编译loader,利⽤缓存来使得 rebuild 更快,遗憾的是作者表示已经不会继 续开发此项⽬,类似的替代者是thread-loader 164 | * 外部扩展(externals): 将不怎么需要更新的第三⽅库脱离webpack打包,不被打⼊bundle中,从⽽ 减少打包时间,⽐如jQuery⽤script标签引⼊ 165 | * dll: 采⽤webpack的 DllPlugin 和 DllReferencePlugin 引⼊dll,让⼀些基本不会改动的代码先打包 成静态资源,避免反复编译浪费时间 166 | * 利⽤缓存: webpack.cache 、babel-loader.cacheDirectory、 HappyPack.cache 都可以利⽤缓存 提⾼rebuild效率缩⼩⽂件搜索范围: ⽐如babel-loader插件,如果你的⽂件仅存在于src中,那么可以 include: path.resolve(__dirname,'src') ,当然绝⼤多数情况下这种操作的提升有限,除⾮不⼩⼼ build了node_modules⽂件 167 | 168 | 169 | 170 | ## 如何提⾼webpack的构建速度? 171 | 172 | 1. 多⼊⼝情况下,使⽤ CommonsChunkPlugin 来提取公共代码 173 | 2. 通过 externals 配置来提取常⽤库 174 | 3. 利⽤ DllPlugin 和 DllReferencePlugin 预编译资源模块 通过 DllPlugin 来对那些我们引⽤但是绝对 不会修改的npm包来进⾏预编译,再通过 DllReferencePlugin 将预编译的模块加载进来。 175 | 4. 使⽤ Happypack 实现多线程加速编译 176 | 5. 使⽤ webpack-uglify-parallel 来提升 uglifyPlugin 的压缩速度。 原理上 webpack-uglifyparallel 采⽤了多核并⾏压缩来提升压缩速度 177 | 6. 使⽤ Tree-shaking 和 Scope Hoisting 来剔除多余代码 178 | 179 | 180 | 181 | ## 怎么配置单页应用?怎么配置多页应用? 182 | 183 | 单⻚应⽤可以理解为webpack的标准模式,直接在 entry 中指定单⻚应⽤的⼊⼝即可,这⾥不再赘述多 ⻚应⽤的话,可以使⽤webpack的 AutoWebPlugin 来完成简单⾃动化的构建,但是前提是项⽬的⽬录 结构必须遵守他预设的规范。 多⻚应⽤中要注意的是: 184 | 185 | * 每个⻚⾯都有公共的代码,可以将这些代码抽离出来,避免重复的加载。⽐如,每个⻚⾯都引⽤了 同⼀套css样式表 186 | * 随着业务的不断扩展,⻚⾯可能会不断的追加,所以⼀定要让⼊⼝的配置⾜够灵活,避免每次添加 新⻚⾯还需要修改构建配置 -------------------------------------------------------------------------------- /前端/构建打包/基本概念.md: -------------------------------------------------------------------------------- 1 | # 基本概念 2 | 3 | [面试必备:常见的webpack / Vite面试题汇总 - 掘金 (juejin.cn)](https://juejin.cn/post/7207659644487893051) 4 | 5 | ## 为什么需要打包工具 6 | 7 | 开发时,我们会使用框架 (React、Vue) ,ES6 模块化语法,Less/Sass 等 CSS 预处理器等语法进行开发,这样的代码要想在浏览器运行必须经过编译成浏览器能识别的 JS、CSS语法才能运行。所以我们需要打包工具帮我们做完这些事。除此之外,打包还能压缩代码、做兼容性处理、提升代码性能等。 8 | 9 | 10 | 11 | ## 有哪些打包构建工具 12 | 13 | * Rollup 14 | * Parcel 15 | * Snowpack 16 | * Vite 17 | * Webpack -------------------------------------------------------------------------------- /大数据/Hadoop/HDFS.md: -------------------------------------------------------------------------------- 1 | ## HDFS 2 | 3 | ## 优缺点 4 | 5 | 6 | 7 | ## 架构 8 | 9 | ## 写流程 10 | 11 | 12 | 13 | ## 读流程 14 | 15 | 16 | 17 | ## 创建文件流程 18 | 19 | 20 | 21 | ## 文件块大小 22 | 23 | 24 | 25 | ## 常用配置文件 26 | 27 | 28 | 29 | ## 版本区别 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /大数据/Spark/RDD.md: -------------------------------------------------------------------------------- 1 | # RDD 2 | 3 | ## 什么是 RDD 4 | 5 | RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是 Spark 中最基本的数据处理模型。代码中是一个抽象类,它代表一个弹性的、不可变、可分区、里面的元素可并行计算的集合。 6 | 7 | ## RDD 特点 8 | 9 | * 分区:RDD逻辑上是分区的,每个分区的数据是抽象存在的,计算的时候会通过一个compute函数得到每个分区的数据。如果RDD是通过已有的文件系统构建,则compute函数是读取指定文件系统中的数据,如果RDD是通过其他RDD转换而来,则compute函数是执行转换逻辑将其他RDD的数据进行转换。 10 | * 只读:对RDD进行改动,只能通过RDD的转换操作,由一个RDD得到一个新的RDD 11 | * 依赖:RDDs通过操作算子进行转换,转换得到的新RDD包含了从其他RDDs衍生所必需的信息,RDDs之间维护着这种血缘关系,也称之为依赖。依赖包括两种 12 | * 窄依赖,RDDs之间分区是一一对应的 13 | * 宽依赖,下游RDD的每个分区与上游RDD(也称之为父RDD)的每个分区都有关,是多对多的关系。 14 | * 缓存:如果在应用程序中多次使用同一个RDD,可以将该RDD缓存起来,该RDD只有在第一次计算的时候会根据血缘关系得到分区的数据,在后续其他地方用到该RDD的时候,会直接从缓存处取而不用再根据血缘关系计算,这样就加速后期的重用。 15 | * CheckPoint:虽然RDD的血缘关系天然地可以实现容错,当RDD的某个分区数据失败或丢失,可以通过血缘关系重建。但是对于长时间迭代型应用来说,随着迭代的进行,RDDs之间的血缘关系会越来越长,一旦在后续迭代过程中出错,则需要通过非常长的血缘关系去重建,势必影响性能。为此,RDD支持checkpoint将数据保存到持久化的存储中,这样就可以切断之前的血缘关系,因为checkpoint后的RDD不需要知道它的父RDDs了,它可以从checkpoint处拿到数据。 16 | 17 | ## RDD 算子 18 | 19 | RDD整体上分为Value类型和Key-Value类型 20 | 21 | 比如map、flatmap等这些,回答几个,讲一下原理就差不多了 22 | 23 | map:遍历RDD,将函数f应用于每一个元素,返回新的RDD(transformation算子)。 24 | 25 | foreach:遍历RDD,将函数f应用于每一个元素,无返回值(action算子)。 26 | 27 | mapPartitions:遍历操作RDD中的每一个分区,返回新的RDD(transformation算子)。 28 | 29 | foreachPartition:遍历操作RDD中的每一个分区。无返回值(action算子)。 30 | 31 | # RDD 五大核心属性 32 | 33 | ``` 34 | * Internally, each RDD is characterized by five main properties: 35 | * 36 | * - A list of partitions 37 | * - A function for computing each split 38 | * - A list of dependencies on other RDDs 39 | * - Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned) 40 | * - Optionally, a list of preferred locations to compute each split on (e.g. block locations for 41 | * an HDFS file) 42 | * 43 | * All of the scheduling and execution in Spark is done based on these methods, allowing each RDD 44 | ``` 45 | 46 | * A list of partitions (一个分区列表):这里表示一个RDD很多分区, 每一个分区内部是包含了该RDD的部分数据, Spark中任务是以Task线程的方式运行, 一个分区就对应一个Task线程, 分区列表是实现分布式并行计算的重要属性 47 | ![image](https://github.com/user-attachments/assets/24f8ac56-3357-4911-8cb8-db0c8eb88412) 48 | 49 | * 用户可以在创建RDD时指定RDD的分区个数, 如果没有指定, 那么就会采用默认值.分区数的默认值的计算公式如下: RDD的分区数 = max(文件的block个数, defaultMinPartitions),通过Spark Context读取HDFS上的文件来计算分区数 50 | * A function for computing each split (作用分区中的函数):一个计算每个分区的函数,这里表示Spark中RDD的计算是以分区为单位的,每个RDD都会实现compute计算函数以达到这个目的. 51 | * A list of dependencies on other RDDs (对其他RDD的依赖关系):一个RDD会依赖于其他多个RDD, 这里涉及到RDD与RDD之间的依赖关系,Spark 任务的容错机制就是根据这个特性(血统)而来; 52 | * Optionally, a Partioner for key-value RDDs (针对k-v的分区器):当数据为 KV 类型数据时,可以通过设定分区器(可选)自定义数据的分区 53 | * Optionally, a list of preferred locations to compute each split on (数据本地性):一个列表,存储每个Partition的优先位置(可选项),这里涉及到数据的本地性,数据块位置最优。spark任务在调度的时候会优先考虑存有数据的节点开启计算任务,减少数据的网络传输,提升计算效率 54 | 55 | 56 | ## RDD 和 DataFrame, DataSet 区别和联系 57 | 58 | RDD(Resilient Distributed Dataset) 59 | 60 | RDD是Spark中最基本的数据结构,代表了一个不可变的、分区记录的集合。它允许程序员在大型集群上以容错的方式执行内存计算。RDD具有惰性机制,只有在遇到action算子时才会开始遍历运算。此外,RDD提供了丰富的转换和动作操作,如map、filter、reduce等,使得用户可以方便地对数据进行处理。 61 | 62 | DataFrame 63 | 64 | 与RDD不同,DataFrame在Spark中是以列的形式组织的数据结构,类似于关系数据库中的表。DataFrame是一个不可变的分布式数据集合,它允许开发人员将数据结构(类型)加到分布式数据集合上,从而实现更高级别的抽象。DataFrame提供了更友好的API,让用户能够更轻松地进行数据查询和分析。 65 | 66 | Dataset 67 | 68 | Dataset是DataFrame API的扩展,它提供了类型安全(type-safe)和面向对象(object-oriented)的编程接口。Dataset允许用户在Spark中进行高效的数据处理,同时保持了强类型检查的优点。Dataset利用Catalyst optimizer,允许用户通过类似于SQL的表达式对数据进行查询。 69 | 70 | 三者之间的关系与比较 71 | 72 | RDD、DataFrame和Dataset在Spark中各有优势,它们之间的关系可以归结为以下几点: 73 | 74 | * 共性:三者都是Spark平台下的分布式弹性数据集,为处理超大型数据提供了便利。它们都具有惰性机制、共同的函数(如map、filter、排序等)以及自动缓存运算的特性。此外,三者都有partition的概念,使得数据可以在集群的不同节点上并行处理。 75 | * 区别:DataFrame和Dataset相比于RDD提供了更高级别的抽象和更友好的API。DataFrame以列的形式组织数据,使得数据查询和分析更加便捷。而Dataset则提供了类型安全,使得代码更加健壮和易于维护。 76 | * 应用场景:对于简单的数据处理任务,RDD可能是一个不错的选择。然而,对于需要更复杂查询和分析的任务,DataFrame和Dataset可能更为合适。在实际应用中,开发人员可以根据具体的需求和场景选择最合适的数据结构。 77 | 78 | ## 简述RDD的容错机制 ? 79 | 80 | https://www.cnblogs.com/duanxz/p/6329675.html 81 | 82 | ## 简述什么是 RDD 沿袭 83 | 在Apache Spark中,RDD沿袭(也称为血统,Lineage)是RDD的一个核心概念,它指的是RDD数据的创建和转换历史。每个RDD都记录了它是如何从其他RDD通过一系列转换操作生成的。以下是RDD沿袭的一些关键点: 84 | 85 | 转换操作记录: 86 | 87 | RDD的沿袭记录了所有转换操作,如map、filter、reduce等,这些操作定义了RDD之间的依赖关系。 88 | 依赖关系: 89 | 90 | RDD之间的依赖关系可以是窄依赖或宽依赖。窄依赖意味着子RDD的每个分区是由父RDD的一个或少数几个分区经过一对一的转换生成的。宽依赖则意味着子RDD的每个分区可能由多个父RDD的分区生成。 91 | 容错能力: 92 | 93 | RDD的沿袭为Spark提供了容错能力。如果某个RDD的分区数据丢失,Spark可以利用沿袭信息重新计算丢失的数据。 94 | 数据重构: 95 | 96 | 当RDD被持久化(缓存)时,如果部分数据丢失,Spark可以使用其沿袭信息重新构建丢失的数据,而不需要从头开始重新计算整个数据集。 97 | 优化执行计划: 98 | 99 | Spark的DAGScheduler可以根据RDD的沿袭信息优化作业的执行计划,包括识别可以并行执行的任务和需要按顺序执行的任务。 100 | 内存和存储效率: 101 | 102 | 通过沿袭信息,Spark可以更有效地管理内存和存储资源,因为只有实际需要的数据才会被重新计算和存储。 103 | 转换与行动: 104 | 105 | RDD的转换操作是惰性的,不会立即执行,直到遇到行动操作时,才会根据沿袭信息触发实际的计算。 106 | 数据流: 107 | 108 | RDD沿袭描述了数据在Spark应用程序中的流动方式,从源头数据集开始,通过一系列的转换操作,最终形成结果数据集。 109 | 可扩展性: 110 | 111 | 沿袭机制使得Spark能够轻松扩展新的转换操作,同时保持容错和优化执行计划的能力。 112 | 可视化和调试: 113 | 114 | RDD的沿袭信息可以被可视化,帮助开发者理解数据的来源和转换过程,从而更容易地调试和优化Spark应用程序。 115 | RDD沿袭是Spark设计中的一个关键特性,它为Spark提供了强大的容错能力、优化执行计划的能力,以及高效的内存和存储管理。 116 | 117 | 118 | ## RDD 的缓存级别 119 | 120 | 121 | 9. RDD的cache和persist的区别? 122 | -------------------------------------------------------------------------------- /大数据/Spark/SparkStream.md: -------------------------------------------------------------------------------- 1 | # SparkStream 2 | 3 | 简述Spark Streaming怎么实现数据持久化保存 ? 4 | 5 | 简述Saprk Streaming从Kafka中读取数据两种方式 ? 6 | 7 | 简述Spark Streaming的工作原理 ? 8 | 9 | 简述Spark Streaming和Structed Streaming ? 10 | 11 | 简述Spark Streaming的双流join的过程,怎么做的 ? 12 | -------------------------------------------------------------------------------- /大数据/Spark/内存.md: -------------------------------------------------------------------------------- 1 | # 内存 2 | 3 | * 简述Spark的内存模型( 重要详细 ) ? 4 | * 简述Spark怎么基于内存计算的 ? 5 | * 简述Executor如何内存分配 ? 6 | -------------------------------------------------------------------------------- /大数据/Spark/划分.md: -------------------------------------------------------------------------------- 1 | # 划分 2 | 3 | 1. 简述Spark的阶段划分流程 ? 4 | 2. Spark的stage如何划分?在源码中是怎么判断属于ShuGle Map Stage或Result Stage的 ? 5 | 3. 简述Spark的Job、Stage、Task分别介绍下,如何划分 ? 6 | 4. 简述为什么要划分Stage ? 7 | 5. 简述DAGScheduler如何划分? 8 | -------------------------------------------------------------------------------- /大数据/Spark/宽窄依赖.md: -------------------------------------------------------------------------------- 1 | # 宽窄依赖 2 | 3 | * 简述Spark join在什么情况下会变成窄依赖 ? 4 | * RDD的宽依赖和窄依赖,举例一些算子 ? 5 | * Spark SQL的GroupBy会造成窄依赖吗 ? 6 | * 简述Spark的宽依赖和窄依赖,为什么要这么划分 ? 7 | * 简述为什么要根据宽依赖划分Stage ? 8 | -------------------------------------------------------------------------------- /大数据/Spark/常见算子.md: -------------------------------------------------------------------------------- 1 | # 常见算子 2 | 3 | 1. 简述reduceByKey和groupByKey的区别和作用 ? 4 | 2. 简述reduceByKey和reduce的区别 ? 5 | 3. 简述使用reduceByKey出现数据倾斜怎么办 ? 6 | 4. 简述Spark的map和flatmap的区别 ? 7 | 5. 简述map和mapPartition的区别 ? 8 | 6. 简述GroupBy是行动算子吗 ? 9 | 7. 简述Spark join的分类 ? 10 | 8. 简述Spark map join的实现原理 ? 11 | -------------------------------------------------------------------------------- /安全/架构安全性.md: -------------------------------------------------------------------------------- 1 | # 架构安全性 2 | 3 | 凤凰架构 4 | 5 | - [**认证**](https://jingyecn.top:18080/architect-perspective/general-architecture/system-security/authentication)(Authentication):系统如何正确分辨出操作用户的真实身份? 6 | - [**授权**](https://jingyecn.top:18080/architect-perspective/general-architecture/system-security/authorization)( Authorization):系统如何控制一个用户该看到哪些数据、能操作哪些功能? 7 | - [**凭证**](https://jingyecn.top:18080/architect-perspective/general-architecture/system-security/credentials)(Credential):系统如何保证它与用户之间的承诺是双方当时真实意图的体现,是准确、完整且不可抵赖的? 8 | - [**保密**](https://jingyecn.top:18080/architect-perspective/general-architecture/system-security/confidentiality)(Confidentiality):系统如何保证敏感数据无法被包括系统管理员在内的内外部人员所窃取、滥用? 9 | - [**传输**](https://jingyecn.top:18080/architect-perspective/general-architecture/system-security/transport-security)(Transport Security):系统如何保证通过网络传输的信息无法被第三方窃听、篡改和冒充? 10 | - [**验证**](https://jingyecn.top:18080/architect-perspective/general-architecture/system-security/verification)(Verification):系统如何确保提交到每项服务中的数据是合乎规则的,不会对系统稳定性、数据一致性、正确性产生风险? -------------------------------------------------------------------------------- /工程/SSH.md: -------------------------------------------------------------------------------- 1 | # SSH 2 | 3 | ## **1. 初见 SSH** 4 | 5 | SSH是一种协议标准,用于在网络主机之间进行加密的一种协议,其目的是实现**安全远程登录**以及其它**安全网络服务**。 6 | 7 | > SSH 仅仅是一**协议标准**,其具体的实现有很多,既有开源实现的 OpenSSH,也有商业实现方案。使用范围最广泛的当然是开源实现 OpenSSH。 8 | 9 | 为什么要搞这么个协议呢?其实,很久很久以前,互联网通信都是明文的,一旦在中间环节被某些中间商截获了,我们的通信内容就暴漏无疑。 10 | 11 | 所以芬兰就有这么一位叫做 Tatu Ylonen 的人设计了 SSH 协议,将信息加密,这样就像上面说的,即使我们的登陆信息在中间被人截获了,我们的密码也不会被泄露。 12 | 13 | 目前 SSH 协议已经在全世界广泛被使用,且已经在成为各个 Linux 发行版的标配。 14 | 15 | ## **2. SSH 工作原理** 16 | 17 | 在讨论 SSH 的原理和使用前,我们需要分析一个问题:**为什么需要 SSH?** 18 | 19 | 从 1.1 节 SSH 的定义中可以看出,SSH 和 Telnet、FTP 等协议主要的区别在于**安全性**。这就引出下一个问题:**如何实现数据的安全呢?**首先想到的实现方案肯定是对数据进行**加密**。加密的方式主要有两种: 20 | 21 | 1. 对称加密(也称为秘钥加密) 22 | 2. 非对称加密(也称公钥加密) 23 | 24 | 所谓对称加密,指加密解密使用同一套秘钥。如下图所示: 25 | 26 | ![img](https://ask.qcloudimg.com/http-save/yehe-5449090/vkxl8q2ur1.png) 27 | 28 | 图1-1:对称加密-Client端 29 | 30 | ![img](https://ask.qcloudimg.com/http-save/yehe-5449090/pja5so5q72.png) 31 | 32 | 图1-2:对称加密-Server端 33 | 34 | 对称加密的加密强度高,很难破解。但是在实际应用过程中不得不面临一个棘手的问题:**如何安全的保存密钥呢?** 35 | 36 | 尤其是考虑到数量庞大的 Client 端,很难保证密钥不被泄露。一旦一个 Client端的密钥被窃取,那么整个系统的安全性也就不复存在。为了解决这个问题,**非对称加密**应运而生。非对称加密有两个密钥:**“公钥”**和**“私钥”**。 37 | 38 | > 两个密钥的特性:公钥加密后的密文,只能通过对应的私钥进行解密。而通过公钥推理出私钥的可能性微乎其微。 39 | 40 | 下面看下使用非对称加密方案的登录流程: 41 | 42 | ![img](https://ask.qcloudimg.com/http-save/yehe-5449090/q4vanjgmyc.jpeg) 43 | 44 | 图1-3:非对称加密登录流程 45 | 46 | 1. 远程 Server 收到 Client 端用户 TopGun 的登录请求,Server 把自己的公钥发给用户。 47 | 2. Client 使用这个公钥,将密码进行加密。 48 | 3. Client 将加密的密码发送给 Server端。 49 | 4. 远程 Server 用自己的私钥,解密登录密码,然后验证其合法性。 50 | 5. 若验证结果,给 Client 相应的响应。 51 | 52 | > 私钥是 Server 端独有,这就保证了 Client 的登录信息即使在网络传输过程中被窃据,也没有私钥进行解密,保证了数据的安全性,这充分利用了非对称加密的特性。 53 | 54 | ### 这样就一定安全了吗? 55 | 56 | 上述流程会有一个问题:**Client 端如何保证接受到的公钥就是目标 Server 端的?** 如果一个攻击者中途拦截 Client 的登录请求,向其发送自己的公钥,Client 端用攻击者的公钥进行[数据加密](https://cloud.tencent.com/solution/domesticencryption?from=10680)。攻击者接收到加密信息后再用自己的私钥进行解密,不就窃取了 Client 的登录信息了吗?这就是所谓的中间人攻击。 57 | 58 | ![img](https://ask.qcloudimg.com/http-save/yehe-5449090/rcwv2g4nqc.jpeg) 59 | 60 | 图1-4:中间人攻击 61 | 62 | ### **2.1 SSH 中是如何解决这个问题的?** 63 | 64 | #### **2.1.1 基于口令的认证** 65 | 66 | 从上面的描述可以看出,问题就在于**如何对 Server 的公钥进行认证?**在 https中可以通过 CA 来进行公证,可是 SSH 的 **Publish key**和 **Private key** 都是自己生成的,没法公证。 67 | 68 | 只能通过 Client 端自己对公钥进行确认。通常在第一次登录的时候,系统会出现下面提示信息: 69 | 70 | ```javascript 71 | The authenticity of host 'ssh-server.example.com (12.18.429.21)' can't be established. 72 | RSA key fingerprint is 98:2e:d7:e0:de:9f:ac:67:28:c2:42:2d:37:16:58:4d. 73 | Are you sure you want to continue connecting (yes/no)? 74 | ``` 75 | 76 | 上面的信息说的是:无法确认主机 ssh-server.example.com(12.18.429.21)的真实性,不过知道它的公钥指纹,是否继续连接? 77 | 78 | > 之所以用 fingerprint 代替 key,主要是 key 过于长(RSA 算法生成的公钥有1024 位),很难直接比较。所以,对公钥进行 Hash 生成一个 128 位的指纹,这样就方便比较了。 79 | 80 | 如果输入 **yes** 后,会出现下面信息: 81 | 82 | ```javascript 83 | Warning: Permanently added 'ssh-server.example.com,12.18.429.21' (RSA) to the list of known hosts. 84 | Password: (enter password) 85 | ``` 86 | 87 | 该 host 已被确认,并被追加到文件 **known_hosts** 中,然后就需要输入密码,之后的流程就按照图 1-3 进行。 88 | 89 | #### **2.1.2 基于公钥认证** 90 | 91 | 在上面介绍的登录流程中可以发现,每次登录都需要输入密码,很麻烦。SSH 提供了另外一种可以免去输入密码过程的登录方式:公钥登录。流程如下: 92 | 93 | ![img](https://ask.qcloudimg.com/http-save/yehe-5449090/avi6hg6iib.jpeg) 94 | 95 | 图1-5:公钥认证流程 96 | 97 | 1. Client 将自己的公钥存放在 Server 上,追加在文件 authorized_keys 中。注意:Client 端的 Public key 是 Client 手动 Copy 到 Server端的,SSH 建立连接过程中没有公钥的交换操作。 98 | 2. Server 端接收到 Client 的连接请求后,会在 authorized_keys 中匹配到 Client 的公钥 pubKey,并生成随机数 R,用 Client 的公钥对该随机数进行加密得到 pubKey(R),然后将加密后信息发送给 Client。 99 | 3. Client 端通过私钥进行解密得到随机数 R,然后对随机数 R 和本次会话的 SessionKey 利用 MD5 生成摘要 Digest1,发送给 Server 端。 100 | 4. Server 端会也会对 R 和 SessionKey 利用同样摘要算法生成 Digest2。 101 | 5. Server 端会最后比较 Digest1 和 Digest2 是否相同,完成认证过程。 102 | 103 | 注意:在步骤1中,Client 将自己的公钥存放在 Server 上。需要用户手动将公钥 Copy 到 Server 上。这就是在配置 SSH 的时候进程进行的操作。下图是 GitHub 上 SSH keys 设置视图: 104 | 105 | ![img](https://ask.qcloudimg.com/http-save/yehe-5449090/48zrpjs5x0.jpeg) 106 | 107 | GitHub 中 SSH keys 设置 108 | 109 | 在步骤 2 中,Server 端根据什么信息在 authorized_keys 中进行查找的呢?主要是根据 Client 在认证的开始会发送一个 KeyID 给 Server,这个 KeyID 会唯一对应该 Client 的一个 PublicKey,Server 就是通过该 KeyID 在 authorized_keys 进行查找对应的 PublicKey。 110 | 111 | ## **3. SSH 实践** 112 | 113 | ### **3.1 生成密钥操作** 114 | 115 | 经过上面的原理分析,下面三行命令的含义应该很容易理解了: 116 | 117 | ```javascript 118 | ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa 119 | cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys 120 | chmod 0600 ~/.ssh/authorized_keys 121 | ``` 122 | 123 | ssh-keygen 是用于生产密钥的工具。 124 | 125 | - -t:指定生成密钥类型(rsa、dsa、ecdsa 等) 126 | - -P:指定 passphrase,用于确保私钥的安全 127 | - -f:指定存放密钥的文件(公钥文件默认和私钥同目录下,不同的是存放公钥的文件名需要加上后缀 .pub) 128 | 129 | 首先看下面 ~/.ssh 中的四个文件: 130 | 131 | ![img](https://ask.qcloudimg.com/http-save/yehe-5449090/vd8f93e91b.jpeg) 132 | 133 | SSH 涉及文件 134 | 135 | 1. id_rsa:保存私钥 136 | 2. id_rsa.pub:保存公钥 137 | 3. authorized_keys:保存已授权的客户端公钥 138 | 4. known_hosts:保存已认证的远程主机 ID(关于 known_hosts 详情,见文末更新内容) 139 | 140 | 四个角色的关系如下图所示: 141 | 142 | ![img](https://ask.qcloudimg.com/http-save/yehe-5449090/1yvqjsvcoi.jpeg) 143 | 144 | SSH 结构简图 145 | 146 | > 需要注意的是:一台主机可能既是 Client,也是 Server。所以会同时拥有authorized_keys 和 known_hosts。 147 | 148 | ### **3.2 登录操作** 149 | 150 | ```javascript 151 | # 以用户名user,登录远程主机host 152 | $ ssh user@host 153 | 154 | # 本地用户和远程用户相同,则用户名可省去 155 | $ ssh host 156 | 157 | # SSH默认端口22,可以用参数p修改端口 158 | $ ssh -p 2017 user@host 159 | ``` 160 | 161 | ## **4. 其它一些补充** 162 | 163 | 下面关于 SSH 的 **known_hosts** 机制的一些补充。 164 | 165 | ### **4.1 known_hosts 中存储的内容是什么?** 166 | 167 | known_hosts 中存储是已认证的远程主机 host key,每个 SSH Server 都有一个 **secret, unique ID, called a host key**。 168 | 169 | ### **4.2 host key 何时加入 known_hosts 的?** 170 | 171 | 当我们第一次通过 SSH 登录远程主机的时候,Client 端会有如下提示: 172 | 173 | ```javascript 174 | Host key not found from the list of known hosts. 175 | Are you sure you want to continue connecting (yes/no)? 176 | ``` 177 | 178 | 此时,如果我们选择 **yes**,那么该 host key 就会被加入到 Client 的known_hosts 中,格式如下: 179 | 180 | ```javascript 181 | # domain name+encryption algorithm+host key 182 | example.hostname.com ssh-rsa AAAAB4NzaC1yc2EAAAABIwAAAQEA... 183 | ``` 184 | 185 | ### **4.3 为什么需要 known_hosts?** 186 | 187 | 最后探讨下为什么需要 known_hosts,这个文件主要是通过 Client 和 Server的双向认证,从而避免中间人(**man-in-the-middle attack**)攻击,每次Client 向 Server 发起连接的时候,不仅仅 Server 要验证 Client 的合法性,Client 同样也需要验证 Server 的身份,SSH Client 就是通过 known_hosts 中的 host key 来验证 Server 的身份的。 188 | 189 | > 这种方案足够安全吗?当然不,比如第一次连接一个未知 Server 的时候,known_hosts 还没有该 Server 的 host key,这不也可能遭到**中间人**攻击吗?这可能只是安全性和可操作性之间的折中吧。 190 | 191 | ## SSH 如何配置 192 | 193 | ### 连接方式 194 | 195 | 基本的ssh连接方法是 196 | 197 | ```bash 198 | ssh username@ip 199 | ``` 200 | 201 | `username`表示该机器的用户名,`ip`表示对应的ip地址。比如,笔者在`10.22.75.212`的用户名是`qiangzibro`,只需要在终端输入 202 | 203 | ```bash 204 | ssh qiangzibro@10.22.75.212 205 | ``` 206 | 207 | 接下来,终端会提示你一条信息,输入yes回车,会提示你输入密码,就像这样。 208 | 209 | ![img](https://pic3.zhimg.com/v2-bbc1298be24fdfb6a63775e751f21a3a_b.png) 210 | 211 | 先别着急使用下去,稍加配置可以让我们使用得更加舒心、安全。 212 | 213 | ### 基本配置 214 | 215 | #### 给ip地址取别名 216 | 217 | 长长的ip地址不好记,给ip地址取个别名吧!在mac或者linux上,可以编辑`/etc/hosts`这个文件,由于是系统文件,需要使用管理员权限。编辑器可以自选,笔者使用的是`neovim` 218 | 219 | ```bash 220 | sudo nvim /etc/hosts 221 | ``` 222 | 223 | 比如我想给`10.22.75.177`取名叫`lab1`,添加下面一行: 224 | 225 | ```bash 226 | 10.22.75.177 lab1 227 | ``` 228 | 229 | 在以后的使用中,凡是需要用到ip地址,可以直接用别名代理,比如 230 | 231 | ```bash 232 | ping lab1 233 | ``` 234 | 235 | #### 给特定主机上的用户取别名 236 | 237 | ip地址取完别名后,我们可以使用类似 238 | 239 | ```bash 240 | ssh qiangzibro@lab1 241 | ``` 242 | 243 | 的方式进行连接,实际上,这样的连接方式还可以进一步简化。 244 | 245 | 也就是说,给`qiangzibro@lab1`也取个别名 246 | 247 | 不建议自己造轮子,早年间笔者曾写过比如`alias sshqiang="ssh qiangzibro@lab1”`的别名,其实没有太大别要,因为进行下面配置可以让以后使用更方便: 248 | 249 | 类unix系统(mac或者linux)可以直接编辑`~/.ssh/config`这个文件,如果没有,自己创建一个。语法如下 250 | 251 | ```bash 252 | Host l1 253 | HostName lab1 254 | Port 22 255 | User qiangzibro 256 | ``` 257 | 258 | 配置很简单,四行分别表示别名、远程主机ip、远程主机ssh端口、远程主机用户名。然后我们可以用 259 | 260 | ```bash 261 | ssh l1 262 | ``` 263 | 264 | 进行连接。 265 | 266 | #### 免密码登录 267 | 268 | 经常使用密码登录,一个问题是有安全风险,另外一个是麻烦,太懒了啊,每次输密码多麻烦!ssh还提供一种使用密匙验证的方式进行登录,相信大家在配置github免密登录时也遇到过。百度百科上对其解释如下: 269 | 270 | > 原理是你必须为自己创建一对密匙,并把公用密匙放在需要访问的服务器上。如果你要连接到SSH服务器上,客户端软件就会向服务器发出请求,请求用你的密匙进行安全验证。服务器收到请求之后,先在该服务器上你的主目录下寻找你的公用密匙,然后把它和你发送过来的公用密匙进行比较。如果两个密匙一致,服务器就用公用密匙加密“质询”(challenge)并把它发送给客户端软件。客户端软件收到“质询”之后就可以用你的私人密匙解密再把它发送给服务器。 271 | 272 | 也就是说,把本地公钥拷贝到远程服务器上,就不需要每次登录使用密码了。具体讲,是把本地`~/.ssh/id_rsa.pub`内的内容拷贝到远程`~/.ssh/authorized_keys`文件里。首先看看本地有没有公钥: 273 | 274 | ```text 275 | cat ~/.ssh/id_rsa.pub 276 | ``` 277 | 278 | 没有,则生成一个 279 | 280 | ```text 281 | ssh-keygen -t rsa 282 | ``` 283 | 284 | 一路回车按下去,便生成在了`~/.ssh/id_rsa.pub` 285 | 286 | 你可以使用复制粘贴最原始的方法,而这种操作也有命令简化了: 287 | 288 | ```bash 289 | ssh-copy-id l1 # 将本地公钥拷贝到远程名为l1用户下,也就是/home/qiangzibro/.ssh/authorized_keys里 290 | ``` 291 | -------------------------------------------------------------------------------- /开源实习.md: -------------------------------------------------------------------------------- 1 | # GSoc 2 | 3 | https://summerofcode.withgoogle.com/ 4 | 5 | ## 时间表 6 | 7 | 2023 具体时间表 8 | 9 | https://developers.google.com/open-source/gsoc/timeline 10 | 11 | | 阶段 | 开始时间 | 截至时间 | 12 | | ------------------------------------------------------------ | ----------------------------------- | -------------------------- | 13 | | 指导组织可以开始向谷歌提交申请 | 1 月 23 日 - 18:00 | | 14 | | 导师组织申请截止日期 | 2 月 7 日 - 下午 6:00 UTC | | 15 | | Google 计划管理员审核组织申请 | 2 月 7 日 | 2 月 21 日 | 16 | | **公布的接受指导组织名单** | 世界标准时间 2 月 22 日 - 下午 6:00 | | 17 | | 潜在的 GSoC 贡献者与指导组织讨论应用想法 | 2 月 22 日 | 3 月19 日 | 18 | | **GSoC 贡献者申请期** | 3 月 20 日 - 18:00 | 4 月 4 日 - 18:00 UTC | 19 | | 来自组织管理员的 GSoC 贡献者提案排名 | 世界标准时间 4 月 27 日 - 18:00 | | 20 | | 宣布接受 GSoC 贡献者项目 | 5 月 4 日 - 下午 6:00 UTC | | 21 | | 社区结合期 。GSoC 贡献者结识导师,阅读文档,加快速度开始他们的项目 | 5 月 4 日 | 5 月 28 日 | 22 | | 编码正式开始! | 5月 29 日 | | 23 | | 导师和 GSoC 贡献者可以开始**提交中期评估** | 7 月 10 日 - 下午 6:00 UTC | 7 月 14 日 - 下午 6:00 UTC | 24 | | 工作时间 ,GSoC 贡献者在导师的指导下开展他们的项目 | 7 月 14 日 | 8月21日 | 25 | | 最后一周:GSoC 贡献者提交他们的最终工作产品和他们的最终导师评估(标准编码期) | 8 月 21 日 | 8 月 28 日 18:00 UTC | 26 | | 导师提交最终的 GSoC 贡献者评估(标准编码期) | 8 月 28 日 | 9 月 4 日 - 18:00 UTC | 27 | | Google Summer of Code 2023 公布初步结果 | 9月5日 | | 28 | | **具有延长时间表的** GSoC 贡献者继续编码 | 9月4日 | 11月6日 | 29 | | 所有 GSoC 贡献者提交最终工作产品和最终评估的最后日期 | 11月6日18:00 UTC | | 30 | | 导师提交 GSoC 贡献者项目评估的最后日期 | 11 月 13 日 - 18:00 UTC | | 31 | 32 | 33 | 34 | ## 关键时间点 35 | 36 | 2 月 22 日公布项目,开始找合适的,大胆冲啊!!! 37 | 38 | 39 | 40 | 下面是大佬的经验: 41 | 42 | 3 月 20 日开始申请,4 月 4 日结束,结束之前要提交一份 Proposals 43 | 44 | **每一份 Proposal 要包含你对其中一个 Idea 的设想、实施方案、时间规划等,Proposal 需要写的具体内容因组织者的要求而异。** 45 | 46 | 此后由组织负责审阅所有学生提交的 Proposals,选出他们所认为优秀的。在约一个月之后,Google 会宣布被选中的学生列表,如果你顺利通过,便可以开始当年的 GSoC 之旅了! 47 | 48 | 49 | 申请参考范文 50 | 51 | [Optimization of Distance Between Methods in Single Java Class](https://link.zhihu.com/?target=https%3A//docs.google.com/document/d/1lWXpWhUN6cE06sjQANjWxamc_X3ddbSphTRSofChLyk/edit%3Fusp%3Dsharing) 52 | 53 | [Regression Testing Tool and HTML Report Generator for Pull Request](https://link.zhihu.com/?target=https%3A//docs.google.com/document/d/1xu6SE4qeKTRQ45R9FSLOQB-t5ExzBGyvU9FLGifvxY0/edit%3Fusp%3Dsharing)。 54 | 55 | 除了一份质量过硬的 Proposal 之外,在宣布最终名单之前和导师们的套磁也是非常重要的!多在 Mailing List 中刷刷脸,积极参与讨论。**如果能够提出 Bug 甚至修复 Bug 或者实现新的 Feature 那是最好的。Checkstyle 的导师在最开始就清楚的写到学生的 Fixed Issue 数是很重要的指标。后来在 deadline 之前我足足交了 8 个 PR 位列十多个竞争者第一,这点也为我最终被选中加了不少分。** 56 | 57 | 58 | 59 | 60 | 61 | # OSPP 62 | 63 | https://summer-ospp.ac.cn/#/org 64 | 65 | 社区报名时间:3月28日-4月15日 66 | 社区上线项目时间:4月21日-5月10日 67 | 68 | 69 | 70 | # Linux 开源基金会 71 | 72 | [Mentees - Linux Foundation Documentation](https://docs.linuxfoundation.org/lfx/mentorship/mentees) 73 | 74 | ## 春季实习 3 月 1 日 - 5 月 31 日 75 | 76 | **Spring Term: March 1st - May 31st** 77 | 78 | - Mentorships available on LFX Mentorship: mid-January(1 月中旬项目上线) 79 | - Mentee applications open on LFX: approximately 4 weeks(项目申请:实习开始前 4-6 周左右) 80 | - Mentee application review and acceptance: approximately during the 2 weeks before the term begins.(申请评估和录取:实习开始前 2 周左右) -------------------------------------------------------------------------------- /操作系统/其他.md: -------------------------------------------------------------------------------- 1 | # 其他 2 | 3 | ## 问题 4 | 5 | * 谈谈用户态内核态 6 | 7 | * 谈谈宏内核和微内核 8 | 9 | * Linux 内核 vs Windows 内核 10 | 11 | * ubuntu开机的时候系统做了什么? 12 | 13 | * 什么是实时操作系统 14 | 15 | * 实模式和保护模式区别 16 | 17 | ## 回答 18 | 19 | ### 谈谈用户态内核态 20 | 21 | **为什么要分内核态和用户态** 22 | 23 | 为了安全性。在cpu的一些指令中,有的指令如果用错,将会导致整个系统崩溃。分了内核态和用户态后,当用户需要操作这些指令时候,内核为其提供了API,可以通过系统调用陷入内核,让内核去执行这些操作。 24 | 25 | **用户态和内核态区别** 26 | 27 | 用户态和内核态是操作系统的两种运行级别,两者最大的区别就是**特权级不同**。用户态拥有最低的特权级,内核态拥有较高的特权级。运行在用户态的程序不能直接访问操作系统内核数据结构和程序。内核态和用户态之间的转换方式主要包括:**系统调用,异常和中断。** 28 | 29 | **请你来说一说用户态到内核态的转化原理** 30 | 31 | 1)用户态切换到内核态的3种方式 32 | 33 | 1. 系统调用 34 | 35 | 这是用户进程主动要求切换到内核态的一种方式,用户进程通过系统调用申请操作系统提供的服务程序完成工作。而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的ine 80h中断。系统调用是操作系统的最小功能单位,是操作系统提供的用户接口,系统调用本身是一种**软中断**。 36 | 37 | 2. 异常 38 | 39 | 异常,也叫做内中断。当CPU在执行运行在用户态的程序时,发现了某些事件不可知的异常,这是会触发由当前运行进程切换到处理此。异常的内核相关程序中,也就到了内核态,比如**缺页异常**。 40 | 41 | 3. 外围设备的中断 42 | 43 | 硬中断,当外围设备完成用户请求的操作之后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条将要执行的指令,转而去执行中断信号的处理程序,如果先执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了有用户态到内核态的切换。比**如硬盘读写操作完成**,系统会切换到硬盘读写的中断处理程序中执行后续操作等。 44 | 45 | 2)切换操作 46 | 47 | 从出发方式看,可以在认为存在前述3种不同的类型,但是从最终实际完成由用户态到内核态的切换操作上来说,涉及的关键步骤是完全一样的,没有任何区别,都相当于执行了一个中断响应的过程,**因为系统调用实际上最终是中断机制实现的**,而异常和中断处理机制基本上是一样的,用户态切换到内核态的步骤主要包括: 48 | 49 | 1. 从当前进程的描述符中提取其内核栈的ss0及esp0信息。 50 | 51 | 2. **使用ss0和esp0指向的内核栈将当前进程的cs,eip,eflags,ss,esp信息保存起来**,这个过程也完成了**由用户栈找到内核栈**的切换过程,同时保存了被暂停执行的程序的下一条指令。 52 | 53 | 3. 将先前由中断向量检索得到的中断处理程序的cs,eip信息装入相应的寄存器,开始执行**中断处理程序**,这时就转到了内核态的程序执行了。 54 | 55 | ### 谈谈宏内核和微内核 56 | 57 | **宏内核:** 58 | 59 | 除了最基本的进程、线程管理、内存管理外,将文件系统,驱动,网络协议等等都集成在内核里面,例如linux内核。 60 | 优点:效率高。 61 | 62 | 缺点:稳定性差,开发过程中的bug经常会导致整个系统挂掉。 63 | 64 | **微内核:** 65 | 66 | 内核中只有最基本的调度、内存管理。驱动、文件系统等都是用户态的守护进程去实现的。 67 | 68 | 优点:稳定,驱动等的错误只会导致相应进程死掉,不会导致整个系统都崩溃 69 | 70 | 缺点:效率低。典型代表QNX,QNX的文件系统是跑在用户态的进程,称为resmgr的东西,是订阅发布机制,文件系统的错误只会导致这个守护进程挂掉。不过数据吞吐量就比较不乐观了。 71 | 72 | ### Linux 内核 vs Windows 内核 73 | 74 | 操作系统核心的东西就是内核,这次我们就来看看,**Linux 内核和 Windows 内核有什么区别?** 75 | 76 | ------ 77 | 78 | #### 内核 79 | 80 | 什么是内核呢? 81 | 82 | 计算机是由各种外部硬件设备组成的,比如内存、cpu、硬盘等,如果每个应用都要和这些硬件设备对接通信协议,那这样太累了,所以这个中间人就由内核来负责,**让内核作为应用连接硬件设备的桥梁**,应用程序只需关心与内核交互,不用关心硬件的细节。 83 | 84 | ![内核](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost4@main/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E5%86%85%E6%A0%B8/Kernel_Layout.png) 85 | 86 | 内核有哪些能力呢? 87 | 88 | 现代操作系统,内核一般会提供 4 个基本能力: 89 | 90 | * 管理进程、线程,决定哪个进程、线程使用 CPU,也就是进程调度的能力; 91 | * 管理内存,决定内存的分配和回收,也就是内存管理的能力; 92 | * 管理硬件设备,为进程与硬件设备之间提供通信能力,也就是硬件通信能力; 93 | * 提供系统调用,如果应用程序要运行更高权限运行的服务,那么就需要有系统调用,它是用户程序与操作系统之间的接口。 94 | 95 | 内核是怎么工作的? 96 | 97 | 内核具有很高的权限,可以控制 cpu、内存、硬盘等硬件,而应用程序具有的权限很小,因此大多数操作系统,把内存分成了两个区域: 98 | 99 | * 内核空间,这个内存空间只有内核程序可以访问; 100 | * 用户空间,这个内存空间专门给应用程序使用; 101 | 102 | 用户空间的代码只能访问一个局部的内存空间,而内核空间的代码可以访问所有内存空间。因此,当程序使用用户空间时,我们常说该程序在**用户态**执行,而当程序使内核空间时,程序则在**内核态**执行。 103 | 104 | 应用程序如果需要进入内核空间,就需要通过系统调用,下面来看看系统调用的过程: 105 | 106 | ![img](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost4@main/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E5%86%85%E6%A0%B8/systemcall.png) 107 | 108 | 内核程序执行在内核态,用户程序执行在用户态。当应用程序使用系统调用时,会产生一个中断。发生中断后, CPU 会中断当前在执行的用户程序,转而跳转到中断处理程序,也就是开始执行内核程序。内核处理完后,主动触发中断,把 CPU 执行权限交回给用户程序,回到用户态继续工作。 109 | 110 | ------ 111 | 112 | #### Linux 的设计 113 | 114 | Linux 的开山始祖是来自一位名叫 Linus Torvalds 的芬兰小伙子,他在 1991 年用 C 语言写出了第一版的 Linux 操作系统,那年他 22 岁。 115 | 116 | 完成第一版 Linux 后,Linus Torvalds 就在网络上发布了 Linux 内核的源代码,每个人都可以免费下载和使用。 117 | 118 | Linux 内核设计的理念主要有这几个点: 119 | 120 | * *MultiTask*,多任务 121 | * *SMP*,对称多处理 122 | * *ELF*,可执行文件链接格式 123 | * *Monolithic Kernel*,宏内核 124 | 125 | **MultiTask** 126 | 127 | MultiTask 的意思是**多任务**,代表着 Linux 是一个多任务的操作系统。 128 | 129 | 多任务意味着可以有多个任务同时执行,这里的「同时」可以是并发或并行: 130 | 131 | * 对于单核 CPU 时,可以让每个任务执行一小段时间,时间到就切换另外一个任务,从宏观角度看,一段时间内执行了多个任务,这被称为并发。 132 | * 对于多核 CPU 时,多个任务可以同时被不同核心的 CPU 同时执行,这被称为并行。 133 | 134 | **SMP** 135 | 136 | SMP 的意思是**对称多处理**,代表着每个 CPU 的地位是相等的,对资源的使用权限也是相同的,多个 CPU 共享同一个内存,每个 CPU 都可以访问完整的内存和硬件资源。 137 | 138 | 这个特点决定了 Linux 操作系统不会有某个 CPU 单独服务应用程序或内核程序,而是每个程序都可以被分配到任意一个 CPU 上被执行。 139 | 140 | **ELF** 141 | 142 | ELF 的意思是**可执行文件链接格式**,它是 Linux 操作系统中可执行文件的存储格式,你可以从下图看到它的结构: 143 | 144 | ![ELF 文件格式](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost4@main/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E5%86%85%E6%A0%B8/Elf.png) 145 | 146 | ELF 把文件分成了一个个分段,每一个段都有自己的作用,具体每个段的作用这里我就不详细说明了,感兴趣的同学可以去看《程序员的自我修养——链接、装载和库》这本书。 147 | 148 | 另外,ELF 文件有两种索引,Program header table 中记录了「运行时」所需的段,而 Section header table 记录了二进制文件中各个「段的首地址」。 149 | 150 | 那 ELF 文件怎么生成的呢? 151 | 152 | 我们编写的代码,首先通过「编译器」编译成汇编代码,接着通过「汇编器」变成目标代码,也就是目标文件,最后通过「链接器」把多个目标文件以及调用的各种函数库链接起来,形成一个可执行文件,也就是 ELF 文件。 153 | 154 | 那 ELF 文件是怎么被执行的呢? 155 | 156 | 执行 ELF 文件的时候,会通过「装载器」把 ELF 文件装载到内存里,CPU 读取内存中的指令和数据,于是程序就被执行起来了。 157 | 158 | **Monolithic Kernel** 159 | 160 | Monolithic Kernel 的意思是**宏内核**,Linux 内核架构就是宏内核,意味着 Linux 的内核是一个完整的可执行程序,且拥有最高的权限。 161 | 162 | 宏内核的特征是系统内核的所有模块,比如进程调度、内存管理、文件系统、设备驱动等,都运行在内核态。 163 | 164 | 不过,Linux 也实现了动态加载内核模块的功能,例如大部分设备驱动是以可加载模块的形式存在的,与内核其他模块解藕,让驱动开发和驱动加载更为方便、灵活。 165 | 166 | ![分别为宏内核、微内核、混合内核的操作系统结构](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost4@main/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E5%86%85%E6%A0%B8/OS-structure2.png) 167 | 168 | 与宏内核相反的是**微内核**,微内核架构的内核只保留最基本的能力,比如进程调度、虚拟机内存、中断等,把一些应用放到了用户空间,比如驱动程序、文件系统等。这样服务与服务之间是隔离的,单个服务出现故障或者完全攻击,也不会导致整个操作系统挂掉,提高了操作系统的稳定性和可靠性。 169 | 170 | 微内核内核功能少,可移植性高,相比宏内核有一点不好的地方在于,由于驱动程序不在内核中,而且驱动程序一般会频繁调用底层能力的,于是驱动和硬件设备交互就需要频繁切换到内核态,这样会带来性能损耗。华为的鸿蒙操作系统的内核架构就是微内核。 171 | 172 | 还有一种内核叫**混合类型内核**,它的架构有点像微内核,内核里面会有一个最小版本的内核,然后其他模块会在这个基础上搭建,然后实现的时候会跟宏内核类似,也就是把整个内核做成一个完整的程序,大部分服务都在内核中,这就像是宏内核的方式包裹着一个微内核。 173 | 174 | ------ 175 | 176 | #### Windows 设计 177 | 178 | 当今 Windows 7、Windows 10 使用的内核叫 Windows NT,NT 全称叫 New Technology。 179 | 180 | 下图是 Windows NT 的结构图片: 181 | 182 | ![Windows NT 的结构](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost4@main/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E5%86%85%E6%A0%B8/windowNT.png) 183 | 184 | Windows 和 Linux 一样,同样支持 MultiTask 和 SMP,但不同的是,**Window 的内核设计是混合型内核**,在上图你可以看到内核中有一个 *MicroKernel* 模块,这个就是最小版本的内核,而整个内核实现是一个完整的程序,含有非常多模块。 185 | 186 | Windows 的可执行文件的格式与 Linux 也不同,所以这两个系统的可执行文件是不可以在对方上运行的。 187 | 188 | Windows 的可执行文件格式叫 PE,称为**可移植执行文件**,扩展名通常是`.exe`、`.dll`、`.sys`等。 189 | 190 | PE 的结构你可以从下图中看到,它与 ELF 结构有一点相似。 191 | 192 | ![PE 文件结构](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost4@main/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E5%86%85%E6%A0%B8/pe.png) 193 | 194 | ------ 195 | 196 | #### 总结 197 | 198 | 对于内核的架构一般有这三种类型: 199 | 200 | * 宏内核,包含多个模块,整个内核像一个完整的程序; 201 | * 微内核,有一个最小版本的内核,一些模块和服务则由用户态管理; 202 | * 混合内核,是宏内核和微内核的结合体,内核中抽象出了微内核的概念,也就是内核中会有一个小型的内核,其他模块就在这个基础上搭建,整个内核是个完整的程序; 203 | 204 | Linux 的内核设计是采用了宏内核,Window 的内核设计则是采用了混合内核。 205 | 206 | 这两个操作系统的可执行文件格式也不一样, Linux 可执行文件格式叫作 ELF,Windows 可执行文件格式叫作 PE。 207 | 208 | ### ubuntu开机的时候系统做了什么 209 | 210 | 1. 加载 BIOS(基本输入输出系统) 211 | 1. 硬件自检 212 | 2. 把权限交给启动程序 213 | 2. 读取 MBR(主引导记录),MBR 在存储设备最前面,MBR 包含 214 | * 调用操作系统的机器码 215 | * 分区表 216 | * 主引导记录签名 217 | 3. 运行 BootLoader 218 | * 初始化硬件,建立内存空间映射图,调整软硬件环境 219 | * Boot Loader有若干种:grub、lilo、spfdisk 220 | 4. 加载内核:操作系统初始化 221 | 5. 用户层init依据inittab文件来设定运行等级 222 | 223 | 224 | 225 | ### 什么是实时操作系统 226 | 227 | 实时操作系统(Real-time operating system, RTOS),又称即时操作系统,它会按照排序运行、管理系统资源,并为开发应用程序提供一致的基础。 实时操作系统与一般的操作系统相比,最大的特色就是“实时性”,如果有一个任务需要执行,实时操作系统会马上(在较短时间内)执行该任务,不会有较长的延时。这种特性保证了各个任务的及时执行。 228 | 229 | ### 实模式和保护模式区别 230 | -------------------------------------------------------------------------------- /操作系统/常见机制.md: -------------------------------------------------------------------------------- 1 | # 常见机制 2 | 3 | ## 问题 4 | 5 | ### 系统调用 6 | 7 | * 系统调用是什么? 8 | * 系统调用流程? 9 | * 系统调用的函数栈帧存在哪里? 10 | * 系统调用的中断现场存在哪里? 11 | * 说一下系统调用,系统调用是怎么从用户到内核的,底层实现 12 | 13 | ### 中断与异常 14 | 15 | * 中断和异常的定义? 16 | * 中断的处理过程? 17 | * 什么是软中断和硬中断,有什么区别? 18 | * 系统里有哪些软中断? 19 | * 如何定位软中断 CPU 使用率过高的问题? 20 | 21 | ### 信号 22 | 23 | * 什么是信号 24 | * 如何编写正确且安全的信号处理函数? 25 | * kill -9 原理 26 | * SIGSEGV 信号讲讲 27 | * sigkill 讲讲 28 | 29 | ## 回答 30 | 31 | ## 系统调用 32 | 33 | * 系统调用是什么? 34 | * 系统调用流程? 35 | * 系统调用的函数栈帧存在哪里? 36 | * 系统调用的中断现场存在哪里? 37 | * 说一下系统调用,系统调用是怎么从用户到内核的,底层实现 38 | 39 | 40 | 41 | ### 中断与异常 42 | 43 | #### 中断和异常的定义? 44 | 45 | 先来看看什么是中断?**在计算机中,中断是系统用来响应硬件设备请求的一种机制,操作系统收到硬件的中断请求,会打断正在执行的进程,然后调用内核中的中断处理程序来响应请求。** 46 | 47 | 这样的解释可能过于学术了,容易云里雾里,我就举个生活中取外卖的例子。 48 | 49 | 小林中午搬完砖,肚子饿了,点了份白切鸡外卖,这次我带闪了,没有被某团大数据杀熟。虽然平台上会显示配送进度,但是我也不能一直傻傻地盯着呀,时间很宝贵,当然得去干别的事情,等外卖到了配送员会通过「电话」通知我,电话响了,我就会停下手中地事情,去拿外卖。 50 | 51 | 这里的打电话,其实就是对应计算机里的中断,没接到电话的时候,我可以做其他的事情,只有接到了电话,也就是发生中断,我才会停下当前的事情,去进行另一个事情,也就是拿外卖。 52 | 53 | 从这个例子,我们可以知道,中断是一种异步的事件处理机制,可以提高系统的并发处理能力。 54 | 55 | 操作系统收到了中断请求,会打断其他进程的运行,所以**中断请求的响应程序,也就是中断处理程序,要尽可能快的执行完,这样可以减少对正常进程运行调度地影响。** 56 | 57 | 而且,中断处理程序在响应中断时,可能还会「临时关闭中断」,这意味着,如果当前中断处理程序没有执行完之前,系统中其他的中断请求都无法被响应,也就说中断有可能会丢失,所以中断处理程序要短且快。 58 | 59 | 还是回到外卖的例子,小林到了晚上又点起了外卖,这次为了犒劳自己,共点了两份外卖,一份小龙虾和一份奶茶,并且是由不同地配送员来配送,那么问题来了,当第一份外卖送到时,配送员给我打了长长的电话,说了一些杂七杂八的事情,比如给个好评等等,但如果这时另一位配送员也想给我打电话。 60 | 61 | 很明显,这时第二位配送员因为我在通话中(相当于关闭了中断响应),自然就无法打通我的电话,他可能尝试了几次后就走掉了(相当于丢失了一次中断)。 62 | 63 | ------ 64 | 65 | #### 中断的处理过程? 66 | 67 | #### 什么是软中断和硬中断,有什么区别? 68 | 69 | 前面我们也提到了,中断请求的处理程序应该要短且快,这样才能减少对正常进程运行调度地影响,而且中断处理程序可能会暂时关闭中断,这时如果中断处理程序执行时间过长,可能在还未执行完中断处理程序前,会丢失当前其他设备的中断请求。 70 | 71 | 那 Linux 系统**为了解决中断处理程序执行过长和中断丢失的问题,将中断过程分成了两个阶段,分别是「上半部和下半部分」**。 72 | 73 | * **上半部用来快速处理中断**,一般会暂时关闭中断请求,主要负责处理跟硬件紧密相关或者时间敏感的事情。 74 | * **下半部用来延迟处理上半部未完成的工作**,一般以「内核线程」的方式运行。 75 | 76 | 前面的外卖例子,由于第一个配送员长时间跟我通话,则导致第二位配送员无法拨通我的电话,其实当我接到第一位配送员的电话,可以告诉配送员说我现在下楼,剩下的事情,等我们见面再说(上半部),然后就可以挂断电话,到楼下后,在拿外卖,以及跟配送员说其他的事情(下半部)。 77 | 78 | 这样,第一位配送员就不会占用我手机太多时间,当第二位配送员正好过来时,会有很大几率拨通我的电话。 79 | 80 | 再举一个计算机中的例子,常见的网卡接收网络包的例子。 81 | 82 | 网卡收到网络包后,通过 DMA 方式将接收到的数据写入内存,接着会通过**硬件中断**通知内核有新的数据到了,于是内核就会调用对应的中断处理程序来处理该事件,这个事件的处理也是会分成上半部和下半部。 83 | 84 | 上部分要做的事情很少,会先禁止网卡中断,避免频繁硬中断,而降低内核的工作效率。接着,内核会触发一个**软中断**,把一些处理比较耗时且复杂的事情,交给「软中断处理程序」去做,也就是中断的下半部,其主要是需要从内存中找到网络数据,再按照网络协议栈,对网络数据进行逐层解析和处理,最后把数据送给应用程序。 85 | 86 | 所以,中断处理程序的上部分和下半部可以理解为: 87 | 88 | * **上半部直接处理硬件请求,也就是硬中断**,主要是负责耗时短的工作,特点是快速执行; 89 | * **下半部是由内核触发,也就说软中断**,主要是负责上半部未完成的工作,通常都是耗时比较长的事情,特点是延迟执行; 90 | 91 | 还有一个**区别,硬中断(上半部)是会打断 CPU 正在执行的任务,然后立即执行中断处理程序,而软中断(下半部)是以内核线程的方式执行,并且每一个 CPU 都对应一个软中断内核线程,名字通常为「ksoftirqd/CPU 编号」**,比如 0 号 CPU 对应的软中断内核线程的名字是 `ksoftirqd/0` 92 | 93 | 不过,**软中断不只是包括硬件设备中断处理程序的下半部,一些内核自定义事件也属于软中断,比如内核调度等、RCU 锁(内核里常用的一种锁)等**。 94 | 95 | 软中断除了硬中断可以触发, 还有什么程序里面自己调用init指令去触发软中断,其他触发具体看下面软中断类型 96 | 97 | ------ 98 | 99 | #### 系统里有哪些软中断? 100 | 101 | 在 Linux 系统里,我们可以通过查看 `/proc/softirqs` 的 内容来知晓「软中断」的运行情况,以及 `/proc/interrupts` 的 内容来知晓「硬中断」的运行情况。 102 | 103 | 接下来,就来简单的解析下 `/proc/softirqs` 文件的内容,在我服务器上查看到的文件内容如下: 104 | 105 | ![img](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E8%BD%AF%E4%B8%AD%E6%96%AD/softirqs.png) 106 | 107 | 你可以看到,每一个 CPU 都有自己对应的不同类型软中断的**累计运行次数**,有 3 点需要注意下。 108 | 109 | **第一点,要注意第一列的内容,它是代表着软中断的类型**,在我的系统里,软中断包括了 10 个类型,分别对应不同的工作类型,比如 `NET_RX` 表示网络接收中断,`NET_TX` 表示网络发送中断、`TIMER` 表示定时中断、`RCU` 表示 RCU 锁中断、`SCHED` 表示内核调度中断。 110 | 111 | **第二点,要注意同一种类型的软中断在不同 CPU 的分布情况**,正常情况下,同一种中断在不同 CPU 上的累计次数相差不多,比如我的系统里,`NET_RX` 在 CPU0 、CPU1、CPU2、CPU3 上的中断次数基本是同一个数量级,相差不多。 112 | 113 | 第三点,这些数值是系统运行以来的累计中断次数,数值的大小没什么参考意义,但是系统的**中断次数的变化速率**才是我们要关注的,我们可以使用 `watch -d cat /proc/softirqs` 命令查看中断次数的变化速率。 114 | 115 | 前面提到过,软中断是以内核线程的方式执行的,我们可以用 `ps` 命令可以查看到,下面这个就是在我的服务器上查到软中断内核线程的结果: 116 | 117 | ![img](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E8%BD%AF%E4%B8%AD%E6%96%AD/ksoftirqd.png) 118 | 119 | 可以发现,内核线程的名字外面都有有中括号,这说明 ps 无法获取它们的命令行参数,所以一般来说,名字在中括号里的都可以认为是内核线程。 120 | 121 | 而且,你可以看到有 4 个 `ksoftirqd` 内核线程,这是因为我这台服务器的 CPU 是 4 核心的,每个 CPU 核心都对应着一个内核线程。 122 | 123 | #### 如何定位软中断 CPU 使用率过高的问题? 124 | 125 | 要想知道当前的系统的软中断情况,我们可以使用 `top` 命令查看,下面是一台服务器上的 top 的数据: 126 | 127 | ![img](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E8%BD%AF%E4%B8%AD%E6%96%AD/top_si.png) 128 | 129 | 上图中的黄色部分 `si`,就是 CPU 在软中断上的使用率,而且可以发现,每个 CPU 使用率都不高,两个 CPU 的使用率虽然只有 3% 和 4% 左右,但是都是用在软中断上了。 130 | 131 | 另外,也可以看到 CPU 使用率最高的进程也是软中断 `ksoftirqd`,因此可以认为此时系统的开销主要来源于软中断。 132 | 133 | 如果要知道是哪种软中断类型导致的,我们可以使用 `watch -d cat /proc/softirqs` 命令查看每个软中断类型的中断次数的变化速率。 134 | 135 | ![img](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E8%BD%AF%E4%B8%AD%E6%96%AD/watch.png) 136 | 137 | **一般对于网络 I/O 比较高的 Web 服务器,`NET_RX` 网络接收中断的变化速率相比其他中断类型快很多。** 138 | 139 | 如果发现 `NET_RX` 网络接收中断次数的变化速率过快,接下来就可以使用 `sar -n DEV` 查看网卡的网络包接收速率情况,然后分析是哪个网卡有大量的网络包进来。 140 | 141 | ![img](https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost3@main/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E8%BD%AF%E4%B8%AD%E6%96%AD/sar_dev.png) 142 | 143 | 接着,在通过 `tcpdump` 抓包,分析这些包的来源,如果是非法的地址,可以考虑加防火墙,如果是正常流量,则要考虑硬件升级等。 144 | 145 | ### 信号 146 | 147 | #### 什么是信号 148 | 149 | 一个信号就是一条小消息,它通知进程系统中发生了一个某种类型的事件。 Linux 系统上支持的30 种不同类型的信号。 每种信号类型都对应于某种系统事件。低层的硬件异常是由内核异常处理程序处理的, 150 | 正常情况下,对用户进程而言是不可见的。信号提供了一种机制,通知用户进程发生了这些异常。 151 | 152 | 1. 发送信号:内核通过更新目的进程上下文中的某个状态,发送(递送)一个信号给目的进程。发送信号可以有如下两种原因: 153 | 154 | * 内核检测到一个系统事件,比如除零错误或者子进程终止。 155 | * 一个进程调用了kill 函数, 显式地要求内核发送一个信号给目的进程。一个进程可以发送信号给它自己。 156 | 157 | 2. 接收信号:当目的进程被内核强迫以某种方式对信号的发送做出反应时,它就接收了信号。进程可以忽略这个信号,终止或者通过执行一个称为信号处理程序(signal handler)的用户层函数捕获这个信号。 158 | 159 | #### 如何编写正确且安全的信号处理函数? 160 | 161 | 1. 处理程序要尽可能简单。 避免麻烦的最好方法是保持处理程序尽可能小和简单。例如,处理程序可能只是简单地设置全局标志并立即返回;所有与接收信号相关的处理都由主程序执行,它周期性地检查(并重置)这个标志。 162 | 163 | 2. 在处理程序中只调用异步信号安全的函数。 所谓异步信号安全的函数(或简称安全的函数)能够被信号处理程序安全地调用,原因有二:**要么它是可重入的(例如只访问局部变量),要么它不能被信号处理程序中断。** 164 | 165 | 3. 保存和恢复errno。 许多Linux 异步信号安全的函数都会在出错返回时设置errno在处理程序中调用这样的函数可能会干扰主程序中其他依赖于分。解决方法是在进人处理程序时把errno 保存在一个局部变量中,在处理程序返回前恢复它。注意,只有在处理程序要返回时才有此必要。如果处理程序调用_exit终止该进程,那么就不需要这样做了。 166 | 167 | 4. 阻塞所有的信号,保护对共享全局数据结构的访问。 如果处理程序和主程序或其他处理程序共享一个全局数据结构,那么在访问(读或者写)该数据结构时,你的处理程序和主程序应该暂时阻塞所有的信号。这条规则的原因是从主程序访问一个数据结构d 通常需要一系列的指令,如果指令序列被访问d 的处理程序中断,那么处理程序可能会发现d 的状态不一致,得到不可预知的结果。在访问d时暂时阻塞信号保证了处理程序不会中断该指令序列。 168 | 169 | 5. 用volatile 声明全局变量。 考虑一个处理程序和一个main 函数,它们共享一个全局变量g 。处理程序更新g,main 周期性地读g, 对于一个优化编译器而言,main 中g的值看上去从来没有变化过,因此使用缓存在寄存器中g 的副本来满足对g 的每次引用是很安全的。如果这样,main 函数可能永远都无法看到处理程序更新过的值。可以用volatile 类型限定符来定义一个变量,告诉编译器不要缓存这个变量。例如:volatile 限定符强迫编译器毎次在代码中引用g时,都要从内存中读取g的 170 | 值。一般来说,和其他所有共享数据结构一样,应该暂时阻塞信号,保护每次对全局变量的访问。 171 | 172 | ```c 173 | volatile int g; 174 | ``` 175 | 176 | 6. 用`sig_atomic_t`声明标志。在常见的处理程序设计中,处理程序会写全局标志来记录收到了信号。 177 | 主程序周期性地读这个标志,响应信号,再清除该标志。对于通过这种方式来共享的标志,C 提供一种整型数据类型`sig_atomic_t`对它的读和写保证会是原子的(不可中断的)。 178 | 179 | 7. 信号的一个与直觉不符的方面是未处理的信号是不排队的。因为 pending 位向量中每种类型的信号只对应有一位,所以每种类型最多只能有一个未处理的信号。关键思想是如果存在一个未处理的信号就表明至少有一个信号到达了。 180 | 181 | #### kill -9 原理 182 | -------------------------------------------------------------------------------- /数据库/MySQL/SQL 练习.md: -------------------------------------------------------------------------------- 1 | # SQL 练习 2 | 3 | ## SQL 必知必会 50 题 4 | 5 | [牛客网在线编程_编程学习|练习题_数据结构|系统设计题库 (nowcoder.com)](https://www.nowcoder.com/exam/oj?page=1&tab=SQL篇&topicId=298) 6 | 7 | ### 检索数据 8 | 9 | #### **SQL60** **从 Customers 表中检索所有的 ID** 10 | 11 | ``` 12 | 13 | ``` 14 | 15 | 16 | 17 | ### 排序检索数据 18 | 19 | 20 | 21 | ### 过滤数据 22 | 23 | 24 | 25 | ### 高级数据过滤 26 | 27 | 28 | 29 | ### 用通配符进行过滤 30 | 31 | 32 | 33 | ### 创建计算字段 34 | 35 | 36 | 37 | ### 使用函数处理数据 38 | 39 | 40 | 41 | ### 汇总数据 42 | 43 | 44 | 45 | ### 分组数据 46 | 47 | 48 | 49 | ### 使用子查询 50 | 51 | 52 | 53 | ### 联结表 54 | 55 | 56 | 57 | ### 创建高级联结 58 | 59 | 60 | 61 | ### 组合查询 -------------------------------------------------------------------------------- /数据库/MySQL/主从同步.md: -------------------------------------------------------------------------------- 1 | # 主从同步(复制) 2 | 3 | [看一遍就了解:聊聊MySQL主从 - 掘金 (juejin.cn)](https://juejin.cn/post/7070290856967667742) 4 | 5 | [高频面试:如何解决MySQL主从复制延时问题 - 掘金 (juejin.cn)](https://juejin.cn/post/7218948376166514745#heading-13) 6 | 7 | ## 问题 8 | 9 | * 什么是数据库主从,有什么用 10 | * 数据库主从复制原理 11 | * 主主、主从、主备的区别 12 | * MySQL 是怎么保证主从一致的 13 | * 数据库主从延迟的原因与解决方案 14 | * 数据库高可用 15 | 16 | ## 回答 17 | 18 | ### 什么是数据库主从,有什么用 19 | 20 | **主从数据库**是什么意思呢,主是主库的意思,从是从库的意思。数据库主库对外提供读写的操作,从库对外提供读的操作。 21 | 22 | ![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/83abaffb40cc4420906892e8d9beeb44~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) 23 | 24 | **数据库为什么需要主从架构呢?** 25 | 26 | - 高可用,实时灾备,用于故障切换。比如主库挂了,可以切从库。 27 | - 读写分离,提供查询服务,减少主库压力,提升性能 28 | - 备份数据,避免影响业务。 29 | 30 | 31 | 32 | ### 数据库主从复制原理 33 | 34 | **主从复制原理**,简言之,分三步曲进行: 35 | 36 | - 主数据库有个`bin log`二进制文件,记录了所有增删改`SQL`语句。(binlog线程) 37 | - 从数据库把主数据库的`bin log`文件的`SQL` 语句复制到自己的中继日志 `relay log`(io线程) 38 | - 从数据库的`relay log`重做日志文件,再执行一次这些sql语句。(Sql执行线程) 39 | 40 | 详细的主从复制过程如图: 41 | 42 | ![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b28baac9e1b54d9a9e4b04ad648faeef~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) 43 | 44 | 上图主从复制过程分了五个步骤进行: 45 | 46 | 1. 主库的更新SQL(update、insert、delete)被写到binlog 47 | 2. 从库发起连接,连接到主库。 48 | 3. 此时主库创建一个`binlog dump thread`,把`bin log`的内容发送到从库。 49 | 4. 从库启动之后,创建一个`I/O`线程,读取主库传过来的`bin log`内容并写入到`relay log` 50 | 5. 从库还会创建一个SQL线程,从`relay log`里面读取内容,从`ExecMasterLog_Pos`位置开始执行读取到的更新事件,将更新内容写入到`slave`的db 51 | 52 | 53 | 54 | 55 | 56 | ### 主主、主从、主备的区别 57 | 58 | **数据库主主**:两台都是主数据库,同时对外提供读写操作。客户端访问任意一台。数据存在双向同步。 59 | 60 | ![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2cf239b2e8b04edca6cfa825111a0fb4~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) 61 | 62 | **数据库主从**:一台是主数据库,同时对外提供读写操作。一台是**从数据库,对外提供读的操作**。数据从主库同步到从库。 63 | 64 | ![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f92ccbfd2ed84ed8b0c99f2fb5d65805~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) 65 | 66 | **数据库主备**:一台是主数据库,同时对外提供读写操作。一台是**备库,只作为备份作用,不对外提供读写,主机挂了它就取而代之**。数据从主库同步到备库。 67 | 68 | ![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f1dd9d6e1bfe404398ac225e06d858ac~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) 69 | 70 | 从库和备库,就是slave库功能不同因此叫法才不一样而已。一般slave库都会对外提供读的功能的,因此,大家日常听得比较多就是**主从**。 71 | 72 | 73 | 74 | ### MySQL 是怎么保证主从一致的 75 | 76 | 我们学习数据库的**主从复制原理**后,了解到**从库**拿到并执行主库的binlog日志,就可以保持数据与主库一致了。这是为什么呢?哪些情况会导致不一致呢? 77 | 78 | #### 长链接 79 | 80 | 主库和从库在同步数据的过程中断怎么办呢,数据不就会丢失了嘛。因此主库与从库之间维持了一个**长链接**,主库内部有一个线程,专门服务于从库的这个**长链接**的。 81 | 82 | #### binlog格式 83 | 84 | binlog 日志有三种格式,分别是`statement,row和mixed`。 85 | 86 | 如果是`statement`格式,binlog记录的是**SQL的原文**,如果主库和从库选的索引不一致,可能会导致主库不一致。我们来分析一下。假设主库执行删除这个SQL(其中`a和create_time`都有索引)如下: 87 | 88 | ```sql 89 | delete from t where a > '666' and create_time<'2022-03-01' limit 1; 90 | ``` 91 | 92 | 我们知道,数据选择了`a`索引和选择`create_time`索引,最后`limit 1`出来的数据一般是不一样的。所以就会存在这种情况:在binlog = `statement`格式时,主库在执行这条SQL时,使用的是索引a,而从库在执行这条SQL时,使用了索引`create_time`。最后主从数据不一致了。 93 | 94 | **如何解决这个问题呢?** 95 | 96 | 可以把binlog格式修改为`row`。`row`格式的`binlog`日志,记录的不是**SQL原文**,而是两个`event:Table_map 和 Delete_rows`。Table_map event说明要操作的表,Delete_rows event用于定义要删除的行为,记录删除的具体行数。`row`格式的binlog记录的就是要删除的主键ID信息,因此不会出现主从不一致的问题。 97 | 98 | 但是如果SQL删除10万行数据,使用row格式就会很占空间的,10万条数据都在binlog里面,写binlog的时候也很耗IO。但是`statement`格式的binlog可能会导致数据不一致,因此设计MySQL的大叔想了一个折中的方案,`mixed`格式的binlog。所谓的mixed格式其实就是`row`和`statement`格式混合使用,当MySQL判断可能数据不一致时,就用`row`格式,否则使用就用`statement`格式。 99 | 100 | 101 | 102 | 103 | 104 | ### 数据库主从延迟的原因与解决方案 105 | 106 | #### 什么是主从延迟 107 | 108 | **主从延迟**是怎么定义的呢? 与主从数据同步相关的时间点有三个 109 | 110 | 1. 主库执行完一个事务,写入binlog,我们把这个时刻记为`T1`; 111 | 2. 主库同步数据给从库,从库接收完这个binlog的时刻,记录为`T2`; 112 | 3. 从库执行完这个事务,这个时刻记录为`T3`。 113 | 114 | 所谓主从延迟,其实就是指同一个事务,在从库执行完的时间和在主库执行完的时间差值,即`T3-T1`。 115 | 116 | #### 主从延迟的原因 117 | 118 | **哪些情况会导致主从延迟呢?** 119 | 120 | 1. 如果从库所在的机器比主库的**机器性能差,会导致主从延迟**,这种情况比较好解决,只需选择**主从库一样规格的机器**就好。 121 | 2. 如果**从库的压力大,也会导致主从延迟**。比如主库直接影响业务的,大家可能使用会**比较克制**,因此一般**查询都打到从库**了,结果导致从库查询消耗大量CPU,影响同步速度,最后导致主从延迟。这种情况的话,可以搞了**一主多从**的架构,即多接几个从库分摊读的压力。另外,还可以把binlog接入到Hadoop这类系统,让它们提供查询的能力。 122 | 3. **大事务也会导致主从延迟**。如果一个事务执行就要10分钟,那么主库执行完后,给到从库执行,最后这个事务可能就会导致从库延迟10分钟啦。日常开发中,我们为什么特别强调,不要一次性delete太多SQL,需要分批进行,其实也是为了避免大事务。另外,大表的DDL语句,也会导致大事务,大家日常开发关注一下哈。 123 | 4. **网络延迟**也会导致主从延迟,这种情况你只能优化你的网络啦,比如带宽20M升级到100M类似意思等。 124 | 5. 如果从**数据库过多**也会导致主从延迟,因此要避免复制的从节点数量过多。从库数据一般以3-5个为宜。 125 | 6. **低版本的MySQL**只支持单线程复制,如果主库并发高,来不及传送到从库,就会导致延迟。可以换用更高版本的Mysql,可以支持多线程复制。 126 | 127 | 128 | 129 | #### 主从延迟的解决方案 130 | 131 | 我们一般会把从库落后的时间作为一个重点的数据库指标做监控和报警,正常的时间是在毫秒级别,一旦落后的时间达到了秒级别就需要告警了。 132 | 133 | 解决该问题的方法,除了缩短主从延迟的时间,还有一些其它的方法,基本原理都是尽量不查询从库,具体解决方案如下: 134 | 135 | - **使用缓存**:我们在同步写数据库的同时,也把数据写到缓存,查询数据时,会先查询缓存,不过这种情况会带来 MySQL 和 Redis 数据一致性问题。 136 | - **查询主库**: 137 | - 直接查询主库,这种情况会给主库太大压力,不建议这种方式。 138 | - 在实际应用场景中,对于一些非常核心的场景,比如库存,支付订单等,需要直接查询主库,其它非核心场景,就不要去查主库了。 139 | 140 | - **数据冗余**:对于一些异步处理的场景,如果只扔数据 ID,消费数据时,需要查询从库,我们可以把数据全部都扔给消息队列,这样消费者就无需再查询从库。(这种情况应该不太能出现,数据转了一圈,MySQL 主从还没有同步好,直接去撕 DBA 吧) 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | ### 数据库高可用 149 | 150 | - 双机主备高可用 151 | - 一主一从 152 | - 一主多从 153 | - MariaDB同步多主机集群 154 | - 数据库中间件 155 | 156 | #### 双机主备高可用 157 | 158 | ![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6355073a768f48e19ad171be64bb994e~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) 159 | 160 | - **架构描述**:两台机器A和B,A为主库,负责读写,B为备库,只备份数据。如果A库发生故障,B库成为主库负责读写。修复故障后,A成为备库,主库B同步数据到备库A 161 | - **优点**:一个机器故障了可以自动切换,操作比较简单。 162 | - **缺点**:只有一个库在工作,读写压力大,未能实现读写分离,并发也有一定限制 163 | 164 | #### 一主一从 165 | 166 | ![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6e44d62b86db4c14a3f526d15279faeb~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) 167 | 168 | - **架构描述**: 两台机器A和B,A为主库,负责读写,B为从库,负责读数据。如果A库发生故障,B库成为主库负责读写。修复故障后,A成为从库,主库B同步数据到从库A。 169 | - **优点**: 从库支持读,分担了主库的压力,提升了并发度。一个机器故障了可以自动切换,操作比较简单。 170 | - **缺点**: 一台从库,并发支持还是不够,并且一共两台机器,**还是存在同时故障的机率,不够高可用**。 171 | 172 | #### 一主多从 173 | 174 | ![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/97d21e42b4a745ec8ff2777943819cf0~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) 175 | 176 | - **架构描述**: 一台主库多台从库,A为主库,负责读写,B、C、D为从库,负责读数据。如果A库发生故障,B库成为主库负责读写,C、D负责读。修复故障后,A也成为从库,主库B同步数据到从库A。 177 | - **优点**: 多个从库支持读,分担了主库的压力,明显提升了读的并发度。 178 | - **缺点**: 只有台主机写,因此写的并发度不高 179 | 180 | #### MariaDB同步多主机集群 181 | 182 | ![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/91edeaa545774026bdd0777f8247d115~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) 183 | 184 | - **架构描述**:有代理层实现负载均衡,多个数据库可以同时进行读写操作;各个数据库之间可以通过`Galera Replication`方法进行数据同步,每个库理论上数据是完全一致的。 185 | - **优点**: 读写的并发度都明显提升,可以任意节点读写,可以自动剔除故障节点,具有较高的可靠性。 186 | - **缺点**: 数据量不支持特别大。要避免大事务卡死,如果集群节点一个变慢,其他节点也会跟着变慢。 187 | 188 | #### 数据库中间件 189 | 190 | ![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/031d9e391d2c42dabeee33f1db613da0~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) 191 | 192 | - 架构描述:mycat分片存储,每个分片配置一主多从的集群。 193 | - 优点:解决高并发高数据量的高可用方案 194 | - 缺点:维护成本比较大。 195 | 196 | 197 | 198 | -------------------------------------------------------------------------------- /数据库/MySQL/存储引擎.md: -------------------------------------------------------------------------------- 1 | # 存储引擎 2 | 3 | ## 问题 4 | 5 | * MySQL 常见的存储引擎 InnoDB、MyISAM、MEMORY 的区别?适用场景分别是 6 | * MySQL 存储引擎与 MyISAM 与 InnoDB 如何选择 7 | * MySQL的MyISAM与InnoDB两种存储引擎在,事务、锁级别,各自的适用场景? 8 | 9 | ## 回答 10 | 11 | ### MySQL 常见的存储引擎 InnoDB、MyISAM 的区别?适用场景分别是 12 | 13 | * InnoDB 支持事务,MyISAM 不支持事务。 14 | * InnoDB 支持外键约束,MyISAM 不支持。 15 | * InnoDB 索引叶子节点和数据不分离,MyISAM 索引叶子节点只是数据的指针。 16 | * InnoDB 支持表、行(默认)级锁,而 MyISAM 支持表级锁 17 | * InnoDB 不保存每张表的条目数量,MyISAM 保存,因此 select count(*) 能 O(1) 复杂度查询。 18 | 19 | 20 | 21 | **适用场景**: 22 | 23 | MyISAM适合: 插入不频繁,查询非常频繁,如果执行大量的SELECT,MyISAM是更好的选择, 没有事务。 24 | 25 | InnoDB适合: 可靠性要求比较高,或者要求事务; 表更新和查询都相当的频繁, 大量的INSERT或UPDATE 26 | 27 | -------------------------------------------------------------------------------- /数据库/MySQL/读写分离.md: -------------------------------------------------------------------------------- 1 | # 读写分离 2 | 3 | [(4条消息) MySQL关系数据库的读写分离的四种方案_数据库读写分离_inrgihc的博客-CSDN博客](https://blog.csdn.net/inrgihc/article/details/108293738) 4 | 5 | ## 问题 6 | 7 | * 为什么要读写分离 8 | * 如何实现读写分离 9 | 10 | ## 回答 11 | 12 | ### 为什么要读写分离 13 | 14 | 读写分离是一种常见的数据库优化策略,它的主要目的是提高系统的性能和可用性。在传统的数据库架构中,读写操作通常都是在同一台服务器上进行的,这样容易出现读写冲突,导致系统响应变慢,甚至出现数据丢失的情况。 15 | 16 | 通过读写分离,可以将读操作和写操作分别分配到不同的服务器上进行处理,从而避免了读写冲突问题,提高了系统的并发能力和稳定性。同时,读写分离还可以通过增加读取服务器的数量来提高系统的读取速度,进一步提升系统的性能。 17 | 18 | 19 | 20 | 21 | ### 如何实现读写分离 22 | 23 | mysql 本身不能实现读写分离的功能,需要借助中间件实现,主要基于主从复制原理 24 | 25 | 26 | 27 | #### 1、基于MySQL proxy代理的方式 28 | 29 | 在应用和数据库之间增加代理层,代理层接收应用对数据库的请求,根据不同请求类型转发到不同的实例,在实现读写分离的同时可以实现负载均衡。 30 | 31 | (1)实现原理 32 | 33 | ![img](https://img-blog.csdnimg.cn/20200829134620375.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2lucmdpaGM=,size_16,color_FFFFFF,t_70) 34 | 35 | 36 | (2)开源方案 37 | MySQL的代理最常见的是mysql-proxy、cobar、mycat、Atlas等。这种方式对于应用来说,MySQL Proxy是完全透明的,应用则只需要连接到MySQL Proxy的监听端口即可。当然,这样proxy机器可能成为单点失效,但完全可以使用多个proxy机器做为冗余,在应用服务器的连接池配置中配置到多 个proxy的连接参数即可。 38 | 39 | * mysql-proxy是一个轻量的中间代理,是官方提供的mysql中间件产品可以实现负载平衡,读写分离,failover等,依靠内部一个lua脚本实现读写语句的判断。项目地址: https://github.com/mysql/mysql-proxy ,该项目已经六七年没有维护了,官方也不建议应用于生成环境。 40 | * cobar是阿里提供的一个中间件,已经停止更新。项目地址:https://github.com/alibaba/cobar 41 | * mycat的前身就是cobar,活跃度比较高,完全使用java语言开发。 项目地址:https://github.com/MyCATApache/Mycat-Server ,该项目当前已经有8.3k的点赞量。 42 | * moeba(变形虫)是阿里工程师陈思儒基于java开发的一款数据库读写分离的项目(读写分离只是它的一个小功能),与MySQL官方的MySQLProxy相比,作者强调的是amoeba配置的方便(基于XML的配置文件,用SQLJEP语法书写规则,比基于lua脚本的MySQL Proxy简单)。更多详细介绍请参考:https://www.biaodianfu.com/amoeba.html , 下载地址:https://sourceforge.net/projects/amoeba/ 。 43 | * Atlas奇虎360的一个开源中间代理,是在mysql官方mysql-proxy 0.8.2的基础上进行了优化,增加一些新的功能特性。 项目地址: https://github.com/Qihoo360/Atlas ,该项目当前已经有4.4k的点赞量。 44 | 45 | #### 2、基于应用内路由的方式 46 | 47 | 基于应用内路由的方式即为在应用程序中实现,针对不同的请求类型去不同的实例执行sql。 48 | 49 | (1)实现原理 50 | 51 | ![img](https://img-blog.csdnimg.cn/20200829142652187.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2lucmdpaGM=,size_16,color_FFFFFF,t_70) 52 | 53 | (2)实现方案 54 | 基于 spring 的 aop 实现: 用 aop 来拦截 spring 项目的 dao 层方法,根据方法名称就可以判断要执行的 sql 类型(即是read还是write类型),进而动态切换主从数据源。类似项目有: 55 | 56 | 多数据源切换:https://gitee.com/baomidou/dynamic-datasource-spring-boot-starter 57 | 58 | 59 | 60 | #### 3 、基于mysql-connector-java的jdbc驱动方式 61 | 62 | (1)实现原理 63 | 使用mysql驱动Connector/J的可以实现读写分离。即在jdbc的url中配置为如下的形示: 64 | 65 | jdbc:mysql:replication://master,slave1,slave2,slave3/test 66 | 67 | (2)实现方案 68 | java程序通过在连接MySQL的jdbc中配置主库与从库等地址,jdbc会自动将读请求发送给从库,将写请求发送给主库,此外,mysql的jdbc驱动还能够实现多个从库的负载均衡。 69 | 70 | 关于mysql的jdbc说明官方文档地址:https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-jdbc-url-format.html 71 | 关于mysql的读写分离文档地址:https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-source-replica-replication-connection.html 72 | 73 | 74 | 75 | #### 4、基于sharding-jdbc的方式 76 | 77 | sharding-sphere是强大的读写分离、分表分库中间件,sharding-jdbc是sharding-sphere的核心模块。 78 | 79 | (1)实现原理 80 | 81 | ![img](https://img-blog.csdnimg.cn/20200829150355955.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2lucmdpaGM=,size_16,color_FFFFFF,t_70) 82 | 83 | 84 | (2)实现方案 85 | sharding-jdbc可以与springboot集成。官方网址:https://shardingsphere.apache.org/ 86 | 87 | ``` 88 | 89 | org.apache.shardingsphere 90 | sharding-jdbc-spring-boot-starter 91 | 4.0.1 92 | 93 | ``` 94 | 95 | 96 | -------------------------------------------------------------------------------- /数据库/Redis/Redis.md: -------------------------------------------------------------------------------- 1 | # Redis 2 | 3 | https://blog.csdn.net/weixin_46594796/article/details/115906020 4 | 5 | [图解Redis介绍 | 小林coding (xiaolincoding.com)](https://xiaolincoding.com/redis/) 6 | 7 | [20道Redis经典面试题!(珍藏版) - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/427496556) 8 | 9 | ## NoSQL 10 | 11 | ## 数据结构 12 | 13 | ## 发布订阅 14 | 15 | ## 事务和锁 16 | 17 | ## Redis 持久化 18 | 19 | ## 主从复制 20 | 21 | ## 集群 22 | 23 | ## 应用问题 -------------------------------------------------------------------------------- /数据库/Redis/主从复制.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PokIsemaine/interview/bd5da05b557897a71361f8dbb9a6a6d25cf28d46/数据库/Redis/主从复制.md -------------------------------------------------------------------------------- /数据库/Redis/发布订阅.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PokIsemaine/interview/bd5da05b557897a71361f8dbb9a6a6d25cf28d46/数据库/Redis/发布订阅.md -------------------------------------------------------------------------------- /数据库/Redis/基本概念.md: -------------------------------------------------------------------------------- 1 | # 基本概念 2 | 3 | ## 问题 4 | 5 | * 什么是 Redis? 6 | * Redis 和 Memcached 有什么区别 7 | * 为什么用 Redis 作为 MySQL 的缓存 8 | 9 | ## 回答 10 | 11 | ### 什么是 Redis? 12 | 13 | * Redis 是一种基于内存的数据库,对数据的读写操作都是在内存中完成,因此**读写速度非常快**,常用于**缓存,消息队列、分布式锁等场景**。 14 | * Redis 提供了多种数据类型来支持不同的业务场景,比如 String(字符串)、Hash(哈希)、 List (列表)、Set(集合)、Zset(有序集合)、Bitmaps(位图)、HyperLogLog(基数统计)、GEO(地理信息)、Stream(流) 15 | * 对数据类型的操作都是**原子性**的,因为执行命令由单线程负责的,不存在并发竞争的问题。 16 | * Redis 还支持**事务 、持久化、Lua 脚本、多种集群方案(主从复制模式、哨兵模式、切片机群模式)、发布/订阅模式,内存淘汰机制、过期删除机制**等等。 17 | 18 | 19 | 20 | ### Redis 和 Memcached 有什么区别 21 | 22 | Redis 与 Memcached **共同点**: 23 | 24 | 1. 都是基于内存的数据库,一般都用来当做缓存使用。 25 | 2. 都有过期策略。 26 | 3. 两者的性能都非常高。 27 | 28 | Redis 与 Memcached **区别**: 29 | 30 | - Redis 支持的数据类型更丰富(String、Hash、List、Set、ZSet),而 Memcached 只支持最简单的 key-value 数据类型; 31 | - Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memcached 没有持久化功能,数据全部存在内存之中,Memcached 重启或者挂掉后,数据就没了; 32 | - Redis 原生支持集群模式,Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据; 33 | - Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持; 34 | 35 | 36 | 37 | ### 为什么用 Redis 作为 MySQL 的缓存 38 | 39 | 主要是因为 **Redis 具备「高性能」和「高并发」两种特性**。 40 | 41 | ***1、Redis 具备高性能*** 42 | 43 | 假如用户第一次访问 MySQL 中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据缓存在 Redis 中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了,操作 Redis 缓存就是直接操作内存,所以速度相当快。 44 | 45 | ![img](https://cdn.xiaolincoding.com//mysql/other/37e4378d2edcb5e217b00e5f12973efd.png) 46 | 47 | 如果 MySQL 中的对应数据改变的之后,同步改变 Redis 缓存中相应的数据即可,不过这里会有 Redis 和 MySQL 双写一致性的问题,后面我们会提到。 48 | 49 | ***2、 Redis 具备高并发*** 50 | 51 | 单台设备的 Redis 的 QPS(Query Per Second,每秒钟处理完请求的次数) 是 MySQL 的 10 倍,Redis 单机的 QPS 能轻松破 10w,而 MySQL 单机的 QPS 很难破 1w。 52 | 53 | 所以,直接访问 Redis 能够承受的请求是远远大于直接访问 MySQL 的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。 -------------------------------------------------------------------------------- /数据库/Redis/应用问题.md: -------------------------------------------------------------------------------- 1 | # 应用问题 2 | 3 | ## 问题 4 | 5 | * Redis 如何实现延迟队列? 6 | * Redis的大 key 如何处理? 7 | * Redis 管道有什么用? 8 | * Redis 事务支持回滚吗? 9 | * 如何用 Redis 实现分布式锁的? 10 | 11 | ## 回答 -------------------------------------------------------------------------------- /数据库/Redis/持久化.md: -------------------------------------------------------------------------------- 1 | # 持久化 2 | 3 | ## 问题 4 | 5 | * Redis 如何实现数据不丢失? 6 | * AOF 日志是如何实现的? 7 | * RDB 快照是如何实现的? 8 | * 为何会有混合持久化? 9 | 10 | 11 | 12 | ## 回答 13 | 14 | #### Redis 如何实现数据不丢失? 15 | 16 | Redis 的读写操作都是在内存中,所以 Redis 性能才会高,但是当 Redis 重启后,内存中的数据就会丢失,那为了保证内存中的数据不会丢失,Redis 实现了数据持久化的机制,这个机制会把数据存储到磁盘,这样在 Redis 重启就能够从磁盘中恢复原有的数据。 17 | 18 | Redis 共有三种数据持久化的方式: 19 | 20 | - **AOF 日志**:每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里; 21 | - **RDB 快照**:将某一时刻的内存数据,以二进制的方式写入磁盘; 22 | - **混合持久化方式**:Redis 4.0 新增的方式,集成了 AOF 和 RBD 的优点; 23 | 24 | #### AOF 日志是如何实现的? 25 | 26 | Redis 在执行完一条写操作命令后,就会把该命令以追加的方式写入到一个文件里,然后 Redis 重启时,会读取该文件记录的命令,然后逐一执行命令的方式来进行数据恢复。 27 | 28 | ![img](https://cdn.xiaolincoding.com//mysql/other/6f0ab40396b7fc2c15e6f4487d3a0ad7-20230309232240301.png) 29 | 30 | 我这里以「*set name xiaolin*」命令作为例子,Redis 执行了这条命令后,记录在 AOF 日志里的内容如下图: 31 | 32 | ![img](https://cdn.xiaolincoding.com//mysql/other/337021a153944fd0f964ca834e34d0f2-20230309232243363.png) 33 | 34 | 我这里给大家解释下。 35 | 36 | 「*3」表示当前命令有三个部分,每部分都是以「$+数字」开头,后面紧跟着具体的命令、键或值。然后,这里的「数字」表示这部分中的命令、键或值一共有多少字节。例如,「$3 set」表示这部分有 3 个字节,也就是「set」命令这个字符串的长度。 37 | 38 | > 为什么先执行命令,再把数据写入日志呢? 39 | 40 | Reids 是先执行写操作命令后,才将该命令记录到 AOF 日志里的,这么做其实有两个好处。 41 | 42 | - **避免额外的检查开销**:因为如果先将写操作命令记录到 AOF 日志里,再执行该命令的话,如果当前的命令语法有问题,那么如果不进行命令语法检查,该错误的命令记录到 AOF 日志里后,Redis 在使用日志恢复数据时,就可能会出错。 43 | - **不会阻塞当前写操作命令的执行**:因为当写操作命令执行成功后,才会将命令记录到 AOF 日志。 44 | 45 | 当然,这样做也会带来风险: 46 | 47 | - **数据可能会丢失:** 执行写操作命令和记录日志是两个过程,那当 Redis 在还没来得及将命令写入到硬盘时,服务器发生宕机了,这个数据就会有丢失的风险。 48 | - **可能阻塞其他操作:** 由于写操作命令执行成功后才记录到 AOF 日志,所以不会阻塞当前命令的执行,但因为 AOF 日志也是在主线程中执行,所以当 Redis 把日志文件写入磁盘的时候,还是会阻塞后续的操作无法执行。 49 | 50 | > AOF 写回策略有几种? 51 | 52 | 先来看看,Redis 写入 AOF 日志的过程,如下图: 53 | 54 | ![img](https://cdn.xiaolincoding.com//mysql/other/4eeef4dd1bedd2ffe0b84d4eaa0dbdea-20230309232249413.png) 55 | 56 | 具体说说: 57 | 58 | 1. Redis 执行完写操作命令后,会将命令追加到 server.aof_buf 缓冲区; 59 | 2. 然后通过 write() 系统调用,将 aof_buf 缓冲区的数据写入到 AOF 文件,此时数据并没有写入到硬盘,而是拷贝到了内核缓冲区 page cache,等待内核将数据写入硬盘; 60 | 3. 具体内核缓冲区的数据什么时候写入到硬盘,由内核决定。 61 | 62 | Redis 提供了 3 种写回硬盘的策略,控制的就是上面说的第三步的过程。 在 Redis.conf 配置文件中的 appendfsync 配置项可以有以下 3 种参数可填: 63 | 64 | - **Always**,这个单词的意思是「总是」,所以它的意思是每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘; 65 | - **Everysec**,这个单词的意思是「每秒」,所以它的意思是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,然后每隔一秒将缓冲区里的内容写回到硬盘; 66 | - **No**,意味着不由 Redis 控制写回硬盘的时机,转交给操作系统控制写回的时机,也就是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,再由操作系统决定何时将缓冲区内容写回硬盘。 67 | 68 | 我也把这 3 个写回策略的优缺点总结成了一张表格: 69 | 70 | ![img](https://cdn.xiaolincoding.com//mysql/other/98987d9417b2bab43087f45fc959d32a-20230309232253633.png) 71 | 72 | > AOF 日志过大,会触发什么机制? 73 | 74 | AOF 日志是一个文件,随着执行的写操作命令越来越多,文件的大小会越来越大。 如果当 AOF 日志文件过大就会带来性能问题,比如重启 Redis 后,需要读 AOF 文件的内容以恢复数据,如果文件过大,整个恢复的过程就会很慢。 75 | 76 | 所以,Redis 为了避免 AOF 文件越写越大,提供了 **AOF 重写机制**,当 AOF 文件的大小超过所设定的阈值后,Redis 就会启用 AOF 重写机制,来压缩 AOF 文件。 77 | 78 | AOF 重写机制是在重写时,读取当前数据库中的所有键值对,然后将每一个键值对用一条命令记录到「新的 AOF 文件」,等到全部记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件。 79 | 80 | 举个例子,在没有使用重写机制前,假设前后执行了「*set name xiaolin*」和「*set name xiaolincoding*」这两个命令的话,就会将这两个命令记录到 AOF 文件。 81 | 82 | ![img](https://cdn.xiaolincoding.com//mysql/other/723d6c580c05400b3841bc69566dd61b-20230309232257343.png) 83 | 84 | 但是**在使用重写机制后,就会读取 name 最新的 value(键值对) ,然后用一条 「set name xiaolincoding」命令记录到新的 AOF 文件**,之前的第一个命令就没有必要记录了,因为它属于「历史」命令,没有作用了。这样一来,一个键值对在重写日志中只用一条命令就行了。 85 | 86 | 重写工作完成后,就会将新的 AOF 文件覆盖现有的 AOF 文件,这就相当于压缩了 AOF 文件,使得 AOF 文件体积变小了。 87 | 88 | > 重写 AOF 日志的过程是怎样的? 89 | 90 | Redis 的**重写 AOF 过程是由后台子进程 \*bgrewriteaof\* 来完成的**,这么做可以达到两个好处: 91 | 92 | - 子进程进行 AOF 重写期间,主进程可以继续处理命令请求,从而避免阻塞主进程; 93 | - 子进程带有主进程的数据副本,这里使用子进程而不是线程,因为如果是使用线程,多线程之间会共享内存,那么在修改共享内存数据的时候,需要通过加锁来保证数据的安全,而这样就会降低性能。而使用子进程,创建子进程时,父子进程是共享内存数据的,不过这个共享的内存只能以只读的方式,而当父子进程任意一方修改了该共享内存,就会发生「写时复制」,于是父子进程就有了独立的数据副本,就不用加锁来保证数据安全。 94 | 95 | 触发重写机制后,主进程就会创建重写 AOF 的子进程,此时父子进程共享物理内存,重写子进程只会对这个内存进行只读,重写 AOF 子进程会读取数据库里的所有数据,并逐一把内存数据的键值对转换成一条命令,再将命令记录到重写日志(新的 AOF 文件)。 96 | 97 | **但是重写过程中,主进程依然可以正常处理命令**,那问题来了,重写 AOF 日志过程中,如果主进程修改了已经存在 key-value,那么会发生写时复制,此时这个 key-value 数据在子进程的内存数据就跟主进程的内存数据不一致了,这时要怎么办呢? 98 | 99 | 为了解决这种数据不一致问题,Redis 设置了一个 **AOF 重写缓冲区**,这个缓冲区在创建 bgrewriteaof 子进程之后开始使用。 100 | 101 | 在重写 AOF 期间,当 Redis 执行完一个写命令之后,它会**同时将这个写命令写入到 「AOF 缓冲区」和 「AOF 重写缓冲区」**。 102 | 103 | ![img](https://cdn.xiaolincoding.com//mysql/other/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0ODI3Njc0,size_16,color_FFFFFF,t_70-20230309232301042.png) 104 | 105 | 也就是说,在 bgrewriteaof 子进程执行 AOF 重写期间,主进程需要执行以下三个工作: 106 | 107 | - 执行客户端发来的命令; 108 | - 将执行后的写命令追加到 「AOF 缓冲区」; 109 | - 将执行后的写命令追加到 「AOF 重写缓冲区」; 110 | 111 | 当子进程完成 AOF 重写工作(*扫描数据库中所有数据,逐一把内存数据的键值对转换成一条命令,再将命令记录到重写日志*)后,会向主进程发送一条信号,信号是进程间通讯的一种方式,且是异步的。 112 | 113 | 主进程收到该信号后,会调用一个信号处理函数,该函数主要做以下工作: 114 | 115 | - 将 AOF 重写缓冲区中的所有内容追加到新的 AOF 的文件中,使得新旧两个 AOF 文件所保存的数据库状态一致; 116 | - 新的 AOF 的文件进行改名,覆盖现有的 AOF 文件。 117 | 118 | 信号函数执行完后,主进程就可以继续像往常一样处理命令了。 119 | 120 | TIP 121 | 122 | AOF 日志的内容就暂时提这些,想更详细了解 AOF 日志的工作原理,可以详细看这篇:[AOF 持久化是怎么实现的](https://xiaolincoding.com/redis/storage/aof.html) 123 | 124 | #### RDB 快照是如何实现的? 125 | 126 | 因为 AOF 日志记录的是操作命令,不是实际的数据,所以用 AOF 方法做故障恢复时,需要全量把日志都执行一遍,一旦 AOF 日志非常多,势必会造成 Redis 的恢复操作缓慢。 127 | 128 | 为了解决这个问题,Redis 增加了 RDB 快照。所谓的快照,就是记录某一个瞬间东西,比如当我们给风景拍照时,那一个瞬间的画面和信息就记录到了一张照片。 129 | 130 | 所以,RDB 快照就是记录某一个瞬间的内存数据,记录的是实际数据,而 AOF 文件记录的是命令操作的日志,而不是实际的数据。 131 | 132 | 因此在 Redis 恢复数据时, RDB 恢复数据的效率会比 AOF 高些,因为直接将 RDB 文件读入内存就可以,不需要像 AOF 那样还需要额外执行操作命令的步骤才能恢复数据。 133 | 134 | > RDB 做快照时会阻塞线程吗? 135 | 136 | Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave,他们的区别就在于是否在「主线程」里执行: 137 | 138 | - 执行了 save 命令,就会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,**会阻塞主线程**; 139 | - 执行了 bgsave 命令,会创建一个子进程来生成 RDB 文件,这样可以**避免主线程的阻塞**; 140 | 141 | Redis 还可以通过配置文件的选项来实现每隔一段时间自动执行一次 bgsave 命令,默认会提供以下配置: 142 | 143 | ```c 144 | save 900 1 145 | save 300 10 146 | save 60 10000 147 | ``` 148 | 149 | 别看选项名叫 save,实际上执行的是 bgsave 命令,也就是会创建子进程来生成 RDB 快照文件。 只要满足上面条件的任意一个,就会执行 bgsave,它们的意思分别是: 150 | 151 | - 900 秒之内,对数据库进行了至少 1 次修改; 152 | - 300 秒之内,对数据库进行了至少 10 次修改; 153 | - 60 秒之内,对数据库进行了至少 10000 次修改。 154 | 155 | 这里提一点,Redis 的快照是**全量快照**,也就是说每次执行快照,都是把内存中的「所有数据」都记录到磁盘中。所以执行快照是一个比较重的操作,如果频率太频繁,可能会对 Redis 性能产生影响。如果频率太低,服务器故障时,丢失的数据会更多。 156 | 157 | > RDB 在执行快照的时候,数据能修改吗? 158 | 159 | 可以的,执行 bgsave 过程中,Redis 依然**可以继续处理操作命令**的,也就是数据是能被修改的,关键的技术就在于**写时复制技术(Copy-On-Write, COW)。** 160 | 161 | 执行 bgsave 命令的时候,会通过 fork() 创建子进程,此时子进程和父进程是共享同一片内存数据的,因为创建子进程的时候,会复制父进程的页表,但是页表指向的物理内存还是一个,此时如果主线程执行读操作,则主线程和 bgsave 子进程互相不影响。 162 | 163 | ![img](https://cdn.xiaolincoding.com//mysql/other/c34a9d1f58d602ff1fe8601f7270baa7-20230309232304226.png) 164 | 165 | 如果主线程执行写操作,则被修改的数据会复制一份副本,然后 bgsave 子进程会把该副本数据写入 RDB 文件,在这个过程中,主线程仍然可以直接修改原来的数据。 166 | 167 | ![img](https://cdn.xiaolincoding.com//mysql/other/ebd620db8a1af66fbeb8f4d4ef6adc68-20230309232308604.png) 168 | 169 | TIP 170 | 171 | RDB 快照的内容就暂时提这些,想更详细了解 RDB 快照的工作原理,可以详细看这篇:[RDB 快照是怎么实现的?](https://xiaolincoding.com/redis/storage/rdb.html) 172 | 173 | #### 为何会有混合持久化? 174 | 175 | RDB 优点是数据恢复速度快,但是快照的频率不好把握。频率太低,丢失的数据就会比较多,频率太高,就会影响性能。 176 | 177 | AOF 优点是丢失数据少,但是数据恢复不快。 178 | 179 | 为了集成了两者的优点, Redis 4.0 提出了**混合使用 AOF 日志和内存快照**,也叫混合持久化,既保证了 Redis 重启速度,又降低数据丢失风险。 180 | 181 | 混合持久化工作在 **AOF 日志重写过程**,当开启了混合持久化时,在 AOF 重写日志时,fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。 182 | 183 | 也就是说,使用了混合持久化,AOF 文件的**前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据**。 184 | 185 | ![img](https://cdn.xiaolincoding.com//mysql/other/f67379b60d151262753fec3b817b8617-20230309232312657.png) 186 | 187 | 这样的好处在于,重启 Redis 加载数据的时候,由于前半部分是 RDB 内容,这样**加载的时候速度会很快**。 188 | 189 | 加载完 RDB 的内容后,才会加载后半部分的 AOF 内容,这里的内容是 Redis 后台子进程重写 AOF 期间,主线程处理的操作命令,可以使得**数据更少的丢失**。 190 | 191 | **混合持久化优点:** 192 | 193 | - 混合持久化结合了 RDB 和 AOF 持久化的优点,开头为 RDB 的格式,使得 Redis 可以更快的启动,同时结合 AOF 的优点,有减低了大量数据丢失的风险。 194 | 195 | **混合持久化缺点:** 196 | 197 | - AOF 文件中添加了 RDB 格式的内容,使得 AOF 文件的可读性变得很差; 198 | - 兼容性差,如果开启混合持久化,那么此混合持久化 AOF 文件,就不能用在 Redis 4.0 之前版本了。 -------------------------------------------------------------------------------- /数据库/Redis/线程模型.md: -------------------------------------------------------------------------------- 1 | # Redis 线程模型 2 | 3 | ## 问题 4 | 5 | * Redis 是单线程吗? 6 | * Redis 单线程模式是怎么样的? 7 | * Redis 采用单线程为什么还这么快? 8 | * Redis6.0 之前为什么使用单线程? 9 | * Redis6.0 之后为什么引入了多线程? 10 | 11 | ## 回答 12 | 13 | ### Redis 是单线程吗? 14 | 15 | **Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」这个过程是由一个线程(主线程)来完成的**,这也是我们常说 Redis 是单线程的原因。 16 | 17 | 但是,**Redis 程序并不是单线程的**,Redis 在启动的时候,是会**启动后台线程**(BIO)的: 18 | 19 | - **Redis 在 2.6 版本**,会启动 2 个后台线程,分别处理关闭文件、AOF 刷盘这两个任务; 20 | - **Redis 在 4.0 版本之后**,新增了一个新的后台线程,用来异步释放 Redis 内存,也就是 lazyfree 线程。例如执行 unlink key / flushdb async / flushall async 等命令,会把这些删除操作交给后台线程来执行,好处是不会导致 Redis 主线程卡顿。因此,当我们要删除一个大 key 的时候,不要使用 del 命令删除,因为 del 是在主线程处理的,这样会导致 Redis 主线程卡顿,因此我们应该使用 unlink 命令来异步删除大key。 21 | 22 | 之所以 Redis 为「关闭文件、AOF 刷盘、释放内存」这些任务创建单独的线程来处理,是因为这些任务的操作都是很耗时的,如果把这些任务都放在主线程来处理,那么 Redis 主线程就很容易发生阻塞,这样就无法处理后续的请求了。 23 | 24 | 后台线程相当于一个消费者,生产者把耗时任务丢到任务队列中,消费者(BIO)不停轮询这个队列,拿出任务就去执行对应的方法即可。 25 | 26 | ![img](https://cdn.xiaolincoding.com/gh/xiaolincoder/redis/%E5%85%AB%E8%82%A1%E6%96%87/%E5%90%8E%E5%8F%B0%E7%BA%BF%E7%A8%8B.jpg) 27 | 28 | 关闭文件、AOF 刷盘、释放内存这三个任务都有各自的任务队列: 29 | 30 | - BIO_CLOSE_FILE,关闭文件任务队列:当队列有任务后,后台线程会调用 close(fd) ,将文件关闭; 31 | - BIO_AOF_FSYNC,AOF刷盘任务队列:当 AOF 日志配置成 everysec 选项后,主线程会把 AOF 写日志操作封装成一个任务,也放到队列中。当发现队列有任务后,后台线程会调用 fsync(fd),将 AOF 文件刷盘, 32 | - BIO_LAZY_FREE,lazy free 任务队列:当队列有任务后,后台线程会 free(obj) 释放对象 / free(dict) 删除数据库所有对象 / free(skiplist) 释放跳表对象; 33 | 34 | ### Redis 单线程模式是怎么样的? 35 | 36 | Redis 6.0 版本之前的单线模式如下图: 37 | 38 | ![img](https://cdn.xiaolincoding.com/gh/xiaolincoder/redis/%E5%85%AB%E8%82%A1%E6%96%87/redis%E5%8D%95%E7%BA%BF%E7%A8%8B%E6%A8%A1%E5%9E%8B.drawio.png) 39 | 40 | 图中的蓝色部分是一个事件循环,是由主线程负责的,可以看到网络 I/O 和命令处理都是单线程。 Redis 初始化的时候,会做下面这几件事情: 41 | 42 | - 首先,调用 epoll_create() 创建一个 epoll 对象和调用 socket() 创建一个服务端 socket 43 | - 然后,调用 bind() 绑定端口和调用 listen() 监听该 socket; 44 | - 然后,将调用 epoll_ctl() 将 listen socket 加入到 epoll,同时注册「连接事件」处理函数。 45 | 46 | 初始化完后,主线程就进入到一个**事件循环函数**,主要会做以下事情: 47 | 48 | - 首先,先调用**处理发送队列函数**,看是发送队列里是否有任务,如果有发送任务,则通过 write 函数将客户端发送缓存区里的数据发送出去,如果这一轮数据没有发送完,就会注册写事件处理函数,等待 epoll_wait 发现可写后再处理 。 49 | - 接着,调用 epoll_wait 函数等待事件的到来: 50 | - 如果是**连接事件**到来,则会调用**连接事件处理函数**,该函数会做这些事情:调用 accpet 获取已连接的 socket -> 调用 epoll_ctl 将已连接的 socket 加入到 epoll -> 注册「读事件」处理函数; 51 | - 如果是**读事件**到来,则会调用**读事件处理函数**,该函数会做这些事情:调用 read 获取客户端发送的数据 -> 解析命令 -> 处理命令 -> 将客户端对象添加到发送队列 -> 将执行结果写到发送缓存区等待发送; 52 | - 如果是**写事件**到来,则会调用**写事件处理函数**,该函数会做这些事情:通过 write 函数将客户端发送缓存区里的数据发送出去,如果这一轮数据没有发送完,就会继续注册写事件处理函数,等待 epoll_wait 发现可写后再处理 。 53 | 54 | 以上就是 Redis 单线模式的工作方式,如果你想看源码解析,可以参考这一篇:[为什么单线程的 Redis 如何做到每秒数万 QPS ?](https://mp.weixin.qq.com/s/oeOfsgF-9IOoT5eQt5ieyw) 55 | 56 | ### Redis 采用单线程为什么还这么快? 57 | 58 | 官方使用基准测试的结果是,**单线程的 Redis 吞吐量可以达到 10W/每秒**,如下图所示: 59 | 60 | ![img](https://cdn.xiaolincoding.com/gh/xiaolincoder/redis/%E5%85%AB%E8%82%A1%E6%96%87/%E6%80%A7%E8%83%BD.png) 61 | 62 | 之所以 Redis 采用单线程(网络 I/O 和执行命令)那么快,有如下几个原因: 63 | 64 | - Redis 的大部分操作**都在内存中完成**,并且采用了高效的数据结构,因此 Redis 瓶颈可能是机器的内存或者网络带宽,而并非 CPU,既然 CPU 不是瓶颈,那么自然就采用单线程的解决方案了; 65 | - Redis 采用单线程模型可以**避免了多线程之间的竞争**,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题。 66 | - Redis 采用了 **I/O 多路复用机制**处理大量的客户端 Socket 请求,IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听 Socket 和已连接 Socket。内核会一直监听这些 Socket 上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。 67 | 68 | ### Redis 6.0 之前为什么使用单线程? 69 | 70 | 我们都知道单线程的程序是无法利用服务器的多核 CPU 的,那么早期 Redis 版本的主要工作(网络 I/O 和执行命令)为什么还要使用单线程呢?我们不妨先看一下Redis官方给出的[FAQ (opens new window)](https://link.juejin.cn/?target=https%3A%2F%2Fredis.io%2Ftopics%2Ffaq)。 71 | 72 | ![img](https://cdn.xiaolincoding.com/gh/xiaolincoder/redis/%E5%85%AB%E8%82%A1%E6%96%87/redis%E5%AE%98%E6%96%B9%E5%8D%95%E7%BA%BF%E7%A8%8B%E5%9B%9E%E7%AD%94.png) 73 | 74 | 核心意思是:**CPU 并不是制约 Redis 性能表现的瓶颈所在**,更多情况下是受到内存大小和网络I/O的限制,所以 Redis 核心网络模型使用单线程并没有什么问题,如果你想要使用服务的多核CPU,可以在一台服务器上启动多个节点或者采用分片集群的方式。 75 | 76 | 除了上面的官方回答,选择单线程的原因也有下面的考虑。 77 | 78 | 使用了单线程后,可维护性高,多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,**增加了系统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗**。 79 | 80 | 81 | 82 | ### Redis 6.0 之后为什么引入了多线程? 83 | 84 | 虽然 Redis 的主要工作(网络 I/O 和执行命令)一直是单线程模型,但是**在 Redis 6.0 版本之后,也采用了多个 I/O 线程来处理网络请求**,**这是因为随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 I/O 的处理上**。 85 | 86 | 所以为了提高网络 I/O 的并行度,Redis 6.0 对于网络 I/O 采用多线程来处理。**但是对于命令的执行,Redis 仍然使用单线程来处理,\**所以大家\**不要误解** Redis 有多线程同时执行命令。 87 | 88 | Redis 官方表示,**Redis 6.0 版本引入的多线程 I/O 特性对性能提升至少是一倍以上**。 89 | 90 | Redis 6.0 版本支持的 I/O 多线程特性,默认情况下 I/O 多线程只针对发送响应数据(write client socket),并不会以多线程的方式处理读请求(read client socket)。要想开启多线程处理客户端读请求,就需要把 Redis.conf 配置文件中的 io-threads-do-reads 配置项设为 yes。 91 | 92 | ```c 93 | //读请求也使用io多线程 94 | io-threads-do-reads yes 95 | ``` 96 | 97 | 同时, Redis.conf 配置文件中提供了 IO 多线程个数的配置项。 98 | 99 | ```c 100 | // io-threads N,表示启用 N-1 个 I/O 多线程(主线程也算一个 I/O 线程) 101 | io-threads 4 102 | ``` 103 | 104 | 关于线程数的设置,官方的建议是如果为 4 核的 CPU,建议线程数设置为 2 或 3,如果为 8 核 CPU 建议线程数设置为 6,线程数一定要小于机器核数,线程数并不是越大越好。 105 | 106 | 因此, Redis 6.0 版本之后,Redis 在启动的时候,默认情况下会**额外创建 6 个线程**(*这里的线程数不包括主线程*): 107 | 108 | - Redis-server : Redis的主线程,主要负责执行命令; 109 | - bio_close_file、bio_aof_fsync、bio_lazy_free:三个后台线程,分别异步处理关闭文件任务、AOF刷盘任务、释放内存任务; 110 | - io_thd_1、io_thd_2、io_thd_3:三个 I/O 线程,io-threads 默认是 4 ,所以会启动 3(4-1)个 I/O 多线程,用来分担 Redis 网络 I/O 的压力。 -------------------------------------------------------------------------------- /数据库/Redis/配置文件.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PokIsemaine/interview/bd5da05b557897a71361f8dbb9a6a6d25cf28d46/数据库/Redis/配置文件.md -------------------------------------------------------------------------------- /数据库/Redis/集群.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PokIsemaine/interview/bd5da05b557897a71361f8dbb9a6a6d25cf28d46/数据库/Redis/集群.md -------------------------------------------------------------------------------- /数据库/leveldb&rocksdb/leveldb.md: -------------------------------------------------------------------------------- 1 | # 问题 2 | 3 | * leveldb 概述 4 | * leveldb 存储机制 5 | * leveldb 日志 6 | * leveldb 查找机制 7 | * leveldb 如何修改 8 | * 跳表 9 | * 查询有缓存吗 10 | * write_batch 细节 11 | * leveldb 事务 12 | * 讲讲布隆过滤器 13 | * leveldb 怎么优化查询的 14 | * leveldb可以用在多台主机之间吗,我说leveldb是单机的,他问怎么做可以实现分布式,我说用一致性哈希算法,之后他又问一台机器宕机后,需不需要转移数据 15 | * leveldb 和 redis 对比、区别 16 | * leveldb 中数据结构的实现 17 | * leveldb 多路归并 18 | * leveldb 读写放大 19 | * leveldb 可以加索引吗 20 | * lsm 树介绍 21 | * boltdb、badgerdb 和 leveldb 区别 22 | * 了解 rocksdb 吗 23 | * leveldb源码怎么看的,看了哪些部分 24 | * memtable的实现、memtable如何实现无锁下的并发读写安全、为什么memtable选择跳表,innodb选择B+树? 25 | * 如果你来优化leveldb,你会怎么优化 26 | * 你刚才说到不同的存储介质,你知道ssd,hhd每秒的IO量级吗 27 | * 说一下你所知道的leveldb、rocksdb的区别 28 | * 如果你来实现多线程compaction,你会怎么实现 29 | * leveldb的出现是为了解决什么问题 30 | * leveldb有什么优点 31 | * leveldb的快照了解吗 32 | * 说一下leveldb读写的过程 33 | * 现在有个黄页数据,比较一下LSM-Tree、哈希存储、B+-Tree三种存储引擎的优劣以及性能瓶颈 34 | * 现在有种新的存储介质,随机读写和顺序读写的速度相近,你有什么优化leveldb的存储引擎的方案 35 | * leveldb 迭代器 ? 收获? arena 作用 ? 36 | * immutable memtable dump 成 sstable 时,读的是哪一个? 37 | * 为什么 lsm 对磁盘友好? 38 | * 讲一讲 leveldb 压缩 39 | * 一直添加不相同的key,compaction 还有用吗? 40 | * kv 存储优势,劣势 41 | * 对比 b+ 树,lsm树 42 | * leveldb 读写放大 43 | * 两种写入模式分析 :in place 模式和 append模式 44 | * 介绍kv存储怎么做优化,讲了下kv分离的实现 45 | * 为什么 LSM 写入速度快 46 | * 是否了解过其他基于leveldb的生产级的数据库 47 | * leveldb的特点 48 | * leveldb使用场景 49 | * 为什么不直接使用redis,而需要有本项目/RocksDB 50 | * 是否考虑怎么在KV数据库项目中加入事务 51 | * leveldb为啥要搞这样的分层设计? 52 | * leveldb为什么要从上层往下层读呢?不能从下层开始? 53 | * leveldb 读哪一层sst最耗时,为什么? 54 | * 如果前台不停的读,后台在做compaction ,会发生什么? 55 | * levelDB 读写? 56 | * levelDB 怎么做 GC ? 57 | * levelDB 这种读写放大怎么解决的? 58 | * leveldb内存有哪几种形式, 59 | * leveldb读数据顺序; leveldb删除数据; 60 | * leveldb和mysql优缺点; 61 | * leveldb如何实现MVCC机制的; 62 | * LSMtree知道,TSM知道吗; 63 | * 导致 write stall 的原因 64 | * leveldb 写入是顺序写入吗 65 | * 多线程并发的时候,leveldb 能保证顺序写吗? 66 | * 为什么 bwtree 相比 lsmtree 的 list 性能好 67 | * levelDB为什么选用skiplist做索引 68 | * levelDB适用于写多读少的问题,讲了随机读取与顺序读取的区别 69 | * 为什么mysql不像levelDB这样做把连续数据存在连续页,并且不使用像levelDB这样的skiplist做索引,或者说基于此做一些拓展呢?我说我想想,(其实我想回答的设计里面的不同,可能mysql为了支持复杂的serach insert操作所以如此)面官微笑着说不用了,下一个吧。。。果然预期所想会问到你答不上来为止。 70 | * leveldb 的优势,劣势 71 | * 读第 0 层 sstable是怎么做的,第 0 层 compaction 怎么做的 72 | * 多线程写,写 wal 会不会压力很大,怎么解决 73 | * leveldb 的写队列需要一把大锁去保护,如果用无锁队列怎么设计 74 | * leveldb 的version是什么,什么时候会变化 75 | * 项目中提到levelDB,让我讲讲levelDB?讲讲跳表?为什么不考虑用其它的数据库?怎么进行的优化?提到Grafana,知不知道Linux上的其它监控软件?为什么不考虑rocksDB 76 | * levelDB的SST文件是利于读还是利于写? 77 | * leveldb 中如果经过多次 compact,底层文件系统产生了很多碎片,WAL 还能保持高效的顺序写性能吗? 78 | * 内存不够用咋办,leveldb 是内存数据库还是磁盘数据库? 79 | 80 | * LSM-tree 原理和特点 81 | * 为什么要追加写 82 | * LSM-tree 的工业实现 83 | * LSM-tree 的合并方式(合并超出阈值的部分还是全部?优缺点 84 | * Bloom filter 的实现 85 | * Bloom filter 如何持久化 86 | * 缓存的索引怎么做缓存淘汰 87 | * 缓存的索引一个 SSTable 对应一个 map 用一个全局 map 的优缺点 88 | * 如何做 LSM-tree 的 value 缓存(page cache 有用吗 89 | * LSM-tree 合并的时候怎么处理读请求 90 | * 如何实现 LSM-tree 的 MVCC 91 | 92 | 导致 write stall 的原因 93 | leveldb 写入是顺序写入吗 94 | 多线程并发的时候,leveldb 能保证顺序写吗? 95 | 96 | -------------------------------------------------------------------------------- /数据库/leveldb&rocksdb/rocksdb.md: -------------------------------------------------------------------------------- 1 | # 问题 2 | 3 | * 讲讲 rocksdb 的 column family 4 | * 你刚刚有提到过 MyRocks,那我看你简历也有写了解RocksDB,要不你给我讲讲lsm-tree? 5 | levelDB跟RocksDB有什么区别? 6 | * 简单介绍下rocksdb 7 | * RocksDB当中DB对象使用单例模式禁止copy 8 | * rocksdb引擎的优点,LSM树讲解 9 | * 定位到rocksdb的迭代器性能问题,去看了下它的迭代器实现,找到问题原因,解决问题。中间还遇到瓶颈在kernel page fault上,也优化了下 10 | * 谈到rocksdb了,开始问rocksdb 11 | * 架构,包括memtable、immem、sst这些烂大街的 12 | * memtable skiplist vs. rb-tree 13 | * 跨cf原子性 14 | * disable掉wal后有没有办法保证跨cf原子性,然后开始引导我,讨论了下可以O_DIRECT + fsync的manifest实现 15 | * rocksdb 压缩 16 | 17 | 18 | * 详细介绍NoSQL的项目(滴滴做的RocksDB二次开发?) 19 | * 讲讲LSM-Tree 20 | * MemTable直接到SST吗?(中间还有个immutable) 21 | * 你写的SST多大?大小怎么权衡 22 | * 布隆过滤器介绍一下,如果每个SST有一个BloomFilter,读盘还是有很大的开销,怎么办?(我说的全局BF,但是要考虑数据量大的时候BF的大小还有命中率) 23 | * 缓存粒度多大?DataBlock,KV还是SST,这几种粒度又各有什么问题,你为什么这么选择? 24 | * 并发策略的粒度,MVCC是针对KV还是SST 25 | * MemTable转Immutable没落盘时怎么读的?(糟了没考虑到,可能是整个面试里比较有问题的地方了,现场思考应对了一下) 26 | 27 | rocksdb-> 与 mysql 存储引擎对比? 28 | rocksdb读写 -------------------------------------------------------------------------------- /数据结构算法/TopK.md: -------------------------------------------------------------------------------- 1 | # TopK 2 | 3 | ## **问题描述** 4 | 5 | 从arr[1, n]这n个数中,找出最大的k个数,这就是经典的TopK问题。 6 | 7 | **栗子**: 8 | 9 | 从arr[1, 12]={5,3,7,1,8,2,9,4,7,2,6,6} 这n=12个数中,找出最大的k=5个。 10 | 11 | ## 解决方案 12 | 13 | ### **一、排序** 14 | 15 | ![图片](https://mmbiz.qpic.cn/mmbiz_png/YrezxckhYOzzA7pbponFmibHaMYQ5Vkk9sq2dj1EBcJOILlsSEwEaaicRoxicVfmSya1Ka2TzTNa3yicBXuricq28Xg/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1) 16 | 17 | 排序是最容易想到的方法,将n个数排序之后,取出最大的k个,即为所得。 18 | 19 | **伪代码**: 20 | 21 | sort(arr, 1, n); 22 | 23 | return arr[1, k]; 24 | 25 | **时间复杂度**:$O(n*log(n))$ 26 | 27 | **分析**:明明只需要TopK,却将全局都排序了,这也是这个方法复杂度非常高的原因。那能不能不全局排序,而只局部排序呢?这就引出了第二个优化方法。 28 | 29 | ### **二、局部排序** 30 | 31 | 不再全局排序,只对最大的k个排序。 32 | 33 | ![图片](https://mmbiz.qpic.cn/mmbiz_png/YrezxckhYOzzA7pbponFmibHaMYQ5Vkk9pNPrxictUYvjERicF7DnvaJ4SZHdjVCSXJE3WJlUIVQ9ujPiaSOicTYOmw/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1) 34 | 35 | 冒泡是一个很常见的排序方法,每冒一个泡,找出最大值,冒k个泡,就得到TopK。 36 | 37 | **伪代码**: 38 | 39 | ``` 40 | for(i=1 to k){ 41 | bubble_find_max(arr,i); 42 | } 43 | 44 | return arr[1, k]; 45 | ``` 46 | 47 | **时间复杂度**:O(n*k) 48 | 49 | **分析**:冒泡,将全局排序优化为了局部排序,非TopK的元素是不需要排序的,节省了计算资源。不少朋友会想到,需求是TopK,是不是这最大的k个元素也不需要排序呢?这就引出了第三个优化方法。 50 | 51 | ### **三、堆** 52 | 53 | **思路**:只找到TopK,不排序TopK。 54 | 55 | ![图片](https://mmbiz.qpic.cn/mmbiz_png/YrezxckhYOzzA7pbponFmibHaMYQ5Vkk9EkBEbuOR4lfCTZYn7oVPY70G2JHhGwanicvB4DkgicYjxdDSsT7M3KVQ/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1) 56 | 57 | 先用前k个元素生成一个小顶堆,这个小顶堆用于存储,当前最大的k个元素。 58 | 59 | ![图片](https://mmbiz.qpic.cn/mmbiz_png/YrezxckhYOzzA7pbponFmibHaMYQ5Vkk9fCKiazIwA0MaQMLhjr3NiavXDib6cSysEFlibGJg6uTZbUsXd3fy5NbCZA/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1) 60 | 61 | 接着,从第k+1个元素开始扫描,和堆顶(堆中最小的元素)比较,如果被扫描的元素大于堆顶,则替换堆顶的元素,并调整堆,以保证堆内的k个元素,总是当前最大的k个元素。 62 | 63 | ![图片](https://mmbiz.qpic.cn/mmbiz_png/YrezxckhYOzzA7pbponFmibHaMYQ5Vkk9xBxbWpznlYmMsItZ7Vd3B4MA0TzALhulicsNWrQ1icjaacibL19XOmicVg/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1) 64 | 65 | 直到,扫描完所有n-k个元素,最终堆中的k个元素,就是猥琐求的TopK。 66 | 67 | **伪代码**: 68 | 69 | ``` 70 | heap[k] = make_heap(arr[1, k]); 71 | for(i=k+1 to n){ 72 | adjust_heap(heep[k],arr[i]); 73 | } 74 | 75 | return heap[k]; 76 | ``` 77 | 78 | **时间复杂度**:$O(n*lg(k))$ 79 | 80 | *画外音:n个元素扫一遍,假设运气很差,每次都入堆调整,调整时间复杂度为堆的高度,即lg(k),故整体时间复杂度是n\*lg(k)。* 81 | 82 | **分析**:堆,将冒泡的TopK排序优化为了TopK不排序,节省了计算资源。堆,是求TopK的经典算法,那还有没有更快的方案呢? 83 | 84 | ### **四、随机选择(快速选择算法)** 85 | 86 | 随机选择算在是《算法导论》中一个经典的算法,其时间复杂度为O(n),是一个线性复杂度的方法。 87 | 88 | 这个方法并不是所有同学都知道,为了将算法讲透,先聊一些前序知识,一个所有程序员都应该烂熟于胸的经典算法:快速排序。 89 | 90 | *画外音:* 91 | 92 | *(1)如果有朋友说,“不知道快速排序,也不妨碍我写业务代码呀”…额...* 93 | 94 | *(2)除非校招,我在面试过程中从不问快速排序,默认所有工程师都知道;* 95 | 96 | **其伪代码是**: 97 | 98 | ```c++ 99 | void quick_sort(int[]arr, int low, inthigh){ 100 | if(low== high) return; 101 | int i = partition(arr, low, high); 102 | quick_sort(arr, low, i-1); 103 | quick_sort(arr, i+1, high); 104 | 105 | } 106 | ``` 107 | 108 | 其核心算法思想是,分治法。 109 | 110 | **分治法**(Divide&Conquer),把一个大的问题,转化为若干个子问题(Divide),每个子问题“**都**”解决,大的问题便随之解决(Conquer)。这里的关键词是**“都”**。从伪代码里可以看到,快速排序递归时,先通过partition把数组分隔为两个部分,两个部分“都”要再次递归。 111 | 112 | 分治法有一个特例,叫减治法。 113 | 114 | **减治法**(Reduce&Conquer),把一个大的问题,转化为若干个子问题(Reduce),这些子问题中“**只**”解决一个,大的问题便随之解决(Conquer)。这里的关键词是**“只”**。 115 | 116 | **二分查找binary_search**,BS,是一个典型的运用减治法思想的算法,其伪代码是: 117 | 118 | ```c++ 119 | int BS(int[]arr, int low, inthigh, int target){ 120 | if(low> high) return -1; 121 | mid= (low+high)/2; 122 | if(arr[mid]== target) return mid; 123 | if(arr[mid]> target) 124 | return BS(arr, low, mid-1, target); 125 | else 126 | return BS(arr, mid+1, high, target); 127 | } 128 | ``` 129 | 130 | 从伪代码可以看到,二分查找,一个大的问题,可以用一个mid元素,分成左半区,右半区两个子问题。而左右两个子问题,只需要解决其中一个,递归一次,就能够解决二分查找全局的问题。 131 | 132 | 通过分治法与减治法的描述,可以发现,分治法的复杂度一般来说是大于减治法的: 133 | 134 | 快速排序:$O(n*lg(n))$ 135 | 136 | 二分查找:$O(lg(n))$ 137 | 138 | 话题收回来,**快速排序**的核心是: 139 | 140 | i = partition(arr, low, high); 141 | 142 | **这个partition是干嘛的呢?** 143 | 144 | 顾名思义,partition会把整体分为两个部分。 145 | 146 | 更具体的,会用数组arr中的一个元素(默认是第一个元素t=arr[low])为划分依据,将数据arr[low, high]划分成左右两个子数组: 147 | 148 | - 左半部分,都比t大 149 | - 右半部分,都比t小 150 | - 中间位置i是划分元素 151 | 152 | ![图片](https://mmbiz.qpic.cn/mmbiz_png/YrezxckhYOzzA7pbponFmibHaMYQ5Vkk9ic4TjkicnCpTAVJJqq4WgbE6tQhl5XORGiaJnZj3UboKBpUhTq1kad5Ng/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1) 153 | 154 | 以上述TopK的数组为例,先用第一个元素t=arr[low]为划分依据,扫描一遍数组,把数组分成了两个半区: 155 | 156 | - 左半区比t大 157 | - 右半区比t小 158 | - 中间是t 159 | 160 | partition返回的是t最终的位置i。 161 | 162 | 很容易知道,partition的时间复杂度是O(n)。 163 | 164 | *画外音:把整个数组扫一遍,比t大的放左边,比t小的放右边,最后t放在中间N[i]。* 165 | 166 | **partition和TopK问题有什么关系呢?** 167 | 168 | TopK是希望求出arr[1,n]中最大的k个数,那如果找到了**第k大**的数,做一次partition,不就一次性找到最大的k个数了么? 169 | 170 | *画外音:即partition后左半区的k个数。* 171 | 172 | 问题变成了arr[1, n]中找到第k大的数。 173 | 174 | 再回过头来看看**第一次**partition,划分之后: 175 | 176 | i = partition(arr, 1, n); 177 | 178 | - 如果i大于k,则说明arr[i]左边的元素都大于k,于是只递归arr[1, i-1]里第k大的元素即可; 179 | - 如果i小于k,则说明说明第k大的元素在arr[i]的右边,于是只递归arr[i+1, n]里第k-i大的元素即可; 180 | 181 | *画外音:这一段非常重要,多读几遍。* 182 | 183 | 这就是**随机选择**算法randomized_select,RS,其伪代码如下: 184 | 185 | ```c++ 186 | int RS(arr, low, high, k){ 187 | if(low== high) return arr[low]; 188 | i= partition(arr, low, high); 189 | temp= i-low; //数组前半部分元素个数 190 | if(temp>=k) 191 | return RS(arr, low, i-1, k); //求前半部分第k大 192 | else 193 | return RS(arr, i+1, high, k-i); //求后半部分第k-i大 194 | } 195 | ``` 196 | 197 | ![图片](https://mmbiz.qpic.cn/mmbiz_png/YrezxckhYOzzA7pbponFmibHaMYQ5Vkk9kb1JqQdt0av8kK59VvicibDNOrbIXsZicaHAFhk8u1A7BQW1aYIr84pQg/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1) 198 | 199 | 这是一个典型的减治算法,递归内的两个分支,最终只会执行一个,它的时间复杂度是O(n)。 200 | 201 | 再次强调一下: 202 | 203 | - **分治法**,大问题分解为小问题,小问题都要递归各个分支,例如:快速排序 204 | - **减治法**,大问题分解为小问题,小问题只要递归一个分支,例如:二分查找,随机选择 205 | 206 | 通过随机选择(randomized_select),找到arr[1, n]中第k大的数,再进行一次partition,就能得到TopK的结果。 207 | 208 | ### **五、总结** 209 | 210 | TopK,不难;其思路优化过程,不简单: 211 | 212 | - **全局排序**,O(n*lg(n)) 213 | - **局部排序**,只排序TopK个数,O(n*k) 214 | - **堆**,TopK个数也不排序了,O(n*lg(k)) 215 | - 分治法,每个分支“都要”递归,例如:快速排序,O(n*lg(n)) 216 | - 减治法,“只要”递归一个分支,例如:二分查找O(lg(n)),随机选择O(n) 217 | - TopK的另一个解法:**随机选择**+partition 218 | -------------------------------------------------------------------------------- /数据结构算法/其他.md: -------------------------------------------------------------------------------- 1 | # 其他 2 | 3 | ## 问题 4 | 5 | * dp 和分治算法的区别和应用场景 6 | * 数组和链表的区别 7 | 8 | ## dp 和分治算法的区别和应用场景 9 | 10 | * 共同点:两者都是把原问题分解为子问题,然后将子问题的解合并形成原问题的解 11 | * 不同点 12 | * 分治法将分解后的子问题看成相互独立的,通过用递归来做。 13 | * 动态规划将分解后的子问题理解为相互间有联系,有重叠部分,需要记忆,通常用迭代来做。 14 | 15 | ## 数组和链表的区别 16 | 17 | 逻辑结构 18 | 19 | * 数组 20 | 21 | * 数组在内存中连续; 22 | 23 | * 使用数组之前,必须事先固定数组长度,不支持动态改变数组大小; 24 | 25 | * 数组元素增加时,有可能会数组越界 26 | 27 | * 数组元素减少时,会造成内存浪费; 28 | 29 | * 数组增删时需要移动其它元素 30 | 31 | * 链表 32 | * 链表采用动态内存分配的方式,在内存中不连续 33 | * 支持动态增加或者删除元素 34 | * 需要时可以使用malloc或者new来申请内存,不用时使用free或者delete来释放内存 35 | 36 | 内存结构 37 | 38 | * 数组从栈上分配内存,使用方便,但是自由度小 39 | * 链表从堆上分配内存,自由度大,但是要注意内存泄漏 40 | 41 | 访问效率 42 | 43 | * 数组在内存中顺序存储,可通过下标访问,访问效率高 44 | * 链表访问效率低,如果想要访问某个元素,需要从头遍历 45 | 46 | 越界问题 47 | 48 | * 数组的大小是固定的,所以存在访问越界的风险 49 | * 只要可以申请得到链表空间,链表就无越界风险 50 | 51 | 使用场景 52 | 53 | * 空间: 54 | * 数组的存储空间是栈上分配的,存储密度大,当要求存储的大小变化不大时,且可以事先确定大小,宜采用数组存储数据 55 | * 链表的存储空间是堆上动态申请的,当要求存储的长度变化较大时,且事先无法估量数据规模,宜采用链表存储 56 | * 时间 57 | * 数组访问效率高。当线性表的操作主要是进行查找,很少插入和删除时,宜采用数组结构 58 | * 链表插入、删除效率高,当线性表要求频繁插入和删除时,宜采用链表结构 -------------------------------------------------------------------------------- /数据结构算法/图论.md: -------------------------------------------------------------------------------- 1 | # 图论 2 | 3 | ## 最小生成树 4 | 5 | ### Prim 算法 6 | 7 | [858. Prim算法求最小生成树 - AcWing题库](https://www.acwing.com/problem/content/860/) 8 | 9 | ```c++ 10 | #include 11 | using namespace std; 12 | 13 | vector> graph; 14 | const int INF = 0x3f3f3f3f; 15 | int n, m; 16 | 17 | int prim() { 18 | vector dist(n + 1, INF); //存储各个节点到生成树的距离 19 | vector st(n + 1, false); 20 | 21 | int res = 0; 22 | dist[1] = 0; // 从 1 号节点开始生成 23 | 24 | for(int i = 0; i < n; ++i) { //每次循环选出一个点加入到生成树 25 | int t = -1; 26 | for(int j = 1; j <= n; ++j) { //每个节点一次判断 27 | if(!st[j] && (t == -1 || dist[j] < dist[t])) { 28 | t = j; 29 | } 30 | } 31 | 32 | //如果孤立点,直返输出不能,然后退出 33 | if(dist[t] == 0x3f3f3f3f) { 34 | return 0x3f3f3f3f; 35 | } 36 | 37 | st[t] = 1; // 选择该点 38 | res += dist[t]; 39 | for(int j = 1; j <= n; ++j) { 40 | if(!st[j] && dist[j] > graph[t][j]) { //从 t 到节点 i 的距离小于原来距离,则更新。 41 | dist[j] = graph[t][j]; 42 | } 43 | } 44 | } 45 | 46 | return res; 47 | } 48 | 49 | int main() { 50 | cin >> n >> m; 51 | graph.resize(n + 1, vector(n + 1, INF)); 52 | 53 | for(int i = 0; i < m; ++i) { 54 | int a, b, c; 55 | cin >> a >> b >> c; 56 | graph[a][b] = graph[b][a] = min(graph[a][b], c); 57 | } 58 | 59 | int t = prim(); 60 | if(t == INF) cout << "impossible"; 61 | else cout << t; 62 | return 0; 63 | } 64 | ``` 65 | 66 | 67 | 68 | ### Kruskal 算法 69 | 70 | [859. Kruskal算法求最小生成树 - AcWing题库](https://www.acwing.com/problem/content/861/) 71 | 72 | * 将所有边按照权值的大小进行升序排序,然后从小到大一一判断。 73 | * 如果这个边与之前选择的所有边不会组成回路,就选择这条边;反之,舍去。 74 | * 直到具有 n 个顶点的连通网筛选出来 n-1 条边为止。 75 | * 筛选出来的边和所有的顶点构成此连通网的最小生成树。 76 | 77 | $O(ElogE)$ 78 | 79 | ```c++ 80 | #include 81 | using namespace std; 82 | 83 | int n, m; 84 | 85 | vector fa; 86 | 87 | struct Edge { 88 | int a, b, w; 89 | bool operator < (const Edge& rhs) { 90 | return this->w < rhs.w; 91 | } 92 | }; 93 | 94 | vector edges; 95 | int find(int x) { 96 | return fa[x] = (fa[x] == x ? x : find(fa[x])); 97 | } 98 | 99 | int cnt = 0; 100 | int res = 0; // 保留的边数量 101 | 102 | void kruskal() { 103 | for(int i = 0; i < m; ++i) { 104 | int fx = find(edges[i].a); 105 | int fy = find(edges[i].b); 106 | if(fx != fy) { 107 | res += edges[i].w; 108 | fa[fx] = fy; 109 | ++cnt; 110 | } 111 | } 112 | } 113 | 114 | int main() { 115 | cin >> n >> m; 116 | fa.resize(n + 1); 117 | for(int i = 1; i <= n; ++i) fa[i] = i; 118 | for(int i = 1; i <= m; ++i) { 119 | int a, b, c; 120 | cin >> a >> b >> c; 121 | edges.push_back({a, b, c}); 122 | } 123 | 124 | sort(edges.begin(), edges.end()); //按边长排序 125 | 126 | kruskal(); 127 | 128 | if(cnt < n - 1) { //如果保留的边小于点数-1,则不能连通 129 | cout << "impossible"; 130 | return 0; 131 | } 132 | 133 | cout << res; 134 | 135 | return 0; 136 | } 137 | ``` 138 | 139 | 140 | 141 | ## 最短路 142 | 143 | ### Dijkstra 144 | 145 | 每次从 「未求出最短路径的点」中 **取出** 距离距离起点 **最小路径的点**,以这个点为桥梁 **刷新**「未求出最短路径的点」的距离 146 | 147 | * 只能做正权图 148 | * $O(mlogn)$ 149 | 150 | [850. Dijkstra求最短路 II - AcWing题库](https://www.acwing.com/problem/content/852/) 151 | 152 | ```c++ 153 | #include 154 | using namespace std; 155 | 156 | using PII = pair; 157 | 158 | int n, m; 159 | vector> graph; 160 | 161 | int dijkstra() { 162 | vector dist(n + 1, 0x3f3f3f3f); 163 | vector st(n + 1, false); // 判断是否已经求出最短路径 164 | 165 | priority_queue, greater> heap; // {edgeLen, node} 166 | dist[1] = 0; 167 | heap.push({0, 1}); 168 | 169 | while(!heap.empty()) { 170 | auto t = heap.top(); 171 | heap.pop(); 172 | 173 | auto [_, node] = t; 174 | 175 | if(st[node]) continue; 176 | st[node] = true; 177 | 178 | // 以 node 为桥梁更新每个出边 179 | for(const auto & [nextNode, edgeLen] : graph[node]) { 180 | if(dist[nextNode] > dist[node] + edgeLen) { 181 | dist[nextNode] = dist[node] + edgeLen; 182 | heap.push({dist[nextNode], nextNode}); 183 | } 184 | } 185 | } 186 | 187 | 188 | return dist[n] == 0x3f3f3f3f ? -1 : dist[n]; 189 | } 190 | 191 | int main() { 192 | cin >> n >> m; 193 | graph.resize(n + 1); 194 | for(int i = 0; i < m; ++i) { 195 | int a, b, c; 196 | cin >> a >> b >> c; 197 | graph[a].push_back({b, c}); 198 | } 199 | 200 | cout << dijkstra(); 201 | return 0; 202 | } 203 | ``` 204 | 205 | 206 | 207 | ### Spfa 208 | 209 | * 正权负权都可以 210 | 211 | * 期望 $O(km)$,最坏$O(nm)$ , 网格图会被卡 212 | 213 | ``` 214 | 215 | ``` 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | ## 搜索 224 | 225 | ### DFS 和 BFS 区别 226 | 227 | * BFS 适合找最短路,广度优先 228 | * DFS 适合搜索全部解,深度优先,递归 229 | -------------------------------------------------------------------------------- /数据结构算法/树.md: -------------------------------------------------------------------------------- 1 | # 树 2 | 3 | 树是由n个结点组成的有限集合 4 | 5 | n=0为空树 6 | 7 | n>0有一个结点为根只有后继结点,其他结点划分为m个互不相交的有限集合,每个集合本身是一棵树,称之为根的子树。每个子树的根节点有且只有一个直接前驱,但可以有0个或多个后继 8 | 9 | 度,树的度,叶子结点(终端结点),分支结点(非终端结点),内部结点,孩子,双亲,兄弟,祖先和子孙,层数,树的深度或高度,堂兄弟,无序树,有序树,结点的次序,最左结点(长子),次子,森林(删除一棵树的根就能得到一个森林) 10 | 11 | * $结点数=分支数+1$ 12 | * $分支数=\sum 度数不为0的结点数 * 度数$ 13 | * 树的存储形式:双亲表示法,孩子链表表示法,孩子兄弟表示法 14 | 15 | ## 二叉树 16 | 17 | **特点:** 18 | 19 | 每个结点最多有两棵子树 20 | 21 | 二叉树的子树有左右之分不可以颠倒,只有一棵子树树也必须分清左右子树 22 | 23 | **二叉树的性质:** 24 | 25 | * 第i层最多有$2^{i-1}$ 26 | 27 | * 深度为k的二叉树最多有 $2^k-1$ 结点 28 | 29 | * 叶节点树数为 $n_0$,度数为2的结点数为 $n_2$, $n_0=n_2+1$ 30 | 31 | * 令二叉树的分支数(所有结点的度数和)为 $B$ , $n_0+n_1+n_2=B+1=n=n_1+2*n_2+1$ 32 | 33 | * n个结点的二叉树:高最多为$n$,最低为$\lfloor log_2n \rfloor+1$(完全二叉树) 34 | 35 | * n个结点树转为二叉树后,没有右孩子的结点个数=分支结点个数+1,这个1是因为根节点也没有右孩子 36 | 37 | **满二叉树**: 38 | 39 | 一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。 40 | 41 | 除了叶结点外每一个结点都有左右子叶且叶子结点都处在最底层的二叉树 42 | 43 | 满二叉树:深度为k且有$2^k-1$个结点的二叉树,不存在度为1的结点 44 | 45 | 满m叉树:第k层有$2^{k-1}$个结点 46 | 47 | **完全二叉树:** 48 | 49 | 若设二叉树的高度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第h层有叶子结点,并且叶子结点都是从左到右依次排布,这就是完全二叉树。 50 | 51 | 叶节点只能出现在最下层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树。 52 | 53 | 深度为k,n个结点的二叉树,当且仅当每个结点和深度为k的满二叉树编号从1-n的结点一一对应 54 | 55 | 若设二叉树的高度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第h层有叶子结点,并且叶子结点都是从左到右依次排布,这就是完全二叉树。 56 | 57 | 性质 58 | 59 | * 叶子结点只可能在层次最大的两层上出现 60 | * 结点无左子女那么一定没有右子女 61 | * 度为1的结点数为$(n+1)\%2$,要么0要么1 62 | * 度为0的结点数(叶子结点)为$\lfloor(n+1)/2\rfloor$ 63 | * 度为2的结点数为$\lfloor(n+1)/2\rfloor-1$ 64 | * 二叉树:度数为0的结点数=度数为2的结点数+1 65 | * 编号最大的分支结点是$\lfloor n/2\rfloor$编号最小的叶子结点是$\lfloor n/2\rfloor$+1 66 | * 具有n个结点的完全二叉树的深度$k=\lfloor log_2n\rfloor+1$ 67 | * 如果对一棵有n个结点的完全二叉树按层次次序从1开始编号,则对任意结点i有: 68 | 69 | i=1,序号结点是根 70 | 71 | i>1,其双亲结点是$\lfloor i/2 \rfloor$ 72 | 73 | 2i<=n,序号为i的结点的左子女结点的序号为2i 74 | 75 | 2i>n序号为i的结点没有左子女 76 | 77 | 2i+1<=n,序号为i的结点的右子女结点的序号为2i+1 78 | 79 | 2i+1>n,序号为i的结点的没有右子女结点 80 | 81 | **堆** 82 | 83 | 堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。 84 | 85 | ## 二叉查找树(BST) 86 | 87 | [浅谈算法和数据结构: 七 二叉查找树](http://www.cnblogs.com/yangecnu/p/Introduce-Binary-Search-Tree.html) 88 | 89 | 二叉查找树的特点: 90 | 91 | 1. 若任意节点的左子树不空,则左子树上所有结点的 值均小于它的根结点的值; 92 | 2. 若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 93 | 3. 任意节点的左、右子树也分别为二叉查找树; 94 | 4. 没有键值相等的节点(no duplicate nodes)。 95 | 96 | ## 平衡二叉树(Self-balancing binary search tree) 97 | 98 | [平衡二叉树](https://baike.baidu.com/item/平衡二叉树)(百度百科,平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等) 99 | 100 | 平衡二叉树又被称为AVL树(区别于AVL算法),它是一棵二叉排序树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。 101 | 102 | ## 红黑树 103 | 104 | 红黑树特点: 105 | 106 | 1. 每个节点非红即黑; 107 | 2. 根节点总是黑色的; 108 | 3. 每个叶子节点都是黑色的空节点(NIL节点); 109 | 4. 如果节点是红色的,则它的子节点必须是黑色的(反之不一定); 110 | 5. 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。 111 | 112 | 红黑树的应用: 113 | 114 | TreeMap、TreeSet以及JDK1.8的HashMap底层都用到了红黑树。 115 | 116 | **为什么要用红黑树?** 117 | 118 | 简单来说红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。详细了解可以查看 [漫画:什么是红黑树?](https://juejin.im/post/5a27c6946fb9a04509096248)(也介绍到了二叉查找树,非常推荐) 119 | 120 | 推荐文章: 121 | 122 | * [漫画:什么是红黑树?](https://juejin.im/post/5a27c6946fb9a04509096248)(也介绍到了二叉查找树,非常推荐) 123 | * [寻找红黑树的操作手册](http://dandanlove.com/2018/03/18/red-black-tree/)(文章排版以及思路真的不错) 124 | * [红黑树深入剖析及Java实现](https://zhuanlan.zhihu.com/p/24367771)(美团点评技术团队) 125 | 126 | ## B,B+,B*树 127 | 128 | [二叉树学习笔记之B树、B+树、B*树](https://yq.aliyun.com/articles/38345) 129 | 130 | [《B-树,B+树,B*树详解》](https://blog.csdn.net/aqzwss/article/details/53074186) 131 | 132 | [《B-树,B+树与B*树的优缺点比较》](https://blog.csdn.net/bigtree_3721/article/details/73632405) 133 | 134 | B-树(或B树)是一种平衡的多路查找(又称排序)树,在文件系统中有所应用。主要用作文件的索引。其中的B就表示平衡(Balance) 135 | 136 | 1. B+ 树的叶子节点链表结构相比于 B- 树便于扫库,和范围检索。 137 | 2. B+树支持range-query(区间查询)非常方便,而B树不支持。这是数据库选用B+树的最主要原因。 138 | 3. B*树 是B+树的变体,B*树分配新结点的概率比B+树要低,空间使用率更高; 139 | 140 | ## LSM 树 141 | 142 | [[HBase\] LSM树 VS B+树](https://blog.csdn.net/dbanote/article/details/8897599) 143 | 144 | B+树最大的性能问题是会产生大量的随机IO 145 | 146 | 为了克服B+树的弱点,HBase引入了LSM树的概念,即Log-Structured Merge-Trees。 147 | 148 | [LSM树由来、设计思想以及应用到HBase的索引](http://www.cnblogs.com/yanghuahui/p/3483754.html) 149 | 150 | ## 哈夫曼树 151 | 152 | ### 什么是哈夫曼树 153 | 154 | 给定 N 个权值作为 N 个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。 155 | 156 | ### 路径和路径长度 157 | 158 | 在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。 159 | 160 | 通路中分支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1。 161 | 162 | 163 | 164 | ![img](https://pic1.zhimg.com/80/v2-f07e3b68c7798559e664eeb6cf20ba3c_1440w.webp) 165 | 166 | 167 | 168 | ### 节点的权和带权路径长度 169 | 170 | 若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积。 171 | 172 | 173 | 174 | ![img](https://pic2.zhimg.com/80/v2-561bb507a8adcbbed29281abac1d3485_1440w.webp) 175 | 176 | 177 | 178 | ### 树的带权路径长度 179 | 180 | 树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL。 181 | 182 | 如上图:数的带权路径长度为: 183 | 184 | WPL = (2+3) * 3 + 4 * 2 + 6 * 1 = 29 185 | 186 | 187 | 188 | ### 哈夫曼树的构造 189 | 190 | 假设有n个权值,则构造出的哈夫曼树有n个叶子结点。 n个权值分别设为 w1、w2、…、wn,则哈夫曼树的构造规则为: 191 | 192 | (1) 将w1、w2、…,wn看成是有n 棵树的森林(每棵树仅有一个结点); 193 | 194 | (2) 在森林中选出两个根结点的权值最小的树合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和; 195 | 196 | (3)从森林中删除选取的两棵树,并将新树加入森林; 197 | 198 | (4)重复(2)、(3)步,直到森林中只剩一棵树为止,该树即为所求得的哈夫曼树。 199 | 200 | 例如:对 2,3,4,6 这四个数进行构造 201 | 202 | 203 | 204 | ![img](https://pic3.zhimg.com/80/v2-5c5bdbcdbfbacf4565bb5e99445b964a_1440w.webp) 205 | 206 | 207 | 208 | ### 特性 209 | 210 | * 带权路径长度最短树 211 | * $WPL=\sum_{k=1}^nW_kL_k$ 212 | * 哈夫曼树只有度为0和2的结点 213 | * 权值大的结点离根结点近 214 | * n个叶子的哈夫曼树的形态一般不唯一,但是WPL相同 215 | * n个叶子的哈夫曼树有2n-1个结点 216 | * 总结点数个数(多于1时)不能为偶数 217 | * **n个权值均不相同的**(注意前提)字符构成哈夫曼树:树中一定没有度为1的结点,树中权值最小的结点一定是兄弟结点,树中任意非叶结点的权值一定不小于下一层任意结点的权值 218 | * 哈夫曼编码是最优的前缀编码(任何一个编码不是其他任何编码的前缀) 219 | 220 | 手撕哈夫曼编码 221 | 222 | ``` 223 | 224 | ``` 225 | 226 | 227 | 228 | ## 树状数组 229 | 230 | 231 | 232 | ## 线段树 233 | -------------------------------------------------------------------------------- /数据结构算法/海量数据.md: -------------------------------------------------------------------------------- 1 | # 海量数据 2 | 3 | ## 1、如何从大量的 URL 中找出相同的 URL? 4 | 5 | - 题目描述 6 | 7 | 给定 a、b 两个文件,各存放 50 亿个 URL,每个 URL 各占 64B,内存限制是 4G。请找出 a、b 两个文件共同的 URL。 8 | 9 | ### 解答思路 10 | 11 | #### 1. 分治策略 12 | 13 | 每个 URL 占 64B,那么 50 亿个 URL 占用的空间大小约为 320GB。 14 | 15 | > 5, 000, 000, 000 _64B ≈ 5GB_ 64 = 320GB 16 | 17 | 由于内存大小只有 4G,因此,我们不可能一次性把所有 URL 加载到内存中处理。对于这种类型的题目,一般采用**分治策略**,即:把一个文件中的 URL 按照某个特征划分为多个小文件,使得每个小文件大小不超过 4G,这样就可以把这个小文件读到内存中进行处理了。 18 | 19 | **思路如下**: 20 | 21 | 首先遍历文件 a,对遍历到的 URL 求 `hash(URL) % 1000` ,根据计算结果把遍历到的 URL 存储到 a0, a1, a2, ..., a999,这样每个大小约为 300MB。使用同样的方法遍历文件 b,把文件 b 中的 URL 分别存储到文件 b0, b1, b2, ..., b999 中。这样处理过后,所有可能相同的 URL 都在对应的小文件中,即 a0 对应 b0, ..., a999 对应 b999,不对应的小文件不可能有相同的 URL。那么接下来,我们只需要求出这 1000 对小文件中相同的 URL 就好了。 22 | 23 | 接着遍历 ai( `i∈[0,999]` ),把 URL 存储到一个 HashSet 集合中。然后遍历 bi 中每个 URL,看在 HashSet 集合中是否存在,若存在,说明这就是共同的 URL,可以把这个 URL 保存到一个单独的文件中。 24 | 25 | #### 2. 前缀树 26 | 27 | 一般而言,URL 的长度差距不会不大,而且前面几个字符,绝大部分相同。这种情况下,非常适合使用**字典树**(trie tree) 这种数据结构来进行存储,降低存储成本的同时,提高查询效率。 28 | 29 | ### 方法总结 30 | 31 | #### 分治策略 32 | 33 | 1. 分而治之,进行哈希取余; 34 | 2. 对每个子文件进行 HashSet 统计。 35 | 36 | #### 前缀树 37 | 38 | 1. 利用字符串的公共前缀来降低存储成本,提高查询效率。 39 | 40 | ## 2、如何从大量数据中找出高频词? 41 | 42 | ### 题目描述 43 | 44 | 有一个 1GB 大小的文件,文件里每一行是一个词,每个词的大小不超过 16B,内存大小限制是 1MB,要求返回频数最高的 100 个词(Top 100)。 45 | 46 | ### 解答思路 47 | 48 | 由于内存限制,我们依然无法直接将大文件的所有词一次读到内存中。因此,同样可以采用**分治策略**,把一个大文件分解成多个小文件,保证每个文件的大小小于 1MB,进而直接将单个小文件读取到内存中进行处理。 49 | 50 | **思路如下**: 51 | 52 | 首先遍历大文件,对遍历到的每个词 x,执行 `hash(x) % 5000` ,将结果为 i 的词存放到文件 ai 中。遍历结束后,我们可以得到 5000 个小文件。每个小文件的大小为 200KB 左右。如果有的小文件大小仍然超过 1MB,则采用同样的方式继续进行分解。 53 | 54 | 接着统计每个小文件中出现频数最高的 100 个词。最简单的方式是使用 HashMap 来实现。其中 key 为词,value 为该词出现的频率。具体方法是:对于遍历到的词 x,如果在 map 中不存在,则执行 `map.put(x, 1)` ;若存在,则执行 `map.put(x, map.get(x)+1)` ,将该词频数加 1。 55 | 56 | 上面我们统计了每个小文件单词出现的频数。接下来,我们可以通过维护一个**小顶堆**来找出所有词中出现频数最高的 100 个。具体方法是:依次遍历每个小文件,构建一个**小顶堆**,堆大小为 100。如果遍历到的词的出现次数大于堆顶词的出现次数,则用新词替换堆顶的词,然后重新调整为**小顶堆**,遍历结束后,小顶堆上的词就是出现频数最高的 100 个词。 57 | 58 | ### 方法总结 59 | 60 | 1. 分而治之,进行哈希取余; 61 | 2. 使用 HashMap 统计频数; 62 | 3. 求解**最大**的 TopN 个,用**小顶堆**;求解**最小**的 TopN 个,用**大顶堆**。 63 | 64 | ## 3、如何找出某一天访问百度网站最多的 IP? 65 | 66 | ### 题目描述 67 | 68 | 现有海量日志数据保存在一个超大文件中,该文件无法直接读入内存,要求从中提取某天访问百度次数最多的那个 IP。 69 | 70 | ### 解答思路 71 | 72 | 这道题只关心某一天访问百度最多的 IP,因此,可以首先对文件进行一次遍历,把这一天访问百度 IP 的相关信息记录到一个单独的大文件中。接下来采用的方法与上一题一样,大致就是先对 IP 进行哈希映射,接着使用 HashMap 统计重复 IP 的次数,最后计算出重复次数最多的 IP。 73 | 74 | > 注:这里只需要找出出现次数最多的 IP,可以不必使用堆,直接用一个变量 max 即可。 75 | 76 | ### 方法总结 77 | 78 | 1. 分而治之,进行哈希取余; 79 | 2. 使用 HashMap 统计频数; 80 | 3. 求解**最大**的 TopN 个,用**小顶堆**;求解**最小**的 TopN 个,用**大顶堆**。 81 | 82 | ## 4、如何在大量的数据中找出不重复的整数? 83 | 84 | ### 题目描述 85 | 86 | 在 2.5 亿个整数中找出不重复的整数。注意:内存不足以容纳这 2.5 亿个整数。 87 | 88 | ### 解答思路 89 | 90 | #### 方法一:分治法 91 | 92 | 与前面的题目方法类似,先将 2.5 亿个数划分到多个小文件,用 HashSet/HashMap 找出每个小文件中不重复的整数,再合并每个子结果,即为最终结果。 93 | 94 | #### 方法二:位图法 95 | 96 | **位图**,就是用一个或多个 bit 来标记某个元素对应的值,而键就是该元素。采用位作为单位来存储数据,可以大大节省存储空间。 97 | 98 | 位图通过使用位数组来表示某些元素是否存在。它可以用于快速查找,判重,排序等。不是很清楚?我先举个小例子。 99 | 100 | 假设我们要对 `[0,7]` 中的 5 个元素 (6, 4, 2, 1, 5) 进行排序,可以采用位图法。0~7 范围总共有 8 个数,只需要 8bit,即 1 个字节。首先将每个位都置 0: 101 | 102 | ``` 103 | 0 0 0 0 0 0 0 0Copy to clipboardErrorCopied 104 | ``` 105 | 106 | 然后遍历 5 个元素,首先遇到 6,那么将下标为 6 的位的 0 置为 1;接着遇到 4,把下标为 4 的位 的 0 置为 1: 107 | 108 | ``` 109 | 0 0 0 0 1 0 1 0Copy to clipboardErrorCopied 110 | ``` 111 | 112 | 依次遍历,结束后,位数组是这样的: 113 | 114 | ``` 115 | 0 1 1 0 1 1 1 0Copy to clipboardErrorCopied 116 | ``` 117 | 118 | 每个为 1 的位,它的下标都表示了一个数: 119 | 120 | ``` 121 | for i in range(8): 122 | if bits[i] == 1: 123 | print(i)Copy to clipboardErrorCopied 124 | ``` 125 | 126 | 这样我们其实就已经实现了排序。 127 | 128 | 对于整数相关的算法的求解,**位图法**是一种非常实用的算法。假设 int 整数占用 4B,即 32bit,那么我们可以表示的整数的个数为 232。 129 | 130 | **那么对于这道题**,我们用 2 个 bit 来表示各个数字的状态: 131 | 132 | - 00 表示这个数字没出现过; 133 | - 01 表示这个数字出现过一次(即为题目所找的不重复整数); 134 | - 10 表示这个数字出现了多次。 135 | 136 | 那么这 232 个整数,总共所需内存为 232*2b=1GB。因此,当可用内存超过 1GB 时,可以采用位图法。假设内存满足位图法需求,进行下面的操作: 137 | 138 | 遍历 2.5 亿个整数,查看位图中对应的位,如果是 00,则变为 01,如果是 01 则变为 10,如果是 10 则保持不变。遍历结束后,查看位图,把对应位是 01 的整数输出即可。 139 | 140 | 当然,本题中特别说明:**内存不足以容纳这 2.5 亿个整数**,2.5 亿个整数的内存大小为:2.5e8/1024/1024/1024 * 4=3.72GB, 如果内存大于 1GB,是可以通过位图法解决的。 141 | 142 | ### 方法总结 143 | 144 | **判断数字是否重复的问题**,位图法是一种非常高效的方法,当然前提是:内存要满足位图法所需要的存储空间。 145 | 146 | ## 5、如何在大量的数据中判断一个数是否存在? 147 | 148 | ### 题目描述 149 | 150 | 给定 40 亿个不重复的没排过序的 unsigned int 型整数,然后再给定一个数,如何快速判断这个数是否在这 40 亿个整数当中? 151 | 152 | ### 解答思路 153 | 154 | #### 方法一:分治法 155 | 156 | 依然可以用分治法解决,方法与前面类似,就不再次赘述了。 157 | 158 | #### 方法二:位图法 159 | 160 | 由于 unsigned int 数字的范围是 `[0, 1 << 32)`,我们用 `1<<32=4,294,967,296` 个 bit 来表示每个数字。初始位均为 0,那么总共需要内存:4,294,967,296b≈512M。 161 | 162 | 我们读取这 40 亿个整数,将对应的 bit 设置为 1。接着读取要查询的数,查看相应位是否为 1,如果为 1 表示存在,如果为 0 表示不存在。 163 | 164 | ### 方法总结 165 | 166 | **判断数字是否存在、判断数字是否重复的问题**,位图法是一种非常高效的方法。6、毒药毒白鼠,找出哪个瓶子中是毒药 167 | 168 | 有1000个一模一样的瓶子,其中有999瓶是普通的水,有1瓶是毒药。任何喝下毒药的生命都会在一星期之后死亡。现在你只有10只小白鼠和1个星期的时间,如何检验出哪个瓶子有毒药? 169 | 170 | ## 6、如何查询最热门的查询串? 171 | 172 | ### 题目描述 173 | 174 | 搜索引擎会通过日志文件把用户每次检索使用的所有查询串都记录下来,每个查询串的长度不超过 255 字节。 175 | 176 | 假设目前有 1000w 个记录(这些查询串的重复度比较高,虽然总数是 1000w,但如果除去重复后,则不超过 300w 个)。请统计最热门的 10 个查询串,要求使用的内存不能超过 1G。(一个查询串的重复度越高,说明查询它的用户越多,也就越热门。) 177 | 178 | ### 解答思路 179 | 180 | 每个查询串最长为 255B,1000w 个串需要占用 约 2.55G 内存,因此,我们无法将所有字符串全部读入到内存中处理。 181 | 182 | #### 方法一:分治法 183 | 184 | 分治法依然是一个非常实用的方法。 185 | 186 | 划分为多个小文件,保证单个小文件中的字符串能被直接加载到内存中处理,然后求出每个文件中出现次数最多的 10 个字符串;最后通过一个小顶堆统计出所有文件中出现最多的 10 个字符串。 187 | 188 | 方法可行,但不是最好,下面介绍其他方法。 189 | 190 | #### 方法二:HashMap 法 191 | 192 | 虽然字符串总数比较多,但去重后不超过 300w,因此,可以考虑把所有字符串及出现次数保存在一个 HashMap 中,所占用的空间为 300w*(255+4)≈777M(其中,4 表示整数占用的 4 个字节)。由此可见,1G 的内存空间完全够用。 193 | 194 | **思路如下**: 195 | 196 | 首先,遍历字符串,若不在 map 中,直接存入 map,value 记为 1;若在 map 中,则把对应的 value 加 1,这一步时间复杂度 `O(N)` 。 197 | 198 | 接着遍历 map,构建一个 10 个元素的小顶堆,若遍历到的字符串的出现次数大于堆顶字符串的出现次数,则进行替换,并将堆调整为小顶堆。 199 | 200 | 遍历结束后,堆中 10 个字符串就是出现次数最多的字符串。这一步时间复杂度 `O(Nlog10)` 。 201 | 202 | #### 方法三:前缀树法 203 | 204 | 方法二使用了 HashMap 来统计次数,当这些字符串有大量相同前缀时,可以考虑使用前缀树来统计字符串出现的次数,树的结点保存字符串出现次数,0 表示没有出现。 205 | 206 | **思路如下**: 207 | 208 | 在遍历字符串时,在前缀树中查找,如果找到,则把结点中保存的字符串次数加 1,否则为这个字符串构建新结点,构建完成后把叶子结点中字符串的出现次数置为 1。 209 | 210 | 最后依然使用小顶堆来对字符串的出现次数进行排序。 211 | 212 | ### 方法总结 213 | 214 | 前缀树经常被用来统计字符串的出现次数。它的另外一个大的用途是字符串查找,判断是否有重复的字符串等。 215 | 216 | ## 7、如何统计不同电话号码的个数? 217 | 218 | ### 题目描述 219 | 220 | 已知某个文件内包含一些电话号码,每个号码为 8 位数字,统计不同号码的个数。 221 | 222 | ### 解答思路 223 | 224 | 这道题本质还是求解**数据重复**的问题,对于这类问题,一般首先考虑位图法。 225 | 226 | 对于本题,8 位电话号码可以表示的号码个数为 108 个,即 1 亿个。我们每个号码用一个 bit 来表示,则总共需要 1 亿个 bit,内存占用约 12M。 227 | 228 | **思路如下**: 229 | 230 | 申请一个位图数组,长度为 1 亿,初始化为 0。然后遍历所有电话号码,把号码对应的位图中的位置置为 1。遍历完成后,如果 bit 为 1,则表示这个电话号码在文件中存在,否则不存在。bit 值为 1 的数量即为 不同电话号码的个数。 231 | 232 | ### 方法总结 233 | 234 | 求解数据重复问题,记得考虑位图法。 235 | 236 | ## 8、 如何从 5 亿个数中找出中位数? 237 | 238 | ### 题目描述 239 | 240 | 从 5 亿个数中找出中位数。数据排序后,位置在最中间的数就是中位数。当样本数为奇数时,中位数为 第 `(N+1)/2` 个数;当样本数为偶数时,中位数为 第 `N/2` 个数与第 `1+N/2` 个数的均值。 241 | 242 | ### 解答思路 243 | 244 | 如果这道题没有内存大小限制,则可以把所有数读到内存中排序后找出中位数。但是最好的排序算法的时间复杂度都为 `O(NlogN)` 。这里使用其他方法。 245 | 246 | #### 方法一:双堆法 247 | 248 | 维护两个堆,一个大顶堆,一个小顶堆。大顶堆中最大的数**小于等于**小顶堆中最小的数;保证这两个堆中的元素个数的差不超过 1。 249 | 250 | 若数据总数为**偶数**,当这两个堆建好之后,**中位数就是这两个堆顶元素的平均值**。当数据总数为**奇数**时,根据两个堆的大小,**中位数一定在数据多的堆的堆顶**。 251 | 252 | ``` 253 | class MedianFinder { 254 | 255 | private PriorityQueue maxHeap; 256 | private PriorityQueue minHeap; 257 | 258 | /** initialize your data structure here. */ 259 | public MedianFinder() { 260 | maxHeap = new PriorityQueue<>(Comparator.reverseOrder()); 261 | minHeap = new PriorityQueue<>(Integer::compareTo); 262 | } 263 | 264 | public void addNum(int num) { 265 | if (maxHeap.isEmpty() || maxHeap.peek() > num) { 266 | maxHeap.offer(num); 267 | } else { 268 | minHeap.offer(num); 269 | } 270 | 271 | int size1 = maxHeap.size(); 272 | int size2 = minHeap.size(); 273 | if (size1 - size2 > 1) { 274 | minHeap.offer(maxHeap.poll()); 275 | } else if (size2 - size1 > 1) { 276 | maxHeap.offer(minHeap.poll()); 277 | } 278 | } 279 | 280 | public double findMedian() { 281 | int size1 = maxHeap.size(); 282 | int size2 = minHeap.size(); 283 | 284 | return size1 == size2 285 | ? (maxHeap.peek() + minHeap.peek()) * 1.0 / 2 286 | : (size1 > size2 ? maxHeap.peek() : minHeap.peek()); 287 | } 288 | }Copy to clipboardErrorCopied 289 | ``` 290 | 291 | > 见 LeetCode No.295: 292 | 293 | 以上这种方法,需要把所有数据都加载到内存中。当数据量很大时,就不能这样了,因此,这种方法**适用于数据量较小的情况**。5 亿个数,每个数字占用 4B,总共需要 2G 内存。如果可用内存不足 2G,就不能使用这种方法了,下面介绍另一种方法。 294 | 295 | #### 方法二:分治法 296 | 297 | 分治法的思想是把一个大的问题逐渐转换为规模较小的问题来求解。 298 | 299 | 对于这道题,顺序读取这 5 亿个数字,对于读取到的数字 num,如果它对应的二进制中最高位为 1,则把这个数字写到 f1 中,否则写入 f0 中。通过这一步,可以把这 5 亿个数划分为两部分,而且 f0 中的数都大于 f1 中的数(最高位是符号位)。 300 | 301 | 划分之后,可以非常容易地知道中位数是在 f0 还是 f1 中。假设 f1 中有 1 亿个数,那么中位数一定在 f0 中,且是在 f0 中,从小到大排列的第 1.5 亿个数与它后面的一个数的平均值。 302 | 303 | > **提示**,5 亿数的中位数是第 2.5 亿与右边相邻一个数求平均值。若 f1 有一亿个数,那么中位数就是 f0 中从第 1.5 亿个数开始的两个数求得的平均值。 304 | 305 | 对于 f0 可以用次高位的二进制继续将文件一分为二,如此划分下去,直到划分后的文件可以被加载到内存中,把数据加载到内存中以后直接排序,找出中位数。 306 | 307 | > **注意**,当数据总数为偶数,如果划分后两个文件中的数据有相同个数,那么中位数就是数据较小的文件中的最大值与数据较大的文件中的最小值的平均值。 308 | 309 | ### 方法总结 310 | 311 | 分治法,真不错! 312 | 313 | ## 9、如何按照 query 的频度排序? 314 | 315 | ### 题目描述 316 | 317 | 有 10 个文件,每个文件大小为 1G,每个文件的每一行存放的都是用户的 query,每个文件的 query 都可能重复。要求按照 query 的频度排序。 318 | 319 | ### 解答思路 320 | 321 | 如果 query 的重复度比较大,可以考虑一次性把所有 query 读入内存中处理;如果 query 的重复率不高,那么可用内存不足以容纳所有的 query,这时候就需要采用分治法或其他的方法来解决。 322 | 323 | #### 方法一:HashMap 法 324 | 325 | 如果 query 重复率高,说明不同 query 总数比较小,可以考虑把所有的 query 都加载到内存中的 HashMap 中。接着就可以按照 query 出现的次数进行排序。 326 | 327 | #### 方法二:分治法 328 | 329 | 分治法需要根据数据量大小以及可用内存的大小来确定问题划分的规模。对于这道题,可以顺序遍历 10 个文件中的 query,通过 Hash 函数 `hash(query) % 10` 把这些 query 划分到 10 个小文件中。之后对每个小文件使用 HashMap 统计 query 出现次数,根据次数排序并写入到另外一个单独文件中。 330 | 331 | 接着对所有文件按照 query 的次数进行排序,这里可以使用归并排序(由于无法把所有 query 都读入内存,因此需要使用外排序)。 332 | 333 | ### 方法总结 334 | 335 | - 内存若够,直接读入进行排序; 336 | 337 | - 内存不够,先划分为小文件,小文件排好序后,整理使用外排序进行归并。 338 | 339 | ## 10、如何找出排名前 500 的数? 340 | 341 | ### 题目描述 342 | 343 | 有 20 个数组,每个数组有 500 个元素,并且有序排列。如何在这 20*500 个数中找出前 500 的数? 344 | 345 | ### 解答思路 346 | 347 | 对于 TopK 问题,最常用的方法是使用堆排序。对本题而言,假设数组降序排列,可以采用以下方法: 348 | 349 | 首先建立大顶堆,堆的大小为数组的个数,即为 20,把每个数组最大的值存到堆中。 350 | 351 | 接着删除堆顶元素,保存到另一个大小为 500 的数组中,然后向大顶堆插入删除的元素所在数组的下一个元素。 352 | 353 | 重复上面的步骤,直到删除完第 500 个元素,也即找出了最大的前 500 个数。 354 | 355 | > 为了在堆中取出一个数据后,能知道它是从哪个数组中取出的,从而可以从这个数组中取下一个值,可以把数组的指针存放到堆中,对这个指针提供比较大小的方法。 356 | 357 | ``` 358 | import lombok.Data; 359 | 360 | import java.util.Arrays; 361 | import java.util.PriorityQueue; 362 | 363 | /** 364 | * @author https://github.com/yanglbme 365 | */ 366 | @Data 367 | public class DataWithSource implements Comparable { 368 | /** 369 | * 数值 370 | */ 371 | private int value; 372 | 373 | /** 374 | * 记录数值来源的数组 375 | */ 376 | private int source; 377 | 378 | /** 379 | * 记录数值在数组中的索引 380 | */ 381 | private int index; 382 | 383 | public DataWithSource(int value, int source, int index) { 384 | this.value = value; 385 | this.source = source; 386 | this.index = index; 387 | } 388 | 389 | /** 390 | * 391 | * 由于 PriorityQueue 使用小顶堆来实现,这里通过修改 392 | * 两个整数的比较逻辑来让 PriorityQueue 变成大顶堆 393 | */ 394 | @Override 395 | public int compareTo(DataWithSource o) { 396 | return Integer.compare(o.getValue(), this.value); 397 | } 398 | } 399 | 400 | class Test { 401 | public static int[] getTop(int[][] data) { 402 | int rowSize = data.length; 403 | int columnSize = data[0].length; 404 | 405 | // 创建一个columnSize大小的数组,存放结果 406 | int[] result = new int[columnSize]; 407 | 408 | PriorityQueue maxHeap = new PriorityQueue<>(); 409 | for (int i = 0; i < rowSize; ++i) { 410 | // 将每个数组的最大一个元素放入堆中 411 | DataWithSource d = new DataWithSource(data[i][0], i, 0); 412 | maxHeap.add(d); 413 | } 414 | 415 | int num = 0; 416 | while (num < columnSize) { 417 | // 删除堆顶元素 418 | DataWithSource d = maxHeap.poll(); 419 | result[num++] = d.getValue(); 420 | if (num >= columnSize) { 421 | break; 422 | } 423 | 424 | d.setValue(data[d.getSource()][d.getIndex() + 1]); 425 | d.setIndex(d.getIndex() + 1); 426 | maxHeap.add(d); 427 | } 428 | return result; 429 | 430 | } 431 | 432 | public static void main(String[] args) { 433 | int[][] data = { 434 | {29, 17, 14, 2, 1}, 435 | {19, 17, 16, 15, 6}, 436 | {30, 25, 20, 14, 5}, 437 | }; 438 | 439 | int[] top = getTop(data); 440 | System.out.println(Arrays.toString(top)); // [30, 29, 25, 20, 19] 441 | } 442 | }Copy to clipboardErrorCopied 443 | ``` 444 | 445 | ### 方法总结 446 | 447 | 求 TopK,不妨考虑一下堆排序? 448 | -------------------------------------------------------------------------------- /智力题场景题/场景题.md: -------------------------------------------------------------------------------- 1 | # 场景题 2 | 3 | ## 敏感词过滤 TRIE 树 4 | -------------------------------------------------------------------------------- /测试/流量回放.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PokIsemaine/interview/bd5da05b557897a71361f8dbb9a6a6d25cf28d46/测试/流量回放.md -------------------------------------------------------------------------------- /测试/自动化测试与测试工具.md: -------------------------------------------------------------------------------- 1 | ## 回答 2 | 3 | ### 自动化测试 4 | 5 | #### 你觉得自动化测试有什么意义,都需要做些什么 6 | 7 | 解题思路 8 | 9 | 自动化测试的意义在于 10 | 11 | 1、可以对程序的新版本自动执行回归测试 12 | 13 | 2、可以执行手工测试困难或者不可能实现的测试,如压力测试,并发测试, 14 | 15 | 3、能够更好的利用资源,节省时间和人力 执行自动化测试之前首先判断这个项目是不是和推广自动化测试,然后对项目做需求分析,指定测试计划,搭建自动化测试框架,设计测试用例,执行测试,评估 16 | 17 | 18 | 19 | #### 请你说一下能不能用机器学习去进行自动化测试,如何监控异常流量,如果是脉冲呢,如何和正常流量作区分 20 | 21 | 解题思路 22 | 23 | 如何用机器学习去进行自动化测试,就是让自动化测试变得更加智能,在自动化测试过程中,当测试功能模块越来越多,没有太多的时间去全部覆盖,我们可以采用归纳学习的方式,基于自动化测试的执行结果或者手工测试执行的结果为数据输入,然后基于一定的模型(例如:以通过率和模块的重要率计算的平均值)得出测试推荐模块,或者当要执行一个功能模块时,基于历史数据和模型(bug出现的错误相关性,功能相关性等)计算出与该功能模块相关性最大模块,并推荐测试。 如何监控异常流量 1、抓包 tcpdump -i eth0 -w server.cap 对包文件使用第三方工具如:wireshark做分析 2、iftop yum install iftop 3、iptraf yum install iptraf –y 或 yum install iptraf-ng -y 启动命令ifptraf-ng 24 | 25 | #### 请说一下手动测试与自动化测试的优缺点 26 | 27 | 解题思路 28 | 29 | 手工测试缺点: 30 | 31 | 1、重复的手工回归测试,代价昂贵、容易出错。 32 | 33 | 2、依赖于软件测试人员的能力。 手工测试优点: 34 | 35 | 1、测试人员具有经验和对错误的猜测能力。 36 | 37 | 2、测试人员具有审美能力和心理体验。 38 | 39 | 3、测试人员具有是非判断和逻辑推理能力。 自动化测试的优点: 40 | 41 | 1、对程序的回归测试更方便。这可能是自动化测试最主要的任务,特别是在程序修改比较频繁时,效果是非常明显的。由于回归测试的动作和用例是完全设计好的,测试期望的结果也是完全可以预料的,将回归测试自动运行,可以极大提高测试效率,缩短回归测试时间。 42 | 43 | 2、可以运行更多更繁琐的测试。自动化的一个明显的好处是可以在较少的时间内运行更多的测试。 44 | 45 | 3、可以执行一些手工测试困难或不可能进行的测试。比如,对于大量用户的测试,不可能同时让足够多的测试人员同时进行测试,但是却可以通过自动化测试模拟同时有许多用户,从而达到测试的目的。 46 | 47 | 4、更好地利用资源。将繁琐的任务自动化,可以提高准确性和测试人员的积极性,将测试技术人员解脱出来投入更多精力设计更好的测试用例。有些测试不适合于自动测试,仅适合于手工测试,将可自动测试的测试自动化后,可以让测试人员专注于手工测试部分,提高手工测试的效率。 48 | 49 | 5、测试具有一致性和可重复性。由于测试是自动执行的,每次测试的结果和执行的内容的一致性是可以得到保障的,从而达到测试的可重复的效果。 50 | 51 | 6、测试的复用性。由于自动测试通常采用脚本技术,这样就有可能只需要做少量的甚至不做修改,实现在不同的测试过程中使用相同的用例。 7、增加软件信任度。由于测试是自动执行的,所以不存在执行过程中的疏忽和错误,完全取决于测试的设计质量。一旦软件通过了强有力的自动测试后,软件的信任度自然会增加。 自动化测试的缺点: 52 | 53 | 1、不能取代手工测试 54 | 55 | 2、手工测试比自动测试发现的缺陷更多 56 | 57 | 3、对测试质量的依赖性极大 58 | 59 | 4、测试自动化不能提高有效性 60 | 61 | 5、测试自动化可能会制约软件开发。由于自动测试比手动测试更脆弱,所以维护会受到限制,从而制约软件的开发。 62 | 63 | 6、工具本身并无想像力 64 | 65 | #### 请你说一说你知道的自动化测试框架 66 | 67 | 解题思路 68 | 69 | 1、模块化测试框架 模块化测试脚本框架(TEST MODulARITY FRAMEWORK)需要创建小而独立的可以描述的模块、片断以及待测应用程序的脚本。这些树状结构的小脚本组合起来,就能组成能用于特定的测试用例的脚本。在五种框架中,模块化框架是最容易掌握和使用的。在一个组件上方建立一个抽象层使其在余下的应用中隐藏起来,这是众所周知的编程技巧。这样应用同组件中的修改隔离开来,提供了程序设计的模块化特性。模块化测试脚本框架使用这一抽象或者封装的原理来提高自动测试组合的可维护性和可升级性。 2、测试库框架 测试库框架(Test Library Architecture)与模块化测试脚本框架很类似,并且具有同样的优点。不同的是测试库框架把待测应用程序分解为过程和函数而不是脚本。这个框架需要创建描述模块、片断以及待测应用程序的功能库文件。 3、关键字驱动或表驱动的测试框架 对于一个独立于应用的自动化框架,关键字驱动(KEYWORD DRIVEN)I9LJJ试和表驱动(TABLE DRIVEN)测试是可以互换的术语。这个框架需要开发数据表和关键字。这些数据表和关键字独立于执行它们的测试自动化工具,并可以用来“驱动"待测应用程序和数据的测试脚本代码,关键宇驱动测试看上去与手工测试用例很类似。在一个关键字驱动测试中,把待测应用程序的功能和每个测试的执行步骤一起写到一个表中。这个测试框架可以通过很少的代码来产生大量的测试用例。同样的代码在用数据表来产生各个测试用例的同时被复用。 4、数据驱动测试框架 数据驱动(DATA DRIVEN),LJ试是一个框架。在这里测试的输入和输出数据是从数据文件中读取(数据池,ODBC源,CSV文件,EXCEL文件,ADO对象等)并且通过捕获工具生成或者手工生成的代码脚本被载入到变量中。在这个框架中,变量不仅被用来存放输入值还被用来存放输出的验证值。整个程序中,测试脚本来读取数值文件,记载测试状态和信息。这类似于表驱动测试,在表驱动测 试中,它的测试用例是包含在数据文件而不是在脚本中,对于数据而言,脚本仅仅是一个“驱动器”,或者是一个传送机构。然而,数据驱动测试不同于表驱动测试,尽管导航数据并不包含在表结构中。在数据驱动测试中,数据文件中只包含测试数据。这个框架意图减少需要执行所有测试用例所需要的总的测试脚本数。数据驱动需要很少的代码来产生大量的测试用例,这与表驱动极其类似。 5、混合测试自动化(Hybrid Test Automation)框架 最普遍的执行框架是上面介绍的所有技术的一个结合,取其长处,弥补其不足。这个混合测试框架是由大部分框架随着时间并经过若干项目演化而来的 70 | 71 | ### 测试工具 72 | 73 | #### 请你说一下jmeter 74 | 75 | 解题思路 76 | 77 | Jmeter:Apache JMeter是Apache组织开发的基于Java的压力测试工具。用于对软件做压力测试,它最初被设计用于Web应用测试,但后来扩展到其他测试领域。 它可以用于测试静态和动态资源,例如静态文件、Java 小服务程序、CGI 脚本、Java 对象、数据库、FTP 服务器, 等等。JMeter 可以用于对服务器、网络或对象模拟巨大的负载,来自不同压力类别下测试它们的强度和分析整体性能。另外,JMeter能够对应用程序做功能/回归测试,通过创建带有断言的脚本来验证你的程序返回了你期望的结果。为了最大限度的灵活性,JMeter允许使用正则表达式创建断言。 为什么使用Jmeter: • 开源免费,基于Java编写,可集成到其他系统可拓展各个功能插件 • 支持接口测试,压力测试等多种功能,支持录制回放,入门简单 • 相较于自己编写框架或其他开源工具,有较为完善的UI界面,便于接口调试 • 多平台支持,可在Linux,Windows,Mac上运行 用例生成与导出: Jmeter的用例格式为jmx文件,实际为xml格式,感兴趣可以学习下自己定制生成想要的jmx文件。 生成原则: 每个功能模块为一个独立的jmx文件。增加可维护性。(尽量不要将一个jmx文件放入太多功能,后期维护成本会很高。) 模块的私有变量保存在模块中,多模块共有的(例如服务器ip端口等)可以考虑存在单独的文件中读取。 接口测试不要放太多线程,毕竟不是做压力测试,意义也不大。 导出方法: 编写测试用例 文件——保存为——确定: Jmeter运行模式及参数 GUI模式 打开已有的jmx文件(文件——打开) 点击启动按钮运行 命令行模式 依赖: 配置jmeter环境变量(windows下为将${jmeterhome}/bin加入Path变量) 如果未加入环境变量,在执行的时候可以直接给出全路径或在${jmeterhome}/bin下执行 命令: jmeter -n -t -l 参数: -h 帮助 -> 打印出有用的信息并退出 -n 非 GUI 模式 -> 在非 GUI 模式下运行 JMeter -t 测试文件 -> 要运行的 JMeter 测试脚本文件 -l jtl文件 -> 记录结果的文件 -r 远程执行 -> 启动远程服务 -H 代理主机 -> 设置 JMeter 使用的代理主机 -P 代理端口 -> 设置 JMeter 使用的代理主机的端口号 -j 日志文件->设置JMeter日志文件的名称 实例: JMeter -n -t my_test.jmx -l log.jtl -H my.proxy.server -P 8000 执行步骤: JMeter 默认去当前目录寻找脚本文件,并把日志记录在当前目录。比如你在 C:\tools\apache-jmeter-2.11\bin 目录下执行以上命令,JMeter 会去该目录下寻找 test.jmx 脚本并把执行结果放在该目录。如果你的脚本在其他目录,而且想要把执行结果放在另外文件夹,可以使用绝对路径告诉 JMeter。 执行结果查看: GUI界面打开聚合报告 在GUI界面创建一个聚合报告 聚合报告界面点击浏览,选中生成的.jtl文件,打开 Jmeter使用 Jmeter创建接口测试计划实例 测试用例应该作为测试的基础内容,而用例的结构可能划分,则是用例的基础(忽然在这里想说一下,用例仅仅是一项测试活动的纲要,有最好,没有的话能保证质量也OK。更不用说用例的格式问题,无论是表格还是导图,其实都无所谓!本文的用例是指jmx文件中的控件结构)。 • 模块名称(测试计划):每个模块独立划分为一个jmx文件(例如登陆模块),最好与接口类一一对应。对应的服务器信息,数据库信息等可存在这里。 • 数据准备:用于测试数据的准备(例如账号信息)。 • 结果查看:用于放置需要查看结果的控件(例如结果树)。 • 线程组:所有的接口测试用例放在线程组下,集中定义线程等信息 • 获取线程对应测试数据:用于获取针对独立线程的测试数据,例如在数据准备里面获得了账号信息,在这里根据账号信息去数据库获取对应的名称,ID等信息。 • 请求名称:用简单控制器为文件夹,内有不同的请求。简单控制器为一个独立的接口,不同请求对应不同的代码路径(例如成功请求,失败请求等)。建议请求名称最好用英文形式,否则后期持续集成或许会出现问题(no zuo no die!)。 • 在每条请求内放置正则匹配(用于应对需要返回值作为下次请求的参数的情况)以及断言。 78 | 79 | #### 请你来聊一聊appium断言 80 | 81 | 解题思路 82 | 83 | appium-unittest单元测试框架中,TestCase 类提供了一些方法来检查并报告故障,如下图 : 上面所提供的断言方法(assertRses(), assertRsesRegexp()除外)接收 msg 参数,如果指定, 将体作为失败的错误信息。 try: num = input("Enter a number:") assert (num == 10), "The number is not 10!" except AssertionError,msg: print msg print ("Sadly, num not equals to 10") 在上面的程序中,运行到的python 的异常与断言。通过 raw_input()方法要求用户输入一个数字,通过 arrsert 判断用户输入的 num 是否等于10 ; 通过 python 的 AssertionError 类型的异常来实捕获这个异常, msg 接收异常信息并打印, 注意, msg 所结构的异常信息是我们自定义的("The number is not10!") 。 •assertEqual(first, second, msg=None):判断 first 和 second 的值是否相等,如果不相等则测试失败,msg 用于定义失败后所抛出的异 常信息。 •assertNotEqual(first, second, msg=None):测试 first 和 second 不相等,如果相等,则测试失败。 •assertTure(expr,msg=None)、assertFalse(expr,msg=None):测试 expr 为 Ture(或为 False) 以下为python 2.7 版新增的断言方法: •assertIs(first, second, msg=None)、assertIsNot(first, second, msg=None):测试的 first 和 second 是(或 不是)相同的对象。 •assertIsNone(expr, msg=None)、assertIsNotNone(expr, msg=None):测试 expr 是(或 不是)为 None •assertIn(first, second, msg=None)、assertNotIn(first, second, msg=None):测试 first 是(或不是)在 second 中。second 包含是否包含 first 。 •assertIsInstance(obj, cls, msg=None)、assertNotIsInstance(obj, cls, msg=None):测试 obj 不(或 不是)cls 的一个实例。 (obj 和 cls 可以是一个类或元组) ,要检查他们的类型使用 assertIs(type(obj), cls)。 84 | 85 | #### 请问你有用过什么测试工具吗,用过哪些? 86 | 87 | 解题思路 88 | 89 | 自动化测试工具用过selenium和appium 性能测试工具有用过Jmeter 90 | 91 | #### 请你说一说app测试的工具 92 | 93 | 解题思路 94 | 95 | 功能测试自动化 a) 轻量接口自动化测试 jmeter, b) APP UI层面的自动化 android:UI Automator Viewer,Android Junit,Instrumentation,UIAutomator, iOS:基于Instrument的iOS UI自动化, 性能测试 a) Web前端性能测试 网络抓包工具:Wireshark 网页文件大小 webpagetest pagespeed insight chrome adb b) APP端性能测试 Android内存占用分析:MAT iOS内存问题分析:ARC模式 Android WebView性能分析: iOS WebView性能分析 c) 后台服务性能测试 负载,压力,耐久性 可拓展性,基准 工具:apacheAB,Jmeter,LoadRunner, 专项测试 a) 兼容性测试 手工测试:操作系统,分辨率,rom,网络类型 云平台:testin,脚本编写,Android。 b) 流量测试 Android自带的流量管理, iOS自带的Network tcpdump抓包 WiFi代理抓包:Fiddler 流量节省方法:压缩数据,json优于xml;WebP优于传统的JPG,PNG;控制访问的频次;只获取必要的数据;缓存; c) 电量测试 基于测试设备的方法,购买电量表进行测试。 GSam Battery Monitoe Pro iOS基于Instrument Energy工具 d) 弱网络测试 手机自带的网络状况模拟工具 基于代理的弱网络的模拟: 工具:windows:Network Delay Simulator Mac:Network Link Conditioner -------------------------------------------------------------------------------- /计算机网络/HTTP 演化.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PokIsemaine/interview/bd5da05b557897a71361f8dbb9a6a6d25cf28d46/计算机网络/HTTP 演化.png -------------------------------------------------------------------------------- /计算机网络/HTTP 演化.xmind: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PokIsemaine/interview/bd5da05b557897a71361f8dbb9a6a6d25cf28d46/计算机网络/HTTP 演化.xmind -------------------------------------------------------------------------------- /计算机网络/HTTPS 优化.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PokIsemaine/interview/bd5da05b557897a71361f8dbb9a6a6d25cf28d46/计算机网络/HTTPS 优化.png -------------------------------------------------------------------------------- /计算机网络/HTTPS 优化.xmind: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PokIsemaine/interview/bd5da05b557897a71361f8dbb9a6a6d25cf28d46/计算机网络/HTTPS 优化.xmind -------------------------------------------------------------------------------- /计算机网络/工具命令.md: -------------------------------------------------------------------------------- 1 | # 工具命令 2 | 3 | ## 问题 4 | 5 | * 如何在 Linux 系统中查看 TCP 状态? 6 | * TTL是什么?有什么用处,通常那些工具会用到它?(ping? traceroute? ifconfig? netstat?) 7 | 8 | ## 回答 9 | 10 | ### 如何在 Linux 系统中查看 TCP 状态? 11 | 12 | TCP 的连接状态查看,在 Linux 可以通过 `netstat -napt` 命令查看。 13 | 14 | ![TCP 连接状态查看](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9jZG4uanNkZWxpdnIubmV0L2doL3hpYW9saW5jb2Rlci9JbWFnZUhvc3QyLyVFOCVBRSVBMSVFNyVBRSU5NyVFNiU5QyVCQSVFNyVCRCU5MSVFNyVCQiU5Qy9UQ1AtJUU0JUI4JTg5JUU2JUFDJUExJUU2JThGJUExJUU2JTg5JThCJUU1JTkyJThDJUU1JTlCJTlCJUU2JUFDJUExJUU2JThDJUE1JUU2JTg5JThCLzE4LmpwZw?x-oss-process=image/format,png) 15 | 16 | ### **ping命令查看丢包** 17 | 18 | 比如我们知道目的地的域名是 `baidu.com`。想知道你的机器到baidu服务器之间,有没有产生丢包行为。可以使用ping命令。 19 | 20 | ![ping查看丢包](https://img-blog.csdnimg.cn/img_convert/56bdca9995c0c2a343b2b73b67933b78.png) 21 | 22 | 倒数第二行里有个`100% packet loss`,意思是丢包率100%。 23 | 24 | 但这样其实你只能知道**你的机器和目的机器之间有没有丢包。** 25 | 26 | **那如果你想知道你和目的机器之间的这条链路,哪个节点丢包了,有没有办法呢?** 27 | 28 | 有。 29 | 30 | ### **mtr命令** 31 | 32 | mtr命令可以查看到你的机器和目的机器之间的每个节点的丢包情况。 33 | 34 | 像下面这样执行命令。 35 | 36 | ![mtr_icmp](https://img-blog.csdnimg.cn/img_convert/4a2d8dbfb648bcced864fb653af9f036.png) 37 | 38 | 其中 -r 是指report,以报告的形式打印结果。 39 | 40 | 可以看到`Host`那一列,出现的都是链路中间每一跳的机器,`Loss`的那一列就是指这一跳对应的丢包率。 41 | 42 | 需要注意的是,中间有一些是host是`???`,那个是因为**mtr默认用的是ICMP包**,有些节点限制了**ICMP包**,导致不能正常展示。 43 | 44 | 我们可以在mtr命令里加个`-u`,也就是使用**udp包**,就能看到部分???对应的IP。 45 | 46 | ![mtr-udp](https://img-blog.csdnimg.cn/img_convert/0650adc524ab7d82028dc83cfc9961e1.png) 47 | 48 | 把**ICMP包和UDP包的结果**拼在一起看,就是**比较完整**的链路图了。 49 | 50 | 还有个小细节,`Loss`那一列,我们在icmp的场景下,关注**最后一行**,如果是0%,那不管前面loss是100%还是80%都无所谓,那些都是**节点限制**导致的**虚报**。 51 | 52 | 但如果**最后一行是20%,再往前几行都是20%左右**,那说明丢包就是从最接近的那一行开始产生的,长时间是这样,那很可能这一跳出了点问题。如果是公司内网的话,你可以带着这条线索去找对应的网络同事。如果是外网的话,那耐心点等等吧,别人家的开发会比你更着急。 53 | 54 | ![图片](https://img-blog.csdnimg.cn/img_convert/7142a4e285024dc6aadea4255984c485.png) 55 | 56 | ## TCP抓包工具 57 | 58 | **tcpdump**:支持命令行格式,常在Linux服务器中抓取和分析网络报 59 | 60 | ```shell 61 | tcpdump –i eth0 ’port 1111‘ -X -c 3 62 | ``` 63 | 64 | `-i`:指定监听的网络接口; 65 | 66 | `-c`:在收到指定的包的数目后,tcpdump就会停止; 67 | 68 | `-X`:告诉tcpdump命令,需要把协议头和包内容都原原本本的显示出来(tcpdump会以16进制和ASCII的形式显示),这在进行协议分析时是绝对的利器。 69 | 70 | **wireshark:**除了可以抓包,可以提供可视化的网络报分析同行 71 | -------------------------------------------------------------------------------- /计算机网络/数据链路层.md: -------------------------------------------------------------------------------- 1 | # 数据链路层 2 | 3 | ## 问题 4 | 5 | * 数据链路层上的三个基本问题 6 | * 请你说说TCP IP数据链路层的交互过程 7 | * 以太网中的 CSMA CD 协议 8 | * PPP 协议 9 | * 为什么 PPP 协议不使用序号和确认机制 10 | 11 | ## 回答 12 | 13 | ### 数据链路层上的三个基本问题 14 | 15 | * 封装成帧:将网络层传下来的分组前后分别添加首部和尾部,这样就构成了帧。首部和尾部的一个重要作用是帧定界,也携带了一些必要的控制信息,对于每种数据链路层协议都规定了帧的数据部分的最大长度。 16 | 17 | * 透明传输:帧使用首部和尾部进行定界,如果帧的数据部分含有和首部和尾部相同的内容, 那么帧的开始和结束的位置就会判断错,因此需要在数据部分中出现有歧义的内容前边插入转义字符,如果数据部分出现转义字符,则在该转义字符前再加一个转义字符。在接收端进行处理之后可以还原出原始数据。这个过程透明传输的内容是转义字符,用户察觉不到转义字符的存在。 18 | 19 | * 差错检测:目前数据链路层广泛使用循环冗余检验(CRC)来检查数据传输过程中是否产生比特差错。 20 | 21 | ### 请你说说TCP IP数据链路层的交互过程 22 | 23 | 网络层等到数据链层用mac地址作为通信目标,数据包到达网络等准备往数据链层发送的时候,首先会去自己的ARP 缓存表(存着ip-mac对应关系)去查找改目标ip的mac地址,如果查到了,就讲目标ip的mac地址封装到链路层数据包的包头。如果缓存中没有找到,会发起一个广播:who is ip XXX tell ip XXX,所有收到的广播的机器看这个ip是不是自己的,如果是自己的,则以单拨的形式将自己的mac地址回复给请求的机器 24 | 25 | ### 以太网中的 CSMA CD 协议 26 | 27 | CSMA/CD 为载波侦听多路访问/冲突检测,是像以太网这种广播网络采用的一种机制,我们知道在以太网中多台主机在同一个信道中进行数据传输,CSMA/CD 很好的解决了共享信道通信中出现的问题,它的工作原理主要包括两个部分: 28 | 29 | * 载波监听:当使用 CSMA/CD 协议时,总线上的各个节点都在监听信道上是否有信号在传输,如果有的话,表明信道处于忙碌状态,继续保持监听,直到信道空闲为止。如果发现信道是空闲的,就立即发送数据。 30 | 31 | * 冲突检测:当两个或两个以上节点同时监听到信道空闲,便开始发送数据,此时就会发生碰撞(数据的传输延迟也可能引发碰撞)。当两个帧发生冲突时,数据帧就会破坏而失去了继续传输的意义。在数据的发送过程中,以太网是一直在监听信道的,当检测到当前信道冲突,就立即停止这次传输,避免造成网络资源浪费,同时向信道发送一个「冲突」信号,确保其它节点也发现该冲突。之后采用一种二进制退避策略让待发送数据的节点随机退避一段时间之后重新。 32 | 33 | ### PPP 协议 34 | 35 | 互联网用户通常需要连接到某个 ISP 之后才能接入到互联网,PPP(点对点)协议是用户计算机和 ISP 进行通信时所使用的数据链路层协议。点对点协议为点对点连接上传输多协议数据包提供了一个标准方法。该协议设计的目的主要是用来通过拨号或专线方式建立点对点连接发送数据,使其成为各种主机、网桥和路由器之间简单连接的一种解决方案。 36 | 37 | PPP 协议具有以下特点: 38 | 39 | * PPP 协议具有动态分配 IP 地址的能力,其允许在连接时刻协商 IP 地址。 40 | * PPP 支持多种网络协议,例如 TCP/IP、NETBEUI 等。 41 | * PPP 具有差错检测能力,但不具备纠错能力,所以 PPP 是不可靠传输协议。 42 | * 无重传的机制,网络开销小,速度快。 43 | * PPP 具有身份验证的功能。 44 | 45 | ### 为什么 PPP 协议不使用序号和确认机制 46 | 47 | * IETF 在设计因特网体系结构时把其中最复杂的部分放在 TCP 协议中,而网际协议 IP 则相对比较简单,它提供的是不可靠的数据包服务,在这种情况下,数据链路层没有必要提供比 IP 协议更多的功能。若使用能够实现可靠传输的数据链路层协议,则开销就要增大,这在数据链路层出现差错概率不大时是得不偿失的。 48 | 49 | * 即使数据链路层实现了可靠传输,但其也不能保证网络层的传输也是可靠的,当数据帧在路由器中从数据链路层上升到网络层后,仍有可能因为网络层拥塞而被丢弃。 50 | 51 | * PPP 协议在帧格式中有帧检验序列,对每一个收到的帧,PPP 都会进行差错检测,若发现差错,则丢弃该帧。 52 | -------------------------------------------------------------------------------- /计算机网络/物理层.md: -------------------------------------------------------------------------------- 1 | # 物理层 2 | 3 | ## 问题 4 | 5 | * 物理层主要做什么 6 | 7 | * 几种常用的宽带接入技术 8 | 9 | * 通道复用技术 10 | 11 | * 主机之间的通信方式 12 | 13 | ## 回答 14 | 15 | #### 物理层主要做什么 16 | 17 | 作为 OSI 参考模型最低的一层,物理层是整个开放系统的基础,该层利用传输介质为通信的两端建立、管理和释放物理连接,实现比特流的透明传输。物理层考虑的是怎样才能在连接各种计算机的传输媒体上传输数据比特流,其尽可能地屏蔽掉不同种类传输媒体和通信手段的差异,使物理层上面的数据链路层感觉不到这些差异,这样就可以使数据链路层只考虑完成本层的协议和服务,而不必考虑网络的具体传输媒体和通信手段是什么。 18 | 19 | #### 几种常用的宽带接入技术 20 | 21 | 我们一般将速率超过 1 Mbps 的接入称为宽带接入,目前常用的宽带接入技术主要包括:ADSL 和 FTTx + LAN。 22 | 23 | #### ADSL 24 | 25 | ADSL 全称为非对称用户数字环路,是铜线宽带接入技术的一种。其非对称体现在用户上行和下行的传输速率不相等,一般上行速率较低,下行速率高。这种接入技术适用于有宽带业务需求的家庭用户或者中小型商务用户等。 26 | 27 | #### FTTx + LAN 28 | 29 | 其中 FTTx 英文翻译为 Fiber To The X,这里的 X 指任何地方,我们可以理解为光纤可以接入到任何地方,而 LAN 指的是局域网。FTTx + LAN 是一种在接入网全部或部分采用光纤传输介质,构成光纤用户线路,从而实现用户高速上网的接入技术,其中用户速率可达 20 Mbps。这种接入技术投资规模小,网络拓展性强,网络可靠稳定,使得其应用广泛,目前是城市汇总较为普及的一种宽带接入技术。 30 | 31 | 其它还有 光纤同轴混合网(HFC)、光接入技术(有源和无源光纤系统)和无线接入技术等等。 32 | 33 | #### 通道复用技术 34 | 35 | * 频分复用(FDM,Frequency Division Multiplexing) 36 | 频分复用将传输信道的总带宽按频率划分为若干个子频带或子信道,每个子信道传输一路信号。用户分到一定的频带后,在数据传输的过程中自始至终地占用这个频带。由于每个用户所分到的频带不同,使得传输信道在同一时刻能够支持不同用户进行数据传输,从而实现复用。除了传统意义上的 FDM 外,目前正交频分复用(OFDM)已在高速通信系统中得到广泛应用。 37 | 38 | * 时分复用(TDM,Time Division Multiplexing) 39 | 顾名思义,时分复用将信道传输信息的时间划分为若干个时间片,每一个时分复用的用户在每一个 TDM 帧中占用固定时隙进行数据传输。用户所分配到的时隙是固定的,所以时分复用有时也叫做同步时分复用。这种分配方式能够便于调节控制,但是也存在缺点,当某个信道空闲时,其他繁忙的信道无法占用该空闲信道,因此会降低信道利用率。 40 | 41 | * 波分复用(WDM,Wavelength Division Multiplexing) 42 | 在光通信领域通常按照波长而不是频率来命名,因为光的频率和波长具有单一对应关系,因此 WDM 本质上也是 FDM,光通信系统中,通常由光来运载信号进行传输,WDM 是在一条光纤上传输多个波长光信号,其将 1 根光纤看做多条「虚拟」光纤,每条「虚拟」光纤工作在不同的波长上,从而极大地提高了光纤的传输容量。 43 | 44 | * 码分复用(CDM,Code Division Multiplexing) 45 | 码分复用是靠不同的编码来区分各路原始信号的一种复用方式,不同的用户使用相互正交的码字携带信息。由于码组相互正交,因此接收方能够有效区分不同的用户数据,从而实现每一个用户可以在同样的时间在同样的频带进行数据传输,频谱资源利用率高。其主要和各种多址接入技术相结合从而产生各种接入技术,包括无线和优先接入。 46 | 47 | #### 主机之间的通信方式 48 | 49 | * 单工通信:也叫单向通信,发送方和接收方是固定的,消息只能单向传输。例如采集气象数据、家庭电费,网费等数据收集系统,或者打印机等应用主要采用单工通信。 50 | 51 | * 半双工通信:也叫双向交替通信,通信双方都可以发送消息,但同一时刻同一信道只允许单方向发送数据。例如传统的对讲机使用的就是半双工通信。 52 | 53 | * 全双工通信:也叫双向同时通信,全双工通信允许通信双方同时在两个方向是传输,其要求通信双方都具有独立的发送和接收数据的能力。例如平时我们打电话,自己说话的同时也能听到对面的声音。 54 | -------------------------------------------------------------------------------- /计算机网络/设备.md: -------------------------------------------------------------------------------- 1 | # 设备 2 | 3 | ## 问题 4 | 5 | * 集线器、网桥、交换机、路由器、网关和防火墙都工作在OSI七层协议的哪一层?有什么用 6 | 7 | ## 回答 8 | 9 | ### 集线器、网桥、交换机、路由器、网关和防火墙都工作在OSI七层协议的哪一层?有什么用 10 | 11 | 集线器、网桥、交换机、路由器、网关和防火墙都是常见的网络设备,他们都是工作在OSI的哪一层,经常有人傻傻分不清楚。 12 | 13 | * 集线器:通常只是起到信号放大和多终端连接的作用,所以工作在物理层。 14 | * 网桥:工作在数据链路层,在不同或相同类型的LAN之间存储并转发数据帧,必要时进行链路层上的协议转换。可连接两个或多个网络,在其中传送信息包。 15 | * 交换机:一般指工作在数据链路层的传统交换机,即二层交换机,有多个冲突域和广播域,使用了物理地址(MAC地址)转发以太网帧格式的数据。原理等同于多端口网桥。作用是连接数个相同网段的不同主机,减少网内冲突,隔离冲突域。利用存储转发和过滤技术来从物理上分割网段。 16 | * 随着安全和管理的需要以及VLAN技术的应用,当下大型局域网已普遍应用了具有部分路由功能三层交换机,其能够做到一次路由、多次转发,大大减少了广播风暴的危害,三层交换机顾名思义就是工作在OSI协议的第三层网络层。 17 | * 路由器:则是一种典型的网络层设备,在不同的网络间存储并转发分组。可在异种网络之间(即不同类型的局域网互连,局域网与广域网,广域网与广域网)传输数据并进行路径选择,使用专门的软件协议从逻辑上对整个网络进行划分。 18 | * 网关:对高层协议(包括传输层及更高层次)进行转换的网间连接器。允许使用不兼容的协议,比如SPX/IPX和TCP/IP的系统和网络互连。因为协议转换是网关最重要的功能,所以是工作在传输层及以上层次。 19 | * 防火墙:传统的包过滤防火墙工作在网络层,电路网关防火墙工作在传输层(线路级防火墙工作在会话层),应用网关类的代理防火墙则工作在OSI最高的应用层。 20 | -------------------------------------------------------------------------------- /设计模式/单例模式.md: -------------------------------------------------------------------------------- 1 | # 单例模式 2 | 3 | **单例**是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。 4 | 5 | 单例拥有与全局变量相同的优缺点。 尽管它们非常有用, 但却会破坏代码的模块化特性。 6 | 7 | 在某些其他上下文中, 你不能使用依赖于单例的类。 你也将必须使用单例类。 绝大多数情况下, 该限制会在创建单元测试时出现。 8 | 9 | ## 意图 10 | 11 | **单例模式**是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点 12 | 13 | ## 问题 14 | 15 | **与普通函数对比、与全局变量对比** 16 | 17 | 单例模式同时解决了两个问题, **所以违反了*单一职责原则***: 18 | 19 | 1. **保证一个类只有一个实例**。 为什么会有人想要控制一个类所拥有的实例数量? 最常见的原因是控制某些共享资源 (例如数据库或文件) 的访问权限。 20 | 21 | 它的运作方式是这样的: 如果你创建了一个对象, 同时过一会儿后你决定再创建一个新对象, 此时你会获得之前已创建的对象, 而不是一个新对象。 22 | 23 | 注意, 普通构造函数无法实现上述行为, 因为构造函数的设计决定了它**必须**总是返回一个新对象。 24 | 25 | ![一个对象的全局访问节点](https://refactoringguru.cn/images/patterns/content/singleton/singleton-comic-1-zh.png) 26 | 27 | 客户端甚至可能没有意识到它们一直都在使用同一个对象。 28 | 29 | 2. **为该实例提供一个全局访问节点**。 还记得你 (好吧, 其实是我自己) 用过的那些存储重要对象的全局变量吗? 它们在使用上十分方便, 但同时也非常不安全, 因为任何代码都有可能覆盖掉那些变量的内容, 从而引发程序崩溃。 30 | 31 | 和全局变量一样, 单例模式也允许在程序的任何地方访问特定对象。 **但是它可以保护该实例不被其他**代码覆盖。 32 | 33 | 还有一点: **你不会希望解决同一个问题的代码分散在程序各处的。 因此更好的方式是将其放在同一个类中, 特别是当其他代码已经依赖这个类时更应该如此**。 34 | 35 | 如今, 单例模式已经变得非常流行, 以至于人们会将只解决上文描述中任意一个问题的东西称为*单例*。 36 | 37 | 38 | 39 | ## 解决方案 40 | 41 | 所有单例的实现都包含以下两个相同的步骤: 42 | 43 | - 将默认构造函数设为私有, 防止其他对象使用单例类的 `new`运算符。 44 | - 新建一个**静态构建方法作为构造函数**。 该函数会 “偷偷” 调用私有构造函数来创建对象, 并将其保存在一个静态成员变量中。 此后所有对于该函数的调用都将返回这一缓存对象。 45 | 46 | 如果你的代码能够访问单例类, 那它就能调用单例类的静态方法。 无论何时调用该方法, 它总是会返回相同的对象。 47 | 48 | 49 | 50 | ## 适用场景 51 | 52 | **如果程序中的某个类对于所有客户端只有一个可用的实例, 可以使用单例模式。** 53 | 54 | 单例模式禁止通过除特殊构建方法以外的任何方式来创建自身类的对象。 该方法可以创建一个新对象, 但如果该对象已经被创建, 则返回已有的对象。 55 | 56 | **如果你需要更加严格地控制全局变量, 可以使用单例模式。** 57 | 58 | 单例模式与全局变量不同, 它保证类只存在一个实例。 除了单例类自己以外, 无法通过任何方式替换缓存的实例。 59 | 60 | 请注意, 你可以随时调整限制并设定生成单例实例的数量, 只需修改 `获取实例`方法, 即 getInstance 中的代码即可实现。 61 | 62 | 63 | 64 | ## 实现方式 65 | 66 | 1. 在类中添加一个私有静态成员变量用于保存单例实例。 67 | 2. 声明一个公有静态构建方法用于获取单例实例。 68 | 3. 在静态方法中实现"延迟初始化"。 该方法会在首次被调用时创建一个新对象, 并将其存储在静态成员变量中。 此后该方法每次被调用时都返回该实例。 69 | 4. 将类的构造函数设为私有。 类的静态方法仍能调用构造函数, 但是其他对象不能调用。 70 | 5. 检查客户端代码, 将对单例的构造函数的调用替换为对其静态构建方法的调用。 71 | 72 | 73 | 74 | ## 优缺点 75 | 76 | 优点 77 | 78 | - 你可以保证一个类只有一个实例。 79 | - 你获得了一个指向该实例的全局访问节点。 80 | - 仅在首次请求单例对象时对其进行初始化。 81 | 82 | 缺点 83 | 84 | - 违反了*单一职责原则*。 该模式同时解决了两个问题。 85 | - 单例模式可能掩盖不良设计, 比如程序各组件之间相互了解过多等。 86 | - 该模式在多线程环境下需要进行特殊处理, 避免多个线程多次创建单例对象。 87 | - 单例的客户端代码单元测试可能会比较困难, 因为许多测试框架以基于继承的方式创建模拟对象。 由于单例类的构造函数是私有的, 而且绝大部分语言无法重写静态方法, 所以你需要想出仔细考虑模拟单例的方法。 要么干脆不编写测试代码, 或者不使用单例模式。 88 | 89 | ## 代码 90 | 91 | 要点 92 | 93 | - 全局只有一个实例:static 特性,同时禁止用户自己声明并定义实例(把构造函数设为 private) 94 | - 线程安全 95 | - 禁止赋值和拷贝 96 | - 用户通过接口获取实例:使用 static 类成员函数 97 | 98 | https://blog.csdn.net/songchuwang1868/article/details/87882778?spm=1001.2101.3001.6650.1 99 | 100 | https://www.cnblogs.com/sunchaothu/p/10389842.html#222 101 | 102 | **饿汉模式(一开始就初始化单例对象)** 103 | 104 | 优点:不用担心[多线程](https://so.csdn.net/so/search?q=多线程&spm=1001.2101.3001.7020)问题。 105 | 106 | 缺点:可能在整个程序中就没有用到这个单例对象,造成浪费。 107 | 108 | ```c++ 109 | class Singleton{ 110 | public: 111 | static Singleton* getInstatce(){ 112 | return &m_instance; 113 | } 114 | Singleton(Singleton const & single) = delete; 115 | Singleton& operator = (const Singleton& single) = delete; 116 | private: 117 | Singleton(){} 118 | static Singleton m_instance; 119 | }; 120 | //类外初始化 121 | Singleton Singleton::m_instance; 122 | ``` 123 | 124 | **懒汉模式(需要的时候在实例化单例对象)这个版本的双检查所有指令重排问题,不要写!!!** 125 | 126 | 优点:不会像饿汉模式一样造成资源浪费。只是需要考虑多线程安全,实现上稍稍复杂一点。 127 | 128 | ```c++ 129 | class Singleton 130 | { 131 | public: 132 | static Singleton* GetInstance() 133 | { 134 | if (p_singleton_ == nullptr)//第一次检查:实例化单例对象后,就不会再进入加锁逻辑 135 | { 136 | std::lock_guard lock(mux_); 137 | //第二次检查:可能两个线程同时通过第一次检查,一个线程获得锁时,可能另外一个线程已经实例化单体 138 | if (p_singleton_ == nullptr) 139 | { 140 | p_singleton_ = new Singleton(); 141 | } 142 | } 143 | return p_singleton_; 144 | } 145 | private: 146 | Singleton(); 147 | static Singleton * p_singleton_ ; 148 | static std::mutex mux_; 149 | }; 150 | 151 | std::mutex Singleton::mux_; 152 | Singleton * Singleton::p_singleton_ = nullptr; 153 | int main() 154 | { 155 | auto p1 = Singleton::GetInstance(); 156 | auto p2 = Singleton::GetInstance(); 157 | bool result=( p1 == p2); 158 | std::cout << result << std::endl; 159 | 160 | return 0; 161 | } 162 | ``` 163 | 164 | **c++ 11 static 懒汉模式** 165 | 166 | ```c++ 167 | #include 168 | 169 | class Singleton 170 | { 171 | public: 172 | ~Singleton(){ 173 | std::cout<<"destructor called!"< If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization. 203 | > 如果当变量在初始化的时候,并发同时进入声明语句,并发线程将会阻塞等待初始化结束。 204 | 205 | 这样保证了并发线程在获取静态局部变量的时候一定是初始化过的,所以具有线程安全性。 206 | 207 | [C++静态变量的生存期](https://stackoverflow.com/questions/246564/what-is-the-lifetime-of-a-static-variable-in-a-c-function) 是从声明到程序结束,这也是一种懒汉式。 208 | 209 | 210 | 211 | **单例模板** 212 | 213 | ```c++ 214 | // brief: a singleton base class offering an easy way to create singleton 215 | #include 216 | 217 | template 218 | class Singleton{ 219 | public: 220 | static T& get_instance(){ 221 | static T instance; 222 | return instance; 223 | } 224 | virtual ~Singleton(){ 225 | std::cout<<"destructor called!"<{ 242 | // !!!! attention!!! 243 | // needs to be friend in order to 244 | // access the private constructor/destructor 245 | friend class Singleton; 246 | public: 247 | DerivedSingle(const DerivedSingle&)=delete; 248 | DerivedSingle& operator =(const DerivedSingle&)= delete; 249 | private: 250 | DerivedSingle()=default; 251 | }; 252 | 253 | int main(int argc, char* argv[]){ 254 | DerivedSingle& instance1 = DerivedSingle::get_instance(); 255 | DerivedSingle& instance2 = DerivedSingle::get_instance(); 256 | return 0; 257 | } 258 | ``` 259 | 260 | 以上实现一个单例的模板基类,使用方法如例子所示意,子类需要**将自己作为模板参数T** 传递给 `Singleton` 模板; 同时需要**将基类声明为友元**,这样才能调用子类的私有构造函数。 261 | 262 | 基类模板的实现要点是: 263 | 264 | 1. 构造函数需要是 **protected**,这样子类才能继承; 265 | 2. 使用了[奇异递归模板模式](https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern)CRTP(Curiously recurring template pattern) 266 | 3. get instance 方法和 2.2.3 的static local方法一个原理。 267 | 4. 在这里基类的析构函数可以不需要 virtual ,因为子类在应用中只会用 Derived 类型,保证了析构时和构造时的类型一致 -------------------------------------------------------------------------------- /设计模式/基本概念.md: -------------------------------------------------------------------------------- 1 | # 基本概念 2 | 3 | 不要直接讲某种模式怎么实现,有什么特点。应该先表示你对设计模式的正确认识 4 | 5 | ## 为什么要学设计模式 6 | 7 | 设计模式是针对软件设计中常见问题的工具箱, 其中的工具就是各种经过实践验证的解决方案。 即使你从未遇到过这些问题, 了解模式仍然非常有用, 因为它能指导你如何使用面向对象的设计原则来解决各种问题。(就像写题目分析模型套经典模板) 8 | 9 | 设计模式定义了一种让你和团队成员能够更高效沟通的通用语言。 你只需说 “哦, 这里用单例就可以了”, 所有人都会理解这条建议背后的想法。 只要知晓模式及其名称, 你就无需解释什么是单例。 10 | 11 | ## 关于模式的争议 12 | 13 | * 一种针对不完善编程语言的蹩脚解决方案(策略模式在绝大部分现代编程语言中可以简单地使用匿名 (lamb­da) 函数来实现。) 14 | * 低效的解决方案(教条主义,不会变通) 15 | * 不当使用:如果你只有一把铁锤, 那么任何东西看上去都像是钉子。 16 | 17 | ## 设计原则 18 | 19 | * 依赖倒置 20 | * 开放封闭 21 | * 单一职责 22 | * 替换原则 23 | * 接口隔离原则 24 | * 优先组合而非继承 25 | * 封装变化点 26 | * 针对接口编程而不是实现 27 | 28 | ## 从设计模式中学到什么 29 | 30 | * 软件工程复杂度的根源是变化,面向对象的设计模式可以帮助抵御变化(稳定和变化是相对的) 31 | * 重构的时候考虑设计模式 32 | * 时间轴动态思维发现问题 33 | * 依据原则进行评判,而不是代码 -------------------------------------------------------------------------------- /设计模式/工厂模式.md: -------------------------------------------------------------------------------- 1 | # 工厂模式 2 | 3 | ## 意图 4 | 5 | **工厂方法模式**是一种创建型设计模式, 其在父类中提供一个创建对象的方法, **允许子类决定实例化对象的类型。** 6 | 7 | ## 问题 8 | 9 | 假设你正在开发一款物流管理应用。 最初版本只能处理卡车运输, 因此大部分代码都在位于名为 `卡车`的类中。 10 | 11 | 一段时间后, 这款应用变得极受欢迎。 你每天都能收到十几次来自海运公司的请求, 希望应用能够支持海上物流功能。 12 | 13 | ![在程序中新增一个运输类会遇到问题](https://refactoringguru.cn/images/patterns/diagrams/factory-method/problem1-zh.png) 14 | 15 | 如果代码其余部分与现有类已经存在耦合关系, 那么向程序中添加新类其实并没有那么容易。 16 | 17 | 这可是个好消息。 但是代码问题该如何处理呢? 目前, 大部分代码都与 `卡车`类相关。 在程序中添加 `轮船`类需要修改全部代码。 更糟糕的是, 如果你以后需要在程序中支持另外一种运输方式, 很可能需要再次对这些代码进行大幅修改。 18 | 19 | 最后, 你将不得不编写繁复的代码, 根据不同的运输对象类, 在应用中进行不同的处理。 20 | 21 | 22 | 23 | ## 解决方案 24 | 25 | 工厂方法模式建议使用特殊的*工厂*方法代替对于对象构造函数的直接调用 (即使用 `new`运算符)。 不用担心, 对象仍将通过 `new`运算符创建, 只是该运算符改在工厂方法中调用罢了。 工厂方法返回的对象通常被称作 “产品”。 26 | 27 | ![创建者类结构](https://refactoringguru.cn/images/patterns/diagrams/factory-method/solution1.png) 28 | 29 | 子类可以修改工厂方法返回的对象类型。 30 | 31 | 乍看之下, 这种更改可能毫无意义: 我们只是改变了程序中调用构造函数的位置而已。 但是, 仔细想一下, 现在你可以在子类中重写工厂方法, 从而改变其创建产品的类型。 32 | 33 | 但有一点需要注意:仅当这些产品具有共同的基类或者接口时, 子类才能返回不同类型的产品, 同时基类中的工厂方法还应将其返回类型声明为这一共有接口。 34 | 35 | ![产品对象层次结构](https://refactoringguru.cn/images/patterns/diagrams/factory-method/solution2-zh.png) 36 | 37 | 所有产品都必须使用同一接口。 38 | 39 | 举例来说, `卡车`Truck和 `轮船`Ship类都必须实现 `运输`Trans­port接口, 该接口声明了一个名为 `deliv­er`交付的方法。 每个类都将以不同的方式实现该方法: 卡车走陆路交付货物, 轮船走海路交付货物。 `陆路运输`Road­Logis­tics类中的工厂方法返回卡车对象, 而 `海路运输`Sea­Logis­tics类则返回轮船对象。 40 | 41 | ![使用工厂方法模式后的代码结构](https://refactoringguru.cn/images/patterns/diagrams/factory-method/solution3-zh.png) 42 | 43 | 只要产品类实现一个共同的接口, 你就可以将其对象传递给客户代码, 而无需提供额外数据。 44 | 45 | 调用工厂方法的代码 (通常被称为*客户端*代码) 无需了解不同子类返回实际对象之间的差别。 客户端将所有产品视为抽象的 `运输` 。 客户端知道所有运输对象都提供 `交付`方法, 但是并不关心其具体实现方式。 46 | 47 | 48 | 49 | ## 适用场景 50 | 51 | **当你在编写代码的过程中, 如果无法预知对象确切类别及其依赖关系时, 可使用工厂方法。** 52 | 53 | 工厂方法将创建产品的代码与实际使用产品的代码分离, 从而能在不影响其他代码的情况下扩展产品创建部分代码。 54 | 55 | 例如, 如果需要向应用中添加一种新产品, 你只需要开发新的创建者子类, 然后重写其工厂方法即可。 56 | 57 | **如果你希望用户能扩展你软件库或框架的内部组件, 可使用工厂方法。** 58 | 59 | 继承可能是扩展软件库或框架默认行为的最简单方法。 但是当你使用子类替代标准组件时, 框架如何辨识出该子类? 60 | 61 | 解决方案是将各框架中构造组件的代码集中到单个工厂方法中, 并在继承该组件之外允许任何人对该方法进行重写。 62 | 63 | 让我们看看具体是如何实现的。 假设你使用开源 UI 框架编写自己的应用。 你希望在应用中使用圆形按钮, 但是原框架仅支持矩形按钮。 你可以使用 `圆形按钮`Round­But­ton子类来继承标准的 `按钮`But­ton类。 但是, 你需要告诉 `UI框架`UIFrame­work类使用新的子类按钮代替默认按钮。 64 | 65 | 为了实现这个功能, 你可以根据基础框架类开发子类 `圆形按钮 UI`UIWith­Round­But­tons , 并且重写其 `cre­ate­But­ton`创建按钮方法。 基类中的该方法返回 `按钮`对象, 而你开发的子类返回 `圆形按钮`对象。 现在, 你就可以使用 `圆形按钮 UI`类代替 `UI框架`类。 就是这么简单! 66 | 67 | **如果你希望复用现有对象来节省系统资源, 而不是每次都重新创建对象, 可使用工厂方法。** 68 | 69 | 在处理大型资源密集型对象 (比如数据库连接、 文件系统和网络资源) 时, 你会经常碰到这种资源需求。 70 | 71 | 让我们思考复用现有对象的方法: 72 | 73 | 1. 首先, 你需要创建存储空间来存放所有已经创建的对象。 74 | 2. 当他人请求一个对象时, 程序将在对象池中搜索可用对象。 75 | 3. … 然后将其返回给客户端代码。 76 | 4. 如果没有可用对象, 程序则创建一个新对象 (并将其添加到对象池中)。 77 | 78 | 这些代码可不少! 而且它们必须位于同一处, 这样才能确保重复代码不会污染程序。 79 | 80 | 可能最显而易见, 也是最方便的方式, 就是将这些代码放置在我们试图重用的对象类的构造函数中。 但是从定义上来讲, 构造函数始终返回的是**新对象**, 其无法返回现有实例。 81 | 82 | 因此, 你需要有一个既能够创建新对象, 又可以重用现有对象的普通方法。 这听上去和工厂方法非常相像。 83 | 84 | 85 | 86 | ## 实现方式 87 | 88 | 1. 让所有产品都遵循同一接口。 该接口必须声明对所有产品都有意义的方法。 89 | 90 | 2. 在创建类中添加一个空的工厂方法。 该方法的返回类型必须遵循通用的产品接口。 91 | 92 | 3. 在创建者代码中找到对于产品构造函数的所有引用。 将它们依次替换为对于工厂方法的调用, 同时将创建产品的代码移入工厂方法。 93 | 94 | 你可能需要在工厂方法中添加临时参数来控制返回的产品类型。 95 | 96 | 工厂方法的代码看上去可能非常糟糕。 其中可能会有复杂的 `switch`分支运算符, 用于选择各种需要实例化的产品类。 但是不要担心, 我们很快就会修复这个问题。 97 | 98 | 4. 现在, 为工厂方法中的每种产品编写一个创建者子类, 然后在子类中重写工厂方法, 并将基本方法中的相关创建代码移动到工厂方法中。 99 | 100 | 5. 如果应用中的产品类型太多, 那么为每个产品创建子类并无太大必要, 这时你也可以在子类中复用基类中的控制参数。 101 | 102 | 例如, 设想你有以下一些层次结构的类。 基类 `邮件`及其子类 `航空邮件`和 `陆路邮件` ; `运输`及其子类 `飞机`, `卡车`和 `火车` 。 `航空邮件`仅使用 `飞机`对象, 而 `陆路邮件`则会同时使用 `卡车`和 `火车`对象。 你可以编写一个新的子类 (例如 `火车邮件` ) 来处理这两种情况, 但是还有其他可选的方案。 客户端代码可以给 `陆路邮件`类传递一个参数, 用于控制其希望获得的产品。 103 | 104 | 6. 如果代码经过上述移动后, 基础工厂方法中已经没有任何代码, 你可以将其转变为抽象类。 如果基础工厂方法中还有其他语句, 你可以将其设置为该方法的默认行为。 105 | 106 | ## 工厂方法模式优缺点 107 | 108 | 优点 109 | 110 | - 你可以避免创建者和具体产品之间的紧密耦合。 111 | - *单一职责原则*。 你可以将产品创建代码放在程序的单一位置, 从而使得代码更容易维护。 112 | - *开闭原则*。 无需更改现有客户端代码, 你就可以在程序中引入新的产品类型。 113 | 114 | 缺点 115 | 116 | - 应用工厂方法模式需要引入许多新的子类, 代码可能会因此变得更复杂。 最好的情况是将该模式引入创建者类的现有层次结构中。 117 | 118 | ## 代码 119 | 120 | ```c++ 121 | /** 122 | * The Product interface declares the operations that all concrete products must 123 | * implement. 124 | */ 125 | 126 | class Product { 127 | public: 128 | virtual ~Product() {} 129 | virtual std::string Operation() const = 0; 130 | }; 131 | 132 | /** 133 | * Concrete Products provide various implementations of the Product interface. 134 | */ 135 | class ConcreteProduct1 : public Product { 136 | public: 137 | std::string Operation() const override { 138 | return "{Result of the ConcreteProduct1}"; 139 | } 140 | }; 141 | class ConcreteProduct2 : public Product { 142 | public: 143 | std::string Operation() const override { 144 | return "{Result of the ConcreteProduct2}"; 145 | } 146 | }; 147 | 148 | /** 149 | * The Creator class declares the factory method that is supposed to return an 150 | * object of a Product class. The Creator's subclasses usually provide the 151 | * implementation of this method. 152 | */ 153 | 154 | class Creator { 155 | /** 156 | * Note that the Creator may also provide some default implementation of the 157 | * factory method. 158 | */ 159 | public: 160 | virtual ~Creator(){}; 161 | virtual Product* FactoryMethod() const = 0; 162 | /** 163 | * Also note that, despite its name, the Creator's primary responsibility is 164 | * not creating products. Usually, it contains some core business logic that 165 | * relies on Product objects, returned by the factory method. Subclasses can 166 | * indirectly change that business logic by overriding the factory method and 167 | * returning a different type of product from it. 168 | */ 169 | 170 | std::string SomeOperation() const { 171 | // Call the factory method to create a Product object. 172 | Product* product = this->FactoryMethod(); 173 | // Now, use the product. 174 | std::string result = "Creator: The same creator's code has just worked with " + product->Operation(); 175 | delete product; 176 | return result; 177 | } 178 | }; 179 | 180 | /** 181 | * Concrete Creators override the factory method in order to change the 182 | * resulting product's type. 183 | */ 184 | class ConcreteCreator1 : public Creator { 185 | /** 186 | * Note that the signature of the method still uses the abstract product type, 187 | * even though the concrete product is actually returned from the method. This 188 | * way the Creator can stay independent of concrete product classes. 189 | */ 190 | public: 191 | Product* FactoryMethod() const override { 192 | return new ConcreteProduct1(); 193 | } 194 | }; 195 | 196 | class ConcreteCreator2 : public Creator { 197 | public: 198 | Product* FactoryMethod() const override { 199 | return new ConcreteProduct2(); 200 | } 201 | }; 202 | 203 | /** 204 | * The client code works with an instance of a concrete creator, albeit through 205 | * its base interface. As long as the client keeps working with the creator via 206 | * the base interface, you can pass it any creator's subclass. 207 | */ 208 | void ClientCode(const Creator& creator) { 209 | // ... 210 | std::cout << "Client: I'm not aware of the creator's class, but it still works.\n" 211 | << creator.SomeOperation() << std::endl; 212 | // ... 213 | } 214 | 215 | /** 216 | * The Application picks a creator's type depending on the configuration or 217 | * environment. 218 | */ 219 | 220 | int main() { 221 | std::cout << "App: Launched with the ConcreteCreator1.\n"; 222 | Creator* creator = new ConcreteCreator1(); 223 | ClientCode(*creator); 224 | std::cout << std::endl; 225 | std::cout << "App: Launched with the ConcreteCreator2.\n"; 226 | Creator* creator2 = new ConcreteCreator2(); 227 | ClientCode(*creator2); 228 | 229 | delete creator; 230 | delete creator2; 231 | return 0; 232 | } 233 | ``` 234 | 235 | ## 扩展扯一下抽象工厂 236 | 237 | > 如果代码需要与多个不同系列的相关产品交互, 但是由于无法提前获取相关信息, 或者出于对未来扩展性的考虑, 你不希望代码基于产品的具体类进行构建, 在这种情况下, 你可以使用抽象工厂。 238 | > 239 | > 抽象工厂为你提供了一个接口, 可用于创建每个系列产品的对象。 只要代码通过该接口创建对象, 那么你就不会生成与应用程序已生成的产品类型不一致的产品。 240 | > 241 | > - 如果你有一个基于一组[抽象方法](https://refactoringguru.cn/design-patterns/factory-method)的类, 且其主要功能因此变得不明确, 那么在这种情况下可以考虑使用抽象工厂模式。 242 | > - 在设计良好的程序中, *每个类仅负责一件事*。 如果一个类与多种类型产品交互, 就可以考虑将工厂方法抽取到独立的工厂类或具备完整功能的抽象工厂类中。 -------------------------------------------------------------------------------- /设计模式/观察者模式.md: -------------------------------------------------------------------------------- 1 | # 观察者模式 2 | 3 | ## 意图 4 | 5 | **观察者模式**是一种行为设计模式, 允许你定义一种订阅机制, 可在对象事件发生时通知多个 “观察” 该对象的其他对象。 6 | 7 | ## 问题 8 | 9 | 假如你有两种类型的对象: `顾客`和 `商店` 。 顾客对某个特定品牌的产品非常感兴趣 (例如最新型号的 iPhone 手机), 而该产品很快将会在商店里出售。 10 | 11 | 顾客可以每天来商店看看产品是否到货。 但如果商品尚未到货时, 绝大多数来到商店的顾客都会空手而归。 12 | 13 | ![访问商店或发送垃圾邮件](https://refactoringguru.cn/images/patterns/content/observer/observer-comic-1-zh.png) 14 | 15 | 前往商店和发送垃圾邮件 16 | 17 | 另一方面, 每次新产品到货时, 商店可以向所有顾客发送邮件 (可能会被视为垃圾邮件)。 这样, 部分顾客就无需反复前往商店了, 但也可能会惹恼对新产品没有兴趣的其他顾客。 18 | 19 | **我们似乎遇到了一个矛盾: 要么让顾客浪费时间检查产品是否到货, 要么让商店浪费资源去通知没有需求的顾客。** 20 | 21 | ## 解决方案 22 | 23 | 拥有一些值得关注的状态的对象通常被称为*目标*, 由于它要将自身的状态改变通知给其他对象, 我们也将其称为*发布者* (pub­lish­er)。 所有希望关注发布者状态变化的其他对象被称为*订阅者* (sub­scribers)。 24 | 25 | **观察者模式建议你为发布者类添加订阅机制, 让每个对象都能订阅或取消订阅发布者事件流**。 不要害怕! 这并不像听上去那么复杂。 实际上, 该机制包括 **1) 一个用于存储订阅者对象引用的列表成员变量; 2) 几个用于添加或删除该列表中订阅者的公有方法。** 26 | 27 | ![订阅机制](https://refactoringguru.cn/images/patterns/diagrams/observer/solution1-zh.png) 28 | 29 | 订阅机制允许对象订阅事件通知。 30 | 31 | 现在, 无论何时发生了重要的发布者事件, 它都要遍历订阅者并调用其对象的特定通知方法。 32 | 33 | 实际应用中可能会有十几个不同的订阅者类跟踪着同一个发布者类的事件, 你不会希望发布者与所有这些类相耦合的。 此外如果他人会使用发布者类, 那么你甚至可能会对其中的一些类一无所知。 34 | 35 | 因此, 所有订阅者都必须实现同样的接口, 发布者仅通过该接口与订阅者交互。 接口中必须声明通知方法及其参数, 这样发布者在发出通知时还能传递一些上下文数据。 36 | 37 | ![通知方法](https://refactoringguru.cn/images/patterns/diagrams/observer/solution2-zh.png) 38 | 39 | 发布者调用订阅者对象中的特定通知方法来通知订阅者。 40 | 41 | 如果你的应用中有多个不同类型的发布者, 且希望订阅者可兼容所有发布者, 那么你甚至可以进一步让所有发布者遵循同样的接口。 该接口仅需描述几个订阅方法即可。 这样订阅者就能在不与具体发布者类耦合的情况下通过接口观察发布者的状态。 42 | 43 | ## 适合应用场景 44 | 45 | **当一个对象状态的改变需要改变其他对象, 或实际对象是事先未知的或动态变化的时, 可使用观察者模式。** 46 | 47 | 当你使用图形用户界面类时通常会遇到一个问题。 比如, 你创建了自定义按钮类并允许客户端在按钮中注入自定义代码, 这样当用户按下按钮时就会触发这些代码。 48 | 49 | 观察者模式允许任何实现了订阅者接口的对象订阅发布者对象的事件通知。 你可在按钮中添加订阅机制, 允许客户端通过自定义订阅类注入自定义代码。 50 | 51 | **当应用中的一些对象必须观察其他对象时, 可使用该模式。 但仅能在有限时间内或特定情况下使用。** 52 | 53 | 订阅列表是动态的, 因此订阅者可随时加入或离开该列表。 54 | 55 | ## 实现方式 56 | 57 | 1. 仔细检查你的业务逻辑, 试着将其拆分为两个部分: 独立于其他代码的核心功能将作为发布者; 其他代码则将转化为一组订阅类。 58 | 59 | 2. 声明订阅者接口。 该接口至少应声明一个 `update`方法。 60 | 61 | 3. 声明发布者接口并定义一些接口来在列表中添加和删除订阅对象。 记住发布者必须仅通过订阅者接口与它们进行交互。 62 | 63 | 4. 确定存放实际订阅列表的位置并实现订阅方法。 通常所有类型的发布者代码看上去都一样, 因此将列表放置在直接扩展自发布者接口的抽象类中是显而易见的。 具体发布者会扩展该类从而继承所有的订阅行为。 64 | 65 | 但是, 如果你需要在现有的类层次结构中应用该模式, 则可以考虑使用组合的方式: 将订阅逻辑放入一个独立的对象, 然后让所有实际订阅者使用该对象。 66 | 67 | 5. 创建具体发布者类。 每次发布者发生了重要事件时都必须通知所有的订阅者。 68 | 69 | 6. 在具体订阅者类中实现通知更新的方法。 绝大部分订阅者需要一些与事件相关的上下文数据。 这些数据可作为通知方法的参数来传递。 70 | 71 | 但还有另一种选择。 订阅者接收到通知后直接从通知中获取所有数据。 在这种情况下, 发布者必须通过更新方法将自身传递出去。 另一种不太灵活的方式是通过构造函数将发布者与订阅者永久性地连接起来。 72 | 73 | 7. 客户端必须生成所需的全部订阅者, 并在相应的发布者处完成注册工作。 74 | 75 | ## 优缺点 76 | 77 | 优点 78 | 79 | - *开闭原则*。 你无需修改发布者代码就能引入新的订阅者类 (如果是发布者接口则可轻松引入发布者类)。 80 | - 你可以在运行时建立对象之间的联系。 81 | 82 | 缺点 83 | 84 | - 订阅者的通知顺序是随机的。 85 | 86 | ## 代码 87 | 88 | ```c++ 89 | /** 90 | * Observer Design Pattern 91 | * 92 | * Intent: Lets you define a subscription mechanism to notify multiple objects 93 | * about any events that happen to the object they're observing. 94 | * 95 | * Note that there's a lot of different terms with similar meaning associated 96 | * with this pattern. Just remember that the Subject is also called the 97 | * Publisher and the Observer is often called the Subscriber and vice versa. 98 | * Also the verbs "observe", "listen" or "track" usually mean the same thing. 99 | */ 100 | 101 | #include 102 | #include 103 | #include 104 | 105 | class IObserver { 106 | public: 107 | virtual ~IObserver(){}; 108 | virtual void Update(const std::string &message_from_subject) = 0; 109 | }; 110 | 111 | class ISubject { 112 | public: 113 | virtual ~ISubject(){}; 114 | virtual void Attach(IObserver *observer) = 0; 115 | virtual void Detach(IObserver *observer) = 0; 116 | virtual void Notify() = 0; 117 | }; 118 | 119 | /** 120 | * The Subject owns some important state and notifies observers when the state 121 | * changes. 122 | */ 123 | 124 | class Subject : public ISubject { 125 | public: 126 | virtual ~Subject() { 127 | std::cout << "Goodbye, I was the Subject.\n"; 128 | } 129 | 130 | /** 131 | * The subscription management methods. 132 | */ 133 | void Attach(IObserver *observer) override { 134 | list_observer_.push_back(observer); 135 | } 136 | void Detach(IObserver *observer) override { 137 | list_observer_.remove(observer); 138 | } 139 | void Notify() override { 140 | std::list::iterator iterator = list_observer_.begin(); 141 | HowManyObserver(); 142 | while (iterator != list_observer_.end()) { 143 | (*iterator)->Update(message_); 144 | ++iterator; 145 | } 146 | } 147 | 148 | void CreateMessage(std::string message = "Empty") { 149 | this->message_ = message; 150 | Notify(); 151 | } 152 | void HowManyObserver() { 153 | std::cout << "There are " << list_observer_.size() << " observers in the list.\n"; 154 | } 155 | 156 | /** 157 | * Usually, the subscription logic is only a fraction of what a Subject can 158 | * really do. Subjects commonly hold some important business logic, that 159 | * triggers a notification method whenever something important is about to 160 | * happen (or after it). 161 | */ 162 | void SomeBusinessLogic() { 163 | this->message_ = "change message message"; 164 | Notify(); 165 | std::cout << "I'm about to do some thing important\n"; 166 | } 167 | 168 | private: 169 | std::list list_observer_; 170 | std::string message_; 171 | }; 172 | 173 | class Observer : public IObserver { 174 | public: 175 | Observer(Subject &subject) : subject_(subject) { 176 | this->subject_.Attach(this); 177 | std::cout << "Hi, I'm the Observer \"" << ++Observer::static_number_ << "\".\n"; 178 | this->number_ = Observer::static_number_; 179 | } 180 | virtual ~Observer() { 181 | std::cout << "Goodbye, I was the Observer \"" << this->number_ << "\".\n"; 182 | } 183 | 184 | void Update(const std::string &message_from_subject) override { 185 | message_from_subject_ = message_from_subject; 186 | PrintInfo(); 187 | } 188 | void RemoveMeFromTheList() { 189 | subject_.Detach(this); 190 | std::cout << "Observer \"" << number_ << "\" removed from the list.\n"; 191 | } 192 | void PrintInfo() { 193 | std::cout << "Observer \"" << this->number_ << "\": a new message is available --> " << this->message_from_subject_ << "\n"; 194 | } 195 | 196 | private: 197 | std::string message_from_subject_; 198 | Subject &subject_; 199 | static int static_number_; 200 | int number_; 201 | }; 202 | 203 | int Observer::static_number_ = 0; 204 | 205 | void ClientCode() { 206 | Subject *subject = new Subject; 207 | Observer *observer1 = new Observer(*subject); 208 | Observer *observer2 = new Observer(*subject); 209 | Observer *observer3 = new Observer(*subject); 210 | Observer *observer4; 211 | Observer *observer5; 212 | 213 | subject->CreateMessage("Hello World! :D"); 214 | observer3->RemoveMeFromTheList(); 215 | 216 | subject->CreateMessage("The weather is hot today! :p"); 217 | observer4 = new Observer(*subject); 218 | 219 | observer2->RemoveMeFromTheList(); 220 | observer5 = new Observer(*subject); 221 | 222 | subject->CreateMessage("My new car is great! ;)"); 223 | observer5->RemoveMeFromTheList(); 224 | 225 | observer4->RemoveMeFromTheList(); 226 | observer1->RemoveMeFromTheList(); 227 | 228 | delete observer5; 229 | delete observer4; 230 | delete observer3; 231 | delete observer2; 232 | delete observer1; 233 | delete subject; 234 | } 235 | 236 | int main() { 237 | ClientCode(); 238 | return 0; 239 | } 240 | ``` 241 | 242 | -------------------------------------------------------------------------------- /设计模式/适配器模式.md: -------------------------------------------------------------------------------- 1 | # 适配器模式 2 | 3 | ## 意图 4 | 5 | **适配器模式**是一种结构型设计模式, 它能使接口不兼容的对象能够相互合作。 6 | 7 | ## 问题 8 | 9 | 假如你正在开发一款股票市场监测程序, 它会从不同来源下载 XML 格式的股票数据, 然后向用户呈现出美观的图表。 10 | 11 | 在开发过程中, 你决定在程序中整合一个第三方智能分析函数库。 但是遇到了一个问题, 那就是分析函数库只兼容 JSON 格式的数据。 12 | 13 | ![整合分析函数库之前的程序结构](https://refactoringguru.cn/images/patterns/diagrams/adapter/problem-zh.png) 14 | 15 | 你无法 “直接” 使用分析函数库, 因为它所需的输入数据格式与你的程序不兼容。 16 | 17 | 你可以修改程序库来支持 XML。 但是, 这可能需要修改部分依赖该程序库的现有代码。 甚至还有更糟糕的情况, 你可能根本没有程序库的源代码, 从而无法对其进行修改。 18 | 19 | ## 解决方案 20 | 21 | 你可以创建一个*适配器*。 这是一个特殊的对象, 能够转换对象接口, 使其能与其他对象进行交互。 22 | 23 | 适配器模式通过封装对象将复杂的转换过程隐藏于幕后。 被封装的对象甚至察觉不到适配器的存在。 例如, 你可以使用一个将所有数据转换为英制单位 (如英尺和英里) 的适配器封装运行于米和千米单位制中的对象。 24 | 25 | 适配器不仅可以转换不同格式的数据, 其还有助于采用不同接口的对象之间的合作。 它的运作方式如下: 26 | 27 | 1. 适配器实现与其中一个现有对象兼容的接口。 28 | 2. 现有对象可以使用该接口安全地调用适配器方法。 29 | 3. 适配器方法被调用后将以另一个对象兼容的格式和顺序将请求传递给该对象。 30 | 31 | 有时你甚至可以创建一个双向适配器来实现双向转换调用。 32 | 33 | ![适配器解决方案](https://refactoringguru.cn/images/patterns/diagrams/adapter/solution-zh.png) 34 | 35 | 让我们回到股票市场程序。 为了解决数据格式不兼容的问题, 你可以为分析函数库中的每个类创建将 XML 转换为 JSON 格式的适配器, 然后让客户端仅通过这些适配器来与函数库进行交流。 当某个适配器被调用时, 它会将传入的 XML 数据转换为 JSON 结构, 并将其传递给被封装分析对象的相应方法。 36 | 37 | ## 适用 38 | 39 | **当你希望使用某个类, 但是其接口与其他代码不兼容时, 可以使用适配器类。** 40 | 41 | 适配器模式允许你创建一个中间层类, 其可作为代码与遗留类、 第三方类或提供怪异接口的类之间的转换器。 42 | 43 | 如果您需要复用这样一些类, 他们处于同一个继承体系, 并且他们又有了额外的一些共同的方法, 但是这些共同的方法不是所有在这一继承体系中的子类所具有的共性。 44 | 45 | **你可以扩展每个子类, 将缺少的功能添加到新的子类中。 但是, 你必须在所有新子类中重复添加这些代码, 这样会使得代码有[坏味道](https://refactoringguru.cn/smells/duplicate-code)。** 46 | 47 | **将缺失功能添加到一个适配器类中是一种优雅得多的解决方案**。 然后你可以将缺少功能的对象封装在适配器中, 从而动态地获取所需功能。 如要这一点正常运作, 目标类必须要有通用接口, 适配器的成员变量应当遵循该通用接口。 这种方式同[装饰](https://refactoringguru.cn/design-patterns/decorator)模式非常相似。 48 | 49 | ## 优缺点 50 | 51 | 优点 52 | 53 | - *单一职责原则*你可以将接口或数据转换代码从程序主要业务逻辑中分离。 54 | - *开闭原则*。 只要客户端代码通过客户端接口与适配器进行交互, 你就能在不修改现有客户端代码的情况下在程序中添加新类型的适配器。 55 | 56 | 缺点 57 | 58 | - 代码整体复杂度增加, 因为你需要新增一系列接口和类。 有时直接更改服务类使其与其他代码兼容会更简单。 59 | 60 | -------------------------------------------------------------------------------- /资源.md: -------------------------------------------------------------------------------- 1 | # 资源 2 | 3 | 很多知识点都是搬运后重新归纳的,跪谢下面这些资料的作者,如有遗漏,欢迎补充。 4 | ## 八股文 5 | 6 | 互联网面试相关知识点学习笔记 7 | 8 | 小林 coding 9 | 10 | 拓跋阿秀 11 | 12 | C++后台开发方向的面经总结 13 | 14 | Java Guide 15 | 16 | linux C++ 服务器/后台开发 秋招整理资料 17 | 18 | 捡田螺的小男孩 19 | 20 | 小白 debug 21 | 22 | [MySQL八股文连环45问(背诵版) - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/403656116) 23 | 24 | [Java 面试指南 | JavaGuide(Java面试+学习指南)](https://javaguide.cn/) 25 | 26 | 微信公众号:前端充电宝 27 | 28 | 29 | 30 | 大数据 31 | 32 | [will-che/BigData-Interview: :dart: :star2:[大数据面试题\]分享自己在网络上收集的大数据相关的面试题以及自己的答案总结.目前包含Hadoop/Hive/Spark/Flink/Hbase/Kafka/Zookeeper框架的面试题知识总结 (github.com)](https://github.com/will-che/BigData-Interview) --------------------------------------------------------------------------------