├── C++.md ├── README.md ├── Redis.md ├── 场景题.md ├── 手撕源码.md ├── 操作系统.md ├── 数据库.md ├── 计算机网络.md └── 设计模式.md /C++.md: -------------------------------------------------------------------------------- 1 | 多态: 2 | 3 | 1. 虚函数表和虚函数表指针 4 | 5 | 对于含有虚函数的类,编译器会在编译阶段创建一个虚函数表vftable(按照虚函数的声明顺序保存虚函数的地址),并在创建类对象时插入一个虚函数表指针vfptr,该指向vftable的首地址(也就是第一个虚函数的地址)。 虚函数表是静态的,供该类的所有对象共享,在编译期确定,存放在全局(静态)变量区的.rodata段。 虚函数表指针是动态的,该类的每个对象都有一个,在运行期确定,存放位置和对象的存放位置相同(堆区/栈区)。 继承时,子类会深拷贝一份父类的虚函数表,如果子类重写了虚函数,那么子类的虚函数表中对应的虚函数地址会被覆盖为重写的虚函数地址。 6 | 7 | 2. 多态为什么函数传参是传指针或者引用,按值传大概率不行 8 | 9 | 按值传递会导致切片问题,会导致丢失派生类的信息,从而无法实现多态; 10 | 11 | 按值传递需要拷贝副本,性能开销大; 12 | 13 | 3. 构造函数和析构函数可以定义为虚函数吗 14 | 15 | 在继承中,先构造基类再构造子类。如果构造函数是虚函数,则需要通过虚函数表调用,但是此时对象还没有实例化,无法访问虚函数表若父类不是虚析构,使用父类指针指向子类,就只会调用父类的析构函数(子类的析构函数在父类中不存在,父类不能够调用子类的析构函数)没法按照子类->父类的顺序进行析构 16 | 17 | 4. 哪些函数不能是虚函数 18 | 19 | 构造函数、静态成员函数(编译器确定,不能动态绑定)、友元函数(不能继承)、内联函数(编译器展开,不能动态绑定) 20 | 21 | 5. 静态多态 22 | 23 | 函数重载:c++中编译器会根据函数参数数量、类型和顺序的不同,生成不同的函数签名,在调用该函数时,编译器会根据传递给函数的参数类型和数量来选择匹配的函数。泛型:包括类模版,函数模版等,一份抽象的模版代码可以根据参数类型实例化出多份不同的代码。 24 | 25 | 6. 拷贝构造/赋值函数 26 | 27 | 默认构造函数、有参构造函数、拷贝构造函数(传参必须是引用方式,否则会无穷递归,因为按值传递时候会继续调用拷贝构造函数) 28 | 29 | 7. 移动构造/赋值函数 30 | 31 | 移动构造函数(必须要加noexcept,否则如vector之类的在扩容时不会调用移动构造,只会调用拷贝构造) 32 | 33 | 将资源的所有权从一个对象转移到另一个对象 34 | 35 | 8. 菱形继承 36 | 37 | 虚继承,关键字virtual 38 | 39 | 9. overload和override的区别 40 | 41 | overload是同一作用域内有多个具有相同名称但参数列表不同的函数,编译时多态或静态多态。override是派生类函数重写基类的虚函数,运行时多态或动态多态 42 | 43 | 10. 深拷贝与浅拷贝 44 | 45 | 浅拷贝:仅复制对象的基本数据类型和指针成员的值,但是不会赋值指针指向的内存,可能导致两个对象共享相同内存; 46 | 47 | 深拷贝:不仅拷贝对象的基本数据类型和指针成员的值,还会拷贝指针所指向的内存。 48 | 49 | 11. this指针 50 | 51 | this指针是指向当前对象的一个隐式指针,可以在成员函数内部使用它来引用对象的成员。 52 | 53 | 关键字: 54 | 55 | 12. const 56 | 57 | 修饰变量:只读变量,不能修改 58 | 59 | 修饰函数:修饰函数的返回值,表示函数的返回值只读,不能被修改; 60 | 61 | 修饰指针:常量指针(int* const)、指向常量的指针(const int*) 62 | 63 | 修饰成员函数:不能修改任何成员变量,因此也只能调用常成员函数 64 | 65 | 13. sizeof 66 | 67 | sizeof 是一个用于获取数据类型或对象的大小(占用的字节数)的运算符。 68 | 69 | 14. volatile 70 | 71 | 防止编译器优化 72 | 73 | 指示变量可能被外部因素更改 74 | 75 | 场景:多任务环境下各任务间共享的标志应该加 volatile;存储器映射的硬件寄存器通常也要加 volatile 说明,因为每次对它的读写都可能有不同意义 76 | 77 | 15. defineinline 78 | 79 | define宏定义只在预处理阶段起作用,仅仅是简单的文本替换,没有类型检查 80 | 81 | inline在编译阶段进行替换,有类型检查 82 | 83 | 16. 为什么使用内联函数inline 84 | 85 | 减少函数调用开销,函数调用涉及压栈、传参、弹栈,内联展开避免了这些开销,直接在调用点插入函数体; 86 | 87 | 减少跳转和缓存不命中 88 | 89 | 17. 递归函数栈使用上可能出现什么问题 90 | 91 | 如果递归函数没有终止条件或者深度过大,会导致栈溢出 92 | 93 | 18. extern 94 | 95 | 声明外部变量或函数,使得不同的源文件共享相同的变量或者函数 96 | 97 | extern “C”:可以让 C++ 来调用 C 语言当中的变量或函数。 98 | 99 | 19. 4种类型转换 100 | 101 | static_cast:基本数据类型间转换,如int转为double,也可以用于基类和派生类间的上行转换(派生类指针—>基类指针) 102 | 103 | dynamic_cast:主要用于基类和派生类间的安全类型转换,运行时执行安全检查,要求基类必须有虚函数。流程:首先,dynamic_cast 通过查询对象的 vptr 来获取其 RTTI;然后,dynamic_cast 比较请求的目标类型与从 RTTI 获得的实际类型。如果目标类型是实际类型或其基类,则转换成功。如果目标类型是派生类,dynamic_cast 会检查类层次结构,以确定转换是否合法。如果在类层次结构中找到了目标类型,则转换成功;否则,转换失败。const_cast:去const属性,常量指针转换为非常量指针,并且仍然指向原来的对象 104 | 105 | reinterpreter_cast:仅仅重新解释类型,但没有进行二进制的转换 106 | 107 | 20. struct和union区别 108 | 109 | struct:每个成员有自己的内存空间,结构体的总大小是各成员大小之和,可以安全地访问所有成员; 110 | 111 | union:所有成员共享同一块内存空间,大小等于最大成员的大小。 112 | 113 | 21. struct和class区别 114 | 115 | class 中类中的成员以及继承默认都是 private 属性的。而在 struct 中结构体中的成员和继承默认都是 public 属性的。 class 可以用于定义模板参数,struct 不能用于定义模板参数。 116 | 117 | 22. static 118 | 119 | 修饰全局变量:将变量作用域限制在当前文件中,其他文件无法访问; 120 | 121 | 修饰局部变量:静态局部变量在函数调用结束后并不会被销毁,而是保留其值,并在下一次调用该函数时继续使用; 122 | 123 | 修饰成员变量:成为静态成员变量,属于整个类,在所有实例之间共享; 124 | 125 | 修饰成员函数:静态成员函数,可以直接通过类名来访问,而无需创建对象,没有this指针。 126 | 127 | 23. New/delete和malloc/free区别 128 | 129 | 语法不同:malloc/free 是一个 C 语言的函数,而 new/delete 是 C++ 的运算符。分配内存的方式不同:malloc 只分配内存,而 new 会分配内存并且调用对象的构造函数来初始化对象。返回值不同:malloc 返回一个 void 指针,需要自己强制类型转换,而 new 返回一个指向对象类型的指针。malloc 需要传入分配的大小,而 new 编译器会自动计算所构造对象的大小 130 | 131 | 24. noexcept 132 | 133 | 表示函数内部不会抛出异常,有助于简化调用该函数的代码,而且编译器确认函数不会抛出异常,它就能执行某些特殊的优化操作。 134 | 135 | 25. mutable 136 | 137 | mutable只能用来修饰类的成员变量,表示可以在const成员函数中修改。STL: 138 | 139 | 26. stl容器及其实现: 140 | 141 | array:静态数组,不可扩充,内存连续,栈上存储,大小需要在编译期确定vector:动态数组,空间不够时会重新分配内存;内存连续,堆上数组deque:双端数组,可对头部和尾部进行插入和删除的操作,也是动态数组;内部有个中控器,维护每个缓冲区的地址,真实的数据存在缓冲区内,缓冲区内部是连续的,但是缓冲区之间是分散的,所以访问速度慢; 142 | 143 | list:双向循环链表,可以对任意位置进行快速插入或删除元素,动态存储分配,遍历速度较慢。 144 | 145 | 27. vector扩容 146 | 147 | 申请2倍于现有大小的内存空间,将现有元素调用拷贝构造函数到新内存上,对原空间上的元素调用析构函数,并释放原有空间 148 | 149 | 28. push_back()emplace_back()的区别 150 | 151 | push_back()先创建临时对象,然后将临时对象拷贝到容器中,最后销毁临时对象;emplace_back()直接在容器中就地构造对象 152 | 153 | 29. set/map 154 | 155 | 用红黑树实现,插入时自动排序。红黑树是一种自平衡的二叉搜索树,兼顾了插入时和搜索时的效率,插入、删除查找的复杂度都是O(logn),相比严格平衡的AVL树,红黑树是弱平衡的,插入最多旋转2次,删除最多旋转3次。 156 | 157 | 30. unordered_set/unordered_map 158 | 159 | 哈希表/散列表,插入的元素是无序的,不允许容器中有重复的元素;查找的复杂度是O(1);数据插入和查找的时间复杂度很低,而代价是占用比较多的内存; 160 | 161 | 31. 哈希表内部原理 162 | 163 | 哈希桶(buckets):存放key值不同,但哈希值相同的容器(一般是链表);如何避免单个哈希桶的元素过多?当元素大于阈值后可以rehash,或者把链表换成红黑树or哈希表 ;哈希桶向量(buckets vector):用于存放哈希桶的指针 节点(node):用于存放key-value 桶大小,节点数量:表示当前一共有几个桶,一共有几个节点 哈希冲突处理方法:开放寻址,链地址法,拉链法 164 | 165 | 32. stl线程安全 166 | 167 | 不是线程安全的,单线程写和多线程读,写的时候可能会导致读的迭代器失效。多线程写更有可能 168 | 169 | C++11新特性 170 | 171 | 33. autodecltype 172 | 173 | auto:编译器在编译期自动推导变量类型decltype:编译器在编译期自动推导表达式类型 174 | 175 | 34. lambda表达式 176 | 177 | lambda表达式本质就是一个仿函数。它通过编译器生成一个匿名类的对象来实现,这个匿名类重载了函数调用运算符 operator(),使得我们可以像调用普通函数一样来调用lambda表达式。 178 | 179 | [capture list] (params list) mutable exception -> return_type { function body } 180 | 181 | 35. 右值引用、移动语义、完美转发 182 | 183 | 可以取地址的、有名字的是左值,不能取地址、没有名字的是右值; 184 | 185 | 移动语义:允许资源(如动态内存)从一个对象转移到另一个对象的机制 move(将左值强制转化为右值引用)完美转发:允许函数将参数以几乎完全相同的形式转发给其他函数,保证参数的左值或右值特性被保留 forword 186 | 187 | 36. 智能指针 188 | 189 | RAII:资源获取即初始化,资源获取应该在构造函数中进行,资源释放应该在析构函数中进行。 190 | 191 | 智能指针:一种资源管理工具,用于管理动态分配的内存,避免内存泄漏。 192 | 193 | unique_ptr:对一个对象独占所有权,不与其他unique_ptr共享,不支持拷贝 194 | 195 | shared_ptr:多个shared_ptr可以共享同一个对象,通过引用计数来跟踪有多少个 shared_ptr 指向该资源,引用计数为0时自动释放。 196 | 197 | 底层实现:shared_ptr 的实现是采用引用计数的原理,它除了有一个指向数据的原始指针外,还有一个指向资源控制块(包括引用计数值,次级引用计数,删除器等)的指针。 198 | 199 | weak_ptr:弱引用的智能指针,不参与管理对象的生命周期,解决循环引用的问题。 200 | 201 | 37. shared_ptr是否线程安全 202 | 203 | 多线程对同一个引用计数增加或减少是安全的,原子性 204 | 205 | 多线程对shared_ptr指向的资源进行读写不是线程安全的,需要额外的同步机制 206 | 207 | 内存管理: 208 | 209 | 38. 内存模型,地址由低到高: 210 | 211 | 代码段:存放程序的二进制机器码 212 | 213 | 数据段data:存放初始化的全局变量、静态变量和常量 214 | 215 | BSS段:存放未初始化的全局变量和静态变量 216 | 217 | 堆区:由程序员分配和释放,所有malloc/free,new/delete都在堆上进行操作。会造成内存泄漏。由于堆区存在内存管理、地址映射等复杂操作,故效率较低。此外还会产生内存碎片影响效率。但空间大 218 | 219 | 栈区:存放局部变量、函数参数值、形参等。由编译器分配和释放,栈区只需要改变栈指针的位置,因此效率高。但栈区空间小 220 | 221 | 39. C++代码到程序执行过程 222 | 223 | 预处理:主要处理#include指令,宏替换,条件编译等,生成.i文件 224 | 225 | 编译:对源代码进行语法分析、词法分析,生成汇编代码,产生.s文件, 226 | 227 | 汇编:将汇编代码翻译成机器码,生成.o目标文件 228 | 229 | 链接:将目标文件和库文件链接在一起,生成最终的可执行文件 230 | 231 | 40. 动态库静态库 232 | 233 | 动态库中的代码和数据是运行时加载到进程中的,而不是链接时静态编译到可执行文件中的。当动态库被关闭时(动态库代码从内存中释放),对应的实例无法继续使用。静态库中的代码和数据是链接时静态编译到可执行文件中的,而不是运行时加载到进程中的。 234 | 235 | 41. 内存泄漏 236 | 237 | 申请了一块内存空间,使用完毕后没有释放掉。 238 | 239 | 检测方法:采用RAII思想。 240 | 241 | 使用GDB,执行某个函数前后,call malloc_stats(),对比in use bytes大小是否变化 242 | 243 | 使用valgrind,valgrind --leak-check=yes program arg1 arg2 244 | 245 | 42. 指针引用 246 | 247 | 指针是一个变量,它保存了另一个变量的内存地址;引用是另一个变量的别名,与原变量共享内存地址。 指针可以被重新赋值,指向不同的变量;引用在初始化后不能更改,始终指向同一个变量。 指针可以为 nullptr,表示不指向任何变量;引用必须绑定到一个变量,不能为 nullptr。 使用指针需要对其进行解引用以获取或修改其指向的变量的值;引用可以直接使用,无需解引用。 248 | 249 | 43. 空指针可以访问成员函数吗 250 | 251 | 如果用到this指针(即访问成员变量),则不可以,否则可以 252 | 253 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 记录一下我个人转码的一系列学习笔记,主要是从面经和八股中总结出来的计算机网络、操作系统、数据库、C++、设计模式、场景题、redis的一些高频考点,以及一些C++面试过程中常见的手撕代码,如stl,智能指针,多线程/进程,线程池,单例模式以及LRU等。 2 | -------------------------------------------------------------------------------- /Redis.md: -------------------------------------------------------------------------------- 1 | Redis**持久化**策略:**AOF日志,RDB快照混合模式** 2 | 3 | 1. 执行命令后写日志(防止命令出错,不会阻塞当前命令),写回策略(总是,每秒,系统控制);AOF重写机制,减小日志体积,重写期间,主线程执行命令后会将命令写入**重写缓冲区**,以及**写时复制**(恢复数据较慢)。 4 | 2. 记录某个时间点的实际数据(记录频率不好设置);记录时,主线程可以读写,基于**写时复制**(子进程拷贝内存)。 5 | 6 | **过期删除**策略:惰性删除+定期删除(简单易用,兼顾内存占用和CPU占用) 7 | 8 | **内存淘汰**策略:①不提供写入服务,②淘汰有过期时间的key(随机,时间,lru,lfu),③所有数据中淘汰(随机,lru(额外存储上次访问时间,删除时随机选择几个,时间最久的删除),lfu(存储上次访问时间以及访问次数)) 9 | 10 | 11 | 12 | **缓存****更新**策略:CacheAside(应用程序自己实现缓存和数据库交互问题):先操作数据库,再删除缓存,因为缓存操作速度很快,一般比数据库操作快非常多,不一致性问题发生概率低;Write/ReadThrough(应用数据只与缓存交互,不操作数据库,读时缓存中有直接返回,没有由缓存组件查询数据库更新缓存,写时写缓存,若缓存没有则直接写数据库);WriteBack(只更新缓存,由其他线程异步写回缓存数据)。 13 | 14 | 延迟双删:删除缓存,更新数据库,再过一段时间再删除一次缓存。(延迟时间不好设置,也存在问题) 15 | 16 | 1. 缓存**穿透**:请求数据在缓存中和数据库中都不存在,所有请求都打到数据库 17 | 18 | 解决思路:①缓存空对象:简单实现,内存消耗大,②布隆过滤(使用hash加二进制位(bitmap)判断请求合法性):在缓存之前就判断请求合法性,可能误判(主要是由于hash冲突)(可以在每个哈希位统计次数,即可实现删除键) 19 | 20 | 1. 缓存**雪崩**:大量key同时失效或redis宕机(高可用),导致大量请求同时请求数据库 21 | 22 | 解决思路:①TTL增加随机项,②redis集群,③多级缓存,④降级限流策略 23 | 24 | 1. 缓存**击穿**:部分热点key或重建较复杂的key同时失效(缓存雪崩的子集),导致大量请求同时请求数据库 25 | 26 | 解决思路:①互斥锁(基于SETNX实现),保证只有一个线程可以写入缓存时,其他线程失败重试;②逻辑过期,不设置TTL而是在数据内加入一个过期时间,供于读取缓存时判断。 27 | 28 | 29 | 30 | **数据类型**:String(缓存对象,分布式锁,共享session);List(消息队列);Hash(缓存对象(更省内存));Set(集合操作(共同好友等));Zset(排行榜);Bitmap(签到统计):二进制位;Hyperloglog(UV统计):提供不精确的去重计数。 31 | 32 | **String**:简单动态字符串,二进制格式,保存了长度字段; 33 | 34 | List:双向链表+压缩列表->quicklist,压缩列表紧凑的格式节省了空间,但其基于连续内存且易发生连锁更新现象。Quicklist即使用链表结构把压缩列表分段后串在一起。 35 | 36 | Hash:压缩列表+哈希表->listpack+哈希表,listpack没有记录前一个节点的大小,避免了连锁更新的问题。 37 | 38 | Set:整数集合+哈希表,哈希表基于数组加链表,rehash基于渐进式hash,保存两个hash表,达到阈值时切换。 39 | 40 | ZSet:压缩列表+跳表->listpack+跳表,跳表在创建节点时采用随机的办法来增加层数,为什么不用红黑树,跳表更加简单,更适合做范围查询,内存开销更小。为什么不用B+树,跳表更简单,增删更方便,单节点占用内存更小。 41 | 42 | 43 | 44 | Redis**为什么快**:①主要基于内存操作;②单线程模式避免线程竞争;③采用了I/O多路复用机制(一个线程同时监听多个,select:遍历查找发生读写的socket,epoll:构建红黑树+事件触发模式)处理大量的客户端Socket请求。 45 | 46 | 47 | 48 | Redis是**单线程**吗?单线程指的是接受请求解析处理数据读写由一个主线程来完成,但它还有后台线程,包括了处理关闭文件、AOF刷盘、异步释放内存的后台线程。 49 | 50 | 51 | 52 | **哈希分槽**:对2^16取模,得到槽号,每个redis实例有多个哈希槽。优势:扩缩容方便,期间仍可提供服务,只需移动部分的槽即可,且可以更灵活地根据实例的硬件情况分配槽。而一致性哈希若某个节点挂了可能导致后续节点雪崩。 53 | 54 | **主从****复制**:集群通常读写分离,第一次同步时,从服务器删除数据,并进行RDB全量同步;后续主服务器进行写操作,将同步命令异步发给从服务器(可能造成数据不一致)(长连接)。增量同步:环形缓冲区,维护offset字段 55 | 56 | **哨兵****模式**:主从节点故障自动转移,哨兵检测主节点异常->发现的哨兵投票希望成为leader->进行主从切换(选新主->从节点指向新主->通知客户主节点切换->旧主切换为从节点)。 57 | 58 | 集群**脑裂**:主节点临时异常,导致新主节点出现,此时原主节点故障期间的数据操作丢失,主节点检测到异常(与从节点连接消失或上一次同步时间过去较久)时,限制数据操作,给客户端返回错误信息。 59 | 60 | 61 | 62 | **大Key**问题:操作大key耗时较久,客户端超时阻塞;使用del删除大Key,阻塞工作线程;持久化时创建子进程复制页表时间增加,主线程修改大key时拷贝物理内存耗时。删除方案:①分批次删除,②异步删除unlink。 63 | 64 | **热Key**问题: 65 | 66 | 监控:解析AOF日志,或是通过其他脚本统计key的访问次数; 67 | 68 | 解决:①建立Redis集群,分摊压力;②数据预热,在高并发之前,提前将热点数据加载进缓存;③ 限流和降级,保护好后台服务;④使用多级缓存,一级缓存使用本地内存,缓存最近频繁访问的数据,速度快,容量小,当从二级缓存或后端数据库获取数据时同步更新本地缓存;二级缓存使用分布式缓存系统如redis,缓存相对频繁访问的数据,速度快容量大,当从后端数据库获取数据时,同步更新二级缓存;当数据在后端数据库中发生变化时,异步更新二级缓存。 69 | 70 | 71 | 72 | | | 事务 | Lua脚本 | 管道 | Mget\Mset | 73 | | --- | --- | :--- | :--- | :--- | 74 | | 网络请求次数 | 多次请求 | 单次请求 | 单次请求 | 单次请求 | 75 | | 响应次数 | 单次响应 | | | | 76 | | 执行原子性 | 保证,阻塞其他请求 | 保证,阻塞其他请求 | 不保证,其他请求仍可执行 | 原生命令,保证 | 77 | | 错误后续是否执行 | 仍执行 | 不执行 | 仍执行 | 原生命令 | 78 | | 错误是否回滚 | 不回滚 | 不回滚 | 不回滚 | 原生命令、回滚 | 79 | | 获得前面结果 | 不可以 | 可以 | 不可以 | | 80 | 81 | 82 | Redis事务,一个事务在执行时会阻塞其他请求,保证一定的原子性,但不支持事务回滚,且事务中不能获取前面的执行结果。 83 | 84 | Lua脚本,脚本内部可以获取中间命令的执行结果,且可以保证原子性,但lua脚本也会阻塞其他,尽量不要执行过大开销的操作。 85 | 86 | 管道技术,只需建立一次通信,批量发送请求与接收结果,缓解网络延迟的影响,不保证原子性(只能保证单个客户端内的串行执行)。 87 | 88 | Mget,mget只能进行一种操作,即get,而管道中可以执行更多样的命令。Mget一次性执行后返回结果,而管道则依次执行后返回结果。Mget为为原生命令,可以保证原子性,管道不保证原子性。 89 | 90 | 面对hash分槽时无法保证原子性。 91 | 92 | 93 | 94 | 分布式锁,获取锁:set nx px;解锁:先判断是否为锁持有者,再删除锁(可重入锁将次数-1) 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /场景题.md: -------------------------------------------------------------------------------- 1 | **1、****3000个数里面找最大的k个数**:堆排序,构造大/小根堆:从后到前,从倒数第一个非叶子节点len/2-1开始(buildHeap);从上到下,根和大于根的最大子节点交换(adjustOnce);排序:首尾交换,重新构造大/小根堆,重复操作。 2 | 3 | 时间复杂度:O(nlogk) 4 | 5 | 排序算法场景分析: 6 | 7 | 数据量较少:插入、选择 8 | 9 | 基本有序(原本有序,新插入元素):插入、冒泡 10 | 11 | 随机分布:快速 12 | 13 | 数据量大:快速、堆、归并 14 | 15 | 稳定:归并、冒泡、插入、桶、基数 16 | 17 | 不稳定:选择、快速、堆 18 | 19 | **外部排序:**将数据分成若干个小块,每个小块都能够放入内存中进行排序。通过归并排序等算法对每个小块进行排序后,再将所有小块合并成一个有序的大块(大根堆or小根堆)。 20 | 21 | **2、****有一个文件,里面全部是不重复的整数,整数最大值不超过七位数,input是文件,output是一个文件,将文件里面整数排****序**:使用位图,每个位只代表整数是否存在,遍历每个读取到的整数,将位图中对应索引的值设为true,遍历位图,按顺序将值位true的数写入文件。 22 | 23 | 如何优化离散数据分布:采用分段位图,将数据分为多个段,仅当段内有数据时,才分配内存。 24 | 25 | **3、****亿级别搜索日志**,从中筛选出**top10的热词**。将数据分成多个小块,并在多台计算机上同时运行分布式程序。每个计算机都执行“map”和“reduce”两个阶段的操作:Map阶段:输入数据被分割成小块,每个块由一个特定的计算机节点处理。对于每个块,该节点会应用用户定义的“map”函数,将输入键值对映射为中间键值对。比如可以把文本文件中的每一行转化为(word, 1)的形式。Reduce阶段:所有中间键值对被收集合并,然后按照键进行排序,最终输出结果。每个计算节点都会应用用户定义的“reduce”函数,将具有相同键的值聚合在一起,生成最终结果。例如在上述例子中,reduce阶段可以把所有key为word的value相加,得到这个单词在文本中出现的次数。 26 | 27 | **4、**两个**50亿url的文件**,找到其中**相同的url**,也是哈希分桶,接着内存够用了,接着在桶内用哈希表再查找相同的,也可以构建前缀树进行查找,若长度不会很长的话。 28 | 29 | **5、**海量数据如何寻找中位数,从二进制最高位开始以0和1进行分治,找中点。 30 | 31 | **6、****秒杀系统**:发布秒杀活动时,将商品与库存写入redis。当用户进行抢购时,直接对redis的库存进行操作。扣减redis内库存数量后,发送扣减成功的消息到消息队列,由后续的订单服务和支付服务进行处理。最后再由mysql进行库存扣减。**超卖问题**:秒杀时分两步:首先判断库存是否充足,之后扣减库存数量。这两步必须要保证操作原子性;少卖问题:扣减了库存,但是消息没发出,订单生成失败,导致没有卖出去。扣减库存后,发送消息队列需要有重试策略,如果消息发送失败需要重试,超过重试次数后,则需要持久化到磁盘记录下来,由补偿服务扫描是否存在少卖问题,进行后续业务处理。 32 | 33 | **7、****40亿个qq号怎么去重**:大数据去重可以是用bitmap,可以分批次读入内存(将Bitmap按照一定的规则进行划分(如按照数据区间划分,0-10000是一个Bitmap),将其分成若干个较小的区块,每个区块对应一个Bitmap。这样可以避免一次性将整个Bitmap加载到内存中),将qq号对应的位置置为1,最后遍历位图即可得到去重过的qq号。进一步节省空间可以考虑使用位图压缩算法,或者多层级的位图,适用于较稀疏的数据分布。若对结果的准确性没有很严格的要求,也可以使用布隆过滤器,会存在一定的误判。 34 | 35 | **8、****密码如何传输和存储**:传输一定得用https,在服务端,通过原密码+salt经过哈希加密算法(可以使用慢哈希)进行比对,服务端一般不存储原始密码,而比较加密之后的密钥。多次输错后如何锁定:限制IP或限制用户,一般来说限制IP。基于Redis,key为标识IP登录的唯一标识符,value为输错次数;或者直接在mysql表内存储相应字段也可以。 36 | 37 | **9、****推荐系统设计:**召回:粗筛->精排粗筛:对每个视频抽取特征进行聚类,计算每个视频和类中心的相关性,按照相关性进行排序。用户特征到来后,计算和每一类中心的相关性,筛选出相关性最高的前k类视频。精排:在前k类视频中,对每一类选出和相关性正相关数量的视频,推荐给用户 38 | 39 | **10、****广告系统设计:** 40 | 41 | **架构层面:** 42 | 43 | 前端展示:用户通过微信客户端查看朋友圈页面。后端服务:负载均衡器分发请求,广告投放服务选择广告,用户画像服务构建用户画像,广告效果监测服务记录效果。数据存储:使用 MySQL存储数据,Redis 缓存高频数据,Kafka 或 RabbitMQ 处理异步任务。 (存储用户数据(基本信息、行为数据如点击历史、偏好设置),广告数据(存储广告的基本信息,投放规则,排期信息等),广告投放记录(记录广告的展示、点击等行为))**业务逻辑层面:** 44 | 45 | 广告选择:根据用户画像和广告排期规则选择广告。广告展示:展示广告给用户,并从缓存或数据库中获取广告数据。广告点击处理:记录点击行为,并将用户添加到广告屏蔽列表中。广告效果监测:记录点击率和转化率等指标,并优化广告投放策略。 46 | 47 | **11、****分布式kv一致性哈希:**一致性哈希:包含两步:对存储节点进行哈希计算,如计算存储节点的IP哈希值,结果对2^32取余;对key进行哈希计算,结果对2^32取余;根据key的哈希值,顺时针寻找到的第一个存储节点,就是存储该key的存储节点。在一致哈希算法中,如果增加或者移除一个节点,仅影响该节点在哈希环上顺时针相邻的后继节点,其它数据也不会受到影响,但是一致性哈希算法并不保证节点能够在哈希环上分布均匀。为了解决分布不均匀的问题,加入虚拟节点,因为节点越多,分布越均匀。不再将真实节点映射到哈希环上,而是将虚拟节点映射到哈希环上,并将虚拟节点映射到实际节点,所以这里有「两层」映射关系。 48 | 49 | -------------------------------------------------------------------------------- /手撕源码.md: -------------------------------------------------------------------------------- 1 | 1、快排 2 | 3 | ```cpp 4 | void quicksort(vector& nums, int left, int right) { 5 | if (left >= right) { 6 | return; 7 | } 8 | int pivot = left, l = left + 1, r = right; 9 | while (l < r) { 10 | while (nums[l] >= nums[pivot] && l < r) { 11 | l++; 12 | } 13 | while (nums[r] <= nums[pivot] && l < r) { 14 | r--; 15 | } 16 | swap(nums[l], nums[r]); 17 | } 18 | swap(nums[pivot], nums[r]); 19 | quicksort(nums, left, r - 1); 20 | quicksort(nums, r + 1, right); 21 | } 22 | ``` 23 | 24 | 25 | 26 | 2、堆排序 27 | 28 | ```cpp 29 | //建堆 30 | void buildHeap(vector& nums,int size) { 31 | for(int i=size/2-1;i>=0;--i) { 32 | adjustOnce(nums,i,size); 33 | } 34 | } 35 | //调整一次 36 | void adjustOnce(vector& nums,int root,int size) { 37 | int l=root*2+1,r=root*2+2,maxNode=root; 38 | if(lnums[maxNode]) maxNode=l; 39 | if(rnums[maxNode]) maxNode=r; 40 | if(maxNode!=root) { 41 | std::swap(nums[root],nums[maxNode]); 42 | adjustOnce(nums,maxNode,size); 43 | } 44 | } 45 | //堆排序 46 | void heapSort(vector& nums){ 47 | buildHeap(nums,nums.size()); 48 | for(int i=nums.size()-1;i>0;--i) { 49 | std::swap(nums[0],nums[i]); 50 | buildHeadp(nums,i); 51 | } 52 | } 53 | ``` 54 | 55 | 56 | 57 | 3、LRU 58 | 59 | ```cpp 60 | class Node { 61 | public: 62 | int key, value; 63 | Node *prev, *next; 64 | Node(int k, int v) : key(k), value(v) {} 65 | }; 66 | 67 | class LRUCache { 68 | private: 69 | int capacity; 70 | Node *dummy; 71 | unordered_map key_to_node; 72 | 73 | void remove(Node *x) { 74 | x -> prev -> next = x -> next; 75 | x -> next -> prev = x -> prev; 76 | } 77 | 78 | void push_front(Node *x) { 79 | x -> prev = dummy; 80 | x -> next = dummy -> next; 81 | dummy -> next -> prev = x; 82 | dummy -> next = x; 83 | } 84 | 85 | Node *get_node(int key) { 86 | auto it = key_to_node.find(key); 87 | if (it == key_to_node.end()) { 88 | return nullptr; 89 | } 90 | auto node = it -> second; 91 | remove(node); 92 | push_front(node); 93 | return node; 94 | } 95 | public: 96 | LRUCache(int capacity) : capacity(capacity), dummy(new Node()) { 97 | dummy -> prev = dummy; 98 | dummy -> next = dummy; 99 | } 100 | int get(int key) { 101 | auto node = get_node(key); 102 | return node ? node -> value : -1; 103 | } 104 | 105 | void put(int key, int value) { 106 | auto node = get_node(key); 107 | if (node) { 108 | node -> value = value; 109 | return; 110 | } 111 | key_to_node[key] = new Node(key, value); 112 | push_front(node); 113 | if (key_to_node.size() > capacity) { 114 | auto back_node = dummy -> prev; 115 | key_to_node.erase(back_node -> key); 116 | remove(back_node); 117 | delete back_node; 118 | } 119 | } 120 | 121 | }; 122 | ``` 123 | 124 | 125 | 126 | 4、多线程 127 | 128 | ```cpp 129 | #include 130 | #include 131 | #include 132 | #include 133 | #include 134 | 135 | using namespace std; 136 | 137 | mutex mtx; 138 | condition_variable cond; 139 | 140 | vector s = { 'A', 'B', 'C' }; 141 | int N = s.size(); 142 | int print_times = 10; 143 | char message = 'A'; 144 | 145 | void handler(int i) { 146 | for (int j = 0; j < print_times; ++j) { 147 | unique_lock lk(mtx); 148 | while (message != s[i]) { 149 | cond.wait(lk); 150 | } 151 | 152 | cout << this_thread::get_id() << " : " << message << endl; 153 | message = s[(i + 1) % N]; 154 | 155 | //lk.unlock(); 156 | cond.notify_all(); 157 | } 158 | } 159 | 160 | int main() { 161 | vector thr; 162 | 163 | for (int i = 0; i < N; ++i) { 164 | thr.push_back(thread(handler, i)); 165 | } 166 | 167 | for (int i = 0; i < N; ++i) { 168 | thr[i].join(); 169 | } 170 | 171 | return 0; 172 | } 173 | ``` 174 | 175 | 176 | 177 | 5、queue 178 | 179 | ```cpp 180 | #include 181 | #include 182 | using namespace std; 183 | 184 | template 185 | class myqueue { 186 | public: 187 | myqueue() {} 188 | void push(const T& value) { 189 | data.push_back(value); 190 | } 191 | void pop() { 192 | if (!data.empty()) { 193 | data.erase(data.begin()); 194 | } 195 | else { 196 | cout << "Queue is empty!" << endl; 197 | } 198 | } 199 | const T& front() const { 200 | if (!data.empty()) { 201 | return data.front(); 202 | } 203 | else { 204 | throw out_of_range("Queue is empty!"); 205 | } 206 | } 207 | size_t size() const { 208 | return data.size(); 209 | } 210 | bool empty() const { 211 | return data.empty(); 212 | } 213 | private: 214 | vector data; 215 | }; 216 | 217 | int main() { 218 | myqueue q; // 创建一个整型队列 219 | 220 | // 入队操作 221 | q.push(5); 222 | q.push(10); 223 | q.push(15); 224 | 225 | // 出队操作 226 | cout << "出队:" << endl; 227 | q.pop(); // 出队 5 228 | q.pop(); // 出队 10 229 | 230 | // 获取队头元素 231 | cout << "获取队头元素:" << endl; 232 | cout << q.front() << endl; // 应该是 15 233 | 234 | // 判断队列是否为空 235 | cout << "判断队列是否为空:" << endl; 236 | cout << q.empty() << endl; // 应该是 0(false) 237 | 238 | return 0; 239 | } 240 | ``` 241 | 242 | 243 | 244 | 6、stack 245 | 246 | ```cpp 247 | #include 248 | #include 249 | using namespace std; 250 | 251 | template 252 | class mystack { 253 | public: 254 | mystack() {} 255 | void push(const T& value) { 256 | data.push_back(value); 257 | } 258 | void pop() { 259 | if (!data.empty()) { 260 | data.pop_back(); 261 | } 262 | else { 263 | cout << "Stack is empty!" << endl; 264 | } 265 | } 266 | const T& top() const { 267 | if (!data.empty()) { 268 | return data.back(); 269 | } 270 | else { 271 | throw out_of_range("Stack is empty!"); 272 | } 273 | } 274 | size_t size() const { 275 | return data.size(); 276 | } 277 | bool empty() const { 278 | return data.empty(); 279 | } 280 | private: 281 | vector data; 282 | }; 283 | 284 | int main() { 285 | mystack s; // 创建一个整型栈 286 | 287 | // 入栈操作 288 | s.push(5); 289 | s.push(10); 290 | s.push(15); 291 | 292 | // 出栈操作 293 | cout << s.top() << endl; // 15 294 | s.pop(); 295 | cout << s.top() << endl; // 10 296 | s.pop(); 297 | cout << s.top() << endl; // 5 298 | 299 | // 判断栈是否为空 300 | cout << "判断栈是否为空:" << endl; 301 | cout << s.empty() << endl; // 0(false) 302 | 303 | return 0; 304 | } 305 | ``` 306 | 307 | 308 | 309 | 7、string 310 | 311 | ```cpp 312 | #include 313 | #include // For strlen, strcpy, etc. 314 | 315 | class mystring { 316 | public: 317 | mystring() : data(nullptr), length(0) {} 318 | mystring(const char* str) { 319 | if (str == nullptr) { 320 | data = nullptr; 321 | length = 0; 322 | } 323 | else { 324 | length = strlen(str); 325 | data = new char[length + 1]; 326 | strcpy(data, str); 327 | } 328 | } 329 | mystring(const mystring& other) { 330 | length = other.length; 331 | if (other.data == nullptr) { 332 | data = nullptr; 333 | } 334 | else { 335 | data = new char[length + 1]; 336 | strcpy(data, other.data); 337 | } 338 | } 339 | ~mystring() { 340 | if (data != nullptr) { 341 | delete[] data; 342 | data = nullptr; 343 | length = 0; 344 | } 345 | } 346 | mystring& operator=(const mystring& other) { 347 | if (this == &other) { 348 | return *this; 349 | } 350 | delete[] data; 351 | length = other.length; 352 | if (other.data == nullptr) { 353 | data = nullptr; 354 | } 355 | else { 356 | data = new char[length + 1]; 357 | strcpy(data, other.data); 358 | } 359 | return *this; 360 | } 361 | const char* c_str() const { 362 | return data; 363 | } 364 | size_t size() const { 365 | return length; 366 | } 367 | mystring operator+(const mystring& other) const { 368 | mystring result; 369 | result.length = length + other.length; 370 | result.data = new char[result.length + 1]; 371 | strcpy(result.data, data); 372 | strcat(result.data, other.data); 373 | return result; 374 | } 375 | char& operator[](size_t index) { 376 | if (index >= length) { 377 | throw std::out_of_range("Index out of range"); 378 | } 379 | return data[index]; 380 | } 381 | private: 382 | char* data; 383 | size_t length; 384 | }; 385 | 386 | int main() { 387 | mystring str1("Hello"); 388 | mystring str2("World"); 389 | 390 | mystring str3 = str1 + str2; 391 | std::cout << "Concatenated string: " << str3.c_str() << std::endl; 392 | 393 | str3[0] = 'h'; // Changing the first character 394 | std::cout << "Modified string: " << str3.c_str() << std::endl; 395 | 396 | mystring str4; 397 | str4 = "Assignment test"; 398 | std::cout << "Assigned string: " << str4.c_str() << std::endl; 399 | 400 | return 0; 401 | } 402 | ``` 403 | 404 | 405 | 406 | 8、unique_ptr 407 | 408 | ```cpp 409 | #include 410 | template 411 | class myunique_ptr { 412 | private: 413 | T* ptr_; 414 | public: 415 | //禁用默认和拷贝构造 416 | myunique_ptr() = delete; 417 | myunique_ptr(const myunique_ptr& p) = delete; 418 | myunique_ptr(T* ptr = nullptr) :ptr_(ptr) {} 419 | myunique_ptr(myunique_ptr&& p) { 420 | ptr_ = p.ptr_; 421 | p.ptr_ = nullptr; 422 | } 423 | //禁用拷贝赋值 424 | myunique_ptr& operator=(const myunique_ptr& p) = delete; 425 | myunique_ptr& operator=(myunique_ptr&& p) { 426 | if(&p != this) { 427 | if (ptr_ != nullptr) { 428 | delete ptr_; 429 | } 430 | ptr_ = p.ptr_; 431 | p.ptr_ = nullptr; 432 | } 433 | return *this; 434 | } 435 | ~myunique_ptr() { 436 | if (ptr_ != nullptr) { 437 | delete ptr_; 438 | ptr_ = nullptr; 439 | } 440 | } 441 | 442 | }; 443 | ``` 444 | 445 | 446 | 447 | 9、vector 448 | 449 | ```cpp 450 | #include 451 | using namespace std; 452 | 453 | template 454 | class vector { 455 | public: 456 | vector(int size = 10) { 457 | first_ = new T[size]; 458 | last_ = first_; 459 | end_ = first_ + size; 460 | } 461 | ~vector() { 462 | delete[] first_; 463 | first_ = last_ = end_ = nullptr; 464 | } 465 | vector(const vector& rhs) { 466 | int size = rhs.end_ - rhs.first_; 467 | first_ = new T[size]; 468 | int len = rhs.last_ - rhs.first_; 469 | for (int i = 0; i < len; i++) { 470 | first_[i] = rhs.first_[i]; 471 | } 472 | last_ = first_ + len; 473 | end_ = first_ + size; 474 | } 475 | vector& operator=(const vector& rhs) { 476 | if (this == &rhs) { 477 | return *this; 478 | } 479 | delete[] first_; 480 | int size = rhs.end_ - rhs.first_; 481 | first_ = new T[size]; 482 | int len = rhs.last_ - rhs.first_; 483 | for (int i = 0; i < len; i++) { 484 | first_[i] = rhs.first_[i]; 485 | } 486 | last_ = first_ + len; 487 | end_ = first_ + size; 488 | return *this; 489 | } 490 | void push_back(const T& val) { 491 | if (full()) { 492 | expand(); 493 | } 494 | *last_++ = val; 495 | } 496 | void pop_back() { 497 | if (empty()) { 498 | return; 499 | } 500 | --last_; 501 | } 502 | T back() const { 503 | return *(last_ - 1); 504 | } 505 | bool full() const { 506 | return last_ == end_; 507 | } 508 | bool empty() const { 509 | return first_ == last_; 510 | } 511 | int size() const { 512 | return last_ - first_; 513 | } 514 | private: 515 | T* first_; //指向数组起始的位置 516 | T* last_; //指向数组中有效元素的后继位置 517 | T* end_; //指向数组空间的后继位置 518 | 519 | void expand() { 520 | int size = end_ - first_; 521 | T* ptr = new T[2 * size]; 522 | for (int i = 0; i < size; i++) { 523 | ptr[i] = first_[i]; 524 | } 525 | delete[] first_; 526 | first_ = ptr; 527 | last_ = first_ + size; 528 | end_ = first_ + 2 * size; 529 | } 530 | }; 531 | 532 | int main() { 533 | vector vec; 534 | for (int i = 0; i < 10; i++) { 535 | vec.push_back(i); 536 | cout << vec.back() << " " ; 537 | } 538 | cout << endl; 539 | return 0; 540 | }#include 541 | using namespace std; 542 | 543 | template 544 | class vector { 545 | public: 546 | vector(int size = 10) { 547 | first_ = new T[size]; 548 | last_ = first_; 549 | end_ = first_ + size; 550 | } 551 | ~vector() { 552 | delete[] first_; 553 | first_ = last_ = end_ = nullptr; 554 | } 555 | vector(const vector& rhs) { 556 | int size = rhs.end_ - rhs.first_; 557 | first_ = new T[size]; 558 | int len = rhs.last_ - rhs.first_; 559 | for (int i = 0; i < len; i++) { 560 | first_[i] = rhs.first_[i]; 561 | } 562 | last_ = first_ + len; 563 | end_ = first_ + size; 564 | } 565 | vector& operator=(const vector& rhs) { 566 | if (this == &rhs) { 567 | return *this; 568 | } 569 | delete[] first_; 570 | int size = rhs.end_ - rhs.first_; 571 | first_ = new T[size]; 572 | int len = rhs.last_ - rhs.first_; 573 | for (int i = 0; i < len; i++) { 574 | first_[i] = rhs.first_[i]; 575 | } 576 | last_ = first_ + len; 577 | end_ = first_ + size; 578 | return *this; 579 | } 580 | void push_back(const T& val) { 581 | if (full()) { 582 | expand(); 583 | } 584 | *last_++ = val; 585 | } 586 | void pop_back() { 587 | if (empty()) { 588 | return; 589 | } 590 | --last_; 591 | } 592 | T back() const { 593 | return *(last_ - 1); 594 | } 595 | bool full() const { 596 | return last_ == end_; 597 | } 598 | bool empty() const { 599 | return first_ == last_; 600 | } 601 | int size() const { 602 | return last_ - first_; 603 | } 604 | private: 605 | T* first_; //指向数组起始的位置 606 | T* last_; //指向数组中有效元素的后继位置 607 | T* end_; //指向数组空间的后继位置 608 | 609 | void expand() { 610 | int size = end_ - first_; 611 | T* ptr = new T[2 * size]; 612 | for (int i = 0; i < size; i++) { 613 | ptr[i] = first_[i]; 614 | } 615 | delete[] first_; 616 | first_ = ptr; 617 | last_ = first_ + size; 618 | end_ = first_ + 2 * size; 619 | } 620 | }; 621 | 622 | int main() { 623 | vector vec; 624 | for (int i = 0; i < 10; i++) { 625 | vec.push_back(i); 626 | cout << vec.back() << " " ; 627 | } 628 | cout << endl; 629 | return 0; 630 | } 631 | ``` 632 | 633 | 634 | 635 | 10、shared_ptr 636 | 637 | ```cpp 638 | #include 639 | template 640 | class smart { 641 | private: 642 | T* ptr_; 643 | int* count_; 644 | 645 | public: 646 | //构造函数 647 | smart(T* ptr = nullptr):ptr_(ptr) { 648 | if (ptr_) { 649 | count_ = new int(1); 650 | } 651 | else { 652 | count_ = new int(0); 653 | } 654 | } 655 | //拷贝构造 656 | smart(const smart& ptr) { 657 | if (this != &ptr) { 658 | ptr_ = ptr.ptr_; 659 | count_ = ptr.count_; 660 | (*this->count_)++; 661 | } 662 | } 663 | //重载operator= 664 | smart& operator=(const smart& ptr) { 665 | if (this == &ptr) { 666 | return *this; 667 | } 668 | if (this->ptr_) { 669 | (*this->count_)--; 670 | if (*this->count_ == 0) { 671 | delete this->ptr_; 672 | delete this->count_; 673 | } 674 | } 675 | this->ptr_ = ptr.ptr_; 676 | this->count_ = ptr.count_; 677 | (*this->count_)++; 678 | return *this; 679 | } 680 | //operator*重载 681 | T& operator*() { 682 | if (this->ptr_) { 683 | return *(this->ptr_); 684 | } 685 | } 686 | //operator->重载 687 | T* operator->() { 688 | if (this->ptr_) { 689 | return this->ptr_; 690 | } 691 | } 692 | //析构函数 693 | ~smart() { 694 | (*this->count_)--; 695 | if (this->count_ == 0) { 696 | delete this->ptr_; 697 | delete this->count_; 698 | } 699 | } 700 | 701 | }; 702 | 703 | int main() { 704 | smart sp1(new int(42)); 705 | smart sp2 = sp1; 706 | std::cout << *sp1 << std::endl; // 42 707 | std::cout << *sp2 << std::endl; // 42 708 | 709 | return 0; 710 | } 711 | ``` 712 | 713 | 714 | 715 | 11、单例模式 716 | 717 | ```cpp 718 | #include 719 | #include 720 | 721 | class Singleton { 722 | private: 723 | static Singleton* instance; 724 | static std::mutex mtx; // 互斥量用于保护实例的访问 725 | Singleton() {} // 将构造函数设为私有,防止外部直接实例化对象 726 | public: 727 | // 获取实例的静态方法 728 | static Singleton* getInstance() { 729 | // 双重检查锁定确保在多线程环境下仅有一个实例被创建 730 | if (instance == nullptr) { 731 | std::lock_guard lock(mtx); // 自动加锁互斥量 732 | if (instance == nullptr) { 733 | instance = new Singleton(); 734 | } 735 | } 736 | return instance; 737 | } 738 | 739 | // 示例方法 740 | void doSomething() { 741 | std::cout << "Singleton is doing something." << std::endl; 742 | } 743 | }; 744 | 745 | // 静态成员变量初始化 746 | Singleton* Singleton::instance = nullptr; 747 | std::mutex Singleton::mtx; 748 | 749 | int main() { 750 | // 获取单例对象的实例 751 | Singleton* s1 = Singleton::getInstance(); 752 | // 调用示例方法 753 | s1->doSomething(); 754 | 755 | // 获取单例对象的另一个实例 756 | Singleton* s2 = Singleton::getInstance(); 757 | // 检查是否为同一个实例 758 | if (s1 == s2) { 759 | std::cout << "s1 and s2 are the same instance." << std::endl; 760 | } else { 761 | std::cout << "s1 and s2 are different instances." << std::endl; 762 | } 763 | 764 | return 0; 765 | } 766 | ``` 767 | 768 | 769 | 770 | 12、 771 | 772 | -------------------------------------------------------------------------------- /操作系统.md: -------------------------------------------------------------------------------- 1 | 1. 进程通信有哪些方式,什么原理 2 | 3 | 匿名管道:只能用于有亲缘关系的进程间,通过文件描述符访问,半双工 4 | 5 | 有名管道:无亲缘关系的进程间通信,创建一个特殊的文件 6 | 7 | 消息队列:存放在内核空间,链表形式保存,如果不读取或者删除会一直保存 8 | 9 | 共享内存:两个进程不同的虚拟内存映射到同一块物理内存 10 | 11 | 信号 12 | 13 | 信号量:一种计数器,用于控制对共享内存的访问,P操作申请资源,V操作释放资源 14 | 15 | 套接字:不同主机间进行网络通信 16 | 17 | 2. 进程、线程、协程的区别 18 | 19 | 进程: 20 | 21 | 进程是操作系统分配资源的最小单位,每个进程都有自己独立的地址空间和资源进程之间相互独立,不能直接访问对方的资源 22 | 23 | 进程间切换开销较大,包括保存和恢复进程状态、切换地址空间等,通常由操作系统负责管理线程: 24 | 25 | 线程是进程内的一个执行单元,是CPU调度的最小单位,多个线程共享同一进程的地址空间和资源,包括全局变量,静态变量等 26 | 27 | 线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈) 28 | 29 | 线程之间的切换开销较小,通常由操作系统的线程调度器负责管理多线程程序只要有一个线程崩溃,整个程序就崩溃了,但多进程程序中一个进程崩溃对其他进程没影响 30 | 31 | 协程: 32 | 33 | 协程是一种轻量级的线程,也称为用户态线程或者轻量级线程,它由用户程序控制而不是操作系统控制 34 | 35 | 协程可以在同一个线程内实现并发执行,通过协作式调度来切换执行权,可以实现非抢占式多任务处理协程之间的切换开销非常小,因为切换是由用户程序自己控制的,不需要操作系统介入协程常用于处理大量的、IO 密集型的并发任务,例如网络编程、异步IO 等场景 36 | 37 | ,协程是异步 38 | 39 | 3. 虚拟内存中COW(写时复制)算法的理解 40 | 41 | 通过写时复制,读时共享,实现在共享资源上进行修改而不影响其他进程(在需要修改共享资源时才复制到私有内存空间中进行修改,以减少不必要的内存拷贝,节省资源) 42 | 43 | 4. 物理内存和虚拟内存的区别 44 | 45 | 虚拟内存允许应用程序访问比实际物理内存更大的内存空间。将物理内存与磁盘空间结合,从而使应用程序能够在需要时使用更多的内存。扩展可用内存,共享内存,内存保护(使得不同的进程相互隔离避免冲突) 46 | 47 | 5. 缺页怎么处理 48 | 6. 用户态和内核态的区别 49 | 50 | 用户态只能受限地访问内存,且不允许访问系统硬件资源,没有占用CPU的能力,CPU资源可以被其它程序获取;内核态可以访问内存所有数据以及系统硬件资源,也可以进行程序的切换 51 | 52 | 当用户程序需要执行特权操作或访问受限资源时,需要通过系统调用向操作系统发起请求。系统调用会触发由用户态切换到内核态,并将控制权转移到操作系统内核,由内核执行相应的操作。 53 | 54 | 为什么要分? 55 | 56 | 安全性,封装性,利于调度 57 | 58 | 7. 一个进程open磁盘文件read文件writeclose过程去判断内核态和用户的执行 59 | 60 | Open:用户态调用open函数,通过系统调用进入内核态,内核进行检查,找到文件描述符返回给用户态;Read:用户态调用read函数,通过系统调用进入内核态,内核态检验文件描述符,找到文件对象,读取相应字节数到缓冲区,内核将数据从内核缓冲区拷贝到用户进程缓冲区,返回读取字节数给用户进程;Write: 用户态调用write函数,通过系统调用进入内核态,内核态检验文件描述符,将数据从用户态进程的缓冲区拷贝到内核缓冲区,再将内核缓冲区的数据写入文件,返回写入字节数给用户态进程;Close: 用户态调用close函数,通过系统调用进入内核态,检查文件描述符合法性,更新文件描述符,并返回。 61 | 62 | 8. 线程相对进程的优点 63 | 9. 用户线程和内核线程之间的映射关系是什么样的 64 | 65 | 一对一(并发性强,开销大),多对一(并发性差,开销小),一对多 66 | 67 | 10. 用户线程切换会陷入到内核态嘛 68 | 69 | 不会,用户线程是在用户空间实现的线程,不是由内核管理,是由用户态的线程库来完成线程的管理,切换也是; 70 | 71 | 11. 用户进程为什么能比线程做到更轻量 72 | 12. 多路复用/epoll、poll、select区别,epoll为什么高效 73 | 74 | 单个进程/线程可以同时处理多个IO请求。 75 | 76 | Select:文件描述符放入一个集合中,调用select时,将这个集合从用户空间拷贝到内核空间(缺点1:每次都要复制,开销大),由内核根据就绪状态修改该集合的内容。(缺点2:集合大小有限制);采用水平触发机制。select函数返回后,需要通过遍历这个集合,找到就绪的文件描述符(缺点3:轮询的方式效率较低),当文件描述符的数量增加时,效率会线性下降;poll:和select几乎没有区别,区别在于文件描述符的存储方式不同,poll采用链表的方式存储,没有最大存储数量的限制;相比select和poll每次查询都将文件描述符集合从用户空间拷贝到内核空间,epoll在内核空间利用红黑树对文件描述符集合进行了保存,避免了拷贝。此外,epoll在内核空间维护了一个双向链表用于保存就绪的文件描述符,当文件描述符就绪时采用回调机制将该文件描述符加入链表中;支持水平触发和边缘触发,采用边缘触发机制时,只有活跃的描述符才会触发回调函数。 77 | 78 | 13. 线程同步有哪些方式 79 | 80 | 互斥锁:互斥锁是内核对象,只有拥有互斥锁的线程才有访问共享资源的权限,保证同一时刻只有一个线程可以访问。信号量:信号量是内核对象,它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。如果为0,则将线程放入一个队列中等待 81 | 82 | 条件变量:条件变量用于线程间的同步,通常与互斥锁一起使用,用于等待某个条件成立后再继续执行。事件:允许一个线程在处理完一个任务后,主动唤醒另外一个线程执行任务。事件分为手动重置事件和自动重置事件。手动重置事件被设置为激发状态后,会唤醒所有等待的线程,而且一直保持为激发状态,直到程序重新把它设置为未激发状态。自动重置事件被设置为激发状态后,会唤醒一个等待中的线程,然后自动恢复为未激发状态。临界区:任意时刻只允许一个线程对临界资源进行访问。拥有临界区对象的线程可以访问该临界资源,其它试图访问该资源的线程将被挂起,直到临界区对象被释放 83 | 84 | 14. 同步与互斥 85 | 86 | 同步:多个进程因为合作而使得进程的执行有一定的先后顺序。比如某个进程需要另一个进程提供的消息,获得消息之前进入阻塞态;互斥:多个进程在同一时刻只有一个进程能运行到临界区 87 | 88 | 15. 并发、并行和异步 89 | 90 | 并发:在一个时间段中同时有多个程序在运行,但其实任一时刻,只有一个程序在CPU上运行,宏观上的并发是通过不断的切换实现的;并行:在多CPU系统中,多个程序无论宏观还是微观上都是同时执行的异步(和同步相比):同步是顺序执行,异步是在等待某个资源的时候继续做自己的事 91 | 92 | 16. 锁有哪些 93 | 94 | 互斥锁,读写锁,自旋锁(自旋锁是一种忙等待的锁,当线程尝试获取锁时,如果锁已被其他线程占用,该线程将会一直在一个循环中不断地检查锁是否可用),条件变量 95 | 96 | 17. 进程调度策略有哪些 97 | 98 | 先来先服务、最短作业优先、最短剩余时间优先、时间片轮转、优先级调度、多级反馈队列调度(多个队列,优先级递减,时间片递增) 99 | 100 | 18. 阻塞IO和非阻塞IO 101 | 102 | 阻塞IO:用户执行read,线程被阻塞,直到内核数据准备好通知线程; 103 | 104 | 非阻塞IO:read后立即返回,此时不断轮询内核(需要主动去检查),询问数据是否准备好;(均为同步) 105 | 106 | 19. 同步IO和异步IO 107 | 108 | 异步IO:read后直接返回,内核自动将数据拷贝到用户空间,不用进程自己完成(不需要主动检查),完成后通知进程。 109 | 110 | 20. 零拷贝 111 | 112 | 数据不需要从一个内存区域复制到另一个内存区域,从而减少上下文切换和cpu拷贝时间。 113 | 114 | 普通的文件传输:dma拷贝进内核,read,发生1次cpu拷贝进用户空间和2次上下文切换,write发生1次cpu拷贝进内核空间和2次上下文切换,最后由dma拷贝进硬件。一共4次上下文切换和4次拷贝。mmap+write:内核缓冲区地址与用户缓冲区进行映射,从而实现内存共享,减少一次拷贝,一共4次上下文切换和3次拷贝(1次cpu拷贝从输入缓冲区到输出缓冲区)。sendfile(kafka):直接由dma完成数据的拷贝,不需要cpu参与,一共2次上下文切换和2次拷贝(没有cpu拷贝)。 115 | 116 | 21. 死锁产生条件 117 | 118 | 互斥、占有并等待、非抢占、循环等待 119 | 120 | 22. 页式存储和段式存储 121 | 122 | 把虚拟内存和物理内存划分为大小相等的部分称为页,分配时以页为单位。虚拟内存地址被切分为页号和偏移量,根据页号从页表中查询对应的物理页号,加上偏移量后得到物理内存地址。 123 | 124 | 125 | 126 | 虚拟内存按照自身逻辑关系划分为若干个段(segment)(如代码段,数据段,堆栈段),内存空间被动态划分为长度不同的区域,分配时以段为单位,每段在内存中占据连续空间,各段可以不相邻; 127 | 128 | 23. 有哪些页面置换算法 129 | 130 | 在程序运行过程中,如果要访问的页面不在内存中,就发生缺页中断从而将该页调入内存中。此时如果内存已无空闲空间,系统必须从内存中调出一个页面到磁盘中来腾出空间。页面置换算法的主要目标是使页面置换频率最低(也可以说缺页率最低)。 131 | 132 | 1. 最佳页面置换算法:置换以后不需要或者最远的将来才需要的页面,是一种理论上的算法; 133 | 2. 先进先出FIFO; 134 | 3. 时钟算法:使用环形链表,再使用一个指针指向最老的页面,若其访问位为1,给第二次机会,并将访问位置0; 135 | 4. 最近最少使用算法LRU: 置换出未使用时间最长的一页;实现方式:维护时间戳,或者维护一个所有页面的链表。当一个页面被访问时,将这个页面移到链表表头。这样就能保证链表表尾的页面是最近最久未访问的。 136 | 137 | 触发页面中断的原因:访问未映射的页面;访问已被置换的页面; 138 | 139 | 1. 异常控制流:陷阱、中断、异常和信号 140 | 141 | 陷阱是有意造成的“异常”,是执行一条指令的结果,主要作用是实现系统调用,当进程执行这条指令后,会中断当前的控制流,陷入到内核态; 142 | 143 | 中断由处理器外部硬件产生,包括I/O中断,定时器引起的时钟中断,断点中断等。分为上半部和下半部分:上半部直接处理硬件请求,也就是硬中断,主要是负责耗时短的工作,特点是快速执行;下半部是由内核触发,也就说软中断,主要是负责上半部未完成的工作,通常都是耗时比较长的事情,特点是延迟执行;异常是一种错误情况,是执行当前指令的结果; 144 | 145 | 信号的作用是用来通知进程发生了某种系统事件 146 | 147 | 2. CPU缓存一致性 148 | 149 | CPU多个核心的缓存在操作共享数据时,可能造成各核心自己的缓存内数据不一致,要将修改后的数据广播出去,以及多个事务时保证串行化,可以通过MESI协议,将缓存状态定义为修改、独占、共享、失效,来减少总线压力。 150 | 151 | -------------------------------------------------------------------------------- /数据库.md: -------------------------------------------------------------------------------- 1 | 1. 为什么用B+树,不用B树或者AVL(优点)/哈希表 2 | 3 | IO次数少,B+树中间节点只存放索引,数据都在叶子节点中,中间节点可以存放更多索引项,索引树更加矮胖 4 | 5 | 范围查询效率高,B树需要遍历整个树,B+树只需要遍历叶子节点中的链表 6 | 7 | 查询效率更稳定,每次查询从根节点到叶节点路径长度相同 8 | 9 | 有大量的冗余节点,插入删除效率更高 10 | 11 | 哈希索引只支持精确查找,不支持部分和范围查找,无法用于排序和分组,并且遇到大量哈希值相等的情况后查找效率会降低 12 | 13 | 2. 聚簇非聚簇索引的区别 14 | 15 | 数据的物理排列顺序和索引排列顺序一致,优点是查询速度快。非聚簇索引只记录逻辑顺序,并不改变物理顺序 16 | 17 | 3. 什么情况下索引会失效 18 | 19 | 以“%”开头的like语句 20 | 21 | Or前后没有同时使用索引 22 | 23 | 数据类型出现隐式转化 24 | 25 | 对于多列索引必须满足最左匹配原则 26 | 27 | 如果mysql估计全表扫描比索引快,则不使用索引 28 | 29 | 4. 哪些情况下应该or不应该建立索引 30 | 31 | 某列经常作为最大最小值,经常被查询的字段,经常用作表连接的字段,经常出现在Order by/group by/distinct后面的字段 32 | 33 | 很少进行查找的字段,有大量重复数据的字段(性别),经常更新的字段,含有空值的字段,大文本、图片字段(开销大,效率低)。 34 | 35 | 5. 有哪些索引 36 | 37 | 普通索引,唯一索引,主键索引(不允许有空值),单列索引和多列索引,覆盖索引(索引包含了所有满足查询所需要的数据,查询的时候只需要读取索引不需要回表读取数据)、聚簇索引和非聚簇索引 38 | 39 | 6. 什么是事务 40 | 41 | 是一个操作序列,不可分割的工作单位,以begin transaction开始,以rollback/commit结束 42 | 43 | 7. Mysql的隔离级别,如何实现四个隔离级别的 44 | 45 | 未提交读:导致脏读,不可重复读,幻读 46 | 47 | 提交读:只能看见已经提交事务所做的改变,避免脏读 48 | 49 | 可重复读,避免不可重复读 50 | 51 | 可串行化:强制事务串行执行,使之不可能相互冲突,解决幻读问题。 52 | 53 | 8. 并发事务下会发生什么问题 54 | 55 | 丢失修改,脏读,不可重复读(一个事务内多次读同一个数据前后不一样),幻读(当同一查询多次执行时,由于其它事务在这个数据范围内执行了插入操作,会导致每次返回不同的结果集) 56 | 57 | 9. 数据库事务的原子性如何保证,持久性隔离性呢(ACID还有个一致性) 58 | 59 | 用回滚日志实现,反向执行日志中的操作 60 | 61 | 重做日志 62 | 63 | MVCC(多版本并发控制)或锁机制 64 | 65 | 10. 数据库的索引是如何实现的 66 | 11. 快照读当前读 67 | 68 | 快照读:在事务开始时确定一个一致的时间点(即快照),然后从该时间点读取数据。是事务开始时的一个静态快照,事务过程中其他事务的修改不会对其产生影响 69 | 70 | 当前读:在读取数据时,直接读取当前数据的最新版本 71 | 72 | 12. 关系型数据库非关系型数据库的区别 73 | 74 | 前者高度组织化结构化数据;后者存储的数据结构不固定更加灵活,可以减少一些空间和时间的开销后者更加容易水平扩展前者支持结构化查询语言,支持复杂的查询功能和表关联。后者只能进行简单的查询前者支持事务,具有ACID特性。后者则是BASE,最终一致性 75 | 76 | 13. MySQL解析过程,执行过程 77 | 78 | 1、连接到 MySQL 服务器:客户端与 MySQL 服务器建立TCP连接。2、解析查询语句:MySQL 服务器接收到客户端发送的查询请求后,首先进行语法解析和语义解析,确保查询语句的正确性。3、查询缓存:MySQL 服务器检查查询缓存,如果之前执行过相同的查询,并且查询结果在缓存中,则直接返回缓存结果,省去了后续步骤的执行。4、优化器生成执行计划:对于未命中查询缓存的查询,MySQL 服务器使用查询优化器根据查询条件、表结构、索引等信息生成查询的执行计划。优化器会尝试选择最优的执行计划,以提高查询性能。5、执行查询:MySQL 服务器根据优化器生成的执行计划执行查询操作。这包括从磁盘加载数据、执行排序和过滤、计算聚合等操作。6、返回结果:执行完成后,MySQL 服务器将查询结果返回给客户端。7、断开连接:客户端与 MySQL 服务器断开连接,释放资源。 79 | 80 | 14. 三种日志 81 | 82 | redo log(重做日志):记录事务提交后的数据修改信息,以便在数据库重启后重做这些操作,保证事务的持久性。 83 | 84 | undo log(回滚日志):记录数据修改前的状态,当事务回滚时,通过读取 Undo Log 中的记录,撤销事务对数据的修改,保证事务原子性。 85 | 86 | binlog(归档日志):主要记录用户对数据库操作的sql语句(除了查询语句),用于主从同步和基于时间点的还原,与 Redo log 不同,Binlog 是按顺序记录的,并且在服务器重启后不会自动清除。 87 | 88 | 15. 如何优化数据库 89 | 90 | Sql语句优化:分析慢查询日志,通过日志去找出IO大的SQL以及未命中索引的SQL;避免使用SELECT*,而是指定需要的列,尽量减少在WHERE子句中使用函数,这样可以利用索引;使用EXPLAIN分析查询的性能,是否使用了索引以及如何优化查询路径。 91 | 92 | 索引优化:注意会引起索引失效的情况,以及在适合的地方建立索引。 93 | 94 | 数据库表结构优化:设计表时遵循三大范式 95 | 96 | 系统配置优化:操作系统增加TCP支持的队列数 97 | 98 | 硬件优化:cpu多核且高频,增大内存,使用固态硬盘 99 | 100 | 16. 分表: 101 | 102 | 水平切分:将同一个表中的记录拆分到多个结构相同的表中(策略:哈希取模;根据ID范围来分)。当一个表的数据不断增多时,Sharding 是必然的选择,它可以将数据分布到集群的不同节点上,从而缓解单个数据库的压力;垂直切分:将一张表按列切分成多个表。可以将不常用的字段单独放在同一个表中;把大字段独立放入一个表中;或者把经常使用的字段(关系密切的)放在一张表中。垂直切分之后业务更加清晰,系统之间整合或扩展容易,数据维护简单 103 | 104 | 17. 什么是MVCC: 105 | 106 | 多版本并发控制,MVCC在每行记录后面都保存有两个隐藏的列,用来存储创建版本号和删除版本号。当事务要访问数据时,它会看到该数据的一个一致的快照,这个快照是在事务开始之前最新的版本。这样,读操作不会被写操作阻塞。 107 | 108 | 18. mysqlserver层 109 | 110 | server层负责建立连接、分析和执行sql。主要包括连接器,查询缓存,解析器,预处理器,优化器,执行器等。 111 | 112 | 19. SQL注入攻击 113 | 114 | 攻击者通过在输入字段中插入恶意SQL代码,利用应用程序未充分验证用户输入的漏洞,达到篡改、读取或破坏数据库的目的。 115 | 116 | 解决方法:输入验证与过滤,确保只接受预期的字符和格式;使用预编译语句,将sql语句与参数分开,确保参数不会被解析为sql的一部分;确保数据库账户只具有完成其工作所需的最小权限 117 | 118 | -------------------------------------------------------------------------------- /计算机网络.md: -------------------------------------------------------------------------------- 1 | 1. HTTP报文结构 2 | 3 | 请求行:请求方法get/post,url,http版本 4 | 5 | 请求头:用户标识,请求体长度,类型,cookie 6 | 7 | 请求体:内容 8 | 9 | 10 | 11 | 状态行:http版本,状态码,状态消息 12 | 13 | 响应头:内容长度,内容类型 14 | 15 | 响应体:内容 16 | 17 | 2. TCP报文结构 18 | 19 | 源端口和目的端口,序号(乱序重排),确认号(应答机制),数据偏移(头部长度),标志位(ACK,FIN,SYN),接收窗口(流量控制),校验和(数据校验) 20 | 21 | 3. Socket通信的基本流程,使用的函数,TCP三次握手对应了socket里面的哪些流程 22 | 23 | 在客户端和服务器端分别创建一个Socket对象 24 | 25 | 在服务器端,调用bind()函数绑定服务器的IP地址和端口号 26 | 27 | 在客户端,调用connect()函数连接对应ip和端口号的服务器。在服务器端,调用listen()函数监听连接请求,最后调用accept()函数接受客户端的连接。 28 | 29 | 建立连接后,客户端和服务端都可以通过socket对象发送和接受数据,用send()发送数据,recv()接收数据 30 | 31 | 通信结束后分别调用close()关闭连接 32 | 33 | 4. TCP中的端口有什么作用 34 | 35 | 唯一标识应用程序,连接和数据流,实现了数据的准确传输和多应用程序并发运行的需求 36 | 37 | 5. TCP三次握手过程 38 | 39 | 第一次握手:**SYN_SENT** 40 | 41 | 1.客户端将SYN标志位置为1 42 | 43 | 2.生成一个序列号seq=J,进入SYN_SENT状态 44 | 45 | 第二次握手: **SYN_RCVD** 46 | 47 | 1.服务器端接收客户端的连接: ACK=1 48 | 49 | 2.服务器会回发一个确认序号: ack=客户端的序号 + 1 50 | 51 | 3.服务器端会向客户端发起连接请求: SYN=1 52 | 53 | 4.服务器会生成一个随机序号:seq = K 54 | 55 | 第三次握手: **ESTABLISHED** 56 | 57 | 1.客户单应答服务器的连接请求:ACK=1 58 | 59 | 2.客户端回复收到了服务器端的数据:ack=服务端的序号 + 1 60 | 61 | 6. TCP慢启动算法讲一下(拥塞控制算法流程) 62 | 63 | 拥塞控制主要由:慢启动,拥塞避免,快重传,快恢复组成 64 | 65 | 慢启动:刚开始发送数据时,先把拥塞窗口设置成一个最大报文段MSS,每经过一个传输轮次RTT后拥塞窗口翻倍 66 | 67 | 拥塞避免:当拥塞窗口达到慢开始门限,开始拥塞避免算法,拥塞窗口大小不再指数增加,而是线性增加,即每经过一个RTT只增加1MSS 68 | 69 | 如果这期间网络出现拥塞(没有收到确认),就把慢开始门限ssthresh设置为出现拥塞时发送方窗口值的一半,然后把拥塞窗口cwnd重新设置为1,执行慢开始(不使用快重传的情况) 70 | 71 | 快重传:当 TCP 接收方收到一个“乱序”的数据包时,它会立即发送一个重复确认给发送方。如果发送方收到连续的三个重复确认,则认为这个数据包丢失,并立即重传该数据包,而不必等待超时。 72 | 73 | 快恢复:当发送方连续收到三个重复确认时,就把慢开始门限减半,然后执行拥塞避免算法。 74 | 75 | 7. TCPUDP区别 76 | 77 | TCP是面向连接的,UDP无连接 78 | 79 | TCP可靠,UDP不可靠 80 | 81 | TCP只支持点到点通信,UDP支持一对一,一对多,多对一,多对多 82 | 83 | TCP面向字节流,UDP面向报文 84 | 85 | TCP有拥塞控制机制,UDP没有,网络出现拥塞不会使主机发送速率降低 86 | 87 | TCP首部开销(20字节)比UDP首部开销(8字节)要大 88 | 89 | 8. 如何去优化TCP传输协议,若在跨服务,较长链路情况下 90 | 91 | 开启tcpFastOpen(基于cookie),可以将客户端再次发起请求的握手减少至1RTT。长链路情况(高时延带宽积)应增大发送窗口。增大发送、接受缓冲区。启用SACK,避免重复传输。优化timewait状态,如减少时间,复用timewait等。 92 | 93 | 9. TCP/IP四层主要是哪四层,都有哪些协议,OSI七层哪七层 94 | 95 | 应用层:http,dns,ftp,ssh 96 | 97 | 传输层:tcp,udp 98 | 99 | 网络层:ip,arp 100 | 101 | 网络接口层 102 | 103 | 104 | 105 | 应用层、表示层、会话层/传输层/网络层/数据链路层、物理层 106 | 107 | 10. 为什么TCP要设置MSS(最大报文长度) 108 | 109 | 防止报文过长,在IP层进行分片,而网络层没有重传机制,若发生丢失,则整个的TCP报文都要重传。 110 | 111 | 11. 如何通过UDP实现可靠传输 112 | 113 | QUIC协议(多路复用),当某个流发⽣丢包时,只会阻塞这个流,其他流不会受到影响;连接时减少了握手交互次数 114 | 115 | 12. TCP怎么实现可靠传输 116 | 117 | 数据包校验 118 | 119 | 对失序数据包重新排序(tcp报文有序列号) 120 | 121 | 丢弃重复数据 122 | 123 | 应答机制:接收方收到数据后会发送一个确认 124 | 125 | 超时重发:发送方发出数据后,启动一个定时器,超时未收到接收方的确认,则重新发送这个数据 126 | 127 | 流量控制:确保接收端能接受发送方的数据而不会缓冲区溢出 128 | 129 | 13. 流量控制拥塞控制区别 130 | 131 | 流量控制属于通信双方协商,拥塞控制属于通信链路全局 132 | 133 | 流量控制需要双方各维护一个发送窗、一个接受窗,对任意⼀⽅,接收窗⼤⼩由⾃身决定,发送窗⼤⼩由接收⽅响应的TCP报⽂段中窗⼝值确定;拥塞控制的拥塞窗⼝⼤⼩变化由试探性发送⼀定数据量数据探查⽹络状况后⽽⾃适应调整。 134 | 135 | 14. 详细的流量控制说一下/滑动窗口的原理是什么,如何保证消息的顺序 136 | 137 | 使用滑动窗口实现流量控制。防止发送方发送速率太快,接收方缓存区不够导致溢出。接收方会维护一个接收窗口 receiver window(rwnd,窗口大小单位是字节),接受窗口的大小是根据自己的资源情况动态调整的,在返回ACK时将接受窗口大小放在TCP报文中的窗口字段告知发送方。发送窗口的大小不能超过接受窗口的大小,只有当发送方发送并收到确认之后,才能将发送窗口右移。发送窗口的上限为接受窗口和拥塞窗口中的较小值。接受窗口表明了接收方的接收能力,拥塞窗口表明了网络的传送能力。 138 | 139 | 140 | 141 | 通过在发送方和接收方之间维护一个有序序列号,发送方发送数据时,附带序列号,接收方按照序列号顺序接收数据并进行重组 142 | 143 | 15. 怎么知道是不是达到发送窗口的阈值 144 | 145 | 可以通过监控拥塞窗口大小、接收方的接收窗口大小以及网络延迟和丢包情况等因素来判断是否达到发送窗口的阈值 146 | 147 | 16. 什么是零窗口 148 | 149 | 如果接收方没有能力接收数据,就会将接收窗口设置为0,这时发送方必须暂停发送数据,但是会启动一个持续计时器(persistence timer),到期后发送一个大小为1字节的探测数据包,以查看接收窗口状态。如果接收方能够接收数据,就会在返回的报文中更新接收窗口大小,恢复数据传送 150 | 151 | 17. websocket协议: 152 | 153 | 用于在客户端和服务器之间进行全双工通信的协议,允许双方在单个持久连接上实时地双向发送数据。 154 | 155 | 18. RPChttp区别 156 | 157 | RPC是远程过程调用,允许一个程序调用位于另一台计算机上的子程序或服务,就像调用本地函数一样,相较于http更专注于远程过程调用和高效的通信;而HTTP则更适用于传输超文本和其他资源。 158 | 159 | 19. tcp粘包怎么处理:约定bit数切割 160 | 161 | TCP粘包是指TCP的缓冲区内包含多个应用层报文 162 | 163 | 164 | 165 | 原因: 一个TCP报文包含多个应用层报文接收方未能及时从TCP缓冲区中读取数据 166 | 167 | 解决方法: 应用层报文添加包含数据长度的头部设置应用层报文的开始与结束标志应用层报文固定长度 168 | 169 | 20. httpshttp区别 170 | 171 | 端口不同:HTTP使用的是80端口,HTTPS使用443端口;HTTP(超文本传输协议)信息是明文传输,HTTPS运行在SSL(Secure Socket Layer)之上,添加了加密和认证机制,更加安全;HTTPS由于加密解密会带来更大的CPU和内存开销;HTTPS通信需要证书,一般需要向证书颁发机构(CA)购买 172 | 173 | 21. TLS4次握手: 174 | 175 | 客户端发起加密通信请求,协商加密和摘要算法->服务端确定加密和摘要算法和数字证书(CA)->客户端校验证书,取出公钥,对前面协商的2个随机数+再生成一个随机数加密成【会话密钥】,后续使用这个密钥加密传输;发送随机数+加密方式改变+握手结束通知->服务器使用私钥生成【会话密钥】,握手结束。 176 | 177 | 22. ssl连接过程 178 | 179 | 协商加密和摘要算法->返回证书-> 验证证书合法性,利用公钥获得数字签名->公钥加密对称秘钥和对称秘钥加密的报文摘要->服务器根据私钥获得对称秘钥,解密报文摘要,和计算的报文摘要对比->ssl建立完成 180 | 181 | 23. 非对称加密对称加密 182 | 183 | 对称加密:加密和解密采用相同的密钥。如:DES、RC2、RC4     非对称加密:需要两个密钥:公钥和私钥。如果用公钥加密,需要用私钥才能解密。如:RSA   * 区别:对称加密速度更快,通常用于大量数据的加密;非对称加密安全性更高(不需要传送私钥) 184 | 185 | 对称密钥加密来保证通信内容的保密性和传输效率,同时使用非对称密钥加密来保证通信双方的身份验证和会话密钥的安全传输。 186 | 187 | 24. CA具体如何实现数字签名认证的 188 | 189 | 服务器把自己的公钥注册到CA 190 | 191 | CA用自己的私钥将服务器的公钥数字签名并颁发数字证书 192 | 193 | 客户端拿到服务器的数字证书后,使用CA的公钥确认服务器的数字证书的真实性 194 | 195 | 从数字证书获取服务器公钥后使用它对报文加密后发送,服务器用私钥对报文解密 196 | 197 | 25. 摘要算法 198 | 199 | 客户端在发送明⽂之前会通过摘要算法算出明⽂的摘要值,发送的时候把「摘要值 + 明⽂」⼀同加密成密⽂后,发送给服务器,服务器解密后,⽤相同的摘要算法算出发送过来的明⽂,通过⽐较客户端携带的「摘要值」和当前算出的「摘要值」,若「摘要值」相同,说明数据是完整的。 200 | 201 | 26. https用的对称密钥还是非对称密钥,原理,建立连接的过程 202 | 203 | 都有 204 | 205 | 27. 你了解的网络攻击方式有哪些,SYN攻击的防范方法 206 | 207 | SYN攻击:是一种利用TCP协议中的三次握手过程来对目标服务器进行拒绝服务(DDoS)的网络攻击。攻击者发送大量伪造的TCP连接请求(SYN包),但是不完成后续的握手过程,导致目标服务器耗尽资源,无法处理正常的连接请求,从而造成拒绝服务。 208 | 209 | 210 | 211 | 防范方式: 212 | 213 | 限制同时打开SYN半连接的数目缩短SYN半连接的Time out 时间关闭不必要的服务 214 | 215 | 28. 浏览器中输入URL后发生了什么 216 | 217 | 1、当用户在浏览器中输入网址时,首先会进行DNS解析,将域名转换为对应的IP地址。浏览器会向本地DNS服务器发出DNS查询请求,如果本地DNS服务器缓存了该域名的IP地址,则直接返回;否则,本地DNS服务器会向根域名服务器、顶级域名服务器和权威域名服务器依次发出查询请求,直到获取到域名的IP地址; 218 | 219 | 2、浏览器获得域名对应的IP地址以后,浏览器向服务器请求建立连接,发起三次握手;3、TCP/IP连接建立起来后,浏览器向服务器发送HTTP请求;4、服务器收到请求根据路径找到对应控制器处理请求,并将结果返回给浏览器;5、浏览器解析渲染,遇到js,css等静态资源引用时,再次重复上述步骤;6、最终呈现完整页面。 220 | 221 | 29. mac地址的作用 222 | 223 | 唯一标识网络设备的硬件地址 224 | 225 | 30. tcp如何判断网络是否拥塞 226 | 227 | 通过丢包情况,延迟(往返时间RTT),重传超时情况 228 | 229 | 31. 四次挥手 230 | 231 | 第一次挥手:Client将FIN置为1,发送一个序列号seq给Server;进入FIN_WAIT_1状态;第二次挥手:Server收到FIN之后,发送一个ACK=1,acknowledge number=收到的序列号+1;进入CLOSE_WAIT状态。此时客户端已经没有要发送的数据了,但仍可以接受服务器发来的数据。第三次挥手:Server将FIN置1,发送一个序列号给Client;进入LAST_ACK状态;第四次挥手:Client收到服务器的FIN后,进入TIME_WAIT状态;接着将ACK置1,发送一个acknowledge number=序列号+1给服务器;服务器收到后,确认acknowledge number后,变为CLOSED状态,不再向客户端发送数据。客户端等待2*MSL(报文段最长寿命)时间后,也进入CLOSED状态。完成四次挥手。 232 | 233 | (频繁的短连接会导致出现多个TIME_WAIT状态) 234 | 235 | 32. getpost区别 236 | 237 | GET是幂等的,即读取同一个资源,总是得到相同的数据,POST不是幂等的;GET一般用于从服务器获取资源,而POST有可能改变服务器上的资源;请求形式上:GET请求的数据附在URL之后,在HTTP请求头中;POST请求的数据在请求体中;安全性:GET请求可被缓存、收藏、保留到历史记录,且其请求数据明文出现在URL中。POST的参数不会被保存,安全性相对较高;GET只允许ASCII字符,POST对数据类型没有要求,也允许二进制数据;GET的长度有限制(操作系统或者浏览器),而POST数据大小无限制 238 | 239 | 33. http中常见的状态码 240 | 241 | 2xx状态码:操作成功。200 OK3xx状态码:重定向。301 永久重定向;302暂时重定向4xx状态码:客户端错误。400 Bad Request;401 Unauthorized;403 Forbidden;404 Not Found;5xx状态码:服务端错误。500服务器内部错误;501服务不可用 242 | 243 | 34. 强缓存协商缓存 244 | 245 | 强缓存:在缓存有效期内,浏览器不向服务器发送请求,直接从本地缓存中读取数据。 246 | 247 | 协商缓存:每次请求都会与服务器进行交互。浏览器会向服务器发送请求,询问资源是否有更新。服务器会根据请求中的某些标识来判断资源是否发生了变化,并返回相应的响应。 248 | 249 | 35. http1.0与http1.1的区别,2.0与1.1的区别,3.0了解过吗 250 | 251 | 1.0与1.1: 252 | 253 | 使用长连接的方式改善了 HTTP/1.0 短连接造成的性能开销。支持管道(pipeline)网络传输,只要第一个请求发出去了,不必等其回来,就可以发第二个请求出去,可以减少整体的响应时间。 254 | 255 | 2.0与1.1: 256 | 257 | 头部压缩,如果同时发出多个请求,头是一样或相似的,HPACK算法会消除重复的部分; 258 | 259 | 二进制格式,头部和数据体都是二进制,统称为帧,增加数据传输的效率; 260 | 261 | 并发传输,针对不同的HTTP请求用不同的Stream ID来区分,接收端根据Stream ID有序组装HTTP消息,因此可以并发不同的Stream。 262 | 263 | 服务器推送,允许服务器主动向客户端推送资源,而不需要客户端显式请求。 264 | 265 | 266 | 267 | 3.0: 268 | 269 | http1.1的管道虽然解决了请求的队头阻塞,但没有解决响应的队头阻塞。http2.0也存在TCP层的队头阻塞(因为TCP的流控制和拥塞控制机制会导致数据包的排队现象)。 270 | 271 | 所以3.0采用基于UDP的QUIC代替TCP,QUIC有如下特点: 272 | 273 | 无队头阻塞,当某个流发⽣丢包时,只会阻塞这个流,其他流不会受到影响; 274 | 275 | 更快的连接建立,连接时减少了握手交互次数; 276 | 277 | 连接迁移,当网络环境变化时(例如从Wi-Fi切换到4G),连接可以无缝迁移,保持会话的持续性。 278 | 279 | 36. 长轮询短轮询 280 | 281 | 长轮询:客户端的请求在服务端等待,直到服务器有新的数据可用再返回响应,可以减少无效的往返次数。 282 | 283 | 短轮询:客户端定期向服务器发起请求,询问是否有新的数据,如果服务器上有新的数据就返回这些数据,否则返回空响应。 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | -------------------------------------------------------------------------------- /设计模式.md: -------------------------------------------------------------------------------- 1 | 1. 单例设计模式 2 | 3 | 保证一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例。 4 | 5 | 通过限制构造函数(通过设置其为私有)从而限制单例类的实例化。之后在定义类时包含一个该类的静态私有对象,以便创建单例类的实例。 6 | 7 | 2. 工厂方法设计模式 8 | 9 | 工厂方法模式提供了一种创建对象的最佳方式,它在父类中声明一个创建对象的接口,让子类决定实例化哪一个类。工厂方法模式将实例化的过程推迟到子类。 10 | 11 | 场景:无法预知对象确切类别及其依赖关系时 12 | 13 | 3. 抽象工厂设计模式 14 | 15 | 抽象工厂模式提供了一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。它允许客户端使用一个共同的接口来创建一组相关的对象,而不需要知道它们的具体实现。 16 | 17 | --------------------------------------------------------------------------------