Parcourir la source

更新了面经

seamew il y a 1 an
Parent
commit
db2f23bbf2
85 fichiers modifiés avec 1555 ajouts et 6 suppressions
  1. 66 0
      算法/排序/数字出现次数.md
  2. 3 0
      算法/贪心/求数组中比左边元素都大同时比右边元素都小的元素.md
  3. 63 0
      面经/问答/JVM.md
  4. 67 0
      面经/问答/Mysql.md
  5. BIN
      面经/问答/assets/04163db3b7ca4b4c8fbfdf4e7253a98ftplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp
  6. BIN
      面经/问答/assets/13e4361407ba46979e802eaa654dcf67.png
  7. BIN
      面经/问答/assets/1418b09699fe4614a594a565c55055d5tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp
  8. BIN
      面经/问答/assets/16c1e0e2878c44dctplv-t2oaga2asx-jj-mark3024000q75.awebp
  9. BIN
      面经/问答/assets/16c1e0e28889eafftplv-t2oaga2asx-jj-mark3024000q75.awebp
  10. BIN
      面经/问答/assets/16c1e0e289a351b2tplv-t2oaga2asx-jj-mark3024000q75.awebp
  11. BIN
      面经/问答/assets/16c1e0e289f9213etplv-t2oaga2asx-jj-mark3024000q75.awebp
  12. BIN
      面经/问答/assets/16c1e0e28aee99a1tplv-t2oaga2asx-jj-mark3024000q75.awebp
  13. BIN
      面经/问答/assets/16c1e0e2ca893b49tplv-t2oaga2asx-jj-mark3024000q75.awebp
  14. BIN
      面经/问答/assets/16c1e0e2d7df4fbftplv-t2oaga2asx-jj-mark3024000q75.awebp
  15. BIN
      面经/问答/assets/16c1e0e2db1c4812tplv-t2oaga2asx-jj-mark3024000q75-16934721993069.awebp
  16. BIN
      面经/问答/assets/16c1e0e2db1c4812tplv-t2oaga2asx-jj-mark3024000q75.awebp
  17. BIN
      面经/问答/assets/16c1e0e2e2a2835etplv-t2oaga2asx-jj-mark3024000q75.awebp
  18. BIN
      面经/问答/assets/16c1e0e2e507aec0tplv-t2oaga2asx-jj-mark3024000q75.awebp
  19. BIN
      面经/问答/assets/16c1e0e3178398fctplv-t2oaga2asx-jj-mark3024000q75.awebp
  20. BIN
      面经/问答/assets/16c1e26a5ecf086etplv-t2oaga2asx-jj-mark3024000q75.awebp
  21. BIN
      面经/问答/assets/20201104163915661.png
  22. BIN
      面经/问答/assets/20201104164026393.png
  23. BIN
      面经/问答/assets/20201104164057943.png
  24. BIN
      面经/问答/assets/2535cfeb9cfb44d4a0a04506f09f7485tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp
  25. BIN
      面经/问答/assets/26f88373d8454682b9e0c1d4fd1611b4-20230309233114856.png
  26. BIN
      面经/问答/assets/2802786ab4f52c1e248904e5cef33a74.png
  27. BIN
      面经/问答/assets/2ae0ed790c7e7403f215acb2bd82e884.png
  28. BIN
      面经/问答/assets/316b81a42ea843a192cc2f3578fbdb81tplv-k3u1fbpfcp-zoom-in-crop-mark1512000-16927837871897.awebp
  29. BIN
      面经/问答/assets/316b81a42ea843a192cc2f3578fbdb81tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp
  30. BIN
      面经/问答/assets/31b9745a2fa94fb693a7ea1257aa5f7ftplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp
  31. BIN
      面经/问答/assets/33f858652afc4be6bb1d7b1f1dc33eaatplv-k3u1fbpfcp-zoom-in-crop-mark1512000-169278452952817.awebp
  32. BIN
      面经/问答/assets/33f858652afc4be6bb1d7b1f1dc33eaatplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp
  33. BIN
      面经/问答/assets/3层跳表-跨度.drawio.png
  34. BIN
      面经/问答/assets/3层跳表-跨度.png
  35. BIN
      面经/问答/assets/517e2f7939394b3ca35936702a107f07tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp
  36. BIN
      面经/问答/assets/559260f5ef944cd1a146d07308275b89tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp
  37. BIN
      面经/问答/assets/60d6b91f49ac4d11bee43f86f33566d6tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp
  38. BIN
      面经/问答/assets/6a68be69f6b64ad1a8df234d9f0772detplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp
  39. BIN
      面经/问答/assets/775865f6bd894dfba8d373ee54d79af1.png
  40. BIN
      面经/问答/assets/788acb3c73174c42b603fe67e96c89f5tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp
  41. BIN
      面经/问答/assets/7d45e677f6c2428d9b9db7022416fe26tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp
  42. BIN
      面经/问答/assets/80a2bcb823e04e05a10c8f19de2930b9tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp
  43. BIN
      面经/问答/assets/a6e0e622b7384ac78f733e471b280c27tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp
  44. BIN
      面经/问答/assets/a851f56a384b4a0f9da76644d645ae1ftplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp
  45. BIN
      面经/问答/assets/b52e004bd4654cb1b51cdb409182156ftplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp
  46. BIN
      面经/问答/assets/c7787bc99996429381a1049580f6c24atplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp
  47. BIN
      面经/问答/assets/cdc14698f629c74bf5a239cc8a611aeb.png
  48. BIN
      面经/问答/assets/d4ce0c7fdf3b4039a0e4b0b200af731atplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp
  49. BIN
      面经/问答/assets/d9f2d1f2d3674a50900eda504ecaa326tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp
  50. BIN
      面经/问答/assets/db568766644a4d10b8a91cdd2f8a4070.png
  51. BIN
      面经/问答/assets/f0d9d314c75c4bd989dadf044ecd6307tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp
  52. BIN
      面经/问答/assets/f3e463285d0e4d6299fca19c7ef7e334tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp
  53. BIN
      面经/问答/assets/fc2b320077124da39bade7adeaf7ef08tplv-k3u1fbpfcp-zoom-in-crop-mark1512000-16927837168295.awebp
  54. BIN
      面经/问答/assets/fc2b320077124da39bade7adeaf7ef08tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp
  55. BIN
      面经/问答/assets/image-20230824174350063.png
  56. BIN
      面经/问答/assets/image-20230824174809579.png
  57. BIN
      面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70-16928701359154.png
  58. BIN
      面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70-16928703073727.png
  59. BIN
      面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70-16928703609439.png
  60. BIN
      面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70-169287036683211.png
  61. BIN
      面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70-169287037295013.png
  62. BIN
      面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70-169287037995315.png
  63. BIN
      面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70-169287038514317.png
  64. BIN
      面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70-169287039073419.png
  65. BIN
      面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70-169287039639021.png
  66. BIN
      面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70-169287040255723.png
  67. BIN
      面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70-169287040597125.png
  68. BIN
      面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70-169287041216327.png
  69. BIN
      面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70-169287042233929.png
  70. BIN
      面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70-169287043099231.png
  71. BIN
      面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70-169287043746233.png
  72. BIN
      面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70-169287061300535.png
  73. BIN
      面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70.png
  74. BIN
      面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3N1MjIzMTU5NTc0Mg==,size_16,color_FFFFFF,t_70-169347279895916.png
  75. BIN
      面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3N1MjIzMTU5NTc0Mg==,size_16,color_FFFFFF,t_70.png
  76. 217 0
      面经/问答/kafka.md
  77. 272 1
      面经/问答/redis.md
  78. 441 0
      面经/问答/vue.md
  79. 60 1
      面经/问答/基础.md
  80. 0 2
      面经/问答/并发编程.md
  81. 83 1
      面经/问答/情景设计题.md
  82. 14 0
      面经/问答/操作系统.md
  83. 183 1
      面经/问答/智力问题.md
  84. 34 0
      面经/问答/职业问题.md
  85. 52 0
      面经/问答/计网.md

+ 66 - 0
算法/排序/数字出现次数.md

@@ -0,0 +1,66 @@
+## 统计数组中不同元素出现的次数(时间复杂度O(n),空间复杂度o(1))
+
+常规思路是利用map进行统计,但不满足时间和空间复杂度的要求;
+正确思路:
+遍历数组,通过当前元素的值a作为下标,找到下一个元素。最后得到的数组中,下标(因为数组的下标都是从0开始的,所以需要+1)为数组中出现的元素,每个下标对应的值取反输出即是该元素出现的次数。
+
+若当前元素a小于0,则跳过;
+若当前元素a大于0,则判断其作为下标对应的元素b是否大于0。
+若b大于0,则把b的值赋值给a,并把b置为-1;
+若b小于0,则把a置为0,并把b自减1;
+注意遍历时,若当前元素为正数依旧处理该当前元素;
+
+```cpp
+// 举例
+vector<int> arr = {2, 5, 5, 2, 3};
+下标都+1;
+/*
+1、遍历数组,第一个arr[1]=2,然后看下标为2的元素是arr[2]=5。
+2、把arr[2]对应的5赋值给arr[1],然后arr[2]就设置为-1
+3、然后重复整个过程直到结束
+它的整个变化过程就是这样
+- {2, 5, 5, 2, 3}
+- 5, [-1], 5, 2, 3
+- 3, [-1], 5, 2, [-1]
+- 5, [-1], [-1], 2, [-1]
+- [0], [-1], [-1], 2, [-2]
+- [0], [-2], [-1], [0], [-2]
+这个结果表示:1有0个,2有2个,3有一个,4有0个,5有2个
+*/
+```
+
+```cpp
+#include <bits/stdc++.h>
+
+using namespace std;
+
+void sort(vector<int>& nums) {
+    int i = 0;
+    int n = (int) nums.size();
+    while (i < n) {
+        int temp = nums[i] - 1;
+        if (temp < 0) {
+            i++;
+            continue;
+        }
+        if (nums[temp] > 0) {
+            nums[i] = nums[temp];
+            nums[temp] = -1;
+        } else {
+            nums[temp]--;
+            nums[i] = 0;
+        }
+    }
+}
+
+int main() {
+    vector<int> nums{2, 5, 5, 2, 3};
+    sort(nums);
+    for (int i = 0; i < nums.size(); i++) {
+        if (nums[i] < 0)
+            cout << i + 1 << " " << (-nums[i]) << endl;
+    }
+    return 0;
+}
+```
+

+ 3 - 0
算法/贪心/求数组中比左边元素都大同时比右边元素都小的元素.md

@@ -0,0 +1,3 @@
+## 求数组中比左边元素都大同时比右边元素都小的元素,返回这些元素的索引。要求复杂度O(N)
+
+对于每个元素,如果它比左侧最大的值要大,同时比右侧最小的值要小,就满足条件。用两个数组维护,left_max[i] 表示原数组 [0, i) 的最大值,right_min[i] 表示原数组 (i, n) 的最小值。

+ 63 - 0
面经/问答/JVM.md

@@ -20,12 +20,55 @@ Minor GC 通常在新生代中的 Eden 区域满时,或者 Survivor 区域中
 - **Major GC 是清理老年代。**
 - **Full GC 是清理整个堆空间—包括年轻代和老年代。**
 
+## 频繁发生GC如何优化
+
+`jstat -gcutil 5280 1000` 命令是用于监视 Java 虚拟机的垃圾回收统计信息的命令。这个命令通常在命令行中执行,用于实时监测 Java 应用程序的内存管理和垃圾回收情况。
+
+解释一下命令的各个部分:
+
+- `jstat`: Java 虚拟机统计信息工具,用于监测虚拟机状态和性能数据。
+- `-gcutil`: 表示查询垃圾回收的统计信息,包括各个垃圾回收器的使用情况。
+- `5280`: 进程的进程 ID 或虚拟机标识符(JVM ID),用于指定要监控的 Java 虚拟机实例。这个值需要根据实际情况进行替换。
+- `1000`: 表示每隔 1000 毫秒(1秒)收集一次统计信息并输出。
+
+### 可能原因
+
+* 内存泄漏(代码有问题,对象引用没及时释放,导致对象不能及时回收)。
+* 死循环。在死循环中,对象不断被创建,但无法被正常释放,最终导致堆内存耗尽,触发 Full GC 进行垃圾回收。
+* 大对象。 尤其是大对象,80%以上的情况就是他。 那么大对象从哪里来的呢?
+* 数据库(包括MySQL和MongoDB等NoSQL数据库),结果集太大。
+* 第三方接口传输的大对象。
+* 消息队列,消息太大。
+
+## JVM调优
+
+对应的参数列表
+
+```shell
+-XX:+PrintGC 输出GC日志
+-XX:+PrintGCDetails 输出GC的详细日志
+-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
+-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
+-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
+-Xloggc:../logs/gc.log 日志文件的输出路径
+```
+
+当元空间出现内存碎片化时,我们会着重关注是不是创建了大量的类加载器。
+
+通过进一步分析,发现是由于反射导致创建大量 DelegatingClassLoader。其核心原理如下:
+
+在 JVM 上,最初是通过 JNI 调用来实现方法的反射调用,当 JVM 注意到通过反射经常访问某个方法时,它将生成字节码来执行相同的操作,称为膨胀(inflation)机制。如果使用字节码的方式,则会为该方法生成一个 DelegatingClassLoader,如果存在大量方法经常反射调用,则会导致创建大量 DelegatingClassLoader。
+
 ## JVM调优
 
 -Xmx –Xms:指定 java 堆最大值(默认值是物理内存的 1/4(<1GB))和初始 java 堆最小值(默认值是物理内存的 1/64(<1GB))
 
 开发过程中,通常会将 -Xms 与 -Xmx 两个参数配置成相同的值,其目的是为了能够在 java 垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小而浪费资源。
 
+
+
+创建大量MAP导致内存暴涨,优化kafka消息
+
 ## CMS垃圾收集器
 
 CMS(标记并发清除垃圾收集器),对比以前的jvm垃圾收集器,最大区别在于并发,当GC线程工作的时候,用户线程不会完全停止,会与GC线程并发执行
@@ -146,3 +189,23 @@ jvm内存区域大体上可以分为线程私有和线程共有两大部分
 
 
 
+## 内存泄漏和内存溢出的区别
+
+在Java中,内存泄漏(Memory Leak)和内存溢出(Memory Overflow)是两个不同的概念,它们有一些区别。
+
+1. 内存泄漏(Memory Leak): 内存泄漏指的是在程序运行过程中,由于错误的内存管理导致无法再被使用的对象仍然占据着内存空间,而这些占用的内存无法被释放。这意味着系统的可用内存逐渐减少,直到最终导致内存耗尽。常见的内存泄漏场景包括:
+   - 未正确释放动态分配的内存:如果程序在分配了内存后没有及时释放,这些内存就会一直占据系统资源。
+   - 对象引用仍存在但不再需要:如果某个对象的引用仍然存在,即使该对象不再需要,垃圾回收器也无法回收该对象所占用的内存。
+2. 内存溢出(Memory Overflow): 内存溢出指的是程序在申请内存时,无法获得足够的内存空间以满足其需求,导致无法继续执行程序或者出现异常。当程序需要分配更多内存而可用内存不足时,就会发生内存溢出。常见的内存溢出场景包括:
+   - 堆内存溢出:当创建过多的对象并且无法被垃圾回收时,堆内存可能会耗尽。这通常是由于程序中存在内存泄漏或者申请了过大的对象导致的。
+   - 栈内存溢出:当递归调用层级过深、方法调用过多或者栈帧过大时,栈内存可能会耗尽。每个方法调用都会在栈上创建一个栈帧,如果栈帧数量超过了栈的容量,就会导致栈内存溢出。
+
+区别:
+
+- 内存泄漏是指无法释放已经分配的内存,导致内存持续占用和耗尽;而内存溢出则是指无法获得足够的内存空间来满足需求,导致程序无法执行或者出现异常。
+- 内存泄漏通常是逐渐发生的,随着时间的推移导致内存占用不断增加;而内存溢出通常是在程序执行过程中突然发生的,因为需要的内存超过了可用内存。
+- 内存泄漏是程序中的错误行为,通常是由于忘记释放资源或者引用未及时清理等原因导致的;而内存溢出可能是因为程序设计问题,如递归调用或者对象太大等。
+
+## 内存泄漏
+
+hbase配置类需要成为static

+ 67 - 0
面经/问答/Mysql.md

@@ -474,3 +474,70 @@ MySQL中存储索引用到的数据结构是B+树,B+树的查询时间跟树
 一、**从内存角度上说**,数据库中的索引一般是在磁盘上,数据量大的情况可能无法一次性装入内存,B+树的设计可以允许数据分批加载。
 
 二、**从业务场景上说**,如果只选择一个数据那确实是hash更快,但是数据库中经常会选中多条,这时候由于B+树索引有序,并且又有链表相连,它的查询效率比hash就快很多了。
+
+## B+树插入流程
+
+B+树是B树的一种变形形式,一颗B+树包含根节点、内部节点和叶子节点,B+树上的叶子节点存储关键字以及相应记录的地址,叶子节点以上各层作为索引使用。
+
+一棵m阶的B+树定义如下:
+
+* 1)根结点最少包含1个关键字个数,最多包含m-1个关键字;
+* 2)B+树内部结点不保存数据,只用于索引,所有数据(或者说记录)都保存在叶子结点中;
+* 3)内部结点最少有ceil(m / 2) (向上取整)- 1个关键字,最多有m-1个关键字(或者说内部结点最多有m个子树);
+* 4)内部结点中的key都按照从小到大的顺序排列,对于内部结点中的一个key,左树中的所有key都小于它,右子树中的key都大于等于它,叶子结点中的记录也按照key的大小排列;
+* 5)每个叶子结点都存有相邻叶子结点的指针,叶子结点本身依关键字的大小自小而大顺序连接;
+  如下图是一颗标准的5阶B+树:
+
+![image-20230824174350063](assets/image-20230824174350063.png)
+
+![image-20230824174809579](assets/image-20230824174809579.png)
+
+![img](assets/20201104163915661.png)
+
+​	
+
+下面我们通过一个简单的示例说明B+树的详细构建过程:
+
+我们以构建一个5阶B+树为例,根据B+树特性,最少包含2个关键字,最多可以包含4个关键字,当不满足上面条件的时候,可能就需要进行分裂操作了。
+
+![img](assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70-169287061300535.png)
+
+## 数据库三大范式
+
+* 第一范式:列不可再分
+* 第二范式:属性完全依赖主键
+* 第三范式:属性不依赖于其他非主键,属性直接依赖于主键
+
+## limit与深度翻页
+
+最常见的分页写法就是使用limit,在分页查询时,会在 LIMIT 后面传两个参数,一个是偏移量(offset),一个是获取的条数(limit)。
+
+实现方式是先查询offset+limit条数据,再将offset条数据丢弃给用户返回剩下的limit条数据。比如limit 10000,10实际上是mysql查找到前10010条数据,之后丢弃前面的10000行后再返回
+
+这样子当偏移量很小时,查询速度很快,但是随着 offset 变大时,查询速度会越来越慢,因为查找的数据越来越多
+
+### 优化
+
+- 方式一:
+
+```mysql
+select * from t1 where id >= 300000 order by id limit 10
+```
+
+避免了扫描前offset条记录
+
+但是每次查询都需要拿到上一页的最大/小id。比如当前在第3页,需要查询第5页的数据就没办法了
+
+* 方式二:
+
+结合普通limit与方式一,解决方式二的问题,但是offset要尽量小
+
+```mysql
+select * from t1 where id > 300000 order by id limit 10, 10
+```
+
+* 方式三:
+
+```mysql
+select * from t1 where id > (select id from t1 order by id limit 300000, 1) limit 10
+```

BIN
面经/问答/assets/04163db3b7ca4b4c8fbfdf4e7253a98ftplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp


BIN
面经/问答/assets/13e4361407ba46979e802eaa654dcf67.png


BIN
面经/问答/assets/1418b09699fe4614a594a565c55055d5tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp


BIN
面经/问答/assets/16c1e0e2878c44dctplv-t2oaga2asx-jj-mark3024000q75.awebp


BIN
面经/问答/assets/16c1e0e28889eafftplv-t2oaga2asx-jj-mark3024000q75.awebp


BIN
面经/问答/assets/16c1e0e289a351b2tplv-t2oaga2asx-jj-mark3024000q75.awebp


BIN
面经/问答/assets/16c1e0e289f9213etplv-t2oaga2asx-jj-mark3024000q75.awebp


BIN
面经/问答/assets/16c1e0e28aee99a1tplv-t2oaga2asx-jj-mark3024000q75.awebp


BIN
面经/问答/assets/16c1e0e2ca893b49tplv-t2oaga2asx-jj-mark3024000q75.awebp


BIN
面经/问答/assets/16c1e0e2d7df4fbftplv-t2oaga2asx-jj-mark3024000q75.awebp


BIN
面经/问答/assets/16c1e0e2db1c4812tplv-t2oaga2asx-jj-mark3024000q75-16934721993069.awebp


BIN
面经/问答/assets/16c1e0e2db1c4812tplv-t2oaga2asx-jj-mark3024000q75.awebp


BIN
面经/问答/assets/16c1e0e2e2a2835etplv-t2oaga2asx-jj-mark3024000q75.awebp


BIN
面经/问答/assets/16c1e0e2e507aec0tplv-t2oaga2asx-jj-mark3024000q75.awebp


BIN
面经/问答/assets/16c1e0e3178398fctplv-t2oaga2asx-jj-mark3024000q75.awebp


BIN
面经/问答/assets/16c1e26a5ecf086etplv-t2oaga2asx-jj-mark3024000q75.awebp


BIN
面经/问答/assets/20201104163915661.png


BIN
面经/问答/assets/20201104164026393.png


BIN
面经/问答/assets/20201104164057943.png


BIN
面经/问答/assets/2535cfeb9cfb44d4a0a04506f09f7485tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp


BIN
面经/问答/assets/26f88373d8454682b9e0c1d4fd1611b4-20230309233114856.png


BIN
面经/问答/assets/2802786ab4f52c1e248904e5cef33a74.png


BIN
面经/问答/assets/2ae0ed790c7e7403f215acb2bd82e884.png


BIN
面经/问答/assets/316b81a42ea843a192cc2f3578fbdb81tplv-k3u1fbpfcp-zoom-in-crop-mark1512000-16927837871897.awebp


BIN
面经/问答/assets/316b81a42ea843a192cc2f3578fbdb81tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp


BIN
面经/问答/assets/31b9745a2fa94fb693a7ea1257aa5f7ftplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp


BIN
面经/问答/assets/33f858652afc4be6bb1d7b1f1dc33eaatplv-k3u1fbpfcp-zoom-in-crop-mark1512000-169278452952817.awebp


BIN
面经/问答/assets/33f858652afc4be6bb1d7b1f1dc33eaatplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp


BIN
面经/问答/assets/3层跳表-跨度.drawio.png


BIN
面经/问答/assets/3层跳表-跨度.png


BIN
面经/问答/assets/517e2f7939394b3ca35936702a107f07tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp


BIN
面经/问答/assets/559260f5ef944cd1a146d07308275b89tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp


BIN
面经/问答/assets/60d6b91f49ac4d11bee43f86f33566d6tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp


BIN
面经/问答/assets/6a68be69f6b64ad1a8df234d9f0772detplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp


BIN
面经/问答/assets/775865f6bd894dfba8d373ee54d79af1.png


BIN
面经/问答/assets/788acb3c73174c42b603fe67e96c89f5tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp


BIN
面经/问答/assets/7d45e677f6c2428d9b9db7022416fe26tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp


BIN
面经/问答/assets/80a2bcb823e04e05a10c8f19de2930b9tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp


BIN
面经/问答/assets/a6e0e622b7384ac78f733e471b280c27tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp


BIN
面经/问答/assets/a851f56a384b4a0f9da76644d645ae1ftplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp


BIN
面经/问答/assets/b52e004bd4654cb1b51cdb409182156ftplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp


BIN
面经/问答/assets/c7787bc99996429381a1049580f6c24atplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp


BIN
面经/问答/assets/cdc14698f629c74bf5a239cc8a611aeb.png


BIN
面经/问答/assets/d4ce0c7fdf3b4039a0e4b0b200af731atplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp


BIN
面经/问答/assets/d9f2d1f2d3674a50900eda504ecaa326tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp


BIN
面经/问答/assets/db568766644a4d10b8a91cdd2f8a4070.png


BIN
面经/问答/assets/f0d9d314c75c4bd989dadf044ecd6307tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp


BIN
面经/问答/assets/f3e463285d0e4d6299fca19c7ef7e334tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp


BIN
面经/问答/assets/fc2b320077124da39bade7adeaf7ef08tplv-k3u1fbpfcp-zoom-in-crop-mark1512000-16927837168295.awebp


BIN
面经/问答/assets/fc2b320077124da39bade7adeaf7ef08tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp


BIN
面经/问答/assets/image-20230824174350063.png


BIN
面经/问答/assets/image-20230824174809579.png


BIN
面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70-16928701359154.png


BIN
面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70-16928703073727.png


BIN
面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70-16928703609439.png


BIN
面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70-169287036683211.png


BIN
面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70-169287037295013.png


BIN
面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70-169287037995315.png


BIN
面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70-169287038514317.png


BIN
面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70-169287039073419.png


BIN
面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70-169287039639021.png


BIN
面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70-169287040255723.png


BIN
面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70-169287040597125.png


BIN
面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70-169287041216327.png


BIN
面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70-169287042233929.png


BIN
面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70-169287043099231.png


BIN
面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70-169287043746233.png


BIN
面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70-169287061300535.png


BIN
面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dlaXhpYW9odWFp,size_16,color_FFFFFF,t_70.png


BIN
面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3N1MjIzMTU5NTc0Mg==,size_16,color_FFFFFF,t_70-169347279895916.png


BIN
面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3N1MjIzMTU5NTc0Mg==,size_16,color_FFFFFF,t_70.png


+ 217 - 0
面经/问答/kafka.md

@@ -200,3 +200,220 @@ Kafka从0.11.x版本开始引入这种分配策略,它主要有两个目的:
   * 如果是:去数据库中修改订单状态为已取消
 
  * 如果否:记录当前消息的offset,并不再继续消费之后的消息。等待1分钟后,再次 向kafka拉取该offset及之后的消息,继续进⾏判断,以此反复。
+
+## 消息队列对比
+
+| **对比方向** | **概要**                                                     |
+| ------------ | ------------------------------------------------------------ |
+| 吞吐量       | 万级的 ActiveMQ 和 RabbitMQ 的吞吐量(ActiveMQ 的性能最差)要比 十万级甚至是百万级的 RocketMQ 和 Kafka 低一个数量级。 |
+| 可用性       | 都可以实现高可用。ActiveMQ 和 RabbitMQ 都是基于主从架构实现高可用性。RocketMQ 基于分布式架构。 kafka 也是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 |
+| 时效性       | RabbitMQ 基于 erlang 开发,所以并发能力很强,性能极其好,延时很低,达到微秒级。其他三个都是 ms 级。 |
+| 功能支持     | 除了 Kafka,其他三个功能都较为完备。 Kafka 功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用,是事实上的标准 |
+| 消息丢失     | ActiveMQ 和 RabbitMQ 丢失的可能性非常低, RocketMQ 和 Kafka 理论上不会丢失。 |
+
+**总结:**
+
+- ActiveMQ 的社区算是比较成熟,但是较目前来说,ActiveMQ 的性能比较差,而且版本迭代很慢,不推荐使用。
+- RabbitMQ 在吞吐量方面虽然稍逊于 Kafka 和 RocketMQ ,但是由于它基于 erlang 开发,所以并发能力很强,性能极其好,延时很低,达到微秒级。但是也因为 RabbitMQ 基于 erlang 开发,所以国内很少有公司有实力做 erlang 源码级别的研究和定制。如果业务场景对并发量要求不是太高(十万级、百万级),那这四种消息队列中,RabbitMQ 一定是你的首选。如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。
+- RocketMQ 阿里出品,Java 系开源项目,源代码我们可以直接阅读,然后可以定制自己公司的 MQ,并且 RocketMQ 有阿里巴巴的实际业务场景的实战考验。RocketMQ 社区活跃度相对较为一般,不过也还可以,文档相对来说简单一些,然后接口这块不是按照标准 JMS 规范走的有些系统要迁移需要修改大量代码。还有就是阿里出台的技术,你得做好这个技术万一被抛弃,社区黄掉的风险,那如果你们公司有技术实力我觉得用 RocketMQ 挺好的
+- Kafka 的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,ms 级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展。同时 kafka 最好是支撑较少的 topic 数量即可,保证其超高吞吐量。kafka 唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略这个特性天然适合大数据实时计算以及日志收集。
+
+## 为什么选择使用kafka
+
+1. **实时数据流处理**: 您的项目需要处理煤矿大数据的实时采集、传输、存储和分发,其中实时性是关键。Kafka作为一个分布式流处理平台,能够高效地处理大量的实时数据流。结合HBase,Kafka可以将实时数据快速传输到HBase存储层,并在整个数据处理流程中保持低延迟。
+2. **高吞吐量和可扩展性**: 您提到的每秒10000条的消息速率以及每日产生80GB数据,显示了您需要处理大规模数据量的能力。Kafka以其高吞吐量和可扩展性,能够有效地处理这些数据负载,同时保持较低的延迟。
+3. **数据流治理和重复消费防护**: 在您的项目中,数据传输模块需要能够确保数据流的可靠性。Kafka提供了数据流治理的能力,您可以使用策略模式来判断数据治理函数,同时利用Redis缓存Topic分区偏移量以防止重复消费。
+4. **生态系统和社区支持**: Kafka作为一个成熟的消息中间件,拥有广泛的生态系统和社区支持。这对于您在整合各个组件、解决问题以及在将来可能的需求变化时都非常有帮助。
+5. **与HBase的结合**: Kafka和HBase是一对很好的搭档。通过Kafka将实时数据传输到HBase,您可以实现实时数据的快速存储和查询。Kafka提供了一个连接数据产生者和数据消费者的桥梁,使得数据流处理能够与数据存储紧密结合。
+
+综上所述,选择使用Kafka是出于其在实时数据流处理、高吞吐量、可扩展性、数据流治理以及与HBase的结合方面的优势。
+
+### 为什么选择kafka2
+
+|            | Kafka                                                        | RocketMQ                                                     | RabbitMQ                                                     |
+| ---------- | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
+| 单机吞吐量 | 17.3w/s                                                      | 11.6w/s                                                      | 2.6w/s(消息做持久化)                                       |
+| 开发语言   | Scala/Java                                                   | Java                                                         | Erlang                                                       |
+| 主要维护者 | Apache                                                       | Alibaba                                                      | Mozilla/Spring                                               |
+| 订阅形式   | 基于topic,按照topic进行正则匹配的发布订阅模式               | 基于topic/messageTag,按照消息类型、属性进行正则匹配的发布订阅模式 | 提供了4种:direct, topic ,Headers和fanout。fanout就是广播模式 |
+| 持久化     | 支持大量堆积                                                 | 支持大量堆积                                                 | 支持少量堆积                                                 |
+| 顺序消息   | 支持                                                         | 支持                                                         | 不支持                                                       |
+| 集群方式   | 天然的Leader-Slave,无状态集群,每台服务器既是Master也是Slave | 常用 多对’Master-Slave’ 模式,开源版本需手动切换Slave变成Master | 支持简单集群,'复制’模式,对高级集群模式支持不好。           |
+| 性能稳定性 | 较差                                                         | 一般                                                         | 好                                                           |
+
+- RabbitMQ是开源的,有比较稳定的支持,活跃度也高,但是不是Java语言开发的。
+- 很多公司用RocketMQ,是阿里出品的。
+- 如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的。
+
+## 让你写一个消息队列,该如何进行架构设计?
+
+![image.png](assets/80a2bcb823e04e05a10c8f19de2930b9tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp)
+
+1. 首先是消息队列的整体流程,producer发送消息给broker,broker存储好,broker再发送给consumer消费,consumer回复消费确认等。
+2. producer发送消息给broker,broker发消息给consumer消费,那就需要两次RPC了,RPC如何设计呢?可以参考开源框架Dubbo,你可以说说服务发现、序列化协议等等
+3. broker考虑如何持久化呢,是放文件系统还是数据库呢,会不会消息堆积呢,消息堆积如何处理呢。
+4. 消费关系如何保存呢? 点对点还是广播方式呢?广播关系又是如何维护呢?zk还是config server
+5. 消息可靠性如何保证呢?如果消息重复了,如何幂等处理呢?
+6. 消息队列的高可用如何设计呢? 可以参考Kafka的高可用保障机制。多副本 -> leader & follower -> broker 挂了重新选举 leader 即可对外服务。
+7. 消息事务特性,与本地业务同个事务,本地消息落库;消息投递到服务端,本地才删除;定时任务扫描本地消息库,补偿发送。
+8. MQ得伸缩性和可扩展性,如果消息积压或者资源不够时,如何支持快速扩容,提高吞吐?可以参照一下 Kafka 的设计理念,broker -> topic -> partition,每个 partition 放一个机器,就存一部分数据。如果现在资源不够了,简单啊,给 topic 增加 partition,然后做数据迁移,增加机器,不就可以存放更多数据,提供更高的吞吐量了吗。
+
+### Zookeeper 在 Kafka 中的作用
+
+Zookeeper在Kafka中扮演着关键的角色,它是Kafka集群中的一个核心组件,用于管理、协调和维护整个Kafka集群的状态。以下是Zookeeper在Kafka中的主要作用:
+
+1. **集群管理和协调**: Zookeeper负责管理Kafka集群中的各个节点的状态和信息,包括Broker、Topic、Partition等的元数据信息。它维护了整个集群的拓扑结构和分布式状态,确保集群的正常运行。
+2. **Leader选举**: 在Kafka的分区中,每个分区都有一个Leader和多个Follower。当Leader节点故障或不可用时,Zookeeper协助进行新Leader的选举过程,确保分区的高可用性。
+3. **Topic和Partition的元数据管理**: Kafka的元数据,如Topic和Partition的信息、副本分布等,都由Zookeeper进行管理和存储。消费者可以通过Zookeeper获取这些元数据,以便知道在哪里找到所需的数据。
+4. **Broker注册和发现**: Kafka集群中的每个Broker在启动时都会向Zookeeper注册自己的信息,包括主题分区信息、副本分布等。消费者和生产者可以通过Zookeeper获取Broker的地址和状态信息,以便与合适的Broker通信。
+5. **消费者组协调**: 当消费者以消费者组的形式订阅主题时,Zookeeper协助管理消费者的组成员,以及每个消费者在分区中的位置(偏移量)。这有助于实现消费者的负载均衡和故障转移。
+6. **偏移量管理**: Zookeeper存储了消费者的偏移量信息,即消费者上次消费的位置。这样即使消费者断开连接后重新连接,也可以从之前的位置继续消费,确保数据不会重复消费或遗漏。
+7. **负载均衡**:上面也说过了 Kafka 通过给特定 Topic 指定多个 Partition, 而各个 Partition 可以分布在不同的 Broker 上, 这样便能提供比较好的并发能力。 对于同一个 Topic 的不同 Partition,Kafka 会尽力将这些 Partition 分布到不同的 Broker 服务器上。当生产者产生消息后也会尽量投递到不同 Broker 的 Partition 里面。当 Consumer 消费的时候,Zookeeper 可以根据当前的 Partition 数量以及 Consumer 数量来实现动态负载均衡。
+
+## kafka为什么在2.8之后舍弃了zookeeper
+
+1. **简化架构**: 在以前的版本中,Kafka使用了Zookeeper来管理元数据、选举、存储偏移量等,这导致了Kafka集群的架构比较复杂。采用KRaft可以使得整体架构更加简化,因为KRaft将元数据和状态的管理集成到Kafka本身中,减少了对外部依赖。
+2. **一致性和可靠性**: KRaft基于Raft一致性算法,提供了更强的一致性保证。这可以帮助Kafka更好地处理元数据和状态的管理,从而提高整个系统的可靠性。
+3. **降低维护成本**: 使用Kafka自身来管理元数据和状态,可以减少对Zookeeper的依赖,从而降低了维护的复杂性。此外,Zookeeper和Kafka的不同版本之间可能会出现不兼容性问题,这也增加了维护和升级的难度。
+4. **性能优化**: KRaft被设计为适合于Kafka的使用场景,因此它可以通过优化来提升一些性能指标。对于元数据管理和状态同步等方面,KRaft可以更好地满足Kafka的需求。
+
+## Raft
+
+Raft 集群中每个节点都处于以下三种角色之一:
+
+- **Leader**: 所有请求的处理者,接收客户端发起的操作请求,写入本地日志后同步至集群其它节点。
+- **Follower**: 请求的被动更新者,从 leader 接收更新请求,写入本地文件。如果客户端的操作请求发送给了 follower,会首先由 follower 重定向给 leader。
+- **Candidate**: 如果 follower 在一定时间内没有收到 leader 的心跳,则判断 leader 可能已经故障,此时启动 leader election 过程,本节点切换为 candidate 直到选主结束。
+
+### 选举
+
+每开始一次新的选举,称为一个**任期**(**term**),每个 term 都有一个严格递增的整数与之关联。
+
+每当 candidate 触发 leader election 时都会增加 term,如果一个 candidate 赢得选举,他将在本 term 中担任 leader 的角色。但并不是每个 term 都一定对应一个 leader,有时候某个 term 内会由于选举超时导致选不出 leader,这时 candicate 会递增 term 号并开始新一轮选举。
+
+![img](assets/f3e463285d0e4d6299fca19c7ef7e334tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp)
+
+Term 更像是一个**逻辑时钟**(**logic clock**)的作用,有了它,就可以发现哪些节点的状态已经过期。每一个节点都保存一个 current term,在通信时带上这个 term 号。
+
+节点间通过 RPC 来通信,主要有两类 RPC 请求:
+
+- **RequestVote RPCs**: 用于 candidate 拉票选举。
+- **AppendEntries RPCs**: 用于 leader 向其它节点复制日志以及同步心跳。
+
+### 状态变化
+
+![img](assets/d9f2d1f2d3674a50900eda504ecaa326tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp)
+
+Raft 的选主基于一种心跳机制,集群中每个节点刚启动时都是 follower 身份(**Step: starts up**),leader 会周期性的向所有节点发送心跳包来维持自己的权威,那么首个 leader 是如何被选举出来的呢?方法是如果一个 follower 在一段时间内没有收到任何心跳,也就是选举超时,那么它就会主观认为系统中没有可用的 leader,并发起新的选举(**Step: times out, starts election**)。
+
+这里有一个问题,即这个“选举超时时间”该如何制定?如果所有节点在同一时刻启动,经过同样的超时时间后同时发起选举,整个集群会变得低效不堪,极端情况下甚至会一直选不出一个主节点。Raft 巧妙的使用了一个随机化的定时器,让每个节点的“超时时间”在一定范围内随机生成,这样就大大的降低了多个节点同时发起选举的可能性。
+
+![img](assets/fc2b320077124da39bade7adeaf7ef08tplv-k3u1fbpfcp-zoom-in-crop-mark1512000-16927837168295.awebp)
+
+图:一个五节点 Raft 集群的初始状态,所有节点都是 follower 身份,term 为 1,且每个节点的选举超时定时器不同
+
+若 follower 想发起一次选举,follower 需要先增加自己的当前 term,并将身份切换为 candidate。然后它会向集群其它节点发送“请给自己投票”的消息(RequestVote RPC)。
+
+![img](assets/316b81a42ea843a192cc2f3578fbdb81tplv-k3u1fbpfcp-zoom-in-crop-mark1512000-16927837871897.awebp)
+
+图:S1 率先超时,变为 candidate,term + 1,并向其它节点发出拉票请求
+
+### Candicate 状态转换过程
+
+Follower 切换为 candidate 并向集群其他节点发送“请给自己投票”的消息后,接下来会有三种可能的结果,也即上面**节点状态图中 candidate 状态向外伸出的三条线**。
+
+**1. 选举成功(Step: receives votes from majority of servers)**
+
+当candicate从整个集群的**大多数**(N/2+1)节点获得了针对同一 term 的选票时,它就赢得了这次选举,立刻将自己的身份转变为 leader 并开始向其它节点发送心跳来维持自己的权威。
+
+![img](assets/a851f56a384b4a0f9da76644d645ae1ftplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp)
+
+图:“大部分”节点都给了 S1 选票
+
+![img](assets/a6e0e622b7384ac78f733e471b280c27tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp)
+
+图:S1 变为 leader,开始发送心跳维持权威
+
+每个节点针对每个 term 只能投出一张票,并且按照先到先得的原则。这个规则确保只有一个 candidate 会成为 leader。
+
+**2. 选举失败(Step: discovers current leader or new term)**
+
+Candidate 在等待投票回复的时候,可能会突然收到其它自称是 leader 的节点发送的心跳包,如果这个心跳包里携带的 term **不小于** candidate 当前的 term,那么 candidate 会承认这个 leader,并将身份切回 follower。这说明其它节点已经成功赢得了选举,我们只需立刻跟随即可。但如果心跳包中的 term 比自己小,candidate 会拒绝这次请求并保持选举状态。
+
+![img](assets/d4ce0c7fdf3b4039a0e4b0b200af731atplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp)
+
+图:S4、S2 依次开始选举
+
+![img](assets/2535cfeb9cfb44d4a0a04506f09f7485tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp)
+
+图:S4 成为 leader,S2 在收到 S4 的心跳包后,由于 term 不小于自己当前的 term,因此会立刻切为 follower 跟随 S4
+
+**3. 选举超时(Step: times out, new election)**
+
+第三种可能的结果是 candidate 既没有赢也没有输。如果有多个 follower 同时成为 candidate,选票是可能被瓜分的,如果没有任何一个 candidate 能得到大多数节点的支持,那么每一个 candidate 都会超时。此时 candidate 需要增加自己的 term,然后发起新一轮选举。如果这里不做一些特殊处理,选票可能会一直被瓜分,导致选不出 leader 来。这里的“特殊处理”指的就是前文所述的**随机化选举超时时间**。
+
+![img](assets/04163db3b7ca4b4c8fbfdf4e7253a98ftplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp)
+
+图:S1 ~ S5 都在参与选举
+
+![img](assets/517e2f7939394b3ca35936702a107f07tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp)
+
+图:没有任何节点愿意给他人投票
+
+![img](assets/b52e004bd4654cb1b51cdb409182156ftplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp)
+
+图:如果没有随机化超时时间,所有节点将会继续同时发起选举……
+
+### Leader 状态转换过程
+
+节点状态图中的最后一条线是:**discovers server with higher term**。想象一个场景:当 leader 节点发生了宕机或网络断连,此时其它 follower 会收不到 leader 心跳,首个触发超时的节点会变为 candidate 并开始拉票(由于随机化各个 follower 超时时间不同),由于该 candidate 的 term 大于原 leader 的 term,因此所有 follower 都会投票给它,这名 candidate 会变为新的 leader。一段时间后原 leader 恢复了,收到了来自新leader 的心跳包,发现心跳中的 term 大于自己的 term,此时该节点会立刻切换为 follower 并跟随的新 leader。
+
+上述流程的动画模拟如下:
+
+![img](assets/33f858652afc4be6bb1d7b1f1dc33eaatplv-k3u1fbpfcp-zoom-in-crop-mark1512000-169278452952817.awebp) 
+
+图:S4 作为 term2 的 leader
+
+![img](assets/7d45e677f6c2428d9b9db7022416fe26tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp) 
+
+图:S4 宕机,S5 即将率先超时
+
+![](assets/1418b09699fe4614a594a565c55055d5tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp)
+
+图:S5 当选 term3 的 leader
+
+![img](assets/f0d9d314c75c4bd989dadf044ecd6307tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp) 
+
+图:S4 宕机恢复后收到了来自 S5 的 term3 心跳
+
+![img](assets/31b9745a2fa94fb693a7ea1257aa5f7ftplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp)
+
+图:S4 立刻变为 S5 的 follower
+
+以上就是 Raft 的选主逻辑,但还有一些细节(譬如是否给该 candidate 投票还有一些其它条件)依赖算法的其它部分基础,我们会在后续“安全性”一章描述。
+
+当票选出 leader 后,leader 也该承担起相应的责任了,这个责任是什么?就是下一章将介绍的“日志复制”。
+
+
+
+### 复制解析
+
+一旦 leader 被票选出来,它就承担起领导整个集群的责任了,开始接收客户端请求,并将操作包装成日志,并复制到其它节点上去。
+
+整体流程如下:
+
+- Leader 为客户端提供服务,客户端的每个请求都包含一条即将被状态复制机执行的指令。
+- Leader 把该指令作为一条新的日志附加到自身的日志集合,然后向其它节点发起**附加条目请求**(**AppendEntries RPC**),来要求它们将这条日志附加到各自本地的日志集合。
+- 当这条日志已经确保被**安全的复制**,即大多数(N/2+1)节点都已经复制后,leader 会将该日志 **apply** 到它本地的状态机中,然后把操作成功的结果返回给客户端。
+
+整个集群的日志模型可以宏观表示为下图(x ← 3 代表 x 赋值为 3):
+
+![img](assets/60d6b91f49ac4d11bee43f86f33566d6tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp)
+
+每条日志除了存储状态机的操作指令外,还会拥有一个**唯一的整数索引值**(**log index**)来表明它在日志集合中的位置。此外,每条日志还会存储一个 **term** 号(日志条目方块最上方的数字,相同颜色 term 号相同),该 term 表示 leader 收到这条指令时的当前任期,term 相同的 log 是由同一个 leader 在其任期内发送的。
+
+当一条日志被 leader 节点认为可以安全的 apply 到状态机时,称这条日志是 **committed**(上图中的 **committed entries**)。那么什么样的日志可以被 commit 呢?答案是:**当 leader 得知这条日志被集群过半的节点复制成功时**。因此在上图中我们可以看到 (term3, index7) 这条日志以及之前的日志都是 committed,尽管有两个节点拥有的日志并不完整。
+
+Raft 保证所有 committed 日志都已经被**持久化**,且“**最终**”一定会被状态机apply。
+
+注:这里的“最终”用词很微妙,它表明了一个特点:Raft 保证的只是集群内日志的一致性,而我们真正期望的集群对外的状态机一致性需要我们做一些额外工作,这一点在《线性一致性与读性能优化》一章会着重介绍。

+ 272 - 1
面经/问答/redis.md

@@ -28,7 +28,7 @@ Redis 是可以对 key 设置过期时间的,因此需要有相应的机制将
 
 - 如果一个 key 已经过期,而这个 key 又仍然保留在数据库中,那么只要这个过期 key 一直没有被访问,它所占用的内存就不会释放,造成了一定的内存空间浪费。所以,惰性删除策略对内存不友好。
 
-### 定期删除
+### 定期删除
 每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。
 
 定期删除策略的**优点**:
@@ -525,6 +525,168 @@ int main() {
 
 可以看得出,这是按照实际占用字节数进行分配内存的,这样可以节省内存空间。
 
+### ZSET(跳表)
+
+Redis 只有 Zset 对象的底层实现用到了跳表,跳表的优势是能支持平均 O(logN) 复杂度的节点查找。
+
+zset 结构体里有两个数据结构:一个是跳表,一个是哈希表。这样的好处是既能进行高效的范围查询,也能进行高效单点查询。
+
+```c
+typedef struct zset {
+    dict *dict;
+    zskiplist *zsl;
+} zset;
+```
+
+Zset 对象在执行数据插入或是数据更新的过程中,会依次在跳表和哈希表中插入或更新相应的数据,从而保证了跳表和哈希表中记录的信息一致。但是我们讨论的时候,都会说跳表是 Zset 对象的底层数据结构,而不会提及哈希表,是因为 struct zset 中的哈希表只是用于以常数复杂度获取元素权重,大部分操作都是跳表实现的。
+
+#### 跳表结构设计
+
+链表在查找元素的时候,因为需要逐一查找,所以查询效率非常低,时间复杂度是O(N),于是就出现了跳表。**跳表是在链表基础上改进过来的,实现了一种「多层」的有序链表**,这样的好处是能快读定位数据。
+
+![img](assets/2ae0ed790c7e7403f215acb2bd82e884.png)
+
+图中头节点有 L0~L2 三个头指针,分别指向了不同层级的节点,然后每个层级的节点都通过指针连接起来:
+
+- L0 层级共有 5 个节点,分别是节点1、2、3、4、5;
+- L1 层级共有 3 个节点,分别是节点 2、3、5;
+- L2 层级只有 1 个节点,也就是节点 3 。
+
+如果我们要在链表中查找节点 4 这个元素,只能从头开始遍历链表,需要查找 4 次,而使用了跳表后,只需要查找 2 次就能定位到节点 4,因为可以在头节点直接从 L2 层级跳到节点 3,然后再往前遍历找到节点 4。
+
+可以看到,这个查找过程就是在多个层级上跳来跳去,最后定位到元素。当数据量很大时,跳表的查找复杂度就是 O(logN)。
+
+那跳表节点是怎么实现多层级的呢?这就需要看「跳表节点」的数据结构了,如下:
+
+```c
+typedef struct zskiplistNode {
+    //Zset 对象的元素值
+    sds ele;
+    //元素权重值
+    double score;
+    //后向指针
+    struct zskiplistNode *backward;
+  
+    //节点的level数组,保存每层上的前向指针和跨度
+    struct zskiplistLevel {
+        struct zskiplistNode *forward;
+        unsigned long span;
+    } level[];
+} zskiplistNode;
+```
+
+Zset 对象要同时保存「元素」和「元素的权重」,对应到跳表节点结构里就是 sds 类型的 ele 变量和 double 类型的 score 变量。每个跳表节点都有一个后向指针(struct zskiplistNode *backward),指向前一个节点,目的是为了方便从跳表的尾节点开始访问节点,这样倒序查找时很方便。
+
+跳表是一个带有层级关系的链表,而且每一层级可以包含多个节点,每一个节点通过指针连接起来,实现这一特性就是靠跳表节点结构体中的**zskiplistLevel 结构体类型的 level 数组**。
+
+level 数组中的每一个元素代表跳表的一层,也就是由 zskiplistLevel 结构体表示,比如 leve[0] 就表示第一层,leve[1] 就表示第二层。zskiplistLevel 结构体里定义了「指向下一个跳表节点的指针」和「跨度」,跨度时用来记录两个节点之间的距离。
+
+比如,下面这张图,展示了各个节点的跨度。
+
+![img](assets/3层跳表-跨度.png)
+
+第一眼看到跨度的时候,以为是遍历操作有关,实际上并没有任何关系,遍历操作只需要用前向指针(struct zskiplistNode *forward)就可以完成了。
+
+**跨度实际上是为了计算这个节点在跳表中的排位**。具体怎么做的呢?因为跳表中的节点都是按序排列的,那么计算某个节点排位的时候,从头节点点到该结点的查询路径上,将沿途访问过的所有层的跨度累加起来,得到的结果就是目标节点在跳表中的排位。
+
+举个例子,查找图中节点 3 在跳表中的排位,从头节点开始查找节点 3,查找的过程只经过了一个层(L2),并且层的跨度是 3,所以节点 3 在跳表中的排位是 3。
+
+另外,图中的头节点其实也是 zskiplistNode 跳表节点,只不过头节点的后向指针、权重、元素值都没有用到,所以图中省略了这部分。
+
+问题来了,由谁定义哪个跳表节点是头节点呢?这就介绍「跳表」结构体了,如下所示:
+
+```c
+typedef struct zskiplist {
+    struct zskiplistNode *header, *tail;
+    unsigned long length;
+    int level;
+} zskiplist;
+```
+
+跳表结构里包含了:
+
+- 跳表的头尾节点,便于在O(1)时间复杂度内访问跳表的头节点和尾节点;
+- 跳表的长度,便于在O(1)时间复杂度获取跳表节点的数量;
+- 跳表的最大层数,便于在O(1)时间复杂度获取跳表中层高最大的那个节点的层数量;
+
+####  跳表节点查询过程
+
+查找一个跳表节点的过程时,跳表会从头节点的最高层开始,逐一遍历每一层。在遍历某一层的跳表节点时,会用跳表节点中的 SDS 类型的元素和元素的权重来进行判断,共有两个判断条件:
+
+- 如果当前节点的权重「小于」要查找的权重时,跳表就会访问该层上的下一个节点。
+- 如果当前节点的权重「等于」要查找的权重时,并且当前节点的 SDS 类型数据「小于」要查找的数据时,跳表就会访问该层上的下一个节点。
+
+如果上面两个条件都不满足,或者下一个节点为空时,跳表就会使用目前遍历到的节点的 level 数组里的下一层指针,然后沿着下一层指针继续查找,这就相当于跳到了下一层接着查找。
+
+举个例子,下图有个 3 层级的跳表。
+
+![img](assets/3层跳表-跨度.drawio.png)
+
+如果要查找「元素:abcd,权重:4」的节点,查找的过程是这样的:
+
+- 先从头节点的最高层开始,L2 指向了「元素:abc,权重:3」节点,这个节点的权重比要查找节点的小,所以要访问该层上的下一个节点;
+- 但是该层的下一个节点是空节点( leve[2]指向的是空节点),于是就会跳到「元素:abc,权重:3」节点的下一层去找,也就是 leve[1];
+- 「元素:abc,权重:3」节点的 leve[1] 的下一个指针指向了「元素:abcde,权重:4」的节点,然后将其和要查找的节点比较。虽然「元素:abcde,权重:4」的节点的权重和要查找的权重相同,但是当前节点的 SDS 类型数据「大于」要查找的数据,所以会继续跳到「元素:abc,权重:3」节点的下一层去找,也就是 leve[0];
+- 「元素:abc,权重:3」节点的 leve[0] 的下一个指针指向了「元素:abcd,权重:4」的节点,该节点正是要查找的节点,查询结束。
+
+#### 跳表节点层数设置
+
+跳表的相邻两层的节点数量的比例会影响跳表的查询性能。
+
+举个例子,下图的跳表,第二层的节点数量只有 1 个,而第一层的节点数量有 6 个。
+
+![img](assets/2802786ab4f52c1e248904e5cef33a74.png)
+
+这时,如果想要查询节点 6,那基本就跟链表的查询复杂度一样,就需要在第一层的节点中依次顺序查找,复杂度就是 O(N) 了。所以,为了降低查询复杂度,我们就需要维持相邻层结点数间的关系。
+
+**跳表的相邻两层的节点数量最理想的比例是 2:1,查找复杂度可以降低到 O(logN)**。
+
+下图的跳表就是,相邻两层的节点数量的比例是 2 : 1。
+
+![img](assets/cdc14698f629c74bf5a239cc8a611aeb.png)
+
+> **那怎样才能维持相邻两层的节点数量的比例为 2 : 1 呢?**
+
+如果采用新增节点或者删除节点时,来调整跳表节点以维持比例的方法的话,会带来额外的开销。
+
+Redis 则采用一种巧妙的方法是,**跳表在创建节点的时候,随机生成每个节点的层数**,并没有严格维持相邻两层的节点数量比例为 2 : 1 的情况。
+
+具体的做法是,**跳表在创建节点时候,会生成范围为[0-1]的一个随机数,如果这个随机数小于 0.25(相当于概率 25%),那么层数就增加 1 层,然后继续生成下一个随机数,直到随机数的结果大于 0.25 结束,最终确定该节点的层数**。
+
+这样的做法,相当于每增加一层的概率不超过 25%,层数越高,概率越低,层高最大限制是 64。
+
+虽然我前面讲解跳表的时候,图中的跳表的「头节点」都是 3 层高,但是其实**如果层高最大限制是 64,那么在创建跳表「头节点」的时候,就会直接创建 64 层高的头节点**。
+
+如下代码,创建跳表时,头节点的 level 数组有 ZSKIPLIST_MAXLEVEL个元素(层),节点不存储任何 member 和 score 值,level 数组元素的 forward 都指向NULL, span值都为0。
+
+```c
+/* Create a new skiplist. */
+zskiplist *zslCreate(void) {
+    int j;
+    zskiplist *zsl;
+
+    zsl = zmalloc(sizeof(*zsl));
+    zsl->level = 1;
+    zsl->length = 0;
+    zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);
+    for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {
+        zsl->header->level[j].forward = NULL;
+        zsl->header->level[j].span = 0;
+    }
+    zsl->header->backward = NULL;
+    zsl->tail = NULL;
+    return zsl;
+}
+```
+
+其中,ZSKIPLIST_MAXLEVEL 定义的是最高的层数,Redis 7.0 定义为 32,Redis 5.0 定义为 64,Redis 3.0 定义为 32。
+
+####  为什么用跳表而不用平衡树?
+
+- **从内存占用上来比较,跳表比平衡树更灵活一些**。平衡树每个节点包含 2 个指针(分别指向左右子树),而跳表每个节点包含的指针数目平均为 1/(1-p),具体取决于参数 p 的大小。如果像 Redis里的实现一样,取 p=1/4,那么平均每个节点包含 1.33 个指针,比平衡树更有优势。
+- **在做范围查找的时候,跳表比平衡树操作要简单**。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在跳表上进行范围查找就非常简单,只需要在找到小值之后,对第 1 层链表进行若干步的遍历就可以实现。
+- **从算法实现难度上来比较,跳表比平衡树要简单得多**。平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而跳表的插入和删除只需要修改相邻节点的指针,操作简单又快速。
+
 ## Redis主从复制
 
 ![图片](assets/22c7fe97ce5d3c382b08d83a4d8a5b96.png)
@@ -781,3 +943,112 @@ Redis 配置里有一个参数 min-slaves-max-lag,表示一旦所有的从节
 **等到新主节点上线时,就只有新主节点能接收和处理客户端请求,此时,新写的数据会被直接写到新主节点中。而原主节点会被哨兵降为从节点,即使它的数据被清空了,也不会有新数据丢失。我再来给你举个例子。**
 
 假设我们将 min-slaves-to-write 设置为 1,把 min-slaves-max-lag 设置为 12s,把哨兵的 down-after-milliseconds 设置为 10s,主节点因为某些原因卡住了 15s,导致哨兵判断主节点客观下线,开始进行主从切换。同时,因为原主节点卡住了 15s,没有一个从节点能和原主节点在 12s 内进行数据复制,原主节点也无法接收客户端请求了。这样一来,主从切换完成后,也只有新主节点能接收请求,不会发生脑裂,也就不会发生数据丢失的问题了
+
+## 为什么要有哨兵机制?
+
+### 为什么要有哨兵机制?
+
+在 Redis 的主从架构中,由于主从模式是读写分离的,如果主节点(master)挂了,那么将没有主节点来服务客户端的写操作请求,也没有主节点给从节点(slave)进行数据同步了。
+
+![主节点挂了](assets/db568766644a4d10b8a91cdd2f8a4070.png)
+
+这时如果要恢复服务的话,需要人工介入,选择一个「从节点」切换为「主节点」,然后让其他从节点指向新的主节点,同时还需要通知上游那些连接 Redis 主节点的客户端,将其配置中的主节点 IP 地址更新为「新主节点」的 IP 地址。
+
+这样也不太“智能”了,要是有一个节点能监控「主节点」的状态,当发现主节点挂了 ,它自动将一个「从节点」切换为「主节点」的话,那么可以节省我们很多事情啊!
+
+Redis 在 2.8 版本以后提供的**哨兵(\*Sentinel\*)机制**,它的作用是实现**主从节点故障转移**。它会监测主节点是否存活,如果发现主节点挂了,它就会选举一个从节点切换为主节点,并且把新主节点的相关信息通知给从节点和客户端。
+
+### 哨兵机制是如何工作的?
+
+哨兵其实是一个运行在特殊模式下的 Redis 进程,所以它也是一个节点。从“哨兵”这个名字也可以看得出来,它相当于是“观察者节点”,观察的对象是主从节点。
+
+当然,它不仅仅是观察那么简单,在它观察到有异常的状况下,会做出一些“动作”,来修复异常状态。
+
+哨兵节点主要负责三件事情:**监控、选主、通知**。
+
+![哨兵的职责](assets/775865f6bd894dfba8d373ee54d79af1.png)
+
+###  如何判断主节点真的故障了?
+
+哨兵会每隔 1 秒给所有主从节点发送 PING 命令,当主从节点收到 PING 命令后,会发送一个响应命令给哨兵,这样就可以判断它们是否在正常运行。
+
+![哨兵监控主从节点](assets/26f88373d8454682b9e0c1d4fd1611b4-20230309233114856.png)
+
+如果主节点或者从节点没有在规定的时间内响应哨兵的 PING 命令,哨兵就会将它们标记为「**主观下线**」。这个「规定的时间」是配置项 `down-after-milliseconds` 参数设定的,单位是毫秒。
+
+> **主观下线?难道还有客观下线?**
+
+是的没错,客观下线只适用于主节点。
+
+之所以针对「主节点」设计「主观下线」和「客观下线」两个状态,是因为有可能「主节点」其实并没有故障,可能只是因为主节点的系统压力比较大或者网络发送了拥塞,导致主节点没有在规定时间内响应哨兵的 PING 命令。
+
+所以,为了减少误判的情况,哨兵在部署的时候不会只部署一个节点,而是用多个节点部署成**哨兵集群**(*最少需要三台机器来部署哨兵集群*),**通过多个哨兵节点一起判断,就可以就可以避免单个哨兵因为自身网络状况不好,而误判主节点下线的情况**。同时,多个哨兵的网络同时不稳定的概率较小,由它们一起做决策,误判率也能降低。
+
+具体是怎么判定主节点为「客观下线」的呢?
+
+当一个哨兵判断主节点为「主观下线」后,就会向其他哨兵发起命令,其他哨兵收到这个命令后,就会根据自身和主节点的网络状况,做出赞成投票或者拒绝投票的响应。
+
+![img](assets/13e4361407ba46979e802eaa654dcf67.png)
+
+当这个哨兵的赞同票数达到哨兵配置文件中的 quorum 配置项设定的值后,这时主节点就会被该哨兵标记为「客观下线」。
+
+例如,现在有 3 个哨兵,quorum 配置的是 2,那么一个哨兵需要 2 张赞成票,就可以标记主节点为“客观下线”了。这 2 张赞成票包括哨兵自己的一张赞成票和另外两个哨兵的赞成票。
+
+PS:quorum 的值一般设置为哨兵个数的二分之一加1,例如 3 个哨兵就设置 2。
+
+哨兵判断完主节点客观下线后,哨兵就要开始在多个「从节点」中,选出一个从节点来做新主节点。
+
+## Redis热KEY问题
+
+### 引发问题
+
+- 占用大量的CPU资源,影响其他请求并导致整体性能降低。
+- 集群架构下,产生访问倾斜,即某个数据分片被大量访问,而其他数据分片处于空闲状态,可能引起该数据分片的连接数被耗尽,新的连接建立请求被拒绝等问题。
+- 在抢购或秒杀场景下,可能因商品对应库存Key的请求量过大,超出Redis处理能力造成超卖。
+- 热Key的请求压力数量超出Redis的承受能力易造成缓存击穿,即大量请求将被直接指向后端的存储层,导致存储访问量激增甚至宕机,从而影响其他业务。
+
+### 如何查找
+
+1. 根据业务经验,预估哪些是热key。
+
+   - 优点:简单直接。
+   - 缺点:但并不是所有业务都能预估出哪些key是热key。
+
+2. 在客户端收集。在操作redis之前,加上统计频次的逻辑,然后将统计数据发送给一个聚合计算的服务进行统计。
+
+   - 优点:方案简单。
+   - 缺点:无法支持大公司多语言环境的SDK,或者说多语言SDK对齐比较困难。此外SDK的维护升级成本会很高。
+
+3. 在proxy层收集。有些服务在请求redis之前会请求一个proxy服务,这种场景可以使用在proxy层收集热key数据,收集机制类似于在客户端收集。
+
+   - 优点:方案对使用方完全透明;没有SDK多语言异构和升级成本高的问题。
+   - 缺点:并不是所有场景都会有proxy层。
+
+4. redis集群监控。如果出现某个实例qps倾斜,说明可能存在热key。
+
+   - 优点:不需要额外开发。
+   - 缺点:每次发生状况需要人工排查,因为热key只是导致qps倾斜的一种可能。
+
+5. redis 4.0版本之后热点key发现功能。执行redis-cli时加上
+
+   ```shell
+   –-hotkeys
+   ```
+
+   选项即可。
+
+   - 优点:不需要额外开发。
+   - 缺点:该参数在执行的时候,如果key比较多,执行耗时会非常长,由此导致查询结果的实时性并不好。
+
+6. redis客户端使用TCP协议与服务端进行交互。通过脚本监听端口,解析网络包并进行分析。
+
+   - 优点:对原有的业务系统没有改造。
+   - 缺点:开发成本高,维护困难,有丢包可能性。
+
+### 如何解决
+
+1. 热key统计可以使用LFU数据结构并结合上面的发现方法,将最热topN的key进行统计,然后在client端使用本地缓存,从而降低redis集群对热key的访问量,但这种方法带来两个问题:
+   1. 如果对所有热key进行本地缓存,那么本地缓存是否会过大,从而影响应用程序本身的性能开销。
+   2. 可能需要保证本地缓存和redis数据的一致性。
+2. 将热key加上前缀或者后缀,把热key的数量从1个变成实例个数,利用分片特性将这n个key分散在不同节点上,这样就可以在访问的时候,采用客户端[负载均衡](https://cloud.tencent.com/product/clb?from_column=20065&from=20065)的方式,随机选择一个key进行访问,将访问压力分散到不同的实例中。这个方案有个明显的缺点,就是缓存的维护成本大:假如有n为100,则更新或者删除key的时候需要操作100个key。
+3. 利用读写分离,通过主从复制的方式,增加slave节点来实现读请求的负载均衡。这个方案明显的缺点就是使用机器硬抗热key的数据,资源耗费严重;而且引入读写分离架构,增加节点数量,都会增加系统的复杂度降低稳定性。

+ 441 - 0
面经/问答/vue.md

@@ -0,0 +1,441 @@
+## VUE原理
+
+### 响应式原理
+
+**检测data变化的核心API`Object.defindeProperty`**
+
+基本使用
+
+```javascript
+const data = {};
+let name = "张三";
+
+Object.defineProperty(data,'name',{
+    get:function(){
+        console.log('触发get')
+        return name
+    },
+    set:function(newVal){
+        console.log('触发set')
+        name=newVal
+    }
+})
+
+//测试
+console.log(data.name)   // 触发get  张三
+data.name = '李四'         // 触发set
+```
+
+### 虚拟DOOM -- diff算法
+
+1. 通过树进行比较
+
+`diff` 算法用来比较两棵 `Virtual DOM` 树的差异,如果需要两棵树的完全比较,那么 `diff` 算法的时间复杂度为`O(n^3)`。但是在前端当中,你很少会跨越层级地移动 `DOM` 元素,所以 `Virtual DOM` 只会对同一个层级的元素进行对比,如下图所示, `div` 只会和同一层级的 `div` 对比,第二层级的只会跟第二层级对比,这样算法复杂度就可以达到 `O(n)`。
+
+![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/7/23/16c1e26a5ecf086e~tplv-t2oaga2asx-jj-mark:3024:0:0:0:q75.awebp)
+
+2. 通过列表进行比较
+
+​	子节点的对比算法,例如      `p, ul, div` 的顺序换成了 `div, p, ul`。这个该怎么对比?如果按照同层级进行顺序对比的话,它们都会被替换掉。如 `p` 和 `div` 的 `tagName` 不同,`p` 会被 `div` 所替代。最终,三个节点都会被替换,这样 `DOM` 开销就非常大。而实际上是不需要替换节点,而只需要经过节点移动就可以达到,我们只需知道怎么进行移动。
+
+​	将这个问题抽象出来其实就是字符串的最小编辑距离问题(`Edition Distance`),最常见的解决方法是 `Levenshtein Distance` , `Levenshtein Distance` 是一个度量两个字符序列间差异的字符串度量标准,两个单词之间的 `Levenshtein Distance` 是将一个单词转换为另一个单词所需的单字符编辑(插入、删除或替换)的最小数量。`Levenshtein Distance` 是1965年由苏联数学家 Vladimir Levenshtein 发明的。`Levenshtein Distance` 也被称为编辑距离(`Edit Distance`),通过**动态规划**求解,时间复杂度为 `O(M*N)`。
+
+### vue源码
+
+vue的virtual dom借鉴了开源库snabbdom,其实主要属性
+
+* `tag` 属性即这个`vnode`的标签属性
+* `data` 属性包含了最后渲染成真实`dom`节点后,节点上的`class`,`attribute`,`style`以及绑定的事件
+* `children` 属性是`vnode`的子节点
+* `text` 属性是文本属性
+* `elm` 属性为这个`vnode`对应的真实`dom`节点
+* `key` 属性是`vnode`的标记,在`diff`过程中可以提高`diff`的效率
+
+### 原理解析
+
+虚拟dom原理流程
+
+> **模板 ==> 渲染函数 ==> 虚拟DOM树 ==> 真实DOM**
+
+* vuejs通过编译将模板(template)转成渲染函数(render),执行渲染函数可以得到一个虚拟节点树
+
+* 在对 Model 进行操作的时候,会触发对应 Dep 中的 Watcher 对象。Watcher 对象会调用对应的 update 来修改视图。
+
+虚拟 DOM 的实现原理主要包括以下 3 部分:
+
+* 用 JavaScript 对象模拟真实 DOM 树,对真实 DOM 进行抽象;
+* diff 算法 — 比较两棵虚拟 DOM 树的差异;
+* pach 算法 — 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树。
+
+### 构建过程
+
+1. 初始化vue
+
+```javascript
+function Vue (options) {
+  if (process.env.NODE_ENV !== 'production' &&
+    !(this instanceof Vue)
+  ) {
+    warn('Vue is a constructor and should be called with the `new` keyword')
+  }
+  this._init(options)
+}
+// 通过查看 Vue 的 function,我们知道 Vue 只能通过 new 关键字初始化,然后调用 this._init 方法,该方法在 src/core/instance/init.js 中定义。
+
+Vue.prototype._init = function (options?: Object) {
+    const vm: Component = this
+
+    // 省略一系列其它初始化的代码
+
+    if (vm.$options.el) {
+        console.log('vm.$options.el:',vm.$options.el);
+        vm.$mount(vm.$options.el)
+    }
+}
+```
+
+2. `Vue` 实例挂载
+
+```javascript
+const mount = Vue.prototype.$mount
+Vue.prototype.$mount = function (
+  el?: string | Element,
+  hydrating?: boolean
+): Component {
+  el = el && query(el)
+  
+   // 省略一系列初始化以及逻辑判断代码  
+ 
+  return mount.call(this, el, hydrating)
+}
+// 我们发现最终还是调用用原先原型上的 $mount 方法挂载 ,原先原型上的 $mount 方法在 src/platforms/web/runtime/index.js 中定义 。
+Vue.prototype.$mount = function (
+  el?: string | Element,
+  hydrating?: boolean
+): Component {
+  el = el && inBrowser ? query(el) : undefined
+  return mountComponent(this, el, hydrating)
+}
+// 我们发现$mount 方法实际上会去调用 mountComponent 方法,这个方法定义在 src/core/instance/lifecycle.js 文件中
+export function mountComponent (
+  vm: Component,
+  el: ?Element,
+  hydrating?: boolean
+): Component {
+  vm.$el = el
+  // 省略一系列其它代码
+  let updateComponent
+  /* istanbul ignore if */
+  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
+    updateComponent = () => {
+      // 生成虚拟 vnode   
+      const vnode = vm._render()
+      // 更新 DOM
+      vm._update(vnode, hydrating)
+     
+    }
+  } else {
+    updateComponent = () => {
+      vm._update(vm._render(), hydrating)
+    }
+  }
+
+  // 实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法  
+  new Watcher(vm, updateComponent, noop, {
+    before () {
+      if (vm._isMounted && !vm._isDestroyed) {
+        callHook(vm, 'beforeUpdate')
+      }
+    }
+  }, true /* isRenderWatcher */)
+  hydrating = false
+
+  return vm
+}
+```
+
+3. 创建虚拟node
+
+```javascript
+// Vue 的 _render 方法是实例的一个私有方法,它用来把实例渲染成一个虚拟 Node。它的定义在 src/core/instance/render.js 文件中:
+Vue.prototype._render = function (): VNode {
+    const vm: Component = this
+    const { render, _parentVnode } = vm.$options
+    let vnode
+    try {
+      // 省略一系列代码  
+      currentRenderingInstance = vm
+      // 调用 createElement 方法来返回 vnode
+      vnode = render.call(vm._renderProxy, vm.$createElement)
+    } catch (e) {
+      handleError(e, vm, `render`){}
+    }
+    // set parent
+    vnode.parent = _parentVnode
+    console.log("vnode...:",vnode);
+    return vnode
+  }
+// Vue.js 利用 _createElement 方法创建 VNode,它定义在 src/core/vdom/create-elemenet.js 中:
+export function _createElement (
+  context: Component,
+  tag?: string | Class<Component> | Function | Object,
+  data?: VNodeData,
+  children?: any,
+  normalizationType?: number
+): VNode | Array<VNode> {
+    
+  // 省略一系列非主线代码
+  
+  if (normalizationType === ALWAYS_NORMALIZE) {
+    // 场景是 render 函数不是编译生成的
+    children = normalizeChildren(children)
+  } else if (normalizationType === SIMPLE_NORMALIZE) {
+    // 场景是 render 函数是编译生成的
+    children = simpleNormalizeChildren(children)
+  }
+  let vnode, ns
+  if (typeof tag === 'string') {
+    let Ctor
+    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
+    if (config.isReservedTag(tag)) {
+      // 创建虚拟 vnode
+      vnode = new VNode(
+        config.parsePlatformTagName(tag), data, children,
+        undefined, undefined, context
+      )
+    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
+      // component
+      vnode = createComponent(Ctor, data, context, children, tag)
+    } else {
+      vnode = new VNode(
+        tag, data, children,
+        undefined, undefined, context
+      )
+    }
+  } else {
+    vnode = createComponent(tag, data, context, children)
+  }
+  if (Array.isArray(vnode)) {
+    return vnode
+  } else if (isDef(vnode)) {
+    if (isDef(ns)) applyNS(vnode, ns)
+    if (isDef(data)) registerDeepBindings(data)
+    return vnode
+  } else {
+    return createEmptyVNode()
+  }
+}
+// _createElement 方法有 5 个参数,context 表示 VNode 的上下文环境,它是 Component 类型;tag表示标签,它可以是一个字符串,也可以是一个 Component;data 表示 VNode 的数据,它是一个 VNodeData 类型,可以在 flow/vnode.js 中找到它的定义;children 表示当前 VNode 的子节点,它是任意类型的,需要被规范为标准的 VNode 数组;
+```
+
+### diff过程
+
+```javascript
+// Vue.js 源码实例化了一个 watcher,这个 ~ 被添加到了在模板当中所绑定变量的依赖当中,一旦 model 中的响应式的数据发生了变化,这些响应式的数据所维护的 dep 数组便会调用 dep.notify() 方法完成所有依赖遍历执行的工作,这包括视图的更新,即 updateComponent 方法的调用。watcher 和 updateComponent方法定义在  src/core/instance/lifecycle.js 文件中 。
+export function mountComponent (
+  vm: Component,
+  el: ?Element,
+  hydrating?: boolean
+): Component {
+  vm.$el = el
+  // 省略一系列其它代码
+  let updateComponent
+  /* istanbul ignore if */
+  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
+    updateComponent = () => {
+      // 生成虚拟 vnode   
+      const vnode = vm._render()
+      // 更新 DOM
+      vm._update(vnode, hydrating)
+     
+    }
+  } else {
+    updateComponent = () => {
+      vm._update(vm._render(), hydrating)
+    }
+  }
+
+  // 实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法  
+  new Watcher(vm, updateComponent, noop, {
+    before () {
+      if (vm._isMounted && !vm._isDestroyed) {
+        callHook(vm, 'beforeUpdate')
+      }
+    }
+  }, true /* isRenderWatcher */)
+  hydrating = false
+
+  return vm
+}
+// 完成视图的更新工作事实上就是调用了vm._update方法,这个方法接收的第一个参数是刚生成的Vnode,调用的vm._update方法定义在 src/core/instance/lifecycle.js中。
+  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
+    const vm: Component = this
+    const prevEl = vm.$el
+    const prevVnode = vm._vnode
+    const restoreActiveInstance = setActiveInstance(vm)
+    vm._vnode = vnode
+    if (!prevVnode) {
+      // 第一个参数为真实的node节点,则为初始化
+      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
+    } else {
+      // 如果需要diff的prevVnode存在,那么对prevVnode和vnode进行diff
+      vm.$el = vm.__patch__(prevVnode, vnode)
+    }
+    restoreActiveInstance()
+    // update __vue__ reference
+    if (prevEl) {
+      prevEl.__vue__ = null
+    }
+    if (vm.$el) {
+      vm.$el.__vue__ = vm
+    }
+    // if parent is an HOC, update its $el as well
+    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
+      vm.$parent.$el = vm.$el
+    }
+  }
+// 在这个方法当中最为关键的就是 vm.__patch__ 方法,这也是整个 virtual-dom 当中最为核心的方法,主要完成了prevVnode 和 vnode 的 diff 过程并根据需要操作的 vdom 节点打 patch,最后生成新的真实 dom 节点并完成视图的更新工作。接下来,让我们看下 vm.__patch__的逻辑过程, vm.__patch__ 方法定义在 src/core/vdom/patch.js 中。
+function patch (oldVnode, vnode, hydrating, removeOnly) {
+    ......
+    if (isUndef(oldVnode)) {
+      // 当oldVnode不存在时,创建新的节点
+      isInitialPatch = true
+      createElm(vnode, insertedVnodeQueue)
+    } else {
+      // 对oldVnode和vnode进行diff,并对oldVnode打patch  
+      const isRealElement = isDef(oldVnode.nodeType)
+      if (!isRealElement && sameVnode(oldVnode, vnode)) {
+        // patch existing root node
+        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
+      } 
+	......
+  }
+}
+// 在 patch 方法中,我们看到会分为两种情况,一种是当 oldVnode 不存在时,会创建新的节点;另一种则是已经存在 oldVnode ,那么会对 oldVnode 和 vnode 进行 diff 及 patch 的过程。其中 patch 过程中会调用 sameVnode 方法来对对传入的2个 vnode 进行基本属性的比较,只有当基本属性相同的情况下才认为这个2个vnode 只是局部发生了更新,然后才会对这2个 vnode 进行 diff,如果2个 vnode 的基本属性存在不一致的情况,那么就会直接跳过 diff 的过程,进而依据 vnode 新建一个真实的 dom,同时删除老的 dom节点。
+
+function sameVnode (a, b) {
+  return (
+    a.key === b.key &&
+    a.tag === b.tag &&
+    a.isComment === b.isComment &&
+    isDef(a.data) === isDef(b.data) &&
+    sameInputType(a, b)
+  )
+}
+// diff 过程中主要是通过调用 patchVnode 方法进行的:
+      function patchVnode (oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
+    ...... 
+    const elm = vnode.elm = oldVnode.elm
+    const oldCh = oldVnode.children
+    const ch = vnode.children
+    // 如果vnode没有文本节点
+    if (isUndef(vnode.text)) {
+      // 如果oldVnode的children属性存在且vnode的children属性也存在  
+      if (isDef(oldCh) && isDef(ch)) {
+        // updateChildren,对子节点进行diff  
+        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
+      } else if (isDef(ch)) {
+        if (process.env.NODE_ENV !== 'production') {
+          checkDuplicateKeys(ch)
+        }
+        // 如果oldVnode的text存在,那么首先清空text的内容,然后将vnode的children添加进去  
+        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
+        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
+      } else if (isDef(oldCh)) {
+        // 删除elm下的oldchildren
+        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
+      } else if (isDef(oldVnode.text)) {
+        // oldVnode有子节点,而vnode没有,那么就清空这个节点  
+        nodeOps.setTextContent(elm, '')
+      }
+    } else if (oldVnode.text !== vnode.text) {
+      // 如果oldVnode和vnode文本属性不同,那么直接更新真是dom节点的文本元素
+      nodeOps.setTextContent(elm, vnode.text)
+    }
+    ......
+  }
+
+
+```
+
+从以上代码得知,
+
+`diff` 过程中又分了好几种情况,`oldCh` 为 `oldVnode`的子节点,`ch` 为 `Vnode`的子节点:
+
+- 首先进行文本节点的判断,若 `oldVnode.text !== vnode.text`,那么就会直接进行文本节点的替换;
+- 在`vnode`  没有文本节点的情况下,进入子节点的 `diff`;
+- 当 `oldCh` 和 `ch` 都存在且不相同的情况下,调用 `updateChildren` 对子节点进行 `diff`;
+- 若 `oldCh`不存在,`ch` 存在,首先清空 `oldVnode` 的文本节点,同时调用 `addVnodes` 方法将 `ch` 添加到`elm`真实 `dom` 节点当中;
+- 若 `oldCh`存在,`ch`不存在,则删除 `elm` 真实节点下的 `oldCh` 子节点;
+- 若 `oldVnode` 有文本节点,而 `vnode` 没有,那么就清空这个文本节点。
+
+### diff过程  --- 无key
+
+1. 首先从第一个节点开始比较,不管是 `oldCh` 还是 `newCh` 的起始或者终止节点都不存在 `sameVnode` ,同时节点属性中是不带 `key`标记的,因此第一轮的 `diff` 完后,`newCh`的 `startVnode` 被添加到 `oldStartVnode`的前面,同时 `newStartIndex`前移一位;
+
+![图片描述](assets/16c1e0e2878c44dctplv-t2oaga2asx-jj-mark3024000q75.awebp)
+
+2. 第二轮的 `diff`中,满足 `sameVnode(oldStartVnode, newStartVnode)`,因此对这2个 `vnode` 进行`diff`,最后将 `patch` 打到 `oldStartVnode` 上,同时 `oldStartVnode`和 `newStartIndex` 都向前移动一位
+
+![图片描述](assets/16c1e0e28889eafftplv-t2oaga2asx-jj-mark3024000q75.awebp)
+
+3. 第三轮的 `diff` 中,满足 `sameVnode(oldEndVnode, newStartVnode)`,那么首先对  `oldEndVnode`和`newStartVnode` 进行 `diff`,并对 `oldEndVnode`进行 `patch`,并完成  `oldEndVnode` 移位的操作,最后`newStartIndex`前移一位,`oldStartVnode` 后移一位;
+
+![图片描述](assets/16c1e0e289a351b2tplv-t2oaga2asx-jj-mark3024000q75.awebp)
+
+4. 第四轮的 `diff`中,过程同步骤3;
+
+![图片描述](assets/16c1e0e289f9213etplv-t2oaga2asx-jj-mark3024000q75.awebp)
+
+5. 第五轮的 `diff` 中,同过程1;
+
+![图片描述](assets/16c1e0e28aee99a1tplv-t2oaga2asx-jj-mark3024000q75.awebp)
+
+6. 遍历的过程结束后,`newStartIdx > newEndIdx`,说明此时 `oldCh` 存在多余的节点,那么最后就需要将这些多余的节点删除。
+
+![图片描述](assets/16c1e0e2ca893b49tplv-t2oaga2asx-jj-mark3024000q75.awebp)
+
+### diff过程 -- 有key
+
+1. 首先从第一个节点开始比较,不管是 `oldCh` 还是 `newCh` 的起始或者终止节点都不存在 `sameVnode`,但节点属性中是带 `key` 标记的, 然后在 `oldKeyToIndx` 中找到对应的节点,这样第一轮 `diff` 过后 `oldCh` 上的`B节点`被删除了,但是 `newCh` 上的`B节点`上 `elm` 属性保持对 `oldCh` 上 `B节点` 的`elm`引用。
+
+![图片描述](assets/16c1e0e2db1c4812tplv-t2oaga2asx-jj-mark3024000q75-16934721993069.awebp)
+
+2. 第二轮的 `diff` 中,满足 `sameVnode(oldStartVnode, newStartVnode)`,因此对这2个 `vnode` 进行`diff`,最后将 `patch` 打到 `oldStartVnode`上,同时 `oldStartVnode` 和 `newStartIndex` 都向前移动一位
+
+![图片描述](assets/16c1e0e2d7df4fbftplv-t2oaga2asx-jj-mark3024000q75.awebp)
+
+3. 第三轮的 `diff`中,满足 `sameVnode(oldEndVnode, newStartVnode)`,那么首先对 `oldEndVnode` 和`newStartVnode` 进行 `diff`,并对 `oldEndVnode` 进行 `patch`,并完成 `oldEndVnode` 移位的操作,最后`newStartIndex` 前移一位,`oldStartVnode`后移一位;
+
+![图片描述](assets/16c1e0e2e2a2835etplv-t2oaga2asx-jj-mark3024000q75.awebp)
+
+4. 第四轮的`diff`中,过程同步骤2;
+
+![图片描述](assets/16c1e0e2e507aec0tplv-t2oaga2asx-jj-mark3024000q75.awebp)
+
+5. 第五轮的`diff`中,因为此时 `oldStartIndex` 已经大于 `oldEndIndex`,所以将剩余的 `Vnode` 队列插入队列最后。
+
+![图片描述](assets/16c1e0e3178398fctplv-t2oaga2asx-jj-mark3024000q75.awebp)
+
+## vue生命周期
+
+![在这里插入图片描述](assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3N1MjIzMTU5NTc0Mg==,size_16,color_FFFFFF,t_70-169347279895916.png)
+
+生命周期: vue实例从创建到销毁的过程。
+
+声明周期钩子: 就是生命周期事件的别名而已
+
+主要的生命周期函数分类:
+
+* 创建期间的生命周期函数:
+  * beforeCreate:实例刚在内存中被创建出来,此时,还没有初始化好data 和 methods 属性
+  * created:实例已经完成了模板的编译,但是还没有挂载到页面中
+  * beforeMount:此时已经完成了模板的翻译,但是还有完全挂载到页面中
+  * mounted:此时,已经将编译好的模板,挂载到了页面指定的容器中显示
+* 运行期间的生命周期函数:
+  * beforeUpdate:状态更新之前执行此函数,此时 data 中的状态值是最新的,但是界面上显示的数据还是旧的,因为此时还没有开始重新渲染DOM节点
+  * updated:实例更新完毕之后调用次函数,此时 data 中的状态值 和 界面上显示的数据,都已经完成了更新,界面已经被重新渲染好了
+* 销毁期间的生命周期函数:
+  * beforeDestroy:实例销毁之前调用,在这一步,实例仍然完全可用
+    当执行 beforeDestroy 钩子函数的时候,Vue实例就已经从运行阶段进入到了销毁阶段;当执行 beforeDestroy 的时候,实例身上所有的 data 和所有的 methods, 以及 过滤器、指令、、 都处于可用状态,此时,还没有真正执行销毁的过程
+  * destroyed:Vue 实例销毁后调用。调用后,vue 实例 指示的所有东西都会解绑,所有的事件监听器会被移除,所有的子实例也会被销毁

+ 60 - 1
面经/问答/基础.md

@@ -116,9 +116,68 @@ protected void finalize() throws Throwable { }
 ## 设计模式的原则
 
 1. 单一职责原则(Single Responsibility Principle,SRP):一个类应该只有一个引起它变化的原因。一个类应该只负责完成单一的职责或功能,这样可以提高代码的可读性和可维护性。
-2. 开放封闭原则(Open Closed Principle,OCP):软件实体(类、模块等)应该对扩展开放,对修改关闭。通过使用抽象和接口,可以使得系统容易进行扩展,而不需要修改已有的代码。
+2. 开闭原则(Open Closed Principle,OCP):软件实体(类、模块等)应该对扩展开放,对修改关闭。通过使用抽象和接口,可以使得系统容易进行扩展,而不需要修改已有的代码。
 3. 里氏替换原则(Liskov Substitution Principle,LSP):子类应该能够替换其父类并且保持程序的逻辑正确性。子类应该遵守父类定义的接口和规范。
 4. 依赖倒置原则(Dependency Inversion Principle,DIP):高层模块不应该直接依赖于低层模块,二者都应该依赖于抽象。抽象不应该依赖于具体实现细节,而是应该依赖于抽象。
 5. 接口隔离原则(Interface Segregation Principle,ISP):客户端不应该依赖于它不需要的接口。一个类或模块应该只有与其相关的接口,而不应该依赖于不需要的接口。
 6. 迪米特法则(Law of Demeter,LoD):一个对象应该对其他对象有尽可能少的了解。减少对象之间的直接依赖关系可以提高代码的可维护性和灵活性。
 7. 组合/聚合复用原则(Composition/Aggregation Reuse Principle,CARP):优先使用组合或聚合关系,而不是继承关系,来实现代码复用。组合和聚合关系更加灵活,避免了类之间的紧耦合。
+
+这些设计模式通常是为了遵循面向对象设计中的一些基本原则而产生的,虽然并非每个模式都严格对应一个特定的原则,但它们可以相互支持以实现更好的软件设计。以下是一些设计模式与相应原则之间的关系示例:
+
+1. **工厂模式 (Factory Pattern)**: 原则:依赖倒置原则 (Dependency Inversion Principle, DIP)。通过工厂模式,高层模块可以依赖于抽象的工厂接口,而不依赖于具体产品的创建细节。
+2. **单例模式 (Singleton Pattern)**: 原则:无特定对应。单例模式旨在确保一个类只有一个实例,以实现全局访问点和避免多次实例化。
+3. **策略模式 (Strategy Pattern)**: 原则:开闭原则 (Open/Closed Principle, OCP)。通过策略模式,可以在不修改客户端代码的情况下添加、修改或替换算法策略。
+4. **观察者模式 (Observer Pattern)**: 原则:开闭原则 (Open/Closed Principle, OCP)、依赖倒置原则 (Dependency Inversion Principle, DIP)。观察者模式允许主题与观察者之间保持松散的耦合,新的观察者可以随时添加,不会影响主题的代码。
+5. **适配器模式 (Adapter Pattern)**: 原则:里氏替换原则 (Liskov Substitution Principle, LSP)。适配器模式将一个类的接口适配为另一个类的接口,确保适配后的类能够替代原始类。
+6. **装饰器模式 (Decorator Pattern)**: 原则:开闭原则 (Open/Closed Principle, OCP)。通过装饰器模式,可以在不修改现有代码的情况下添加新的功能,扩展对象的行为。
+7. **代理模式 (Proxy Pattern)**: 原则:代理模式可以与多个原则关联,如单一职责原则 (Single Responsibility Principle, SRP)(代理负责控制访问)、开闭原则 (Open/Closed Principle, OCP)(通过代理可以添加额外行为而不修改原始类)等。
+8. **模板方法模式 (Template Method Pattern)**: 原则:无特定对应。模板方法模式定义了算法的框架,允许子类重写算法的特定步骤,但保持了整体的结构。
+
+## OOM如何排查
+
+### 为什么会发生OOM
+
+OOM 全称 “Out Of Memory”,表示内存耗尽。当 JVM 因为没有足够的内存来为对象分配空间,并且垃圾回收器也已经没有空间可回收时,就会抛出这个错误
+
+为什么会出现 OOM,一般由这些问题引起
+
+1. 分配过少:JVM 初始化内存小,业务使用了大量内存;或者不同 JVM 区域分配内存不合理
+2. 代码漏洞:某一个对象被频繁申请,不用了之后却没有被释放,导致内存耗尽
+
+**内存泄漏**:申请使用完的内存没有释放,导致虚拟机不能再次使用该内存,此时这段内存就泄露了。因为申请者不用了,而又不能被虚拟机分配给别人用
+
+**内存溢出**:申请的内存超出了 JVM 能提供的内存大小,此时称之为溢出
+
+内存泄漏持续存在,最后一定会溢出,两者是因果关系
+
+### 排查方法
+
+#### 命令方式
+
+1. 输入命令查看 JVM 内存分布 `jmap -heap PID`
+2. `jmap -histo:live PID| more`,JVM 内存对象列表按照对象所占内存大小排序
+
+#### Dump 文件分析
+
+Dump 文件是 Java 进程的内存镜像,其中主要包括 **系统信息**、**虚拟机属性**、**完整的线程 Dump**、**所有类和对象的状态** 等信息
+
+当程序发生内存溢出或 GC 异常情况时,怀疑 JVM 发生了 **内存泄漏**,这时我们就可以导出 Dump 文件分析
+
+JVM 启动参数配置添加以下参数
+
+- -XX:+HeapDumpOnOutOfMemoryError
+- -XX:HeapDumpPath=./(参数为 Dump 文件生成路径)
+
+> **当 JVM 发生 OOM 异常自动导出 Dump 文件,文件名称默认格式:`java_pid{pid}.hprof`**
+
+上面配置是在应用抛出 OOM 后自动导出 Dump,或者可以在 JVM 运行时导出 Dump 文件
+
+```shell
+jmap -dump:file=[文件路径] [pid]
+ 
+# 示例
+jmap -dump:file=./jvmdump.hprof 15162
+```
+
+使用**visualVM**分析

+ 0 - 2
面经/问答/并发编程.md

@@ -2,8 +2,6 @@
 
 在Java中,`wait()`和`notify()`是`Object`类的两个方法,它们用于实现线程间的协作。`wait()`使一个线程进入等待状态,直到另一个线程发出通知唤醒它。而notify()则用于唤醒正在等待的线程。
 
-
-
 `wait()`和`notify()`必须放在`synchronized`块中是因为这些方法依赖于对象的监视器锁(也称为互斥锁)。只有获得了对象的监视器锁(该锁即为调用`wait`方法的对象)的线程才能调用`wait()`和`notify()`方法。如果这些方法不在同步块中使用,就无法保证线程安全性。
 
 为了解决「lost wake up 问题」

+ 83 - 1
面经/问答/情景设计题.md

@@ -252,4 +252,86 @@ QL本身的性能已经到达极限了,但是耗时仍然很长,可能由于
 ## 如何设计B站?
 
 * 用户推荐方向:可以从视频分类,然后按照用户标签去分析。
-* 缓存,可以用本地缓存,类似map/reduce批量去更新
+* 缓存,可以用本地缓存,类似map/reduce批量去更新
+
+## 什么是负载均衡
+
+* 第 1 层:客户端层 -> 反向代理层 的负载均衡。通过 DNS 轮询
+
+* 第 2 层:反向代理层 -> Web 层 的负载均衡。通过 Nginx 的负载均衡模块
+
+* 第 3 层:Web 层 -> 业务服务层 的负载均衡。通过服务治理框架的负载均衡模块
+
+* 第 4 层:业务服务层 -> 数据存储层 的负载均衡。通过数据的水平分布,数据均匀了,理论上请求也会均匀。比如通过买家ID分片类似
+
+## 限流算法介绍
+
+限制到达系统的并发请求数,使得系统能够正常的处理 部分 用户的请求,来保证系统的稳定性。
+
+### 计数器法
+
+最简单粗暴的一种算法,比如说一个接口一分钟内的请求不能超过60次
+
+1. 划分时间窗口
+2. 窗口内请求,计数器加一
+3. 超过限制,本窗口内拒绝请求,等到下个窗口,计数器重置,再执行请求
+
+应用场景:短信限流,比如一个用户一分钟内一条短信,可以使用计数器限流
+
+问题:临界问题,两个时间窗口交界处,可能会执行任务,近似于通知执行两倍量的任务
+
+![img](assets/788acb3c73174c42b603fe67e96c89f5tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp)
+
+### 滑动窗口算法
+
+在固定窗口划分多个小窗口,分别在小时间窗口里记录访问次数,随着时间推动窗口滑动并删除过期窗口,即去除之前的访问次数,最终统计所有小窗口的可访问总数。
+
+demo:将1min分为4个小窗口,每个小窗口能处理25个请求。通过虚线表示窗口大小(当前窗口大小为2,所以能处理50个请求)。同时窗口可以随时间滑动,比如15s后,窗口滑动到(15s-45s)这个范围内,然后在新的窗口里进行统计
+
+![img](assets/559260f5ef944cd1a146d07308275b89tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp)
+
+当滑动窗口的小时间窗口划分越多,那么滑动窗口的滑动就越平滑,限流的统计就更精确。
+
+### 漏桶算法
+
+![img](assets/6a68be69f6b64ad1a8df234d9f0772detplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp)
+
+内部维护一个容器,以恒定的速度出水,不管上游的速度多快。请求到达时直接放入到漏桶中,如果容量达到上限(阈值),则进行丢弃(执行限流策略)。
+
+漏桶算法无法短时间处理突发流量,是一种恒定的限流算法。
+
+### 令牌算法(rateLimter)
+
+![img](assets/c7787bc99996429381a1049580f6c24atplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp)
+
+令牌桶是网络流量整形(Traffic Shaping)和速度限制(Rate Limting)中最常使用的算法,对于每个请求,都需要从令牌桶中获得一个令牌,如果没有令牌则触发限流策略。
+
+系统以一个恒定的速度往固定容量的容器放入令牌,如果有请求,则需要从令牌桶中取出令牌才能放行。
+
+由于令牌桶有固定大小,当请求速度<生成令牌速度,令牌桶会被填满,因此令牌桶可以处理突发流量。
+
+## linux的tail
+
+1. **命令行解析**: 使用 Python 的 `argparse` 模块来解析命令行参数,包括文件路径、行数、是否实时监视等选项。
+2. **打开文件**: 使用 Python 的 `open` 函数打开用户指定的文件。我会使用 `with` 语句来确保在使用完文件后正确关闭它。
+3. **定位到末尾**: 使用 `file.seek()` 方法将文件指针定位到文件末尾。这样,我就可以从末尾开始逐行读取。
+4. **逐行读取和输出**: 使用循环,我会从文件末尾逐行读取内容,并将这些内容打印到终端。我会维护一个计数器,确保只输出指定数量的行。
+5. **实时监视**: 如果需要支持实时监视,我会使用一个循环来不断检查文件是否有新内容添加。我可以使用 `time.sleep()` 等方法来实现定期检查,并在发现新行时输出到终端。
+6. **错误处理**: 我会考虑处理可能出现的错误,例如文件不存在、无法打开文件等情况。在出现错误时,我会向用户提供友好的错误消息。
+7. **边界情况**: 我会确保处理一些边界情况,例如用户提供的行数大于文件总行数的情况
+
+如果是大文件直接读取inode文件内容
+
+**定位到末尾的块**: 文件的 inode 包含了文件数据块的指针。你可以首先通过 inode 获取文件的大小,然后确定需要读取的块数量。这可以帮助你跳过文件的开头部分。
+
+## 负载均衡策略
+
+* 轮询
+* 加权轮询
+* 哈希算法
+  * IP哈希
+  * URL哈希
+  * 一致性哈希
+* 最小响应时间
+* 随机
+* 粘性

+ 14 - 0
面经/问答/操作系统.md

@@ -576,3 +576,17 @@ epoll 支持两种事件触发模式,分别是**边缘触发(\*edge-triggere
 而针 C-SCAN 算法的优化则叫 C-LOOK,它的工作方式,磁头在每个方向上仅仅移动到最远的请求位置,然后立即反向移动,而不需要移动到磁盘的最始端或最末端,**反向移动的途中不会响应请求**。
 
 ![C-LOOK 算法](assets/磁盘调度-C-LOOK算法.png)
+
+## 进程调度算法,面向用户和纯后台会有不一样嘛?
+
+**面向用户任务**:一般是多级反馈队列
+
+- 这些任务通常是由用户发起的,如图形界面应用程序、交互式应用等。
+- 用户期望快速响应,因此系统调度算法可能倾向于为这些任务提供更多的 CPU 时间,以确保用户体验良好。
+- 响应时间和交互性可能是关键指标,调度算法可能会考虑优先级调度,使得用户任务更容易获得 CPU 时间。
+
+**纯后台任务**:时间片居多
+
+- 这些任务通常是自动化或批处理任务,不需要实时响应,如系统维护、定期备份等。
+- 对于后台任务,系统调度算法可能会考虑平衡资源的使用,以避免它们占用过多的 CPU 时间,影响面向用户任务的性能。
+- 可能会使用限制性调度策略,以限制后台任务的资源使用,或者使用带有低优先级的队列。

+ 183 - 1
面经/问答/智力问题.md

@@ -40,4 +40,186 @@
 
  **第四种方法是Hash法**。如果这1亿个书里面有很多重复的数,先通过Hash法,把这1亿个数字去重复,这样如果重复率很高的话,会减少很大的内存用量,从而缩小运算空间,然后通过分治法或最小堆法查找最大的10000个数。
 
- **第五种方法采用最小堆**。首先读入前10000个数来创建大小为10000的最小堆,建堆的时间复杂度为O(mlogm)(m为数组的大小即为10000),然后遍历后续的数字,并于堆顶(最小)数字进行比较。如果比最小的数小,则继续读取后续数字;如果比堆顶数字大,则替换堆顶元素并重新调整堆为最小堆。整个过程直至1亿个数全部遍历完为止。然后按照中序遍历的方式输出当前堆中的所有10000个数字。该算法的时间复杂度为O(nmlogm),空间复杂度是10000(常数)。
+ **第五种方法采用最小堆**。首先读入前10000个数来创建大小为10000的最小堆,建堆的时间复杂度为O(mlogm)(m为数组的大小即为10000),然后遍历后续的数字,并于堆顶(最小)数字进行比较。如果比最小的数小,则继续读取后续数字;如果比堆顶数字大,则替换堆顶元素并重新调整堆为最小堆。整个过程直至1亿个数全部遍历完为止。然后按照中序遍历的方式输出当前堆中的所有10000个数字。该算法的时间复杂度为O(nmlogm),空间复杂度是10000(常数)。
+
+## 给定100亿个无符号的乱序的整数序列,如何求出这100亿个数的中位数(中位数指的是排序后最中间那个数),内存只有**512M**。
+
+中位数问题可以看做一个统计问题,而不是排序问题,无符号整数大小为4B,则能表示的数的范围为为0 ~ 2^32 - 1(40亿),如果没有限制内存大小,则可以用一个2^32(4GB)大小的数组(也叫做桶)来保存100亿个数中每个无符号数出现的次数。遍历这100亿个数,当元素值等于桶元素索引时,桶元素的值加1。当统计完100亿个数以后,则从索引为0的值开始累加桶的元素值,当累加值等于50亿时,这个值对应的索引为中位数。时间复杂度为O(n)。
+
+但是由于内存只有512M,所以上述方法不合适。
+
+如果100亿个数字保存在一个大文件中,可以依次读一部分文件到内存(不超过内存限制),将每个数字用二进制表示,比较二进制的最高位(第32位,符号位,0是正,1是负)。
+
+如果数字的最高位为0,则将这个数字写入 file_0文件中;如果最高位为 1,则将该数字写入file_1文件中。 从而将100亿个数字分成了两个文件。
+
+假设 file_0文件中有 60亿 个数字,file_1文件中有 40亿 个数字。那么中位数就在 file_0 文件中,并且是 file_0 文件中所有数字排序之后的第 10亿 个数字。因为file_1中的数都是负数,file_0中的数都是正数,也即这里一共只有40亿个负数,那么排序之后的第50亿个数一定位于file_0中。
+
+现在,我们只需要处理 file_0 文件了,不需要再考虑file_1文件。对于 file_0 文件,同样采取上面的措施处理:将file_0文件依次读一部分到内存(不超过内存限制),将每个数字用二进制表示,比较二进制的 次高位(第31位),如果数字的次高位为0,写入file_0_0文件中;如果次高位为1,写入file_0_1文件 中。
+
+现假设 file_0_0文件中有30亿个数字,file_0_1中也有30亿个数字,则中位数就是:file_0_0文件中的数字从小到大排序之后的第10亿个数字。
+
+抛弃file_0_1文件,继续对 file_0_0文件 根据 次次高位(第30位) 划分,假设此次划分的两个文件为:file_0_0_0中有5亿个数字,file_0_0_1中有25亿个数字,那么中位数就是 file_0_0_1文件中的所有数字排序之后的 第 5亿 个数。
+
+按照上述思路,直到划分的文件可直接加载进内存时,就可以直接对数字进行快速排序,找出中位数了。
+
+## 统计不同号码的个数
+
+## 题目描述
+
+**已知某个文件内包含大量电话号码,每个号码为8位数字,如何统计不同号码的个数?内存限制100M**
+
+## 思路分析
+
+这类题目其实是求解数据重复的问题。对于这类问题,可以使用**位图法**处理
+
+8位电话号码可以表示的范围为00000000~99999999。如果用 bit表示一个号码,那么总共需要1亿个bit,总共需要大约**10MB**的内存。
+
+申请一个位图并初始化为0,然后遍历所有电话号码,**把遍历到的电话号码对应的位图中的bit设置为1**。当遍历完成后,如果bit值为1,则表示这个电话号码在文件中存在,否则这个bit对应的电话号码在文件中不存在。
+
+最后这个**位图中bit值为1的数量**就是不同电话号码的个数了。
+
+那么如何确定电话号码对应的是位图中的哪一位呢?
+
+可以使用下面的方法来做**电话号码和位图的映射**。
+
+```java
+00000000 对应位图最后一位:0×0000…000001。
+00000001 对应位图倒数第二位:0×0000…0000010(1 向左移 1 位)。
+00000002 对应位图倒数第三位:0×0000…0000100(1 向左移 2 位)。
+……
+00000012 对应位图的倒数第十三位:0×0000…0001 0000 0000 0000(1 向左移 12 位)。
+```
+
+也就是说,电话号码就是1这个数字左移的次数。
+
+## 出现频率最高的100个词
+
+### 题目描述
+
+假如有一个**1G**大小的文件,文件里每一行是一个词,每个词的大小不超过**16byte**,要求返回出现频率最高的100个词。内存大小限制是**10M**
+
+### 解法1
+
+由于内存限制,我们无法直接将大文件的所有词一次性读到内存中。
+
+可以采用**分治策略**,把一个大文件分解成多个小文件,保证每个文件的大小小于10M,进而直接将单个小文件读取到内存中进行处理。
+
+**第一步**,首先遍历大文件,对遍历到的每个词x,执行 `hash(x) % 500`,将结果为i的词存放到文件f(i)中,遍历结束后,可以得到500个小文件,每个小文件的大小为2M左右;
+
+**第二步**,接着统计每个小文件中出现频数最高的100个词。可以使用HashMap来实现,其中key为词,value为该词出现的频率。
+
+对于遍历到的词x,如果在map中不存在,则执行 `map.put(x, 1)。`
+
+若存在,则执行 `map.put(x, map.get(x)+1)`,将该词出现的次数加1。
+
+**第三步**,在第二步中找出了每个文件出现频率最高的100个词之后,通过维护一个**小顶堆**来找出所有小文件中出现频率最高的100个词。
+
+具体方法是,遍历第一个文件,把第一个文件中出现频率最高的100个词构建成一个小顶堆。
+
+如果第一个文件中词的个数小于100,可以继续遍历第二个文件,直到构建好有100个结点的小顶堆为止。
+
+继续遍历其他小文件,如果遍历到的词的出现次数大于堆顶上词的出现次数,可以用新遍历到的词替换堆顶的词,然后重新调整这个堆为小顶堆。
+
+当遍历完所有小文件后,这个小顶堆中的词就是出现频率最高的100个词。
+
+总结一下,这种解法的主要思路如下:
+
+1. 采用**分治**的思想,进行哈希取余
+2. 使用**HashMap**统计每个小文件单词出现的次数
+3. 使用**小顶堆**,遍历步骤2中的小文件,找出词频top100的单词
+
+但是很容易可以发现问题,在第二步中,如果这个1G的大文件中有某个词词频过高,可能导致小文件大小超过10m。这种情况下该怎么处理呢?
+
+接下来看另外一种解法。
+
+### 解法2
+
+**第一步**:
+
+使用多路归并排序对大文件进行排序,这样相同的单词肯定是紧挨着的
+
+多路归并排序对大文件进行排序的步骤如下:
+
+① 将文件按照顺序切分成大小不超过2m的小文件,总共500个小文件
+
+② 使用10MB内存**分别**对 500 个小文件中的单词进行**排序**
+
+③ 使用一个大小为500大小的堆,对500个小文件进行**多路排序**,结果写到一个大文件中
+
+其中第三步,对500个小文件进行多路排序的思路如下:
+
+- 初始化一个最小堆,大小就是有序小文件的个数500。堆中的每个节点存放每个有序小文件对应的输入流。
+- 按照每个有序文件中的下一行数据对所有文件输入流进行排序,单词小的输入文件流放在堆顶。
+- 拿出堆顶的输入流,并其下一行数据写入到最终排序的文件中,如果拿出来的输入流中还有数据的话,那么将这个输入流再一次添加到栈中。否则说明该文件输入流中没有数据了,那么可以关闭这个流。
+- 循环这个过程,直到所有文件输入流都没有数据为止。
+
+**第二步**:
+
+① 初始化一个100个节点的**小顶堆**,用于保存100个出现频率最多的单词
+
+② 遍历整个文件,一个单词一个单词的从文件中取出来,并计数
+
+③ 等到遍历的单词和上一个单词不同的话,那么上一个单词及其频率如果大于堆顶的词的频率,那么放在堆中,否则不放
+
+最终,小顶堆中就是出现频率前100的单词了。
+
+解法2相对解法1,更加严谨,如果某个词词频过高或者整个文件都是同一个词的话,解法1不适用。
+
+## 查找两个大文件共同的URL
+
+### 题目
+
+给定 a、b 两个文件,各存放 50 亿个 URL,每个 URL 各占 64B,找出 a、b 两个文件共同的 URL。内存限制是 4G。
+
+### 分析
+
+每个 URL 占 64B,那么 50 亿个 URL占用的空间大小约为 320GB。
+
+5,000,000,000 * 64B ≈ 320GB
+
+由于内存大小只有 4G,因此,不可能一次性把所有 URL 加载到内存中处理。
+
+可以采用分治策略,也就是把一个文件中的 URL 按照某个特征划分为多个小文件,使得每个小文件大小不超过 4G,这样就可以把这个小文件读到内存中进行处理了。
+
+首先遍历文件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 就好了。
+
+接着遍历 ai( `i∈[0,999]`),把 URL 存储到一个 HashSet 集合中。然后遍历 bi 中每个 URL,看在 HashSet 集合中是否存在,若存在,说明这就是共同的 URL,可以把这个 URL 保存到一个单独的文件中。
+
+## 如何找出排名前 500 的数?
+
+### 题目描述
+
+有 1w 个数组,每个数组有 500 个元素,并且有序排列。如何在这 10000*500 个数中找出前 500 的数?
+
+### 分析
+
+* 方法一:
+
+对于这种topk问题,更常用的方法是使用**堆排序**。
+
+对本题而言,假设数组降序排列,可以采用以下方法:
+
+首先建立大顶堆,堆的大小为数组的个数,即为10000 ,把每个数组最大的值存到堆中。
+
+接着删除堆顶元素,保存到另一个大小为 500 的数组中,然后向大顶堆插入删除的元素所在数组的下一个元素。
+
+重复上面的步骤,直到删除完第 500 个元素,也即找出了最大的前 500 个数。
+
+* 方法二
+
+类似map/reduce,找出10台机器上top500,然后再top500,可以使用快排序,堆排序
+
+* 方法三
+
+如果全是正数,使用bit数组,统计下标+1,最后计算count == 500
+
+## 50万个TCP一台机器能否保证
+
+通过以下几个方面分析
+
+1. 能否使用nginx代理
+2. 能否使用多个网卡
+3. 文件描述符大小更改
+4. 内存大小更改
+5. 磁盘空间更改
+6. 最大的流量

+ 34 - 0
面经/问答/职业问题.md

@@ -0,0 +1,34 @@
+## 最成功的事情
+
+解决了JMX的bug,项目按期上线
+
+## 职业发展
+
+java后端开发-》架构师
+
+### 语言基础
+
+java架构师,顾名思义,你的主流开发语言是java。放宽了讲,语言本身也不过是一种工具,只是用你最熟练的来解决问题而已。
+
+所以作为java架构师,java语言不仅仅是应用层面,jvm底层的相关原理类,依然是需要去掌握的点
+
+### 开发框架
+
+这是将架构师的工作落地的直接手段。因为架构的终极目标是业务指标。不能利于业务的进展,架构是没有意义的。
+
+而体现在业务上最简单粗暴的就是开发框架。如果一个架构师连基本的开发框架都吃不透,那就别谈了。
+
+### 中间件
+
+依然是工具。各种中间件是辅助你完成业务架构设计的桥梁。需要异步和消息那就得用消息队列,需要缓存那就得用redis。这都是老生常谈的话题。然而,这里我要强调的是,作为架构师对中间件的掌握不能停留在使用层面,要完全的掌控它。因为任何技术的引入你都要确保在它出现问题或者现有功能不满足你的需求时,架构师能够第一时间来找到解决问题的办法。另一方面是选型。设计当前业务系统,同一个功能点,可能有n种同类别的中间件来实现。那么选哪个最合适?这就需要你对主流中间件特性的一种把控。
+
+### 解决方案
+
+如果说上面是一堆的工具,那么你必须具备常见领域里的一些解决方案的积累。单点登录、分布式事务、高并发秒杀、复杂工作流、支付系统设计……太多了。在开发和日常生活中勤积累,遇到合适的场景时积极去实践和试错。
+
+### 架构思维
+
+作为架构师,你必须具备一定的思维。在阅读大师的源码时,你会发现设计这套东西的人,思维真的不简单。如果渐渐的找到这种感觉,说明你的思维在慢慢升华。当遇到同类场景和问题时,你慢慢的就具备了自己解决问题的一些巧妙的思维,来指导你去分析和实战。至于上面提到的一系列技术,那属于执行层面的事情。想明白了问题确定好思路。到实施层面事情其实已经搞定了一大半,选择合适的工具解决它就可以了
+
+
+

+ 52 - 0
面经/问答/计网.md

@@ -488,3 +488,55 @@ RPC(Remote Procedure Call)协议是一种用于实现分布式系统中不
 对于主流的 HTTP/1.1,虽然它现在叫**超文本**协议,支持音频视频,但 HTTP 设计初是用于做网页**文本**展示的,所以它传的内容以字符串为主。Header 和 Body 都是如此。在 Body 这块,它使用 **Json** 来**序列化**结构体数据。但是这里面的内容非常多的**冗余**,显得**非常啰嗦**。
 
 而 RPC,因为它定制化程度更高,可以采用体积更小的 Protobuf 或其他序列化协议去保存结构体数据,同时也不需要像 HTTP 那样考虑各种浏览器行为,比如 302 重定向跳转啥的。**因此性能也会更好一些,这也是在公司内部微服务中抛弃 HTTP,选择使用 RPC 的最主要原因。**
+
+## HTTP 499 问题处理方法合集
+
+[nginx](https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fnginx%2Fnginx) 中的 `src/http/ngx_http_special_response.c` 文件中对 499 状态码进行了定义:
+
+```c
+c复制代码    ngx_string(ngx_http_error_494_page), /* 494, request header too large */
+    ngx_string(ngx_http_error_495_page), /* 495, https certificate error */
+    ngx_string(ngx_http_error_496_page), /* 496, https no certificate */
+    ngx_string(ngx_http_error_497_page), /* 497, http to https */
+    ngx_string(ngx_http_error_404_page), /* 498, canceled */
+    ngx_null_string,                     /* 499, client has closed connection */
+```
+
+从注释上,我们可以看到 499 表示**客户端主动断开连接**。
+
+表面上 499 是客户端主动断开,然而在实际业务开发中,当出现 HTTP 499 状态码时,大部分都是由于服务端请求时间过长,导致客户端等的“不耐烦”了,因此断开了连接。
+
+
+
+### 1、服务端接口请求超时
+
+在客户端请求服务端接口时,有些接口请求确实很慢。这种情况呢,就是接口是真的慢,不是偶然现象,是什么时候请求都慢,这个也最好解决,优化接口即可。
+
+### 2、nginx 导致断开连接
+
+就是连续两次过快的 post 请求就会出现 499 的情况,是 nginx 认为这是不安全的连接,主动断开了客户端的连接。
+
+### 3、固定时间出现 499 问题
+
+可能是机器这段时间执行定时任务之类的
+
+### 4、偶尔出现一下 499 问题
+
+MYSQL 会有将脏页数据刷到磁盘的操作,这个我们具体我们会有一片单独的文章介绍。在 MYSQL 执行刷脏页的时候,会占用系统资源,这个时候,我们的查询请求就有可能比平时慢了。
+
+## 调用close函数用户态内核态分别发送了什么
+
+**用户态**:
+
+1. 程序调用 `close()` 方法:在用户态,应用程序发起了关闭套接字连接的请求。
+2. 发送关闭请求到内核:操作系统会将关闭请求传递到内核态,准备执行套接字的关闭操作。
+3. 用户态等待:在关闭操作完成之前,用户态可能会等待,因为关闭操作可能涉及到网络数据传输和其他底层操作。
+
+**内核态**:
+
+1. 内核接收关闭请求:内核接收到用户态发起的关闭请求。
+2. 进行数据传输:如果还有未发送的数据,内核会尝试将这些数据发送出去,确保对方接收到数据。
+3. 发送关闭通知:内核会向对方发送一个关闭通知,表示本端即将关闭连接。
+4. 等待关闭通知:内核等待对方发送关闭通知,以确保双方都已经准备好关闭连接。
+5. 执行关闭操作:当双方都准备好关闭连接时,内核执行实际的关闭操作,释放相关资源。
+6. 回收资源:内核回收连接所占用的内存、文件描述符等资源。