[
  {
    "path": ".gitignore",
    "content": "# Created by .ignore support plugin (hsz.mobi)\n\n.idea\n"
  },
  {
    "path": "Go/go.md",
    "content": "\n# Go和Java的区别 \n\n\n# go和Java的线程模型区别\n\n# go和Java的GC区别\n"
  },
  {
    "path": "Java-JVM/JVM的启动过程.md",
    "content": "\n* [JVM的启动过程](#jvm的启动过程)\n    * [1、JVM的装入环境和配置](#1jvm的装入环境和配置)\n    * [2、装载JVM](#2装载jvm)\n    * [3、初始化JVM，获得本地调用接口](#3初始化jvm获得本地调用接口)\n    * [4、运行Java程序](#4运行java程序)\n\n\n# JVM的启动过程\n## 1、JVM的装入环境和配置\n\n在学习这个之前，我们需要了解一件事情，就是JDK和JRE的区别。\n\nJDK是面向开发人员使用的SDK，它提供了Java的开发环境和运行环境，JDK中包含了JRE。\n\nJRE是Java的运行环境，是面向所有Java程序的使用者，包括开发者。\n\nJRE = 运行环境 = JVM。\n\n如果安装了JDK，会发现电脑中有两套JRE，一套位于/Java/jre.../下，一套位于/Java/jdk.../jre下。那么问题来了，一台机器上有两套以上JRE，谁来决定运行那一套呢？这个任务就落到java.exe身上，java.exe的任务就是找到合适的JRE来运行java程序。\n\njava.exe按照以下的顺序来选择JRE：\n\n1、自己目录下有没有JRE\n\n2、父目录下有没有JRE\n\n3、查询注册表： HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Runtime Environment\\\"当前JRE版本号\"\\JavaHome\n\n这几步的主要核心是为了找到JVM的绝对路径。\n\n\njvm.cfg的路径为：JRE路径\\lib\\\"CPU架构\"\\jvm.fig\n\njvm.cfg的内容大致如下：\n\n\n-client KNOWN\n-server KNOWN\n-hotspot ALIASED_TO -client\n-classic WARN\n-native ERROR\n-green ERROR\n\nKNOWN 表示存在 、IGNORE 表示不存在 、ALIASED_TO 表示给别的JVM去一个别名\nWARN 表示不存在时找一个替代 、ERROR 表示不存在抛出异常\n\n## 2、装载JVM\n通过第一步找到JVM的路径后，Java.exe通过LoadJavaVM来装入JVM文件。\nLoadLibrary装载JVM动态连接库，然后把JVM中的到处函数JNI_CreateJavaVM和JNI_GetDefaultJavaVMIntArgs 挂接到InvocationFunction 变量的CreateJavaVM和GetDafaultJavaVMInitArgs 函数指针变量上。JVM的装载工作完成。\n\n## 3、初始化JVM，获得本地调用接口\n\n调用InvocationFunction -> CreateJavaVM也就是JVM中JNI_CreateJavaVM方法获得JNIEnv结构的实例。\n\n## 4、运行Java程序\n\nJVM运行Java程序的方式有两种：jar包 与 Class\n\n运行jar 的时候，Java.exe调用GetMainClassName函数，该函数先获得JNIEnv实例然后调用JarFileJNIEnv类中getManifest()，从其返回的Manifest对象中取getAttrebutes(\"Main-Class\")的值，即jar 包中文件：META-INF/MANIFEST.MF指定的Main-Class的主类名作为运行的主类。之后main函数会调用Java.c中LoadClass方法装载该主类（使用JNIEnv实例的FindClass）。\n\n运行Class的时候，main函数直接调用Java.c中的LoadClass方法装载该类。"
  },
  {
    "path": "Java-JVM/JVM调优.md",
    "content": "\n* [原则](#原则)\n* [jvm调优](#jvm调优)\n  * [JVM调优目标](#jvm调优目标)\n  * [举例](#举例)\n  * [JVM调优的步骤](#jvm调优的步骤)\n  * [JVM参数解析及调优](#jvm参数解析及调优)\n\n\n# 原则\n- 大多数的Java应用不需要进行JVM优化；\n- 大多数导致GC问题的原因是代码层面的问题导致的（代码层面）；\n- 上线之前，应先考虑将机器的JVM参数设置到最优；\n- 减少创建对象的数量（代码层面）；\n- 减少使用全局变量和大对象（代码层面）；\n- 优先架构调优和代码调优，JVM优化是不得已的手段（代码、架构层面）；\n- 分析GC情况优化代码比优化JVM参数更好（代码层面）；\n# jvm调优\n### JVM调优目标\n- 延迟：GC低停顿和GC低频率；\n- 低内存占用；\n- 高吞吐量;\n### 举例\n- Heap 内存使用率 <= 70%;\n- Old generation内存使用率<= 70%;\n- avgpause <= 1秒;\n- Full gc 次数0 或 avg pause interval >= 24小时 ;\n### JVM调优的步骤\n1. 分析GC日志及dump文件，判断是否需要优化，确定瓶颈问题点；\n2. 确定JVM调优量化目标；\n3. 确定JVM调优参数（根据历史JVM参数来调整）；\n4. 依次调优内存、延迟、吞吐量等指标；\n5. 对比观察调优前后的差异；\n6. 不断的分析和调整，直到找到合适的JVM参数配置；\n7. 找到最合适的参数，将这些参数应用到所有服务器，并进行后续跟踪。\n### JVM参数解析及调优\n- -Xmx4g –Xms4g –Xmn1200m –Xss512k -XX:NewRatio=4 -XX:SurvivorRatio=8 -XX:PermSize=100m -XX:MaxPermSize=256m -XX:MaxTenuringThreshold=15\n- XX:+PrintGCDetails\n  - -Xlog\n- -Xmx4g：堆内存最大值为4GB。\n- -Xms4g：初始化堆内存大小为4GB。\n  - 初始化堆内存大小，默认为物理内存的1/64(小于1GB)。\n- -Xmn1200m：设置年轻代大小为1200MB。增大年轻代后，将会减小年老代大小。此值对系统性能影响较大，Sun官方推荐配置为整个堆的3/8。\n- -Xss512k：设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1MB，以前每个线程堆栈大小为256K。应根据应用线程所需内存大小进行调整。在相同物理内存下，减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的，不能无限生成，经验值在3000~5000左右。\n- -XX:NewRatio=4：设置年轻代（包括Eden和两个Survivor区）与年老代的比值（除去持久代）。设置为4，则年轻代与年老代所占比值为1：4，年轻代占整个堆栈的1/5\n- -XX:SurvivorRatio=8：设置年轻代中Eden区与Survivor区的大小比值。设置为8，则两个Survivor区与一个Eden区的比值为2:8，一个Survivor区占整个年轻代的1/10\n- -XX:PermSize=100m：初始化永久代大小为100MB。\n- -XX:MaxPermSize=256m：设置持久代大小为256MB。\n- -XX:MaxTenuringThreshold=15：设置垃圾最大年龄。如果设置为0的话，则年轻代对象不经过Survivor区，直接进入年老代。对于年老代比较多的应用，可以提高效率。如果将此值设置为一个较大值，则年轻代对象会在Survivor区进行多次复制，这样可以增加对象再年轻代的存活时间，增加在年轻代即被回收的概论。\n- -XX:MaxDirectMemorySize=1G：直接内存。报java.lang.OutOfMemoryError: Direct buffer memory异常可以上调这个值。\n- -XX:+DisableExplicitGC：禁止运行期显式地调用System.gc()来触发fulll GC。\n- -XX:ConcGCThreads=4：CMS垃圾回收器并行线程线，推荐值为CPU核心数。\n- -XX:ParallelGCThreads=8：新生代并行收集器的线程数。\n- -XX:+HeapDumpOnOutOfMemoryError 当首次遭遇内存溢出时Dump出此时的堆内 ，再使用jhat分析\n- 内存优化示例\n  - java heap：参数-Xms和-Xmx，建议扩大至3-4倍FullGC后的老年代空间占用。\n  - 永久代：-XX:PermSize和-XX:MaxPermSize，建议扩大至1.2-1.5倍FullGc后的永久带空间占用。\n  - 新生代：-Xmn，建议扩大至1-1.5倍FullGC之后的老年代空间占用。\n  - 老年代：2-3倍FullGC后的老年代空间占用。\n- 延迟优化示例\n  - 应用程序可接受的平均停滞时间: 此时间与测量的Minor\n  - GC持续时间进行比较。可接受的Minor GC频率：Minor\n  - GC的频率与可容忍的值进行比较。\n  - 可接受的最大停顿时间:最大停顿时间与最差情况下FullGC的持续时间进行比较。\n  - 可接受的最大停顿发生的频率：基本就是FullGC的频率。\n  - 新生代空间越大，Minor GC的GC时间越长，频率越低。如果想减少其持续时长，就需要减少其空间大小。如果想减小其频率，就需要加大其空间大小。"
  },
  {
    "path": "Java-JVM/Java即时编译.md",
    "content": "# 什么是\n- 常见的编译型语言如C++，通常会把代码直接编译成CPU所能理解的机器码来运行。而Java为了实现“一次编译，处处运行”的特性，把编译的过程分成两部分，首先它会先由javac编译成通用的中间形式——字节码，然后再由解释器逐条将字节码解释为机器码来执行。所以在性能上，Java通常不如C++这类编译型语言\n- 为了优化Java的性能 ，JVM在解释器之外引入了即时（Just In\n  Time）编译器：当程序运行时，解释器首先发挥作用，代码可以直接执行。随着时间推移，即时编译器逐渐发挥作用，把越来越多的代码编译优化成本地代码，来获取更高的执行效率。解释器这时可以作为编译运行的降级手段，在一些不可靠的编译优化出现问题时，再切换回解释执行，保证程序可以正常运行\n- 即时编译器极大地提高了Java程序的运行速度，而且跟静态编译相比，即时编译器可以选择性地编译热点代码，省去了很多编译时间，也节省很多的空间。目前，即时编译器已经非常成熟了，在性能层面甚至可以和编译型语言相比。不过在这个领域，大家依然在不断探索如何结合不同的编译方式，使用更加智能的手段来提升程序的运行速度。\n\n# Java的执行过程\n\n## Java的执行过程整体可以分为两个部分\n\n1. 第一步由javac将源码编译成字节码，在这个过程中会进行词法分析、语法分析、语义分析，编译原理中这部分的编译称为前端编译\n2. 接下来无需编译直接逐条将字节码解释执行，在解释执行的过程中，虚拟机同时对程序运行的信息进行收集，在这些信息的基础上，编译器会逐渐发挥作用，它会进行后端编译——把字节码编译成机器码，但不是所有的代码都会被编译，只有被JVM认定为的热点代码，才可能被编译。\n   - **热点代码** JVM中会设置一个阈值，当方法或者代码块的在一定时间内的调用次数超过这个阈值时就会被编译，存入codeCache中。当下次执行时，再遇到这段代码，就会从codeCache中读取机器码，直接执行，以此来提升程序运行的性能\n\n## 1.JVM中的编译器\n\nJVM中集成了两种编译器\n\n### Client Compiler\n\n- Client Compiler注重启动速度和局部的优化\n- HotSpot VM带有一个Client Compiler C1编译器。这种编译器启动速度快，但是性能比较Server Compiler来说会差一些。C1会做三件事\n    - 局部简单可靠的优化，比如字节码上进行的一些基础优化，方法内联、常量传播等，放弃许多耗时较长的全局优化。\n    - 将字节码构造成高级中间表示（High-level Intermediate Representation，以下称为HIR），HIR与平台无关，通常采用图结构，更适合JVM对程序进行优化。\n    - 最后将HIR转换成低级中间表示（Low-level Intermediate\n      Representation，以下称为LIR），在LIR的基础上会进行寄存器分配、窥孔优化（局部的优化方式，编译器在一个基本块或者多个基本块中，针对已经生成的代码，结合CPU自己指令的特点，通过一些认为可能带来性能提升的转换规则或者通过整体的分析，进行指令转换，来提升代码性能）等操作，最终生成机器码。\n\n### Server Compiler\n\n- Server Compiler则更加关注全局的优化，性能会更好，但由于会进行更多的全局分析，所以启动速度会变慢\n- Server Compiler主要关注一些编译耗时较长的全局优化，甚至会还会根据程序运行的信息进行一些不可靠的激进优化。这种编译器的启动时间长，适用于长时间运行的后台程序，它的性能通常比Client\n  Compiler高30%以上。目前，Hotspot虚拟机中使用的Server Compiler有两种：C2和Graal\n    - C2 Compiler\n        - C2编译器在进行编译优化时，会使用一种控制流与数据流结合的图数据结构，称为Ideal Graph。 Ideal\n          Graph表示当前程序的数据流向和指令间的依赖关系，依靠这种图结构，某些优化步骤（尤其是涉及浮动代码块的那些优化步骤）变得不那么复杂\n        - Ideal\n          Graph的构建是在解析字节码的时候，根据字节码中的指令向一个空的Graph中添加节点，Graph中的节点通常对应一个指令块，每个指令块包含多条相关联的指令，JVM会利用一些优化技术对这些指令进行优化，比如Global\n          Value Numbering、常量折叠等，解析结束后，还会进行一些死代码剔除的操作。生成Ideal\n          Graph后，会在这个基础上结合收集的程序运行信息来进行一些全局的优化，这个阶段如果JVM判断此时没有全局优化的必要，就会跳过这部分优化。\n        - 构建Ideal Graph其实就是把指令添加到Ideal Graph，然后形成一个指向这个指令的节点，并且用各种优化手段优化这些指令\n        - 无论是否进行全局优化，Ideal Graph都会被转化为一种更接近机器层面的MachNode Graph，最后编译的机器码就是从MachNode Graph中得的，生成机器码前还会有一些包括寄存器分配、窥孔优化等操作\n    - Graal Compiler\n        - 从JDK 9开始，Hotspot VM中集成了一种新的Server Compiler，Graal编译器。相比C2编译器，Graal有这样几种关键特性\n        - JVM会在解释执行的时候收集程序运行的各种信息，然后编译器会根据这些信息进行一些基于预测的激进优化，比如分支预测，根据程序不同分支的运行概率，选择性地编译一些概率较大的分支。Graal比C2更加青睐这种优化，所以Graal的峰值性能通常要比C2更好\n            - 使用Java编写，对于Java语言，尤其是新特性，比如Lambda、Stream等更加友好。\n            - 更深层次的优化，比如虚函数的内联、部分逃逸分析等\n        - Graal编译器可以通过Java虚拟机参数-XX:+UnlockExperimentalVMOptions -XX:\n          +UseJVMCICompiler启用。当启用时，它将替换掉HotSpot中的C2编译器，并响应原本由C2负责的编译请求。\n\n## 2.分层编译\n\n### 什么是\n\n- 在Java 7以前，需要研发人员根据服务的性质去选择编译器\n- 对于需要快速启动的，或者一些不会长期运行的服务，可以采用编译效率较高的C1，对应参数-client。长期运行的服务，或者对峰值性能有要求的后台服务，可以采用峰值性能更好的C2，对应参数-server\n- Java 7开始引入了分层编译的概念，它结合了C1和C2的优势，追求启动速度和峰值性能的一个平衡。分层编译将JVM的执行状态分为了五个层次\n\n### 五个层级\n\n- 层次\n    - 解释执行。\n    - 执行不带profiling的C1代码。\n        - profiling就是收集能够反映程序执行状态的数据。其中最基本的统计数据就是方法的调用次数，以及循环回边的执行次数\n    - 执行仅带方法调用次数以及循环回边执行次数profiling的C1代码。\n    - 执行带所有profiling的C1代码。\n    - 执行C2代码。\n- 特点\n    - 通常情况下，C2代码的执行效率要比C1代码的高出30%以上。C1层执行的代码，按执行效率排序从高至低则是1层>2层>3层。\n    - 这5个层次中，1层和4层都是终止状态，当一个方法到达终止状态后，只要编译后的代码并没有失效，那么JVM就不会再次发出该方法的编译请求的。\n    - 服务实际运行时，JVM会根据服务运行情况，从解释执行开始，选择不同的编译路径，直到到达终止状态\n- 常见的编译路径\n  ![](../img/jvm/常见的编译路径.png)\n    - 图中第①条路径，代表编译的一般情况，热点方法从解释执行到被3层的C1编译，最后被4层的C2编译。\n    -\n    如果方法比较小（比如Java服务中常见的getter/setter方法），3层的profiling没有收集到有价值的数据，JVM就会断定该方法对于C1代码和C2代码的执行效率相同，就会执行图中第②条路径。在这种情况下，JVM会在3层编译之后，放弃进入C2编译，直接选择用1层的C1编译运行。\n    - 在C1忙碌的情况下，执行图中第③条路径，在解释执行过程中对程序进行profiling ，根据信息直接由第4层的C2编译。\n    - 前文提到C1中的执行效率是1层>2层>3层，第3层一般要比第2层慢35%以上，所以在C2忙碌的情况下，执行图中第④条路径。这时方法会被2层的C1编译，然后再被3层的C1编译，以减少方法在3层的执行时间。\n    - 如果编译器做了一些比较激进的优化，比如分支预测，在实际运行时发现预测出错，这时就会进行反优化，重新进入解释执行，图中第⑤条执行路径代表的就是反优化。\n- 总的来说，C1的编译速度更快，C2的编译质量更高，分层编译的不同编译路径，也就是JVM根据当前服务的运行情况来寻找当前服务的最佳平衡点的一个过程。从JDK 8开始，JVM默认开启分层编译。\n\n## 3.即时编译的触发\n\n### Java虚拟机根据方法的调用次数以及循环回边的执行次数来触发即时编译\n- 循环回边\n    - 循环回边是一个控制流图中的概念，程序中可以简单理解为往回跳转的指令，比如下面这段代码：\n      ```java\n      public void nlp(Object obj) {\n         int sum = 0;\n         for (int i = 0; i < 200; i++) {\n             sum += i;\n         }\n      }\n      ```\n\n    - 上面这段代码经过编译生成下面的字节码。其中，偏移量为18的字节码将往回跳至偏移量为4的字节码中。在解释执行时，每当运行一次该指令，Java虚拟机便会将该方法的循环回边计数器加1。 \n    - 字节码 \n      ```java\n        public void nlp(java.lang.Object); \n          Code:\n             0: iconst_0 \n             1: istore_1 \n             2: iconst_0 \n             3: istore_2 \n             4: iload_2 \n             5: sipush 200 \n             8: if_icmpge 21 \n             11: iload_1 \n             12: iload_2 \n             13:iadd \n             14: istore_1 \n             15: iinc 2, 1 \n             18: goto 4 \n             21: return\n      ```\n      \n- 在即时编译过程中，编译器会识别循环的头部和尾部。上面这段字节码中，循环体的头部和尾部分别为偏移量为11的字节码和偏移量为15的字节码。编译器将在循环体结尾增加循环回边计数器的代码，来对循环进行计数\n\n当方法的调用次数和循环回边的次数的和，超过由参数`-XX:CompileThreshold`指定的阈值时（使用C1时，默认值为1500；使用C2时，默认值为10000），就会触发即时编译\n\n开启分层编译的情况下，`-XX:CompileThreshold`参数设置的阈值将会失效，触发编译会由以下的条件来判断 \n\n- 方法调用次数大于由参数`-XX:TierXInvocationThreshold`指定的阈值乘以系数。 \n- 方法调用次数大于由参数`-XX:TierXMINInvocationThreshold`指定的阈值乘以系数，并且方法调用次数和循环回边次数之和大于由参数-XX:TierXCompileThreshold指定的阈值乘以系数时。 \n### 分层编译触发条件公式 \n- `i >TierXInvocationThreshold * s || (i > TierXMinInvocationThreshold * s && i + b > TierXCompileThreshold * s)`i为调用次数，b是循环回边次数 \n- 上述满足其中一个条件就会触发即时编译，并且JVM会根据当前的编译方法数以及编译线程数动态调整系数s。\n\n# 编译优化\n即时编译器会对正在运行的服务进行一系列的优化，包括字节码解析过程中的分析，根据编译过程中代码的一些中间形式来做局部优化，还会根据程序依赖图进行全局优化，最后才会生成机器码\n## 1. 中间表达形式（Intermediate Representation）\n在编译原理中，通常把编译器分为前端和后端，前端编译经过词法分析、语法分析、语义分析生成中间表达形式（Intermediate Representation，以下称为IR），后端会对IR进行优化，生成目标代码\n- Java字节码就是一种IR，但是字节码的结构复杂，字节码这样代码形式的IR也不适合做全局的分析优化。现代编译器一般采用图结构的IR，静态单赋值（Static Single Assignment，SSA）IR是目前比较常用的一种。这种IR的特点是每个变量只能被赋值一次，而且只有当变量被赋值之后才能使用\n  - SSA IR\n   ``` \n   Plain Text\n    {\n        a = 1;\n        a = 2;\n        b = a;\n    }\n  ```\n  - 上述代码中我们可以轻易地发现a = 1的赋值是冗余的，但是编译器不能。传统的编译器需要借助数据流分析，从后至前依次确认哪些变量的值被覆盖掉。不过，如果借助了SSA IR，编译器则可以很容易识别冗余赋值。\n  - 上面代码的SSA IR形式的伪代码可以表示为\n  ```Plain Text\n  {\n      a_1 = 1;\n      a_2 = 2;\n      b_1 = a_2;\n  }\n  ```\n  - 由于SSA IR中每个变量只能赋值一次，所以代码中的a在SSA IR中会分成a_1、a_2两个变量来赋值，这样编译器就可以很容易通过扫描这些变量来发现a_1的赋值后并没有使用，赋值是冗余的\n- 我们可以将编译器的每一种优化看成一个图优化算法，它接收一个IR图，并输出经过转换后的IR图。编译器优化的过程就是一个个图节点的优化串联起来的\n## 2.方法内联\n- 方法内联，是指在编译过程中遇到方法调用时，将目标方法的方法体纳入编译范围之中，并取代原方法调用的优化手段。JIT大部分的优化都是在内联的基础上进行的，方法内联是即时编译器中非常重要的一环。\n- Java服务中存在大量getter/setter方法，如果没有方法内联，在调用getter/setter时，程序执行时需要保存当前方法的执行位置，创建并压入用于getter/setter的栈帧、访问字段、弹出栈帧，最后再恢复当前方法的执行。内联了对 getter/setter的方法调用后，上述操作仅剩字段访问\n- 编译器的大部分优化都是在方法内联的基础上。所以一般来说，内联的方法越多，生成代码的执行效率越高。但是对于即时编译器来说，内联的方法越多，编译时间也就越长，程序达到峰值性能的时刻也就比较晚\n### 虚函数内联（多态、接口）\n内联是JIT提升性能的主要手段，但是虚函数使得内联是很难的，因为在内联阶段并不知道他们会调用哪个方法。例如，我们有一个数据处理的接口，这个接口中的一个方法有三种实现add、sub和multi，JVM是通过保存虚函数表Virtual Method Table（以下称为VMT）存储class对象中所有的虚函数，class的实例对象保存着一个VMT的指针，程序运行时首先加载实例对象，然后通过实例对象找到VMT，通过VMT找到对应方法的地址，所以虚函数的调用比直接指向方法地址的classic call性能上会差一些。很不幸的是，Java中所有非私有的成员函数的调用都是虚调用\n- 比如如下代码将会优化\n```java\n    public class SimpleInliningTest {\n    public static void main(String[] args) throws InterruptedException {\n        VirtualInvokeTest obj = new VirtualInvokeTest();\n        VirtualInvoke1 obj1 = new VirtualInvoke1();\n        for (int i = 0; i < 100000; i++) {\n            invokeMethod(obj);\n            invokeMethod(obj1);\n        }\n        Thread.sleep(1000);\n    }\n    public static void invokeMethod(VirtualInvokeTest obj) {\n        obj.methodCall();\n    }\n    private static class VirtualInvokeTest {\n        public void methodCall() {\n            System.out.println(\"virtual call\");\n        }\n    }\n    private static class VirtualInvoke1 extends VirtualInvokeTest {\n        @Override\n        public void methodCall() {\n            super.methodCall();\n        }\n    }\n}\n```\n\n- 如下代码多个实现未被优化\n```java\n    public class SimpleInliningTest {\n    public static void main(String[] args) throws InterruptedException {\n        VirtualInvokeTest obj = new VirtualInvokeTest();\n        VirtualInvoke1 obj1 = new VirtualInvoke1();\n        VirtualInvoke2 obj2 = new VirtualInvoke2();\n        for (int i = 0; i < 100000; i++) {\n            invokeMethod(obj);\n            invokeMethod(obj1);\n            invokeMethod(obj2);\n        }\n        Thread.sleep(1000);\n    }\n    public static void invokeMethod(VirtualInvokeTest obj) {\n        obj.methodCall();\n    }\n    private static class VirtualInvokeTest {\n        public void methodCall() {\n            System.out.println(\"virtual call\");\n        }\n    }\n    private static class VirtualInvoke1 extends VirtualInvokeTest {\n        @Override\n        public void methodCall() {\n            super.methodCall();\n        }\n    }\n    private static class VirtualInvoke2 extends VirtualInvokeTest {\n        @Override\n        public void methodCall() {\n            super.methodCall();\n        }\n    }\n}\n```\n\n## 3. 逃逸分析\n逃逸分析是“一种确定指针动态范围的静态分析，它可以分析在程序的哪些地方可以访问到指针”。Java虚拟机的即时编译器会对新建的对象进行逃逸分析，判断对象是否逃逸出线程或者方法。即时编译器判断对象是否逃逸的依据有两种\n- 对象是否被存入堆中（静态字段或者堆中对象的实例字段），一旦对象被存入堆中，其他线程便能获得该对象的引用，即时编译器就无法追踪所有使用该对象的代码位置。\n- 对象是否被传入未知代码中，即时编译器会将未被内联的代码当成未知代码，因为它无法确认该方法调用会不会将调用者或所传入的参数存储至堆中，这种情况，可以直接认为方法调用的调用者以及参数是逃逸的。\n### 1、锁消除\n- 在学习Java并发编程时会了解锁消除，而锁消除就是在逃逸分析的基础上进行的。\n- 如果即时编译器能够证明锁对象不逃逸，那么对该锁对象的加锁、解锁操作没就有意义。因为线程并不能获得该锁对象。在这种情况下，即时编译器会消除对该不逃逸锁对象的加锁、解锁操作。实际上，编译器仅需证明锁对象不逃逸出线程，便可以进行锁消除。由于Java虚拟机即时编译的限制，上述条件被强化为证明锁对象不逃逸出当前编译的方法。不过，基于逃逸分析的锁消除实际上并不多见。\n### 2、栈上分配\n- 我们都知道Java的对象是在堆上分配的，而堆是对所有对象可见的。同时，JVM需要对所分配的堆内存进行管理，并且在对象不再被引用时回收其所占据的内存\n- 如果逃逸分析能够证明某些新建的对象不逃逸，那么JVM完全可以将其分配至栈上，并且在new语句所在的方法退出时，通过弹出当前方法的栈桢来自动回收所分配的内存空间。这样一来，我们便无须借助垃圾回收器来处理不再被引用的对象\n### 3、标量替换\n过Hotspot虚拟机，并没有进行实际的栈上分配，而是使用了标量替换这一技术。所谓的标量，就是仅能存储一个值的变量，比如Java代码中的基本类型。与之相反，聚合量则可能同时存储多个值，其中一个典型的例子便是Java的对象。编译器会在方法内将未逃逸的聚合量分解成多个标量，以此来减少堆上分配。下面是一个标量替换的例子\n  ```java\n    public class Example{\n      @AllArgsConstructor\n      class Cat{\n          int age;\n          int weight;\n      }\n      public static void example(){\n          Cat cat = new Cat(1,10);\n          addAgeAndWeight(cat.age,Cat.weight);\n      }\n  }\n  ```\n  - 经过逃逸分析，cat对象未逃逸出example()的调用，因此可以对聚合量cat进行分解，得到两个标量age和weight，进行标量替换后的伪代码：\n    ```java\n        public class Example{\n        @AllArgsConstructor\n        class Cat{\n            int age;\n            int weight;\n        }\n        public static void example(){\n            int age = 1;\n            int weight = 10;\n            addAgeAndWeight(age,weight);\n        }\n    }\n    ```\n\n### 4、部分逃逸分析\n部分逃逸分析也是Graal对于概率预测的应用。通常来说，如果发现一个对象逃逸出了方法或者线程，JVM就不会去进行优化，但是Graal编译器依然会去分析当前程序的执行路径，它会在逃逸分析基础上收集、判断哪些路径上对象会逃逸，哪些不会。然后根据这些信息，在不会逃逸的路径上进行锁消除、栈上分配这些优化手段。\n## 4. Loop Transformations\nC2编译器在构建Ideal Graph后会进行很多的全局优化，其中就包括对循环的转换，最重要的两种转换就是循环展开和循环分离\n### 1、循环展开\n循环展开是一种循环转换技术，它试图以牺牲程序二进制码大小为代价来优化程序的执行速度，是一种用空间换时间的优化手段。\n- 循环展开通过减少或消除控制程序循环的指令，来减少计算开销，这种开销包括增加指向数组中下一个索引或者指令的指针算数等。如果编译器可以提前计算这些索引，并且构建到机器代码指令中，那么程序运行时就可以不必进行这种计算。也就是说有些循环可以写成一些重复独立的代码。比如下面这个循环\n```java\n    public void loopRolling(){\n        for(int i = 0;i<200;i++){\n            delete(i);\n        }\n    }\n```\n- 上面的代码需要循环删除200次，通过循环展开可以得到下面这段代码：\n```java\n    public void loopRolling(){\n        for(int i = 0;i<200;i+=5){\n            delete(i);\n            delete(i+1);\n            delete(i+2);\n            delete(i+3);\n            delete(i+4);\n        }\n    }\n```\n- 这样展开就可以减少循环的次数，每次循环内的计算也可以利用CPU的流水线提升效率。当然这只是一个示例，实际进行展开时，JVM会去评估展开带来的收益，再决定是否进行展开。\n### 2、循环分离\n循环分离也是循环转换的一种手段。它把循环中一次或多次的特殊迭代分离出来，在循环外执行。举个例子，下面这段代码\n```java\nint a = 10;\nfor(int i = 0;i<10;i++){\n    b[i] = x[i] + x[a];\n    a = i;\n}\n```\n可以看出这段代码除了第一次循环a = 10以外，其他的情况a都等于i-1。所以可以把特殊情况分离出去，变成下面这段代码\n```java\nb[0] = x[0] + 10;\nfor(int i = 1;i<10;i++){\n    b[i] = x[i] + x[i-1];\n}\n```\n这种等效的转换消除了在循环中对a变量的需求，从而减少了开销。\n## 5. 窥孔优化与寄存器分配\n前文提到的窥孔优化是优化的最后一步，这之后就会程序就会转换成机器码，窥孔优化就是将编译器所生成的中间代码（或目标代码）中相邻指令，将其中的某些组合替换为效率更高的指令组，常见的比如强度削减、常数合并等，看下面这个例子就是一个强度削减的例子\n- 强度削减\n  - y1=x1*3  经过强度削减后得到  y1=(x1<<1)+x1\n  - 编译器使用移位和加法削减乘法的强度，使用更高效率的指令组\n- 寄存器分配\n  - 寄存器分配也是一种编译的优化手段，在C2编译器中普遍的使用。它是通过把频繁使用的变量保存在寄存器中，CPU访问寄存器的速度比内存快得多，可以提升程序的运行速度。\n  - 寄存器分配和窥孔优化是程序优化的最后一步。经过寄存器分配和窥孔优化之后，程序就会被转换成机器码保存在codeCache中\n\n# 参考文章\n- https://tech.meituan.com/2020/10/22/java-jit-practice-in-meituan.html"
  },
  {
    "path": "Java-JVM/Java对象头.md",
    "content": "# Java对象头\n\n由于Java面向对象的思想，在JVM中需要大量存储对象，存储时为了实现一些额外的功能，需要在对象中添加一些标记字段用于增强对象功能，这些标记字段组成了对象头。\n\n# 对象头形式\n\nJVM中对象头的方式有以下两种（以32位JVM为例）：\n\n## 普通对象\n\n```text\n|--------------------------------------------------------------|\n|                     Object Header (64 bits)                  |\n|------------------------------------|-------------------------|\n|        Mark Word (32 bits)         |    Klass Word (32 bits) |\n|------------------------------------|-------------------------|\n```\n\n## 数组对象\n\n```text\n|---------------------------------------------------------------------------------|\n|                                 Object Header (96 bits)                         |\n|--------------------------------|-----------------------|------------------------|\n|        Mark Word(32bits)       |    Klass Word(32bits) |  array length(32bits)  |\n|--------------------------------|-----------------------|------------------------|\n```\n\n# 对象头的组成\n\n## Mark Word\n\n这部分主要用来存储对象自身的运行时数据，如hashcode、gc分代年龄等。mark word的位长度为JVM的一个Word大小，也就是说32位JVM的Mark word为32位，64位JVM为64位。\n为了让一个字大小存储更多的信息，JVM将字的最低两个位设置为标记位，不同标记位下的Mark Word示意如下：\n\n```text\n|-------------------------------------------------------|--------------------|\n|                  Mark Word (32 bits)                  |       State        |\n|-------------------------------------------------------|--------------------|\n| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 |       Normal       |\n|-------------------------------------------------------|--------------------|\n|  thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 |       Biased       |\n|-------------------------------------------------------|--------------------|\n|               ptr_to_lock_record:30          | lock:2 | Lightweight Locked |\n|-------------------------------------------------------|--------------------|\n|               ptr_to_heavyweight_monitor:30  | lock:2 | Heavyweight Locked |\n|-------------------------------------------------------|--------------------|\n|                                              | lock:2 |    Marked for GC   |\n|-------------------------------------------------------|--------------------|\n```\n\n其中各部分的含义如下：\n\nlock:2位的锁状态标记位，由于希望用尽可能少的二进制位表示尽可能多的信息，所以设置了lock标记。该标记的值不同，整个mark word表示的含义不同。\n\n|biased_lock|    lock|    状态|\n|:----------|:----|:-----|\n|0|    01    |无锁|\n|1|    01    |偏向锁|\n|0|    00    |轻量级锁|\n|0|    10    |重量级锁|\n|0|    11    |GC标记|\n\n- biased_lock：对象是否启用偏向锁标记，只占1个二进制位。为1时表示对象启用偏向锁，为0时表示对象没有偏向锁。\n- age：4位的Java对象年龄。在GC中，如果对象在Survivor区复制一次，年龄增加1。当对象达到设定的阈值时，将会晋升到老年代。默认情况下，并行GC的年龄阈值为15，并发GC的年龄阈值为6。由于age只有4位，所以最大值为15，这就是-XX:MaxTenuringThreshold选项最大值为15的原因。\n- identity_hashcode：25位的对象标识Hash码，采用延迟加载技术。调用方法System.identityHashCode()计算，并会将结果写到该对象头中。当对象被锁定时，该值会移动到管程Monitor中。\n- thread：持有偏向锁的线程ID。\n- epoch：偏向时间戳。\n- ptr_to_lock_record：指向栈中锁记录的指针。\n- ptr_to_heavyweight_monitor：指向管程Monitor的指针。\n\n64位下的标记字与32位的相似，不再赘述：\n```text\n|------------------------------------------------------------------------------|--------------------|\n|                                  Mark Word (64 bits)                         |       State        |\n|------------------------------------------------------------------------------|--------------------|\n| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 |       Normal       |\n|------------------------------------------------------------------------------|--------------------|\n| thread:54 |       epoch:2        | unused:1 | age:4 | biased_lock:1 | lock:2 |       Biased       |\n|------------------------------------------------------------------------------|--------------------|\n|                       ptr_to_lock_record:62                         | lock:2 | Lightweight Locked |\n|------------------------------------------------------------------------------|--------------------|\n|                     ptr_to_heavyweight_monitor:62                   | lock:2 | Heavyweight Locked |\n|------------------------------------------------------------------------------|--------------------|\n|                                                                     | lock:2 |    Marked for GC   |\n|------------------------------------------------------------------------------|--------------------|\n```\n## class pointer\n这一部分用于存储对象的类型指针，该指针指向它的类元数据，JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小，即32位的JVM为32位，64位的JVM为64位。\n\n如果应用的对象过多，使用64位的指针将浪费大量内存，统计而言，64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项+UseCompressedOops开启指针压缩，其中，oop即ordinary object pointer普通对象指针。开启该选项后，下列指针将压缩至32位：\n\n1. 每个Class的属性指针（即静态变量）\n2. 每个对象的属性指针（即对象变量）\n3. 普通对象数组的每个元素指针\n\n## array length\n如果对象是一个数组，那么对象头还需要有额外的空间用于存储数组的长度，这部分数据的长度也随着JVM架构的不同而不同：32位的JVM上，长度为32位；64位JVM则为64位。64位JVM如果开启+UseCompressedOops选项，该区域长度也将由64位压缩至32位。\n\n\n# 参考文章\n\n- https://www.jianshu.com/p/3d38cba67f8b"
  },
  {
    "path": "Java-JVM/内存分配与回收策略.md",
    "content": "\n* [对象优先在 Eden 分配](#对象优先在-eden-分配)\n* [大对象直接进入老年代](#大对象直接进入老年代)\n* [长期存活的对象进入老年代](#长期存活的对象进入老年代)\n* [动态对象年龄判定](#动态对象年龄判定)\n* [空间分配担保](#空间分配担保)\n\n### 对象优先在 Eden 分配\n大多数情况下，对象在新生代 Eden 上分配，当 Eden 空间不够时，发起 Minor GC。\n### 大对象直接进入老年代\n- 大对象是指需要连续内存空间的对象，最典型的大对象是那种很长的字符串以及数组。\n- 经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象\n- -XX:PretenureSizeThreshold，大于此值的对象直接在老年代分配，避免在 Eden 和 Survivor 之间的大量内存复制。\n### 长期存活的对象进入老年代\n- 为对象定义年龄计数器，对象在 Eden 出生并经过 Minor GC 依然存活，将移动到 Survivor 中，年龄就增加 1 岁，增加到一定年龄则移动到老年代中。\n- -XX:MaxTenuringThreshold 用来定义年龄的阈值\n### 动态对象年龄判定\n虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代，如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半，则年龄大于或等于该年龄的对象可以直接进入老年代，无需等到MaxTenuringThreshold 中要求的年龄。\n### 空间分配担保\n- 在发生 Minor GC 之前，虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间，如果条件成立的话，那么 Minor GC 可以确认是安全的\n- 如果不成立的话则虚拟机会先查看- XX：HandlePromotionFailure参数的设置值是否允许担保失败，如果允许那么就会继续检查老年代 最大可用的连续空间是否大于历次晋升到老年代对象的平均大小，如果大于，将尝试着进行一次 Minor GC；如果小于，或者-XX： HandlePromotionFailure设置不允许冒险，那么就要进行一次 Full GC。\n  - 解释一下“冒险”是冒了什么风险：前面提到过，新生代使用复制收集算法，但为了内存利用率， 只使用其中一个Survivor空间来作为轮换备份，因此当出现大量对象在Minor GC后仍然存活的情况 ——最极端的情况就是内存回收后新生代中所有对象都存活，需要老年代进行分配担保，把Survivor无 法容纳的对象直接送入老年代，这与生活中贷款担保类似。老年代要进行这样的担保，前提是老年代 本身还有容纳这些对象的剩余空间，但一共有多少对象会在这次回收中活下来在实际完成内存回收之 前是无法明确知道的，所以只能取之前每一次回收晋升到老年代对象容量的平均大小作为经验值，与 老年代的剩余空间进行比较，决定是否进行Full GC来让老年代腾出更多空间。"
  },
  {
    "path": "Java-JVM/内存结构.md",
    "content": "# 内存结构\n## JDK1.7和1.8对比\n<img src=\"../img/jvm/java7内存结构.png\" alt=\"java7内存结构\"  width=\"50%\" /><img src=\"../img/jvm/java8内存结构.png\" alt=\"java8内存结构\"  width=\"50%\" />\n\n# 程序计数器\n- 程序计数器（Program Counter Register）是一块较小的内存空间，它可以看作是当前线程所执行的 字节码的行号指示器\n- 字节码解释器工作时就是通过改变这个计数器 的值来选取下一条需要执行的字节码指令\n# Java虚拟机栈\n虚拟机栈描述的是Java方法执行的线程内存模型：\n- 每个方法被执行的时候，Java虚拟机都 会同步创建一个栈帧[1]（Stack Frame）用于存储局部变量表、操作数栈、动态连接、方法出口等信 息\n- 每一个方法被调用直至执行完毕的过程，就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。\n- 可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小，在 JDK 1.4 中默认为 256K，而在 JDK1.5+ 默认为 1M：\n  - java -Xss2M HackTheJava\n- 异常\n  - 当线程请求的栈深度超过最大值，会抛出 StackOverflowError 异常；\n  - 栈进行动态扩展时如果无法申请到足够内存，会抛出 OutOfMemoryError 异常。\n# 本地方法栈\n本地方法栈（Native Method Stacks）与虚拟机栈所发挥的作用是非常相似的，其区别只是虚拟机 栈为虚拟机执行Java方法（也就是字节码）服务，而本地方法栈则是为虚拟机使用到的本地（Native） 方法服务。\n# 堆\n“几乎”所有的对象实例都在这里分配内存\n## 堆的分代（g1前）\n- 新生代 Eden:Survivor=8:1:1\n  - Eden\n  - Survivor\n    - to\n    - from\n- 老年代\n- JVM参数 新生代：老年代=1：2\n  - -Xms 初始值\n  - -Xmx 最大值\n\n## 新的堆内存结构\n\nG1 之前的 JVM 内存模型\n\n<img src=\"../img/jvm/g1之前的垃圾收集器.png\" align=\"center\"  width=\"60%\" />\n\nG1收集器的内存模型\n\n<img src=\"../img/jvm/G1内存布局.png\" align=\"center\"  width=\"60%\" />\n\n堆内存会被切分成为很多个固定大小区域（Region），每个是连续范围的虚拟内存。\n\n堆内存中一个区域 (Region) 的大小，可以通过 -XX:G1HeapRegionSize 参数指定，大小区间最小 1M 、最大 32M ，总之是 2 的幂次方。\n\n默认是将堆内存按照 2048 份均分。\n\n每个 Region 被标记了 E、S、O 和 H，这些区域在逻辑上被映射为 Eden，Survivor 和老年代。\n\n存活的对象从一个区域转移（即复制或移动）到另一个区域。区域被设计为并行收集垃圾，可能会暂停所有应用线程。如上图所示，区域可以分配到 Eden，survivor 和老年代。\n\n此外，还有第四种类型，被称为巨型区域（Humongous Region）。\n\nHumongous 区域主要是为存储超过 50% 标准 region 大小的对象设计，它用来专门存放巨型对象。如果一个 H 区装不下一个巨型对象，那么 G1 会寻找连续的 H 分区来存储。为了能找到连续的 H 区，有时候不得不启动 Full GC 。\n\n## 逃逸分析\n逃逸分析(Escape Analysis)是目前Java虚拟机中比较前沿的优化技术。这是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析，Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上\n- 逃逸分析的基本行为就是分析对象动态作用域\n- 举例\n  - 方法逃逸\n    - 当一个对象在方法中被定义后，它可能被外部方法所引用，例如作为调用参数传递到其他地方中，称为方法逃逸\n    - 代码\n      - ```java\n        public static StringBuffer craeteStringBuffer(String s1, String s2) {\n           StringBuffer sb = new StringBuffer();\n           sb.append(s1);\n           sb.append(s2);\n           return sb;\n        }\n        ```\n      - StringBuffer sb是一个方法内部变量，上述代码中直接将sb返回，这样这个StringBuffer有可能被其他方法所改变，这样它的作用域就不只是在方法内部，虽然它是一个局部变量，称其逃逸到了方法外部\n      - 上述代码如果想要StringBuffer sb不逃出方法，可以这样写\n      - ```java\n        public static String createStringBuffer(String s1, String s2) {\n            StringBuffer sb = new StringBuffer();\n            sb.append(s1);\n            sb.append(s2);\n            return sb.toString();\n        }\n        ```\n      - 不直接返回 StringBuffer，那么StringBuffer将不会逃逸出方法\n  - 线程逃逸\n    - 甚至还有可能被外部线程访问到，譬如赋值给类变量或可以在其他线程中访问的实例变量，称为线程逃逸\n- 使用逃逸分析，编译器可以对代码做如下优化\n  - 一、同步省略。如果一个对象被发现只能从一个线程被访问到，那么对于这个对象的操作可以不考虑同步。\n  - 二、将堆分配转化为栈分配。如果一个对象在子程序中被分配，要使指向该对象的指针永远不会逃逸，对象可能是栈分配的候选，而不是堆分配。\n    - JVM参数\n      - -XX:+DoEscapeAnalysis ： 表示开启逃逸分析\n      - -XX:-DoEscapeAnalysis ： 表示关闭逃逸分析\n    - 对象的栈上内存分配\n      - 我们知道，在一般情况下，对象和数组元素的内存分配是在堆内存上进行的。但是随着JIT编译器的日渐成熟，很多优化使这种分配策略并不绝对，JIT编译器就可以在编译期间根据逃逸分析的结果，来决定是否可以将对象的内存分配从堆转化为栈\n  - 三、分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到，那么对象的部分（或全部）可以不存储在内存，而是存储在CPU寄存器中。\n# 方法区\n## 1.7\n\n<img src=\"../img/jvm/java7堆和方法区.png\" align=\"center\"  width=\"50%\" />\n\n用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。\n\nHotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小，因为它受到很多因素影响，并且每次Full GC 之后永久代的大小都会改变，所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区，从 JDK\n\n## 1.8 \n开始，移除永久代，并把方法区移至元空间，它位于本地内存中，而不是虚拟机内存中。\n\n## 方法区的回收\n- 因为方法区主要存放永久代对象，而永久代对象的回收率比新生代低很多，所以在方法区上进行回收性价比不高\n- 主要是对常量池的回收和对类的卸载\n- 为了避免内存溢出，在大量使用反射和动态代理的场景都需要虚拟机具备类卸载功能\n- 类的卸载条件很多，需要满足以下三个条件，并且满足了条件也不一定会被卸载\n  - 该类所有的实例都已经被回收，此时堆中不存在该类的任何实例。\n  - 加载该类的 ClassLoader 已经被回收。\n  - 该类对应的 Class 对象没有在任何地方被引用，也就无法在任何地方通过反射访问该类方法。\n# 运行时常量池\n运行时常量池是方法区的一部分。 \n- Class 文件中的常量池（编译器生成的字面量和符号引用）会在类加载后被放入这个区域。\n  - 运行时常量池（Runtime Constant Pool）是方法区的一部分。Class文件中除了有类的版本、字 段、方法、接口等描述信息外，还有一项信息是常量池表（Constant Pool Table），用于存放编译期生 成的各种字面量与符号引用，这部分内容将在类加载后存放到方法区的运行时常量池中。\n- 除了在编译期生成的常量，还允许动态生成，例如 String 类的 intern()。\n# 直接内存\n直接内存（Direct Memory）并不是虚拟机运行时数据区的一部分\n- 在JDK 1.4中新加入了NIO（New Input/Output）类，引入了一种基于通道（Channel）与缓冲区 （Buffer）的I/O方式，它可以使用Native函数库直接分配堆外内存，然后通过一个存储在Java堆里面的 DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能，因为避免了 在Java堆和Native堆中来回复制数据。\n  - 受到 本机总内存（包括物理内存、SWAP分区或者分页文件）大小以及处理器寻址空间的限制\n\n# 参考文章\n- https://www.hollischuang.com/archives/2398"
  },
  {
    "path": "Java-JVM/垃圾回收.md",
    "content": "# 判断一个对象能否被回收\n## 引用计数法\n- 为对象添加一个引用计数器，当对象增加一个引用时计数器加 1，引用失效时计数器减 1。引用计数为 0 的对象可被回收。\n- 在两个对象出现循环引用的情况下，此时引用计数器永远不为 0，导致无法对它们进行回收。正是因为循环引用的存在，因此 Java 虚拟机不使用引用计数算法。\n## 可达性算法\n\n<div style=\"text-align: center;\">\n    <img src=\"../img/jvm/gcRoot.png\" width=\"70%\" />\n</div>\n\n以 GC Roots 为起始点进行搜索，可达的对象都是存活的，不可达的对象可被回收。\n## 哪些可以作为是根节点\n- 虚拟机栈（栈帧中的本地变量表）中引用的对象\n- 本地方法栈中 JNI（即一般说的 Native 方法）引用的对象\n- 方法区中类静态属性引用的对象\n- 方法区中常量引用的对象\n- 正在运⾏的线程\n- 锁住的对象\n\n这种算法的优点是简单、高效，但缺点是有可能出现\"内存泄漏\"，即存在一些对象已经无用了，但是由于这些对象与GC Roots对象之间存在间接引用，导致这些对象不能被回收。\n\n除了可达性分析算法外，还有一些高级的垃圾回收算法，如G1垃圾收集器的Region To Space算法，以及ZGC垃圾收集器的读屏障算法等。这些算法都能够更加精确地判断对象是否可以被回收，以及更加高效地进行垃圾回收。\n\n## 方法区的回收\n## finalize()\n当一个对象可被回收时，如果需要执行该对象的 finalize() 方法，那么就有可能在该方法中让对象重新被引用，从而实现自救。自救只能进行一次，如果回收的对象之前调用了 finalize() 方法自救，后面回收时不会再调用该方法。\n- 经过可达性分析后如果对象没有域root有任何相关的引用链，则会被标记，随后经过一次筛选，筛选的条件就是是否需要执行finalize()方法，如果对象没有覆盖finalize方法，或者已经被虚拟机调用过。则没必要执行\n- 如果需要执行，则把对象放在一个名为F-Queue的队列中中，并由一个优先级低的由虚拟机创建的线程去执行他们的finalize()方法，这里的执行只是触发，并不一定要等待他们执行完，否则如果该方法之心缓慢甚至死循环，则会阻塞队列\n## 引用类型\n无论是通过引用计数算法判断对象的引用数量，还是通过可达性分析算法判断对象是否可达，判定对象是否可被回收都与引用有关。\n\n引用类型的种类\n### 强引用\n- 被强引用关联的对象不会被回收。\n- 使用 new 一个新对象的方式来创建强引用。`Object obj = new Object();`\n### 软引用\n  - 被软引用关联的对象只有在内存不够的情况下才会被回收。\n  - 使用 SoftReference 类来创建软引用。\n  ```java\n    Object obj = new Object();\n    SoftReference<Object> sf = new SoftReference<>(obj);\n    obj = null;//使对象只被软引用关联\n  ```\n### 弱引用\n- 被弱引用关联的对象一定会被回收，也就是说它只能存活到下一次垃圾回收发生之前。\n- 使用 WeakReference 类来创建弱引用。\n  ```java\n    Object obj = new Object();\n    WeakReference<Object> sf = new WeakReference<>(obj);\n    obj = null;\n  ```\n### 虚引用\n- 又称为幽灵引用或者幻影引用，一个对象是否有虚引用的存在，不会对其生存时间造成影响，也无法通过虚引用得到一个对象。\n- 为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知。\n- 使用 PhantomReference 来创建虚引用。\n  ```java\n    Object obj = new Object();\n    PhantomReference<Object> sf = new PhantomReference<>(obj,null);\n    obj = null;\n  ```\n# 分代收集理论\n当前商业虚拟机的垃圾收集器，大多数都遵循了“分代收集”（Generational Collection）[1]的理论进 行设计，分代收集名为理论，实质是一套符合大多数程序运行实际情况的经验法则，它建立在两个分 代假说之上：\n- 1）弱分代假说（Weak Generational Hypothesis）：绝大多数对象都是朝生夕灭的。\n- 2）强分代假说（Strong Generational Hypothesis）：熬过越多次垃圾收集过程的对象就越难以消 亡。\n\n这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则：收集器应该将Java堆划分 出不同的区域，然后将回收对象依据其年龄（年龄即对象熬过垃圾收集过程的次数）分配到不同的区 域之中存储\n\n# GC定义\n## 新生代收集（Minor GC/Young GC）\n指目标只是新生代的垃圾收集。\n- 当Eden区满了的时候，会触发Young GC\n## 老年代收集（Major GC/Old GC）\n指目标只是老年代的垃圾收集。目前只有CMS收集器会有单 独收集老年代的行为。另外请注意“Major GC”这个说法现在有点混淆，在不同资料上常有不同所指， 读者需按上下文区分到底是指老年代的收集还是整堆收集。\n## 混合收集（Mixed GC）\n指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收 集器会有这种行为。\n## 整堆收集（Full GC） \n收集整个Java堆和方法区的垃圾收集\n- 在发生Young GC的时候，虚拟机会检测之前每次晋升到老年代的平均大小是否大于年老代的剩余空间，如果大于，则直接进行Full GC；\n- 如果小于，但设置了Handle PromotionFailure，那么也会执行Full GC。\n  - -XX:HandlePromotionFailure：是否设置空间分配担保 JDK7及以后这个参数就失效了. 只要老年代的连续空间大于新生代对象的总大小或者历次晋升到老年代的对象的平均大小就进行MinorGC，否则FullGC\n- 永久代空间不足，会触发Full GC\n- System.gc()也会触发Full GC\n- 堆中分配很大的对象 所谓大对象，是指需要大量连续内存空间的java对象，例如很长的数组，此种对象会直接进入老年代，而老年代虽然有很大的剩余空间，但是无法找到足够大的连续空间来分配给当前对象，此种情况就会触发JVM进行Full GC。\n# 回收算法\n## 标记-清除\n\n<div style=\"text-align: center;\">\n    <img src=\"../img/jvm/标记清除.png\" width=\"70%\" />\n</div>\n\n### 原理\n首先标记出所有需要回 收的对象，在标记完成后，统一回收掉所有被标记的对象，也可以反过来，标记存活的对象，统一回 收所有未被标记的对象\n### 优缺点\n- 优点\n  - 简单\n- 缺点\n  - 标记和清除过程效率都不高；\n  - 会产生大量不连续的内存碎片，导致无法给大对象分配内存。\n## 标记-复制\n\n<div style=\"text-align: center;\">\n    <img src=\"../img/jvm/标记复制.png\" width=\"70%\" />\n</div>\n\n### 原理\n- 为了解决标记-清除算法面对大量可回收对象时执行效率低 的问题\n- 它将可用 内存按容量划分为大小相等的两块，每次只使用其中的一块。当这一块的内存用完了，就将还存活着 的对象复制到另外一块上面，然后再把已使用过的内存空间一次清理掉\n- 新生代采用此算法\n### 优缺点\n- 优点\n  - 不会产生内存碎片\n- 缺点\n  - 空间浪费\n  - 对象存活率较高时就要进行较多的复制操作，效率将会降低\n## 标记-整理\n\n<div style=\"text-align: center;\">\n    <img src=\"../img/jvm/标记整理.png\" width=\"70%\" />\n</div>\n\n### 原理\n- 让所有存活的对象都向一端移动，然后直接清理掉端边界以外的内存。\n- 老年代采用此算法\n### 优缺点\n- 优点\n  - 不会产生内存碎片\n- 缺点\n  - 需要移动大量对象，处理效率比较低。\n\n# Hotspot算法实现细节\n## 根节点枚举GC Roots\n- 必须暂定用户线程STW\n- 需要在一个能保障一致性的快照中进行\n  - 如果在分析过程对象引用关系不断变化肯定是不行的\n- 但是如果在此时依次枚举成百上万的关系肯定会耗费大大量的时间\n- 所以应当是能直接获取到哪些地方存放着对象的引用\n- 一个数据结构OopMap\n  - 一旦类加载完成，就会把对象内什么偏移量上是什么类型的数据计算出来，在特定的位置记录下栈里和寄存器里哪些位置是引用\n  - 相当于把引用已经存在了这里，扫描时直接来拿就好了\n## 安全点Safe Point\n- 在OopMap的协助下，HotSpot可以快速准确地完成GC Roots枚举，但一个很现实的问题随之而 来：可能导致引用关系变化，或者说导致OopMap内容变化的指令非常多，如果为每一条指令都生成 对应的OopMap，那将会需要大量的额外存储空间，这样垃圾收集伴随而来的空间成本就会变得无法 忍受的高昂。\n- 实际上HotSpot也的确没有为每条指令都生成OopMap，前面已经提到，只是在“特定的位置”记录 了这些信息，这些位置被称为安全点（Safepoint）。\n- 让线程跑到安全点再GC\n  - 抢先式中断\n    - GC时中断所有的用户线程\n    - 如果发现该线程不在安全点则恢复，直到它跑到了安全点\n      - 现在几乎没有虚拟机实现采用抢先式中断来暂停线程响应GC事件。\n  - 主动式中断\n    - 不主动中断线程，设置一个标志位，让线程自己主动轮询这个标志位，一旦发现标志位为真，就自己在最近的安全点主动中断挂起\n## 安全区域Safe Region\n如果线程未获取到处理器时间则无法到达安全点\n- 于是设置一个安全区域，在这个区域的任意地方开始GC都是安全的\n  - 执行到安全区时标识自己已进入，当离开时检查是否完成了根节点枚举，完成了继续执行，否则一直等待直到收到离开安全区的信号为止\n## 记忆集Remembered Set与卡表Card Table\n在Java虚拟机（JVM）中，垃圾收集器（GC）负责回收不再使用的对象以释放内存。Java堆内存通常被分为几个部分，主要包括：\n\n1. 年轻代（Young Generation）：新创建的对象被分配在这里，大部分对象在这里很快被回收。\n2. 老年代（Old Generation 或 Tenured Generation）：经过一定次数垃圾回收仍然存活的对象会被移到这里。\n3. 永久代（Permanent Generation，在Java 8中被元空间Metaspace取代）：存放类和方法信息等。\n\n跨代引用，即一个老年代对象引用了一个年轻代对象，或者反过来，会影响垃圾收集器的效率。在JVM中，主要使用了两种算法来处理跨代引用的问题：\n\n1. **记忆集（Remembered Set）**：用于记录从老年代到年轻代的引用。垃圾收集器在进行Minor GC（只清理年轻代的GC）时，只会检查年轻代内存。然而，如果存在老年代对象引用年轻代对象，这些年轻代的对象即使没有在年轻代内部被引用，也不能被当作垃圾回收。记忆集就是用来避免整个老年代扫描这种情况的，它记录下哪些老年代的区域包含指向年轻代对象的引用，从而只扫描这些区域。\n\n2. **卡表（Card Table）**：它是记忆集的一种实现，通常使用一个字节数组来实现。堆被分成了许多小的区域，这些区域称为“卡（cards）”。每个卡对应于记忆集中的一个条目。当老年代对象持有年轻代对象的引用时，这个引用所在的卡被标记为脏卡（dirty card），表示该区域有横跨不同代的引用。Minor GC时，垃圾收集器将只检查这些脏卡对应的老年代部分，来更新跨代引用。\n\n处理好跨代引用对于垃圾回收的性能至关重要。一个高效的GC必须尽可能快地确定哪些对象是垃圾，而跨代引用如果处理不当，会大大增加GC的时间，因此记忆集和卡表等机制是GC优化中非常关键的组成部分。\n## 写屏障Write Barrier\n- 还没有解决卡表元素如何维 护的问题，例如它们何时变脏、谁来把它们变脏等\n- 卡表元素何时变脏的答案是很明确的\n  - 有其他分代区域中对象引用了本区域对象时，其对应的 卡表元素就应该变脏\n  - 变脏时间点原则上应该发生在引用类型字段赋值的那一刻\n- 但问题是如何变 脏，即如何在对象赋值的那一刻去更新维护卡表呢？假如是解释执行的字节码，那相对好处理，虚拟 机负责每条字节码指令的执行，有充分的介入空间；但在编译执行的场景中呢？经过即时编译后的代 码已经是纯粹的机器指令流了，这就必须找到一个在机器码层面的手段，把维护卡表的动作放到每一 个赋值操作之中。\n  - 在HotSpot虚拟机里是通过写屏障（Write Barrier）技术维护卡表状态的\n  - 写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切 面[2]，在引用对象赋值时会产生一个环形（Around）通知，供程序执行额外的动作，也就是说赋值的 前后都在写屏障的覆盖范畴内\n    - 在赋值前的部分的写屏障叫作写前屏障（Pre-Write Barrier），在赋值 后的则叫作写后屏障（Post-Write Barrier）\n    - 应用写屏障后，虚拟机就会为所有赋值操作生成相应的指令，一旦收集器在写屏障中增加了更新 卡表操作，无论更新的是不是老年代对新生代对象的引用，每次只要对引用进行更新，就会产生额外 的开销，不过这个开销与Minor GC时扫描整个老年代的代价相比还是低得多的。\n## 并发的可达性分析\n### 为什么需要并发标记\n首先GC的前提是：基于一个能保证一致性的快照中，这就意味必须要STW，STW的停顿时间是和需要标记的堆的对象数量成正比的，所以能相对削弱这部分的停顿时间，收益是客观的，如果不冻结用户线程就需要GC线程和用户线程同时运行，这就需要并发标记\n### 三色标记Tri-color Marking(并发标记使用的方法)\n#### 什么是三色标记\n\n<div style=\"text-align: center;\">\n    <img src=\"../img/jvm/三色标记1.png\" width=\"70%\" />\n</div>\n\n在遍历对象图的过程中，把访问的对象都按照是否访问过标记成3种颜色\n- `白色` 还未被垃圾回收器访问过，开始阶段所有对象都是白色的，在结束阶段如果对象还是白色则不可达\n- `黑色` 对象已经被垃圾回收器访问过，且这个对象的所有引用都已经被扫描过，是安全存活的\n- `灰色` 对象被垃圾回收器访问过，但是这个对象至少存在一个引用没有被访问过\n\n**问题**：\n\n如果现在GC和用户线程同时运行，GC在标记颜色，用户线程在修改引用关系，就会出现并发标记的问题\n##### 并发标记存在的问题\n- 把原本死亡的对象标记为存活\n  - 其实这个是小问题因为这就产生了浮动的垃圾，下次清理就好了\n  - 出现浮动垃圾\n    - 本来应该被回收的对象，但是因为并发标记的原因，被标记为存活，这就产生了浮动垃圾\n    - 例子：GC线程先标记了A为存活，用户线程修改A=null,这时候A依然可达，就是浮动垃圾\n- 把原本存活的对象标记为死亡【对象消失的情况】\n  - 这个问题就非常严重了，需要的对象被回收，程序肯定会发生错误\n  - 例子：某个对象 A 在标记过程中被标记为黑色（即标记已经完成），表示 A 和 A 所引用的所有对象都已被访问。但在标记完成后，应用程序线程修改了 A 的引用，将其引用指向了一个新对象 B。此时，如果 B 尚未被标记，而 A 已经是黑色，则 B 可能永远不会被标记为存活对象。这种情况会导致对象 B 被误认为是死亡对象，即使它实际上仍然被引用并存活。\n\n##### 正常标记\n\n<div style=\"text-align: center;\">\n    <img src=\"../img/jvm/正常标记.png\" width=\"70%\" />\n</div>\n\n\n##### 并发标记-对象消失的情况\n- 正在扫描的灰色对象的一个引用被切断，与一个原本已经被扫描过的黑色对象产生了引用关系\n\n<div style=\"text-align: center;\">\n    <img src=\"../img/jvm/对象消失情况1.png\" width=\"50%\" />\n</div>\n\n- 原本是引用链的一部分，但是被切断了并与原本扫描的黑色节点产生了引用关系\n\n<div style=\"text-align: center;\">\n    <img src=\"../img/jvm/对象消失情况2.png\" width=\"50%\" />\n</div>\n\n#### 对象消失的条件\n当且仅当以下两个条件同时满足时会产生对象消失问题\n\n- 条件一 赋值器插入了一条或者多条黑色到白色对象的引用\n- 条件二 赋值器删除了全部灰色对象到白色对象的直接或间接引用\n\n这两个条件只要破坏一个即可\n\n<div style=\"text-align: center;\">\n    <img src=\"../img/jvm/对象消失的条件.png\" width=\"50%\" />\n</div>\n\n#### 对象消失的情况-解决方案\n\n##### 写屏障（Write Barrier）\n写屏障是一种技术，用于捕获应用程序线程对对象引用的修改。当应用程序修改一个引用时，写屏障可以确保新引用的对象也被正确地标记。例如：\n\n- 增量更新（Incremental Update）：当一个对象已经是黑色时，如果其引用发生了变化（指向一个新的白色对象），则写屏障会将新的引用对象标记为灰色，确保它在之后的标记过程中被扫描。增量更新破坏的是第一个条件，在新增一条引用时，将该记录保存。实际的实现中，通常是将引用相关的节点进行重新标记\n\n- 原始快照（Snapshot-at-the-Beginning，SATB）：在标记开始时快照所有的引用关系，写屏障会将旧的引用标记为灰色，即使它被更新。这可以确保所有的对象都会被正确标记，而不会因为引用更新而漏标。原始快照破坏的是第二个条件，当灰色对象要删除指向白色对象的引用关系时，就将这个要删除的引用记录下来，并发扫描结束后，在将这些记录重新扫描一次。\n\n##### 读屏障（Read Barrier）\n读屏障是在读取引用对象时触发的机制，通常用于更加激进的垃圾收集器（例如 ZGC）。当应用线程试图访问某个对象时，读屏障可以确保这个对象是被正确标记的。\n\n### 三色标记法与现代垃圾回收器\n对于读写屏障，以Java HotSpot VM为例，其并发标记时对漏标的处理方案如下：\n\n- CMS：写屏障 + 增量更新\n- G1：写屏障 + SATB（原始快照）\n- ZGC：读屏障\n\n#### 为什么G1用SATB？CMS用增量更新？\n增量更新：黑色对象新增一条指向白色对象的引用，那么要进行深入扫描白色对象及它的引用对象。\n\n原始快照：灰色对象删除了一条指向白色对象的引用，实际上就产生了浮动垃圾，好处是不需要像 CMS 那样 remark，再走一遍 root trace 这种相当耗时的流程。\n\n我的理解：SATB相对增量更新效率会高(当然SATB可能造成更多的浮动垃圾)，因为不需要在重新标记阶段再次深度扫描被删除引用对象，而CMS对增量引用的根对象会做深度扫描，G1因为很多对象都位于不同的region，CMS就一块老年代区域，重新深度扫描对象的话G1的代价会比CMS高，所以G1选择SATB不深度扫描对象，只是简单标记，等到下一轮GC再深度扫描。\n\n# 垃圾回收器\n## Serial收集器\n### 流程\n\n<div style=\"text-align: center;\">\n    <img src=\"../img/jvm/Serial收集器工作流程.png\" width=\"70%\" />\n</div>\n\n### 特点\n- 单线程\n- 停顿时间长\n- 但对应资源紧张的服务器却是最优的选择\n  - 单核\n  - 内存小\n  - 因为它没有切换线程的开销\n- 它是 Client 场景下的默认新生代收集器，因为在该场景下内存一般来说不会很大。它收集一两百兆垃圾的停顿时间可以控制在一百多毫秒以内，只要不是太频繁，这点停顿时间是可以接受的。\n## ParNew收集器\n### 流程\n\n<div style=\"text-align: center;\">\n    <img src=\"../img/jvm/ParNew收集器工作流程.png\" width=\"70%\" />\n</div>\n\n### 特点\n- 多线程\n- 实际就是Serial的多线程版本\n- 只有它能与CMS 收集器配合工作。\n## ParallelScavenge收集器\n### 特点\n- 基于标记-复制算法\n- 多线程\n- 其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间，而它的目标是达到一个可控制的吞吐量\n  - 这里的吞吐量指 CPU 用于运行用户程序的时间占总时间的比值\n- 吞吐量优先收集器\n### JVM参数\n- -XX：MaxGCPauseMillis\n  - 大于0的毫秒值\n  - 控制最大垃圾收集停顿时间，收集器尽力暴走内存回收花费的时间不超过用户设定的值\n  - 如果设置小一些，使得垃圾回收快一点，其实是通过减少新生代的大小来实现的，回收300M肯定比回收500M花的时间少，但是GC 也会变得更频繁，吞吐量就下来了\n- -XX：GCTimeRatio\n  - 直接设置吞吐量大小，也就是GC时间占总时间的比率\n  - 大于0小于100的整数\n  - 譬如把此参数设置为19，那允许的最大垃圾收集时间就占总时间的5% （即1/(1+19)），默认值为99，即允许最大1%（即1/(1+99)）的垃圾收集时间\n- -XX：+UseAdaptiveSizePolicy\n  - 激活后不需要人工指定新生代的大小、新生代和老年代的比值、晋升老年代的大小等细节参数，虚拟机会自适应，自动调节\n## SerialOld收集器\n### 流程\n\n<div style=\"text-align: center;\">\n    <img src=\"../img/jvm/SerialOld收集器工作流程.png\" width=\"70%\" />\n</div>\n\n### 特点\n- Serial的老年代版本\n- 单线程\n- 使用标记-整理算法\n## ParallelOld收集器\n### 流程\n\n<div style=\"text-align: center;\">\n    <img src=\"../img/jvm/ParallelOld收集器工作流程.png\" width=\"70%\" />\n</div>\n\n### 特点\n- Parallel Scavenge收集器的老年代版本\n- 多线程\n- 注重吞吐量\n## CMS收集器\n### 流程\n\n<div style=\"text-align: center;\">\n    <img src=\"../img/jvm/CMS收集器工作流程.png\" width=\"70%\" />\n</div>\n\n- 初始标记\n  - 仅仅只是标记一下GC Roots能直接关联到的对象，速度很快\n  - 需要STW\n- 并发标记\n  - 从GC Roots的直接关联对象开始遍历整个对 象图的过程\n  - 并发\n- 重新标记\n  - 为了修正并发标记期间，因用户程序继续运作而导致标记产生变动的那一部分对象的 标记记录，这个阶段的停顿时间通常会比初始标记阶段稍长一 些，但也远比并发标记阶段的时间短\n  - 并发\n- 并发清除\n  - 清理删除掉标记阶段判断的已经死亡的 对象，由于不需要移动存活对象，所以这个阶段也是可以与用户线程同时并发的\n  - 并发\n### 特点\n- 吞吐量低：低停顿时间是以牺牲吞吐量为代价的，导致 CPU 利用率不够高\n- 无法处理浮动垃圾，可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾，这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在，因此需要预留出一部分内存，意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾，就会出现 Concurrent Mode Failure，这时虚拟机将临时启用 Serial Old 来替代 CMS\n  - 并发阶段用户线程也在产生垃圾，只能下一次清理\n  - 浮动垃圾\n- 标记 - 清除算法导致的空间碎片，往往出现老年代空间剩余，但无法找到足够大连续空间来分配当前对象，不得不提前触发一次 Full GC。\n## G1收集器\n### 流程\n\n<div style=\"text-align: center;\">\n    <img src=\"../img/jvm/G1收集器工作流程.png\" width=\"70%\" />\n</div>\n\n- 初始标记\n  - 仅仅只是标记一下GC Roots能直接关联到的对象，并且修改TAMS 指针的值，让下一阶段用户线程并发运行时，能正确地在可用的Region中分配新对象。\n  - STW\n- 并发标记\n  - 从GC Root开始对堆中对象进行可达性分析，递归扫描整个堆 里的对象图，找出要回收的对象，这阶段耗时较长，但可与用户程序并发执行。当对象图扫描完成以 后，还要重新处理SATB记录下的在并发时有引用变动的对象\n  - 并发\n- 最终标记\n  - 对用户线程做另一个短暂的暂停，用于处理并发阶段结束后仍遗留 下来的最后那少量的SATB记录\n  - STW\n- 筛选回收\n  - 负责更新Region的统计数据，对各个Region的回 收价值和成本进行排序，根据用户所期望的停顿时间来制定回收计划，可以自由选择任意多个Region 构成回收集，然后把决定回收的那一部分Region的存活对象复制到空的Region中，再清理掉整个旧 Region的全部空间。这里的操作涉及存活对象的移动，是必须暂停用户线程，由多条收集器线程并行 完成的。\n  - STW\n### 特点\n- 主要面向服务端应用的垃圾收集器。\n- 基于Region的堆内存布局\n  - 不在基于分代，而是回收任何区域，基于哪块内存存放的垃圾数量最多，回收收益最大\n  - G1不再坚持固定大小以及固定数量的 分代区域划分，而是把连续的Java堆划分为多个大小相等的独立区域（Region），每一个Region都可以根据需要，扮演新生代的Eden空间、Survivor空间，或者老年代空间\n\n    <div style=\"text-align: center;\">\n    <img src=\"../img/jvm/G1内存布局.png\" width=\"50%\" />\n    </div>\n\n    - 收集器能够对扮演不同角色的 Region采用不同的策略去处理，这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的 旧对象都能获取很好的收集效果\n    - 跨Region引用对象如何解决\n      - 使用记忆集避免全堆作为GC Roots扫描，\n      - 但是G1的记忆集设计更复杂，在本质上是一种哈希表，key是别的region的起始位置，value是一个集合储存卡表的索引号，这里的卡表是“双向的”（卡表是我指向谁，但是这种结构还记录谁指向我）比之前的要复杂\n    - 并发标记新对象的标记\n      - 原始快照\n      - 每一个Region上都设计了两个名为TAMS（top at mark start）的指针，把region的一部分空间划分出来用于并发回收过程中的新对象的内存分配，并发回收时新分配的对象地址都必须要在这两个指针位置以上\n  - 每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免 在整个Java堆中进行全区域的垃圾收集\n- Humongous区域\n专门用来存储大对象。G1认为只要大小超过了一个 Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数-XX：G1HeapRegionSize设 定，取值范围为1MB～32MB，且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象， 将会被存放在N个连续的Humongous Region之中，G1的大多数行为都把Humongous Region作为老年代 的一部分来进行看待\n## ZGC收集器\n\nZGC（The Z Garbage Collector）是JDK 11中推出的一款低延迟垃圾回收器，它的设计目标包括：\n\n- 停顿时间不超过10ms；\n- 停顿时间不会随着堆的大小，或者活跃对象的大小而增加；\n- 支持8MB~4TB级别的堆（未来支持16TB）。\n\n从设计目标来看，我们知道ZGC适用于大内存低延迟服务的内存管理和回收。本文主要介绍ZGC在低延时场景中的应用和卓越表现，文章内容主要分为四部分：\n\n- GC之痛：介绍实际业务中遇到的GC痛点，并分析CMS收集器和G1收集器停顿时间瓶颈；\n- ZGC原理：分析ZGC停顿时间比G1或CMS更短的本质原因，以及背后的技术原理；\n- ZGC调优实践：重点分享对ZGC调优的理解，并分析若干个实际调优案例；\n- 升级ZGC效果：展示在生产环境应用ZGC取得的效果。\n\n### GC之痛\n很多低延迟高可用Java服务的系统可用性经常受GC停顿的困扰。GC停顿指垃圾回收期间STW（Stop The World），当STW时，所有应用线程停止活动，等待GC停顿结束。以美团风控服务为例，部分上游业务要求风控服务65ms内返回结果，并且可用性要达到99.99%。但因为GC停顿，我们未能达到上述可用性目标。当时使用的是CMS垃圾回收器，单次Young GC 40ms，一分钟10次，接口平均响应时间30ms。通过计算可知，有（40ms + 30ms) * 10次 / 60000ms = 1.12%的请求的响应时间会增加0 ~ 40ms不等，其中30ms * 10次 / 60000ms = 0.5%的请求响应时间会增加40ms。可见，GC停顿对响应时间的影响较大。为了降低GC停顿对系统可用性的影响，我们从降低单次GC时间和降低GC频率两个角度出发进行了调优，还测试过G1垃圾回收器，但这三项措施均未能降低GC对服务可用性的影响。\n\n### CMS与G1停顿时间瓶颈\n在介绍ZGC之前，首先回顾一下CMS和G1的GC过程以及停顿时间的瓶颈。CMS新生代的Young GC、G1和ZGC都基于标记-复制算法，但算法具体实现的不同就导致了巨大的性能差异。\n\n标记-复制算法应用在CMS新生代（ParNew是CMS默认的新生代垃圾回收器）和G1垃圾回收器中。标记-复制算法可以分为三个阶段：\n\n- 标记阶段，即从GC Roots集合开始，标记活跃对象；\n- 转移阶段，即把活跃对象复制到新的内存地址上；\n- 重定位阶段，因为转移导致对象的地址发生了变化，在重定位阶段，所有指向对象旧地址的指针都要调整到对象新的地址上。\n\n下面以G1为例，通过G1中标记-复制算法过程（G1的Young GC和Mixed GC均采用该算法），分析G1停顿耗时的主要瓶颈。G1垃圾回收周期如下图所示：\n\n<div style=\"text-align: center;\">\n    <img src=\"../img/jvm/G1垃圾回收周期.png\" width=\"70%\" />\n</div>\n\nG1的混合回收过程可以分为标记阶段、清理阶段和复制阶段。\n\n#### 标记阶段停顿分析\n\n初始标记阶段：初始标记阶段是指从GC Roots出发标记全部直接子节点的过程，该阶段是STW的。由于GC Roots数量不多，通常该阶段耗时非常短。\n并发标记阶段：并发标记阶段是指从GC Roots开始对堆中对象进行可达性分析，找出存活对象。该阶段是并发的，即应用线程和GC线程可以同时活动。并发标记耗时相对长很多，但因为不是STW，所以我们不太关心该阶段耗时的长短。\n再标记阶段：重新标记那些在并发标记阶段发生变化的对象。该阶段是STW的。\n#### 清理阶段停顿分析\n\n清理阶段清点出有存活对象的分区和没有存活对象的分区，该阶段不会清理垃圾对象，也不会执行存活对象的复制。该阶段是STW的。\n#### 复制阶段停顿分析\n\n复制算法中的转移阶段需要分配新内存和复制对象的成员变量。转移阶段是STW的，其中内存分配通常耗时非常短，但对象成员变量的复制耗时有可能较长，这是因为复制耗时与存活对象数量与对象复杂度成正比。对象越复杂，复制耗时越长。\n四个STW过程中，初始标记因为只标记GC Roots，耗时较短。再标记因为对象数少，耗时也较短。清理阶段因为内存分区数量少，耗时也较短。转移阶段要处理所有存活的对象，耗时会较长。因此，G1停顿时间的瓶颈主要是标记-复制中的转移阶段STW。为什么转移阶段不能和标记阶段一样并发执行呢？主要是G1未能解决转移过程中准确定位对象地址的问题。\n\nG1的Young GC和CMS的Young GC，其标记-复制全过程STW，这里不再详细阐述。\n\n### ZGC原理\n全并发的ZGC\n\n与CMS中的ParNew和G1类似，ZGC也采用标记-复制算法，不过ZGC对该算法做了重大改进：ZGC在标记、转移和重定位阶段几乎都是并发的，这是ZGC实现停顿时间小于10ms目标的最关键原因。\n\nZGC垃圾回收周期如下图所示\n\n<div style=\"text-align: center;\">\n    <img src=\"../img/jvm/ZGC垃圾回收周期.png\" width=\"70%\" />\n</div>\n\n\nZGC只有三个STW阶段：初始标记，再标记，初始转移。其中，初始标记和初始转移分别都只需要扫描所有GC Roots，其处理时间和GC Roots的数量成正比，一般情况耗时非常短；再标记阶段STW时间很短，最多1ms，超过1ms则再次进入并发标记阶段。即，ZGC几乎所有暂停都只依赖于GC Roots集合大小，停顿时间不会随着堆的大小或者活跃对象的大小而增加。与ZGC对比，G1的转移阶段完全STW的，且停顿时间随存活对象的大小增加而增加。\n\n### ZGC关键技术\nZGC通过着色指针和读屏障技术，解决了转移过程中准确访问对象的问题，实现了并发转移。大致原理描述如下：并发转移中“并发”意味着GC线程在转移对象的过程中，应用线程也在不停地访问对象。假设对象发生转移，但对象地址未及时更新，那么应用线程可能访问到旧地址，从而造成错误。而在ZGC中，应用线程访问对象将触发“读屏障”，如果发现对象被移动了，那么“读屏障”会把读出来的指针更新到对象的新地址上，这样应用线程始终访问的都是对象的新地址。那么，JVM是如何判断对象被移动过呢？就是利用对象引用的地址，即着色指针。下面介绍着色指针和读屏障技术细节。\n\n#### 着色指针\n> 着色指针是一种将信息存储在指针中的技术。\n\nZGC仅支持64位系统，它把64位虚拟地址空间划分为多个子空间，如下图所示：\n\n<div style=\"text-align: center;\">\n    <img src=\"../img/jvm/ZGC-虚拟地址空间.png\" width=\"70%\" />\n</div>\n\n\n其中，[0~4TB) 对应Java堆，[4TB ~ 8TB) 称为M0地址空间，[8TB ~ 12TB) 称为M1地址空间，[12TB ~ 16TB) 预留未使用，[16TB ~ 20TB) 称为Remapped空间。\n\n当应用程序创建对象时，首先在堆空间申请一个虚拟地址，但该虚拟地址并不会映射到真正的物理地址。ZGC同时会为该对象在M0、M1和Remapped地址空间分别申请一个虚拟地址，且这三个虚拟地址对应同一个物理地址，但这三个空间在同一时间有且只有一个空间有效。ZGC之所以设置三个虚拟地址空间，是因为它使用“空间换时间”思想，去降低GC停顿时间。“空间换时间”中的空间是虚拟空间，而不是真正的物理空间。后续章节将详细介绍这三个空间的切换过程。\n\n与上述地址空间划分相对应，ZGC实际仅使用64位地址空间的第0~41位，而第42~45位存储元数据，第47~63位固定为0。\n\n<div style=\"text-align: center;\">\n    <img src=\"../img/jvm/ZGC-虚拟地址空间2.png\" width=\"70%\" />\n</div>\n\n\nZGC将对象存活信息存储在42~45位中，这与传统的垃圾回收并将对象存活信息放在对象头中完全不同。\n\n#### 读屏障\n> 读屏障是JVM向应用代码插入一小段代码的技术。当应用线程从堆中读取对象引用时，就会执行这段代码。需要注意的是，仅“从堆中读取对象引用”才会触发这段代码。\n\n读屏障示例：\n```java\nObject o = obj.FieldA   // 从堆中读取引用，需要加入屏障\n<Load barrier>\nObject p = o  // 无需加入屏障，因为不是从堆中读取引用\no.dosomething() // 无需加入屏障，因为不是从堆中读取引用\nint i =  obj.FieldB  //无需加入屏障，因为不是对象引用\n```\nZGC中读屏障的代码作用：在对象标记和转移过程中，用于确定对象的引用地址是否满足条件，并作出相应动作。\n\n### ZGC并发处理演示\n接下来详细介绍ZGC一次垃圾回收周期中地址视图的切换过程：\n\n- 初始化：ZGC初始化之后，整个内存空间的地址视图被设置为Remapped。程序正常运行，在内存中分配对象，满足一定条件后垃圾回收启动，此时进入标记阶段。\n- 并发标记阶段：第一次进入标记阶段时视图为M0，如果对象被GC标记线程或者应用线程访问过，那么就将对象的地址视图从Remapped调整为M0。所以，在标记阶段结束之后，对象的地址要么是M0视图，要么是Remapped。如果对象的地址是M0视图，那么说明对象是活跃的；如果对象的地址是Remapped视图，说明对象是不活跃的。\n- 并发转移阶段：标记结束后就进入转移阶段，此时地址视图再次被设置为Remapped。如果对象被GC转移线程或者应用线程访问过，那么就将对象的地址视图从M0调整为Remapped。\n\n其实，在标记阶段存在两个地址视图M0和M1，上面的过程显示只用了一个地址视图。之所以设计成两个，是为了区别前一次标记和当前标记。也即，第二次进入并发标记阶段后，地址视图调整为M1，而非M0。\n\n着色指针和读屏障技术不仅应用在并发转移阶段，还应用在并发标记阶段：将对象设置为已标记，传统的垃圾回收器需要进行一次内存访问，并将对象存活信息放在对象头中；而在ZGC中，只需要设置指针地址的第42~45位即可，并且因为是寄存器访问，所以速度比访问内存更快。\n\n<div style=\"text-align: center;\">\n    <img src=\"../img/jvm/ZGC并发处理.png\" width=\"70%\" />\n</div>\n\n### ZGC调优实践\nZGC不是“银弹”，需要根据服务的具体特点进行调优。网络上能搜索到实战经验较少，调优理论需自行摸索，我们在此阶段也耗费了不少时间，最终才达到理想的性能。本文的一个目的是列举一些使用ZGC时常见的问题，帮助大家使用ZGC提高服务可用性\n\n#### 调优基础知识\n理解ZGC重要配置参数\n\n以我们服务在生产环境中ZGC参数配置为例，说明各个参数的作用：\n\n重要参数配置样例：\n```java\n-Xms10G -Xmx10G \n-XX:ReservedCodeCacheSize=256m -XX:InitialCodeCacheSize=256m \n-XX:+UnlockExperimentalVMOptions -XX:+UseZGC \n-XX:ConcGCThreads=2 -XX:ParallelGCThreads=6 \n-XX:ZCollectionInterval=120 -XX:ZAllocationSpikeTolerance=5 \n-XX:+UnlockDiagnosticVMOptions -XX:-ZProactive \n-Xlog:safepoint,classhisto*=trace,age*,gc*=info:file=/opt/logs/logs/gc-%t.log:time,tid,tags:filecount=5,filesize=50m \n```\n\n-Xms -Xmx：堆的最大内存和最小内存，这里都设置为10G，程序的堆内存将保持10G不变。 -XX:ReservedCodeCacheSize -XX:InitialCodeCacheSize：设置CodeCache的大小， JIT编译的代码都放在CodeCache中，一般服务64m或128m就已经足够。我们的服务因为有一定特殊性，所以设置的较大，后面会详细介绍。 -XX:+UnlockExperimentalVMOptions -XX:+UseZGC：启用ZGC的配置。 -XX:ConcGCThreads：并发回收垃圾的线程。默认是总核数的12.5%，8核CPU默认是1。调大后GC变快，但会占用程序运行时的CPU资源，吞吐会受到影响。 -XX:ParallelGCThreads：STW阶段使用线程数，默认是总核数的60%。 -XX:ZCollectionInterval：ZGC发生的最小时间间隔，单位秒。 -XX:ZAllocationSpikeTolerance：ZGC触发自适应算法的修正系数，默认2，数值越大，越早的触发ZGC。 -XX:+UnlockDiagnosticVMOptions -XX:-ZProactive：是否启用主动回收，默认开启，这里的配置表示关闭。 -Xlog：设置GC日志中的内容、格式、位置以及每个日志的大小。\n\n#### 理解ZGC触发时机\n\n相比于CMS和G1的GC触发机制，ZGC的GC触发机制有很大不同。ZGC的核心特点是并发，GC过程中一直有新的对象产生。如何保证在GC完成之前，新产生的对象不会将堆占满，是ZGC参数调优的第一大目标。因为在ZGC中，当垃圾来不及回收将堆占满时，会导致正在运行的线程停顿，持续时间可能长达秒级之久。\n\nZGC有多种GC触发机制，总结如下：\n\n- 阻塞内存分配请求触发：当垃圾来不及回收，垃圾将堆占满时，会导致部分线程阻塞。我们应当避免出现这种触发方式。日志中关键字是“Allocation Stall”。\n- 基于分配速率的自适应算法：最主要的GC触发方式，其算法原理可简单描述为”ZGC根据近期的对象分配速率以及GC时间，计算出当内存占用达到什么阈值时触发下一次GC”。自适应算法的详细理论可参考彭成寒《新一代垃圾回收器ZGC设计与实现》一书中的内容。通过ZAllocationSpikeTolerance参数控制阈值大小，该参数默认2，数值越大，越早的触发GC。我们通过调整此参数解决了一些问题。日志中关键字是“Allocation Rate”。\n- 基于固定时间间隔：通过ZCollectionInterval控制，适合应对突增流量场景。流量平稳变化时，自适应算法可能在堆使用率达到95%以上才触发GC。流量突增时，自适应算法触发的时机可能会过晚，导致部分线程阻塞。我们通过调整此参数解决流量突增场景的问题，比如定时活动、秒杀等场景。日志中关键字是“Timer”。\n- 主动触发规则：类似于固定间隔规则，但时间间隔不固定，是ZGC自行算出来的时机，我们的服务因为已经加了基于固定时间间隔的触发机制，所以通过-ZProactive参数将该功能关闭，以免GC频繁，影响服务可用性。 日志中关键字是“Proactive”。\n- 预热规则：服务刚启动时出现，一般不需要关注。日志中关键字是“Warmup”。\n- 外部触发：代码中显式调用System.gc()触发。 日志中关键字是“System.gc()”。\n- 元数据分配触发：元数据区不足时导致，一般不需要关注。 日志中关键字是“Metadata GC Threshold”。\n\n#### 理解ZGC日志\n\n一次完整的GC过程，需要注意的点已在图中标出。\n\n<div style=\"text-align: center;\">\n    <img src=\"../img/jvm/ZGC-GC日志.png\" width=\"70%\" />\n</div>\n\n\n注意：该日志过滤了进入安全点的信息。正常情况，在一次GC过程中还穿插着进入安全点的操作。\n\nGC日志中每一行都注明了GC过程中的信息，关键信息如下：\n\n- Start：开始GC，并标明的GC触发的原因。上图中触发原因是自适应算法。\n- Phase-Pause Mark Start：初始标记，会STW。\n- Phase-Pause Mark End：再次标记，会STW。\n- Phase-Pause Relocate Start：初始转移，会STW。\n- Heap信息：记录了GC过程中Mark、Relocate前后的堆大小变化状况。High和Low记录了其中的最大值和最小值，我们一般关注High中Used的值，如果达到100%，在GC过程中一定存在内存分配不足的情况，需要调整GC的触发时机，更早或者更快地进行GC。\n- GC信息统计：可以定时的打印垃圾收集信息，观察10秒内、10分钟内、10个小时内，从启动到现在的所有统计信息。利用这些统计信息，可以排查定位一些异常点。\n\n日志中内容较多，关键点已用红线标出，含义较好理解，更详细的解释大家可以自行在网上查阅资料。\n\n<div style=\"text-align: center;\">\n    <img src=\"../img/jvm/ZGC-GC日志2.png\" width=\"70%\" />\n</div>\n\n\n#### 理解ZGC停顿原因\n\n我们在实战过程中共发现了6种使程序停顿的场景，分别如下：\n\n- GC时，初始标记：日志中Pause Mark Start。\n- GC时，再标记：日志中Pause Mark End。\n- GC时，初始转移：日志中Pause Relocate Start。\n- 内存分配阻塞：当内存不足时线程会阻塞等待GC完成，关键字是”Allocation Stall”。\n\n<div style=\"text-align: center;\">\n    <img src=\"../img/jvm/ZGC-GC日志3.png\" width=\"70%\" />\n</div>\n\n- 安全点：所有线程进入到安全点后才能进行GC，ZGC定期进入安全点判断是否需要GC。先进入安全点的线程需要等待后进入安全点的线程直到所有线程挂起。\n- dump线程、内存：比如jstack、jmap命令。\n\n<div style=\"text-align: center;\">\n    <img src=\"../img/jvm/ZGC-GC日志4.png\" width=\"70%\" />\n</div>\n\n\n<div style=\"text-align: center;\">\n    <img src=\"../img/jvm/ZGC-虚拟地址空间.png\" width=\"70%\" />\n</div>\n\n\n### 调优案例\n我们维护的服务名叫Zeus，它是美团的规则平台，常用于风控场景中的规则管理。规则运行是基于开源的表达式执行引擎Aviator。Aviator内部将每一条表达式转化成Java的一个类，通过调用该类的接口实现表达式逻辑。\n\nZeus服务内的规则数量超过万条，且每台机器每天的请求量几百万。这些客观条件导致Aviator生成的类和方法会产生很多的ClassLoader和CodeCache，这些在使用ZGC时都成为过GC的性能瓶颈。接下来介绍两类调优案例。\n\n内存分配阻塞，系统停顿可达到秒级\n\n#### 案例一：秒杀活动中流量突增，出现性能毛刺\n\n日志信息：对比出现性能毛刺时间点的GC日志和业务日志，发现JVM停顿了较长时间，且停顿时GC日志中有大量的“Allocation Stall”日志。\n\n分析：这种案例多出现在“自适应算法”为主要GC触发机制的场景中。ZGC是一款并发的垃圾回收器，GC线程和应用线程同时活动，在GC过程中，还会产生新的对象。GC完成之前，新产生的对象将堆占满，那么应用线程可能因为申请内存失败而导致线程阻塞。当秒杀活动开始，大量请求打入系统，但自适应算法计算的GC触发间隔较长，导致GC触发不及时，引起了内存分配阻塞，导致停顿。\n\n解决方法：\n\n1. 开启”基于固定时间间隔“的GC触发机制：-XX:ZCollectionInterval。比如调整为5秒，甚至更短。\n2. 增大修正系数-XX:ZAllocationSpikeTolerance，更早触发GC。ZGC采用正态分布模型预测内存分配速率，模型修正系数ZAllocationSpikeTolerance默认值为2，值越大，越早的触发GC，Zeus中所有集群设置的是5。\n\n#### 案例二：压测时，流量逐渐增大到一定程度后，出现性能毛刺\n\n日志信息：平均1秒GC一次，两次GC之间几乎没有间隔。\n\n分析：GC触发及时，但内存标记和回收速度过慢，引起内存分配阻塞，导致停顿。\n\n解决方法：增大-XX:ConcGCThreads， 加快并发标记和回收速度。ConcGCThreads默认值是核数的1/8，8核机器，默认值是1。该参数影响系统吞吐，如果GC间隔时间大于GC周期，不建议调整该参数。\n\nGC Roots 数量大，单次GC停顿时间长\n\n#### 案例三： 单次GC停顿时间30ms，与预期停顿10ms左右有较大差距\n\n日志信息：观察ZGC日志信息统计，“Pause Roots ClassLoaderDataGraph”一项耗时较长。\n\n分析：dump内存文件，发现系统中有上万个ClassLoader实例。我们知道ClassLoader属于GC Roots一部分，且ZGC停顿时间与GC Roots成正比，GC Roots数量越大，停顿时间越久。再进一步分析，ClassLoader的类名表明，这些ClassLoader均由Aviator组件生成。分析Aviator源码，发现Aviator对每一个表达式新生成类时，会创建一个ClassLoader，这导致了ClassLoader数量巨大的问题。在更高Aviator版本中，该问题已经被修复，即仅创建一个ClassLoader为所有表达式生成类。\n\n解决方法：升级Aviator组件版本，避免生成多余的ClassLoader。\n\n#### 案例四：服务启动后，运行时间越长，单次GC时间越长，重启后恢复\n\n日志信息：观察ZGC日志信息统计，“Pause Roots CodeCache”的耗时会随着服务运行时间逐渐增长。\n\n分析：CodeCache空间用于存放Java热点代码的JIT编译结果，而CodeCache也属于GC Roots一部分。通过添加-XX:+PrintCodeCacheOnCompilation参数，打印CodeCache中的被优化的方法，发现大量的Aviator表达式代码。定位到根本原因，每个表达式都是一个类中一个方法。随着运行时间越长，执行次数增加，这些方法会被JIT优化编译进入到Code Cache中，导致CodeCache越来越大。\n\n解决方法：JIT有一些参数配置可以调整JIT编译的条件，但对于我们的问题都不太适用。我们最终通过业务优化解决，删除不需要执行的Aviator表达式，从而避免了大量Aviator方法进入CodeCache中。\n\n值得一提的是，我们并不是在所有这些问题都解决后才全量部署所有集群。即使开始有各种各样的毛刺，但计算后发现，有各种问题的ZGC也比之前的CMS对服务可用性影响小。所以从开始准备使用ZGC到全量部署，大概用了2周的时间。在之后的3个月时间里，我们边做业务需求，边跟进这些问题，最终逐个解决了上述问题，从而使ZGC在各个集群上达到了一个更好表现。\n\n### 升级ZGC效果\n#### 延迟降低\nTP(Top Percentile)是一项衡量系统延迟的指标：TP999表示99.9%请求都能被响应的最小耗时；TP99表示99%请求都能被响应的最小耗时。\n\n在Zeus服务不同集群中，ZGC在低延迟（TP999 < 200ms）场景中收益较大：\n\n- TP999：下降12~142ms，下降幅度18%~74%。\n- TP99：下降5~28ms，下降幅度10%~47%。 \n\n超低延迟（TP999 < 20ms）和高延迟（TP999 > 200ms）服务收益不大，原因是这些服务的响应时间瓶颈不是GC，而是外部依赖的性能。\n\n#### 吞吐下降\n对吞吐量优先的场景，ZGC可能并不适合。例如，Zeus某离线集群原先使用CMS，升级ZGC后，系统吞吐量明显降低。究其原因有二：第一，ZGC是单代垃圾回收器，而CMS是分代垃圾回收器。单代垃圾回收器每次处理的对象更多，更耗费CPU资源；第二，ZGC使用读屏障，读屏障操作需耗费额外的计算资源。\n\n### 总结\nZGC作为下一代垃圾回收器，性能非常优秀。ZGC垃圾回收过程几乎全部是并发，实际STW停顿时间极短，不到10ms。这得益于其采用的着色指针和读屏障技术。\n\nZeus在升级JDK 11+ZGC中，通过将风险和问题分类，然后各个击破，最终顺利实现了升级目标，GC停顿也几乎不再影响系统可用性。\n\n最后推荐大家升级ZGC，Zeus系统因为业务特点，遇到了较多问题，而风控其他团队在升级时都非常顺利。\n\n## Shenandoah GC\nShenandoah GC是一种面向高吞吐量、低延迟的垃圾回收器，它与G1 GC类似，也是基于标记-清除算法。与G1 GC不同的是，Shenandoah GC在运行过程中会自动调整堆内存大小，以适应不同负载。同时，Shenandoah GC支持在运行过程中动态调整GC参数，以适应不同负载。\n\n### 1. Shenandoah GC 的最大特点\n- 并发压缩：Shenandoah GC 的最大特点是其**并发压缩（Concurrent Compaction）**能力。在垃圾收集过程中，Shenandoah GC 可以与应用线程并发地压缩堆内存空间。这意味着在整个垃圾收集过程中，只有极短的停顿时间，因为传统 GC 中需要长时间暂停应用线程来进行内存压缩的部分被并发完成了。\n\n- 极低停顿时间：Shenandoah GC 设计的核心目标是将垃圾收集的停顿时间控制在 10 毫秒以下，即使在大内存（如数百 GB）的情况下，也能实现这一目标。\n\n### 2. Shenandoah GC 的其他特点\n- 区域化堆布局：与 G1 GC 类似，Shenandoah GC 也将堆分成多个区域（Region），这些区域可以独立地进行垃圾收集和压缩。\n\n- 并发阶段：Shenandoah GC 的大部分垃圾收集工作在并发阶段完成，包括标记、更新引用、压缩等操作。这减少了应用线程的停顿时间。\n\n- 标记-清除-压缩算法：Shenandoah 使用标记-清除-压缩的算法，其中压缩阶段是并发的。这是它实现低停顿的关键之一。\n\n- “Brooks Pointer”：Shenandoah 使用一种称为 “Brooks Pointer” 的技术来支持并发压缩。每个对象都有一个额外的指针（Brooks Pointer），指向其自身或移动后的新位置，这样即使在压缩过程中，应用线程也能正确地访问对象。\n\n### 3. Shenandoah GC 与其他垃圾收集器的区别\n#### 3.1 与 G1 GC 的区别\n- 停顿时间：\n  - G1 GC：G1 GC 的设计目标是提供可预测的停顿时间，它通过增量收集来减少长时间停顿。G1 GC 的堆压缩在混合收集阶段进行，可能会引发较长的停顿。\n  - Shenandoah GC：Shenandoah GC 在所有阶段都尽量并发执行，特别是压缩阶段，这使得 Shenandoah GC 的停顿时间更短。\n\n- 并发压缩：\n\n  - G1 GC：在 G1 GC 中，老年代的压缩（或者说堆压缩）会导致较长时间的停顿。\n  - Shenandoah GC：Shenandoah 的压缩是并发进行的，因此不会导致长时间的停顿。\n#### 3.2 与 ZGC 的区别\n- 支持范围：\n\n  - ZGC：ZGC 主要设计用于处理超大堆（TB 级）的内存场景，并且也以极低的停顿时间为目标。ZGC 使用的是基于颜色指针的算法（Colored Pointers）和读屏障（Read Barriers）。\n  - Shenandoah GC：Shenandoah 支持的堆大小虽然大，但不及 ZGC 的 TB 级别。Shenandoah 的目标是提供低停顿时间的同时在大多数场景中都能高效运行。\n\n- 复杂性：\n  - ZGC：ZGC 的实现相对复杂，它需要操作系统和硬件支持（如页表保护和内存映射）来管理并发的内存回收和指针更新。\n  - Shenandoah GC：Shenandoah 的设计相对简单，它依赖于 \"Brooks Pointer\" 和标准的指针操作，而不是像 ZGC 那样复杂的机制。\n#### 3.3 与 Parallel GC 的区别\n目标：\n\n- Parallel GC：追求高吞吐量，垃圾收集时会暂停所有应用线程，进行并行收集。\n- Shenandoah GC：追求低停顿时间，通过并发收集来最大限度地减少应用线程的暂停。\n### 4. 总结\nShenandoah GC 的最大特点是其并发压缩能力，这使得它在垃圾收集过程中几乎不会长时间暂停应用线程，从而实现极低的停顿时间。与 G1 GC 和 ZGC 相比，Shenandoah GC 的目标是为中到大型堆内存提供低停顿时间的垃圾收集，同时其实现方式也相对较为直接和简单。Shenandoah 适合那些对停顿时间非常敏感的应用场景。\n\n\n# 不同版本jdk默认的垃圾收集器\n| JDK版本 | 默认垃圾收集器 |\n| --- | --- |\n| JDK 8 | Parallel |\n| JDK 11 | G1 |\n| JDK 17 | G1 |\n| JDK 21 | G1 |\n\n# 参考文章\n- https://tech.meituan.com/2020/08/06/new-zgc-practice-in-meituan.html\n- https://www.cnblogs.com/hongdada/p/14578950.html"
  },
  {
    "path": "Java-JVM/类加载机制.md",
    "content": "# 类加载机制\n### 有哪些类加载器\n![](../img/jvm/类加载器.png)\n#### 1.引导类加载器 `bootstrap classloader`\n启动类加载器主要加载的是JVM自身需要的类，这个类加载使用C++语言实现的\n#### 2.扩展类加载器 `extensions classloader`\n它负责加载`JAVA_HOME/lib/ext`目录下或者由系统变量`-Djava.ext.dir`指定位路径中的类库，开发者可以直接使用标准扩展类加载器\n#### 3.应用程序类加载器 `application classloader`\n应用程序加载器是指 Sun公司实现的`sun.misc.Launcher$AppClassLoader`。它负责加载系统类路径`java -classpath`或`-D java.class.path` 指定路径下的类库，也就是我们经常用到的classpath路径\n#### 4.自定义类加载器 `java.lang.classloder`\n继承java.lang.ClassLoader类的方式\n# 生命周期\n### 1. 加载\n通过类的完全限定名称获取定义该类的二进制字节流。\n- 其中二进制字节流可以从以下方式中获取\n  - 从 ZIP 包读取，成为 JAR、EAR、WAR 格式的基础。\n  - 从网络中获取，最典型的应用是 Applet。\n  - 运行时计算生成，例如动态代理技术，在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass的代理类的二进制字节流。\n  - 由其他文件生成，例如由 JSP 文件生成对应的 Class 类。\n- 将该字节流表示的静态存储结构转换为方法区的运行时存储结构。\n- 在内存中生成一个代表该类的 Class 对象，作为方法区中该类各种数据的访问入口\n### 2. 验证\n确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求，并且不会危害虚拟机自身的安全\n### 3. 准备\n准备阶段是正式为类变量（即静态变量，被static修饰的变量,并非实例变量）分配内存并设置类变量初 始值的阶段\n- 类变量是被 static 修饰的变量，准备阶段为类变量分配内存并设置初始值，使用的是方法区的内存\n  - 初始值一般为 0 值，例如下面的类变量 value 被初始化为 0 而不是 123。 `public static int value = 123;`\n  - 如果类变量是常量，那么它将初始化为表达式所定义的值而不是 0。例如下面的常量 value 被初始化为 123 而不是0。 `public static final int value = 123;`\n- 实例变量不会在这阶段分配内存，它会在对象实例化时随着对象一起被分配在堆中。应该注意到，实例化不是类加载的一个过程，类加载发生在所有实例化操作之前，并且类加载只进行一次，实例化可以进行多次。\n### 4. 解析\n将常量池的符号引用替换为直接引用的过程\n- 符号引用（Symbolic References）：符号引用以一组符号来描述所引用的目标，符号可以是任何 形式的字面量，只要使用时能无歧义地定位到目标即可\n- 直接引用（Direct References）：直接引用是可以直接指向目标的指针、相对偏移量或者是一个能 间接定位到目标的句柄\n\n举个例子来说，现在调用方法hello()，这个方法的地址是1234567，那么hello就是符号引用，1234567就是直接引用。\n\n在解析阶段，虚拟机会把所有的类名，方法名，字段名这些符号引用替换为具体的内存地址或偏移量，也就是直接引用。\n### 5. 初始化\n在类的class文件中，包含两个特殊的方法：`<clinit>`和`<init>`。这两个方法由编译器自动生成，分别代表类构造器和构造函数。其中构造函数可以由编程人员实现，而类构造器则由编译器自动生成。而初始化阶段则负责调用类构造器，来初始化变量和资源。\n`<clinit>`方法由编译器自动收集类的赋值动作和静态语句块（static{}块）中的语句合并生成的，它有以下特点：\n\n编译器收集的顺序由源文件中语句的顺序决定，静态语句块只能访问到在它之前定义的变量，在它之后定义的变量，它只能进行赋值操作，但不能访问。\n\n```java\npublic class Test {\n  static {\n      // 变量赋值编译可以正常通过\n      value = 2;\n      // 访问变量编译失败\n      System.out.println(i);\n  }\n\n    static int value = 1;\n}\n```\n- 虚拟机保证在子类的`<clinit>`方法执行之前，父类的`<clinit>`方法已经执行完毕。因此父类中的操作对于子类都是可见的。\n- 接口的`<clinit>`方法执行之前，不需要先执行父接口的`<clinit>`方法，只有父接口中定义的变量被使用时，父接口才会初始化。同时接口的实现类在初始化时也不会执行父接口的`<clinit>`方法。\n- ``<clinit>``方法不是必须的，如果一个类或者接口没有变量赋值和静态语句块，则编译器可以不生成`<clinit>`方法。\n- 虚拟机会保证`<clinit>`方法在多线程中被正确的加锁、同步。如果多个线程同时去初始化一个类，那么只有一个线程去执行`<clinit>`方法，其他线程会被阻塞。\n\n\n# 双亲委派模型\n该模型要求除了顶层的启动类加载器外，其它的类加载器都要有自己的父类加载器\n![](../img/jvm/类加载器.png)\n## 工作过程\n一个类加载器首先将类加载请求转发到父类加载器，只有当父类加载器无法完成时才尝试自己加载\n### 好处\n保证了基础类是唯一的\n- 它使得类有了层次的划分。就拿`java.lang.Object`来说，你加载它经过一层层委托最终是由Bootstrap ClassLoader来加载的，也就是最终都是由Bootstrap ClassLoader去找`<JAVA_HOME>\\lib`中`rt.jar`里面的`java.lang.Object`加载到JVM中。\n- 这样如果有不法分子自己造了个`java.lang.Object`,里面嵌了不好的代码，如果我们是按照双亲委派模型来实现的话，最终加载到JVM中的只会是我们`rt.jar`里面的东西，也就是这些核心的基础类代码得到了保护。因为这个机制使得系统中只会出现一个`java.lang.Object`。不会乱套了。你想想如果我们JVM里面有两个Object,那岂不是天下大乱了\n### ClassLoader用过吗？用在哪些方面\n#### 1、依赖冲突\n实际项目中如果出现需要同时依赖多个版本的jar包，可以自定义类加载器分别加载各自的依赖jar即可\n#### 2、热加载\n可以让RestartClassLoader为自定义的类加载器，其核心是loadClass的加载方式，我们发现其通过修改了双亲委托机制，默认优先从自己加载，如果自己没有加载到，从从parent进行加载。这样保证了业务代码可以优先被RestartClassLoader加载。进而通过重新加载RestartClassLoader即可完成应用代码部分的重新加载。\n#### 3、加密保护\n- 打包时加密class文件\n- 加载时使用实现解密的classloader\n\n## 怎么打破双亲委派模型？\n- 自定义类加载器，并使用自定义类加载器\n- SPI也打破了双亲委派模型\n- Tomcat的模型\n\n## Tomcat是如何隔离Web应用的？\n\nTomcat通过自定义的类加载器WebAppClassLoader打破了双亲委托机制，目的就是为了优化加载Web应用目录下的类。Tomcat 作为 Servlet 容器，它负责加载我们Servlet 类，此外它还负责加载 Servlet 所依赖的 JAR 包。并且Tomcat 本身也是也是一个 Java 程序，因此它需要加载自己的类和依赖的 JAR 包。\n\n如果Tomcat里面运行了两个Web应用程序，两个Web应用程序中有同名的Servlet，但功能不同，Tomcat需要同时加载和管理这两个同名的 Servlet 类，保证它们不会冲突，因此 Web 应用之间的类需要隔离。\n\n如果如两个 Web 应用都依赖同一个第三方的 JAR 包，比如 Spring，那 Spring 的 JAR 包被加载到内存后，Tomcat 要保证这两个 Web 应用能够共享，也就是说Spring 的 JAR 包只被加载一次，否则随着依赖的第三方JAR 包增多，JVM 的内存会膨胀。\n\n同时，跟JVM一样，我们需要隔离Tomcat本身的类和Web应用类。\n### Tomcat的类加载器的层次结构\n为了解决AppClassLoader，同类名的Servlet类只能被加载一次。Tomcat 自定义一个类加载器WebAppClassLoader， 并且给每个 Web 应用创建一个类加载器实例。Context 容器组件对应一个 Web应用，因此，每个 Context 容器负责创建和维护一个WebAppClassLoader 加载器实例。原理是，不同的加载器实例加载的类被认为是不同的类，即使它们的类名相同。这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间，每一个 Web 应用都有自己的类空间，Web 应用之间通过各自的类加载器互相隔离。\n\n![](../img/jvm/Tomcat类加载器图.png)\n\n为了Tomcat可以存放多个Web应用，Tomcat实现了Web应用的隔离，从而达到可以加载不同Web应用下相同名的Servlet类，从而编写了自己的类加载器，其内部类加载器模型如上图所示\n1. SharedClassLoader：该类加载器存在是为了解决不同Web应用之间共享类库，并且不会重复加载相同的类。它作为WebAppClassLoader的父加载器，专门加载Web应用之间的共享类。\n2. CatalinaClassloader：该类加载器专门加载Tomcat自身的类，从而和web应用的类做一个隔离。\n3. CommonClassLoad：CatalinaClassLader实现了Tomcat类和web应用类的隔离，如果二者之间需要共享一些类怎么办？这里就需要CommonClassLoad，它所加载的所有类都可以被SharedClassLoader和CatalinaClassLoader使用，从而实现web应用和tomcat对一些类的共享。\n### 补充Spring的加载问题\n在 JVM 的实现中有一条隐含的规则，默认情况下，如果一个类由类加载器 A 加载，那么这个类的依赖类也是由相同的类加载器加载。比如 Spring 作为一个 Bean 工厂，它需要创建业务类的实例，并且在创建业务类实例之前需要加载这些类。Spring 是通过调用Class.forName来加载业务类的。\n\n我在前面提到，Web 应用之间共享的 JAR 包可以交给SharedClassLoader 来加载，从而避免重复加载。Spring作为共享的第三方 JAR 包，它本身是由SharedClassLoader 来加载的，Spring 又要去加载业务类，按照前面那条规则，加载 Spring 的类加载器也会用来加载业务类，但是业务类在 Web 应用目录下，不在SharedClassLoader 的加载路径下，这该怎么办呢？\n\n于是线程上下文加载器登场了，它其实是一种类加载器传递机制。为什么叫作“线程上下文加载器”呢，因为这个类加载器保存在线程私有数据里，只要是同一个线程，一旦设置了线程上下文加载器，在线程后续执行过程中就能把这个类加载器取出来用。因此 Tomcat 为每个 Web 应用创建一个WebAppClassLoarder 类加载器，并在启动 Web 应用的线程里设置线程上下文加载器，这样 Spring 在启动时就将线程上下文加载器取出来，用来加载 Bean。\n\n# 参考文章\n- https://www.cnblogs.com/jay-wu/p/11590571.html\n- https://juejin.cn/post/6844903733952774152\n- https://blog.csdn.net/Wangdiankun/article/details/105819963\n"
  },
  {
    "path": "Java-基础/JavaIO.md",
    "content": "* [文件io](#文件io)\n  * [磁盘操作](#磁盘操作)\n  * [字节流](#字节流)\n  * [字符流](#字符流)\n* [序列化](#序列化)\n  * [什么是序列化](#什么是序列化)\n  * [序列化的实现](#序列化的实现)\n  * [案例](#案例)\n  * [transient](#transient)\n* [网络io](#网络io)\n  * [InetAddress](#inetaddress)\n  * [URL](#url)\n  * [socket](#socket)\n  * [特点](#特点)\n* [NIO](#nio)\n  * [什么是NIO](#什么是nio)\n  * [流与块](#流与块)\n  * [通道 Channel](#通道-channel)\n  * [缓冲区Buffer](#缓冲区buffer)\n  * [选择器 Selector](#选择器-selector)\n    * [选择器](#选择器)\n      * [什么是选择器](#什么是选择器)\n    * [流程](#流程)\n  * [NIO的bug](#nio的bug)\n# 文件io\n## 磁盘操作\n```java\npublic static void listAllFiles(File dir){\n    if(dir == null || !dir.exists()){\n        return;\n    }\n    if (dir.isFile()){\n        System.out.println(dir.getName());\n        return;\n    }\n    for (File file : dir.listFiles()) {\n        listAllFiles(file);\n    }\n}\n```\n## 字节流\n可以处理任意类型的数据\n```java\npublic static void copyFile(String src, String dist) throws IOException {\n    FileInputStream in = new FileInputStream(src);\n    FileOutputStream out = new FileOutputStream(dist);\n\n    byte[] buffer = new byte[20 * 1024];\n    int cnt;\n    //read()最多读取buffer.length个字节\n    //返回的是实际读取的个数\n    //返回-1的时候表示读到eof，即文件尾\n    while ((cnt = in.read(buffer, 0, buffer.length)) != -1) {\n        out.write(buffer, 0, cnt);\n    }\n    in.close();\n    out.close();\n}\n```\n- inputStream\n  - FileInputStream\n- outputStream\n  - FileOutputStream\n## 字符流\n- 只能处理字符类型的数据\n```java\npublic static void readFileContent(String filePath) throws IOException {\n    FileReader fileReader = new FileReader(filePath);\n    BufferedReader bufferedReader = new BufferedReader(fileReader);\n    String line;\n    while ((line = bufferedReader.readLine()) != null) {\n        System.out.println(line);\n    }\n    //装饰者模式使得BufferedReader 组合了一个Reader对象\n    //在调用BufferedReader的close()方法时会去调用Reader的close()方法\n    //因此只要一个close()即可\n    bufferedReader.close();\n}\n```\n\n- 编码与解码\n  - 编码就是把字符转换为字节，而解码是把字节重新组合成字符。\n- Reader\n- InputStreamReader\n实现从字节流解码成字符流\n- Writer\n- InputStreamWriter\n实现字符流编码成为字节流\n\n# 序列化\n## 什么是序列化\n- 序列化就是一种用来处理对象流的机制，将对象的内容进行流化。可以对流化后的对象进行读写操作，可以将流化后的对象传输于网络之间。序列化是为了解决在对象流读写操作时所引发的问题\n- 方便存储和传输\n- 序列化：`ObjectOutputStream.writeObject()`\n- 反序列化：`ObjectInputStream.readObject()`\n## 序列化的实现\n将需要被序列化的类实现Serialize接口\n## 案例\n```java\nprivate static class A implements Serializable{\n    private int x;\n    private String y;\n\n    public A(int x, String y) {\n        this.x = x;\n        this.y = y;\n    }\n\n    @Override\n    public String toString() {\n        return \"A{\" +\n                \"x=\" + x +\n                \", y='\" + y + '\\'' +\n                '}';\n    }\n}\n\npublic static void main(String[] args) throws IOException, ClassNotFoundException {\n    A a1= new A(123,\"abc\");\n    String objectFile = \"a1\";\n\n    ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(objectFile));\n    objectOutputStream.writeObject(a1);\n    objectOutputStream.close();\n\n    ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(objectFile));\n    A a2 = (A) objectInputStream.readObject();\n    objectInputStream.close();\n    System.out.println(a2);\n}\n```\n```text\n输出:\nA{x=123, y='abc'}\n```\n## transient\n- transient 关键字可以使一些属性不会被序列化\n- ArrayList 中存储数据的数组 elementData 是用 transient 修饰的，因为这个数组是动态扩展的，并不是所有的空间都被使用，因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法，使得可以只序列化数组中有内容的那部分数据。\n  - `private transient Object[] elementData;`\n\n# 网络io\n## InetAddress\n用于表示网络上的硬件资源，即 IP 地址；\n- InetAddress.getByName(String host);\n- InetAddress.getByAddress(byte[] address);\n## URL\n案例\n```java\npublic static void main(String[] args) throws IOException {\n    URL url = new URL(\"http://www.baidu.com\");\n    //字节流\n    InputStream is = url.openStream();\n    //字符流\n    InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8);\n    //提供缓存功能\n    BufferedReader br = new BufferedReader(isr);\n    String line;\n    while ((line = br.readLine()) != null) {\n        System.out.println(line);\n    }\n    br.close();\n}\n```\n## socket\n```java\npublic class IOServer {\n    public static void main(String[] args) throws Exception {\n        ServerSocket serverSocket = new ServerSocket(8000);\n        //接收新线程\n        new Thread(() -> {\n            while (true) {\n                try {\n                    //1-阻塞方法获取新连接\n                    Socket socket = serverSocket.accept();\n                    //2-每一个新连接都创建一个线程，负责读取数据\n                    new Thread(() -> {\n                        try {\n                            int len;\n                            byte[] data = new byte[1024];\n                            InputStream inputStream = socket.getInputStream();\n                            //3-按字节流方式读取数据\n                            while ((len = inputStream.read(data)) != -1) {\n                                System.out.println(new String(data, 0, len));\n                            }\n                        } catch (IOException e) {\n                            e.printStackTrace();\n                        }\n                    }).start();\n                } catch (Exception e) {\n                    e.printStackTrace();\n                }\n            }\n        }).start();\n    }\n}\n```\n![](../img/io/socket示意图.png)\n## 特点\n同步阻塞，一个线程只能处理一个请求，如果要处理多个请求需要开启多个线程\n\n# NIO\n## 什么是NIO\n新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的，弥补了原来的 I/O 的不足，提供了高速的、面向块的 I/O\n## 流与块\nI/O 与 NIO 最重要的区别是数据打包和传输的方式，I/O 以流的方式处理数据，而 NIO 以块的方式处理数据。\n## 通道 Channel\n- 是对原 I/O 包中的流的模拟，可以通过它读取和写入数据\n- 特点\n  - 通道与流的不同之处在于，流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类)，而通道是双向的，可以用于读、写或者同时用于读写。\n- 类型\n  - FileChannel：从文件中读写数据；\n  - DatagramChannel：通过 UDP 读写网络中数据；\n  - SocketChannel：通过 TCP 读写网络中数据；\n  - ServerSocketChannel：可以监听新进来的 TCP 连接，对每一个新进来的连接都会创建一个 SocketChannel。\n## 缓冲区Buffer\n- 发送给一个通道的所有数据都必须首先放到缓冲区中，同样地，从通道中读取的任何数据都要先读到缓冲区中。也就是说，不会直接对通道进行读写数据，而是要先经过缓冲区。\n- 缓冲区实质上是一个数组，但它不仅仅是一个数组。缓冲区提供了对数据的结构化访问，而且还可以跟踪系统的读/写进程。\n- 类型\n  - ByteBuffer\n  - CharBuffer\n  - ShortBuffer\n  - IntBuffer\n  - LongBuffer\n  - FloatBuffer\n  - DoubleBuffer\n- 缓冲区状态变量\n  - capacity：最大容量；\n  - position：当前已经读写的字节数；\n    - 下一个要被读取或者写入的元素的索引\n  - limit：还可以读写的字节数。\n    - 缓冲区中第一个不能被读或者写的位置\n- 状态变量的改变过程\n  1. 新建一个大小为 8 个字节的缓冲区，此时 position 为 0，而 limit = capacity = 8。capacity 变量不会改变\n     ![](../img/io/nio状态变量改变过程1.png)\n  2. 从输入通道中读取 5 个字节数据写入缓冲区中，此时 position 为 5，limit 保持不变。\n     ![](../img/io/nio状态变量改变过程2.png)\n  3. 在将缓冲区的数据写到输出通道之前，需要先调用 flip() 方法，这个方法将 limit 设置为当前 position，并将position 设置为 0。\n     ![](../img/io/nio状态变量改变过程3.png)\n  4. 从缓冲区中取 4 个字节到输出缓冲中，此时 position 设为 4。\n     ![](../img/io/nio状态变量改变过程4.png)\n  5. 最后需要调用 clear() 方法来清空缓冲区，此时 position 和 limit 都被设置为最初位置。\n     ![](../img/io/nio状态变量改变过程5.png)\n- 文件 NIO 实例\n```java\npublic static void fastCopy(String src, String dist) throws IOException {\n    //获取源文件的输入字节流\n    FileInputStream fin = new FileInputStream(src);\n    //获取输入字节流的文件通道\n    FileChannel fcin = fin.getChannel();\n    //获取目标文件的输出字节流\n    FileOutputStream fout = new FileOutputStream(dist);\n    //获取输出字节流的文件通道\n    FileChannel fcout = fout.getChannel();\n    //为缓冲区分配1024字节\n    ByteBuffer buffer = ByteBuffer.allocateDirect(1024);\n    while (true) {\n        //从输入通道中读取数据到缓冲区\n        int r = fcin.read(buffer);\n        //read()返回 -1 表示EOF\n        if (r == -1) {\n            break;\n        }\n        //切换读写\n        buffer.flip();\n        //把缓冲区的内容写入输出文件中\n        fcout.write(buffer);\n        //清空缓冲区\n        buffer.clear();\n    }\n}\n```\n## 选择器 Selector\n### 选择器\n#### 什么是选择器\n![](../img/io/nio选择器.png)\n- NIO 常常被叫做非阻塞 IO，主要是因为 NIO 在网络通信中的非阻塞特性被广泛使用。\n- NIO 实现了 IO 多路复用中的 Reactor 模型，一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去监听多个通道 Channel 上的事件，从而让一个线程就可以处理多个事件。\n- 通过配置监听的通道 Channel 为非阻塞，那么当 Channel 上的 IO 事件还未到达时，就不会进入阻塞状态一直等待，而是继续轮询其它 Channel，找到 IO 事件已经到达的 Channel 执行。\n- 因为创建和切换线程的开销很大，因此使用一个线程来处理多个事件而不是一个线程处理一个事件，对于 IO 密集型的应用具有很好地性能。\n- 应该注意的是，只有套接字 Channel 才能配置为非阻塞，而 FileChannel 不能，因为 FileChannel 配置非阻塞也没有意义。\n### 流程\n- 创建选择器\n`Selector selector = Selector.open();`\n- 将通道注册到选择器上\n  ```\n  ServerSocketChannel ssChannel = ServerSocketChannel.open();\n  ssChannel.configureBlocking(false);\n  ssChannel.register(selector, SelectionKey.OP_ACCEPT);\n  ```\n  - 事件\n    - `SelectionKey.OP_CONNECT`\n    - `SelectionKey.OP_ACCEPT`\n    - `SelectionKey.OP_READ`\n    - `SelectionKey.OP_WRITE`\n- 监听事件\n  `int num = selector.select();`\n  - 它会一直阻塞直到有至少一个事件到达。\n- 获取到达的事件\n```java\nSet<SelectionKey> keys = selector.selectedKets();\nIterator<SelectionKey> keyIterator = keys.iterator();\nwhile(keyIterator.hasNext()){\n    SelectionKey key = keyIterator.next();\n    if(key.isAcceptable()){\n        //....\n    } else if (key.isReadable()){\n        //....    \n    }\n    keyIterator.remove();\n}\n``` \n- 事件循环\n  因为一次 select() 调用不能处理完所有的事件，并且服务器端有可能需要一直监听事件，因此服务器端处理事件的代码一般会放在一个死循环内。\n```java\nwhile(true){\n    int num = selector.select();\n    Set<SelectionKey> keys = selector.selectedKets();\n    Iterator<SelectionKey> keyIterator = keys.iterator();\n    while(keyIterator.hasNext()){\n        SelectionKey key = keyIterator.next();\n        if(key.isAcceptable()){\n            //....\n        } else if (key.isReadable()){\n            //....    \n        }\n        keyIterator.remove();\n    }    \n}\n```\n- nio示例\n```java\npublic class NIOServer {\n    public static void main(String[] args) throws IOException {\n        Selector selector = Selector.open();\n        ServerSocketChannel ssChannel = ServerSocketChannel.open();\n        ssChannel.configureBlocking(false);\n        ssChannel.register(selector, SelectionKey.OP_ACCEPT);\n\n        ServerSocket serverSocket = ssChannel.socket();\n        InetSocketAddress address = new InetSocketAddress(\"127.0.0.1\", 8888);\n        serverSocket.bind(address);\n\n        while (true) {\n            selector.select();\n            Set<SelectionKey> keys = selector.selectedKeys();\n            Iterator<SelectionKey> keyIterator = keys.iterator();\n\n            while (keyIterator.hasNext()) {\n                SelectionKey key = keyIterator.next();\n\n                if (key.isAcceptable()) {\n                    ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();\n                    //服务器会为每个新连接创建一个SocketChannel\n                    SocketChannel sChannel = ssChannel1.accept();\n                    sChannel.configureBlocking(false);\n\n                    //这个新连接主要用于从客服端读取数据\n                    sChannel.register(selector, SelectionKey.OP_READ);\n                } else if (key.isReadable()) {\n                    SocketChannel sChannel = (SocketChannel) key.channel();\n                    System.out.println(readDataFromSocketChannel(sChannel));\n                    sChannel.close();\n                }\n                keyIterator.remove();\n            }\n        }\n    }\n\n    private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {\n        ByteBuffer buffer = ByteBuffer.allocate(1024);\n        StringBuilder data = new StringBuilder();\n        while (true) {\n            buffer.clear();\n            int n = sChannel.read(buffer);\n            if (n == -1) {\n                break;\n            }\n            buffer.flip();\n            int limit = buffer.limit();\n            char[] dst = new char[limit];\n            for (int i = 0; i < limit; i++) {\n                dst[i] = (char) buffer.get(i);\n            }\n            data.append(dst);\n            buffer.clear();\n        }\n        return data.toString();\n    }\n}\n```\n```java\npublic static class NIOClient{\n    public static void main(String[] args) throws IOException {\n        Socket socket = new Socket(\"127.0.0.1\",8888);\n        OutputStream out = socket.getOutputStream();\n        String s = \"hello world\";\n        out.write(s.getBytes(StandardCharsets.UTF_8));\n        out.close();\n    }\n}\n```\n## NIO的bug\nJDK NIO的BUG，例如臭名昭著的epoll bug，它会导致Selector空轮询，最终导致CPU 100%。官方声称在JDK1.6版本的update18修复了该问题，但是直到JDK1.7版本该问题仍旧存在，只不过该BUG发生概率降低了一些而已，它并没有被根本解决\n\n**Selector BUG出现的原因**\n\n因为poll和epoll对于突然中断的连接socket会对返回的eventSet事件集合置为EPOLLHUP或者EPOLLERR，eventSet事件集合发生了变化，这就导致Selector会被唤醒，唤醒后遍历，若Selector的轮询结果为空，也没有wakeup或新消息处理，则发生空轮询，CPU使用率100%，\n\n**Netty的解决办法**\n- 对Selector的select操作周期进行统计，每完成一次空的select操作进行一次计数，\n- 若在某个周期内连续发生N次空轮询，则触发了epoll死循环bug。\n- 重建Selector，判断是否是其他线程发起的重建请求，若不是则将原SocketChannel从旧的Selector上去除注册，重新注册到新的Selector上，并将原来的Selector关闭。\n"
  },
  {
    "path": "Java-基础/Java关键字.md",
    "content": "* [final](#final)\n  * [修饰变量](#修饰变量)\n  * [修饰方法](#修饰方法)\n  * [修饰类](#修饰类)\n  * [底层原理](#底层原理)\n  * [final关键字的好处](#final关键字的好处)\n  * [为什么匿名内部类引用外部类的变脸必须是final](#为什么匿名内部类引用外部类的变脸必须是final)\n* [static](#static)\n  * [静态变量](#静态变量)\n  * [静态方法](#静态方法)\n  * [静态语句块](#静态语句块)\n  * [静态内部类](#静态内部类)\n  * [初始化顺序](#初始化顺序)\n  * [底层原理](#底层原理-1)\n    * [final fianaly finanize](#final-fianaly-finanize)\n\n# final\n## 修饰变量\n- 声明数据为常量，可以是编译时常量，也可以是在运行时被初始化后不能被改变的常量\n  - 对于基本类型，final 使数值不变；\n  - 对于引用类型，final 使引用不变，也就不能引用其它对象，但是被引用的对象本身是可以修改的。\n- 修饰成员变量需要显式初始化\n  - 声明时初始化\n  - 构造时对其赋值\n## 修饰方法\n- 声明方法不能被子类重写。\n- private 方法隐式地被指定为 final，如果在子类中定义的方法和基类中的一个 private 方法签名相同，此时子类的方法不是重写基类方法，而是在子类中定义了一个新的方法。\n## 修饰类\n声明类不允许被继承\n## 底层原理\n- 写final域的重排序规则\n  - 在构造函数内对一个final域的写入，与随后把这个被构造对象的引用赋值给一个引用变量，这两个操作之间不能重排序。\n  - （先写入final变量，后调用该对象引用）\n  - 原因：编译器会在final域的写之后，插入一个StoreStore屏障\n- 读final域的重排序规则\n  - 初次读一个包含final域的对象的引用，与随后初次读这个final域，这两个操作之间不能重排序\n  - （先读对象的引用，后读final变量）\n  - 编译器会在读final域操作的前面插入一个LoadLoad屏障\n## final关键字的好处\n1. final关键字提高了性能。JVM和Java应用都会缓存final变量。\n2. final变量可以安全的在多线程环境下进行共享，而不需要额外的同步开销。\n3. 使用final关键字，JVM会对方法、变量及类进行优化。\n## 为什么匿名内部类引用外部类的变量必须是final\n- 匿名内部类在编译的时候会编译为一个单独的类，并且匿名内部类引用的外部类的变量会作为构造函数的参数传入\n- 如果匿名内部类能访问非final的变量，但如果修改了这个非final的变量会影响匿名内部类，因为匿名内部类感知不到数据已修改造成数据不同步\n- 就好比说你创建了我，并且给了我一个你的东西使用但是你又不让我能直接拿到(引用传递)非要我copy一个一模一样的（值传递），但是运行的时候你把这点东西改了，而我又感知不了，就造成数据不一致\n- 所以和Java8 lambda表达式需要变量为final一样，如果把值传递改为引用传递就不会报错了\n\n## chatGPT的回答\n```text\nq:AVA final的底层原理\na:\n\n在Java中，使用final关键字修饰的变量具有不可变性，即它们的值一旦被赋值就不能再被修改。final关键字可以用于变量、方法和类，但它们的底层实现方式是不同的。\n\n对于final变量，Java编译器会在编译时将它们替换为常量，即在使用到final变量的地方直接用变量的值替代。这种替换是在编译期完成的，因此不会影响运行时性能。\n\n对于final方法，它们的实现方式与普通方法基本相同，但是在子类中不能被重写。这是因为final方法在编译期已经被绑定，所以无法在运行时动态绑定到子类的方法。\n\n对于final类，它们的实现方式与普通类基本相同，但是不能被继承。这是因为final类在编译期已经被声明为不可继承，所以无法在运行时进行继承。\n\n总之，final关键字的底层实现方式是通过在编译期进行常量替换、绑定和声明为不可继承来实现的。由于这种实现方式是在编译期完成的，所以不会对运行时性能产生影响，同时也保证了final变量、方法和类的不可变性。\n```\n\n# static\n## 静态变量\n又称为类变量，也就是说这个变量属于类的，类所有的实例都共享静态变量，可以直接通过类名来访问它。静态变量在内存中只存在一份\n## 静态方法\n静态方法在类加载的时候就存在了，它不依赖于任何实例。所以静态方法必须有实现，也就是说它不能是抽象方法。\n## 静态语句块\n静态语句块在类初始化时运行一次。\n## 静态内部类\n非静态内部类依赖于外部类的实例，而静态内部类不需要\n## 初始化顺序\n- 静态变量和静态语句块优先于实例变量和普通语句块，静态变量和静态语句块的初始化顺序取决于它们在代码中的顺序。\n- 静态代码块<>静态变量>实例变量>普通语句块>构造函数的初始化\n  - 存在继承的情况下，初始化顺序为：\n    1. 父类（静态变量、静态语句块）\n    2. 子类（静态变量、静态语句块）\n    3. 父类（实例变量、普通语句块）\n    4. 父类（构造函数）\n    5. 子类（实例变量、普通语句块）\n    6. 子类（构造函数）\n- ![](../img/基础/java关键字初始化顺序1.png)\n  - 输出结果\n    ```\n    静态变量\n    静态初始化块\n    变量\n    初始化块\n    构造器\n    ``` \n- ![](../img/基础/java关键字初始化顺序2.png)\n  - 输出结果\n  ```\n       父类--静态变量\n       父类--静态初始化块\n       子类--静态变量\n       子类--静态初始化块\n       子类main方法\n       父类--变量\n       父类--初始化块\n       父类--构造器\n       i=9, j=0\n       子类--变量\n       子类--初始化块\n       子类--构造器\n       i=9,j=20\n    ```\n## 底层原理\n- 使用static修饰的变量或方法是类级别的，它们属于类本身而不是类的实例。因此，static变量和方法在类被加载时就已经被初始化，并且存储在JVM的方法区中。\n- JVM的方法区是用于存储类信息、常量、静态变量、编译器编译后的代码等数据的一块内存区域。在JVM启动时，方法区被创建，并且在整个JVM生命周期中都存在。\n- 当JVM加载一个类时，它会将类的信息读入内存，并在方法区中创建一个运行时常量池，用于存储字面量、符号引用等信息。同时，JVM会为类的静态变量分配内存并进行初始化，将它们存储在方法区中。静态方法也会在方法区中创建，并且可以直接通过类名调用。\n- 因此，使用static修饰的对象会在类加载时被初始化并存储在JVM的方法区中。它们不属于类的实例，而是属于类本身，可以通过类名直接访问。这种初始化方式具有一定的效率和内存优势，但也需要注意静态变量和方法的作用域和生命周期问题。\n\n\n\n### final fianaly finanize\n- final 用于声明属性,方法和类, 分别表示属性不可变, 方法不可覆盖, 类不可继承.\n- finally 是异常处理语句结构的一部分，表示总是执行.\n- finalize Finalize是object类中的一个方法，子类可以重写finalize()方法实现对资源的回收。垃圾回收只负责回收内存，并不负责资源的回收，资源回收要由程序员完成，Java虚拟机在垃圾回收之前会先调用垃圾对象的finalize方法用于使对象释放资源（如关闭连接、关闭文件），之后才进行垃圾回收，这个方法一般不会显式地调用，在垃圾回收时垃圾回收器会主动调用。\n"
  },
  {
    "path": "Java-基础/Java类型.md",
    "content": "* [基本类型](#基本类型)\n  * [float或者double怎么判断＝0?](#float或者double怎么判断0)\n* [包装类型](#包装类型)\n  * [包装类型的equals](#包装类型的equals)\n\n# 基本类型\n|  基本类型   | 大小  | 备注  |\n|  ----  | ----  | --- |\n| int  | 4字节 32位 | Java默认的整数类型 |\n| short  |  2字节 16位 | --- |\n| byte  | 1字节 8位 | --- |\n| float  | 4字节 32位 | --- |\n| double  | 8字节 64位 | Java默认的小数类型 |\n| long  | 8字节 64位 | --- |\n| boolean  | 1字节 |  true/false  |\n| char  | 2字节 16位 | --- |\n\n### float或者double怎么判断＝0?\n\n- 一般而言用误差内比较float为1e-6，double为1e-15，即如果Math.abs(a)<1e-6即可认为=0\n- 如果非常严格要求精度 BigDecimal\n\n# 包装类型\n\n|  包装类型   | 缓存  |\n|  ----  | ----  | \n| Integer  | IntegerCache [-128,127] | \n| Short  |  ShortCache.cache [-128,127] | \n| Byte  | [-128, 127] | \n| Float  | 4字节 32位 | \n| Double  | 8字节 64位 |\n| Long  | [-128, 127] |\n| Boolean  | TRUE FALSE | \n| Character  | [0, 127] | \n\n## 包装类型的equals\n- 包装类的equals首先会判断是否是本类型如果不是直接返回false\n- 否则比较值\n- 装箱其实就是调用了 包装类的valueOf()方法，拆箱其实就是调用了 xxxValue()方法。`Integer i = 10 等价于 Integer i = Integer.valueOf(10)\n  int n = i 等价于 int n = i.intValue()`; \n\n## 判断创建多少个对象\n因为大多包装类型都用cache，在判断时考虑是否超出了cache范围，否则会创建新对象\n\n## new Integer(1) 和 Integer.valueOf(1)的区别是什么\n\nInteger.valueOf(1)如果范围在`[-128,127]`之间是直接使用缓存不会创建对象\n```java\npublic static Integer valueOf(int i) {\n    if (i >= IntegerCache.low && i <= IntegerCache.high)\n        return IntegerCache.cache[i + (-IntegerCache.low)];\n    return new Integer(i);\n}\n```"
  },
  {
    "path": "Java-基础/Java长期支持版本.md",
    "content": "* [Java8](#java8)\n  * [Lambda表达式](#lambda表达式)\n  * [函数式接口](#函数式接口)\n  * [默认方法](#默认方法)\n  * [方法引用](#方法引用)\n  * [Stream API](#stream-api)\n    * [Collections](#collections)\n  * [Optional](#optional)\n  * [Date Time API](#date-time-api)\n  * [重复注解](#重复注解)\n  * [Base64](#base64)\n  * [JVM的新特性](#jvm的新特性)\n* [Java11](#java11)\n  * [增加了一系列好用的字符串处理方法](#增加了一系列好用的字符串处理方法)\n  * [用于 Lambda 参数的局部变量语法](#用于-lambda-参数的局部变量语法)\n  * [标准化HTTP Client](#标准化http-client)\n  * [单个命令编译运行源代码](#单个命令编译运行源代码)\n  * [ZGC](#zgc)\n# Java8\n## Lambda表达式\n```java\n语法格式:\n(parameters) -> expression 或 (parameters) -> { statements;}\n代码示例：\nArrays.asList(\"jay\",\"Eason\",\"\"SHE).foreach((s) -> System.out.print(s));\n```\n允许把函数作为一个方法的参数（函数作为参数传递到方法中）\n## 函数式接口\n- @FunctionalInterface\n```java\n@FunctionalInterface\npublic interface Runnable{\n    public abstract void run();\n}\n```\n- 只有一个函数\n## 默认方法\n默认方法就是一个在接口里面有了一个实现的方法。它允许将新方法添加到接口，但不强制实现了该接口的类必须实现新的方法。\n```java\npublic interface ISingerService{\n    //默认方法\n  default void sing(){\n     System.out.printLn(\"唱歌\");\n  }\n  void writeSong();\n}\n\npublic class JaySingerServiceImpl implements ISingerService{\n    @Override\n    public void writeSong(){\n      System.out.printLn(\"写了一首七里香\");\n    }\n}\n```\n## 方法引用 \n可以直接引用已有Java类或对象（实例）的方法或构造器。\n```java\n//利用函数式接口Consumer的accept方法实现打印,Lambda表达式如下\nConsumer<String> consumer = x -> System.out.printLn(x);\nconsumer.accept(\"jay\");\n//引用PrintStream类（也就是System.out的类型）的printLn方法，这就是方法引用\nconsumer = System.out::println;\nconsumer.accept(\"sss\");\n```\n## Stream API \n### Collections\n- filter 筛选\n- map流映射\n- reduce 将流中的元素组合起来\n- collect 返回集合\n- sorted 排序\n- flatMap 流转换\n- limit返回指定流个数\n- distinct去除重复元素\n## Optional\n用来解决NullPointerException。Optional代替if...else解决空指针问题，使代码更加简洁。\n## Date Time API\nLocalDate /LocalDateTime\n## 重复注解\n重复注解，即一个注解可以在一个类、属性或者方法上同时使用多次；用@Repeatable定义重复注解\n\n```java\nimport java.lang.annotation.Repeatable;\n\n@Repeatable(ScheduleTimes.class)\npublic @interface ScheduleTime{\n    String value();\n}\n\npublic @interface ScheduleTimes{\n  ScheduleTimes[] value();\n}\n\npublic class ScheduleTimeTask{\n    \n    @ScheduleTime(\"10\")\n    @ScheduleTime(\"12\")\n    public void doSomething(){};\n}\n```\n## Base64\nJava 8把Base64编码的支持加入到官方库中~\n- `String encoded = Base64.getEncoder().encodeToString(str.getBytes( StandardCharsets.UTF_8));`\n- `String decoded = new String(Base64.getDecoder().decode(encoded), StandardCharsets.UTF_8);`\n## JVM的新特性\n使用元空间Metaspace代替持久代（PermGen space），JVM参数使用-XX:MetaSpaceSize和-XX:MaxMetaspaceSize设置大小。\n# Java11\n## 增加了一系列好用的字符串处理方法\n- isBlank() 判空。\n- strip() 去除首尾空格\n- stripLeading() 去除字符串首部空格\n- stripTrailing() 去除字符串尾部空格\n- lines() 分割获取字符串流。\n- repeat() 复制字符串\n## 用于 Lambda 参数的局部变量语法\n```java\nvar map = new HashMap<String,Object>();\nmap.put(\"aaa\",\"aaa\");\nmap.forEach((k,v)->{\n    System.out.println(k+\":\"+v);    \n})\n```\n## 标准化HTTP Client\nJava 9 引入Http Client API,Java 10对它更新，Java 11 对它进行标准化。这几个版本后，Http Client几乎被完全重写，支持HTTP/1.1和HTTP/2 ，也支持 websockets。\n## 单个命令编译运行源代码\n- Java 11之前\n  - // 编译 `javac Jay.java`\n  - // 运行 `java Jay`\n- Java 11之后 `java Jay.java`\n## ZGC\n可伸缩低延迟垃圾收集器\n- GC 停顿时间不超过 10ms\n- 既能处理几百 MB 的小堆，也能处理几个 TB 的大堆\n- 应用吞吐能力不会下降超过 15%（与 G1 回收算法相比）\n- 方便在此基础上引入新的 GC 特性和利用 colord\n- 针以及 Load barriers 优化奠定基础\n- 当前只支持 Linux/x64 位平台\n\n# Java17\n\n## 封闭类和接口（Sealed Classes and Interfaces, JEP 409）：\n\n引入封闭类和接口，允许开发者限制哪些类可以扩展或实现它们。这有助于设计更可预测的类层次结构。\n\n语法示例：\n```java\n\npublic sealed class Shape permits Circle, Square { }\n```\n\n## 模式匹配 for switch (Pattern Matching for switch, Preview, JEP 406)：\n\n扩展 switch 语句，以支持基于模式匹配的分支操作，这使得 switch 更加灵活和强大。\n\n语法示例：\n```java\nswitch (obj) {\n  case Integer i -> System.out.println(\"Integer: \" + i);\n  case String s -> System.out.println(\"String: \" + s);\n  default -> System.out.println(\"Other: \" + obj);\n}\n```\n## 强度减少（Strong Encapsulation, JEP 403）：\n\n更严格地限制模块化系统中的反射访问，使默认情况下不能通过反射访问模块的内部 API。\n## 外部函数和内存 API (Foreign Function & Memory API, Incubator, JEP 412)：\n\n引入新的 API，使 Java 程序能够安全、高效地调用非 Java 代码（如 C 函数），并操作非堆内存。\n## 增强的伪随机数生成器（Enhanced Pseudo-Random Number Generators, JEP 356）：\n\n增加了一组新的接口和实现，以提高伪随机数生成的灵活性和可扩展性。\n## 弃用和移除：\n\n永久删除 Applets，并移除 RMI Activation，这些旧的功能已经不再使用。\n\n# Java21\n\n## 结构化并发（Structured Concurrency, JEP 428）：\n\n通过将多个任务作为单个结构化单元来管理，以简化多线程编程，增强可读性和可维护性。\n## 模式匹配 for switch (Pattern Matching for switch, JEP 441)：\n\n这是对 Java 17 中引入的模式匹配 switch 的正式化，在 Java 21 中已经成为标准功能。\n## 记录模式（Record Patterns, JEP 440）：\n\n扩展了模式匹配功能，使其能够匹配 Record 类型，方便解构和处理 Record 类的字段。\n## 虚拟线程（Virtual Threads, JEP 444）：\n\n引入了虚拟线程的概念，极大地简化了并发编程，使得创建和管理大量线程变得更加轻量级。\n## String 模式匹配和增强（String Templates, JEP 430）：\n\n新增了字符串模板功能，简化了字符串插值操作。\n## 外部函数和内存 API (Foreign Function & Memory API, JEP 442)：\n\n这项功能在 Java 17 中作为孵化器引入，而在 Java 21 中则被进一步增强和正式化。\n## 垃圾收集器改进：\n\n在垃圾收集器方面，特别是 ZGC 和 G1 等垃圾收集器，做了大量优化以提升性能和稳定性。\n## 向量 API (Vector API, JEP 438)：\n\n进一步改进和稳定，允许开发者利用向量化硬件加速批量计算任务"
  },
  {
    "path": "Java-基础/Object.md",
    "content": "\n* [equals](#equals)\n* [hashcode](#hashcode)\n* [toString](#tostring)\n* [clone](#clone)\n* [wait](#wait)\n* [notify](#notify)\n* [finalize](#finalize)\n* [重写了equals后为什么要重写hashcode，如果不重写，会有什么影响](#重写了equals后为什么要重写hashcode如果不重写会有什么影响)\n\n# equals\n- 对于基本类型，== 判断两个值是否相等，基本类型没有 equals() 方法。\n- 对于引用类型，== 判断两个变量是否引用同一个对象（即是否指向同一个内存地址），而 equals() 判断引用的对象是否等价(默认情况下，Object 类中的 equals() 方法与 == 相同，但许多类重写了这个方法，需要判断重写后的方法返回的内容来判断是否相同来判断对象是否相等)。\n# hashcode\n- hashCode() 返回散列值，而 equals() 是用来判断两个对象是否等价。等价的两个对象散列值一定相同，但是散列值相同的两个对象不一定等价。\n- 在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法，保证等价的两个对象散列值也相等。\n  - 重写equals的一般步骤 \n    - 检查两个对象是否为同一个对象，如果是，则返回true。 \n    - 检查传入的对象是否为空，如果是，则返回false。 \n    - 检查传入的对象是否与当前对象属于同一类，如果不是，则返回false。 \n    - 将传入的对象转换成当前对象的类型。 \n    - 比较两个对象的属性是否相等，如果都相等，则返回true，否则返回false。\n- hashCode方法实际上必须要完成的一件事情就是，为该equals方法认定为相同的对象返回相同的哈希值\n# toString\n- 一般生成对象字符串\n# clone\n- 浅拷贝\n- 引用一个对象，拷贝基本数据类型的值，引用类型只拷贝引用\n- 深拷贝\n- 引用不同对象，拷贝基本数据类型的值，引用类型创建新对象\n# wait\n- 挂起线程\n# notify\n- 唤醒线程\n# finalize\n- GC时调用，回收资源\n\n# 重写了equals后为什么要重写hashcode，如果不重写，会有什么影响\n- 每个覆盖了equals方法的类中，必须覆盖hashCode。如果不这么做，就违背了hashCode的通用约定。进而导致该类无法结合所以与散列的集合一起正常运作，这里指的是HashMap、HashSet、HashTable、ConcurrentHashMap。\n  - hashcode源码注释中的大致意思是：当我们将equals方法重写后有必要将hashCode方法也重写，这样做才能保证不违背hashCode方法中“相同对象必须有相同哈希值”的约定\n- 比如hashmap里的put操作就会判断key的hashcode是否相等，如果出现重写了equals但未重写hashcode，则可能出现这种情况：hashMap.put(\"k\",\"v1\")，hashMap.put(\"k\":\"v2\")，而不是使用v2替换v1的值，这样我们的HashMap就乱套了"
  },
  {
    "path": "Java-基础/String.md",
    "content": "\n* [final](#final)\n  * [不可变的原因](#不可变的原因)\n* [储存数据](#储存数据)\n* [string pool](#string-pool)\n  * [new String(\"abc\")会创建几个对象](#new-stringabc会创建几个对象)\n  * [intern()](#intern)\n  * [str1 + \" a nice day\"](#str1---a-nice-day)\n  * [\"a\" + \"b\" + \"c\"](#a--b--c)\n* [StringBuilder](#stringbuilder)\n* [StringBuffer](#stringbuffer)\n\n# final\n## 不可变的原因\n在Java中，String类型被设计成final的主要原因是为了保证字符串的不可变性。这意味着一旦一个字符串被创建，它的值就不能再被改变。这种不可变性带来了很多好处，例如：\n\n- 线程安全：由于字符串是不可变的，所以在多线程环境下，不需要担心并发访问的问题。\n\n- 缓存哈希值：由于字符串的哈希值是不可变的，所以可以在哈希表等数据结构中缓存字符串的哈希值，提高性能。\n\n- 安全性：不可变的字符串可以防止在处理字符串时，因为修改字符串而导致的潜在安全问题，例如SQL注入攻击。\n\n- 简化代码：由于字符串是不可变的，所以在编写代码时，不需要担心字符串值被改变的情况，这使得代码更加简单和可读。\n\n此外，String类型作为Java中最常用的类型之一，设计成final还可以提高字符串的性能，因为编译器可以对字符串的操作进行一些优化，例如重复使用相同的字符串对象、避免创建新的字符串对象等。\n# 储存数据\n- Java8内部使用char数组储存数据\n  - `private final char value[];`\n- Java9后使用byte数组同时使用coder来标识使用哪种编码\n  - `private final byte[] value;`\n  - `private final byte coder;`\n# string pool\n### new String(\"abc\")会创建几个对象\n- 使用这种方式一共会创建两个字符串对象（前提是 String Pool 中还没有 \"abc\" 字符串对象）。\n- \"abc\" 属于字符串字面量，因此编译时期会在 String Pool 中创建一个字符串对象，指向这个 \"abc\" 字符串字面量；\n- 而使用 new 的方式会在堆中创建一个字符串对象。\n### intern()\n字符串常量池（String Pool）保存着所有字符串字面量（literal strings），这些字面量在编译时期就确定。不仅如此，还可以使用 String 的 intern() 方法在运行过程中将字符串添加到 String Pool 中。当一个字符串调用 intern() 方法时，如果 String Pool 中已经存在一个字符串和该字符串值相等（使用 equals() 方法 进行确定），那么就会返回 String Pool 中字符串的引用；否则，就会在 String Pool 中添加一个新的字符串，并返回这个新字符串的引用。\n```\n下面示例中，s1 和 s2 采用 new String() 的方式新建了两个不同字符串，而 s3 和 s4 是通过 s1.intern() 方法取得一\n个字符串引用。intern() 首先把 s1 引用的字符串放到 String Pool 中，然后返回这个字符串引用。因此 s3 和 s4 引用\n的是同一个字符串。\nString s1 = new String(\"aaa\"); \nString s2 = new String(\"aaa\"); \nSystem.out.println(s1 == s2); // false\n \nString s3 = s1.intern(); \nString s4 = s1.intern(); \nSystem.out.println(s3 == s4); // true\n\n如果是采用 \"bbb\" 这种字面量的形式创建字符串，会自动地将字符串放入 String Pool 中。\nString s5 = \"bbb\"; \nString s6 = \"bbb\"; \nSystem.out.println(s5 == s6); // true\n```\n### str1 + \" a nice day\"\n- 编译为 new StringBuilder().append(str1).append(\" a nice day\");\n- 但是如果str1被final修饰，此变量会在初始化时加载到常量池，所以会直接变为str1的值\"value\"+\"a nice day\"\n### \"a\" + \"b\" + \"c\"\n编译优化不会创建对象\n# StringBuilder\n- 可变 byte数组没有被final修饰\n- 线程不安全\n- 速度快\n# StringBuffer\n- 可变 byte数组没有被final修饰\n- 线程安全\n  - 方法被synchronized修饰\n- 速度慢\n\n大量字符串操作使用他们\n  - 操作少量的数据: 适用 String\n  - 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder\n  - 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer\n\n# String 的hashcode()方法\nString也是遵守equals的标准的，也就是 s.equals(s1)为true，则s.hashCode()==s1.hashCode()也为true。此处并不关注eqauls方法，而是讲解 hashCode()方法，String.hashCode()有点意思，而且在面试中也可能被问到。先来看一下代码：\n```java\npublic int hashCode() {\n        int h = hash;\n        if (h == 0 && value.length > 0) {\n            char val[] = value;\n            for (int i = 0; i < value.length; i++) {\n                h = 31 * h + val[i];\n            }\n            hash = h;\n        }\n        return h;\n    }\n```\n## 为什么要选31作为乘数呢？\n从网上的资料来看，一般有如下两个原因：\n\n- 31是一个不大不小的质数，是作为 hashCode 乘子的优选质数之一。另外一些相近的质数，比如37、41、43等等，也都是不错的选择。那么为啥偏偏选中了31呢？请看第二个原因。\n- 31可以被 JVM 优化，31 * i = (i << 5) - i。\n\n# 参考文章\n- https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247483956&idx=1&sn=1c19164967621fa5449a7830d006c8f9&scene=19#wechat_redirect"
  },
  {
    "path": "Java-基础/反射.md",
    "content": "\n* [什么是反射](#什么是反射)\n* [java.lang.reflect](#javalangreflect)\n* [反射的优缺点](#反射的优缺点)\n    * [优点](#优点)\n        * [可扩展性](#可扩展性)\n        * [类浏览器和可视化开发环境](#类浏览器和可视化开发环境)\n        * [调试器和测试工具](#调试器和测试工具)\n    * [缺点](#缺点)\n        * [性能开销](#性能开销)\n        * [安全限制](#安全限制)\n        * [内部暴露](#内部暴露)\n\n\n## 什么是反射\n- 每个类都有一个 Class 对象，包含了与类有关的信息。当编译一个新类时，会产生一个同名的 .class 文件，该文件内容保存着 Class 对象。\n- 类加载相当于 Class 对象的加载，类在第一次使用时才动态加载到 JVM 中。也可以使用Class.forName(\"com.mysql.jdbc.Driver\") 这种方式来控制类的加载，该方法会返回一个 Class 对象。\n- 反射可以提供运行时的类信息，并且这个类可以在运行时才加载进来，甚至在编译时期该类的 .class 不存在也可以加载进来。\n## java.lang.reflect\n- Field ：可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段；\n- Method ：可以使用 invoke() 方法调用与 Method 对象关联的方法；\n- Constructor ：可以用 Constructor 创建新的对象。\n## 反射的优缺点\n### 优点\n#### 可扩展性\n应用程序可以利用全限定名创建可扩展对象的实例，来使用来自外部的用户自定义类。\n#### 类浏览器和可视化开发环境\n一个类浏览器需要可以枚举类的成员。可视化开发环境（如 IDE）可以从利用反射中可用的类型信息中受益，以帮助程序员编写正确的代码。\n#### 调试器和测试工具\n调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义，以确保一组测试中有较高的代码覆盖率。\n### 缺点\n#### 性能开销\n反射涉及了动态类型的解析，所以 JVM 无法对这些代码进行优化。因此，反射操作的效率要比那些\n非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。\n#### 安全限制\n使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的\n环境中运行，如 Applet，那么这就是个问题了\n#### 内部暴露\n由于反射允许代码执行一些在正常情况下不被允许的操作（比如访问私有的属性和方法），所以使\n用反射可能会导致意料之外的副作用，这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性，因\n此当平台发生改变的时候，代码的行为就有可能也随着变化。\n\n## java反射中，Class.forName和classloader的区别\njava中class.forName()和classLoader都可用来对类进行加载。\n\nclass.forName()前者除了将类的.class文件加载到jvm中之外，还会对类进行解释，执行类中的static块。\n\n而classLoader只干一件事情，就是将.class文件加载到jvm中，不会执行static中的内容,只有在newInstance才会去执行static块。\n\nClass.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数，创建类的对象\n```java\npublic static Class<?> forName(String className)\n            throws ClassNotFoundException {\n    Class<?> caller = Reflection.getCallerClass();\n    return forName0(className, true, ClassLoader.getClassLoader(caller), caller);\n}\n```\n```java\npublic static Class<?> forName(String name, boolean initialize,ClassLoader loader)throws ClassNotFoundException{\n    Class<?> caller = null;\n    SecurityManager sm = System.getSecurityManager();\n    if (sm != null) {\n        // Reflective call to get caller class is only needed if a security manager\n        // is present.  Avoid the overhead of making this call otherwise.\n        caller = Reflection.getCallerClass();\n        if (sun.misc.VM.isSystemDomainLoader(loader)) {\n            ClassLoader ccl = ClassLoader.getClassLoader(caller);\n            if (!sun.misc.VM.isSystemDomainLoader(ccl)) {\n                sm.checkPermission(\n                    SecurityConstants.GET_CLASSLOADER_PERMISSION);\n            }\n        }\n    }\n    return forName0(name, initialize, loader, caller);\n}\n```\n# 参考文章\n- https://blog.csdn.net/qq_27093465/article/details/52262340"
  },
  {
    "path": "Java-基础/容器-collection.md",
    "content": "\n* [Collection](#collection)\n* [List](#list)\n* [常用的子类](#常用的子类)\n  * [Vector](#vector)\n  * [LinkedList](#linkedlist)\n  * [ArrayList](#arraylist)\n  * [CopyOnWriteArrayList](#copyonwritearraylist)\n  * [集合对比](#集合对比)\n* [Set](#set)\n  * [HashSet](#hashset)\n  * [LinkedHashSet](#linkedhashset)\n  * [TreeSet](#treeset)\n* [queue](#queue)\n  * [添加](#添加)\n  * [删除](#删除)\n  * [获取](#获取)\n  * [BlockingQueue](#blockingqueue)\n    * [插入](#插入)\n    * [移除](#移除)\n    * [获取数据](#获取数据)\n    * [常见的实现Queue queue = new LinkedList&lt;&gt;();](#常见的实现queue-queue--new-linkedlist)\n* [参考文章](#参考文章)\n\n# Collection\n![](../img/基础/Java容器类关系图.png)\n# List\n- 实现了Collection接口\n- List接口特性：是有序的，元素是可重复的\n- 允许元素为null\n# 常用的子类\n## Vector\n- 底层结构是数组，初始容量为10，每次增长2倍\n- 它是线程同步的，已被ArrayList替代\n- Vector 也是一个动态数组结构，一个元老级别的类，早在 jdk1.1 就引入进来了，之后在 jdk1.2 里引进 ArrayList，ArrayList 可以说是 Vector 的一个迷你版，ArrayList 大部分的方法和 Vector 比较相似！\n- 两者不同的是，Vector 中的方法都加了synchronized，保证操作是线程安全的，但是效率低，而 ArrayList 所有的操作都是非线程安全的，执行效率高，但不安全！\n- 对于 Vector，虽然可以在多线程环境下使用，但是在迭代遍历元素的时候依然会报错，抛ConcurrentModificationException异常！\n- 在 JDK 中 Vector 已经属于过时的类，官方不建议在程序中采用，如果想要在多线程环境下使用 Vector，建议直接使用并发包中的CopyOnWriteArrayList！\n- Stack\n  - Stack 是 Vector 的一个子类，本质也是一个动态数组结构，不同的是，它的数据结构是先进后出，取名叫栈！\n  - 不过，关于 Java 中 Stack 类，有很多的质疑声，栈更适合用队列结构来实现，这使得 Stack 在设计上不严谨，因此，官方推荐使用 Deque 下的类来是实现栈！\n## LinkedList\n- 底层结构是双向链表\n- 实现了Deque接口，因此我们可以像操作栈和队列一样操作它\n- 线程非同步\n- LinkedList 是一个双向链表结构，在任意位置插入、删除都很方便，但是不支持随机取值，每次都只能从一端开始遍历，直到找到查询的对象，然后返回；不过，它不像 ArrayList 那样需要进行内存拷贝，因此相对来说效率较高，但是因为存在额外的前驱和后继节点指针，因此占用的内存比 ArrayList 多一些。\n## ArrayList\n- 底层结构是数组，初始容量为10，每次增长1.5倍\n  - ArrayList的扩容\n    - 原理\n      - 调用系统函数的copy方法\n      - 一个数组，可以不断地添加元素，而不出现数组下标越界异常。怎么实现？扩容\n- 在增删时候，需要数组的拷贝复制(navite 方法由C/C++实现)，性能还是不差的！\n- 线程非同步\n- ArrayList 是一个动态数组结构，支持随机存取，在指定的位置插入、删除效率低（因为要移动数组元素）；如果内部数组容量不足则自动扩容，扩容系数为原来的1.5倍，因此当数组很大时，效率较低。扩容也是调用Arrays.copyOf方法\n- 当然，插入删除也不是效率非常低，在某些场景下，比如尾部插入、删除，因为不需要移动数组元素，所以效率也很高哦！\n- ArrayList 是一个非线程安全的类，在多线程环境下使用迭代器遍历元素时，会报错，抛ConcurrentModificationException异常！\n  - 迭代器\n    - 迭代器删除元素\n    - 单线程和多线程的区别 https://blog.csdn.net/weixin_35681869/article/details/113812708\n- addAll方法\n  - 底层使用native arraycopy方法，内存拷贝数组速度会更快\n  - 大数据量时推荐使用，小数据量时与for循环对比不明显\n- 如果要从列表的中间添加元素是怎么实现的?\n  - 将当前在该位置的元素index（如果有）和任何后续元素向右移动,也是直接使用System.arraycopy\n  - arr[index] = data\n  - size+1\n\n## CopyOnWriteArrayList\n- 原理：在修改时，复制出一个新数组，修改的操作在新数组中完成，最后将新数组交由array变量指向。\n- 写加锁，读不加锁 ReentrantLock\n- 缺点：CopyOnWrite容器只能保证数据的最终一致性，不能保证数据的实时一致性。\n- 适合在读多写少的场景下使用\n- iterator\n  - 返回一个拷贝数据的对象COWIterator\n\nCopyOnWrite容器是一种并发容器，它通过在修改容器时先将容器复制一份，然后在复制的容器上进行修改，最后将原容器引用指向复制的容器，从而实现并发访问的线程安全。因为每次修改都需要复制整个容器，所以CopyOnWrite容器在修改操作上的性能相对较差，但是在读操作上的性能比较好，因为读操作不需要加锁。\n\n虽然CopyOnWrite容器能够实现线程安全，但是由于每次修改都需要复制整个容器，所以对于实时性要求较高的场景，使用CopyOnWrite容器可能会导致数据的实时一致性问题。因为当多个线程同时进行写操作时，每个线程都会在自己的副本上进行修改，这就导致了在某个时间点，不同线程所看到的数据可能不一致。因此，如果要求数据的实时一致性，使用CopyOnWrite容器可能不是最佳选择。\n## 集合对比\n- ArrayList（动态数组结构），查询快（随意访问或顺序访问），增删慢，但在末尾插入删除，速度与LinkedList相差无几，但是是非线程安全的！\n- LinkedList（双向链表结构），查询慢，增删快，也是非线程安全的！\n- Vector（动态数组结构），因为方法加了同步锁，相比 ArrayList 执行都慢，基本不再使用，如果需要在多线程下使用，推荐使用并发容器中的CopyOnWriteArrayList来操作，效率高！\n- Stack（栈结构）继承于Vector，数据是先进后出，基本不再使用，如果要实现栈，推荐使用 Deque 下的 ArrayDeque，效率比 Stack 高！\n`https://juejin.im/post/6844903728324018189`\n# Set\n- 实现了Collection接口\n- Set接口特性：无序的，元素不可重复\n- 底层大多数是Map结构的实现\n- 常用的三个子类都是非同步的\n## HashSet\n- 底层数据结构是哈希表(是一个元素为链表的数组) + 红黑树\n- 实际上就是封装了HashMap\n- 元素无序，可以为null\n## LinkedHashSet\n- 底层数据结构由哈希表(是一个元素为链表的数组)和双向链表组成。\n- 父类是HashSet\n- 实际上就是LinkHashMap\n- 元素可以为null\n## TreeSet\n- 底层实际上是一个TreeMap实例(红黑树)\n- 可以实现排序的功能\n- 元素不能为null\n# queue\n## 添加\noffer，add 区别：\n- 一些队列有大小限制，因此如果想在一个满的队列中加入一个新项，多出的项就会被拒绝。\n- 这时新的 offer 方法就可以起作用了。它不是对调用 add() 方法抛出一个 unchecked 异常，而只是得到由 offer() 返回的 false。\n## 删除\npoll，remove 区别：\n- remove() 和 poll() 方法都是从队列中删除第一个元素。remove() 的行为与 Collection 接口的版本相似， 但是新的 poll() 方法在用空集合调用时不是抛出异常，只是返回 null。因此新的方法更适合容易出现异常条件的情况。\n## 获取\npeek，element区别：\n- element() 和 peek() 用于在队列的头部查询元素。与 remove() 方法类似，在队列为空时， element() 抛出一个异常，而 peek() 返回 null。\n## BlockingQueue\n### 插入\n- add(e) 抛出异常\n- offer(e) 特殊值\n- put(e) 阻塞\n- offer(e, time, unit) 超时\n### 移除\n- remove() 抛出异常\n- poll() 特殊值\n- take() 阻塞\n- poll(time, unit) 超时\n### 获取数据\n- element() 抛出异常\n- peek() 特殊值\n\n### 常见的实现\nQueue<T> queue = new LinkedList<>();\n\nBlockingQueue（阻塞队列）详解 https://www.cnblogs.com/aspirant/p/8657801.html\n\n# 参考文章\n- https://segmentfault.com/a/1190000021237438\n- https://mp.weixin.qq.com/s/H6lxTfpedzzDz2QXihhdmw\n- https://segmentfault.com/a/1190000023308658\n- https://blog.csdn.net/weixin_39797532/article/details/112337531\n- https://pdai.tech/md/java/collection/java-collection-all.html\n\n\n\n\n"
  },
  {
    "path": "Java-基础/容器-map.md",
    "content": "# Map\n![](../img/基础/Java容器类关系图.png)\n## HashMap\n- 存储的结构是key-value键值对，不像Collection是单列集合\n- 阅读Map前最好知道什么是散列表和红黑树\n## 特点\n- k和v允许为null，存储无序\n- 非同步\n  - HashMap 虽然很强大，但是它是非线程安全的，也就是说，如果在多线程环境下使用，可能因为程序自动扩容操作将单向链表转变成了循环链表，在查询遍历元素的时候，造成程序死循环！此时 CPU 直接会飙到 100%！\n    - 如果我们想在多线程环境下使用 HashMap，其中一个推荐的解决办法就是使用 java 并发包下的 ConcurrentHashMap 类！\n- 散列表容量大于64且链表大于8时，转成红黑树\n  - 为什么是8\n    - 源码的注释，根据泊松分布原理发生冲突 并且链表长度为8的概率已经非常小了不到千万分之一\n- 底层是散列表+红黑树。初始容量为16，装载因子为0.75，每次扩容2倍\n  - 为什么是0.75，时间和空间的权衡，如果太小比如0.5那么空间为一半的时候就发生扩容，浪费空间，太大如1，满了才扩容，hash冲突的概率大大增加，那为什么不是0.6或者0.8呢，这里估计是调优得出的参数 可以看看这篇文章 https://segmentfault.com/a/1190000023308658\n  - ![](../img/hashmap/HashMap链表结构.png)\n\n\n## 结构\n### Node[] table，即哈希桶数组\n```java\nstatic class Node<K,V> implements Map.Entry<K,V> {\n        final int hash;\n        final K key;\n        V value;\n        Node<K,V> next;\n\n        Node(int hash, K key, V value, Node<K,V> next) {\n            this.hash = hash;\n            this.key = key;\n            this.value = value;\n            this.next = next;\n        }\n    }\n```\n### 插入\n#### 插入流程\n- ①.判断键值对数组table[i]是否为空或为null，否则执行resize()进行扩容； \n- ②.根据键值key计算hash值得到插入的数组索引i，如果table[i]==null，直接新建节点添加，转向⑥，如果table[i]不为空，转向③； \n- ③.判断table[i]的首个元素是否和key一样，如果相同直接覆盖value，否则转向④，这里的相同指的是hashCode以及equals； \n- ④.判断table[i] 是否为treeNode，即table[i] 是否是红黑树，如果是红黑树，则直接在树中插入键值对，否则转向⑤； \n- ⑤.遍历table[i]，判断链表长度是否大于8，大于8的话把链表转换为红黑树，在红黑树中执行插入操作，否则进行链表的插入操作；遍历过程中若发现key已经存在直接覆盖value即可； \n- ⑥.插入成功后，判断实际存在的键值对数量size是否超多了最大容量threshold，如果超过，进行扩容。\n\n![](../img/hashmap/HashMap插入流程.png)\n\n#### 插入代码\n```java\n\nstatic final int hash(Object key) {\n    int h;\n    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);\n}\n\npublic V put(K key, V value) {\n    //对key做hash\n    return putVal(hash(key), key, value, false, true);\n}\n\nfinal V putVal(int hash, K key, V value, boolean onlyIfAbsent,\n                   boolean evict) {\n    Node<K,V>[] tab; Node<K,V> p; int n, i;\n    //tab节点数组为空则新建\n    if ((tab = table) == null || (n = tab.length) == 0)\n        n = (tab = resize()).length;\n    //计算index-hash，是否存在，不存在直接封装为node插入\n    if ((p = tab[i = (n - 1) & hash]) == null)\n        tab[i] = newNode(hash, key, value, null);\n    else {\n    // hash相同，值存在情况    \n        Node<K,V> e; K k;\n        //hash相同, key相同,覆盖原值\n        if (p.hash == hash &&\n            ((k = p.key) == key || (key != null && key.equals(k))))\n            e = p;\n        //节点是红黑树\n        else if (p instanceof TreeNode)\n            //使用红黑树的插入操作\n            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);\n        else {\n            //节点是链表\n            //遍历链表\n            for (int binCount = 0; ; ++binCount) {\n                if ((e = p.next) == null) {\n                    //尾插\n                    p.next = newNode(hash, key, value, null);\n                    //判断链表长度是否为8，是则链表转为红黑树\n                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st\n                        treeifyBin(tab, hash);\n                    break;\n                }\n                //hash 相同，key 相同，跳出循环，在后面覆盖值\n                if (e.hash == hash &&\n                    ((k = e.key) == key || (key != null && key.equals(k))))\n                    break;\n                p = e;\n            }\n        }\n        if (e != null) { // existing mapping for key\n            V oldValue = e.value;\n            if (!onlyIfAbsent || oldValue == null)\n                e.value = value;\n            afterNodeAccess(e);\n            return oldValue;\n        }\n    }\n    ++modCount;\n    // 节点数量大于 阈值则扩容\n    if (++size > threshold)\n        resize();\n    afterNodeInsertion(evict);\n    return null;\n}\n```\n\n#### hash冲突使用链地址法\n- 链接地址法的思路是将哈希值相同的元素构成一个同义词的单链表\n- `int threshold; `            // 扩容阈值\n  - `threshold`就是在此Load factor和length(数组长度)对应下允许的最大元素数目，超过这个数目就重新resize(扩容)，扩容后的HashMap容量是之前容量的两倍\n- `final float loadFactor;`    // 负载因子\n  - 0.75\n- `transient int modCount;`  // 出现线程问题时，负责及时抛异常\n- `transient int size;`     // HashMap中实际存在的Node数量\n- 解决hash冲突的几种方法 https://cloud.tencent.com/developer/article/1672781\n#### HashMap的容量为什么要初始化为2的n次幂\n```java\nstatic final int tableSizeFor(int cap) {\n    int n = cap - 1;\n    n |= n >>> 1;\n    n |= n >>> 2;\n    n |= n >>> 4;\n    n |= n >>> 8;\n    n |= n >>> 16;\n    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;\n}\n ```\n- 向集合中添加元素时，会使用(n - 1) & hash的计算方法来得出该元素在集合中的位置\n- 扩容时调用resize()方法中的部分源码，可以看出会新建一个tab，然后遍历旧的tab，将旧的元素经过e.hash & (newCap - 1)的计算添加进新的tab中，还是用(n - 1) & hash的计算方法\n\n可见这个(n - 1) & hash的计算方法有着千丝万缕的关系，符号&是按位与的计算，这是位运算，特别高效，按位与&的计算方法是，只有当对应位置的数据都为1时，运算结果也为1，当HashMap的容量是2的n次幂时，(n-1)的2进制也就是1111111***111这样形式的，这样与添加元素的hash值进行位运算时，能够充分的散列，使得添加的元素均匀分布在HashMap的每个位置上，减少hash碰撞\n\n#### 为什么计算hash要无符号右移16位\n- Key的哈希值会与该值的高16位做异或操作，进一步增加随机性，如果只是单纯的返回hashcode，做运算的始终是低16位，而hashmap长度大多数小于2的16次方\n```java\n    static final int hash(Object key) {\n        int h;\n        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);\n    }\n```\n#### 为什么计算hash是异或\n- 按位与运算符（&）\n  - 两位同时为“1”，结果才为“1”，否则为0，结果偏向于0\n- 按位或运算符（|）\n  - 参加运算的两个对象只要有一个为1，其值为1。结果偏向于1\n- 异或运算符（^）\n  - 参加运算的两个对象，如果两个相应位为“异”（值不同），则该位结果为1，否则为0。所以更好的扰动了\n\n### 查找\n```java\npublic V get(Object key) {\n    Node<K,V> e;\n    return (e = getNode(hash(key), key)) == null ? null : e.value;\n}\n    \nfinal Node<K,V> getNode(int hash, Object key) {\n    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;\n    if ((tab = table) != null && (n = tab.length) > 0 &&\n        (first = tab[(n - 1) & hash]) != null) {\n        //检查首节点\n        if (first.hash == hash && // always check first node\n            ((k = first.key) == key || (key != null && key.equals(k))))\n            return first;\n        if ((e = first.next) != null) {\n            //判断是否为红黑树\n            if (first instanceof TreeNode)\n                //红黑树的查找方式\n                return ((TreeNode<K,V>)first).getTreeNode(hash, key);\n            //否则遍历桶\n            do {\n                if (e.hash == hash &&\n                    ((k = e.key) == key || (key != null && key.equals(k))))\n                    return e;\n            } while ((e = e.next) != null);\n        }\n    }\n    return null;\n}\n```\n### 扩容\n```java\nfinal Node<K,V>[] resize() {\n    Node<K,V>[] oldTab = table;\n    //原数组大小\n    int oldCap = (oldTab == null) ? 0 : oldTab.length;\n    //原数组扩容阈值\n    int oldThr = threshold;\n    int newCap, newThr = 0;\n    //原数组大小不为0\n    if (oldCap > 0) {\n        //超过最大值就不再扩充，随你碰撞吧\n        if (oldCap >= MAXIMUM_CAPACITY) {\n            threshold = Integer.MAX_VALUE;\n            return oldTab;\n        }\n        //没超过就扩充为原来的2倍\n        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&\n                 oldCap >= DEFAULT_INITIAL_CAPACITY)\n            newThr = oldThr << 1; // double threshold\n    }\n    //原数组阈值不为0\n    else if (oldThr > 0) // initial capacity was placed in threshold\n        //新数组大小为阈值\n        newCap = oldThr;\n    else {               // zero initial threshold signifies using defaults\n        //原数组阈值为0，则新数组的大小和阈值为默认\n        newCap = DEFAULT_INITIAL_CAPACITY;\n        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);\n    }\n    //如果新阈值为0，设置阈值\n    if (newThr == 0) {\n        float ft = (float)newCap * loadFactor;\n        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?\n                  (int)ft : Integer.MAX_VALUE);\n    }\n    threshold = newThr;\n    @SuppressWarnings({\"rawtypes\",\"unchecked\"})\n    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];\n    table = newTab;\n    if (oldTab != null) {\n        //遍历原数组的每个节点，插入到新数组\n        for (int j = 0; j < oldCap; ++j) {\n            Node<K,V> e;\n            if ((e = oldTab[j]) != null) {\n                oldTab[j] = null;\n                if (e.next == null)\n                    newTab[e.hash & (newCap - 1)] = e;\n                else if (e instanceof TreeNode)\n                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);\n                else { // preserve order\n                    //采用 高低位方式转移原数组到新数组的位置，解决环链问题，详细的看下面的解析\n                    Node<K,V> loHead = null, loTail = null;\n                    Node<K,V> hiHead = null, hiTail = null;\n                    Node<K,V> next;\n                    do {\n                        next = e.next;\n                        if ((e.hash & oldCap) == 0) {\n                            if (loTail == null)\n                                loHead = e;\n                            else\n                                loTail.next = e;\n                            loTail = e;\n                        }\n                        else {\n                            if (hiTail == null)\n                                hiHead = e;\n                            else\n                                hiTail.next = e;\n                            hiTail = e;\n                        }\n                    } while ((e = next) != null);\n                    if (loTail != null) {\n                        loTail.next = null;\n                        newTab[j] = loHead;\n                    }\n                    if (hiTail != null) {\n                        hiTail.next = null;\n                        newTab[j + oldCap] = hiHead;\n                    }\n                }\n            }\n        }\n    }\n    return newTab;\n}\n```\n### jdk1.7多线程扩容死循环问题\n```java\nvoid transfer(Entry[] newTable, boolean rehash) {\n   int newCapacity = newTable.length;\n   // 外层循环遍历数组槽（slot）\n   for (Entry<K,V> e : table) {\n       // 内层循环遍历单链表\n       while(null != e) {\n           // 记录当前节点的next节点\n           Entry<K,V> next = e.next;\n           if (rehash) {\n               e.hash = null == e.key ? 0 : hash(e.key);\n           }\n           // 找到元素在新数组中的槽（slot）\n           int i = indexFor(e.hash, newCapacity);\n           // 用头插法将元素插入新的数组\n           e.next = newTable[i];\n           newTable[i] = e;\n           // 遍历下一个节点\n           e = next;\n       }\n   }\n}\n```\n#### 在单线程情况下，假设A、B、C三个节点处在一个链表上，扩容后依然处在一个链表上，代码执行过程如下：\n![](../img/基础/jdk1.7hashmap单线程扩容流程.png)\n\n需要注意的几点是\n- 单链表在转移的过程中会被反转\n- table是线程共享的，而newTable是不共享的\n- 执行table = newTable后，其他线程就可以看到转移线程转移后的结果了\n#### 多线程下扩容\n其实主要结合这4步，然后结合图示就明白了\n```java\n  Entry<K,V> next = e.next;\n  e.next = newTable[i];\n  newTable[i] = e;\n  e = next;\n```\n![](../img/基础/jdk1.7hashmap多线程扩容流程.png)\n### jdk1.8的扩容\n```java\n// 低位链表头节点，尾结点\n// 低位链表就是扩容前后，所处的槽（slot）的下标不变\n// 如果扩容前处于table[n]，扩容后还是处于table[n]\nNode<K,V> loHead = null, loTail = null;\n// 高位链表头节点，尾结点\n// 高位链表就是扩容后所处槽（slot）的下标 = 原来的下标 + 新容量的一半\n// 如果扩容前处于table[n]，扩容后处于table[n + newCapacity / 2]\n// Node<K,V> hiHead = null, hiTail = null;\n```\n\n```java\nNode<K,V> next;\ndo {\n    next = e.next;\n    //当前槽上的链表在扩容前和扩容后，所在的槽（slot）下标是否一致\n    if ((e.hash & oldCap) == 0) {\n        if (loTail == null)\n            loHead = e;\n        else\n            loTail.next = e;\n        loTail = e;\n    }\n    else {\n        if (hiTail == null)\n            hiHead = e;\n        else\n            hiTail.next = e;\n        hiTail = e;\n    }\n} while ((e = next) != null);\nif (loTail != null) {\n    loTail.next = null;\n    // 低位链表在扩容后，所处槽的下标不变\n    newTab[j] = loHead;\n}\nif (hiTail != null) {\n    hiTail.next = null;\n    // 高位链表在扩容后，所处槽的下标 = 原来的下标 + 扩容前的容量（也就是扩容后容量的一半）\n    newTab[j + oldCap] = hiHead;\n}\n```\n\n等到链表被分成高位链表和低位链表后，再一次性转移到新的table。这样就完成了单链表在扩容过程中的转移，使用两条链表的好处就是转移前后的链表不会倒置，顺序一致则不会因为多线程扩容而导致死循环\n\n### JDK1.8hashmap 依然不安全的原因\nJDK1.8 中，由于多线程对HashMap进行put操作，调用了HashMap#putVal()，具体原因：假设两个线程A、B都在进行put操作，并且hash函数计算出的插入下标是相同的，当线程A执行完第六行代码后由于时间片耗尽导致被挂起，而线程B得到时间片后在该下标处插入了元素，完成了正常的插入，然后线程A获得时间片，由于之前已经进行了hash碰撞的判断，所有此时不会再进行判断，而是直接进行插入，这就导致了线程B插入的数据被线程A覆盖了，从而线程不安全\n\n### 为什么要使用红黑树而不是AVL\n- 1、为什么不直接使用树\n  - 大部分情况hashmap的数据量发生hash冲突的概率其实是很小的，此时使用链表是最佳选择\n- 2、为什么是红黑树？\n  - AVL树是完全平衡二叉树，在节点插入时、删除时都会调整树结构来平衡，因此会消耗更多的时间\n  - 虽然AVL的查找时间由于树高度更低而更快，但是插入和删除花费时间比红黑树更长，在hashmap这种情况下更适用红黑树\n### 为什么不是B+树\n- b+树的特点是矮胖，这样叶子结点可以存储大量数据，减少磁盘IO，并不是说不可以，但是相对应用场景来讲，用红黑树更加适合\n\n### 为什么不使用跳表\n- 跳表的随机性：跳表的层次结构是基于随机的，它并不能像红黑树那样提供严格的平衡性。尽管跳表的时间复杂度也是 O(log n)，但跳表实现依赖随机数生成来构建多级索引，可能会导致性能不如红黑树稳定。\n- 空间开销：跳表需要维护多层索引，会增加额外的空间消耗，特别是在 HashMap 中，每个桶原本只是链表结构，如果改为跳表，整个结构变得更复杂，需要更多的指针和索引，内存开销增加。\n\n### 如果HashMap里有100万条数据，remove掉90万条，HashMap的数组会不会变小\n在Java中，HashMap的数组大小在HashMap创建时确定，并且通常不会自动缩小。因此，当你从HashMap中删除大量条目后，HashMap的数组大小不会减小，但是HashMap的占用内存大小也不会一直增加。\n\n即使从HashMap中删除了90%的条目，HashMap的“capacity”和“load factor”将保持不变。这意味着虽然内存中保留了之前容纳100万条目的数组，但实际上用于存储数据的内存将会减少，因为90%的项已被删除。\n\n尽管数组大小不会在删除后自动缩小，但随着元素被移除而留下的空间会被重新利用。在Java中，HashMap通过垃圾回收器来释放不再使用的内存，因此即使HashMap中有大量数据被删除，这些内存空间最终会被垃圾回收器回收。\n\n总而言之，尽管HashMap的数组大小不会缩小，但是通过删除大量数据后，Java的垃圾回收器会逐渐回收未使用的内存空间，因此HashMap的占用空间不会一直增加。Java的垃圾回收机制会确保内存空间被有效地管理和重用。\n\n## LinkedHashMap\n- 底层是散列表+红黑树+双向链表，父类是HashMap\n- 允许为null，插入有序\n- 非同步\n- 提供插入顺序和访问顺序两种，访问顺序是符合LRU算法的，一般用于扩展(默认是插入顺序)\n- 迭代与初始容量无关(迭代的是维护的双向链表)\n- 大多使用HashMap的API，只不过在内部重写了某些方法，维护了双向链表\n\n### 复杂度\n- 增删查改时间复杂度均为O(1)\n- containsKey()时间复杂度是O(1)\n- containsValue()时间复杂度是O(N)\n## TreeMap\n- 底层是红黑树，保证了时间复杂度为log(n)\n- 可以对其进行排序，使用Comparator或者Comparable\n- 只要compare或者CompareTo认定该元素相等，那就相等\n- 非同步\n- 自然排序(手动排序)，元素不能为null\n\n### 复杂度\n- 增删查改时间复杂度均为O(logN)\n- containsKey()时间复杂度O(logN)\n- containsValue()时间复杂度O(N)，因为value是无序的，所以要依次遍历\n\n## ConcurrentHashMap\n- 底层是散列表+红黑树，支持高并发操作\n- key和value都不能为null\n- 线程是安全的，利用CAS算法和部分操作上锁实现\n- get方法是非阻塞，无锁的。重写Node类，通过volatile修饰next来实现每次获取都是最新设置的值\n  ```java\n  volatile V val;\n  volatile Node<K,V> next;\n  ```\n- 在高并发环境下，统计数据(计算size…等等)其实是无意义的，因为在下一时刻size值就变化了。\n- 在 JDK1.7 中，ConcurrentHashMap 类采用了分段锁的思想，将 HashMap 进行切割，把 HashMap 中的哈希数组切分成小数组（Segment），每个小数组有 n 个 HashEntry 组成，其中 Segment 继承自ReentrantLock（可重入锁），从而实现并发控制！\n- 从 jdk1.8 开始，ConcurrentHashMap 类取消了 Segment 分段锁，采用 CAS + synchronized来保证并发安全，数据结构跟 jdk1.8 中 HashMap 结构保持一致，都是 数组 + 链表（当链表长度大于8时，链表结构转为红黑树）结构。\n- jdk1.8 中的 ConcurrentHashMap 中 synchronized 只锁定当前链表或红黑树的首节点，只要节点 hash 不冲突，就不会产生并发，相比 JDK1.7 的 ConcurrentHashMap 效率又提升了 N 倍！\n### ConcurrentHashMap扩容机制\n#### 1、ConcurrentHashMap 在 JDK 1.7 中的实现\n\n在 JDK 1.7 版本及之前的版本中，ConcurrentHashMap 为了解决 HashTable 会锁住整个 hash 表的问题，提出了分段锁的解决方案，分段锁就是将一个大的 hash 表分解成若干份小的 hash 表，需要加锁时就针对小的 hash 表进行加锁，从而来提升 hash 表的性能。JDK1.7 中的 ConcurrentHashMap 引入了 Segment 对象，将整个 hash 表分解成一个一个的 Segment 对象，每个 Segment 对象呢可以看作是一个细粒度的 HashMap。\n\nSegment 对象继承了 ReentrantLock 类，因为 Segment 对象它就变成了一把锁，这样就可以保证数据的安全。 在 Segment 对象中通过 HashEntry 数组来维护其内部的 hash 表。每个 HashEntry 就代表了 map 中的一个 K-V，如果发生 hash 冲突时，在该位置就会形成链表。\n\nJDK1.7 中，ConcurrentHashMap 的整体结构可以描述为下图的样子：\n![](../img/基础/jdk1.7concurrentHashmap整体结构.png)\n\n##### put 方法\n```java\n public V put(K key, V value) {\n    Segment<K,V> s;\n    if (value == null)\n        throw new NullPointerException();\n    // 二次哈希，以保证数据的分散性，避免哈希冲突\n    int hash = hash(key.hashCode());\n    int j = (hash >>> segmentShift) & segmentMask;\n    // Unsafe 调用方式，直接获取相应的 Segment\n    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck\n         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment\n        s = ensureSegment(j);\n    return s.put(key, hash, value, false);\n}\n```\n\n在 put 方法中，首先是通过二次哈希减小哈希冲突的可能性，根据 hash 值以 Unsafe 调用方式，直接获取相应的 Segment，最终将数据添加到容器中是由 segment对象的 put 方法来完成。Segment对象的 put 方法源代码如下：\n\n```java\nfinal V put(K key, int hash, V value, boolean onlyIfAbsent) {\n    // 无论如何，确保获取锁 scanAndLockForPut会去查找是否有key相同Node\n    ConcurrentHashMap.HashEntry<K,V> node = tryLock() ? null :\n            scanAndLockForPut(key, hash, value);\n    V oldValue;\n    try {\n        ConcurrentHashMap.HashEntry<K,V>[] tab = table;\n        int index = (tab.length - 1) & hash;\n        ConcurrentHashMap.HashEntry<K,V> first = entryAt(tab, index);\n        for (ConcurrentHashMap.HashEntry<K,V> e = first;;) {\n            // 更新已存在的key\n            if (e != null) {\n                K k;\n                if ((k = e.key) == key ||\n                        (e.hash == hash && key.equals(k))) {\n                    oldValue = e.value;\n                    if (!onlyIfAbsent) {\n                        e.value = value;\n                        ++modCount;\n                    }\n                    break;\n                }\n                e = e.next;\n            }\n            else {\n                if (node != null)\n                    node.setNext(first);\n                else\n                    node = new ConcurrentHashMap.HashEntry<K,V>(hash, key, value, first);\n                int c = count + 1;\n                // 判断是否需要扩容\n                if (c > threshold && tab.length < MAXIMUM_CAPACITY)\n                    rehash(node);\n                else\n                    setEntryAt(tab, index, node);\n                ++modCount;\n                count = c;\n                oldValue = null;\n                break;\n            }\n        }\n    } finally {\n        unlock();\n    }\n    return oldValue;\n}\n```\n由于 Segment 对象本身就是一把锁，所以在新增数据的时候，相应的 Segment对象块是被锁住的，其他线程并不能操作这个 Segment 对象，这样就保证了数据的安全性，在扩容时也是这样的，在 JDK1.7 中的 ConcurrentHashMap扩容只是针对 Segment 对象中的 HashEntry 数组进行扩容，还因为 Segment 对象是一把锁，所以在 rehash 的过程中，其他线程无法对 segment 的 hash 表做操作，这就解决了 HashMap 中 put 数据引起的闭环问题\n#### 2、ConcurrentHashMap 在 JDK 1.8 中的实现\n先从容器安全说起，在容器安全上，1.8 中的 ConcurrentHashMap 放弃了 JDK1.7 中的分段技术，而是采用了 CAS 机制 + synchronized 来保证并发安全性，但是在 ConcurrentHashMap 实现里保留了 Segment 定义，这仅仅是为了保证序列化时的兼容性而已，并没有任何结构上的用处\n\n在存储结构上，JDK1.8 中 ConcurrentHashMap 放弃了 HashEntry 结构而是采用了跟 HashMap 结构非常相似，采用 Node 数组加链表（链表长度大于8时转成红黑树）的形式，Node 节点设计如下：\n\n```java\nstatic class Node<K,V> implements Map.Entry<K,V> {\n        final int hash;\n        final K key;\n        volatile V val;\n        volatile Node<K,V> next;\n        ...省略...\n }       \n```\n跟 HashMap 一样 Key 字段被 final 修饰，说明在生命周期内，key 是不可变的， val 字段被 volatile 修饰了，这就保证了 val 字段的可见性。\n\nJDK1.8 中的 ConcurrentHashMap 结构如下图所示：\n\n![](../img/基础/jdk1.8concurrentHashmap整体结构.png)\n\n##### putVal 方法\n```java\nfinal V putVal(K key, V value, boolean onlyIfAbsent) {\n    // 如果 key 为空，直接返回\n    if (key == null || value == null) throw new NullPointerException();\n    // 两次 hash ，减少碰撞次数\n    int hash = spread(key.hashCode());\n    // 记录链表节点得个数\n    int binCount = 0;\n    // 无条件得循环遍历整个 node 数组，直到成功\n    for (ConcurrentHashMap.Node<K,V>[] tab = table;;) {\n        ConcurrentHashMap.Node<K,V> f; int n, i, fh;\n        // lazy-load 懒加载的方式，如果当前 tab 容器为空，则初始化 tab 容器\n        if (tab == null || (n = tab.length) == 0)\n            tab = initTable();\n\n        // 通过Unsafe.getObjectVolatile()的方式获取数组对应index上的元素，如果元素为空，则直接无所插入\n        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {\n            //// 利用CAS去进行无锁线程安全操作\n            if (casTabAt(tab, i, null,\n                    new ConcurrentHashMap.Node<K,V>(hash, key, value, null)))\n                break;                   // no lock when adding to empty bin\n        }\n        // 如果 fh == -1 ，说明正在扩容，那么该线程也去帮扩容\n        else if ((fh = f.hash) == MOVED)\n            // 协作扩容操作\n            tab = helpTransfer(tab, f);\n        else {\n            // 如果上面都不满足，说明存在 hash 冲突，则使用 synchronized 加锁。锁住链表或者红黑树的头结点，来保证操作安全\n            V oldVal = null;\n            synchronized (f) {\n                if (tabAt(tab, i) == f) {\n\n                    if (fh >= 0) {// 表示该节点是链表\n                        binCount = 1;\n                        // 遍历该节点上的链表\n                        for (ConcurrentHashMap.Node<K,V> e = f;; ++binCount) {\n                            K ek;\n                            //这里涉及到相同的key进行put就会覆盖原先的value\n                            if (e.hash == hash &&\n                                    ((ek = e.key) == key ||\n                                            (ek != null && key.equals(ek)))) {\n                                oldVal = e.val;\n                                if (!onlyIfAbsent)\n                                    e.val = value;\n                                break;\n                            }\n                            ConcurrentHashMap.Node<K,V> pred = e;\n                            if ((e = e.next) == null) {//插入链表尾部\n                                pred.next = new ConcurrentHashMap.Node<K,V>(hash, key,\n                                        value, null);\n                                break;\n                            }\n                        }\n                    }\n                    else if (f instanceof ConcurrentHashMap.TreeBin) {// 该节点是红黑树节点\n                        ConcurrentHashMap.Node<K,V> p;\n                        binCount = 2;\n                        if ((p = ((ConcurrentHashMap.TreeBin<K,V>)f).putTreeVal(hash, key,\n                                value)) != null) {\n                            oldVal = p.val;\n                            if (!onlyIfAbsent)\n                                p.val = value;\n                        }\n                    }\n                }\n            }\n            // 插入完之后，判断链表长度是否大于8，大于8就需要转换为红黑树\n            if (binCount != 0) {\n                if (binCount >= TREEIFY_THRESHOLD)\n                    treeifyBin(tab, i);\n                // 如果存在相同的key ，返回原来的值\n                if (oldVal != null)\n                    return oldVal;\n                break;\n            }\n        }\n    }\n    //统计 size，并且检测是否需要扩容\n    addCount(1L, binCount);\n    return null;\n}\n```\nputVal 方法主要做了以下几件事：\n  - 第一步、在 ConcurrentHashMap 中不允许 key val 字段为空，所以第一步先校验key value 值，key、val 两个字段都不能是 null 才继续往下走，否则直接返回一个 NullPointerException 错误，这点跟 HashMap 有区别，HashMap 是可以允许为空的。\n  - 第二步、判断容器是否初始化，如果容器没有初始化，则调用 initTable 方法初始化，initTable 方法如下：\n##### initTable方法 \n```java\n/**\n * Initializes table, using the size recorded in sizeCtl.\n */\nprivate final Node<K,V>[] initTable() {\n    Node<K,V>[] tab; int sc;\n    while ((tab = table) == null || tab.length == 0) {\n        // 负数表示正在初始化或扩容，等待\n        if ((sc = sizeCtl) < 0)\n            // 自旋等待\n            Thread.yield(); // lost initialization race; just spin\n        // 执行 CAS 操作，期望将 sizeCtl 设置为 -1，-1 是正在初始化的标识\n        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {\n        // CAS 抢到了锁\n            try {\n            // 对 table 进行初始化，初始化长度为指定值，或者默认值 16\n                if ((tab = table) == null || tab.length == 0) {\n                    // sc 在初始化的时候用户可能会自定义，如果没有自定义，则是默认的\n                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;\n                    // 创建数组\n                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];\n                    table = tab = nt;\n                    // 指定下次扩容的大小，相当于 0.75 × n\n                    sc = n - (n >>> 2);\n                }\n            } finally {\n                sizeCtl = sc;\n            }\n            break;\n        }\n    }\n    return tab;\n}\n```\nTable 本质上就是一个 Node 数组，其初始化过程也就是对 Node 数组的初始化过程，方法中使用了 CAS 策略执行初始化操作。初始化流程为：\n1. 判断 sizeCtl 值是否小于 0，如果小于 0 则表示 ConcurrentHashMap 正在执行初始化操作，所以需要先等待一会，如果其它线程初始化失败还可以顶替上去\n2. 如果 sizeCtl 值大于等于 0，则基于 CAS 策略抢占标记 sizeCtl 为 -1，表示 ConcurrentHashMap 正在执行初始化，然后构造 table，并更新 sizeCtl 的值\n3. 根据双哈希之后的 hash 值找到数组对应的下标位置，如果该位置未存放节点，也就是说不存在 hash 冲突，则使用 CAS 无锁的方式将数据添加到容器中，并且结束循环。\n4. 如果并未满足第三步，则会判断容器是否正在被其他线程进行扩容操作，如果正在被其他线程扩容，则放弃添加操作，加入到扩容大军中（ConcurrentHashMap 扩容操作采用的是多线程的方式，后面我们会讲到），扩容时并未跳出死循环，这一点就保证了容器在扩容时并不会有其他线程进行数据添加操作，这也保证了容器的安全性。\n5. 如果 hash 冲突，则进行链表操作或者红黑树操作（如果链表树超过8，则修改链表为红黑树），在进行链表或者红黑树操作时，会使用 synchronized 锁把头节点被锁住了，保证了同时只有一个线程修改链表，防止出现链表成环。\n6. 进行 addCount(1L, binCount) 操作，该操作会更新 size 大小，判断是否需要扩容，addCount 方法源码如下：\n\n##### addCount方法\n```java\n// X传入的是1，check 传入的是 putVal 方法里的 binCount，没有hash冲突的话为0，冲突就会大于1\nprivate final void addCount(long x, int check) {\nConcurrentHashMap.CounterCell[] as; long b, s;\n// 统计ConcurrentHashMap里面节点个数\nif ((as = counterCells) != null ||\n        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {\n    ConcurrentHashMap.CounterCell a; long v; int m;\n    boolean uncontended = true;\n    if (as == null || (m = as.length - 1) < 0 ||\n            (a = as[ThreadLocalRandom.getProbe() & m]) == null ||\n            !(uncontended =\n                    U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {\n        fullAddCount(x, uncontended);\n        return;\n    }\n    if (check <= 1)\n        return;\n    s = sumCount();\n}\n// check就是binCount，binCount 最小都为0，所以这个条件一定会为true\nif (check >= 0) {\n    ConcurrentHashMap.Node<K,V>[] tab, nt; int n, sc;\n    // 这儿是自旋，需同时满足下面的条件\n    // 1. 第一个条件是map.size 大于 sizeCtl，也就是说需要扩容\n    // 2. 第二个条件是`table`不为null\n    // 3. 第三个条件是`table`的长度不能超过最大容量\n    while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&\n            (n = tab.length) < MAXIMUM_CAPACITY) {\n        int rs = resizeStamp(n);\n        // 该判断表示已经有线程在进行扩容操作了\n        if (sc < 0) {\n            if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||\n                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||\n                    transferIndex <= 0)\n                break;\n            // 如果可以帮助扩容，那么将 sc 加 1. 表示多了一个线程在帮助扩容\n            if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))\n                transfer(tab, nt);\n        }\n        // 如果不在扩容，将 sc 更新：标识符左移 16 位 然后 + 2. 也就是变成一个负数。高 16 位是标识符，低 16 位初始是 2\n        else if (U.compareAndSwapInt(this, SIZECTL, sc,\n                (rs << RESIZE_STAMP_SHIFT) + 2))\n            transfer(tab, null);\n        s = sumCount();\n    }\n}\n```\naddCount 方法做了两个工作：\n1. 对 map 的 size 加一\n2. 检查是否需要扩容，或者是否正在扩容。如果需要扩容，就调用扩容方法，如果正在扩容，就帮助其扩容。\n\n##### transfer 扩容方法\n以下两种情况下可能触发扩容操作\n\n- 调用 put 方法新增元素之后，会调用 addCount 方法来更新 size 大小，并检查是否需要进行扩容，当数组元素个数达到阈值时，会触发transfer方法\n- 触发了 tryPresize 操作， tryPresize 操作会触发扩容操作，有两种情况会触发 tryPresize 操作：\n  - 第一种情况：当某节点的链表元素个数达到阈值 8 时，这时候需要将链表转成红黑树，在结构转换之前会，会先判断数组长度 n 是否小于阈值MIN_TREEIFY_CAPACITY，默认是64，如果小于则会调用tryPresize方法把数组长度扩大到原来的两倍，并触发transfer方法，重新调整节点的位置。\n  - 第二种情况：在 putAll 操作时会先触发 tryPresize 操作。\n\n```java\nprivate final void transfer(ConcurrentHashMap.Node<K,V>[] tab, ConcurrentHashMap.Node<K,V>[] nextTab) {\n    int n = tab.length, stride;\n    // 多线程扩容，每核处理的量小于16，则强制赋值16\n    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)\n        stride = MIN_TRANSFER_STRIDE; // subdivide range\n    // nextTab 为空，先实例化一个新的数组\n    if (nextTab == null) {            // initiating\n        try {\n            @SuppressWarnings(\"unchecked\")\n            // 新数组的大小是原来的两倍\n            ConcurrentHashMap.Node<K,V>[] nt = (ConcurrentHashMap.Node<K,V>[])new ConcurrentHashMap.Node<?,?>[n << 1];\n            nextTab = nt;\n        } catch (Throwable ex) {      // try to cope with OOME\n            sizeCtl = Integer.MAX_VALUE;\n            return;\n        }\n        // 更新成员变量\n        nextTable = nextTab;\n        // 更新转移下标，就是 老的 tab 的 length\n        transferIndex = n;\n    }\n    // bound ：该线程此次可以处理的区间的最小下标，超过这个下标，就需要重新领取区间或者结束扩容\n    // advance： 该参数\n    int nextn = nextTab.length;\n    // 创建一个 fwd 节点，用于占位。当别的线程发现这个槽位中是 fwd 类型的节点，则跳过这个节点。\n    ConcurrentHashMap.ForwardingNode<K,V> fwd = new ConcurrentHashMap.ForwardingNode<K,V>(nextTab);\n    // advance 变量指的是是否继续递减转移下一个桶，如果为 true，表示可以继续向后推进，反之，说明还没有处理好当前桶，不能推进\n    boolean advance = true;\n    // 完成状态，如果是 true，表示扩容结束\n    boolean finishing = false; // to ensure sweep before committing nextTab\n    // 死循环,i 表示下标，bound 表示当前线程可以处理的当前桶区间最小下标\n    for (int i = 0, bound = 0;;) {\n        ConcurrentHashMap.Node<K,V> f; int fh;\n        while (advance) {\n            int nextIndex, nextBound;\n            if (--i >= bound || finishing)\n                advance = false;\n            else if ((nextIndex = transferIndex) <= 0) {\n                i = -1;\n                advance = false;\n            }\n            else if (U.compareAndSwapInt\n                    (this, TRANSFERINDEX, nextIndex,\n                            nextBound = (nextIndex > stride ?\n                                    nextIndex - stride : 0))) {\n                bound = nextBound;\n                i = nextIndex - 1;\n                advance = false;\n            }\n        }\n        if (i < 0 || i >= n || i + n >= nextn) {\n            int sc;\n            if (finishing) {\n                nextTable = null;\n                table = nextTab;\n                sizeCtl = (n << 1) - (n >>> 1);\n                return;\n            }\n            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {\n                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)\n                    return;\n                finishing = advance = true;\n                i = n; // recheck before commit\n            }\n        }\n        else if ((f = tabAt(tab, i)) == null)\n            advance = casTabAt(tab, i, null, fwd);\n        else if ((fh = f.hash) == MOVED)\n            advance = true; // already processed\n        else {\n            synchronized (f) {\n            // 这儿多判断一次，是否为了防止可能出现的remove()操作\n                if (tabAt(tab, i) == f) {\n                    // 旧链表上该节点的数据，会被分成低位和高位，低位就是在新链表上的位置跟旧链表上一样，\n                    // 高位就是在新链表的位置是旧链表位置加上旧链表的长度\n                    ConcurrentHashMap.Node<K,V> ln, hn;\n                    if (fh >= 0) {\n                        int runBit = fh & n;\n                        ConcurrentHashMap.Node<K,V> lastRun = f;\n                        for (ConcurrentHashMap.Node<K,V> p = f.next; p != null; p = p.next) {\n                            int b = p.hash & n;\n                            if (b != runBit) {\n                                runBit = b;\n                                lastRun = p;\n                            }\n                        }\n                        if (runBit == 0) {\n                            ln = lastRun;\n                            hn = null;\n                        }\n                        else {\n                            hn = lastRun;\n                            ln = null;\n                        }\n                        for (ConcurrentHashMap.Node<K,V> p = f; p != lastRun; p = p.next) {\n                            int ph = p.hash; K pk = p.key; V pv = p.val;\n                            // 该节点哈希值与旧链表长度与运算，结果为0，则在低位节点上，反之，在高位节点上\n                            if ((ph & n) == 0)\n                                ln = new ConcurrentHashMap.Node<K,V>(ph, pk, pv, ln);\n                            else\n                                hn = new ConcurrentHashMap.Node<K,V>(ph, pk, pv, hn);\n                        }\n                        setTabAt(nextTab, i, ln);\n                        // 在nextTable i + n 位置处插上链表\n                        setTabAt(nextTab, i + n, hn);\n                        // 在table i 位置处插上ForwardingNode 表示该节点已经处理过了\n                        setTabAt(tab, i, fwd);\n                        advance = true;\n                    }\n                    else if (f instanceof ConcurrentHashMap.TreeBin) {\n                        // 如果是TreeBin，则按照红黑树进行处理，处理逻辑与上面一致\n                        // 红黑树的逻辑跟节点一模一样，最后也会分高位和低位\n                        ConcurrentHashMap.TreeBin<K,V> t = (ConcurrentHashMap.TreeBin<K,V>)f;\n                        ConcurrentHashMap.TreeNode<K,V> lo = null, loTail = null;\n                        ConcurrentHashMap.TreeNode<K,V> hi = null, hiTail = null;\n                        int lc = 0, hc = 0;\n                        for (ConcurrentHashMap.Node<K,V> e = t.first; e != null; e = e.next) {\n                            int h = e.hash;\n                            ConcurrentHashMap.TreeNode<K,V> p = new ConcurrentHashMap.TreeNode<K,V>\n                                    (h, e.key, e.val, null, null);\n                            if ((h & n) == 0) {\n                                if ((p.prev = loTail) == null)\n                                    lo = p;\n                                else\n                                    loTail.next = p;\n                                loTail = p;\n                                ++lc;\n                            }\n                            else {\n                                if ((p.prev = hiTail) == null)\n                                    hi = p;\n                                else\n                                    hiTail.next = p;\n                                hiTail = p;\n                                ++hc;\n                            }\n                        }\n                        // 如果树的节点数小于等于 6，那么转成链表，反之，创建一个新的树\n                        ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :\n                                (hc != 0) ? new ConcurrentHashMap.TreeBin<K,V>(lo) : t;\n                        hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :\n                                (lc != 0) ? new ConcurrentHashMap.TreeBin<K,V>(hi) : t;\n                        setTabAt(nextTab, i, ln);\n                        setTabAt(nextTab, i + n, hn);\n                        setTabAt(tab, i, fwd);\n                        advance = true;\n                    }\n                }\n            }\n        }\n    }\n}\n```\ntransfer 大致做了以下几件事件:\n1. 计算出每个线程每次可以处理的个数，根据 Map 的长度，计算出每个线程（CPU）需要处理的桶（table数组的个数），默认每个线程每次处理 16 个桶，如果小于 16 个，则强制变成 16 个桶。\n2. 对 nextTab 初始化，如果传入的新 table nextTab 为空，则对 nextTab 初始化，默认是原 table 的两倍\n3. 引入 ForwardingNode、advance、finishing 变量来辅助扩容，ForwardingNode 表示该节点已经处理过，不需要在处理，advance 表示该线程是否可以下移到下一个桶（true：表示可以下移），finishing 表示是否结束扩容（true：结束扩容，false：未结束扩容） ，具体的逻辑就不说了\n4. 跳过一些其他细节，直接到数据迁移这一块，在数据转移的过程中会加 synchronized 锁，锁住头节点，同步化操作，防止 putVal 的时候向链表插入数据\n5. 进行数据迁移，如果这个桶上的节点是链表或者红黑树，则会将节点数据分为低位和高位，计算的规则是通过该节点的 hash 值跟为扩容之前的 table 容器长度进行位运算（&），如果结果为 0 ，则将数据放在新表的低位（当前 table 中为 第 i 个位置，在新表中还是第 i 个位置），结果不为 0 ，则放在新表的高位（当前 table 中为第 i 个位置，在新表中的位置为 i + 当前 table 容器的长度）。\n6. 如果桶挂载的是红黑树，不仅需要分离出低位节点和高位节点，还需要判断低位和高位节点在新表以链表还是红黑树的形式存放。\n\n## HashTable\n- 与hashmap对比较大的区别是方法加锁，使用synchronized，线程安全\n- Hashtable key和value不为空\n\n## IdentityHashMap\n- IdentityHashMap 从名字上看，感觉表示唯一的 HashMap，然后并不是，别被它的名称所欺骗哦。\n- IdentityHashMap 的数据结构很简单，底层实际就是一个 Object 数组，在存储上并没有使用链表来存储，而是将 K 和 V 都存放在 Object 数组上。\n- 当添加元素的时候，会根据 Key 计算得到散列位置，如果发现该位置上已经有改元素，直接进行新值替换；如果没有，直接进行存放。当元素个数达到一定阈值时，Object 数组会自动进行扩容处理。\n- IdentityHashMap 的实现也不同于 HashMap，虽然也是数组，不过 IdentityHashMap 中没有用到链表，解决冲突的方式是计算下一个有效索引，并且将数据 key 和 value 紧挨着存入 map 中，即table[i]=key、table[i+1]=value；\n- IdentityHashMap 允许key、value都为null，当key为null的时候，默认会初始化一个Object对象作为key；\n## WeakHashMap\n- WeakHashMap 是 Map 体系中一个很特殊的成员，它的特殊之处在于 WeakHashMap 里的元素可能会被 GC 自动删除，即使程序员没有显示调用 remove() 或者 clear() 方法。\n- 换言之，当向 WeakHashMap 中添加元素的时候，再次遍历获取元素，可能发现它已经不见了\n- WeakHashMap 的 key 使用了弱引用类型，在垃圾回收器线程扫描它所管辖的内存区域的过程中，一旦发现了只具有弱引用的对象，不管当前内存空间足够与否，都会回收它的内存。\n- 不过，由于垃圾回收器是一个优先级很低的线程，因此不一定会很快发现那些只具有弱引用的对象。\n- WeakHashMap 跟普通的 HashMap 不同，在存储数据时，key 被设置为弱引用类型，而弱引用类型在 java 中，可能随时被 jvm 的 gc 回收，所以再次通过获取对象时，可能得到空值，而 value 是在访问数组内容的时候，进行清除。\n- 可能很多人觉得这样做很奇葩，其实不然，WeekHashMap 的这个特点特别适用于需要缓存的场景。\n- 在缓存场景下，由于系统内存是有限的，不能缓存所有对象，可以使用 WeekHashMap 进行缓存对象，即使缓存丢失，也可以通过重新计算得到，不会造成系统错误。\n- 比较典型的例子，Tomcat 中的 ConcurrentCache 类就使用了 WeekHashMap 来实现数据缓存。\n\n# 参考文章\n- https://segmentfault.com/a/1190000021237438\n- https://mp.weixin.qq.com/s/H6lxTfpedzzDz2QXihhdmw\n- https://segmentfault.com/a/1190000023308658\n- https://blog.csdn.net/weixin_39797532/article/details/112337531\n- https://pdai.tech/md/java/collection/java-collection-all.html\n\n\n\n\n"
  },
  {
    "path": "Java-基础/异常.md",
    "content": "\n* [Throwable](#throwable)\n  * [Error](#error)\n  * [Exception](#exception)\n* [try catch finally机制](#try-catch-finally机制)\n* [OOM &amp;&amp; SOF](#oom--sof)\n  * [发生了内存泄露或溢出怎么办](#发生了内存泄露或溢出怎么办)\n  * [内存泄漏的场景](#内存泄漏的场景)\n  * [内存溢出的场景](#内存溢出的场景)\n    * [Java Heap 溢出](#java-heap-溢出)\n    * [虚拟机栈和本地方法栈溢出](#虚拟机栈和本地方法栈溢出)\n    * [运行时常量池溢出](#运行时常量池溢出)\n    * [方法区溢出](#方法区溢出)\n    * [java.lang.OutOfMemoryError: GC overhead limit exceeded](#javalangoutofmemoryerror-gc-overhead-limit-exceeded)\n  * [SOF （堆栈溢出 StackOverflow）](#sof-堆栈溢出-stackoverflow)\n  * [如何避免发生内存泄露和溢出](#如何避免发生内存泄露和溢出)\n* [参考文章](#参考文章)\n\n## Throwable\n![](../img/基础/Java异常.png)\n### Error\n表示 JVM 无法处理的错误\n- OutOfMemoryError\n- StackOverflowError\n### Exception\n- 受检异常\n  - 需要用 try...catch... 语句捕获并进行处理，并且可以从异常中恢复；\n    - IOException\n      - ClassNotFoundException\n- 非受检异常\n  - 是程序运行时错误，例如除 0 会引发 Arithmetic Exception，此时程序崩溃并且无法恢复\n    - RuntimeException\n      - NullPointerException\n      - IllegalArgumentException\n## try catch finally机制\n- 有当try代码块发生异常的时候，才会执行到catch代码块\n- 不管try中是否发生异常，finally都会执行。\n  - 以下两种情况例外：\n    - 一：try中不发生异常时，try块中有System.exit(0);\n    - 二：try中发生异常时，catch中有System.exit(0);\n    - 说明：System.exit(0) 代码的作用的退出虚拟机;\n- 若finally块内有return语句，则以finally块内的return为准\n   - 如果try 或者 catch内也有return 其实是先执行了try 或者 catch代码块中的return语句的，\n   - 但是由于finally的机制，执行完try或者catch内的代码以后并不会立刻结束函数，还会执行finally块代码，\n   - 若finally也有return语句，则会覆盖try块或者catch块中的return语句\n- 若finally代码块中有return语句，则屏蔽catch代码块中抛出的异常\n\n## OOM && SOF\nOutOfMemoryError异常: 除了程序计数器外，虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(OOM)异常的可能，\n\n内存泄露：指程序中动态分配内存给一些临时对象，但是对象不会被GC所回收，它始终占用内存。即被分配的对象可达但已无用。\n\n内存溢出：指程序运行过程中无法申请到足够的内存而导致的一种错误。内存溢出通常发生于OLD段或Perm段垃圾回收后，仍然无内存空间容纳新的Java对象的情况。\n\n从定义上可以看出内存泄露是内存溢出的一种诱因，不是唯一因素。\n\n栈溢出：当应用程序递归太深而发生堆栈溢出时，抛出该错误。\n### 发生了内存泄露或溢出怎么办\n一般的异常信息：java.lang.OutOfMemoryError:Java heap spacess\njava堆用于存储对象实例，我们只要不断的创建对象，并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象，就会在对象数量达到最大堆容量限制后产生内存溢出异常。\n\n（1）通过参数 -XX:+HeapDumpOnOutOfMemoryError 让虚拟机在出现OOM异常的时候Dump出内存映像以便于分析。\n\n（2）一般手段是先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转存快照进行分析，重点是确认内存中的对象是否是必要的，先分清是因为内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。（到底是出现了内存泄漏还是内存溢出）\n\n哪些对象被怀疑为内存泄漏，哪些对象占的空间最大及对象的调用关系，还可以分析线程状态，可以观察到线程被阻塞在哪个对象上，从而判断系统的瓶颈。\n\n（3）如果是内存泄漏，可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄漏对象时通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收。 找到引用信息，可以准确的定位出内存泄漏的代码位置。（HashMap中的元素的某些属性改变了，影响了hashcode的值会发生内存泄漏）\n\n（4）如果不存在内存泄漏，就应当检查虚拟机的参数(-Xmx与-Xms)的设置是否适当，是否可以调大；修改代码逻辑，把某些对象生命周期过长，持有状态时间过长等情况的代码修改。\n\n\n\n### 内存泄漏的场景\n   （1）使用静态的集合类\n\n静态的集合类的生命周期和应用程序的生命周期一样长，所以在程序结束前容器中的对象不能被释放，会造成内存泄露。\n\n解决办法是最好不使用静态的集合类，如果使用的话，在不需要容器时要将其赋值为null。\n\n修改hashset中对象的参数值，且参数是计算哈希值的字段\n\n（2）单例模式可能会造成内存泄露（长生命周期的对象持有短生命周期对象的引用）\n\n单例模式只允许应用程序存在一个实例对象，并且这个实例对象的生命周期和应用程序的生命周期一样长，如果单例对象中拥有另一个对象的引用的话，这个被引用的对象就不能被及时回收。\n\n解决办法是单例对象中持有的其他对象使用弱引用，弱引用对象在GC线程工作时，其占用的内存会被回收掉。\n\n（3）数据库、网络、输入输出流，这些资源没有显示的关闭\n\n垃圾回收只负责内存回收，如果对象正在使用资源的话，Java虚拟机不能判断这些对象是不是正在进行操作，比如输入输出，也就不能回收这些对象占用的内存，所以在资源使用完后要调用close()方法关闭。\n\n### 内存溢出的场景\n#### Java Heap 溢出\n\n在jvm规范中，堆中的内存是用来生成对象实例和数组的。 \n\n如果细分，堆内存还可以分为年轻代和年老代，年轻代包括一个eden区和两个survivor区。\n   \n当生成新对象时，内存的申请过程如下：\n\n- jvm先尝试在eden区分配新建对象所需的内存；\n- 如果内存大小足够，申请结束，否则下一步；\n- jvm启动youngGC，试图将eden区中不活跃的对象释放掉，释放后若Eden空间仍然不足以放入新对象，则试图将部分Eden中活跃对象放入Survivor区；\n- Survivor区被用来作为Eden及old的中间交换区域，当OLD区空间足够时，Survivor区的对象会被移到Old区，否则会被保留在Survivor区；\n- 当OLD区空间不够时，JVM会在OLD区进行full GC；\n- full GC后，若Survivor及OLD区仍然无法存放从Eden复制过来的部分对象，导致JVM无法在Eden区为新对象创建内存区域，则出现”out of memory错误”： outOfMemoryError：java heap space\n\n\n#### 虚拟机栈和本地方法栈溢出\n如果线程请求的栈深度大于虚拟机所允许的最大深度，将抛出StackOverflowError异常。\n不断创建线程，如果虚拟机在扩展栈时无法申请到足够的内存空间，则抛出OutOfMemoryError异常\n这里需要注意当栈的大小越大可分配的线程数就越少。\n用Xss设置\n\n\n#### 运行时常量池溢出\n异常信息：java.lang.OutOfMemoryError:PermGen space\n如果要向运行时常量池中添加内容，最简单的做法就是使用String.intern()这个Native方法。\n该方法的作用是：如果池中已经包含一个等于此String的字符串，则返回代表池中这个字符串的String对象；否则，将此String对象包含的字符串添加到常量池中，并且返回此String对象的引用。\n由于常量池分配在方法区内，我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小，从而间接限制其中常量池的容量。\n\n\n#### 方法区溢出\n异常信息：java.lang.OutOfMemoryError: PermGen space\n\n方法区用于存放Class的相关信息，如类名、访问修饰符、常量池、字段描述、方法描述等。\n\n\n所以如果程序加载的类过多，或者使用反射、gclib等这种动态代理生成类的技术，就可能导致该区发生内存溢出\n\n方法区溢出也是一种常见的内存溢出异常，一个类如果要被垃圾收集器回收，判定条件是很苛刻的。在经常动态生成大量Class的应用中，要特别注意这点。\n\n我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小\n\n#### java.lang.OutOfMemoryError: GC overhead limit exceeded\n原因：执行垃圾收集的时间比例太大, 有效的运算量太小. 默认情况下, 如果GC花费的时间超过 98%, 并且GC回收的内存少于 2%, JVM就会抛出这个错误。\n\n目的是为了让应用终止，给开发者机会去诊断问题。一般是应用程序在有限的内存上创建了大量的临时对象或者弱引用对象，从而导致该异常。\n\n解决方法：\n1. 大对象在使用之后指向null。\n2. 增加参数，-XX:-UseGCOverheadLimit，关闭这个特性；\n3. 增加heap大小，-Xmx1024m\n\n\n\n### SOF （堆栈溢出 StackOverflow）\nStackOverflowError 的定义：当应用程序递归太深而发生堆栈溢出时，抛出该错误。 因为栈一般默认为1-2M，一旦出现死循环或者是大量的递归调用，在不断的压栈过程中，造成栈容量超过1M而导致溢出。\n\n栈溢出的原因：\n- 递归调用\n- 大量循环或死循环\n- 全局变量是否过多\n- 数组、List、map数据过大\n\n\n\n### 如何避免发生内存泄露和溢出\n1. 尽早释放无用对象的引用\n2. 使用字符串处理，避免使用String，应大量使用StringBuffer，每一个String对象都得独立占用内存一块区域\n3. 尽量少用静态变量，因为静态变量存放在永久代（方法区），永久代基本不参与垃圾回收\n4. 避免在循环中创建对象\n5. 开启大型文件或从数据库一次拿了太多的数据很容易造成内存溢出，所以在这些地方要大概计算一下数据量的最大值是多少，并且设定所需最小及最大的内存空间值。\n\n\n# 参考文章\n- https://www.cnblogs.com/haimishasha/p/11329510.html"
  },
  {
    "path": "Java-基础/数组.md",
    "content": "\n* [初始化](#初始化)\n  * [静态初始化](#静态初始化)\n  * [动态初始化](#动态初始化)\n* [Java 数组和内存](#java-数组和内存)\n* [参考文章](#参考文章)\n\n\n# 初始化\n## 静态初始化\nint[] a = {1,2,3};\n## 动态初始化\nint[] a = new int[3];\n\n# Java 数组和内存\nJava 数组在内存中的存储是这样的：\n\n数组对象（这里可以看成一个指针）存储在栈中。\n\n数组元素存储在堆中。\n\n如下图所示：只有当 JVM 执行 new String[] 时，才会在堆中开辟相应的内存区域。数组对象 array 可以视为一个指针，指向这块内存的存储地址。\n\n![](../img/基础/Java数组内存.png)\n\n# 参考文章\n- https://dunwu.github.io/javacore/basics/java-array.html#_1-4-java-%E6%95%B0%E7%BB%84%E5%92%8C%E5%86%85%E5%AD%98"
  },
  {
    "path": "Java-基础/泛型.md",
    "content": "\n* [什么是泛型？为什么要使用泛型？](#什么是泛型为什么要使用泛型)\n* [泛型的使用](#泛型的使用)\n* [泛型上下边界](#泛型上下边界)\n    * [上边界](#上边界)\n    * [下边界](#下边界)\n\n## java 中泛型标记符：\n\n- E - Element (在集合[*主要分为Collection和Map两个接口]中使用，因为集合中存放的是元素)\n- T - Type（Java 类）\n- K - Key（键）\n- V - Value（值）\n- N - Number（数值类型）\n- ？ - 表示不确定的 java 类型\n## 什么是泛型？为什么要使用泛型？\n- 泛型，即“参数化类型”。一提到参数，最熟悉的就是定义方法时有形参，然后调用此方法时传递实参。那么参数化类型怎么理解呢？顾名思义，就是将类型由原来的具体的类型参数化，类似于方法中的变量参数，此时类型也定义成参数形式（可以称之为类型形参），然后在使用/调用时传入具体的类型（类型实参）。\n- 泛型的本质是为了参数化类型（在不创建新的类型的情况下，通过泛型指定的不同类型来控制形参具体限制的类型）。也就是说在泛型使用过程中，操作的数据类型被指定为一个参数，这种参数类型可以用在类、接口和方法中，分别被称为泛型类、泛型接口、泛型方法。\n## 泛型的使用 \n- 泛型类\n- 泛型接口\n- 泛型方法\n\n参考 https://blog.csdn.net/zhanshixiang/article/details/82559259\n\n## 泛型上下边界\n### 上边界\nextends\n- 类型必须继承指定的某个类(class)，或实现某个接口(interface)。\n### 下边界\nsuper\n- 指定的类型不能小于操作的类，即指定类及其父类\n### 边界参考 https://www.jianshu.com/p/00d50c1e46a9"
  },
  {
    "path": "Java-基础/继承.md",
    "content": "\n* [访问修饰符](#访问修饰符)\n* [抽象类与接口](#抽象类与接口)\n  * [抽象类](#抽象类)\n  * [接口](#接口)\n* [super](#super)\n* [重写与重载](#重写与重载)\n  * [重写（Override）](#重写override)\n  * [重载（Overload）](#重载overload)\n* [多态的实现与原理](#多态的实现与原理)\n\n\n## 访问修饰符\n- private\n- protected\n- default\n- public\n\n\n|访问权限|本类|本包的类|子类|非子类的外包类|\n|---|---|---|---|---|\n|public\t    |是\t|是\t|是\t|是|\n|protected\t|是\t|是\t|是\t|否|\n|default\t|是\t|是\t|否\t|否|\n|private\t|是\t|否\t|否\t|否|\n## 抽象类与接口\n### 抽象类\n- 抽象类和抽象方法都使用 abstract 关键字进行声明。如果一个类中包含抽象方法，那么这个类必须声明为抽象类。\n- 抽象类和普通类最大的区别是，抽象类不能被实例化，需要继承抽象类才能实例化其子类。\n### 接口\n- 接口是抽象类的延伸，在 Java 8 之前，它可以看成是一个完全抽象的类，也就是说它不能有任何的方法实现。\n- 从 Java 8 开始，接口也可以拥有默认的方法实现，这是因为不支持默认方法的接口的维护成本太高了。在 Java 8 之前，如果一个接口想要添加新的方法，那么要修改所有实现了该接口的类。\n- 接口的成员（字段 + 方法）默认都是 public 的，并且不允许定义为 private 或者 protected。\n- 接口的字段默认都是 static 和 final 的。\n## super\n- 访问父类的构造函数：可以使用 super() 函数访问父类的构造函数，从而委托父类完成一些初始化的工作。\n- 访问父类的成员：如果子类重写了父类的某个方法，可以通过使用 super 关键字来引用父类的方法实现。\n## 重写与重载\n### 重写（Override）\n存在于继承体系中，指子类实现了一个与父类在方法声明上完全相同的一个方法\n- 子类方法的访问权限必须大于等于父类方法；\n- 子类方法的返回类型必须是父类方法返回类型或为其子类型。\n- 子类方法抛出的异常类型必须是父类抛出异常类型或为其子类型\n### 重载（Overload）\n- 存在于同一个类中，指一个方法与已经存在的方法名称上相同，但是参数类型、个数、顺序至少有一个不同。\n- 应该注意的是，返回值不同，其它都相同不算是重载。\n## 多态的实现与原理\n多态指同一个行为具有不同的表现形式。java多态主要体现在两个方面：重写和重载。 重载时，调用相同的方法名，但可以根据参数的不同，对应不同的方法行为。 重写时，子类继承父类方法，并重写，通过父类引用指向子类对象，调用相同的方法名时，也可产生不同的行为。\n\n多态允许具体访问时实现方法的动态绑定。Java对于动态绑定的实现主要依赖于方法表，通过继承和接口的多态实现有所不同\n- 继承\n  - 在执行某个方法时，在方法区中找到该类的方法表，再确认该方法在方法表中的偏移量，找到该方法后如果被重写则直接调用，否则认为没有重写父类该方法，这时会按照继承关系搜索父类的方法表中该偏移量对应的方法\n- 接口\n  - Java 允许一个类实现多个接口，从某种意义上来说相当于多继承，这样同一个接口的的方法在不同类方法表中的位置就可能不一样了。所以不能通过偏移量的方法，而是通过搜索完整的方法表\n\n# 参考文章\n- https://www.nowcoder.com/questionTerminal/e83cdcb915f246a4866dac132128259a"
  },
  {
    "path": "Java-基础/虚拟线程.md",
    "content": "# 虚拟线程\n\n## 背景\n2022-09-20，JDK 19 发布了GA版本，备受瞩目的协程功能也算尘埃落地，不过，此次 GA版本并不是以协程来命名，而是使用了 Virtual\n\nThread（虚拟线程），并且是 preview预览版本。小编最早关注到协程功能是在 2020年，那时孵化项目叫做 Java project Loom，\n使用的是 Fiber（直译为：纤维，意译为：轻量级线程，即协程），但是 GA版本为何最终被定义为 Virtual Thread（虚拟线程），原因不得而知。\n\n- GA: General Availability，正式发布的版本，在国外都是用 GA来指代 release版本；\n- JEP: JDK Enhancement Proposal, JDK增强建议，JEP是一个JDK核心技术相关的增强建议文档；\n\n为什么需要虚拟线程\n既然 Java官方推出一个和线程这么相近的概念，必定是要解决线程的某些问题，因此，我们先回顾下线程的一些特点：\n\nJava中的线程是对操作系统线程的一个简单包装，线程的创建，调度和销毁等都是由操作系统完成；\n线程切换需要消耗CPU时间，这部分时间是与业务无关的；\n线程的性能直接受操作系统处理能力的影响；\n\n因此，线程是一种重量级的资源，作为一名 Java程序员应该深有体会。所以，为了更好的管理线程，Java采用了池化（线程池）的方式进行管理线程，避免线程频繁创建和销毁带来的开销。但是，尽管线程池避免线程大部分创建和销毁的开销，但是线程的调度还是直接受操作系统的影响，那么有没有更好的方式来打破这种限制，因此，虚拟线程就孕育而生。\n在 JDK 19源码中，官方直接在 java.lang包下新增一个 VirtualThread类来表示虚拟线程，为了更好的区分虚拟线程和原有的 Thread线程，官方给 Thread类赋予了一个高大上的名字：平台线程。\n下面给出了 JDK 19中虚拟线程的 Diagram截图以及平台线程和系统线程的关系图：\n\n![虚拟线程的 Diagram截图以及平台线程和系统线程的关系图.png](..%2Fimg%2FJava%E5%9F%BA%E7%A1%80%2F%E8%99%9A%E6%8B%9F%E7%BA%BF%E7%A8%8B%E7%9A%84%20Diagram%E6%88%AA%E5%9B%BE%E4%BB%A5%E5%8F%8A%E5%B9%B3%E5%8F%B0%E7%BA%BF%E7%A8%8B%E5%92%8C%E7%B3%BB%E7%BB%9F%E7%BA%BF%E7%A8%8B%E7%9A%84%E5%85%B3%E7%B3%BB%E5%9B%BE.png)\n\n![os和系统线程.png](..%2Fimg%2FJava%E5%9F%BA%E7%A1%80%2Fos%E5%92%8C%E7%B3%BB%E7%BB%9F%E7%BA%BF%E7%A8%8B.png)\n\n## 如何创建虚拟线程\n\n### 1.通过 Thread.startVirtualThread()创建\n如下示例代码，通过 Thread.startVirtualThread()可以创建一个新的并且已启动的虚拟线程，该方法等价于 Thread.ofVirtual().start(task)：\n```java\npublic class VirtualThreadTest {\n\n    public static void main(String[] args) {\n        CustomThread customThread = new CustomThread();\n// 创建并且启动虚拟线程  \n        Thread.startVirtualThread(customThread);\n    }\n}\n\nclass CustomThread implements Runnable {\n    @Override\n    public void run() {\n        System.out.println(\"CustomThread run\");\n    }\n}\n```\n### 2.通过 Thread.ofVirtual()创建\n如下示例代码，通过 Thread.ofVirtual().unstarted()方式可以创建一个新的未启动的虚拟线程，然后通过 Thread.start()来启动线程，也可以通过 Thread.ofVirtual().start()直接创建一个新的并已启动的虚拟线程：\n\n```java\npublic class VirtualThreadTest {\n\n    public static void main(String[] args) {\n        CustomThread customThread = new CustomThread();\n// 创建并且不启动虚拟线程，然后 unStarted.start()方法启动虚拟线程  \n        Thread unStarted = Thread.ofVirtual().unstarted(customThread);\n        unStarted.start();\n\n// 等同于  \n        Thread.ofVirtual().start(customThread);\n    }\n}\n\nclass CustomThread implements Runnable {\n    @Override\n    public void run() {\n        System.out.println(\"CustomThread run\");\n    }\n}  \n```\n### 3.通过 ThreadFactory创建\n如下示例代码，通过 ThreadFactory.newThread()方式就能创建一个虚拟线程，然后通过 Thread.start()来启动线程：\n```java\npublic class VirtualThreadTest {\n\n    public static void main(String[] args) {\n        CustomThread customThread = new CustomThread();\n// 获取线程工厂类  \n        ThreadFactory factory = Thread.ofVirtual().factory();\n// 创建虚拟线程  \n        Thread thread = factory.newThread(customThread);\n// 启动线程  \n        thread.start();\n    }\n}\n\nclass CustomThread implements Runnable {\n    @Override\n    public void run() {\n        System.out.println(\"CustomThread run\");\n    }\n}\n```\n\n### 4.通过 Executors.newVirtualThreadPerTaskExecutor()创建\n如下示例代码，通过 JDK自带的Executors工具类方式创建一个虚拟线程，然后通过 executor.submit()来启动线程：\n```java\npublic class VirtualThreadTest {\n\n    public static void main(String[] args) {\n        CustomThread customThread = new CustomThread();\n        ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();\n        executor.submit(customThread);\n    }\n}\n\nclass CustomThread implements Runnable {\n    @Override\n    public void run() {\n        System.out.println(\"CustomThread run\");\n    }\n}\n```\n\n通过上述列举的 4种创建虚拟线程的方式可以看出，官方为了降低虚拟线程的门槛，尽力复用原有的Thread线程类，这样可以平滑的过渡到虚拟线程的使用。不过，在\nJava 19中，虚拟线程还是一个预览功能，默认关闭，需要使用参数 --enable-preview 来启用该功能，预览功能源码和启动虚拟线程指令如下：\n```java\n// Thread 源码，通过 @PreviewFeature 注解来标注 虚拟线程为 预览功能  \npublic class Thread implements Runnable {\n    /**\n     * Creates a virtual thread to execute a task and schedules it to execute.  \n     This method is equivalent to: Thread.ofVirtual().start(task);  \n     Params: task – the object to run when the thread executes  \n     Returns: a new, and started, virtual thread  \n     Throws: UnsupportedOperationException – if preview features are not enabled  \n     Since: 19  \n     See Also: Inheritance when creating threads  \n     * @param task\n     * @return\n     */\n    @PreviewFeature(feature = PreviewFeature.Feature.VIRTUAL_THREADS)\n    public static Thread startVirtualThread(Runnable task) {\n        Objects.requireNonNull(task);\n        // 判断是否开启虚拟线程功能  \n        PreviewFeatures.ensureEnabled();\n        var thread = ThreadBuilders.newVirtualThread(null, null, 0, task);\n        thread.start();\n        return thread;\n    }\n\n    // 异常信息提醒 可以通过 --enable-preview 开启虚拟线程功能  \n    public static void ensureEnabled() {\n        if (!isEnabled()) {\n            throw new UnsupportedOperationException(\n                    \"Preview Features not enabled, need to run with --enable-preview\");\n        }\n    }\n}  \n\n```\nIDEA 中配置 --enable-preview 如下图：\n\n![启用虚拟线程idea.png](..%2Fimg%2FJava%E5%9F%BA%E7%A1%80%2F%E5%90%AF%E7%94%A8%E8%99%9A%E6%8B%9F%E7%BA%BF%E7%A8%8Bidea.png)\n\n\n为了更好的感受虚拟线程的性能，我们模拟一个对比测试用例：分别使用虚拟线程和线程池执行10w个任务，每个线程任务睡眠10ms，统计各自的总耗时和创建的最大平台线程总数，示例代码如下：\n\n```java\n// 虚拟线程\npublic class VirtualThreadTest {\n    static List<Integer> list = new ArrayList<>();\n\n    public static void main(String[] args) {\n// 开启一个线程来监控当前的平台线程（系统线程）总数\n        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);\n        scheduledExecutorService.scheduleAtFixedRate(() -> {\n            ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();\n            ThreadInfo[] threadInfo = threadBean.dumpAllThreads(false, false);\n            saveMaxThreadNum(threadInfo.length);\n        }, 10, 10, TimeUnit.MILLISECONDS);\n\n        long start = System.currentTimeMillis();\n        ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();\n        for (int i = 0; i < 10000; i++) {\n            executor.submit(() -> {\n// 线程睡眠 10ms，可以等同于模拟业务耗时10ms\n                try {\n                    TimeUnit.MILLISECONDS.sleep(10);\n                } catch (InterruptedException e) {\n\n                }\n            });\n        }\n        executor.close();\n        System.out.println(\"max：\" + list.get(0) + \" platform thread/os thread\");\n        System.out.printf(\"totalMillis：%dms\\n\", System.currentTimeMillis() - start);\n    }\n}\n\npublic class ThreadTest {\n    static List<Integer> list = new ArrayList<>();\n    public static void main(String[] args) {\n// 开启一个线程来监控当前的平台线程（系统线程）总数\n        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);\n        scheduledExecutorService.scheduleAtFixedRate(() -> {\n            ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();\n            ThreadInfo[] threadInfo = threadBean.dumpAllThreads(false, false);\n            saveMaxThreadNum(threadInfo.length);\n        }, 1, 1, TimeUnit.SECONDS);\n\n        long start = System.currentTimeMillis();\n        ExecutorService executor = Executors.newFixedThreadPool(200);\n        for (int i = 0; i < 100000; i++) {\n            executor.submit(() -> {\n                try {\n// 线程睡眠 10ms，可以等同于模拟业务耗时10ms\n                    TimeUnit.MILLISECONDS.sleep(10);\n                } catch (InterruptedException e) {\n\n                }\n            });\n        }\n        executor.close();\n        System.out.println(\"max：\" + list.get(0) + \" platform thread/os thread\");\n        System.out.printf(\"totalMillis：%dms\\n\", System.currentTimeMillis() - start);\n    }\n}\n\n    // 保存平台线程的创建的最大总数\n    public static List<Integer> saveMaxThreadNum(int num) {\n        if (list.isEmpty()) {\n            list.add(num);\n        } else {\n            Integer integer = list.get(0);\n            if (num > integer) {\n                list.add(0, num);\n            }\n        }\n        return list;\n    }\n```\n两个示例的运行结果：\n\n![运行结果.png](..%2Fimg%2FJava%E5%9F%BA%E7%A1%80%2F%E8%BF%90%E8%A1%8C%E7%BB%93%E6%9E%9C.png)\n\n通过运行结果可以发现：\n\n\n- 使用虚拟线程执行 10w个任务总耗时为：129ms，最大创建了 18个平台线程；\n- 使用线程池执行 10w个任务总耗时为：6103 ms，最大创建了 207个平台线程；\n- 两者总耗时差50倍，最大创建的平台线程总数差 10倍，因此性能差可想而知；\n\n### 核心源码解析\n首先从 VirtualThread类开始，源码如下：\n```java\n/**\n * A thread that is scheduled by the Java virtual machine rather than the operating system.  \n */\nfinal class VirtualThread extends BaseVirtualThread {\n\n    /**\n     * Creates a new {@code VirtualThread} to run the given task with the given  \n     * scheduler. If the given scheduler is {@code null} and the current thread  \n     * is a platform thread then the newly created virtual thread will use the  \n     * default scheduler. If given scheduler is {@code null} and the current  \n     * thread is a virtual thread then the current thread's scheduler is used.  \n     *\n     * @param scheduler the scheduler or null  \n     * @param name thread name  \n     * @param characteristics characteristics  \n     * @param task the task to execute  \n     */\n    VirtualThread(Executor scheduler, String name, int characteristics, Runnable task) {\n        super(name, characteristics, /*bound*/ false);\n        Objects.requireNonNull(task);\n\n// choose scheduler if not specified  \n        if (scheduler == null) {\n            Thread parent = Thread.currentThread();\n            if (parent instanceof VirtualThread vparent) {\n                scheduler = vparent.scheduler;\n            } else {\n                scheduler = DEFAULT_SCHEDULER;\n            }\n        }\n\n        this.scheduler = scheduler;\n        this.cont = new VThreadContinuation(this, task);\n        this.runContinuation = this::runContinuation;\n    }\n\n    /**\n     * 创建默认的调度器  \n     * Creates the default scheduler.  \n     */\n    @SuppressWarnings(\"removal\")\n    private static ForkJoinPool createDefaultScheduler() {\n        ForkJoinWorkerThreadFactory factory = pool -> {\n            PrivilegedAction<ForkJoinWorkerThread> pa = () -> new CarrierThread(pool);\n            return AccessController.doPrivileged(pa);\n        };\n        PrivilegedAction<ForkJoinPool> pa = () -> {\n            int parallelism, maxPoolSize, minRunnable;\n            String parallelismValue = System.getProperty(\"jdk.virtualThreadScheduler.parallelism\");\n            String maxPoolSizeValue = System.getProperty(\"jdk.virtualThreadScheduler.maxPoolSize\");\n            String minRunnableValue = System.getProperty(\"jdk.virtualThreadScheduler.minRunnable\");\n            if (parallelismValue != null) {\n                parallelism = Integer.parseInt(parallelismValue);\n            } else {\n                parallelism = Runtime.getRuntime().availableProcessors();\n            }\n            if (maxPoolSizeValue != null) {\n                maxPoolSize = Integer.parseInt(maxPoolSizeValue);\n                parallelism = Integer.min(parallelism, maxPoolSize);\n            } else {\n                maxPoolSize = Integer.max(parallelism, 256);\n            }\n            if (minRunnableValue != null) {\n                minRunnable = Integer.parseInt(minRunnableValue);\n            } else {\n                minRunnable = Integer.max(parallelism / 2, 1);\n            }\n            Thread.UncaughtExceptionHandler handler = (t, e) -> { };\n            boolean asyncMode = true; // FIFO  \n            return new ForkJoinPool(parallelism, factory, handler, asyncMode,\n                    0, maxPoolSize, minRunnable, pool -> true, 30, SECONDS);\n        };\n        return AccessController.doPrivileged(pa);\n    }\n}  \n\n```\n通过 VirtualThread类的源码可以总结出：\n\n- VirtualThread继承 BaseVirtualThread类，BaseVirtualThread类继承 Thread类；\n- 虚拟线程是 JVM进行调度的，而不是操作系统；\n- VirtualThread类是一个终态类，因此该类无法被继承，无法被扩展；\n\nVirtualThread类，只提供了一个构造器，接收 4个参数：\n\n- Executor scheduler：如果给定的调度器为空并且当前线程是平台线程，那么新创建的虚拟线程将使用默认调度程序（底层采用 ForkJoinPool），如果给定的调度器为空并且当前线程是虚拟线程，则使用当前线程的调度程序\n- String name：自定义线程名\n- int characteristics：线程特征值\n- Runnable task：需要执行的任务\n\n然后我们看下 JDK中创建虚拟线程的源码：\n```java\npublic class Thread implements Runnable {\n    /**\n     * Creates a virtual thread to execute a task and schedules it to execute.  \n     This method is equivalent to: Thread.ofVirtual().start(task);  \n     Params: task – the object to run when the thread executes  \n     Returns: a new, and started, virtual thread  \n     Throws: UnsupportedOperationException – if preview features are not enabled  \n     Since: 19  \n     See Also: Inheritance when creating threads  \n     * @param task\n     * @return\n     */\n    @PreviewFeature(feature = PreviewFeature.Feature.VIRTUAL_THREADS)\n    public static Thread startVirtualThread(Runnable task) {\n        Objects.requireNonNull(task);\n// 判断是否开启虚拟线程功能  \n        PreviewFeatures.ensureEnabled();\n        var thread = ThreadBuilders.newVirtualThread(null, null, 0, task);\n        thread.start();\n        return thread;\n    }\n\n    // 异常信息提醒 可以通过 --enable-preview 开启虚拟线程功能  \n    public static void ensureEnabled() {\n        if (!isEnabled()) {\n            throw new UnsupportedOperationException(\n                    \"Preview Features not enabled, need to run with --enable-preview\");\n        }\n    }\n}\n\nclass ThreadBuilders {\n    static Thread newVirtualThread(Executor scheduler,\n                                   String name,\n                                   int characteristics,\n                                   Runnable task) {\n        if (ContinuationSupport.isSupported()) {\n            return new VirtualThread(scheduler, name, characteristics, task);\n        } else {\n            if (scheduler != null)\n                throw new UnsupportedOperationException();\n            return new BoundVirtualThread(name, characteristics, task);\n        }\n    }\n\n    /**\n     * Returns a builder for creating a virtual {@code Thread} or {@code ThreadFactory}\n     * that creates virtual threads.  \n     *\n     * @apiNote The following are examples using the builder:  \n     * {@snippet :\n     * // Start a virtual thread to run a task.  \n     * Thread thread = Thread.ofVirtual().start(runnable);  \n     *\n     * // A ThreadFactory that creates virtual threads  \n     * ThreadFactory factory = Thread.ofVirtual().factory();  \n     * }\n     *\n     * @return A builder for creating {@code Thread} or {@code ThreadFactory} objects.  \n     * @throws UnsupportedOperationException if preview features are not enabled  \n     * @since 19\n     */\n    @PreviewFeature(feature = PreviewFeature.Feature.VIRTUAL_THREADS)\n    public static Builder.OfVirtual ofVirtual() {\n        PreviewFeatures.ensureEnabled();\n        return new ThreadBuilders.VirtualThreadBuilder();\n    }\n}  \n\n```\nThread.startVirtualThread()创建虚拟线程，会调用ThreadBuilders.newVirtualThread()，最终调用 new VirtualThread()构造器来创建虚拟线程。\n\n从上文我们在介绍虚拟线程创建的 4种方式也可以看出，虚拟线程创建的入口在 Thread 或者 Executors 类中，和以前使用线程或者线程池的习惯保持一致。\n\n```java\nfinal class VirtualThread extends BaseVirtualThread {\n    /**\n     * Mounts this virtual thread onto the current platform thread. On  \n     * return, the current thread is the virtual thread.  \n     */\n    @ChangesCurrentThread\n    private void mount() {\n// sets the carrier thread  \n        Thread carrier = Thread.currentCarrierThread();\n        setCarrierThread(carrier);\n\n// sync up carrier thread interrupt status if needed  \n        if (interrupted) {\n            carrier.setInterrupt();\n        } else if (carrier.isInterrupted()) {\n            synchronized (interruptLock) {\n// need to recheck interrupt status  \n                if (!interrupted) {\n                    carrier.clearInterrupt();\n                }\n            }\n        }\n\n// set Thread.currentThread() to return this virtual thread  \n        carrier.setCurrentThread(this);\n    }\n\n\n    /**\n     * Unmounts this virtual thread from the carrier. On return, the  \n     * current thread is the current platform thread.  \n     */\n    @ChangesCurrentThread\n    private void unmount() {\n// set Thread.currentThread() to return the platform thread  \n        Thread carrier = this.carrierThread;\n        carrier.setCurrentThread(carrier);\n\n// break connection to carrier thread, synchronized with interrupt  \n        synchronized (interruptLock) {\n            setCarrierThread(null);\n        }\n        carrier.clearInterrupt();\n    }\n}  \n\n```\nmount() 和 unmount() 是虚拟线程两个核心方法：\n\n- mount()，可以将此虚拟线程挂载到当前平台线程上，返回时，当前线程是虚拟线程；\n- unmount()，从载体线程卸载此虚拟线程，返回时，当前线程是平台线程\n\n通过这两个方式可以看出虚拟线程是搭载在平台线程上运行，运行结束后，从平台线程上卸载。\n\n### 3种线程的关系\nVirtualThread，Platform Thread，OS Thread 三者的关系如下图：\n\n![三线程关系.png](..%2Fimg%2FJava%E5%9F%BA%E7%A1%80%2F%E4%B8%89%E7%BA%BF%E7%A8%8B%E5%85%B3%E7%B3%BB.png)\n\n说明：\n- 在现有的线程模型下，一个 Java线程相当于一个操作系统线程，多个虚拟线程需要挂载在一个平台线程（载体线程）上，每个平台线程和系统线程一一对应。因此，VirtualThread是属于 JVM级别的线程，由JVM调度，它是非常轻量级的资源，使用完后立即被销毁，因此就不需要像平台线程一样使用池化（线程池）。\n- 虚拟线程在执行到 IO 操作或 Blocking操作时，会自动切换到其他虚拟线程执行，从而避免当前线程等待，可以高效通过少数线程去调度大量虚拟线程，最大化提升线程的执行效率。\n\n## 总结\n\n- Virtual Thread将会在性能上带来的巨大提高，不过，目前业界80~90%的代码还跑在 Java 8上，等 JDK\n- 19投入实际生产环境，可能需要一个漫长的过程；\n- 虚拟线程高度复用了现有的 Thread线程的功能，方便现有方式平滑迁移到虚拟线程；\n- 虚拟线程是将 Thread作为载体线程，它并没有改变原来的线程模型；\n- 虚拟线程是 JVM调度的，而不是操作系统调度；\n- 使用虚拟线程可以显著提高程序吞吐量；\n- 虚拟线程适合 并发任务数量很高 或者 IO密集型的场景，对于 计算密集型任务还需通过过增加CPU核心解决，或者利用分布式计算资源来来解决；\n- 虚拟线程目前只是一个预览功能，只能从源码和简单的测试来分析，并无真实生产环境的验证；\n\n曾一段时间内，JDK一直致力于 Reactor响应式编程，试图从这条路子来提升 Java的性能，但是最终发现：响应式编程难理解，难调试，难使用，\n\n因此又把焦点转向了同步编程，为了改善性能，虚拟线程诞生了。或许虚拟线程很难在短时间内运用到实际生产中，但是通过官方的JDK版本发布，我们可以看到：尽管是 Oracle这样的科技型巨头也会走弯路，了解 JDK的动态，可以帮助我们更好的把握学习 Java的重心以及后面的发展趋势。\n\n\n# 参考文章\n- 文章来源 https://juejin.cn/post/7258180488358084669\n\n"
  },
  {
    "path": "Java-基础/虚拟线程2.md",
    "content": "# jdk21虚拟线程\njdk21已经正式发布了虚拟线程\n\n虚拟线程和系统线程的绑定关系\n![](../img/Java基础/虚拟线程和系统线程.png)\n\n## 虚拟线程实现原理\n虚拟线程是一种轻量级（用户模式）线程，这种线程是由Java虚拟机调度，而不是操作系统。虚拟线程占用空间小，任务切换开销几乎可以忽略不计，因此可以极大量地创建和使用。总体来看，虚拟线程实现如下：\n\n```java\nvirtual thread = continuation + scheduler\n```\n\n虚拟线程会把任务（一般是java.lang.Runnable）包装到一个Continuation实例中：\n\n- 当任务需要阻塞挂起的时候，会调用Continuation的yield操作进行阻塞\n- 当任务需要解除阻塞继续执行的时候，Continuation会被继续执行\n\nScheduler也就是执行器，会把任务提交到一个载体线程池中执行：\n\n- 执行器是java.util.concurrent.Executor的子类\n- 虚拟线程框架提供了一个默认的ForkJoinPool用于执行虚拟线程任务\n\n下文会把carrier thread称为\"载体线程\"，指的是负责执行虚拟线程中任务的平台线程，或者说运行虚拟线程的平台线程称为它的载体线程\n\n操作系统调度系统线程，而Java平台线程与系统线程一一映射，所以平台线程被操作系统调度，但是虚拟线程是由JVM调度。JVM把虚拟线程分配给平台线程的操作称为mount（挂载），反过来取消分配平台线程的操作称为unmount（卸载）：\n\n- mount操作：虚拟线程挂载到平台线程，虚拟线程中包装的Continuation栈数据帧或者引用栈数据会被拷贝到平台线程的线程栈，这是一个从堆复制到栈的过程\n- unmount操作：虚拟线程从平台线程卸载，大多数虚拟线程中包装的Continuation栈数据帧会留在堆内存中\n\n这个mount -> run -> unmount过程用伪代码表示如下：\n\n```java\nmount();\ntry {\n    Continuation.run();\n} finally {\n    unmount();\n}\n```\n\n从Java代码的角度来看，虚拟线程和它的载体线程暂时共享一个OS线程实例这个事实是不可见，因为虚拟线程的堆栈跟踪和线程本地变量与平台线程是完全隔离的。JDK中专门是用了一个FIFO模式的ForkJoinPool作为虚拟线程的调度程序，从这个调度程序看虚拟线程任务的执行流程大致如下：\n\n- 调度器（线程池）中的平台线程等待处理任务\n\n![](../img/Java基础/调度器中的平台线程等待处理任务.png))\n\n- 一个虚拟线程被分配平台线程，该平台线程作为运载线程执行虚拟线程中的任务\n\n![](../img/Java基础/虚拟线程2.png))\n\n- 虚拟线程运行其Continuation，从而执行基于Runnable包装的用户任务\n\n![](../img/Java基础/虚拟线程3.png))\n\n- 虚拟线程任务执行完成，标记Continuation终结，标记虚拟线程为终结状态，清空一些上下文变量，运载线程\"返还\"到调度器（线程池）中作为平台线程等待处理下一个任务\n\n![](../img/Java基础/虚拟线程4.png)\n\n上面是描述一般的虚拟线程任务执行情况，在执行任务时候首次调用Continuation#run()获取锁（ReentrantLock）的时候会触发Continuation的yield操作让出控制权，等待虚拟线程重新分配运载线程并且执行，见下面的代码：\n\n```java\npublic class VirtualThreadLock {\n\n    public static void main(String[] args) throws Exception {\n        ReentrantLock lock = new ReentrantLock();\n        Thread.startVirtualThread(() -> {\n            lock.lock();     // <------ 这里确保锁已经被另一个虚拟线程持有\n        });\n        Thread.sleep(1000);\n        Thread.startVirtualThread(() -> {\n            System.out.println(\"first\");\n            lock.lock();\n            try {\n                System.out.println(\"second\");\n            } finally {\n                lock.unlock();\n            }\n            System.out.println(\"third\");\n        });\n        Thread.sleep(Long.MAX_VALUE);\n    }\n}\n```\n\n- 虚拟线程中任务执行时候首次调用Continuation#run()执行了部分任务代码，然后尝试获取锁，会导致Continuation的yield操作让出控制权（任务切换），也就是unmount，运载线程栈数据会移动到Continuation栈的数据帧中，保存在堆内存，虚拟线程任务完成（但是虚拟线程没有终结，同时其Continuation也没有终结和释放），运载线程被释放到执行器中等待新的任务；如果Continuation的yield操作失败，则会对运载线程进行park调用，阻塞在运载线程上\n\n![](../img/Java基础/虚拟线程5.png)\n\n- 当锁持有者释放锁之后，会唤醒虚拟线程获取锁（成功后），虚拟线程会重新进行mount，让虚拟线程任务再次执行，有可能是分配到另一个运载线程中执行，Continuation栈会的数据帧会被恢复到运载线程栈中，然后再次调用Continuation#run()恢复任务执行：\n\n![](../img/Java基础/虚拟线程6.png)\n\n- 最终虚拟线程任务执行完成，标记Continuation终结，标记虚拟线程为终结状态，清空一些上下文变量，运载线程\"返还\"到调度器（线程池）中作为平台线程等待处理下一个任务\n\nContinuation组件十分重要，它既是用户真实任务的包装器，也是任务切换虚拟线程与平台线程之间数据转移的一个句柄，它提供的yield操作可以实现任务上下文的中断和恢复。由于Continuation被封闭在java.base/jdk.internal.vm下，可以通过增加编译参数--add-exports java.base/jdk.internal.vm=ALL-UNNAMED暴露对应的功能，从而编写实验性案例，IDEA中可以按下图进行编译参数添加：\n\n![](../img/Java基础/虚拟线程7.png)\n\n然后编写和运行下面的例子：\n\n```java\nimport jdk.internal.vm.Continuation;\nimport jdk.internal.vm.ContinuationScope;\n\npublic class ContinuationDemo {\n\n    public static void main(String[] args) {\n        ContinuationScope scope = new ContinuationScope(\"scope\");\n        Continuation continuation = new Continuation(scope, () -> {\n            System.out.println(\"Running before yield\");\n            Continuation.yield(scope);\n            System.out.println(\"Running after yield\");\n        });\n        System.out.println(\"First run\");\n        // 第一次执行Continuation.run\n        continuation.run();\n        System.out.println(\"Second run\");\n        // 第二次执行Continuation.run\n        continuation.run();\n        System.out.println(\"Done\");\n    }\n}\n\n// 运行代码，神奇的结果出现了\nFirst run\nRunning before yield\nSecond run\nRunning after yield\nDone\n\n```\n这里可以看出Continuation的奇妙之处，Continuation实例进行yield调用后，再次调用其run方法就可以从yield的调用之处往下执行，从而实现了程序的中断和恢复。\n\n## 源码分析\n todo\n\n# 文章参考\n- https://www.cnblogs.com/throwable/p/16758997.html"
  },
  {
    "path": "Java-多线程/AQS.md",
    "content": "# AQS（AbstractQueuedSynchronizer）\n## 工作原理概要\nAbstractQueuedSynchronizer又称为队列同步器(后面简称AQS)，它是用来构建锁或其他同步组件的基础框架，内部通过一个int类型的成员变量state来控制同步状态\n\n当state=0时，则说明没有任何线程占有共享资源的锁，当state=1时，则说明有线程目前正在使用共享变量，其他线程必须加入同步队列进行等待\n\nAQS内部通过内部类Node构成FIFO的同步队列来完成线程获取锁的排队工作，同时利用内部类ConditionObject构建等待队列\n\n当Condition调用wait()方法后，线程将会加入等待队列中\n\n而当Condition调用signal()方法后，线程将从等待队列转移动同步队列中进行锁竞争。\n\n注意这里涉及到两种队列，一种的同步队列，当线程请求锁而等待的后将加入同步队列等待，而另一种则是等待队列(可有多个)，通过Condition调用await()方法释放锁后，将加入等待队列\n## 同步队列模型\n### 属性\n- `private transient volatile Node head;` //指向同步队列队头\n- `private transient volatile Node tail;` //指向同步的队尾\n- `private volatile int state;` //同步状态，0代表锁未被占用，1代表锁已被占用\n### 示意图\n![](../img/Java多线程/同步队列模型示意图.png)\n- head指向同步队列的头部，注意head为空结点，不存储信息\n- tail则是同步队列的队尾\n- 同步队列采用的是双向链表的结构这样可方便队列进行结点增删操作\n- state变量则是代表同步状态，执行当线程调用lock方法进行加锁后，如果此时state的值为0，则说明当前线程可以获取到锁(在本篇文章中，锁和同步状态代表同一个意思)，同时将state设置为1，表示获取成功。如果state已为1，也就是当前锁已被其他线程持有，那么当前执行线程将被封装为Node结点加入同步队列等待\n### Node\n属性\n```java\nstatic final class Node {\n    //共享模式\n    static final Node SHARED = new Node();\n    //独占模式\n    static final Node EXCLUSIVE = null;\n\n    //标识线程已处于结束状态\n    static final int CANCELLED =  1;\n    //等待被唤醒状态\n    static final int SIGNAL    = -1;\n    //条件状态，\n    static final int CONDITION = -2;\n    //在共享模式中使用表示获得的同步状态会被传播\n    static final int PROPAGATE = -3;\n\n    //等待状态,存在CANCELLED、SIGNAL、CONDITION、PROPAGATE 4种\n    volatile int waitStatus;\n\n    //同步队列中前驱结点\n    volatile Node prev;\n\n    //同步队列中后继结点\n    volatile Node next;\n\n    //请求锁的线程\n    volatile Thread thread;\n\n    //等待队列中的后继结点，这个与Condition有关，稍后会分析\n    Node nextWaiter;\n\n    //判断是否为共享模式\n    final boolean isShared() {\n        return nextWaiter == SHARED;\n    }\n\n    //获取前驱结点\n    final Node predecessor() throws NullPointerException {\n        Node p = prev;\n        if (p == null)\n            throw new NullPointerException();\n        else\n            return p;\n    }\n\n    //.....\n}\n```\n- `SHARED`和`EXCLUSIVE`常量分别代表共享模式和独占模式\n  - 共享模式是一个锁允许多条线程同时操作，如信号量Semaphore采用的就是基于AQS的共享模式实现的\n  - 独占模式则是同一个时间段只能有一个线程对共享资源进行操作，多余的请求线程需要排队等待，如ReentranLock\n- `waitStatus`  则表示当前被封装成Node结点的等待状态\n  - `CANCELLED`\n  值为1，在同步队列中等待的线程等待超时或被中断，需要从同步队列中取消该Node的结点，其结点的waitStatus为CANCELLED，即结束状态，进入该状态后的结点将不会再变化\n  - `SIGNAL`\n  值为-1，被标识为该等待唤醒状态的后继结点，当其前继结点的线程释放了同步锁或被取消，将会通知该后继结点的线程执行。说白了，就是处于唤醒状态，只要前继结点释放锁，就会通知标识为SIGNAL状态的后继结点的线程执行\n  - `CONDITION`\n  值为-2，与Condition相关，该标识的结点处于等待队列中，结点的线程等待在Condition上，当其他线程调用了Condition的signal()方法后，CONDITION状态的结点将从等待队列转移到同步队列中，等待获取同步锁\n  - `PROPAGATE`\n  值为-3，与共享模式相关，在共享模式中，该状态标识结点的线程处于可运行状态\n  - `0状态`\n  值为0，代表初始化状态\n- `pre` 指向当前Node结点的前驱结点\n- `next` 指向当前Node结点的后继结点\n- `thread` 存储的请求锁的线程\n- `nextWaiter` 与Condition相关，代表等待队列中的后继结点\n\n# ReentrantLock\nReentrantLock内部存在3个实现类，分别是Sync、NonfairSync、FairSync\n## Sync (extends AbstractQueuedSynchronizer)\nSync继承自AQS实现了解锁tryRelease()方法\n## NonfairSync (extends Sync) 非公平锁\n### lock\n```java\n/**\n * 非公平锁实现\n */\nstatic final class NonfairSync extends Sync {\n    //加锁\n    final void lock() {\n        //执行CAS操作，获取同步状态\n        if (compareAndSetState(0, 1))\n       //成功则将独占锁线程设置为当前线程  \n          setExclusiveOwnerThread(Thread.currentThread());\n        else\n            //否则再次请求同步状态\n            acquire(1);\n    }\n}\n```\n这里获取锁时，首先对同步状态执行CAS操作，尝试把state的状态从0设置为1，如果返回true则代表获取同步状态成功，也就是当前线程获取锁成，可操作临界资源，如果返回false，则表示已有线程持有该同步状态(其值为1)，获取锁失败，注意这里存在并发的情景，也就是可能同时存在多个线程设置state变量，因此是CAS操作保证了state变量操作的原子性。返回false后，执行 acquire(1)方法，该方法是AQS中的方法，它对中断不敏感，即使线程获取同步状态失败，进入同步队列，后续对该线程执行中断操作也不会从同步队列中移出，方法如下\n\n```java\npublic final void acquire(int arg) {\n    //再次尝试获取同步状态\n    if (!tryAcquire(arg) &&\n        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))\n        selfInterrupt();\n}\n```\n这里传入参数arg表示要获取同步状态后设置的值(即要设置state的值)，因为要获取锁，而status为0时是释放锁，1则是获取锁，所以这里一般传递参数为1，进入方法后首先会执行tryAcquire(arg)方法，在前面分析过该方法在AQS中并没有具体实现，而是交由子类实现，因此该方法是由ReetrantLock类内部实现的\n\n```java\n//NonfairSync类\nstatic final class NonfairSync extends Sync {\n\n    protected final boolean tryAcquire(int acquires) {\n         return nonfairTryAcquire(acquires);\n     }\n }\n\n//Sync类\nabstract static class Sync extends AbstractQueuedSynchronizer {\n\n  //nonfairTryAcquire方法\n  final boolean nonfairTryAcquire(int acquires) {\n      final Thread current = Thread.currentThread();\n      int c = getState();\n      //判断同步状态是否为0，并尝试再次获取同步状态\n      if (c == 0) {\n          //执行CAS操作\n          if (compareAndSetState(0, acquires)) {\n              setExclusiveOwnerThread(current);\n              return true;\n          }\n      }\n      //如果当前线程已获取锁，属于重入锁，再次获取锁后将status值加1\n      else if (current == getExclusiveOwnerThread()) {\n          int nextc = c + acquires;\n          if (nextc < 0) // overflow\n              throw new Error(\"Maximum lock count exceeded\");\n          //设置当前同步状态，当前只有一个线程持有锁，因为不会发生线程安全问题，可以直接执行 setState(nextc);\n          setState(nextc);\n          return true;\n      }\n      return false;\n  }\n  //省略其他代码\n}\n```\n从代码执行流程可以看出，这里做了两件事\n\n1. 一是尝试再次获取同步状态，如果获取成功则将当前线程设置为OwnerThread，否则失败，\n2. 二是判断当前线程current是否为OwnerThread，如果是则属于重入锁，state自增1，并获取锁成功，返回true，反之失败，返回false，也就是tryAcquire(arg)执行失败，返回false。\n\n需要注意的是nonfairTryAcquire(int acquires)内部使用的是CAS原子性操作设置state值，可以保证state的更改是线程安全的，因此只要任意一个线程调用nonfairTryAcquire(int acquires)方法并设置成功即可获取锁，不管该线程是新到来的还是已在同步队列的线程，毕竟这是非公平锁，并不保证同步队列中的线程一定比新到来线程请求(可能是head结点刚释放同步状态然后新到来的线程恰好获取到同步状态)先获取到锁，这点跟后面还会讲到的公平锁不同。ok~，接着看之前的方法acquire(int arg)\n\n```java\npublic final void acquire(int arg) {\n    //再次尝试获取同步状态\n    if (!tryAcquire(arg) &&\n        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))\n        selfInterrupt();\n}\n```\n\n如果tryAcquire(arg)返回true，acquireQueued自然不会执行，这是最理想的，因为毕竟当前线程已获取到锁，如果tryAcquire(arg)返回false，则会执行addWaiter(Node.EXCLUSIVE)进行入队操作,由于ReentrantLock属于独占锁，因此结点类型为Node.EXCLUSIVE，下面看看addWaiter方法具体实现\n\n```java\nprivate Node addWaiter(Node mode) {\n    //将请求同步状态失败的线程封装成结点\n    Node node = new Node(Thread.currentThread(), mode);\n\n    Node pred = tail;\n    //如果是第一个结点加入肯定为空，跳过。\n    //如果非第一个结点则直接执行CAS入队操作，尝试在尾部快速添加\n    if (pred != null) {\n        node.prev = pred;\n        //使用CAS执行尾部结点替换，尝试在尾部快速添加\n        if (compareAndSetTail(pred, node)) {\n            pred.next = node;\n            return node;\n        }\n    }\n    //如果第一次加入或者CAS操作没有成功执行enq入队操作\n    enq(node);\n    return node;\n}\n```\n\n创建了一个Node.EXCLUSIVE类型Node结点用于封装线程及其相关信息，其中tail是AQS的成员变量，指向队尾(这点前面的我们分析过AQS维持的是一个双向的链表结构同步队列)，如果是第一个结点，则为tail肯定为空，那么将执行enq(node)操作，如果非第一个结点即tail指向不为null，直接尝试执行CAS操作加入队尾，如果CAS操作失败还是会执行enq(node)，继续看enq(node)：\n\n```java\nprivate Node enq(final Node node) {\n    //死循环\n    for (;;) {\n         Node t = tail;\n         //如果队列为null，即没有头结点\n         if (t == null) { // Must initialize\n             //创建并使用CAS设置头结点\n             if (compareAndSetHead(new Node()))\n                 tail = head;\n         } else {//队尾添加新结点\n             node.prev = t;\n             if (compareAndSetTail(t, node)) {\n                 t.next = node;\n                 return t;\n             }\n         }\n     }\n    }\n```\n这个方法使用一个死循环进行CAS操作，可以解决多线程并发问题。这里做了两件事，一是如果还没有初始同步队列则创建新结点并使用compareAndSetHead设置头结点，tail也指向head，二是队列已存在，则将新结点node添加到队尾。注意这两个步骤都存在同一时间多个线程操作的可能，如果有一个线程修改head和tail成功，那么其他线程将继续循环，直到修改成功，这里使用CAS原子操作进行头结点设置和尾结点tail替换可以保证线程安全，从这里也可以看出head结点本身不存在任何数据，它只是作为一个牵头结点，而tail永远指向尾部结点(前提是队列不为null)。\n![](../img/Java多线程/node节点数据结构.png)\n\n添加到同步队列后，结点就会进入一个自旋过程，即每个结点都在观察时机待条件满足获取同步状态，然后从同步队列退出并结束自旋，回到之前的acquire()方法，自旋过程是在acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法中执行的，代码如下\n\n```java\nfinal boolean acquireQueued(final Node node, int arg) {\n    boolean failed = true;\n    try {\n        boolean interrupted = false;\n        //自旋，死循环\n        for (;;) {\n            //获取前驱结点\n            final Node p = node.predecessor();\n            当且仅当p为头结点才尝试获取同步状态\n            if (p == head && tryAcquire(arg)) {\n                //将node设置为头结点\n                setHead(node);\n                //清空原来头结点的引用便于GC\n                p.next = null; // help GC\n                failed = false;\n                return interrupted;\n            }\n            //如果前驱结点不是head，判断是否挂起线程\n            if (shouldParkAfterFailedAcquire(p, node) &&\n                parkAndCheckInterrupt())\n                interrupted = true;\n        }\n    } finally {\n        if (failed)\n            //最终都没能获取同步状态，结束该线程的请求\n            cancelAcquire(node);\n    }\n}\n\n```\n当前线程在自旋(死循环)中获取同步状态，当且仅当前驱结点为头结点才尝试获取同步状态，这符合FIFO的规则，即先进先出，其次head是当前获取同步状态的线程结点，只有当head释放同步状态唤醒后继结点，后继结点才有可能获取到同步状态，因此后继结点在其前继结点为head时，才进行尝试获取同步状态，其他时刻将被挂起。进入if语句后调用setHead(node)方法，将当前线程结点设置为head\n\n```java\n//设置为头结点\nprivate void setHead(Node node) {\n        head = node;\n        //清空结点数据\n        node.thread = null;\n        node.prev = null;\n}\n```\n设置为node结点被设置为head后，其thread信息和前驱结点将被清空，因为该线程已获取到同步状态(锁)，正在执行了，也就没有必要存储相关信息了，head只有保存指向后继结点的指针即可，便于head结点释放同步状态后唤醒后继结点，执行结果如下图\n![](../img/Java多线程/node节点数据结构2.png)\n\n从图可知更新head结点的指向，将后继结点的线程唤醒并获取同步状态，调用setHead(node)将其替换为head结点，清除相关无用数据。当然如果前驱结点不是head，那么执行如下\n\n```java\n//如果前驱结点不是head，判断是否挂起线程\nif (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())\n\n      interrupted = true;\n}\n\nprivate static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {\n        //获取当前结点的等待状态\n        int ws = pred.waitStatus;\n        //如果为等待唤醒（SIGNAL）状态则返回true\n        if (ws == Node.SIGNAL)\n            return true;\n        //如果ws>0 则说明是结束状态，\n        //遍历前驱结点直到找到没有结束状态的结点\n        if (ws > 0) {\n            do {\n                node.prev = pred = pred.prev;\n            } while (pred.waitStatus > 0);\n            pred.next = node;\n        } else {\n            //如果ws小于0又不是SIGNAL状态，\n            //则将其设置为SIGNAL状态，代表该结点的线程正在等待唤醒。\n            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);\n        }\n        return false;\n    }\n\nprivate final boolean parkAndCheckInterrupt() {\n        //将当前线程挂起\n        LockSupport.park(this);\n        //获取线程中断状态,interrupted()是判断当前中断状态，\n        //并非中断线程，因此可能true也可能false,并返回\n        return Thread.interrupted();\n}\n```\nshouldParkAfterFailedAcquire()方法的作用是判断当前结点的前驱结点是否为SIGNAL状态(即等待唤醒状态)，如果是则返回true。如果结点的ws为CANCELLED状态(值为1>0),即结束状态，则说明该前驱结点已没有用应该从同步队列移除，执行while循环，直到寻找到非CANCELLED状态的结点。倘若前驱结点的ws值不为CANCELLED，也不为SIGNAL(当从Condition的条件等待队列转移到同步队列时，结点状态为CONDITION因此需要转换为SIGNAL)，那么将其转换为SIGNAL状态，等待被唤醒。\n\n若shouldParkAfterFailedAcquire()方法返回true，即前驱结点为SIGNAL状态同时又不是head结点，那么使用parkAndCheckInterrupt()方法挂起当前线程，称为WAITING状态，需要等待一个unpark()操作来唤醒它，到此ReetrantLock内部间接通过AQS的FIFO的同步队列就完成了lock()操作，这里我们总结成逻辑流程图\n![](../img/Java多线程/aqs-lock流程.png)\n\n关于获取锁的操作，这里看看另外一种可中断的获取方式，即调用ReentrantLock类的lockInterruptibly()或者tryLock()方法，最终它们都间接调用到doAcquireInterruptibly()\n```java\n private void doAcquireInterruptibly(int arg)\n        throws InterruptedException {\n        final Node node = addWaiter(Node.EXCLUSIVE);\n        boolean failed = true;\n        try {\n            for (;;) {\n                final Node p = node.predecessor();\n                if (p == head && tryAcquire(arg)) {\n                    setHead(node);\n                    p.next = null; // help GC\n                    failed = false;\n                    return;\n                }\n                if (shouldParkAfterFailedAcquire(p, node) &&\n                    parkAndCheckInterrupt())\n                    //直接抛异常，中断线程的同步状态请求\n                    throw new InterruptedException();\n            }\n        } finally {\n            if (failed)\n                cancelAcquire(node);\n        }\n    }\n```\n最大的不同是\n```java\nif (shouldParkAfterFailedAcquire(p, node) &&\n                    parkAndCheckInterrupt())\n     //直接抛异常，中断线程的同步状态请求\n       throw new InterruptedException();\n```\n检测到线程的中断操作后，直接抛出异常，从而中断线程的同步状态请求，移除同步队列，ok~,加锁流程到此\n### unlock\n```java\n//ReentrantLock类的unlock\npublic void unlock() {\n    sync.release(1);\n}\n\n//AQS类的release()方法\npublic final boolean release(int arg) {\n    //尝试释放锁\n    if (tryRelease(arg)) {\n\n        Node h = head;\n        if (h != null && h.waitStatus != 0)\n            //唤醒后继结点的线程\n            unparkSuccessor(h);\n        return true;\n    }\n    return false;\n}\n\n//ReentrantLock类中的内部类Sync实现的tryRelease(int releases) \nprotected final boolean tryRelease(int releases) {\n\n      int c = getState() - releases;\n      if (Thread.currentThread() != getExclusiveOwnerThread())\n          throw new IllegalMonitorStateException();\n      boolean free = false;\n      //判断状态是否为0，如果是则说明已释放同步状态\n      if (c == 0) {\n          free = true;\n          //设置Owner为null\n          setExclusiveOwnerThread(null);\n      }\n      //设置更新同步状态\n      setState(c);\n      return free;\n  }\n```\n释放同步状态的操作相对简单些，tryRelease(int releases)方法是ReentrantLock类中内部类自己实现的，因为AQS对于释放锁并没有提供具体实现，必须由子类自己实现。释放同步状态后会使用unparkSuccessor(h)唤醒后继结点的线程，这里看看unparkSuccessor(h)\n\n```java\nprivate void unparkSuccessor(Node node) {\n    //这里，node一般为当前线程所在的结点。\n    int ws = node.waitStatus;\n    if (ws < 0)//置零当前线程所在的结点状态，允许失败。\n        compareAndSetWaitStatus(node, ws, 0);\n\n    Node s = node.next;//找到下一个需要唤醒的结点s\n    if (s == null || s.waitStatus > 0) {//如果为空或已取消\n        s = null;\n        for (Node t = tail; t != null && t != node; t = t.prev)\n            if (t.waitStatus <= 0)//从这里可以看出，<=0的结点，都是还有效的结点。\n                s = t;\n    }\n    if (s != null)\n        LockSupport.unpark(s.thread);//唤醒\n}\n```\n从代码执行操作来看，这里主要作用是用unpark()唤醒同步队列中最前边未放弃线程(也就是状态为CANCELLED的线程结点s)。此时，回忆前面分析进入自旋的函数acquireQueued()，s结点的线程被唤醒后，会进入acquireQueued()函数的if (p == head && tryAcquire(arg))的判断，如果p!=head也不会有影响，因为它会执行shouldParkAfterFailedAcquire()，由于s通过unparkSuccessor()操作后已是同步队列中最前边未放弃的线程结点，那么通过shouldParkAfterFailedAcquire()内部对结点状态的调整，s也必然会成为head的next结点，因此再次自旋时p==head就成立了，然后s把自己设置成head结点，表示自己已经获取到资源了，最终acquire()也返回了，这就是独占锁释放的过程。\nok~，关于独占模式的加锁和释放锁的过程到这就分析完，总之呢，在AQS同步器中维护着一个同步队列，当线程获取同步状态失败后，将会被封装成Node结点，加入到同步队列中并进行自旋操作，当当前线程结点的前驱结点为head时，将尝试获取同步状态，获取成功将自己设置为head结点。在释放同步状态时，则通过调用子类(ReetrantLock中的Sync内部类)的tryRelease(int releases)方法释放同步状态，释放成功则唤醒后继结点的线程。\n\n## FairSync (extends Sync) 公平锁\n\n### lock\n与非公平锁不同的是，在获取锁的时，公平锁的获取顺序是完全遵循时间上的FIFO规则，也就是说先请求的线程一定会先获取锁，后来的线程肯定需要排队，这点与前面我们分析非公平锁的nonfairTryAcquire(int acquires)方法实现有锁不同，下面是公平锁中tryAcquire()方法的实现\n```java\n//公平锁FairSync类中的实现\nprotected final boolean tryAcquire(int acquires) {\n            final Thread current = Thread.currentThread();\n            int c = getState();\n            if (c == 0) {\n            //注意！！这里先判断同步队列是否存在结点\n                if (!hasQueuedPredecessors() &&\n                    compareAndSetState(0, acquires)) {\n                    setExclusiveOwnerThread(current);\n                    return true;\n                }\n            }\n            else if (current == getExclusiveOwnerThread()) {\n                int nextc = c + acquires;\n                if (nextc < 0)\n                    throw new Error(\"Maximum lock count exceeded\");\n                setState(nextc);\n                return true;\n            }\n            return false;\n        }\n```\n该方法与nonfairTryAcquire(int acquires)方法唯一的不同是在使用CAS设置尝试设置state值前，调用了hasQueuedPredecessors()判断同步队列是否存在结点，如果存在必须先执行完同步队列中结点的线程，当前线程进入等待状态。这就是非公平锁与公平锁最大的区别，即公平锁在线程请求到来时先会判断同步队列是否存在结点，如果存在先执行同步队列中的结点线程，当前线程将封装成node加入同步队列等待。而非公平锁呢，当线程请求到来时，不管同步队列是否存在线程结点，直接尝试获取同步状态，获取成功直接访问共享资源，但请注意在绝大多数情况下，非公平锁才是我们理想的选择，毕竟从效率上来说非公平锁总是胜于公平锁。\n\n## Condition\n在并发编程中，每个Java对象都存在一组监视器方法，如wait()、notify()以及notifyAll()方法，通过这些方法，我们可以实现线程间通信与协作（也称为等待唤醒机制），如生产者-消费者模式，而且这些方法必须配合着synchronized关键字使用，关于这点，如果想有更深入的理解，可观看博主另外一篇博文【 深入理解Java并发之synchronized实现原理】，与synchronized的等待唤醒机制相比Condition具有更多的灵活性以及精确性，这是因为notify()在唤醒线程时是随机(同一个锁)，而Condition则可通过多个Condition实例对象建立更加精细的线程控制，也就带来了更多灵活性了，我们可以简单理解为以下两点\n- 通过Condition能够精细的控制多线程的休眠与唤醒。\n- 对于一个锁，我们可以为多个线程间建立不同的Condition。\n\n### 主要方法\n```java\npublic interface Condition {\n\n /**\n  * 使当前线程进入等待状态直到被通知(signal)或中断\n  * 当其他线程调用singal()或singalAll()方法时，该线程将被唤醒\n  * 当其他线程调用interrupt()方法中断当前线程\n  * await()相当于synchronized等待唤醒机制中的wait()方法\n  */\n void await() throws InterruptedException;\n\n //当前线程进入等待状态，直到被唤醒，该方法不响应中断要求\n void awaitUninterruptibly();\n\n //调用该方法，当前线程进入等待状态，直到被唤醒或被中断或超时\n //其中nanosTimeout指的等待超时时间，单位纳秒\n long awaitNanos(long nanosTimeout) throws InterruptedException;\n\n  //同awaitNanos，但可以指明时间单位\n  boolean await(long time, TimeUnit unit) throws InterruptedException;\n\n //调用该方法当前线程进入等待状态，直到被唤醒、中断或到达某个时\n //间期限(deadline),如果没到指定时间就被唤醒，返回true，其他情况返回false\n  boolean awaitUntil(Date deadline) throws InterruptedException;\n\n //唤醒一个等待在Condition上的线程，该线程从等待方法返回前必须\n //获取与Condition相关联的锁，功能与notify()相同\n  void signal();\n\n //唤醒所有等待在Condition上的线程，该线程从等待方法返回前必须\n //获取与Condition相关联的锁，功能与notifyAll()相同\n  void signalAll();\n}\n```\n### Condition的使用案例-生产者消费者模式\n这里我们通过一个卖烤鸭的案例来演示多生产多消费者的案例，该场景中存在两条生产线程t1和t2，用于生产烤鸭，也存在两条消费线程t3，t4用于消费烤鸭，4条线程同时执行，需要保证只有在生产线程产生烤鸭后，消费线程才能消费，否则只能等待，直到生产线程产生烤鸭后唤醒消费线程，注意烤鸭不能重复消费。ResourceByCondition类中定义product()和consume()两个方法，分别用于生产烤鸭和消费烤鸭，并且定义ReentrantLock锁，用于控制product()和consume()的并发，由于必须在烤鸭生成完成后消费线程才能消费烤鸭，否则只能等待，因此这里定义两组Condition对象，分别是producer_con和consumer_con，前者拥有控制生产线程，后者拥有控制消费线程，这里我们使用一个标志flag来控制是否有烤鸭，当flag为true时，代表烤鸭生成完毕，生产线程必须进入等待状态同时唤醒消费线程进行消费，消费线程消费完毕后将flag设置为false，代表烤鸭消费完成，进入等待状态，同时唤醒生产线程生产烤鸭，具体代码如下\n```java\npackage com.zejian.concurrencys;\n\nimport java.util.concurrent.locks.Condition;\nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReentrantLock;\n\n/**\n * Created by zejian on 2017/7/22.\n * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]\n */\npublic class ResourceByCondition {\n    private String name;\n    private int count = 1;\n    private boolean flag = false;\n\n    //创建一个锁对象。\n    Lock lock = new ReentrantLock();\n\n    //通过已有的锁获取两组监视器，一组监视生产者，一组监视消费者。\n    Condition producer_con = lock.newCondition();\n    Condition consumer_con = lock.newCondition();\n\n    /**\n     * 生产\n     * @param name\n     */\n    public  void product(String name)\n    {\n        lock.lock();\n        try\n        {\n            while(flag){\n                try{\n                    producer_con.await();\n                }catch(InterruptedException e){\n                    \n                }\n            }\n            this.name = name + count;\n            count++;\n            System.out.println(Thread.currentThread().getName()+\"...生产者5.0...\"+this.name);\n            flag = true;\n            consumer_con.signal();//直接唤醒消费线程\n        }\n        finally\n        {\n            lock.unlock();\n        }\n    }\n\n    /**\n     * 消费\n     */\n    public  void consume()\n    {\n        lock.lock();\n        try\n        {\n            while(!flag){\n                try{\n                    consumer_con.await();\n                }catch(InterruptedException e)\n                {}\n            }\n            System.out.println(Thread.currentThread().getName()+\"...消费者.5.0.......\"+this.name);//消费烤鸭1\n            flag = false;\n            producer_con.signal();//直接唤醒生产线程\n        }\n        finally\n        {\n            lock.unlock();\n        }\n    }\n}\n\n```\n执行代码\n```java\npackage com.zejian.concurrencys;\n/**\n * Created by zejian on 2017/7/22.\n * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]\n */\npublic class Mutil_Producer_ConsumerByCondition {\n\n    public static void main(String[] args) {\n        ResourceByCondition r = new ResourceByCondition();\n        Mutil_Producer pro = new Mutil_Producer(r);\n        Mutil_Consumer con = new Mutil_Consumer(r);\n        //生产者线程\n        Thread t0 = new Thread(pro);\n        Thread t1 = new Thread(pro);\n        //消费者线程\n        Thread t2 = new Thread(con);\n        Thread t3 = new Thread(con);\n        //启动线程\n        t0.start();\n        t1.start();\n        t2.start();\n        t3.start();\n    }\n}\n\n/**\n * @decrition 生产者线程\n */\nclass Mutil_Producer implements Runnable {\n    private ResourceByCondition r;\n\n    Mutil_Producer(ResourceByCondition r) {\n        this.r = r;\n    }\n\n    public void run() {\n        while (true) {\n            r.product(\"北京烤鸭\");\n        }\n    }\n}\n\n/**\n * @decrition 消费者线程\n */\nclass Mutil_Consumer implements Runnable {\n    private ResourceByCondition r;\n\n    Mutil_Consumer(ResourceByCondition r) {\n        this.r = r;\n    }\n\n    public void run() {\n        while (true) {\n            r.consume();\n        }\n    }\n}\n```\n正如代码所示，我们通过两者Condition对象单独控制消费线程与生产消费，这样可以避免消费线程在唤醒线程时唤醒的还是消费线程，如果是通过synchronized的等待唤醒机制实现的话，就可能无法避免这种情况，毕竟同一个锁，对于synchronized关键字来说只能有一组等待唤醒队列，而不能像Condition一样，同一个锁拥有多个等待队列。synchronized的实现方案如下，\n```java\npublic class KaoYaResource {\n\n    private String name;\n    private int count = 1;//烤鸭的初始数量\n    private boolean flag = false;//判断是否有需要线程等待的标志\n    /**\n     * 生产烤鸭\n     */\n    public synchronized void product(String name){\n        while(flag){\n            //此时有烤鸭，等待\n            try {\n                this.wait();\n            } catch (InterruptedException e) {\n                e.printStackTrace();\n            }\n        }\n        this.name=name+count;//设置烤鸭的名称\n        count++;\n        System.out.println(Thread.currentThread().getName()+\"...生产者...\"+this.name);\n        flag=true;//有烤鸭后改变标志\n        notifyAll();//通知消费线程可以消费了\n    }\n\n    /**\n     * 消费烤鸭\n     */\n    public synchronized void consume(){\n        while(!flag){//如果没有烤鸭就等待\n            try{this.wait();}catch(InterruptedException e){}\n        }\n        System.out.println(Thread.currentThread().getName()+\"...消费者........\"+this.name);//消费烤鸭1\n        flag = false;\n        notifyAll();//通知生产者生产烤鸭\n    }\n}\n```\n如上代码，在调用notify()或者 notifyAll()方法时，由于等待队列中同时存在生产者线程和消费者线程，所以我们并不能保证被唤醒的到底是消费者线程还是生产者线程，而Codition则可以避免这种情况。嗯，了解完Condition的使用方式后，下面我们将进一步探讨Condition背后的实现机制\n\n### Condition的实现原理\nCondition的具体实现类是AQS的内部类`ConditionObject`，前面我们分析过AQS中存在两种队列，一种是`同步队列`，一种是`等待队列`，而等待队列就相对于Condition而言的。注意在使用Condition前必须获得锁，同时在Condition的等待队列上的结点与前面同步队列的结点是同一个类即Node，其结点的waitStatus的值为CONDITION。在实现类ConditionObject中有两个结点分别是firstWaiter和lastWaiter，firstWaiter代表等待队列第一个等待结点，lastWaiter代表等待队列最后一个等待结点，如下\n```java\n public class ConditionObject implements Condition, java.io.Serializable {\n    //等待队列第一个等待结点\n    private transient Node firstWaiter;\n    //等待队列最后一个等待结点\n    private transient Node lastWaiter;\n    //省略其他代码.......\n}\n```\n\n每个Condition都对应着一个等待队列，也就是说如果一个锁上创建了多个Condition对象，那么也就存在多个等待队列。等待队列是一个FIFO的队列，在队列中每一个节点都包含了一个线程的引用，而该线程就是Condition对象上等待的线程。\n\n当一个线程调用了await()相关的方法，那么该线程将会释放锁，并构建一个Node节点封装当前线程的相关信息加入到等待队列中进行等待，直到被唤醒、中断、超时才从队列中移出。Condition中的等待队列模型如下\n\n![](../img/Java多线程/condition中的等待队列模型.png)\n\n正如图所示，Node节点的数据结构，在等待队列中使用的变量与同步队列是不同的，Condtion中等待队列的结点只有直接指向的后继结点并没有指明前驱结点，而且使用的变量是nextWaiter而不是next，这点我们在前面分析结点Node的数据结构时讲过。\n\n`firstWaiter`指向等待队列的头结点，`lastWaiter`指向等待队列的尾结点，等待队列中结点的状态只有两种即`CANCELLED`和`CONDITION`，前者表示线程已结束需要从等待队列中移除，后者表示条件结点等待被唤醒。\n\n再次强调每个Codition对象对于一个等待队列，也就是说AQS中只能存在`一个同步队列`，但可拥有`多个等待队列`。下面从代码层面看看被调用await()方法(其他await()实现原理类似)的线程是如何加入等待队列的，而又是如何从等待队列中被唤醒的\n\n```java\npublic final void await() throws InterruptedException {\n      //判断线程是否被中断\n      if (Thread.interrupted())\n          throw new InterruptedException();\n      //创建新结点加入等待队列并返回\n      Node node = addConditionWaiter();\n      //释放当前线程锁即释放同步状态\n      int savedState = fullyRelease(node);\n      int interruptMode = 0;\n      //判断结点是否同步队列(SyncQueue)中,即是否被唤醒\n      while (!isOnSyncQueue(node)) {\n          //挂起线程\n          LockSupport.park(this);\n          //判断是否被中断唤醒，如果是退出循环。\n          if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)\n              break;\n      }\n      //被唤醒后执行自旋操作争取获得锁，同时判断线程是否被中断\n      if (acquireQueued(node, savedState) && interruptMode != THROW_IE)\n          interruptMode = REINTERRUPT;\n       // clean up if cancelled\n      if (node.nextWaiter != null) \n          //清理等待队列中不为CONDITION状态的结点\n          unlinkCancelledWaiters();\n      if (interruptMode != 0)\n          reportInterruptAfterWait(interruptMode);\n  }\n```\n执行addConditionWaiter()添加到等待队列。\n\n```java\n private Node addConditionWaiter() {\n    Node t = lastWaiter;\n      // 判断是否为结束状态的结点并移除\n      if (t != null && t.waitStatus != Node.CONDITION) {\n          unlinkCancelledWaiters();\n          t = lastWaiter;\n      }\n      //创建新结点状态为CONDITION\n      Node node = new Node(Thread.currentThread(), Node.CONDITION);\n      //加入等待队列\n      if (t == null)\n          firstWaiter = node;\n      else\n          t.nextWaiter = node;\n      lastWaiter = node;\n      return node;\n}\n```\n\nawait()方法主要做了3件事\n\n- 一是调用addConditionWaiter()方法将当前线程封装成node结点加入等待队列\n- 二是调用fullyRelease(node)方法释放同步状态并唤醒后继结点的线程。\n- 三是调用isOnSyncQueue(node)方法判断结点是否在同步队列中\n\n注意是个while循环，如果同步队列中没有该结点就直接挂起该线程，需要明白的是如果线程被唤醒后就调用`acquireQueued(node, savedState)`执行自旋操作争取锁，即当前线程结点从等待队列转移到同步队列并开始努力获取锁。\n\n接着看看唤醒操作singal()方法\n```java\n public final void signal() {\n     //判断是否持有独占锁，如果不是抛出异常\n   if (!isHeldExclusively())\n          throw new IllegalMonitorStateException();\n      Node first = firstWaiter;\n      //唤醒等待队列第一个结点的线程\n      if (first != null)\n          doSignal(first);\n }\n```\n\n这里signal()方法做了两件事，一是判断当前线程是否持有独占锁，没有就抛出异常，从这点也可以看出只有独占模式先采用等待队列，而共享模式下是没有等待队列的，也就没法使用Condition。二是唤醒等待队列的第一个结点，即执行doSignal(first)\n\n```java\n private void doSignal(Node first) {\n    do {\n        //移除条件等待队列中的第一个结点，\n        //如果后继结点为null，那么说没有其他结点将尾结点也设置为null\n        if ( (firstWaiter = first.nextWaiter) == null)\n            lastWaiter = null;\n        first.nextWaiter = null;\n    //如果被通知节点没有进入到同步队列并且条件等待队列还有不为空的节点，则继续循环通知后续结点\n    } while (!transferForSignal(first) &&\n        (first = firstWaiter) != null);\n}\n\n//transferForSignal方法\nfinal boolean transferForSignal(Node node) {\n    //尝试设置唤醒结点的waitStatus为0，即初始化状态\n    //如果设置失败，说明当期结点node的waitStatus已不为\n    //CONDITION状态，那么只能是结束状态了，因此返回false\n    //返回doSignal()方法中继续唤醒其他结点的线程，注意这里并\n    //不涉及并发问题，所以CAS操作失败只可能是预期值不为CONDITION，\n    //而不是多线程设置导致预期值变化，毕竟操作该方法的线程是持有锁的。\n    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))\n         return false;\n\n        //加入同步队列并返回前驱结点p\n        Node p = enq(node);\n        int ws = p.waitStatus;\n        //判断前驱结点是否为结束结点(CANCELLED=1)或者在设置\n        //前驱节点状态为Node.SIGNAL状态失败时，唤醒被通知节点代表的线程\n        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))\n            //唤醒node结点的线程\n            LockSupport.unpark(node.thread);\n        return true;\n    }\n```\n\n注释说得很明白了，这里我们简单整体说明一下，doSignal(first)方法中做了两件事\n\n- 从条件等待队列移除被唤醒的节点，然后重新维护条件等待队列的firstWaiter和lastWaiter的指向。\n- 二是将从等待队列移除的结点加入同步队列(在transferForSignal()方法中完成的)，如果进入到同步队列失败并且条件等待队列还有不为空的节点，则继续循环唤醒后续其他结点的线程。\n\n到此整个signal()的唤醒过程就很清晰了，即signal()被调用后，先判断当前线程是否持有独占锁，如果有，那么唤醒当前Condition对象中等待队列的第一个结点的线程，并从等待队列中移除该结点，移动到同步队列中，\n\n如果加入同步队列失败，那么继续循环唤醒等待队列中的其他结点的线程，如果成功加入同步队列，那么如果其前驱结点是否已结束或者设置前驱节点状态为Node.SIGNAL状态失败，则通过LockSupport.unpark()唤醒被通知节点代表的线程，到此signal()任务完成，\n\n注意被唤醒后的线程，将从前面的await()方法中的while循环中退出，因为此时该线程的结点已在同步队列中，那么while (!isOnSyncQueue(node))将不在符合循环条件，进而调用AQS的acquireQueued()方法加入获取同步状态的竞争中，这就是等待唤醒机制的整个流程实现原理，\n\n流程如下图所示（注意无论是同步队列还是等待队列使用的Node数据结构都是同一个，不过是使用的内部变量不同罢了）\n\n![](../img/Java多线程/等待唤醒机制的流程实现原理.png)\n\n## 同步工具类\n### CountDownLatch\n用来控制一个线程等待多个线程。维护了一个计数器 cnt，每次调用 countDown() 方法会让计数器的值减 1，减到 0 的时候，那些因为调用 await() 方法而在等待的线程就会被唤醒\n```java\npublic class MyCountDownLatch {\n\n    public static void main(String[] args) throws InterruptedException {\n        int totalThread = 10;\n        CountDownLatch countDownLatch = new CountDownLatch(totalThread);\n        ExecutorService executorService = Executors.newCachedThreadPool();\n        for (int i = 0; i < totalThread; i++) {\n            executorService.execute(()->{\n                System.out.println(\"run...\");\n                countDownLatch.countDown();\n            });\n        }\n        countDownLatch.await();\n        System.out.println(\"end...\");\n        executorService.shutdown();\n    }\n}\n```\n```text\nrun...\nrun...\nrun...\nrun...\nrun...\nrun...\nrun...\nrun...\nrun...\nrun...\nend...\n```\n#### 源码分析\nCountDownLatch也是依赖于AQS的state来实现\n\n```java\n public CountDownLatch(int count) {\n    if (count < 0) throw new IllegalArgumentException(\"count < 0\");\n    this.sync = new Sync(count);\n}\n\nSync(int count) {\n    setState(count);\n}\n\nprotected final void setState(int newState) {\n    state = newState;\n}\n```\n\ncountDown()\n```java\npublic void countDown() {\n    sync.releaseShared(1);\n}\n\npublic final boolean releaseShared(int arg) {\n    //只有当 state 减为 0 的时候，tryReleaseShared 才返回 true\n    if (tryReleaseShared(arg)) {\n        doReleaseShared();\n        return true;\n    }\n    return false;\n}\n```\n```java\n//自旋-1\nprotected boolean tryReleaseShared(int releases) {\n    // Decrement count; signal when transition to zero\n    for (;;) {\n        int c = getState();\n        if (c == 0)\n            return false;\n        int nextc = c-1;\n        if (compareAndSetState(c, nextc))\n            return nextc == 0;\n    }\n}\n```\n```java\n//调用这个方法的时候，state == 0,那么就调用下面的方法进行唤醒阻塞队列中的线程\nprivate void doReleaseShared() {\n    for (;;) {\n        Node h = head;\n        if (h != null && h != tail) {\n            int ws = h.waitStatus;\n            if (ws == Node.SIGNAL) {\n                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))\n                    continue;            // loop to recheck cases\n                unparkSuccessor(h);\n            }\n            else if (ws == 0 &&\n                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))\n                continue;                // loop on failed CAS\n        }\n        if (h == head)                   // loop if head changed\n            break;\n    }\n}\n```\nawait()\n```java\npublic void await() throws InterruptedException {\n    sync.acquireSharedInterruptibly(1);\n}\n    \npublic final void acquireSharedInterruptibly(int arg)\n        throws InterruptedException {\n        if (Thread.interrupted())\n            throw new InterruptedException();\n        if (tryAcquireShared(arg) < 0)\n            doAcquireSharedInterruptibly(arg);\n}\n//tryAcquireShared()的作用是尝试获取共享锁。如果\"锁计数器=0\"，即锁是可获取状态，则返回1；否则，锁是不可获取状态，则返回-1\nprotected int tryAcquireShared(int acquires) {\n    return (getState() == 0) ? 1 : -1;\n}\n```\n```java\nprivate void doAcquireSharedInterruptibly(int arg)\n    throws InterruptedException {\n    // 创建\"当前线程\"的Node节点，且Node中记录的锁是\"共享锁\"类型；并将该节点添加到CLH队列末尾。\n    final Node node = addWaiter(Node.SHARED);\n    boolean failed = true;\n    try {\n        for (;;) {\n            // 获取上一个节点。\n            // 如果上一节点是CLH队列的表头，则\"尝试获取共享锁\"。\n            final Node p = node.predecessor();\n            if (p == head) {\n                //一直判断state是否为0\n                int r = tryAcquireShared(arg);\n                if (r >= 0) {\n                    setHeadAndPropagate(node, r);\n                    p.next = null; // help GC\n                    failed = false;\n                    return;\n                }\n            }\n        // (上一节点不是CLH队列的表头) 当前线程一直等待，直到获取到共享锁。\n        // 如果线程在等待过程中被中断过，则再次中断该线程(还原之前的中断状态)。\n        if (shouldParkAfterFailedAcquire(p, node) &&\n                parkAndCheckInterrupt())\n                throw new InterruptedException();\n        }\n    } finally {\n        if (failed)\n            cancelAcquire(node);\n    }\n}\n```\nawait()方法是怎么“阻塞”当前线程的，已经非常明白了。其实说白了，就是当你调用了countDownLatch.await()方法后，你当前线程就会进入了一个死循环当中，在这个死循环里面，会不断的进行判断，通过调用tryAcquireShared方法，不断判断我们上面说的那个计数器，看看它的值是否为0了（为0的时候，其实就是我们调用了足够多次数的countDownLatch.countDown（）方法的时候），如果是为0的话，tryAcquireShared就会返回1，然后跳出了循环，也就不再“阻塞”当前线程了。\n\n### CyclicBarrier\n用来控制多个线程互相等待，只有当多个线程都到达时，这些线程才会继续执行\n```java\npublic class MyCyclicBarrier {\n\n    public static void main(String[] args) {\n        int totalThread = 10;\n        CyclicBarrier cyclicBarrier = new CyclicBarrier(totalThread);\n        ExecutorService executorService = Executors.newCachedThreadPool();\n        for (int i = 0; i < totalThread; i++) {\n            executorService.execute(()->{\n                System.out.println(\"before...\");\n                try {\n                    cyclicBarrier.await();\n                } catch (InterruptedException | BrokenBarrierException e) {\n                    e.printStackTrace();\n                }\n                System.out.println(\"after...\");\n            });\n        }\n        executorService.shutdown();\n    }\n}\n```\n```text\nbefore...\nbefore...\nbefore...\nbefore...\nbefore...\nbefore...\nbefore...\nbefore...\nbefore...\nbefore...\nafter...\nafter...\nafter...\nafter...\nafter...\nafter...\nafter...\nafter...\nafter...\nafter...\n```\n#### 源码分析\n```java\npublic CyclicBarrier(int parties) {\n    this(parties, null);\n}\n\npublic CyclicBarrier(int parties, Runnable barrierAction) {\n    if (parties <= 0) \n        throw new IllegalArgumentException();\n    this.parties = parties;\n    this.count = parties;\n    this.barrierCommand = barrierAction;\n}\n```\nawait()\n\n调用await方法的线程告诉CyclicBarrier自己已经到达同步点，然后当前线程被阻塞。直到parties个参与线程调用了await方法\n```java\npublic int await() throws InterruptedException, BrokenBarrierException {\n    try {\n        return dowait(false, 0L);\n    } catch (TimeoutException toe) {\n        throw new Error(toe);\n    }\n}\n```\n```java\nprivate int dowait(boolean timed, long nanos) throws InterruptedException, BrokenBarrierException, TimeoutException {\n    final ReentrantLock lock = this.lock;\n    // 抢锁\n    lock.lock();\n    try {\n        // 获取 Generation\n        final Generation g = generation;\n\n        // 如果这代损坏了，抛出异常\n        if (g.broken)\n            throw new BrokenBarrierException();\n\n        // 当前线程被中断过，\n        if (Thread.interrupted()) {\n            // 将损坏状态设置为true\n            // 并通知其他阻塞在此栅栏上的线程\n            breakBarrier();\n            throw new InterruptedException();\n        }\n\n        // 线程数减 1\n        int index = --count;\n        // index = 0 表示公共屏障点被触发\n        if (index == 0) {\n            boolean ranAction = false;\n            try {\n                // 执行栅栏任务\n                final Runnable command = barrierCommand;\n                if (command != null)\n                    command.run();\n                // 开始执行\n                ranAction = true;\n                // 唤醒之前等待的线程\n                nextGeneration();\n                return 0;\n            } finally {\n                // 如果执行栅栏任务的时候失败了，就将损坏状态设置为true\n                if (!ranAction)\n                    breakBarrier();\n            }\n        }\n\n        // loop until tripped, broken, interrupted, or timed out\n        for (;;) {\n            try {\n                // 没有时间限制，则无期限等待\n                if (!timed)\n                    trip.await();\n                // 有时间限制，则等待一定时间\n                else if (nanos > 0L)\n                    nanos = trip.awaitNanos(nanos);\n            } catch (InterruptedException ie) {\n                // 当前代没有损坏\n                if (g == generation && ! g.broken) {\n                    // 让栅栏失效\n                    breakBarrier();\n                    throw ie;\n                } else {\n                   // 上面条件不满足，说明这个线程不是这代的\n                   // 就不会影响当前这代栅栏的执行，所以，就打个中断标记 Thread.currentThread().interrupt();\n                }\n            }\n\n            // 当有任何一个线程中断了，就会调用breakBarrier方法\n            // 就会唤醒其他的线程，其他线程醒来后，也要抛出异常\n            if (g.broken)\n                throw new BrokenBarrierException();\n\n            // g != generation表示正常换代了，返回当前线程所在栅栏的下标\n            // 如果 g == generation，说明还没有换代，那为什么会醒了？\n            // 因为一个线程可以使用多个栅栏，当别的栅栏唤醒了这个线程，就会走到这里，所以需要判断是否是当前代。\n            // 正是因为这个原因，才需要generation来保证正确。\n            if (g != generation)\n                return index;\n\n            // 如果有时间限制，且时间小于等于0，销毁栅栏并抛出异常\n            if (timed && nanos <= 0L) {\n                breakBarrier();\n                throw new TimeoutException();\n            }\n        }\n    } finally {\n        lock.unlock();\n    }\n}\n```\n### Semaphore\nSemaphore 类似于操作系统中的信号量，可以控制对互斥资源的访问线程数。\n```java\npublic class MySemaphore {\n\n    public static void main(String[] args) {\n        //最大并发量为3\n        int clientCount= 3;\n        //一共有10个线程去竞争\n        int totalRequestCount = 10;\n        Semaphore semaphore = new Semaphore(clientCount);\n        ExecutorService executorService = Executors.newCachedThreadPool();\n        for (int i = 0; i < totalRequestCount; i++) {\n            executorService.execute(()->{\n                try {\n                    semaphore.acquire();\n                    System.out.println(\"当前线程:\"+Thread.currentThread().getName()+\"获取了一个许可，还剩:\"+semaphore.availablePermits());\n                    TimeUnit.SECONDS.sleep(2);\n                } catch (InterruptedException e) {\n                    e.printStackTrace();\n                }finally {\n                    semaphore.release();\n                    System.out.println(\"当前线程:\"+Thread.currentThread().getName()+\"释放了一个许可，还剩:\"+semaphore.availablePermits());\n                }\n            });\n        }\n        executorService.shutdown();\n    }\n}\n```\n```text\n当前线程:pool-1-thread-1获取了一个许可，还剩:1\n当前线程:pool-1-thread-3获取了一个许可，还剩:0\n当前线程:pool-1-thread-2获取了一个许可，还剩:1\n当前线程:pool-1-thread-3释放了一个许可，还剩:3\n当前线程:pool-1-thread-4获取了一个许可，还剩:2\n当前线程:pool-1-thread-2释放了一个许可，还剩:3\n当前线程:pool-1-thread-6获取了一个许可，还剩:0\n当前线程:pool-1-thread-1释放了一个许可，还剩:3\n当前线程:pool-1-thread-5获取了一个许可，还剩:1\n当前线程:pool-1-thread-6释放了一个许可，还剩:3\n当前线程:pool-1-thread-8获取了一个许可，还剩:1\n当前线程:pool-1-thread-7获取了一个许可，还剩:2\n当前线程:pool-1-thread-4释放了一个许可，还剩:3\n当前线程:pool-1-thread-5释放了一个许可，还剩:3\n当前线程:pool-1-thread-9获取了一个许可，还剩:0\n当前线程:pool-1-thread-7释放了一个许可，还剩:3\n当前线程:pool-1-thread-10获取了一个许可，还剩:2\n当前线程:pool-1-thread-9释放了一个许可，还剩:3\n当前线程:pool-1-thread-8释放了一个许可，还剩:3\n当前线程:pool-1-thread-10释放了一个许可，还剩:3\n```\n#### 源码分析\n底层还是依赖AQS，基本的源码看上文就不详细解析了，初始化如下默认使用非公平锁构造并设置state的值\n```java\nSemaphore semaphore = new Semaphore(2);\n\npublic Semaphore(int permits) {\n    sync = new NonfairSync(permits);\n}\n\n```\n```java\nSync(int permits) {\n    setState(permits);\n}\n\nprotected final void setState(int newState) {\n    state = newState;\n}\n```\n\n**acquire()**\n\n```java\npublic void acquire() throws InterruptedException {\n    sync.acquireSharedInterruptibly(1);\n}\n\npublic final void acquireSharedInterruptibly(int arg)\n        throws InterruptedException {\n        if (Thread.interrupted())\n            throw new InterruptedException();\n        //如果返回值 < 0 表示获取状态失败，需要被加入同步队列等待\n        if (tryAcquireShared(arg) < 0)\n            doAcquireSharedInterruptibly(arg);\n}\n\nprotected int tryAcquireShared(int acquires) {\n        return nonfairTryAcquireShared(acquires);\n}\n\nfinal int nonfairTryAcquireShared(int acquires) {\n        //循环获取可用的state值，用总的-1，并CAS修改值\n        for (;;) {\n            int available = getState();\n            int remaining = available - acquires;\n            if (remaining < 0 ||\n                compareAndSetState(available, remaining))\n                return remaining;\n        }\n}\n```\n失败则封装节点加入等待队列竞争锁\n```java\nprivate void doAcquireSharedInterruptibly(int arg)\n        throws InterruptedException {\n    final Node node = addWaiter(Node.SHARED);\n    boolean failed = true;\n    try {\n        for (;;) {\n            //获得当前节点pre节点\n            final Node p = node.predecessor();\n            if (p == head) {\n                int r = tryAcquireShared(arg);\n                if (r >= 0) {\n                    setHeadAndPropagate(node, r);\n                    p.next = null; // help GC\n                    failed = false;\n                    return;\n                }\n            }\n            //重组双向链表，清空无效节点，挂起当前线程\n            if (shouldParkAfterFailedAcquire(p, node) &&\n                parkAndCheckInterrupt())\n                throw new InterruptedException();\n        }\n    } finally {\n        if (failed)\n            cancelAcquire(node);\n    }\n}\n```\nrelease()\n```java\npublic void release() {\n    sync.releaseShared(1);\n}\n```\n```java\npublic final boolean releaseShared(int arg) {\n    //释放共享锁\n    if (tryReleaseShared(arg)) {\n        //唤醒所有共享节点线程\n        doReleaseShared();\n        return true;\n    }\n    return false;\n}\n```\n```java\nprivate void doReleaseShared() {\n    for (;;) {\n        Node h = head;\n        if (h != null && h != tail) {\n            int ws = h.waitStatus;\n            if (ws == Node.SIGNAL) {//是否需要唤醒后继节点\n                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))//修改状态为初始0\n                    continue;\n                unparkSuccessor(h);//唤醒h.nex节点线程\n            }\n            else if (ws == 0 &&\n                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE));\n        }\n        if (h == head)                   // loop if head changed\n            break;\n    }\n}\n```\n# 参考文章\n- https://blog.csdn.net/javazejian/article/details/75043422\n- https://www.jianshu.com/p/3fe5e41dc46f\n- https://blog.csdn.net/BThinker/article/details/104417813"
  },
  {
    "path": "Java-多线程/CAS.md",
    "content": "\n# CAS(Compare-and-Swap)\n## 原理\n- 实际上是使用了unsafe的CAS操作\n- 用期望值去与内存地址的值判断是否相等，相等则用更新值替换，否则CAS等待\n## 参数\ncompareAndSwapInt\n- this 对象\n- Offset 内存地址\n- expect 旧的期望值\n- update 新的值\n\n\n## CAS的问题\nCAS操作是一种无锁算法，相对于使用锁机制，它能够更好地利用多核处理器的优势，提高并发性能。但是，CAS操作也存在一些问题，包括：\n\n- `ABA问题`：在CAS操作中，如果内存位置的当前值与预期原值相等，就会执行修改操作。但是，如果在修改之前，有其他线程对内存位置的值进行了两次修改，从而又恢复为了原来的值，此时CAS操作可能会认为没有被修改过，从而执行了修改操作。这种情况被称为ABA问题。\n\n- `自旋次数问题`：如果CAS操作失败，需要重试，这时会进入自旋状态，不断地进行CAS操作，直到操作成功为止。如果自旋次数太多，会浪费大量的CPU资源，降低系统的性能。\n\n- `只能保证单个变量的原子操作`：CAS操作只能保证单个变量的原子操作，对于多个变量的操作，需要加锁才能保证原子性。\n\n针对上述问题，可以采取一些措施来解决。例如，\n\n- 针对ABA问题，可以使用带有版本号的CAS操作，从而保证在执行修改操作前，内存位置的值没有被修改过；\n- 针对自旋次数问题，可以设置一个自旋次数的上限，超过上限时放弃自旋，转为使用锁机制；\n- 针对多个变量的原子操作，可以使用分段锁等方式来保证原子性。"
  },
  {
    "path": "Java-多线程/Java对象头.md",
    "content": "\n# Java对象头\n## 对象在内存中的布局\n在JVM中，对象在内存中的布局分为三块区域：对象头、实例数据和对齐填充。如下\n![](../img/Java多线程/对象在内存中的布局.png)\n- 实例变量：存放类的属性数据信息，包括父类的属性信息，如果是数组的实例部分还包括数组的长度，这部分内存按4字节对齐。\n- 填充数据：由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的，仅仅是为了字节对齐，这点了解即可。\n\nJava对象头一般占有2个机器码（在32位虚拟机中，1个机器码等于4字节，也就是32bit，在64位虚拟机中，1个机器码是8个字节，也就是64bit），但是 如果对象是数组类型，则需要3个机器码，因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小，但是无法从数组的元数据来确认数组的大小，所以用一块来记录数组长度\n\n### Mark Word（标记字段）\n#### 默认存储结构\n![](../img/Java多线程/MarkWord默认储存结构.png)\n- 锁状态\n- 哈希码（HashCode）\n- GC分代年龄\n- 是否为偏向锁\n- 锁标志位\n#### 变化的结构\n![](../img/Java多线程/MarkWord变化的储存结构.png)\n- 锁状态标志\n  - 轻量级锁\n  - 重量级锁\n    - 重量级锁也就是通常说synchronized的对象锁，锁标识位为10，其中指针指向的是monitor对象（也称为管程或监视器锁）的起始地址\n    - 在Java虚拟机(HotSpot)中，monitor是由ObjectMonitor实现的\n      ![](../img/Java多线程/ObjectMonitor.png)\n      1. _WaitSet 和 _EntryList，用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象)，\n      2. _owner指向持有ObjectMonitor对象的线程\n      3. 当多个线程同时访问一段同步代码时，首先会进入 _EntryList 集合\n      4. 当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1，若线程调用 wait() 方法，将释放当前持有的monitor，owner变量恢复为null，count自减1\n      5. 同时该线程进入 WaitSe t集合中等待被唤醒\n      6. 若当前线程执行完毕也将释放monitor(锁)并复位变量的值，以便其他线程进入获取monitor(锁)\n- GC标记\n- 偏向锁\n### Class Pointer（类型指针）"
  },
  {
    "path": "Java-多线程/ThreadLocal.md",
    "content": "\n# ThreadLocal\n## 原理\n- 线程局部变量\n- 只在当前线程拥有，绝对的线程安全\n- 一个线程内可以存在多个 ThreadLocal 对象，所以其实是 ThreadLocal 内部维护了一个 Map\n\n## ThreadLocalMap\n- ThreadLocalMap 解决 hash 冲突的方式采用的是线性探测法，如果发生冲突会继续寻找下一个空的位置\n- 内存泄漏\n  - Entry为弱引用实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用，弱引用的特点是，如果这个对象只存在弱引用，那么在下一次垃圾回收的时候必然会被清理掉。\n  - 所以如果 ThreadLocal 没有被外部强引用的情况下，在垃圾回收的时候会被清理掉的，这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是，value 是强引用，不会被清理，这样一来就会出现 key 为 null 的 value\n  - 一定要remove\n\n## 源码分析\n```java\npublic class ThreadLocal<T> {\n    private final int threadLocalHashCode = nextHashCode();\n    private static AtomicInteger nextHashCode = new AtomicInteger();\n    \n    private static final int HASH_INCREMENT = 0x61c88647;\n    \n    private static int nextHashCode() {\n      return nextHashCode.getAndAdd(HASH_INCREMENT);\n    }\n    \n    static class ThreadLocalMap {\n        static class Entry extends WeakReference<ThreadLocal<?>> {\n          /** The value associated with this ThreadLocal. */\n          Object value;\n          //Entry 数组，然后 Entry 里面弱引用了 ThreadLocal 作为 Key。\n          Entry(ThreadLocal<?> k, Object v) {\n            super(k);\n            value = v;\n          }\n        }\n        private Entry[] table;\n    }\n}\n```\n\n### get\n先获取当前线程，然后获取当前线程中的ThreadLocalMap，然后以当前的ThreadLocal为key，到ThreadLocalMap中获取value\n```java\npublic T get() {\n    //获取当前线程\n    Thread t = Thread.currentThread();\n    ThreadLocalMap map = getMap(t);\n    if (map != null) {\n        ThreadLocalMap.Entry e = map.getEntry(this);\n        if (e != null) {\n            @SuppressWarnings(\"unchecked\")\n            T result = (T)e.value;\n            return result;\n        }\n    }\n    return setInitialValue();\n}\n\n//获取当前线程中的ThreadLocalMap\nThreadLocalMap getMap(Thread t) {\n    //ThreadLocalMap是存在于Thread类里的\n    return t.threadLocals;\n}\n```\n通过取模获取数组下标，如果没有冲突直接返回数据，否则同样出现遍历的情况\n```java\nprivate Entry getEntry(ThreadLocal<?> key) {\n    //用key的hash值与数组长度 得到index\n    int i = key.threadLocalHashCode & (table.length - 1);\n    Entry e = table[i];\n    //线性探测，如果值为空找下一个\n    if (e != null && e.get() == key)\n        return e;\n    else\n        return getEntryAfterMiss(key, i, e);\n}\n```\n```java\nprivate Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {\n    Entry[] tab = table;\n    int len = tab.length;\n\n    while (e != null) {\n        ThreadLocal<?> k = e.get();\n        if (k == key)\n            return e;\n        if (k == null)\n            //或主动清理无用的内存，但是如果清理的太多，自然慢了\n            expungeStaleEntry(i);\n        else\n            i = nextIndex(i, len);\n        e = tab[i];\n    }\n    return null;\n}\n```\n### set\n首先获取当前线程，然后获取当前线程中存储的threadLocals变量，此变量其实就是ThreadLocalMap，最后看此ThreadLocalMap是否为空，为空就创建一个新的Map，不为空则以当前的ThreadLocal为key，存储当前value\n```java\npublic void set(T value) {\n    Thread t = Thread.currentThread();\n    ThreadLocalMap map = getMap(t);\n    if (map != null)\n        map.set(this, value);\n    else\n        createMap(t, value);\n}\n```\nThreadLocalMap内部使用一个数组来保存数据，类似HashMap；每个ThreadLocal在初始化的时候会分配一个threadLocalHashCode，然后和数组的长度进行取模操作，所以就会出现hash冲突的情况，在HashMap中处理冲突是使用数组+链表的方式，而在ThreadLocalMap中，可以看到直接使用nextIndex，进行遍历操作，明显性能更差\n```java\nprivate void set(ThreadLocal<?> key, Object value) {\n\n    // We don't use a fast path as with get() because it is at\n    // least as common to use set() to create new entries as\n    // it is to replace existing ones, in which case, a fast\n    // path would fail more often than not.\n\n    Entry[] tab = table;\n    int len = tab.length;\n    int i = key.threadLocalHashCode & (len-1);\n\n    for (Entry e = tab[i];\n         e != null;\n         e = tab[i = nextIndex(i, len)]) {\n        ThreadLocal<?> k = e.get();\n\n        if (k == key) {\n            e.value = value;\n            return;\n        }\n\n        if (k == null) {\n            replaceStaleEntry(key, value, i);\n            return;\n        }\n    }\n\n    tab[i] = new Entry(key, value);\n    int sz = ++size;\n    if (!cleanSomeSlots(i, sz) && sz >= threshold)\n        rehash();\n}\n```\n\n```java\nvoid createMap(Thread t, T firstValue) {\n    t.threadLocals = new ThreadLocalMap(this, firstValue);\n}\n```\n\n所以通过分析可以大致知道以下几个问题\n1. ThreadLocalMap在解决冲突时，通过线性遍历法的方式，非常影响性能；\n2. key是弱引用，如果忘记remove会造成内存泄露\n\n\n## 使用场景\n1. 线程间数据隔离，各线程的 ThreadLocal 互不影响\n2. 方便同一个线程使用某一对象，避免不必要的参数传递\n3. 全链路追踪中的 traceId 或者流程引擎中上下文的传递一般采用 ThreadLocal\n4. Spring 事务管理器采用了 ThreadLocal\n5. Spring MVC 的 RequestContextHolder 的实现使用了 ThreadLocal\n\n### 每个线程维护了一个“序列号”\n```java\npublic class SerialNum {\n  // The next serial number to be assigned\n  private static int nextSerialNum = 0;\n\n  private static ThreadLocal serialNum = new ThreadLocal() {\n    protected synchronized Object initialValue() {\n      return new Integer(nextSerialNum++);\n    }\n  };\n\n  public static int get() {\n    return ((Integer) (serialNum.get())).intValue();\n  }\n}\n```\n### Session的管理\n```java\nprivate static final ThreadLocal threadSession = new ThreadLocal();  \n  \npublic static Session getSession() throws InfrastructureException {  \n    Session s = (Session) threadSession.get();  \n    try {  \n        if (s == null) {  \n            s = getSessionFactory().openSession();  \n            threadSession.set(s);  \n        }  \n    } catch (HibernateException ex) {  \n        throw new InfrastructureException(ex);  \n    }  \n    return s;  \n}  \n\n```\n### SimpleDateFormat\n```java\nimport java.text.DateFormat;\nimport java.text.SimpleDateFormat;\n \npublic class DateUtils {\n    public static final ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(){\n        @Override\n        protected DateFormat initialValue() {\n            return new SimpleDateFormat(\"yyyy-MM-dd\");\n        }\n    };\n}\n\nDateUtils.df.get().format(new Date());\n```\n\n## 手动释放ThreadLocal遗留存储?你怎么去设计/实现？\n包装其父类remove方法为静态方法，如果是spring项目， 可以借助于bean的声明周期， 在拦截器的afterCompletion阶段进行调用。\n\n## 弱引用导致内存泄漏，那为什么key不设置为强引用\n\n如果key设置为强引用， 当threadLocal实例释放后， threadLocal=null， 但是threadLocal会有强引用指向threadLocalMap，threadLocalMap.Entry又强引用threadLocal， 这样会导致threadLocal不能正常被GC回收。\n\n弱引用虽然会引起内存泄漏， 但是也有set、get、remove方法操作对null key进行擦除的补救措施， 方案上略胜一筹。\n\n## 线程执行结束后会不会自动清空Entry的value\n事实上，当currentThread执行结束后， threadLocalMap变得不可达从而被回收，Entry等也就都被回收了，但这个环境就要求不对Thread进行复用，但是我们项目中经常会复用线程来提高性能， 所以currentThread一般不会处于终止状态。\n\n## threadlocal如果不remove，出问题了怎么补救？\nthreadLocal和thread是绑定的，生命周期相同，那么，kill掉这个线程可以释放ThreadLocalMap\n\n## FastThreadLocal\n\n所以怎么改呢？\n\n前面提到 ThreadLocal hash 冲突的线性探测法不好，还有 Entry 的弱引用可能会发生内存泄漏，这些都和 ThreadLocalMap 有关，所以需要搞个新的 map 来替换 ThreadLocalMap。\n\n而这个 ThreadLocalMap 又是 Thread 里面的一个成员变量，这么一看 Thread 也得动一动，但是我们又无法修改 Thread 的代码，所以配套的还得弄个新的 Thread。\n\n所以我们不仅得弄个新的 ThreadLocal、ThreadLocalMap 还得弄个配套的 Thread 来用上新的 ThreadLocalMap 。\n\n所以如果想改进 ThreadLocal ，就需要动这三个类。\n\n对应到 Netty 的实现就是 FastThreadLocal、InternalThreadLocalMap、FastThreadLocalThread\n\n![](../img/Java多线程/threadlocal模型.png)\n\n### 源码分析\n\nFastThreadLocal\n```java\npublic class FastThreadLocal<V> {\n  //可以看到有个叫 variablesToRemoveIndex 的类成员，并且用 final 修饰的，所以等于每个 FastThreadLocal 都有个共同的不可变 int 值  \n  private static final int variablesToRemoveIndex = InternalThreadLocalMap.nextVariableIndex();\n  //在 FastThreadLocal 构造的时候就被赋值了，且也被 final 修饰，所以也不可变，这个 index 就是我上面说的给每个新 FastThreadLocal 都发个唯一的下标，这样每个 index 就都知道自己的位置了\n  private final int index;\n\n  public FastThreadLocal() {\n    index = InternalThreadLocalMap.nextVariableIndex();\n  }\n}\n```\n在 InternalThreadLocalMap 也定义了一个静态原子类，每次调用 nextVariableIndex 就返回且递增，没有什么别的赋值操作，从这里也可以得知 variablesToRemoveIndex 的值为 0，因为它属于常量赋值，第一次调用时 nextIndex 的值为 0\n```java\nstatic final AtomicInteger nextIndex = new AtomicInteger();\npublic static int nextVariableIndex() {\n    int index = nextIndex.getAndIncrement();\n    if (index < 0) {\n        nextIndex.decrementAndGet();\n        throw new IllegalStateException(\"too many thread-local indexed variables\");\n    }\n    return index;\n}\n```\n上文ThreadLocalMap保存数据是使用entry数组，然后 Entry 里面弱引用了 ThreadLocal 作为 Key\n\n而 InternalThreadLocalMap 有点不太一样\n\n```java\npublic final class InternalThreadLocalMap extends UnpaddedInternalThreadLocalMap {\n    //通过object数组来实现\n    public static final Object UNSET = new Object();\n    private InternalThreadLocalMap() {\n      super(newIndexedVariableTable());\n    }\n    //默认生成32长度的数组，并且填充默认值\n    private static Object[] newIndexedVariableTable() {\n      Object[] array = new Object[32];\n      Arrays.fill(array, UNSET);\n      return array;\n    }\n}\n```\n\n可以看到， InternalThreadLocalMap 好像放弃了 map 的形式，没用定义 key 和 value，而是一个 Object 数组？\n\n那它是如何通过 Object 来存储  FastThreadLocal 和对应的 value 的呢？我们从 FastThreadLocal#set 开始分析：\n\n```java\npublic final void set(V value) {\n    //如果塞入的不是默认值\n    if (value != InternalThreadLocalMap.UNSET) {\n      InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();\n      if (setKnownNotUnset(threadLocalMap, value)) {\n        registerCleaner(threadLocalMap);\n      }\n    } else {\n      remove();\n    }\n}\n\nprivate boolean setKnownNotUnset(InternalThreadLocalMap threadLocalMap, V value) {\n    //index就是构造时被分配得到的下标\n    if (threadLocalMap.setIndexedVariable(index, value)) {\n        addToVariablesToRemove(threadLocalMap, this);\n        return true;\n    }\n    return false;\n}\n\npublic boolean setIndexedVariable(int index, Object value) {\n    Object[] lookup = indexedVariables;\n    if (index < lookup.length) {\n        //直接通过索引找到位置，进行替换\n        Object oldValue = lookup[index];\n        lookup[index] = value;\n        return oldValue == UNSET;\n    } else {\n        //如果index大于数组size，进行扩容\n        expandIndexedVariableTableAndSet(index, value);\n        return true;\n    }\n}\n```\n可以看到，根据传入构造 FastThreadLocal 生成的唯一 index 可以直接从 Object 数组里面找到下标并且进行替换，这样一来压根就不会产生冲突，逻辑很简单，完美。\n\n那如果塞入的 value 不是 UNSET(默认值)，则执行 addToVariablesToRemove 方法，这个方法又有什么用呢？\n```java\nprivate static void addToVariablesToRemove(InternalThreadLocalMap threadLocalMap, FastThreadLocal<?> variable) {\n    Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);\n    Set<FastThreadLocal<?>> variablesToRemove;\n    //如果v不是默认值，且v == null\n    if (v == InternalThreadLocalMap.UNSET || v == null) {\n        //构建一个set\n        variablesToRemove = Collections.newSetFromMap(new IdentityHashMap<FastThreadLocal<?>, Boolean>());\n        //将这个set放入object数组的第0个位置\n        threadLocalMap.setIndexedVariable(variablesToRemoveIndex, variablesToRemove);\n    } else {\n        //反之将v转为set\n        variablesToRemove = (Set<FastThreadLocal<?>>) v;\n    }\n    //将传入的FastThreadLocal存入set\n    variablesToRemove.add(variable);\n}\n```\n是不是看着有点奇怪？这是啥操作？别急，看我画个图来解释解释：\n![](../img/Java多线程/nternalThreadLocalMap里的object数组.png)\n\n这就是 Object 数组的核心关系图了，第一个位置放了一个 set ，set 里面存储了所有使用的 FastThreadLocal 对象，然后数组后面的位置都放 value。\n\n那为什么要放一个 set 保存所有使用的 FastThreadLocal 对象？\n\n用于删除，你想想看，假设现在要清空线程里面的所有 FastThreadLocal ，那必然得有一个地方来存放这些 FastThreadLocal 对象，这样才能找到这些家伙，然后干掉。\n\n所以刚好就把数组的第一个位置腾出来放一个 set 来保存这些 FastThreadLocal 对象，如果要删除全部 FastThreadLocal 对象的时候，只需要遍历这个 set ，得到 FastThreadLocal 的 index 找到数组对应的 位置将 value 置空，然后把 FastThreadLocal 从 set 中移除即可。\n\n刚好 FastThreadLocal 里面实现了这个方法，我们来看下：\n```java\npublic static void removeAll() {\n        InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.getIfSet();\n        if (threadLocalMap == null) {\n            return;\n        }\n\n        try {\n            //得到v,v就是那个set\n            Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);\n            if (v != null && v != InternalThreadLocalMap.UNSET) {\n                @SuppressWarnings(\"unchecked\")\n                Set<FastThreadLocal<?>> variablesToRemove = (Set<FastThreadLocal<?>>) v;\n                //将set转成数组遍历\n                FastThreadLocal<?>[] variablesToRemoveArray =\n                        variablesToRemove.toArray(new FastThreadLocal[0]);\n                for (FastThreadLocal<?> tlv: variablesToRemoveArray) {\n                    //分别调用remove\n                    tlv.remove(threadLocalMap);\n                }\n            }\n        } finally {\n            //这个是将线程里的map置空，完成整体的移出\n            InternalThreadLocalMap.remove();\n        }\n    }\n```\n内容可能有点多了，我们做下小结，理一理上面说的：\n\n首先 InternalThreadLocalMap 没有采用 ThreadLocalMap k-v形式的存储方式，而是用 Object 数组来存储 FastThreadLocal 对象和其 value，具体是在第一个位置存放了一个包含所使用的 FastThreadLocal 对象的 set，然后后面存储所有的 value。\n\n之所以需要个 set 是为了存储所有使用的 FastThreadLocal 对象，这样就能找到这些对象，便于后面的删除工作。\n\n之所以数组其他位置可以直接存储 value ，是因为每个 FastThreadLocal 构造的时候已经被分配了一个唯一的下标，这个下标对应的就是 value 所处的下标。\n\n下面再来看下get方法\n```java\npublic final V get() {\n        InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();\n        Object v = threadLocalMap.indexedVariable(index);\n        if (v != InternalThreadLocalMap.UNSET) {\n            return (V) v;\n        }\n\n        V value = initialize(threadLocalMap);\n        registerCleaner(threadLocalMap);\n        return value;\n}\n\npublic Object indexedVariable(int index) {\n    //直接进行数组的下标的获取\n    Object[] lookup = indexedVariables;\n    return index < lookup.length? lookup[index] : UNSET;\n}\n```\n看一下InternalThreadLocalMap.get()\n```java\npublic static InternalThreadLocalMap get() {\n      Thread thread = Thread.currentThread();\n      if (thread instanceof FastThreadLocalThread) {\n          return fastGet((FastThreadLocalThread) thread);\n      } else {\n          return slowGet();\n      }\n}\nprivate static InternalThreadLocalMap fastGet(FastThreadLocalThread thread) {\n      InternalThreadLocalMap threadLocalMap = thread.threadLocalMap();\n      if (threadLocalMap == null) {\n          thread.setThreadLocalMap(threadLocalMap = new InternalThreadLocalMap());\n      }\n      return threadLocalMap;\n}\n\nprivate static InternalThreadLocalMap slowGet() {\n      ThreadLocal<InternalThreadLocalMap> slowThreadLocalMap = UnpaddedInternalThreadLocalMap.slowThreadLocalMap;\n      InternalThreadLocalMap ret = slowThreadLocalMap.get();\n      if (ret == null) {\n          ret = new InternalThreadLocalMap();\n          slowThreadLocalMap.set(ret);\n      }\n      return ret;\n}\n```\n\n这里之所以分了 fastGet 和 slowGet 是为了做一个兼容，假设有个不熟悉的人，他用了 FastThreadLocal 但是没有配套使用 FastThreadLocalThread ，然后调用 FastThreadLocal#get 的时候去 Thread 里面找 InternalThreadLocalMap 那不就傻了吗，会报错的。\n\n所以就再弄了个 slowThreadLocalMap ，它是个 ThreadLocal ，里面保存 InternalThreadLocalMap 来兼容一下这个情况。\n\n从这里我们也能得知，FastThreadLocal 最好和 FastThreadLocalThread 配套使用，不然就隔了一层了。\n```java\nFastThreadLocal<String> threadLocal = new FastThreadLocal<String>();\nThread t = new FastThreadLocalThread(new Runnable() { //记得要 new FastThreadLocalThread\n     public void run() {\n\t     threadLocal.get()；\n\t     ....\n     }\n });\n```\n最后看下remove\n```java\npublic final void remove() {\n    remove(InternalThreadLocalMap.getIfSet());\n}\n\npublic final void remove(InternalThreadLocalMap threadLocalMap) {\n    if (threadLocalMap == null) {\n        return;\n    }\n\n    Object v = threadLocalMap.removeIndexedVariable(index);\n    //通过删除数组对应下标的value\n    removeFromVariablesToRemove(threadLocalMap, this);\n\n    if (v != InternalThreadLocalMap.UNSET) {\n        try {\n            onRemoval((V) v);\n        } catch (Exception e) {\n            PlatformDependent.throwException(e);\n        }\n    }\n}\n\npublic Object removeIndexedVariable(int index) {\n    Object[] lookup = indexedVariables;\n    if (index < lookup.length) {\n        Object v = lookup[index];\n        //找到位置，覆盖成初始值\n        lookup[index] = UNSET;\n        return v;\n    } else {\n        return UNSET;\n    }\n}\n//找到set，调用set的remove方法\nprivate static void removeFromVariablesToRemove(\n    InternalThreadLocalMap threadLocalMap, FastThreadLocal<?> variable) {\n\n    Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);\n\n    if (v == InternalThreadLocalMap.UNSET || v == null) {\n        return;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    Set<FastThreadLocal<?>> variablesToRemove = (Set<FastThreadLocal<?>>) v;\n    variablesToRemove.remove(variable);\n}\n```\n\n很简单对吧，把数组里的 value 给覆盖了，然后再到  set  里把对应的 FastThreadLocal 对象给删了。\n\n不过看到这里，可能有人会发出疑惑，内存泄漏相关的点呢？\n\n其实吧，可以看到 FastThreadLocal 就没用弱引用，所以它把无用 FastThreadLocal 的清理就寄托到规范使用上，即没用了就主动调用 remove 方法。\n\n你可以这样写\n```java\n@Override\npublic void run() {\n    try {\n        //...\n    } finally {\n        FastThreadLocal.removeAll();\n    }\n}\n```\n\n我们再来总结一下：\n\n- FastThreadLocal 通过分配下标直接定位 value ，不会有 hash 冲突，效率较高。\n- FastThreadLocal 采用空间换时间的方式来提高效率。\n- FastThreadLocal 需要配套 FastThreadLocalThread 使用，不然还不如原生 ThreadLocal。\n- FastThreadLocal 使用最好配套 FastThreadLocalRunnable，这样执行完任务后会主动调用 removeAll 来移除所有 FastThreadLocal ，防止内存泄漏。\n- FastThreadLocal 的使用也是推荐用完之后，主动调用 remove。\n\n# 参考文章：\n- https://www.pdai.tech/md/java/thread/java-thread-x-threadlocal.html#java-%E5%BC%80%E5%8F%91%E6%89%8B%E5%86%8C%E4%B8%AD%E6%8E%A8%E8%8D%90%E7%9A%84-threadlocal\n- https://www.cnblogs.com/javazhiyin/p/11834121.html\n- https://juejin.cn/post/6844903966363353101\n- https://juejin.cn/post/7005084861975232549?utm_source=gold_browser_extension\n"
  },
  {
    "path": "Java-多线程/volatile.md",
    "content": "\n# 前提了解\n## 机器内存模型\n\n![](../img/Java多线程/机器内存模型.png)\n\n由于CPU和内存的速度差异（内存的处理速度远低于CPU）所以引入了CPU缓存，一般都有几级缓存，处理数据时先从CPU缓存读到寄存器，如果没有才会访问主内存以提高速度，CPU处理完也会先刷新值到寄存器->CPU缓存->主内存\n### 多核下的缓存一致性问题\n\n#### 问题\n\n`核0`读取了一个字节，根据局部性原理，它相邻的字节同样被被读入`核0`的缓存，`核3`做了上面同样的工作，这样`核0`与`核3`的缓存拥有同样的数据，`核0`修改了那个字节，被修改后，那个字节被写回`核0`的缓存，但是该信息并没有写回`主存`，`核3`访问该字节，由于`核0`并未将数据写回`主存`，数据不同步。\n\n#### 方案\n\n1、**锁总线**\n- 前端总线(也叫CPU总线)是所有CPU与芯片组连接的主干道，负责CPU与外界所有部件的通信，包括高速缓存、内存、北桥，其控制总线向各个部件发送控制信号、通过地址总线发送地址信号指定其要访问的部件、通过数据总线双向传输\n- 总线上发出一个LOCK#信号，其他CPU不能操作该变量，相当于阻塞其他CPU\n- 开销大\n  \n2、**多核CPU多级缓存一致性协议MESI**\n\nMESI 是指4中状态的首字母。每个Cache line有4个状态\n\n| 状态  | 描述 | 监听任务 | \n| ------------- | ------------- | ------------- |\n| M 修改 (Modified)  | 该Cache line有效，数据被修改了，和内存中的数据不一致，数据只存在于本Cache中。  | 表示当前CPU的高速缓存中的变量副本是独占的，而且和主存中的变量值不一致，而且别的CPU的flag不可能是这个状态。如果别的CPU想要读取变量的值，不能直接读主内存中的值，而是需要将处于M状态的变量刷新回主内存才可以。|\n| E 独享、互斥 (Exclusive)  | 该Cache line有效，数据和内存中的数据一致，数据只存在于本Cache中。  | 表示当前CPU的高速缓存中的变量副本是独占的，别的CPU高速缓存中该变量的副本不能处于该状态，但是，处于E状态的高速缓存变量的值和主内存中的变量值是一致的。 |\n| S 共享 (Shared)  | 该Cache line有效，数据和内存中的数据一致，数据存在于很多Cache中。  | 处于S状态表示CPU中的变量副本和主存中数据一致，而且多个CPU都可以处于S状态，举例，当多个CPU读取主内存的值的时候高速缓存的flag就处于S状态。 |\n| I 无效 (Invalid)  | 该Cache line无效。  | 表示当前CPU的高速缓存的变量副本处于不合法状态，不可以直接使用，需要从主内存重新读取，flag的初始状态就是I。 |\n                             \n## 指令重排\n为了使得处理器内部的运算单元能尽量被充分利用，处理器可能会对输入代码进行乱序执行（Out-Of-Order Execution）优化\n\n- 编译器优化的重排\n  - 编译器在不改变单线程程序语义的前提下，可以重新安排语句的执行顺序\n- 指令并行的重排\n  - 现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性(即后一个执行的语句无需依赖前面执行的语句的结果)，处理器可以改变语句对应的机器指令的执行顺序\n- 内存系统的重排\n  - 由于处理器使用缓存和读写缓存冲区，这使得加载(load)和存储(store)操作看上去可能是在乱序执行，因为三级缓存的存在，导致内存与缓存的数据同步存在时间差\n\n## JMM\n### 模型\n![](../img/Java多线程/JMM模型.png)\n\nJMM是一种抽象概念并不存在，他描述是一种规则，JMM中所有的变量都储存在主内存中，主内存是共享区域，所有线程都可以访问，但是对变量的操作需要在工作内存中进行，所以需要线程把变量拷贝到工作内存操作完后写回主内存\n### 要保证的三个特性\n- 原子性\n  - 原子性指的是一个操作是不可中断的，即使是在多线程环境下，一个操作一旦开始就不会被其他线程影响\n  - 比如对于一个静态变量int x，两条线程同时对他赋值，线程A赋值为1，而线程B赋值为2，不管线程如何运行，最终x的值要么是1，要么是2，线程A和线程B间的操作是没有干扰的，这就是原子性操作，不可被中断的特点\n- 可见性\n  - 可见性指的是当一个线程修改了某个共享变量的值，其他线程是否能够马上得知这个修改的值\n- 有序性\n  - 有序性是指对于单线程的执行代码，我们总是认为代码的执行是按顺序依次执行的，这样的理解并没有毛病，毕竟对于单线程而言确实如此，但对于多线程环境，则可能出现乱序现象，因为程序编译成机器码指令后可能会出现指令重排现象，重排后的指令与原指令的顺序未必一致\n### happens-before 原则\n上述特性如何保证，可以使用synchronized和volatile来保证，但是编写可能会显得十分麻烦，所以有happens-before 原则来辅助\n#### 内容\n- 程序顺序原则，即在一个线程内必须保证语义串行性，也就是说按照代码顺序执行。\n- 锁规则 解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前，也就是说，如果对于一个锁解锁后，再加锁，那么加锁的动作必须在解锁动作之后(同一个锁)。\n- volatile规则 volatile变量的写，先发生于读，这保证了volatile变量的可见性，简单的理解就是，volatile变量在每次被线程访问时，都强迫从主内存中读该变量的值，而当该变量发生变化时，又会强迫将最新的值刷新到主内存，任何时刻，不同的线程总是能够看到该变量的最新值。\n- 线程启动规则 线程的start()方法先于它的每一个动作，即如果线程A在执行线程B的start方法之前修改了共享变量的值，那么当线程B执行start方法时，线程A对共享变量的修改对线程B可见\n- 传递性 A先于B ，B先于C 那么A必然先于C\n- 线程终止规则 线程的所有操作先于线程的终结，Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前，修改了共享变量，线程A从线程B的join方法成功返回后，线程B对共享变量的修改将对线程A可见。\n- 线程中断规则 对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生，可以通过Thread.interrupted()方法检测线程是否中断。\n- 对象终结规则 对象的构造函数执行，结束先于finalize()方法\n\n# volatile作用和特点\n- 保证可见性\n- 不具备原子性\n## volatile可见性如何实现\n### 1、内存屏障：\n\n- volatile 变量在读写时会插入内存屏障（Memory Barrier），这意味着：\n  - 对 volatile 变量的写操作会强制线程将该变量的值写入主内存，确保其他线程能够看到最新的值。\n  - 对 volatile 变量的读操作会强制读取主内存中的值，而不是从本地缓存中读取。\n\n**内存屏障** 在指令间插入一条Memory Barrier则会告诉编译器和CPU，不管什么指令都不能和这条Memory Barrier指令重排序，也就是说 通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化\n\n| 屏障类型 | 指令示例 | 说明|\n| ------ | ------ | ------ |\n| LoadLoad | Load1;LoadLoad;Load2 | 保证Load1的读取操作在Load2及后续读取操作之前执行 |\n| StoreStore | Store1;StoreStore;Store2 | 在Store2及其后的写操作执行前，保证Store1的写操作已刷新到主内存 |\n| LoadStore | Load1;LoadStore;Store2 | 在Store2及其后的写操作执行前，保证Load1的读操作已读取结束 |\n| StoreLoad | Store1;StoreLoad;Load2 | 保证store1的写操作已刷新到主内存之后，load2及其后的读操作才能执行 |\n\n### 2、禁止重排序：\n\n- volatile 变量的写操作不会被重排序到该操作之前，也不会将该操作后的读取与之前的操作重排序。这有助于确保在多线程环境下的可见性。\n\n# volatile解决可见性的代码\n```java\npublic class TestVolatile {\n    public static void main(String[] args) {\n        ThreadDemo td = new ThreadDemo();\n        new Thread(td).start();\n        while (true) {\n            if (td.isFlag()) {\n                System.out.println(\"------------------\");\n                break;\n            }\n        }\n    }\n}\n\nclass ThreadDemo implements Runnable {\n    private boolean flag = false;\n    @Override\n    public void run() {\n        try {\n            Thread.sleep(200);\n        } catch (InterruptedException e) {\n        }\n        flag = true;\n        System.out.println(\"flag=\" + isFlag());\n\n    }\n    public boolean isFlag() {\n        return flag;\n    }\n\n    public void setFlag(boolean flag) {\n        this.flag = flag;\n    }\n}\n\n// 此时输出： 并且陷入死循环\n    flag=true\n\n//修改代码\n// 给变量加上volatile关键字，实现内存可见性\nprivate volatile boolean flag = false;\n\n输出：\n        flag=true\n        ------------------\n\n        Process finished with exit code 0\n\n```\n# 验证volatile不具备原子性\n```java\npublic class TestVolatile1 {\n    public static void main(String[] args) {\n        myData myData=new myData();\n        for (int i = 0; i <20 ; i++) {\n            new Thread(()->{\n                for (int j = 0; j <1000 ; j++) {\n                    myData.addPlusPlus();\n                }\n            },String.valueOf(i)).start();\n        }\n        while (Thread.activeCount()>2){ //使用IntelliJ IDEA的读者请注意，\n       // 在IDEA中运行这段程序，会由于IDE自动创建一条名为Monito rCtrl-Break的线程,所以为2\n            Thread.yield();//当前线程由执行态变为就绪态，让出cpu\n        }\n        System.out.println(myData.num);\n    }\n}\n\nclass myData{\n     volatile int num=0;\n    public void addPlusPlus(){\n        this.num++;\n    }\n}\n\n```\n每次结果都不一样，并且不是20000，说明不具有原子性。\n\n这里我们得出一个结论 num++ 在多线程下是不安全的\n尽管用了volatile 第三步能够及时写入到内存。但是它不具备原子性，比如线程A从栈中取出i,此时完成了自增，发生了线程调度，此时线程B取出栈的值，尽管线程A里的值发生了更改，但是还未写到栈里，此时线程B操作的还是之前的值。这就证明了volatile不具备原子性。\n\n可以使用AtomicInteger解决\n# 参考文章\n- https://blog.csdn.net/jerry11112/article/details/106870835"
  },
  {
    "path": "Java-多线程/线程.md",
    "content": "# 线程的生命状态\n![](../img/Java多线程/线程的生命状态.png)\n### 新建new\n创建后尚未启动。\n### 可运行runnable\n- 可能正在运行，也可能正在等待 CPU 时间片。\n- 包含了操作系统线程状态中的 Running 和 Ready。\n### 阻塞blocked\n等待获取一个排它锁，如果其线程释放了锁就会结束此状态。\n### 等待waiting\n等待其它线程显式地唤醒，否则不会被分配 CPU 时间片。\n- 进入和退出方法\n  - 没有设置 Timeout 参数的 Object.wait() 方法\n    - Object.notify() / Object.notifyAll()\n- 没有设置 Timeout 参数的 Thread.join() 方法\n  - 被调用的线程执行完毕\n- LockSupport.park() 方法\n  - LockSupport.unpark(Thread)\n### 期限等待timed waiting\n无需等待其它线程显式地唤醒，在一定时间之后会被系统自动唤醒。\n- 进入退出方法\n  - Thread.sleep() 方法\n    - 时间结束\n  - 设置了 Timeout 参数的 Object.wait() 方法\n    - 时间结束 / Object.notify() / Object.notifyAll()\n  - 设置了 Timeout 参数的 Thread.join() 方法\n    - 时间结束 / 被调用的线程执行完毕\n  - LockSupport.parkNanos() 方法\n    - LockSupport.unpark(Thread)\n  - LockSupport.parkUntil() 方法\n    - LockSupport.unpark(Thread)\n### 死亡terminated\n可以是线程结束任务之后自己结束，或者产生了异常而结束。\n# 使用线程\n### 继承Thread\n```java\npublic class MyThread extends Thread {\n  @Override\n  public void run() {\n\n  }\n\n  public static void main(String[] args) {\n    MyThread myThread = new MyThread();\n    myThread.start();\n  }\n}\n```\n- 同样也是需要实现 run() 方法，因为 Thread 类也实现了 Runnable 接口。\n- 适用于无返回值，不需要继承父类\n### 实现Runnable接口\n```java\npublic class MyRunnable implements Runnable{\n    @Override\n    public void run() {\n        \n    }\n\n    public static void main(String[] args) {\n        MyRunnable runnable = new MyRunnable();\n        Thread thread = new Thread(runnable);\n        thread.start();\n    }\n}\n```\n- 需要实现 run() 方法。通过 Thread 调用 start() 方法来启动线程。\n- 适用于需要继承父类，无返回值\n### 实现Callable接口\n```java\npublic class MyCallable implements Callable<Integer> {\n    @Override\n    public Integer call() throws Exception {\n        return 0;\n    }\n\n    public static void main(String[] args) throws ExecutionException, InterruptedException {\n        MyCallable callable = new MyCallable();\n        FutureTask<Integer> futureTask = new FutureTask<>(callable);\n        Thread thread = new Thread(futureTask);\n        thread.start();\n        System.out.println(futureTask.get());\n    }\n}\n```\n- 与 Runnable 相比，Callable 可以有返回值，返回值通过 FutureTask 进行封装。\n- 适用于需要继承父类，且有返回值\n#### Callable如何返回值的\nCallable实现返回的本质是FutureTask实现类 `public FutureTask(Callable<V> callable)`我们传入Callable构造FutureTask\n##### FutureTask\n- 变量\n```java\n//线程执行的状态变量\nprivate volatile int state;\n//新建\nprivate static final int NEW          = 0;\n//正在设置任务结果\nprivate static final int COMPLETING   = 1;\n//正常执行完成\nprivate static final int NORMAL       = 2;\n//异常\nprivate static final int EXCEPTIONAL  = 3;\n//取消\nprivate static final int CANCELLED    = 4;\n//中断中\nprivate static final int INTERRUPTING = 5;\n//中断\nprivate static final int INTERRUPTED  = 6;\n\n//传入的Callable对象\n/** The underlying callable; nulled out after running */\nprivate Callable<V> callable;\n//返回值\n/** The result to return or exception to throw from get() */\nprivate Object outcome; // non-volatile, protected by state reads/writes\n/** The thread running the callable; CASed during run() */\nprivate volatile Thread runner;\n/** Treiber stack of waiting threads */\nprivate volatile WaitNode waiters;\n```\n- 构造器\n```java\npublic FutureTask(Callable<V> callable) {\n    if (callable == null)\n        throw new NullPointerException();\n    this.callable = callable;\n    //初始时状态为0\n    this.state = NEW;       // ensure visibility of callable\n}\n```\n- run\n```java\npublic void run() {\n    if (state != NEW ||!UNSAFE.compareAndSwapObject(this, runnerOffset,null, Thread.currentThread()))\n        return;\n    try {\n        Callable<V> c = callable;\n        if (c != null && state == NEW) {\n            V result;\n            boolean ran;\n            try {\n                //调用执行返回值\n                result = c.call();\n                ran = true;\n            } catch (Throwable ex) {\n                result = null;\n                ran = false;\n                setException(ex);\n            }\n            //正常走set方法\n            if (ran)\n                set(result);\n        }\n    } finally {\n        // runner must be non-null until state is settled to\n        // prevent concurrent calls to run()\n        runner = null;\n        // state must be re-read after nulling runner to prevent\n        // leaked interrupts\n        int s = state;\n        if (s >= INTERRUPTING)\n            handlePossibleCancellationInterrupt(s);\n    }\n}\n```\n```java\nprotected void set(V v) {\n    //CAS设置状态值为1 \n    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {\n        //赋值返回值给outcome\n        outcome = v;\n        //CAS设置状态值为2 \n        UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state\n        //从队列移除，唤醒其他等待node\n        finishCompletion();\n    }\n}\n```\n- get方法\n```java\npublic V get() throws InterruptedException, ExecutionException {\n    int s = state;\n    //如果没有执行完则等待\n    if (s <= COMPLETING)\n        s = awaitDone(false, 0L);\n    return report(s);\n}\n    \nprivate int awaitDone(boolean timed, long nanos) throws InterruptedException {\n    final long deadline = timed ? System.nanoTime() + nanos : 0L;\n    WaitNode q = null;\n    boolean queued = false;\n    for (;;) {\n        //线程中断了，移除等待节点\n        if (Thread.interrupted()) {\n            removeWaiter(q);\n            throw new InterruptedException();\n        }\n        //获取任务状态\n        int s = state;\n        //是正在执行的其他状态，直接返回状态值\n        if (s > COMPLETING) {\n            if (q != null)\n                q.thread = null;\n            return s;\n        }\n        //如果任务正在设置执行结果，则挂起当前get线程\n        else if (s == COMPLETING) // cannot time out yet\n            Thread.yield();\n        //如果任务还未执行，或正在执行的过程 将当前get线程封装为等待节点\n        else if (q == null)\n            q = new WaitNode();\n        //如果q不为空，但是q没有加入到等待队列，则CAS将等待节点添加到waiters链表的头节点\n        else if (!queued)\n            queued = UNSAFE.compareAndSwapObject(this, waitersOffset,\n                                                 q.next = waiters, q);\n        //如果是期限等待，需要根据时间来移除等待节点或者挂起线程（有时间的挂起）\n        else if (timed) {\n            nanos = deadline - System.nanoTime();\n            if (nanos <= 0L) {\n                removeWaiter(q);\n                return state;\n            }\n            LockSupport.parkNanos(this, nanos);\n        }\n        //否则挂起线程,这里unpark可以有下面2种情况\n          //1.任务执行完毕了，在finishCompletion方法中会唤醒所有在等待的线程\n          //2.等待的线程自身因为被中断等原因而被唤醒。\n        else\n            LockSupport.park(this);\n    }\n}\n\nprivate V report(int s) throws ExecutionException {\n    Object x = outcome;\n    //正常结束返回结果\n    if (s == NORMAL)\n        return (V)x;\n    if (s >= CANCELLED)\n        throw new CancellationException();\n    throw new ExecutionException((Throwable)x);\n}\n```\n# 线程基本方法\n### wait\n- 调用 wait() 使得线程等待某个条件满足，线程在等待时会被挂起，当其他线程的运行使得这个条件满足时，其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。\n  - 它们都属于 Object 的一部分，而不属于 Thread\n- 只能用在同步方法或者同步控制块中使用，否则会在运行时抛出 IllegalMonitorStateException。\n- 使用 wait() 挂起期间，线程会释放锁。这是因为，如果没有释放锁，那么其它线程就无法进入对象的同步方法或者同步控制块中，那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程，造成死锁。\n### sleep\nThread.sleep(millisec) 方法会休眠当前正在执行的线程，millisec 单位为毫秒。\n### yield\n对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分，可以切换给其它线程来执行。该方法只是对线程调度器的一个建议，而且也只是建议具有相同优先级的其它线程可以运行。\n### interrupt\n通过调用一个线程的 interrupt() 来中断该线程，如果该线程处于阻塞、限期等待或者无限期等待状态，那么就会抛出 InterruptedException，从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞\n### join\n在线程中调用另一个线程的 join() 方法，会将当前线程挂起，而不是忙等待，直到目标线程结束。\n### notify\n调用 notify() 或者 notifyAll() 来唤醒挂起的线程\n### await() signal() signalAll()\njava.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调，可以在 Condition 上调用 await() 方法使线程等待，其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。相比于 wait() 这种等待方式，await() 可以指定等待的条件，因此更加灵活\n```java\npublic class AwaitSignalExample {\n  private final Lock lock = new ReentrantLock();\n  private final Condition condition = lock.newCondition();\n\n  public void before(){\n    lock.lock();\n    try {\n      System.out.println(\"before\");\n      condition.signalAll();\n    }finally {\n      lock.unlock();\n    }\n  }\n\n  public void after(){\n    lock.lock();\n    try {\n      condition.await();\n      System.out.println(\"after\");\n    }catch (InterruptedException e){\n      e.printStackTrace();\n    }finally {\n      lock.unlock();\n    }\n  }\n\n  public static void main(String[] args) {\n    ExecutorService executorService = Executors.newCachedThreadPool();\n    AwaitSignalExample example = new AwaitSignalExample();\n    executorService.execute(example::after);\n    executorService.execute(example::before);\n  }\n}\n```\n```text\nbefore\nafter\n```\n# Java里怎么保证多个线程的互斥性\n## 什么是互斥\n为了解决竞争条件带来的问题，我们可以对资源上锁。多个线程共同读写的资源称为共享资源，也叫 临界资源。涉及操作临界资源的代码区域称为 临界区（Critical Section）。同一时刻，只能有一个线程进入临界区。我们把这种情况称为互斥，即不允许多个线程同时对共享资源进行操作。\n- 所以问答这个问题其实就是回答加锁，而Java的锁有lock、synchronized\n## 衍生出来就是线程怎么同步的问题\n### 什么是同步\n多个线程通过协作的方式，对相同资源进行操作，这种行为称为同步。同步实际上就是线程间的合作，只不过合作时需要操作同一资源。\n#### 著名的例子就是生产者-消费者问题\n现在有一个生产者和一个消费者，生产者负责生产资源，并放在盒子中，盒子的容量无限大；消费者从盒子中取走资源，如果盒子中没有资源，则需要等待。\n ```java\n //伪代码\n private static Box box = new Box();\n private static int boxSize = 0;\n \n public static void producer() {\n     wait(box);\n     //往 box 中放入资源，boxSize++\n     signal(box);\n }\n \n public static void consumer() {\n     while (boxSize == 0); //资源为零时阻塞\n     wait(box);\n     //从 box 中取出资源，boxSize--\n     signal(box);\n }\n \n public static void main(String[] args) {\n     parbegin(producer, consumer); //两个函数由两个线程并发执行\n }\n ```\n##### wait() / notify()方法实现\n```java\npublic class ProCon1 {\n    private static final int FULL = 5;\n    private static String lock = \"lock\";\n    private static Integer count = 0;\n\n    class Producer implements Runnable {\n\n        @Override\n        public void run() {\n            while(true){\n                try {\n                    Thread.sleep(200);\n                } catch (InterruptedException e) {\n                    e.printStackTrace();\n                }\n                synchronized (lock) {\n                    while (count >= FULL){\n                        try {\n                            lock.wait();\n                        } catch (InterruptedException e) {\n                            e.printStackTrace();\n                        }\n                    }\n                    count+=2;\n                    System.out.println(Thread.currentThread().getName()+\" producer:: \"+count);\n                    lock.notifyAll();\n                }\n            }\n        }\n    }\n\n    class Consumer implements Runnable{\n\n        @Override\n        public void run() {\n            while(true){\n                try {\n                    Thread.sleep(500);\n                } catch (InterruptedException e) {\n                    e.printStackTrace();\n                }\n                synchronized (lock){\n                    while(count == 0){\n                        try {\n                            lock.wait();\n                        } catch (InterruptedException e) {\n                            e.printStackTrace();\n                        }\n                    }\n                    count--;\n                    System.out.println(Thread.currentThread().getName()+\" consumer:: \"+ count);\n                    lock.notifyAll();\n                }\n            }\n        }\n    }\n\n    public static void main(String[] args) {\n        ProCon1 t = new ProCon1();\n        new Thread(t.new Producer()).start();\n        new Thread(t.new Consumer()).start();\n        new Thread(t.new Consumer()).start();\n        new Thread(t.new Consumer()).start();\n    }\n}\n```\n##### await() / signal()方法\n```java\npublic class ProCon2 {\n  private static final int FULL = 5;\n  private static Integer count = 0;\n\n  private final Lock lock = new ReentrantLock();\n  final Condition put = lock.newCondition();\n  final Condition get = lock.newCondition();\n\n\n  class Producer implements Runnable {\n\n    @Override\n    public void run() {\n      for (int i = 0; i < FULL; i++) {\n        try {\n          Thread.sleep(1000);\n        } catch (InterruptedException e) {\n          e.printStackTrace();\n        }\n        lock.lock();\n        try {\n          while (count == FULL){\n            try {\n              put.wait();\n            } catch (InterruptedException e) {\n              e.printStackTrace();\n            }\n          }\n          count++;\n          System.out.println(Thread.currentThread().getName()+\" producer:: \"+count);\n          get.signal();\n        }finally {\n          lock.unlock();\n        }\n      }\n    }\n  }\n\n  class Consumer implements Runnable{\n\n    @Override\n    public void run() {\n      for (int i = 0; i < FULL; i++) {\n        try {\n          Thread.sleep(1000);\n        } catch (InterruptedException e) {\n          e.printStackTrace();\n        }\n        lock.lock();\n        try {\n          while(count == 0){\n            try {\n              get.wait();\n            } catch (InterruptedException e) {\n              e.printStackTrace();\n            }\n          }\n          count--;\n          System.out.println(Thread.currentThread().getName()+\" consumer:: \"+ count);\n          put.signal();\n        }finally {\n          lock.unlock();\n        }\n      }\n    }\n  }\n\n  public static void main(String[] args) {\n    ProCon2 t = new ProCon2();\n    new Thread(t.new Producer()).start();\n    new Thread(t.new Consumer()).start();\n  }\n}\n```\n##### BlockingQueue阻塞队列方法\n```java\npublic class ProCon3 {\n    private static Integer count = 0;\n    final BlockingQueue<Integer> bq = new ArrayBlockingQueue<Integer>(5);\n\n    public static void main(String[] args)  {\n        ProCon3 t = new ProCon3();\n        new Thread(t.new Producer()).start();\n        new Thread(t.new Consumer()).start();\n        new Thread(t.new Consumer()).start();\n        new Thread(t.new Producer()).start();\n    }\n    class Producer implements Runnable {\n        @Override\n        public void run() {\n            for (int i = 0; i < 5; i++) {\n                try {\n                    Thread.sleep(1000);\n                } catch (Exception e) {\n                    e.printStackTrace();\n                }\n                try {\n                    bq.put(1);\n                    count++;\n                    System.out.println(Thread.currentThread().getName() + \"produce:: \" + count);\n                } catch (InterruptedException e) {\n                    // TODO Auto-generated catch block\n                    e.printStackTrace();\n                }\n            }\n        }\n    }\n    class Consumer implements Runnable {\n\n        @Override\n        public void run() {\n            for (int i = 0; i < 5; i++) {\n                try {\n                    Thread.sleep(1000);\n                } catch (InterruptedException e1) {\n                    e1.printStackTrace();\n                }\n                try {\n                    bq.take();\n                    count--;\n                    System.out.println(Thread.currentThread().getName()+ \"consume:: \" + count);\n                } catch (Exception e) {\n                    // TODO: handle exception\n                    e.printStackTrace();\n                }\n            }\n        }\n    }\n}\n```\n##### Semaphore方法\n```java\npublic class ProCon4 {\n    int count = 0;\n    final Semaphore put = new Semaphore(5);//初始令牌个数\n    final Semaphore get = new Semaphore(0);\n    final Semaphore mutex = new Semaphore(1);\n\n\n    public static void main(String[] args) {\n        ProCon4 t = new ProCon4();\n        new Thread(t.new Producer()).start();\n        new Thread(t.new Consumer()).start();\n        new Thread(t.new Consumer()).start();\n        new Thread(t.new Producer()).start();\n    }\n\n    class Producer implements Runnable {\n        @Override\n        public void run() {\n            for (int i = 0; i < 5; i++) {\n                try {\n                    Thread.sleep(1000);\n                } catch (Exception e) {\n                    e.printStackTrace();\n                }\n                try {\n                    put.acquire();//注意顺序\n                    mutex.acquire();\n                    count++;\n                    System.out.println(Thread.currentThread().getName() + \"produce:: \" + count);\n                } catch (Exception e) {\n                    e.printStackTrace();\n                } finally {\n                    mutex.release();\n                    get.release();\n                }\n\n            }\n        }\n    }\n\n    class Consumer implements Runnable {\n\n        @Override\n        public void run() {\n            for (int i = 0; i < 5; i++) {\n                try {\n                    Thread.sleep(1000);\n                } catch (InterruptedException e1) {\n                    e1.printStackTrace();\n                }\n                try {\n                    get.acquire();//注意顺序\n                    mutex.acquire();\n                    count--;\n                    System.out.println(Thread.currentThread().getName() + \"consume:: \" + count);\n                } catch (Exception e) {\n                    e.printStackTrace();\n                } finally {\n                    mutex.release();\n                    put.release();\n                }\n            }\n        }\n    }\n}\n```\n# 线程和进程的区别\n## 进程\n比如Linux上我们输入一个top命令，展示出来的的就是一个个进程，比如我们运行的一个鉴权服务就是一个进程\n## 线程\n进程中的一个执行任务（控制单元），负责当前进程中程序的执行。一个进程至少有一个线程，一个进程可以运行多个线程，多个线程可共享数据。\n## 总结\n- `根本区别：`进程是操作系统资源分配的基本单位，而线程是处理器任务调度和执行的基本单位\n- `资源开销：`每个进程都有独立的代码和数据空间（程序上下文），程序之间的切换会有较大的开销；线程可以看做轻量级的进程，同一类线程共享代码和数据空间，每个线程都有自己独立的运行栈和程序计数器（PC），线程之间切换的开销小。\n- `包含关系：`如果一个进程内有多个线程，则执行过程不是一条线的，而是多条线（线程）共同完成的；线程是进程的一部分，所以线程也被称为轻权进程或者轻量级进程。\n- `内存分配：`同一进程的线程共享本进程的地址空间和资源，而进程之间的地址空间和资源是相互独立的\n- `影响关系：`一个进程崩溃后，在保护模式下不会对其他进程产生影响，但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。\n- `执行过程：`每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行，必须依存在应用程序中，由应用程序提供多个线程执行控制，两者均可并发执行\n\n# 怎么让多个线程有序执行\n## join方法\n```java\npublic class ThreadTest1 {\n    public static void main(String[] args) {\n        Thread t1 = new Thread(new Work(null));\n        Thread t2 = new Thread(new Work(t1));\n        Thread t3 = new Thread(new Work(t2));\n        t1.setName(\"第一个\");\n        t2.setName(\"第二个\");\n        t3.setName(\"第三个\");\n        t1.start();\n        t2.start();\n        t3.start();\n    }\n    static class Work implements Runnable {\n        private Thread thread;\n        public Work(Thread thread) {\n            this.thread = thread;\n        }\n        @Override\n        public void run() {\n            if (thread != null) {\n                try {\n                    thread.join();\n                } catch (InterruptedException e) {\n                    e.printStackTrace();\n                }\n            }\n            execute();\n        }\n        private void execute() {\n            System.out.println(\"线程\" + Thread.currentThread().getName() + \"执行\");\n        }\n    }\n}\n\n```\n\n## lock-condition\n```java\npublic class Test {\n    Lock lock = new ReentrantLock();\n    Condition condition1 = lock.newCondition();\n    Condition condition2 = lock.newCondition();\n    Condition condition3 = lock.newCondition();\n\n    private int flag = 1;\n\n    public void printA() throws InterruptedException {\n        lock.lock();\n        try {\n            while (flag != 1) {\n                condition1.await();\n            }\n            System.out.println(\"A working\");\n            flag = 2;\n            condition2.signal();\n        } finally {\n            lock.unlock();\n        }\n\n    }\n\n    public void printB() throws InterruptedException {\n        lock.lock();\n        try {\n            while (flag != 2) {\n                condition2.await();\n            }\n            System.out.println(\"B working\");\n            flag = 3;\n            condition3.signal();\n        } finally {\n            lock.unlock();\n        }\n\n    }\n\n    public void printC() throws InterruptedException {\n        lock.lock();\n        try {\n            while (flag != 3) {\n                condition3.await();\n            }\n            System.out.println(\"C working\");\n            flag = 1;\n            condition1.signal();\n        } finally {\n            lock.unlock();\n        }\n\n    }\n\n    public static void main(String[] args) {\n        Test test = new Test();\n\n        new Thread(() -> {\n            try {\n                test.printA();\n            } catch (InterruptedException e) {\n                e.printStackTrace();\n            }\n        }).start();\n\n        new Thread(() -> {\n            try {\n                test.printB();\n            } catch (InterruptedException e) {\n                e.printStackTrace();\n            }\n        }).start();\n\n        new Thread(() -> {\n            try {\n                test.printC();\n            } catch (InterruptedException e) {\n                e.printStackTrace();\n            }\n        }).start();\n    }\n}\n```\n# Java线程和操作系统的线程区别\n## Java线程在操作系统上本质\nJDK1.2之前，**程序员们为JVM开发了自己的一个线程调度内核，而到操作系统层面就是用户空间内的线程实现**。而到了JDK1.2及以后，JVM选择了更加稳健且方便使用的操作系统原生的线程模型，通过系统调用，将程序的线程交给了操作系统内核进行调度\n\n也就是说，**现在的Java中线程的本质，其实就是操作系统中的线程**，Linux下是基于pthread库实现的轻量级进程，Windows下是原生的系统Win32 API提供系统调用从而实现多线程\n\n## 操作系统中的进程（线程）状态\n![](../img/Linux/线程状态.png)\n\n这里需要着重解释一点，在现在的操作系统中，因为线程依旧被视为轻量级进程，所以操作系统中线程的状态实际上和进程状态是一致的模型。\n## 操作系统中线程和Java线程状态的关系\n从实际意义上来讲，操作系统中的线程除去new和terminated状态，一个线程真实存在的状态，只有：\n\n- ready：表示线程已经被创建，正在等待系统调度分配CPU使用权。\n- running：表示线程获得了CPU使用权，正在进行运算\n- waiting：表示线程等待（或者说挂起），让出CPU资源给其他线程使用\n\n为什么除去new和terminated状态？是因为这两种状态实际上并不存在于线程运行中，所以也没什么实际讨论的意义。\n\n对于Java中的线程状态：\n\n无论是Timed Waiting ，Waiting还是Blocked，对应的都是操作系统线程的**waiting（等待**）状态。 而Runnable状态，则对应了操作系统中的ready和running状态。\n\n## 我们在java中每起一个线程，操作系统会给其分配哪些资源\n在Java中启动一个新线程时，操作系统会为该线程分配一些资源，包括：\n\n1. 线程的执行堆栈：每个线程都有自己的执行堆栈，用于存储局部变量、方法调用和返回地址等信息。\n2. 线程的调度信息：包括线程的优先级、状态等。\n3. 线程的上下文信息：包括程序计数器、CPU寄存器和其他与CPU相关的状态。\n4. 线程的内存空间：每个线程都有自己的内存空间，用于存储线程相关的对象和数据。\n\n此外，操作系统还会为线程分配一定的CPU时间片，用于执行线程中的代码。当线程的时间片用完后，操作系统会将其切换出CPU，切换到其他线程执行。\n\n# 参考文章\n- https://blog.csdn.net/CringKong/article/details/79994511\n- https://developer.aliyun.com/article/52846"
  },
  {
    "path": "Java-多线程/线程池.md",
    "content": "# 创建线程池的方式\n### 1、Executors\n#### newCachedThreadPool\n创建一个可缓存的线程池，若线程数超过处理所需，缓存一段时间后会回收，若线程数不够，则新建线程\n`Executors.newCachedThreadPool();`\n#### newFixedThreadPool\n创建一个固定大小的线程池，可控制并发的线程数，超出的线程会在队列中等待\n`Executors.newFixedThreadPool(3)`\n#### newScheduledThreadPool\n创建一个周期性的线程池，支持定时及周期性执行任务\n`Executors.newScheduledThreadPool(3);`\n#### newSingleThreadExecutor\n创建一个单线程的线程池，可保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行\n`Executors.newSingleThreadExecutor();`\n### 2、ThreadPoolExecutor\n# ThreadPoolExecutor\n## 参数含义\n#### 1、corePoolSize\n核心线程数，线程池中始终存活的线程数\n#### 2、maximumPoolSize\n最大线程数，线程池中允许的最大线程数\n#### 3、keepAliveTime\n存活时间，线程没有任务执行时最多保持多久时间会终止\n#### 4、unit\n单位，参数keepAliveTime的时间单位\n#### 5、workQueue\n一个阻塞队列，用来存储等待执行的任务\n- `ArrayBlockingQueue`\t一个由数组结构组成的有界阻塞队列。\n- `LinkedBlockingQueue`\t一个由链表结构组成的有界阻塞队列。\n- `SynchronousQueue`\t一个不存储元素的阻塞队列，即直接提交给线程不保持它们。\n- `PriorityBlockingQueue`\t一个支持优先级排序的无界阻塞队列。\n- `DelayQueue`\t一个使用优先级队列实现的无界阻塞队列，只有在延迟期满时才能从中提取元素。\n- `LinkedTransferQueue`\t一个由链表结构组成的无界阻塞队列。与SynchronousQueue类似，还含有非阻塞方法。\n#### 6、threadFactory\n线程工厂，主要用来创建线程，默及正常优先级、非守护线程\n#### 7、handler\n拒绝策略，拒绝处理任务时的策略，4种可选，默认为AbortPolicy\n- `AbortPolicy`\t`中止策略` 拒绝并抛出异常。\n  - 默认\n- `CallerRunsPolicy`\t`调用者运行策略` 当触发拒绝策略时，只要线程池没有关闭，就由提交任务的当前线程处理。 \n  - 一般在不允许失败的、对性能要求不高、并发量较小的场景下使用，因为线程池一般情况下不会关闭，也就是提交的任务一定会被运行，但是由于是调用者线程自己执行的，当多次提交任务时，就会阻塞后续任务执行，性能和效率自然就慢了\n- `DiscardOldestPolicy`\t`弃老策略` 抛弃队列头部（最旧）的一个任务，并执行当前任务。\n  - 这个策略还是会丢弃任务，丢弃时也是毫无声息，但是特点是丢弃的是老的未执行的任务，而且是待执行优先级较高的任务。基于这个特性，我能想到的场景就是，发布消息，和修改消息，当消息发布出去后，还未执行，此时更新的消息又来了，这个时候未执行的消息的版本比现在提交的消息版本要低就可以被丢弃了。因为队列中还有可能存在消息版本更低的消息会排队执行，所以在真正处理消息的时候一定要做好消息的版本比较。\n- `DiscardPolicy`\t`丢弃策略`。 如果你提交的任务无关紧要，你就可以使用它 。因为它就是个空实现，会悄无声息的吞噬你的的任务。所以这个策略基本上不用了\n# ThreadPoolExecutor原理流程\n![](../img/Java多线程/ThreadPoolExecutor原理流程.png)\n- 如果当前运行的线程少于 corePoolSize，则会创建新的线程来执行新的任务；\n- 如果运行的线程个数等于或者大于 corePoolSize，则会将提交的任务存放到阻塞队列 workQueue 中；\n- 如果当前 workQueue 队列已满的话，则会创建新的线程来执行任务；\n- 如果线程个数已经超过了 maximumPoolSize，则会使用饱和策略 RejectedExecutionHandler 来进行处理\n### private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));\n记录线程池中Worker工作线程数量和线程池的状态 int类型是32位，它的高3位，表示线程池的状态，低29位表示Worker的数量\n### addWorker方法\n```java\nprivate boolean addWorker(Runnable firstTask, boolean core) {\n    //利用死循环和CAS函数，实现乐观锁，来实现多线程改变ct1值的并发问题\n    // 因为ctl值代表两个东西，工作线程数量和线程池状态。\n    //这里就用了两个for循环，一个是线程池状态的for循环，一个是工作线程数量的for循环\n    retry:\n    for (;;) {\n        int c = ctl.get();\n        //获取线程池运行状态rs,\n        int rs = runStateOf(c);\n\n        //首先判断线程池状态和任务队列状态，\n        //来判断能否创建新的工作线程\n        if (rs >= SHUTDOWN &&\n            ! (rs == SHUTDOWN &&\n               firstTask == null &&\n               ! workQueue.isEmpty()))\n            return false;\n\n        for (;;) {\n            //线程池中工作线程数量wc\n            int wc = workerCountOf(c);\n            //当线程池工作线程数量wc大于线程上限CAPACITY,\n            //或者用户规定核心池数量corePoolSize或用户规定最大线程池数量maximumPoolSize\n            //表示不能创建工作线程了，所以返回false\n            if (wc >= CAPACITY ||\n                wc >= (core ? corePoolSize : maximumPoolSize))\n                return false;\n            //使用CAS函数，使工作线程数量wc加一\n            if (compareAndIncrementWorkerCount(c))\n                break retry;\n            //来到这里表示CAS函数失败，那么就要循环重新判断\n            //但是c还代表线程状态，如果线程状态改变，那么就必须跳转到retry循环\n            c = ctl.get();  // Re-read ctl\n            if (runStateOf(c) != rs)\n                continue retry;\n            // else CAS failed due to workerCount change; retry inner loop\n        }\n    }\n    //工作线程是否开始，即调用了线程的start方法\n    boolean workerStarted = false;\n    //工作线程是否添加到工作线程队列workers中\n    boolean workerAdded = false;\n    Worker w = null;\n    try {\n        //创建一个Worker对象\n        w = new Worker(firstTask);\n        //得到Worker所拥有的线程thread\n        final Thread t = w.thread;\n        if (t != null) {\n            final ReentrantLock mainLock = this.mainLock;\n            // 并发锁\n            mainLock.lock();\n            try {\n                // Recheck while holding lock.\n                // Back out on ThreadFactory failure or if\n                // shut down before lock acquired.\n                //获取线程池运行状态rs\n                int rs = runStateOf(ctl.get());\n                //当线程池是运行状态，或者是SHUTDOWN状态但firstTask为null,\n                if (rs < SHUTDOWN ||\n                    (rs == SHUTDOWN && firstTask == null)) {\n                    //如果线程t已经被开启，就抛出异常\n                    if (t.isAlive()) // precheck that t is startable\n                        throw new IllegalThreadStateException();\n                    //将w添加到工作线程集合workers中\n                    workers.add(w);\n                    //获取工作线程集合workers的个数\n                    int s = workers.size();\n                    //记录线程池历史最大的工作线程个数\n                    if (s > largestPoolSize)\n                        largestPoolSize = s;\n                    workerAdded = true;\n                }\n            } finally {\n                mainLock.unlock();\n            }\n            //如果已经添加到工作线程队列中，那么开启线程\n            if (workerAdded) {\n                t.start();\n                workerStarted = true;\n            }\n        }\n    } finally {\n        // 如果开启 工作线程失败，那么这个任务也就没有执行\n        //因此移除这个任务w(如果队列中有)，减少工作线程数量，因为这个数量在之前已经增加了\n        if (! workerStarted)\n            addWorkerFailed(w);\n    }\n    return workerStarted;\n}\n```\n\n首先for循环判断线程池状态，非SHUTDOWN才可以添加线程，不能大于最大线程数，CAS增加线程数量，添加后传递runnable对象新建worker，加锁判断线程池状态，添加worker到hashset workers集合里，添加成功后启动线程start方法\n# 如何释放线程\nWorker里定义了释放线程的方法，线程池封装线程为Worker，在Worker里定义了run方法\n```java\npublic void run() {\n    runWorker(this);\n}\n```\n```java\nfinal void runWorker(Worker w) {\n    Thread wt = Thread.currentThread();\n    Runnable task = w.firstTask;\n    w.firstTask = null;\n    w.unlock(); // allow interrupts\n    boolean completedAbruptly = true;\n    try {\n        while (task != null || (task = getTask()) != null) {\n            w.lock();\n            // If pool is stopping, ensure thread is interrupted;\n            // if not, ensure thread is not interrupted.  This\n            // requires a recheck in second case to deal with\n            // shutdownNow race while clearing interrupt\n            if ((runStateAtLeast(ctl.get(), STOP) ||\n                 (Thread.interrupted() &&\n                  runStateAtLeast(ctl.get(), STOP))) &&\n                !wt.isInterrupted())\n                wt.interrupt();\n            try {\n                beforeExecute(wt, task);\n                Throwable thrown = null;\n                try {\n                    task.run();\n                } catch (RuntimeException x) {\n                    thrown = x; throw x;\n                } catch (Error x) {\n                    thrown = x; throw x;\n                } catch (Throwable x) {\n                    thrown = x; throw new Error(x);\n                } finally {\n                    afterExecute(task, thrown);\n                }\n            } finally {\n                task = null;\n                w.completedTasks++;\n                w.unlock();\n            }\n        }\n        completedAbruptly = false;\n    } finally {\n        processWorkerExit(w, completedAbruptly);\n    }\n}\n```\n一旦跳出while循环，即进入到processWorkExit方法，这就是回收Worker,在这里先看下getTask()方法，该方法可以避免移除核心线程\n```java\nprivate Runnable getTask() {\n    boolean timedOut = false; // Did the last poll() time out?\n\n    for (;;) {\n        int c = ctl.get();\n        int rs = runStateOf(c);\n\n        // 判断线程池的运行状态\n        // 如果线程池已经开始关闭或者已经处于关闭，任务队列为空时 worker 数 -1\n        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {\n            // 循环 + CAS\n            decrementWorkerCount();\n            return null;\n        }\n\n        int wc = workerCountOf(c);\n\n        // 判断是否需要超时限制\n        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;\n\n        // 当线程获取任务超时，修改 worker 数\n        if ((wc > maximumPoolSize || (timed && timedOut))\n            && (wc > 1 || workQueue.isEmpty())) {\n            // CAS 修改\n            if (compareAndDecrementWorkerCount(c))\n                return null;\n            // CAS 修改失败后循环重试\n            continue;\n        }\n\n        try {\n            // 超时时间为设置的 keepAliveTime 值\n            Runnable r = timed ?\n                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :\n                workQueue.take();\n            if (r != null)\n                return r;\n            timedOut = true;\n        } catch (InterruptedException retry) {\n            timedOut = false;\n        }\n    }\n}\n\n```\n```java\nprivate void processWorkerExit(Worker w, boolean completedAbruptly) {\n    // 如果是意外退出任务处理流程 将 worker 数 -1  \n  if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted\n      decrementWorkerCount();\n\n  final ReentrantLock mainLock = this.mainLock;\n  mainLock.lock();\n  try {\n      // 加锁实现完成任务数的统计及队列移除\n      completedTaskCount += w.completedTasks;\n      //从workers-hashset 里remove工作线程\n      workers.remove(w);\n  } finally {\n      mainLock.unlock();\n  }\n\n  tryTerminate();\n\n  int c = ctl.get();\n  if (runStateLessThan(c, STOP)) {\n      if (!completedAbruptly) {\n          // 如果线程池还处于运行中，并且线程是正常退出 则判断是否需要补充新 worker\n          int min = allowCoreThreadTimeOut ? 0 : corePoolSize;\n          if (min == 0 && ! workQueue.isEmpty())\n              min = 1;\n          if (workerCountOf(c) >= min)\n              return; // replacement not needed\n      }\n      addWorker(null, false);\n  }\n}\n```\n# 如何设置线程数\n#### 没有固定的计算方式，需要以业务来衡量\n#### 市面上说的其实都不太准确\n- CPU 密集型任务  Ncpu+1\n- IO 密集型任务   2*Ncpu\n#### （（线程等待时间+线程CPU时间）/线程CPU时间 ）* CPU数目\n线程等待时间所占比例越高，需要越多线程。线程CPU时间所占比例越高，需要越少线程。\n## 线程池被创建后里面有线程吗？如果没有的话，你知道有什么方法对线程池进行预热吗？\n线程池被创建后如果没有任务过来，里面是不会有线程的。如果需要预热的话可以调用下面的两个方法\n- 全部启动： `prestartAllCoreThreads()`\n- 仅启动一个 `prestartCoreThread()`\n\n## 线上机器突然重启/宕机，线程池里的阻塞队列中任务怎么办？\n- 这种情况内存中的任务肯定会丢失\n- 关键在于保存任务的执行状态：未提交、已提交、已完成，将状态持久化到db中，服务恢复后通过判断任务的状态来继续执行任务\n\n## 运行时能修改线程数么？\n`setCorePoolSize`\n\nJDK允许线程池使用方通过ThreadPoolExecutor的实例来动态设置线程池的核心策略，以setCorePoolSize为方法例，在运行期线程池使用方调用此方法设置corePoolSize之后，线程池会直接覆盖原来的corePoolSize值，并且基于当前值和原始值的比较结果采取不同的处理策略。对于当前值小于当前工作线程数的情况，说明有多余的worker线程，此时会向当前idle的worker线程发起中断请求以实现回收，多余的worker在下次idle的时候也会被回收；对于当前值大于原始值且当前队列中有待执行任务，则线程池会创建新的worker线程来执行队列任务\n\n# 参考文章\n\n- https://blog.csdn.net/ksws01/article/details/110845897\n"
  },
  {
    "path": "Java-多线程/锁机制.md",
    "content": "# Synchronized\n### 原理\n### Synchronized的语义底层是通过一个monitor的对象来完成\n### 同步一个代码块\n- 它只作用于同一个对象，如果调用两个对象上的同步代码块，就不会进行同步\n- 同步代码块原理\n  - 具体实现是在编译之后在同步方法调用前加入一个 monitorenter 指令，在退出方法和异常处插入 monitorexit 的指令\n    - `monitorenter` 每个对象都是一个监视器锁（monitor）。当monitor被占用时就会处于锁定状态，线程执行monitorenter指令时尝试获取monitor的所有权，过程如下\n      - 如果monitor的进入数为0，则该线程进入monitor，然后将进入数设置为1，该线程即为monitor的所有者；\n      - 如果线程已经占有该monitor，只是重新进入，则进入monitor的进入数加1；\n      - 如果其他线程已经占用了monitor，则该线程进入阻塞状态，直到monitor的进入数为0，再重新尝试获取monitor的所有权；\n    - `monitorexit` 执行monitorexit的线程必须是objecterf所对应的monitor的所有者。指令执行时，monitor的进入数减1，如果减1后进入数为0，那线程退出monitor，不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。\n### 同步一个方法\n它和同步代码块一样，作用于同一个对象。\n![](../img/Java多线程/synchronized指令.png)\n- 方法的同步并没有通过指令 monitorenter 和 monitorexit 来完成（理论上其实也可以通过这两条指令来实现），不过相对于普通方法，其常量池中多了 ACC_SYNCHRONIZED 标示符\n- 当方法调用时，调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置，如果设置了，执行线程将先获取monitor，获取成功之后才能执行方法体，方法执行完后再释放monitor。在方法执行期间，其他任何线程都无法再获得同一个monitor对象。\n### 同步一个类\n作用于整个类，也就是说两个线程调用同一个类的不同对象上的这种同步语句，也会进行同步。\n### 同步一个静态方法\n作用于整个类。\n\nSynchronized是可重入锁\n# Lock\n## ReentrantLock\n可重入锁\n\n默认是非公平锁\n### 组件\n- `Sync` Sync继承自AQS实现了解锁tryRelease()方法\n- `NonfairSync` 继承自Sync，实现了获取锁的tryAcquire()方法\n\n# NonfairSync代码分析\n\n## lock()\n代码\n```java\n/**\n * 非公平锁的实现\n */\nstatic final class NonfairSync extends Sync {\n        /**\n         * 加锁 首先CAS尝试把state的状态从0置为1，成功则获得锁，否则执行 acquire(1)方法，为AQS中的方法\n         */\n        final void lock() {\n            //执行CAS操作，获取同步状态\n            if (compareAndSetState(0, 1))\n                //成功则将独占锁线程设置为当前线程\n                setExclusiveOwnerThread(Thread.currentThread());\n            else\n                //否则再次请求同步状态\n                acquire(1);\n        }\n    }\n```\n### acquire()\n```java\npublic final void acquire(int arg) {\n    //再次尝试获取同步状态\n    if (!tryAcquire(arg) &&\n        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))\n        selfInterrupt();\n}\n```\n- 这里传入参数arg表示要获取同步状态后设置的值(即要设置state的值)，因为要获取锁，而status为0时是释放锁，1则是获取锁，所以这里一般传递参数为1，进入方法后首先会执行tryAcquire(arg)方法，在前面分析过该方法在AQS中并没有具体实现，而是交由子类实现，因此该方法是由ReentrantLock类内部实现的\n\n### tryAcquire() & nonfairTryAcquire()\n```java\n//NonfairSync\nprotected final boolean tryAcquire(int acquires) {\n    return nonfairTryAcquire(acquires);\n}\n```\n```java\nabstract static class Sync extends AbstractQueuedSynchronizer {\n    final boolean nonfairTryAcquire(int acquires) {\n        final Thread current = Thread.currentThread();\n        int c = getState();\n        //判断同步状态是否为0，并尝试再次获取同步状态\n        if (c == 0) {\n            //执行CAS操作\n            if (compareAndSetState(0, acquires)) {\n                setExclusiveOwnerThread(current);\n                return true;\n            }\n        }\n        //如果当前线程已获取锁，属于重入锁，再次获取锁后将status值加1\n        else if (current == getExclusiveOwnerThread()) {\n            int nextc = c + acquires;\n            if (nextc < 0) // overflow\n                throw new Error(\"Maximum lock count exceeded\");\n            //设置当前同步状态，当前只有一个线程持有锁，因为不会发生线程安全问题，可以直接执行setState(nextc);\n            setState(nextc);\n            return true;\n        }\n        return false;\n    }\n}\n```\n从代码执行流程可以看出，这里做了两件事\n- 一是尝试再次获取同步状态，如果获取成功则将当前线程设置为OwnerThread，否则失败\n- 二是判断当前线程current是否为OwnerThread，如果是则属于重入锁，state自增1，并获取锁成功，返回true，反之失败，返回false，也就是tryAcquire(arg)执行失败，返回false。需要注意的是nonfairTryAcquire(int acquires)内部使用的是CAS原子性操作设置state值，可以保证state的更改是线程安全的，因此只要任意一个线程调用nonfairTryAcquire(int acquires)方法并设置成功即可获取锁，不管该线程是新到来的还是已在同步队列的线程，毕竟这是非公平锁，并不保证同步队列中的线程一定比新到来线程请求(可能是head结点刚释放同步状态然后新到来的线程恰好获取到同步状态)先获取到锁，这点跟后面还会讲到的公平锁不同\n\n### addWaiter() & enq()\n在acquire中，如果tryAcquire(arg)返回true，acquireQueued自然不会执行，这是最理想的，因为毕竟当前线程已获取到锁，如果tryAcquire(arg)返回false，则会执行addWaiter(Node.EXCLUSIVE)进行入队操作,由于ReentrantLock属于独占锁，因此结点类型为Node.EXCLUSIVE\n\naddWaiter\n```java\nprivate Node addWaiter(Node mode) {\n    //将请求同步状态失败的线程封装成结点\n    Node node = new Node(Thread.currentThread(), mode);\n    // Try the fast path of enq; backup to full enq on failure\n    Node pred = tail;\n    //如果是第一个结点加入肯定为空，跳过。\n    //如果非第一个结点则直接执行CAS入队操作，尝试在尾部快速添加\n    if (pred != null) {\n        node.prev = pred;\n        //使用CAS执行尾部结点替换，尝试在尾部快速添加\n        if (compareAndSetTail(pred, node)) {\n            pred.next = node;\n            return node;\n        }\n    }\n    //如果第一次加入或者CAS操作没有成功执行enq入队操作\n    enq(node);\n    return node;\n}\n```\n如果是第一个结点，则为tail肯定为空，那么将执行enq(node)操作\n\nenq\n```java\nprivate Node enq(final Node node) {\n    //死循环\n    for (;;) {\n        Node t = tail;\n        //如果队列为null，即没有头结点\n        if (t == null) { // Must initialize\n            //创建并使用CAS设置头结点\n            if (compareAndSetHead(new Node()))\n                tail = head;\n        } else {\n            //队尾添加新结点\n            node.prev = t;\n            if (compareAndSetTail(t, node)) {\n                t.next = node;\n                return t;\n            }\n        }\n    }\n}\n```\n这里做了两件事，一是如果还没有初始同步队列则创建新结点并使用compareAndSetHead设置头结点，tail也指向head，二是队列已存在，则将新结点node添加到队尾\n\n### acquireQueued() & setHead() & shouldParkAfterFailedAcquire()\n添加到同步队列后，结点就会进入一个自旋过程，即每个结点都在观察时机待条件满足获取同步状态，然后从同步队列退出并结束自旋，回到之前的acquire()方法，自旋过程是在acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法中执行的\n\nacquireQueued\n```java\nfinal boolean acquireQueued(final Node node, int arg) {\n    boolean failed = true;\n    try {\n        boolean interrupted = false;\n        //自旋，死循环\n        for (;;) {\n            //获取前驱结点\n            final Node p = node.predecessor();\n            //当且仅当p为头结点才尝试获取同步状态\n            if (p == head && tryAcquire(arg)) {\n                //将node设置为头结点\n                setHead(node);\n                //清空原来头结点的引用便于GC\n                p.next = null; // help GC\n                failed = false;\n                return interrupted;\n            }\n            //如果前驱结点不是head,判断是否挂起线程\n            if (shouldParkAfterFailedAcquire(p, node) &&\n                parkAndCheckInterrupt())\n                interrupted = true;\n        }\n    } finally {\n        if (failed)\n            //最终都没能获取同步状态，结束该线程的请求\n            cancelAcquire(node);\n    }\n}\n```\n当前线程在自旋(死循环)中获取同步状态，当且仅当前驱结点为头结点才尝试获取同步状态，这符合FIFO的规则，即先进先出，其次head是当前获取同步状态的线程结点，只有当head释放同步状态唤醒后继结点，后继结点才有可能获取到同步状态，因此后继结点在其前继结点为head时，才进行尝试获取同步状态，其他时刻将被挂起\n\nsetHead\n```java\n//设置头结点\nprivate void setHead(Node node) {\n    head = node;\n    //清空节点数据\n    node.thread = null;\n    node.prev = null;\n}\n```\n设置为node结点被设置为head后，其thread信息和前驱结点将被清空，因为该线程已获取到同步状态(锁)，正在执行了，也就没有必要存储相关信息了，head只有保存指向后继结点的指针即可，便于head结点释放同步状态后唤醒后继结点\n\n如果前驱结点不是head\n\nshouldParkAfterFailedAcquire\n```java\nprivate static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {\n    //获取当前结点的等待状态\n    int ws = pred.waitStatus;\n    //如果为等待唤醒(SIGNAL) 状态则返回true\n    if (ws == Node.SIGNAL)\n        return true;\n    //如果ws>0则说明是结束状态，\n    //遍历前驱结点直到找到没有结束状态的结点\n    if (ws > 0) {\n        do {\n            node.prev = pred = pred.prev;\n        } while (pred.waitStatus > 0);\n        pred.next = node;\n    } else {\n        //如果ws小于0又不是SIGNAL状态，\n        //则将其设置为SIGNAL状态，代表该结点的线程正在等待唤醒。\n        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);\n    }\n    return false;\n}\n```\nshouldParkAfterFailedAcquire()方法的作用是判断当前结点的前驱结点是否为SIGNAL状态(即等待唤醒状态)，如果是则返回true\n\n如果结点的ws为CANCELLED状态(值为1>0),即结束状态，则说明该前驱结点已没有用应该从同步队列移除，执行while循环，直到寻找到非CANCELLED状态的结点\n\n倘若前驱结点的ws值不为CANCELLED，也不为SIGNAL(当从Condition的条件等待队列转移到同步队列时，结点状态为CONDITION因此需要转换为SIGNAL)，那么将其转换为SIGNAL状态，等待被唤醒\n\n若shouldParkAfterFailedAcquire()方法返回true，即前驱结点为SIGNAL状态同时又不是head结点，那么使用parkAndCheckInterrupt()方法挂起当前线程，称为WAITING状态，需要等待一个unpark()操作来唤醒它\n\n### lock流程\n![](../img/Java多线程/lock流程.png)\n\n## unlock()\n```java\npublic void unlock() {\n    sync.release(1);\n}\n```\n### release() & tryRelease()\n```java\n//AQS类的release()方法\npublic final boolean release(int arg) {\n    //尝试释放锁\n    if (tryRelease(arg)) {\n        Node h = head;\n        if (h != null && h.waitStatus != 0)\n            //唤醒后继结点的线程\n            unparkSuccessor(h);\n        return true;\n    }\n    return false;\n}\n```\n```java\n//ReentrantLock类中的内部类Sync实现的tryRelease\nprotected final boolean tryRelease(int releases) {\n    int c = getState() - releases;\n    if (Thread.currentThread() != getExclusiveOwnerThread())\n        throw new IllegalMonitorStateException();\n    boolean free = false;\n    //判断状态是否为0，如果是则说明已释放同步状态\n    if (c == 0) {\n        free = true;\n        //设置Owner为null\n        setExclusiveOwnerThread(null);\n    }\n    //设置更新同步状态\n    setState(c);\n    return free;\n}\n```\n### unparkSuccessor()\n```java\nprivate void unparkSuccessor(Node node) {\n    //这里，node一般为当前线程所在的结点\n    int ws = node.waitStatus;\n    if (ws < 0)\n        //置零当前线程所在的结点状态，允许失败\n        compareAndSetWaitStatus(node, ws, 0);\n\n    //找到下一个需要唤醒的结点s\n    Node s = node.next;\n    //如果为空或已取消\n    if (s == null || s.waitStatus > 0) {\n        s = null;\n        for (Node t = tail; t != null && t != node; t = t.prev)\n            //从这里可以看出，<=0的结点，都还是有效的结点\n            if (t.waitStatus <= 0)\n                s = t;\n    }\n    if (s != null)\n        //唤醒\n        LockSupport.unpark(s.thread);\n}\n``` \n\n## FairSync\n继承自Sync，实现了获取锁的tryAcquire()方法\n\n### tryAcquire()\n不同点\n```java\n//公平锁FairSync\nprotected final boolean tryAcquire(int acquires) {\n    final Thread current = Thread.currentThread();\n    int c = getState();\n    if (c == 0) {\n        //主要，这里先判断同步队列是否存在结点\n        if (!hasQueuedPredecessors() &&\n            compareAndSetState(0, acquires)) {\n            setExclusiveOwnerThread(current);\n            return true;\n        }\n    }\n    else if (current == getExclusiveOwnerThread()) {\n        int nextc = c + acquires;\n        if (nextc < 0)\n            throw new Error(\"Maximum lock count exceeded\");\n        setState(nextc);\n        return true;\n    }\n    return false;\n}\n```\n### hasQueuedPredecessors()\n该方法与nonfairTryAcquire(int acquires)方法唯一的不同是在使用CAS设置尝试设置state值前，调用了hasQueuedPredecessors()判断同步队列是否存在结点，如果存在必须先执行完同步队列中结点的线程，当前线程进入等待状态。这就是非公平锁与公平锁最大的区别，即公平锁在线程请求到来时先会判断同步队列是否存在结点，如果存在先执行同步队列中的结点线程，当前线程将封装成node加入同步队列等待。而非公平锁呢，当线程请求到来时，不管同步队列是否存在线程结点，直接尝试获取同步状态，获取成功直接访问共享资源，但请注意在绝大多数情况下，非公平锁才是我们理想的选择，毕竟从效率上来说非公平锁总是胜于公平锁。\n\n# synchronized锁优化\n### 自旋锁\n#### 循环\n- 循环（自旋）一段时间，如果在这段时间内能获得锁，就可以避免进入阻塞状态\n- 需要进行忙循环操作占用 CPU 时间，它只适用于共享数据的锁定状态很短的场景。\n#### 在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数不再固定了，而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。\n### 锁消除\n为了保证数据的完整性，在进行操作时需要对这部分操作进行同步控制，但是在有些情况下，JVM检测到不可能存在共享数据竞争，这是JVM会对这些同步锁进行锁消除\n- 用vector举例\n- StringBuffer\n### 锁粗化\n- 如果一系列的连续操作都对同一个对象反复加锁和解锁，频繁的加锁操作就会导致性能损耗。\n- 上一节的示例代码中连续的 append() 方法就属于这类情况。如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁，将会把加锁的范围扩展（粗化）到整个操作序列的外部。对于上一节的示例代码就是扩展到第一个append() 操作之前直至最后一个 append() 操作之后，这样只需要加锁一次就可以了。\n  - vector每次add的时候都需要加锁操作，JVM检测到对同一个对象（vector）连续加锁、解锁操作，会合并一个更大范围的加锁、解锁操作，即加锁解锁操作会移到for循环之外。\n### 锁升级\n- 偏向锁\n  - 在大多数情况下，锁不仅不存在多线程竞争，而且总是由同一线程多次获得，为了让线程获得锁的代价更低，引进了偏向锁\n    - 当一个线程访问同步块并获取锁时，会在对象头和栈帧中的锁记录里储存锁偏向的线程ID，再次进入和退出同步块时不需要CAS来加锁或解锁，只需简单测试下对象头里是否储存着锁偏向的线程ID，如果成功则获得锁，失败则查看是否是偏向锁，是，尝试把偏向的线程ID改为自己，否，CAS竞争锁\n  - 偏向锁是在单线程执行代码块时使用的机制，如果在多线程并发的环境下（即线程A尚未执行完同步代码块，线程B发起了申请锁的申请），则一定会转化为轻量级锁或者重量级锁\n    - 锁的撤销\n    - 出现多线程竞争\n- 轻量级锁\n  - 首先会在当前线程的栈帧中创建用于储存锁记录的空间Lock Record，并把对象头里的mark word复制到锁记录中，然后线程尝试用CAS把对象头里的mark word替换为指向锁记录Lock Record的指针，并将Lock Record里的owner指针指向对象的Mark Word，成功，则获取锁，否则自旋来获取锁，若自旋超过一定次数，或者一个线程在持有锁，一个在自旋，又有第三个来访时，轻量级锁膨胀为重量级锁\n- 重量级锁\n  - 所有未获取到锁的线程都阻塞而不是自旋\n# 死锁\n### 什么是死锁\n是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去\n#### 产生死锁的原因\n1. 因为系统资源不足。\n2. 进程运行推进顺序不合适。\n3. 资源分配不当等。\n如果系统资源充足，进程的资源请求都能够得到满足，死锁出现的可能性就很低，否则就会因争夺有限的资源而陷入死锁。其次，进程运行推进顺序与速度不同，也可能产生死锁。\n#### 死锁的必要条件\n1. 互斥条件：一个资源每次只能被一个进程使用。\n2. 请求与保持条件：一个进程因请求资源而阻塞时，对已获得的资源保持不放。\n3. 不剥夺条件:进程已获得的资源，在末使用完之前，不能强行剥夺。\n4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。\n\n### 死锁代码举例\n```java\npublic class DeadLock {\n    //创建两个对象，用两个线程分别先后独占\n    private Boolean flag1 = true;\n    private Boolean flag2 = false;\n\n    public static void main(String[] args) {\n        DeadLock deadLock = new DeadLock();\n\n        new Thread(new Runnable() {\n            @Override\n            public void run() {\n                System.out.println(\"线程1开始，作用是当flag1 = true 时，将flag2也改为 true\");\n                synchronized (deadLock.flag1){\n                    if(deadLock.flag1){\n                        try{\n                            //睡眠1s ,模拟业务执行耗时，并保证两个线程进入死锁状态\n                            Thread.sleep(1000);\n                        }catch (InterruptedException e){\n                            e.printStackTrace();\n                        }\n                        System.out.println(\"flag1 = true,准备锁住flag2...\");\n                        synchronized (deadLock.flag2){\n                            deadLock.flag2 = true;\n                        }\n                    }\n                }\n            }\n        }).start();\n\n        new Thread(new Runnable() {\n            @Override\n            public void run() {\n                System.out.println(\"线程2开始，作用是当flag2 = false 时，将flag1也改为 false\");\n                synchronized (deadLock.flag2){\n                    if(!deadLock.flag2){\n                        try{\n                            //睡眠1s ,模拟业务执行耗时，并保证两个线程进入死锁状态\n                            Thread.sleep(1000);\n                        }catch (InterruptedException e){\n                            e.printStackTrace();\n                        }\n                        System.out.println(\"flag2 = false,准备锁住flag1...\");\n                        synchronized (deadLock.flag1){\n                            deadLock.flag1 = false;\n                        }\n\n                    }\n                }\n            }\n        }).start();\n    }\n}\n```\n以上代码，可以用一个死锁的图解释。线程1独占对象1，想要访问对象2，而对象2此时已经独占对象2，在等待对象1的资源释放，此时线程1因无法获取到对象2而无法向下执行，因此没法释放对象1，线程2同理，造成了死锁状态，两个线程都阻塞在等待资源处\n\n### 如何预防线程死锁\n#### 预防\n死锁的预防基本思想打破产生死锁的四个必要条件中的一个或几个，保证系统不会进入死锁状态。\n- 比如\n  - 打破互斥条件：允许进程同时访问某些资源\n  - 打破不剥夺条件：允许进程从占有者占有的资源中强行剥夺一些资源\n  - 打破请求与保持条件：进程在运行前一次性地向系统申请它所需要的全部资源\n  - 打破循环等待条件：实行资源有序分配策略\n#### 避免\n- 加锁顺序（线程按照一定的顺序加锁）\n- 加锁时限（线程尝试获取锁的时候加上一定的时限，超过时限则放弃对该锁的请求，并释放自己占有的锁）\n- 死锁检测\n\n### 怎么判断JVM里是否出现死锁\n\n- jps+jstack方式排查\n  - 查找程序运行端口\n    ```shell\n       > jps -l\n       18714 sun.tools.jps.Jps\n       18703 jvm.DeadLock\n    ```\n\n  - jstack打印堆栈信息，发现死锁存在的位置，进行排查\n    ```shell\n    > jstack -l 18703\n    ```\n    ![](../img/Java多线程/jstack死锁监测.png)\n- jconsole方式排查\n  - 选择线程，监测死锁。会将死锁的线程信息都展示出来\n- jvisualvm\n  - 选择对应进程即可直观看到死锁的存在\n\n# synchronized锁和lock锁的区别\n- Lock是一个接口而synchronized是java中的关键字，synchronized是内置的语言实现，synchronized是在JVM层面上是实现的，出异常时JVM会自动释放锁定，但是Lock不行，Lock是通过代码实现的，要保证锁定一定会被释放，就必须将unLock()放到finally{}中；\n- synchronized在发生异常时，会自动释放线程占有的锁，因此不会导致死锁现象发生。\n- Lock可以让等待锁的线程响应中断，线程可以中断去干别的事务，而synchronized却不行，使用synchronized时，等待的线程会一直等待下去，不能够响应中断\n- 通过Lock可以知道有没有成功获取锁，而synchronized却无法办到\n- Lock可以提高多个线程进行读操作的效率\n总结：当资源竞争激烈时，Lock的性能要远远优于synchronized\n\n# 参考文章\n- https://www.cnblogs.com/valjeanshaw/p/13124689.html"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "Linux/linux.md",
    "content": "# Linux\n\n## 文件和目录的操作\n\n- `ls` 显示文件和目录列表\n- `cd` 切换目录\n- `pwd` 显示当前工作目录\n- `mkdir` 创建目录\n- `rmdir` 删除空目录\n- `touch` 生成一个空文件或更改文件的时间\n- `cp` 复制文件或目录\n- `mv` 移动文件或目录、文件或目录改名\n- `rm` 删除文件或目录\n- `ln` 建立链接文件\n- `find` 查找文件\n- `file/stat` 查看文件类型或文件属性信息\n- `echo` 把内容重定向到指定的文件中 ，有则打开，无则创建\n- `管道命令 |` 将前面的结果给后面的命令，例如：`ls -la | wc `，将ls的结果加油wc命令来统计字数\n- 重定向 `>` 是覆盖模式，`>>` 是追加模式\n  - 例如：`echo \"Java3y,zhen de hen xihuan ni\" > qingshu.txt `把左边的输出放到右边的文件里去\n    \n    ## 查看文件\n- `cat` 查看文本文件内容\n- `more` 可以分页看\n- `less` 不仅可以分页，还可以方便地搜索，回翻等操作\n- `tail -10` 查看文件的尾部的10行\n- `head -20` 查看文件的头部20行\n  \n  ## 管理用户\n  \n  ### 用户管理\n- `useradd` 添加用户\n- `usermod` 修改用户\n- `userdel` 删除用户\n  \n  ### 组管理\n- `groupadd` 添加组 \n- `groupmod` 修改组 \n- `groupdel` 删除组\n  \n  ### 批量管理用户：\n- 成批添加/更新一组账户：`newusers`\n- 成批更新用户的口令：`chpasswd`\n  \n  ### 组成员管理：\n- 向标准组中添加用户\n  - `gpasswd -a <用户账号名> <组账号名>`\n  - `usermod -G <组账号名> <用户账号名>`\n- 从标准组中删除用户\n  - `gpasswd -d <用户账号名> <组账号名>`\n    \n    ### 口令管理\n- 口令时效设置： 修改 /etc/login.defs 的相关配置参数\n- 口令维护(禁用、恢复和删除用户口令)： `passwd`\n- 设置已存在用户的口令时效： `change`\n  \n  ### 切换用户\n- `su`\n- `sudo`\n  \n  ### 用户相关的命令：\n- `id`：显示用户当前的uid、gid和用户所属的组列表\n- `groups`：显示指定用户所属的组列表\n- `whoami`：显示当前用户的名称\n- `w/who`：显示登录用户及相关信息\n- `newgrp`：用于转换用户的当前组到指定的组账号，用户必须属于该组才可以正确执行该命令\n  \n  ## 进程管理\n- `ps`：查找出进程的信息 查看自己的进程\n  - `ps -l` 查看系统所有进程\n  - `ps aux` 查看特定的进程\n  - `ps aux | grep threadx` \n- `nice`和`renice`：调整进程的优先级\n- `kill`：杀死进程\n- `free`：查看内存使用状况\n- `top` ：查看实时刷新的系统进程信息\n- `netstat` 查看占用端口的进程\n  - `netstat -anp | grep port`\n- 进程状态\n  - `R` running or runnable (on run queue)正在执行或者可执行，此时进程位于执行队列中。\n  - `D` uninterruptible sleep (usually I/O)不可中断阻塞，通常为 IO 阻塞。\n  - `S` interruptible sleep (waiting for an event to complete)可中断阻塞，此时进程正在等待某个事件完成。\n  - `Z` zombie (terminated but not reaped by its parent)僵死，进程已经终止但是尚未被其父进程获取信息。\n  - `T` stopped (either by a job control signal or because it is being traced)结束，进程既可以被作业控制信号结束，也可能是正在被追踪。\n- `SIGCHLD` 当一个子进程改变了它的状态时（停止运行，继续运行或者退出），有两件事会发生在父进程中\n  - 得到 SIGCHLD 信号；\n  - waitpid() 或者 wait() 调用会返回。\n    - wait() 父进程调用 wait() 会一直阻塞，直到收到一个子进程退出的 SIGCHLD 信号，之后 wait() 函数会销毁子进程并返回。 如果成功，返回被收集的子进程的进程 ID；如果调用进程没有子进程，调用就会失败，此时返回 -1，同时 errno 被置为 ECHILD。 参数 status 用来保存被收集的子进程退出时的一些状态，如果对这个子进程是如何死掉的毫不在意，只想把这个子进程消灭掉，可以设置这个参数为 NULL。\n    - waitpid() 作用和 wait() 完全相同，但是多了两个可由用户控制的参数 pid 和 options。 pid 参数指示一个子进程的 ID，表示只关心这个子进程退出的 SIGCHLD 信号。如果 pid=-1 时，那么和 wait() 作用相同，都是关心所有子进程退出的 SIGCHLD 信号。 options 参数主要有 WNOHANG 和 WUNTRACED 两个选项，WNOHANG 可以使 waitpid() 调用变成非阻塞的，也就是说它会立即返回，父进程可以继续执行其它任务。\n  - 其中子进程发送的 SIGCHLD 信号包含了子进程的信息，比如进程 ID、进程状态、进程使用 CPU 的时间等。\n  - 在子进程退出时，它的进程描述符不会立即释放，这是为了让父进程得到子进程信息，父进程通过 wait() 和\n  - waitpid() 来获得一个已经退出的子进程的信息。\n- 僵尸进程孤儿进程什么原因导致的，哪个危害大，怎么解决\n  - `孤儿进程` 一个父进程退出，而它的一个或多个子进程还在运行，那么这些子进程将成为孤儿进程。 孤儿进程将被 init 进程（进程号为 1）所收养，并由 init 进程对它们完成状态收集工作。 由于孤儿进程会被 init 进程收养，所以孤儿进程不会对系统造成危害。\n  - `僵尸进程` 一个子进程的进程描述符在子进程退出时不会释放，只有当父进程通过 wait() 或 waitpid() 获取了子进程信息后才会释放。如果子进程退出，而父进程并没有调用 wait() 或 waitpid()，那么子进程的进程描述符仍然保存在系统中，这 种进程称之为僵尸进程。 僵尸进程通过 ps 命令显示出来的状态为 Z（zombie）。\n    - 系统所能使用的进程号是有限的，如果产生大量僵尸进程，将因为没有可用的进程号而导致系统不能产生新的进程。\n    - 要消灭系统中大量的僵尸进程，只需要将其父进程杀死，此时僵尸进程就会变成孤儿进程，从而被 init 进程所收养，\n    - 这样 init 进程就会释放所有的僵尸进程所占有的资源，从而结束僵尸进程。\n- 作业管理\n  - jobs：列举作业号码和名称\n  - bg: 在后台恢复运行\n  - fg：在前台恢复运行\n  - ctrl+z：暂时停止某个进程\n- 自动化任务\n  - at\n  - cron\n- 管理守护进程\n  - chkconfig\n  - service\n  - ntsysv\n    \n    ## 打包和压缩文件\n    \n    ### 压缩\n- `gzip filename`\n- `bzip2 filename`\n- `tar -czvf filename`\n  \n  ### 解压\n- `gzip -d filename.gz`\n- `bzip2 -d filename.bz2`\n- `tar -xzvf filename.tar.gz`\n  \n  ## grep+正则表达式\n- `grep -n mystr myfile` 在文件 myfile 中查找包含字符串 mystr的行\n- `grep  '^[a-zA-Z]'  myfile `显示 myfile 中第一个字符为字母的所有行\n  \n  ## Vi编辑器\n  \n  ### 普通模式\n- `G`用于直接跳转到文件尾\n- `ZZ`用于存盘退出Vi\n- `ZQ`用于不存盘退出Vi\n- `/`和`？`用于查找字符串\n- `n`继续查找下一个\n- `yy`复制一行\n- `p`粘帖在下一行，P粘贴在前一行\n- `dd` 删除一行文本\n- `u` 取消上一次编辑操作（undo）\n  \n  ### 插入模式\n- 使用i或a或o进去插入模式\n- 使用esc返回普通模式\n  \n  ### 命令行模式\n- `w`保存当前编辑文件，但并不退出\n- `w newfile`  存为另外一个名为 “newfile” 的文件\n- `wq` 用于存盘退出Vi\n- `q!`用于不存盘退出Vi\n- `q`用于直接退出Vi （未做修改)\n  \n  ### 设置Vi环境\n- `set autoindent`  缩进,常用于程序的编写\n- `set noautoindent` 取消缩进\n- `set number` 在编辑文件时显示行号\n- `set tabstop=value` 设置显示制表符的空格字符个数\n- `set` 显示设置的所有选项\n  \n  ## 权限管理\n- 改变文件或目录的权限：chmod\n- 改变文件或目录的属主（所有者）：chown\n- 改变文件或目录所属的组：chgrp\n- 设置文件的缺省生成掩码：umask\n- 文件扩展属性\n  - 显示扩展属性：`lsattr [-adR] [文件|目录]`\n  - 修改扩展属性：`chattr [-R] [[-+=][属性]] <文件|目录>`\n    \n    ## 网络管理\n    \n    ### 网络接口相关\n- `ifconfig`：查看网络接口信息\n- `ifup/ifdown`：开启或关闭接口\n  \n  ### 临时配置相关\n- `route`命令：可以临时地设置内核路由表\n- `hostname`命令：可以临时地修改主机名\n- `sysctl`命令：可以临时地开启内核的包转发\n- `ifconfig`命令：可以临时地设置网络接口的IP参数\n  \n  ### 网络检测的常用工具：\n- `ifconfig` 检测网络接口配置\n- `route` 检测路由配置\n- `ping` 检测网络连通性\n- `netstat` 查看网络状态\n- `lsof` 查看指定IP 和/或 端口的进程的当前运行情况\n- `host/dig/nslookup` 检测DNS解析\n- `traceroute` 检测到目的主机所经过的路由器\n- `tcpdump` 显示本机网络流量的状态\n  \n  ### 安装软件\n- `yum`\n- `rpm`\n- `wget`\n  \n  ## cpu100%怎么排查\n  \n  ### 1、问题复现\n  \n  ### 2、第一查看程序运行日志\n  \n  ### 3、排查\n- 执行“`top`”命令 查看CPU最高的进程pid\n- 执行“`top -Hp 进程号`”命令 查看java进程下的所有线程占CPU的情况。\n- 执行“`printf \"%x\\n 10`\"命令 把进程号转为16进制，方便在堆栈中查找线程号\n- 执行 “`jstack 进程号 | grep 线程ID`”\n  - 可以查看线程的状态判断问题\n    - NEW,未启动的。不会出现在Dump中。\n    - RUNNABLE,在虚拟机内执行的。运行中状态，可能里面还能看到locked字样，表明它获得了某把锁。\n    - BLOCKED,受阻塞并等待监视器锁。被某个锁(synchronizers)給block住了。\n    - WATING,无限期等待另一个线程执行特定操作。等待某个condition或monitor发生，一般停留在park(), wait(), sleep(),join() 等语句里。\n    - TIMED_WATING,有时限的等待另一个线程的特定操作。和WAITING的区别是wait() 等语句加上了时间限制 wait(timeout)。\n    - TERMINATED,已退出的。\n  - 注意deadlock\n- `jmap -dump pid` 导出dump文件供一些分析工具分析\n\n## 用户态和内核态\n\n- 为了区分不同的程序的不同权限，人们发明了内核态和用户态的概念。\n- 用户态和内核态是操作系统的两种运行级别，两者最大的区别就是特权级不同。用户态拥有最低的特权级，内核态拥有较高的特权级。 运行在用户态的程序不能直接访问操作系统内核数据结构和程序。\n- 内核态和用户态之间的转换方式主要包括：系统调用，异常和中断。\n  \n  ### 用户态和内核态的转换\n  \n  #### 系统调用\n  \n  这是用户进程主动要求切换到内核态的一种方式，用户进程通过系统调用申请操作系统提供的服务程序完成工作。而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现，例如Linux的ine 80h中断。\n  \n  #### 异常\n  \n  当CPU在执行运行在用户态的程序时，发现了某些事件不可知的异常，这是会触发由当前运行进程切换到处理此。异常的内核相关程序中，也就到了内核态，比如缺页异常。\n  \n  #### 外围设备的中断\n  \n  当外围设备完成用户请求的操作之后，会向CPU发出相应的中断信号，这时CPU会暂停执行下一条将要执行的指令，转而去执行中断信号的处理程序，如果先执行的指令是用户态下的程序，那么这个转换的过程自然也就发生了有用户态到内核态的切换。比如硬盘读写操作完成，系统会切换到硬盘读写的中断处理程序中执行后续操作等。\n\n### 切换操作\n\n从出发方式看，可以在认为存在前述3种不同的类型，但是从最终实际完成由用户态到内核态的切换操作上来说，涉及的关键步骤是完全一样的，没有任何区别，都相当于执行了一个中断响应的过程，因为系统调用实际上最终是中断机制实现的，而异常和中断处理机制基本上是一样的，用户态切换到内核态的步骤主要包括：\n\n1. 从当前进程的描述符中提取其内核栈的ss0及esp0信息。\n2. 使用ss0和esp0指向的内核栈将当前进程的cs,eip，eflags，ss,esp信息保存起来，这个过程也完成了由用户栈找到内核栈的切换过程，同时保存了被暂停执行的程序的下一条指令。\n3. 将先前由中断向量检索得到的中断处理程序的cs，eip信息装入相应的寄存器，开始执行中断处理程序，这时就转到了内核态的程序执行了。\n\n# Linux 的进程、线程、文件描述符是什么\n\n## 一、进程是什么\n\n首先，抽象地来说，我们的计算机就是这个东西：\n\n<img src=\"../img/Linux/进程是什么.png\" width=\"50%\" />\n\n这个大的矩形表示计算机的内存空间，其中的小矩形代表进程，左下角的圆形表示磁盘，右下角的图形表示一些输入输出设备，比如鼠标键盘显示器等等。另外，注意到内存空间被划分为了两块，上半部分表示用户空间，下半部分表示内核空间。\n\n用户空间装着用户进程需要使用的资源，比如你在程序代码里开一个数组，这个数组肯定存在用户空间；内核空间存放内核进程需要加载的系统资源，这一些资源一般是不允许用户访问的。但是注意有的用户进程会共享一些内核空间的资源，比如一些动态链接库等等。\n\n我们用 C 语言写一个 hello 程序，编译后得到一个可执行文件，在命令行运行就可以打印出一句 hello world，然后程序退出。在操作系统层面，就是新建了一个进程，这个进程将我们编译出来的可执行文件读入内存空间，然后执行，最后退出。\n\n你编译好的那个可执行程序只是一个文件，不是进程，可执行文件必须要载入内存，包装成一个进程才能真正跑起来。进程是要依靠操作系统创建的，每个进程都有它的固有属性，比如进程号（PID）、进程状态、打开的文件等等，进程创建好之后，读入你的程序，你的程序才被系统执行。\n\n那么，操作系统是如何创建进程的呢？对于操作系统，进程就是一个数据结构，我们直接来看 Linux 的源码：\n\n```cpp\nstruct task_struct {\n    // 进程状态\n    long              state;\n    // 虚拟内存结构体\n    struct mm_struct  *mm;\n    // 进程号\n    pid_t              pid;\n    // 指向父进程的指针\n    struct task_struct __rcu  *parent;\n    // 子进程列表\n    struct list_head        children;\n    // 存放文件系统信息的指针\n    struct fs_struct        *fs;\n    // 一个数组，包含该进程打开的文件指针\n    struct files_struct        *files;\n};\n```\n\ntask_struct 就是 Linux 内核对于一个进程的描述，也可以称为「进程描述符」。源码比较复杂，我这里就截取了一小部分比较常见的。\n\n其中比较有意思的是 mm 指针和 files 指针。mm 指向的是进程的虚拟内存，也就是载入资源和可执行文件的地方；files 指针指向一个数组，这个数组里装着所有该进程打开的文件的指针。\n\n## 二、文件描述符是什么\n\n先说 files，它是一个文件指针数组。一般来说，一个进程会从 files[0] 读取输入，将输出写入 files[1]，将错误信息写入 files[2]。\n\n举个例子，以我们的角度 C 语言的 printf 函数是向命令行打印字符，但是从进程的角度来看，就是向 files[1] 写入数据；同理，scanf 函数就是进程试图从 files[0] 这个文件中读取数据。\n\n每个进程被创建时，files 的前三位被填入默认值，分别指向标准输入流、标准输出流、标准错误流。我们常说的「文件描述符」就是指这个文件指针数组的索引，所以程序的文件描述符默认情况下 0 是输入，1 是输出，2 是错误。\n\n我们可以重新画一幅图：\n\n<img src=\"../img/Linux/文件描述符是什么.png\" width=\"50%\" />\n\n对于一般的计算机，输入流是键盘，输出流是显示器，错误流也是显示器，所以现在这个进程和内核连了三根线。因为硬件都是由内核管理的，我们的进程需要通过「系统调用」让内核进程访问硬件资源。\n\n> PS：不要忘了，Linux 中一切都被抽象成文件，设备也是文件，可以进行读和写。\n\n如果我们写的程序需要其他资源，比如打开一个文件进行读写，这也很简单，进行系统调用，让内核把文件打开，这个文件就会被放到 files 的第 4 个位置：\n\n<img src=\"../img/Linux/文件描述符和系统资源.png\" width=\"50%\" />\n\n明白了这个原理，输入重定向就很好理解了，程序想读取数据的时候就会去 files[0] 读取，所以我们只要把 files[0] 指向一个文件，那么程序就会从这个文件中读取数据，而不是从键盘：\n\n```shell\ncommand < file.txt\n```\n\n<img src=\"../img/Linux/输入重定向.png\" width=\"50%\" />\n\n同理，输出重定向就是把 files[1] 指向一个文件，那么程序的输出就不会写入到显示器，而是写入到这个文件中：\n\n```shell\ncommand > file.txt\n```\n\n<img src=\"../img/Linux/输出重定向.png\" width=\"50%\" />\n\n错误重定向也是一样的，就不再赘述。\n\n管道符其实也是异曲同工，把一个进程的输出流和另一个进程的输入流接起一条「管道」，数据就在其中传递，不得不说这种设计思想真的很优美：\n\n```shell\ncmd1 | cmd2 | cmd3\n```\n\n<img src=\"../img/Linux/管道.png\" width=\"50%\" />\n\n到这里，你可能也看出「Linux 中一切皆文件」设计思路的高明了，不管是设备、另一个进程、socket 套接字还是真正的文件，全部都可以读写，统一装进一个简单的 files 数组，进程通过简单的文件描述符访问相应资源，具体细节交于操作系统，有效解耦，优美高效。\n\n## 三、线程是什么\n\n首先要明确的是，多进程和多线程都是并发，都可以提高处理器的利用效率，所以现在的关键是，多线程和多进程有啥区别。\n\n为什么说 Linux 中线程和进程基本没有区别呢，因为从 Linux 内核的角度来看，并没有把线程和进程区别对待。\n\n我们知道系统调用 fork() 可以新建一个子进程，函数 pthread() 可以新建一个线程。但无论线程还是进程，都是用 task_struct 结构表示的，唯一的区别就是共享的数据区域不同。\n\n换句话说，线程看起来跟进程没有区别，只是线程的某些数据区域和其父进程是共享的，而子进程是拷贝副本，而不是共享。就比如说，mm 结构和 files 结构在线程中都是共享的，我画两张图你就明白了：\n\n<img src=\"../img/Linux/父进程和子进程.png\" width=\"50%\" />\n\n<img src=\"../img/Linux/父进程和子线程.png\" width=\"50%\" />\n\n所以说，我们的多线程程序要利用锁机制，避免多个线程同时往同一区域写入数据，否则可能造成数据错乱。\n\n那么你可能问，既然进程和线程差不多，而且多进程数据不共享，即不存在数据错乱的问题，为什么多线程的使用比多进程普遍得多呢？\n\n因为现实中数据共享的并发更普遍呀，比如十个人同时从一个账户取十元，我们希望的是这个共享账户的余额正确减少一百元，而不是希望每人获得一个账户的拷贝，每个拷贝账户减少十元。\n\n当然，必须要说明的是，只有 Linux 系统将线程看做共享数据的进程，不对其做特殊看待，其他的很多操作系统是对线程和进程区别对待的，线程有其特有的数据结构，我个人认为不如 Linux 的这种设计简洁，增加了系统的复杂度。\n\n在 Linux 中新建线程和进程的效率都是很高的，对于新建进程时内存区域拷贝的问题，Linux 采用了 copy-on-write 的策略优化，也就是并不真正复制父进程的内存空间，而是等到需要写操作时才去复制。所以 Linux 中新建进程和新建线程都是很迅速的。\n\n## 创建进程的方式\n\n在Linux中主要提供了fork、vfork、clone三个进程创建方法。\n在linux源码中这三个调用的执行过程是执行fork(),vfork(),clone()时，通过一个系统调用表映射到sys_fork(),sys_vfork(),sys_clone(),再在这三个函数中去调用do_fork()去做具体的创建进程工作。\n\n### fork\n\nfork创建一个进程时，子进程只是完全复制父进程的资源，复制出来的子进程有自己的task_struct结构和pid,但却复制父进程其它所有的资源\n\n### vfork\n\nvfork系统调用不同于fork，用vfork创建的子进程与父进程共享地址空间，也就是说子进程完全运行在父进程的地址空间上，如果这时子进程修改了某个变量，这将影响到父进程\n\n### clone\n\n系统调用fork()和vfork()是无参数的，而clone()则带有参数。fork()是全部复制，vfork()是共享内存，而clone() 是则可以将父进程资源有选择地复制给子进程，而没有复制的数据结构则通过指针的复制让子进程共享，具体要复制哪些资源给子进程，由参数列表中的 clone_flags来决定\n\n## 进程切换\n\n为了控制进程的执行，内核必须有能力挂起正在CPU上运行的进程，并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说，任何进程都是在操作系统内核的支持下运行的，是与内核紧密相关的。\n\n从一个进程的运行转到另一个进程上运行，这个过程中经过下面这些变化：\n\n1. 保存处理机上下文，包括程序计数器和其他寄存器。\n2. 更新PCB信息。\n3. 把进程的PCB移入相应的队列，如就绪、在某事件阻塞等队列。\n4. 选择另一个进程执行，并更新其PCB。\n5. 更新内存管理的数据结构。\n6. 恢复处理机上下文。\n\n## 进程的阻塞\n\n正在执行的进程，由于期待的某些事件未发生，如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等，则由系统自动执行阻塞原语(Block)，使自己由运行状态变为阻塞状态。可见，进程的阻塞是进程自身的一种主动行为，也因此只有处于运行态的进程（获得CPU），才可能将其转为阻塞状态。当进程进入阻塞状态，是不占用CPU资源的。\n\n## 线程、进程、协程\n\n### 进程\n\n一个进程好比是一个程序，它是 资源分配的最小单位 。同一时刻执行的进程数不会超过核心数。不过如果问单核CPU能否运行多进程？答案又是肯定的。单核CPU也可以运行多进程，只不过不是同时的，而是极快地在进程间来回切换实现的多进程。举个简单的例子，就算是十年前的单核CPU的电脑，也可以聊QQ的同时看视频。\n\n### 线程\n\n如果说进程和进程之间相当于程序与程序之间的关系，那么线程与线程之间就相当于程序内的任务和任务之间的关系。所以线程是依赖于进程的，也称为 「微进程」 。它是 程序执行过程中的最小单元 。\n\n一个程序内包含了多种任务。打个比方，用播放器看视频的时候，视频输出的画面和声音可以认为是两种任务。当你拖动进度条的时候又触发了另外一种任务\n\n### 进程与线程的区别\n\n- 进程是CPU资源分配的基本单位，线程是独立运行和独立调度的基本单位（CPU上真正运行的是线程）。\n- 进程拥有自己的资源空间，一个进程包含若干个线程，线程与CPU资源分配无关，多个线程共享同一进程内的资源。\n- 线程的调度与切换比进程快很多。\n\n### 进程上下文切换和线程上下文切换\n\n首先是进程上下文切换。在切换内容方面，进程上下文切换涉及的内容较为广泛。由于进程是操作系统中进行资源分配和调度的基本单位，它拥有自己的独立内存空间和系统资源，因此进程上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源，还包括了内核堆栈、寄存器等内核空间的资源。这些资源在切换时都需要被保存和恢复，以确保新进程能够在切换后顺利执行。\n\n在发生场景上，进程上下文切换主要发生在以下几种情况：一是进程主动调用某些系统调用时，如等待IO完成或者获取锁，这时进程无法继续运行，操作系统会触发上下文切换；二是进程分配到的时间片用完，或者有更高优先级的进程需要抢占CPU时，也会发生上下文切换。\n\n接下来是线程上下文切换。在切换内容方面，线程上下文切换主要涉及线程在执行过程中的运行条件和状态，如程序计数器、栈信息、寄存器的值等。由于线程共享进程的内存空间，因此线程上下文切换不需要像进程上下文切换那样涉及大量的内存和资源管理。\n\n线程上下文切换发生场景主要包括：线程主动让出CPU，例如调用了Thread.sleep()或Object.wait()等方法；当一个线程的时间片用完，需要切换到另一个线程继续执行；或者线程因为阻塞或等待某个事件而无法继续执行时，调度器会切换到另一个线程继续执行。\n\n### 协程\n\n协程，又称微线程，纤程。英文名Coroutine。一句话说明什么是线程：协程是一种用户态的轻量级线程。\n\n协程拥有自己的寄存器上下文和栈。协程调度切换时，将寄存器上下文和栈保存到其他地方，在切回来的时候，恢复先前保存的寄存器上下文和栈。因此：\n\n协程能保留上一次调用时的状态（即所有局部状态的一个特定组合），每次过程重入时，就相当于进入上一次调用的状态，换种说法：进入上一次离开时所处逻辑流的位置。\n\n协程的好处：\n\n- 无需线程上下文切换的开销\n- 无需原子操作锁定及同步的开销\n- 方便切换控制流，简化编程模型\n- 高并发+高扩展性+低成本：一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。\n\n缺点：\n\n- 无法利用多核资源：协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要，除非是cpu密集型应用。\n- 进行阻塞（Blocking）操作（如IO时）会阻塞掉整个程序\n  \n  ## 文件描述符fd\n  \n  文件描述符（File descriptor）是计算机科学中的一个术语，是一个用于表述指向文件的引用的抽象化概念。\n\n文件描述符在形式上是一个非负整数。实际上，它是一个索引值，指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时，内核向进程返回一个文件描述符。在程序设计中，一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。\n\n## 缓存 I/O\n\n缓存 I/O 又被称作标准 I/O，大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中，操作系统会将 I/O 的数据缓存在文件系统的页缓存（ page cache ）中，也就是说，数据会先被拷贝到操作系统内核的缓冲区中，然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。\n\n缓存 I/O 的缺点：\n\n- 数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作，这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。\n\n## IO模型\n\n### 一个IO操作通常包括两个阶段：\n\n等待数据准备好；\n\n从内核向进程复制数据；\n\n- 对于一个套接字上的输入操作，第一步通常涉及等待数据从网络中到达。当所等待分组到达时，它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。\n  \n  ### Linux/Unix五种IO模型\n  \n  #### 阻塞式 IO (Blocking IO)\n\n<img src=\"../img/netty/阻塞式io.png\" width=\"50%\" />\n\n过程\n\n- 使用系统调用，并一直阻塞直到内核将数据准备好，之后再由内核缓冲区复制到用户态，在等待内核准备的这段时间什么也干不了\n- 如图函数调用期间，一直被阻塞，直到数据准备好且从内核复制到用户程序才返回，这种IO模型为阻塞式IO。\n\n优缺点\n\n- 优点：程序简单，在阻塞等待数据期间进程/线程挂起，基本不会占用 CPU 资源。\n- 缺点：每个连接需要独立的进程/线程单独处理，当并发请求量大时为了维护程序，内存、线程切换开销较大，这种模型在实际生产中很少使用。\n  \n  #### 非阻塞式 IO (Non-blocking IO)\n\n<img src=\"../img/netty/非阻塞式io.png\" width=\"50%\" />\n\n过程描述\n\n- 内核在没有准备好数据的时候会返回错误码，而调用程序不会休眠，而是不断轮询询问内核数据是否准备好\n- 下图函数调用时，如果数据没有准备好，不像阻塞式IO那样一直被阻塞，而是返回一个错误码。数据准备好时，函数成功返回。\n- 应用程序对这样一个非阻塞描述符循环调用成为轮询。\n- 非阻塞式IO的轮询会耗费大量cpu，通常在专门提供某一功能的系统中才会使用。通过为套接字的描述符属性设置非阻塞式，可使用该功能\n\n优缺点\n\n- 优点 不会阻塞在内核的等待数据过程，每次发起的 I/O 请求可以立即返回，不用阻塞等待，实时性较好。\n- 缺点 轮询将会不断地询问内核，这将占用大量的 CPU 时间，系统资源利用率较低，所以一般 Web 服务器不使用这种 I/O 模型。\n  \n  #### IO 复用 (I/O multiplexing)\n\n<img src=\"../img/netty/io复用.png\" width=\"50%\" />\n\n过程描述\n\n- 类似与非阻塞，只不过轮询不是由用户线程去执行，而是由内核去轮询，内核监听程序监听到数据准备好后，调用内核函数复制数据到用户态\n- 下图中select这个系统调用，充当代理类的角色，不断轮询注册到它这里的所有需要IO的文件描述符，有结果时，把结果告诉被代理的recvfrom函数，它本尊再亲自出马去拿数据\n- IO多路复用至少有两次系统调用，如果只有一个代理对象，性能上是不如前面的IO模型的，但是由于它可以同时监听很多套接字，所以性能比前两者高\n- 主要是select和epoll。一个线程可以对多个IO端口进行监听，当socket有读写事件时分发到具体的线程进行处理\n\n优缺点\n\n- 优点 可以基于一个阻塞对象，同时在多个描述符上等待就绪，而不是使用多个线程(每个文件描述符一个线程)，这样可以大大节省系统资源。\n- 缺点 当连接数较少时效率相比多线程+阻塞 I/O 模型效率较低，可能延迟更大，因为单个连接处理需要 2 次系统调用，占用时间会有增加\n  \n  #### 信号驱动式 IO (signal driven I/O (SIGIO))\n\n<img src=\"../img/netty/信号驱动式io.png\" width=\"50%\" />\n\n过程描述\n\n- 信号驱动式I/O：首先我们允许Socket进行信号驱动IO,并安装一个信号处理函数，进程继续运行并不阻塞。\n- 使用信号，内核在数据准备就绪时通过信号来进行通知\n- 首先开启信号驱动io套接字，并使用sigaction系统调用来安装信号处理程序，内核直接返回，不会阻塞用户态\n- 数据准备好时，内核会发送SIGIO信号，收到信号后开始进行io操作\n\n优缺点\n\n- 优点 线程并没有在等待数据时被阻塞，可以提高资源的利用率。\n- 缺点\n  - 信号 I/O 在大量 IO 操作时可能会因为信号队列溢出导致没法通知。\n  - 信号驱动 I/O 尽管对于处理 UDP 套接字来说有用，即这种信号通知意味着到达一个数据报，或者返回一个异步错误。\n  - 但是，对于 TCP 而言，信号驱动的 I/O 方式近乎无用，因为导致这种通知的条件为数众多，每一个来进行判别会消耗很大资源，与前几种方式相比优势尽失。\n    \n    #### 异步 IO (asynchronous I/O)\n    \n    <img src=\"../img/netty/异步io.png\" width=\"50%\" />\n\n过程描述\n\n- 异步IO依赖信号处理程序来进行通知\n- 不过异步IO与前面IO模型不同的是：前面的都是数据准备阶段的阻塞与非阻塞，异步IO模型通知的是IO操作已经完成，而不是数据准备完成\n- 异步IO才是真正的非阻塞，主进程只负责做自己的事情，等IO操作完成(数据成功从内核缓存区复制到应用程序缓冲区)时通过回调函数对数据进行处理\n- 相对于同步IO，异步IO不是顺序执行。用户进程进行aio_read系统调用之后，无论内核数据是否准备好，都会直接返回给用户进程，然后用户态进程可以去做别的事情。等到socket数据准备好了，内核直接复制数据给进程，然后从内核向进程发送通知。IO两个阶段，进程都是非阻塞的。\n\n优缺点\n\n- 优点 异步 I/O 能够充分利用 DMA 特性，让 I/O 操作与计算重叠。\n- 缺点 要实现真正的异步 I/O，操作系统需要做大量的工作。目前 Windows 下通过 IOCP 实现了真正的异步 I/O。\n  \n  # select、poll、epoll\n  \n  ## select\n  \n  ```shell\n  int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);\n  ```\n  \n  select 函数监视的文件描述符分3类，分别是writefds、readfds、和exceptfds。调用后select函数会阻塞，直到有描述符就绪（有数据 可读、可写、或者有except），或者超时（timeout指定等待时间，如果立即返回设为null即可），函数返回。当select函数返回后，可以通过遍历fdset，来找到就绪的描述符。\n\nselect目前几乎在所有的平台上支持，其良好跨平台支持也是它的一个优点。select的一 个缺点在于单个进程能够监视的文件描述符的数量存在最大限制，在Linux上一般为1024，可以通过修改宏定义甚至重新编译内核的方式提升这一限制，但 是这样也会造成效率的降低。\n\n## poll\n\n```shell\nint poll (struct pollfd *fds, unsigned int nfds, int timeout);\n```\n\n不同与select使用三个位图来表示三个fdset的方式，poll使用一个 pollfd的指针实现。\n\n```shell\nstruct pollfd {\n    int fd; /* file descriptor */\n    short events; /* requested events to watch */\n    short revents; /* returned events witnessed */\n};\n```\n\npollfd结构包含了要监视的event和发生的event，不再使用select“参数-值”传递的方式。同时，pollfd并没有最大数量限制（但是数量过大后性能也是会下降）。 和select函数一样，poll返回后，需要轮询pollfd来获取就绪的描述符。\n\n从上面看，select和poll都需要在返回后，通过遍历文件描述符来获取已经就绪的socket。事实上，同时连接的大量客户端在一时刻可能只有很少的处于就绪状态，因此随着监视的描述符数量的增长，其效率也会线性下降。\n\npoll本质上和select没有区别，它将用户传入的数组拷贝到内核空间，然后查询每个fd对应的设备状态，如果设备就绪则在设备等待队列中加入一项并继续遍历，如果遍历完所有fd后没有发现就绪设备，则挂起当前进程，直到设备就绪或者主动超时，被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。\n\n## epoll\n\nepoll是在2.6内核中提出的，是之前的select和poll的增强版本。相对于select和poll来说，epoll更加灵活，没有描述符限制。epoll使用一个文件描述符管理多个描述符，将用户关系的文件描述符的事件存放到内核的一个事件表中，这样在用户空间和内核空间的copy只需一次。\n\n### epoll操作过程\n\nepoll操作过程需要三个接口，分别如下：\n\n```shell\nint epoll_create(int size)；//创建一个epoll的句柄，size用来告诉内核这个监听的数目一共有多大\nint epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)；\nint epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);\n```\n\n#### int epoll_create(int size);\n\n- 创建一个epoll的句柄，size用来告诉内核这个监听的数目一共有多大，这个参数不同于select()中的第一个参数，给出最大监听的fd+1的值，参数size并不是限制了epoll所能监听的描述符最大个数，只是对内核初始分配内部数据结构的一个建议。\n- 当创建好epoll句柄后，它就会占用一个fd值，在linux下如果查看/proc/进程id/fd/，是能够看到这个fd的，所以在使用完epoll后，必须调用close()关闭，否则可能导致fd被耗尽。\n  \n#### int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)；\n  \n函数是对指定描述符fd执行op操作。\n- epfd：是epoll_create()的返回值。\n- op：表示op操作，用三个宏来表示：添加EPOLL_CTL_ADD，删除EPOLL_CTL_DEL，修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。\n- fd：是需要监听的fd（文件描述符）\n- epoll_event：是告诉内核需要监听什么事，struct epoll_event结构如下：\n  \n  ```shell\n  struct epoll_event {\n  __uint32_t events;  /* Epoll events */\n  epoll_data_t data;  /* User data variable */\n  };\n\n  //events可以是以下几个宏的集合：\n  EPOLLIN ：表示对应的文件描述符可以读（包括对端SOCKET正常关闭）；\n  EPOLLOUT：表示对应的文件描述符可以写；\n  EPOLLPRI：表示对应的文件描述符有紧急的数据可读（这里应该表示有带外数据到来）；\n  EPOLLERR：表示对应的文件描述符发生错误；\n  EPOLLHUP：表示对应的文件描述符被挂断；\n  EPOLLET： 将EPOLL设为边缘触发(Edge Triggered)模式，这是相对于水平触发(Level Triggered)来说的。\n  EPOLLONESHOT：只监听一次事件，当监听完这次事件之后，如果还需要继续监听这个socket的话，需要再次把这个socket加入到EPOLL队列里\n\n  ```\n#### int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);\n- 等待epfd上的io事件，最多返回maxevents个事件。\n- 参数events用来从内核得到事件的集合，maxevents告之内核这个events有多大，这个maxevents的值不能大于创建epoll_create()时的size，参数timeout是超时时间（毫秒，0会立即返回，-1将不确定，也有说法说是永久阻塞）。该函数返回需要处理的事件数目，如返回0表示已超时。\n\n### 工作模式\nepoll对文件描述符的操作有两种模式：LT（level trigger）和ET（edge trigger）。LT模式是默认模式，LT模式与ET模式的区别如下：\n\nLT模式：当epoll_wait检测到描述符事件发生并将此事件通知应用程序，应用程序可以不立即处理该事件。下次调用epoll_wait时，会再次响应应用程序并通知此事件。\n- LT(level triggered)是缺省的工作方式，并且同时支持block和no-block socket.在这种做法中，内核告诉你一个文件描述符是否就绪了，然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作，内核还是会继续通知你的。\n\n\nET模式：当epoll_wait检测到描述符事件发生并将此事件通知应用程序，应用程序必须立即处理该事件。如果不处理，下次调用epoll_wait时，不会再次响应应用程序并通知此事件。\n- ET(edge-triggered)是高速工作方式，只支持no-block socket。在这种模式下，当描述符从未就绪变为就绪时，内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪，并且不会再为那个文件描述符发送更多的就绪通知，直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如，你在发送，接收或者接收请求，或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误）。但是请注意，如果一直不对这个fd作IO操作(从而导致它再次变成未就绪)，内核不会发送更多的通知(only once)\n- ET模式在很大程度上减少了epoll事件被重复触发的次数，因此效率要比LT模式高。epoll工作在ET模式的时候，必须使用非阻塞套接口，以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。\n\n### epoll总结\n在 select/poll中，进程只有在调用一定的方法后，内核才对所有监视的文件描述符进行扫描，而epoll事先通过epoll_ctl()来注册一 个文件描述符，一旦基于某个文件描述符就绪时，内核会采用类似callback的回调机制，迅速激活这个文件描述符，当进程调用epoll_wait() 时便得到通知。(此处去掉了遍历文件描述符，而是通过监听回调的的机制。这正是epoll的魅力所在。)\n\nepoll的优点主要是一下几个方面：\n- 监视的描述符数量不受限制，它所支持的FD上限是最大可以打开文件的数目，这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左 右，具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。select的最大缺点就是进程打开的fd是有数量限制的。这对 于连接数量比较大的服务器来说根本不能满足。虽然也可以选择多进程的解决方案( Apache就是这样实现的)，不过虽然linux上面创建进程的代价比较小，但仍旧是不可忽视的，加上进程间数据同步远比不上线程间同步的高效，所以也不是一种完美的方案。\n- IO的效率不会随着监视fd的数量的增长而下降。epoll不同于select和poll轮询的方式，而是通过每个fd定义的回调函数来实现的。只有就绪的fd才会执行回调函数。\n- 如果没有大量的idle -connection或者dead-connection，epoll的效率并不会比select/poll高很多，但是当遇到大量的idle- connection，就会发现epoll的效率大大高于select/poll。\n\n# 进程间通信方式\n进程间通信（Inter-Process Communication, IPC）是指在操作系统中，不同进程之间为了数据共享或协作，进行数据交换的方式。不同的操作系统提供了多种进程间通信的机制，常见的进程间通信方式包括以下几种：\n\n## 1. `管道（Pipes）`\n\n  - `无名管道（Anonymous Pipe）`\n    特点：无名管道是一种半双工通信机制，即数据只能在一个方向上传递（单向），适用于父子进程之间的通信。\n    \n    使用场景：无名管道常用于 Unix 系统中的简单进程间通信，如父子进程间的消息传递。\n    \n    局限性：只能在具有亲缘关系的进程之间使用，数据只能单向传输。\n  \n  - `命名管道（Named Pipe，FIFO）`\n\n    特点：命名管道支持双向通信，且允许不相关的进程之间通信。命名管道在文件系统中有一个名字，可以被多个进程打开进行读写。\n\n    使用场景：用于具有无亲缘关系的进程间通信，且适用于跨不同终端或网络的进程间通信。\n\n    局限性：在某些系统中，命名管道的性能可能不如其他 IPC 方式。\n\n## 2. `信号（Signals）`\n\n特点：信号是一种异步通信方式，用于通知进程发生了某种事件。信号只能传递非常简单的信息，通常只表示某种事件的发生，而不能携带复杂的数据。\n\n使用场景：信号通常用于进程之间的通知，比如让一个进程中断或终止某个操作。\n\n局限性：信号传递的信息有限，且如果没有处理好信号可能会导致进程中断。\n\n## 3. `消息队列（Message Queues）`\n\n特点：消息队列允许进程以消息的形式发送和接收数据。消息队列在内核中维护，多个进程可以通过向消息队列中添加消息或从中读取消息来进行通信。消息可以是任意类型的数据。\n\n使用场景：适用于复杂的多进程通信，尤其是需要携带多种类型数据或结构化数据的场景。\n\n局限性：需要进程配合内核进行管理，队列大小有限制，可能会有阻塞和等待的问题。\n\n## 4. `共享内存（Shared Memory）`\n\n特点：共享内存是进程间通信最快的方式之一。多个进程可以直接访问相同的内存区域，从而实现数据共享。共享内存允许不同进程直接读写公共内存区，但需要同步机制来防止竞争条件。\n\n使用场景：适用于需要快速、大量数据共享的场景，如图像处理、大量数据的实时传递等。\n\n局限性：需要额外的同步机制（如信号量或互斥锁）来避免同时访问共享内存时的数据竞争问题。\n\n## 5. `信号量（Semaphores）`\n\n特点：信号量主要用于解决并发进程的同步和互斥问题。信号量本质上是一个计数器，用于控制多个进程对共享资源的访问，常与共享内存结合使用。\n\n使用场景：在进程间需要同步或互斥访问共享资源时使用，如进程锁定共享内存的读写操作。\n\n局限性：信号量不能直接传递数据，仅用于协调对共享资源的访问。\n\n## 6. `套接字（Sockets）`\n\n特点：套接字不仅用于网络通信，还可以用于本地进程间通信。通过本地的 UNIX 套接字（Unix Domain Sockets），进程可以通过类似网络通信的方式进行数据传输。\n\n使用场景：套接字适用于网络和本地进程间通信，特别是在需要进程之间跨计算机通信的分布式系统中。\n\n局限性：与其他本地通信方式相比，套接字通信的开销相对较大，适合大规模、跨主机的进程间通信。\n\n## 7. `文件映射（Memory-Mapped Files, MMF）`\n\n特点：文件映射是一种将文件内容映射到进程的地址空间的通信方式。多个进程可以通过将同一个文件映射到它们的地址空间来实现共享数据。\n\n使用场景：用于多个进程之间共享文件中的数据，且适合大文件的共享访问，如操作系统中的虚拟内存管理。\n\n局限性：进程间需要同步访问文件映射的内存区域，并且映射的内容受限于文件系统。\n\n## 8. `管道符重定向`\n\n特点：这是基于 Shell 的一种通信方式，将一个进程的输出作为另一个进程的输入，常见的形式是使用 | 管道符。通过这种方式，一个进程可以将其标准输出传递给另一个进程的标准输入。\n\n使用场景：常用于命令行程序的组合，如将一个命令的输出通过管道符传递给另一个命令。\n\n局限性：仅限于进程标准输入和输出的简单重定向，不能用于复杂的数据共享。\n\n## 9. `远程过程调用（RPC, Remote Procedure Call）`\n\n特点：RPC 允许进程调用另一个进程的函数，就像在本地进程中调用函数一样。RPC 隐藏了进程间通信的细节，使得进程之间的通信像本地调用一样简单。\n\n使用场景：适用于分布式系统中进程之间的通信，如微服务架构中的服务间调用。\n\n局限性：RPC 需要序列化和反序列化数据，网络延迟可能影响性能。\n\n## 10. `DBus`\n\n特点：DBus 是 Linux 环境中的进程间通信系统，允许多个进程通过总线进行通信。DBus 支持一对一和一对多通信，适合桌面应用程序之间的通信。\n\n使用场景：常用于桌面系统和嵌入式系统中进程间的消息传递和服务调用。\n\n局限性：与共享内存等方式相比，DBus 的传输性能较低，适用于消息级别的通信。\n\n## `总结`\n\n- 简单数据传递：信号、管道、消息队列等适合简单数据或控制信号的传递。\n- 大规模数据共享：共享内存、文件映射适合高效的大数据传输，但需要额外的同步机制。\n- 分布式或远程通信：套接字、RPC 适合分布式系统中的进程通信。\n- 同步与互斥：信号量适合用于进程间同步，常与共享内存结合使用。\n- 不同的进程间通信机制各有优缺点，具体使用哪种方式，取决于通信的数据量、速度、进程间的关系以及系统的整体架构。\n\n# Linux物理内存和虚拟内存\n`物理内存`： 真实的内存，就是我们常说的那个4G、8G、16G的内存条。\n\n`虚拟内存`： 是一个概念，并不是实际的内存，对于4G内存的Linux系统来说，虚拟内存也为4G，其中1G为系统的内存，剩下的3G为应用程序的内存。\n\n## 内存映射\nLinux 内核给每个进程都提供了一个独立的虚拟地址空间，并且这个地址空间是连续的，进程就可以很方便地访问内存，也就是我们常说的虚拟内存虚拟内存\n\n这里会有一种错觉，我们每个进程都占有了这么多的空间，那么多个进程怎么办？事实上并没有那么多的空间，其实这个本质是就是一种“自欺欺人”的做法，每个进程都以为自己占据了全部的地址空间；其实只有在实际使用虚拟内存的时候，才会分配物理内存；\n\n通过内存映射将虚拟内存地址映射到物理内存地址，对实际使用虚拟内存并分配的物理内存进行管理；\n\n作用就是：只会将某一进程此刻需要的内存大小映射到物理内存，其它暂时不需要的内容交换到硬盘存储即可。当进程需要使用在硬盘中的内容或者需要动态申请内存时，操作系统会利用缺页操作，触发一次内存映射，将另外的物理内存映射进虚拟内存，供程序使用，这样对于进程而言，则认为内存总是够用的。\n\n各个进程均拥有3G虚拟内存，那么操作系统是如何做到各进程所使用的实际物理内存不会互相占用呢？实际上，各个进程均有自己的内存映射表。任意一个时刻，在一个CPU上只有一个进程在运行。所以对于此CPU来讲，在这一时刻，整个系统只存在一个4GB的虚拟地址空间，这个虚拟地址空间是面向此进程的。当进程发生切换的时候，虚拟地址空间也随着切换。由此可以看出，每个进程都有自己的虚拟地址空间，只有此进程运行的时候，其虚拟地址空间才被运行它的CPU所知。在其它时刻，其虚拟地址空间对于CPU来说，是不可知的。所以尽管每个进程都可以有4 GB的虚拟地址空间，但在CPU眼中，只有一个虚拟地址空间存在。虚拟地址空间的变化，随着进程切换而变化。\n## 内存映射原理\n\n<img src=\"../img/Linux/内存映射.png\" width=\"50%\" />\n\n虚拟内存映射到物理内存地址，内核为每一个进程维护了一张表，记录了他们对应的映射关系；\n\n而当进程访问的虚拟地址在页表中查不到时，系统会产生一个**缺页异常**，进入内核空间分配物理内存、更新进程页表，最后再返回用户空间，恢复进程的运行。\n\nLinux采用了四级页表来管理内存页，多级页表就是把内存分成区块来管理，将原来的映射关系改成区块索引和区块内的偏移。由于虚拟内存空间通常只用了很少一部分，那么，多级页表就只保存这些使用中的区块，这样就可以大大地减少页表的项数。\n\n# 页面置换算法\n页面置换算法是一种用于解决虚拟内存中页面置换问题的算法，主要目的是选择一个页面被置换出去以腾出空间，以便将更紧急的页面调入内存。常见的页面置换算法有：\n\n1. 最优页面置换算法（OPT）：选择在未来最长时间内不再被访问的页面进行置换，是理论上最优的页面置换算法。\n2. 先进先出页面置换算法（FIFO）：选择最早被调入内存的页面进行置换。\n3. 最近最少使用页面置换算法（LRU）：选择最近一段时间内最少被访问的页面进行置换。\n4. 时钟页面置换算法（Clock）：基于FIFO算法，使用一个时钟指针指向当前扫描的页面，如果页面的访问位为0，则置换该页面，如果为1，则将该页面的访问位设置为0，并继续扫描下一个页面，直到找到需要置换的页面。\n5. 最不经常使用页面置换算法（NFU）：根据页面被访问的次数来选择最不经常使用的页面进行置换。\n\n这些算法的实现方式和效率各有不同，可以根据具体的应用场景选择适合的算法。\n\n# 进程调度算法\n进程调度算法是用于选择下一个要执行的进程的算法，常见的进程调度算法有：\n\n1. 先来先服务调度算法（FCFS）：按照进程到达的顺序进行调度，先到达的进程先执行，简单易实现，但无法充分利用CPU。\n2. 最短进程优先调度算法（SJF）：按照进程执行时间的长度进行调度，执行时间短的进程先执行，可最大限度地减少平均等待时间和周转时间，但需要预先知道进程的执行时间，实现复杂。\n3. 时间片轮转调度算法（RR）：将CPU的执行时间分为若干个时间片，每个进程在一个时间片内执行，时间片用完后，将进程放入就绪队列的末尾，再轮到下一个进程执行，简单易实现，但进程的响应时间和周转时间长。\n4. 最高优先级调度算法（HPF）：按照进程优先级进行调度，优先级高的进程先执行，可充分利用CPU，但可能出现低优先级进程长时间等待的情况，造成饥饿现象。\n5. 多级反馈队列调度算法（MFQ）：将就绪队列分为若干个队列，每个队列拥有不同的时间片大小，进程优先级高的进入低时间片的队列，优先级低的进入高时间片的队列，可兼顾短进程和长进程的需求，但实现较为复杂。\n\n# 操作系统里的轮询和中断\n在操作系统中，轮询（Polling）和中断（Interrupt）是两种不同的处理方式。\n\n轮询是一种主动的查询方式，它通过循环检查某个状态是否发生变化来获取信息。例如，在读取键盘输入时，程序会不断地检查键盘输入缓冲区是否有数据可读，如果有则读取数据，否则就继续等待。轮询的优点是简单易懂，但它会占用大量的CPU时间，效率较低。\n\n中断是一种被动的处理方式，它在外部事件发生时被触发，通常是硬件设备或其他程序向操作系统发出请求。例如，在读取键盘输入时，程序可以注册一个中断处理函数，在键盘输入缓冲区有数据可读时，硬件会触发中断，操作系统会调用中断处理函数来处理输入。中断的优点是可以减少CPU的占用时间，提高效率。\n\n在实际应用中，操作系统往往采用轮询和中断相结合的方式来处理输入输出事件。例如，可以使用中断处理键盘输入事件，但是当CPU空闲时可以使用轮询方式来检查其他设备是否有数据可读。这样可以充分利用CPU时间，提高系统的效率。\n\n# 参考文章\n- https://segmentfault.com/a/1190000003063859\n- https://blog.csdn.net/qq_34170700/article/details/106996450\n- https://zhuanlan.zhihu.com/p/70256971\n- https://labuladong.gitee.io/algo/5/35/\n- https://www.nowcoder.com/interview/center?entranceType=%E5%AF%BC%E8%88%AA%E6%A0%8F\n```\n"
  },
  {
    "path": "Mybatis/mybatis.md",
    "content": "\n* [mybatis](#mybatis)\n    * [什么是mybatis](#什么是mybatis)\n    * [JDBC执行六步骤](#jdbc执行六步骤)\n    * [mybatis执行8步骤](#mybatis执行8步骤)\n        * [步骤](#步骤)\n    * [MyBatis 整体架构](#mybatis-整体架构)\n        * [基础支持层](#基础支持层)\n        * [核心处理层](#核心处理层)\n    * [mybatis缓存](#mybatis缓存)\n        * [一级缓存（Local Cache）](#一级缓存local-cache)\n    * [一级缓存配置](#一级缓存配置)\n        * [二级缓存](#二级缓存)\n        * [mybatis封装参数执行SQL](#mybatis封装参数执行sql)\n    * [mybatis中$和#的区别](#mybatis中和的区别)\n\n# mybatis\n\n## 什么是mybatis\n\nMyBatis 是一款旨在帮助开发人员屏蔽底层重复性原生 JDBC 代码的持久化框架，其支持通过映射文件配置或注解将 ResultSet 映射为 Java 对象。相对于其它 ORM 框架，MyBatis 更为轻量级，支持定制化 SQL\n和动态 SQL，方便优化查询性能，同时包含了良好的缓存机制\n\n## JDBC执行六步骤\n\n- 注册驱动\n- 获取Connection连接\n- 执行预编译\n- 执行SQL\n- 封装结果集\n- 释放资源\n```java\npublic void findStudent() {\n    Connection conn = null;\n    Statement stmt = null;\n    ResultSet rs = null;\n\n    try {\n        //注册MySQL驱动\n        Class.forName(\"com.mysql.jdbc.Driver\");\n        //连接数据库的基本信息\n        String url = \"jdbc:mysql://localhost:3306/db\";\n        String username = \"root\";\n        String password = \"password\";\n        //创建连接对象\n        conn = DriverManager.getConnection(url, username, password);\n        //保存查询结果\n        List<Student> stuList = new ArrayList<>();\n        //创建statement，执行SQL\n        stmt = conn.createStatement();\n        //执行查询\n        rs = stmt.executeQuery(\"select * from student\");\n        while (rs.next()) {\n            //取出结果封装为对象\n            Student student = new Student();\n            student.setAge(rs.getInt(\"age\"));\n            student.setName(rs.getString(\"name\"));\n            stuList.add(student);\n        }\n    } catch (Exception e) {\n        e.printStackTrace();\n    } finally {\n        //关闭资源\n        try {\n            if (rs != null) {\n                rs.close();\n            }\n            if (stmt != null) {\n                stmt.close();\n            }\n            if (conn != null) {\n                conn.close();\n            }\n        } catch (SQLException e) {\n            e.printStackTrace();\n        }\n    }\n}\n```\n## mybatis执行8步骤\n<img src=\"../img/mybatis/mybatis执行8步走.png\" width=\"50%\" />\n\n```java\npublic void testStart() throws IOException {\n    //1.mybatis 主配置文件\n    String config = \"mybatis-config.xml\";\n    //2.读取配置文件\n    InputStream in = Resources.getResourceAsStream(config);\n    //3.创建SqlSessionFactory对象，目的是为了获取SqlSession\n    SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);\n    //4.获取SqlSession，SqlSession能执行sql\n    SqlSession sqlSession = factory.openSession();\n    //5.执行session的select\n    List<Student> students = sqlSession.selectList(\"com.dao.UserDao.selectAll\");\n    //6.循环输出查询结果\n    students.forEach(System.out::println);\n    //7.关闭资源\n    sqlSession.close();\n}\n```\n### 步骤\n1. 读取MyBatis的核心配置文件。mybatis-config.xml为MyBatis的全局配置文件，用于配置数据库连接、属性、类型别名、类型处理器、插件、环境配置、映射器（mapper.xml）等信息，这个过程中有一个比较重要的部分就是映射文件其实是配在这里的；这个核心配置文件最终会被封装成一个Configuration对象\n2. 加载映射文件。映射文件即SQL映射文件，该文件中配置了操作数据库的SQL语句，映射文件是在mybatis-config.xml中加载；可以加载多个映射文件。常见的配置的方式有两种，一种是package扫描包，一种是mapper找到配置文件的位置。\n3. 构造会话工厂获取SqlSessionFactory。这个过程其实是用建造者设计模式使用SqlSessionFactoryBuilder对象构建的，SqlSessionFactory的最佳作用域是应用作用域。\n4. 创建会话对象SqlSession。由会话工厂创建SqlSession对象，对象中包含了执行SQL语句的所有方法，每个线程都应该有它自己的 SqlSession\n   实例。SqlSession的实例不是线程安全的，因此是不能被共享的，所以它的最佳的作用域是请求或方法作用域。\n    1. SqlSession： 对外提供了用户和数据库之间交互需要的所有方法，隐藏了底层的细节。默认实现类是DefaultSqlSession。\n5. Executor执行器。是MyBatis的核心，负责SQL语句的生成和查询缓存的维护，它将根据SqlSession传递的参数动态地生成需要执行的SQL语句，同时负责查询缓存的维护\n    1. Executor： SqlSession向用户提供操作数据库的方法，但和数据库操作有关的职责都会委托给Executor。下面是不同的实现类赋予了不同的能力\n        1. SimpleExecutor -- SIMPLE 就是普通的执行器。\n        2. ReuseExecutor-执行器会重用预处理语句（PreparedStatements）\n        3. BatchExecutor --它是批处理执行器\n6. MappedStatement对象。MappedStatement是对解析的SQL的语句封装，一个MappedStatement代表了一个sql语句标签，如下：\n   ```xml\n   <!--一个动态sql标签就是一个`MappedStatement`对象-->\n   <select\tid=\"selectUserList\"\tresultType=\"com.mybatis.User\"> \n   select * from t_user\n   </select>\n   ```\n7. 输入参数映射。输入参数类型可以是基本数据类型，也可以是Map、List、POJO类型复杂数据类型，这个过程类似于JDBC的预编译处理参数的过程，有两个属性 parameterType和parameterMap\n8. 封装结果集。可以封装成多种类型可以是基本数据类型，也可以是Map、List、POJO类型复杂数据类型。封装结果集的过程就和JDBC封装结果集是一样的。也有两个常用的属性resultType和resultMap。\n\n## MyBatis 整体架构\n\n<img src=\"../img/mybatis/mybatis整体架构.png\" width=\"50%\" />\n\n### 基础支持层\n\n- 反射模块：提供封装的反射 API，方便上层调用。\n- 类型转换：为简化配置文件提供了别名机制，并且实现了 Java 类型和 JDBC 类型的互转。\n- 日志模块：能够集成多种第三方日志框架。\n- 资源加载模块：对类加载器进行封装，提供加载类文件和其它资源文件的功能。\n- 数据源模块：提供数据源实现并能够集成第三方数据源模块。\n- 事务管理：可以和 Spring 集成开发，对事务进行管理。\n- 缓存模块：提供一级缓存和二级缓存，将部分请求拦截在缓存层。\n- Binding 模块：在调用 SqlSession 相应方法执行数据库操作时，需要指定映射文件中的 SQL 节点，MyBatis 通过 Binding 模块将自定义 Mapper\n  接口与映射文件关联，避免拼写等错误导致在运行时才发现相应异常。\n\n### 核心处理层\n\n<img src=\"../img/mybatis/核心处理层.png\" width=\"50%\" />\n\n- SqlSession 接口定义了暴露给应用程序调用的 API，接口层在收到请求时会调用核心处理层的相应模块完成具体的数据库操作\n- 配置解析：MyBatis 初始化时会加载配置文件、映射文件和 Mapper 接口的注解信息，解析后会以对象的形式保存到 Configuration 对象中\n- SQL 解析与 scripting 模块：MyBatis 支持通过配置实现动态 SQL，即根据不同入参生成 SQL\n- SQL 执行与结果解析：Executor 负责维护缓存和事务管理，并将数据库相关操作委托给 StatementHandler，ParmeterHadler 负责完成 SQL 语句的实参绑定并通过 Statement 对象执行\n  SQL，通过 ResultSet 返回结果，交由 ResultSetHandler 处理\n\n## mybatis缓存\n\n### 一级缓存（Local Cache）\n\n在应用运行过程中，我们有可能在一次数据库会话中，执行多次查询条件完全相同的SQL，MyBatis提供了一级缓存的方案优化这部分场景，如果是相同的SQL语句，会优先命中一级缓存，避免直接对数据库进行查询，提高性能\n\n- 每个SqlSession中持有了Executor，每个Executor中有一个LocalCache。当用户发起查询时，MyBatis根据当前执行的语句生成MappedStatement，在Local\n  Cache进行查询，如果缓存命中的话，直接返回结果给用户，如果缓存没有命中的话，查询数据库，结果写入Local Cache，最后返回结果给用户\n- 一级缓存配置\n    -\n    开发者只需在MyBatis的配置文件中，添加如下语句，就可以使用一级缓存。共有两个选项，SESSION或者STATEMENT，默认是SESSION级别，即在一个MyBatis会话中执行的所有语句，都会共享这一个缓存。一种是STATEMENT级别，可以理解为缓存只对当前执行的这一个Statement有效\n    - `<setting name=\"localCacheScope\" value=\"SESSION\"/>`\n- 特点\n    - MyBatis一级缓存的生命周期和SqlSession一致。\n    - MyBatis一级缓存内部设计简单，只是一个没有容量限定的HashMap，在缓存的功能性上有所欠缺。\n    - MyBatis的一级缓存最大范围是SqlSession内部，有多个SqlSession或者分布式的环境下，数据库写操作会引起脏数据，建议设定缓存级别为Statement。\n    - 同一个sqlsession里update\\delete都会使缓存实现\n\n### 二级缓存\n\n在上文中提到的一级缓存中，其最大的共享范围就是一个SqlSession内部，如果多个SqlSession之间需要共享缓存，则需要使用到二级缓存。开启二级缓存后，会使用CachingExecutor装饰Executor，进入一级缓存的查询流程前，先在CachingExecutor进行二级缓存的查询\n\n- 二级缓存开启后，同一个namespace下的所有操作语句，都影响着同一个Cache，即二级缓存被多个SqlSession共享，是一个全局的变量。\n- 当开启缓存后，数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库\n- 二级缓存配置\n    - 在MyBatis的配置文件中开启二级缓存。`<setting name=\"cacheEnabled\" value=\"true\"/>`\n    - 在MyBatis的映射XML中配置cache或者 cache-ref 。\n      cache-ref代表引用别的命名空间的Cache配置，两个命名空间的操作使用的是同一个Cache。`<cache-ref namespace=\"mapper.StudentMapper\"/>`\n- 特点\n    - 当sqlsession没有调用commit()方法时，二级缓存并没有起到作用。\n    - update操作会刷新该namespace下的二级缓存。\n    - 二级缓存不适应用于映射文件中存在多表查询\n    - 在分布式环境下，由于默认的MyBatis\n      Cache实现都是基于本地的，分布式环境下必然会出现读取到脏数据，需要使用集中式缓存将MyBatis的Cache接口实现，有一定的开发成本，直接使用Redis、Memcached等分布式缓存可能成本更低，安全性也更高\n- 建议在生产中关闭缓存，单纯作为ORM框架使用即可\n\n### mybatis封装参数执行SQL\n\n- mybatis会使用MapperProxyFactory类中的newInstance(MapperProxy mapperProxy)方法来使用JDK动态代理生成EmployeeMapper代理对象,通过代理对象来与数据库进行会话.\n\n```java\npublic class MapperProxyFactory {\n    /*\n    ...\n    */\n    protected T newInstance(MapperProxy<T> mapperProxy) {\n        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);\n    }\n\n    public T newInstance(SqlSession sqlSession) {\n        final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);\n        return newInstance(mapperProxy);\n    }\n}\n```\n\n- 该行代码会调用代理对象的invoke方法\n\n```java\npublic class MapperProxy<T> implements InvocationHandler, Serializable {\n    @Override\n    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {\n        try {\n            if (Object.class.equals(method.getDeclaringClass())) {\n                return method.invoke(this, args);\n            } else if (isDefaultMethod(method)) {\n                return invokeDefaultMethod(proxy, method, args);\n            }\n        } catch (Throwable t) {\n            throw ExceptionUtil.unwrapThrowable(t);\n        }\n        //该方法首先在缓存中查找是否存在目标方法,如果不存在,则创建一个新的MapperMethod对象并且缓存\n        final MapperMethod mapperMethod = cachedMapperMethod(method);\n        return mapperMethod.execute(sqlSession, args);\n    }\n}\n```\n- 在创建MapperMethod对象时,会同时初始化这两个对象,他们是构造方法的参数\n```java\npublic MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {\n    this.command = new SqlCommand(config, mapperInterface, method);\n    this.method = new MethodSignature(config, mapperInterface, method);\n}\n```\n- `private final SqlCommand command;`//该类负责封装SQL语句的标签类型(如:SELECT,UPDATE,DELETE,INSERT)和目标方法名\n- `private final MethodSignature method;`//该类负责封装方法的参数和返回值类型等信息\n   ```java\n    public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {\n         //...\n         最后一行方法需要注意 ，该条语句实例化了一个ParamNameResolver类对象,该类主要的作用就是解析参数.\n         this.paramNameResolver = new ParamNameResolver(configuration, method);\n       }\n   ```\n    ```java\n    public ParamNameResolver(Configuration config, Method method) {\n        final Class<?>[] paramTypes = method.getParameterTypes();\n        //获取方法上定义的@Param注解\n        final Annotation[][] paramAnnotations = method.getParameterAnnotations();\n        final SortedMap<Integer, String> map = new TreeMap<Integer, String>();\n        int paramCount = paramAnnotations.length;\n        // get names from @Param annotations\n        for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {\n          if (isSpecialParameter(paramTypes[paramIndex])) {\n            // skip special parameters\n            continue;\n          }\n          String name = null;\n          for (Annotation annotation : paramAnnotations[paramIndex]) {\n            if (annotation instanceof Param) {\n              hasParamAnnotation = true;\n              name = ((Param) annotation).value();\n              break;\n            }\n          }\n          if (name == null) {\n            // @Param was not specified.\n            if (config.isUseActualParamName()) {\n              name = getActualParamName(method, paramIndex);\n            }\n            if (name == null) {\n              // use the parameter index as the name (\"0\", \"1\", ...)\n              // gcode issue #71\n              name = String.valueOf(map.size());\n            }\n          }\n          //解析后放入一个map里，按顺序设置kv，例如{ 0: id ,1 : name}\n          map.put(paramIndex, name);\n        }\n        names = Collections.unmodifiableSortedMap(map);\n      }\n    ```\n- 这两个对象`SqlCommand`、`MethodSignature`都初始化完毕后，我们回到MapperProxy.invoke()方法，继续执行`mapperMethod.execute(sqlSession, args)`\n  - 这里会根据SqlCommand里的参数获取到方法是select、update还是delete等等，执行相应的方法\n```java\npublic Object execute(SqlSession sqlSession, Object[] args) {\n    Object result;\n    switch (command.getType()) {\n      case INSERT: {\n      Object param = method.convertArgsToSqlCommandParam(args);\n        result = rowCountResult(sqlSession.insert(command.getName(), param));\n        break;\n      }\n      case UPDATE: {\n        Object param = method.convertArgsToSqlCommandParam(args);\n        result = rowCountResult(sqlSession.update(command.getName(), param));\n        break;\n      }\n      case DELETE: {\n        Object param = method.convertArgsToSqlCommandParam(args);\n        result = rowCountResult(sqlSession.delete(command.getName(), param));\n        break;\n      }\n      case SELECT:\n        if (method.returnsVoid() && method.hasResultHandler()) {\n          executeWithResultHandler(sqlSession, args);\n          result = null;\n        } else if (method.returnsMany()) {\n          result = executeForMany(sqlSession, args);\n        } else if (method.returnsMap()) {\n          result = executeForMap(sqlSession, args);\n        } else if (method.returnsCursor()) {\n          result = executeForCursor(sqlSession, args);\n        } else {\n          Object param = method.convertArgsToSqlCommandParam(args);\n          result = sqlSession.selectOne(command.getName(), param);\n        }\n        break;\n      case FLUSH:\n        result = sqlSession.flushStatements();\n        break;\n      default:\n        throw new BindingException(\"Unknown execution method for: \" + command.getName());\n    }\n    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {\n      throw new BindingException(\"Mapper method '\" + command.getName() \n          + \" attempted to return null from a method with a primitive return type (\" + method.getReturnType() + \").\");\n    }\n    return result;\n  }\n```\n以INSERT为例，会执行method.convertArgsToSqlCommandParam(args);\n```java\npublic Object convertArgsToSqlCommandParam(Object[] args) {\n      return paramNameResolver.getNamedParams(args);\n}\n```\n这里会进入`ParamNameResolver.getNamedParams`方法,ParamNameResolver之前已经初始化完，这里遍历names并设置name为arg传入的值，构造param返回\n```java\npublic Object getNamedParams(Object[] args) {\n    final int paramCount = names.size();\n    if (args == null || paramCount == 0) {\n      return null;\n    } else if (!hasParamAnnotation && paramCount == 1) {\n      return args[names.firstKey()];\n    } else {\n      final Map<String, Object> param = new ParamMap<Object>();\n      int i = 0;\n      for (Map.Entry<Integer, String> entry : names.entrySet()) {\n        param.put(entry.getValue(), args[entry.getKey()]);\n        // add generic param names (param1, param2, ...)\n        final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);\n        // ensure not to overwrite parameter named with @Param\n        if (!names.containsValue(genericParamName)) {\n          param.put(genericParamName, args[entry.getKey()]);\n        }\n        i++;\n      }\n      return param;\n    }\n  }\n```\n参数封装完毕，调用 `result = rowCountResult(sqlSession.insert(command.getName(), param));`，并执行SQL获取结果，封装结果返回\n\n## mybatis中$和#的区别\n- `#{}` mybatis生成sql时会使用占位符 ? 替换，并使用预编译，能有效的防止SQL注入\n- `${}` 生成SQL时直接设置值\n"
  },
  {
    "path": "Netty/netty.md",
    "content": "\n* [netty](#netty)\n    * [重要的组件](#重要的组件)\n        * [Channel](#channel)\n        * [ChannelFuture](#channelfuture)\n        * [EventLoop](#eventloop)\n        * [ChannelHandler](#channelhandler)\n        * [ChannelPipeline](#channelpipeline)\n        * [TaskQueue](#taskqueue)\n    * [netty的使用示例](#netty的使用示例)\n        * [服务端](#服务端)\n        * [客户端](#客户端)\n    * [服务端 Netty 的工作架构图](#服务端-netty-的工作架构图)\n    * [TCP粘包/拆包问题](#tcp粘包拆包问题)\n        * [什么是粘包拆包](#什么是粘包拆包)\n        * [发生的原因](#发生的原因)\n        * [粘包解决策略](#粘包解决策略)\n        * [netty粘包问题解决方案](#netty粘包问题解决方案)\n    * [解编码技术](#解编码技术)\n        * [Java序列化的缺点](#java序列化的缺点)\n        * [Google的protobuf](#google的protobuf)\n        * [Facebook的Thrift](#facebook的thrift)\n        * [JBoss的Marshalling](#jboss的marshalling)\n        * [MessagePack](#messagepack)\n    * [高性能的原因](#高性能的原因)\n        * [非阻塞io](#非阻塞io)\n        * [零拷贝](#零拷贝)\n        * [内存池](#内存池)\n        * [高效的Reactor线程模型](#高效的reactor线程模型)\n            * [Reactor 单线程模型](#reactor-单线程模型)\n            * [Reactor 多线程模型](#reactor-多线程模型)\n            * [（采用）主从 Reactor 多线程模型](#采用主从-reactor-多线程模型)\n        * [无锁化串行设计](#无锁化串行设计)\n        * [高性能的序列化框架](#高性能的序列化框架)\n        * [灵活的TCP 参数配置能力](#灵活的tcp-参数配置能力)\n* [netty相关问题](#netty相关问题)\n    * [JUC线程池和Netty线程池在架构设计上有什么区别？](#juc线程池和netty线程池在架构设计上有什么区别)\n    * [netty的线程池设计中，是如何实现线程懒加载的](#netty的线程池设计中是如何实现线程懒加载的)\n    * [netty的选择器策略设定的目的是什么](#netty的选择器策略设定的目的是什么)\n    * [nioeventloop可以处理普通任务、定时任务以及IO事件，具体的流程是什么](#nioeventloop可以处理普通任务定时任务以及io事件具体的流程是什么)\n    * [eventloop可以对于多个channel，两者之间是如何绑定的](#eventloop可以对于多个channel两者之间是如何绑定的)\n    * [什么是水平触发，什么是边缘触发](#什么是水平触发什么是边缘触发)\n    * [netty的AttributeKey、AttributeMap和Attribute](#netty的attributekeyattributemap和attribute)\n    * [netty的fastThreadLocal fast在什么地方](#netty的fastthreadlocal-fast在什么地方)\n    * [guava和netty的异步回调模式](#guava和netty的异步回调模式)\n    * [handler是怎么连接起来的](#handler是怎么连接起来的)\n* [参考文章](#参考文章)\n\n\n# netty\n## 重要的组件\n### Channel\nChannel 是 Netty 网络操作抽象类，它除了包括基本的 I/O 操作，如 bind、connect、read、write 之外，还包括了 Netty 框架相关的一些功能，如获取该 Channe l的 EventLoop\n### ChannelFuture\nNetty 为异步非阻塞，即所有的 I/O 操作都为异步的，因此，我们不能立刻得知消息是否已经被处理了。Netty 提供了 ChannelFuture 接口，通过该接口的 addListener() 方法注册一个 ChannelFutureListener，当操作执行成功或者失败时，监听就会自动触发返回结果\n### EventLoop\n- Netty 基于事件驱动模型，使用不同的事件来通知我们状态的改变或者操作状态的改变。它定义了在整个连接的生命周期里当有事件发生的时候处理的核心抽象\n- Channel 为Netty 网络操作抽象类，EventLoop 主要是为Channel 处理 I/O 操作，两者配合参与 I/O 操作\n- 当一个连接到达时，Netty 就会注册一个 Channel，然后从 EventLoopGroup 中分配一个 EventLoop 绑定到这个Channel上，在该Channel的整个生命周期中都是有这个绑定的 EventLoop 来服务的\n### ChannelHandler\n- ChannelHandler 为 Netty 中最核心的组件，它充当了所有处理入站和出站数据的应用程序逻辑的容器。ChannelHandler 主要用来处理各种事件，这里的事件很广泛，比如可以是连接、数据接收、异常、数据转换等。\n- ChannelHandler 有两个核心子类 ChannelInboundHandler 和 ChannelOutboundHandler，其中 ChannelInboundHandler 用于接收、处理入站数据和事件，而 ChannelOutboundHandler 则相反\n### ChannelPipeline\nNetty 的 ChannelPipeline，它维护了一个 ChannelHandler 责任链，负责拦截或者处理 inbound（入站）和 outbound（出站）的事件和操作。这一节给出更深层次的描述。\n\nChannelPipeline 实现了一种高级形式的拦截过滤器模式，使用户可以完全控制事件的处理方式，以及 Channel 中各个 ChannelHandler 如何相互交互。\n\n每个 Netty Channel 包含了一个 ChannelPipeline（其实 Channel 和 ChannelPipeline 互相引用），而 ChannelPipeline 又维护了一个由 ChannelHandlerContext 构成的双向循环列表，其中的每一个 ChannelHandlerContext 都包含一个 ChannelHandler。（前文描述的时候为了简便，直接说 ChannelPipeline 包含了一个 ChannelHandler 责任链，这里给出完整的细节。）\n\n<img src=\"../img/netty/ChannelPipeline.png\" width=\"50%\" />\n\nrContext、ChannelHandler、Channel、ChannelPipeline 这几个组件之间互相引用，互为各自的属性，你中有我、我中有你。\n\n在处理入站事件的时候，入站事件及数据会从 Pipeline 中的双向链表的头 ChannelHandlerContext 流向尾 ChannelHandlerContext，并依次在其中每个 ChannelInboundHandler（例如解码 Handler）中得到处理；出站事件及数据会从 Pipeline 中的双向链表的尾 ChannelHandlerContext 流向头 ChannelHandlerContext，并依次在其中每个 ChannelOutboundHandler（例如编码 Handler）中得到处理。\n\n<img src=\"../img/netty/ChannelPipeline_inout.png\" width=\"50%\" />\n\n### TaskQueue\n在 Netty 的每一个 NioEventLoop 中都有一个 TaskQueue，设计它的目的是在任务提交的速度大于线程的处理速度的时候起到缓冲作用。或者用于异步地处理 Selector 监听到的 IO 事件\n\n<img src=\"../img/netty/TaskQueue.png\" width=\"50%\" />\n\nNetty 中的任务队列有三种使用场景：\n\n1. 处理用户程序的自定义普通任务的时候\n2. 处理用户程序的自定义定时任务的时候\n3. 非当前 Reactor 线程调用当前 Channel 的各种方法的时候。\n\n## netty的使用示例\n### 服务端\n```java\npublic void startNetty() {\n    EventLoopGroup acceptor = new NioEventLoopGroup();\n    EventLoopGroup worker = new NioEventLoopGroup();\n    try {\n        ServerBootstrap bootstrap = new ServerBootstrap();\n        bootstrap.group(acceptor, worker)\n                .option(ChannelOption.SO_BACKLOG, 1024)\n                .channel(NioServerSocketChannel.class)\n                .childHandler(new ChannelInitializer<SocketChannel>() {\n                    @Override\n                    public void initChannel(SocketChannel e) throws Exception {\n                        e.pipeline().addLast(\"http-codec\",new HttpServerCodec());\n                        e.pipeline().addLast(\"aggregator\",new HttpObjectAggregator(65536));\n                        e.pipeline().addLast(\"http-chunked\",new ChunkedWriteHandler());\n                        e.pipeline().addLast(\"handler\",new WsHandler());\n                    }\n                });\n        int port = 8888;\n        ChannelFuture f = bootstrap.bind(port).sync();\n        f.channel().closeFuture().sync();\n    } catch (InterruptedException e) {\n        e.printStackTrace();\n    } finally {\n        acceptor.shutdownGracefully();\n        worker.shutdownGracefully();\n    }\n\n}\n```\n### 客户端\n```java\npublic void connect(String host, int port) throws Exception {\n    EventLoopGroup worker = new NioEventLoopGroup();\n    try {\n        Bootstrap b = new Bootstrap();\n        b.group(worker)\n                .channel(NioSocketChannel.class)\n                .handler(new ChannelInitializer<SocketChannel>() {\n                    @Override\n                    public void initChannel(SocketChannel ch) throws Exception {\n                        ch.pipeline().addLast(new SimpleClientHandler());\n                    }\n                });\n        ChannelFuture f = b.connect(host, port).sync();\n        f.channel().closeFuture().sync();\n    } finally {\n        worker.shutdownGracefully();\n    }\n}\n```\n## 服务端 Netty 的工作架构图\n\n![](../img/netty/服务端Netty的工作架构图.png)\n\n关于这张图，作以下几点说明：\n\n1. Netty 抽象出两组线程池：BossGroup 和 WorkerGroup，也可以叫做 BossNioEventLoopGroup 和 WorkerNioEventLoopGroup。每个线程池中都有 NioEventLoop 线程。BossGroup 中的线程专门负责和客户端建立连接，WorkerGroup 中的线程专门负责处理连接上的读写。BossGroup 和 WorkerGroup 的类型都是 NioEventLoopGroup。\n2. NioEventLoopGroup 相当于一个事件循环组，这个组中含有多个事件循环，每个事件循环就是一个 NioEventLoop。\n3. NioEventLoop 表示一个不断循环的执行事件处理的线程，每个 NioEventLoop 都包含一个 Selector，用于监听注册在其上的 Socket 网络连接（Channel）。\n4. NioEventLoopGroup 可以含有多个线程，即可以含有多个 NioEventLoop。\n5. 每个 BossNioEventLoop 中循环执行以下三个步骤：\n   1. select：轮训注册在其上的 ServerSocketChannel 的 accept 事件（OP_ACCEPT 事件）\n   2. processSelectedKeys：处理 accept 事件，与客户端建立连接，生成一个 NioSocketChannel，并将其注册到某个 WorkerNioEventLoop 上的 Selector 上\n   3. runAllTasks：再去以此循环处理任务队列中的其他任务\n6. 每个 WorkerNioEventLoop 中循环执行以下三个步骤：\n   1. select：轮训注册在其上的 NioSocketChannel 的 read/write 事件（OP_READ/OP_WRITE 事件）\n   2. processSelectedKeys：在对应的 NioSocketChannel 上处理 read/write 事件\n   3. runAllTasks：再去以此循环处理任务队列中的其他任务\n7. 在以上两个processSelectedKeys步骤中，会使用 Pipeline（管道），Pipeline 中引用了 Channel，即通过 Pipeline 可以获取到对应的 Channel，Pipeline 中维护了很多的处理器（拦截处理器、过滤处理器、自定义处理器等）。这里暂时不详细展开讲解 Pipeline。\n\n## TCP粘包/拆包问题\n### 什么是粘包拆包\n一个完整的包在发送过程中可能会被拆成多个包进行发送\n- 启用Nagle算法（可配置是否启用）对较小的数据包进行合并\n            \n也可能把很多个小的包封装成一个大的包发送\n### 发生的原因\n应用程序write写入的字节大小大于套接口发送缓冲区的大小\n\n进行MSS(最大报文长度)大小的TCP分段\n- 当TCP报文长度-TCP头部长度>MSS的时候将发生拆包。\n- MSS常常使用1460，是因为MTU最大为1500，减去IP头(20字节)和TCP头(20字节)后为1460。\n            \n以太网帧的payload大于MTU进行IP分片\n- MTU是指IP层在一个数据包内最大能传输的字节数\n- MTU= MSS+TCP层头部长度+IP层头部长度\n            \n服务器在接收到数据后，放到缓冲区中，如果消息没有被及时从缓存区取走，下次在取数据的时候可能就会出现一次取出多个数据包的情况，造成粘包现象\n### 粘包解决策略\n- 消息定长，每个报文的大小固定为200字节不够空位补空格\n- 在包尾增加回车换行符进行分割，如FTP协议\n- 将消息分为消息头和消息体，消息头包含消息总长度\n### netty粘包问题解决方案\n- LineBasedFrameDecoder和StringDecoder解码器（按行切换的文本解码器）\n  - LineBasedFrameDecoder 依次遍历ByteBuf中的可读字节，判断是否有\\n或者\\r\\n，如果有就以此为结束位置，从可读索引到结束位置区间的字节就组成了一行\n  - StringDecoder 将接收到的对象转换为字符串，然后继续调用后的Handler\n  - 添加到ChannelPipeline中\n- DelimiterBasedFrameDecoder以分隔符作为结束标识的解码器\n- FixedLengthFrameDecoder固定长度解码器\n## 解编码技术\n### Java序列化的缺点\n- 性能太低\n- 码流太大\n- 不跨语言\n### Google的protobuf\n### Facebook的Thrift\n### JBoss的Marshalling\n### MessagePack\n## 高性能的原因\n### 非阻塞io\n采用IO多路复用技术，让多个IO的阻塞复用到一个select线程阻塞上，能有效的应对大量的并发请求\n### 零拷贝\n- Netty 的接收和发送 ByteBuffer 采用 DIRECT BUFFERS，使用堆外直接内存进行 Socket 读写，不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存（HEAP BUFFERS）进行 Socket 读写，JVM 会将堆内存 Buffer 拷贝一份到直接内存中，然后才写入 Socket 中。相比于堆外直接内存，消息在发送过程中多了一次缓冲区的内存拷贝。\n- Netty 提供了组合 Buffer 对象，可以聚合多个 ByteBuffer 对象，用户可以像操作一个 Buffer 那样方便的对组合 Buffer 进行操作，避免了传统通过内存拷贝的方式将几个小 Buffer 合并成一个大的Buffer。\n- Netty的文件传输采用了transferTo方法，它可以直接将文件缓冲区的数据发送到目标Channel，避免了传统通过循环 write 方式导致的内存拷贝问题\n### 内存池\n基于对象池的 ByteBuf可以重用 ByteBuf对象，内部维护了一个内存池，可以循环利用已创建的 ByteBuf，提升内存的使用效率，降低由于高负载导致的频繁GC。测试表明使用内存池后的Nety在高负载、大并发的冲击下内存和GC更加平稳\n### 高效的Reactor线程模型\n#### Reactor 单线程模型\n<img src=\"../img/netty/reactor单线程.png\" width=\"50%\" />\n\n流程\n- Reactor 对象通过 Select 监控客户端请求事件，收到事件后通过 Dispatch 进行分发。\n- 如果是建立连接请求事件，则由 Acceptor 通过 Accept 处理连接请求，然后创建一个 Handler 对象处理连接完成后的后续业务处理。\n- 如果不是建立连接事件，则 Reactor 会分发调用连接对应的 Handler 来响应。\n- Handler 会完成 Read→业务处理→Send 的完整业务流程。这个过程中，无论是事件监听、事件分发、还是事件处理，都始终只有 一个线程 执行所有的事情\n\n优点：模型简单，没有多线程、进程通信、竞争的问题，全部都在一个线程中完成。\n\n缺点：性能问题，只有一个线程，无法完全发挥多核 CPU 的性能。Handler 在处理某个连接上的业务时，整个进程无法处理其他连接事件，很容易导致性能瓶颈。\n\n可靠性问题，线程意外跑飞，或者进入死循环，会导致整个系统通信模块不可用，不能接收和处理外部消息，造成节点故障。\n\n使用场景：客户端的数量有限，业务处理非常快速，比如 Redis，业务处理的时间复杂度 O(1)。\n#### Reactor 多线程模型\n<img src=\"../img/netty/reactor多线程.png\" width=\"50%\" />\n\n流程\n- Reactor 对象通过 Select 监控客户端请求事件，收到事件后通过 Dispatch 进行分发。\n- 如果是建立连接请求事件，则由 Acceptor 通过 Accept 处理连接请求，然后创建一个 Handler 对象处理连接完成后续的各种事件。\n- 如果不是建立连接事件，则 Reactor 会分发调用连接对应的 Handler 来响应。\n- Handler 只负责响应事件，不做具体业务处理，通过 Read 读取数据后，会分发给后面的 Worker 线程池进行业务处理。\n- Worker 线程池会分配独立的线程完成真正的业务处理，如何将响应结果发给 Handler 进行处理。\n- Handler 收到响应结果后通过 Send 将响应结果返回给 Client。\n\n优点：可以充分利用多核 CPU 的处理能力。\n\n缺点：多线程数据共享和访问比较复杂；Reactor 承担所有事件的监听和响应，在单线程中运行，高并发场景下容易成为性能瓶颈。\n\n#### （采用）主从 Reactor 多线程模型\n<img src=\"../img/netty/主从Reactor多线程模型.png\" width=\"50%\" />\n\n流程\n- Reactor 主线程 MainReactor 对象通过 Select 监控建立连接事件，收到事件后通过 Acceptor 接收，处理建立连接事件。\n- Acceptor 处理建立连接事件后，MainReactor 将连接分配 Reactor 子线程给 SubReactor 进行处理。\n- SubReactor 将连接加入连接队列进行监听，并创建一个 Handler 用于处理各种连接事件。\n- 当有新的事件发生时，SubReactor 会调用连接对应的 Handler 进行响应。\n- Handler 通过 Read 读取数据后，会分发给后面的 Worker 线程池进行业务处理。\n- Worker 线程池会分配独立的线程完成真正的业务处理，如何将响应结果发给 Handler 进行处理。\n- Handler 收到响应结果后通过 Send 将响应结果返回给 Client。\n\n优点：父线程与子线程的数据交互简单职责明确，父线程只需要接收新连接，子线程完成后续的业务处理。\n\n父线程与子线程的数据交互简单，Reactor 主线程只需要把新连接传给子线程，子线程无需返回数据。\n\n这种模型在许多项目中广泛使用，包括 Nginx 主从 Reactor 多进程模型，Memcached 主从多线程，Netty 主从多线程模型的支持。\n### 无锁化串行设计\n消息的处理尽可能在一个线程内完成，期间不进行线程切换，避免了多线程竞争和同步锁的使用\n### 高性能的序列化框架\nNetty 默认提供了对Google Protobuf 的支持，通过扩展Netty 的编解码接口，可以实现其它的高性能序列化框架\n### 灵活的TCP 参数配置能力\n合理设置TCP 参数在某些场景下对于性能的提升可以起到显著的效果，例如SO_RCVBUF 和SO_SNDBUF。如果设置不当，对性能的影响是非常大的\n- SO_RCVBUF 和SO_SNDBUF：通常建议值为128K 或者256K；\n- SO_TCPNODELAY：NAGLE 算法通过将缓冲区内的小封包自动相连，组成较大的封包，阻止大量小封包的发送阻塞网络，从而提高网络应用效率。但是对于时延敏感的应用场景需要关闭该优化算法；\n- 软中断：如果Linux 内核版本支持RPS（2.6.35 以上版本），开启RPS 后可以实现软中断，提升网络吞吐量。RPS根据数据包的源地址，目的地址以及目的和源端口，计算出一个hash 值，然后根据这个hash 值来选择软中断运行的cpu，从上层来看，也就是说将每个连接和cpu 绑定，并通过这个hash 值，来均衡软中断在多个cpu 上，提升网络并行处理性能\n\n# netty相关问题\n\n## JUC线程池和Netty线程池在架构设计上有什么区别？\nJUC线程池（Java Util Concurrent线程池）和Netty线程池都是用于管理和复用线程的工具，但它们的架构设计有一些区别：\n\n1. 任务执行方式：JUC线程池是基于阻塞队列的方式执行任务，即将任务放入队列中，然后等待线程来取出并执行任务。而Netty线程池则是基于事件循环的方式执行任务，即通过事件的触发来执行相应的任务。\n2. 线程数量的管理：JUC线程池中线程的数量是固定的，由程序员手动设置。而Netty线程池中线程的数量是动态的，可以根据负载情况自动调整。\n3. 线程池的结构：JUC线程池是一个单一的线程池，所有的任务都是由同一个线程池管理。而Netty线程池采用了多级线程池的结构，即将线程池分成多个层级，不同的层级负责不同的任务，从而提高了性能和效率。\n4. 线程池的作用范围：JUC线程池是一个通用的线程池，可以用于任何类型的应用程序。而Netty线程池是专门为网络应用程序设计的，它使用了一些专门的技术来提高网络通信的效率和性能。\n\n总的来说，JUC线程池更加通用，适用于各种类型的应用程序，而Netty线程池则专门针对网络应用程序进行了优化，能够更好地提高网络通信的效率和性能。\n\n\n## netty的线程池设计中，是如何实现线程懒加载的\n在Netty的线程池设计中，实现线程懒加载主要是通过ChannelPipeline的设计来实现的。\n\n在Netty中，每个Channel都有一个ChannelPipeline，ChannelPipeline由一系列的ChannelHandler组成，每个ChannelHandler都会处理ChannelPipeline中的事件。当一个Channel被创建时，其对应的ChannelPipeline并不会立即创建并初始化所有的ChannelHandler，而是等到第一次有事件触发时才进行初始化。\n\n当有事件发生时，Netty会从线程池中获取一个空闲的线程来执行ChannelPipeline中的ChannelHandler，如果线程池中没有空闲线程，则会根据需要创建新的线程。在这个过程中，线程池会根据负载情况动态调整线程的数量。\n\n由于ChannelPipeline的设计，Netty能够实现线程懒加载，即只有在有事件发生时才会创建和初始化ChannelHandler和对应的线程。这样能够有效地减少线程的创建和销毁，提高了线程的利用率，同时也能够节省系统资源。\n\n## netty的选择器策略设定的目的是什么\nNetty是一个基于NIO（非阻塞I/O）的网络框架，它使用Java的选择器（Selector）来实现事件驱动的网络编程。在Netty中，选择器策略的设定是为了实现更好的网络性能和可靠性。\n\n选择器策略指定了Netty如何处理网络事件，包括连接、读、写和异常等。它的目的是通过调整选择器策略来提高网络的吞吐量和响应速度，并减少网络故障的风险。\n\n具体来说，选择器策略的设定会影响以下方面：\n\n1. 网络吞吐量：选择器策略决定了Netty如何处理网络事件，它可以影响事件处理的效率和网络吞吐量。例如，如果选择的策略是轮询（Round Robin）方式，那么Netty将按照连接的顺序依次处理事件，这种方式可能会导致某些连接被阻塞，从而降低整个网络的吞吐量。\n2. 响应速度：选择器策略也会影响Netty的响应速度。如果选择的策略是优先级（Priority）方式，那么Netty将优先处理重要的事件，这种方式可以提高网络的响应速度。\n3. 网络故障：选择器策略还可以影响网络故障的风险。例如，如果选择的策略是故障转移（Failover）方式，那么Netty将在一个连接失败时自动切换到另一个可用的连接，从而提高网络的可靠性。\n\n因此，选择器策略的设定对于Netty的性能和可靠性都非常重要，需要根据实际的应用场景来选择最适合的策略。\n\n## nioeventloop可以处理普通任务、定时任务以及IO事件，具体的流程是什么\nNioEventLoop是Netty中的一个关键组件，它负责处理所有的I/O事件、定时任务和普通任务。下面是NioEventLoop处理这些任务的具体流程：\n\n1. 初始化：当NioEventLoop创建时，它会初始化一些必要的变量和数据结构，包括选择器（Selector）和任务队列（Task Queue）等。\n2. 运行循环：NioEventLoop会进入一个无限循环中，不断地处理I/O事件、定时任务和普通任务。在循环的每个周期内，NioEventLoop会执行以下步骤：\n   1. 处理I/O事件：NioEventLoop会从选择器中获取已经就绪的I/O事件，并将其分发到相应的ChannelHandler中进行处理。具体来说，NioEventLoop会调用ChannelHandler的相应方法（如channelRead()）来处理读取事件、写入事件和连接事件等。\n   2. 处理定时任务：NioEventLoop会检查当前是否有定时任务需要执行。如果有，它会从定时任务队列中获取任务，并执行相应的操作。例如，如果定时任务是定时发送心跳包，则NioEventLoop会将心跳包发送到相应的服务器。\n   3. 处理普通任务：NioEventLoop还可以处理普通任务，包括用户自定义的任务和Netty内部的任务。当有普通任务需要执行时，NioEventLoop会将其加入到任务队列中，并在下一个周期中执行。\n   4. 处理关闭事件：如果NioEventLoop被关闭，则循环会停止并退出。\n3. 优化：NioEventLoop还会执行一些优化操作，例如调整I/O事件的顺序、批量处理任务、调整定时任务的延迟等，以提高系统的性能和响应速度。\n\n总体来说，NioEventLoop采用一个高效的事件循环模型，通过不断地处理I/O事件、定时任务和普通任务，实现了高性能、低延迟的网络通信。\n\n## eventloop可以对于多个channel，两者之间是如何绑定的\n在Netty中，一个EventLoop对象可以被多个Channel对象共享，而一个Channel对象也只会绑定到一个EventLoop对象上。这种关系是通过EventLoopGroup来建立的。\n\n在Netty中，每个EventLoopGroup对象都包含一个或多个EventLoop对象，而每个EventLoop对象则维护着一个选择器（Selector）以及一个任务队列（TaskQueue）。当一个Channel对象被创建时，它会被绑定到一个EventLoop对象上，而EventLoop对象则负责处理该Channel上的所有事件。\n\n多个Channel对象可以共享一个EventLoop对象，因为EventLoop对象内部使用的是事件循环机制。EventLoop对象会维护一个任务队列，以此来处理Channel对象的所有事件。当多个Channel对象共享一个EventLoop对象时，它们的事件会被加入到同一个任务队列中，并由同一个EventLoop对象来处理。\n\n在Netty中，Channel对象和EventLoop对象之间的绑定关系是通过EventLoopGroup对象来实现的，EventLoopGroup对象会根据一定的算法选择一个EventLoop对象来绑定Channel对象，这种绑定关系是动态的，可以随着系统负载的变化而自动调整。这样做的目的是为了最大化地利用系统资源，提高系统的性能和稳定性。\n\n## 什么是水平触发，什么是边缘触发\n水平触发（Level-Triggered）和边缘触发（Edge-Triggered）是两种不同的IO事件触发模式，常用于IO多路复用中。\n\n水平触发是指在IO事件未被处理的情况下，只要该IO事件的状态处于“可读”或“可写”状态，系统就会不断地通知该IO事件的处理程序去处理该事件。也就是说，水平触发是在IO事件状态未变化的情况下，不断地通知事件处理程序去处理事件。通常情况下，IO事件的状态变化比较频繁，因此水平触发的方式会不断地触发事件处理程序，导致系统资源的浪费。\n\n边缘触发是指只有在IO事件状态发生变化时才会通知事件处理程序去处理该事件。也就是说，边缘触发是在IO事件状态发生变化时，才触发事件处理程序去处理事件。由于边缘触发只在事件状态变化时触发事件处理程序，因此可以避免不必要的事件处理，减少系统资源的浪费，提高系统性能。\n\n在Netty中，默认情况下采用的是水平触发的方式，但也提供了边缘触发的选项。边缘触发需要调用ChannelOption.TCP_NODELAY方法来设置，一旦设置了边缘触发模式，Netty就会在事件状态变化时触发事件处理程序去处理事件。\n\nJava NIO（New IO）使用的是水平触发（Level-Triggered）的方式，也就是说，只要IO事件的状态处于“可读”或“可写”状态，系统就会不断地通知该IO事件的处理程序去处理该事件，直到该事件被处理完成。Java NIO是通过Selector来实现多路复用，Selector会不断地轮询已经注册的Channel，当Channel有就绪事件时，Selector就会通知事件处理程序去处理该事件。因此，Java NIO使用的是水平触发的方式，而非边缘触发。\n\n## netty的AttributeKey、AttributeMap和Attribute\n在Netty中，AttributeKey、AttributeMap和Attribute都是用于在Channel和ChannelHandlerContext之间共享数据的机制。\n\nAttributeKey是一个类似于Map中的键值对，可以被用于在Channel和ChannelHandlerContext之间存储和共享数据。每个AttributeKey对象都对应一个唯一的key值。\n\nAttributeMap是一个接口，表示一个存储Attribute对象的容器。它是一个可扩展的接口，定义了一些常用的方法，如添加、获取和删除Attribute对象等。在Netty中，Channel和ChannelHandlerContext都实现了AttributeMap接口，因此都可以用于存储和共享数据。\n\nAttribute是一个类，它是AttributeMap中的一个元素。每个Attribute对象都与一个唯一的AttributeKey相关联，可以用于在Channel和ChannelHandlerContext之间存储和共享数据。在Netty中，Attribute对象通过AttributeMap接口的方法进行添加、获取和删除等操作。\n\n通过使用AttributeKey、AttributeMap和Attribute，Netty提供了一种方便的机制，可以在不同的Channel和ChannelHandlerContext之间共享数据，实现了数据在不同组件之间的传递和共享。\n\n## netty的fastThreadLocal fast在什么地方\n在Netty中，FastThreadLocal是一个高性能的ThreadLocal实现，其中的Fast表示其具有较高的性能。\n\n与JDK提供的ThreadLocal相比，FastThreadLocal具有更快的访问速度和更低的内存占用，这是由于它的底层实现方式不同。JDK的ThreadLocal底层使用的是Map数据结构，而FastThreadLocal则使用的是数组，通过使用数组可以避免Map的开销。\n\n在FastThreadLocal的底层实现中，每个线程都有自己的数组，这些数组中存储了该线程对应的FastThreadLocal变量的值。这样，在访问线程局部变量时，不需要进行同步操作，从而提高了访问的性能。\n\n因此，FastThreadLocal在Netty中被称为“Fast”，因为它能够提供更快的线程局部变量的访问速度和更低的内存占用，从而提高了Netty的性能表现。\n\n## guava和netty的异步回调模式\nGuava和Netty都提供了异步回调模式的支持。\n\n在Guava中，异步回调模式的核心是ListenableFuture接口，它表示一个异步操作的结果，并且可以通过注册回调函数的方式获取异步操作的结果。ListenableFuture是一个可监听的Future，可以在异步操作完成后触发回调函数，从而实现异步回调的功能。\n\n在Netty中，异步回调模式的核心是ChannelFuture接口，它表示一个I/O操作的结果，并且可以通过注册回调函数的方式获取I/O操作的结果。ChannelFuture也是一个可监听的Future，可以在I/O操作完成后触发回调函数，从而实现异步回调的功能。\n\n虽然Guava和Netty都提供了异步回调模式的支持，但它们在实现上有一些不同。Guava的异步回调模式更加通用，可以应用于任何异步操作，而Netty的异步回调模式则是针对I/O操作的，因此更加专业化。\n\n此外，在使用上，Guava的异步回调模式可以通过Futures.addCallback()方法注册回调函数，而Netty的异步回调模式则可以通过ChannelFuture.addListener()方法注册回调函数。\n\n## handler是怎么连接起来的\n在Netty中，Handler是事件处理器，用于处理与Channel相关的事件。多个Handler被组成一个ChannelPipeline，构成事件处理器链。ChannelPipeline是Netty中处理事件的核心组件之一，通过它将多个Handler串联在一起，形成一个处理事件的链条。每个Channel都有一个ChannelPipeline。\n\n当一个Channel的I/O事件发生时，事件将被传递到ChannelPipeline中，从而被事件处理器链中的某个Handler进行处理。事件处理器链中的每个Handler都可以决定将事件传递到下一个Handler，或者将事件截止在当前Handler。这个过程类似于Java中的责任链模式，每个Handler负责处理一部分事件，然后将其传递给下一个Handler。\n\n在Netty中，ChannelPipeline中的Handler是有序的，每个Handler都有自己的名字（name），通过名字可以唯一标识一个Handler。当一个新的Handler被加入到ChannelPipeline中时，它会被插入到ChannelPipeline的指定位置。ChannelPipeline会维护Handler的顺序，并保证事件按照顺序依次被处理。\n\n在创建ChannelPipeline时，可以通过addFirst()、addLast()、addBefore()、addAfter()等方法来添加Handler，这些方法会根据不同的位置要求将Handler插入到事件处理器链的不同位置。\n\n在ChannelPipeline中，Handler之间的连接是通过ChannelHandlerContext实现的。ChannelHandlerContext是ChannelPipeline中的一个上下文对象，它维护了ChannelPipeline中的一些状态信息，以及当前Handler在ChannelPipeline中的位置。当一个Handler需要将事件传递给下一个Handler时，它会通过ChannelHandlerContext来获得下一个Handler的引用，从而完成事件的传递。因此，ChannelHandlerContext在事件处理器链中扮演了一个关键的角色，它将不同的Handler连接起来，形成一个处理事件的链条。\n\n# 参考文章\n- https://blog.csdn.net/chenssy/article/details/78703551\n- https://www.baiyp.ren/Linux%E7%BA%BF%E7%A8%8B%E6%A8%A1%E5%9E%8B.html\n- https://cloud.tencent.com/developer/article/1754078"
  },
  {
    "path": "README.md",
    "content": "<h1 align=\"center\">JavaDeveloperBrain</h1>\n\n<div align=\"center\">\n\n[comment]: <> ([![GitHub issues]&#40;https://img.shields.io/github/issues/Swayingleaves/JavaDeveloperBrain?style=for-the-badge&#41;]&#40;https://github.com/Swayingleaves/JavaDeveloperBrain/issues&#41;)\n[![GitHub forks](https://img.shields.io/github/forks/Swayingleaves/JavaDeveloperBrain?style=for-the-badge)](https://github.com/Swayingleaves/JavaDeveloperBrain/network)\n[![GitHub stars](https://img.shields.io/github/stars/Swayingleaves/JavaDeveloperBrain?style=for-the-badge)](https://github.com/Swayingleaves/JavaDeveloperBrain/stargazers)\n![Java进阶](https://img.shields.io/badge/JAVA-%E5%9F%BA%E7%A1%80%2F%E8%BF%9B%E9%98%B6-green?style=for-the-badge)\n\n</div>\n\n<p align=\"center\">[Java工程师必备+学习+知识点+面试]：包含计算机网络知识、JavaSE、JVM、Spring、Springboot、SpringCloud、Mybatis、多线程并发、netty、MySQL、MongoDB、Elasticsearch、Redis、HBASE、RabbitMQ、RocketMQ、Pulsar、Kafka、Zookeeper、Linux、设计模式、智力题、项目架构、分布式相关、算法、面试题</p>\n\n---\n\n\n<h3 align=\"center\">最近更新: <a href=\"数据结构和算法/hotcode.md\">热门算法top100</a></h3>\n\n<h4 align=\"center\">:star2:<a href=\"TODO.md\">TODO list</a>:star2:</h4>\n\n\n# 内容概览[↓↓](#最后)\n\n<table>\n<thead>\n\n</thead>\n<tbody>\n  <tr>\n    <th rowspan=\"1\"><b>Java</b><a href=\"#java-基础部分\">↓↓</a></th>\n    <td ><a href=\"#java-基础部分\">基础部分</a></td>\n    <td ><a href=\"#java-jvm\">JVM</a></td>\n    <td ><a href=\"#java-多线程\">多线程</a></td>\n    <td ></td>\n    <td ></td>\n  </tr>\n  <tr>\n    <th  rowspan=\"2\"><b>计算机网络</b><a href=\"#计算机网络\">↓↓</a></th>\n    <td ><a href=\"#计算机网络\">网络协议分层</a></td>\n    <td ><a href=\"#tcp报文\">TCP</a></td>\n    <td ><a href=\"#UDP报文\">UDP</a></td>\n    <td ><a href=\"#三次握手\">三次握手</a></td>\n    <td ><a href=\"#四次挥手\">四次挥手</a></td>\n  </tr>\n  <tr>\n    <td ><a href=\"#TCP怎么保障可靠传输\">TCP怎么保障可靠传输</a></td>\n    <td ><a href=\"#HTTPS\">HTTPS</a></td>\n    <td ><a href=\"#HTTP面试题\">HTTP面试题</a></td>\n    <td ><a href=\"#TLS\">TLS</a></td>\n    <td ></td>\n    <td ></td>\n  </tr>\n  <tr>\n    <th rowspan=\"2\"><b>数据库</b><a href=\"#数据库\">↓↓</a></th>\n    <td ><a href=\"#mysql\">MySQL</a></td>\n    <td ><a href=\"#mongodb\">MongoDB</a></td>\n    <td ><a href=\"#hbase\">HBASE</a></td>\n    <td ><a href=\"#nebula-graph\">NebulaGraph</a></td>\n    <td ><a href=\"#elasticsearch\">Elasticsearch</a></td>\n  </tr>\n  <tr>\n    <td ><a href=\"#redis-1\">Redis</a></td>\n    <td ><a href=\"#sql问题\">SQL问题</a></td>\n    <td ></td>\n    <td ></td>\n    <td ></td>\n  </tr>\n  <tr>\n    <th rowspan=\"2\"><b>消息队列</b><a href=\"#消息队列\">↓↓</a></th>\n    <td ><a href=\"#redis\">Redis</a></td>\n    <td ><a href=\"#rabbitmq\">RabbitMQ</a></td>\n    <td ><a href=\"#rocketmq\">RocketMQ</a></td>\n    <td ><a href=\"#kafka\">Kafka</a></td>\n    <td ><a href=\"#zookeeper\">Zookeeper</a></td>\n  </tr>\n  <tr>\n    <td ><a href=\"#pulsar\">Pulsar</a></td>\n    <td ></td>\n    <td ></td>\n    <td ></td>\n    <td ></td>\n  </tr>\n\n  <tr>\n    <th rowspan=\"2\"><b>框架</b><a href=\"#框架\">↓↓</a></th>\n    <td ><a href=\"#spring\">Spring</a></td>\n    <td ><a href=\"#springmvc\">SpringMVC</a></td>\n    <td ><a href=\"#springboot\">SpringBoot</a></td>\n    <td ><a href=\"#springcloud\">SpringCloud</a></td>\n    <td ><a href=\"#springcloudalibaba\">SpringCloudAlibaba</a></td>\n  </tr>\n <tr>\n    <td ><a href=\"#mybatis\">Mybatis</a></td>\n    <td ><a href=\"#netty\">Netty</a></td>\n    <td ></td>\n    <td ></td>\n    <td ></td>\n  </tr>\n  <tr>\n    <th ><b>Linux</b><a href=\"#linux\">↓↓</a></th>\n    <td ><a href=\"#linux的进程线程文件描述符是什么\">进程-线程-文件描述符</a></td>\n    <td ><a href=\"#IO模型\">IO模型</a></td>\n    <td ><a href=\"#selectpollepoll\">select-poll-epoll</a></td>\n    <td ></td>\n    <td ></td>\n  </tr>\n  <tr>\n    <th ><b>分布式相关</b><a href=\"#分布式相关\">↓↓</a></th>\n    <td ><a href=\"#分布式锁\">分布式锁</a></td>\n    <td ><a href=\"#分布式事务\">分布式事务</a></td>\n    <td ><a href=\"#分布式唯一ID设计\">分布式唯一ID设计</a></td>\n    <td ><a href=\"#CAP理论\">CAP理论</a></td>\n    <td ><a href=\"#一致性算法\">一致性算法</a></td>\n  </tr>\n  <tr>\n    <th rowspan=\"1\"><b>架构</b><a href=\"#架构\">↓↓</a></th>\n    <td ><a href=\"#系统设计\">系统设计</a></td>\n    <td ><a href=\"#计算和储存分离\">计算和储存分离</a></td>\n    <td ><a href=\"#DDD领域驱动设计\">DDD领域驱动设计</a></td>\n    <td ></td>\n    <td ></td>\n  </tr>\n  <tr>\n    <th rowspan=\"1\"><b>容器技术</b><a href=\"#容器技术\">↓↓</a></th>\n    <td ><a href=\"#docker\">Docker</a></td>\n    <td ><a href=\"#kubernetes\">Kubernetes</a></td>\n    <td ></td>\n    <td ></td>\n    <td ></td>\n  </tr>\n  <tr>\n    <th rowspan=\"3\"><b>数据结构和算法</b><a href=\"#数据结构和算法\">↓↓</a></th>\n    <td ><a href=\"#排序算法\">排序算法</a></td>\n    <td ><a href=\"#树相关\">树相关</a></td>\n    <td ><a href=\"#BFS\">BFS</a></td>\n    <td ><a href=\"#DFS\">DFS</a></td>\n    <td ><a href=\"#回溯算法\">回溯算法</a></td>\n  </tr>\n    <tr>\n        <td ><a href=\"#二分法\">二分法</a></td>\n        <td ><a href=\"#贪心算法\">贪心算法</a></td>\n        <td ><a href=\"#动态规划\">动态规划</a></td>\n        <td ><a href=\"#分治思想\">分治思想</a></td>\n        <td ><a href=\"#数据结构和算法\">leetcode_top_150</a></td>\n    </tr>\n    <tr>\n        <td ><a href=\"#数据结构和算法\">热门常考算法</a></td>\n        <td ></td>\n        <td ></td>\n        <td ></td>\n        <td ></td>\n    </tr>\n  <tr>\n    <th rowspan=\"1\"><b>设计模式</b><a href=\"#设计模式\">↓↓</a></th>\n    <td ></td>\n    <td ></td>\n    <td ></td>\n    <td ></td>\n    <td ></td>\n  </tr>\n  <tr>\n    <th rowspan=\"1\"><b>大数据</b><a href=\"#大数据\">↓↓</a></th>\n    <td ><a href=\"#hadoop\">Hadoop</a></td>\n    <td ><a href=\"#hive\">Hive</a></td>\n    <td ><a href=\"#spark\">Spark</a></td>\n    <td ><a href=\"#flink\">Flink</a></td>\n    <td ></td>\n  </tr>\n  <tr>\n    <th rowspan=\"1\"><b>面试</b><a href=\"#面试解答\">↓↓</a></th>\n    <td ><a href=\"#职业规划和学习习惯\">职业规划和学习习惯</a></td>\n    <td ><a href=\"#场景设计\">场景设计</a></td>\n    <td ><a href=\"#智力题\">智力题</a></td>\n    <td ><a href=\"#面试解答\">面试解答</a></td>\n    <td ><a href=\"#商城类问题\">商城类问题</a></td>\n  </tr>\n</tbody>\n</table>\n\n# 内容详情\n\n## <a>Java-基础部分</a>[↑↑](#内容概览)\n\n- [基本类型](Java-基础/Java类型.md)\n- [包装类型](Java-基础/Java类型.md)\n- [关键字](Java-基础/Java关键字.md)\n- [Object](Java-基础/Object.md)\n- [String](Java-基础/String.md)\n- [数组](Java-基础/数组.md)\n- [继承](Java-基础/继承.md)\n- [反射](Java-基础/反射.md)\n- [异常](Java-基础/异常.md)\n- [泛型](Java-基础/泛型.md)\n- 容器\n    - [List](Java-基础/容器-collection.md#list)\n        - [Vector](Java-基础/容器-collection.md#vector)\n        - [LinkedList](Java-基础/容器-collection.md#linkedlist)\n        - [ArrayList](Java-基础/容器-collection.md#arraylist)\n        - [CopyOnWriteArrayList](Java-基础/容器-collection.md#copyonwritearraylist)\n    - [Set](Java-基础/容器-collection.md#set)\n        - [HashSet](Java-基础/容器-collection.md#hashset)\n        - [LinkedHashSet](Java-基础/容器-collection.md#linkedhashset)\n        - [TreeSet](Java-基础/容器-collection.md#treeset)\n    - [queue](Java-基础/容器-collection.md#queue)\n    - [Map](Java-基础/容器-map.md#map)\n        - [HashMap](Java-基础/容器-map.md#hashmap)\n        - [LinkedHashMap](Java-基础/容器-map.md#linkedhashmap)\n        - [TreeMap](Java-基础/容器-map.md#treemap)\n        - [ConcurrentHashMap](Java-基础/容器-map.md#concurrenthashmap)\n        - [IdentityHashMap](Java-基础/容器-map.md#identityhashmap)\n        - [WeakHashMap](Java-基础/容器-map.md#weakhashmap)\n- [Java-IO](Java-基础/JavaIO.md)\n    - [文件io](Java-基础/JavaIO.md#文件io)\n    - [网络io](Java-基础/JavaIO.md#网络io)\n    - [NIO](Java-基础/JavaIO.md#nio)\n- [Java长期支持版本新特性](Java-基础/Java长期支持版本.md)\n- [Java虚拟线程-预览](Java-基础/虚拟线程.md)\n- [Java虚拟线程-jdk21正式发布](Java-基础/虚拟线程2.md)\n\n## <a>Java-JVM</a>[↑↑](#内容概览)\n\n- [内存结构](Java-JVM/内存结构.md)\n    - [程序计数器](Java-JVM/内存结构.md#程序计数器)\n    - [Java虚拟机栈](Java-JVM/内存结构.md#java虚拟机栈)\n    - [本地方法栈](Java-JVM/内存结构.md#本地方法栈)\n    - [堆](Java-JVM/内存结构.md#堆)\n    - [方法区](Java-JVM/内存结构.md#方法区)\n    - [运行时常量池](Java-JVM/内存结构.md#运行时常量池)\n    - [直接内存](Java-JVM/内存结构.md#直接内存)\n- [垃圾回收](Java-JVM/垃圾回收.md)\n    - [判断一个对象能否被回收](Java-JVM/垃圾回收.md#判断一个对象能否被回收)\n        - [引用计数法](Java-JVM/垃圾回收.md#引用计数法)\n        - [可达性算法](Java-JVM/垃圾回收.md#可达性算法)\n    - [哪些可以作为是根节点](Java-JVM/垃圾回收.md#哪些可以作为是根节点)\n\n    * [方法区的回收](Java-JVM/垃圾回收.md#方法区的回收)\n    * [finalize()](Java-JVM/垃圾回收.md#finalize)\n    * [引用类型](Java-JVM/垃圾回收.md#引用类型)\n        * [强引用](Java-JVM/垃圾回收.md#强引用)\n        * [软引用](Java-JVM/垃圾回收.md#软引用)\n        * [弱引用](Java-JVM/垃圾回收.md#弱引用)\n        * [虚引用](Java-JVM/垃圾回收.md#虚引用)\n\n    - [分代收集理论](Java-JVM/垃圾回收.md#分代收集理论)\n    - [Java对象头](Java-JVM/Java对象头.md)\n    - [GC定义](Java-JVM/垃圾回收.md#gc定义)\n        - [新生代收集（Minor GC/Young GC）](Java-JVM/垃圾回收.md#新生代收集minor-gcyoung-gc)\n        - [老年代收集（Major GC/Old GC）](Java-JVM/垃圾回收.md#老年代收集major-gcold-gc)\n        - [混合收集（Mixed GC）](Java-JVM/垃圾回收.md#混合收集mixed-gc)\n        - [整堆收集（Full GC）](Java-JVM/垃圾回收.md#整堆收集full-gc)\n    - [回收算法](Java-JVM/垃圾回收.md#回收算法)\n        - [标记-清除](Java-JVM/垃圾回收.md#标记-清除)\n        - [标记-复制](Java-JVM/垃圾回收.md#标记-复制)\n        - [标记-整理](Java-JVM/垃圾回收.md#标记-整理)\n    - [Hotspot算法实现细节](Java-JVM/垃圾回收.md#hotspot算法实现细节)\n        - [根节点枚举GC Roots](Java-JVM/垃圾回收.md#根节点枚举gc-roots)\n        - [安全点Safe Point](Java-JVM/垃圾回收.md#安全点safe-point)\n        - [安全区域Safe Region](Java-JVM/垃圾回收.md#安全区域safe-region)\n        - [记忆集Remembered Set与卡表Card Table](Java-JVM/垃圾回收.md#记忆集remembered-set与卡表card-table)\n        - [写屏障Write Barrier](Java-JVM/垃圾回收.md#写屏障write-barrier)\n    - [并发的可达性分析](Java-JVM/垃圾回收.md#并发的可达性分析)\n        - [为什么需要并发标记](Java-JVM/垃圾回收.md#为什么需要并发标记)\n        - [三色标记Tri-color Marking](Java-JVM/垃圾回收.md#三色标记tri-color-marking)\n            - [什么是三色标记](Java-JVM/垃圾回收.md#什么是三色标记)\n    - [垃圾回收器](Java-JVM/垃圾回收.md#垃圾回收器)\n        - [Serial收集器](Java-JVM/垃圾回收.md#serial收集器)\n        - [ParNew收集器](Java-JVM/垃圾回收.md#parnew收集器)\n        - [ParallelScavenge收集器](Java-JVM/垃圾回收.md#parallelscavenge收集器)\n        - [SerialOld收集器](Java-JVM/垃圾回收.md#serialold收集器)\n        - [ParallelOld收集器](Java-JVM/垃圾回收.md#parallelold收集器)\n        - [CMS收集器](Java-JVM/垃圾回收.md#cms收集器)\n        - [G1收集器](Java-JVM/垃圾回收.md#g1收集器)\n        - [ZGC收集器](Java-JVM/垃圾回收.md#zgc收集器)\n- [内存分配与回收策略](Java-JVM/内存分配与回收策略.md)\n    - [对象优先在 Eden 分配](Java-JVM/内存分配与回收策略.md#对象优先在-eden-分配)\n    - [大对象直接进入老年代](Java-JVM/内存分配与回收策略.md#大对象直接进入老年代)\n    - [长期存活的对象进入老年代](Java-JVM/内存分配与回收策略.md#长期存活的对象进入老年代)\n    - [动态对象年龄判定](Java-JVM/内存分配与回收策略.md#动态对象年龄判定)\n    - [空间分配担保](Java-JVM/内存分配与回收策略.md#空间分配担保)\n- [类加载机制](Java-JVM/类加载机制.md)\n    - [有哪些类加载器](Java-JVM/类加载机制.md#有哪些类加载器)\n    - [生命周期](Java-JVM/类加载机制.md#生命周期)\n    - [双亲委派模型](Java-JVM/类加载机制.md#双亲委派模型)\n- [JVM调优](Java-JVM/JVM调优.md)\n    - [原则](Java-JVM/JVM调优.md#原则)\n    - [jvm调优](Java-JVM/JVM调优.md#jvm调优)\n    - [JVM调优目标](Java-JVM/JVM调优.md#jvm调优目标)\n    - [JVM调优的步骤](Java-JVM/JVM调优.md#jvm调优的步骤)\n    - [JVM参数解析及调优](Java-JVM/JVM调优.md#jvm参数解析及调优)\n- [Java即时编译](Java-JVM/Java即时编译.md)\n    - [什么是](Java-JVM/Java即时编译.md#什么是)\n    - [Java的执行过程](Java-JVM/Java即时编译.md#java的执行过程)\n    - [1.JVM中的编译器](Java-JVM/Java即时编译.md#1jvm中的编译器)\n    - [2.分层编译](Java-JVM/Java即时编译.md#2分层编译)\n    - [3.即时编译的触发](Java-JVM/Java即时编译.md#3即时编译的触发)\n    - [编译优化](Java-JVM/Java即时编译.md#编译优化)\n        - [1. 中间表达形式（Intermediate Representation）](Java-JVM/Java即时编译.md#1-中间表达形式intermediate-representation)\n        - [2.方法内联](Java-JVM/Java即时编译.md#2方法内联)\n        - [3. 逃逸分析](Java-JVM/Java即时编译.md#3-逃逸分析)\n        - [4. Loop Transformations](Java-JVM/Java即时编译.md#4-loop-transformations)\n        - [5. 窥孔优化与寄存器分配](Java-JVM/Java即时编译.md#5-窥孔优化与寄存器分配)\n\n## <a>Java-多线程</a>[↑↑](#内容概览)\n\n- [线程](Java-多线程/线程.md)\n    - [线程的生命状态](Java-多线程/线程.md#线程的生命状态)\n        - [新建new](Java-多线程/线程.md#新建new)\n        - [可运行runnable](Java-多线程/线程.md#可运行runnable)\n        - [阻塞blocked](Java-多线程/线程.md#阻塞blocked)\n        - [等待waiting](Java-多线程/线程.md#等待waiting)\n        - [期限等待timed waiting](Java-多线程/线程.md#期限等待timed-waiting)\n        - [死亡terminated](Java-多线程/线程.md#死亡terminated)\n    - [使用线程](Java-多线程/线程.md#使用线程)\n        * [继承Thread](Java-多线程/线程.md#继承thread)\n        * [实现Runnable接口](Java-多线程/线程.md#实现runnable接口)\n        * [实现Callable接口](Java-多线程/线程.md#实现callable接口)\n        * [Callable如何返回值的](Java-多线程/线程.md#callable如何返回值的)\n        * [FutureTask](Java-多线程/线程.md#futuretask)\n\n    * [线程基本方法](Java-多线程/线程.md#线程基本方法)\n        * [wait](Java-多线程/线程.md#wait)\n        * [sleep](Java-多线程/线程.md#sleep)\n        * [yield](Java-多线程/线程.md#yield)\n        * [interrupt](Java-多线程/线程.md#interrupt)\n        * [join](Java-多线程/线程.md#join)\n        * [notify](Java-多线程/线程.md#notify)\n        * [await() signal() signalAll()](Java-多线程/线程.md#await-signal-signalall)\n    * [Java里怎么保证多个线程的互斥性](Java-多线程/线程.md#java里怎么保证多个线程的互斥性)\n    * [线程和进程的区别](Java-多线程/线程.md#线程和进程的区别)\n    * [怎么让多个线程有序执行](Java-多线程/线程.md#怎么让多个线程有序执行)\n        * [join方法](Java-多线程/线程.md#join方法)\n        * [线程池](Java-多线程/线程.md#线程池)\n        * [lock-condition](Java-多线程/线程.md#lock-condition)\n    * [Java线程和操作系统的线程区别](Java-多线程/线程.md#java线程和操作系统的线程区别)\n        * [Java线程在操作系统上本质](Java-多线程/线程.md#java线程在操作系统上本质)\n        * [操作系统中的进程（线程）状态](Java-多线程/线程.md#操作系统中的进程线程状态)\n        * [操作系统中线程和Java线程状态的关系](Java-多线程/线程.md#操作系统中线程和java线程状态的关系)\n- [volatile](Java-多线程/volatile.md)\n    * [机器内存模型](Java-多线程/volatile.md#机器内存模型)\n        * [多核下的缓存一致性问题](Java-多线程/volatile.md#多核下的缓存一致性问题)\n        * [指令重排](Java-多线程/volatile.md#指令重排)\n    * [JMM](Java-多线程/volatile.md#jmm)\n    * [作用](Java-多线程/volatile.md#作用)\n        * [可见性](Java-多线程/volatile.md#可见性)\n        * [禁止指令重排](Java-多线程/volatile.md#禁止指令重排)\n    * [volatile解决可见性的代码](Java-多线程/volatile.md#volatile解决可见性的代码)\n    * [验证volatile不具备原子性](Java-多线程/volatile.md#验证volatile不具备原子性)\n- [Java对象头](Java-多线程/Java对象头.md)\n- [锁机制](Java-多线程/锁机制.md)\n    * [Synchronized](Java-多线程/锁机制.md#synchronized)\n    * [Lock](Java-多线程/锁机制.md#lock)\n        * [ReentrantLock](Java-多线程/锁机制.md#reentrantlock)\n    * [锁优化](Java-多线程/锁机制.md#锁优化)\n        * [自旋锁](Java-多线程/锁机制.md#自旋锁)\n        * [循环](Java-多线程/锁机制.md#循环)\n        * [锁消除](Java-多线程/锁机制.md#锁消除)\n        * [锁粗化](Java-多线程/锁机制.md#锁粗化)\n        * [锁升级](Java-多线程/锁机制.md#锁升级)\n    * [死锁](Java-多线程/锁机制.md#死锁)\n    * [synchronized锁和lock锁的区别](Java-多线程/锁机制.md#synchronized锁和lock锁的区别)\n- [线程池](Java-多线程/线程池.md)\n    * [创建线程池的方式](Java-多线程/线程池.md#创建线程池的方式)\n    * [ThreadPoolExecutor](Java-多线程/线程池.md#threadpoolexecutor-1)\n    * [ThreadPoolExecutor原理流程](Java-多线程/线程池.md#threadpoolexecutor原理流程)\n    * [如何释放线程](Java-多线程/线程池.md#如何释放线程)\n    * [如何设置线程数](Java-多线程/线程池.md#如何设置线程数)\n- [CAS](Java-多线程/CAS.md)\n    * [原理](Java-多线程/CAS.md#原理)\n    * [参数](Java-多线程/CAS.md#参数)\n    * [CAS的问题](Java-多线程/CAS.md#cas的问题)\n- [AQS](Java-多线程/AQS.md)\n    * [AQS（AbstractQueuedSynchronizer）](Java-多线程/AQS.md#aqsabstractqueuedsynchronizer)\n        * [工作原理概要](Java-多线程/AQS.md#工作原理概要)\n        * [同步队列模型](Java-多线程/AQS.md#同步队列模型)\n    * [ReentrantLock](Java-多线程/AQS.md#reentrantlock)\n        * [Sync (extends AbstractQueuedSynchronizer)](Java-多线程/AQS.md#sync-extends-abstractqueuedsynchronizer)\n        * [NonfairSync (extends Sync) 非公平锁](Java-多线程/AQS.md#nonfairsync-extends-sync-非公平锁)\n        * [FairSync (extends Sync) 公平锁](Java-多线程/AQS.md#fairsync-extends-sync-公平锁)\n        * [Condition](Java-多线程/AQS.md#condition)\n        * [同步工具类](Java-多线程/AQS.md#同步工具类)\n- [ThreadLocal](Java-多线程/ThreadLocal.md)\n    * [原理](Java-多线程/ThreadLocal.md#原理)\n    * [ThreadLocalMap](Java-多线程/ThreadLocal.md#threadlocalmap)\n    * [源码分析](Java-多线程/ThreadLocal.md#源码分析)\n    * [使用场景](Java-多线程/ThreadLocal.md#使用场景)\n    * [手动释放ThreadLocal遗留存储?你怎么去设计/实现？](Java-多线程/ThreadLocal.md#手动释放threadlocal遗留存储你怎么去设计实现)\n    * [弱引用导致内存泄漏，那为什么key不设置为强引用](Java-多线程/ThreadLocal.md#弱引用导致内存泄漏那为什么key不设置为强引用)\n    * [线程执行结束后会不会自动清空Entry的value](Java-多线程/ThreadLocal.md#线程执行结束后会不会自动清空entry的value)\n    * [threadlocal如果不remove，出问题了怎么补救？](Java-多线程/ThreadLocal.md#threadlocal如果不remove出问题了怎么补救)\n    * [FastThreadLocal](Java-多线程/ThreadLocal.md#fastthreadlocal)\n\n## <a>计算机网络</a>[↑↑](#内容概览)\n\n- [网络协议分层](计算机网络/网络协议分层.md)\n    * [OSI 7层(基本只是拿来作比较)](计算机网络/网络协议分层.md#osi-7层基本只是拿来作比较)\n    * [TCP/IP 5(4)层](计算机网络/网络协议分层.md#tcpip-54层)\n- #### <a href=\"计算机网络/TCP报文.md\">TCP报文</a>\n- #### <a href=\"计算机网络/UDP报文.md\">UDP报文</a>\n- [IP报文](计算机网络/IP报文.md)\n- [TCP/IP](计算机网络/TCP_IP.md)\n    * [UDP 和 TCP 的特点](计算机网络/TCP_IP.md#udp-和-tcp-的特点)\n        * [UDP](计算机网络/TCP_IP.md#udp)\n        * [TCP](计算机网络/TCP_IP.md#tcp)\n\n    * #### <a href=\"计算机网络/TCP_IP.md#三次握手\">三次握手</a>\n    * #### <a href=\"计算机网络/TCP_IP.md#四次挥手\">四次挥手</a>\n    * #### <a href=\"计算机网络/TCP_IP.md#tcp怎么保障可靠传输\">TCP怎么保障可靠传输</a>\n        * [数据合理分片和排序](计算机网络/TCP_IP.md#数据合理分片和排序)\n        * [数据校验：校验和](计算机网络/TCP_IP.md#数据校验校验和)\n        * [TCP 的接收端会丢弃重复的数据](计算机网络/TCP_IP.md#tcp-的接收端会丢弃重复的数据)\n        * [超时重传](计算机网络/TCP_IP.md#超时重传)\n        * [流量控制](计算机网络/TCP_IP.md#流量控制)\n        * [拥塞控制](计算机网络/TCP_IP.md#拥塞控制)\n        * [ARQ协议](计算机网络/TCP_IP.md#arq协议)\n    * [如何实现可靠UDP传输](计算机网络/TCP_IP.md#如何实现可靠udp传输)\n    * [HTTP长连接还是短连接？](计算机网络/TCP_IP.md#http长连接还是短连接)\n- [HTTP](计算机网络/HTTP.md)\n    * [特点](计算机网络/HTTP.md#特点)\n    * [方法](计算机网络/HTTP.md#方法)\n    * [状态码](计算机网络/HTTP.md#状态码)\n    * #### <a href=\"计算机网络/HTTP.md#https\">HTTPS</a>\n        * [什么是HTTPS](计算机网络/HTTP.md#什么是https)\n        * [HTTPS解决的问题](计算机网络/HTTP.md#https解决的问题)\n        * [HTTPS加密过程](计算机网络/HTTP.md#https加密过程)\n    * [HTTPS的CA证书放了什么，公钥放在CA里吗？](计算机网络/HTTP.md#https的ca证书放了什么公钥放在ca里吗)\n    * [CA证书是在客户端还是服务器](计算机网络/HTTP.md#ca证书是在客户端还是服务器)\n    * [TLS](计算机网络/TLS.md)\n    * [HTTP1.1和HTTP1.0的主要区别](计算机网络/HTTP.md#http11和http10的主要区别)\n    * [HTTP2.0和HTTP1.x的区别](计算机网络/HTTP.md#http20和http1x的区别)\n    * [HTTP的request和response格式](计算机网络/HTTP.md#http的request和response格式)\n- [cookie](计算机网络/cookie和session.md)\n- [session](计算机网络/cookie和session.md)\n- [JWT](计算机网络/JWT.md)\n- [跨域](计算机网络/跨域.md)\n- [网络攻击行为](计算机网络/网络攻击行为.md)\n    * [CSRF攻击](计算机网络/网络攻击行为.md#csrf攻击)\n    * [XSS](计算机网络/网络攻击行为.md#xss)\n    * [SQL注入](计算机网络/网络攻击行为.md#sql注入)\n    * [DDOS](计算机网络/网络攻击行为.md#ddos)\n    * [SYN Flood攻击](计算机网络/网络攻击行为.md#syn-flood攻击)\n- [CDN](计算机网络/CDN.md)\n- #### <a href=\"计算机网络/HTTP面试题.md\">HTTP面试题</a>\n    * [在浏览器中输入url地址显示主页的过程](计算机网络/HTTP面试题.md#在浏览器中输入url地址显示主页的过程)\n    * [QPS和TPS的区别](计算机网络/HTTP面试题.md#qps和tps的区别)\n    * [有哪些编码格式(GBK,UTF-8,ISO-)有没有想过为什么会有这么多的编码格式](计算机网络/HTTP面试题.md#有哪些编码格式gbkutf-8iso-有没有想过为什么会有这么多的编码格式)\n    * [实现一个长URL转短URL](计算机网络/HTTP面试题.md#实现一个长url转短url)\n\n## <a>数据库</a>[↑↑](#内容概览)\n\n### MySQL[↑↑](#内容概览)\n\n- [MySQL](数据库/MySQL.md)\n    - [架构](数据库/MySQL.md#架构)\n    - [储存引擎](数据库/MySQL.md#储存引擎-1)\n        - [InnoDB](数据库/MySQL.md#innodb)\n        - [MyISAM](数据库/MySQL.md#myisam)\n    - [索引](数据库/MySQL.md#索引)\n    - [事务](数据库/MySQL.md#事务)\n        - [ACID](数据库/MySQL.md#acid)\n            - [原子性（Atomicity，或称不可分割性）](数据库/MySQL.md#原子性atomicity或称不可分割性)\n            - [一致性（Consistency）](数据库/MySQL.md#一致性consistency)\n            - [隔离性（Isolation）](数据库/MySQL.md#隔离性isolation)\n            - [持久性（Durability）](数据库/MySQL.md#持久性durability)\n    - [事务日志](数据库/MySQL.md#事务日志)\n        - [redo log（重做日志）](数据库/MySQL.md#redo-log重做日志)\n        - [undo log（回滚日志）](数据库/MySQL.md#undo-log回滚日志)\n    - [二进制日志( binlog )](数据库/MySQL.md#二进制日志-binlog-)\n    - [锁](数据库/MySQL.md#锁)\n        - [行级锁](数据库/MySQL.md#行级锁)\n        - [表级锁](数据库/MySQL.md#表级锁)\n        - [页锁](数据库/MySQL.md#页锁)\n    - [切分](数据库/MySQL.md#切分)\n        - [水平切分](数据库/MySQL.md#水平切分)\n        - [垂直切分](数据库/MySQL.md#垂直切分)\n    - [复制](数据库/MySQL.md#复制)\n        - [主从复制](数据库/MySQL.md#主从复制)\n    - [中间件](数据库/MySQL.md#中间件)\n        - [mycat](数据库/MySQL.md#mycat)\n        - [ShardingSphere](数据库/MySQL.md#shardingsphere)\n    - [SQL优化](数据库/MySQL.md#sql优化)\n    - [MySQL与PostGreSQL的区别](数据库/MySQL.md#mysql与postgresql的区别)\n\n### SQL问题\n\n* [count(*)分页](数据库/count.md)\n\n### MongoDB[↑↑](#内容概览)\n\n* [MongoDB](数据库/MongoDB.md#mongodb)\n* [特点](数据库/MongoDB.md#特点)\n* [关键组件](数据库/MongoDB.md#关键组件)\n* [单机mongo架构](数据库/MongoDB.md#单机mongo架构)\n* [集群模式1-MongoDB 复制（副本集）Replica set(主从关系)](数据库/MongoDB.md#集群模式1-mongodb-复制副本集replica-set主从关系)\n* [集群模式2-MongoDB 分片](数据库/MongoDB.md#集群模式2-mongodb-分片)\n* [WiredTiger存储引擎](数据库/MongoDB.md#wiredtiger存储引擎)\n\n### HBASE[↑↑](#内容概览)\n\n* [HBASE](数据库/Hbase.md#hbase)\n    * [什么是？](数据库/Hbase.md#什么是)\n    * [列式存储](数据库/Hbase.md#列式存储)\n    * [架构](数据库/Hbase.md#架构)\n        * [架构图](数据库/Hbase.md#架构图)\n* [HBase 架构组件](数据库/Hbase.md#hbase-架构组件)\n    * [Regions](数据库/Hbase.md#regions)\n    * [HBase Master](数据库/Hbase.md#hbase-master)\n    * [Zookeeper](数据库/Hbase.md#zookeeper)\n    * [HBase Meta Table](数据库/Hbase.md#hbase-meta-table)\n    * [Region Server 组成](数据库/Hbase.md#region-server-组成)\n    * [HBase 写数据步骤](数据库/Hbase.md#hbase-写数据步骤)\n    * [HBase MemStore](数据库/Hbase.md#hbase-memstore)\n    * [HBase Region Flush](数据库/Hbase.md#hbase-region-flush)\n    * [HBase HFile](数据库/Hbase.md#hbase-hfile)\n    * [HBase Read 合并](数据库/Hbase.md#hbase-read-合并)\n    * [HBase Minor Compaction](数据库/Hbase.md#hbase-minor-compaction)\n    * [HBase Major Compaction](数据库/Hbase.md#hbase-major-compaction)\n    * [Region = Contiguous Keys](数据库/Hbase.md#region--contiguous-keys)\n    * [Region 分裂](数据库/Hbase.md#region-分裂)\n    * [Region 负载均衡](数据库/Hbase.md#region-负载均衡)\n    * [HDFS 数据备份](数据库/Hbase.md#hdfs-数据备份)\n    * [HBase 故障恢复](数据库/Hbase.md#hbase-故障恢复)\n* [Apache HBase 架构的优缺点](数据库/Hbase.md#apache-hbase-架构的优缺点)\n    * [优点](数据库/Hbase.md#优点)\n    * [缺点](数据库/Hbase.md#缺点)\n\n### Elasticsearch[↑↑](#内容概览)\n\n* [Elasticsearch](数据库/Elasticsearch.md#elasticsearch)\n    * [es的特点](数据库/Elasticsearch.md#es的特点)\n    * [应用场景](数据库/Elasticsearch.md#应用场景)\n* [Elasticsearch基本概念](数据库/Elasticsearch.md#elasticsearch基本概念)\n    * [索引(index)](数据库/Elasticsearch.md#索引index)\n    * [类型(type)](数据库/Elasticsearch.md#类型type)\n    * [文档(document)](数据库/Elasticsearch.md#文档document)\n    * [映射(mapping)](数据库/Elasticsearch.md#映射mapping)\n    * [倒排索引](数据库/Elasticsearch.md#倒排索引)\n* [集群](数据库/Elasticsearch.md#集群)\n    * [基本概念](数据库/Elasticsearch.md#基本概念)\n        * [节点(Node)](数据库/Elasticsearch.md#节点node)\n        * [集群(Cluster)](数据库/Elasticsearch.md#集群cluster)\n        * [分片索引(Shard)](数据库/Elasticsearch.md#分片索引shard)\n        * [索引副本(Replica)](数据库/Elasticsearch.md#索引副本replica)\n    * [集群简单原理](数据库/Elasticsearch.md#集群简单原理)\n    * [插入数据流程](数据库/Elasticsearch.md#插入数据流程)\n    * [查询数据流程](数据库/Elasticsearch.md#查询数据流程)\n* [选举算法](数据库/Elasticsearch.md#选举算法)\n* [es性能优化](数据库/Elasticsearch.md#es性能优化)\n    * [加大filesystem cache大小](数据库/Elasticsearch.md#加大filesystem-cache大小)\n    * [数据预热](数据库/Elasticsearch.md#数据预热)\n    * [冷热分离](数据库/Elasticsearch.md#冷热分离)\n    * [document设计](数据库/Elasticsearch.md#document设计)\n    * [禁止直接分页](数据库/Elasticsearch.md#禁止直接分页)\n* [es的分词器有哪些](数据库/Elasticsearch.md#es的分词器有哪些)\n* [es为什么这么快](数据库/Elasticsearch.md#es为什么这么快)\n* [es 的分页方案](数据库/Elasticsearch.md#es的分页方案)\n* [es 的查询流程](数据库/Elasticsearch.md#es的查询流程)\n\n### Nebula Graph[↑↑](#内容概览)\n\n* [什么是Nebula Graph](数据库/Nebula.md#什么是nebula-graph)\n    * [什么是图数据库](数据库/Nebula.md#什么是图数据库)\n    * [Nebula Graph 的优势](数据库/Nebula.md#nebula-graph-的优势)\n* [数据模型](数据库/Nebula.md#数据模型)\n    * [数据模型](数据库/Nebula.md#数据模型-1)\n        * [图空间（Space）](数据库/Nebula.md#图空间space)\n        * [点（Vertex）](数据库/Nebula.md#点vertex)\n        * [边（Edge）](数据库/Nebula.md#边edge)\n        * [标签（Tag）](数据库/Nebula.md#标签tag)\n        * [边类型（Edge type）](数据库/Nebula.md#边类型edge-type)\n        * [属性（Properties）](数据库/Nebula.md#属性properties)\n    * [有向属性图](数据库/Nebula.md#有向属性图)\n* [路径](数据库/Nebula.md#路径)\n    * [walk](数据库/Nebula.md#walk)\n    * [trail](数据库/Nebula.md#trail)\n    * [path](数据库/Nebula.md#path)\n* [点 VID](数据库/Nebula.md#点-vid)\n* [服务架构](数据库/Nebula.md#服务架构)\n    * [架构总览](数据库/Nebula.md#架构总览)\n    * [Meta 服务](数据库/Nebula.md#meta-服务-1)\n    * [Graph服务](数据库/Nebula.md#graph服务)\n    * [Storage服务](数据库/Nebula.md#storage服务)\n\n## <a>消息队列</a>[↑↑](#内容概览)\n\n### Redis[↑↑](#内容概览)\n\n- [Redis](消息队列/Redis.md)\n\n### RabbitMQ[↑↑](#内容概览)\n\n- [RabbitMQ](消息队列/RabbitMQ.md)\n    * [概念介绍](消息队列/RabbitMQ.md#概念介绍)\n    * [架构图](消息队列/RabbitMQ.md#架构图)\n    * [exchange类型](消息队列/RabbitMQ.md#exchange类型)\n        * [Direct](消息队列/RabbitMQ.md#direct)\n        * [Fanout](消息队列/RabbitMQ.md#fanout)\n        * [Topic](消息队列/RabbitMQ.md#topic)\n    * [RabbitMQ 消息持久化](消息队列/RabbitMQ.md#rabbitmq-消息持久化)\n    * [集群](消息队列/RabbitMQ.md#集群)\n    * [交换器无法根据自身类型和路由键找到符合条件队列时，会如何处理？](消息队列/RabbitMQ.md#交换器无法根据自身类型和路由键找到符合条件队列时会如何处理)\n    * [RabbitMQ 的六种模式](消息队列/RabbitMQ.md#rabbitmq-的六种模式)\n    * [死信队列应用场景](消息队列/RabbitMQ.md#死信队列应用场景)\n    * [事务机制](消息队列/RabbitMQ.md#事务机制)\n    * [Confirm模式](消息队列/RabbitMQ.md#confirm模式)\n        * [producer端confirm模式的实现原理](消息队列/RabbitMQ.md#producer端confirm模式的实现原理)\n        * [开启confirm模式的方法](消息队列/RabbitMQ.md#开启confirm模式的方法)\n        * [编程模式](消息队列/RabbitMQ.md#编程模式)\n\n### RocketMQ[↑↑](#内容概览)\n\n- [RocketMQ](消息队列/RocketMQ.md#rocketmq)\n    * [架构图](消息队列/RocketMQ.md#架构图)\n    * [组件](消息队列/RocketMQ.md#组件)\n        * [NameServer](消息队列/RocketMQ.md#nameserver)\n        * [Broker](消息队列/RocketMQ.md#broker)\n        * [Producer](消息队列/RocketMQ.md#producer)\n        * [Consumer](消息队列/RocketMQ.md#consumer)\n    * [消息特性](消息队列/RocketMQ.md#消息特性)\n    * [消息功能](消息队列/RocketMQ.md#消息功能)\n    * [rocket的事务实现机制](消息队列/RocketMQ.md#rocket的事务实现机制)\n    * [Broker 集群部署架构](消息队列/RocketMQ.md#broker-集群部署架构)\n        * [多 Master 模式](消息队列/RocketMQ.md#多-master-模式)\n        * [多 Master 多 Salve - 异步复制 模式](消息队列/RocketMQ.md#多-master-多-salve---异步复制-模式)\n        * [多 Master 多 Salve - 同步双写 模式](消息队列/RocketMQ.md#多-master-多-salve---同步双写-模式)\n        * [Dledger 模式](消息队列/RocketMQ.md#dledger-模式)\n\n### Kafka[↑↑](#内容概览)\n\n- [Kafka](消息队列/Kafka.md)\n    * [架构图](消息队列/Kafka.md#架构图)\n    * [概念](消息队列/Kafka.md#概念)\n        * [topic](消息队列/Kafka.md#topic)\n        * [partition](消息队列/Kafka.md#partition)\n        * [segment](消息队列/Kafka.md#segment)\n        * [offset](消息队列/Kafka.md#offset)\n        * [broker](消息队列/Kafka.md#broker)\n        * [producer](消息队列/Kafka.md#producer)\n        * [consumer](消息队列/Kafka.md#consumer)\n    * [Kafka零拷贝](消息队列/Kafka.md#kafka零拷贝)\n    * [常见问题](消息队列/Kafka.md#常见问题)\n        * [kafka中zookeeper的作用](消息队列/Kafka.md#kafka中zookeeper的作用)\n        * [kafka的consumer是拉模式还是推模式](消息队列/Kafka.md#kafka的consumer是拉模式还是推模式)\n        * [kafka生产者丢消息情况](消息队列/Kafka.md#kafka生产者丢消息情况)\n        * [kafka消费者丢消息情况](消息队列/Kafka.md#kafka消费者丢消息情况)\n        * [Kafka如何保证高可用性](消息队列/Kafka.md#kafka如何保证高可用性)\n        * [Kafka的消息保存在哪里？Kafka的消息是如何分区的？](消息队列/Kafka.md#kafka的消息保存在哪里kafka的消息是如何分区的)\n        * [Kafka是如何处理流量峰值的？](消息队列/Kafka.md#kafka是如何处理流量峰值的)\n        * [Kafka消息压缩方式](消息队列/Kafka.md#kafka消息压缩方式)\n        * [Kafka的重平衡机制是什么？](消息队列/Kafka.md#kafka的重平衡机制是什么)\n        * [Kafka中的生产者和消费者是什么？Kafka是如何确保数据的顺序性和一致性的？](消息队列/Kafka.md#kafka中的生产者和消费者是什么kafka是如何确保数据的顺序性和一致性的)\n        * [kafka消费组怎么消费一个topic的数据](消息队列/Kafka.md#kafka消费组怎么消费一个topic的数据)\n        * [Kafka有哪些优缺点？](消息队列/Kafka.md#kafka有哪些优缺点)\n        * [Kafka的API是什么？如何使用Kafka API实现生产和消费？](消息队列/Kafka.md#kafka的api是什么如何使用kafka-api实现生产和消费)\n        * [如何部署和扩展Kafka集群？](消息队列/Kafka.md#如何部署和扩展kafka集群)\n\n### Zookeeper[↑↑](#内容概览)\n\n- [Zookeeper](消息队列/Zookeeper.md)\n    * [概念](消息队列/Zookeeper.md#概念)\n    * [用zookeeper可以干嘛](消息队列/Zookeeper.md#用zookeeper可以干嘛)\n    * [数据结构](消息队列/Zookeeper.md#数据结构)\n        * [ZNode](消息队列/Zookeeper.md#znode)\n    * [监听机制](消息队列/Zookeeper.md#监听机制)\n    * [角色](消息队列/Zookeeper.md#角色)\n        * [leader](消息队列/Zookeeper.md#leader)\n        * [follower](消息队列/Zookeeper.md#follower)\n        * [Observer](消息队列/Zookeeper.md#observer)\n    * [Zookeeper Leader 选举原理](消息队列/Zookeeper.md#zookeeper-leader-选举原理)\n    * [常见的问题](消息队列/Zookeeper.md#常见的问题)\n        * [什么是Zookeeper？它的作用是什么？](消息队列/Zookeeper.md#什么是zookeeper它的作用是什么)\n        * [Zookeeper是如何实现数据的一致性和可靠性的？](消息队列/Zookeeper.md#zookeeper是如何实现数据的一致性和可靠性的)\n        * [Zookeeper中的watcher是什么？如何使用watcher机制实现分布式锁？](消息队列/Zookeeper.md#zookeeper中的watcher是什么如何使用watcher机制实现分布式锁)\n        * [Zookeeper的性能瓶颈在哪里？如何优化Zookeeper的性能？](消息队列/Zookeeper.md#zookeeper的性能瓶颈在哪里如何优化zookeeper的性能)\n        * [如何在Zookeeper集群中进行数据的备份和恢复？](消息队列/Zookeeper.md#如何在zookeeper集群中进行数据的备份和恢复)\n\n### Pulsar[↑↑](#内容概览)\n\n- [Pulsar](消息队列/Pulsar.md)\n    * [pulsar的优势](消息队列/Pulsar.md#pulsar的优势)\n* [Apache Pulsar 架构](消息队列/Pulsar.md#apache-pulsar-架构)\n    * [Topic 与分区](消息队列/Pulsar.md#topic-与分区)\n    * [物理分区与逻辑分区](消息队列/Pulsar.md#物理分区与逻辑分区)\n* [消息存储原理与 ID 规则](消息队列/Pulsar.md#消息存储原理与-id-规则)\n    * [消息 ID 生成规则](消息队列/Pulsar.md#消息-id-生成规则)\n    * [分片机制详解：Legder 和 Entry](消息队列/Pulsar.md#分片机制详解legder-和-entry)\n* [消息副本与存储机制](消息队列/Pulsar.md#消息副本与存储机制)\n    * [消息元数据组成](消息队列/Pulsar.md#消息元数据组成)\n    * [消息副本机制](消息队列/Pulsar.md#消息副本机制)\n* [消息恢复机制](消息队列/Pulsar.md#消息恢复机制)\n* [pulsar的消息模式](消息队列/Pulsar.md#pulsar的消息模式)\n    * [独占模式（Exclusive）](消息队列/Pulsar.md#独占模式exclusive)\n    * [灾备模式（Failover）](消息队列/Pulsar.md#灾备模式failover)\n    * [共享模式（Shared）](消息队列/Pulsar.md#共享模式shared)\n* [定时和延时消息](消息队列/Pulsar.md#定时和延时消息)\n    * [相关概念](消息队列/Pulsar.md#相关概念)\n    * [适用场景](消息队列/Pulsar.md#适用场景)\n    * [使用方式](消息队列/Pulsar.md#使用方式)\n    * [定时消息](消息队列/Pulsar.md#定时消息)\n    * [延时消息](消息队列/Pulsar.md#延时消息)\n    * [使用说明和限制](消息队列/Pulsar.md#使用说明和限制)\n* [消息重试与死信机制](消息队列/Pulsar.md#消息重试与死信机制)\n    * [自动重试](消息队列/Pulsar.md#自动重试)\n    * [自定义参数设置](消息队列/Pulsar.md#自定义参数设置)\n    * [重试规则](消息队列/Pulsar.md#重试规则)\n    * [重试消息的消息属性](消息队列/Pulsar.md#重试消息的消息属性)\n    * [重试消息的消息 ID 流转](消息队列/Pulsar.md#重试消息的消息-id-流转)\n    * [主动重试](消息队列/Pulsar.md#主动重试)\n\n- [常见面试题](消息队列/mq常见面试题.md)\n    * [什么是消息队列](消息队列/mq常见面试题.md#什么是消息队列)\n    * [为什么要使用消息队列](消息队列/mq常见面试题.md#为什么要使用消息队列)\n    * [如何保证消息队列高可用](消息队列/mq常见面试题.md#如何保证消息队列高可用)\n    * [如何保证消息队列不被重复消费（幂等性）](消息队列/mq常见面试题.md#如何保证消息队列不被重复消费幂等性)\n    * [如何保证消息的可靠传输](消息队列/mq常见面试题.md#如何保证消息的可靠传输)\n        * [生产者丢数据](消息队列/mq常见面试题.md#生产者丢数据)\n        * [MQ丢数据](消息队列/mq常见面试题.md#mq丢数据)\n        * [消费者丢数据](消息队列/mq常见面试题.md#消费者丢数据)\n    * [如何保证消息的顺序性](消息队列/mq常见面试题.md#如何保证消息的顺序性)\n    * [如何处理消息堆积](消息队列/mq常见面试题.md#如何处理消息堆积)\n    * [mq 中的消息过期失效了](消息队列/mq常见面试题.md#mq-中的消息过期失效了)\n\n## <a>Redis</a>[↑↑](#内容概览)\n\n* [特点](Redis/Redis.md#特点)\n* [Redis为什么这么快](Redis/Redis.md#redis为什么这么快)\n* [常见使用场景](Redis/Redis.md#常见使用场景)\n* [数据类型](Redis/Redis.md#数据类型)\n    * [redisObject](Redis/Redis.md#redisobject)\n    * [string](Redis/Redis.md#string)\n    * [list](Redis/Redis.md#list)\n    * [hash](Redis/Redis.md#hash)\n    * [set](Redis/Redis.md#set)\n    * [zset（sorted set）](Redis/Redis.md#zsetsorted-set)\n    * [bitmap](Redis/Redis.md#bitmap)\n    * [HyperLogLog](Redis/Redis.md#hyperloglog)\n* [内存回收策略](Redis/Redis.md#内存回收策略)\n* [持久化方式](Redis/Redis.md#持久化方式)\n    * [RDB快照](Redis/Redis.md#rdb快照)\n    * [AOF追加](Redis/Redis.md#aof追加)\n* [Redis 中的事务](Redis/Redis.md#redis-中的事务)\n* [常问故障场景](Redis/Redis.md#常问故障场景)\n    * [缓存雪崩](Redis/Redis.md#缓存雪崩)\n    * [缓存穿透](Redis/Redis.md#缓存穿透)\n* [集群](Redis/Redis.md#集群)\n    * [主从复制模式](Redis/Redis.md#主从复制模式)\n    * [Sentinel（哨兵）模式](Redis/Redis.md#sentinel哨兵模式)\n    * [Cluster 集群模式](Redis/Redis.md#cluster-集群模式)\n* [Redis Cluster 节点通信原理：Gossip 算法](Redis/Redis.md#redis-cluster-节点通信原理gossip-算法)\n    * [Gossip 简介](Redis/Redis.md#gossip-简介)\n    * [节点状态和消息类型](Redis/Redis.md#节点状态和消息类型)\n* [Redis cluster伸缩的原理](Redis/Redis.md#redis-cluster伸缩的原理)\n* [redis索引](Redis/Redis.md#redis索引)\n\n### <a>Redis常见面试题</a>[↑↑](#内容概览)\n\n* [储存结构和使用场景](Redis/Redis常见面试题.md#储存结构和使用场景)\n* [淘汰策略](Redis/Redis常见面试题.md#淘汰策略)\n\n## <a>Spring</a>[↑↑](#内容概览)\n\n- [Spring](Spring/Spring.md)\n    * [架构图](Spring/Spring.md#架构图)\n    * [模块](Spring/Spring.md#模块)\n    * [IOC](Spring/Spring.md#ioc)\n        * [IOC和DI的概念](Spring/Spring.md#ioc和di的概念)\n        * [使用IOC的好处](Spring/Spring.md#使用ioc的好处)\n        * [Spring IoC的初始化过程](Spring/Spring.md#spring-ioc的初始化过程)\n        * [Spring bean的生命周期](Spring/Spring.md#spring-bean的生命周期)\n        * [bean的作用域](Spring/Spring.md#bean的作用域)\n        * [循环依赖问题](Spring/Spring.md#循环依赖问题)\n    * [AOP](Spring/Spring.md#aop)\n        * [AOP原理](Spring/Spring.md#aop原理)\n        * [AOP术语](Spring/Spring.md#aop术语)\n        * [Spring对AOP的支持](Spring/Spring.md#spring对aop的支持)\n    * [怎么定义一个注解](Spring/Spring.md#怎么定义一个注解)\n        * [引入依赖](Spring/Spring.md#引入依赖)\n        * [定义注解](Spring/Spring.md#定义注解)\n    * [事务](Spring/Spring.md#事务)\n        * [Spring 支持两种方式的事务管理](Spring/Spring.md#spring-支持两种方式的事务管理)\n        * [事务的传播性 Propagation](Spring/Spring.md#事务的传播性-propagation)\n        * [spring事务失效的场景](Spring/Spring.md#spring事务失效的场景)\n    * [spring使用的设计模式](Spring/Spring.md#spring使用的设计模式)\n        * [简单工厂](Spring/Spring.md#简单工厂)\n        * [工厂方法](Spring/Spring.md#工厂方法)\n        * [单例模式](Spring/Spring.md#单例模式)\n        * [适配器模式](Spring/Spring.md#适配器模式)\n        * [装饰器模式](Spring/Spring.md#装饰器模式)\n        * [代理模式](Spring/Spring.md#代理模式)\n    * [spring中properties和yml的加载顺序](Spring/Spring.md#spring中properties和yml的加载顺序)\n    * [使用@Autowired注解自动装配的过程是怎样的？](Spring/Spring.md#使用autowired注解自动装配的过程是怎样的)\n    * [@Autowired和@Resource之间的区别](Spring/Spring.md#autowired和resource之间的区别)\n    * [Spring中BeanFactory与FactoryBean的区别](Spring/Spring.md#spring中beanfactory与factorybean的区别)\n\n### SpringMVC[↑↑](#内容概览)\n\n- [SpringMVC](Spring/SpringMVC.md)\n    * [流程](Spring/SpringMVC.md#流程)\n    * [执行流程](Spring/SpringMVC.md#执行流程)\n\n### SpringBoot[↑↑](#内容概览)\n\n- [SpringBoot](Spring/SpringBoot.md)\n    * [springboot启动流程](Spring/SpringBoot.md#springboot启动流程)\n    * [怎么让Spring把Body变成一个对象](Spring/SpringBoot.md#怎么让spring把body变成一个对象)\n    * [SpringBoot的starter实现原理是什么？](Spring/SpringBoot.md#springboot的starter实现原理是什么)\n\n## <a>Springcloud</a>[↑↑](#内容概览)\n\n* [服务注册与发现](SpringCloud/springcloud.md#服务注册与发现)\n    * [eureka](SpringCloud/springcloud.md#eureka)\n    * [consul](SpringCloud/springcloud.md#consul)\n    * [ribbon](SpringCloud/springcloud.md#ribbon)\n    * [loadbalancer](SpringCloud/springcloud.md#loadbalancer)\n    * [feign](SpringCloud/springcloud.md#feign)\n    * [openFeign](SpringCloud/springcloud.md#openfeign)\n    * [hystrix](SpringCloud/springcloud.md#hystrix)\n    * [resilience4j](SpringCloud/springcloud.md#resilience4j)\n    * [zuul](SpringCloud/springcloud.md#zuul)\n    * [zuul2](SpringCloud/springcloud.md#zuul2)\n    * [getway](SpringCloud/springcloud.md#getway)\n    * [springcloud config](SpringCloud/springcloud.md#springcloud-config)\n    * [Nacos](SpringCloud/springcloud.md#nacos)\n\n## <a>SpringcloudAlibaba</a>[↑↑](#内容概览)\n\n* [SpringcloudAlibaba](SpringCloud/springcloud.md#springcloudalibaba)\n    * [Nacos](SpringCloud/springcloud.md#nacos-1)\n    * [Sentienl](SpringCloud/springcloud.md#sentienl)\n\n## <a>Linux</a>[↑↑](#内容概览)\n\n* [文件和目录的操作](Linux/linux.md#文件和目录的操作)\n* [查看文件](Linux/linux.md#查看文件)\n* [管理用户](Linux/linux.md#管理用户)\n* [进程管理](Linux/linux.md#进程管理)\n* [打包和压缩文件](Linux/linux.md#打包和压缩文件)\n* [grep+正则表达式](Linux/linux.md#grep)\n* [Vi编辑器](Linux/linux.md#Vi编辑器)\n* [权限管理](Linux/linux.md#权限管理)\n* [网络管理](Linux/linux.md#网络管理)\n* [cpu100%怎么排查](Linux/linux.md#cpu100怎么排查)\n* [用户空间与内核空间](Linux/linux.md#用户空间与内核空间)\n* #### <a href=\"Linux/linux.md#linux-的进程线程文件描述符是什么\">Linux的进程、线程、文件描述符是什么</a>\n* [进程切换](Linux/linux.md#进程切换)\n* [进程的阻塞](Linux/linux.md#进程的阻塞)\n* [文件描述符fd](Linux/linux.md#文件描述符fd)\n* [缓存 I/O](Linux/linux.md#缓存-io)\n* #### <a href=\"Linux/linux.md#io模型\">IO模型</a>\n* #### <a href=\"Linux/linux.md#selectpollepoll\">select、poll、epoll</a>\n* [进程间8种通信方式详解](Linux/linux.md#进程间8种通信方式详解)\n* [Linux物理内存和虚拟内存](Linux/linux.md#linux物理内存和虚拟内存)\n* [页面置换算法](Linux/linux.md#页面置换算法)\n* [进程调度算法](Linux/linux.md#进程调度算法)\n\n## <a>Mybatis</a>[↑↑](#内容概览)\n\n- [什么是mybatis](Mybatis/mybatis.md)\n- [JDBC执行六步骤](Mybatis/mybatis.md)\n- [mybatis执行8步骤](Mybatis/mybatis.md)\n- [MyBatis整体架构](Mybatis/mybatis.md)\n- [mybatis缓存](Mybatis/mybatis.md)\n\n## <a>Netty</a>[↑↑](#内容概览)\n\n* [重要的组件](Netty/netty.md#重要的组件)\n    * [Channel](Netty/netty.md#channel)\n    * [ChannelFuture](Netty/netty.md#channelfuture)\n    * [EventLoop](Netty/netty.md#eventloop)\n    * [ChannelHandler](Netty/netty.md#channelhandler)\n    * [ChannelPipeline](Netty/netty.md#channelpipeline)\n    * [TaskQueue](Netty/netty.md#taskqueue)\n* [netty的使用示例](Netty/netty.md#netty的使用示例)\n    * [服务端](Netty/netty.md#服务端)\n    * [客户端](Netty/netty.md#客户端)\n* [TCP粘包/拆包问题](Netty/netty.md#tcp粘包拆包问题)\n    * [什么是粘包拆包](Netty/netty.md#什么是粘包拆包)\n    * [发生的原因](Netty/netty.md#发生的原因)\n    * [粘包解决策略](Netty/netty.md#粘包解决策略)\n    * [netty粘包问题解决方案](Netty/netty.md#netty粘包问题解决方案)\n* [解编码技术](Netty/netty.md#解编码技术)\n    * [Java序列化的缺点](Netty/netty.md#java序列化的缺点)\n    * [Google的protobuf](Netty/netty.md#google的protobuf)\n    * [Facebook的Thrift](Netty/netty.md#facebook的thrift)\n    * [JBoss的Marshalling](Netty/netty.md#jboss的marshalling)\n    * [MessagePack](Netty/netty.md#messagepack)\n* [高性能的原因](Netty/netty.md#高性能的原因)\n    * [非阻塞io](Netty/netty.md#非阻塞io)\n    * [零拷贝](Netty/netty.md#零拷贝)\n    * [内存池](Netty/netty.md#内存池)\n    * [高效的Reactor线程模型](Netty/netty.md#高效的reactor线程模型)\n        * [Reactor 单线程模型](Netty/netty.md#reactor-单线程模型)\n        * [Reactor 多线程模型](Netty/netty.md#reactor-多线程模型)\n        * [（采用）主从 Reactor 多线程模型](Netty/netty.md#采用主从-reactor-多线程模型)\n    * [无锁化串行设计](Netty/netty.md#无锁化串行设计)\n    * [高性能的序列化框架](Netty/netty.md#高性能的序列化框架)\n    * [灵活的TCP 参数配置能力](Netty/netty.md#灵活的tcp-参数配置能力)\n* [netty相关问题](Netty/netty.md#netty相关问题)\n\n## <a>分布式相关</a>[↑↑](#内容概览)\n\n- #### <a href=\"分布式相关/分布式锁.md\">分布式锁</a>\n    * [基于数据库](分布式相关/分布式锁.md#基于数据库)\n    * [Redis](分布式相关/分布式锁.md#redis)\n    * [zookeeper](分布式相关/分布式锁.md#zookeeper)\n- #### <a href=\"分布式相关/分布式事务.md\">分布式事务</a>\n    * [两阶段提交](分布式相关/分布式事务.md#两阶段提交)\n    * [TCC（Try-Confirm-Cancel）](分布式相关/分布式事务.md#tcctry-confirm-cancel)\n    * [本地消息表](分布式相关/分布式事务.md#本地消息表)\n    * [可靠消息最终一致性](分布式相关/分布式事务.md#可靠消息最终一致性)\n    * [尽最大努力通知](分布式相关/分布式事务.md#尽最大努力通知)\n    * [Apache Seata](分布式相关/ApacheSeata.md)\n- #### <a href=\"分布式相关/分布式ID.md#分布式唯一id设计\">分布式唯一ID设计</a>\n    * [UUID](分布式相关/分布式ID.md##uuid)\n    * [多台MySQL服务器](分布式相关/分布式ID.md##多台mysql服务器)\n    * [Twitter Snowflake](分布式相关/分布式ID.md##twitter-snowflake)\n    * [百度UidGenerator算法](分布式相关/分布式ID.md##百度uidgenerator算法)\n    * [美团Leaf算法](分布式相关/分布式ID.md##美团leaf算法)\n- #### <a href=\"分布式相关/CAP.md\">CAP理论</a>  \n    * [一致性 Consistency](分布式相关/CAP.md#一致性-consistency)\n    * [可用性 Availability](分布式相关/CAP.md#可用性-availability)\n    * [分区容错性 Partition Tolerance](分布式相关/CAP.md#分区容错性-partition-tolerance)\n    * [常见的注册中心](#常见的注册中心)\n- [BASE](分布式相关/BASE.md)\n    * [基本可以  Basically Available](分布式相关/BASE.md#基本可以--basically-available)\n    * [软状态  Soft-state](分布式相关/BASE.md#软状态--soft-state)\n    * [最终一致性  Eventually Consistent](分布式相关/BASE.md#最终一致性--eventually-consistent)\n- #### <a href=\"分布式相关/一致性算法.md\">一致性算法</a>\n    * [Paxos](分布式相关/一致性算法.md#paxos)\n    * [Raft](分布式相关/一致性算法.md#raft)\n\n## <a>容器技术</a>[↑↑](#内容概览)\n\n- #### <a href=\"容器技术/docker.md\">Docker</a>\n    * [Docker简介](容器技术/docker.md#docker简介)\n    * [Docker常用命令](容器技术/docker.md#docker常用命令)\n    * [Docker应用架构](容器技术/docker.md#docker应用架构)\n    * [底层实现原理](容器技术/docker.md#底层实现原理)\n- #### <a href=\"容器技术/k8s.md\">Kubernetes</a>\n\n## <a>数据结构和算法</a>[↑↑](#内容概览)\n\n- #### <a href=\"数据结构和算法/排序算法.md\">排序算法</a>\n- #### <a href=\"数据结构和算法\">树相关</a> --todo\n- #### <a href=\"数据结构和算法\">BFS</a> --todo\n- #### <a href=\"数据结构和算法\">DFS</a> --todo\n- #### <a href=\"数据结构和算法\">回溯算法</a> --todo\n- #### <a href=\"数据结构和算法\">二分法</a> --todo\n- #### <a href=\"数据结构和算法\">贪心算法</a> --todo\n- #### <a href=\"数据结构和算法\">动态规划</a> --todo\n- #### <a href=\"数据结构和算法\">分治思想</a> --todo\n- #### <a href=\"数据结构和算法/leetcodetop150.md\">leetcode_top_150</a>\n- #### <a href=\"数据结构和算法/hotcode.md\">热门算法</a>\n- [LRU](数据结构和算法/LRU.md)\n- [LFU](数据结构和算法/LFU.md)\n- [加减乘除](数据结构和算法/加减乘除.md)\n\n## <a>设计模式</a>[↑↑](#内容概览)\n\n- [工厂模式](设计模式/工厂模式.md)\n- [单例模式](设计模式/单例模式.md)\n- 建造者模式\n- 原型模式\n- 适配器模式\n- [装饰器模式](设计模式/装饰者模式.md)\n- 代理模式\n- 外观模式\n- 桥接模式\n- 组合模式\n- 享元模式\n- [策略模式](设计模式/策略模式.md)\n- 模板方法模式\n- 观察者模式\n- 迭代子模式\n- [责任链模式](设计模式/责任链模式.md)\n- 备忘录模式\n- 状态模式\n- 访问者模式\n- 中介者模式\n- 解释器模式\n\n## <a>职业规划和学习习惯</a>[↑↑](#内容概览)\n\n- [项目中遇到的问题](职业规划和学习习惯/职业规划和学习习惯.md#项目中遇到的问题)\n- [职业规划](职业规划和学习习惯/职业规划和学习习惯.md#职业规划)\n- [平时规则](职业规划和学习习惯/职业规划和学习习惯.md#平时规则)\n\n## <a>场景设计</a>[↑↑](#内容概览)\n\n- [有A、B两个大文件，每个文件几十G,而内存只有4G,其中A文件存放学号+姓名，而B文件存放学号+分数，要求生成文件C，存放姓名和分数。怎么实现?](场景设计/场景设计.md)\n- [秒杀系统怎么设计](场景设计/场景设计.md#秒杀系统怎么设计)\n- [唯一ID设计](场景设计/场景设计.md#唯一ID设计)\n- [产品上线出问题怎么定位错误](场景设计/场景设计.md#产品上线出问题怎么定位错误)\n- [大量并发查询用户商品信息，MySQL压力大查询慢，保证速度怎么优化方案](场景设计/场景设计.md#大量并发查询用户商品信息，MySQL压力大查询慢，保证速度怎么优化方案)\n- [海量日志数据，提取出某日访问百度次数最多的那个IP。给定a、b两个文件，各存放50亿个url,每个url各 占64字节，内存限制是4G,让你找出a、b文件共同的url?](场景设计/场景设计.md)\n- [一般内存不足而需要分析的数据又很大的问题都可以使用分治的思想，将数据hash(x)%1000分为小文件再分别加载小文件到内存中处理即可](场景设计/场景设计.md#一般内存不足而需要分析的数据又很大的问题都可以使用分治的思想将数据hashx1000分为小文件再分别加载小文件到内存中处理即可)\n- [如何保证接口的幂等性](场景设计/场景设计.md#如何保证接口的幂等性)\n- [缓存和数据库不一致问题](场景设计/场景设计.md#缓存和数据库不一致问题)\n- [什么是SPI](场景设计/场景设计.md#什么是SPI)\n- [什么是RPC？](场景设计/场景设计.md#什么是rpc)\n- [gRPC](场景设计/场景设计.md#gRPC)\n- [一个优秀的RPC框架需要考虑的问题](场景设计/场景设计.md#一个优秀的RPC框架需要考虑的问题)\n- [什么是DDD](场景设计/场景设计.md#什么是ddd)\n- [Java实现生产者消费者](场景设计/场景设计.md#java实现生产者消费者)\n- [Java实现BlockQueue](场景设计/场景设计.md#java实现blockqueue)\n- [解决哈希冲突的方法](场景设计/场景设计.md#解决哈希冲突的方法)\n- [排行榜设计](场景设计/场景设计.md#排行榜设计)\n\n## <a>智力题</a>[↑↑](#内容概览)\n\n- [100只试管里有-只是有毒的，现在有10个小白鼠，如何最快速地判断出那只试管有毒](智力题/智力题.md)\n- [共1000瓶药水，其中I瓶有毒药。已知小白鼠喝毒药一天内死若想在一天内找到毒药，最少需要几只小白鼠?](智力题/智力题.md)\n- [只有两个无刻度的水桶，-个可以装6L水，-一个可以装5L水，如何在桶里装入3L的水](智力题/智力题.md)\n- [25匹马，5个赛道， 每次只能同时有5匹马跑，最少比赛几次选出前三名?家里有两个孩子,一个是女孩，另一个也是女孩的概率是多少?](智力题/智力题.md)\n- [烧一根不均匀的绳，从头烧到尾总共需要1个小时。现在有若干条材质相同的绳子，问如何用烧绳的方法来计时一个小时十五分钟呢?](智力题/智力题.md)\n- [共12个一样的小球，其中只有一个重量与其它不一一样(未知轻重)，给你一个天平，找出那个不同重量的球?](智力题/智力题.md)\n- [有10瓶药，每瓶有10粒药，其中有一瓶是变质的。好药每颗重1克，变质的药每颗比好药重0.1克。问怎样用天秤称一次找出变质的那瓶药？](智力题/智力题.md)\n- [你有两个罐子，50个红色弹球，50个蓝色弹球，如何将这100个球放入到两个罐子，随机选出一个罐子取出的球为红球的概率最大?](智力题/智力题.md)\n- [抢30是双人游戏，游戏规则是:第一个人喊\"1\"或\"2\"，第二个人要接着往下喊一个或两个数，然后再轮到第一个人。 两人轮流进行下去。最后喊30的人获胜。问喊数字的最佳策略。](智力题/智力题.md)\n- [某人进行10次打靶，每次打靶可能的得分为0到10分，10次打靶共得90分的可能性有多少种](智力题/智力题.md)\n\n## <a>架构</a>[↑↑](#内容概览)\n\n- #### <a href=\"架构/系统设计.md\">系统设计</a>\n- #### <a href=\"架构/计算和储存分离.md\">计算和储存分离</a>\n- #### <a href=\"架构/DDD领域驱动设计.md\">DDD领域驱动设计</a>\n\n## <a>大数据</a>[↑↑](#内容概览)\n\n- #### <a href=\"大数据/Hadoop/Hadoop.md\">Hadoop</a>\n  - #### <a href=\"大数据/Hadoop/hadoop面试问题.md\">hadoop面试问题</a>\n- #### <a href=\"大数据/Flink/Flink.md\">Flink</a>\n- #### <a href=\"大数据/Hive/Hive.md\">Hive</a>\n- #### <a href=\"大数据/Spark/Spark.md\">Spark</a>\n\n## <a>面试解答</a>[↑↑](#内容概览)\n\n- [HR会问什么](面试解答/HR会问什么.md)\n- [面试解答6月牛客](面试解答/面试解答2021-06.md)\n- [面试解答7月牛客](面试解答/面试解答2021-07.md)\n- [面试解答9月牛客](面试解答/面试解答2021-09.md)\n- [面试解答10月牛客](面试解答/面试解答2021-10.md)\n\n## <a>商城类问题</a>[↑↑](#内容概览)\n\n- [秒杀](商城类问题/商城类问题.md#秒杀)\n- [超卖](商城类问题/商城类问题.md#如何解决超卖问题)\n- [订单延迟](商城类问题/商城类问题.md#订单延时取消怎么做)\n\n# 免责声明[↑↑](#内容概览)\n\n> 某些知识点、观点、图片是从各种优秀博主、作者、大佬们的文章里或文献里提取的，我只是搬运工，如果觉得有侵犯到您的权益，请联系我，我将根据您的要求修改（添加您的出处链接、删除、修改....），谢谢大佬！\n\n\n# 最后[↑↑](#内容概览)\n\n> 不积跬步无以至千里\n\n[//]: # (## 微信交流)\n\n[//]: # ()\n[//]: # (> 可以关注我的微信公众号，一些学习资料关注后可以分享给你)\n\n[//]: # ()\n[//]: # ()\n[//]: # (<img src=\"img%2F微信公共号二维码.png\" align=\"center\" width=\"50%\" />)"
  },
  {
    "path": "Redis/Redis.md",
    "content": "# redis\n## 特点\n- Key-Value健值类型存储\n- 支持数据可靠存储及落地\n- 单进程单线程高性能服务器\n- 单机qps(每秒查询率)可以达到10w.\n- 适合小数据量高速读写访问\n## Redis为什么这么快\n- 完全基于内存 没有磁盘IO上的开销\n- 优化的数据结构\n- 采用单线程\n  - 避免了不必要的上下文切换和竞争条件，也不存在多进程或者多线程导致的切换而消耗 CPU，不用去考虑各种锁的问题，不存在加锁释放锁操作，没有因为可能出现死锁而导致的性能消耗\n  - Redis 没有使用多线程？为什么不使用多线程？\n    - 虽然说 Redis 是单线程模型，但是， 实际上，Redis 在 4.0 之后的版本中就已经加入了对多线程的支持。\n    - 引入多线程主要是为了提高网络 IO 读写性能\n    - 对比\n      ![](../img/redis/redis6之前版本.png)\n\n      ![](../img/redis/duoxianc.png)\n\n\n        - 主要负责接受客户端连接并且分发到各个Io线程，而io线程负责读取客户端命令，命令读取完由主线程执行命令，主线程执行完命令后再由Io线程把回复数据返回给客户端\n        - 不把处理命令交给各个io线程去执行，这里就涉及到了竞争问题，因为数据库是共享的，多个线程必要加锁，而锁是一个耗时的操作还会涉及多线程之间的上下文切换\n- 使用多路I/O复用模型，非阻塞IO\n## 常见使用场景\n#### 缓存\n缓存现在几乎是所有中大型网站都在用的必杀技，合理的利用缓存不仅能够提升网站访问速度，还能大大降低数据库的压力。Redis提供了键过期功能，也提供了灵活的键淘汰策略，所以，现在Redis用在缓存的场合非常多。Redis作为缓存使用可能涉及缓存雪崩、缓存穿透、缓存击穿等问题\n#### 排行榜 \n很多网站都有排行榜应用的，如京东的月度销量榜单、商品按时间的上新排行榜等。Redis提供的有序集合数据类构能实现各种复杂的排行榜应用\n#### 计数器\n什么是计数器，如电商网站商品的浏览量、视频网站视频的播放数等。为了保证数据实时效，每次浏览都得给+1，并发量高时如果每次都请求数据库操作无疑是种挑战和压力。Redis提供的incr命令来实现计数器功能，内存操作，性能非常好，非常适用于这些计数场景。\n#### 分布式会话\n集群模式下，在应用不多的情况下一般使用容器自带的session复制功能就能满足，当应用增多相对复杂的系统中，一般都会搭建以Redis等内存数据库为中心的session服务，session不再由容器管理，而是由session服务及内存数据库管理\n#### 分布式锁\n在很多互联网公司中都使用了分布式技术，分布式技术带来的技术挑战是对同一个资源的并发访问，如全局ID、减库存、秒杀等场景，并发量不大的场景可以使用数据库的悲观锁、乐观锁来实现，但在并发量高的场合中，利用数据库锁来控制资源的并发访问是不太理想的，大大影响了数据库的性能。可以利用Redis的setnx功能来编写分布式的锁，如果设置返回1说明获取锁成功，否则获取锁失败，实际应用中要考虑的细节要更多\n#### 社交网络\n点赞、踩、关注/被关注、共同好友等是社交网站的基本功能，社交网站的访问量通常来说比较大，而且传统的关系数据库类型不适合存储这种类型的数据，Redis提供的哈希、集合等数据结构能很方便的的实现这些功能\n#### 最新列表\nRedis列表结构，LPUSH可以在列表头部插入一个内容ID作为关键字，LTRIM可用来限制列表的数量，这样列表永远为N个ID，无需查询最新的列表，直接根据ID去到对应的内容页即可\n#### 消息系统\n消息队列是大型网站必用中间件，如ActiveMQ、RabbitMQ、Kafka等流行的消息队列中间件，主要用于业务解耦、流量削峰及异步处理实时性低的业务。Redis提供了发布/订阅及阻塞队列功能，能实现一个简单的消息队列系统。另外，这个不能和专业的消息中间件相比\n## 数据类型\n### redisObject\nredisObject 的定义位于 redis.h ,Redis的每种数据类型都是套用该对象\n```cpp\ntypedef struct redisObject {\n    // 类型\n    unsigned type:4;\n    // 对齐位\n    unsigned notused:2;\n    // 编码方式\n    unsigned encoding:4;\n    // LRU 时间（相对于 server.lruclock）\n    unsigned lru:22;\n    // 引用计数\n    int refcount;\n    // 指向对象的值\n    void *ptr;\n} robj;\n```\ntype 、 encoding 和 ptr 是最重要的三个属性。\n\ntype 记录了对象所保存的值的类型，它的值可能是以下常量的其中一个（定义位于 redis.h）：\n```cpp\n/*\n * 对象类型\n */\n#define REDIS_STRING 0  // 字符串\n#define REDIS_LIST 1    // 列表\n#define REDIS_SET 2     // 集合\n#define REDIS_ZSET 3    // 有序集\n#define REDIS_HASH 4    // 哈希表\n```\nencoding 记录了对象所保存的值的编码，它的值可能是以下常量的其中一个（定义位于 redis.h）：\n```cpp\n/*\n * 对象编码\n */\n#define REDIS_ENCODING_RAW 0            // 编码为字符串\n#define REDIS_ENCODING_INT 1            // 编码为整数\n#define REDIS_ENCODING_HT 2             // 编码为哈希表\n#define REDIS_ENCODING_ZIPMAP 3         // 编码为 zipmap\n#define REDIS_ENCODING_LINKEDLIST 4     // 编码为双端链表\n#define REDIS_ENCODING_ZIPLIST 5        // 编码为压缩列表\n#define REDIS_ENCODING_INTSET 6         // 编码为整数集合\n#define REDIS_ENCODING_SKIPLIST 7       // 编码为跳跃表\n```\nptr 是一个指针，指向实际保存值的数据结构，这个数据结构由 type 属性和 encoding 属性决定。\n\n举个例子，如果一个 redisObject 的 type 属性为 REDIS_LIST ， encoding 属性为 REDIS_ENCODING_LINKEDLIST ，那么这个对象就是一个 Redis 列表，它的值保存在一个双端链表内，而 ptr 指针就指向这个双端链表；\n\n每个类型都会有两种或以上的实现\n\n<img src=\"../img/redis/类型底层实现.png\" width=\"50%\" />\n\n### string\n#### int整数\n如果保存的字符串是整数值，并且这个整数值可以用long类型来表示，那么ptr指针的void*则转化为C语言源生的long类型\n\t\t\t\n#### SDS（raw简单动态字符串）\n- sds实现 > 39字节\n\n结构图\n\n<img src=\"../img/redis/sds结构图.png\" width=\"50%\" />\n\n我们把上图中非char数组（变量名为buf）的部分都统称为header\n\n`len` 为buf分配的内存空间已使用的长度，即我们看见的，有效的字符串\n\n`alloc` buf分配的内存空间的总长度，alloc – len 就是未使用的空间，当然这长度不包括SDS字符串头和结尾NULL\n\n`flags` 只使用了低三位表示类型，值为0-4，分别表示sdshdr5到sdshdr64这五种类型。高五位没有用处，目的是根据字符串的长度的不同选择不同的sds结构体\n\n```cpp\n/* 因为生的跟别人不一样（内部结构不一样），老五（sdshdr5）从来不被使用 */\n  struct __attribute__ ((__packed__)) sdshdr5 {\n      unsigned char flags; /* 低三位表示类型, 高五位表示字符串长度 */\n      char buf[];\n  };\n  \n  struct __attribute__ ((__packed__)) sdshdr8 {\n      uint8_t len; /* 字符串长度*/\n      uint8_t alloc; /* 分配长度 */\n      unsigned char flags; /* 低三位表示类型，高五位未使用 */\n      char buf[];\n  };\n  \n  struct __attribute__ ((__packed__)) sdshdr16 {\n      uint16_t len; /* 字符串长度*/\n      uint16_t alloc; /* 分配长度 */\n      unsigned char flags; /* 低三位表示类型，高五位未使用 */\n      char buf[];\n  };\n  \n  struct __attribute__ ((__packed__)) sdshdr32 {\n      uint32_t len; /* 字符串长度*/\n      uint32_t alloc; /* 分配长度 */\n      unsigned char flags; /* 低三位表示类型，高五位未使用 */\n      char buf[];\n  };\n  \n  struct __attribute__ ((__packed__)) sdshdr64 {\n      uint64_t len; /* 字符串长度*/\n      uint64_t alloc; /* 分配长度 */\n      unsigned char flags; /* 低三位表示类型，高五位未使用 */\n      char buf[];\n  };\n```\n- 为何要定义不同的结构体\n  - 结构体的主要区别是len和alloc的类型（uint8，uint16等等），定义不同的结构体是为了存储不同长度的字符串，根据不同长度定义不同的类型是为了节省一部分空间大小，毕竟在Redis字符串非常多，哪怕一点点优化积累起来都很可观\n  - 与其他的结构体不同，sdshdr5没有定义char数组和alloc字段，他的值存储在flag没有被使用的高五位中，所以sdshdr5对应的SDS_TYPE_5类型字符串只能保存原串长度小于等于2^5 = 32，因此，它不能为字符串分配空余空间。如果字符串需要动态增长，那么它就必然要重新分配内存才行。所以说，这种类型的sds字符串更适合存储静态的短字符串\n\n`buf` 这是一个没有指明长度的字符数组，这是C语言中定义字符数组的一种特殊写法，称为柔性数组（flexible array member），只能定义在一个结构体的最后一个字段上\n- 这个字符数组的长度等于最大容量+1\n- 之所以字符数组的长度比最大容量多1个字节，就是为了在字符串长度达到最大容量时仍然有1个字节NULL结束符，即ASCII码为0的’\\0’字符，这样字符串可以和c语言源生的字符串兼容\n\n使用sds结构的优点\n- `有利于减少内存碎片`，提高存储效率\n- `常数复杂度获取字符串长度` len\n- `杜绝缓冲区溢出` C语言字符串不记录自身长度，也容易造成缓冲区溢出。而当SDS对自身字符串进行修改时，API会先检查SDS的剩余空间是否满足需要（获取alloc减len），如果不满足，则会先拓展空间，再执行API\n- `空间预分配`\n  - SDS在重新分配空间的时候，会预分配一些空间来作为冗余。当SDS的len属性长度小于1MB时，Redis会分配和len相同长度的free空间。至于为什么这样分配呢，上次用了len长度的空间，那么下次程序可能也会用len长度的空间，所以Redis就为你预分配这么多的空间\n  - 但是当SDS的len属性长度大于1MB时，程序将多分配1M的未使用空间。这个时候再根据这种惯性预测来分配的话就有点得不偿失了。所以Redis是将1MB设为一个风险值，没过风险值你用多少我就给你多少，过了的话那这个风险值就是我能给你临界值。\n- `惰性空间释放` Redis的内存回收采用惰性回收，即你把字符串变短了，那么多余的内存空间也不会立刻还给操作系统，先留着，用header的字段将其记录下来，以防接下来又要被使用呢\n- `二进制安全` 因为`\\0`字符串在SDS中没有意义，他作为结束符的任务已经被header字段给替代了，所以与c语言不一样的，SDS是二进制安全的\n  - 使用len字段而不是以\\0来判断是否结束，使用`\\0`只是为了兼容C的字符串而使用原生的api\n\n#### embstr编码的简单动态字符串\n- sds实现 <=39 字节 版本不同不一样，现在好像是44\n  - 具体原因就是因为各个结构体定义的字节数加起来+字符串的大小是否超过64来判断\n- embstr编码是专门用来保存短字符串的一种优化编码方式\n- raw会调用两次内存分配函数来创建redisObject结构和sdshdr结构，而embstr编码则通过调用一次内存分配函数来分配一块连续的空间，空间内一次包含了redisObject和sdshdr两个结构\n\n#### string的操作\n- `set hello world` 添加\n- `get hello` 获取\n- `del hello` 删除\n- `SETNX key value` 只有在 key 不存在时设置 key 的值。\n- `INCR key` 将 key 中储存的数字值增一。\n- `DECR key` 将 key 中储存的数字值减一。\n\n--- \n\n### list\nlist压缩列表\n\t\t\t\nlinkedlist双端列表\n- 结构\n```cpp\ntypedef struct listNode {\n    // 前置节点\n    struct listNode *prev;\n    // 后置节点\n    struct listNode *next;\n    // 节点的值\n    void *value;\n} listNode;\n\n```\n- 列表的节点（注意不是列表的定义）定义如上，除了双向链表必须的前后指针外，为了实现通用性，支持不同类型数据的存储，Redis将节点类型的数据域定义为void *类型，从而模拟了“泛型”\n\n#### list的操作\n- rpush \n```shell\n> rpush list-key item\n(integer) 1\n> rpush list-key item\n(integer) 3\n> rpush list-key item2\n(integer) 2\n```\n- lrange\n```shell\nlrange list-key 0 -1\n> \"item\" 2)\n> \"item2\" 3\n> \"item\"\n```\n- lindex\n```shell\nlindex list-key 1\n> \"item2\"\n```\n- lpop\n```shell\nlpop list-key\n> \"item\"\n```\n\n#### 应用场景\n发布与订阅或者说消息队列、慢查询\n\n--- \n\n### hash\n\nRedis的哈希类型是一个字符串字段和字符串值之间的映射表，这种数据类型可以用来表示对象。Redis中存储的每个哈希可以存储232 - 1键值对（字段和值）。\n\n内部来看，Redis使用两种不同的编码方式来存储哈希：\n\n- ziplist（压缩列表）: 当哈希类型的元素数量较小，并且每个元素的大小也较小时，Redis会使用ziplist作为哈希的内部存储结构。ziplist是一个特别设计的紧凑数据结构，它将所有的字段和值紧密排列在一起以节省空间。zziplist是一个字节数组，允许快速的访问，对小的元素数量非常有效率。\n\n- hashtable（哈希表）: 当哈希类型的元素数量增加或者元素的大小增加，达到一定的阈值时，Redis会改用一个真正的哈希表来存储这些字段和值。这时，每个字段会被散列（通过哈希函数）到哈希表中的不同槽位。每个槽位保存了指向字段和值对应节点的指针。\n\n转换之间的具体阈值是可以配置的，通过hash-max-ziplist-entries和hash-max-ziplist-value配置可以定义什么时候将ziplist升级为哈希表。\n\n在两种数据结构中：\n\n- Ziplist 编码的哈希表是一系列的连续内存块，其中包含了节点的编码长度，前一个节点的长度（以便可以向前或向后遍历），以及节点的内容。\n- Hashtable 编码的哈希中，每个节点存储一个指向下一个节点的指针（因为它使用链表来处理散列冲突），字段本身，以及字段对应的值。\nRedis使用渐进式rehash机制，新的或更新的元素总是添加到新哈希表中，而读取操作会查看新旧表。旧哈希表的元素会逐渐移动到新哈希表中，直到旧表为空。这使得Redis可以继续服务请求，同时完成rehash操作。\n\n\n#### hashtable字典\n结构体\n\n<img src=\"../img/redis/hashtable结构体.png\" width=\"50%\" />\n\n- dict是字典的包装对象，居于最外层\n- ht[2]是包含两个项的哈希表的数组，一般情况下，只使用h[0]，h[1]只有在rehash的时候才会使用\n  - 字典通过“拉链法”来解决冲突问题的，dictEntry结构体的*next指针指向了其拉链列表的下一个节点。\n- dictht是哈希表的结构，他除了一个数组table用来存放键值对以外，还有used字段表示目前已有键值对，size表示数组大小，sizemark=size-1，用来hash索引\n- dictType是类型特定函数\n  - HashFunction 计算哈希值的函数\n  - KeyDup 复制键的函数\n  - ValDup 复制值的函数\n  - KeyCompare 对比键的函数\n  - KeyDestructor 销毁键的函数\n  - ValDestructor 销毁值的函数\n\ndict的rehash\n- 3步骤\n  - 扩展备用的ht[1]，将它的容量扩张到第一个大于ht[0].used*2的 2的n次方\n  - 将ht[0]的值重新经过hash索引之后迁移到ht[1]上。\n  - 释放ht[0]，将ht[1]设为ht[0]，创建新的空表ht[1]。\n- Rehash是渐进式的\n  - Rehash不是一步完成的，而是在操作过程中渐进式的。字典维持一个索引计数器rehashidx用来记录当前正在操作的索引，从ht[0]的0号索引上开始，一个项一个项的迁移到ht[1]，直到完成所有迁移，rehashidx变成-1。\n  - 在rehash期间，所有新增字段添加在ht[1]中，而删除，更新操作会在两个表上同时进行。查找时先找ht[0]，再找ht[1]。\n\n\n负载因子（load factor）是用来决定是否需要进行rehash的一个指标。Redis 的哈希表有以下两个负载因子相关的阈值：\n\n- 负载增加的阈值: 当哈希表的负载因子超过 1（即，已存储的元素数量等于哈希表的大小时），如果我们继续向哈希表添加新的元素，Redis 将会初始化一个新的哈希表，其大小大约是原来的两倍，并开始渐进式地将旧表中的元素迁移到新表中。\n\n- 渐进式rehashing: 在rehash操作进行的同时，Redis 仍然可以响应命令请求。当一个新的写入命令被执行，Redis 会将该键值对存入新的哈希表。同时，在执行读取操作时，Redis 会同时检查新旧两个哈希表。最重要的是，在处理每个命令后，Redis 会花费一点时间（由hresize命令配置的数量指定）将旧哈希表的几项元素迁移到新表中。\n\n- 负载减少的阈值: 当哈希表在rehash后的负载因子小于 0.1（即，表中元素数为桶数的十分之一）时，Redis 会减小哈希表的大小，以节省内存空间。\n\nRedis 中哈希表的扩张和收缩是为了保持效率和节省内存。随着数据的增加，避免哈希碰撞变得更重要；同样的，当数据删除后，减少内存的使用也同等重要。通过负载因子的动态调整，Redis 的哈希表可以平衡性能和资源消耗。\n\n#### hash的操作\n\n```shell\n# 添加\nhset hash-key sub-key1 value1\nhset hash-key sub-key2 value2\n\n# 获取全部\nhgetall hash-key \n\n# 删除\nhdel hash-key sub-key2 \n\n# 获取\nhget hash-key sub-key1 \n```\n#### 应用场景\n特别适合存储对象\n\n--- \n\n### set\n#### intset整数集合\n- 结构\n  ```cpp\n  typedef struct intset {\n    uint32_t encoding;\n    uint32_t length;\n    int8_t contents[];\n  } intset;\n  ```\n  - Encoding 存储编码方式\n  - Length inset的长度，即元素数量\n  - Content Int数组，用来保存元素，各个项在数组中按数值从小到大排序，不包含重复项\n- 当一个集合中只包含整数值，并且元素数量不多时，redis使用整数集合作为set的底层实现\n- 当在一个int16类型的整数集合中插入一个int32类型的值，整个集合的所有元素都会转换成32类型\n- 整数集合只支持升级操作，不支持降级操作\n\n#### hashtable字典\n同上\n#### set的操作\n\n- sadd 添加\n```shell\n> sadd set-key item\n(integer) 1\n> sadd set-key item2\n(integer) 1\n> sadd set-key item3\n(integer) 1\n> sadd set-key item\n(integer) 0\n```\n- smembers 返回集合中的所有成员\n```shell\n> smembers set-key\n\"item\" \n\"item2\" \n\"item3\"\n```    \n- sismember set-key item4 判断 member 元素是否是集合 key 的成员\n\n```shell\n> sismember set-key item4\n(integer) 0\n> sismember set-key item\n(integer) 1\n```\n- SREM 移除集合中一个或多个成员\n```shell\nSREM key member1 [member2]\n```\n- SDIFF 返回第一个集合与其他集合之间的差异。\n```shell\nSDIFF key1 [key2]\n```\n- SINTER 返回给定所有集合的交集\n```shell\nSINTER key1 [key2]\n```\n- SUNION 返回所有给定集合的并集\n```shell\nSUNION key1 [key2]\n```\n#### 应用场景\n需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景\n\n--- \n### zset（sorted set）\n#### ziplist压缩列表\n当一个列表只包含少量元素，并且每个元素要么就是小整数值，要么就是长度比较短的字符串，那么Redis使用ziplist作为列表实现\n- 压缩表是为了节约内存而开发的，压缩表可以包含任意个节点，每个节点保存一个字节数组（字符串）或一个整数值\n#### skiplist跳跃表\n- 当元素数量比较多，或者元素成员是比较长的字符串时，底层实现采用跳跃表\n- 跳跃表是一种有序数据结构，他在一个节点中维持多个指向其他节点的指针\n- 跳跃表的平均复杂度为O(logN)，最坏为O(N)，其效率可以和平衡树相媲美，而且跟平衡树相比，实现简单\n- 示意图\n\n  ![](../img/redis/skiplist.png)\t\t\t\t\t\n  - 每一个竖列其实是一个节点。如果能通过在节点中维持多个指向不同节点的指针（比如node4（值为21）就有三个指针，分别指向node5（33），node6（37），node8（55）），那么就会得到一个平衡的跳跃表\n  - 跳跃表最难的，就是保持平衡，维持平衡的跳跃表难度要大于维持平衡的二叉树。故而易于实现的，是实现概率平衡，而不是强制平衡\n- 跳跃表的查询\n  - 示例\n  \n  ![](../img/redis/跳跃表的查询.png)\n\t\t\t\t\t\t\n  - 跳跃表的查询是从顶层往下找，那么会先从第顶层开始找，方式就是循环比较，如过顶层节点的下一个节点为空说明到达末尾，会跳到第二层，继续遍历，直到找到对应节点\n    - 查找元素 117\n    - 比较 21， 比 21 大，且21有后继，向后面找\n    - 比较 37, 比 37大，且37节点同层没有后继了，则从 37 的下面一层开始找\n    - 比较 71, 比 71 大，且71节点同层没有后继了，则从 71 的下面一层开始找\n    - 比较 85， 比 85 大，且85有后继，向后面找\n    - 比较 117， 等于 117， 找到了节点\n\n#### Redis中的跳跃表的实现\n结构\n```cpp\n#define ZSKIPLIST_MAXLEVEL 32\n#define ZSKIPLIST_P 0.25\n\ntypedef struct zskiplistNode {\n    //成员对象\n    robj *obj;\n    //分值\n    double score;\n    //后向指针\n    struct zskiplistNode *backward;\n    struct zskiplistLevel {\n        //前向指针\n        struct zskiplistNode *forward;\n        //跨度\n        unsigned int span;\n    } level[];\n} zskiplistNode;\n\ntypedef struct zskiplist {\n    //跳跃表的表头节点和表尾节点\n    struct zskiplistNode *header, *tail;\n    // 表中节点的数量\n    unsigned long length;\n    // 表中层数最大的节点层数\n    int level;\n} zskiplist;\n\ntypedef struct zset {\n    dict *dict;\n    zskiplist *zsl;\n} zset;\n```\n\nzskiplistNode 表示跳跃表节点结构\n- obj 存放着该节点对于的成员对象，一般指向一个sds结构\n- score是double结构，存储分数值。\n- backward，后退指针，指向列表前一个node\n- level [ ]数组，表示一个节点可以有多个层\n  - 数组里面的项是zskiplistLevel结构，可以看到，每一层都有一个跳跃指针forward\n  - 跨度span，顾名思义，就是用来记录跨度的，相邻的节点跨度为1。\n  - 注意：跨度的用处是用来计算某个节点在跳跃表中的排位的，zset的排序按score从小到大排序。比如我查找到node7，通过将沿途的所有跨度累加，我们可以得到其排在列表中的序列\n\n![](../img/redis/redis-skiplist.png)\n\nzskiplist 表示跳跃表结构\n- zskiplist中有指向整个跳跃表两端的head指针和tail指针\n- 记录跳跃表长度的leng字段。\n- Int型的level用来记录目前整个跳跃表中最高的层数。\n\n##### 一般情况下维持平衡跳跃表的实现\n- 在跳跃表中插入一个新的节点时，程序需要确定两个要素：该节点的位置，以及层数\n- 因为有序集合按照score排序，故而位置可以按照score比出，确定位置。\n- 确定了位置后，再确定node的层数，可以采用抛硬币的方式，一次正面，层数+1，直到反面出现为止。因为抛硬币会使层数L的值满足参数为 p = 1/2 的几何分布，在数量足够大时，可以近似平衡。\n- 用抛硬币的方式，可以使level+1的概率为2分之一，也就是说，k层节点的数量是k+1层的1/2 ，你可以把它看成是一个二叉树。\n\n##### Redis维持平衡跳跃表的实现\n幂次定律\n- 含义是：如果某件事的发生频率和它的某个属性成幂关系，那么这个频率就可以称之为符合幂次定律。\n- 表现是：少数几个事件的发生频率占了整个发生频率的大部分， 而其余的大多数事件只占整个发生频率的一个小部分。\n- 说人话版：越大的数，出现的概率越小。\n\n实现算法\n- 当Redis在跳跃表中插入一个新的节点时，程序需要确定两个要素：该节点的位置，以及层数\n- Redis的实现与一般维持平衡跳跃表的实现大同小异，Redis中跳跃表的层数也是在插入的时候确定，按照分数找好位置后，Redis会生成一个1-32的数作为层数。\n- Redis的level+1的概率是1/4,所以Redis的跳跃表是一个四叉树\n  \n  ```cpp\n  /* Returns a random level for the new skiplist node we are going to create.\n  * The return value of this function is between 1 and ZSKIPLIST_MAXLEVEL\n  * (both inclusive), with a powerlaw-alike distribution where higher\n  * levels are less likely to be returned.\n  * \n  * 返回一个介于 1 和 ZSKIPLIST_MAXLEVEL 之间的随机值，作为节点的层数。\n  * \n  * 根据幂次定律(power law)，数值越大，函数生成它的几率就越小\n  * \n  * T = O(N)\n  */\n  #define ZSKIPLIST_MAXLEVEL 32 /* Should be enough for 2^32 elements */\n  #define ZSKIPLIST_P 0.25      /* Skiplist P = 1/4 */\n  int zslRandomLevel(void) {\n      int level = 1;\n      while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))\n          level += 1;\n      return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;\n  }\n  ```\n  - 指定节点最大层数 MaxLevel，指定概率 p， 默认层数 lvl 为1\n  - 生成一个0~1的随机数r，若r<p，且lvl<MaxLevel ，则lvl ++\n  - 重复第 2 步，直至生成的r >p 为止，此时的 lvl 就是要插入的层数。\n  - 总结 高层概率总是越小的，底层的概率总是最大的\n\n#### zset的操作\n- ZADD 添加\n```shell\nZADD key score1 member1 [score2 member2]\n```\n- ZRNAK 查看排名\n```shell\nZRNAK key member\n```\n- ZREVRNAK 查看排名(倒序) 最大的返回是0\n```shell\nZREVRNAK key member\n```\n- ZRANGE 通过索引区间返回有序集合指定区间内的成员\n```shell\nZRANGE key start stop [WITHSCORES]\n\n## 查看前10名，sorce排序\n127.0.0.1:6379> zrange test 0 10\n1) \"xiaoming\"\n2) \"xiaohong\"\n3) \"xiaogang\"\n4) \"xinxin\"\n5) \"ghg\"\n6) \"dahua\"\n```\n- ZREVRANGE 通过索引区间返回有序集合指定区间内的成员(倒序)\n```shell\nZREVRANGE key start stop [WITHSCORES]\n\n## 查看前10名，sorce倒序\n127.0.0.1:6379> zrevrange test 0 10\n1) \"dahua\"\n2) \"ghg\"\n3) \"xinxin\"\n4) \"xiaogang\"\n5) \"xiaohong\"\n6) \"xiaoming\"\n```\n- ZREVRANGE 获取所有member的排名\n```shell\nZREVRANGE key 0 -1\n\n127.0.0.1:6379> zrevrange test 0 -1\n1) \"dahua\"\n2) \"ghg\"\n3) \"xinxin\"\n4) \"xiaogang\"\n5) \"xiaohong\"\n6) \"xiaoming\"\n```\n- ZSCORE 获取member的分数\n```shell\nZSCORE key member\n\n127.0.0.1:6379> ZSCORE test dahua\n\"104\"\n```\n- ZCRAD 获取zset key的大小\n```shell\nZCRAD key\n\n127.0.0.1:6379> zcard test\n(integer) 6\n```\n- ZREM 移除有序集合中的一个或多个成员\n```shell\nZREM key member [member ...]\n```\n#### 应用场景\n需要对数据根据某个权重进行排序的场景。比如在直播系统中，实时排行信息包含直播间在线用户列表，各种礼物排行榜，弹幕消息（可以理解为按消息维度的消息排行榜）等信息\n\n#### 为什么使用跳表不使用别的数据结构？\n1. **不需要复杂的平衡**，平衡树的插入和删除操作可能引发子树的调整，逻辑复杂，而skiplist的插入和删除只需要修改相邻节点的指针，操作简单又快速\n2. **适合范围查找**，在做范围查找的时候，平衡树比skiplist操作要复杂。在平衡树上，我们找到指定范围的小值之后，还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造，这里的中序遍历并不容易实现。而在skiplist上进行范围查找就非常简单，只需要在找到小值之后，对第1层链表进行若干步的遍历就可以实现\n3. **算法实现上更简单**\n\n--- \n\n### bitmap\nbitmap 存储的是连续的二进制数字（0 和 1），通过 bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态，key 就是对应元素本身 。我们知道 8 个 bit 可以组成一个 byte，所以 bitmap 本身会极大的节省储存空间\n#### 常用命令\nsetbit 、getbit 、bitcount、bitop\n#### 应用场景\n1、适合需要保存状态信息（比如是否签到、是否登录...）并需要进一步对这些信息进行分析的场景。比如用户签到情况、活跃用户情况、用户行为统计（比如是否点赞过某个视频）\n- 使用场景一：用户行为分析 很多网站为了分析你的喜好，需要研究你点赞过的内容\n  - 记录你喜欢过 001 号小姐姐\n  ```shell\n   127.0.0.1:6379> setbit beauty_girl_001 uid 1\n  ```\n  - 使用场景二：统计活跃用户\n    - 使用时间作为 key，然后用户 ID 为 offset，如果当日活跃过就设置为 1\n    - 对一个或多个保存二进制位的字符串 key 进行位元操作，并将结果保存到 destkey 上。\n    - BITOP 命令支持 AND 、 OR 、 NOT 、 XOR 这四种操作中的任意一种参数\n    ```shell\n    BITOP operation destkey key [key ...]\n    ```\n    ```shell\n    # 初始化数据: \n    127.0.0.1:6379> setbit 20210308 1 1\n    (integer) 0\n    127.0.0.1:6379> setbit 20210308 2 1\n    (integer) 0\n    127.0.0.1:6379> setbit 20210309 1 1\n    (integer) 0\n    # 统计20210308~20210309总活跃用户数: 1\n    127.0.0.1:6379> bitop and desk1 20210308 20210309\n    (integer) 1\n    127.0.0.1:6379> bitcount desk1\n    (integer) 1\n    # 统计20210308~20210309在线活跃用户数: 2\n    127.0.0.1:6379> bitop or desk2 20210308 20210309\n    (integer) 1\n    127.0.0.1:6379> bitcount desk2\n    (integer) 2\n    ```\n  - 使用场景三：用户在线状态\n    - 取或者统计用户在线状态，使用 bitmap 是一个节约空间效率又高的一种方法。\n    - 一个 key，然后用户 ID 为 offset，如果在线就设置为 1，不在线就设置为 0。\n\n2、布隆过滤器\n\n布隆过滤器使用场景\n- 原本有10亿个号码，现在又来了10万个号码，要快速准确判断这10万个号码是否在10亿个号码库中？\n- 接触过爬虫的，应该有这么一个需求，需要爬虫的网站千千万万，对于一个新的网站url，我们如何判断这个url我们是否已经爬过了？\n- 垃圾邮箱的过滤。\n\n图示\n\n![](../img/redis/布隆过滤器图示1.png)\t\t\n\n一种数据结构，是由一串很长的二进制向量组成，可以将其看成一个二进制数组。既然是二进制，那么里面存放的不是0，就是1，但是初始默认值都是0\n\n原理\n\n添加数据\n- 当要向布隆过滤器中添加一个元素key时，我们通过多个hash函数，算出一个值，然后将这个值所在的方格置为1。\n- 比如，下图hash1(key)=1，那么在第2个格子将0变为1（数组是从0开始计数的），hash2(key)=7，那么将第8个格子置位1，依次类推\n- 图示\n![](../img/redis/布隆过滤器添加数据.png)\t\t\t\t\t\t\t\t\n\n判断数据是否存在\n- 通过hash计算出来的位置只要都是1，这个数据一定存在？否，因为其他的key也很有可能通过相同的hash计算出相同的位置，所以，只能判断某个key一定不存在，不能判断一定存在\n\n优缺点\n- 优点 二进制组成的数组，占用内存极少，并且插入和查询速度都足够快\n- 缺点 随着数据的增加，误判率会增加；还有无法判断数据一定存在；另外还有一个重要缺点，无法删除数据\n\n#### 如何在大数据量里实时判断用户名是否存在\n- 首先可以使用布隆，如果判断不存在则直接返回可使用\n- 如果存在，如果允许误判则返回，如果100%确认，则可以通过名字长度去查不同的缓存\n\n--- \n\n### HyperLogLog\nRedis HyperLogLog 是用来做基数统计的算法，HyperLogLog 的优点是，在输入元素的数量或者体积非常非常大时，计算基数所需的空间总是固定 的、并且是很小的。\n\n基数计算（cardinality counting）指的是统计一批数据中的不重复元素的个数，常见于计算独立用户数（UV）、维度的独立取值数等等\n\n比如 1,2,3,4,7,4,7   不重复的数据是 1,2,3,4,7 基数为5\n#### HyperLogLog的原理\nHyperLogLog（简称 HLL）是一种基数（cardinality）算法，用于估计一个集合中的元素数量。它的主要思想是利用概率统计的方法，以极小的空间复杂度来实现高精度的基数统计。\n\nHyperLogLog 的核心是哈希函数和桶。它使用哈希函数将每个元素映射到一个固定大小的二进制字符串（一般为 64 位），然后将这个字符串划分为若干个组（一般为 2^b 个），每个组对应一个桶。对于每个桶，使用特定的算法计算其中元素的基数估计值，并将所有桶的估计值进行组合，得到最终的基数估计值。\n\n具体来说，HyperLogLog 使用了一种特殊的哈希函数，称为 MurmurHash。它可以将任意长度的输入值哈希为一个固定长度的输出值，具有较低的冲突率和较高的随机性。在将元素哈希到二进制字符串时，使用了一种特殊的技巧，称为“前缀零计数法”。它的基本思想是，在二进制字符串中找到第一个 1 的位置，将其前面的所有 0 计数，并将计数值作为该元素所属的桶的索引。\n\n对于每个桶，HyperLogLog 使用了一种特殊的算法，称为“基数估计算法”。它的基本思想是，将桶中所有元素哈希后的二进制字符串中最长的前缀零的长度（即“前导零数”）作为该桶的基数估计值。然后，将所有桶的基数估计值进行组合，得到最终的基数估计值。为了提高估计值的精度，HyperLogLog 还使用了多个哈希函数和多个桶，并对不同的哈希函数和桶进行加权。\n\n需要注意的是，HyperLogLog 是一种概率算法，它的估计值可能存在一定的误差，但是可以通过适当地调整桶的数量和哈希函数的数量来控制误差的大小。另外，HyperLogLog 也具有一定的局限性，比如不能对元素进行添加和删除操作。因此，在使用 HyperLogLog 时需要根据具体应用场景进行选择和权衡。\n\n---\n\n## 内存回收策略\n### Redis过期策略:删除过期时间的key值\n- `定时过期` 每个设置过期时间的key都需要创建一个定时器，到过期时间就会立即清除。该策略可以立即清除过期的数据，对内存很友好；但是会占用大量的CPU资源去处理过期的数据，从而影响缓存的响应时间和吞吐量\n- `惰性过期` 只有当访问一个key时，才会判断该key是否已过期，过期则清除。该策略可以最大化地节省CPU资源，却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问，从而不会被清除，占用大量内存\n- `定期过期` 每隔一定的时间，会扫描一定数量的数据库的expires字典中一定数量的key，并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时，可以在不同情况下使得CPU和内存资源达到最优的平衡效果\n### Redis淘汰策略:内存使用到达maxmemory上限时触发内存淘汰数据\n- LRU算法 最近最少使用算法,也就是说默认删除最近最少使用的键\n- 淘汰策略\n  1. `noeviction`：当内存不足以容纳新写入数据时，新写入操作会报错。\n  2. `allkeys-lru`：当内存不足以容纳新写入数据时，在键空间中，移除最近最少使用的key。\n  3. `allkeys-random`：当内存不足以容纳新写入数据时，在键空间中，随机移除某个key。\n  4. `volatile-lru`：当内存不足以容纳新写入数据时，在设置了过期时间的键空间中，移除最近最少使用的key。\n  5. `volatile-random`：当内存不足以容纳新写入数据时，在设置了过期时间的键空间中，随机移除某个key。\n  6. `volatile-ttl`：当内存不足以容纳新写入数据时，在设置了过期时间的键空间中，有更早过期时间的key优先移除。\n### redis的设置过期时间底层原理\nredis针对TTL时间有专门的dict进行存储，就是redisDb当中的dict *expires字段，dict顾名思义就是一个hashtable，key为对应的rediskey，value为对应的TTL时间。\n```\ntypedef struct redisDb {\n    dict *dict;                 /* The keyspace for this DB */\n    dict *expires;              /* Timeout of keys with a timeout set */\n    ...\n}\n```\n过期键的判断\n\n通过查询过期字典，检查下面的条件判断是否过期\n\n- 检查给定的键是否在过期字典中，如果存在就获取键的过期时间\n- 检查当前 UNIX 时间戳是否大于键的过期时间，是就过期，否则未过期\n\n## 持久化方式\n### RDB快照\n- 当条件满足时 Redis会将某个时间点的数据集保存到一个 RDB文件中，数据的读取和恢复都可以直接通过该文件\n- 什么情况下会触发 RDB操作持久化我们的数据？\n  - 使用 save命令手动持久化数据 需要注意的是，save命令会造成阻塞，在 RDB文件生成期间 Redis不会处理其他的请求\n  - 手动或者自动执行 bgsave命令（bgsave即 background save\n    - 该命令执行时 Redis会调用一个 fork();函数，继而创建一条子线程，rdb文件的生成 就会交给该子线程来处理\n      - 父进程继续接收并处理客户端发来的命令，而子进程开始将内存中的数据写入硬盘中的临时文件\n    - 当子进程写入完所有数据后，会用该临时文件替换旧的RDB文件，至此 一次快照操作完成；子进程退出\n    - 这种方式的优劣已经显而易见了，优点是不会出现阻塞问题 且基本上不会影响到主线程；缺点是子线程会占用一定的 CPU性能\n    - 虽有小弊病但不致命，所以这种方式使用的会更多一些（所有的自动执行默认都使用的是该命令）\n- 在 Redis.conf 配置文件中\n  - `save 900 1`           #在900秒(15分钟)之后，如果至少有1个key发生变化，Redis就会自动触发BGSAVE命令创建快照。\n  - `save 300 10`          #在300秒(5分钟)之后，如果至少有10个key发生变化，Redis就会自动触发BGSAVE命令创建快照。\n  - `save 60 10000`        #在60秒(1分钟)之后，如果至少有10000个key发生变化，Redis就会自动触发BGSAVE命令创建快照。\n### AOF追加\n与快照持久化相比，AOF 持久化 的实时性更好，因此已成为主流的持久化方案。默认情况下 Redis 没有开启 AOF（append only file）方式的持久化，可以通过 appendonly 参数开启：\n- appendonly yes\n\n开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令，Redis 就会将该命令写入硬盘中的 AOF 文件。AOF 文件的保存位置和 RDB 文件的位置相同，都是通过 dir 参数设置的，默认的文件名是 appendonly.aof。\n\n在 Redis 的配置文件中存在三种不同的 AOF 持久化方式，它们分别是：\n- appendfsync always    #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度\n- appendfsync everysec  #每秒钟同步一次，显示地将多个写命令同步到硬盘\n- appendfsync no        #让操作系统决定何时进行同步\n\naof文件的自动重写\n- 该功能可以最大程度的对 aof文件进行瘦身 同时保证数据的完整性\n  - 将重复或者无效的命令从新文件中剔除\n  - 将过期的数据从新文件中剔除\n  - 将可以进行合并的命令进行合并操作，并记录在新文件中\n- 具体实现\n  - 在重写开始之前 Redis会先确认有没有 bgsave（RDB持久化）或者bgrewriteaof（AOF重写）在执行\n  - 主进程 fork出一条子进程，在 fork期间 Redis是阻塞的\n  - 子进程 fork完毕后，主进程会继续处理客户端的请求，所有写命令依然写入缓冲区并根据策略同步到磁盘，保证原有 AOF文件完整和正确\n    - 但需要注意的是，子进程在完成 fork后就不再共享主进程的内存了\n    - 所以在子进程重写 aof文件这段时间内 为了防止丢失数据，主进程不仅要将 数据写入 aof_buf还要写入 aof_rewrite_buf\n  - 子进程根据内存快照，按照命令重写规则写入到新的 AOF文件\n  - 子进程写完新的 AOF文件后，会向主进程发信号，主进程更新统计信息\n  - 主进程将 aof_rewrite_buf中的数据写入到新的 AOF文件中\n  - 使用新的 AOF文件覆盖旧的 AOF文件，重写完成\n## Redis 中的事务\n### 命令\n- `MULTI` 使用 MULTI命令后可以输入多个命令。Redis 不会立即执行这些命令，而是将它们放到队列，当调用了EXEC命令将执行所有命令\n  - 开始事务（MULTI）。\n  - 命令入队(批量操作 Redis 的命令，先进先出（FIFO）的顺序执行)。\n  - 执行事务(EXEC)。\n- `DISCARD` 取消一个事务，它会清空事务队列中保存的所有命令\n- `WATCH` 用于监听指定的键，当调用 EXEC 命令执行事务时，如果一个被 WATCH 命令监视的键被修改的话，整个事务都不会执行，直接返回失败\n\n- Redis 是不支持 roll back 的，因而不满足原子性的（而且不满足持久性）\n\n- Redis 事务提供了一种将多个命令请求打包的功能。然后，再按顺序执行打包的所有命令，并且不会被中途打断。\n\n### Java操作\n```java\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.data.redis.core.StringRedisTemplate;\nimport org.springframework.data.redis.core.SessionCallback;\nimport org.springframework.stereotype.Service;\n\n@Service\npublic class RedisService {\n\n    @Autowired\n    private StringRedisTemplate redisTemplate;\n\n    public void executeTransaction() {\n        redisTemplate.execute(new SessionCallback<Object>() {\n            @Override\n            public <K, V> Object execute(org.springframework.data.redis.core.RedisOperations<K, V> operations) throws DataAccessException {\n                operations.multi(); // 开始事务\n                operations.opsForValue().set(\"key1\", \"value1\");\n                operations.opsForValue().set(\"key2\", \"value2\");\n                return operations.exec(); // 执行事务\n            }\n        });\n    }\n}\n\n```\n## 常问故障场景\n### 缓存雪崩\n什么是 缓存在同一时间大面积的失效，后面的请求都直接落到了数据库上，造成数据库短时间内承受大量请求。 这就好比雪崩一样，摧枯拉朽之势，数据库的压力可想而知，可能直接就被这么多请求弄宕机了\n#### 解决方案\n- 针对 Redis 服务不可用的情况\n  - 采用 Redis 集群，避免单机出现问题整个缓存服务都没办法使用。\n  - 限流，避免同时处理大量的请求\n- 针对热点缓存失效的情况\n  - 设置不同的失效时间比如随机设置缓存的失效时间。\n  - 缓存永不失效\n### 缓存穿透\n什么是 缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中，导致请求直接到了数据库上，根本没有经过缓存这一层。举个例子：某个黑客故意制造我们缓存中不存在的 key 发起大量请求，导致大量请求落到数据库\n#### 解决方案\n- 首先 对请求进行限制，只允许合法的请求获取数据，防止恶意用户发送大量请求\n- 缓存无效 key\n- 布隆过滤器\n  - 将所有可能存在的有效 ID 添加到布隆过滤器中。\n  - 在查询数据之前，先检查布隆过滤器，如果数据可能存在，再去查询缓存或数据库。\n## 集群\n### 主从复制模式\nRedis 提供了复制（replication）功能，可以实现当一台数据库中的数据更新后，自动将更新的数据同步到其他数据库上\n\n在复制的概念中，数据库分为两类，一类是主数据库（master），另一类是从数据库(slave）。主数据库可以进行读写操作，当写操作导致数据变化时会自动将数据同步给从数据库。而从数据库一般是只读的，并接受主数据库同步过来的数据。一个主数据库可以拥有多个从数据库，而一个从数据库只能拥有一个主数据库\n\n引入主从复制机制的目的有两个\n- 一个是读写分离，分担 \"master\" 的读写压力\n- 一个是方便做容灾恢复\n\n#### 主从复制原理\n- 从数据库启动成功后，连接主数据库，发送 SYNC 命令；\n- 主数据库接收到 SYNC 命令后，开始执行 BGSAVE 命令生成 RDB 文件并使用缓冲区记录此后执行的所有写命令；\n- 主数据库 BGSAVE 执行完后，向所有从数据库发送快照文件，并在发送期间继续记录被执行的写命令；\n- 从数据库收到快照文件后丢弃所有旧数据，载入收到的快照；\n- 主数据库快照发送完毕后开始向从数据库发送缓冲区中的写命令；\n- 从数据库完成对快照的载入，开始接收命令请求，并执行来自主数据库缓冲区的写命令；（从数据库初始化完成）\n- 主数据库每执行一个写命令就会向从数据库发送相同的写命令，从数据库接收并执行收到的写命令（从数据库初始化完成后的操作）\n- 出现断开重连后，2.8之后的版本会将断线期间的命令传给重数据库，增量复制。\n- 主从刚刚连接的时候，进行全量同步；全同步结束后，进行增量同步。当然，如果有需要，slave 在任何时候都可以发起全量同步。Redis 的策略是，无论如何，首先会尝试进行增量同步，如不成功，要求从机进行全量同步。\n#### 主从复制优缺点\n优点\n- 支持主从复制，主机会自动将数据同步到从机，可以进行读写分离；\n- 为了分载 Master 的读操作压力，Slave 服务器可以为客户端提供只读操作的服务，写服务仍然必须由Master来完成；\n- Slave 同样可以接受其它 Slaves 的连接和同步请求，这样可以有效的分载 Master 的同步压力；\n- Master Server 是以非阻塞的方式为 Slaves 提供服务。所以在 Master-Slave 同步期间，客户端仍然可以提交查询或修改请求；\n- Slave Server 同样是以非阻塞的方式完成数据同步。在同步期间，如果有客户端提交查询请求，Redis则返回同步之前的数据；\n\n缺点\n- Redis不具备自动容错和恢复功能，主机从机的宕机都会导致前端部分读写请求失败，需要等待机器重启或者手动切换前端的IP才能恢复（也就是要人工介入）；\n- 主机宕机，宕机前有部分数据未能及时同步到从机，切换IP后还会引入数据不一致的问题，降低了系统的可用性；\n- 如果多个 Slave 断线了，需要重启的时候，尽量不要在同一时间段进行重启。因为只要 Slave 启动，就会发送sync 请求和主机全量同步，当多个 Slave 重启的时候，可能会导致 Master IO 剧增从而宕机。\n- Redis 较难支持在线扩容，在集群容量达到上限时在线扩容会变得很复杂；\n### Sentinel（哨兵）模式\n哨兵模式是一种特殊的模式，首先 Redis 提供了哨兵的命令，哨兵是一个独立的进程，作为进程，它会独立运行。其原理是哨兵通过发送命令，等待Redis服务器响应，从而监控运行的多个 Redis 实例\n\n#### 原理图\n<img src=\"../img/redis/哨兵模式.png\" width=\"50%\" />\n\n#### 哨兵模式的作用\n- 通过发送命令，让 Redis 服务器返回监控其运行状态，包括主服务器和从服务器；\n- 当哨兵监测到 master 宕机，会自动将 slave 切换成 master ，然后通过发布订阅模式通知其他的从服务器，修改配置文件，让它们切换主机；\n\n#### 多哨兵模式\n<img src=\"../img/redis/多哨兵模式.png\" width=\"50%\" />\n\n一个哨兵进程对Redis服务器进行监控，也可能会出现问题，为此，我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控，这样就形成了多哨兵模式\n\n#### 故障切换的过程\n- 假设主服务器宕机，哨兵1先检测到这个结果，系统并不会马上进行 failover 过程，仅仅是哨兵1主观的认为主服务器不可用，这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用，并且数量达到一定值时，那么哨兵之间就会进行一次投票，投票的结果由一个哨兵发起，进行 failover 操作。切换成功后，就会通过发布订阅模式，让各个哨兵把自己监控的从服务器实现切换主机，这个过程称为客观下线。这样对于客户端而言，一切都是透明的。\n\n#### 哨兵模式的工作方式\n- 每个Sentinel（哨兵）进程以每秒钟一次的频率向整个集群中的 Master 主服务器，Slave 从服务器以及其他Sentinel（哨兵）进程发送一个 PING 命令。\n- 如果一个实例（instance）距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值， 则这个实例会被 Sentinel（哨兵）进程标记为主观下线（SDOWN）\n- 如果一个 Master 主服务器被标记为主观下线（SDOWN），则正在监视这个 Master 主服务器的所有 Sentinel（哨兵）进程要以每秒一次的频率确认 Master 主服务器的确进入了主观下线状态\n- 当有足够数量的 Sentinel（哨兵）进程（大于等于配置文件指定的值）在指定的时间范围内确认 Master 主服务器进入了主观下线状态（SDOWN）， 则 Master 主服务器会被标记为客观下线（ODOWN）\n- 在一般情况下， 每个 Sentinel（哨兵）进程会以每 10 秒一次的频率向集群中的所有 Master 主服务器、Slave 从服务器发送 INFO 命令。\n- 当 Master 主服务器被 Sentinel（哨兵）进程标记为客观下线（ODOWN）时，Sentinel（哨兵）进程向下线的 Master 主服务器的所有 Slave 从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。\n- 若没有足够数量的 Sentinel（哨兵）进程同意 Master主服务器下线， Master 主服务器的客观下线状态就会被移除。若 Master 主服务器重新向 Sentinel（哨兵）进程发送 PING 命令返回有效回复，Master主服务器的主观下线状态就会被移除\n#### 哨兵模式的优缺点\n优点\n- 哨兵模式是基于主从模式的，所有主从的优点，哨兵模式都具有。\n- 主从可以自动切换，系统更健壮，可用性更高(可以看作自动版的主从复制)。\n\n缺点\n- Redis较难支持在线扩容，在集群容量达到上限时在线扩容会变得很复杂。\n### Cluster 集群模式\nRedis Cluster是一种服务器 Sharding 技术，3.0版本开始正式提供。Redis 的哨兵模式基本已经可以实现高可用，读写分离 ，但是在这种模式下每台 Redis 服务器都存储相同的数据，很浪费内存，所以在 redis3.0上加入了 Cluster 集群模式，实现了 Redis 的分布式存储，也就是说每台 Redis 节点上存储不同的内容\n\n#### 主从复制模型\n- 为了保证高可用，redis-cluster集群引入了主从复制模型，一个主节点对应一个或者多个从节点，当主节点宕机的时候，就会启用从节点\n\n##### 数据分区方式\n###### 顺序分布\n\n<img src=\"../img/redis/cluster数据分布方式-顺序分布.png\" width=\"50%\" />\n  \n特点：键值业务相关；数据分散，但是容易造成访问倾斜；支持顺序访问；支持批量操作\n\n###### 哈希分布\n\n<img src=\"../img/redis/cluster数据分布方式-hash分布.png\" width=\"50%\" />\n\n特点：数据分散度高；键值分布与业务无关；不支持顺序访问；支持批量操作。\n\n###### 一致性哈希分布\n\n问题：对于上面介绍的哈希分布，大家可以想一下，如果向集群中增加节点，或者集群中有节点宕机，这个时候应该怎么处理？\n\n增加节点\n\n<img src=\"../img/redis/一致性hash增加节点.png\" width=\"50%\" />\n    \n- 如上图所示，总共10个数据通过节点取余hash(key)%/3 的方式分布到3个节点，这时候由于访问量变大，要进行扩容，由 3 个节点变为 4 个节点。\n- 我们发现，如图所示，数据除了标红的1 2 没有进行迁移，别的数据都要进行变动，达到了80%，如果这时候并发很高，80%的数据都要从下层节点（比如数据库）获取，会给下层节点造成很大的访问压力，这是不能接受的。\n- 即使我们进行翻倍扩容，从3个节点增加到6个节点，其数据迁移也在50%左右\n   \n删除节点\n\n<img src=\"../img/redis/一致性hash删除节点.png\" width=\"50%\" />\n\n上图其实不管是哪一个节点宕机，其数据迁移量都会超过50%。基本上也是我们所不能接受的 \n\n那么如何使得集群中新增节点或者删除节点时，数据迁移量最少？——一致性哈希算法诞生。\n      \n- 原理图\n\n<img src=\"../img/redis/一致性hash原理图.png\" width=\"30%\" />\n\n假设有一个哈希环，从0到2的32次方，均匀的分成三份，中间存放三个节点，沿着顺时针旋转，从Node1到Node2之间的数据，存放在Node2节点上；从Node2到Node3之间的数据，存放在Node3节点上，依次类推。\n\n假设Node1节点宕机，那么原来Node3到Node1之间的数据这时候改为存放到Node2节点上，Node2到Node3之间数据保持不变，原来Node1到Node2之间的数据还是存放在Node2上，也就是只影响三分之一的数据，节点越多，影响数据越少\n\n虚拟节点\n- hash 算法并不是保证绝对的平衡，如果 cache 较少的话，对象并不能被均匀的映射到 cache 上，为了解决这种情况， consistent hashing 引入了“虚拟节点”的概念，它可以如下定义\n  - 虚拟节点（ virtual node ）是实际节点在 hash 空间的复制品（ replica ），一实际个节点对应了若干个“虚拟节点”，这个对应个数也成为“复制个数”，“虚拟节点”在 hash 空间中以 hash 值排列\n- 仍以仅部署 cache A 和 cache C 的情况为例。现在我们引入虚拟节点，并设置“复制个数”为 2 ，这就意味着一共会存在 4 个“虚拟节点”， cache A1, cache A2 代表了 cache A； cache C1, cache C2 代表了 cache C 。此时，对象到“虚拟节点”的映射关系为：\n  - objec1->cache A2 ； objec2->cache A1 ； objec3->cache C1 ； objec4->cache C2 ；\n- 因此对象 object1 和 object2 都被映射到了 cache A 上，而 object3 和 object4 映射到了 cache C 上；平衡性有了很大提高。 引入“虚拟节点”后，映射关系就从 { 对象 -> 节点 } 转换到了 { 对象 -> 虚拟节点 } 。查询物体所在 cache 时的映射关系如下图 所示\n\n<img src=\"../img/redis/虚拟节点.png\" width=\"50%\" />\n\n#### Redis Cluster虚拟槽分区\nRedis集群数据分布没有使用一致性哈希分布，而是使用虚拟槽分区概念\n\nRedis内部内置了序号 0-16383 个槽位，每个槽位可以用来存储一个数据集合，将这些槽位按顺序分配到集群中的各个节点。每次新的数据到来，会通过哈希函数 CRC16(key) 算出将要存储的槽位下标，然后通过该下标找到前面分配的Redis节点，最后将数据存储到该节点中\n\n<img src=\"../img/redis/redis-hash槽.png\" width=\"50%\" />\n\n特点\n- 解耦 数据 和 节点 之间的关系，简化了节点 扩容 和 收缩 难度。\n- 节点自身 维护槽的 映射关系，不需要 客户端 或者 代理服务 维护 槽分区元数据。\n- 支持 节点、槽、键 之间的 映射查询，用于 数据路由、在线伸缩 等场景\n- 只有一个数据库db0\n\n# Redis Cluster 节点通信原理：Gossip 算法\n\n## Gossip 简介\nGossip 协议，顾名思义，就像流言蜚语一样，利用一种随机、带有传染性的方式，将信息传播到整个网络中，并在一定时间内，使得系统内的所有节点数据一致。对你来说，掌握这个协议不仅能很好地理解这种最常用的，实现最终一致性的算法，也能在后续工作中得心应手地实现数据的最终一致性。\n\nGossip 协议又称 epidemic 协议（epidemic protocol），是基于流行病传播方式的节点或者进程之间信息交换的协议，在P2P网络和分布式系统中应用广泛，它的方法论也特别简单：\n\n在一个处于有界网络的集群里，如果每个节点都随机与其他节点交换特定信息，经过足够长的时间后，集群各个节点对该份信息的认知终将收敛到一致。\n这里的“特定信息”一般就是指集群状态、各节点的状态以及其他元数据等。Gossip协议是完全符合 BASE 原则，可以用在任何要求最终一致性的领域，比如分布式存储和注册中心。另外，它可以很方便地实现弹性集群，允许节点随时上下线，提供快捷的失败检测和动态负载均衡等。\n\n此外，Gossip 协议的最大的好处是，即使集群节点的数量增加，每个节点的负载也不会增加很多，几乎是恒定的。这就允许 Redis Cluster 或者 Consul 集群管理的节点规模能横向扩展到数千个。\n\n## 节点状态和消息类型\nRedis Cluster 中的每个节点都维护一份自己视角下的当前整个集群的状态，主要包括：\n- 当前集群状态\n- 集群中各节点所负责的 slots信息，及其migrate状态\n- 集群中各节点的master-slave状态\n- 集群中各节点的存活状态及怀疑Fail状态\n  也就是说上面的信息，就是集群中Node相互八卦传播流言蜚语的内容主题，而且比较全面，既有自己的更有别人的，这么一来大家都相互传，最终信息就全面而且一致了。\n\nRedis Cluster 的节点之间会相互发送多种消息，较为重要的如下所示：\n\n- MEET：通过「cluster meet ip port」命令，已有集群的节点会向新的节点发送邀请，加入现有集群，然后新节点就会开始与其他节点进行通信；\n- PING：节点按照配置的时间间隔向集群中其他节点发送 ping 消息，消息中带有自己的状态，还有自己维护的集群元数据，和部分其他节点的元数据；\n- PONG: 节点用于回应 PING 和 MEET 的消息，结构和 PING 消息类似，也包含自己的状态和其他信息，也可以用于信息广播和更新；\n- FAIL: 节点 PING 不通某节点后，会向集群所有节点广播该节点挂掉的消息。其他节点收到消息后标记已下线。\n\n通过上述这些消息，集群中的每一个实例都能获得其它所有实例的状态信息。这样一来，即使有新节点加入、节点故障、Slot 变更等事件发生，实例间也可以通过 PING、PONG 消息的传递，完成集群状态在每个实例上的同步。下面，我们依次来看看几种常见的场景。\n\n### 定时 PING/PONG 消息\nRedis Cluster 中的节点都会定时地向其他节点发送 PING 消息，来交换各个节点状态信息，检查各个节点状态，包括在线状态、疑似下线状态 PFAIL 和已下线状态 FAIL。\n\nRedis 集群的定时 PING/PONG 的工作原理可以概括成两点：\n\n- 一是，每个实例之间会按照一定的频率，从集群中随机挑选一些实例，把 PING 消息发送给挑选出来的实例，用来检测这些实例是否在线，并交换彼此的状态信息。PING 消息中封装了发送消息的实例自身的状态信息、部分其它实例的状态信息，以及 Slot 映射表。\n- 二是，一个实例在接收到 PING 消息后，会给发送 PING 消息的实例，发送一个 PONG 消息。PONG 消息包含的内容和 PING 消息一样。\n\n下图显示了两个实例间进行 PING、PONG 消息传递的情况，其中实例一为发送节点，实例二是接收节点\n\n![](../img/redis/redis-cluster-pingpong.png)\n\n### 新节点上线\nRedis Cluster 加入新节点时，客户端需要执行 CLUSTER MEET 命令，如下图所示。\n\n![](../img/redis/redis-cluster-新节点上线.png)\n\n节点一在执行 CLUSTER MEET 命令时会首先为新节点创建一个 clusterNode 数据，并将其添加到自己维护的 clusterState 的 nodes 字典中。有关 clusterState 和 clusterNode 关系，我们在最后一节会有详尽的示意图和源码来讲解。\n\n然后节点一会根据据 CLUSTER MEET 命令中的 IP 地址和端口号，向新节点发送一条 MEET 消息。新节点接收到节点一发送的MEET消息后，新节点也会为节点一创建一个 clusterNode 结构，并将该结构添加到自己维护的 clusterState 的 nodes 字典中。\n\n接着，新节点向节点一返回一条PONG消息。节点一接收到节点B返回的PONG消息后，得知新节点已经成功的接收了自己发送的MEET消息。\n\n最后，节点一还会向新节点发送一条 PING 消息。新节点接收到该条 PING 消息后，可以知道节点A已经成功的接收到了自己返回的P ONG消息，从而完成了新节点接入的握手操作。\n\nMEET 操作成功之后，节点一会通过稍早时讲的定时 PING 机制将新节点的信息发送给集群中的其他节点，让其他节点也与新节点进行握手，最终，经过一段时间后，新节点会被集群中的所有节点认识。\n\n### 节点疑似下线和真正下线\nRedis Cluster 中的节点会定期检查已经发送 PING 消息的接收方节点是否在规定时间 ( cluster-node-timeout ) 内返回了 PONG 消息，如果没有则会将其标记为疑似下线状态，也就是 PFAIL 状态，如下图所示。\n\n![](../img/redis/节点疑似下线和真正下线1.png)\n\n然后，节点一会通过 PING 消息，将节点二处于疑似下线状态的信息传递给其他节点，例如节点三。节点三接收到节点一的 PING 消息得知节点二进入 PFAIL 状态后，会在自己维护的 clusterState 的 nodes 字典中找到节点二所对应的 clusterNode 结构，并将主节点一的下线报告添加到 clusterNode 结构的 fail_reports 链表中。\n\n![](../img/redis/节点疑似下线和真正下线2.png)\n\n随着时间的推移，如果节点十 (举个例子) 也因为 PONG 超时而认为节点二疑似下线了，并且发现自己维护的节点二的 clusterNode 的 fail_reports 中有半数以上的主节点数量的未过时的将节点二标记为 PFAIL 状态报告日志，那么节点十将会把节点二将被标记为已下线 FAIL 状态，并且节点十会立刻向集群其他节点广播主节点二已经下线的 FAIL 消息，所有收到 FAIL 消息的节点都会立即将节点二状态标记为已下线。如下图所示。\n\n![](../img/redis/节点疑似下线和真正下线3.png)\n\n需要注意的是，报告疑似下线记录是由时效性的，如果超过 cluster-node-timeout *2 的时间，这个报告就会被忽略掉，让节点二又恢复成正常状态。\n\n\n# Redis cluster伸缩的原理\n## 集群扩容\n<img src=\"../img/redis/Redis-cluster扩容原理.png\" width=\"50%\" />\n\n每个master把一部分槽和数据迁移到新的节点node04\n\n## 集群收缩\n<img src=\"../img/redis/Redis-cluster收缩原理.png\" width=\"50%\" />\n\n- 如果下线的是slave，那么通知其他节点忘记下线的节点\n- 如果下线的是master，那么将此master的slot迁移到其他master之后，通知其他节点忘记此master节点\n- 其他节点都忘记了下线的节点之后，此节点就可以正常停止服务了\n\n## redis cluster为什么没有使用一致性hash算法，而是使用了哈希槽预分片？\n缓存热点问题：一致性哈希算法在节点太少时，容易因为数据分布不均匀而造成缓存热点的问题。一致性哈希算法可能集中在某个hash区间内的值特别多，会导致大量的数据涌入同一个节点，造成master的热点问题(如同一时间20W的请求都在某个hash区间内)。\n\n## redis的hash槽为什么是16384(2^14)个卡槽，而不是65536(2^16)个？\n- 如果槽位为65536，发送心跳信息的消息头达8k，发送的心跳包过于庞大。\n- redis的集群主节点数量基本不可能超过1000个。集群节点越多，心跳包的消息体内携带的数据越多。如果节点过1000个，也会导致网络拥堵。因此redis作者，不建议redis cluster节点数量超过1000个。 那么，对于节点数在1000以内的redis cluster集群，16384个槽位够用了。没有必要拓展到65536个。\n- 槽位越小，节点少的情况下，压缩率高。\n\n# redis索引\nRedis并不支持索引，需要自己来维护\n\n对于非范围唯一索引，我们可以简单的把索引存为k-v即可\n\n对于范围索引或非唯一索引，则要使用redis 的 zset来实现。\n\n举例一个传统的用户系统例子\n```java\nuid 用户id\nname 用户名\ncredit 用户积分\ntype 类型\n```\n可以直接放到一个hashset中\n```shell\nhmset usr:1 uid 1 name aaa credit 10 type 0\nhmset usr:2 uid 2 name bbb credit 20 type 1\n```\n通过uid检索很快，但是如果要查询type=1的用户，则只能全扫描！\n\n在关系数据库中，我们可以简单在type上建立索引\n```sql\nselect * from usr where type=1\n```\n这样的SQL就可以高效执行了。redis中需要我们自己再维护一个zset\n```shell\nzadd usr.index.type 0 0:1\nzadd usr.index.type 0 1:2\n```\n注意,所有权重都设置成0,这样可以直接按值检索,然后可以通过\n```shell\nzrangebylex usr.index.type [1: (1;\n```\n\n# 参考文章\n- https://www.jianshu.com/p/53083f5f2ddc\n- https://cloud.tencent.com/developer/article/1608410\n"
  },
  {
    "path": "Redis/Redis常见面试题.md",
    "content": "## 储存结构和使用场景\n- `string` 字符串、数字，一些简单的k-v数据\n- `list` 数组、集合，简单消息队列（缺点很多，比如不能重复消费，不能多订阅，无\"消息持久化\"，无确认机制）\n- `hash` 储存对象\n- `set` 不可重复数据、做交并集的数据\n- `zset` 排序的不可重复数据，比如 一个直播间的礼物排行榜\n\n## 淘汰策略\n1. `noeviction`：当内存不足以容纳新写入数据时，新写入操作会报错。\n2. `allkeys-lru`：当内存不足以容纳新写入数据时，在键空间中，移除最近最少使用的key。\n3. `allkeys-random`：当内存不足以容纳新写入数据时，在键空间中，随机移除某个key。\n4. `volatile-lru`：当内存不足以容纳新写入数据时，在设置了过期时间的键空间中，移除最近最少使用的key。\n5. `volatile-random`：当内存不足以容纳新写入数据时，在设置了过期时间的键空间中，随机移除某个key。\n6. `volatile-ttl`：当内存不足以容纳新写入数据时，在设置了过期时间的键空间中，有更早过期时间的key优先移除。\n\n## AOF文件很大怎么办\n执行 BGREWRITEAOF 命令，对AOF文件重写\n\n## 为什么Redis使用跳表，不使用红黑树或B+树\n\nRedis 使用跳表的原因\n\nRedis 在实现有序集合（ZSet）时，使用的是跳表（SkipList），而没有选择红黑树或 B+ 树。\n\n### 2.1 跳表的优点\n\n- 实现简单：跳表的结构相对简单，使用多级索引进行加速。虽然在理想情况下，它的时间复杂度和红黑树相同，都是 O(log n)，但跳表的实现代码简单且直观，插入和删除也较为轻量级。\n- 支持区间查询：Redis 使用的跳表结构特别适合范围查询，跳表允许快速的从某个节点开始进行逐步遍历，非常适合 Redis 中经常需要的范围查询操作（如查找某个分数范围内的所有元素）。\n- 顺序性：跳表天然是有序的，Redis 的有序集合要求能够对元素按照分数进行排序并保持有序，这一要求跳表可以很好地满足。\n### 2.2 为什么不使用红黑树？\n- 红黑树的复杂性：红黑树的实现虽然也能保证 O(log n) 的时间复杂度，但插入、删除和调整平衡的逻辑相对复杂，特别是在 Redis 这种高性能、实时要求的系统中，代码简洁和可维护性很重要。跳表在处理区间查询时的遍历逻辑比红黑树更直观。\n- 区间操作性能：红黑树虽然可以通过中序遍历实现区间查询，但性能和跳表相比，略显不足。跳表从某个节点可以直接开始线性扫描，而红黑树需要从根节点逐步找到起始点，跳表在这方面有天然优势。\n### 2.3 为什么不选择 B+ 树？\n- B+ 树适合磁盘存储：和 HashMap 类似，B+ 树在内存中的查找效率并不如跳表。B+ 树的节点结构复杂，维护成本高，且 Redis 是基于内存的数据库系统，不需要像 B+ 树那样通过层次结构优化磁盘 I/O。\n- Redis 的简单高效设计理念：Redis 追求的是极简和高效，而跳表比 B+ 树的实现简单，操作也更加高效。\n\n总结：\n\n- Redis 选择跳表：跳表的实现简单，支持快速区间查询，非常适合 Redis 中有序集合的场景需求。相比红黑树，跳表在遍历和范围查询中表现更优，而相比 B+ 树，跳表的内存使用更加轻量级，更加适合 Redis 的内存结构。\n"
  },
  {
    "path": "Spring/Spring.md",
    "content": "# spring\n\n## 架构图\n\n<img src=\"../img/spring/spring架构图.png\" width=\"50%\" />\n\n## 模块\n\n### Core Container\n\n核心容器(Core Container)\n\n- `spring-beans` 该模块是依赖注入IoC与DI的最基本实现\n- `spring-core` 该模块是Bean工厂与bean的装配\n- `spring-context` 该模块构架于核心模块之上，它扩展了 BeanFactory，为它添加了 Bean 生命周期控制、框架事件体系以及资源加载透明化等功能。ApplicationContext 是该模块的核心接口，它的超类是\n  BeanFactory。与BeanFactory 不同，ApplicationContext 容器实例化后会自动对所有的单实例 Bean 进行实例化与依赖关系的装配，使之处于待用状态\n- `spring-context-indexer` 该模块是 Spring 的类管理组件和 Classpath 扫描\n- `spring-context-support` 该模块是对 Spring IOC 容器的扩展支持，以及 IOC 子容器\n- `spring-expression` 该模块是Spring表达式语言块是统一表达式语言（EL）的扩展模块，可以查询、管理运行中的对象，同时也方便的可以调用对象方法、操作数组、集合等\n\n### Data Access/Integration\n\n数据访问/集成\n\n- `spring-jdbc` 该模块提供了 JDBC抽象层，它消除了冗长的 JDBC 编码和对数据库供应商特定错误代码的解析\n- `spring-tx` 该模块支持编程式事务和声明式事务，可用于实现了特定接口的类和所有的 POJO 对象。编程式事务需要自己写beginTransaction()、commit()、rollback()\n  等事务管理方法，声明式事务是通过注解或配置由 spring 自动处理，编程式事务粒度更细\n- `spring-orm` 该模块提供了对流行的对象关系映射 API的集成，包括 JPA、JDO 和 Hibernate 等。通过此模块可以让这些 ORM 框架和 spring 的其它功能整合，比如前面提及的事务管理\n- `spring-oxm` 该模块提供了对 OXM 实现的支持，比如JAXB、Castor、XML Beans、JiBX、XStream等\n- `spring-jms` 该模块包含生产（produce）和消费（consume）消息的功能。从Spring 4.1开始，集成了 spring-messaging 模块\n\n### Web\n\n网络部分\n\n- `spring-web` 该模块为 Spring 提供了最基础 Web 支持，主要建立于核心容器之上，通过 Servlet 或者 Listeners 来初始化 IOC 容器，也包含一些与 Web 相关的支持\n- `spring-webmvc` 该模块众所周知是一个的 Web-Servlet 模块，实现了 Spring MVC（model-view-Controller）的 Web 应用\n- `spring-websocket` 该模块主要是与 Web 前端的全双工通讯的协议\n- `spring-webflux` 该模块是一个新的非堵塞函数式 Reactive Web 框架，可以用来建立异步的，非阻塞，事件驱动的服务，并且扩展性非常好。\n\n### 面向切面编程(AOP和Aspects)\n\n- `spring-aop` 该模块是Spring的另一个核心模块，是 AOP 主要的实现模块\n- `spring-aspects` 该模块提供了对 AspectJ 的集成，主要是为 Spring AOP提供多种 AOP 实现方法，如前置方法后置方法等\n\n### 设备(Instrumentation)\n\n- `spring-instrument` 该模块是基于JAVA SE 中的\"java.lang.instrument\"进行设计的，应该算是 AOP的一个支援模块，主要作用是在 JVM\n  启用时，生成一个代理类，程序员通过代理类在运行时修改类的字节，从而改变一个类的功能，实现 AOP 的功能\n\n### 消息(Messaging)\n\n- `spring-messaging` 是从 Spring4 开始新加入的一个模块，主要职责是为 Spring 框架集成一些基础的报文传送应用\n\n### 测试(Test)\n\n- `spring-test` 主要为测试提供支持的，通过 JUnit 和 TestNG 组件支持单元测试和集成测试。它提供了一致性地加载和缓存 Spring 上下文，也提供了用于单独测试代码的模拟对象（mock object）\n\n## IOC\n\n### IOC是什么？\n\n控制反转即IoC (Inversion of Control)，它把传统上由程序代码直接操控的对象的调用权交给容器，通过容器来实现对象组件的装配和管理。所谓的“控制反转”概念就是对组件对象控制权的转移，从程序代码本身转移到了外部容器。\n\nSpring IOC 负责创建对象，管理对象（通过依赖注入（DI），装配对象，配置对象，并且管理这些对象的整个生命周期。\n\n### 使用IOC的好处\n\n- 不用自己组装，拿来就用。\n- 享受单例的好处，效率高，不浪费空间\n- 便于单元测试，方便切换mock组件\n- 便于进行AOP操作，对于使用者是透明的\n- 统一配置，便于修改\n\n## BeanFactory 和 ApplicationContext有什么区别\n\nBeanFactory和ApplicationContext是Spring的两大核心接口，都可以当做Spring的容器。其中ApplicationContext是BeanFactory的子接口。\n\n### 依赖关系\n\nBeanFactory：是Spring里面最底层的接口，包含了各种Bean的定义，读取bean配置文档，管理bean的加载、实例化，控制bean的生命周期，维护bean之间的依赖关系。\n\nApplicationContext接口作为BeanFactory的派生，除了提供BeanFactory所具有的功能外，还提供了更完整的框架功能：\n\n- 继承MessageSource，因此支持国际化。\n- 统一的资源文件访问方式。\n- 提供在监听器中注册bean的事件。\n- 同时加载多个配置文件。\n- 载入多个（有继承关系）上下文 ，使得每一个上下文都专注于一个特定的层次，比如应用的web层。\n\n### 加载方式\n\nBeanFactroy采用的是延迟加载形式来注入Bean的，即只有在使用到某个Bean时(调用getBean())\n，才对该Bean进行加载实例化。这样，我们就不能发现一些存在的Spring的配置问题。如果Bean的某一个属性没有注入，BeanFacotry加载后，直至第一次使用调用getBean方法才会抛出异常。\n\nApplicationContext，它是在容器启动时，一次性创建了所有的Bean。这样，在容器启动时，我们就可以发现Spring中存在的配置错误，这样有利于检查所依赖属性是否注入。\nApplicationContext启动后预载入所有的单实例Bean，通过预载入单实例bean ,确保当你需要的时候，你就不用等待，因为它们已经创建好了。\n\n相对于基本的BeanFactory，ApplicationContext 唯一的不足是占用内存空间。当应用程序配置Bean较多时，程序启动较慢。\n\n### 创建方式\n\nBeanFactory通常以编程的方式被创建，ApplicationContext还能以声明的方式创建，如使用ContextLoader。\n\n### 注册方式\n\nBeanFactory和ApplicationContext都支持BeanPostProcessor、BeanFactoryPostProcessor的使用，但两者之间的区别是：BeanFactory需要手动注册，而ApplicationContext则是自动注册。\n\n### Spring IoC的初始化过程\n\n#### IOC粗略总结\n\n1. 首先入口是xml或者注解或者其他形式，要实现beanDefinationReader接口，然后 读取的时候，会将他们解析为bean的定义信息；\n2. beanFactory在加载bean信息实例化（底层用的反射）之前，spring加了一个接口beanFactoryPostProcessor,用作扩展用。\n3. beanFactory内部实例化bean之后，在要初始化bean对象之前，增加了一个一系列aware接口，将他的容器，以及工厂都暴露出来供使用者做扩展用。\n4. 经过一些列容器以及容器对象的注入之后，在初始化之前，spring又增加了一个接口 beanPostProcessor,该接口是可以作用于所有创建的bean,在初始化前后，均能通过重写该接口获取bean对象进行定制操作。\n5. 经过实现初始化接口完成初始化功能。\n6. 经过实现销毁接口disposableBean结束其生命。\n\n#### 重要的组件\n\n- `BeanDefinition` 描述bean的属性的接口，例如bean的scope是单例还是多例，构造方法，有哪些property value，依赖等，相当于对这个bean的一份身份描述\n    - Bean配置 --> BeanDefinition --> Bean对象\n    -\n    懒加载情况下，refresh只是把BeanDefinition注册到BeanFactory中，而不是把Bean注册到BeanFactory中。在调用上下文的getBean的时候才会去根据BeanDefinition生成具体的bean对象\n- `BeanDefinitionMap`\n- `BeanFactory`\n    - spring的基础bean容器\n    - 相当于存放所有bean的容器\n    - 主要负责管理和提供各种 bean 的生命周期\n- `ApplicationContext`\n    - BeanFactory 的子接口，在 BeanFactory 的基础上构建，是相对比较高级的 IoC 容器实现。包含 BeanFactory\n      的所有功能，还提供了其他高级的特性，比如：事件发布、国际化信息支持、统一资源加载策略等。正常情况下，我们都是使用的 ApplicationContext\n    - 相当于丰富了beanfactory的功能，这里理解为上下文就好\n- `FactoryBean`\n    - FactoryBean 是 Spring 提供的一个接口，允许开发者通过实现该接口来创建复杂的 bean 对象。\n    - 用于自定义对象的创建过程\n#### 源码解析\n\n首先抛开其他组件的启动，我们只需要引入spring-context就可以启动一个容器了\n\n```xml\n\n<dependency>\n    <groupId>org.springframework</groupId>\n    <artifactId>spring-context</artifactId>\n</dependency>\n```\n\n而在springboot出来之前最常见的加载bean的方式是读取配置文件\n\n```java\npublic static void main(String[]args){\n        ApplicationContext context=new ClassPathXmlApplicationContext(\"classpath:applicationfile.xml\");\n}\n```\n\n这里ApplicationContext是一个接口，主要的实现类有：\n\n- ClassPathXmlApplicationContext 需要一个 xml 配置文件在系统中的路径\n- FileSystemXmlApplicationContext 需要一个 xml 配置文件在系统中的路径\n- AnnotationConfigApplicationContext 基于注解，大势所趋\n\n下面的分析都基于 ClassPathXmlApplicationContext 进行分析，因为比较好理解点\n\n在 resources 目录新建一个配置文件，文件名随意，通常叫 application.xml 或 application-xxx.xml就可以了,对应的类实现一个：\n\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<beans xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n       xmlns=\"http://www.springframework.org/schema/beans\"\n       xsi:schemaLocation=\"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd\"\n       default-autowire=\"byName\">\n\n    <bean id=\"messageService\" class=\"com.javadoop.example.MessageServiceImpl\"/>\n</beans>\n```\n\nmain\n\n```java\npublic class App {\n    public static void main(String[] args) {\n        // 用我们的配置文件来启动一个 ApplicationContext\n        ApplicationContext context = new ClassPathXmlApplicationContext(\"classpath:application.xml\");\n\n        System.out.println(\"context 启动成功\");\n\n        // 从 context 中取出我们的 Bean，而不是用 new MessageServiceImpl() 这种方式\n        MessageService messageService = context.getBean(MessageService.class);\n        // 这句将输出: hello world\n        System.out.println(messageService.getMessage());\n    }\n}\n```\n\n在构造方法中的refresh()方法是启动加载整个容器的关键方法\n\n方法在springboot容器启动时也会加载,方法为\n\n- org.springframework.boot.SpringApplication#run\n- org.springframework.boot.SpringApplication#refreshContext\n\n```java\n@Override\npublic void refresh()throws BeansException,IllegalStateException{\n    // 1. 首先是一个synchronized加锁，当然要加锁，不然你先调一次refresh()然后这次还没处理完又调一次，就会乱套了；\n    synchronized (this.startupShutdownMonitor){\n        // 2. 这个方法是做准备工作的，记录容器的启动时间、标记“已启动”状态、处理配置文件中的占位符，可以点进去看看，这里就不多说了。\n        prepareRefresh();\n\n        // 3. 这个就很重要了，这一步是把配置文件解析成一个个Bean，并且注册到BeanFactory中，注意这里只是注册进去，并没有初始化。先继续往下看，等会展开这个方法详细解读\n        ConfigurableListableBeanFactory beanFactory=obtainFreshBeanFactory();\n\n        //4. 这个方法的作用是：设置 BeanFactory 的类加载器，添加几个 BeanPostProcessor，手动注册几个特殊的 bean，这里都是spring里面的特殊处理，然后继续往下看\n        prepareBeanFactory(beanFactory);\n\n        try{\n            // 5. 方法是提供给子类的扩展点，到这里的时候，所有的 Bean 都加载、注册完成了，但是都还没有初始化，具体的子类可以在这步的时候添加一些特殊的 BeanFactoryPostProcessor 的实现类，来完成一些其他的操作。\n            postProcessBeanFactory(beanFactory);\n    \n            // 6. 接下来是这个方法是调用 BeanFactoryPostProcessor 各个实现类的 postProcessBeanFactory(factory) 方法；\n            invokeBeanFactoryPostProcessors(beanFactory);\n    \n            // 7. 然后这个方法注册 BeanPostProcessor 的实现类，和上面的BeanFactoryPostProcessor 是有区别的，这个方法调用的其实是PostProcessorRegistrationDelegate类的registerBeanPostProcessors方法；\n            // 这个类里面有个内部类BeanPostProcessorChecker，BeanPostProcessorChecker里面有两个方法postProcessBeforeInitialization和postProcessAfterInitialization，这两个方法分别在 Bean 初始化之前和初始化之后得到执行。\n            // 然后回到refresh()方法中继续往下看\n            registerBeanPostProcessors(beanFactory);\n    \n            // 8. 方法是初始化当前 ApplicationContext 的 MessageSource，国际化处理，继续往下\n            initMessageSource();\n    \n            // 9. 方法初始化当前 ApplicationContext 的事件广播器继续往下\n            initApplicationEventMulticaster();\n    \n            // 10. 方法初始化一些特殊的 Bean（在初始化 singleton beans 之前）；继续往下\n            onRefresh();\n    \n            // 11. 方法注册事件监听器，监听器需要实现 ApplicationListener 接口；继续往下\n            registerListeners();\n    \n            // 12. 重点到了 初始化所有的 singleton beans（单例bean），懒加载（non-lazy-init）的除外，这个方法也是等会细说\n            finishBeanFactoryInitialization(beanFactory);\n    \n            // 13. 方法是最后一步，广播事件，ApplicationContext 初始化完成\n            finishRefresh();\n        } catch(BeansException ex){\n            if(logger.isWarnEnabled()){\n                logger.warn(\"Exception encountered during context initialization - \"+\"cancelling refresh attempt: \"+ex);\n            }\n\n            // Destroy already created singletons to avoid dangling resources.\n            // 销毁已经初始化的 singleton 的 Beans，以免有些 bean 会一直占用资源\n            destroyBeans();\n    \n            // Reset 'active' flag.\n            cancelRefresh(ex);\n    \n            // Propagate exception to caller.\n            throw ex;\n        } finally{\n            // Reset common introspection caches in Spring's core, since we\n            // might not ever need metadata for singleton beans anymore...\n            resetCommonCaches();\n        }\n    }\n}\n```\n\n### Spring bean的生命周期\n\nSpring Bean的生命周期分为四个阶段和多个扩展点。扩展点又可以分为影响多个Bean和影响单个Bean。整理如下：\n\n四个阶段\n\n- 实例化 Instantiation\n  - Spring 容器根据 bean 的定义创建一个 bean 实例。这可以通过调用构造函数或使用工厂方法实现。\n- 属性赋值 Populate\n    - Spring 容器将配置的属性值注入到 bean 中，这个过程可以通过依赖注入来完成，包括简单类型属性和引用其他 bean。\n- 初始化 Initialization\n    - 在 bean 属性设置完成后，Spring 调用初始化方法，确保 bean 在使用之前处于有效状态，且具备运行所需的所有配置和资源。可以通过以下方式实现：\n        - @PostConstruct 注解的方法。\n        - 实现 InitializingBean 接口，重写 afterPropertiesSet() 方法。\n        - 在配置文件中指定初始化方法。\n- 销毁 Destruction\n    - 当 bean 的生命周期结束时，Spring 容器负责调用销毁方法。可以通过以下方式实现：\n        - @PreDestroy 注解的方法。\n        - 实现 DisposableBean 接口，重写 destroy() 方法。\n        - 在配置文件中指定销毁方法。\n\n多个扩展点\n\n- 影响多个Bean\n    - BeanPostProcessor(作用于初始化阶段的前后)\n    - InstantiationAwareBeanPostProcessor(作用于实例化阶段的前后)\n- 影响单个Bean\n    - Aware(Aware类型的接口的作用就是让我们能够拿到Spring容器中的一些资源)\n        - Aware Group1\n            - BeanNameAware\n            - BeanClassLoaderAware\n            - BeanFactoryAware\n        - Aware Group2\n            - EnvironmentAware\n            - EmbeddedValueResolverAware(实现该接口能够获取Spring EL解析器，用户的自定义注解需要支持spel表达式的时候可以使用)\n            - ApplicationContextAware(ResourceLoaderAware\\ApplicationEventPublisherAware\\MessageSourceAware)\n    - 生命周期(实例化和属性赋值都是Spring帮助我们做的，能够自己实现的有初始化和销毁两个生命周期阶段)\n        - InitializingBean\n        - DisposableBean\n\n### bean的作用域\n\nSpring Bean 中所说的作用域，在配置文件中即是“scope”\n\n在面向对象程序设计中作用域一般指对象或变量之间的可见范围。\n\n而在Spring容器中是指其创建的Bean对象相对于其他Bean对象的请求可见范围。\n\n在Spring 容器当中，一共提供了5种作用域类型，在配置文件中，通过属性scope来设置bean的作用域范围\n\n#### singleton\n\n```xml\n\n<bean id=\"userInfo\" class=\"cn.lovepi.UserInfo\" scope=\"singleton\"></bean>\n```\n\n当Bean的作用域为singleton的时候,Spring容器中只会存在一个共享的Bean实例，所有对Bean的请求只要id与bean的定义相匹配，则只会返回bean的同一实例。单一实例会被存储在单例缓存中，为Spring的缺省作用域。\n\n#### prototype\n\n```xml\n\n<bean id=\"userInfo\" class=\"cn.lovepi.UserInfo\" scope=\" prototype \"></bean>\n```\n\n每次对该Bean请求的时候，Spring IoC都会创建一个新的作用域。\n\n对于有状态的Bean应该使用prototype，对于无状态的Bean则使用singleton\n\n#### request\n\n```xml\n\n<bean id=\"userInfo\" class=\"cn.lovepi.UserInfo\" scope=\" request \"></bean>\n```\n\nRequest作用域针对的是每次的Http请求，Spring容器会根据相关的Bean的\n\n定义来创建一个全新的Bean实例。而且该Bean只在当前request内是有效的。\n\n#### session\n\n```xml\n\n<bean id=\"userInfo\" class=\"cn.lovepi.UserInfo\" scope=\" session \"></bean>\n```\n\n针对http session起作用，Spring容器会根据该Bean的定义来创建一个全新的Bean的实例。而且该Bean只在当前http session内是有效的。\n\n#### global session\n\n```xml\n\n<bean id=\"userInfo\" class=\"cn.lovepi.UserInfo\" scope=\"globalSession\"></bean>\n```\n\n类似标准的http session作用域，不过仅仅在基于portlet的web应用当中才有意义。Portlet规范定义了全局的Session的概念。他被所有构成某个portlet外部应用中的各种不同的portlet所共享。在global\nsession作用域中所定义的bean被限定于全局的portlet session的生命周期范围之内。\n\n### 循环依赖问题\n\n#### [三级缓存](#三级缓存)\n\nSpring 解决循环依赖的核心就是提前暴露对象，而提前暴露的对象就是放置于第二级缓存中。下表是三级缓存的说明：\n\n|名称|    描述|\n| --- | --- |\n|singletonObjects |    一级缓存，存放完整的 Bean。|\n|earlySingletonObjects    |二级缓存，存放提前暴露的Bean，Bean 是不完整的，未完成属性注入和执行 init 方法。|\n|singletonFactories    |三级缓存，存放的是 Bean 工厂，主要是生产 Bean，存放到二级缓存中。|\n\n所有被Spring 管理的 Bean，最终都会存放在 singletonObjects 中，这里面存放的 Bean 是经历了所有生命周期的（除了销毁的生命周期），完整的，可以给用户使用的。\n\nearlySingletonObjects 存放的是已经被实例化，但是还没有注入属性和执行 init 方法的 Bean。\n\nsingletonFactories 存放的是生产 Bean 的工厂。\n\nBean 都已经实例化了，为什么还需要一个生产 Bean 的工厂呢？这里实际上是跟 AOP 有关，如果项目中不需要为 Bean 进行代理，那么这个 Bean 工厂就会直接返回一开始实例化的对象，如果需要使用 AOP 进行代理，那么这个工厂就会发挥重要的作用了，这也是本文需要重点关注的问题之一。\n\n#### [三级缓存的作用](#三级缓存的作用)\n\n- Spring 会先从一级缓存 singletonObjects 中尝试获取 Bean。\n- 若是获取不到，而且对象正在建立中，就会尝试从二级缓存 earlySingletonObjects 中获取 Bean。\n- 若还是获取不到，且允许从三级缓存 singletonFactories 中经过 singletonFactory 的 getObject() 方法获取 Bean 对象，就会尝试从三级缓存 singletonFactories 中获取 Bean。\n- 若是在三级缓存中获取到了 Bean，会将该 Bean 存放到二级缓存中。\n\n\n#### [解决循环依赖](#解决循环依赖)\nSpring 是如何通过上面介绍的三级缓存来解决循环依赖的呢？这里只用 A，B 形成的循环依赖来举例：\n\n1. 实例化 A，此时 A 还未完成属性填充和初始化方法（@PostConstruct）的执行，A 只是一个半成品。\n2. 为 A 创建一个 Bean 工厂，并放入到  singletonFactories 中。\n3. 发现 A 需要注入 B 对象，但是一级、二级、三级缓存均为发现对象 B。\n4. 实例化 B，此时 B 还未完成属性填充和初始化方法（@PostConstruct）的执行，B 只是一个半成品。\n5. 为 B 创建一个 Bean 工厂，并放入到  singletonFactories 中。\n6. 发现 B 需要注入 A 对象，此时在一级、二级未发现对象 A，但是在三级缓存中发现了对象 A，从三级缓存中得到对象 A，并将对象 A 放入二级缓存中，同时删除三级缓存中的对象 A。（注意，此时的 A 还是一个半成品，并没有完成属性填充和执行初始化方法）\n7. 将对象 A 注入到对象 B 中。\n8. 对象 B 完成属性填充，执行初始化方法，并放入到一级缓存中，同时删除二级缓存中的对象 B。（此时对象 B 已经是一个成品）\n9. 对象 A 得到对象 B，将对象 B 注入到对象 A 中。（对象 A 得到的是一个完整的对象 B）\n10. 对象 A 完成属性填充，执行初始化方法，并放入到一级缓存中，同时删除二级缓存中的对象 A。\n\n#### [为什么需要三级缓存，二级缓存能解决么](#为什么需要三级缓存，二级缓存能解决么)\n\n[1、尝试使用两级缓存解决依赖冲突](#为什么需要三级缓存，二级缓存能解决么)\n\n第三级缓存的目的是为了延迟代理对象的创建，因为如果没有依赖循环的话，那么就不需要为其提前创建代理，可以将它延迟到初始化完成之后再创建。\n\n既然目的只是延迟的话，那么我们是不是可以不延迟创建，而是在实例化完成之后，就为其创建代理对象，这样我们就不需要第三级缓存了。因此，我们可以将 addSingletonFactory() 方法进行改造。\n\n```java\nprotected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {\n    Assert.notNull(singletonFactory, \"Singleton factory must not be null\");\n\n    synchronized (this.singletonObjects) {\n        // 判断一级缓存中不存在此对象\n        if (!this.singletonObjects.containsKey(beanName)) { \n            // 直接从工厂中获取 Bean\n            Object o = singletonFactory.getObject();\n\n            // 添加至二级缓存中\n            this.earlySingletonObjects.put(beanName, o);\n            this.registeredSingletons.add(beanName);\n        }\n    }\n}\n```\n这样的话，每次实例化完 Bean 之后就直接去创建代理对象，并添加到二级缓存中。\n\n测试结果是完全正常的，Spring 的初始化时间应该也是不会有太大的影响，因为如果 Bean 本身不需要代理的话，是直接返回原始 Bean 的，并不需要走复杂的创建代理 Bean 的流程。\n\n[2、三级缓存的意义](#为什么需要三级缓存，二级缓存能解决么)\n\n测试证明，二级缓存也是可以解决循环依赖的。为什么 Spring 不选择二级缓存，而要额外多添加一层缓存，使用三级缓存呢？\n\n如果 Spring 选择二级缓存来解决循环依赖的话，那么就意味着所有 Bean 都需要在实例化完成之后就立马为其创建代理，而 Spring 的设计原则是在 Bean 初始化完成之后才为其创建代理。\n\n\n> 使用三级缓存而非二级缓存并不是因为只有三级缓存才能解决循环引用问题，其实二级缓存同样也能很好解决循环引用问题。使用三级而非二级缓存并非出于 IOC 的考虑，而是出于 AOP 的考虑，即若使用二级缓存，在 AOP 情形注入到其他 Bean的，不是最终的代理对象，而是原始对象。\n\n## Spring框架中的单例bean是否线程安全\n\nSpring框架中的单例bean是线程安全的吗？它是如何处理线程并发问题的?\n\n不是，Spring框架中的单例bean不是线程安全的。\n\nspring 中的 bean 默认是单例模式，spring 框架并没有对单例 bean 进行多线程的封装处理。实际上大部分 spring bean 是无状态的（比如 dao 类），在某种程度上来说 bean 也是安全的，但如果 bean\n有状态的话（比如 view model ）就要开发者自己去保证线程安全了，最简单的就是改变 bean 的作用域，把“singleton”变更为“prototype”，这样请求 bean 相当于 new Bean()了， 保证线程安全了。\n\n- 有状态就是有数据存储功能。\n- 无状态就是不会保存数据。\n\nSpring如何处理线程并发问题?\n\n一般只有无状态的Bean才可以在多线程下共享，大部分是无状态的Bean。当存有状态的Bean的时候，spring一般是使用ThreadLocal进行处理，解决线程安全问题。\n\nThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。 同步机制采用了“时间换空间”的方式，仅提供一份变量，不同的线程获取锁，没获得锁的线程则需要排队。而ThreadLocal采用了“空间换时间”的方式。\nThreadLocal会为每一个线程提供一个独立的变量副本，从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本，所以没有相同变量的访问冲突问题。所以在编写多线程代码时，可以把不安全的变量封装进ThreadLocal。\n\n## AOP\n\nOOP(Object-Oriented Programming)面向对象编程，允许开发者定义纵向的关系，但并适用于定义横向的关系，导致了大量代码的重复，而不利于各个模块的重用。\n\nAOP(Aspect-Oriented Programming)\n，一般称为面向切面编程，作为面向对象的一种补充，用于将那些与业务无关，但却对多个对象产生影响的公共行为和逻辑，抽取并封装为一个可重用的模块，这个模块被命名为“切面”（Aspect），减少系统中的重复代码，降低了模块间的耦合度，同时提高了系统的可维护性。可用于权限认证、日志、事务处理等。\n\n### AOP原理\n\n原理是在IOC过程中，创建bean实例时，最后都会对bean进行处理来实现增强，对于AOP来说就是创建代理类\n\n- 底层是动态代理技术\n    - JDK动态代理(基于接口)\n    - CGLib动态代理(基于类)\n    - 在Spring AOP中，如果使用的是单例，推荐使用CGLib代理\n\n### JDK动态代理\n\n(一）实现原理\n\nJDK的动态代理是基于反射实现。JDK通过反射，生成一个代理类，这个代理类实现了原来那个类的全部接口，并对接口中定义的所有方法进行了代理。当我们通过代理对象执行原来那个类的方法时，代理类底层会通过反射机制，回调我们实现的InvocationHandler接口的invoke方法。并且这个代理类是Proxy类的子类（记住这个结论，后面测试要用）。这就是JDK动态代理大致的实现方式。\n\n（二）优点\n\nJDK动态代理是JDK原生的，不需要任何依赖即可使用；\n\n通过反射机制生成代理类的速度要比CGLib操作字节码生成代理类的速度更快；\n\n（三）缺点\n\n如果要使用JDK动态代理，被代理的类必须实现了接口，否则无法代理；\n\nJDK动态代理无法为没有在接口中定义的方法实现代理，假设我们有一个实现了接口的类，我们为它的一个不属于接口中的方法配置了切面，Spring仍然会使用JDK的动态代理，但是由于配置了切面的方法不属于接口，为这个方法配置的切面将不会被织入。\n\nJDK动态代理执行代理方法时，需要通过反射机制进行回调，此时方法执行的效率比较低；\n\n### CGLib动态代理\n\n（一）实现原理\n\nCGLib实现动态代理的原理是，底层采用了ASM字节码生成框架，直接对需要代理的类的字节码进行操作，生成这个类的一个子类，并重写了类的所有可以重写的方法，在重写的过程中，将我们定义的额外的逻辑（简单理解为Spring中的切面）织入到方法中，对方法进行了增强。而通过字节码操作生成的代理类，和我们自己编写并编译后的类没有太大区别。\n\n（二）优点\n\n使用CGLib代理的类，不需要实现接口，因为CGLib生成的代理类是直接继承自需要被代理的类；\n\nCGLib生成的代理类是原来那个类的子类，这就意味着这个代理类可以为原来那个类中，所有能够被子类重写的方法进行代理；\n\nCGLib生成的代理类，和我们自己编写并编译的类没有太大区别，对方法的调用和直接调用普通类的方式一致，所以CGLib执行代理方法的效率要高于JDK的动态代理；\n\n（三）缺点\n\n由于CGLib的代理类使用的是继承，这也就意味着如果需要被代理的类是一个final类，则无法使用CGLib代理；\n\n由于CGLib实现代理方法的方式是重写父类的方法，所以无法对final方法，或者private方法进行代理，因为子类无法重写这些方法；\n\nCGLib生成代理类的方式是通过操作字节码，这种方式生成代理类的速度要比JDK通过反射生成代理类的速度更慢；\n\n### AOP术语\n\n#### 连接点(Join point)\n\n能够被拦截的地方\n\n#### 切点(Poincut)\n\n具体定位的连接点\n\n#### 增强/通知(Advice)\n\n表示添加到切点的一段逻辑代码，并定位连接点的方位信息\n\n#### 织入(Weaving)\n\n将增强/通知添加到目标类的具体连接点上的过程。\n\n#### 引入/引介(Introduction)\n\n允许我们向现有的类添加新方法或属性。是一种特殊的增强！\n\n#### 切面(Aspect)\n\n切面由切点和增强/通知组成，它既包括了横切逻辑的定义、也包括了连接点的定义\n\n### Spring对AOP的支持\n\n- 基于代理的经典SpringAOP：需要实现接口，手动创建代理\n- 纯POJO切面：使用XML配置，aop命名空间\n- @AspectJ注解驱动的切面：使用注解的方式，这是最简洁和最方便的！\n\n## 怎么定义一个注解\n\n### 引入依赖\n\n```xml\n\n<dependency>\n    <groupId>org.springframework.boot</groupId>\n    <artifactId>spring-boot-starter-aop</artifactId>\n</dependency>\n```\n\n### 定义注解\n\n#### 元注解\n\njava.lang.annotation 提供了四种元注解，专门注解其他的注解（在自定义注解的时候，需要使用到元注解）：\n\n- @Documented – 注解是否将包含在JavaDoc中\n- @Retention – 什么时候使用该注解\n- @Target – 注解用于什么地方\n- @Inherited – 是否允许子类继承该注解\n\n##### @Retention\n定义该注解的生命周期\n\n- RetentionPolicy.SOURCE : 在编译阶段丢弃。这些注解在编译结束之后就不再有任何意义，所以它们不会写入字节码。@Override, @SuppressWarnings都属于这类注解。\n- RetentionPolicy.CLASS : 在类加载的时候丢弃。在字节码文件的处理中有用。注解默认使用这种方式\n- RetentionPolicy.RUNTIME : 始终不会丢弃，运行期也保留该注解，因此可以使用反射机制读取该注解的信息。我们自定义的注解通常使用这种方式。\n\n##### @Target\n表示该注解用于什么地方。默认值为任何元素，表示该注解用于什么地方。可用的ElementType 参数包括\n\n- ElementType.CONSTRUCTOR: 用于描述构造器\n- ElementType.FIELD: 成员变量、对象、属性（包括enum实例）\n- ElementType.LOCAL_VARIABLE: 用于描述局部变量\n- ElementType.METHOD: 用于描述方法\n- ElementType.PACKAGE: 用于描述包\n- ElementType.PARAMETER: 用于描述参数\n- ElementType.TYPE: 用于描述类、接口(包括注解类型) 或enum声明\n\n##### @Documented\n一个简单的Annotations 标记注解，表示是否将注解信息添加在java文档中。\n\n##### @Inherited\n定义该注释和子类的关系\n\n@Inherited 元注解是一个标记注解，@Inherited 阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited 修饰的annotation 类型被用于一个class，则这个annotation 将被用于该class\n的子类。\n\n### 示例\n\n自定义一个检查是否登录的注解\n\n```java\n\n@Target({ElementType.TYPE, ElementType.METHOD})\n@Retention(RetentionPolicy.RUNTIME)\n@Documented\npublic @interface CheckLogin {\n\n}\n```\n\n实现\n\n```java\n\n@Aspect\n@Component\n@Slf4j\n@Order(1)\npublic class CheckLoginAspect {\n\n    @Autowired\n    RedisTemplate redisTemplate;\n\n    @Before(\"execution(* *..controller..*(..))\")\n    public void before(JoinPoint joinPoint) {\n        MethodSignature signature = (MethodSignature) joinPoint.getSignature();\n        Method method = signature.getMethod();\n        CheckLogin annotation = method.getAnnotation(CheckLogin.class);\n\n        if (annotation == null) {\n            //获取类上注解\n            annotation = joinPoint.getTarget().getClass().getAnnotation(CheckLogin.class);\n        }\n        if (annotation != null) {\n            //获取到请求的属性\n            ServletRequestAttributes attributes =\n                    (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();\n            //获取到请求对象\n            HttpServletRequest request = attributes.getRequest();\n            String ssoToken = HttpUtil.getSsoToken(request);\n            if (ssoToken != null) {\n                String loginUserTokenKey = AuthRedisKeyUtil.getLoginUserTokenKey(ssoToken);\n                if (redisTemplate.hasKey(loginUserTokenKey)) {\n                    //通过\n                } else {\n                    throw new LoginException(\"登录已过期\");\n                }\n            } else {\n                throw new IllegalRequestException(\"非法请求\");\n            }\n        }\n    }\n}\n```\n\n使用\n\n```java\n\n@RestController\npublic class AccountController {\n\n    @CheckLogin\n    @GetMapping(\"/query\")\n    public JSONObject queryRegulation(Integer pageNum, Integer pageSize) {\n        //....业务逻辑\n    }\n}\n```\n\n### @Autowired原理\n@Autowired 是 Spring 框架中用于自动装配依赖的一种注解。它简化了依赖注入的过程，允许 Spring 通过类型或名称自动查找并注入所需的 Bean\n\n1. 组件扫描：\n\n- Spring 启动时，会根据配置（如 @ComponentScan）扫描指定的包，查找被 @Component、@Service、@Repository、@Controller 等注解标记的类，并将其注册为 Spring 的 Bean。\n\n2. 依赖解析：\n\n- 当 Spring 容器创建一个 Bean 时，如果该 Bean 的属性标注了 @Autowired，Spring 将对这些属性进行依赖解析。\n- Spring 会查找容器中匹配的 Bean，依据优先顺序：\n    - 根据类型匹配（默认方式）。\n    - 如果有多个同类型的 Bean，可以使用 @Qualifier 指定具体的 Bean 名称。\n\n3. 注入：\n\n一旦找到匹配的 Bean，Spring 会通过反射将其注入到标注了 @Autowired 的属性中。\n\n4. 生命周期管理：\n\nSpring 还会管理这些 Bean 的生命周期，包括初始化和销毁等操作。\n\n\n## 事务\n\n### Spring 支持两种方式的事务管理\n\n#### 1、编程式事务管理\n\n- TransactionTemplate\n\n```java\n@Autowired\nprivate TransactionTemplate transactionTemplate;\n\npublic void testTransaction(){\n    transactionTemplate.execute(new TransactionCallbackWithoutResult(){\n        @Override\n        protected void doInTransactionWithoutResult(TransactionStatus status){\n                try{\n                    //...业务代码\n                }catch(Exception e){\n                    status.setRollbackOnly();\n                }\n        }\n    });\n}\n```\n\n- TransactionManager\n\n```java\n    @Autowired\nprivate PlatformTransactionManager transactionManager;\n\npublic void testTransaction2(){\n        TransactionStatus status=transactionManager.getTransaction(new DefaultTransactionDefinition());\n        try{\n            //...业务代码\n            transactionManager.commit(status);\n        }catch(Exception e){\n            transactionManager.rollback(status);\n        }\n}\n```\n\n#### 2、注解\n\n- @Transactional\n\n### 事务的传播性 Propagation\n\n① PROPAGATION_REQUIRED：如果当前没有事务，就创建一个新事务，如果当前存在事务，就加入该事务，该设置是最常用的设置。\n\n② PROPAGATION_SUPPORTS：支持当前事务，如果当前存在事务，就加入该事务，如果当前不存在事务，就以非事务执行。\n\n③ PROPAGATION_MANDATORY：支持当前事务，如果当前存在事务，就加入该事务，如果当前不存在事务，就抛出异常。\n\n④ PROPAGATION_REQUIRES_NEW：创建新事务，无论当前存不存在事务，都创建新事务。\n\n⑤ PROPAGATION_NOT_SUPPORTED：以非事务方式执行操作，如果当前存在事务，就把当前事务挂起。\n\n⑥ PROPAGATION_NEVER：以非事务方式执行，如果当前存在事务，则抛出异常。\n\n⑦ PROPAGATION_NESTED：如果当前存在事务，则在嵌套事务内执行。如果当前没有事务，则按REQUIRED属性执行。\n\n### spring事务失效的场景\n\n1. 非被Spring管理的Bean上的事务： 如果你在一个非被Spring容器管理的Bean（例如通过new关键字直接创建的对象）上使用事务注解，事务将不会生效。Spring的事务管理是基于AOP（面向切面编程）实现的，因此只能在由Spring容器管理的Bean上起作用。\n\n2. 未捕获的异常： 如果在事务内发生未捕获的运行时异常，事务将回滚。但是，如果异常被捕获并在方法内处理，事务可能不会回滚。确保在事务边界内处理异常或者允许异常传播到事务管理器以便正确回滚。\n\n    ```java\n    @Transactional\n    public void transactionalMethod() {\n        try {\n            // some code that may throw an exception\n        } catch (Exception e) {\n            // handle the exception (not recommended within a transaction)\n        }\n    }\n    ```\n\n3. 嵌套事务问题： Spring事务支持嵌套事务，但是嵌套事务的行为取决于底层事务管理器的支持。如果使用的事务管理器不支持嵌套事务，嵌套事务可能会被忽略，导致事务行为不一致。\n\n4. 方法调用问题： Spring事务是通过AOP实现的，它依赖于代理对象来拦截方法调用并处理事务。如果你在同一个类内部调用一个带有事务注解的方法，事务可能不会起作用，因为代理对象无法拦截内部方法的调用。确保事务注解生效，要么调用方法是通过代理对象，要么通过self-invocation，例如通过this关键字。\n    ```java\n    @Transactional\n    public class MyService {\n        public void outerMethod() {\n            innerMethod(); // Transactional annotation may not work here\n            this.innerMethod(); // Transactional annotation should work here\n        }\n    \n        @Transactional\n        public void innerMethod() {\n            // some transactional logic\n        }\n    }\n    \n    ```\n5. 异步方法问题： 如果使用了异步方法（通过@Async注解），事务可能会失效。在异步方法内，事务上下文可能无法正确传播，导致事务不起作用。要在异步方法中使用事务，可以使用TransactionContext传播方式。\n\n\n## spring使用的设计模式\n\n### 简单工厂\n\n**实现方式：**\n\nBeanFactory。Spring中的BeanFactory就是简单工厂模式的体现，根据传入一个唯一的标识来获得Bean对象，但是否是在传入参数后创建还是传入参数前创建这个要根据具体情况来定。\n\n**实现原理：**\n\nbean容器的启动阶段：\n\n读取bean的xml配置文件,将bean元素分别转换成一个BeanDefinition对象。\n然后通过BeanDefinitionRegistry将这些bean注册到beanFactory中，保存在它的一个ConcurrentHashMap中。\n将BeanDefinition注册到了beanFactory之后，在这里Spring为我们提供了一个扩展的切口，允许我们通过实现接口BeanFactoryPostProcessor\n在此处来插入我们定义的代码。典型的例子就是：PropertyPlaceholderConfigurer，我们一般在配置数据库的dataSource时使用到的占位符的值，就是它注入进去的。\n\n容器中bean的实例化阶段：\n\n实例化阶段主要是通过反射或者CGLIB对bean进行实例化，在这个阶段Spring又给我们暴露了很多的扩展点：\n\n各种的Aware接口 ，比如 BeanFactoryAware，对于实现了这些Aware接口的bean，在实例化bean时Spring会帮我们注入对应的BeanFactory的实例。 BeanPostProcessor接口\n，实现了BeanPostProcessor接口的bean，在实例化bean时Spring会帮我们调用接口中的方法。 InitializingBean接口\n，实现了InitializingBean接口的bean，在实例化bean时Spring会帮我们调用接口中的方法。 DisposableBean接口\n，实现了DisposableBean接口的bean，在该bean死亡时Spring会帮我们调用接口中的方法。\n\n**设计意义：**\n\n松耦合。\n可以将原来硬编码的依赖，通过Spring这个beanFactory这个工厂来注入依赖，也就是说原来只有依赖方和被依赖方，现在我们引入了第三方——spring这个beanFactory，由它来解决bean之间的依赖问题，达到了松耦合的效果.\n\nbean的额外处理。 通过Spring接口的暴露，在实例化bean的阶段我们可以进行一些额外的处理，这些额外的处理只需要让bean实现对应的接口即可，那么spring就会在bean的生命周期调用我们实现的接口来处理该bean。[非常重要]\n\n### 工厂方法\n\n**实现方式：**\n\nFactoryBean接口。\n\n**实现原理：**\n\n实现了FactoryBean接口的bean是一类叫做factory的bean。其特点是，spring会在使用getBean()调用获得该bean时，会自动调用该bean的getObject()\n方法，所以返回的不是factory这个bean，而是这个bean.getOjbect()方法的返回值。\n\n### 单例模式\n\nSpring依赖注入Bean实例默认是单例的。\n\nSpring的依赖注入（包括lazy-init方式）都是发生在AbstractBeanFactory的getBean里。getBean的doGetBean方法调用getSingleton进行bean的创建。\n\n### 适配器模式\n\n**实现方式：**\n\nSpringMVC中的适配器HandlerAdatper。\n\n**实现原理：**\n\nHandlerAdatper根据Handler规则执行不同的Handler。\n\n**实现过程：**\n\nDispatcherServlet根据HandlerMapping返回的handler，向HandlerAdatper发起请求，处理Handler。\n\nHandlerAdapter根据规则找到对应的Handler并让其执行，执行完毕后Handler会向HandlerAdapter返回一个ModelAndView，最后由HandlerAdapter向DispatchServelet返回一个ModelAndView。\n\n### 装饰器模式\n\n**实现方式：**\n\nSpring中用到的包装器模式在类名上有两种表现：一种是类名中含有Wrapper，另一种是类名中含有Decorator。\n\n**实质：**\n\n动态地给一个对象添加一些额外的职责。\n\n就增加功能来说，Decorator模式相比生成子类更为灵活。\n\n### 代理模式\n\n**实现方式：**\n\nAOP底层，就是动态代理模式的实现。\n\n**动态代理：**\n\n在内存中构建的，不需要手动编写代理类\n\n## spring中properties和yml的加载顺序\n\n相同内容properties和yml的加载顺序是properties优先\n\n## 使用@Autowired注解自动装配的过程是怎样的？\n\n使用@Autowired注解来自动装配指定的bean。在使用@Autowired注解之前需要在Spring配置文件进行配置，<context:annotation-config />。\n\n在启动spring\nIoC时，容器自动装载了一个AutowiredAnnotationBeanPostProcessor后置处理器，当容器扫描到@Autowied、@Resource或@Inject时，就会在IoC容器自动查找需要的bean，并装配给该对象的属性。在使用@Autowired时，首先在容器中查询对应类型的bean：\n\n- 如果查询结果刚好为一个，就将该bean装配给@Autowired指定的数据；\n- 如果查询的结果不止一个，那么@Autowired会根据名称来查找；\n- 如果上述查找的结果为空，那么会抛出异常。解决方法是，使用required=false。\n\n## @Autowired和@Resource之间的区别\n\n@Autowired可用于：构造函数、成员变量、Setter方法\n\n@Autowired和@Resource之间的区别\n\n- @Autowired默认是按照类型装配注入的，默认情况下它要求依赖对象必须存在（可以设置它required属性为false）。\n- @Resource默认是按照名称来装配注入的，只有当找不到与名称匹配的bean才会按照类型来装配注入。\n\n## Spring中BeanFactory与FactoryBean的区别\n\n### BeanFactory\n\nBeanFactory是一个接口，它是Spring中工厂的顶层规范，是SpringIoc容器的核心接口，它定义了getBean()、containsBean()等管理Bean的通用方法。Spring的容器都是它的具体实现如：\n\n- DefaultListableBeanFactory\n- XmlBeanFactory\n- ApplicationContext\n\n这些实现类又从不同的维度分别有不同的扩展。\n\n### FactoryBean\n\n首先它是一个Bean，但又不仅仅是一个Bean。它是一个能生产或修饰对象生成的工厂Bean，类似于设计模式中的工厂模式和装饰器模式。它能在需要的时候生产一个对象，且不仅仅限于它自身，它能返回任何Bean的实例。\n\nFactoryBean表现的是一个工厂的职责。 即一个Bean A如果实现了FactoryBean接口，那么A就变成了一个工厂，根据A的名称获取到的实际上是工厂调用getObject()\n返回的对象，而不是A本身，如果要获取工厂A自身的实例，那么需要在名称前面加上'&'符号。\n\n- getObject('name')返回工厂中的实例\n- getObject('&name')返回工厂本身的实例\n\n# 参考文章\n\n- https://www.jianshu.com/p/5e7c0713731f\n- https://blog.csdn.net/nuomizhende45/article/details/81158383\n- https://www.cnblogs.com/javazhiyin/p/10905294.html\n- https://www.jianshu.com/p/1dec08d290c1\n- https://blog.csdn.net/icarus_wang/article/details/51586776\n- https://cloud.tencent.com/developer/article/1512235\n- https://zhuanlan.zhihu.com/p/114244039\n- https://blog.csdn.net/qq_41701956/article/details/116354268\n- https://blog.csdn.net/weixin_41980692/article/details/105803311\n- https://juejin.cn/post/6844903967600836621\n- https://juejin.cn/post/6882266649509298189"
  },
  {
    "path": "Spring/SpringBoot.md",
    "content": "# springboot\n## springboot启动流程\n### 启动类上注解：@SpringBootApplication\n#### @SpringBootConfiguration\n根据Javadoc可知，该注解作用就是将当前的类作为一个JavaConfig，然后触发注解@EnableAutoConfiguration和@ComponentScan的处理，本质上与@Configuration注解没有区别\n#### @EnableAutoConfiguration\n@EnableAutoConfiguration:实现自动装配的核心注解\n- @AutoConfigurationPackage\n  - 注册当前启动类的根 package\n  - 注册 org.springframework.boot.autoconfigure.AutoConfigurationPackages 的 BeanDefinition\n- @Import(AutoConfigurationImportSelector.class)\n  - 自动装配核心功能的实现实际是通过 AutoConfigurationImportSelector(加载自动装配类)类\n  - AutoConfigurationImportSelector 类实现了 ImportSelector接口\n    - 实现了这个接口中的 selectImports方法\n      - 方法实现 重要的getAutoConfigurationEntry()方法\n        1. 判断自动装配是否打开，默认是true可以通过application.yml设置\n        2. 获取@EnableAutoConfiguration里的exclude和excludeName内容以便排除\n        3. 获取需要自动装配的所有配置类，读取META-INF/spring.factories druid 数据库连接池的 Spring Boot Starter 就创建了META-INF/spring.factories文件\n        4. 筛选满足@ConditionalOnXXX注解的类，生效才会被加载\n      - 该方法主要用于获取所有符合条件的类的全限定类名，这些类需要被加载到 IoC 容器中\n#### @ComponentScan\n扫描的 Spring 对应的组件，如 @Componet，@Repository\n- 我们可以通过 basePackages 等属性来细粒度的定制 @ComponentScan 自动扫描的范围，如果不指定，则默认Spring框架实现会从声明 @ComponentScan 所在类的package进行扫描，所以 SpringBoot 的启动类最好是放在根package下，我们自定义的类就放在对应的子package下，这样就可以不指定 basePackages\n### 启动类中的main方法：org.springframework.boot.SpringApplication#run(java.lang.Class<?>, java.lang.String...)\n- 从spring.factories配置文件中加载EventPublishingRunListener对象，该对象拥有SimpleApplicationEventMulticaster属性，即在SpringBoot启动过程的不同阶段用来发射内置的生命周期事件;\n  - spring-bean包下META-INF/spring.factories\n- 准备环境变量，包括系统变量，环境变量，命令行参数，默认变量，servlet相关配置变量，随机值以及配置文件（比如application.properties）等;\n  - 而后就会去创建Environment——这个时候会去加载application配置文件\n- 控制台打印SpringBoot的bannner标志；\n- 根据不同类型环境创建不同类型的applicationcontext容器，因为这里是servlet环境，所以创建的是AnnotationConfigServletWebServerApplicationContext容器对象；\n- 从spring.factories配置文件中加载FailureAnalyzers对象,用来报告SpringBoot启动过程中的异常；\n- 为刚创建的容器对象做一些初始化工作，准备一些容器属性值等，对ApplicationContext应用一些相关的后置处理和调用各个ApplicationContextInitializer的初始化方法来执行一些初始化逻辑等；\n- 刷新容器，这一步至关重要。比如调用bean factory的后置处理器，注册BeanPostProcessor后置处理器，初始化事件广播器且广播事件，初始化剩下的单例bean和SpringBoot创建内嵌的Tomcat服务器等等重要且复杂的逻辑都在这里实现，主要步骤可见代码的注释，关于这里的逻辑会在以后的spring源码分析专题详细分析；\n  - // 1）在context刷新前做一些准备工作，比如初始化一些属性设置，属性合法性校验和保存容器中的一些早期事件等；\n  - // 2）让子类刷新其内部bean factory,注意SpringBoot和Spring启动的情况执行逻辑不一样\n  - // 3）对bean factory进行配置，比如配置bean factory的类加载器，后置处理器等\n  - // 4）完成bean factory的准备工作后，此时执行一些后置处理逻辑，子类通过重写这个方法来在BeanFactory创建并预准备完成以后做进一步的设置\n    - // 在这一步，所有的bean definitions将会被加载，但此时bean还不会被实例化\n  - // 5）执行BeanFactoryPostProcessor的方法即调用bean factory的后置处理器：\n    - // BeanDefinitionRegistryPostProcessor（触发时机：bean定义注册之前）和BeanFactoryPostProcessor（触发时机：bean定义注册之后bean实例化之前）\n  - // 6）注册bean的后置处理器BeanPostProcessor，注意不同接口类型的BeanPostProcessor；在Bean创建前后的执行时机是不一样的\n  - // 7）初始化国际化MessageSource相关的组件，比如消息绑定，消息解析等\n  - // 8）初始化事件广播器，如果bean factory没有包含事件广播器，那么new一个SimpleApplicationEventMulticaster广播器对象并注册到bean factory中\n  - // 9）AbstractApplicationContext定义了一个模板方法onRefresh，留给子类覆写，比如ServletWebServerApplicationContext覆写了该方法来创建内嵌的tomcat容器\n  - // 10）注册实现了ApplicationListener接口的监听器，之前已经有了事件广播器，此时就可以派发一些early application events\n  - // 11）完成容器bean factory的初始化，并初始化所有剩余的单例bean。这一步非常重要，一些bean postprocessor会在这里调用。\n  - // 12）完成容器的刷新工作，并且调用生命周期处理器的onRefresh()方法，并且发布ContextRefreshedEvent事件\n- 执行刷新容器后的后置处理逻辑，注意这里为空方法；\n- 调用ApplicationRunner和CommandLineRunner的run方法，我们实现这两个接口可以在spring容器启动后需要的一些东西比如加载一些业务数据等;\n- 报告启动异常，即若启动过程中抛出异常，此时用FailureAnalyzers来报告异常;\n- 最终返回容器对象，这里调用方法没有声明对象来接收。\n```java\npublic static void main(String[] args) throws Exception {\n   SpringApplication.run(new Class<?>[0], args);\n}\npublic static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {\n   // 新建SpringApplication对象，再调用run方法\n   return new SpringApplication(primarySources).run(args);\n}\npublic ConfigurableApplicationContext run(String... args) {\n   // stopWatch用于统计run启动过程时长\n   StopWatch stopWatch = new StopWatch();\n   // 开始计时\n   stopWatch.start();\n   // 创建ConfigurableApplicationContext对象\n   ConfigurableApplicationContext context = null;\n   // exceptionReporters集合用来存储SpringApplication启动过程的异常，SpringBootExceptionReporter且通过spring.factories方式来加载\n   Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();\n   // 配置headless属性\n   configureHeadlessProperty();\n   /**\n    * 从spring.factories配置文件中加载到EventPublishingRunListener对象并赋值给SpringApplicationRunListeners\n    * # Run Listeners\n    * org.springframework.boot.SpringApplicationRunListener=\\\n    * org.springframework.boot.context.event.EventPublishingRunListener\n    */\n   SpringApplicationRunListeners listeners = getRunListeners(args);\n   // 启动SpringApplicationRunListeners监听\n   listeners.starting();\n   try {\n      // 创建ApplicationArguments对象，封装了args参数\n      ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);\n      // 备配置参数有app.properties，外部配置参数比如jvm启动参数等\n      ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);\n      // 配置spring.beaninfo.ignore属性\n      configureIgnoreBeanInfo(environment);\n      // 打印springboot的bannner\n      Banner printedBanner = printBanner(environment);\n      // 根据不同类型创建不同类型的spring applicationcontext容器\n      context = createApplicationContext();\n      /**\n       * 异常报告\n       * 从spring.factories配置文件中加载exceptionReporters，其中ConfigurableApplicationContext.class作为FailureAnalyzers构造方法的参数\n       * # Error Reporters\n       * org.springframework.boot.SpringBootExceptionReporter=\\\n       * org.springframework.boot.diagnostics.FailureAnalyzers\n       */\n      exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,\n            new Class[] { ConfigurableApplicationContext.class }, context);\n      // 准备容器事项：调用各个ApplicationContextInitializer的initialize方法\n      // 和触发SpringApplicationRunListeners的contextPrepared及contextLoaded方法等\n      prepareContext(context, environment, listeners, applicationArguments, printedBanner);\n      // 刷新容器，这一步至关重要\n      refreshContext(context);\n      // 执行刷新容器后的后置处理逻辑，注意这里为空方法\n      afterRefresh(context, applicationArguments);\n      // 停止stopWatch计时\n      stopWatch.stop();\n      // 打印springboot的启动时常\n      if (this.logStartupInfo) {\n         new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);\n      }\n      // 触发SpringApplicationRunListener的started方法，通知spring容器已经启动\n      listeners.started(context);\n      // 调用ApplicationRunner和CommandLineRunner的run方法，实现spring容器启动后需要做的一些东西\n      callRunners(context, applicationArguments);\n   }\n   // 若上面的方法抛出异常，将异常添加到exceptionReporters集合中，并抛出 IllegalStateException 异常。\n   catch (Throwable ex) {\n      handleRunFailure(context, ex, exceptionReporters, listeners);\n      throw new IllegalStateException(ex);\n   }\n\n   try {\n      // 当容器刷新完毕等，触发SpringApplicationRunListeners数组的running方法\n      listeners.running(context);\n   }\n   catch (Throwable ex) {\n      // 若上面的方法抛出异常，将异常添加到exceptionReporters集合中，并抛出 IllegalStateException 异常。\n      handleRunFailure(context, ex, exceptionReporters, null);\n      throw new IllegalStateException(ex);\n   }\n   return context;\n}\n\n```\n\n### 简化\n\nSpring Boot的启动流程大致如下：\n\n1. 引导类（Main Class）\nSpring Boot应用通常包含一个主类，使用@SpringBootApplication注解标记。该注解结合了@Configuration、@EnableAutoConfiguration和@ComponentScan注解。\n```java\n@SpringBootApplication\npublic class Application {\n    public static void main(String[] args) {\n        SpringApplication.run(Application.class, args);\n    }\n}\n```\n2. SpringApplication.run()调用SpringApplication.run()方法，起始启动过程：创建SpringApplication实例：并设置初始属性。\n\n3. 准备环境（prepareEnvironment）创建ConfigurableEnvironment对象，设置应用程序的属性和环境变量。\n\n4. 创建上下文（createApplicationContext）根据应用类型创建ApplicationContext（常用的如AnnotationConfigServletWebServerApplicationContext）。\n\n5. 注册Listeners（registerListeners）注册各种ApplicationListener，比如监听Spring应用的事件（如ApplicationEnvironmentPreparedEvent、ApplicationPreparedEvent等）。\n\n6. 准备上下文（prepareContext）设置上下文的属性、环境和资源等，进行初始化。\n\n7. 加载Sources（load）根据注解（如@SpringBootApplication）加载配置类，进行组件扫描并注册到Spring容器。\n\n8. 执行自动配置（auto-configuration）通过@EnableAutoConfiguration，Spring Boot会根据classpath中的库自动配置Bean。例如，检测到Tomcat会自动配置相应的Servlet。\n\n9. 刷新上下文（refreshContext）触发上下文的刷新，完成所有Bean的初始化和配置。\n\n10. 调用CommandLine runners和Application runners如果定义了CommandLineRunner或ApplicationRunner，会在上下文刷新后执行。\n\n11. 启动嵌入式容器如果应用是Web应用，Spring Boot会启动嵌入式Tomcat、Jetty或Undertow等。\n\n12. 应用就绪最后，Spring Boot应用进入就绪状态，等待请求或事件。\n\n**总结**\n\nSpring Boot的启动过程整合了多个步骤，通过自动配置和注解简化了Spring应用的开发过程。整个过程涉及环境处理、上下文创建、Bean注册和必要的监听器等，使得开发者可以专注于业务逻辑，而无需关注复杂的配置。\n\n## 怎么让Spring把Body变成一个对象\n- @RequestBody注解原理\n- 详细看springmvc的处理流程\n## SpringBoot的starter实现原理是什么？\n原理就是因为在@EnableAutoConfiguration注解，会自动的扫描jar包下的META-INF/spring.factories文件的配置类，写在这里面的类都是需要被自动加载的\n\n将configuration类中定义的bean加入spring到容器中。就相当于加载之前我们自己配置组件的xml文件。而现在SpringBoot自己定义了一个默认的值，然后直接加载进入了Spring容器。\n\nSpringBoot提供的自动配置依赖模块都以spring-boot-starter-为命名前缀，并且这些依赖都在org.springframework.boot下。 所有的spring-boot-starter都有约定俗成的默认配置，但允许调整这些配置调整默认的行为。\n## spring 和springboot的区别\nSpring Boot基本上是Spring框架的扩展，它消除了设置Spring应用程序所需的XML配置，为更快，更高效的开发生态系统铺平了道路。\n\nSpring Boot中的一些特征：\n\n- 创建独立的Spring应用。\n- 嵌入式Tomcat、Jetty、 Undertow容器（无需部署war文件）。\n- 提供的starters 简化构建配置\n- 尽可能自动配置spring应用。\n- 提供生产指标,例如指标、健壮检查和外部化配置\n- 完全没有代码生成和XML配置要求\n\nMaven依赖\n\n首先，让我们看一下使用Spring创建Web应用程序所需的最小依赖项\n```xml\n<dependency>\n    <groupId>org.springframework</groupId>\n    <artifactId>spring-web</artifactId>\n    <version>5.1.0.RELEASE</version>\n</dependency>\n<dependency>\n    <groupId>org.springframework</groupId>\n    <artifactId>spring-webmvc</artifactId>\n    <version>5.1.0.RELEASE</version>\n</dependency>\n```\n与Spring不同，Spring Boot只需要一个依赖项来启动和运行Web应用程序：\n```xml\n<dependency>\n    <groupId>org.springframework.boot</groupId>\n    <artifactId>spring-boot-starter-web</artifactId>\n    <version>2.0.6.RELEASE</version>\n</dependency>\n```\n在进行构建期间，所有其他依赖项将自动添加到项目中。\n\n另一个很好的例子就是测试库。我们通常使用Spring Test，JUnit，Hamcrest和Mockito库。在Spring项目中，我们应该将所有这些库添加为依赖项。但是在Spring Boot中，我们只需要添加spring-boot-starter-test依赖项来自动包含这些库。\n\nspring在运行前需要使用xml文件做很多配置，而springboot帮我们实现了这些配置的自动加载，基于注解和简单的yml配置即可\n\nspring的web程序还是打包为war然后再Tomcat里运行，而springboot内嵌了Tomcat直接打成可运行的jar\n\n## Spring Boot 可执行 Jar 包运行原理\nSpring Boot 有一个很方便的功能就是可以将应用打成可执行的 Jar。那么大家有没想过这个 Jar 是怎么运行起来的呢？本篇博客就来介绍下 Spring Boot 可执行 Jar 包的运行原理。\n\n### 打可执行 Jar 包\n将 Spring Boot 应用打成可执行 Jar包很容易，只需要在 pom 中加上一个 Spring Boot 提供的插件，然后在执行mvn package即可\n```xml\n<build>\n    <plugins>\n        <plugin>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-maven-plugin</artifactId>\n        </plugin>\n    </plugins>\n</build>\n```\n运行完mvn package后，我们会在 target 目录下看到两个 jar 文件。myproject-0.0.1-SNAPSHOT.jar 和 myproject-0.0.1-SNAPSHOT.jar.original。第一个 jar 文件就是我们应用的可执行 jar 包，第二个 Jar 文件是应用原始的 jar 包。\n\n### 可执行 Jar 包内部结构\n```text\n可执行 jar 目录结构\n├─BOOT-INF\n│  ├─classes\n│  └─lib\n├─META-INF\n│  ├─maven\n│  ├─app.properties\n│  ├─MANIFEST.MF      \n└─org\n    └─springframework\n        └─boot\n            └─loader\n                ├─archive\n                ├─data\n                ├─jar\n                └─util\n\n```\n我们先来重点关注两个地方：META-INF 下面的 Jar 包描述文件和 BOOT-INF 这个目录。\n\nMANIFEST.MF 文件\n```properties\nManifest-Version: 1.0\nArchiver-Version: Plexus Archiver\nBuilt-By: xxxx\nStart-Class: com.xxxx.AppServer\nSpring-Boot-Classes: BOOT-INF/classes/\nSpring-Boot-Lib: BOOT-INF/lib/\nSpring-Boot-Version: 2.1.6.RELEASE\nCreated-By: Apache Maven 3.3.9\nBuild-Jdk: 1.8.0_73\nMain-Class: org.springframework.boot.loader.JarLauncher\n\n```\n在上面我们看到一个熟悉的配置Main-Class: org.springframework.boot.loader.JarLauncher。我们大概能猜到这个类是整个系统的入口。\n\n再看下 BOOT-INF 这个目录下面，我们会发现里面是我们项目打出来的 class 文件和项目依赖的 Jar 包。看到这里，你可能已经猜到 Spring Boot 是怎么启动项目的了。\n\n### JarLauncher\n```java\npublic class JarLauncher extends ExecutableArchiveLauncher {\n\n\tstatic final String BOOT_INF_CLASSES = \"BOOT-INF/classes/\";\n\n\tstatic final String BOOT_INF_LIB = \"BOOT-INF/lib/\";\n\n\tpublic JarLauncher() {\n\t}\n\n\tprotected JarLauncher(Archive archive) {\n\t\tsuper(archive);\n\t}\n\n\t@Override\n\tprotected boolean isNestedArchive(Archive.Entry entry) {\n\t\tif (entry.isDirectory()) {\n\t\t\treturn entry.getName().equals(BOOT_INF_CLASSES);\n\t\t}\n\t\treturn entry.getName().startsWith(BOOT_INF_LIB);\n\t}\n\n\tpublic static void main(String[] args) throws Exception {\n        //项目入口，重点在launch这个方法中\n\t\tnew JarLauncher().launch(args);\n\t}\n\n}\n\n```\n```java\n//launch方法\nprotected void launch(String[] args) throws Exception {\n    JarFile.registerUrlProtocolHandler();\n    //创建LaunchedURLClassLoader。如果根类加载器和扩展类加载器没有加载到某个类的话，就会通过LaunchedURLClassLoader这个加载器来加载类。这个加载器会从Boot-INF下面的class目录和lib目录下加载类。\n    ClassLoader classLoader = createClassLoader(getClassPathArchives());\n    //这个方法会读取jar描述文件中的Start-Class属性，然后通过反射调用到这个类的main方法。\n    launch(args, getMainClass(), classLoader);\n}\n\n```\n简单总结\n- Spring Boot 可执行 Jar 包的入口点是 JarLauncher 的 main 方法；\n- 这个方法的执行逻辑是先创建一个 LaunchedURLClassLoader，这个加载器加载类的逻辑是：先判断根类加载器和扩展类加载器能否加载到某个类，如果都加载不到就从 Boot-INF 下面的 class 和 lib 目录下去加载；\n- 读取Start-Class属性，通过反射机制调用启动类的 main 方法，这样就顺利调用到我们开发的 Spring Boot 主启动类的 main 方法了。\n# 参考文章\n- https://www.jianshu.com/p/ffe5ebe17c3a\n- https://www.cnblogs.com/54chensongxia/p/11419796.html\n\n"
  },
  {
    "path": "Spring/SpringMVC.md",
    "content": "\n* [springMVC](#springmvc)\n   * [流程](#流程)\n   * [执行流程](#执行流程)\n\n\n# springMVC\n## 流程\n![](../img/spring/springmvc流程.png)\n## 执行流程\n1. **用户发送请求至前端控制器DispatcherServlet**\n   1. DispatcherServlet：前端控制器。用户请求到达前端控制器，它就相当于mvc模式中的c，dispatcherServlet是整个流程控制的中心，由它调用其它组件处理用户的请求，dispatcherServlet的存在降低了组件之间的耦合性,系统扩展性提高。由框架实现\n   2. doService doDispatch(request, response);\n2. **DispatcherServlet收到请求调用处理器映射器HandlerMapping**\n   1. HandlerMapping：处理器映射器。HandlerMapping负责根据用户请求的url找到Handler即处理器，springmvc提供了不同的映射器实现不同的映射方式，根据一定的规则去查找,例如：xml配置方式，实现接口方式，注解方式等。由框架实现\n3. **处理器映射器根据请求url找到具体的处理器，生成处理器执行链HandlerExecutionChain(包括处理器对象和处理器拦截器)一并返回给DispatcherServlet**\n4. **DispatcherServlet根据处理器Handler获取处理器适配器HandlerAdapter执行HandlerAdapter处理一系列的操作，如：参数封装，数据格式转换，数据验证等操作**\n   1. Handler：处理器。Handler 是继DispatcherServlet前端控制器的后端控制器，在DispatcherServlet的控制下Handler对具体的用户请求进行处理。由于Handler涉及到具体的用户业务请求，所以一般情况需要程序员根据业务需求开发Handler。\n   2. 将http报文转换为对象\n      1. HttpMessageConverter接口\n         1. `canRead` http->object\n         2. `canWrite` 对象的序列化输出\n      2. HttpMessageConverter有很多的实现类，根据HTTP协议的Accept和Content-Type属性，以及参数数据类型来判别使用哪一种HttpMessageConverter\n5. **执行处理器Handler(Controller，也叫页面控制器)**\n6. **Handler执行完成返回ModelAndView**\n7. **HandlerAdapter将Handler执行结果ModelAndView返回到DispatcherServlet**\n   1. HandlAdapter：处理器适配器。通过HandlerAdapter对处理器进行执行，这是适配器模式的应用，通过扩展适配器可以对更多类型的处理器进行执行。由框架实现。\n8. **DispatcherServlet将ModelAndView传给ViewReslover视图解析器**\n   1. ModelAndView是springmvc的封装对象，将model和view封装在一起\n9. **ViewReslover解析后返回具体View**\n   1. ViewResolver：视图解析器。ViewResolver负责将处理结果生成View视图，ViewResolver首先根据逻辑视图名解析成物理视图名即具体的页面地址，再生成View视图对象，最后对View进行渲染将处理结果通过页面展示给用户\n10. **DispatcherServlet对View进行渲染视图（即将模型数据model填充至视图中）**\n    1. View:是springmvc的封装对象，是一个接口, springmvc框架提供了很多的View视图类型，包括：jspview，pdfview,jstlView、freemarkerView、pdfView等。一般情况下需要通过页面标签或页面模版技术将模型数据通过页面展示给用户，需要由程序员根据业务需求开发具体的页面。\n11. **DispatcherServlet响应用户**"
  },
  {
    "path": "SpringCloud/springcloud.md",
    "content": "* [springcloud](#springcloud)\n    * [服务注册与发现](#服务注册与发现)\n        * [eureka](#eureka)\n        * [consul](#consul)\n    * [服务负载与调用](#服务负载与调用)\n        * [ribbon](#ribbon)\n        * [loadbalancer](#loadbalancer)\n    * [服务负载与调用](#服务负载与调用-1)\n        * [feign](#feign)\n        * [openFeign](#openfeign)\n    * [服务熔断与降级](#服务熔断与降级)\n        * [hystrix](#hystrix)\n        * [resilience4j](#resilience4j)\n    * [服务网关](#服务网关)\n        * [zuul](#zuul)\n        * [zuul2](#zuul2)\n        * [getway](#getway)\n    * [服务分布式配置](#服务分布式配置)\n        * [springcloud config](#springcloud-config)\n        * [Nacos](#nacos)\n* [springcloudAlibaba](#springcloudalibaba)\n    * [Nacos](#nacos-1)\n        * [服务注册中心](#服务注册中心)\n        * [服务配置](#服务配置)\n        * [服务总线](#服务总线)\n    * [Sentienl](#sentienl)\n# springcloud\nSpring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发，如服务发现注册、配置中心、智能路由、消息总线、负载均衡、断路器、数据监控等，都可以用Spring Boot的开发风格做到一键启动和部署。\n\nSpring Cloud并没有重复制造轮子，它只是将各家公司开发的比较成熟、经得起实际考验的服务框架组合起来，通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理，最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。\n## 服务注册与发现\n### eureka\n服务治理组件，包括服务端的注册中心和客户端的服务发现机制；\n### consul\n基于Hashicorp Consul的服务治理组件。\n## 服务负载与均衡\n### ribbon\n负载均衡的服务调用组件，具有多种负载均衡调用策略；\n### loadbalancer\n## 服务负载与调用\n### feign\n基于Ribbon和Hystrix的声明式服务调用组件；\n### openFeign\n基于Ribbon和Hystrix的声明式服务调用组件，可以动态创建基于Spring MVC注解的接口实现用于服务调用，在Spring Cloud 2.0中已经取代Feign成为了一等公民。\n## 服务熔断与降级\n### hystrix\n服务容错组件，实现了断路器模式，为依赖服务的出错和延迟提供了容错能力；\n### resilience4j\n## 服务网关\n### zuul\nAPI网关组件，对请求提供路由及过滤功能。\n### zuul2\n### getway\nAPI网关组件，对请求提供路由及过滤功能。\n## 服务分布式配置\n### springcloud config\n集中配置管理工具，分布式系统中统一的外部配置管理，默认使用Git来存储配置，可以支持客户端配置的刷新及加密、解密操作。\n### Nacos\n## 总线\n### Spring Cloud Bus\n用于传播集群状态变化的消息总线，使用轻量级消息代理链接分布式系统中的节点，可以用来动态刷新集群中的服务配置。\n# springcloudAlibaba\n## Nacos\nNacos 是一个开源的分布式系统服务发现、配置管理和服务管理平台。它主要包含以下功能：\n\n服务发现与注册：Nacos 可以管理服务的注册和发现，支持 DNS 和 HTTP/RESTful 方式。\n配置管理：Nacos 可以动态管理配置，支持多种数据类型和版本控制。\n服务管理：Nacos 可以对服务进行健康检查、流量管理、负载均衡等。\n### 服务发现与注册\nNacos 实现服务发现和注册的核心代码位于 nacos/naming 目录下，包括以下文件：\n\n- naming-common/src/main/java/com/alibaba/nacos/api/naming: 定义了服务发现和注册的 API 接口和数据模型。\n- naming-core/src/main/java/com/alibaba/nacos/naming: 实现了服务发现和注册的核心逻辑。\n- naming-impl/src/main/java/com/alibaba/nacos/naming: 实现了服务发现和注册的具体实现。\n\n下面简单介绍一下 Nacos 的服务发现和注册的实现流程。\n\n#### 服务注册流程\n服务提供者向 Nacos 注册服务时，会调用 NamingService.registerInstance() 方法，该方法会做以下几件事情：\n\n1. 将服务实例的元数据封装为 Instance 对象，包括服务名、IP、端口号、健康状态、元数据等。\n2. 将 Instance 对象转换为 InstanceEntity 对象，包含了实例 ID 和实例元数据的 JSON 字符串。\n3. 将 InstanceEntity 对象存储到 Nacos 中，可以存储到内存中或者持久化到磁盘中。\n\n#### 服务发现流程\n服务消费者向 Nacos 发现服务时，会调用 NamingService.getAllInstances() 或 NamingService.selectInstances() 方法，该方法会做以下几件事情：\n\n1. 从 Nacos 中获取服务实例的元数据，包括服务名、IP、端口号、健康状态、元数据等。\n2. 将元数据封装为 Instance 对象，存储到本地缓存中。\n3. 根据负载均衡算法选择一个服务实例处理请求，可以选择轮询、随机、权重等算法。\n\n服务发现和注册的核心逻辑在 naming-core 目录下的 com.alibaba.nacos.naming 包中实现，主要包括以下类：\n\n- com.alibaba.nacos.naming.core.InstancesManager: 维护服务实例的元数据和状态信息。\n- com.alibaba.nacos.naming.core.Cluster: 维护一个服务的所有实例信息和负载均衡策略。\n- com.alibaba.nacos.naming.core.InstanceOperator: 实现了服务实例的注册、注销和更新操作。\n- com.alibaba.nacos.naming.core.DomainsManager: 维护多个服务的信息，包括服务名、集群名、命名空间等。\n\n服务发现和注册的具体实现在 naming-impl 目录下的 com.alibaba.nacos.naming 包中，主要包括以下类：\n\n- com.alibaba.nacos.naming.push.PushService: 实现了服务实例的推送功能。\n- com.alibaba.nacos.naming.healthcheck.HealthCheckProcessor: 实现了服务实例的健康检查功能。\n- com.alibaba.nacos.naming.misc.GlobalConfig: 存储了一些全局配置，例如默认权重值、心跳间隔\n\n### 配置动态刷新\n\n从远端服务器获取变更数据的主要模式有两种：推（push）和拉（pull）。Push 模式简单来说就是服务端主动将数据变更信息推送给客户端，这种模式优点是时效性好，服务端数据发生变更可以立马通知到客户端，但这种模式需要服务端维持与客户端的心跳连接，会增加服务端实现的复杂度，服务端也需要占用更多的资源来维持与客户端的连接。\n\n而 Pull 模式则是客户端主动去服务器请求数据，例如，每间隔10ms就向服务端发起请求获取数据。显而易见pull模式存在时效性问题。请求的间隔也不太好设置，间隔太短，对服务器请求压力过大。间隔时间过长，那么必然会造成时效性很差。而且如果配置长时间不更新，并且存在大量的客户端就会产生大量无效的pull请求。\n\nNacos 没有采用上述的两种模式，而是采用了长轮询方式结合了推和拉的优点：\n\n<img src=\"../img/springcloud/nacos/nacosconfig动态刷新机制.png\" width=\"50%\" />\n\n长轮询也是轮询，因此 Nacos 客户端会默认每10ms向服务端发起请求，当客户端请求服务端时会在请求头上携带长轮询的超时时间，默认是30s。而服务端接收到该请求时会hang住请求，为了防止客户端超时会在请求头携带的超时时间上减去500ms，因此默认会hang住请求29.5s。在这期间如果服务端发生了配置变更会产生相应的事件，监听到该事件后，会响应对应的客户端。这样一来客户端不会频繁发起轮询请求，而服务端也不需要维持与客户端的心跳，兼备了时效性和复杂度。\n\n<img src=\"../img/springcloud/nacos/nacosonfig动态刷新流程图.png\" width=\"80%\" />\n\n\n> 1.4版本nacos使用Http短连接+长轮询的方式，客户端发起http请求，服务端hold住请求，当配置变更时响应客户端，超时时间30s。\n> 2.0版本nacos用gRPC长连接代替了http短连接长轮询。配置同步采用推拉结合的方式。\n\n#### Nacos Config 长轮询源码剖析\n首先，打开 com.alibaba.cloud.nacos.NacosConfigBootstrapConfiguration 这个类，从类名也可以看出该类是Nacos Config的启动配置类，是Nacos Config自动装配的入口。在该类中的 nacosConfigManager 方法实例化了一个 NacosConfigManager 对象，并注册到容器中：\n\n```java\n@Bean\n@ConditionalOnMissingBean\npublic NacosConfigManager nacosConfigManager(\n\t\tNacosConfigProperties nacosConfigProperties) {\n\treturn new NacosConfigManager(nacosConfigProperties);\n}\n```\n在 NacosConfigManager 的构造器中调用了 createConfigService 方法，这是一个静态方法用来创建 ConfigService 对象的单例。\n```java\n/**\n * Compatible with old design,It will be perfected in the future.\n */\nstatic ConfigService createConfigService(\n\t\tNacosConfigProperties nacosConfigProperties) {\n    // 双重检查锁模式的单例\n\tif (Objects.isNull(service)) {\n\t\tsynchronized (NacosConfigManager.class) {\n\t\t\ttry {\n\t\t\t\tif (Objects.isNull(service)) {\n\t\t\t\t\tservice = NacosFactory.createConfigService(\n\t\t\t\t\t\t\tnacosConfigProperties.assembleConfigServiceProperties());\n\t\t\t\t}\n\t\t\t}\n\t\t\tcatch (NacosException e) {\n\t\t\t\tlog.error(e.getMessage());\n\t\t\t\tthrow new NacosConnectionFailureException(\n\t\t\t\t\t\tnacosConfigProperties.getServerAddr(), e.getMessage(), e);\n\t\t\t}\n\t\t}\n\t}\n\treturn service;\n}\n```\nConfigService 的具体实现是 NacosConfigService，在该类的构造器中主要初始化了 HttpAgent 和 ClientWorker 对象。ClientWorker 的构造器中则初始化了几个线程池：\n\n```java\npublic ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager,\n        final Properties properties) {\n    this.agent = agent;\n    this.configFilterChainManager = configFilterChainManager;\n    \n    // Initialize the timeout parameter\n    init(properties);\n    \n    // 创建具有定时执行功能的单线程池，用于定时执行 checkConfigInfo 方法\n    this.executor = Executors.newScheduledThreadPool(1, new ThreadFactory() {\n        @Override\n        public Thread newThread(Runnable r) {\n            Thread t = new Thread(r);\n            t.setName(\"com.alibaba.nacos.client.Worker.\" + agent.getName());\n            t.setDaemon(true);\n            return t;\n        }\n    });\n    \n    // 创建具有定时执行功能的且线程数与cpu核数相对应的线程池，用于根据需要动态刷新的配置文件执行 LongPollingRunnable，因此长轮询任务是可以有多个并行的\n    this.executorService = Executors\n            .newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() {\n                @Override\n                public Thread newThread(Runnable r) {\n                    Thread t = new Thread(r);\n                    t.setName(\"com.alibaba.nacos.client.Worker.longPolling.\" + agent.getName());\n                    t.setDaemon(true);\n                    return t;\n                }\n            });\n            \n    // 每10ms执行一次 checkConfigInfo 方法\n    this.executor.scheduleWithFixedDelay(new Runnable() {\n        @Override\n        public void run() {\n            try {\n                checkConfigInfo();\n            } catch (Throwable e) {\n                LOGGER.error(\"[\" + agent.getName() + \"] [sub-check] rotate check error\", e);\n            }\n        }\n    }, 1L, 10L, TimeUnit.MILLISECONDS);\n}\n\nprivate void init(Properties properties) {\n    // 长轮询的超时时间，默认为30秒，此参数会被放到请求头中带到服务端，服务端会根据该参数去做长轮询的hold\n    timeout = Math.max(ConvertUtils.toInt(properties.getProperty(PropertyKeyConst.CONFIG_LONG_POLL_TIMEOUT),\n            Constants.CONFIG_LONG_POLL_TIMEOUT), Constants.MIN_CONFIG_LONG_POLL_TIMEOUT);\n    \n    taskPenaltyTime = ConvertUtils\n            .toInt(properties.getProperty(PropertyKeyConst.CONFIG_RETRY_TIME), Constants.CONFIG_RETRY_TIME);\n    \n    this.enableRemoteSyncConfig = Boolean\n            .parseBoolean(properties.getProperty(PropertyKeyConst.ENABLE_REMOTE_SYNC_CONFIG));\n}\n\n/**\n * Check config info.\n */\npublic void checkConfigInfo() {\n    // Dispatch taskes.\n    // 获取需要监听的文件数量\n    int listenerSize = cacheMap.size();\n    // Round up the longingTaskCount.\n    // 默认一个 LongPollingRunnable 可以处理监听3k个配置文件的变化，超过3k个才会创建新的 LongPollingRunnable\n    int longingTaskCount = (int) Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());\n    if (longingTaskCount > currentLongingTaskCount) {\n        for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {\n            // The task list is no order.So it maybe has issues when changing.\n            executorService.execute(new LongPollingRunnable(i));\n        }\n        currentLongingTaskCount = longingTaskCount;\n    }\n}\n```\nLongPollingRunnable 类主要用于检查本地配置，以及长轮询地去服务端获取变更配置的 dataid 和 group，其代码位于 com.alibaba.nacos.client.config.impl.ClientWorker 类，代码如下：\n```java\nclass LongPollingRunnable implements Runnable {\n    \n    private final int taskId;\n    \n    public LongPollingRunnable(int taskId) {\n        this.taskId = taskId;\n    }\n    \n    @Override\n    public void run() {\n        \n        List<CacheData> cacheDatas = new ArrayList<CacheData>();\n        List<String> inInitializingCacheList = new ArrayList<String>();\n        try {\n            // check failover config\n            // 遍历本地缓存的配置\n            for (CacheData cacheData : cacheMap.values()) {\n                if (cacheData.getTaskId() == taskId) {\n                    cacheDatas.add(cacheData);\n                    try {\n                        // 检查本地配置\n                        checkLocalConfig(cacheData);\n                        if (cacheData.isUseLocalConfigInfo()) {\n                            cacheData.checkListenerMd5();\n                        }\n                    } catch (Exception e) {\n                        LOGGER.error(\"get local config info error\", e);\n                    }\n                }\n            }\n            \n            // check server config\n            // 通过长轮询检查服务端配置\n            List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);\n            if (!CollectionUtils.isEmpty(changedGroupKeys)) {\n                LOGGER.info(\"get changedGroupKeys:\" + changedGroupKeys);\n            }\n            \n            for (String groupKey : changedGroupKeys) {\n                String[] key = GroupKey.parseKey(groupKey);\n                String dataId = key[0];\n                String group = key[1];\n                String tenant = null;\n                if (key.length == 3) {\n                    tenant = key[2];\n                }\n                try {\n                    String[] ct = getServerConfig(dataId, group, tenant, 3000L);\n                    CacheData cache = cacheMap.get(GroupKey.getKeyTenant(dataId, group, tenant));\n                    cache.setContent(ct[0]);\n                    if (null != ct[1]) {\n                        cache.setType(ct[1]);\n                    }\n                    LOGGER.info(\"[{}] [data-received] dataId={}, group={}, tenant={}, md5={}, content={}, type={}\",\n                            agent.getName(), dataId, group, tenant, cache.getMd5(),\n                            ContentUtils.truncateContent(ct[0]), ct[1]);\n                } catch (NacosException ioe) {\n                    String message = String\n                            .format(\"[%s] [get-update] get changed config exception. dataId=%s, group=%s, tenant=%s\",\n                                    agent.getName(), dataId, group, tenant);\n                    LOGGER.error(message, ioe);\n                }\n            }\n            for (CacheData cacheData : cacheDatas) {\n                if (!cacheData.isInitializing() || inInitializingCacheList\n                        .contains(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant))) {\n                    cacheData.checkListenerMd5();\n                    cacheData.setInitializing(false);\n                }\n            }\n            inInitializingCacheList.clear();\n            \n            executorService.execute(this);\n            \n        } catch (Throwable e) {\n            \n            // If the rotation training task is abnormal, the next execution time of the task will be punished\n            LOGGER.error(\"longPolling error : \", e);\n            executorService.schedule(this, taskPenaltyTime, TimeUnit.MILLISECONDS);\n        }\n    }\n}\n```\n上面有个 checkUpdateDataIds 方法，用于获取发生变更了的配置文件的dataId列表，它同样位于 ClientWorker 内。如下：\n```java\n/**\n * Fetch the dataId list from server.\n *\n * @param cacheDatas              CacheDatas for config infomations.\n * @param inInitializingCacheList initial cache lists.\n * @return String include dataId and group (ps: it maybe null).\n * @throws Exception Exception.\n */\nList<String> checkUpdateDataIds(List<CacheData> cacheDatas, List<String> inInitializingCacheList) throws Exception {\n    // 拼接出配置文件的唯一标识\n    StringBuilder sb = new StringBuilder();\n    for (CacheData cacheData : cacheDatas) {\n        if (!cacheData.isUseLocalConfigInfo()) {\n            sb.append(cacheData.dataId).append(WORD_SEPARATOR);\n            sb.append(cacheData.group).append(WORD_SEPARATOR);\n            if (StringUtils.isBlank(cacheData.tenant)) {\n                sb.append(cacheData.getMd5()).append(LINE_SEPARATOR);\n            } else {\n                sb.append(cacheData.getMd5()).append(WORD_SEPARATOR);\n                sb.append(cacheData.getTenant()).append(LINE_SEPARATOR);\n            }\n            if (cacheData.isInitializing()) {\n                // It updates when cacheData occours in cacheMap by first time.\n                inInitializingCacheList\n                        .add(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant));\n            }\n        }\n    }\n    boolean isInitializingCacheList = !inInitializingCacheList.isEmpty();\n    return checkUpdateConfigStr(sb.toString(), isInitializingCacheList);\n}\n\n/**\n * Fetch the updated dataId list from server.\n *\n * @param probeUpdateString       updated attribute string value.\n * @param isInitializingCacheList initial cache lists.\n * @return The updated dataId list(ps: it maybe null).\n * @throws IOException Exception.\n */\nList<String> checkUpdateConfigStr(String probeUpdateString, boolean isInitializingCacheList) throws Exception {\n    \n    Map<String, String> params = new HashMap<String, String>(2);\n    params.put(Constants.PROBE_MODIFY_REQUEST, probeUpdateString);\n    Map<String, String> headers = new HashMap<String, String>(2);\n    // 长轮询的超时时间\n    headers.put(\"Long-Pulling-Timeout\", \"\" + timeout);\n    \n    // told server do not hang me up if new initializing cacheData added in\n    if (isInitializingCacheList) {\n        headers.put(\"Long-Pulling-Timeout-No-Hangup\", \"true\");\n    }\n    \n    if (StringUtils.isBlank(probeUpdateString)) {\n        return Collections.emptyList();\n    }\n    \n    try {\n        // In order to prevent the server from handling the delay of the client's long task,\n        // increase the client's read timeout to avoid this problem.\n        \n        long readTimeoutMs = timeout + (long) Math.round(timeout >> 1);\n        // 向服务端发起一个http请求，该请求在服务端配置没有变更的情况下默认会hang住30s\n        HttpRestResult<String> result = agent\n                .httpPost(Constants.CONFIG_CONTROLLER_PATH + \"/listener\", headers, params, agent.getEncode(),\n                        readTimeoutMs);\n        \n        if (result.ok()) {\n            setHealthServer(true);\n            // 响应状态是成功则解析响应体得到 dataId、group、tenant 等信息并返回\n            return parseUpdateDataIdResponse(result.getData());\n        } else {\n            setHealthServer(false);\n            LOGGER.error(\"[{}] [check-update] get changed dataId error, code: {}\", agent.getName(),\n                    result.getCode());\n        }\n    } catch (Exception e) {\n        setHealthServer(false);\n        LOGGER.error(\"[\" + agent.getName() + \"] [check-update] get changed dataId exception\", e);\n        throw e;\n    }\n    return Collections.emptyList();\n}\n```\n客户端对 listener 接口的请求会进入到服务端的 com.alibaba.nacos.config.server.controller.ConfigController#listener 方法进行处理，该方法主要是调用了 com.alibaba.nacos.config.server.controller.ConfigServletInner#doPollingConfig 方法。代码如下：\n```java\n/**\n * 轮询接口\n */\npublic String doPollingConfig(HttpServletRequest request, HttpServletResponse response,\n                              Map<String, String> clientMd5Map, int probeRequestSize)\n    throws IOException, ServletException {\n\n    // 如果支持长轮询则进入长轮询的流程\n    if (LongPollingService.isSupportLongPolling(request)) {\n        longPollingService.addLongPollingClient(request, response, clientMd5Map, probeRequestSize);\n        return HttpServletResponse.SC_OK + \"\";\n    }\n\n    // else 兼容短轮询逻辑\n    List<String> changedGroups = MD5Util.compareMd5(request, response, clientMd5Map);\n\n    // 兼容短轮询result\n    String oldResult = MD5Util.compareMd5OldResult(changedGroups);\n    String newResult = MD5Util.compareMd5ResultString(changedGroups);\n\n    String version = request.getHeader(Constants.CLIENT_VERSION_HEADER);\n    if (version == null) {\n        version = \"2.0.0\";\n    }\n    int versionNum = Protocol.getVersionNumber(version);\n\n    /**\n     * 2.0.4版本以前, 返回值放入header中\n     */\n    if (versionNum < START_LONGPOLLING_VERSION_NUM) {\n        response.addHeader(Constants.PROBE_MODIFY_RESPONSE, oldResult);\n        response.addHeader(Constants.PROBE_MODIFY_RESPONSE_NEW, newResult);\n    } else {\n        request.setAttribute(\"content\", newResult);\n    }\n\n    // 禁用缓存\n    response.setHeader(\"Pragma\", \"no-cache\");\n    response.setDateHeader(\"Expires\", 0);\n    response.setHeader(\"Cache-Control\", \"no-cache,no-store\");\n    response.setStatus(HttpServletResponse.SC_OK);\n    return HttpServletResponse.SC_OK + \"\";\n}\n```\n我们主要关注上面的 com.alibaba.nacos.config.server.service.LongPollingService#addLongPollingClient 长轮询流程的方法。代码如下：\n```java\npublic void addLongPollingClient(HttpServletRequest req, HttpServletResponse rsp, Map<String, String> clientMd5Map,\n                                 int probeRequestSize) {\n\n    String str = req.getHeader(LongPollingService.LONG_POLLING_HEADER);\n    String noHangUpFlag = req.getHeader(LongPollingService.LONG_POLLING_NO_HANG_UP_HEADER);\n    String appName = req.getHeader(RequestUtil.CLIENT_APPNAME_HEADER);\n    String tag = req.getHeader(\"Vipserver-Tag\");\n    int delayTime = SwitchService.getSwitchInteger(SwitchService.FIXED_DELAY_TIME, 500);\n    /**\n     * 提前500ms返回响应，为避免客户端超时 @qiaoyi.dingqy 2013.10.22改动  add delay time for LoadBalance\n     */\n    long timeout = Math.max(10000, Long.parseLong(str) - delayTime);\n    if (isFixedPolling()) {\n        timeout = Math.max(10000, getFixedPollingInterval());\n        // do nothing but set fix polling timeout\n    } else {\n        long start = System.currentTimeMillis();\n        List<String> changedGroups = MD5Util.compareMd5(req, rsp, clientMd5Map);\n        if (changedGroups.size() > 0) {\n            generateResponse(req, rsp, changedGroups);\n            LogUtil.clientLog.info(\"{}|{}|{}|{}|{}|{}|{}\",\n                System.currentTimeMillis() - start, \"instant\", RequestUtil.getRemoteIp(req), \"polling\",\n                clientMd5Map.size(), probeRequestSize, changedGroups.size());\n            return;\n        } else if (noHangUpFlag != null && noHangUpFlag.equalsIgnoreCase(TRUE_STR)) {\n            LogUtil.clientLog.info(\"{}|{}|{}|{}|{}|{}|{}\", System.currentTimeMillis() - start, \"nohangup\",\n                RequestUtil.getRemoteIp(req), \"polling\", clientMd5Map.size(), probeRequestSize,\n                changedGroups.size());\n            return;\n        }\n    }\n    String ip = RequestUtil.getRemoteIp(req);\n    // 一定要由HTTP线程调用，否则离开后容器会立即发送响应\n    final AsyncContext asyncContext = req.startAsync();\n    // AsyncContext.setTimeout()的超时时间不准，所以只能自己控制\n    asyncContext.setTimeout(0L);\n\t\n\t// 在 ClientLongPolling 的 run 方法会将 ClientLongPolling 实例（携带了本次请求的相关信息）放入 allSubs 中，然后会在29.5s后再执行另一个 Runnable，该 Runnable 用于等待29.5s后依旧没有相应的配置变更时对客户端进行响应，并将相应的 ClientLongPolling 实例从 allSubs 中移出\n    scheduler.execute(\n        new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));\n}\n```\n而 LongPollingService 实现了 AbstractEventListener，也就是说能接收事件通知，在其 com.alibaba.nacos.config.server.service.LongPollingService#onEvent 方法中可以看到，它关注的是 LocalDataChangeEvent 事件：\n```java\n@Override\npublic void onEvent(Event event) {\n    if (isFixedPolling()) {\n        // ignore\n    } else {\n        if (event instanceof LocalDataChangeEvent) {\n            LocalDataChangeEvent evt = (LocalDataChangeEvent)event;\n            scheduler.execute(new DataChangeTask(evt.groupKey, evt.isBeta, evt.betaIps));\n        }\n    }\n}\n```\n在nacos上修改配置后就会产生 LocalDataChangeEvent 事件，此时 LongPollingService 也就能监听到，当收到该事件时就会遍历 allSubs，找到匹配的请求并将 groupKey 返回给客户端。具体代码在 DataChangeTask 中：\n```java\nclass DataChangeTask implements Runnable {\n    @Override\n    public void run() {\n        try {\n            ConfigService.getContentBetaMd5(groupKey);\n            for (Iterator<ClientLongPolling> iter = allSubs.iterator(); iter.hasNext(); ) {\n                ClientLongPolling clientSub = iter.next();\n                if (clientSub.clientMd5Map.containsKey(groupKey)) {\n                    // 如果beta发布且不在beta列表直接跳过\n                    if (isBeta && !betaIps.contains(clientSub.ip)) {\n                        continue;\n                    }\n\n                    // 如果tag发布且不在tag列表直接跳过\n                    if (StringUtils.isNotBlank(tag) && !tag.equals(clientSub.tag)) {\n                        continue;\n                    }\n\n                    getRetainIps().put(clientSub.ip, System.currentTimeMillis());\n                    iter.remove(); // 删除订阅关系\n                    LogUtil.clientLog.info(\"{}|{}|{}|{}|{}|{}|{}\",\n                        (System.currentTimeMillis() - changeTime),\n                        \"in-advance\",\n                        RequestUtil.getRemoteIp((HttpServletRequest)clientSub.asyncContext.getRequest()),\n                        \"polling\",\n                        clientSub.clientMd5Map.size(), clientSub.probeRequestSize, groupKey);\n                    clientSub.sendResponse(Arrays.asList(groupKey));\n                }\n            }\n        } catch (Throwable t) {\n            LogUtil.defaultLog.error(\"data change error:\" + t.getMessage(), t.getCause());\n        }\n    }\n\n    DataChangeTask(String groupKey) {\n        this(groupKey, false, null);\n    }\n\n    DataChangeTask(String groupKey, boolean isBeta, List<String> betaIps) {\n        this(groupKey, isBeta, betaIps, null);\n    }\n\n    DataChangeTask(String groupKey, boolean isBeta, List<String> betaIps, String tag) {\n        this.groupKey = groupKey;\n        this.isBeta = isBeta;\n        this.betaIps = betaIps;\n        this.tag = tag;\n    }\n\n    final String groupKey;\n    final long changeTime = System.currentTimeMillis();\n    final boolean isBeta;\n    final List<String> betaIps;\n    final String tag;\n}\n```\n当客户端收到变更的dataid+group后，就会去服务端获取最新的配置数据，并更新本地数据 cacheData，然后发送数据变更事件，整个流程结束。\n\n- 获取服务端最新配置数据的方法：com.alibaba.nacos.client.config.impl.ClientWorker#getServerConfig\n- 发送数据变更事件的方法：com.alibaba.nacos.client.config.impl.CacheData#checkListenerMd5\n\n## Sentienl\nSentinel是阿里巴巴开源的一款流量控制和熔断降级框架，主要用于微服务架构中服务的流量控制和熔断降级。其限流实现原理主要分为两个部分：\n\n> 统计信息收集\n\nSentinel会在运行过程中对服务的各种统计信息进行收集，包括请求的响应时间、请求通过的QPS（每秒查询率）、线程池队列大小等指标。这些指标通过定义的规则进行分析，判断当前请求是否超过了设定的阈值。\n\n> 阈值判断\n\nSentinel根据收集到的统计信息，通过定义的规则对请求进行判断。规则中包括以下几个要素：\n\n资源名：对哪个资源进行限流\n流控模式：直接拒绝或者匀速通过\n流控阈值：单位时间内允许通过的请求个数\n统计时间窗口：多长时间内统计一次流量，单位秒\n降级处理：当请求超过阈值时的处理策略，如直接拒绝、返回默认值等\nSentinel会根据以上规则进行限流，当请求超过阈值时，根据设置的降级处理策略进行处理，比如直接拒绝请求、返回默认值等。同时，Sentinel还可以进行自适应的流控，根据实际情况调整阈值，保证服务的可用性和稳定性。\n\n### 工作原理\n#### Slot 插槽\n在 Sentinel 里面，所有的资源都对应一个资源名称（resourceName），每次资源调用都会创建一个 Entry 对象。Entry 可以通过对主流框架的适配自动创建，也可以通过注解的方式或调用 SphU API 显式创建。Entry 创建的时候，同时也会创建一系列功能插槽（slot chain），这些插槽有不同的职责，例如:\n\n- NodeSelectorSlot 负责收集资源的路径，并将这些资源的调用路径，以树状结构存储起来，用于根据调用路径来限流降级；\n- ClusterBuilderSlot 则用于存储资源的统计信息以及调用者信息，例如该资源的 RT, QPS, thread count 等等，这些信息将用作为多维度限流，降级的依据；\n- StatisticSlot 则用于记录、统计不同纬度的 runtime 指标监控信息；\n- FlowSlot 则用于根据预设的限流规则以及前面 slot 统计的状态，来进行流量控制；\n- AuthoritySlot 则根据配置的黑白名单和调用来源信息，来做黑白名单控制；\n- DegradeSlot 则通过统计信息以及预设的规则，来做熔断降级；\n- SystemSlot 则通过系统的状态，例如 load1 等，来控制总的入口流量；\n\nSentinel 提供了插槽接口 ProcessorSlot，其中提供了方法 enrty 处理进入请求 和 exit 处理请求结束操作\n```java\npublic interface ProcessorSlot<T> {\n \n    /**\n     * Entrance of this slot.\n     *\n     * @param context         current {@link Context}\n     * @param resourceWrapper current resource\n     * @param param           generics parameter, usually is a {@link com.alibaba.csp.sentinel.node.Node}\n     * @param count           tokens needed\n     * @param prioritized     whether the entry is prioritized\n     * @param args            parameters of the original call\n     * @throws Throwable blocked exception or unexpected error\n     */\n    void entry(Context context, ResourceWrapper resourceWrapper, T param, int count, boolean prioritized,\n               Object... args) throws Throwable;\n \n    /**\n     * Means finish of {@link #entry(Context, ResourceWrapper, Object, int, boolean, Object...)}.\n     *\n     * @param context         current {@link Context}\n     * @param resourceWrapper current resource\n     * @param obj             relevant object (e.g. Node)\n     * @param count           tokens needed\n     * @param prioritized     whether the entry is prioritized\n     * @param args            parameters of the original call\n     * @throws Throwable blocked exception or unexpected error\n     */\n    void fireEntry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized,\n                   Object... args) throws Throwable;\n \n    /**\n     * Exit of this slot.\n     *\n     * @param context         current {@link Context}\n     * @param resourceWrapper current resource\n     * @param count           tokens needed\n     * @param args            parameters of the original call\n     */\n    void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args);\n \n    /**\n     * Means finish of {@link #exit(Context, ResourceWrapper, int, Object...)}.\n     *\n     * @param context         current {@link Context}\n     * @param resourceWrapper current resource\n     * @param count           tokens needed\n     * @param args            parameters of the original call\n     */\n    void fireExit(Context context, ResourceWrapper resourceWrapper, int count, Object... args);\n}\n```\n总体的框架如下:\n\n![](../img/springcloud/sentinel/sentinel-slot.png)\n\nSentinel 将 SlotChainBuilder 作为 SPI 接口进行扩展，使得 Slot Chain 具备了扩展的能力。您可以自行加入自定义的 slot 并编排 slot 间的顺序，从而可以给 Sentinel 添加自定义的功能。\n\n![](../img/springcloud/sentinel/Sentinel-自定义slot.png)\n\n#### RuleManager 规则管理器\n每个 Slot 插槽背后都对应着一个 RuleManager 的实现类，简单理解就是每个 Slot 有一套规则，规则验证处理由对应的 RuleManager 来进行处理。\n\n流量控制：FlowSolt 对应 FlowRuleManager\n\n降级控制：DegradeSlot  对应 DegradeRuleManager\n\n权限控制：AuthoritySlot 对应 AuthorityRuleManager\n\n系统规则控制： SystemSlot 对应 SystemRuleManager\n\n#### 降级控制实现原理\n1. 新增资源配置降级规则，目前对于降级策有如下三种：\n\nRT：平均响应时间 (DEGRADE_GRADE_RT)：当 1s 内持续进入 5 个请求，对应时刻的平均响应时间（秒级）均超过阈值（count，以 ms 为单位），那么在接下的时间窗口（DegradeRule 中的 timeWindow，以 s 为单位）之内，对这个方法的调用都会自动地熔断（抛出 DegradeException）。注意 Sentinel 默认统计的 RT 上限是 4900 ms，超出此阈值的都会算作 4900 ms，若需要变更此上限可以通过启动配置项 -Dcsp.sentinel.statistic.max.rt=xxx 来配置。\n\n![](../img/springcloud/sentinel/降级-RT.png)\n\n异常比例：当资源的每秒请求量 >= 5，并且每秒异常总数占通过量的比值超过阈值（DegradeRule 中的 count）之后，资源进入降级状态，即在接下的时间窗口（DegradeRule 中的 timeWindow，以 s 为单位）之内，对这个方法的调用都会自动地返回。异常比率的阈值范围是 [0.0, 1.0]，代表 0% - 100%。\n\n![](../img/springcloud/sentinel/降级-异常比例.png)\n\n异常数：当资源近 1 分钟的异常数目超过阈值之后会进行熔断。注意由于统计时间窗口是分钟级别的，若 timeWindow 小于 60s，则结束熔断状态后仍可能再进入熔断状态。\n\n![](../img/springcloud/sentinel/降级-异常数.png)\n\n限流结果信息\n\n```text\nBlocked by Sentinel (flow limiting)\n```\n\n2. 实现逻辑\n\n在之前我们已经提及 Sentinel 是通过 slot 链来实现的，对于降级功能其提供了 DegradeSlot，实现源码如下：\n```java\npublic class DegradeSlot extends AbstractLinkedProcessorSlot<DefaultNode> {\n \n    @Override\n    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args)\n        throws Throwable {\n        DegradeRuleManager.checkDegrade(resourceWrapper, context, node, count);\n        fireEntry(context, resourceWrapper, node, count, prioritized, args);\n    }\n \n    @Override\n    public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {\n        fireExit(context, resourceWrapper, count, args);\n    }\n}\n```\n通过上面代码我们可以了解到，限流规则的实现是在 DegradeRuleManager 的checkDegrade中来处理的，限流可以-配置多个规则，依次按照规则来处理。\n```java\npublic static void checkDegrade(ResourceWrapper resource, Context context, DefaultNode node, int count)\n        throws BlockException {\n \n        Set<DegradeRule> rules = degradeRules.get(resource.getName());\n        if (rules == null) {\n            return;\n        }\n \n        for (DegradeRule rule : rules) {\n            if (!rule.passCheck(context, node, count)) {\n                throw new DegradeException(rule.getLimitApp(), rule);\n            }\n        }\n    }\n```\n在 DegradeRule 的 passCheck 方法中我们可以看到可以根据 RT、异常数和异常比例来进行熔断降级处理。\n```java\n@Override\n    public boolean passCheck(Context context, DefaultNode node, int acquireCount, Object... args) {\n        if (cut.get()) {\n            return false;\n        }\n \n        ClusterNode clusterNode = ClusterBuilderSlot.getClusterNode(this.getResource());\n        if (clusterNode == null) {\n            return true;\n        }\n \n\t\t// 请求处理时间\n        if (grade == RuleConstant.DEGRADE_GRADE_RT) {\n            double rt = clusterNode.avgRt();\n            if (rt < this.count) {\n                passCount.set(0);\n                return true;\n            }\n \n            // Sentinel will degrade the service only if count exceeds.\n            if (passCount.incrementAndGet() < rtSlowRequestAmount) {\n                return true;\n            }\n        } else if (grade == RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO) {\n\t\t\t//异常比例\n            double exception = clusterNode.exceptionQps();\n            double success = clusterNode.successQps();\n            double total = clusterNode.totalQps();\n            // If total amount is less than minRequestAmount, the request will pass.\n            if (total < minRequestAmount) {\n                return true;\n            }\n \n            // In the same aligned statistic time window,\n            // \"success\" (aka. completed count) = exception count + non-exception count (realSuccess)\n            double realSuccess = success - exception;\n            if (realSuccess <= 0 && exception < minRequestAmount) {\n                return true;\n            }\n \n            if (exception / success < count) {\n                return true;\n            }\n        } else if (grade == RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT) {\n\t\t\t//异常数\n            double exception = clusterNode.totalException();\n            if (exception < count) {\n                return true;\n            }\n        }\n \n        if (cut.compareAndSet(false, true)) {\n            ResetTask resetTask = new ResetTask(this);\n            pool.schedule(resetTask, timeWindow, TimeUnit.SECONDS);\n        }\n \n        return false;\n    }\n```\n\n#### 流量控制实现原理\n接下来我们了解学习一下 Sentinel 是如何实现流量控制的\n\n流量控制（flow control），其原理是监控应用流量的 QPS 或并发线程数等指标，当达到指定的阈值时对流量进行控制，以避免被瞬时的流量高峰冲垮，从而保障应用的高可用性。\n\nFlowSlot 会根据预设的规则，结合前面 NodeSelectorSlot、ClusterNodeBuilderSlot、StatisticSlot 统计出来的实时信息进行流量控制。\n\n限流的直接表现是在执行 Entry nodeA = SphU.entry(resourceName) 的时候抛出 FlowException 异常。FlowException 是 BlockException 的子类，您可以捕捉 BlockException 来自定义被限流之后的处理逻辑。\n\n同一个资源可以创建多条限流规则。FlowSlot 会对该资源的所有限流规则依次遍历，直到有规则触发限流或者所有规则遍历完毕。\n\n一条限流规则主要由下面几个因素组成，我们可以组合这些元素来实现不同的限流效果：\n\n- resource：资源名，即限流规则的作用对象\n- count: 限流阈值\n- grade: 限流阈值类型（QPS 或并发线程数）\n- limitApp: 流控针对的调用来源，若为 default 则不区分调用来源\n- strategy: 调用关系限流策略\n- controlBehavior: 流量控制效果（直接拒绝、Warm Up、匀速排队）\n\n流控-QPS配置\n\n![](../img/springcloud/sentinel/流控-QPS配置.png)\n\n流控-线程数配置\n\n![](../img/springcloud/sentinel/流控-线程数配置.png)\n\n##### 实现流程\nSentinel 提供了 FlowSlot 用来进行流量控制，流量规则的最终实现在 FlowRuleChecker 的 checkFlow 中实现的。\n```java\npublic class FlowSlot extends AbstractLinkedProcessorSlot<DefaultNode> {\n \n    private final FlowRuleChecker checker;\n \n    public FlowSlot() {\n        this(new FlowRuleChecker());\n    }\n \n    /**\n     * Package-private for test.\n     *\n     * @param checker flow rule checker\n     * @since 1.6.1\n     */\n    FlowSlot(FlowRuleChecker checker) {\n        AssertUtil.notNull(checker, \"flow checker should not be null\");\n        this.checker = checker;\n    }\n \n    @Override\n    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,\n                      boolean prioritized, Object... args) throws Throwable {\n        checkFlow(resourceWrapper, context, node, count, prioritized);\n \n        fireEntry(context, resourceWrapper, node, count, prioritized, args);\n    }\n \n    void checkFlow(ResourceWrapper resource, Context context, DefaultNode node, int count, boolean prioritized)\n        throws BlockException {\n        checker.checkFlow(ruleProvider, resource, context, node, count, prioritized);\n    }\n \n    @Override\n    public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {\n        fireExit(context, resourceWrapper, count, args);\n    }\n \n    private final Function<String, Collection<FlowRule>> ruleProvider = new Function<String, Collection<FlowRule>>() {\n        @Override\n        public Collection<FlowRule> apply(String resource) {\n            // Flow rule map should not be null.\n            Map<String, List<FlowRule>> flowRules = FlowRuleManager.getFlowRuleMap();\n            return flowRules.get(resource);\n        }\n    };\n}\n```\n在 checkFlow 中会依次获取我们配置的流控规则，然后依次进行流控判断处理，如果被流控则抛出异常 FlowException\n```java\npublic void checkFlow(Function<String, Collection<FlowRule>> ruleProvider, ResourceWrapper resource,\n                          Context context, DefaultNode node, int count, boolean prioritized) throws BlockException {\n        if (ruleProvider == null || resource == null) {\n            return;\n        }\n        Collection<FlowRule> rules = ruleProvider.apply(resource.getName());\n        if (rules != null) {\n            for (FlowRule rule : rules) {\n                if (!canPassCheck(rule, context, node, count, prioritized)) {\n                    throw new FlowException(rule.getLimitApp(), rule);\n                }\n            }\n        }\n    }\n```\n在 canPassCheck 中会判断是集群限流还是本地限流\n```java\npublic boolean canPassCheck(/*@NonNull*/ FlowRule rule, Context context, DefaultNode node, int acquireCount,\n                                                    boolean prioritized) {\n        String limitApp = rule.getLimitApp();\n        if (limitApp == null) {\n            return true;\n        }\n \n        if (rule.isClusterMode()) {\n            return passClusterCheck(rule, context, node, acquireCount, prioritized);\n        }\n \n        return passLocalCheck(rule, context, node, acquireCount, prioritized);\n    }\n```\n如果是本地限流则获取节点信息，然后根据流控规则进行流控判断\n```java\nprivate static boolean passLocalCheck(FlowRule rule, Context context, DefaultNode node, int acquireCount,\n                                          boolean prioritized) {\n        Node selectedNode = selectNodeByRequesterAndStrategy(rule, context, node);\n        if (selectedNode == null) {\n            return true;\n        }\n \n        return rule.getRater().canPass(selectedNode, acquireCount, prioritized);\n    }\n```\n当 QPS 超过某个阈值的时候，则采取措施进行流量控制。流量控制的手段包括以下几种：直接拒绝、Warm Up、匀速排队。对应 FlowRule 中的 controlBehavior 字段。\n\n直接拒绝（RuleConstant.CONTROL_BEHAVIOR_DEFAULT）方式是默认的流量控制方式，当QPS超过任意规则的阈值后，新的请求就会被立即拒绝，拒绝方式为抛出FlowException。这种方式适用于对系统处理能力确切已知的情况下，比如通过压测确定了系统的准确水位时。具体的例子参见 FlowQpsDemo。\n\nWarm Up（RuleConstant.CONTROL_BEHAVIOR_WARM_UP）方式，即预热/冷启动方式。当系统长期处于低水位的情况下，当流量突然增加时，直接把系统拉升到高水位可能瞬间把系统压垮。通过\"冷启动\"，让通过的流量缓慢增加，在一定时间内逐渐增加到阈值上限，给冷系统一个预热的时间，避免冷系统被压垮。详细文档可以参考 流量控制 - Warm Up 文档\n\n目前 Sentinel 对于流量控制提供了如下几种方式：\n\n- 直接拒绝（DefaultController）：支持抛出异常\n```java\n    @Override\n    public boolean canPass(Node node, int acquireCount, boolean prioritized) {\n        int curCount = avgUsedTokens(node);\n        if (curCount + acquireCount > count) {\n            if (prioritized && grade == RuleConstant.FLOW_GRADE_QPS) {\n                long currentTime;\n                long waitInMs;\n                currentTime = TimeUtil.currentTimeMillis();\n                waitInMs = node.tryOccupyNext(currentTime, acquireCount, count);\n                if (waitInMs < OccupyTimeoutProperty.getOccupyTimeout()) {\n                    node.addWaitingRequest(currentTime + waitInMs, acquireCount);\n                    node.addOccupiedPass(acquireCount);\n                    sleep(waitInMs);\n \n                    // PriorityWaitException indicates that the request will pass after waiting for {@link @waitInMs}.\n                    throw new PriorityWaitException(waitInMs);\n                }\n            }\n            return false;\n        }\n        return true;\n    }\n```\n- 匀速排队（RateLimiterController）：判断等待时间，如果等待时间过长也是会限流，并且使用 Thread.sleep 如果配置不正确可能会导致线程过多。\n```java\n@Override\n    public boolean canPass(Node node, int acquireCount, boolean prioritized) {\n        // Pass when acquire count is less or equal than 0.\n        if (acquireCount <= 0) {\n            return true;\n        }\n        // Reject when count is less or equal than 0.\n        // Otherwise,the costTime will be max of long and waitTime will overflow in some cases.\n        if (count <= 0) {\n            return false;\n        }\n \n        long currentTime = TimeUtil.currentTimeMillis();\n        // Calculate the interval between every two requests.\n        long costTime = Math.round(1.0 * (acquireCount) / count * 1000);\n \n        // Expected pass time of this request.\n        long expectedTime = costTime + latestPassedTime.get();\n \n        if (expectedTime <= currentTime) {\n            // Contention may exist here, but it's okay.\n            latestPassedTime.set(currentTime);\n            return true;\n        } else {\n            // Calculate the time to wait.\n            long waitTime = costTime + latestPassedTime.get() - TimeUtil.currentTimeMillis();\n            if (waitTime > maxQueueingTimeMs) {\n                return false;\n            } else {\n                long oldTime = latestPassedTime.addAndGet(costTime);\n                try {\n                    waitTime = oldTime - TimeUtil.currentTimeMillis();\n                    if (waitTime > maxQueueingTimeMs) {\n                        latestPassedTime.addAndGet(-costTime);\n                        return false;\n                    }\n                    // in race condition waitTime may <= 0\n                    if (waitTime > 0) {\n                        Thread.sleep(waitTime);\n                    }\n                    return true;\n                } catch (InterruptedException e) {\n                }\n            }\n        }\n        return false;\n    }\n```\n- Warm Up（WarmUpController 和 WarmUpRateLimiterController）：预热启动\n```java\n    @Override\n    public boolean canPass(Node node, int acquireCount, boolean prioritized) {\n        long passQps = (long) node.passQps();\n \n        long previousQps = (long) node.previousPassQps();\n        syncToken(previousQps);\n \n        // 开始计算它的斜率\n        // 如果进入了警戒线，开始调整他的qps\n        long restToken = storedTokens.get();\n        if (restToken >= warningToken) {\n            long aboveToken = restToken - warningToken;\n            // 消耗的速度要比warning快，但是要比慢\n            // current interval = restToken*slope+1/count\n            double warningQps = Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count));\n            if (passQps + acquireCount <= warningQps) {\n                return true;\n            }\n        } else {\n            if (passQps + acquireCount <= count) {\n                return true;\n            }\n        }\n \n        return false;\n    }\n```\n# 参考文章\n- https://blog.51cto.com/zero01/5367363\n- https://juejin.cn/post/6986887722283565069\n- https://blog.csdn.net/qq924862077/article/details/97423682"
  },
  {
    "path": "TODO.md",
    "content": "# TODO list\n\n- 面试题：三个线程交替打印ABC\n- clickhouse\n- java stream流的原理"
  },
  {
    "path": "分布式相关/ApacheSeata.md",
    "content": "* [Apache Seata](#apache-seata)\n  * [Seata 是什么?](#seata-是什么)\n  * [AT 模式](#at-模式)\n  * [TCC 模式](#tcc-模式)\n  * [Saga 模式](#saga-模式)\n  * [XA 模式](#xa-模式)\n* [链接](#链接)\n\n\n# Apache Seata\n\n## Seata 是什么?\n\nSeata 是一款开源的分布式事务解决方案，致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA\n事务模式，为\n用户打造一站式的分布式解决方案。\n\n在 Seata 的架构中，一共有三个角色：\n\n![seata架构.png](..%2Fimg%2F%E5%88%86%E5%B8%83%E5%BC%8F%E7%9B%B8%E5%85%B3%2Fseata%2Fseata%E6%9E%B6%E6%9E%84.png)\n\n- TC (Transaction Coordinator) - 事务协调者：维护全局和分支事务的状态，驱动全局事务提交或回滚。\n- TM (Transaction Manager) - 事务管理器：定义全局事务的范围，开始全局事务、提交或回滚全局事务。\n- RM ( Resource Manager ) - 资源管理器：管理分支事务处理的资源( Resource )，与 TC 交谈以注册分支事务和报告分支事务的状态，并驱动分支事务提交或回滚。\n\n其中，TC 为单独部署的 Server 服务端，TM 和 RM 为嵌入到应用中的 Client 客户端。\n\n在 Seata 中，一个分布式事务的生命周期如下：\n\n![seata生命周期.png](..%2Fimg%2F%E5%88%86%E5%B8%83%E5%BC%8F%E7%9B%B8%E5%85%B3%2Fseata%2Fseata%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F.png)\n\n- TM 请求 TC 开启一个全局事务。TC 会生成一个 XID 作为该全局事务的编号。\n    - XID，会在微服务的调用链路中传播，保证将多个微服务的子事务关联在一起。\n- RM 请求 TC 将本地事务注册为全局事务的分支事务，通过全局事务的 XID 进行关联。\n- TM 请求 TC 告诉 XID 对应的全局事务是进行提交还是回滚。\n- TC 驱动 RM 们将 XID 对应的自己的本地事务进行提交还是回滚。\n\n## AT 模式\n\n### 前提\n\n基于支持本地 ACID 事务的关系型数据库。\n\nJava 应用，通过 JDBC 访问数据库。\n\n### 整体机制\n\n两阶段提交协议的演变：\n\n- 一阶段：业务数据和回滚日志记录在同一个本地事务中提交，释放本地锁和连接资源。\n\n- 二阶段：\n    - 提交异步化，非常快速地完成。\n    - 回滚通过一阶段的回滚日志进行反向补偿。\n\n### 写隔离\n\n- 一阶段本地事务提交前，需要确保先拿到 全局锁 。\n- 拿不到 全局锁 ，不能提交本地事务。\n- 拿 全局锁 的尝试被限制在一定范围内，超出范围将放弃，并回滚本地事务，释放本地锁。\n\n以一个示例来说明：\n\n两个全局事务 tx1 和 tx2，分别对 a 表的 m 字段进行更新操作，m 的初始值 1000。\n\ntx1 先开始，开启本地事务，拿到本地锁，更新操作 m = 1000 - 100 = 900。本地事务提交前，先拿到该记录的 全局锁 ，本地提交释放本地锁。\ntx2 后开始，开启本地事务，拿到本地锁，更新操作 m = 900 - 100 = 800。本地事务提交前，尝试拿该记录的 全局锁 ，tx1 全局提交前，该记录的全局锁被\ntx1 持有，tx2 需要重试等待 全局锁 。\n\n![at模式事物举例1.png](..%2Fimg%2F%E5%88%86%E5%B8%83%E5%BC%8F%E7%9B%B8%E5%85%B3%2Fseata%2Fat%E6%A8%A1%E5%BC%8F%E4%BA%8B%E7%89%A9%E4%B8%BE%E4%BE%8B1.png)\n\ntx1 二阶段全局提交，释放 全局锁 。tx2 拿到 全局锁 提交本地事务。\n\n![at模式事物举例2.png](..%2Fimg%2F%E5%88%86%E5%B8%83%E5%BC%8F%E7%9B%B8%E5%85%B3%2Fseata%2Fat%E6%A8%A1%E5%BC%8F%E4%BA%8B%E7%89%A9%E4%B8%BE%E4%BE%8B2.png)\n\n如果 tx1 的二阶段全局回滚，则 tx1 需要重新获取该数据的本地锁，进行反向补偿的更新操作，实现分支的回滚。\n\n此时，如果 tx2 仍在等待该数据的 全局锁，同时持有本地锁，则 tx1 的分支回滚会失败。分支的回滚会一直重试，直到 tx2 的 全局锁\n等锁超时，放弃 全局锁 并回滚本地事务释放本地锁，tx1 的分支回滚最终成功。\n\n因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的，所以不会发生 脏写 的问题。\n\n### 读隔离\n\n在数据库本地事务隔离级别 读已提交（Read Committed） 或以上的基础上，Seata（AT 模式）的默认全局隔离级别是 读未提交（Read\nUncommitted） 。\n\n如果应用在特定场景下，必需要求全局的 读已提交 ，目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。\n\n![at模式事物举例3.png](..%2Fimg%2F%E5%88%86%E5%B8%83%E5%BC%8F%E7%9B%B8%E5%85%B3%2Fseata%2Fat%E6%A8%A1%E5%BC%8F%E4%BA%8B%E7%89%A9%E4%B8%BE%E4%BE%8B3.png)\n\nSELECT FOR UPDATE 语句的执行会申请 全局锁 ，如果 全局锁 被其他事务持有，则释放本地锁（回滚 SELECT FOR UPDATE\n语句的本地执行）并重试。这个过程中，查询是被 block 住的，直到 全局锁 拿到，即读取的相关数据是 已提交 的，才返回。\n\n出于总体性能上的考虑，Seata 目前的方案并没有对所有 SELECT 语句都进行代理，仅针对 FOR UPDATE 的 SELECT 语句。\n\n### 工作机制\n\n以一个示例来说明整个 AT 分支的工作过程。\n\n业务表：product\n\n| Field | Type         | Key |\n|-------|--------------|-----|\n| id    | bigint(20)   | PRI \n| name  | varchar(100) |\n| since | varchar(100) |\n\nAT 分支事务的业务逻辑：\n\n```sql\nupdate product\nset name = 'GTS'\nwhere name = 'TXC';\n```\n\n#### 一阶段\n\n过程：\n\n- 解析 SQL：得到 SQL 的类型（UPDATE），表（product），条件（where name = 'TXC'）等相关的信息。\n- 查询前镜像：根据解析得到的条件信息，生成查询语句，定位数据。\n\n  ```sql\n  select id, name, since\n  from product\n  where name = 'TXC';\n  ```\n  \n  得到前镜像：\n  \n  | id | name | since |\n  |----|------|-------|\n  | 1  | TXC  | 2014  |\n\n- 执行业务 SQL：更新这条记录的 name 为 'GTS'。\n- 查询后镜像：根据前镜像的结果，通过 主键 定位数据。\n\n  ```sql\n  select id, name, since\n  from product\n  where id = 1;\n  ```\n\n  得到后镜像：\n  \n  | id | name | since |\n  |----|------|-------|\n  | 1  | GTS  | 2014  |\n\n- 插入回滚日志：把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录，插入到 UNDO_LOG 表中。\n  \n  ```json\n  \n  \n  {\n    \"branchId\": 641789253,\n    \"undoItems\": [\n      {\n        \"afterImage\": {\n          \"rows\": [\n            {\n              \"fields\": [\n                {\n                  \"name\": \"id\",\n                  \"type\": 4,\n                  \"value\": 1\n                },\n                {\n                  \"name\": \"name\",\n                  \"type\": 12,\n                  \"value\": \"GTS\"\n                },\n                {\n                  \"name\": \"since\",\n                  \"type\": 12,\n                  \"value\": \"2014\"\n                }\n              ]\n            }\n          ],\n          \"tableName\": \"product\"\n        },\n        \"beforeImage\": {\n          \"rows\": [\n            {\n              \"fields\": [\n                {\n                  \"name\": \"id\",\n                  \"type\": 4,\n                  \"value\": 1\n                },\n                {\n                  \"name\": \"name\",\n                  \"type\": 12,\n                  \"value\": \"TXC\"\n                },\n                {\n                  \"name\": \"since\",\n                  \"type\": 12,\n                  \"value\": \"2014\"\n                }\n              ]\n            }\n          ],\n          \"tableName\": \"product\"\n        },\n        \"sqlType\": \"UPDATE\"\n      }\n    ],\n    \"xid\": \"xid:xxx\"\n  }\n  ```\n\n- 提交前，向 TC 注册分支：申请 product 表中，主键值等于 1 的记录的 全局锁 。\n- 本地事务提交：业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交。\n- 将本地事务提交的结果上报给 TC。\n\n#### 二阶段-回滚\n\n- 收到 TC 的分支回滚请求，开启一个本地事务，执行如下操作。\n- 通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。\n- 数据校验：拿 UNDO LOG 中的后镜与当前数据进行比较，如果有不同，说明数据被当前全局事务之外的动作做了修改。这种情况，需要根据配置策略来做处理，详细的说明在另外的文档中介绍。\n- 根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句：\n\n  ```sql\n  update product\n  set name = 'TXC'\n  where id = 1;\n  ```\n\n- 提交本地事务。并把本地事务的执行结果（即分支事务回滚的结果）上报给 TC。\n\n#### 二阶段-提交\n\n- 收到 TC 的分支提交请求，把请求放入一个异步任务的队列中，马上返回提交成功的结果给 TC。\n- 异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录。\n\n## TCC 模式\n![tcc.png](..%2Fimg%2F%E5%88%86%E5%B8%83%E5%BC%8F%E7%9B%B8%E5%85%B3%2Fseata%2Ftcc.png)\n\nTCC（Try-Confirm-Cancel） 实际上是服务化的两阶段提交协议，业务开发者需要实现这三个服务接口，第一阶段服务由业务代码编排来调用 Try 接口进行资源预留，所有参与者的 Try 接口都成功了，事务管理器会提交事务，并调用每个参与者的 Confirm 接口真正提交业务操作，否则调用每个参与者的 Cancel 接口回滚事务。\n\n## Saga 模式\n\nSaga模式是SEATA提供的长事务解决方案，在Saga模式中，业务流程中每个参与者都提交本地事务，当出现某一个参与者失败则补偿前面已经成功的参与者，一阶段正向服务和二阶段补偿服务都由业务开发实现。\n\n![saga.png](..%2Fimg%2F%E5%88%86%E5%B8%83%E5%BC%8F%E7%9B%B8%E5%85%B3%2Fseata%2Fsaga.png)\n\n理论基础：Hector & Kenneth 发表论⽂ Sagas （1987）\n\n### 适用场景：\n- 业务流程长、业务流程多\n- 参与者包含其它公司或遗留系统服务，无法提供 TCC 模式要求的三个接口\n### 优势：\n- 一阶段提交本地事务，无锁，高性能\n- 事件驱动架构，参与者可异步执行，高吞吐\n- 补偿服务易于实现\n### 缺点：\n- 不保证隔离性\n\n## XA 模式\n\n### 前提\n- 支持XA 事务的数据库。\n- Java 应用，通过 JDBC 访问数据库。\n\n### 整体机制\n在 Seata 定义的分布式事务框架内，利用事务资源（数据库、消息服务等）对 XA 协议的支持，以 XA 协议的机制来管理分支事务的一种 事务模式。\n\n![xa.png](..%2Fimg%2F%E5%88%86%E5%B8%83%E5%BC%8F%E7%9B%B8%E5%85%B3%2Fseata%2Fxa.png)\n\n执行阶段：\n\n- 可回滚：业务 SQL 操作放在 XA 分支中进行，由资源对 XA 协议的支持来保证 可回滚\n- 持久化：XA 分支完成后，执行 XA prepare，同样，由资源对 XA 协议的支持来保证 持久化（即，之后任何意外都不会造成无法回滚的情况）\n\n完成阶段：\n\n- 分支提交：执行 XA 分支的 commit\n- 分支回滚：执行 XA 分支的 rollback\n\n### 工作机制\n#### 1. 整体运行机制\n   XA 模式 运行在 Seata 定义的事务框架内：\n\n![xa工作机制.png](..%2Fimg%2F%E5%88%86%E5%B8%83%E5%BC%8F%E7%9B%B8%E5%85%B3%2Fseata%2Fxa%E5%B7%A5%E4%BD%9C%E6%9C%BA%E5%88%B6.png)\n\n执行阶段（E xecute）：\n  - XA start/XA end/XA prepare + SQL + 注册分支\n\n\n完成阶段（F inish）：\n  - XA commit/XA rollback\n\n\n#### 2. 数据源代理\n\nXA 模式需要 XAConnection。\n\n获取 XAConnection 两种方式：\n\n- 方式一：要求开发者配置 XADataSource\n- 方式二：根据开发者的普通 DataSource 来创建\n\n第一种方式，给开发者增加了认知负担，需要为 XA 模式专门去学习和使用 XA 数据源，与 透明化 XA 编程模型的设计目标相违背。\n\n第二种方式，对开发者比较友好，和 AT 模式使用一样，开发者完全不必关心 XA 层面的任何问题，保持本地编程模型即可。\n\n我们优先设计实现第二种方式：数据源代理根据普通数据源中获取的普通 JDBC 连接创建出相应的 XAConnection。\n\n类比 AT 模式的数据源代理机制，如下：\n\n![xa数据源代理.png](..%2Fimg%2F%E5%88%86%E5%B8%83%E5%BC%8F%E7%9B%B8%E5%85%B3%2Fseata%2Fxa%E6%95%B0%E6%8D%AE%E6%BA%90%E4%BB%A3%E7%90%86.png)\n\n但是，第二种方法有局限：无法保证兼容的正确性。\n\n实际上，这种方法是在做数据库驱动程序要做的事情。不同的厂商、不同版本的数据库驱动实现机制是厂商私有的，我们只能保证在充分测试过的驱动程序上是正确的，开发者使用的驱动程序版本差异很可能造成机制的失效。\n\n这点在 Oracle 上体现非常明显。参见 Druid issue：https://github.com/alibaba/druid/issues/3707\n\n综合考虑，XA 模式的数据源代理设计需要同时支持第一种方式：基于 XA 数据源进行代理。\n\n类比 AT 模式的数据源代理机制，如下：\n\n![xa数据源代理2.png](..%2Fimg%2F%E5%88%86%E5%B8%83%E5%BC%8F%E7%9B%B8%E5%85%B3%2Fseata%2Fxa%E6%95%B0%E6%8D%AE%E6%BA%90%E4%BB%A3%E7%90%862.png)\n\n#### 3. 分支注册\n   XA start 需要 Xid 参数。\n\n这个 Xid 需要和 Seata 全局事务的 XID 和 BranchId 关联起来，以便由 TC 驱动 XA 分支的提交或回滚。\n\n目前 Seata 的 BranchId 是在分支注册过程，由 TC 统一生成的，所以 XA 模式分支注册的时机需要在 XA start 之前。\n\n将来一个可能的优化方向：\n\n把分支注册尽量延后。类似 AT 模式在本地事务提交之前才注册分支，避免分支执行失败情况下，没有意义的分支注册。\n\n这个优化方向需要 BranchId 生成机制的变化来配合。BranchId 不通过分支注册过程生成，而是生成后再带着 BranchId 去注册分支。\n\n### XA 模式的使用\n从编程模型上，XA 模式与 AT 模式保持完全一致。\n\n可以参考 Seata 官网的样例：[seata-xa](https://github.com/apache/incubator-seata-samples/tree/master/xa-sample/springboot-feign-seata-xa)\n\n样例场景是 Seata 经典的，涉及库存、订单、账户 3 个微服务的商品订购业务。\n\n在样例中，上层编程模型与 AT 模式完全相同。只需要修改数据源代理，即可实现 XA 模式与 AT 模式之间的切换。\n\n```java\n    @Bean(\"dataSource\")\n    public DataSource dataSource(DruidDataSource druidDataSource) {\n        // DataSourceProxy for AT mode\n        // return new DataSourceProxy(druidDataSource);\n\n        // DataSourceProxyXA for XA mode\n        return new DataSourceProxyXA(druidDataSource);\n    }\n```\n\n\n# 链接\n\n- https://seata.apache.org/zh-cn/docs/overview/what-is-seata\n- https://seata.apache.org/zh-cn/blog/seata-quick-start/\n- https://seata.apache.org/zh-cn/blog/seata-at-tcc-saga/"
  },
  {
    "path": "分布式相关/BASE.md",
    "content": "\n* [BASE](#base)\n  * [基本可以  Basically Available](#基本可以--basically-available)\n  * [软状态  Soft-state](#软状态--soft-state)\n  * [最终一致性  Eventually Consistent](#最终一致性--eventually-consistent)\n\n\n# BASE\nBASE 理论是对 CAP 中一致性 C 和可用性 A 权衡的结果，其来源于对大规模互联网系统分布式实践的总结，是基于 CAP 定理逐步演化而来的，它大大降低了我们对系统的要求\n\n### 基本可以  Basically Available\n- 基本可用是指分布式系统在出现不可预知故障的时候，允许损失部分可用性。但是，这绝不等价于系统不可用\n- 响应时间上的损失: 正常情况下，处理用户请求需要 0.5s 返回结果，但是由于系统出现故障，处理用户请求的时间变为 3 s。\n- 系统功能上的损失：正常情况下，用户可以使用系统的全部功能，但是由于系统访问量突然剧增，系统的部分非核心功能无法使用\n### 软状态  Soft-state\n软状态指允许系统中的数据存在中间状态（CAP 理论中的数据不一致），并认为该中间状态的存在不会影响系统的整体可用性，即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。\n### 最终一致性  Eventually Consistent\n最终一致性强调的是系统中所有的数据副本，在经过一段时间的同步后，最终能够达到一个一致的状态。因此，最终一致性的本质是需要系统保证最终数据能够达到一致，而不需要实时保证系统数据的强一致性"
  },
  {
    "path": "分布式相关/CAP.md",
    "content": "\n* [CAP理论](#cap理论)\n  * [一致性 Consistency](#一致性-consistency)\n  * [可用性 Availability](#可用性-availability)\n  * [分区容错性 Partition Tolerance](#分区容错性-partition-tolerance)\n  * [常见的注册中心](#常见的注册中心)\n\n# CAP理论\n图\n![](../img/分布式相关/cap.png)\n\nCAP 定理（CAP theorem）指出对于一个分布式系统来说，当设计读写操作时，只能能同时满足以下三点中的两个\n- CAP 理论中分区容错性 P 是一定要满足的，在此基础上，只能满足可用性 A 或者一致性 C\n\n### 一致性 Consistency\n\nConsistency，一致性的意思。\n\n一致性就是说，我们读写数据必须是一摸一样的。\n\n比如一条数据，分别存在两个服务器中，server1和server2。\n\n我们此时将数据a通过server1修改为数据b。此时如果我们访问server1访问的应该是b。\n\n当我们访问server2的时候，如果返回的还是未修改的a，那么则不符合一致性，如果返回的是b，则符合数据的一致性。\n### 可用性 Availability\nAvailability，可用性的意思。\n\n这个比较好理解，就是说，只要我对服务器，发送请求，服务器必须对我进行响应，保证服务器一直是可用的\n### 分区容错性 Partition Tolerance\nPartition tolerance，分区容错的意思。\n\n一般来说，分布式系统是分布在多个位置的。比如我们的一台服务器在北京，一台在上海。可能由于天气等原因的影响。造成了两条服务器直接不能互相通信，数据不能进行同步。这就是分区容错。我们认为，分区容错是不可避免的。也就是说 P 是必然存在的。\n\n网络时区造成的分区情况\n\n## 常见的注册中心\n| |Nacos|Eureka|Consul|CoreDNS|Zookeeper|\n|---|---|---|---|---|---|\n|一致性协议|\tCP+AP|\tAP|\tCP|\t— |\tCP|\n|健康检查|\tTCP/HTTP/MYSQL/Client Beat|\tClient Beat|\tTCP/HTTP/gRPC/Cmd|\t—\t|Keep Alive|\n|负载均衡策略|\t权重/metadata|/Selector|\tRibbon|\tFabio|\tRoundRobin|\t— |\n|雪崩保护|\t|有\t|有\t|无\t|无\t|无|\n|自动注销实例|\t|支持\t|支持\t|支持\t|不支持\t|支持|\n|访问协议|\tHTTP/DNS|\tHTTP|\tHTTP/DNS|\tDNS\t|TCP|\n|监听支持|\t支持|\t支持|\t支持\t|不支持|\t支持|\n|多数据中心|\t支持|\t支持\t|支持\t|不支持|不支持|\n|跨注册中心同步|\t支持|\t不支持|\t支持|\t不支持|\t不支持|\n|SpringCloud集成|\t支持|\t支持|\t支持|\t不支持|\t支持|\n|Dubbo集成|\t支持|\t不支持|\t支持|\t不支持|\t支持|\n|K8S集成|\t支持|\t不支持|\t支持|\t支持|\t不支持|\n"
  },
  {
    "path": "分布式相关/一致性算法.md",
    "content": "\n* [一致性算法](#一致性算法)\n    * [Paxos](#paxos)\n    * [Raft](#raft)\n\n\n# 一致性算法\n## Paxos\n## Raft\n- 对Paxos的简化实现版本\n\nRaft是一个用户管理日志一致性的协议，它将分布式一致性问题分解为多个子问题：Leader选举、日志复制、安全性、日志压缩等。Raft将系统中的角色分为领导者（Leader）、跟从者（Follower）和候选者（Candidate）\n- Leader（领导者-日志管理） 负责日志的同步管理，处理来自客户端的请求，与 Follower 保持这 heartBeat 的联系；\n- Follower（追随者-日志同步） 刚启动时所有节点为Follower状态，响应Leader的日志同步请求，响应Candidate的请求， 把请求到 Follower 的事务转发给 Leader；\n- Candidate（候选者-负责选票） 负责选举投票，Raft 刚启动时由一个节点从 Follower 转为 Candidate 发起选举，选举出Leader 后从 Candidate 转为 Leader 状态；\n\nTerm（任期）\n- 在 Raft 中使用了一个可以理解为周期（第几届、任期）的概念，用 Term 作为一个周期，每个 Term 都是一个连续递增的编号，每一轮选举都是一个 Term 周期，在一个 Term 中只能产生一个 Leader；当某节点收到的请求中 Term 比当前 Term 小时则拒绝该请求\n\nRPC\n- Raft算法中服务器节点之间通信使用远程过程调用（RPC），并且基本的一致性算法只需要两种类型的RPC，为了在服务器之间传输快照增加了第三种 RPC。\n  - RequestVote RPC：候选人在选举期间发起。\n  - AppendEntries RPC：领导人发起的一种心跳机制，复制日志也在该命令中完成。\n  - InstallSnapshot RPC：领导者使用该RPC来发送快照给太落后的追随者。\n\n选举（Election）\n\nRaft 的选举由定时器来触发，每个节点的选举定时器时间都是不一样的，开始时状态都为 Follower 某个节点定时器触发选举后 Term 递增，状态由 Follower 转为 Candidate，向其他节点 发起 RequestVote RPC 请求，这时候有三种可能的情况发生\n- 1：该 RequestVote 请求接收到 n/2+1（过半数）个节点的投票，从 Candidate 转为 Leader， 向其他节点发送 heartBeat 以保持 Leader 的正常运转。\n  - 赢得了多数的选票，成功选举为Leader\n- 2：在此期间如果收到其他节点发送过来的 AppendEntries RPC 请求，如该节点的 Term 大 则当前节点转为 Follower，否则保持 Candidate 拒绝该请求。\n  - 收到了Leader的消息，表示有其它服务器已经抢先当选了Leader\n- 3：Election timeout 发生则 Term 递增，重新发起选举\n  - 没有服务器赢得多数的选票，Leader选举失败，等待选举时间超时后发起下一次选举\n\n在一个 Term 期间每个节点只能投票一次，所以当有多个 Candidate 存在时就会出现每个 Candidate 发起的选举都存在接收到的投票数都不过半的问题，这时每个 Candidate 都将 Term 递增、重启定时器并重新发起选举，由于每个节点中定时器的时间都是随机的，所以就不会多次 存在有多个 Candidate 同时发起投票的问题。\n- 如果不设置定时器，或者定时器各节点都是一样的就会出现同时发起选举，但都未得到选票过半，然后选举失败后又重新选举，循环\n\n日志同步\n- 在 Raft 中leader当接收到客户端的日志（事务请求）后先把该日志追加到本地的 Log 中，然后通过 heartbeat 把该 Entry 同步给其他 Follower，Follower 接收到日志后记录日志然后向 Leader 发送 ACK，当 Leader 收到大多数（n/2+1）Follower 的 ACK 信息后将该日志设置为已提交并追加到 本地磁盘中，通知客户端并在下个 heartbeat 中 Leader 将通知所有的 Follower 将该日志存储在 自己的本地磁盘中。\n\n安全性（Safety）\n- 拥有最新的已提交的log entry的Follower才有资格成为Leader\n  - 这个保证是在RequestVote RPC中做的，Candidate在发送RequestVote RPC时，要带上自己的最后一条日志的term和log index，其他节点收到消息时，如果发现自己的日志比请求中携带的更新，则拒绝投票。日志比较的原则是，如果本地的最后一条log entry的term更大，则term大的更新，如果term一样大，则log index更大的更新。\n- Leader只能推进commit index来提交当前term的已经复制到大多数服务器上的日志，旧term日志的提交要等到提交当前term的日志来间接提交（log index 小于 commit index的日志被间接提交）。\n"
  },
  {
    "path": "分布式相关/分布式ID.md",
    "content": "* [分布式唯一ID设计](#分布式唯一id设计)\n  * [UUID](#uuid)\n  * [多台MySQL服务器](#多台mysql服务器)\n  * [Twitter Snowflake](#twitter-snowflake)\n  * [百度UidGenerator算法](#百度uidgenerator算法)\n  * [美团Leaf算法](#美团leaf算法)\n    * [Leaf-segment数据库方案 号段模式](#leaf-segment数据库方案-号段模式)\n    * [Leaf-snowflake方案](#leaf-snowflake方案)\n\n\n## 分布式唯一ID设计\n### UUID\n缺点很明显，太长且无序\n### 多台MySQL服务器\n- 既然MySQL可以产生自增ID，那么用多台MySQL服务器，能否组成一个高性能的分布式发号器呢？ 显然可以。\n- 假设用8台MySQL服务器协同工作，第一台MySQL初始值是1，每次自增8，第二台MySQL初始值是2，每次自增8，依次类推。前面用一个 round-robin load balancer 挡着，每来一个请求，由 round-robin balancer 随机地将请求发给8台MySQL中的任意一个，然后返回一个ID\n- 这个方法跟单台数据库比，缺点是ID不是严格递增的，只是粗略递增的。不过这个问题不大，我们的目标是粗略有序，不需要严格递增。\n### Twitter Snowflake\n核心思想是：采用bigint（64bit）作为id生成类型，并将所占的64bit 划分成多段。\n- ①1位标识：由于long基本类型在Java中是带符号的，最高位是符号位，正数是0，负数是1，所以id一般是正数，最高位是0。\n- ②41位时间截(毫秒级）：需要注意的是，41位时间截不是存储当前时间的时间截，而是存储时间截的差值（当前时间截 - 开始时间截）得到的值，这里的开始时间截，一般是指我们的id生成器开始使用的时间截，由我们的程序来指定。41位的毫秒时间截，可以使用69年（即T =（1L << 41）/（1000 60 60 24 365）= 69）。\n- ③10位的数据机器位：包括5位数据中心标识Id（datacenterId）、5位机器标识Id(workerId)，最多可以部署1024个节点（即1 << 10 = 1024）。超过这个数量，生成的ID就有可能会冲突。\n- ④12位序列：毫秒内的计数，12位的计数顺序号支持每个节点每毫秒（同一机器，同一时间截）产生4096个ID序号（即1 << 12 = 4096）。\n  PS：全部结构标识（1+41+10+12=64）加起来刚好64位，刚好凑成一个Long型。\n### 百度UidGenerator算法\nuid-generator使用的就是snowflake，只是在生产机器id，也叫做workId时有所不同。\n\nuid-generator中的workId是由uid-generator自动生成的，并且考虑到了应用部署在docker上的情况，在uid-generator中用户可以自己去定义workId的生成策略，默认提供的策略是：应用启动时由数据库分配。说的简单一点就是：应用在启动时会往数据库表(uid-generator需要新增一个WORKER_NODE表)中去插入一条数据，数据插入成功后返回的该数据对应的自增唯一id就是该机器的workId，而数据由host，port组成。\n\n对于uid-generator中的workId，占用了22个bit位，时间占用了28个bit位，序列化占用了13个bit位，需要注意的是，和原始的snowflake不太一样，时间的单位是秒，而不是毫秒，workId也不一样，同一个应用每重启一次就会消费一个workId。\n### 美团Leaf算法\n#### Leaf-segment数据库方案 号段模式\n- 分段获取\n- 即当号段消费到某个点时就异步的把下一个号段加载到内存中\n#### Leaf-snowflake方案\nLeaf中的snowflake模式和原始snowflake算法的不同点，也主要在workId的生成，Leaf中workId是基于ZooKeeper的顺序Id来生成的，每个应用在使用Leaf-snowflake时，在启动时都会都在Zookeeper中生成一个顺序Id，相当于一台机器对应一个顺序节点，也就是一个workId。\n"
  },
  {
    "path": "分布式相关/分布式事务.md",
    "content": "\n* [分布式事务](#分布式事务)\n    * [两阶段提交](#两阶段提交)\n        * [原理](#原理)\n        * [大致的流程](#大致的流程)\n        * [问题](#问题)\n    * [TCC（Try-Confirm-Cancel）](#tcctry-confirm-cancel)\n        * [原理](#原理-1)\n            * [Try 阶段](#try-阶段)\n            * [Confirm 阶段](#confirm-阶段)\n            * [Cancel 阶段](#cancel-阶段)\n        * [特点](#特点)\n    * [本地消息表](#本地消息表)\n        * [原理](#原理-2)\n        * [本地消息表实现的条件](#本地消息表实现的条件)\n        * [容错机制](#容错机制)\n    * [可靠消息最终一致性](#可靠消息最终一致性)\n    * [尽最大努力通知](#尽最大努力通知)\n        * [大致流程](#大致流程)\n\n\n# 分布式事务\n## 两阶段提交\n### 原理\n两阶段提交，顾名思义就是要分两步提交。存在一个负责协调各个本地资源管理器的事务管理器，本地资源管理器一般是由数据库实现，事务管理器在第一阶段的时候询问各个资源管理器是否都就绪？如果收到每个资源的回复都是 yes，则在第二阶段提交事务，如果其中任意一个资源的回复是 no, 则回滚事务。\n### 大致的流程\n第一阶段（prepare）：事务管理器向所有本地资源管理器发起请求，询问是否是 ready 状态，所有参与者都将本事务能否成功的信息反馈发给协调者\n\n第二阶段 (commit/rollback)：事务管理器根据所有本地资源管理器的反馈，通知所有本地资源管理器，步调一致地在所有分支上提交或者回滚\n### 问题\n同步阻塞\n- 当参与事务者存在占用公共资源的情况，其中一个占用了资源，其他事务参与者就只能阻塞等待资源释放，处于阻塞状态。\n\n单点问题\n- 一旦事务管理器出现故障，整个系统不可用\n\n数据不一致\n- 在阶段二，如果事务管理器只发送了部分 commit 消息，此时网络发生异常，那么只有部分参与者接收到 commit 消息，也就是说只有部分参与者提交了事务，使得系统数据不一致。\n\n无容错\n- 失败一个节点即失败\n\n不确定性\n- 当事务管理器发送 commit 之后，并且此时只有一个参与者收到了 commit，那么当该参与者与事务管理器同时宕机之后，重新选举的事务管理器无法确定该条消息是否提交成功。\n## TCC（Try-Confirm-Cancel）\n### 原理\n#### Try 阶段\n尝试执行，完成所有业务检查（一致性）, 预留必须业务资源（准隔离性）\n#### Confirm 阶段\n确认执行真正执行业务，不作任何业务检查，只使用 Try 阶段预留的业务资源，Confirm 操作满足幂等性。要求具备幂等设计，Confirm 失败后需要进行重试\n#### Cancel 阶段\n取消执行，释放 Try 阶段预留的业务资源 Cancel 操作满足幂等性 Cancel 阶段的异常和 Confirm 阶段异常处理方案基本上一致\n### 特点\n实现复杂度高\n\n在 Try 阶段，是对业务系统进行检查及资源预览，比如订单和存储操作，需要检查库存剩余数量是否够用，并进行预留，预留操作的话就是新建一个可用库存数量字段，Try 阶段操作是对这个可用库存数量进行操作。\n## 本地消息表\n### 原理\n该方案中会有消息生产者与消费者两个角色，假设系统 A 是消息生产者，系统 B 是消息消费者，其大致流程如下\n\n图\n![](../img/分布式相关/本地消息表.png)\n\n1、当系统 A 被其他系统调用发生数据库表更新操作，首先会更新数据库的业务表，其次会往相同数据库的消息表中插入一条数据，两个操作发生在同一个事务中\n\n2、系统 A 的脚本定期轮询本地消息往 mq 中写入一条消息，如果消息发送失败会进行重试\n\n3、系统 B 消费 mq 中的消息，并处理业务逻辑。如果本地事务处理失败，会在继续消费 mq 中的消息进行重试，如果业务上的失败，可以通知系统 A 进行回滚操作\n\n### 本地消息表实现的条件\n- 消费者与生成者的接口都要支持幂等\n- 生产者需要额外的创建消息表\n- 需要提供补偿逻辑，如果消费者业务失败，需要生产者支持回滚操作\n\n### 容错机制\n- 步骤 1 失败时，事务直接回滚\n- 步骤 2、3 写 mq 与消费 mq 失败会进行重试\n- 步骤 3 业务失败系统 B 向系统 A 发起事务回滚操作\n        \n此方案的核心是将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列，再通过业务规则自动或人工发起重试。人工重试更多的是应用于支付场景，通过对账系统对事后问题的处理。\n## 可靠消息最终一致性\n图\n![](../img/分布式相关/消息最终一致性.png)\t\t\t\n\t\t\n流程\n- A 系统先向 mq 发送一条 prepare 消息，如果 prepare 消息发送失败，则直接取消操作\n- 如果消息发送成功，则执行本地事务\n- 如果本地事务执行成功，则向 mq 发送一条 confirm 消息，如果发送失败，则发送回滚消息\n- B 系统定期消费 mq 中的 confirm 消息，执行本地事务，并发送 ack 消息。如果 B 系统中的本地事务失败，会一直不断重试，如果是业务失败，会向 A 系统发起回滚请求\n- mq 会定期轮询所有 prepared 消息调用系统 A 提供的接口查询消息的处理情况，如果该 prepare 消息本地事务处理成功，则重新发送 confirm 消息，否则直接回滚该消息（回滚A系统的本地事务？）\n        \n该方案与本地消息最大的不同是去掉了本地消息表，其次本地消息表依赖消息表重试写入 mq 这一步由本方案中的轮询 prepare 消息状态来重试或者回滚该消息替代。其实现条件与余容错方案基本一致。目前市面上实现该方案的只有阿里的 RocketMq。\n## 尽最大努力通知\n### 大致流程\n- 系统 A 本地事务执行完之后，发送个消息到 MQ；\n- 这里会有个专门消费 MQ 的服务，这个服务会消费 MQ 并调用系统 B 的接口；\n- 要是系统 B 执行成功就 ok 了；要是系统 B 执行失败了，那么最大努力通知服务就定时尝试重新调用系统 B, 反复 N 次，最后还是不行就放弃。\n        \n最大努力通知最常见的场景就是支付回调，支付服务收到第三方服务支付成功通知后，先更新自己库中订单支付状态，然后同步通知订单服务支付成功。如果此次同步通知失败，会通过异步脚本不断重试地调用订单服务的接口。\n\n# Apache Seata\n\n[ApacheSeata](ApacheSeata.md)"
  },
  {
    "path": "分布式相关/分布式锁.md",
    "content": "\n* [分布式锁](#分布式锁)\n  * [基于数据库](#基于数据库)\n    * [怎么实现](#怎么实现)\n      * [创建一张锁表](#创建一张锁表)\n      * [添加锁](#添加锁)\n      * [释放锁](#释放锁)\n      * [重入锁判断](#重入锁判断)\n      * [加锁以及释放锁的代码示例](#加锁以及释放锁的代码示例)\n      * [完整流程](#完整流程)\n  * [以上代码还存在一些问题:](#以上代码还存在一些问题)\n  * [Redis](#redis)\n    * [使用SETNX 命令](#使用setnx-命令)\n      * [使用redis 的set(String key, String value, String nxxx, String expx, int time)命令](#使用redis-的setstring-key-string-value-string-nxxx-string-expx-int-time命令)\n      * [代码示例](#代码示例)\n      * [完整流程](#完整流程-1)\n      * [Redis 分布式锁真的安全吗？](#redis-分布式锁真的安全吗)\n      * [锁过期时间不好评估怎么办？](#锁过期时间不好评估怎么办)\n      * [那当「主从发生切换」时，这个分布锁会依旧安全吗？](#那当主从发生切换时这个分布锁会依旧安全吗)\n      * [Redlock（红锁）](#redlock红锁)\n      * [Redis 的 Redlock 有什么问题？一定安全吗？](#redis-的-redlock-有什么问题一定安全吗)\n      * [业界争论 Redlock，到底在争论什么？哪种观点是对的？](#业界争论-redlock到底在争论什么哪种观点是对的)\n      * [分布式锁到底用 Redis 还是 Zookeeper？](#分布式锁到底用-redis-还是-zookeeper)\n  * [zookeeper](#zookeeper)\n    * [可以直接使用zookeeper第三方库Curator客户端，这个客户端中封装了一个可重入的锁服务。](#可以直接使用zookeeper第三方库curator客户端这个客户端中封装了一个可重入的锁服务)\n    * [参考文章](#参考文章)\n\n\n# 分布式锁\n\n## 基于数据库\n\n<img src=\"../img/分布式相关/分布式锁-数据库.png\" width=\"50%\" />\n\n基于数据库的分布式锁, 常用的一种方式是使用表的唯一约束特性。当往数据库中成功插入一条数据时, 代表只获取到锁。将这条数据从数据库中删除，则释放锁\n### 怎么实现\n#### 创建一张锁表\n```sql\nCREATE TABLE `methodLock` (\n    `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',\n    `method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁定的方法名',\n    `cust_id` varchar(1024) NOT NULL DEFAULT '客户端唯一编码',\n    `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间，自动生成',\n    PRIMARY KEY (`id`),\n    UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE\n)\nENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';\n\n```\n#### 添加锁\n```sql\ninsert into methodLock(method_name,cust_id) values (‘method_name’,‘cust_id’)\n```\n- 这里cust_id 可以是机器的mac地址+线程编号, 确保一个线程只有唯一的一个编号。通过这个编号， 可以有效的判断是否为锁的创建者，从而进行锁的释放以及重入锁判断\n#### 释放锁\n```sql\ndelete from methodLock where method_name ='method_name' and cust_id = 'cust_id'\n```\n#### 重入锁判断\n```sql\nselect 1 from methodLock where method_name ='method_name' and cust_id = 'cust_id'\n```\n#### 加锁以及释放锁的代码示例\n```java\n/**\n* 获取锁\n  */\npublic boolean lock(String methodName){\n    boolean success = false;\n    //获取客户唯一识别码,例如:mac+线程信息\n    String custId = getCustId();\n    try{\n    //添加锁\n        success = insertLock(methodName, custId);\n    } catch(Exception e) {\n    //如添加失败\n    }\n    return success;\n}\n\n/**\n* 释放锁\n  */\npublic boolean unlock(String methodName) {\n  boolean success = false;\n  //获取客户唯一识别码,例如:mac+线程信息\n  String custId = getCustId();\n  try{\n    //添加锁\n    success = deleteLock(methodName, custId);\n  } catch(Exception e) {\n    //如添加失败\n  }\n  return success;\n}\n```\n#### 完整流程\n```java\n public void test() {\n        String methodName = \"methodName\";\n        //判断是否重入锁\n        if (!checkReentrantLock(methodName)) {\n            //非重入锁\n            while (!lock(methodName)) {\n                //获取锁失败, 则阻塞至获取锁\n                try{\n                    Thread.sleep(100)\n                } catch(Exception e) {\n                }\n            }\n        }\n        //TODO 业务处理\n\n        //释放锁\n        unlock(methodName);\n}   \n```\n- 以上代码还存在一些问题:\n  - \n  - 没有失效时间。 解决方案:设置一个定时处理, 定期清理过期锁\n  - 单点问题。 解决方案: 弄几个备份数据库，数据库之前双向同步，一旦挂掉快速切换到备库上  \n\n## Redis\n<img src=\"../img/分布式相关/分布式锁-redis命令.png\" width=\"50%\" />\n\n### 使用SETNX 命令\n#### 使用redis 的set(String key, String value, String nxxx, String expx, int time)命令\n- 第一个为key，我们使用key来当锁，因为key是唯一的。\n- 第二个为value，我们传的是custId，这里cust_id 可以是机器的mac地址+线程编号, 确保一个线程只有唯一的一个编号。通过这个编号， 可以有效的判断是否为锁的创建者，从而进行锁的释放以及重入锁判断\n- 第三个为nxxx，这个参数我们填的是NX，意思是SET IF NOT EXIST，即当key不存在时，我们进行set操作；若key已经存在，则不做任何操作\n- 第四个为expx，这个参数我们传的是PX，意思是我们要给这个key加一个过期的设置，具体时间由第五个参数决定。\n- 第五个为time，与第四个参数相呼应，代表key的过期时间。\n#### 代码示例\n```java\nprivate static final String LOCK_SUCCESS = \"OK\";\nprivate static final String SET_IF_NOT_EXIST = \"NX\";\nprivate static final String SET_WITH_EXPIRE_TIME = \"PX\";\nprivate static final Long RELEASE_SUCCESS = 1L;\n\n// Redis客户端\nprivate Jedis jedis;\n\n/**\n* 尝试获取分布式锁\n* @param lockKey 锁\n* @param expireTime 超期时间\n* @return 是否获取成功\n  */\npublic boolean lock(String lockKey, int expireTime) {\n      //获取客户唯一识别码,例如:mac+线程信息\n      String custId = getCustId();\n      String result = jedis.set(lockKey, custId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);\n    \n      if (LOCK_SUCCESS.equals(result)) {\n         return true;\n      }\n\n      return false;\n  }\n\n/**\n* 释放分布式锁\n* @param lockKey 锁\n* @param requestId 请求标识\n* @return 是否释放成功\n  */\npublic boolean unlock(String lockKey,) {\n  //获取客户唯一识别码,例如:mac+线程信息\n  String custId = getCustId();\n  String script = \"if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end\";\n  Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(custId));\n\n  if (RELEASE_SUCCESS.equals(result)) {\n     return true;\n  }\n  return false;\n}\n\n/**\n* 获取锁信息\n* @param lockKey 锁\n* @return 是否重入锁\n  */\npublic boolean checkReentrantLock(String lockKey){\n  //获取客户唯一识别码,例如:mac+线程信息\n  String custId = getCustId();\n\n  //获取当前锁的客户唯一表示码\n  String currentCustId = redis.get(lockKey);\n  if (custId.equals(currentCustId)) {\n      return true;\n  }\n  return false;\n}\n```\n#### 完整流程\n```java\npublic void test() {\n  String lockKey = \"lockKey\";\n  //判断是否重入锁\n  if (!checkReentrantLock(lockKey)) {\n      //非重入锁\n      while (!lock(lockKey)) {\n          //获取锁失败, 则阻塞至获取锁\n          try{\n                Thread.sleep(100)\n          } catch(Exception e) {\n          }\n      }\n  }\n  //TODO 业务处理\n\n  //释放锁\n  unlock(lockKey);\n}\n```\n#### Redis 分布式锁真的安全吗？\n- set命令的问题（加锁和解锁非原子操作）\n  - 锁过期释放别人的锁：a拿到锁操作资源，但是超时了锁自动释放，b新拿到锁开始操作，这时候a处理完了释放锁，但是此时释放是b的锁\n  - 解决方案：1、增大预估锁超时时间，2、在加锁时设置一个唯一标识进去，并且解锁必须原子执行-lua脚本\n- 严谨的流程\n  - 加锁：SET lock_key $unique_id EX $expire_time NX\n  - 操作共享资源\n  - 释放锁：Lua 脚本，先 GET 判断锁是否归属自己，再 DEL 释放锁\n#### 锁过期时间不好评估怎么办？\n- 锁的过期时间如果评估不好，这个锁就会有「提前」过期的风险\n- `方案`：加锁时，先设置一个过期时间，然后我们开启一个「守护线程」，定时去检测这个锁的失效时间，如果锁快要过期了，操作共享资源还未完成，那么就自动对锁进行「续期」，重新设置过期时间。\n- Java使用 `Redisson` 在使用分布式锁时，它就采用了「自动续期」的方案来避免锁过期，这个守护线程我们一般也把它叫做「看门狗」线程\n#### 那当「主从发生切换」时，这个分布锁会依旧安全吗？\n- 我们在使用 Redis 时，一般会采用主从集群 + 哨兵的模式部署，这样做的好处在于，当主库异常宕机时，哨兵可以实现「故障自动切换」，把从库提升为主库，继续提供服务，以此保证可用性。\n- `场景`\n  - 客户端 1 在主库上执行 SET 命令，加锁成功\n  - 此时，主库异常宕机，SET 命令还未同步到从库上（主从复制是异步的）\n  - 从库被哨兵提升为新主库，这个锁在新的主库上，丢失了！\n- 解决方案\n  - Redlock（红锁）\n#### Redlock（红锁）\nRedlock 的方案基于 2 个前提：\n- 不再需要部署从库和哨兵实例，只部署主库\n- 但主库要部署多个，官方推荐至少 5 个实例\n\n也就是说，想用使用 Redlock，你至少要部署 5 个 Redis 实例，而且都是主库，它们之间没有任何关系，都是一个个孤立的实例。\n> 注意：不是部署 Redis Cluster，就是部署 5 个简单的 Redis 实例。\n\nRedlock 具体如何使用呢？整体的流程是这样的，一共分为 5 步：\n\n1. 客户端先获取「当前时间戳T1」\n2. 客户端依次向这 5 个 Redis 实例发起加锁请求（用前面讲到的 SET 命令），且每个请求会设置超时时间（毫秒级，要远小于锁的有效时间），如果某一个实例加锁失败（包括网络超时、锁被其它人持有等各种异常情况），就立即向下一个 Redis 实例申请加锁\n3. 如果客户端从 >=3 个（大多数）以上 Redis 实例加锁成功，则再次获取「当前时间戳T2」，如果 T2 - T1 < 锁的过期时间，此时，认为客户端加锁成功，否则认为加锁失败\n4. 加锁成功，去操作共享资源（例如修改 MySQL 某一行，或发起一个 API 请求）\n5. 加锁失败，向「全部节点」发起释放锁请求（前面讲到的 Lua 脚本释放锁）\n\n总结一下，有 4 个重点：\n1. 客户端在多个 Redis 实例上申请加锁\n2. 必须保证大多数节点加锁成功\n3. 大多数节点加锁的总耗时，要小于锁设置的过期时间\n4. 释放锁，要向全部节点发起释放锁请求\n\n- 为什么要在多个实例上加锁？\n  - 本质上是为了「容错」，部分实例异常宕机，剩余的实例加锁成功，整个锁服务依旧可用。\n- 为什么大多数加锁成功，才算成功？\n  - 如果只存在「故障」节点，只要大多数节点正常，那么整个系统依旧是可以提供正确服务的。\n- 为什么步骤 3 加锁成功后，还要计算加锁的累计耗时？\n  - 因为操作的是多个节点，所以耗时肯定会比操作单个实例耗时更久，而且，因为是网络请求，网络情况是复杂的，有可能存在延迟、丢包、超时等情况发生，网络请求越多，异常发生的概率就越大。\n  - 所以，即使大多数节点加锁成功，但如果加锁的累计耗时已经「超过」了锁的过期时间，那此时有些实例上的锁可能已经失效了，这个锁就没有意义了。\n- 为什么释放锁，要操作所有节点？\n  - 在某一个 Redis 节点加锁时，可能因为「网络原因」导致加锁失败。\n  - 例如，客户端在一个 Redis 实例上加锁成功，但在读取响应结果时，网络问题导致读取失败，那这把锁其实已经在 Redis 上加锁成功了。\n  - 所以，释放锁时，不管之前有没有加锁成功，需要释放「所有节点」的锁，以保证清理节点上「残留」的锁。\n\n#### Redis 的 Redlock 有什么问题？一定安全吗？\n- 详细的可以参见这篇文章讲的非常详细https://mp.weixin.qq.com/s/ybiN5Q89wI0CnLURGUz4vw\n#### 业界争论 Redlock，到底在争论什么？哪种观点是对的？\n- 详细的可以参见这篇文章讲的非常详细https://mp.weixin.qq.com/s/ybiN5Q89wI0CnLURGUz4vw\n- 总结一下\n  - 有人认为RedLock在遇到1、网络延迟 2、GC进程暂停 3、时钟漂移 问题时存在安全性问题\n    - `GC进程暂停` 如果A请求锁定s1、s2、s3、s4、s5，拿到锁后进入GC，耗时长，锁过期释放了，此时B请求锁定s1、s2、s3、s4、s5，也拿到锁，此时A GC完毕，认为成功获取到了锁，B也认为获取到了锁（冲突）\n    - `时钟漂移` 客户端 1 获取节点 A、B、C 上的锁，但由于网络问题，无法访问 D 和 E ，节点 C 上的时钟「向前跳跃」，导致锁到期 客户端 2 获取节点 C、D、E 上的锁，由于网络问题，无法访问 A 和 B ，客户端 1 和 2 现在都相信它们持有了锁（冲突）\n  - 提出 fecing token 的方案\n    - 客户端在获取锁时，锁服务可以提供一个「递增」的 token ，客户端拿着这个 token 去操作共享资源 ，共享资源可以根据 token 拒绝「后来者」的请求\n    - 比如a获取锁后得到token33，因为GC锁过期了，B去获取锁，拿到了token34，a GC完毕用token33去操作资源，被拒绝，因为不是最新的token34 \n  - redlock作者的回复\n    - 对于时钟类问题 并不需要完全一致的时钟，只需要大体一致就可以了，允许有「误差」。例如要计时 5s，但实际可能记了 4.5s，之后又记了 5.5s，有一定误差，但只要不超过「误差范围」锁失效时间即可，这种对于时钟的精度要求并不是很高，而且这也符合现实环境。\n    - 解释网络延迟、GC 问题 由于计算过程中需要 判断`T2 - T1 < 锁的过期时间 ` 所以这个问题不存在\n    - 质疑 fencing token 机制  资源服务器基本没有拒绝「旧 token」的能力。这种方案很难实现\n#### 分布式锁到底用 Redis 还是 Zookeeper？\n- 客户端 1 创建临时节点后，Zookeeper 是如何保证让这个客户端一直持有锁呢？\n  - 客户端 1 此时会与 Zookeeper 服务器维护一个 Session，这个 Session 会依赖客户端「定时心跳」来维持连接\n  - 如果 Zookeeper 长时间收不到客户端的心跳，就认为这个 Session 过期了，也会把这个临时节点删除。\n\nZookeeper 的优点：\n- 不需要考虑锁的过期时间\n- watch 机制，加锁失败，可以 watch 等待锁释放，实现乐观锁\n\n但它的劣势是：\n- 性能不如 Redis\n- 部署和运维成本高\n- 客户端与 Zookeeper 的长时间失联，锁被释放问题\n\n所以并非说哪个是最优解，各有优缺点，但现实中Redis使用的更多\n## zookeeper\n<img src=\"../img/分布式相关/分布式锁-zookeeper.png\" width=\"50%\" />\n\n基于zookeeper临时有序节点可以实现的分布式锁。大致思想即为：每个客户端对某个方法加锁时，在zookeeper上的与该方法对应的指定节点的目录下，生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单，只需要判断有序节点中序号最小的一个。 当释放锁的时候，只需将这个瞬时节点删除即可。同时，其可以避免服务宕机导致的锁无法释放，而产生的死锁问题。\n### 可以直接使用zookeeper第三方库Curator客户端，这个客户端中封装了一个可重入的锁服务。\n\n完整流程\n```java\npublic void test() {\n    //Curator提供的InterProcessMutex是分布式锁的实现。通过acquire获得锁，并提供超时机制，release方法用于释放锁。\n    InterProcessMutex lock = new InterProcessMutex(client, ZK_LOCK_PATH);\n    try {\n        //获取锁\n        if (lock.acquire(10 * 1000, TimeUnit.SECONDS)) {\n        //TODO 业务处理\n        }\n    } catch (Exception e) {\n            e.printStackTrace();\n    } finally {\n        try {\n            //释放锁\n            lock.release();\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n    }\n\n}\n```\n### 参考文章\n- https://juejin.cn/post/6844903863363829767\n- https://mp.weixin.qq.com/s/ybiN5Q89wI0CnLURGUz4vw"
  },
  {
    "path": "商城类问题/商城类问题.md",
    "content": "\n* [商城类问题](#商城类问题)\n  * [秒杀](#秒杀)\n    * [项目亮点](#项目亮点)\n    * [秒杀流程](#秒杀流程)\n    * [一. 环境的搭建](#一-环境的搭建)\n    * [二. 登录功能的实现](#二-登录功能的实现)\n      * [1、 数据库设计](#1-数据库设计)\n      * [2、 对登录密码进行两次MD5：细节描述](#2-对登录密码进行两次md5细节描述)\n      * [3、JSR303参数检验](#3jsr303参数检验)\n      * [4、分布式Session？](#4分布式session)\n    * [三. 实现秒杀功能](#三-实现秒杀功能)\n      * [1、数据库设计](#1数据库设计)\n      * [2、商品列表页](#2商品列表页)\n      * [3、商品详情页](#3商品详情页)\n      * [4、订单详情页](#4订单详情页)\n    * [四. JMeter压测](#四-jmeter压测)\n      * [1、JMeter入门](#1jmeter入门)\n      * [2、Redis压测工具redis-benchmark](#2redis压测工具redis-benchmark)\n    * [五. 页面优化技术](#五-页面优化技术)\n      * [1、页面缓存+URL缓存+对象缓存](#1页面缓存url缓存对象缓存)\n      * [2、页面静态化，前后端分离](#2页面静态化前后端分离)\n      * [3、防超卖](#3防超卖)\n      * [4、CDN优化](#4cdn优化)\n    * [六. 接口优化](#六-接口优化)\n      * [1、思路：减少数据库访问](#1思路减少数据库访问)\n      * [2、优化思路](#2优化思路)\n      * [3、秒杀接口优化](#3秒杀接口优化)\n    * [七. 安全优化](#七-安全优化)\n      * [1、秒杀接口地址隐藏](#1秒杀接口地址隐藏)\n      * [2、数学公式验证码](#2数学公式验证码)\n      * [3、接口限流防刷](#3接口限流防刷)\n      * [4、使用一个通用拦截器](#4使用一个通用拦截器)\n    * [八. 总结](#八-总结)\n      * [1.问题总结（主要从三个方面：项目本身的问题、可能出现的问题、可改进的地方）](#1问题总结主要从三个方面项目本身的问题可能出现的问题可改进的地方)\n      * [1.1 项目本身的问题](#11-项目本身的问题)\n      * [1.2 可能出现的问题](#12-可能出现的问题)\n      * [1.3 可改进的地方](#13-可改进的地方)\n  * [如何解决超卖问题？](#如何解决超卖问题)\n    * [redis的库存如何与数据库的库存保持一致？](#redis的库存如何与数据库的库存保持一致)\n    * [redis 预减成功，DB扣减库存失败怎么办？](#redis-预减成功db扣减库存失败怎么办)\n  * [订单延时取消怎么做？](#订单延时取消怎么做)\n    * [方案分析](#方案分析)\n      * [数据库轮询](#数据库轮询)\n      * [JDK的延迟队列](#jdk的延迟队列)\n      * [时间轮算法](#时间轮算法)\n      * [redis zset](#redis-zset)\n      * [Redis 过期回调](#redis-过期回调)\n      * [使用消息队列](#使用消息队列)\n  * [参考文章](#参考文章)\n\n# 商城类问题\n\n## 秒杀\n### 项目亮点\n- 使用分布式Session，可以实现让多台服务器同时可以响应\n- 使用redis做缓存提高访问速度和并发量，减少数据库压力，利用内存标记减少redis的访问\n- 使用页面静态化，加快用户访问速度，提高QPS，缓存页面至浏览器，前后端分离降低服务器压力\n- 使用消息队列完成异步下单，提升用户体验，削峰和降流\n- 安全性优化：双重md5密码校验，秒杀接口地址的隐藏，接口限流防刷，数学公式验证码\n### 秒杀流程\n1. 登录进入商品列表页面，静态资源缓存\n2. 点击进入商品详情页面，静态资源缓存，Ajax获取验证码等动态信息\n3. 点击秒杀, 将验证码结果和商品ID传给后端，如果结果正确。动态生成随机串UUID,结合用户ID和商品ID存入redis，并将path传给前端。前端获取path后，再根据path地址调用秒杀服务\n4. 服务端获取请求的path参数，去查缓存是否在\n5. 如果存在，并且Redis还有库存，预减redis库存，看是否已经生成订单，没有的话就将请求入消息队列\n6. 从消息队列中取消息：获取商品ID和用户ID，判断数据库库存，然后下单\n7. 下单：减库存，生成订单\n8. 前端轮询订单生成结果。50ms继续轮询或者秒杀是否成功和失败\n\n### 一. 环境的搭建\n### 二. 登录功能的实现\n主要内容\n\n- 数据库设计\n- 明文密码两次MD5处理\n- JSR303参数检验+全局异常处理\n- 分布式Session\n\n#### 1、 数据库设计\n\n- 不做注册，直接登录，在MySQL中直接创建表\n- 用户表包括id、nickname、password、salt、头像、注册时间、上次登录时间、登录次数等字段\n\n#### 2、 对登录密码进行两次MD5：细节描述\n\n- 加密的目的：第一次是因为http是明文传输的，第二次为了防止数据库被盗\n\n#### 3、JSR303参数检验\n\n- 通过对输入的参数LoginVo加注解@validated，然后在传入的参数mobile和password上加上注解判断，如@NotNull判断是否为空，也可以自定义\n- 全局异常处理\n\n#### 4、分布式Session？\n\n### 三. 实现秒杀功能\n主要内容\n- 数据库设计\n- 商品列表页\n- 商品详情页\n- 订单详情页\n\n#### 1、数据库设计\n\n- 括商品表、商品表订单表、秒杀商品表、秒杀商品订单表\n\n#### 2、商品列表页\n\n- 为了展示秒杀商品的详情需要goods和miaosha_goods中的信息，所以封装一个GoodsVo，包括价格、库存、秒杀起始时间\n\n#### 3、商品详情页\n\n#### 4、订单详情页\n\n- 这里也是秒杀功能的实现\n- 在控制层先判断库存、然后判断订单是否存在、如果都没有就下单\n- 下单顺序为减库存、下定单、写入秒杀订单\n### 四. JMeter压测\n主要内容\n- JMeter入门\n- 自定义变量模拟多个用户\n- JMeter命令行使用\n- Redis压测工具redis-benchmark\n- Spring Boot打war包\n\n#### 1、JMeter入门\n\n- JMeter在windows下是图形界面\n  - 打开jmeter.bat运行图形界面\n  - 测试计划中添加线程组\n  - 在线程组中添加HTTP请求默认值（就是端口号）\n  - 在线程组中添加HTTP请求（就是要测试的类的URL）、这里可以设置带参数\n  - 在线程组中添加监听器进监听\n  - 也可以通过自定义模拟多用户（写一个文件，导入即可）\n- JMeter在Linux下是命令行进行操\n  - 在Windows上录好jmx\n  - 命令行：sh jmeter.sh -n -t XXX.jmx -l result.jtl\n  - 把result.jtl导入到jmeter\n\n结果是五千并发的情况下，QPS为一千三左右\n\n#### 2、Redis压测工具redis-benchmark\n\n- redis-benchmark -h 127.0.0.1 -p 6379 -c 100 -n 100000\n- -c为100个并发连接，-n为100000个请求\n- Redis的QPS在十万左右\n### 五. 页面优化技术\n内容\n- 页面缓存+URL缓存+对象缓存\n- 页面静态化，前后端分离\n- 静态资源优化\n- CDN优化\n\n#### 1、页面缓存+URL缓存+对象缓存\n\n- 秒杀的瓶颈在于数据库，所以要加上各种粒度的缓存，最大的是页面缓存、最小的是对象缓存\n- 页面缓存步骤（这里指的是商品列表）：\n  - 从redisService中取缓存\n  - 若缓存中没有则手动渲染，利用thymeleaf模板\n  - 然后将页面加入缓存，并返回渲染页面\n  - 不宜时间太长，设置为60s即可\n- URL缓存（指的是商品详情页）\n  - 与页面缓存步骤基本一致，但是需要取缓存和加缓存时要加入参数，GoodsId\n- 对象缓存（指的是User对象）\n  - 前面的页面缓存和URL缓存适合变化不大的，缓存时间比较短\n  - 对象缓存是长期缓存，所以需要有个更新的步骤\n  - 第一步是取缓存\n  - 若缓存中没有则去数据库中查找，并加入缓存；如数据库中没有就报错\n  - 更新用户的密码\n- 加缓存之后的QPS大概3000\n- 需要先更新数据库，后删除缓存；顺序不能反，会导致数据不一致：若线程1先删除缓存，然后线程2读操作，发现缓存中没有，把数据库中的旧数据加入缓存，然后线程1更新数据库，就会导致缓存与数据库数据不一致\n\n#### 2、页面静态化，前后端分离\n\n- 页面静态化无非就是使用纯html页面+Ajax请求json数据后再填充页面\n- 若A页面跳转到B页面之前需要条件判断可以先在A页面中利用ajax请求判断后再跳转\n- 如果不需要条件判断可以直接跳转到B的静态页面，让B自己用ajax请求数据\n\n#### 3、防超卖\n\n- 发生在减库存的时候\n- 解决方法是在Update语句中加一个判断\n- 还有一种情况是一个用户同时发了两个请求，假如库存充足，且没有订单生成，那么就会减两次库存\n- 解决办法是建立用户和商品的唯一索引\n- 做到以上两点是不会发生超卖的\n\n#### 4、CDN优化\n\n- CDN是内容分发网络，相当于缓存，只是部署在全国各地，当用户发起请求时，会找最近的CDN获取资源\n- 并发大的瓶颈在于数据库，所以解决办法是加各种缓存：从浏览器开始，做页面的静态化，将静态页面缓存在浏览器中；请求到达网站之前可以部署一些CDN，让请求首先访问CDN；然后是页面缓存、URL缓存、对象缓存；\n- 加缓存的缺点：数据可能不一致，只能做一个平衡\n### 六. 接口优化\n内容\n- Redis预减库存减少数据库访问\n- 内存标记减少Redis访问\n- RabbitMQ队列缓冲，异步下单，增强用户体验\n- RabbitMQ安装与Spring Boot集成\n- 访问Nginx水平扩展\n- 压测\n\n#### 1、思路：减少数据库访问\n\n1. 系统初始化，把商品库存数量加载到Redis中\n2. 收到请求，Redis预减库存，库存不足，直接返回，否则进入3\n3. 请求入队，立即返回排队中\n4. 请求出队，生成订单，减少库存\n5. 客户端轮询，是否秒杀成功\n\n#### 2、优化思路\n\n1. 系统初始化，把商品库存数量加载到Redis\n2. 收到请求，Redis预减库存，库存不足，直接返回，否则进入3\n3. 请求入队，立即返回排队中（异步下单）\n4. 请求出队，生成订单，减少库存，把订单写入Redis中\n5. 客户端轮询，判断是否秒杀成功\n\n#### 3、秒杀接口优化\n\n1. 之前的没有库存预热的步骤是：查库存-查订单-修改库存-生成订单\n2. 系统初始化时把库存加载到数据库：MiaoshaController 继承InitializingBean实现afterPropertiesSet方法即可\n3. 在上一步库存预热之后，执行步骤为：查Redis库存-判断是否存在订单-进入队列-在出队时才对数据库进行操作\n4. 这一步还可以有一个优化，就是内存标记，使用一个Map，将商品ID设置为false，当买空时，设为true；然后每次不是直接访问Redis进行库存查询，而是对商品ID进行条件判断\n5. 内存标记的优点是减少对Redis的访问（当商品已经卖完之后）\n### 七. 安全优化\n主要内容\n\n- 秒杀接口地址隐藏\n- 数学公式验证码\n- 接口限流防刷\n\n#### 1、秒杀接口地址隐藏\n\n- 虽然前端页面在秒杀未开始时秒杀按钮设置为不可用，但是有可能用户通过前端js代码找到秒杀地址在秒杀未开始时直接访问，秒杀接口隐藏的目的是用户通过js获取到的秒杀地址并不能让其完成秒杀功能\n- 在秒杀之前要先通过Controller中的/path路径下的类随机生成一个path，然后和用户ID一起存入Redis，在执行秒杀的时候再从Redis中取Path进行验证，然后进行秒杀\n\n#### 2、数学公式验证码\n\n- 作用：接口防刷；错开请求\n- 在获取Path是进行验证\n\n#### 3、接口限流防刷\n\n- 当一个用户访问接口时，把访问次数写入缓存，并设置有效期\n- 一分钟之内如果用户访问，则缓存中的访问次数加一，如果次数超限进行限流操作\n- 如果一分钟内没有超限，缓存中数据消失，下次再访问时重新写入缓存\n\n#### 4、使用一个通用拦截器\n\n- 首先写一个注解AccessLimit\n- 后面每个类只需要加注解即可设置防刷次数\n- 定义拦截器：继承HandlerInterceptorAdapter类\n### 八. 总结\n#### 1.问题总结（主要从三个方面：项目本身的问题、可能出现的问题、可改进的地方）\n\n#### 1.1 项目本身的问题\n\n- 画一下项目的架构图\n- 讲一下秒杀流程\n- 秒杀模块怎么设计的\n- 秒杀部分是怎么做的\n- 分布式Session是怎么实现的\n- 如何解决超卖？mysql锁\n- 如何解决重复下单？mysql唯一索引\n- 如何防刷？验证码+通用拦截器限流\n- 消息队列的作用？异步削峰\n- 压测没有？用什么压测？QPS是多少？\n- 库存预减用的是哪个redis方法\n\n#### 1.2 可能出现的问题\n\n- 缓存和数据库数据一致性如何保证？\n- 如果项目中的redis服务挂掉，如何减轻数据库的压力\n- 假如减了库存但用户没有支付，怎么将库存还原继续进行抢购\n\n#### 1.3 可改进的地方\n\n- 系统瓶颈在哪？如何查找，如何再优化？\n- 除了你项目里面的优化，你还有什么优化策略吗？（同上一个问题）\n- 项目难点及问题解决\n- 使用了大量缓存，那么就存在缓存击穿和缓存雪崩以及缓存一致性等问题\n- 大量的使用缓存，对于缓存服务器也有很大的压力，如何减少redis的访问\n- 在高并发请求的业务场景，大量请求来不及处理，甚至出现请求堆积的情况\n- 怎么保证一个用户不能重复下单\n- 怎么解决超卖现象\n- 页面静态化的过程\n\n## 如何解决超卖问题？\n- 在sql加上判断防止数据为负数\n- 数据库加唯一索引防止用户重复购买\n- Redis预减库存减少数据库访问 \n### redis的库存如何与数据库的库存保持一致？\nredis的数量不是库存,他的作用仅仅只是为了阻挡多余的请求透穿到DB，起到一个保护的作用\n因为秒杀的商品有限，比如10个，让1万个请求区访问DB是没有意义的，因为最多也就只能10个\n请求下单成功，所有这个是一个伪命题，我们是不需要保持一致的\n- Redis和数据库库存缓存更新一致问题\n  - 定时查数据库更新Redis缓存\n\n### redis 预减成功，DB扣减库存失败怎么办？\n其实我们可以不用太在意，对用户而言，秒杀不中是正常现象，秒杀中才是意外，单个用户秒杀中\n- 本来就是小概率事件，出现这种情况对于用户而言没有任何影响\n- 对于商户而言，本来就是为了活动拉流量人气的，卖不完还可以省一部分费用，但是活动还参与了，也就没有了任何影响\n- 对网站而言，最重要的是体验，只要网站不崩溃，对用户而言没有任何影响\n\n## 订单延时取消怎么做？\n\n### 方案分析\n\n#### 数据库轮询\n- 该方案通常是在小型项目中使用，即通过一个线程定时的去扫描数据库，通过订单时间来判断是否有超时的订单，然后进行update或delete等操作\n\n优点:\n- 简单易行，支持集群操作\n\n缺点:\n- 对服务器内存消耗大\n- 存在延迟，比如你每隔3分钟扫描一次，那最坏的延迟时间就是3分钟\n- 假设你的订单有几千万条，每隔几分钟这样扫描一次，数据库损耗极大\n\n\n#### JDK的延迟队列\n该方案是利用JDK自带的DelayQueue来实现，这是一个无界阻塞队列，该队列只有在延迟期满的时候才能从中获取元素，放入DelayQueue中的对象，是必须实现Delayed接口的。\n\n优点:\n- 效率高,任务触发时间延迟低。\n\n缺点:\n- 服务器重启后，数据全部消失，怕宕机 \n- 集群扩展相当麻烦 \n- 因为内存条件限制的原因，比如下单未付款的订单数太多，那么很容易就出现OOM异常 \n- 代码复杂度较高\n\n#### 时间轮算法\n![](../img/商城类问题/时间轮.png)\n\n时间轮算法可以类比于时钟，如上图箭头（指针）按某一个方向按固定频率轮动，每一次跳动称为一个 tick。这样可以看出定时轮由个3个重要的属性参数，ticksPerWheel（一轮的tick数），tickDuration（一个tick的持续时间）以及 timeUnit（时间单位），例如当ticksPerWheel=60，tickDuration=1，timeUnit=秒，这就和现实中的始终的秒针走动完全类似了。\n\n如果当前指针指在1上面，我有一个任务需要4秒以后执行，那么这个执行的线程回调或者消息将会被放在5上。那如果需要在20秒之后执行怎么办，由于这个环形结构槽数只到8，如果要20秒，指针需要多转2圈。位置是在2圈之后的5上面（20 % 8 + 1）\n\n我们用Netty的HashedWheelTimer来实现\n```xml\n<dependency>\n    <groupId>io.netty</groupId>\n    <artifactId>netty-all</artifactId>\n    <version>4.1.24.Final</version>\n</dependency>\n```\n```java\n\nimport io.netty.util.HashedWheelTimer;\nimport io.netty.util.Timeout;\nimport io.netty.util.Timer;\nimport io.netty.util.TimerTask;\nimport java.util.concurrent.TimeUnit;\n\npublic class HashedWheelTimerTest {\n\n    static class MyTimerTask implements TimerTask{\n        boolean flag;\n        public MyTimerTask(boolean flag){\n            this.flag = flag;\n        }\n\n        public void run(Timeout timeout) throws Exception {\n            // TODO Auto-generated method stub\n             System.out.println(\"要去数据库删除订单了。。。。\");\n             this.flag =false;\n\n        }\n\n    }\n\n    public static void main(String[] argv) {\n        MyTimerTask timerTask = new MyTimerTask(true);\n        Timer timer = new HashedWheelTimer();\n        timer.newTimeout(timerTask, 5, TimeUnit.SECONDS);\n        int i = 1;\n\n        while(timerTask.flag){\n            try {\n                Thread.sleep(1000);\n            } catch (InterruptedException e) {\n                // TODO Auto-generated catch block\n                e.printStackTrace();\n\n            }\n            System.out.println(i+\"秒过去了\");\n            i++;\n        }\n    }\n}\n```\n优点:\n- 效率高,任务触发时间延迟时间比delayQueue低，代码复杂度比delayQueue低。\n\n缺点:\n- 服务器重启后，数据全部消失，怕宕机\n- 集群扩展相当麻烦\n- 因为内存条件限制的原因，比如下单未付款的订单数太多，那么很容易就出现OOM异常\n\n#### redis zset\n利用redis的zset,zset是一个有序集合，每一个元素(member)都关联了一个score,通过score排序来取集合中的值\n- 添加元素:ZADD key score member [[score member] [score member] …]\n- 按顺序查询元素:ZRANGE key start stop [WITHSCORES]\n- 查询元素score:ZSCORE key member\n- 移除元素:ZREM key member [member …]\n\n那么如何实现呢？我们将订单超时时间戳与订单号分别设置为score和member,系统扫描第一个元素判断是否超时\n\n\n#### Redis 过期回调\nRedis 的key过期回调事件，也能达到延迟队列的效果，简单来说我们开启监听key是否过期的事件，一旦key过期会触发一个callback事件。\n\n修改redis.conf文件开启notify-keyspace-events Ex\n```shell\nnotify-keyspace-events Ex\n```\n\nRedis监听配置，注入Bean RedisMessageListenerContainer\n\n```java\n@Configuration\npublic class RedisListenerConfig {\n    @Bean\n    RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {\n\n        RedisMessageListenerContainer container = new RedisMessageListenerContainer();\n        container.setConnectionFactory(connectionFactory);\n        return container;\n    }\n}\n```\n编写Redis过期回调监听方法，必须继承KeyExpirationEventMessageListener ，有点类似于MQ的消息监听。\n```java\n@Component\npublic class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {\n \n    public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {\n        super(listenerContainer);\n    }\n    @Override\n    public void onMessage(Message message, byte[] pattern) {\n        String expiredKey = message.toString();\n        System.out.println(\"监听到key：\" + expiredKey + \"已过期\");\n    }\n}\n```\n\n#### 使用消息队列\nRabbitMQ 延时队列\n\n利用 RabbitMQ 做延时队列是比较常见的一种方式，而实际上RabbitMQ 自身并没有直接支持提供延迟队列功能，而是通过 RabbitMQ 消息队列的 TTL和 DXL这两个属性间接实现的。\n- TTL 顾名思义：指的是消息的存活时间，RabbitMQ可以通过x-message-tt参数来设置指定Queue（队列）和 Message（消息）上消息的存活时间，它的值是一个非负整数，单位为微秒。如果同时设置队列和队列中消息的TTL，则TTL值以两者中较小的值为准。而队列中的消息存在队列中的时间，一旦超过TTL过期时间则成为Dead Letter（死信）。\n- DLX即死信交换机，绑定在死信交换机上的即死信队列。RabbitMQ的 Queue（队列）可以配置两个参数x-dead-letter-exchange 和 x-dead-letter-routing-key（可选），一旦队列内出现了Dead Letter（死信），则按照这两个参数可以将消息重新路由到另一个Exchange（交换机），让消息重新被消费。\n\n\n下边结合一张图看看如何实现超30分钟未支付关单功能，我们将订单消息A0001发送到延迟队列order.delay.queue，并设置x-message-tt消息存活时间为30分钟，当到达30分钟后订单消息A0001成为了Dead Letter（死信），延迟队列检测到有死信，通过配置x-dead-letter-exchange，将死信重新转发到能正常消费的关单队列，直接监听关单队列处理关单逻辑即可。\n![](../img/商城类问题/消息队列实现订单延迟关闭.png)\n\n发送消息时指定消息延迟的时间\n```java\npublic void send(String delayTimes) {\n        amqpTemplate.convertAndSend(\"order.pay.exchange\", \"order.pay.queue\",\"大家好我是延迟数据\", message -> {\n            // 设置延迟毫秒值\n            message.getMessageProperties().setExpiration(String.valueOf(delayTimes));\n            return message;\n        });\n    }\n}\n```\n设置延迟队列出现死信后的转发规则\n```java\n/**\n     * 延时队列\n     */\n    @Bean(name = \"order.delay.queue\")\n    public Queue getMessageQueue() {\n        return QueueBuilder\n                .durable(RabbitConstant.DEAD_LETTER_QUEUE)\n                // 配置到期后转发的交换\n                .withArgument(\"x-dead-letter-exchange\", \"order.close.exchange\")\n                // 配置到期后转发的路由键\n                .withArgument(\"x-dead-letter-routing-key\", \"order.close.queue\")\n                .build();\n    }\n```\n\n## 参考文章\n- https://github.com/qiurunze123/miaosha\n- https://blog.csdn.net/weixin_41891177/article/details/107775394\n- https://blog.csdn.net/weixin_44406146/article/details/107800771\n- https://www.cnblogs.com/wyq178/p/11261711.html\n- https://zhuanlan.zhihu.com/p/59944775\n- https://mp.weixin.qq.com/s/fQ94NgKeR6qQAcIe0CrusA\n- https://segmentfault.com/a/1190000022718540\n"
  },
  {
    "path": "场景设计/场景设计.md",
    "content": "\n* [场景设计](#场景设计)\n    * [有A、B两个大文件，每个文件几十G，而内存只有4G，其中A文件存放学号+姓名，而B文件存放学号+分数，要求生成文件C，存放姓名和分数。怎么实现？](#有ab两个大文件每个文件几十g而内存只有4g其中a文件存放学号姓名而b文件存放学号分数要求生成文件c存放姓名和分数怎么实现)\n    * [秒杀系统怎么设计](#秒杀系统怎么设计)\n        * [秒杀存在的问题](#秒杀存在的问题)\n        * [如何解决这些问题](#如何解决这些问题)\n    * [产品上线出问题怎么定位错误](#产品上线出问题怎么定位错误)\n    * [大量并发查询用户商品信息，MySQL压力大查询慢，保证速度怎么优化方案](#大量并发查询用户商品信息mysql压力大查询慢保证速度怎么优化方案)\n    * [海量日志数据，提取出某日访问百度次数最多的那个IP。](#海量日志数据提取出某日访问百度次数最多的那个ip)\n    * [给定a、b两个文件，各存放50亿个url，每个url各占64字节，内存限制是4G，让你找出a、b文件共同的url？](#给定ab两个文件各存放50亿个url每个url各占64字节内存限制是4g让你找出ab文件共同的url)\n        * [方案1](#方案1)\n        * [方案2](#方案2)\n    * [一般内存不足而需要分析的数据又很大的问题都可以使用分治的思想，将数据hash(x)%1000分为小文件再分别加载小文件到内存中处理即可](#一般内存不足而需要分析的数据又很大的问题都可以使用分治思想将数据hashx1000分为小文件再分别加载小文件到内存中处理即可)\n    * [如何保证接口的幂等性](#如何保证接口的幂等性)\n        * [什么是幂等性](#什么是幂等性)\n        * [什么情况下需要幂等](#什么情况下需要幂等)\n        * [如何保证幂等](#如何保证幂等)\n            * [1、token机制](#1token机制)\n            * [2、乐观锁机制](#2乐观锁机制)\n            * [3、唯一主键](#3唯一主键)\n            * [4、防重表](#4防重表)\n            * [5、唯一ID](#5唯一id)\n    * [缓存和数据库不一致问题](#缓存和数据库不一致问题)\n      * [更新缓存和更新数据库](#更新缓存和更新数据库)\n      * [删缓存和更新数据库](#删缓存和更新数据库)\n          * [先删缓存，再更新数据库](#先删缓存再更新数据库)\n          * [先更新数据库，再删缓存](#先更新数据库再删缓存)\n      * [数据库和缓存数据强一致怎么办](#数据库和缓存数据强一致怎么办)\n          * [缓存延时双删](#缓存延时双删)\n          * [那么，这个休眠500毫秒怎么确定的，具体该休眠多久呢？](#那么这个休眠500毫秒怎么确定的具体该休眠多久呢)\n          * [如果你用了mysql的读写分离架构怎么办？](#如果你用了mysql的读写分离架构怎么办)\n          * [采用这种同步淘汰策略，吞吐量降低怎么办？](#采用这种同步淘汰策略吞吐量降低怎么办)\n          * [删缓存失败了怎么办：重试机制](#删缓存失败了怎么办重试机制)\n          * [binlog](#binlog)\n    * [什么是SPI](#什么是spi)\n        * [SPI 实践](#spi-实践)\n    * [什么是RPC？](#什么是rpc)\n        * [RPC demo](#rpc-demo)\n    * [gRPC](#grpc)\n        * [gRPC与REST](#grpc与rest)\n        * [demo](#demo)\n            * [grpc](#grpc-1)\n            * [client](#client)\n            * [server](#server)\n    * [一个优秀的RPC框架需要考虑的问题](#一个优秀的rpc框架需要考虑的问题)\n    * [什么是DDD？](#什么是ddd)\n        * [MVC](#mvc)\n        * [那么DDD为什么可以去解决以上的问题呢？](#那么ddd为什么可以去解决以上的问题呢)\n        * [什么样的系统适配DDD](#什么样的系统适配ddd)\n        * [DDD的代码怎么做](#ddd的代码怎么做)\n    * [Java实现生产者消费者](#java实现生产者消费者)\n        * [wait()和notify()方法的实现](#wait和notify方法的实现)\n        * [可重入锁ReentrantLock的实现](#可重入锁reentrantlock的实现)\n            * [阻塞队列BlockingQueue的实现](#阻塞队列blockingqueue的实现)\n            * [信号量Semaphore的实现](#信号量semaphore的实现)\n    * [Java实现BlockQueue](#java实现blockqueue)\n    * [解决哈希冲突的方法](#解决哈希冲突的方法)\n        * [开放定址法](#开放定址法)\n            * [线行探查法](#线行探查法)\n            * [平方探查法](#平方探查法)\n            * [双散列函数探查法](#双散列函数探查法)\n        * [链地址法（拉链法）](#链地址法拉链法)\n        * [再哈希法](#再哈希法)\n        * [建立公共溢出区](#建立公共溢出区)\n    * [排行榜设计](#排行榜设计)\n        * [基于数据库](#基于数据库)\n        * [基于Redis](#基于redis)\n        * [类似于微信计数榜，如何设计不同用户看到的朋友圈的排行榜不一样](#类似于微信计数榜如何设计不同用户看到的朋友圈的排行榜不一样)\n        * [最近七天排行榜怎么弄](#最近七天排行榜怎么弄)\n        * [亿级用户排行榜](#亿级用户排行榜)\n            * [按段位分桶](#按段位分桶)\n                * [计算top100](#计算top100)\n            * [按积分分桶](#按积分分桶)\n* [参考文章](#参考文章)\n\n# 场景设计\n## 有A、B两个大文件，每个文件几十G，而内存只有4G，其中A文件存放学号+姓名，而B文件存放学号+分数，要求生成文件C，存放姓名和分数。怎么实现？\n- hash(学号)%1000，A到a0....a1000,B到b0~b1000\n- 学号相同的人一定hash到相同序号的小文件\n- 加载序号相同的小文件（比如：读取a2和b2）用map储存再按姓名+分数写入C即可\n## 秒杀系统怎么设计\n### 秒杀存在的问题\n- 高并发、瞬间请求量极大\n- 黄牛、黑客恶意请求\n- 链接暴露问题\n- 数据库压力问题\n- 库存不足和超卖问题\n### 如何解决这些问题\n- 页面静态化\n  - 秒杀活动的页面，大多数内容都是固定不变的，如商品名称，商品图片等等，可以对活动页面做静态化处理，减少访问服务端的请求。秒杀用户会分布在全国各地，有的在上海，有的在深圳，地域相差很远，网速也各不相同。为了让用户最快访问到活动页面，可以使用CDN（Content Delivery Network，内容分发网络）。CDN可以让用户就近获取所需内容。\n- 按钮至灰控制\n  - 秒杀活动开始前，按钮一般需要置灰的。只有时间到了，才能变得可以点击。这是防止，秒杀用户在时间快到的前几秒，疯狂请求服务器，然后秒杀时间点还没到，服务器就自己挂了。\n- 服务单一职责\n  - 我们都知道微服务设计思想，也就是把各个功能模块拆分，功能那个类似的放一起，再用分布式的部署方式。\n  - 如用户登录相关的，就设计个用户服务，订单相关的就搞个订单服务，再到礼物相关的就搞个礼物服务等等。那么，秒杀相关的业务逻辑也可以放到一起，搞个秒杀服务，单独给它搞个秒杀数据库。\n  - 服务单一职责有个好处：如果秒杀没抗住高并发的压力，秒杀库崩了，服务挂了，也不会影响到系统的其他服务。\n- 秒杀链接加盐\n  - 链接如果明文暴露的话，会有人获取到请求Url，提前秒杀了。因此，需要给秒杀链接加盐。可以把URL动态化，如通过MD5加密算法加密随机的字符串去做url。\n- 限流\n  - 一般有两种方式限流：nginx限流和redis限流。\n  - 为了防止某个用户请求过于频繁，我们可以对同一用户限流；\n  - 为了防止黄牛模拟几个用户请求，我们可以对某个IP进行限流；\n  - 为了防止有人使用代理，每次请求都更换IP请求，我们可以对接口进行限流。\n  - 为了防止瞬时过大的流量压垮系统，还可以使用阿里的Sentinel、Hystrix组件进行限流。\n- 分布式锁\n  - 可以使用redis分布式锁解决超卖问题。\n  - 使用Redis的SET EX PX NX + 校验唯一随机值,再删除释放锁。\n  - 为了更严谨，一般也是用lua脚本代替。lua脚本如下：\n- MQ异步处理\n  - 如果瞬间流量特别大，可以使用消息队列削峰，异步处理。用户请求过来的时候，先放到消息队列，再拿出来消费。\n- 限流&降级&熔断\n  - 限流，就是限制请求，防止过大的请求压垮服务器；\n  - 降级，就是秒杀服务有问题了，就降级处理，不要影响别的服务；\n  - 熔断，服务有问题就熔断，一般熔断降级是一起出现。\n## 产品上线出问题怎么定位错误\n- 复现问题\n- top jstack \n## 大量并发查询用户商品信息，MySQL压力大查询慢，保证速度怎么优化方案\n读写分离\n## 海量日志数据，提取出某日访问百度次数最多的那个IP。\n- 可以考虑采用“分而治之”的思想，按照IP地址的Hash(IP)%1024值，把海量IP日志分别存储到1024个小文件中。这样，每个小文件最多包含4MB个IP地址\n- 对于每一个小文件，可以构建一个IP为key，出现次数为value的Hash map，同时记录当前出现次数最多的那个IP地址\n- 可以得到1024个小文件中的出现次数最多的IP，再依据常规的排序算法得到总体上出现次数最多的IP；\n## 给定a、b两个文件，各存放50亿个url，每个url各占64字节，内存限制是4G，让你找出a、b文件共同的url？\n### 方案1\n- 遍历文件a，对每个url求取hash(url)%1000，然后根据所取得的值将url分别存储到1000个小文件（记为a0,a1,...,a999）中。这样每个小文件的大约为300M。\n- 遍历文件b，采取和a相同的方式将url分别存储到1000小文件（记为b0,b1,...,b999）。这样处理后，所有可能相同的url都在对应的小文件（a0vsb0,a1vsb1,...,a999vsb999）中，不对应的小文件不可能有相同的url。然后我们只要求出1000对小文件中相同的url即可。\n- 求每对小文件中相同的url时，可以把其中一个小文件的url存储到hash_set中。然后遍历另一个小文件的每个url，看其是否在刚才构建的hash_set中，如果是，那么就是共同的url，存到文件里面就可以了。\n### 方案2\n如果允许有一定的误差，使用布隆过滤器\n## 一般内存不足而需要分析的数据又很大的问题都可以使用分治的思想，将数据hash(x)%1000分为小文件再分别加载小文件到内存中处理即可\n\n## 如何保证接口的幂等性\n\n### 什么是幂等性\n幂等性是系统服务对外一种承诺，承诺只要调用接口成功，外部多次调用对系统的影响是一致的。声明为幂等的服务会认为外部调用失败是常态，并且失败之后必然会有重试。\n\n### 什么情况下需要幂等\n\n以SQL为例：\n\n- SELECT col1 FROM tab1 WHER col2=2，无论执行多少次都不会改变状态，是天然的幂等。\n- UPDATE tab1 SET col1=1 WHERE col2=2，无论执行成功多少次状态都是一致的，因此也是幂等操作。\n- UPDATE tab1 SET col1=col1+1 WHERE col2=2，每次执行的结果都会发生变化，这种不是幂等的。\n- insert into user(userid,name) values(1,'a') 如userid为唯一主键，即重复操作上面的业务，只会插入一条用户数据，具备幂等性。\n  - 如userid不是主键，可以重复，那上面业务多次操作，数据都会新增多条，不具备幂等性。\n- delete from user where userid=1，多次操作，结果一样，具备幂等性\n\n### 如何保证幂等\n\n#### 1、token机制\n- 服务端提供了发送token的接口。我们在分析业务的时候，哪些业务是存在幂等问题的，就必须在执行业务前，先去获取token，服务器会把token保存到redis中。\n- 然后调用业务接口请求时，把token携带过去，一般放在请求头部。\n- 服务器判断token是否存在redis中，存在表示第一次请求，然后删除token,继续执行业务。\n- 如果判断token不存在redis中，就表示是重复操作，直接返回重复标记给client，这样就保证了业务代码，不被重复执行。\n\n**关键点 先删除token，还是后删除token。**\n\n后删除token：如果进行业务处理成功后，删除redis中的token失败了，这样就导致了有可能会发生重复请求，因为token没有被删除。这个问题其实是数据库和缓存redis数据不一致问题，后续会写文章进行讲解。\n\n先删除token：如果系统出现问题导致业务处理出现异常，业务处理没有成功，接口调用方也没有获取到明确的结果，然后进行重试，但token已经删除掉了，服务端判断token不存在，认为是重复请求，就直接返回了，无法进行业务处理了。\n\n先删除token可以保证不会因为重复请求，业务数据出现问题。出现业务异常，可以让调用方配合处理一下，重新获取新的token，再次由业务调用方发起重试请求就ok了。\n\n**token机制缺点**\n\n业务请求每次请求，都会有额外的请求（一次获取token请求、判断token是否存在的业务）。其实真实的生产环境中，1万请求也许只会存在10个左右的请求会发生重试，为了这10个请求，我们让9990个请求都发生了额外的请求。\n\n#### 2、乐观锁机制\n\n- 这种方法适合在更新的场景中，update t_goods set count = count -1 , version = version + 1 where good_id=2 and version = 1\n- 根据version版本，也就是在操作库存前先获取当前商品的version版本号，然后操作的时候带上此version号。我们梳理下，我们第一次操作库存时，得到version为1，调用库存服务version变成了2；但返回给订单服务出现了问题，订单服务又一次发起调用库存服务，当订单服务传如的version还是1，再执行上面的sql语句时，就不会执行；因为version已经变为2了，where条件就不成立。这样就保证了不管调用几次，只会真正的处理一次。\n- 乐观锁主要使用于处理读多写少的问题\n\n#### 3、唯一主键\n\n这个机制是利用了数据库的主键唯一约束的特性，解决了在insert场景时幂等问题。但主键的要求不是自增的主键，这样就需要业务生成全局唯一的主键。\n\n如果是分库分表场景下，路由规则要保证相同请求下，落地在同一个数据库和同一表中，要不然数据库主键约束就不起效果了，因为是不同的数据库和表主键不相关\n\n#### 4、防重表\n\n使用订单号orderNo做为去重表的唯一索引，把唯一索引插入去重表，再进行业务操作，且他们在同一个事务中。这个保证了重复请求时，因为去重表有唯一约束，导致请求失败，避免了幂等问题。这里要注意的是，去重表和业务表应该在同一库中，这样就保证了在同一个事务，即使业务操作失败了，也会把去重表的数据回滚。这个很好的保证了数据一致性。\n\n#### 5、唯一ID\n\n调用接口时，生成一个唯一id，redis将数据保存到集合中（去重），存在即处理过。\n\n## 缓存和数据库不一致问题\n### 更新缓存和更新数据库\n大部分观点认为，做缓存不应该是去更新缓存，而是应该删除缓存，然后由下个请求去去缓存，发现不存在后再读取数据库，写入缓存。观点引用：《分布式之数据库和缓存双写一致性方案解析》孤独烟\n\n原因一：线程安全角度同时有请求A和请求B进行更新操作，那么会出现（1）线程A更新了数据库（2）线程B更新了数据库（3）线程B更新了缓存（4）线程A更新了缓存这就出现请求A更新缓存应该比请求B更新缓存早才对，但是因为网络等原因，B却比A更早更新了缓存。这就导致了脏数据，因此不考虑。\n\n原因二：业务场景角度有如下两点：\n1. 如果你是一个写数据库场景比较多，而读数据场景比较少的业务需求，采用这种方案就会导致，数据压根还没读到，缓存就被频繁的更新，浪费性能。\n2. 如果你写入数据库的值，并不是直接写入缓存的，而是要经过一系列复杂的计算再写入缓存。那么，每次写入数据库后，都再次计算写入缓存的值，无疑是浪费性能的。显然，删除缓存更为适合。\n\n### 删缓存和更新数据库\n#### 先删缓存，再更新数据库\n该方案会导致请求数据不一致同时有一个请求A进行更新操作，另一个请求B进行查询操作。那么会出现如下情形:\n1. 请求A进行写操作，删除缓存\n2. 请求B查询发现缓存不存在\n3. 请求B去数据库查询得到旧值\n4. 请求B将旧值写入缓存\n5. 请求A将新值写入数据库上述情况就会导致不一致的情形出现。\n而且，如果不采用给缓存设置过期时间策略，该数据永远都是脏数据。\n#### 先更新数据库，再删缓存\n这种情况不存在并发问题么？不是的。假设这会有两个请求，一个请求A做查询操作，一个请求B做更新操作，那么会有如下情形产生\n1. 缓存刚好失效\n2. 请求A查询数据库，得一个旧值\n3. 请求B将新值写入数据库\n4. 请求B删除缓存\n5. 请求A将查到的旧值写入缓存ok，如果发生上述情况，确实是会发生脏数据。 然而，发生这种情况的概率又有多少呢？发生上述情况有一个先天性条件，就是步骤（3）的写数据库操作比步骤（2）的读数据库操作耗时更短，才有可能使得步骤（4）先于步骤（5）。可是，大家想想，数据库的读操作的速度远快于写操作的（不然做读写分离干嘛，做读写分离的意义就是因为读操作比较快，耗资源少），因此步骤（3）耗时比步骤（2）更短，这一情形很难出现。\n\n先更新数据库，再删缓存依然会有问题，不过，问题出现的可能性会因为上面说的原因，变得比较低！(补充说明：我用了“先更新数据库，再删缓存”且不设过期时间策略，会不会有问题呢？由于先缓存和更新数据库不是原子的，如果更新了数据库，程序歇逼，就没删缓存，由于没有过期策略，就永远脏数据了。)所以，如果你想实现基础的缓存数据库双写一致的逻辑，那么在大多数情况下，在不想做过多设计，增加太大工作量的情况下，请先更新数据库，再删缓存!\n### 数据库和缓存数据强一致怎么办\n没有办法做到绝对的一致性，这是由CAP理论决定的，缓存系统适用的场景就是非强一致性的场景，所以它属于CAP中的AP。所以，我们得委曲求全，可以去做到BASE理论中说的最终一致性。\n\n大佬们给出了到达最终一致性的解决思路，主要是针对上面两种双写策略（先删缓存，再更新数据库/先更新数据库，再删缓存）导致的脏数据问题，进行相应的处理，来保证最终一致性。\n\n#### 缓存延时双删\n步骤\n1. 先删除缓存\n2. 再写数据库\n3. 休眠500毫秒（根据具体的业务时间来定）\n4. 再次删除缓存。\n\n#### 那么，这个休眠500毫秒怎么确定的，具体该休眠多久呢？\n\n针对上面的情形，读者应该自行评估自己的项目的读数据业务逻辑的耗时。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上，加几百ms即可。这么做的目的，就是确保读请求结束，写请求可以删除读请求造成的缓存脏数据。\n\n\n#### 如果你用了mysql的读写分离架构怎么办？\n\nok，在这种情况下，造成数据不一致的原因如下，还是两个请求，一个请求A进行更新操作，另一个请求B进行查询操作。\n1. 请求A进行写操作，删除缓存\n2. 请求A将数据写入数据库了，\n3. 请求B查询缓存发现，缓存没有值\n4. 请求B去从库查询，这时，还没有完成主从同步，因此查询到的是旧值\n5. 请求B将旧值写入缓存\n6. 数据库完成主从同步，从库变为新值上述情形，就是数据不一致的原因。还是使用双删延时策略。只是，睡眠时间修改为在主从同步的延时时间基础上，加几百ms。\n\n#### 采用这种同步淘汰策略，吞吐量降低怎么办？\n\nok，那就将第二次删除作为异步的。自己起一个线程，异步删除。这样，写的请求就不用沉睡一段时间后了，再返回。这么做，加大吞吐量。\n\n#### 删缓存失败了怎么办：重试机制\n看似问题都已经解决了，但其实，还有一个问题没有考虑到，那就是删除缓存的操作，失败了怎么办？比如延时双删的时候，第二次缓存删除失败了，那不还是没有清除脏数据吗？解决方案就是再加上一个重试机制，保证删除缓存成功\n\n流程如下所示\n1. 更新数据库数据；\n2. 缓存因为种种问题删除失败\n3. 将需要删除的key发送至消息队列\n4. 自己消费消息，获得需要删除的key\n5. 继续重试删除操作，直到成功然而，\n\n该方案有一个缺点，对业务线代码造成大量的侵入。\n\n于是有了方案二，在方案二中，启动一个订阅程序去订阅数据库的binlog，获得需要操作的数据。在应用程序中，另起一段程序，获得这个订阅程序传来的信息，进行删除缓存操作\n\n#### binlog\n流程如下所示\n1. 更新数据库数据\n2. 数据库会将操作信息写入binlog日志当中\n3. 订阅程序提取出所需要的数据以及key\n4. 另起一段非业务代码，获得该信息\n5. 尝试删除缓存操作，发现删除失败\n6. 将这些信息发送至消息队列\n7. 重新从消息队列中获得该数据，重试操作。\n\n## 什么是SPI\nSPI 全称为 (Service Provider Interface) ，是JDK内置的一种服务提供发现机制\n\n### SPI 实践\n接下来我们来如何来利用 SPI 实现刚才提到的可拔插 IOC 容器。\n\n既然刚才都提到了 SPI 的本质就是面向接口编程，所以自然我们首先需要定义一个接口：\n\n![](../img/场景设计/spi-接口.png)\n\n其中包含了一些 Bean 容器所必须的操作：注册、获取、释放 bean。\n\n为了让其他人也能实现自己的 IOC 容器，所以我们将这个接口单独放到一个 Module 中，可供他人引入实现。\n\n![](../img/场景设计/spi-实现.png)\n\n所以当我要实现一个单例的 IOC 容器时，我只需要新建一个 Module 然后引入刚才的模块并实现 CicadaBeanFactory 接口即可。\n\n当然其中最重要的则是需要在 resources 目录下新建一个 META-INF/services/top.crossoverjie.cicada.base.bean.CicadaBeanFactory 文件，文件名必须得是我们之前定义接口的全限定名（SPI 规范）。\n![](../img/场景设计/spi-ioc.png)\n\n其中的内容便是我们自己实现类的全限定名：\n```shell\ntop.crossoverjie.cicada.bean.ioc.CicadaIoc\n```\n可以想象最终会通过这里的全限定名来反射创建对象。\n\n只不过这个过程 Java 已经提供 API 屏蔽掉了：\n```java\npublic static CicadaBeanFactory getCicadaBeanFactory() {\n    ServiceLoader<CicadaBeanFactory> cicadaBeanFactories = ServiceLoader.load(CicadaBeanFactory.class);\n    if (cicadaBeanFactories.iterator().hasNext()){\n        return cicadaBeanFactories.iterator().next() ;\n    }\n    return new CicadaDefaultBean();\n}\n```\n当 classpath 中存在我们刚才的实现类（引入实现类的 jar 包），便可以通过 java.util.ServiceLoader 工具类来找到所有的实现类（可以有多个实现类同时存在，只不过通常我们只需要一个）。\n\n一些都准备好之后，使用自然就非常简单了。\n```xml\n<dependency>\n    <groupId>top.crossoverjie.opensource</groupId>\n    <artifactId>cicada-ioc</artifactId>\n    <version>2.0.4</version>\n</dependency>\n```\n我们只需要引入这个依赖便能使用它的实现，当我们想换一种实现方式时只需要更换一个依赖即可。\n\n这样就做到了不修改一行代码灵活的可拔插选择 IOC 容器了。\n\nSPI 的一些其他应用\n\nMySQL 的驱动包也是利用 SPI 来实现自己的连接逻辑。\n\n![](../img/场景设计/mysql-spi.png)\n\n总结来说：\n\n- 提供一个接口\n- 在resource下新建META-INF/services目录，在目录下新建接口的全限定名文件\n- 服务方实现接口\n- 调用ServiceLoad.load()\n\n## 什么是RPC？\nRPC（Remote Procedure Call）远程过程调用，简单的理解是一个节点请求另一个节点提供的服务\n\n1. 首先客户端需要告诉服务器，需要调用的函数，这里函数和进程ID存在一个映射，客户端远程调用时，需要查一下函数，找到对应的ID，然后执行函数的代码。\n2. 客户端需要把本地参数传给远程函数，本地调用的过程中，直接压栈即可，但是在远程调用过程中不再同一个内存里，无法直接传递函数的参数，因此需要客户端把参数转换成字节流，传给服务端，然后服务端将字节流转换成自身能读取的格式，是一个序列化和反序列化的过程。\n3. 数据准备好了之后，如何进行传输？网络传输层需要把调用的ID和序列化后的参数传给服务端，然后把计算好的结果序列化传给客户端，因此TCP层即可完成上述过程，gRPC中采用的是HTTP2协议。\n\n总结一下：\n```text\n// Client端 \n//    Student student = Call(ServerAddr, addAge, student)\n1. 将这个调用映射为Call ID。\n2. 将Call ID，student（params）序列化，以二进制形式打包\n3. 把2中得到的数据包发送给ServerAddr，这需要使用网络传输层\n4. 等待服务器返回结果\n5. 如果服务器调用成功，那么就将结果反序列化，并赋给student，年龄更新\n\n// Server端\n1. 在本地维护一个Call ID到函数指针的映射call_id_map，可以用Map<String, Method> callIdMap\n2. 等待客户端请求\n3. 得到一个请求后，将其数据包反序列化，得到Call ID\n4. 通过在callIdMap中查找，得到相应的函数指针\n5. 将student（params）反序列化后，在本地调用addAge()函数，得到结果\n6. 将student结果序列化后通过网络返回给Client\n```\n\n- 在微服务的设计中，一个服务A如果访问另一个Module下的服务B，可以采用HTTP REST传输数据，并在两个服务之间进行序列化和反序列化操作，服务B把执行结果返回过来。\n- 由于HTTP在应用层中完成，整个通信的代价较高，远程过程调用中直接基于TCP进行远程调用，数据传输在传输层TCP层完成，更适合对效率要求比较高的场景，RPC主要依赖于客户端和服务端之间建立Socket链接进行，底层实现比REST更复杂。\n\n### RPC demo\n客户端\n```java\npublic class RPCClient<T> {\n    public static <T> T getRemoteProxyObj(final Class<?> serviceInterface, final InetSocketAddress addr) {\n        // 1.将本地的接口调用转换成JDK的动态代理，在动态代理中实现接口的远程调用\n        return (T) Proxy.newProxyInstance(serviceInterface.getClassLoader(), new Class<?>[]{serviceInterface},\n                new InvocationHandler() {\n                    @Override\n                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {\n                        Socket socket = null;\n                        ObjectOutputStream output = null;\n                        ObjectInputStream input = null;\n                        try{\n                            // 2.创建Socket客户端，根据指定地址连接远程服务提供者\n                            socket = new Socket();\n                            socket.connect(addr);\n\n                            // 3.将远程服务调用所需的接口类、方法名、参数列表等编码后发送给服务提供者\n                            output = new ObjectOutputStream(socket.getOutputStream());\n                            output.writeUTF(serviceInterface.getName());\n                            output.writeUTF(method.getName());\n                            output.writeObject(method.getParameterTypes());\n                            output.writeObject(args);\n\n                            // 4.同步阻塞等待服务器返回应答，获取应答后返回\n                            input = new ObjectInputStream(socket.getInputStream());\n                            return input.readObject();\n                        }finally {\n                            if (socket != null){\n                                socket.close();\n                            }\n                            if (output != null){\n                                output.close();\n                            }\n                            if (input != null){\n                                input.close();\n                            }\n                        }\n                    }\n                });\n    }\n}\n```\n服务端\n```java\npublic class ServiceCenter implements Server {\n\n    private static ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());\n\n    private static final HashMap<String, Class> serviceRegistry = new HashMap<String, Class>();\n\n    private static boolean isRunning = false;\n\n    private static int port;\n\n\n    public ServiceCenter(int port){\n        ServiceCenter.port = port;\n    }\n\n\n    @Override\n    public void start() throws IOException {\n        ServerSocket server = new ServerSocket();\n        server.bind(new InetSocketAddress(port));\n        System.out.println(\"Server Start .....\");\n        try{\n            while(true){\n                executor.execute(new ServiceTask(server.accept()));\n            }\n        }finally {\n            server.close();\n        }\n    }\n\n    @Override\n    public void register(Class serviceInterface, Class impl) {\n        serviceRegistry.put(serviceInterface.getName(), impl);\n    }\n\n    @Override\n    public boolean isRunning() {\n        return isRunning;\n    }\n\n    @Override\n    public int getPort() {\n        return port;\n    }\n\n    @Override\n    public void stop() {\n        isRunning = false;\n        executor.shutdown();\n    }\n   private static class ServiceTask implements Runnable {\n        Socket client = null;\n\n        public ServiceTask(Socket client) {\n            this.client = client;\n        }\n\n        @Override\n        public void run() {\n            ObjectInputStream input = null;\n            ObjectOutputStream output = null;\n            try{\n                input = new ObjectInputStream(client.getInputStream());\n                String serviceName = input.readUTF();\n                String methodName = input.readUTF();\n                Class<?>[] parameterTypes = (Class<?>[]) input.readObject();\n                Object[] arguments = (Object[]) input.readObject();\n                Class serviceClass = serviceRegistry.get(serviceName);\n                if(serviceClass == null){\n                    throw new ClassNotFoundException(serviceName + \"not found!\");\n                }\n                Method method = serviceClass.getMethod(methodName, parameterTypes);\n                Object result = method.invoke(serviceClass.newInstance(), arguments);\n\n                output = new ObjectOutputStream(client.getOutputStream());\n                output.writeObject(result);\n            }catch (Exception e){\n                e.printStackTrace();\n            }finally {\n                if(output!=null){\n                    try{\n                        output.close();\n                    }catch (IOException e){\n                        e.printStackTrace();\n                    }\n                }\n                if (input != null) {\n                    try {\n                        input.close();\n                    } catch (IOException e) {\n                        e.printStackTrace();\n                    }\n                }\n                if (client != null) {\n                    try {\n                        client.close();\n                    } catch (IOException e) {\n                        e.printStackTrace();\n                    }\n                }\n            }\n        }\n    }\n}\n\npublic class ServiceProducerImpl implements ServiceProducer{\n    @Override\n    public String sendData(String data) {\n        return \"I am service producer!!!, the data is \"+ data;\n    }\n}\n\npublic class RPCTest {\n    public static void main(String[] args) throws IOException {\n        new Thread(new Runnable() {\n            @Override\n            public void run() {\n                try {\n                    Server serviceServer = new ServiceCenter(8088);\n                    serviceServer.register(ServiceProducer.class, ServiceProducerImpl.class);\n                    serviceServer.start();\n                } catch (IOException e) {\n                    e.printStackTrace();\n                }\n            }\n        }).start();\n        ServiceProducer service = RPCClient.getRemoteProxyObj(ServiceProducer.class, new InetSocketAddress(\"localhost\", 8088));\n        System.out.println(service.sendData(\"test\"));\n    }\n}\n```\n\n## gRPC\n### gRPC与REST\n- REST通常以业务为导向，将业务对象上执行的操作映射到HTTP动词，格式非常简单，可以使用浏览器进行扩展和传输，通过JSON数据完成客户端和服务端之间的消息通信，直接支持请求/响应方式的通信。不需要中间的代理，简化了系统的架构，不同系统之间只需要对JSON进行解析和序列化即可完成数据的传递。\n- 但是REST也存在一些弊端，比如只支持请求/响应这种单一的通信方式，对象和字符串之间的序列化操作也会影响消息传递速度，客户端需要通过服务发现的方式，知道服务实例的位置，在单个请求获取多个资源时存在着挑战，而且有时候很难将所有的动作都映射到HTTP动词。\n- 正是因为REST面临一些问题，因此可以采用gRPC作为一种替代方案，gRPC 是一种基于二进制流的消息协议，可以采用基于Protocol Buffer的IDL定义grpc API,这是Google公司用于序列化结构化数据提供的一套语言中立的序列化机制，客户端和服务端使用HTTP/2以Protocol Buffer格式交换二进制消息。\n- gRPC的优势是，设计复杂更新操作的API非常简单，具有高效紧凑的进程通信机制，在交换大量消息时效率高，远程过程调用和消息传递时可以采用双向的流式消息方式，同时客户端和服务端支持多种语言编写，互操作性强；不过gRPC的缺点是不方便与JavaScript集成，某些防火墙不支持该协议。\n- 注册中心：当项目中有很多服务时，可以把所有的服务在启动的时候注册到一个注册中心里面，用于维护服务和服务器之间的列表，当注册中心接收到客户端请求时，去找到该服务是否远程可以调用，如果可以调用需要提供服务地址返回给客户端，客户端根据返回的地址和端口，去调用远程服务端的方法，执行完成之后将结果返回给客户端。这样在服务端加新功能的时候，客户端不需要直接感知服务端的方法，服务端将更新之后的结果在注册中心注册即可，而且当修改了服务端某些方法的时候，或者服务降级服务多机部署想实现负载均衡的时候，我们只需要更新注册中心的服务群即可。\n![](../img/场景设计/grpc注册中心.png)\n\n### demo\n这里使用SpringBoot+gRPC的形式实现RPC调用过程 项目结构分为三部分：client、grpc、server\n#### grpc\n![](../img/场景设计/grpc.png)\n```xml\n<dependency>\n      <groupId>io.grpc</groupId>\n      <artifactId>grpc-all</artifactId>\n       <version>1.12.0</version>\n </dependency>\n```\n```xml\n<build>\n        <extensions>\n            <extension>\n                <groupId>kr.motd.maven</groupId>\n                <artifactId>os-maven-plugin</artifactId>\n                <version>1.4.1.Final</version>\n            </extension>\n        </extensions>\n        <plugins>\n            <plugin>\n                <groupId>org.xolstice.maven.plugins</groupId>\n                <artifactId>protobuf-maven-plugin</artifactId>\n                <version>0.5.0</version>\n                <configuration>\n                    <pluginId>grpc-java</pluginId>\n                    <protocArtifact>com.google.protobuf:protoc:3.0.2:exe:${os.detected.classifier}</protocArtifact>\n                    <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.2.0:exe:${os.detected.classifier}</pluginArtifact>\n                </configuration>\n                <executions>\n                    <execution>\n                        <goals>\n                            <goal>compile</goal>\n                            <goal>compile-custom</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n```\n创建.proto文件\n```protobuf\nsyntax = \"proto3\";   // 语法版本\n\n// stub选项\noption java_package = \"com.shgx.grpc.api\";\noption java_outer_classname = \"RPCDateServiceApi\";\noption java_multiple_files = true;\n\n// 定义包名\npackage com.shgx.grpc.api;\n\n// 服务接口定义，服务端和客户端都要遵守该接口进行通信\nservice RPCDateService {\n    rpc getDate (RPCDateRequest) returns (RPCDateResponse) {}\n}\n\n// 定义消息（请求）\nmessage RPCDateRequest {\n    string userName = 1;\n}\n\n// 定义消息（响应）\nmessage RPCDateResponse {\n    string serverDate = 1;\n}\n\n```\nmvn complie\n\n生成代码：\n![](../img/场景设计/grpc-代码.png)\n\n#### client\n![](../img/场景设计/client.png)\n\n根据gRPC中的项目配置在client和server两个Module的pom.xml添加依赖\n```xml\n        <dependency>\n            <groupId>com.shgx</groupId>\n            <artifactId>grpc</artifactId>\n            <version>0.0.1-SNAPSHOT</version>\n            <scope>compile</scope>\n        </dependency>\n```\n编写GRPCClient\n```java\npublic class GRPCClient {\n    private static final String host = \"localhost\";\n    private static final int serverPort = 9999;\n\n    public static void main( String[] args ) throws Exception {\n        ManagedChannel managedChannel = ManagedChannelBuilder.forAddress( host, serverPort ).usePlaintext().build();\n        try {\n            RPCDateServiceGrpc.RPCDateServiceBlockingStub rpcDateService = RPCDateServiceGrpc.newBlockingStub( managedChannel );\n            RPCDateRequest rpcDateRequest = RPCDateRequest\n                    .newBuilder()\n                    .setUserName(\"shgx\")\n                    .build();\n            RPCDateResponse rpcDateResponse = rpcDateService.getDate( rpcDateRequest );\n            System.out.println( rpcDateResponse.getServerDate() );\n        } finally {\n            managedChannel.shutdown();\n        }\n    }\n}\n\n```\n#### server\n![](../img/场景设计/server.png)\n\n按照2.2.3 client的方式添加依赖\n\n创建RPCDateServiceImpl\n```java\npublic class RPCDateServiceImpl extends RPCDateServiceGrpc.RPCDateServiceImplBase{\n    @Override\n    public void getDate(RPCDateRequest request, StreamObserver<RPCDateResponse> responseObserver) {\n        RPCDateResponse rpcDateResponse = null;\n        Date now=new Date();\n        SimpleDateFormat simpleDateFormat = new SimpleDateFormat(\"今天是\"+\"yyyy年MM月dd日 E kk点mm分\");\n        String nowTime = simpleDateFormat.format( now );\n        try {\n            rpcDateResponse = RPCDateResponse\n                    .newBuilder()\n                    .setServerDate( \"Welcome \" + request.getUserName()  + \", \" + nowTime )\n                    .build();\n        } catch (Exception e) {\n            responseObserver.onError(e);\n        } finally {\n            responseObserver.onNext( rpcDateResponse );\n        }\n        responseObserver.onCompleted();\n    }\n}\n```\n创建GRPCServer\n```java\npublic class GRPCServer {\n    private static final int port = 9999;\n    public static void main( String[] args ) throws Exception {\n        Server server = ServerBuilder.\n                forPort(port)\n                .addService( new RPCDateServiceImpl() )\n                .build().start();\n        System.out.println( \"grpc服务端启动成功, 端口=\" + port );\n        server.awaitTermination();\n    }\n}\n```\n## 一个优秀的RPC框架需要考虑的问题\n1. `微服务化`应用都基于微服务化，实现资源调用离不开远程调用\n2. `多实例问题` 一个服务可能有多个实例，你在调用时，要如何获取这些实例的地址呢？--- 这时候就需要一个服务注册中心，从服务注册中心获取服务的实例列表，再从中选择一个进行调用。\n3. `负载均衡` 选哪个调用好呢？这时候就需要负载均衡了，于是又得考虑如何实现复杂均衡\n4. `缓存` 总不能每次调用时都去注册中心查询实例列表吧，这样效率多低呀，于是又有了缓存，有了缓存，就要考虑缓存的更新问题\n5. `异步调用` 客户端总不能每次调用完都干等着服务端返回数据吧,于是就要支持异步调用;\n   - Future实现\n6. `版本控制` 服务端的接口修改了，老的接口还有人在用，怎么办？总不能让他们都改了吧？这就需要版本控制了；\n7. `线程池` 服务端总不能每次接到请求都马上启动一个线程去处理吧？于是就需要线程池；\n8. `未处理完的请求` 服务端关闭时，还没处理完的请求怎么办？是直接结束呢，还是等全部请求处理完再关闭呢？\n\n## 什么是DDD？\n### MVC\n要说DDD，不得不先看看MVC，我相信基本上99%的java开发读者，不管你是计科专业出身还是跨专业，初学spring或者springboot的时候，接触到的代码分层都是MVC\n这说明了MVC有它自身独有的优势：\n- 开发人员可以只关注整个结构中的其中某一层；\n- 可以很容易的用新的实现来替换原有层次的实现；\n- 可以降低层与层之间的依赖；\n- 有利于标准化；\n- 利于各层逻辑的复用。\n\n但是真实情况是这样吗？随着你系统功能迭代，业务逻辑越来越复杂之后。MVC三层中，V层作为数据载体，C层作为逻辑路由都是很薄的一层，大量的代码都堆积在了M层（模型层）。一个service的类，动辄几百上千行，大的甚至几万行，逻辑嵌套复杂，主业务逻辑不清晰。service做的稍微轻量化一点的，代码就像是胶水，把数据库执行逻辑与控制返回给前端的逻辑胶在一起，主次不清晰。\n一看你的工程，类啊，代码量啊都不少，你甚至不知道如何入手去修改“屎山”一样的代码。\n\n### 那么DDD为什么可以去解决以上的问题呢？\n\nDDD核心思想是什么呢？解耦！让业务不是像炒大锅饭一样混在一起，而是一道道工序复杂的美食，都有他们自己独立的做法。\n\nDDD的价值观里面，任何业务都是某个业务领域模型的职责体现。A领域只会去做A领域的事情，A领域想去修改B领域，需要找中介（防腐层）去对B领域完成操作。我想完成一个很长的业务逻辑动作，在划分好业务边界之后，交给业务服务的编排者（应用服务）去组织业务模型（聚合）完成逻辑。\n\n这样，每个服务（领域）只会做自己业务边界内的事情，最小细粒度的去定义需求的实现。原先空空的贫血模型摇身一变变成了充血模型。原理冗长的service里面类似到处set，get值这种与业务逻辑无关的数据载体包装代码，都会被去除，进到应用服务层，你的代码就是你的业务逻辑。逻辑清晰，可维护性高！\n\n### 什么样的系统适配DDD\n中小规模的系统，本身业务体量小，功能单一，选择mvc架构无疑是最好的。 项目化交付的系统，研发周期短，一天到晚按照甲方的需求定制功能。\n\n中大规模系统，产品化模式，业务可持续迭代，可预见的业务逻辑复杂性的系统。\n\n### DDD的代码怎么做\n\n// TODO\n\n## Java实现生产者消费者\n### wait()和notify()方法的实现\n```java\npublic class Test1 {\n    private static Integer count = 0;\n    private static final Integer FULL = 10;\n    private static String LOCK = \"lock\";\n    \n    public static void main(String[] args) {\n        Test1 test1 = new Test1();\n        new Thread(test1.new Producer()).start();\n        new Thread(test1.new Consumer()).start();\n        new Thread(test1.new Producer()).start();\n        new Thread(test1.new Consumer()).start();\n        new Thread(test1.new Producer()).start();\n        new Thread(test1.new Consumer()).start();\n        new Thread(test1.new Producer()).start();\n        new Thread(test1.new Consumer()).start();\n    }\n    class Producer implements Runnable {\n        @Override\n        public void run() {\n            for (int i = 0; i < 10; i++) {\n                try {\n                    Thread.sleep(3000);\n                } catch (Exception e) {\n                    e.printStackTrace();\n                }\n                synchronized (LOCK) {\n                    while (count == FULL) {\n                        try {\n                            LOCK.wait();\n                        } catch (Exception e) {\n                            e.printStackTrace();\n                        }\n                    }\n                    count++;\n                    System.out.println(Thread.currentThread().getName() + \"生产者生产，目前总共有\" + count);\n                    LOCK.notifyAll();\n                }\n            }\n        }\n    }\n    class Consumer implements Runnable {\n        @Override\n        public void run() {\n            for (int i = 0; i < 10; i++) {\n                try {\n                    Thread.sleep(3000);\n                } catch (InterruptedException e) {\n                    e.printStackTrace();\n                }\n                synchronized (LOCK) {\n                    while (count == 0) {\n                        try {\n                            LOCK.wait();\n                        } catch (Exception e) {\n                        }\n                    }\n                    count--;\n                    System.out.println(Thread.currentThread().getName() + \"消费者消费，目前总共有\" + count);\n                    LOCK.notifyAll();\n                }\n            }\n        }\n    }\n}\n```\n### 可重入锁ReentrantLock的实现\n```java\npublic class Test2 {\n    private static Integer count = 0;\n    private static final Integer FULL = 10;\n    //创建一个锁对象\n    private Lock lock = new ReentrantLock();\n    //创建两个条件变量，一个为缓冲区非满，一个为缓冲区非空\n    private final Condition notFull = lock.newCondition();\n    private final Condition notEmpty = lock.newCondition();\n    public static void main(String[] args) {\n        Test2 test2 = new Test2();\n        new Thread(test2.new Producer()).start();\n        new Thread(test2.new Consumer()).start();\n        new Thread(test2.new Producer()).start();\n        new Thread(test2.new Consumer()).start();\n        new Thread(test2.new Producer()).start();\n        new Thread(test2.new Consumer()).start();\n        new Thread(test2.new Producer()).start();\n        new Thread(test2.new Consumer()).start();\n    }\n    class Producer implements Runnable {\n        @Override\n        public void run() {\n            for (int i = 0; i < 10; i++) {\n                try {\n                    Thread.sleep(3000);\n                } catch (Exception e) {\n                    e.printStackTrace();\n                }\n                //获取锁\n                lock.lock();\n                try {\n                    while (count == FULL) {\n                        try {\n                            notFull.await();\n                        } catch (InterruptedException e) {\n                            e.printStackTrace();\n                        }\n                    }\n                    count++;\n                    System.out.println(Thread.currentThread().getName()\n                            + \"生产者生产，目前总共有\" + count);\n                    //唤醒消费者\n                    notEmpty.signal();\n                } finally {\n                    //释放锁\n                    lock.unlock();\n                }\n            }\n        }\n    }\n    class Consumer implements Runnable {\n        @Override\n        public void run() {\n            for (int i = 0; i < 10; i++) {\n                try {\n                    Thread.sleep(3000);\n                } catch (InterruptedException e1) {\n                    e1.printStackTrace();\n                }\n                lock.lock();\n                try {\n                    while (count == 0) {\n                        try {\n                            notEmpty.await();\n                        } catch (Exception e) {\n                            e.printStackTrace();\n                        }\n                    }\n                    count--;\n                    System.out.println(Thread.currentThread().getName()\n                            + \"消费者消费，目前总共有\" + count);\n                    notFull.signal();\n                } finally {\n                    lock.unlock();\n                }\n            }\n        }\n    }\n}\n```\n#### 阻塞队列BlockingQueue的实现\n```java\npublic class Test3 {\n    private static Integer count = 0;\n    //创建一个阻塞队列\n    final BlockingQueue blockingQueue = new ArrayBlockingQueue<>(10);\n    public static void main(String[] args) {\n        Test3 test3 = new Test3();\n        new Thread(test3.new Producer()).start();\n        new Thread(test3.new Consumer()).start();\n        new Thread(test3.new Producer()).start();\n        new Thread(test3.new Consumer()).start();\n        new Thread(test3.new Producer()).start();\n        new Thread(test3.new Consumer()).start();\n        new Thread(test3.new Producer()).start();\n        new Thread(test3.new Consumer()).start();\n    }\n    class Producer implements Runnable {\n        @Override\n        public void run() {\n            for (int i = 0; i < 10; i++) {\n                try {\n                    Thread.sleep(3000);\n                } catch (Exception e) {\n                    e.printStackTrace();\n                }\n                try {\n                    blockingQueue.put(1);\n                    count++;\n                    System.out.println(Thread.currentThread().getName()\n                            + \"生产者生产，目前总共有\" + count);\n                } catch (InterruptedException e) {\n                    e.printStackTrace();\n                }\n            }\n        }\n    }\n    class Consumer implements Runnable {\n        @Override\n        public void run() {\n            for (int i = 0; i < 10; i++) {\n                try {\n                    Thread.sleep(3000);\n                } catch (InterruptedException e1) {\n                    e1.printStackTrace();\n                }\n                try {\n                    blockingQueue.take();\n                    count--;\n                    System.out.println(Thread.currentThread().getName()\n                            + \"消费者消费，目前总共有\" + count);\n                } catch (InterruptedException e) {\n                    e.printStackTrace();\n                }\n            }\n        }\n    }\n}\n```\n#### 信号量Semaphore的实现\n```java\npublic class Test4 {\n    private static Integer count = 0;\n    //创建三个信号量\n    final Semaphore notFull = new Semaphore(10);\n    final Semaphore notEmpty = new Semaphore(0);\n    final Semaphore mutex = new Semaphore(1);\n    public static void main(String[] args) {\n        Test4 test4 = new Test4();\n        new Thread(test4.new Producer()).start();\n        new Thread(test4.new Consumer()).start();\n        new Thread(test4.new Producer()).start();\n        new Thread(test4.new Consumer()).start();\n        new Thread(test4.new Producer()).start();\n        new Thread(test4.new Consumer()).start();\n        new Thread(test4.new Producer()).start();\n        new Thread(test4.new Consumer()).start();\n    }\n    class Producer implements Runnable {\n        @Override\n        public void run() {\n            for (int i = 0; i < 10; i++) {\n                try {\n                    Thread.sleep(3000);\n                } catch (InterruptedException e) {\n                    e.printStackTrace();\n                }\n                try {\n                    notFull.acquire();\n                    mutex.acquire();\n                    count++;\n                    System.out.println(Thread.currentThread().getName()\n                            + \"生产者生产，目前总共有\" + count);\n                } catch (InterruptedException e) {\n                    e.printStackTrace();\n                } finally {\n                    mutex.release();\n                    notEmpty.release();\n                }\n            }\n        }\n    }\n    class Consumer implements Runnable {\n        @Override\n        public void run() {\n            for (int i = 0; i < 10; i++) {\n                try {\n                    Thread.sleep(3000);\n                } catch (InterruptedException e1) {\n                    e1.printStackTrace();\n                }\n                try {\n                    notEmpty.acquire();\n                    mutex.acquire();\n                    count--;\n                    System.out.println(Thread.currentThread().getName()\n                            + \"消费者消费，目前总共有\" + count);\n                } catch (InterruptedException e) {\n                    e.printStackTrace();\n                } finally {\n                    mutex.release();\n                    notFull.release();\n                }\n            }\n        }\n    }\n}\n\n```\n## Java实现BlockQueue\n```java\npublic class BlockingQueue<E> {\n\n    /**\n     * 有界队列内部固定长度，因此可以用数组实现\n     */\n    private Object[] elements;\n\n    /**\n     * 队列的头和尾下标\n     */\n    private int head = 0, tail = 0;\n\n    /**\n     * 队列目前的长度\n     */\n    private int size;\n    private ReentrantLock lock = new ReentrantLock();\n    private Condition notEmpty = lock.newCondition();\n    private Condition notFull = lock.newCondition();\n\n    public BlockingQueue(int capacity) {\n        this.elements = new Object[capacity];\n    }\n\n    public void put(E e) {\n        lock.lock();\n        try {\n            while (size == elements.length)\n                notFull.await();\n            elements[tail] = e;\n            if (++tail == elements.length) {\n                tail = 0;\n            }\n            size++;\n            notEmpty.signal();\n\n        } catch (InterruptedException ex) {\n            ex.printStackTrace();\n        } finally {\n            lock.unlock();\n        }\n    }\n\n    public E take() {\n        lock.lock();\n        E e = null;\n        try {\n            while (size == 0) {\n                notEmpty.await();\n            }\n            e = (E) elements[head];\n            elements[head] = null;\n            if (++head == elements.length)\n                head = 0;\n            size--;\n            notFull.signal();\n\n        } catch (InterruptedException ex) {\n            ex.printStackTrace();\n        } finally {\n            lock.unlock();\n        }\n        return e;\n    }\n\n    public int size() {\n        lock.lock();\n        try {\n            return size;\n        } finally {\n            lock.unlock();\n        }\n    }\n}\n```\n\n## 解决哈希冲突的方法\n### 开放定址法\n从发生冲突的那个单元起，按照一定的次序，从哈希表中找到一个空闲的单元。然后把发生冲突的元素存入到该单元的一种方法。开放定址法需要的表长度要大于等于所需要存放的元素。\n\n在开放定址法中解决冲突的方法有：线行探查法、平方探查法、双散列函数探查法。\n\n开放定址法的缺点在于删除元素的时候不能真的删除，否则会引起查找错误，只能做一个特殊标记。只到有下个元素插入才能真正删除该元素。\n\n#### 线行探查法\n线行探查法是开放定址法中最简单的冲突处理方法，它从发生冲突的单元起，依次判断下一个单元是否为空，当达到最后一个单元时，再从表首依次判断。直到碰到空闲的单元或者探查完全部单元为止。\n\n可以参考csdn上flash对该方法的演示：\nhttp://student.zjzk.cn/course_ware/data_structure/web/flash/cz/kfdzh.swf\n\n#### 平方探查法\n平方探查法即是发生冲突时，用发生冲突的单元d[i], 加上 1²、 2²等。即d[i] + 1²，d[i] + 2², d[i] + 3²…直到找到空闲单元。\n\n在实际操作中，平方探查法不能探查到全部剩余的单元。不过在实际应用中，能探查到一半单元也就可以了。若探查到一半单元仍找不到一个空闲单元，表明此散列表太满，应该重新建立。\n\n#### 双散列函数探查法\n这种方法使用两个散列函数hl和h2。其中hl和前面的h一样，以关键字为自变量，产生一个0至m—l之间的数作为散列地址；h2也以关键字为自变量，产生一个l至m—1之间的、并和m互素的数(即m不能被该数整除)作为探查序列的地址增量(即步长)，探查序列的步长值是固定值l；对于平方探查法，探查序列的步长值是探查次数i的两倍减l；对于双散列函数探查法，其探查序列的步长值是同一关键字的另一散列函数的值。\n\n### 链地址法（拉链法）\n链接地址法的思路是将哈希值相同的元素构成一个同义词的单链表，并将单链表的头指针存放在哈希表的第i个单元中，查找、插入和删除主要在同义词链表中进行。链表法适用于经常进行插入和删除的情况。\n\n如下一组数字,(32、40、36、53、16、46、71、27、42、24、49、64)哈希表长度为13，哈希函数为H(key)=key%13,则链表法结果如下：\n```java\n0       \n1  -> 40 -> 27 -> 53 \n2\n3  -> 16 -> 42\n4\n5\n6  -> 32 -> 71\n7  -> 46\n8\n9\n10 -> 36 -> 49\n11 -> 24\n12 -> 64\n```\n注：在java中，链接地址法也是HashMap解决哈希冲突的方法之一，jdk1.7完全采用单链表来存储同义词，jdk1.8则采用了一种混合模式，对于链表长度大于8的，会转换为红黑树存储。\n\n### 再哈希法\n就是同时构造多个不同的哈希函数：\n\nHi = RHi(key)   i= 1,2,3 … k;\n\n当H1 = RH1(key)  发生冲突时，再用H2 = RH2(key) 进行计算，直到冲突不再产生，这种方法不易产生聚集，但是增加了计算时间。\n\n### 建立公共溢出区\n将哈希表分为公共表和溢出表，当溢出发生时，将所有溢出数据统一放到溢出区。\n\n## 排行榜设计\n### 基于数据库\n基于MySQL，order by\n\n缺点：\n- 速度慢\n### 基于Redis\n主要考察sort set 也就是zset\n\nzadd添加数据后，zrevrange获取排序后的排名\n\n### 类似于微信计数榜，如何设计不同用户看到的朋友圈的排行榜不一样\nkey的设计比较重要，比如aa用户和bb用户\n```shell\nzadd step:aa 1000 小明\nzadd step:bb 1000 小明\n```\n\n同理时间也可以通过key的设计解决\n```shell\nzadd step:aa:20210929 1000 小明\nzadd step:aa:20210929 1000 小明\n```\n\n但是上述设计会导致每个用户都有一个排行榜，存储的数据巨大，其实可以考虑只在用户查询时通过好友关系去生成\n\n那朋友圈排行榜的：微信头像、点赞数 怎么获取呢\n- 可以使用hmset hash储存对象，需要时通过zset储存的key去查询即可\n\n### 最近七天排行榜怎么弄\n前面我们说的都是每日排行榜。\n\n假设面试官要求我们提供一个最近七天、上一周、上一月、上个季度、这一年排行榜啥的，又该怎么搞呢？\n\n其实这还是在考察你对于 Redis 有序集合 API 的掌握程度。\n\n也就是这个 API：\n- zinterstore/zunionstore destination numkeys key [key ...] [weights weight [weight ...]] [aggregate sum|min|max] 获取交集/并集\n  - zinterstore/zunionstore其实就是交集/并集\n  - destination 将交集/并集的结果保存到这个键中\n  - numkeys 需要做交集/并集的集合的个数\n  - key [key ...] 具体参与交集/并集的集合\n  - weights weight [weight ...] 每个参与计算的集合的权重。在做交集/并集计算时，每个集合中的 member 会把自己的 score 乘以这个权重，默认为 1。\n  - aggregate sum|min|max 对于各个集合中的相同元素是 sum(求和)、min(取最小值)还是max(取最大值)，默认为 sum。\n\n比如现在有一些数据\n```shell\nzadd sport:ranking:why:20210222 43243 why 2341 mx 8764 les 42321 skr\nzadd sport:ranking:why:20210223 57632 why 24354 mx 4231 les 43512 skr 5341 jay\nzadd sport:ranking:why:20210224 10026 why 12344 mx 54312 les 34531 skr 43512 jay\nzadd sport:ranking:why:20210225 54312 why 32451 mx 23412 les 21341 skr 56321 jay\nzadd sport:ranking:why:20210226 3212 why 63421 mx 53652 les 45621 skr 5723 jay\nzadd sport:ranking:why:20210227 5462 why 10158 mx 30169 les 48858 skr 66079 jay\nzadd sport:ranking:why:20210228 43553 why 4451 mx 7431 les 9563 skr 8232 jay\n```\n![](../img/场景设计/redis排行榜7天.png)\n\n现在我们要求出最近 7 天的排行榜，就用下面这行命令，命令有点复杂，但是对着命令格式看，还是很清晰的：\n```shell\nzunionstore sport:ranking:why:last_seven_day 7 sport:ranking:why:20210222 sport:ranking:why:20210223 sport:ranking:why:20210224 sport:ranking:why:20210225 sport:ranking:why:20210226 sport:ranking:why:20210227 sport:ranking:why:20210228 weights 1 1 1 1 1 1 1 aggregate sum\n```\n![](../img/场景设计/redis排行榜7天2.png)\n\n上面用的是并集，如果我们的要求是对最近 7 天，每天都上传运动数据的人进行排序，就用交集来算。\n\n命令和上面的一致，只是把 zunionstore 修改为 zinterstore 即可。\n\n另外为了有对比，合并之后的队列名称也修改一下，命令如下：\n```shell\nzinterstore sport:ranking:why:last_seven_day_zinterstore 7 sport:ranking:why:20210222 sport:ranking:why:20210223 sport:ranking:why:20210224 sport:ranking:why:20210225 sport:ranking:why:20210226 sport:ranking:why:20210227 sport:ranking:why:20210228 weights 1 1 1 1 1 1 1 aggregate sum\n```\n![](../img/场景设计/redis排行榜最近7天.png)\n\n知道最近 7 天的做法了，我们又有每一天数据，上一周、上一月、上个季度、这一年排行榜啥的不都是这个套路吗\n\n### 亿级用户排行榜\n\n#### 按段位分桶\n由于数据量比较大，所以需要类似于分成一个个小文件的思想去统计每一部分的数据\n\n比如游戏里的段位，统计国服前100，可以把王者、大师、砖石、铂金、黄金、白银、青铜 分为不同的桶，每个分段的人在不同的桶里(假设还是用zset存储用户的段位)\n那计算全服排名即可先计算在某个段位桶的排名x,再获取这个段位桶前的所有桶的大小y1,y2...yn，排名就计算出来了x+y1+y2....yn\n##### 计算top100\n分桶后，直接在段位最大的桶里计算top100即可\n#### 按积分分桶\n[0-5000] [5001-10000] .....[10000000-x]\n\n这种可能会出现热点问题，比如处于0-5000区间的人会非常多（可能很多人都是没有打排位）用户的落点其实并不是均匀的，那就需要通过其他预测算法去预估每个区间的人数了\n\n\n\n\n\n# 参考文章\n- https://segmentfault.com/a/1190000020172463\n- https://www.jianshu.com/p/dc1e5091a0d8\n- https://juejin.cn/post/6844903866681524238\n- https://www.jianshu.com/p/7d6853140e13\n- https://blog.csdn.net/weixin_37704921/article/details/89212111\n- https://juejin.cn/post/7007382308667785253\n- https://tech.meituan.com/2017/04/21/mt-leaf.html\n- https://github.com/Snailclimb/JavaGuide/blob/master/docs/system-design/micro-service/%E5%88%86%E5%B8%83%E5%BC%8Fid%E7%94%9F%E6%88%90%E6%96%B9%E6%A1%88%E6%80%BB%E7%BB%93.md\n- https://juejin.cn/post/6844903486895865864\n- https://blog.csdn.net/Kurozaki_Kun/article/details/80877612\n- https://cloud.tencent.com/developer/article/1672781\n- https://www.cnblogs.com/thisiswhy/p/14470861.html\n- https://zhuanlan.zhihu.com/p/347257359\n\n\n"
  },
  {
    "path": "大数据/Flink/Flink.md",
    "content": ""
  },
  {
    "path": "大数据/Hadoop/Hadoop.md",
    "content": "# hadoop\n\n## Hadoop分布式文件系统：架构和设计\nHadoop分布式文件系统(HDFS)被设计成适合运行在通用硬件(commodity hardware)上的分布式文件系统。它和现有的分布式文件系统有很多共同点。但同时，它和其他的分布式文件系统的区别也是很明显的。HDFS是一个高度容错性的系统，适合部署在廉价的机器上。HDFS能提供高吞吐量的数据访问，非常适合大规模数据集上的应用。HDFS放宽了一部分POSIX约束，来实现流式读取文件系统数据的目的。HDFS在最开始是作为Apache Nutch搜索引擎项目的基础架构而开发的。HDFS是Apache Hadoop Core项目的一部分。这个项目的地址是https://hadoop.apache.org/core/。\n\n## Namenode 和 Datanode\nHDFS采用master/slave架构。一个HDFS集群是由一个Namenode和一定数目的Datanodes组成。Namenode是一个中心服务器，负责管理文件系统的名字空间(namespace)以及客户端对文件的访问。集群中的Datanode一般是一个节点一个，负责管理它所在节点上的存储。HDFS暴露了文件系统的名字空间，用户能够以文件的形式在上面存储数据。从内部看，一个文件其实被分成一个或多个数据块，这些块存储在一组Datanode上。Namenode执行文件系统的名字空间操作，比如打开、关闭、重命名文件或目录。它也负责确定数据块到具体Datanode节点的映射。Datanode负责处理文件系统客户端的读写请求。在Namenode的统一调度下进行数据块的创建、删除和复制。\n\n![hdfs架构.png](..%2F..%2Fimg%2Fbigdata%2Fhadoop%2Fhdfs%E6%9E%B6%E6%9E%84.png)\n\nNamenode和Datanode被设计成可以在普通的商用机器上运行。这些机器一般运行着GNU/Linux操作系统(OS)。HDFS采用Java语言开发，因此任何支持Java的机器都可以部署Namenode或Datanode。由于采用了可移植性极强的Java语言，使得HDFS可以部署到多种类型的机器上。一个典型的部署场景是一台机器上只运行一个Namenode实例，而集群中的其它机器分别运行一个Datanode实例。这种架构并不排斥在一台机器上运行多个Datanode，只不过这样的情况比较少见。\n集群中单一Namenode的结构大大简化了系统的架构。Namenode是所有HDFS元数据的仲裁者和管理者，这样，用户数据永远不会流过Namenode。\n\n## 文件系统的名字空间 (namespace)\nHDFS支持传统的层次型文件组织结构。用户或者应用程序可以创建目录，然后将文件保存在这些目录里。文件系统名字空间的层次结构和大多数现有的文件系统类似：用户可以创建、删除、移动或重命名文件。当前，HDFS不支持用户磁盘配额和访问权限控制，也不支持硬链接和软链接。但是HDFS架构并不妨碍实现这些特性。\nNamenode负责维护文件系统的名字空间，任何对文件系统名字空间或属性的修改都将被Namenode记录下来。应用程序可以设置HDFS保存的文件的副本数目。文件副本的数目称为文件的副本系数，这个信息也是由Namenode保存的。\n\n## 数据复制\nHDFS被设计成能够在一个大集群中跨机器可靠地存储超大文件。它将每个文件存储成一系列的数据块，除了最后一个，所有的数据块都是同样大小的。为了容错，文件的所有数据块都会有副本。每个文件的数据块大小和副本系数都是可配置的。应用程序可以指定某个文件的副本数目。副本系数可以在文件创建的时候指定，也可以在之后改变。HDFS中的文件都是一次性写入的，并且严格要求在任何时候只能有一个写入者。\nNamenode全权管理数据块的复制，它周期性地从集群中的每个Datanode接收心跳信号和块状态报告(Blockreport)。接收到心跳信号意味着该Datanode节点工作正常。块状态报告包含了一个该Datanode上所有数据块的列表。\n\n![namenode.png](..%2F..%2Fimg%2Fbigdata%2Fhadoop%2Fnamenode.png)\n\n### 副本存放: 最最开始的一步\n副本的存放是HDFS可靠性和性能的关键。优化的副本存放策略是HDFS区分于其他大部分分布式文件系统的重要特性。这种特性需要做大量的调优，并需要经验的积累。HDFS采用一种称为机架感知(rack-aware)的策略来改进数据的可靠性、可用性和网络带宽的利用率。目前实现的副本存放策略只是在这个方向上的第一步。实现这个策略的短期目标是验证它在生产环境下的有效性，观察它的行为，为实现更先进的策略打下测试和研究的基础。\n\n大型HDFS实例一般运行在跨越多个机架的计算机组成的集群上，不同机架上的两台机器之间的通讯需要经过交换机。在大多数情况下，同一个机架内的两台机器间的带宽会比不同机架的两台机器间的带宽大。\n\n通过一个机架感知的过程，Namenode可以确定每个Datanode所属的机架id。一个简单但没有优化的策略就是将副本存放在不同的机架上。这样可以有效防止当整个机架失效时数据的丢失，并且允许读数据的时候充分利用多个机架的带宽。这种策略设置可以将副本均匀分布在集群中，有利于当组件失效情况下的负载均衡。但是，因为这种策略的一个写操作需要传输数据块到多个机架，这增加了写的代价。\n\n在大多数情况下，副本系数是3，HDFS的存放策略是将一个副本存放在本地机架的节点上，一个副本放在同一机架的另一个节点上，最后一个副本放在不同机架的节点上。这种策略减少了机架间的数据传输，这就提高了写操作的效率。机架的错误远远比节点的错误少，所以这个策略不会影响到数据的可靠性和可用性。于此同时，因为数据块只放在两个（不是三个）不同的机架上，所以此策略减少了读取数据时需要的网络传输总带宽。在这种策略下，副本并不是均匀分布在不同的机架上。三分之一的副本在一个节点上，三分之二的副本在一个机架上，其他副本均匀分布在剩下的机架中，这一策略在不损害数据可靠性和读取性能的情况下改进了写的性能。\n\n当前，这里介绍的默认副本存放策略正在开发的过程中。\n\n### 副本选择\n为了降低整体的带宽消耗和读取延时，HDFS会尽量让读取程序读取离它最近的副本。如果在读取程序的同一个机架上有一个副本，那么就读取该副本。如果一个HDFS集群跨越多个数据中心，那么客户端也将首先读本地数据中心的副本。\n### 安全模式\nNamenode启动后会进入一个称为安全模式的特殊状态。处于安全模式的Namenode是不会进行数据块的复制的。Namenode从所有的 Datanode接收心跳信号和块状态报告。块状态报告包括了某个Datanode所有的数据块列表。每个数据块都有一个指定的最小副本数。当Namenode检测确认某个数据块的副本数目达到这个最小值，那么该数据块就会被认为是副本安全(safely replicated)的；在一定百分比（这个参数可配置）的数据块被Namenode检测确认是安全之后（加上一个额外的30秒等待时间），Namenode将退出安全模式状态。接下来它会确定还有哪些数据块的副本没有达到指定数目，并将这些数据块复制到其他Datanode上。\n## 文件系统元数据的持久化\nNamenode上保存着HDFS的名字空间。对于任何对文件系统元数据产生修改的操作，Namenode都会使用一种称为EditLog的事务日志记录下来。例如，在HDFS中创建一个文件，Namenode就会在Editlog中插入一条记录来表示；同样地，修改文件的副本系数也将往Editlog插入一条记录。Namenode在本地操作系统的文件系统中存储这个Editlog。整个文件系统的名字空间，包括数据块到文件的映射、文件的属性等，都存储在一个称为FsImage的文件中，这个文件也是放在Namenode所在的本地文件系统上。\nNamenode在内存中保存着整个文件系统的名字空间和文件数据块映射(Blockmap)的映像。这个关键的元数据结构设计得很紧凑，因而一个有4G内存的Namenode足够支撑大量的文件和目录。当Namenode启动时，它从硬盘中读取Editlog和FsImage，将所有Editlog中的事务作用在内存中的FsImage上，并将这个新版本的FsImage从内存中保存到本地磁盘上，然后删除旧的Editlog，因为这个旧的Editlog的事务都已经作用在FsImage上了。这个过程称为一个检查点(checkpoint)。在当前实现中，检查点只发生在Namenode启动时，在不久的将来将实现支持周期性的检查点。\nDatanode将HDFS数据以文件的形式存储在本地的文件系统中，它并不知道有关HDFS文件的信息。它把每个HDFS数据块存储在本地文件系统的一个单独的文件中。Datanode并不在同一个目录创建所有的文件，实际上，它用试探的方法来确定每个目录的最佳文件数目，并且在适当的时候创建子目录。在同一个目录中创建所有的本地文件并不是最优的选择，这是因为本地文件系统可能无法高效地在单个目录中支持大量的文件。当一个Datanode启动时，它会扫描本地文件系统，产生一个这些本地文件对应的所有HDFS数据块的列表，然后作为报告发送到Namenode，这个报告就是块状态报告。\n## 通讯协议\n所有的HDFS通讯协议都是建立在TCP/IP协议之上。客户端通过一个可配置的TCP端口连接到Namenode，通过ClientProtocol协议与Namenode交互。而Datanode使用DatanodeProtocol协议与Namenode交互。一个远程过程调用(RPC)模型被抽象出来封装ClientProtocol和Datanodeprotocol协议。在设计上，Namenode不会主动发起RPC，而是响应来自客户端或 Datanode 的RPC请求。\n## 健壮性\nHDFS的主要目标就是即使在出错的情况下也要保证数据存储的可靠性。常见的三种出错情况是：Namenode出错, Datanode出错和网络割裂(network partitions)。\n### 磁盘数据错误，心跳检测和重新复制\n每个Datanode节点周期性地向Namenode发送心跳信号。网络割裂可能导致一部分Datanode跟Namenode失去联系。Namenode通过心跳信号的缺失来检测这一情况，并将这些近期不再发送心跳信号Datanode标记为宕机，不会再将新的IO请求发给它们。任何存储在宕机Datanode上的数据将不再有效。Datanode的宕机可能会引起一些数据块的副本系数低于指定值，Namenode不断地检测这些需要复制的数据块，一旦发现就启动复制操作。在下列情况下，可能需要重新复制：某个Datanode节点失效，某个副本遭到损坏，Datanode上的硬盘错误，或者文件的副本系数增大。\n### 集群均衡\nHDFS的架构支持数据均衡策略。如果某个Datanode节点上的空闲空间低于特定的临界点，按照均衡策略系统就会自动地将数据从这个Datanode移动到其他空闲的Datanode。当对某个文件的请求突然增加，那么也可能启动一个计划创建该文件新的副本，并且同时重新平衡集群中的其他数据。这些均衡策略目前还没有实现。\n### 数据完整性\n从某个Datanode获取的数据块有可能是损坏的，损坏可能是由Datanode的存储设备错误、网络错误或者软件bug造成的。HDFS客户端软件实现了对HDFS文件内容的校验和(checksum)检查。当客户端创建一个新的HDFS文件，会计算这个文件每个数据块的校验和，并将校验和作为一个单独的隐藏文件保存在同一个HDFS名字空间下。当客户端获取文件内容后，它会检验从Datanode获取的数据跟相应的校验和文件中的校验和是否匹配，如果不匹配，客户端可以选择从其他Datanode获取该数据块的副本。\n### 元数据磁盘错误\nFsImage和Editlog是HDFS的核心数据结构。如果这些文件损坏了，整个HDFS实例都将失效。因而，Namenode可以配置成支持维护多个FsImage和Editlog的副本。任何对FsImage或者Editlog的修改，都将同步到它们的副本上。这种多副本的同步操作可能会降低Namenode每秒处理的名字空间事务数量。然而这个代价是可以接受的，因为即使HDFS的应用是数据密集的，它们也非元数据密集的。当Namenode重启的时候，它会选取最近的完整的FsImage和Editlog来使用。\nNamenode是HDFS集群中的单点故障(single point of failure)所在。如果Namenode机器故障，是需要手工干预的。目前，自动重启或在另一台机器上做Namenode故障转移的功能还没实现。\n### 快照\n快照支持某一特定时刻的数据的复制备份。利用快照，可以让HDFS在数据损坏时恢复到过去一个已知正确的时间点。HDFS目前还不支持快照功能，但计划在将来的版本进行支持。\n## 数据组织\n### 数据块\nHDFS被设计成支持大文件，适用HDFS的是那些需要处理大规模的数据集的应用。这些应用都是只写入数据一次，但却读取一次或多次，并且读取速度应能满足流式读取的需要。HDFS支持文件的“一次写入多次读取”语义。一个典型的数据块大小是64MB。因而，HDFS中的文件总是按照64M被切分成不同的块，每个块尽可能地存储于不同的Datanode中。\n### Staging\n客户端创建文件的请求其实并没有立即发送给Namenode，事实上，在刚开始阶段HDFS客户端会先将文件数据缓存到本地的一个临时文件。应用程序的写操作被透明地重定向到这个临时文件。当这个临时文件累积的数据量超过一个数据块的大小，客户端才会联系Namenode。Namenode将文件名插入文件系统的层次结构中，并且分配一个数据块给它。然后返回Datanode的标识符和目标数据块给客户端。接着客户端将这块数据从本地临时文件上传到指定的Datanode上。当文件关闭时，在临时文件中剩余的没有上传的数据也会传输到指定的Datanode上。然后客户端告诉Namenode文件已经关闭。此时Namenode才将文件创建操作提交到日志里进行存储。如果Namenode在文件关闭前宕机了，则该文件将丢失。\n上述方法是对在HDFS上运行的目标应用进行认真考虑后得到的结果。这些应用需要进行文件的流式写入。如果不采用客户端缓存，由于网络速度和网络堵塞会对吞估量造成比较大的影响。这种方法并不是没有先例的，早期的文件系统，比如AFS，就用客户端缓存来提高性能。为了达到更高的数据上传效率，已经放松了POSIX标准的要求。\n### 流水线复制\n当客户端向HDFS文件写入数据的时候，一开始是写到本地临时文件中。假设该文件的副本系数设置为3，当本地临时文件累积到一个数据块的大小时，客户端会从Namenode获取一个Datanode列表用于存放副本。然后客户端开始向第一个Datanode传输数据，第一个Datanode一小部分一小部分(4 KB)地接收数据，将每一部分写入本地仓库，并同时传输该部分到列表中第二个Datanode节点。第二个Datanode也是这样，一小部分一小部分地接收数据，写入本地仓库，并同时传给第三个Datanode。最后，第三个Datanode接收数据并存储在本地。因此，Datanode能流水线式地从前一个节点接收数据，并在同时转发给下一个节点，数据以流水线的方式从前一个Datanode复制到下一个。\n## 可访问性\nHDFS给应用提供了多种访问方式。用户可以通过Java API接口访问，也可以通过C语言的封装API访问，还可以通过浏览器的方式访问HDFS中的文件。通过WebDAV协议访问的方式正在开发中。\n### DFSShell\nHDFS以文件和目录的形式组织用户数据。它提供了一个命令行的接口(DFSShell)让用户与HDFS中的数据进行交互。命令的语法和用户熟悉的其他shell(例如 bash, csh)工具类似。下面是一些动作/命令的示例：\n\n| 动作                                | 命令                               |\n|-------------------------------------|------------------------------------|\n| 创建一个名为 /foodir 的目录         | `bin/hadoop dfs -mkdir /foodir`     |\n| 再次创建一个名为 /foodir 的目录     | `bin/hadoop dfs -mkdir /foodir`     |\n| 查看名为 /foodir/myfile.txt 的文件内容 | `bin/hadoop dfs -cat /foodir/myfile.txt` |\n\n动作命令创建一个名为/foodir的目录bin/hadoop dfs -mkdir /foodir创建一个名为/foodir的目录bin/hadoop dfs -mkdir /foodir查看名为/foodir/myfile.txt的文件内容bin/hadoop dfs -cat /foodir/myfile.txt\nDFSShell 可以用在那些通过脚本语言和文件系统进行交互的应用程序上。\n### DFSAdmin\nDFSAdmin 命令用来管理HDFS集群。这些命令只有HDSF的管理员才能使用。下面是一些动作/命令的示例：\n动作命令将集群置于安全模式bin/hadoop dfsadmin -safemode enter显示Datanode列表bin/hadoop dfsadmin -report使Datanode节点datanodename退役bin/hadoop dfsadmin -decommission datanodename\n\n| 动作                         | 命令                                      |\n|------------------------------|-------------------------------------------|\n| 将集群置于安全模式           | `bin/hadoop dfsadmin -safemode enter`     |\n| 显示Datanode列表              | `bin/hadoop dfsadmin -report`             |\n| 使Datanode节点 datanodename 退役 | `bin/hadoop dfsadmin -decommission datanodename` |\n\n### 浏览器接口\n一个典型的HDFS安装会在一个可配置的TCP端口开启一个Web服务器用于暴露HDFS的名字空间。用户可以用浏览器来浏览HDFS的名字空间和查看文件的内容。\n## 存储空间回收\n### 文件的删除和恢复\n当用户或应用程序删除某个文件时，这个文件并没有立刻从HDFS中删除。实际上，HDFS会将这个文件重命名转移到/trash目录。只要文件还在/trash目录中，该文件就可以被迅速地恢复。文件在/trash中保存的时间是可配置的，当超过这个时间时，Namenode就会将该文件从名字空间中删除。删除文件会使得该文件相关的数据块被释放。注意，从用户删除文件到HDFS空闲空间的增加之间会有一定时间的延迟。\n只要被删除的文件还在/trash目录中，用户就可以恢复这个文件。如果用户想恢复被删除的文件，他/她可以浏览/trash目录找回该文件。/trash目录仅仅保存被删除文件的最后副本。/trash目录与其他的目录没有什么区别，除了一点：在该目录上HDFS会应用一个特殊策略来自动删除文件。目前的默认策略是删除/trash中保留时间超过6小时的文件。将来，这个策略可以通过一个被良好定义的接口配置。\n### 减少副本系数\n当一个文件的副本系数被减小后，Namenode会选择过剩的副本删除。下次心跳检测时会将该信息传递给Datanode。Datanode遂即移除相应的数据块，集群中的空闲空间加大。同样，在调用setReplication API结束和集群中空闲空间增加间会有一定的延迟。\n## 参考资料\n- HDFS Java API: https://hadoop.apache.org/core/docs/current/api/\n- HDFS 源代码: https://hadoop.apache.org/core/version_control.html\n"
  },
  {
    "path": "大数据/Hadoop/hadoop面试问题.md",
    "content": "\n# 什么是Hadoop？它解决了什么问题？\nHadoop是一个开源的分布式计算框架，设计用于存储和处理大规模数据集。它通过将数据分布在多个廉价计算节点上并行处理来解决传统单机系统无法处理的大数据问题。Hadoop的核心优势在于它的扩展性和容错性，可以处理数百TB甚至PB级的数据。\n\n# Hadoop的核心组件是什么？简要描述它们的作用。\nHadoop的核心组件包括：\n\n- HDFS (Hadoop Distributed File System)：主要用于文件的分布式存储，适合存储大规模数据集，具备高容错性和高吞吐量的特点。\n- MapReduce：Hadoop的数据处理模型，通过分布式计算框架实现大规模数据集的并行处理。\n- YARN (Yet Another Resource Negotiator)：资源管理和调度系统，负责集群资源的管理和任务调度。\n\n# 什么是HDFS（Hadoop分布式文件系统）？它的特点是什么？\nHDFS是Hadoop的分布式文件系统，设计用于大规模数据存储和高吞吐量的文件访问。它的主要特点有：\n\n- 高可扩展性：能水平扩展以处理大量数据。\n- 高容错性：通过数据块副本机制确保数据的可靠性。\n- 高吞吐量：优化了大数据量传输，使得读写操作高效。\n\n# 解释MapReduce的工作原理\nMapReduce是Hadoop的核心计算框架，分为两个主要阶段：\n\n- Map阶段：输入数据被分割为小片段，由多个Mapper进行并行处理，每个Mapper生成中间键值对。\n- Reduce阶段：将中间结果按键合并，由Reducer进行处理，生成最终结果。\n\n# Mapper和Reducer的作用是什么？它们之间的交互是怎样的？\n- Mapper：处理输入数据片段，将其转换为中间的键值对。\n- Reducer：处理来自Mapper的键值对，将具有相同键的所有值汇总并生成最终输出。\n- 交互：Mapper的输出被分发给Reducer，并在此过程中进行排序和分组，然后Reducer对其进行处理。\n\n# 什么是Combiner？它的作用是什么？\nCombiner是一种优化器，介于Mapper和Reducer之间，用于在Mapper本地进行部分汇总，减少数据传输量，提高MapReduce任务效率。\n\n# YARN是什么？它与经典MapReduce相比有什么优势？\nYARN是Hadoop的资源管理框架，负责集群资源管理和任务调度。与经典MapReduce相比，YARN的优势在于：\n\n- 更好的资源利用：支持不同类型的计算任务，增强资源调度和管理的灵活性。\n- 改进的扩展性：更易扩展到更多节点。\n- 多功能性：允许在同一集群上运行多种计算框架。\n\n# 为什么Hadoop 2版本引入了YARN？\nHadoop 2版本引入了YARN，以解决Hadoop 1.x版本中MapReduce的缺陷。YARN解决了以下问题：\n- 降低资源管理复杂度：YARN将资源管理与计算框架解耦，使得资源管理更加灵活。\n- 提升性能：YARN支持动态资源分配，可以动态调整计算资源的使用。\n- 提升容错性：YARN支持容错性，可以自动恢复丢失的节点。\n- 提升扩展性：YARN支持多种计算框架，可以轻松集成其他计算框架。\n- 降低学习成本：YARN的API与MapReduce API相似，使得用户可以快速适应新的计算框架。\n- 降低维护成本：YARN可以自动管理资源，减少运维成本。\n- 降低开发成本：YARN可以支持多种计算框架，使得开发人员可以快速适应新的计算框架。\n- 降低成本：YARN可以支持多种计算框架，使得成本降低。\n- 降低风险：YARN可以支持多种计算框架，使得风险降低。\n\n# 什么是Apache Hive？它的作用是什么？\nApache Hive是一个开源的数据仓库工具，用于查询和分析存储在Hadoop上的数据。它提供了SQL接口，使得用户可以像操作本地数据库一样操作Hadoop上的数据。\n\n# Apache Spark和Hadoop的关系是什么？\nApache Spark是一个快且通用的集群计算系统，能够与Hadoop生态系统集成，特别是可以与HDFS等存储系统配合使用。Spark相比于传统MapReduce，提供了更高效率的数据处理和更丰富的API，支持批处理、流处理和机器学习等多种任务。\n\n# 什么是Apache HBase？它与HDFS有什么区别？\nApache HBase是一个分布式的NoSQL数据库，基于Hadoop生态系统，用于实时读写访问超大规模数据集，类似于Google的Bigtable。与HDFS不同，HBase支持快速的读写操作和随机访问，而HDFS主要用于批处理和大数据分析。\n\n# 如何对Hadoop作业进行性能优化？\n\n- 数据本地化：确保计算发生在数据所在的地方，减少数据传输。\n- 合理设置Mapper和Reducer数量：根据输入数据量和任务需求进行调优。\n- 调优Mapper和Reducer的内存和CPU配置：确保资源使用最经济高效。\n- 使用Combiner：减少数据传输量，提高效率。\n- 调优HDFS配置：通过优化块大小和副本数量提高I/O性能。\n\n# 什么是数据本地性？为什么它对Hadoop作业执行效率很重要？\n数据本地性指任务在数据存储的节点上运行，从而减少数据传输量，提高作业效率。在Hadoop中，计算节点和数据存储节点往往是同一组物理节点，因此按照数据本地性原则，可以减少网络I/O，提高性能。\n\n# 如何避免数据倾斜的问题？\n\n- 合理设计键分区：确保数据均匀分布到各个Reducer。\n- 使用Combiner：在Mapper端进行部分聚合，减少数据倾斜。\n- 分批处理：对于超级节点的数据，分多批次进行处理。\n- 调优分区策略：通过自定义分区函数来防止数据集中到少数Reducer上。\n\n# 你有使用Hadoop处理过哪些大数据问题？可以具体描述一下你的经验吗？\n在实际工作中，我使用Hadoop处理过例如日志分析、用户行为分析、数据ETL（提取、转换、加载）等大数据问题。具体经验包括：\n\n- 日志分析：通过MapReduce程序解析和归档海量服务器日志，提取有用信息如错误日志和访问日志。\n- 用户行为分析：使用Hive和Pig从社交网络数据中提取用户行为模式，生成报告供商业决策使用。\n- 数据ETL：将异构数据源的数据导入HDFS，进行清洗、转换并存储，为后续的数据分析做好准备。\n\n# 你如何处理处理Hadoop集群中的故障？这个过程中遇到过什么挑战？\n\n处理Hadoop集群中的故障包括以下几个步骤：\n\n- 监控和报警：通过使用Ganglia、Nagios等工具进行实时监控，设置告警机制及时发现故障。\n- 日志分析：深入分析Hadoop的各类日志（HDFS、YARN、MapReduce）找到故障原因。\n- 节点恢复和替换：对失效节点进行修复或替换，重新启动相关服务。\n- 数据恢复：使用HDFS的副本机制恢复丢失的数据块，确保数据完整性。\n- 遇到的挑战包括硬件故障导致的数据丢失、紧急情况下快速排查问题的压力以及协调多个组件之间的兼容性和稳定性。\n\n## Hadoop面试题（一）\n\n### 1、集群的最主要瓶颈\n&emsp; 磁盘IO\n\n### 2、Hadoop运行模式\n&emsp; 单机版、伪分布式模式、完全分布式模式\n\n### 3、Hadoop生态圈的组件并做简要描述\n&emsp; 1）Zookeeper：是一个开源的分布式应用程序协调服务,基于zookeeper可以实现同步服务，配置维护，命名服务。  \n&emsp; 2）Flume：一个高可用的，高可靠的，分布式的海量日志采集、聚合和传输的系统。   \n&emsp; 3）Hbase：是一个分布式的、面向列的开源数据库, 利用Hadoop HDFS作为其存储系统。   \n&emsp; 4）Hive：基于Hadoop的一个数据仓库工具，可以将结构化的数据档映射为一张数据库表，并提供简单的sql 查询功能，可以将sql语句转换为MapReduce任务进行运行。   \n&emsp; 5）Sqoop：将一个关系型数据库中的数据导进到Hadoop的 HDFS中，也可以将HDFS的数据导进到关系型数据库中。\n\n### 4、解释“hadoop”和“hadoop 生态系统”两个概念\n&emsp; Hadoop是指Hadoop框架本身；hadoop生态系统，不仅包含hadoop，还包括保证hadoop框架正常高效运行其他框架，比如zookeeper、Flume、Hbase、Hive、Sqoop等辅助框架。\n\n### 5、请列出正常工作的Hadoop集群中Hadoop都分别需要启动哪些进程，它们的作用分别是什么?\n&emsp; 1）NameNode：它是hadoop中的主服务器，管理文件系统名称空间和对集群中存储的文件的访问，保存有metadate。  \n&emsp; 2）SecondaryNameNode：它不是namenode的冗余守护进程，而是提供周期检查点和清理任务。帮助NN合并editslog，减少NN启动时间。  \n&emsp; 3）DataNode：它负责管理连接到节点的存储（一个集群中可以有多个节点）。每个存储数据的节点运行一个datanode守护进程。  \n&emsp; 4）ResourceManager（JobTracker）：JobTracker负责调度DataNode上的工作。每个DataNode有一个TaskTracker，它们执行实际工作。  \n&emsp; 5）NodeManager：（TaskTracker）执行任务。  \n&emsp; 6）DFSZKFailoverController：高可用时它负责监控NN的状态，并及时的把状态信息写入ZK。它通过一个独立线程周期性的调用NN上的一个特定接口来获取NN的健康状态。FC也有选择谁作为Active NN的权利，因为最多只有两个节点，目前选择策略还比较简单（先到先得，轮换）。  \n&emsp; 7）JournalNode：高可用情况下存放namenode的editlog文件。  \n\n\n## Hadoop面试题总结（二）——HDFS\n\n### 1、 HDFS 中的 block 默认保存几份？\n&emsp; 默认保存3份\n\n### 2、HDFS 默认 BlockSize 是多大？\n&emsp; 默认64MB\n\n### 3、负责HDFS数据存储的是哪一部分？\n&emsp; DataNode负责数据存储\n\n### 4、SecondaryNameNode的目的是什么？\n&emsp; 他的目的使帮助NameNode合并编辑日志，减少NameNode 启动时间\n\n### 5、文件大小设置，增大有什么影响？\n&emsp; HDFS中的文件在物理上是分块存储（block），块的大小可以通过配置参数( dfs.blocksize)来规定，默认大小在hadoop2.x版本中是128M，老版本中是64M。  \n&emsp; **思考：为什么块的大小不能设置的太小，也不能设置的太大？**  \n&emsp; &emsp; HDFS的块比磁盘的块大，其目的是为了最小化寻址开销。如果块设置得足够大，从磁盘传输数据的时间会明显大于定位这个块开始位置所需的时间。\n因而，**传输一个由多个块组成的文件的时间取决于磁盘传输速率**。  \n&emsp; 如果寻址时间约为10ms，而传输速率为100MB/s，为了使寻址时间仅占传输时间的1%，我们要将块大小设置约为100MB。默认的块大小128MB。  \n&emsp; 块的大小：10ms×100×100M/s = 100M，如图  \n<img src=\"https://github.com/Dr11ft/BigDataGuide/blob/master/Pics/Hadoop%E9%9D%A2%E8%AF%95%E9%A2%98Pics/HDFS%E5%9D%97.png\"/>  \n&emsp; 增加文件块大小，需要增加磁盘的传输速率。\n\n### 6、hadoop的块大小，从哪个版本开始是128M\n&emsp; Hadoop1.x都是64M，hadoop2.x开始都是128M。\n\n### 7、HDFS的存储机制（☆☆☆☆☆）\n&emsp; HDFS存储机制，包括HDFS的**写入数据过程**和**读取数据过程**两部分  \n&emsp; **HDFS写数据过程**\n<p align=\"center\">\n<img src=\"https://github.com/Dr11ft/BigDataGuide/blob/master/Pics/Hadoop%E9%9D%A2%E8%AF%95%E9%A2%98Pics/HDFS%E5%86%99%E6%95%B0%E6%8D%AE%E6%B5%81%E7%A8%8B.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n&emsp; 1）客户端通过Distributed FileSystem模块向NameNode请求上传文件，NameNode检查目标文件是否已存在，父目录是否存在。  \n&emsp; 2）NameNode返回是否可以上传。  \n&emsp; 3）客户端请求第一个 block上传到哪几个datanode服务器上。  \n&emsp; 4）NameNode返回3个datanode节点，分别为dn1、dn2、dn3。  \n&emsp; 5）客户端通过FSDataOutputStream模块请求dn1上传数据，dn1收到请求会继续调用dn2，然后dn2调用dn3，将这个通信管道建立完成。  \n&emsp; 6）dn1、dn2、dn3逐级应答客户端。  \n&emsp; 7）客户端开始往dn1上传第一个block（先从磁盘读取数据放到一个本地内存缓存），以packet为单位，dn1收到一个packet就会传给dn2，dn2传给dn3；\ndn1每传一个packet会放入一个应答队列等待应答。  \n&emsp; 8）当一个block传输完成之后，客户端再次请求NameNode上传第二个block的服务器。（重复执行3-7步）。\n\n&emsp; **HDFS读数据过程**\n<p align=\"center\">\n<img src=\"https://github.com/Dr11ft/BigDataGuide/blob/master/Pics/Hadoop%E9%9D%A2%E8%AF%95%E9%A2%98Pics/HDFS%E8%AF%BB%E6%95%B0%E6%8D%AE%E6%B5%81%E7%A8%8B.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n&emsp; 1）客户端通过Distributed FileSystem向NameNode请求下载文件，NameNode通过查询元数据，找到文件块所在的DataNode地址。  \n&emsp; 2）挑选一台DataNode（就近原则，然后随机）服务器，请求读取数据。  \n&emsp; 3）DataNode开始传输数据给客户端（从磁盘里面读取数据输入流，以packet为单位来做校验）。  \n&emsp; 4）客户端以packet为单位接收，先在本地缓存，然后写入目标文件。\n\n### 8、secondary namenode工作机制（☆☆☆☆☆）\n<p align=\"center\">\n<img src=\"https://github.com/Dr11ft/BigDataGuide/blob/master/Pics/Hadoop%E9%9D%A2%E8%AF%95%E9%A2%98Pics/secondary%20namenode%E5%B7%A5%E4%BD%9C%E6%9C%BA%E5%88%B6.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n**1）第一阶段：NameNode启动**  \n&emsp; （1）第一次启动NameNode格式化后，创建fsimage和edits文件。如果不是第一次启动，直接加载编辑日志和镜像文件到内存。   \n&emsp; （2）客户端对元数据进行增删改的请求。   \n&emsp; （3）NameNode记录操作日志，更新滚动日志。   \n&emsp; （4）NameNode在内存中对数据进行增删改查。  \n**2）第二阶段：Secondary NameNode工作**  \n&emsp; （1）Secondary NameNode询问NameNode是否需要checkpoint。直接带回NameNode是否检查结果。  \n&emsp; （2）Secondary NameNode请求执行checkpoint。  \n&emsp; （3）NameNode滚动正在写的edits日志。  \n&emsp; （4）将滚动前的编辑日志和镜像文件拷贝到Secondary NameNode。  \n&emsp; （5）Secondary NameNode加载编辑日志和镜像文件到内存，并合并。  \n&emsp; （6）生成新的镜像文件fsimage.chkpoint。  \n&emsp; （7）拷贝fsimage.chkpoint到NameNode。  \n&emsp; （8）NameNode将fsimage.chkpoint重新命名成fsimage。\n\n### 9、NameNode与SecondaryNameNode 的区别与联系？（☆☆☆☆☆）\n**机制流程看第7题**  \n1）区别  \n&emsp; （1）NameNode负责管理整个文件系统的元数据，以及每一个路径（文件）所对应的数据块信息。  \n&emsp; （2）SecondaryNameNode主要用于定期合并命名空间镜像和命名空间镜像的编辑日志。  \n2）联系：  \n&emsp; （1）SecondaryNameNode中保存了一份和namenode一致的镜像文件（fsimage）和编辑日志（edits）。  \n&emsp; （2）在主namenode发生故障时（假设没有及时备份数据），可以从SecondaryNameNode恢复数据。\n\n### 10、HDFS组成架构（☆☆☆☆☆）\n<p align=\"center\">\n<img src=\"https://github.com/Dr11ft/BigDataGuide/blob/master/Pics/Hadoop%E9%9D%A2%E8%AF%95%E9%A2%98Pics/HDFS%E7%BB%84%E6%88%90%E6%9E%B6%E6%9E%84.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n架构主要由四个部分组成，分别为**HDFS Client、NameNode、DataNode和Secondary NameNode**。下面我们分别介绍这四个组成部分。  \n1）Client：就是客户端。       \n&emsp; （1）文件切分。文件上传HDFS的时候，Client将文件切分成一个一个的Block，然后进行存储；         \n&emsp; （2）与NameNode交互，获取文件的位置信息；  \n&emsp; （3）与DataNode交互，读取或者写入数据；      \n&emsp; （4）Client提供一些命令来管理HDFS，比如启动或者关闭HDFS；  \n&emsp; （5）Client可以通过一些命令来访问HDFS；  \n2）NameNode：就是Master，它是一个主管、管理者。  \n&emsp; （1）管理HDFS的名称空间；  \n&emsp; （2）管理数据块（Block）映射信息；  \n&emsp; （3）配置副本策略；  \n&emsp; （4）处理客户端读写请求。  \n3）DataNode：就是Slave。NameNode下达命令，DataNode执行实际的操作。  \n&emsp; （1）存储实际的数据块；  \n&emsp; （2）执行数据块的读/写操作。  \n4）Secondary NameNode：并非NameNode的热备。当NameNode挂掉的时候，它并不能马上替换NameNode并提供服务。  \n&emsp; （1）辅助NameNode，分担其工作量；  \n&emsp; （2）定期合并Fsimage和Edits，并推送给NameNode；  \n&emsp; （3）在紧急情况下，可辅助恢复NameNode。\n\n### 11、HAnamenode 是如何工作的? （☆☆☆☆☆）\n<p align=\"center\">\n<img src=\"https://github.com/Dr11ft/BigDataGuide/blob/master/Pics/Hadoop%E9%9D%A2%E8%AF%95%E9%A2%98Pics/HAnamenode%E5%B7%A5%E4%BD%9C%E6%9C%BA%E5%88%B6.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\nZKFailoverController主要职责  \n&emsp; 1）健康监测：周期性的向它监控的NN发送健康探测命令，从而来确定某个NameNode是否处于健康状态，如果机器宕机，心跳失败，那么zkfc就会标记它处于一个不健康的状态。  \n&emsp; 2）会话管理：如果NN是健康的，zkfc就会在zookeeper中保持一个打开的会话，如果NameNode同时还是Active状态的，那么zkfc还会在Zookeeper中占有一个类型为短暂类型的znode，当这个NN挂掉时，这个znode将会被删除，然后备用的NN，将会得到这把锁，升级为主NN，同时标记状态为Active。  \n&emsp; 3）当宕机的NN新启动时，它会再次注册zookeper，发现已经有znode锁了，便会自动变为Standby状态，如此往复循环，保证高可靠，需要注意，目前仅仅支持最多配置2个NN。  \n&emsp; 4）master选举：如上所述，通过在zookeeper中维持一个短暂类型的znode，来实现抢占式的锁机制，从而判断那个NameNode为Active状态\n\n## Hadoop面试题总结（三）——MapReduce\n\n### 1、谈谈Hadoop序列化和反序列化及自定义bean对象实现序列化?\n1）序列化和反序列化  \n&emsp; （1）序列化就是把内存中的对象，转换成字节序列（或其他数据传输协议）以便于存储（持久化）和网络传输。   \n&emsp; （2）反序列化就是将收到字节序列（或其他数据传输协议）或者是硬盘的持久化数据，转换成内存中的对象。  \n&emsp; （3）Java的序列化是一个重量级序列化框架（Serializable），一个对象被序列化后，会附带很多额外的信息（各种校验信息，header，继承体系等），不便于在网络中高效传输。所以，hadoop自己开发了一套序列化机制（Writable），精简、高效。  \n2）自定义bean对象要想序列化传输步骤及注意事项：  \n&emsp; （1）必须实现Writable接口  \n&emsp; （2）反序列化时，需要反射调用空参构造函数，所以必须有空参构造  \n&emsp; （3）重写序列化方法  \n&emsp; （4）重写反序列化方法  \n&emsp; （5）注意反序列化的顺序和序列化的顺序完全一致  \n&emsp; （6）要想把结果显示在文件中，需要重写toString()，且用\"\\t\"分开，方便后续用  \n&emsp; （7）如果需要将自定义的bean放在key中传输，则还需要实现comparable接口，因为mapreduce框中的shuffle过程一定会对key进行排序\n\n### 2、FileInputFormat切片机制（☆☆☆☆☆）\njob提交流程源码详解  \n&emsp; waitForCompletion()  \n&emsp; submit();  \n&emsp; // 1、建立连接  \n&emsp; &emsp; connect();   \n&emsp; &emsp; &emsp; // 1）创建提交job的代理  \n&emsp; &emsp; &emsp; new Cluster(getConfiguration());  \n&emsp; &emsp; &emsp; &emsp; // （1）判断是本地yarn还是远程  \n&emsp; &emsp; &emsp; &emsp; initialize(jobTrackAddr, conf);  \n&emsp; // 2、提交job  \n&emsp; submitter.submitJobInternal(Job.this, cluster)  \n&emsp; &emsp; // 1）创建给集群提交数据的Stag路径  \n&emsp; &emsp; Path jobStagingArea = JobSubmissionFiles.getStagingDir(cluster, conf);  \n&emsp; &emsp; // 2）获取jobid ，并创建job路径  \n&emsp; &emsp; JobID jobId = submitClient.getNewJobID();  \n&emsp; &emsp; // 3）拷贝jar包到集群  \n&emsp; &emsp; copyAndConfigureFiles(job, submitJobDir);  \n&emsp; &emsp; rUploader.uploadFiles(job, jobSubmitDir);  \n&emsp; &emsp; // 4）计算切片，生成切片规划文件  \n&emsp; &emsp; writeSplits(job, submitJobDir);  \n&emsp; &emsp; maps = writeNewSplits(job, jobSubmitDir);  \n&emsp; &emsp; input.getSplits(job);  \n&emsp; &emsp; // 5）向Stag路径写xml配置文件  \n&emsp; &emsp; writeConf(conf, submitJobFile);  \n&emsp; &emsp; conf.writeXml(out);  \n&emsp; &emsp; // 6）提交job,返回提交状态  \n&emsp; &emsp; status = submitClient.submitJob(jobId, submitJobDir.toString(), job.getCredentials());\n\n### 3、在一个运行的Hadoop 任务中，什么是InputSplit？（☆☆☆☆☆）\nFileInputFormat源码解析(input.getSplits(job))  \n（1）找到你数据存储的目录。  \n（2）开始遍历处理（规划切片）目录下的每一个文件。  \n（3）遍历第一个文件ss.txt。  \n&emsp; a）获取文件大小fs.sizeOf(ss.txt);。  \n&emsp; b）计算切片大小computeSliteSize(Math.max(minSize,Math.min(maxSize,blocksize)))=blocksize=128M。  \n&emsp; c）**默认情况下，切片大小=blocksize**。  \n&emsp; d）开始切，形成第1个切片：ss.txt—0:128M 第2个切片ss.txt—128:256M 第3个切片ss.txt—256M:300M（每次切片时，都要判断切完剩下的部分是否大于块的1.1倍，**不大于1.1倍就划分一块切片**）。   \n&emsp; e）将切片信息写到一个切片规划文件中。  \n&emsp; f）整个切片的核心过程在getSplit()方法中完成。  \n&emsp; g）数据切片只是在逻辑上对输入数据进行分片，并不会再磁盘上将其切分成分片进行存储。InputSplit只记录了分片的元数据信息，比如起始位置、长度以及所在的节点列表等。  \n&emsp; h）注意：block是HDFS上物理上存储的存储的数据，切片是对数据逻辑上的划分。  \n（4）**提交切片规划文件到yarn上，yarn上的MrAppMaster就可以根据切片规划文件计算开启maptask个数**。\n\n### 4、如何判定一个job的map和reduce的数量?\n1）map数量  \n&emsp; splitSize=max{minSize,min{maxSize,blockSize}}  \n&emsp; map数量由处理的数据分成的block数量决定default_num = total_size / split_size;  \n2）reduce数量  \n&emsp; reduce的数量job.setNumReduceTasks(x);x 为reduce的数量。不设置的话默认为 1。\n\n### 5、 Maptask的个数由什么决定？\n&emsp; 一个job的map阶段MapTask并行度（个数），由客户端提交job时的切片个数决定。\n\n### 6、MapTask和ReduceTask工作机制（☆☆☆☆☆）（也可回答MapReduce工作原理）\n**MapTask工作机制**\n<p align=\"center\">\n<img src=\"https://github.com/Dr11ft/BigDataGuide/blob/master/Pics/Hadoop%E9%9D%A2%E8%AF%95%E9%A2%98Pics/MR-Pics/MapTask%E5%B7%A5%E4%BD%9C%E6%9C%BA%E5%88%B6.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n（1）Read阶段：Map Task通过用户编写的RecordReader，从输入InputSplit中解析出一个个key/value。  \n（2）Map阶段：该节点主要是将解析出的key/value交给用户编写map()函数处理，并产生一系列新的key/value。  \n（3）Collect收集阶段：在用户编写map()函数中，当数据处理完成后，一般会调用OutputCollector.collect()输出结果。在该函数内部，它会将生成的key/value分区（调用Partitioner），并写入一个环形内存缓冲区中。  \n（4）Spill阶段：即“溢写”，当环形缓冲区满后，MapReduce会将数据写到本地磁盘上，生成一个临时文件。需要注意的是，将数据写入本地磁盘之前，先要对数据进行一次本地排序，并在必要时对数据进行合并、压缩等操作。  \n（5）Combine阶段：当所有数据处理完成后，MapTask对所有临时文件进行一次合并，以确保最终只会生成一个数据文件。\n\n**ReduceTask工作机制**\n<p align=\"center\">\n<img src=\"https://github.com/Dr11ft/BigDataGuide/blob/master/Pics/Hadoop%E9%9D%A2%E8%AF%95%E9%A2%98Pics/MR-Pics/ReduceTask%E5%B7%A5%E4%BD%9C%E6%9C%BA%E5%88%B6.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n（1）Copy阶段：ReduceTask从各个MapTask上远程拷贝一片数据，并针对某一片数据，如果其大小超过一定阈值，则写到磁盘上，否则直接放到内存中。  \n（2）Merge阶段：在远程拷贝数据的同时，ReduceTask启动了两个后台线程对内存和磁盘上的文件进行合并，以防止内存使用过多或磁盘上文件过多。  \n（3）Sort阶段：按照MapReduce语义，用户编写reduce()函数输入数据是按key进行聚集的一组数据。为了将key相同的数据聚在一起，Hadoop采用了基于排序的策略。 由于各个MapTask已经实现对自己的处理结果进行了局部排序，因此，ReduceTask只需对所有数据进行一次归并排序即可。  \n（4）Reduce阶段：reduce()函数将计算结果写到HDFS上。\n\n### 7、描述mapReduce有几种排序及排序发生的阶段（☆☆☆☆☆）\n1）排序的分类：  \n&emsp; （1）部分排序：  \n&emsp; &emsp; MapReduce根据输入记录的键对数据集排序。保证输出的每个文件内部排序。  \n&emsp; （2）全排序：  \n&emsp; &emsp; 如何用Hadoop产生一个全局排序的文件？最简单的方法是使用一个分区。但该方法在处理大型文件时效率极低，因为一台机器必须处理所有输出文件，从而完全丧失了MapReduce所提供的并行架构。  \n&emsp; &emsp; 替代方案：首先创建一系列排好序的文件；其次，串联这些文件；最后，生成一个全局排序的文件。主要思路是使用一个分区来描述输出的全局排序。例如：可以为待分析文件创建3个分区，在第一分区中，记录的单词首字母a-g，第二分区记录单词首字母h-n, 第三分区记录单词首字母o-z。  \n&emsp; （3）辅助排序：（GroupingComparator分组）  \n&emsp; &emsp; Mapreduce框架在记录到达reducer之前按键对记录排序，但键所对应的值并没有被排序。甚至在不同的执行轮次中，这些值的排序也不固定，因为它们来自不同的map任务且这些map任务在不同轮次中完成时间各不相同。一般来说，大多数MapReduce程序会避免让reduce函数依赖于值的排序。但是，有时也需要通过特定的方法对键进行排序和分组等以实现对值的排序。  \n&emsp; （4）二次排序：  \n&emsp; &emsp; 在自定义排序过程中，如果compareTo中的判断条件为两个即为二次排序。  \n2）自定义排序WritableComparable  \n&emsp; bean对象实现WritableComparable接口重写compareTo方法，就可以实现排序  \n&emsp; &emsp; @Override  \n&emsp; &emsp; public int compareTo(FlowBean o) {  \n&emsp; &emsp; &emsp; // 倒序排列，从大到小  \n&emsp; &emsp; &emsp; return this.sumFlow > o.getSumFlow() ? -1 : 1;  \n&emsp; &emsp; }  \n3）排序发生的阶段：  \n&emsp; （1）一个是在map side发生在spill后partition前。  \n&emsp; （2）一个是在reduce side发生在copy后 reduce前。\n\n### 8、描述mapReduce中shuffle阶段的工作流程，如何优化shuffle阶段（☆☆☆☆☆）\n<img src=\"https://github.com/Dr11ft/BigDataGuide/blob/master/Pics/Hadoop%E9%9D%A2%E8%AF%95%E9%A2%98Pics/MR-Pics/mapReduce%E4%B8%ADshuffle%E9%98%B6%E6%AE%B5%E7%9A%84%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B.png\"/>  \n\n分区，排序，溢写，拷贝到对应reduce机器上，增加combiner，压缩溢写的文件。\n\n### 9、描述mapReduce中combiner的作用是什么，一般使用情景，哪些情况不需要，及和reduce的区别？\n1）Combiner的意义就是对每一个maptask的输出进行局部汇总，以减小网络传输量。  \n2）Combiner能够应用的前提是不能影响最终的业务逻辑，而且，Combiner的输出kv应该跟reducer的输入kv类型要对应起来。  \n3）Combiner和reducer的区别在于运行的位置。  \n&emsp; Combiner是在每一个maptask所在的节点运行；  \n&emsp; Reducer是接收全局所有Mapper的输出结果。\n\n<img src=\"https://github.com/Dr11ft/BigDataGuide/blob/master/Pics/Hadoop%E9%9D%A2%E8%AF%95%E9%A2%98Pics/MR-Pics/mapReduce%E4%B8%ADcombiner%E4%BD%9C%E7%94%A8.png\"/>\n\n### 10、如果没有定义partitioner，那数据在被送达reducer前是如何被分区的？\n&emsp; 如果没有自定义的 partitioning，则默认的 partition 算法，即根据每一条数据的 key 的 hashcode 值摸运算（%）reduce 的数量，得到的数字就是“分区号“。\n\n### 11、MapReduce 出现单点负载多大，怎么负载平衡？ （☆☆☆☆☆）\n&emsp; 通过Partitioner实现\n\n### 12、MapReduce 怎么实现 TopN？ （☆☆☆☆☆）\n&emsp; 可以自定义groupingcomparator，对结果进行最大值排序，然后再reduce输出时，控制只输出前n个数。就达到了topn输出的目的。\n\n### 13、Hadoop的缓存机制（Distributedcache）（☆☆☆☆☆）\n&emsp; 分布式缓存一个最重要的应用就是在进行join操作的时候，如果一个表很大，另一个表很小，我们就可以将这个小表进行广播处理，即每个计算节点上都存一份，然后进行map端的连接操作，经过我的实验验证，这种情况下处理效率大大高于一般的reduce端join，广播处理就运用到了分布式缓存的技术。  \n&emsp; DistributedCache将拷贝缓存的文件到Slave节点在任何Job在节点上执行之前，文件在每个Job中只会被拷贝一次，缓存的归档文件会被在Slave节点中解压缩。将本地文件复制到HDFS中去，接着Client会通过addCacheFile() 和addCacheArchive()方法告诉DistributedCache在HDFS中的位置。当文件存放到文地时，JobClient同样获得DistributedCache来创建符号链接，其形式为文件的URI加fragment标识。当用户需要获得缓存中所有有效文件的列表时，JobConf 的方法 getLocalCacheFiles() 和getLocalArchives()都返回一个指向本地文件路径对象数组。\n\n### 14、如何使用mapReduce实现两个表的join?（☆☆☆☆☆）\n&emsp; 1）reduce side join : 在map阶段，map函数同时读取两个文件File1和File2，为了区分两种来源的key/value数据对，对每条数据打一个标签（tag）,比如：tag=0 表示来自文件File1，tag=2 表示来自文件File2。  \n&emsp; 2）map side join : Map side join 是针对以下场景进行的优化：两个待连接表中，有一个表非常大，而另一个表非常小，以至于小表可以直接存放到内存中。这样，我们可以将小表复制多份，让每个map task 内存中存在一份（比如存放到hash table 中），然后只扫描大表：对于大表中的每一条记录key/value，在hash table 中查找是否有相同的key 的记录，如果有，则连接后输出即可。\n\n### 15、什么样的计算不能用mr来提速？\n&emsp; 1）数据量很小。  \n&emsp; 2）繁杂的小文件。  \n&emsp; 3）索引是更好的存取机制的时候。  \n&emsp; 4）事务处理。  \n&emsp; 5）只有一台机器的时候。\n\n### 16、ETL是哪三个单词的缩写\n&emsp; Extraction-Transformation-Loading的缩写，中文名称为数据提取、转换和加载。  \n\n## Hadoop面试题（四）——YARN\n\n### 1、简述hadoop1与hadoop2 的架构异同\n&emsp; 1）加入了yarn解决了资源调度的问题。  \n&emsp; 2）加入了对zookeeper的支持实现比较可靠的高可用。\n\n### 2、为什么会产生 yarn,它解决了什么问题，有什么优势？\n&emsp; 1）Yarn最主要的功能就是解决运行的用户程序与yarn框架完全解耦。  \n&emsp; 2）Yarn上可以运行各种类型的分布式运算程序（mapreduce只是其中的一种），比如mapreduce、storm程序，spark程序……\n\n### 3、HDFS的数据压缩算法?（☆☆☆☆☆）\n&emsp; Hadoop中常用的压缩算法有**bzip2、gzip、lzo、snappy**，其中lzo、snappy需要操作系统安装native库才可以支持。  \n&emsp; 数据可以压缩的位置如下所示。\n<p align=\"center\">\n<img src=\"https://github.com/Dr11ft/BigDataGuide/blob/master/Pics/Hadoop%E9%9D%A2%E8%AF%95%E9%A2%98Pics/YARN-Pics/MapReduce%E6%95%B0%E6%8D%AE%E5%8E%8B%E7%BC%A9.png\"/>  \n<p align=\"center\">\n</p>\n</p>  \n\n&emsp; **企业开发用的比较多的是snappy**。\n\n### 4、Hadoop的调度器总结（☆☆☆☆☆）\n（1）默认的调度器FIFO  \n&emsp; Hadoop中默认的调度器，它先按照作业的优先级高低，再按照到达时间的先后选择被执行的作业。  \n（2）计算能力调度器Capacity Scheduler  \n&emsp; 支持多个队列，每个队列可配置一定的资源量，每个队列采用FIFO调度策略，为了防止同一个用户的作业独占队列中的资源，该调度器会对同一用户提交的作业所占资源量进行限定。调度时，首先按以下策略选择一个合适队列：计算每个队列中正在运行的任务数与其应该分得的计算资源之间的比值，选择一个该比值最小的队列；然后按以下策略选择该队列中一个作业：按照作业优先级和提交时间顺序选择，同时考虑用户资源量限制和内存限制。   \n（3）公平调度器Fair Scheduler  \n&emsp; 同计算能力调度器类似，支持多队列多用户，每个队列中的资源量可以配置，同一队列中的作业公平共享队列中所有资源。实际上，Hadoop的调度器远不止以上三种，最近，出现了很多针对新型应用的Hadoop调度器。\n\n### 5、MapReduce 2.0 容错性（☆☆☆☆☆）\n1）MRAppMaster容错性  \n&emsp; 一旦运行失败，由YARN的ResourceManager负责重新启动，最多重启次数可由用户设置，默认是2次。一旦超过最高重启次数，则作业运行失败。   \n2）Map Task/Reduce  \n&emsp; Task Task周期性向MRAppMaster汇报心跳；一旦Task挂掉，则MRAppMaster将为之重新申请资源，并运行之。最多重新运行次数可由用户设置，默认4次。\n\n### 6、mapreduce推测执行算法及原理（☆☆☆☆☆）\n1）作业完成时间取决于最慢的任务完成时间  \n&emsp; 一个作业由若干个Map 任务和Reduce 任务构成。因硬件老化、软件Bug 等，某些任务可能运行非常慢。  \n&emsp; 典型案例：系统中有99%的Map任务都完成了，只有少数几个Map老是进度很慢，完不成，怎么办？  \n2）推测执行机制  \n&emsp; 发现拖后腿的任务，比如某个任务运行速度远慢于任务平均速度。为拖后腿任务启动一个备份任务，同时运行。谁先运行完，则采用谁的结果。  \n3）不能启用推测执行机制情况  \n&emsp; （1）任务间存在严重的负载倾斜；  \n&emsp; （2）特殊任务，比如任务向数据库中写数据。  \n4）算法原理  \n&emsp; 假设某一时刻，任务T的执行进度为progress，则可通过一定的算法推测出该任务的最终完成时刻estimateEndTime。另一方面，如果此刻为该任务启动一个备份任务，则可推断出它可能的完成时刻estimateEndTime`,于是可得出以下几个公式：  \n&emsp; &emsp; estimateEndTime=estimatedRunTime+taskStartTime  \n&emsp; &emsp; estimatedRunTime=(currentTimestamp-taskStartTime)/progress  \n&emsp; &emsp; estimateEndTime`= currentTimestamp+averageRunTime  \n&emsp; 其中，currentTimestamp为当前时刻；taskStartTime为该任务的启动时刻；averageRunTime为已经成功运行完成的任务的平均运行时间。这样，MRv2总是选择（estimateEndTime- estimateEndTime·）差值最大的任务，并为之启动备份任务。为了防止大量任务同时启动备份任务造成的资源浪费，MRv2为每个作业设置了同时启动的备份任务数目上限。  \n&emsp; 推测执行机制实际上采用了经典的算法优化方法：以空间换时间，它同时启动多个相同任务处理相同的数据，并让这些任务竞争以缩短数据处理时间。显然，这种方法需要占用更多的计算资源。在集群资源紧缺的情况下，应合理使用该机制，争取在多用少量资源的情况下，减少作业的计算时间。  \n\n## Hadoop面试题总结（五）——优化问题\n\n### 1、MapReduce跑得慢的原因？（**☆☆☆☆☆**）\nMapreduce 程序效率的瓶颈在于两点：  \n1）计算机性能  \n&emsp; CPU、内存、磁盘健康、网络  \n2）I/O 操作优化  \n&emsp; （1）数据倾斜  \n&emsp; （2）map和reduce数设置不合理  \n&emsp; （3）reduce等待过久  \n&emsp; （4）小文件过多   \n&emsp; （5）大量的不可分块的超大文件   \n&emsp; （6）spill次数过多  \n&emsp; （7）merge次数过多等\n\n### 2、MapReduce优化方法（☆☆☆☆☆）\n1）数据输入  \n&emsp; （1）合并小文件：在执行mr任务前将小文件进行合并，大量的小文件会产生大量的map任务，增大map任务装载次数，而任务的装载比较耗时，从而导致mr运行较慢。   \n&emsp; （2）采用ConbinFileInputFormat来作为输入，解决输入端大量小文件场景。  \n2）map阶段  \n&emsp; （1）减少spill次数：通过调整io.sort.mb及sort.spill.percent参数值，增大触发spill的内存上限，减少spill次数，从而减少磁盘 IO。   \n&emsp; （2）减少merge次数：通过调整io.sort.factor参数，增大merge的文件数目，减少merge的次数，从而缩短mr处理时间。    \n&emsp; （3）在 map 之后先进行combine处理，减少I/O。  \n3）reduce阶段  \n&emsp; （1）合理设置map和reduce数：两个都不能设置太少，也不能设置太多。太少，会导致task等待，延长处理时间；太多，会导致 map、reduce任务间竞争资源，造成处理超时等错误。   \n&emsp; （2）设置map、reduce共存：调整slowstart.completedmaps参数，使map运行到一定程度后，reduce也开始运行，减少reduce的等待时间。  \n&emsp; （3）规避使用reduce，因为Reduce在用于连接数据集的时候将会产生大量的网络消耗。  \n&emsp; （4）合理设置reduce端的buffer，默认情况下，数据达到一个阈值的时候，buffer中的数据就会写入磁盘，然后reduce会从磁盘中获得所有的数据。也就是说，buffer和reduce是没有直接关联的，中间多个一个写磁盘->读磁盘的过程，既然有这个弊端，那么就可以通过参数来配置，使得buffer中的一部分数据可以直接输送到reduce，从而减少IO开销：mapred.job.reduce.input.buffer.percent，默认为0.0。当值大于0的时候，会保留指定比例的内存读buffer中的数据直接拿给reduce使用。这样一来，设置buffer需要内存，读取数据需要内存，reduce计算也要内存，所以要根据作业的运行情况进行调整。  \n4）IO传输  \n&emsp; （1）采用数据压缩的方式，减少网络IO的的时间。安装Snappy和LZOP压缩编码器。  \n&emsp; （2）使用SequenceFile二进制文件  \n5）数据倾斜问题  \n&emsp; （1）数据倾斜现象\n&emsp; &emsp; 数据频率倾斜——某一个区域的数据量要远远大于其他区域。  \n&emsp; &emsp; 数据大小倾斜——部分记录的大小远远大于平均值。  \n&emsp; （2）如何收集倾斜数据  \n&emsp; &emsp; 在reduce方法中加入记录map输出键的详细情况的功能。\n```java\npublic static final String MAX_VALUES = \"skew.maxvalues\";\nprivate int maxValueThreshold;\n\n@Override\npublic void configure(JobConf job) {\n     maxValueThreshold = job.getInt(MAX_VALUES, 100);\n}\n\n@Override\npublic void reduce(Text key, Iterator<Text> values,\n                     OutputCollector<Text, Text> output,\n                     Reporter reporter) throws IOException {\n     int i = 0;\n     while (values.hasNext()) {\n         values.next();\n         i++;\n     }\n     if (++i > maxValueThreshold) {\n         log.info(\"Received \" + i + \" values for key \" + key);\n     }\n}\n```\n&emsp; （3）减少数据倾斜的方法  \n&emsp; &emsp; 方法1：抽样和范围分区  \n&emsp; &emsp; &emsp; 可以通过对原始数据进行抽样得到的结果集来预设分区边界值。  \n&emsp; &emsp; 方法2：自定义分区   \n&emsp; &emsp; &emsp; 另一个抽样和范围分区的替代方案是基于输出键的背景知识进行自定义分区。例如，如果map输出键的单词来源于一本书。其中大部分必然是省略词（stopword）。那么就可以将自定义分区将这部分省略词发送给固定的一部分reduce实例。而将其他的都发送给剩余的reduce实例。  \n&emsp; &emsp; 方法3：Combine  \n&emsp; &emsp; &emsp; 使用Combine可以大量地减小数据频率倾斜和数据大小倾斜。在可能的情况下，combine的目的就是聚合并精简数据。\n\n### 3、HDFS小文件优化方法（☆☆☆☆☆）\n1）HDFS小文件弊端：  \n&emsp; HDFS上每个文件都要在namenode上建立一个索引，这个索引的大小约为150byte，这样当小文件比较多的时候，就会产生很多的索引文件，一方面会大量占用namenode的内存空间，另一方面就是索引文件过大是的索引速度变慢。   \n2）解决的方式：   \n&emsp; （1）Hadoop本身提供了一些文件压缩的方案。\n&emsp; （2）从系统层面改变现有HDFS存在的问题，其实主要还是小文件的合并，然后建立比较快速的索引。  \n3）Hadoop自带小文件解决方案  \n&emsp; （1）Hadoop Archive：  \n&emsp; &emsp; 是一个高效地将小文件放入HDFS块中的文件存档工具，它能够将多个小文件打包成一个HAR文件，这样在减少namenode内存使用的同时。   \n&emsp; （2）Sequence file：  \n&emsp; &emsp; sequence file由一系列的二进制key/value组成，如果为key小文件名，value为文件内容，则可以将大批小文件合并成一个大文件。   \n&emsp; （3）CombineFileInputFormat：  \n&emsp; &emsp; CombineFileInputFormat是一种新的inputformat，用于将多个文件合并成一个单独的split，另外，它会考虑数据的存储位置。  \n\n\n\n# 链接\n- https://github.com/MoRan1607/BigDataGuide"
  },
  {
    "path": "大数据/Hive/Hive.md",
    "content": ""
  },
  {
    "path": "大数据/Spark/Spark.md",
    "content": ""
  },
  {
    "path": "容器技术/docker.md",
    "content": "* [docker](#docker)\n  * [Docker简介](#docker简介)\n    * [是什么](#是什么)\n    * [能干嘛](#能干嘛)\n  * [Docker常用命令](#docker常用命令)\n    * [帮助命令](#帮助命令)\n    * [镜像命令](#镜像命令)\n    * [容器命令](#容器命令)\n  * [Docker应用架构](#docker应用架构)\n  * [底层实现原理](#底层实现原理)\n    * [Linux Namespace 命名空间](#linux-namespace-命名空间)\n    * [Linux Cgroups 控制组](#linux-cgroups-控制组)\n    * [联合文件系统UnionFS](#联合文件系统unionfs)\n    * [容器格式](#容器格式)\n    * [网络](#网络)\n      * [基本原理](#基本原理)\n      * [创建网络参数](#创建网络参数)\n* [参考文章](#参考文章)\n\n# docker\n## Docker简介\n### 是什么\n解决了运行环境和配置问题软件容器，方便做持续集成并有助于整体发布的容器虚拟化技术。\n### 能干嘛\n容器虚拟化技术\n\n一次构建、随处运行\n- 更快速的应用交付和部署\n- 更便捷的升级和扩缩容\n- 更简单的系统运维\n- 更高效的计算资源利用\n\n## Docker常用命令\n### 帮助命令\n- docker version\n- docker info\n- docker --help\n### 镜像命令\n- docker images 列出本地主机上的镜像\n  - OPTIONS说明：\n    - -a :列出本地所有的镜像（含中间映像层）\n    - -q :只显示镜像ID。\n    - --digests :显示镜像的摘要信息\n    - --no-trunc :显示完整的镜像信息\n- docker search 某个XXX镜像名字\n  - 网站 https://hub.docker.com\n  - 命令 docker search [OPTIONS] 镜像名字\n    - OPTIONS说明：\n      - --no-trunc : 显示完整的镜像描述\n      - -s : 列出收藏数不小于指定值的镜像。\n      - --automated : 只列出 automated build类型的镜像；\n- docker pull 某个XXX镜像名字 下载镜像\n  - docker pull 镜像名字[:TAG]\n- docker rmi 某个XXX镜像名字ID 删除镜像\n  - 删除单个 docker rmi  -f 镜像ID\n  - 删除多个 docker rmi -f 镜像名1:TAG 镜像名2:TAG\n  - 删除全部 docker rmi -f $(docker images -qa)\n### 容器命令\n有镜像才能创建容器，这是根本前提(下载一个CentOS镜像演示)\n- docker pull centos\n\n新建并启动容器\n- docker run [OPTIONS] IMAGE [COMMAND] [ARG...]\n\n列出当前所有正在运行的容器\n- docker ps [OPTIONS]\n\n退出容器\n- exit 容器停止退出\n- ctrl+P+Q 容器不停止退出\n\n启动容器\n- docker start 容器ID或者容器名\n\n重启容器\n- docker restart 容器ID或者容器名\n\n停止容器\n- docker stop 容器ID或者容器名\n\n强制停止容器\n- docker kill 容器ID或者容器名\n\n删除已停止的容器\n- docker rm 容器ID\n\n一次性删除多个容器\n- docker rm -f $(docker ps -a -q)\n- docker ps -a -q | xargs docker rm\n\n重要\n\n启动守护式容器\n- docker run -d 容器名 \n\n查看容器日志\n- docker logs -f -t --tail 容器ID\n  -  -t 是加入时间戳\n  -  -f 跟随最新的日志打印\n  -  --tail 数字 显示最后多少条\n\n查看容器内运行的进程\n- docker top 容器ID\n\n查看容器内部细节\n- docker inspect 容器ID\n\n进入正在运行的容器并以命令行交互\n- docker exec -it 容器ID bashShell\n- 重新进入docker attach 容器ID\n- 上述两个区别\n  - attach 直接进入容器启动命令的终端，不会启动新的进程\n  - exec 是在容器中打开新的终端，并且可以启动新的进程\n\n从容器内拷贝文件到主机上\n- docker cp  容器ID:容器内路径 目的主机路径\n\n\n## Docker应用架构\nDocker 采用了 C/S 架构，包括客户端和服务端。Docker 守护进程 （Daemon）作为服务端接受来自客户端的请求，并处理这些请求（创建、运行、分发容器）。\n\n客户端和服务端既可以运行在一个机器上，也可通过 socket 或者 RESTful API 来进行通信。\n\nDocker 守护进程一般在宿主主机后台运行，等待接收来自客户端的消息。\n\nDocker 客户端则为用户提供一系列可执行命令，用户用这些命令实现跟 Docker 守护进程交互。\n![](../img/容器技术/docker/docker应用架构.png)\n## 底层实现原理\nDocker 底层的核心技术包括 Linux 上的命名空间（Namespaces）、控制组（Control groups）、Union 文件系统（Union file systems）和容器格式（Container format）。\n\n我们知道，传统的虚拟机通过在宿主主机中运行 hypervisor 来模拟一整套完整的硬件环境提供给虚拟机的操作系统。虚拟机系统看到的环境是可限制的，也是彼此隔离的。 这种直接的做法实现了对资源最完整的封装，但很多时候往往意味着系统资源的浪费。 例如，以宿主机和虚拟机系统都为 Linux 系统为例，虚拟机中运行的应用其实可以利用宿主机系统中的运行环境。\n\n我们知道，在操作系统中，包括内核、文件系统、网络、PID、UID、IPC、内存、硬盘、CPU 等等，所有的资源都是应用进程直接共享的。 要想实现虚拟化，除了要实现对内存、CPU、网络IO、硬盘IO、存储空间等的限制外，还要实现文件系统、网络、PID、UID、IPC等等的相互隔离。 前者相对容易实现一些，后者则需要宿主机系统的深入支持。\n\n随着 Linux 系统对于命名空间功能的完善实现，程序员已经可以实现上面的所有需求，让某些进程在彼此隔离的命名空间中运行。大家虽然都共用一个内核和某些运行时环境（例如一些系统命令和系统库），但是彼此却看不到，都以为系统中只有自己的存在。这种机制就是容器（Container），利用命名空间来做权限的隔离控制，利用 cgroups 来做资源分配。\n\n### Linux Namespace 命名空间\n命名空间是 Linux 内核一个强大的特性。每个容器都有自己单独的命名空间，运行在其中的应用都像是在独立的操作系统中运行一样。命名空间保证了容器之间彼此互不影响。\n\n### Linux Cgroups 控制组\n控制组（cgroups）是 Linux 内核的一个特性，主要用来对共享资源进行隔离、限制、审计等。只有能控制分配到容器的资源，才能避免当多个容器同时运行时的对系统资源的竞争。\n\n控制组技术最早是由 Google 的程序员在 2006 年提出，Linux 内核自 2.6.24 开始支持。\n\n控制组可以提供对容器的内存、CPU、磁盘 IO 等资源的限制和审计管理。\n\n有了namespace为什么还要cgroup:\n\nDocker 容器使用 linux namespace 来隔离其运行环境，使得容器中的进程看起来就像一个独立环境中运行一样。但是，光有运行环境隔离还不够，因为这些进程还是可以不受限制地使用系统资源，比如网络、磁盘、CPU以及内存 等。关于其目的，一方面，是为了防止它占用了太多的资源而影响到其它进程；另一方面，在系统资源耗尽的时候，linux 内核会触发 OOM，这会让一些被杀掉的进程成了无辜的替死鬼。因此，为了让容器中的进程更加可控，Docker 使用 Linux cgroups 来限制容器中的进程允许使用的系统资源。\n\n### 联合文件系统UnionFS\n\n联合文件系统（UnionFS）是一种分层、轻量级并且高性能的文件系统，它支持对文件系统的修改作为一次提交来一层层的叠加，同时可以将不同目录挂载到同一个虚拟文件系统下(unite several directories into a single virtual filesystem)。\n\n联合文件系统是 Docker 镜像的基础。镜像可以通过分层来进行继承，基于基础镜像（没有父镜像），可以制作各种具体的应用镜像。\n\n另外，不同 Docker 容器就可以共享一些基础的文件系统层，同时再加上自己独有的改动层，大大提高了存储的效率。\n\nDocker 中使用的 AUFS（Advanced Multi-Layered Unification Filesystem）就是一种联合文件系统。 AUFS 支持为每一个成员目录（类似 Git 的分支）设定只读（readonly）、读写（readwrite）和写出（whiteout-able）权限, 同时 AUFS 里有一个类似分层的概念, 对只读权限的分支可以逻辑上进行增量地修改(不影响只读部分的)。\n\nDocker 目前支持的联合文件系统包括 OverlayFS, AUFS, Btrfs, VFS, ZFS 和 Device Mapper。\n\n各 Linux 发行版 Docker 推荐使用的存储驱动如下表。\n\n| Linux 发行版 |Docker 推荐使用的存储驱动|\n|---|---|\n|Docker on Ubuntu| overlay2 (16.04 +)|\n|Docker on Debian| overlay2 (Debian Stretch), aufs, devicemapper|\n|Docker on CentOS| overlay2|\n|Docker on Fedora| overlay2|\n\n在可能的情况下，推荐 使用 overlay2 存储驱动，overlay2 是目前 Docker 默认的存储驱动，以前则是 aufs。你可以通过配置来使用以上提到的其他类型的存储驱动。\n\n### 容器格式\n最初，Docker 采用了 LXC 中的容器格式。从 0.7 版本以后开始去除 LXC，转而使用自行开发的 libcontainer，从 1.11 开始，则进一步演进为使用 runC 和 containerd。\n\n### 网络\nDocker 的网络实现其实就是利用了 Linux 上的网络命名空间和虚拟网络设备（特别是 veth pair）。建议先熟悉了解这两部分的基本概念再阅读本章。\n#### 基本原理\n\n首先，要实现网络通信，机器需要至少一个网络接口（物理接口或虚拟接口）来收发数据包；此外，如果不同子网之间要进行通信，需要路由机制。\n\nDocker 中的网络接口默认都是虚拟的接口。虚拟接口的优势之一是转发效率较高。 Linux 通过在内核中进行数据复制来实现虚拟接口之间的数据转发，发送接口的发送缓存中的数据包被直接复制到接收接口的接收缓存中。对于本地系统和容器内系统看来就像是一个正常的以太网卡，只是它不需要真正同外部网络设备通信，速度要快很多。\n\nDocker 容器网络就利用了这项技术。它在本地主机和容器内分别创建一个虚拟接口，并让它们彼此连通（这样的一对接口叫做 veth pair）。\n#### 创建网络参数\n\nDocker 创建一个容器的时候，会执行如下操作：\n\n创建一对虚拟接口，分别放到本地主机和新容器中；\n\n本地主机一端桥接到默认的 docker0 或指定网桥上，并具有一个唯一的名字，如 veth65f9；\n\n容器一端放到新容器中，并修改名字作为 eth0，这个接口只在容器的命名空间可见；\n\n从网桥可用地址段中获取一个空闲地址分配给容器的 eth0，并配置默认路由到桥接网卡 veth65f9。\n\n完成这些之后，容器就可以使用 eth0 虚拟网卡来连接其他容器和其他网络。\n\n可以在 docker run 的时候通过 --net 参数来指定容器的网络配置，有4个可选值：\n- --net=bridge 这个是默认值，连接到默认的网桥。\n- --net=host 告诉 Docker 不要将容器网络放到隔离的命名空间中，即不要容器化容器内的网络。此时容器使用本地主机的网络，它拥有完全的本地主机接口访问权限。容器进程可以跟主机其它 root 进程一样可以打开低范围的端口，可以访问本地网络服务比如 D-bus，还可以让容器做一些影响整个主机系统的事情，比如重启主机。因此使用这个选项的时候要非常小心。如果进一步的使用 --privileged=true，容器会被允许直接配置主机的网络堆栈。\n- --net=container:NAME_or_ID 让 Docker 将新建容器的进程放到一个已存在容器的网络栈中，新容器进程有自己的文件系统、进程列表和资源限制，但会和已存在的容器共享 IP 地址和端口等网络资源，两者进程可以直接通过 lo 环回接口通信。\n- --net=none 让 Docker 将新容器放到隔离的网络栈中，但是不进行网络配置。之后，用户可以自己进行配置。\n# 参考文章\n- https://yeasy.gitbook.io/docker_practice/introduction\n"
  },
  {
    "path": "容器技术/k8s.md",
    "content": "\n![](../img/容器技术/k8s/k8s图标.png)\n\n# 简介\n\nKubernetes 是 Google 团队发起的开源项目，它的目标是管理跨多个主机的容器，提供基本的部署，维护以及应用伸缩，主要实现语言为 Go 语言。Kubernetes 是：\n\n- 易学：轻量级，简单，容易理解\n- 便携：支持公有云，私有云，混合云，以及多种云平台\n- 可拓展：模块化，可插拔，支持钩子，可任意组合\n- 自修复：自动重调度，自动重启，自动复制\n\nKubernetes 构建于 Google 数十年经验，一大半来源于 Google 生产环境规模的经验。结合了社区最佳的想法和实践。\n\n在分布式系统中，部署，调度，伸缩一直是最为重要的也最为基础的功能。Kubernetes 就是希望解决这一序列问题的。\n\nKubernetes 目前在GitHub进行维护。\n\n# 基本概念\n![](../img/容器技术/k8s/k8s基本概念.png)\n\n1. Pod\n最小的可部署单位，一个 Pod 可以包含一个或多个紧密相关的容器。\n容器共享网络和存储资源。\n2. Node\nKubernetes 集群中的工作机器，可以是虚拟机或物理机。\n每个 Node 包含运行 Pod 的必要组件，如 Kubelet、Kube-proxy 和容器运行时。\n3. ReplicaSet\n确保在任何时间都有指定数量的 Pod 副本在运行。\n提供负载均衡和高可用性。\n4. Deployment\n用于管理应用的声明式更新。\n可控制 ReplicaSet 的创建和扩缩容。\n5. Service\n定义一组 Pod 的访问策略，通过一个固定的 IP 和 DNS 名称提供负载均衡。\n支持多种类型，如 ClusterIP、NodePort 和 LoadBalancer。\n6. Namespace\n用于在同一集群中隔离资源，适合不同环境（如开发、测试、生产）。\n逻辑上分隔资源，便于访问控制。\n7. Volume\n持久化存储的基础，Pod 的存储方案。\n支持多种类型的存储，如 NFS、PVC 等。\n8. ConfigMap 和 Secret\nConfigMap：用于存储非敏感的配置信息，以键值对的形式提供给容器。\nSecret：用于存储敏感信息（如密码、证书），提供更安全的处理方式。\n9. Ingress\n管理外部用户访问服务的规则，可以提供负载均衡、SSL 终端等功能。\n通常用于 HTTP/HTTPS 流量的路由。\n10. Cluster\n由多个 Node 组成的整体，Kubernetes 控制器在集群中管理和调度资源。\n\n## k8s架构\n\n![](../img/容器技术/k8s/k8s架构.png)\n\n## 常用的命令\n\n以下是一些常用的 kubectl 命令，用于管理 Kubernetes 集群中的资源：\n### 1.查看资源\n- kubectl get pods：获取所有 Pod 的信息。\n- kubectl get services：获取所有服务的信息。\n- kubectl get deployments：获取所有部署的信息。\n- kubectl get nodes：获取所有节点的信息。\n### 2.查看详细信息\n\n- kubectl describe pod <pod_name>：显示特定 Pod 的详细信息。\n- kubectl describe service <service_name>：显示特定服务的详细信息。\n### 3.创建资源\n- kubectl create -f <filename>：从配置文件创建资源。\n- kubectl apply -f <filename>：应用配置文件以创建或更新资源。\n### 4.删除资源\n- kubectl delete pod <pod_name>：删除特定的 Pod。\n- kubectl delete service <service_name>：删除特定的服务。\n- kubectl delete deployment <deployment_name>：删除特定的部署。\n### 5.日志和执行命令\n- kubectl logs <pod_name>：获取 Pod 的日志。\n- kubectl exec -it <pod_name> -- /bin/bash：在运行中的 Pod 中执行交互式 shell。\n### 6.扩展和缩放\n- kubectl scale deployment <deployment_name> --replicas=<num>：扩展或缩小部署的副本数量。\n- kubectl autoscale deployment <deployment_name> --min=<min> --max=<max> --cpu-percent=<percent>：基于 CPU 使用情况自动扩展部署。\n### 7.更新资源\n- kubectl edit <resource_type> <resource_name>：编辑资源的配置。\n- kubectl rollout restart deployment <deployment_name>：重启部署以应用更新。\n### 8.命名空间\n- kubectl get pods -n <namespace>：在指定命名空间中获取资源。\n- kubectl create namespace <namespace>：创建一个新的命名空间。\n\n## 案例：使用k8s启动一个springboot服务\n\n使用 Kubernetes 启动一个 Spring Boot 服务的流程如下：\n\n### 1. **准备 Spring Boot 应用**\n\n确保你已经有一个打包好的 Spring Boot 应用，通常以 JAR 文件形式存在。可以使用 Maven 或 Gradle 进行构建。\n\n### 2. **编写 Dockerfile**\n\n在 Spring Boot 源码根目录下创建一个 `Dockerfile` 文件。\n\n```dockerfile\n# 使用 Java 基础镜像\nFROM openjdk:11-jre-slim\n# 复制 JAR 文件到镜像中\nCOPY target/your-spring-boot-app.jar app.jar\n# 运行应用\nENTRYPOINT [\"java\",\"-jar\",\"/app.jar\"]\n```\n\n### 3. **构建 Docker 镜像**\n\n在命令行中，导航到项目根目录并执行以下命令：\n\n```bash\ndocker build -t your-image-name:latest .\n```\n\n### 4. **推送 Docker 镜像到容器仓库**\n\n将 Docker 镜像上传到公共或私有 Docker 仓库（如 Docker Hub）。\n\n```bash\ndocker tag your-image-name:latest your-repo/your-image-name:latest\ndocker push your-repo/your-image-name:latest\n```\n\n### 5. **编写 Kubernetes 部署配置**\n\n创建一个名为 `deployment.yaml` 的文件，定义 Deployment 和 Service。\n\n```yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: springboot-deployment\nspec:\n  replicas: 2\n  selector:\n    matchLabels:\n      app: springboot-app\n  template:\n    metadata:\n      labels:\n        app: springboot-app\n    spec:\n      containers:\n        - name: springboot-container\n          image: your-repo/your-image-name:latest\n          ports:\n            - containerPort: 8080 # 根据应用的端口进行修改\n\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: springboot-service\nspec:\n  type: NodePort\n  ports:\n    - port: 8080\n      targetPort: 8080\n      nodePort: 30000 # 可用的节点端口\n  selector:\n    app: springboot-app\n```\n\n### 6. **部署到 Kubernetes**\n\n使用 kubectl 命令将配置应用到 Kubernetes 集群。\n\n```bash\nkubectl apply -f deployment.yaml\n```\n\n### 7. **查看部署状态**\n\n使用以下命令查看应用的状态：\n\n```bash\nkubectl get pods\nkubectl get services\n```\n\n### 8. **访问 Spring Boot 服务**\n\n通过集群的节点 IP 和端口访问 Spring Boot 应用：\n\n```bash\nhttp://<node-ip>:30000\n```\n\n### 总结\n\n以上步骤完成了使用 Kubernetes 启动 Spring Boot 服务的流程，包括 Docker 镜像的构建、上传和 Kubernetes 配置的创建与应用。确保 Kubernetes 集群已成功运行，并已正确配置网络访问。"
  },
  {
    "path": "性能测试/docker安装配置influxdb-v2+grafana+jemter.md",
    "content": "# 1、安装docker\n\nhttps://www.docker.com/get-started\n\n![](../img/性能测试/安装docker.png)\n\n\n# 2、安装配置influxdb\n\nhttps://docs.influxdata.com/influxdb/v2.0/install/?t=Docker#persist-data-outside-the-influxdb-container\n\n在本地新建一个存放容器influx数据的目录\n\n```shell\nmkdir influxdb-docker-data-volume && cd $_\n```\n比如我是在本地用户的目录下新建\n\n![](../img/性能测试/存放influxDB.png)\n\n启动\n```shell\ndocker run -d --name=influxdb -p 8086:8086 -v ${PWD}/influxdb/:/var/lib/influxdb2 influxdb\n```\n启动成功\n\n![](../img/性能测试/启动influxDB.png)\n\n访问influx的UI界面配置一些登录和数据库信息\n\nhttp://localhost:8086/\n\n![](../img/性能测试/influxDB-ui1.png)\n![](../img/性能测试/influxDB-ui2.png)\n![](../img/性能测试/influxDB-ui3.png)\n![](../img/性能测试/influxDB-ui4.png)\n\n这里可以看到用户的信息\n\n![](../img/性能测试/influxDB-ui5.png)\n\ninfluxdb v2使用token登录检验，这里可以获取到token的值\n\n![](../img/性能测试/influxDB-ui6.png)\n\n你也可以新建一个token并为他设置读写权限，这里的token等下在grafana添加数据源连接时需要用到\n\n![](../img/性能测试/influxDB-ui7.png)\n\n# 3、运行grafana\n\n```shell\ndocker run -d --name grafana -p 3000:3000 grafana/grafana\n```\n![](../img/性能测试/运行grafana.png)\n\n访问grafana的UI界面\n\nhttp://localhost:3000/login\n\n![](../img/性能测试/grafana-ui.png)\n\n输入默认用户名密码\n```shell\nadmin  admin\n```\n\n这里可以在进入容器后修改grafana的配置文件\n```shell\ndocker exec -it grafana bash\ncat /etc/grafana/grafana.ini\n```\n![](../img/性能测试/grafana-admin.png)\n\n要求修改密码，我这里skip,你也可以修改一下\n\n![](../img/性能测试/grafana-skip.png)\n\n进入后直接添加数据源\n\n![](../img/性能测试/grafana-添加数据源.png)\n![](../img/性能测试/grafana-添加数据源2.png)\n\ninfluxdb v2选择flux\n\n![](../img/性能测试/grafana-添加数据源3.png)\n\n连接信息如图：\n```shell\nURL：http://localhost:8086\nAccess: Server(default)\nOrganization：jemter    # 这里就是在上面配置influxDB时创建的\nToken：xxxx # 复制上面提到的token\nDefault Bucket：jemter   # 上文创建的bucket\n```\n![](../img/性能测试/grafana-添加数据源4.png)\n\n这里不出意外会得到一个错误\n\n![](../img/性能测试/grafana-添加数据源5.png)\n\n调出控制台，看下接口的信息，发现出现connection refused错误\n\n![](../img/性能测试/grafana-添加数据源6.png)\n\n这里分析下，接口直接请求的是grafana的接口，在容器内再请求127.0.0.1:8086 influxDB接口，问题就出现在这\n\n![](../img/性能测试/grafana-添加数据源7.png)\n\n我们看下docker的容器网络\n\ndocker的默认网桥bridge\n\n![](../img/性能测试/16ad4794b38e90e4~tplv-t2oaga2asx-watermark.awebp.webp)\n\n如上图所示为Docker中bridge驱动模式的示意图，其中蓝色的模块表示主机上的网卡。当Docker启动时会自动在主机上创建一个虚拟网桥docker0，使用默认网络模式创建docker容器时会自动创建一对儿veth pair接口，一端连接在docker容器中（如图容器中的eth0），一端连接在虚拟网桥docker0上（如图veth）。这种veth pair是一种虚拟网络设备，主要用于不同namespace中（意味着网络隔离）的网络通信，它总是成对存在的。在这里可以把它想象成一对儿靠虚拟网线连接起来的两个虚拟网卡，一端连接着docker容器，一端连接着虚拟网桥docker0。\n\n通过这种方式，不同docker容器之间可以通过ip地址互相通信，也可以通过虚拟网桥访问主机上的网络eth0（添加iptables规则，将docker容器对目标地址发出的访问通过地址伪装的方式修改为主机对目标地址进行访问）。\n\n**也就是说容器间可以通信通过的是ip**\n\n于是\n```shell\ndocker exec -it influxdb bash\n \nip addr\n```\n![](../img/性能测试/ip.png)\n\n把ip拿过来修改\n\n![](../img/性能测试/grafana-添加数据源8.png)\n\n再次测试save&test\n\n![](../img/性能测试/grafana-添加数据源9.png)\n\n完成后，我们去仪表板下载一个模板\n\nhttps://grafana.com/grafana/dashboards 在这里可以搜索jmeter，有很多模板\n\n![](../img/性能测试/grafana-添加模板.png)\n\n这里有一个ID可以试试5496，如图import一个模板\n\n![](../img/性能测试/grafana-添加模板2.png)\n\n点击load，再import\n\n![](../img/性能测试/grafana-添加模板3.png)\n\n效果如下，有红色感叹号是因为这是influxDB 2.0版本以下的模板，查询SQL和新版的不一致，不过可以edit每个版块的SQL即可呈现数据\n\n![](../img/性能测试/grafana-添加模板4.png)\n\n你也可以选择v2版本的模板\n\n# 4、配置jmeter\n```shell\n添加一个线程组\n添加一个接口请求\n添加一个结果树\n添加一个Backend Listener\n```\n![](../img/性能测试/jmeter配置1.png)\n![](../img/性能测试/jmeter配置2.png)\n![](../img/性能测试/jmeter配置3.png)\n\n- influxdbUrl修改为http://localhost:8086/api/v2/write?org=jmeter&bucket=jmeter\n- 注意如果是其他的org和bucket需要按你在influxDB里配置的修改\n- application随便写\n- measurement也是随便写，但是这个字段在后面筛选数据时会用到\n\n然后点击Add，添加一个name value\n- name为influxdbToken\n- value的值可以点Detail cv填进去\n\n![](../img/性能测试/jmeter配置4.png)\n\n然后我们先设置loop count 为循环，测试一下数据\n\n![](../img/性能测试/jmeter配置5.png)\n\n运行开始可以看下日志有没有错误，如果有错误，按日志提示修改\n\n![](../img/性能测试/jmeter配置6.png)\n\n# 5、配置UI和查询语句\n回到influxDB的UI，这里也有一些图表显示，我们来查询一下数据看看\n\nfrom 选择bucket ，相当于选择数据库\n\n![](../img/性能测试/influxdb-ui-查询数据.png)\n\n后面的都是过滤条件filter\n\n在第一个filter我们选择_measurement 选择  codetop2这里就是刚才在jmeter配置的\n\n![](../img/性能测试/jmeter配置.png)\n\n这里可以看出来jmeter传了一些数据到influxDB，具体的这些参数的含义可以去研究下，但是看名字比如avg是平均数吧，count是总的请求条数？\n\n![](../img/性能测试/influxdb-ui-查询数据2.png)\n\n我们这样选择\n\n![](../img/性能测试/influxdb-ui-查询数据3.png)\n\n然后点击submit,其实看得出来，这里就是在可视化的配置查询语句\n\n这里显示10条，就是刚才我只发送了10次请求就停下来了，如果你是循环应该会一直递增\n\n![](../img/性能测试/influxdb-ui-查询数据4.png)\n\n因为这是可视化的，我们需要把语句copy一下，去刚才的低版本模板修改SQL（新版本基于influxdb v2 flux的模板写的查询语句应该是不需要修改的）\n\n![](../img/性能测试/influxdb-ui-查询数据5.png)\n\n把这里的语句copy一下，回到grafana的这个模板，我们编辑一下这个版块\n\n![](../img/性能测试/grafana-添加模板5.png)\n\n可以看到这个SQL是influxDB v2版本以下的查询语句，我们把刚才在influxDB的UI copy的SQL替换图示的SQL\n\n![](../img/性能测试/grafana-添加模板6.png)\n\n因为我这里不是一直循环，所以目前没有数据，因为influxDB是时序数据库，数据查询都是带时间条件的不带默认是截止是now，我们可以选择过去1小时，如果还是没有数据可以选择3小时\n\n![](../img/性能测试/grafana-添加模板7.png)\n\n可以看到现在查询出来了，最后我们点击apply\n\n![](../img/性能测试/grafana-添加模板8.png)\n\n回到模板首页，可以看到不报错了，数字也有了\n\n![](../img/性能测试/grafana-添加模板9.png)\n\n类似的，其他版块的也可以这样修改，其实原理很简单，就是通过在grafana选择一个展示的图表，然后在这个图表下配置查询influxDB的语句，选择好查询的数据，图表就展示出来了\n\n最后如果选择的listenter是这个，传给influxDB的值又不一样\n\n![](../img/性能测试/jmeter-选择不同jar.png)\n\n按名字猜（我没找到文档）这里connectTime应该是连接时间，duration是持续时间，ttfb是响应时间\n\n![](../img/性能测试/influxdb-ui-ttfb.png)\n# 6、这里以ttfb来做个图表再来举个例子\n这里我们退出刚才的模板，直接新建一个panel\n\n![](../img/性能测试/grafana-模板-新建.png)\n\npanel选择time series，数据源选择我们添加的influxDB\n\n![](../img/性能测试/grafana-模板-新建2.png)\n\ninfluxDB这里选择如图\n\n![](../img/性能测试/grafana-模板-新建3.png)\n\n因为还写不来这个SQL，先copy自动生成的吧\n\n![](../img/性能测试/grafana-模板-新建4.png)\n\n把SQL粘贴到\n\n![](../img/性能测试/grafana-模板-新建5.png)\n\n配置一下间隔和时间，也可以不用设置哦，反正随便调，测试嘛\n\n![](../img/性能测试/grafana-模板-新建6.png)\n\njmeter如果没有停，一直在循环的话，应该有如下效果\n\n效果如下\n\n![](../img/性能测试/grafana-模板-新建7.png)\n![](../img/性能测试/grafana-模板-新建8.png)\n\n改个名字\n\n![](../img/性能测试/grafana-模板-新建9.png)\n\n到仪表板观看效果，这样一个相应时间的时序图版块就OK了\n\n![](../img/性能测试/grafana-模板-新建10.png)\n\n再把其他字段也这样，选择一个相应的panel，填入SQL，一个大的仪表板就做好了，类似刚才的那个模板\n# 参考文章\n- https://www.jianshu.com/p/dd0456b8054c\n- https://juejin.cn/post/6844903847383547911\n"
  },
  {
    "path": "数据库/Elasticsearch.md",
    "content": "\n* [Elasticsearch](#elasticsearch)\n  * [es的特点](#es的特点)\n  * [应用场景](#应用场景)\n* [Elasticsearch基本概念](#elasticsearch基本概念)\n  * [索引(index)](#索引index)\n  * [类型(type)](#类型type)\n  * [文档(document)](#文档document)\n  * [映射(mapping)](#映射mapping)\n  * [倒排索引](#倒排索引)\n    * [Posting List](#posting-list)\n    * [Term Dictionary](#term-dictionary)\n    * [Term Index](#term-index)\n    * [FST(finite-state transducer有限状态转换器)](#fstfinite-state-transducer有限状态转换器)\n* [集群](#集群)\n  * [基本概念](#基本概念)\n    * [节点(Node)](#节点node)\n    * [集群(Cluster)](#集群cluster)\n    * [分片索引(Shard)](#分片索引shard)\n    * [索引副本(Replica)](#索引副本replica)\n  * [集群简单原理](#集群简单原理)\n  * [插入数据流程](#插入数据流程)\n    * [储存](#储存)\n  * [查询数据流程](#查询数据流程)\n* [选举算法](#选举算法)\n  * [7.X之前的选主流程](#7x之前的选主流程)\n    * [什么时候开始选主？](#什么时候开始选主)\n    * [Bully算法缺陷](#bully算法缺陷)\n      * [Master假死](#master假死)\n      * [脑裂](#脑裂)\n        * [如何解决脑裂问题](#如何解决脑裂问题)\n      * [仍然存在的问题](#仍然存在的问题)\n  * [7.X之后的选主流程](#7x之后的选主流程)\n    * [ES实现的Raft算法选主流程](#es实现的raft算法选主流程)\n      * [动态维护参选节点列表](#动态维护参选节点列表)\n* [es性能优化](#es性能优化)\n  * [加大filesystem cache大小](#加大filesystem-cache大小)\n  * [数据预热](#数据预热)\n  * [冷热分离](#冷热分离)\n  * [document设计](#document设计)\n  * [禁止直接分页](#禁止直接分页)\n* [es的分词器有哪些](#es的分词器有哪些)\n* [es为什么这么快](#es为什么这么快)\n* [参考文章](#参考文章)\n\n# Elasticsearch\n## es的特点\nelasticsearch 是一个兼有搜索引擎和NoSQL数据库功能的开源系统，基于Java/Lucene构建，可以用于全文搜索，结构化搜索以及近实时分析。可以说Lucene是当今最先进，最高效的全功能开源搜索引擎框架\n## 应用场景\n- 搜索\n- 监控\n# Elasticsearch基本概念\n## 索引(index)\nElasticSearch把数据存放到一个或者多个索引(indices)中。如果用关系型数据库模型对比，索引(index)的地位与数据库实例(database)相当\n## 类型(type)\n- 每个文档在ElasticSearch中都必须设定它的类型。文档类型使得同一个索引中在存储结构不同文档时，只需要依据文档类型就可以找到对应的参数映射(Mapping)信息，方便文档的存取。\n- 可以理解为关系型数据库里的表\n- 在7.x版本被移除\n## 文档(document)\n- 可以理解为关系数据库中的一行数据\n- 文档(Document)由一个或者多个域(Field)组成，每个域(Field)由一个域名(此域名非彼域名)和一个或者多个值组成(有多个值的值称为多值域(multi-valued))\n## 映射(mapping)\n- mapping是对索引库中的索引字段及其数据类型进行定义，类似于关系型数据库中的表结构。ES默认动态创建索引和索引类型的mapping，这就像是关系型数据中的，无需定义表结构，更不用指定字段的数据类型。当然也可以手动指定mapping类型。\n- 表结构\n## 倒排索引\n- 正向索引一般都是文档1->关键词1 位置->关键词2 位置\n- 倒排索引是 关键词1->文档1 位置  ->文档2 位置\n- 在搜索引擎中，每个文档都有一个对应的文档 ID，文档内容被表示为一系列关键词的集合。例如，文档 1 经过分词，提取了 20 个关键词，每个关键词都会记录它在文档中出现的次数和出现位置。那么，倒排索引就是关键词到文档 ID 的映射，每个关键词都对应着一系列的文件，这些文件中都出现了关键词。\n### Posting List\n上面所说的文章的ID就是被放在一个int的数组，存储了所有符合某个term的文档id，实际上，除此之外还包含：文档的数量、词条在每个文档中出现的次数、出现的位置、每个文档的长度、所有文档的平均长度等，在计算相关度时使用\n### Term Dictionary\n假设我们有很多个 term，比如：\n\n`Carla,Sara,Elin,Ada,Patty,Kate,Selena`\n\n如果按照这样的顺序排列，找出某个特定的 term 一定很慢，因为 term 没有排序，需要全部过滤一遍才能找出特定的 term。排序之后就变成了：\n\n`Ada,Carla,Elin,Kate,Patty,Sara,Selena`\n\n再使用二分查找可以用 logN 次磁盘查找得到目标。\n\n### Term Index\n但是磁盘的随机读操作仍然是非常昂贵的（一次 random access 大概需要 10ms 的时间）。所以尽量少的读磁盘，有必要把一些数据缓存到内存里。但是整个 term dictionary 本身又太大了，无法完整地放到内存里。于是就有了 term index。term index 有点像一本字典的大的章节表。比如：\n```java\nA 开头的 term ……………. Xxx 页\nC 开头的 term ……………. Yyy 页\nE 开头的 term ……………. Zzz 页\n```\n如果所有的 term 都是英文字符的话，可能这个 term index 就真的是 26 个英文字符表构成的了。但是实际的情况是，term 未必都是英文字符，term 可以是任意的 byte 数组。而且 26 个英文字符也未必是每一个字符都有均等的 term，比如 x 字符开头的 term 可能一个都没有，而 s 开头的 term 又特别多。实际的 term index 是一棵 trie 树（前缀树、字典树）：\n\n![](../img/数据库/elasticsearch/trem-index.png)\n\n例子是一个包含 \"A\", \"to\", \"tea\", \"ted\", \"ten\", \"i\", \"in\", 和 \"inn\" 的 trie 树。这棵树不会包含所有的 term，它包含的是 term 的一些前缀。通过 term index 可以快速地定位到 term dictionary 的某个 offset，然后从这个位置再往后顺序查找。\n### FST(finite-state transducer有限状态转换器)\n实际上，Lucene 内部的 Term Index 是用的「变种的」trie前缀树，即 FST 。FST 比 trie树好在哪？trie树只共享了前缀，而 FST 既共享前缀也共享后缀，更加的节省空间。\n\n# 数据如何存储\n\n\n# 集群\n## 基本概念\n### 节点(Node)\n一个es实例即为一个节点，一台机器可以有多个节点，正常使用下每个实例都应该会部署在不同的机器上\n### 集群(Cluster)\n一个ES集群由多个节点（node）组成， 每个集群都有一个共同的集群名称最为标识\n### 分片索引(Shard)\n如果我们的索引数据量很大，超过硬件存放单个文件的限制，就会影响查询请求的速度，ES引入了分片技术。一个分片本身就是一个完成的搜索引擎，文档存储在分片中，而分片会被分配到集群中的各个节点中，随着集群的扩大和缩小，ES会自动的将分片在节点之间进行迁移，以保证集群能保持一种平衡\n### 索引副本(Replica)\n副本（replica shard）就是shard的冗余备份，它的主要作用\n- 冗余备份，防止数据丢失；\n- shard异常时负责容错和负载均衡；\n## 集群简单原理\n![](../img/数据库/elasticsearch/es数据流程.png)\n- es的数据流程为index -> type -> mapping -> document -> field。\n- index被拆分为多个shard\n  - 多个shard分布在不同的机器上\n  - 不同的shard又有备份replica\n- ES 集群多个节点，会自动选举一个节点为 master 节点，这个 master 节点其实就是干一些管理的工作的，比如维护索引元数据、负责切换 primary shard 和 replica shard 身份等。要是 master 节点宕机了，那么会重新选举一个节点为 master 节点。\n- 如果是非 master 节点宕机了，那么会由 master 节点，让那个宕机节点上的 primary shard 的身份转移到其他机器上的 replica shard。接着你要是修复了那个宕机机器，重启了之后，master 节点会控制将缺失的 replica shard 分配过去，同步后续修改的数据之类的，让集群恢复正常。\n\n## 插入数据流程\n![](../img/数据库/elasticsearch/集群1.png)\t\t\n写索引是只能写在主分片上，然后同步到副本分片,但是上图中每个节点上都有主分片，一条数据是根据什么策略写到指定的分片上呢？\n- 这个过程是根据下面这个公式决定的\n  - `shard = hash(routing) % number_of_primary_shards`\n![](../img/数据库/elasticsearch/写数据流程.png)\t\t\n- （1）数据写请求发送到 node1 节点，通过路由计算得到值为1，那么对应的数据会应该在主分片S1上。\n- （2）node1节点将请求转发到 S1 主分片所在的节点node2，node2 接受请求并写入到磁盘。\n- （3）并发将数据复制到三个副本分片R1上，其中通过乐观并发控制数据的冲突。一旦所有的副本分片都报告成功，则节点 node2将向node1节点报告成功，然后node1节点向客户端报告成功。\n\n### 储存\n经过Lucene进行分词等预处理后数据先储存在内存中，并将此次操作写入事务日志(tansLog)，然后每隔一秒将内存的数据刷入segment缓存中，segment保证此时的数据已经可以被检索了，然后Lucene每隔30分钟或者segment的size大于512M时刷入磁盘\n\n## 查询数据流程\n- 客户端发送请求到一个 coordinate node 。\n- 协调节点将搜索请求转发到所有的 shard 对应的 primary shard 或 replica shard ，都可以。\n- query phase：每个 shard 将自己的搜索结果（其实就是一些 doc id ）返回给协调节点，由协调节点进行数据的合并、排序、分页等操作，产出最终结果。\n- fetch phase：接着由协调节点根据 doc id 去各个节点上拉取实际的 document 数据，最终返回给客户端。\n- Elasticsearch查询方式\n  - 查询的方式简单分为两种\n    - 通过ID搜索出对应的Doc\n      - 检索内存的Translog文件\n      - 检索硬盘的Translog文件\n      - 检索硬盘的Segement文件\n    - 通过query匹配相关的Doc\n      - 从内存和硬盘的Segement文件中查找\n  - 查询可以分为三个阶段\n    - QUERY_AND_FETCH （查询完就返回整个Doc内容）\n    - QUERY_THEN_FETCH （先查询出对应的Doc id ，然后再根据Doc id 匹配去对应的文档）\n    - DFS_QUERY_THEN_FETCH （先算分，再查询）\n  - 用得最多的就是QUERY_THEN_FETCH\n    - 向各个主分片和副本分片分发请求\n    - 得到各个节点返回的doc id，组成doc id集合\n    - 再次请求各个分片拿到对应的完整Doc\n\n# 选举算法\n## 7.X之前的选主流程\nZen Discovery\n\n采用Bully算法，它假定所有节点都有一个唯一的ID，使用该ID对节点进行排序。任何时候的当前Leader都是参与集群的最高ID节点。该算法的优点是易于实现。但是，当拥有最大ID的节点处于不稳定状态的场景下会有问题。例如，Master负载过重而假死，集群拥有第二大ID的节点被选为新主，这时原来的Master恢复，再次被选为新主，然后又假死\n\nES 通过推迟选举，直到当前的 Master 失效来解决上述问题，只要当前主节点不挂掉，就不重新选主。但是容易产生脑裂（双主），为此，再通过“法定得票人数过半”解决脑裂问题\n\n只有一个 Leader将当前版本的全局集群状态推送到每个节点。 ZenDiscovery（默认）过程就是这样的:\n- 每个节点计算最高的已知节点ID，并向该节点发送领导投票\n- 如果一个节点收到足够多的票数，并且该节点也为自己投票，那么它将扮演领导者的角色，开始发布集群状态。\n- 所有节点都会参数选举,并参与投票,但是,只有有资格成为 master 的节点的投票才有效.\n\n有多少选票赢得选举的定义就是所谓的法定人数。 在弹性搜索中，法定大小是一个可配置的参数。 （一般配置成:可以成为master节点数n/2+1）\n\n### 什么时候开始选主？\n- 集群启动\n- Master 失效\n  - 非 Master 节点运行的 MasterFaultDetection 检测到 Master 失效,在其注册的 listener 中执行 handleMasterGone,执行 rejoin 操作,重新选主.注意,即使一个节点认为 Master 失效也会进入选主流程\n\n### Bully算法缺陷\n#### Master假死\nMaster节点承担的职责负载过重的情况下，可能无法即时对组内成员作出响应，这种便是假死。例如一个集群中的Master假死，其他节点开始选主，刚刚选主成功，原来的Master恢复了，因为原来Master节点的Id优先级最高，又开始一轮选主，重新把原来Master选举为Master\n\n为了解决这个问题，当Master节点假死的时候会去探测是是不是真的挂了，如果不是会继续推迟选主过程\n\n#### 脑裂\n当发生网络分区故障就会发生脑裂，就会出现双主情况，那么这个时候是十分危险的因为两个新形成的集群会同时索引和修改集群的数据。\n\n##### 如何解决脑裂问题\n\n有一个minimum_master_nodes设置，集群中节点必须大于这个数字才进行选主，否则不进行，例如10个节点，现在发生网络分区，3个节点一组，7个节点另外一组，最开始master在3节点那边\n\nminimum_master_nodes设置5\n\n这个时候，3节点那组，master会判断有7个节点退出，然后检查minimum_master_nodes，发现小于配置，放弃master节点身份，重新开始选主，但是因为minimum_master_nodes配置，无法进行选主\n\n7节点那组，发现master真的凉了，联系不上了，开始选主，选出新的Master节点\n\n#### 仍然存在的问题\nZen的minimum_master_nodes设置经常配置错误，这会使群集更容易出现裂脑和丢失数据的风险\n\n## 7.X之后的选主流程\n7.X之后的ES，采用一种新的选主算法，实际上是 Raft 的实现，但并非严格按照 Raft 论文实现，而是做了一些调整。\n\nRaft是工程上使用较为广泛分布式共识协议，是多个节点对某个事情达成一致的看法，即使是在部分节点故障、网络延时、网络分区的情况下。\n\n其设计原则如下：\n- 容易理解\n- 减少状态的数量，尽可能消除不确定性\n\nRaft 将问题分解为：Leader 选举，日志复制，安全性，将这三个问题独立思考。\n\n在 Raft 中，节点可能的状态有三种，其转换关系如下：\n\n![](../img/数据库/elasticsearch/raft节点状态.png)\n\n正常情况下，集群中只有一个 Leader，其他节点全是 Follower 。Follower 都是被动接收请求，从不发送主动任何请求。Candidate 是从 Follower 到 Leader的中间状态。\n\nRaft 中引入任期（term）的概念，每个 term 内最多只有一个 Leader。term在 Raft 算法中充当逻辑时钟的作用。服务器之间通信的时候会携带这个 term，如果节点发现消息中的 term小于自己的 term，则拒绝这个消息，如果大于本节点的 term，则更新自己的 term。如果一个 Candidate 或者 Leader 发现自己的任期号过期了，它会立即回到 Follower 状态。\n\nRaft 选举流程为：\n\n- 增加节点本地的 current term ，切换到Candidate状态\n- 投自己一票\n- 并行给其他节点发送 RequestVote RPC（让大家投他）。\n\n然后等待其他节点的响应，会有如下三种结果：\n\n- 如果接收到大多数服务器的选票，那么就变成Leader\n- 如果收到了别人的投票请求，且别人的term比自己的大，那么候选者退化为follower\n- 如果选举过程超时，再次发起一轮选举\n\n通过下面的约束来确定唯一 Leader（选举安全性）：\n\n- 同一任期内，每个节点只有一票\n- 得票(日志信息不旧于Candidate的日志信息)过半则当选为 Leader\n- 成为 Leader 后，向其他节点发送心跳消息来确定自己的地位并阻止新的选举。\n\n当同时满足以下条件时，Follower同意投票：\n\n- RequestVote请求包含的term大于等于当前term\n- 日志信息不旧于Candidate的日志信息\n- first-come-first-served 先来先得\n\n### ES实现的Raft算法选主流程\nES 实现的 Raft 中，选举流程与标准的有很多区别：\n\n- 初始为 Candidate状态\n- 执行 PreVote 流程，并拿到 maxTermSeen\n- 准备 RequestVote 请求（StartJoinRequest），基于maxTermSeen，将请求中的 term 加1（尚未增加节点当前 term）\n- 并行发送 RequestVote，异步处理。目标节点列表中包括本节点。\n\nES 实现中，候选人不先投自己，而是直接并行发起 RequestVote，这相当于候选人有投票给其他候选人的机会。这样的好处是可以在一定程度上避免3个节点同时成为候选人时，都投自己，无法成功选主的情况。\n\nES 不限制每个节点在某个 term 上只能投一票，节点可以投多票，这样会产生选出多个主的情况：\n\n![](../img/数据库/elasticsearch/多主情况.png)\n\n- Node2被选为主：收到的投票为：Node2,Node3\n- Node3被选为主：收到的投票为：Node3,Node1\n\n对于这种情况，ES 的处理是让最后当选的 Leader 成功。作为 Leader，如果收到 RequestVote请求，他会无条件退出 Leader 状态。在本例中，Node2先被选为主，随后他收到 Node3的 RequestVote 请求，那么他退出 Leader 状态，切换为CANDIDATE，并同意向发起 RequestVote候选人投票。因此最终 Node3成功当选为 Leader。\n\n#### 动态维护参选节点列表\n在此之前，我们讨论的前提是在集群节点数量不变的情况下，现在考虑下集群扩容，缩容，节点临时或永久离线时是如何处理的。在7.x 之前的版本中，用户需要手工配置 minimum_master_nodes，来明确告诉集群过半节点数应该是多少，并在集群扩缩容时调整他。现在，集群可以自行维护。\n\n在取消了discovery.zen.minimum_master_nodes配置后，ES 如何判断多数？是自己计算和维护minimum_master_nodes值么？不，现在的做法不再记录“quorum” 的具体数值，取而代之的是记录一个节点列表，这个列表中保存所有具备 master 资格的节点（有些情况下不是这样，例如集群原本只有1个节点，当增加到2个的时候，这个列表维持不变，因为如果变成2，当集群任意节点离线，都会导致无法选主。这时如果再增加一个节点，集群变成3个，这个列表中就会更新为3个节点），称为 VotingConfiguration，他会持久化到集群状态中。\n\n在节点加入或离开集群之后，Elasticsearch会自动对VotingConfiguration做出相应的更改，以确保集群具有尽可能高的弹性。在从集群中删除更多节点之前，等待这个调整完成是很重要的。你不能一次性停止半数或更多的节点。（感觉大面积缩容时候这个操作就比较感人了，一部分一部分缩）\n\n默认情况下，ES 自动维护VotingConfiguration，有新节点加入的时候比较好办，但是当有节点离开的时候，他可能是暂时的重启，也可能是永久下线。你也可以人工维护 VotingConfiguration，配置项为：cluster.auto_shrink_voting_configuration，当你选择人工维护时，有节点永久下线，需要通过 voting exclusions API 将节点排除出去。如果使用默认的自动维护VotingConfiguration，也可以使用 voting exclusions API 来排除节点，例如一次性下线半数以上的节点。\n\n如果在维护VotingConfiguration时发现节点数量为偶数，ES 会将其中一个排除在外，保证 VotingConfiguration是奇数。因为当是偶数的情况下，网络分区将集群划分为大小相等的两部分，那么两个子集群都无法达到“多数”的条件。\n\n# es性能优化\n## 加大filesystem cache大小\nes 的搜索引擎严重依赖于底层的 filesystem cache ，你如果给 filesystem cache 更多的内存，尽量让内存可以容纳所有的 idx segment file  索引数据文件，那么你搜索的时候就基本都是走内存的，性能会非常高 \n## 数据预热\n先查数据出来讲热数据留在cache里\n## 冷热分离\n热数据和冷数据单独设计index，确保热数据不会被冷数据冲掉\n## document设计\n把查询字段放入es即可，其他的全量数据存入数据库，比如hbase，常见的场景就是es存索引，HBASE存数据全部字段\n## 禁止直接分页\n拉下才能查看下一页，用上一页的scroll  id\n\n# es的分词器有哪些\n- ik分词器\n- jeba分词器\n\n# es为什么这么快\n- 倒排索引\n\n# es 的分页方案\n## form size\n使用from和size参数进行分页，可以通过指定起始位置和返回的文档数量来获取分页数据。例如，如果要获取第 10-20 条文档，可以设置from为9，size为10，即：\n```sql\nGET /my_index/_search\n{\n    \"from\": 9,\n    \"size\": 10,\n    \"query\": {\n        \"match_all\": {}\n    }\n}\n\n```\n## 滚动查询\nscroll API 允许在结果集中使用游标来滚动浏览结果，从而进行分页。这个API通常用于处理大型结果集。使用scroll API进行分页，需要在第一次查询中设置scroll参数，该参数指定了结果集的存活时间，然后使用scroll_id来获取下一页结果集。例如，假设要获取第 10-20 条文档，可以使用以下步骤：\n\n第一次查询：\n```sql\nGET /my_index/_search?scroll=1m\n{\n    \"size\": 10,\n    \"query\": {\n        \"match_all\": {}\n    }\n}\n\n```\n在第一次查询中，指定了scroll参数，并设置size为10，表示每次返回10条文档。查询返回的结果中包含了scroll_id。\n\n获取下一页结果集：\n```sql\nGET /_search/scroll\n{\n    \"scroll_id\": \"scroll_id\",\n    \"scroll\": \"1m\"\n}\n\n```\n## search after\nES（Elasticsearch）的 Search After 是一种基于游标的分页方式，可以用于获取大量数据时的分页操作。相比于使用 from 和 size 参数的方式，Search After 在处理大数据量时更加高效。\n\nSearch After 使用一个类似于游标的方式，在查询后保存上一页的最后一条文档的排序字段值，并在下一页查询时使用该值作为起始点来获取下一页的结果集。以下是 Search After 的使用方法：\n\n首先进行第一次查询，不需要指定 Search After 参数：\n```sql\nGET /my_index/_search\n{\n    \"size\": 10,\n    \"sort\": [\n        {\"timestamp\": \"desc\"},\n        {\"_id\": \"asc\"}\n    ],\n    \"query\": {\n        \"match_all\": {}\n    }\n}\n\n```\n在上面的示例中，我们使用 sort 参数按照 timestamp 降序排列，并且使用 _id 升序排列。这是为了保证结果的稳定性，因为在查询过程中可能会有新的文档被添加进来。查询返回的结果中包含了第一页的文档数据和 Search After 参数值。\n\n在获取下一页时，需要将上一页的最后一个文档的排序字段值作为 Search After 参数传入查询中：\n```sql\nGET /my_index/_search\n{\n    \"size\": 10,\n    \"sort\": [\n        {\"timestamp\": \"desc\"},\n        {\"_id\": \"asc\"}\n    ],\n    \"search_after\": [last_timestamp, last_id],\n    \"query\": {\n        \"match_all\": {}\n    }\n}\n\n```\n在上面的示例中，我们将上一页最后一个文档的 timestamp 和 _id 作为 Search After 参数传入查询中，以获取下一页的文档数据。重复执行该步骤，直到获取到指定范围的文档数据。\n\n注意，使用 Search After 时需要注意以下几点：\n\n需要使用唯一的排序字段，以保证分页的正确性。\n在排序字段相同时，需要使用唯一的 _id 排序，以保证分页的正确性。\n在处理大数据量时，需要适当调整分页大小，避免查询效率过低。\n# es 的查询流程\nES（Elasticsearch）集群的查询流程如下：\n- 客户端向任意一个节点发送查询请求；\n- 接收到查询请求的节点是协调节点，它会负责查询的协调工作，包括解析查询语句、路由计算、分片的选择等；\n- 协调节点会根据查询条件、索引配置等信息计算出需要查询哪些分片，然后将查询请求转发给对应的数据节点；\n- 数据节点接收到查询请求后，会在本地执行查询操作，并返回查询结果给协调节点；\n- 协调节点将所有分片的查询结果进行合并，计算排名等信息，并将最终结果返回给客户端。\n\n在查询流程中，协调节点负责协调各个数据节点的工作，而数据节点负责实际的查询操作。为了提高查询效率，ES会根据查询条件和数据分布情况选择合适的分片进行查询，从而减少数据的传输和处理。\n\n值得注意的是，ES集群中的任意一个节点都可以充当协调节点或数据节点。这意味着，如果一个节点宕机或者网络故障，查询请求会被自动转发到其他节点上继续处理，从而保证了查询的可用性和稳定性。\n\n# 参考文章\n- https://blog.csdn.net/qq_34820803/article/details/104798716\n- https://blog.csdn.net/qq_33330687/article/details/105681994\n"
  },
  {
    "path": "数据库/Hbase.md",
    "content": "\n* [HBASE](#hbase)\n    * [什么是？](#什么是)\n    * [列式存储](#列式存储)\n        * [储存图](#储存图)\n        * [Row Key](#row-key)\n        * [列族ColumnFamily](#列族columnfamily)\n        * [列：属于某一个列簇，在 HBase 中可以进行动态的添加](#列属于某一个列簇在-hbase-中可以进行动态的添加)\n        * [Cell : 是指具体的 Value](#cell--是指具体的-value)\n        * [TimeStamp ：在这张图里面没有显示出来，这个是指版本号，用时间戳（TimeStamp ）来表示。](#timestamp-在这张图里面没有显示出来这个是指版本号用时间戳timestamp-来表示)\n    * [架构](#架构)\n        * [架构图](#架构图)\n* [HBase 架构组件](#hbase-架构组件)\n    * [Regions](#regions)\n    * [HBase Master](#hbase-master)\n    * [Zookeeper](#zookeeper)\n    * [这些组件是如何一起工作的](#这些组件是如何一起工作的)\n    * [第一次读和写操作](#第一次读和写操作)\n    * [HBase Meta Table](#hbase-meta-table)\n    * [Region Server 组成](#region-server-组成)\n    * [HBase 写数据步骤](#hbase-写数据步骤)\n    * [HBase MemStore](#hbase-memstore)\n    * [HBase Region Flush](#hbase-region-flush)\n    * [HBase HFile](#hbase-hfile)\n        * [HBase HFile 文件结构](#hbase-hfile-文件结构)\n        * [HFile 索引](#hfile-索引)\n    * [HBase Read 合并](#hbase-read-合并)\n    * [HBase Minor Compaction](#hbase-minor-compaction)\n    * [HBase Major Compaction](#hbase-major-compaction)\n    * [Region = Contiguous Keys](#region--contiguous-keys)\n    * [Region 分裂](#region-分裂)\n    * [Region 负载均衡](#region-负载均衡)\n    * [HDFS 数据备份](#hdfs-数据备份)\n    * [HBase 故障恢复](#hbase-故障恢复)\n* [Apache HBase 架构的优缺点](#apache-hbase-架构的优缺点)\n    * [优点](#优点)\n    * [缺点](#缺点)\n* [参考文章](#参考文章)\n\n\n# HBASE\n## 什么是？\nHbase 是分布式、面向列的开源数据库（其实准确的说是面向列族）。HDFS 为 Hbase 提供可靠的底层数据存储服务，MapReduce 为 Hbase 提供高性能的计算能力，Zookeeper 为 Hbase 提供稳定服务和 Failover 机制，因此我们说 Hbase 是一个通过大量廉价的机器解决海量数据的高速存储和读取的分布式数据库解决方案。\n## 列式存储\n### 储存图\n![](../img/数据库/HBASE/储存图1.png)\n![](../img/数据库/HBASE/储存图2.png)\n### Row Key\n- 主键 不宜过长\n- 扫描的方式\n  - 基于row key的单行查询\n  - 基于row key的范围查询\n  - 全表扫描\n### 列族ColumnFamily\n- 列族下面可以包含任意多的列\n- 官方的推荐最好是小于等于3\n### 列：属于某一个列簇，在 HBase 中可以进行动态的添加\n### Cell : 是指具体的 Value\n### TimeStamp ：在这张图里面没有显示出来，这个是指版本号，用时间戳（TimeStamp ）来表示。\nTimeStamp 是实现 Hbase 多版本的关键。在 Hbase 中使用不同的 timestame 来标识相同 rowkey 行对应的不通版本的数据。在写入数据的时候，如果用户没有指定对应的 timestamp，Hbase 会自动添加一个 timestamp，timestamp 和服务器时间保持一致。在\nHbase 中，相同 rowkey 的数据按照 timestamp 倒序排列。默认查询的是最新的版本，用户 可同指定 timestamp 的值来读取旧版本的数据。\n## 架构\n### 架构图\n![](../img/数据库/HBASE/架构图.png)\n\n# HBase 架构组件\n物理上，Hbase 是由三种类型的 server 组成的的主从式（master-slave）架构：\n\n- Region Server 负责处理数据的读写请求，客户端请求数据时直接和 Region Server 交互。\n- HBase Master 负责 Region 的分配，DDL（创建，删除 table）等操作。\n- Zookeeper，作为 HDFS 的一部分，负责维护集群状态。\n\n当然底层的存储都是基于 Hadoop HDFS 的：\n\n- Hadoop DataNode 负责存储 Region Server 所管理的数据。所有的 HBase 数据都存储在 HDFS 文件中。Region Server 和 HDFS DataNode 往往是分布在一起的，这样 Region Server 就能够实现数据本地化（data locality，即将数据放在离需要者尽可能近的地方）。HBase 的数据在写的时候是本地的，但是当 region 被迁移的时候，数据就可能不再满足本地性了，直到完成 compaction，才能又恢复到本地。\n- Hadoop NameNode 维护了所有 HDFS 物理 data block 的元信息。\n\n![](../img/数据库/HBASE/h1.png)\n\n## Regions\nHBase 表（Table）根据 rowkey 的范围被水平拆分成若干个 region。每个 region 都包含了这个region 的 start key 和 end key 之间的所有行（row）。Regions 被分配给集群中的某些节点来管理，即 Region Server，由它们来负责处理数据的读写请求。每个 Region Server 大约可以管理 1000 个 regions。\n\n![](../img/数据库/HBASE/region.png)\n\n## HBase Master\n也叫 HMaster，负责 Region 的分配，DDL（创建，删除表）等操作：\n\n统筹协调所有 region server：\n\n- 启动时分配 regions，在故障恢复和负载均衡时重分配 regions\n- 监控集群中所有 Region Server 实例（从 Zookeeper 获取通知信息）\n\n管理员功能：\n- 提供创建，删除和更新 HBase Table 的接口\n\n![](../img/数据库/HBASE/hmaster.png)\n\n## Zookeeper\nHBase 使用 Zookeeper 做分布式管理服务，来维护集群中所有服务的状态。Zookeeper 维护了哪些 servers 是健康可用的，并且在 server 故障时做出通知。Zookeeper 使用一致性协议来保证分布式状态的一致性。注意这需要三台或者五台机器来做一致性协议。\n\n![](../img/数据库/HBASE/zookeeper.png)\n\n## 这些组件是如何一起工作的\nZookeeper 用来协调分布式系统中集群状态信息的共享。Region Servers 和 在线 HMaster（active HMaster）和 Zookeeper 保持会话（session）。Zookeeper 通过心跳检测来维护所有临时节点（ephemeral nodes）。\n\n![](../img/数据库/HBASE/如何共同工作.png)\n\n每个 Region Server 都会创建一个 ephemeral 节点。HMaster 会监控这些节点来发现可用的 Region Servers，同样它也会监控这些节点是否出现故障。\n\nHMaster 们会竞争创建 ephemeral 节点，而 Zookeeper 决定谁是第一个作为在线 HMaster，保证线上只有一个 HMaster。在线 HMaster（active HMaster） 会给 Zookeeper 发送心跳，不在线的待机 HMaster （inactive HMaster） 会监听 active HMaster 可能出现的故障并随时准备上位。\n\n如果有一个 Region Server 或者 HMaster 出现故障或各种原因导致发送心跳失败，它们与 Zookeeper 的 session 就会过期，这个 ephemeral 节点就会被删除下线，监听者们就会收到这个消息。Active HMaster 监听的是 region servers 下线的消息，然后会恢复故障的 region server 以及它所负责的 region 数据。而 Inactive HMaster 关心的则是 active HMaster 下线的消息，然后竞争上线变成 active HMaster。\n\n（点评：这一段非常重要，涉及到分布式系统设计中的一些核心概念，包括集群状态、一致性等。可以看到 Zookeeper 是沟通一切的桥梁，所有的参与者都和 Zookeeper 保持心跳会话，并从 Zookeeper 获取它们需要的集群状态信息，来管理其它节点，转换角色，这也是分布式系统设计中很重要的思想，由专门的服务来维护分布式集群状态信息。）\n\n## 第一次读和写操作\n有一个特殊的 HBase Catalog 表叫 Meta table（它其实是一张特殊的 HBase 表），包含了集群中所有 regions 的位置信息。Zookeeper 保存了这个 Meta table 的位置。\n\n当 HBase 第一次读或者写操作到来时：\n\n- 客户端从 Zookeeper 那里获取是哪一台 Region Server 负责管理 Meta table。\n- 客户端会查询那台管理 Meta table 的 Region Server，进而获知是哪一台 Region Server 负责管理本次数据请求所需要的 rowkey。客户端会缓存这个信息，以及 Meta table 的位置信息本身。\n- 然后客户端回去访问那台 Region Server，获取数据。\n\n对于以后的的读请求，客户端从可以缓存中直接获取 Meta table 的位置信息（在哪一台 Region Server 上），以及之前访问过的 rowkey 的位置信息（哪一台 Region Server 上），除非因为 Region 被迁移了导致缓存失效。这时客户端会重复上面的步骤，重新获取相关位置信息并更新缓存。\n\n![](../img/数据库/HBASE/第一次读写.png)\n\n（点评：客户端读写数据，实际上分了两步：第一步是定位，从 Meta table 获取 rowkey 属于哪个 Region Server 管理；第二步再去相应的 Region Server 读写数据。这里涉及到了两个 Region Server，要理解它们各自的角色功能。关于 Meta table 下面会详细介绍。）\n\n## HBase Meta Table\n\nMeta table 是一个特殊的 HBase table，它保存了系统中所有的 region 列表。这张 table 类似一个 b-tree，结构大致如下：\n\n- Key：table, region start key, region id\n- Value：region server\n\n![](../img/数据库/HBASE/hbase-meta-table.png)\n\n## Region Server 组成\nRegion Server 运行在 HDFS DataNode 上，由以下组件组成：\n\n- `WAL`：Write Ahead Log 是分布式文件系统上的一个文件，用于存储新的还未被持久化存储的数据，它被用来做故障恢复。\n- `BlockCache`：这是读缓存，在内存中存储了最常访问的数据，是 LRU（Least Recently Used）缓存。\n- `MemStore`：这是写缓存，在内存中存储了新的还未被持久化到硬盘的数据。当被写入硬盘时，数据会首先被排序。注意每个 Region 的每个 Column Family 都会有一个 MemStore。\n- `HFile` 在硬盘上（HDFS）存储 HBase 数据，以有序 KeyValue 的形式。\n\n![](../img/数据库/HBASE/regionServer.png)\n\n（点评：这一段是重中之重，理解 Region Server 的组成对理解 HBase 的架构至关重要，要充分认识 Region Server 的功能，以及每个组件的作用，这些组件的行为和功能在后续的段落中都会一一展开。）\n\n\n## HBase 写数据步骤\n当客户端发起一个写数据请求（Put 操作），第一步首先是将数据写入到 WAL 中：\n\n- 新数据会被追加到 WAL 文件尾部。\n- WAL 用来在故障恢复时恢复还未被持久化的数据。\n\n![](../img/数据库/HBASE/hbase写入1.png)\n\n数据被写入 WAL 后，会被加入到 MemStore 即写缓存。然后服务端就可以向客户端返回 ack 表示写数据完成。\n\n（点评：注意数据写入时 WAL 和 MemStore 更新的顺序，不能调换，必须先 WAL 再 MemStore。如果反过来，先更新完 MemStore，此时 Region Server 发生 crash，内存中的更新就丢失了，而此时数据还未被持久化到 WAL，就无法恢复了。理论上 WAL 就是 MemStore 中数据的一个镜像，应该保持一致，除非发生系统 crash。另外注意更新 WAL 是在文件尾部追加的方式，这种磁盘操作性能很高，不会太影响请求的整体响应时间。）\n\n![](../img/数据库/HBASE/hbase写入2.png)\n\n## HBase MemStore\nMemStore 在内存中缓存 HBase 的数据更新，以有序 KeyValues 的形式，这和 HFile 中的存储形式一样。每个 Column Family 都有一个 MemStore，所有的更新都以 Column Family 为单位进行排序。\n\n![](../img/数据库/HBASE/memStore.png)\n\n## HBase Region Flush\nMemStore 中累积了足够多的的数据后，整个有序数据集就会被写入一个新的 HFile 文件到 HDFS 上。HBase 为每个 Column Family 都创建一个 HFile，里面存储了具体的 Cell，也即 KeyValue 数据。随着时间推移，HFile 会不断产生，因为 KeyValue 会不断地从 MemStore 中被刷写到硬盘上。\n\n注意这也是为什么 HBase 要限制 Column Family 数量的一个原因。每个 Column Family 都有一个 MemStore；如果一个 MemStore 满了，所有的 MemStore 都会被刷写到硬盘。同时它也会记录最后写入的数据的最大序列号（sequence number），这样系统就能知道目前为止哪些数据已经被持久化了。\n\n最大序列号是一个 meta 信息，被存储在每个 HFile 中，来表示持久化进行到哪条数据了，应该从哪里继续。当 region 启动时，这些序列号会被读取，取其中最大的一个，作为基础序列号，后面的新的数据更新就会在该值的基础上递增产生新的序列号。\n\n![](../img/数据库/HBASE/regionFlush.png)\n\n（点评：这里有个序列号的概念，每次 HBase 数据更新都会绑定一个新的自增序列号。而每个 HFile 则会存储它所保存的数据的最大序列号，这个元信息非常重要，它相当于一个 commit point，告诉我们在这个序列号之前的数据已经被持久化到硬盘了。它不仅在 region 启动时会被用到，在故障恢复时，也能告诉我们应该从 WAL 的什么位置开始回放数据的历史更新记录。）\n\n## HBase HFile\n数据存储在 HFile 中，以 Key/Value 形式。当 MemStore 累积了足够多的数据后，整个有序数据集就会被写入一个新的 HFile 文件到 HDFS 上。整个过程是一个顺序写的操作，速度非常快，因为它不需要移动磁盘头。（注意 HDFS 不支持随机修改文件操作，但支持 append 操作。）\n\n![](../img/数据库/HBASE/hflie.png)\n\n### HBase HFile 文件结构\nHFile 使用多层索引来查询数据而不必读取整个文件，这种多层索引类似于一个 B+ tree：\n\n- KeyValues 有序存储。\n- rowkey 指向 index，而 index 则指向了具体的 data block，以 64 KB 为单位。\n- 每个 block 都有它的叶索引。\n- 每个 block 的最后一个 key 都被存储在中间层索引。\n- 索引根节点指向中间层索引。\n\ntrailer 指向原信息数据块，它是在数据持久化为 HFile 时被写在 HFile 文件尾部。trailer 还包含例如布隆过滤器和时间范围等信息。布隆过滤器用来跳过那些不包含指定 rowkey 的文件，时间范围信息则是根据时间来过滤，跳过那些不在请求的时间范围之内的文件。\n![](../img/数据库/HBASE/hfile文件结构.png)\n\n### HFile 索引\n刚才讨论的索引，在 HFile 被打开时会被载入内存，这样数据查询只要一次硬盘查询。\n\n![](../img/数据库/HBASE/hfile索引.png)\n\n## HBase Read 合并\n我们已经发现，每行（row）的 KeyValue cells 可能位于不同的地方，这些 cell 可能被写入了 HFile，可能是最近刚更新的，还在 MemStore 中，也可能最近刚读过，缓存在 Block Cache 中。所以，当你读一行 row 时，系统怎么将对应的 cells 返回呢？一次 read 操作会将 Block Cache，MemStore 和 HFile 中的 cell 进行合并：\n\n- 首先 scanner 从 Block Cache 读取 cells。最近读取的 KeyValue 都被缓存在这里，这是 一个 LRU 缓存。\n- 然后 scanner 读取 MemStore，即写缓存，包含了最近更新的数据。\n- 如果 scanner 没有在 BlockCache 和 MemStore 都没找到对应的 cells，则 HBase 会使用 Block Cache 中的索引和布隆过滤器来加载对应的 HFile 到内存，查找到请求的 row cells。\n\n![](../img/数据库/HBASE/hbaseRead合并.png)\n\n之前讨论过，每个 MemStore 可能会有多个 HFile，所以一次 read 请求可能需要多读个文件，这可能会影响性能，这被称为读放大（read amplification）。\n\n（点评：从时间轴上看，一个个的 HFile 也是有序的，本质上它们保存了每个 region 的每个 column family 的数据历史更新。所以对于同一个 rowkey 的同一个 cell，它可能也有多个版本的数据分布在不同的 HFile 中，所以可能需要读取多个 HFiles，这样性能开销会比较大，尤其是当不满足 data locality 时这种 read amplification 情况会更加严重。这也是后面会讲到的 compaction 必要的原因）\n\n![](../img/数据库/HBASE/hbaseRead合并2.png)\n\n## HBase Minor Compaction\n\nHBase 会自动合并一些小的 HFile，重写成少量更大的 HFiles。这个过程被称为 minor compaction。它使用归并排序算法，将小文件合并成大文件，有效减少 HFile 的数量。\n\n![](../img/数据库/HBASE/hbaseMinorCompaction.png)\n\n## HBase Major Compaction\nMajor Compaction 合并重写每个 Column Family 下的所有的 HFiles，成为一个单独的大 HFile，在这个过程中，被删除的和过期的 cell 会被真正从物理上删除，这能提高读的性能。但是因为 major compaction 会重写所有的 HFile，会产生大量的硬盘 I/O 和网络开销。这被称为写放大（Write Amplification）。\n\nMajor compaction 可以被设定为自动调度。因为存在 write amplification 的问题，major compaction 一般都安排在周末和半夜。MapR 数据库对此做出了改进，并不需要做 compaction。Major compaction 还能将因为服务器 crash 或者负载均衡导致的数据迁移重新移回到离 Region Server 的地方，这样就能恢复 data locality。\n\n![](../img/数据库/HBASE/hbaseMajorCompaction.png)\n\n## Region = Contiguous Keys\n我们再来回顾一下 region 的概念：\n\n- HBase Table 被水平切分成一个或数个 regions。每个 region 包含了连续的，有序的一段 rows，以 start key 和 end key 为边界。\n- 每个 region 的默认大小为 1GB。\n- region 里的数据由 Region Server 负责读写，和 client 交互。\n- 每个 Region Server 可以管理约 1000 个 regions（它们可能来自一张表或者多张表）。\n\n![](../img/数据库/HBASE/regioninfo.png)\n\n## Region 分裂\n一开始每个 table 默认只有一个 region。当一个 region 逐渐变得很大时，它会分裂（split）成两个子 region，每个子 region 都包含了原来 region 一半的数据，这两个子 region 并行地在原来这个 region server 上创建，这个分裂动作会被报告给 HMaster。处于负载均衡的目的，HMaster 可能会将新的 region 迁移给其它 region server。\n\n![](../img/数据库/HBASE/region分裂.png)\n\n## Region 负载均衡\nSplitting 一开始是发生在同一台 region server 上的，但是出于负载均衡的原因，HMaster 可能会将新的 regions 迁移给其它 region server，这会导致那些 region server 需要访问离它比较远的 HDFS 数据，直到 major compaction 的到来，它会将那些远方的数据重新移回到离 region server 节点附近的地方。\n\n（点评：注意这里的迁移的概念，只是逻辑上的迁移，即将某个 region 交给另一个 region server 管理。）\n\n![](../img/数据库/HBASE/region负载均衡.png)\n\n## HDFS 数据备份\n所有的读写都发生在 HDFS 的主 DataNode 节点上。 HDFS 会自动备份 WAL 和 HFile 的文件 blocks。HBase 依赖于 HDFS 来保证数据完整安全。当数据被写入 HDFS 时，一份会写入本地节点，另外两个备份会被写入其它节点。\n\n![](../img/数据库/HBASE/hdfs数据备份.png)\n\nWAL 和 HFiles 都会持久化到硬盘并备份。那么 HBase 是怎么恢复 MemStore 中还未被持久化到 HFile 的数据呢？下面的章节会讨论这个问题。\n\n![](../img/数据库/HBASE/hdfs数据备份2.png)\n\n## HBase 故障恢复\n当某个 Region Server 发生 crash 时，它所管理的 region 就无法被访问了，直到 crash 被检测到，然后故障恢复完成，这些 region 才能恢复访问。Zookeeper 依靠心跳检测发现节点故障，然后 HMaster 会收到 region server 故障的通知。\n\n当 HMaster 发现某个 region server 故障，HMaster 会将这个 region server 所管理的 regions 分配给其它健康的 region servers。为了恢复故障的 region server 的 MemStore 中还未被持久化到 HFile 的数据，HMaster 会将 WAL 分割成几个文件，将它们保存在新的 region server 上。每个 region server 然后回放各自拿到的 WAL 碎片中的数据，来为它所分配到的新 region 建立 MemStore。\n\n![](../img/数据库/HBASE/hbase故障恢复.png)\n\nWAL 包含了一系列的修改操作，每个修改都表示一个 put 或者 delete 操作。这些修改按照时间顺序依次写入，持久化时它们被依次写入 WAL 文件的尾部。\n\n当数据仍然在 MemStore 还未被持久化到 HFile 怎么办呢？WAL 文件会被回放。操作的方法是读取 WAL 文件，排序并添加所有的修改记录到 MemStore，最后 MemStore 会被刷写到 HFile。\n\n![](../img/数据库/HBASE/hbase故障恢复2.png)\n\n（点评：故障恢复是 HBase 可靠性保障的一个重要特性。WAL 在这里扮演了关键角色，在分割 WAL 时，数据会根据 region 分配到对应的新的 region server 上，然后 region server 负责回放这一部分数据到 MemStore 中。）\n\n# Apache HBase 架构的优缺点\n\n## 优点\n\n强一致性：\n\n- 当 write 返回时，所有的 reader 都会读到同样的值。\n  \n自动扩展性\n\n- 数据变大时 region 会分裂。\n- 使用 HDFS 存储备份数据。\n\n内置恢复功能\n\n- 使用 Write Ahead Log （类似于文件系统中的日志）\n\n与 Hadoop 结合：\n\n- 使用 MapReduce 处理 HBase 数据会非常直观。\n\n## 缺点\nApache HBase 也有问题\n\n业务持续可靠性：\n\n- WAL 回放很慢。\n- 故障恢复很慢。\n- Major Compaction 时候 I/O 会飙升。\n# 参考文章\n- https://segmentfault.com/a/1190000019959411"
  },
  {
    "path": "数据库/MongoDB.md",
    "content": "\n* [MongoDB](#mongodb)\n* [特点](#特点)\n* [关键组件](#关键组件)\n    * [_id](#_id)\n    * [集合](#集合)\n    * [游标](#游标)\n    * [数据库](#数据库)\n    * [文档](#文档)\n    * [字段](#字段)\n* [单机mongo架构](#单机mongo架构)\n* [集群模式1-MongoDB 复制（副本集）Replica set(主从关系)](#集群模式1-mongodb-复制副本集replica-set主从关系)\n    * [什么是](#什么是)\n    * [复制结构图](#复制结构图)\n    * [复制原理](#复制原理)\n* [集群模式2-MongoDB 分片](#集群模式2-mongodb-分片)\n    * [什么是](#什么是-1)\n    * [分片集群结构](#分片集群结构)\n    * [三个主要组件](#三个主要组件)\n        * [Routers mongos](#routers-mongos)\n        * [Config Server](#config-server)\n        * [Shard](#shard)\n    * [Shard Keys 分片键](#shard-keys-分片键)\n        * [关于collection（类似mysql中的table）分片](#关于collection类似mysql中的table分片)\n        * [关于collection的切分规则](#关于collection的切分规则)\n            * [按范围（range） 切分chunk](#按范围range-切分chunk)\n            * [按hash 切分chunk](#按hash-切分chunk)\n    * [Chunks 块](#chunks-块)\n        * [块大小](#块大小)\n        * [块拆分](#块拆分)\n        * [块迁移](#块迁移)\n        * [块平衡](#块平衡)\n        * [不可分割/巨型块](#不可分割巨型块)\n* [WiredTiger存储引擎](#wiredtiger存储引擎)\n    * [文档级别的并发 Document Level Concurrency](#文档级别的并发-document-level-concurrency)\n    * [快照与检查点 Snapshots and Checkpoints](#快照与检查点-snapshots-and-checkpoints)\n    * [日志 Journal](#日志-journal)\n    * [压缩 Compression](#压缩-compression)\n    * [内存使用](#内存使用)\n* [参考文档](#参考文档)\n\n# MongoDB\nMongoDB 是由 C++语言编写的，是一个基于分布式文件存储的开源数据库系统。 再高负载的情况下，添加更多的节点，可以保证服务器性能。 MongoDB 旨在给 WEB 应用提供可扩展的高性能数据存储解决方案。MongoDB 将数据存储为一个文档，数据结构由键值(key=>value)对组成。 MongoDB 文档类似于 JSON 对象。字段值可以包含其他文档，数组及文档数组。\n# 特点\n- 面向文档的数据库\n- 非关系型数据库\n# 关键组件\n## _id\n- 文档的主键，如果没有指定则会自动创建\n\n- ObjectId(_id)\n  ![](../img/数据库/MongoDB/_id.png)\n  - 时间+机器ID+进程ID+计数器\n  - 最重要的是开头的四个字节的时间信息，为Unix时间戳。后面三个字节是机器ID,两个字节的进程ID，三个字节的计数器。计数器会自动增长，可以保证同一进程、同一时刻内不会重复。\n## 集合\n- collection\n- 等同于表\n## 游标\n指向查询结果集的指针，可以遍历游标检索结果\n## 数据库\n## 文档\n集合中的记录称为文档\n## 字段\nkv键值对\n\n# 单机mongo架构\n\n- client 客户端发请求\n- mongodb query language 查询解析层\n- mongodb data model 抽象存储层\n- storage engine 存储引擎层（类似MySQL，储存引擎是插拔式的，可选多种）\n  - wired tiger 目前主流默认\n  - MMAPV1\n  - In Memory\n- 基于journaling log做宕机恢复（类比mysql的redo log）：写数据之前顺序追加先写磁盘log，然后到内存，然后将定期将内存写入到磁盘中去，如果在内存写入到磁盘过程中挂掉了，会通过journaling log将数据恢复。由此可见不是准实时写入，但是可以达到高吞吐。\n\n# 集群模式1-MongoDB 复制（副本集）Replica set(主从关系)\n## 什么是\n- MongoDB复制是将数据同步在多个服务器的过程。\n- 复制提供了数据的冗余备份，并在多个服务器上存储数据副本，提高了数据的可用性， 并可以保证数据的安全性。\n- 复制还允许您从硬件故障和服务中断中恢复数据。\n## 复制结构图\n![](../img/数据库/MongoDB/复制结构图.png)\n## 复制原理\n- mongodb的复制至少需要两个节点。其中一个是主节点，负责处理客户端请求，其余的都是从节点，负责复制主节点上的数据。\n- mongodb各个节点常见的搭配方式为：一主一从、一主多从。\n- 主节点记录在其上的所有操作oplog，从节点定期轮询主节点获取这些操作，然后对自己的数据副本执行这些操作，从而保证从节点的数据与主节点一致。\n- 主宕机后，replica set中会重新选举。客户端就会写到新的primary上。\n- 如果RS中有一半机器上宕机的，将无法写入，只能读。\n\n# 集群模式2-MongoDB 分片\t\n## 什么是\n- 在Mongodb里面存在另一种集群，就是分片技术,可以满足MongoDB数据量大量增长的需求。\n- 当MongoDB存储海量的数据时，一台机器可能不足以存储数据，也可能不足以提供可接受的读写吞吐量。这时，我们就可以通过在多台机器上分割数据，使得数据库系统能存储和处理更多的数据。\n## 分片集群结构\n![](../img/数据库/MongoDB/分片集群结构.png)\n## 三个主要组件\n### Routers mongos\n客户端请求会通过mongos router（也可以是多个router，可以理解为网关代理），通过路由层可以把数据路由到具体的shard上，在这个过程中会存储许多的元信息 meta，简单理解元信息就是索引，存储的是哪个key存在了哪个shard上。同时元信息服务器【config servers】本身也是个replica set，本身也是主从复制的，提供高可用。\n### Config Server\n- mongod实例，存储了整个 ClusterMetadata，其中包括 chunk信息\n- 配置服务器上存储了分片集群的元数据\n- 配置服务器上存储集群元数据在config数据库中，mongo实例缓存这些数据并通过它们选择到各个shard的访问路径\n\n### Shard\n用于存储实际的数据块，实际生产环境中一个shard server角色可由几台机器组个一个replica set承担，防止主机单点故障\n\n## Shard Keys 分片键\nMongoDB 使用分片键在分片之间分发集合的文档。分片键由文档中的一个或多个字段组成。\n\n###  关于collection（类似mysql中的table）分片\n当查询某个collection数据的时候，router（mongos）会路由到具体的shard（Replication set）中，根据shard规则可能数据都在一个shard中，也可能存在多个。\n\ncollection会自动分层多个chunk，如下图collection1的白色的框框，每个chunk会被自动负载均衡到不同的shard（Replication set），即实际保证的是chunk在不同shard的高可用（根据设置的副本的数量），另外类似于redis的tag方法，mongodb支持zones方法\n\n![](../img/数据库/MongoDB/collection分片.png)\n\n### 关于collection的切分规则\n#### 按范围（range） 切分chunk\n类比mysql的按照id分，比如前1w个id放入a1,2w内的放在a2..\n\n![](../img/数据库/MongoDB/范围分片chunk.png)\n\n问题：字段如果是时间等类似相近的分类字段，会存在写入热点问题，会存在chunk集中存在某个shard上。\n\n注意：key应该为建立索引Single Field，联合索引Compound Index也可以。chunk默认为64MB，超过64MB的被分割。\n\n所有的chunk收尾相连就可以构成整个collection表\n#### 按hash 切分chunk\n![](../img/数据库/MongoDB/hash分片chunk.png)\n\n问题：无法规避hash冲突问题，即无法彻底规避热点问题\n\n注意：key必须为hash索引（key不允许设置唯一索引属性，也达不到唯一Unique Indexes），这样才能得到int整型去模预先配置的chunk（数据量大于默认chunk大小）或者聚合（优化）的时候影响写入性能。\n\n## Chunks 块\nMongoDB 将分片数据分成块。每个块都有一个基于shard key的包含的下限和排他的上限范围 。\n### 块大小\nMongoDB 中的默认块大小为 64 兆字节。您可以 增加或减少块大小。\n- 小块以更频繁的迁移为代价导致更均匀的数据分布。这会使查询路由 ( mongos) 层代价更多\n- 大块导致更少的迁移。从网络角度和查询路由层的内部开销来看，这都更有效。但是，这些效率是以数据潜在的不均匀分布为代价的。\n\n对于许多部署，以牺牲稍微不那么均匀分布的数据集为代价来避免频繁和潜在的虚假迁移是有意义的\n\n### 块拆分\n拆分是一个防止块变得太大的过程。当块增长超过指定的块大小时，或者如果块中的文档数量超过每个块要迁移的最大文档数，MongoDB 会根据块表示的分片键值拆分块。一个块可以在必要时分成多个块。插入和更新可能会触发拆分。拆分是一种有效的元数据更改。要创建拆分，MongoDB的确实不迁移任何数据或影响的碎片。\n\n![](../img/数据库/MongoDB/块拆分.png)\n\n拆分可能会导致分片中集合的块分布不均匀。在这种情况下，平衡器会在分片之间重新分配块\n\n### 块迁移\nMongoDB 迁移分片集群中的块，以在分片之间均匀分布分片集合的块。迁移可能是：\n- 手动的。仅在有限的情况下使用手动迁移，例如在批量插入期间分发数据。有关更多详细信息，请参阅手动迁移块。\n- 自动的。当分片集合的块在分片之间分布不均匀时，平衡器进程会自动迁移块。有关更多详细信息，请参阅迁移阈值。\n\n### 块平衡\n该平衡器是管理数据块迁移的后台进程。如果最大和最小分片之间的块数差异超过 迁移阈值，则平衡器开始跨集群迁移块以确保数据的均匀分布。\n\n![](../img/数据库/MongoDB/块平衡.png)\n\n### 不可分割/巨型块\n在某些情况下，块可以增长到超过指定的块大小，但不能进行拆分。最常见的情况是当一个块表示单个分片键值时。由于块无法拆分，它会继续增长超过块大小，成为巨型块。随着这些巨型块的不断增长，它们可能会成为性能瓶颈，尤其是在分片键值出现频率很高的情况下\n\n# WiredTiger存储引擎\nMongoDB默认的储存引擎可适用于大多数场景\n## 文档级别的并发 Document Level Concurrency\nWiredTiger使用文档级并发控制进行写操作。因此，多个客户端可以并发同时修改集合的不同文档。\n\n对于大多数读写操作，WiredTiger使用乐观并发控制模式。WiredTiger仅在全局、数据库和集合级别使用意向锁。当存储引擎检测到两个操作之间存在冲突时，将引发写冲突，从而导致MongoDB自动重试该操作。\n\n一些全局操作（通常是涉及多个数据库的短暂操作）仍然需要全局“实例范围级别的”锁。其他一些操作（例如删除集合）仍然需要独占数据库锁。\n## 快照与检查点 Snapshots and Checkpoints\nWiredTiger使用MultiVersion并发控制（MVCC）方式。在操作开始时，WiredTiger为操作提供数据的时间点快照。快照提供了内存数据的一致视图。\n\n写入磁盘时，WiredTiger将所有数据文件中的快照中的所有数据以一致的方式写入磁盘。现在持久的数据充当数据文件中的检查点。该检查点可确保数据文件直到最后一个检查点（包括最后一个检查点）都保持一致；即检查点可以充当恢复点。\n\n从3.6版本开始，MongoDB配置WiredTiger以60秒的间隔创建检查点（即将快照数据写入磁盘）。在早期版本中，MongoDB将检查点设置为在WiredTiger中以60秒的间隔或在写入2GB日志数据时对用户数据进行检查，以先到者为准。\n\n在写入新检查点期间，先前的检查点仍然有效。这样，即使MongoDB在写入新检查点时终止或遇到错误，重启后，MongoDB仍可从上一个有效检查点恢复。\n\n当WiredTiger的元数据表被原子更新以引用新的检查点时，新的检查点将变为可访问且永久的。一旦可以访问新的检查点，WiredTiger就会从旧的检查点释放页面。\n\n使用WiredTiger，即使没有日志，MongoDB也可以从最后一个检查点恢复；但是，要恢复上一个检查点之后所做的更改，请运行日志功能。\n\n- 从MongoDB 4.0开始，您不能指定–nojournal选项或storage.journal.enabled：使用WiredTiger存储引擎的副本集成员为false。\n## 日志 Journal\n\nWiredTiger将预写日志（即日志）与检查点结合使用以确保数据持久性。\n\nWiredTiger日志保留检查点之间的所有数据修改。如果MongoDB在检查点之间退出，它将使用日志重播自上一个检查点以来修改的所有数据。有关MongoDB将日志数据写入磁盘的频率的信息，具体请参阅日志处理。\n\nWiredTiger日志使用快速压缩库进行压缩。要指定其他压缩算法或不进行压缩，请使用storage.wiredTiger.engineConfig.journalCompressor设置参数。有关更改日志压缩器的详细信息，请参阅“更改WiredTiger日志压缩器”文档。\n\n如果日志记录小于或等于128字节（WiredTiger的最小日志记录大小），则WiredTiger不会压缩该记录。\n\n您可以通过将storage.journal.enabled设置为false来禁用独立实例的日志记录，这可以减少维护日志记录的开销。对于独立实例，不使用日志意味着MongoDB意外退出时，您将丢失最后一个检查点之前的所有数据修改信息。\n\n## 压缩 Compression\n\n使用WiredTiger，MongoDB支持对所有集合和索引进行压缩。压缩可最大程度地减少存储空间的使用量，但会增加CPU的开销。\n\n默认情况下，WiredTiger对所有集合使用块压缩和snappy压缩库，对所有索引使用前缀压缩。\n\n对于集合，还提供以下块压缩库：\n- zlib\n- zstd（从MongoDB 4.2开始支持）\n\n要指定替代压缩算法或不压缩，请使用storage.wiredTiger.collectionConfig.blockCompressor参数设置。\n\n对于索引，要禁用前缀压缩，请使用storage.wiredTiger.indexConfig.prefixCompression设置。\n\n压缩设置还可以在集合和索引创建期间基于每个集合和每个索引进行配置。请参见指定存储引擎选项和db.collection.createIndex（）storageEngine选项。\n\n对于大多数压缩工作负载，默认压缩设置可以平衡存储效率和处理要求。\n\n默认情况下，WiredTiger日志也被压缩。有关日志压缩的信息，请参阅日志。\n## 内存使用\n通过WiredTiger，MongoDB可以利用WiredTiger内部缓存和文件系统缓存。\n\n从MongoDB 3.4开始，默认的WiredTiger内部缓存大小是以下两者中的较大者：\n- 50％（RAM-1 GB）或256 MB。\n- 例如，在总共有4GB RAM的系统上，WiredTiger缓存将使用1.5GB RAM（0.5 *（4 GB-1 GB）= 1.5 GB）。相反，总内存为1.25 GB的系统将为WiredTiger缓存分配256 MB，因为这是总RAM的一半以上减去一GB（0.5 *（1.25 GB-1 GB）= 128 MB <256 MB） 。\n\n通过文件系统缓存，MongoDB 自动使用 WiredTiger 缓存或其他进程未使用的所有空闲内存。\n# 参考文档\n- https://juejin.cn/post/6844904186300071943\n- https://segmentfault.com/a/1190000022271347\n- https://docs.mongodb.com/manual/core/wiredtiger/"
  },
  {
    "path": "数据库/MySQL.md",
    "content": "# MySQL\n## 架构\n\n<img src=\"../img/数据库/MySQL/MySQL架构.png\" width=\"50%\" />\n\n### 客户端\n各种语言都提供了连接mysql数据库的方法，比如jdbc、php、go等，可根据选择 的后端开发语言选择相应的方法或框架连接mysql\n### server层\n包括连接器、查询缓存、分析器、优化器、执行器等，涵盖mysql的大多数核心服务功能，以及所有的内置函数（例如日期、世家、数 学和加密函数等），所有跨存储引擎的功能都在这一层实现，比如存储过程、触发器、视图等\n- `连接器` 连接器负责来自客户端的连接、获取用户权限、维持和管理连接\n- `查询缓存`\n  - mysql拿到一个查询请求后，会先到查询缓存查看之前是否执行过这条语句\n  - MySQL 8.0 版本直接将查询缓存的整块功能删掉了\n- `分析器`\n  - 词法分析（识别关键字，操作，表名，列名）\n  - 语法分析 (判断是否符合语法）\n- `优化器` 优化器是在表里面有多个索引的时候，决定使用哪个索引；或者在一个语句有多表关联（join）的时候，决定各个表的连接顺序。优化器阶段完成后，这个语句的执行方案就确定下来了，然后进入执行器阶段\n- `执行器`\n  - 首先，肯定是要判断权限，就是有没有权限执行这条SQL。工作中可能会对某些客户端进行权限控制\n  - 如果有权限，就打开表继续执行。打开表的时候，执行器就会根据表的引擎定义，去使用这个引擎提供的接口\n\n最终对结果集进行过滤、排序以及键值对的比较等\n- cpu密集型\n### 储存引擎\n负责数据的存储和提取，是真正与底层物理文件打交道的组件。 数据本质是存储在磁盘上的，通过特定的存储引擎对数据进行有组织的存放并根据业务需要对数据进行提取。存储引擎的架构模式是插件式的，支持Innodb，MyIASM、Memory等多个存储引擎。现在最常用的存储引擎是Innodb，它从mysql5.5.5版本开始成为了默认存储引擎\n- io密集型\n### 物理文件层\n存储数据库真正的表数据、日志等。物理文件包括：redolog、undolog、binlog、errorlog、querylog、slowlog、data、index等\n\n\n## 储存引擎\nMySQL 5.7 支持的存储引擎有 InnoDB、MyISAM、Memory、Merge、Archive、CSV、BLACKHOLE 等。可以使用SHOW ENGINES;语句查看系统所支持的引擎类型\n\n<img src=\"../img/数据库/MySQL/MySQL储存引擎比较.png\" width=\"50%\" />\n\n### InnoDB\n特点\n- 灾难恢复性好\n- 支持事务\n- 使用行级锁\n- 支持外键关联\n- 支持热备份\n### MyISAM\n- 不支持事务使用表级锁\n- 并发性差主机宕机后，MyISAM表易损坏\n- 灾难恢复性不佳可以配合锁，实现操作系统下的复制备份、迁移\n- 只缓存索引，数据的缓存是利用操作系统缓冲区来实现的。可能引发过多的系统调用且效率不佳\n- 数据紧凑存储，因此可获得更小的索引和更快的全表扫描性能\n### MEMORY\n- 使用内存\n- 重启后数据会丢失\n\n### ARCHIVE\n该存储引擎非常适合存储大量独立的、作为历史记录的数据。区别于InnoDB和MyISAM这两种引擎，ARCHIVE提供了压缩功能，拥有高效的插入速度，但是这种引擎不支持索引，所以查询性能较差一些\n\n### InnoDB\n#### 架构图\n\n<img src=\"../img/数据库/MySQL/innodb架构图.png\" width=\"50%\" />\n\n#### 架构划分\n##### 内存结构\n###### 缓冲池 (Buffer Pool) \nbuffer pool是主存中的一个区域，InnoDB在访问表和索引数据时在这里进行缓存。buffer pool允许直接从内存访问常用数据，从而提高处理速度。在专用服务器上，多达80%的物理内存通常分配给缓冲池\n- `预读` 磁盘读写，并不是按需读取，而是按页读取，一次至少读一页数据（一般是16K），如果未来要读取的数据就在页中，就能够省去后续的磁盘IO，提高效率\n- `LRU` 管理这些缓冲页\n  - `传统的LRU` 把入缓冲池的页放到LRU的头部，作为最近访问的元素，从而最晚被淘汰\n    1. 页已经在缓冲池里，那就只做“移至”LRU头部的动作，而没有页被淘汰；\n    2. 页不在缓冲池里，除了做“放入”LRU头部的动作，还要做“淘汰”LRU尾部页的动作\n  - `MySQL LRU`\n    - 问题：`预读失效` 由于预读(Read-Ahead)，提前把页放入了缓冲池，但最终MySQL并没有从页中读取数据，称为预读失效\n      - 如何对预读失效进行优化？\n        - 思路\n          1. 让预读失败的页，停留在缓冲池LRU里的时间尽可能短；\n          2. 让真正被读取的页，才挪到缓冲池LRU的头部；\n        - 方法\n          - 将LRU分为两个部分\n          <img src=\"../img/数据库/MySQL/缓冲池lru.png\" width=\"60%\" />\n          \n            - 新生代(new sublist)\n            - 老生代(old sublist)\n          - 新老生代首尾相连，即：新生代的尾(tail)连接着老生代的头(head)\n          - 新页（例如被预读的页）加入缓冲池时，只加入到老生代头部\n            - 如果数据真正被读取（预读成功），才会加入到新生代的头部\n            - 如果数据没有被读取，则会比新生代里的“热数据页”更早被淘汰出缓冲池\n    - 问题：`缓冲池污染` 当某一个SQL语句，要批量扫描大量数据时，可能导致把缓冲池的所有页都替换出去，导致大量热数据被换出，MySQL性能急剧下降，这种情况叫缓冲池污染\n      - 例如，有一个数据量较大的用户表，当执行： `select * from user where name like \"%shenjian%\";` 虽然结果集可能只有少量数据，但这类like不能命中索引，必须全表扫描，就需要访问大量的页：\n        1. 把页加到缓冲池（插入老生代头部）；\n        2. 从页里读出相关的row（插入新生代头部）；\n        3. row里的name字段和字符串shenjian进行比较，如果符合条件，加入到结果集中；\n        4. …直到扫描完所有页中的所有row…\n        - 如此一来，所有的数据页都会被加载到新生代的头部，但只会访问一次，真正的热数据被大量换出。\n      - 老生代停留时间窗口\n        1. 假设T=老生代停留时间窗口；\n        2. 插入老生代头部的页，即使立刻被访问，并不会立刻放入新生代头部；\n        3. 只有满足“被访问”并且“在老生代停留时间”大于T，才会被放入新生代头部\n        - 举例\n        - <img src=\"../img/数据库/MySQL/缓冲池污染老生代停留时间窗口.png\" width=\"60%\" />\n\n参数\n- `innodb_buffer_pool_size` 配置缓冲池的大小，在内存允许的情况下，DBA往往会建议调大这个参数，越多数据和索引放到内存里，数据库的性能会越好\n- `innodb_old_blocks_pct` 老生代占整个LRU链长度的比例，默认是37，即整个LRU中新生代与老生代长度比例是63:37。\n- `innodb_old_blocks_time` 老生代停留时间窗口，单位是毫秒，默认是1000，即同时满足“被访问”与“在老生代停留时间超过1秒”两个条件，才会被插入到新生代头部\n\n总结\n1. 缓冲池(buffer pool)是一种常见的降低磁盘访问的机制；\n2. 缓冲池通常以页(page)为单位缓存数据；\n3. 缓冲池的常见管理算法是LRU，memcache，OS，InnoDB都使用了这种算法；\n4. InnoDB对普通LRU进行了优化：\n   1. 将缓冲池分为老生代和新生代，入缓冲池的页，优先进入老生代，页被访问，才进入新生代，以解决预读失效的问题\n   2. 页被访问，且在老生代停留时间超过配置阈值的，才进入新生代，以解决批量数据访问，大量热数据淘汰的问题\n###### 写缓冲 (Change Buffer)\n目的是提升 InnoDB 性能，加速写请求，避免每次写入都进行磁盘 IO。 在MySQL5.5之前，叫插入缓冲(insert buffer)，只针对insert做了优化；现在对delete和update也有效，叫做写缓冲(change buffer),它是一种应用在非唯一普通索引页(non-unique secondary index page)不在缓冲池中，对页进行了写操作，并不会立刻将磁盘页加载到缓冲池，而仅仅记录缓冲变更(buffer changes)，等未来数据被读取时，再将数据合并(merge)恢复到缓冲池中的技术。写缓冲的目的是降低写操作的磁盘IO，提升数据库性能\n\n情况一\n\n\n<img src=\"../img/数据库/MySQL/写缓冲1.png\" width=\"60%\" />\n\n情况二\n\n<img src=\"../img/数据库/MySQL/写缓冲2.png\" width=\"60%\" />\n\n加入写缓冲优化\n\n<img src=\"../img/数据库/MySQL/写缓冲3.png\" width=\"60%\" />\n\n此后的读取情况\n\n<img src=\"../img/数据库/MySQL/写缓冲4.png\" width=\"60%\" />\n\n为什么写缓冲优化，仅适用于非唯一普通索引页呢？\n- 如果索引设置了唯一(unique)属性，在进行修改操作时，InnoDB必须进行唯一性检查。也就是说，索引页即使不在缓冲池，磁盘上的页读取无法避免(否则怎么校验是否唯一？)，此时就应该直接把相应的页放入缓冲池再进行修改\n- 除了数据页被访问，还有哪些场景会触发刷写缓冲中的数据呢？\n  1. 有一个后台线程，会认为数据库空闲时；\n  2. 数据库缓冲池不够用时；\n  3. 数据库正常关闭时；\n  4. redo log写满时\n\n什么业务场景，适合开启InnoDB的写缓冲机制？\n- 什么时候不适合\n  1. 数据库都是唯一索引；\n  2. 或者，写入一个数据后，会立刻读取它；\n- 适合\n  1. 数据库大部分是非唯一索引；\n  2. 业务是写多读少，或者不是写后立刻读取；\n###### 自适应哈希索引 (Adaptive Hash Index)\nInnoDB的哈希索引\n1. InnoDB用户无法手动创建哈希索引，这一层上说，InnoDB确实不支持哈希索引；\n2. InnoDB会自调优(self-tuning)，如果判定建立自适应哈希索引(Adaptive Hash Index, AHI)，能够提升查询效率，InnoDB自己会建立相关哈希索引，这一层上说，InnoDB又是支持哈希索引的；\n- 原理\n<img src=\"../img/数据库/MySQL/自适应hash函数.png\" width=\"60%\" />\n\t\t\t\t\t\n  - 为啥叫“自适应(adaptive)”哈希索引？\n  - 系统自己判断“应该可以加速查询”而建立的，不需要用户手动建立，故称“自适应”。\n  - 系统会不会判断失误，是不是一定能加速？\n  - 不是一定能加速，有时候会误判。\n###### 日志缓冲 (Log Buffer) \nredo log，binlog都先写入到日志缓冲区，然后再追加写入到磁盘日志文件中，主要的目的是把磁盘随机IO写优化成磁盘批量写和顺序写（磁盘顺序写的性能要比随机写高非常多）\n- 事务提交时，将redo log写入Log Buffer，就会认为事务提交成功\n- 如果写入Log Buffer的数据，write入OS cache之前，数据库崩溃，就会出现数据丢失\n- 如果写入OS cache的数据，fsync入磁盘之前，操作系统奔溃，也可能出现数据丢失\n  - `策略一`：最佳性能(`innodb_flush_log_at_trx_commit=0`) 每隔一秒，才将Log Buffer中的数据批量write入OS cache，同时MySQL主动fsync。 这种策略，如果数据库奔溃，有一秒的数据丢失。\n  - `策略二`：强一致(`innodb_flush_log_at_trx_commit=1`) 每次事务提交，都将Log Buffer中的数据write入OS cache，同时MySQL主动fsync。 这种策略，是InnoDB的默认配置，为的是保证事务ACID特性。\n  - `策略三`：折衷(`innodb_flush_log_at_trx_commit=2`) 每次事务提交，都将Log Buffer中的数据write入OS cache； 每隔一秒，MySQL主动将OS cache中的数据批量fsync。这种策略，如果操作系统奔溃，最多有一秒的数据丢失。\n##### 磁盘结构\n//TODO\n#### 特点总结\n- 支持事务处理，\n- 支持外键，\n- 支持崩溃修复能力和并发控制。如果需要对事务的完整性要求比较高（比如银行），要求实现并发控制（比如售票），那选择InnoDB有很大的优势。如果需要频繁的更新、删除操作的数据库，也可以选择InnoDB，因为支持事务的提交（commit）和回滚（rollback）\n### MyISAM\n- 不支持事务\n- 不支持行级锁\n- MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多，而且恢复的速度也更慢\n- MyISAM 支持压缩表和空间数据索引\n- 插入数据快，空间和内存使用比较低。如果表主要是用于插入新记录和读出记录，那么选择MyISAM能实现处理高效率。如果应用的完整性、并发性要求比 较低，也可以使用\n\n## MyISAM和InnoDB的区别总结\n\n### 存储结构\n**MyISAM**：每个MyISAM在磁盘上存储成三个文件。分别为：表定义文件、数据文件、索引文件。第一个文件的名字以表的名字开始，扩展名指出文件类型。.frm文件存储表定义。数据文件的扩展名为.MYD (MYData)。索引文件的扩展名是.MYI (MYIndex)。\n\n**InnoDB**：所有的表都保存在同一个数据文件中（也可能是多个文件，或者是独立的表空间文件），InnoDB表的大小只受限于操作系统文件的大小，一般为2GB。\n\n### 存储空间\n**MyISAM**： MyISAM支持三种不同的存储格式：静态表(默认，但是注意数据末尾不能有空格，会被去掉)、动态表、压缩表。当表在创建之后并导入数据之后，不会再进行修改操作，可以使用压缩表，极大的减少磁盘的空间占用。\n\n**InnoDB**： 需要更多的内存和存储，它会在主内存中建立其专用的缓冲池用于高速缓冲数据和索引。\n\n### 可移植性、备份及恢复\n**MyISAM**：数据是以文件的形式存储，所以在跨平台的数据转移中会很方便。在备份和恢复时可单独针对某个表进行操作。\n\n**InnoDB**：免费的方案可以是拷贝数据文件、备份 binlog，或者用 mysqldump，在数据量达到几十G的时候就相对痛苦了。\n\n### 事务支持\n**MyISAM**：强调的是性能，每次查询具有原子性,其执行数度比InnoDB类型更快，但是不提供事务支持。\n\n**InnoDB**：提供事务支持事务，外部键等高级数据库功能。 具有事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant))型表。\n\n### AUTO_INCREMENT \n**MyISAM**：可以和其他字段一起建立联合索引。引擎的自动增长列必须是索引，如果是组合索引，自动增长可以不是第一列，他可以根据前面几列进行排序后递增。\n\n**InnoDB**：InnoDB中必须包含只有该字段的索引。引擎的自动增长列必须是索引，如果是组合索引也必须是组合索引的第一列。\n\n### 表锁差异\n**MyISAM**： 只支持表级锁，用户在操作myisam表时，select，update，delete，insert语句都会给表自动加锁，如果加锁以后的表满足insert并发的情况下，可以在表的尾部插入新的数据。\n\n**InnoDB**： 支持事务和行级锁，是innodb的最大特色。行锁大幅度提高了多用户并发操作的新能。但是InnoDB的行锁，只是在WHERE的主键是有效的，非主键的WHERE都会锁全表的。\n\n### 全文索引MySql全文索引\n**MyISAM**：支持 FULLTEXT类型的全文索引\n\n**InnoDB**：不支持FULLTEXT类型的全文索引，但是innodb可以使用sphinx插件支持全文索引，并且效果更好。\n\n### 表主键\n**MyISAM**：允许没有任何索引和主键的表存在，索引都是保存行的地址。\n\n**InnoDB**：如果没有设定主键或者非空唯一索引，就会自动生成一个6字节的主键(用户不可见)，数据是主索引的一部分，附加索引保存的是主索引的值。\n\n### 表的具体行数\n**MyISAM**： 保存有表的总行数，如果select count() from table;会直接取出出该值。\n\n**InnoDB**： 没有保存表的总行数，如果使用select count(*) from table；就会遍历整个表，消耗相当大，但是在加了wehre条件后，myisam和innodb处理的方式都一样。\n\n### CRUD操作\n**MyISAM**：如果执行大量的SELECT，MyISAM是更好的选择。\n\n**InnoDB**：如果你的数据执行大量的INSERT或UPDATE，出于性能方面的考虑，应该使用InnoDB表。\n### 外键\n**MyISAM**：不支持\n\n**InnoDB**：支持\n\n## 索引\n- 按数据结构分类可分为：B+tree索引、Hash索引、Full-text索引。\n- 按物理存储分类可分为：聚簇索引、二级索引（辅助索引）。\n- 按字段特性分类可分为：主键索引、普通索引、前缀索引。\n- 按字段个数分类可分为：单列索引、联合索引（复合索引、组合索引）。\n### B树\n#### b+ tree\n- B+tree 非叶子节点只存储键值信息\n\n<img src=\"../img/数据库/MySQL/b加树.png\" width=\"60%\" />\n\n- 叶子结点单链表有序\n- 利用磁盘预读特性\n  - 为了减少磁盘 I/O 操作，磁盘往往不是严格按需读取，而是每次都会预读。预读过程中，磁盘进行顺序读取，顺序读 取不需要进行磁盘寻道，并且只需要很短的磁盘旋转时间，速度会非常快。 操作系统一般将内存和磁盘分割成固定大小的块，每一块称为一页，内存与磁盘以页为单位交换数据。数据库系统将 索引的一个节点的大小设置为页的大小，使得一次 I/O 就能完全载入一个节点。并且可以利用预读特性，相邻的节点 也能够被预先载入。\n#### 为什么数据库索引用B+树，而不用list、map、二叉树或红黑树\n- 为什么不用b树\n  - b+树只有叶子节点存放数据，而b树每个索引节点都会存放数据，查询范围数据时io次数会大大增加\n  - b+树叶子结点也是链表，在范围查找时也是非常高效\n- 为什么不使用hash\n  - hash只能值匹配不能实现范围查询\n  - 不能实现排序\n  - 不能联合索引\n  - 数据量大，hash冲突大\n- 为什么不是AVL\n  - b+树的高度远低于AVL，查找效率更高\n- 为什么不是红黑树\n  - 树高度\n  - 红黑树的平衡\n#### MySQL B+树一般几层，怎么算的\n非叶子节点就是一个16k大小的page，所以对于一棵树能存多少数据，主要就看非叶节点能存下多少个`主键ID+指针`了\n\n下面以一个高度=2，且主键ID是一个bigint(8字节)来分析可以存下多少数据(这个假设是有意义的，在绝大部分主键id都是一个自增的bigint)。\n\nInnodb中一个指针是6字节长度。所以`主键ID+指针`总共就占14字节。所以一个16K大小的节点可以存下的`主键ID+指针`个数=16K/14=16384/14=1170，也就是说一个高度=2的B+树可以放下1170个叶子节点，即1170个用于存放行数据的page，即可以存放的行数据的大小=1170 * 16K=18720K=1.8M，准确的说这是树上的，还有很多不在树上的，所以实际能放下的数据不止1.8M。\n\n如果下面我们分析高度=3，即有两层非叶子节点的B+树，能存放多少数据。根节点的16k的page可以存放16k/14=1170个`主键ID+指针`，即第二层就可以有1170个page。所以总共树上可以放的叶子节点的个数=1170 * 1170=1368900，所以能放下的数据=1368900 * 16K=21902400K=21G。同理，因为不是所有的行数据都在树上，所以高度=3的B+树不止放下21G的数据的。\n\n所以，在实际中，绝大部分的表的索引树的高度都不会超过4。\n#### b tree\n所有节点都会储存信息\n<img src=\"../img/数据库/MySQL/b树.png\" width=\"60%\" />\n\n#### 联合索引的树存储结构\n\n<img src=\"../img/数据库/MySQL/联合索引结构.png\" width=\"60%\" />\n\n联合索引的所有索引列都出现在索引树上，并依次比较三列的大小。\n\n### 哈希索引\n哈希索引能以 O(1) 时间进行查找，但是失去了有序性\n- 无法用于排序与分组\n- 只支持精确查找，无法用于部分查找和范围查找\n### 全文索引\nMyISAM 存储引擎支持全文索引，用于查找文本中的关键词，而不是直接比较是否相等\n### 空间数据索引\n- MyISAM 存储引擎支持空间数据索引（R-Tree），可以用于地理数据存储。空间数据索引会从所有维度来索引数据，\n- 可以有效地使用任意维度来进行组合查询\n- 必须使用 GIS 相关的函数来维护数据。\n### MySQL 索引失效\n- `违反最左匹配原则` 最左匹配原则：最左优先，以最左边的为起点任何连续的索引都能匹配上，如不连续，则匹配不上。\n- `多条件联合查询时最好建联合索引`\n- 遇到`范围`查询（>、<、between、like）就会停止匹配。\n  - 使用范围后的索引会失效，例如 `select * from table where a > 1 and b= 9` a索引生效，b索引不生效\n- 使用`不等于`（!= 、<>）\n  - 这种要看MySQL是怎么优化的，因为`=`是走索引的比如有1%的数据，而`!= `相当于查询另外99%的数据，相当于全表扫描了所以选择不走索引\n- 如`计算、函数、（手动或自动）类型转换`等操作，会导致索引失效而进行全表扫描。\n- in 值较多时会失效\n  - 推测是查找的效率 < 全表扫描\n- like以通配符开头（'%abc'）\n- 索引列类型不一致\n\n### like索引失效原理\n```sql\nwhere name like \"a%\"\nwhere name like \"%a%\"\nwhere name like \"%a\"\n```\n我们先来了解一下%的用途\n\n- %放在右边，代表查询以\"a\"开头的数据，如：abc\n- 两个%%，代表查询数据中包含\"a\"的数据，如：cab、cba、abc\n- %放在左边，代表查询以\"a\"为结尾的数据，如cba\n\n为什么%放在右边有时候能用到索引\n- %放右边叫做：前缀\n- %%叫做：中缀\n- %放在左边叫做：后缀\n\n没错，这里依然是最佳左前缀法则这个概念 如果索引是字符串，那么B+树是由字符串组成的。\n\n字符串的排序方式：先按照第一个字母排序，如果第一个字母相同，就按照第二个字母排序。。。以此类推\n\n**一、%号放右边（前缀）**\n\n由于B+树的索引顺序，是按照首字母的大小进行排序，前缀匹配又是匹配首字母。所以可以在B+树上进行有序的查找，查找首字母符合要求的数据。所以有些时候可以用到索引。\n\n**二、%号放左边**\n\n是匹配字符串尾部的数据，我们上面说了排序规则，尾部的字母是没有顺序的，所以不能按照索引顺序查询，就用不到索引。\n\n**三、两个%%号**\n\n这个是查询任意位置的字母满足条件即可，只有首字母是进行索引排序的，其他位置的字母都是相对无序的，所以查找任意位置的字母是用不上索引的。\n### ICP(index condition pushdown)\n索引下推\n\n首先，我们可以通过如下语句开启或关闭Myslq的ICP特性：\n```sql\nSET optimizer_switch = 'index_condition_pushdown=off'; //关闭\nSET optimizer_switch = 'index_condition_pushdown=on';  //开启\n```\n\n#### 怎么理解ICP\nIndex Condition Pushdown (ICP)是MySQL用索引去表里取数据的一种优化。如果禁用ICP，引擎层会穿过索引在基表中寻找数据行，然后返回给MySQL Server层，再去为这些数据行进行WHERE后的条件的过滤。ICP启用，如果部分WHERE条件能使用索引中的字段，MySQL Server 会把这部分下推到引擎层。存储引擎通过使用索引条目，然后推索引条件进行评估，使用这个索引把满足的行从表中读取出。ICP能减少引擎层访问基表的次数和MySQL Server 访问存储引擎的次数。\n\n用两张图解释下：\n\n关闭ICP：\n\n<img src=\"../img/数据库/MySQL/关闭icp.png\" width=\"60%\" />\n\n此时，索引符合之前推文提过的最左前缀原理，当多列索引的某一列是范围查询后，之后的字段便不会走索引。\n\n开启ICP:\n\n<img src=\"../img/数据库/MySQL/开启ICP.png\" width=\"60%\" />\n\n开启ICP后，查询同样符合最左前缀规则，但是当多列索引的某一列是范围查询后，之后的字段还是会被下推到存储引擎（Storage Engine）层进行条件判断，过滤出符合条件的数据后再返回给Server层。而由于在引擎层就能够过滤掉大量的数据，这样无疑能够减少了对base table和mysql server的访问次数。从而提升了性能。\n\n#### 举例\n假设我们有一个表 employees，包含以下字段：\n```sql\nCREATE TABLE employees (\n    id INT PRIMARY KEY,\n    name VARCHAR(100),\n    age INT,\n    department VARCHAR(50),\n    salary DECIMAL(10, 2)\n);\n```\n表上有一个联合索引：\n```sql\nCREATE INDEX idx_age_salary ON employees(age, salary);\n```\n现在我们有以下查询：\n\n```sql\nSELECT * FROM employees WHERE age = 30 AND salary > 50000;\n```\n传统的查询处理方式\n\n- 索引扫描：数据库使用索引 idx_age_salary 找到所有 age = 30 的行。\n- 回表查询：根据找到的行号去表中读取完整的行数据。\n- 过滤：对读取的行数据应用 salary > 50000 的条件，过滤掉不满足条件的行。\n\n在这个过程中，数据库必须读取所有 age = 30 的行，然后再进一步过滤 salary > 50000 的行，这可能会读取很多不必要的行。\n\n索引下推的查询处理方式\n\n- 索引扫描：数据库使用索引 idx_age_salary，但在扫描索引时就可以应用 salary > 50000 的条件。\n- 回表查询：仅对满足 age = 30 且 salary > 50000 的行进行回表查询。\n\n通过在索引扫描阶段就进行一部分过滤，索引下推减少了回表查询的次数，从而提升了查询效率。\n\n\n#### ICP的使用条件\n只能用于二级索引(secondary index)。\n\nexplain显示的执行计划中type值\n\n（join 类型）为range、 ref、 eq_ref或者ref_or_null。\n\n且查询需要访问表的整行数据，即不能直接通过二级索引的元组数据获得查询结果(索引覆盖)。\n\n对于InnnoDB表，ICP仅用于二级索引。（ICP的目的是减少全行读取的次数，从而减少IO操作），对于innodb聚集索引，完整的记录已被读入到innodb缓冲区，在这种情况下，ICP不会减少io。\n\nICP可以用于MyISAM和InnnoDB存储引擎，不支持分区表（5.7将会解决这个问题）\n### 索引优化\n- `独立的列`\n  - 在进行查询时，索引列不能是表达式的一部分，也不能是函数的参数，否则无法使用索引 `SELECT actor_id FROM sakila.actor WHERE actor_id + 1 = 5;`\n- `多列索引`\n  - 在需要使用多个列作为条件进行查询时，使用多列索引比使用多个单列索引性能更好 `SELECT film_id, actor_ id FROM sakila.film_actor WHERE actor_id = 1 AND film_id = 1;`\n- `索引列的顺序`\n  - 让选择性最强的索引列放在前面\n  - 索引的选择性是指：不重复的索引值和记录总数的比值。最大值为 1，此时每个记录都有唯一的索引与其对应。选择 性越高，每个记录的区分度越高，查询效率也越高\n- `前缀索引`\n  - 对于 BLOB、TEXT 和 VARCHAR 类型的列，必须使用前缀索引，只索引开始的部分字符\n- `覆盖索引`\n  - 索引包含所有需要查询的字段的值\n### MyISAM的索引\n- MyISAM的索引与行记录是分开存储的，叫做非聚集索引（UnClustered Index）\n- 主键索引与普通索引是两棵独立的索引B+树，通过索引列查找时，先定位到B+树的叶子节点，再通过指针定位到行记录\n\n<img src=\"../img/数据库/MySQL/myISAM索引.png\" width=\"60%\" />\n\n### InnoDB的索引\nInnoDB的主键索引与行记录是存储在一起的，故叫做聚集索引（Clustered Index）\n- 没有单独区域存储行记录\n- 主键索引的叶子节点，存储主键，与对应行记录（而不是指针）\n- 普通索引的叶子节点，存储主键\n\n<img src=\"../img/数据库/MySQL/innodb的索引.png\" width=\"60%\" />\n\n\n- 所以主键不宜用很长的列\n\n### 主键和唯一索引的区别\n- 主键一定是唯一性索引，唯一性索引并不一定就是主键\n- 一个表中可以有多个唯一性索引，但只能有一个主键\n- 主键列不允许空值，而唯一性索引列允许空值\n  - 注意唯一索引的null值会使唯一索引失效，一般设置为不为空或者空字符串`''`\n\n## 联合索引的底层组织方式\n数据：\n\n<img src=\"../img/数据库/MySQL/联合索引1.png\" width=\"60%\" />\n\n现在建立联合索引(b,c,d)\n\nbcd联合索引在B+树上的结构图：\n\n<img src=\"../img/数据库/MySQL/联合索引2.png\" width=\"60%\" />\n\n\n### 联合索引具体查找步骤\n当我们的SQL语言可以应用到索引的时候，比如 select * from T1 where b = 12 and c = 14 and d = 3 ；也就是T1表中a列为4的这条记录。\n\n查找步骤具体如下：\n\n1. 存储引擎首先从根节点（一般常驻内存）开始查找，第一个索引的第一个索引列为1,12大于1，第二个索引的第一个索引列为56,12小于56，于是从这俩索引的中间读到下一个节点的磁盘文件地址（此处实际上是存在一个指针的，指向的是下一个节点的磁盘位置）。\n2. 进行一次磁盘IO，将此节点值加载后内存中，然后根据第一步一样进行判断，发现 数据都是匹配的，然后根据指针将此联合索引值所在的叶子节点也从磁盘中加载后内存，此时又发生了一次磁盘IO，最终根据叶子节点中索引值关联的 主键值  。\n3. 根据主键值  回表 去主键索引树（聚簇索引）中查询具体的行记录。\n\n<img src=\"../img/数据库/MySQL/联合索引3.png\" width=\"60%\" />\n\n## 锁\n### 行级锁\n加在数据行(row)上的锁，行级锁是粒度最低的锁，发生锁冲突的概率也最低、并发度最高。但是加锁慢、开销大，容易发生死锁现象\n- InnoDB行锁是通过给索引上的索引项加锁来实现的\n- Record lock：记录锁，单个行记录上的锁\n- Gap lock：间隙锁，锁定一个范围，不包括记录本身\n- Next-key lock：record+gap临键锁，锁定一个范围，包含记录本身\n### 表级锁\n加在表(table)上的锁，粒度最高，发生锁冲突的概率大，并发度较低。一次将整个表锁定，加锁快、开销小。\n### 页锁\n页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快，但冲突多，行级冲突少，但速度慢。所以取了折衷的页级，一次锁定相邻的一组记录。只有BDB引擎支持页级锁\n\n## 事务\n事务是由存储引擎实现的\n### 事务特性ACID\n#### 1、原子性（Atomicity，或称不可分割性）\n- 定义 原子性是指一个事务是一个不可分割的工作单位，其中的操作要么都做，要么都不做；如果事务中一个sql语句执行失败，则已执行的语句也必须回滚，数据库退回到事务前的状态\n- 实现原理 undo log\n#### 2、一致性（Consistency）\n- 一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态，也就是说一个事务执行之前和执行之后都必须处于一致性状态\n- 如转账的栗子: 假设用户A和用户B两者的钱加起来一共是5000，那么不管A和B之间如何转账，转几次账，事务结束后两个用户的钱相加起来应该还得是5000，这就是事务的一致性\n#### 3、隔离性（Isolation）\n- 事务内部的操作与其他事务是隔离的，并发执行的各个事务之间不能互相干扰\n  - (一个事务)写操作对(另一个事务)写操作的影响：锁机制保证隔离性\n  - (一个事务)写操作对(另一个事务)读操作的影响：MVCC保证隔离性\n\n[隔离级别](#)\n\n##### [3.1、读未提交：read uncommitted](#)\n事物A和事物B，事物A未提交的数据，事物B可以读取到 这里读取到的数据叫做“脏数据 \n##### [3.2、读已提交：read committed](#)\n事物A和事物B，事物A提交的数据，事物B才能读取到 这种隔离级别会导致“不可重复读取”,再次读发现和第一次读的内容不一致\n- 使用行锁解决读未提交问题，脏读问题\n##### [3.3、可重复读：repeatable read](#)\n事务A和事务B，事务A提交之后的数据，事务B读取不到（B始终读到的数据是第一次读到的）\n- mvcc解决不可重复读问题\n- 锁解决幻读问题\n##### [3.4、串行化：serializable](#)\n事务A和事务B，事务A在操作数据库时，事务B只能排队等待\n- 这种级别可以避免“幻像读”，每一次读取的都是数据库中真实存在数据，事务A与事务B串行，而不并发\n\n##### MVCC解决不可重复读\nMVCC 实现三大要素\n\n1-`隐式字段`\n\n- 在 Innodb 存储引擎中，在有聚簇索引的情况下每一行记录中都会隐藏俩个字段，如果没有聚簇索引则还有一个 6byte 的隐藏主键\n- 俩个隐式字段为 DB_TRX_ID，DB_ROLL_PTR，没有聚簇索引还会有 DB_ROW_ID 这个字段\n  - `DB_TRX_ID`：记录创建这条记录上次修改他的事务 ID。\n  - `DB_ROLL_PTR`：回滚指针，指向这条记录的上一个版本\n\n2-`undo log（回滚日志）` undo log 保存的是一个版本链，也就是使用 DB_ROLL_PTR 这个字段来连接的。\n\n- 当事务不可见时就会读取相应的之前版本链上的版本\n\n3-`ReadView` 通过隐藏列和版本链，MySQL可以将数据恢复到指定版本；但是具体要恢复到哪个版本，则需要根据ReadView来确定。所谓ReadView，是指事务（记做事务A）在某一时刻给整个事务系统（trx_sys）打快照，之后再进行读操作时，会将读取到的数据中的事务id与trx_sys快照比较，从而判断数据对该ReadView是否可见，即对事务A是否可见\n\n- 当前系统中还有哪些活跃的读写事务，把它们的事务id放到一个列表中，我们把这个列表命名为为m_ids\n- 如果被访问版本的 trx_id 属性值小于 m_ids 列表中最小的事务id，表明生成该版本的事务在生成 ReadView 前已经提交，所以该版本可以被当前事务访问。\n- 如果被访问版本的 trx_id 属性值大于 m_ids 列表中最大的事务id，表明生成该版本的事务在生成 ReadView 后才生成，所以该版本不可以被当前事务访问。\n- 如果被访问版本的 trx_id 属性值在 m_ids 列表中最大的事务id和最小事务id之间，那就需要判断一下 trx_id 属性值是不是在 m_ids 列表中，如果在，说明创建 ReadView 时生成该版本的事务还是活跃的，该版本不可以被访问；如果不在，说明创建 ReadView 时生成该版本的事务已经被提交，该版本可以被访问。\n             \n```\n1、初始：\n  事务1\n  事务2\n  事务3\n  事务4 读取\n  事务5\n          \n2、事务2提交了：\n  事务1\n  事务2 commit\n  事务3\n  事务4\n  事务5\n          \n3、事务4打快照：\n  事务1\n  事务3\n  事务4 生成trx_sys[1-5]\n  事务5\n            \n4、事务4读取事务0：\n  事务1\n  事务3\n  事务4 再次读取，内容如果包含事务0的内容，0 < trx_sys[1-5]最小1 可以被读取\n  事务5\n          \n5、事务4读取事务6：\n  事务1\n  事务3\n  事务4 再次读取，内容如果包含事务6的内容，6 > trx_sys[1-5]最大5 不可以被读取\n  事务5\n            \n  事务6\n            \n6、事务4读取事务3：\n  事务1\n  事务3\n  事务4 再次读取，内容如果包含事务3的内容，3 在 trx_sys[1-5]中 ，因为创建时还活跃(创建时事务3还未提交)，不可以被读取\n  事务5\n            \n  事务6\n          \n7、事务4读取事务2：\n  事务1\n  事务3\n  事务4 再次读取，内容如果包含事务2的内容，2 在 trx_sys[1-5]中 ，因为创建时事务2已提交，可以被读取\n  事务5\n            \n  事务6\n```\n\n##### 幻读问题\n当一个事务在读取某个范围的记录时，另外一个事务在这个范围内插入了一条新的数据，当事务再次进行读取数据时，发现比第一次读取记录多了一条，这就是所谓的幻读，两次读取的结果不一致.\n\n虽然可以达到可重复读取，但是会导致“幻像读”\n\n- 加锁读解决幻读问题\n  - 示例\n    - 共享锁读取 `select...lock in share mode`\n      - 共享锁事务之间的读取是可以正常获取的\n      - 共享锁之间的update是需要等待锁释放的\n    - 排它锁读取 `select...for update`\n      - 读取和修改都是不允许的\n      - innodb_lock_wait_timeout 设置全局的锁超时时间\n  - 加锁读在查询时会对查询的数据加锁（共享锁或排它锁）。由于锁的特性，当某事务对数据进行加锁读后，其他事务无法对数据进行写操作，因此可以避免脏读和不可重复读。\n  - 而避免幻读，则需要通过next-key lock。next-key lock是行锁的一种，实现相当于record lock(记录锁) + gap lock(间隙锁)；其特点是不仅会锁住记录本身(record lock的功能)，还会锁定一个范围(gap lock的功能)。因此，加锁读同样可以避免脏读、不可重复读和幻读，保证隔离性\n\n####  4、持久性（Durability）\n定义 持久性是指事务一旦提交，它对数据库的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。\n- 实现原理：redo log\n## 事务日志\t\n### redo log（重做日志）\n- 保证事务持久性\n- MySQL每执行一条语句，先将记录写入redo log buffer ，然后写入OS buffer（cache），系统调用fsync()刷入redo log file磁盘\n- redo log 采用大小固定，循环写入的方式，写到结尾会回到开头写\n- MySQL提供了buffer pool（内存）来提高读取速度，并且对数据的修改也会首先写入bufferpool等待后刷入磁盘，如果在还未刷入磁盘时发生宕机就会发生数据丢失，所以引入redo log（磁盘）对数据的修改也会记录在redo log中，DB宕机后重启，InnoDB会首先去查看数据页中的LSN的数值。这个值代表数据页被刷新回磁盘的LSN的大小。然后再去查看redo log的LSN的大小。如果数据页中的LSN值大说明数据页领先于redo log刷新回磁盘，不需要进行恢复。反之需要从redo log中恢复数据\n  - LSN (逻辑序列号)\n    - LSN不仅记录在重做日志中，还存在于每个页中，在每个页的头部，值FIL_PAGE_LSN记录该页的LSN。表示页最后刷新时LSN的大小\n    - 单调递增的\n- 与bing日志的不同\n  - bing日志使数据库可以基于时间点恢复数据，也用于主从复制而且是MySQL服务层提供的，二进制\n### undo log（回滚日志）\n- 事务原子性和隔离性实现的基础\n- 当事务对数据库进行修改时，InnoDB会生成对应的undo log；如果事务执行失败或调用了rollback，导致事务需要回滚，便可以利用undo log中的信息将数据回滚到修改之前的样子\n- undo log属于逻辑日志，它记录的是sql执行相关的信息。当发生回滚时，InnoDB会根据undo log的内容做与之前相反的工作：对于每个insert，回滚时会执行delete；对于每个delete，回滚时会执行insert；对于每个update，回滚时会执行一个相反的update，把数据改回去\n## 二进制日志( binlog )\n用于记录数据库执行的写入性操作(不包括查询)信息，以二进制的形式保存在磁盘中\n- Server 层进行记录\n- binlog 是通过追加的方式进行写入的，可以通过 max_binlog_size 参数设置每个 binlog文件的大小，当文件大小达到给定值之后，会生成新的文件来保存日志\n- binlog使用场景\n  - `主从复制` 在 Master 端开启 binlog ，然后将 binlog 发送到各个 Slave 端， Slave 端重放 binlog 从而达到主从数据一致\n  - `数据恢复` 通过使用 mysqlbinlog 工具来恢复数据\n- binlog刷盘时机\n  - 对于 InnoDB 存储引擎而言，只有在事务提交时才会记录 biglog ，此时记录还在内存中，那么 biglog 是什么时候刷到磁盘中的呢？ mysql 通过 sync_binlog 参数控制 biglog 的刷盘时机，取值范围是 0-N\n    - 0：不去强制要求，由系统自行判断何时写入磁盘；\n    - 1：每次 commit 的时候都要将 binlog 写入磁盘；\n    - N：每N个事务，才会将 binlog 写入磁盘。\n- binlog日志格式\n  - `STATMENT`\n    - MySQL 5.7.7 之前，默认的格式是 STATEMENT\n    - 基于 SQL 语句的复制( statement-based replication, SBR )，每一条会修改数据的sql语句会记录到 binlog 中\n      - * 优点： 不需要记录每一行的变化，减少了` binlog ` 日志量，节约了 ` IO ` , 从而提高了性能；\n      - * 缺点： 在某些情况下会导致主从数据不一致，比如执行` sysdate() ` 、 ` slepp() ` 等 。\n  - `ROW`\n    - MySQL 5.7.7 之后，默认值是 ROW\n    - 基于行的复制( row-based replication, RBR )，不记录每条sql语句的上下文信息，仅需记录哪条数据被修改了\n      - 优点： 不会出现某些特定情况下的存储过程、或function、或trigger的调用和触发无法被正确复制的问题 ；\n      - 缺点： 会产生大量的日志，尤其是 alter table 的时候会让日志暴涨\n  - `MIXED`\n    - 基于 STATMENT 和 ROW 两种模式的混合复制( mixed-based replication, MBR )，一般的复制使用 STATEMENT 模式保存 binlog ，对于 STATEMENT 模式无法复制的操作使用 ROW 模式保存 binlog\n- 日志格式通过 binlog-format 指定\n\n## redo log和binlog的区别\n- redo log和binlog的产生方式不同。redo log是在物理存储引擎层产生，而binlog是在MySQL数据库的Server层产生的，并且binlog不仅针对InnoDB存储引擎，MySQL数据库中的任何存储引擎对数据库的更改都会产生binlog。\n- redo log和binlog的记录形式不同。MySQL Server层产生的binlog记录的是一种逻辑日志，即通过SQL语句的方式来记录数据库的修改；而InnoDB层产生的redo log是一种物理格式日志，其记录的是对于磁盘中每一个数据页的修改。\n- redo log和binlog记录的时间点不同。binlog只是在事务提交完成后进行一次写入，而redo log则是在事务进行中不断地被写入，redo log并不是随着事务提交的顺序进行写入的，这也就是说在redo log 中针对一个事务会有多个不连续的记录日志。\n\n## redo log和binlog一致性问题\nbinlog和redo log都是在事务提交阶段记录的。这时我们不禁会有一些疑问：\n\n- 是先写binlog还是先写redo log的呢？\n- 写binlog和redo log的顺序对于数据库系统的持久性和主从复制会不会产生影响？\n- 如果有影响，MySQL又是怎么做到binlog和redo log的一致性的呢？\n\n带着这些问题，我深入地研究了MySQL中binlog和redo log的一致性问题。\n\n### 先写binlog还是先写redo log的呢？\n#### 假设一：先写redo log再写binlog\n想象一下，如果数据库系统在写完一个事务的redo log时发生crash，而此时这个事务的binlog还没有持久化。在数据库恢复后，主库会根据redo log中去完成此事务的重做，主库中就有可这个事务的数据。但是，由于此事务并没有产生binlog，即使主库恢复后，关于此事务的数据修改也不会同步到从库上，这样就产生了主从不一致的错误。\n#### 假设二：先写binlog再写redo log\n想象一下，如果数据库系统在写完一个事务的binlog时发生crash，而此时这个事务的redo log还没有持久化，或者说此事务的redo log还没记录完（至少没有记录commit log）。在数据库恢复后，从库会根据主库中记录的binlog去回放此事务的数据修改。但是，由于此事务并没有产生完整提交的redo log，主库在恢复后会回滚该事务，这样也会产生主从不一致的错误。\n\n通过上面的假设和分析，我们可以看出，不管是先写redo log还是先写binlog，都有可能会产生主从不一致的错误，那么MySQL又是怎么做到binlog和redo log的一致性的呢？\n### MySQL的内部XA（两阶段提交）\nXA-2PC (two phase commit, 两阶段提交 )\n\nXA是由X/Open组织提出的分布式事务的规范。XA规范主要定义了(全局)事务管理器(TM: Transaction Manager)和(局部)资源管理器(RM: Resource Manager)之间的接口。XA为了实现分布式事务，将事务的提交分成了两个阶段：也就是2PC (tow phase commit)，XA协议就是通过将事务的提交分为两个阶段来实现分布式事务。\n- prepare 阶段：第一阶段，事务管理器向所有涉及到的数据库服务器发出prepare\"准备提交\"请求，数据库收到请求后执行数据修改和日志记录等处理，处理完成后只是把事务的状态改成\"可以提交\",然后把结果返回给事务管理器.\n- commit 阶段：事务管理器收到回应后进入第二阶段，如果在第一阶段内有任何一个数据库的操作发生了错误，或者事务管理器收不到某个数据库的回应，则认为事务失败，回撤所有数据库的事务。数据库服务器收不到第二阶段的确认提交请求，也会把\"可以提交\"的事务回撤。如果第一阶段中所有数据库都提交成功，那么事务管理器向数据库服务器发出\"确认提交\"请求，数据库服务器把事务的\"可以提交\"状态改为\"提交完成\"状态，然后返回应答。\n\nMySQL中的XA实现分为：外部XA和内部XA。前者是指我们通常意义上的分布式事务实现；后者是指单台MySQL服务器中，Server层作为TM(事务协调者)，而服务器中的多个数据库实例作为RM，而进行的一种分布式事务，也就是MySQL跨库事务；也就是一个事务涉及到同一条MySQL服务器中的两个innodb数据库(因为其它引擎不支持XA)。\n\n**内部XA的额外功能**\n\n在MySQL内部，在事务提交时利用两阶段提交(内部XA的两阶段提交)很好地解决了上面提到的binlog和redo log的一致性问题：\n\n- 第一阶段： InnoDB Prepare阶段。此时SQL已经成功执行，并生成事务ID(xid)信息及redo和undo的内存日志。此阶段InnoDB会写事务的redo log，但要注意的是，此时redo log只是记录了事务的所有操作日志，并没有记录提交（commit）日志，因此事务此时的状态为Prepare。此阶段对binlog不会有任何操作。\n- 第二阶段：commit 阶段，这个阶段又分成两个步骤。第一步写binlog（先调用write()将binlog内存日志数据写入文件系统缓存，再调用fsync()将binlog文件系统缓存日志数据永久写入磁盘）；第二步完成事务的提交（commit），此时在redo log中记录此事务的提交日志（增加commit 标签）。\n可以看出，此过程中是先写redo log再写binlog的。但需要注意的是，在第一阶段并没有记录完整的redo log（不包含事务的commit标签），而是在第二阶段记录完binlog后再写入redo log的commit 标签。还要注意的是，在这个过程中是以第二阶段中binlog的写入与否作为事务是否成功提交的标志。\n\n通过上述MySQL内部XA的两阶段提交就可以解决binlog和redo log的一致性问题。数据库在上述任何阶段crash，主从库都不会产生不一致的错误。\n\n此时的崩溃恢复过程如下：\n\n- 如果数据库在记录此事务的binlog之前和过程中发生crash。数据库在恢复后认为此事务并没有成功提交，则会回滚此事务的操作。与此同时，因为在binlog中也没有此事务的记录，所以从库也不会有此事务的数据修改。\n- 如果数据库在记录此事务的binlog之后发生crash。此时，即使是redo log中还没有记录此事务的commit 标签，数据库在恢复后也会认为此事务提交成功（因为在上述两阶段过程中，binlog写入成功就认为事务成功提交了）。它会扫描最后一个binlog文件，并提取其中的事务ID（xid），InnoDB会将那些状态为Prepare的事务（redo log没有记录commit 标签）的xid和Binlog中提取的xid做比较，如果在Binlog中存在，则提交该事务，否则回滚该事务。这也就是说，binlog中记录的事务，在恢复时都会被认为是已提交事务，会在redo log中重新写入commit标志，并完成此事务的重做（主库中有此事务的数据修改）。与此同时，因为在binlog中已经有了此事务的记录，所有从库也会有此事务的数据修改。\n\n### 总结\n上述利用两阶段提交解决了事务提交时binlog和redo log的一致性问题，此过程的实现是在MySQL 5.6 之前。但是此过程存在严重缺陷：此过程中为了保证MySQL Server层binlog的写入顺序和InnoDB层的事务提交顺序是一致的，MySQL数据库内部使用了prepare_commit_mutex这个锁。但是在启用了这个锁之后，并不能并发写入binlog，从而导致了group commit失效。这个问题在MySQL 5.6中的Binary Log Group Commit（BLGC）得到解决。\n\n\n## 切分\n### 水平切分\n水平切分又称为 Sharding，它是将同一个表中的记录拆分到多个结构相同的表中\n- 当一个表的数据不断增多时，Sharding 是必然的选择，它可以将数据分布到集群的不同节点上，从而缓存单个数据库的压力。\n  - 优点\n    - 单库单表的数据保持在一定的量级，有助于性能的提高。\n    - 切分的表的结构相同，应用层改造较少，只需要增加路由规则即可。\n    - 提高了系统的稳定性和负载能力。\n  - 缺点\n    - 切分后，数据是分散的，很难利用数据库的Join操作，跨库Join性能较差。\n    - 拆分规则难以抽象。\n    - 分片事务的一致性难以解决。\n    - 数据扩容的难度和维护量极大。\n### 垂直切分\n垂直切分是将一张表按列切分成多个表，通常是按照列的关系密集程度进行切分，也可以利用垂直切分将经常被使用的列和不经常被使用的列切分到不同的表中\n- 优点\n  - 拆分后业务清晰，拆分规则明确。\n  - 系统之间进行整合或扩展很容易。\n  - 按照成本、应用的等级、应用的类型等奖表放到不同的机器上，便于管理。\n  - 便于实现动静分离、冷热分离的数据库表的设计模式。\n  - 数据维护简单。\n- 缺点\n  - 部分业务表无法关联(Join), 只能通过接口方式解决，提高了系统的复杂度。\n  - 受每种业务的不同限制，存在单库性能瓶颈，不易进行数据扩展和提升性能。\n  - 事务处理复杂。\n## 复制\n### 主从复制\n#### 原理\n主库会生成一个 log dump 线程,用来给从库 I/O 线程传 Binlog 数据。 从库的 I/O 线程会去请求主库的 Binlog，并将得到的 Binlog 写到本地的 relay log (中继日志)文件中。 SQL 线程,会读取 relay log 文件中的日志，并解析成 SQL 语句逐一执行。\n- 主节点\n  - 打开 Master 端的 Binlog 功能\n  - `log dump thread` 当从节点连接主节点时，主节点会为其创建一个 log dump 线程，用于发送和读取 Binlog 的内容。在读取 Binlog 中的操作时，log dump 线程会对主节点上的 Binlog 加锁；当读取完成发送给从节点之前，锁会被释放。主节点会为自己的每一个从节点创建一个 log dump 线程\n- 从库\n  - `I/O 线程` 当从节点上执行start slave命令之后，从节点会创建一个 I/O 线程用来连接主节点，请求主库中更新的Binlog。I/O 线程接收到主节点的 log dump 进程发来的更新之后，保存在本地 relay-log（中继日志）中\n  - `SQL 线程` SQL 线程负责读取 relay log 中的内容，解析成具体的操作并执行，最终保证主从数据的一致性\n#### 主从复制的模式\n- 异步模式 (async-mode)\n  - 这种模式下，主节点不会主动推送数据到从节点，主库在执行完客户端提交的事务后会立即将结果返给给客户端，并不关心从库是否已经接收并处理，这样就会有一个问题，主节点如果崩溃掉了，此时主节点上已经提交的事务可能并没有传到从节点上，如果此时，强行将从提升为主，可能导致新主节点上的数据不完整\n    - 从库主动拉\n    - 不保证数据完整复制\n  - 默认\n- 半同步模式(semi-sync)\n  - 介于异步复制和全同步复制之间，主库在执行完客户端提交的事务后不是立刻返回给客户端，而是等待至少一个从库接收到并写到 relay log 中才返回成功信息给客户端（只能保证主库的 Binlog 至少传输到了一个从节点上），否则需要等待直到超时时间然后切换成异步模式再提交。\n    - 等待至少一个从库回复\n    - 超时后使用异步模式\n- 全同步模式\n  - 指当主库执行完一个事务，然后所有的从库都复制了该事务并成功执行完才返回成功信息给客户端。因为需要等待所有从库执行完该事务才能返回成功信息，所以全同步复制的性能必然会收到严重的影响\n    - 所有从库全部完成后主库才确认成功\n#### 问题\n延迟\n- 主库的TPS很高，从库来不及同步\n- 解决\n  - 官方称为Enhanced Multi-threaded Slaves，即MTS\n  - MySQL 5.7 版本引入了基于组提交的并行复制\n  - 多线程并发执行relay log提交的事务\n### 读写分离\n- Apache ShardingSphere\n- 主库主写，从库主读\n## 中间件\n### mycat\nMyCAT主要是通过对SQL的拦截，然后经过一定规则的分片解析、路由分析、读写分离分析、缓存分析等，然后将SQL发给后端真实的数据块，并将返回的结果做适当处理返回给客户端\n### ShardingSphere\nSharding-JDBC、Sharding-Proxy和Sharding-Sidecar（计划中）这3款相互独立的产品组成\n#### Sharding-JDBC\n1. SQL 解析：解析分为词法解析和语法解析。先通过词法解析器将这句 SQL 拆分为一个个不可再分的单词，再使用语法解析器对 SQL 进行理解，并最终提炼出解析上下文。简单来说就是要理解这句 SQL，明白它的构造和行为，这是下面的优化、路由、改写、执行和归并的基础。\n2. SQL 路由：根据解析上下文匹配用户对这句 SQL 所涉及的库和表配置的分片策略，并根据分片策略生成路由后的 SQL。路由后的 SQL 有一条或多条，每一条都对应着各自的真实物理分片。\n3. SQL 改写：将 SQL 改写为在真实数据库中可以正确执行的语句（逻辑 SQL 到物理 SQL 的映射，例如把逻辑表名改成带编号的分片表名）。\n4. SQL 执行：通过多线程执行器异步执行路由和改写之后得到的 SQL 语句。\n5. 结果归并：将多个执行结果集归并以便于通过统一的 JDBC 接口输出。\n#### Sharding-Proxy\n- Sharding-Proxy的定位是透明化的数据库代理，它封装了数据库二进制协议，用于完成对异构语言的支持\n- 前端（Frontend）负责与客户端进行网络通信，采用的是基于NIO的客户端/服务器框架，在Windows和Mac操作系统下采用NIO模型，Linux系统自动适配为Epoll模型，在通信的过程中完成对MySQL协议的编解码；\n- 核心组件（Core-module）得到解码的MySQL命令后，开始调用Sharding-Core对SQL进行解析、改写、路由、归并等核心功能；\n- 后端（Backend）与真实数据库的交互暂时借助基于BIO的Hikari连接池。BIO的方式在数据库集群规模很大，或者一主多从的情况下，性能会有所下降。所以未来我们还会提供NIO的方式连接真实数据库\n\n## 数据库三范式\n### 第一范式\n1NF是对属性的原子性，要求属性具有原子性，不可再分解；\n\n_表：字段1、 字段2(字段2.1、字段2.2)、字段3 ......_\n\n如学生（学号，姓名，性别，出生年月日），如果认为最后一列还可以再分成（出生年，出生月，出生日），它就不是一范式了，否则就是；\n### 第二范式\n2NF是对记录的唯一性，要求记录有唯一标识，即实体的唯一性，即不存在部分依赖；\n\n_表：学号、课程号、姓名、学分;_\n\n这个表明显说明了两个事务:学生信息, 课程信息;由于非主键字段必须依赖主键，这里学分依赖课程号，姓名依赖与学号，所以不符合二范式。\n### 第三范式\n3NF是对字段的冗余性，要求任何字段不能由其他字段派生出来，它要求字段没有冗余，即不存在传递依赖；\n\n_表: 学号, 姓名, 年龄, 学院名称, 学院电话_\n\n因为存在依赖传递: (学号) → (学生)→(所在学院) → (学院电话) 。\n\n## SQL优化\n### explain\nExplain 可以用来分析select、update、delete、insert等语句，开发人员可以通过分析 Explain 结果来优化查询语句\n### 属性\n\n<img src=\"../img/数据库/MySQL/explain.png\" width=\"60%\" />\n\n<img src=\"../img/数据库/MySQL/explain2.png\" width=\"60%\" />\n\n- `id`: SELECT 查询的标识符. 每个 SELECT 都会自动分配一个唯一的标识符.\n  - SQL执行的顺序的标识,SQL从大到小的执行\n    1. id相同时，执行顺序由上至下\n    2. 如果是子查询，id的序号会递增，id值越大优先级越高，越先被执行\n    3. id如果相同，可以认为是一组，从上往下顺序执行；在所有组中，id值越大，优先级越高，越先执行\n- `select_type`: SELECT 查询的类型.\n  - `SIMPLE`, 表示此查询不包含 UNION 查询或子查询\n  - `PRIMARY`, 表示此查询是最外层的查询\n  - `UNION`, 表示此查询是 UNION 的第二或随后的查询\n  - `DEPENDENT UNION`, UNION 中的第二个或后面的查询语句, 取决于外面的查询\n  - `UNION RESULT`, UNION 的结果\n  - `SUBQUERY`, 子查询中的第一个 SELECT\n  - `DEPENDENT SUBQUERY`: 子查询中的第一个 SELECT, 取决于外面的查询. 即子查询依赖于外层查询的结果.\n- `table`: 查询的是哪个表\n- `partitions`: 匹配的分区\n- `type`: 表示MySQL在表中找到所需行的方式，又称“访问类型” ，效率：`NULL > system > const > eq_ref > ref > range ~ index_merge > index > ALL`\n  - `NULL`: MySQL在优化过程中分解语句，执行时甚至不用访问表或索引，例如从一个索引列里选取最小值可以通过单独索引查找完成\n  - `system`: 表只有一行记录（等于系统表），这是const类型的特列，平时不会出现，这个也可以忽略不计。\n  - `const`: 表示通过索引一次就找到了，const用于比较primary key或者unique索引。因为只匹配一行数据，所以很快如将主键置于where列表中，MySQL就能将该查询转换为一个常量。\n  - `eq_ref`: 唯一性索引扫描，对于每个索引键，表中只有一条记录与之匹配。常见于主键或唯一索引扫描。\n  - `ref`: 非唯一性索引扫描，返回匹配某个单独值的所有行，本质上也是一种索引访问，它返回所有匹配某个单独值的行，然而，它可能会找到多个符合条件的行，所以他应该属于查找和扫描的混合体\n  - `range`: 表示使用索引范围查询, 通过索引字段范围获取表中部分数据记录. 这个类型通常出现在 =, <>, >, >=, <, <=, IS NULL, <=>, BETWEEN, IN() 操作中.\n    - 当 type 是 range 时, 那么 EXPLAIN 输出的 ref 字段为 NULL, 并且 key_len 字段是此次查询中使用到的索引的最长的那个\n  - `index`: 表示全索引扫描(full index scan), 和 ALL 类型类似, 只不过 ALL 类型是全表扫描, 而 index 类型则仅仅扫描所有的索引, 而不扫描数据.\n    - index 类型通常出现在: 所要查询的数据直接在索引树中就可以获取到, 而不需要扫描数据. 当是这种情况时, Extra 字段 会显示 Using index\n  - `ALL`: 表示全表扫描, 这个类型的查询是性能最差的查询之一. 通常来说, 我们的查询不应该出现 ALL 类型的查询, 因为这样的查询在数据量大的情况下, 对数据库的性能是巨大的灾难. 如一个查询是 ALL 类型查询, 那么一般来说可以对相应的字段添加索引来避免\n\n- `possible_keys`: 此次查询中可能选用的索引\n- `key`: 此次查询中确切使用到的索引.\n- `key_len`: 表示索引中使用的字节数，可通过该列计算查询中使用的索引的长度（key_len显示的值为索引字段的最大可能长度，并非实际使用长度，即key_len是根据表定义计算而得，不是通过表内检索出的）\n- `ref`: 哪个字段或常数与 key 一起被使用\n- `rows`: 显示此查询一共扫描了多少行. 这个是一个估计值.\n- `filtered`: 这个字段表示存储引擎返回的数据在server层过滤后，剩下多少满足查询的记录数量的比例，注意是百分比，不是具体记录数。这个字段不重要\n- `extra`: 额外的信息\n  - `Using where`:列数据是从仅仅使用了索引中的信息而没有读取实际的行动的表返回的，这发生在对表的全部的请求列都是同一个索引的部分的时候，表示mysql服务器将在存储引擎检索行后再进行过滤\n  - `Using temporary`：表示MySQL需要使用临时表来存储结果集，常见于排序和分组查询\n  - `Using filesort`：MySQL中无法利用索引完成的排序操作称为“文件排序”\n    - 在使用order by关键字的时候，如果待排序的内容不能由所使用的索引直接完成排序的话，MySQL有可能就要进行文件排序\n    - filesort是通过相应的排序算法将取得的数据在内存中进行排序，所使用的内存区域也就是通过sort_buffer_size 系统变量所设置的排序区。这个排序区是每个Thread 独享的，可能同一时刻在MySQL 中存在多个 sort buffer 内存区域\n    - 比如 SELECT id FROM testing WHERE room_number=1000 ORDER BY id ;只有ID索引，explain可能出现using where;using filesort就是无法直接使用索引完成排序，如果加上room_number索引，则结果只有using where\n  - `Using join buffer`：改值强调了在获取连接条件时没有使用索引，并且需要连接缓冲区来存储中间结果。如果出现了这个值，那应该注意，根据查询的具体情况可能需要添加索引来改进能。\n  - `Impossible where`：这个值强调了where语句会导致没有符合条件的行。\n  - `Select tables optimized away`：这个值意味着仅通过使用索引，优化器可能仅从聚合函数结果中返回一行\n### 如何做慢查询排查的\n#### 什么是慢查询日志\n具体指运行时间超过long_query_time值的SQL，则会被记录到慢查询日志中。long_query_time的默认值为10，意思是运行10秒以上的语句。\n- 如果不是调优需要的话，一般不建议启动该参数，因为开启慢查询日志会或多或少带来一定的性能影响。慢查询日志支持将日志记录写入文件\n- 默认关闭：`SHOW VARIABLES LIKE '%slow_query_log%';`\n\n  <img src=\"../img/数据库/MySQL/慢查询配置.png\" width=\"60%\" />\n\n  - 临时生效：`set global slow_query_log=1;`\n  - 永久生效，修改配置文件my.cnf（其它系统变量也是如此）\n- 查询当前系统中有多少条慢查询记录\n  - `show global status like '%Slow_queries%';`\n#### 使用工具分析\n- mysql自带的 mysqldumpslow,\n  - `mysqldumpslow  /var/lib/mysql/mysql-slow.log`\n    - 得到的信息\n      - 主要功能是, 统计不同慢sql的\n      - 出现次数(Count),\n      - 执行最长时间(Time),\n      - 累计总耗费时间(Time),\n      - 等待锁的时间(Lock),\n      - 发送给客户端的行总数(Rows),\n      - 扫描的行总数(Rows),\n      - 用户以及sql语句本身(抽象了一下格式, 比如 limit 1, 20 用 limit N,N 表示).\n  - 一般一台服务器有很多数据库，这样根本看不出来啊\n- mysqlsla\n  - 需要单独安装\n  - 使用举例\n    - 统计慢查询文件为/data/mysql/127-slow.log的所有select的慢查询sql，并显示执行时间最长的100条sql，并写到sql_select.log中去\n    - `mysqlsla -lt slow  -sf \"+select\" -top 100  /data/mysql/127-slow.log >/tmp/sql_select.log`\n      <img src=\"../img/数据库/MySQL/mysqlsla.png\" width=\"60%\" />\n    \n  - 返回参数\n    - `Count`, sql的执行次数及占总的slow log数量的百分比.\n    - `Time`, 执行时间, 包括总时间, 平均时间, 最小, 最大时间, 时间占到总慢sql时间的百分比.\n    - `95% of Time`, 去除最快和最慢的sql, 覆盖率占95%的sql的执行时间.\n    - `Lock Time`, 等待锁的时间.95% of Lock , 95%的慢sql等待锁时间.Rows sent, 结果行统计数量, 包括平均, 最小, 最大数量.\n    - `Rows examined`, 扫描的行数量.\n    - `Database`, 属于哪个[数据库]\n    - `Users`, 哪个用户,IP, 占到所有用户执行的sql百分比\n    - `Query abstract`, 抽象后的sql语句\n    - `Query sample`, sql语句\n  - 对于得到这个信息还可以进一步分析，就是登陆到mysql 的客户端，登陆数据库，执行 EXPLAIN查看sql具体的 type 信息。\n### select * select col 主要区别\n- select * 是查询表的所有字段，数据返回量肯定比较大\n- 如果只是查询单独字段，最好写单独字段不要查询全部字段\n- 如果查询单独字段比如select abc，而写了select * 如果abc有索引，则会读完索引的数据再去读其他data造成性能问题\n### select count(*)  count(1)  count(col) 主要区别\ncount(1) 和count(*) 没有什么很大区别\n\ncount(1) 和 count(col)\n- 主要区别是 count(1)会统计值为null的数据,count(*)也不会忽略\n- 而count(col)会忽略\n\n## MySQL与PostGreSQL的区别\n一.PostgreSQL相对于MySQL的优势\n1. 在SQL的标准实现上要比MySQL完善，而且功能实现比较严谨；\n2. 存储过程的功能支持要比MySQL好，具备本地缓存执行计划的能力；\n3. 对表连接支持较完整，优化器的功能较完整，支持的索引类型很多，复杂查询能力较强；\n4. PG主表采用堆表存放，MySQL采用索引组织表，能够支持比MySQL更大的数据量。\n5. PG的主备复制属于物理复制，相对于MySQL基于binlog的逻辑复制，数据的一致性更加可靠，复制性能更高，对主机性能的影响也更小。\n6. MySQL的存储引擎插件化机制，存在锁机制复杂影响并发的问题，而PG不存在。\n\n二、MySQL相对于PG的优势：\n1. innodb的基于回滚段实现的MVCC机制，相对PG新老数据一起存放的基于XID的MVCC机制，是占优的。因此MySQL的速度是高于PG的；\n2. MySQL采用索引组织表，这种存储方式非常适合基于主键匹配的查询、删改操作，但是对表结构设计存在约束；\n3. MySQL的优化器较简单，系统表、运算符、数据类型的实现都很精简，非常适合简单的查询操作；\n4. MySQL分区表的实现要优于PG的基于继承表的分区实现，主要体现在分区个数达到上千上万后的处理性能差异较大。\n# 参考文章\n- https://www.zhihu.com/question/20596402/answer/529312016\n- https://segmentfault.com/a/1190000013695030\n- https://cloud.tencent.com/developer/article/1704743\n- https://zhuanlan.zhihu.com/p/73035620\n- https://www.jianshu.com/p/c6483ded042d\n- https://juejin.cn/post/6844904073955639304\n- https://blog.csdn.net/huangjw_806/article/details/100927097\n- https://juejin.cn/post/6844903439760097294\n- https://juejin.cn/post/6860655290732249095\n- https://blog.csdn.net/yuan2019035055/article/details/122310447?utm_medium=distribute.pc_feed_blog_category.none-task-blog-classify_tag-5.nonecasedepth_1-utm_source=distribute.pc_feed_blog_category.none-task-blog-classify_tag-5.nonecase"
  },
  {
    "path": "数据库/Nebula.md",
    "content": "* [什么是Nebula Graph](#什么是nebula-graph)\n  * [什么是图数据库](#什么是图数据库)\n  * [Nebula Graph 的优势](#nebula-graph-的优势)\n    * [开源](#开源)\n    * [高性能](#高性能)\n    * [易扩展](#易扩展)\n    * [易开发](#易开发)\n    * [高可靠访问控制](#高可靠访问控制)\n    * [生态多样化](#生态多样化)\n    * [兼容 openCypher 查询语言](#兼容-opencypher-查询语言)\n    * [面向未来硬件，读写平衡](#面向未来硬件读写平衡)\n    * [灵活数据建模](#灵活数据建模)\n    * [广受欢迎](#广受欢迎)\n    * [适用场景](#适用场景)\n    * [欺诈检测](#欺诈检测)\n    * [实时推荐](#实时推荐)\n    * [知识图谱](#知识图谱)\n    * [社交网络](#社交网络)\n* [数据模型](#数据模型)\n  * [数据模型](#数据模型-1)\n    * [图空间（Space）](#图空间space)\n    * [点（Vertex）](#点vertex)\n    * [边（Edge）](#边edge)\n    * [标签（Tag）](#标签tag)\n    * [边类型（Edge type）](#边类型edge-type)\n    * [属性（Properties）](#属性properties)\n  * [有向属性图](#有向属性图)\n* [路径](#路径)\n  * [walk](#walk)\n  * [trail](#trail)\n    * [cycle](#cycle)\n    * [circuit](#circuit)\n  * [path](#path)\n* [点 VID](#点-vid)\n  * [VID 的特点](#vid-的特点)\n  * [VID 使用建议](#vid-使用建议)\n  * [VID 生成建议](#vid-生成建议)\n  * [定义和修改 VID 与其数据类型](#定义和修改-vid-与其数据类型)\n  * [\"查询起始点\"(start vid) 与全局扫描](#查询起始点start-vid-与全局扫描)\n* [服务架构](#服务架构)\n  * [架构总览](#架构总览)\n    * [Meta 服务](#meta-服务)\n    * [Graph 服务和 Storage 服务](#graph-服务和-storage-服务)\n      * [易扩展](#易扩展-1)\n      * [高可用](#高可用)\n      * [节约成本](#节约成本)\n      * [更多可能性](#更多可能性)\n  * [Meta 服务](#meta-服务-1)\n    * [Meta 服务架构](#meta-服务架构)\n    * [Meta 服务功能](#meta-服务功能)\n      * [管理用户账号](#管理用户账号)\n      * [管理分片](#管理分片)\n      * [管理图空间](#管理图空间)\n      * [管理 Schema 信息](#管理-schema-信息)\n      * [管理 TTL 信息](#管理-ttl-信息)\n      * [管理作业](#管理作业)\n  * [Graph服务](#graph服务)\n    * [Graph 服务架构](#graph-服务架构)\n    * [Parser](#parser)\n    * [Validator](#validator)\n      * [校验元数据信息](#校验元数据信息)\n      * [校验上下文引用信息](#校验上下文引用信息)\n      * [校验类型推断](#校验类型推断)\n      * [校验 \\* 代表的信息](#校验--代表的信息)\n      * [校验输入输出](#校验输入输出)\n    * [Planner](#planner)\n      * [优化前](#优化前)\n      * [优化过程](#优化过程)\n    * [Executor](#executor)\n      * [代码结构](#代码结构)\n  * [Storage服务](#storage服务)\n    * [优势](#优势)\n    * [Storage 服务架构](#storage-服务架构)\n      * [Storage interface 层](#storage-interface-层)\n      * [Consensus 层](#consensus-层)\n      * [Store Engine 层](#store-engine-层)\n    * [KVStore](#kvstore)\n    * [数据存储格式](#数据存储格式)\n      * [点数据存储格式](#点数据存储格式)\n      * [边数据存储格式](#边数据存储格式)\n    * [属性说明](#属性说明)\n    * [数据分片](#数据分片)\n      * [切边与存储放大](#切边与存储放大)\n      * [分片算法](#分片算法)\n    * [Raft](#raft)\n      * [关于 Raft 的简单介绍](#关于-raft-的简单介绍)\n      * [Multi Group Raft](#multi-group-raft)\n      * [批量（Batch）操作](#批量batch操作)\n      * [leader 切换（Transfer Leadership）](#leader-切换transfer-leadership)\n      * [成员变更](#成员变更)\n    * [与 HDFS 的区别](#与-hdfs-的区别)\n* [参考文章](#参考文章)\n\n\n# 什么是Nebula Graph\nNebula Graph 是一款开源的、分布式的、易扩展的原生图数据库，能够承载包含数千亿个点和数万亿条边的超大规模数据集，并且提供毫秒级查询\n\n![](../img/数据库/nebula/nebula架构.png)\n\n## 什么是图数据库\n图数据库是专门存储庞大的图形网络并从中检索信息的数据库。它可以将图中的数据高效存储为点（Vertex）和边（Edge），还可以将属性（Property）附加到点和边上\n\n![](../img/数据库/nebula/图示例.png)\n\n图数据库适合存储大多数从现实抽象出的数据类型。世界上几乎所有领域的事物都有内在联系，像关系型数据库这样的建模系统会提取实体之间的关系，并将关系单独存储到表和列中，而实体的类型和属性存储在其他列甚至其他表中，这使得数据管理费时费力。\n\nNebula Graph 作为一个典型的图数据库，可以将丰富的关系通过边及其类型和属性自然地呈现。\n\n## Nebula Graph 的优势\n### 开源\nNebula Graph 是在 Apache 2.0 条款下开发的。越来越多的人，如数据库开发人员、数据科学家、安全专家、算法工程师，都参与到 Nebula Graph 的设计和开发中来，欢迎访问 Nebula Graph GitHub 主页参与开源项目。\n\n### 高性能\n基于图数据库的特性使用 C++ 编写的 Nebula Graph，可以提供毫秒级查询。众多数据库中，Nebula Graph 在图数据服务领域展现了卓越的性能，数据规模越大，Nebula Graph 优势就越大。详情请参见 Nebula Graph benchmarking 页面。\n\n### 易扩展\nNebula Graph 采用 shared-nothing 架构，支持在不停止数据库服务的情况下扩缩容。\n\n### 易开发\nNebula Graph 提供 Java、Python、C++ 和 Go 等流行编程语言的客户端，更多客户端仍在开发中。详情请参见 Nebula Graph clients。\n\n### 高可靠访问控制\nNebula Graph 支持严格的角色访问控制和 LDAP（Lightweight Directory Access Protocol）等外部认证服务，能够有效提高数据安全性。详情请参见验证和授权。\n\n### 生态多样化\nNebula Graph 开放了越来越多的原生工具，例如 Nebula Graph Studio、Nebula Console、Nebula Exchange 等，更多工具可以查看生态工具概览。\n\n此外，Nebula Graph 还具备与 Spark、Flink、HBase 等产品整合的能力，在这个充满挑战与机遇的时代，大大增强了自身的竞争力。\n\n### 兼容 openCypher 查询语言\nNebula Graph 查询语言，简称为 nGQL，是一种声明性的、部分兼容 openCypher 的文本查询语言，易于理解和使用。详细语法请参见 nGQL 指南。\n\n### 面向未来硬件，读写平衡\n闪存型设备有着极高的性能，并且价格快速下降， Nebula Graph 是一个面向 SSD 设计的产品，相比于基于 HDD + 大内存的产品，更适合面向未来的硬件趋势，也更容易做到读写平衡。\n\n### 灵活数据建模\n用户可以轻松地在 Nebula Graph 中建立数据模型，不必将数据强制转换为关系表。而且可以自由增加、更新和删除属性。详情请参见数据模型。\n\n### 广受欢迎\n腾讯、美团、京东、快手、360 等科技巨头都在使用 Nebula Graph。详情请参见 Nebula Graph 官网。\n\n### 适用场景\nNebula Graph 可用于各种基于图的业务场景。为节约转换各类数据到关系型数据库的时间，以及避免复杂查询，建议使用 Nebula Graph。\n\n### 欺诈检测\n金融机构必须仔细研究大量的交易信息，才能检测出潜在的金融欺诈行为，并了解某个欺诈行为和设备的内在关联。这种场景可以通过图来建模，然后借助 Nebula Graph，可以很容易地检测出诈骗团伙或其他复杂诈骗行为。\n\n### 实时推荐\nNebula Graph 能够及时处理访问者产生的实时信息，并且精准推送文章、视频、产品和服务。\n\n### 知识图谱\n自然语言可以转化为知识图谱，存储在 Nebula Graph 中。用自然语言组织的问题可以通过智能问答系统中的语义解析器进行解析并重新组织，然后从知识图谱中检索出问题的可能答案，提供给提问人。\n\n### 社交网络\n人际关系信息是典型的图数据，Nebula Graph 可以轻松处理数十亿人和数万亿人际关系的社交网络信息，并在海量并发的情况下，提供快速的好友推荐和工作岗位查询。\n\n# 数据模型\n\n## 数据模型\nNebula Graph 数据模型使用 6 种基本的数据模型：\n### 图空间（Space）\n图空间用于隔离不同团队或者项目的数据。不同图空间的数据是相互隔离的，可以指定不同的存储副本数、权限、分片等。\n\n### 点（Vertex）\n点用来保存实体对象，特点如下：\n- 点是用点标识符（VID）标识的。VID在同一图空间中唯一。VID 是一个 int64，或者 fixed_string(N)。\n- 点可以有 0 到多个 Tag。\n\n### 边（Edge）\n边是用来连接点的，表示两个点之间的关系或行为，特点如下：\n\n- 两点之间可以有多条边。\n- 边是有方向的，不存在无向边。\n- 四元组 <起点 VID、Edge type、边排序值 (rank)、终点 VID> 用于唯一标识一条边。边没有 EID。\n- 一条边有且仅有一个 Edge type。\n- 一条边有且仅有一个 rank，类型为 int64，默认值为 0。\n\n### 标签（Tag）\nTag 由一组事先预定义的属性构成。\n\n### 边类型（Edge type）\nEdge type 由一组事先预定义的属性构成。\n\n### 属性（Properties）\n属性是指以键值对（Key-value pair）形式存储的信息。\n\n## 有向属性图\nNebula Graph 使用有向属性图模型，指点和边构成的图，这些边是有方向的，点和边都可以有属性。\n\n下表为篮球运动员数据集的结构示例，包括两种类型的点（player、team）和两种类型的边（serve、follow）。\n\n| 类型 |\t名称 | \t属性名（数据类型）                                        |\t说明 |\n| --- | --- |---------------------------------------------------| --- |\n|Tag|\tplayer| \tname (string) age（int）\t                          |表示球员。|\n|Tag|\tteam\t| name (string)\t                                    |表示球队。|\n|Edge type|\tserve| \tstart_year (int) end_year (int)                  |\t表示球员的行为。该行为将球员和球队联系起来，方向是从球员到球队。|\n|Edge type\t|follow\t| degree (int)|\t表示球员的行为 该行为将两个球员联系起来，方向是从一个球员到另一个球员。 |\n\n> Nebula Graph 中没有无向边，只支持有向边。\n\n# 路径\n\n图论中一个非常重要的概念是路径，路径是指一个有限或无限的边序列，这些边连接着一系列点。\n\n路径的类型分为三种：walk、trail、path。关于路径的详细说明，请参见维基百科。\n\n本文以下图为例进行简单介绍。\n\n![](../img/数据库/nebula/无限边.png)\n\n## walk\n\nwalk类型的路径由有限或无限的边序列构成。遍历时点和边可以重复。\n\n查看示例图，由于 C、D、E 构成了一个环，因此该图包含无限个路径，例如A->B->C->D->E、A->B->C->D->E->C、A->B->C->D->E->C->D。\n\n> GO语句采用的是walk类型路径。\n\n## trail\ntrail类型的路径由有限的边序列构成。遍历时只有点可以重复，边不可以重复。柯尼斯堡七桥问题的路径类型就是trail。\n\n查看示例图，由于边不可以重复，所以该图包含有限个路径，最长路径由 5 条边组成：A->B->C->D->E->C。\n\n> MATCH、FIND PATH和GET SUBGRAPH语句采用的是trail类型路径。\n\n在 trail 类型中，还有cycle和circuit两种特殊的路径类型，以下图为例对这两种特殊的路径类型进行介绍。\n\n![](../img/数据库/nebula/有限边.png)\n\n### cycle\n\ncycle 是封闭的 trail 类型的路径，遍历时边不可以重复，起点和终点重复，并且没有其他点重复。在此示例图中，最长路径由三条边组成：A->B->C->A或C->D->E->C.\n\n### circuit\n\ncircuit 也是封闭的 trail 类型的路径，遍历时边不可以重复，除起点和终点重复外，可能存在其他点重复。在此示例图中，最长路径为：A->B->C->D->E->C->A。\n\n## path\npath类型的路径由有限的边序列构成。遍历时点和边都不可以重复。\n\n查看示例图，由于点和边都不可以重复，所以该图包含有限个路径，最长路径由 4 条边组成：A->B->C->D->E。\n\n# 点 VID\n在 Nebula Graph 中，一个点由点的 ID 唯一标识，即 VID 或 Vertex ID。\n\n## VID 的特点\n- VID 数据类型只可以为定长字符串FIXED_STRING(<N>)或INT64；一个图空间只能选用其中一种 VID 类型。\n- VID 在一个图空间中必须唯一，其作用类似于关系型数据库中的主键（索引+唯一约束）。但不同图空间中的 VID 是完全独立无关的。\n- 点 VID 的生成方式必须由用户自行指定，系统不提供自增 ID 或者 UUID。\n- VID 相同的点，会被认为是同一个点。例如：\n  - VID 相当于一个实体的唯一标号，例如一个人的身份证号。Tag 相当于实体所拥有的类型，例如\"滴滴司机\"和\"老板\"。不同的 Tag 又相应定义了两组不同的属性，例如\"驾照号、驾龄、接单量、接单小号\"和\"工号、薪水、债务额度、商务电话\"。\n  - 同时操作相同 VID 并且相同 Tag 的两条INSERT语句（均无IF NOT EXISTS参数），晚写入的INSERT会覆盖先写入的。\n  - 同时操作包含相同 VID 但是两个不同TAG A和TAG B的两条INSERT语句，对TAG A的操作不会影响TAG B。\n- VID 通常会被（LSM-tree 方式）索引并缓存在内存中，因此直接访问 VID 的性能最高。\n\n## VID 使用建议\n- Nebula Graph 1.x 只支持 VID 类型为INT64，从 2.x 开始支持INT64和FIXED_STRING(<N>)。在CREATE SPACE中通过参数vid_type可以指定 VID 类型。\n- 可以使用id()函数，指定或引用该点的 VID；\n- 可以使用LOOKUP或者MATCH语句，来通过属性索引查找对应的 VID;\n- 性能上，直接通过 VID 找到点的语句性能最高，例如DELETE xxx WHERE id(xxx) == \"player100\"，或者GO FROM \"player100\"等语句。通过属性先查找 VID，再进行图操作的性能会变差，例如LOOKUP | GO FROM $-.ids等语句，相比前者多了一次内存或硬盘的随机读（LOOKUP）以及一次序列化（|）。\n\n## VID 生成建议\nVID 的生成工作完全交给应用端，有一些通用的建议：\n\n- （最优）通过有唯一性的主键或者属性来直接作为 VID；属性访问依赖于 VID;\n- 通过有唯一性的属性组合来生成 VID，属性访问依赖于属性索引。\n- 通过 snowflake 等算法生成 VID，属性访问依赖于属性索引；\n- 如果个别记录的主键特别长，但绝大多数记录的主键都很短的情况，不要将FIXED_STRING(<N>)的N设置成超大，这会浪费大量内存和硬盘，也会降低性能。此时可通过 BASE64，MD5，hash 编码加拼接的方式来生成。\n- 如果用 hash 方式生成 int64 VID：在有 10 亿个点的情况下，发生 hash 冲突的概率大约是 1/10。边的数量与碰撞的概率无关。\n\n## 定义和修改 VID 与其数据类型\nVID 的数据类型必须在创建图空间时定义，且一旦定义无法修改。\n\nVID 必须在插入点时设置，且一旦设置无法修改。\n\n## \"查询起始点\"(start vid) 与全局扫描\n\n绝大多数情况下，Nebula Graph 的查询语句（MATCH、GO、LOOKUP）的执行计划，必须要通过一定方式找到查询起始点的 VID（start vid）。\n\n定位 start vid 只有两种方式：\n1. 例如 `GO FROM \"player100\" OVER` 是在语句中显式的指明 start vid 是 \"player100\"；\n2. 例如 `LOOKUP ON player WHERE player.name == \"Tony Parker\"` 或者 `MATCH (v:player {name:\"Tony Parker\"})`，是通过属性 player.name 的索引来定位到 start vid；\n\n> match (n) return n; 会返回错误Scan vertices or edges need to specify a limit number, or limit number can not push down.，这是一个全局扫描，需要用LIMIT子句限制返回数量才能执行。\n\n# 服务架构\n## 架构总览\nNebula Graph 由三种服务构成：Graph 服务、Meta 服务和 Storage 服务，是一种存储与计算分离的架构。\n\n每个服务都有可执行的二进制文件和对应进程，用户可以使用这些二进制文件在一个或多个计算机上部署 Nebula Graph 集群。\n\n下图展示了 Nebula Graph 集群的经典架构。\n\n![](../img/数据库/nebula/集群经典架构.png)\n\n### Meta 服务\n在 Nebula Graph 架构中，Meta 服务是由 nebula-metad 进程提供的，负责数据管理，例如 Schema 操作、集群管理和用户权限管理等。\n\n### Graph 服务和 Storage 服务\n\nNebula Graph 采用计算存储分离架构。Graph 服务负责处理计算请求，Storage 服务负责存储数据。它们由不同的进程提供，Graph 服务是由 nebula-graphd 进程提供，Storage 服务是由 nebula-storaged 进程提供。计算存储分离架构的优势如下：\n\n#### 易扩展\n分布式架构保证了 Graph 服务和 Storage 服务的灵活性，方便扩容和缩容。\n\n#### 高可用\n如果提供 Graph 服务的服务器有一部分出现故障，其余服务器可以继续为客户端提供服务，而且 Storage 服务存储的数据不会丢失。服务恢复速度较快，甚至能做到用户无感知。\n\n#### 节约成本\n计算存储分离架构能够提高资源利用率，而且可根据业务需求灵活控制成本。\n\n#### 更多可能性\n基于分离架构的特性，Graph 服务将可以在更多类型的存储引擎上单独运行，Storage 服务也可以为多种目的计算引擎提供服务。\n\n## Meta 服务\n### Meta 服务架构\n![](../img/数据库/nebula/meta服务架构.png)\n\nMeta 服务是由 nebula-metad 进程提供的，用户可以根据场景配置 nebula-metad 进程数量：\n- 测试环境中，用户可以在 Nebula Graph 集群中部署 1 个或 3 个 nebula-metad 进程。如果要部署 3 个，用户可以将它们部署在 1 台机器上，或者分别部署在不同的机器上。\n- 生产环境中，建议在 Nebula Graph 集群中部署 3 个 nebula-metad 进程。请将这些进程部署在不同的机器上以保证高可用。\n\n所有 nebula-metad 进程构成了基于 Raft 协议的集群，其中一个进程是 leader，其他进程都是 follower。\n\nleader 是由多数派选举出来，只有 leader 能够对客户端或其他组件提供服务，其他 follower 作为候补，如果 leader 出现故障，会在所有 follower 中选举出新的 leader。\n\n> leader 和 follower 的数据通过 Raft 协议保持一致，因此 leader 故障和选举新 leader 不会导致数据不一致。更多关于 Raft 的介绍见 Storage 服务\n\n### Meta 服务功能\n#### 管理用户账号\nMeta 服务中存储了用户的账号和权限信息，当客户端通过账号发送请求给 Meta 服务，Meta 服务会检查账号信息，以及该账号是否有对应的请求权限。\n\n更多 Nebula Graph 的访问控制说明，请参见身份验证。\n\n#### 管理分片\nMeta 服务负责存储和管理分片的位置信息，并且保证分片的负载均衡。\n\n#### 管理图空间\nNebula Graph 支持多个图空间，不同图空间内的数据是安全隔离的。Meta 服务存储所有图空间的元数据（非完整数据），并跟踪数据的变更，例如增加或删除图空间。\n\n#### 管理 Schema 信息\nNebula Graph 是强类型图数据库，它的 Schema 包括 Tag、Edge type、Tag 属性和 Edge type 属性。\n\nMeta 服务中存储了 Schema 信息，同时还负责 Schema 的添加、修改和删除，并记录它们的版本。\n\n更多 Nebula Graph 的 Schema 信息，请参见数据模型。\n\n#### 管理 TTL 信息\nMeta 服务存储 TTL（Time To Live）定义信息，可以用于设置数据生命周期。数据过期后，会由 Storage 服务进行处理，具体过程参见 TTL。\n\n#### 管理作业\nMeta 服务中的作业管理模块负责作业的创建、排队、查询和删除。\n\n## Graph服务\nGraph 服务主要负责处理查询请求，包括解析查询语句、校验语句、生成执行计划以及按照执行计划执行四个大步骤，本文将基于这些步骤介绍 Graph 服务。\n\n### Graph 服务架构\n![](../img/数据库/nebula/graph服务架构.png)\n\n查询请求发送到 Graph 服务后，会由如下模块依次处理：\n\n1. Parser：词法语法解析模块。\n2. Validator：语义校验模块。\n3. Planner：执行计划与优化器模块。\n4. Executor：执行引擎模块。\n\n### Parser\nParser 模块收到请求后，通过 Flex（词法分析工具）和 Bison（语法分析工具）生成的词法语法解析器，将语句转换为抽象语法树（AST），在语法解析阶段会拦截不符合语法规则的语句。\n\n例如`GO FROM \"Tim\" OVER like WHERE properties(edge).likeness > 8.0 YIELD dst(edge)`语句转换的 AST 如下。\n\n![](../img/数据库/nebula/parser转换ast.png)\n### Validator\nValidator 模块对生成的 AST 进行语义校验，主要包括：\n\n#### 校验元数据信息\n校验语句中的元数据信息是否正确。\n\n例如解析 OVER、WHERE和YIELD 语句时，会查找 Schema 校验 Edge type、Tag 的信息是否存在，或者插入数据时校验插入的数据类型和 Schema 中的是否一致。\n\n#### 校验上下文引用信息\n校验引用的变量是否存在或者引用的属性是否属于变量。\n\n例如语句`$var = GO FROM \"Tim\" OVER like YIELD dst(edge) AS ID; GO FROM $var.ID OVER serve YIELD dst(edge)`，Validator 模块首先会检查变量 var 是否定义，其次再检查属性 ID 是否属于变量 var。\n\n#### 校验类型推断\n推断表达式的结果类型，并根据子句校验类型是否正确。\n\n例如 WHERE 子句要求结果是 bool、null 或者 empty。\n\n#### 校验 * 代表的信息\n查询语句中包含 * 时，校验子句时需要将 * 涉及的 Schema 都进行校验。\n\n例如语句`GO FROM \"Tim\" OVER * YIELD dst(edge), properties(edge).likeness, dst(edge)`，校验OVER子句时需要校验所有的 Edge type，如果 Edge type 包含 like和serve，该语句会展开为`GO FROM \"Tim\" OVER like,serve YIELD dst(edge), properties(edge).likeness, dst(edge)`。\n\n#### 校验输入输出\n校验管道符（|）前后的一致性。\n\n例如语句`GO FROM \"Tim\" OVER like YIELD dst(edge) AS ID | GO FROM $-.ID OVER serve YIELD dst(edge)`，Validator 模块会校验 $-.ID 在管道符左侧是否已经定义。\n\n校验完成后，Validator 模块还会生成一个默认可执行，但是未进行优化的执行计划，存储在目录 src/planner 内。\n### Planner\n如果配置文件 nebula-graphd.conf 中 enable_optimizer 设置为 false，Planner 模块不会优化 Validator 模块生成的执行计划，而是直接交给 Executor 模块执行。\n\n如果配置文件 nebula-graphd.conf中enable_optimizer 设置为 true，Planner 模块会对 Validator 模块生成的执行计划进行优化。如下图所示。\n\n![](../img/数据库/nebula/planner优化.png)\n\n#### 优化前\n如上图右侧未优化的执行计划，每个节点依赖另一个节点，例如根节点 Project 依赖 Filter、Filter 依赖 GetNeighbor，最终找到叶子节点 Start，才能开始执行（并非真正执行）。\n\n在这个过程中，每个节点会有对应的输入变量和输出变量，这些变量存储在一个哈希表中。由于执行计划不是真正执行，所以哈希表中每个 key 的 value 值都为空（除了 Start 节点，起始数据会存储在该节点的输入变量中）。哈希表定义在仓库 nebula-graph 内的 src/context/ExecutionContext.cpp 中。\n\n例如哈希表的名称为 ResultMap，在建立 Filter 这个节点时，定义该节点从 ResultMap[\"GN1\"] 中读取数据，然后将结果存储在 ResultMap[\"Filter2\"] 中，依次类推，将每个节点的输入输出都确定好。\n\n#### 优化过程\nPlanner 模块目前的优化方式是 RBO（rule-based optimization），即预定义优化规则，然后对 Validator 模块生成的默认执行计划进行优化。新的优化规则 CBO（cost-based optimization）正在开发中。优化代码存储在仓库 nebula-graph 的目录 src/optimizer/ 内。\n\nRBO 是一个自底向上的探索过程，即对于每个规则而言，都会由执行计划的根节点（示例是Project）开始，一步步向下探索到最底层的节点，在过程中查看是否可以匹配规则。\n\n如上图所示，探索到节点 Filter 时，发现依赖的节点是 GetNeighbor，匹配预先定义的规则，就会将 Filter 融入到 GetNeighbor 中，然后移除节点 Filter，继续匹配下一个规则。在执行阶段，当算子 GetNeighbor 调用 Storage 服务的接口获取一个点的邻边时，Storage 服务内部会直接将不符合条件的边过滤掉，这样可以极大地减少传输的数据量，该优化称为过滤下推。\n### Executor\nExecutor 模块包含调度器（Scheduler）和执行器（Executor），通过调度器调度执行计划，让执行器根据执行计划生成对应的执行算子，从叶子节点开始执行，直到根节点结束。如下图所示。\n\n![](../img/数据库/nebula/executor执行.png)\n\n每一个执行计划节点都一一对应一个执行算子，节点的输入输出在优化执行计划时已经确定，每个算子只需要拿到输入变量中的值进行计算，最后将计算结果放入对应的输出变量中即可，所以只需要从节点 Start 一步步执行，最后一个算子的输出变量会作为最终结果返回给客户端。\n\n#### 代码结构\nNebula Graph 的代码层次结构如下：\n```text\n|--src\n   |--context    //校验期和执行期上下文\n   |--daemons\n   |--executor   //执行算子\n   |--mock\n   |--optimizer  //优化规则\n   |--parser     //词法语法分析\n   |--planner    //执行计划结构\n   |--scheduler  //调度器\n   |--service\n   |--util       //基础组件\n   |--validator  //语句校验\n   |--visitor\n\n```\n## Storage服务\nNebula Graph 的存储包含两个部分，一个是 Meta 相关的存储，称为 Meta 服务，在前文已有介绍。\n\n另一个是具体数据相关的存储，称为 Storage 服务。其运行在 nebula-storaged 进程中。本文仅介绍 Storage 服务的架构设计。\n### 优势\n- 高性能（自研 KVStore）\n- 易水平扩展（Shared-nothing 架构，不依赖 NAS 等硬件设备）\n- 强一致性（Raft）\n- 高可用性（Raft）\n- 支持向第三方系统进行同步（例如全文索引）\n### Storage 服务架构\n![](../img/数据库/nebula/storage服务架构.png)\n\nStorage 服务是由 nebula-storaged 进程提供的，用户可以根据场景配置 nebula-storaged 进程数量，例如测试环境 1 个，生产环境 3 个。\n\n所有 nebula-storaged 进程构成了基于 Raft 协议的集群，整个服务架构可以分为三层，从上到下依次为：\n\n#### Storage interface 层\n\nStorage 服务的最上层，定义了一系列和图相关的 API。API 请求会在这一层被翻译成一组针对分片的 KV 操作，例如：\n\n- getNeighbors：查询一批点的出边或者入边，返回边以及对应的属性，并且支持条件过滤。\n- insert vertex/edge：插入一条点或者边及其属性。\n- getProps：获取一个点或者一条边的属性。\n\n正是这一层的存在，使得 Storage 服务变成了真正的图存储，否则 Storage 服务只是一个 KV 存储服务。\n\n#### Consensus 层\n\nStorage 服务的中间层，实现了 Multi Group Raft，保证强一致性和高可用性。\n\n#### Store Engine 层\n\nStorage 服务的最底层，是一个单机版本地存储引擎，提供对本地数据的get、put、scan等操作。相关接口存储在KVStore.h和KVEngine.h文件，用户可以根据业务需求定制开发相关的本地存储插件。\n### KVStore\nNebula Graph 使用自行开发的 KVStore，而不是其他开源 KVStore，原因如下：\n- 需要高性能 KVStore。\n- 需要以库的形式提供，实现高效计算下推。对于强 Schema 的 Nebula Graph 来说，计算下推时如何提供 Schema 信息，是高效的关键。\n- 需要数据强一致性。\n\n基于上述原因，Nebula Graph 使用 RocksDB 作为本地存储引擎，实现了自己的 KVStore，有如下优势：\n- 对于多硬盘机器，Nebula Graph 只需配置多个不同的数据目录即可充分利用多硬盘的并发能力。\n- 由 Meta 服务统一管理所有 Storage 服务，可以根据所有分片的分布情况和状态，手动进行负载均衡。\n- 定制预写日志（WAL），每个分片都有自己的 WAL。\n- 支持多个图空间，不同图空间相互隔离，每个图空间可以设置自己的分片数和副本数。\n\n### 数据存储格式\n图存储的主要数据是点和边，Nebula Graph 将点和边的信息存储为 key，同时将点和边的属性信息存储在 value 中，以便更高效地使用属性过滤。\n\n#### 点数据存储格式\n相比 Nebula Graph 2.x 版本，3.x 版本的每个点多了一个不含 TagID 字段并且无 value 的 key，用于支持无 Tag 的点。\n\n![](../img/数据库/nebula/点数据储存格式.png)\n\n|字段|\t说明|\n| --- | --- |\n|Type|\tkey 类型。长度为 1 字节。|\n|PartID|\t数据分片编号。长度为 3 字节。此字段主要用于 Storage 负载均衡（balance）时方便根据前缀扫描整个分片的数据。|\n|VertexID|\t点 ID。当点 ID 类型为 int 时，长度为 8 字节；当点 ID 类型为 string 时，长度为创建图空间时指定的fixed_string长度。|\n|TagID|\t点关联的 Tag ID。长度为 4 字节。|\n|SerializedValue|\t序列化的 value，用于保存点的属性信息。|\n\n#### 边数据存储格式\n![](../img/数据库/nebula/边数据储存格式.png)\n\n|字段|\t说明|\n| --- | --- |\n|Type|\tkey 类型。长度为 1 字节。|\n|PartID|\t数据分片编号。长度为 3 字节。此字段主要用于 Storage 负载均衡（balance）时方便根据前缀扫描整个分片的数据。|\n|VertexID|\t点 ID。前一个VertexID在出边里表示起始点 ID，在入边里表示目的点 ID；后一个VertexID出边里表示目的点 ID，在入边里表示起始点 ID。|\n|Edge| type\t边的类型。大于 0 表示出边，小于 0 表示入边。长度为 4 字节。|\n|Rank|\t用来处理两点之间有多个同类型边的情况。用户可以根据自己的需求进行设置，例如存放交易时间、交易流水号等。长度为 8 字节，|\n|PlaceHolder|\t预留字段。长度为 1 字节。|\n|SerializedValue|\t序列化的 value，用于保存边的属性信息。|\n\n### 属性说明\nNebula Graph 使用强类型 Schema。\n\n对于点或边的属性信息，Nebula Graph 会将属性信息编码后按顺序存储。由于属性的长度是固定的，查询时可以根据偏移量快速查询。在解码之前，需要先从 Meta 服务中查询具体的 Schema 信息（并缓存）。同时为了支持在线变更 Schema，在编码属性时，会加入对应的 Schema 版本信息。\n### 数据分片\n由于超大规模关系网络的节点数量高达百亿到千亿，而边的数量更会高达万亿，即使仅存储点和边两者也远大于一般服务器的容量。因此需要有方法将图元素切割，并存储在不同逻辑分片（Partition）上。Nebula Graph 采用边分割的方式。\n\n![](../img/数据库/nebula/数据分片边分隔方式.png)\n\n#### 切边与存储放大\nNebula Graph 中逻辑上的一条边对应着硬盘上的两个键值对（key-value pair），在边的数量和属性较多时，存储放大现象较明显。边的存储方式如下图所示。\n\n![](../img/数据库/nebula/切边与放大储存.png)\n\n上图以最简单的两个点和一条边为例，起点 SrcVertex 通过边 EdgeA 连接目的点 DstVertex，形成路径(SrcVertex)-[EdgeA]->(DstVertex)。这两个点和一条边会以 6 个键值对的形式保存在存储层的两个不同分片，即 Partition x 和 Partition y 中，详细说明如下：\n- 点 SrcVertex 的键值保存在 Partition x 中。\n- 边 EdgeA 的第一份键值，这里用 EdgeA_Out 表示，与 SrcVertex 一同保存在 Partition x 中。key 的字段有 Type、PartID（x）、VID（Src，即点 SrcVertex 的 ID）、EdgeType（符号为正，代表边方向为出）、Rank（0）、VID（Dst，即点 DstVertex 的 ID）和 PlaceHolder。SerializedValue 即 Value，是序列化的边属性。\n- 点 DstVertex 的键值保存在 Partition y 中。\n- 边 EdgeA 的第二份键值，这里用 EdgeA_In 表示，与 DstVertex 一同保存在 Partition y 中。key 的字段有 Type、PartID（y）、VID（Dst，即点 DstVertex 的 ID）、EdgeType（符号为负，代表边方向为入）、Rank（0）、VID（Src，即点 SrcVertex 的 ID）和 PlaceHolder。SerializedValue 即 Value，是序列化的边属性，与 EdgeA_Out 中该部分的完全相同。\n\nEdgeA_Out 和 EdgeA_In 以方向相反的两条边的形式存在于存储层，二者组合成了逻辑上的一条边 EdgeA。EdgeA_Out 用于从起点开始的遍历请求，例如(a)-[]->()；EdgeA_In 用于指向目的点的遍历请求，或者说从目的点开始，沿着边的方向逆序进行的遍历请求，例如例如()-[]->(a)。\n\n如 EdgeA_Out 和 EdgeA_In 一样，Nebula Graph 冗余了存储每条边的信息，导致存储边所需的实际空间翻倍。因为边对应的 key 占用的硬盘空间较小，但 value 占用的空间与属性值的长度和数量成正比，所以，当边的属性值较大或数量较多时候，硬盘空间占用量会比较大。\n\n如果对边进行操作，为了保证两个键值对的最终一致性，可以开启 TOSS 功能，开启后，会先在正向边所在的分片进行操作，然后在反向边所在分片进行操作，最后返回结果。\n\n#### 分片算法\n\n分片策略采用静态 Hash 的方式，即对点 VID 进行取模操作，同一个点的所有 Tag、出边和入边信息都会存储到同一个分片，这种方式极大地提升了查询效率。\n\n> 创建图空间时需指定分片数量，分片数量设置后无法修改，建议设置时提前满足业务将来的扩容需求。\n\n多机集群部署时，分片分布在集群内的不同机器上。分片数量在 CREATE SPACE 语句中指定，此后不可更改。\n\n如果需要将某些点放置在相同的分片（例如在一台机器上），可以参考公式或代码。\n\n下文用简单代码说明 VID 和分片的关系。\n\n```cpp\n// 如果 ID 长度为 8，为了兼容 1.0，将数据类型视为 int64。\nuint64_t vid = 0;\n        if (id.size() == 8) {\n        memcpy(static_cast<void*>(&vid), id.data(), 8);\n        } else {\n        MurmurHash2 hash;\n        vid = hash(id.data());\n        }\n        PartitionID pId = vid % numParts + 1;\n\n```\n简单来说，上述代码是将一个固定的字符串进行哈希计算，转换成数据类型为 int64 的数字（int64 数字的哈希计算结果是数字本身），将数字取模，然后加 1，即：\n```text\npId = vid % numParts + 1;\n```\n示例的部分参数说明如下。\n\n| 参数 |\t说明 |\n| --- | --- |\n|%|\t取模运算。|\n|numParts|\tVID所在图空间的分片数，即 CREATE SPACE 语句中的partition_num值。|\n|pId|\tVID所在分片的 ID。|\n\n例如有 100 个分片，VID为 1、101 和 1001 的三个点将会存储在相同的分片。分片 ID 和机器地址之间的映射是随机的，所以不能假定任何两个分片位于同一台机器上。\n\n### Raft\n#### 关于 Raft 的简单介绍\n分布式系统中，同一份数据通常会有多个副本，这样即使少数副本发生故障，系统仍可正常运行。这就需要一定的技术手段来保证多个副本之间的一致性。\n\n基本原理：Raft 就是一种用于保证多副本一致性的协议。Raft 采用多个副本之间竞选的方式，赢得”超过半数”副本投票的（候选）副本成为 Leader，由 Leader 代表所有副本对外提供服务；其他 Follower 作为备份。当该 Leader 出现异常后（通信故障、运维命令等），其余 Follower 进行新一轮选举，投票出一个新的 Leader。Leader 和 Follower 之间通过心跳的方式相互探测是否存活，并以 Raft-wal 的方式写入硬盘，超过多个心跳仍无响应的副本会认为发生故障。\n> 因为 Raft-wal 需要定期写硬盘，如果硬盘写能力瓶颈会导致 Raft 心跳失败，导致重新发起选举。硬盘 IO 严重堵塞情况下，会导致长期无法选举出 Leader。\n\n读写流程：对于客户端的每个写入请求，Leader 会将该写入以 Raft-wal 的方式，将该条同步给其他 Follower，并只有在“超过半数”副本都成功收到 Raft-wal 后，才会返回客户端该写入成功。对于客户端的每个读取请求，都直接访问 Leader，而 Follower 并不参与读请求服务。\n\n故障流程：场景 1：考虑一个配置为单副本（图空间）的集群；如果系统只有一个副本时，其自身就是 Leader；如果其发生故障，系统将完全不可用。场景 2：考虑一个配置为 3 副本（图空间）的集群；如果系统有 3 个副本，其中一个副本是 Leader，其他 2 个副本是 Follower；即使原 Leader 发生故障，剩下两个副本仍可投票出一个新的 Leader（以及一个 Follower），此时系统仍可使用；但是当这 2 个副本中任一者再次发生故障后，由于投票人数不足，系统将完全不可用。\n\n> Raft 多副本的方式与 HDFS 多副本的方式是不同的，Raft 基于“多数派”投票，因此副本数量不能是偶数。\n\n#### Multi Group Raft\n由于 Storage 服务需要支持集群分布式架构，所以基于 Raft 协议实现了 Multi Group Raft，即每个分片的所有副本共同组成一个 Raft group，其中一个副本是 leader，其他副本是 follower，从而实现强一致性和高可用性。Raft 的部分实现如下。\n\n由于 Raft 日志不允许空洞，Nebula Graph 使用 Multi Group Raft 缓解此问题，分片数量较多时，可以有效提高 Nebula Graph 的性能。但是分片数量太多会增加开销，例如 Raft group 内部存储的状态信息、WAL 文件，或者负载过低时的批量操作。\n\n实现 Multi Group Raft 有 2 个关键点：\n\n- 共享 Transport 层\n\n每一个 Raft group 内部都需要向对应的 peer 发送消息，如果不能共享 Transport 层，会导致连接的开销巨大。\n\n- 共享线程池\n\n如果不共享一组线程池，会造成系统的线程数过多，导致大量的上下文切换开销。\n\n#### 批量（Batch）操作\nNebula Graph 中，每个分片都是串行写日志，为了提高吞吐，写日志时需要做批量操作，但是由于 Nebula Graph 利用 WAL 实现一些特殊功能，需要对批量操作进行分组，这是 Nebula Graph 的特色。\n\n例如无锁 CAS 操作需要之前的 WAL 全部提交后才能执行，如果一个批量写入的 WAL 里包含了 CAS 类型的 WAL，就需要拆分成粒度更小的几个组，还要保证这几组 WAL 串行提交。\n\n#### leader 切换（Transfer Leadership）\nleader 切换对于负载均衡至关重要，当把某个分片从一台机器迁移到另一台机器时，首先会检查分片是不是 leader，如果是的话，需要先切换 leader，数据迁移完毕之后，通常还要重新均衡 leader 分布。\n\n对于 leader 来说，提交 leader 切换命令时，就会放弃自己的 leader 身份，当 follower 收到 leader 切换命令时，就会发起选举。\n#### 成员变更\n为了避免脑裂，当一个 Raft group 的成员发生变化时，需要有一个中间状态，该状态下新旧 group 的多数派需要有重叠的部分，这样就防止了新的 group 或旧的 group 单方面做出决定。为了更加简化，Diego Ongaro 在自己的博士论文中提出每次只增减一个 peer 的方式，以保证新旧 group 的多数派总是有重叠。Nebula Graph 也采用了这个方式，只不过增加成员和移除成员的实现有所区别。具体实现方式请参见 Raft Part class 里 addPeer/removePeer 的实现。\n\n### 与 HDFS 的区别\n\nStorage 服务基于 Raft 协议实现的分布式架构，与 HDFS 的分布式架构有一些区别。例如：\n\n- Storage 服务本身通过 Raft 协议保证一致性，副本数量通常为奇数，方便进行选举 leader，而 HDFS 存储具体数据的 DataNode 需要通过 NameNode 保证一致性，对副本数量没有要求。\n- Storage 服务只有 leader 副本提供读写服务，而 HDFS 的所有副本都可以提供读写服务。\n- Storage 服务无法修改副本数量，只能在创建图空间时指定副本数量，而 HDFS 可以调整副本数量。\n- Storage 服务是直接访问文件系统，而 HDFS 的上层（例如 HBase）需要先访问 HDFS，再访问到文件系统，远程过程调用（RPC）次数更多。\n\n总而言之，Storage 服务更加轻量级，精简了一些功能，架构没有 HDFS 复杂，可以有效提高小块存储的读写性能。\n# 参考文章\n- https://docs.nebula-graph.com.cn/3.0.1/\n\n\n\n\n\n\n\n"
  },
  {
    "path": "数据库/SQL面试题.md",
    "content": "## 联合索引\n对列col1、列col2和列col3建一个联合索引\n\n联合索引 test_col1_col2_col3 实际建立了(col1)、(col1,col2)、(col,col2,col3)三个索引。\n```sql\nSELECT * FROM test WHERE col1=“1” AND clo2=“2” AND clo4=“4”\n```\n上面这个查询语句执行时会依照最左前缀匹配原则，检索时会使用索引(col1,col2)进行数据匹配。 \n\n对于联合索引(col1,col2,col3)，查询语句SELECT * FROM test WHERE col2=2;是否能够触发索引？\n```sql\nEXPLAIN SELECT col2 FROM test WHERE col2=2;\nEXPLAIN SELECT * FROM test WHERE col2=2;\nEXPLAIN SELECT col1 FROM test WHERE col1=1;\nEXPLAIN SELECT * FROM test WHERE col1=1;\n\n```\n观察上述两个explain结果中的type字段。查询中分别是：\n- type: index\n- type: ALL\n- type: ref\n- type: ref\n\n## 建表有什么优化的地方\n- 表需要主键\n- 不使用外键\n- 字段设置尽量不设为null\n- 尽量建联合索引\n- 查询频繁使用的字段记得加索引\n- 数据值区分不大，数据量小情况不建索引\n- 尽量满足三范式，但也是根据业务需求可以添加冗余字段\n- 尽量选择小的数据类型，数据类型选择上尽量tinyint(1字节)>smallint(2字节)>int(4字节)>bigint(8字节)，比如逻辑删除yn字段上（1代表可用，0代表）就可以选择tinyint（1字节）类型\n- 避免宽表，能拆分就拆分，一个表往往跟一个实体域对应，就像设计对象的时候一样，保持单一原则\n- 尽量避免使用text和blob，如果非使用不可，将类型为text和blob的字段在独立成一张新表，然后使用主键对应原表\n- 金额 decimal\n- 如果表的数量可以预测到非常大，最好在建表的时候，就进行分表，不至于一时间数据量非常大导致效率问题\n\n## 写SQL的时候要注意什么\n- 尽量不要使用select * 使用 select col \n- 使用count(*)或count(1)来统计行数来查询，使用count(列)的时候，需要在查看列中这个是否为null,不会统计此列为null的情况，而且mysql已经对count(*)做了优化\n- 联合索引要注意where条件满足最左匹配\n- 不要使用 where 函数(列名)= ss  这样,会使索引失效\n\n## char和varchar区别\n1. 存储方式：char是定长的，即它会为存储的每个字符串分配固定长度的空间，而varchar是变长的，它会根据存储的字符串长度动态分配空间。\n2. 存储空间：由于char是定长的，所以它在存储短字符串时可能会浪费空间，而varchar可以更加有效地利用空间，因为它只会分配实际需要的空间。\n3. 查询性能：由于char是定长的，所以在进行查询时速度可能会更快，因为MySQL可以直接计算偏移量，而不需要遍历整个字符串。而在varchar中，由于字符串长度不确定，需要进行遍历计算，因此查询速度可能会慢一些。\n4. 默认值：如果没有显式指定默认值，char的默认值为空格字符串，而varchar的默认值为NULL。\n\n## sql in 的最大长度\n\nOracle中，in语句中可放的最大参数个数是1000个。之前遇到超过1000的情况，可用如下语句，但如此多参数项目会低，可考虑用别的方式优化。\n\nmysql中，in语句中参数个数是不限制的。不过对整段sql语句的长度有了限制（max_allowed_packet）。默认是4M\n\n如果需要处理大量数据，可以考虑增加 max_allowed_packet 的值来提高查询效率。可以通过修改 MySQL 的配置文件或者在启动命令中指定该变量的值来进行修改。例如：\n```text\nmysql --max_allowed_packet=64M\n```\n请注意，在修改此变量的值之前，需要仔细考虑系统资源和性能等方面的因素，以避免对系统造成不必要的负担。\n\n## 聚集索引和非聚集索引的区别\n在数据库中，聚集索引和非聚集索引是两种不同的索引类型，它们的区别如下：\n\n1. 物理排序方式不同：聚集索引按照索引列的值来对整个表进行物理排序，而非聚集索引则是单独维护一个索引表，该表的数据项指向原始数据表中的对应记录。\n2. 数据存储方式不同：聚集索引的数据存储方式与表的物理存储方式相同，因此一个表只能有一个聚集索引；而非聚集索引则是单独存储在一个索引表中，一个表可以有多个非聚集索引。\n3. 访问方式不同：当使用聚集索引时，查询的结果集可以直接从聚集索引中获取，因为索引列的值与整个表的数据都存储在同一个物理块中；而使用非聚集索引时，查询的结果集需要通过两次索引查找来获取，第一次查找获取索引表中的行指针，第二次查找根据行指针查找对应的数据行。\n4. 插入操作效率不同：当使用聚集索引时，新插入的数据需要按照索引列的值来进行排序，因此插入效率较低；而使用非聚集索引时，新插入的数据可以直接插入索引表中，插入效率较高。\n\n总的来说，聚集索引适用于对经常访问的列进行查询，特别是范围查询和排序操作，而非聚集索引则适用于对少量记录进行单行查询的情况。\n\n## 那么哪些场景适合建唯一索引，哪些场景适合建普通索引\n建立唯一索引和建立普通索引的关键在于所需的数据一致性和查询效率。\n\n建立唯一索引适合以下场景：\n1. 需要确保某个字段的唯一性。例如，用户注册时需要保证用户名的唯一性，可以在该字段上建立唯一索引，避免重复用户名的注册。\n2. 经常使用该字段进行查询，且该字段数据量较小。由于唯一索引可以加速查询，当需要使用该字段作为查询条件时，建立唯一索引可以提高查询效率。当该字段的数据量较小时，唯一索引的查询效率更高。\n3. 不需要频繁进行修改操作。唯一索引会对插入、更新和删除操作产生额外的开销，因为数据库需要确保插入、更新或删除的数据在该字段上唯一。如果需要频繁进行修改操作，则可能会影响性能。\n\n建立普通索引适合以下场景：\n\n1. 不需要唯一性约束。如果某个字段不需要唯一性约束，可以建立普通索引，加速该字段的查询操作。\n2. 经常需要使用该字段进行查询，并且数据量较大。由于普通索引可以加速查询，当需要使用该字段作为查询条件时，建立普通索引可以提高查询效率。当该字段的数据量较大时，普通索引的查询效率更高。\n3. 需要频繁进行修改操作。相比于唯一索引，普通索引对插入、更新和删除操作的开销较小，因此建立普通索引对于需要频繁进行修改操作的场景更为适合。\n\n总的来说，建立唯一索引可以保证数据的一致性，但对于频繁修改操作的场景可能会影响性能。而建立普通索引则可以加速查询操作，但不保证数据的唯一性。在实际使用中，需要根据具体的业务场景和数据情况来综合考虑建立唯一索引还是普通索引。\n\n## b+树的有序性怎么保证\nB+树的有序性是通过B+树的插入、删除和分裂合并等操作来保证的。\n\n对于插入操作，B+树会按照插入的键值大小进行比较，找到插入位置并插入到合适的位置上，使得树仍然保持有序性。\n\n对于删除操作，B+树会找到要删除的键值所在的位置，并将其删除，然后通过调整B+树的结构来保持有序性。\n\n对于分裂操作，当某个节点达到了最大容量时，需要将其分裂成两个节点。分裂后，左节点包含原节点的一半键值，并且保持有序性，右节点包含剩余的键值，并且也保持有序性。\n\n对于合并操作，当某个节点的键值太少时，需要将其和相邻的节点合并。合并后，节点中的键值也保持有序性。\n\n通过这些操作，B+树保证了每个节点的键值都按照从小到大的顺序排列，同时树中的叶子节点也是按照从小到大的顺序排列的，从而保证了B+树的有序性。"
  },
  {
    "path": "数据库/count.md",
    "content": "\n* [分页count(*)的优化](#分页count的优化)\n    * [场景](#场景)\n    * [count(*)的工作原理](#count的工作原理)\n    * [解决方案](#解决方案)\n        * [show table status](#show-table-status)\n        * [缓存总数](#缓存总数)\n            * [redis缓存](#redis缓存)\n            * [数据库单独表来记录总数](#数据库单独表来记录总数)\n        * [做上/下一页](#做上下一页)\n        * [es统计](#es统计)\n* [总结](#总结)\n\n# 分页count(*)的优化\n## 场景\n现在有单表数量在千万级别，前端需要做分页展示，分页需要返回表总数，页总数等等，这里涉及到用count(*)统计全表数量\n\n但是这个过程总是很慢，耗时一般在4-5s以上\n## count(*)的工作原理\n在不同的MySQL引擎中，count(*)有不同的实现方式。\n\n- MyISAM引擎把一个表的总行数存在了磁盘上，因此执行count(*)的时候会直接返回这个数，效率很高；\n- InnoDB引擎执行count(*)的时候，需要把数据一行一行地从引擎里面读出来，然后累积计数，所以比较麻烦。\n\n虽然针对没有where条件的SQL，MySQL是有优化的，优化器会选择成本最小的辅助索引查询计数，但是有时候预估的成本会大于全表扫描\n\n而innodb为什么不也存一个总行数呢，由于多版本并发控制（MVCC）的原因，InnoDB表“应该返回多少行”也是不确定的\n\n## 解决方案\n### show table status\n如果你用过show table status 命令的话，就会发现这个命令的输出结果里面也有一个TABLE_ROWS用于显示这个表当前有多少行，这个命令执行挺快的，那这个TABLE_ROWS能代替count(*)吗？\n```sql\n-- mysql\nshow table status where Name = 'xxx';\n-- pgsql\nshow tables;\n```\n其实这个索引统计的值是通过采样来估算的。实际上，TABLE_ROWS就是从这个采样估算得来的，因此它也很不准。有多不准呢，官方文档说误差可能达到40%到50%。所以，show table status命令显示的行数也不能直接使用。\n\n优点：\n- 快\n\n缺点：\n- 估计的总数，并非精确\n### 缓存总数\n#### redis缓存\n这种方案，可以将总数统计放在Redis缓存中，每次新增、删除需要同步结果到Redis和数据库，之后查询总数直接查询Redis\n\n优点：\n- 快\n\n缺点：\n- 缓存和数据的一致性问题\n#### 数据库单独表来记录总数\n可以使用触发器，在数据库表insert、delete时将数据更新到一个专门记录数量的表中\n\n优点：\n- 快\n\n缺点：\n- 使用触发器可能会影响插入删除性能\n### 做上/下一页\n要求前端不分页，用户只能通过上一页，下一页来查询数据，后端通过表主键id来做查询\n```sql\nselect xx from table where id > #{lastId} and xxx = yyy limit 1 offset 1000\n```\n这里要求主键ID最好是自增的数字类型\n### es统计\n数据全保存在es，后续直接所有操作直接操作es即可\n\n优点：\n- 快\n\n缺点：\n- es和关系型数据库的区别，且大部分场景es也不适用\n\n# 总结\n综上，我的方案选择优先级\n1. 不做分页，做上下页滚动\n2. 返回大概总数，不需要精确\n3. 数据库单独表来存总数\n4. Redis\n5. 直接count(*)，可能会超时\n6. es\n"
  },
  {
    "path": "数据结构和算法/01-最长回文子串.md",
    "content": "# 子序列和子串\n\n现在有 abcd\n\nab bc cd 子串(连续字符)\nabd 子序列(非连续字符)\n\n双指针算法\n\n- 相向型双指针\n- 同向\n- 背向\n\n```text\nfor 起点 O(n)\n    for 终点 O(n)\n        判断中间的子串是否是回文串 O(n)\n\n```\n\nO(n^3)的解法\n\n```java\npublic class Solution {\n    public String longestPalindrome(String s) {\n        for (int len = s.length(); len >= 1; len--) {\n            for (int i = 0; i + len <= s.length(); i++) {\n                int l = i, r = i + len - 1;\n                while (l < r && s.charAt(l) == s.charAt(r)) {\n                    l++;\n                    r--;\n                }\n                if (l >= r) {\n                    return s.substring(i, i + len);\n                }\n            }\n        }\n        return \"\";\n    }\n}\n```\n以上代码问题：\n1. 没有异常处理\n2. 命名不规范\n3. 缩进不规范\n\n```java\n\n```"
  },
  {
    "path": "数据结构和算法/LFU.md",
    "content": "# LFU\n请你为 最不经常使用（LFU）缓存算法设计并实现数据结构。\n\n实现 LFUCache 类：\n\nLFUCache(int capacity) - 用数据结构的容量 capacity 初始化对象\nint get(int key) - 如果键存在于缓存中，则获取键的值，否则返回 -1。\nvoid put(int key, int value) - 如果键已存在，则变更其值；如果键不存在，请插入键值对。当缓存达到其容量时，则应该在插入新项之前，使最不经常使用的项无效。在此问题中，当存在平局（即两个或更多个键具有相同使用频率）时，应该去除 最近最久未使用 的键。\n注意「项的使用次数」就是自插入该项以来对其调用 get 和 put 函数的次数之和。使用次数会在对应项被移除后置为 0 。\n\n为了确定最不常使用的键，可以为缓存中的每个键维护一个 使用计数器 。使用计数最小的键是最久未使用的键。\n\n当一个键首次插入到缓存中时，它的使用计数器被设置为 1 (由于 put 操作)。对缓存中的键执行 get 或 put 操作，使用计数器的值将会递增。\n\n## 方法一：哈希表 + 平衡二叉树\n```java\nclass LFUCache {\n    // 缓存容量，时间戳\n    int capacity, time;\n    Map<Integer, Node> key_table;\n    TreeSet<Node> S;\n\n    public LFUCache(int capacity) {\n        this.capacity = capacity;\n        this.time = 0;\n        key_table = new HashMap<Integer, Node>();\n        S = new TreeSet<Node>();\n    }\n    \n    public int get(int key) {\n        if (capacity == 0) {\n            return -1;\n        }\n        // 如果哈希表中没有键 key，返回 -1\n        if (!key_table.containsKey(key)) {\n            return -1;\n        }\n        // 从哈希表中得到旧的缓存\n        Node cache = key_table.get(key);\n        // 从平衡二叉树中删除旧的缓存\n        S.remove(cache);\n        // 将旧缓存更新\n        cache.cnt += 1;\n        cache.time = ++time;\n        // 将新缓存重新放入哈希表和平衡二叉树中\n        S.add(cache);\n        key_table.put(key, cache);\n        return cache.value;\n    }\n    \n    public void put(int key, int value) {\n        if (capacity == 0) {\n            return;\n        }\n        if (!key_table.containsKey(key)) {\n            // 如果到达缓存容量上限\n            if (key_table.size() == capacity) {\n                // 从哈希表和平衡二叉树中删除最近最少使用的缓存\n                key_table.remove(S.first().key);\n                S.remove(S.first());\n            }\n            // 创建新的缓存\n            Node cache = new Node(1, ++time, key, value);\n            // 将新缓存放入哈希表和平衡二叉树中\n            key_table.put(key, cache);\n            S.add(cache);\n        } else {\n            // 这里和 get() 函数类似\n            Node cache = key_table.get(key);\n            S.remove(cache);\n            cache.cnt += 1;\n            cache.time = ++time;\n            cache.value = value;\n            S.add(cache);\n            key_table.put(key, cache);\n        }\n    }\n}\n\nclass Node implements Comparable<Node> {\n    int cnt, time, key, value;\n\n    Node(int cnt, int time, int key, int value) {\n        this.cnt = cnt;\n        this.time = time;\n        this.key = key;\n        this.value = value;\n    }\n\n    public boolean equals(Object anObject) {\n        if (this == anObject) {\n            return true;\n        }\n        if (anObject instanceof Node) {\n            Node rhs = (Node) anObject;\n            return this.cnt == rhs.cnt && this.time == rhs.time;\n        }\n        return false;\n    }\n\n    public int compareTo(Node rhs) {\n        return cnt == rhs.cnt ? time - rhs.time : cnt - rhs.cnt;\n    }\n\n    public int hashCode() {\n        return cnt * 1000000007 + time;\n    }\n}\n```\n## 方法二：双哈希表\n```java\nclass LFUCache {\n    int minfreq, capacity;\n    Map<Integer, Node> key_table;\n    Map<Integer, LinkedList<Node>> freq_table;\n\n    public LFUCache(int capacity) {\n        this.minfreq = 0;\n        this.capacity = capacity;\n        key_table = new HashMap<Integer, Node>();;\n        freq_table = new HashMap<Integer, LinkedList<Node>>();\n    }\n    \n    public int get(int key) {\n        if (capacity == 0) {\n            return -1;\n        }\n        if (!key_table.containsKey(key)) {\n            return -1;\n        }\n        Node node = key_table.get(key);\n        int val = node.val, freq = node.freq;\n        freq_table.get(freq).remove(node);\n        // 如果当前链表为空，我们需要在哈希表中删除，且更新minFreq\n        if (freq_table.get(freq).size() == 0) {\n            freq_table.remove(freq);\n            if (minfreq == freq) {\n                minfreq += 1;\n            }\n        }\n        // 插入到 freq + 1 中\n        LinkedList<Node> list = freq_table.getOrDefault(freq + 1, new LinkedList<Node>());\n        list.offerFirst(new Node(key, val, freq + 1));\n        freq_table.put(freq + 1, list);\n        key_table.put(key, freq_table.get(freq + 1).peekFirst());\n        return val;\n    }\n    \n    public void put(int key, int value) {\n        if (capacity == 0) {\n            return;\n        }\n        if (!key_table.containsKey(key)) {\n            // 缓存已满，需要进行删除操作\n            if (key_table.size() == capacity) {\n                // 通过 minFreq 拿到 freq_table[minFreq] 链表的末尾节点\n                Node node = freq_table.get(minfreq).peekLast();\n                key_table.remove(node.key);\n                freq_table.get(minfreq).pollLast();\n                if (freq_table.get(minfreq).size() == 0) {\n                    freq_table.remove(minfreq);\n                }\n            }\n            LinkedList<Node> list = freq_table.getOrDefault(1, new LinkedList<Node>());\n            list.offerFirst(new Node(key, value, 1));\n            freq_table.put(1, list);\n            key_table.put(key, freq_table.get(1).peekFirst());\n            minfreq = 1;\n        } else {\n            // 与 get 操作基本一致，除了需要更新缓存的值\n            Node node = key_table.get(key);\n            int freq = node.freq;\n            freq_table.get(freq).remove(node);\n            if (freq_table.get(freq).size() == 0) {\n                freq_table.remove(freq);\n                if (minfreq == freq) {\n                    minfreq += 1;\n                }\n            }\n            LinkedList<Node> list = freq_table.getOrDefault(freq + 1, new LinkedList<Node>());\n            list.offerFirst(new Node(key, value, freq + 1));\n            freq_table.put(freq + 1, list);\n            key_table.put(key, freq_table.get(freq + 1).peekFirst());\n        }\n    }\n}\n\nclass Node {\n    int key, val, freq;\n\n    Node(int key, int val, int freq) {\n        this.key = key;\n        this.val = val;\n        this.freq = freq;\n    }\n}\n```\n\n# 文章参考\n- https://leetcode-cn.com/problems/lfu-cache/solution/lfuhuan-cun-by-leetcode-solution/"
  },
  {
    "path": "数据结构和算法/LRU.md",
    "content": "\n# LRU\n\n运用你所掌握的数据结构，设计和实现一个LRU (最近最少使用) 缓存机制 。\n实现 LRUCache 类：\n\nLRUCache(int capacity) 以正整数作为容量capacity 初始化 LRU 缓存\nint get(int key) 如果关键字 key 存在于缓存中，则返回关键字的值，否则返回 -1 。\nvoid put(int key, int value)如果关键字已经存在，则变更其数据值；如果关键字不存在，则插入该组「关键字-值」。当缓存容量达到上限时，它应该在写入新数据之前删除最久未使用的数据值，从而为新的数据值留出空间。\n\n\n自定义双链表+hashmap\n```java\npublic class LRUCache {\n    class Node {\n        int key;\n        int value;\n        Node prev;\n        Node next;\n\n        public Node() {\n        }\n\n        public Node(int _key, int _value) {\n            key = _key;\n            value = _value;\n        }\n    }\n\n    private Map<Integer, Node> cache = new HashMap<Integer, Node>();\n    private int size;\n    private int capacity;\n    private Node head, tail;\n\n    public LRUCache(int capacity) {\n        this.size = 0;\n        this.capacity = capacity;\n        // 使用伪头部和伪尾部节点\n        head = new Node();\n        tail = new Node();\n        head.next = tail;\n        tail.prev = head;\n    }\n\n    public int get(int key) {\n        Node node = cache.get(key);\n        if (node == null) {\n            return -1;\n        }\n        // 如果 key 存在，先通过哈希表定位，再移到头部\n        moveToHead(node);\n        return node.value;\n    }\n\n    public void put(int key, int value) {\n        Node node = cache.get(key);\n        if (node == null) {\n            // 如果 key 不存在，创建一个新的节点\n            Node newNode = new Node(key, value);\n            // 添加进哈希表\n            cache.put(key, newNode);\n            // 添加至双向链表的头部\n            addToHead(newNode);\n            ++size;\n            if (size > capacity) {\n                // 如果超出容量，删除双向链表的尾部节点\n                Node tail = removeTail();\n                // 删除哈希表中对应的项\n                cache.remove(tail.key);\n                --size;\n            }\n        } else {\n            // 如果 key 存在，先通过哈希表定位，再修改 value，并移到头部\n            node.value = value;\n            moveToHead(node);\n        }\n    }\n\n    private void addToHead(Node node) {\n        node.prev = head;\n        node.next = head.next;\n        head.next.prev = node;\n        head.next = node;\n    }\n\n    private void removeNode(Node node) {\n        node.prev.next = node.next;\n        node.next.prev = node.prev;\n    }\n\n    private void moveToHead(Node node) {\n        removeNode(node);\n        addToHead(node);\n    }\n\n    private Node removeTail() {\n        Node res = tail.prev;\n        removeNode(res);\n        return res;\n    }\n}\n```\nLinkedHashMap实现\n```java\nclass LRUCache {\n    int capacity;\n    LinkedHashMap<Integer,Integer> map = new LinkedHashMap<>();\n    \n    public LRUCache(int capacity) {\n        this.capacity = capacity;\n    }\n    \n    public int get(int key) {\n        if(map.containsKey(key)){\n            make(key);\n            return map.get(key);\n        }\n        return -1;\n    }\n\n    public void make(int key){\n        int value =map.get(key);\n        map.remove(key);\n        map.put(key,value);\n    }\n    \n    public void put(int key, int value) {\n        if(map.containsKey(key)){\n            map.put(key,value);\n            make(key);\n            return;\n        }\n        if(map.size() == capacity){\n             int key1 = map.keySet().iterator().next();\n             map.remove(key1);\n        }\n        map.put(key,value);\n    }\n}\n\n```\n# 文章参考\n- https://leetcode-cn.com/problems/lru-cache/solution/lruhuan-cun-ji-zhi-by-leetcode-solution/\n"
  },
  {
    "path": "数据结构和算法/hotcode.md",
    "content": "# 常考的算法题\n\n## 3. 无重复字符的最长子串\n\nhttps://leetcode.cn/problems/longest-substring-without-repeating-characters/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个字符串 s ，请你找出其中不含有重复字符的 最长 子串 的长度。\n\n\n\n> 示例 1:\n> \n> 输入: s = \"abcabcbb\"\n> \n> 输出: 3\n> \n> 解释: 因为无重复字符的最长子串是 \"abc\"，所以其长度为 3。\n\n> 示例 2:\n> \n> 输入: s = \"bbbbb\"\n> \n> 输出: 1\n> \n> 解释: 因为无重复字符的最长子串是 \"b\"，所以其长度为 1。\n\n> 示例 3:\n> \n> 输入: s = \"pwwkew\"\n> \n> 输出: 3\n> \n> 解释: 因为无重复字符的最长子串是 \"wke\"，所以其长度为 3。\n\n请注意，你的答案必须是 子串 的长度，\"pwke\" 是一个子序列，不是子串。\n\n\n提示：\n\n- 0 <= s.length <= 5 * 10^4\n- s 由英文字母、数字、符号和空格组成\n\n```java\n/**\n * 计算给定字符串中最长无重复字符子串的长度\n *\n * @param s 输入的字符串，只包含小写字母\n * @return 返回最长无重复字符子串的长度\n */\nclass Solution {\n    /**\n     * 寻找字符串中最长无重复字符子串的长度\n     *\n     * 使用滑动窗口的方法，通过HashMap存储每个字符出现的次数\n     * 遍历字符串，当遇到重复字符时，更新起始位置并移除重复字符\n     * 记录当前子串的最大长度，并在遍历结束后返回结果\n     *\n     * @param s 输入的字符串\n     * @return 最长无重复字符子串的长度\n     */\n    public int lengthOfLongestSubstring(String s) {\n        // 创建一个HashMap用于存储字符及其出现次数\n        Map<Character, Integer> map = new HashMap<>();\n        int result = 0; // 初始化最长子串长度为0\n\n        // 双指针，start表示子串的起始位置，end表示遍历的当前位置\n        for (int start = 0, end = 0; end < s.length(); end++) {\n            // 获取当前遍历到的字符\n            char right = s.charAt(end);\n            // 更新HashMap中字符的出现次数\n            map.put(right, map.getOrDefault(right, 0) + 1);\n\n            // 当遇到重复字符时，更新子串起始位置并减少对应字符计数\n            while (map.get(right) > 1) {\n                char left = s.charAt(start);\n                map.put(left, map.get(left) - 1);\n                start++;\n            }\n\n            // 更新最长子串长度\n            result = Math.max(result, end - start + 1);\n        }\n\n        // 返回最长子串长度\n        return result;\n    }\n}\n```\n\n\n## 206. 反转链表\nhttps://leetcode.cn/problems/reverse-linked-list/description/\n\n给你单链表的头节点 head ，请你反转链表，并返回反转后的链表。\n \n\n>示例 1：\n>\n>\n>输入：head = [1,2,3,4,5]\n>\n>输出：[5,4,3,2,1]\n\n>示例 2：\n>\n>\n>输入：head = [1,2]\n>\n>输出：[2,1]\n\n>示例 3：\n>\n>输入：head = []\n>\n>输出：[]\n \n\n提示：\n\n- 链表中节点的数目范围是 [0, 5000]\n- -5000 <= Node.val <= 5000\n\n迭代法\n```java\npublic ListNode reverseList(ListNode head) {\n    // 前一个节点\n    ListNode prev = null;\n    // 当前节点 \n    ListNode curr = head;\n    // 当当前节点不为空时继续遍历\n    while (curr != null) { \n        // 暂存当前节点的下一个节点\n        ListNode nextTemp = curr.next; \n        // 当前节点指向前一个节点，实现反转\n        curr.next = prev; \n        // 将当前节点变成前一个节点，为下一轮循环做准备\n        prev = curr; \n        // 移动到下一个节点\n        curr = nextTemp; \n    }\n    return prev; // 返回反转后的头节点\n}\n```\n\n递归法\n```java\npublic ListNode reverseList(ListNode head) {\n    // 递归终止条件：链表为空或只有一个节点，直接返回原链表或头节点自身\n    if (head == null || head.next == null) { \n        return head;\n    }\n    // 递归调用反转下一个子链表\n    ListNode p = reverseList(head.next); \n    // 将当前节点的下一个节点的next指向当前节点，实现指针反转\n    head.next.next = head; \n    // 将当前节点的next置空，避免与原链表相连\n    head.next = null; \n    // 返回反转后的头节点（原本是尾节点）\n    return p; \n}\n```\n\n## 146. LRU缓存机制\nhttps://leetcode.cn/problems/lru-cache/description/\n\n请你设计并实现一个满足  LRU (最近最少使用) 缓存 约束的数据结构。\n\n实现 LRUCache 类：\n\nLRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存\n\nint get(int key) 如果关键字 key 存在于缓存中，则返回关键字的值，否则返回 -1 。\n\nvoid put(int key, int value) 如果关键字 key 已经存在，则变更其数据值 value ；如果不存在，则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ，则应该 逐出 最久未使用的关键字。\n\n函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。\n\n \n\n>示例：\n>\n>输入\n>\n>[\"LRUCache\", \"put\", \"put\", \"get\", \"put\", \"get\", \"put\", \"get\", \"get\", >\"get\"]\n>\n>[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]\n>\n>输出\n>\n>[null, null, null, 1, null, -1, null, -1, 3, 4]\n>\n>解释\n>- LRUCache lRUCache = new LRUCache(2);\n>- lRUCache.put(1, 1); // 缓存是 {1=1}\n>- lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}\n>- lRUCache.get(1);    // 返回 1\n>- lRUCache.put(3, 3); // 该操作会使得关键字 2 作废，缓存是 {1=1, 3=3}\n>- lRUCache.get(2);    // 返回 -1 (未找到)\n>- lRUCache.put(4, 4); // 该操作会使得关键字 1 作废，缓存是 {4=4, 3=3}\n>- lRUCache.get(1);    // 返回 -1 (未找到)\n>- lRUCache.get(3);    // 返回 3\n>- lRUCache.get(4);    // 返回 4\n \n\n提示：\n\n- 1 <= capacity <= 3000\n- 0 <= key <= 10000\n- 0 <= value <= 10^5\n- 最多调用 2 * 10^5 次 get 和 put\n\n```java\nclass LRUCache {\n\n    static class Node{\n        int key;\n        int value;\n        Node pre;\n        Node next;\n\n        public Node(){\n\n        }\n        public Node(int key,int value){\n            this.key = key;\n            this.value = value;\n        }\n    }\n\n    Map<Integer,Node> map = new HashMap<>();\n    Node head,tail;\n    int size;\n    int cap;\n\n    public LRUCache(int capacity) {\n        this.cap = capacity;\n        head = new Node();\n        tail = new Node();\n        head.next = tail;\n        tail.pre = head;\n    }\n    \n    public int get(int key) {\n        Node n = map.get(key);\n        if(n == null){\n            return -1;\n        }\n        moveToHead(n);\n        return n.value;\n    }\n    \n    public void put(int key, int value) {\n        Node n = map.get(key);\n        if(n == null){\n            Node node = new Node(key,value);\n            map.put(key,node);\n            addToHead(node);\n            if(++size > cap){\n                Node re = removeTail();\n                map.remove(re.key);\n            }\n        }else{\n            n.value = value;\n            moveToHead(n);\n        }\n    }\n\n    public Node remove(Node n){\n        n.next.pre = n.pre;\n        n.pre.next = n.next;\n        return n;\n    }\n\n    public Node removeTail(){\n        Node n = tail.pre;\n        remove(n);\n        return n;\n    }\n\n    public void addToHead(Node n){\n        n.next = head.next;\n        n.pre = head;\n        head.next.pre = n;\n        head.next = n;\n        \n    }\n\n    public void moveToHead(Node n){\n        remove(n);\n        addToHead(n);\n    }\n}\n\n/**\n * Your LRUCache object will be instantiated and called as such:\n * LRUCache obj = new LRUCache(capacity);\n * int param_1 = obj.get(key);\n * obj.put(key,value);\n */\n```\n\n## 215. 数组中的第K个最大元素\nhttps://leetcode.cn/problems/kth-largest-element-in-an-array/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定整数数组 nums 和整数 k，请返回数组中第 k 个最大的元素。\n\n请注意，你需要找的是数组排序后的第 k 个最大的元素，而不是第 k 个不同的元素。\n\n你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。\n\n \n\n>示例 1:\n>\n>输入: [3,2,1,5,6,4], k = 2\n>\n>输出: 5\n\n>示例 2:\n>\n>输入: [3,2,3,1,2,4,5,5,6], k = 4\n>\n>输出: 4\n \n\n提示：\n\n- 1 <= k <= nums.length <= 10^5\n- -10^4 <= nums[i] <= 10^4\n\n```java\nclass Solution {\n    /**\n     * 寻找第k大的元素。\n     * 使用随机化快速选择算法，避免了完整排序的需要，提高了效率。\n     * \n     * @param nums 输入的整数数组。\n     * @param k 指定的第k大的元素。\n     * @return 返回数组中第k大的元素。\n     */\n    public int findKthLargest(int[] nums, int k) {\n        // 调用处理函数，初始化搜索范围为整个数组，寻找第k大的元素位置\n        return handle(nums, 0, nums.length - 1, nums.length - k);\n    }\n\n    /**\n     * 处理函数，用于寻找第k大的元素。\n     * 通过随机化选择一个基准元素，并进行部分排序，逐步缩小搜索范围。\n     * \n     * @param nums 输入的整数数组。\n     * @param left 当前搜索范围的左边界。\n     * @param right 当前搜索范围的右边界。\n     * @param k 指定的第k大的元素。\n     * @return 返回数组中第k大的元素。\n     */\n    public int handle(int[] nums, int left, int right, int k) {\n        // 当左边界小于右边界时，继续搜索\n        while (left < right) {\n            // 通过随机选择基准元素，进行部分排序，返回基准元素的最终位置\n            int p = sort(nums, left, right);\n            // 如果基准元素位置等于k，搜索结束\n            if (p == k) {\n                break;\n            } else if (p < k) {\n                // 如果基准元素位置小于k，调整左边界\n                left = p + 1;\n            } else {\n                // 如果基准元素位置大于k，调整右边界\n                right = p - 1;\n            }\n        }\n        // 返回第k大的元素\n        return nums[k];\n    }\n\n    /**\n     * 随机化选择基准元素并进行部分排序。\n     * 使用随机选择的基准元素，将小于基准元素的元素放到基准元素的左边，大于基准元素的元素放到右边。\n     * \n     * @param nums 输入的整数数组。\n     * @param left 当前搜索范围的左边界。\n     * @param right 当前搜索范围的右边界。\n     * @return 返回基准元素的最终位置。\n     */\n    public int sort(int[] nums, int left, int right) {\n        // 随机选择一个基准元素的位置\n        int random = new Random().nextInt(right - left + 1) + left;\n        swap(nums, left, random);\n        int pd = nums[left];\n        int lt = left;\n        // 遍历数组，将小于基准元素的元素放到基准元素的左边\n        for (int i = left + 1; i <= right; i++) {\n            if (nums[i] < pd) {\n                swap(nums, i, ++lt);\n            }\n        }\n        // 将基准元素放到正确的位置\n        swap(nums, left, lt);\n        // 返回基准元素的最终位置\n        return lt;\n    }\n\n    /**\n     * 交换数组中两个位置的元素。\n     * \n     * @param nums 输入的整数数组。\n     * @param a 要交换的第一个位置。\n     * @param b 要交换的第二个位置。\n     */\n    public void swap(int[] nums, int a, int b) {\n        // 临时存储a位置的元素\n        int temp = nums[a];\n        // 将a位置的元素换成b位置的元素\n        nums[a] = nums[b];\n        // 将b位置的元素换成临时存储的元素\n        nums[b] = temp;\n    }\n}\n```\n\n## 25. K 个一组翻转链表\nhttps://leetcode.cn/problems/reverse-nodes-in-k-group/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你链表的头节点 head ，每 k 个节点一组进行翻转，请你返回修改后的链表。\n\nk 是一个正整数，它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍，那么请将最后剩余的节点保持原有顺序。\n\n你不能只是单纯的改变节点内部的值，而是需要实际进行节点交换。\n\n\n\n>示例 1：\n>\n>![k个一组翻转链表1.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2Fk%E4%B8%AA%E4%B8%80%E7%BB%84%E7%BF%BB%E8%BD%AC%E9%93%BE%E8%A1%A81.png)\n>\n>输入：head = [1,2,3,4,5], k = 2\n>\n>输出：[2,1,4,3,5]\n\n>示例 2：\n>\n>![k个一组翻转链表2.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2Fk%E4%B8%AA%E4%B8%80%E7%BB%84%E7%BF%BB%E8%BD%AC%E9%93%BE%E8%A1%A82.png)\n>\n>输入：head = [1,2,3,4,5], k = 3\n>\n>输出：[3,2,1,4,5]\n\n\n提示：\n- 链表中的节点数目为 n\n- 1 <= k <= n <= 5000\n- 0 <= Node.val <= 1000\n\n```java\n/**\n * Definition for singly-linked list.\n * public class ListNode {\n *     int val;\n *     ListNode next;\n *     ListNode() {}\n *     ListNode(int val) { this.val = val; }\n *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }\n * }\n */\nclass Solution {\n    /**\n     * 反转给定链表的连续k个节点，并返回修改后的链表头。\n     * \n     * @param head 链表的头节点\n     * @param k 需要反转的节点数\n     * @return 反转后的链表头节点\n     */\n    public ListNode reverseKGroup(ListNode head, int k) {\n        // 检查是否有足够的节点可以反转\n        ListNode tail = head;\n        for (int i = 0; i < k; i++) {\n            if (tail == null) {\n                return head;\n            }\n            tail = tail.next;\n        }\n        \n        // 反转前k个节点\n        ListNode newHead = reverse(head, tail);\n\n        // 继续反转剩余的链表，并将结果连接到已反转部分的末尾\n        head.next = reverseKGroup(tail, k);\n\n        return newHead;\n    }\n\n    /**\n     * 反转从head到tail之间的链表节点。\n     * \n     * @param head 需要反转部分的起始节点\n     * @param tail 需要反转部分的结束节点（不包括）\n     * @return 反转后的起始节点\n     */\n    private ListNode reverse(ListNode head, ListNode tail) {\n        ListNode pre = null, next = null;\n        // 反转链表\n        while (head != tail) {\n            next = head.next;\n            head.next = pre;\n            pre = head;\n            head = next;\n        }\n        return pre;\n    }\n}\n```\n\n## 15. 三数之和\n\nhttps://leetcode.cn/problems/3sum/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个整数数组 nums ，判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ，同时还满足 nums[i] + nums[j] + nums[k] == 0 。请\n\n你返回所有和为 0 且不重复的三元组。\n\n注意：答案中不可以包含重复的三元组。\n\n\n\n\n\n>示例 1：\n>\n>输入：nums = [-1,0,1,2,-1,-4]\n>\n>输出：[[-1,-1,2],[-1,0,1]]\n>\n>解释：\n>- nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。\n>- nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。\n>- nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。\n>- 不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。\n>\n>注意，输出的顺序和三元组的顺序并不重要。\n\n>示例 2：\n>\n>输入：nums = [0,1,1]\n>\n>输出：[]\n>\n>解释：唯一可能的三元组和不为 0 。\n\n>示例 3：\n>\n>输入：nums = [0,0,0]\n>\n>输出：[[0,0,0]]\n>\n>解释：唯一可能的三元组和为 0 。\n\n\n提示：\n\n- 3 <= nums.length <= 3000\n- -10^5 <= nums[i] <= 10^5\n\n```java\nclass Solution {\n    /**\n     * 寻找数组中所有不重复的三元组，这些三元组的和为零。\n     * \n     * @param nums 输入的整数数组\n     * @return 返回一个列表，包含所有和为零的不重复三元组\n     */\n    public List<List<Integer>> threeSum(int[] nums) {\n        List<List<Integer>> result = new ArrayList<>();\n        int length = nums.length;\n        \n        // 对数组进行排序，以便后续使用双指针技术\n        Arrays.sort(nums);\n\n        // 遍历数组，寻找所有可能的三元组\n        for (int i = 0; i < length; i++) {\n            // 如果当前元素大于零，说明后续不会有和为零的三元组，直接返回结果\n            if (nums[i] > 0) {\n                return result;\n            }\n            // 跳过重复的元素，以避免重复的三元组\n            if (i > 0 && nums[i] == nums[i - 1]) {\n                continue;\n            }\n            int left = i + 1; // 左指针\n            int right = length - 1; // 右指针\n            while (left < right) {\n                int sum = nums[i] + nums[left] + nums[right];\n                // 如果和小于零，左指针向右移动\n                if (sum < 0) {\n                    left++;\n                }\n                // 如果和大于零，右指针向左移动\n                else if (sum > 0) {\n                    right--;\n                }\n                // 如果和等于零，找到一个符合条件的三元组\n                else {\n                    result.add(Arrays.asList(nums[i], nums[left], nums[right]));\n                    // 移动右指针，跳过重复的元素\n                    while (left < right && nums[right] == nums[right - 1]) {\n                        right--;\n                    }\n                    while (left < right && nums[left] == nums[left + 1]) {\n                        left++;\n                    }\n                    // 左指针向右移动，右指针向左移动，继续寻找下一个可能的三元组\n                    right--;\n                    left++;\n                }\n            }\n        }\n        return result;\n    }\n}\n```\n\n## 53. 最大子数组和\n给你一个整数数组 nums ，请你找出一个具有最大和的连续子数组（子数组最少包含一个元素），返回其最大和。\n\n子数组\n是数组中的一个连续部分。\n\n \n\n>示例 1：\n>\n>输入：nums = [-2,1,-3,4,-1,2,1,-5,4]\n>\n>输出：6\n>\n>解释：连续子数组 [4,-1,2,1] 的和最大，为 6 。\n\n>示例 2：\n>\n>输入：nums = [1]\n>\n>输出：1\n\n>示例 3：\n>\n>输入：nums = [5,4,-1,7,8]\n>\n>输出：23\n \n\n提示：\n\n- 1 <= nums.length <= 10^5\n- -10^4 <= nums[i] <= 10^4\n\n```java\npublic class Solution {\n    /**\n     * 使用Kadane算法寻找最大子数组和。\n     *\n     * @param nums 包含整数的数组，表示输入的序列。\n     * @return 数组nums中最大连续子数组的和。\n     */\n    public int maxSubArray(int[] nums) {\n        // 初始化当前和以及最大和为数组的第一个元素\n        int curSum = nums[0];\n        int maxSum = nums[0];\n\n        // 从数组的第二个元素开始遍历\n        for (int i = 1; i < nums.length; i++) {\n            // 更新当前和，要么加上当前数字，要么如果当前数字更大则从当前数字开始新的子数组\n            curSum = Math.max(nums[i], curSum + nums[i]);\n\n            // 如果当前和大于最大和，则更新最大和\n            maxSum = Math.max(maxSum, curSum);\n        }\n\n        // 返回最大子数组和\n        return maxSum;\n    }\n}\n```\n\n## 补充题4. 手撕快速排序\nhttps://leetcode.cn/problems/sort-an-array/description/\n\n给你一个整数数组 nums，请你将该数组升序排列。\n\n \n\n>示例 1：\n>\n>输入：nums = [5,2,3,1]\n>\n>输出：[1,2,3,5]\n\n>示例 2：\n>\n>输入：nums = [5,1,1,2,0,0]\n>\n>输出：[0,0,1,1,2,5]\n \n\n提示：\n\n- 1 <= nums.length <= 5 * 10^4\n- -5 * 10^4 <= nums[i] <= 5 * 10^4\n\n```java\n/**\n * 使用随机化快速排序算法对数组进行排序。\n * 该类的主要功能是提供一个排序方法sortArray，它使用随机化选择的枢轴元素来提高排序效率。\n */\nclass Solution {\n    /**\n     * 对给定数组进行随机化快速排序。\n     * \n     * @param nums 输入的整数数组。\n     * @return 排序后的整数数组。\n     */\n    public int[] sortArray(int[] nums) {\n        // 通过递归调用help函数对数组进行随机化快速排序\n        help(nums, 0, nums.length - 1);\n        return nums;\n    }\n\n    /**\n     * 辅助函数，用于执行随机化快速排序。\n     * \n     * @param nums 输入的整数数组。\n     * @param l    当前子数组的左边界。\n     * @param r    当前子数组的右边界。\n     */\n    void help(int[] nums, int l, int r) {\n        // 当左边界小于右边界时，执行排序\n        if (l < r) {\n            // 通过调用part函数选择枢轴元素，并重新组织数组\n            int p = part(nums, l, r);\n            // 对枢轴元素左侧的子数组进行递归调用\n            help(nums, l, p - 1);\n            // 对枢轴元素右侧的子数组进行递归调用\n            help(nums, p + 1, r);\n        }\n    }\n\n    /**\n     * 选择一个随机的枢轴元素，并根据枢轴元素的值重新组织数组。\n     * \n     * @param nums 输入的整数数组。\n     * @param l    当前子数组的左边界。\n     * @param r    当前子数组的右边界。\n     * @return 枢轴元素的最终位置。\n     */\n    int part(int[] nums, int l, int r) {\n        // 随机选择一个位置作为枢轴元素\n        int random = new Random().nextInt(r - l + 1) + l;\n        swap(nums, l, random);\n        // 用枢轴元素的值初始化索引位置\n        int index = nums[l];\n        int lt = l;\n        // 遍历数组，将小于枢轴元素的值移到左侧\n        for (int i = l + 1; i <= r; i++) {\n            if (nums[i] < index) {\n                swap(nums, i, ++lt);\n            }\n        }\n        // 将枢轴元素放到正确的位置上\n        swap(nums, l, lt);\n        return lt;\n    }\n\n    /**\n     * 交换数组中两个位置的元素。\n     * \n     * @param nums 输入的整数数组。\n     * @param a    需要交换的第一个位置。\n     * @param b    需要交换的第二个位置。\n     */\n    void swap(int[] nums, int a, int b) {\n        // 临时存储nums[a]的值\n        int temp = nums[a];\n        // 将nums[a]的值替换为nums[b]的值\n        nums[a] = nums[b];\n        // 将nums[b]的值替换为temp的值，即原来nums[a]的值\n        nums[b] = temp;\n    }\n}\n```\n\n## 21. 合并两个有序链表\nhttps://leetcode.cn/problems/merge-two-sorted-lists/description/?envType=study-plan-v2&envId=top-interview-150\n\n将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。\n\n\n\n>示例 1：\n>\n>![alt text](../img/数据结构和算法/合并两个有序链表.png)\n>\n>输入：l1 = [1,2,4], l2 = [1,3,4]\n>\n>输出：[1,1,2,3,4,4]\n\n>示例 2：\n>\n>输入：l1 = [], l2 = []\n>\n>输出：[]\n\n>示例 3：\n>\n>输入：l1 = [], l2 = [0]\n>\n>输出：[0]\n\n\n提示：\n\n- 两个链表的节点数目范围是 [0, 50]\n- -100 <= Node.val <= 100\n- l1 和 l2 均按 非递减顺序 排列\n\n```java\n/**\n * Definition for singly-linked list.\n * public class ListNode {\n *     int val;\n *     ListNode next;\n *     ListNode() {}\n *     ListNode(int val) { this.val = val; }\n *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }\n * }\n */\nclass Solution {\n    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {\n        if(list1 == null){\n            return list2;\n        }\n        if(list2 == null){\n            return list1;\n        }\n        ListNode n = null;\n        if(list1.val > list2.val){\n            n = list2;\n            n.next = mergeTwoLists(list1,list2.next);\n        }else{\n            n = list1;\n            n.next = mergeTwoLists(list1.next,list2);\n        }\n        return n;\n    }\n}\n```\n\n## 1. 两数之和\nhttps://leetcode.cn/problems/two-sum/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个整数数组 nums 和一个整数目标值 target，请你在该数组中找出 和为目标值 target  的那 两个 整数，并返回它们的数组下标。\n\n你可以假设每种输入只会对应一个答案。但是，数组中同一个元素在答案里不能重复出现。\n\n你可以按任意顺序返回答案。\n\n\n\n>示例 1：\n>\n>输入：nums = [2,7,11,15], target = 9\n>\n>输出：[0,1]\n>\n>解释：因为 nums[0] + nums[1] == 9 ，返回 [0, 1] 。\n\n>示例 2：\n>\n>输入：nums = [3,2,4], target = 6\n>\n>输出：[1,2]\n\n>示例 3：\n>\n>输入：nums = [3,3], target = 6\n>\n>输出：[0,1]\n\n```java\nclass Solution {\n    public int[] twoSum(int[] nums, int target) {\n        Map<Integer,Integer> map =new HashMap<>();\n        for(int i=0;i<nums.length;i++){\n            if(map.containsKey(nums[i])){\n                return new int[]{i,map.get(nums[i])};\n            }else{\n                map.put(target-nums[i],i);\n            }\n        }\n        return new int[]{};\n    }\n}\n```\n\n## 5. 最长回文子串\nhttps://leetcode.cn/problems/longest-palindromic-substring/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个字符串 s，找到 s 中最长的 回文子串。\n\n \n\n>示例 1：\n>\n>输入：s = \"babad\"\n>\n>输出：\"bab\"\n>\n>解释：\"aba\" 同样是符合题意的答案。\n\n>示例 2：\n>\n>输入：s = \"cbbd\"\n>\n>输出：\"bb\"\n \n\n提示：\n\n- 1 <= s.length <= 1000\n- s 仅由数字和英文字母组成\n\n```java\n/**\n * Solution类用于解决寻找给定字符串中的最长回文子串的问题。\n */\nclass Solution {\n    /**\n     * 寻找并返回给定字符串s中的最长回文子串。\n     * \n     * @param s 输入的字符串\n     * @return 返回最长的回文子串\n     */\n    public String longestPalindrome(String s) {\n        String res = \"\"; // 初始化结果字符串为空\n        // 遍历字符串s中的每个字符，尝试以每个字符为中心扩展回文子串\n        for (int i = 0; i < s.length(); i++) {\n            // 尝试以当前字符为中心的奇数长度的回文子串\n            String s1 = check(s, i, i);\n            // 尝试以当前字符和下一个字符为中心的偶数长度的回文子串\n            String s2 = check(s, i, i + 1);\n\n            // 更新结果字符串为当前找到的较长的回文子串\n            res = res.length() > s1.length() ? res : s1;\n            res = res.length() > s2.length() ? res : s2;\n        }\n        return res; // 返回最终找到的最长回文子串\n    }\n\n    /**\n     * 检查并返回以left和right为中心的回文子串。\n     * \n     * @param s 输入的字符串\n     * @param left 回文子串的左边界\n     * @param right 回文子串的右边界\n     * @return 返回找到的回文子串\n     */\n    public String check(String s, int left, int right) {\n        while (left >= 0 && right < s.length()) {\n            // 如果左右字符相等，回文串长度加2，分别向左右两边扩展\n            if (s.charAt(left) == s.charAt(right)) {\n                left--;\n                right++;\n            } else {\n                // 如果左右字符不等，说明当前回文串以left和right为中心的尝试失败，退出循环\n                break;\n            }\n        }\n        // 返回找到的回文子串，注意需要从left+1开始，因为left和right在匹配过程中已经向中心移动了一步\n        return s.substring(left + 1, right);\n    }\n}\n```\n\n## 102. 二叉树的层序遍历\nhttps://leetcode.cn/problems/binary-tree-level-order-traversal/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你二叉树的根节点 root ，返回其节点值的 层序遍历 。 （即逐层地，从左到右访问所有节点）。\n\n \n\n>示例 1：\n>\n>![alt text](../img/数据结构和算法/二叉树的层序遍历.png)\n>\n>输入：root = [3,9,20,null,null,15,7]\n>\n>输出：[[3],[9,20],[15,7]]\n\n>示例 2：\n>\n>输入：root = [1]\n>\n>输出：[[1]]\n\n>示例 3：\n>\n>输入：root = []\n>\n>输出：[]\n \n\n提示：\n\n- 树中节点数目在范围 [0, 2000] 内\n- -1000 <= Node.val <= 1000\n\n```java\n/**\n * 二叉树的层序遍历实现。\n * \n * @param root 二叉树的根节点。\n * @return 层序遍历的结果，以二维列表表示。\n */\npublic List<List<Integer>> levelOrder(TreeNode root) {\n    List<List<Integer>> result = new ArrayList<>();\n    if (root == null) return result;\n\n    Queue<TreeNode> queue = new LinkedList<>();\n    queue.offer(root);\n\n    while (!queue.isEmpty()) {\n        int levelSize = queue.size();\n        List<Integer> currentLevel = new ArrayList<>();\n        // 遍历当前层的节点\n        for (int i = 0; i < levelSize; i++) {\n            TreeNode currentNode = queue.poll();\n            currentLevel.add(currentNode.val);\n\n            // 将当前节点的子节点加入队列，以便下一轮遍历\n            if (currentNode.left != null) {\n                queue.offer(currentNode.left);\n            }\n            if (currentNode.right != null) {\n                queue.offer(currentNode.right);\n            }\n        }\n        // 将当前层的节点值加入结果列表\n        result.add(currentLevel);\n    }\n    return result;\n}\n```\n\n## 33. 搜索旋转排序数组\nhttps://leetcode.cn/problems/search-in-rotated-sorted-array/description/?envType=study-plan-v2&envId=top-interview-150\n\n整数数组 nums 按升序排列，数组中的值 互不相同 。\n\n在传递给函数之前，nums 在预先未知的某个下标 k（0 <= k < nums.length）上进行了 旋转，使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]]（下标 从 0 开始 计数）。例如， [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。\n\n给你 旋转后 的数组 nums 和一个整数 target ，如果 nums 中存在这个目标值 target ，则返回它的下标，否则返回 -1 。\n\n你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。\n\n \n\n>示例 1：\n>\n>输入：nums = [4,5,6,7,0,1,2], target = 0\n>\n>输出：4\n\n>示例 2：\n>\n>输入：nums = [4,5,6,7,0,1,2], target = 3\n>\n>输出：-1\n\n>示例 3：\n>\n>输入：nums = [1], target = 0\n>\n>输出：-1\n \n\n提示：\n\n- 1 <= nums.length <= 5000\n- -10^4 <= nums[i] <= 10^4\n- nums 中的每个值都 独一无二\n- 题目数据保证 nums 在预先未知的某个下标上进行了旋转\n- -10^4 <= target <= 10^4\n\n```java\npublic int search(int[] nums, int target) {\n    if (nums == null || nums.length == 0) {\n        return -1;\n    }\n    \n    int left = 0, right = nums.length - 1;\n    \n    while (left <= right) {\n        int mid = left + (right - left) / 2;\n        if (nums[mid] == target) {\n            return mid;\n        }\n        \n        // 判断哪边是有序的\n        if (nums[left] <= nums[mid]) { // 左边有序\n            if (target >= nums[left] && target < nums[mid]) {\n                right = mid - 1;\n            } else {\n                left = mid + 1;\n            }\n        } else { // 右边有序\n            if (target > nums[mid] && target <= nums[right]) {\n                left = mid + 1;\n            } else {\n                right = mid - 1;\n            }\n        }\n    }\n    \n    return -1;\n}\n```\n\n## 200. 岛屿数量\nhttps://leetcode.cn/problems/number-of-islands/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个由 '1'（陆地）和 '0'（水）组成的的二维网格，请你计算网格中岛屿的数量。\n\n岛屿总是被水包围，并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。\n\n此外，你可以假设该网格的四条边均被水包围。\n\n \n\n>示例 1：\n>\n>输入：grid = [\n> \n>  [\"1\",\"1\",\"1\",\"1\",\"0\"],\n> \n>  [\"1\",\"1\",\"0\",\"1\",\"0\"],\n> \n>  [\"1\",\"1\",\"0\",\"0\",\"0\"],\n> \n>  [\"0\",\"0\",\"0\",\"0\",\"0\"]\n>\n>]\n>输出：1\n\n>示例 2：\n>\n>输入：grid = [\n>\n>  [\"1\",\"1\",\"0\",\"0\",\"0\"],\n>\n>  [\"1\",\"1\",\"0\",\"0\",\"0\"],\n>\n>  [\"0\",\"0\",\"1\",\"0\",\"0\"],\n>\n>  [\"0\",\"0\",\"0\",\"1\",\"1\"]\n>\n>]\n>\n>输出：3\n \n\n提示：\n\n- m == grid.length\n- n == grid[i].length\n- 1 <= m, n <= 300\n- grid[i][j] 的值为 '0' 或 '1'\n\n```java\nclass Solution {\n    private int[][] directions = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; // 上下左右四个方向\n\n    public int numIslands(char[][] grid) {\n        if (grid == null || grid.length == 0) {\n            return 0;\n        }\n        int m = grid.length;\n        int n = grid[0].length;\n        int islandCount = 0;\n\n        for (int i = 0; i < m; i++) {\n            for (int j = 0; j < n; j++) {\n                if (grid[i][j] == '1') {\n                    islandCount++; // 发现新的岛屿，计数加一\n                    dfs(grid, i, j); // 深度优先遍历，将与之相连的所有陆地标记为水\n                }\n            }\n        }\n        return islandCount;\n    }\n\n    private void dfs(char[][] grid, int row, int col) {\n        if (row < 0 || row >= grid.length || col < 0 || col >= grid[0].length || grid[row][col] == '0') {\n            return; // 越界或者已经是水了，直接返回\n        }\n        \n        grid[row][col] = '0'; // 将当前陆地标记为水，防止重复访问\n        \n        // 对当前陆地的上下左右四个方向进行深度优先搜索\n        for (int[] direction : directions) {\n            int newRow = row + direction[0];\n            int newCol = col + direction[1];\n            dfs(grid, newRow, newCol);\n        }\n    }\n}\n```\n\n## 20. 有效的括号\nhttps://leetcode.cn/problems/valid-parentheses/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个只包括 '('，')'，'{'，'}'，'['，']' 的字符串 s ，判断字符串是否有效。\n\n有效字符串需满足：\n\n- 左括号必须用相同类型的右括号闭合。\n- 左括号必须以正确的顺序闭合。\n- 每个右括号都有一个对应的相同类型的左括号。\n\n\n>示例 1：\n>\n>输入：s = \"()\"\n>\n>输出：true\n\n>示例 2：\n>\n>输入：s = \"()[]{}\"\n>\n>输出：true\n\n>示例 3：\n>\n>输入：s = \"(]\"\n>\n>输出：false\n\n\n提示：\n\n- 1 <= s.length <= 10^4\n- s 仅由括号 '()[]{}' 组成\n\n```java\nclass Solution {\n    /**\n     * 检查字符串s是否为有效的括号字符串。\n     * 有效的括号字符串是指括号完全匹配的字符串，即每个左括号都有对应的右括号，且左右括号的顺序正确。\n     *\n     * @param s 输入的字符串，包含括号字符\n     * @return 如果字符串是有效的括号字符串，返回true；否则返回false\n     */\n    public boolean isValid(String s) {\n        // 如果字符串为空或长度为奇数，则不可能是有效的括号字符串\n        if (s == null || s.length() % 2 != 0) {\n            return false;\n        }\n        \n        // 使用哈希表存储括号的对应关系\n        Map<Character, Character> map = new HashMap<>(){\n            {\n                put(')', '(');\n                put('}', '{');\n                put(']', '[');\n            }\n        };\n        \n        // 使用双端队列来处理括号匹配\n        Deque<Character> dq = new LinkedList<>();\n        \n        // 遍历字符串中的每个字符\n        for (int i = 0; i < s.length(); i++) {\n            char c = s.charAt(i);\n            \n            // 如果当前字符是右括号，则检查队列顶部的左括号是否匹配\n            if (map.containsKey(c)) {\n                // 如果队列为空或队列顶部的左括号与当前右括号不匹配，则字符串无效\n                if (dq.isEmpty() || map.get(c) != dq.peek()) {\n                    return false;\n                }\n                // 匹配成功，弹出队列顶部的左括号\n                dq.pop();\n            } else {\n                // 如果当前字符是左括号，则将其压入队列\n                dq.push(c);\n            }\n        }\n        \n        // 如果队列为空，则说明所有括号都正确匹配，字符串有效\n        return dq.isEmpty();\n    }\n}\n```\n\n## 121. 买卖股票的最佳时机\n\nhttps://leetcode.cn/problems/best-time-to-buy-and-sell-stock/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个数组 prices ，它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。\n\n你只能选择 某一天 买入这只股票，并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。\n\n返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润，返回 0 。\n\n>示例 1：\n>\n>输入：[7,1,5,3,6,4]\n>\n>输出：5\n>\n>解释：在第 2 天（股票价格 = 1）的时候买入，在第 5 天（股票价格 = 6）的时候卖出，最大利润 = 6-1 = 5 。\n>\n>注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格；同时，你不能在买入前卖出股票。\n\n>示例 2：\n>\n>输入：prices = [7,6,4,3,1]\n>\n>输出：0\n>\n>解释：在这种情况下, 没有交易完成, 所以最大利润为 0。\n\n提示：\n\n- 1 <= prices.length <= 10^5\n- 0 <= prices[i] <= 10^4\n\n\n\n```java\n/**\n * 计算股票中可获得的最大利润\n *\n * 给定一个整数数组 `prices`，其中第 `i` 个元素代表了第 `i` 天的股票价格，\n * 设计一个算法来找到最大的利润。你可以尽可能地完成更多的交易（多次买卖一支股票）。\n * 然而，你不能同时参与多笔交易（你必须在再次购买前出售掉之前的股票）。\n *\n * @param {number[]} prices - 一个整数数组，表示每天的股票价格\n * @return {number} - 返回最大可能的利润\n */\nclass Solution {\n    public int maxProfit(int[] prices) {\n        // 如果价格数组为空或长度为0，直接返回0，表示无法进行交易\n        if (prices == null || prices.length == 0) {\n            return 0;\n        }\n        \n        // 初始化最大利润为0\n        int max = 0;\n        // 初始化最小价格为数组第一个元素\n        int min = prices[0];\n\n        // 遍历价格数组\n        for (int i = 0; i < prices.length; i++) {\n            // 更新最大利润，取当前利润（当天价格减去最小价格）与已知最大利润的较大值\n            max = Math.max(max, prices[i] - min);\n            // 更新最小价格，取当前价格与已知最小价格的较小值\n            min = Math.min(min, prices[i]);\n        }\n\n        // 返回最大利润\n        return max;\n    }\n}\n```\n\n## 88. 合并两个有序数组\n\nhttps://leetcode.cn/problems/merge-sorted-array/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2，另有两个整数 m 和 n ，分别表示 nums1 和 nums2 中的元素数目。\n\n请你 合并 nums2 到 nums1 中，使合并后的数组同样按 非递减顺序 排列。\n\n注意：最终，合并后数组不应由函数返回，而是存储在数组 nums1 中。为了应对这种情况，nums1 的初始长度为 m + n，其中前 m\n个元素表示应合并的元素，后 n 个元素为 0 ，应忽略。nums2 的长度为 n 。\n\n>示例 1：\n>\n>输入：nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3\n>\n>输出：[1,2,2,3,5,6]\n>\n>解释：需要合并 [1,2,3] 和 [2,5,6] 。\n>合并结果是 [1,2,2,3,5,6] ，其中斜体加粗标注的为 nums1 中的元素。\n\n>示例 2：\n>\n>输入：nums1 = [1], m = 1, nums2 = [], n = 0\n>\n>输出：[1]\n>\n>解释：需要合并 [1] 和 [] 。\n>合并结果是 [1] 。\n\n>示例 3：\n>\n>输入：nums1 = [0], m = 0, nums2 = [1], n = 1\n>\n>输出：[1]\n>\n>解释：需要合并的数组是 [] 和 [1] 。\n>合并结果是 [1] 。\n>注意，因为 m = 0 ，所以 nums1 中没有元素。nums1 中仅存的 0 仅仅是为了确保合并结>果可以顺利存放到 nums1 中。\n\n提示：\n\n- nums1.length == m + n\n- nums2.length == n\n- 0 <= m, n <= 200\n- 1 <= m + n <= 200\n- -10^9 <= nums1[i], nums2[j] <= 10^9\n\n进阶：你可以设计实现一个时间复杂度为 O(m + n) 的算法解决此问题吗？\n\n\n\n```java\n/**\n * Solution类提供了一个方法来合并两个已排序的数组。\n * 它将nums2合并到nums1中，并确保合并后的nums1仍然是有序的。\n */\nclass Solution {\n    /**\n     * 合并两个已排序的数组。\n     * \n     * @param nums1 第一个有序数组，合并后的元素将存储在这个数组中。\n     * @param m nums1中原有的元素个数。\n     * @param nums2 第二个有序数组，它的元素将被合并到nums1中。\n     * @param n nums2中原有的元素个数。\n     * 注意：参数m和n是原数组中已有的元素数量，而不是数组的长度。\n     */\n    public void merge(int[] nums1, int m, int[] nums2, int n) {\n        /* 从nums1和nums2的末尾开始比较，并将较大的元素从后向前填充到nums1中 */\n        int i = m + n - 1;\n        while (n > 0) {\n            /* 当nums1中还有元素，且nums1的最后一个元素大于nums2的最后一个元素时 */\n            if (m > 0 && (nums1[m - 1] > nums2[n - 1])) {\n                /* 将nums1的最后一个元素放入nums1的当前位置 */\n                nums1[i] = nums1[m - 1];\n                /* 移动nums1的指针 */\n                m--;\n            } else {\n                /* 否则，将nums2的最后一个元素放入nums1的当前位置 */\n                nums1[i] = nums2[n - 1];\n                /* 移动nums2的指针 */\n                n--;\n            }\n            /* 移动nums1的指针到前一个位置，准备下一次填充 */\n            i--;\n        }\n    }\n}\n```\n\n## 141. 环形链表\nhttps://leetcode.cn/problems/linked-list-cycle/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个链表的头节点 head ，判断链表中是否有环。\n\n如果链表中有某个节点，可以通过连续跟踪 next 指针再次到达，则链表中存在环。 为了表示给定链表中的环，评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置（索引从 0 开始）。注意：pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。\n\n如果链表中存在环 ，则返回 true 。 否则，返回 false 。\n\n\n\n>示例 1：\n>\n> ![alt text](../img/数据结构和算法/环形链表1.png)\n>\n>输入：head = [3,2,0,-4], pos = 1\n>\n>输出：true\n>\n>解释：链表中有一个环，其尾部连接到第二个节点。\n\n>示例 2：\n>\n> ![alt text](../img/数据结构和算法/环形链表2.png)\n>\n>输入：head = [1,2], pos = 0\n>\n>输出：true\n>\n>解释：链表中有一个环，其尾部连接到第一个节点。\n\n>示例 3：\n>\n> ![alt text](../img/数据结构和算法/环形链表3.png)\n>\n>输入：head = [1], pos = -1\n>输出：false\n>解释：链表中没有环。\n\n\n提示：\n\n- 链表中节点的数目范围是 [0, 10^4]\n- -10^5 <= Node.val <= 10^5\n- pos 为 -1 或者链表中的一个 有效索引 。\n\n\n进阶：你能用 O(1)（即，常量）内存解决此问题吗？\n\n```java\n/**\n * Definition for singly-linked list.\n * class ListNode {\n *     int val;\n *     ListNode next;\n *     ListNode(int x) {\n *         val = x;\n *         next = null;\n *     }\n * }\n */\n/**\n * Solution类用于检测链表中是否存在环。\n */\npublic class Solution {\n    /**\n     * 检测链表中是否存在环。\n     * \n     * @param head 链表的头节点。\n     * @return 如果链表中存在环，则返回true；否则返回false。\n     */\n    public boolean hasCycle(ListNode head) {\n        // 如果链表为空或只有一个节点，则不存在环\n        if(head == null || head.next == null){\n            return false;\n        }\n        // 快指针初始化为头节点的下一个节点，慢指针初始化为头节点\n        ListNode fast = head.next;\n        ListNode slow = head;\n        // 当快慢指针不相等时，继续移动\n        while(fast != slow){\n            // 如果快指针到达链表末尾，则不存在环\n            if(fast == null || fast.next == null){\n                return false;\n            }\n            // 快指针每次移动两步，慢指针每次移动一步\n            fast = fast.next.next;\n            slow = slow.next;\n        }\n        // 如果存在环，快慢指针最终会相遇\n        return true;\n    }\n}\n```\n\n## 46. 全排列\nhttps://leetcode.cn/problems/permutations/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个不含重复数字的数组 nums ，返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。\n\n \n\n>示例 1：\n>\n>输入：nums = [1,2,3]\n>\n>输出：[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]\n\n>示例 2：\n>\n>输入：nums = [0,1]\n>\n>输出：[[0,1],[1,0]]\n\n>示例 3：\n>\n>输入：nums = [1]\n>\n>输出：[[1]]\n \n\n提示：\n\n- 1 <= nums.length <= 6\n- -10 <= nums[i] <= 10\n- nums 中的所有整数 互不相同\n\n```java\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class Solution {\n    public List<List<Integer>> permute(int[] nums) {\n        List<List<Integer>> results = new ArrayList<>();\n        backtrack(results, new ArrayList<>(), nums);\n        return results;\n    }\n\n    private void backtrack(List<List<Integer>> results, List<Integer> currentPermutation, int[] nums) {\n        // 基准情况：当当前排列的大小等于nums的长度时，将其添加到结果列表\n        if (currentPermutation.size() == nums.length) {\n            results.add(new ArrayList<>(currentPermutation));\n            return;\n        }\n\n        // 遍历nums中的每个元素\n        for (int num : nums) {\n            // 如果当前元素还没有被使用过（即不在currentPermutation中）\n            if (!currentPermutation.contains(num)) {\n                // 将当前元素添加到排列中\n                currentPermutation.add(num);\n                // 递归生成剩余元素的排列\n                backtrack(results, currentPermutation, nums);\n                // 回溯：移除最后一个添加的元素，尝试下一个可能的元素\n                currentPermutation.remove(currentPermutation.size() - 1);\n            }\n        }\n    }\n}\n```\n\n## 236. 二叉树的最近公共祖先\nhttps://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。\n\n百度百科中最近公共祖先的定义为：“对于有根树 T 的两个节点 p、q，最近公共祖先表示为一个节点 x，满足 x 是 p、q 的祖先且 x 的深度尽可能大（一个节点也可以是它自己的祖先）。”\n\n \n\n>示例 1：\n>\n>![alt text](../img/数据结构和算法/二叉树的最近公共祖先1.png)\n>\n>输入：root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1\n>\n>输出：3\n>\n>解释：节点 5 和节点 1 的最近公共祖先是节点 3 。\n\n>示例 2：\n>\n>![alt text](../img/数据结构和算法/二叉树的最近公共祖先2.png)\n>\n>输入：root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4\n>\n>输出：5\n>\n>解释：节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可\n以为节点本身。\n\n>示例 3：\n>\n>输入：root = [1,2], p = 1, q = 2\n>\n>输出：1\n \n\n提示：\n\n- 树中节点数目在范围 [2, 10^5] 内。\n- -10^9 <= Node.val <= 10^9\n- 所有 Node.val 互不相同 。\n- p != q\n- p 和 q 均存在于给定的二叉树中。\n\n```java\nclass TreeNode {\n    int val;\n    TreeNode left;\n    TreeNode right;\n    TreeNode(int x) { val = x; }\n}\n\npublic class Solution {\n    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {\n        // 如果根节点为空或者已经找到了p或q，则返回当前节点\n        if (root == null || root == p || root == q) {\n            return root;\n        }\n        \n        // 在左子树中查找p和q\n        TreeNode left = lowestCommonAncestor(root.left, p, q);\n        // 在右子树中查找p和q\n        TreeNode right = lowestCommonAncestor(root.right, p, q);\n        \n        // 如果p和q分别位于当前节点的左右子树中，则当前节点即为最近公共祖先\n        if (left != null && right != null) {\n            return root;\n        }\n        \n        // 如果p和q都在左子树中，则返回左子树查找到的结果\n        if (left != null) {\n            return left;\n        }\n        // 如果p和q都在右子树中，则返回右子树查找到的结果\n        return right;\n    }\n}\n```\n\n## 103. 二叉树的锯齿形层序遍历\nhttps://leetcode.cn/problems/binary-tree-zigzag-level-order-traversal/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你二叉树的根节点 root ，返回其节点值的 锯齿形层序遍历 。（即先从左往右，再从右往左进行下一层遍历，以此类推，层与层之间交替进行）。\n\n \n\n>示例 1：\n>\n>![alt text](../img/数据结构和算法/二叉树的锯齿形层序遍历.png)\n>\n>输入：root = [3,9,20,null,null,15,7]\n>\n>输出：[[3],[20,9],[15,7]]\n\n>示例 2：\n>\n>输入：root = [1]\n>\n>输出：[[1]]\n\n>示例 3：\n>\n>输入：root = []\n>\n>输出：[]\n \n\n提示：\n\n- 树中节点数目在范围 [0, 2000] 内\n- -100 <= Node.val <= 100\n\n```java\n/**\n * Definition for a binary tree node.\n * public class TreeNode {\n *     int val;\n *     TreeNode left;\n *     TreeNode right;\n *     TreeNode() {}\n *     TreeNode(int val) { this.val = val; }\n *     TreeNode(int val, TreeNode left, TreeNode right) {\n *         this.val = val;\n *         this.left = left;\n *         this.right = right;\n *     }\n * }\n */\n/**\n * 二叉树的锯齿形层序遍历实现。\n *\n * @param root 二叉树的根节点。\n * @return 锯齿形层序遍历的结果，以二维列表表示。\n */\npublic List<List<Integer>> zigzagLevelOrder(TreeNode root) {\n    List<List<Integer>> result = new ArrayList<>();\n    if (root == null) return result;\n\n    Queue<TreeNode> queue = new LinkedList<>();\n    queue.offer(root);\n    boolean leftToRight = true; // 标记当前层的遍历方向\n\n    while (!queue.isEmpty()) {\n        int levelSize = queue.size();\n        List<Integer> currentLevel = new ArrayList<>(levelSize);\n\n        for (int i = 0; i < levelSize; i++) {\n            TreeNode currentNode = queue.poll();\n            // 根据当前层的遍历方向决定添加顺序\n            if (leftToRight) {\n                currentLevel.add(currentNode.val);\n            } else {\n                currentLevel.add(0, currentNode.val); // 在列表头部添加元素以实现从右向左\n            }\n\n            // 添加子节点到队列中，以便下一层的遍历\n            if (currentNode.left != null) {\n                queue.offer(currentNode.left);\n            }\n            if (currentNode.right != null) {\n                queue.offer(currentNode.right);\n            }\n        }\n        // 完成一层后，切换遍历方向\n        leftToRight = !leftToRight;\n        result.add(currentLevel);\n    }\n    return result;\n}\n```\n\n## 92. 反转链表 II\nhttps://leetcode.cn/problems/reverse-linked-list-ii/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你单链表的头指针 head 和两个整数 left 和 right ，其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点，返回 反转后的链表 。\n\n\n>示例 1：\n>\n>![翻转链表2.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E7%BF%BB%E8%BD%AC%E9%93%BE%E8%A1%A82.png)\n>\n>输入：head = [1,2,3,4,5], left = 2, right = 4\n>\n>输出：[1,4,3,2,5]\n\n>示例 2：\n>\n>输入：head = [5], left = 1, right = 1\n>\n>输出：[5]\n\n\n提示：\n\n- 链表中节点数目为 n\n- 1 <= n <= 500\n- -500 <= Node.val <= 500\n- 1 <= left <= right <= n\n\n```java\n/**\n * Definition for singly-linked list.\n * public class ListNode {\n *     int val;\n *     ListNode next;\n *     ListNode() {}\n *     ListNode(int val) { this.val = val; }\n *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }\n * }\n */\n/**\n * 反转链表中指定范围的节点。\n * 通过创建一个虚拟节点dummy来简化链表操作，避免处理空链表的特殊情况。\n * 使用pre指针来追踪需要反转的区间之前的节点，cur指针用于反转操作，next指针用于临时存储cur的下一个节点。\n */\nclass Solution {\n    /**\n     * 反转链表中从left到right的节点。\n     * \n     * @param head 链表的头节点\n     * @param left 需要反转的起始位置\n     * @param right 需要反转的结束位置\n     * @return 反转后的链表头节点\n     */\n    public ListNode reverseBetween(ListNode head, int left, int right) {\n        // 创建一个虚拟节点作为反转操作的起点\n        ListNode dummy = new ListNode();\n        dummy.next = head;\n        // pre节点用于指向反转区间的前一个节点\n        ListNode pre = dummy;\n        // 将pre移动到需要反转区间的前一个位置\n        for(int i=1;i<left;i++){\n            pre = pre.next;\n        }\n        // cur节点指向需要反转区间的起始节点\n        ListNode cur = pre.next;\n        // next节点用于临时存储cur的下一个节点，以备反转使用\n        ListNode next = null;\n        // 依次反转从left到right的节点\n        for(int i = left;i<right;i++){\n            // 保存cur的下一个节点\n            next = cur.next;\n            // 将cur的next指向下一个的下一个节点，为反转做准备\n            cur.next = next.next;\n            // 将next的next指向pre的next，即原本cur的位置，完成一次节点反转\n            next.next = pre.next;\n            // 将pre的next指向next，完成反转区间的连接\n            pre.next = next;\n        }\n        // 返回反转后的链表头节点\n        return dummy.next;\n    }\n}\n```\n\n## 54. 螺旋矩阵\nhttps://leetcode.cn/problems/spiral-matrix/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个 m 行 n 列的矩阵 matrix ，请按照 顺时针螺旋顺序 ，返回矩阵中的所有元素。\n\n\n\n>示例 1：\n>\n> ![alt text](../img/数据结构和算法/螺旋矩阵1.png)\n>\n>输入：matrix = [[1,2,3],[4,5,6],[7,8,9]]\n>\n>输出：[1,2,3,6,9,8,7,4,5]\n\n>示例 2：\n>\n> ![alt text](../img/数据结构和算法/螺旋矩阵2.png)\n>\n>输入：matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]\n>\n>输出：[1,2,3,4,8,12,11,10,9,5,6,7]\n\n\n提示：\n\n- m == matrix.length\n- n == matrix[i].length\n- 1 <= m, n <= 10\n- -100 <= matrix[i][j] <= 100\n\n```java\n/**\n * 解决方案类，提供矩阵螺旋遍历的功能。\n */\nclass Solution {\n    /**\n     * 对给定的二维矩阵进行螺旋遍历，并返回遍历结果的顺序。\n     * \n     * @param matrix 二维整数数组，代表待遍历的矩阵。\n     * @return 返回一个整数列表，包含矩阵螺旋遍历的顺序。\n     */\n    public List<Integer> spiralOrder(int[][] matrix) {\n        List<Integer> res = new ArrayList<>();\n        if (matrix == null || matrix.length == 0) {\n            return res;\n        }\n        \n        int m = matrix.length;  // 矩阵的行数\n        int n = matrix[0].length;  // 矩阵的列数\n        int left = 0, right = n - 1, top = 0, bottom = m - 1;  // 定义矩阵的四个边界\n        int cnt = m * n;  // 计数器，用于记录剩余元素的数量\n        \n        while (cnt >= 1) {\n            // 从左到右遍历上边 这一步骤是遍历top上边界\n            for (int i = left; i <= right && cnt >= 1; i++) {\n                res.add(matrix[top][i]);\n                cnt--;\n            }\n            top++;\n            \n            // 从上到下遍历右边 这一步骤是遍历right右边界\n            for (int i = top; i <= bottom && cnt >= 1; i++) {\n                res.add(matrix[i][right]);\n                cnt--;\n            }\n            right--;\n            \n            // 从右到左遍历下边 这步骤是遍历bottom下边界\n            for (int i = right; i >= left && cnt >= 1; i--) {\n                res.add(matrix[bottom][i]);\n                cnt--;\n            }\n            bottom--;\n            \n            // 从下到上遍历左边 这步骤是遍历left左边界\n            for (int i = bottom; i >= top && cnt >= 1; i--) {\n                res.add(matrix[i][left]);\n                cnt--;\n            }\n            left++;\n        }\n        \n        return res;\n    }\n}\n```\n\n## 23. 合并 K 个升序链表\nhttps://leetcode.cn/problems/merge-k-sorted-lists/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个链表数组，每个链表都已经按升序排列。\n\n请你将所有链表合并到一个升序链表中，返回合并后的链表。\n\n \n\n>示例 1：\n>\n>输入：lists = [[1,4,5],[1,3,4],[2,6]]\n>\n>输出：[1,1,2,3,4,4,5,6]\n>\n>解释：链表数组如下：\n>\n>[\n>\n>  1->4->5,\n>\n>  1->3->4,\n>\n>  2->6\n>\n>]\n>\n>将它们合并到一个有序链表中得到。\n>\n>1->1->2->3->4->4->5->6\n\n>示例 2：\n>\n>输入：lists = []\n>\n>输出：[]\n\n>示例 3：\n>\n>输入：lists = [[]]\n>\n>输出：[]\n \n\n提示：\n\n- k == lists.length\n- 0 <= k <= 10^4\n- 0 <= lists[i].length <= 500\n- -10^4 <= lists[i][j] <= 10^4\n- lists[i] 按 升序 排列\n- lists[i].length 的总和不超过 10^4\n\n```java\n/**\n * Definition for singly-linked list.\n * public class ListNode {\n *     int val;\n *     ListNode next;\n *     ListNode() {}\n *     ListNode(int val) { this.val = val; }\n *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }\n * }\n */\n/**\n * 解决方案类，提供方法以合并多个已排序的链表为一个有序链表。\n */\nclass Solution {\n\n    /**\n     * 主要方法：合并K个已排序的链表。\n     * \n     * @param lists 一个数组，其中包含K个已排序的链表的头节点。\n     * @return 返回一个新的已排序链表的头节点，该链表由输入的所有链表合并而成。\n     */\n    public ListNode mergeKLists(ListNode[] lists) {\n        // 初始化结果链表为空\n        ListNode res = null;\n\n        // 遍历所有链表\n        for (int i = 0; i < lists.length; i++) {\n            // 将当前链表与结果链表合并\n            res = mergeTwo(res, lists[i]);\n        }\n\n        // 返回最终合并后的链表头节点\n        return res;\n    }\n\n    /**\n     * 辅助方法：合并两个已排序的链表。\n     * \n     * @param l1 第一个已排序链表的头节点。\n     * @param l2 第二个已排序链表的头节点。\n     * @return 返回合并后新链表的头节点。\n     */\n    public ListNode mergeTwo(ListNode l1, ListNode l2) {\n        // 如果任一链表为空，直接返回非空链表的头节点\n        if (l1 == null || l2 == null) {\n            return l1 == null ? l2 : l1;\n        }\n\n        // 创建哑节点作为新链表的起点\n        ListNode head = new ListNode(0);\n        ListNode tail = head; // tail用于追踪新链表的最后一个节点\n\n        // 分别用指针p1和p2遍历两个链表\n        ListNode p1 = l1, p2 = l2;\n        \n        // 当两个链表都未遍历完时\n        while (p1 != null && p2 != null) {\n            // 比较两个链表当前节点的值，将较小值的节点加入新链表\n            if (p1.val < p2.val) {\n                tail.next = p1;\n                p1 = p1.next;\n            } else {\n                tail.next = p2;\n                p2 = p2.next;\n            }\n            \n            // tail指针向后移动\n            tail = tail.next;\n        }\n        \n        // 合并剩余部分，哪个链表未结束就将其剩余部分接到新链表尾部\n        tail.next = p1 == null ? p2 : p1;\n\n        // 返回新链表的头节点（哑节点的下一个节点）\n        return head.next;\n    }\n}\n```\n\n## 300. 最长递增子序列\nhttps://leetcode.cn/problems/longest-increasing-subsequence/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个整数数组 nums ，找到其中最长严格递增子序列的长度。\n\n子序列 是由数组派生而来的序列，删除（或不删除）数组中的元素而不改变其余元素的顺序。例如，[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的\n子序列\n。\n\n \n>示例 1：\n>\n>输入：nums = [10,9,2,5,3,7,101,18]\n>\n>输出：4\n>\n>解释：最长递增子序列是 [2,3,7,101]，因此长度为 4 。\n\n>示例 2：\n>\n>输入：nums = [0,1,0,3,2,3]\n>\n>输出：4\n\n>示例 3：\n>\n>输入：nums = [7,7,7,7,7,7,7]\n>\n>输出：1\n \n\n提示：\n\n- 1 <= nums.length <= 2500\n- -10^4 <= nums[i] <= 10^4\n\n```java\npublic class Solution {\n    public int lengthOfLIS(int[] nums) {\n        // 创建一个与nums数组长度相同的dp数组，dp[i]表示以nums[i]结尾的最长递增子序列的长度\n        int[] dp = new int[nums.length];\n        int maxLength = 1;  // 最长递增子序列的初始长度至少为1（单个元素）\n        \n        for (int i = 0; i < nums.length; i++) {\n            dp[i] = 1;  // 每个元素自身可以形成一个长度为1的递增子序列\n            \n            // 遍历之前的元素，寻找可以连接到当前元素的更长递增子序列\n            for (int j = 0; j < i; j++) {\n                if (nums[i] > nums[j]) {  // 如果当前元素大于前面的元素，说明可以连接形成一个更长的递增子序列\n                    dp[i] = Math.max(dp[i], dp[j] + 1);  // 更新dp[i]的值\n                }\n            }\n            \n            // 更新最长递增子序列的长度\n            maxLength = Math.max(maxLength, dp[i]);\n        }\n        \n        return maxLength;  // 返回最长递增子序列的长度\n    }\n}\n```\n\n## 160. 相交链表\nhttps://leetcode.cn/problems/intersection-of-two-linked-lists/description/\n\n给你两个单链表的头节点 headA 和 headB ，请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点，返回 null 。\n\n图示两个链表在节点 c1 开始相交：\n\n![alt text](../img/数据结构和算法/相交链表1.png)\n\n题目数据 保证 整个链式结构中不存在环。\n\n注意，函数返回结果后，链表必须 保持其原始结构 。\n\n自定义评测：\n\n评测系统 的输入如下（你设计的程序 不适用 此输入）：\n\n- intersectVal - 相交的起始节点的值。如果不存在相交节点，这一值为 0\n- listA - 第一个链表\n- listB - 第二个链表\n- skipA - 在 listA 中（从头节点开始）跳到交叉节点的节点数\n- skipB - 在 listB 中（从头节点开始）跳到交叉节点的节点数\n\n评测系统将根据这些输入创建链式数据结构，并将两个头节点 headA 和 headB 传递给你的程序。如果程序能够正确返回相交节点，那么你的解决方案将被 视作正确答案 。\n\n \n\n>示例 1：\n>\n>![alt text](../img/数据结构和算法/相交链表2.png)\n>\n>输入：intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], >skipA = 2, skipB = 3\n>\n>输出：Intersected at '8'\n>\n>解释：相交节点的值为 8 （注意，如果两个链表相交则不能为 0）。\n>\n>从各自的表头开始算起，链表 A 为 [4,1,8,4,5]，链表 B 为 [5,6,1,8,4,5]。\n>\n>在 A 中，相交节点前有 2 个节点；在 B 中，相交节点前有 3 个节点。\n>\n>— 请注意相交节点的值不为 1，因为在链表 A 和链表 B 之中值为 1 的节点 (A 中第二个节点和 B 中第三个节点) 是不同的节点。换句话说，它们在内存中指向两个不同的位置，而链表 A 和链表 B 中值为 8 的节点 (A 中第三个节点，B 中第四个节点) 在内存中指向相同的位置。\n \n\n>示例 2：\n>\n>![alt text](../img/数据结构和算法/相交链表3.png)\n>\n>输入：intersectVal = 2, listA = [1,9,1,2,4], listB = [3,2,4], skipA = >3, skipB = 1\n>\n>输出：Intersected at '2'\n>\n>解释：相交节点的值为 2 （注意，如果两个链表相交则不能为 0）。\n>\n>从各自的表头开始算起，链表 A 为 [1,9,1,2,4]，链表 B 为 [3,2,4]。\n>\n>在 A 中，相交节点前有 3 个节点；在 B 中，相交节点前有 1 个节点。\n\n>示例 3：\n>\n>![alt text](../img/数据结构和算法/相交链表4.png)\n>\n>输入：intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, >skipB = 2\n>\n>输出：null\n>\n>解释：从各自的表头开始算起，链表 A 为 [2,6,4]，链表 B 为 [1,5]。\n>\n>由于这两个链表不相交，所以 intersectVal 必须为 0，而 skipA 和 skipB 可以是任意>值。\n>\n>这两个链表不相交，因此返回 null 。\n \n\n提示：\n\n- listA 中节点数目为 m\n- listB 中节点数目为 n\n- 1 <= m, n <= 3 * 10^4\n- 1 <= Node.val <= 10^5\n- 0 <= skipA <= m\n- 0 <= skipB <= n\n- 如果 listA 和 listB 没有交点，intersectVal 为 0\n- 如果 listA 和 listB 有交点，intersectVal == listA[skipA] == listB[skipB]\n\n```java\n/**\n * Definition for singly-linked list.\n * public class ListNode {\n *     int val;\n *     ListNode next;\n *     ListNode(int x) {\n *         val = x;\n *         next = null;\n *     }\n * }\n */\npublic class Solution {\n    /**\n     * 获取两个链表的交点节点。\n     * 本方法通过让两个指针分别从两个链表的头部开始遍历，当一个指针到达链表尾部时，\n     * 它会切换到另一个链表继续遍历。由于两个链表在某个点可能相交，因此，\n     * 当两个指针都遍历到链表的交点时，它们将指向同一个节点。\n     * 如果两个链表没有交点，那么两个指针最终都会到达另一个链表的尾部，此时返回null。\n     *\n     * @param headA 第一个链表的头部节点。\n     * @param headB 第二个链表的头部节点。\n     * @return 返回两个链表的交点节点，如果没有交点，则返回null。\n     */\n    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {\n        // 检查输入的链表头是否为空，如果有一个为空，则直接返回null，表示没有交点\n        if(headA == null || headB == null){\n            return null;\n        }\n        // 初始化两个指针pa和pb，分别指向两个链表的头部\n        ListNode pa = headA,pb = headB;\n        // 当两个指针指向不同的节点时，循环继续\n        while(pa != pb){\n            // 如果pa到达了链表A的尾部，将它重置为链表B的头部，继续遍历\n            pa = pa == null? headB:pa.next;\n            // 如果pb到达了链表B的尾部，将它重置为链表A的头部，继续遍历\n            pb = pb == null? headA:pb.next;\n        }\n        // 当循环结束时，pa和pb要么同时为null，要么指向相同的节点，即为链表的交点\n        // 如果没有交点，pa和pb都会遍历到另一个链表的尾部变为null\n        return pa;\n    }\n}\n```\n\n## 415. 字符串相加\nhttps://leetcode.cn/problems/add-strings/description/\n\n给定两个字符串形式的非负整数 num1 和num2 ，计算它们的和并同样以字符串形式返回。\n\n你不能使用任何內建的用于处理大整数的库（比如 BigInteger）， 也不能直接将输入的字符串转换为整数形式。\n\n \n\n>示例 1：\n>\n>输入：num1 = \"11\", num2 = \"123\"\n>\n>输出：\"134\"\n\n>示例 2：\n>\n>输入：num1 = \"456\", num2 = \"77\"\n>\n>输出：\"533\"\n\n>示例 3：\n>\n>输入：num1 = \"0\", num2 = \"0\"\n>\n>输出：\"0\"\n \n\n提示：\n\n- 1 <= num1.length, num2.length <= 10^4\n- num1 和num2 都只包含数字 0-9\n- num1 和num2 都不包含任何前导零\n\n```java\n/**\n * Solution类提供了一个方法来将两个非负整数字符串相加。\n * 它不使用整数转换，而是直接对字符串中的数字字符进行操作，模拟手动加法的过程。\n */\nclass Solution {\n    /**\n     * 将两个非负整数字符串相加。\n     * \n     * @param num1 第一个非负整数字符串。\n     * @param num2 第二个非负整数字符串。\n     * @return 返回两个字符串表示的整数相加的结果。\n     */\n    public String addStrings(String num1, String num2) {\n        // 初始化两个指针，分别指向两个字符串的末尾\n        int i = num1.length() - 1;\n        int j = num2.length() - 1;\n        \n        // 用于进位的变量\n        int add = 0;\n        // 使用StringBuilder来构建结果字符串\n        StringBuilder sb = new StringBuilder();\n        \n        // 循环直到两个字符串都遍历完且没有进位\n        while (i >= 0 || j >= 0 || add != 0) {\n            // 获取num1当前位的数字，如果已经遍历完则为0\n            int x = i >= 0 ? num1.charAt(i--) - '0' : 0;\n            // 获取num2当前位的数字，如果已经遍历完则为0\n            int y = j >= 0 ? num2.charAt(j--) - '0' : 0;\n            // 计算当前位的和，包括进位\n            int result = x + y + add;\n            // 将当前位的和添加到StringBuilder中\n            sb.append(result % 10);\n            // 更新进位值\n            add = result / 10;\n        }\n        \n        // 将StringBuilder中的数字反转并转换为字符串返回，模拟加法结果的读取顺序\n        return sb.reverse().toString();\n    }\n}\n```\n\n## 143. 重排链表\nhttps://leetcode.cn/problems/reorder-list/description/\n\n\n给定一个单链表 L 的头节点 head ，单链表 L 表示为：\n\nL0 → L1 → … → Ln - 1 → Ln\n\n请将其重新排列后变为：\n\nL0 → Ln → L1 → Ln - 1 → L2 → Ln - 2 → …\n\n不能只是单纯的改变节点内部的值，而是需要实际的进行节点交换。\n\n \n\n>示例 1：\n>\n>![alt text](../img/数据结构和算法/重排链表1.png)\n>\n>输入：head = [1,2,3,4]\n>\n>输出：[1,4,2,3]\n\n>示例 2：\n>\n>![alt text](../img/数据结构和算法/重排链表2.png)\n>\n>输入：head = [1,2,3,4,5]\n>\n>输出：[1,5,2,4,3]\n \n\n提示：\n\n- 链表的长度范围为 [1, 5 * 10^4]\n- 1 <= node.val <= 1000\n\n```java\n/**\n * Definition for singly-linked list.\n * public class ListNode {\n *     int val;\n *     ListNode next;\n *     ListNode() {}\n *     ListNode(int val) { this.val = val; }\n *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }\n * }\n */\n/**\n * 解决列表重排问题的类。\n * 通过找到中点，将列表分为两部分，反转后半部分，然后合并两部分来实现列表的重排。\n */\nclass Solution {\n    /**\n     * 重新排列给定的单链表。\n     * \n     * @param head 单链表的头节点。\n     */\n    public void reorderList(ListNode head) {\n        // 如果链表为空，则无需操作\n        if(head==null){\n            return;\n        }\n        // 找到链表的中点\n        ListNode mid = findMid(head);\n        // 初始化两个指针，分别指向前半部分和后半部分的起始位置\n        ListNode head1 = head;\n        ListNode head2 = mid.next;\n        // 反转后半部分链表\n        head2 = reverse(head2);\n        // 断开中点连接，为合并做准备\n        mid.next = null;\n        // 合并两个链表\n        mergeTwo(head1, head2);\n    }\n\n    /**\n     * 找到单链表的中点节点。\n     * 使用快慢指针法，快指针每次走两步，慢指针每次走一步，当快指针到达末尾时，慢指针位于中点。\n     * \n     * @param head 单链表的头节点。\n     * @return 单链表的中点节点。\n     */\n    public ListNode findMid(ListNode head){\n        ListNode fast = head.next;\n        ListNode slow = head;\n        while(fast != null && fast.next != null){\n            fast = fast.next.next;\n            slow = slow.next;\n        }\n        return slow;\n    }\n\n    /**\n     * 反转单链表。\n     * 通过迭代方式反转链表，每次将当前节点的next指向前一个节点，然后移动到下一个节点，直到链表结束。\n     * \n     * @param head 需要反转的链表的头节点。\n     * @return 反转后的链表的头节点。\n     */\n    public ListNode reverse(ListNode head){\n        ListNode pre = null;\n        ListNode next = null;\n        ListNode cur = head;\n        while(cur != null){\n            next = cur.next;\n            cur.next = pre;\n            pre = cur;\n            cur = next;\n        }\n        return pre;\n    }\n\n    /**\n     * 合并两个链表。\n     * 将第二个链表的节点依次插入到第一个链表的间隔节点中，形成重排后的链表。\n     * \n     * @param head1 第一个链表的头节点。\n     * @param head2 第二个链表的头节点。\n     */\n    public void mergeTwo(ListNode head1, ListNode head2){\n        ListNode l1 = null;\n        ListNode l2 = null;\n        while(head1 != null && head2 != null){\n            l1 = head1.next;\n            l2 = head2.next;\n            head1.next = head2;\n            head2.next = l1;\n            head1 = l1;\n            head2 = l2;\n        }\n    }\n}\n```\n\n## 42. 接雨水\n\nhttps://leetcode.cn/problems/trapping-rain-water/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定 n 个非负整数表示每个宽度为 1 的柱子的高度图，计算按此排列的柱子，下雨之后能接多少雨水。\n\n>示例 1：\n>\n>![接雨水.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E6%8E%A5%E9%9B%A8%E6%B0%B4.png)\n>\n>输入：height = [0,1,0,2,1,0,1,3,2,1,2,1]\n>\n>输出：6\n>\n>解释：上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图，在这种情况下，可以接 6 个单位的雨水（蓝色部分表示雨水）。\n\n>示例 2：\n>\n>输入：height = [4,2,0,3,2,5]\n>\n>输出：9\n\n提示：\n\n- n == height.length\n- 1 <= n <= 2 * 10^4\n- 0 <= height[i] <= 10^5\n\n\n\n```java\nclass Solution {\n      /**\n     * 计算一个数组中可以容纳的雨水总量。\n     * 该方法通过构建左右两个辅助数组，分别记录每个位置左侧和右侧的最大高度，以此来确定每个位置可以形成水槽的最大高度。\n     * 最终，通过遍历数组，计算每个位置可以容纳的水量，并累加得到总水量。\n     *\n     * @param height 表示每个位置的高度的数组。\n     * @return 返回可以容纳的雨水总量。\n     */\n    public int trap(int[] height) {\n        // 数组长度\n        int len = height.length;\n        // 初始化左侧最大高度数组\n        int[] left = new int[len];\n        // 初始化右侧最大高度数组\n        int[] right = new int[len];\n\n        // 从左向右遍历数组，填充左侧最大高度数组\n        for (int i = 1; i < len; i++) {\n            left[i] = Math.max(left[i - 1], height[i - 1]);\n        }\n        // 从右向左遍历数组，填充右侧最大高度数组\n        for (int i = len - 2; i >= 0; i--) {\n            right[i] = Math.max(right[i + 1], height[i + 1]);\n        }\n\n        // 初始化结果变量，用于累计可以容纳的雨水总量\n        int res = 0;\n\n        // 遍历数组，计算每个位置可以容纳的水量，并累加到结果变量中\n        for (int i = 0; i < len; i++) {\n            // 计算当前位置可以形成水槽的最大高度\n            int m = Math.min(left[i], right[i]);\n            // 累加实际可以容纳的水量，如果当前位置的高度大于等于最大高度，则该位置不能容纳水，累加0\n            res += Math.max(0, m - height[i]);\n        }\n\n        // 返回可以容纳的雨水总量\n        return res;\n    }\n}\n```\n单调栈解法，横向求解\n```java\nclass Solution {\n    public int trap(int[] height) {\n        //4,2,0,3,2,5\n        int ans = 0;\n        Deque<Integer> stack = new LinkedList<Integer>();\n        int n = height.length;\n        for (int i = 0; i < n; ++i) {\n            while (!stack.isEmpty() && height[i] > height[stack.peek()]) {\n                int top = stack.pop();\n                if (stack.isEmpty()) {\n                    break;\n                }\n                int left = stack.peek();\n                int currWidth = i - left - 1;\n                int currHeight = Math.min(height[left], height[i]) - height[top];\n                ans += currWidth * currHeight;\n            }\n            stack.push(i);\n        }\n        return ans;\n    }\n}\n```\n\n## 142. 环形链表 II\nhttps://leetcode.cn/problems/linked-list-cycle-ii/description/\n\n给定一个链表的头节点  head ，返回链表开始入环的第一个节点。 如果链表无环，则返回 null。\n\n如果链表中有某个节点，可以通过连续跟踪 next 指针再次到达，则链表中存在环。 为了表示给定链表中的环，评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置（索引从 0 开始）。如果 pos 是 -1，则在该链表中没有环。注意：pos 不作为参数进行传递，仅仅是为了标识链表的实际情况。\n\n不允许修改 链表。\n\n \n\n>示例 1：\n>\n>![alt text](../img/数据结构和算法/环形链表2-1.png)\n>\n>输入：head = [3,2,0,-4], pos = 1\n>\n>输出：返回索引为 1 的链表节点\n>\n>解释：链表中有一个环，其尾部连接到第二个节点。\n\n>示例 2：\n>\n>![alt text](../img/数据结构和算法/环形链表2-2.png)\n>\n>输入：head = [1,2], pos = 0\n>\n>输出：返回索引为 0 的链表节点\n>\n>解释：链表中有一个环，其尾部连接到第一个节点。\n\n>示例 3：\n>\n>![alt text](../img/数据结构和算法/环形链表2-3.png)\n>\n>输入：head = [1], pos = -1\n>\n>输出：返回 null\n>\n>解释：链表中没有环。\n \n\n提示：\n\n- 链表中节点的数目范围在范围 [0, 10^4] 内\n- -10^5 <= Node.val <= 10^5\n- pos 的值为 -1 或者链表中的一个有效索引\n\n\n```java\n/**\n * Definition for singly-linked list.\n * class ListNode {\n *     int val;\n *     ListNode next;\n *     ListNode(int x) {\n *         val = x;\n *         next = null;\n *     }\n * }\n */\npublic class Solution {\n    public ListNode detectCycle(ListNode head) {\n        if(head==null){\n            return null;\n        }\n        ListNode fast = head;\n        ListNode slow = head;\n        ListNode root = head;\n        while(fast!=null && fast.next !=null){\n            fast = fast.next.next;\n            slow = slow.next;\n            if(slow==fast){\n                //从链表的头部到环的入口的距离，等于从快慢指针相遇点到环的入口的距离\n                while(root!=slow){\n                    slow = slow.next;\n                    root = root.next;\n                }\n                return root;\n            }\n        }\n        return null;\n    }\n}\n```\n\n## 56. 合并区间\nhttps://leetcode.cn/problems/merge-intervals/description/?envType=study-plan-v2&envId=top-interview-150\n\n以数组 intervals 表示若干个区间的集合，其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间，并返回 一个不重叠的区间数组，该数组需恰好覆盖输入中的所有区间 。\n\n\n\n>示例 1：\n>\n>输入：intervals = [[1,3],[2,6],[8,10],[15,18]]\n>\n>输出：[[1,6],[8,10],[15,18]]\n>\n>解释：区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].\n\n>示例 2：\n>\n>输入：intervals = [[1,4],[4,5]]\n>\n>输出：[[1,5]]\n>\n>解释：区间 [1,4] 和 [4,5] 可被视为重叠区间。\n\n\n提示：\n\n- 1 <= intervals.length <= 10^4\n- intervals[i].length == 2\n- 0 <= starti <= endi <= 10^4\n\n```java\nclass Solution {\n       /**\n     * 合并区间。\n     * 给定一个区间列表，其中每个区间用包含两个整数的数组表示。返回一个新的区间列表，其中包含所有输入区间的所有元素，且新列表中的区间是按非递减顺序排列并且不相交的。\n     * \n     * @param intervals 输入的区间数组，每个区间由一对整数表示。\n     * @return 返回一个新的区间数组，包含所有输入区间的所有元素，且区间不相交。\n     */\n    public int[][] merge(int[][] intervals) {\n        // 按区间的起始位置对区间进行排序，确保后续合并时总是处理起始位置较小的区间\n        Arrays.sort(intervals,(o1,o2)->Integer.compare(o1[0],o2[0]));\n\n        // 使用ArrayList来动态存储合并后的区间，以便在合并过程中灵活添加或修改区间\n        List<int[]> res = new ArrayList<>();\n        \n        // 遍历排序后的区间数组\n        for(int i=0;i< intervals.length;i++){\n            // 提取当前区间的起始和结束位置\n            int left = intervals[i][0],right = intervals[i][1];\n            int size = res.size();\n            \n            // 如果当前结果列表为空，或当前区间与上一区间不相交，则直接添加当前区间到结果列表\n            if(size==0 || res.get(size-1)[1] < left){\n                res.add(new int[]{left,right});\n            }else{\n                // 如果当前区间与上一区间相交，则更新上一区间的结束位置为两区间结束位置的较大值\n                res.get(size-1)[1] = Math.max(res.get(size-1)[1],right); \n            }\n        }\n        \n        // 将结果列表转换为二维数组形式返回\n        return res.toArray(new int[res.size()][]);\n    }\n}\n```\n\n## 124. 二叉树中的最大路径和\nhttps://leetcode.cn/problems/binary-tree-maximum-path-sum/description/?envType=study-plan-v2&envId=top-interview-150\n\n二叉树中的 路径 被定义为一条节点序列，序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点，且不一定经过根节点。\n\n路径和 是路径中各节点值的总和。\n\n给你一个二叉树的根节点 root ，返回其 最大路径和 。\n\n \n\n>示例 1：\n>\n>![](../img/数据结构和算法/二叉树中的最大路径和1.png)\n>\n>\n>输入：root = [1,2,3]\n>\n>输出：6\n>\n>解释：最优路径是 2 -> 1 -> 3 ，路径和为 2 + 1 + 3 = 6\n\n>示例 2：\n>\n>![](../img/数据结构和算法/二叉树中的最大路径和2.png)\n>\n>输入：root = [-10,9,20,null,null,15,7]\n>\n>输出：42\n>\n>解释：最优路径是 15 -> 20 -> 7 ，路径和为 15 + 20 + 7 = 42\n \n\n提示：\n\n树中节点数目范围是 [1, 3 * 10^4]\n-1000 <= Node.val <= 1000\n\n\n```java\n/**\n * Definition for a binary tree node.\n * public class TreeNode {\n *     int val;\n *     TreeNode left;\n *     TreeNode right;\n *     TreeNode() {}\n *     TreeNode(int val) { this.val = val; }\n *     TreeNode(int val, TreeNode left, TreeNode right) {\n *         this.val = val;\n *         this.left = left;\n *         this.right = right;\n *     }\n * }\n */\n/**\n * 定义 Solution 类，用于计算二叉树中的最大路径和。\n */\nclass Solution {\n    int sum = Integer.MIN_VALUE; // 初始化变量以存储最大路径和\n\n    /**\n     * 计算二叉树中的最大路径和。\n     *\n     * @param root 二叉树的根节点\n     * @return 最大路径和\n     */\n    public int maxPathSum(TreeNode root) {\n        getMax(root); // 获取最大路径和\n        return sum; // 返回最大路径和\n    }\n\n    /**\n     * 辅助方法，找出从特定节点开始的最大路径和。\n     *\n     * @param root 当前二叉树节点\n     * @return 从当前节点开始的最大路径和\n     */\n    public int getMax(TreeNode root) {\n        if (root == null) {\n            return 0; // 如果当前节点为空，返回0\n        }\n\n        int left = Math.max(0, getMax(root.left)); // 计算左子树的最大路径和\n        int right = Math.max(0, getMax(root.right)); // 计算右子树的最大路径和\n\n        int n = left + right + root.val; // 计算包括当前节点在内的路径和\n\n        sum = Math.max(sum, n); // 更新全局最大路径和\n\n        return Math.max(left, right) + root.val; // 返回从当前节点开始的最大路径和\n    }\n}\n```\n\n## 72. 编辑距离\nhttps://leetcode.cn/problems/edit-distance/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你两个单词 word1 和 word2， 请返回将 word1 转换成 word2 所使用的最少操作数  。\n\n你可以对一个单词进行如下三种操作：\n\n- 插入一个字符\n- 删除一个字符\n- 替换一个字符\n \n\n>示例 1：\n>\n>输入：word1 = \"horse\", word2 = \"ros\"\n>\n>输出：3\n>\n>解释：\n>\n>horse -> rorse (将 'h' 替换为 'r')\n>\n>rorse -> rose (删除 'r')\n>\n>rose -> ros (删除 'e')\n\n>示例 2：\n>\n>输入：word1 = \"intention\", word2 = \"execution\"\n>\n>输出：5\n>\n>解释：\n>- intention -> inention (删除 't')\n>- inention -> enention (将 'i' 替换为 'e')\n>- enention -> exention (将 'n' 替换为 'x')\n>- exention -> exection (将 'n' 替换为 'c')\n>- exection -> execution (插入 'u')\n \n\n提示：\n\n- 0 <= word1.length, word2.length <= 500\n- word1 和 word2 由小写英文字母组成\n\n```java\n/**\n * Solution类提供了一个方法来计算两个字符串之间的最小编辑距离。\n * 编辑距离指的是将一个字符串转换成另一个字符串所需的最少操作次数，操作包括插入、删除和替换字符。\n */\nclass Solution {\n    /**\n     * 计算两个字符串之间的最小编辑距离。\n     * \n     * @param word1 第一个字符串\n     * @param word2 第二个字符串\n     * @return 两个字符串之间的最小编辑距离\n     */\n    public int minDistance(String word1, String word2) {\n        // 获取两个字符串的长度\n        int m = word1.length();\n        int n = word2.length();\n\n        // 如果其中一个字符串为空，则最小编辑距离为另一个字符串的长度\n        if(m*n==0){\n            return m+n;\n        }\n\n        // 初始化动态规划表格，dp[i][j]表示word1的前i个字符和word2的前j个字符之间的最小编辑距离\n        int[][] dp = new int[m+1][n+1];\n\n        // 初始化表格的第一列和第一行，分别表示将word1的前i个字符转换为空字符串和将空字符串转换为word2的前j个字符的最小编辑距离\n        for(int i=0;i<=m;i++){\n            dp[i][0] = i;\n        }\n        for(int i=0;i<=n;i++){\n            dp[0][i] = i;\n        }\n\n        // 填充动态规划表格，计算所有dp[i][j]的值\n        for(int i=1;i<=m;i++){\n            for(int j=1;j<=n;j++){\n                // 计算三种操作（插入、删除、替换）对应的编辑距离\n                //从 w1[i] -> w2[j] 增加或删除需要的步骤为 w1[i-1]+1 同理反向 w2[j] -> w1[i] 增加或删除需要的步骤为 w2[j-1]+1\n                int add = dp[i-1][j]+1;\n                int del = dp[i][j-1]+1;\n                // w1[i] -> w2[j] 如果当前字符相同，则不需要替换操作 如果不同则需要上一个最小步骤 +1\n                int mod = dp[i-1][j-1];\n                // 如果当前字符不相同，则需要进行替换操作，因此编辑距离加1\n                if (word1.charAt(i - 1) != word2.charAt(j - 1)) {\n                    mod += 1;\n                }\n                // 取三种操作中的最小值作为dp[i][j]的值\n                dp[i][j] = Math.min(add,Math.min(del,mod));   \n            }\n        }\n\n        // 返回整个表格的最后一个值，即为两个字符串之间的最小编辑距离\n        return dp[m][n];\n    }\n}\n```\n\n## 19. 删除链表的倒数第 N 个结点\nhttps://leetcode.cn/problems/remove-nth-node-from-end-of-list/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个链表，删除链表的倒数第 n 个结点，并且返回链表的头结点。\n\n\n\n>示例 1：\n>\n>![删除链表的倒数第n个节点.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E5%88%A0%E9%99%A4%E9%93%BE%E8%A1%A8%E7%9A%84%E5%80%92%E6%95%B0%E7%AC%ACn%E4%B8%AA%E8%8A%82%E7%82%B9.png)\n>\n>输入：head = [1,2,3,4,5], n = 2\n>\n>输出：[1,2,3,5]\n\n>示例 2：\n>\n>输入：head = [1], n = 1\n>\n>输出：[]\n\n>示例 3：\n>\n>输入：head = [1,2], n = 1\n>\n>输出：[1]\n\n\n提示：\n\n- 链表中结点的数目为 sz\n- 1 <= sz <= 30\n- 0 <= Node.val <= 100\n- 1 <= n <= sz\n\n```java\n/**\n * Definition for singly-linked list.\n * public class ListNode {\n *     int val;\n *     ListNode next;\n *     ListNode() {}\n *     ListNode(int val) { this.val = val; }\n *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }\n * }\n */\nclass Solution {\n    /**\n     * 从单链表的末尾开始数第n个节点，并移除该节点。\n     * \n     * @param head 链表的头节点\n     * @param n    需要移除的节点位置，从末尾开始计数\n     * @return 返回修改后的链表的头节点\n     */\n    public ListNode removeNthFromEnd(ListNode head, int n) {\n        // 使用哑节点dummy来简化链表操作，避免处理头节点移除的特殊情况\n        ListNode dummy = new ListNode(0, head);\n        ListNode fast = head;\n        ListNode slow = dummy;\n        \n        // 将fast指针向前移动n个位置，为后续同步移动做准备\n        for (int i = 0; i < n; ++i) {\n            fast = fast.next;\n        }\n\n        // 使用快慢指针法，fast每次移动一步，slow每次移动一步，直到fast到达末尾\n        while (fast != null) {\n            fast = fast.next;\n            slow = slow.next;\n        }\n        \n        // 此时slow指向需要移除的节点的前一个节点，将其next指向下一个节点的下一个节点，实现移除操作\n        slow.next = slow.next.next;\n        // 返回修改后的链表的头节点，即dummy的下一个节点\n        return dummy.next;\n    }\n}\n\n```\n\n## 93. 复原IP地址\nhttps://leetcode.cn/problems/restore-ip-addresses/description/\n\n有效 IP 地址 正好由四个整数（每个整数位于 0 到 255 之间组成，且不能含有前导 0），整数之间用 '.' 分隔。\n\n例如：\"0.1.2.201\" 和 \"192.168.1.1\" 是 有效 IP 地址，但是 \"0.011.255.245\"、\"192.168.1.312\" 和 \"192.168@1.1\" 是 无效 IP 地址。\n\n给定一个只包含数字的字符串 s ，用以表示一个 IP 地址，返回所有可能的有效 IP 地址，这些地址可以通过在 s 中插入 '.' 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。\n\n \n\n>示例 1：\n>\n>输入：s = \"25525511135\"\n>\n>输出：[\"255.255.11.135\",\"255.255.111.35\"]\n\n>示例 2：\n>\n>输入：s = \"0000\"\n>\n>输出：[\"0.0.0.0\"]\n\n>示例 3：\n>\n>输入：s = \"101023\"\n>\n>输出：[\"1.0.10.23\",\"1.0.102.3\",\"10.1.0.23\",\"10.10.2.3\",\"101.0.2.3\"]\n \n\n提示：\n\n- 1 <= s.length <= 20\n- s 仅由数字组成\n\n```java\n/**\n * 用于恢复给定字符串s的所有可能的IP地址。\n */\npublic class Solution {\n\n    // 存储所有可能的IP地址\n    List<String> res = new ArrayList<>();\n    // 用于构建和存储当前正在形成的IP地址\n    StringBuilder sb = new StringBuilder();\n\n    /**\n     * 深度优先搜索（DFS）函数，用于尝试所有可能的IP地址组合。\n     * @param cnt 当前已经构建的IP地址段数\n     * @param s 剩余的字符串，用于构建剩下的IP地址段\n     */\n    public void dfs(int cnt, String s) {\n        // 如果已经构建了4个IP地址段，或者没有剩余的字符串了\n        if (cnt == 4 || s.length() == 0) {\n            // 如果构建了4个IP地址段且没有剩余字符串，说明找到了一个有效的IP地址\n            if (cnt == 4 && s.length() == 0) {\n                res.add(sb.toString());\n            }\n            return;\n        }\n\n        // 尝试构建下一个IP地址段\n        for (int i = 0; i < 3 && i < s.length(); i++) {\n            // 如果当前段不是第一个段，但以0开头，则不符合IP地址规则，跳出循环\n            if (i != 0 && s.charAt(0) == '0') {\n                break;\n            }\n            // 提取当前尝试的IP地址段\n            String sub = s.substring(0, i + 1);\n            // 如果当前段是一个有效的IP地址段（值小于等于255）\n            if (Integer.valueOf(sub) <= 255) {\n                // 如果已经有一个或多个段，则在当前段前加一个点号\n                if (sb.length() > 0) {\n                    sub = \".\" + sub;\n                }\n                // 将当前段添加到正在构建的IP地址中\n                sb.append(sub);\n                // 递归调用DFS，尝试构建下一个IP地址段\n                dfs(cnt + 1, s.substring(i + 1));\n                // 回溯，移除刚刚添加的当前段，以便尝试其他可能的组合\n                sb.delete(sb.length() - sub.length(), sb.length());\n            }\n        }\n    }\n\n    /**\n     * 主函数，启动深度优先搜索以恢复所有可能的IP地址。\n     * @param s 输入的字符串，用于恢复IP地址\n     * @return 所有可能的IP地址列表\n     */\n    public List<String> restoreIpAddresses(String s) {\n        dfs(0, s);\n        return res;\n    }\n}\n```\n\n## 1143. 最长公共子序列\nhttps://leetcode.cn/problems/longest-common-subsequence/description/\n\n给定两个字符串 text1 和 text2，返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ，返回 0 。\n\n一个字符串的 子序列 是指这样一个新的字符串：它是由原字符串在不改变字符的相对顺序的情况下删除某些字符（也可以不删除任何字符）后组成的新字符串。\n\n例如，\"ace\" 是 \"abcde\" 的子序列，但 \"aec\" 不是 \"abcde\" 的子序列。\n两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。\n\n \n\n>示例 1：\n>\n>输入：text1 = \"abcde\", text2 = \"ace\" \n>\n>输出：3  \n>\n>解释：最长公共子序列是 \"ace\" ，它的长度为 3 。\n\n>示例 2：\n>\n>输入：text1 = \"abc\", text2 = \"abc\"\n>\n>输出：3\n>\n>解释：最长公共子序列是 \"abc\" ，它的长度为 3 。\n\n>示例 3：\n>\n>输入：text1 = \"abc\", text2 = \"def\"\n>\n>输出：0\n>\n>解释：两个字符串没有公共子序列，返回 0 。\n \n\n提示：\n\n- 1 <= text1.length, text2.length <= 1000\n- text1 和 text2 仅由小写英文字符组成。\n\n```java\nclass Solution {\n        /**\n     * 计算两个字符串的最长公共子序列的长度。\n     * 最长公共子序列是一个字符串中，同时出现在两个字符串中的最长的子序列（不一定连续）。\n     * \n     * @param text1 第一个字符串\n     * @param text2 第二个字符串\n     * @return 返回两个字符串的最长公共子序列的长度\n     */\n    public int longestCommonSubsequence(String text1, String text2) {\n        // 获取两个字符串的长度\n        int len1 = text1.length();\n        int len2 = text2.length();\n\n        // 初始化动态规划数组，dp[i][j]表示text1的前i个字符和text2的前j个字符的最长公共子序列的长度\n        int[][] dp = new int[len1+1][len2+1];\n\n        // 遍历两个字符串的所有字符\n        for(int i=1;i<=len1;i++){\n            for(int j=1;j<=len2;j++){\n                // 当两个字符相等时，当前的最长公共子序列长度是在前一个长度基础上加1\n                if(text1.charAt(i-1) == text2.charAt(j-1)){\n                    dp[i][j] = dp[i-1][j-1]+1;\n                }else{\n                    // 当两个字符不相等时，当前的最长公共子序列长度是取两种情况的较大值\n                    dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);\n                }\n            }\n        }\n\n        // 返回整个字符串的最长公共子序列的长度\n        return dp[len1][len2];\n    }\n}\n```\n\n## 94. 二叉树的中序遍历\nhttps://leetcode.cn/problems/binary-tree-inorder-traversal/\n\n给定一个二叉树的根节点 root ，返回 它的 中序 遍历 。\n\n \n\n>示例 1：\n>\n>![alt text](../img/数据结构和算法/二叉树的中序遍历.png)\n>\n>输入：root = [1,null,2,3]\n>\n>输出：[1,3,2]\n\n>示例 2：\n>\n>输入：root = []\n>\n>输出：[]\n\n>示例 3：\n>\n>\n>输入：root = [1]\n>\n>输出：[1]\n \n\n提示：\n\n- 树中节点数目在范围 [0, 100] 内\n- -100 <= Node.val <= 100\n\n```java\n/**\n * Definition for a binary tree node.\n * public class TreeNode {\n *     int val;\n *     TreeNode left;\n *     TreeNode right;\n *     TreeNode() {}\n *     TreeNode(int val) { this.val = val; }\n *     TreeNode(int val, TreeNode left, TreeNode right) {\n *         this.val = val;\n *         this.left = left;\n *         this.right = right;\n *     }\n * }\n */\nclass Solution {\n    /**\n     * 对二叉树进行中序遍历，并返回遍历结果的列表。\n     *\n     * @param root 二叉树的根节点，类型为 TreeNode\n     * @return 一个整数列表，包含了中序遍历的结果。如果树为空，返回空列表\n     */\n    public List<Integer> inorderTraversal(TreeNode root) {\n        // 初始化结果列表和双端队列\n        LinkedList<Integer> res = new LinkedList<>();\n        Deque<Object> dq = new LinkedList<>();\n\n        // 如果根节点为空，直接返回空列表\n        if (root == null) {\n            return res;\n        }\n\n        // 将根节点入队\n        dq.push(root);\n\n        // 当双端队列不为空时，进行遍历\n        while (!dq.isEmpty()) {\n            // 弹出队首元素\n            Object o = dq.pop();\n\n            // 如果元素是整数，将其添加到结果列表的末尾\n            if (o instanceof Integer) {\n                res.addLast((int) o);\n            } else {\n                // 否则，元素为 TreeNode，处理左右子节点\n                TreeNode n = (TreeNode) o;\n                // 先处理右子节点\n                if (n.right != null) {\n                    dq.push(n.right);\n                }\n                // 将节点值入队\n                dq.push(n.val);\n                // 再处理左子节点\n                if (n.left != null) {\n                    dq.push(n.left);\n                }\n            }\n        }\n\n        // 返回遍历结果\n        return res;\n    }\n}\n```\n\n## 82. 删除排序链表中的重复元素 II\nhttps://leetcode.cn/problems/remove-duplicates-from-sorted-list-ii/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个已排序的链表的头 head ， 删除原始链表中所有重复数字的节点，只留下不同的数字 。返回 已排序的链表 。\n\n\n\n>示例 1：\n>\n>![删除排序链表中的重复元素2-1.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E5%88%A0%E9%99%A4%E6%8E%92%E5%BA%8F%E9%93%BE%E8%A1%A8%E4%B8%AD%E7%9A%84%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A02-1.png)\n>\n>输入：head = [1,2,3,3,4,4,5]\n>\n>输出：[1,2,5]\n\n>示例 2：\n>\n> ![alt text](../img/数据结构和算法/删除排序链表中的重复元素2.png)\n>\n>输入：head = [1,1,1,2,3]\n>\n>输出：[2,3]\n\n\n提示：\n\n- 链表中节点数目在范围 [0, 300] 内\n- -100 <= Node.val <= 100\n- 题目数据保证链表已经按升序 排列\n\n```java\n/**\n * Definition for singly-linked list.\n * public class ListNode {\n *     int val;\n *     ListNode next;\n *     ListNode() {}\n *     ListNode(int val) { this.val = val; }\n *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }\n * }\n */\n/**\n * Solution类用于解决删除链表中所有重复元素的问题，使每个元素只出现一次。\n */\nclass Solution {\n    /**\n     * 删除链表中所有重复元素。\n     * \n     * @param head 链表的头节点。\n     * @return 返回删除重复元素后的链表头节点。\n     */\n    public ListNode deleteDuplicates(ListNode head) {\n        // 如果链表为空，则直接返回null。\n        if(head == null){\n            return null;\n        }\n        // 使用哑节点dummy来简化链表操作，避免处理头节点的特殊情况。\n        ListNode dummy = new ListNode();\n        dummy.next = head;\n        ListNode pre = dummy;\n        ListNode cur = head;\n\n        // 遍历链表，直到cur或cur的下一个节点为空。\n        while(cur!= null && cur.next != null){\n            // 如果当前节点的值和下一个节点的值相同，说明有重复。\n            if(cur.val == cur.next.val){\n                int v = cur.val;\n                // 移动cur直到不再有重复的值，这一步是为了找到重复序列的末尾。\n                while(cur!= null && cur.val == v){\n                    cur = cur.next;\n                }\n                // 将pre的下一个节点指向cur，从而跳过重复的序列。\n                pre.next = cur;\n            }else{\n                // 如果当前节点和下一个节点的值不同，更新pre和cur的位置。\n                pre = cur;\n                cur = cur.next;\n            }\n        }\n        // 返回更新后的链表头节点，即dummy的下一个节点。\n        return dummy.next;\n    }\n}\n```\n\n## 704. 二分查找\nhttps://leetcode.cn/problems/binary-search/description/\n\n给定一个 n 个元素有序的（升序）整型数组 nums 和一个目标值 target  ，写一个函数搜索 nums 中的 target，如果目标值存在返回下标，否则返回 -1。\n\n\n>示例 1:\n>\n>输入: nums = [-1,0,3,5,9,12], target = 9\n>\n>输出: 4\n>\n>解释: 9 出现在 nums 中并且下标为 4\n\n>示例 2:\n>\n>输入: nums = [-1,0,3,5,9,12], target = 2\n>\n>输出: -1\n>\n>解释: 2 不存在 nums 中因此返回 -1\n \n\n提示：\n\n- 你可以假设 nums 中的所有元素是不重复的。\n- n 将在 [1, 10000]之间。\n- nums 的每个元素都将在 [-9999, 9999]之间。\n\n```java\n/**\n * 在排序数组中查找特定目标值的索引。\n * 该类提供了一个方法来执行二分查找，以高效地找到目标值的索引。\n */\nclass Solution {\n    /**\n     * 在排序数组中搜索目标值。\n     * 使用二分查找算法来提高搜索效率。\n     * \n     * @param nums 排序后的整数数组，不为空且至少包含一个元素。\n     * @param target 需要查找的目标整数。\n     * @return 目标值在数组中的索引；如果不存在，则返回-1。\n     */\n    public int search(int[] nums, int target) {\n        /* 初始化左右指针 */\n        int left = 0, right = nums.length - 1;\n        /* 当左指针不大于右指针时，执行循环 */\n        while (left <= right) {\n            /* 计算中间索引，避免整数溢出 */\n            int mid = left + (right - left) / 2;\n            /* 如果中间值等于目标值，返回中间索引 */\n            if (nums[mid] == target) {\n                return mid;\n            }\n            /* 如果中间值小于目标值，移动左指针到中间索引的右边 */\n            else if (nums[mid] < target) {\n                left = mid + 1;\n            }\n            /* 如果中间值大于目标值，移动右指针到中间索引的左边 */\n            else {\n                right = mid - 1;\n            }\n        }\n        /* 如果没有找到目标值，返回-1 */\n        return -1;\n    }\n}\n```\n\n## 199. 二叉树的右视图\nhttps://leetcode.cn/problems/binary-tree-right-side-view/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个二叉树的 根节点 root，想象自己站在它的右侧，按照从顶部到底部的顺序，返回从右侧所能看到的节点值。\n\n \n\n>示例 1:\n>\n>![alt text](../img/数据结构和算法/二叉树的右视图.png)\n>\n>输入: [1,2,3,null,5,null,4]\n>\n>输出: [1,3,4]\n\n>示例 2:\n>\n>输入: [1,null,3]\n>\n>输出: [1,3]\n\n>示例 3:\n>\n>输入: []\n>\n>输出: []\n \n\n提示:\n\n- 二叉树的节点个数的范围是 [0,100]\n- -100 <= Node.val <= 100 \n\n```java\nimport java.util.*;\n\nclass TreeNode {\n    int val;\n    TreeNode left;\n    TreeNode right;\n    TreeNode(int x) { val = x; }\n}\n\npublic class Solution {\n    public List<Integer> rightSideView(TreeNode root) {\n        List<Integer> result = new ArrayList<>();\n        if (root == null) return result;\n\n        Queue<TreeNode> queue = new LinkedList<>();\n        queue.offer(root);\n\n        while (!queue.isEmpty()) {\n            int levelSize = queue.size();\n            for (int i = 0; i < levelSize; i++) {\n                TreeNode current = queue.poll();\n                // 每一层的最后一个节点添加到结果列表\n                if (i == levelSize - 1) {\n                    result.add(current.val);\n                }\n                \n                // 将当前节点的左右子节点加入队列，以便下一轮遍历\n                if (current.left != null) {\n                    queue.offer(current.left);\n                }\n                if (current.right != null) {\n                    queue.offer(current.right);\n                }\n            }\n        }\n        return result;\n    }\n}\n```\n\n## 31. 下一个排列\nhttps://leetcode.cn/problems/next-permutation/description/\n\n整数数组的一个 排列  就是将其所有成员以序列或线性顺序排列。\n\n例如，arr = [1,2,3] ，以下这些都可以视作 arr 的排列：[1,2,3]、[1,3,2]、[3,1,2]、[2,3,1] 。\n\n整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地，如果数组的所有排列根据其字典顺序从小到大排列在一个\n\n容器中，那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列，那么这个数组必须重排为字典序最小的排列（即，其元素按升序排列）。\n\n例如，arr = [1,2,3] 的下一个排列是 [1,3,2] 。\n\n类似地，arr = [2,3,1] 的下一个排列是 [3,1,2] 。\n\n而 arr = [3,2,1] 的下一个排列是 [1,2,3] ，因为 [3,2,1] 不存在一个字典序更大的排列。\n\n给你一个整数数组 nums ，找出 nums 的下一个排列。\n\n必须 原地 修改，只允许使用额外常数空间。\n\n \n\n>示例 1：\n>\n>输入：nums = [1,2,3]\n>\n>输出：[1,3,2]\n\n>示例 2：\n>\n>输入：nums = [3,2,1]\n>\n>输出：[1,2,3]\n\n>示例 3：\n>\n>输入：nums = [1,1,5]\n>\n>输出：[1,5,1]\n \n\n提示：\n\n- 1 <= nums.length <= 100\n- 0 <= nums[i] <= 100\n\n```java\n/**\n * 解决方案类，提供排列算法的功能。\n */\nclass Solution {\n    /**\n     * 获取下一个排列。\n     * \n     * @param nums 输入的整数数组，将对其进行就地修改以得到下一个排列。\n     */\n    public void nextPermutation(int[] nums) {\n        // 从倒数第二个元素开始向前搜索，寻找第一个相邻升序对(i, i+1)，满足nums[i] < nums[i+1]\n        //123465\n        int i = nums.length - 2;\n        while (i >= 0 && nums[i] >= nums[i + 1]) {\n            i--;\n        }\n        \n        // 如果找到了这样的i，那么再从后向前搜索，寻找第一个大于nums[i]的元素nums[j]\n        if (i >= 0) {\n            int j = nums.length - 1;\n            while (j >= 0 && nums[i] >= nums[j]) {\n                j--;\n            }\n            \n            // 交换nums[i]和nums[j]，以确保之后的反转操作能产生正确的下一个排列\n            swap(nums, i, j);\n        }\n        \n        // 将i之后的元素反转，得到下一个排列\n        reverse(nums, i + 1);\n    }\n\n    /**\n     * 交换数组中两个位置的元素。\n     * \n     * @param nums 输入的整数数组。\n     * @param i    要交换的元素的第一个位置。\n     * @param j    要交换的元素的第二个位置。\n     */\n    public void swap(int[] nums, int i, int j) {\n        int temp = nums[i];\n        nums[i] = nums[j];\n        nums[j] = temp;\n    }\n\n    /**\n     * 反转数组中指定范围的元素。\n     * \n     * @param nums 输入的整数数组。\n     * @param start 要反转的元素的起始位置。\n     */\n    public void reverse(int[] nums, int start) {\n        int left = start, right = nums.length - 1;\n        while (left < right) {\n            swap(nums, left, right);\n            left++;\n            right--;\n        }\n    }\n}\n```\n\n## 4. 寻找两个正序数组的中位数\nhttps://leetcode.cn/problems/median-of-two-sorted-arrays/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定两个大小分别为 m 和 n 的正序（从小到大）数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。\n\n算法的时间复杂度应该为 O(log (m+n)) 。\n\n \n\n>示例 1：\n>\n>输入：nums1 = [1,3], nums2 = [2]\n>\n>输出：2.00000\n>\n>解释：合并数组 = [1,2,3] ，中位数 2\n\n>示例 2：\n>\n>输入：nums1 = [1,2], nums2 = [3,4]\n>\n>输出：2.50000\n>\n>解释：合并数组 = [1,2,3,4] ，中位数 (2 + 3) / 2 = 2.5\n \n\n \n\n提示：\n\n- nums1.length == m\n- nums2.length == n\n- 0 <= m <= 1000\n- 0 <= n <= 1000\n- 1 <= m + n <= 2000\n- -10^6 <= nums1[i], nums2[i] <= 10^6\n\n```java\nclass Solution {\n    /**\n     * 寻找两个有序数组的中位数。\n     * 通过二分查找的方式寻找中位数，避免了合并数组的需要。\n     * \n     * @param nums1 第一个有序数组\n     * @param nums2 第二个有序数组\n     * @return 两个数组的中位数\n     */\n    public double findMedianSortedArrays(int[] nums1, int[] nums2) {\n        int n = nums1.length;\n        int m = nums2.length;\n        int left = (n + m + 1) / 2;\n        int right = (n + m + 2) / 2;\n        // 由于中位数是两个数组合并后的中位数，因此left和right分别代表合并后的数组的第left和right个元素（半数中位数的情况）\n        // 将偶数和奇数的情况合并，如果是奇数，会求两次同样的 k 。\n        return (getKth(nums1, 0, n - 1, nums2, 0, m - 1, left) + getKth(nums1, 0, n - 1, nums2, 0, m - 1, right)) * 0.5;\n    }\n\n    /**\n     * 获取两个有序数组中的第k小的元素。\n     * 该方法通过二分查找的方式在两个有序数组中找到第k小的元素，用于支持findMedianSortedArrays方法。\n     * \n     * @param nums1 第一个有序数组\n     * @param start1 nums1的起始索引\n     * @param end1 nums1的结束索引\n     * @param nums2 第二个有序数组\n     * @param start2 nums2的起始索引\n     * @param end2 nums2的结束索引\n     * @param k 需要找到的第k小的元素的索引\n     * @return 第k小的元素\n     */\n    private int getKth(int[] nums1, int start1, int end1, int[] nums2, int start2, int end2, int k) {\n        int len1 = end1 - start1 + 1;\n        int len2 = end2 - start2 + 1;\n        // 为了简化逻辑，让len1始终小于等于len2，如果len1大于len2，则交换两个数组的角色\n        //让 len1 的长度小于 len2，这样就能保证如果有数组空了，一定是 len1 \n        if (len1 > len2) {\n            return getKth(nums2, start2, end2, nums1, start1, end1, k);\n        }\n        if (len1 == 0) {\n            // 如果nums1为空，则nums2中的第k个元素就是答案\n            return nums2[start2 + k - 1];\n        }\n\n        if (k == 1) {\n            // 如果k为1，直接返回两个数组的起始元素中的较小值\n            return Math.min(nums1[start1], nums2[start2]);\n        }\n        //求中位数可以转换为求 k大的数 又可以转换为每次排除掉 k/2个元素\n        int i = start1 + Math.min(len1, k / 2) - 1;\n        int j = start2 + Math.min(len2, k / 2) - 1;\n\n        if (nums1[i] > nums2[j]) {\n            // 如果nums1中的第i个元素大于nums2中的第j个元素，则中位数在nums1的前i个元素中，同时排除掉 num2的前j个元素\n            //下一次寻找就变成了在 (k-j) 个元素中寻找\n            return getKth(nums1, start1, end1, nums2, j + 1, end2, k - (j - start2 + 1));\n        } else {\n            // 反之，在nums1的第i+1到末尾的元素和nums2的前j个元素中寻找第k-(i-start1+1)小的元素\n            return getKth(nums1, i + 1, end1, nums2, start2, end2, k - (i - start1 + 1));\n        }\n    }\n}\n```\n\n## 232. 用栈实现队列\nhttps://leetcode.cn/problems/implement-queue-using-stacks/description/\n\n请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作（push、pop、peek、empty）：\n\n实现 MyQueue 类：\n\n- void push(int x) 将元素 x 推到队列的末尾\n- int pop() 从队列的开头移除并返回元素\n- int peek() 返回队列开头的元素\n- boolean empty() 如果队列为空，返回 true ；否则，返回 false\n\n说明：\n\n- 你 只能 使用标准的栈操作 —— 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。\n- 你所使用的语言也许不支持栈。你可以使用 list 或者 deque（双端队列）来模拟一个栈，只要是标准的栈操作即可。\n \n\n>示例 1：\n>\n>输入：\n>\n>[\"MyQueue\", \"push\", \"push\", \"peek\", \"pop\", \"empty\"]\n>\n>[[], [1], [2], [], [], []]\n>\n>输出：\n>\n>[null, null, null, 1, 1, false]\n>\n>解释：\n>- MyQueue myQueue = new MyQueue();\n>- myQueue.push(1); // queue is: [1]\n>- myQueue.push(2); // queue is: [1, 2] (leftmost is front of the >queue)\n>- myQueue.peek(); // return 1\n>- myQueue.pop(); // return 1, queue is [2]\n>- myQueue.empty(); // return false\n \n\n提示：\n\n- 1 <= x <= 9\n- 最多调用 100 次 push、pop、peek 和 empty\n- 假设所有操作都是有效的 （例如，一个空的队列不会调用 pop 或者 peek 操作）\n\n```java\nclass MyQueue {\n    Deque<Integer> in;\n    Deque<Integer> out;\n    public MyQueue() {\n        in = new LinkedList<>();\n        out = new LinkedList<>();\n    }\n    \n    public void push(int x) {\n        in.push(x);\n    }\n    \n    public int pop() {\n        if(out.isEmpty()){\n            pushToOut();\n        }\n        return out.pop();\n    }\n    \n    public int peek() {\n        if(out.isEmpty()){\n            pushToOut();\n        }\n        return out.peek();\n    }\n    \n    public boolean empty() {\n        return in.isEmpty() && out.isEmpty();\n    }\n\n    public void pushToOut(){\n        while(!in.isEmpty()){\n            out.push(in.pop());\n        }\n    }\n}\n\n/**\n * Your MyQueue object will be instantiated and called as such:\n * MyQueue obj = new MyQueue();\n * obj.push(x);\n * int param_2 = obj.pop();\n * int param_3 = obj.peek();\n * boolean param_4 = obj.empty();\n */\n```\n\n## 148. 排序链表\nhttps://leetcode.cn/problems/sort-list/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你链表的头结点 head ，请将其按 升序 排列并返回 排序后的链表 。\n\n \n\n>示例 1：\n>\n>![alt text](../img/数据结构和算法/排序链表1.png)\n>\n>输入：head = [4,2,1,3]\n>\n>输出：[1,2,3,4]\n\n>示例 2：\n>\n>![alt text](../img/数据结构和算法/排序链表2.png)\n>\n>输入：head = [-1,5,3,4,0]\n>\n>输出：[-1,0,3,4,5]\n\n>示例 3：\n>\n>输入：head = []\n>\n>输出：[]\n \n\n提示：\n\n- 链表中节点的数目在范围 [0, 5 * 10^4] 内\n- -10^5 <= Node.val <= 10^5\n\n```java\n/**\n * Definition for singly-linked list.\n * public class ListNode {\n *     int val;\n *     ListNode next;\n *     ListNode() {}\n *     ListNode(int val) { this.val = val; }\n *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }\n * }\n */\n/**\n * Solution类提供了一个方法来对链表进行排序。\n * 它实现了归并排序算法，该算法是递归地将链表分割成更小的部分，然后将这些部分合并成一个排序好的链表。\n */\nclass Solution {\n    /**\n     * 对给定链表进行排序。\n     * \n     * @param head 链表的头节点。\n     * @return 排序后的链表的头节点。\n     */\n    public ListNode sortList(ListNode head) {\n        return mergeSort(head);\n    }\n\n    /**\n     * 归并排序的递归部分。\n     * 它首先找到链表的中间点，然后将链表分割成两部分，分别对这两部分进行排序，最后将排序好的两部分合并。\n     * \n     * @param head 链表的头节点。\n     * @return 排序后的链表的头节点。\n     */\n    public ListNode mergeSort(ListNode head){\n        // 如果链表为空或只有一个节点，无需排序，直接返回\n        if(head == null || head.next==null){\n            return head;\n        }\n        ListNode slow = head,fast = head.next;\n        // 寻找链表的中间点\n        while(fast!=null && fast.next!=null){\n            slow = slow.next;\n            fast = fast.next.next;\n        }\n        // 递归地对右半部分进行排序\n        ListNode m = mergeSort(slow.next);\n        slow.next = null;\n        // 递归地对左半部分进行排序\n        ListNode l = mergeSort(head);\n        // 合并排序好的两部分\n        return mergeTwo(m,l);\n    }\n\n    /**\n     * 合并两个已排序的链表。\n     * \n     * @param n1 第一个链表的头节点。\n     * @param n2 第二个链表的头节点。\n     * @return 合并后的链表的头节点。\n     */\n    public ListNode mergeTwo(ListNode n1,ListNode n2){\n        // 如果其中一个链表为空，直接返回另一个链表\n        if(n1 == null){\n            return n2;\n        }\n        if(n2 == null){\n            return n1;\n        }\n        ListNode newNode;\n        // 比较两个链表的当前节点，将较小值作为新链表的节点，并递归地合并剩余部分\n        if(n1.val < n2.val){\n            newNode = n1;\n            newNode.next = mergeTwo(n1.next,n2);\n        }else{\n            newNode = n2;\n            newNode.next = mergeTwo(n1,n2.next);\n        }\n        return newNode;\n    }\n}\n```\n\n## 69. x 的平方根 \nhttps://leetcode.cn/problems/sqrtx/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个非负整数 x ，计算并返回 x 的 算术平方根 。\n\n由于返回类型是整数，结果只保留 整数部分 ，小数部分将被 舍去 。\n\n注意：不允许使用任何内置指数函数和算符，例如 pow(x, 0.5) 或者 x ** 0.5 。\n\n \n\n>示例 1：\n>\n>输入：x = 4\n>\n>输出：2\n\n>示例 2：\n>\n>输入：x = 8\n>\n>输出：2\n>\n>解释：8 的算术平方根是 2.82842..., 由于返回类型是整数，小数部分将被舍去。\n \n\n提示：\n\n- 0 <= x <= 2^31 - 1\n\n\n```java\nclass Solution {\n    public int mySqrt(int x) {\n        // 初始化左右边界和结果\n        int left = 0;\n        int right = x;\n        int res = -1;\n\n        // 二分查找\n        while (left <= right) {\n            // 计算中间值\n            int mid = left + (right - left) / 2;\n\n            // 如果中间值的平方小于等于 x，则更新结果为当前中间值，并将左边界向右移动一位\n            if ((long) mid * mid <= x) {\n                res = mid;\n                left = mid + 1;\n            } else {\n                // 如果中间值的平方大于 x，则将右边界向左移动一位\n                right = mid - 1;\n            }\n        }\n\n        return res; // 返回结果\n    }\n}\n```\n\n## 8. 字符串转换整数 (atoi)\nhttps://leetcode.cn/problems/string-to-integer-atoi/description/\n\n请你来实现一个 myAtoi(string s) 函数，使其能将字符串转换成一个 32 位有符号整数。\n\n函数 myAtoi(string s) 的算法如下：\n\n- 空格：读入字符串并丢弃无用的前导空格（\" \"）\n- 符号：检查下一个字符（假设还未到字符末尾）为 '-' 还是 '+'。如果两者都不存在，则假定结果为正。\n- 转换：通过跳过前置零来读取该整数，直到遇到非数字字符或到达字符串的结尾。如果没有读取数字，则结果为0。\n- 舍入：如果整数数超过 32 位有符号整数范围 [−231,  231 − 1] ，需要截断这个整数，使其保持在这个范围内。具体来说，小于 −231 的整数应该被舍入为 −231 ，大于 231 − 1 的整数应该被舍入为 231 − 1 。\n\n返回整数作为最终结果。\n\n \n\n>示例 1：\n>\n>输入：s = \"42\"\n>\n>输出：42\n>\n>解释：加粗的字符串为已经读入的字符，插入符号是当前读取的字符。\n>\n>带下划线线的字符是所读的内容，插入符号是当前读入位置。\n>第 1 步：\"42\"（当前没有读入字符，因为没有前导空格）\n>         ^\n>第 2 步：\"42\"（当前没有读入字符，因为这里不存在 '-' 或者 '+'）\n>         ^\n>第 3 步：\"42\"（读入 \"42\"）\n           ^\n>示例 2：\n>\n>输入：s = \" -042\"\n>\n>输出：-42\n>\n>解释：\n>\n>第 1 步：\"   -042\"（读入前导空格，但忽视掉）\n>            ^\n>第 2 步：\"   -042\"（读入 '-' 字符，所以结果应该是负数）\n>             ^\n>第 3 步：\"   -042\"（读入 \"042\"，在结果中忽略前导零）\n               ^\n>示例 3：\n>\n>输入：s = \"1337c0d3\"\n>\n>输出：1337\n>\n>解释：\n>\n>第 1 步：\"1337c0d3\"（当前没有读入字符，因为没有前导空格）\n>         ^\n>第 2 步：\"1337c0d3\"（当前没有读入字符，因为这里不存在 '-' 或者 '+'）\n>         ^\n>第 3 步：\"1337c0d3\"（读入 \"1337\"；由于下一个字符不是一个数字，所以读入停止）\n             ^\n>示例 4：\n>\n>输入：s = \"0-1\"\n>\n>输出：0\n>\n>解释：\n>\n>第 1 步：\"0-1\" (当前没有读入字符，因为没有前导空格)\n>         ^\n>第 2 步：\"0-1\" (当前没有读入字符，因为这里不存在 '-' 或者 '+')\n>         ^\n>第 3 步：\"0-1\" (读入 \"0\"；由于下一个字符不是一个数字，所以读入停止)\n          ^\n>示例 5：\n>\n>输入：s = \"words and 987\"\n>\n>输出：0\n>\n>解释：\n>\n>读取在第一个非数字字符“w”处停止。\n\n \n\n提示：\n\n- 0 <= s.length <= 200\n- s 由英文字母（大写和小写）、数字（0-9）、' '、'+'、'-' 和 '.' 组成\n\n```java\n/**\n * Solution类提供了一个方法来将字符串转换为整数。\n * 它模拟了标准的atoi（字符串转换为整数）功能，处理了空格、符号位和数字的边界情况。\n */\nclass Solution {\n    /**\n     * 将字符串s转换为一个整数。\n     * \n     * @param s 输入的字符串，可能包含空格、正负号和数字。\n     * @return 返回转换后的整数。如果字符串不能转换为有效的整数，或者结果超出了int类型的范围，则返回特定的值。\n     */\n    public int myAtoi(String s) {\n        // 初始化索引为0，用于遍历字符串\n        int index = 0;\n\n        // 如果字符串为空，则直接返回0\n        if(s == null){\n            return index;\n        }\n\n        // 获取字符串的长度\n        int len = s.length();\n\n        // 跳过字符串开头的所有空格\n        while(index < len && s.charAt(index) == ' '){\n            index++;\n        }\n\n        // 如果字符串全是空格，则返回0\n        if(index==len){\n            return 0;\n        }\n\n        // 初始化符号位为1，表示正数\n        int symbol = 1;\n\n        // 获取第一个非空格字符\n        char cc = s.charAt(index);\n        // 如果是正号，则跳过它\n        if(cc == '+'){\n            index++;\n        }\n\n        // 如果是负号，则更新符号位为-1，并跳过它\n        if(cc == '-'){\n            index++;\n            symbol=-1;\n        }\n\n        // 初始化结果值为0\n        int res = 0;\n        // 遍历剩余的字符串，将数字转换为整数\n        while(index<len){\n            // 获取当前字符减去'0'后的数字值\n            int a = s.charAt(index) - '0';\n            // 如果字符不是数字，则退出循环\n            if(a>9 || a<0){\n                break;\n            }\n            // 检查结果是否超出int类型的上限\n            if(res > Integer.MAX_VALUE/10 || (res == Integer.MAX_VALUE/10 && a > Integer.MAX_VALUE%10)){\n                return Integer.MAX_VALUE;\n            }\n            // 检查结果是否低于int类型的下限\n            if(res < Integer.MIN_VALUE/10 || (res == Integer.MIN_VALUE/10 && a > -(Integer.MIN_VALUE%10))){\n                return Integer.MIN_VALUE;\n            }\n            // 更新结果值\n            res = res*10 + a*symbol;\n            // 移动到下一个字符\n            index++;\n        }\n        // 返回最终的转换结果\n        return res;\n\n    }\n}\n```\n\n## 22. 括号生成\nhttps://leetcode.cn/problems/generate-parentheses/description/?envType=study-plan-v2&envId=top-interview-150\n\n数字 n 代表生成括号的对数，请你设计一个函数，用于能够生成所有可能的并且 有效的 括号组合。\n\n \n\n>示例 1：\n>\n>输入：n = 3\n>\n>输出：[\"((()))\",\"(()())\",\"(())()\",\"()(())\",\"()()()\"]\n\n>示例 2：\n>\n>输入：n = 1\n>\n>输出：[\"()\"]\n \n\n提示：\n\n- 1 <= n <= 8\n\n```java\nclass Solution {\n    public List<String> generateParenthesis(int n) {\n        List<String> result = new ArrayList<>();\n        generateCombinations(result, \"\", n, n);\n        return result;\n    }\n\n    private void generateCombinations(List<String> result, String current, int left, int right) {\n        // 基本情况：如果左右括号都用完了，将当前组合添加到结果列表中\n        if (left == 0 && right == 0) {\n            result.add(current);\n            return;\n        }\n        \n        // 如果还有左括号可用，可以放一个左括号\n        if (left > 0) {\n            generateCombinations(result, current + \"(\", left - 1, right);\n        }\n        \n        // 只有在右括号比左括号多的情况下，才能放右括号，保证生成的括号序列是合法的\n        if (right > left) {\n            generateCombinations(result, current + \")\", left, right - 1);\n        }\n    }\n}\n```\n\n## 70. 爬楼梯\nhttps://leetcode.cn/problems/climbing-stairs/description/?envType=study-plan-v2&envId=top-interview-150\n\n假设你正在爬楼梯。需要 n 阶你才能到达楼顶。\n\n每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢？\n\n \n\n>示例 1：\n>\n>输入：n = 2\n>\n>输出：2\n>\n>解释：有两种方法可以爬到楼顶。\n>1. 1 阶 + 1 阶\n>2. 2 阶\n\n>示例 2：\n>\n>输入：n = 3\n>\n>输出：3\n>\n>解释：有三种方法可以爬到楼顶。\n>1. 1 阶 + 1 阶 + 1 阶\n>2. 1 阶 + 2 阶\n>3. 2 阶 + 1 阶\n \n\n提示：\n\n- 1 <= n <= 45\n\n```java\nclass Solution {\n    public int climbStairs(int n) {\n        if (n == 1) {\n            return 1; // 如果只有1阶台阶，只有一种方法可以到达楼顶\n        }\n\n        int[] dp = new int[n + 1]; // 创建一个数组来存储到每个台阶的方法数\n        dp[1] = 1; // 到第一阶台阶只有1种方法\n        dp[2] = 2; // 到第二阶台阶有两种方法\n\n        for (int i = 3; i <= n; i++) {\n            dp[i] = dp[i - 1] + dp[i - 2]; // 当前台阶的方法数是前两个台阶之和\n        }\n\n        return dp[n]; // 返回到达第n阶台阶的方法数\n    }\n}\n```\n\n## 2. 两数相加\nhttps://leetcode.cn/problems/add-two-numbers/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你两个 非空 的链表，表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的，并且每个节点只能存储 一位 数字。\n\n请你将两个数相加，并以相同形式返回一个表示和的链表。\n\n你可以假设除了数字 0 之外，这两个数都不会以 0 开头。\n\n\n\n>示例 1：\n>\n> ![alt text](../img/数据结构和算法/链表-两数相加.png)\n>\n>输入：l1 = [2,4,3], l2 = [5,6,4]\n>\n>输出：[7,0,8]\n>\n>解释：342 + 465 = 807.\n\n>示例 2：\n>\n>输入：l1 = [0], l2 = [0]\n>\n>输出：[0]\n\n>示例 3：\n>\n>输入：l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]\n>\n>输出：[8,9,9,9,0,0,0,1]\n\n\n提示：\n\n- 每个链表中的节点数在范围 [1, 100] 内\n- 0 <= Node.val <= 9\n- 题目数据保证列表表示的数字不含前导零\n\n```java\n/**\n * Definition for singly-linked list.\n * public class ListNode {\n *     int val;\n *     ListNode next;\n *     ListNode() {}\n *     ListNode(int val) { this.val = val; }\n *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }\n * }\n */\n/**\n * 解决方案类，用于处理两个链表表示的数字相加的问题。\n */\nclass Solution {\n    /**\n     * 添加两个由链表表示的数字。\n     * \n     * @param l1 第一个链表的头节点，表示一个非负整数。\n     * @param l2 第二个链表的头节点，表示一个非负整数。\n     * @return 返回一个新的链表，表示两个输入链表表示的数字之和。\n     */\n    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {\n        // 哑节点用于简化链表操作，避免处理空链表的特殊情况\n        ListNode dummy = new ListNode();\n        ListNode head = dummy;\n\n        // 用于处理进位的变量\n        int add = 0;\n\n        // 循环直到两个链表都遍历完且没有进位\n        while (l1 != null || l2 != null || add != 0) {\n            // 获取当前节点值，如果节点为null则默认值为0\n            int x = l1 == null ? 0 : l1.val;\n            int y = l2 == null ? 0 : l2.val;\n\n            // 计算当前位的和，包括进位\n            int result = x + y + add;\n\n            // 创建新节点，值为当前位的和对10取余\n            head.next = new ListNode(result % 10);\n\n            // 更新进位值，为当前位和除以10的商\n            add = result / 10;\n\n            // 移动到下一个节点\n            l1 = l1 == null ? null : l1.next;\n            l2 = l2 == null ? null : l2.next;\n\n            // 移动head指针到新创建的节点\n            head = head.next;\n        }\n\n        // 返回新链表的头节点，即哑节点的下一个节点\n        return dummy.next;\n    }\n}\n```\n\n## 165. 比较版本号\nhttps://leetcode.cn/problems/compare-version-numbers/description/\n\n给你两个 版本号字符串 version1 和 version2 ，请你比较它们。版本号由被点 '.' 分开的修订号组成。修订号的值 是它 转换为整数 并忽略前导零。\n\n比较版本号时，请按 从左到右的顺序 依次比较它们的修订号。如果其中一个版本字符串的修订号较少，则将缺失的修订号视为 0。\n\n返回规则如下：\n\n- 如果 version1 < version2 返回 -1，\n- 如果 version1 > version2 返回 1，\n- 除此之外返回 0。\n \n\n>示例 1：\n>\n>输入：version1 = \"1.2\", version2 = \"1.10\"\n>\n>输出：-1\n>\n>解释：\n>\n>version1 的第二个修订号为 \"2\"，version2 的第二个修订号为 \"10\"：2 < 10，所以 version1 < version2。\n\n>示例 2：\n>\n>输入：version1 = \"1.01\", version2 = \"1.001\"\n>\n>输出：0\n>\n>解释：\n>\n>忽略前导零，\"01\" 和 \"001\" 都代表相同的整数 \"1\"。\n\n>示例 3：\n>\n>输入：version1 = \"1.0\", version2 = \"1.0.0.0\"\n>\n>输出：0\n>\n>解释：\n>\n>version1 有更少的修订号，每个缺失的修订号按 \"0\" 处理。\n\n \n\n提示：\n\n- 1 <= version1.length, version2.length <= 500\n- version1 和 version2 仅包含数字和 '.'\n- version1 和 version2 都是 有效版本号\n- version1 和 version2 的所有修订号都可以存储在 32 位整数 中\n\n```java\nclass Solution {\n    /**\n     * 比较两个版本号的大小。\n     * 版本号由数字和点号组成，例如\"1.1\"或\"2.2.1\"。\n     * 比较规则是按照从左到右的顺序比较每个数字部分，如果某个数字部分大于对方，则这个版本号大于对方；\n     * 如果某个数字部分小于对方，则这个版本号小于对方；如果所有数字部分都相等，则两个版本号相等。\n     * \n     * @param version1 第一个版本号字符串\n     * @param version2 第二个版本号字符串\n     * @return 返回比较结果，如果version1大于version2返回1，如果version1小于version2返回-1，如果两个版本号相等返回0。\n     */\n    public int compareVersion(String version1, String version2) {\n        int v1Len = version1.length(); // 获取第一个版本号字符串的长度\n        int v2Len = version2.length(); // 获取第二个版本号字符串的长度\n\n        int i = 0, j = 0; // 初始化两个指针i和j，分别用于遍历两个版本号字符串\n\n        while (i < v1Len || j < v2Len) {\n            int x = 0; // 用于存储当前遍历到的第一个版本号的数字部分\n            // 遍历第一个版本号字符串，直到遇到点号或到达字符串末尾，将数字字符转换为整数\n            for (; i < v1Len && version1.charAt(i) != '.'; i++) {\n                x = x * 10 + version1.charAt(i) - '0';\n            }\n            i++; // 跳过点号\n\n            int y = 0; // 用于存储当前遍历到的第二个版本号的数字部分\n            // 遍历第二个版本号字符串，直到遇到点号或到达字符串末尾，将数字字符转换为整数\n            for (; j < v2Len && version2.charAt(j) != '.'; j++) {\n                y = y * 10 + version2.charAt(j) - '0';\n            }\n            j++; // 跳过点号\n\n            // 如果当前数字部分不相等，则返回比较结果（大于返回1，小于返回-1）\n            if (x != y) {\n                return x > y ? 1 : -1;\n            }\n        }\n\n        // 如果所有数字部分都比较完毕，且没有不相等的情况，则版本号相等，返回0\n        return 0;\n    }\n}\n```\n\n## 239. 滑动窗口最大值\nhttps://leetcode.cn/problems/sliding-window-maximum/description/\n\n给你一个整数数组 nums，有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。\n\n返回 滑动窗口中的最大值 。\n\n \n\n>示例 1：\n>\n>输入：nums = [1,3,-1,-3,5,3,6,7], k = 3\n>\n>输出：[3,3,5,5,6,7]\n>\n>解释：\n>\n>滑动窗口的位置                    |最大值\n>\n>\n>[1  3  -1] -3  5  3  6  7       |3\n>\n> 1 [3  -1  -3] 5  3  6  7       |3\n>\n> 1  3 [-1  -3  5] 3  6  7       |5\n>\n> 1  3  -1 [-3  5  3] 6  7       |5\n>\n> 1  3  -1  -3 [5  3  6] 7       |6\n>\n> 1  3  -1  -3  5 [3  6  7]      |7\n\n>示例 2：\n>\n>输入：nums = [1], k = 1\n>\n>输出：[1]\n \n\n提示：\n\n- 1 <= nums.length <= 10^5\n- -10^4 <= nums[i] <= 10^4\n- 1 <= k <= nums.length\n\n```java\n/**\n * 解决最大滑动窗口问题的类。\n * 该类提供了一个方法来找出给定数组中每个滑动窗口内的最大元素。\n */\nclass Solution {\n    /**\n     * 计算给定数组的每个滑动窗口内的最大元素。\n     * \n     * @param nums 原始整数数组\n     * @param k 滑动窗口的大小\n     * @return 包含每个滑动窗口最大元素的数组\n     */\n    public int[] maxSlidingWindow(int[] nums, int k) {\n        // 初始化结果数组，长度为原始数组长度减去窗口大小加一\n        int[] res = new int[nums.length - k + 1];\n        // 使用双端队列来维护当前窗口内的元素，队列中的元素按降序排列\n        Deque<Integer> dq = new LinkedList<>();\n        for (int i = 0; i < nums.length; i++) {\n            // 维护队列的顺序，如果当前元素大于队列尾部元素，则移除队列尾部元素\n            while (!dq.isEmpty() && nums[i] > dq.peekLast()) {\n                dq.removeLast();\n            }\n            // 将当前元素加入队列尾部\n            dq.offerLast(nums[i]);\n            // 如果当前索引大于等于窗口大小，且队列头部元素等于窗口前一个元素，则移除队列头部元素\n            // 窗口移动后移除队首\n            if (i >= k && nums[i - k] == dq.peekFirst()) {\n                dq.removeFirst();\n            }\n            // 当当前索引大于等于窗口大小减一时，将队列头部元素（即当前窗口内的最大元素）记录到结果数组中\n            if (i >= k - 1) {\n                res[i - k + 1] = dq.peekFirst();\n            }\n        }\n        // 返回结果数组\n        return res;\n    }\n}\n```\n\n## 41. 缺失的第一个正数\nhttps://leetcode.cn/problems/first-missing-positive/description/\n\n给你一个未排序的整数数组 nums ，请你找出其中没有出现的最小的正整数。\n\n请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。\n \n\n>示例 1：\n>\n>输入：nums = [1,2,0]\n>\n>输出：3\n>\n>解释：范围 [1,2] 中的数字都在数组中。\n\n>示例 2：\n>\n>输入：nums = [3,4,-1,1]\n>\n>输出：2\n>\n>解释：1 在数组中，但 2 没有。\n\n>示例 3：\n>\n>输入：nums = [7,8,9,11,12]\n>\n>输出：1\n>\n>解释：最小的正数 1 没有出现。\n \n\n提示：\n\n- 1 <= nums.length <= 10^5\n- -2^31 <= nums[i] <= 2^31 - 1\n\n```java\nclass Solution {\n        /**\n     * 寻找缺失的第一个正整数。\n     * 在给定的数组中，找到第一个缺失的正整数。数组中可能包含重复的数字，也可能是负数或零。\n     * \n     * @param nums 输入的整数数组，可能包含负数、零和重复的正整数。\n     * @return 返回第一个缺失的正整数，如果所有正整数均存在，则返回数组长度加一。\n     */\n    public int firstMissingPositive(int[] nums) {\n        int len = nums.length;\n        \n        // 遍历数组，通过交换元素使其处于正确的位置上\n        for (int i = 0; i < len; i++) {\n            // 当前元素是正整数且在有效范围内，并且它不在正确的位置上\n            while (nums[i] > 0 && nums[i] <= len && nums[i] != nums[nums[i] - 1]) {\n                // 交换当前元素与其正确位置上的元素\n                int temp = nums[nums[i] - 1];\n                nums[nums[i] - 1] = nums[i];\n                nums[i] = temp;\n            }\n        }\n        \n        // 再次遍历数组，寻找第一个缺失的正整数\n        for (int i = 0; i < len; i++) {\n            // 如果当前元素不等于其应该在的位置上的数字，则返回该位置加一\n            if (nums[i] != i + 1) {\n                return i + 1;\n            }\n        }\n        \n        // 如果所有正整数均在数组中，则返回数组长度加一\n        return len + 1;\n    }\n}\n```\n\n## LCR 140. 训练计划 II\nhttps://leetcode.cn/problems/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof/description/\n\n给定一个头节点为 head 的链表用于记录一系列核心肌群训练项目编号，请查找并返回倒数第 cnt 个训练项目编号。\n\n \n\n>示例 1：\n>\n>输入：head = [2,4,7,8], cnt = 1\n>\n>输出：8\n \n\n提示：\n\n- 1 <= head.length <= 100\n- 0 <= head[i] <= 100\n- 1 <= cnt <= head.length\n\n```java\n/**\n * Definition for singly-linked list.\n * public class ListNode {\n *     int val;\n *     ListNode next;\n *     ListNode(int x) { val = x; }\n * }\n */\nclass Solution {\n    /**\n     * 此函数用于获取链表中距离尾部指定距离的节点。\n     *\n     * @param head 链表的头节点，类型为ListNode\n     * @param k    距离尾部的节点数，整型数值\n     * @return     返回链表中第k个从尾部开始的节点，类型为ListNode\n     *\n     * 此函数通过双指针法实现，首先快指针移动k步，然后两个指针同时移动，\n     * 当快指针到达链表末尾时，慢指针正好位于目标位置。\n     */\n    public ListNode getKthFromEnd(ListNode head, int k) {\n        ListNode fast = head; // 初始化快指针\n        ListNode slow = head; // 初始化慢指针\n\n        // 快指针先移动k步\n        while (k-- > 0) {\n            fast = fast.next;\n        }\n\n        // 双指针同时移动，直到快指针到达链表末尾\n        while (fast != null) {\n            fast = fast.next;\n            slow = slow.next;\n        }\n\n        // 返回目标节点\n        return slow;\n    }\n}\n```\n\n## 322. 零钱兑换\nhttps://leetcode.cn/problems/coin-change/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个整数数组 coins ，表示不同面额的硬币；以及一个整数 amount ，表示总金额。\n\n计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额，返回 -1 。\n\n你可以认为每种硬币的数量是无限的。\n\n \n\n>示例 1：\n>\n>输入：coins = [1, 2, 5], amount = 11\n>\n>输出：3 \n>\n>解释：11 = 5 + 5 + 1\n\n>示例 2：\n>\n>输入：coins = [2], amount = 3\n>\n>输出：-1\n\n>示例 3：\n>\n>输入：coins = [1], amount = 0\n>\n>输出：0\n \n\n提示：\n\n- 1 <= coins.length <= 12\n- 1 <= coins[i] <= 2^31 - 1\n- 0 <= amount <= 10^4\n\n```java\npublic class Solution {\n    public int coinChange(int[] coins, int amount) {\n        // 创建一个数组dp，dp[i]表示凑成金额i所需的最少硬币数\n        int[] dp = new int[amount + 1];\n        // 初始时，凑成金额为0需要0个硬币，其他金额需要的硬币数初始化为amount+1表示不可达\n        Arrays.fill(dp, amount + 1);\n        dp[0] = 0; // 金额为0时，不需要硬币\n\n        // 动态规划过程，遍历金额从1到amount\n        for (int i = 1; i <= amount; i++) {\n            // 对于每个金额i，遍历硬币面额\n            for (int coin : coins) {\n                // 如果当前硬币面额小于等于金额i，并且使用该硬币可以减少所需的硬币数，则更新dp[i]的值\n                if (coin <= i) {\n                    dp[i] = Math.min(dp[i], dp[i - coin] + 1);\n                }\n            }\n        }\n\n        // 如果dp[amount]仍然为初始值amount+1，表示无法凑成总金额，返回-1；否则返回dp[amount]，即最少硬币数\n        return dp[amount] == amount + 1 ? -1 : dp[amount];\n    }\n}\n```\n\n## 76. 最小覆盖子串\nhttps://leetcode.cn/problems/minimum-window-substring/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串，则返回空字符串 \"\" 。\n\n\n\n注意：\n\n- 对于 t 中重复字符，我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。\n- 如果 s 中存在这样的子串，我们保证它是唯一的答案。\n\n\n>示例 1：\n>\n>输入：s = \"ADOBECODEBANC\", t = \"ABC\"\n>\n>输出：\"BANC\"\n>\n>解释：最小覆盖子串 \"BANC\" 包含来自字符串 t 的 'A'、'B' 和 'C'。\n\n>示例 2：\n>\n>输入：s = \"a\", t = \"a\"\n>\n>输出：\"a\"\n>\n>解释：整个字符串 s 是最小覆盖子串。\n\n>示例 3:\n>\n>输入: s = \"a\", t = \"aa\"\n>\n>输出: \"\"\n>\n>解释: t 中两个字符 'a' 均应包含在 s 的子串中，\n>因此没有符合条件的子字符串，返回空字符串。\n\n\n提示：\n\n- m == s.length\n- n == t.length\n- 1 <= m, n <= 10^5\n- s 和 t 由英文字母组成\n\n```java\nclass Solution {\n    public String minWindow(String s, String t) {\n        char[] cs = s.toCharArray();\n        char[] ct = t.toCharArray();\n\n        int[] count = new int[128];\n        // 将字符串t中每个字母出现的次数统计出来，这里--可以理解为有这么多的坑要填\n        for(char c: ct){\n            count[c]--;\n        }\n        String res = \"\";\n        //left=窗口左控制 right=窗口右控制\n        for(int left=0,right=0,cnt=0;right<cs.length;right++){\n            // 利用字符cs[right]去填count数组的坑\n            count[cs[right]]++;\n            // 如果填完坑之后发现，坑没有满或者刚好满，那么这个填坑是有效的，否则如果坑本来就是满的，这次填坑是无效的\n            // 注意其他非t中出现的字符，count数组的值是0，原来坑就是满的，那么填入count数组中，count[cs[right]]肯定大于0\n            if(count[cs[right]] <= 0){\n                cnt++;\n            }\n            // 如果cnt等于ct.length，那么说明窗口内已经包含t了，这时就要考虑移动左指针了，只有当左指针指向的字符是冗余的情况下，即count[cs[right]]>0，才能保证去掉该字符后，窗口中仍然包含t\n            // 注意cnt达到字符串t的长度后，它的值就不会再变化了，因为窗口内包含t之后，就会一直包含\n            while(cnt == ct.length && count[cs[left]] >0 ){\n                count[cs[left]]--;\n                left++;\n            }\n            // 当窗口内包含t后，计算此时窗口内字符串的长度，更新res\n            if(cnt == ct.length){\n                if(res.equals(\"\") || res.length() > (right-left+1)){\n                    res = s.substring(left,right+1);\n                }\n            }\n        }\n        return res;\n    }\n}\n```\n\n## 78. 子集\nhttps://leetcode.cn/problems/subsets/description/\n\n给你一个整数数组 nums ，数组中的元素 互不相同 。返回该数组所有可能的子集（幂集）。\n\n解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。\n\n \n\n>示例 1：\n>\n>输入：nums = [1,2,3]\n>\n>输出：[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]\n\n>示例 2：\n>\n>输入：nums = [0]\n>\n>输出：[[],[0]]\n \n\n提示：\n\n- 1 <= nums.length <= 10\n- -10 <= nums[i] <= 10\n- nums 中的所有元素 互不相同\n\n```java\nclass Solution {\n    /**\n     * 提供一个整数数组 nums，返回所有可能的子集（幂集）。\n     *\n     * @param nums 整数数组，表示给定的集合\n     * @return res 返回一个列表，其中每个元素也是一个列表，表示所有可能的子集（幂集）\n     */\n    List<List<Integer>> res = new ArrayList<>();\n    List<Integer> list = new ArrayList<>();\n\n    public List<List<Integer>> subsets(int[] nums) {\n        // 调用帮助方法，从数组的第一个元素开始生成子集\n        help(nums, 0);\n        // 返回结果列表\n        return res;\n    }\n\n    /**\n     * 递归帮助方法，用于生成子集\n     *\n     * @param nums 整数数组，表示给定的集合\n     * @param start 当前处理的元素在数组中的起始位置\n     */\n    public void help(int[] nums, int start) {\n        // 将当前子集添加到结果列表中\n        res.add(new ArrayList<>(list));\n        \n        // 如果当前子集等于原数组长度，结束递归\n        if (list.size() == nums.length) {\n            return;\n        }\n        \n        // 遍历数组，从当前起始位置开始\n        for (int i = start; i < nums.length; i++) {\n            // 如果当前元素不在子集中，将其添加并继续生成子集\n            if (!list.contains(nums[i])) {\n                list.add(nums[i]);\n                // 递归调用，从下一个元素开始\n                help(nums, i + 1);\n                // 回溯，移除添加的元素\n                list.remove(list.size() - 1);\n            }\n        }\n    }\n}\n```\n\n## 105. 从前序与中序遍历序列构造二叉树\nhttps://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定两个整数数组 preorder 和 inorder ，其中 preorder 是二叉树的先序遍历， inorder 是同一棵树的中序遍历，请构造二叉树并返回其根节点。\n\n\n\n>示例 1:\n>\n>\n>输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]\n>\n>输出: [3,9,20,null,null,15,7]\n\n>示例 2:\n>\n>输入: preorder = [-1], inorder = [-1]\n>\n>输出: [-1]\n\n\n提示:\n\n- 1 <= preorder.length <= 3000\n- inorder.length == preorder.length\n- -3000 <= preorder[i], inorder[i] <= 3000\n- preorder 和 inorder 均 无重复 元素\n- inorder 均出现在 preorder\n- preorder 保证 为二叉树的前序遍历序列\n- inorder 保证 为二叉树的中序遍历序列\n\n```java\n/**\n * Definition for a binary tree node.\n * public class TreeNode {\n *     int val;\n *     TreeNode left;\n *     TreeNode right;\n *     TreeNode() {}\n *     TreeNode(int val) { this.val = val; }\n *     TreeNode(int val, TreeNode left, TreeNode right) {\n *         this.val = val;\n *         this.left = left;\n *         this.right = right;\n *     }\n * }\n */\nclass Solution {\n    /**\n     * 此代码实现了一种方法，根据给定的前序遍历和中序遍历数组构建二叉树。\n     *\n     * @param preorder 前序遍历数组，其中根节点是数组的第一个元素\n     * @param inorder 中序遍历数组，数组中的元素顺序反映了从左到右的顺序\n     * @return 由给定遍历构建的二叉树的根节点\n     */\n    public TreeNode buildTree(int[] preorder, int[] inorder) {\n        // 初始化根节点索引为0，并创建一个哈希映射存储中序遍历中的元素及其索引\n        int rootIndex = 0;\n        Map<Integer, Integer> map = new HashMap<>();\n        \n        // 遍历中序遍历数组，将元素及其索引存入哈希映射\n        for (int i = 0; i < inorder.length; i++) {\n            map.put(inorder[i], i);\n        }\n        \n        // 从根节点开始递归构建二叉树\n        return build(0, preorder.length - 1, preorder);\n    }\n\n    /**\n     * 递归函数，根据前序遍历和中序遍历的子序列构建二叉树的子树。\n     *\n     * @param left 子序列的左边界\n     * @param right 子序列的右边界\n     * @param preorder 前序遍历数组的子序列\n     * @return 构建完成的子树的根节点\n     */\n    private TreeNode build(int left, int right, int[] preorder) {\n        // 当左边界大于右边界时，表示子序列为空，返回null\n        if (left > right) {\n            return null;\n        }\n        \n        // 获取当前子树的根节点值，并更新根节点索引\n        int rootVal = preorder[rootIndex++];\n        TreeNode root = new TreeNode(rootVal);\n        \n        // 递归构建左子树和右子树\n        root.left = build(left, map.get(rootVal) - 1, preorder);\n        root.right = build(map.get(rootVal) + 1, right, preorder);\n        \n        // 返回根节点\n        return root;\n    }\n}\n```\n\n## 43. 字符串相乘\nhttps://leetcode.cn/problems/multiply-strings/description/\n\n给定两个以字符串形式表示的非负整数 num1 和 num2，返回 num1 和 num2 的乘积，它们的乘积也表示为字符串形式。\n\n注意：不能使用任何内置的 BigInteger 库或直接将输入转换为整数。\n\n \n\n>示例 1:\n>\n>输入: num1 = \"2\", num2 = \"3\"\n>\n>输出: \"6\"\n\n>示例 2:\n>\n>输入: num1 = \"123\", num2 = \"456\"\n>\n>输出: \"56088\"\n \n\n提示：\n\n- 1 <= num1.length, num2.length <= 200\n- num1 和 num2 只能由数字组成。\n- num1 和 num2 都不包含任何前导零，除了数字0本身。\n\n```java\nclass Solution {\n        /**\n     * 字符串表示的两个数相乘。\n     * 该方法通过模拟手动乘法的过程，首先将两个数的每一位相乘，然后将结果累加到正确的位置。\n     * 最后，对累加结果进行进位调整和格式化，得到最终的乘积字符串。\n     *\n     * @param num1 第一个乘数的字符串表示。\n     * @param num2 第二个乘数的字符串表示。\n     * @return 两个字符串表示的数相乘的结果。\n     */\n    public String multiply(String num1, String num2) {\n        // 如果任一乘数的首位为0，则结果直接为0。\n        if(num1.charAt(0)=='0' || num2.charAt(0)=='0'){\n            return \"0\";\n        }\n        int len1 = num1.length();\n        int len2 = num2.length();\n\n        // 初始化一个数组来存储中间计算结果，长度为两个乘数字符串长度之和。\n        int[] sum = new int[len1+len2];\n\n        // 从个位开始，逐位相乘并将结果累加到sum数组的正确位置。\n        for(int i=len1-1;i>=0;i--){\n            int x = num1.charAt(i) -'0';\n            for(int j=len2-1;j>=0;j--){\n                int y = num2.charAt(j)-'0';\n                sum[i+j+1] += x*y;\n            }\n        }\n\n        // 对sum数组进行进位处理，从低位向高位依次检查，如果有进位则向高位进位。\n        int sumLen = sum.length;\n        for(int i=sumLen-1;i>0;i--){\n            sum[i-1] += sum[i] /10;\n            sum[i] %= 10;\n        }\n\n        // 使用StringBuilder构建最终的结果字符串。\n        StringBuilder sb = new StringBuilder();\n        // 如果最高位为0，则从下一位开始构建字符串。\n        int index = sum[0] ==0 ? 1:0;\n        while(index <sumLen){\n            sb.append(sum[index++]);\n        }\n        return sb.toString();\n    }\n}\n```\n\n## 155. 最小栈\nhttps://leetcode.cn/problems/min-stack/description/?envType=study-plan-v2&envId=top-interview-150\n\n设计一个支持 push ，pop ，top 操作，并能在常数时间内检索到最小元素的栈。\n\n实现 MinStack 类:\n\n- MinStack() 初始化堆栈对象。\n- void push(int val) 将元素val推入堆栈。\n- void pop() 删除堆栈顶部的元素。\n- int top() 获取堆栈顶部的元素。\n- int getMin() 获取堆栈中的最小元素。\n\n\n>示例 1:\n>\n>输入：\n>\n>[\"MinStack\",\"push\",\"push\",\"push\",\"getMin\",\"pop\",\"top\",\"getMin\"]\n>\n>[[],[-2],[0],[-3],[],[],[],[]]\n>\n>输出：\n>\n>[null,null,null,null,-3,null,0,-2]\n>\n>解释：\n>- MinStack minStack = new MinStack();\n>- minStack.push(-2);\n>- minStack.push(0);\n>- minStack.push(-3);\n>- minStack.getMin();   --> 返回 -3.\n>- minStack.pop();\n>- minStack.top();      --> 返回 0.\n>- minStack.getMin();   --> 返回 -2.\n\n\n提示：\n\n- -2^31 <= val <= 2^31 - 1\n- pop、top 和 getMin 操作总是在 非空栈 上调用\n- push, pop, top, and getMin最多被调用 3 * 10^4 次\n\n```java\nclass MinStack {\n    public static class Node{\n        int val;\n        int min;\n        Node next;\n\n        public Node(int val,int min,Node next){\n            this.val = val;\n            this.min = min;\n            this.next = next;\n        }\n\n    }\n\n    Node head;\n    public MinStack() {\n\n    }\n    \n    public void push(int val) {\n        if(head == null){\n            head = new Node(val,val,null);\n        }else{\n            head = new Node(val,Math.min(head.min,val),head);\n        }\n    }\n    \n    public void pop() {\n        head = head.next;\n    }\n    \n    public int top() {\n        return head.val;\n    }\n    \n    public int getMin() {\n        return head.min;\n    }\n}\n\n/**\n * Your MinStack object will be instantiated and called as such:\n * MinStack obj = new MinStack();\n * obj.push(val);\n * obj.pop();\n * int param_3 = obj.top();\n * int param_4 = obj.getMin();\n */\n```\n\n## 32. 最长有效括号\nhttps://leetcode.cn/problems/longest-valid-parentheses/description/\n\n给你一个只包含 '(' 和 ')' 的字符串，找出最长有效（格式正确且连续）括号\n子串\n的长度。\n\n \n\n>示例 1：\n>\n>输入：s = \"(()\"\n>\n>输出：2\n>\n>解释：最长有效括号子串是 \"()\"\n\n>示例 2：\n>\n>输入：s = \")()())\"\n>\n>输出：4\n>\n>解释：最长有效括号子串是 \"()()\"\n\n>示例 3：\n>\n>输入：s = \"\"\n>\n>输出：0\n \n\n提示：\n\n- 0 <= s.length <= 3 * 10^4\n- s[i] 为 '(' 或 ')'\n\n```java\nclass Solution {\n    /**\n     * 计算给定字符串中最长有效括号的长度。\n     * 有效括号是指以正确顺序匹配的括号对。\n     * \n     * @param s 输入的字符串，可能包含括号字符和其他字符\n     * @return 返回最长有效括号的长度\n     */\n    public int longestValidParentheses(String s) {\n        // 初始化左右括号计数器和最长有效长度\n        int left = 0, right = 0, max = 0;\n        \n        // 从字符串开始遍历，计算正向遍历中的最长有效括号长度\n        for (int i = 0; i < s.length(); i++) {\n            if (s.charAt(i) == '(') {\n                left++;\n            } else {\n                right++;\n            }\n            // 当左右括号平衡时，当前长度为2*right，因为每个右括号匹配一个左括号\n            if (left == right) {\n                max = Math.max(max, 2 * right);\n            } else if (right > left) {\n                // 如果右括号多于左括号，重置计数器\n                left = right = 0;\n            }\n        }\n        \n        // 重置左右括号计数器\n        left = right = 0;\n        \n        // 从字符串末尾开始遍历，计算反向遍历中的最长有效括号长度\n        for (int i = s.length() - 1; i > 0; i--) {\n            if (s.charAt(i) == '(') {\n                left++;\n            } else {\n                right++;\n            }\n            // 当左右括号平衡时，当前长度为2*left，因为每个左括号匹配一个右括号\n            if (left == right) {\n                max = Math.max(max, 2 * left);\n            } else if (left > right) {\n                // 如果左括号多于右括号，重置计数器\n                left = right = 0;\n            }\n        }\n        \n        // 返回最长有效括号的长度\n        return max;\n    }\n}\n```\n\n## 151. 翻转字符串里的单词\nhttps://leetcode.cn/problems/reverse-words-in-a-string/description/\n\n给你一个字符串 s ，请你反转字符串中 单词 的顺序。\n\n单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。\n\n返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。\n\n注意：输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中，单词间应当仅用单个空格分隔，且不包含任何额外的空格。\n\n \n\n>示例 1：\n>\n>输入：s = \"the sky is blue\"\n>\n>输出：\"blue is sky the\"\n\n>示例 2：\n>\n>输入：s = \"  hello world  \"\n>\n>输出：\"world hello\"\n>\n>解释：反转后的字符串中不能存在前导空格和尾随空格。\n\n>示例 3：\n>\n>输入：s = \"a good   example\"\n>\n>输出：\"example good a\"\n>\n>解释：如果两个单词间有多余的空格，反转后的字符串需要将单词间的空格减少到仅有一个。\n \n\n提示：\n\n- 1 <= s.length <= 10^4\n- s 包含英文大小写字母、数字和空格 ' '\n- s 中 至少存在一个 单词\n \n\n进阶：如果字符串在你使用的编程语言中是一种可变数据类型，请尝试使用 O(1) 额外空间复杂度的 原地 解法。\n\n```java\nclass Solution {\n    /**\n     * 反转字符串s中的单词顺序。\n     * \n     * @param s 输入的字符串，包含单词和单词之间的空格。\n     * @return 返回反转后的单词顺序的字符串。\n     */\n    public String reverseWords(String s) {\n        // 用于存储反转后的单词列表\n        List<String> res = new ArrayList<>();\n        // StringBuilder用于拼接单词\n        StringBuilder sb = new StringBuilder();\n\n        // 将字符串转换为字符数组，便于遍历\n        char[] arr = s.toCharArray();\n        // 遍历字符数组，包括最后一个位置，以便处理边界情况\n        for(int i=0;i<=arr.length;i++){\n            // 到达数组末尾或遇到空格，表示一个单词结束\n            if(i == arr.length || arr[i]==' '){\n                // 如果StringBuilder中存在字符，说明已经完成一个单词的拼接\n                if(sb.length()!=0){\n                    // 将拼接完成的单词添加到列表的头部，实现反转顺序\n                    res.add(0,sb.toString());\n                    // 清空StringBuilder，为下一个单词的拼接做准备\n                    sb = new StringBuilder();\n                }\n            }else{\n                // 拼接当前字符到StringBuilder中\n                sb.append(arr[i]);\n            }\n        }\n        // 使用空格连接列表中的所有单词，得到最终结果\n        return String.join(\" \",res);\n    }\n}\n```\n\n## 129. 求根节点到叶节点数字之和\nhttps://leetcode.cn/problems/sum-root-to-leaf-numbers/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个二叉树的根节点 root ，树中每个节点都存放有一个 0 到 9 之间的数字。\n\n每条从根节点到叶节点的路径都代表一个数字：\n\n- 例如，从根节点到叶节点的路径 1 -> 2 -> 3 表示数字 123 。\n\n计算从根节点到叶节点生成的 所有数字之和 。\n\n叶节点 是指没有子节点的节点。\n\n\n\n>示例 1：\n>\n>\n>输入：root = [1,2,3]\n>\n>输出：25\n>\n>解释：\n>- 从根到叶子节点路径 1->2 代表数字 12\n>- 从根到叶子节点路径 1->3 代表数字 13\n>- 因此，数字总和 = 12 + 13 = 25\n\n>示例 2：\n>\n>\n>输入：root = [4,9,0,5,1]\n>\n>输出：1026\n>\n>解释：\n>- 从根到叶子节点路径 4->9->5 代表数字 495\n>- 从根到叶子节点路径 4->9->1 代表数字 491\n>- 从根到叶子节点路径 4->0 代表数字 40\n>\n>因此，数字总和 = 495 + 491 + 40 = 1026\n\n\n提示：\n\n- 树中节点的数目在范围 [1, 1000] 内\n- 0 <= Node.val <= 9\n- 树的深度不超过 10\n\n```java\n\n/**\n * Definition for a binary tree node.\n * public class TreeNode {\n *     int val;\n *     TreeNode left;\n *     TreeNode right;\n *     TreeNode() {}\n *     TreeNode(int val) { this.val = val; }\n *     TreeNode(int val, TreeNode left, TreeNode right) {\n *         this.val = val;\n *         this.left = left;\n *         this.right = right;\n *     }\n * }\n */\n/**\n * 定义 Solution 类，用于计算二叉树中从根到叶子节点路径所代表的数字之和。\n */\nclass Solution {\n    int res = 0; // 初始化用于存储总和的变量\n\n    /**\n     * 计算二叉树中从根到叶子节点路径的数字总和。\n     *\n     * @param root 二叉树的根节点\n     * @return 从根到叶子节点路径所代表的数字之和\n     */\n    public int sumNumbers(TreeNode root) {\n        help(root, 0); // 调用辅助函数，初始累加值为0\n        return res; // 返回最终结果\n    }\n\n    /**\n     * 递归方法，用于计算从根到当前节点路径上的数字总和。\n     *\n     * @param root 当前二叉树节点\n     * @param k 从根节点到当前节点的累加值\n     */\n    public void help(TreeNode root, int k) {\n        if (root == null) {\n            return; // 基本情况：如果当前节点为空，直接返回\n        }\n        \n        int sum = k * 10 + root.val; // 计算从根到当前节点的累加值\n        \n        // 如果是叶子节点，将路径上的值累积到总和中\n        if (root.left == null && root.right == null) {\n            res += sum;\n        }\n        \n        // 递归调用左右子节点，并传递更新后的累加值\n        help(root.left, sum);\n        help(root.right, sum);\n    }\n}\n```\n\n## 104. 二叉树的最大深度\nhttps://leetcode.cn/problems/maximum-depth-of-binary-tree/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个二叉树 root ，返回其最大深度。\n\n二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。\n\n\n\n>示例 1：\n>\n>\n>![二叉树的最大深度.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%9C%80%E5%A4%A7%E6%B7%B1%E5%BA%A6.png)\n>\n>\n>输入：root = [3,9,20,null,null,15,7]\n>\n>输出：3\n\n>示例 2：\n>\n>输入：root = [1,null,2]\n>\n>输出：2\n\n\n提示：\n\n- 树中节点的数量在 [0, 10^4] 区间内。\n- -100 <= Node.val <= 100\n\n```java\n/**\n * Definition for a binary tree node.\n * public class TreeNode {\n *     int val;\n *     TreeNode left;\n *     TreeNode right;\n *     TreeNode() {}\n *     TreeNode(int val) { this.val = val; }\n *     TreeNode(int val, TreeNode left, TreeNode right) {\n *         this.val = val;\n *         this.left = left;\n *         this.right = right;\n *     }\n * }\n */\nclass Solution {\n    /**\n     * 计算二叉树的最大深度。\n     * 二叉树的最大深度定义为从根节点到最远叶子节点的最长路径上的节点数。\n     *\n     * @param root 二叉树的根节点，如果树为空，则返回0。\n     * @return 返回二叉树的最大深度。\n     */\n    public int maxDepth(TreeNode root) {\n        // 如果根节点为空，表示树为空，深度为0。\n        if(root==null){\n            return 0;\n        }\n        // 递归计算左子树的最大深度。\n        int left = maxDepth(root.left);\n        // 递归计算右子树的最大深度。\n        int right = maxDepth(root.right);\n        // 返回左子树和右子树中较大的深度，并加1表示当前根节点的深度。\n        return Math.max(left,right)+1;\n    }\n}\n```\n\n## 101. 对称二叉树\nhttps://leetcode.cn/problems/symmetric-tree/description/?envType=study-plan-v2&envId=top-interview-150\n\n\n给你一个二叉树的根节点 root ， 检查它是否轴对称。\n\n\n\n>示例 1：\n>\n>![对称二叉树1.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E5%AF%B9%E7%A7%B0%E4%BA%8C%E5%8F%89%E6%A0%911.png)\n>\n>输入：root = [1,2,2,3,4,4,3]\n>\n>输出：true\n\n>示例 2：\n>\n>![对称二叉树2.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E5%AF%B9%E7%A7%B0%E4%BA%8C%E5%8F%89%E6%A0%912.png)\n>\n>输入：root = [1,2,2,null,3,null,3]\n>\n>输出：false\n\n\n提示：\n\n- 树中节点数目在范围 [1, 1000] 内\n- -100 <= Node.val <= 100\n\n```java\n/**\n * Definition for a binary tree node.\n * public class TreeNode {\n *     int val;\n *     TreeNode left;\n *     TreeNode right;\n *     TreeNode() {}\n *     TreeNode(int val) { this.val = val; }\n *     TreeNode(int val, TreeNode left, TreeNode right) {\n *         this.val = val;\n *         this.left = left;\n *         this.right = right;\n *     }\n * }\n */\n/**\n * 判断一个树是否为对称二叉树。\n * 对称二叉树的定义是：如果一个树的左子树和右子树在结构上对称（镜像），并且对应节点的值相等，则这棵树是对称的。\n */\npublic class Solution {\n    /**\n     * 主要的判断函数，检查给定的根节点是否构成对称二叉树。\n     * \n     * @param root 根节点，用于开始检查对称性的递归过程。\n     * @return 如果树是对称的，则返回true；否则返回false。\n     */\n    public boolean isSymmetric(TreeNode root) {\n        // 如果根节点为空，空树自然是对称的\n        if(root==null){\n            return true;\n        }\n        // 调用辅助函数，检查左子树和右子树是否对称\n        return help(root.left,root.right);\n    }\n\n    /**\n     * 辅助递归函数，用于真正检查左右子树的对称性。\n     * \n     * @param left  左子树的当前节点。\n     * @param right 右子树的当前节点。\n     * @return 如果左右子树在当前节点及其子节点上对称，则返回true；否则返回false。\n     */\n    public boolean help(TreeNode left,TreeNode right){\n        // 如果左右子节点都为空，说明对称性成立\n        if(left == null && right == null){\n            return true;\n        }\n        // 如果只有一个子节点为空，或者节点值不相等，对称性不成立\n        if(left == null || right==null || left.val != right.val){\n            return false;\n        }\n        // 递归检查左右子节点的子节点是否对称\n        return help(left.left,right.right) && help(left.right,right.left);\n    }\n}\n```\n\n## 144. 二叉树的前序遍历\nhttps://leetcode.cn/problems/binary-tree-preorder-traversal/description/\n\n给你二叉树的根节点 root ，返回它节点值的 前序 遍历。\n\n \n\n>示例 1：\n>\n>![alt text](../img/数据结构和算法/二叉树的前序遍历1.png)\n>\n>输入：root = [1,null,2,3]\n>\n>输出：[1,2,3]\n\n>示例 2：\n>\n>输入：root = []\n>\n>输出：[]\n\n>示例 3：\n>\n>输入：root = [1]\n>\n>输出：[1]\n\n>示例 4：\n>\n>![alt text](../img/数据结构和算法/二叉树的前序遍历2.png)\n>\n>输入：root = [1,2]\n>\n>输出：[1,2]\n\n>示例 5：\n>\n>![alt text](../img/数据结构和算法/二叉树的前序遍历3.png)\n>\n>输入：root = [1,null,2]\n>\n>输出：[1,2]\n \n\n提示：\n\n- 树中节点数目在范围 [0, 100] 内\n- -100 <= Node.val <= 100\n\n```java\n/**\n * Definition for a binary tree node.\n * public class TreeNode {\n *     int val;\n *     TreeNode left;\n *     TreeNode right;\n *     TreeNode() {}\n *     TreeNode(int val) { this.val = val; }\n *     TreeNode(int val, TreeNode left, TreeNode right) {\n *         this.val = val;\n *         this.left = left;\n *         this.right = right;\n *     }\n * }\n */\nclass Solution {\n    public List<Integer> preorderTraversal(TreeNode root) {\n        List<Integer> res = new ArrayList<>();\n        if(root==null){\n            return res;\n        }\n        Deque<TreeNode> dq = new LinkedList<>();\n        dq.push(root);\n        while(!dq.isEmpty()){\n            TreeNode n = dq.pop();\n            res.add(n.val);\n             if(n.right!=null){\n                dq.push(n.right);\n            }\n            if(n.left!=null){\n                dq.push(n.left);\n            }\n           \n        }\n        return res;\n    }\n}\n```\n\n## 110. 平衡二叉树\nhttps://leetcode.cn/problems/balanced-binary-tree/description/\n\n给定一个二叉树，判断它是否是 \n平衡二叉树\n  \n\n \n\n>示例 1：\n>\n>![alt text](../img/数据结构和算法/平衡二叉树1.png)\n>\n>输入：root = [3,9,20,null,null,15,7]\n>\n>输出：true\n\n>示例 2：\n>\n>![alt text](../img/数据结构和算法/平衡二叉树2.png)\n>\n>输入：root = [1,2,2,3,3,null,null,4,4]\n>\n>输出：false\n\n>示例 3：\n>\n>输入：root = []\n>\n>输出：true\n \n\n提示：\n\n- 树中的节点数在范围 [0, 5000] 内\n- -10^4 <= Node.val <= 10^4\n\n```java\n/**\n * Definition for a binary tree node.\n * public class TreeNode {\n *     int val;\n *     TreeNode left;\n *     TreeNode right;\n *     TreeNode() {}\n *     TreeNode(int val) { this.val = val; }\n *     TreeNode(int val, TreeNode left, TreeNode right) {\n *         this.val = val;\n *         this.left = left;\n *         this.right = right;\n *     }\n * }\n */\n/**\n * 判断二叉树是否为平衡二叉树。\n * 平衡二叉树的定义是：对于任意节点，其左子树和右子树的高度差的绝对值不超过1，并且左右子树都是平衡二叉树。\n */\npublic class Solution {\n\n    /**\n     * 主要函数，判断给定的根节点是否属于一个平衡二叉树。\n     * @param root 二叉树的根节点\n     * @return 如果树是平衡的，返回true；否则返回false。\n     */\n    public boolean isBalanced(TreeNode root) {\n        // 调用辅助函数判断根节点及其子树是否平衡\n        return help(root) >= 0;\n    }\n\n    /**\n     * 辅助函数，递归地判断以当前节点为根的子树是否平衡，并返回子树的高度。\n     * @param root 当前子树的根节点\n     * @return 如果子树是平衡的，返回子树的高度；否则返回-1。\n     */\n    public int help(TreeNode root){\n        // 如果当前节点为空，子树高度为0\n        if(root == null){\n            return 0;\n        }\n        // 递归计算左子树的高度\n        int left = help(root.left);\n        // 递归计算右子树的高度\n        int right = help(root.right);\n        // 如果左右子树都是平衡的，并且高度差的绝对值不超过1，则当前子树也是平衡的\n        if(left>=0 && right >=0 && Math.abs(left-right) <= 1){\n            // 返回当前子树的高度，即左右子树中较高的高度加1\n            return Math.max(left,right)+1;\n        }\n        // 如果当前子树不平衡，返回-1\n        return -1;\n    }\n}\n```\n\n## 39. 组合总和\nhttps://leetcode.cn/problems/combination-sum/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ，找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ，并以列表形式返回。你可以按 任意顺序 返回这些组合。\n\ncandidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同，则两种组合是不同的。 \n\n对于给定的输入，保证和为 target 的不同组合数少于 150 个。\n\n \n\n>示例 1：\n>\n>输入：candidates = [2,3,6,7], target = 7\n>\n>输出：[[2,2,3],[7]]\n>\n>解释：\n>\n>2 和 3 可以形成一组候选，2 + 2 + 3 = 7 。注意 2 可以使用多次。\n>\n>7 也是一个候选， 7 = 7 。\n>\n>仅有这两种组合。\n\n>示例 2：\n>\n>输入: candidates = [2,3,5], target = 8\n>\n>输出: [[2,2,2,2],[2,3,3],[3,5]]\n>\n>示例 3：\n>\n>输入: candidates = [2], target = 1\n>\n>输出: []\n \n\n提示：\n\n- 1 <= candidates.length <= 30\n- 2 <= candidates[i] <= 40\n- candidates 的所有元素 互不相同\n- 1 <= target <= 40\n\n```java\nclass Solution {\n    // 结果集合，用于存储所有满足条件的组合\n    List<List<Integer>> res = new ArrayList<>();\n    // 辅助栈，用于记录当前搜索路径上的元素\n    Deque<Integer> list = new LinkedList<>();\n\n    /**\n     * 计算组合总和\n     * @param candidates 无重复元素的整数数组候选人\n     * @param target 目标整数和\n     * @return 所有可能的组合列表\n     */\n    public List<List<Integer>> combinationSum(int[] candidates, int target) {\n        // 获取候选人数组的长度\n        int len = candidates.length;\n        // 如果数组长度小于0，直接返回空结果（实际上此条件不会触发，仅为逻辑完整性考虑）\n        if (len < 0) {\n            return res;\n        }\n        // 对候选人数组进行排序，便于剪枝操作\n        Arrays.sort(candidates);\n        // 从第一个元素开始深度优先搜索\n        dfs(candidates, target, 0);\n        // 返回所有满足条件的组合\n        return res;\n    }\n\n    /**\n     * 深度优先搜索实现函数\n     * @param candidates 候选人数组\n     * @param target 剩余需要达到的目标和\n     * @param index 当前搜索的起始下标，避免重复使用同一层级的元素\n     */\n    public void dfs(int[] candidates, int target, int index) {\n        // 如果目标和为0，说明找到了一个合法组合\n        if (target == 0) {\n            // 将当前组合复制并添加到结果列表中\n            res.add(new ArrayList<>(list));\n            return;\n        }\n        // 遍历数组，从index开始搜索，允许重复使用元素但同一层级不重复选择\n        for (int i = index; i < candidates.length; i++) {\n            // 如果目标减去当前元素值小于0，说明此路不通，直接结束本次循环\n            if (target - candidates[i] < 0) {\n                break;\n            }\n            // 选择当前元素，将其添加到路径中\n            list.addLast(candidates[i]);\n            // 递归搜索剩余部分，由于可以重复使用元素，下一轮搜索仍然从i开始\n            dfs(candidates, target - candidates[i], i);\n            // 回溯，移除刚加入的元素，尝试下一个选择\n            list.removeLast();\n        }\n    }\n}\n```\n\n## 543. 二叉树的直径\nhttps://leetcode.cn/problems/diameter-of-binary-tree/description/\n\n给你一棵二叉树的根节点，返回该树的 直径 。\n\n二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root 。\n\n两节点之间路径的 长度 由它们之间边数表示。\n\n \n\n>示例 1：\n>\n>![alt text](../img/数据结构和算法/二叉树的直径.png)\n>\n>\n>输入：root = [1,2,3,4,5]\n>\n>输出：3\n>\n>解释：3 ，取路径 [4,2,1,3] 或 [5,2,1,3] 的长度。\n\n>示例 2：\n>\n>输入：root = [1,2]\n>\n>输出：1\n \n\n提示：\n\n树中节点数目在范围 [1, 10^4] 内\n-100 <= Node.val <= 100\n\n```java\n/**\n * Definition for a binary tree node.\n * public class TreeNode {\n *     int val;\n *     TreeNode left;\n *     TreeNode right;\n *     TreeNode() {}\n *     TreeNode(int val) { this.val = val; }\n *     TreeNode(int val, TreeNode left, TreeNode right) {\n *         this.val = val;\n *         this.left = left;\n *         this.right = right;\n *     }\n * }\n */\nclass Solution {\n    int max= 0;\n    public int diameterOfBinaryTree(TreeNode root) {\n        help(root);\n        return max;\n    }\n\n    public int help(TreeNode root){\n        if(root == null){\n            return 0;\n        }\n        int left = help(root.left);\n        int right = help(root.right);\n        max= Math.max(max,left+right);\n        return Math.max(left,right)+1;\n    }\n}\n```\n\n## 470. 用 Rand7() 实现 Rand10()\nhttps://leetcode.cn/problems/implement-rand10-using-rand7/description/\n\n给定方法 rand7 可生成 [1,7] 范围内的均匀随机整数，试写一个方法 rand10 生成 [1,10] 范围内的均匀随机整数。\n\n你只能调用 rand7() 且不能调用其他方法。请不要使用系统的 Math.random() 方法。\n\n每个测试用例将有一个内部参数 n，即你实现的函数 rand10() 在测试时将被调用的次数。请注意，这不是传递给 rand10() 的参数。\n\n \n\n>示例 1:\n>\n>输入: 1\n>\n>输出: [2]\n\n>示例 2:\n>\n>输入: 2\n>\n>输出: [2,8]\n\n>示例 3:\n>\n>输入: 3\n>\n>输出: [3,8,10]\n \n\n提示:\n\n- 1 <= n <= 10^5\n \n\n进阶:\n\nrand7()调用次数的 期望值 是多少 ?\n你能否尽量少调用 rand7() ?\n\n```java\n/**\n * The rand7() API is already defined in the parent class SolBase.\n * public int rand7();\n * @return a random integer in the range 1 to 7\n */\n/**\n * 使用 rand7() 函数生成 rand10() 函数的解决方案。\n * 继承自SolBase类，提供了一种通过 rand7() 函数来实现 rand10() 函数的方法。\n */\nclass Solution extends SolBase {\n    /**\n     * 生成一个1到10之间的随机整数。\n     * 通过精心设计的算法，使用两次调用rand7()函数的结果来模拟rand10()函数的行为。\n     * \n     * @return 1到10之间的随机整数。\n     */\n    public int rand10() {\n        // 生成第一个rand7()结果，用于后续计算。\n        int a = rand7();\n        // 生成第二个rand7()结果，同样用于后续计算。\n        int b = rand7();\n\n        // 确保a不为7，因为7会导致某些情况无法正确生成所需的随机数。\n        while(a==7){\n            a=rand7();\n        }\n        // 确保b不大于5，这样可以通过(a%2)的结果将可能的值范围缩小到0到9之间。\n        while(b>5){\n            b = rand7();\n        }\n        // 根据a和b的值，通过运算得到1到10之间的随机整数。\n        return ((a%2) == 0 ?0:5)+b;\n    }\n}\n```\n\n## 48. 旋转图像\nhttps://leetcode.cn/problems/rotate-image/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。\n\n你必须在 原地 旋转图像，这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。\n\n>示例 1：\n>\n>![旋转图像1.png](..%2Fimg%2F%CB%E3%B7%A8%2F%D0%FD%D7%AA%CD%BC%CF%F11.png)\n>\n>输入：matrix = [[1,2,3],[4,5,6],[7,8,9]]\n>\n>输出：[[7,4,1],[8,5,2],[9,6,3]]\n\n>示例 2：\n>\n>![旋转图像2.png](..%2Fimg%2F%CB%E3%B7%A8%2F%D0%FD%D7%AA%CD%BC%CF%F12.png)\n>\n>输入：matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]\n>\n>输出：[[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]\n\n\n提示：\n\n- n == matrix.length == matrix[i].length\n- 1 <= n <= 20\n- -1000 <= matrix[i][j] <= 1000\n\n```java\n/**\n * 该类提供了一个方法来旋转二维矩阵。\n * 矩阵旋转是将矩阵的行与列互换，并翻转矩阵的左右两侧，以实现顺时针旋转90度的效果。\n */\nclass Solution {\n    /**\n     * 旋转二维矩阵。\n     * \n     * @param matrix 一个二维整数数组，表示待旋转的矩阵。\n     *               矩阵的旋转是在原地进行，即不使用额外的存储空间。\n     */\n    public void rotate(int[][] matrix) {\n        // 获取矩阵的长度，矩阵为正方形，因此只需获取一次长度\n        int len = matrix.length;\n\n        // 第一步：通过交换每行的元素与对应列的元素，实现行与列的互换\n        // 这一步是为了解决矩阵旋转中的对角线元素交换问题\n        for(int i=0;i<len;i++){\n            for(int j=0;j<i;j++){\n                // 临时变量用于存储待交换的元素\n                int temp = matrix[j][i];\n                // 交换行i和列j的元素\n                matrix[j][i] = matrix[i][j];\n                matrix[i][j] = temp;\n            }\n        }\n\n        // 第二步：通过翻转每行的元素，实现矩阵的左右翻转\n        // 这一步是为了解决矩阵旋转中的水平翻转问题\n        for(int i=0;i<len;i++){\n            for(int j=0;j<len/2;j++){\n                // 临时变量用于存储待交换的元素\n                int temp = matrix[i][len-j-1];\n                // 翻转行i中的元素\n                matrix[i][len-j-1] = matrix[i][j];\n                matrix[i][j] = temp;\n            }\n        }\n    }\n}\n```\n\n## 98. 验证二叉搜索树\nhttps://leetcode.cn/problems/validate-binary-search-tree/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个二叉树的根节点 root ，判断其是否是一个有效的二叉搜索树。\n\n有效 二叉搜索树定义如下：\n\n- 节点的左子树只包含 小于 当前节点的数。\n- 节点的右子树只包含 大于 当前节点的数。\n- 所有左子树和右子树自身必须也是二叉搜索树。\n \n\n>示例 1：\n>\n>![alt text](../img/数据结构和算法/验证二叉搜索树1.png)\n>\n>输入：root = [2,1,3]\n>\n>输出：true\n\n>示例 2：\n>\n>![alt text](../img/数据结构和算法/验证二叉搜索树2.png)\n>\n>输入：root = [5,1,4,null,null,3,6]\n>\n>输出：false\n>\n>解释：根节点的值是 5 ，但是右子节点的值是 4 。\n \n\n提示：\n\n- 树中节点数目范围在[1, 10^4] 内\n- -2^31 <= Node.val <= 2^31 - 1\n\n```java\n/**\n * Definition for a binary tree node.\n * public class TreeNode {\n *     int val;\n *     TreeNode left;\n *     TreeNode right;\n *     TreeNode(int x) { val = x; }\n * }\n */\nclass Solution {\n    private long pre = Long.MIN_VALUE; // 用于记录前一个遍历节点的值\n\n    public boolean isValidBST(TreeNode root) {\n        if (root == null) return true;\n\n        // 首先检查左子树是否为二叉搜索树\n        if (!isValidBST(root.left)) {\n            return false;\n        }\n\n        // 检查当前节点的值是否大于前一个节点的值\n        if (root.val <= pre) {\n            return false;\n        }\n        pre = root.val; // 更新pre为当前节点的值，供后续节点比较使用\n\n        // 最后检查右子树是否为二叉搜索树\n        return isValidBST(root.right);\n    }\n}\n```\n\n## 34. 在排序数组中查找元素的第一个和最后一个位置\nhttps://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个按照非递减顺序排列的整数数组 nums，和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。\n\n如果数组中不存在目标值 target，返回 [-1, -1]。\n\n你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。\n\n \n\n>示例 1：\n>\n>输入：nums = [5,7,7,8,8,10], target = 8\n>\n>输出：[3,4]\n\n>示例 2：\n>\n>输入：nums = [5,7,7,8,8,10], target = 6\n>\n>输出：[-1,-1]\n\n>示例 3：\n>\n>输入：nums = [], target = 0\n>\n>输出：[-1,-1]\n \n\n提示：\n\n- 0 <= nums.length <= 10^5\n- -10^9 <= nums[i] <= 10^9\n- nums 是一个非递减数组\n- -10^9 <= target <= 10^9\n\n```java\n/**\n * 在排序数组中查找给定目标值的起始和结束位置。\n * 该类提供了一个方法来解决在排序数组中查找元素范围的问题。\n */\nclass Solution {\n    /**\n     * 在排序数组中查找目标值的起始和结束位置。\n     * \n     * @param nums 排序数组，其中可能包含目标值。\n     * @param target 目标值，我们需要找到它在数组中的起始和结束位置。\n     * @return 包含两个整数的数组，分别表示目标值在数组中的起始和结束位置。\n     *         如果数组中不存在目标值，则返回 [-1, -1]。\n     */\n    public int[] searchRange(int[] nums, int target) {\n        int left = 0;\n        int right = nums.length - 1;\n        // 使用二分查找来定位目标值的初始位置\n        while (left <= right) {\n            int mid = left + (right - left) / 2;\n            if (target == nums[mid]) {\n                // 一旦找到目标值，向左和向右扫描以确定范围\n                int l = mid, r = mid;\n                // 向左扫描，找到起始位置\n                while (l > 0 && nums[l - 1] == target) {\n                    l--;\n                }\n                // 向右扫描，找到结束位置\n                while (r < nums.length - 1 && nums[r + 1] == target) {\n                    r++;\n                }\n                // 返回目标值的起始和结束位置\n                return new int[]{l, r};\n            } else if (nums[mid] > target) {\n                // 如果中间值大于目标值，缩小右边界\n                right = mid - 1;\n            } else {\n                // 如果中间值小于目标值，增大左边界\n                left = mid + 1;\n            }\n        }\n        // 如果未找到目标值，返回 [-1, -1]\n        return new int[]{-1, -1};\n    }\n}\n```\n\n## 394. 字符串解码\nhttps://leetcode.cn/problems/decode-string/description/\n\n给定一个经过编码的字符串，返回它解码后的字符串。\n\n编码规则为: k[encoded_string]，表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。\n\n你可以认为输入字符串总是有效的；输入字符串中没有额外的空格，且输入的方括号总是符合格式要求的。\n\n此外，你可以认为原始数据不包含数字，所有的数字只表示重复的次数 k ，例如不会出现像 3a 或 2[4] 的输入。\n\n \n\n>示例 1：\n>\n>输入：s = \"3[a]2[bc]\"\n>\n>输出：\"aaabcbc\"\n\n>示例 2：\n>\n>输入：s = \"3[a2[c]]\"\n>\n>输出：\"accaccacc\"\n\n>示例 3：\n>\n>输入：s = \"2[abc]3[cd]ef\"\n>\n>输出：\"abcabccdcdcdef\"\n\n>示例 4：\n>\n>输入：s = \"abc3[cd]xyz\"\n>\n>输出：\"abccdcdcdxyz\"\n \n\n提示：\n\n- 1 <= s.length <= 30\n- s 由小写英文字母、数字和方括号 '[]' 组成\n- s 保证是一个 有效 的输入。\n- s 中所有整数的取值范围为 [1, 300] \n\n```java\nclass Solution {\n    public String decodeString(String s) {\n        Deque<Character> dq = new LinkedList<>();\n\n        for(char c:s.toCharArray()){\n            if(c != ']'){\n                // 把所有的字母push进去，除了]\n                dq.push(c);\n            }else{\n                 //step 1: 取出[] 内的字符串\n                StringBuilder sb = new StringBuilder();\n                while(!dq.isEmpty() && Character.isLetter(dq.peek())){\n                    sb.insert(0,dq.pop());\n                }\n                //[ ]内的字符串\n                String sub = sb.toString();\n                 // 去除[\n                dq.pop();\n                 //step 2: 获取倍数数字\n                sb = new StringBuilder();\n                while(!dq.isEmpty() && Character.isDigit(dq.peek())){\n                    sb.insert(0,dq.pop());\n                }\n                 //倍数\n                int cnt = Integer.valueOf(sb.toString());\n                 //step 3: 根据倍数把字母再push回去\n                while(cnt-->0){\n                    for(char ch:sub.toCharArray()){\n                        dq.push(ch);\n                    }\n                }\n            }\n        }\n        //把栈里面所有的字母取出来\n        StringBuilder retv = new StringBuilder();\n        while(!dq.isEmpty()){\n            retv.insert(0,dq.pop());\n        }\n        return retv.toString();\n    }\n}\n```\n\n## 113. 路径总和 II\nhttps://leetcode.cn/problems/path-sum-ii/description/\n\n给你二叉树的根节点 root 和一个整数目标和 targetSum ，找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。\n\n叶子节点 是指没有子节点的节点。\n\n \n\n>示例 1：\n>\n>\n>输入：root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22\n>\n>输出：[[5,4,11,2],[5,8,4,5]]\n\n>示例 2：\n>\n>\n>输入：root = [1,2,3], targetSum = 5\n>\n>输出：[]\n\n>示例 3：\n>\n>输入：root = [1,2], targetSum = 0\n>\n>输出：[]\n \n\n提示：\n\n- 树中节点总数在范围 [0, 5000] 内\n- -1000 <= Node.val <= 1000\n- -1000 <= targetSum <= 1000\n\n```java\n/**\n * Definition for a binary tree node.\n * public class TreeNode {\n *     int val;\n *     TreeNode left;\n *     TreeNode right;\n *     TreeNode() {}\n *     TreeNode(int val) { this.val = val; }\n *     TreeNode(int val, TreeNode left, TreeNode right) {\n *         this.val = val;\n *         this.left = left;\n *         this.right = right;\n *     }\n * }\n */\nclass Solution {\n    List<List<Integer>> res= new ArrayList<>();\n    List<Integer> list = new ArrayList<>();\n    public List<List<Integer>> pathSum(TreeNode root, int targetSum) {\n        if(root == null){\n            return res;\n        }\n        help(root,targetSum,0);\n        return res;\n    }\n\n        /**\n     * 寻找从根节点到叶子节点路径和等于指定目标和的所有路径。\n     * \n     * @param root 二叉树的根节点\n     * @param targetSum 目标和\n     * @param sum 当前路径的和\n     */\n    public void help(TreeNode root, int targetSum, int sum) {\n        // 如果当前节点为空，则返回\n        if (root == null) {\n            return;\n        }\n        // 获取当前节点的值\n        int rootVal = root.val;\n        // 将当前节点的值加入到路径列表中\n        list.add(rootVal);\n        // 更新当前路径的和\n        sum += rootVal;\n        // 如果当前节点是叶子节点且当前路径和等于目标和，则将当前路径加入到结果列表中\n        if (root.left == null && root.right == null && sum == targetSum) {\n            res.add(new ArrayList<>(list));\n        }\n        // 递归处理左子树\n        help(root.left, targetSum, sum);\n        // 递归处理右子树\n        help(root.right, targetSum, sum);\n        // 回溯，移除当前节点的值 from 路径列表\n        list.remove(list.size() - 1);\n    }\n}\n```\n\n## 240. 搜索二维矩阵 II\nhttps://leetcode.cn/problems/search-a-2d-matrix-ii/description/\n\n编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性：\n\n- 每行的元素从左到右升序排列。\n- 每列的元素从上到下升序排列。\n \n\n>示例 1：\n>\n>![alt text](../img/数据结构和算法/搜索二维矩阵2-1.png)\n>\n>输入：matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 5\n>\n>输出：true\n\n>示例 2：\n>\n>![alt text](../img/数据结构和算法/搜索二维矩阵2-2.png)\n>\n>输入：matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 20\n>\n>输出：false\n \n\n提示：\n\n- m == matrix.length\n- n == matrix[i].length\n- 1 <= n, m <= 300\n- -10^9 <= matrix[i][j] <= 10^9\n- 每行的所有元素从左到右升序排列\n- 每列的所有元素从上到下升序排列\n- -10^9 <= target <= 10^9\n\n```java\n/**\n * 解决在二维矩阵中查找特定目标值的问题。\n * 矩阵每一行都是升序排列，每一列也都是升序排列。\n */\nclass Solution {\n    /**\n     * 在二维矩阵中搜索目标值。\n     * \n     * @param matrix 二维整数矩阵，每一行和每一列都是升序排列。\n     * @param target 要搜索的目标值。\n     * @return 如果目标值存在于矩阵中，返回true；否则返回false。\n     */\n    public boolean searchMatrix(int[][] matrix, int target) {\n        // 矩阵的行数\n        int m = matrix.length;\n        // 遍历每一行\n        for(int[] row:matrix){\n            // 在当前行中搜索目标值\n            int index = search(row,target);\n            // 如果找到目标值，返回true\n            if(index >=0){\n                return true;\n            }\n        }\n        // 如果所有行都搜索完毕仍未找到目标值，返回false\n        return false;\n    }\n\n    /**\n     * 在有序数组中搜索目标值。\n     * \n     * @param array 有序整数数组。\n     * @param target 要搜索的目标值。\n     * @return 如果目标值存在于数组中，返回其索引；否则返回-1。\n     */\n    public int search(int[] array,int target){\n        // 定义数组的左右边界\n        int left = 0, right = array.length -1;\n        // 使用二分查找法搜索目标值\n        while(left<=right){\n            // 计算中间位置的索引\n            int mid = left+ (right-left)/2;\n            // 如果中间位置的值等于目标值，返回中间位置的索引\n            if(array[mid] == target){\n                return mid;\n            // 如果中间位置的值小于目标值，忽略左半部分，更新左边界\n            }else if(array[mid] < target){\n                left = mid+1;\n            // 如果中间位置的值大于目标值，忽略右半部分，更新右边界\n            }else{\n                right = mid-1;\n            }\n        }\n        // 如果未找到目标值，返回-1\n        return -1;\n    }\n}\n```\n\n## 64. 最小路径和\n给定一个包含非负整数的 m x n 网格 grid ，请找出一条从左上角到右下角的路径，使得路径上的数字总和为最小。\n\n说明：每次只能向下或者向右移动一步。\n\n \n\n>示例 1：\n>\n>\n>输入：grid = [[1,3,1],[1,5,1],[4,2,1]]\n>\n>输出：7\n>\n>解释：因为路径 1→3→1→1→1 的总和最小。\n\n>示例 2：\n>\n>输入：grid = [[1,2,3],[4,5,6]]\n>\n>输出：12\n \n\n提示：\n\n- m == grid.length\n- n == grid[i].length\n- 1 <= m, n <= 200\n- 0 <= grid[i][j] <= 200\n\n```java\n/**\n * 解决最小路径和问题的类。\n * 该类提供了一个方法来计算在一个给定的二维网格中，从左上角到右下角的最小路径和。\n * 网格中的每个单元格包含一个非负整数，路径和是沿途经过的单元格值的总和。\n */\nclass Solution {\n    /**\n     * 计算最小路径和。\n     * 使用动态规划方法，从左上角开始，逐步向右下角计算到达每个单元格的最小路径和。\n     * \n     * @param grid 二维网格，包含非负整数。\n     * @return 返回从左上角到右下角的最小路径和。\n     */\n    public int minPathSum(int[][] grid) {\n        // 获取网格的行数和列数\n        int m = grid.length;\n        int n = grid[0].length;\n\n        // 初始化一个数组用于存储到达每个列的最小路径和\n        int[] dp = new int[n];\n\n        // 遍历网格的每一行\n        for(int i=0;i<m;i++){\n            // 遍历每一行的每一列\n            for(int j=0;j<n;j++){\n                // 如果当前列是第一列，只能从上方到达，所以当前的最小路径和就是上方的最小路径和\n                if(j==0){\n                    dp[j] = dp[j];\n                }else if(i==0){ // 如果当前行是第一行，只能从左方到达，所以当前的最小路径和是左边的最小路径和\n                    dp[j] = dp[j-1];\n                }else{ // 如果既不在第一行也不在第一列，当前的最小路径和是从左边或上方到达的最小路径和中的较小值\n                    dp[j] = Math.min(dp[j],dp[j-1]);\n                }\n                // 将当前单元格的值加到到达当前单元格的最小路径和上\n                dp[j]+=grid[i][j];\n            }\n        }\n        // 返回到达右下角的最小路径和\n        return dp[n-1];\n    }\n}\n```\n\n## 221. 最大正方形\nhttps://leetcode.cn/problems/maximal-square/description/?envType=study-plan-v2&envId=top-interview-150\n\n在一个由 '0' 和 '1' 组成的二维矩阵内，找到只包含 '1' 的最大正方形，并返回其面积。\n\n \n\n>示例 1：\n>\n>![alt text](../img/数据结构和算法/最大正方形1.png)\n>\n>输入：matrix = [[\"1\",\"0\",\"1\",\"0\",\"0\"],[\"1\",\"0\",\"1\",\"1\",\"1\"],[\"1\",\"1\",\"1\",\"1\",\"1\"],[\"1\",\"0\",\"0\",\"1\",\"0\"]]\n>\n>输出：4\n\n>示例 2：\n>\n>![alt text](../img/数据结构和算法/最大正方形2.png)\n>\n>输入：matrix = [[\"0\",\"1\"],[\"1\",\"0\"]]\n>\n>输出：1\n\n>示例 3：\n>\n>输入：matrix = [[\"0\"]]\n>\n>输出：0\n \n\n提示：\n\n- m == matrix.length\n- n == matrix[i].length\n- 1 <= m, n <= 300\n- matrix[i][j] 为 '0' 或 '1'\n\n```java\n/**\n * 此类提供了一个解决方案，用于计算给定矩阵中最大正方形的边长，该正方形由'1'字符组成。\n */\nclass Solution {\n    /**\n     * 计算给定二进制字符矩阵中最大正方形的边长。\n     * \n     * @param matrix 一个二维字符数组，其中'1'表示白色单元格，'0'表示黑色单元格。\n     * @return 返回矩阵中最大全为'1'的正方形的边长，如果不存在这样的正方形，则返回0。\n     */\n    public int maximalSquare(char[][] matrix) {\n        int m = matrix.length; // 获取矩阵的行数\n        int n = matrix[0].length; // 获取矩阵的列数\n\n        // 初始化一个动态规划数组，用于存储以(i, j)为右下角的最大正方形边长\n        int[][] dp = new int[m+1][n+1];\n        int max = 0; // 用于记录最大正方形的边长\n\n        // 遍历矩阵，计算最大正方形的边长\n        for(int i=1; i<=m; i++){\n            for(int j=1; j<=n; j++){\n                // 当当前位置为'1'时，更新dp值并计算最大边长\n                if(matrix[i-1][j-1]=='1'){\n                    dp[i][j] = Math.min(dp[i-1][j-1], Math.min(dp[i-1][j], dp[i][j-1])) + 1;\n                    max = Math.max(max, dp[i][j]);\n                }\n            }\n        }\n\n        // 返回最大正方形的面积（边长的平方）\n        return max*max;\n    }\n}\n```\n\n## 162. 寻找峰值\nhttps://leetcode.cn/problems/find-peak-element/description/?envType=study-plan-v2&envId=top-interview-150\n\n峰值元素是指其值严格大于左右相邻值的元素。\n\n给你一个整数数组 nums，找到峰值元素并返回其索引。数组可能包含多个峰值，在这种情况下，返回 任何一个峰值 所在位置即可。\n\n你可以假设 nums[-1] = nums[n] = -∞ 。\n\n你必须实现时间复杂度为 O(log n) 的算法来解决此问题。\n\n \n\n>示例 1：\n>\n>输入：nums = [1,2,3,1]\n>\n>输出：2\n>\n>解释：3 是峰值元素，你的函数应该返回其索引 2。\n\n>示例 2：\n>\n>输入：nums = [1,2,1,3,5,6,4]\n>\n>输出：1 或 5 \n>\n>解释：你的函数可以返回索引 1，其峰值元素为 2；\n     或者返回索引 5， 其峰值元素为 6。\n \n\n提示：\n\n- 1 <= nums.length <= 1000\n- -2^31 <= nums[i] <= 2^31 - 1\n- 对于所有有效的 i 都有 nums[i] != nums[i + 1]\n\n```java\n/**\n * 寻找峰值元素的函数。\n * 峰值元素是大于其相邻元素的元素，数组两端视为负无穷大。\n * 实现时间复杂度为O(log n)的二分查找算法。\n *\n * @param nums 整型数组，其中包含至少一个峰值元素。\n * @return 峰值元素的索引位置。\n */\npublic int findPeakElement(int[] nums) {\n    if (nums == null || nums.length == 0) {\n        return -1; // 或根据题目要求抛出异常\n    }\n    \n    int left = 0, right = nums.length - 1;\n    \n    while (left < right) {\n        int mid = left + (right - left) / 2;\n        if (nums[mid] > nums[mid + 1]) {\n            // 下降趋势，峰值可能在左边，包括当前mid\n            right = mid;\n        } else {\n            // 上升趋势，峰值一定在右边，不包括当前mid\n            left = mid + 1;\n        }\n    }\n    \n    // 当left == right时，根据题设nums[-1] = nums[n] = -∞，则left位置为峰值\n    return left;\n}\n```\n\n## 14. 最长公共前缀\nhttps://leetcode.cn/problems/longest-common-prefix/description/?envType=study-plan-v2&envId=top-interview-150\n\n编写一个函数来查找字符串数组中的最长公共前缀。\n\n如果不存在公共前缀，返回空字符串 \"\"。\n\n>示例 1：\n>\n>输入：strs = [\"flower\",\"flow\",\"flight\"]\n>\n>输出：\"fl\"\n\n>示例 2：\n>\n>输入：strs = [\"dog\",\"racecar\",\"car\"]\n>\n>输出：\"\"\n>\n>解释：输入不存在公共前缀。\n\n\n提示：\n\n- 1 <= strs.length <= 200\n- 0 <= strs[i].length <= 200\n- strs[i] 仅由小写英文字母组成\n\n\n```java\n/**\n * Solution类用于解决找出一组字符串中的最长公共前缀的问题。\n */\nclass Solution {\n    /**\n     * 寻找字符串数组中所有字符串的最长公共前缀。\n     * \n     * @param strs 字符串数组，包含了需要比较的字符串。\n     * @return 返回最长公共前缀字符串。如果输入为null，则返回空字符串。\n     */\n    public String longestCommonPrefix(String[] strs) {\n        // 如果输入字符串数组为null，直接返回空字符串\n        if(strs == null){\n            return \"\";\n        }\n        \n        // 以数组第一个字符串作为初始的最长公共前缀\n        String tmp = strs[0];\n        \n        // 遍历数组中的每个字符串，与当前的最长公共前缀进行比较\n        for(String str:strs){\n            // 如果当前字符串不以最长公共前缀开头，则缩短最长公共前缀\n            while(!str.startsWith(tmp)){\n                // 如果最长公共前缀已经为空，则没有公共前缀，直接返回空字符串\n                if(tmp.length() == 0){\n                    return \"\";\n                }\n                // 缩短最长公共前缀，去掉最后一个字符\n                tmp = tmp.substring(0,tmp.length()-1);\n            }\n        }\n        // 循环结束后，返回最终确定的最长公共前缀\n        return tmp;\n    }\n}\n```\n\n## 128. 最长连续序列\nhttps://leetcode.cn/problems/longest-consecutive-sequence/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个未排序的整数数组 nums ，找出数字连续的最长序列（不要求序列元素在原数组中连续）的长度。\n\n请你设计并实现时间复杂度为 O(n) 的算法解决此问题。\n\n\n\n>示例 1：\n>\n>输入：nums = [100,4,200,1,3,2]\n>\n>输出：4\n>\n>解释：最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。\n\n>示例 2：\n>\n>输入：nums = [0,3,7,2,5,8,4,6,0,1]\n>\n>输出：9\n\n\n提示：\n\n- 0 <= nums.length <= 10^5\n- -10^9 <= nums[i] <= 10^9\n\n```java\n/**\n * 解决方案类，用于找出给定数组中最长的连续子序列的长度。\n */\nclass Solution {\n    /**\n     * 计算最长连续子序列的长度。\n     * \n     * @param nums 输入的整数数组。\n     * @return 返回最长连续子序列的长度。\n     */\n    public int longestConsecutive(int[] nums) {\n        // 使用HashSet存储数组元素，以便快速检查某个数是否存在。\n        Set<Integer> set = new HashSet<>();\n        for (int n : nums) {\n            set.add(n);\n        }\n        // 初始化最长连续子序列的长度为0。\n        int max = 0;\n        for (int n : nums) {\n            // 检查当前数的前一个数是否存在于HashSet中，如果不存在，则当前数可能是连续子序列的起点。\n            if (!set.contains(n - 1)) {\n                // 初始化当前连续子序列的长度为1。\n                int cnt = 1;\n                // 从当前数开始，向后连续检查直到找不到下一个连续的数。\n                int cur = n;\n                while (set.contains(cur + 1)) {\n                    cur += 1;\n                    cnt += 1;\n                }\n                // 更新最长连续子序列的长度。\n                max = Math.max(max, cnt);\n            }\n        }\n        // 返回最长连续子序列的长度。\n        return max;\n    }\n} \n```\n\n## 234. 回文链表\nhttps://leetcode.cn/problems/palindrome-linked-list/description/\n\n给你一个单链表的头节点 head ，请你判断该链表是否为回文链表。如果是，返回 true ；否则，返回 false 。\n\n \n\n>示例 1：\n>\n>![alt text](../img/数据结构和算法/回文链表1.png)\n>\n>输入：head = [1,2,2,1]\n>\n>输出：true\n\n>示例 2：\n>\n>![alt text](../img/数据结构和算法/回文链表2.png)\n>\n>输入：head = [1,2]\n>\n>输出：false\n \n\n提示：\n\n- 链表中节点数目在范围[1, 10^5] 内\n- 0 <= Node.val <= 9\n\n```java\n/**\n * Definition for singly-linked list.\n * public class ListNode {\n *     int val;\n *     ListNode next;\n *     ListNode() {}\n *     ListNode(int val) { this.val = val; }\n *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }\n * }\n */\n/**\n * 判断一个链表是否为回文结构。\n */\nclass Solution {\n    /**\n     * 判断给定链表是否为回文结构。\n     * \n     * @param head 链表的头节点\n     * @return 如果链表是回文结构，则返回true；否则返回false。\n     */\n    public boolean isPalindrome(ListNode head) {\n        // 找到链表中点\n        ListNode mid = findMid(head);\n        // 将中点之后的链表反转\n        ListNode re = reverse(mid);\n        // 比较前半部分和反转后的后半部分是否相同\n        while(head != null && re != null) {\n            if(head.val != re.val) {\n                return false;\n            }\n            head = head.next;\n            re = re.next;\n        }\n        return true;\n    }\n\n    /**\n     * 找到链表的中点节点。\n     * \n     * @param head 链表的头节点\n     * @return 链表的中点节点\n     */\n    public ListNode findMid(ListNode head) {\n        ListNode slow = head;\n        ListNode fast = head;\n        // 使用快慢指针找到链表中点\n        while(fast != null && fast.next != null) {\n            slow = slow.next;\n            fast = fast.next.next;\n        }\n        return slow;\n    }\n\n    /**\n     * 反转链表。\n     * \n     * @param head 需要反转的链表的头节点\n     * @return 反转后的链表的头节点\n     */\n    public ListNode reverse(ListNode head) {\n        ListNode pre = null;\n        ListNode cur = head;\n        ListNode next = null;\n        // 反转链表\n        while(cur != null) {\n            next = cur.next;\n            cur.next = pre;\n            pre = cur;\n            cur = next;\n        }\n        return pre;\n    }\n}\n```\n\n## 112. 路径总和\nhttps://leetcode.cn/problems/path-sum/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径，这条路径上所有节点值相加等于目标和 targetSum 。如果存在，返回 true ；否则，返回 false 。\n\n叶子节点 是指没有子节点的节点。\n\n\n\n>示例 1：\n>\n>![路径总和1.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E8%B7%AF%E5%BE%84%E6%80%BB%E5%92%8C1.png)\n>\n>输入：root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22\n>\n>输出：true\n>\n>解释：等于目标和的根节点到叶节点路径如上图所示。\n\n>示例 2：\n>\n>![路径总和2.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E8%B7%AF%E5%BE%84%E6%80%BB%E5%92%8C2.png)\n>\n>输入：root = [1,2,3], targetSum = 5\n>\n>输出：false\n>\n>解释：树中存在两条根节点到叶子节点的路径：\n>- (1 --> 2): 和为 3\n>- (1 --> 3): 和为 4\n>- 不存在 sum = 5 的根节点到叶子节点的路径。\n\n>示例 3：\n>\n>输入：root = [], targetSum = 0\n>\n>输出：false\n>\n>解释：由于树是空的，所以不存在根节点到叶子节点的路径。\n\n\n提示：\n\n- 树中节点的数目在范围 [0, 5000] 内\n- -1000 <= Node.val <= 1000\n- -1000 <= targetSum <= 1000\n\n```java\nclass Solution {\n    // 递归函数，判断是否存在从当前节点到叶子节点的路径，使得节点值之和等于目标和\n    public boolean hasPathSum(TreeNode root, int targetSum) {\n        // 如果当前节点为空，则返回 false\n        if(root == null){\n            return false;\n        }\n        // 如果当前节点是叶子节点，判断当前节点值是否等于目标和\n        if(root.left == null && root.right == null){\n            return root.val == targetSum;\n        }\n        // 递归判断左子树和右子树，更新目标和为减去当前节点值后的值\n        return hasPathSum(root.left,targetSum-root.val) || hasPathSum(root.right,targetSum-root.val);\n    }\n}\n```\n\n## 169. 多数元素\n\nhttps://leetcode.cn/problems/majority-element/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个大小为 n 的数组 nums ，返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。\n\n你可以假设数组是非空的，并且给定的数组总是存在多数元素。\n\n>示例 1：\n>\n>输入：nums = [3,2,3]\n>\n>输出：3\n\n>示例 2：\n>\n>输入：nums = [2,2,1,1,1,2,2]\n>\n>输出：2\n\n提示：\n\n- n == nums.length\n- 1 <= n <= 5 * 10^4\n- -10^9 <= nums[i] <= 10^9\n\n进阶：尝试设计时间复杂度为 O(n)、空间复杂度为 O(1) 的算法解决此问题。\n\n\n\n```java\n/**\n * 解决方案类，用于寻找数组中的多数元素。\n * 多数元素定义为在数组中出现次数超过数组长度一半的元素。\n */\nclass Solution {\n    /**\n     * 寻找数组中的多数元素。\n     * 使用摩尔投票算法来找到出现次数超过数组长度一半的元素。\n     * \n     * @param nums 输入的整数数组，假设该数组一定存在多数元素。\n     * @return 返回多数元素。\n     */\n    public int majorityElement(int[] nums) {\n        // 初始化一个变量来存储当前的候选多数元素。\n        Integer last = null;\n        // 初始化计数器，用于跟踪当前候选多数元素的票数。\n        int cnt = 0;\n        // 遍历数组中的每个元素。\n        for (int num : nums) {\n            // 当计数器为0时，选择新的候选多数元素。\n            if (cnt == 0) {\n                last = num;\n            }\n            // 如果当前元素等于候选多数元素，计数器加1；否则计数器减1。\n            cnt += last == num ? 1 : -1;\n        }\n        // 返回最后的候选多数元素，根据摩尔投票算法的特性，最后的候选者即为多数元素。\n        return last;\n    }\n}\n```\n\n## 662. 二叉树最大宽度\nhttps://leetcode.cn/problems/maximum-width-of-binary-tree/description/\n\n给你一棵二叉树的根节点 root ，返回树的 最大宽度 。\n\n树的 最大宽度 是所有层中最大的 宽度 。\n\n每一层的 宽度 被定义为该层最左和最右的非空节点（即，两个端点）之间的长度。将这个二叉树视作与满二叉树结构相同，两端点间会出现一些延伸到这一层的 null 节点，这些 null 节点也计入长度。\n\n题目数据保证答案将会在  32 位 带符号整数范围内。\n\n \n\n>示例 1：\n>\n>![alt text](../img/数据结构和算法/二叉树最大宽度1.png)\n>\n>\n>输入：root = [1,3,2,5,3,null,9]\n>\n>输出：4\n>\n>解释：最大宽度出现在树的第 3 层，宽度为 4 (5,3,null,9) 。\n\n>示例 2：\n>\n>![alt text](../img/数据结构和算法/二叉树最大宽度2.png)\n>\n>\n>输入：root = [1,3,2,5,null,null,9,6,null,7]\n>\n>输出：7\n>\n>解释：最大宽度出现在树的第 4 层，宽度为 7 (6,null,null,null,null,null,7) 。\n\n>示例 3：\n>\n>![alt text](../img/数据结构和算法/二叉树最大宽度3.png)\n>\n>输入：root = [1,3,2,5]\n>\n>输出：2\n>\n>解释：最大宽度出现在树的第 2 层，宽度为 2 (3,2) 。\n \n\n提示：\n\n- 树中节点的数目范围是 [1, 3000]\n- -100 <= Node.val <= 100\n\n```java\n/**\n * Definition for a binary tree node.\n * public class TreeNode {\n *     int val;\n *     TreeNode left;\n *     TreeNode right;\n *     TreeNode() {}\n *     TreeNode(int val) { this.val = val; }\n *     TreeNode(int val, TreeNode left, TreeNode right) {\n *         this.val = val;\n *         this.left = left;\n *         this.right = right;\n *     }\n * }\n */\n/**\n * Solution类用于解决计算二叉树宽度的问题。\n */\nclass Solution {\n    /**\n     * 计算二叉树的宽度。\n     * 宽度定义为最左边节点和最右边节点的索引之差加一。\n     * \n     * @param root 二叉树的根节点\n     * @return 二叉树的宽度\n     */\n    public int widthOfBinaryTree(TreeNode root) {\n        // 使用双端队列存储节点和它们对应的索引\n        Deque<TreeNode> nodeQueue = new LinkedList<>();\n        Deque<Integer> indexQueue = new LinkedList<>();\n\n        // 初始化队列，将根节点和索引0入队\n        nodeQueue.offer(root);\n        indexQueue.offer(0);\n\n        // 用于记录最大的宽度\n        int max = 0;\n\n        // 当队列不为空时，循环处理队列中的节点\n        while(!nodeQueue.isEmpty()){\n            // 获取当前层的节点数量\n            int size = nodeQueue.size();\n\n            // 记录当前层的最左边节点的索引\n            int leftIndex = indexQueue.peek();\n            // 初始化当前层的最右边节点的索引\n            int rightIndex = 0;\n\n            // 遍历当前层的节点\n            for(int i=0;i<size;i++){\n                // 出队一个节点及其索引\n                TreeNode n = nodeQueue.poll();\n                int idx = indexQueue.poll();\n                // 更新当前层的最右边节点的索引\n                rightIndex = idx;\n\n                // 如果节点有左子节点，将左子节点及其索引入队\n                if(n.left!= null){\n                    nodeQueue.offer(n.left);\n                    indexQueue.offer(2*idx);\n                }\n                // 如果节点有右子节点，将右子节点及其索引入队\n                if(n.right != null){\n                    nodeQueue.offer(n.right);\n                    indexQueue.offer(2*idx+1);\n                }\n            }\n            // 更新最大宽度\n            max = Math.max(max,rightIndex-leftIndex+1);\n        }\n        // 返回最大宽度\n        return max;\n    }\n}\n```\n\n## 718. 最长重复子数组\nhttps://leetcode.cn/problems/maximum-length-of-repeated-subarray/description/\n\n给两个整数数组 nums1 和 nums2 ，返回 两个数组中 公共的 、长度最长的子数组的长度 。\n\n \n\n>示例 1：\n>\n>输入：nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]\n>\n>输出：3\n>\n>解释：长度最长的公共子数组是 [3,2,1] 。\n\n>示例 2：\n>\n>输入：nums1 = [0,0,0,0,0], nums2 = [0,0,0,0,0]\n>\n>输出：5\n\n提示：\n\n- 1 <= nums1.length, nums2.length <= 1000\n- 0 <= nums1[i], nums2[i] <= 100\n\n```java\n/**\n * Solution类用于解决两个数组中最长公共子序列的长度问题。\n */\nclass Solution {\n    /**\n     * 计算两个数组nums1和nums2的最长公共子序列的长度。\n     * \n     * @param nums1 第一个整数数组。\n     * @param nums2 第二个整数数组。\n     * @return 返回最长公共子序列的长度。\n     */\n    public int findLength(int[] nums1, int[] nums2) {\n        // nums1和nums2的长度\n        int m = nums1.length;\n        int n = nums2.length;\n        // dp数组用于动态规划存储子问题的解\n        int[][] dp = new int[m+1][n+1];\n        // ans用于记录最长公共子序列的长度\n        int ans = 0;\n        // 遍历数组nums1和nums2的所有元素\n        for(int i=1;i<=m;i++){\n            for(int j=1;j<=n;j++){\n                // 如果nums1和nums2当前对应的元素相等，则dp[i][j]等于dp[i-1][j-1]加1\n                if(nums1[i-1] == nums2[j-1]){\n                    dp[i][j] = dp[i-1][j-1]+1;\n                }\n                // 更新ans为当前找到的最长公共子序列的长度\n                ans = Math.max(ans,dp[i][j]);\n            }\n        }\n        // 返回最长公共子序列的长度\n        return ans;\n    }\n}\n```\n\n## 179. 最大数\nhttps://leetcode.cn/problems/largest-number/description/\n\n给定一组非负整数 nums，重新排列每个数的顺序（每个数不可拆分）使之组成一个最大的整数。\n\n注意：输出结果可能非常大，所以你需要返回一个字符串而不是整数。\n\n \n\n>示例 1：\n>\n>输入：nums = [10,2]\n>\n>输出：\"210\"\n\n>示例 2：\n>\n>输入：nums = [3,30,34,5,9]\n>\n>输出：\"9534330\"\n \n\n提示：\n\n- 1 <= nums.length <= 100\n- 0 <= nums[i] <= 10^9\n\n```java\n/**\n * 解决方案类，提供数组转换为最大数的功能。\n */\nclass Solution {\n    /**\n     * 将给定的非负整数数组转换为它们能组成的最大数的字符串表示。\n     * \n     * @param nums 非负整数数组\n     * @return 组成的最大数的字符串表示，如果输入为空则返回空\n     */\n    public String largestNumber(int[] nums) {\n        // 检查输入数组是否为空或空数组\n        if(nums == null || nums.length == 0){\n            return null;\n        }\n        \n        // 将整数数组转换为字符串数组\n        String[] arr = new String[nums.length];\n        for(int i = 0; i< arr.length;i++){\n            arr[i] = String.valueOf(nums[i]);\n        }\n        \n        // 自定义排序规则，将字符串数组按照组合后的数值大小降序排列\n        Arrays.sort(arr,(a,b)-> (b+a).compareTo(a+b));\n        \n        // 使用StringBuilder拼接排序后的字符串数组\n        StringBuilder sb = new StringBuilder();\n        for(String str:arr){\n            sb.append(str);\n        }\n        \n        // 将StringBuilder转换为字符串\n        String res = sb.toString();\n        \n        // 如果结果的首位字符是'0'，则将结果设置为\"0\"\n        if(res.charAt(0) == '0'){\n            res = \"0\";\n        }\n        \n        return res;\n    }\n}\n```\n\n## 62. 不同路径\nhttps://leetcode.cn/problems/unique-paths/description/\n\n一个机器人位于一个 m x n 网格的左上角 （起始点在下图中标记为 “Start” ）。\n\n机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角（在下图中标记为 “Finish” ）。\n\n问总共有多少条不同的路径？\n\n \n\n>示例 1：\n>\n>![alt text](../img/数据结构和算法/不同路径.png)\n>\n>输入：m = 3, n = 7\n>\n>输出：28\n\n>示例 2：\n>\n>输入：m = 3, n = 2\n>\n>输出：3\n>\n>解释：\n>\n>从左上角开始，总共有 3 条路径可以到达右下角。\n>1. 向右 -> 向下 -> 向下\n>2. 向下 -> 向下 -> 向右\n>3. 向下 -> 向右 -> 向下\n\n>示例 3：\n>\n>输入：m = 7, n = 3\n>\n>输出：28\n>\n>示例 4：\n>\n>输入：m = 3, n = 3\n>\n>输出：6\n \n\n提示：\n\n- 1 <= m, n <= 100\n- 题目数据保证答案小于等于 2 * 10^9\n\n```java\n/**\n * 解决从左上角到右下角的路径数量问题。\n * 使用动态规划方法，dp[i][j]表示到达第i行第j列的位置有几种路径。\n */\nclass Solution {\n    /**\n     * 计算从左上角到右下角的路径数量。\n     * \n     * @param m 表示网格的行数。\n     * @param n 表示网格的列数。\n     * @return 返回从左上角到右下角的路径数量。\n     */\n    public int uniquePaths(int m, int n) {\n        // 初始化动态规划数组，大小为(m+1)x(n+1)，初始值为0\n        int[][] dp = new int[m+1][n+1];\n        \n        // 初始化第一行和第一列的路径数量为1，因为只能向右或向下移动\n        for(int i=0;i<m;i++){\n            dp[i][0] = 1;\n        }\n        for(int j=0;j<n;j++){\n            dp[0][j] = 1;\n        }\n        \n        // 从第二行第二列开始，计算每个位置的路径数量\n        // dp[i][j]的值由其上方和左方的两个位置的路径数量之和得到\n        for(int i=1;i<m;i++){\n            for(int j=1;j<n;j++){\n                dp[i][j] = dp[i-1][j] + dp[i][j-1];\n            }\n        }\n        \n        // 返回右下角位置的路径数量\n        return dp[m-1][n-1];\n    }\n}\n```\n\n## 227. 基本计算器 II\nhttps://leetcode.cn/problems/basic-calculator-ii/description/\n\n给你一个字符串表达式 s ，请你实现一个基本计算器来计算并返回它的值。\n\n整数除法仅保留整数部分。\n\n你可以假设给定的表达式总是有效的。所有中间结果将在 [-231, 231 - 1] 的范围内。\n\n注意：不允许使用任何将字符串作为数学表达式计算的内置函数，比如 eval() 。\n\n \n\n>示例 1：\n>\n>输入：s = \"3+2*2\"\n>\n>输出：7\n\n>示例 2：\n>\n>输入：s = \" 3/2 \"\n>\n>输出：1\n\n>示例 3：\n>\n>输入：s = \" 3+5 / 2 \"\n>\n>输出：5\n \n\n提示：\n\n- 1 <= s.length <= 3 * 10^5\n- s 由整数和算符 ('+', '-', '*', '/') 组成，中间由一些空格隔开\n- s 表示一个 有效表达式\n- 表达式中的所有整数都是非负整数，且在范围 [0, 2^31 - 1] 内\n- 题目数据保证答案是一个 32-bit 整数\n\n```java\n/**\n * 解决方案类，提供一个方法来计算给定字符串表示的数学表达式的值。\n */\nclass Solution {\n    /**\n     * 计算给定字符串表示的数学表达式的值。\n     * 表达式包含整数、运算符（+、-、*、/）和空格。\n     * \n     * @param s 表达式字符串\n     * @return 表达式的计算结果\n     */\n    public int calculate(String s) {\n        int len = s.length();\n        int num = 0; // 用于累计当前数字\n        char sign = '+'; // 记录当前运算符\n        Deque<Integer> dq = new LinkedList<>(); // 使用双端队列存储中间结果\n\n        // 遍历字符串中的每个字符\n        for(int i=0;i<len;i++){\n            char c = s.charAt(i);\n\n            // 如果字符是数字，则更新当前数字的值\n            if(c >= '0'){\n                num = num*10-'0' + c;\n            }\n\n            // 当遇到非数字字符或字符串末尾时，根据当前运算符进行计算并更新队列\n            if((c < '0' && c!=' ') || i == len-1){\n                switch(sign){\n                    case '+': dq.push(num); break;\n                    case '-': dq.push(-num); break;\n                    case '*': dq.push(dq.pop()*num); break;\n                    case '/': dq.push(dq.pop()/num); break;\n                }\n                sign = c; // 更新当前运算符\n                num = 0; // 重置当前数字\n            }\n        }\n\n        int res = 0; // 最终结果\n        // 遍历队列，累加队列中的所有数\n        while(!dq.isEmpty()){\n            res += dq.pop();\n        }\n        return res;\n    }\n}\n```\n\n## 122. 买卖股票的最佳时机 II\n\nhttps://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个整数数组 prices ，其中 prices[i] 表示某支股票第 i 天的价格。\n\n在每一天，你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买，然后在 同一天 出售。\n\n返回 你能获得的 最大 利润 。\n\n>示例 1：\n>\n>输入：prices = [7,1,5,3,6,4]\n>\n>输出：7\n>\n>解释：在第 2 天（股票价格 = 1）的时候买入，在第 3 天（股票价格 = 5）的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。\n>随后，在第 4 天（股票价格 = 3）的时候买入，在第 5 天（股票价格 = 6）的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3。\n>最大总利润为 4 + 3 = 7 。\n\n>示例 2：\n>\n>输入：prices = [1,2,3,4,5]\n>\n>输出：4\n>\n>解释：在第 1 天（股票价格 = 1）的时候买入，在第 5 天 （股票价格 = 5）的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。最大总利润为 4 。\n\n>示例 3：\n>\n>输入：prices = [7,6,4,3,1]\n>\n>输出：0\n>\n>解释：在这种情况下, 交易无法获得正利润，所以不参与交易可以获得最大利润，最大利润为 0。\n\n提示：\n\n- 1 <= prices.length <= 3 * 10^4\n- 0 <= prices[i] <= 10^4\n\n\n\n```java\n/**\n * Solution类用于解决股票最大利润的问题。\n * 它提供了一个方法来计算给定价格数组中，最多进行一次买卖操作所能获得的最大利润。\n */\nclass Solution {\n    /**\n     * 计算股票最大利润。\n     * \n     * @param prices 表示股票每日价格的整数数组。\n     * @return 返回最大利润。如果数组长度小于等于1，则返回0，因为无法进行交易。\n     */\n    public int maxProfit(int[] prices) {\n        // 如果价格数组长度小于等于1，无法进行交易，直接返回0\n        if (prices.length <= 1) {\n            return 0;\n        }\n        // 初始化最大利润为0\n        int max = 0;\n        // 遍历价格数组，从第二天开始计算每一天的利润\n        for (int i = 1; i < prices.length; i++) {\n            // 如果当前价格大于前一天的价格，说明有利润\n            if (prices[i] > prices[i - 1]) {\n                // 累加利润到max\n                max += prices[i] - prices[i - 1];\n            }\n        }\n        // 返回累计的最大利润\n        return max;\n    }\n}\n```\n\n## 226. 翻转二叉树\nhttps://leetcode.cn/problems/invert-binary-tree/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一棵二叉树的根节点 root ，翻转这棵二叉树，并返回其根节点。\n\n\n\n>示例 1：\n>\n>![翻转二叉树1.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E7%BF%BB%E8%BD%AC%E4%BA%8C%E5%8F%89%E6%A0%911.png)\n>\n>输入：root = [4,2,7,1,3,6,9]\n>\n>输出：[4,7,2,9,6,3,1]\n\n>示例 2：\n>\n>![翻转二叉树2.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E7%BF%BB%E8%BD%AC%E4%BA%8C%E5%8F%89%E6%A0%912.png)\n>\n>输入：root = [2,1,3]\n>\n>输出：[2,3,1]\n\n>示例 3：\n>\n>输入：root = []\n>\n>输出：[]\n\n\n提示：\n\n- 树中节点数目范围在 [0, 100] 内\n- -100 <= Node.val <= 100\n\n```java\n/**\n * Definition for a binary tree node.\n * public class TreeNode {\n *     int val;\n *     TreeNode left;\n *     TreeNode right;\n *     TreeNode() {}\n *     TreeNode(int val) { this.val = val; }\n *     TreeNode(int val, TreeNode left, TreeNode right) {\n *         this.val = val;\n *         this.left = left;\n *         this.right = right;\n *     }\n * }\n */\n/**\n * 该类提供了一个解决方案，用于翻转二叉树。\n * 二叉树的翻转是指将二叉树中每个节点的左子节点和右子节点互换。\n */\nclass Solution {\n    /**\n     * 翻转二叉树。\n     * \n     * @param root 二叉树的根节点。\n     * @return 翻转后的二叉树的根节点。\n     */\n    public TreeNode invertTree(TreeNode root) {\n        // 如果当前节点为空或者为叶子节点（没有左右子节点），则无需翻转，直接返回当前节点。\n        if(root == null || (root.left==null && root.right==null)){\n            return root;\n        }\n        // 交换当前节点的左子节点和右子节点。\n        TreeNode temp  = root.left;\n        root.left = root.right;\n        root.right = temp;\n        // 递归翻转当前节点的左子树和右子树。\n        invertTree(root.left);\n        invertTree(root.right);\n        // 返回翻转后的当前节点。\n        return root;\n    }\n}\n```\n\n## 695. 岛屿的最大面积\nhttps://leetcode.cn/problems/max-area-of-island/description/\n\n给你一个大小为 m x n 的二进制矩阵 grid 。\n\n岛屿 是由一些相邻的 1 (代表土地) 构成的组合，这里的「相邻」要求两个 1 必须在 水平或者竖直的四个方向上 相邻。你可以假设 grid 的四个边缘都被 0（代表水）包围着。\n\n岛屿的面积是岛上值为 1 的单元格的数目。\n\n计算并返回 grid 中最大的岛屿面积。如果没有岛屿，则返回面积为 0 。\n\n \n\n>示例 1：\n>\n>![alt text](../img/数据结构和算法/岛屿的最大面积.png)\n>\n>\n>输入：grid = [[0,0,1,0,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,1,1,0,1,0,0,0,0,0,0,0,0],[0,1,0,0,1,1,0,0,1,0,1,0,0],[0,1,0,0,1,1,0,0,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,1,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,0,0,0,0,0,0,1,1,0,0,0,0]]\n>\n>输出：6\n>\n>解释：答案不应该是 11 ，因为岛屿只能包含水平或垂直这四个方向上的 1 。\n\n>示例 2：\n>\n>输入：grid = [[0,0,0,0,0,0,0,0]]\n>\n>输出：0\n \n\n提示：\n\n- m == grid.length\n- n == grid[i].length\n- 1 <= m, n <= 50\n- grid[i][j] 为 0 或 1\n\n```java\n/**\n * 此类提供了一个解决方案，用于计算给定二维网格中最大岛屿的面积。\n */\nclass Solution {\n    /**\n     * 网格的行数\n     */\n    int m = 0;\n    /**\n     * 网格的列数\n     */\n    int n = 0;\n\n    /**\n     * 计算给定二维网格中最大岛屿的面积。\n     *\n     * @param grid 一个二维整数数组，其中0表示水域，1表示陆地。\n     * @return 返回网格中的最大岛屿面积。\n     */\n    public int maxAreaOfIsland(int[][] grid) {\n        m = grid.length;\n        n = grid[0].length;\n        int max = 0;\n        \n        // 遍历网格，寻找并计算岛屿面积\n        for (int i = 0; i < m; i++) {\n            for (int j = 0; j < n; j++) {\n                if (grid[i][j] == 1) {\n                    int area = dfs(grid, i, j);\n                    max = Math.max(max, area);\n                }\n            }\n        }\n        \n        return max;\n    }\n\n    /**\n     * 深度优先搜索（DFS）遍历相邻陆地，计算岛屿面积。\n     *\n     * @param grid 一个二维整数数组，其中0表示水域，1表示陆地。\n     * @param i 当前访问的单元格的行索引。\n     * @param j 当前访问的单元格的列索引。\n     * @return 返回以(i, j)为起点的岛屿的面积。\n     */\n    private int dfs(int[][] grid, int i, int j) {\n        // 检查是否越界或已访问过的位置\n        if (i < 0 || i >= m || j < 0 || j >= n || grid[i][j] == 0) {\n            return 0;\n        }\n        \n        // 标记当前位置为已访问\n        grid[i][j] = 0;\n        int cnt = dfs(grid, i + 1, j);   // 右边\n        cnt += dfs(grid, i - 1, j);   // 左边\n        cnt += dfs(grid, i, j + 1);   // 下面\n        cnt += dfs(grid, i, j - 1);   // 上面\n        return cnt + 1;  // 包含当前访问的陆地\n    }\n}\n```\n\n## 删除排序链表中的重复元素\nhttps://leetcode.cn/problems/remove-duplicates-from-sorted-list/description/\n\n给定一个已排序的链表的头 head ， 删除所有重复的元素，使每个元素只出现一次 。返回 已排序的链表 。\n\n \n\n>示例 1：\n>\n>![alt text](../img/数据结构和算法/删除排序链表中的重复元素1.png)\n>\n>\n>输入：head = [1,1,2]\n>\n>输出：[1,2]\n\n>示例 2：\n>\n>![alt text](../img/数据结构和算法/删除排序链表中的重复元素2.png)\n>\n>输入：head = [1,1,2,3,3]\n>\n>输出：[1,2,3]\n \n\n提示：\n\n- 链表中节点数目在范围 [0, 300] 内\n- -100 <= Node.val <= 100\n- 题目数据保证链表已经按升序 排列\n\n```java\n/**\n * Definition for singly-linked list.\n * public class ListNode {\n *     int val;\n *     ListNode next;\n *     ListNode() {}\n *     ListNode(int val) { this.val = val; }\n *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }\n * }\n */\nclass Solution {\n    public ListNode deleteDuplicates(ListNode head) {\n        ListNode cur = head;\n        while(cur!= null && cur.next != null){\n            if(cur.val == cur.next.val){\n                cur.next = cur.next.next;\n            }else{\n                cur = cur.next;\n            }\n        }\n        return head;\n    }\n}\n```\n\n## 152. 乘积最大子数组\nhttps://leetcode.cn/problems/maximum-product-subarray/description/\n\n给你一个整数数组 nums ，请你找出数组中乘积最大的非空连续子数组（该子数组中至少包含一个数字），并返回该子数组所对应的乘积。\n\n测试用例的答案是一个 32-位 整数。\n\n \n>示例 1:\n>\n>输入: nums = [2,3,-2,4]\n>\n>输出: 6\n>\n>解释: 子数组 [2,3] 有最大乘积 6。\n\n>示例 2:\n>\n>输入: nums = [-2,0,-1]\n>\n>输出: 0\n>\n>解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。\n \n\n提示:\n\n- 1 <= nums.length <= 2 * 10^4\n- -10 <= nums[i] <= 10\n- nums 的任何前缀或后缀的乘积都 保证 是一个 32-位 整数\n\n```java\npublic class MaximumProductSubarray {\n\n        /**\n     * 计算数组中任意非空子数组的最大乘积。\n     * 该方法通过动态规划的方式，避免了计算所有子数组乘积的高复杂度。\n     * 利用最大值和最小值的相互转换，以及乘法的性质，高效地求解最大乘积。\n     * 特别处理了全负数的情况，确保结果的正确性。\n     *\n     * @param nums 输入的整数数组，可以包含负数和0。\n     * @return 返回任意非空子数组的最大乘积。\n     */\n    public int maxProduct(int[] nums) {\n        // 初始化最大乘积和最小乘积为数组的第一个元素。\n        // 最大乘积和最小乘积的初始值相同，因为乘以一个负数会交换最大和最小的角色。\n        int maxProd = nums[0];\n        int minProd = nums[0];\n        // 初始化结果为数组的第一个元素，作为后续比较的最大乘积。\n        int result = nums[0];\n\n        // 从数组的第二个元素开始遍历。\n        for (int i = 1; i < nums.length; i++) {\n            // 如果当前元素是负数，交换最大和最小乘积的值。\n            // 这是因为乘以一个负数会改变最大和最小乘积的相对关系。\n            if (nums[i] < 0) {\n                int temp = maxProd;\n                maxProd = minProd;\n                minProd = temp;\n            }\n            // 更新最大乘积，考虑两种情况：当前元素单独作为子数组的乘积，或者当前元素与之前的最大乘积相乘。\n            maxProd = Math.max(nums[i], maxProd * nums[i]);\n            // 更新最小乘积，考虑两种情况：当前元素单独作为子数组的乘积，或者当前元素与之前的最大乘积相乘。\n            minProd = Math.min(nums[i], minProd * nums[i]);\n            // 更新结果，保留最大的乘积。\n            result = Math.max(result, maxProd);\n        }\n\n        // 返回计算出的最大乘积。\n        return result;\n    }\n\n    public static void main(String[] args) {\n        MaximumProductSubarray solution = new MaximumProductSubarray();\n        int[] nums1 = {2, 3, -2, 4};\n        System.out.println(solution.maxProduct(nums1)); // 输出 6\n\n        int[] nums2 = {-2, 0, -1};\n        System.out.println(solution.maxProduct(nums2)); // 输出 0\n    }\n}\n```\n\n## 198. 打家劫舍\n你是一个专业的小偷，计划偷窃沿街的房屋。每间房内都藏有一定的现金，影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统，如果两间相邻的房屋在同一晚上被小偷闯入，系统会自动报警。\n\n给定一个代表每个房屋存放金额的非负整数数组，计算你 不触动警报装置的情况下 ，一夜之内能够偷窃到的最高金额。\n\n \n\n>示例 1：\n>\n>输入：[1,2,3,1]\n>\n>输出：4\n>\n>解释：偷窃 1 号房屋 (金额 = 1) ，然后偷窃 3 号房屋 (金额 = 3)。\n     偷窃到的最高金额 = 1 + 3 = 4 。\n\n>示例 2：\n>\n>输入：[2,7,9,3,1]\n>\n>输出：12\n>\n>解释：偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9)，接着偷窃 5 号房屋 (金额 = 1)。偷窃到的最高金额 = 2 + 9 + 1 = 12 。\n \n\n提示：\n\n- 1 <= nums.length <= 100\n- 0 <= nums[i] <= 400\n\n```java\nclass Solution {\n    public int rob(int[] nums) {\n        if (nums == null || nums.length == 0) {\n            return 0; // 处理边界情况，如果数组为空则直接返回0\n        }\n\n        if (nums.length == 1) {\n            return nums[0]; // 只有一间房屋时直接返回该房屋金额\n        }\n\n        int[] dp = new int[nums.length]; // 创建一个数组用来存储偷盗到每个房屋的最大金额\n\n        dp[0] = nums[0]; // 初始化第一间房屋的偷盗金额\n        dp[1] = Math.max(nums[0], nums[1]); // 初始化第二间房屋的偷盗金额\n\n        for (int i = 2; i < nums.length; i++) {\n            dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]); // 当前房屋可以选择偷窃或者不偷窃\n        }\n\n        return dp[nums.length - 1]; // 返回最后一间房屋的最大偷盗金额\n    }\n\n    public static void main(String[] args) {\n        Solution solution = new Solution();\n\n        // Test Cases\n        int[] nums1 = {1, 2, 3, 1};\n        System.out.println(solution.rob(nums1)); // Output: 4\n\n        int[] nums2 = {2, 7, 9, 3, 1};\n        System.out.println(solution.rob(nums2)); // Output: 12\n    }\n}\n```\n\n## 补充题6. 手撕堆排序\n给你一个整数数组 nums，请你将该数组升序排列。\n\n \n\n>示例 1：\n>\n>输入：nums = [5,2,3,1]\n>\n>输出：[1,2,3,5]\n\n>示例 2：\n>\n>输入：nums = [5,1,1,2,0,0]\n>\n>输出：[0,0,1,1,2,5]\n \n\n提示：\n\n- 1 <= nums.length <= 5 * 10^4\n- -5 * 10^4 <= nums[i] <= 5 * 10^4\n\n```java\nimport java.util.*;\n\npublic class HeapSort {\n\n        /**\n     * 堆排序实现对数组的升序排序。\n     * 堆排序是一种基于比较的排序算法，使用了分治策略和数据结构中的堆（一种近似完全二叉树）。\n     * 算法分为两个主要步骤：构建最大堆和调整堆结构进行排序。\n     *\n     * @param nums 待排序的整型数组。\n     */\n    public int[] heapSort(int[] nums) {\n        int n = nums.length;\n\n        // 构建最大堆，从最后一个非叶子节点开始，自下而上、自右至左调整结构。\n        for (int i = n / 2 - 1; i >= 0; i--)\n            heapify(nums, n, i);\n\n        // 一个个将堆顶元素（当前最大值）移至数组末尾，并调整剩余元素为最大堆。\n        for (int i = n - 1; i > 0; i--) {\n            // 将当前根节点与最后一个元素交换。\n            int temp = nums[0];\n            nums[0] = nums[i];\n            nums[i] = temp;\n\n            // 调整减小后的堆（不包括已排序的最后一个元素）。\n            heapify(nums, i, 0);\n        }\n        return nums;\n    }\n\n    /**\n     * 调整索引i处的元素，使其所在的子树成为最大堆。\n     * 这个过程递归进行，确保整个数组满足最大堆的性质。\n     *\n     * @param nums 待调整的整型数组。\n     * @param n    数组的长度。\n     * @param i    当前需要堆化的节点索引。\n     */\n    void heapify(int[] nums, int n, int i) {\n        int largest = i; // 初始化最大元素为当前根节点\n        int left = 2 * i + 1; // 左孩子索引\n        int right = 2 * i + 2; // 右孩子索引\n\n        // 如果左孩子存在且大于当前最大元素，则更新最大元素索引\n        if (left < n && nums[left] > nums[largest])\n            largest = left;\n\n        // 如果右孩子存在且大于当前最大元素，则再次更新最大元素索引\n        if (right < n && nums[right] > nums[largest])\n            largest = right;\n\n        // 如果最大元素不是当前根节点，则交换它们的位置，并继续堆化受影响的子树\n        if (largest != i) {\n            int swap = nums[i];\n            nums[i] = nums[largest];\n            nums[largest] = swap;\n\n            heapify(nums, n, largest);\n        }\n    }\n\n    public static void main(String[] args) {\n        HeapSort sorter = new HeapSort();\n        int[] nums1 = {5, 2, 3, 1};\n        sorter.heapSort(nums1);\n        System.out.println(Arrays.toString(nums1)); // Output: [1, 2, 3, 5]\n\n        int[] nums2 = {5, 1, 1, 2, 0, 0};\n        sorter.heapSort(nums2);\n        System.out.println(Arrays.toString(nums2)); // Output: [0, 0, 1, 1, 2, 5]\n    }\n}\n```\n\n## 24. 两两交换链表中的节点\nhttps://leetcode.cn/problems/swap-nodes-in-pairs/description/\n\n给你一个链表，两两交换其中相邻的节点，并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题（即，只能进行节点交换）。\n\n \n\n>示例 1：\n>\n>![alt text](../img/数据结构和算法/两两交换链表中的节点.png)\n>\n>输入：head = [1,2,3,4]\n>\n>输出：[2,1,4,3]\n\n>示例 2：\n>\n>输入：head = []\n>\n>输出：[]\n\n>示例 3：\n>\n>输入：head = [1]\n>\n>输出：[1]\n \n\n提示：\n\n- 链表中节点的数目在范围 [0, 100] 内\n- 0 <= Node.val <= 100\n\n```java\n/**\n * Definition for singly-linked list.\n * public class ListNode {\n *     int val;\n *     ListNode next;\n *     ListNode() {}\n *     ListNode(int val) { this.val = val; }\n *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }\n * }\n */\nclass Solution {\n    /**\n     * 交换链表中的每对节点。\n     * \n     * 本函数接收一个链表的头节点，旨在交换链表中每对相邻节点。\n     * 例如，如果输入链表为 1->2->3->4，那么输出链表应为 2->1->4->3。\n     * 使用哑节点(dummy)来简化链表操作，避免处理空链表的特殊情况。\n     * \n     * @param head 链表的头节点\n     * @return 返回交换后的链表头节点\n     */\n    public ListNode swapPairs(ListNode head) {\n        // 创建一个哑节点，用于简化链表操作\n        ListNode dummy = new ListNode(-1);\n        // 将哑节点指向原链表的头节点\n        dummy.next = head;\n        // 使用cur指针追踪当前操作的节点\n        ListNode cur = dummy;\n        // 当当前节点的下一个节点及其下下个节点都不为空时，执行交换操作\n        while (cur.next != null && cur.next.next != null) {\n            // 分别获取当前节点的下两个节点\n            ListNode n1 = cur.next;\n            ListNode n2 = cur.next.next;\n            // 将当前节点指向下下个节点，实现第一步交换\n            cur.next = n2;\n            // 将第一个节点指向原第二个节点的下一个节点，完成第一对节点的交换\n            n1.next = n2.next;\n            // 将第二个节点指向第一个节点，完成交换\n            n2.next = n1;\n            // 将当前节点移动到刚交换的对的第一个节点\n            cur = n1;\n        }\n        // 返回交换后的链表头节点，即哑节点的下一个节点\n        return dummy.next;\n    }\n}\n```\n\n## 139. 单词拆分\nhttps://leetcode.cn/problems/word-break/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true。\n\n注意：不要求字典中出现的单词全部都使用，并且字典中的单词可以重复使用。\n\n \n\n>示例 1：\n>\n>输入: s = \"leetcode\", wordDict = [\"leet\", \"code\"]\n>\n>输出: true\n>\n>解释: 返回 true 因为 \"leetcode\" 可以由 \"leet\" 和 \"code\" 拼接成。\n\n>示例 2：\n>\n>输入: s = \"applepenapple\", wordDict = [\"apple\", \"pen\"]\n>\n>输出: true\n>\n>解释: 返回 true 因为 \"applepenapple\" 可以由 \"apple\" \"pen\" \"apple\" 拼接成。\n>\n> 注意，你可以重复使用字典中的单词。\n\n>示例 3：\n>\n>输入: s = \"catsandog\", wordDict = [\"cats\", \"dog\", \"sand\", \"and\", \"cat\"]\n>\n>输出: false\n \n\n提示：\n\n- 1 <= s.length <= 300\n- 1 <= wordDict.length <= 1000\n- 1 <= wordDict[i].length <= 20\n- s 和 wordDict[i] 仅由小写英文字母组成\n- wordDict 中的所有字符串 互不相同\n\n```java\nimport java.util.List;\nimport java.util.Set;\n\npublic class Solution {\n        /**\n     * 检查字符串s是否可以被wordDict中的单词列表拆分。\n     * \n     * @param s 待检查的字符串\n     * @param wordDict 单词列表，用于拆分字符串s\n     * @return 如果字符串s可以被拆分，则返回true；否则返回false。\n     */\n    public boolean wordBreak(String s, List<String> wordDict) {\n        // 将单词列表转换为集合，以便快速查找\n        Set<String> wordSet = new HashSet<>(wordDict);\n        // 获取字符串s的长度\n        int n = s.length();\n        \n        // dp数组，dp[i]表示字符串s的前i个字符是否可以被wordDict中的单词拆分\n        // dp[i] 表示前i个字符能否被拆分\n        boolean[] dp = new boolean[n + 1];\n        // 空字符串可以被看作是被有效拆分的\n        dp[0] = true;  // 空字符串可以被拆分\n        \n        // 遍历字符串s的每个字符，检查是否可以被拆分\n        for (int i = 1; i <= n; i++) {\n            // 尝试从0到i-1的每个位置作为拆分点\n            for (int j = 0; j < i; j++) {\n                // 如果前j个字符可以被拆分，并且第j个字符到第i个字符的子串也在wordDict中\n                if (dp[j] && wordSet.contains(s.substring(j, i))) {\n                    // 则字符串s的前i个字符可以被拆分\n                    dp[i] = true;\n                    // 找到一个有效的拆分点后，无需继续查找\n                    break;\n                }\n            }\n        }\n        // 返回整个字符串s是否可以被拆分\n        return dp[n];\n    }\n\n    public static void main(String[] args) {\n        Solution solution = new Solution();\n\n        // Test Cases\n        String s1 = \"leetcode\";\n        List<String> wordDict1 = List.of(\"leet\", \"code\");\n        System.out.println(solution.wordBreak(s1, wordDict1));  // Output: true\n\n        String s2 = \"applepenapple\";\n        List<String> wordDict2 = List.of(\"apple\", \"pen\");\n        System.out.println(solution.wordBreak(s2, wordDict2));  // Output: true\n\n        String s3 = \"catsandog\";\n        List<String> wordDict3 = List.of(\"cats\", \"dog\", \"sand\", \"and\", \"cat\");\n        System.out.println(solution.wordBreak(s3, wordDict3));  // Output: false\n    }\n}\n```\n\n## 560. 和为K的子数组\nhttps://leetcode.cn/problems/subarray-sum-equals-k/description/\n\n给你一个整数数组 nums 和一个整数 k ，请你统计并返回 该数组中和为 k 的子数组的个数 。\n\n子数组是数组中元素的连续非空序列。\n\n \n\n>示例 1：\n>\n>输入：nums = [1,1,1], k = 2\n>\n>输出：2\n\n>示例 2：\n>\n>输入：nums = [1,2,3], k = 3\n>\n>输出：2\n \n\n提示：\n\n- 1 <= nums.length <= 2 * 10^4\n- -1000 <= nums[i] <= 1000\n- -10^7 <= k <= 10^7\n\n```java\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class SubarraySumEqualsK {\n\n    /**\n     * 计算数组中和为k的连续子数组的数量。\n     * 通过维护一个前缀和的map，来记录每个前缀和出现的次数。\n     * 当遇到当前前缀和与k的差在map中存在时，说明存在一个或多个子数组的和为k。\n     * \n     * @param nums 输入的整数数组\n     * @param k 目标子数组的和\n     * @return 和为k的连续子数组的数量\n     */\n    public int subarraySum(int[] nums, int k) {\n        // 初始化计数器，用于记录和为k的子数组数量\n        int count = 0;\n        // 初始化前缀和，用于计算当前位置的前缀和\n        int sum = 0;\n        // 初始化map，用于存储前缀和出现的次数，初始时包含前缀和为0的情况，出现次数为1\n        Map<Integer, Integer> map = new HashMap<>();\n        map.put(0, 1);\n\n        // 遍历数组，计算每个位置的前缀和，并更新map\n        for (int i = 0; i < nums.length; i++) {\n            // 累加当前元素到前缀和\n            sum += nums[i];\n            // 如果当前前缀和减去k在map中存在，说明存在一个或多个子数组的和为k\n            if (map.containsKey(sum - k)) {\n                // 累加找到的和为k的子数组数量到计数器\n                count += map.get(sum - k);\n            }\n            // 更新map，记录当前前缀和出现的次数\n            map.put(sum, map.getOrDefault(sum, 0) + 1);\n        }\n        \n        // 返回和为k的子数组数量\n        return count;\n    }\n\n    public static void main(String[] args) {\n        SubarraySumEqualsK solution = new SubarraySumEqualsK();\n        \n        int[] nums1 = {1, 1, 1};\n        int k1 = 2;\n        System.out.println(solution.subarraySum(nums1, k1)); // Output: 2\n        \n        int[] nums2 = {1, 2, 3};\n        int k2 = 3;\n        System.out.println(solution.subarraySum(nums2, k2)); // Output: 2\n    }\n}\n```\n\n## 209. 长度最小的子数组\nhttps://leetcode.cn/problems/minimum-size-subarray-sum/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个含有 n 个正整数的数组和一个正整数 target 。\n\n找出该数组中满足其总和大于等于 target 的长度最小的 子数组\n[numsl, numsl+1, ..., numsr-1, numsr] ，并返回其长度。如果不存在符合条件的子数组，返回 0 。\n\n\n\n>示例 1：\n>\n>输入：target = 7, nums = [2,3,1,2,4,3]\n>\n>输出：2\n>\n>解释：子数组 [4,3] 是该条件下的长度最小的子数组。\n\n>示例 2：\n>\n>输入：target = 4, nums = [1,4,4]\n>\n>输出：1\n\n>示例 3：\n>\n>输入：target = 11, nums = [1,1,1,1,1,1,1,1]\n>\n>输出：0\n\n\n提示：\n\n- 1 <= target <= 10^9\n- 1 <= nums.length <= 10^5\n- 1 <= nums[i] <= 10^5\n\n```java\nclass Solution {\n    /**\n     * 计算最小连续子数组长度，使得子数组之和大于等于目标值target。\n     *\n     * @param target 目标值，需要找到的子数组最小和必须大于等于此值\n     * @param nums   整型数组，用于查找满足条件的子数组\n     * @return 返回最小连续子数组长度，如果找不到满足条件的子数组则返回0\n     */\n    public int minSubArrayLen(int target, int[] nums) {\n        // 初始化滑动窗口的左边界\n        int left = 0;\n        // 初始化窗口内元素之和\n        int sum = 0;\n        // 初始化结果变量，设置为最大整数，用于存储最小子数组长度\n        int result = Integer.MAX_VALUE;\n\n        // 遍历数组，更新滑动窗口\n        for (int right = 0; right < nums.length; right++) {\n            // 将当前元素加入窗口内和\n            sum += nums[right];\n\n            // 当窗口内和大于等于目标值时，更新最小子数组长度\n            while (sum >= target) {\n                result = Math.min(result, right - left + 1);\n                // 移除窗口左边界元素，缩小窗口\n                sum -= nums[left++];\n            }\n        }\n        // 如果结果仍为最大整数，表示未找到满足条件的子数组，返回0\n        return result == Integer.MAX_VALUE ? 0 : result;\n    }\n}\n```\n\n## 153. 寻找旋转排序数组中的最小值\nhttps://leetcode.cn/problems/find-minimum-in-rotated-sorted-array/description/?envType=study-plan-v2&envId=top-interview-150\n\n已知一个长度为 n 的数组，预先按照升序排列，经由 1 到 n 次 旋转 后，得到输入数组。例如，原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到：\n- 若旋转 4 次，则可以得到 [4,5,6,7,0,1,2]\n- 若旋转 7 次，则可以得到 [0,1,2,4,5,6,7]\n\n注意，数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]] 。\n\n给你一个元素值 互不相同 的数组 nums ，它原来是一个升序排列的数组，并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。\n\n你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。\n\n \n\n>示例 1：\n>\n>输入：nums = [3,4,5,1,2]\n>\n>输出：1\n>\n>解释：原数组为 [1,2,3,4,5] ，旋转 3 次得到输入数组。\n\n>示例 2：\n>\n>输入：nums = [4,5,6,7,0,1,2]\n>\n>输出：0\n>\n>解释：原数组为 [0,1,2,4,5,6,7] ，旋转 3 次得到输入数组。\n\n>示例 3：\n>\n>输入：nums = [11,13,15,17]\n>\n>输出：11\n>\n>解释：原数组为 [11,13,15,17] ，旋转 4 次得到输入数组。\n \n\n提示：\n\n- n == nums.length\n- 1 <= n <= 5000\n- -5000 <= nums[i] <= 5000\n- nums 中的所有整数 互不相同\n- nums 原来是一个升序排序的数组，并进行了 1 至 n 次旋转\n\n```java\n/**\n * 解决寻找旋转排序数组中的最小值的问题。\n * 旋转排序数组是指原数组为非递减数组，将数组从某个位置分割成两部分，然后将两部分的顺序调换后形成的数组。\n * 例如，原数组[0,1,2,4,5,6,7]在数字4处旋转后变为[4,5,6,7,0,1,2]。\n */\nclass Solution {\n    /**\n     * 寻找旋转排序数组中的最小值。\n     * \n     * @param nums 一个旋转后的非递减排序数组\n     * @return 数组中的最小值\n     */\n    public int findMin(int[] nums) {\n        int left = 0, right = nums.length - 1;\n        // 使用二分查找法定位最小值的位置\n        while (left < right) {\n            // 计算中间位置，避免整数溢出\n            int mid = left + (right - left) / 2;\n            // 如果中间位置的值大于最右边的值，说明最小值在mid右侧\n            if (nums[mid] > nums[right]) {\n                left = mid + 1;\n            } else {\n                // 否则，最小值在mid或其左侧\n                right = mid;\n            }\n        }\n        // 当left等于right时，找到最小值的位置\n        return nums[left];\n    }\n}\n```\n\n# 来源统计\n- https://codetop.cc/home"
  },
  {
    "path": "数据结构和算法/leetcodetop150.md",
    "content": "# 数组 / 字符串\n\n## 88. 合并两个有序数组\n\nhttps://leetcode.cn/problems/merge-sorted-array/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2，另有两个整数 m 和 n ，分别表示 nums1 和 nums2 中的元素数目。\n\n请你 合并 nums2 到 nums1 中，使合并后的数组同样按 非递减顺序 排列。\n\n注意：最终，合并后数组不应由函数返回，而是存储在数组 nums1 中。为了应对这种情况，nums1 的初始长度为 m + n，其中前 m\n个元素表示应合并的元素，后 n 个元素为 0 ，应忽略。nums2 的长度为 n 。\n\n示例 1：\n\n输入：nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3\n\n输出：[1,2,2,3,5,6]\n\n解释：需要合并 [1,2,3] 和 [2,5,6] 。\n合并结果是 [1,2,2,3,5,6] ，其中斜体加粗标注的为 nums1 中的元素。\n\n示例 2：\n\n输入：nums1 = [1], m = 1, nums2 = [], n = 0\n\n输出：[1]\n\n解释：需要合并 [1] 和 [] 。\n合并结果是 [1] 。\n\n示例 3：\n\n输入：nums1 = [0], m = 0, nums2 = [1], n = 1\n\n输出：[1]\n\n解释：需要合并的数组是 [] 和 [1] 。\n合并结果是 [1] 。\n注意，因为 m = 0 ，所以 nums1 中没有元素。nums1 中仅存的 0 仅仅是为了确保合并结果可以顺利存放到 nums1 中。\n\n提示：\n\n- nums1.length == m + n\n- nums2.length == n\n- 0 <= m, n <= 200\n- 1 <= m + n <= 200\n- -10^9 <= nums1[i], nums2[j] <= 10^9\n\n进阶：你可以设计实现一个时间复杂度为 O(m + n) 的算法解决此问题吗？\n\n\n\n```java\nclass Solution {\n    public void merge(int[] nums1, int m, int[] nums2, int n) {\n        int i = m + n - 1;\n        while (n > 0) {\n            if (m > 0 && (nums1[m - 1] > nums2[n - 1])) {\n                nums1[i] = nums1[m - 1];\n                m--;\n            } else {\n                nums1[i] = nums2[n - 1];\n                n--;\n            }\n            i--;\n        }\n    }\n}\n```\n\n## 27. 移除元素\n\nhttps://leetcode.cn/problems/remove-element/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个数组 nums 和一个值 val，你需要 原地 移除所有数值等于 val 的元素。元素的顺序可能发生改变。然后返回 nums 中与 val\n不同的元素的数量。\n\n假设 nums 中不等于 val 的元素数量为 k，要通过此题，您需要执行以下操作：\n\n更改 nums 数组，使 nums 的前 k 个元素包含不等于 val 的元素。nums 的其余元素和 nums 的大小并不重要。\n返回 k。\n\n用户评测：\n\n评测机将使用以下代码测试您的解决方案：\n\n```java\nint[] nums = [...]; // 输入数组\nint val = ...; // 要移除的值\nint[] expectedNums = [...]; // 长度正确的预期答案。\n// 它以不等于 val 的值排序。\n\nint k = removeElement(nums, val); // 调用你的实现\n\nassert k ==expectedNums.length;\n\nsort(nums, 0,k); // 排序 nums 的前 k 个元素\nfor(\nint i = 0;\ni<actualLength;i++){\n        assert nums[i]==expectedNums[i];\n        }\n```\n\n如果所有的断言都通过，你的解决方案将会 通过。\n\n示例 1：\n\n输入：nums = [3,2,2,3], val = 3\n\n输出：2, nums = [2,2,_,_]\n\n解释：你的函数函数应该返回 k = 2, 并且 nums 中的前两个元素均为 2。\n你在返回的 k 个元素之外留下了什么并不重要（因此它们并不计入评测）。\n\n示例 2：\n\n输入：nums = [0,1,2,2,3,0,4,2], val = 2\n\n输出：5, nums = [0,1,4,0,3,_,_,_]\n\n解释：你的函数应该返回 k = 5，并且 nums 中的前五个元素为 0,0,1,3,4。\n注意这五个元素可以任意顺序返回。\n你在返回的 k 个元素之外留下了什么并不重要（因此它们并不计入评测）。\n\n提示：\n\n- 0 <= nums.length <= 100\n- 0 <= nums[i] <= 50\n- 0 <= val <= 100\n\n\n\n```java\nclass Solution {\n    public int removeElement(int[] nums, int val) {\n        int left = 0;\n        int right = nums.length;\n        while (left < right) {\n            if (nums[left] == val) {\n                nums[left] = nums[right - 1];\n                right--;\n            } else {\n                left++;\n            }\n        }\n        return left;\n    }\n}\n```\n\n## 26. 删除有序数组中的重复项\n\nhttps://leetcode.cn/problems/remove-duplicates-from-sorted-array/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个 非严格递增排列 的数组 nums ，请你 原地 删除重复出现的元素，使每个元素 只出现一次 ，返回删除后数组的新长度。元素的\n相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。\n\n考虑 nums 的唯一元素的数量为 k ，你需要做以下事情确保你的题解可以被通过：\n\n更改数组 nums ，使 nums 的前 k 个元素包含唯一元素，并按照它们最初在 nums 中出现的顺序排列。nums 的其余元素与 nums 的大小不重要。\n返回 k 。\n\n判题标准:\n\n系统会用下面的代码来测试你的题解:\n\n```java\nint[] nums = [...]; // 输入数组\nint[] expectedNums = [...]; // 长度正确的期望答案\n\nint k = removeDuplicates(nums); // 调用\n\nassert k ==expectedNums.length;\nfor(\nint i = 0;\ni<k;i++){\n        assert nums[i]==expectedNums[i];\n        }\n```\n\n如果所有断言都通过，那么您的题解将被 通过。\n\n示例 1：\n\n输入：nums = [1,1,2]\n\n输出：2, nums = [1,2,_]\n\n解释：函数应该返回新的长度 2 ，并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。\n\n示例 2：\n\n输入：nums = [0,0,1,1,1,2,2,3,3,4]\n\n输出：5, nums = [0,1,2,3,4]\n\n解释：函数应该返回新的长度 5 ， 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。\n\n提示：\n\n- 1 <= nums.length <= 3 * 10^4\n- -10^4 <= nums[i] <= 10^4\n- nums 已按 非严格递增 排列\n\n```java\nclass Solution {\n    public int removeDuplicates(int[] nums) {\n        if (nums.length == 0) {\n            return 0;\n        }\n        int left = 0;\n        int right = 1;\n        while (right < nums.length) {\n            if (nums[left] != nums[right]) {\n                nums[left + 1] = nums[right];\n                left++;\n            }\n            right++;\n        }\n        return left + 1;\n    }\n}\n```\n\n## 80. 删除有序数组中的重复项 II\n\nhttps://leetcode.cn/problems/remove-duplicates-from-sorted-array-ii/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个有序数组 nums ，请你 原地 删除重复出现的元素，使得出现次数超过两次的元素只出现两次 ，返回删除后数组的新长度。\n\n不要使用额外的数组空间，你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。\n\n说明：\n\n为什么返回数值是整数，但输出的答案是数组呢？\n\n请注意，输入数组是以「引用」方式传递的，这意味着在函数里修改输入数组对于调用者是可见的。\n\n你可以想象内部操作如下:\n\n```java\n// nums 是以“引用”方式传递的。也就是说，不对实参做任何拷贝\nint len = removeDuplicates(nums);\n\n// 在函数里修改输入数组对于调用者是可见的。\n// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。\nfor(\nint i = 0;\ni<len;i++){\n\nprint(nums[i]);\n}\n```\n\n示例 1：\n\n输入：nums = [1,1,1,2,2,3]\n\n输出：5, nums = [1,1,2,2,3]\n\n解释：函数应返回新长度 length = 5, 并且原数组的前五个元素被修改为 1, 1, 2, 2, 3。 不需要考虑数组中超出新长度后面的元素。\n\n示例 2：\n\n输入：nums = [0,0,1,1,1,1,2,3,3]\n\n输出：7, nums = [0,0,1,1,2,3,3]\n\n解释：函数应返回新长度 length = 7, 并且原数组的前七个元素被修改为 0, 0, 1, 1, 2, 3, 3。不需要考虑数组中超出新长度后面的元素。\n\n提示：\n\n- 1 <= nums.length <= 3 * 10^4\n- -10^4 <= nums[i] <= 10^4\n- nums 已按升序排列\n\n\n\n```java\nclass Solution {\n    public int removeDuplicates(int[] nums) {\n        int len = nums.length;\n        if (len <= 2) {\n            return len;\n        }\n        int slow = 2, fast = 2;\n        while (fast < len) {\n            if (nums[slow - 2] != nums[fast]) {\n                nums[slow] = nums[fast];\n                ++slow;\n            }\n            ++fast;\n        }\n        return slow;\n    }\n}\n```\n\n## 169. 多数元素\n\nhttps://leetcode.cn/problems/majority-element/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个大小为 n 的数组 nums ，返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。\n\n你可以假设数组是非空的，并且给定的数组总是存在多数元素。\n\n示例 1：\n\n输入：nums = [3,2,3]\n\n输出：3\n\n示例 2：\n\n输入：nums = [2,2,1,1,1,2,2]\n\n输出：2\n\n提示：\n\n- n == nums.length\n- 1 <= n <= 5 * 10^4\n- -10^9 <= nums[i] <= 10^9\n\n进阶：尝试设计时间复杂度为 O(n)、空间复杂度为 O(1) 的算法解决此问题。\n\n\n\n```java\nclass Solution {\n    public int majorityElement(int[] nums) {\n        Integer last = null;\n        int cnt = 0;\n        for (int num : nums) {\n            if (cnt == 0) {\n                last = num;\n            }\n            cnt += last == num ? 1 : -1;\n        }\n        return last;\n    }\n}\n```\n\n## 189. 轮转数组\n\nhttps://leetcode.cn/problems/rotate-array/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个整数数组 nums，将数组中的元素向右轮转 k 个位置，其中 k 是非负数。\n\n示例 1:\n\n输入: nums = [1,2,3,4,5,6,7], k = 3\n\n输出: [5,6,7,1,2,3,4]\n\n解释:\n向右轮转 1 步: [7,1,2,3,4,5,6]\n向右轮转 2 步: [6,7,1,2,3,4,5]\n向右轮转 3 步: [5,6,7,1,2,3,4]\n\n示例 2:\n\n输入：nums = [-1,-100,3,99], k = 2\n\n输出：[3,99,-1,-100]\n\n解释:\n向右轮转 1 步: [99,-1,-100,3]\n向右轮转 2 步: [3,99,-1,-100]\n\n提示：\n\n- 1 <= nums.length <= 10^5\n- -2^31 <= nums[i] <= 2^31 - 1\n- 0 <= k <= 10^5\n\n\n\n```java\nclass Solution {\n    public void rotate(int[] nums, int k) {\n        int len = nums.length;\n        k %= len;\n        reverse(nums, 0, len - 1);\n        reverse(nums, 0, k - 1);\n        reverse(nums, k, len - 1);\n    }\n\n    public void reverse(int[] nums, int start, int end) {\n        while (start < end) {\n            int tmp = nums[end];\n            nums[end] = nums[start];\n            nums[start] = tmp;\n            start++;\n            end--;\n        }\n    }\n}\n```\n\n## 121. 买卖股票的最佳时机\n\nhttps://leetcode.cn/problems/best-time-to-buy-and-sell-stock/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个数组 prices ，它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。\n\n你只能选择 某一天 买入这只股票，并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。\n\n返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润，返回 0 。\n\n示例 1：\n\n输入：[7,1,5,3,6,4]\n\n输出：5\n\n解释：在第 2 天（股票价格 = 1）的时候买入，在第 5 天（股票价格 = 6）的时候卖出，最大利润 = 6-1 = 5 。\n注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格；同时，你不能在买入前卖出股票。\n\n示例 2：\n\n输入：prices = [7,6,4,3,1]\n\n输出：0\n\n解释：在这种情况下, 没有交易完成, 所以最大利润为 0。\n\n提示：\n\n- 1 <= prices.length <= 10^5\n- 0 <= prices[i] <= 10^4\n\n\n\n```java\nclass Solution {\n    public int maxProfit(int[] prices) {\n        if (prices == null || prices.length == 0) {\n            return 0;\n        }\n        int max = 0;\n        int min = prices[0];\n        for (int i = 0; i < prices.length; i++) {\n            max = Math.max(max, prices[i] - min);\n            min = Math.min(min, prices[i]);\n        }\n        return max;\n    }\n}\n```\n\n## 122. 买卖股票的最佳时机 II\n\nhttps://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个整数数组 prices ，其中 prices[i] 表示某支股票第 i 天的价格。\n\n在每一天，你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买，然后在 同一天 出售。\n\n返回 你能获得的 最大 利润 。\n\n示例 1：\n\n输入：prices = [7,1,5,3,6,4]\n\n输出：7\n\n解释：在第 2 天（股票价格 = 1）的时候买入，在第 3 天（股票价格 = 5）的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。\n随后，在第 4 天（股票价格 = 3）的时候买入，在第 5 天（股票价格 = 6）的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3。\n最大总利润为 4 + 3 = 7 。\n\n示例 2：\n\n输入：prices = [1,2,3,4,5]\n\n输出：4\n\n解释：在第 1 天（股票价格 = 1）的时候买入，在第 5 天 （股票价格 = 5）的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。\n最大总利润为 4 。\n\n示例 3：\n\n输入：prices = [7,6,4,3,1]\n\n输出：0\n\n解释：在这种情况下, 交易无法获得正利润，所以不参与交易可以获得最大利润，最大利润为 0。\n\n提示：\n\n- 1 <= prices.length <= 3 * 10^4\n- 0 <= prices[i] <= 10^4\n\n\n\n```java\nclass Solution {\n    public int maxProfit(int[] prices) {\n        if (prices.length <= 1) {\n            return 0;\n        }\n        int max = 0;\n        for (int i = 1; i < prices.length; i++) {\n            if (prices[i] > prices[i - 1]) {\n                max += prices[i] - prices[i - 1];\n            }\n        }\n        return max;\n    }\n}\n```\n\n## 55. 跳跃游戏\n\nhttps://leetcode.cn/problems/jump-game/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个非负整数数组 nums ，你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。\n\n判断你是否能够到达最后一个下标，如果可以，返回 true ；否则，返回 false 。\n\n示例 1：\n\n输入：nums = [2,3,1,1,4]\n\n输出：true\n\n解释：可以先跳 1 步，从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。\n\n示例 2：\n\n输入：nums = [3,2,1,0,4]\n\n输出：false\n\n解释：无论怎样，总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 ， 所以永远不可能到达最后一个下标。\n\n提示：\n\n- 1 <= nums.length <= 10^4\n- 0 <= nums[i] <= 10^5\n\n\n\n```java\nclass Solution {\n    public boolean canJump(int[] nums) {\n        int n = nums.length;\n        int rightmost = 0;\n        for (int i = 0; i < n; i++) {\n            if (i <= rightmost) {\n                rightmost = Math.max(rightmost, i + nums[i]);\n                if (rightmost >= n - 1) {\n                    return true;\n                }\n            }\n        }\n        return false;\n    }\n}\n```\n\n## 45. 跳跃游戏 II\n\nhttps://leetcode.cn/problems/jump-game-ii/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。\n\n每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说，如果你在 nums[i] 处，你可以跳转到任意 nums[i + j] 处:\n\n- 0 <= j <= nums[i]\n- i + j < n\n\n返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]。\n\n示例 1:\n\n输入: nums = [2,3,1,1,4]\n\n输出: 2\n\n解释: 跳到最后一个位置的最小跳跃数是 2。\n从下标为 0 跳到下标为 1 的位置，跳 1 步，然后跳 3 步到达数组的最后一个位置。\n\n示例 2:\n\n输入: nums = [2,3,0,1,4]\n\n输出: 2\n\n提示:\n\n- 1 <= nums.length <= 10^4\n- 0 <= nums[i] <= 1000\n- 题目保证可以到达 nums[n-1]\n\n\n\n```java\nclass Solution {\n    public int jump(int[] nums) {\n        int ans = 0;\n        int max = 0;\n        int end = 0;\n        for (int i = 0; i < nums.length - 1; i++) {\n            max = Math.max(max, i + nums[i]);\n            if (i == end) {\n                end = max;\n                ans++;\n            }\n        }\n        return ans;\n    }\n}\n```\n\n## 274. H 指数\n\nhttps://leetcode.cn/problems/h-index/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个整数数组 citations ，其中 citations[i] 表示研究者的第 i 篇论文被引用的次数。计算并返回该研究者的 h 指数。\n\n根据维基百科上 h 指数的定义：h 代表“高引用次数” ，一名科研人员的 h 指数 是指他（她）至少发表了 h 篇论文，并且 至少 有 h\n篇论文被引用次数大于等于 h 。如果 h 有多种可能的值，h 指数 是其中最大的那个。\n\n示例 1：\n\n输入：citations = [3,0,6,1,5]\n\n输出：3\n\n解释：给定数组表示研究者总共有 5 篇论文，每篇论文相应的被引用了 3, 0, 6, 1, 5 次。\n由于研究者有 3 篇论文每篇 至少 被引用了 3 次，其余两篇论文每篇被引用 不多于 3 次，所以她的 h 指数是 3。\n\n示例 2：\n\n输入：citations = [1,3,1]\n\n输出：1\n\n提示：\n\n- n == citations.length\n- 1 <= n <= 5000\n- 0 <= citations[i] <= 1000\n\n\n\n```java\nclass Solution {\n    public int hIndex(int[] citations) {\n        Arrays.sort(citations);\n        int h = 0;\n        int i = citations.length - 1;\n        while (i >= 0 && citations[i] > h) {\n            h++;\n            i--;\n        }\n        return h;\n    }\n}\n```\n\n## 380. O(1) 时间插入、删除和获取随机元素\n\nhttps://leetcode.cn/problems/insert-delete-getrandom-o1/description/?envType=study-plan-v2&envId=top-interview-150\n\n实现RandomizedSet 类：\n\n- RandomizedSet() 初始化 RandomizedSet 对象\n- bool insert(int val) 当元素 val 不存在时，向集合中插入该项，并返回 true ；否则，返回 false 。\n- bool remove(int val) 当元素 val 存在时，从集合中移除该项，并返回 true ；否则，返回 false 。\n- int getRandom() 随机返回现有集合中的一项（测试用例保证调用此方法时集合中至少存在一个元素）。每个元素应该有 相同的概率\n  被返回。\n\n你必须实现类的所有函数，并满足每个函数的 平均 时间复杂度为 O(1) 。\n\n示例：\n\n输入\n\n[\"RandomizedSet\", \"insert\", \"remove\", \"insert\", \"getRandom\", \"remove\", \"insert\", \"getRandom\"]\n[[], [1], [2], [2], [], [1], [2], []]\n\n输出\n\n[null, true, false, true, 2, true, false, 2]\n\n解释\n\nRandomizedSet randomizedSet = new RandomizedSet();\n\nrandomizedSet.insert(1); // 向集合中插入 1 。返回 true 表示 1 被成功地插入。\n\nrandomizedSet.remove(2); // 返回 false ，表示集合中不存在 2 。\n\nrandomizedSet.insert(2); // 向集合中插入 2 。返回 true 。集合现在包含 [1,2] 。\n\nrandomizedSet.getRandom(); // getRandom 应随机返回 1 或 2 。\n\nrandomizedSet.remove(1); // 从集合中移除 1 ，返回 true 。集合现在包含 [2] 。\n\nrandomizedSet.insert(2); // 2 已在集合中，所以返回 false 。\n\nrandomizedSet.getRandom(); // 由于 2 是集合中唯一的数字，getRandom 总是返回 2 。\n\n提示：\n\n- -2^31 <= val <= 2^31 - 1\n- 最多调用 insert、remove 和 getRandom 函数 2 * 10^5 次\n- 在调用 getRandom 方法时，数据结构中 至少存在一个 元素。\n\n\n\n```java\nclass RandomizedSet {\n    List<Integer> nums;\n    Map<Integer, Integer> indices;\n    Random random;\n\n    public RandomizedSet() {\n        nums = new ArrayList<Integer>();\n        indices = new HashMap<Integer, Integer>();\n        random = new Random();\n    }\n\n    public boolean insert(int val) {\n        if (indices.containsKey(val)) {\n            return false;\n        }\n        int index = nums.size();\n        nums.add(val);\n        indices.put(val, index);\n        return true;\n    }\n\n    public boolean remove(int val) {\n        if (!indices.containsKey(val)) {\n            return false;\n        }\n        int index = indices.get(val);\n        int last = nums.get(nums.size() - 1);\n        nums.set(index, last);\n        indices.put(last, index);\n        nums.remove(nums.size() - 1);\n        indices.remove(val);\n        return true;\n    }\n\n    public int getRandom() {\n        int randomIndex = random.nextInt(nums.size());\n        return nums.get(randomIndex);\n    }\n}\n\n```\n\n## 238. 除自身以外数组的乘积\n\nhttps://leetcode.cn/problems/product-of-array-except-self/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个整数数组 nums，返回 数组 answer ，其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。\n\n题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。\n\n请 不要使用除法，且在 O(n) 时间复杂度内完成此题。\n\n示例 1:\n\n输入: nums = [1,2,3,4]\n\n输出: [24,12,8,6]\n\n示例 2:\n\n输入: nums = [-1,1,0,-3,3]\n\n输出: [0,0,9,0,0]\n\n提示：\n\n- 2 <= nums.length <= 10^5\n- -30 <= nums[i] <= 30\n- 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内\n\n\n\n```java\nclass Solution {\n    public int[] productExceptSelf(int[] nums) {\n        int length = nums.length;\n\n        // L 和 R 分别表示左右两侧的乘积列表\n        int[] L = new int[length];\n        int[] R = new int[length];\n\n        int[] answer = new int[length];\n\n        // L[i] 为索引 i 左侧所有元素的乘积\n        // 对于索引为 '0' 的元素，因为左侧没有元素，所以 L[0] = 1\n        L[0] = 1;\n        for (int i = 1; i < length; i++) {\n            L[i] = nums[i - 1] * L[i - 1];\n        }\n\n        // R[i] 为索引 i 右侧所有元素的乘积\n        // 对于索引为 'length-1' 的元素，因为右侧没有元素，所以 R[length-1] = 1\n        R[length - 1] = 1;\n        for (int i = length - 2; i >= 0; i--) {\n            R[i] = nums[i + 1] * R[i + 1];\n        }\n\n        // 对于索引 i，除 nums[i] 之外其余各元素的乘积就是左侧所有元素的乘积乘以右侧所有元素的乘积\n        for (int i = 0; i < length; i++) {\n            answer[i] = L[i] * R[i];\n        }\n\n        return answer;\n    }\n}\n```\n\n## 134. 加油站\n\nhttps://leetcode.cn/problems/gas-station/description/?envType=study-plan-v2&envId=top-interview-150\n\n在一条环路上有 n 个加油站，其中第 i 个加油站有汽油 gas[i] 升。\n\n你有一辆油箱容量无限的的汽车，从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发，开始时油箱为空。\n\n给定两个整数数组 gas 和 cost ，如果你可以按顺序绕环路行驶一周，则返回出发时加油站的编号，否则返回 -1 。如果存在解，则 保证 它是\n唯一 的。\n\n示例 1:\n\n输入: gas = [1,2,3,4,5], cost = [3,4,5,1,2]\n\n输出: 3\n\n解释:\n\n从 3 号加油站(索引为 3 处)出发，可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油\n\n- 开往 4 号加油站，此时油箱有 4 - 1 + 5 = 8 升汽油\n- 开往 0 号加油站，此时油箱有 8 - 2 + 1 = 7 升汽油\n- 开往 1 号加油站，此时油箱有 7 - 3 + 2 = 6 升汽油\n- 开往 2 号加油站，此时油箱有 6 - 4 + 3 = 5 升汽油\n- 开往 3 号加油站，你需要消耗 5 升汽油，正好足够你返回到 3 号加油站。\n- 因此，3 可为起始索引。\n\n示例 2:\n\n输入: gas = [2,3,4], cost = [3,4,3]\n\n输出: -1\n\n解释:\n\n- 你不能从 0 号或 1 号加油站出发，因为没有足够的汽油可以让你行驶到下一个加油站。\n- 我们从 2 号加油站出发，可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油\n- 开往 0 号加油站，此时油箱有 4 - 3 + 2 = 3 升汽油\n- 开往 1 号加油站，此时油箱有 3 - 3 + 3 = 3 升汽油\n- 你无法返回 2 号加油站，因为返程需要消耗 4 升汽油，但是你的油箱只有 3 升汽油。\n- 因此，无论怎样，你都不可能绕环路行驶一周。\n\n提示:\n\n- gas.length == n\n- cost.length == n\n- 1 <= n <= 10^5\n- 0 <= gas[i], cost[i] <= 10^4\n\n\n\n```java\nclass Solution {\n    public int canCompleteCircuit(int[] gas, int[] cost) {\n        int n = gas.length;\n        int i = 0;\n        while (i < n) {\n            int sumOfGas = 0, sumOfCost = 0;\n            int cnt = 0;\n            while (cnt < n) {\n                int j = (i + cnt) % n;\n                sumOfGas += gas[j];\n                sumOfCost += cost[j];\n                if (sumOfCost > sumOfGas) {\n                    break;\n                }\n                cnt++;\n            }\n            if (cnt == n) {\n                return i;\n            } else {\n                i = i + cnt + 1;\n            }\n        }\n        return -1;\n    }\n}\n```\n\n## 135. 分发糖果\n\nhttps://leetcode.cn/problems/candy/description/?envType=study-plan-v2&envId=top-interview-150\n\nn 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。\n\n你需要按照以下要求，给这些孩子分发糖果：\n\n- 每个孩子至少分配到 1 个糖果。\n- 相邻两个孩子评分更高的孩子会获得更多的糖果。\n- 请你给每个孩子分发糖果，计算并返回需要准备的 最少糖果数目 。\n\n示例 1：\n\n输入：ratings = [1,0,2]\n\n输出：5\n\n解释：你可以分别给第一个、第二个、第三个孩子分发 2、1、2 颗糖果。\n\n示例 2：\n\n输入：ratings = [1,2,2]\n\n输出：4\n\n解释：你可以分别给第一个、第二个、第三个孩子分发 1、2、1 颗糖果。\n第三个孩子只得到 1 颗糖果，这满足题面中的两个条件。\n\n提示：\n\n- n == ratings.length\n- 1 <= n <= 2 * 10^4\n- 0 <= ratings[i] <= 2 * 10^4\n\n\n\n```java\nclass Solution {\n    public int candy(int[] ratings) {\n        int n = ratings.length;\n        int[] left = new int[n];\n        for (int i = 0; i < n; i++) {\n            if (i > 0 && ratings[i] > ratings[i - 1]) {\n                left[i] = left[i - 1] + 1;\n            } else {\n                left[i] = 1;\n            }\n        }\n        int right = 0, res = 0;\n        for (int i = n - 1; i >= 0; i--) {\n            if (i < n - 1 && ratings[i] > ratings[i + 1]) {\n                right++;\n            } else {\n                right = 1;\n            }\n            res += Math.max(left[i], right);\n        }\n        return res;\n    }\n}\n```\n\n## 42. 接雨水\n\nhttps://leetcode.cn/problems/trapping-rain-water/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定 n 个非负整数表示每个宽度为 1 的柱子的高度图，计算按此排列的柱子，下雨之后能接多少雨水。\n\n示例 1：\n\n![接雨水.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E6%8E%A5%E9%9B%A8%E6%B0%B4.png)\n\n输入：height = [0,1,0,2,1,0,1,3,2,1,2,1]\n\n输出：6\n\n解释：上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图，在这种情况下，可以接 6 个单位的雨水（蓝色部分表示雨水）。\n\n示例 2：\n\n输入：height = [4,2,0,3,2,5]\n\n输出：9\n\n提示：\n\n- n == height.length\n- 1 <= n <= 2 * 10^4\n- 0 <= height[i] <= 10^5\n\n\n\n```java\nclass Solution {\n    public int trap(int[] height) {\n        int len = height.length;\n        int[] left = new int[len];\n        int[] right = new int[len];\n\n        for (int i = 1; i < len; i++) {\n            left[i] = Math.max(left[i - 1], height[i - 1]);\n        }\n        for (int i = len - 2; i >= 0; i--) {\n            right[i] = Math.max(right[i + 1], height[i + 1]);\n        }\n        int res = 0;\n        for (int i = 0; i < len; i++) {\n            int m = Math.min(left[i], right[i]);\n            res += Math.max(0, m - height[i]);\n        }\n        return res;\n    }\n}\n```\n\n## 13. 罗马数字转整数\n\nhttps://leetcode.cn/problems/roman-to-integer/description/?envType=study-plan-v2&envId=top-interview-150\n\n罗马数字包含以下七种字符: I， V， X， L，C，D 和 M。\n\n| 字符 | 数值   |\n|----|------|\n| I  | 1    |\n| V  | 5    |\n| X  | 10   |\n| L  | 50   |\n| C  | 100  |\n| D  | 500  |\n| M  | 1000 |\n\n例如， 罗马数字 2 写做 II ，即为两个并列的 1 。12 写做 XII ，即为 X + II 。 27 写做 XXVII, 即为 XX + V + II 。\n\n通常情况下，罗马数字中小的数字在大的数字的右边。但也存在特例，例如 4 不写做 IIII，而是 IV。数字 1 在数字 5 的左边，所表示的数等于大数\n5 减小数 1 得到的数值 4 。同样地，数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况：\n\n- I 可以放在 V (5) 和 X (10) 的左边，来表示 4 和 9。\n- X 可以放在 L (50) 和 C (100) 的左边，来表示 40 和 90。\n- C 可以放在 D (500) 和 M (1000) 的左边，来表示 400 和 900。\n\n给定一个罗马数字，将其转换成整数。\n\n示例 1:\n\n输入: s = \"III\"\n\n输出: 3\n\n示例 2:\n\n输入: s = \"IV\"\n\n输出: 4\n\n示例 3:\n\n输入: s = \"IX\"\n\n输出: 9\n\n示例 4:\n\n输入: s = \"LVIII\"\n\n输出: 58\n\n解释: L = 50, V= 5, III = 3.\n\n示例 5:\n\n输入: s = \"MCMXCIV\"\n\n输出: 1994\n\n解释: M = 1000, CM = 900, XC = 90, IV = 4.\n\n提示：\n\n- 1 <= s.length <= 15\n- s 仅含字符 ('I', 'V', 'X', 'L', 'C', 'D', 'M')\n- 题目数据保证 s 是一个有效的罗马数字，且表示整数在范围 [1, 3999] 内\n- 题目所给测试用例皆符合罗马数字书写规则，不会出现跨位等情况。\n- IL 和 IM 这样的例子并不符合题目要求，49 应该写作 XLIX，999 应该写作 CMXCIX 。\n- 关于罗马数字的详尽书写规则，可以参考 [罗马数字 - Mathematics](https://b2b.partcommunity.com/community/knowledge/zh_CN/detail/10753/%E7%BD%97%E9%A9%AC%E6%95%B0%E5%AD%97#knowledge_article) 。\n\n\n```java\nclass Solution {\n    \n    public int romanToInt(String s) {\n        HashMap<Character, Integer> map = new HashMap<>();\n        map.put('I', 1);\n        map.put('V', 5);\n        map.put('X', 10);\n        map.put('L', 50);\n        map.put('C', 100);\n        map.put('D', 500);\n        map.put('M', 1000);\n        int result = 0;\n        char[] str = s.toCharArray();\n        for (int i = 0; i < str.length-1; i++) {\n            if (map.get(str[i]) >= map.get(str[i + 1])) {\n                result += map.get(str[i]);\n            } else {\n                result -= map.get(str[i]);\n            }\n        }\n        result += map.get(str[str.length-1]);\n        return result;\n    }\n}\n```\n\n## 12. 整数转罗马数字\nhttps://leetcode.cn/problems/integer-to-roman/description/?envType=study-plan-v2&envId=top-interview-150\n\n七个不同的符号代表罗马数字，其值如下：\n\n| 字符 | 数值   |\n|----|------|\n| I  | 1    |\n| V  | 5    |\n| X  | 10   |\n| L  | 50   |\n| C  | 100  |\n| D  | 500  |\n| M  | 1000 |\n\n罗马数字是通过添加从最高到最低的小数位值的转换而形成的。将小数位值转换为罗马数字有以下规则：\n\n- 如果该值不是以 4 或 9 开头，请选择可以从输入中减去的最大值的符号，将该符号附加到结果，减去其值，然后将其余部分转换为罗马数字。\n- 如果该值以 4 或 9 开头，使用 减法形式，表示从以下符号中减去一个符号，例如 4 是 5 (V) 减 1 (I): IV ，9 是 10 (X) 减 1 (I)：IX。仅使用以下减法形式：4 (IV)，9 (IX)，40 (XL)，90 (XC)，400 (CD) 和 900 (CM)。\n- 只有 10 的次方（I, X, C, M）最多可以连续附加 3 次以代表 10 的倍数。你不能多次附加 5 (V)，50 (L) 或 500 (D)。如果需要将符号附加4次，请使用 减法形式。\n\n给定一个整数，将其转换为罗马数字。\n\n\n\n示例 1：\n\n输入：num = 3749\n\n输出： \"MMMDCCXLIX\"\n\n解释：\n\n- 3000 = MMM 由于 1000 (M) + 1000 (M) + 1000 (M)\n- 700 = DCC 由于 500 (D) + 100 (C) + 100 (C)\n- 40 = XL 由于 50 (L) 减 10 (X)\n- 9 = IX 由于 10 (X) 减 1 (I)\n\n注意：49 不是 50 (L) 减 1 (I) 因为转换是基于小数位\n\n示例 2：\n\n输入：num = 58\n\n输出：\"LVIII\"\n\n解释：\n\n- 50 = L\n- 8 = VIII\n\n示例 3：\n\n输入：num = 1994\n\n输出：\"MCMXCIV\"\n\n解释：\n\n- 1000 = M\n- 900 = CM\n- 90 = XC\n- 4 = IV\n\n\n提示：\n\n- 1 <= num <= 3999\n\n\n```java\nclass Solution {\n    int[] values = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1};\n    String[] symbols = {\"M\", \"CM\", \"D\", \"CD\", \"C\", \"XC\", \"L\", \"XL\", \"X\", \"IX\", \"V\", \"IV\", \"I\"};\n\n    public String intToRoman(int num) {\n        StringBuffer roman = new StringBuffer();\n        for (int i = 0; i < values.length; ++i) {\n            int value = values[i];\n            String symbol = symbols[i];\n            while (num >= value) {\n                num -= value;\n                roman.append(symbol);\n            }\n            if (num == 0) {\n                break;\n            }\n        }\n        return roman.toString();\n    }\n}\n```\n\n## 58. 最后一个单词的长度\nhttps://leetcode.cn/problems/length-of-last-word/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个字符串 s，由若干单词组成，单词前后用一些空格字符隔开。返回字符串中 最后一个 单词的长度。\n\n单词 是指仅由字母组成、不包含任何空格字符的最大 子字符串 。\n\n示例 1：\n\n输入：s = \"Hello World\"\n\n输出：5\n\n解释：最后一个单词是“World”，长度为 5。\n\n示例 2：\n\n输入：s = \"   fly me   to   the moon  \"\n\n输出：4\n\n解释：最后一个单词是“moon”，长度为 4。\n\n示例 3：\n\n输入：s = \"luffy is still joyboy\"\n\n输出：6\n\n解释：最后一个单词是长度为 6 的“joyboy”。\n\n\n提示：\n\n- 1 <= s.length <= 10^4\n- s 仅有英文字母和空格 ' ' 组成\n- s 中至少存在一个单词\n\n\n```java\nclass Solution {\n    public int lengthOfLastWord(String s) {\n        int index = s.length() - 1;\n        while (s.charAt(index) == ' ') {\n            index--;\n        }\n        int wordLength = 0;\n        while (index >= 0 && s.charAt(index) != ' ') {\n            wordLength++;\n            index--;\n        }\n        return wordLength;\n    }\n}\n```\n\n## 14. 最长公共前缀\nhttps://leetcode.cn/problems/longest-common-prefix/description/?envType=study-plan-v2&envId=top-interview-150\n\n编写一个函数来查找字符串数组中的最长公共前缀。\n\n如果不存在公共前缀，返回空字符串 \"\"。\n\n示例 1：\n\n输入：strs = [\"flower\",\"flow\",\"flight\"]\n\n输出：\"fl\"\n\n示例 2：\n\n输入：strs = [\"dog\",\"racecar\",\"car\"]\n\n输出：\"\"\n\n解释：输入不存在公共前缀。\n\n\n提示：\n\n- 1 <= strs.length <= 200\n- 0 <= strs[i].length <= 200\n- strs[i] 仅由小写英文字母组成\n\n\n```java\nclass Solution {\n    public String longestCommonPrefix(String[] strs) {\n        if(strs == null){\n            return \"\";\n        }\n        String tmp = strs[0];\n        for(String str:strs){\n            while(!str.startsWith(tmp)){\n                if(tmp.length() == 0){\n                    return \"\";\n                }\n                tmp = tmp.substring(0,tmp.length()-1);\n            }\n        }\n        return tmp;\n    }\n}\n```\n## 151. 反转字符串中的单词\nhttps://leetcode.cn/problems/reverse-words-in-a-string/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个字符串 s ，请你反转字符串中 单词 的顺序。\n\n单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。\n\n返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。\n\n注意：输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中，单词间应当仅用单个空格分隔，且不包含任何额外的空格。\n\n\n\n示例 1：\n\n输入：s = \"the sky is blue\"\n\n输出：\"blue is sky the\"\n\n示例 2：\n\n输入：s = \"  hello world  \"\n\n输出：\"world hello\"\n\n解释：反转后的字符串中不能存在前导空格和尾随空格。\n\n示例 3：\n\n输入：s = \"a good   example\"\n\n输出：\"example good a\"\n\n解释：如果两个单词间有多余的空格，反转后的字符串需要将单词间的空格减少到仅有一个。\n\n\n提示：\n\n- 1 <= s.length <= 10^4\n- s 包含英文大小写字母、数字和空格 ' '\n- s 中 至少存在一个 单词\n\n\n```java\nclass Solution {\n    public String reverseWords(String s) {\n        List<String> res = new ArrayList<>();\n        StringBuilder sb = new StringBuilder();\n\n        char[] arr = s.toCharArray();\n        for(int i=0;i<=arr.length;i++){\n            if(i == arr.length || arr[i]==' '){\n                if(sb.length()!=0){\n                    res.add(0,sb.toString());\n                    sb = new StringBuilder();\n                }\n            }else{\n                sb.append(arr[i]);\n            }\n        }\n        return String.join(\" \",res);\n    }\n}\n```\n\n## 6. Z 字形变换\n\nhttps://leetcode.cn/problems/zigzag-conversion/description/?envType=study-plan-v2&envId=top-interview-150\n\n将一个给定字符串 s 根据给定的行数 numRows ，以从上往下、从左到右进行 Z 字形排列。\n\n比如输入字符串为 \"PAYPALISHIRING\" 行数为 3 时，排列如下：\n\nP   A   H   N\n\nA P L S I I G\n\nY   I   R\n\n之后，你的输出需要从左往右逐行读取，产生出一个新的字符串，比如：\"PAHNAPLSIIGYIR\"。\n\n请你实现这个将字符串进行指定行数变换的函数：\n\nstring convert(string s, int numRows);\n\n\n示例 1：\n\n输入：s = \"PAYPALISHIRING\", numRows = 3\n\n输出：\"PAHNAPLSIIGYIR\"\n\n示例 2：\n\n输入：s = \"PAYPALISHIRING\", numRows = 4\n\n输出：\"PINALSIGYAHRPI\"\n\n解释：\n\nP     I    N\n\nA   L S  I G\n\nY A   H R\n\nP     I\n\n示例 3：\n\n输入：s = \"A\", numRows = 1\n\n输出：\"A\"\n\n\n提示：\n\n- 1 <= s.length <= 1000\n- s 由英文字母（小写和大写）、',' 和 '.' 组成\n- 1 <= numRows <= 1000\n\n\n```java\nclass Solution {\n    public String convert(String s, int numRows) {\n        if(numRows==1){\n            return s;\n        }\n\n        List<StringBuilder> res = new ArrayList<>();\n        for(int i=0;i<Math.min(s.length(),numRows);i++){\n            res.add(new StringBuilder());\n        }\n\n        int row = 0;\n        boolean down = false;\n\n        for(char c:s.toCharArray()){\n            res.get(row).append(c);\n            if(row==0|| row == numRows-1){\n                down = !down;\n            }\n            row += down?1:-1;\n        }\n\n        StringBuilder fs = new StringBuilder(); \n        for(StringBuilder sb:res){\n            fs.append(sb.toString());\n        }\n        return fs.toString();\n    }\n}\n```\n\n## 28. 找出字符串中第一个匹配项的下标\nhttps://leetcode.cn/problems/find-the-index-of-the-first-occurrence-in-a-string/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你两个字符串 haystack 和 needle ，请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标（下标从 0 开始）。如果 needle 不是 haystack 的一部分，则返回  -1 。\n\n\n\n示例 1：\n\n输入：haystack = \"sadbutsad\", needle = \"sad\"\n\n输出：0\n\n解释：\"sad\" 在下标 0 和 6 处匹配。\n\n第一个匹配项的下标是 0 ，所以返回 0 。\n\n示例 2：\n\n输入：haystack = \"leetcode\", needle = \"leeto\"\n\n输出：-1\n\n解释：\"leeto\" 没有在 \"leetcode\" 中出现，所以返回 -1 。\n\n\n提示：\n\n- 1 <= haystack.length, needle.length <= 10^4\n- haystack 和 needle 仅由小写英文字符组成\n\n\n```java\nclass Solution {\n    public int strStr(String haystack, String needle) {\n        if (needle.isEmpty()) {\n            return 0;\n        }\n\n        int m = haystack.length();\n        int n = needle.length();\n\n        for (int i = 0; i <= m - n; i++) {\n            int j;\n            for (j = 0; j < n; j++) {\n                if (haystack.charAt(i + j) != needle.charAt(j)) {\n                    break;\n                }\n            }\n            if (j == n) {\n                return i;  // 返回第一个匹配项的下标\n            }\n        }\n\n        return -1;  // 如果没有找到匹配项，返回 -1\n    }\n}\n```\n\n## 68. 文本左右对齐\nhttps://leetcode.cn/problems/text-justification/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个单词数组 words 和一个长度 maxWidth ，重新排版单词，使其成为每行恰好有 maxWidth 个字符，且左右两端对齐的文本。\n\n你应该使用 “贪心算法” 来放置给定的单词；也就是说，尽可能多地往每行中放置单词。必要时可用空格 ' ' 填充，使得每行恰好有 maxWidth 个字符。\n\n要求尽可能均匀分配单词间的空格数量。如果某一行单词间的空格不能均匀分配，则左侧放置的空格数要多于右侧的空格数。\n\n文本的最后一行应为左对齐，且单词之间不插入额外的空格。\n\n注意:\n\n- 单词是指由非空格字符组成的字符序列。\n- 每个单词的长度大于 0，小于等于 maxWidth。\n- 输入单词数组 words 至少包含一个单词。\n\n\n示例 1:\n\n输入: words = [\"This\", \"is\", \"an\", \"example\", \"of\", \"text\", \"justification.\"], maxWidth = 16\n\n输出:\n\n[\n\"This    is    an\",\n\n\"example  of text\",\n\n\"justification.  \"\n\n]\n\n示例 2:\n\n输入:words = [\"What\",\"must\",\"be\",\"acknowledgment\",\"shall\",\"be\"], maxWidth = 16\n\n输出:\n[\n\n\"What   must   be\",\n\n\"acknowledgment  \",\n\n\"shall be        \"\n\n]\n\n解释: 注意最后一行的格式应为 \"shall be    \" 而不是 \"shall     be\",\n因为最后一行应为左对齐，而不是左右两端对齐。       \n第二行同样为左对齐，这是因为这行只包含一个单词。\n\n示例 3:\n\n输入:words = [\"Science\",\"is\",\"what\",\"we\",\"understand\",\"well\",\"enough\",\"to\",\"explain\",\"to\",\"a\",\"computer.\",\"Art\",\"is\",\"everything\",\"else\",\"we\",\"do\"]，maxWidth = 20\n\n输出:\n\n[\n\n\"Science  is  what we\",\n\n\"understand      well\",\n\n\"enough to explain to\",\n\n\"a  computer.  Art is\",\n\n\"everything  else  we\",\n\n\"do                  \"\n\n]\n\n\n提示:\n\n- 1 <= words.length <= 300\n- 1 <= words[i].length <= 20\n- words[i] 由小写英文字母和符号组成\n- 1 <= maxWidth <= 100\n- words[i].length <= maxWidth\n\n\n```java\nclass Solution {\n    public List<String> fullJustify(String[] words, int maxWidth) {\n        List<String> ans = new ArrayList<String>();\n        int right = 0, n = words.length;\n        while (true) {\n            int left = right; // 当前行的第一个单词在 words 的位置\n            int sumLen = 0; // 统计这一行单词长度之和\n            // 循环确定当前行可以放多少单词，注意单词之间应至少有一个空格\n            while (right < n && sumLen + words[right].length() + right - left <= maxWidth) {\n                sumLen += words[right++].length();\n            }\n\n            // 当前行是最后一行：单词左对齐，且单词之间应只有一个空格，在行末填充剩余空格\n            if (right == n) {\n                StringBuffer sb = join(words, left, n, \" \");\n                sb.append(blank(maxWidth - sb.length()));\n                ans.add(sb.toString());\n                return ans;\n            }\n\n            int numWords = right - left;\n            int numSpaces = maxWidth - sumLen;\n\n            // 当前行只有一个单词：该单词左对齐，在行末填充剩余空格\n            if (numWords == 1) {\n                StringBuffer sb = new StringBuffer(words[left]);\n                sb.append(blank(numSpaces));\n                ans.add(sb.toString());\n                continue;\n            }\n\n            // 当前行不只一个单词\n            int avgSpaces = numSpaces / (numWords - 1);\n            int extraSpaces = numSpaces % (numWords - 1);\n            StringBuffer sb = new StringBuffer();\n            sb.append(join(words, left, left + extraSpaces + 1, blank(avgSpaces + 1))); // 拼接额外加一个空格的单词\n            sb.append(blank(avgSpaces));\n            sb.append(join(words, left + extraSpaces + 1, right, blank(avgSpaces))); // 拼接其余单词\n            ans.add(sb.toString());\n        }\n    }\n\n    // blank 返回长度为 n 的由空格组成的字符串\n    public String blank(int n) {\n        StringBuffer sb = new StringBuffer();\n        for (int i = 0; i < n; ++i) {\n            sb.append(' ');\n        }\n        return sb.toString();\n    }\n\n    // join 返回用 sep 拼接 [left, right) 范围内的 words 组成的字符串\n    public StringBuffer join(String[] words, int left, int right, String sep) {\n        StringBuffer sb = new StringBuffer(words[left]);\n        for (int i = left + 1; i < right; ++i) {\n            sb.append(sep);\n            sb.append(words[i]);\n        }\n        return sb;\n    }\n}\n```\n\n# 双指针\n## 125. 验证回文串\nhttps://leetcode.cn/problems/valid-palindrome/description/?envType=study-plan-v2&envId=top-interview-150\n\n如果在将所有大写字符转换为小写字符、并移除所有非字母数字字符之后，短语正着读和反着读都一样。则可以认为该短语是一个 回文串 。\n\n字母和数字都属于字母数字字符。\n\n给你一个字符串 s，如果它是 回文串 ，返回 true ；否则，返回 false 。\n\n\n\n示例 1：\n\n输入: s = \"A man, a plan, a canal: Panama\"\n\n输出：true\n\n解释：\"amanaplanacanalpanama\" 是回文串。\n\n示例 2：\n\n输入：s = \"race a car\"\n\n输出：false\n\n解释：\"raceacar\" 不是回文串。\n\n示例 3：\n\n输入：s = \" \"\n\n输出：true\n\n解释：在移除非字母数字字符之后，s 是一个空字符串 \"\" 。\n由于空字符串正着反着读都一样，所以是回文串。\n\n\n提示：\n\n- 1 <= s.length <= 2 * 10^5\n- s 仅由可打印的 ASCII 字符组成\n\n\n```java\nclass Solution {\n    public boolean isPalindrome(String s) {\n        if(s == null){\n            return false;\n        }\n        int length = s.length();\n        int left = 0, right = length-1;\n        while(left < right){\n            while(left < right && !isValid(s.charAt(left))){\n                left++;\n            }\n            while(left < right && !isValid(s.charAt(right))){\n                right--;\n            }\n            if(left < right && !isEqual(s.charAt(left),s.charAt(right))){\n                return false;\n            }\n            left++;\n            right--;\n        }\n        return true;\n    }\n\n    public boolean isValid(char c){\n        return Character.isLetter(c) || Character.isDigit(c);\n    }\n\n    public boolean isEqual(char a,char b){\n        return Character.toLowerCase(a) == Character.toLowerCase(b);\n    }\n}\n```\n\n## 392. 判断子序列\nhttps://leetcode.cn/problems/is-subsequence/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定字符串 s 和 t ，判断 s 是否为 t 的子序列。\n\n字符串的一个子序列是原始字符串删除一些（也可以不删除）字符而不改变剩余字符相对位置形成的新字符串。（例如，\"ace\"是\"abcde\"的一个子序列，而\"aec\"不是）。\n\n进阶：\n\n如果有大量输入的 S，称作 S1, S2, ... , Sk 其中 k >= 10亿，你需要依次检查它们是否为 T 的子序列。在这种情况下，你会怎样改变代码？\n\n致谢：\n\n特别感谢 @pbrother 添加此问题并且创建所有测试用例。\n\n\n\n示例 1：\n\n输入：s = \"abc\", t = \"ahbgdc\"\n\n输出：true\n\n示例 2：\n\n输入：s = \"axc\", t = \"ahbgdc\"\n\n输出：false\n\n\n提示：\n\n- 0 <= s.length <= 100\n- 0 <= t.length <= 10^4\n- 两个字符串都只由小写字符组成。\n\n\n```java\nclass Solution {\n    public boolean isSubsequence(String s, String t) {\n\n        int index = -1;\n        for(char c:s.toCharArray()){\n            index = t.indexOf(c,index+1);\n            if(index == -1){\n                return false;\n            }\n        }\n        return true;\n\n    }\n}\n```\n\n## 167. 两数之和 II - 输入有序数组\nhttps://leetcode.cn/problems/two-sum-ii-input-array-is-sorted/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个下标从 1 开始的整数数组 numbers ，该数组已按 非递减顺序排列  ，请你从数组中找出满足相加之和等于目标数 target 的两个数。如果设这两个数分别是 numbers[index1] 和 numbers[index2] ，则 1 <= index1 < index2 <= numbers.length 。\n\n以长度为 2 的整数数组 [index1, index2] 的形式返回这两个整数的下标 index1 和 index2。\n\n你可以假设每个输入 只对应唯一的答案 ，而且你 不可以 重复使用相同的元素。\n\n你所设计的解决方案必须只使用常量级的额外空间。\n\n\n示例 1：\n\n输入：numbers = [2,7,11,15], target = 9\n\n输出：[1,2]\n\n解释：2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。返回 [1, 2] 。\n\n示例 2：\n\n输入：numbers = [2,3,4], target = 6\n\n输出：[1,3]\n\n解释：2 与 4 之和等于目标数 6 。因此 index1 = 1, index2 = 3 。返回 [1, 3] 。\n\n示例 3：\n\n输入：numbers = [-1,0], target = -1\n\n输出：[1,2]\n\n解释：-1 与 0 之和等于目标数 -1 。因此 index1 = 1, index2 = 2 。返回 [1, 2] 。\n\n\n提示：\n\n- 2 <= numbers.length <= 3 * 10^4\n- -1000 <= numbers[i] <= 1000\n- numbers 按 非递减顺序 排列\n- -1000 <= target <= 1000\n- 仅存在一个有效答案\n\n```java\nclass Solution {\n    public int[] twoSum(int[] numbers, int target) {\n        int i = 0,j= numbers.length -1;\n        while(i<j){\n            int sum = numbers[i] + numbers[j];\n            if(sum == target){\n                return new int[]{i+1,j+1};\n            }else if(sum > target){\n                j--;\n            }else{\n                i++;\n            }\n        }\n        return null;\n    }\n}\n```\n## 11. 盛最多水的容器\nhttps://leetcode.cn/problems/container-with-most-water/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个长度为 n 的整数数组 height 。有 n 条垂线，第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。\n\n找出其中的两条线，使得它们与 x 轴共同构成的容器可以容纳最多的水。\n\n返回容器可以储存的最大水量。\n\n说明：你不能倾斜容器。\n\n\n\n示例 1：\n\n![盛水最多的容器.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E7%9B%9B%E6%B0%B4%E6%9C%80%E5%A4%9A%E7%9A%84%E5%AE%B9%E5%99%A8.png)\n\n输入：[1,8,6,2,5,4,8,3,7]\n\n输出：49\n\n解释：图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下，容器能够容纳水（表示为蓝色部分）的最大值为 49。\n\n示例 2：\n\n输入：height = [1,1]\n\n输出：1\n\n\n提示：\n\n- n == height.length\n- 2 <= n <= 10^5\n- 0 <= height[i] <= 10^4\n\n```java\nclass Solution {\n    public int maxArea(int[] height) {\n        int left = 0,right = height.length - 1;\n        int res = 0;\n        while(left < right){\n            int h = Math.min(height[left],height[right]);\n            res = Math.max(res,h * (right - left));\n            if(height[right] <= height[left]){\n                right--;\n            }else{\n                left++;\n            }\n        }\n        return res;\n    }\n}\n```\n\n## 15. 三数之和\n\nhttps://leetcode.cn/problems/3sum/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个整数数组 nums ，判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ，同时还满足 nums[i] + nums[j] + nums[k] == 0 。请\n\n你返回所有和为 0 且不重复的三元组。\n\n注意：答案中不可以包含重复的三元组。\n\n\n\n\n\n示例 1：\n\n输入：nums = [-1,0,1,2,-1,-4]\n\n输出：[[-1,-1,2],[-1,0,1]]\n\n解释：\n- nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。\n- nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。\n- nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。\n- 不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。\n\n注意，输出的顺序和三元组的顺序并不重要。\n\n示例 2：\n\n输入：nums = [0,1,1]\n\n输出：[]\n\n解释：唯一可能的三元组和不为 0 。\n\n示例 3：\n\n输入：nums = [0,0,0]\n\n输出：[[0,0,0]]\n\n解释：唯一可能的三元组和为 0 。\n\n\n提示：\n\n- 3 <= nums.length <= 3000\n- -10^5 <= nums[i] <= 10^5\n\n```java\nclass Solution {\n    public List<List<Integer>> threeSum(int[] nums) {\n        List<List<Integer>>  res = new ArrayList<>();\n        int n = nums.length;\n        Arrays.sort(nums);\n\n        for(int i=0;i<n;i++){\n            if(nums[i] >0){\n                return res;\n            }\n            if(i>0 && nums[i] == nums[i-1]){\n                continue;\n            }\n            int left = i+1;\n            int right = n-1;\n            while(left<right){\n                int sum = nums[i]+nums[left]+nums[right];\n                if(sum <0){\n                    left++;\n                }else if(sum >0){\n                    right--;\n                }else{\n                    res.add(Arrays.asList(nums[i],nums[left],nums[right]));\n                    while(left<right && nums[right] == nums[right-1]){\n                        right--;\n                    }\n                    while(left<right && nums[left] == nums[left+1]){\n                        left++;\n                    }\n                    right--;\n                    left++;\n                }\n            }\n        }\n        return res;\n    }\n}\n```\n\n# 滑动窗口\n## 209. 长度最小的子数组\nhttps://leetcode.cn/problems/minimum-size-subarray-sum/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个含有 n 个正整数的数组和一个正整数 target 。\n\n找出该数组中满足其总和大于等于 target 的长度最小的 子数组\n[numsl, numsl+1, ..., numsr-1, numsr] ，并返回其长度。如果不存在符合条件的子数组，返回 0 。\n\n\n\n示例 1：\n\n输入：target = 7, nums = [2,3,1,2,4,3]\n\n输出：2\n\n解释：子数组 [4,3] 是该条件下的长度最小的子数组。\n\n示例 2：\n\n输入：target = 4, nums = [1,4,4]\n\n输出：1\n\n示例 3：\n\n输入：target = 11, nums = [1,1,1,1,1,1,1,1]\n\n输出：0\n\n\n提示：\n\n- 1 <= target <= 10^9\n- 1 <= nums.length <= 10^5\n- 1 <= nums[i] <= 10^5\n\n```java\nclass Solution {\n    public int minSubArrayLen(int target, int[] nums) {\n        //滑动窗口\n         int left = 0;\n         int sum = 0;\n         int result = Integer.MAX_VALUE;\n\n         for(int right=0;right<nums.length;right++){\n             sum += nums[right];\n\n             while(sum >= target){\n                 result = Math.min(result,right-left+1);\n                 sum -= nums[left++];\n             }\n         }\n         return result == Integer.MAX_VALUE ? 0: result;\n    }\n}\n```\n\n## 3. 无重复字符的最长子串\n\nhttps://leetcode.cn/problems/longest-substring-without-repeating-characters/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个字符串 s ，请你找出其中不含有重复字符的 最长 子串 的长度。\n\n\n\n示例 1:\n\n输入: s = \"abcabcbb\"\n\n输出: 3\n\n解释: 因为无重复字符的最长子串是 \"abc\"，所以其长度为 3。\n\n示例 2:\n\n输入: s = \"bbbbb\"\n\n输出: 1\n\n解释: 因为无重复字符的最长子串是 \"b\"，所以其长度为 1。\n\n示例 3:\n\n输入: s = \"pwwkew\"\n\n输出: 3\n\n解释: 因为无重复字符的最长子串是 \"wke\"，所以其长度为 3。\n\n请注意，你的答案必须是 子串 的长度，\"pwke\" 是一个子序列，不是子串。\n\n\n提示：\n\n- 0 <= s.length <= 5 * 10^4\n- s 由英文字母、数字、符号和空格组成\n\n```java\nclass Solution {\n    public int lengthOfLongestSubstring(String s) {\n       Map<Character,Integer> map = new HashMap<>();\n       int res = 0;\n       for(int start =0,end =0;end < s.length();end++){\n           char right = s.charAt(end);\n           map.put(right,map.getOrDefault(right,0)+1);\n           while(map.get(right) >1){\n               char left = s.charAt(start);\n               map.put(left,map.get(left)-1);\n               start++;\n           }\n           res = Math.max(res,end-start+1);\n       } \n       return res;\n    }\n}\n```\n\n## 30. 串联所有单词的子串\nhttps://leetcode.cn/problems/substring-with-concatenation-of-all-words/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个字符串 s 和一个字符串数组 words。 words 中所有字符串 长度相同。\n\ns 中的 串联子串 是指一个包含  words 中所有字符串以任意顺序排列连接起来的子串。\n\n- 例如，如果 words = [\"ab\",\"cd\",\"ef\"]， 那么 \"abcdef\"， \"abefcd\"，\"cdabef\"， \"cdefab\"，\"efabcd\"， 和 \"efcdab\" 都是串联子串。 \"acdbef\" 不是串联子串，因为他不是任何 words 排列的连接。\n\n返回所有串联子串在 s 中的开始索引。你可以以 任意顺序 返回答案。\n\n\n\n示例 1：\n\n输入：s = \"barfoothefoobarman\", words = [\"foo\",\"bar\"]\n\n输出：[0,9]\n\n解释：因为 words.length == 2 同时 words[i].length == 3，连接的子字符串的长度必须为 6。\n子串 \"barfoo\" 开始位置是 0。它是 words 中以 [\"bar\",\"foo\"] 顺序排列的连接。\n子串 \"foobar\" 开始位置是 9。它是 words 中以 [\"foo\",\"bar\"] 顺序排列的连接。\n输出顺序无关紧要。返回 [9,0] 也是可以的。\n\n示例 2：\n\n输入：s = \"wordgoodgoodgoodbestword\", words = [\"word\",\"good\",\"best\",\"word\"]\n\n输出：[]\n\n解释：因为 words.length == 4 并且 words[i].length == 4，所以串联子串的长度必须为 16。\ns 中没有子串长度为 16 并且等于 words 的任何顺序排列的连接。\n所以我们返回一个空数组。\n\n示例 3：\n\n输入：s = \"barfoofoobarthefoobarman\", words = [\"bar\",\"foo\",\"the\"]\n\n输出：[6,9,12]\n\n解释：因为 words.length == 3 并且 words[i].length == 3，所以串联子串的长度必须为 9。\n子串 \"foobarthe\" 开始位置是 6。它是 words 中以 [\"foo\",\"bar\",\"the\"] 顺序排列的连接。\n子串 \"barthefoo\" 开始位置是 9。它是 words 中以 [\"bar\",\"the\",\"foo\"] 顺序排列的连接。\n子串 \"thefoobar\" 开始位置是 12。它是 words 中以 [\"the\",\"foo\",\"bar\"] 顺序排列的连接。\n\n\n提示：\n\n- 1 <= s.length <= 10^4\n- 1 <= words.length <= 5000\n- 1 <= words[i].length <= 30\n- words[i] 和 s 由小写英文字母组成\n\n```java\nclass Solution {\n    public List<Integer> findSubstring(String s, String[] words) {\n        int n = s.length(), m = words.length, w = words[0].length();\n        // 统计 words 中「每个目标单词」的出现次数\n        Map<String, Integer> map = new HashMap<>();\n        for (String str : words) map.put(str, map.getOrDefault(str, 0) + 1);\n        List<Integer> ans = new ArrayList<>();\n        for (int i = 0; i < w; i++) {\n            // 构建一个当前子串对应的哈希表，统计当前子串中「每个目标单词」的出现次数\n            Map<String, Integer> temp = new HashMap<>();\n            // 滑动窗口的大小固定是 m * w，每次将下一个单词添加进 temp，上一个单词移出 temp\n            for (int j = i; j + w <= n; j += w) {   \n                String cur = s.substring(j, j + w);\n                temp.put(cur, temp.getOrDefault(cur, 0) + 1);\n                if (j >= i + (m * w)) {\n                    int idx = j - m * w;\n                    String prev = s.substring(idx, idx + w);\n                    if (temp.get(prev) == 1) temp.remove(prev);\n                    else temp.put(prev, temp.get(prev) - 1);\n                    if (!temp.getOrDefault(prev, 0).equals(map.getOrDefault(prev, 0))) continue;\n                }\n                if (!temp.getOrDefault(cur, 0).equals(map.getOrDefault(cur, 0))) continue;\n                // 上面两个 continue 可以减少 map 之间的 equals 操作\n                if (temp.equals(map)) ans.add(j - (m - 1) * w);\n            }\n        }\n        return ans;\n    }\n}\n```\n\n## 76. 最小覆盖子串\nhttps://leetcode.cn/problems/minimum-window-substring/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串，则返回空字符串 \"\" 。\n\n\n\n注意：\n\n- 对于 t 中重复字符，我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。\n- 如果 s 中存在这样的子串，我们保证它是唯一的答案。\n\n\n示例 1：\n\n输入：s = \"ADOBECODEBANC\", t = \"ABC\"\n\n输出：\"BANC\"\n\n解释：最小覆盖子串 \"BANC\" 包含来自字符串 t 的 'A'、'B' 和 'C'。\n\n示例 2：\n\n输入：s = \"a\", t = \"a\"\n\n输出：\"a\"\n\n解释：整个字符串 s 是最小覆盖子串。\n\n示例 3:\n\n输入: s = \"a\", t = \"aa\"\n\n输出: \"\"\n\n解释: t 中两个字符 'a' 均应包含在 s 的子串中，\n因此没有符合条件的子字符串，返回空字符串。\n\n\n提示：\n\n- m == s.length\n- n == t.length\n- 1 <= m, n <= 10^5\n- s 和 t 由英文字母组成\n\n```java\nclass Solution {\n    public String minWindow(String s, String t) {\n        char[] cs = s.toCharArray();\n        char[] ct = t.toCharArray();\n\n        int[] count = new int[128];\n        // 将字符串t中每个字母出现的次数统计出来，这里--可以理解为有这么多的坑要填\n        for(char c: ct){\n            count[c]--;\n        }\n        String res = \"\";\n        //left=窗口左控制 right=窗口右控制\n        for(int left=0,right=0,cnt=0;right<cs.length;right++){\n            // 利用字符cs[right]去填count数组的坑\n            count[cs[right]]++;\n            // 如果填完坑之后发现，坑没有满或者刚好满，那么这个填坑是有效的，否则如果坑本来就是满的，这次填坑是无效的\n            // 注意其他非t中出现的字符，count数组的值是0，原来坑就是满的，那么填入count数组中，count[cs[right]]肯定大于0\n            if(count[cs[right]] <= 0){\n                cnt++;\n            }\n            // 如果cnt等于ct.length，那么说明窗口内已经包含t了，这时就要考虑移动左指针了，只有当左指针指向的字符是冗余的情况下，即count[cs[right]]>0，才能保证去掉该字符后，窗口中仍然包含t\n            // 注意cnt达到字符串t的长度后，它的值就不会再变化了，因为窗口内包含t之后，就会一直包含\n            while(cnt == ct.length && count[cs[left]] >0 ){\n                count[cs[left]]--;\n                left++;\n            }\n            // 当窗口内包含t后，计算此时窗口内字符串的长度，更新res\n            if(cnt == ct.length){\n                if(res.equals(\"\") || res.length() > (right-left+1)){\n                    res = s.substring(left,right+1);\n                }\n            }\n        }\n        return res;\n    }\n}\n```\n# 矩阵\n## 36. 有效的数独\nhttps://leetcode.cn/problems/valid-sudoku/description/?envType=study-plan-v2&envId=top-interview-150\n\n请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 ，验证已经填入的数字是否有效即可。\n\n- 数字 1-9 在每一行只能出现一次。\n- 数字 1-9 在每一列只能出现一次。\n- 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。（请参考示例图）\n\n\n注意：\n\n- 一个有效的数独（部分已被填充）不一定是可解的。\n- 只需要根据以上规则，验证已经填入的数字是否有效即可。\n- 空白格用 '.' 表示。\n\n\n示例 1：\n\n![有效的数独.png](..%2Fimg%2F%CB%E3%B7%A8%2F%D3%D0%D0%A7%B5%C4%CA%FD%B6%C0.png)\n\n输入：board =\n\n[[\"5\",\"3\",\".\",\".\",\"7\",\".\",\".\",\".\",\".\"]\n\n,[\"6\",\".\",\".\",\"1\",\"9\",\"5\",\".\",\".\",\".\"]\n\n,[\".\",\"9\",\"8\",\".\",\".\",\".\",\".\",\"6\",\".\"]\n\n,[\"8\",\".\",\".\",\".\",\"6\",\".\",\".\",\".\",\"3\"]\n\n,[\"4\",\".\",\".\",\"8\",\".\",\"3\",\".\",\".\",\"1\"]\n\n,[\"7\",\".\",\".\",\".\",\"2\",\".\",\".\",\".\",\"6\"]\n\n,[\".\",\"6\",\".\",\".\",\".\",\".\",\"2\",\"8\",\".\"]\n\n,[\".\",\".\",\".\",\"4\",\"1\",\"9\",\".\",\".\",\"5\"]\n\n,[\".\",\".\",\".\",\".\",\"8\",\".\",\".\",\"7\",\"9\"]]\n\n输出：true\n\n示例 2：\n\n输入：board =\n\n[[\"8\",\"3\",\".\",\".\",\"7\",\".\",\".\",\".\",\".\"]\n\n,[\"6\",\".\",\".\",\"1\",\"9\",\"5\",\".\",\".\",\".\"]\n\n,[\".\",\"9\",\"8\",\".\",\".\",\".\",\".\",\"6\",\".\"]\n\n,[\"8\",\".\",\".\",\".\",\"6\",\".\",\".\",\".\",\"3\"]\n\n,[\"4\",\".\",\".\",\"8\",\".\",\"3\",\".\",\".\",\"1\"]\n\n,[\"7\",\".\",\".\",\".\",\"2\",\".\",\".\",\".\",\"6\"]\n\n,[\".\",\"6\",\".\",\".\",\".\",\".\",\"2\",\"8\",\".\"]\n\n,[\".\",\".\",\".\",\"4\",\"1\",\"9\",\".\",\".\",\"5\"]\n\n,[\".\",\".\",\".\",\".\",\"8\",\".\",\".\",\"7\",\"9\"]]\n\n输出：false\n\n解释：除了第一行的第一个数字从 5 改为 8 以外，空格内其他数字均与 示例1 相同。 但由于位于左上角的 3x3 宫内有两个 8 存在, 因此这个数独是无效的。\n\n\n提示：\n\n- board.length == 9\n- board[i].length == 9\n- board[i][j] 是一位数字（1-9）或者 '.'\n\n```java\nclass Solution {\n    public boolean isValidSudoku(char[][] board) {\n        boolean[][] row = new boolean[10][10];\n        boolean[][] col = new boolean[10][10];\n        boolean[][] area = new boolean[10][10];\n\n        for(int i = 0;i< 9;i++){\n            for(int j=0;j<9;j++){\n                char c = board[i][j];\n                if(c == '.'){\n                    continue;\n                }\n                int idx = i /3 * 3 + j/3;\n                int n = c - '0';\n                if(row[i][n] || col[j][n] || area[idx][n]){\n                    return false;\n                }\n                row[i][n] = col[j][n] = area[idx][n] = true;\n            }\n        }\n        return true;\n    }\n}\n```\n\n## 54. 螺旋矩阵\nhttps://leetcode.cn/problems/spiral-matrix/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个 m 行 n 列的矩阵 matrix ，请按照 顺时针螺旋顺序 ，返回矩阵中的所有元素。\n\n\n\n示例 1：\n\n![螺旋矩阵1.png](..%2Fimg%2F%CB%E3%B7%A8%2F%C2%DD%D0%FD%BE%D8%D5%F31.png)\n\n输入：matrix = [[1,2,3],[4,5,6],[7,8,9]]\n\n输出：[1,2,3,6,9,8,7,4,5]\n\n示例 2：\n\n![螺旋矩阵2.png](..%2Fimg%2F%CB%E3%B7%A8%2F%C2%DD%D0%FD%BE%D8%D5%F32.png)\n\n输入：matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]\n\n输出：[1,2,3,4,8,12,11,10,9,5,6,7]\n\n\n提示：\n\n- m == matrix.length\n- n == matrix[i].length\n- 1 <= m, n <= 10\n- -100 <= matrix[i][j] <= 100\n\n```java\nclass Solution {\n    public List<Integer> spiralOrder(int[][] matrix) {\n        List<Integer> res = new ArrayList<>();\n        int m =matrix.length;\n        int n = matrix[0].length;\n        int left = 0,right = n-1,top = 0,boom = m-1;\n        int cnt = m*n;\n        while(cnt>=1){\n            for(int i=left;i<=right && cnt>=1;i++){\n                res.add(matrix[top][i]);\n                cnt--;\n            }\n            top++;\n            for(int i=top;i<=boom&& cnt>=1;i++){\n                res.add(matrix[i][right]);\n                cnt--;\n            }\n            right--;\n            for(int i=right;i>=left&& cnt>=1;i--){\n                res.add(matrix[boom][i]);\n                cnt--;\n            }\n            boom--;\n            for(int i=boom;i>=top&& cnt>=1;i--){\n                res.add(matrix[i][left]);\n                cnt--;\n            }\n            left++;\n        }\n        return res; \n    }\n}\n```\n\n## 48. 旋转图像\nhttps://leetcode.cn/problems/rotate-image/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。\n\n你必须在 原地 旋转图像，这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。\n\n示例 1：\n\n![旋转图像1.png](..%2Fimg%2F%CB%E3%B7%A8%2F%D0%FD%D7%AA%CD%BC%CF%F11.png)\n\n输入：matrix = [[1,2,3],[4,5,6],[7,8,9]]\n\n输出：[[7,4,1],[8,5,2],[9,6,3]]\n\n示例 2：\n\n![旋转图像2.png](..%2Fimg%2F%CB%E3%B7%A8%2F%D0%FD%D7%AA%CD%BC%CF%F12.png)\n\n输入：matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]\n\n输出：[[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]\n\n\n提示：\n\n- n == matrix.length == matrix[i].length\n- 1 <= n <= 20\n- -1000 <= matrix[i][j] <= 1000\n\n```java\nclass Solution {\n    public void rotate(int[][] matrix) {\n        int len = matrix.length;\n\n        for(int i=0;i<len;i++){\n            for(int j=0;j<i;j++){\n                int temp = matrix[j][i];\n                matrix[j][i] = matrix[i][j];\n                matrix[i][j] = temp;\n            }\n        }\n\n        for(int i=0;i<len;i++){\n            for(int j=0;j<len/2;j++){\n                int temp = matrix[i][len-j-1];\n                matrix[i][len-j-1] = matrix[i][j];\n                matrix[i][j] = temp;\n            }\n        }\n    }\n}\n```\n\n## 73. 矩阵置零\nhttps://leetcode.cn/problems/set-matrix-zeroes/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个 m x n 的矩阵，如果一个元素为 0 ，则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。\n\n示例 1：\n\n![矩阵置零1.png](..%2Fimg%2F%CB%E3%B7%A8%2F%BE%D8%D5%F3%D6%C3%C1%E31.png)\n\n输入：matrix = [[1,1,1],[1,0,1],[1,1,1]]\n\n输出：[[1,0,1],[0,0,0],[1,0,1]]\n\n示例 2：\n\n![矩阵置零2.png](..%2Fimg%2F%CB%E3%B7%A8%2F%BE%D8%D5%F3%D6%C3%C1%E32.png)\n\n输入：matrix = [[0,1,2,0],[3,4,5,2],[1,3,1,5]]\n\n输出：[[0,0,0,0],[0,4,5,0],[0,3,1,0]]\n\n\n提示：\n\n- m == matrix.length\n- n == matrix[0].length\n- 1 <= m, n <= 200\n- -2^31 <= matrix[i][j] <= 2^31 - 1\n\n```java\n我们可以用两个标记数组分别记录每一行和每一列是否有零出现。\n\n具体地，我们首先遍历该数组一次，如果某个元素为 000，那么就将该元素所在的行和列所对应标记数组的位置置为 true。最后我们再次遍历该数组，用标记数组更新原数组即可。\n\nclass Solution {\n  public void setZeroes(int[][] matrix) {\n    int m = matrix.length, n = matrix[0].length;\n    boolean[] row = new boolean[m];\n    boolean[] col = new boolean[n];\n    for (int i = 0; i < m; i++) {\n      for (int j = 0; j < n; j++) {\n        if (matrix[i][j] == 0) {\n          row[i] = col[j] = true;\n        }\n      }\n    }\n    for (int i = 0; i < m; i++) {\n      for (int j = 0; j < n; j++) {\n        if (row[i] || col[j]) {\n          matrix[i][j] = 0;\n        }\n      }\n    }\n  }\n}\n\n```\n\n## 289. 生命游戏\nhttps://leetcode.cn/problems/game-of-life/description/?envType=study-plan-v2&envId=top-interview-150\n\n根据 百度百科 ， 生命游戏 ，简称为 生命 ，是英国数学家约翰·何顿·康威在 1970 年发明的细胞自动机。\n\n给定一个包含 m × n 个格子的面板，每一个格子都可以看成是一个细胞。每个细胞都具有一个初始状态： 1 即为 活细胞 （live），或 0 即为 死细胞 （dead）。每个细胞与其八个相邻位置（水平，垂直，对角线）的细胞都遵循以下四条生存定律：\n\n1. 如果活细胞周围八个位置的活细胞数少于两个，则该位置活细胞死亡；\n1. 如果活细胞周围八个位置有两个或三个活细胞，则该位置活细胞仍然存活；\n1. 如果活细胞周围八个位置有超过三个活细胞，则该位置活细胞死亡；\n1. 如果死细胞周围正好有三个活细胞，则该位置死细胞复活；\n\n下一个状态是通过将上述规则同时应用于当前状态下的每个细胞所形成的，其中细胞的出生和死亡是同时发生的。给你 m x n 网格面板 board 的当前状态，返回下一个状态。\n\n\n\n示例 1：\n\n![生命游戏1.png](..%2Fimg%2F%CB%E3%B7%A8%2F%C9%FA%C3%FC%D3%CE%CF%B71.png)\n\n输入：board = [[0,1,0],[0,0,1],[1,1,1],[0,0,0]]\n\n输出：[[0,0,0],[1,0,1],[0,1,1],[0,1,0]]\n\n示例 2：\n\n![生命游戏2.png](..%2Fimg%2F%CB%E3%B7%A8%2F%C9%FA%C3%FC%D3%CE%CF%B72.png)\n\n输入：board = [[1,1],[1,0]]\n\n输出：[[1,1],[1,1]]\n\n\n提示：\n\n- m == board.length\n- n == board[i].length\n- 1 <= m, n <= 25\n- board[i][j] 为 0 或 1\n\n```java\nclass Solution {\n    public void gameOfLife(int[][] board) {\n        int[] neighbors = {0, 1, -1};\n\n        int rows = board.length;\n        int cols = board[0].length;\n\n        // 遍历面板每一个格子里的细胞\n        for (int row = 0; row < rows; row++) {\n            for (int col = 0; col < cols; col++) {\n\n                // 对于每一个细胞统计其八个相邻位置里的活细胞数量\n                int liveNeighbors = 0;\n\n                for (int i = 0; i < 3; i++) {\n                    for (int j = 0; j < 3; j++) {\n\n                        if (!(neighbors[i] == 0 && neighbors[j] == 0)) {\n                            // 相邻位置的坐标\n                            int r = (row + neighbors[i]);\n                            int c = (col + neighbors[j]);\n\n                            // 查看相邻的细胞是否是活细胞\n                            if ((r < rows && r >= 0) && (c < cols && c >= 0) && (Math.abs(board[r][c]) == 1)) {\n                                liveNeighbors += 1;\n                            }\n                        }\n                    }\n                }\n\n                // 规则 1 或规则 3 \n                if ((board[row][col] == 1) && (liveNeighbors < 2 || liveNeighbors > 3)) {\n                    // -1 代表这个细胞过去是活的现在死了\n                    board[row][col] = -1;\n                }\n                // 规则 4\n                if (board[row][col] == 0 && liveNeighbors == 3) {\n                    // 2 代表这个细胞过去是死的现在活了\n                    board[row][col] = 2;\n                }\n            }\n        }\n\n        // 遍历 board 得到一次更新后的状态\n        for (int row = 0; row < rows; row++) {\n            for (int col = 0; col < cols; col++) {\n                if (board[row][col] > 0) {\n                    board[row][col] = 1;\n                } else {\n                    board[row][col] = 0;\n                }\n            }\n        }\n    }\n}\n```\n\n# 哈希表\n## 383. 赎金信\nhttps://leetcode.cn/problems/ransom-note/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你两个字符串：ransomNote 和 magazine ，判断 ransomNote 能不能由 magazine 里面的字符构成。\n\n如果可以，返回 true ；否则返回 false 。\n\nmagazine 中的每个字符只能在 ransomNote 中使用一次。\n\n\n\n示例 1：\n\n输入：ransomNote = \"a\", magazine = \"b\"\n\n输出：false\n\n示例 2：\n\n输入：ransomNote = \"aa\", magazine = \"ab\"\n\n输出：false\n\n示例 3：\n\n输入：ransomNote = \"aa\", magazine = \"aab\"\n\n输出：true\n\n\n提示：\n\n- 1 <= ransomNote.length, magazine.length <= 10^5\n- ransomNote 和 magazine 由小写英文字母组成\n\n```java\nclass Solution {\n    public boolean canConstruct(String ransomNote, String magazine) {\n        if (ransomNote.length() > magazine.length()) {\n            return false;\n        }\n        int[] cnt = new int[26];\n        for (char c : magazine.toCharArray()) {\n            cnt[c - 'a']++;\n        }\n        for (char c : ransomNote.toCharArray()) {\n            cnt[c - 'a']--;\n            if(cnt[c - 'a'] < 0) {\n                return false;\n            }\n        }\n        return true;\n    }\n}\n```\n\n## 205. 同构字符串\nhttps://leetcode.cn/problems/isomorphic-strings/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定两个字符串 s 和 t ，判断它们是否是同构的。\n\n如果 s 中的字符可以按某种映射关系替换得到 t ，那么这两个字符串是同构的。\n\n每个出现的字符都应当映射到另一个字符，同时不改变字符的顺序。不同字符不能映射到同一个字符上，相同字符只能映射到同一个字符上，字符可以映射到自己本身。\n\n\n\n示例 1:\n\n输入：s = \"egg\", t = \"add\"\n\n输出：true\n\n示例 2：\n\n输入：s = \"foo\", t = \"bar\"\n\n输出：false\n\n示例 3：\n\n输入：s = \"paper\", t = \"title\"\n\n输出：true\n\n\n提示：\n\n- 1 <= s.length <= 5 * 10^4\n- t.length == s.length\n- s 和 t 由任意有效的 ASCII 字符组成\n\n```java\nclass Solution {\n    public boolean isIsomorphic(String s, String t) {\n        Map<Character, Character> s2t = new HashMap<Character, Character>();\n        Map<Character, Character> t2s = new HashMap<Character, Character>();\n        int len = s.length();\n        for (int i = 0; i < len; ++i) {\n            char x = s.charAt(i), y = t.charAt(i);\n            if ((s2t.containsKey(x) && s2t.get(x) != y) || (t2s.containsKey(y) && t2s.get(y) != x)) {\n                return false;\n            }\n            s2t.put(x, y);\n            t2s.put(y, x);\n        }\n        return true;\n    }\n}\n```\n\n## 290. 单词规律\nhttps://leetcode.cn/problems/word-pattern/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一种规律 pattern 和一个字符串 s ，判断 s 是否遵循相同的规律。\n\n这里的 遵循 指完全匹配，例如， pattern 里的每个字母和字符串 s 中的每个非空单词之间存在着双向连接的对应规律。\n\n\n\n示例1:\n\n输入: pattern = \"abba\", s = \"dog cat cat dog\"\n\n输出: true\n\n示例 2:\n\n输入:pattern = \"abba\", s = \"dog cat cat fish\"\n\n输出: false\n\n示例 3:\n\n输入: pattern = \"aaaa\", s = \"dog cat cat dog\"\n\n输出: false\n\n\n提示:\n\n- 1 <= pattern.length <= 300\n- pattern 只包含小写英文字母\n- 1 <= s.length <= 3000\n- s 只包含小写英文字母和 ' '\n- s 不包含 任何前导或尾随对空格\n- s 中每个单词都被 单个空格 分隔\n\n```java\nclass Solution {\n    public boolean wordPattern(String pattern, String str) {\n        Map<String, Character> str2ch = new HashMap<String, Character>();\n        Map<Character, String> ch2str = new HashMap<Character, String>();\n        int m = str.length();\n        int i = 0;\n        for (int p = 0; p < pattern.length(); ++p) {\n            char ch = pattern.charAt(p);\n            if (i >= m) {\n                return false;\n            }\n            int j = i;\n            while (j < m && str.charAt(j) != ' ') {\n                j++;\n            }\n            String tmp = str.substring(i, j);\n            if (str2ch.containsKey(tmp) && str2ch.get(tmp) != ch) {\n                return false;\n            }\n            if (ch2str.containsKey(ch) && !tmp.equals(ch2str.get(ch))) {\n                return false;\n            }\n            str2ch.put(tmp, ch);\n            ch2str.put(ch, tmp);\n            i = j + 1;\n        }\n        return i >= m;\n    }\n}\n```\n\n## 242. 有效的字母异位词\nhttps://leetcode.cn/problems/valid-anagram/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定两个字符串 s 和 t ，编写一个函数来判断 t 是否是 s 的字母异位词。\n\n注意：若 s 和 t 中每个字符出现的次数都相同，则称 s 和 t 互为字母异位词。\n\n\n\n示例 1:\n\n输入: s = \"anagram\", t = \"nagaram\"\n\n输出: true\n\n示例 2:\n\n输入: s = \"rat\", t = \"car\"\n\n输出: false\n\n\n提示:\n\n- 1 <= s.length, t.length <= 5 * 10^4\n- s 和 t 仅包含小写字母\n\n```java\nclass Solution {\n    public boolean isAnagram(String s, String t) {\n        if (s.length() != t.length()) {\n            return false;\n        }\n        int[] table = new int[26];\n        for (int i = 0; i < s.length(); i++) {\n            table[s.charAt(i) - 'a']++;\n        }\n        for (int i = 0; i < t.length(); i++) {\n            table[t.charAt(i) - 'a']--;\n            if (table[t.charAt(i) - 'a'] < 0) {\n                return false;\n            }\n        }\n        return true;\n    }\n}\n```\n\n## 49. 字母异位词分组\nhttps://leetcode.cn/problems/group-anagrams/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个字符串数组，请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。\n\n字母异位词 是由重新排列源单词的所有字母得到的一个新单词。\n\n\n\n示例 1:\n\n输入: strs = [\"eat\", \"tea\", \"tan\", \"ate\", \"nat\", \"bat\"]\n\n输出: [[\"bat\"],[\"nat\",\"tan\"],[\"ate\",\"eat\",\"tea\"]]\n\n示例 2:\n\n输入: strs = [\"\"]\n\n输出: [[\"\"]]\n\n示例 3:\n\n输入: strs = [\"a\"]\n\n输出: [[\"a\"]]\n\n\n提示：\n\n- 1 <= strs.length <= 10^4\n- 0 <= strs[i].length <= 100\n- strs[i] 仅包含小写字母\n\n```java\nclass Solution {\n    public List<List<String>> groupAnagrams(String[] strs) {\n        Map<String, List<String>> map = new HashMap<String, List<String>>();\n        for (String str : strs) {\n            char[] array = str.toCharArray();\n            Arrays.sort(array);\n            String key = new String(array);\n            List<String> list = map.getOrDefault(key, new ArrayList<String>());\n            list.add(str);\n            map.put(key, list);\n        }\n        return new ArrayList<List<String>>(map.values());\n    }\n}\n```\n\n## 1. 两数之和\nhttps://leetcode.cn/problems/two-sum/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个整数数组 nums 和一个整数目标值 target，请你在该数组中找出 和为目标值 target  的那 两个 整数，并返回它们的数组下标。\n\n你可以假设每种输入只会对应一个答案。但是，数组中同一个元素在答案里不能重复出现。\n\n你可以按任意顺序返回答案。\n\n\n\n示例 1：\n\n输入：nums = [2,7,11,15], target = 9\n\n输出：[0,1]\n\n解释：因为 nums[0] + nums[1] == 9 ，返回 [0, 1] 。\n\n示例 2：\n\n输入：nums = [3,2,4], target = 6\n\n输出：[1,2]\n\n示例 3：\n\n输入：nums = [3,3], target = 6\n\n输出：[0,1]\n\n```java\nclass Solution {\n    public int[] twoSum(int[] nums, int target) {\n        Map<Integer,Integer> map =new HashMap<>();\n        for(int i=0;i<nums.length;i++){\n            if(map.containsKey(nums[i])){\n                return new int[]{i,map.get(nums[i])};\n            }else{\n                map.put(target-nums[i],i);\n            }\n        }\n        return new int[]{};\n    }\n}\n```\n\n## 202. 快乐数\nhttps://leetcode.cn/problems/happy-number/description/?envType=study-plan-v2&envId=top-interview-150\n\n编写一个算法来判断一个数 n 是不是快乐数。\n\n「快乐数」 定义为：\n\n- 对于一个正整数，每一次将该数替换为它每个位置上的数字的平方和。\n- 然后重复这个过程直到这个数变为 1，也可能是 无限循环 但始终变不到 1。\n- 如果这个过程 结果为 1，那么这个数就是快乐数。\n\n如果 n 是 快乐数 就返回 true ；不是，则返回 false 。\n\n\n\n示例 1：\n\n输入：n = 19\n\n输出：true\n\n解释：\n\n12 + 92 = 82\n\n82 + 22 = 68\n\n62 + 82 = 100\n\n12 + 02 + 02 = 1\n\n示例 2：\n\n输入：n = 2\n\n输出：false\n\n\n提示：\n\n- 1 <= n <= 2^31 - 1\n\n```java\n//快慢指针法\n\nclass Solution {\n\n     public int getNext(int n) {\n        int totalSum = 0;\n        while (n > 0) {\n            int d = n % 10;\n            n = n / 10;\n            totalSum += d * d;\n        }\n        return totalSum;\n    }\n\n    public boolean isHappy(int n) {\n        int slowRunner = n;\n        int fastRunner = getNext(n);\n        while (fastRunner != 1 && slowRunner != fastRunner) {\n            slowRunner = getNext(slowRunner);\n            fastRunner = getNext(getNext(fastRunner));\n        }\n        return fastRunner == 1;\n    }\n}\n```\n\n## 219. 存在重复元素 II\nhttps://leetcode.cn/problems/contains-duplicate-ii/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个整数数组 nums 和一个整数 k ，判断数组中是否存在两个 不同的索引 i 和 j ，满足 nums[i] == nums[j] 且 abs(i - j) <= k 。如果存在，返回 true ；否则，返回 false 。\n\n\n\n示例 1：\n\n输入：nums = [1,2,3,1], k = 3\n\n输出：true\n\n示例 2：\n\n输入：nums = [1,0,1,1], k = 1\n\n输出：true\n\n示例 3：\n\n输入：nums = [1,2,3,1,2,3], k = 2\n\n输出：false\n\n\n\n\n提示：\n\n- 1 <= nums.length <= 10^5\n- -10^9 <= nums[i] <= 10^9\n- 0 <= k <= 10^5\n\n```java\n//滑动窗口\nclass Solution {\n  public boolean containsNearbyDuplicate(int[] nums, int k) {\n    Set<Integer> set = new HashSet<Integer>();\n    int length = nums.length;\n    for (int i = 0; i < length; i++) {\n      if (i > k) {\n        set.remove(nums[i - k - 1]);\n      }\n      if (!set.add(nums[i])) {\n        return true;\n      }\n    }\n    return false;\n  }\n}\n```\n\n## 128. 最长连续序列\nhttps://leetcode.cn/problems/longest-consecutive-sequence/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个未排序的整数数组 nums ，找出数字连续的最长序列（不要求序列元素在原数组中连续）的长度。\n\n请你设计并实现时间复杂度为 O(n) 的算法解决此问题。\n\n\n\n示例 1：\n\n输入：nums = [100,4,200,1,3,2]\n\n输出：4\n\n解释：最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。\n\n示例 2：\n\n输入：nums = [0,3,7,2,5,8,4,6,0,1]\n\n输出：9\n\n\n提示：\n\n- 0 <= nums.length <= 10^5\n- -10^9 <= nums[i] <= 10^9\n\n```java\nclass Solution {\n    public int longestConsecutive(int[] nums) {\n        Set<Integer> set = new HashSet<>();\n        for(int n:nums){\n            set.add(n);\n        }\n        int max=0;\n        for(int n:nums){\n            if(!set.contains(n-1)){\n                int cnt = 1;\n                int cur = n;\n                while(set.contains(cur+1)){\n                    cur+=1;\n                    cnt+=1;\n                }\n                max = Math.max(max,cnt);\n            }\n        }\n        return max;\n    }\n}   \n```\n# 区间\n## 228. 汇总区间\nhttps://leetcode.cn/problems/summary-ranges/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个  无重复元素 的 有序 整数数组 nums 。\n\n返回 恰好覆盖数组中所有数字 的 最小有序 区间范围列表 。也就是说，nums 的每个元素都恰好被某个区间范围所覆盖，并且不存在属于某个范围但不属于 nums 的数字 x 。\n\n列表中的每个区间范围 [a,b] 应该按如下格式输出：\n\n- \"a->b\" ，如果 a != b\n- \"a\" ，如果 a == b\n\n\n示例 1：\n\n输入：nums = [0,1,2,4,5,7]\n\n输出：[\"0->2\",\"4->5\",\"7\"]\n\n解释：区间范围是：\n\n[0,2] --> \"0->2\"\n\n[4,5] --> \"4->5\"\n\n[7,7] --> \"7\"\n\n示例 2：\n\n输入：nums = [0,2,3,4,6,8,9]\n\n输出：[\"0\",\"2->4\",\"6\",\"8->9\"]\n\n解释：区间范围是：\n\n[0,0] --> \"0\"\n\n[2,4] --> \"2->4\"\n\n[6,6] --> \"6\"\n\n[8,9] --> \"8->9\"\n\n\n提示：\n\n- 0 <= nums.length <= 20\n- -2^31 <= nums[i] <= 2^31 - 1\n- nums 中的所有值都 互不相同\n- nums 按升序排列\n\n```java\nclass Solution {\n    public List<String> summaryRanges(int[] nums) {\n        List<String> ret = new ArrayList<String>();\n        int i = 0;\n        int n = nums.length;\n        while (i < n) {\n            int low = i;\n            i++;\n            while (i < n && nums[i] == nums[i - 1] + 1) {\n                i++;\n            }\n            int high = i - 1;\n            StringBuffer temp = new StringBuffer(Integer.toString(nums[low]));\n            if (low < high) {\n                temp.append(\"->\");\n                temp.append(Integer.toString(nums[high]));\n            }\n            ret.add(temp.toString());\n        }\n        return ret;\n    }\n}\n```\n\n## 56. 合并区间\nhttps://leetcode.cn/problems/merge-intervals/description/?envType=study-plan-v2&envId=top-interview-150\n\n以数组 intervals 表示若干个区间的集合，其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间，并返回 一个不重叠的区间数组，该数组需恰好覆盖输入中的所有区间 。\n\n\n\n示例 1：\n\n输入：intervals = [[1,3],[2,6],[8,10],[15,18]]\n\n输出：[[1,6],[8,10],[15,18]]\n\n解释：区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].\n\n示例 2：\n\n输入：intervals = [[1,4],[4,5]]\n\n输出：[[1,5]]\n\n解释：区间 [1,4] 和 [4,5] 可被视为重叠区间。\n\n\n提示：\n\n- 1 <= intervals.length <= 10^4\n- intervals[i].length == 2\n- 0 <= starti <= endi <= 10^4\n\n```java\nclass Solution {\n    public int[][] merge(int[][] intervals) {\n        Arrays.sort(intervals,(o1,o2)->Integer.compare(o1[0],o2[0]));\n        List<int[]> res = new ArrayList<>();\n        for(int i=0;i< intervals.length;i++){\n            int left = intervals[i][0],right = intervals[i][1];\n            int size = res.size();\n            if(size==0 || res.get(size-1)[1] < left){\n                res.add(new int[]{left,right});\n            }else{\n                res.get(size-1)[1] = Math.max(res.get(size-1)[1],right); \n            }\n        }\n        return res.toArray(new int[res.size()][]);\n    }\n}\n```\n## 57. 插入区间\nhttps://leetcode.cn/problems/insert-interval/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个 无重叠的 ，按照区间起始端点排序的区间列表 intervals，其中 intervals[i] = [starti, endi] 表示第 i 个区间的开始和结束，并且 intervals 按照 starti 升序排列。同样给定一个区间 newInterval = [start, end] 表示另一个区间的开始和结束。\n\n在 intervals 中插入区间 newInterval，使得 intervals 依然按照 starti 升序排列，且区间之间不重叠（如果有必要的话，可以合并区间）。\n\n返回插入之后的 intervals。\n\n注意 你不需要原地修改 intervals。你可以创建一个新数组然后返回它。\n\n\n\n示例 1：\n\n输入：intervals = [[1,3],[6,9]], newInterval = [2,5]\n\n输出：[[1,5],[6,9]]\n\n示例 2：\n\n输入：intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8]\n\n输出：[[1,2],[3,10],[12,16]]\n\n解释：这是因为新的区间 [4,8] 与 [3,5],[6,7],[8,10] 重叠。\n\n\n提示：\n\n- 0 <= intervals.length <= 10^4\n- intervals[i].length == 2\n- 0 <= starti <= endi <= 10^5\n- intervals 根据 starti 按 升序 排列\n- newInterval.length == 2\n- 0 <= start <= end <= 10^5\n\n```java\nclass Solution {\n    public int[][] insert(int[][] intervals, int[] newInterval) {\n        int left = newInterval[0];\n        int right = newInterval[1];\n        boolean placed = false;\n        List<int[]> ansList = new ArrayList<int[]>();\n        for (int[] interval : intervals) {\n            if (interval[0] > right) {\n                // 在插入区间的右侧且无交集\n                if (!placed) {\n                    ansList.add(new int[]{left, right});\n                    placed = true;                    \n                }\n                ansList.add(interval);\n            } else if (interval[1] < left) {\n                // 在插入区间的左侧且无交集\n                ansList.add(interval);\n            } else {\n                // 与插入区间有交集，计算它们的并集\n                left = Math.min(left, interval[0]);\n                right = Math.max(right, interval[1]);\n            }\n        }\n        if (!placed) {\n            ansList.add(new int[]{left, right});\n        }\n        int[][] ans = new int[ansList.size()][2];\n        for (int i = 0; i < ansList.size(); ++i) {\n            ans[i] = ansList.get(i);\n        }\n        return ans;\n    }\n}\n```\n\n## 452. 用最少数量的箭引爆气球\nhttps://leetcode.cn/problems/minimum-number-of-arrows-to-burst-balloons/description/?envType=study-plan-v2&envId=top-interview-150\n\n有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ，其中points[i] = [xstart, xend] 表示水平直径在 xstart 和 xend之间的气球。你不知道气球的确切 y 坐标。\n\n一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭，若有一个气球的直径的开始和结束坐标为 xstart，xend， 且满足  xstart ≤ x ≤ xend，则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后，可以无限地前进。\n\n给你一个数组 points ，返回引爆所有气球所必须射出的 最小 弓箭数 。\n\n\n示例 1：\n\n输入：points = [[10,16],[2,8],[1,6],[7,12]]\n\n输出：2\n\n解释：气球可以用2支箭来爆破:\n\n-在x = 6处射出箭，击破气球[2,8]和[1,6]。\n\n-在x = 11处发射箭，击破气球[10,16]和[7,12]。\n\n示例 2：\n\n输入：points = [[1,2],[3,4],[5,6],[7,8]]\n\n输出：4\n\n解释：每个气球需要射出一支箭，总共需要4支箭。\n\n示例 3：\n\n输入：points = [[1,2],[2,3],[3,4],[4,5]]\n\n输出：2\n\n解释：气球可以用2支箭来爆破:\n- 在x = 2处发射箭，击破气球[1,2]和[2,3]。\n- 在x = 4处射出箭，击破气球[3,4]和[4,5]。\n\n\n提示:\n\n- 1 <= points.length <= 10^5\n- points[i].length == 2\n- -2^31 <= xstart < xend <= 2^31 - 1\n\n```java\nclass Solution {\n    public int findMinArrowShots(int[][] points) {\n        if(points.length ==0){\n            return 0;\n        }\n        Arrays.sort(points,Comparator.comparingInt(o->o[1]));\n        int cnt=1;\n        int end = points[0][1];\n        for(int i=1;i<points.length;i++){\n            if(points[i][0]<=end){\n                continue;\n            }\n            cnt++;\n            end = points[i][1];\n        }\n        return cnt;\n    }\n}\n```\n\n# 栈\n## 20. 有效的括号\nhttps://leetcode.cn/problems/valid-parentheses/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个只包括 '('，')'，'{'，'}'，'['，']' 的字符串 s ，判断字符串是否有效。\n\n有效字符串需满足：\n\n- 左括号必须用相同类型的右括号闭合。\n- 左括号必须以正确的顺序闭合。\n- 每个右括号都有一个对应的相同类型的左括号。\n\n\n示例 1：\n\n输入：s = \"()\"\n\n输出：true\n\n示例 2：\n\n输入：s = \"()[]{}\"\n\n输出：true\n\n示例 3：\n\n输入：s = \"(]\"\n\n输出：false\n\n\n提示：\n\n- 1 <= s.length <= 10^4\n- s 仅由括号 '()[]{}' 组成\n\n```java\nclass Solution {\n  public boolean isValid(String s) {\n    if(s == null || s.length()%2!=0){\n      return false;\n    }\n    Map<Character,Character> map = new HashMap<>(){\n      {\n        put(')','(');\n        put('}','{');\n        put(']','[');\n      }\n    };\n    Deque<Character> dq = new LinkedList<>();\n    for(int i=0;i<s.length();i++){\n      char c = s.charAt(i);\n      if(map.containsKey(c)){\n        if(dq.isEmpty() || map.get(c) != dq.peek()){\n          return false;\n        }\n        dq.pop();\n      }else{\n        dq.push(c);\n      }\n    }\n    return dq.isEmpty();\n  }\n}\n```\n\n## 71. 简化路径\nhttps://leetcode.cn/problems/simplify-path/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个字符串 path ，表示指向某一文件或目录的 Unix 风格 绝对路径 （以 '/' 开头），请你将其转化为更加简洁的规范路径。\n\n在 Unix 风格的文件系统中，一个点（.）表示当前目录本身；此外，两个点 （..） 表示将目录切换到上一级（指向父目录）；两者都可以是复杂相对路径的组成部分。任意多个连续的斜杠（即，'//'）都被视为单个斜杠 '/' 。 对于此问题，任何其他格式的点（例如，'...'）均被视为文件/目录名称。\n\n请注意，返回的 规范路径 必须遵循下述格式：\n\n- 始终以斜杠 '/' 开头。\n- 两个目录名之间必须只有一个斜杠 '/' 。\n- 最后一个目录名（如果存在）不能 以 '/' 结尾。\n- 此外，路径仅包含从根目录到目标文件或目录的路径上的目录（即，不含 '.' 或 '..'）。\n\n返回简化后得到的 规范路径 。\n\n\n\n示例 1：\n\n输入：path = \"/home/\"\n\n输出：\"/home\"\n\n解释：注意，最后一个目录名后面没有斜杠。\n\n示例 2：\n\n输入：path = \"/../\"\n\n输出：\"/\"\n\n解释：从根目录向上一级是不可行的，因为根目录是你可以到达的最高级。\n\n示例 3：\n\n输入：path = \"/home//foo/\"\n\n输出：\"/home/foo\"\n\n解释：在规范路径中，多个连续斜杠需要用一个斜杠替换。\n\n示例 4：\n\n输入：path = \"/a/./b/../../c/\"\n\n输出：\"/c\"\n\n\n提示：\n\n- 1 <= path.length <= 3000\n- path 由英文字母，数字，'.'，'/' 或 '_' 组成。\n- path 是一个有效的 Unix 风格绝对路径。\n\n```java\nclass Solution {\n    public String simplifyPath(String path) {\n        String[] names = path.split(\"/\");\n        Deque<String> stack = new ArrayDeque<String>();\n        for (String name : names) {\n            if (\"..\".equals(name)) {\n                if (!stack.isEmpty()) {\n                    stack.pollLast();\n                }\n            } else if (name.length() > 0 && !\".\".equals(name)) {\n                stack.offerLast(name);\n            }\n        }\n        StringBuffer ans = new StringBuffer();\n        if (stack.isEmpty()) {\n            ans.append('/');\n        } else {\n            while (!stack.isEmpty()) {\n                ans.append('/');\n                ans.append(stack.pollFirst());\n            }\n        }\n        return ans.toString();\n    }\n}\n```\n\n## 155. 最小栈\nhttps://leetcode.cn/problems/min-stack/description/?envType=study-plan-v2&envId=top-interview-150\n\n设计一个支持 push ，pop ，top 操作，并能在常数时间内检索到最小元素的栈。\n\n实现 MinStack 类:\n\n- MinStack() 初始化堆栈对象。\n- void push(int val) 将元素val推入堆栈。\n- void pop() 删除堆栈顶部的元素。\n- int top() 获取堆栈顶部的元素。\n- int getMin() 获取堆栈中的最小元素。\n\n\n示例 1:\n\n输入：\n\n[\"MinStack\",\"push\",\"push\",\"push\",\"getMin\",\"pop\",\"top\",\"getMin\"]\n\n[[],[-2],[0],[-3],[],[],[],[]]\n\n输出：\n\n[null,null,null,null,-3,null,0,-2]\n\n解释：\n- MinStack minStack = new MinStack();\n- minStack.push(-2);\n- minStack.push(0);\n- minStack.push(-3);\n- minStack.getMin();   --> 返回 -3.\n- minStack.pop();\n- minStack.top();      --> 返回 0.\n- minStack.getMin();   --> 返回 -2.\n\n\n提示：\n\n- -2^31 <= val <= 2^31 - 1\n- pop、top 和 getMin 操作总是在 非空栈 上调用\n- push, pop, top, and getMin最多被调用 3 * 10^4 次\n\n```java\nclass MinStack {\n    public static class Node{\n        int val;\n        int min;\n        Node next;\n\n        public Node(int val,int min,Node next){\n            this.val = val;\n            this.min = min;\n            this.next = next;\n        }\n\n    }\n\n    Node head;\n    public MinStack() {\n\n    }\n    \n    public void push(int val) {\n        if(head == null){\n            head = new Node(val,val,null);\n        }else{\n            head = new Node(val,Math.min(head.min,val),head);\n        }\n    }\n    \n    public void pop() {\n        head = head.next;\n    }\n    \n    public int top() {\n        return head.val;\n    }\n    \n    public int getMin() {\n        return head.min;\n    }\n}\n\n/**\n * Your MinStack object will be instantiated and called as such:\n * MinStack obj = new MinStack();\n * obj.push(val);\n * obj.pop();\n * int param_3 = obj.top();\n * int param_4 = obj.getMin();\n */\n```\n\n## 150. 逆波兰表达式求值\nhttps://leetcode.cn/problems/evaluate-reverse-polish-notation/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个字符串数组 tokens ，表示一个根据 逆波兰表示法 表示的算术表达式。\n\n请你计算该表达式。返回一个表示表达式值的整数。\n\n注意：\n\n- 有效的算符为 '+'、'-'、'*' 和 '/' 。\n- 每个操作数（运算对象）都可以是一个整数或者另一个表达式。\n- 两个整数之间的除法总是 向零截断 。\n- 表达式中不含除零运算。\n- 输入是一个根据逆波兰表示法表示的算术表达式。\n- 答案及所有中间计算结果可以用 32 位 整数表示。\n\n\n示例 1：\n\n输入：tokens = [\"2\",\"1\",\"+\",\"3\",\"*\"]\n\n输出：9\n\n解释：该算式转化为常见的中缀算术表达式为：((2 + 1) * 3) = 9\n\n示例 2：\n\n输入：tokens = [\"4\",\"13\",\"5\",\"/\",\"+\"]\n\n输出：6\n\n解释：该算式转化为常见的中缀算术表达式为：(4 + (13 / 5)) = 6\n\n示例 3：\n\n输入：tokens = [\"10\",\"6\",\"9\",\"3\",\"+\",\"-11\",\"*\",\"/\",\"*\",\"17\",\"+\",\"5\",\"+\"]\n\n输出：22\n\n解释：该算式转化为常见的中缀算术表达式为：\n\n((10 * (6 / ((9 + 3) * -11))) + 17) + 5\n\n= ((10 * (6 / (12 * -11))) + 17) + 5\n\n= ((10 * (6 / -132)) + 17) + 5\n\n= ((10 * 0) + 17) + 5\n\n= (0 + 17) + 5\n\n= 17 + 5\n\n= 22\n\n\n提示：\n\n- 1 <= tokens.length <= 10^4\n- tokens[i] 是一个算符（\"+\"、\"-\"、\"*\" 或 \"/\"），或是在范围 [-200, 200] 内的一个整数\n\n\n逆波兰表达式：\n逆波兰表达式是一种后缀表达式，所谓后缀就是指算符写在后面。\n\n- 平常使用的算式则是一种中缀表达式，如 ( 1 + 2 ) * ( 3 + 4 ) 。\n- 该算式的逆波兰表达式写法为 ( ( 1 2 + ) ( 3 4 + ) * ) 。\n\n逆波兰表达式主要有以下两个优点：\n\n- 去掉括号后表达式无歧义，上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。\n- 适合用栈操作运算：遇到数字则入栈；遇到算符则取出栈顶两个数字进行计算，并将结果压入栈中\n\n```java\nclass Solution {\n    public int evalRPN(String[] tokens) {\n        Deque<Integer> stack = new LinkedList<Integer>();\n        int n = tokens.length;\n        for (int i = 0; i < n; i++) {\n            String token = tokens[i];\n            if (isNumber(token)) {\n                stack.push(Integer.parseInt(token));\n            } else {\n                int num2 = stack.pop();\n                int num1 = stack.pop();\n                switch (token) {\n                    case \"+\":\n                        stack.push(num1 + num2);\n                        break;\n                    case \"-\":\n                        stack.push(num1 - num2);\n                        break;\n                    case \"*\":\n                        stack.push(num1 * num2);\n                        break;\n                    case \"/\":\n                        stack.push(num1 / num2);\n                        break;\n                    default:\n                }\n            }\n        }\n        return stack.pop();\n    }\n\n    public boolean isNumber(String token) {\n        return !(\"+\".equals(token) || \"-\".equals(token) || \"*\".equals(token) || \"/\".equals(token));\n    }\n}\n```\n\n## 224. 基本计算器\nhttps://leetcode.cn/problems/basic-calculator/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个字符串表达式 s ，请你实现一个基本计算器来计算并返回它的值。\n\n注意:不允许使用任何将字符串作为数学表达式计算的内置函数，比如 eval() 。\n\n\n\n示例 1：\n\n输入：s = \"1 + 1\"\n\n输出：2\n\n示例 2：\n\n输入：s = \" 2-1 + 2 \"\n\n输出：3\n\n示例 3：\n\n输入：s = \"(1+(4+5+2)-3)+(6+8)\"\n\n输出：23\n\n\n提示：\n\n- 1 <= s.length <= 3 * 10^5\n- s 由数字、'+'、'-'、'('、')'、和 ' ' 组成\n- s 表示一个有效的表达式\n- '+' 不能用作一元运算(例如， \"+1\" 和 \"+(2 + 3)\" 无效)\n- '-' 可以用作一元运算(即 \"-1\" 和 \"-(2 + 3)\" 是有效的)\n- 输入中不存在两个连续的操作符\n- 每个数字和运行的计算将适合于一个有符号的 32位 整数\n\n```java\nclass Solution {\n    public int calculate(String s) {\n        Deque<Integer> nums = new LinkedList<>();\n        nums.addLast(0);\n        Deque<Character> ops = new LinkedList<>();\n        s = s.replaceAll(\" \",\"\");\n        char[] arr = s.toCharArray();\n        int len = arr.length;\n        for(int i=0;i < len;i++){\n            char c = arr[i];\n            //(\n            if(c == '('){\n                ops.addLast(c);\n              //)   \n            }else if(c == ')'){\n                while(!ops.isEmpty()){\n                    char op = ops.peekLast();\n                    if(op != '('){\n                        calc(nums,ops);\n                    }else{\n                        ops.pollLast();\n                        break;\n                    }\n                }\n            }else{\n                //数字\n                if(Character.isDigit(c)){\n                    int sum = 0;\n                    int j = i;\n                    while(j < len && Character.isDigit(arr[j])){\n                        sum = sum * 10 + (arr[j++] - '0');\n                    }\n                    nums.addLast(sum);\n                    i = j - 1;\n                }else{\n                    //计算符号\n                    if(i>0 && ( arr[i-1] == '(' || arr[i-1] == '+' ||arr[i-1] == '-')){\n                        nums.addLast(0);\n                    }\n                    while(!ops.isEmpty() && ops.peekLast() != '('){\n                        calc(nums,ops);\n                    }\n                    ops.addLast(c);\n                }\n            }\n        }\n        while(!ops.isEmpty()){\n            calc(nums,ops);\n        }\n        return nums.peekLast();\n    }\n\n    public void calc(Deque<Integer> nums,Deque<Character> ops){\n        if(nums.isEmpty() || nums.size() < 2){\n            return;\n        }\n        if(ops.isEmpty()){\n            return;\n        }\n        int b = nums.pollLast();\n        int a = nums.pollLast();\n        char op = ops.pollLast();\n        int res  = op == '+' ? a+b : a - b;\n        nums.addLast(res);\n    }\n}\n```\n\n# 链表\n## 141. 环形链表\nhttps://leetcode.cn/problems/linked-list-cycle/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个链表的头节点 head ，判断链表中是否有环。\n\n如果链表中有某个节点，可以通过连续跟踪 next 指针再次到达，则链表中存在环。 为了表示给定链表中的环，评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置（索引从 0 开始）。注意：pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。\n\n如果链表中存在环 ，则返回 true 。 否则，返回 false 。\n\n\n\n示例 1：\n\n![环形链表1.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A81.png)\n\n输入：head = [3,2,0,-4], pos = 1\n\n输出：true\n\n解释：链表中有一个环，其尾部连接到第二个节点。\n\n示例 2：\n\n![环形链表2.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A82.png)\n\n输入：head = [1,2], pos = 0\n\n输出：true\n\n解释：链表中有一个环，其尾部连接到第一个节点。\n\n示例 3：\n\n![环形链表3.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A83.png)\n\n输入：head = [1], pos = -1\n输出：false\n解释：链表中没有环。\n\n\n提示：\n\n- 链表中节点的数目范围是 [0, 10^4]\n- -10^5 <= Node.val <= 10^5\n- pos 为 -1 或者链表中的一个 有效索引 。\n\n\n进阶：你能用 O(1)（即，常量）内存解决此问题吗？\n\n```java\n/**\n * Definition for singly-linked list.\n * class ListNode {\n *     int val;\n *     ListNode next;\n *     ListNode(int x) {\n *         val = x;\n *         next = null;\n *     }\n * }\n */\npublic class Solution {\n    public boolean hasCycle(ListNode head) {\n        if(head == null || head.next == null){\n            return false;\n        }\n        ListNode fast = head.next;\n        ListNode slow = head;\n        while(fast != slow){\n            if(fast == null || fast.next == null){\n                return false;\n            }\n            fast = fast.next.next;\n            slow = slow.next;\n        }\n        return true;\n    }\n}\n```\n## 2. 两数相加\nhttps://leetcode.cn/problems/add-two-numbers/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你两个 非空 的链表，表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的，并且每个节点只能存储 一位 数字。\n\n请你将两个数相加，并以相同形式返回一个表示和的链表。\n\n你可以假设除了数字 0 之外，这两个数都不会以 0 开头。\n\n\n\n示例 1：\n\n![链表-两数相加.png](..%2Fimg%2F%CB%E3%B7%A8%2F%C1%B4%B1%ED-%C1%BD%CA%FD%CF%E0%BC%D3.png)\n\n输入：l1 = [2,4,3], l2 = [5,6,4]\n\n输出：[7,0,8]\n\n解释：342 + 465 = 807.\n\n示例 2：\n\n输入：l1 = [0], l2 = [0]\n\n输出：[0]\n\n示例 3：\n\n输入：l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]\n\n输出：[8,9,9,9,0,0,0,1]\n\n\n提示：\n\n- 每个链表中的节点数在范围 [1, 100] 内\n- 0 <= Node.val <= 9\n- 题目数据保证列表表示的数字不含前导零\n\n```java\n/**\n * Definition for singly-linked list.\n * public class ListNode {\n *     int val;\n *     ListNode next;\n *     ListNode() {}\n *     ListNode(int val) { this.val = val; }\n *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }\n * }\n */\nclass Solution {\n    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {\n        ListNode dummy = new ListNode();\n        ListNode head = dummy;\n\n        int add = 0;\n        while(l1 != null || l2 != null || add!=0){\n            int x = l1==null?0:l1.val;\n            int y = l2==null?0:l2.val;\n            int result = x+y+add;\n            head.next = new ListNode(result%10);\n            add = result/10;\n            l1 = l1==null?null:l1.next;\n            l2 = l2==null?null:l2.next;\n            head = head.next;\n        }\n        return dummy.next;\n    }\n}\n```\n\n## 21. 合并两个有序链表\nhttps://leetcode.cn/problems/merge-two-sorted-lists/description/?envType=study-plan-v2&envId=top-interview-150\n\n将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。\n\n\n\n示例 1：\n\n![合并两个有序链表.png](..%2Fimg%2F%CB%E3%B7%A8%2F%BA%CF%B2%A2%C1%BD%B8%F6%D3%D0%D0%F2%C1%B4%B1%ED.png)\n\n输入：l1 = [1,2,4], l2 = [1,3,4]\n\n输出：[1,1,2,3,4,4]\n\n示例 2：\n\n输入：l1 = [], l2 = []\n\n输出：[]\n\n示例 3：\n\n输入：l1 = [], l2 = [0]\n\n输出：[0]\n\n\n提示：\n\n- 两个链表的节点数目范围是 [0, 50]\n- -100 <= Node.val <= 100\n- l1 和 l2 均按 非递减顺序 排列\n\n```java\n/**\n * Definition for singly-linked list.\n * public class ListNode {\n *     int val;\n *     ListNode next;\n *     ListNode() {}\n *     ListNode(int val) { this.val = val; }\n *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }\n * }\n */\nclass Solution {\n    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {\n        if(list1 == null){\n            return list2;\n        }\n        if(list2 == null){\n            return list1;\n        }\n        ListNode n = null;\n        if(list1.val > list2.val){\n            n = list2;\n            n.next = mergeTwoLists(list1,list2.next);\n        }else{\n            n = list1;\n            n.next = mergeTwoLists(list1.next,list2);\n        }\n        return n;\n    }\n}\n```\n\n## 138. 随机链表的复制\nhttps://leetcode.cn/problems/copy-list-with-random-pointer/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个长度为 n 的链表，每个节点包含一个额外增加的随机指针 random ，该指针可以指向链表中的任何节点或空节点。\n\n构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成，其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点，并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。\n\n例如，如果原链表中有 X 和 Y 两个节点，其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ，同样有 x.random --> y 。\n\n返回复制链表的头节点。\n\n用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示：\n\n- val：一个表示 Node.val 的整数。\n- random_index：随机指针指向的节点索引（范围从 0 到 n-1）；如果不指向任何节点，则为  null 。\n\n你的代码 只 接受原链表的头节点 head 作为传入参数。\n\n\n\n示例 1：\n\n![随机链表的复制1.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E9%9A%8F%E6%9C%BA%E9%93%BE%E8%A1%A8%E7%9A%84%E5%A4%8D%E5%88%B61.png)\n\n输入：head = [[7,null],[13,0],[11,4],[10,2],[1,0]]\n\n输出：[[7,null],[13,0],[11,4],[10,2],[1,0]]\n\n示例 2：\n\n![随机链表的复制2.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E9%9A%8F%E6%9C%BA%E9%93%BE%E8%A1%A8%E7%9A%84%E5%A4%8D%E5%88%B62.png)\n\n输入：head = [[1,1],[2,1]]\n\n输出：[[1,1],[2,1]]\n\n示例 3：\n\n![随机链表的复制3.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E9%9A%8F%E6%9C%BA%E9%93%BE%E8%A1%A8%E7%9A%84%E5%A4%8D%E5%88%B63.png)\n\n输入：head = [[3,null],[3,0],[3,null]]\n\n输出：[[3,null],[3,0],[3,null]]\n\n\n提示：\n\n- 0 <= n <= 1000\n- -10^4 <= Node.val <= 10^4\n- Node.random 为 null 或指向链表中的节点。\n\n```java\n/*\n// Definition for a Node.\nclass Node {\n    int val;\n    Node next;\n    Node random;\n\n    public Node(int val) {\n        this.val = val;\n        this.next = null;\n        this.random = null;\n    }\n}\n*/\n\nclass Solution {\n    public Node copyRandomList(Node head) {\n        if(head == null){\n            return head;\n        }\n        Node node = head;\n        Map<Node,Node> map  = new HashMap<>();\n        while(node != null){\n            Node clone  = new Node(node.val);\n            map.put(node,clone);\n            node = node.next;\n        }\n        node = head;\n        while(node != null){\n            map.get(node).next = map.get(node.next);\n            map.get(node).random = map.get(node.random);\n            node = node.next;\n        }\n        return map.get(head);\n    }\n}\n```\n\n## 92. 反转链表 II\nhttps://leetcode.cn/problems/reverse-linked-list-ii/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你单链表的头指针 head 和两个整数 left 和 right ，其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点，返回 反转后的链表 。\n\n\n示例 1：\n\n![翻转链表2.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E7%BF%BB%E8%BD%AC%E9%93%BE%E8%A1%A82.png)\n\n输入：head = [1,2,3,4,5], left = 2, right = 4\n\n输出：[1,4,3,2,5]\n\n示例 2：\n\n输入：head = [5], left = 1, right = 1\n\n输出：[5]\n\n\n提示：\n\n- 链表中节点数目为 n\n- 1 <= n <= 500\n- -500 <= Node.val <= 500\n- 1 <= left <= right <= n\n\n```java\n/**\n * Definition for singly-linked list.\n * public class ListNode {\n *     int val;\n *     ListNode next;\n *     ListNode() {}\n *     ListNode(int val) { this.val = val; }\n *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }\n * }\n */\nclass Solution {\n    public ListNode reverseBetween(ListNode head, int left, int right) {\n        ListNode dummy = new ListNode();\n        dummy.next = head;\n        ListNode pre = dummy;\n        for(int i=1;i<left;i++){\n            pre = pre.next;\n        }\n        ListNode cur = pre.next;\n        ListNode next = null;\n        //1-2-3-4\n        for(int i = left;i<right;i++){\n            next = cur.next;\n            //1-2-4 3-4\n            cur.next = next.next;\n            //1-2-4 3-2\n            next.next = pre.next;\n            //1-3-2-4\n            pre.next = next;\n        }\n        return dummy.next;\n    }\n}\n```\n\n## 25. K 个一组翻转链表\nhttps://leetcode.cn/problems/reverse-nodes-in-k-group/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你链表的头节点 head ，每 k 个节点一组进行翻转，请你返回修改后的链表。\n\nk 是一个正整数，它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍，那么请将最后剩余的节点保持原有顺序。\n\n你不能只是单纯的改变节点内部的值，而是需要实际进行节点交换。\n\n\n\n示例 1：\n\n![k个一组翻转链表1.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2Fk%E4%B8%AA%E4%B8%80%E7%BB%84%E7%BF%BB%E8%BD%AC%E9%93%BE%E8%A1%A81.png)\n\n输入：head = [1,2,3,4,5], k = 2\n\n输出：[2,1,4,3,5]\n\n示例 2：\n\n![k个一组翻转链表2.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2Fk%E4%B8%AA%E4%B8%80%E7%BB%84%E7%BF%BB%E8%BD%AC%E9%93%BE%E8%A1%A82.png)\n\n输入：head = [1,2,3,4,5], k = 3\n\n输出：[3,2,1,4,5]\n\n\n提示：\n- 链表中的节点数目为 n\n- 1 <= k <= n <= 5000\n- 0 <= Node.val <= 1000\n\n```java\n/**\n * Definition for singly-linked list.\n * public class ListNode {\n *     int val;\n *     ListNode next;\n *     ListNode() {}\n *     ListNode(int val) { this.val = val; }\n *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }\n * }\n */\nclass Solution {\n    public ListNode reverseKGroup(ListNode head, int k) {\n        ListNode tail = head;\n        for(int i =0;i<k;i++){\n            if(tail == null){\n                return head;\n            }\n            tail = tail.next;\n        }\n        ListNode newHead = reverse(head,tail);\n        head.next= reverseKGroup(tail,k);\n        return newHead;\n    }\n\n    public ListNode reverse(ListNode head,ListNode tail){\n        ListNode pre = null,next = null;\n        while(head != tail){\n            next = head.next;\n            head.next = pre;\n            pre = head;\n            head = next;\n        }\n        return pre;\n    }\n}\n```\n\n## 19. 删除链表的倒数第 N 个结点\nhttps://leetcode.cn/problems/remove-nth-node-from-end-of-list/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个链表，删除链表的倒数第 n 个结点，并且返回链表的头结点。\n\n\n\n示例 1：\n\n![删除链表的倒数第n个节点.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E5%88%A0%E9%99%A4%E9%93%BE%E8%A1%A8%E7%9A%84%E5%80%92%E6%95%B0%E7%AC%ACn%E4%B8%AA%E8%8A%82%E7%82%B9.png)\n\n输入：head = [1,2,3,4,5], n = 2\n\n输出：[1,2,3,5]\n\n示例 2：\n\n输入：head = [1], n = 1\n\n输出：[]\n\n示例 3：\n\n输入：head = [1,2], n = 1\n\n输出：[1]\n\n\n提示：\n\n- 链表中结点的数目为 sz\n- 1 <= sz <= 30\n- 0 <= Node.val <= 100\n- 1 <= n <= sz\n\n```java\n/**\n * Definition for singly-linked list.\n * public class ListNode {\n *     int val;\n *     ListNode next;\n *     ListNode() {}\n *     ListNode(int val) { this.val = val; }\n *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }\n * }\n */\nclass Solution {\n    public ListNode removeNthFromEnd(ListNode head, int n) {\n        ListNode dummy = new ListNode();\n        dummy.next = head;\n\n        ListNode fast = head;\n        ListNode slow = head;\n        while(fast!=null && n-->0){\n            fast = fast.next;\n        }\n        ListNode pre = dummy;\n        while(fast!=null){\n            pre = slow;\n            fast = fast.next;\n            slow = slow.next;\n        }\n        pre.next = slow.next;\n        return dummy.next;\n    }\n}\n```\n\n## 82. 删除排序链表中的重复元素 II\nhttps://leetcode.cn/problems/remove-duplicates-from-sorted-list-ii/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个已排序的链表的头 head ， 删除原始链表中所有重复数字的节点，只留下不同的数字 。返回 已排序的链表 。\n\n\n\n示例 1：\n\n![删除排序链表中的重复元素2-1.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E5%88%A0%E9%99%A4%E6%8E%92%E5%BA%8F%E9%93%BE%E8%A1%A8%E4%B8%AD%E7%9A%84%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A02-1.png)\n\n输入：head = [1,2,3,3,4,4,5]\n\n输出：[1,2,5]\n\n示例 2：\n\n![删除排序链表中的重复元素2-2.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E5%88%A0%E9%99%A4%E6%8E%92%E5%BA%8F%E9%93%BE%E8%A1%A8%E4%B8%AD%E7%9A%84%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A02-2.png)\n\n输入：head = [1,1,1,2,3]\n\n输出：[2,3]\n\n\n提示：\n\n- 链表中节点数目在范围 [0, 300] 内\n- -100 <= Node.val <= 100\n- 题目数据保证链表已经按升序 排列\n\n```java\n/**\n * Definition for singly-linked list.\n * public class ListNode {\n *     int val;\n *     ListNode next;\n *     ListNode() {}\n *     ListNode(int val) { this.val = val; }\n *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }\n * }\n */\nclass Solution {\n    public ListNode deleteDuplicates(ListNode head) {\n        if(head == null){\n            return null;\n        }\n        ListNode dummy = new ListNode();\n        dummy.next = head;\n        ListNode pre = dummy;\n        ListNode cur = head;\n\n        while(cur!= null && cur.next != null){\n            if(cur.val == cur.next.val){\n                int v = cur.val;\n                while(cur!= null && cur.val == v){\n                    cur = cur.next;\n                }\n                pre.next = cur;\n            }else{\n                pre = cur;\n                cur = cur.next;\n            }\n        }\n        return dummy.next;\n    }\n}\n```\n\n## 61. 旋转链表\nhttps://leetcode.cn/problems/rotate-list/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个链表的头节点 head ，旋转链表，将链表每个节点向右移动 k 个位置。\n\n\n\n示例 1：\n\n![旋转链表1.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E6%97%8B%E8%BD%AC%E9%93%BE%E8%A1%A81.png)\n\n输入：head = [1,2,3,4,5], k = 2\n\n输出：[4,5,1,2,3]\n\n示例 2：\n\n![旋转链表2.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E6%97%8B%E8%BD%AC%E9%93%BE%E8%A1%A82.png)\n\n输入：head = [0,1,2], k = 4\n\n输出：[2,0,1]\n\n\n提示：\n\n- 链表中节点的数目在范围 [0, 500] 内\n- -100 <= Node.val <= 100\n- 0 <= k <= 2 * 10^9\n\n```java\nclass Solution {\n    public ListNode rotateRight(ListNode head, int k) {\n        // 如果k为0或者链表为空或者链表只有一个节点，直接返回原始链表\n        if (k == 0 || head == null || head.next == null) {\n            return head;\n        }\n\n        int n = 1; // 链表中节点的个数\n        ListNode iter = head;\n\n        // 遍历链表，找到链表长度n\n        while (iter.next != null) {\n            iter = iter.next;\n            n++;\n        }\n\n        // 计算需要移动的步数\n        int add = n - k % n;\n        \n        // 如果移动的步数等于链表长度，说明不需要移动，直接返回原始链表\n        if (add == n) {\n            return head;\n        }\n\n        // 将链表连接成环形\n        iter.next = head;\n\n        // 找到新的链表头节点的位置\n        while (add-- > 0) {\n            iter = iter.next;\n        }\n        \n        // 分割链表，得到新的头节点\n        ListNode ret = iter.next;\n        iter.next = null;\n        \n        return ret;\n    }\n}\n```\n\n## 86. 分隔链表\nhttps://leetcode.cn/problems/partition-list/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个链表的头节点 head 和一个特定值 x ，请你对链表进行分隔，使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。\n\n你应当 保留 两个分区中每个节点的初始相对位置。\n\n\n\n示例 1：\n\n![分隔链表.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E5%88%86%E9%9A%94%E9%93%BE%E8%A1%A8.png)\n\n输入：head = [1,4,3,2,5,2], x = 3\n\n输出：[1,2,2,4,3,5]\n\n示例 2：\n\n输入：head = [2,1], x = 2\n\n输出：[1,2]\n\n\n提示：\n\n- 链表中节点的数目在范围 [0, 200] 内\n- -100 <= Node.val <= 100\n- -200 <= x <= 200\n\n```java\nclass Solution {\n    public ListNode partition(ListNode head, int x) {\n        // 创建两个虚拟节点，用来存储小于 x 和大于等于 x 的节点\n        ListNode small = new ListNode(0); // 小于 x 的节点\n        ListNode smallHead = small; // 小节点的头部\n        ListNode large = new ListNode(0); // 大于等于 x 的节点\n        ListNode largeHead = large; // 大节点的头部\n        \n        // 遍历原始链表\n        while (head != null) {\n            if (head.val < x) {\n                // 将小于 x 的节点放入 small 链表中\n                small.next = head;\n                small = small.next;\n            } else {\n                // 将大于等于 x 的节点放入 large 链表中\n                large.next = head;\n                large = large.next;\n            }\n            // 移动原始链表指针\n            head = head.next;\n        }\n        \n        // 将large链表结尾指向null，避免循环\n        large.next = null;\n        \n        // 将小节点链表和大节点链表连接起来\n        small.next = largeHead.next;\n        \n        // 返回新链表的头部（小节点链表的头部）\n        return smallHead.next;\n    }\n}\n\n这段代码将根据给定值 x 将链表分为两部分，一部分是小于 x 的节点，另一部分是大于等于 x 的节点。最后将这两部分重新连接起来并返回新链表的头部。\n```\n\n## 146. LRU 缓存\n\nhttps://leetcode.cn/problems/lru-cache/description/?envType=study-plan-v2&envId=top-interview-150\n\n请你设计并实现一个满足  LRU (最近最少使用) 缓存 约束的数据结构。\n\n实现 LRUCache 类：\n- LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存\n- int get(int key) 如果关键字 key 存在于缓存中，则返回关键字的值，否则返回 -1 。\n- void put(int key, int value) 如果关键字 key 已经存在，则变更其数据值 value ；如果不存在，则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ，则应该 逐出 最久未使用的关键字。\n\n函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。\n\n\n\n示例：\n\n输入\n\n[\"LRUCache\", \"put\", \"put\", \"get\", \"put\", \"get\", \"put\", \"get\", \"get\", \"get\"]\n\n[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]\n\n输出\n\n[null, null, null, 1, null, -1, null, -1, 3, 4]\n\n解释\n\nLRUCache lRUCache = new LRUCache(2);\n\nlRUCache.put(1, 1); // 缓存是 {1=1}\n\nlRUCache.put(2, 2); // 缓存是 {1=1, 2=2}\n\nlRUCache.get(1);    // 返回 1\n\nlRUCache.put(3, 3); // 该操作会使得关键字 2 作废，缓存是 {1=1, 3=3}\n\nlRUCache.get(2);    // 返回 -1 (未找到)\n\nlRUCache.put(4, 4); // 该操作会使得关键字 1 作废，缓存是 {4=4, 3=3}\n\nlRUCache.get(1);    // 返回 -1 (未找到)\n\nlRUCache.get(3);    // 返回 3\n\nlRUCache.get(4);    // 返回 4\n\n\n提示：\n\n- 1 <= capacity <= 3000\n- 0 <= key <= 10000\n- 0 <= value <= 10^5\n- 最多调用 2 * 10^5 次 get 和 put\n\n```java\nclass LRUCache {\n\n    static class Node{\n        int key;\n        int value;\n        Node pre;\n        Node next;\n\n        public Node(){\n\n        }\n        public Node(int key,int value){\n            this.key = key;\n            this.value = value;\n        }\n    }\n\n    Map<Integer,Node> map = new HashMap<>();\n    Node head,tail;\n    int size;\n    int cap;\n\n    public LRUCache(int capacity) {\n        this.cap = capacity;\n        head = new Node();\n        tail = new Node();\n        head.next = tail;\n        tail.pre = head;\n    }\n    \n    public int get(int key) {\n        Node n = map.get(key);\n        if(n == null){\n            return -1;\n        }\n        moveToHead(n);\n        return n.value;\n    }\n    \n    public void put(int key, int value) {\n        Node n = map.get(key);\n        if(n == null){\n            Node node = new Node(key,value);\n            map.put(key,node);\n            addToHead(node);\n            if(++size > cap){\n                Node re = removeTail();\n                map.remove(re.key);\n            }\n        }else{\n            n.value = value;\n            moveToHead(n);\n        }\n    }\n\n    public Node remove(Node n){\n        n.next.pre = n.pre;\n        n.pre.next = n.next;\n        return n;\n    }\n\n    public Node removeTail(){\n        Node n = tail.pre;\n        remove(n);\n        return n;\n    }\n\n    public void addToHead(Node n){\n        n.next = head.next;\n        n.pre = head;\n        head.next.pre = n;\n        head.next = n;\n        \n    }\n\n    public void moveToHead(Node n){\n        remove(n);\n        addToHead(n);\n    }\n}\n\n/**\n * Your LRUCache object will be instantiated and called as such:\n * LRUCache obj = new LRUCache(capacity);\n * int param_1 = obj.get(key);\n * obj.put(key,value);\n */\n```\n\n# 二叉树\n## 104. 二叉树的最大深度\nhttps://leetcode.cn/problems/maximum-depth-of-binary-tree/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个二叉树 root ，返回其最大深度。\n\n二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。\n\n\n\n示例 1：\n\n\n![二叉树的最大深度.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%9C%80%E5%A4%A7%E6%B7%B1%E5%BA%A6.png)\n\n\n输入：root = [3,9,20,null,null,15,7]\n\n输出：3\n\n示例 2：\n\n输入：root = [1,null,2]\n\n输出：2\n\n\n提示：\n\n- 树中节点的数量在 [0, 10^4] 区间内。\n- -100 <= Node.val <= 100\n\n```java\n/**\n * Definition for a binary tree node.\n * public class TreeNode {\n *     int val;\n *     TreeNode left;\n *     TreeNode right;\n *     TreeNode() {}\n *     TreeNode(int val) { this.val = val; }\n *     TreeNode(int val, TreeNode left, TreeNode right) {\n *         this.val = val;\n *         this.left = left;\n *         this.right = right;\n *     }\n * }\n */\nclass Solution {\n    public int maxDepth(TreeNode root) {\n        if(root==null){\n            return 0;\n        }\n        int left = maxDepth(root.left);\n        int right = maxDepth(root.right);\n        return Math.max(left,right)+1;\n    }\n}\n```\n\n## 100. 相同的树\nhttps://leetcode.cn/problems/same-tree/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你两棵二叉树的根节点 p 和 q ，编写一个函数来检验这两棵树是否相同。\n\n如果两个树在结构上相同，并且节点具有相同的值，则认为它们是相同的。\n\n\n\n示例 1：\n\n![相同的树1.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E7%9B%B8%E5%90%8C%E7%9A%84%E6%A0%911.png)\n\n输入：p = [1,2,3], q = [1,2,3]\n\n输出：true\n\n示例 2：\n\n![相同的树2.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E7%9B%B8%E5%90%8C%E7%9A%84%E6%A0%912.png)\n\n输入：p = [1,2], q = [1,null,2]\n\n输出：false\n\n示例 3：\n\n![相同的树3.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E7%9B%B8%E5%90%8C%E7%9A%84%E6%A0%913.png)\n\n输入：p = [1,2,1], q = [1,1,2]\n\n输出：false\n\n\n提示：\n\n- 两棵树上的节点数目都在范围 [0, 100] 内\n- -10^4 <= Node.val <= 10^4\n\n```java\n/**\n * Definition for a binary tree node.\n * public class TreeNode {\n *     int val;\n *     TreeNode left;\n *     TreeNode right;\n *     TreeNode() {}\n *     TreeNode(int val) { this.val = val; }\n *     TreeNode(int val, TreeNode left, TreeNode right) {\n *         this.val = val;\n *         this.left = left;\n *         this.right = right;\n *     }\n * }\n */\nclass Solution {\n    public boolean isSameTree(TreeNode p, TreeNode q) {\n        if(p == null && q==null){\n            return true;\n        }\n        if(p == null || q==null){\n            return false;\n        }\n        return p.val == q.val && isSameTree(p.left,q.left) && isSameTree(p.right,q.right);\n    }\n}\n```\n\n## 226. 翻转二叉树\nhttps://leetcode.cn/problems/invert-binary-tree/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一棵二叉树的根节点 root ，翻转这棵二叉树，并返回其根节点。\n\n\n\n示例 1：\n\n![翻转二叉树1.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E7%BF%BB%E8%BD%AC%E4%BA%8C%E5%8F%89%E6%A0%911.png)\n\n输入：root = [4,2,7,1,3,6,9]\n\n输出：[4,7,2,9,6,3,1]\n\n示例 2：\n\n![翻转二叉树2.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E7%BF%BB%E8%BD%AC%E4%BA%8C%E5%8F%89%E6%A0%912.png)\n\n输入：root = [2,1,3]\n\n输出：[2,3,1]\n\n示例 3：\n\n输入：root = []\n\n输出：[]\n\n\n提示：\n\n- 树中节点数目范围在 [0, 100] 内\n- -100 <= Node.val <= 100\n\n```java\n/**\n * Definition for a binary tree node.\n * public class TreeNode {\n *     int val;\n *     TreeNode left;\n *     TreeNode right;\n *     TreeNode() {}\n *     TreeNode(int val) { this.val = val; }\n *     TreeNode(int val, TreeNode left, TreeNode right) {\n *         this.val = val;\n *         this.left = left;\n *         this.right = right;\n *     }\n * }\n */\nclass Solution {\n    public TreeNode invertTree(TreeNode root) {\n        if(root == null || (root.left==null && root.right==null)){\n            return root;\n        }\n        TreeNode temp  = root.left;\n        root.left = root.right;\n        root.right = temp;\n        invertTree(root.left);\n        invertTree(root.right);\n        return root;\n    }\n}\n```\n\n## 101. 对称二叉树\nhttps://leetcode.cn/problems/symmetric-tree/description/?envType=study-plan-v2&envId=top-interview-150\n\n\n给你一个二叉树的根节点 root ， 检查它是否轴对称。\n\n\n\n示例 1：\n\n![对称二叉树1.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E5%AF%B9%E7%A7%B0%E4%BA%8C%E5%8F%89%E6%A0%911.png)\n\n输入：root = [1,2,2,3,4,4,3]\n\n输出：true\n\n示例 2：\n\n![对称二叉树2.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E5%AF%B9%E7%A7%B0%E4%BA%8C%E5%8F%89%E6%A0%912.png)\n\n输入：root = [1,2,2,null,3,null,3]\n\n输出：false\n\n\n提示：\n\n- 树中节点数目在范围 [1, 1000] 内\n- -100 <= Node.val <= 100\n\n```java\n/**\n * Definition for a binary tree node.\n * public class TreeNode {\n *     int val;\n *     TreeNode left;\n *     TreeNode right;\n *     TreeNode() {}\n *     TreeNode(int val) { this.val = val; }\n *     TreeNode(int val, TreeNode left, TreeNode right) {\n *         this.val = val;\n *         this.left = left;\n *         this.right = right;\n *     }\n * }\n */\nclass Solution {\n    public boolean isSymmetric(TreeNode root) {\n        if(root==null){\n            return true;\n        }\n        return help(root.left,root.right);\n    }\n\n    public boolean help(TreeNode left,TreeNode right){\n        if(left == null && right == null){\n            return true;\n        }\n        if(left == null || right==null || left.val != right.val){\n            return false;\n        }\n        return help(left.left,right.right) && help(left.right,right.left);\n    }\n}\n```\n\n## 105. 从前序与中序遍历序列构造二叉树\nhttps://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定两个整数数组 preorder 和 inorder ，其中 preorder 是二叉树的先序遍历， inorder 是同一棵树的中序遍历，请构造二叉树并返回其根节点。\n\n\n\n示例 1:\n\n\n输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]\n\n输出: [3,9,20,null,null,15,7]\n\n示例 2:\n\n输入: preorder = [-1], inorder = [-1]\n\n输出: [-1]\n\n\n提示:\n\n- 1 <= preorder.length <= 3000\n- inorder.length == preorder.length\n- -3000 <= preorder[i], inorder[i] <= 3000\n- preorder 和 inorder 均 无重复 元素\n- inorder 均出现在 preorder\n- preorder 保证 为二叉树的前序遍历序列\n- inorder 保证 为二叉树的中序遍历序列\n\n```java\n/**\n * Definition for a binary tree node.\n * public class TreeNode {\n *     int val;\n *     TreeNode left;\n *     TreeNode right;\n *     TreeNode() {}\n *     TreeNode(int val) { this.val = val; }\n *     TreeNode(int val, TreeNode left, TreeNode right) {\n *         this.val = val;\n *         this.left = left;\n *         this.right = right;\n *     }\n * }\n */\nclass Solution {\n    int rootIndex = 0;\n    Map<Integer,Integer> map =new HashMap<>();\n    public TreeNode buildTree(int[] preorder, int[] inorder) {\n        for(int i=0;i<inorder.length;i++){\n            map.put(inorder[i],i);\n        }\n        return build(0,preorder.length-1,preorder);\n    }\n\n    public TreeNode build(int left,int right,int[] preorder){\n        if(left>right){\n            return null;\n        }\n        int rootVal = preorder[rootIndex++];\n        TreeNode root = new TreeNode(rootVal);\n\n        root.left = build(left,map.get(rootVal)-1,preorder);\n        root.right = build(map.get(rootVal)+1,right,preorder);\n\n        return root;\n    }\n}\n```\n\n## 106. 从中序与后序遍历序列构造二叉树\nhttps://leetcode.cn/problems/construct-binary-tree-from-inorder-and-postorder-traversal/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定两个整数数组 inorder 和 postorder ，其中 inorder 是二叉树的中序遍历， postorder 是同一棵树的后序遍历，请你构造并返回这颗 二叉树 。\n\n\n\n示例 1:\n\n![从中序和后序遍历构造二叉树.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E4%BB%8E%E4%B8%AD%E5%BA%8F%E5%92%8C%E5%90%8E%E5%BA%8F%E9%81%8D%E5%8E%86%E6%9E%84%E9%80%A0%E4%BA%8C%E5%8F%89%E6%A0%91.png)\n\n输入：inorder = [9,3,15,20,7], postorder = [9,15,7,20,3]\n\n输出：[3,9,20,null,null,15,7]\n\n示例 2:\n\n输入：inorder = [-1], postorder = [-1]\n\n输出：[-1]\n\n\n提示:\n\n- 1 <= inorder.length <= 3000\n- postorder.length == inorder.length\n- -3000 <= inorder[i], postorder[i] <= 3000\n- inorder 和 postorder 都由 不同 的值组成\n- postorder 中每一个值都在 inorder 中\n- inorder 保证是树的中序遍历\n- postorder 保证是树的后序遍历\n\n```java\n/**\n * Definition for a binary tree node.\n * public class TreeNode {\n *     int val;\n *     TreeNode left;\n *     TreeNode right;\n *     TreeNode() {}\n *     TreeNode(int val) { this.val = val; }\n *     TreeNode(int val, TreeNode left, TreeNode right) {\n *         this.val = val;\n *         this.left = left;\n *         this.right = right;\n *     }\n * }\n */\nclass Solution {\n    public TreeNode buildTree(int[] inorder, int[] postorder) {\n        return build(inorder,0,inorder.length-1,postorder,0,postorder.length-1);\n    }\n\n    public TreeNode build(int[] inorder,int inStart,int inEnd,int[] postorder,int postStart,int postEnd){\n        if(inStart>inEnd){\n            return null;\n        }\n        int index = 0;\n        int rootVal = postorder[postEnd];\n\n        for(int i=inStart;i<=inEnd;i++){\n            if(inorder[i]==rootVal){\n                index = i;\n                break;\n            }\n        }\n        TreeNode root = new TreeNode(rootVal);\n        int leftSize = index - inStart;\n        root.left = build(inorder,inStart,index-1,postorder,postStart,postStart+leftSize-1);\n        root.right = build(inorder,index+1,inEnd,postorder,postStart+leftSize,postEnd-1);\n\n        return root;\n    }\n}\n```\n\n## 117. 填充每个节点的下一个右侧节点指针 II\nhttps://leetcode.cn/problems/populating-next-right-pointers-in-each-node-ii/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个二叉树：\n\n```java\nstruct Node {\nint val;\nNode *left;\nNode *right;\nNode *next;\n}\n```\n\n填充它的每个 next 指针，让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点，则将 next 指针设置为 NULL 。\n\n初始状态下，所有 next 指针都被设置为 NULL 。\n\n\n\n示例 1：\n\n![删除![填充每个节点的下一个右侧节点指针2-1.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E5%A1%AB%E5%85%85%E6%AF%8F%E4%B8%AA%E8%8A%82%E7%82%B9%E7%9A%84%E4%B8%8B%E4%B8%80%E4%B8%AA%E5%8F%B3%E4%BE%A7%E8%8A%82%E7%82%B9%E6%8C%87%E9%92%882-1.png)排序链表中的重复元素2-1.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E5%88%A0%E9%99%A4%E6%8E%92%E5%BA%8F%E9%93%BE%E8%A1%A8%E4%B8%AD%E7%9A%84%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A02-1.png)\n\n输入：root = [1,2,3,4,5,null,7]\n\n输出：[1,#,2,3,#,4,5,7,#]\n\n解释：给定二叉树如图 A 所示，你的函数应该填充它的每个 next 指针，以指向其下一个右侧节点，如图 B 所示。序列化输出按层序遍历顺序（由 next 指针连接），'#' 表示每层的末尾。\n\n示例 2：\n\n输入：root = []\n\n输出：[]\n\n\n提示：\n\n- 树中的节点数在范围 [0, 6000] 内\n- -100 <= Node.val <= 100\n\n```java\nclass Solution {\n    public Node connect(Node root) {\n        if (root == null) {\n            return null;\n        }\n\n        Node head = root; // 当前层的头节点\n\n        // 循环遍历每一层，从上至下\n        while (head != null) {\n            Node dummy = new Node(0); //下一层的虚拟头节点\n            Node temp = dummy; //当前处理的节点\n            \n            // 遍历当前层，连接下一层的节点\n            for (Node cur = head; cur != null; cur = cur.next) {\n                if (cur.left != null) {\n                    temp.next = cur.left;\n                    temp = temp.next; //移动temp\n                }\n                if (cur.right != null) {\n                    temp.next = cur.right;\n                    temp = temp.next; //移动temp\n                }\n            }\n            // 移动到下一层的实际头节点处\n            head = dummy.next;\n        }\n        \n        return root;\n    }\n}\n```\n\n## 114. 二叉树展开为链表\n\nhttps://leetcode.cn/problems/flatten-binary-tree-to-linked-list/description/?envType=study-plan-v2&envId=top-interview-150\n\n\n给你二叉树的根结点 root ，请你将它展开为一个单链表：\n\n- 展开后的单链表应该同样使用 TreeNode ，其中 right 子指针指向链表中下一个结点，而左子指针始终为 null 。\n- 展开后的单链表应该与二叉树 先序遍历 顺序相同。\n\n\n示例 1：\n\n![二叉树的最大深度.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%9C%80%E5%A4%A7%E6%B7%B1%E5%BA%A6.png)\n\n输入：root = [1,2,5,3,4,null,6]\n\n输出：[1,null,2,null,3,null,4,null,5,null,6]\n\n示例 2：\n\n输入：root = []\n\n输出：[]\n\n示例 3：\n\n输入：root = [0]\n\n输出：[0]\n\n\n提示：\n\n- 树中结点数在范围 [0, 2000] 内\n- -100 <= Node.val <= 100\n\n```java\nclass Solution {\n    public void flatten(TreeNode root) {\n        if (root == null) {\n            return;\n        }\n        \n        // 将左子树展开为链表\n        flatten(root.left);\n        // 将右子树展开为链表\n        flatten(root.right);\n        \n        // 保存右子树\n        TreeNode right = root.right;\n        \n        // 将左子树移到右子树上，连接起来\n        root.right = root.left;\n        root.left = null;\n        \n        // 找到现在链表的最后一个节点\n        while (root.right != null) {\n            root = root.right;\n        }\n        \n        // 将原来的右子树接在链表最后\n        root.right = right;\n    }\n}\n```\n\n## 112. 路径总和\nhttps://leetcode.cn/problems/path-sum/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径，这条路径上所有节点值相加等于目标和 targetSum 。如果存在，返回 true ；否则，返回 false 。\n\n叶子节点 是指没有子节点的节点。\n\n\n\n示例 1：\n\n![路径总和1.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E8%B7%AF%E5%BE%84%E6%80%BB%E5%92%8C1.png)\n\n输入：root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22\n\n输出：true\n\n解释：等于目标和的根节点到叶节点路径如上图所示。\n\n示例 2：\n\n![路径总和2.png](..%2Fimg%2F%E7%AE%97%E6%B3%95%2F%E8%B7%AF%E5%BE%84%E6%80%BB%E5%92%8C2.png)\n\n输入：root = [1,2,3], targetSum = 5\n\n输出：false\n\n解释：树中存在两条根节点到叶子节点的路径：\n- (1 --> 2): 和为 3\n- (1 --> 3): 和为 4\n- 不存在 sum = 5 的根节点到叶子节点的路径。\n\n示例 3：\n\n输入：root = [], targetSum = 0\n\n输出：false\n\n解释：由于树是空的，所以不存在根节点到叶子节点的路径。\n\n\n提示：\n\n- 树中节点的数目在范围 [0, 5000] 内\n- -1000 <= Node.val <= 1000\n- -1000 <= targetSum <= 1000\n\n```java\nclass Solution {\n    // 递归函数，判断是否存在从当前节点到叶子节点的路径，使得节点值之和等于目标和\n    public boolean hasPathSum(TreeNode root, int targetSum) {\n        // 如果当前节点为空，则返回 false\n        if(root == null){\n            return false;\n        }\n        // 如果当前节点是叶子节点，判断当前节点值是否等于目标和\n        if(root.left == null && root.right == null){\n            return root.val == targetSum;\n        }\n        // 递归判断左子树和右子树，更新目标和为减去当前节点值后的值\n        return hasPathSum(root.left,targetSum-root.val) || hasPathSum(root.right,targetSum-root.val);\n    }\n}\n```\n\n## 129. 求根节点到叶节点数字之和\nhttps://leetcode.cn/problems/sum-root-to-leaf-numbers/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个二叉树的根节点 root ，树中每个节点都存放有一个 0 到 9 之间的数字。\n\n每条从根节点到叶节点的路径都代表一个数字：\n\n- 例如，从根节点到叶节点的路径 1 -> 2 -> 3 表示数字 123 。\n\n计算从根节点到叶节点生成的 所有数字之和 。\n\n叶节点 是指没有子节点的节点。\n\n\n\n示例 1：\n\n\n输入：root = [1,2,3]\n\n输出：25\n\n解释：\n- 从根到叶子节点路径 1->2 代表数字 12\n- 从根到叶子节点路径 1->3 代表数字 13\n- 因此，数字总和 = 12 + 13 = 25\n\n示例 2：\n\n\n输入：root = [4,9,0,5,1]\n\n输出：1026\n\n解释：\n- 从根到叶子节点路径 4->9->5 代表数字 495\n- 从根到叶子节点路径 4->9->1 代表数字 491\n- 从根到叶子节点路径 4->0 代表数字 40\n\n因此，数字总和 = 495 + 491 + 40 = 1026\n\n\n提示：\n\n- 树中节点的数目在范围 [1, 1000] 内\n- 0 <= Node.val <= 9\n- 树的深度不超过 10\n\n```java\n\n/**\n * Definition for a binary tree node.\n * public class TreeNode {\n *     int val;\n *     TreeNode left;\n *     TreeNode right;\n *     TreeNode() {}\n *     TreeNode(int val) { this.val = val; }\n *     TreeNode(int val, TreeNode left, TreeNode right) {\n *         this.val = val;\n *         this.left = left;\n *         this.right = right;\n *     }\n * }\n */\n/**\n * 定义 Solution 类，用于计算二叉树中从根到叶子节点路径所代表的数字之和。\n */\nclass Solution {\n    int res = 0; // 初始化用于存储总和的变量\n\n    /**\n     * 计算二叉树中从根到叶子节点路径的数字总和。\n     *\n     * @param root 二叉树的根节点\n     * @return 从根到叶子节点路径所代表的数字之和\n     */\n    public int sumNumbers(TreeNode root) {\n        help(root, 0); // 调用辅助函数，初始累加值为0\n        return res; // 返回最终结果\n    }\n\n    /**\n     * 递归方法，用于计算从根到当前节点路径上的数字总和。\n     *\n     * @param root 当前二叉树节点\n     * @param k 从根节点到当前节点的累加值\n     */\n    public void help(TreeNode root, int k) {\n        if (root == null) {\n            return; // 基本情况：如果当前节点为空，直接返回\n        }\n        \n        int sum = k * 10 + root.val; // 计算从根到当前节点的累加值\n        \n        // 如果是叶子节点，将路径上的值累积到总和中\n        if (root.left == null && root.right == null) {\n            res += sum;\n        }\n        \n        // 递归调用左右子节点，并传递更新后的累加值\n        help(root.left, sum);\n        help(root.right, sum);\n    }\n}\n```\n\n## 124. 二叉树中的最大路径和\nhttps://leetcode.cn/problems/binary-tree-maximum-path-sum/description/?envType=study-plan-v2&envId=top-interview-150\n\n二叉树中的 路径 被定义为一条节点序列，序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点，且不一定经过根节点。\n\n路径和 是路径中各节点值的总和。\n\n给你一个二叉树的根节点 root ，返回其 最大路径和 。\n\n \n\n示例 1：\n\n![](../img/数据结构和算法/二叉树中的最大路径和1.png)\n\n\n输入：root = [1,2,3]\n\n输出：6\n\n解释：最优路径是 2 -> 1 -> 3 ，路径和为 2 + 1 + 3 = 6\n\n示例 2：\n\n![](../img/数据结构和算法/二叉树中的最大路径和2.png)\n\n输入：root = [-10,9,20,null,null,15,7]\n\n输出：42\n\n解释：最优路径是 15 -> 20 -> 7 ，路径和为 15 + 20 + 7 = 42\n \n\n提示：\n\n树中节点数目范围是 [1, 3 * 10^4]\n-1000 <= Node.val <= 1000\n\n\n```java\n/**\n * Definition for a binary tree node.\n * public class TreeNode {\n *     int val;\n *     TreeNode left;\n *     TreeNode right;\n *     TreeNode() {}\n *     TreeNode(int val) { this.val = val; }\n *     TreeNode(int val, TreeNode left, TreeNode right) {\n *         this.val = val;\n *         this.left = left;\n *         this.right = right;\n *     }\n * }\n */\n/**\n * 定义 Solution 类，用于计算二叉树中的最大路径和。\n */\nclass Solution {\n    int sum = Integer.MIN_VALUE; // 初始化变量以存储最大路径和\n\n    /**\n     * 计算二叉树中的最大路径和。\n     *\n     * @param root 二叉树的根节点\n     * @return 最大路径和\n     */\n    public int maxPathSum(TreeNode root) {\n        getMax(root); // 获取最大路径和\n        return sum; // 返回最大路径和\n    }\n\n    /**\n     * 辅助方法，找出从特定节点开始的最大路径和。\n     *\n     * @param root 当前二叉树节点\n     * @return 从当前节点开始的最大路径和\n     */\n    public int getMax(TreeNode root) {\n        if (root == null) {\n            return 0; // 如果当前节点为空，返回0\n        }\n\n        int left = Math.max(0, getMax(root.left)); // 计算左子树的最大路径和\n        int right = Math.max(0, getMax(root.right)); // 计算右子树的最大路径和\n\n        int n = left + right + root.val; // 计算包括当前节点在内的路径和\n\n        sum = Math.max(sum, n); // 更新全局最大路径和\n\n        return Math.max(left, right) + root.val; // 返回从当前节点开始的最大路径和\n    }\n}\n```\n\n## 173. 二叉搜索树迭代器\nhttps://leetcode.cn/problems/binary-search-tree-iterator/description/?envType=study-plan-v2&envId=top-interview-150\n\n实现一个二叉搜索树迭代器类BSTIterator ，表示一个按中序遍历二叉搜索树（BST）的迭代器：\n- BSTIterator(TreeNode root) 初始化 BSTIterator 类的一个对象。BST 的根节点 root 会作为构造函数的一部分给出。指针应初始化为一个不存在于 BST 中的数字，且该数字小于 BST 中的任何元素。\n- boolean hasNext() 如果向指针右侧遍历存在数字，则返回 true ；否则返回 false 。\n- int next()将指针向右移动，然后返回指针处的数字。\n注意，指针初始化为一个不存在于 BST 中的数字，所以对 next() 的首次调用将返回 BST 中的最小元素。\n\n你可以假设 next() 调用总是有效的，也就是说，当调用 next() 时，BST 的中序遍历中至少存在一个下一个数字。\n\n \n\n示例：\n\n![](../img/数据结构和算法/二叉搜索迭代树.png)\n\n输入\n\n[\"BSTIterator\", \"next\", \"next\", \"hasNext\", \"next\", \"hasNext\", \"next\", \"hasNext\", \"next\", \"hasNext\"]\n\n[[[7, 3, 15, null, null, 9, 20]], [], [], [], [], [], [], [], [], []]\n\n输出\n\n[null, 3, 7, true, 9, true, 15, true, 20, false]\n\n解释\n\nBSTIterator bSTIterator = new BSTIterator([7, 3, 15, null, null, 9, 20]);\n\nbSTIterator.next();    // 返回 3\n\nbSTIterator.next();    // 返回 7\n\nbSTIterator.hasNext(); // 返回 True\n\nbSTIterator.next();    // 返回 9\n\nbSTIterator.hasNext(); // 返回 True\n\nbSTIterator.next();    // 返回 15\n\nbSTIterator.hasNext(); // 返回 True\n\nbSTIterator.next();    // 返回 20\n\nbSTIterator.hasNext(); // 返回 False\n \n\n提示：\n\n- 树中节点的数目在范围 [1, 10^5] 内\n- 0 <= Node.val <= 106\n- 最多调用 10^5 次 hasNext 和 next 操作\n \n\n进阶：\n\n你可以设计一个满足下述条件的解决方案吗？next() 和 hasNext() 操作均摊时间复杂度为 O(1) ，并使用 O(h) 内存。其中 h 是树的高度。\n\n这段代码是一个用于二叉搜索树（BST）的迭代器实现。下面我会对代码的每一行添加注释来解释其功能。\n\n\n```java\n// 定义BSTIterator类，用于遍历二叉搜索树（BST）\nclass BSTIterator {\n    // 当前遍历的节点指针\n    private TreeNode cur;\n    // 用于存储节点的栈，辅助中序遍历\n    private Deque<TreeNode> stack;\n\n    // 构造方法，初始化迭代器的当前节点为根节点，并创建一个空的栈\n    public BSTIterator(TreeNode root) {\n        cur = root;  // 设置当前节点为根节点\n        stack = new LinkedList<TreeNode>();  // 创建一个新的链表作为栈来存储节点\n    }\n    \n    // 返回下一个最小的节点值，实现迭代器的next方法\n    public int next() {\n        // 一直沿着当前节点的左子树走，将走过的节点压入栈中，直到当前节点为空\n        while (cur != null) {\n            stack.push(cur);  // 将当前节点压入栈中\n            cur = cur.left;  // 移动到当前节点的左子节点上\n        }\n        // 取出栈顶元素作为下一个要返回的节点（此时cur指向null，因此下一个节点是栈顶元素）\n        cur = stack.pop();  // 弹出栈顶元素作为当前节点\n        int ret = cur.val;  // 获取当前节点的值并返回\n        // 将当前节点移动到右子树上（准备处理下一个next请求时继续遍历右子树）\n        cur = cur.right;  // 更新当前节点为其右子节点（如果有的话）\n        return ret;  // 返回当前节点的值\n    }\n    \n    // 判断是否还有下一个节点可以遍历，实现迭代器的hasNext方法\n    public boolean hasNext() {\n        // 如果当前节点不为空或者栈不为空（即还有未遍历的节点），则返回true表示还有下一个元素可以遍历\n        return cur != null || !stack.isEmpty();  // 返回是否有下一个节点的标志位（true或false）\n    }\n}\n```\n这段代码的核心思想是利用一个栈来辅助实现中序遍历，从而能够在不知道BST结构的情况下按顺序访问所有节点。`next()`方法用于返回下一个最小的节点值，而`hasNext()`方法用于判断是否还有下一个节点可以遍历。\n\n## 222. 完全二叉树的节点个数\nhttps://leetcode.cn/problems/count-complete-tree-nodes/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一棵 完全二叉树 的根节点 root ，求出该树的节点个数。\n\n完全二叉树 的定义如下：在完全二叉树中，除了最底层节点可能没填满外，其余每层节点数都达到最大值，并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层，则该层包含 1~ 2h 个节点。\n\n \n\n示例 1：\n\n![](../img/数据结构和算法/完全二叉树的节点个数.png)\n\n输入：root = [1,2,3,4,5,6]\n\n输出：6\n\n示例 2：\n\n输入：root = []\n\n输出：0\n\n示例 3：\n\n输入：root = [1]\n\n输出：1\n \n\n提示：\n\n- 树中节点的数目范围是[0, 5 * 10^4]\n- 0 <= Node.val <= 5 * 10^4\n- 题目数据保证输入的树是 完全二叉树\n\n```java\npublic class TreeNode {\n    int val;\n    TreeNode left;\n    TreeNode right;\n    TreeNode(int x) { val = x; }\n}\n\npublic class Solution {\n    public int countNodes(TreeNode root) {\n        if (root == null) return 0;\n        \n        // 计算左子树高度\n        int leftHeight = getHeight(root.left);\n        // 计算右子树高度\n        int rightHeight = getHeight(root.right);\n        \n        // 如果左右子树高度相等，说明最后一层未满，直接按照满二叉树计算节点数再加1（根节点）\n        if (leftHeight == rightHeight) {\n            return (1 << leftHeight) + countNodes(root.right); // 2^leftHeight 加上右子树的节点数\n        } \n        // 如果左子树比右子树高1，说明右子树是满的，计算右子树节点数再加1（根节点）加上左子树最后一层的额外节点数\n        else {\n            return (1 << rightHeight) + countNodes(root.left); // 2^rightHeight 加上左子树的节点数\n        }\n    }\n\n    private int getHeight(TreeNode node) {\n        int height = 0;\n        while (node != null) {\n            height++;\n            node = node.left;\n        }\n        return height;\n    }\n}\n```\n\n## 236. 二叉树的最近公共祖先\nhttps://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。\n\n百度百科中最近公共祖先的定义为：“对于有根树 T 的两个节点 p、q，最近公共祖先表示为一个节点 x，满足 x 是 p、q 的祖先且 x 的深度尽可能大（一个节点也可以是它自己的祖先）。”\n\n \n\n示例 1：\n\n![alt text](../img/数据结构和算法/二叉树的最近公共祖先1.png)\n\n输入：root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1\n\n输出：3\n\n解释：节点 5 和节点 1 的最近公共祖先是节点 3 。\n\n示例 2：\n\n![alt text](../img/数据结构和算法/二叉树的最近公共祖先2.png)\n\n输入：root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4\n\n输出：5\n\n解释：节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可\n以为节点本身。\n\n示例 3：\n\n输入：root = [1,2], p = 1, q = 2\n\n输出：1\n \n\n提示：\n\n- 树中节点数目在范围 [2, 10^5] 内。\n- -10^9 <= Node.val <= 10^9\n- 所有 Node.val 互不相同 。\n- p != q\n- p 和 q 均存在于给定的二叉树中。\n\n```java\nclass TreeNode {\n    int val;\n    TreeNode left;\n    TreeNode right;\n    TreeNode(int x) { val = x; }\n}\n\npublic class Solution {\n    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {\n        // 如果根节点为空或者已经找到了p或q，则返回当前节点\n        if (root == null || root == p || root == q) {\n            return root;\n        }\n        \n        // 在左子树中查找p和q\n        TreeNode left = lowestCommonAncestor(root.left, p, q);\n        // 在右子树中查找p和q\n        TreeNode right = lowestCommonAncestor(root.right, p, q);\n        \n        // 如果p和q分别位于当前节点的左右子树中，则当前节点即为最近公共祖先\n        if (left != null && right != null) {\n            return root;\n        }\n        \n        // 如果p和q都在左子树中，则返回左子树查找到的结果\n        if (left != null) {\n            return left;\n        }\n        // 如果p和q都在右子树中，则返回右子树查找到的结果\n        return right;\n    }\n}\n```\n\n# 二叉树层次遍历\n## 199. 二叉树的右视图\nhttps://leetcode.cn/problems/binary-tree-right-side-view/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个二叉树的 根节点 root，想象自己站在它的右侧，按照从顶部到底部的顺序，返回从右侧所能看到的节点值。\n\n \n\n示例 1:\n\n![alt text](../img/数据结构和算法/二叉树的右视图.png)\n\n输入: [1,2,3,null,5,null,4]\n\n输出: [1,3,4]\n\n示例 2:\n\n输入: [1,null,3]\n\n输出: [1,3]\n\n示例 3:\n\n输入: []\n\n输出: []\n \n\n提示:\n\n- 二叉树的节点个数的范围是 [0,100]\n- -100 <= Node.val <= 100 \n\n```java\nimport java.util.*;\n\nclass TreeNode {\n    int val;\n    TreeNode left;\n    TreeNode right;\n    TreeNode(int x) { val = x; }\n}\n\npublic class Solution {\n    public List<Integer> rightSideView(TreeNode root) {\n        List<Integer> result = new ArrayList<>();\n        if (root == null) return result;\n\n        Queue<TreeNode> queue = new LinkedList<>();\n        queue.offer(root);\n\n        while (!queue.isEmpty()) {\n            int levelSize = queue.size();\n            for (int i = 0; i < levelSize; i++) {\n                TreeNode current = queue.poll();\n                // 每一层的最后一个节点添加到结果列表\n                if (i == levelSize - 1) {\n                    result.add(current.val);\n                }\n                \n                // 将当前节点的左右子节点加入队列，以便下一轮遍历\n                if (current.left != null) {\n                    queue.offer(current.left);\n                }\n                if (current.right != null) {\n                    queue.offer(current.right);\n                }\n            }\n        }\n        return result;\n    }\n}\n```\n\n## 637. 二叉树的层平均值\nhttps://leetcode.cn/problems/average-of-levels-in-binary-tree/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个非空二叉树的根节点 root , 以数组的形式返回每一层节点的平均值。与实际答案相差 10-5 以内的答案可以被接受。\n\n \n\n示例 1：\n\n![alt text](../img/数据结构和算法/二叉树的层平均值1.png)\n\n输入：root = [3,9,20,null,null,15,7]\n\n输出：[3.00000,14.50000,11.00000]\n\n解释：第 0 层的平均值为 3,第 1 层的平均值为 14.5,第 2 层的平均值为 11 。\n\n因此返回 [3, 14.5, 11] 。\n\n示例 2:\n\n![alt text](../img/数据结构和算法/二叉树的层平均值2.png)\n\n输入：root = [3,9,20,15,7]\n\n输出：[3.00000,14.50000,11.00000]\n \n\n提示：\n\n- 树中节点数量在 [1, 10^4] 范围内\n- -2^31 <= Node.val <= 2^31 - 1\n\n```java\nimport java.util.*;\n\nclass TreeNode {\n    int val;\n    TreeNode left;\n    TreeNode right;\n    TreeNode(int x) { val = x; }\n}\n\n/**\n * 平均值计算类，用于计算二叉树各层的平均值。\n */\npublic class Solution {\n    /**\n     * 计算二叉树各层的平均值。\n     * \n     * @param root 二叉树的根节点。\n     * @return 各层的平均值列表。\n     */\n    public List<Double> averageOfLevels(TreeNode root) {\n        List<Double> averages = new ArrayList<>();\n        // 如果根节点为空，则直接返回空列表\n        if (root == null) return averages;\n\n        Queue<TreeNode> queue = new LinkedList<>();\n        queue.offer(root);\n\n        // 使用广度优先搜索遍历二叉树的每一层\n        while (!queue.isEmpty()) {\n            int levelSize = queue.size();\n            double sum = 0;\n            // 遍历当前层的节点，计算这一层的节点值总和\n            for (int i = 0; i < levelSize; i++) {\n                TreeNode currentNode = queue.poll();\n                sum += currentNode.val;\n\n                // 如果当前节点有左子节点，将左子节点加入队列\n                if (currentNode.left != null) {\n                    queue.offer(currentNode.left);\n                }\n                // 如果当前节点有右子节点，将右子节点加入队列\n                if (currentNode.right != null) {\n                    queue.offer(currentNode.right);\n                }\n            }\n            // 计算当前层的平均值，并加入结果列表\n            // 计算当前层的平均值并添加到结果列表中\n            averages.add(sum / levelSize);\n        }\n        return averages;\n    }\n}\n\n```\n\n## 102. 二叉树的层序遍历\nhttps://leetcode.cn/problems/binary-tree-level-order-traversal/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你二叉树的根节点 root ，返回其节点值的 层序遍历 。 （即逐层地，从左到右访问所有节点）。\n\n \n\n示例 1：\n\n![alt text](../img/数据结构和算法/二叉树的层序遍历.png)\n\n输入：root = [3,9,20,null,null,15,7]\n\n输出：[[3],[9,20],[15,7]]\n\n示例 2：\n\n输入：root = [1]\n\n输出：[[1]]\n\n示例 3：\n\n输入：root = []\n\n输出：[]\n \n\n提示：\n\n- 树中节点数目在范围 [0, 2000] 内\n- -1000 <= Node.val <= 1000\n\n```java\n/**\n * 二叉树的层序遍历实现。\n * \n * @param root 二叉树的根节点。\n * @return 层序遍历的结果，以二维列表表示。\n */\npublic List<List<Integer>> levelOrder(TreeNode root) {\n    List<List<Integer>> result = new ArrayList<>();\n    if (root == null) return result;\n\n    Queue<TreeNode> queue = new LinkedList<>();\n    queue.offer(root);\n\n    while (!queue.isEmpty()) {\n        int levelSize = queue.size();\n        List<Integer> currentLevel = new ArrayList<>();\n        // 遍历当前层的节点\n        for (int i = 0; i < levelSize; i++) {\n            TreeNode currentNode = queue.poll();\n            currentLevel.add(currentNode.val);\n\n            // 将当前节点的子节点加入队列，以便下一轮遍历\n            if (currentNode.left != null) {\n                queue.offer(currentNode.left);\n            }\n            if (currentNode.right != null) {\n                queue.offer(currentNode.right);\n            }\n        }\n        // 将当前层的节点值加入结果列表\n        result.add(currentLevel);\n    }\n    return result;\n}\n```\n\n## 103. 二叉树的锯齿形层序遍历\nhttps://leetcode.cn/problems/binary-tree-zigzag-level-order-traversal/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你二叉树的根节点 root ，返回其节点值的 锯齿形层序遍历 。（即先从左往右，再从右往左进行下一层遍历，以此类推，层与层之间交替进行）。\n\n \n\n示例 1：\n\n![alt text](../img/数据结构和算法/二叉树的锯齿形层序遍历.png)\n\n输入：root = [3,9,20,null,null,15,7]\n\n输出：[[3],[20,9],[15,7]]\n\n示例 2：\n\n输入：root = [1]\n\n输出：[[1]]\n\n示例 3：\n\n输入：root = []\n\n输出：[]\n \n\n提示：\n\n- 树中节点数目在范围 [0, 2000] 内\n- -100 <= Node.val <= 100\n\n```java\n/**\n * Definition for a binary tree node.\n * public class TreeNode {\n *     int val;\n *     TreeNode left;\n *     TreeNode right;\n *     TreeNode() {}\n *     TreeNode(int val) { this.val = val; }\n *     TreeNode(int val, TreeNode left, TreeNode right) {\n *         this.val = val;\n *         this.left = left;\n *         this.right = right;\n *     }\n * }\n */\n/**\n * 二叉树的锯齿形层序遍历实现。\n *\n * @param root 二叉树的根节点。\n * @return 锯齿形层序遍历的结果，以二维列表表示。\n */\npublic List<List<Integer>> zigzagLevelOrder(TreeNode root) {\n    List<List<Integer>> result = new ArrayList<>();\n    if (root == null) return result;\n\n    Queue<TreeNode> queue = new LinkedList<>();\n    queue.offer(root);\n    boolean leftToRight = true; // 标记当前层的遍历方向\n\n    while (!queue.isEmpty()) {\n        int levelSize = queue.size();\n        List<Integer> currentLevel = new ArrayList<>(levelSize);\n\n        for (int i = 0; i < levelSize; i++) {\n            TreeNode currentNode = queue.poll();\n            // 根据当前层的遍历方向决定添加顺序\n            if (leftToRight) {\n                currentLevel.add(currentNode.val);\n            } else {\n                currentLevel.add(0, currentNode.val); // 在列表头部添加元素以实现从右向左\n            }\n\n            // 添加子节点到队列中，以便下一层的遍历\n            if (currentNode.left != null) {\n                queue.offer(currentNode.left);\n            }\n            if (currentNode.right != null) {\n                queue.offer(currentNode.right);\n            }\n        }\n        // 完成一层后，切换遍历方向\n        leftToRight = !leftToRight;\n        result.add(currentLevel);\n    }\n    return result;\n}\n```\n\n# 二叉搜索树\n## 530. 二叉搜索树的最小绝对差\nhttps://leetcode.cn/problems/minimum-absolute-difference-in-bst/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个二叉搜索树的根节点 root ，返回 树中任意两不同节点值之间的最小差值 。\n\n差值是一个正数，其数值等于两值之差的绝对值。\n\n \n\n示例 1：\n\n![alt text](../img/数据结构和算法/二叉搜索树的最小绝对差1.png)\n\n输入：root = [4,2,6,1,3]\n\n输出：1\n\n示例 2：\n\n![alt text](../img/数据结构和算法/二叉搜索树的最小绝对差2.png)\n\n输入：root = [1,0,48,null,null,12,49]\n\n输出：1\n \n\n提示：\n\n- 树中节点的数目范围是 [2, 10^4]\n- 0 <= Node.val <= 10^5\n\n```java\n/**\n * Definition for a binary tree node.\n * public class TreeNode {\n *     int val;\n *     TreeNode left;\n *     TreeNode right;\n *     TreeNode(int x) { val = x; }\n * }\n */\n\npublic class Solution {\n    private int minDiff = Integer.MAX_VALUE; // 初始化最小差值为最大整数\n    private TreeNode prev = null; // 用于记录上一个访问的节点\n\n    public int getMinimumDifference(TreeNode root) {\n        inorderTraversal(root); // 进行中序遍历\n        return minDiff;\n    }\n\n    /**\n     * 中序遍历二叉搜索树，同时更新最小差值。\n     * \n     * @param node 当前访问的节点。\n     */\n    private void inorderTraversal(TreeNode node) {\n        if (node == null) return;\n\n        inorderTraversal(node.left); // 先遍历左子树\n\n        // 处理当前节点，如果prev不为空，则计算与prev的差值并更新minDiff\n        if (prev != null) {\n            minDiff = Math.min(minDiff, node.val - prev.val);\n        }\n        prev = node; // 更新prev为当前节点\n\n        inorderTraversal(node.right); // 遍历右子树\n    }\n}\n```\n\n## 230. 二叉搜索树中第K小的元素\nhttps://leetcode.cn/problems/kth-smallest-element-in-a-bst/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个二叉搜索树的根节点 root ，和一个整数 k ，请你设计一个算法查找其中第 k 小的元素（从 1 开始计数）。\n\n \n\n示例 1：\n\n![alt text](../img/数据结构和算法/二叉搜索树中第K小的元素1.png)\n\n输入：root = [3,1,4,null,2], k = 1\n\n输出：1\n\n示例 2：\n\n![alt text](../img/数据结构和算法/二叉搜索树中第K小的元素2.png)\n\n输入：root = [5,3,6,2,4,null,null,1], k = 3\n\n输出：3\n \n提示：\n\n- 树中的节点数为 n 。\n- 1 <= k <= n <= 10^4\n- 0 <= Node.val <= 10^4\n\n```java\n/**\n * Definition for a binary tree node.\n * public class TreeNode {\n *     int val;\n *     TreeNode left;\n *     TreeNode right;\n *     TreeNode(int x) { val = x; }\n * }\n */\nclass Solution {\n    private int count = 0; // 计数器，用于记录已经遍历过的节点数量\n    private int kthVal = -1; // 用于存储第k小的元素的值\n\n    public int kthSmallest(TreeNode root, int k) {\n        inorderTraversal(root, k); // 进行中序遍历\n        return kthVal;\n    }\n\n    /**\n     * 中序遍历二叉搜索树的递归辅助函数，同时寻找第k小的元素。\n     * \n     * @param node 当前访问的节点。\n     * @param k 目标是找到第k小的元素。\n     */\n    private void inorderTraversal(TreeNode node, int k) {\n        if (node == null || count >= k) return;\n\n        inorderTraversal(node.left, k); // 先遍历左子树\n\n        // 访问当前节点，计数器加一\n        count++;\n        if (count == k) {\n            kthVal = node.val; // 找到第k小的元素，将其值赋给kthVal\n            return; // 已经找到，无需继续遍历\n        }\n\n        inorderTraversal(node.right, k); // 继续遍历右子树\n    }\n}\n```\n\n## 98. 验证二叉搜索树\nhttps://leetcode.cn/problems/validate-binary-search-tree/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个二叉树的根节点 root ，判断其是否是一个有效的二叉搜索树。\n\n有效 二叉搜索树定义如下：\n\n- 节点的左子树只包含 小于 当前节点的数。\n- 节点的右子树只包含 大于 当前节点的数。\n- 所有左子树和右子树自身必须也是二叉搜索树。\n \n\n示例 1：\n\n![alt text](../img/数据结构和算法/验证二叉搜索树1.png)\n\n输入：root = [2,1,3]\n\n输出：true\n\n示例 2：\n\n![alt text](../img/数据结构和算法/验证二叉搜索树2.png)\n\n输入：root = [5,1,4,null,null,3,6]\n\n输出：false\n\n解释：根节点的值是 5 ，但是右子节点的值是 4 。\n \n\n提示：\n\n- 树中节点数目范围在[1, 10^4] 内\n- -2^31 <= Node.val <= 2^31 - 1\n\n```java\n/**\n * Definition for a binary tree node.\n * public class TreeNode {\n *     int val;\n *     TreeNode left;\n *     TreeNode right;\n *     TreeNode(int x) { val = x; }\n * }\n */\nclass Solution {\n    private long pre = Long.MIN_VALUE; // 用于记录前一个遍历节点的值\n\n    public boolean isValidBST(TreeNode root) {\n        if (root == null) return true;\n\n        // 首先检查左子树是否为二叉搜索树\n        if (!isValidBST(root.left)) {\n            return false;\n        }\n\n        // 检查当前节点的值是否大于前一个节点的值\n        if (root.val <= pre) {\n            return false;\n        }\n        pre = root.val; // 更新pre为当前节点的值，供后续节点比较使用\n\n        // 最后检查右子树是否为二叉搜索树\n        return isValidBST(root.right);\n    }\n}\n```\n\n# 图\n## 200. 岛屿数量\nhttps://leetcode.cn/problems/number-of-islands/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个由 '1'（陆地）和 '0'（水）组成的的二维网格，请你计算网格中岛屿的数量。\n\n岛屿总是被水包围，并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。\n\n此外，你可以假设该网格的四条边均被水包围。\n\n \n\n示例 1：\n\n输入：grid = [\n \n  [\"1\",\"1\",\"1\",\"1\",\"0\"],\n \n  [\"1\",\"1\",\"0\",\"1\",\"0\"],\n \n  [\"1\",\"1\",\"0\",\"0\",\"0\"],\n \n  [\"0\",\"0\",\"0\",\"0\",\"0\"]\n\n]\n输出：1\n\n示例 2：\n\n输入：grid = [\n\n  [\"1\",\"1\",\"0\",\"0\",\"0\"],\n\n  [\"1\",\"1\",\"0\",\"0\",\"0\"],\n\n  [\"0\",\"0\",\"1\",\"0\",\"0\"],\n\n  [\"0\",\"0\",\"0\",\"1\",\"1\"]\n\n]\n\n输出：3\n \n\n提示：\n\n- m == grid.length\n- n == grid[i].length\n- 1 <= m, n <= 300\n- grid[i][j] 的值为 '0' 或 '1'\n\n```java\nclass Solution {\n    private int[][] directions = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; // 上下左右四个方向\n\n    public int numIslands(char[][] grid) {\n        if (grid == null || grid.length == 0) {\n            return 0;\n        }\n        int m = grid.length;\n        int n = grid[0].length;\n        int islandCount = 0;\n\n        for (int i = 0; i < m; i++) {\n            for (int j = 0; j < n; j++) {\n                if (grid[i][j] == '1') {\n                    islandCount++; // 发现新的岛屿，计数加一\n                    dfs(grid, i, j); // 深度优先遍历，将与之相连的所有陆地标记为水\n                }\n            }\n        }\n        return islandCount;\n    }\n\n    private void dfs(char[][] grid, int row, int col) {\n        if (row < 0 || row >= grid.length || col < 0 || col >= grid[0].length || grid[row][col] == '0') {\n            return; // 越界或者已经是水了，直接返回\n        }\n        \n        grid[row][col] = '0'; // 将当前陆地标记为水，防止重复访问\n        \n        // 对当前陆地的上下左右四个方向进行深度优先搜索\n        for (int[] direction : directions) {\n            int newRow = row + direction[0];\n            int newCol = col + direction[1];\n            dfs(grid, newRow, newCol);\n        }\n    }\n}\n```\n\n## 130. 被围绕的区域\nhttps://leetcode.cn/problems/surrounded-regions/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个 m x n 的矩阵 board ，由若干字符 'X' 和 'O' 组成，捕获 所有 被围绕的区域：\n\n- 连接：一个单元格与水平或垂直方向上相邻的单元格连接。\n- 区域：连接所有 '0' 的单元格来形成一个区域。\n- 围绕：如果您可以用 'X' 单元格 连接这个区域，并且区域中没有任何单元格位于 board 边缘，则该区域被 'X' 单元格围绕。\n\n通过将输入矩阵 board 中的所有 'O' 替换为 'X' 来 捕获被围绕的区域。\n\n \n\n示例 1：\n\n输入：board = [[\"X\",\"X\",\"X\",\"X\"],[\"X\",\"O\",\"O\",\"X\"],[\"X\",\"X\",\"O\",\"X\"],[\"X\",\"O\",\"X\",\"X\"]]\n\n输出：[[\"X\",\"X\",\"X\",\"X\"],[\"X\",\"X\",\"X\",\"X\"],[\"X\",\"X\",\"X\",\"X\"],[\"X\",\"O\",\"X\",\"X\"]]\n\n解释：\n\n![alt text](../img/数据结构和算法/被围绕的区域.png)\n\n在上图中，底部的区域没有被捕获，因为它在 board 的边缘并且不能被围绕。\n\n示例 2：\n\n输入：board = [[\"X\"]]\n\n输出：[[\"X\"]]\n\n \n\n提示：\n\n- m == board.length\n- n == board[i].length\n- 1 <= m, n <= 200\n- board[i][j] 为 'X' 或 'O'\n\n```java\n/**\n * 解决方案类，用于处理二维字符数组上的特定问题。\n */\nclass Solution {\n    /**\n     * 四个方向的移动数组，用于深度优先搜索。\n     */\n    int[][] dir = new int[][]{{0,1},{0,-1},{-1,0},{1,0}};\n    int n;\n    int m;\n\n    /**\n     * 对给定的二维字符数组进行处理。\n     * \n     * @param board 二维字符数组，表示游戏板。\n     */\n    public void solve(char[][] board) {\n        // 如果游戏板为空或长度为0，则直接返回。\n        if (board == null || board.length == 0) {\n            return;\n        }\n        // 初始化游戏板的行数和列数。\n        m = board.length;\n        n = board[0].length;\n\n        // 从每一行的两端开始进行深度优先搜索。\n        for(int i=0;i<m;i++){\n            dfs(board,i,0);\n            dfs(board,i,n-1);\n        }\n        // 从每一列的两端开始进行深度优先搜索。\n        for(int j=0;j<n;j++){\n            dfs(board,0,j);\n            dfs(board,m-1,j);\n        }\n\n        // 将所有标记为'T'的恢复为'O'，将所有剩余的'O'标记为'X'。\n        for(int i=0;i<m;i++){\n            for(int j=0;j<n;j++){\n                if(board[i][j]=='T'){\n                    board[i][j]='O';\n                }else if(board[i][j]=='O'){\n                    board[i][j]='X';\n                }\n            }\n        }\n    }\n\n    /**\n     * 使用深度优先搜索标记相邻的'O'字符。\n     * \n     * @param board 二维字符数组，表示游戏板。\n     * @param i 当前行索引。\n     * @param j 当前列索引。\n     */\n    public void dfs(char[][] board,int i,int j){\n        // 如果当前位置超出边界或不是'O'字符，则返回。\n        if(i<0 || i>=m||j<0||j>=n || board[i][j]!='O'){\n            return;\n        }\n        // 将当前位置的字符标记为'T'，表示已经访问过。\n        board[i][j] = 'T';\n        // 遍历四个方向，对相邻的'O'字符进行深度优先搜索。\n        for(int[] d:dir){\n            dfs(board,i+d[0],j+d[1]);\n        }\n    }\n}\n```\n\n## 133. 克隆图\nhttps://leetcode.cn/problems/clone-graph/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你无向 连通 图中一个节点的引用，请你返回该图的 深拷贝（克隆）。\n\n图中的每个节点都包含它的值 val（int） 和其邻居的列表（list[Node]）。\n```java\nclass Node {\n    public int val;\n    public List<Node> neighbors;\n}\n```\n\n测试用例格式：\n\n简单起见，每个节点的值都和它的索引相同。例如，第一个节点值为 1（val = 1），第二个节点值为 2（val = 2），以此类推。该图在测试用例中使用邻接列表表示。\n\n邻接列表 是用于表示有限图的无序列表的集合。每个列表都描述了图中节点的邻居集。\n\n给定节点将始终是图中的第一个节点（值为 1）。你必须将 给定节点的拷贝 作为对克隆图的引用返回。\n\n \n\n示例 1：\n\n![alt text](../img/数据结构和算法/克隆图.png)\n\n输入：adjList = [[2,4],[1,3],[2,4],[1,3]]\n\n输出：[[2,4],[1,3],[2,4],[1,3]]\n\n解释：\n\n图中有 4 个节点。\n\n节点 1 的值是 1，它有两个邻居：节点 2 和 4 。\n\n节点 2 的值是 2，它有两个邻居：节点 1 和 3 。\n\n节点 3 的值是 3，它有两个邻居：节点 2 和 4 。\n\n节点 4 的值是 4，它有两个邻居：节点 1 和 3 。\n\n示例 2：\n\n![alt text](../img/数据结构和算法/克隆图2.png)\n\n\n输入：adjList = [[]]\n\n输出：[[]]\n\n解释：输入包含一个空列表。该图仅仅只有一个值为 1 的节点，它没有任何邻居。\n\n示例 3：\n\n输入：adjList = []\n\n输出：[]\n\n解释：这个图是空的，它不含任何节点。\n \n\n提示：\n\n- 这张图中的节点数在 [0, 100] 之间。\n- 1 <= Node.val <= 100\n- 每个节点值 Node.val 都是唯一的，\n- 图中没有重复的边，也没有自环。\n- 图是连通图，你可以从给定节点访问到所有节点。\n\n```java\nclass Solution {\n    private Map<Node, Node> hashMap;\n\n    public Node cloneGraph(Node node) {\n        if (node == null) {\n            return null;\n        }\n        \n        hashMap = new HashMap<>();\n        \n        // 使用 DFS 克隆图\n        return dfs(node);\n    }\n    \n    private Node dfs(Node node) {\n        // 如果节点已访问过，直接从哈希表中返回其克隆节点\n        if (hashMap.containsKey(node)) {\n            return hashMap.get(node);\n        }\n        \n        // 创建新节点并加入哈希表\n        Node cloneNode = new Node(node.val, new ArrayList<>());\n        hashMap.put(node, cloneNode);\n        \n        // 遍历原节点的邻居，克隆它们并添加到当前克隆节点的邻居列表\n        for (Node neighbor : node.neighbors) {\n            cloneNode.neighbors.add(dfs(neighbor));\n        }\n        \n        return cloneNode;\n    }\n}\n```\n\n## 399. 除法求值\nhttps://leetcode.cn/problems/evaluate-division/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个变量对数组 equations 和一个实数值数组 values 作为已知条件，其中 equations[i] = [Ai, Bi] 和 values[i] 共同表示等式 Ai / Bi = values[i] 。每个 Ai 或 Bi 是一个表示单个变量的字符串。\n\n另有一些以数组 queries 表示的问题，其中 queries[j] = [Cj, Dj] 表示第 j 个问题，请你根据已知条件找出 Cj / Dj = ? 的结果作为答案。\n\n返回 所有问题的答案 。如果存在某个无法确定的答案，则用 -1.0 替代这个答案。如果问题中出现了给定的已知条件中没有出现的字符串，也需要用 -1.0 替代这个答案。\n\n注意：输入总是有效的。你可以假设除法运算中不会出现除数为 0 的情况，且不存在任何矛盾的结果。\n\n注意：未在等式列表中出现的变量是未定义的，因此无法确定它们的答案。\n\n \n\n示例 1：\n\n输入：equations = [[\"a\",\"b\"],[\"b\",\"c\"]], values = [2.0,3.0], queries = [[\"a\",\"c\"],[\"b\",\"a\"],[\"a\",\"e\"],[\"a\",\"a\"],[\"x\",\"x\"]]\n\n输出：[6.00000,0.50000,-1.00000,1.00000,-1.00000]\n\n解释：\n\n条件：a / b = 2.0, b / c = 3.0\n\n问题：a / c = ?, b / a = ?, a / e = ?, a / a = ?, x / x = ?\n\n结果：[6.0, 0.5, -1.0, 1.0, -1.0 ]\n\n注意：x 是未定义的 => -1.0\n\n示例 2：\n\n输入：equations = [[\"a\",\"b\"],[\"b\",\"c\"],[\"bc\",\"cd\"]], values = [1.5,2.5,5.0], queries = [[\"a\",\"c\"],[\"c\",\"b\"],[\"bc\",\"cd\"],[\"cd\",\"bc\"]]\n\n输出：[3.75000,0.40000,5.00000,0.20000]\n\n示例 3：\n\n输入：equations = [[\"a\",\"b\"]], values = [0.5], queries = [[\"a\",\"b\"],[\"b\",\"a\"],[\"a\",\"c\"],[\"x\",\"y\"]]\n\n输出：[0.50000,2.00000,-1.00000,-1.00000]\n \n\n提示：\n\n- 1 <= equations.length <= 20\n- equations[i].length == 2\n- 1 <= Ai.length, Bi.length <= 5\n- values.length == equations.length\n- 0.0 < values[i] <= 20.0\n- 1 <= queries.length <= 20\n- queries[i].length == 2\n- 1 <= Cj.length, Dj.length <= 5\n- Ai, Bi, Cj, Dj 由小写英文字母与数字组成\n\n要解决这个问题，我们可以使用图的数据结构来表示变量之间的关系，并利用广度优先搜索（BFS）来查询两个变量之间的除法结果。首先，我们需要构建一个邻接表来表示变量之间的关系及它们的除法值。然后，对于每个查询，我们尝试从源变量走到目标变量，计算路径上的除法结果乘积。\n\n步骤概览：\n- 构建图：遍历给定的等式，将每个变量作为节点，并根据等式关系和值构建有向带权图。\n- 执行查询：对于每个查询，使用BFS尝试从源节点到达目标节点，同时累积路径上的权值乘积。如果不能到达，则返回-1.0。\n\n下面是具体的Java实现代码：\n\n```java\nimport java.util.*;\n\nclass Solution {\n    /**\n     * 计算给定方程组中各个查询的比值。\n     * \n     * @param equations 方程组，每个方程由两个变量和一个等式组成。\n     * @param values 方程组中每个方程的比值。\n     * @param queries 查询列表，每个查询包含两个变量，用于求比值。\n     * @return 每个查询的比值数组，如果无法计算则返回-1.0。\n     */\n    public double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries) {\n        // 构建变量之间的图\n        Map<String, Map<String, Double>> graph = buildGraph(equations, values);\n        double[] results = new double[queries.size()];\n        \n        // 遍历查询，使用BFS算法计算比值\n        for (int i = 0; i < queries.size(); ++i) {\n            String src = queries.get(i).get(0);\n            String dst = queries.get(i).get(1);\n            results[i] = bfs(src, dst, graph);\n        }\n        \n        return results;\n    }\n\n    /**\n     * 构建变量之间的图。\n     * \n     * @param equations 方程组。\n     * @param values 方程组中每个方程的比值。\n     * @return 图的表示，使用邻接表存储。\n     */\n    private Map<String, Map<String, Double>> buildGraph(List<List<String>> equations, double[] values) {\n        Map<String, Map<String, Double>> graph = new HashMap<>();\n        for (int i = 0; i < equations.size(); ++i) {\n            String u = equations.get(i).get(0);\n            String v = equations.get(i).get(1);\n            double value = values[i];\n            \n            // 初始化变量u和v的邻接表\n            graph.putIfAbsent(u, new HashMap<>());\n            graph.putIfAbsent(v, new HashMap<>());\n            // 添加边u->v和v->u，权重分别为value和1/value\n            graph.get(u).put(v, value);\n            graph.get(v).put(u, 1 / value); // 反向边，用于从v到u的查询\n        }\n        return graph;\n    }\n\n    /**\n     * 使用BFS算法在图中寻找从start到end的路径比值。\n     * \n     * @param start 起始变量。\n     * @param end 目标变量。\n     * @param graph 变量之间的图。\n     * @return 从start到end的路径比值，如果不存在路径则返回-1.0。\n     */\n    private double bfs(String start, String end, Map<String, Map<String, Double>> graph) {\n        // 检查起始和目标变量是否在图中\n        if (!graph.containsKey(start) || !graph.containsKey(end)) {\n            return -1.0;\n        }\n        \n        Queue<String> queue = new LinkedList<>();\n        queue.offer(start);\n        Map<String, Boolean> visited = new HashMap<>();\n        visited.put(start, true);\n        Map<String, Double> weight = new HashMap<>();\n        weight.put(start, 1.0);\n        \n        while (!queue.isEmpty()) {\n            String curr = queue.poll();\n            if (curr.equals(end)) {\n                return weight.get(curr);\n            }\n            for (Map.Entry<String, Double> entry : graph.get(curr).entrySet()) {\n                String next = entry.getKey();\n                double nextWeight = entry.getValue() * weight.get(curr);\n                // 如果next未被访问过，则加入队列并更新权重\n                if (!visited.containsKey(next)) {\n                    queue.offer(next);\n                    visited.put(next, true);\n                    weight.put(next, nextWeight);\n                }\n            }\n        }\n        \n        return -1.0;\n    }\n}\n```\n\n这段代码首先通过buildGraph方法根据给定的等式和值构建图，然后对每个查询调用bfs方法进行广度优先搜索，寻找从源节点到目标节点的路径并计算结果。如果找不到路径，则返回-1.0。\n\n## 207. 课程表\nhttps://leetcode.cn/problems/course-schedule/description/?envType=study-plan-v2&envId=top-interview-150\n\n你这个学期必须选修 numCourses 门课程，记为 0 到 numCourses - 1 。\n\n在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出，其中 prerequisites[i] = [ai, bi] ，表示如果要学习课程 ai 则 必须 先学习课程  bi 。\n\n例如，先修课程对 [0, 1] 表示：想要学习课程 0 ，你需要先完成课程 1 。\n请你判断是否可能完成所有课程的学习？如果可以，返回 true ；否则，返回 false 。\n\n \n\n示例 1：\n\n输入：numCourses = 2, prerequisites = [[1,0]]\n\n输出：true\n\n解释：总共有 2 门课程。学习课程 1 之前，你需要完成课程 0 。这是可能的。\n\n示例 2：\n\n输入：numCourses = 2, prerequisites = [[1,0],[0,1]]\n\n输出：false\n\n解释：总共有 2 门课程。学习课程 1 之前，你需要先完成​课程 0 ；并且学习课程 0 之前，你还应先完成课程 1 。这是不可能的。\n \n\n提示：\n\n- 1 <= numCourses <= 2000\n- 0 <= prerequisites.length <= 5000\n- prerequisites[i].length == 2\n- 0 <= ai, bi < numCourses\n- prerequisites[i] 中的所有课程对 互不相同\n\n```java\nimport java.util.*;\n\nclass Solution {\n    /**\n     * 判断是否能完成所有课程学习，根据先修课程关系。\n     * \n     * @param numCourses 总课程数。\n     * @param prerequisites 先修课程对列表。\n     * @return 是否能完成所有课程学习。\n     */\n    public boolean canFinish(int numCourses, int[][] prerequisites) {\n        // 构建邻接表表示课程的先修关系\n        Map<Integer, List<Integer>> adj = new HashMap<>();\n        for (int[] pair : prerequisites) {\n            adj.computeIfAbsent(pair[1], k -> new ArrayList<>()).add(pair[0]);\n        }\n        \n        // 记录每个课程的状态：0-未搜索，1-搜索中，2-已搜索完成\n        int[] state = new int[numCourses];\n        \n        // 遍历所有课程，执行深度优先搜索检测环\n        for (int i = 0; i < numCourses; ++i) {\n            if (!dfs(i, adj, state)) {\n                return false;\n            }\n        }\n        \n        return true;\n    }\n\n    /**\n     * 深度优先搜索检测课程是否有环。\n     * \n     * @param course 当前课程索引。\n     * @param adj 邻接表，表示课程的先修关系。\n     * @param state 各课程的状态记录。\n     * @return 是否有环。\n     */\n    private boolean dfs(int course, Map<Integer, List<Integer>> adj, int[] state) {\n        // 如果当前课程正在搜索中，说明有环\n        if (state[course] == 1) {\n            return false;\n        }\n        // 如果已经搜索完成，直接返回true\n        if (state[course] == 2) {\n            return true;\n        }\n        \n        // 标记课程为搜索中\n        state[course] = 1;\n        // 遍历所有后续课程\n        if (adj.containsKey(course)) {\n            for (int nextCourse : adj.get(course)) {\n                // 如果后续课程 DFS 后发现有环，返回false\n                if (!dfs(nextCourse, adj, state)) {\n                    return false;\n                }\n            }\n        }\n        \n        // 课程及其后续课程均无环，标记为已搜索完成\n        state[course] = 2;\n        return true;\n    }\n}\n```\n\n## 210. 课程表 II\nhttps://leetcode.cn/problems/course-schedule-ii/description/?envType=study-plan-v2&envId=top-interview-150\n\n现在你总共有 numCourses 门课需要选，记为 0 到 numCourses - 1。给你一个数组 prerequisites ，其中 prerequisites[i] = [ai, bi] ，表示在选修课程 ai 前 必须 先选修 bi 。\n\n例如，想要学习课程 0 ，你需要先完成课程 1 ，我们用一个匹配来表示：[0,1] 。\n返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序，你只要返回 任意一种 就可以了。如果不可能完成所有课程，返回 一个空数组 。\n\n \n\n示例 1：\n\n输入：numCourses = 2, prerequisites = [[1,0]]\n\n输出：[0,1]\n\n解释：总共有 2 门课程。要学习课程 1，你需要先完成课程 0。因此，正确的课程顺序为 [0,1] 。\n\n示例 2：\n\n输入：numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]\n\n输出：[0,2,1,3]\n\n解释：总共有 4 门课程。要学习课程 3，你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。\n\n因此，一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。\n\n示例 3：\n\n输入：numCourses = 1, prerequisites = []\n\n输出：[0]\n \n\n提示：\n- 1 <= numCourses <= 2000\n- 0 <= prerequisites.length <= numCourses * (numCourses - 1)\n- prerequisites[i].length == 2\n- 0 <= ai, bi < numCourses\n- ai != bi\n- 所有[ai, bi] 互不相同\n\n```java\nimport java.util.*;\n\n/**\n * 解决课程安排问题的类。\n * 通过拓扑排序找出可以按照一定顺序学习的课程。\n */\nclass Solution {\n    /**\n     * 寻找一个可能的课程学习顺序。\n     * \n     * @param numCourses 课程总数。\n     * @param prerequisites 课程的先决条件数组，每个元素是一个数组，其中包含两个课程编号，表示第二个课程是第一个课程的先决条件。\n     * @return 如果存在一个学习顺序，则返回一个包含所有课程编号的数组；如果不存在这样的顺序，则返回一个空数组。\n     */\n    public int[] findOrder(int numCourses, int[][] prerequisites) {\n        // 初始化邻接表，用于表示课程之间的依赖关系\n        List<List<Integer>> adj = new ArrayList<>();\n        for (int i = 0; i < numCourses; i++) {\n            adj.add(new ArrayList<>());\n        }\n        \n        // 初始化入度数组，用于记录每门课程的入度\n        int[] inDegree = new int[numCourses];\n        \n        // 根据先决条件构建邻接表和入度数组\n        // 建立邻接表和入度统计\n        for (int[] pair : prerequisites) {\n            adj.get(pair[1]).add(pair[0]);\n            inDegree[pair[0]]++;\n        }\n        \n        // 初始化队列，用于存储入度为0的课程\n        Queue<Integer> queue = new LinkedList<>();\n        for (int i = 0; i < numCourses; i++) {\n            if (inDegree[i] == 0) {\n                queue.offer(i);\n            }\n        }\n        \n        // 初始化结果数组，用于存放拓扑排序的结果\n        int[] order = new int[numCourses];\n        int index = 0;\n        \n        // 使用宽度优先搜索进行拓扑排序\n        while (!queue.isEmpty()) {\n            int currCourse = queue.poll();\n            order[index++] = currCourse;\n            \n            for (int nextCourse : adj.get(currCourse)) {\n                inDegree[nextCourse]--;\n                if (inDegree[nextCourse] == 0) {\n                    queue.offer(nextCourse);\n                }\n            }\n        }\n        \n        // 检查是否所有课程都已被安排\n        // 检查是否存在环，即是否所有课程都被安排\n        if (index == numCourses) {\n            return order;\n        } else {\n            return new int[0];\n        }\n    }\n}\n```\n\n# 图的广度优先搜索\n\n## 909. 蛇梯棋\nhttps://leetcode.cn/problems/snakes-and-ladders/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个大小为 n x n 的整数矩阵 board ，方格按从 1 到 n2 编号，编号遵循 转行交替方式 ，从左下角开始 （即，从 board[n - 1][0] 开始）每一行交替方向。\n\n玩家从棋盘上的方格 1 （总是在最后一行、第一列）开始出发。\n\n每一回合，玩家需要从当前方格 curr 开始出发，按下述要求前进：\n\n\n- 选定目标方格 next ，目标方格的编号符合范围 [curr + 1, min(curr + 6, n2)] 。\n\n    - 该选择模拟了掷 六面体骰子 的情景，无论棋盘大小如何，玩家最多只能有 6 个目的地。\n\n- 传送玩家：如果目标方格 next 处存在蛇或梯子，那么玩家会传送到蛇或梯子的目的地。否则，玩家传送到目标方格 next 。 \n\n- 当玩家到达编号 n2 的方格时，游戏结束。\n\nr 行 c 列的棋盘，按前述方法编号，棋盘格中可能存在 “蛇” 或 “梯子”；如果 board[r][c] != -1，那个蛇或梯子的目的地将会是 board[r][c]。编号为 1 和 n2 的方格上没有蛇或梯子。\n\n注意，玩家在每回合的前进过程中最多只能爬过蛇或梯子一次：就算目的地是另一条蛇或梯子的起点，玩家也 不能 继续移动。\n\n- 举个例子，假设棋盘是 [[-1,4],[-1,3]] ，第一次移动，玩家的目标方格是 2 。那么这个玩家将会顺着梯子到达方格 3 ，但 不能 顺着方格 3 上的梯子前往方格 4 。\n\n返回达到编号为 n2 的方格所需的最少移动次数，如果不可能，则返回 -1。\n\n \n\n示例 1：\n\n![alt text](../img/数据结构和算法/蛇梯棋.png)\n\n输入：board = [[-1,-1,-1,-1,-1,-1],[-1,-1,-1,-1,-1,-1],[-1,-1,-1,-1,-1,-1],[-1,35,-1,-1,13,-1],[-1,-1,-1,-1,-1,-1],[-1,15,-1,-1,-1,-1]]\n\n输出：4\n\n解释：\n\n首先，从方格 1 [第 5 行，第 0 列] 开始。 \n\n先决定移动到方格 2 ，并必须爬过梯子移动到到方格 15 。\n\n然后决定移动到方格 17 [第 3 行，第 4 列]，必须爬过蛇到方格 13 。\n\n接着决定移动到方格 14 ，且必须通过梯子移动到方格 35 。 \n\n最后决定移动到方格 36 , 游戏结束。 \n\n可以证明需要至少 4 次移动才能到达最后一个方格，所以答案是 4 。 \n\n示例 2：\n\n输入：board = [[-1,-1],[-1,3]]\n\n输出：1\n \n\n提示：\n\n- n == board.length == board[i].length\n- 2 <= n <= 20\n- grid[i][j] 的值是 -1 或在范围 [1, n2] 内\n- 编号为 1 和 n2 的方格上没有蛇或梯子\n\n```java\n/**\n * 解决方案类，提供解决蛇梯棋游戏的函数。\n */\nclass Solution {\n    /**\n     * 计算玩蛇梯棋游戏所需的最小移动次数。\n     * \n     * @param board 蛇梯棋盘的二维数组表示，-1 表示普通格子，其他正数表示可以跳转的格子。\n     * @return 返回到达终点的最小移动次数，如果无法到达终点则返回 -1。\n     */\n    public int snakesAndLadders(int[][] board) {\n        // 棋盘的边长\n        int n = board.length;\n        // 棋盘上所有格子的数量，加上起点和终点\n        int m = n * n + 1;\n        // 将二维棋盘转换为一维数组，方便处理\n        int[] boardArr = new int[m];\n        // 用于标记在遍历棋盘时的行方向\n        boolean flag = true;\n        // 将二维棋盘转换为一维数组\n        int idx = 0;\n        for (int i = n - 1; i >= 0; i--) {\n            if (flag) {\n                for (int j = 0; j < n; j++) {\n                    boardArr[++idx] = board[i][j];\n                }\n            } else {\n                for (int j = n - 1; j >= 0; j--) {\n                    boardArr[++idx] = board[i][j];\n                }\n            }\n            flag = !flag;\n        }\n        // 动态规划数组，dp[i] 表示到达棋盘上第 i 个格子所需的最小移动次数\n        // 定义数组，dp[i]表示到达i点时，所耗费的最小移动次数。\n        int[] dp = new int[m];\n        // 使用双端队列来进行广度优先搜索\n        Deque<Integer> deque = new LinkedList<>();\n        // 从起点开始\n        deque.addLast(1);\n        // 遍历棋盘，直到队列为空\n        while (!deque.isEmpty()) {\n            // 取出队列头部的格子\n            Integer node = deque.removeFirst();\n            // 如果到达终点，则返回最小移动次数\n            if (node == m - 1)\n                return dp[m - 1];\n            // 尝试从当前格子移动到周围的六个格子\n            for (int i = 1; i <= 6 && i + node < m; i++) {\n                int newIdx;\n                // 如果当前格子有蛇或梯子，则直接跳转\n                if (boardArr[node + i] != -1) {\n                    newIdx = boardArr[node + i];\n                } else {\n                    newIdx = node + i;\n                }\n                // 如果新格子已经访问过或者无法到达，则跳过\n                // 如果曾经访问过这个节点了。\n                if (dp[newIdx] != 0 || newIdx == node) {\n                    continue;\n                }\n                // 更新到达新格子的最小移动次数\n                // 由于是BFS。因此一定是最小的步数。\n                // dp[newIdx] = Math.min(dp[newIdx], dp[node] + 1);\n                dp[newIdx] = dp[node] + 1;\n                // 将新格子加入队列，继续搜索\n                deque.addLast(newIdx);\n            }\n        }\n        // 如果无法到达终点，则返回 -1\n        if (dp[m - 1] == 0)\n            return -1;\n        // 返回到达终点的最小移动次数\n        return dp[m - 1];\n    }\n}\n```\n\n## 433. 最小基因变化\nhttps://leetcode.cn/problems/minimum-genetic-mutation/description/?envType=study-plan-v2&envId=top-interview-150\n\n基因序列可以表示为一条由 8 个字符组成的字符串，其中每个字符都是 'A'、'C'、'G' 和 'T' 之一。\n\n假设我们需要调查从基因序列 start 变为 end 所发生的基因变化。一次基因变化就意味着这个基因序列中的一个字符发生了变化。\n\n例如，\"AACCGGTT\" --> \"AACCGGTA\" 就是一次基因变化。\n另有一个基因库 bank 记录了所有有效的基因变化，只有基因库中的基因才是有效的基因序列。（变化后的基因必须位于基因库 bank 中）\n\n给你两个基因序列 start 和 end ，以及一个基因库 bank ，请你找出并返回能够使 start 变化为 end 所需的最少变化次数。如果无法完成此基因变化，返回 -1 。\n\n注意：起始基因序列 start 默认是有效的，但是它并不一定会出现在基因库中。\n\n \n\n示例 1：\n\n输入：start = \"AACCGGTT\", end = \"AACCGGTA\", bank = [\"AACCGGTA\"]\n\n输出：1\n\n示例 2：\n\n输入：start = \"AACCGGTT\", end = \"AAACGGTA\", bank = [\"AACCGGTA\",\"AACCGCTA\",\"AAACGGTA\"]\n\n输出：2\n\n示例 3：\n\n输入：start = \"AAAAACCC\", end = \"AACCCCCC\", bank = [\"AAAACCCC\",\"AAACCCCC\",\"AACCCCCC\"]\n\n输出：3\n \n\n提示：\n\n- start.length == 8\n- end.length == 8\n- 0 <= bank.length <= 10\n- bank[i].length == 8\n- start、end 和 bank[i] 仅由字符 ['A', 'C', 'G', 'T'] 组成\n\n```java\nimport java.util.*;\n\nclass Solution {\n    public int minMutation(String start, String end, String[] bank) {\n        Set<String> dict = new HashSet<>(Arrays.asList(bank));\n        if (!dict.contains(end)) return -1;\n        \n        char[] genes = {'A', 'C', 'G', 'T'};\n        Queue<Pair<String, Integer>> queue = new LinkedList<>();\n        queue.offer(new Pair<>(start, 0));\n        \n        while (!queue.isEmpty()) {\n            Pair<String, Integer> pair = queue.poll();\n            String curStr = pair.getKey();\n            int step = pair.getValue();\n            \n            if (curStr.equals(end)) return step;\n            \n            for (int i = 0; i < curStr.length(); i++) {\n                char[] chars = curStr.toCharArray();\n                for (char gene : genes) {\n                    chars[i] = gene;\n                    String nextStr = new String(chars);\n                    \n                    if (dict.contains(nextStr)) {\n                        queue.offer(new Pair<>(nextStr, step + 1));\n                        dict.remove(nextStr); // 防止重复访问\n                    }\n                }\n            }\n        }\n        return -1;\n    }\n}\n\n// 辅助类，用于存储基因序列及其对应步数\nclass Pair<K, V> {\n    private K key;\n    private V value;\n\n    public Pair(K key, V value) {\n        this.key = key;\n        this.value = value;\n    }\n\n    public K getKey() {\n        return key;\n    }\n\n    public V getValue() {\n        return value;\n    }\n}\n```\n\n# 字典树\n## 208. 实现 Trie (前缀树)\nhttps://leetcode.cn/problems/implement-trie-prefix-tree/description/?envType=study-plan-v2&envId=top-interview-150\n\nTrie（发音类似 \"try\"）或者说 前缀树 是一种树形数据结构，用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景，例如自动补完和拼写检查。\n\n请你实现 Trie 类：\n\n- Trie() 初始化前缀树对象。\n- void insert(String word) 向前缀树中插入字符串 word 。\n- boolean search(String word) 如果字符串 word 在前缀树中，返回 true（即，在检索之前已经插入）；否则，返回 - false 。\n- boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ，返回 true ；否则，返回 false 。\n \n\n示例：\n\n输入\n[\"Trie\", \"insert\", \"search\", \"search\", \"startsWith\", \"insert\", \"search\"]\n\n[[], [\"apple\"], [\"apple\"], [\"app\"], [\"app\"], [\"app\"], [\"app\"]]\n\n输出\n\n[null, null, true, false, true, null, true]\n\n解释\n- Trie trie = new Trie();\n- trie.insert(\"apple\");\n- trie.search(\"apple\");   // 返回 True\n- trie.search(\"app\");     // 返回 False\n- trie.startsWith(\"app\"); // 返回 True\n- trie.insert(\"app\");\n- trie.search(\"app\");     // 返回 True\n \n\n提示：\n\n- 1 <= word.length, prefix.length <= 2000\n- word 和 prefix 仅由小写英文字母组成\n- insert、search 和 startsWith 调用次数 总计 不超过 3 * 10^4 次\n\n```java\n/**\n * Trie树的节点类。\n * 用于存储字符串的前缀树结构，每个节点包含26个子节点，代表26个英文字母。\n * isEndOfWord标志位用于标记当前节点是否为一个单词的结束。\n */\nclass TrieNode {\n    TrieNode[] children = new TrieNode[26];\n    boolean isEndOfWord;\n\n    /**\n     * 节点的构造函数。\n     * 初始化isEndOfWord为false，并将所有子节点设置为null。\n     */\n    TrieNode() {\n        isEndOfWord = false;\n        for (int i = 0; i < 26; i++) {\n            children[i] = null;\n        }\n    }\n}\n\n/**\n * Trie树（前缀树）类。\n * 提供插入、搜索和判断前缀是否存在等操作。\n */\nclass Trie {\n    private TrieNode root;\n\n    /**\n     * Trie树的构造函数。\n     * 初始化前缀树的根节点。\n     */\n    public Trie() {\n        root = new TrieNode();\n    }\n\n    /**\n     * 插入一个单词到Trie树中。\n     * \n     * @param word 要插入的单词。\n     */\n    public void insert(String word) {\n        TrieNode node = root;\n        for (char c : word.toCharArray()) {\n            int index = c - 'a';\n            if (node.children[index] == null) {\n                node.children[index] = new TrieNode();\n            }\n            node = node.children[index];\n        }\n        node.isEndOfWord = true;\n    }\n\n    /**\n     * 在Trie树中搜索一个单词。\n     * \n     * @param word 要搜索的单词。\n     * @return 如果单词存在，则返回true；否则返回false。\n     */\n    public boolean search(String word) {\n        TrieNode node = root;\n        for (char c : word.toCharArray()) {\n            int index = c - 'a';\n            if (node.children[index] == null) {\n                return false;\n            }\n            node = node.children[index];\n        }\n        return node.isEndOfWord;\n    }\n\n    /**\n     * 判断Trie树中是否包含指定的前缀。\n     * \n     * @param prefix 要判断的前缀。\n     * @return 如果前缀存在，则返回true；否则返回false。\n     */\n    public boolean startsWith(String prefix) {\n        TrieNode node = root;\n        for (char c : prefix.toCharArray()) {\n            int index = c - 'a';\n            if (node.children[index] == null) {\n                return false;\n            }\n            node = node.children[index];\n        }\n        return true;\n    }\n}\n```\n\n## 211. 添加与搜索单词 - 数据结构设计\nhttps://leetcode.cn/problems/design-add-and-search-words-data-structure/description/?envType=study-plan-v2&envId=top-interview-150\n\n请你设计一个数据结构，支持 添加新单词 和 查找字符串是否与任何先前添加的字符串匹配 。\n\n实现词典类 WordDictionary ：\n\n- WordDictionary() 初始化词典对象\n- void addWord(word) 将 word 添加到数据结构中，之后可以对它进行匹配\n- bool search(word) 如果数据结构中存在字符串与 word 匹配，则返回 true ；否则，返回  false 。word 中可能包含一些 '.' ，每个 . 都可以表示任何一个字母。\n \n\n示例：\n\n输入：\n\n[\"WordDictionary\",\"addWord\",\"addWord\",\"addWord\",\"search\",\"search\",\"search\",\"search\"]\n\n[[],[\"bad\"],[\"dad\"],[\"mad\"],[\"pad\"],[\"bad\"],[\".ad\"],[\"b..\"]]\n\n输出：\n\n[null,null,null,null,false,true,true,true]\n\n解释：\n- WordDictionary wordDictionary = new WordDictionary();\n- wordDictionary.addWord(\"bad\");\n- wordDictionary.addWord(\"dad\");\n- wordDictionary.addWord(\"mad\");\n- wordDictionary.search(\"pad\"); // 返回 False\n- wordDictionary.search(\"bad\"); // 返回 True\n- wordDictionary.search(\".ad\"); // 返回 True\n- wordDictionary.search(\"b..\"); // 返回 True\n \n\n提示：\n\n- 1 <= word.length <= 25\n- addWord 中的 word 由小写英文字母组成\n- search 中的 word 由 '.' 或小写英文字母组成\n- 最多调用 10^4 次 addWord 和 search\n\n```java\n/**\n * 字典类，用于存储和查找单词。\n */\nclass WordDictionary {\n    /**\n     * Trie树的根节点。\n     */\n    private TrieNode root;\n\n    /**\n     * 构造函数，初始化字典。\n     */\n    public WordDictionary() {\n        root = new TrieNode();\n    }\n\n    /**\n     * 向字典中添加一个单词。\n     * \n     * @param word 要添加的单词。\n     */\n    public void addWord(String word) {\n        TrieNode node = root;\n        for (char c : word.toCharArray()) {\n            int index = c - 'a';\n            if (node.children[index] == null) {\n                node.children[index] = new TrieNode();\n            }\n            node = node.children[index];\n        }\n        node.isEndOfWord = true;\n    }\n\n    /**\n     * 在字典中搜索一个单词是否存在。\n     * \n     * @param word 要搜索的单词。\n     * @return 如果单词存在则返回true，否则返回false。\n     */\n    public boolean search(String word) {\n        return searchHelper(root, word, 0);\n    }\n\n    /**\n     * 辅助搜索函数，用于递归搜索Trie树。\n     * \n     * @param node 当前搜索的Trie节点。\n     * @param word 要搜索的单词。\n     * @param depth 当前搜索字的深度。\n     * @return 如果单词存在则返回true，否则返回false。\n     */\n    private boolean searchHelper(TrieNode node, String word, int depth) {\n        if (depth == word.length()) {\n            return node.isEndOfWord;\n        }\n        char c = word.charAt(depth);\n        if (c != '.') {\n            int index = c - 'a';\n            if (node.children[index] != null && searchHelper(node.children[index], word, depth + 1)) {\n                return true;\n            }\n        } else {\n            for (TrieNode child : node.children) {\n                if (child != null && searchHelper(child, word, depth + 1)) {\n                    return true;\n                }\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Trie节点类，用于构建Trie树。\n     */\n    private static class TrieNode {\n        /**\n         * 子节点数组，用于存储字母'a'到'z'的节点。\n         */\n        TrieNode[] children = new TrieNode[26];\n        /**\n         * 标记当前节点是否为一个单词的结尾。\n         */\n        boolean isEndOfWord;\n\n        /**\n         * 构造函数，初始化Trie节点。\n         */\n        TrieNode() {\n            isEndOfWord = false;\n            for (int i = 0; i < 26; i++) {\n                children[i] = null;\n            }\n        }\n    }\n}\n```\n\n# 回溯\n## 17. 电话号码的字母组合\nhttps://leetcode.cn/problems/letter-combinations-of-a-phone-number/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个仅包含数字 2-9 的字符串，返回所有它能表示的字母组合。答案可以按 任意顺序 返回。\n\n给出数字到字母的映射如下（与电话按键相同）。注意 1 不对应任何字母。\n\n\n![alt text](../img/数据结构和算法/电话号码的组合.png)\n \n\n示例 1：\n\n输入：digits = \"23\"\n\n输出：[\"ad\",\"ae\",\"af\",\"bd\",\"be\",\"bf\",\"cd\",\"ce\",\"cf\"]\n\n示例 2：\n\n输入：digits = \"\"\n\n输出：[]\n\n示例 3：\n\n输入：digits = \"2\"\n\n输出：[\"a\",\"b\",\"c\"]\n \n\n提示：\n\n- 0 <= digits.length <= 4\n- digits[i] 是范围 ['2', '9'] 的一个数字。\n\n```java\nimport java.util.*;\n\nclass Solution {\n    // 定义一个映射，将数字字符映射到其对应的字母集合\n    private static final String[] KEYS = {\"\", \"\", \"abc\", \"def\", \"ghi\", \"jkl\", \"mno\", \"pqrs\", \"tuv\", \"wxyz\"};\n\n    /**\n     * 回溯算法生成所有可能的字母组合\n     * @param digits 输入的数字字符串\n     * @return 所有可能的字母组合列表\n     */\n    public List<String> letterCombinations(String digits) {\n        List<String> combinations = new ArrayList<>();\n        if (digits == null || digits.length() == 0) {\n            return combinations;\n        }\n        \n        backtrack(combinations, digits, new StringBuilder(), 0);\n        return combinations;\n    }\n\n    /**\n     * 回溯过程中的递归函数\n     * @param combinations 结果列表，用于保存所有组合\n     * @param digits 输入的数字字符串\n     * @param sb 当前组合的StringBuilder对象\n     * @param index 当前处理的数字字符在digits中的索引\n     */\n    private void backtrack(List<String> combinations, String digits, StringBuilder sb, int index) {\n        // 基准情况：如果已经处理完digits中的所有字符，则将当前组合加入结果列表\n        if (index == digits.length()) {\n            combinations.add(sb.toString());\n            return;\n        }\n        \n        // 获取当前数字对应的字母集合\n        String letters = KEYS[digits.charAt(index) - '0'];\n        \n        // 遍历当前数字对应的每一个字母\n        for (char letter : letters.toCharArray()) {\n            // 添加当前字母到组合中，并递归处理下一个数字字符\n            sb.append(letter);\n            backtrack(combinations, digits, sb, index + 1);\n            \n            // 回溯：移除刚刚添加的字母，尝试下一个字母\n            sb.deleteCharAt(sb.length() - 1);\n        }\n    }\n}\n```\n\n## 77. 组合\nhttps://leetcode.cn/problems/combinations/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定两个整数 n 和 k，返回范围 [1, n] 中所有可能的 k 个数的组合。\n\n你可以按 任何顺序 返回答案。\n\n \n\n示例 1：\n\n输入：n = 4, k = 2\n\n输出：\n\n[\n\n  [2,4],\n\n  [3,4],\n\n  [2,3],\n\n  [1,2],\n\n  [1,3],\n\n  [1,4],\n\n]\n\n示例 2：\n\n输入：n = 1, k = 1\n\n输出：[[1]]\n \n\n提示：\n\n- 1 <= n <= 20\n- 1 <= k <= n\n\n```java\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class Solution {\n    public List<List<Integer>> combine(int n, int k) {\n        // 定义结果列表，用来存储所有可能的组合\n        List<List<Integer>> results = new ArrayList<>();\n\n        // 定义回溯函数，用于生成组合\n        backtrack(results, new ArrayList<>(), n, k, 1);\n\n        return results;\n    }\n\n    private void backtrack(List<List<Integer>> results, List<Integer> combination, int n, int k, int start) {\n        // 基准情况：如果组合的长度等于k，则将当前组合加入结果列表\n        if (combination.size() == k) {\n            results.add(new ArrayList<>(combination));\n            return;\n        }\n\n        // 从start开始遍历，避免重复添加相同的元素\n        for (int i = start; i <= n; i++) {\n            // 将当前元素添加到组合中\n            combination.add(i);\n            // 递归生成下一个元素的组合，i+1表示下一个待选元素应从i+1开始\n            backtrack(results, combination, n, k, i + 1);\n            // 移除最后一个元素，回溯尝试下一个可能的元素\n            combination.remove(combination.size() - 1);\n        }\n    }\n}\n```\n\n## 46. 全排列\nhttps://leetcode.cn/problems/permutations/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个不含重复数字的数组 nums ，返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。\n\n \n\n示例 1：\n\n输入：nums = [1,2,3]\n\n输出：[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]\n\n示例 2：\n\n输入：nums = [0,1]\n\n输出：[[0,1],[1,0]]\n\n示例 3：\n\n输入：nums = [1]\n\n输出：[[1]]\n \n\n提示：\n\n- 1 <= nums.length <= 6\n- -10 <= nums[i] <= 10\n- nums 中的所有整数 互不相同\n\n```java\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class Solution {\n    public List<List<Integer>> permute(int[] nums) {\n        List<List<Integer>> results = new ArrayList<>();\n        backtrack(results, new ArrayList<>(), nums);\n        return results;\n    }\n\n    private void backtrack(List<List<Integer>> results, List<Integer> currentPermutation, int[] nums) {\n        // 基准情况：当当前排列的大小等于nums的长度时，将其添加到结果列表\n        if (currentPermutation.size() == nums.length) {\n            results.add(new ArrayList<>(currentPermutation));\n            return;\n        }\n\n        // 遍历nums中的每个元素\n        for (int num : nums) {\n            // 如果当前元素还没有被使用过（即不在currentPermutation中）\n            if (!currentPermutation.contains(num)) {\n                // 将当前元素添加到排列中\n                currentPermutation.add(num);\n                // 递归生成剩余元素的排列\n                backtrack(results, currentPermutation, nums);\n                // 回溯：移除最后一个添加的元素，尝试下一个可能的元素\n                currentPermutation.remove(currentPermutation.size() - 1);\n            }\n        }\n    }\n}\n```\n\n## 39. 组合总和\nhttps://leetcode.cn/problems/combination-sum/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ，找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ，并以列表形式返回。你可以按 任意顺序 返回这些组合。\n\ncandidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同，则两种组合是不同的。 \n\n对于给定的输入，保证和为 target 的不同组合数少于 150 个。\n\n \n\n示例 1：\n\n输入：candidates = [2,3,6,7], target = 7\n\n输出：[[2,2,3],[7]]\n\n解释：\n\n2 和 3 可以形成一组候选，2 + 2 + 3 = 7 。注意 2 可以使用多次。\n\n7 也是一个候选， 7 = 7 。\n\n仅有这两种组合。\n\n示例 2：\n\n输入: candidates = [2,3,5], target = 8\n\n输出: [[2,2,2,2],[2,3,3],[3,5]]\n\n示例 3：\n\n输入: candidates = [2], target = 1\n\n输出: []\n \n\n提示：\n\n- 1 <= candidates.length <= 30\n- 2 <= candidates[i] <= 40\n- candidates 的所有元素 互不相同\n- 1 <= target <= 40\n\n```java\nclass Solution {\n    // 结果集合，用于存储所有满足条件的组合\n    List<List<Integer>> res = new ArrayList<>();\n    // 辅助栈，用于记录当前搜索路径上的元素\n    Deque<Integer> list = new LinkedList<>();\n\n    /**\n     * 计算组合总和\n     * @param candidates 无重复元素的整数数组候选人\n     * @param target 目标整数和\n     * @return 所有可能的组合列表\n     */\n    public List<List<Integer>> combinationSum(int[] candidates, int target) {\n        // 获取候选人数组的长度\n        int len = candidates.length;\n        // 如果数组长度小于0，直接返回空结果（实际上此条件不会触发，仅为逻辑完整性考虑）\n        if (len < 0) {\n            return res;\n        }\n        // 对候选人数组进行排序，便于剪枝操作\n        Arrays.sort(candidates);\n        // 从第一个元素开始深度优先搜索\n        dfs(candidates, target, 0);\n        // 返回所有满足条件的组合\n        return res;\n    }\n\n    /**\n     * 深度优先搜索实现函数\n     * @param candidates 候选人数组\n     * @param target 剩余需要达到的目标和\n     * @param index 当前搜索的起始下标，避免重复使用同一层级的元素\n     */\n    public void dfs(int[] candidates, int target, int index) {\n        // 如果目标和为0，说明找到了一个合法组合\n        if (target == 0) {\n            // 将当前组合复制并添加到结果列表中\n            res.add(new ArrayList<>(list));\n            return;\n        }\n        // 遍历数组，从index开始搜索，允许重复使用元素但同一层级不重复选择\n        for (int i = index; i < candidates.length; i++) {\n            // 如果目标减去当前元素值小于0，说明此路不通，直接结束本次循环\n            if (target - candidates[i] < 0) {\n                break;\n            }\n            // 选择当前元素，将其添加到路径中\n            list.addLast(candidates[i]);\n            // 递归搜索剩余部分，由于可以重复使用元素，下一轮搜索仍然从i开始\n            dfs(candidates, target - candidates[i], i);\n            // 回溯，移除刚加入的元素，尝试下一个选择\n            list.removeLast();\n        }\n    }\n}\n```\n\n## 52. N 皇后 II\nhttps://leetcode.cn/problems/n-queens-ii/description/?envType=study-plan-v2&envId=top-interview-150\n\nn 皇后问题 研究的是如何将 n 个皇后放置在 n × n 的棋盘上，并且使皇后彼此之间不能相互攻击。\n\n给你一个整数 n ，返回 n 皇后问题 不同的解决方案的数量。\n\n \n\n示例 1：\n\n![alt text](../img/数据结构和算法/n皇后2.png)\n\n输入：n = 4\n\n输出：2\n\n解释：如上图所示，4 皇后问题存在两个不同的解法。\n\n示例 2：\n\n输入：n = 1\n\n输出：1\n \n\n提示：\n\n- 1 <= n <= 9\n\n```java\npublic class Solution {\n    private int count = 0; // 用于记录符合条件的解的数量\n\n    public int totalNQueens(int n) {\n        if (n <= 0) {\n            return count;\n        }\n        int[] queens = new int[n]; // 用于记录每行皇后的列位置，初始化全为0\n        placeQueens(queens, 0, n);\n        return count;\n    }\n\n    private void placeQueens(int[] queens, int row, int n) {\n        if (row == n) { // 已经在n行都放置了皇后，说明找到了一个解\n            count++;\n            return;\n        }\n        for (int col = 0; col < n; col++) {\n            if (isValid(queens, row, col)) { // 检查当前位置是否可以放置皇后\n                queens[row] = col; // 放置皇后\n                placeQueens(queens, row + 1, n); // 在下一行尝试放置皇后\n            }\n        }\n    }\n\n    private boolean isValid(int[] queens, int row, int col) {\n        // 检查列是否有冲突\n        for (int i = 0; i < row; i++) {\n            if (queens[i] == col) {\n                return false;\n            }\n        }\n        // 检查左对角线是否有冲突\n        for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {\n            if (queens[i] == j) {\n                return false;\n            }\n        }\n        // 检查右对角线是否有冲突\n        for (int i = row - 1, j = col + 1; i >= 0 && j < queens.length; i--, j++) {\n            if (queens[i] == j) {\n                return false;\n            }\n        }\n        return true;\n    }\n}\n```\n\n## 22. 括号生成\nhttps://leetcode.cn/problems/generate-parentheses/description/?envType=study-plan-v2&envId=top-interview-150\n\n数字 n 代表生成括号的对数，请你设计一个函数，用于能够生成所有可能的并且 有效的 括号组合。\n\n \n\n示例 1：\n\n输入：n = 3\n\n输出：[\"((()))\",\"(()())\",\"(())()\",\"()(())\",\"()()()\"]\n\n示例 2：\n\n输入：n = 1\n\n输出：[\"()\"]\n \n\n提示：\n\n- 1 <= n <= 8\n\n```java\nclass Solution {\n    public List<String> generateParenthesis(int n) {\n        List<String> result = new ArrayList<>();\n        generateCombinations(result, \"\", n, n);\n        return result;\n    }\n\n    private void generateCombinations(List<String> result, String current, int left, int right) {\n        // 基本情况：如果左右括号都用完了，将当前组合添加到结果列表中\n        if (left == 0 && right == 0) {\n            result.add(current);\n            return;\n        }\n        \n        // 如果还有左括号可用，可以放一个左括号\n        if (left > 0) {\n            generateCombinations(result, current + \"(\", left - 1, right);\n        }\n        \n        // 只有在右括号比左括号多的情况下，才能放右括号，保证生成的括号序列是合法的\n        if (right > left) {\n            generateCombinations(result, current + \")\", left, right - 1);\n        }\n    }\n}\n```\n\n## 79. 单词搜索\nhttps://leetcode.cn/problems/word-search/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中，返回 true ；否则，返回 false 。\n\n单词必须按照字母顺序，通过相邻的单元格内的字母构成，其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。\n\n \n\n示例 1：\n\n![alt text](../img/数据结构和算法/单词搜索1.png)\n\n输入：board = [[\"A\",\"B\",\"C\",\"E\"],[\"S\",\"F\",\"C\",\"S\"],[\"A\",\"D\",\"E\",\"E\"]], word = \"ABCCED\"\n\n输出：true\n\n示例 2：\n\n![alt text](../img/数据结构和算法/单词搜索2.png)\n\n输入：board = [[\"A\",\"B\",\"C\",\"E\"],[\"S\",\"F\",\"C\",\"S\"],[\"A\",\"D\",\"E\",\"E\"]], word = \"SEE\"\n\n输出：true\n\n示例 3：\n\n![alt text](../img/数据结构和算法/单词搜索3.png)\n\n输入：board = [[\"A\",\"B\",\"C\",\"E\"],[\"S\",\"F\",\"C\",\"S\"],[\"A\",\"D\",\"E\",\"E\"]], word = \"ABCB\"\n\n输出：false\n \n\n提示：\n\n- m == board.length\n- n = board[i].length\n- 1 <= m, n <= 6\n- 1 <= word.length <= 15\n- board 和 word 仅由大小写英文字母组成\n\n```java\n/**\n * Solution类用于解决检查单词是否可以在给定的二维字符板上找到的问题。\n * 它使用深度优先搜索（DFS）算法来遍历板上的字符，并尝试匹配给定的单词。\n */\nclass Solution {\n    /**\n     * 记录每个位置是否已被访问，以避免重复访问。\n     */\n    private boolean[][] visited;\n    /**\n     * m和n分别表示二维字符板的行数和列数。\n     */\n    private int m, n;\n    /**\n     * board存储二维字符板的内容。\n     */\n    private char[][] board;\n    /**\n     * word是要在字符板上查找的单词。\n     */\n    private String word;\n    \n    /**\n     * 检查单词是否可以在字符板上找到。\n     * \n     * @param board 二维字符板\n     * @param word 要查找的单词\n     * @return 如果单词可以在字符板上找到，则返回true；否则返回false。\n     */\n    public boolean exist(char[][] board, String word) {\n        this.board = board;\n        this.word = word;\n        m = board.length;\n        n = board[0].length;\n        visited = new boolean[m][n];\n        \n        // 遍历字符板上的每个位置，尝试从每个位置开始匹配单词\n        for (int i = 0; i < m; i++) {\n            for (int j = 0; j < n; j++) {\n                if (dfs(i, j, 0)) {\n                    return true;\n                }\n            }\n        }\n        return false;\n    }\n    \n    /**\n     * 使用深度优先搜索（DFS）来尝试匹配单词。\n     * \n     * @param i 当前行位置\n     * @param j 当前列位置\n     * @param k 当前单词中字符的索引\n     * @return 如果从当前位置开始可以匹配到单词，则返回true；否则返回false。\n     */\n    private boolean dfs(int i, int j, int k) {\n        // 如果已经匹配到单词的最后一个字符，则返回true\n        if (k == word.length()) {\n            return true;\n        }\n        // 如果当前位置无效或已被访问，或者当前位置的字符与目标字符不匹配，则返回false\n        if (i < 0 || i >= m || j < 0 || j >= n || visited[i][j] || board[i][j] != word.charAt(k)) {\n            return false;\n        }\n        \n        visited[i][j] = true;\n        // 尝试在上、下、左、右四个方向进行深度优先搜索\n        // 上下左右四个方向搜索\n        if (dfs(i - 1, j, k + 1) || dfs(i + 1, j, k + 1) || dfs(i, j - 1, k + 1) || dfs(i, j + 1, k + 1)) {\n            return true;\n        }\n        \n        // 如果当前路径无法匹配单词，则回溯，将当前位置的访问状态重置为未访问\n        // 回溯\n        visited[i][j] = false;\n        return false;\n    }\n}\n```\n\n# 分治\n## 108. 将有序数组转换为二叉搜索树\nhttps://leetcode.cn/problems/convert-sorted-array-to-binary-search-tree/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个整数数组 nums ，其中元素已经按 升序 排列，请你将其转换为一棵 \n平衡\n 二叉搜索树。\n\n \n\n示例 1：\n\n![alt text](../img/数据结构和算法/将有序数组转换为二叉搜索树1.png)\n\n输入：nums = [-10,-3,0,5,9]\n输出：[0,-3,9,-10,null,5]\n解释：[0,-10,5,null,-3,null,9] 也将被视为正确答案：\n\n![alt text](../img/数据结构和算法/将有序数组转换为二叉搜索树2.png)\n\n示例 2：\n\n![alt text](../img/数据结构和算法/将有序数组转换为二叉搜索树3.png)\n\n输入：nums = [1,3]\n输出：[3,1]\n解释：[1,null,3] 和 [3,1] 都是高度平衡二叉搜索树。\n \n\n提示：\n\n1 <= nums.length <= 10^4\n-10^4 <= nums[i] <= 10^4\nnums 按 严格递增 顺序排列\n\n```java\n/**\n * Definition for a binary tree node.\n * public class TreeNode {\n *     int val;\n *     TreeNode left;\n *     TreeNode right;\n *     TreeNode() {}\n *     TreeNode(int val) { this.val = val; }\n *     TreeNode(int val, TreeNode left, TreeNode right) {\n *         this.val = val;\n *         this.left = left;\n *         this.right = right;\n *     }\n * }\n */\nclass Solution {\n    public TreeNode sortedArrayToBST(int[] nums) {\n        return buildBST(nums, 0, nums.length - 1);\n    }\n\n    private TreeNode buildBST(int[] nums, int left, int right) {\n        // 如果区间为空，返回空节点\n        if (left > right) {\n            return null;\n        }\n        \n        // 选择中间位置的元素作为根节点\n        int mid = left + (right - left) / 2;\n        TreeNode root = new TreeNode(nums[mid]);\n        \n        // 递归构建左子树和右子树\n        root.left = buildBST(nums, left, mid - 1);\n        root.right = buildBST(nums, mid + 1, right);\n        \n        return root;\n    }\n}\n```\n\n## 148. 排序链表\nhttps://leetcode.cn/problems/sort-list/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你链表的头结点 head ，请将其按 升序 排列并返回 排序后的链表 。\n\n \n\n示例 1：\n\n![alt text](../img/数据结构和算法/排序链表1.png)\n\n输入：head = [4,2,1,3]\n\n输出：[1,2,3,4]\n\n示例 2：\n\n![alt text](../img/数据结构和算法/排序链表2.png)\n\n输入：head = [-1,5,3,4,0]\n\n输出：[-1,0,3,4,5]\n\n示例 3：\n\n输入：head = []\n\n输出：[]\n \n\n提示：\n\n- 链表中节点的数目在范围 [0, 5 * 10^4] 内\n- -10^5 <= Node.val <= 10^5\n\n```java\n/**\n * Definition for singly-linked list.\n * public class ListNode {\n *     int val;\n *     ListNode next;\n *     ListNode() {}\n *     ListNode(int val) { this.val = val; }\n *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }\n * }\n */\n/**\n * Definition for singly-linked list.\n * public class ListNode {\n *     int val;\n *     ListNode next;\n *     ListNode() {}\n *     ListNode(int val) { this.val = val; }\n *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }\n * }\n */\n/**\n * Solution类提供了一个方法来对链表进行排序。\n * 它实现了归并排序算法，该算法是递归地将链表分割成更小的部分，然后将这些部分合并成一个排序好的链表。\n */\nclass Solution {\n    /**\n     * 对给定链表进行排序。\n     * \n     * @param head 链表的头节点。\n     * @return 排序后的链表的头节点。\n     */\n    public ListNode sortList(ListNode head) {\n        return mergeSort(head);\n    }\n\n    /**\n     * 归并排序的递归部分。\n     * 它首先找到链表的中间点，然后将链表分割成两部分，分别对这两部分进行排序，最后将排序好的两部分合并。\n     * \n     * @param head 链表的头节点。\n     * @return 排序后的链表的头节点。\n     */\n    public ListNode mergeSort(ListNode head){\n        // 如果链表为空或只有一个节点，无需排序，直接返回\n        if(head == null || head.next==null){\n            return head;\n        }\n        ListNode slow = head,fast = head.next;\n        // 寻找链表的中间点\n        while(fast!=null && fast.next!=null){\n            slow = slow.next;\n            fast = fast.next.next;\n        }\n        // 递归地对右半部分进行排序\n        ListNode m = mergeSort(slow.next);\n        slow.next = null;\n        // 递归地对左半部分进行排序\n        ListNode l = mergeSort(head);\n        // 合并排序好的两部分\n        return mergeTwo(m,l);\n    }\n\n    /**\n     * 合并两个已排序的链表。\n     * \n     * @param n1 第一个链表的头节点。\n     * @param n2 第二个链表的头节点。\n     * @return 合并后的链表的头节点。\n     */\n    public ListNode mergeTwo(ListNode n1,ListNode n2){\n        // 如果其中一个链表为空，直接返回另一个链表\n        if(n1 == null){\n            return n2;\n        }\n        if(n2 == null){\n            return n1;\n        }\n        ListNode newNode;\n        // 比较两个链表的当前节点，将较小值作为新链表的节点，并递归地合并剩余部分\n        if(n1.val < n2.val){\n            newNode = n1;\n            newNode.next = mergeTwo(n1.next,n2);\n        }else{\n            newNode = n2;\n            newNode.next = mergeTwo(n1,n2.next);\n        }\n        return newNode;\n    }\n}\n```\n\n## 427. 建立四叉树\nhttps://leetcode.cn/problems/construct-quad-tree/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个 n * n 矩阵 grid ，矩阵由若干 0 和 1 组成。请你用四叉树表示该矩阵 grid 。\n\n你需要返回能表示矩阵 grid 的 四叉树 的根结点。\n\n四叉树数据结构中，每个内部节点只有四个子节点。此外，每个节点都有两个属性：\n\n- val：储存叶子结点所代表的区域的值。1 对应 True，0 对应 False。注意，当 isLeaf 为 False 时，你可以把 True 或者 False 赋值给节点，两种值都会被判题机制 接受 。\n\n- isLeaf: 当这个节点是一个叶子结点时为 True，如果它有 4 个子节点则为 False 。\n\n```java\nclass Node {\n    public boolean val;\n    public boolean isLeaf;\n    public Node topLeft;\n    public Node topRight;\n    public Node bottomLeft;\n    public Node bottomRight;\n}\n```\n\n我们可以按以下步骤为二维区域构建四叉树：\n\n1. 如果当前网格的值相同（即，全为 0 或者全为 1），将 isLeaf 设为 True ，将 val 设为网格相应的值，并将四个子节点都设为 Null 然后停止。\n2. 如果当前网格的值不同，将 isLeaf 设为 False， 将 val 设为任意值，然后如下图所示，将当前网格划分为四个子网格。\n3. 使用适当的子网格递归每个子节点。\n\n![alt text](../img/数据结构和算法/建立四叉树1.png)\n\n如果你想了解更多关于四叉树的内容，可以参考 wiki 。\n\n四叉树格式：\n\n你不需要阅读本节来解决这个问题。只有当你想了解输出格式时才会这样做。输出为使用层序遍历后四叉树的序列化形式，其中 null 表示路径终止符，其下面不存在节点。\n\n它与二叉树的序列化非常相似。唯一的区别是节点以列表形式表示 [isLeaf, val] 。\n\n如果 isLeaf 或者 val 的值为 True ，则表示它在列表 [isLeaf, val] 中的值为 1 ；如果 isLeaf 或者 val 的值为 False ，则表示值为 0 。\n\n \n\n示例 1：\n\n![alt text](../img/数据结构和算法/建立四叉树2.png)\n\n输入：grid = [[0,1],[1,0]]\n\n输出：[[0,1],[1,0],[1,1],[1,1],[1,0]]\n\n解释：此示例的解释如下：\n\n请注意，在下面四叉树的图示中，0 表示 false，1 表示 True 。\n\n![alt text](../img/数据结构和算法/建立四叉树3.png)\n\n示例 2：\n\n![alt text](../img/数据结构和算法/建立四叉树4.png)\n\n输入：grid = [[1,1,1,1,0,0,0,0],[1,1,1,1,0,0,0,0],[1,1,1,1,1,1,1,1],[1,1,1,1,1,1,1,1],[1,1,1,1,0,0,0,0],[1,1,1,1,0,0,0,0],[1,1,1,1,0,0,0,0],[1,1,1,1,0,0,0,0]]\n\n输出：[[0,1],[1,1],[0,1],[1,1],[1,0],null,null,null,null,[1,0],[1,0],[1,1],[1,1]]\n\n解释：网格中的所有值都不相同。我们将网格划分为四个子网格。\n\ntopLeft，bottomLeft 和 bottomRight 均具有相同的值。\n\ntopRight 具有不同的值，因此我们将其再分为 4 个子网格，这样每个子网格都具有相同的值。\n\n解释如下图所示：\n\n![alt text](../img/数据结构和算法/建立四叉树5.png)\n\n提示：\n\n- n == grid.length == grid[i].length\n- n == 2x 其中 0 <= x <= 6\n\n```java\nclass Solution {\n    /**\n     * 构建四叉树的辅助函数，用于递归处理网格。\n     *\n     * @param grid 输入的n*n矩阵\n     * @param row  当前处理网格的起始行\n     * @param col  当前处理网格的起始列\n     * @param length 当前处理网格的边长\n     * @return 返回构建好的四叉树节点\n     */\n    private Node buildQuadTree(int[][] grid, int row, int col, int length) {\n        // 如果当前网格大小为1，直接创建叶节点\n        if (length == 1) {\n            return new Node(grid[row][col] == 1, true, null, null, null, null);\n        }\n        \n        // 递归划分四个子网格并构建子节点\n        Node topLeft = buildQuadTree(grid, row, col, length / 2);\n        Node topRight = buildQuadTree(grid, row, col + length / 2, length / 2);\n        Node bottomLeft = buildQuadTree(grid, row + length / 2, col, length / 2);\n        Node bottomRight = buildQuadTree(grid, row + length / 2, col + length / 2, length / 2);\n        \n        // 如果四个子节点都是叶子节点且值相同，合并为一个叶节点\n        if (topLeft.isLeaf && topRight.isLeaf && bottomLeft.isLeaf && bottomRight.isLeaf\n                && topLeft.val == topRight.val && topLeft.val == bottomLeft.val && topLeft.val == bottomRight.val) {\n            return new Node(topLeft.val, true, null, null, null, null);\n        } else {\n            // 否则，创建一个内部节点指向四个子节点\n            return new Node(false, false, topLeft, topRight, bottomLeft, bottomRight);\n        }\n    }\n\n    public Node construct(int[][] grid) {\n        /**\n         * 主函数，接收一个n*n的矩阵grid，返回表示该矩阵的四叉树的根节点。\n         *\n         * @param grid n*n的二维矩阵，由0和1组成\n         * @return 四叉树的根节点\n         */\n        return buildQuadTree(grid, 0, 0, grid.length);\n    }\n}\n```\n\n## 23. 合并 K 个升序链表\nhttps://leetcode.cn/problems/merge-k-sorted-lists/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个链表数组，每个链表都已经按升序排列。\n\n请你将所有链表合并到一个升序链表中，返回合并后的链表。\n\n \n\n示例 1：\n\n输入：lists = [[1,4,5],[1,3,4],[2,6]]\n\n输出：[1,1,2,3,4,4,5,6]\n\n解释：链表数组如下：\n\n[\n\n  1->4->5,\n\n  1->3->4,\n\n  2->6\n\n]\n\n将它们合并到一个有序链表中得到。\n\n1->1->2->3->4->4->5->6\n\n示例 2：\n\n输入：lists = []\n\n输出：[]\n\n示例 3：\n\n输入：lists = [[]]\n\n输出：[]\n \n\n提示：\n\n- k == lists.length\n- 0 <= k <= 10^4\n- 0 <= lists[i].length <= 500\n- -10^4 <= lists[i][j] <= 10^4\n- lists[i] 按 升序 排列\n- lists[i].length 的总和不超过 10^4\n\n```java\n/**\n * Definition for singly-linked list.\n * public class ListNode {\n *     int val;\n *     ListNode next;\n *     ListNode() {}\n *     ListNode(int val) { this.val = val; }\n *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }\n * }\n */\n/**\n * 解决方案类，提供方法以合并多个已排序的链表为一个有序链表。\n */\nclass Solution {\n\n    /**\n     * 主要方法：合并K个已排序的链表。\n     * \n     * @param lists 一个数组，其中包含K个已排序的链表的头节点。\n     * @return 返回一个新的已排序链表的头节点，该链表由输入的所有链表合并而成。\n     */\n    public ListNode mergeKLists(ListNode[] lists) {\n        // 初始化结果链表为空\n        ListNode res = null;\n\n        // 遍历所有链表\n        for (int i = 0; i < lists.length; i++) {\n            // 将当前链表与结果链表合并\n            res = mergeTwo(res, lists[i]);\n        }\n\n        // 返回最终合并后的链表头节点\n        return res;\n    }\n\n    /**\n     * 辅助方法：合并两个已排序的链表。\n     * \n     * @param l1 第一个已排序链表的头节点。\n     * @param l2 第二个已排序链表的头节点。\n     * @return 返回合并后新链表的头节点。\n     */\n    public ListNode mergeTwo(ListNode l1, ListNode l2) {\n        // 如果任一链表为空，直接返回非空链表的头节点\n        if (l1 == null || l2 == null) {\n            return l1 == null ? l2 : l1;\n        }\n\n        // 创建哑节点作为新链表的起点\n        ListNode head = new ListNode(0);\n        ListNode tail = head; // tail用于追踪新链表的最后一个节点\n\n        // 分别用指针p1和p2遍历两个链表\n        ListNode p1 = l1, p2 = l2;\n        \n        // 当两个链表都未遍历完时\n        while (p1 != null && p2 != null) {\n            // 比较两个链表当前节点的值，将较小值的节点加入新链表\n            if (p1.val < p2.val) {\n                tail.next = p1;\n                p1 = p1.next;\n            } else {\n                tail.next = p2;\n                p2 = p2.next;\n            }\n            \n            // tail指针向后移动\n            tail = tail.next;\n        }\n        \n        // 合并剩余部分，哪个链表未结束就将其剩余部分接到新链表尾部\n        tail.next = p1 == null ? p2 : p1;\n\n        // 返回新链表的头节点（哑节点的下一个节点）\n        return head.next;\n    }\n}\n```\n\n# Kadane 算法\n## 53. 最大子数组和\n给你一个整数数组 nums ，请你找出一个具有最大和的连续子数组（子数组最少包含一个元素），返回其最大和。子数组\n是数组中的一个连续部分。\n\n示例 1：\n\n输入：nums = [-2,1,-3,4,-1,2,1,-5,4]\n\n输出：6\n\n解释：连续子数组 [4,-1,2,1] 的和最大，为 6 。\n\n示例 2：\n\n输入：nums = [1]\n\n输出：1\n\n示例 3：\n\n输入：nums = [5,4,-1,7,8]\n\n输出：23\n \n\n提示：\n\n- 1 <= nums.length <= 10^5\n- -10^4 <= nums[i] <= 10^4\n\n```java\npublic class Solution {\n    /**\n     * 使用Kadane算法寻找最大子数组和。\n     *\n     * @param nums 包含整数的数组，表示输入的序列。\n     * @return 数组nums中最大连续子数组的和。\n     */\n    public int maxSubArray(int[] nums) {\n        // 初始化当前和以及最大和为数组的第一个元素\n        int curSum = nums[0];\n        int maxSum = nums[0];\n\n        // 从数组的第二个元素开始遍历\n        for (int i = 1; i < nums.length; i++) {\n            // 更新当前和，要么加上当前数字，要么如果当前数字更大则从当前数字开始新的子数组\n            curSum = Math.max(nums[i], curSum + nums[i]);\n\n            // 如果当前和大于最大和，则更新最大和\n            maxSum = Math.max(maxSum, curSum);\n        }\n\n        // 返回最大子数组和\n        return maxSum;\n    }\n}\n```\n\n## 918. 环形子数组的最大和\nhttps://leetcode.cn/problems/maximum-sum-circular-subarray/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个长度为 n 的环形整数数组 nums ，返回 nums 的非空 子数组 的最大可能和 。\n\n环形数组 意味着数组的末端将会与开头相连呈环状。形式上， nums[i] 的下一个元素是 nums[(i + 1) % n] ， nums[i] 的前一个元素是 nums[(i - 1 + n) % n] 。\n\n子数组 最多只能包含固定缓冲区 nums 中的每个元素一次。形式上，对于子数组 nums[i], nums[i + 1], ..., nums[j] ，不存在 i <= k1, k2 <= j 其中 k1 % n == k2 % n 。\n\n \n\n示例 1：\n\n输入：nums = [1,-2,3,-2]\n\n输出：3\n\n解释：从子数组 [3] 得到最大和 3\n\n示例 2：\n\n输入：nums = [5,-3,5]\n\n输出：10\n\n解释：从子数组 [5,5] 得到最大和 5 + 5 = 10\n\n示例 3：\n\n输入：nums = [3,-2,2,-3]\n\n输出：3\n\n解释：从子数组 [3] 和 [3,-2,2] 都可以得到最大和 3\n \n\n提示：\n\n- n == nums.length\n- 1 <= n <= 3 * 10^4\n- -3 * 10^4 <= nums[i] <= 3 * 10^4​​​​​​​\n\n```java\n/**\n * 解决方案类，提供数组相关的算法解决方案。\n */\nclass Solution {\n    /**\n     * 计算一个循环数组中的最大子数组和。\n     * 循环数组意味着数组的末尾会连接到数组的开头形成一个圈。\n     * \n     * @param nums 原始数组，假设不为空且至少包含一个元素。\n     * @return 返回计算出的最大子数组和。\n     */\n    public int maxSubarraySumCircular(int[] nums) {\n        /* 数组长度 */\n        int n = nums.length;\n        /* 使用双端队列来存储每个索引及其对应的累积和 */\n        Deque<int[]> queue = new ArrayDeque<int[]>();\n        /* pre 用于记录当前的累积和，res 用于记录最大的子数组和 */\n        int pre = nums[0], res = nums[0];\n        /* 将初始状态（索引0，累积和nums[0]）加入队列 */\n        queue.offerLast(new int[]{0, pre});\n        \n        /* 遍历两倍于数组长度的距离，以处理循环数组的情况 */\n        for (int i = 1; i < 2 * n; i++) {\n            /* 移除所有已经不在当前窗口内的元素 */\n            while (!queue.isEmpty() && queue.peekFirst()[0] < i - n) {\n                queue.pollFirst();\n            }\n            /* 更新当前累积和 */\n            pre += nums[i % n];\n            /* 更新最大子数组和 */\n            res = Math.max(res, pre - queue.peekFirst()[1]);\n            /* 移除所有累积和小于等于当前累积和的元素，以优化队列 */\n            while (!queue.isEmpty() && queue.peekLast()[1] >= pre) {\n                queue.pollLast();\n            }\n            /* 将当前索引和累积和对加入队列 */\n            queue.offerLast(new int[]{i, pre});\n        }\n        /* 返回最大子数组和 */\n        return res;\n    }\n}\n```\n\n# 二分查找\n## 35. 搜索插入位置\nhttps://leetcode.cn/problems/search-insert-position/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个排序数组和一个目标值，在数组中找到目标值，并返回其索引。如果目标值不存在于数组中，返回它将会被按顺序插入的位置。\n\n请必须使用时间复杂度为 O(log n) 的算法。\n\n \n\n示例 1:\n\n输入: nums = [1,3,5,6], target = 5\n\n输出: 2\n\n示例 2:\n\n输入: nums = [1,3,5,6], target = 2\n\n输出: 1\n\n示例 3:\n\n输入: nums = [1,3,5,6], target = 7\n\n输出: 4\n \n\n提示:\n\n- 1 <= nums.length <= 10^4\n- -10^4 <= nums[i] <= 10^4\n- nums 为 无重复元素 的 升序 排列数组\n- -10^4 <= target <= 10^4\n\n```java\n/**\n * 在给定的有序数组中搜索目标值，如果找到则返回其索引，否则返回目标值应插入的位置索引，\n * 以保持数组的有序状态。此算法保证时间复杂度为 O(log n)。\n *\n * @param nums   一个无重复元素的升序排列数组\n * @param target 要搜索或插入的目标值\n * @return 目标值在数组中的索引位置，或应该插入的位置索引\n */\npublic int searchInsertPosition(int[] nums, int target) {\n    int left = 0, right = nums.length - 1;\n    \n    while (left <= right) {\n        int mid = left + (right - left) / 2;\n        if (nums[mid] == target) {\n            return mid; // 目标值找到，返回索引\n        } else if (nums[mid] < target) {\n            left = mid + 1; // 调整左边界，在右半部分继续搜索\n        } else {\n            right = mid - 1; // 调整右边界，在左半部分继续搜索\n        }\n    }\n    \n    // 当left > right时，循环结束，此时left为target应插入的位置\n    return left;\n}\n```\n\n## 74. 搜索二维矩阵\nhttps://leetcode.cn/problems/search-a-2d-matrix/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个满足下述两条属性的 m x n 整数矩阵：\n\n- 每行中的整数从左到右按非严格递增顺序排列。\n- 每行的第一个整数大于前一行的最后一个整数。\n\n给你一个整数 target ，如果 target 在矩阵中，返回 true ；否则，返回 false 。\n\n \n\n示例 1：\n\n![alt text](../img/数据结构和算法/搜索二维数组1.png)\n\n\n输入：matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 3\n\n输出：true\n\n示例 2：\n\n![alt text](../img/数据结构和算法/搜索二维数组2.png)\n\n输入：matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 13\n\n输出：false\n \n\n提示：\n\n- m == matrix.length\n- n == matrix[i].length\n- 1 <= m, n <= 100\n- -10^4 <= matrix[i][j], target <= 10^4\n\n```java\n/**\n * 在一个按照非严格递增顺序排列的二维矩阵中搜索目标值，如果找到则返回 true，否则返回 false。\n * 矩阵的行也是按非降序排列的。\n *\n * @param matrix 二维整数矩阵，满足题目中的条件\n * @param target 需要搜索的目标值\n * @return 如果目标值存在于矩阵中返回 true，否则返回 false\n */\n/**\n * 在二维矩阵中搜索目标值。\n * 矩阵每一行的元素从左到右递增，每一列的元素从上到下递增。\n * @param matrix 二维整数矩阵，可能为空或包含零元素。\n * @param target 要搜索的目标整数。\n * @return 如果矩阵中存在目标值，则返回true；否则返回false。\n */\npublic boolean searchMatrix(int[][] matrix, int target) {\n    // 检查矩阵是否为空或没有元素\n    if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {\n        return false;\n    }\n\n    // 获取矩阵的行数和列数\n    int rows = matrix.length;\n    int cols = matrix[0].length;\n    // 初始化二分查找的左右边界\n    int left = 0, right = rows * cols - 1;\n\n    // 使用二分查找在“扁平化”后的矩阵中搜索目标值\n    while (left <= right) {\n        // 计算中间位置的索引，并避免整数溢出\n        int mid = left + (right - left) / 2;\n        // 根据中间位置的索引计算其在矩阵中的实际位置\n        int midVal = matrix[mid / cols][mid % cols];\n\n        // 如果找到目标值，则返回true\n        if (midVal == target) {\n            return true;\n        } else if (midVal < target) {\n            // 如果中间值小于目标值，则目标值在右侧\n            left = mid + 1;\n        } else {\n            // 如果中间值大于目标值，则目标值在左侧\n            right = mid - 1;\n        }\n    }\n\n    // 如果没有找到目标值，则返回false\n    return false;\n}\n```\n\n## 162. 寻找峰值\nhttps://leetcode.cn/problems/find-peak-element/description/?envType=study-plan-v2&envId=top-interview-150\n\n峰值元素是指其值严格大于左右相邻值的元素。\n\n给你一个整数数组 nums，找到峰值元素并返回其索引。数组可能包含多个峰值，在这种情况下，返回 任何一个峰值 所在位置即可。\n\n你可以假设 nums[-1] = nums[n] = -∞ 。\n\n你必须实现时间复杂度为 O(log n) 的算法来解决此问题。\n\n \n\n示例 1：\n\n输入：nums = [1,2,3,1]\n\n输出：2\n\n解释：3 是峰值元素，你的函数应该返回其索引 2。\n\n示例 2：\n\n输入：nums = [1,2,1,3,5,6,4]\n\n输出：1 或 5 \n\n解释：你的函数可以返回索引 1，其峰值元素为 2；\n     或者返回索引 5， 其峰值元素为 6。\n \n\n提示：\n\n- 1 <= nums.length <= 1000\n- -2^31 <= nums[i] <= 2^31 - 1\n- 对于所有有效的 i 都有 nums[i] != nums[i + 1]\n\n```java\n/**\n * 寻找峰值元素的函数。\n * 峰值元素是大于其相邻元素的元素，数组两端视为负无穷大。\n * 实现时间复杂度为O(log n)的二分查找算法。\n *\n * @param nums 整型数组，其中包含至少一个峰值元素。\n * @return 峰值元素的索引位置。\n */\npublic int findPeakElement(int[] nums) {\n    if (nums == null || nums.length == 0) {\n        return -1; // 或根据题目要求抛出异常\n    }\n    \n    int left = 0, right = nums.length - 1;\n    \n    while (left < right) {\n        int mid = left + (right - left) / 2;\n        if (nums[mid] > nums[mid + 1]) {\n            // 下降趋势，峰值可能在左边，包括当前mid\n            right = mid;\n        } else {\n            // 上升趋势，峰值一定在右边，不包括当前mid\n            left = mid + 1;\n        }\n    }\n    \n    // 当left == right时，根据题设nums[-1] = nums[n] = -∞，则left位置为峰值\n    return left;\n}\n```\n\n## 33. 搜索旋转排序数组\nhttps://leetcode.cn/problems/search-in-rotated-sorted-array/description/?envType=study-plan-v2&envId=top-interview-150\n\n整数数组 nums 按升序排列，数组中的值 互不相同 。\n\n在传递给函数之前，nums 在预先未知的某个下标 k（0 <= k < nums.length）上进行了 旋转，使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]]（下标 从 0 开始 计数）。例如， [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。\n\n给你 旋转后 的数组 nums 和一个整数 target ，如果 nums 中存在这个目标值 target ，则返回它的下标，否则返回 -1 。\n\n你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。\n\n \n\n示例 1：\n\n输入：nums = [4,5,6,7,0,1,2], target = 0\n\n输出：4\n\n示例 2：\n\n输入：nums = [4,5,6,7,0,1,2], target = 3\n\n输出：-1\n\n示例 3：\n\n输入：nums = [1], target = 0\n\n输出：-1\n \n\n提示：\n\n- 1 <= nums.length <= 5000\n- -10^4 <= nums[i] <= 10^4\n- nums 中的每个值都 独一无二\n- 题目数据保证 nums 在预先未知的某个下标上进行了旋转\n- -10^4 <= target <= 10^4\n\n```java\npublic int search(int[] nums, int target) {\n    if (nums == null || nums.length == 0) {\n        return -1;\n    }\n    \n    int left = 0, right = nums.length - 1;\n    \n    while (left <= right) {\n        int mid = left + (right - left) / 2;\n        if (nums[mid] == target) {\n            return mid;\n        }\n        \n        // 判断哪边是有序的\n        if (nums[left] <= nums[mid]) { // 左边有序\n            if (target >= nums[left] && target < nums[mid]) {\n                right = mid - 1;\n            } else {\n                left = mid + 1;\n            }\n        } else { // 右边有序\n            if (target > nums[mid] && target <= nums[right]) {\n                left = mid + 1;\n            } else {\n                right = mid - 1;\n            }\n        }\n    }\n    \n    return -1;\n}\n```\n\n## 34. 在排序数组中查找元素的第一个和最后一个位置\nhttps://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个按照非递减顺序排列的整数数组 nums，和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。\n\n如果数组中不存在目标值 target，返回 [-1, -1]。\n\n你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。\n\n \n\n示例 1：\n\n输入：nums = [5,7,7,8,8,10], target = 8\n\n输出：[3,4]\n\n示例 2：\n\n输入：nums = [5,7,7,8,8,10], target = 6\n\n输出：[-1,-1]\n\n示例 3：\n\n输入：nums = [], target = 0\n\n输出：[-1,-1]\n \n\n提示：\n\n- 0 <= nums.length <= 10^5\n- -10^9 <= nums[i] <= 10^9\n- nums 是一个非递减数组\n- -10^9 <= target <= 10^9\n\n```java\n/**\n * 在排序数组中查找给定目标值的起始和结束位置。\n * 该类提供了一个方法来解决在排序数组中查找元素范围的问题。\n */\nclass Solution {\n    /**\n     * 在排序数组中查找目标值的起始和结束位置。\n     * \n     * @param nums 排序数组，其中可能包含目标值。\n     * @param target 目标值，我们需要找到它在数组中的起始和结束位置。\n     * @return 包含两个整数的数组，分别表示目标值在数组中的起始和结束位置。\n     *         如果数组中不存在目标值，则返回 [-1, -1]。\n     */\n    public int[] searchRange(int[] nums, int target) {\n        int left = 0;\n        int right = nums.length - 1;\n        // 使用二分查找来定位目标值的初始位置\n        while (left <= right) {\n            int mid = left + (right - left) / 2;\n            if (target == nums[mid]) {\n                // 一旦找到目标值，向左和向右扫描以确定范围\n                int l = mid, r = mid;\n                // 向左扫描，找到起始位置\n                while (l > 0 && nums[l - 1] == target) {\n                    l--;\n                }\n                // 向右扫描，找到结束位置\n                while (r < nums.length - 1 && nums[r + 1] == target) {\n                    r++;\n                }\n                // 返回目标值的起始和结束位置\n                return new int[]{l, r};\n            } else if (nums[mid] > target) {\n                // 如果中间值大于目标值，缩小右边界\n                right = mid - 1;\n            } else {\n                // 如果中间值小于目标值，增大左边界\n                left = mid + 1;\n            }\n        }\n        // 如果未找到目标值，返回 [-1, -1]\n        return new int[]{-1, -1};\n    }\n}\n```\n\n## 153. 寻找旋转排序数组中的最小值\nhttps://leetcode.cn/problems/find-minimum-in-rotated-sorted-array/description/?envType=study-plan-v2&envId=top-interview-150\n\n已知一个长度为 n 的数组，预先按照升序排列，经由 1 到 n 次 旋转 后，得到输入数组。例如，原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到：\n- 若旋转 4 次，则可以得到 [4,5,6,7,0,1,2]\n- 若旋转 7 次，则可以得到 [0,1,2,4,5,6,7]\n\n注意，数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]] 。\n\n给你一个元素值 互不相同 的数组 nums ，它原来是一个升序排列的数组，并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。\n\n你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。\n\n \n\n示例 1：\n\n输入：nums = [3,4,5,1,2]\n\n输出：1\n\n解释：原数组为 [1,2,3,4,5] ，旋转 3 次得到输入数组。\n\n示例 2：\n\n输入：nums = [4,5,6,7,0,1,2]\n\n输出：0\n\n解释：原数组为 [0,1,2,4,5,6,7] ，旋转 3 次得到输入数组。\n\n示例 3：\n\n输入：nums = [11,13,15,17]\n\n输出：11\n\n解释：原数组为 [11,13,15,17] ，旋转 4 次得到输入数组。\n \n\n提示：\n\n- n == nums.length\n- 1 <= n <= 5000\n- -5000 <= nums[i] <= 5000\n- nums 中的所有整数 互不相同\n- nums 原来是一个升序排序的数组，并进行了 1 至 n 次旋转\n\n```java\n/**\n * 解决寻找旋转排序数组中的最小值的问题。\n * 旋转排序数组是指原数组为非递减数组，将数组从某个位置分割成两部分，然后将两部分的顺序调换后形成的数组。\n * 例如，原数组[0,1,2,4,5,6,7]在数字4处旋转后变为[4,5,6,7,0,1,2]。\n */\nclass Solution {\n    /**\n     * 寻找旋转排序数组中的最小值。\n     * \n     * @param nums 一个旋转后的非递减排序数组\n     * @return 数组中的最小值\n     */\n    public int findMin(int[] nums) {\n        int left = 0, right = nums.length - 1;\n        // 使用二分查找法定位最小值的位置\n        while (left < right) {\n            // 计算中间位置，避免整数溢出\n            int mid = left + (right - left) / 2;\n            // 如果中间位置的值大于最右边的值，说明最小值在mid右侧\n            if (nums[mid] > nums[right]) {\n                left = mid + 1;\n            } else {\n                // 否则，最小值在mid或其左侧\n                right = mid;\n            }\n        }\n        // 当left等于right时，找到最小值的位置\n        return nums[left];\n    }\n}\n```\n\n## 4. 寻找两个正序数组的中位数\nhttps://leetcode.cn/problems/median-of-two-sorted-arrays/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定两个大小分别为 m 和 n 的正序（从小到大）数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。\n\n算法的时间复杂度应该为 O(log (m+n)) 。\n\n \n\n示例 1：\n\n输入：nums1 = [1,3], nums2 = [2]\n\n输出：2.00000\n\n解释：合并数组 = [1,2,3] ，中位数 2\n\n示例 2：\n\n输入：nums1 = [1,2], nums2 = [3,4]\n\n输出：2.50000\n\n解释：合并数组 = [1,2,3,4] ，中位数 (2 + 3) / 2 = 2.5\n \n\n \n\n提示：\n\n- nums1.length == m\n- nums2.length == n\n- 0 <= m <= 1000\n- 0 <= n <= 1000\n- 1 <= m + n <= 2000\n- -106 <= nums1[i], nums2[i] <= 106\n\n```java\nclass Solution {\n    /**\n     * 寻找两个正序数组的中位数。\n     * 使用二分查找法降低时间复杂度至O(log(min(m, n)))，其中m和n分别是两个数组的长度。\n     *\n     * @param nums1 第一个正序数组\n     * @param nums2 第二个正序数组\n     * @return 两个数组合并后的中位数\n     */\n    public double findMedianSortedArrays(int[] nums1, int[] nums2) {\n        // 确保nums1是较短的数组，优化二分查找过程\n        if (nums1.length > nums2.length) {\n            int[] temp = nums1;\n            nums1 = nums2;\n            nums2 = temp;\n        }\n\n        int x = nums1.length;\n        int y = nums2.length;\n        int low = 0;\n        int high = x;\n\n        while (low <= high) {\n            int partitionX = (low + high) / 2;\n            int partitionY = (x + y + 1) / 2 - partitionX;\n\n            // 寻找左右两侧边界值，注意处理边界情况\n            int maxLeftX = (partitionX == 0) ? Integer.MIN_VALUE : nums1[partitionX - 1];\n            int minRightX = (partitionX == x) ? Integer.MAX_VALUE : nums1[partitionX];\n\n            int maxLeftY = (partitionY == 0) ? Integer.MIN_VALUE : nums2[partitionY - 1];\n            int minRightY = (partitionY == y) ? Integer.MAX_VALUE : nums2[partitionY];\n\n            // 检查划分是否满足条件\n            if (maxLeftX <= minRightY && maxLeftY <= minRightX) {\n                // 找到正确的划分，计算中位数\n                if ((x + y) % 2 == 0) {\n                    return ((double)Math.max(maxLeftX, maxLeftY) + Math.min(minRightX, minRightY)) / 2;\n                } else {\n                    return (double)Math.max(maxLeftX, maxLeftY);\n                }\n            } else if (maxLeftX > minRightY) {\n                // 缩小nums1的查找范围\n                high = partitionX - 1;\n            } else {\n                // 扩大nums1的查找范围\n                low = partitionX + 1;\n            }\n        }\n\n        throw new IllegalArgumentException(\"Input arrays are not sorted or not valid.\");\n    }\n}\n```\n\n# 堆\n## 215. 数组中的第K个最大元素\nhttps://leetcode.cn/problems/kth-largest-element-in-an-array/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定整数数组 nums 和整数 k，请返回数组中第 k 个最大的元素。\n\n请注意，你需要找的是数组排序后的第 k 个最大的元素，而不是第 k 个不同的元素。\n\n你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。\n\n \n\n示例 1:\n\n输入: [3,2,1,5,6,4], k = 2\n\n输出: 5\n\n示例 2:\n\n输入: [3,2,3,1,2,4,5,5,6], k = 4\n\n输出: 4\n \n\n提示：\n\n- 1 <= k <= nums.length <= 10^5\n- -10^4 <= nums[i] <= 10^4\n\n```java\nclass Solution {\n    /**\n     * 寻找第k大的元素。\n     * 使用随机化快速选择算法，避免了完整排序的需要，提高了效率。\n     * \n     * @param nums 输入的整数数组。\n     * @param k 指定的第k大的元素。\n     * @return 返回数组中第k大的元素。\n     */\n    public int findKthLargest(int[] nums, int k) {\n        // 调用处理函数，初始化搜索范围为整个数组，寻找第k大的元素位置\n        return handle(nums, 0, nums.length - 1, nums.length - k);\n    }\n\n    /**\n     * 处理函数，用于寻找第k大的元素。\n     * 通过随机化选择一个基准元素，并进行部分排序，逐步缩小搜索范围。\n     * \n     * @param nums 输入的整数数组。\n     * @param left 当前搜索范围的左边界。\n     * @param right 当前搜索范围的右边界。\n     * @param k 指定的第k大的元素。\n     * @return 返回数组中第k大的元素。\n     */\n    public int handle(int[] nums, int left, int right, int k) {\n        // 当左边界小于右边界时，继续搜索\n        while (left < right) {\n            // 通过随机选择基准元素，进行部分排序，返回基准元素的最终位置\n            int p = sort(nums, left, right);\n            // 如果基准元素位置等于k，搜索结束\n            if (p == k) {\n                break;\n            } else if (p < k) {\n                // 如果基准元素位置小于k，调整左边界\n                left = p + 1;\n            } else {\n                // 如果基准元素位置大于k，调整右边界\n                right = p - 1;\n            }\n        }\n        // 返回第k大的元素\n        return nums[k];\n    }\n\n    /**\n     * 随机化选择基准元素并进行部分排序。\n     * 使用随机选择的基准元素，将小于基准元素的元素放到基准元素的左边，大于基准元素的元素放到右边。\n     * \n     * @param nums 输入的整数数组。\n     * @param left 当前搜索范围的左边界。\n     * @param right 当前搜索范围的右边界。\n     * @return 返回基准元素的最终位置。\n     */\n    public int sort(int[] nums, int left, int right) {\n        // 随机选择一个基准元素的位置\n        int random = new Random().nextInt(right - left + 1) + left;\n        swap(nums, left, random);\n        int pd = nums[left];\n        int lt = left;\n        // 遍历数组，将小于基准元素的元素放到基准元素的左边\n        for (int i = left + 1; i <= right; i++) {\n            if (nums[i] < pd) {\n                swap(nums, i, ++lt);\n            }\n        }\n        // 将基准元素放到正确的位置\n        swap(nums, left, lt);\n        // 返回基准元素的最终位置\n        return lt;\n    }\n\n    /**\n     * 交换数组中两个位置的元素。\n     * \n     * @param nums 输入的整数数组。\n     * @param a 要交换的第一个位置。\n     * @param b 要交换的第二个位置。\n     */\n    public void swap(int[] nums, int a, int b) {\n        // 临时存储a位置的元素\n        int temp = nums[a];\n        // 将a位置的元素换成b位置的元素\n        nums[a] = nums[b];\n        // 将b位置的元素换成临时存储的元素\n        nums[b] = temp;\n    }\n}\n```\n\n## 502. IPO\nhttps://leetcode.cn/problems/ipo/description/?envType=study-plan-v2&envId=top-interview-150\n\n假设 力扣（LeetCode）即将开始 IPO 。为了以更高的价格将股票卖给风险投资公司，力扣 希望在 IPO 之前开展一些项目以增加其资本。 由于资源有限，它只能在 IPO 之前完成最多 k 个不同的项目。帮助 力扣 设计完成最多 k 个不同项目后得到最大总资本的方式。\n\n给你 n 个项目。对于每个项目 i ，它都有一个纯利润 profits[i] ，和启动该项目需要的最小资本 capital[i] 。\n\n最初，你的资本为 w 。当你完成一个项目时，你将获得纯利润，且利润将被添加到你的总资本中。\n\n总而言之，从给定项目中选择 最多 k 个不同项目的列表，以 最大化最终资本 ，并输出最终可获得的最多资本。\n\n答案保证在 32 位有符号整数范围内。\n\n \n\n示例 1：\n\n输入：k = 2, w = 0, profits = [1,2,3], capital = [0,1,1]\n\n输出：4\n\n解释：\n\n由于你的初始资本为 0，你仅可以从 0 号项目开始。\n\n在完成后，你将获得 1 的利润，你的总资本将变为 1。\n\n此时你可以选择开始 1 号或 2 号项目。\n\n由于你最多可以选择两个项目，所以你需要完成 2 号项目以获得最大的资本。\n\n因此，输出最后最大化的资本，为 0 + 1 + 3 = 4。\n\n示例 2：\n\n输入：k = 3, w = 0, profits = [1,2,3], capital = [0,1,2]\n\n输出：6\n \n\n提示：\n\n- 1 <= k <= 10^5\n- 0 <= w <= 10^9\n- n == profits.length\n- n == capital.length\n- 1 <= n <= 10^5\n- 0 <= profits[i] <= 10^4\n- 0 <= capital[i] <= 10^9\n\n```java\nimport java.util.*;\n\nclass Solution {\n    public int findMaximizedCapital(int k, int w, int[] profits, int[] capital) {\n        // 定义项目类，包含利润和所需资本，便于优先队列操作\n        class Project implements Comparable<Project> {\n            int profit;\n            int capital;\n\n            Project(int profit, int capital) {\n                this.profit = profit;\n                this.capital = capital;\n            }\n\n            // 按照所需资本从小到大排序\n            @Override\n            public int compareTo(Project other) {\n                return Integer.compare(this.capital, other.capital);\n            }\n        }\n\n        // 创建优先队列保存项目，按照利润从大到小排序\n        PriorityQueue<Project> maxProfitHeap = new PriorityQueue<>((a, b) -> b.profit - a.profit);\n        // 创建优先队列保存可执行的项目，按照所需资本从小到大排序\n        PriorityQueue<Project> minCapitalHeap = new PriorityQueue<>();\n\n        // 初始化项目到minCapitalHeap\n        for (int i = 0; i < profits.length; i++) {\n            minCapitalHeap.offer(new Project(profits[i], capital[i]));\n        }\n\n        while (k-- > 0) {\n            // 当前资本能执行的项目\n            while (!minCapitalHeap.isEmpty() && minCapitalHeap.peek().capital <= w) {\n                maxProfitHeap.offer(minCapitalHeap.poll());\n            }\n            // 若没有可执行的项目且k还有剩余，则跳出循环\n            if (maxProfitHeap.isEmpty()) break;\n            // 执行利润最大的项目\n            w += maxProfitHeap.poll().profit;\n        }\n\n        return w;\n    }\n}\n```\n\n## 373. 查找和最小的 K 对数字\nhttps://leetcode.cn/problems/find-k-pairs-with-smallest-sums/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定两个以 非递减顺序排列 的整数数组 nums1 和 nums2 , 以及一个整数 k 。\n\n定义一对值 (u,v)，其中第一个元素来自 nums1，第二个元素来自 nums2 。\n\n请找到和最小的 k 个数对 (u1,v1),  (u2,v2)  ...  (uk,vk) 。\n\n \n\n示例 1:\n\n输入: nums1 = [1,7,11], nums2 = [2,4,6], k = 3\n\n输出: [1,2],[1,4],[1,6]\n\n解释: 返回序列中的前 3 对数：\n     [1,2],[1,4],[1,6],[7,2],[7,4],[11,2],[7,6],[11,4],[11,6]\n\n示例 2:\n\n输入: nums1 = [1,1,2], nums2 = [1,2,3], k = 2\n\n输出: [1,1],[1,1]\n\n解释: 返回序列中的前 2 对数：\n     [1,1],[1,1],[1,2],[2,1],[1,2],[2,2],[1,3],[1,3],[2,3]\n \n\n提示:\n\n- 1 <= nums1.length, nums2.length <= 10^5\n- -10^9 <= nums1[i], nums2[i] <= 10^9\n- nums1 和 nums2 均为 升序排列\n- 1 <= k <= 10^4\n- k <= nums1.length * nums2.length\n\n```java\nimport java.util.*;\n\nclass Solution {\n    public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {\n        PriorityQueue<int[]> minHeap = new PriorityQueue<>((a, b) -> a[0] + a[1] - b[0] - b[1]);\n        List<List<Integer>> result = new ArrayList<>();\n\n        if (nums1.length == 0 || nums2.length == 0 || k == 0) {\n            return result;\n        }\n\n        // 将起始组合加入堆\n        for (int i = 0; i < Math.min(nums1.length, k); i++) {\n            minHeap.offer(new int[]{nums1[i], nums2[0], 0});\n        }\n\n        while (k-- > 0 && !minHeap.isEmpty()) {\n            int[] current = minHeap.poll();\n            result.add(Arrays.asList(current[0], current[1]));\n\n            // 如果当前组合的第二个元素不是nums2的最后一个元素，则加入下一个可能的组合\n            if (current[2] < nums2.length - 1) {\n                minHeap.offer(new int[]{current[0], nums2[current[2] + 1], current[2] + 1});\n            }\n        }\n\n        return result;\n    }\n}\n```\n\n## 295. 数据流的中位数\nhttps://leetcode.cn/problems/find-median-from-data-stream/description/?envType=study-plan-v2&envId=top-interview-150\n\n中位数是有序整数列表中的中间值。如果列表的大小是偶数，则没有中间值，中位数是两个中间值的平均值。\n\n- 例如 arr = [2,3,4] 的中位数是 3 。\n- 例如 arr = [2,3] 的中位数是 (2 + 3) / 2 = 2.5 。\n\n实现 MedianFinder 类:\n\n- MedianFinder() 初始化 MedianFinder 对象。\n- void addNum(int num) 将数据流中的整数 num 添加到数据结构中。\n- double findMedian() 返回到目前为止所有元素的中位数。与实际答案相差 10-5 以内的答案将被接受。\n\n示例 1：\n\n输入\n\n[\"MedianFinder\", \"addNum\", \"addNum\", \"findMedian\", \"addNum\", \"findMedian\"]\n\n[[], [1], [2], [], [3], []]\n\n输出\n\n[null, null, null, 1.5, null, 2.0]\n\n解释\n\nMedianFinder medianFinder = new MedianFinder();\n\nmedianFinder.addNum(1);    // arr = [1]\n\nmedianFinder.addNum(2);    // arr = [1, 2]\n\nmedianFinder.findMedian(); // 返回 1.5 ((1 + 2) / 2)\n\nmedianFinder.addNum(3);    // arr[1, 2, 3]\n\nmedianFinder.findMedian(); // return 2.0\n\n提示:\n\n- -10^5 <= num <= 10^5\n- 在调用 findMedian 之前，数据结构中至少有一个元素\n- 最多 5 * 10^4 次调用 addNum 和 findMedian\n\n```java\nimport java.util.PriorityQueue;\n\npublic class MedianFinder {\n    // 最大堆，存放较小的一半数字\n    private PriorityQueue<Integer> maxHeap;\n    // 最小堆，存放较大的一半数字\n    private PriorityQueue<Integer> minHeap;\n\n    /** initialize your data structure here */\n    public MedianFinder() {\n        maxHeap = new PriorityQueue<>((a, b) -> b - a); // 最大堆：存放较小的一半数字\n        minHeap = new PriorityQueue<>(); // 最小堆：存放较大的一半数字\n    }\n\n    public void addNum(int num) {\n        // 如果最大堆为空或者当前数字小于最大堆堆顶元素，则放入最大堆\n        if (maxHeap.isEmpty() || num <= maxHeap.peek()) {\n            maxHeap.offer(num);\n            // 如果最大堆的大小超过最小堆，则将最大堆堆顶元素弹出放入最小堆\n            if (maxHeap.size() > minHeap.size() + 1) {\n                minHeap.offer(maxHeap.poll());\n            }\n        } else {\n            minHeap.offer(num); // 放入最小堆\n            // 如果最小堆的大小超过最大堆，则将最小堆堆顶元素弹出放入最大堆\n            if (minHeap.size() > maxHeap.size()) {\n                maxHeap.offer(minHeap.poll());\n            }\n        }\n    }\n\n    public double findMedian() {\n        // 如果最大堆和最小堆大小相同，则取出两个堆的堆顶元素求平均作为中位数\n        if (maxHeap.size() == minHeap.size()) {\n            return (maxHeap.peek() + minHeap.peek()) / 2.0;\n        } else {\n            return maxHeap.peek(); // 否则返回最大堆的堆顶元素作为中位数\n        }\n    }\n\n    public static void main(String[] args) {\n        MedianFinder medianFinder = new MedianFinder();\n        medianFinder.addNum(1);    // arr = [1]\n        medianFinder.addNum(2);    // arr = [1, 2]\n        System.out.println(medianFinder.findMedian()); // 返回 1.5 ((1 + 2) / 2)\n        medianFinder.addNum(3);    // arr = [1, 2, 3]\n        System.out.println(medianFinder.findMedian()); // 返回 2.0\n    }\n}\n```\n\n# 位运算\n## 67. 二进制求和\n\nhttps://leetcode.cn/problems/add-binary/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你两个二进制字符串 a 和 b ，以二进制字符串的形式返回它们的和。\n\n \n\n示例 1：\n\n输入:a = \"11\", b = \"1\"\n\n输出：\"100\"\n\n示例 2：\n\n输入：a = \"1010\", b = \"1011\"\n\n输出：\"10101\"\n \n\n提示：\n\n- 1 <= a.length, b.length <= 10^4\n- a 和 b 仅由字符 '0' 或 '1' 组成\n- 字符串如果不是 \"0\" ，就不含前导零\n\n```java\nclass Solution {\n    // 定义一个方法来实现二进制字符串相加\n    public String addBinary(String a, String b) {\n        StringBuilder sb = new StringBuilder(); // 用于构建结果字符串\n        int carry = 0; // 进位初始化为0\n        int i = a.length() - 1;\n        int j = b.length() - 1;\n\n        // 从字符串末尾开始遍历，同时考虑进位\n        while (i >= 0 || j >= 0 || carry > 0) {\n            int sum = carry; // 当前位的值等于进位\n\n            // 如果a还有位数，加上a的当前位\n            if (i >= 0) {\n                sum += a.charAt(i--) - '0'; // 将字符转换为数字并累加\n            }\n            // 如果b还有位数，加上b的当前位\n            if (j >= 0) {\n                sum += b.charAt(j--) - '0'; // 将字符转换为数字并累加\n            }\n\n            sb.insert(0, sum % 2); // 将当前位的值加入结果字符串的开头\n            carry = sum / 2; // 更新进位\n        }\n\n        return sb.toString(); // 返回结果字符串\n    }\n\n    public static void main(String[] args) {\n        Solution solution = new Solution();\n        String a = \"11\";\n        String b = \"1\";\n        System.out.println(solution.addBinary(a, b)); // 输出：\"100\"\n\n        a = \"1010\";\n        b = \"1011\";\n        System.out.println(solution.addBinary(a, b)); // 输出：\"10101\"\n    }\n}\n```\n\n## 190. 颠倒二进制位\nhttps://leetcode.cn/problems/reverse-bits/description/?envType=study-plan-v2&envId=top-interview-150\n\n颠倒给定的 32 位无符号整数的二进制位。\n\n提示：\n\n- 请注意，在某些语言（如 Java）中，没有无符号整数类型。在这种情况下，输入和输出都将被指定为有符号整数类型，并且不应影响您的实现，因为无论整数是有符号的还是无符号的，其内部的二进制表示形式都是相同的。\n- 在 Java 中，编译器使用二进制补码记法来表示有符号整数。因此，在 示例 2 中，输入表示有符号整数 -3，输出表示有符号整数 -1073741825。\n \n\n示例 1：\n\n输入：n = 00000010100101000001111010011100\n\n输出：964176192 (00111001011110000010100101000000)\n\n解释：输入的二进制串 00000010100101000001111010011100 表示无符号整数 43261596，\n     因此返回 964176192，其二进制表示形式为 00111001011110000010100101000000。\n\n示例 2：\n\n输入：n = 11111111111111111111111111111101\n\n输出：3221225471 (10111111111111111111111111111111)\n\n解释：输入的二进制串 11111111111111111111111111111101 表示无符号整数 4294967293，\n     因此返回 3221225471 其二进制表示形式为 10111111111111111111111111111111 。\n \n\n提示：\n\n- 输入是一个长度为 32 的二进制字符串\n\n```java\npublic class Solution {\n    // you need treat n as an unsigned value\n    public int reverseBits(int n) {\n        int result = 0;\n        // 对输入的32位无符号整数进行颠倒\n        for (int i = 0; i < 32; i++) {\n            result <<= 1; // 将结果向左移一位\n            result |= (n & 1); // 将n的最低位加到result的最低位\n            n >>= 1; // 移动n以处理下一位\n        }\n        return result;\n    }\n\n    public static void main(String[] args) {\n        Solution solution = new Solution();\n        int n1 = 43261596;\n        System.out.println(solution.reverseBits(n1)); // 输出：964176192\n\n        int n2 = -3;\n        System.out.println(solution.reverseBits(n2)); // 输出：3221225471\n    }\n}\n```\n\n## 191. 位1的个数\nhttps://leetcode.cn/problems/number-of-1-bits/description/?envType=study-plan-v2&envId=top-interview-150\n\n编写一个函数，获取一个正整数的二进制形式并返回其二进制表达式中 \n设置位的个数（也被称为汉明重量）。\n\n \n\n示例 1：\n\n输入：n = 11\n\n输出：3\n\n解释：输入的二进制串 1011 中，共有 3 个设置位。\n\n示例 2：\n\n输入：n = 128\n\n输出：1\n\n解释：输入的二进制串 10000000 中，共有 1 个设置位。\n\n示例 3：\n\n输入：n = 2147483645\n\n输出：30\n\n解释：输入的二进制串 11111111111111111111111111111101 中，共有 30 个设置位。\n \n\n提示：\n\n- 1 <= n <= 2^31 - 1\n\n```java\npublic class Solution {\n    // you need to treat n as an unsigned value\n    public int hammingWeight(int n) {\n        int count = 0;\n        while (n != 0) {\n            count += n & 1; // 检查n的最低位是否为1\n            n = n >>> 1; // 无符号右移，将n的最低位移出\n        }\n        return count;\n    }\n\n    public static void main(String[] args) {\n        Solution solution = new Solution();\n        int n1 = 11;\n        System.out.println(solution.hammingWeight(n1)); // 输出：3\n\n        int n2 = 128;\n        System.out.println(solution.hammingWeight(n2)); // 输出：1\n\n        int n3 = 2147483645;\n        System.out.println(solution.hammingWeight(n3)); // 输出：30\n    }\n}\n```\n\n## 136. 只出现一次的数字\nhttps://leetcode.cn/problems/single-number/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个 非空 整数数组 nums ，除了某个元素只出现一次以外，其余每个元素均出现两次。找出那个只出现了一次的元素。\n\n你必须设计并实现线性时间复杂度的算法来解决此问题，且该算法只使用常量额外空间。\n\n \n\n示例 1 ：\n\n输入：nums = [2,2,1]\n\n输出：1\n\n示例 2 ：\n\n输入：nums = [4,1,2,1,2]\n\n输出：4\n\n示例 3 ：\n\n输入：nums = [1]\n\n输出：1\n \n\n提示：\n\n- 1 <= nums.length <= 3 * 10^4\n- -3 * 10^4 <= nums[i] <= 3 * 10^4\n- 除了某个元素只出现一次以外，其余每个元素均出现两次。\n\n```java\npublic class Solution {\n    public int singleNumber(int[] nums) {\n        int result = 0;\n        for (int num : nums) {\n            result ^= num; // 利用异或运算找出只出现一次的元素\n        }\n        return result;\n    }\n\n    public static void main(String[] args) {\n        Solution solution = new Solution();\n\n        int[] nums1 = {2, 2, 1};\n        System.out.println(solution.singleNumber(nums1)); // 输出：1\n\n        int[] nums2 = {4, 1, 2, 1, 2};\n        System.out.println(solution.singleNumber(nums2)); // 输出：4\n\n        int[] nums3 = {1};\n        System.out.println(solution.singleNumber(nums3)); // 输出：1\n    }\n}\n```\n\n## 137. 只出现一次的数字 II\nhttps://leetcode.cn/problems/single-number-ii/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个整数数组 nums ，除某个元素仅出现 一次 外，其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。\n\n你必须设计并实现线性时间复杂度的算法且使用常数级空间来解决此问题。\n\n \n\n示例 1：\n\n输入：nums = [2,2,3,2]\n\n输出：3\n\n示例 2：\n\n输入：nums = [0,1,0,1,0,1,99]\n\n输出：99\n \n\n提示：\n\n- 1 <= nums.length <= 3 * 10^4\n- -2^31 <= nums[i] <= 2^31 - 1\n- nums 中，除某个元素仅出现 一次 外，其余每个元素都恰出现 三次\n\n```java\npublic class Solution {\n    public int singleNumber(int[] nums) {\n        // 初始化统计每一位出现次数的数组\n        int[] counts = new int[32];\n        \n        // 统计每个数字中每一位出现的次数\n        for (int num : nums) {\n            for (int i = 0; i < 32; i++) {\n                counts[i] += (num >> i) & 1; // 统计第i位上出现的次数\n            }\n        }\n        \n        int result = 0;\n        // 根据统计结果组合出只出现一次的元素\n        for (int i = 0; i < 32; i++) {\n            result |= (counts[i] % 3) << i; // 将每一位的统计结果组合起来\n        }\n        \n        return result;\n    }\n\n    public static void main(String[] args) {\n        Solution solution = new Solution();\n\n        int[] nums1 = {2, 2, 3, 2};\n        System.out.println(solution.singleNumber(nums1)); // 输出：3\n\n        int[] nums2 = {0, 1, 0, 1, 0, 1, 99};\n        System.out.println(solution.singleNumber(nums2)); // 输出：99\n    }\n}\n```\n\n## 201. 数字范围按位与\nhttps://leetcode.cn/problems/bitwise-and-of-numbers-range/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你两个整数 left 和 right ，表示区间 [left, right] ，返回此区间内所有数字 按位与 的结果（包含 left 、right 端点）。\n\n \n\n示例 1：\n\n输入：left = 5, right = 7\n\n输出：4\n\n示例 2：\n\n输入：left = 0, right = 0\n\n输出：0\n\n示例 3：\n\n输入：left = 1, right = 2147483647\n\n输出：0\n \n\n提示：\n\n- 0 <= left <= right <= 2^31 - 1\n\n```java\npublic class Solution {\n    public int rangeBitwiseAnd(int left, int right) {\n        // 初始化位移计数器\n        int shift = 0;\n        \n        // 找到 left 和 right 共同的前缀\n        while (left < right) {\n            left >>= 1; // 将 left 向右位移\n            right >>= 1; // 将 right 向右位移\n            shift++; // 记录位移次数\n        }\n        \n        // 将共同前缀左移（以 0 填充）并返回结果\n        return left << shift;\n    }\n\n    public static void main(String[] args) {\n        Solution solution = new Solution();\n\n        int left1 = 5, right1 = 7;\n        System.out.println(solution.rangeBitwiseAnd(left1, right1)); // 输出：4\n\n        int left2 = 0, right2 = 0;\n        System.out.println(solution.rangeBitwiseAnd(left2, right2)); // 输出：0\n\n        int left3 = 1, right3 = 2147483647;\n        System.out.println(solution.rangeBitwiseAnd(left3, right3)); // 输出：0\n    }\n}\n```\n\n# 数学\n## 9. 回文数\nhttps://leetcode.cn/problems/palindrome-number/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个整数 x ，如果 x 是一个回文整数，返回 true ；否则，返回 false 。\n\n回文数是指正序（从左向右）和倒序（从右向左）读都是一样的整数。\n\n例如，121 是回文，而 123 不是。\n \n\n示例 1：\n\n输入：x = 121\n\n输出：true\n\n示例 2：\n\n输入：x = -121\n\n输出：false\n\n解释：从左向右读, 为 -121 。 从右向左读, 为 121- 。因此它不是一个回文数。\n\n示例 3：\n\n输入：x = 10\n\n输出：false\n\n解释：从右向左读, 为 01 。因此它不是一个回文数。\n \n\n提示：\n\n- -2^31 <= x <= 2^31 - 1\n\n```java\npublic class Solution {\n    public boolean isPalindrome(int x) {\n        // 处理负数和以0结尾的数字，它们不可能是回文数\n        if (x < 0 || (x % 10 == 0 && x != 0)) {\n            return false;\n        }\n\n        int reversed = 0;\n        while (x > reversed) {\n            reversed = reversed * 10 + x % 10;\n            x /= 10;\n        }\n\n        // 根据整数的位数是奇数还是偶数判断是否为回文数\n        return x == reversed || x == reversed / 10;\n    }\n\n    public static void main(String[] args) {\n        Solution solution = new Solution();\n\n        int x1 = 121;\n        System.out.println(solution.isPalindrome(x1)); // 输出：true\n\n        int x2 = -121;\n        System.out.println(solution.isPalindrome(x2)); // 输出：false\n\n        int x3 = 10;\n        System.out.println(solution.isPalindrome(x3)); // 输出：false\n    }\n}\n```\n\n## 66. 加一\nhttps://leetcode.cn/problems/plus-one/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个由 整数 组成的 非空 数组所表示的非负整数，在该数的基础上加一。\n\n最高位数字存放在数组的首位， 数组中每个元素只存储单个数字。\n\n你可以假设除了整数 0 之外，这个整数不会以零开头。\n\n \n\n示例 1：\n\n输入：digits = [1,2,3]\n\n输出：[1,2,4]\n\n解释：输入数组表示数字 123。\n\n示例 2：\n\n输入：digits = [4,3,2,1]\n\n输出：[4,3,2,2]\n\n解释：输入数组表示数字 4321。\n\n示例 3：\n\n输入：digits = [0]\n\n输出：[1]\n \n\n提示：\n\n- 1 <= digits.length <= 100\n- 0 <= digits[i] <= 9\n\n```java\nimport java.util.Arrays;\n\npublic class Solution {\n    public int[] plusOne(int[] digits) {\n        int n = digits.length;\n\n        // 从数组末尾开始向前遍历\n        for (int i = n - 1; i >= 0; i--) {\n            if (digits[i] < 9) {\n                digits[i]++;\n                return digits;\n            }\n            // 当数字为 9 时，当前位置变为 0，继续向前进位\n            digits[i] = 0;\n        }\n\n        // 如果在最高位还需要进位，数组长度需要加一\n        int[] newNumber = new int[n + 1];\n        newNumber[0] = 1;\n\n        return newNumber;\n    }\n\n    public static void main(String[] args) {\n        Solution solution = new Solution();\n\n        int[] digits1 = {1, 2, 3};\n        System.out.println(Arrays.toString(solution.plusOne(digits1))); // 输出：[1, 2, 4]\n\n        int[] digits2 = {4, 3, 2, 1};\n        System.out.println(Arrays.toString(solution.plusOne(digits2))); // 输出：[4, 3, 2, 2]\n\n        int[] digits3 = {0};\n        System.out.println(Arrays.toString(solution.plusOne(digits3))); // 输出：[1]\n    }\n}\n```\n\n## 172. 阶乘后的零\nhttps://leetcode.cn/problems/factorial-trailing-zeroes/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个整数 n ，返回 n! 结果中尾随零的数量。\n\n提示 n! = n * (n - 1) * (n - 2) * ... * 3 * 2 * 1\n\n \n\n示例 1：\n\n输入：n = 3\n\n输出：0\n\n解释：3! = 6 ，不含尾随 0\n\n示例 2：\n\n输入：n = 5\n\n输出：1\n\n解释：5! = 120 ，有一个尾随 0\n\n示例 3：\n\n输入：n = 0\n\n输出：0\n \n\n提示：\n\n0 <= n <= 10^4\n\n```java\n计算阶乘后的零的数量其实可以看成是统计乘法过程中因为包含了2和5相乘而产生的0的个数。因为每对2和5相乘会产生一个0，而在阶乘的乘法过程中，2的个数肯定比5多，所以计算5的个数就可以得到0的个数。\n\npublic class Solution {\n    public int trailingZeroes(int n) {\n        int count = 0;\n        \n        // 统计包含的5的个数\n        while (n > 0) {\n            n /= 5;\n            count += n;\n        }\n        \n        return count;\n    }\n\n    public static void main(String[] args) {\n        Solution solution = new Solution();\n\n        int n1 = 3;\n        System.out.println(solution.trailingZeroes(n1)); // 输出：0\n\n        int n2 = 5;\n        System.out.println(solution.trailingZeroes(n2)); // 输出：1\n\n        int n3 = 0;\n        System.out.println(solution.trailingZeroes(n3)); // 输出：0\n    }\n}\n\n```\n\n## 69. x 的平方根 \nhttps://leetcode.cn/problems/sqrtx/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个非负整数 x ，计算并返回 x 的 算术平方根 。\n\n由于返回类型是整数，结果只保留 整数部分 ，小数部分将被 舍去 。\n\n注意：不允许使用任何内置指数函数和算符，例如 pow(x, 0.5) 或者 x ** 0.5 。\n\n \n\n示例 1：\n\n输入：x = 4\n\n输出：2\n\n示例 2：\n\n输入：x = 8\n\n输出：2\n\n解释：8 的算术平方根是 2.82842..., 由于返回类型是整数，小数部分将被舍去。\n \n\n提示：\n\n- 0 <= x <= 2^31 - 1\n\n\n```java\nclass Solution {\n    public int mySqrt(int x) {\n        // 初始化左右边界和结果\n        int left = 0;\n        int right = x;\n        int res = -1;\n\n        // 二分查找\n        while (left <= right) {\n            // 计算中间值\n            int mid = left + (right - left) / 2;\n\n            // 如果中间值的平方小于等于 x，则更新结果为当前中间值，并将左边界向右移动一位\n            if ((long) mid * mid <= x) {\n                res = mid;\n                left = mid + 1;\n            } else {\n                // 如果中间值的平方大于 x，则将右边界向左移动一位\n                right = mid - 1;\n            }\n        }\n\n        return res; // 返回结果\n    }\n}\n```\n\n## 50. Pow(x, n)\nhttps://leetcode.cn/problems/powx-n/description/?envType=study-plan-v2&envId=top-interview-150\n\n实现 pow(x, n) ，即计算 x 的整数 n 次幂函数（即，xn ）。\n\n \n\n示例 1：\n\n输入：x = 2.00000, n = 10\n\n输出：1024.00000\n\n示例 2：\n\n输入：x = 2.10000, n = 3\n\n输出：9.26100\n\n示例 3：\n\n输入：x = 2.00000, n = -2\n\n输出：0.25000\n\n解释：2-2 = 1/22 = 1/4 = 0.25\n \n\n提示：\n\n- -100.0 < x < 100.0\n- -2^31 <= n <= 2^31-1\n- n 是一个整数\n- 要么 x 不为零，要么 n > 0 。\n- -10^4 <= xn <= 10^4\n\n```java\npublic class Solution {\n    public double myPow(double x, int n) {\n        if (n == 0) {\n            return 1.0;\n        }\n\n        // 转成 long 类型来避免整数溢出\n        long N = n;\n        \n        // 如果n为负数，将x变为1/x，n取相反数\n        if (N < 0) {\n            x = 1 / x;\n            N = -N;\n        }\n\n        double ans = 1.0;\n        double cur_product = x;\n\n        for (long i = N; i > 0; i /= 2) {\n            if (i % 2 == 1) {\n                ans *= cur_product;\n            }\n            cur_product *= cur_product;\n        }\n\n        return ans;\n    }\n\n    public static void main(String[] args) {\n        Solution solution = new Solution();\n\n        double x1 = 2.00000;\n        int n1 = 10;\n        System.out.println(solution.myPow(x1, n1)); // 输出：1024.00000\n\n        double x2 = 2.10000;\n        int n2 = 3;\n        System.out.println(solution.myPow(x2, n2)); // 输出：9.26100\n\n        double x3 = 2.00000;\n        int n3 = -2;\n        System.out.println(solution.myPow(x3, n3)); // 输出：0.25000\n    }\n}\n```\n\n## 149. 直线上最多的点数\nhttps://leetcode.cn/problems/max-points-on-a-line/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个数组 points ，其中 points[i] = [xi, yi] 表示 X-Y 平面上的一个点。求最多有多少个点在同一条直线上。\n\n \n\n示例 1：\n\n![alt text](../img/数据结构和算法/直线上最多的点数1.png)\n\n输入：points = [[1,1],[2,2],[3,3]]\n\n输出：3\n\n示例 2：\n\n![alt text](../img/数据结构和算法/直线上最多的点数2.png)\n\n输入：points = [[1,1],[3,2],[5,3],[4,1],[2,3],[1,4]]\n\n输出：4\n \n\n提示：\n\n- 1 <= points.length <= 300\n- points[i].length == 2\n- -10^4 <= xi, yi <= 10^4\n- points 中的所有点 互不相同\n\n```java\nimport java.util.HashMap;\nimport java.util.Map;\n\nclass Solution {\n    public int maxPoints(int[][] points) {\n        int n = points.length;\n        if (n <= 2) {\n            return n; // 如果点的数量小于等于2，直接返回点的数量\n        }\n\n        int maxPoints = 1; // 初始化最大点数为1，至少有一个点在同一直线上\n        for (int i = 0; i < n - 1; i++) {\n            Map<String, Integer> slopeCount = new HashMap<>(); // 用于记录当前点i与其他点斜率的Map\n            int samePointCount = 0; // 记录与点i相同位置的点数量\n            int localMax = 0; // 当前最大的点数\n\n            for (int j = i + 1; j < n; j++) {\n                int x1 = points[i][0], y1 = points[i][1];\n                int x2 = points[j][0], y2 = points[j][1];\n\n                if (x1 == x2 && y1 == y2) { // 如果两点相同\n                    samePointCount++; // 增加相同点数量\n                } else {\n                    String slope = getSlope(x1, y1, x2, y2); // 计算斜率\n                    slopeCount.put(slope, slopeCount.getOrDefault(slope, 1) + 1); // 存储斜率及对应的点数\n                    localMax = Math.max(localMax, slopeCount.get(slope)); // 更新当前最大点数\n                }\n                // 更新通过点i的直线上的最大点数\n                maxPoints = Math.max(maxPoints, localMax + samePointCount);\n            }\n        }\n\n        return maxPoints; // 返回最终结果\n    }\n\n    // 计算两点的斜率，并以字符串形式返回，避免精度问题\n    private String getSlope(int x1, int y1, int x2, int y2) {\n        int dx = x2 - x1;\n        int dy = y2 - y1;\n\n        int gcd = getGCD(dx, dy);\n        dx /= gcd;\n        dy /= gcd;\n\n        return dx + \"/\" + dy; // 返回以字符串表示的斜率\n    }\n\n    // 计算最大公约数\n    private int getGCD(int a, int b) {\n        if (b == 0) {\n            return a; // 辗转相除法计算最大公约数\n        }\n        return getGCD(b, a % b);\n    }\n\n    public static void main(String[] args) {\n        Solution solution = new Solution();\n\n        int[][] points1 = {{1, 1}, {2, 2}, {3, 3}};\n        System.out.println(solution.maxPoints(points1)); // 输出：3\n\n        int[][] points2 = {{1, 1}, {3, 2}, {5, 3}, {4, 1}, {2, 3}, {1, 4}};\n        System.out.println(solution.maxPoints(points2)); // 输出：4\n    }\n}\n```\n\n# 一维动态规划\n## 70. 爬楼梯\nhttps://leetcode.cn/problems/climbing-stairs/description/?envType=study-plan-v2&envId=top-interview-150\n\n假设你正在爬楼梯。需要 n 阶你才能到达楼顶。\n\n每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢？\n\n \n\n示例 1：\n\n输入：n = 2\n\n输出：2\n\n解释：有两种方法可以爬到楼顶。\n1. 1 阶 + 1 阶\n2. 2 阶\n\n示例 2：\n\n输入：n = 3\n\n输出：3\n\n解释：有三种方法可以爬到楼顶。\n1. 1 阶 + 1 阶 + 1 阶\n2. 1 阶 + 2 阶\n3. 2 阶 + 1 阶\n \n\n提示：\n\n- 1 <= n <= 45\n\n```java\nclass Solution {\n    public int climbStairs(int n) {\n        if (n == 1) {\n            return 1; // 如果只有1阶台阶，只有一种方法可以到达楼顶\n        }\n\n        int[] dp = new int[n + 1]; // 创建一个数组来存储到每个台阶的方法数\n        dp[1] = 1; // 到第一阶台阶只有1种方法\n        dp[2] = 2; // 到第二阶台阶有两种方法\n\n        for (int i = 3; i <= n; i++) {\n            dp[i] = dp[i - 1] + dp[i - 2]; // 当前台阶的方法数是前两个台阶之和\n        }\n\n        return dp[n]; // 返回到达第n阶台阶的方法数\n    }\n\n    public static void main(String[] args) {\n        Solution solution = new Solution();\n\n        // Test Cases\n        int n1 = 2;\n        System.out.println(solution.climbStairs(n1)); // Output: 2\n\n        int n2 = 3;\n        System.out.println(solution.climbStairs(n2)); // Output: 3\n    }\n}\n```\n\n## 198. 打家劫舍\n你是一个专业的小偷，计划偷窃沿街的房屋。每间房内都藏有一定的现金，影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统，如果两间相邻的房屋在同一晚上被小偷闯入，系统会自动报警。\n\n给定一个代表每个房屋存放金额的非负整数数组，计算你 不触动警报装置的情况下 ，一夜之内能够偷窃到的最高金额。\n\n \n\n示例 1：\n\n输入：[1,2,3,1]\n\n输出：4\n\n解释：偷窃 1 号房屋 (金额 = 1) ，然后偷窃 3 号房屋 (金额 = 3)。\n     偷窃到的最高金额 = 1 + 3 = 4 。\n\n示例 2：\n\n输入：[2,7,9,3,1]\n\n输出：12\n\n解释：偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9)，接着偷窃 5 号房屋 (金额 = 1)。\n     偷窃到的最高金额 = 2 + 9 + 1 = 12 。\n \n\n提示：\n\n- 1 <= nums.length <= 100\n- 0 <= nums[i] <= 400\n\n```java\nclass Solution {\n    public int rob(int[] nums) {\n        if (nums == null || nums.length == 0) {\n            return 0; // 处理边界情况，如果数组为空则直接返回0\n        }\n\n        if (nums.length == 1) {\n            return nums[0]; // 只有一间房屋时直接返回该房屋金额\n        }\n\n        int[] dp = new int[nums.length]; // 创建一个数组用来存储偷盗到每个房屋的最大金额\n\n        dp[0] = nums[0]; // 初始化第一间房屋的偷盗金额\n        dp[1] = Math.max(nums[0], nums[1]); // 初始化第二间房屋的偷盗金额\n\n        for (int i = 2; i < nums.length; i++) {\n            dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]); // 当前房屋可以选择偷窃或者不偷窃\n        }\n\n        return dp[nums.length - 1]; // 返回最后一间房屋的最大偷盗金额\n    }\n\n    public static void main(String[] args) {\n        Solution solution = new Solution();\n\n        // Test Cases\n        int[] nums1 = {1, 2, 3, 1};\n        System.out.println(solution.rob(nums1)); // Output: 4\n\n        int[] nums2 = {2, 7, 9, 3, 1};\n        System.out.println(solution.rob(nums2)); // Output: 12\n    }\n}\n```\n\n## 139. 单词拆分\nhttps://leetcode.cn/problems/word-break/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true。\n\n注意：不要求字典中出现的单词全部都使用，并且字典中的单词可以重复使用。\n\n \n\n示例 1：\n\n输入: s = \"leetcode\", wordDict = [\"leet\", \"code\"]\n\n输出: true\n\n解释: 返回 true 因为 \"leetcode\" 可以由 \"leet\" 和 \"code\" 拼接成。\n\n示例 2：\n\n输入: s = \"applepenapple\", wordDict = [\"apple\", \"pen\"]\n\n输出: true\n\n解释: 返回 true 因为 \"applepenapple\" 可以由 \"apple\" \"pen\" \"apple\" 拼接成。\n     注意，你可以重复使用字典中的单词。\n\n示例 3：\n\n输入: s = \"catsandog\", wordDict = [\"cats\", \"dog\", \"sand\", \"and\", \"cat\"]\n\n输出: false\n \n\n提示：\n\n- 1 <= s.length <= 300\n- 1 <= wordDict.length <= 1000\n- 1 <= wordDict[i].length <= 20\n- s 和 wordDict[i] 仅由小写英文字母组成\n- wordDict 中的所有字符串 互不相同\n\n```java\nimport java.util.List;\nimport java.util.Set;\n\npublic class Solution {\n    public boolean wordBreak(String s, List<String> wordDict) {\n        Set<String> wordSet = new HashSet<>(wordDict);\n        int n = s.length();\n        \n        // dp[i] 表示前i个字符能否被拆分\n        boolean[] dp = new boolean[n + 1];\n        dp[0] = true;  // 空字符串可以被拆分\n        \n        for (int i = 1; i <= n; i++) {\n            for (int j = 0; j < i; j++) {\n                if (dp[j] && wordSet.contains(s.substring(j, i))) {\n                    dp[i] = true;\n                    break;\n                }\n            }\n        }\n\n        return dp[n];\n    }\n\n    public static void main(String[] args) {\n        Solution solution = new Solution();\n\n        // Test Cases\n        String s1 = \"leetcode\";\n        List<String> wordDict1 = List.of(\"leet\", \"code\");\n        System.out.println(solution.wordBreak(s1, wordDict1));  // Output: true\n\n        String s2 = \"applepenapple\";\n        List<String> wordDict2 = List.of(\"apple\", \"pen\");\n        System.out.println(solution.wordBreak(s2, wordDict2));  // Output: true\n\n        String s3 = \"catsandog\";\n        List<String> wordDict3 = List.of(\"cats\", \"dog\", \"sand\", \"and\", \"cat\");\n        System.out.println(solution.wordBreak(s3, wordDict3));  // Output: false\n    }\n}\n```\n\n## 322. 零钱兑换\nhttps://leetcode.cn/problems/coin-change/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个整数数组 coins ，表示不同面额的硬币；以及一个整数 amount ，表示总金额。\n\n计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额，返回 -1 。\n\n你可以认为每种硬币的数量是无限的。\n\n \n\n示例 1：\n\n输入：coins = [1, 2, 5], amount = 11\n\n输出：3 \n\n解释：11 = 5 + 5 + 1\n\n示例 2：\n\n输入：coins = [2], amount = 3\n\n输出：-1\n\n示例 3：\n\n输入：coins = [1], amount = 0\n\n输出：0\n \n\n提示：\n\n- 1 <= coins.length <= 12\n- 1 <= coins[i] <= 2^31 - 1\n- 0 <= amount <= 10^4\n\n```java\npublic class Solution {\n    public int coinChange(int[] coins, int amount) {\n        // 创建一个数组dp，dp[i]表示凑成金额i所需的最少硬币数\n        int[] dp = new int[amount + 1];\n        // 初始时，凑成金额为0需要0个硬币，其他金额需要的硬币数初始化为amount+1表示不可达\n        Arrays.fill(dp, amount + 1);\n        dp[0] = 0; // 金额为0时，不需要硬币\n\n        // 动态规划过程，遍历金额从1到amount\n        for (int i = 1; i <= amount; i++) {\n            // 对于每个金额i，遍历硬币面额\n            for (int coin : coins) {\n                // 如果当前硬币面额小于等于金额i，并且使用该硬币可以减少所需的硬币数，则更新dp[i]的值\n                if (coin <= i) {\n                    dp[i] = Math.min(dp[i], dp[i - coin] + 1);\n                }\n            }\n        }\n\n        // 如果dp[amount]仍然为初始值amount+1，表示无法凑成总金额，返回-1；否则返回dp[amount]，即最少硬币数\n        return dp[amount] == amount + 1 ? -1 : dp[amount];\n    }\n}\n```\n\n代码解析：\n\n- 我们创建一个数组dp来存储每个金额所需的最少硬币数。初始时，所有金额的最少硬币数都设为amount+1，表示不可达状态。只有金额为0时，需要的硬币数为0。\n- 然后我们遍历每个金额（从1到amount），对于每个金额，我们尝试使用每一种硬币去凑成这个金额。如果可以凑成并且使用该硬币可以减少所需的硬币数，我们就更新dp数组的值。\n- 最后，我们检查dp[amount]的值。如果它仍然是初始值amount+1，表示无法凑成总金额，返回-1；否则返回dp[amount]的值，即最少硬币数。\n\n## 300. 最长递增子序列\nhttps://leetcode.cn/problems/longest-increasing-subsequence/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个整数数组 nums ，找到其中最长严格递增子序列的长度。\n\n子序列 是由数组派生而来的序列，删除（或不删除）数组中的元素而不改变其余元素的顺序。例如，[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的\n子序列\n。\n\n \n示例 1：\n\n输入：nums = [10,9,2,5,3,7,101,18]\n\n输出：4\n\n解释：最长递增子序列是 [2,3,7,101]，因此长度为 4 。\n\n示例 2：\n\n输入：nums = [0,1,0,3,2,3]\n\n输出：4\n\n示例 3：\n\n输入：nums = [7,7,7,7,7,7,7]\n\n输出：1\n \n\n提示：\n\n- 1 <= nums.length <= 2500\n- -10^4 <= nums[i] <= 10^4\n\n```java\npublic class Solution {\n    public int lengthOfLIS(int[] nums) {\n        // 创建一个与nums数组长度相同的dp数组，dp[i]表示以nums[i]结尾的最长递增子序列的长度\n        int[] dp = new int[nums.length];\n        int maxLength = 1;  // 最长递增子序列的初始长度至少为1（单个元素）\n        \n        for (int i = 0; i < nums.length; i++) {\n            dp[i] = 1;  // 每个元素自身可以形成一个长度为1的递增子序列\n            \n            // 遍历之前的元素，寻找可以连接到当前元素的更长递增子序列\n            for (int j = 0; j < i; j++) {\n                if (nums[i] > nums[j]) {  // 如果当前元素大于前面的元素，说明可以连接形成一个更长的递增子序列\n                    dp[i] = Math.max(dp[i], dp[j] + 1);  // 更新dp[i]的值\n                }\n            }\n            \n            // 更新最长递增子序列的长度\n            maxLength = Math.max(maxLength, dp[i]);\n        }\n        \n        return maxLength;  // 返回最长递增子序列的长度\n    }\n}\n```\n\n# 多维动态规划\n## 120. 三角形最小路径和\nhttps://leetcode.cn/problems/triangle/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个三角形 triangle ，找出自顶向下的最小路径和。\n\n每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说，如果正位于当前行的下标 i ，那么下一步可以移动到下一行的下标 i 或 i + 1 。\n\n \n\n示例 1：\n\n输入：triangle = [[2],[3,4],[6,5,7],[4,1,8,3]]\n\n输出：11\n\n解释：如下面简图所示：\n\n   2\n\n  3 4\n\n 6 5 7\n\n4 1 8 3\n\n自顶向下的最小路径和为 11（即，2 + 3 + 5 + 1 = 11）。\n\n示例 2：\n\n输入：triangle = [[-10]]\n\n输出：-10\n \n\n提示：\n\n- 1 <= triangle.length <= 200\n- triangle[0].length == 1\n- triangle[i].length == triangle[i - 1].length + 1\n- -10^4 <= triangle[i][j] <= 10^4\n\n\n```java\nclass Solution {\n    // 定义公共方法minimumTotal，参数是一个二维列表triangle，表示三角形中的数字。\n    public int minimumTotal(List<List<Integer>> triangle) {\n        // 判断输入的三角形是否为空或者其大小为0，如果是则返回0（这里没有路径可走）。\n        if(triangle == null || triangle.size() == 0){\n            return 0;\n        }\n        \n        int m = triangle.size(); // 获取三角形的高度（行数）\n        int[][] dp = new int[m+1][m+1]; // 创建一个二维数组dp，用于存储中间计算结果。这里比三角形多一行和一列是为了方便计算边界情况。\n        \n        // 从三角形的底部开始向上遍历每一行（每一层）\n        for(int i= m-1; i >= 0; i--){\n            List<Integer> cur = triangle.get(i); // 获取当前行的数字列表\n            for(int j = 0; j < cur.size(); j++){ // 遍历当前行的每一个数字\n                // 对于当前数字，它的最小路径和等于它下方的两个数字（下一层的两个相邻格子）中的较小值加上当前数字自身。这里的公式基于动态规划的思路。dp[i+1][j]、dp[i+1][j+1]表示下一行中两个相邻格子的最小路径和。\n                dp[i][j] = Math.min(dp[i+1][j], dp[i+1][j+1]) + cur.get(j); \n            }\n        }\n        // 最终答案存储在dp[0][0]中，表示从三角形顶端到底部的最小路径和。返回结果。\n        return dp[0][0];\n    }\n}\n```\n\n## 64. 最小路径和\n给定一个包含非负整数的 m x n 网格 grid ，请找出一条从左上角到右下角的路径，使得路径上的数字总和为最小。\n\n说明：每次只能向下或者向右移动一步。\n\n \n\n示例 1：\n\n\n输入：grid = [[1,3,1],[1,5,1],[4,2,1]]\n\n输出：7\n\n解释：因为路径 1→3→1→1→1 的总和最小。\n\n示例 2：\n\n输入：grid = [[1,2,3],[4,5,6]]\n\n输出：12\n \n\n提示：\n\n- m == grid.length\n- n == grid[i].length\n- 1 <= m, n <= 200\n- 0 <= grid[i][j] <= 200\n\n```java\n/**\n * 解决最小路径和问题的类。\n * 该类提供了一个方法来计算在一个给定的二维网格中，从左上角到右下角的最小路径和。\n * 网格中的每个单元格包含一个非负整数，路径和是沿途经过的单元格值的总和。\n */\nclass Solution {\n    /**\n     * 计算最小路径和。\n     * 使用动态规划方法，从左上角开始，逐步向右下角计算到达每个单元格的最小路径和。\n     * \n     * @param grid 二维网格，包含非负整数。\n     * @return 返回从左上角到右下角的最小路径和。\n     */\n    public int minPathSum(int[][] grid) {\n        // 获取网格的行数和列数\n        int m = grid.length;\n        int n = grid[0].length;\n\n        // 初始化一个数组用于存储到达每个列的最小路径和\n        int[] dp = new int[n];\n\n        // 遍历网格的每一行\n        for(int i=0;i<m;i++){\n            // 遍历每一行的每一列\n            for(int j=0;j<n;j++){\n                // 如果当前列是第一列，只能从上方到达，所以当前的最小路径和就是上方的最小路径和\n                if(j==0){\n                    dp[j] = dp[j];\n                }else if(i==0){ // 如果当前行是第一行，只能从左方到达，所以当前的最小路径和是左边的最小路径和\n                    dp[j] = dp[j-1];\n                }else{ // 如果既不在第一行也不在第一列，当前的最小路径和是从左边或上方到达的最小路径和中的较小值\n                    dp[j] = Math.min(dp[j],dp[j-1]);\n                }\n                // 将当前单元格的值加到到达当前单元格的最小路径和上\n                dp[j]+=grid[i][j];\n            }\n        }\n        // 返回到达右下角的最小路径和\n        return dp[n-1];\n    }\n}\n```\n\n## 63. 不同路径 II\nhttps://leetcode.cn/problems/unique-paths-ii/description/?envType=study-plan-v2&envId=top-interview-150\n\n一个机器人位于一个 m x n 网格的左上角 （起始点在下图中标记为 “Start” ）。\n\n机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角（在下图中标记为 “Finish”）。\n\n现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径？\n\n网格中的障碍物和空位置分别用 1 和 0 来表示。\n\n \n\n示例 1：\n\n![alt text](../img/数据结构和算法/不同路径2-1.png)\n\n输入：obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]\n\n输出：2\n\n解释：3x3 网格的正中间有一个障碍物。\n\n从左上角到右下角一共有 2 条不同的路径：\n\n1. 向右 -> 向右 -> 向下 -> 向下\n\n2. 向下 -> 向下 -> 向右 -> 向右\n\n示例 2：\n\n![alt text](../img/数据结构和算法/不同路径2-2.png)\n\n输入：obstacleGrid = [[0,1],[0,0]]\n\n输出：1\n \n\n提示：\n\n- m == obstacleGrid.length\n- n == obstacleGrid[i].length\n- 1 <= m, n <= 100\n- obstacleGrid[i][j] 为 0 或 1\n\n```java\n/**\n * 计算有障碍物的网格中从左上角到右下角的唯一路径数量\n *\n * @param obstacleGrid 二维整数数组，表示网格中的障碍物分布。1 表示障碍物，0 表示可通行。\n * @return 返回一个整数，表示从左上角到右下角的唯一路径数量。如果路径被障碍物阻断，则返回 0。\n */\nclass Solution {\n    /**\n     * 计算有障碍物的网格中从左上角到右下角的唯一路径数量\n     *\n     * @param obstacleGrid 二维整数数组，表示网格中的障碍物分布。1 表示障碍物，0 表示可通行。\n     * @return 返回一个整数，表示从左上角到右下角的唯一路径数量。如果路径被障碍物阻断，则返回 0。\n     */\n    public int uniquePathsWithObstacles(int[][] obstacleGrid) {\n        // 获取网格的行数和列数\n        int m = obstacleGrid.length;\n        int n = obstacleGrid[0].length;\n        \n        // 初始化动态规划数组，比实际网格多一行一列，用于边界处理\n        int[][] dp = new int[m+1][n+1];\n\n        // 初始化第一列的路径数\n        for(int i=0; i<m; i++){\n            if(obstacleGrid[i][0] == 1){\n                break;\n            }\n            dp[i][0] = 1;\n        }\n\n        // 初始化第一行的路径数\n        for(int i=0; i<n; i++){\n            if(obstacleGrid[0][i] == 1){\n                break;\n            }\n            dp[0][i] = 1;\n        }\n\n        // 动态规划计算每一步的路径数\n        for(int i=1; i<m; i++){\n            for(int j = 1; j<n; j++){\n                // 如果当前位置没有障碍物，路径数等于上方和左方的路径数之和\n                dp[i][j] = obstacleGrid[i][j]==1 ? 0 : dp[i][j-1] + dp[i-1][j];\n            }\n        }\n\n        // 返回右下角的路径数\n        return dp[m-1][n-1];\n    }\n}\n```\n\n## 5. 最长回文子串\nhttps://leetcode.cn/problems/longest-palindromic-substring/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个字符串 s，找到 s 中最长的 回文子串。\n\n \n\n示例 1：\n\n输入：s = \"babad\"\n\n输出：\"bab\"\n\n解释：\"aba\" 同样是符合题意的答案。\n\n示例 2：\n\n输入：s = \"cbbd\"\n\n输出：\"bb\"\n \n\n提示：\n\n- 1 <= s.length <= 1000\n- s 仅由数字和英文字母组成\n\n```java\nclass Solution {\n    public String longestPalindrome(String s) {\n        String res= \"\";\n        for(int i =0;i<s.length();i++){\n            String s1 = check(s,i,i);\n            String s2= check(s,i,i+1);\n\n            res = res.length() > s1.length() ? res:s1;\n            res = res.length() > s2.length() ? res:s2;\n        }\n        return res;\n    }\n\n    public String check(String s,int left,int right){\n        while(left>=0 && right<s.length()){\n            if(s.charAt(left) == s.charAt(right)){\n                 left--;\n                right++; \n            }else{\n                break;\n            }\n            \n        }\n        return s.substring(left+1,right);\n    }\n}\n```\n\n## 97. 交错字符串\nhttps://leetcode.cn/problems/interleaving-string/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定三个字符串 s1、s2、s3，请你帮忙验证 s3 是否是由 s1 和 s2 交错 组成的。\n\n两个字符串 s 和 t 交错 的定义与过程如下，其中每个字符串都会被分割成若干 非空 子字符串：\n\n- s = s1 + s2 + ... + sn\n- t = t1 + t2 + ... + tm\n- |n - m| <= 1\n- 交错 是 s1 + t1 + s2 + t2 + s3 + t3 + ... 或者 t1 + s1 + t2 + s2 + t3 + s3 + ...\n\n注意：a + b 意味着字符串 a 和 b 连接。\n\n \n\n示例 1：\n\n\n输入：s1 = \"aabcc\", s2 = \"dbbca\", s3 = \"aadbbcbcac\"\n\n输出：true\n\n示例 2：\n\n输入：s1 = \"aabcc\", s2 = \"dbbca\", s3 = \"aadbbbaccc\"\n\n输出：false\n\n示例 3：\n\n输入：s1 = \"\", s2 = \"\", s3 = \"\"\n\n输出：true\n \n\n提示：\n\n- 0 <= s1.length, s2.length <= 100\n- 0 <= s3.length <= 200\n- s1、s2、和 s3 都由小写英文字母组成\n\n```java\n/**\n * 解决方案类，用于检查两个字符串是否可以通过交错组合成另一个字符串。\n */\nclass Solution {\n    /**\n     * 检查两个字符串s1和s2是否可以通过交错组合成字符串s3。\n     * \n     * @param s1 第一个字符串\n     * @param s2 第二个字符串\n     * @param s3 目标字符串，需要验证是否由s1和s2交错组成\n     * @return 如果s1和s2可以交错组成s3，则返回true；否则返回false。\n     */\n    public boolean isInterleave(String s1, String s2, String s3) {\n        // 获取输入字符串的长度\n        int n = s1.length(), m = s2.length(), t = s3.length();\n        // 如果s1和s2的长度之和不等于s3的长度，则不可能通过交错组合成s3\n        if (n + m != t) {\n            return false;\n        }\n        // 初始化动态规划数组，f[i][j]表示s1的前i个字符和s2的前j个字符是否可以交错组成s3的前p个字符（p=i+j-1）\n        boolean[][] f = new boolean[n + 1][m + 1];\n        // 空字符串和空字符串可以交错组成空字符串\n        f[0][0] = true;\n        // 遍历所有可能的交错组合情况\n        for (int i = 0; i <= n; ++i) {\n            for (int j = 0; j <= m; ++j) {\n                int p = i + j - 1;\n                // 如果不是空字符串，检查s1的字符是否可以和当前交错位置的字符匹配\n                if (i > 0) {\n                    f[i][j] = f[i][j] || (f[i - 1][j] && s1.charAt(i - 1) == s3.charAt(p));\n                }\n                // 如果不是空字符串，检查s2的字符是否可以和当前交错位置的字符匹配\n                if (j > 0) {\n                    f[i][j] = f[i][j] || (f[i][j - 1] && s2.charAt(j - 1) == s3.charAt(p));\n                }\n            }\n        }\n        // 返回最终结果，即s1和s2的整个字符串是否可以交错组成s3\n        return f[n][m];\n    }\n}\n```\n\n## 72. 编辑距离\nhttps://leetcode.cn/problems/edit-distance/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你两个单词 word1 和 word2， 请返回将 word1 转换成 word2 所使用的最少操作数  。\n\n你可以对一个单词进行如下三种操作：\n\n- 插入一个字符\n- 删除一个字符\n- 替换一个字符\n \n\n示例 1：\n\n输入：word1 = \"horse\", word2 = \"ros\"\n\n输出：3\n\n解释：\n\nhorse -> rorse (将 'h' 替换为 'r')\n\nrorse -> rose (删除 'r')\n\nrose -> ros (删除 'e')\n\n示例 2：\n\n输入：word1 = \"intention\", word2 = \"execution\"\n\n输出：5\n\n解释：\n- intention -> inention (删除 't')\n- inention -> enention (将 'i' 替换为 'e')\n- enention -> exention (将 'n' 替换为 'x')\n- exention -> exection (将 'n' 替换为 'c')\n- exection -> execution (插入 'u')\n \n\n提示：\n\n- 0 <= word1.length, word2.length <= 500\n- word1 和 word2 由小写英文字母组成\n\n```java\n/**\n * Solution类提供了一个方法来计算两个字符串之间的最小编辑距离。\n * 编辑距离指的是将一个字符串转换成另一个字符串所需的最少操作次数，操作包括插入、删除和替换字符。\n */\nclass Solution {\n    /**\n     * 计算两个字符串之间的最小编辑距离。\n     * \n     * @param word1 第一个字符串\n     * @param word2 第二个字符串\n     * @return 两个字符串之间的最小编辑距离\n     */\n    public int minDistance(String word1, String word2) {\n        // 获取两个字符串的长度\n        int m = word1.length();\n        int n = word2.length();\n\n        // 如果其中一个字符串为空，则最小编辑距离为另一个字符串的长度\n        if(m*n==0){\n            return m+n;\n        }\n\n        // 初始化动态规划表格，dp[i][j]表示word1的前i个字符和word2的前j个字符之间的最小编辑距离\n        int[][] dp = new int[m+1][n+1];\n\n        // 初始化表格的第一列和第一行，分别表示将word1的前i个字符转换为空字符串和将空字符串转换为word2的前j个字符的最小编辑距离\n        for(int i=0;i<=m;i++){\n            dp[i][0] = i;\n        }\n        for(int i=0;i<=n;i++){\n            dp[0][i] = i;\n        }\n\n        // 填充动态规划表格，计算所有dp[i][j]的值\n        for(int i=1;i<=m;i++){\n            for(int j=1;j<=n;j++){\n                // 计算三种操作（插入、删除、替换）对应的编辑距离\n                int left = dp[i-1][j]+1;\n                int down = dp[i][j-1]+1;\n                int left_down = dp[i-1][j-1];\n                // 如果当前字符不相同，则需要进行替换操作，因此编辑距离加1\n                if (word1.charAt(i - 1) != word2.charAt(j - 1)) {\n                    left_down += 1;\n                }\n                // 取三种操作中的最小值作为dp[i][j]的值\n                dp[i][j] = Math.min(left,Math.min(down,left_down));   \n            }\n        }\n\n        // 返回整个表格的最后一个值，即为两个字符串之间的最小编辑距离\n        return dp[m][n];\n    }\n}\n```\n\n## 123. 买卖股票的最佳时机 III\nhttps://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iii/description/?envType=study-plan-v2&envId=top-interview-150\n\n给定一个数组，它的第 i 个元素是一支给定的股票在第 i 天的价格。\n\n设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。\n\n注意：你不能同时参与多笔交易（你必须在再次购买前出售掉之前的股票）。\n\n \n\n示例 1:\n\n输入：prices = [3,3,5,0,0,3,1,4]\n\n输出：6\n\n解释：在第 4 天（股票价格 = 0）的时候买入，在第 6 天（股票价格 = 3）的时候卖出，这笔交易所能获得利润 = 3-0 = 3 。\n     随后，在第 7 天（股票价格 = 1）的时候买入，在第 8 天 （股票价格 = 4）的时候卖出，这笔交易所能获得利润 = 4-1 = 3 。\n\n示例 2：\n\n输入：prices = [1,2,3,4,5]\n\n输出：4\n\n解释：在第 1 天（股票价格 = 1）的时候买入，在第 5 天 （股票价格 = 5）的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。   \n     注意你不能在第 1 天和第 2 天接连购买股票，之后再将它们卖出。   \n     因为这样属于同时参与了多笔交易，你必须在再次购买前出售掉之前的股票。\n\n示例 3：\n\n输入：prices = [7,6,4,3,1] \n\n输出：0 \n\n解释：在这个情况下, 没有交易完成, 所以最大利润为 0。\n\n示例 4：\n\n输入：prices = [1]\n\n输出：0\n \n\n提示：\n\n- 1 <= prices.length <= 10^5\n- 0 <= prices[i] <= 10^5\n\n```java\n/**\n * 计算股票中最大利润的类\n */\nclass Solution {\n    /**\n     * 计算给定价格数组中能获得的最大利润\n     *\n     * @param prices 一个整数数组，表示每天的股票价格\n     * @return 返回一个整数，表示在允许进行两次交易的情况下，能获得的最大利润\n     */\n    public int maxProfit(int[] prices) {\n        int n = prices.length; // 获取价格数组的长度\n        int buy1 = -prices[0], sell1 = 0; // 初始化第一次交易的买入和卖出价格\n        int buy2 = -prices[0], sell2 = 0; // 初始化第二次交易的买入和卖出价格\n\n        // 遍历价格数组，更新买入和卖出价格\n        for (int i = 1; i < n; ++i) {\n            buy1 = Math.max(buy1, -prices[i]); // 更新第一次交易的买入价格\n            sell1 = Math.max(sell1, buy1 + prices[i]); // 更新第一次交易的卖出价格\n            buy2 = Math.max(buy2, sell1 - prices[i]); // 更新第二次交易的买入价格\n            sell2 = Math.max(sell2, buy2 + prices[i]); // 更新第二次交易的卖出价格\n        }\n\n        // 返回第二次交易后的最大利润\n        return sell2;\n    }\n}\n```\n\n## 188. 买卖股票的最佳时机 IV\nhttps://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iv/description/?envType=study-plan-v2&envId=top-interview-150\n\n给你一个整数数组 prices 和一个整数 k ，其中 prices[i] 是某支给定的股票在第 i 天的价格。\n\n设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。也就是说，你最多可以买 k 次，卖 k 次。\n\n注意：你不能同时参与多笔交易（你必须在再次购买前出售掉之前的股票）。\n\n \n\n示例 1：\n\n输入：k = 2, prices = [2,4,1]\n\n输出：2\n\n解释：在第 1 天 (股票价格 = 2) 的时候买入，在第 2 天 (股票价格 = 4) 的时候卖出，这笔交易所能获得利润 = 4-2 = 2 。\n\n示例 2：\n\n输入：k = 2, prices = [3,2,6,5,0,3]\n\n输出：7\n\n解释：在第 2 天 (股票价格 = 2) 的时候买入，在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。\n     随后，在第 5 天 (股票价格 = 0) 的时候买入，在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3 。\n \n\n提示：\n\n- 1 <= k <= 100\n- 1 <= prices.length <= 1000\n- 0 <= prices[i] <= 1000\n\n```java\n/**\n * Solution类用于解决给定股票价格数组中，最多进行k次交易的情况下，获得最大利润的问题。\n */\nclass Solution {\n    /**\n     * 计算最大利润。\n     * \n     * @param k 最多可以进行的交易次数。\n     * @param prices 股票价格数组。\n     * @return 可以获得的最大利润。\n     */\n    public int maxProfit(int k, int[] prices) {\n        // 如果价格数组为空，直接返回0。\n        if (prices.length == 0) {\n            return 0;\n        }\n\n        // n为股票价格数组的长度。\n        int n = prices.length;\n        // k的取值不能大于n/2，因为至少需要一天来买入和卖出。\n        k = Math.min(k, n / 2);\n        // buy数组用于记录第i天进行第j次交易时的买入状态下的最大利润。\n        int[][] buy = new int[n][k + 1];\n        // sell数组用于记录第i天进行第j次交易时的卖出状态下的最大利润。\n        int[][] sell = new int[n][k + 1];\n\n        // 初始化第一天的买入情况，即买入一股的成本为第一天的股票价格。\n        buy[0][0] = -prices[0];\n        // 初始化第一天的卖出情况，即尚未进行交易，利润为0。\n        sell[0][0] = 0;\n        // 初始化其他天数和交易次数的买入和卖出情况为一个较小值。\n        for (int i = 1; i <= k; ++i) {\n            buy[0][i] = sell[0][i] = Integer.MIN_VALUE / 2;\n        }\n\n        // 遍历每一天和每一次交易，更新买入和卖出的最大利润。\n        for (int i = 1; i < n; ++i) {\n            // 更新不进行交易的情况下，持有股票和不持有股票的最大利润。\n            buy[i][0] = Math.max(buy[i - 1][0], sell[i - 1][0] - prices[i]);\n            for (int j = 1; j <= k; ++j) {\n                // 更新进行交易的情况下，买入和卖出的最大利润。\n                buy[i][j] = Math.max(buy[i - 1][j], sell[i - 1][j] - prices[i]);\n                sell[i][j] = Math.max(sell[i - 1][j], buy[i - 1][j - 1] + prices[i]);\n            }\n        }\n\n        // 返回最后一天卖出时的最大利润。\n        return Arrays.stream(sell[n - 1]).max().getAsInt();\n    }\n}\n```\n\n## 221. 最大正方形\nhttps://leetcode.cn/problems/maximal-square/description/?envType=study-plan-v2&envId=top-interview-150\n\n在一个由 '0' 和 '1' 组成的二维矩阵内，找到只包含 '1' 的最大正方形，并返回其面积。\n\n \n\n示例 1：\n\n![alt text](../img/数据结构和算法/最大正方形1.png)\n\n输入：matrix = [[\"1\",\"0\",\"1\",\"0\",\"0\"],[\"1\",\"0\",\"1\",\"1\",\"1\"],[\"1\",\"1\",\"1\",\"1\",\"1\"],[\"1\",\"0\",\"0\",\"1\",\"0\"]]\n\n输出：4\n\n示例 2：\n\n![alt text](../img/数据结构和算法/最大正方形2.png)\n\n输入：matrix = [[\"0\",\"1\"],[\"1\",\"0\"]]\n\n输出：1\n\n示例 3：\n\n输入：matrix = [[\"0\"]]\n\n输出：0\n \n\n提示：\n\n- m == matrix.length\n- n == matrix[i].length\n- 1 <= m, n <= 300\n- matrix[i][j] 为 '0' 或 '1'\n\n```java\n/**\n * 此类提供了一个解决方案，用于计算给定矩阵中最大正方形的边长，该正方形由'1'字符组成。\n */\nclass Solution {\n    /**\n     * 计算给定二进制字符矩阵中最大正方形的边长。\n     * \n     * @param matrix 一个二维字符数组，其中'1'表示白色单元格，'0'表示黑色单元格。\n     * @return 返回矩阵中最大全为'1'的正方形的边长，如果不存在这样的正方形，则返回0。\n     */\n    public int maximalSquare(char[][] matrix) {\n        int m = matrix.length; // 获取矩阵的行数\n        int n = matrix[0].length; // 获取矩阵的列数\n\n        // 初始化一个动态规划数组，用于存储以(i, j)为右下角的最大正方形边长\n        int[][] dp = new int[m+1][n+1];\n        int max = 0; // 用于记录最大正方形的边长\n\n        // 遍历矩阵，计算最大正方形的边长\n        for(int i=1; i<=m; i++){\n            for(int j=1; j<=n; j++){\n                // 当当前位置为'1'时，更新dp值并计算最大边长\n                if(matrix[i-1][j-1]=='1'){\n                    dp[i][j] = Math.min(dp[i-1][j-1], Math.min(dp[i-1][j], dp[i][j-1])) + 1;\n                    max = Math.max(max, dp[i][j]);\n                }\n            }\n        }\n\n        // 返回最大正方形的面积（边长的平方）\n        return max*max;\n    }\n}\n```\n\n"
  },
  {
    "path": "数据结构和算法/二叉树.md",
    "content": "\n* [二叉树](#二叉树)\n    * [满二叉树(Full Binary Tree)](#满二叉树full-binary-tree)\n    * [完全二叉树(Complete Binary Tree)](#完全二叉树complete-binary-tree)\n    * [平衡二叉树(Balanced Binary Tree AVL)](#平衡二叉树balanced-binary-tree-avl)\n    * [二叉搜索树(Binary Search Tree)](#二叉搜索树binary-search-tree)\n    * [红黑树(Red Black Tree)](#红黑树red-black-tree)\n* [参考文章](#参考文章)\n\n# 二叉树\n\n## 满二叉树(Full Binary Tree)\n![](../img/数据结构和算法/二叉树/满二叉树.png)\n\n除最后一层无任何子节点外，每一层上的所有节点都有两个子节点，最后一层都是叶子节点。满足下列性质：\n1. 一颗树深度为h，最大层数为k，深度与最大层数相同，k=h；\n2. 叶子节点数（最后一层）为2k−1；\n3. 第 i 层的节点数是：2i−1；\n4. 总节点数是：2k-1，且总节点数一定是奇数。\n\n## 完全二叉树(Complete Binary Tree)\n![](../img/数据结构和算法/二叉树/完全二叉树.png)\n\n若设二叉树的深度为h，除第 h 层外，其它各层 (1～h-1) 的结点数都达到最大个数，第 h 层所有的结点都连续集中在最左边，这就是完全二叉树。满足下列性质：\n1. 只允许最后一层有空缺结点且空缺在右边，即叶子节点只能在层次最大的两层上出现；\n2. 对任一节点，如果其右子树的深度为j，则其左子树的深度必为j或j+1。 即度为1的点只有1个或0个；\n3. 除最后一层，第 i 层的节点数是：2i−1；\n4. 有n个节点的完全二叉树，其深度为：log2n+1或为log2n+1；\n5. 满二叉树一定是完全二叉树，完全二叉树不一定是满二叉树。\n\n## 平衡二叉树(Balanced Binary Tree AVL)\n![](../img/数据结构和算法/二叉树/平衡二叉树.png)\n\nwindows对进程地址空间的管理用到了AVL树\n\n又被称为AVL树，它是一颗空树或左右两个子树的高度差的绝对值不超过 1，并且左右两个子树都是一棵平衡二叉树。\n\n## 二叉搜索树(Binary Search Tree)\n![](../img/数据结构和算法/二叉树/二叉搜索树.png)\n\n又称二叉查找树、二叉排序树（Binary Sort Tree）。它是一颗空树或是满足下列性质的二叉树：\n1. 若左子树不空，则左子树上所有节点的值均小于或等于它的根节点的值；\n2. 若右子树不空，则右子树上所有节点的值均大于或等于它的根节点的值；\n3. 左、右子树也分别为二叉排序树。\n\n## 红黑树(Red Black Tree)\n![](../img/数据结构和算法/二叉树/红黑树.png)\n\n是每个节点都带有颜色属性（颜色为红色或黑色）的自平衡二叉查找树，满足下列性质：\n1. 节点是红色或黑色；\n2. 根节点是黑色；\n3. 所有叶子节点都是黑色；\n4. 每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)\n5. 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。\n\nJAVA的map用到了红黑树\n# 参考文章\n- https://www.cnblogs.com/sunshineliulu/p/7775063.html"
  },
  {
    "path": "数据结构和算法/二叉树遍历.md",
    "content": "https://leetcode-cn.com/problems/binary-tree-preorder-traversal/solution/leetcodesuan-fa-xiu-lian-dong-hua-yan-shi-xbian-2/"
  },
  {
    "path": "数据结构和算法/加减乘除.md",
    "content": "* [加](#加)\n    * [字符串相加](#字符串相加)\n* [减](#减)\n* [乘](#乘)\n    * [字符串相乘](#字符串相乘)\n* [除](#除)\n    * [两数相除](#两数相除)\n\n# 加\n## 字符串相加\n给定两个字符串形式的非负整数 num1 和num2 ，计算它们的和并同样以字符串形式返回。\n\n你不能使用任何內建的用于处理大整数的库（比如 BigInteger）， 也不能直接将输入的字符串转换为整数形式。\n\n示例 1：\n```html\n输入：num1 = \"11\", num2 = \"123\"\n输出：\"134\"\n```\n\n示例 2：\n```html\n输入：num1 = \"456\", num2 = \"77\"\n输出：\"533\"\n```\n示例 3：\n```html\n输入：num1 = \"0\", num2 = \"0\"\n输出：\"0\"\n```\n```java\nclass Solution {\n    public String addStrings(String num1, String num2) {\n        int i = num1.length() - 1;\n        int j = num2.length() - 1;\n\n\n        StringBuffer sb = new StringBuffer();\n        int add = 0;\n        while(i >= 0 || j >= 0 || add != 0){\n            int x = i >= 0 ? num1.charAt(i) - '0' : 0;\n            int y = j >= 0 ? num2.charAt(j) - '0' : 0;\n\n            int result = x + y + add;\n            sb.append(result % 10);\n            add = result / 10;\n            i--;\n            j--;\n        }\n        return sb.reverse().toString();\n    }\n}\n```\n# 减\n给定两个字符串形式的非负整数 num1 和num2 ，计算它们的差。\n\n注意：\n\n1. num1 和num2 都只会包含数字 0-9\n2. num1 和num2 都不包含任何前导零\n3. 你不能使用任何內建 BigInteger 库\n\n```java\npublic static void main(String[] args) {\n    String a = \"234\";\n    String b = \"1234\";\n    if(a.length() < b.length()){\n        String sub = sub(b, a);\n        String s = \"0\".equals(sub) ? \"0\" : \"-\" + sub;\n        System.out.println(s);\n    }else{\n        System.out.println(sub(a, b));\n    }\n}\n\npublic static String sub(String num1, String num2) {\n    int i = num1.length() - 1;\n    int j = num2.length() - 1;\n\n    StringBuffer sb = new StringBuffer();\n    int add = 0;\n    while(i >= 0 || j >= 0){\n        int x = i >= 0 ? num1.charAt(i) - '0' : 0;\n        int y = j >= 0 ? num2.charAt(j) - '0' : 0;\n\n        int result = x - y - add + 10;\n        sb.append(result % 10);\n        add = x -y - add < 0 ? 1 :0;\n        i--;\n        j--;\n    }\n\n    String re = sb.reverse().toString();\n\n    int k = 0;\n    for (; k < re.length(); k++) {\n        if (re.charAt(k) != '0') {\n            break;\n        }\n    }\n    return re.substring(k);\n}\n```\n\n# 乘\n## 字符串相乘\n链接：https://leetcode-cn.com/problems/multiply-strings/\n\n给定两个以字符串形式表示的非负整数 num1 和 num2，返回 num1 和 num2 的乘积，它们的乘积也表示为字符串形式。\n\n示例 1:\n```html\n输入: num1 = \"2\", num2 = \"3\"\n输出: \"6\"\n```\n示例2:\n```html\n输入: num1 = \"123\", num2 = \"456\"\n输出: \"56088\"\n```\n说明：\n\n1. num1和num2的长度小于110。\n2. num1 和num2 只包含数字0-9。\n3. num1 和num2均不以零开头，除非是数字 0 本身。\n4. 不能使用任何标准库的大数类型（比如 BigInteger）或直接将输入转换为整数来处理。\n\n```java\nclass Solution {\n    public String multiply(String num1, String num2) {\n        if(num1.charAt(0) == '0'|| num2.charAt(0)=='0'){\n            return \"0\";\n        }\n        int l1 = num1.length();\n        int l2 = num2.length();\n        int[] ansArr = new int[l1+l2];\n        //保存每一位的乘积结果\n        for(int i=l1-1;i>=0;i--){\n            int x = num1.charAt(i) - '0';\n            for(int j=l2-1;j>=0;j--){\n                int y = num2.charAt(j) - '0';\n                ansArr[i+j+1] += x * y;\n            }\n        }\n        // 从低位开始向前计算\n        for(int i= l1+l2 -1;i>0;i--){\n            ansArr[i-1] += ansArr[i] /10;\n            ansArr[i] %= 10;\n        }\n        //前置0去掉\n        int index = ansArr[0] == 0 ? 1:0;\n        \n        StringBuffer ans = new StringBuffer();\n        while(index < l1+l2){\n            ans.append(ansArr[index]);\n            index++;\n        }\n        return ans.toString();\n    }\n}\n```\n# 除\nhttps://leetcode-cn.com/problems/divide-two-integers/\n\n## 两数相除\n给定两个整数，被除数 dividend 和除数 divisor。将两数相除，要求不使用乘法、除法和 mod 运算符。\n\n返回被除数 dividend 除以除数 divisor 得到的商。\n\n整数除法的结果应当截去（truncate）其小数部分，例如：truncate(8.345) = 8 以及 truncate(-2.7335) = -2\n\n\n\n示例 1:\n```html\n输入: dividend = 10, divisor = 3\n输出: 3\n解释: 10/3 = truncate(3.33333..) = truncate(3) = 3\n```\n\n示例 2:\n```html\n输入: dividend = 7, divisor = -3\n输出: -2\n解释: 7/-3 = truncate(-2.33333..) = -2\n```\n\n```java\nclass Solution {\n    int MIN = Integer.MIN_VALUE, MAX = Integer.MAX_VALUE;\n    int LIMIT = -1073741824; // MIN 的一半\n    public int divide(int a, int b) {\n        if (a == MIN && b == -1) return MAX;\n        boolean flag = false;\n        if ((a > 0 && b < 0) || (a < 0 && b > 0)) flag = true;\n        if (a > 0) a = -a;\n        if (b > 0) b = -b;\n        int ans = 0;\n        while (a <= b){\n            int c = b, d = -1;\n            while (c >= LIMIT && d >= LIMIT && c >= a - c){\n                c += c; d += d;\n            }\n            a -= c;\n            ans += d;\n        }\n        return flag ? ans : -ans;\n    }\n}\n\n对于全程不使用 long 的做法，我们需要将所有数映射到负数进行处理（以 00 为分割点，负数所能表示的范围更大）。\n基本思路为：\n\n起始先对边界情况进行特判；\n记录最终结果的符号，并将两数都映射为负数；\n可以预处理出倍增数组，或采取逐步增大 dividend 来逼近 divisor 的方式。\n由于操作数都是负数，因此自倍增过程中，如果操作数小于 INT_MIN 的一半（-1073741824），则代表发生溢出。\n\n作者：AC_OIer\n链接：https://leetcode-cn.com/problems/divide-two-integers/solution/gong-shui-san-xie-dui-xian-zhi-tiao-jian-utb9/\n来源：力扣（LeetCode）\n著作权归作者所有。商业转载请联系作者获得授权，非商业转载请注明出处。\n```\n"
  },
  {
    "path": "数据结构和算法/排序算法.md",
    "content": "\n* [排序算法](#排序算法)\n  * [冒泡排序](#冒泡排序)\n  * [选择排序](#选择排序)\n  * [插入排序](#插入排序)\n  * [希尔排序](#希尔排序)\n  * [归并排序<g-emoji class=\"g-emoji\" alias=\"exclamation\" fallback-src=\"https://github.githubassets.com/images/icons/emoji/unicode/2757.png\">❗</g-emoji>](#归并排序)\n  * [快速排序<g-emoji class=\"g-emoji\" alias=\"exclamation\" fallback-src=\"https://github.githubassets.com/images/icons/emoji/unicode/2757.png\">❗</g-emoji>](#快速排序)\n  * [堆排序<g-emoji class=\"g-emoji\" alias=\"exclamation\" fallback-src=\"https://github.githubassets.com/images/icons/emoji/unicode/2757.png\">❗</g-emoji>](#堆排序)\n  * [计数排序](#计数排序)\n  * [桶排序](#桶排序)\n  * [基数排序](#基数排序)\n* [参考文章](#参考文章)\n\n\n# 排序算法\n![](../img/数据结构和算法/排序算法.png)\n\n### 冒泡排序\n冒泡排序要对一个列表多次重复遍历。它要比较相邻的两项，并且交换顺序排错的项。每对 列表实行一次遍历，就有一个最大项排在了正确的位置。大体上讲，列表的每一个数据项都会在 其相应的位置“冒泡”。如果列表有n项，第一次遍历就要比较n-1对数据。需要注意，一旦列 表中最大(按照规定的原则定义大小)的数据是所比较的数据对中的一个，它就会沿着列表一直 后移，直到这次遍历结束。\n\n![](../img/数据结构和算法/冒泡排序动图.gif)\n\n```java\npublic class BubbleSort {\n    public static void bubbleSort(int[] arr) {\n        int temp = 0;\n        boolean swap;\n        // 每次需要排序的长度\n        for (int i = arr.length - 1; i > 0; i--) {\n            swap = false;\n            // 从第一个元素到第i个元素\n            for (int j = 0; j < i; j++) {\n                if (arr[j] > arr[j + 1]) {\n                    temp = arr[j];\n                    arr[j] = arr[j + 1];\n                    arr[j + 1] = temp;\n                    swap = true;\n                }\n            }\n            if (!swap) {\n                break;\n            }\n        }\n    }\n}\n```\n### 选择排序\n选择排序提高了冒泡排序的性能，它每遍历一次列表只交换一次数据，即进行一次遍历时找 到最大的项，完成遍历后，再把它换到正确的位置。和冒泡排序一样，第一次遍历后，最大的数 据项就已归位，第二次遍历使次大项归位。这个过程持续进行，一共需要n-1次遍历来排好n个数 据，因为最后一个数据必须在第n-1次遍历之后才能归位。\n\n![](../img/数据结构和算法/选择排序动图.gif)\n\n```java\npublic class SelectSort {\n    // 选择排序：每一轮选择最小元素交换到未排定部分的开头\n\n    public int[] sortArray(int[] nums) {\n        int len = nums.length;\n        // 循环不变量：[0, i) 有序，且该区间里所有元素就是最终排定的样子\n        for (int i = 0; i < len - 1; i++) {\n            // 选择区间 [i, len - 1] 里最小的元素的索引，交换到下标 i\n            int minIndex = i;\n            for (int j = i + 1; j < len; j++) {\n                if (nums[j] < nums[minIndex]) {\n                    minIndex = j;\n                }\n            }\n            swap(nums, i, minIndex);\n        }\n        return nums;\n    }\n\n    private void swap(int[] nums, int index1, int index2) {\n        int temp = nums[index1];\n        nums[index1] = nums[index2];\n        nums[index2] = temp;\n    }\n}\n```\n### 插入排序\n插入排序的算法复杂度仍然是O(n2)，但其工作原理稍有不同。它总是保持一个位置靠前的 已排好的子表，然后每一个新的数据项被“插入”到前边的子表里，排好的子表增加一项。我们认为只含有一个数据项的列表是已经排好的。每排后面一个数据(从1开始到n-1)，这 个的数据会和已排好子表中的数据比较。比较时，我们把之前已经排好的列表中比这个数据大的移到它的右边。当子表数据小于当前数据，或者当前数据已经和子表的所有数据比较了时，就可 以在此处插入当前数据项。\n\n![](../img/数据结构和算法/插入排序动图.gif)\n\n```java\npublic class InsertSort {\n    public static void insertionSort(int[] arr) {\n        for (int i = 1; i < arr.length; ++i) {\n            int value = arr[i];\n            int position = i;\n            while (position > 0 && arr[position - 1] > value) {\n                arr[position] = arr[position - 1];\n                position--;\n            }\n            arr[position] = value;\n        }\n    }\n}\n```\n### 希尔排序\n希尔排序有时又叫做“缩小间隔排序”，它以插入排序为基础，将原来要排序的列表划分为一些子列表，再对每一个子列表执行插入排序，从而实现对插入排序性能的改进。划分子列的特定方法是希尔排序的关键。我们并不是将原始列表分成含有连续元素的子列，而是确定一个划分列表的增量“i”，这个i更准确地说，是划分的间隔。然后把每间隔为i的所有元素选出来组成子列表，然后对每个子序列进行插入排序，最后当i=1时，对整体进行一次直接插入排序\n\n![](../img/数据结构和算法/希尔排序动图.gif)\n\n```java\npublic class Solution {\n\n    // 希尔排序\n\n    public int[] sortArray(int[] nums) {\n        int len = nums.length;\n        int h = 1;\n\n        // 使用 Knuth 增量序列\n        // 找增量的最大值\n        while (3 * h + 1 < len) {\n            h = 3 * h + 1;\n        }\n\n        while (h >= 1) {\n            // insertion sort\n            for (int i = h; i < len; i++) {\n                insertionForDelta(nums, h, i);\n            }\n            h = h / 3;\n        }\n        return nums;\n    }\n\n    /**\n     * 将 nums[i] 插入到对应分组的正确位置上，其实就是将原来 1 的部分改成 gap\n     */\n    private void insertionForDelta(int[] nums, int gap, int i) {\n        int temp = nums[i];\n        int j = i;\n        // 注意：这里 j >= deta 的原因\n        while (j >= gap && nums[j - gap] > temp) {\n            nums[j] = nums[j - gap];\n            j -= gap;\n        }\n        nums[j] = temp;\n    }\n}\n```\n### 归并排序\n:exclamation::exclamation::exclamation:\n\n归并排序是一种递归算法，它持续地将一个列表分成两半。如果列表是空的或者 只有一个元素，那么根据定义，它就被排序好了(最基本的情况)。如果列表里的元素超过一个，我们就把列表拆分，然后分别对两个部分调用递归排序。一旦这两个部分被排序好了，然后就可以对这两部分数列进行归并了。归并是这样一个过程:把两个排序好了的列表结合在一起组合成一个单一的有序的新列表。有自顶向下（递归法）和自底向上的两种实现方法。\n\n![](../img/数据结构和算法/归并排序动图.gif)\n\n```java\npublic class MergeSort {\n    public static void mergeSort(int[] arr) {\n        int[] temp = new int[arr.length];\n        internalMergeSort(arr, temp, 0, arr.length - 1);\n    }\n\n    private static void internalMergeSort(int[] arr, int[] temp, int left, int right) {\n        //当left==right的时，已经不需要再划分了\n        if (left < right) {\n            int middle = (left + right) / 2;\n            //左子数组\n            internalMergeSort(arr, temp, left, middle);\n            //右子数组\n            internalMergeSort(arr, temp, middle + 1, right);\n            //合并两个子数组\n            mergeSortedArray(arr, temp, left, middle, right);\n        }\n    }\n\n    /**\n     * 合并两个有序子序列\n     */\n    private static void mergeSortedArray(int[] arr, int[] temp, int left, int middle, int right) {\n        int i = left;\n        int j = middle + 1;\n        int k = 0;\n        while (i <= middle && j <= right) {\n            temp[k++] = arr[i] <= arr[j] ? arr[i++] : arr[j++];\n        }\n        while (i <= middle) {\n            temp[k++] = arr[i++];\n        }\n        while (j <= right) {\n            temp[k++] = arr[j++];\n        }\n        //把数据复制回原数组\n        for (i = 0; i < k; ++i) {\n            arr[left + i] = temp[i];\n        }\n    }\n}\n```\n### 快速排序\n:exclamation::exclamation::exclamation:\n\n快速排序由C. A. R. Hoare在1962年提出。它的基本思想是：通过一趟排序将要排序的数据分割成独立的两部分，其中一部分的所有数据都比另外一部分的所有数据都要小，然后再按此方法对这两部分数据分别进行快速排序，整个排序过程可以递归进行，以此达到整个数据变成有序序列。\n\n**算法步骤**\n- 从数列中挑出一个元素，称为\"基准\"（pivot），\n- 重新排序数列，所有比基准值小的元素摆放在基准前面，所有比基准值大的元素摆在基准后面（相同的数可以到任何一边）。在这个分区结束之后，该基准就处于数列的中间位置。这个称为分区（partition）操作。\n- 递归地（recursively）把小于基准值元素的子数列和大于基准值元素的子数列排序。\n\n![](../img/数据结构和算法/快速排序动图.gif)\n\n```java\npublic class QuickSort {\n    public int[] sortArray(int[] nums) {\n        quickSort(nums, 0, nums.length - 1);\n        return nums;\n    }\n\n    public void quickSort(int[] nums, int left, int right) {\n        if (left < right) {\n            int pIndex = part(nums, left, right);\n            quickSort(nums, left, pIndex - 1);\n            quickSort(nums, pIndex + 1, right);\n        }\n    }\n\n    public int part(int[] nums, int left, int right) {\n        int p = new Random().nextInt(right - left + 1) + left;\n        swap(nums, left, p);\n\n        int pData = nums[left];\n        int lt = left;\n        for (int i = left + 1; i <= right; i++) {\n            if (nums[i] < pData) {\n                lt++;\n                swap(nums, i, lt);\n            }\n        }\n        swap(nums, left, lt);\n        return lt;\n    }\n\n    public void swap(int[] nums, int a, int b) {\n        int tmp = nums[a];\n        nums[a] = nums[b];\n        nums[b] = tmp;\n    }\n}\n```\n### 堆排序\n:exclamation::exclamation::exclamation:\n\n堆排序就是把最大堆堆顶的最大数取出，将剩余的堆继续调整为最大堆，再次将堆顶的最大数取出，这个过程持续到剩余数只有一个时结束。在堆中定义以下几种操作：\n\n![](../img/数据结构和算法/堆排序动图.gif)\n\n```java\npublic class Solution {\n\n    public int[] sortArray(int[] nums) {\n        int len = nums.length;\n        // 将数组整理成堆\n        heapify(nums);\n\n        // 循环不变量：区间 [0, i] 堆有序\n        for (int i = len - 1; i >= 1; ) {\n            // 把堆顶元素（当前最大）交换到数组末尾\n            swap(nums, 0, i);\n            // 逐步减少堆有序的部分\n            i--;\n            // 下标 0 位置下沉操作，使得区间 [0, i] 堆有序\n            siftDown(nums, 0, i);\n        }\n        return nums;\n    }\n\n    /**\n     * 将数组整理成堆（堆有序）\n     */\n    private void heapify(int[] nums) {\n        int len = nums.length;\n        // 只需要从 i = (len - 1) / 2 这个位置开始逐层下移\n        for (int i = (len - 1) / 2; i >= 0; i--) {\n            siftDown(nums, i, len - 1);\n        }\n    }\n\n    /**\n     * @param nums\n     * @param k    当前下沉元素的下标\n     * @param end  [0, end] 是 nums 的有效部分\n     */\n    private void siftDown(int[] nums, int k, int end) {\n        while (2 * k + 1 <= end) {\n            int j = 2 * k + 1;\n            if (j + 1 <= end && nums[j + 1] > nums[j]) {\n                j++;\n            }\n            if (nums[j] > nums[k]) {\n                swap(nums, j, k);\n            } else {\n                break;\n            }\n            k = j;\n        }\n    }\n\n    private void swap(int[] nums, int index1, int index2) {\n        int temp = nums[index1];\n        nums[index1] = nums[index2];\n        nums[index2] = temp;\n    }\n}\n```\n### 计数排序\n计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序，计数排序要求输入的数据必须是有确定范围的整数。\n\n步骤\n1. 找出待排序的数组中最大和最小的元素\n2. 统计数组中每个值为i的元素出现的次数，存入数组C的第i项\n3. 对所有的计数累加（从C中的第一个元素开始，每一项和前一项相加）\n4. 反向填充目标数组：将每个元素i放在新数组的第C(i)项，每放一个元素就将C(i)减去1\n\n![](../img/数据结构和算法/计数排序动图.gif)\n\n```java\npublic class Solution {\n\n    // 计数排序\n\n    private static final int OFFSET = 50000;\n\n    public int[] sortArray(int[] nums) {\n        int len = nums.length;\n        // 由于 -50000 <= A[i] <= 50000\n        // 因此\"桶\" 的大小为 50000 - (-50000) = 10_0000\n        // 并且设置偏移 OFFSET = 50000，目的是让每一个数都能够大于等于 0\n        // 这样就可以作为 count 数组的下标，查询这个数的计数\n        int size = 10_0000;\n\n        // 计数数组\n        int[] count = new int[size];\n        // 计算计数数组\n        for (int num : nums) {\n            count[num + OFFSET]++;\n        }\n\n        // 把 count 数组变成前缀和数组\n        for (int i = 1; i < size; i++) {\n            count[i] += count[i - 1];\n        }\n\n        // 先把原始数组赋值到一个临时数组里，然后回写数据\n        int[] temp = new int[len];\n        System.arraycopy(nums, 0, temp, 0, len);\n\n        // 为了保证稳定性，从后向前赋值\n        for (int i = len - 1; i >= 0; i--) {\n            int index = count[temp[i] + OFFSET] - 1;\n            nums[index] = temp[i];\n            count[temp[i] + OFFSET]--;\n        }\n        return nums;\n    }\n}\n```\n### 桶排序\n桶排序是计数排序的升级版。它利用了函数的映射关系，高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效，我们需要做到这两点：\n- 在额外空间充足的情况下，尽量增大桶的数量\n- 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中\n\n![](../img/数据结构和算法/桶排序.jpeg)\n\n```java\npublic class Solution {\n\n    // 桶排序\n    // 1 <= A.length <= 10000\n    // -50000 <= A[i] <= 50000\n\n    // 10_0000\n\n    private static final int OFFSET = 50000;\n\n    public int[] sortArray(int[] nums) {\n        int len = nums.length;\n        // 第 1 步：将数据转换为 [0, 10_0000] 区间里的数\n        for (int i = 0; i < len; i++) {\n            nums[i] += OFFSET;\n        }\n\n        // 第 2 步：观察数据，设置桶的个数\n        // 步长：步长如果设置成 10 会超出内存限制\n        int step = 1000;\n        // 桶的个数\n        int bucketLen = 10_0000 / step;\n\n        int[][] temp = new int[bucketLen + 1][len];\n        int[] next = new int[bucketLen + 1];\n\n        // 第 3 步：分桶\n        for (int num : nums) {\n            int bucketIndex = num / step;\n            temp[bucketIndex][next[bucketIndex]] = num;\n            next[bucketIndex]++;\n        }\n\n        // 第 4 步：对于每个桶执行插入排序\n        for (int i = 0; i < bucketLen + 1; i++) {\n            insertionSort(temp[i], next[i] - 1);\n        }\n\n        // 第 5 步：从桶里依次取出来\n        int[] res = new int[len];\n        int index = 0;\n        for (int i = 0; i < bucketLen + 1; i++) {\n            int curLen = next[i];\n            for (int j = 0; j < curLen; j++) {\n                res[index] = temp[i][j] - OFFSET;\n                index++;\n            }\n        }\n        return res;\n    }\n\n    private void insertionSort(int[] arr, int endIndex) {\n        for (int i = 1; i <= endIndex; i++) {\n            int temp = arr[i];\n            int j = i;\n            while (j > 0 && arr[j - 1] > temp) {\n                arr[j] = arr[j - 1];\n                j--;\n            }\n            arr[j] = temp;\n        }\n    }\n}\n```\n### 基数排序\n基数排序(Radix Sort)是桶排序的扩展，它的基本思想是：将整数按位数切割成不同的数字，然后按每个位数分别比较。\n- 排序过程：将所有待比较数值（正整数）统一为同样的数位长度，数位较短的数前面补零。然后，从最低位开始，依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。\n\n![](../img/数据结构和算法/基数排序动图.gif)\n\n```java\npublic class Solution {\n\n    // 基数排序：低位优先\n\n    private static final int OFFSET = 50000;\n\n    public int[] sortArray(int[] nums) {\n        int len = nums.length;\n\n        // 预处理，让所有的数都大于等于 0，这样才可以使用基数排序\n        for (int i = 0; i < len; i++) {\n            nums[i] += OFFSET;\n        }\n\n        // 第 1 步：找出最大的数字\n        int max = nums[0];\n        for (int num : nums) {\n            if (num > max) {\n                max = num;\n            }\n        }\n\n        // 第 2 步：计算出最大的数字有几位，这个数值决定了我们要将整个数组看几遍\n        int maxLen = getMaxLen(max);\n\n        // 计数排序需要使用的计数数组和临时数组\n        int[] count = new int[10];\n        int[] temp = new int[len];\n\n        // 表征关键字的量：除数\n        // 1 表示按照个位关键字排序\n        // 10 表示按照十位关键字排序\n        // 100 表示按照百位关键字排序\n        // 1000 表示按照千位关键字排序\n        int divisor = 1;\n        // 有几位数，外层循环就得执行几次\n        for (int i = 0; i < maxLen; i++) {\n\n            // 每一步都使用计数排序，保证排序结果是稳定的\n            // 这一步需要额外空间保存结果集，因此把结果保存在 temp 中\n            countingSort(nums, temp, divisor, len, count);\n\n            // 交换 nums 和 temp 的引用，下一轮还是按照 nums 做计数排序\n            int[] t = nums;\n            nums = temp;\n            temp = t;\n\n            // divisor 自增，表示采用低位优先的基数排序\n            divisor *= 10;\n        }\n\n        int[] res = new int[len];\n        for (int i = 0; i < len; i++) {\n            res[i] = nums[i] - OFFSET;\n        }\n        return res;\n    }\n\n    private void countingSort(int[] nums, int[] res, int divisor, int len, int[] count) {\n        // 1、计算计数数组\n        for (int i = 0; i < len; i++) {\n            // 计算数位上的数是几，先取个位，再十位、百位\n            int remainder = (nums[i] / divisor) % 10;\n            count[remainder]++;\n        }\n\n        // 2、变成前缀和数组\n        for (int i = 1; i < 10; i++) {\n            count[i] += count[i - 1];\n        }\n\n        // 3、从后向前赋值\n        for (int i = len - 1; i >= 0; i--) {\n            int remainder = (nums[i] / divisor) % 10;\n            int index = count[remainder] - 1;\n            res[index] = nums[i];\n            count[remainder]--;\n        }\n\n        // 4、count 数组需要设置为 0 ，以免干扰下一次排序使用\n        for (int i = 0; i < 10; i++) {\n            count[i] = 0;\n        }\n    }\n\n    /**\n     * 获取一个整数的最大位数\n     */\n    private int getMaxLen(int num) {\n        int maxLen = 0;\n        while (num > 0) {\n            num /= 10;\n            maxLen++;\n        }\n        return maxLen;\n    }\n}\n```\n\n---\n\n# 参考文章\n- https://www.runoob.com/w3cnote/ten-sorting-algorithm.html\n- https://zhuanlan.zhihu.com/p/42586566\n- https://leetcode-cn.com/problems/sort-an-array/solution/fu-xi-ji-chu-pai-xu-suan-fa-java-by-liweiwei1419/\n- https://mp.weixin.qq.com/s/VX9LwTK77RUPLBFHPS1Z1A"
  },
  {
    "path": "智力题/智力题.md",
    "content": "# 智力题\n## 100只试管里有一只是有毒的，现在有10个小白鼠，如何最快速地判断出那只试管有毒\n- 100试管分为10组每组10只，1个小白鼠对应一组\n  - 每组混在一起\n- 死的那组10只，分5-5两组\n  - 9只\n- 5-5死一只\n  - 8只\n- 有毒的那一组可以用5只老鼠来试，死一只\n  - 7只\n## 一共 1000 瓶药水，其中 1 瓶有毒药。已知小白鼠喝毒药一天内死若想在一天内找到毒药，最少需要几只小白鼠？\n答案：10 只。\n- 0 000 000 001表示 1 号老鼠，喝了药水 1 。\n- 0 000 000 010表示 2 号老鼠，喝了药水 2 。\n- 0 000 000 011表示 1 号、 2 号老鼠，喝了药水 3 。\n- … …\n- 1 111 101 000表示 4、6、7、8、9、10号老鼠，喝了药水 1000。\n按照上述的方法依次喝\n\n第一回合，1 号老鼠喝药水 1\n\n第二回合，2 号老鼠喝药水 2\n\n...\n\n第一千回合，4、6、7、8、9、10号老鼠喝药水 1000\n\n喝完一天时，看 10 只老鼠的状态，根据老鼠状态就知道哪瓶药水有毒了。\n\n比如最后只是 2 号老鼠死了，那就说明第2瓶药水有毒；如果4、6、7、8、9、10死了，那就说明第1000瓶药水有毒！\n\n想明白这个道理，再来看怎么答案为何是10\n\n我们需要让二进制表示的种类数>=药水总数（1000）\n\n求的是最小值，显然，为10的时候满足要求\n\n也可以这么计算：⌈ log N ⌉，log底数为2\n## 只有两个无刻度的水桶，一个可以装6L水，一个可以装5L水，如何在桶里装入3L的水\n- 5L装满，倒入6L,6L剩1L没装满\n- 5L装满,倒入6L,6L满，5L剩4L\n- 6L倒掉，5L的4L倒入6L\n- 5L装满，倒入2L给6L，剩3L\n## 25匹马，5个赛道，每次只能同时有5匹马跑，最少比赛几次选出前三名？\n前5次，每个赛道比出第一的马\n- A赛道：A1、A2、A3、A4、A5\n- B赛道：B1、B2、B3、B4、B5\n- C赛道：C1、C2、C3、C4、C5\n- .......\n- E赛道\n\n第六次，5个赛道的第一（A1、B1......E1）在一个赛道比，选出25匹马的第一\n- A1、B1、C2.........E1\n\n第7次，A2和B1比选第二\n\n第8次\n- 第七次，如果A2赢 A3、B1、C1比\n- 如果B1赢 A2、B2、C1比\n## 家里有两个孩子，一个是女孩，另一个也是女孩的概率是多少？\n2分之一\n## 烧一根不均匀的绳，从头烧到尾总共需要1个小时。现在有若干条材质相同的绳子，问如何用烧绳的方法来计时一个小时十五分钟呢?\n主要是解决15分钟，首先用绳1点燃一段，绳子2点燃两端，绳子2烧完需要半小时，烧完的时候点燃绳子1的另一端，开始计时，就是这时候绳子1烧完就是15分钟，烧完点完绳3，然后烧完就是1小时15分钟\n## 一共12个一样的小球， 其中只有一个重量与其它不一样(未知轻重)，给你一个天平，找出那个不同重量的球？\n- 将12个小球分为三组（因为分成两组不能找到重量不一样的球在哪组），为A组、B组、C组\n- 将三组球分别两两称重，找到重量和另外两组不同的那一组（只要有两组可以使天平平衡，重量不一致的球必然在第三组）。假设坏的球在C组\n- 将C组的球分成两组C1和C2，每组两个球，这时从A组和B组里找到两个正常的球，分别和C1和C2去称，天平不能平衡说明重量不一致的球就在哪组。假设在C1\n- 将C1组的球分别和正常的球去称，天平不平衡时就能找到重量与其他不一致的球。\n## 有10瓶药，每瓶有10粒药，其中有一瓶是变质的。好药每颗重1克，变质的药每颗比好药重0.1克。问怎样用天秤称一次找出变质的那瓶药？\n- 将这10瓶药标好号1-10。\n- 然后按照瓶子的标号取药，1号药瓶取1粒药，2号药瓶取2粒药，3号药瓶取3例药，以此类推，取完10瓶药一起放到天平上去称。如果没有变质的药，重量应该是55克，这时多出几克，几号药瓶就是变质的。例如55.3克，那么变质的药就是3号药瓶的。\n## 你有两个罐子，50个红色弹球，50个蓝色弹球，如何将这100个球放入到两个罐子，随机选出一个罐子取出的球为红球的概率最大？\n将一个红球放到一个罐子中，另一个罐子放49个红球和50个蓝球，这样随便选出一个罐子取出红球的概率是1/2 * 1 + 1/2 * 49 /（49+50），接近0.75。\n## 抢 30 是双人游戏\n游戏规则是：第一个人喊 “ 1 ”或 “ 2 ”，第二个人要接着往下喊一个或两个数，然后再轮到第一个人。两人轮流进行下去。最后喊 30 的人获胜。问喊数字的最佳策略。\n答案：尽量喊3的倍数。\n\n解析：倒着看，其实，喊 27 时，就决定胜负了。假设 A 喊了 27，B只能喊 28 或 29 ，下个回合，A 一定可以喊30。也就是说，喊 27 者必胜。\n\n再倒着看，其实喊 24 时，就定胜负了。假设 A 喊了 24 ，B 只能喊 25 或 26 ，下个回合 A 一定能喊 27 。\n\n由于喊 27 者必胜，因此喊 24 者也必胜。\n\n同理可以推出：喊 3 的倍数者必胜。\n\n然后就会发现，这个游戏，谁先喊，谁一定输。\n## 某人进行10次打靶，每次打靶可能的得分为0到10分，10次打靶共得 90分的可能性有多少种\n一共在10个靶上丢了10环，考虑把10个相同的小球放到一排10个杯子里边的情况有多少种。10个杯子之间有9个隔板，假设我们有一排19个小坑，选出其中10个小坑放球，另外那9个坑放隔板，那么第1个隔板之前的小球（可能为0个，也可能为10个）放入第1个杯子，第1个和第2个隔板之间的小球（可能为0个，也可能为10个）放入第2个杯子，依此类推，第9个隔板之后的小球放入第10个杯子。那么情况一共有C(10, 19) ＝92378种。\n## 一天内时针和分针重合多少次\n22次\n\n时针也会走并不是禁止不动，所以如果每小时会重合一次就是24次，但是时针也在走，一天时针会走2圈，总共就是24-2=22次\n\n也可以用度数法\n- 分针每分钟转动1/60*360=6度\n- 时钟每分钟转动1/60/12*360=0.5度\n- 分针要追上时针，需要比时钟多跑圈数。超过一圈需要时间360/（6-0.5）分钟，一天总共有24小时，那么总共重合24*60/(360/6-0..5) = 22次"
  },
  {
    "path": "架构/DDD领域驱动设计.md",
    "content": "\n* [DDD（Domain Driven Design）领域驱动设计](#ddddomain-driven-design领域驱动设计)\n  * [软件架构模式的演进](#软件架构模式的演进)\n  * [微服务设计和拆分的困境](#微服务设计和拆分的困境)\n  * [为什么 DDD 适合微服务？](#为什么-ddd-适合微服务)\n  * [DDD 与微服务的关系](#ddd-与微服务的关系)\n* [DDD的一些术语](#ddd的一些术语)\n  * [领域和子域](#领域和子域)\n  * [核心域、通用域和支撑域](#核心域通用域和支撑域)\n  * [通用语言和限界上下文](#通用语言和限界上下文)\n  * [通用语言](#通用语言)\n  * [限界上下文](#限界上下文)\n  * [实体和值对象](#实体和值对象)\n  * [实体](#实体)\n  * [值对象](#值对象)\n  * [聚合和聚合根](#聚合和聚合根)\n    * [聚合](#聚合)\n    * [聚合根](#聚合根)\n  * [领域服务和应用服务](#领域服务和应用服务)\n    * [领域服务](#领域服务)\n    * [应用服务](#应用服务)\n  * [领域事件](#领域事件)\n  * [资源库【仓储】](#资源库仓储)\n* [DDD分层架构](#ddd分层架构)\n  * [三层架构如何演进到 DDD 分层架构](#三层架构如何演进到-ddd-分层架构)\n  * [微服务要有合理的架构分层](#微服务要有合理的架构分层)\n  * [项目级微服务](#项目级微服务)\n  * [企业级中台微服务](#企业级中台微服务)\n  * [应用和资源的解耦与适配](#应用和资源的解耦与适配)\n  * [DDD、中台和微服务的协作模式](#ddd中台和微服务的协作模式)\n  * [中台如何建模？](#中台如何建模)\n* [COLA](#cola)\n  * [start层](#start层)\n  * [adapter层](#adapter层)\n  * [cilent层](#cilent层)\n  * [app层](#app层)\n  * [domain层](#domain层)\n  * [infrastructure层](#infrastructure层)\n  * [COLA架构的特色](#cola架构的特色)\n    * [领域与功能的分包策略](#领域与功能的分包策略)\n    * [业务域和外部依赖解耦](#业务域和外部依赖解耦)\n* [链接](#链接)\n\n\n# DDD（Domain Driven Design）领域驱动设计\n\nDDD是一种设计思想，在设计架构时的让我们可以选择的方式、思路、思考的方向+1，而并不是让所有的架构都是DDD，适合的才是最好的；\n\n## 软件架构模式的演进\n\n在进入今天的主题之前，我们先来了解下背景。\n\n我们知道，这些年来随着设备和新技术的发展，软件的架构模式发生了很大的变化。软件架构模式大体来说经历了从单机、集中式到分布式微服务架构三个阶段的演进\n。随着分布式技术的快速兴起，我们已经进入到了微服务架构时代。\n\n![软件架构模式的演进.png](..%2Fimg%2F%E6%9E%B6%E6%9E%84%2FDDD%2F%E8%BD%AF%E4%BB%B6%E6%9E%B6%E6%9E%84%E6%A8%A1%E5%BC%8F%E7%9A%84%E6%BC%94%E8%BF%9B.png)\n\n我们先来分析一下软件架构模式演进的三个阶段。\n\n- 第一阶段是单机架构： 采用面向过程的设计方法，系统包括客户端 UI 层和数据库两层，采用 C/S\n  架构模式，整个系统围绕数据库驱动设计和开发，并且总是从设计数据库和字段开始。\n- 第二阶段是集中式架构： 采用面向对象的设计方法，系统包括业务接入层、业务逻辑层和数据库层，采用经典的三层架构，也有部分应用采用传统的\n  SOA 架构。这种架构容易使系统变得臃肿，可扩展性和弹性伸缩性差。\n- 第三阶段是分布式微服务架构：\n  随着微服务架构理念的提出，集中式架构正向分布式微服务架构演进。微服务架构可以很好地实现应用之间的解耦，解决单体应用扩展性和弹性伸缩能力不足的问题 。\n\n我们知道，在单机和集中式架构时代，系统分析、设计和开发往往是独立、分阶段割裂进行的。\n\n比如，在系统建设过程中，我们经常会看到这样的情形：A 负责提出需求，B 负责需求分析，C 负责系统设计，D\n负责代码实现，这样的流程很长，经手的人也很多，很容易导致信息丢失\n。最后，就很容易导致需求、设计与代码实现的不一致，往往到了软件上线后，我们才发现很多功能并不是自己想要的，或者做出来的功能跟自己提出的需求偏差太大。\n\n而且在单机和集中式架构这两种模式下，软件无法快速响应需求和业务的迅速变化 ，最终错失发展良机。此时，分布式微服务的出现就有点恰逢其时的意思了。\n\n## 微服务设计和拆分的困境\n\n那进入微服务架构时代以后，微服务确实也解决了原来采用集中式架构的单体应用的很多问题，比如扩展性、弹性伸缩能力、小规模团队的敏捷开发等等。\n\n但在看到这些好处的同时，微服务实践过程中也产生了不少的争论和疑惑：微服务的粒度应该多大呀？微服务到底应该如何拆分和设计呢？微服务的边界应该在哪里？\n\n可以说，很久以来都没有一套系统的理论和方法可以指导微服务的拆分，包括微服务架构模式的提出者 Martin Fowler\n在提出微服务架构的时候，也没有告诉我们究竟应该如何拆分微服务。\n\n于是，在这段较长的时间里，就有不少人对微服务的理解产生了一些曲解。有人认为：微服务很简单，不过就是把原来一个单体包拆分为多个部署包，或者将原来的单体应用架构替换为一套支持微服务架构的技术框架，就算是微服务了。\n还有人说：微服务嘛，就是要微要小，拆得越小效果越好。\n\n但我想，这两年，你在技术圈中一定听说过一些项目因为前期微服务拆分过度，导致项目复杂度过高，无法上线和运维。\n\n综合来看，我认为微服务拆分困境产生的根本原因就是 不知道业务或者微服务的边界到底在什么地方 。换句话说，确定了业务边界和应用边界，这个困境也就迎刃而解了。\n\n那如何确定，是否有相关理论或知识体系支持呢？在回答这些问题之前，我们先来了解一下领域驱动设计与微服务的前世今生。\n\n2004 年埃里克·埃文斯（Eric Evans）发表了《领域驱动设计》（Domain-Driven Design –Tackling Complexity in the Heart of\nSoftware）这本书，从此领域驱动设计（Domain Driven Design，简称 DDD）诞生。DDD\n核心思想是通过领域驱动设计方法定义领域模型，从而确定业务和应用边界，保证业务模型与代码模型的一致性。\n\n但 DDD 提出后在软件开发领域一直都是雷声大，雨点小！直到 Martin Fowler 提出微服务架构，DDD 才真正迎来了自己的时代。\n\n有些熟悉 DDD 设计方法的软件工程师在进行微服务设计时，发现可以利用 DDD 设计方法来建立领域模型，划分领域边界，再根据这些领域边界从业务视角来划分微服务边界。而按照\nDDD 方法设计出的微服务的业务和应用边界都非常合理，可以很好地实现微服务内部和外部的「高内聚、低耦合」。于是越来越多的人开始把\nDDD 作为微服务设计的指导思想。\n\n现在，很多大型互联网企业已经将 DDD 设计方法作为微服务的主流设计方法了。DDD 也从过去雷声大，雨点小，开始真正火爆起来。\n\n## 为什么 DDD 适合微服务？\n\n众里寻他千百度。蓦然回首，那人却在灯火阑珊处。在经历了多年的迷茫和争论后，微服务终于寻到了他的心上人。\n\n那 DDD 到底是何方神圣，拥有什么神器呢？\n\nDDD 是一种处理高度复杂领域的设计思想 ，它试图分离技术实现的复杂性，并围绕业务概念构建领域模型来控制业务的复杂性，以解决软件难以理解，难以演进的问题。DDD\n不是架构，而是一种架构设计方法论 ，它通过边界划分将复杂业务领域简单化，帮我们设计出清晰的领域和应用边界，可以很容易地实现架构演进。\n\nDDD 包括战略设计和战术设计两部分。\n\n- 战略设计主要从业务视角出发 ，建立业务领域模型，划分领域边界，建立通用语言的限界上下文，限界上下文可以作为微服务设计的参考边界。\n- 战术设计则从技术视角出发 ，侧重于领域模型的技术实现，完成软件开发和落地，包括：聚合根、实体、值对象、领域服务、应用服务和资源库等代码逻辑的设计和实现。\n\n我们不妨来看看 DDD 是如何进行战略设计的。\n\nDDD 战略设计会 建立领域模型 ，领域模型可以用于指导微服务的设计和拆分\n。事件风暴是建立领域模型的主要方法，它是一个从发散到收敛的过程。它通常采用用例分析、场景分析和用户旅程分析，尽可能全面不遗漏地分解业务领域，并梳理领域对象之间的关系，这是一个发散的过程。事件风暴过程会产生很多的实体、命令、事件等领域对象，我们将这些领域对象从不同的维度进行聚类，形成如聚合、限界上下文等边界，建立领域模型，这就是一个收敛的过程。\n\n![DDD是如何进行战略设计.png](..%2Fimg%2F%E6%9E%B6%E6%9E%84%2FDDD%2FDDD%E6%98%AF%E5%A6%82%E4%BD%95%E8%BF%9B%E8%A1%8C%E6%88%98%E7%95%A5%E8%AE%BE%E8%AE%A1.png)\n\n我们可以用三步来划定领域模型和微服务的边界。\n\n- 第一步：在事件风暴中梳理业务过程中的用户操作、事件以及外部依赖关系等，根据这些要素梳理出领域实体等领域对象。\n- 第二步：根据领域实体之间的业务关联性，将业务紧密相关的实体进行组合形成聚合，同时确定聚合中的聚合根、值对象和实体。在这个图里，聚合之间的边界是第一层边界，它们在同一个微服务实例中运行，这个边界是逻辑边界，所以用虚线表示。\n-\n\n第三步：根据业务及语义边界等因素，将一个或者多个聚合划定在一个限界上下文内，形成领域模型。在这个图里，限界上下文之间的边界是第二层边界，这一层边界可能就是未来微服务的边界，不同限界上下文内的领域逻辑被隔离在不同的微服务实例中运行，物理上相互隔离，所以是物理边界，边界之间用实线来表示。\n\n有了这两层边界，微服务的设计就不是什么难事了。\n\n在战略设计中我们建立了领域模型，划定了业务领域的边界，建立了通用语言和限界上下文，确定了领域模型中各个领域对象的关系。到这儿，业务端领域模型的设计工作基本就完成了，这个过程同时也基本确定了应用端的微服务边界。\n\n在从业务模型向微服务落地的过程中，也就是从战略设计向战术设计的实施过程中，我们会将领域模型中的领域对象与代码模型中的代码对象建立映射关系，将业务架构和系统架构进行绑定。当我们去响应业务变化调整业务架构和领域模型时，系统架构也会同时发生调整，并同步建立新的映射关系。\n\n## DDD 与微服务的关系\n\n有了上面的讲解，现在我们不妨再次总结下 DDD 与微服务的关系。\n\nDDD 是一种架构设计方法，微服务是一种架构风格，两者从本质上都是为了追求高响应力，而从 业务视角\n去分离应用系统建设复杂度的手段。两者都强调从业务出发，其核心要义是强调根据业务发展，合理划分领域边界，持续调整现有架构，优化现有代码，以保持架构和代码的生命力，也就是我们常说的演进式架构。\n\n- DDD 主要关注：从业务领域视角划分领域边界，构建通用语言进行高效沟通，通过业务抽象，建立领域模型，维持业务和代码的逻辑一致性。\n- 微服务主要关注：运行时的进程间通信、容错和故障隔离，实现去中心化数据管理和去中心化服务治理，关注微服务的独立开发、测试、构建和部署。\n\n# DDD的一些术语\n\n领域驱动设计（DDD）涉及许多特定的术语和概念，这些术语和概念有助于理解和实施领域驱动设计方法。以下是一些重要的DDD术语和概念：\n\n1. 领域（Domain）：指特定问题领域或业务领域，包括相关的业务概念、规则和流程。在DDD中，设计的重点是深入理解和建模这个领域。\n2. 领域模型（Domain Model）：领域模型是对业务领域中重要概念、实体、值对象、聚合以及它们之间关系的抽象表示。它是将业务逻辑和规则映射到软件设计中的核心。\n3. 实体（Entity）：在领域模型中表示具有唯一标识的对象，它具有状态和行为。实体是领域模型中最常见的元素之一，例如，订单、用户等。\n4. 值对象（Value Object）：在领域模型中表示没有唯一标识，仅由其属性值定义的对象。值对象通常用于描述领域中的属性，例如，日期范围、地址等。\n5. 聚合（Aggregate）：一组相关实体和值对象的集合，由一个根实体（聚合根）管理其内部的一致性和完整性。聚合根是对外部的访问入口，保护了聚合内部的一致性。\n6. 聚合根（Aggregate Root）：聚合中的一个实体，作为整个聚合的访问入口。所有对聚合内部的操作都应该通过聚合根进行。\n7. 仓储（Repository）：用于管理领域对象的持久化和检索。仓储提供了一种抽象层，使应用程序可以从数据存储中获取和存储领域对象，而不必关心底层细节。\n8. 领域事件（Domain Event）：表示在领域中发生的重要事件，它们可以被触发和发布，以便其他部分能够对事件做出响应。领域事件通常用于解耦和通信。\n9. 限界上下文（Bounded Context）：指在领域模型中的一个明确定义的边界，限制了一组特定的业务概念和规则。不同的限界上下文可以有不同的术语和含义，但在各自的边界内是一致的。\n10. 领域专家（Domain Expert）：是对特定领域非常了解的人，通常是业务领域中的专业人士，他们与开发团队合作，共同理解和建模业务。\n\n![ddd总结图.png](..%2Fimg%2F%E6%9E%B6%E6%9E%84%2FDDD%2Fddd%E6%80%BB%E7%BB%93%E5%9B%BE.png)\n\n这幅图总结的很全，他把DDD划分不同的层级，最里层是值、属性、唯一标识等，这个是最基本的数据单位，但不能直接使用。然后是实体，这个把基础的数据进行封装，可以直接使用，在代码中就是封装好的一个个实体对象。之后就是领域层，它按照业务划分为不同的领域，比如订单领域、商品领域、支付领域等。最后是应用服务，它对业务逻辑进行编排，也可以理解为业务层。\n\n## 领域和子域\n\n在研究和解决业务问题时，DDD 会按照一定的规则将业务领域进行细分，当领域细分到一定的程度后，DDD\n会将问题范围限定在特定的边界内，在这个边界内建立领域模型，进而用代码实现该领域模型，解决相应的业务问题。简言之，DDD\n的领域就是这个边界内要解决的业务问题域。\n\n领域可以进一步划分为子领域。我们把划分出来的多个子领域称为子域，每个子域对应一个更小的问题域或更小的业务范围。\n\n领域的核心思想就是将问题域逐级细分，来降低业务理解和系统实现的复杂度。通过领域细分，逐步缩小服务需要解决的问题域，构建合适的领域模型。\n> 举个例子：\n>\n> 保险领域，我们可以把保险细分为承保、收付、再保以及理赔等子域，而承保子域还可以继续细分为投保、保全（寿险）、批改（财险）等子子域。\n\n## 核心域、通用域和支撑域\n\n子域可以根据重要程度和功能属性划分为如下：\n\n- 核心域：决定产品和公司核心竞争力的子域，它是业务成功的主要因素和公司的核心竞争力。\n- 通用域：没有太多个性化的诉求，同时被多个子域使用的通用功能的子域。\n- 支撑域：但既不包含决定产品和公司核心竞争力的功能，也不包含通用功能的子域。\n\n核心域、支撑域和通用域的主要目标是：通过领域划分，区分不同子域在公司内的不同功能属性和重要性，从而公司可对不同子域采取不同的资源投入和建设策略，其关注度也会不一样。\n\n很多公司的业务，表面看上去相似，但商业模式和战略方向是存在很大差异的，因此公司的关注点会不一样，在划分核心域、通用域和支撑域时，其结果也会出现非常大的差异。\n\n比如同样都是电商平台的淘宝、天猫、京东和苏宁易购，他们的商业模式是不同的。淘宝是 C2C 网站，个人卖家对个人买家，而天猫、京东和苏宁易购则是\nB2C 网站，是公司卖家对个人买家。即便是苏宁易购与京东都是 B2C\n的模式，苏宁易购是典型的传统线下卖场转型成为电商，京东则是直营加部分平台模式。因此，在公司建立领域模型时，我们就要结合公司战略重点和商业模式，重点关注核心域。\n\n## 通用语言和限界上下文\n\n- 通用语言：就是能够简单、清晰、准确描述业务涵义和规则的语言。\n- 限界上下文：用来封装通用语言和领域对象，提供上下文环境，保证在领域之内的一些术语、业务相关对象等（通用语言）有一个确切的含义，没有二义性。\n\n## 通用语言\n\n通用语言是团队统一的语言，不管你在团队中承担什么角色，在同一个领域的软件生命周期里都使用统一的语言进行交流。那么，通用语言的价值也就很明了，它可以解决交流障碍这个问题，使领域专家和开发人员能够协同合作，从而确保业务需求的正确表达。\n\n这个通用语言到场景落地，大家可能还很模糊，其实就是把领域对象、属性、代码模型对象等，通过代码和文字建立映射关系，可以通过Excel记录这个关系，这样研发可以通过代码知道这个含义，产品或者业务方可以通过文字知道这个含义，沟通起来就不会有歧义，说的简单一点，其实就是统一产品和研发的话术。\n\n直接看下面这幅图：\n\n![通用语言.png](..%2Fimg%2F%E6%9E%B6%E6%9E%84%2FDDD%2F%E9%80%9A%E7%94%A8%E8%AF%AD%E8%A8%80.png)\n\n## 限界上下文\n\n通用语言也有它的上下文环境，为了避免同样的概念或语义在不同的上下文环境中产生歧义，DDD 在战略设计上提出了“限界上下文”这个概念，用来确定语义所在的领域边界。\n\n限界上下文是一个显式的语义和语境上的边界，领域模型便存在于边界之内。边界内，通用语言中的所有术语和词组都有特定的含义。把限界上下文拆解开看，限界就是领域的边界，而上下文则是语义环境。通过领域的限界上下文，我们就可以在统一的领域边界内用统一的语言进行交流。\n\n## 实体和值对象\n\n- 实体 = 唯一身份标识 + 可变性【状态 + 行为】\n- 值对象 = 将一个值用对象的方式进行表述，来表达一个具体的固定不变的概念。\n\n## 实体\n\nDDD中要求实体是唯一的且可持续变化的。意思是说在实体的生命周期内，无论其如何变化，其仍旧是同一个实体。唯一性由唯一的身份标识来决定的。可变性也正反映了实体本身的状态和行为。实体以\nDO（领域对象）的形式存在，每个实体对象都有唯一的 ID。我们可以对一个实体对象进行多次修改，修改后的数据和原来的数据可能会大不相同。但是，由于它们拥有相同的\nID，它们依然是同一个实体。比如商品是商品上下文的一个实体，通过唯一的商品 ID 来标识，不管这个商品的数据如何变化，商品的 ID\n一直保持不变，它始终是同一个商品。\n\n## 值对象\n\n当你只关心某个对象的属性时，该对象便可作为一个值对象。 我们需要将值对象看成不变对象，不要给它任何身份标识，还应该尽量避免像实体对象一样的复杂性。\n\n还是举个订单的例子，订单是一个实体，里面包含地址，这个地址可以只通过属性嵌入的方式形成的订单实体对象，也可以将地址通过json序列化一个string类型的数据，存到DB的一个字段中，那么这个Json串就是一个值对象，是不是很好理解？下面给个简单的图：\n\n![值对象1.png](..%2Fimg%2F%E6%9E%B6%E6%9E%84%2FDDD%2F%E5%80%BC%E5%AF%B9%E8%B1%A11.png)\n\n![值对象2.png](..%2Fimg%2F%E6%9E%B6%E6%9E%84%2FDDD%2F%E5%80%BC%E5%AF%B9%E8%B1%A12.png)\n\n## 聚合和聚合根\n\n### 聚合\n\n聚合：我们把一些关联性极强、生命周期一致的实体、值对象放到一个聚合里。聚合是领域对象的显式分组，旨在支持领域模型的行为和不变性，同时充当一致性和事务性边界。\n\n聚合有一个聚合根和上下文边界，这个边界根据业务单一职责和高内聚原则，定义了聚合内部应该包含哪些实体和值对象，而聚合之间的边界是松耦合的。按照这种方式设计出来的服务很自然就是“高内聚、低耦合”的。\n\n聚合在 DDD 分层架构里属于领域层，领域层包含了多个聚合，共同实现核心业务逻辑。跨多个实体的业务逻辑通过领域服务来实现，跨多个聚合的业务逻辑通过应用服务来实现。比如有的业务场景需要同一个聚合的\nA 和 B 两个实体来共同完成，我们就可以将这段业务逻辑用领域服务来实现；而有的业务逻辑需要聚合 C 和聚合 D\n中的两个服务共同完成，这时你就可以用应用服务来组合这两个服务。\n\n### 聚合根\n\n如果把聚合比作组织，那聚合根就是这个组织的负责人。聚合根也称为根实体，它不仅是实体，还是聚合的管理者。\n\n- 首先它作为实体本身，拥有实体的属性和业务行为，实现自身的业务逻辑。\n- 其次它作为聚合的管理者，在聚合内部负责协调实体和值对象按照固定的业务规则协同完成共同的业务逻辑。\n- 最后在聚合之间，它还是聚合对外的接口人，以聚合根 ID 关联的方式接受外部任务和请求，在上下文内实现聚合之间的业务协同。也就是说，聚合之间通过聚合根\n  ID 关联引用，如果需要访问其它聚合的实体，就要先访问聚合根，再导航到聚合内部实体，外部对象不能直接访问聚合内实体。\n  上面讲的还是有些抽象，下面看一个图就能很好理解：\n\n![聚合.png](..%2Fimg%2F%E6%9E%B6%E6%9E%84%2FDDD%2F%E8%81%9A%E5%90%88.png)\n\n简单概括一下：\n\n- 通过事件风暴（我理解就是头脑风暴，不过我们一般都是先通过个人理解，然后再和相关核心同学进行沟通），得到实体和值对象；\n- 将这些实体和值对象聚合为“投保聚合”和“客户聚合”，其中“投保单”和“客户”是两者的聚合根；\n- 找出与聚合根“投保单”和“客户”关联的所有紧密依赖的实体和值对象；\n- 在聚合内根据聚合根、实体和值对象的依赖关系，画出对象的引用和依赖模型。\n\n## 领域服务和应用服务\n\n### 领域服务\n\n当一些逻辑不属于某个实体时，可以把这些逻辑单独拿出来放到领域服务中，理想的情况是没有领域服务，如果领域服务使用不恰当，慢慢又演化回了以前逻辑都在service层的局面。\n\n可以使用领域服务的情况：\n\n- 执行一个显著的业务操作\n- 对领域对象进行转换\n- 以多个领域对象作为输入参数进行计算，结果产生一个值对象\n\n### 应用服务\n\n应用层作为展现层与领域层的桥梁，是用来表达用例和用户故事的主要手段。\n\n应用层通过应用服务接口来暴露系统的全部功能。在应用服务的实现中，它负责编排和转发，它将要实现的功能委托给一个或多个领域对象来实现，它本身只负责处理业务用例的执行顺序以及结果的拼装。通过这样一种方式，它隐藏了领域层的复杂性及其内部实现机制。\n\n应用层相对来说是较“薄”的一层，除了定义应用服务之外，在该层我们可以进行安全认证，权限校验，持久化事务控制，或者向其他系统发生基于事件的消息通知，另外还可以用于创建邮件以发送给客户等。\n\n## 领域事件\n\n领域事件 = 事件发布 + 事件存储 + 事件分发 + 事件处理。\n\n领域事件是一个领域模型中极其重要的部分，用来表示领域中发生的事件。忽略不相关的领域活动，同时明确领域专家要跟踪或希望被通知的事情，或与其他模型对象中的状态更改相关联，下面简单说明领域事件：\n\n- 事件发布：构建一个事件，需要唯一标识，然后发布；\n- 事件存储：发布事件前需要存储，因为接收后的事件也会存储，可用于重试或对账等；\n- 事件分发：服务内直接发布给订阅者，服务外需要借助消息中间件，比如Kafka，RabbitMQ等；\n- 事件处理：先将事件存储，然后再处理。\n\n比如下订单后，给用户增长积分与赠送优惠券的需求。如果使用瀑布流的方式写代码。一个个逻辑调用，那么不同用户，赠送的东西不同，逻辑就会变得又臭又长。这里的比较好的方式是，用户下订单成功后，发布领域事件，积分聚合与优惠券聚合监听订单发布的领域事件进行处理。\n\n## 资源库【仓储】\n\n仓储介于领域模型和数据模型之间，主要用于聚合的持久化和检索。它隔离了领域模型和数据模型，以便我们关注于领域模型而不需要考虑如何进行持久化。\n\n我们将暂时不使用的领域对象从内存中持久化存储到磁盘中。当日后需要再次使用这个领域对象时，根据 key\n值到数据库查找到这条记录，然后将其恢复成领域对象，应用程序就可以继续使用它了，这就是领域对象持久化存储的设计思想。\n\n# DDD分层架构\n\n在领域驱动设计（DDD）中采用的是松散分层架构，层间关系不那么严格。每层都可能使用它下面所有层的服务，而不仅仅是下一层的服务。每层都可能是半透明的，这意味着有些服务只对上一层可见，而有些服务对上面的所有层都可见。\n\n![DDD分层架构.png](..%2Fimg%2F%E6%9E%B6%E6%9E%84%2FDDD%2FDDD%E5%88%86%E5%B1%82%E6%9E%B6%E6%9E%84.png)\n\n分层的作用，从上往下：\n\n- 用户交互层：web请求，rpc请求，mq消息等外部输入均被视为外部输入的请求，可能修改到内部的业务数据。\n- 业务应用层：与MVC中的service不同的不是，service中存储着大量业务逻辑。但在应用服务的实现中（以功能点为维度），它负责编排、转发、校验等。（在设计和开发时，不要将本该放在领域层的业务逻辑放到应用层中实现。因为庞大的应用层会使领域模型失焦，时间一长你的服务就会演化为传统的三层架构，业务逻辑会变得混乱。）\n- 领域层：或称为模型层，系统的核心，负责表达业务概念，业务状态信息以及业务规则。即包含了该领域（问题域）所有复杂的业务知识抽象和规则定义。该层主要精力要放在领域对象分析上，可以从实体，值对象，聚合（聚合根），领域服务，领域事件，仓储，工厂等方面入手。\n- 基础设施层：主要有2方面内容，一是为领域模型提供持久化机制，当软件需要持久化能力时候才需要进行规划；一是对其他层提供通用的技术支持能力，如消息通信，通用工具，配置等的实现。\n\n应用服务层直接调用基础设施层的一条线，这条线是什么意思呢？领域模型的建立是为了控制对于数据的增删改的业务边界，至于数据查询，不同的报表，不同的页面需要展示的数据聚合不具备强业务领域，因此常见的会使用CQRS方式进行查询逻辑的处理。其它的直接调用，原理类同。\n\n## 三层架构如何演进到 DDD 分层架构\n\n![三层架构如何演进到DDD分层架构.png](..%2Fimg%2F%E6%9E%B6%E6%9E%84%2FDDD%2F%E4%B8%89%E5%B1%82%E6%9E%B6%E6%9E%84%E5%A6%82%E4%BD%95%E6%BC%94%E8%BF%9B%E5%88%B0DDD%E5%88%86%E5%B1%82%E6%9E%B6%E6%9E%84.png)\n\n我们看一下上面这张图，分析一下从三层架构向 DDD 分层架构演进的过程。\n\n首先，你要清楚，三层架构向 DDD 分层架构演进，主要发生在业务逻辑层和数据访问层。\n\nDDD 分层架构在用户接口层引入了 DTO，给前端提供了更多的可使用数据和更高的展示灵活性。\n\nDDD 分层架构对三层架构的业务逻辑层进行了更清晰的划分，改善了三层架构核心业务逻辑混乱，代码改动相互影响大的情况。DDD\n分层架构将业务逻辑层的服务拆分到了应用层和领域层。应用层快速响应前端的变化，领域层实现领域模型的能力。\n\n另外一个重要的变化发生在数据访问层和基础层之间。三层架构数据访问采用 DAO 方式；DDD\n分层架构的数据库等基础资源访问，采用了仓储（Repository）设计模式，通过依赖倒置实现各层对基础资源的解耦。\n\n仓储又分为两部分：仓储接口和仓储实现。仓储接口放在领域层中，仓储实现放在基础层。原来三层架构通用的第三方工具包、驱动、Common、Utility、Config\n等通用的公共的资源类统一放到了基础层。\n\n最后，我想说，传统三层架构向 DDD 分层架构的演进，体现的正是领域驱动设计思想的演进。希望你也感受到了，并尝试将其应用在自己的架构设计中。\n\n当然不是所有的业务服务都合适做DDD架构，DDD合适产品化，可持续迭代，业务逻辑足够复杂的业务系统，中小规模的系统与团队还是不建议使用的，毕竟相比较与MVC架构，成本很大。\n\n## 微服务要有合理的架构分层\n\n微服务设计要有分层的设计思想，让各层各司其职，建立松耦合的层间关系。\n\n不要把与领域无关的逻辑放在领域层实现，保证领域层的纯洁和领域逻辑的稳定，避免污染领域模型。也不要把领域模型的业务逻辑放在应用层，这样会导致应用层过于庞大，最终领域模型会失焦。如果实在无法避免，我们可以引入防腐层，进行新老系统的适配和转换，过渡期完成后，可以直接将防腐层代码抛弃。\n\n微服务内部的分层方式我们已经清楚了，那微服务之间是否也有层次依赖关系呢？如何实现微服务之间的服务集成？\n\n有的微服务可以与前端应用集成，一起完成特定的业务，这是项目级微服务。而有的则是某个职责单一的中台微服务，企业级的业务流程需要将多个这样的微服务组合起来才能完成，这是企业级中台微服务。两类微服务由于复杂度不一样，集成方式也会有差异。\n\n## 项目级微服务\n\n项目级微服务的内部遵循分层架构模型就可以了。领域模型的核心逻辑在领域层实现，服务的组合和编排在应用层实现，通过 API\n网关为前台应用提供服务，实现前后端分离。但项目级的微服务可能会调用其它微服务，你看在下面这张图中，比如某个项目级微服务 B\n调用认证微服务 A，完成登录和权限认证。\n\n通常项目级微服务之间的集成，发生在微服务的应用层，由应用服务调用其它微服务发布在 API 网关上的应用服务。你看下图中微服务 B\n中红色框内的应用服务 B，它除了可以组合和编排自己的领域服务外，还可以组合和编排外部微服务的应用服务。它只要将编排后的服务发布到\nAPI 网关供前端调用，这样前端就可以直接访问自己的微服务了。\n\n![项目级微服务.png](..%2Fimg%2F%E6%9E%B6%E6%9E%84%2FDDD%2F%E9%A1%B9%E7%9B%AE%E7%BA%A7%E5%BE%AE%E6%9C%8D%E5%8A%A1.png)\n\n## 企业级中台微服务\n\n企业级的业务流程往往是多个中台微服务一起协作完成的，那跨中台的微服务如何实现集成呢？\n\n企业级中台微服务的集成不能像项目级微服务一样，在某一个微服务内完成跨微服务的服务组合和编排。\n\n我们可以在中台微服务之上增加一层，你看下面这张图，增加的这一层就位于红色框内，它的主要职能就是处理跨中台微服务的服务组合和编排，以及微服务之间的协调，它还可以完成前端不同渠道应用的适配。如果再将它的业务范围扩大一些，我可以将它做成一个面向不同行业和渠道的服务平台。\n\n我们不妨借用 BFF（服务于前端的后端，Backend for Frontends）这个词，暂且称它为 BFF 微服务。BFF\n微服务与其它微服务存在较大的差异，就是它没有领域模型，因此这个微服务内也不会有领域层。BFF\n微服务可以承担应用层和用户接口层的主要职能，完成各个中台微服务的服务组合和编排，可以适配不同前端和渠道的要求。\n\n![企业级中台微服务.png](..%2Fimg%2F%E6%9E%B6%E6%9E%84%2FDDD%2F%E4%BC%81%E4%B8%9A%E7%BA%A7%E4%B8%AD%E5%8F%B0%E5%BE%AE%E6%9C%8D%E5%8A%A1.png)\n\n## 应用和资源的解耦与适配\n\n传统以数据为中心的设计模式，应用会对数据库、缓存、文件系统等基础资源产生严重依赖。\n\n正是由于它们之间的这种强依赖的关系，我们一旦更换基础资源就会对应用产生很大的影响，因此需要为应用和资源解耦。\n\n在微服务架构中，应用层、领域层和基础层解耦是通过仓储模式，采用依赖倒置的设计方法来实现的。在应用设计中，我们会同步考虑和基础资源的代码适配，那么一旦基础设施资源出现变更（比如换数据库），就可以屏蔽资源变更对业务代码的影响，切断业务逻辑对基础资源的依赖，最终降低资源变更对应用的影响。\n\n## DDD、中台和微服务的协作模式\n\n传统企业可以将需要共享的公共能力进行领域建模，建设可共享的 通用中台。除此之外，传统企业还会将核心能力进行领域建模，建设面向不同渠道的可复用的\n核心中台。\n\n而这里的通用中台和核心中台都属于我们上一讲讲到的业务中台的范畴。\n\nDDD 的子域分为核心域、通用域和支撑域。划分这几个子域的主要目的是为了确定战略资源的投入，一般来说战略投入的重点是核心域，因此后面我们就可以暂时不严格区分支撑域和通用域了。\n\n领域、中台以及微服务虽然属于不同层面的东西，但我们还是可以将他们分解对照，整理出来它们之间的关系。你看下面这张图，我是从 DDD\n领域建模和中台建设这两个不同的视角对同一个企业的业务架构进行分析。\n\n![ddd中台和微服务协作.png](..%2Fimg%2F%E6%9E%B6%E6%9E%84%2FDDD%2Fddd%E4%B8%AD%E5%8F%B0%E5%92%8C%E5%BE%AE%E6%9C%8D%E5%8A%A1%E5%8D%8F%E4%BD%9C.png)\n\n如果将企业内整个业务域作为一个问题域的话，企业内的所有业务就是一个领域。在进行领域细分时，从 DDD\n视角来看，子域可分为核心域、通用域和支撑域。从中台建设的视角来看，业务域细分后的业务中台，可分为核心中台和通用中台。\n\n从领域功能属性和重要性对照来看，通用中台对应 DDD 的通用域和支撑域，核心中台对应 DDD\n的核心域。从领域的功能范围来看，子域与中台是一致的。领域模型所在的限界上下文对应微服务。建立了这个映射关系，我们就可以用 DDD\n来进行中台业务建模了。\n\n我们这里还是以保险领域为例。 保险域的业务中台分为两类：第一类是提供保险核心业务能力的核心中台（比如营销、承保和理赔等业务）；第二类是支撑核心业务流程完成保险全流程的通用中台（比如订单、支付、客户和用户等）。\n\n这里我要提醒你一下：根据 DDD 首先要建立通用语言的原则，在将 DDD 的方法引入中台设计时，我们要先建立中台和 DDD\n的通用语言。这里的子域与中台是一致的，那我们就可以将子域统一为中台。\n\n中台通过事件风暴可以进一步细分，最终完成业务领域建模。中台业务领域的功能不同，限界上下文的数量和大小就会不一样，领域模型也会不一样。\n\n当完成业务建模后，我们就可以采用 DDD 战术设计，设计出聚合、实体、领域事件、领域服务以及应用服务等领域对象，再利用分层架构模型完成微服务的设计。\n\n以上就是 DDD、中台和微服务在应用过程中的协作模式。\n\n## 中台如何建模？\n\n看完了三者的协作模式，我们就顺着上面的话题，接着来聊聊中台如何建模。\n\n中台业务抽象的过程就是业务建模的过程，对应 DDD 的战略设计。系统抽象的过程就是微服务的建设过程，对应 DDD 的战术设计。下面我们就结合\nDDD 领域建模的方法，讲一下中台业务建模的过程。\n\n第一步：\n按照业务流程（通常适用于核心域）或者功能属性、集合（通常适用于通用域或支撑域），将业务域细分为多个中台，再根据功能属性或重要性归类到核心中台或通用中台。核心中台设计时要考虑核心竞争力，通用中台要站在企业高度考虑共享和复用能力。\n\n第二步： 选取中台，根据用例、业务场景或用户旅程完成事件风暴，找出实体、聚合和限界上下文。依次进行领域分解，建立领域模型。\n\n由于不同中台独立建模，某些领域对象或功能可能会重复出现在其它领域模型中，也有可能本该是同一个聚合的领域对象或功能，却分散在其它的中台里，这样会导致领域模型不完整或者业务不内聚。这里先不要着急，这一步我们只需要初步确定主领域模型就可以了，在第三步中我们还会提炼并重组这些领域对象。\n\n第三步： 以主领域模型为基础，扫描其它中台领域模型，检查并确定是否存在重复或者需要重组的领域对象、功能，提炼并重构主领域模型，完成最终的领域模型设计。\n\n第四步： 选择其它主领域模型重复第三步，直到所有主领域模型完成比对和重构。\n\n第五步： 基于领域模型完成微服务设计，完成系统落地。\n\n![中台如何建模.png](..%2Fimg%2F%E6%9E%B6%E6%9E%84%2FDDD%2F%E4%B8%AD%E5%8F%B0%E5%A6%82%E4%BD%95%E5%BB%BA%E6%A8%A1.png)\n\n结合上面这张图，你可以大致了解到 DDD 中台设计的过程。DDD 战略设计包括上述的第一步到第四步，主要为：业务域分解为中台，对中台归类，完成领域建模，建立中台业务模型。DDD\n战术设计是第五步，领域模型映射为微服务，完成中台建设。\n\n![中台如何建模2.png](..%2Fimg%2F%E6%9E%B6%E6%9E%84%2FDDD%2F%E4%B8%AD%E5%8F%B0%E5%A6%82%E4%BD%95%E5%BB%BA%E6%A8%A12.png)\n\n那么如果还是以保险领域为例的话，完成领域建模后，里面的数据我们就可以填上了。这里我选取了通用中台的用户、客户和订单三个中台来做示例。客户中台提炼出了两个领域模型：客户信息和客户视图模型。用户中台提炼出了三个领域模型：用户管理、登录认证和权限模型。订单中台提炼出了订单模型。\n\n这就是中台建模的全流程，当然看似简单的背后，若是遇上复杂的业务总会出现各种各样的问题，不然应用起来也不会有那么多的困难。\n\n# COLA\n\nhttps://www.youtube.com/watch?v=u6528XnMVFo\n\nCOLA 是 Clean Object-Oriented and Layered Architecture的缩写，代表“整洁面向对象分层架构”。 目前COLA已经发展到COLA v4\n\nhttps://github.com/alibaba/COLA\n\nCOLA的架构\n\n![cola架构.png](..%2Fimg%2F%E6%9E%B6%E6%9E%84%2FDDD%2Fcola%E6%9E%B6%E6%9E%84.png)\n\n![cola架构2.png](..%2Fimg%2F%E6%9E%B6%E6%9E%84%2FDDD%2Fcola%E6%9E%B6%E6%9E%842.png)\n\n其次，还有一个官方的表格，介绍了COLA中每个层的命名和含义：\n\n| 层次\t         | 包名\t                 | 功能\t                        | 必选 |\n|-------------|---------------------|----------------------------|----|\n| Adapter层\t   | web                 | \t处理页面请求的Controller\t        | 否  |\n| Adapter层\t   | wireless\t           | 处理无线端的适配\t                  | 否  |\n| Adapter层\t   | wap\t                | 处理wap端的适配\t                 | 否  |\n| App层\t       | executor\t           | 处理request，包括command和query\t | 是  |\n| App层\t       | consumer\t           | 处理外部message\t               | 否  |\n| App层\t       | scheduler\t          | 处理定时任务\t                    | 否  |\n| Domain层\t    | model\t              | 领域模型\t                      | 否  |\n| Domain层\t    | ability\t            | 领域能力，包括DomainService\t      | 否  |\n| Domain层\t    | gateway\t            | 领域网关，解耦利器\t                 | 是  |\n| Infra层\t     | gatewayimpl\t        | 网关实现\t                      | 是  |\n| Infra层\t     | mapper\tibatis数据库映射\t | 否                          |\n| Infra层\t     | config\t配置信息\t        | 否                          |\n| Client SDK\t | api\t                | 服务对外透出的API\t                | 是  |\n| Client SDK\t | dto\t                | 服务对外的DTO\t                  | 是  |\n\n这两张图和一个表格已经把整个COLA架构的绝大部分内容展现给了大家，但是一下子这么多信息量可能很难消化。\n\n既然整个示例架构项目是一个Maven父子结构，那我们就从父模块一个个好好过一遍。\n\n首先父模块的pom.xml包含了如下子模块：\n\n```xml\n\n<modules>\n    <module>demo-web-client</module>\n    <module>demo-web-adapter</module>\n    <module>demo-web-app</module>\n    <module>demo-web-domain</module>\n    <module>demo-web-infrastructure</module>\n    <module>start</module>\n</modules>\n```\n\n## start层\n\n该模块作为整个应用的启动模块（通常是一个SpringBoot应用），只承担启动项目和全局相关配置项的存放职责。代码目录如下：\n\n![start层.png](..%2Fimg%2F%E6%9E%B6%E6%9E%84%2FDDD%2Fcola%2Fstart%E5%B1%82.png)\n\n将启动独立出来，好处是清晰简洁，也能让新人一眼就看出如何运行项目，以及项目的一些基础依赖。\n\n## adapter层\n\n接下来我们按照之前架构图从上到下的顺序，一个个看。\n\n首先是demo-web-adapter模块，这名字是不是很新鲜？但其实，可以理解为平时我们用的controller层（对于Web应用来说），换汤不换药。\n\n在COLA官方博客中，也能找到如下的描述：\n\n> Controller这个名字主要是来自于MVC，因为是MVC，所以自带了Web应用的烙印。然而，随着mobile的兴起，现在很少有应用仅仅只支持Web端，通常的标配是Web，Mobile，WAP三端都要支持。\n\n![adapter层.png](..%2Fimg%2F%E6%9E%B6%E6%9E%84%2FDDD%2Fcola%2Fadapter%E5%B1%82.png)\n\n## cilent层\n\n有了我们说的“controller”层，接下来有的小伙伴肯定就会想，是不是service层啦。\n\n是，也不是。\n\n传统的Web应用中，完全可以只有一个service层给controller层调用，但是作为一个业务应用，除非你真的只是个前端页面的无情吐数据机器，否则很大可能性你的应用会有很多其他上下游调用方，并且你需要提供接口给他们。\n\n这时候你给他们的不应该是一个Web接口，应该是RPC调用的服务层接口，至于原因不是本文的重点，具体就不展开了。\n\n所以在COLA中，你的adapter层，调用了client层，client层中就是你服务接口的定义。\n\n![client层.png](..%2Fimg%2F%E6%9E%B6%E6%9E%84%2FDDD%2Fcola%2Fclient%E5%B1%82.png)\n\n从上图中可以看到，client包里有：\n\n- api文件夹：存放服务接口定义\n- dto文件夹：存放传输实体\n\n注意，这里只是服务接口定义，而不是服务层的具体实现，所以在adapter层中，调用的其实是client层的接口：\n\n```java\n\n@RestController\npublic class CustomerController {\n\n    @Autowiredprivate\n    CustomerServiceI customerService;\n\n    @GetMapping(value = \"/customer\")\n    public MultiResponse<CustomerDTO> listCustomerByName(@RequestParam(required = false) String name) {\n        CustomerListByNameQry customerListByNameQry = new CustomerListByNameQry();\n        customerListByNameQry.setName(name);\n        return customerService.listByName(customerListByNameQry);\n    }\n\n}\n```\n\n而最终接口的具体实现逻辑放到了app层。\n\n```java\n\n@Service\n@CatchAndLog\npublic class CustomerServiceImpl implements CustomerServiceI {\n\n    @Resource\n    private CustomerListByNameQryExe customerListByNameQryExe;\n\n    @Override\n    public MultiResponse<CustomerDTO> listByName(CustomerListByNameQry customerListByNameQry) {\n        return customerListByNameQryExe.execute(customerListByNameQry);\n    }\n\n}\n```\n\n## app层\n\n接着上面说的，我们的app模块作为服务的实现，存放了各个业务的实现类，并且严格按照业务分包，这里划重点，是先按照业务分包，再按照功能分包的，为何要这么做，文章后面还会多说两句，先看图：\n\n![app层.png](..%2Fimg%2F%E6%9E%B6%E6%9E%84%2FDDD%2Fcola%2Fapp%E5%B1%82.png)\n\ncustomer和order分别对应了消费着和订单两个业务子领域。里面是COLA定义app层下面三种功能：\n\n| a    | b         | c                         | d |\n|------|-----------|---------------------------|---|\n| App层 | executor  | 处理request，包括command和query | 是 |\n| App层 | consumer  | 处理外部message               | 否 |\n| App层 | scheduler | 处理定时任务                    | 否 |\n\n可以看到，消息队列的消费者和定时任务，这类平时我们业务开发经常会遇到的场景，也放在app层。\n\n## domain层\n\n接下来便是domain，也就是领域层，先看一下领域层整体结构：\n\n![domain层.png](..%2Fimg%2F%E6%9E%B6%E6%9E%84%2FDDD%2Fcola%2Fdomain%E5%B1%82.png)\n\n可以看到，首先是按照不同的领域（customer和order）分包，里面则是三种主要的文件类型：\n\n1. 领域实体：实体模型可以是充血模型（请自行了解），例如官方示例里的Customer.java如下：\n\n```java\n\n@Data\n@Entitypublic\nclass Customer {\n\n    private String customerId;\n    private String memberId;\n    private String globalId;\n    private long registeredCapital;\n    private String companyName;\n    private SourceType sourceType;\n    private CompanyType companyType;\n\n    public Customer() {\n    }\n\n    public boolean isBigCompany() {\n        return registeredCapital > 10000000; //注册资金大于1000万的是大企业\n    }\n\n    public boolean isSME() {\n        return registeredCapital > 10000 && registeredCapital < 1000000; //注册资金大于10万小于100万的为中小企业\n    }\n\n    public void checkConfilict() {\n        //Per different biz, the check policy could be different, if so, use ExtensionPointif(\"ConflictCompanyName\".equals(this.companyName)){\n        throw new BizException(this.companyName + \" has already existed, you can not add it\");\n    }\n}\n}\n```\n\n1. 领域能力：domainservice文件夹下，是领域对外暴露的服务能力，如上图中的CreditChecker\n2. 领域网关：gateway文件夹下的接口定义，这里的接口你可以粗略的理解成一种SPI，也就是交给infrastructure层去实现的接口。\n\n例如CustomerGateway里定义了接口getByById，要求infrastructure的实现类必须定义如何通过消费者Id获取消费者实体信息，而infrastructure层可以实现任何数据源逻辑，比如，从MySQL获取，从Redis获取，还是从外部API获取等等。\n\n```java\npublic interface CustomerGateway {\n    public Customer getByById(String customerId);\n}\n```\n\n在示例代码的CustomerGatewayImpl（位于infrastructure层）中，CustomerDO（数据库实体）经过MyBatis的查询，转换为了Customer领域实体，进行返回。完成了依赖倒置。\n\n```java\n\n@Componentpublic\nclass CustomerGatewayImpl implements CustomerGateway {\n    @Autowiredprivate\n    CustomerMapper customerMapper;\n\n    public Customer getByById(String customerId) {\n        CustomerDO customerDO = customerMapper.getById(customerId);\n        //Convert to Customerreturn null;\n    }\n}\n```\n\n![domain层转换.png](..%2Fimg%2F%E6%9E%B6%E6%9E%84%2FDDD%2Fcola%2Fdomain%E5%B1%82%E8%BD%AC%E6%8D%A2.png)\n\n## infrastructure层\n\n最后是我们的infrastructure也就是基础设施层，这层有我们刚才提到的gatewayimpl网关实现，也有MyBatis的mapper等数据源的映射和config配置文件。\n\n|        |              |              |\n|--------|--------------|--------------|\n| Infra层 | \tgatewayimpl | \t网关实现        |\t是|\n| Infra层 | \tmapper      | \tibatis数据库映射 |\t否|\n| Infra层 | \tconfig      | \t配置信息        |\t否|\n\n![start层.png](..%2Fimg%2F%E6%9E%B6%E6%9E%84%2FDDD%2Fcola%2Fstart%E5%B1%82.png)\n\n\n所有层讲完了，COLA4.0很简单明了，最后，在引用一段官方介绍博客原文来总结COLA的层级：\n\n1）适配层（Adapter Layer）：负责对前端展示（web，wireless，wap）的路由和适配，对于传统B/S系统而言，adapter就相当于MVC中的controller；\n\n2）应用层（Application Layer）：主要负责获取输入，组装上下文，参数校验，调用领域层做业务处理，如果需要的话，发送消息通知等。层次是开放的，应用层也可以绕过领域层，直接访问基础实施层；\n\n3）领域层（Domain Layer）：主要是封装了核心业务逻辑，并通过领域服务（Domain Service）和领域对象（Domain Entity）的方法对App层提供业务实体和业务逻辑计算。领域是应用的核心，不依赖任何其他层次；\n\n4）基础实施层（Infrastructure Layer）：主要负责技术细节问题的处理，比如数据库的CRUD、搜索引擎、文件系统、分布式服务的RPC等。此外，领域防腐的重任也落在这里，外部依赖需要通过gateway的转义处理，才能被上面的App层和Domain层使用。\n\n## COLA架构的特色\n\n说完了分层架构，我们再来回顾下上面提到的COLA架构的几个特色的设计\n### 领域与功能的分包策略\n也就是下面这张图的意思，先按照领域分包，再按照功能分包，这样做的其中一点好处是能将腐烂控制在该业务域内。\n\n比如消费者customer和订单order两个领域是两个后端开发并行开发，两个人对于dto，util这些文件夹的命名习惯都不同，那么只会腐烂在各自的业务包下面，而不会将dto,util,config等文件夹放在一起，极容易引发文件冲突。\n\n![领域与功能的分包策略.png](..%2Fimg%2F%E6%9E%B6%E6%9E%84%2FDDD%2Fcola%2F%E9%A2%86%E5%9F%9F%E4%B8%8E%E5%8A%9F%E8%83%BD%E7%9A%84%E5%88%86%E5%8C%85%E7%AD%96%E7%95%A5.png)\n\n![领域与功能的分包策略2.png](..%2Fimg%2F%E6%9E%B6%E6%9E%84%2FDDD%2Fcola%2F%E9%A2%86%E5%9F%9F%E4%B8%8E%E5%8A%9F%E8%83%BD%E7%9A%84%E5%88%86%E5%8C%85%E7%AD%96%E7%95%A52.png)\n\n> 前面的包定义，都是功能维度的定义。为了兼顾领域维度的内聚性，我们有必要对包结构进行一下微调，即顶层包结构应该是按照领域划分，让领域内聚。\n\n### 业务域和外部依赖解耦\n\n前面提到的domain和infrastructure层的依赖倒置，是一个非常有用的设计，进一步解耦了取数逻辑的实现。\n\n例如下图中，你的领域实体是商品item，通过gateway接口，你的商品的数据源可以是数据库，也可以是外部的服务API。\n\n如果是外部的商品服务，你经过API调用后，商品域吐出的是一个大而全的DTO（可能包含几十个字段），而在下单这个阶段，订单所需要的可能只是其中几个字段而已。你拿到了外部领域DTO，转为自己领域的Item，只留下标题价格库存等必要的数据字段。\n\n![infrastructure层.png](..%2Fimg%2F%E6%9E%B6%E6%9E%84%2FDDD%2Fcola%2Finfrastructure%E5%B1%82.png)\n\n# 链接\n\n- https://tech.meituan.com/2017/12/22/ddd-in-practice.html\n- https://github.com/alibaba/COLA\n- https://www.cnblogs.com/rude3knife/p/cola-architecture.html\n- https://blog.csdn.net/significantfrank/article/details/100074716\n- https://www.youtube.com/watch?v=u6528XnMVFo\n- https://www.youtube.com/watch?v=7kxZiJppcS0&list=PLn5XLkWHBxyt0jEp8DI_1gAsF6HsxKJRg&index=5\n- https://zq99299.github.io/note-book2/ddd/#%E6%8E%A8%E8%8D%90%E9%98%85%E8%AF%BB\n"
  },
  {
    "path": "架构/系统设计.md",
    "content": "\n# MVC设计\nMVC（Model-View-Controller）是一种软件设计模式，将应用程序划分为三个主要组成部分，分别是模型、视图和控制器。下面是每个组件的详细介绍：\n\n1. 模型（Model）：模型是应用程序中数据和业务逻辑的表示。它包含数据、状态以及操作数据的方法。在MVC中，模型通常是独立于用户界面和控制器的，因此可以被多个视图和控制器共享。\n2. 视图（View）：视图是应用程序中用户界面的表示。它负责显示模型中的数据，并与用户进行交互。在MVC中，视图通常是被控制器调用的，控制器通过视图来向用户呈现模型中的数据。\n3. 控制器（Controller）：控制器负责处理用户输入，并更新模型和视图。它接收来自用户的请求，将其转换为模型操作，并更新视图以反映模型中的更改。在MVC中，控制器是模型和视图之间的中介，它将它们联系在一起。\n\nMVC的主要目的是将应用程序的不同组成部分分离开来，以便它们可以独立开发、测试和维护。通过将模型、视图和控制器分离开来，可以使应用程序更加灵活、可扩展和可维护。另外，MVC还可以使应用程序的用户界面更加清晰和易于使用，因为它将界面逻辑与业务逻辑分离开来。\n\n总之，MVC是一种常见的软件设计模式，它将应用程序划分为模型、视图和控制器三个组成部分，以实现代码的分离和复用，从而提高系统的可维护性、可扩展性和可靠性。\n\n\n# SOA\n\nSOA（Service-Oriented Architecture，面向服务的架构）是一种设计和组织软件系统的方法，其中系统中的各个功能被组织成独立的服务，这些服务通过网络相互通信，实现系统的功能。\n\nSOA架构的核心概念是“服务”，即可被其他应用程序或服务调用的功能单元。服务可以根据其功能和领域进行分类和组织，以形成一个完整的应用程序。下面是SOA架构的一些特点和优势：\n\n1. 面向服务：SOA架构是一种面向服务的架构，系统中的各个功能被组织成独立的服务。这样可以提高系统的灵活性和可扩展性，可以方便地增加、删除或替换服务。\n2. 松耦合：SOA架构通过使用松耦合的通信方式，降低了系统组件之间的耦合度，可以使系统更加灵活、可维护和可扩展。\n3. 可重用性：SOA架构的服务可以被多个应用程序或服务共享和重用，可以提高开发效率和代码复用性。\n4. 分布式：SOA架构中的服务可以分布在不同的物理位置上，可以提高系统的可用性和可靠性。\n5. 与技术无关：SOA架构可以与特定的技术无关，可以使用不同的编程语言和平台来实现服务。\n\n\n\n# OOAP设计\n\nOOAP（Object-Oriented Analysis and Design）是一种基于对象的分析和设计方法，它强调软件系统中的对象、类和关系，并将它们组织成一个可重用、可维护和可扩展的系统。\n\n下面是一个简单的OOAP设计过程：\n\n1. 需求分析：对系统的需求进行分析和收集，确定系统的功能和约束条件。\n2. 概念建模：将系统中的对象抽象出来，将其组织成一个概念模型，用于描述系统的基本结构和行为。\n3. 行为建模：根据系统的需求和概念模型，定义系统中的对象的行为和交互方式。\n4. 设计模式：根据系统的需求和行为模型，选择适当的设计模式，以便实现系统的功能和优化系统的性能。\n5. 类设计：根据设计模式，设计系统中的类和对象，并确定它们的属性、方法和关系。\n6. 接口设计：定义系统的接口，以实现系统内部和外部的通信和交互。\n7. 数据库设计：设计系统的数据结构和数据库，以便存储和管理系统中的数据。\n8. 系统测试：测试系统的功能和性能，并确定系统是否满足需求。\n9. 系统部署：部署和实施系统，确保系统能够正确运行并满足用户的需求。\n\n总之，OOAP是一种结构化的方法，它强调对象的重用性、可维护性和可扩展性。通过OOAP设计，可以设计出高效、可靠和易于维护的软件系统。\n\n# DDD设计\nDDD（Domain-Driven Design）是一种基于领域模型的软件设计方法，强调通过深入领域的了解，来构建更贴近实际业务需求的软件系统。\n\n下面是一个简单的DDD设计过程：\n\n1. 洞察领域：通过与领域专家交流，深入了解业务领域的概念、术语和规则，并确定系统中的核心领域模型。\n2. 模型驱动设计：将领域模型转换为软件模型，设计并实现系统中的领域对象和领域服务。\n3. 上下文边界：将系统划分为多个上下文，每个上下文都有自己的领域模型和边界。\n4. 领域服务：在每个上下文中设计并实现领域服务，提供对领域对象的操作和查询。\n5. 聚合根：在领域模型中标识出聚合根，以便管理聚合内的对象。\n6. 限界上下文：在每个上下文中定义限界上下文，用于限制领域模型的适用范围。\n7. 事件驱动架构：采用事件驱动架构，以支持系统中的异步通信和领域事件处理。\n8. 领域事件：定义和实现领域事件，以便在系统中广播领域模型中的变化。\n9. 实现模式：根据实际情况，选择适当的实现模式，以支持系统的扩展和性能优化。\n\n总之，DDD是一种基于领域模型的软件设计方法，通过深入领域的了解，来构建更贴近实际业务需求的软件系统。采用DDD设计可以帮助我们更好地理解业务领域，提高系统的可维护性和可扩展性。\n\n# CQRS架构\nCQRS（Command Query Responsibility Segregation，命令查询责任分离）是一种架构模式，它通过将系统中的读写操作分离，以提高系统的可扩展性、可维护性和可靠性。下面是CQRS架构的一些特点和优势：\n\n1. 命令和查询分离：CQRS将系统中的读写操作分离，将查询操作和命令操作分别处理。这样可以有效地降低系统复杂度，提高系统的可维护性和可扩展性。\n2. 高度可扩展：CQRS架构可以很容易地扩展，可以根据需求增加更多的读或写节点，或者通过水平扩展来提高系统性能。\n3. 适应高并发：CQRS架构适用于高并发的场景，因为它可以有效地分离读写操作，避免了写操作的锁等待，从而提高了系统的并发性能。\n4. 高度可定制：CQRS架构可以很容易地根据需求进行定制和扩展，可以根据业务需求选择合适的数据存储方式、缓存策略等。\n5. 异步处理：CQRS架构可以采用异步处理方式，例如使用消息队列来实现命令的异步处理，以提高系统的可靠性和性能。\n\n![](../img/架构/CQRS架构.png)\n\n# 高内聚低耦合\n高内聚低耦合是软件工程中常用的一个原则，它是指在系统设计和开发中，模块内部的各个元素之间应该紧密地联系在一起（高内聚），而模块之间的联系应该尽可能地减少（低耦合）。下面分别介绍一下这两个概念：\n\n1. 高内聚：高内聚是指一个模块内部的各个元素之间联系紧密，它们共同实现一个单一的目标或者职责。高内聚的模块通常具有以下特点：\n   1. 模块内部的元素彼此之间互相依赖，共同完成某个单一的任务；\n   2. 模块内部的元素之间的耦合度较高，因此模块内的变化对其他模块的影响比较小；\n   3. 模块的功能和职责比较单一，易于测试、维护和扩展。\n\n2. 低耦合：低耦合是指一个模块与其他模块之间的联系尽可能地少，模块之间的依赖性较小。低耦合的模块通常具有以下特点：\n   1. 模块之间的依赖关系比较少，模块之间的联系比较松散；\n   2. 模块之间的通信采用松散耦合的方式，例如通过接口、消息队列、事件等方式；\n   3. 模块之间的变化对其他模块的影响较小，系统更容易维护和扩展。\n\n通过高内聚低耦合的原则，可以将系统划分为相对独立的模块，每个模块负责完成一个单一的任务或者职责。这样可以使得系统的各个模块更加灵活、可重用、易于维护和扩展。另外，高内聚低耦合的原则还可以提高系统的可靠性、安全性和性能。\n\n# 参考文章\n- http://www.uml.org.cn/qiyezjjs/202104302.asp\n- https://tech.meituan.com/2017/12/22/ddd-in-practice.html"
  },
  {
    "path": "架构/计算和储存分离.md",
    "content": "# 计算和储存分离架构\n\n## 为什么说存储和计算分离的架构才是未来\nhttps://juicefs.com/blog/cn/posts/why-disaggregated-compute-and-storage-is-future/\n\n这篇文章的标题是我们过去几个月经常和客户探讨的一个问题，也是很多大公司正在思考的问题，在这里分享一下我们的观点和经验。\n\n二十年前，大规模存储一般使用的是专有硬件设备方案（NAS），通过特殊的高性能通讯硬件给其他应用提供访问接入。这种方案不太容易扩展，而且价格昂贵，无法满足互联网的高速下的超大规模数据存储需求。\n\n让我们回到 2001 年，Google 的 GFS 开创了先河，第一次用普通的 x86 机器和普通硬盘搭建了大规模存储。当时的 HDD 的吞吐量大概在 50MB/s，通过接入多个硬盘的方式可以提高单机吞吐量到 1GB/s 。但当时的主流网络只有 100Mb，通过网络远程访问数据实在是太慢了。为了解决数据的快速访问，Google 创造性地提出来了计算和存储耦合的架构，在同一个集群中实现计算和存储功能，并将计算的代码移动到数据所在的地方，而不是将数据传输到计算节点，有效解决了分散在各个弱连接的存储节点间的海量数据访问的困难。后来者 Hadoop 等也是完全照搬了这个架构，数据本地化是其中一个非常重要特性来保证整体的性能。还做了很多优化来进一步降低机器间、机柜间的网络带宽消耗。\n\n经过 10 年的发展，网络的性能发生了巨大的变化，从之前主流 100Mb 到 10Gb，增长了100倍，而同时期的 HDD 硬盘的性能基本没有太大变化，倒是单盘的容量增大了很多。由于各种社交网络应用对网络的带宽要求很高，加上核心交换机和 SDN 的强力支撑，不少公司实施了点对点的 10Gb 网络架构（任意两个机器之间都有 10Gb 带宽保障）。另外，各种高效的压缩算法和列存储格式也进一步减少了 IO 数据量，将大数据的瓶颈逐渐由 IO 变成了 CPU。在数据本地化优化得很好的大数据计算集群中，大量网络带宽是闲置的，而因为存储和计算耦合在一个集群中，带来了一些其它问题：\n\n在不同的应用或者发展时期，需要不同的存储空间和计算能力配比，使得机器的选型会比较复杂和纠结；\n当存储空间或计算资源不足时，只能同时对两者进行扩容，导致扩容的经济效率比较低（另一种扩容的资源被浪费了）；\n在云计算场景下，不能实现真正的弹性计算，因为计算集群中也有数据，关闭闲置的计算集群会丢失数据。\n因为以上这些存储和计算耦合导致的问题，不少公司开始思考这种耦合以及数据本地化的必要性。2013 年我初到 Facebook 时，隔壁组的同事就做了一个这方面的研究，看在关闭 Hadoop 的数据本地化优化的情况下，对性能究竟有多少影响。实测表明，对计算任务的整体影响在 2%以内，对数据的本地读优化已经不那么重要了。后来 Facebook 就逐渐往计算和存储分离的架构迁移，也对所用的大数据软件做了些调整以适应这种新的架构，他们在今年的 Apache Spark & AI Summit 上做了主题为 Taking Advantage of a Disaggregated Storage and Compute Architecture 详细的分享，他们把这种架构称为 DisAgg。Google 在这方面应该是做得更早，只是没有太多公开信息可供参考。\n\n在 AWS 等公有云上，基于网络的块存储逐步取代了单机的本地存储，使得公有云上的计算和存储耦合架构更加不合理（数据本地化并不是真实的，DataNode 内的本地读其实在物理层也是远程读）。针对公有云设计的大数据分析服务 Databricks 一开始就是采用了计算和存储分离的架构（直接使用 S3 作为存储），给产品带来了非常大的灵活性，按需创建和自动弹性伸缩的 Spark 集群是一大卖点（还能最大限度使用 Spot 节点来大大降低计算成本），非常受客户欢迎。因为 S3 只是对象存储，用于大数据计算时会有很多问题，Databricks 以及它的客户也被坑过很多次。Databricks 花了不少精力去改进和适配，使得 Databricks 上的 Spark 任务可以更快更稳定。AWS 上的先驱 Netflix 也是使用 S3 作为大数据的存储，他们针对 Hive 等做了很多改造才能稳定使用 （不是开源的S3mper）。JuiceFS 则是把这些改进进一步抽象和产品化，让它们能够更好地服务于包括大数据在内的更多场景，帮助更多的公司改善云上大数据体验，而不用重复地去解决 S3 等对象存储带来的问题。\n\n因为网络的高速发展，以及大数据计算框架对 IO 的优化，使得数据本地化已经不再重要，存储和计算分离的架构才是未来。JuiceFS 正是顺应了这种发展趋势，是架构落后的 HDFS 的更好替代，为云上的大数据提供完全弹性的存储解决方案，让云上的大数据获得真正的弹性（完全按需使用）。"
  },
  {
    "path": "消息队列/Kafka.md",
    "content": "# Kafka\n## 架构图\n![](../img/消息队列/kafka/架构图.png)\n- Producer : 发布消息的客户端\n- Broker：一个从生产者接受并存储消息的客户端\n- Consumer : 消费者从 Broker 中读取消息\n- ZooKeeper：Kafka 通过 ZooKeeper 来存储集群的 meta 信息等\n## 概念\n### topic \nTopic 被称为主题，在 kafka 中，使用一个类别属性来划分消息的所属类，划分消息的这个类称为 topic。topic 相当于消息的分配标签，是一个逻辑概念。主题好比是数据库的表，或者文件系统中的文件夹。 \n### partition\n![](../img/消息队列/kafka/partition.png)\npartition 译为分区，topic 中的消息被分割为一个或多个的 partition，它是一个物理概念，对应到系统上的就是一个或若干个目录，一个分区就是一个 提交日志。消息以追加的形式写入分区，先后以顺序的方式读取。\n#### partiton命名规则\n- 为topic名称+有序序号，第一个partiton序号从0开始，序号最大值为partitions数量减1\n- 例如\n    - topic：report_push\n        - partitions数量都为partitions=4\n          ```shell\n          |--report_push-0\n          |--report_push-1\n          |--report_push-2\n          |--report_push-3\n          ```\n#### kafka 为什么要将 Topic 进行分区？\n如果 Topic 不进行分区，而将 Topic 内的消息存储于一个 broker，那么关于该 Topic 的所有读写请求都将由这一个 broker 处理，吞吐量很容易陷入瓶颈，这显然是不符合高吞吐量应用场景的。有了 Partition 概念以后，假设一个 Topic 被分为 10 个 Partitions，Kafka 会根据一定的算法将 10 个 Partition 尽可能均匀的分布到不同的 broker（服务器）上，当 producer 发布消息时，producer 客户端可以采用 random、key-hash 及 轮询 等算法选定目标 partition，若不指定，Kafka 也将根据一定算法将其置于某一分区上。Partiton 机制可以极大的提高吞吐量，并且使得系统具备良好的水平扩展能力\n- 提高吞吐量\n- 水平扩展\n- 便于old segment快速删除，有效提高磁盘利用率\n### segment \nSegment 被译为段，将 Partition 进一步细分为若干个 segment，每个 segment 文件的大小相等。\n\nsegment file组成：由2大部分组成，分别为index file和data file，此2个文件一一对应，成对出现，后缀”.index”和“.log”分别表示为segment索引文件、数据文件.\n\n图2\n\n![](../img/消息队列/kafka/segmentfile组成.png)\n \n- 索引文件存储大量元数据\n- 数据文件存储大量消息\n- 索引文件中元数据指向对应数据文件中message的物理偏移地址\n#### index对应log关系\n![](../img/消息队列/kafka/index对应log关系.png)\n- 其中以索引文件中元数据3,497为例，依次在数据文件中表示第3个message(在全局partiton表示第368772个message)、以及该消息的物理偏移地址为497。\n- 稀疏索引 segment index file采取稀疏索引存储方式，它减少索引文件大小，通过mmap可以直接内存操作，稀疏索引为数据文件的每个对应message设置一个元数据指针,它比稠密索引节省了更多的存储空间，但查找起来需要消耗更多的时间\n\n.log文件\n- 由很多的message组成\n![](../img/消息队列/kafka/message物理结构.png)\n![](../img/消息队列/kafka/message物理结构2.png)\n\n如何才能判断读取的这条消息读完了\n  - 由上图message的物理结构定义，大致为message的size，定义了消息的长度\n\nsegment文件命名规则：partion全局的第一个segment从0开始，后续每个segment文件名为上一个segment文件最后一条消息的offset值。数值最大为64位long大小，19位数字字符长度，没有数字用0填充。\n\n在partition中如何通过offset查找message\n- 例如读取offset=368776的message\n- 第一步查找segment file 上述图2为例，其中00000000000000000000.index表示最开始的文件，起始偏移量(offset)为0.第二个文件00000000000000368769.index的消息量起始偏移量为368770 = 368769 + 1.同样，第三个文件00000000000000737337.index的起始偏移量为737338=737337 + 1，其他后续文件依次类推，以起始偏移量命名并排序这些文件，只要根据offset **二分查找**文件列表，就可以快速定位到具体文件。 当offset=368776时定位到00000000000000368769.index|log\n- 第二步通过segment file查找message 通过第一步定位到segment file，当offset=368776时，依次定位到00000000000000368769.index的元数据物理位置和00000000000000368769.log的物理偏移地址，然后再通过00000000000000368769.log顺序查找直到offset=368776为止。\n- 分段索引、稀疏存储\n### offset\n每个partition都由一系列有序的、不可变的消息组成，这些消息被连续的追加到partition中。partition中的每个消息都有一个连续的序列号叫做offset,用于partition唯一标识一条消息.\n### broker\n每个 Kafka 中服务器被称为 broker\n\nbroker 接收来自生产者的消息，为消息设置偏移量，并提交消息到磁盘保存。broker 为消费者提供服务，对读取分区的请求作出响应，返回已经提交到磁盘上的消息。\n![](../img/消息队列/kafka/broker.png)\n### producer \n生产者，即消息的发布者，其会将某 topic 的消息发布到相应的 partition 中。生产者在默认情况下把消息均衡地分布到主题的所有分区上，而并不关心特定消息会被写到哪个分区。不过，在某些情况下，生产者会把消息直接写到指定的分区\n### consumer\n消费者，即消息的使用者，一个消费者可以消费多个 topic 的消息，对于某一个 topic 的消息，其只会消费同一个 partition 中的消息\n## Kafka零拷贝\n![](../img/消息队列/kafka/kafka零拷贝1.png)\n第一次：将磁盘文件，读取到操作系统内核缓冲区；\n\n第二次：将内核缓冲区的数据，copy到application应用程序的buffer；\n\n第三步：将application应用程序buffer中的数据，copy到socket网络发送缓冲区(属于操作系统内核的缓冲区)；\n\n第四次：将socket buffer的数据，copy到网卡，由网卡进行网络传输。\n\n传统方式，读取磁盘文件并进行网络发送，经过的四次数据copy是非常繁琐的。实际IO读写，需要进行IO中断，需要CPU响应中断(带来上下文切换)，尽管后来引入DMA来接管CPU的中断请求，但四次copy是存在“不必要的拷贝”的。\n\n![](../img/消息队列/kafka/kafka零拷贝2.png)\n\nKafka使用的zero-copy的应用程序要求内核直接将数据从磁盘文件拷贝到套接字，而无需通过应用程序。零拷贝不仅大大地提高了应用程序的性能，而且还减少了内核与用户模式间的上下文切换。\n\n# 常见问题\n多个partition对应一个消费者组，消费者的数量应小于等于partition的数量\n## kafka中zookeeper的作用\n首先最新的提议表示将在未来取消依赖zookeeper，在2.8版本将使用self-managed quorum来取代\n\n作用\n- broker 注册\n- topic 注册\n- producer 和 consumer 负载均衡\n- 维护 partition 与 consumer 的关系\n- 记录消息消费的进度以及 consumer 注册\n\n## kafka的consumer是拉模式还是推模式\nproducer将消息推送到broker，consumer从broker拉取消息。\n\npush模式的缺点：\n\n由broker决定消息推送的速率，对于不同消费速率的consumer就不太好处理了。消息系统都致力于让consumer以最大的速率最快速的消费消息，但不幸的是，push模式下，当broker推送的速率远大于consumer消费的速率时，consumer恐怕就要崩溃了\n\npull模式的缺点：\n\n- broker需要在数据为空时阻塞\n- broker需要储存数据\n## kafka生产者丢消息情况\n当producer向leader发送数据时，可以通过request.required.acks参数来设置数据可靠性的级别：\n\n- acks=0： 表示producer不需要等待任何broker确认收到消息的回复，就可以继续发送下一条消息。性能最高，但是最容易丢消息。大数据统计报表场景，对性能要求很高，对数据丢失不敏感的情况可以用这种。\n- acks=1： 至少要等待leader已经成功将数据写入本地log，但是不需要等待所有follower是否成功写入。就可以继续发送下一条消息。这种情况下，如果follower没有成功备份数据，而此时leader又挂掉，则消息会丢失。\n- acks=-1或all： 这意味着leader需要等待所有备份(min.insync.replicas配置的备份个数)都成功写入日志，这种策略会保证只要有一个备份存活就不会丢失数据。这是最强的数据保证。一般除非是金融级别，或跟钱打交道的场景才会使用这种配置。当然了如果min.insync.replicas配置的是1则也可能丢消息，跟acks=1情况类似。\n\n## kafka消费者丢消息情况\n如果消费这边配置的是自动提交，万一消费到数据还没处理完，就自动提交offset了，但是此时你consumer直接宕机了，未处理完的数据丢失了，下次也消费不到了。\n\n## Kafka如何保证高可用性\n- `数据的冗余备份`：Kafka 采用分布式的方式存储数据，并将数据分布在多个 Broker 节点上，因此在某个节点出现故障时，其他节点上的数据仍然可用。Kafka 还支持数据副本机制，即每个 Partition 中的数据都会有多个备份，保证了数据的冗余备份。\n- `ZooKeeper 协调机制`：Kafka 使用 ZooKeeper 来实现分布式协调，包括集群中的 Broker 选举、Topic 的元数据管理、Consumer Group 的协调等。ZooKeeper 自身就是一个高可用的分布式协调服务，保证了 Kafka 在运行过程中的高可用性。\n- `消费者的偏移量管理`：Kafka 的 Consumer Group 采用了偏移量（offset）机制来管理消息的消费进度。消费者组中的每个 Consumer 都会记录自己已经消费的消息的偏移量，这个偏移量被保存在 Kafka 的 Topic 中。当某个 Consumer 挂掉后，其他 Consumer 可以接替它继续消费，并从上一个 Consumer 留下的偏移量处继续消费，从而保证了消息的高可靠性。\n- `Controller 选举机制`：Kafka 集群中有一个 Controller，负责管理 Broker 的生命周期和 Topic 的元数据信息，以及一些集群级别的操作。当 Controller 出现故障时，Kafka 会自动进行选举，选出新的 Controller，从而保证了 Kafka 集群的高可用性。\n\n## Kafka的消息保存在哪里？Kafka的消息是如何分区的？\n\nKafka的消息保存在Kafka的broker节点上，以topic为单位进行分区存储。每个topic可以分成多个partition，每个partition是一个有序的、不可变的消息序列，每条消息在被写入之后就不可修改，也不会被删除，只有在过期或者被清理的情况下才会被删除。\n\nKafka使用分区来实现消息的并行处理和负载均衡。一个topic的消息可以分散存储在多个broker节点的多个partition中，每个partition只由一个broker节点负责读写，保证了消息的有序性。在同一个partition中，消息的顺序是有序的，不同partition之间的消息并不保证有序。\n\nKafka使用了一种基于一致性hash算法的分区器来决定将消息发送到哪个partition中，它根据消息的key值计算hash值，然后根据hash值对partition数取模，从而确定该消息属于哪个partition。如果消息没有key值，则使用轮询的方式将消息平均分配到各个partition中。为了避免消息不均衡的情况，可以通过设置partition数目和指定key值来控制消息的分布。\n\n## Kafka是如何处理流量峰值的？\nKafka是一个高吞吐量、低延迟的分布式消息系统，可以处理海量数据的传输。Kafka可以通过多个方式处理流量峰值，以下是一些常见的方法：\n\n- `增加分区数量`：Kafka中，每个主题都可以分为多个分区，增加分区数量可以增加Kafka集群的并行处理能力，从而提高吞吐量。\n- `增加副本数量`：Kafka中，每个分区都有多个副本，增加副本数量可以提高数据的可靠性，并且在发生故障时可以实现快速的恢复。另外，副本也可以在多个Broker之间进行复制，从而增加整个集群的吞吐量。\n- `增加Broker数量`：增加Kafka集群的Broker数量可以提高集群的并行处理能力，从而更好地处理流量峰值。\n- `调整Kafka参数`：Kafka提供了一系列参数，可以通过调整这些参数来优化Kafka的性能，例如调整发送和接收的缓冲区大小、批量处理的数量、消息压缩方式等。\n\n## Kafka消息压缩方式\nKafka支持多种消息压缩方式，可以在生产者和消费者端进行配置。以下是Kafka支持的压缩方式：\n\n- `Gzip压缩`：这种压缩方式可以获得较好的压缩比，但会对CPU有一定的消耗。\n- `Snappy压缩`：这种压缩方式速度较快，但压缩比相对较低，对CPU的消耗也比较小。\n- `LZ4压缩`：这种压缩方式压缩和解压缩的速度都很快，而且不会对CPU造成过大的负担。\n- `Zstandard压缩`：这种压缩方式是在LZ4的基础上进行改进，压缩比和解压速度都比LZ4更好。\n\n## Kafka的重平衡机制是什么？\nKafka的重平衡机制是指在Kafka集群中，当有新的Consumer加入或者已有Consumer从Consumer Group中退出时，Kafka会自动对Consumer Group中的Consumer进行重新分配分区（Partition），以保证每个Consumer所处理的分区数尽可能平均。\n\n具体地，当有新的Consumer加入或者已有Consumer从Consumer Group中退出时，Kafka Controller会触发重平衡操作。重平衡的过程包括以下步骤：\n\n1. 暂停所有的消费者（consumer）并且计算出所有的消费者需要重新分配的 partition\n2. 如果有新的 consumer 加入，Controller 会将该 consumer 加入到 Consumer Group 中，并分配该 consumer 需要消费的 partition\n3. 如果有已经存在的 consumer 退出，Controller 会将该 consumer 对应的 partition 从该 consumer 分配的 partition 中移除，并将该 partition 分配给其他的 consumer\n4. Controller 将新的分配方案发送给每个消费者，并恢复消费者的消费。\n\n需要注意的是，重平衡机制会在消费者感知到消费分区变化之前先暂停所有消费者。因此，如果在 Consumer Group 中的消费者数量比较多，或者 Consumer Group 中的消费者数量变化频繁，重平衡的过程可能会影响消息的消费。因此，在实际应用中，需要合理配置 Consumer Group 的数量和每个 Consumer 所消费的 Partition 数量，以减少重平衡的频率和影响。\n\n## Kafka中的生产者和消费者是什么？Kafka是如何确保数据的顺序性和一致性的？\n在 Kafka 中，生产者（Producer）是指将数据发布到 Kafka 主题的应用程序，而消费者（Consumer）则是从 Kafka 主题中读取数据的应用程序。\n\nKafka 通过分区（Partition）机制来保证数据的顺序性和一致性。每个主题（Topic）可以划分为多个分区，每个分区只由一个消费者组（Consumer Group）中的一个消费者进行消费。在一个分区中，Kafka 保证消息的顺序是有序的，即使有多个生产者向同一个分区发送消息也是如此。当一个消费者从一个分区中读取消息时，Kafka 会返回已经提交（Committed）的最大偏移量（Offset），确保该消费者能够接收到正确的消息。\n\n在 Kafka 中，多个消费者可以组成一个消费者组，消费者组可以协同消费主题中的所有分区，从而实现更高的消费吞吐量。当一个消费者组中的消费者数量发生变化（如新增消费者或消费者退出），Kafka 会触发一次重新平衡（Rebalance）操作，重新分配分区给各个消费者。在重新平衡的过程中，Kafka 会确保每个分区只由一个消费者进行消费，从而保证消费的一致性和顺序性。\n\n## kafka消费组怎么消费一个topic的数据\n消费组可以通过订阅（subscribe）或者手动分配（assign）的方式消费一个topic的数据。\n\n当消费组订阅一个topic时，Kafka会自动为消费者组中的每个消费者分配一个或多个分区，然后每个消费者就可以独立地消费自己被分配的分区中的消息。消费者组中的消费者可以动态地加入或者退出，Kafka会自动重新分配分区。\n\n当消费者组手动分配分区时，消费者可以通过assign()方法指定要消费的分区。在这种情况下，消费者组无法自动地进行分区重新分配，也就是说，消费者加入或者退出时，需要手动重新分配分区。\n\n无论是订阅还是手动分配，消费组可以通过poll()方法从分配给它的分区中拉取数据。Kafka保证在同一个分区内消息的顺序和一致性，但不保证不同分区之间的顺序和一致性。\n\n## Kafka有哪些优缺点？\nKafka是一种分布式的流处理平台，具有以下优点：\n\n优点：\n\n- 高吞吐量和低延迟：Kafka通过分布式存储和基于磁盘的批量操作实现了高吞吐量和低延迟的数据传输。\n- 可伸缩性：Kafka支持横向扩展，可以通过增加Broker节点来扩展集群的吞吐量和存储容量。\n- 可靠性：Kafka采用副本机制实现数据的备份和恢复，确保了数据的可靠性和高可用性。\n- 灵活性：Kafka可以与各种数据处理框架（如Hadoop、Spark等）无缝集成，适用于多种数据处理场景。\n- 消息保留机制：Kafka支持基于时间和大小的消息保留机制，可以设置消息的保存时间和存储大小，避免了消息堆积和数据浪费。\n\n缺点：\n\n- 相对复杂：相比于其他消息队列，Kafka的配置和部署相对复杂。\n- 学习成本高：Kafka的设计思想和内部机制相对复杂，需要较长的学习周期。\n- 硬盘依赖：Kafka将消息持久化到硬盘上，因此对硬件的要求较高。\n- 消息过期：Kafka采用基于时间的消息过期机制，一些过期的消息可能会被误删。\n\n## Kafka的API是什么？如何使用Kafka API实现生产和消费？\nKafka的API主要是基于Java语言的，包括生产者API、消费者API和管理API。\n\n下面简单介绍一下使用Kafka API实现生产和消费的步骤：\n\n引入Kafka客户端依赖\n```xml\n<dependency>\n    <groupId>org.apache.kafka</groupId>\n    <artifactId>kafka-clients</artifactId>\n    <version>{kafka-version}</version>\n</dependency>\n```\n创建生产者实例\n```java\nProperties props = new Properties();\nprops.put(\"bootstrap.servers\", \"localhost:9092\");\nprops.put(\"key.serializer\", \"org.apache.kafka.common.serialization.StringSerializer\");\nprops.put(\"value.serializer\", \"org.apache.kafka.common.serialization.StringSerializer\");\n\nProducer<String, String> producer = new KafkaProducer<>(props);\n```\n在创建生产者实例时，需要指定Kafka集群的地址、键和值的序列化器。\n\n发送消息\n```java\nProducerRecord<String, String> record = new ProducerRecord<>(\"test_topic\", \"key\", \"value\");\n\nproducer.send(record);\n\n```\n创建一个ProducerRecord对象，包括要发送的主题、键和值，然后使用生产者实例的send()方法将其发送到Kafka集群中。\n\n创建消费者实例\n```java\nProperties props = new Properties();\nprops.put(\"bootstrap.servers\", \"localhost:9092\");\nprops.put(\"group.id\", \"test_group\");\nprops.put(\"key.deserializer\", \"org.apache.kafka.common.serialization.StringDeserializer\");\nprops.put(\"value.deserializer\", \"org.apache.kafka.common.serialization.StringDeserializer\");\n\nConsumer<String, String> consumer = new KafkaConsumer<>(props);\n\n```\n在创建消费者实例时，需要指定Kafka集群的地址、消费者组ID以及键和值的反序列化器。\n\n订阅主题并消费消息\n```java\nconsumer.subscribe(Collections.singletonList(\"test_topic\"));\n\nwhile (true) {\n    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));\n\n    for (ConsumerRecord<String, String> record : records) {\n        System.out.println(record.value());\n    }\n}\n\n```\n使用消费者实例的subscribe()方法订阅要消费的主题，然后在一个循环中使用poll()方法从Kafka集群中获取消息，并遍历处理每条消息。\n\n## 如何部署和扩展Kafka集群？\nKafka集群的部署和扩展可以参考以下步骤：\n\n1. 启动多个Kafka Broker：在每个节点上都要启动一个Kafka Broker，每个Broker都要配置成相同的Cluster Name。\n2. 配置Kafka集群：在每个节点上修改Kafka的server.properties配置文件，将以下参数设置为相同的值：\n```bash\nbroker.id=1 # 每个节点的id不能重复\nlisteners=PLAINTEXT://hostname:9092 # 监听地址和端口\nlog.dirs=/tmp/kafka-logs # 消息数据存储目录\nzookeeper.connect=zk1:2181,zk2:2181,zk3:2181 # Zookeeper集群地址\n```\n3. 创建Topic：创建Topic时，需要指定多个Partition，让消息在多个节点上进行分布式存储。\n4. 生产和消费消息：使用相同的方式生产和消费消息，Kafka会根据Partition的配置，将消息在多个节点上进行分布式存储和处理。\n5. 扩展Kafka集群：如果需要扩展Kafka集群，可以在新节点上启动一个Kafka Broker，并修改server.properties配置文件中的broker.id和listeners参数。然后在Topic中增加Partition，Kafka会自动将消息在新节点上进行分布式存储和处理。\n\n在集群部署过程中，需要注意以下几点：\n\n- 每个节点的配置文件需要保持一致。\n- 集群中的每个节点都需要设置唯一的broker.id。\n- Zookeeper集合中的每个节点都需要知道集群中的所有节点信息。\n- 新节点加入后，需要等待一段时间进行重平衡，期间可能会影响到消费者的消费速度。\n- 当节点数量增加时，需要注意调整Kafka的配置，以保证高吞吐量和高可用性。\n\n## Kafka 的分区策略有哪些？\n所谓分区策略就是决定生产者将消息发送到哪个分区的算法。\n\n- RoundRobinPartitioner：轮询分区策略，按照分区编号依次将消息分配到不同的分区。\n- HashedPartitioner：哈希分区策略，将消息的key进行哈希计算，然后将哈希结果对分区数取余，得到消息所在的分区。\n- RangePartitioner：范围分区策略，根据消息key的范围将消息分配到不同的分区。需要在创建主题时指定分区边界。\n- StickyPartitioner：粘性分区策略，将消息发送到同一个分区，直到该分区的消息数量超过阈值，才会将消息发送到下一个分区。这个策略可以用来保证消息的顺序性。\n- CustomPartitioner：自定义分区策略，用户可以根据自己的业务逻辑自定义分区策略。\n\n## kafka怎么顺序消费\nKafka 的顺序消费保证基于 Partition。如果你希望严格保证消息的顺序，应该：\n\n- 将相关消息发送到同一个 Partition（通过设置相同的 key）。\n- 确保消费者线程对单个 Partition 进行消费。\n- 合理设置生产者和消费者的配置。\n\n# 参考文章\n- https://blog.51cto.com/u_15239532/2858247\n- https://www.daimajiaoliu.com/series/kafka/479991a51900405\n- https://chat.openai.com/\n"
  },
  {
    "path": "消息队列/Pulsar.md",
    "content": "\n\n* [Pulsar](#pulsar)\n    * [pulsar的优势](#pulsar的优势)\n* [Apache Pulsar 架构](#apache-pulsar-架构)\n    * [Topic 与分区](#topic-与分区)\n    * [物理分区与逻辑分区](#物理分区与逻辑分区)\n* [消息存储原理与 ID 规则](#消息存储原理与-id-规则)\n    * [消息 ID 生成规则](#消息-id-生成规则)\n    * [分片机制详解：Legder 和 Entry](#分片机制详解legder-和-entry)\n        * [Journals](#journals)\n        * [EntryLogFile](#entrylogfile)\n        * [Index 文件](#index-文件)\n        * [Entry 数据写入](#entry-数据写入)\n        * [Entry 数据读取](#entry-数据读取)\n        * [数据一致性保证：LastLogMark](#数据一致性保证lastlogmark)\n* [消息副本与存储机制](#消息副本与存储机制)\n    * [消息元数据组成](#消息元数据组成)\n    * [消息副本机制](#消息副本机制)\n        * [消息副本分布](#消息副本分布)\n* [消息恢复机制](#消息恢复机制)\n* [pulsar的消息模式](#pulsar的消息模式)\n    * [独占模式（Exclusive）](#独占模式exclusive)\n    * [灾备模式（Failover）](#灾备模式failover)\n    * [共享模式（Shared）](#共享模式shared)\n* [定时和延时消息](#定时和延时消息)\n    * [相关概念](#相关概念)\n    * [适用场景](#适用场景)\n    * [使用方式](#使用方式)\n    * [定时消息](#定时消息)\n    * [延时消息](#延时消息)\n    * [使用说明和限制](#使用说明和限制)\n* [消息重试与死信机制](#消息重试与死信机制)\n    * [自动重试](#自动重试)\n    * [自定义参数设置](#自定义参数设置)\n    * [重试规则](#重试规则)\n    * [重试消息的消息属性](#重试消息的消息属性)\n    * [重试消息的消息 ID 流转](#重试消息的消息-id-流转)\n        * [完整代码示例](#完整代码示例)\n    * [主动重试](#主动重试)\n* [参考文档](#参考文档)\n\n# Pulsar\n## pulsar的优势\n数据强一致\n\n Pulsar 版采用 BookKeeper 一致性协议 实现数据强一致性（类似 RAFT 算法），将消息数据备份写到不同物理机上，并且要求是同步刷盘。当某台物理机出故障时，后台数据复制机制能够对数据快速迁移，保证用户数据备份可用。\n\n高性能低延迟\n\n Pulsar 版能够高效支持百万级消息生产和消费，海量消息堆积且消息堆积容量不设上限，支撑了腾讯计费所有场景；性能方面，单集群 QPS 超过10万，同时在时耗方面有保护机制来保证低延迟，帮助您轻松满足业务性能需求。\n\n百万级 Topic\n\n Pulsar 版计算与存储架构的分离设计，使得  Pulsar 版可以轻松支持百万级消息主题。相比于市场上其他 MQ 产品，整个集群不会因为 Topic 数量增加而导致性能急剧下降。\n\n丰富的消息类型\n\n Pulsar 版提供丰富的消息类型，涵盖普通消息、顺序消息（全局顺序 / 分区顺序）、分布式事务消息、定时消息，满足各种严苛场景下的高级特性需求。\n\n消费者数量无限制\n\n不同于 Kafka 的消息消费模式， Pulsar 版的消费者数量不受限于 Topic 的分区个数，并且会按照一定的算法均衡每个消费者的消息量，业务可按需启动对应的消费者数量。\n\n多协议接入\n\n Pulsar 版的 API 支持 Java、C++、Go 等多语言，并且支持 HTTP 协议，可扩展更多语言的接入，另外还支持开源RocketMQ、RabbitMQ客户端的接入。如果用户只是利用消息队列的基础功能进行消息的生产和消费，可以不用修改代码就完成到  Pulsar 版的迁移。\n\n隔离控制\n\n提供按租户对 Topic 进行隔离的机制，同时可精确管控各个租户的生产和消费速率，保证租户之间互不影响，消息的处理不会出现资源竞争的现象。\n\n# Apache Pulsar 架构\nApache Pulsar 是一个发布-订阅模型的消息系统，由 Broker、Apache BookKeeper、Producer、Consumer 等组件组成。\n\n![](../img/消息队列/pulsar/puslar架构.png)\n\n- Producer ： 消息的生产者，负责发布消息到 Topic。\n- Consumer：消息的消费者，负责从 Topic 订阅消息。\n- Broker：无状态服务层，负责接收和传递消息，集群负载均衡等工作，Broker 不会持久化保存元数据，因此可以快速的上、下线。\n- Apache BookKeeper：有状态持久层，由一组 Bookie 存储节点组成，可以持久化地存储消息。\n\nApache Pulsar 在架构设计上采用了计算与存储分离的模式，消息发布和订阅相关的计算逻辑在 Broker 中完成，数据存储在 Apache BookKeeper 集群的 Bookie 节点上。\n\n## Topic 与分区\nTopic（主题）是某一种分类的名字，消息在 Topic 中可以被存储和发布。生产者往 Topic 中写消息，消费者从 Topic 中读消息。\n\nPulsar 的 Topic 分为 Partitioned Topic 和 Non-Partitioned Topic 两类，Non-Partitioned Topic 可以理解为一个分区数为1的 Topic。实际上在 Pulsar 中，Topic 是一个虚拟的概念，创建一个3分区的 Topic，实际上是创建了3个“分区Topic”，发给这个 Topic 的消息会被发往这个 Topic 对应的多个 “分区Topic”。\n例如：生产者发送消息给一个分区数为3，名为my-topic的 Topic，在数据流向上是均匀或者按一定规则（如果指定了key）发送给了 my-topic-partition-0、my-topic-partition-1 和 my-topic-partition-2 三个“分区 Topic”。\n\n分区 Topic 做数据持久化时，分区是逻辑上的概念，实际存储的单位是分片（Segment）的。\n\n如下图所示，分区 Topic1-Part2 的数据由N个 Segment 组成， 每个 Segment 均匀分布并存储在 Apache BookKeeper 群集中的多个 Bookie 节点中， 每个 Segment 具有3个副本。\n\n![](../img/消息队列/pulsar/topic与分区.png)\n\n## 物理分区与逻辑分区\n逻辑分区和物理分区对比如下：\n\n![](../img/消息队列/pulsar/物理分区和逻辑分区.png)\n\n物理分区：计算与存储耦合，容错需要拷贝物理分区，扩容需要迁移物理分区来达到负载均衡。\n\n逻辑分区：物理“分片”，计算层与存储层隔离，这种结构使得 Apache Pulsar 具备以下优点。\n\nBroker 和 Bookie 相互独立，方便实现独立的扩展以及独立的容错。\nBroker 无状态，便于快速上、下线，更加适合于云原生场景。\n分区存储不受限于单个节点存储容量。\n分区数据分布均匀，单个分区数据量突出不会使整个集群出现木桶效应。\n存储不足扩容时，能迅速利用新增节点平摊存储负载。\n\n# 消息存储原理与 ID 规则\n\n## 消息 ID 生成规则\n在 Pulsar 中，每条消息都有自己的 ID（即 MessageID），MessageID 由四部分组成：ledgerId:entryID:partition-index:batch-index。其中：\n\npartition-index：指分区的编号，在非分区 topic 的时候为 -1。\nbatch-index：在非批量消息的时候为 -1。\n消息 ID 的生成规则由 Pulsar 的消息存储机制决定，Pulsar 中消息存储原理图如下：\n\n![](../img/消息队列/pulsar/消息储存原理.png)\n\n如上图所示，在 Pulsar中，一个 Topic 的每一个分区会对应一系列的 ledger，其中只有一个 ledger 处于 open 状态即可写状态，而每个 ledger 只会存储与之对应的分区下的消息。\n\nPulsar 在存储消息时，会先找到当前分区使用的 ledger ，然后生成当前消息对应的 entry ID，entry ID 在同一个 ledger 内是递增的。每个 ledger 存在的时长或保存的 entry 个数超过阈值后会进行切换，新的消息会存储到同一个 partition 中的下一个 ledger 中。\n\n- 批量生产消息情况下，一个 entry 中可能包含多条消息。\n- 非批量生产的情况下，一个 entry 中包含一条消息（producer 端可以配置这个参数，默认是批量的）。\n\nLedger 只是一个逻辑概念，是数据的一种逻辑组装维度，并没有对应的实体。而 bookie 只会按照 entry 维度进行写入、查找、获取。\n\n## 分片机制详解：Legder 和 Entry\n\nPulsar 中的消息数据以 ledger 的形式存储在 BookKeeper 集群的 bookie 存储节点上。Ledger 是一个只追加的数据结构，并且只有一个写入器，这个写入器负责多个 bookie 的写入。Ledger 的条目会被复制到多个 bookie 中，同时会写入相关的数据来保证数据的一致性。\n\nBookKeeper 需要保存的数据包括：\n\n### Journals\n\njournals 文件里存储了 BookKeeper 的事务日志，在任何针对 ledger 的更新发生前，都会先将这个更新的描述信息持久化到这个 journal 文件中。\nBookKeeper 提供有单独的 sync 线程根据当前 journal 文件的大小来作 journal 文件的 rolling。\n### EntryLogFile\n\n存储真正数据的文件，来自不同 ledger 的 entry 数据先缓存在内存buffer中，然后批量flush到EntryLogFile中。\n默认情况下，所有ledger的数据都是聚合然后顺序写入到同一个EntryLog文件中，避免磁盘随机写。\n### Index 文件\n\n所有 Ledger 的 entry 数据都写入相同的 EntryLog 文件中，为了加速数据读取，会作 ledgerId + entryId 到文件 offset 的映射，这个映射会缓存在内存中，称为 IndexCache。\nIndexCache 容量达到上限时，会被 sync 线程 flush 到磁盘中。\n\n三类数据文件的读写交互如下图：\n\n![](../img/消息队列/pulsar/三类数据文件的读写交互.png)\n\n### Entry 数据写入\n\n1. 数据首先会同时写入 Journal（写入 Journal 的数据会实时落到磁盘）和 Memtable（读写缓存）。\n2. 写入 Memtable 之后，对写入请求进行响应。\n3. Memtable 写满之后，会 flush 到 Entry Logger 和 Index cache，Entry Logger 中保存数据，Index cache 中保存数据的索引信息，\n4. 后台线程将 Entry Logger 和 Index cache 数据落到磁盘。\n\n### Entry 数据读取\n\n- Tailing read 请求：直接从 Memtable 中读取 Entry。\n- Catch-up read（滞后消费）请求：先读取 Index信息，然后索引从 Entry Logger 文件读取 Entry。\n### 数据一致性保证：LastLogMark\n\n- 写入的 EntryLog 和 Index 都是先缓存在内存中，再根据一定的条件周期性的 flush 到磁盘，这就造成了从内存到持久化到磁盘的时间间隔，如果在这间隔内 BookKeeper 进程崩溃，在重启后，我们需要根据 journal 文件内容来恢复，这个 LastLogMark 就记录了从 journal 中什么位置开始恢复。\n- 它其实是存在内存中，当 IndexCache 被 flush 到磁盘后其值会被更新，LastLogMark 也会周期性持久化到磁盘文件，供 Bookkeeper 进程启动时读取来从 journal 中恢复。\n- LastLogMark 一旦被持久化到磁盘，即意味着在其之前的 Index 和 EntryLog 都已经被持久化到了磁盘，那么 journal 在这 LastLogMark 之前的数据都可以被清除了。\n\n# 消息副本与存储机制\n\n## 消息元数据组成\nPulsar 中每个分区 Topic 的消息数据以 ledger 的形式存储在 BookKeeper 集群的 bookie 存储节点上，每个 ledger 包含一组 entry，而 bookie 只会按照 entry 维度进行写入、查找、获取。\n\n> 批量生产消息的情况下，一个 entry 中可能包含多条消息，所以 entry 和消息并不一定是一一对应的。\n\nLedger 和 entry 分别对应不同的元数据。\n\n- ledger 的元数据存储在 zk 上。\n- entry 除了消息数据部分之外，还包含元数据，entry 的数据存储在 bookie 存储节点上。\n\n![](../img/消息队列/pulsar/ledger和entry对应的数据.png)\n\n![](../img/消息队列/pulsar/ledger和entry对比.png)\n\n每个 ledger 在创建的时候，会在现有的 BookKeeper 集群中的可写状态的 bookie 候选节点列表中，选用 ensemble size 对应个数的 bookie 节点，如果没有足够的候选节点则会抛出 BKNotEnoughBookiesExceptio 异常。选出候选节点后，将这些信息组成 <entry id, ensembles> 元组，存储到 ledger 的元数据里的 ensembles 中。\n\n## 消息副本机制\n消息写入流程\n\n![](../img/消息队列/pulsar/消息写入流程.png)\n\n客户端在写入消息时，每个 entry 会向 ledger 当前使用的 ensemble 列表中的 Qw 个 bookie 节点发送写入请求，当收到 Qa 个写确认后，即认为当前消息写入存储成功。同时会通过 LAP（lastAddPushed）和 LAC（LastAddConfirmed）分别标识当前推送的位置和已经收到存储确认的位置。\n\n每个正在推送的 entry 中的 LAC 元数据值，为当前时刻创建发送 entry 请求时，已经收到最新的确认位置值。LAC 所在位置及之前的消息对读客户端是可见的。\n\n同时，pulsar 通过 fencing 机制，来避免同时有多个客户端对同一个 ledger 进行写操作。这里主要适用于一个 topic/partition 的归属关系从一个 broker 变迁到另一个 broker 的场景。\n\n### 消息副本分布\n\n每个 entry 写入时，会根据当前消息的 entry id 和当前使用的 ensembles 列表的开始 entry id（即key值），计算出在当前 entry 需要使用 ensemble 列表中由哪组 Qw 个 bookie 节点进行写入。之后，broker 会向这些 bookie 节点发送写请求，当收到 Qa 个写确认后，即认为当前消息写入存储成功。这时至少能够保证 Qa 个消息的副本个数。\n\n![](../img/消息队列/pulsar/消息副本.png)\n\n如上图所示，ledger 选用了4个 bookie 节点（bookie1-4 这4个节点），每次写入3个节点，当收到2个写入确认即代表消息存储成功。当前 ledger 选中的 ensemble 从 entry 1开始，使用 bookie1、bookie2、bookie3 进行写入，写入 entry 2的时候选用 bookie2、bookie3、bookie4写入，而 entry 3 则会根据计算结果，写入 bookie3、bookie4、bookie1。\n\n# 消息恢复机制\nPulsar 的 BookKeeper 集群中的每个 bookie 在启动的时候，默认自动开启 recovery 的服务，这个服务会进行如下几个事情：\n\n1. auditorElector 审计选举。\n2. replicationWorker 复制任务。\n3. deathWatcher 宕机监控。\n\nBookKeeper 集群中的每个 bookie 节点，会通过 zookeeper 的临时节点机制进行选主，主 bookie 主要处理如下几个事情：\n\n1. 负责监控 bookie 节点的变化。\n2. 到 zk 上面标记出宕机的 bookie 上面的 ledger 为 Underreplicated 状态。\n3. 检查所有的 ledger 的副本数（默认一周一个周期）。\n4. Entry 副本数检查（默认未开启）。\n\n其中 ledger 中的数据是按照 Fragment 维度进行恢复的（每个 Fragment 对应 ledger 下的一组 ensemble 列表，如果一个 ledger 下有多个 ensemble 列表，则需要处理多个 Fragment）。\n\n在进行恢复时，首先要判断出当前的 ledger 中的哪几个 Fragment 中的哪些存储节点需要用新的候选节点进行替换和恢复数据。当 Fragment 中关联的部分 bookie 节点上面没有对应的 entry 数据（默认是按照首、尾 entry 是否存在判断），则这个 bookie 节点需要被替换，当前的这个 Fragment 需要进行数据恢复。\n\nFragment 的数据用新的 bookie 节点进行数据恢复完毕后，更新 ledger 的元数据中当前 Fragment 对应的 ensemble 列表的原数据。\n\n经过此过程，因 bookie 节点宕机引起的数据副本数减少的场景，数据的副本数会逐步的恢复成 Qw 个。\n\n# pulsar的消息模式\n\n为了适用不同场景的需求， Pulsar 版提供多种订阅方式。订阅可以灵活组合出很多可能性：\n\n- 如果您想实现传统的 “发布-订阅消息”形式 ，可以让每个消费者都有一个唯一的订阅名称（独占）。\n- 如果您想实现传统的“消息队列” 形式，可以使多个消费者使用同一个的订阅名称（共享、灾备）。\n- 如果您想同时实现以上两点，可以让一些消费者使用独占方式，剩余消费者使用其他方式。\n\n![](../img/消息队列/pulsar/消息模式.png)\n\n## 独占模式（Exclusive）\n如果两个及以上的消费者尝试以同样方式去订阅主题，消费者将会收到错误，适用于全局有序消费的场景。\n\n```java\nConsumer<byte[]> consumer1 = client.newConsumer()\n                .subscriptionType(SubscriptionType.Exclusive)\n                .topic(topic)\n                .subscriptionName(groupName)\n                .subscribe();\n//consumer1启动成功\n Consumer<byte[]> consumer2 = client.newConsumer()\n                .subscriptionType(SubscriptionType.Exclusive)\n                .topic(topic)\n                .subscriptionName(groupName)\n                .subscribe();\n//consumer2启动失败\n```\n## 灾备模式（Failover）\nconsumer 将会按字典顺序排序，第一个 consumer 被初始化为唯一接受消息的消费者。\n```java\nConsumer<byte[]> consumer1 = client.newConsumer()\n                .subscriptionType(SubscriptionType.Failover)\n                .topic(topic)\n                .subscriptionName(groupName)\n                .subscribe();\n//consumer1启动成功\n Consumer<byte[]> consumer2 = client.newConsumer()\n                .subscriptionType(SubscriptionType.Failover)\n                .topic(topic)\n                .subscriptionName(groupName)\n                .subscribe();\n//consumer2启动成功\n```\n当 master consumer 断开时，所有的消息（未被确认和后续进入的）将会被分发给队列中的下一个 consumer。\n## 共享模式（Shared）\n消息通过 round robin 轮询机制（也可以自定义）分发给不同的消费者，并且每个消息仅会被分发给一个消费者。当消费者断开连接，所有被发送给他，但没有被确认的消息将被重新安排，分发给其它存活的消费者。\n```java\nConsumer<byte[]> consumer = client.newConsumer()\n                .subscriptionType(SubscriptionType.Shared)\n                .topic(topic)\n                .subscriptionName(groupName)\n                .subscribe();\n```\n\n# 定时和延时消息\n## 相关概念\n**定时消息**：消息在发送至服务端后，实际业务并不希望消费端马上收到这条消息，而是推迟到某个时间点被消费，这类消息统称为定时消息。\n\n**延时消息**：消息在发送至服务端后，实际业务并不希望消费端马上收到这条消息，而是推迟一段时间后再被消费，这类消息统称为延时消息。\n\n实际上，定时消息可以看成是延时消息的一种特殊用法，其实现的最终效果和延时消息是一致的。\n\n## 适用场景\n如果系统是一个单体架构，则通过业务代码自己实现延时或利用第三方组件实现基本没有差别；一旦架构复杂起来，形成了一个大型分布式系统，有几十上百个微服务，这时通过应用自己实现定时逻辑会带来各种问题。一旦运行着延时程序的某个节点出现问题，整个延时的逻辑都会受到影响。\n\n针对以上问题，利用延时消息的特性投递到消息队列里，便是一个较好的解决方案，能统一计算延时时间，同时重试和死信机制确保消息不丢失。\n\n具体场景的示例如下：\n\n- 微信红包发出后，生产端发送一条延时24小时的消息，到了24小时消费端程序收到消息，进行用户是否已经领走红包的判断，如果没有则退还到原账户。\n- 小程序下单某商品后，后台存放一条延时30分钟的消息，到时间之后消费端收到消息触发对支付结果的判断，如果没有支付就取消订单，这样就实现了超过30分钟未完成支付就取消订单的逻辑。\n- 微信上用户将某条信息设置待办后，也可以通过发送一条定时消息，服务端到点收到这条定时消息，对用户进行待办项提醒。\n\n## 使用方式\n在  Pulsar 版的 SDK 中提供了专门的 API 来实现定时消息和延时消息。\n\n- 对于定时消息，您需要提供一个消息发送的时刻。\n- 对于延时消息，您需要提供一个时间长度作为延时的时长。\n\n## 定时消息\n定时消息通过生产者producer的 deliverAt() 方法实现，代码示例如下：\n```java\nString value = \"message content\";\ntry {\n        //需要先将显式的时间转换为 Timestamp\n      long timeStamp = new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\").parse(\"2020-11-11 00:00:00\").getTime();\n      //通过调用 producer 的 deliverAt 方法来实现定时消息\n        MessageId msgId = producer.newMessage()\n                .value(value.getBytes())\n                .deliverAt(timeStamp)\n                .send();\n} catch (ParseException e) {\n        //TODO 添加对 Timestamp 解析失败的处理方法\n        e.printStackTrace();\n}\n```\n> 定时消息的时间范围为当前时间开始计算，864000秒（10天）以内的任意时刻。如10月1日12:00开始，最长可以设置到10月11日12:00。\n>\n> 定时消息不可以使用 batch 模式发送，请在创建 producer 的时候把 enableBatch 参数设为 false。\n>  \n> 定时消息的消费模式仅支持使用 Shared 模式进行消费，否则会失去定时效果（Key-shared 也不支持）。\n## 延时消息\n延时消息通过生产者produce的 deliverAfter() 方法实现，代码示例如下：\n```java\nString value = \"message content\";\n\n//需要指定延时的时长\nlong delayTime = 10L;\n//通过调用 producer 的 deliverAfter 方法来实现定时消息\nMessageId msgId = producer.newMessage()\n    .value(value.getBytes())\n    .deliverAfter(delayTime, TimeUnit.SECONDS) //单位可以自由选择\n    .send();\n```\n> 延时消息的时长取值范围为0 - 864000秒（0秒 - 10天）。如10月1日12:00开始，最长可以设置864000秒。如果设置的时间超过这个时间，则直接按864000秒计算，到时会直接投递。\n>\n> 延时消息不可以使用 batch 模式发送，请在创建 producer 的时候把 enableBatch 参数设为 false。\n> \n> 延时消息的消费模式仅支持使用 Shared 模式进行消费，否则会失去延时效果（Key-shared 也不支持）。\n## 使用说明和限制\n使用定时和延时两种类型的消息时，请确保客户端的机器时钟和服务端的机器时钟（所有地域均为UTC+8 北京时间）保持一致，否则会有时差。\n\n定时和延时消息在精度上会有1秒左右的偏差。\n\n定时和延时消息不支持 batch 模式（批量发送），batch 模式会引起消息堆积，保险起见，请在创建 producer 的时候把 enableBatch 参数设为 false。\n\n定时和延时消息的消费模式仅支持使用 Shared 模式进行消费，否则会失去定时或延时效果（Key-shared 也不支持）。\n\n关于定时和延时消息的时间范围，最大均为10天。\n\n使用定时消息时，设置的时刻在当前时刻以后才会有定时效果，否则消息将被立即发送给消费者。\n\n设定定时时间后，从定时的时间点开始计算消息最长保留时间，例如定时到3天后发送，消息最长保留7天，则到了第10天仍未被消费时，消息会被删除。延时消息同理。\n\n普通类型 Topic 支持收发定时/延时消息，调用 使用方式 中的 API 即可发送定时/延时消息。\n\n# 消息重试与死信机制\n重试 Topic 是一种为了确保消息被正常消费而设计的 Topic 。当某些消息第一次被消费者消费后，没有得到正常的回应，则会进入重试 Topic 中，当重试达到一定次数后，停止重试，投递到死信 Topic 中。\n\n当消息进入到死信队列中，表示  Pulsar 版已经无法自动处理这批消息，一般这时就需要人为介入来处理这批消息。您可以通过编写专门的客户端来订阅死信 Topic，处理这批之前处理失败的消息。\n\n## 自动重试\n**相关概念**\n\n重试 Topic：一个重试 Topic 对应一个订阅名（一个订阅者组的唯一标识），以 Topic 形式存在于  Pulsar 版中。当您新建了一个订阅后，会自动创建一个重试 Topic，该 Topic 会自主实现消息重试的机制。\n\n该 Topic 命名为：\n\n- 2.7.1及以上版本集群：`[订阅名]-RETRY`\n- 2.6.1版本集群：`[订阅名]-retry`\n\n**实现原理**\n\n您创建的消费者使用某个订阅名以共享模式订阅了一个 Topic 后，如果开启了 enableRetry 属性，就会自动订阅这个订阅名对应的重试队列。\n\n> 仅共享模式支持自动化重试和死信机制，独占和灾备模式不支持。\n\n这里以 Java 语言客户端为例，在 topic1 创建了一个 sub1 的订阅，客户端使用 sub1 订阅名订阅了 topic1 并开启了 enableRetry，如下所示：\n```java\nConsumer consumer = client.newConsumer()\n    .topic(\"persistent://1******30/my-ns/topic1\")\n    .subscriptionType(SubscriptionType.Shared)//仅共享消费模式支持重试和死信\n    .enableRetry(true)\n    .subscriptionName(\"sub1\")\n    .subscribe();\n```\n此时，topic1 对 sub1 的订阅就形成了带有重试机制的投递模式，sub1 会自动订阅之前在新建订阅时自动创建的重试 Topic 中（可以在控制台 Topic 列表中找到）。当 topic1 中的消息投递第一次未收到消费端 ACK 时，这条消息就会被自动投递到重试 Topic ，并且由于 consumer 自动订阅了这个主题，后续这条消息会在一定的 重试规则下重新被消费。当达到最大重试次数后仍失败，消息会被投递到对应的死信队列，等待人工处理。\n\n## 自定义参数设置\n如果希望自定义配置这些参数，可以使用 deadLetterPolicy API 进行配置，代码如下：\n```java\nConsumer<byte[]> consumer = pulsarClient.newConsumer()\n    .topic(\"persistent://pulsar-****\")\n    .subscriptionName(\"sub1\")\n    .subscriptionType(SubscriptionType.Shared)\n    .enableRetry(true)//开启重试消费\n    .deadLetterPolicy(DeadLetterPolicy.builder()\n          .maxRedeliverCount(maxRedeliveryCount)//可以指定最大重试次数\n          .retryLetterTopic(\"persistent://my-property/my-ns/sub1-retry\")//可以指定重试队列\n          .deadLetterTopic(\"persistent://my-property/my-ns/sub1-dlq\")//可以指定死信队列\n          .build())\n    .subscribe();\n```\n\n## 重试规则\n指定任意延迟时间。第二个参数填写延迟时间，第三个参数指定时间单位。延迟时间和延时消息的取值范围一致，范围在1 - 864000（单位：秒）。\n\n## 重试消息的消息属性\n一条重试消息会给消息带上如下 property。\n```java\n{\n  REAL_TOPIC=\"persistent://my-property/my-ns/test, \n  ORIGIN_MESSAGE_ID=314:28:-1, \n  RETRY_TOPIC=\"persistent://my-property/my-ns/my-subscription-retry, \n  RECONSUMETIMES=16\n}\n```\n- REAL_TOPIC：原 Topic\n- ORIGIN_MESSAGE_ID：最初生产的消息 ID\n- RETRY_TOPIC：重试 Topic\n- RECONSUMETIMES：代表该消息重试的次数\n\n## 重试消息的消息 ID 流转\n消息 ID 流转过程如下所示，您可以借助此规则对相关日志进行分析。\n```text\n原始消费： msgid=1:1:0:1\n第一次重试： msgid=2:1:-1\n第二次重试： msgid=2:2:-1\n第三次重试： msgid=2:3:-1\n.......\n第16次重试： msgid=2:16:0:1\n第17次写入死信队列： msgid=3:1:-1\n```\n### 完整代码示例\n以下为借助  Pulsar 版实现完整消息重试机制的代码示例，供开发者参考。\n\n订阅主题\n```java\nConsumer<byte[]> consumer1 = client.newConsumer()\n        .topic(\"persistent://pulsar-****\")\n        .subscriptionName(\"my-subscription\")\n        .subscriptionType(SubscriptionType.Shared)\n        .enableRetry(true)//开启重试消费\n        //.deadLetterPolicy(DeadLetterPolicy.builder()\n        //         .maxRedeliverCount(maxRedeliveryCount)\n        //         .retryLetterTopic(\"persistent://my-property/my-ns/my-subscription-retry\")//可以指定重试队列\n        //         .deadLetterTopic(\"persistent://my-property/my-ns/my-subscription-dlq\")//可以指定死信队列\n        //         .build())\n        .subscribe();\n```\n执行消费\n```java\nwhile (true) {\n      Message msg = consumer.receive();\n      try {\n            // Do something with the message\n            System.out.printf(\"Message received: %s\", new String(msg.getData()));\n            // Acknowledge the message so that it can be deleted by the message broker\n            consumer.acknowledge(msg);\n      } catch (Exception e) {\n            // select reconsume policy\n            consumer.reconsumeLater(msg, 1000L, TimeUnit.MILLISECONDS);\n            //consumer.reconsumeLater(msg, 1);\n            //consumer.reconsumeLater(msg);\n      }\n}\n```\n\n## 主动重试\n当消费者在某个时间没有成功消费某条消息，如果想重新消费到这条消息时，消费者可以发送一条取消确认消息到  Pulsar 版服务端， Pulsar 版会将这条消息重新发给消费者。 这种方式重试时不会产生新的消息，所以也不能自定义重试间隔。\n\n以下为主动重试的 Java 代码示例：\n\n```java\nwhile (true) {\n    Message msg = consumer.receive();\n    try {\n        // Do something with the message\n        System.out.printf(\"Message received: %s\", new String(msg.getData()));\n        // Acknowledge the message so that it can be deleted by the message broker\n        consumer.acknowledge(msg);\n    } catch (Exception e) {\n        // Message failed to process, redeliver later\n        consumer.negativeAcknowledge(msg);\n    }\n}\n```\n# 参考文档\n- https://cloud.tencent.com/document/product/1179/44779\n- https://cloud.tencent.com/document/product/1179/58089"
  },
  {
    "path": "消息队列/RabbitMQ.md",
    "content": "\n* [RabbitMQ](#rabbitmq)\n    * [概念介绍](#概念介绍)\n    * [架构图](#架构图)\n    * [exchange类型](#exchange类型)\n        * [Direct](#direct)\n        * [Fanout](#fanout)\n        * [Topic](#topic)\n    * [RabbitMQ 消息持久化](#rabbitmq-消息持久化)\n    * [集群](#集群)\n    * [交换器无法根据自身类型和路由键找到符合条件队列时，会如何处理？](#交换器无法根据自身类型和路由键找到符合条件队列时会如何处理)\n    * [RabbitMQ 的六种模式](#rabbitmq-的六种模式)\n    * [死信队列应用场景](#死信队列应用场景)\n    * [事务机制](#事务机制)\n        * [步骤](#步骤)\n        * [事务回滚](#事务回滚)\n    * [Confirm模式](#confirm模式)\n        * [producer端confirm模式的实现原理](#producer端confirm模式的实现原理)\n        * [开启confirm模式的方法](#开启confirm模式的方法)\n        * [编程模式](#编程模式)\n            * [普通confirm模式](#普通confirm模式)\n            * [批量confirm模式](#批量confirm模式)\n            * [异步confirm模式](#异步confirm模式)\n* [参考文章](#参考文章)\n\n\n# RabbitMQ\n## 概念介绍\n- Broker：简单来说就是消息队列服务器实体。\n- Exchange：消息交换机，它指定消息按什么规则，路由到哪个队列。\n- Queue：消息队列载体，每个消息都会被投入到一个或多个队列。\n- Binding：绑定，它的作用就是把exchange和queue按照路由规则绑定起来。\n- Routing Key：路由关键字，exchange根据这个关键字进行消息投递。\n- vhost：虚拟主机，一个broker里可以开设多个vhost，用作不同用户的权限分离。\n    - 出于多租户和安全因素设计的，把AMQP的基本组件划分到一个虚拟的分组中，类似于网络中的namespace概念。当多个不同的用户使用同一个RabbitMQ server提供的服务时，可以划分出多个vhost，每个用户在自己的vhost创建exchange／queue等\n- producer：消息生产者，就是投递消息的程序。\n    - 消息生产者，投递消息\n    - 消息一般包含两个部分：消息体（payload)和标签(Label)\n- consumer：消息消费者，就是接受消息的程序。\n- channel：消息通道，在客户端的每个连接里，可建立多个channel，每个channel代表一个会话任务。\n- connection\n- ![](../img/消息队列/rabbitmq/rabbitmq-connection.png)\n    - 什么是\n        - connection 是 生产者或消费者与 RabbitMQ Broker 建立的连接，是一个TCP连接\n        - 一旦 TCP 连接建立起来，客户端紧接着可以创建一个 AMQP 信道（Channel），每个信道都会被指派一个唯一的 ID\n        - 信道是建立在 Connection 之上的虚拟连接，多个信道复用一个TCP连接，可以减少性能开销，同时也便于管理\n        - 因为一个应用需要向RabbitMQ 中生成或者消费消息的话，都要建一个TCP连接，TCP连接开销非常大，如果遇到使用高峰，性能瓶颈也随之显现\n    - 信道复用连接优势\n        - 复用TCP连接，减少性能开销，便于管理\n        - RabbitMQ 保障每一个信道的私密性\n    - 当每个信道的流量不是很大时，复用单一的 Connection 可以在产生性能瓶颈的情况下有效地节省 TCP 连接资源\n        - 信道本身的流量很大时，这时候多个信道复用一个 Connection 就会产生性能瓶颈，进而使整体的流量被限制了，此时就需要开辟多个 Connection，将这些信道均摊到这些 Connection 中\n## 架构图\n![](../img/消息队列/rabbitmq/架构图.png)\n## exchange类型\n### Direct\n![](../img/消息队列/rabbitmq/exchange-direct.png)\n  \n点对点模式，根据route_key精确匹配\n### Fanout\n![](../img/消息队列/rabbitmq/exchange-fanout.png)\n  \n广播模式，将消息发送到与该exchange绑定的所有queue上\n### Topic\n![](../img/消息队列/rabbitmq/exchange-topic.png)\n\n- 模式匹配，根据route_key模式匹配\n- 以上图中的配置为例，routingKey=”quick.orange.rabbit”的消息会同时路由到Q1与Q2，routingKey=”lazy.orange.fox”的消息会路由到Q1，routingKey=”lazy.brown.fox”的消息会路由到Q2，routingKey=”lazy.pink.rabbit”的消息会路由到Q2（只会投递给Q2一次，虽然这个routingKey与Q2的两个bindingKey都匹配）；routingKey=”quick.brown.fox”、routingKey=”orange”、routingKey=”quick.orange.male.rabbit”的消息将会被丢弃，因为它们没有匹配任何bindingKey。\n## RabbitMQ 消息持久化\n- 默认情况下重启服务器会导致消息丢失\n- 持久化需要满足如下三个条件才可以恢复 RabbitMQ 的数据\n    - 投递消息的时候 durable 设置为 true，消息持久化\n    - 消息已经到达持久化交换器上\n    - 消息已经到达持久化的队列上\n- 持久化的工作原理\n    - Rabbit 会将持久化消息写入磁盘上的持久化日志文件，等消息被消费之后，Rabbit 会把这条消息标识为等待垃圾回收\n## 集群\n- 普通集群模式\n    - 意思就是在多台机器上启动多个 RabbitMQ 实例，每个机器启动一个。你创建的 queue，只会放在一个 RabbitMQ 实例上，但是每个实例都同步 queue 的元数据（元数据可以认为是 queue 的一些配置信息，通过元数据，可以找到 queue 所在实例）。你消费的时候，实际上如果连接到了另外一个实例，那么那个实例会从 queue 所在实例上拉取数据过来。这方案主要是提高吞吐量的，就是说让集群中多个节点来服务某个 queue 的读写操作\n    - 缺点:一个服务节点宕机了，数据就丢失了\n- 镜像集群模式\n    - 这种模式，才是所谓的 RabbitMQ 的高可用模式。跟普通集群模式不一样的是，在镜像集群模式下，你创建的 queue，无论元数据还是 queue 里的消息都会存在于多个实例上，就是说，每个 RabbitMQ 节点都有这个 queue 的一个完整镜像，包含 queue 的全部数据的意思。然后每次你写消息到 queue 的时候，都会自动把消息同步到多个实例的 queue 上。RabbitMQ 有很好的管理控制台，就是在后台新增一个策略，这个策略是镜像集群模式的策略，指定的时候是可以要求数据同步到所有节点的，也可以要求同步到指定数量的节点，再次创建 queue 的时候，应用这个策略，就会自动将数据同步到其他的节点上去了。这样的话，好处在于，你任何一个机器宕机了，没事儿，其它机器（节点）还包含了这个 queue 的完整数据，别的 consumer 都可以到其它节点上去消费数据。\n    - 坏处在于，第一，这个性能开销也太大了吧，消息需要同步到所有机器上，导致网络带宽压力和消耗很重！RabbitMQ 一个 queue 的数据都是放在一个节点里的，镜像集群下，也是每个节点都放这个 queue 的完整数据。\n        - 性能开销大\n        - 没有扩展可言\n## 交换器无法根据自身类型和路由键找到符合条件队列时，会如何处理？\n- 我们对交换机设置参数的时候，有一个标志叫做 mandatory\n- 当mandatory标志位设置为true时\n- 如果exchange根据自身类型和消息routingKey无法找到一个合适的queue存储消息，那么broker就会调用basic.return方法将消息返还给生产者\n- 当mandatory设置为false时\n- 前置条件和上述保持一致，此时 broker会直接将消息丢弃\n## RabbitMQ 的六种模式\n- `simple模式`（即最简单的收发模式） 简单的生产者生产消息，放入队列，消费者消费消息\n- `work` 当生产者生产消息的速度大于消费者消费的速度，就要考虑用 work 工作模式，这样能提高处理速度提高负载\n- `publish`\n    - 1、每个消费者监听自己的队列；\n    - 2、生产者将消息发给broker，由交换机将消息转发到绑定此交换机的每个队列，每个绑定交换机的队列都将接收到消息。\n- `routing`\n    - 消息生产者将消息发送给交换机按照路由判断,路由是字符串(info) 当前产生的消息携带路由字符(对象的方法),交换机根据路由的key\n    - 只能匹配上路由key对应的消息队列,对应的消费者才能消费消息\n- `topic` 话题模式，一个消息被多个消费者获取，消息的目标 queue 可用 BindingKey 以通配符\n\n## 死信队列应用场景\n一般用在较为重要的业务队列中，确保未被正确消费的消息不被丢弃，一般发生消费异常可能原因主要有由于消息信息本身存在错误导致处理异常，处理过程中参数校验异常，或者因网络波动导致的查询异常等等，当发生异常时，当然不能每次通过日志来获取原消息，然后让运维帮忙重新投递消息（没错，以前就是这么干的= =）。通过配置死信队列，可以让未正确处理的消息暂存到另一个队列中，待后续排查清楚问题后，编写相应的处理代码来处理死信消息，这样比手工恢复数据要好太多了。\n\n## 事务机制\n\nRabbitMQ中与事务机制有关的方法有三个：txSelect(), txCommit()以及txRollback()\n- txSelect用于将当前channel设置成transaction模式\n- txCommit用于提交事务\n- txRollback用于回滚事务\n\n在通过txSelect开启事务之后，我们便可以发布消息给broker代理服务器了，如果txCommit提交成功了，则消息一定到达了broker了，如果在txCommit执行之前broker异常崩溃或者由于其他原因抛出异常，这个时候我们便可以捕获异常通过txRollback回滚事务了。\n\n### 步骤\n1. client发送Tx.Select\n2. broker发送Tx.Select-Ok(之后publish)\n3. client发送Tx.Commit\n4. broker发送Tx.Commit-Ok\n### 事务回滚\n```java\ntry {\n    channel.txSelect();\n    channel.basicPublish(exchange, routingKey, MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes());\n    int result = 1 / 0;\n    channel.txCommit();\n} catch (Exception e) {\n    e.printStackTrace();\n    channel.txRollback();\n}\n```\n## Confirm模式\n上面我们介绍了RabbitMQ可能会遇到的一个问题，即生成者不知道消息是否真正到达broker，随后通过AMQP协议层面为我们提供了事务机制解决了这个问题，但是采用事务机制实现会降低RabbitMQ的消息吞吐量，那么有没有更加高效的解决方式呢？答案是采用Confirm模式。\n\n### producer端confirm模式的实现原理\n\n生产者将信道设置成confirm模式，一旦信道进入confirm模式，所有在该信道上面发布的消息都会被指派一个唯一的ID(从1开始)，一旦消息被投递到所有匹配的队列之后，broker就会发送一个确认给生产者（包含消息的唯一ID）,这就使得生产者知道消息已经正确到达目的队列了，如果消息和队列是可持久化的，那么确认消息会将消息写入磁盘之后发出，broker回传给生产者的确认消息中deliver-tag域包含了确认消息的序列号，此外broker也可以设置basic.ack的multiple域，表示到这个序列号之前的所有消息都已经得到了处理。\n\nconfirm模式最大的好处在于他是异步的，一旦发布一条消息，生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息，当消息最终得到确认之后，生产者应用便可以通过回调方法来处理该确认消息，如果RabbitMQ因为自身内部错误导致消息丢失，就会发送一条nack消息，生产者应用程序同样可以在回调方法中处理该nack消息。\n\n在channel 被设置成 confirm 模式之后，所有被 publish 的后续消息都将被 confirm（即 ack） 或者被nack一次。但是没有对消息被 confirm 的快慢做任何保证，并且同一条消息不会既被 confirm又被nack 。\n\n###  开启confirm模式的方法\n已经在transaction事务模式的channel是不能再设置成confirm模式的，即这两种模式是不能共存的。\n\n生产者通过调用channel的confirmSelect方法将channel设置为confirm模式\n\n核心代码:\n```java\n//生产者通过调用channel的confirmSelect方法将channel设置为confirm模式  \nchannel.confirmSelect(); \n```\n### 编程模式\n\n对于固定消息体大小和线程数，如果消息持久化，生产者confirm(或者采用事务机制)，消费者ack那么对性能有很大的影响.\n\n消息持久化的优化没有太好方法，用更好的物理存储（SAS, SSD, RAID卡）总会带来改善。生产者confirm这一环节的优化则主要在于客户端程序的优化之上。归纳起来，客户端实现生产者confirm有三种编程方式：\n\n- 普通confirm模式：每发送一条消息后，调用waitForConfirms()方法，等待服务器端confirm。实际上是一种串行confirm了。\n- 批量confirm模式：每发送一批消息后，调用waitForConfirms()方法，等待服务器端confirm。\n- 异步confirm模式：提供一个回调方法，服务端confirm了一条或者多条消息后Client端会回调这个方法。\n\n事务模式性能是最差的，普通confirm模式性能比事务模式稍微好点，但是和批量confirm模式还有异步confirm模式相比，还是小巫见大巫。批量confirm模式的问题在于confirm之后返回false之后进行重发这样会使性能降低，异步confirm模式(async)编程模型较为复杂，至于采用哪种方式，那是仁者见仁智者见智了\n\n#### 普通confirm模式\n```java\npackage com.hrabbit.rabbitmq.confirm;\n\nimport com.hrabbit.rabbitmq.utils.ConnectionUtils;\nimport com.rabbitmq.client.Channel;\nimport com.rabbitmq.client.Connection;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.IOException;\nimport java.util.concurrent.TimeoutException;\n\n/**\n * @Auther: hrabbit\n * @Date: 2018-07-02 下午3:20\n * @Description:\n */\npublic class SendConfirm {\n    private static final String QUEUE_NAME = \"QUEUE_simple_confirm\";\n\n    @Test\n    public void sendMsg() throws IOException, TimeoutException, InterruptedException {\n        /* 获取一个连接 */\n        Connection connection = ConnectionUtils.getConnection();\n        /* 从连接中创建通道 */\n        Channel channel = connection.createChannel();\n        channel.queueDeclare(QUEUE_NAME, false, false, false, null);\n        //生产者通过调用channel的confirmSelect方法将channel设置为confirm模式\n        channel.confirmSelect();\n        String msg = \"Hello   QUEUE !\";\n        channel.basicPublish(\"\", QUEUE_NAME, null, msg.getBytes());\n        if (!channel.waitForConfirms()) {\n            System.out.println(\"send message 失败\");\n        } else {\n            System.out.println(\" send messgae ok ...\");\n        }\n        channel.close();\n        connection.close();\n    }\n}\n```\n#### 批量confirm模式\n批量confirm模式稍微复杂一点，客户端程序需要定期（每隔多少秒）或者定量（达到多少条）或者两则结合起来publish消息，然后等待服务器端confirm, 相比普通confirm模式，批量极大提升confirm效率，但是问题在于一旦出现confirm返回false或者超时的情况时，客户端需要将这一批次的消息全部重发，这会带来明显的重复消息数量，并且，当消息经常丢失时，批量confirm性能应该是不升反降的。\n```java\npackage com.hrabbit.rabbitmq.confirm;\n\nimport com.hrabbit.rabbitmq.utils.ConnectionUtils;\nimport com.rabbitmq.client.Channel;\nimport com.rabbitmq.client.Connection;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.IOException;\nimport java.util.concurrent.TimeoutException;\n\n/**\n * @Auther: hrabbit\n * @Date: 2018-07-02 下午3:25\n * @Description:\n */\npublic class SendbatchConfirm {\n\n    private static final String QUEUE_NAME = \"QUEUE_simple_confirm\";\n\n    @Test\n    public void sendMsg() throws IOException, TimeoutException, InterruptedException {\n        /* 获取一个连接 */\n        Connection connection = ConnectionUtils.getConnection();\n        /* 从连接中创建通道 */\n        Channel channel = connection.createChannel();\n        channel.queueDeclare(QUEUE_NAME, false, false, false, null);\n        //生产者通过调用channel的confirmSelect方法将channel设置为confirm模式\n        channel.confirmSelect();\n\n        //生产者通过调用channel的confirmSelect方法将channel设置为confirm模式\n        channel.confirmSelect();\n        String msg = \"Hello   QUEUE !\";\n        for (int i = 0; i < 10; i++) {\n            channel.basicPublish(\"\", QUEUE_NAME, null,msg.getBytes());\n        }\n\n        if (!channel.waitForConfirms()) {\n            System.out.println(\"send message error\");\n        } else {\n            System.out.println(\" send messgae ok ...\");\n        }\n        channel.close();\n        connection.close();\n    }\n}\n\n```\n#### 异步confirm模式\nChannel对象提供的ConfirmListener()回调方法只包含deliveryTag（当前Chanel发出的消息序号），我们需要自己为每一个Channel维护一个unconfirm的消息序号集合，每publish一条数据，集合中元素加1，每回调一次handleAck方法，unconfirm集合删掉相应的一条（multiple=false）或多条（multiple=true）记录。从程序运行效率上看，这个unconfirm集合最好采用有序集合SortedSet存储结构。实际上，SDK中的waitForConfirms()方法也是通过SortedSet维护消息序号的。\n```java\npackage com.hrabbit.rabbitmq.confirm;\n\nimport com.hrabbit.rabbitmq.utils.ConnectionUtils;\nimport com.rabbitmq.client.Channel;\nimport com.rabbitmq.client.ConfirmListener;\nimport com.rabbitmq.client.Connection;\n\nimport java.io.IOException;\nimport java.util.Collections;\nimport java.util.SortedSet;\nimport java.util.TreeSet;\nimport java.util.concurrent.TimeoutException;\n\n/**\n * @Auther: hrabbit\n * @Date: 2018-07-02 下午3:28\n * @Description:\n */\npublic class SendAync {\n\n    private static final String QUEUE_NAME = \"QUEUE_simple_confirm_aync\";\n    public static void main(String[] args) throws IOException, TimeoutException {\n        /* 获取一个连接 */\n        Connection connection = ConnectionUtils.getConnection();\n        /* 从连接中创建通道 */\n        Channel channel = connection.createChannel();\n        channel.queueDeclare(QUEUE_NAME, false, false, false, null);\n        //生产者通过调用channel的confirmSelect方法将channel设置为confirm模式\n        channel.confirmSelect();\n        final SortedSet<Long> confirmSet = Collections.synchronizedSortedSet(new TreeSet<Long>());\n        channel.addConfirmListener(new ConfirmListener() {\n            //每回调一次handleAck方法，unconfirm集合删掉相应的一条（multiple=false）或多条（multiple=true）记录。\n            public void handleAck(long deliveryTag, boolean multiple) throws IOException {\n                if (multiple) {\n                    System.out.println(\"--multiple--\");\n                    confirmSet.headSet(deliveryTag + 1).clear();\n                    //用一个SortedSet, 返回此有序集合中小于end的所有元素。\n                    } else {\n                    System.out.println(\"--multiple false--\");\n                    confirmSet.remove(deliveryTag);\n                }\n            }\n            public void handleNack(long deliveryTag, boolean multiple) throws IOException {\n                System.out.println(\"Nack, SeqNo: \" + deliveryTag + \", multiple: \" + multiple);\n                if (multiple) {\n                    confirmSet.headSet(deliveryTag + 1).clear();\n                } else {\n                    confirmSet.remove(deliveryTag);\n                }\n            }\n        });\n        String msg = \"Hello   QUEUE !\";\n        while (true) {\n            long nextSeqNo = channel.getNextPublishSeqNo();\n            channel.basicPublish(\"\", QUEUE_NAME, null, msg.getBytes());\n            confirmSet.add(nextSeqNo);\n        }\n    }\n}\n```\n# 参考文章\n- https://mfrank2016.github.io/breeze-blog/2020/05/04/rabbitmq/rabbitmq-how-to-use-dead-letter-queue/\n- https://blog.csdn.net/u013256816/article/details/55515234\n- https://www.jianshu.com/p/801456df3930"
  },
  {
    "path": "消息队列/Redis.md",
    "content": "\n* [Redis](#redis)\n  * [Redis实现mq主要是依赖数据结构list](#redis实现mq主要是依赖数据结构list)\n  * [不足](#不足)\n\n\n# Redis\n## Redis实现mq主要是依赖数据结构list\nlpush：从队列左边插入数据； rpop：从队列右边取出数据\n## 不足\n- `消息持久化` redis是内存数据库，虽然有aof和rdb两种机制进行持久化，但这只是辅助手段，这两种手段都是不可靠的。当redis服务器宕机时一定会丢失一部分数据，这对于很多业务都是没法接受的\n- `热key性能问题` 不论是用codis还是twemproxy这种集群方案，对某个队列的读写请求最终都会落到同一台redis实例上，并且无法通过扩容来解决问题。如果对某个list的并发读写非常高，就产生了无法解决的热key，严重可能导致系统崩溃\n- `没有确认机制`\n  每当执行rpop消费一条数据，那条消息就被从list中永久删除了。如果消费者消费失败，这条消息也没法找回了。你可能说消费者可以在失败时把这条消息重新投递到进队列，但这太理想了，极端一点万一消费者进程直接崩了呢，比如被kill -9，panic，coredump…\n- `不支持多订阅者`\n  一条消息只能被一个消费者消费，rpop之后就没了。如果队列中存储的是应用的日志，对于同一条消息，监控系统需要消费它来进行可能的报警，BI系统需要消费它来绘制报表，链路追踪需要消费它来绘制调用关系……这种场景redis list就没办法支持了\n- `不支持二次消费`\n  一条消息rpop之后就没了。如果消费者程序运行到一半发现代码有bug，修复之后想从头再消费一次就不行了"
  },
  {
    "path": "消息队列/RocketMQ.md",
    "content": "\n\n* [RocketMQ](#rocketmq)\n  * [架构图](#架构图)\n* [组件](#组件)\n  * [NameServer](#nameserver)\n  * [Broker](#broker)\n  * [Producer](#producer)\n  * [Consumer](#consumer)\n* [消息特性](#消息特性)\n* [消息功能](#消息功能)\n* [rocket的事务实现机制](#rocket的事务实现机制)\n  * [概览](#概览)\n  * [交互流程](#交互流程)\n* [Broker 集群部署架构](#broker-集群部署架构)\n  * [多 Master 模式](#多-master-模式)\n  * [多 Master 多 Salve - 异步复制 模式](#多-master-多-salve---异步复制-模式)\n  * [多 Master 多 Salve - 同步双写 模式](#多-master-多-salve---同步双写-模式)\n  * [Dledger 模式](#dledger-模式)\n* [参考文章](#参考文章)\n\n\n# RocketMQ\n## 架构图\n![](../img/消息队列/rocketmq/架构图.png)\n# 组件\n## NameServer\nNameServer 是一个功能齐全的服务器，主要包括两个功能：\n- Broker 管理，NameServer接受来自 Broker 集群的注册，并提供心跳机制来检查 Broker 是否处于活动状态。\n- 路由管理，每个 NameServer 将保存有关代理集群的完整路由信息和客户端查询的队列信息。\n## Broker\nBroker主要负责消息的存储、投递和查询以及服务高可用保证，为了实现这些功能，Broker包含了以下几个重要子模块\n- Remoting Module远程模块，代理的入口，处理来自客户端的请求。\n- Client Manager，管理客户端（Producer/Consumer），维护Consumer的topic订阅。\n- Store Service存储服务，提供简单的 API 来存储或查询物理磁盘中的消息。\n- HA Service，提供主代理和从代理之间的数据同步功能。\n- Index Service索引服务，通过指定的key为消息建立索引，并提供快速的消息查询。\n\n![](../img/消息队列/rocketmq/broker.png)\n## Producer\n生产者 消息发布的角色，支持分布式集群方式部署。Producer通过MQ的负载均衡模块选择相应的Broker集群队列进行消息投递，投递的过程支持快速失败并且低延迟\n## Consumer\n消费者 消息消费的角色，支持分布式集群方式部署。支持以push推，pull拉两种模式对消息进行消费。同时也支持集群方式和广播方式的消费，它提供实时消息订阅机制，可以满足大多数用户的需求。\n\n# 消息特性\n\n`消息重试`：在消费者返回消息重试的响应后，消息队列RocketMQ版会按照相应的重试规则进行消息重投。\n\n`至少投递一次（At-least-once）`：消息队列RocketMQ版保证消息成功被消费一次。消息队列RocketMQ版的分布式特点和瞬变的网络条件，或者用户应用重启发布的情况下，可能导致消费者收到重复的消息。开发人员应将其应用程序设计为多次处理一条消息不会产生任何错误或不一致性。消息幂等最佳实践请参见消费幂等。\n\n`消息过滤`：消息队列RocketMQ版支持设置消息属性给消息进行分类，消息队列RocketMQ版服务端会根据您订阅消息时设置的过滤条件对消息进行过滤，您将只消费到需要关注的消息。\n# 消息功能\n\n`消息查询`：消息队列RocketMQ版提供了三种消息查询的方式，分别是按Message ID、Message Key以及Topic查询。\n\n`查询消息轨迹`：通过消息轨迹，能清晰定位消息从生产者发出，经由消息队列RocketMQ版服务端，投递给消息消费者的完整链路，方便定位排查问题。\n\n`集群消费和广播消费`：当使用集群消费模式时，消息队列RocketMQ版认为任意一条消息只需要被消费者集群内的任意一个消费者处理即可；当使用广播消费模式时，消息队列RocketMQ版会将每条消息推送给消费者集群内所有注册过的消费者，保证消息至少被每台机器消费一次。\n\n`重置消费位点`：根据时间或位点重置消费进度，允许用户进行消息回溯或者跳过堆积的消息从最新位点消费。\n\n`死信队列`：将无法正常消费的消息储存到特殊的死信队列供后续处理。\n\n`资源报表`：消息生产和消费数据的统计功能。通过该功能，您可查询在一段时间范围内发送至某Topic的消息总量或者TPS（消息生产数据），也可查询在一个时间段内某Topic投递给某Group ID的消息总量或TPS（消息消费数据）。\n\n`监控报警`：您可使用消息队列RocketMQ版提供的监控报警功能，监控某Group ID订阅的某Topic的消息消费状态并接收报警短信，帮助您实时掌握消息消费状态，以便及时处理消费异常。\n\n\n# rocket的事务实现机制\nRocketMQ提供了事务消息的功能，采用2PC(两段式协议)+补偿机制（事务回查）的分布式事务功能，通过消息队列 RocketMQ 版事务消息能达到分布式事务的最终一致。\n\n## 概览\n- `半事务消息`：\n暂不能投递的消息，发送方已经成功地将消息发送到了消息队列 RocketMQ 版服务端，但是服务端未收到生产者对该消息的二次确认，此时该消息被标记成“暂不能投递”状态，处于该种状态下的消息即半事务消息。\n- `消息回查`：\n由于网络闪断、生产者应用重启等原因，导致某条事务消息的二次确认丢失，消息队列 RocketMQ 版服务端通过扫描发现某条消息长期处于“半事务消息”时，需要主动向消息生产者询问该消息的最终状态（Commit 或是 Rollback），该询问过程即消息回查。\n\n## 交互流程\n![](../img/消息队列/rocketmq/rocket的事务.png)\n\n事务消息发送步骤如下：\n1. 发送方将半事务消息发送至消息队列 RocketMQ 版服务端。\n2. 消息队列 RocketMQ 版服务端将消息持久化成功之后，向发送方返回 Ack确认消息已经发送成功，此时消息为半事务消息。\n3. 发送方开始执行本地事务逻辑。\n4. 发送方根据本地事务执行结果向服务端提交二次确认（Commit 或是 Rollback），服务端收到 Commit 状态则将半事务消息标记为可投递，订阅方最终将收到该消息；服务端收到 Rollback 状态则删除半事务消息，订阅方将不会接受该消息。\n\n事务消息回查步骤如下：\n1. 在断网或者是应用重启的特殊情况下，上述步骤 4 提交的二次确认最终未到达服务端，经过固定时间后服务端将对该消息发起消息回查。\n2. 发送方收到消息回查后，需要检查对应消息的本地事务执行的最终结果。\n3. 发送方根据检查得到的本地事务的最终状态再次提交二次确认，服务端仍按照步骤 4 对半事务消息进行操作。\n\n总体而言RocketMQ事务消息分为两条主线\n- 发送流程：发送half message(半消息)，执行本地事务，发送事务执行结果\n- 定时任务回查流程：MQ定时任务扫描半消息，回查本地事务，发送事务执行结果\n\n# Broker 集群部署架构\n开始部署 RocketMQ 之前，我们也做过一些功课，对现在 RocketMQ 支持的集群方案做了一些整理，目前 RocketMQ 支持的集群部署方案有以下4种：\n\n- 多Master模式：一个集群无Slave，全是Master，例如2个Master或者3个Master\n- 多Master多Slave模式-异步复制：每个Master配置一个Slave，有多对Master-Slave，HA采用异步复制方式，主备有短暂消息延迟（毫秒级）\n- 多Master多Slave模式-同步双写：每个Master配置一个Slave，有多对Master-Slave，HA采用同步双写方式，即只有主备都写成功，才向应用返回成功\n- Dledger部署：每个Master配置二个 Slave 组成 Dledger Group，可以有多个 Dledger Group，由 Dledger 实现 Master 选举\n\n## 多 Master 模式\n一个 RocketMQ 集群中所有的节点都是 Master 节点，每个 Master 节点没有 Slave 节点。\n\n![](../img/消息队列/rocketmq/多master模式.png)\n\n这种模式的优缺点如下：\n\n- 优点：配置简单，单个Master宕机或重启维护对应用无影响，在磁盘配置为RAID10时，即使机器宕机不可恢复情况下，由于RAID10磁盘非常可靠，消息也不会丢（异步刷盘丢失少量消息，同步刷盘一条不丢），性能最高；\n- 缺点：单台机器宕机期间，这台机器上未被消费的消息在机器恢复之前不可订阅，消息实时性会受到影响。\n\n## 多 Master 多 Salve - 异步复制 模式\n每个Master配置一个Slave，有多对Master-Slave，HA采用异步复制方式，主备有短暂消息延迟（毫秒级）\n\n![](../img/消息队列/rocketmq/多master多salve.png)\n\n这种模式的优缺点如下：\n\n- 优点：即使磁盘损坏，消息丢失的非常少，且消息实时性不会受影响，同时Master宕机后，消费者仍然可以从Slave消费，而且此过程对应用透明，不需要人工干预，性能同多Master模式几乎一样；\n- 缺点：Master宕机，磁盘损坏情况下会丢失少量消息。\n\n## 多 Master 多 Salve - 同步双写 模式\n每个Master配置一个Slave，有多对Master-Slave，HA采用同步双写方式，即只有主备都写成功，才向应用返回成功\n\n![](../img/消息队列/rocketmq/多master多salve同步双写.png)\n\n这种模式的优缺点如下：\n\n- 优点：数据与服务都无单点故障，Master宕机情况下，消息无延迟，服务可用性与数据可用性都非常高；\n- 缺点：性能比异步复制模式略低（大约低10%左右），发送单个消息的RT会略高，且目前版本在主节点宕机后，备机不能自动切换为主机。\n\n## Dledger 模式\nRocketMQ 4.5 以前的版本大多都是采用 Master-Slave 架构来部署，能在一定程度上保证数据的不丢失，也能保证一定的可用性。\n\n但是那种方式 的缺陷很明显，最大的问题就是当 Master Broker 挂了之后 ，没办法让 Slave Broker 自动 切换为新的 Master Broker，需要手动更改配置将 Slave Broker 设置为 Master Broker，以及重启机器，这个非常麻烦。\n\n在手式运维的期间，可能会导致系统的不可用。\n\n使用 Dledger 技术要求至少由三个 Broker 组成 ，一个 Master 和两个 Slave，这样三个 Broker 就可以组成一个 Group ，也就是三个 Broker 可以分组来运行。一但 Master 宕机，Dledger 就可以从剩下的两个 Broker 中选举一个 Master 继续对外提供服务。\n\n![](../img/消息队列/rocketmq/dledger.png)\n\n使用 Dledger 方式最终的逻辑部署图如下\n\n![](../img/消息队列/rocketmq/dledger逻辑部署图.png)\n\n**高可用**\n\n三个 NameServer 极端情况下，确保集群的可用性，任何两个 NameServer 挂掉也不会影响信息的整体使用。\n\n在上图中每个 Master Broker 都有两个 Slave Broker，这样可以保证可用性，如在同一个 Dledger Group 中 Master Broker 宕机后，Dledger 会去行投票将剩下的节点晋升为 Master Broker。\n\n**高并发**\n\n假设某个Topic的每秒十万消息的写入， 可以增加 Master Broker 然后十万消息的写入会分别分配到不同的 Master Broker ，如有5台 Master Broker 那每个 Broker 就会承载2万的消息写入。\n\n**可伸缩**\n\n如果消息数量增大，需要存储更多的数量和最高的并发，完全可以增加 Broker ，这样可以线性扩展集群。\n\n**海量消息**\n\n数据都是分布式存储的，每个Topic的数据都会分布在不同的 Broker 中，如果需要存储更多的数据，只需要增加 Master Broker 就可以了。\n# 参考文章\n- https://juejin.cn/post/6844904193526857742\n- https://segmentfault.com/a/1190000038318572"
  },
  {
    "path": "消息队列/Zookeeper.md",
    "content": "\n* [Zookeeper](#zookeeper)\n* [概念](#概念)\n* [用zookeeper可以干嘛](#用zookeeper可以干嘛)\n* [数据结构](#数据结构)\n    * [ZNode](#znode)\n        * [ZNode的类型](#znode的类型)\n        * [ZNode的状态信息](#znode的状态信息)\n* [监听机制](#监听机制)\n    * [Watcher注册监听器实例：基于Zookeeper实现简易版配置中心](#watcher注册监听器实例基于zookeeper实现简易版配置中心)\n* [角色](#角色)\n    * [leader](#leader)\n    * [follower](#follower)\n    * [Observer](#observer)\n* [Zookeeper Leader 选举原理](#zookeeper-leader-选举原理)\n    * [1、服务器启动时的 leader 选举](#1服务器启动时的-leader-选举)\n    * [2、运行过程中的 leader 选举](#2运行过程中的-leader-选举)\n* [参考文章](#参考文章)\n\n\n# Zookeeper\n# 概念\n- Zookeeper 是一个分布式协调服务，可用于服务发现，分布式锁，分布式领导选举，配置管理等。\n- Zookeeper 提供了一个类似于 Linux 文件系统的树形结构（可认为是轻量级的内存文件系统，但 只适合存少量信息，完全不适合存储大量文件或者大文件），同时提供了对于每个节点的监控与通知机制。\n\n# 用zookeeper可以干嘛\n- 统一命令服务  域名和Ip的映射关系用zk储存，nginx也可以\n- 统一配置管理，分布式环境下的配置文件同步，将配置文件写入一个znode，其他节点监听这个节点\n- 统一集群管理，分布式环境下各个节点的状态同步，将节点状态写入一个znode，其他节点监听这个节点\n# 数据结构\nzookeeper 提供的名称空间非常类似于标准文件系统，key-value 的形式存储。名称 key 由斜线 / 分割的一系列路径元素，zookeeper 名称空间中的每个节点都是由一个路径标识\n\n## ZNode\nZooKeeper数据模型ZNode 在ZooKeeper中，数据信息被保存在一个个数据节点上，这些节点被称为ZNode。ZNode 是Zookeeper 中最小数据单位，在 ZNode 下面又可以再挂 ZNode，这样一层层下去就形成了一个层次化命名空间 ZNode 树，我们称为 ZNode Tree，它采用了类似文件系统的层级树状结构进行管理\n- 可以储存少量数据（1MB）\n### ZNode的类型\nZookeeper 节点类型可以分为三大类：\n- 持久性节点（Persistent）\n- 临时性节点（Ephemeral）-e\n- 顺序性节点（Sequential）-s -es\n\n在开发中在创建节点的时候通过组合可以生成以下四种节点类型：持久节点、持久顺序节点(-s)、临时节点、临时顺序节点(-es)。不同类型的节点则会有不同的生命周期：\n\n**持久节点**：是Zookeeper中最常见的一种节点类型，所谓持久节点，就是指节点被创建后会一直存在服务器，直到删除操作主动清除\n- create /node\n- 默认类型\n\n**持久顺序节点**：就是有顺序的持久节点，节点特性和持久节点是一样的，只是额外特性表现在顺序上。顺序特性实质是在创建节点的时候，会在节点名后面加上一个数字后缀，来表示其顺序。\n- create -s /node\n\n\n**临时节点**：就是会被自动清理掉的节点，它的生命周期和客户端会话绑在一起，客户端会话结束，节点会被删除掉。与持久性节点不同的是，临时节点不能创建子节点。\n- create -e /node\n\n**临时顺序节点**：就是有顺序的临时节点，和持久顺序节点相同，在其创建的时候会在名字后面加上数字后缀。\n- create -es /node\n### ZNode的状态信息\n整个 ZNode 节点内容包括两部分：节点数据内容和节点状态信息。数据内容是空，其他的属于状态信息。这些节点的状态信息分别的含义如下所示：\n\n```html\ncZxid 就是 Create ZXID，表示节点被创建时的事务ID。\nctime 就是 Create Time，表示节点创建时间。\nmZxid 就是 Modified ZXID，表示节点最后一次被修改时的事务ID。\nmtime 就是 Modified Time，表示节点最后一次被修改的时间。\npZxid 表示该节点的子节点列表最后一次被修改时的事务 ID。只有子节点列表变更才会更新 pZxid，子节点内容变更不会更新。\ncversion 表示子节点的版本号。\ndataVersion 表示内容版本号。\naclVersion 标识acl版本\nephemeralOwner 表示创建该临时节点时的会话 sessionID，如果是持久性节点那么值为 0\ndataLength 表示数据长度。\nnumChildren 表示直系子节点数。\n```\n\n#  监听机制\nZookeeper使用Watcher机制实现分布式数据的发布/订阅功能\n\n一个典型的发布/订阅模型系统定义了一种 一对多的订阅关系，能够让多个订阅者同时监听某一个主题对象，当这个主题对象自身状态变化时，会通知所有订阅者，使它们能够做出相应的处理。\n\n在 ZooKeeper 中，引入了 Watcher 机制来实现这种分布式的通知功能。ZooKeeper 允许客户端向服务端注册一个 Watcher 监听，当服务端的一些指定事件触发了这个 Watcher，那么Zk就会向指定客户端发送一个事件通知来实现分布式的通知功能。\n\n整个Watcher注册与通知过程如图所示。\n\n![](../img/消息队列/zookeeper/监听机制.png)\n\n- Client向ZK注册监听器。监听某个目录 可以监听下面的子节点， 也可以监听下面的数据。\n- 客户端在向Zookeeper服务器注册的同时，会将Watcher对象存储在客户端的WatcherManager当中。用来对各种监听器进行管理。\n- 当Zookeeper服务器触发Watcher事件后，会向客户端发送通知。如果监听的目录中的数据或节点发生了改变，ZK就会发送一个通知到Client。这里的流程是把Watcher对象发送到WatchManager里，则之前存储的watcher对象里面的内容就会被更新。更新之后，Client就会从WatchManager中再次获取到watcher对象，然后调用接收到通知之后的执行逻辑，比如是要把变化后的监听数据拿回来还是去做其他事情\n\n## Watcher注册监听器实例：基于Zookeeper实现简易版配置中心\n创建一个Web项目，将数据库连接信息交给Zookeeper配置中心管理，即：当项目Web项目启动时，从Zookeeper进行MySQL配置参数的拉取 \n\n要求项目通过数据库连接池访问MySQL（连接池可以自由选择熟悉的） \n\n当Zookeeper配置信息变化后Web项目自动感知，正确释放之前连接池，创建新的连接池\n\n第一步：创建web工程，客户端连接ZK集群\n```java\npublic class ZkServlet extends HttpServlet {\n\n    ZkClient zkClient = null;\n    Connection conn = null;\n    DataSource dataSource = null;\n\n    /*\n     * init: servlet对象创建的，调用此方法完成初始化操作\n     * */\n    @Override\n    public void init(ServletConfig servletConfig) throws ServletException {\n        // 客户端连接ZK集群\n        new ZkClient(\"linux121:2181, linux122:2181, linux123:2181\");\n\n        // 判断节点是否存在，不存在创建节点并赋值\n        boolean exists = zkClient.exists(\"/mysql_configuration\");\n        if (!exists) {\n            zkClient.createEphemeral(\"/mysql_configuration\", \"{'driverClassName':'com.mysql.jdbc.Driver', 'url':'jdbc:mysql://linux123:3306/zookeeper?characterEncoding=UTF-8', 'username':'root', 'password':'123'}\");\n        }\n        // 设置自定义的序列化类型\n        zkClient.setZkSerializer(new ZkStrSerializer());\n    }\n}\n```\n这里将Mysql的配置信息组织成Map的格式存储在ZK结群的某一节点上，方便后续客户端的读取和使用。\n\n第二步：从ZK集群上拉取Mysql配置文件信息，并注册监听器\n\n```java\n@Override\nprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n\n    // 初始化zk的监听器\n    registerListener();\n\n    // 客户端从节点上读取数据\n    String data = (String) zkClient.readData(\"/mysql_configuration\");\n    // 利用JSON将读取下的数据格式转为Map类型，方便后续创建数据库连接池对象\n    Map<String, String> map = JSON.parseObject(data, new TypeReference<Map<String, String>>(){});\n    System.out.println(\"MysqlConfiguration: \" + map);\n    try {\n        // 根据拉取的Mysql配置信息获取数据库连接\n        conn = getDruidConnection(map);\n        // 根据连接池查询数据库数据\n        queryMysqlData(conn);\n        // 向前端返回Mysql的配置信息\n        resp.getWriter().write(map.toString());\n\n        // 休眠10s后向zookeeper中写入新的配置文件\n        Thread.sleep(10000);\n        // 休眠10s后，更新该节点的数据，观察监听器的功能\n        zkClient.writeData(\"/mysql_configuration\", \"{'driverClassName':'com.mysql.jdbc.Driver', 'url':'jdbc:mysql://linux123:3306/zookeeper?characterEncoding=UTF-8', 'username':'root', 'password':'12345678'}\");\n\n        // 测试结束，关闭资源\n        conn.close();\n\n    } catch (Exception e) {\n        e.printStackTrace();\n    }\n}\n```\n各自定义函数如下所示：\n```java\n// 注册监听器\npublic void registerListener(final HttpServletResponse resp)\n{\n    // 注册监听器，节点数据改变的类型，接收通知后的处理逻辑定义\n    zkClient.subscribeDataChanges(\"/mysql_configuration\", new IZkDataListener() {\n\n        // path: 是监听的数据路径\n        // data: 改变之后的新的数据\n        public void handleDataChange(String path, Object data) throws Exception {\n            // 定义接收通知之后的处理逻辑\n            // 首先释放原先的连接池\n            conn.close();\n\n            // 根据新的配置信息，创建新的连接池\n            // 获取新的配置信息\n            Map<String, String> newMysqlConf = getMysqlConf((String) data);\n            System.out.println(\"New Mysql Configuration: \" + newMysqlConf);\n\n            // 创建新的连接池连接数据\n            conn = getDruidConnection(newMysqlConf);\n\n            // 根据新连接查询数据\n            queryMysqlData(conn);\n            resp.getWriter().write(\"New Mysql Configuration: \" + newMysqlConf);\n        }\n\n        // 处理数据的删除 -> 节点删除\n        public void handleDataDeleted(String path) throws Exception {\n            System.out.println(path + \" is deleted!!\");\n        }\n    });\n}\n\npublic Map<String, String> getMysqlConf(String data)\n{\n    // 将字符串的data数据转换为Map类型\n    Map<String, String> map = JSON.parseObject((String) data, new TypeReference<Map<String, String>>(){});\n    return map;\n}\n\n// 通过连接池获取jdbc连接对象\npublic Connection getDruidConnection(Map map) throws Exception {\n    dataSource = DruidDataSourceFactory.createDataSource(map);\n    conn = dataSource.getConnection();\n    return conn;\n}\n\n// 查询表中的数据并打印\npublic void queryMysqlData(Connection conn) throws SQLException {\n    Statement statement = conn.createStatement();\n\n    String sql = \"select * from homework\";\n    ResultSet resultSet = statement.executeQuery(sql);\n    while(resultSet.next())\n    {\n        System.out.println(resultSet.getInt(\"id\"));\n        System.out.println(resultSet.getString(\"name\"));\n        System.out.println(resultSet.getString(\"addr\"));\n    }\n}\n```\n利用`subscribeDataChanges`函数来实现对节点数据变化的监听。这里要注意的就是`handleDataChange(String path, Object data)`方法里的data参数是改变之后的新的数据内容。因此通过代码的实际使用也可以重新理解最开始介绍的Watcher机制的过程。如果监听的目录中的数据或节点发生了改变，ZK就会发送一个通知到Client。就是把Watcher对象发送到WatchManager里，则之前存储的watcher对象里面的内容就会被更新。而Client从WatchManager中再次获取到watcher对象，此时的watcher对象的内容就是被更新后的数据信息（也就是参数data数据）\n# 角色\n## leader\n\n- 一个 Zookeeper 集群同一时间只会有一个实际工作的 Leader，它会发起并维护与各 Follwer及 Observer 间的心跳。\n- 所有的写操作必须要通过 Leader 完成再由 Leader 将写操作广播给其它服务器。只要有超过半数节点（不包括 observeer 节点）写入成功，该写请求就会被提交（类 2PC 协议）。\n\n## follower\n- 一个 Zookeeper 集群可能同时存在多个 Follower，它会响应 Leader 的心跳，\n- Follower 可直接处理并返回客户端的读请求，同时会将写请求转发给 Leader 处理，\n- 并且负责在 Leader 处理写请求时对请求进行投票。\n\n## Observer\n- 角色与 Follower 类似，但是无投票权。Zookeeper 需保证高可用和强一致性，为了支持更多的客 户端，需要增加更多 Server；Server 增多，投票阶段延迟增大，影响性能；引入 Observer，\n- Observer 不参与投票； Observers 接受客户端的连接，并将写请求转发给 leader 节点； 加入更多 Observer 节点，提高伸缩性，同时不影响吞吐率。\n\n# Zookeeper Leader 选举原理\n\nzookeeper 的 leader 选举存在两个阶段，一个是服务器启动时 leader 选举，另一个是运行过程中 leader 服务器宕机。在分析选举原理前，先介绍几个重要的参数。\n\n- 服务器 ID(myid)：编号越大在选举算法中权重越大\n- 事务 ID(zxid)：值越大说明数据越新，权重越大\n- 逻辑时钟(epoch-logicalclock)：同一轮投票过程中的逻辑时钟值是相同的，每投完一次值会增加\n\n选举状态：\n\n- LOOKING: 竞选状态\n- FOLLOWING: 随从状态，同步 leader 状态，参与投票\n- OBSERVING: 观察状态，同步 leader 状态，不参与投票\n- LEADING: 领导者状态\n## 1、服务器启动时的 leader 选举\n每个节点启动的时候都 LOOKING 观望状态，接下来就开始进行选举主流程。这里选取三台机器组成的集群为例。第一台服务器 server1启动时，无法进行 leader 选举，当第二台服务器 server2 启动时，两台机器可以相互通信，进入 leader 选举过程。\n1. 每台 server 发出一个投票，由于是初始情况，server1 和 server2 都将自己作为 leader 服务器进行投票，每次投票包含所推举的服务器myid、zxid、epoch，使用（myid，zxid）表示，此时 server1 投票为（1,0），server2 投票为（2,0），然后将各自投票发送给集群中其他机器。\n2. 接收来自各个服务器的投票。集群中的每个服务器收到投票后，首先判断该投票的有效性，如检查是否是本轮投票（epoch）、是否来自 LOOKING 状态的服务器。\n3. 分别处理投票。针对每一次投票，服务器都需要将其他服务器的投票和自己的投票进行对比，对比规则如下：\n   1. 优先比较 epoch\n   2. 检查 zxid，zxid 比较大的服务器优先作为 leader\n   3. 如果 zxid 相同，那么就比较 myid，myid 较大的服务器作为 leader 服务器\n4. 统计投票。每次投票后，服务器统计投票信息，判断是都有过半机器接收到相同的投票信息。server1、server2 都统计出集群中有两台机器接受了（2,0）的投票信息，此时已经选出了 server2 为 leader 节点。\n5. 改变服务器状态。一旦确定了 leader，每个服务器响应更新自己的状态，如果是 follower，那么就变更为 FOLLOWING，如果是 Leader，变更为 LEADING。此时 server3继续启动，直接加入变更自己为 FOLLOWING。\n\n![](../img/消息队列/zookeeper/选取流程.png)\n## 2、运行过程中的 leader 选举\n当集群中 leader 服务器出现宕机或者不可用情况时，整个集群无法对外提供服务，进入新一轮的 leader 选举。\n\n1. 变更状态。leader 挂后，其他非 Oberver服务器将自身服务器状态变更为 LOOKING。\n2. 每个 server 发出一个投票。在运行期间，每个服务器上 zxid 可能不同。\n3. 处理投票。规则同启动过程。\n4. 统计投票。与启动过程相同。\n5. 改变服务器状态。与启动过程相同。\n\n# 常见的问题\n## 什么是Zookeeper？它的作用是什么？\nZookeeper是一个开源的分布式协调服务，用于管理大型分布式系统中的配置信息、命名服务、分布式锁、分布式队列和领导者选举等功能。\n\nZookeeper主要用于协调和管理分布式系统中的各种资源和服务，它提供了一个高可用的、可靠的、高性能的集中式服务，用于解决分布式系统中的一致性问题。它可以让分布式应用程序中的各个节点通过同步的方式访问共享数据，从而保证了数据的一致性和可靠性。\n\nZookeeper的主要作用包括：\n\n1. 配置管理：Zookeeper可以用于集中管理分布式应用程序的配置信息，这样可以方便地对配置进行修改和更新。\n2. 命名服务：Zookeeper可以用于注册和查找分布式系统中的各种服务和节点，例如分布式数据库、Web服务器、消息队列等。\n3. 分布式锁：Zookeeper可以用于实现分布式锁，保证分布式系统中的各个节点能够按照一定的顺序访问共享资源，从而避免竞争和冲突。\n4. 分布式队列：Zookeeper可以用于实现分布式队列，用于处理分布式系统中的消息传递和任务调度。\n5. 领导者选举：Zookeeper可以用于实现领导者选举，确保在分布式系统中只有一个节点成为领导者，从而保证系统的稳定性和可靠性。\n\n## Zookeeper是如何实现数据的一致性和可靠性的？\nZookeeper是如何实现数据的一致性和可靠性的呢？主要有以下几个方面：\n\n- 事务日志：Zookeeper会将每一次写操作记录在本地的事务日志中，以保证数据的可靠性。如果一台服务器崩溃或者网络发生故障，Zookeeper可以通过回放事务日志来恢复数据的一致性。\n- 临时节点：Zookeeper中的临时节点只有在创建它的客户端连接断开时才会被删除，这样可以确保分布式系统中的各个节点能够及时发现其他节点的故障情况，从而实现数据的一致性。\n- 选择合适的数据模型：Zookeeper的数据模型非常简单，仅仅是一个层级结构的命名空间，但是却可以通过这种简单的模型来实现复杂的分布式应用程序。例如，可以通过Zookeeper的数据模型来实现分布式锁、领导者选举等功能。\n- Watcher机制：Zookeeper的Watcher机制可以让客户端在节点的状态发生变化时得到通知，从而及时更新数据，保证数据的一致性。\n- 集群节点的角色：Zookeeper集群中的每个节点都有特定的角色，例如Leader节点、Follower节点等。Leader节点负责处理所有的更新操作，而Follower节点则负责复制Leader节点的数据。这样可以保证数据的可靠性和一致性。\n\n## Zookeeper中的watcher是什么？如何使用watcher机制实现分布式锁？\nZookeeper中的Watcher是一种事件通知机制，用于实现分布式应用程序中的数据监听和触发机制。Watcher机制允许客户端在节点状态发生变化时得到通知，从而可以及时更新数据，保证数据的一致性。\n\n在Zookeeper中，客户端可以在对某个Znode节点进行操作时注册一个Watcher，当该节点的状态发生变化时，Zookeeper会向客户端发送一个事件通知，客户端可以在收到通知后进行相应的处理。\n\n利用Watcher机制，可以实现分布式锁。实现步骤如下：\n\n1. 在Zookeeper上创建一个Znode节点，作为锁的节点。\n2. 客户端需要获取锁时，先在锁节点下创建一个临时有序节点，并获取该节点的名称。\n3. 客户端检查是否自己创建的节点是当前锁节点下的最小序号节点，如果是，则获得了锁。\n4. 如果不是，客户端就监听它前面的节点，等待它的前一个节点被删除（即前一个客户端释放了锁）。\n5. 当前客户端收到Watcher通知后，重复步骤3和4，直到获取到锁。\n6. 客户端释放锁时，删除自己创建的节点即可。\n\n通过上述步骤，客户端就可以利用Zookeeper的Watcher机制实现分布式锁。这种方式不仅能够确保锁的可靠性，还能够避免因为网络等问题导致的死锁问题，具有很好的可靠性和性能表现。\n\n## Zookeeper的性能瓶颈在哪里？如何优化Zookeeper的性能？\nZookeeper的性能瓶颈主要有以下几个方面：\n\n1. 磁盘IO：Zookeeper需要频繁地进行磁盘IO操作，包括写入和读取数据。当集群规模增大时，磁盘IO会成为性能瓶颈。\n2. 网络IO：Zookeeper集群中的各个节点需要频繁地进行通信，包括同步数据和处理请求等操作。网络IO的延迟和带宽瓶颈也会影响Zookeeper的性能。\n3. CPU：Zookeeper需要进行复杂的计算和协调工作，包括选举Leader节点、同步数据等操作，这些操作会占用大量的CPU资源。\n\n针对上述性能瓶颈，可以采取以下几种方式来优化Zookeeper的性能：\n\n1. 增加硬件资源：可以增加磁盘、网络和CPU等硬件资源，以提高Zookeeper的性能。\n2. 分离磁盘：可以将Zookeeper的数据和事务日志存储在不同的磁盘上，以减少磁盘IO的竞争和延迟。\n3. 使用SSD：可以使用SSD代替传统的机械硬盘，以提高磁盘IO的性能。\n4. 调整Zookeeper的参数：可以根据具体的场景和需求调整Zookeeper的参数，包括tickTime、syncLimit、initLimit、maxClientCnxns等。\n5. 使用集群缓存：可以使用集群缓存技术，如Memcached或Redis等，将Zookeeper中的热点数据存储在内存中，以减少磁盘IO的压力。\n6. 使用异步请求：可以使用异步请求的方式来减少网络IO的延迟，提高请求的吞吐量。\n\n## 如何在Zookeeper集群中进行数据的备份和恢复？\n在Zookeeper集群中进行数据备份和恢复是非常重要的，因为Zookeeper存储的数据对于整个分布式应用都非常关键。一旦出现数据丢失或损坏，可能会导致应用无法正常运行。因此，备份和恢复是Zookeeper管理和运维的重要组成部分。\n\n以下是在Zookeeper集群中进行数据备份和恢复的一般步骤：\n\n1. 数据备份\nZookeeper的数据备份通常使用快照（snapshot）机制。快照是Zookeeper数据的一个镜像，可以在恢复数据时使用。通过执行如下命令可以进行快照备份：\n```bash\nbin/zkCli.sh -server server1:2181 get / -d > snapshot.out\n\n```\n其中，server1:2181是Zookeeper集群中的任意一个节点的地址。/表示需要备份的节点路径，-d选项表示以递归方式备份该节点下的所有子节点。\n\n快照备份的缺点是它只能备份数据到快照生成时的状态，不能备份在快照生成后的新数据。因此，为了实现更全面的备份，可以结合使用事务日志（transaction log）进行备份。\n\n事务日志包含了Zookeeper集群中的所有数据更新，因此可以使用它来恢复数据。事务日志备份的命令如下：\n```bash\ncp -r /var/lib/zookeeper/data/version-2/ /path/to/backup/\n```\n其中，/var/lib/zookeeper/data/version-2/是Zookeeper数据目录，/path/to/backup/是备份目录。\n2. 数据恢复\n   恢复数据的过程是将备份的数据替换掉当前的数据。首先需要将所有节点停止，然后将备份数据拷贝到Zookeeper数据目录中，并重启所有节点。\n\n在使用快照进行数据恢复时，可以使用load命令将快照文件导入到Zookeeper中：\n```bash\nbin/zkCli.sh -server server1:2181 load /path/to/snapshot.out\n```\n在使用事务日志进行数据恢复时，只需将备份的数据替换掉当前的数据即可。\n\n需要注意的是，如果Zookeeper集群正在运行，不能直接覆盖数据目录中的数据，否则可能会导致数据丢失。因此，在进行数据恢复时，最好将集群中的所有节点都停止，然后再进行恢复操作。此外，需要确保备份的数据与当前集群的版本兼容。如果备份的数据与当前版本不兼容，可能需要先将集群升级到备份数据的版本，然后再进行恢复。\n# 参考文章\n- https://zhuanlan.zhihu.com/p/363323489\n- https://www.runoob.com/w3cnote/zookeeper-leader.html\n- https://chat.openai.com/"
  },
  {
    "path": "消息队列/mq常见面试题.md",
    "content": "\n* [常见面试题](#常见面试题)\n    * [什么是消息队列](#什么是消息队列)\n    * [为什么要使用消息队列](#为什么要使用消息队列)\n    * [如何保证消息队列高可用](#如何保证消息队列高可用)\n    * [如何保证消息队列不被重复消费（幂等性）](#如何保证消息队列不被重复消费幂等性)\n    * [如何保证消息的可靠传输](#如何保证消息的可靠传输)\n        * [生产者丢数据](#生产者丢数据)\n        * [MQ丢数据](#mq丢数据)\n        * [消费者丢数据](#消费者丢数据)\n    * [如何保证消息的顺序性](#如何保证消息的顺序性)\n    * [如何处理消息堆积](#如何处理消息堆积)\n        * [方法1](#方法1)\n        * [方法2](#方法2)\n    * [mq 中的消息过期失效了](#mq-中的消息过期失效了)\n\n\n# 常见面试题\n## 什么是消息队列\n消息队列（Message Queue）是在消息的传输过程中保存消息的容器，是应用间的通信方式。消息发 送后可以立即返回，由消息系统保证消息的可靠传输，消息发布者只管把消息写到队列里面而不用考虑 谁需要消息，而消息的使用者也不需要知道谁发布的消息，只管到消息队列里面取，这样生产和消费便 可以做到分离。\n## 为什么要使用消息队列\n优点：\n- 异步处理：例如短信通知、终端状态推送、App推送、用户注册等\n- 数据同步：业务数据推送同步\n- 重试补偿：记账失败重试\n- 系统解耦：通讯上下行、终端异常监控、分布式事件中心\n- 流量消峰：秒杀场景下的下单处理\n- 发布订阅：HSF的服务状态变化通知、分布式事件中心\n- 高并发缓冲：日志服务、监控上报\n\n缺点：\n- 系统可用性降低 系统引入的外部依赖越多，越容易挂掉？如何保证消息队列的高可用？\n- 系统复杂度提高 怎么保证消息没有重复消费？怎么处理消息丢失的情况？怎么保证消息传递的顺序性？\n- 一致性问题 A 系统处理完了直接返回成功了，人都以为你这个请求就成功了；但是问题是，要是BCD 三个系统那里，BD 两个系统写库成功了，结果 C 系统写库失败了，咋整？你这数据就不一致了。\n## 如何保证消息队列高可用\nmq的集群模式\n## 如何保证消息队列不被重复消费（幂等性）\n消息队列中，为了保证消息不被重复消费，需要考虑实现幂等性。下面是一些实现幂等性的方式：\n\n1. 消费端在处理消息时，记录已处理的消息ID，防止重复消费。\n2. 消费端在处理消息时，记录已处理的消息内容的Hash值，当重复消费时，判断Hash值是否相等。\n3. 消费端使用唯一标识符作为消息处理的ID，保证同一个消息只被处理一次。\n4. 生产者在发送消息时，给消息赋予全局唯一的ID，消费端在处理消息时，通过ID来判断是否已经消费。这个值可以放在Redis，或者使用数据库主键\n5. 消费端使用分布式锁来控制并发消费，确保同一时刻只有一个消费者消费同一条消息。\n6. 在消息的元数据中添加标记，标记消息是否已经被消费，消费端在消费时，首先判断元数据的标记是否为已消费。\n\n需要注意的是，不同的幂等性实现方式适用于不同的业务场景，选择适合自己的方式需要考虑实际情况和业务需求。同时，在实现幂等性的同时，需要考虑性能和并发性的问题。\n## 如何保证消息的可靠传输\n\n在消息队列中，为了保证消息的可靠传输，需要考虑以下几点：\n\n- 消息的持久化：将消息持久化到磁盘或数据库中，确保即使在服务异常或宕机的情况下，消息不会丢失。\n- 消息的确认机制：生产者在发送消息后，需要等待消息被正确处理或持久化后，才能发送确认消息给生产者。这样可以确保消息被正确传输，避免消息丢失或重复消费。\n- 消息的重试机制：当消息未能被正确处理时，可以根据错误原因和类型进行一定的重试机制，例如延迟重试或指数级退避等策略，确保消息最终被正确处理。\n- 高可用性：消息队列需要考虑高可用性，使用主备、集群等机制，确保即使在节点宕机的情况下，消息队列仍然可以继续工作。\n- 流量控制和负载均衡：在高并发场景下，需要进行流量控制和负载均衡，以避免过多的消息导致消息队列的性能下降。\n- 监控和告警：需要对消息队列进行监控和告警，及时发现问题并进行处理，以保证消息队列的可靠性和稳定性。\n\n综上所述，为了保证消息的可靠传输，需要综合考虑消息队列的多个方面，包括持久化、确认机制、重试机制、高可用性、流量控制和负载均衡、监控和告警等。\n\n### 生产者丢数据\nrabbitmq的confirm模式\n\n在生产者那里设置开启 confirm 模式之后，你每次写的消息都会分配一个唯一的 id，然后如果写入了 RabbitMQ 中，RabbitMQ 会给你回传一个 ack 消息，告诉你说这个消息 ok 了。如果 RabbitMQ 没能处理这个消\n息，会回调你的一个 nack 接口，告诉你这个消息接收失败，你可以重试。而且你可以结合这个机制自\n己在内存里维护每个消息 id 的状态，如果超过一定时间还没接收到这个消息的回调，那么你可以重\n发。\n\n### MQ丢数据\n设置持久化\n\n就是 RabbitMQ 自己弄丢了数据，这个你必须开启 RabbitMQ 的持久化，就是消息写入之后会持久化\n到磁盘，哪怕是 RabbitMQ 自己挂了，恢复之后会自动读取之前存储的数据，一般数据不会丢。除非极\n其罕见的是，RabbitMQ 还没持久化，自己就挂了，可能导致少量数据丢失，但是这个概率较小。\n\n设置持久化有两个步骤：\n- 创建 queue 的时候将其设置为持久化 这样就可以保证 RabbitMQ 持久化 queue 的元数据，但是它是不会持久化 queue 里的数据的。\n- 第二个是发送消息的时候将消息的 deliveryMode 设置为 2 就是将消息设置为持久化的，此时RabbitMQ 就会将消息持久化到磁盘上去。\n\n必须要同时设置这两个持久化才行，RabbitMQ 哪怕是挂了，再次重启，也会从磁盘上重启恢复queue，恢复这个 queue 里的数据。\n\n注意，哪怕是你给 RabbitMQ 开启了持久化机制，也有一种可能，就是这个消息写到了 RabbitMQ中，但是还没来得及持久化到磁盘上，结果不巧，此时 RabbitMQ 挂了，就会导致内存里的一点点数据丢失。\n\n所以，持久化可以跟生产者那边的 confirm 机制配合起来，只有消息被持久化到磁盘之后，才会通知生产者 ack 了，所以哪怕是在持久化到磁盘之前，RabbitMQ 挂了，数据丢了，生产者收不到 ack ，你也是可以自己重发的。\n\n### 消费者丢数据\n关闭自动ack，手动ack\n## 如何保证消息的顺序性\n在生产中经常会有一些类似报表系统这样的系统，需要做 MySQL 的 binlog 同步。比如订单系统要同步订单表的数据到大数据部门的 MySQL 库中用于报表统计分析，通常的做法是基于 Canal 这样的中间件去监听订单数据库的 binlog，然后把这些 binlog 发送到 MQ 中，再由消费者从 MQ 中获取 binlog 落地到大数据部门的 MySQL 中。\n\n在这个过程中，可能会有对某个订单的增删改操作，比如有三条 binlog 执行顺序是增加、修改、删除；消费者愣是换了顺序给执行成删除、修改、增加，这样能行吗？肯定是不行的\n\n### RabbitMQ 消息顺序错乱\n对于 RabbitMQ 来说，导致上面顺序错乱的原因通常是消费者是集群部署，不同的消费者消费到了同一订单的不同的消息，如消费者 A 执行了增加，消费者 B 执行了修改，消费者 C 执行了删除，但是消费者 C 执行比消费者 B 快，消费者 B 又比消费者 A 快，就会导致消费 binlog 执行到数据库的时候顺序错乱，本该顺序是增加、修改、删除，变成了删除、修改、增加。\n\n如下图是 RabbitMQ 可能出现顺序错乱的问题示意图：\n\n![](../img/消息队列/消息顺序性/RabbitMQ消息顺序错乱.png)\n\n### Kafka 消息顺序错乱\n对于 Kafka 来说，一个 topic 下同一个 partition 中的消息肯定是有序的，生产者在写的时候可以指定一个 key，通过我们会用订单号作为 key，这个 key 对应的消息都会发送到同一个 partition 中，所以消费者消费到的消息也一定是有序的。\n\n那么为什么 Kafka 还会存在消息错乱的问题呢？问题就出在消费者身上。通常我们消费到同一个 key 的多条消息后，会使用多线程技术去并发处理来提高消息处理速度，否则一条消息的处理需要耗时几十 ms，1 秒也就只能处理几十条消息，吞吐量就太低了。而多线程并发处理的话，binlog 执行到数据库的时候就不一定还是原来的顺序了。\n\n如下图是 Kafka 可能出现乱序现象的示意图：\n\n![](../img/消息队列/消息顺序性/Kafka消息顺序错乱.png)\n\n### RocketMQ 消息顺序错乱\n对于 RocketMQ 来说，每个 Topic 可以指定多个 MessageQueue，当我们写入消息的时候，会把消息均匀地分发到不同的 MessageQueue 中，比如同一个订单号的消息，增加 binlog 写入到 MessageQueue1 中，修改 binlog 写入到 MessageQueue2 中，删除 binlog 写入到 MessageQueue3 中。\n\n但是当消费者有多台机器的时候，会组成一个 Consumer Group，Consumer Group 中的每台机器都会负责消费一部分 MessageQueue 的消息，所以可能消费者 A 消费了 MessageQueue1 的消息执行增加操作，消费者 B 消费了 MessageQueue2 的消息执行修改操作，消费者 C 消费了 MessageQueue3 的消息执行删除操作，但是此时消费 binlog 执行到数据库的时候就不一定是消费者 A 先执行了，有可能消费者 C 先执行删除操作，因为几台消费者是并行执行，是不能够保证他们之间的执行顺序的。\n\n如下图是 RocketMQ 可能出现乱序现象的示意图：\n\n![](../img/消息队列/消息顺序性/RocketMQ消息顺序错乱.png)\n\n### 如何保证消息的顺序性？\n知道了为什么会出现顺序错乱之后，就要想办法保证消息的顺序性了。从前面可以知道，顺序错乱要么是由于多个消费者消费到了同一个订单号的不同消息，要么是由于同一个订单号的消息分发到了 MQ 中的不同机器中。不同的消息队列保证消息顺序性的方案也各不相同。\n\n### RabbitMQ 保证消息的顺序性\nRabbitMQ 的问题是由于不同的消息都发送到了同一个 queue 中，多个消费者都消费同一个 queue 的消息。解决这个问题，我们可以给 RabbitMQ 创建多个 queue，每个消费者固定消费一个 queue 的消息，生产者发送消息的时候，同一个订单号的消息发送到同一个 queue 中，由于同一个 queue 的消息是一定会保证有序的，那么同一个订单号的消息就只会被一个消费者顺序消费，从而保证了消息的顺序性。\n\n如下图是 RabbitMQ 保证消息顺序性的方案：\n\n![](../img/消息队列/消息顺序性/RabbitMQ保证消息的顺序性.png)\n\n### Kafka 保证消息的顺序性\nKafka 从生产者到消费者消费消息这一整个过程其实都是可以保证有序的，导致最终乱序是由于消费者端需要使用多线程并发处理消息来提高吞吐量，比如消费者消费到了消息以后，开启 32 个线程处理消息，每个线程线程处理消息的快慢是不一致的，所以才会导致最终消息有可能不一致。\n\n所以对于 Kafka 的消息顺序性保证，其实我们只需要保证同一个订单号的消息只被同一个线程处理的就可以了。由此我们可以在线程处理前增加个内存队列，每个线程只负责处理其中一个内存队列的消息，同一个订单号的消息发送到同一个内存队列中即可。\n\n如下图是 Kafka 保证消息顺序性的方案：\n\n![](../img/消息队列/消息顺序性/Kafka保证消息的顺序性.png)\n\n### RocketMQ 保证消息的顺序性\nRocketMQ 的消息乱序是由于同一个订单号的 binlog 进入了不同的 MessageQueue，进而导致一个订单的 binlog 被不同机器上的 Consumer 处理。\n\n要解决 RocketMQ 的乱序问题，我们只需要想办法让同一个订单的 binlog 进入到同一个 MessageQueue 中就可以了。因为同一个 MessageQueue 内的消息是一定有序的，一个 MessageQueue 中的消息只能交给一个 Consumer 来进行处理，所以 Consumer 消费的时候就一定会是有序的。\n\n如下图是 RocketMQ 保证消息顺序性的方案：\n\n![](../img/消息队列/消息顺序性/RocketMQ保证消息的顺序性.png)\n\n## 如何处理消息堆积\n### 方法1\n先修复 consumer 的问题，确保其恢复消费速度，然后将现有 consumer 都停掉\n\n新建一个 topic，partition 是原来的 10 倍，临时建立好原先 10 倍的 queue 数量\n\n然后写一个临时的分发数据的 consumer 程序，这个程序部署上去消费积压的数据，消费之后不做耗时的处理，直接均匀轮询写入临时建立好的 10 倍数量的 queue\n\n接着临时征用 10 倍的机器来部署 consumer，每一批 consumer 消费一个临时 queue 的数据。\n\n这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍，以正常的 10 倍速度来消费数据。等快速消费完积压数据之后，得恢复原先部署的架构，重新用原先的 consumer 机器来消费消息。\n### 方法2\n方法1对于消息不会过期比较好用，但是如果消息过期会被清理，则需要先快速消费数据，比如先把积压的订单先全部消费，但是不处理后面的逻辑，单纯的从消息队列拿出来放在MySQL里，在另外一个时间在批量的读取出来消费处理后面的逻辑\n## mq 中的消息过期失效了\n假设你用的是 RabbitMQ，RabbtiMQ 是可以设置过期时间的，也就是 TTL。如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉，这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在 mq 里，而是大量的数据会直接搞丢。这个情况下，就不是说要增加 consumer 消费积压的消息，因为实际上没啥积压，而是丢了大量的消息。\n\n我们可以采取一个方案，就是批量重导，这个我们之前线上也有类似的场景干过。就是大量积压的时候，我们当时就直接丢弃数据了，然后等过了高峰期以后，比如大家一起喝咖啡熬夜到晚上12点以后，用户都睡觉了。这个时候我们就开始写程序，将丢失的那批数据，写个临时程序，一点一点的查出来，然后重新灌入 mq 里面去，把白天丢的数据给他补回来。也只能是这样了。\n\n假设 1 万个订单积压在 mq 里面，没有处理，其中 1000 个订单都丢了，你只能手动写程序把那 1000个订单给查出来，手动发到 mq 里去再补一次。\n\n\n# 参考文章\n- https://xie.infoq.cn/article/c84491a814f99c7b9965732b1"
  },
  {
    "path": "职业规划和学习习惯/职业规划和学习习惯.md",
    "content": "\n* [职业规划和学习习惯](#职业规划和学习习惯)\n    * [项目遇到的问题](#项目遇到的问题)\n    * [职业规划](#职业规划)\n    * [平时规划](#平时规划)\n        * [看什么书](#看什么书)\n        * [怎么学习](#怎么学习)\n            * [学习的途径](#学习的途径)\n            * [怎么学习新技术](#怎么学习新技术)\n\n\n# 职业规划和学习习惯\n## 项目遇到的问题\n- 最大的问题、挑战\n- 怎么解决\n## 职业规划\n- 自己的优缺点\n- 未来3-5年的规划\n## 平时规划\n### 看什么书\n- 深入理解Java虚拟机第3版-周志明\n- Java多线编程核心技术\n- 高性能MySQL\n- Redis实战\n- netty权威指南\n- 算法导论\n### 怎么学习\n#### 学习的途径\n- 看面经\n- 牛客\n- LeetCode\n- 掘金社区\n- CSDN\n- segmentfault\n- GitHub\n- gitee\n- infoQ\n#### 怎么学习新技术\n一般是看官网，然后看案例博客，然后直接上手写个demo"
  },
  {
    "path": "计算机网络/CDN.md",
    "content": "\n* [<a href=\"#\">什么是CDN</a>](#什么是cdn)\n* [<a href=\"#\">好处</a>](#好处)\n\n\n# [什么是CDN](#)\n- 内容分发网络（CDN）是指一组分布在不同地理位置的服务器，协同工作以提供互联网内容的快速交付。\n- CDN 允许快速传输加载互联网内容所需的资源，包括 HTML 页面、javascript 文件、样式表、图像和视频。 CDN 服务已得到不断普及。如今，大多数 web 流量都通过 CDN 提供服务，包括来自Facebook、Netflix 和亚马逊等主要网站的流量。\n# [好处](#)\n- 缩短网站加载时间 – 通过将内容分发到访问者附近的 CDN 服务器（以及其他优化措施），访问者体验到更快的页面加载时间。由于访问者更倾向于离开加载缓慢的网站，CDN 可以降低跳出率并增加人们在该网站上停留的时间。换句话说，网站速度越快，用户停留的时间越长。\n- 减少带宽成本 – 网站托管的带宽消耗成本是网站的主要费用。通过缓存和其他优化，CDN 能够减少源服务器必须提供的数据量，从而降低网站所有者的托管成本。\n- 增加内容可用性和冗余 – 大流量或硬件故障可能会扰乱正常的网站功能。由于 CDN 具有分布式特性，因此与许多源服务器相比，CDN 可以处理更多流量并更好地承受硬件故障。\n- 改善网站安全性 – CDN 可以通过提供 DDoS 缓解、安全证书的改进以及其他优化措施来提高安全性。"
  },
  {
    "path": "计算机网络/HTTP.md",
    "content": "\n# 特点\n\n无状态\n\n# 方法\n\n## get\n\n获取资源 \n\n## head\n\n获取报文首部\n\n## post\n\n传输实体主体\n\n## put\n\n- 上传文件\n\n- 修改资源完全替换\n  \n  ## patch\n  \n  修改部分资源\n  \n  ## delete\n  \n  删掉文件\n  \n  ## options\n  \n  查询指定的URL支持的方法\n\n- 返回 Allow: GET, POST, HEAD, OPTIONS 这样的内容\n  \n  ## connect\n  \n  使用SSL、TLS把通讯内容加密后通过隧道传输\n  \n  ## trace\n  \n  服务器会把通讯路径返回给客户端\n\n# 状态码\n\n## 1XX\n\n信息性状态码\n接收的请求正在处理\n\n- 100 已经被部分接收\n\n- 101 切换协议\n  \n  ## 2XX\n  \n  成功状态码\n\n- 200 请求的正常处理\n  \n  ## 3XX\n  \n  重定向状态码\n  需要进行附加操作以完成处理\n\n- 302\n  临时重定向\n  \n  ## 4XX\n  \n  客户端错误码\n\n- 400 bad request 语法错误\n\n- 404 资源未找到\n\n- 405 不允许使用该方法\n  \n  ## 5XX\n  \n  服务端错误码\n  服务器处理发生错误\n\n- 500 服务器错误\n\n- 502 充当网关或代理的服务器，从远端服务器接收到了一个无效的请求\n\n- 503 服务不可用\n\n- 504 网关超时\n\n# HTTPS\n\n## 什么是HTTPS\n\n- HTTP协议运行在TCP之上，所有传输的内容都是明文，客户端和服务器端都无法验证对方的身份。HTTPS是运行在SSL/TLS之上的HTTP协议，SSL/TLS 运行在TCP之上。所有传输的内容都经过加密，加密采用对称加密，但对称加密的密钥用服务器方的证书进行了非对称加密。所以说，HTTP 安全性没有 HTTPS高，但是 HTTPS 比HTTP耗费更多服务器资源。\n\n- 所谓HTTPS，其实就是身披SSL协议这层外壳的HTTP\n  \n  - ![](../img/计算机网络/https.png)            \n  \n  - 在采用SSL后，HTTP就拥有了HTTPS的加密、证书和完整性保护这些功能。也就是说HTTP加上加密处理和认证以及完整性保护后即是HTTPS。\n    \n    ## 端口\n  \n  - HTTP的URL由“http://”起始且默认使用端口80\n  \n  - HTTPS的URL由“https://”起始且默认使用端口443\n    \n    ## https解决的问题\n    \n    ### 解决内容可能被窃听的问题——加密\n\n- 对称加密+非对称加密\n  \n  - 对称加密\n    - 密钥只有一个，加密解密为同一个密码，且加解密速度快，典型的对称加密算法有DES、AES等；\n      - 服务器和客户端拥有相同的密钥\n      - 客户端使用密钥加密消息后传给服务器，服务器收到后用密钥解密\n      - 但是需要先传递密钥，此时可能被截获\n  - 非对称加密\n    - 密钥成对出现（且根据公钥无法推知私钥，根据私钥也无法推知公钥），加密解密使用不同密钥（公钥加密需要私钥解密，私钥加密需要公钥解密），相对对称加密速度较慢，典型的非对称加密算法有RSA、DSA等。\n      - 公钥加密私钥解密\n\n- 使用对称密钥的好处是解密的效率比较快，使用非对称密钥的好处是可以使得传输的内容不能被破解，因为就算你拦截到了数据，但是没有对应的私钥，也是不能破解内容的。就比如说你抢到了一个保险柜，但是没有保险柜的钥匙也不能打开保险柜。那我们就将对称加密与非对称加密结合起来,充分利用两者各自的优势，在交换密钥环节使用非对称加密方式，之后的建立通信交换报文阶段则使用对称加密方式。\n  \n  - 具体做法是：发送密文的一方使用对方的公钥进行加密处理“对称的密钥”，然后对方用自己的私钥解密拿到“对称的密钥”，这样可以确保交换的密钥是安全的前提下，使用对称加密方式进行通信。所以，HTTPS采用对称加密和非对称加密两者并用的混合加密机制\n    \n    ### 解决报文可能遭篡改问题——数字签名\n\n- 数字签名有两种功效：\n  \n  - 能确定消息确实是由发送方签名并发出来的，因为别人假冒不了发送方的签名。\n  - 数字签名能确定消息的完整性,证明数据是否未被篡改过。\n\n- 数字签名如何生成:\n  \n  - 将一段文本先用Hash函数生成消息摘要，然后用发送者的私钥加密生成数字签名，与原文文一起传送给接收者\n    \n    - 1、hash(文本)->消息摘要\n    - 2、使用发送者的私钥加密->数字签名\n  \n  - 校验数字签名流程：\n    \n    - 接受者使用发送者的公钥解密数字签名->消息摘要\n    \n    - 用hash函数对原文再生成一个摘要信息->对比解密的消息摘要，判断是否相同\n      \n      ### 解决通信方身份可能被伪装的问题——数字证书\n      \n      A----------->B\n\n- A和B通信\n\nA-------x--C--x------>B\n\n- 如果消息被C截获，并且C用自己的私钥加密，再传递给B，并且也传递了自己的公钥，B验证后发现没问题回传了信息给C，但是消息已经被截获了 \n  \n  ## HTTPS加密过程\n1. 客户端首先请求服务器\n2. 服务器返回CA证书，包含网站信息和公钥A\n- 什么是CA证书\n  - 由CA机构申领一份数字证书\n    - 这种机构没几个而且要钱\n    - 证书的内容\n      - 电子签证机关的信息、公钥用户信息、公钥、权威机构的签字和有效期等等\n  - CA机构生成数字签名\n    - CA机构拥有非对称加密的私钥和公钥。\n    - CA机构对证书明文数据T进行hash。\n    - 对hash后的值用私钥加密，得到数字签名S。\n  - 明文和数字签名共同组成了数字证书，这样一份数字证书就可以颁发给网站了。\n3. 客户端解析证书判断是否合法\n- 客户端如何拿到数字证书认证机构的公开密钥？\n  - 多数浏览器开发商发布版本时，会事先在内部植入常用认证机关的公开密钥\n- 怎么判断证书的合法性\n  - 拿到证书，得到明文T，签名S。\n  - 用CA机构的公钥对S解密（由于是浏览器信任的机构，所以浏览器保有它的公钥。详情见下文），得到S’。\n  - 用证书里指明的hash算法对明文T进行hash得到T’。\n  - 显然通过以上步骤，T’应当等于S‘，除非明文或签名被篡改。所以此时比较S’是否等于T’，等于则表明证书可信。\n- 证书链\n  - 但事实上，证书的验证过程中还存在一个证书信任链的问题，因为我们向 CA 申请的证书一般不是根证书签发的，而是由中间证书签发的\n    - 比如百度的证书，从图你可以看到，证书的层级有三级\n      ![](../img/计算机网络/百度CA证书.png)\n    - 对于这种三级层级关系的证书的验证过程如下：\n      - 客户端收到 http://baidu.com 的证书后，发现这个证书的签发者不是根证书，就无法根据本地已有的根证书中的公钥去验证 http://baidu.com 证书是否可信。于是，客户端根据 http://baidu.com 证书中的签发者，找到该证书的颁发机构是 “GlobalSign Organization Validation CA - SHA256 - G2”，然后向 CA 请求该中间证书。\n      - 请求到证书后发现 “GlobalSign Organization Validation CA - SHA256 - G2” 证书是由 “GlobalSign Root CA” 签发的，由于 “GlobalSign Root CA” 没有再上级签发机构，说明它是根证书，也就是自签证书。应用软件会检查此证书有否已预载于根证书清单上，如果有，则可以利用根证书中的公钥去验证 “GlobalSign Organization Validation CA - SHA256 - G2” 证书，如果发现验证通过，就认为该中间证书是可信的。\n      - “GlobalSign Organization Validation CA - SHA256 - G2” 证书被信任后，可以使用 “GlobalSign Organization Validation CA - SHA256 - G2” 证书中的公钥去验证 http://baidu.com 证书的可信性，如果验证通过，就可以信任 http://baidu.com 证书。\n    - 总括来说，由于用户信任 GlobalSign，所以由 GlobalSign 所担保的 http://baidu.com 可以被信任，另外由于用户信任操作系统或浏览器的软件商，所以由软件商预载了根证书的 GlobalSign 都可被信任。\n  - 为什么需要证书链这么麻烦的流程？Root CA 为什么不直接颁发证书，而是要搞那么多中间层级呢？\n    - 这是为了确保根证书的绝对安全性，将根证书隔离地越严格越好，不然根证书如果失守了，那么整个信任链都会有问题。\n4. 取出公钥A生成随机码KEY\n5. 传递给服务器\n6. 服务器用私钥A解密得到随机码KEY\n7. 之后使用KEY加密传输数据\n\n# Https的CA证书放了什么，公钥放在CA里吗？\n\n- CA证书放了什么？\n  - 电子签证机关的信息、公钥用户信息、公钥、权威机构的签字和有效期等等\n- 公钥放在CA里吗？\n  - yes\n\n# CA证书是在客户端还是服务器\n\n客户端和服务器建立https通信的第一步是，客户端向服务器发起请求，服务器返回CA证书，所以证书是放在服务器，客户端也就是浏览器一般都会内置根证书列表，服务器返回的CA证书一般不会是根证书而是由中间商颁发的，但是可以寻到根证书机构，客户端先寻到根证书，和自己的信任列表对比，一层一层判断证书是否合法\n\n# HTTP1.1和HTTP1.0的主要区别\n\nHTTP1.0最早在网页中使用是在1996年，那个时候只是使用一些较为简单的网页上和网络请求上，而HTTP1.1则在1999年才开始广泛应用于现在的各大浏览器网络请求中，同时HTTP1.1也是当前使用最为广泛的HTTP协议。 主要区别主要体现在：\n\n- 1、长连接\n  - 在HTTP/1.0中，默认使用的是短连接，也就是说每次请求都要重新建立一次连接。HTTP 是基于TCP/IP协议的,每一次建立或者断开连接都需要三次握手四次挥手的开销，如果每次请求都要这样的话，开销会比较大。因此最好能维持一个长连接，可以用个长连接来发多个请求。HTTP 1.1起，默认使用长连接 ,默认开启Connection： keep-alive。 HTTP/1.1的持续连接有非流水线方式和流水线方式 。流水线方式是客户在收到HTTP的响应报文之前就能接着发送新的请求报文。与之相对应的非流水线方式是客户在收到前一个响应后才能发送下一个请求。\n- 2、错误状态响应码\n  - 在HTTP1.1中新增了24个错误状态响应码，如409（Conflict）表示请求的资源与资源的当前状态发生冲突；410（Gone）表示服务器上的某个资源被永久性的删除。\n- 3、缓存处理\n  - 在HTTP1.0中主要使用header里的If-Modified-Since,Expires来做为缓存判断的标准，HTTP1.1则引入了更多的缓存控制策略例如Entity tag，If-Unmodified-Since, If-Match, If-None-Match等更多可供选择的缓存头来控制缓存策略。\n- 4、带宽优化及网络连接的使用\n  - HTTP1.0中，存在一些浪费带宽的现象，例如客户端只是需要某个对象的一部分，而服务器却将整个对象送过来了，并且不支持断点续传功能，HTTP1.1则在请求头引入了range头域，它允许只请求资源的某个部分，即返回码是206（Partial Content），这样就方便了开发者自由的选择以便于充分利用带宽和连接。\n- Host头处理\n  - 在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址，因此，请求消息中的URL并没有传递主机名（hostname）。但随着虚拟主机技术的发展，在一台物理服务器上可以存在多个虚拟主机（Multi-homed Web Servers），并且它们共享一个IP地址。HTTP1.1的请求消息和响应消息都应支持Host头域，且请求消息中如果没有Host头域会报告一个错误（400 Bad Request）\n\n# HTTP2.0和HTTP1.x的区别\n\n- 新的二进制格式\n  - HTTP1.x的解析是基于文本。基于文本协议的格式解析存在天然缺陷，文本的表现形式有多样性，要做到健壮性考虑的场景必然很多，二进制则不同，只认0和1的组合。基于这种考虑HTTP2.0的协议解析决定采用二进制格式，实现方便且健壮。\n- 多路复用\n  - 即连接共享，即每一个request都是是用作连接共享机制的。一个request对应一个id，这样一个连接上可以有多个request，每个连接的request可以随机的混杂在一起，接收方可以根据request的 id将request再归属到各自不同的服务端请求里面。\n    - 不用响应后再能发起请求\n- header压缩\n  - 如上文中所言，对前面提到过HTTP1.x的header带有大量信息，而且每次都要重复发送，HTTP2.0使用encoder来减少需要传输的header大小，通讯双方各自cache一份header fields表，既避免了重复header的传输，又减小了需要传输的大小\n- 服务端推送\n  - HTTP2.0也具有server push功能\n\n# http的request和response格式\n\n## request\n\nHttp Request包括三部分：请求行（request-line），请求头（headers）和请求体（request-body）\n\n例如下面是一个POST请求：\n\n```html\n1. GET /search?hl=zh-CN&source=hp&q=domety&aq=f&oq= HTTP/1.1 \n2. Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-excel, application/vnd.ms-powerpoint, \n3. application/msword, application/x-silverlight, application/x-shockwave-flash, */*  \n4. Referer: <a href=\"http://www.google.cn/\">http://www.google.cn/</a>  \n5. Accept-Language: zh-cn  \n6. Accept-Encoding: gzip, deflate  \n7. User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; TheWorld)  \n8. Host: <a href=\"http://www.google.cn\">www.google.cn</a>  \n9. Connection: Keep-Alive  \n10. Cookie: PREF=ID=80a06da87be9ae3c:U=f7167333e2c3b714:NW=1:TM=1261551909:LM=1261551917:S=ybYcq2wpfefs4V9g; \n11.  \n12. NID=ojj8d-IygaEtSxLgaJmq&jid=jsaudu9hdha\n```\n\n- 第1行：请求行（request-line）\n- 第2-10行：请求头（headers）\n- 第11行：空行\n- 第12行：请求体（request-body）\n\nGET请求的参数数据不会放在请求体里，而是在url中,例如：\n`http://www.google.cn/search?hl=zh-CN&source=hp&q=domety&aq=f&oq=`\n\n## response\n\nHttp Response也包括三部分：响应状态行，响应头和响应体\n\n例子：\n\n```html\n1. HTTP/1.1 200 OK\n2. Date: Sat, 31 Dec 2005 23:59:59 GMT\n3. Content-Type: text/html;charset=ISO-8859-1\n4. Content-Length: 122\n5.\n6. ＜html＞\n7. ＜head＞\n8. ＜title＞Wrox Homepage＜/title＞\n9. ＜/head＞\n10. ＜body＞\n11. ＜!-- body goes here --＞\n12. ＜/body＞\n13. ＜/html＞\n```\n\n- 第1行：响应状态行，http版本和状态码\n\n- 第2-4行：键值对\n\n- 第5行：空行\n\n- 第6-13：响应体\n  \n  # 参考文章\n\n- https://www.cnblogs.com/biyeymyhjob/archive/2012/07/28/2612910.html\n\n- https://www.jianshu.com/p/0e593400d85b"
  },
  {
    "path": "计算机网络/HTTP面试题.md",
    "content": "# 在浏览器中输入url地址显示主页的过程\n\n- DNS解析\n  \n  - 域名到ip的映射\n  - 根域名 顶级域名 权威DNS服务器\n\n- TCP连接\n\n- 发送HTTP请求\n\n- 服务器处理请求并返回HTTP报文\n\n- 浏览器解析渲染页面\n\n- 连接结束\n  \n  # QPS和TPS的区别\n\n- TPS：Transactions Per Second（每秒传输的事物处理个数），即服务器每秒处理的事务数\n\n- QPS：Queries Per Second意思是“每秒查询率”，是一台服务器每秒能够相应的查询次数，是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准\n  \n  # 有哪些编码格式(GBK,UTF-8,ISO-)有没有想过为什么会有这么多的编码格式\n\n- 常见的编码格式\n  \n  - ASCII码\n    - 它能表示128个字符，其中包括英文字符、阿拉伯数字、西文字符以及32个控制字符\n  - 扩展的ASCII码\n    - 原基础上增加\n  - Unicode符号集\n    - 很多人都说Unicode编码，但其实Unicode是一个符号集（世界上所有符号的符号集\n  - UTF-8\n    - UTF-8就是在互联网上使用最广的一种unicode的实现方式\n    - 它可以使用1~4个字节表示一个符号，根据不同的符号而变化字节长度。\n  - GBK/GB2312/GB18030\n    - GBK和GB2312都是针对简体字的编码，只是GB2312只支持六千多个汉字的编码，而GBK支持1万多个汉字编码。而GB18030是用于繁体字的编码。汉字存储时都使用两个字节来储存。\n\n- 因为指定的标准的人不同，应用的场景不同，有时候这个时间满足了，但是未来不满足就得新增，这个地区的满足了，但是另外的地区不满足\n  \n  # 实现一个长URL转短URL\n\n- 利用放号器，初始值为0，对于每一个短链接生成请求，都递增放号器的值，再将此值转换为62进制（a-zA-Z0-9），比如第一次请求时放号器的值为0，对应62进制为a，第二次请求时放号器的值为1，对应62进制为b，第10001次请求时放号器的值为10000，对应62进制为sBc\n\n- 将短链接服务器域名与放号器的62进制值进行字符串连接，即为短链接的URL，比如：t.cn/sBc。\n\n- 重定向过程：生成短链接之后，需要存储短链接到长链接的映射关系，即sBc -> URL，浏览器访问短链接服务器时，根据URL Path取到原始的链接，然后进行302重定向。映射关系可使用K-V存储，比如Redis或Memcache\n  \n  - 一长对多短，设定上限max，达到后可以用缓存存关系用LRU淘汰最近未被使用的\n\n# OSI 7层协议，路由器工作在哪一层，交换机工作在哪一层\n\nOSI7层协议包括：应用层、表示层、会话层、传输层、网络层、数据链路层和物理层。\n\n路由器通常工作在网络层，也就是第3层，用于实现不同网络之间的通信和数据转发，通过寻址和路由选择等技术来实现数据包的转发和传输。路由器主要作用是对不同网络之间的数据进行转发和路由选择，以实现网络之间的通信。\n\n交换机通常工作在数据链路层，也就是第2层，用于实现局域网内部的数据交换和转发。交换机通过学习MAC地址来建立一个端口和MAC地址的映射表，实现对数据包的快速转发和过滤，避免广播风暴和网络拥塞。交换机主要作用是对同一个局域网内的数据进行快速转发和过滤，以实现局域网内部的通信。\n\n# NAT是干嘛的，为什么需要转换\n\nNAT（Network Address Translation，网络地址转换）是一种网络地址转换技术，通常用于将内部网络的私有IP地址转换为公共IP地址，以实现对外访问。\n\nNAT的主要作用是解决IPv4地址短缺问题，使得内部网络中的多个设备可以通过一个公共IP地址访问互联网。在一个企业或家庭网络中，往往会有多台设备共享一个公共IP地址，这些设备都使用内部网络的私有IP地址。当这些设备需要访问互联网时，NAT会将它们的IP地址转换为公共IP地址，并在内部网络和互联网之间进行转发和路由。\n\nNAT需要进行地址转换，主要是因为IPv4地址资源有限，无法为所有的设备都分配公共IP地址，而私有IP地址则可以在内部网络中自由使用。此外，NAT还可以提高网络的安全性，因为公共IP地址只有在经过NAT的转换后才能被内部网络访问，而互联网上的其他设备无法直接访问内部网络。\n\n# 子网掩码是干嘛用的\n\n子网掩码（subnet mask）是一种用于指定IP地址的网络地址部分和主机地址部分的掩码，通常与IP地址一起使用，用于划分网络和主机。\n\nIP地址由32位二进制数组成，通常使用点分十进制表示法，例如192.168.0.1。其中，前24位用于指定网络地址，后8位用于指定主机地址。子网掩码也是一个32位的二进制数，用于指示网络地址和主机地址的范围。\n\n通过子网掩码，可以将IP地址划分为不同的子网。例如，使用子网掩码255.255.255.0可以将IP地址192.168.0.1划分为网络地址192.168.0.0和主机地址1，同时也可以将IP地址192.168.0.2划分为相同的网络地址192.168.0.0和主机地址2，这样就可以将这两个IP地址划分为同一个子网中。\n\n子网掩码的主要作用是实现IP地址的划分和分类，使得网络管理更加灵活和高效。通过合理地划分IP地址，可以减少网络广播和冲突，提高网络的性能和可靠性。同时，子网掩码还可以用于实现网络安全控制，例如通过ACL（Access Control List）实现对不同子网的访问控制。\n\n# tcp整个流程，closed_wait状态?\n\n## 建立连接的过程？（三次握手）\n\nTCP三次握手是TCP协议中建立连接的重要过程，它涉及客户端和服务器的状态变化以及发送请求的具体内容。\n\n一开始，客户端和服务端都处于CLOSE状态。当服务端准备好接受连接时，它会进入LISTEN状态，等待客户端的连接请求。\n\n第一次握手时，客户端会初始化一个随机序号，并发送一个SYN请求连接报文给服务端，这个报文中包含了客户端的初始序列号。此时，客户端的状态变为SYN-SENT，等待服务端的确认。\n\n服务端收到SYN请求后，会进行第二次握手。服务端也会随机初始化一个序号，并将客户端的序列号加1作为确认应答报文ACK，同时发送自己的SYN报文。这样，服务端发送的就是一个SYN+ACK的报文，此时服务端的状态变为SYN-RCVD。\n\n客户端收到服务端的SYN+ACK报文后，会进行第三次握手。客户端会再次发送一个ACK报文给服务端，确认服务端的SYN报文，并将服务端的ACK序列号加1。此时，客户端的状态变为ESTABLISHED，表示连接已建立。服务端收到客户端的ACK报文后，状态也变为ESTABLISHED。\n\n这个三次握手的过程确保了客户端和服务端都能准确地知道对方已准备好进行数据传输，是TCP协议中保证数据可靠传输的重要机制。\n\n## 断开连接过程？（四次挥手）\n\n首先，我们要明确TCP连接是全双工的，这意味着数据可以在两个方向上同时传输。因此，当一方想要关闭连接时，它需要通知另一方，并确保双方的数据传输都已完成，然后才能完全关闭连接。这就是四次挥手的主要目的。\n\n第一次挥手是由客户端发起的。当客户端没有更多的数据要发送时，它会发送一个FIN报文段给服务端。这个FIN报文段的作用是告诉服务端：“我已经没有数据要发送了，你可以开始关闭你那边到我这边的连接了”。同时，客户端会进入FIN_WAIT_1状态，等待服务端的确认。\n\n第二次挥手发生在服务端收到FIN报文段后。服务端会发送一个ACK报文段给客户端，作为对FIN报文段的确认。这个ACK报文段的作用是告诉客户端：“我已经收到你的关闭请求了，我这边会准备关闭连接，但是在我发送完所有的数据之前，连接还不会完全关闭”。此时，服务端进入`CLOSE_WAIT`状态，等待所有数据发送完毕。而客户端在收到ACK后，会进入FIN_WAIT_2状态，等待服务端的FIN报文段。\n\n第三次挥手发生在服务端发送完所有数据后。此时，服务端会发送一个FIN报文段给客户端，告知客户端它也没有数据要发送了，可以关闭连接了。这个FIN报文段的作用是告诉客户端：“我这边也没有数据要发送了，我们可以关闭连接了”。服务端在发送FIN报文段后，会进入LAST_ACK状态，等待客户端的确认。\n\n第四次挥手是客户端在收到服务端的FIN报文段后发起的。客户端会发送一个ACK报文段给服务端，作为对FIN报文段的确认。这个ACK报文段的作用是告诉服务端：“我已经收到你的关闭请求了，我会关闭连接”。在发送完ACK报文段后，客户端会进入TIME_WAIT状态。这个状态会持续一段时间（通常是2MSL，即数据包在网络中的最大生存时间），以确保关闭请求和确认报文段能够被对方正确接收。如果在这段时间内没有收到对方的重新发送请求，那么客户端会最终关闭连接，进入CLOSED状态。而服务端在收到ACK报文段后，会立即关闭连接，进入CLOSED状态。\n\n这就是TCP四次挥手的全过程。通过这个过程，TCP连接能够在确保双方数据传输完成的前提下安全地关闭。这个过程的每一步都是必要的，以确保数据的完整性和可靠性。\n\n# http协议报文格式\n\n首先，HTTP请求报文主要由请求行、请求头部和请求体三个部分构成。请求行包含了请求的方法，比如GET或POST，请求的URL路径，以及HTTP协议的版本。紧接着请求行的是请求头部，它由多个键值对组成，每个键值对代表一种信息或设置，比如Host字段标明请求的目标主机，User-Agent字段描述发出请求的用户代理信息，Accept字段则列出客户端能够理解的内容类型。请求头部之后，可能跟随一个请求体，它通常用于POST或PUT请求中，包含客户端提交给服务器的数据。\n\n其次，HTTP响应报文的结构与请求报文相似，由状态行、响应头部和响应体组成。状态行包含HTTP协议版本、状态码以及状态描述信息。比如，状态码200表示请求成功，404则表示资源未找到。响应头部也包含多个键值对，提供有关响应的附加信息，如Content-Type描述响应体的内容类型，Content-Length指明响应体的长度。响应体则包含服务器返回给客户端的实际数据内容，如HTML文件、图片或其他媒体资源。\n\n# websocket升级流程\n\nWebSocket连接建立的过程：\n\n1. 客户端发起连接请求\n\n首先，客户端（如Web浏览器）会向服务器发起一个HTTP请求，意在建立一个WebSocket连接。这个HTTP请求不同于常规的请求，因为它包含了特殊的头信息，用以表明客户端希望将连接从HTTP升级到WebSocket。\n\n2. 包含特殊的头信息\n\n在HTTP请求头中，必须包含以下关键信息：\n\n- `Upgrade: websocket`：这个头部告诉服务器，客户端希望将连接升级到WebSocket协议。\n- `Connection: Upgrade`：此头部表示客户端要求服务器升级当前的HTTP连接到WebSocket连接。\n- `Sec-WebSocket-Key`：一个由客户端随机生成的密钥，用于服务器在响应中生成对应的`Sec-WebSocket-Accept`值，以证明它理解了WebSocket协议。\n- `Sec-WebSocket-Version`：支持的WebSocket协议版本，通常是13。\n\n可能还包括其他一些可选的头信息，如子协议、扩展等。\n\n3. 服务器响应并升级连接\n\n服务器在接收到这个特殊的HTTP请求后，会检查请求头中的信息，确认是否支持并愿意进行连接升级。如果一切正常，服务器会返回一个HTTP响应，状态码为`101 Switching Protocols`，表示服务器同意将连接从HTTP升级到WebSocket。\n\n服务器的响应中会包含以下头部信息：\n\n- `Upgrade: websocket`：确认连接已经升级到WebSocket。\n\n- `Connection: Upgrade`：表示连接已被升级。\n\n- `Sec-WebSocket-Accept`：根据客户端提供的`Sec-WebSocket-Key`计算得出的值，用于验证服务器的响应。\n4. 完成握手并建立WebSocket连接\n\n当客户端接收到服务器的`101 Switching Protocols`响应后，WebSocket握手过程就完成了。此时，连接已经从HTTP连接正式升级为WebSocket连接。\n\n5. 进行数据传输\n\n一旦WebSocket连接建立成功，客户端和服务器就可以通过这个连接进行双向的、全双工的数据传输了。这意味着客户端和服务器可以同时发送和接收消息。\n\n6. 关闭WebSocket连接\n\n当数据传输完成或者任何一方决定关闭连接时，会发送一个WebSocket关闭帧来通知对方关闭连接。这样，WebSocket连接就会被优雅地关闭。\n\n总的来说，WebSocket连接的建立过程是一个从HTTP协议升级到WebSocket协议的过程，它允许客户端和服务器之间进行实时、双向的通信。\n\n# http1.0，http1.1，http2以及http3的发展以及变化\n\nHTTP（超文本传输协议）是 Web 上数据传输的基础协议。随着网络技术的发展，HTTP 协议也经历了多个版本的迭代，每个版本在性能、安全性和功能上都有不同的改进。以下是 HTTP 1.0、HTTP 1.1、HTTP 2 和 HTTP 3 的发展及其变化概述：\n\n### 1. HTTP/1.0\n\n#### 发展背景\n- **发布**：1996年\n- **主要特点**：HTTP/1.0 是第一个广泛使用的 HTTP 版本，它为 Web 的早期发展奠定了基础。\n\n#### 关键特性\n- **请求-响应模型**：HTTP/1.0 使用简单的请求-响应模型。每个请求都需要一个独立的响应。\n- **无连接**：每个请求和响应都是独立的。服务器处理请求后，关闭连接，这意味着每次请求都需要重新建立连接。\n- **状态码**：引入了状态码的概念，用于表示请求的结果状态，如 200（OK）、404（Not Found）。\n- **内容类型**：支持 `Content-Type` 头，用于标识请求或响应的内容类型。\n\n#### 局限性\n- **性能瓶颈**：每个请求需要建立新的连接，造成了较大的延迟和额外的开销。\n\n### 2. HTTP/1.1\n\n#### 发展背景\n- **发布**：1999年\n- **主要特点**：HTTP/1.1 在 HTTP/1.0 的基础上做出了许多改进，以提高性能和功能。\n\n#### 关键特性\n- **持久连接**：引入了持久连接（Keep-Alive）机制，允许在一个连接上发送多个请求和响应，减少了连接建立的开销。\n- **管道化**：允许在一个连接上并行发送多个请求，但响应必须按照请求顺序返回（后续的请求必须等待前面的请求完成）。\n- **分块传输编码**：支持分块传输编码（Chunked Transfer Encoding），允许动态生成响应内容而不需要事先知道内容的长度。\n- **缓存控制**：引入了更多的缓存相关头部，如 `Cache-Control`，提供了更精细的缓存控制机制。\n- **增强的请求头**：支持更丰富的请求头和响应头，如 `Host` 头（允许多个虚拟主机共享同一个 IP 地址）。\n\n#### 局限性\n- **阻塞问题**：管道化虽然减少了连接数量，但仍然存在头部阻塞问题，即一个长时间的响应会阻塞后续请求的响应。\n\n### 3. HTTP/2\n\n#### 发展背景\n- **发布**：2015年\n- **主要特点**：HTTP/2 是对 HTTP/1.x 的重大改进，旨在提高 Web 性能和效率。\n\n#### 关键特性\n- **二进制协议**：HTTP/2 使用二进制格式而不是文本格式，这使得协议解析更高效。\n- **多路复用**：允许在一个连接上并行处理多个请求和响应，解决了 HTTP/1.x 的头部阻塞问题。每个请求和响应可以被分解成多个帧（Frame）进行传输。\n- **头部压缩**：使用 HPACK 算法对 HTTP 头部进行压缩，减少了冗余数据的传输量。\n- **流量控制**：支持流量控制和优先级，允许客户端和服务器更好地管理数据流量和优先级。\n- **服务器推送**：服务器可以主动推送资源到客户端，减少了客户端请求的延迟。\n\n#### 局限性\n- **复杂性**：尽管 HTTP/2 改进了许多性能问题，但其协议本身的复杂性增加了客户端和服务器的实现复杂度。\n\n### 4. HTTP/3\n\n#### 发展背景\n- **发布**：HTTP/3 的规范在 2021 年成为 RFC 9114，它基于 QUIC 协议（Quick UDP Internet Connections）。\n- **主要特点**：HTTP/3 旨在进一步提升性能，特别是在延迟和连接恢复方面。\n\n#### 关键特性\n- **基于 QUIC**：HTTP/3 使用 QUIC 协议，它是基于 UDP 的传输协议，相较于 TCP 更加高效和灵活。QUIC 提供了内置的加密、连接迁移和更低的连接建立延迟。\n- **零轮次握手**：QUIC 协议支持零轮次握手（0-RTT），使得在重复连接时可以更快速地恢复之前的会话。\n- **多路复用**：类似于 HTTP/2，HTTP/3 支持多路复用，但由于 QUIC 的内建机制，避免了 HTTP/2 的阻塞问题。\n- **改进的错误恢复**：QUIC 提供了更好的错误恢复和丢包恢复机制，减少了网络抖动对性能的影响。\n- **内置加密**：QUIC 协议内置了加密机制，所有 HTTP/3 流量都经过加密，提高了安全性。\n\n#### 局限性\n- **网络兼容性**：由于 HTTP/3 使用 UDP，因此需要网络设备和中间件支持 UDP，以确保兼容性。\n- **推广普及**：虽然 HTTP/3 提供了显著的性能改进，但它的推广和普及仍在进行中，需要时间来广泛部署。\n\n### 总结\n\n- **HTTP/1.0**：提供了基本的请求-响应模型，适用于早期 Web 的需求，但性能有限。\n- **HTTP/1.1**：引入了持久连接、管道化和缓存控制等特性，显著提升了性能，但仍存在阻塞问题。\n- **HTTP/2**：通过二进制协议、多路复用、头部压缩等机制，显著提高了性能和效率，但协议复杂度增加。\n- **HTTP/3**：基于 QUIC 协议，进一步改进了连接延迟、错误恢复和内置加密，提供了更高的性能和安全性，但在网络兼容性和普及方面仍在发展中。\n\n每个版本的 HTTP 协议都在前一个版本的基础上进行了改进，以适应不断变化的 Web 需求和技术环境。\n\n# 对称加密和非对称加密的原理\n**对称加密**和**非对称加密**是两种主要的加密技术，用于保护数据的安全性。它们的原理、特点和使用场景各有不同。\n\n### 对称加密\n\n#### 1. **原理**\n对称加密使用相同的密钥进行数据的加密和解密。这意味着加密和解密过程使用的是同一个密钥，因此密钥的保密性至关重要。\n\n**加密过程**：\n1. 将明文数据（plaintext）通过加密算法和密钥转换为密文（ciphertext）。\n2. 密文可以通过相同的密钥和解密算法还原为明文。\n\n**解密过程**：\n1. 将密文数据通过解密算法和密钥转换回明文数据。\n2. 由于加密和解密都使用相同的密钥，保密的挑战在于如何安全地分发密钥。\n\n#### 2. **常见算法**\n- **AES（高级加密标准）**：广泛使用的对称加密算法，支持 128 位、192 位和 256 位密钥长度。被广泛应用于文件加密、数据传输等领域。\n- **DES（数据加密标准）**：老旧的对称加密算法，使用 56 位密钥。由于其密钥长度较短，已经不再安全。\n- **3DES（Triple DES）**：对 DES 的扩展，通过三次 DES 加密提高安全性，密钥长度通常为 168 位，但也逐渐被认为不够安全。\n- **Blowfish**：对称加密算法，支持可变长度的密钥（32 位到 448 位）。设计上注重速度和灵活性。\n- **Twofish**：Blowfish 的后继算法，支持 128 位、192 位和 256 位密钥长度，是 AES 的候选算法之一。\n\n### 非对称加密\n\n#### 1. **原理**\n非对称加密使用一对密钥：公钥和私钥。公钥用于加密数据，私钥用于解密数据。公钥可以公开发布，而私钥必须保密。\n\n**加密过程**：\n1. 使用接收方的公钥对明文数据进行加密，生成密文。\n2. 只有持有对应私钥的接收方能够解密密文还原明文。\n\n**解密过程**：\n1. 使用私钥对密文进行解密，得到明文数据。\n\n**特性**：\n- 公钥和私钥是不同的，但密钥对之间的数学关系确保了使用公钥加密的数据只能由私钥解密。\n- 非对称加密主要用于数据的安全传输和数字签名。\n\n#### 2. **常见算法**\n- **RSA**：广泛使用的非对称加密算法，基于大整数因子分解的困难性。密钥长度通常为 1024 位或 2048 位。\n- **ECC（椭圆曲线加密）**：基于椭圆曲线数学的加密算法，提供与 RSA 相同安全性的情况下，使用更短的密钥。支持 256 位、384 位和 521 位密钥。\n- **ElGamal**：基于离散对数问题的加密算法，用于加密和数字签名。\n- **DSA（数字签名算法）**：用于数字签名，基于离散对数问题，不用于加密。\n\n### 对称加密与非对称加密的比较\n\n#### **对称加密**\n- **优点**：\n  - 加密和解密速度较快，适用于大量数据加密。\n  - 算法简单，计算效率高。\n- **缺点**：\n  - 密钥管理困难。密钥需要安全地共享和存储，密钥泄露会导致安全问题。\n\n#### **非对称加密**\n- **优点**：\n  - 密钥管理简单。公钥可以公开，私钥仅需保密。\n  - 可以实现数字签名，验证数据的来源和完整性。\n- **缺点**：\n  - 加密和解密速度较慢，主要用于加密少量数据或用于密钥交换。\n  - 计算复杂，性能开销大。\n\n### 实际应用\n- **对称加密**：适用于数据传输（如文件加密）、数据存储（如数据库加密）、保护数据的隐私等。\n- **非对称加密**：适用于安全通信（如 HTTPS）、数字签名（如软件签名）、密钥交换等。\n\n通常，在实际系统中，两者常常结合使用：用非对称加密来安全地交换对称加密密钥，然后用对称加密来加密实际的数据，这样既保证了密钥的安全传输，又能高效地处理数据。"
  },
  {
    "path": "计算机网络/IP报文.md",
    "content": "\n* [<a href=\"#\">图</a>](#图)\n* [<a href=\"#\">组成</a>](#组成)\n    * [<a href=\"#\">首部</a>](#首部)\n        * [<a href=\"#\">版本号</a>](#版本号)\n        * [<a href=\"#\">IP首部长度</a>](#ip首部长度)\n        * [<a href=\"#\">区分服务</a>](#区分服务)\n        * [<a href=\"#\">总长度</a>](#总长度)\n        * [<a href=\"#\">标识</a>](#标识)\n        * [<a href=\"#\">标志</a>](#标志)\n        * [<a href=\"#\">片位移</a>](#片位移)\n        * [<a href=\"#\">生存时间</a>](#生存时间)\n        * [<a href=\"#\">协议</a>](#协议)\n        * [<a href=\"#\">首部校验和</a>](#首部校验和)\n        * [<a href=\"#\">源地址</a>](#源地址)\n        * [<a href=\"#\">目标地址</a>](#目标地址)\n        * [<a href=\"#\">可选字段</a>](#可选字段)\n    * [<a href=\"#\">数据部分</a>](#数据部分)\n\n\n# [图](#)\n![img.png](../img/计算机网络/IP报文.png)\n# [组成](#)\n## [首部](#)\n### [版本号](#)\n  - 指IP协议所使用的版本。4个位。版本号为0100，4，即IPv4，版本号为6，即IPv6\n### [IP首部长度](#)\n  - 表示IP包头长度，该字段用4位表示。最常见的报头长度是0101即20位，当IP报头长度不是4字节整数倍时，就需要对填充域填充\n### [区分服务](#)\n  - 前3位表示报文的优先级，后面的几位分别表示要求更低时延、更高的吞吐量、更高的可靠性、更低的路由代价等。对应位为1即有相应要求，为0则不要求\n### [总长度](#)\n  - IP报文的总长度。报头的长度和数据部分的长度之和。\n### [标识](#)\n  - 唯一的标识主机发送的每一分数据报。通常每发送一个报文，它的值加一。当IP报文长度超过传输网络的MTU（最大传输单元）时必须分片，这个标识字段的值被复制到所有数据分片的标识字段中，使得这些分片在达到最终目的地时可以依照标识字段的内容重新组成原先的数据。\n### [标志](#)\n  - 共3位。R、DF、MF三位。目前只有后两位有效，DF位：为1表示不分片，为0表示分片。MF：为1表示“更多的片”，为0表示这是最后一片\n### [片位移](#)\n  - 本分片在原先数据报文中相对首位的偏移位。（需要再乘以8）\n### [生存时间](#)\n  - IP报文所允许通过的路由器的最大数量。每经过一个路由器，TTL减1，当为0时，路由器将该数据报丢弃。TTL 字段是由发送端初始设置一个 8 bit字段.推荐的初始值由分配数字 RFC 指定，当前值为 64。发送 ICMP 回显应答时经常把 TTL 设为最大值 255。\n### [协议](#)\n  - 指出IP报文携带的数据使用的是哪种协议，以便目的主机的IP层能知道要将数据报上交到哪个进程（不同的协议有专门不同的进程处理）。和端口号类似，此处采用协议号，TCP的协议号为6，UDP的协议号为17。ICMP的协议号为1，IGMP的协议号为2.\n### [首部校验和](#)\n  - 计算IP头部的校验和，检查IP报头的完整性\n  - 为了计算一份数据报的IP检验和，首先把检验和字段置为0。然后，对首部中每个16bit进行二进制反码求和（整个首部看成是由一串16bit的字组成），结果存在检验和字段中。当收到一份IP数据报后，同样对首部中每个16bit进行二进制反码的求和。由于接受方在计算过程中包含了发送方存在首部中的校验和。因此，如果首部在传输过程中没有发生任何差错，那么接受方计算的结果应该为全1。如果结果不是全1（即检验和错误），那么IP就丢弃收到的数据报。\n### [源地址](#)\n  - 标识IP数据报的源端设备\n### [目标地址](#)\n  - 标识IP数据报的目的地址\n### [可选字段](#)\n  - IP支持很多可选选项\n## [数据部分](#)"
  },
  {
    "path": "计算机网络/JWT.md",
    "content": "\n* [<a href=\"#\">json web token</a>](#json-web-token)\n* [<a href=\"#\">格式</a>](#格式)\n  * [<a href=\"#\">header</a>](#header)\n  * [<a href=\"#\">payload</a>](#payload)\n  * [<a href=\"#\">signature</a>](#signature)\n  * [<a href=\"#\">header.payload.signature</a>](#headerpayloadsignature)\n* [<a href=\"#\">特点</a>](#特点)\n\n\n# [json web token](#)\njson令牌\n# [格式](#)\n## [header](#)\n\n- ```json\n    {\n        \"alg\": \"HS256\",\n        \"typ\": \"JWT\"\n    }\n    ```\n  \n  - alg表示签名使用的算法，默认是HMAC SHA256（写成 HS256\n  - typ属性表示这个令牌（token）的类型（type），JWT 令牌统一写为JWT\n- 这个 JSON 对象也要使用 Base64URL 算法转成字符串\n## [payload](#)\n- Payload 部分也是一个 JSON 对象，用来存放实际需要传递的数据JWT 规定了7个官方字段，供选用，除了官方字段，你还可以在这个部分定义私有字段\n  - ```json\n    {\n      \"sub\": \"1234567890\",\n      \"name\": \"John Doe\",\n      \"admin\": true\n    }\n    ```\n- 这个 JSON 对象也要使用 Base64URL 算法转成字符串\n## [signature](#)\n- Signature 部分是对前两部分的签名，防止数据篡改。\n- 首先，需要指定一个密钥（secret）。这个密钥只有服务器才知道，不能泄露给用户。然后，使用 Header 里面指定的签名算法（默认是 HMAC SHA256），按照下面的公式产生签名。\n  - ```html\n    HMACSHA256(base64UrlEncode(header) + \".\" + base64UrlEncode(payload), secret)\n    ```\n- 算出签名以后，把 Header、Payload、Signature 三个部分拼成一个字符串，每个部分之间用\"点\"（.）分隔，就可以返回给用户。\n## [header.payload.signature](#)\n```\neyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gR\nG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ\n```\n# [特点](#)\n- 无状态的\n- 可扩展性\n- 支持跨域认证"
  },
  {
    "path": "计算机网络/TCP_IP.md",
    "content": "\n* [UDP 和 TCP 的特点](#udp-和-tcp-的特点)\n  * [UDP](#udp)\n  * [TCP](#tcp)\n* [三次握手](#三次握手)\n  * [流程](#流程)\n  * [为什么需要三次握手](#为什么需要三次握手)\n* [四次挥手](#四次挥手)\n  * [流程](#流程-1)\n* [TCP怎么保障可靠传输](#tcp怎么保障可靠传输)\n  * [数据合理分片和排序](#数据合理分片和排序)\n  * [数据校验：校验和](#数据校验校验和)\n  * [TCP 的接收端会丢弃重复的数据](#tcp-的接收端会丢弃重复的数据)\n  * [超时重传](#超时重传)\n  * [流量控制](#流量控制)\n  * [拥塞控制](#拥塞控制)\n  * [ARQ协议](#arq协议)\n* [如何实现可靠UDP传输](#如何实现可靠udp传输)\n* [HTTP长连接还是短连接？](#http长连接还是短连接)\n* [参考文章](#参考文章)\n\n## UDP 和 TCP 的特点\n### UDP\n是无连接的，尽最大可能交付，没有拥塞控制，面向报文 （对于应用程序传下来的报文不合并也不拆分，只是添加 UDP 首部），支持一对一、一对多、多对一和多对多 的交互通信。\n### TCP\n是面向连接的，提供可靠交付，有流量控制，拥塞控 制，提供全双工通信，面向字节流（把应用层传下来的报文看成字节流，把字节流组织成大小不等的数据 块），每一条 TCP 连接只能是点对点的（一对一）。\n## 三次握手\n### 流程\n![](../img/计算机网络/tcp三次握手.png)\n\n1. 客户端–发送带有 SYN 标志的数据包–一次握手–服务端\n2. 服务端–发送带有 SYN/ACK 标志的数据包–二次握手–客户端\n3. 客户端–发送带有带有 ACK 标志的数据包–三次握手–服务端\n\n### 为什么需要三次握手\n- 三次握手的目的是建立可靠的通信信道，说到通讯，简单来说就是数据的发送与接收，而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。\n1. 第一次握手：Client 什么都不能确认；Server 确认了对方发送正常，自己接收正常\n2. 第二次握手：Client 确认了：自己发送、接收正常，对方发送、接收正常；Server 确认了：对方发送正常，自己接收正常\n3. 第三次握手：Client 确认了：自己发送、接收正常，对方发送、接收正常；Server 确认了：自己发送、接收正常，对方发送、接收正常\n\n- 第三次握手是为了防止失效的连接请求到达服务器，让服务器错误打开连接。客户端发送的连接请求如果在网络中滞留，那么就会隔很长一段时间才能收到服务器端发回的连接确认。客户端等待一个超时重传时间之后，就会重新请求连接。但是这个滞留的连接请求最后还是会到达服务器，如果不进行三次握手，那么服务器就会打开两个连接。如果有第三次握手，客户端会忽略服务器之后发送的对滞留连接请求的连接确认，不进行第三次握手，因此就不会再次打开连接。\n\n## 四次挥手\n### 流程\n![](../img/计算机网络/tcp四次挥手.png)\n\n1. 客户端-发送一个 FIN，用来关闭客户端到服务器的数据传送\n    - 任何一方都可以在数据传送结束后发出连接释放的通知，待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候，则发出连接释放通知，对方确认后就完全关闭了TCP连接。\n2. 服务器-收到这个 FIN，它发回一 个 ACK，确认序号为收到的序号加1 。和 SYN 一样，一个 FIN 将占用一个序号\n3. 服务器-关闭与客户端的连接，发送一个FIN给客户端\n   - 客户端发送了 FIN 连接释放报文之后，服务器收到了这个报文，就进入了 CLOSE-WAIT 状态。这个状态是为了让服务器端发送还未传送完毕的数据，传送完毕之后，服务器会发送 FIN 连接释放报文。\n4. 客户端-发回 ACK 报文确认，并将确认序号设置为收到序号加1\n   - 客户端接收到服务器端的 FIN 报文后进入此状态，此时并不是直接进入 CLOSED 状态，还需要等待一个时间计时器\n   - 设置的时间 2MSL。这么做有两个理由：\n     - 确保最后一个确认报文能够到达。如果 B 没收到 A 发送来的确认报文，那么就会重新发送连接释放请求报文，A 等待一段时间就是为了处理这种情况的发生。\n     - 等待一段时间是为了让本连接持续时间内所产生的所有报文都从网络中消失，使得下一个新的连接不会出现旧的连接请求报文。\n\n## TCP怎么保障可靠传输\n\n### 数据合理分片和排序\n  - 应用数据被分割成 TCP 认为最适合发送的数据块。\n    - UDP：IP数据报大于1500字节,大于MTU.这个时候发送方IP层就需要分片(fragmentation).把数据报分成若干片,使每一片都小于MTU（Maxitum Transmission Unit 最大传输单元）.而接收方IP层则需要进行数据报的重组.这样就会多做许多事情,而更严重的是,由于UDP的特性,当某一片数据传送中丢失时,接收方便无法重组数据报.将导致丢弃整个UDP数据报.\n    - tcp会按MTU合理分片，接收方会缓存未按序到达的数据，重新排序后再交给应用层。\n  - TCP 给发送的每一个包进行编号，接收方对数据包进行排序，把有序数据传送给应用层。\n###  数据校验：校验和\n  - TCP 将保持它首部和数据的检验和。这是一个端到端的检验和，目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错，TCP 将丢弃这个报文段和不确认收到此报文段。\n\n### TCP 的接收端会丢弃重复的数据\n### 超时重传\n  - 当 TCP 发出一个段后，它启动一个定时器，等待目的端确认收到这个报文段。如果不能及时收到一个确认，将重发这个报文段。\n  - 超时时间应该设置为多少呢？\n    - RTT和RTO\n      - RTT\n        - RTT 就是数据从网络一端传送到另一端所需的时间，也就是包的往返时间\n      - RTO\n        - 超时重传时间是以 RTO （Retransmission Timeout 超时重传时间）表示\n    - RTO较长和较短会发生什么\n      - 当超时时间 RTO 较大时，重发就慢，丢了老半天才重发，没有效率，性能差\n      - 当超时时间 RTO 较小时，会导致可能并没有丢就重发，于是重发的就快，会增加网络拥塞，导致更多的超时，更多的超时导致更多的重发\n    - 超时重传时间 RTO 的值应该略大于报文往返 RTT 的值\n    - 每当遇到一次超时重传的时候，都会将下一次超时时间间隔设为先前值的两倍。两次超时，就说明网络环境差，不宜频繁反复发送 \n### 流量控制\n  - TCP 连接的每一方都有固定大小的缓冲空间，TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据，能提示发送方降低发送的速率，防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议。 （TCP 利用滑动窗口实现流量控制）\n    - 滑动窗口和流量控制\n      - TCP 利用滑动窗口实现流量控制。流量控制是为了控制发送方发送速率，保证接收方来得及接收。 接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小，从而影响发送方的发送速率。将窗口字段设置为 0，则发送方不能发送数据。\n\n### 拥塞控制\n  - 当网络拥塞时，减少数据的发送。\n  - 如果网络出现拥塞，分组将会丢失，此时发送方会继续重传，从而导致网络拥塞程度更高。因此当出现拥塞时，应当控制发送方的速率。这一点和流量控制很像，但是出发点不同。流量控制是为了让接收方能来得及接收，而拥塞控制是为了降低整个网络的拥塞程度。\n  - 为了进行拥塞控制，TCP 发送方要维持一个 拥塞窗口(cwnd) 的状态变量。拥塞控制窗口的大小取决于网络的拥塞程度，并且动态变化。发送方让自己的发送窗口取为拥塞窗口和接收方的接受窗口中较小的一个。\n    - 注意拥塞窗口与发送方窗口的区别：拥塞窗口只是一个状态变量，实际决定发送方能发送多少数据的是发送方窗口。\n    - TCP的拥塞控制采用了四种算法，即 慢开始 、 拥塞避免 、快重传 和 快恢复。在网络层也可以使路由器采用适当的分组丢弃策略（如主动队列管理 AQM），以减少网络拥塞的发生。\n      - 慢开始\n        - 慢开始算法的思路是当主机开始发送数据时，如果立即把大量数据字节注入到网络，那么可能会引起网络阻塞，因为现在还不知道网络的符合情况。经验表明，较好的方法是先探测一下，即由小到大逐渐增大发送窗口，也就是由小到大逐渐增大拥塞窗口数值。cwnd初始值为1，每经过一个传播轮次，cwnd加倍\n      - 拥塞避免\n        - 拥塞避免算法的思路是让拥塞窗口cwnd缓慢增大，即每经过一个往返时间RTT就把发送放的cwnd加1.\n          - 注意到慢开始每个轮次都将 cwnd 加倍，这样会让 cwnd 增长速度非常快，从而使得发送方发送的速度增长速度过快，网络拥塞的可能性也就更高。设置一个慢开始门限 ssthresh，当 cwnd >= ssthresh 时，进入拥塞避免，每个轮次只将 cwnd 加 1。\n          - 如果出现了超时，则令 ssthresh = cwnd / 2，然后重新执行慢开始。\n      - 快重传与快恢复\n        - 在 TCP/IP 中，快速重传和恢复（fast retransmit and recovery，FRR）是一种拥塞控制算法，它能快速恢复丢失的数据包。没有 FRR，如果数据包丢失了，TCP 将会使用定时器来要求传输暂停。在暂停的这段时间内，没有新的或复制的数据包被发送。有了 FRR，如果接收机接收到一个不按顺序的数据段，它会立即给发送机发送一个重复确认。如果发送机接收到三个重复确认，它会假定确认件指出的数据段丢失了，并立即重传这些丢失的数据段。有了 FRR，就不会因为重传时要求的暂停被耽误。 　当有单独的数据包丢失时，快速重传和恢复（FRR）能最有效地工作。当有多个数据信息包在某一段很短的时间内丢失时，它则不能很有效地工作。\n          - 在接收方，要求每次接收到报文段都应该对最后一个已收到的有序报文段进行确认。例如已经接收到 M1 和 M2，此时收到 M4，应当发送对 M2 的确认。\n          - 在发送方，如果收到三个重复确认，那么可以知道下一个报文段丢失，此时执行快重传，立即重传下一个报文段。例如收到三个 M2，则 M3 丢失，立即重传 M3。\n          - 在这种情况下，只是丢失个别报文段，而不是网络拥塞。因此执行快恢复，令 ssthresh = cwnd / 2 ，cwnd =ssthresh，注意到此时直接进入拥塞避免。\n          - 慢开始和快恢复的快慢指的是 cwnd 的设定值，而不是 cwnd 的增长速率。慢开始 cwnd 设定为 1，而快恢复 cwnd设定为 ssthresh。\n        - 重传方法\n          - SACK\n            - 这种方式需要在 TCP 头部「选项」字段里加一个 SACK 的东西，它可以将缓存的地图发送给发送方，这样发送方就可以知道哪些数据收到了，哪些数据没收到，知道了这些信息，就可以只重传丢失的数据\n            - 如果要支持 SACK，必须双方都要支持。在 Linux 下，可以通过 net.ipv4.tcp_sack 参数打开这个功能（Linux 2.4 后默认打开）\n          - Duplicate SACK\n            - Duplicate SACK 又称 D-SACK，其主要使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。\n            - 情况\n              - ACK 丢包\n                - 「接收方」发给「发送方」的两个 ACK 确认应答都丢失了，所以发送方超时后，重传第一个数据包（3000 ~ 3499）\n                - 于是「接收方」发现数据是重复收到的，于是回了一个 SACK = 3000 ~ 3500，告诉「发送方」 3000 ~ 3500 的数据早已被接收了，因为 ACK 都到了 4000 了，已经意味着 4000 之前的所有数据都已收到，所以这个 SACK 就代表着 D-SACK。\n                - 这样「发送方」就知道了，数据没有丢，是「接收方」的 ACK 确认报文丢了\n              - 网络延时\n                - 数据包（1000~1499） 被网络延迟了，导致「发送方」没有收到 Ack 1500 的确认报文。\n                - 而后面报文到达的三个相同的 ACK 确认报文，就触发了快速重传机制，但是在重传后，被延迟的数据包（1000~1499）又到了「接收方」；\n                - 所以「接收方」回了一个 SACK=1000~1500，因为 ACK 已经到了 3000，所以这个 SACK 是 D-SACK，表示收到了重复的包。\n                - 这样发送方就知道快速重传触发的原因不是发出去的包丢了，也不是因为回应的 ACK 包丢了，而是因为网络延迟了。\n\n### ARQ协议\n  - 也是为了实现可靠传输的，它的基本原理就是每发完一个分组就停止发送，等待对方确认。在收到确认后再发下一个分组。\n  - 自动重传请求（Automatic Repeat-reQuest，ARQ）是OSI模型中数据链路层和传输层的错误纠正协议之一。它通过使用确认和超时这两个机制，在不可靠服务的基础上实现可靠的信息传输。如果发送方在发送后一段时间之内没有收到确认帧，它通常会重新发送。ARQ包括停止等待ARQ协议和连续ARQ协议。\n    - 停止等待ARQ协议\n      - 停止等待协议是为了实现可靠传输的，它的基本原理就是每发完一个分组就停止发送，等待对方确认（回复ACK）。如果过了一段时间（超时时间后），还是没有收到 ACK 确认，说明没有发送成功，需要重新发送，直到收到确认后再发下一个分组。在停止等待协议中，若接收方收到重复分组，就丢弃该分组，但同时还要发送确认。\n      - 优缺点：\n        - 优点： 简单\n        - 缺点： 信道利用率低，等待时间长\n      - 处理情况\n        - 无差错情况:\n          - 发送方发送分组,接收方在规定时间内收到,并且回复确认.发送方再次发送。\n        - 出现差错情况（超时重传）:\n          - 停止等待协议中超时重传是指只要超过一段时间仍然没有收到确认，就重传前面发送过的分组（认为刚才发送过的分组丢失了）。因此每发送完一个分组需要设置一个超时计时器，其重传时间应比数据在分组传输的平均往返时间更长一些。这种自动重传方式常称为 自动重传请求 ARQ 。另外在停止等待协议中若收到重复分组，就丢弃该分组，但同时还要发送确认。连续 ARQ 协议 可提高信道利用率。发送维持一个发送窗口，凡位于发送窗口内的分组可连续发送出去，而不需要等待对方确认。接收方一般采用累积确认，对按序到达的最后一个分组发送确认，表明到这个分组位置的所有分组都已经正确收到了。\n        - 确认丢失和确认迟到\n          - 确认丢失 ：确认消息在传输过程丢失。当A发送M1消息，B收到后，B向A发送了一个M1确认消息，但却在传输过程中丢失。而A并不知道，在超时计时过后，A重传M1消息，B再次收到该消息后采取以下两点措施：1. 丢弃这个重复的M1消息，不向上层交付。 2. 向A发送确认消息。（不会认为已经发送过了，就不再发送。A能重传，就证明B的确认消息丢失）。\n          - 确认迟到 ：确认消息在传输过程中迟到。A发送M1消息，B收到并发送确认。在超时时间内没有收到确认消息，A重传M1消息，B仍然收到并继续发送确认消息（B收到了2份M1）。此时A收到了B第二次发送的确认消息。接着发送其他数据。过了一会，A收到了B第一次发送的对M1的确认消息（A也收到了2份确认消息）。处理如下：1. A收到重复的确认后，直接丢弃。2. B收到重复的M1后，也直接丢弃重复的M1。\n    - 连续ARQ协议\n      - 连续 ARQ 协议可提高信道利用率。发送方维持一个发送窗口，凡位于发送窗口内的分组可以连续发送出去，而不需要等待对方确认。接收方一般采用累计确认，对按序到达的最后一个分组发送确认，表明到这个分组为止的所有分组都已经正确收到了。\n      - 优缺点\n        - 优点： 信道利用率高，容易实现，即使确认丢失，也不必重传。\n        - 缺点： 不能向发送方反映出接收方已经正确收到的所有分组的信息。 比如：发送方发送了 5条 消息，中间第三条丢失（3号），这时接收方只能对前两个发送确认。发送方无法知道后三个分组的下落，而只好把后三个全部重传一次。这也叫 Go-Back-N（回退 N），表示需要退回来重传已经发送过的 N 个消息。\n\n## 如何实现可靠UDP传输\n可以参考TCP的可靠传输来实现UDP的可靠传输，例如：超时重传、分片、拥塞控制等\n\n传输层无法确保数据的可靠传输，只能通过应用层来实现。实现的方式可以参考TCP可靠性传输的方式，只是实现不在传输层，实现转移到应用层。\n\n目前有如下开源程序利用UDP实现了可靠的数据传输，分别是RUDP、RTP、UDT\n## HTTP长连接还是短连接？\n在HTTP/1.0中，默认使用的是短连接\n- 也就是说，浏览器和服务器每进行一次HTTP操作，就建立一次连接，但任务结束就中断连接。如果客户端浏览器访问的某个HTML或其他类型的 Web页中包含有其他的Web资源，如JavaScript文件、图像文件、CSS文件等；当浏览器每遇到这样一个Web资源，就会建立一个HTTP会话。\n\nHTTP/1.1起，默认使用长连接，用以保持连接特性\n- Connection:keep-alive\n- 在使用长连接的情况下，当一个网页打开完成后，客户端和服务器之间用于传输HTTP数据的 TCP连接不会关闭，如果客户端再次访问这个服务器上的网页，会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接，它有一个保持时间，可以在不同的服务器软件（如Apache）中设定这个时间。实现长连接要客户端和服务端都支持长连接\n\n## tcp握手时，全连接队列和半连接队列\n在 TCP 连接中，有两个队列，分别为全连接队列和半连接队列。\n\n当客户端向服务器发起 TCP 连接请求（SYN）时，服务器会首先将该请求放入半连接队列中，然后向客户端发送 SYN+ACK 响应，并将响应放入发送缓冲区等待客户端的确认。如果客户端确认收到了 SYN+ACK 响应，则会向服务器发送 ACK 确认，此时 TCP 连接就建立了。\n\n当服务器收到客户端的 ACK 确认后，它将将该连接从半连接队列中移除，并将连接放入全连接队列中等待应用程序的处理。全连接队列存放已完成三次握手的 TCP 连接。这些连接已经准备好与应用程序进行交互，等待应用程序来接收它们。\n\n由于 TCP 连接的建立需要三次握手的过程，因此在高并发的情况下，全连接队列和半连接队列可能会出现积压，从而导致连接请求被拒绝或者超时。因此，合理设置队列大小和调整服务器的参数对于保障系统的稳定性和性能至关重要。\n\n# 参考文章\n- https://www.cnblogs.com/0201zcr/p/4694945.html\n"
  },
  {
    "path": "计算机网络/TCP报文.md",
    "content": "\n* [<a href=\"#\">图</a>](#图)\n* [<a href=\"#\">组成</a>](#组成)\n  * [<a href=\"#\">首部</a>](#首部)\n    * [<a href=\"#\">源端口</a>](#源端口)\n    * [<a href=\"#\">目的端口</a>](#目的端口)\n    * [<a href=\"#\">序号(seq)</a>](#序号seq)\n    * [<a href=\"#\">确认号(ack)</a>](#确认号ack)\n    * [<a href=\"#\">数据偏移</a>](#数据偏移)\n    * [<a href=\"#\">保留</a>](#保留)\n    * [<a href=\"#\">紧急URG</a>](#紧急urg)\n    * [<a href=\"#\">确认ACK</a>](#确认ack)\n    * [<a href=\"#\">推送 PSH</a>](#推送-psh)\n    * [<a href=\"#\">复位RST</a>](#复位rst)\n    * [<a href=\"#\">同步SYN</a>](#同步syn)\n    * [<a href=\"#\">终止FIN</a>](#终止fin)\n    * [<a href=\"#\">窗口</a>](#窗口)\n    * [<a href=\"#\">检验和</a>](#检验和)\n    * [<a href=\"#\">紧急指针</a>](#紧急指针)\n    * [<a href=\"#\">选项</a>](#选项)\n  * [<a href=\"#\">数据部分</a>](#数据部分)\n\n\n# [图](#)\n![img.png](../img/计算机网络/TCP报文.png)\n\n# [组成](#)\n## [首部](#)\n### [源端口](#)\n  - 16bits，范围0~65525\n### [目的端口](#)\n  - 16bits，范围同上\n\n### [序号(seq)](#)\n  - TCP是面向字节流的。在一个TCP连接中传送的字节流中的每一个字节都按顺序编号。整个要传送的字节流的起始序号必须在连接建立时设置。首部中的序号字段值则是指的是本报文段所发送的数据的第一个字节的序号。长度为4字节，序号是32bit的无符号数,序号到达232 - 1后又从0开始。\n### [确认号(ack)](#)\n  - 确认号，32bits，期望收到对方的下一个报文段的数据的第一个字节的序号。\n  - 确认序号为上次接收的最后一个字节序号加1.\n### [数据偏移](#)\n  - 也叫首部长度，占4个bit,它指出TCP报文段的数据起始处距离TCP报文段的起始处有多远\n### [保留](#)\n  - 占6位，保留为今后使用，但目前应置为0。\n### [紧急URG](#)\n  - 紧急比特，1bit，当 URG=1 时，表明紧急指针字段有效。它告诉系统此报文段中有紧急数据，应尽快传送(相当于高优先级的数据)\n### [确认ACK](#)\n  - 仅当ACK = 1时确认号字段才有效，当ACK = 0时确认号无效。TCP规定，在连接建立后所有的传送的报文段都必须把ACK置为1。\n### [推送 PSH](#)\n  - 当两个应用进程进行交互式的通信时，有时在一端的应用进程希望在键入一个命令后立即就能收到对方的响应。在这种情况下，TCP就可以使用推送（push）操作。发送方TCP把PSH置为1，并立即创建一个报文段发送出去。接收方TCP收到PSH=1的报文段，就尽快地（即“推送”向前）交付接收应用进程。而不用再等到整个缓存都填满了后再向上交付。\n### [复位RST](#)\n  - 当RST=1时，表明TCP连接中出现了严重错误（如由于主机崩溃或其他原因），必须释放连接，然后再重新建立传输连接。RST置为1还用来拒绝一个非法的报文段或拒绝打开一个连接。\n### [同步SYN](#)\n  - 在连接建立时用来同步序号。当SYN=1而ACK=0时，表明这是一个连接请求报文段。对方若同意建立连接，则应在响应的报文段中使SYN=1和ACK=1。\n### [终止FIN](#)\n  - 用来释放一个连接。当FIN=1时，表明此报文段的发送发的数据已发送完毕，并要求释放运输连接。\n### [窗口](#)\n  - 16bits，窗口字段用来控制对方发送的数据量，单位为字节。TCP 连接的一端根据设置的缓存空间大小确定自己的接收窗口大小，然后通知对方以确定对方的发送窗口的上限。\n  - 窗口字段明确指出了现在允许对方发送的数据量。窗口值经常在动态变化。\n### [检验和](#)\n  - 占2字节。检验和字段检验的范围包括首部和数据这两部分。和UDP用户数据报一样，在计算检验和时，要在TCP报文段的前面加上12字节的伪首部\n    - 伪首部\n      - 伪首部的出现：发送方或接收方根据IP报文首部获得8字节的源地址+目的地址、2字节的0字段+UDP协议字段、2字节的数据长度，得到12字节伪首部，临时添加在首部前面。\n      - 伪首部的消失：发送方将计算完毕的校验和填入首部的校验和字段后，去除伪首部发送UDP报文。\n      - 伪首部的作用：仅为了计算校验和使用。\n      - 发送方将UDP伪首部、首部、数据每16位一组进行二进制反码求和，再将求和结果求反码，填入校验和字段。\n      - 接收方收到UDP报文后，生成伪首部，将伪首部、首部、数据每16位一组进行二进制反码求和，若求和结果全为1则无差错传输，否则丢弃。\n### [紧急指针](#)\n  - 占2字节。紧急指针仅在URG=1时才有意义，它指出本报文段中的紧急数据的字节数（紧急数据结束后就是普通数据） 。因此，在紧急指针指出了紧急数据的末尾在报文段中的位置。当所有紧急数据都处理完时，TCP就告诉应用程序恢复到正常操作。值得注意的是，即使窗口为0时也可以发送紧急数据。\n### [选项](#)\n  - 长度可变，最长可达40个字节。当没有使用“选项”时，TCP的首部长度是20字节。\n## [数据部分](#)"
  },
  {
    "path": "计算机网络/TLS.md",
    "content": "# TLS的建立流程\nHTTPS协议其实就是HTTP over TSL，TSL(Transport Layer Security) 传输层安全协议是https协议的核心。\n\nTSL可以理解为SSL (Secure Socket Layer)安全套接字层的后续版本。\n\nTSL握手协议如下图所示\n\n![TSL握手协议.png](..%2Fimg%2F%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%2Ftls%2FTSL%E6%8F%A1%E6%89%8B%E5%8D%8F%E8%AE%AE.png)\n\n在建立TCP连接后，开始建立TLS连接。下面抓包分析TLS握手过程，抓包图片来源于传输层安全协议抓包分析之SSL/TLS (自己没抓到这么完整的包，只能搬运过来了，摔)\n\n(1) client端发起握手请求，会向服务器发送一个ClientHello消息，该消息包括其所支持的SSL/TLS版本、Cipher Suite加密算法列表（告知服务器自己支持哪些加密算法）、sessionID、随机数等内容。\n\n![ClientHello.png](..%2Fimg%2F%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%2Ftls%2FClientHello.png)\n\n(2) 服务器收到请求后会向client端发送ServerHello消息，其中包括：\n\nSSL/TLS版本；\n\nsession ID，因为是首次连接会新生成一个session id发给client；\n\nCipher Suite，sever端从Client Hello消息中的Cipher Suite加密算法列表中选择使用的加密算法；\n\nRadmon 随机数。\n\n![ServerHello.png](..%2Fimg%2F%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%2Ftls%2FServerHello.png)\n\n(3) 经过ServerHello消息确定TLS协议版本和选择加密算法之后，就可以开始发送证书给client端了。证书中包含公钥、签名、证书机构等信息。\n\n![cretificate.png](..%2Fimg%2F%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%2Ftls%2Fcretificate.png)\n\n(4) 服务器向client发送ServerKeyExchange消息，消息中包含了服务器这边的EC Diffie-Hellman算法相关参数。此消息一般只在选择使用DHE 和DH_anon等加密算法组合时才会由服务器发出。\n\n![ServerKeyExchange.png](..%2Fimg%2F%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%2Ftls%2FServerKeyExchange.png)\n\n(5) server端发送ServerHelloDone消息，表明服务器端握手消息已经发送完成了。\n\n![ServerHelloDone.png](..%2Fimg%2F%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%2Ftls%2FServerHelloDone.png)\n\n(6) client端收到server发来的证书，会去验证证书，当认为证书可信之后，会向server发送ClientKeyExchange消息，消息中包含客户端这边的EC Diffie-Hellman算法相关参数，然后服务器和客户端都可根据接收到的对方参数和自身参数运算出Premaster secret，为生成会话密钥做准备。\n\n![ClientKeyExchange.png](..%2Fimg%2F%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%2Ftls%2FClientKeyExchange.png)\n\n(7) 此时client端和server端都可以根据之前通信内容计算出Master Secret（加密传输所使用的对称加密秘钥），client端通过发送此消息告知server端开始使用加密方式发送消息。\n\n![ChangeCipherSpec.png](..%2Fimg%2F%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%2Ftls%2FChangeCipherSpec.png)\n\n(8) 客户端使用之前握手过程中获得的服务器随机数、客户端随机数、Premaster secret计算生成会话密钥master secret，然后使用该会话密钥加密之前所有收发握手消息的Hash和MAC值，发送给服务器，以验证加密通信是否可用。服务器将使用相同的方法生成相同的会话密钥以解密此消息，校验其中的Hash和MAC值。\n\n![finish.png](..%2Fimg%2F%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%2Ftls%2Ffinish.png)\n\n(9) 服务器发送ChangeCipherSpec消息，通知客户端此消息以后服务器会以加密方式发送数据。\n\n![ChangeCipherSpec2.png](..%2Fimg%2F%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%2Ftls%2FChangeCipherSpec2.png)\n\n(10) sever端使用会话密钥加密（生成方式与客户端相同，使用握手过程中获得的服务器随机数、客户端随机数、Premaster secret计算生成）之前所有收发握手消息的Hash和MAC值，发送给客户端去校验。若客户端服务器都校验成功，握手阶段完成，双方将按照SSL记录协议的规范使用协商生成的会话密钥加密发送数据。\n\n![finish2.png](..%2Fimg%2F%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%2Ftls%2Ffinish2.png)\n\n# 参考文章\n- https://www.cnblogs.com/snowater/p/7804889.html"
  },
  {
    "path": "计算机网络/UDP报文.md",
    "content": "\n* [<a href=\"#\">图</a>](#图)\n* [<a href=\"#\">组成</a>](#组成)\n    * [<a href=\"#\">首部</a>](#首部)\n        * [<a href=\"#\">伪首部</a>](#伪首部)\n        * [<a href=\"#\">源端口</a>](#源端口)\n        * [<a href=\"#\">目标端口</a>](#目标端口)\n        * [<a href=\"#\">长度</a>](#长度)\n        * [<a href=\"#\">校验和</a>](#校验和)\n    * [<a href=\"#\">数据部分</a>](#数据部分)\n\n\n# [图](#)\n![img.png](../img/计算机网络/UDP报文.png)\n# [组成](#)\n## [首部](#)\n### [伪首部](#)\n### [源端口](#)\n### [目标端口](#)\n### [长度](#)\n  - UDP报文的整个大小，最小为8个字节（仅为首部)\n### [校验和](#)\n  - 在进行检验和计算时，会添加一个伪首部一起进行运算。伪首部（占用12个字节）为：4个字节的源IP地址、4个字节的目的IP地址、1个字节的0、一个字节的数字17、以及占用2个字节UDP长度。这个伪首部不是报文的真正首部，只是引入为了计算校验和。相对于IP协议的只计算首部，UDP检验和会把首部和数据一起进行校验。接收端进行的校验和与UDP报文中的校验和相与，如果无差错应该全为1。如果有误，则将报文丢弃或者发给应用层、并附上差错警告\n## [数据部分](#)"
  },
  {
    "path": "计算机网络/cookie和session.md",
    "content": "# [Cookie](#)\n## [Cookie是什么？](#)\nhttp协议里的cookie包含web cookie和浏览器cookie，他是服务器发送到web浏览器的一小块数据，服务器发送到浏览器的cookie，浏览器会进行存储，并与下一个请求一起发送到服务器，通常，他用于判断两个请求是否来自于同一个浏览器，例如用户保持登录状态\n## [创建cookie](#)\n当接受到客户端的HTTP请求后，服务器可以发送带有响应的set-cookie 标头，cookie通常由浏览器储存，然后cookie和http 请求头一同向服务器发出请求\n## [cookie过期](#)\n如果未指定到期时间，默认会话结束销毁，否则持久化到磁盘，到期删除\n- expires\n- max-age\n\n# [Session](#)\n## [session是什么](#)\n客户端请求服务端，服务端会为这次请求开辟一块内存空间，这个对象便是Session对象，session弥补了HTTP无状态的特性，服务器可以利用session存储客户端在同一个会话期间的一些操作\n## [session如何判断是否是同一会话](#)\n- 开辟session空间后同时会生成sessionID，并通过响应头的set-cookie:  JSESSIONID=xxx命令 向客户端发送要求设置cookie的响应，客户端收到响应后，在本机客户端设置一个JSESSIONID=xxx的cookie的信息，改cookie的过期时间为浏览器会话结束\n- 后面客户端的每次请求头都带上cookie（包含该sessionid）服务器通过读取请求头里的cookie信息获取到sessionid\n## [session的缺点](#)\na服务器储存了session后，服务做了负载均衡，此次访问被转发到服务b，但是服务B没有储存a 的session，会导致session的失效\n- 可以把token储存Redis\n## [禁用cookie后使用session](#)\n在URL后加上sessionid\n"
  },
  {
    "path": "计算机网络/网络协议分层.md",
    "content": "\n# OSI 7层(基本只是拿来作比较)\n- 应用层\n- 表示层\n- 会话层\n- 传输层\n- 网络层\n- 数据链路层\n- 物理层\n\n虽然划分的很细每层都有详细的分工，但是在实际实现中TCP/IP 5(4)层得到了大多数人的认可，于是7层基本可以说淘汰了\n\n# TCP/IP 5(4)层\n- 应用层\n- 传输层\n- 网络层\n- 数据链路层\n- 物理层\n## 应用层\n### 什么是？\n应用层(application-layer）的任务是通过应用进程间的交互来完成特定网络应用。应用层协议定义的是应用进程（进程：主机中正在运行的程序）间的通信和交互的规则。对于不同的网络应用需要不同的应用层协议。在互联网中应用层协议很多，如域名系统DNS，支持万维网应用的 HTTP协议，支持电子邮件的 SMTP协议等等\n### 常见的协议\n#### 域名系统\nDNS 是一个分布式数据库，提供了主机名和 IP 地址之间相互转换的服务。这里的分布式数据库是指，每个站点只保留它自己的那部分数据。 域名具有层次结构，从上到下依次为：根域名、顶级域名、二级域名\n#### 文件传送协议\nFTP 使用 TCP 进行连接，它需要两个连接来传送一个文件\n- 控制连接：服务器打开端口号 21 等待客户端的连接，客户端主动建立连接后，使用这个连接将客户端的命令传送给服务器，并传回服务器的应答。\n- 数据连接：用来传送一个文件数据 \n\n根据数据连接是否是服务器端主动建立，FTP 有主动和被动两种模式：\n- 主动模式：服务器端主动建立数据连接，其中服务器端的端口号为 20，客户端的端口号随机，但是必须大于1024，因为 0~1023 是熟知端口号\n- 被动模式：客户端主动建立数据连接，其中客户端的端口号由客户端自己指定，服务器端的端口号随机。\n#### SMTP电子邮件协议\n#### 远程登录协议\n    \n## 传输层\n### 什么是？\n运输层(transport layer)的主要任务就是负责向两台主机进程之间的通信提供通用的数据传输服务。应用进程利用该服务传送应用层报文。“通用的”是指并不针对某一个特定的网络应用，而是多种应用可以使用同一个运输层服务。由于一台主机可同时运行多个线程，因此运输层有复用和分用的功能。所谓复用就是指多个应用层进程可同时使用下面运输层的服务，分用和复用相反，是运输层把收到的信息分别交付上面应用层中的相应进程。\n### 常见的协议\n#### TCP\n提供面向连接的，可靠的数据传输服务\n#### UDP\n提供无连接的，尽最大努力的数据传输服务（不保证数据传输的可靠性）\n\n## 网络层\n### 什么是？\n在计算机网络中进行通信的两个计算机之间可能会经过很多个数据链路，也可能还要经过很多通信子网。网络层的任务就是选择合适的网间路由和交换结点， 确保数据及时传送 在发送数据时，网络层把运输层产生的报文段或用户数据报封装成分组和包进行传送。在 TCP/IP 体系结构中，由于网络层使用 IP 协议，因此分组也叫 IP 数据报 ，简称 数据报\n\n路由器工作在这层\n\n## 数据链路层\n### 什么是？\n数据链路层(data link layer)通常简称为链路层。两台主机之间的数据传输，总是在一段一段的链路上传送的，这就需要使用专门的链路层的协议，在两个相邻节点之间传送数据时，数据链路层将网络层交下来的 IP 数据报组装成帧，在两个相邻节点间的链路上传送帧。每一帧包括数据和必要的控制信息（如同步信息，地址信息，差错控制等）\n### 封装成帧\n将网络层传下来的分组添加首部和尾部，用于标记帧的开始和结束\n### 透明传输\n帧使用首部和尾部进行定界，如果帧的数据部分含有和首部尾部相同的内容，那么帧的开始和结束位置就会被错误的 判定。需要在数据部分出现首部尾部相同的内容前面插入转义字符。如果数据部分出现转义字符，那么就在转义字符 前面再加个转义字符。在接收端进行处理之后可以还原出原始数据。这个过程透明传输的内容是转义字符，用户察觉不到转义字符的存在。\n### 差错检测\n目前数据链路层广泛使用了循环冗余检验（CRC）来检查比特差错。\n- `CRC` 简单来说就是在传输的比特后面加上循环冗余码，如果接收方按照相同的计算方式计算得出的冗余码相同则正确  \n\n交换机工作在这层\n## 物理层\n### 什么是？  \n物理层(physical layer)的作用是实现相邻计算机节点之间比特流的透明传送，尽可能屏蔽掉具体传输介质和物理设备的差异"
  },
  {
    "path": "计算机网络/网络攻击行为.md",
    "content": "\n* [CSRF攻击](#csrf攻击)\n  * [什么是CSRF攻击](#什么是csrf攻击)\n  * [场景](#场景)\n  * [怎么预防](#怎么预防)\n    * [阻止不明外域的访问](#阻止不明外域的访问)\n    * [提交时要求附加本域才能获取的信息](#提交时要求附加本域才能获取的信息)\n* [XSS](#xss)\n  * [什么是XSS](#什么是xss)\n  * [分类](#分类)\n  * [如何预防XSS](#如何预防xss)\n* [SQL注入](#sql注入)\n  * [什么是SQL注入](#什么是sql注入)\n  * [怎么预防](#怎么预防-1)\n* [DDOS](#ddos)\n  * [什么是DDOS](#什么是ddos)\n  * [怎么预防](#怎么预防-2)\n* [SYN Flood攻击](#syn-flood攻击)\n  * [SYN Flood攻击如何工作？](#syn-flood攻击如何工作)\n  * [防范](#防范)\n    * [连接限制技术：](#连接限制技术)\n    * [连接代理技术：](#连接代理技术)\n      * [SYN Cookie](#syn-cookie)\n      * [Safe Reset](#safe-reset)\n    * [半开连接数检测](#半开连接数检测)\n* [参考文章](#参考文章)\n\n# CSRF攻击\n## 什么是CSRF攻击\nCSRF（Cross-site request forgery）跨站请求伪造：攻击者诱导受害者进入第三方网站，在第三方网站中，向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证，绕过后台的用户验证，达到冒充用户对被攻击的网站执行某项操作的目的\n## 场景\n一个典型的CSRF攻击有着如下的流程：\n- 受害者登录a.com，并保留了登录凭证（Cookie）。\n- 攻击者引诱受害者访问了b.com。\n- b.com 向 a.com 发送了一个请求：a.com/act=xx。浏览器会默认携带a.com的Cookie。\n- a.com接收到请求后，对请求进行验证，并确认是受害者的凭证，误以为是受害者自己发送的请求。\n- a.com以受害者的名义执行了act=xx。\n攻击完成，攻击者在受害者不知情的情况下，冒充受害者，让a.com执行了自己定义的操作。\n## 怎么预防\n### 阻止不明外域的访问\n- 在HTTP协议中，每一个异步请求都会携带两个Header，用于标记来源域名：\n  - Origin Header\n    - 使用Origin Header确定来源域名\n  - Referer Header\n- 这两个Header在浏览器发起请求时，大多数情况会自动带上，并且不能由前端自定义内容。 服务器可以通过解析这两个Header中的域名，确定请求的来源域。\n### 提交时要求附加本域才能获取的信息\n- CSRF Token\n  - 将CSRF Token输出到页面中\n  - 页面提交的请求携带这个Token\n  - 服务器验证Token是否正确\n- 双重Cookie验证\n  - 在用户访问网站页面时，向请求域名注入一个Cookie，内容为随机字符串（例如csrfcookie=v8g9e4ksfhw）。\n  - 在前端向后端发起请求时，取出Cookie，并添加到URL的参数中（接上例POST https://www.a.com/comment?csrfcookie=v8g9e4ksfhw）。\n  - 后端接口验证Cookie中的字段与URL参数中的字段是否一致，不一致则拒绝。\n- Samesite Cookie属性\n  - Samesite 有两个属性值，分别是 Strict 和 Lax\n    - Samesite=Strict\n      - 这种称为严格模式，表明这个 Cookie 在任何情况下都不可能作为第三方 Cookie\n    - Samesite=Lax\n      - 这种称为宽松模式，比 Strict 放宽了点限制：假如这个请求是这种请求（改变了当前页面或者打开了新页面）且同时是个GET请求，则这个Cookie可以作为第三方Cookie\n  - 不成熟\n\n# XSS\n## 什么是XSS\nXSS是一种常见的web安全漏洞，它允许攻击者将恶意代码植入到提供给其它用户使用的页面中\n## 分类\n- 存储型XSS\n  - 主要出现在让用户输入数据，供其他浏览此页的用户进行查看的地方，包括留言、评论、博客日志和各类表单等。应用程序从数据库中查询数据，在页面中显示出来，攻击者在相关页面输入恶意的脚本数据后，用户浏览此类页面时就可能受到攻击。这个流程简单可以描述为：恶意用户的Html输入Web程序->进入数据库->Web程序->用户浏览器\n  - 比如说我写了一个网站，然后攻击者在上面发布了一个文章，内容是这样的 `<script>alert(document.cookie)</script>`,如果我没有对他的内容进行处理，直接存储到数据库，那么下一次当其他用户访问他的这篇文章的时候，服务器从数据库读取后然后响应给客户端，浏览器执行了这段脚本，就会将cookie展现出来，这就是典型的存储型XSS\n- 反射型XSS\n  - 反射型XSS，主要做法是将脚本代码加入URL地址的请求参数里，请求参数进入程序后在页面直接输出，用户点击类似的恶意链接就可能受到攻击。\n## 如何预防XSS\n答案很简单，坚决不要相信用户的任何输入，并过滤掉输入中的所有特殊字符。这样就能消灭绝大部分的XSS攻击。\n- 过滤特殊字符\n  - 避免XSS的方法之一主要是将用户所提供的内容进行过滤(如上面的script标签)。\n- 使用HTTP头指定类型\n  - `w.Header().Set(\"Content-Type\",\"text/javascript\")`\n  - 这样就可以让浏览器解析javascript代码，而不会是html输出。\n\n# SQL注入\n## 什么是SQL注入\n- 攻击者成功的向服务器提交恶意的SQL查询代码，程序在接收后错误的将攻击者的输入作为查询语句的一部分执行，导致原始的查询逻辑被改变，额外的执行了攻击者精心构造的恶意代码\n- 这是最常见的 SQL注入攻击，当我们输如用户名 admin ，然后密码输如`' OR '1'=1='1`的时候，我们在查询用户名和密码是否正确的时候，本来要执行的是`SELECT * FROM user WHERE username='' and password=''`,经过参数拼接后，会执行 SQL语句 SELECT * FROM user WHERE username='' and password='' OR '1'='1'，这个时候1=1是成立，自然就跳过验证了。\n- 但是如果再严重一点，密码输如的是`';DROP TABLE user;--`，那么 SQL命令为`SELECT * FROM user WHERE username='admin' and password='';drop table user;--'` 这个时候我们就直接把这个表给删除了。\n## 怎么预防\n- 在Java中，我们可以使用预编译语句(PreparedStatement)，这样的话即使我们使用 SQL语句伪造成参数，到了服务端的时候，这个伪造 SQL语句的参数也只是简单的字符，并不能起到攻击的作用。\n- 在应用发布之前建议使用专业的SQL注入检测工具进行检测，以及时修补被发现的SQL注入漏洞。网上有很多这方面的开源工具，例如sqlmap、SQLninja等。\n- 避免网站打印出SQL错误信息，比如类型错误、字段不匹配等，把代码里的SQL语句暴露出来，以防止攻击者利用这些错误信息进行SQL注入。\n\n\n# DDOS\n## 什么是DDOS\n分布式拒绝服务攻击（Distributed Denial of Service），简单说就是发送大量请求是使服务器瘫痪\n- TCP的SYN攻击\n  - 在三次握手过程中，服务器发送 SYN-ACK 之后，收到客户端的 ACK 之前的 TCP 连接称为半连接(half-open connect)。此时服务器处于 SYN_RCVD 状态。当收到 ACK 后，服务器才能转入 ESTABLISHED状态.\n  - SYN攻击指的是，攻击客户端在短时间内伪造大量不存在的IP地址，向服务器不断地发送SYN包，服务器回复确认包，并等待客户的确认。由于源地址是不存在的，服务器需要不断的重发直至超时，这些伪造的SYN包将长时间占用未连接队列，正常的SYN请求被丢弃，导致目标系统运行缓慢，严重者会引起网络堵塞甚至系统瘫痪。\n## 怎么预防\n- 分析可疑流量过滤\n  - 关键在于区分攻击流量与正常流量\n- 速率限制\n- 将攻击流量分散至分布式服务器网络，直到网络吸收流量为止\n\n# SYN Flood攻击\nSYN Flood（半开放攻击）是一种拒绝服务（DDoS）攻击，其目的是通过消耗所有可用的服务器资源使服务器不可用于合法流量。通过重复发送初始连接请求（SYN）数据包，攻击者能够压倒目标服务器机器上的所有可用端口，导致目标设备根本不响应合法流量。\n\n## SYN Flood攻击如何工作？\n通过利用TCP连接的握手过程，SYN Flood攻击工作。在正常情况下，TCP连接显示三个不同的进程以进行连接。\n\n1.首先，客户端向服务器发送SYN数据包，以便启动连接。\n\n2.服务器响应该初始包与SYN / ACK包，以确认通信。\n\n3.最后，客户端返回ACK数据包以确认从服务器接收到的数据包。完成这个数据包发送和接收序列后，TCP连接打开并能发送和接收数据。\n\n客户端发送一个 SYN包 给服务端后就退出，而服务端接收到 SYN包 后，会回复一个 SYN+ACK包 给客户端，然后等待客户端回复一个 ACK包。\n\n但此时客户端并不会回复 ACK包，所以服务端只能一直等待直到超时。服务端超时后，会重发 SYN+ACK包 给客户端，默认会重试 5 次，而且每次等待的时间都会增加（可以参考 TCP 协议超时重传的实现）。\n\n另外，当服务端接收到 SYN包 后，会建立一个半连接状态的 Socket。所以，当客户端一直发送 SYN包，但不回复 ACK包，那么将会耗尽服务端的资源，这就是 SYN Flood 攻击。\n\n攻击者通常会伪造IP和更换端口\n\n## 防范\n### 连接限制技术：\n采用SYN Flood攻击防范检测技术，对网络中的新建TCP半开连接数和新建TCP连接速率进行实时检测，通过设置检测阈值来有效地发现攻击流量，然后通过阻断新建连接或释放无效连接来抵御SYN Flood攻击。\n### 连接代理技术：\n采用SYN Cookie或Safe Reset技术对网络中的TCP连接进行代理，通过精确的验证来准确的发现攻击报文，实现为服务器过滤掉恶意连接报文的同时保证常规业务的正常运行。连接代理技术除了可以对已检测到攻击的服务器进行代理防范，也可以对可能的攻击对象事先配置，做到全部流量代理，而非攻击发生后再代理，这样可以避免攻击报文已经造成一定损失。\n\n#### SYN Cookie\nSYN Cookie借鉴了HTTP中Cookie的概念。SYN Cookie技术可理解为，防火墙对TCP新建连接的协商报文进行处理，使其携带认证信息（称之为Cookie），再通过验证客户端回应的协商报文中携带的信息来进行报文有效性确认的一种技术。如图7所示，该技术的实现机制是防火墙在客户端与服务器之间做连接代理，具体过程如下：\n\n![](../img/计算机网络/syncookie.png)\n1. 客户端向服务器发送一个SYN消息。\n2. SYN消息经过防火墙时，防火墙截取该消息，并模拟服务器向客户端回应SYN/ACK消息。其中，SYN/ACK消息中的序列号为防火墙计算的Cookie，此Cookie值是对加密索引与本次连接的客户端信息（如：IP地址、端口号）进行加密运算的结果。\n3. 客户端收到SYN/ACK报文后向服务器发送ACK消息进行确认。防火墙截取这个消息后，提取该消息中的ACK序列号，并再次使用客户端信息与加密索引计算Cookie。如果计算结果与ACK序列号相符，就可以确认发起连接请求的是一个真实的客户端。如果客户端不回应ACK消息，就意味着现实中并不存在这个客户端，此连接是一个仿冒客户端的攻击连接；如果客户端回应的是一个无法通过检测的ACK消息，就意味着此客户端非法，它仅想通过模拟简单的TCP协议栈来耗费服务器的连接资源。来自仿冒客户端或非法客户端的后续报文都会被防火墙丢弃，而且防火墙也不会为此分配TCB资源。\n4. 如果防火墙确认客户端的ACK消息合法，则模拟客户端向服务器发送一个SYN消息进行连接请求，同时分配TCB资源记录此连接的描述信息。此TCB记录了防火墙向服务器发起的连接请求的信息，同时记录了步骤（2）中客户端向服务器发起的连接请求的信息。\n5. 服务器向防火墙回应SYN/ACK消息。\n6. 防火墙收到服务器的SYN/ACK回应消息后，根据已有的连接描述信息，模拟客户端向服务器发送ACK消息进行确认。\n7. 完成以上过程之后，客户端与防火墙之间建立了连接，防火墙与服务器之间也建立了连接，客户端与服务器间关于此次连接的后续数据报文都将通过防火墙进行代理转发。\n防火墙的SYN Cookie技术利用SYN/ACK报文携带的认证信息，对握手协商的ACK报文进行了认证，从而避免了防火墙过早分配TCB资源。当客户端向服务器发送恶意SYN报文时，既不会造成服务器上TCB资源和带宽的消耗，也不会造成防火墙TCB资源的消耗，可以有效防范SYN Flood攻击。在防范SYN Flood攻击的过程中，防火墙作为虚拟的服务器与客户端交互，同时也作为虚拟的客户端与服务器交互，在为服务器过滤掉恶意连接报文的同时保证了常规业务的正常运行。\n\n#### Safe Reset\nSafe Reset技术是防火墙通过对正常TCP连接进行干预来识别合法客户端的一种技术。防火墙对TCP新建连接的协商报文进行处理，修改响应报文的序列号并使其携带认证信息（称之为Cookie），再通过验证客户端回应的协商报文中携带的信息来进行报文有效性确认。\n\n防火墙在利用Safe Reset技术认证新建连接的过程中，对合法客户端的报文进行正常转发，对仿冒客户端以及简单模拟TCP协议栈的恶意客户端发起的新建连接报文进行丢弃，这样服务器就不会为仿冒客户端发起的SYN报文分配连接资源，从而避免了SYN Flood 攻击。如图8所示，Safe Reset技术的实现过程如下：\n![](../img/计算机网络/safereset.png)\n\n1. 客户端向服务器发送一个SYN消息。\n2. SYN消息经过防火墙时，防火墙截取该消息，并模拟服务器向客户端回应SYN/ACK消息。其中，SYN/ACK消息中的ACK序列号与客户端期望的值不一致，同时携带Cookie值。此Cookie值是对加密索引与本次连接的客户端信息（包括：IP地址、端口号）进行加密运算的结果。\n3. 客户端按照协议规定向服务器回应RST消息。防火墙中途截取这个消息后，提取消息中的序列号，并对该序列号进行Cookie校验。成功通过校验的连接被认为是可信的连接，防火墙会分配TCB资源记录此连接的描述信息，而不可信连接的后续报文会被防火墙丢弃。\n4. 完成以上过程之后，客户端再次发起连接请求，防火墙根据已有的连接描述信息判断报文的合法性，对可信连接的所有合法报文直接放行。\n\n由于防火墙仅通过对客户端向服务器首次发起连接的报文进行认证，就能够完成对客户端到服务器的连接检验，而服务器向客户端回应的报文即使不经过防火墙也不会影响正常的业务处理，因此Safe Reset技术也称为单向代理技术。\n\n一般而言，应用服务器不会主动对客户端发起恶意连接，因此服务器响应客户端的报文可以不需要经过防火墙的检查。防火墙仅需要对客户端发往应用服务器的报文进行实时监控。服务器响应客户端的报文可以根据实际需要选择是否经过防火墙，因此Safe Reset能够支持更灵活的组网方式。\n### 半开连接数检测\n这类半开连接与正常的半开连接的区别在于，正常半开连接会随着客户端和服务器端握手报文的交互完成而转变成全连接，而仿冒源IP的半开连接永远不会完成握手报文的交互\n\n监测和释放这类半开连接\n\n# 参考文章\n- https://zhuanlan.zhihu.com/p/29539671\n- http://www.h3c.com/cn/d_200810/618230_30003_0.htm\n\n\n\n  "
  },
  {
    "path": "计算机网络/跨域.md",
    "content": "\n* [<a href=\"#\">什么是跨域？</a>](#什么是跨域)\n* [<a href=\"#\">同源策略</a>](#同源策略)\n* [<a href=\"#\">解决方案</a>](#解决方案)\n  * [<a href=\"#\">JSONP</a>](#jsonp)\n  * [<a href=\"#\">CORS</a>](#cors)\n\n\n# [什么是跨域？](#)\n跨域，指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的，是浏览器对JavaScript施加的安全限制\n\n# [同源策略](#)\n有一个不同就是不同源\n- 域名\n- 协议\n- 端口\n# [解决方案](#)\n## [JSONP](#)\nJSONP 的理念就是，与服务端约定好一个回调函数名，服务端接收到请求后，将返回一段 Javascript，在这段 Javascript 代码中调用了约定好的回调函数，并且将数据作为参数进行传递。当网页接收到这段 Javascript 代码后，就会执行这个回调函数，这时数据已经成功传输到客户端了。\n- 优缺点\n  - 优点\n    - 它不像XMLHttpRequest对象实现的Ajax请求那样受到同源策略的限制；它的兼容性更好，在更加古老的浏览器中都可以运行\n  - 缺点\n    - 它只支持 GET 请求，而不支持 POST 请求等其他类型的 HTTP 请求\n## [CORS](#)\n- [Access-Control-Allow-Origin](#)\n  - 响应首部中可以携带这个头部表示服务器允许哪些域可以访问该资源，其语法如下：\n  - Access-Control-Allow-Origin: <origin> | *\n  - 其中，origin 参数的值指定了允许访问该资源的外域 URI。对于不需要携带身份凭证的请求，服务器可以指定该字段的值为通配符，表示允许来自所有域的请求。\n- [Access-Control-Allow-Credentials](#)\n  - 该字段可选。它的值是一个布尔值，表示是否允许发送Cookie。默认情况下，Cookie不包括在CORS请求之中。设为true，即表示服务器明确许可，Cookie可以包含在请求中，一起发给服务器。其语法如下：\n  - Access-Control-Allow-Credentials: true\n- [Access-Control-Allow-Methods](#)\n  - 该首部字段用于预检请求的响应，指明实际请求所允许使用的HTTP方法。其语法如下：\n  - Access-Control-Allow-Methods: <method>[, <method>]*"
  },
  {
    "path": "设计模式/单例模式.md",
    "content": "\n* [单例模式](#单例模式)\n    * [懒汉式单例](#懒汉式单例)\n    * [饿汉式单例](#饿汉式单例)\n    * [双检锁/双重校验锁（DCL，即 double-checked locking）](#双检锁双重校验锁dcl即-double-checked-locking)\n    * [登记式/静态内部类](#登记式静态内部类)\n\n## 单例模式\n类只有一个实例，且该类能自行创建这个实例的一种模式\n### 懒汉式单例\n```java\npublic class LazySingleton {\n    private static volatile LazySingleton instance = null;\n\n    private LazySingleton(){\n\n    }\n\n    public static synchronized LazySingleton getInstance(){\n        if(instance == null){\n            instance = new LazySingleton();\n        }\n        return instance;\n    }\n}\n```\n该模式的特点是类加载时没有生成单例，只有当第一次调用 getlnstance 方法时才去创建这个单例\n### 饿汉式单例\n\n```java\npublic class HungrySingleton {\n    private static final HungrySingleton instance = new HungrySingleton();\n\n    private HungrySingleton(){\n\n    }\n\n    public static HungrySingleton getInstance(){\n        return instance;\n    }\n}\n```\n该模式的特点是类一旦加载就创建一个单例，保证在调用 getInstance 方法之前单例已经存在了。\n\n### 双检锁/双重校验锁（DCL，即 double-checked locking）\n```java\npublic class DCLSingleton {\n\n    private volatile static DCLSingleton singleton;\n\n    private DCLSingleton() {\n\n    }\n\n    public static DCLSingleton getInstance() {\n        if (singleton == null) {\n            synchronized (DCLSingleton.class) {\n                if (singleton == null) {\n                    singleton = new DCLSingleton();\n                }\n            }\n        }\n        return singleton;\n    }\n\n}\n```\n### 登记式/静态内部类\n```java\npublic class StaticSingleton {\n    private static class SingletonHolder {\n        private static final StaticSingleton INSTANCE = new StaticSingleton();\n    }\n\n    private StaticSingleton() {\n\n    }\n\n    public static StaticSingleton getInstance() {\n        return SingletonHolder.INSTANCE;\n    }\n}\n```"
  },
  {
    "path": "设计模式/工厂模式.md",
    "content": "# 工程模式\n意图：定义一个创建对象的接口，让其子类自己决定实例化哪一个工厂类，工厂模式使其创建过程延迟到子类进行。\n\n## 实现\n\n接口\n```java\npublic interface Shape {\n   void draw();\n}\n```\n实现类\n```java\npublic class Rectangle implements Shape {\n \n   @Override\n   public void draw() {\n      System.out.println(\"Inside Rectangle::draw() method.\");\n   }\n}\n```\n```java\npublic class Square implements Shape {\n \n   @Override\n   public void draw() {\n      System.out.println(\"Inside Square::draw() method.\");\n   }\n}\n```\n```java\npublic class Circle implements Shape {\n \n   @Override\n   public void draw() {\n      System.out.println(\"Inside Circle::draw() method.\");\n   }\n}\n```\n工厂\n```java\npublic class ShapeFactory {\n    \n   //使用 getShape 方法获取形状类型的对象\n   public Shape getShape(String shapeType){\n      if(shapeType == null){\n         return null;\n      }        \n      if(shapeType.equalsIgnoreCase(\"CIRCLE\")){\n         return new Circle();\n      } else if(shapeType.equalsIgnoreCase(\"RECTANGLE\")){\n         return new Rectangle();\n      } else if(shapeType.equalsIgnoreCase(\"SQUARE\")){\n         return new Square();\n      }\n      return null;\n   }\n}\n```\n使用\n```java\npublic class FactoryPatternDemo {\n \n   public static void main(String[] args) {\n      ShapeFactory shapeFactory = new ShapeFactory();\n \n      //获取 Circle 的对象，并调用它的 draw 方法\n      Shape shape1 = shapeFactory.getShape(\"CIRCLE\");\n \n      //调用 Circle 的 draw 方法\n      shape1.draw();\n \n      //获取 Rectangle 的对象，并调用它的 draw 方法\n      Shape shape2 = shapeFactory.getShape(\"RECTANGLE\");\n \n      //调用 Rectangle 的 draw 方法\n      shape2.draw();\n \n      //获取 Square 的对象，并调用它的 draw 方法\n      Shape shape3 = shapeFactory.getShape(\"SQUARE\");\n \n      //调用 Square 的 draw 方法\n      shape3.draw();\n   }\n}\n```\n结果\n```java\nInside Circle::draw() method.\nInside Rectangle::draw() method.\nInside Square::draw() method.\n```\n# 参考文章\n- https://www.runoob.com/design-pattern/factory-pattern.html"
  },
  {
    "path": "设计模式/策略模式.md",
    "content": "\n* [策略模式](#策略模式)\n* [实现](#实现)\n    * [创建一个接口](#创建一个接口)\n    * [创建实现接口的实体类](#创建实现接口的实体类)\n    * [创建Context类](#创建context类)\n    * [使用 Context 来查看当它改变策略 Strategy 时的行为变化](#使用-context-来查看当它改变策略-strategy-时的行为变化)\n* [参考文章](#参考文章)\n\n# 策略模式\n在策略模式（Strategy Pattern）中，一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。\n\n在策略模式中，我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略对象改变 context 对象的执行算法\n\n# 实现\n\n## 创建一个接口\n```java\npublic interface Strategy {\n   public int doOperation(int num1, int num2);\n}\n```\n## 创建实现接口的实体类\n```java\npublic class OperationAdd implements Strategy{\n   @Override\n   public int doOperation(int num1, int num2) {\n      return num1 + num2;\n   }\n}\n```\n```java\npublic class OperationSubtract implements Strategy{\n   @Override\n   public int doOperation(int num1, int num2) {\n      return num1 - num2;\n   }\n}\n```\n```java\npublic class OperationMultiply implements Strategy{\n   @Override\n   public int doOperation(int num1, int num2) {\n      return num1 * num2;\n   }\n}\n```\n## 创建Context类\n```java\npublic class Context {\n   private Strategy strategy;\n \n   public Context(Strategy strategy){\n      this.strategy = strategy;\n   }\n \n   public int executeStrategy(int num1, int num2){\n      return strategy.doOperation(num1, num2);\n   }\n}\n```\n## 使用 Context 来查看当它改变策略 Strategy 时的行为变化\n```java\npublic class StrategyPatternDemo {\n   public static void main(String[] args) {\n      Context context = new Context(new OperationAdd());    \n      System.out.println(\"10 + 5 = \" + context.executeStrategy(10, 5));\n \n      context = new Context(new OperationSubtract());      \n      System.out.println(\"10 - 5 = \" + context.executeStrategy(10, 5));\n \n      context = new Context(new OperationMultiply());    \n      System.out.println(\"10 * 5 = \" + context.executeStrategy(10, 5));\n   }\n}\n```\n# 参考文章\n- https://www.runoob.com/design-pattern/strategy-pattern.html"
  },
  {
    "path": "设计模式/装饰者模式.md",
    "content": "\n* [装饰器模式（装饰者模式）](#装饰器模式装饰者模式)\n    * [创建一个接口](#创建一个接口)\n    * [创建实现接口的实体类](#创建实现接口的实体类)\n    * [创建实现了 Shape 接口的抽象装饰类](#创建实现了-shape-接口的抽象装饰类)\n    * [创建扩展了 ShapeDecorator 类的实体装饰类](#创建扩展了-shapedecorator-类的实体装饰类)\n    * [使用 RedShapeDecorator 来装饰 Shape 对象](#使用-redshapedecorator-来装饰-shape-对象)\n    * [执行程序，输出结果](#执行程序输出结果)\n* [参考文章](#参考文章)\n\n# 装饰器模式（装饰者模式）\n装饰器模式允许向一个现有的对象添加新的功能，同时又不改变其结构。这种类型的设计模式属于结构型模式，它是作为现有的类的一个包装。\n\n这种模式创建了一个装饰类，用来包装原有的类，并在保持类方法签名完整性的前提下，提供了额外的功能。\n\n我们通过下面的实例来演示装饰器模式的用法。其中，我们将把一个形状装饰上不同的颜色，同时又不改变形状类。\n\n## 创建一个接口\n```java\npublic interface Shape {\n   void draw();\n}\n```\n## 创建实现接口的实体类\n```java\npublic class Rectangle implements Shape {\n \n   @Override\n   public void draw() {\n      System.out.println(\"Shape: Rectangle\");\n   }\n}\n```\n```java\npublic class Circle implements Shape {\n \n   @Override\n   public void draw() {\n      System.out.println(\"Shape: Circle\");\n   }\n}\n```\n## 创建实现了 Shape 接口的抽象装饰类\n```java\npublic abstract class ShapeDecorator implements Shape {\n   protected Shape decoratedShape;\n \n   public ShapeDecorator(Shape decoratedShape){\n      this.decoratedShape = decoratedShape;\n   }\n \n   public void draw(){\n      decoratedShape.draw();\n   }  \n}\n```\n## 创建扩展了 ShapeDecorator 类的实体装饰类\n```java\npublic class RedShapeDecorator extends ShapeDecorator {\n \n   public RedShapeDecorator(Shape decoratedShape) {\n      super(decoratedShape);     \n   }\n \n   @Override\n   public void draw() {\n      decoratedShape.draw();         \n      setRedBorder(decoratedShape);\n   }\n \n   private void setRedBorder(Shape decoratedShape){\n      System.out.println(\"Border Color: Red\");\n   }\n}\n```\n## 使用 RedShapeDecorator 来装饰 Shape 对象\n```java\npublic class DecoratorPatternDemo {\n   public static void main(String[] args) {\n \n      Shape circle = new Circle();\n      ShapeDecorator redCircle = new RedShapeDecorator(new Circle());\n      ShapeDecorator redRectangle = new RedShapeDecorator(new Rectangle());\n      //Shape redCircle = new RedShapeDecorator(new Circle());\n      //Shape redRectangle = new RedShapeDecorator(new Rectangle());\n      System.out.println(\"Circle with normal border\");\n      circle.draw();\n \n      System.out.println(\"\\nCircle of red border\");\n      redCircle.draw();\n \n      System.out.println(\"\\nRectangle of red border\");\n      redRectangle.draw();\n   }\n}\n```\n## 执行程序，输出结果\n```java\nCircle with normal border\nShape: Circle\n\nCircle of red border\nShape: Circle\nBorder Color: Red\n\nRectangle of red border\nShape: Rectangle\nBorder Color: Red\n```\n# 参考文章\n- https://www.runoob.com/design-pattern/decorator-pattern.html"
  },
  {
    "path": "设计模式/设计模式面试题.md",
    "content": "\n## 观察者模式和发布订阅模式的区别\n//TODO\n\nhttps://zhuanlan.zhihu.com/p/51357583"
  },
  {
    "path": "设计模式/责任链模式.md",
    "content": "责任链模式是一种行为设计模式，使得多个处理者能够在处理请求时形成一个链条。每个处理者都有机会处理请求，但如果当前处理者无法处理，则将请求传递给链中的下一个处理者。\n\n### 责任链模式的结构\n\n1. **Handler（处理者）**：定义处理请求的接口，并包含对下一个处理者的引用。\n2. **ConcreteHandler（具体处理者）**：实现处理请求的具体逻辑，并决定是否处理请求或将其传递给下一个处理者。\n3. **Client（客户端）**：发起请求并将请求发送到责任链的头部。\n\n### 示例\n\n假设我们有一个日志处理系统，日志可以经过不同级别的处理：`InfoHandler`、`WarningHandler`和`ErrorHandler`。\n\n#### 1. 定义处理者接口\n\n```java\nabstract class Logger {\n    public static int INFO = 1;\n    public static int WARNING = 2;\n    public static int ERROR = 3;\n\n    protected int level;\n    protected Logger nextLogger;\n\n    public void setNextLogger(Logger nextLogger) {\n        this.nextLogger = nextLogger;\n    }\n\n    public void logMessage(int level, String message) {\n        if (this.level <= level) {\n            write(message);\n        }\n        if (nextLogger != null) {\n            nextLogger.logMessage(level, message);\n        }\n    }\n\n    protected abstract void write(String message);\n}\n```\n\n#### 2. 具体处理者实现\n\n```java\nclass InfoLogger extends Logger {\n    public InfoLogger() {\n        this.level = Logger.INFO;\n    }\n\n    @Override\n    protected void write(String message) {\n        System.out.println(\"Info: \" + message);\n    }\n}\n\nclass WarningLogger extends Logger {\n    public WarningLogger() {\n        this.level = Logger.WARNING;\n    }\n\n    @Override\n    protected void write(String message) {\n        System.out.println(\"Warning: \" + message);\n    }\n}\n\nclass ErrorLogger extends Logger {\n    public ErrorLogger() {\n        this.level = Logger.ERROR;\n    }\n\n    @Override\n    protected void write(String message) {\n        System.out.println(\"Error: \" + message);\n    }\n}\n```\n\n#### 3. 客户端代码\n\n```java\npublic class ChainPatternDemo {\n    public static void main(String[] args) {\n        Logger infoLogger = new InfoLogger();\n        Logger warningLogger = new WarningLogger();\n        Logger errorLogger = new ErrorLogger();\n\n        // 设置责任链\n        infoLogger.setNextLogger(warningLogger);\n        warningLogger.setNextLogger(errorLogger);\n\n        // 发送不同级别的日志\n        infoLogger.logMessage(Logger.INFO, \"This is an info message.\");\n        infoLogger.logMessage(Logger.WARNING, \"This is a warning message.\");\n        infoLogger.logMessage(Logger.ERROR, \"This is an error message.\");\n    }\n}\n```\n\n### 运行结果\n\n```\nInfo: This is an info message.\nWarning: This is a warning message.\nError: This is an error message.\n```\n\n### 总结\n\n责任链模式将请求的处理者解耦，允许在运行时改变处理链的结构。通过这种方式，可以灵活添加或更换处理者，而无需修改现有的代码结构，非常适合处理可变或动态的请求处理场景。"
  },
  {
    "path": "面试解答/HR会问什么.md",
    "content": "\n* [问题](#问题)\n    * [请简单介绍一下你自己?](#请简单介绍一下你自己)\n    * [你希望通过这份工作获得什么?](#你希望通过这份工作获得什么)\n    * [你个人短期和长期目标分别是什么?](#你个人短期和长期目标分别是什么)\n    * [给你一个任务，你会怎么做?](#给你一个任务你会怎么做)\n    * [你还有什么问题?](#你还有什么问题)\n    * [说说自己的优缺点?](#说说自己的优缺点)\n    * [你为什么要离开前一家公司?](#你为什么要离开前一家公司)\n    * [为什么你希望来我们公司工作?](#为什么你希望来我们公司工作)\n    * [你希望这个职位的薪水是多少?](#你希望这个职位的薪水是多少)\n    * [你将会如何面对你的新工作呢?](#你将会如何面对你的新工作呢)\n    * [你对加班的看法？](#你对加班的看法)\n* [参考文章](#参考文章)\n\n# 问题\n## 请简单介绍一下你自己?\n回答的时间最好在90 秒，最长不超过3 分钟。一般人回答这个问题过于平常，只说姓名、年龄、爱好、工作经验，这些在简历上都有。其实，企业最希望知道的是求职者能否胜任工作，包括：最强的技能、最深入研究的知识领域、个性中最积极的部分、做过的最成功的事，主要的成就等，这些都可以和学习无关，也可以和学习有关，但要突出积极的个性和做事的能力，说得合情合理企业才会相信。企业很重视一个人的礼貌，求职者要尊重考官，在回答每个问题之后都说一句“谢谢”，企业喜欢有礼貌的求职者。\n\n## 你希望通过这份工作获得什么?\n\n对我来说，最重要的是自己所做的工作是否适合我。我的意思是说，这份工作应该能让我发挥专长——这会给我带来一种满足感。我还希望所做的工作能够对我目前的技能水平形成挑战，从而能促使我提升自己。\n\n## 你个人短期和长期目标分别是什么?\n\n不论在长期还是短期，我的个人策略是根据当前目标评价自己所处的位置，然后相应地修改自己的计划。比如，我每五年就制定一项个人计划，这个计划中包含一个总体目标和一系列短期目标。每6 个月我就回顾一下自己的进展，然后做出必要的修改。很明显，我当前的计划就是实现职业转变，也就是找到更满意的工作。除此之外，我已经实现了近期制定的个人目标。\n\n## 给你一个任务，你会怎么做?\n\n“尝试谈谈，你会怎么做。如果做到你会怎么样,如果做不到,你会如何去调整。”\n\n1. 分析项目的优劣点，明确项目的目标，让后开始分析，看自己的角度和领导的想法、产品的方向是否 xian 相契合。这个主要处理问题站的角度不一样，或者利弊分析不一样。所以我首先会站在领导的角度去考虑这个问题，不管是否想的通，我都会向领导交流学习，了解领导为什么这样做，也许是自己考虑不周全等等;\n\n2. 弄清楚后，再阐述自己的想法，后续会怎么做，手段是什么样的。阐述清楚个人观点 和意见，我想这不仅是对我自己负责，也是对我的工作负责;第3. 做这件事后会有什么样的风险?如果达到了预期目标，后续怎么继续维持下去，如果达不到，有应该用什么方式进行弥补?如何调整?\n\n## 你还有什么问题?\n对待这个问题大有讲究，有三方面的注意点。\n\n1. 判断。通常通过整个面试的全过程可以大概地判断出主考方对自己的兴趣，如仔细询问工作经验、反复询问待遇情况、反复了解上下班路途、表情热切等等，可以看出对方的态度是积极的;反之，若三言两语结束面谈、问题不够深入涉及工作，从未涉及薪水待遇，则可以看出对方的态度很消极。如果判断下来对方的态度是积极的，求职者不妨先问一二个问题证实一下自己的判断，反之则只要问一个问题即可，完全是出于礼貌的需要。\n\n2. 应变。要知道回答这样的问话不是很简单的，即便对方态度十分热忱也有可能由于求职者问话不当而造成误解。 一般说来，在用人单位表示出对求职者极大的兴趣的前提下，针对初试、复试的不同情 况可以询问不同的问题。初试时提出的问题最好少涉及薪金、待遇，而应询问有关工作职责、业务范畴之类，使用人单位感受到求职者的敬业精神;而在复试时则可以讨论诸如薪酬福利、交通、培训等同个人利益比较相关的问题。但切记，问到个人待遇方面的问题要谨慎适度，用人单位介绍过的就不必多问，也不要喋喋不休反复问个不停，更不能表现出算帐本领高超，十分精明。\n\n3. 澄清。每个求职者都应当确信通过数次面试下列问题已是心中十分明了的，如果心中无数，则一定要问清。这些问题主要包括：用人单位规模、求职者的职务与职责、技术与设备水准、产品的水平、市场占有率、用人单位的发展目标(即招人的动机)、求职者所处的部门的纵向(上、下级)和横向(其它部门)的关系、薪资待遇、其他福利等。如果求职者尚未搞清上述的全部问题而不提问或只在某一二个问题上反复计较都是不理智的行为。\n\n## 说说自己的优缺点?\n四大经典不足：\n\n1.自己太追求完美 这个不足的提及率差不多最高了，被试者说自己特别追求完美，以至于自己的同事和下级不能忍受，给其他人带来压力，在今后的工作中应该改正云云。现在每听到这个答案，已经有了犯呕的感觉了，更别说对被试会有太多的好感了。被试这样回答，无非是变相夸奖自己，这是“路人皆知”的问题。优秀的被试往往不是这么回答，即使真有这样的不足，考虑到其提及率，应该也尽量回避了。\n\n2. 自己性格急躁 这个不足，乍听起来好像真是个性上的缺点，但听其一解释，又不得不又有点哭笑了。“我性子急，领导布置我工作，本来可以三天完成，我 一天就想干完。今后的工作中应该改正。”把自己夸了个天花乱坠。我知道你是从别处学来的，但这样回答实在是太蹩脚了。\n\n3. 自己学习不够 被试者无论怎么爱看书学习，也说自己学习不够。当追问为什么的时候，说到相对于竞聘的岗位来说，总是学习不够的，“学无止境”吗!持这种答案的人纯粹是为了应付问题，尽管显得浅陋，但却好像没太自夸什么。下面一个就又开始夸奖自己了。\n\n4. 自己不太注意家庭生活 被试者说到自己是个工作狂，整天加班加点，耽误了与 家人在一起的时间，影响了身体和家庭生活。最后还是不忘在今后的工作中应加以注意，但为了工作也没办法。\n\n合理的回答\n\n事先准备三个和求职的岗位相关联的优点进行阐述，至于缺点还是诚实作答为好。 现在给大家先支几招：\n\n1. 如实回答缺点，但要说明你在进步 表述缺点的态度和方法：人非圣贤，孰能无过?但作为求职面试，用人单位更关心的还是你的优点，当然作为求职者，谁也不会想到通过坦陈自己的缺点来寻找理想的职业岗位。正因为这一原因，我想在没有得到主考官提示的情况下，就不必去主动陈述自己的缺点,如果对方问到，那就如实回答。\n\n2. 面试过程中切忌吹嘘自己 吹嘘自己并不是展示自己的优点，而是在附加自己的缺点，这种吹嘘与个人的实际努力南辕北辙。\n\n## 你为什么要离开前一家公司?\n“我希望能获得一份更好的工作，如果机会来临，我会抓住。我觉得目前的工作，已经达到顶峰，即没有升迁机会。”\n\n——【评论】：回答这个问题时一定要小心，就算在前一个工作受到再大的委屈，对公司有多少的怨言，都千万不要表现出来，尤其要避免对公司本身主管的批评，避免面试官的负面情绪及印象。建议此时最好的回答方式是将问题归咎在自己身上，例如觉得工作没有学习发展的空间，自己想在面试工作的相关产业中多加学习，或是前一份工作与自己的生涯规划不合等等，回答的答案最好是积极正面的。\n\n## 为什么你希望来我们公司工作?\n\n研究表明这个公司所做的工作正是你说希望参与的，并且他们做这个工作的方式极大的吸引了你。 面试官试图从中了解你求职的动机、愿望以及对此项工作的态度。 所以建议从行业、 企业和岗位这三个角度来回答。 比如说：你可能说你的研究表明这个公司所做的工作正是你说希望参与的，并且他们做这个工作的方式极大的吸引了你。例如，如果这个公司由于强大的管理而着称，纳闷你的答案可以提到这个事实，并表示你希望成为这个小组的一员。如果这个公司着重强调研发，那么就强调你希望创造你的事物，而你知道这个公司非常鼓励这样的行为。如果这个公司 强调经济控制，你的答案就应该包含对数字的热爱。\n\n## 你希望这个职位的薪水是多少?\n\n事先了解薪水行情，当面试官主动询问时，尽可能的用一个精确的数据来回答。 讨论薪水是一个很微妙的问题。我们建议你在条件允许的情况下尽可能的拖延用一个精确的数据来回答这个问题。你可以说,“我知道这个工作的薪水的大概范围是￥---到￥---。这个对于我来说是合适的价位。”你也可以用一个问题来回答这个问题: “你可能在这个问题上可以帮助我。你能否告诉我在公司中对相似职位的工作的大概薪水是多少?”。 如果你是在一个最初的面试中遇到这个问题,你可以说你觉得你需要更多的了解这个职位的职能才能够回答。在这个问题上通过询问面试官或者人事高级主管或者自己去寻找结果,你可以尝试去得知这份工作是否有一个工资等级。如果有,并且你能够接受,那么直接回答你满意这个薪水范围好了。 如果面试官继续纠缠这个问题的话,你可以说“我现在的薪水是￥--。和其他人一样,我希望能够提升这个数字,但是我主要的兴趣还是在工作本身。”要记住,获取一个新的工作这件事本身不会使得你能够赚到更多的钱。 如果一个猎头公司也参与了此事的话,你的联系人可能可以帮助你解决这个薪水的问题。他甚至可以帮你介入此事。例如,如果他告诉你这个职位的待遇,然后你告诉他你现在已经赚那么多的,并且希望待遇能够适当的提升,他可能会去雇主那然后提议给你增加10%的待遇。 如果没有获得关于这个职位的合适的信息,而面试官还继续这个话题的话,你可以用一个具体的数字来回答这个问题。你不能给别人留下你将会接受任何待遇的印象。不要很快就把你自己卖掉,但是要继续的强调这个工作本身才是你最看重的东西。不要给面试官留下金钱对你来说是唯一重要的事情的感觉。把薪水的问题和工作本身挂钩。尽可能的在你到面试过程的最后一个阶段之前少谈论薪水的问题。到那个阶段的时候,你就应该知道,这个公司对你有很大的兴趣,这个时候在谈论薪水待遇的话就会有很大的余地了。\n\n## 你将会如何面对你的新工作呢?\n\n1. 给自己时间去思考和制定计划。你永远不要在上任的前三个月就去注意问题、优点和缺点。因此，利用这一时间去吸收，并在着手去完成一件事情之前找到新的见解。\n\n2. 建立信任。坦率地说，你应该在每项工作中做这件事情。但你在和对你是否正直，你的政治立场是什么，以及你的忠诚所在尚存疑虑的人打交道。这是一件至关重要的事情，你的新同事想要了解你，所以请确保你把这件事情做好了。转载儒思HR人力资源网\n\n3. 了解你的前辈。他或她有哪些长处?如果还能联系到他们，他们能提供指导吗?如果联系不到他们，谁是他们信任的同事?在高级职位上，重要的是发出这样的信号，你不是个替换者;你就是你自己。但明白你的前任看重什么是明智之举，确保你不会不知不觉地受到挫折。\n\n## 你对加班的看法？\n现在IT公司基本都加班，但是打工人们都是敢怒不敢言，基本上这个问题是在考察你是否吃苦+是否能奉献，问答这个问题可以说\n\n如果是工作需要我会义不容辞加班，比如为了产品能顺利上线，毕竟我要对自己写出的每一行代码负责。但同时，我也会提高工作效率，减少不必要的加班。\n# 参考文章\n- https://zhuanlan.zhihu.com/p/27564382"
  },
  {
    "path": "面试解答/面试解答2021-06.md",
    "content": "# 点击[:rocket::rocket::rocket:]可以看到知识点在哪\n\n---\n\n# 字节腾讯三轮社招面a经（附个人回答）\n\n> 作者： 洗脸高手\n链接：https://www.nowcoder.com/discuss/659590\n来源：牛客网\n\n## 字节\n\n## 1面\n### 1.简单说下项目\n我项目比较挫，就介绍了下，然后项目就跳过了\n### 2.redis的zset是如何实现的？为什么使用跳表不使用别的数据结构？\nredis的zset基于ziplist和dict以及skiplist实现。这里我简单说了下这三个有什么字段，以及优化的细节。至于为什么使用跳表，我直接回答了官方原文。话说回来感觉zset每个大厂都会考啊。\n### 3.redis的持久化方式\nrdb和aof，这里本来我想扩展说一下。面试官直接说时间有限，不用了，希望能在短时间多问点东西。这场面试整体节奏很快，很多都是简单说下就问下一个问题了。\n### 4.redis的集群方式，各有什么特点\n哨兵配主从，可以保证可靠。redis cluster可以增加可扩展性。\n### 5.redis cluster的通信机制，说一下蜂巢\n基于goosip，具体的不了解，这一段就跳过了。\n### 6.mysql innodb 以及mongodb的索引结构\nb+树，简单讲了下。这一题就结束了。\n### 7.rr级别如何防止幻读\n这一题我听错了，听成了rr级别为什么要防止幻读。所以回答偏了。就说因为rc级别不会有 LostUpdate问题，但是rr级别由于mvcc版本会出现，rr作为更高等级级别必须要处理该问题，所以使用锁来处理，但是快照本身不能解决write skew，所以并没有解决幻写，某种角度来说也没有完全解决幻读。后面发现好像回答方向错了，所以还是简单提了下快照读，当前读，间隙锁，行锁之类的东西。这一题讲的有点久了，失误失误。\n### 8.synchronized原理，讲一下底层实现\n一开始我也就标准的说了下锁升级的过程，也说了mutex lock。最后面试官问到比较深，问我moniter里面啥内容，我就记得waitsets，entrylist，owner，count这几个，然后说了下，Java基础就这么结束了。\n### 9.除了zk你还用过其他注册中心吗？之间有什么区别\nnacos，eureka，consol。有的是cp，有的是ap。注册中心一般走ap就够了。简单说了下原因\n### 10.算法\n开始出的是，最小编辑代价。一看是较难，我担心做不出，要求换了一题。换成了二叉树之字形遍历，由于紧张，结果写了10分钟。尴尬的一笔。\n\n## 2面\n### 1.说下项目\n这回面试官比较关注服务器的数量，qps之类的东西。这些我都记得，不过项目整体比较挫。面试官也就不深问了，直接进入八股文环节。\n### 2.会什么语言？说下jvm吧。\n我只会java，哈哈哈。所以面试官就问java了。jvm问的还挺深，什么tlb，逃逸分析，栈帧里面有什么，还好去年看过全都防出去了。\n### 3.说下metaspace和permGen。\n本来信号就不好，还说英文，我听了几遍才知道问的是元空间和永久代。简单说了下。\n### 4.元空间中创建对象会不会开辟物理地址内存\n这tm问的是什么？我题目都不太懂orz\n### 5.system.gc()一定会触发gc吗？和full gc有什么关系？\n不一定。该题回答的比较朴素。哈哈。java就这么结束了\n### 6.redis的aof文件过大怎么处理，怎么解决，开启aof的方式有几种？了解rewrite命令吗？\n由于不了解这一块儿，几乎团灭了。redis还算是我的强项，结果居然败的这么惨\n### 7.kafka。。。\n回答没用过（其实用过，但是好多年前的事情了，所以就直接说没用过，简历上只写了rocketmq。）\n### 8.用过什么mq，说一下rocketmq的消费方式，什么是死信队列\n只用过rocketmq。集群和广播，面试官问只有这两种吗，我说是啊。然后大家一阵尴尬。 死信队列简单讲下就结束了。\n### 9.说一下exchange。。\n回答说我没用rabbitmq（其实我用过）。面试官原来前面听错了，以为我用过的是rabbitmq，吐槽说难怪消费方式回答只有两种。\n### 10.ack机制，offset何时位移，broker复制原理\n作为mq白痴，基本回答的都是浅尝即止。其实我activemq，rabbitmq，rocket，kafka都用过（毕竟跳槽多）。但是mq确实是我的弱项，唉，这一块儿面的我直接心态崩了，当时心想要挂要挂。\n### 11.mysql索引优化没有走对是什么原因\n终于又问我擅长的了。说了下优化器的策略，面试官说不是想问这个，就问我explain，然后我就把explain的字段都说了一遍，面试官说还是理解错了。然后说算了这题跳过，有点小尴尬\n### 12.explain语句会执行sql吗\n不会，优化器就结束了。所以rows会有误差。\n### 13.mysql两段提交\n说了下binlog和redolg以及分组。说实话，问题直接就说两段提交，不熟悉的人可能都不知道问的是啥啊\n### 14.说下undolog，是不是只有rollback才会触发undolog\n基于回滚保证原子性。由于innodb的锁是锁住索引，所以更新主键之类的的时候，原数据也会保留，所以更新后微commit前一条数据会变两条。 在commit的时候会回滚掉原数据，这个场景有点绕，但是我觉得我应该没回答错。\n### 15.算法\n字符串数组中的字符能否拼接成为某个字符。由于不是牛客原题，当时心态又比较崩。所以这题算然不难但是最后没写出来没走通用例orz。\n\n## 3面\n### 1.java创建一个网络io流。会有什么操作，基于java底层或者网络或者操作系统说一下。\n这块我java就简单了下（因为据说字节哪怕面的是java，也很可能做go，所以感觉多少点语言之外的可能好一点）。主要还是说了操作系统和网络。比如ringbuffer，dma，软中断硬中断之类的。\n### 2.零拷贝说一下\n就简单说了下sendfile和mmap。此外提了句senfile比较类似于网络中的对等概念，然后转到了网络\n### 3.说一下ftp和http的区别\n不了解ftp，跳过\n### 4.文件下载一般用什么协议，udp还是tcp\nudp用的多一点，两个都可以。udp主要nat穿透的话打洞容易，这一题过。\n### 5.close wait和time wait有什么区别\n这两个我回答的时候刚好说反了，哈哈哈\n### 6.怎么保持长链接，为什么需要长链接\nkeep-alive。不然每次链接都要创建fd，浪费资源开销。\n### 7.日志海量收集\n开始的时候没听懂问什么就说日志用kafka啊。最后他提示多台机器，我就说mapreduce，简单说了下mapreduce，就结束了。\n### 8.你说的主要是离线处理，如何实现实时的呢\n流处理的话，flink，spark都没用过，所以不强答了（我简历也没写我会大数据啊。怎么问这个orz）\n### 9.算法\n给出一串字符串，写出选取三个字符组合出来的所有可能。这一题是唯一一题白板，不需要运行。（这题看到我就想用dfs加回朔，但是问了下面试官说固定只选三个，我就傻傻的决定写三层for循环，写完后面试官看了一下说你就写了三层循环啊，我说嗯）\n\n## 腾讯\n\n## 1面\n### 1.说一下mysql的悲观锁乐观锁实现\n乐观锁版本号加自旋。悲观锁就是slect for update或者 share mode\n### 2.java里面是怎么实现的呢\n悲观所主要是synchronized，走的操作系统mutex，又讲了一遍锁省级。乐观锁就是cas，底层是cpu指令。又说了下unsafe相关的内容。\n### 3.如何取舍乐观锁悲观锁场景\n当时面试心情好，就说了大实话。场景取舍类似于上厕所。小号就是乐观锁，毕竟排队一会儿就能等到，配合自旋等待就行了，如果使用悲观锁上下文切换（从厕所走到工位）花费的时间不划算。大号就是悲观锁了，毕竟等待的时间一般会很长。二者取舍就预测是否发生竞争，或者竞争的时间长不长。\n### 4.说一下concurrenthashmap怎么实现。1.8比起1.7有什么优化\ncas+synchronized，1.7则为分段锁。另外优化了寻址算法和hash算法（具体说了下两个公式）。（ps，最重要的红黑树没说，不过感觉不要紧啦）然后说了下怎么计算总数。扩容的步骤。\n### 5. concurrenthashmap和hashtable的区别\n一个最终一致，一个强一致。前者读的时候不加锁。 \n### 6.问一下网络吧。三次握手过程，能不能改为两次\n经典八股文。ack，syn，半链接这些说一下就可以了。不能改为两次，如果没有中间消息丢失在没有超时机制的情况下就会产生网络的死锁。（不过终于到了我最擅长的网络啦，之前除了字节三面就没人问我网络，不愧是腾讯，嘻嘻）\n### 7.四次挥手说一下，能不能改为三次。为什么要等待2msl\n挥手过程又是八股文。可以改为三次，只要服务端开启延迟发送，第二第三步就可能变成一步。2msl是为了保证序列号不紊乱，不产生脏数据。顺便说了下序列号怎么产生的。\n### 8.tcp的一些八股文，校验和，滑动窗口，拥塞控制，流控，nagle算法\n又是基础八股文，tcp最大的缺点也就在流控了。流控还要维护整体网络，正常人一旦页面读取失败肯定会疯狂刷新页面啊。怎么可能等待一段时间再刷页面呢。\n### 9.https加密过程\n这一题我说太多了，从seesionid，sessionticket到加密套件的四个字断含义，加密套件只有两个算法需要填写，而只有一个算法需要填具体值。还有服务端是否需要发送证书，客户端是否需要发送证书。不同加密算法 ECDSA和RSA中间的过程有什么不同，讲到一半就感觉很累，不想讲了。越讲越快。\n### 10.ca证书如何验证\n标准八股文，就一般证书sha1加sha256验证证书完整性。还有就是最重要的密码套件中的算法怎么用（一般就是rsa和sha256）。又讲了下证书链，多级证书。还有openssl的pem转crt等等。什么是自签证书（diss了一下12306，哈哈）。到这里网络就结束了\n### 11.说一下redis的数据结构，zset怎么实现的\n基础的5个加布隆过滤器，hyperloglog，geohash之类的。zest和字节一面说的一样。\n### 12.zset怎么实现排名\nspan字段\n### 13.布隆过滤器怎么实现的，能否有删除功能\nbitmap多个hash，增加计数器功能就能一定量实现删除功能（删除这块儿面试官不知道，所以听完有点懵逼，说自己之后在想一下我说的话）\n### 14.数据库的事务隔离级别，各基于什么方式实现\n我三家公司一➕用的orcale一家用的pgsql，一家用的mysql，所以我就老老实实三个数据库全都说了一遍。面试官估计对pgsql的ssi很感兴趣，我赶紧说那个太学术了，我解释起来有点难就糊弄过去了233\n### 15.常见的索引结构\nlsm树——rocksdb，leveldb  ； b树——mysql的innnodb，mongodb ； hash索引 —— mysql的menory，innodb。 倒排索引 —— es，sloar。还有全文索引，r树。\n### 16.mysql的锁，锁的具体使用\n主要就是行锁，表锁，页锁。然后问其他分类呢，我就说x锁，s锁，ix，is。然后又问我别的分类，我就说行锁，间隙锁，邻间锁。在具体的就是innodb我记得有四个锁，名字记不住哈哈。至于sql具体的使用，有个锁升级降级策略，那个太绕了，简单说了下面试官就放过我了。\n### 17.mysql的组件\n八股文，就强调了一句新版本已经没有缓存了。面试官投来赞许的目光，嘻嘻\n### 18.聚集索引说一下\n就索引会存全部数据，按顺序存储之类的。另外聚集一定有唯一性约束。所以不能走change buffer\n### 19.mvcc\n纯八股文\n### 20.说一下b+树吧，为什么innodb不像redis那样使用跳表。\n因为redis几乎是纯内存，不像mysql要刷磁盘，mysql要保证层级尽可能的低，不然影响性能。\n### 21.说一下redis的主从复制过程\n八股文，不过怎么又绕回redis了，汗。\n### 22.一致性哈希算法说一下，具体扩容过程。\n主要解释了下slot就结束了，具体扩容过程不了解。redis到此问完了。\n### 23.说一下java的垃圾回收器吧，有什么优缺点\n说了zgc，香浓多，g1，cms。面试官主要问了g1和cms，然后问了优缺点又问了使用场景，由于很久不java刷面经了，场景这块儿差点没说上来。java到此也就结束了\n### 24。cglib和jdk代理的原理\n前者操作asm字节码，后者是继承proxy类。说了点具体源码，面试官就让过了。\n### 25.算法：大数加法\n三分钟左右写完过了。然后讲了下思路。\n\n## 2面\n### 1.说项目，项目中遇到什么困难，有没有技术突破\n项目很弱，没啥说的。困难都在异地沟通和管理上，技术突破讲了下同事那hashwheeltimer+zk实现的延迟任务。\n### 2.设计微信朋友圈\n基础三张表之外，就是cdn加速啊。微信的南区北区。ssl3.0。lamport算法保证时间啊。三天可见这种放在手机客户端做过滤之类的东西，分组如何实现。反正就是按照自己的想法设计。说了10分钟。\n\n### 3.算法题- 目标和\n三分钟结束\n\n## 3面\n### 1.项目\n就扯了10分钟。\n### 2.算法- 最长的回文子串\n动态规划代码比较多，所以多写了会儿，五分钟左右吧。\n\n\n# 百度java岗 面经（社招）\n\n> 作者：我真的好想拿1个offer\n链接：https://www.nowcoder.com/discuss/667468?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n说一下 jvm\n\n说一下 Java 类加载机制\n\n说一下 volatile 关键字\n\nVolatile 怎么保证可见性和有序性的\n\n单例设计模式里面的懒汉模式的双重校验，为什么要用 volatile 修饰，如果不用 volatile修饰\n\n多个线程去操作，会有什么问题\n\n说一下线程池吧\n\n说一下 https 怎么保证数据的完整性\n\n说一下 redis 的数据类型\n\n说一下 redis 的持久化\n\nRdb 快照会影响目前线程执行任务嘛？（BGSAVE 用子进程操作，不会影响）\n\n说一下 redis 的淘汰策略（LRU）：\n\n你知道 redis 的 lru 怎么实现的嘛？（不太会）\n\n普通的 LRU 底层，双向链表+hashmap\n\n算法题：旋转数组找最小值\n\n将一个链表向右旋转 k 个数\n\n百度二面面经：\n1. 说一下 Java 的特性，封装继承多态，多态是什么？\n2. 说一下 jvm 的内存模型，垃圾回收\n3. redis 怎么设置分布式锁\n4. 说一下数据库索引\n5. 说一下 redis 和 mysql 的区别\n6. redis 为什么快\n7. Java 的锁的实现方式\n8. TCP 和 UDP 的区别，TCP 三次握手\n9. 说一下 DNS 解析服务\n10. 我们现在的视频是采用什么协议进行传输的\n11. HTTP 是无状态的，怎么保持他的状态\n12. 说一下排序算法，1T 文件怎么排序，\n13. 大顶堆或者小顶堆的插入时间复杂度是多少\n14. 10 亿个数字找最大的 500 个（建立一个容量为 500 的小顶堆，每次来一个数字与堆顶的数字比较，如果比他小，就不要，否则就插入） \n15. 说一下四次挥手 \n16. 说一下滑动窗口 \n17. 短连接和长连接了解吗？ \n\n# 字节跳动后台开发岗社招面试题目分享\n\n> 作者：超越妹妹保佑我有offer\n链接：https://www.nowcoder.com/discuss/667474?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n说一下计算机网络的七层模型\n\nTCP 和 UDP 有什么区别\n\n说一下常用的 I/O 模型\n\n说一下 Java 中的多线程和操作系统中的多线程有什么区别（不会）\n\n线程池有哪些参数，如果让你自己实现一个线程池，你会怎么实现（不会）\n\n算法题：大数相乘 大数相乘要求调通，后面上leetcode 才发现自己这道题根本没有做过。。。所以浪费了很多时间，反问环节说算法还有欠缺。\n\nTCP 保证可靠传输的机制\n\n线程进程，怎么进行上下文切换的，进程切换的过程？中断？谁去执行中断呢？\n\n讲一讲内存分页，怎么进行内存分页\n\n100 个 0～1000 的正整数，怎么找到第一个缺失的数\n\n两个链表交叉，怎么找交叉点\n\n已知rand(5)={0,1,2,3,4,5},怎么生成 rand(7)\n\n一个1g 的数，空间复杂度最小找最小的 10 个\n\n一个10g 的文件，内存空间 200M，怎么按照出现次数排序。考虑内存。。\n\n说一下hashmao，线程安全的 hashmap\n\n说一下Java 中的锁是怎么实现的\n\n算法题：\n\n1、找一个数字在非严格递增数组中的位置，比如 234456 找 4 出现的位置返回{2,3}，如果 没有返回{-1,-1}，因为不太确定先用了二分然后用了中心扩展法，被吐槽时间复杂度太高， 后面说了一下单纯二分找左右边界的方法\n\n2、链表翻转的递归算法，不会递归的写了非递归，后面尝试递归失败了\n\n问了很多我的知识盲区，一直在问框架和操作系统，但是真的不怎么会\n\n算法题：k 个一组翻转链表，没有撕出来\n\n计算递归的时间复杂度\n\n说一下OSI 模型，TCP/IP 模型说一下每一层的协议：应用层：HTTP FTP DNS SMTP\n\n传输层UDP/TCP\n\n网络层IP\n\nDNS 是基于传输层的什么协议的？\n\nTCP 和 UDP 的区别？\n\nTCP 怎么保证到达的数据是有序的？\n\n算法题：\n\n1、三数之和\n\n2、有三种不同价格的商品，你需要取其中的 k 件，请写一个函数计算所有可能得到的价格 \n\n\n# 猿辅导 java岗 社招面试的技术题目汇总\n\n> 作者：无敌哥大大\n链接：https://www.nowcoder.com/discuss/667478?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n1. JDK JRE JVM 的区别？\n\nJVM 就是我们常说的 java 虚拟机，它是整个 java 实现跨平台的 最核心的部分，所有的 java\n程序会首先被编译为.class 的类文件，这种类文件可 以在虚拟机上执行，也就是说 class 并\n不直接与机器的操作系统相对应，而是经过虚拟机间接与操作系统交互，由虚拟机将程序解\n释给本地系统执行。\n\nJDK = JRE+Java 开发工具\n\nJRE = JVM+核心类库（libs）\n\nJVM 是用来执行字节码文件的\n\n2. 浏览器输入一个网址会发生什么变化？\n3. 详细说一下DNS 域名解析服务器？DNS 用的是什么协议（UDP）\n4. TCP 三次握手，那四次挥手呢？为什么要四次挥手？\n5. TCP 怎么保证可靠通信？\n\n超时重传和确认机制\n\n6. TCP 和 UDP 的区别？\nTCP 是面向连接的，TCP 传输的是字节流，TCP 只能点对点，保证传输的可靠性\nUPD 不是面向连接的，传输的是报文，可以一对多，多对一，多对多,提供尽最大努力交付 的\n\n7. 进程和线程区别？\n8. Spring 常用注解？\n9. Spring 事务传播机制？（不知道）\n10. Spring 事务隔离级别？（只知道有五种）\n11. OSI 分层模型\n12. TCP/IP 分层模型？\n13. 网络拥塞一定会导致丢包吗？为什么?\n14. ArrayList 中删除值为指定的数，能够直接 remove 吗？会有什么问题（如果直接 remove， 比如remove 了位置为 0 的元素，则后面的元素会前移，之前的 index 为 1 的 index 就变成 了0，所以会有问题）\n\n算法题：\n\n有三台服务器，分别是一定容量，也就是权重，如果来了若干个包，怎么分配让每 个服务器处理的包的数量和权重成正比？可以把权重相加，比如第一个为10，第二个为 20，第三个为 30.那么加起来就是 60.来一个 包，rand 一下产生一个 0-1 之间的小数，再乘以 60.如果落在 0-10 就分配给第一个服务器，\n如果落在10-30 就分配给第二个服务器，如果落在 30-60 就分配给第三个服务器。\n\n就有点大数定律来求概率的意思，当时没有想到这个方法。。。\n\n# 哔哩哔哩 后台开发 社招半年经验面经\n> 作者：无敌锦鲤附体王\n链接：https://www.nowcoder.com/discuss/667481?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 一面（约 42 min）\n1. 聊项目\n2. 事务的 ACID，说一说概念和实现（说了 MySQL 相关的）\n3. 事务的隔离级别说一说，每个级别的问题？MySQL 默认是哪个隔离级别\n4. 除了 Java 还了解什么语言（C/C++）\n5. C/C++ 和 Java 的区别？（有点紧张，居然忘记说自动垃圾回收了 ）\n6. Full GC 的概念？\n7. GC 算法\n8. Java 标记存活对象使用的是什么算法？\n9. 栈和队列有哪些应用场景？\n10. JVM 的堆和栈分别存什么东西？\n11. 一个静态对象（static）的存储位置？\n12. 手撕算法\n13. 二叉树按前序序列化\n14. 数组按层序反序列化（一开始把面试官的意思理解错了，以为这题还是序列化 ）\n15. 反问环节\n16. 我的知识储备还有哪些地方需要补全？\n\n## 二面（约 50 min）\n1. 聊项目\n2. Kafka 如何保证全局有序性？\n3. Kafka 如何保证高可用（扯了一下 Kafka 的副本机制，leader 选举\n4. MySQL 是怎么解决幻读的？\n5. MySQL 索引的底层数据结构？\n6. 哈希索引的局限性有哪些？\n7. InnoDB 为什么用 B+树而不用平衡二叉树？\n8. 如果索引全部加载到内存中，平衡二叉树和 B+树哪个更优？（答了 B+树）\n9. Java 内存模型的概念？\n10. 可见性的概念？\n11. 指令重排序说一说\n12. 缓存一致性模型？（没听说过……）\n13. 常见的 Happens-Before 能说几条吗？（这个真记不得）\n14. GC 算法\n15. G1 收集器和传统的收集器有什么区别？（答了内存划分的区别和选择回收区域的策略）\n16. G1 分代吗？（记错了，答了不分代）\n17. IO 模型说一说，NIO 底层是 OS 什么函数？（epoll）Reactor 模型了解吗？（不了解……）\n18. 手撕算法\n19. 二叉树剪枝：节点值要么 0 要么 1，要求删去全为 0 的子树\n20. LeetCode 221（这题一段时间以前刷过，但是一开始想错了，在面试官提示之下算是写出来了）\n21. 反问环节\n\n# 猿辅导 java岗二面面经 （社招）\n> 作者：锦鲤美女姐姐\n链接：https://www.nowcoder.com/discuss/667487?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 二面：\n\n面试官稍稍迟到了一段时间\n\n1.项目相关，个人项目是在美团点评做的流处理项目，简单讲了一下逻辑和处理超时数据的\n\n方法。\n\n2.开始聊基础知识相关\n\n（1）volatile 关键字（内存可见性，防止指令重排等）\n\n（2）JVM 结构，什么时候会 OOM。\n\n（3）如何在 mysql 里面生成一个事务死锁（感觉自己答得不好）\n\n（4）内存的管理（段式，页式，段页式。楼主这里把页式和段式特点搞错了）\n\n（5）网络模型，介绍下各个层的协议\n\n（6）如何把 UDP 变成可靠的\n\n（7）os 进程通信方式哪几种\n\n（8）kafka 和 rabbitmq 的结构有什么不同\n\n（9）堆栈的区别\n\n（10）场景题，\n\n3.mysql 索引数据结构，事务隔离级别，解决幻读方式\n\n4.redis 分布式锁流程，如何解决缓存雪崩问题\n\n5.dubbo 用到的协议，http 和 dubbo 协议的区别\n\n6.zookeeper 中的节点类型，服务端宕机后 zk 发生的变化\n\n7.http 和 https 区别，握手方式，加密方式，如何加密\n\n8.http2.0 和 http1.0 区别，http2.0 可以推送弹幕消息吗\n\n9.java 线程池，拒绝策略\n\n10.保证线程安全的方式，CAS 优化，AQS，以及 AQS 是否可以实现非公平锁\n\n11.JVM 内存模型，分代收集算法，什么时候分配在栈，什么时候分配在堆，内存泄漏出现\n\n\n\n做题目\n\n1.通过无向图，确认三角形的个数（A-B,A-C,B-C）代表 ABC 可以形成三角形。（感觉自己\n\n算写出来了，可能时间复杂度不理想）\n\n2.借助数组实现小根堆。这个就是我凉的基本原因，还是自己数据结构没有复习透，忘记了\n\n如何实现小根堆，最后没有写出来，很对不起面试官的提示。\n\n# 社招|快手开发岗一二面面试技术题目\n\n> 作者：牛牛牛比哥哥哥\n链接：https://www.nowcoder.com/discuss/667492?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n1. 自我介绍\n2. 做了哪些工作，收获，如果离职了你怎么把你的工作接给下个人，项目文档一般都写什么\n3. 抽象类和接口的区别，什么场景使用抽象类比较好\n4. new Integer(1) 和 Integer.valueOf(1)的区别是什么\n5. 索引是什么，索引为什么快\n6. 主从数据库的好处\n7. Linux 介绍一下，用 Linux 干过什么事，用过哪些指令\n8. 接口设计的幂等性\n9. 说一下springIOC\n10. 介绍一下redis，为什么用 redis，你项目中 redis 用来干什么\n11. redis 数据类型\n12. redis 持久化是什么，持久化的方式都有哪些\n13. redis 的内存淘汰策略（回答 lru，成功埋坑)\n14. http 的状态码都有哪些，5 开头的状态码表示什么\n15. 观察者模式和发布订阅模式的区别\n16. 单例模式都有哪些实现方式\n17. 微服务了解吗\n18. 算法题：\n19. 手撕lru\n20. 对多态的理解。\n21. 多线程怎么搞？Thread，Runnable，线程池。\n22. HTTP Get 与 Post 区别。\n23. HTTPS 聊一下。\n24. 对JVM 的理解。这里谈到了编译的问题。\n25. 聊一聊JVM 的编译怎么搞的？\n26. 聊聊熟悉的垃圾收集器。\n27. MySQL 事务，隔离级别，隔离级别的实现方式。\n28. 进程线程区别。\n29. mysql 处理死锁机制是怎么样的\n30. mysql MVCC\n31. 线程池的参数理解\n32. HashMap\n33. zookeeper 中的节点类型，服务端宕机后 zk 发生的变化\n34. http 和 https 区别，握手方式，加密方式，如何加密\n35. http2.0 和 http1.0 区别，http2.0 可以推送弹幕消息吗\n36. java 线程池，拒绝策略\n37. 保证线程安全的方式，CAS 优化，AQS，以及 AQS 是否可以实现非公平锁\n38. JVM 内存模型，分代收集算法，什么时候分配在栈，什么时候分配在堆，内存泄漏出现的场景\n\n# vivo面试题目分享~java岗 社招\n\n> 作者：中华大哥哥\n链接：https://www.nowcoder.com/discuss/667577?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 一面\n1、HashMap\n\n底层结构，什么时候扩容，为什么负载因子是0.75等\n\n2、ConcurrentHashMap\n\n底层结构，1.7和1.8有什么不同，为什么是线程安全\n\n3、线程池的创建\n\n七个参数，核心线程数和最大线程数根据什么设置\n\n4、MySQL默认的隔离的机制，怎么解决幻读的？\n\n5、Spring中AOP的实现原理\n\n6、一般什么场景用AOP\n\n7、Redis用于排序的数据结构\n\n8、ZSet底层是怎么实现的\n\n9、Redis实现分布式锁\n\n10、setnx和过期时间分开两个语句设置会出现什么问题\n\n11、还了解哪些分布式锁的实现\n\n12、项目的代码结构\n\n各层之间是怎么调用的（一下忘记忘记了），实体类什么时候调用\n\n13、事务怎么用，是用在哪一层\n\n14、深挖项目功能点，遇到什么难处，解决方法是什么\n\n## 二面\n\n1.java 创建线程的方式，runnable和callable 区别（参数不同）1. java线程的状态有哪些；线\n\n程的几种状态\n\n2说一下公平锁和非公平锁的原理；\n\n3锁有哪些\n\n4问了如何保证多线程通信\n\n5 CAS 的原理给我讲一下，他是怎么保证内存的可见性的。CAS会产生什么问题\n\n6多线程如何保持同步？\n\n7linux中如何查看CPU负载top\n\n8.protobuf 了解不，grpc 了解不，用的什么协议，HTTP2和HTTP1 区别，websocket建立连\n\n接过程\n\n9.RPC你了解过吗？\n\n10 排序算法有哪些，简述冒泡和归并排序，冒泡算法的优化,讲讲归并排序,冒泡的优化知道\n\n吗；回答相等不交换，还有flag做已排序标志的优化；直接插入排序，写一下伪代码或者说\n\n一下思路,插入排序，时间复杂度\n\n# vivo 后台开发岗面试经验分享\n\n> 作者：UMRsama\n链接：https://www.nowcoder.com/discuss/667581?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 一面\n\n1 OSI七层和TCP/IP四层\n2 TCP，UDP，HTTP的报文格式（我懵了）\n\n3 TCP，UDP的区别，为什么TCP 是安全的，TCP三次握手四次挥手\n\n4 HTTP的长连接是怎么做的\n\n5 堆排序原理\n\n6 JVM内存分布\n\n7 set，list，map（线程安全的map，map怎么实现之类的）\n\n8 线程创建，线程池参数\n\n9 linux命令\n\n10 分布式事务，CAP定理，有没有使用过相关的产品\n\n11 事务的ACID\n12.一个手机应用要更改数据库，它的底层是怎么实现的？\n13.程序，进程和线程的区别？举例形容。\n\n14.什么时候情况下要用多线程？\n\n15.进程之间怎么通信的？\n\n16.一个手机应用程序里面的进程和线程分别是怎么进行的？\n\n17.快排\n\n18.字母排序\n\n19.每天花在研究方向上的时间？\n\n20.解释一下图像处理高斯滤波小波的原理\n\n21.滤波器原理，卷积核之类的\n\n22.清晰度最高的图片格式哪一种？\n\n23.jpg 格式的编码前十位是什么？\n\n\n## 二面\n1、java线程的状态有哪些；\n\n2、wait和sleep的区别；\n\n3、wait和notify的使用场景；\n\n4、介绍一下volatile以及原理；\n\n5、介绍一下synchornized以及原理；\n\n6、lock和synchornized的区别；\n\n7、介绍一下AQS;\n\n8、说一下公平锁和非公平锁的原理；\n\n9、hashmap为什么线程不安全，如何保证线程安全，就扯到concurrenthashmap\n\n10、concurrenthashmap1.7和1.8的区别；\n\n11、cas操作是什么，以及可能出现的问题；\n\n12、输入一个url后的过程；\n\n13、负载均衡的算法有哪些；\n\n14、聊了一会rpc，让我说一下dubbo的组件有哪些，没说出来。。。\n\n15、redis中zset，说了一下跳跃表的插入，删除过程；\n\n16、说一下线程池，然后你再平时怎么用的，工作原理，有哪些重要参数，饱和策略有哪些；\n\n# 作业帮后台开发岗社招面经\n\n> 作者：秋招加油加油啊\n链接：https://www.nowcoder.com/discuss/667604?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 一面\n上来先是个人介绍和项目经历\n\n1 递归,写一个递归判读字符串是否回文(aba 是回文,abab 不是)\n\n2单链表逆转,写一个吧\n\n3 单链表有环?说说\n\n4 操作系统的东西:进程和线程的关系\n\n5 线程的通信(想让我回答管程之类的,给机会我不中用啊)\n\n6 知道 time_wait 吧,有什么危害?\n\n7 如何避免 time_wait\n\n8 再来个数据结构吧:\n\n9 大数乘积\n\n10.\"12345678987654321\"这种用字符串表示的数字如何做乘法?\n\n11.session和cookies的区别。\n\n12.https和http的区别。\n\n13.Mysql事务隔离级别。\n\n14.sql注入，以及如何防治。\n\n15.springboot中jar包如何部署。\n\n16.JVM的垃圾回收算法。\n\n17.为什么要有包装类。然后就是int和Integer 的一些问题，balabala。。\n\n18.Nginx服务器了解过嘛。\n\n19.说说Mybatis。。。。\n\n20.场景题：Nginx服务器日志，然后去检索某一类请求的次数。\n\n21.场景题：分布式下设计一个id 生成器。\n\n## 二面\n\n1.输入url后发生了什么，然后具体过程各种追问\n\n2.tcp为什么不两次握手\n\n3.dns迭代和递归过程\n\n4.说一下http 的状态码(401 502 504 实在不记得了)\n\n5.说一下流量控制和拥塞控制的具体过程(各种名词记不得了，说的很乱)\n\n6.写一个守护进程，怎么实现？(不会)\n\n7.进程间的通信方式有哪些？\n\n8.linux怎么查找文件？\n\n9.linux文件夹下有很多文件，怎么查找字符串在这些文件的出现的情况\n\n10.grep用在哪里过\n\n11.数据库了解吗？(没怎么了解，没问了)\n\n12.那你觉得你后台技能中最会的是什么？\n\n13.手写快排\n\n14复杂度\n主要的技术问题就是这些了，后面还问了好多关于之前的工作经验有关的问题\n\n# 作业帮 后台开发岗 一面、二面社招面经\n\n> 作者：抠图怪\n链接：https://www.nowcoder.com/discuss/667616?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 一面\n\n我一面二面其实都是主要问项目，基础知识问的不多，把除了项目有关的都列出来了\n\nLinux常见命令，如何查看磁盘情况，如何查看线程？复制一个文件和一个文件夹的命令相\n\n同吗。\n\n登录的token存在哪里比较好？有关同个账号登录的问题\n\n对Redis的了解\n\n算法：合并两个有序列表\n\n如果同时插入相同的东西（比如注册的时候不小心点了两次，就会插入相同的信息）如何避\n\n免重复插入\n\n比如同时有几百个用户同时想数据库插入相同的东西，如何防止不重复插入相同东西浪费时\n\n间？\n\nget和post的区别。在用户体验上get和post的最大区别，为什么大多数情况下都是用get\n\n而非post？什么数据类型只能post传递？\n\n如果出现了无法通过测试用例测出的异常怎么调试？\n\n编程：输入两个日期，算出中间有多少天\n\n\n## 二面\n\n1，自我介绍\n\n2，说说有哪几种数据结构，每一种具体说说在哪能用到\n\n3，说说进程和线程的区别\n\n4，说说进程之间通信，每一种具体说说\n\n5，说说进程的死锁条件，如何解决死锁\n\n6，说说数据库的事物\n\n7，事物的隔离级别\n\n8，了解redis么\n\n9，mysql的两种引擎\n\n10，7层网络协议，每一层都干嘛的\n\n11，说说http协议\n\n12，说说3次握手和4次挥手\n\n13，设计模式用过哪几种，具体说说\n\n14，gc问没问来着，想不起来了\n\n问答环节35分钟左右，自我感觉还可以\n\n第一题输出小于n的所有质数（前n个质数？）\n\n第二题求给定字符串的最长回文子串\n\n# 高途 社招面试经验分享（后台开发岗）\n> 作者：谢谢谢写写写\n链接：https://www.nowcoder.com/discuss/667960?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n1.自我介绍\n\n2.做过哪些项目，给自己印象最深的项目是什么\n\n3.用redis做二级缓存的时候如何确保高并发数据的一致性，如果有一张订单表，我要你找\n\n到对应用户所拥有的订单，怎么找？数据库的几种事务隔离机制是什么，有哪些？有什么用？\n\n4.Str1=“a” str2=new string(“a”)区别，虚拟机内存空间上如何体现这两个区别，这区别具体在开发中会造成什么问题，如何解决？\n\n5.Java和C++在内存处理上有什么区别？虚拟机的常用垃圾回收机制有什么？什么时候会发生OOM错误（内存溢出错误）\n\n6.Redis的基本数据结构是什么？\n\n7.redis如何做持久化的？\n\n8.给你一个场景，设计秒杀系统，假设有10件商品，先用redis去get 数量，数量-1，然后用set更新redis的数据，如果get数据为0就表示商品卖完了，这种情况安全么，有问题的话如何解决\n\n9.redis加锁上锁的命令是什么\n\n10.Linux awk grep命令是什么，如何用正则表达式匹配AxxxxAxxx？（正则还是用的比较少生疏了。。。）\n\n11.讲一下在浏览器输入URL之后到浏览器出现界面的全过程，系统后面用了哪些协议\n\n12.如果你有很多IP地址，如何找到出现次数最多的前三个IP 地址？（hashMap + heap）\n\n13.如果你有一个100G的IP地址文件，你的机器只有5G存储空间，如何找到出现次数最多的前三IP地址？\n\n14.如果一张订单表特别大，你会如何处理这个表格，如何优化它？\n\n15.算法题：字符串切分+反转\n\n16.盲人有10 双袜子，两双黑的，8双白的，如何在没人帮助下找出黑的（在太阳下晒一晒黑色更吸热）\n\n17.你有什么问题想问我的？\n\n# 作业帮后台开发岗社招面经\n> 作者：牛客有嘻哈x\n链接：https://www.nowcoder.com/discuss/668021?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 一面\n闲聊，自我介绍\n\nint和Integer的区别，为什么有了int还需要Integer\n\nArrayList和LinkedList区别，各有什么特点\n\n进程和线程的区别，联系\n\n多线程编程，死锁检测与预防，死锁的检测手段，怎样避免死锁\n\n讲一讲线程池，讲讲为什么很多公司对于线程池的使用非常谨慎\n\nSQL代码书写：有一个学生信息表包含id，学号，选修课程和该课程的成绩，写一个SQL语句来查找总分最高的前十名同学。\n\n建表过程中索引添加的规范\n\nInnoDB的4种事务隔离级别\n\nSSM和Spring Boot 的比较，Spring Boot 的缺点(没答上来，面试官的解释是Spring Boot封装层数过多导致的性能问题)\n\n假如有10 亿个手机号，怎么样快速判断一个手机号是否在其中(一开始没什么好的思路，面试官一步一步从hash，二分，布隆过滤器引导到位图)\n\n机智题：烧完一整根香需要30分钟，怎么样得到15分钟的计时，怎么样得到7.5分钟的计时\n\n可以入职的时间，介不介意转语言，反问\n\n一面大概40分钟左右，更多的都是基础问题，面试官人超级和蔼，没想出来的问题会先给你一些引导，面试体验极佳！\n\n## 二面\n\n聊了聊项目\n\n算法题：把数组中奇数放在前面，偶数放在后面，并且奇数偶数都要保证从小到大，要求空间复杂度O(1)\n\nMySQL索引结构，说说B树和B+树的区别\n\nMySQL索引什么时候失效，联合索引，聚集索引\n\n写一个单例模式\n\nRedis数据结构，场景题\n\n讲讲Java 的堆内存、GC\n\n说说抽象类和接口的区别\n\n浏览器输入URL地址到页面渲染发生的过程\n\n三次握手，为什么要三次握手\n\n算法题：找出数组中第K 大的数\n\n\n# 去哪儿 后台开发岗 社招 面经\n> 作者：青春记录\n链接：https://www.nowcoder.com/discuss/668024?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 一面\n\n1.自我介绍\n\n2.TCP的特点有哪些？\n\n3.详细介绍一下三次握手和四次挥手\n\n4.为什么是三次握手？挥手为什么有四次？\n\n5.四次挥手中，处于time_wait的连接太多该怎么解决\n\n6.介绍操作系统内存管理？逻辑地址和物理地址以及直接使用物理地址会存在什么问题？\n\n7.JVM垃圾回收\n\n8.mysql事务特性？隔离级别？解决了哪些问题？\n\n9.Innodb在RR级别就可以解决幻读的情况，具体是怎么实现的？\n\n10.int和Integer,自动拆箱和装箱底层是怎么实现的？使用包装类型会存在什么样的问题？\n\n11.什么是死锁？死锁的四个必要条件？在开发中，如何去避免死锁？\n\n12.算法题：数字n代表生成括号的对数，请你设计一个函数，用于能够生成所有可能的并\n\n且有效的括号组合。\n\n13.算法题：给定一个包含非负整数的m x n网格，请找出一条从左上角到右下角的路径，\n\n使得路径上的数字总和为最小。\n\n## 二面\n1.先自我介绍\n\n2.JAVA中的并行框架\n\n3.java中的一些集合类\n\n4.java和c++的区别，各自的优缺点\n\n5.c++指针的用法\n\n6.java是值传递还是引用传递\n\n7.mapreduce的执行过程\n\n8.mapreduce如何实现合并操作\n\n9.mapreduce执行时的瓶颈有哪些？\n\n10.任务并行的几种方式\n\n接着让写一个sql查询\n\n日志表log分为id ame time\n\n查询某用户访问过几个不同的网址（name表示网址名）\n\n写完后面试官让优化成同一用户在不同时间点访问同一网址记为两次\n\n算法题：\n\n1.给了一个待排序数据，写出第一次快排后的结果\n\n2.写一个二分查找某数\n\n之后又问了问了解Hadoop技术生态圈吗？\n\n反问环节\n\n# 用友java岗社招一面、二面面试题目\n> 作者：darksidewow\n链接：https://www.nowcoder.com/discuss/668045?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 一面\n1. 自我介绍一下做的项目\n   2.hashmap 源码相关\n\n3.举例说明hashmap1.7 循环依赖的产生\n\n4.arraylist 相关\n\n5.java 异常\n\n6.说说finally 和finalize\n\n7.aop 你怎么使用的aop\n\n8.spring bean 生命周期\n\n9.spring mvc 请求执行流程\n\n10.spring cloud 介绍一下说说降级和熔断\n\n11.数据库三范式\n\n12.sql 场景题\n\n13.数据库引擎\n\n14.innodb 什么情况下使用行锁和表锁\n\n15.tcp 和udp 区别tcp 三次握手\n\n16.shiro 框架\n\n17.多线程\n\nthreadlocal\n\n18.jvm 堆栈本地方法栈\n\n19.java 锁volatile synchornized RentantLock 的实现AQS\n\n20. 有共享变量可以通过什么方式来保证线程安全\n\n21.说一下dcl 单例模式的实现\n\n22. 如何遍历hashmap\n\n23.后面问了一些前端知识jquery，vue\n\n\n## 二面\n\n1.Mybatis 和JDBC 编程对比\n\n2.# 和$符号区别\n\n3.mybatis 动态sql\n\n4.springboot 启动原理？\n\n5.hashmap 底层实现hashcode equals 方法不重写equals 会怎样\n\n6.hashmap put 一个key 的具体过程\n\n7.hashmap 如何遍历输出\n\n8.一个 User 类，不重写equals 和hashcode 方法，那么多个user 对象put 到hashmap 中会怎么样？\n\n9.hashset 中放入1，2，3，第一次遍历，输出的结果是2，1，3，那么再做一次遍历的话，输出结果会是什么？\n\n10.mysql 的有哪些索引？底层数据结构是什么？\n\n11.主键和普通索引的区别？什么是覆盖索引？\n\n12.给定student 表，有两列，一列是school，一列是姓名，如清华大学张三，要求输出两\n\n列school num，其中num 是对应学校的人数\n\n13.name 相同的人怎么统计？如果经常查询school，想要建索引，要怎么建？school 太长的，要怎么建索引更优？\n\n14.排序算法 ，说自己知道的，大概讲一下\n\n# 作业帮、浩鲸 后台开发岗 社招 面经\n> 作者：darksidewow\n链接：https://www.nowcoder.com/discuss/668074?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 作业帮\n1. 自我介绍\n   \n2.int 和Integer 的区别\n\n3.String StringBuffer StringBuilder 区别\n\n4.TCP 和HTTP 的关系\n\n5.GET 和POST 的关系，GET 的长度是因为HTTP 协议限制的吗（查了一下，不关HTTP 的事，\n\n根据不同浏览器的输入决定）\n\n6.GET 跟PUT 的关系\n\n7.Spring IOC(控制反转，解耦)\n\n8.Spring AOP（面向切面），怎么实现的（动态代理）\n\n9.数据库三大范式\n\n10.循环依赖和解决（没回答好）\n\n11.数据库三大范式\n\n12.varchar 和char（可变和不可变）\n\n13.为什么三次握手，不是两次握手\n\n14.数据库隔离等级，为什么可重复读可以解决什么问题，不可以解决什么问题\n\n15.MYSQL 默认隔离等级，可重复读如何解决不可重复读\n\n16.如何看待加班，以前实习有没有遇到一些问题。\n\n17.简单工厂，抽象工厂，工厂方法。\n\n18.如何保证TCP 传输的可靠性（校验和，分割成适合发送的大小，序列号，拥塞控制，流量控制）\n\n19.反问。\n\n\n## 浩鲸\n\n· Spring Bean 的启动流程\n\n· Spring 循环依赖怎么解决？\n\n· Spring 事务用过吗？\n\n· Spring 中的事务具体怎么实现的？事务怎么传播的？\n\n· 项目中用过AOP 吗？AOP 的实现原理？\n\n· 动态字节码技术、字节码增强技术。\n\n· .class 文件加载过程？\n\n· 类加载器有几种？\n\n· 加载器加载流程？\n\n· 为什么采用类加载器这样的机制？\n\n· Tomcat 用过吗(以为要Tomcat 内核剖析了，吓得不轻。)？\n\n· Tomcat 中的类加载器？\n\n· MySQL 事务隔离级别？\n\n· 四个隔离级别的区别？\n\n· MySQL 默认隔离级别是什么，为什么用这个？\n\n· Oracle 默认隔离级别？\n\n· Oracle 和MySQL 的默认隔离级别为什么不一样？\n\n· InnoDB 实现索引的数据结构？\n\n· B+ 树的特点及实现？\n\n· B+ 树和B 树的区别？\n\n· MySQL 中的聚簇索引和非聚簇索引？一张表聚簇索引能有几个？\n\n· 在MySQL 中加一个联合索引(a, b, c)，按(b, c) 查询走索引吗？\n\n· 加一个索引(a)，查询一定走索引吗？\n\n· 有哪些情况会不走索引？\n\n· 代码：二叉树的非递归先序遍历\n\n· 讲解代码思路、在遍历中每个节点被访问几次？\n\n· 垃圾收集算法\n\n· 虚拟机判断对象是否可以被收集？\n\n· 哪些对象可以当做GC Roots？\n\n· 反问\n\n# 用友java前后端社招一面和两面的技术问题汇总\n> 作者：dililidalala\n链接：https://www.nowcoder.com/discuss/668144?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n1.项目的功能\n\n2.ioc\n\n3.aop\n\n4.jdk 动态代理和cglib 动态代理\n\n5.面向切面编程的使用场景\n\n6.aop 当中的声明式事务\n\n7.说一下事务（acid）\n\n8.讲一下隔离性，mysql 的默认隔离级别，讲解一下可重复读（快照读）\n\n9.讲一下自己熟悉的设计模式，讲一下懒汉式单例模式\n\n10.volatile 和sychronized 关键字的作用\n\n11.还了解其他的锁吗，简单介绍一下\n\n12.sychronized 可以修饰什么，可以修饰静态方法和静态代码块吗\n\n13.讲一下乐观锁和悲观锁\n\n14.讲一下线程池的优点，讲一下线程池的分类和使用场景\n\n15.详细讲一下java 的内存模型，每一个都介绍一下用处\n\n16.讲一下java8 了解的新特性\n\n17.lambda 表达式了解嘛，有用过吗\n\n18.springcloud，springboot，微服务有了解吗？\n\n19.final 关键字的用法\n\n20.讲一下redis 的应用场景，主要是用来做什么的\n\n21.java 中的类加载器是什么，讲一下类加载器的分类\n\n22.分布式锁是怎么实现的\n\n23.看过什么书，学习了那些技术\n\n24.数据库优化，sql 优化讲一下\n\n25.什么是组合索引\n\n26.说一下一次完整的http 请求的过程\n\n27. TCP 怎么实现流量控制和拥塞避免\n\n28. TCP 拥塞避免是每次拥塞后发送窗口都自动再从1 开始吗？\n\n29.Java 动态加载过程，什么时候动态加载，底层怎么实现的\n\n30.类加载过程？\n\n31.都有哪些类加载器，分别加载的哪些类？为什么要设置三个类加载器，设置一个的话有\n\n什么问题？\n\n33.Tomcat 怎么实现不同应用的Jar 包不冲突？（从Tomcat 架构回答）\n\n34阻塞和同步的区别\n\n以上就是一面和二面的技术问题了，除了技术题目之外，每次都有 反问的环节，还会深挖项目。\n\n# 支付宝\n> 作者：冰山Alan\n链接：https://www.nowcoder.com/discuss/668256?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n1. 聊项目\n2. 什么程度需要分库分表\n3. 聊聊GC 算法、收集器\n4. 聊聊堆内存\n5. 正在运行的系统，缓存被击穿了怎么办\n6. 如何横向扩展你的系统（结合项目经验）\n7. Mysql为什么使用B+树\n8. HashMap底层\n9. 手写：\n两个线程交替输出自己的线程名，每次输出5次，共输出3次\n给定一个字符串，如何根据首字母排序，首字母一致的按下一个字母，以此类推\n\n> 作者：冰山Alan\n链接：https://www.nowcoder.com/discuss/668253?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n1. 对JVM的理解\n2. JVM的调优参数\n3. 如何查看是哪个GC比较频繁\n4. 频繁YGC怎么调\n5. RebbitMQ的原理\n6. Mysql为什么用B+树不用B树\n7. 聊聊GC\n8. 多个线程之间怎么通信\n9. 索引有什么缺点吗，所谓的维护成本是哪些\n\n# CVTE web后端一面 实习凉经\n> 作者：SadC1ty\n链接：https://www.nowcoder.com/discuss/668446?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n一共面了40min，电话面\n1. 自我介绍\n2. 项目介绍\n3. SpringBoot的特性\n4. AOP 我是直接回答了AspectJ AOP还有Spring AOP\n5. AOP设计模式，描述一下代理模式\n6. 描述一下装饰器模式和代理模式的区别\n7. 为什么使用MyBatis，回答的是MyBatis的优点\n8. 项目细节（所以hxd们一定要掌握好自己的项目哦！ ）\n9. 面向对象，三大特性\n10. Java里的异常和错误；平时使用的工具来排查；\n11. JVM内存模型\n12. 垃圾回收算法；\n13. 了解的垃圾回收器有哪几种；\n14. 多线程的数据同步怎么实现；\n15. 常用的数据库-我答的是MySQL，常用引擎是什么？\n16. 关于索引的了解？\n17. B+树和B树的区别；\n18. 事务是用来解决什么问题？\n19. 事务的特性？\n20. 事务的隔离机制\n21. 加锁的类型有哪些？\n22. TCP和UDP的区别\n23. TCP如何保证可靠性？\n24. HTTP和HTTPS的区别？\n25. HTTPS的加密过程？\n26. 背包问题的解题思路，动态规划体现的思想\n27. 未来的学习计划\n\n# 松果全部面经\n> 作者：zhz白神\n链接：https://www.nowcoder.com/discuss/668636?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 松果出行\n### 一面\n\n1. 时间管理\n2. 任务管理\n3. 怎么去看源码？\n4. 怎么接触一个新的东西？\n5. 996怎么看？\n6. 人生规划\n7. 接下来的学习计划\n8. 如果朋友找你吃饭你去吗？(合理分析)\n9. 反问\n10. 评价\n11. 学软件工程还是去学基础（大佬建议先去培养解决问题的能力，拆分业务的能力）\n12. 怎么去学一个源码？（这个我是想问他的思路）\n13. 怎么去看一本书？\n14. 网忘了\n\n总共48分钟。\n\n### 二面:\nrabbitmq\n1. 丢失消息\n2. 幂等性\n3. 顺序性\n\nredis\n1. 基本数据结构+底层源码实现\n2. 持久化\n3. 缓存击穿/缓存雪崩/缓存穿透/热点缓存key重建优化/缓存与数据库双写不一致\n4. 集群原理（主从）\n5. 全量复制，部分复制\n6. gossip协议\n7. 分布式锁的实现与原理，后面三个都要讲（zk，setnx，redission）\n8. 哨兵的原理与作用\n\n分布式事务实现方案：\n1. mq\n2. seata\n3. tcc-lcn\n\n每一个的原理\n\nmysql：\n1. 调优\n2. innodb与mysiam的区别\n3. B+树和各种树的区别，和为什么选B+？\n4. B+树索引原理\n5. 为什么B+树3层可以容纳2千万数据（操作系统的页方面的知识）\n6. mvcc\n\nJUC：\n1. aqs实现与原理\n2. cas的底层实现（unsafe）与特性\n3. volatile底层与mesi的几种模式，mesi的缺点，缓存一致性，伪共享等\n4. synchronized的原理，直接将源码（对象锁，waitset，entrylist那些），锁升级\n5. 还有什么锁吗=》同步锁\n\nJVM：\n1. 内存区域\n2. 对象创建与分配\n3. 标量更新\n4. CMS的原理\n5. 三色标记法\n6. 漏标解决方案（增量更新+写屏障，（原始快照+写屏障）\n7. TLAB\n8. GCroot\n9. G1的原理\n10. bitmap，cs，rs，ct\n11. 逃逸分析\n\nskywalking的原理 \n\nkafka用过没\n\n反问\n\n1、评价\n\n### 三面：\n1. 讲项目\n2. 问sentinel的原理，有什么用处\n3. seata的原理与用处\n4. dubbo的一致性hash\n5. redis的集群的原理，有什么问题？\n6. spring的生命周期\n7. juc就二面的一些\n8. 自己最熟的框架\n9. es用过没\n10. 反问\n\n1、评价\n三面面试官比较忙，问了20分钟左右吧\n\nHR：\n都是一些没营养的\n\n# 虎牙 java工程师 社招 面经\n> 作者：dililidalala\n链接：https://www.nowcoder.com/discuss/668797?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n自我介绍。\n\n项目中怎么做的服务端分页？\n\n用的mysql 还是 oracle 数据库？\n\n有一条sql 语句执行很慢，如何排查问题？\n\n如何查看sql 执行计划？explain 命令的执行结果介绍下？\n\n索引为什么能加快查询速度？\n\n问几个Java 基础知识吧，Java 虚拟机你了解吗，介绍一下，能说多少说多少？\n\n假设有一个String str = new String(\"hello world\");这条语句创建了几个对象，分别在 JVM 的 哪个区域？\n\n假设项目中需要用到线程同步，你会考虑怎么实现？\n\nJava 中的锁了解吗，介绍一下？\n\n什么是乐观锁、悲观锁，在Java 中分别有哪些实现类？\n\n线程池用过吗？\n\n如何创建线程池？\n\nExecutors 类可以创建哪些线程池？\n\n线程池初始化参数详细介绍？\n\n什么时候会开启核心线程以外的线程？\n\n什么时候会用到拒绝策略？Spring 原码看过吗？\n\n最后问个场景题，现在要查询数据库，数据两位2 千万行，使用多线程实现，你有什么思路\n\n吗？不能重复读取，数据全部读取完之后才进行数据操作。\n\n假设有一个线程查询失败如何处理？\n\nhashmap concurrenthashmap\n\nlist set map\n\njvm 内存模型 垃圾回收等（都是一些常见的问烂的）\n\n进程、线程结合JVM 说\n\nmysql 事务 锁 （间隙锁 next key lock）\n\nmysql 事务 怎么实现的 对应相关的日志来说 undo log\n\nredo log 等\n\nmysql 索引优化 执行计划\n\nspring 中源码看过啥 说了下 spring aop ,ioc springboot 自动装配\n\nredis 项目中怎么用的。。balabal 项目相关问了还蛮多\n\nmysql 中死锁怎么解决\n\n线程池balabala 常用线程池，平时手写线程池，参数配置（7 个参数） 平时怎么设置 的\n\n无反问环节。\n\n# 虎牙后台开发岗面经（社招岗）\n> 作者：萨桑\n链接：https://www.nowcoder.com/discuss/668855?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\nMysql 事务隔离级别，默认隔离级别是哪个\n\n· 为什么设置“可重复读”为默认级别\n\n· Mysql 索引有哪些\n\n· 是不是所有情况必须用索引\n\n· 聚簇索引和非聚簇索引区别\n\n· 聚簇索引可能存在的问题\n\n· Spring 声明式事务\n\n· 说了一个场景，在一个service 方法里，有a()和b()方法，b()方法加了事务注解，a()方法\n\n调用了b()，问调用a()是否走事务\n\n· Java 容器介绍\n\n· HashMap 数据结构，多线程下不安全的原因有哪些，解决策略是什么\n\n· HashTable 和ConcurrentHashMap 区别\n\n消息系统的表设计\n\nHaving 的作用\n\nMySQL 索引我答了B+索引之后，面试官没问什么\n\nSpring 源码看过吗，说一下ioc,aop 吧\n\nhashmap、hashtable 的区别\n\n手撕冒泡排序\n\n数组元素有100 个，冒泡排序排了多少趟\n\n针对hashmap，如果我有10000 个数，你会怎么设计这个entry 数组的初始长度。\n\nConcurrentHashMap 底层数据结构\n\n1T 数据存的是用户访问信息，机器内存只有16G，怎么查出访问频率最高的前1000 个用户信息。\n\n手撕算法：二叉树中序遍历非递归实现\n\n如何设计一个哈希函数\n\nDictionary 知道吗，说一下底层实现\n\n介绍一下登录的原理\n\n浏览器根据什么原理来存储登录的状态（聊了一下COOKIE 和SESSION 的区别）\n\nHTTP 请求的BODY 内容（这个没背下来，下来补了一下）\n\n使用过抓包工具嘛（没有）\n\nSpring MVC 的执行流程\n\n项目对接支付了嘛（没有）\n\nNginx 缓存了解吗\n\n限流降级，有什么办法Hystrix\n\nspringboot 相比spring，有什么改进\n\nspring jar 包运行的原理（怎么找到main 类，MANIFEST.MF）\n\nspring 定时任务\n\n# 招银网络java社招岗一面和二面技术题目汇总\n> 作者：沙宰曼\n链接：https://www.nowcoder.com/discuss/668859?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\nspringboot 自动配置原理\n\nspring 循环依赖\n\nredis 的bitmap,hyper-loglog 用过吗\n\nredis 过期删除策略\n\nredis 的zset 有什么应用场景\n\nTCP 粘包是什么？\n\nTCP 服务端如何保证失序的包按序到达\n\n索引为什么能加快查询效率\n\n为什么HashMap 不用B+树？\n\nHashMap 的查找时间复杂度？\n\nHashmap 链表大于8 时为什么要变红黑树？\n\nGC 是什么时候进行的？\n\nJVM 讲一下，哪些区，都是干什么的，垃圾回收机制\n\n类加载机制\n\nJAVA 的数据引用有哪些 软引用，硬引用\n\n队列，堆栈的底层实现\n\nJAVA 异常的机制讲一下\n\n数据库索引B 数和 B+树的数据结构\n\n聚簇索引非聚簇索引\n\n数据库引擎了解吗，Innodb 和 MylSam 讲一下\n\njava 反射class.forName()加载类和使用classLoader 加载类的区别\n\n（Class.forName 加载类是将类进行了初始化，而ClassLoader 的loadClass 并没有对类进行初始化，只是把类加载到了虚拟机中。）\n\nmysql 索引项目中用了哪些\n\nredis 在秒杀项目中作用\n\n如何保证redis 中数据都是热点数据（内存淘汰机制）\n\nredis 集群\n\nMybatis 与Hibernate 有哪些不同\n\n数据库了解吗？怎么学习的？除了MySQL 还学过什么？\n\n事务隔离级别？可重复度怎么实现的？\n\nvarchar 和char 区别？什么时候使用哪个？\n\njava 怎么学习的？有没有完整看过一套教学视频？jdk 用的什么版本？\n\nlamda 表达式怎么实现的？\n\nSpring IOC 好处？\n\nSpring AOP\n\nMyBatis 的$和#区别\n\n怎么实现分页（分页工具实现，mybatis 实现）\n\n看过什么源码？说一下arraylist。\n\narraylist 和linkedlist 的区别，linkedlist 是单向的还是双向的，为什么这么设计\n\narraylist 是线程安全的吗？如何实现线程安全\n\nthread 源码，线程状态\n\n# 23届上岸实习，总结面经回馈社区 \n> 作者：AlanWilson\n链接：https://www.nowcoder.com/discuss/669220?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 网易面试(2020-11-18)\n### 一面\nRedis五种数据结构底层与C对比\n\nSpringMVC流程，相同url会出现什么问题\n\n会报异常，registerHandlerMethod方法上注解说明url名称相同时会抛出异常\n\n如何保证缓存一致性\n\n先删除缓存，再修改数据库。如果数据库修改失败了，那么数据库中是旧数据，缓存中是空的，那么数据不会不一致。因为读的时候缓存没有，则读数据库中旧数据，然后更新到缓存中。\n\n可以先把“修改DB”的操作放到一个JVM队列，后面读请求过来之后，“更新缓存”的操作也放进同一个JVM队列,每个队列，对于一个作业线程，按照队列的顺序，依次执行相关操作，这样就可以保证“更新缓存”一定是在DB修改之后，以保证数据一致性\n\nBeanFactory和FactoryBean\n\nhappen-before规则\n\n一个线程解锁前对共享变量的写对解锁后其他线程加锁的读可见\n\n线程对volatile变量的写，对其他线程的读可见\n\n线程启动前的写对启动后的读可见\n\n线程结束前对变量的写，对其它线程得知它结束后的读可见（比如其它线程调用 t1.isAlive() 或 t1.join()等待 它结束）\n\ntcp拥塞控制\n\nhttps 对称加密与非对称加密，如何防止第三方攻击\n\nRedis RDB持节化，10GB文件如何处理\n\n微服务理解，服务出故障如何解决\n\nSeiverizableId\n\n这个序列化ID起着关键的作用，它决定着是否能够成功反序列化！简单来说，java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时，JVM会把传来的字节流中的serialVersionUID与本地实体类中的serialVersionUID进行比较，如果相同则认为是一致的，便可以进行反序列化，否则就会报序列化版本不一致的异常。\n\nvoliatle 可见性原理(lock前缀),有序性原理(4个内存屏障区别)\n\naop原理(chlib，jdk)动态代理和静态代理区别\n\n判断回文链表 时间+空间+优化\n\nHashMap原理(7和8)为啥大于8用红黑树，小于8链表\n\nMySQL的InnoDB和MyISAM区别\n\n索引数据结构为啥不用B树\n\n啥时候建索引，索引如何优化(慢日志，explain字段解释)\n\n最左前缀法则\n\n重做日志，回滚日志\n\n事务，隔离级别，MVCC，乐观锁，悲观锁实现，间隙锁\n\n项目\n\n## 滴滴面试(2020-12-31)\n### 一面\nJava基本数据类型\n\nHashMap1.7/1.8\n\njvm内存模型\n\n双亲委派机制\n\n类加载机制\n\nvolatile/CAS原理\n\nAQS机制\n\n多线程工作原理/7大参数/4大拒绝策略\n\n堆分代/GC算法/逃逸分析/GCROOT\n\n对象分配步骤，TLAB\n\nCMS原理 优缺点\n\nMySQL Innodb和MySiam存储引擎区别\n\nMVCC机制\n\n事务4属性/隔离级别\n\nundo/redo\n\n乐观锁实现\n\nRedis String/Hash 数据结构\n\nList/set/arrayList/LinkedList/底层数据结构，优缺点\n\n场景题：连续内存空间 有数组下标，如何定位内存空间\n\n写SQL\n\n算法：反转字符串\n\n问实习时长，多久能来。\n## 二面\n因为实习时长原因已拒，面经就暂时没写\n\n## 百度面试(2021-4.12-4-19)\n### 一面\nHTTP报文基本组成，基于无状态引处cookie和session区别，用户禁用cookie怎么办(答得token)\n\nTCP三握四挥，三握原因，与UDP原因，TCP可靠性原因(握手链接，ack，快速重传，超时重传，流控，拥塞)\n\nTCP与UDP区别\n\n说说对数据结构理解，从数组，链表引申到二叉树，二叉搜索树，红黑树，B+树，B树。用hashmap和B+树和avl树做对比分析。\n\n快排(自己提出写个非递归的)，冒泡，堆排，LC。分析时间复杂度，空间复杂度\n\nRedis五种基本数据结构，底层原理（SDS,hash，跳表)\n\n设计秒杀(分布式锁 Redisson保证释放锁原子性，setnx保证过期时间原子性)\n\nMySQL存储引擎，InnoDB，MyISAM对比，优缺点\n\nInnodb的索引数据结构，聚簇，非聚簇\n\n### 二面(凉)\n400亿文件找数字存不存在(位图)\n\nhttp渲染过程(面试官***)\n\nx的n次方(快速幂，面试官还是***)\n\nhttp状态码\n\n## 网易面试(新闻大数据平台)\n### 一面\n1. 项目(token与session，cookie异同，禁用cookie怎么办,为什么用token)等相关流程问题，MD5单向加密过程，RSA双向加密过程\n2. MySQL索引数据结构，聚簇，非聚簇，二级索引是聚簇还是非聚簇(非聚簇，因为聚簇索引有序排列)\n3. Java基本类型与包装类型，分别存在哪，Java值传递问题(面试官下了一个套说值传递和引用传递，实际上Java只有值传递，引用传的是地址值)，给了个场景，一个方法接受基本类型参数和引用类型参数，在方法内对元素进行了修改问会不会改变原值\n4. Java线程池，阻塞队列，Java的线程和OS线程区别(我回答的对内核级线程的映射)\n5. 场景题:设计一个优先级队列(用的小顶堆，大于堆顶的优先级直接放入堆，删除堆顶元素)\n6. syn泛洪攻击(我说的采用tcp_synccookie解决)\n7. B+树与B树区别\n8. 说说你熟悉的数据结构\n9. 进程间通信\n10. redis跳表数据结构，时间复杂度，为什么不用红黑树，有哪几种类型\n11. bitmap应用场景，布隆过滤器原理，一定能查到元素存在吗(不能，hash会重复)\n12. string类型和c进行对比分析\n13. redis为什么快\n14. 你是如何做MySQL调优的\n15. 内核态与用户态区别\n16. LC写题 提示了一下，直接秒 100% 最优解\n17. 反馈:基础比较好，有点震惊到了我。但是没有hc了，我去帮你争取一下\n\n# 【已oc】美团平台部门后端开发面经\n> 作者：踏歌1998\n链接：https://www.nowcoder.com/discuss/669272?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n具体部门就不说了，用的语言是java（无疑问...），面试官人都挺好的，包括Hr小姐姐，最终还是去了字节，只能说可惜...\n### 一面（100分钟）\n自我介绍\n\n没看我简历，问我要从哪里开始聊起，我说线程池..（埋雷）\n\n线程池\n\n线程池的四组成，七参数\n\n四种常见的线程池\n\ncachedThreadPool的工作原理（我说了线程池的工作流程）\n\n超过keepAliveTime的线程如何了\n\nHashMap\n\nhashmap在jdk1.7和jdk1.8的区别（链表和红黑树、扩容时的头插和尾插）\n\n在多线程下头插法引起的环化\n\n红黑树的数据结构，如何维持相对平衡\n\nconcurrentHashMap线程安全，分段式锁\n\n多线程\n\nReentrantLock和synchronized的区别\n\nReentrantLock的实现（AQS）\n\nAQS中A代表什么，S代表什么（不清楚..我只知道实现原理..）\n\n什么是公平锁，用什么数据结构实现\n\n什么是线程安全\n\nCAS\n\n如何解决ABA问题（增加版本号）\n\n版本号是什么类型的\n\nVolatile是什么，有什么特性（可见性、非原子性、禁止指令重排）\n\nHappen-before原则\n\nJVM\n\nJVM内存模型\n\nCMS和G1的区别\n\n标记清除算法优化？（不就是改进成标记整理吗）\n\n\nMySQL\n\nMysql中有哪些索引（B+树索引、哈希索引）\n\n两者的区别，为什么不使用哈希索引代替B+树索引\n\n范围查询怎么走索引\n\n聚簇索引和非聚簇索引\n\n\n计算机网络\n\n三次握手中为什么进行第三次握手，如果只有两次会出现什么问题\n\n如果服务器端有很多time-wait，可能是什么问题，怎么优化（我答可能是时间设置过长或受到了攻击..前者可以通过优化参数解决）\n\nhttps加密原理（对称加密和非对称加密）\n\nCA证书的作用\n\n\n操作系统\n\n线程、进程和协程的区别\n\n进程之间的通信方式\n\n线程之间的通信方式\n\n场景题：如果CPU占用率过高，怎么查看（linux top指令），可能是出现了什么原因\n\n\nRedis\n\nRedis为什么速度那么快\n\nRedis有哪些数据类型\n\nzset使用的是什么数据结构（跳表）\n\n跳表是怎么实现的（答成多链表层级查找了，正确的应该是在链表上建多个索引查找）\n\n数据库题\n统计出在一天内订单超过2的用户金额，主键是一个自增的值，有userId,amount,starttime,endtime等\n问了springboot做的项目发布了么？\n\n还问了知道RPC框架吗（因为没用过，只能说知道，但是不是很清楚）\n\n算法题\n力扣54.旋转矩阵，要求输出字符串（秒了，面试官问之前是否写过-。-）穿插的问了一下StringBuilder和StringBuffer的区别\n其他\n反问：之前有几个纠结的点希望他能给我解答\n\n闲聊：研一的课程有哪些？研究方向？学java多久了？一周能实习多久？怎么体现你的学习能力很强？\n\n后记：问的比较深，问到我答不上来/答的不准确为止…第二天18:00前发二面邀请\n\n### 二面（60分钟）\n（记的不是很清楚了，全凭回忆）\n\n自我介绍\n\n\njava\n如果两个线程一起操作，如何保证线程安全\n\n问了解哪些原子类\n\ncountdownlatch实现\n\nTry..catch..finally应用在线程出现问题时解决计数器减一问题（与前一问连着）\n\n\n数据库\n数据库索引有哪些，区别（老八股了）\n\nMysql操作会哪些（怕直接出sql题，就说只会一点点简单的..）\n\n事务的ACID分别是什么\n\n事务的四个隔离级别\n\n从读已提交到可重复读是优化了什么\n\n\n数据结构\n数组和链表的区别\n\n链表在java中有哪些实现（LinkedList/ArrayList）,两者区别是什么\n\n\n算法\n口述链表逆序的过程\n\n问了排序知道哪些，按照时间复杂度说了一下\n\n问了快排的时间复杂度和空间复杂度，并口述快排的思路\n\n算法题：力扣78.子集\n\n后记：\n第二天15:00左右 Hr面， oc\n\n一天后发了offer。效率还是很高的….\n\n# 许愿字节三面！！！附后端面经 \n> 者：xyx...\n链接：https://www.nowcoder.com/discuss/669391?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 字节跳动后端\n### 一面\n1. 堆和栈的区别 以及在操作系统中堆和栈的具体使用\n2. Unicode编码 utf-8 utf-8和utf-16的区别\n3. 查询总分成绩为前三的学生姓名和总分\n4. 一个用户多个订单如何设计\n5. http状态码 https ajax json格式 幂等\n6. python字典的底层格式\n7. DNS解析过程\n8. 想要name查询过快怎么办？一级索引和二级索引之间是怎么作用的\n9. python中is和==的区别\n10. 死锁如何避免死锁\n11. 设计一个线程池\n12. 数据库索引以及为什么要用这种索引\n13. https 数据传输中使用什么加密以及为什么要使用这种加密\n14. http中传输图片是怎么传输的 什么情况下用http，什么情况下用https\n15. 304状态码中除了if modified还可以使用什么？\n16. 算法题：计算字符串中的回文子串个数\n\n### 二面\n1. TCP三次握手 TCP为啥不能两次 举个例子\n2. TCP如何保证可靠传输\n3. 进程通信、线程进程区别、什么用线程什么时候用进程（举例子\n4. 进程上下文切换、为啥进程开销大\n5. 中间还有很多细小的点，记不清了，举例子真的举不出来啊（哭、 算法题：给你一个Linux系统里面的文件路径，输出正确路径，如/a/a/./..输出/a 哎，愁死了，许愿一个三面！！！\n\n# 蚂蚁金服后台开发岗一面二面技术问题汇总（社招）\n> 作者：darksidewow\n链接：https://www.nowcoder.com/discuss/669466?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n1.如何让一个线程优雅地退出\n\n2.外部线程调用该线程的 interrupt()方法注意：如果线程处于阻塞状态，只会抛出interrupt 异常，而不会设置中断状态，所以捕获异常后要做相应处理\n\n3、检测到中断状态，作相应处理，（释放资源，或者进行完当前任务）\n\n4.面向对象，你自己是怎么理解的\n\n5、设计模式知道哪些\n\n6、自己在项目中，设计模式使用到的有哪些\n\n7、多线程编程，你的项目中有哪些运用到了\n\n8、mysql 索引，哪些该建索引，哪些不该建索引\n\n9.那你自己在使用的过程中，对索引有哪些考虑呢？\n\n10.服务器端出现CPU100%，如何去排查问题\n\n11.linux 的命令，你自己使用过的有哪些\n\n12.你知道的通信协议有哪些？\n\n13.tcp 和udp 的区别\n\n14.http 和https 的差别\n\n15.说几个你自己知道的加密算法（只让说了名字，没深入问）、\n\n16.如何理解http 和websocket 的区别\n\n17.websocket 持久性连接，连接时间可以好几天\n\n18.http 是客户端发起请求，服务器端应答请求，客户端主动，服务器端被动\n\n19.websocket 服务器端和客户端都能主动发起交易\n\n20.session 和cookie 分别是什么？\n\n21.重要的数据可以放cookie 吗\n\n22.线上问题，你遇到过什么？具体自己是如何去排查问题的？\n\n23.说下乐观锁和悲观锁\n\n24.CAS 中可能出现的ABA 问题讲讲\n\n25.java 和C++的内存管理上有哪些不同\n\n26.垃圾回收算法具体讲一下\n\n27.sql 里面的group by 是什么作用\n\n28.如果你和你同事有意见上的不同，你会怎么去处理\n\n29如果是在工作当中和领导意见不同呢？\n\n# 作业帮后台开发岗面试技术题目汇总（社招）\n\n> 作者：今天一定能赢\n链接：https://www.nowcoder.com/discuss/669487?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n一、机器学习部分\n\n1.机器学习的整个流程是怎样的？\n\n2.如何处理样本中的缺失数据？\n\n3.做过的项目中用到了哪写机器学习算法？有没有遇到过什么问题？是怎么解决的？\n\n4. 决策树有什么问题？怎么解决\n\n5. 说一说随机森林和逻辑回归在分类问题上使用场景有什么不同？\n\n6.SVM 算法推导\n\n核函数是什么？有哪些？\n\n函数间隔和几何间隔有什么区别？\n\n加大数据量一定会提升SVM 的准确率吗？为什么？\n\n7.SVM 和 LR 的区别？\n\n8.逻辑回归为什么选用 sigmoid 函数？多分类怎么做?\n\n9.逻辑回归可以解决非线性分类问题吗？\n\n10.详细解释 L1 和 L2 正则化\n\n11.了解 xgboost 吗？和 gbdt 的区别？\n\n12.平时看过哪些机器学习方面的书籍？是怎么学习机器学习的？\n\n二、JAVA 部分\n\n1.Java 基础\n\nHashMap 的实现原理，为什么不是线程安全的，并发情况下会有什么问题？\n\nConcurrentHashMap 怎么实现线程安全的\n\n类的加载过程？\n\n类加载器有哪些？双亲委派模型？有什么作用？\n\n2.JVM\n\nJava 内存管理模型\n\n垃圾回收算法：CMS，G1\n\n如何判断一个对象是否要被回收\n\n3.Spring\n\nSpring Bean 加载，解决循环引用\n\nSpring AOP 原理\n\nFactoryBean 和 BeanFactory 区别\n\n4.数据库\n\n事务的隔离级别？平时用的是哪种？\n\nMySQL 常用存储引擎，InnoDB 数据是怎么存储的\n\nLeft Join 是怎么执行的\n\n聚簇索引，二级索引，联合索引\n\n怎么判断一个查询走没走索引，like 走索引吗?\n\nHash 可以做索引吗？为什么 InnoDB 不使用 Hash 索引？\n\n如何利用索引提升查询速率(任何优化一个慢查询)\n\n5.MyBatis\n\nMyBatis 执行一个 Select 查询的流程？\n\n有哪些Executor\n\n插件原理\n\n二级缓存机制？6.多线程并发\n\n线程生成方式有哪些？Callable 返回值？\n\nThreadLocal 原理\n\nvolatile 关键字原理\n\n线程池参数\n\n拒绝策略有哪些\n\n同步队列哪几种？区别？\n\n线程通信方式有哪些\n\nSynchronized 和 ReentraintLock 区别\n\n编程实现两个线程循环打印ABABAB\n\n三、数据结构算法\n\n双向链表如何判断有交叉？如何找到交叉点？\n\n# 滴滴Java面试\n> 作者：象牙山首富谢广坤\n链接：https://www.nowcoder.com/discuss/669687?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n没投过滴滴Java ，两点接到面试两点半开始。\n\n面试时间45min 面试官很好\n\n1. 聊项目\n2. 我说了所有功能的思路\n3. 说多端登录一端下线怎么实现的？\n4. 我说了所有遇到的问题和解决办法，还有现在没解决的bug\n5. 说说cookie和session\n6. 我说了我加密cookie的问题\n7. 项目是上线跟人做的吗？ 这里聊了十分钟 我很少让他去问 说自己的思路。\n8. 说说es底层怎么实现的？\n9. 分词 倒排 说了一个例子\n10. 聊基础 说hashmap，我说了线程安全，扩容机制，加载因子，put过程\n11. 他反问 转移机制？\n12. 问我扩容时候有一个要添加进来怎么办？\n13. 1.7和1.8区别\n14. 什么情况从红黑树变成链表？\n15. 聊Redis 让我说持久化 数据结构，我说还说了宕机恢复\n16. 问我kv低层怎么实现的\n17. 跳表知道吗？\n18. 聊MySQL\n19. 我说了索引\n20. 反问 hash索引和b树索引可以手动切换吗？\n21. b和b+什么区别？\n22. spring事务隔离级别？\n23. 平时怎么使用，哪个注解\n24. 聊jvm 我说了full gc 数据区域 gc算法 gcroot\n25. 什么情况被gc判定无用，引用计数优缺点，为什么要分代，元空间和永久代，四种引用\n\n26. 反问 你说了四种引用，那你说说threadlocal 我说了怎么用，底层，干啥的，内存泄漏\n\n27. 问创建线程方法？\n28. 五层模型\n29. 各层都有什么协议干嘛的？\n30. 说说http底层\n31. 说说tcp和udp 用塞避免 \n32. git冲突知道吗？ \n33. Linux问怎么查看一个文件？ \n34. 我说了cat more tail vi vim \n35. 思考题 1000个橘子 放入10个箱子，无论我要多少橘子都可以从箱子的不同排序拿出来怎么办？ 提示二进制\n36. 算法\n37. 说说排序算法有啥\n38. 怎么求两个数组并集\n39. 手撕快排 归并\n40. 时间复杂度？\n41. 说说桶排序 \n42. 反问两个一个亿大小的文件怎么取并集\n\n反问什么想问的\n需要什么样的人！\n他说喜欢钻研源码\n我说我会aqs 然后我说了aqs源码\n反问 队列双向链表还是单向\n双向\n反问我没问你锁呢我问问吧\n我说还是算了\n\n很多问题想不起来来，滴滴面试不求成功，当锻炼面试谈吐和查缺补漏\n\n\n# 万物心选java实习一面面经\n> 作者：cai的抠脚\n链接：https://www.nowcoder.com/discuss/670587?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n求二面！许愿一波\n\n（面试时间：一小时）\n\n1. 自我介绍\n2. 接受转语言吗？PHP\n3. TCP 和UDP区别\n4. TCP如何保证可靠（重传机制，流量控制，拥塞控制）\n5. 传输包丢了怎么办，有哪个字段是验证这个的？ CRC循环冗余校验码？\n6. DNS怎么解析域名的，基于UDP还是TCP，UDP，端口多少53\n7. 本地hosts文件-》本地域名服务器-》根域名-》顶级域名-》二级域名\n8. content-type有哪些类型(没说几个)\n9. 请求头有哪些内容（说了协议版本，请求方式，content-type，content-length）\n10. 进程，线程区别\n11. 进程的状态\n12. 11用户空间和内核空间？\n13. 进程通信方式（匿名管道，有名管道，消息队列，信号，信号量，socket）\n14. 虚拟内存\n15. 什么是线程安全，如何保证\n16. 线程安全的集合类（ConcurrentHashMap，CopyOnWriteArrayList，HashTable）\n17. 数组中删除重复元素\n18. 链表和数组的区别\n19. 快排的原理以及过程\n20. redis常用命令\n21. 一次执行多个命令该怎么做？\n22. redis数据结构有哪几种\n23. redis存验证码？token，怎么存？\n24. 数据库设计了哪些表\n25. 权限控制是怎么做的\n26. 最后问学习上有什么建议？\n27. 基础还行...缺少项目经验。\n\n# 补面经\n\n> 作者：RJMavis\n链接：https://www.nowcoder.com/discuss/671108?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 5.27招银提前批一面\n1. 垃圾处理算法\n2. OOM什么时候会发生\n3. hashmap\n4. hashmap为什么会有链表和红黑树的转换\n5. 如何删除排序树的一个节点\n6. 数据库索引\n7. 数据库隔离级别\n8. 算法题：给定一个数组arr，返回数量超过一半的数字，如果没有则返回-1\n\n## 6.8招银二面\n1. kafka，具体说一下项目中的kafka是怎么用的\n2. kafka会发生信息丢失，怎么解决\n3. kafka如何保证消息不被重复消费（什么什么策略）\n4. 在答kafka的时候答了一波redis，但是面试官不问redis，盯着kafka问\n5. 整体感觉还好吧，基础没问，算法没撕\n6. 就问了十几分钟项目，over\n7. 过了一会等来了hr面\n\n## 6.8 tp link一面\n1. 自我介绍，项目\n2. 数据库遇到海量数据时如何优化\n3. 常用的排序算法以及时间空间复杂度\n4. 为什么用快排\n5. 数据量很大时快排的优势\n\n## 6.9 tp link二面\n1. 自我介绍，介绍项目\n2. 牛客网项目如何解决高并发问题\n3. 20匹马，每次上场5匹，选出跑得最快的三匹，不需要知道三匹之间的快慢顺序，最优算法？\n\n## 6.10字节跳动西瓜视频\n### 一面\n1. 介绍项目，登录功能如何实现\n2. redis怎么提高性能\n3. 操作系统的四种IO模型\n4. redis集群讲一下\n5. redis的数据类型，讲一下Stirng类型的特点\n6. kafka的topic和partition有了解吗\n7. kafka中topic的的消息能保证顺序吗\n8. 算法题：二叉树的右视图\n9. 对折链表相加，比如1-2-3-4-5，输出6-6-3\n10. 整体一面感觉很好，面试官很友善\n11. 面完就让在房间里等一会，五六分钟的样子二面面试官进来了\n\n### 二面\n1. 介绍项目，逮住教研室的项目问\n2. 给你一个IP地址，如何给对应的主机传输信息\n3. 不会IP还是逮着IP问，子网分布什么的\n4. 算法题：对折链表，1-2-3-4-5，输出1-5-2-4-3\n5. 写了用栈实现的，让再写一个常数空间复杂度的\n\n# 浩鲸后台开发面试题目总结\n> 作者：今天一定能赢\n链接：https://www.nowcoder.com/discuss/671194?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n1. 自我介绍，说自己的项目经历和工作经历\n\n2. HashMap（自己说的很细，面试官说了句挺不错，问你几个很细的问题）\n\n3. HashMap1.8 之后有没有循环依赖的问题\n\n4. reHash 那段源码你看没有\n\n5. 默认长度是 16，那我初始大小给 5 他会是多少\n\n6. 长度为什么是 2 的幂次\n\n7. HashMap 翻转链表的时候原来在 3 位置的元素，他会到哪\n\n8. ConcurrentHashMap\n\n9. 说 1.8 它变成了 Node+Cas+Syn 的方式，你讲下 CAS（我顺着 ABA 也说了一遍）\n\n10. ABA 问题是比较经典。但还有两个缺陷你知道吗\n\n11. Synchronized 讲一下\n\n12. Synchronized 原子可见有序的原理\n\n13. Java 对象头里还有哪些东西你记得吗\n\n14. 最近在看什么书\n\n15. 这本书收获最大的地方在哪（排查 OOM 异常）\n\n16. Redis 的数据类型讲一下\n\n17. Mysql 事务隔离级别（幻读是怎么解决的，我记得是加锁，怎么加锁忘了，面试官提醒是阶段锁）\n\n18. JDBC\n\n19. 不关闭连接会发生什么问题（从网络角度来讲，会有大量的 CLOSE_WAIT 状态，因为没有及时的释放资源，从jvm 来讲，可能会造成内存泄漏）\n\n20. 项目里边用的 JWT 是 Java Web Token 的意思吗（Json Web Token）\n\n21 你这个 JWT 在分布式里边会不会出现失效的问题\n\n22. 哦，分布式你不太了解，那这个 token 会不会出现被破解的问题呢。\n\n23 面试官跟我科普了一下现在大厂网络安全的一些做法\n\n24. Spring 简单讲一下\n\n25 Spring AOP 的原理\n\n26 Spring 里边用到了哪些设计模式\n\n27 线程池以及线程池的核心参数，作用28. 四个拒绝策略（29. 反转链表\n\n30. 连续子序列的最大和\n\n# 映客后台开发岗社招面试经验分享！\n> 作者：ce、欢笙\n链接：https://www.nowcoder.com/discuss/671209?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 一面：\n\n一面电话面试，基本常见问题，50min 一直问问题\n\n1. 堆排序\n\n2. 快速排序\n\n3. 单向链表 如何快速得到长度\n\n4. 浏览器请求的过程\n\n5. HTTP 无状态\n\n6. 1.1 和 1.0 区别\n\n7. 讲一下 https\n\n8. 抓包 工具 能否看到 ack 和状态码\n\n9. 网络编程讲一下 socket 同步通信 异步通信 select poll epoll\n\n10.不同语言之前通讯方式（跨语言通信方案） C 和 Java 之间\n\n11.长短连接实现\n\n12.sychronized 静态方法和不同方法的区别\n\n13.死锁线程的方法 互斥资源\n\n14.线程状态\n\n主线程能拿到子线程的执行结果和异常吗\n\n15.常见的集合类 线程安全\n\n\n\n\n\n\n16.自旋锁和互斥锁区别\n\n17.哈希冲突解决方法\n\n18.JVM 内存模型\n\n19.链表和数组的区别\n\n20.数据库 聚集索引和非聚集索引区别\n\n21.一条 sql 语句是怎么执行的\n\n22.隔离级别 一般使用哪种 存在哪些问题\n\n23.数据库 单体部署吗？集群有哪些问？主从复制的问题？ 数据不一致怎\n\n么解决？？\n\n24.中间件有用过吗 ZK？kafka？\n\n25.Redis 如何部署 集群\n\nRDB 和 AOF 介绍\n\n26.ES 索引和文档的区别\n\n分片和副本配置分片缺失对检索有影响吗\n\n27.链表删除指定节点。。。\n\n28.日志文件很多行 rest 接口提供 如何能够快速去对某个字符串进行筛选？？\n\n29.设计模式 命令模式和策略模式。。。\n\n## 二面二面视频面，问题没全记下，感觉面试官层次高了一截，问题也比较发散，而且不太好答，面了一个多小时\n\n1. 树的遍历 两种方法 计算节点的兄弟节点的差\n\n2. 大数据 日志 时间最长的 10 个 url mapred\n\n3. 线程调度 内核态和用户态切换 用户态内的进行线程调度 协程\n\n4. 了解哪些开发框架\n\n共同点响应式编程\n\n5. BIO NIO Tomcat 的作用\n\n6. 反向代理的作用 静态资源具体怎么处理\n\n7. 印象深刻的事情\n\n## 三面\n\n本来说就两面，隔了一周似乎是又加了一面，估计是大boss，自我介绍完简单\n\n问了问简历相关的一些问题，基本都是大方向的问题，之后就开始听大佬讲故事，\n\n大概半个小时\n\n# 映客服务端一面+二面+HR面面经\n\n> 作者：百可\n链接：https://www.nowcoder.com/discuss/671563?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 一面\n1. 输入一个网址，出现页面的全过程\n2. http的请求方法有哪些\n3. get与post的区别\n4. 进程与线程的区别\n5. 线程共享的有什么，不共享的有什么\n6. 死锁的必要条件\n7. 死锁的预防\n8. hashmap的原理\n9. LRU缓存的实现\n10. mysql的锁\n11. 事务的隔离有哪些\n12. 说一下每个隔离的区别\n13. mysql默认使用哪个隔离\n14. 日常应用中应该使用已提交读还是可重复读\n15. 用什么语句实现数据库锁\n16. 数据库使用b+树的好处\n17. 联合索引的使用原则\n18. redis 用过？kafka用过吗？（没有，没有）\n19. 编程：两个栈实现一个队列。\n\n## 映客二面50min\n\n1. new delete malloc free\n2. delete delete []\n3. 一个对象数组，不用delete[]使用delete有什么影响。\n4. const与define\n5. cpp的面向对象\n6. 重载与重写\n7. 纯虚函数\n8. 虚函数的原理\n9. 结构体与联合体的区别\n10. 修改联合体的多个值有什么影响\n11. 进程线程协程\n12. Io模型\n13. io多路复用的类别\n14. epoll的底层实现\n15. mysql的存储引擎\n16. innodb与myisam的区别\n17. innodb的索引有哪些\n18. 事务是什么\n19. 如果一个表查询，插入等很慢，你怎么做？\n20. git 的本地仓库和远程仓库\n\n后来查的：在本地使用git init 建立了一个仓库 A，就是本地仓库\n    在github 上建立的仓库就是远程仓库\n\n21. JMM\n22. 内存溢出遇到过吗\n23. 线程池的创建流程是怎样的（线程池的实现原理）\n24. CMS 垃圾回收器\n\n\n## 映客hr面10多min\n\n1.自我介绍\n\n2.说下具体的事情，印证自己的评价。3.转语言能接受？\n\n4.找工作toB toC考虑这个？\n\n5.目前offer有吗\n\n6.期望薪资？\n\n7.反问\n\n# 滴滴秋储java后端实习一面二面（凉了但没完全凉）\n\n> 作者：coder_tq\n链接：https://www.nowcoder.com/discuss/671754?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 6.2日 一面（40min）\n1. 项目（10min）\n2. equals与==\n3. Integer之间==\n4. Integer如何放到常量池\n5. volatile说一说\n6. 可见性有什么作用。\n7. 如果java内存模型中去掉了线程内存，直接修改主内存会出现什么情况。\n8. 单例模式说一说\n9. 实现方式，区别\n10. 枚举是如何防止反射破坏单例模式的\n11. 动态代理模式说一说\n12. JDK和cglib动态代理区别\n13. CAS说一说 使用场景\n14. TCP三次握手 两次握手行不行，为什么\n15. 滑动窗口（没答上）\n16. linux相关\n17. 基础指令有什么\n18. vim的缺点？ 会将文件全部加载入内存中\n19. CPU突然飙高怎么检查（答成内存飙高的检查方式了。。。）\n20. 支付业务，客户不小心点了两次，发送两次请求，如何判断。（服务器生成序列号放入redis，发送请求后删除）\n21. 如何应对高并发的秒杀？ 算法 令牌桶\n22. 怎么判断服务可能出现异常，通过什么数值可以监测\n23. 算法 二叉树前序遍历 字符串的倒叙\n\n反问\n\n## 6.7日二面\n1. 自我介绍\n2. 项目\n3. 数据库怎么设计的 用到了什么范式（范式记不太清就说了说设计）\n4. 外连接内连接笛卡尔积\n5. 在哪里用到了这几种连接\n6. 注解是什么，如何进行处理的，让你实现一个注解你怎么写\n7. 单例模式 你怎么写，如果通过反射有没有可能破坏单例模式，该怎么处理\n8. 项目中用到了哪些设计模式\n9. 进程之间的锁（说成了线程间的）你知道进程和线程的区别嘛\n10. 了解过文件锁嘛\n11. 访问www.baidu.com的过程\n12. 两个进程能不能同时监听同一个端口，UDP和TCP的同一个端口呢？\n13. 等通知，然后就没有消息了。。。\n\n6.15收到oc，不得不说滴滴流程有一点点慢。。。\n\n# 浦发银行软件开发岗面试经验分享（社招）\n\n> 作者：牛客895370106号\n链接：https://www.nowcoder.com/discuss/671771?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 浦发银行\n1. 学过的语言？C、C++、Java\n2. String和StringBuilder的区别？final、拼接效率\n面试官：还有吗？--没看过StringBuilder源码\n3. TreeSet底层？红黑树复杂度？（八股文）\n4. HashSet 底层？HashMapa 的复杂度？Hash冲突解决方案？（八股文）\n5. 如果key是String，那它的hash值是怎么得到的？\n面试官：我是问你String的hashCode是怎么计算的？\n思考了片刻，老实交代：我没看过这个源码，但它应该是和ASCII码有关\n6. 平时怎么调试？Debug打断点\n7. 还有吗？线上出问题如何定位或调试？分析日志文件\n8. 10000个数找最大的100个？秒给小顶堆、TopK思路\n9. 小顶堆思路的时间复杂度？o(N)？因为要遍历\n10. 面试官：那如果是找1000个呢？应该还是o(N)吧...\n11. 那维护堆的代价呢，你再计算一遍？我猜o(NlogN)，应该是错的，胡乱分析一通\n12. 平时写项目遇到死循环怎么解决？郁闷了好久...实在想不出来，随后说，死锁这种情\n况可以吗？\n控制台输入jps+jstack指令\n11、那像for、while这种死循环你怎么定位？\n12、手头有offer吗？来深圳没什么问题吧？我待会儿把你的简历给我leader看一下\n13、反问：您如何定位死循环？\n\n## 高途\n\n1. 单例模式\n2. ArrayList和LinkedList的区别\n3. 创建线程的几种方式\n4. 线程池的几个核心参数和执行流程\n5. 线程池是怎么控制核心线程数和最大线程数的（答得不太好，瞎扯了队列）\n6. JUC包下了解什么工具（我答了concurrentHashMap），接着问了concurrentHashMap的分段锁和使用场景\n7. Atomic和CAS\n8. Spring中使用的设计模式（我只答出了工厂BeanFactory和代理AOP）\n9. Spring中的事务的隔离级别和传播特性（传播特性没答全）\n10. SQL题，三张表：部门、员工、工资，查询某个部门2019年年薪超过12万的员工\n11. 手撕代码，二叉树的前序遍历\n12. JVM -Xss参数表示什么（不知道）\n13. 死锁产生的条件，怎么解决死锁（只知道粗粒度锁和设置锁优先级）\n14. 场景题：在一个服务中调用另一个服务的接口保证数据一致性，例如：提交订单不会提交两次（分布式锁？其他不清楚了）\n15. 场景题：高并发场景下数据库压力很大，怎么解决？（使用索引，使用缓存，分库分表？当然还存在IO瓶颈）后来面试官又提出了MQ 消息队列也行。\n\n# 金山云软件开发社招岗面筋\n> 作者：牛客541601154号\n链接：https://www.nowcoder.com/discuss/671775?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 一面面经：\n\n1. 先自我介绍然后讲一下最拿得出手的一个项目（我讲了我做的区块链fabric的农产品溯源项目）不知不觉聊了将近30分钟\n2. Java多态机制\n3. 策略模式\n4. 线程池，假如让你实现一个线程池，你会怎么实现？（我使用list存储核心线程，然后不够再扩容，用完了经过最大等待时间再回收......）\n面试官说其实数据结构用数组、链表都可以，有空多看看源码的设计思路\n5. 线程池的核心参数\n6. Netty了解吗？（没用过，只知道用了基于buffer的非阻塞的NIO底层实现）\n7. NIO底层实现\n8. TCP和UDP的区别\n9. TCP和UDP的首部报文格式差异\n10. TCP三次握手和四次挥手\n11. SQL题：订单表的订单号没建唯一索引有大量重复的订单号，怎么去重？\n12. SQL 题：学生课程成绩表（student_id,course_id,score），查询所有课程成绩都>60分的学生的student_id\n13. 算法题：从1亿个数中找出最大的100个（我说了堆排序，建大堆100次。然后面试官问一定要排序吗？不排序可以吗？）\n\n（后来想了，遍历一遍，准备100的变量存储最大100个数的下标，遍历每一个数的同时跟那100 个小标的数比较）\n\n\n## 二面\n\n1. TCP 三次握手,四次挥手,Time_wait状态过多的优化\n2. 说说select,epoll\n3. Mysql的默认隔离级别？什么是不可重复读\n4. select语句的执行过程\n5. select poll epoll 的区别？epoll的数据结构\n6. TCP三次连接，这个listen backLog有什么作用\n7. TCP 四次挥手，Time WAIT发生在哪方？两个超时重传时间的作用？大量timeout怎么处理？\n8. https的加密过程？证书认证的过程\n9. 进程间的通信方式？有哪些信号通信是不可靠的？为什么是不可靠的？\n10. 线程死锁的情况，怎么解决？\n11. 1 2 35 5 35 2 56 统计2 35出现的次数，按序排序\n12. 请求出现超时，但应用查不到这个请求日志，怎么排查\n13. 僵尸进程怎么解决？协程有了解过嘛\n\n反问:第一次面试表现怎么样:计网一般般,后面还可以\n\n# 腾讯PCG QQ客户端一面面经（凉经）\n\n> 作者：RookieProgrammer\n链接：https://www.nowcoder.com/discuss/671881?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n日期：2021年03月22日\n\n## 八股部分\n1. 询问项目 10~20min\n2. 比较Vector 与ArrayList\n3. 比较ArrayList与linkedlst\n4. Java有无符号类型吗？网络上传来一个64位可以用long接吗（可以但会溢出）\n5. Object里都有什么方法，equals，clone，wait，notify、hashcode、toString，讲下他们的作用\n6. 深拷贝与浅拷贝\n7. Java有内存泄露吗\n8. 强弱引用是什么，区别在哪，使用场景\n9. 反射是什么，哪些框架会用到\n10. Spring的AOP、IOC是什么\n11. 进程间的通信方式哪几种、\n12. 平衡二叉树是什么？用普通二叉搜索树不行吗？\n13. TCP、UDP讲一下（三次握手、拥塞控制）\n## 手撕代码部分：\n1. problem1：无序数组的中位数（排序、大顶堆小顶堆）\n2. problem2：两个100000位的整数，计算相加和（有时间考虑有符号情况）\n\n# 腾讯TEG 一、二面面经分享（已拿offer）\n\n> 作者：RookieProgrammer\n链接：https://www.nowcoder.com/discuss/671884?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 腾讯TEG后台一面\n日期：2021年03月29日\n\n### 基础知识与项目\n1. 询问项目\n2. 除了时延， 还有什么可以刻画分布式系统的吞吐\n3. 分布式系统负载均衡，需要注意哪些指标\n4. 计算机哪些资源参与负载均衡算法的计算（部署位置）（因为区块链是一种分布式数据库，所以面试官才这么问）\n5. 分布式系统有了解吗\n6. 设计分布式系统最大的挑战是什么（CAP, BASE\n7. 常用哪些编程语言\n8. Java做过项目吗，C/C++了解过吗，为什么学java而不是C++或Go\n9. Java相对于C++, golang的优势\n10. HashMap与HashTable比较\n11. 红黑树查询的复杂度logN，动态增删复杂度logN\n12. 红黑树相对于一般二叉树的特点，相对于平衡二叉树的特点\n13. 平衡二叉树做查询logN、增删的复杂度N(恰好删除根节点，二叉树重建)（查询、增删复杂度很容易被问）\n14. HashMap线程安全吗(不)，应该用ConcurrentHashMap，原因是什么(CAS, 1.8之前怎么实现的)\n15. Java多线程的实现方法(四种)\n16. 为什么用线程池\n17. 资源占用会不会很高\n18. 参加过什么竞赛、项目\n19. 网络I/O, I/O模型(阻塞、非阻塞)\n20. 为什么非阻塞并发度更高 + I/O过程描述,有几次拷贝\n21. 进程、线程区别\n22. 为什么进程是程序运行的基本单位，而线程不是? (linux中 进程、线程实现几乎一样，调度也相同，主要区别在于二者内存使用上的不同)\n23. 多进程的通信手段 (其中一种是 信号 or 信号量?)\n24. 线程间通信方式\n25. 进程间通信，线程间同步，通信与同步的区别\n26. 进程也可以同步，为什么线程没有通信\n27. 程序内存空间分为哪几块\n28. static的作用\n29. 堆栈的区别，用来保存什么\n30. HTTPS对于HTTP的改进\n31. HTTPS的过程\n32. 客户端怎么验证服务端的签名\n33. 用UDP怎么实现TCP\n34. 有哪些排序算法，哪些是稳定的，如何衡量稳定性\n\n### 手撕代码：\n1. 十亿个整数，找出最小的十个\n\n快排不行，内存不够\n内存不够 → 小顶堆(实现过程怎么样)\n为什么小顶堆可以，快排不行(因为递归吗)\n简化一些，有很多的机器，用快排怎么做\n每个机器排序一部分，找出最小十个\n然后取出最小值最小的十台机器进行合并\n给一堆数，比如1-9，任意取3个数进行排列组合，输出所有可能(数字可重复)\n\n## 0406 腾讯TEG二面\n1. 什么是同态加密\n2. 一些项目相关的东西\n3. a. PBFT, CFT\nb. 比特币、以太坊、fabric的吞吐\n4. 实现分布式一致的方式：\n5. 通信交互\n6. 时钟同步（谷歌一篇论文，分布式存储数据库），难做到强一致\n\n# 七牛云等公司java岗社招基础题目汇总\n\n> 作者：蓝的天白的云\n链接：https://www.nowcoder.com/discuss/671889?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n楼主是社招菜鸡，最近想换个环境，面试了一些公司，然后总结了下被问道的基础技术问题，题目就不分公司写了，全写一起好了\n\n1. string和stringbuffer和stringbuilder的区别\n2. concurrenthashmap 了解多少，1.7和1.8区别\n3. 分段锁说一说\n4. CAS知道吗，介绍一下\n5. hashmap 底层数据结构，jdk1.8之前和jdk1.8\n6. hashmap为什么链表超过8转化为红黑树？\n7. 介绍一下线程池，如果自己创建一个线程池，线程池包括哪些参数\n8. 多线程start和run方法的区别\n9. 抽象类和接口的区别，为什么要有抽象类？\n10. arraylist和linkedlist区别，是不是线程安全\n11. jvm了解多少，重要！！【垃圾回收机制】【类加载机制】 追问：双亲委派、jvm 内存模型和内存结构、minor gc和full gc\n12. mysql创建索引的注意事项\n13. redis 怎么样、缓存类中间件用过哪些？kafka和rocketMQ了解多少\n14. 自动装箱int和integer比较\n15. try catch执行顺序中间插入return，执行顺序\n16. 多态的理解 三个条件，继承、重写、向上转型（父类引用指向子类对象）\n17. 手写单例模式， 懒汉式、饿汉式 追加：如何保证线程安全\n18. arraylist和linkedlist区别， 追问：两个都是不是线程安全的\n19. arraylist底层为啥线程不安全， 答：并发情况下size++不安全考虑并发要用vector\n20. 手写SQL查找成绩总和top3的学生\n21. 创建线程的几种方法，都介绍一下\n22. 三层的B+树可以存多少信息，页表自己定义，节点大小自己估算 上亿级别，按页表16KB 算，long占4 个字节，16KB/4B = 4K 4K × 4K × 4K = 6.4×10^1021.B树和B+树介绍一下，说说区别\n23. 死锁四大条件\n24. 如何避免死锁\n25. 银行家算法（预防死锁的方法）\n26. linux会哪些命令，介绍10个\n27. socket编程了解多少\n28. 聚簇索引和非聚簇索引\n29. linux文件系统\n30. 你知道哪些文件系统，说说结构\n31. http是有状态还是无状态？是有连接还是无连接？ 无状态、无连接\n\n# 七牛云 软件开发岗 社招面经\n\n> 作者：AreUReady?\n链接：https://www.nowcoder.com/discuss/672035?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 计算机网络\n1. TCP四次挥手\n2. OSI 5层，发送邮件属于哪一层，基于什么协议（传输层协议），为什么\n3. TCP与UDP区别\n4. 拥塞控制\n5. HTTP响应码\n6. 发生502，应该先查看什么，发生500应该先查看什么\n7. 顺着这个问题问我了解Linux操作吗（了解过一些常用命令）\n8. 说一下，给一个单词，查看一个文件的第一行有几个这个单词，怎么实现（不会shell......，说了下可能会用到的命令，以及大致思路）\n9. 然后问了下平时开发怎么测试的\n## 数据结构\n1. 问了常用的数据结构，说了栈、队列、数组、树，\n2. 然后问了下链表和数组的区别\n3. 有了解过排序吗，说说哪些排序是稳定的，哪些是不稳定的，时间复杂度是多少，空间复杂度\n4. 手写快排，说说思路\n5. 刚刚说了树，说一下树的应用场景（说了索引）\n## 数据库\n1. MySQL除了树，是什么树，还有哪些类型的索引\n2. 说说事务的隔离级别\n3. 什么是幻读\n4. 项目表结构\n5. 说说表的对应关系（一对多、多对一之类的）\n## Java\n1. HashMap，JDK 7与JDK 8做了哪些改进，解决了什么问题\n2. 如何解决多线程问题，你用过哪些锁\n3. sync与ReentrantLock区别\n4. 线程池用过吗，有哪几种\n5. 如何停止一个线程，会发生什么事情\n6. 碰到过哪些异常，OOM可以被捕获吗\n7. 反射了解吗\n8. 注解了解吗\n## Redis\n1. 用过Redis吗，用过缓存\n2. Redis底层数据结构了解过吗？说了字典和跳表\n3. 跳表和红黑树\n4. 问答环节问知道Redis分布式锁吗（说了setnx），问我看过什么开源框架吗，了解过\n## docker 、k8s吗\n\n场景题：统计视频直播一天中哪个时间段人数最多\n\n# 暑期实习面经\n\n> 作者：断剑孤雪\n链接：https://www.nowcoder.com/discuss/672060?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n暑期实习面经：\n这是一篇迟到的面经，因为面完后比较忙一直没有时间整理，凭借自己脑中的印象回忆实习面试题用来回馈牛客\n\n## 字节飞书后端\n### 一面：\n1. 八大排序各自的复杂度，稳定性等\n2. 计算机的虚拟内存？\n#### 算法题部分：\n1. 打印1-100 的质数\n2. 寻找数组仅出现过一次的数，其他数都出现了两次\n3. 股票问题|  ||  ||| (1,2 ,3  从easy  到hard)\n4.  场景题： 如何在10GB 的日志中找到一个出现两次的日志记录\n\n### 二面：\n#### 计算机网络：\n1. DNS 污染，DNS 劫持 ， 泛洪攻击\n2. 三次握手和四次挥手，timewait 状态\n3. Mac 的作用，NAT的作用\n\n#### 操作系统：\n1. 进程间通信方式\n2. 虚拟内存\n3. 切换进程的上下文\n\n#### 场景问题：\n1. 设计一个用户登录页面, 保证用户长时间保存登录状态需要做哪些\n2. 浏览器的cookie 问题\n\n#### redis 和 内存的区别和联系\n\n#### 算法题：\n\n已写在leetcode\n\nhttps://leetcode-cn.com/circle/discuss/rRusa7/\n\n## 阿里java ：\n### 一面：\n1. 问了问学科竞赛，建模竞赛的有关事宜，之后深挖项目\n\n之后进入基础部分：\n2. redis  的数据结构\n3. redis  的跳表底层实现复杂度等等\n4. redis为何用跳表而不用红黑树\n这里因为我此前也疑惑过这个问题，所以专门去看了看作者的回答\n\n5. 各大排序算法以及复杂度和最坏以及最好的情况进行具体的栗子讲解\n6.  Array.sort 的底层中，为何一部分用快排而不用堆排序，工业界中为啥用快排多于堆排\n\n#### 计算机网络部分：\n1. 三次握手，四次挥手 （四次挥手有可能三次嘛？还是一定要四次）\n2. 这里确实学到了当三次挥手已经进行数据传输完成后，就可以直接断开无需四次挥手\n\n3.  ping 解释如何实现互通\n\n最后： 最近有看什么书？\n\n### 二面：\n1. 深挖项目一波，（感觉被打击到了）\n2. redis  集群部署情况？\n3. 简要叙述kafka 如何实现生产者和消费者\n4. 各大排序算法的最好最坏，稳定性以及原因\n5. redis 会出现因为多线程而造成数据读写错误嘛\n\n这里我主要用说redis 用了单线程不会有这种情况，但是父线程会fork 一个子线程来进行操作备份\n\n6. redis为何快\n7. 你之前说redis6.0 采用了多线程，你觉得为何要这么改？\n为了应对越来越复杂的qps 要求，redis作出了让步\n但是redis6.0 默认是禁止多线程的，可以在配置中打开，选择权给了用户\n\n#### 计算机网络\n\n1. udp  和 tcp  部分\n\n## 拼多多后台：\n### 一面：\n1. 排序算法部分\n2. 项目部分（还是深挖了不少）\n3. 计算机网络，拥塞控制，三次握手，长短连接等\n4. redis部分   现场给场景，现在写redis 查询代码（这点不会）\n5. 用栈实现队列  用队列实现栈\n6. 算法题：\n7. 数组中取三个数积最大 \n8. 合并括号（十分钟写完）\n\nnotes： 第4部分不会写原本认为挂了，过两天约二面\n\n### 二面：\n1. 输入www.baidu.com  过程\n2.  https  的具体过程,特别是加密\n3.  算法题部分\n这一部分把我问懵了，一道接着一道（大概有4,5 道）\n写出来了之后优化，优化之后分析时间复杂度\n（当时早上10点，早饭没吃已经饿晕了）匆忙答一波\n\n美团外卖后端：\n算法题：\n\n1：合并区间\n\n基础部分：\n\n2 : 线程池具体分析\n\n3：redis 数据结构\n\n4 :redis  和数据库 如何保证数据强一致性\n\n5：java 的原子类，各种锁\n\n6：java 并发编程的一套。。。\n\n7：kafka 的设计和原理\n\n（美团的八股文问的应该蛮多的）\n\n面试官很好：一直在引导我， 应该是高P\n\n最后介绍了一波美团外卖hhh\n\n可惜被挂，面试中也确实不少问题没回答上来，心理坦然\n\n快手国际化：\n先怼了一波项目：\n\n然后同样让我觉得我的项目一无是处2333\n\n计算机网络：\n三次握手，四次挥手，然后流量控制，拥塞控制，tcp 长短连接等等\n\n数据库：\n深入问了数据库的底层实现，比如redolog ，undo log, bin log\n\n隔离级别等等，  B+树\n\nMVCC 机制的底层实现原理是什么？\n\njava :\nhashmap\n\n回答了hash map从1.7 到1.8 的各种改变如何避免的死循环（但是依旧无法线程安全，因为没有版本号和锁机制的存在无法保证线程安全）， 以及比较惊艳的扰动函数\n\ncurrentHashmap\n\n回答了currentHashmap 的改进和演变\n\n算法题：\n\n间隔打印链表：\n\n这点比较尴尬，写完核心代码后，面对官要完全实现这个程序，然后我写了链表类等，最后打印出了链表首地址233  场面一度比较尴尬\n\n面试官人很好， 说我基础不错了，可以多看看一些场景类设计题\n\n后来接受了阿里的意向，放弃快手的主管面\n\n大家加油！\n\n# 快手日常实习 效率工程 java后端\n\n> 作者：coder_tq\n链接：https://www.nowcoder.com/discuss/672139?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 6.3一面 40min\n\n1. 类加载过程\n2. 符号引用是什么\n3. 类和引用和对象分别位于JVM的什么位置\n4. 执行一个函数的过程\n5. 方法中对数据的操作过程\n6. 栈是线程共享还是私有\n7. JMM\n8. 没有volatile会发生什么\n9. volatile的作用是什么\n10. volatile是怎么实现的\n11. java中锁有什么\n12. 乐观锁与悲观锁的区别\n13. 乐观锁具体流程\n14. CAS怎么获取具体的数据\n15. 原子整型类底层是怎么实现的\n16. ABA问题是什么\n17. Lock的实现类\n18. synchronized说一说\n19. 和Lock的区别\n20. Lock的底层？\n21. synchronized底层\n22. synchronized是给谁上锁\n23. ArrayList底层\n24. MySQL事务隔离级别\n25. 四种隔离级别分别对应什么锁\n26. 间隙锁是什么\n27. MVCC是什么\n28. MVCC具体流程\n29. AOP原理\n30. JDK动态代理和cglib动态代理\n31. 反射是什么\n32. 反射具体原理\n\n算法\n\n1. 二叉树前序遍历\n2. 判断链表是否有环\n\n## 6.9 二面 70min\n1. HashMap底层原理\n2. hashcode是怎么计算出来的\n3. 阈值\n4. 多线程操作会发生什么 （JDK1.7链表成环）\n5. 如果使用自定义的类作为key要做什么，不重写会发生什么\n6. 如果保证多线程用什么\n7. hashTable？\n8. 项目\n9. SpringMVC与SpringBoot的区别\n10. 用过那些注解\n11. 如果使用service注解controller会怎么样\n12. bean的装配过程\n13. Autowire和Resource的区别（https://www.cnblogs.com/aspirant/p/10431029.html）\n14. SpringAOP是什么\n15. SpringMVC页面访问流程\n16. jdbc与iBatis区别\n17. iBatis是否可以有类似重载的操作\n18. try catch finally\n19. 线程的几种状态\n20. wait sleep join区别\n\nSQL的题目\n\n1. 使用SQL语句实现自旋锁（version列）\n2. 查询重复数据\n3. 查询多列同时重复数据\n\n算法\n\n无序数列，取前K大数字\n\n\n# 蘑菇街后台开发岗社招面筋\n\n> 作者：六丸今天也要加油\n链接：https://www.nowcoder.com/discuss/672253?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n1.自我介绍。\n\n2.你熟悉哪些。答：java 语言，spring 系列框架运用，mysql，hbase，redis，kafka等。\n\n4.说一说mysql两种存储引擎，谈了锁，存储结构，索引等区别。谈一下数据库事务，答了ACID，让我再详细说下这几个概念。追问mvcc，间隙锁。问了一个关于间隙锁的问题，\n\n5.讲一下你对Redis的了解，谈了数据类型，使用场景，跳表，项目中咋用的。追问RDB，AOF区别，谈了RDB 保存一段时间内的数据，子进程完成操作。AOF记录操作命令。追问\n\n6.说说你对java 集合的了解，JUC里面的类，讲了semaphore,cyclicBarrier,countDownLatch的使用场景与区别。追问底层怎么实现，讲了一下AQS，追问再细致点，\n\n7.map 如何解决冲突，1.7，1.8区别，讲讲put源码。\n\n8.tcp，拥塞控制，讲了一下慢开始，拥塞避免（乘法减小，加法增大），追问详细解释一下乘法减小，加法增大。\n\n9.讲一下udp。追问怎么让udp像tcp一样可靠，尴尬地一批说了这个不了解，躺好了。\n\n10.说一下kafka分区设置，怎么持久化的，怎么确定向哪一个分区写消息，为什么快（答零拷贝，只需一次用户态与内核态切换，扯了一点用户态和内核态的东西，追问再详细地说一\n\n下）。\n\n11.kafka 消息幂等性怎么做呢，offset批量提交如何做的。我说手动批量提交的，他说那万一有一个消息没有持久化成功怎么办。\n\n12.讲一哈spring，谈了ioc，aop，jdk动态代理和cglib，反射，spring中用到的设计模式。\n\n13.项目相关，自己讲了一堆项目场景，难点，如何解决。\n\n14.反问环节，蘑菇街月活，和小红书区别，你觉得蘑菇街前景怎么样。\n\n# 作业帮 后台开发岗 社招面试经验分享\n\n> 作者：蓝的天白的云\n链接：https://www.nowcoder.com/discuss/672269?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 一面\n\n1. HashMap和HashTable的区别？\n2. HashMap和HashTable扩容有什么区别？\n3. HashMap中的链表和红黑树时间复杂度是多少？\n4. 红黑树相对于链表还有什么好处？\n5. 线程安全的HashMap一般用什么？\n6. ConcurrentHashMap和HashMap有什么区别？\n7. 1String a = \"abc\";\n8. 2String b = new String(\"abc\");\n9. 程序是否堆空间配置参数设的越大越好？\n10. CMS回收算法的处理流程是什么？\n11. CMS会标记几次对象？\n12. G1的原理是什么样的？\n13. G1相对于其他回收算法有什么不同？\n14. 让Metaspace溢出有什么办法？\n15. 反复加载类会对Metaspace造成溢出吗？\n16. String会由自定义ClassLoader加载吗？\n17. 有什么办法造成栈溢出？\n18. 栈里面有什么东西？\n19. 如何让调用`x.b()`时让事务生效？\n20. CDN的处理逻辑？\n21. CDN的回源逻辑？\n22. 如何实现使用token进行身份验证？\n23. 如何保证token的安全？\n24. 当前数据库是单库单表吗？\n25. 如何减少数据库读压力？\n26. 多机主从数据库如何规划？分库分表为了解决什么问题？\n\n\n## 二面\n1. 自我介绍\n2. 为什么用ASM？\n3. 如何判断一个类是否要修改？\n4. 类修改的流程是什么？\n5. ASM用了什么设计模式？\n6. 为什么用Gradle？\n7. CI的流程是什么？\n8. 为什么使用Spring Boot？\n9. AOP是如何实现的？\n10. 除了Spring Data JPA还有什么ORM框架？\n11. 这些ORM框架是如何实现的？\n12. 有没有关注内存调优？\n13. 如何判断字段区分度？\n14. 如何使用token进行的身份验证？\n15. token刷新周期有多长？\n16. token如何进行校验？\n17. token如何生成的？\n18. 如何根据UUID生成原理发现规律性？\n19. 如何解决UUID重复的问题？\n20. 为什么要用CDN？\n21. 静态文件有哪些文件？\n22. 如何设计一个线程池？\n23. Java的线程池如何实行拒绝策略吗？\n\n编程题：100 块红包，6个人能抢到，每人抢到金额大于或者等于10 块，必须抢完\n\n# 猫眼实习Java一面面经\n\n> 作者：二本菜鸡求个大厂offer\n链接：https://www.nowcoder.com/discuss/672545?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n1. 实习项目，尬聊\n2. ArrayList数据结构\n3. ArrayList扩容、删除元素方式\n4. subList返回什么（答曰没用过）\n5. 线程安全的List\n6. COWArrayList原理、适用场景、缺点\n7. 排序Map\n8. TreeMap排序正序还是倒序\n9. TreeMap倒序排序怎么做\n10. TreeMap按value排序怎么做（答曰重写比较器，返回value比较结果，面试官说可以回去看一下。。。）\n11. HashMap数据结构（答曰Node数组，面试官懵了）\n12. HashMap put流程、扩容、可否修改初始容量\n13. HashMap指定初始容量有什么好处（答曰减少扩容开销）\n14. 扩容开销很大吗（答曰涉及节点转移，懵了）\n15. ConcurrentHashMap原理\n16. 线程有多少种状态（懵了）\n17. AQS和synchronized区别、性能\n18. synchronized锁升级\n19. 线程池最主要的三个参数，流程\n20. 自己定义线程池好还是用jdk的好\n21. jdk提供的线程池（答曰没用过）\n22. 线程池里给线程命名的是什么\n23. MySQL默认隔离级别\n24. B+树\n25. 可重复读存在什么问题\n26. 幻读是什么（懵了）\n27. next-key lock\n28. MVCC\n29. undolog、redolog、binlog都用来干嘛的\n30. 为什么有了binlog还要redolog\n31. 出了几个场景判断走不走索引\n32. 索引abc  select ... where a in (...) 走不走索引（懵了，走的吧）\n33. 算法：力扣第一道变种\n34. 循环依赖\n35. 最近看什么书\n36. 反问：业务（进去再分）\n37. 反问：转正（问hr）\n38. 反问：提升（这次问的很浅，没深入，问得不多。要多研究再深入）\n\n# 蘑菇街软开岗社招面经分享\n\n> 作者：SkyFiree\n链接：https://www.nowcoder.com/discuss/673031?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n问题：\n\n1. 聊10分钟项目\n2. 让你实现生产者消费者，类似阻塞队列那种的，你怎么实现？object类的notify和 wait+while循环\n3. redis的常用数据结构以及使用场景。\n4. mysql的是底层什么数据结构？为什么要B+树？\n5. 线程池，为什么要用线程池？满了怎么办？如果我想换个方式，改为满了之后先扩充最大核心数呢？\n6. 双亲委派机制的过程？为什么要这个双亲委派机制？\n7. netty？\n8. 问到了分表分库，假设有好多订单，现在分表分库了，我如何迅速找到我要的一堆数据。\n9. Map接口有哪些实现类\n10. 讲一下LinkedHashMap？\n11. 如何得到一个线程安全的Map？\n12. Java中有哪些锁？讲一下synchronized和ReentrantLock 的区别?\n13. Spring AOP是怎么实现的？\n14. JDK动态代理和CGLIB有什么区别？既然有没有实现接口都可以用CGLIB，为什么Spring还要使用JDK动态代理？\n15. Spring AOP不能对哪些类进行增强？（没有被Spring管理的类，当时没想出来）\n16. Spring是怎么解决循环依赖的？多例对象之间的循环依赖？单例和多例之间的循环依赖？\n17. MyBatis 中$和#的区别？既然$不安全，为什么还需要$，什么时候会用到它\n18. MySQL的ACID特性分别是怎么实现的？\n19. MySQL的事务隔离级别是怎么实现的？\n20. 用过什么缓存框架？用过什么RPC框架？用过什么消息队列？\n21. 除了Java自带的序列化之外，你还了解哪些序列化框架？ \n22. 差不多就是这样了，相比其他公司的面试，感觉问的问题还是多的。\n\n# 斗鱼后台开发岗社招面经\n> 作者：Bladefire\n链接：https://www.nowcoder.com/discuss/673040?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 斗鱼\n以下是除了项目之外的我记得的其他问题：\n### 一面\n1. spring和springboot有什么不同，在启动方式上呢\n2. 实际项目中使用redis，用来做什么，为什么用redis做缓存不用mysql 持久化，redis为什么比mysql快\n3. jvm内存模型，垃圾回收的区域，年轻代比例，如何设置大小，垃圾回收算法\n4. 重要servlet的主类在哪里，\n5. countDownLanch实现案例，其他线程执行到该位置后，主线程再执行，主线程是如何实现等待的\n6. nio,bio,aio\n7. 不用redis,redison,zokeeper做单机的项目如何实现秒杀\n8. sql是如何做的优化\n9. mysql 的索引的数据结构，b树和b+树的区别，是平衡树吗\n\n### 二面\n1. spring2.1版本有哪些新特点\n2. 项目的价值\n3. 单例模式的写法\n4. spring是如何管理bean的\n5. 项目中主要做的工作\n6. 项目为企业创造的价值\n7. 项目的主要功能\n\n\n\n## 蘑菇街一面\n1. 上来扣项目，让我介绍，之后对我的某个逻辑设场景题，让我想办法解决，\n2. 问了redis并发相关的场景，还有提高响应速度，我说了些用消息队列解耦之类的。\n3. 之后引申到消息队列消息丢失之类的，因为我用的rabbitmq利用confirm 机制+磁盘持久化+消费应答。\n4. http内部字段，怎么保持长连接，如果客户端挂了，服务器怎么办（我答的就是保活计时器+探测报文），\n5. 浏览器输入www.baidu.com后发生的过程（包括域名解析过程）\n6. 项目缓存雪崩，缓存穿透，\n7. 慢sql 怎么处理（explain或者慢查询日志，加索引啥的），\n8. abcd四个字段，你如何建联合索引（使用频率最高优先），\n9. 还问了java基础，记不清了，挺简单\n10. spring加载过程（底层源码方法），如何处理循环引用bean（利用三级缓存），\n11. linux中发现cpu使用过高，你怎么办（我不太会linux，只说了ps查进程，之后top分析cpu使用率）\n12. 总的来说场景题还是多一些的，有两个想不起来了\n### 蘑菇街二面（20分钟）\n面试官是个leader，很严肃就是全程问了项目\n\n# 滴滴秋储实习一面 (6.17 19:00~20:00)\n\n> 作者：luluivy\n链接：https://www.nowcoder.com/discuss/673071?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n1. java中锁使用的场景以及原理 面试官想要知道Lock, ReentrantLock, AQS 这些\n2. 线程池\n3. 事务以及特点\n4. 数据库如何根据索引查询数据\n5. 知道MySQL可以存储多少行吗\n6. SQL如何解析\n7. 了解脏读吗\n8. 了解幻读吗\n9. Java虚拟机内存模型\n10. 如何监控GC,能不能使用日志查询\n11. 有哪些对象可以作为GC Root\n12. new一个对象会触发哪些操作\n13. 内存回收算法\n14. 如何判断对象被回收\n15. TCP三次握手和四次挥手\n\n三道算法题（只需讲思路）\n\n1. 如何判断一个链表是否有环\n2. 判断镜像二叉树\n3. 接雨水问题\n\n可以展示一下你精通的地方，可以是项目中的难点，如何解决，可以是平时阅读的代码，心得感悟，也可以你平时写的博客\n\n# 社招一年半经验分享：阿里一面、美团一面java岗\n\n> 作者：第一次航海旅行\n链接：https://www.nowcoder.com/discuss/673081?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 阿里\n\n### 一面\n\n1. 线程安全的类有哪些，平时有使用么，用来解决什么问题\n2. mysql日志文件有哪些，分别介绍下作用\n3. 你们项目为什么用redis，快在哪，怎么保证高性能，高并发的\n4. redis 字典结构，hash 冲突怎么办，rehash，负载因子\n5. jvm了解哪些参数，用过哪些指令\n6. zookeeper 的基本原理，数据模型，znode类型，应用场景有哪些\n7. 一个热榜功能怎么设计，怎么设计缓存，如何保证缓存和数据库的一致性\n8. 容器化技术了解么，主要解决什么问题，原理是什么\n\n算法：对于一个字符串，计算其中最长回文子串的长度\n\n项目介绍\n\n\n## 美团\n因为之前的部门一面通过后，该部门没有hc了，就给我推荐到其他部门了，大厂hc还是挺紧张的\n\n### 一面\n\n1. redis集群，为什么是16384，哨兵模式，选举过程，会有脑裂问题么，raft算法，优缺点\n2. jvm 类加载器，自定义类加载器，双亲委派机制，优缺点，tomcat类加载机制\n3. tomcat热部署，热加载了解么，怎么做到的\n4. cms 收集器过程，g1 收集器原理，怎么实现可预测停顿的，region的大小，结构\n5. 内存溢出，内存泄漏遇到过么，什么场景产生的，怎么解决的\n6. 锁升级过程，轻量锁可以变成偏向锁么，偏向锁可以变成无锁么，自旋锁，对象头结构， 锁状态变化过程\n7. kafka重平衡，重启服务怎么保证kafka不发生重平衡，有什么方案\n8. 怎么理解分布式和微服务，为什么要拆分服务，会产生什么问题，怎么解决这些问题\n\n# 社招一年半经验分享：美团二面、三面\n\n> 作者：第一次航海旅行\n链接：https://www.nowcoder.com/discuss/673084?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 二面\n\n1. Innodb的结构了解么，磁盘页和缓存区是怎么配合，以及查找的，缓冲区和磁盘数据不一致怎么办，mysql突然宕机了会出现数据丢失么\n2. redis 字符串实现，sds和c区别，空间预分配\n3. redis有序集合怎么实现的，跳表是什么，往跳表添加一个元素的过程，添加和获取元素，获取分数的时间复杂度，为什么不用红黑树，红黑树有什么特点，左旋右旋操作\n4. io 模型了解么，多路复用，selete，poll，epoll，epoll 的结构，怎么注册事件，et和lt模 式\n5. 怎么理解高可用，如何保证高可用，有什么弊端，熔断机制，怎么实现\n6. 对于高并发怎么看，怎么算高并发，你们项目有么，如果有会产生什么问题，怎么解决\n7. 项目介绍\n8. 你们用的什么消息中间件，kafka，为什么用kafka，高吞吐量，怎么保证高吞吐量的，设计模型，零拷贝\n9. 算法1：给定一个长度为N的整形数组arr，其中有N个互不相等的自然数1-N，请实现arr的排序，但是不要把下标0∼N−1位置上的数通过直接赋值的方式替换成1∼N\n10. 算法2：判断一个树是否是平衡二叉树\n11. 算法3：给定一个二叉树，请计算节点值之和最大的路径的节点值之和是多少，这个路径的开 始节点和结束节点可以是二叉树中的任意节点\n\n\n\n## 三面\n\n项目介绍\n\n算法：求一个float数的立方根，牛顿迭代法\n\n什么时候能入职，你对岗位的期望是什么\n\n你还在面其他公司么，目前是一个什么流程\n\n\n# 1.3年工作经验双非裸辞搜狐java岗面经\n\n> 作者：萨桑\n链接：https://www.nowcoder.com/discuss/673429?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n搜狐1面64min过\n\n1. 自我介绍\n2. 有其他offer吗？\n3. tcp三次握手，四次挥手为什么是4次？\n4. http1.0 1.1 2.0？\n5. redis分布式锁的实现方式？\n6. redis 数据结构有哪些，zset底层数据结构是什么，讲一下？\n7. == equals区别，如果hashcode相等代表equals相等吗？\n8. hashmap 底层实现方式『tableSizeFor，hash』？\n9. 1.8相比1.7为什么头插变尾插？\n10. 线程安全的map有哪些？为什么用concurrenthashmap？底层实现是什么？\n11. 什么时候变成红黑树？双哈希表？「这些其实讲到了源码层面，initTable，resize，tryPresize，\n12. Thread.yield，ForwardingNode。」\n13. volatile 讲一下，MESI？\n14. Spring IOC，AOP你的理解讲一下？\n15. Spring注入方式知道哪些？\n16. bean是线程安全的吗？\n17. 缓存穿透，雪崩，击穿讲下你的理解。雪崩问题的解决方案？\n18. 实际场景提：活动页面，对于该页面数据有些用户本身没有缓存，有些用户有对应缓存。参加活动了的用户才有缓存，来了大量请求如何确保用户的请求正常返回。\n19. Innodb，memory 区别？聚簇索引，非聚簇索引区别？B 树，B+树区别？innodb还有什么索引？回表操作再走一遍主表吗？什么情况下不走？\n20. 4种事务隔离级别和分别的问题？\n21. Linux 命令用的多吗？awk用过吗？\n22. SQl调优你怎么做的？\n23. Java垃圾收集器知道哪些？高并发情况下用哪个？\n\n算法：\n1. 二叉树中序遍历，递归非递归实现\n2. 写一下单例模式\n\n有什么问我的？\n\n哪里需要提高？\n\n框架仅停留在使用层面-14，15，16题\n\n业务上思考的少-16题\n\n部门技术栈?\n\n部门所做业务？\n\n后续面试流程？-1 年多经验两轮技术面\n\n# 去哪儿后台开发岗社招面经\n\n> 作者：山鲁亚尔\n链接：https://www.nowcoder.com/discuss/673434?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n1. Java基本数据类型\n2. 装箱和拆箱\n3. Java的特点 \n4. 怎么实现多态的\n5. 什么动态绑定，什么是动态代理（这个没答好）\n6. 熟悉哪些Java集合类\n7. ArrayList和LinkedList的区别和适用场景\n8. HashMap的底层实现，以及put 操作，hash函数，二次扰动，扩容机制\n9. LinkHashMap了解吗？（不了解）\n10. ConcurrentHashMap的底层实现，怎么做到线程安全的\n11. 比较了JDK 1.7和JDK 1.8中HashMap和ConcurrentHashMap的不同\n12. 写过多线程Java编程吗，都是怎么写的（答：Synchronized）\n13. Synchronized底层是怎么实现的，同步代码块和同步方法\n14. 扯到了Synchronized的锁优化，偏向锁、轻量级锁、重量级锁\n15. 说一下wait和sleep的区别\n16. 可不可以自己手动加锁，手动加锁你怎么实现，底层又是怎么实现的（不太明白，扯了lock和unlock指令）\n17. 上面扯到指令，然后又开始了类加载机制\n18. 对象何时初始化，初始化的顺序\n19. 类加载器以及双亲委派机制，以及破坏双亲委派机制的场景\n20. Java怎么去实现共享操作，多线程访问同一数据（不了解）\n21. 怎么创建线程，说一下线程池，自己手动构造线程池的核心参数，线程池的工作原理\n22. 说一说AQS，底层怎么实现的（没答好，当时已经有点蒙了，问题太多了，也有很多没答好） \n23. 然后CAS是什么（这个我知道）\n24. 说一下Future和FutureTask，以及他们之间的区别（说得七七八八）\n25. 怎么实现在一个线程中，把计算结果这个操作放在一边执行，然后线程继续别的操作（不了解）\n26. 怎么实现多个计算线程全部到达之后再进行下一步的操作（我说了CountDownLatch和join）\n27. 最后算法题：给一个数组和target值，找出长度最长的且和等于target的连续子数组的长度（写了个O(n^2)的，但是面试官说有O (n)的，我没想出来）放下算法题代码，O(n)复杂度。\n\n#  民生银行软件开发岗面经（社招）\n> 作者：食人鬼\n链接：https://www.nowcoder.com/discuss/673460?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 一面\n1. 自我介绍\n2. 项目介绍\n3. 项目中遇到的难题\n4. 项目中用到了线程池，用的什么线程池，怎么配置的参数？\n5. 项目中的并发临界资源是怎么处理的？\n6. AtomicInteger怎么实现的？\n7. CAS三大问题及解决方式\n8. 事务隔离级别，MySQL 默认级别，（可重复读），为啥使用可重复读？（可重复读+MVCC达到了序列化要求）\n9. 一个类里面有两个方法A和B，方法A 有@Transaction，B没有，但B调用了A，外界调用B会不会触发事务？\n10. OS 进程间通信的方式？Java使用的哪种方式？\n11. 介绍一下Java中的锁？可重入锁如何实现的可重入？\n12. 浏览器从输入URL到返回结果中间经历了什么？\n13. 分析一下快速排序的时间复杂度和算法复杂度？\n14. 你有什么要问的？\n\n## 二面\n1. 项目中各个组件作用\n2. redis分布式锁：保证同一时刻多个请求只有一个可以操作业务，使用setnx+expire+getset\n3. 单点登录，多个应用系统中用户只需要登陆一次就可以访问所有应用系统（我说的是不同服务器，面试官说不对让我下去查资料），使用一个全局的token。\n4. 用户浏览器登录到返回过程（要包含数据库层）\n5. get和post区别，还问了一下幂等性是如何实现的，这就涉及到我的知识盲区了\n6. stringbuffer和stringbuilder\n7. concurrentHashMap来写写get和put的实现逻辑伪代码也行\n8. HashMap和HashTable的区别\n9. 给一串数据模拟HashMap的put过程\n10. hashtable，hashmap，concurrenthashma\n11. ioc和aop\n12. springmvc流程\n13. 数据表如何设计，不会，拜拜\n14. 各种树的区别，红黑树的优点\n15. collection 的实现类\n\n# 端点科技面经\n\n> 作者：二本渣渣求offer\n链接：https://www.nowcoder.com/discuss/673530?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n没录音，只为了mysql，redis，spring\n就记得那么多了\n\n1. redis和mysql不一致问题 解决方案\n2. redis的数据结构都介绍一遍\n3. 缓存穿透 布隆过滤器原理\n4. aof 和 rdb 哪个效率好 为什么\n5. redis是怎么处理过期建的\n6. redis内存淘汰机制\n7. mysql 索引有哪些结构\n8. hash结构什么情况下用\n9. 什么时候需要设索引\n10. 索引优化做过吗\n11. 隔离级别\n12. binglog redolog 为什么要两次提交\n13. mysql语句执行流程\n14. 分析器作用\n15. springbean生命周期\n16. ioc有哪些实现方式\n17. 怎么防止依赖注入\n18. aop项目里哪里用到了\n19. 设计模式\n\n# 蘑菇街后台开发岗一年经验社招面筋\n> 作者：已经跑路再也不见\n链接：https://www.nowcoder.com/discuss/673923?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 一面\n\n1. 自我介绍\n2. TCP连接处理，后台如何去做\n3. TCP握手和释放过程\n4. HTTP请求格式，请求头里都有哪些信息\n5. HTTP 常用请求方法，get和post区别\n6. 用post可以发get的请求吗，即在url后面跟着参数\n7. 常用响应码有哪些\n8. Java容器概括\n9. HashMap结构，为什么链表长度超过8才升级成红黑树，直接用红黑树合适吗？\n10. hash地址计算方法用md5()替换可以吗，并解释原因\n11. ArrayList和LinkedList分别应用场景，如果插入删除和随机访问操作次数都差不多，用哪一种，如何对比\n12. 以往一个项目询问，用户权限这一部分怎么设计\n13. 设计数据库，索引设计如何考虑\n14. 为什么优先考虑B+树索引，优点在哪\n15. 多请求访问后台，如何去做并保证并发性\n16. 平时线程池的使用，如果让自己设计一个线程池如何设计（设计哪些组件以及对应执行什么功能，每个组件分别用什么类或容器实现）\n17. 线程池阻塞队列用哪个Queue实现\n18. 线程数目如何设置，为什么IO较多情况下建议设置更多线程数目，如果允许内存足够大的\n19. 情况下设置超多线程数目会有什么问题\n20. JWT原理，加密过程\n21. 图片上传服务器，多个图片服务器如何去做，主图片服务器宕掉如何替换其它的\n\n## 二面\n\n1. 二面主要在问项目\n2. 自我介绍\n3. 以往一个项目介绍\n4. 项目中技术点的询问，架构设计，数据库设计等\n5. Vue和EasyUI的对比考虑\n6. SpringBoot和传统SSM的不同处\n7. SpringBoot自动配置原理\n8. SpringMVC内部配置原理，有哪些Bean类\n9. 设计数据库考虑哪些方面\n10. B+树数据结构\n11. 聚簇索引使用场景，页分裂问题\n12. JWT的数据格式，为什么要设计这样的数据格式\n13. tomcat的accesslog日志的使用\n14. 如果内存使用率较高，如何监控到某个线程并具体到某一行代码\n15. Linux使用，命令\n16. IO多路复用\n17. 对于日志文件，查看前10的URL，用什么命令\n18. 平时在学的东西，学习方式，深度和广度，以最近在学的一个例子为例\n\n## 反问三面（HR面）\n\n1. 自我介绍\n2. 一些hr 常规的问题\n\n# 度小满金融Java一二面凉经\n\n> 作者：小洪1617\n链接：https://www.nowcoder.com/discuss/673974?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n二面完了就没有消息了，估计是凉透了，把面经发出来，希望能帮助到大家~\n\n## 一面\n1. 如何设计Restful接口\n2. get和post的区别\n3. 常见的HTTP请求头有哪些，User-Agent的作用\n4. JDK1.8之后的新特性（不包括1.8）\n5. 说一下MySQL的事务隔离级别，RC和RR分别解决了什么问题\n6. 聚簇索引和非聚簇索引的区别\n7. 索引失效的原因可能是\n8. Redis有哪些数据结构，常用场景\n9. Redis除了做缓存，还能做什么\n10. JVM内存模型，1.7和1.8的区别\n11. 常见的GC算法，年轻代和老年代一般用哪种算法\n12. G1相比CMS的优势\n13. JUC包，CopyOnWriteArrayList是什么\n14. ConcurrentHashMap，1.7和1.8的区别\n15. synchronized是可重入锁吗（因为1.8的时候我提到了synchronized的锁升级）\n16. 给定一个字符串，形式是\"00000011111\"，找到第一个1的位置\n\n## 二面\n1. RPC和HTTP的区别\n2. Java的Map有哪些实现类，分别简要介绍一下\n3. 说一下HashMap的原理，为什么用链地址去解决冲突，为什么用红黑树\n4. kafka如何保证消息不丢失\n5. MySQL的事务隔离机制，如何实现的\n6. 什么是事务，事务的四大特性，一致性是如何实现的\n7. MVCC是什么，如何实现的\n8. 说一下常见的设计模式，实现一下单例模式\n9. 单例模式有哪几种，二者有什么区别，什么时候用哪种\n10. TopK算法\n11. 优先队列和堆的区别\n\n总结：我简历上没有写消息队列跟RPC，没想到还是问了，所以这两题不会，其他都答得挺好的。面试官最后给我的评价是知识深度可以，但广度还需要加强。所以大家如果要准备该公司的Java面试的话，还是要去看一下RPC跟Kafka相关的知识，至少也要提前去看点面经。\n\n# 小米社招Java开发一面面经\n> 作者：小洪1617\n链接：https://www.nowcoder.com/discuss/673975?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n写在前面：问了很多问题，目前还在等消息。。\n\n1. 说一下了解的Java数据结构有哪些（ArrayList,LinkedList,Queue,HashMap,ConcurrentHashMap,CopyOnWriteArrayList）\n2. HashMap的原理（用哈希数组）\n3. 数组存的是什么，为什么要转换成红黑树，为什么不能是其他树（链表转红黑树，红黑树相对平衡，调整效率快）\n4. HashMap是否线程安全，会导致什么问题（不是，会导致更新丢失，比如balabala）\n5. 除了更新丢失，HashMap还会造成什么问题，1.7和1.8的区别？（1.7头插入会导致死循环，1.8改用尾插法）\n6. 如果要线程安全，应该用什么类（ConcurrentHashMap）\n7. ConcurrentHashMap的实现？1.7和1.8都说一下。（分段锁，synchronized，CAS）\n8. synchronized和CAS有什么区别，synchronized的实现原理是什么，CAS呢，CAS如何解决ABA问题（有锁，无锁。monitor（Owner字段，EntryQ字段（互斥锁）），判断有无改变，版本号）\n9. synchronized和Lock有什么区别，Lock的实现原理是什么（AQS，使用CLH锁，维护一个双向队列，存储阻塞线程。每个线程一直监听前一个节点的状态，如果调用了unlock，则停止自旋。）\n10. 线程池的重要参数有哪些（poolSize,corePoolSize,maximumPoolSize,ThreadFactory,RejectionHandler)\n11. RejectionHandler有哪些，具体如何操作（4种还是5种来着，略）\n12. 线程池的线程在执行完任务会立刻回收吗？（保留corePoolSize个核心线程）\n13. Spring的IOC原理是什么，AOP原理是什么，默认是哪一种代理，两个代理的区别（反射，获取配置的类和属性，然后在运行时注入依赖。代理，JDK，CGLIB）\n14. Spring Bean初始化有哪几种方式（忘了）\n15. Spring如何解决循环依赖的（忘了，跟他说没有复习Spring，面试官说回去要巩固一下。答案的话应该是用三次缓存）\n16. Redis有哪些常用的数据结构，说说它们的常用场景（String,Hash,List,Sorted Set, Set)\n17. Sorted Set的数据结构是什么（ziplist+skiplist，细说了什么是skiplist）\n18. Redis如何删除过期键（定期+惰性）\n19. Redis如何持久化（RDB+AOF）\n20. Redis分片有了解过吗？（说成了高可用，不会）\n21. Redis高可用，那主从同步，如何更换主节点（哨兵，监控，投票。追问：细说投票算法？不记得了）\n22. MySQL有哪些事务隔离级别，分别解决了什么问题（RU，RC，RR，Serial。。。追问什么是间隙锁，就是锁住间隙避免了幻读）\n23. MVCC如何实现的？（日志，redo log，undo log，binlog）\n24. 索引有哪些类型（哈希索引，B树，B+树，说了一下有什么区别）\n25. 为什么不用范围搜索就用B树，为什么不能用哈希索引？（应该是树可以减少磁盘的IO消耗，但具体为什么说不出来）\n26. 什么是聚簇索引，什么是非聚簇索引？（InnoDB）\n27. 说一下MySQL的分库分表\n28. 遇到慢查询，如何解决？（explain，索引，覆盖索引，limit等）\n29. 如果已经用到了索引，但因为数据量太大，比如几个亿，如何解决？（分治。追问：细说？加redis缓存，分库分表）\n30. 说一下JVM的内存模型（堆，栈，Program Counter，元空间）\n31. 什么时候会触发GC（年轻代不足以分配对象，老年代不足以分配年轻代晋升的对象）\n32. 用过哪些RPC框架，有没有看过Spring Cloud的源码（SpringCloud，Netty。无）\n33. 算法题，二叉树的层序遍历（用了迭代解法）\n\n总结：问了很多，而且我又说得比较多，所以整个面试下来，感觉都口干舌燥了。。\n\n# 字节抖音Java一二面凉经 \n> 作者：小洪1617\n链接：https://www.nowcoder.com/discuss/673976?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n写在前面：一面问得过于简单，二面过于困难。整体的体验不太好。。\n\n## 一面\n1. Java如何实现线程安全（synchronized，ReentrantLock，AtomicInteger，ThreadLocal，CAS）\n2. CAS如何解决ABA问题（版本号）\n3. AtomicInteger的原理（UnSafe类，底层是一句CPU指令，避免了并发问题）\n4. 可重入锁是什么，非可重入锁又是什么（略\n5. 代码，实现生产者和消费者，一个长度100的buffer，10个生产者线程，10个消费者线程\n（我用了ReentrantLock跟Condition，结果忘记了锁的包路径是啥了，我写成了java.util.concurrent.，后来才知道是java.util.concurrent.locks.。。。）\n6. 对着代码提问，判定条件的while能不能换成if，为什么？为什么用signalAll，可不可以换成signal，二者有什么区别？\n7. Spring，AOP是什么，IOC是什么\n8. 二叉树的概念？红黑树又是什么，红黑树和其他平衡树的区别在哪\n9. TCP三次握手的过程，重发报文的过程。\n10. TCP和UDP的区别\n11. 说一下哪个项目觉得最有挑战，有几个模块，介绍一下\n12. 代码，LeetCode76\n\n## 二面\n1. MySQL的事务特性，事务隔离级别，分别解决了什么问题\n2. 间隙锁是什么，具体什么时候会加锁（具体什么时候加锁，这里要把所有情况都说清楚。。\n3. SQLite如何加锁\n4. Java里的锁，有哪几种（synchronized和Reentrantlock）\n5. ReentrantLock有哪些特性（可重入，公平锁），可重入是如何实现的（有一个引用数，非可重入只有01值）\n6. 当某个线程获取ReentrantLock失败时，是否会从内核态切换回用户态？ReentrantLock如何存储阻塞的线程的？（AQS，不断轮询前一个结点是否状态发生了变化）所以什么是自旋锁？\n7. JVM，说一下最熟悉的GC（我说了CMS，讲了并行回收，浮动垃圾，最短STW等等），然后追问我CMS的整个回收流程，标记，清理等等，年轻代怎么回收等等。（被问倒了。\n8. Redis的持久化如何做到的？（RDB+AOF）\n9. RDB具体是如何实现的，RDB生成快照的时候，Redis会阻塞掉吗？（使用BgSave，fork一个子进程去并行生成快照，不会阻塞）\n10. 既然生成快照的中途依然可以执行Redis，那么从节点获取到快照是不完整的，如何同步？（主从同步，先建立连接，然后命令传播，两个结点中的buffer队列里存储一个offset，差值就是需要同步的值）\n11. 设计题，设计一个扫码登陆（不会）那换成设计微信红包功能（MySQL的字段，Redis缓存一致性，发红包如何add字段，抢红包如何修改字段，通过一个唯一的版本号去保证CAS的ABA得到解决。但说了很久，面试官依然认为思路混乱）\n12. 算法题，n*n的矩阵，只能向右或向下移动，从最左上方移动到最右下方，把所有的路径输出（回溯，但剪枝忘了。前面的也答得不好，差不多就溜溜球了，也没有继续挣扎了。。）\n\n碎碎念\n一面的面试官爱理不理的，感觉就不是很想招人。但最后出了一题hard，也做出来了，感觉应该还是能过的，确实也通过了。\n\n但二面真的太难了，每一个问题都会一直细问，追问。其实ReentrantLock，还有MySQL的锁，Redis的持久化过程，我都有认真去复习的，但真的追问得太细了。。其实当时他第一题问“MySQL具体什么时候加锁”，我就挺懵的了。因为这个题我确实研究过很久，要综合考虑隔离级别，是否用了主键索引，二级索引，是否 存在回表等等的。所以当时我也不知道怎么回答，然后冷静下来就定位到了间隙锁上也就是肯定为RR级别，接着把大概的select，insert，delete等等的都说了，但后面还要继续说更细节的情况。我也不知道是我对题目的理解有问题，还是面试官想要的答案跟我不一致。反正挫败感很强，因为我记得当时看“这条SQL语句加了什么锁”，真的看了很久，而且自己也动手去测试了，结果还是没能满足面试官。。\n\n面试官看我对锁的理解“不够深入”，于是转向了Java里的锁。问完ReentrantLock的特性，又问什么是可重入锁，说完又问具体是怎么实现的。直到这里我还是完全OK的，但后面的“线程在用户态和内核态的切换”我就完全不懂了，面试官诱导了一下ReentrantLock如何实现，我大概说了一下AQS跟CLH锁，感觉又被挖坑了，跟前面说的“可能答案”自相矛盾。。\n\n接着的Redis持久化，也追问得很厉害，从持久化问到主从同步。。中间追问的时候描述得也比较“模糊”，后来在提示下才知道是问主从同步了，然后把整个过程都说了一下。。\n\n接着的设计题，没接触过。。说了很多，感觉还是不行。\n\n算法题基本上已经是“垃圾时间”啦，确实也出了一题很简单的题，大概做出来之后就算了。面试官问我如何优化，我也深知已经没戏，就直接放弃说不会了。然后面试就到这里。\n\n总的来说，其实二面的面试官挺温柔的，但问的题对于我来说太难了。。所以，第二天收到HR的感谢信。over。\n\n# 大众工匠一二面面经(已offer)\n> 作者：帅哥学java\n链接：https://www.nowcoder.com/discuss/674152?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n写在前面：一面23min，二面35min，整体感觉回答的不错，已offer，还得继续努力！\n## 一面\n1. 自我介绍\n2. 你有一些实战的项目经验吗？(说了自己的前后端分离项目)\n3. spring相关的东西你都了解过哪些？\n4. springcloud有用过吗？(学过，但是回答的我了解过，只是没做过类似的分布式项目)\n5. ArrayList和LinkedList区别？\n6. 往LinkedList里面放入元素的话，内部是如何决定它的位置的？\n7. 介绍一下HashMap(八股文)？\n8. 二叉树、b+树、红黑树的区别？\n9. 多线程这块儿你有用过吗？\n10. 平时哪些情况下会用多线程？\n11. 用两台电脑部署同样的服务，你觉得synchronized管用吗？\n12. 场景题：手机上和电脑上同时进行打款操作，怎么保证数据的安全性？就是扣款这个服务在部署在两个机器上，PC端进行扣款路由到第一个机器上，拿到了锁，但是手机端进行扣款路由到第二个机器节点上，两个锁是相互独立的，那么怎么保证数据的安全？\n13. 有用过redis做过锁吗？(说了说redis的分布式锁)\n14. spring的Bean的生命周期？(创建bean的时候大概都经过哪些重要的步骤，你知道它会经过哪些工厂吗？)\n15. 说一下spring ioc？\n16. 说一下spring aop？\n\n你这边有什么问题需要了解的吗？\n反问：\n1. 问了问总共有几面？\n2. 表现怎么样？(面试官说挺好，复试没问题)\n## 二面\n1. 自我介绍\n2. 介绍项目，说了说功能的实现\n3. 说一下项目的亮点，我就说了说点赞模块的设计以及登录的token设计\n4. 说一下fastdfs的上传原理？\n5. 了解jvm调优吗？实际用到过吗？\n6. 线程池了解多少？说一说工作原理？(提到了常见的三种创建方式以及实际工作中创建线程池的方式)\n7. 说一说redis set的底层？\n8. 了解CAS吗，简单说说？\n9. AQS呢？\n10. 既然线程池提到了阻塞队列，说一说阻塞队列的实现原理？\n11. 有用过实例图吗(我答了UML图啥的，但是这块儿我平常不常用所以答的不是很好)\n12. 组合与聚合的区别？\n13. spring ioc aop？\n14. 垃圾回收算法以及垃圾回收器了解吗？说一说G1垃圾回收器使用的算法？\n\n后面开始聊人生、聊职业规划巴拉巴拉\n反问：\n1. 公司的技术栈？\n2. 去了自己主要负责什么业务？\n3. 有转正机会吗？\n\n我感觉总体答的一般，但是二面后5分钟就接到了HR的电话，当时很激动哈哈，然后第二天就收到了offer，经过这两面感觉自己还是有很多不足，还得继续努力哇！\n\n# 拼多多服务端社招面筋\n\n> 作者：已经跑路再也不见\n链接：https://www.nowcoder.com/discuss/674339?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 一面\n\n项目聊了20***要问到用的技术栈、中间件\n\n### Java 知识:\n1.  异常体系设计\n1.  GC 过程，调优过程、死锁处理\n1.  线程池设计，线程数量如何配置选择(高低并发、任务执行时间长以及短的各种场景)\n1.  synchonized 和 lock 的实现，synchonized 底层实现、锁升级\n\n### 操作系统:\n1. 基础知识八股文，进程、线程的区别，线程同步、进程通信\n2. 常用的 linux 命令 回答有用过ping、ssh，由此引发到计算机网络部分，ping、ssh 分别属于哪一层，实现方式\n\n### 计算机网络:\n1. 三次握手和四次挥手\n2. 为什么握手是 3 次，两次可以吗，4 次呢 如果server 端没有收到第三次 ack，但是收到了 client 端发送的数据，server端会怎么处理\n3. 为什么挥手需要 4 次\n4. 介绍一下 tcp，如何保证可靠传输\n5. http 1/1.1/2 的区别 主要回答了连接复用、长连接等方面\n6. https 相关问题\n\n### 算法:\n1. 手写单例模式\n2. 反转链表 leetcode 206\n\n## 二面\n\n项目经历聊了大概20min,比较关注项目经历中有难度、挑战的事情\n\n### 算法:\n\n1. 手写无锁队列\n\n2. 遍历二叉树(非递归) leetcode 144\n\n### 数据库:\n\n1. 索引的实现方式\n2. hash、B+、B 树实现的优劣对比(Mysql MongoDB 分别是怎么实现的)\n3. 数据库的事务、隔离级别、实现方式\n4. 开源社区: 日常工作中有没有参与经历过开源项目，看过什么源码，对该技术的理解\n5. 聊到了redis、kafka; redis 性能高效的原因(重新实现了数据结构、IO 多路复用、多路复现的底层实现epoll，单线程基于内存)\n6. 持久化方式rdb、aof\n\n# 美团基础架构部实习1，2面（OC）\n\n> 作者：最肯忘却故人诗\n链接：https://www.nowcoder.com/discuss/675413?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n之前面了美团的优选实习，1 面和2面结束后一直没消息，后来告知hc没了，然后应该被美团基础架构部捞了，今天二面结束。面试官问我能实习几个月，我说了两个月，然后面试官让我和导师沟通下，看看能不能实习三个月。\n## 一面：6.17 17:00-18:10\n1. 自我介绍\n2. 如何查找热帖\n3. 10亿个帖子怎么找到TOP 100\n4. 接口和抽象类的区别\n5. ArrayList和LinkedList的区别\n6. voliate关键字\n7. Lock锁和synchronized锁区别\n8. 线程池的工作模式\n9. 常用原子类\n10. CAS的原理及缺点\n11. ThreadLocal原理\n12. fianl关键字原理及被fianl关键字修改的变量是否能被重写（可以反射重写）\n13. JVM内存模型\n14. 常用的垃圾回收算法\n15. 什么时候FullGC\n16. IP,TCP,UDP,HTTP,Socket有什么区别\n17. 长连接和短链接的区别\n18. Http可以实现长连接吗\n19. 进程和线程的区别\n20. NIO模型和NIO常见的三种实现方式\n21. 输入URL发生的事情\n22. 三次握手\n23. 如何建立一个Socket连接\n24. 算法：两数之和\n## 二面：6.24 15:00-16:00\n1. 自我介绍\n2. 项目介绍\n3. ES的架构\n4. 敏感词的过滤\n5. mysql的隔离级别\n6. mysql间隙锁\n7. mysql如何解决各种问题（共享锁和排他锁）\n8. MVCC版本控制\n9. B+树\n10. 输入URL发生的事情\n11. TCP四次挥手\n12. TCP中的定时器\n13. tcp长连接判断对方断开的方法\n14. 算法：动态规划 硬币数\n15. 自我评价，优点及缺点\n16. 自学中的困难\n\n\n# 科大讯飞一面、贝壳面经一、二面\n> 作者：offer——searching\n链接：https://www.nowcoder.com/discuss/675492?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 科大讯飞一面：\n自我介绍\n### 网络协议：\n1. DNS域名服务器单次程耗时50ms,客户端到服务端耗时10ms，后端逻辑1s，共计耗时多少？（后端1s+IP获取100ms+TCP30ms+信息往返20ms,提到RST等知识点，不知正确与否）\n2. HTTPS过程（TCP三次握手后，客户端发送加密协议等信息通知服务端，服务端制造证书和公钥发送给客户端，客户端公钥加密私钥发送给服务端）\n3. HTTP1.0和2.0区别（长连接，多路复用，请求方法增加，请求头压缩等等，本来是问2.0和3.0，我说只知道1.0和2.0的，就问这个了）\n### JVM：\n1. 如何辨别内存泄漏和内存溢出（先说明概念，通过USS判断是否内存泄漏，引出CMS和G1，提到了CMS的清理过程、G1的分区结构和性价比列表）\n2. 分析进程CPU占用飙升的原因（谈了谈JAVA对象头的结构和虚拟机栈，判断可能是其他进程修改了引用信息，不知对错）\n### JUC：\n只记得谈了线程池\n### Redis:\n介绍了RedLock和8种数据结构\n### Mysql：\n1. 好像谈了索引，完全记不得了\n2. 写了两道SQL语句，主要是group by、count和子查询\n\n你的职业规划\n\n你要通过哪些方面的学习成为技术专家\n### 期望薪资\n## 贝壳一面：\n1. 自我介绍\n2. 谈谈hashmap\n3. 红黑树转换条件除了冲突元素超过8以外，还有什么条件？（当时不知道，条件是数组长度大于64）\n4. 为什么数组长度是2的幂次（回答了位运算和底层二进制，面试官补充说还有rehash）\n5. 又谈了谈concurrentHashMap，问了size方法的底层实现（就说了加锁）\n6. Mysql索引，三层索引最多的数据量（不知道，面试官说是1kw左右，根据缓存量2M除以索引长度11字节计算，但我算起来是20W）\n7. Redis为什么快（只答了内存操作，无并发，面试官补充数据结构和NIO）\n8. 惰性删除机制（忘了,问如果是自己如何设计,答fork线程维护，面试官纠正随机抽取）\n9. kafka为什么快（回答牺牲可靠性，纠正：顺序读写，批量文件和zeroCopy技术）\n10. kafka为什么不可靠（谈了kafka的发送策略和主从复制，补充：生产能力超过消费能力，producer消息只存缓存）\n11. 一道算法题，环链表识别和长度计算\n12. 快慢指针法（当时忘了怎么用双指针返回长度，强行用hashmap写出来了）\n13. 面完20分钟通知1小时后二面\n### 二面：\n1. 自我介绍\n2. 谈谈hashmap,put和get方法的实现（简单说了说）\n3. 说说了hashmap的rehash（做原有元素的高低位判断，引申到了redis的渐进式rehash）\n4. jmm模型（CPU、高速缓存和内存；堆内存和直接内存）\n5. 你还知道哪些集合类（ArrayList,LinkedList,HashSet,HashTable）\n6. mysql索引类型（hash和B+树），绘画B+树的索引结构\n7. 口述索引查找过程，为什么不用B树（程序预读原理和叶子节点链表）\n8. 一道sql语句（用了group by，面试官补充了having）\n9. 两个有序数组的原地合并（双指针）\n10. offer和其他面试的情况\n\n总结：\n\n科大讯飞的面试官不问原理喜欢问实际案例，答的时候要尽量延申，就算不会也要说相关的原理知识，看重网络协议知识\n贝壳一面面试官有些问题答得不全会帮你补充，问的很细，让我知道了以前面试的时候有多少加分点没答出来\n二面面试官是团队负责人比较看重沟通能力和代码编程的性格，当时我今天第四面，人已经麻了，表现的不是很好，希望能过！\n\n# 21届补招：字节电商化社区二面，知乎一面\n\n> 作者：offer——searching\n链接：https://www.nowcoder.com/discuss/675897?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 字节二面：\n1. 自我介绍和项目简介\n2. mysql索引（B+和hash）\n3. 网络请求的过程\n4. 说说NIO（select,poll,epoll）\n5. 八股方面还有一些但是记不清了 \n\n算法题：\n1. leetocede 40. 组合总和 II 当时开始想成三数和那题的解法了，用迭代写了20分钟也没写出来\n\n当产品经理提出在你看来很琐碎的需求，你会怎么办（和产品经理沟通，要求说明需求应用价值，如果无法说服其修改需求，按正常流程开发）\n\n手中offer情况\n\n实习公司同事对你的评价\n\n同学对你的评价\n\n## 知乎一面：\n1. 自我介绍和项目简介\n2. mysql索引（B+和hash，这个知识点问的面试官太多了，我都懒得写回答了！这，就是八股！）\n3. 聚集索引和非聚集索引（同上）\n4. Mysql的主从复制（从数据库依据redolog完成一致性）\n5. binlog和redolog的差异，以及记录写入的先后性（，binlog二进制数据文件，redolog逻辑命令。先后顺序，当时回答binlog先，redolog后，不知对错，面试官没纠正，应该对了）\n6. mysql事务特性（泛谈了ACID，和MVCC）\n7. 谈谈锁（sync关键字和ReentrantLock）\n8. sync关键字和ReentrantLock的区别（层级，功能，重量级三个方面）\n9. 说说分布式锁（谈了Redis和Zookeeper的分布式锁实现原理，也说了自己用mysql做的分布式锁）\n10. 可重入锁在过期前续期失败会发生什么（说了事务回滚和yeid让出）\n11. 谈谈GC（说了CMS和G1，详细介绍完CMS流程，垃圾对象判断、三色标记和清理算法后面试官喊停了）\n12. 被重新标记后，垃圾对象就一定会被回收吗？（不一定，清理时仍会进行判断，如重新可达就不会清理）\n13. 说说引用计数器（忘了）\n14. 知道安全点和安全区域吗？（忘了）\n15. 根对象是什么（只说了虚拟机栈上对象、静态类变量，实际还有锁持有对象等等）\n16. 用过缓存操作吗？（Redis实现过存储）\n17. 缓存数据时遇到过什么问题吗？（没反应过来，好久没人问这些了，要求面试官描述详细情景，结果是缓存雪崩和缓存穿透，就说了些八股，随机过期、布隆过滤器、限流和在数据库操作上加分布式锁）\n18. 删除缓存数据时，是先删数据库还是先删缓存（答了先删缓存，面试官纠正：先数据库，否则缓存会在二次查询时恢复数据）\n19. 说说spring ioc（说了基本特性，一些bean对象实例化和存入容器的过程）\n20. 谈谈springmvc的事务（当时记不清了，就说和mysql事务差不多，又谈了事务传播策略，编程式事务和声明式事务）\n21. 说说Tranctional注解，注解加在save方法上，方法执行失败时会发生什么（不清楚，当时扯了段AOP的实现防止冷场）\n22. spring bean的生命周期（记不清了，就细说了下初始化流程，泛谈了存入容器等待调用和destory，提了下三级缓存和对象的提前暴露）\n\n算法题：\n1. 环链表的逻辑判断（快慢指针法，写的时候有点急，细节没注意好）\n\n总结：\n1. 字节二面的面试官技术八股问的比较少，反而比较关心我的个人经历和沟通能力，可能因为我之前的字节面试对表达能力评估较低，还特意问了我是否面试会紧张，影响逻辑表达\n2. 知乎一面面试官对八股问的较多，同时会描述场景追问实现思路，考验理论联系实际的能力\n\n# 货拉拉实习java一二面面经\n> 作者：追ぅ風\n链接：https://www.nowcoder.com/discuss/676510?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n先介绍一下本人背景，双非大二小菜鸡一枚，五月开始海投简历，由于没有过硬的项目经历（没有微服务那套）,学历也不行，大厂简历全挂了，中厂只有东方财富和货拉拉有反馈，东方财富因为实习时间太短，人家没要，只有货拉拉约了面试，许愿hr面\n\n## 一面（大概45min）\n介绍项目\n\n### 数据库：\n1. mysql有哪些日志？binlog有哪几种格式？\n2. mysql隔离级别有几种？\n3. MVCC说一下\n4. mysql有哪些锁？\n5. 如何定位SQL执行慢\n6. count(1) , count(*), count(主键) 哪个执行快？\n7. 数据库表设计主要注意什么？\n\n### redis:\n1. 有哪些数据结构？\n2. rehash过程？提到了渐进式 rehash 和 rehash 时机\n\n### java：\n1. java有哪些map？\n2. HashMap源码有看过吗？\n3. ConcurrentHashMap如何保证线程安全的？\n4. java中有哪些锁？他们的区别是什么？\n5. JVM有哪些垃圾回收期？说了一下 G1 和 CMS的区别\n6. 内存泄露和内存溢出\n7. 如何停止一个线程？\n\n### 框架：\n1. 说一下对 IOC 和 AOP的理解？\n2. AOP用来做什么的？\n3. SpringCloud有哪些组件？\n4. eureka 和 nacos 做注册中心的区别是什么？\n5. Springboot 全局异常如何处理？\n\n问了一下了不了解kafka？我接触过，但是简历上没写\n\n闲聊：\n有看过那些书？\n为什么想出来实习？\n\n反问环节\n\n## 二面（大概 30min）\n1. 介绍项目\n2. 线程池参数，拒绝策略，实际使用时线程池参数如何配置\n3. 常用排序算法\n4. tcp三次握手，四次挥手\n5. java的锁，说一下synchronized\n6. jvm知道哪些知识？说了一下运行时数据区，介绍一下G1，说一下G1回收的详细过程\n\n反问环节\n\n总结就是，货拉拉的面试挺常规的，基本都能答出来，只是答得好不好就不好说了，许愿hr面\n\n# 美团实习面试（一面、二面）\n> 作者：牛客114938609号\n链接：https://www.nowcoder.com/discuss/676901?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 一面：\n1. 谈项目，讨论项目的总体设计流程，其中简单提到redis的使用场景\n2. redis如何与数据库保持数据一致性\n3. 有用redis解决哪些场景\n4. mysql索引了解吗\n5. 为什么使用B+树而不是B树\n6. Innodb与MylSAM区别\n7. Java都有哪些创建线程的方式\n8. 线程池的参数说一下\n9. 线程池都有哪些拒绝策略，线程池的工作流程\n10. TCP三次握手与四次挥手\n11. 说一下TCP中的定时器(说了七种，从建立连接、发送数据、关闭连接都有哪些定时器)\n12. spring的事务传播\n13. springboot的启动流程\n\n## 二面\n1. 先让我手写一个快排热热身\n2. CPU的调度策略\n3. TCP如何保证可靠传输\n4. ConcurrentHashMap如何保证线程安全性\n5. HashMap为何改为尾插法\n6. 公平锁如何保证公平性\n7. 接口类与抽象类的区别\n8. 悲观锁与乐观锁\n9. 什么情况下产生死锁\n10. MVCC用来解决哪些隔离级别\n11. 如何没有MVCC会如何\n12. ACID每个单独介绍，并且也说了一下都是靠什么才保证对应的特性的\n13. CMS与G1有什么区别\n14. CMS为什么不使用标记整理算法\n15. 什么时候执行Full GC\n16. 单线程的redis为什么这么快\n17. redis如何删除过期key\n18. redis内存淘汰策略\n19. 最后问了一些关于spring bean的问题，生命周期，作用域\n\n\n"
  },
  {
    "path": "面试解答/面试解答2021-07.md",
    "content": "# 点击[:rocket::rocket::rocket:]可以看到知识点在哪\n\n---\n\n# 微店面经（已OC）\n\n> 作者：二本菜鸡不进大厂不改名\n链接：https://www.nowcoder.com/discuss/677437?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 一面(大约半小时)：\n### Redis:\n1. 存储结构，使用场景\n2. 淘汰策略\n3. 项目的使用场景\n### JVM：\n1. JMM结构\n2. 垃圾回收算法，底层\n### JAVA：\n1. HashMap源码已经不安全的情况\n2. ConcurrentHashMap源码\n3. ArrayList和LinkedList源码\n### Spring:\n循环依赖问题\n### Mysql:\n1. 索引\n2. MVCC\n3. 间隙锁\n\n## 二面( (大约50分钟))：\n1. 项目中如何使用SpringSecurity权限过滤问题(问的很细)\n2. AOP的执行流程(要非常详细的那种)\n3. AOP动态代理，如果都只想使用CGLIB如何解决\n4. AQS\n5. JAVA锁的应用场景，以及如何使用\n6. ABA问题\n7. Spring的设计模式，以及应用在那个地方\n8. JVM内存(具体到每个里放什么)\n9. new对象的过程(要求很详细的那种)\n\n有些问题回答的不是太好。。希望能过，求个offer\n二面过了，下午hr面，\n\n# 字节跳动-后端飞书-日常实习一二面（已offer）\n> 作者：小牛子练习生\n链接：https://www.nowcoder.com/discuss/677445?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 7-1 一面（42min）\n飞书面飞书，面试官提前上线，十分友好，第一次面试，嘴都瓢了。一面非常愉快，问的也比较基础。\n\n1. 自我介绍\n2. 到岗、实习时间，下学期有课吗？\n3. 项目\n4. 涉及多少张表\n5. 负责前端还是后端（全栈）\n6. 前后端分离API接口如何实现鉴权（JWT）\n7. 项目中Redis使用，如何解决Redis宕机后系统不可用（脚手架集成；集群、封装Redis工具类并捕获异常，查数据库）\n8. TCP 三次握手，每一次握手是为了做什么，为什么需要第三次握手\n9. OSI 七层模型\n10. 应用层、传输层和网络层常用协议\n11. 数据链路层传输什么信号（MAC帧，PPP帧？说了两个）\n12. HTTP 和 HTTPS 的区别（HTTPS=HTTP+SSL，加密传输，对称/非对称加密）\n13. JWT包括那几个部分（head、payload、signature）\n14. 数据库三大范式\n15. 事务特性\n16. 事务隔离级别\n17. MQ 消息重复消费解决方案（忘了，应该是设计业务幂等性、防重表）\n18. Redis 缓存击穿、缓存穿透和缓存雪崩区别\n19. Spring AOP实现原理，为什么要使用AOP（动态代理，jdk，cglib；代码重用巴拉巴拉）\n20. 抽象类与接口区别\n21. 屏幕共享，手写快排（3min）\n22. 写一个 Controller，RESTful API，接受两个int 参数，返回相加结果，讲解涉及注解原理\n23. 介绍部门情况\n\n### 反问\n1. 学习方向\n2. 表现如何（应该问怎么改进的）\n3. 转正？\n4. 什么时候有结果\n\n## 7-5 二面（53min）\n又是一个很和蔼的面试官！我讲了几分钟才发现麦没开。。。\n\n自我介绍（暗示面试官Java不错）\n1. 系统设计，包含模块\n2. Redis用来干什么（脚手架集成；缓存页面、登录过期校验、JWT……）\n3. 脚手架为什么要用Redis实现登录呢？（……SpringSecurity+JWT实现鉴权）\n4. 为什么用Redis作为缓存，不使用 应用服务器（Tomcat/JVM）作为缓存？（布吉岛，舍友跟我说：应用服务器是本地缓存、Redis服务器是分布式缓存）\n\n5. 为什么要同时重写equals和hashcode（Java开始了，八股文，顺便讲了一下HashMap）\n6. Java NIO学过嘛，讲讲三大组件？（Selector、Channel、Buffer）\n7. Buffer缓冲区原理（Buffer对象，数组/内存块，直接写入，写入读出Channel过程）\n8. Java常见设计模式（单例、工厂、模板方法、动态代理……）\n9. 听过策略模式吗？（听是听过……嘿嘿，面试官说用的最多）\n10. 实现线程安全的单例模式注意点？（存在问题，双重检测，volatile、synchronized、私有构造器）\n11. 如何中断一个正在运行的线程？（interrupt，忘记说volatile变量了）\n12. 如果不响应interrupt呢？（volatile变量？中断synchronized方法是一个意思吗？，面试官给我举了个例子：迅雷暂停就是中断，让我思考思考）\n13. MyBatis如何实现数据库字段与JavaBean间映射（I/O流读取XML文件，其中包含类全限定名，通过反射实例化对象）\n14. 如果是你实现，会使用什么技术实现数据库映射到JavaBean？（反射，面试官一直嗯嗯嗯我也不知道对不对）\n15. 反射是是什么？不要说怎么使用，底层实现原理（运行期动态获取/操作类；面试官解答：类型技术，Class作为实例模板，反射获取Class类模板）\n16. ThreadLocal用过吗？用来干什么（线程本地变量，线程隔离；静态内部类ThreadLocalMap、Entry继承弱引用）\n17. 你说说为什么ThreadLocal会内存泄漏？（ThreadLocal弱引用，Java结束了）\n18. HTTP请求响应包含什么内容（请求行/头/体、响应行/头/体）\n19. 跨域问题（协议/域名/端口号其中有不同，就存在跨域；@CrossOrigin、网关）\n20. 数据库为什么要第一范式？（不会，后来查了下，不满足就不是关系型数据库？）\n21. 联表查询join原理，两个表join为例（没了解，Nested-Loop Join）\n22. 你自己如何实现呢？（两张表=两个对象，各取一个相同字段，等值连接，求并结果集）\n23. 搞个题吧，归并排序（屏幕共享，嘿嘿，最后边界问题直接跟面试官讲思路了，呜呜）\n\n### 我这边问题完了，你有要问的吗？\n1. 表现如何，有什么改进？（Java是OK的，数据库有欠缺，基础不够扎实）\n2. 什么时候出结果（这两天）\n3. 总结教训，数据结构、算法一定要加强练习，数据库理论实战都要深入。\n4. 好好学习吧，小牛子\n\n\n# 22提前批-新加坡Shopee后端一面+二面凉经\n> 作者：shjsjhsj1212\n链接：https://www.nowcoder.com/discuss/677514?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n# 一面 - 07.01\n\n\n### 问题\n1. 项目介绍，有什么难点/困难？\n2. 设置user和product的类结构，实现user登陆场景和买卖场景下所需要的字段名称和类型。\n3. 多个user同时注册，用一样的用户名称，如何保证只会创建一条记录？\n4. 注册时，password的传输和加密方式？\n5. 登陆的时候，输入用户名和密码，点击登陆会发生什么事情？后端的处理？\n6. Session和Cookie\n7. Session id如何产生？\n8. 登陆成功后，后端向前端传递的数据内容？\n9. BigDemical的实现原理\n10. sql注入\n11. csrf和预防方式\n12. 登陆时，用户信息的获取和校验？\n13. 如何能加速信息的获取？\n14. 缓存对于登陆多次过的用户有效，如果是没有登陆过呢？\n### 算法\n1. 算法1：get max spent category（product属性） by userId（根据场景设计题实现）\n2. 算法2：对于输入的字符串处理，添加数字对应数量的括号并输出，并且注意层级。\n```\n样例a: 021 -> 0((2)1)\n样例b: 312 -> (((3))1(2))\n样例c: 0000 -> 0000\n样例d: 101 -> (1)0(1)\n样例e: 111000 -> (111)000\n样例f: 1 -> (1)\n样例g: 221 -> ((22)1)\n```\n## 二面 - 07.06\n\n### 问题\n1. 介绍一个你自己的项目（项目描述，使用的技术，结果）\n2. 项目为什么要使用这个技术？\n3. Dubbo, RabbitMQ, Docker, K8s, Redis等\n4. 介绍下Dubbo\n5. 问项目的细节，比如数据的来源，和其他平台的对接，是否为分布式服务，负载均衡，项目的部署等等\n6. 问另外一个rpc的项目\n7. 序列化协议的对比和介绍\n8. Nacos的介绍\n9. 负载均衡算法的实现（和Nacos相关）\n10. 注册中心的作用\n11. 平时使用过的设计模式？\n12. Java和Python实现设计模式的时候，如单例模式，有什么区别？\n13. 编码题：写一个双重校验锁的单例模式\n14. 为什么要使用关键字volatile？\n15. 去除volatile的后果？\n### 设计题：\n1. 设计数据库表格字段和简单的sql，能够实现展示数据和分页查询。\n2. 设计一种分页查询缓存方案，给出具体的key和value的样例。\n3. 询问了上一次面试中有什么没有回答好的地方，然后又问了一次\n\n# 北京城市网邻（58同城）社招面经\n> 作者：牛客745417734号\n链接：https://www.nowcoder.com/discuss/677558?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n1. 自我介绍。\n2. 介绍一下项目，负责哪些模块，有看过源码吗？\n3. currenthashMap和hashTable区别， 为什么线程安全的，hashmap1.7和1.8有什么区别。\n4. HashMap为什么使用红黑树，是怎么扩容的，扩容时做了什么。\n5. Jvm 分为哪些区域，运行时数据区又分为哪几个。\n6. Mysql存储引擎，有什么区别，复杂的SQL一般怎么优化，\n7. 索引有哪些， user表中的性别添加索引会生效吗，使用like ， in, != (<>), or  索引有效吗，为什么使用B+tree，是怎么实现的\n8. redis数据结构，redis的key过期会立即删除吗？\n9. GC，引用计数和可达性分析，算法 标记清除，标记复制，标记整理\n10. spring的IOC与AOP，哪些注解创建bean对象，springboot与spring区别，为什么使用SpringBoot\n11. java8新特性\n\n# 微店面经\n> 作者：牛客590954564号\n链接：https://www.nowcoder.com/discuss/677646?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n上来聊了会天，面试官人很好，很和善，然后提前会告诉我不会的问题直接就说不了解就好了。\n1. 自我介绍\n2. 了解ConcurrentHashMap吗？\n3. HashMap为啥每次扩容是原来的2倍\n4. ConcurrentHashMap的put操作过程\n5. put里面什么情况下会用CAS，什么情况下用synchronized的机制\n6. sleep(0)的作用是什么\n7. mysql默认事务隔离级别\n8. 幻读和不可重复的区别\n9. RR级别会出现幻读吗\n10. Mysql如何解决幻读的机制？\n11. 说一下MVCC\n12. MySQL MVCC具体实现方式是哪一种\n13. 说一下TCP四次挥手，越详细越好\n14. 为什么是四次挥手，最后一次如果不挥手会有问题吗？\n15. 为什么是等待2MSL？为什么不是1MSL\n16. Synchronized锁升级过程\n17. 为什么要引入锁的升级机制？\n18. 偏向锁主要解决什么问题\n19. 偏向锁是怎么加锁的？ \n\n总结一下，看了不少微店的面经，发现微店很关注并发编程的问题，包括线程池，包括锁和CAS等等，如果后面有面试的同学要考虑重点复习一下并发。\n明天二面加油加油\n\n# 微店二面面经\n\n> 作者：牛客590954564号\n链接：https://www.nowcoder.com/discuss/677911?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n全程40分钟\n面试官声音比较低沉，有几次中间麦不太好就没听清，还让重复的好几次，总体讲面试体验挺好的\n过程比较顺利，之前看到别的帖子也有比较多的相似的问题，所以有一些准备\n1. 自我介绍\n2. 项目中遇到的问题？怎么解决的？\n3. 有什么收获？\n4. 问比较熟悉什么中间件（说了ES，zk，Redis）\n5. zk的选举机制\n6. Redis和zk实现分布式锁说一下？\n7. OAuth2原理（我主要说的是Spring Security获取token的流程）\n8. Spring设计模式\n9. 代理模式在哪用到了？\n10. 动态代理具体哪几种实现，简单说一下\n11. CGLIB实现原理\n12. new对象流程\n13. Java对象引用，使用场景（强软弱虚，刚一问没反应过来，面试官提醒了一下才说出来的）\n14. 一亿个<IP地址，城市>这种形式的数据，找出指定的一条数据\n15. 反问：问了下技术栈，新人的培养\n\n最后面试官还体贴的让我在国外注意个人防护，i了i了\n看到网上其他同学的流程结合下自己的 不得不说 微店效率真的高，几天走完全流程，最后期待一个HR面\n--------------------------------\n当天更新，二面试后收到通过信息，约下周一HR面\n\n# shopee2022提前批新加坡后端一面7.2 90min \n\n> 作者：老师说我是搞算法的\n链接：https://www.nowcoder.com/discuss/678130?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n体感凉经~~~~问得很多很细，抠细节，有的想不起来了\n面试体验极佳，面试官人很好，全程引导，有说有笑\n\n自我介绍\n（中间想不起来了）\n\n1. JVM字节码文件对象的结构（对象头有啥，对象体有啥...）\n2. JVM运行期内存空间，每块的作用\n3. 对象一定在堆中吗？（方法区里有静态变量和常量）\n4. 对象一定在堆和方法区中吗？（。。。）\n5. 字符串常量池（在堆中）\n6. 面试官讲：其实虚拟机栈里也是可以存放对象的balabala...\n7. 虚拟机的类加载机制，具体步骤及对应完成的事情\n8. JVM的双亲委派模型\n9. ArrayList和Linkedlist底层\n10. ArrayList扩容机制\n11. JVM垃圾收集每种算法，实现方式，各自特点\n12. 调用system.gc()一定会发生垃圾收集吗？为什么？\n13. 谈一谈你对于垃圾收集机制在实际使用中的理解\n14. 说一下对于树的理解\n15. 二叉树，二叉查找树，红黑树\n16. 设计模式的几大基本原则（单一功能，开放，封闭...）\n17. 说一下你理解的几种设计模式\n18. MYSQL的事务隔离机制（3种问题，4种隔离机制）\n19. MYSQL的A C I D怎样实现的？\n20. MVCC原理\n21. MYSQL redo_log原理（实现D）,undo_log原理（实现A）\n22. 阿里巴巴Java开发手册有没有读过能介绍一下吗（。。。）\n23. MYSQL分布式锁，Redis分布式锁了解吗\n24. Redis的淘汰机制以及过期策略说一下\n25. redis除了作为中间键缓存还能用于什么功能，说一下你的理解\n26. 做个题吧\n```\n[1,null,2,3]建成树，然后中序遍历\n```\n\n# 两年社招后端开发面经\n> 作者：人为\n链接：https://www.nowcoder.com/discuss/679333?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 前言\n- 笔者在一家国有银行的科技部门干了两年 做的主要是线上消费贷款的后台部分\n- 心想着干了差不多两年 也是时候出来看看机会了\n- 大概是从年初开始准备的吧\n- 准备思路就是三块：0 简历 1 技术（主要是看书）2 项目 3 leetcode（不过社招考察的不多其实）\n### 简历\n简历首先决定了你有没有面试机会 其次还决定了面试时面试官会怎么提问你 我觉得多花些时间在简历上边还是挺重要的，一开始我也是很嫌弃认真写简历，不过后面投多几家石沉大海 你就会知道简历需要好好写了 不过不用指望自己能憋两三天就把简历写出来 可以边面试边改善 这样比较有感觉 也容易\n\n### 技术\n因为我是java系，看的书如下：深入理解java虚拟机、redis设计与实现、从paxos到zookeeper、apache kafka实战、java并发编程的艺术、mysql技术内幕。看的时候其实看得也挺快的 主要就是泛泛的看一遍 感觉特别重要的才会比较认真看 也可以先上面对着这几个方面留意一下比较常见的面试题，然后再来有针对性的看书会好一些\n\n### 项目\n项目的话 其实就两方面，一方面是你简历上写的东西 对你简历上写的东西要特别熟悉 不然很容易被问穿 这就会有些尴尬 当然如果做的项目确实比较简单 也应该适当包装一下（比如无中生有，不然有时简历关都过不了 不过对于无中生有的部分也需要比较熟悉就是了） 然后工作一两年的建议就别写精通XXX了 我一般就是了解、熟悉；好像跑偏了，除了简历上的东西，另一方面会问你一些有关你们系统的开放性问题，这就需要你平时对你们系统有一些了解，比如你们生产有多少台机器、数据库怎样部署的之类的，这些就需要平时积累 如果平时没积累，就多找熟悉的同事问问（这里要谢谢欢哥，我的面试经理）\n\n### leetcode\n我反正直接躺平 一开始还会每天刷一道medium 后面直接两三天挑一道easy 就保持手感 因为没有面字节 所以以前的老本倒也够用了\n\n接下里就是具体的面经啦 这里按照面试的时间顺序排一下\n\n## 涂鸦一面\n\n1. 项目并发控制 updatepdate 0 乐观锁\n2. 简历ibatis\n3. redis哨兵心跳 事务 pipeline\n4. 数据库全表查询 select * 高性能mysql\n5. jvm 栈 垃圾回收 碎片 stop the world\n6. 内存布局 enum java1.6 1.8\n7. 设计文档输出\n8. 学习kafka\n9. 锁升级 synchronized\n10. 服务如何开发给第三方\n11. 接口如何审核 控制第三方调用\n## 涂鸦二面\n\n1. 聊聊最近一个项目，遇到哪些业务上的难点、技术上的难点，怎么解决的\n2. Redis分布式锁怎么实现的\n3. Weblogic平时有使用到吗 对tomcat的原理了解吗\n4. 对分布式了解吗 分布式需要解决哪些问题呢\n5. 最近这两年最大的技术进步是哪些呢 讲讲oracle索引 什么时候会出现死锁呢\n6. 对未来的规划是怎么样的呢\n7. 有什么想问我的吗\n## 涂鸦hr面\n1. 对公司了解吗 事后有去了解过吗\n2. 为什么想离开现在的公司\n3. 想去哪些类型的公司 为什么 有什么期望\n4. 在工作中遇到最大的难题 怎么解决的\n5. 作为技术 最讨厌什么\n6. 为什么不想去bat 有和同事了解过吗\n7. 对于这个岗位进来要做的东西 了解吗\n8. 现在薪资多少 期望薪资多少\n9. 涂鸦一面的面试官聊得久一些 二面就比较快 hr面感觉和hr不是很对口 后面就卒了 面试反馈挺快的 一般今天面完 第二天就反馈了\n\n## akulaku\n### 一面\n\n1. Redis为什么不用hashmap\n2. 等额本息等额本金的区别\n3. 订单库存服务不一致\n4. 主键递增\n5. 工作流回滚\n6. volatile 原子变量\n### 二面\n\n1. 准入 攻破怎么办\n2. redis数据结构 hashmap使用\n3. jvm内存布局\n4. 什么时候回收\n5. hr面\n\n额hr问题忘了 这家因为后边给的工资比较少 所以就没去\n\n## 平安一面\n\n1. 你在建行做的是什么 为什么想要换工作 对新的工作有什么期待\n2. 了解过springboot吗\n3. 了解springcloud 它的各个组件是怎么样的\n4. 了解mybatis吗 它的分页组件是怎么实现的\n5. 你用过多线程吗 知道线程池的原理吗\n6. 了解hive hbase吗\n7. 说出自己的三个缺点 三个优点\n8. 最近比较有技术含量的一个项目是什么\n9. 一般一个需求下来你是怎么做的\n10. Redis有哪几种数据结构\n11. Redis数据结构的选取？有什么原则吗\n12. 有生产上堆栈分析的经验吗\n13. 生产上有出现线程堵塞的经验吗\n## 平安二面\n\n1. 讲讲你在建行做的什么\n2. 讲讲你们的流程\n3. 你做的有什么难点吗 你负责的东西是什么呢\n4. 你现在还是在做这个吗\n5. 你们怎么解决客户额度的问题呢\n6. 讲讲你项目里用到的技术\n7. 你们数据库数据量大吗\n8. 几千万的数据量会有性能问题吗 怎么解决大表关联\n9. 讲讲多线程\n10. 用过微服务吗 你们服务器扛得住吗\n## hr面\n也就是问一些常规问题 忘记记录了 这家也谈完薪资了 应该会去这一家\n\n## amber 一面\n\n1. 讲讲layer2的实现方式\n2. 讲讲arbitrum\n3. 讲讲助记词是怎么生成私钥的\n4. 怎么把一条链的提现和充值接入钱包\n5. 用什么语言写过dapp吗\n6. 以太坊地址有大小写区分吗\n7. 波场和以太坊有什么区别 为什么波场比较快\n8. Btc和以太坊两条链有什么区别\n9. 搭建过以太坊节点吗\n10. 如果有很多个账户 怎么监听账户的余额变动了\n11. Redis机构树在数据库怎么存的 为什么要这么设计\n12. 在redis怎么存的 在数据库如何优化\n13. 给一个字符串 写出他的全排列\n## amber hr面\n\n1. 了解我们公司是做什么的吗\n2. 年终考核和绩效挂钩吗 拿了什么 为什么\n3. 为什么想要离开现在的公司\n4. 为什么对区块链这么有信仰\n5. 对未来的规划是什么\n6. 可以接受先做开发后面再接触钱包相关的吗\n7. 对加班的态度怎样\n8. 现在的薪资及期望薪资\n9. 本来一开始我是最想去这家公司的 但是他们要求有过钱包的设计经验 所以就卒了 不过后面也觉得无所谓了。。。\n\n## 波场一面\n\n1. 讲讲你对区块链的了解\n2. 讲讲全节点 轻节点\n3. 如果要你设计钱包 你觉得有什么侧重点\n4. 讲讲concurrenthashmap如何实现并发 扩容呢\n5. 用过消息队列吗 如何防止消息丢失\n6. 做一道leetcode题\n7. 有什么要问我的吗\n8. 这一家同上。。。一面卒。\n\n## 招联一面\n\n1. 为什么要离职\n2. 期望薪资\n3. 讲讲比较有代表性的项目\n4. 授信数据从哪里来的\n5. 你们贷款流程设计时有什么原则\n6. 征信挂了怎么办 怎么优化（一天缓存）\n7. 为什么要迁移表 迁移之后数据库压力就解决了吗\n8. Redis 数据结构 应用场景 怎么解决并发访问 怎么解决数据一致性 用来缓存什么东西\n9. Kafka消息积压怎么处理\n10. 了解springboot吗 了解微服务吗\n11. 了解分布式事务如何解决吗（二阶段提交 三阶段提交）\n12. myisam和innodb的区别\n13. b+树索引存储位置 覆盖索引 什么时候走索引比不走索引慢\n14. 什么场景下需要用到分布式\n15. 不断fullgc怎么处理\n16. Jvm内存分布\n17. 有什么想问我的吗\n18. 招联很奇怪 我投了差不多一周 某天周六突然打电话问我能不能面试 然后就当成面了 聊了有四十多分钟 后面就没有音讯了 感觉是刷kpi吧\n\n## 微众一面\n\n1. 讲讲几个项目 你们系统的作用\n2. 遇到过你认为的难题\n3. 开户阶段就只做了开户吗\n4. 怎么协调开户和额度扣减 怎么解决\n5. 分布式事务的解决方式\n6. 避免出现一笔申请多次推送\n7. 工作流出错后不能重复提交 没办法将推送放到后面吗\n8. Java线程池的参数 核心线程最大线程 和tomcat线程池的区别\n9. Bean的几种初始化方式 默认方式\n10. mybatis怎么从一个mapper组装成一个类\n11. Aop怎么实现的 有aop不生效的情况吗（独立事务不对）\n12. 轮起怎么避免对外服务失败\n13. Zk的作用 谁调用zk\n14. oracle和mysql的区别\n15. Mysql的索引 主键索引和非主键索引的区别\n16. 授信编号如何生成的\n17. 系统之间的调用通过消息队列吗\n18. 你们基于tcp吗 http吗 用的什么http框架\n19. 怎么使用多线程的\n20. 产品场景很多，针对代码里面大量的ifelse 考虑过怎么优化\n21. 单例模式使用过吗 有哪几种初始化方式\n22. 什么时候索引失效 为什么这种情况会失效\n23. 单索引和联合索引的区别 如何决定采用顺序\n24. 查询一定时间段内的流水 如何建立索引\n25. 平时有了解什么新技术吗\n26. 对于新环境预计多久能上手\n27. 有什么要问我的吗\n28. 你们加班程度怎样 对于加班怎么看\n29. 面试情况 如何考虑\n30. 对于微众的了解\n31. 一面聊得比较久 差不多70分钟 过了一周了 才跟我说他们领导会联系我二面。。。反正现在还没开始 如果有后续我可以补上来。\n\n## 微众二面\n\n1. 简单自我介绍一下\n2. 讲讲你负责的东西\n3. 授信是怎么计算额度的\n4. 你们就负责转发请求吗？\n5. 调用第三方接口怎么处理报错的 有没有什么优化方法（异步、多线程）java有没有什么自带的获取结果的方法（future）异步调用如果超时了怎么办（不会了 timer）\n6. 你们系统tps多少 每天调用开户次数多少 有多少成功开出来呢\n7. java object有哪些方法（hashcode equals notify wait）\n8. 你们的开发框架是怎么样的\n9. Hashcode 和 equals有哪些区别呢 什么场景下使用呢\n10. equals和compareto有什么区别\n11. 重写一个对象的hashcode和equals怎么权衡 hashcode一定要重写吗\n12. Hashmap的put操作是怎么做的 哈希操作是如何进行的呢\n13. 单例模式有哪几种写法（非懒加载、懒加载）\n14. 懒加载怎么避免并发问题呢（双重确认加锁）\n15. 第一层确认的作用 第二层确认的作用\n16. Mybatis什么情况下会sql注入呢\n17. 你了解sql注入吗 能不能将一个具体的例子\n18. 你们用的是什么数据库 了解mysql吗\n19. Mysql的主键索引非主键索引什么区别\n20. 什么时候非主键索引不需要回表呢（覆盖索引的时候）\n21. 非主键索引什么时候失效\n22. 非主键索引A B C，如果A范围查找，B还上索引吗（不会）\n23. 你的专业是统计学 为什么想要做开发呢 计算机相关的知识是怎么学的\n24. 平时有哪些兴趣爱好\n25. 对技术了解如何\n26. 现在的面试情况\n27. 为什么想要跳槽\n28. 有什么想要问我的\n29. 二面挂了 感觉在技术上还差了一丢丢 需要再积累一段时间\n\n## 招银技术面\n\n1. Stringbuffer Stringbuilder的区别\n2. Mybatis一级缓存二级缓存的区别\n3. springmvc常用的注解\n4. Mysql两个引擎的区别\n5. 有jvm调优经验吗 讲讲java内存模型\n6. 了解微服务吗 讲讲你对微服务的理解\n7. 了解springcloud吗 讲讲你对spirngcloud几个组件的理解\n8. 讲讲简单工厂模式和抽象工厂模式\n9. 讲讲认为比较重要的项目\n10. 主要流程是怎么样子的\n11. 说说你们系统的作用 整个交易路线是怎么样子\n12. 第三方接口报错怎么办\n13. 如果增加了很多第三方接口 怎么保证服务时间不增加 又保证用户体验 可以从技术和架构的角度讲讲\n14. 你们怎么协同其他开发中心的进度的？一个需求拿来怎么确保上线时间\n15. 觉得最重要的文档是什么？\n16. 怎么确保接口文档正确高效\n17. 你之前是做数据分析的 为什么现在做开发了\n18. 你本科是统计学 那计算机基础有学习吗\n19. 现在的offer情况\n20. 周日做了在线笔试题。。。后面就联系我在线面试 是三个有些年纪的面试官 因为他们问的ssm相关的 我没咋准备，不对 是根本没准备 所以答得不好 卒\n\n## 腾讯一面\n\n1. 第三方调起你们的服务是怎么认证的（appkey 证书）\n2. 你能讲讲证书是什么吗\n3. 额度数据从哪里来的 渠道怎么区分\n4. Redis集群怎么部署的 一条写入什么时候可以当成提交了\n5. 全部从节点都确认会有性能问题 怎么优化\n6. 了解raft算法吗 它是怎么达到一致的\n7. 灰度切换怎么做\n8. 了解oracle怎么同步数据到redis吗\n9. 了解java的哪些字符串对象 讲讲hashmap\n10. springboot的启动流程\n11. Mybatis的延迟加载 一二级缓存 $&[的区别\n12. 了解docker吗 docker创建镜像什么命令\n13. 回滚机制 推送的时候如果涉及多个渠道怎么办呢\n14. 项目里面的难点 你负责的内容\n15. 数据迁移怎么做的\n16. 一面的时候我是奔溃的 感觉腾讯问得会比较深一些 碰巧ssm、zk是我的软肋 虽然写在简历上 但是并不熟 加上之前的面试官都没问过我。。。 我进行到后面都想跟面试官说要不今天到这 但是后面居然过了 一面一个小时\n\n## 腾讯二面\n\n1. 做一道链表合并的题 能不能就地算法完成\n2. Linux用得比较多的命令\n3. Grep怎么高亮\n4. linux去重命令\n5. 怎么查出哪个进程占用了8080端口 部署在哪个路径下\n6. 大学专业 为什么做开发 有学过计算机相关的知识吗\n7. 了解进程 线程 协程吗\n8. 内核态、用户态的区别\n9. 了解常用的加密算法吗\n10. 你们项目中有用到加密吗\n11. MD5算法是什么加密算法 有公私钥吗\n12. 随心贷有什么项目难度\n13. 你们银行的服务是怎么控制调用的\n14. 二面聊得比较简单 差不多40分钟 感觉面试官就是走个流程吧 二面卒\n## 蚂蚁一面\n\n1. 简单自我介绍一下以及讲讲你擅长的技术\n2. 额度恢复扣减是怎么实现的 有了解吗\n3. 标准贷款和额度贷款有什么区别\n4. 贷款合约表里面有什么字段\n5. 快贷里面有多个产品 那是怎么区分的 会不会出现一笔贷款对应多个贷款的情况\n6. 第三方场景的准入是怎么做的 他们负责还是你们负责\n7. Redis你们用来做什么呢\n8. Redis为什么会比mysql快 从几个角度分析\n9. Redis上的数据是拿来缓存呢还是就只放在redis上\n10. 如何保证redis和数据库的一致性问题\n11. 如果redis上三分钟有效期的临时数据在申请过程中过期了怎么办（看门狗）\n12. 你们项目中有用到多线程吗\n13. 你们有多少台机器 每天的访问量怎么样\n14. 如果有很多并发的请求进来 可以怎么利用多线程优化（异步）如果线程队列满了呢？（ 消息队列）\n15. 蚂蚁的面试官感觉挺有水平的 会问你擅长哪些技术 聊了差不多四十分钟 二十分钟项目 二十分钟技术 你答不上来也会引导你 是我这次面试体验里边最好的了 一面应该是过了 看面试官什么时候联系我二面 有后续的话我可以再更新\n\n## 蚂蚁二面（十八分钟）\n全程问项目。。。\n\n不出意料 挂了 经验尚浅 同志仍需努力！\n\n总结，最后再随便唠叨几点\n1. 我是裸辞的 就是提了离职才开始找工作的 对于那些很讨厌自己一个人待在家里的人非常不建议（比如我）但是其实也可以尝试一下 嘿嘿 毕竟也算比较新奇的体验 反正就是要考虑清楚啦\n2. 面试的时候最好从难度简单的开始 一开始就大厂 容易把自己搞自闭 而且浪费机会 就是循序渐进\n3. 面试后最好做一下记录 至少要复盘一下自己答得不好得 去弄明白 很大概率下个面试官还问。。。\n4. 面试其实也是个展现自我的过程 就是和面试官的沟通和交流也挺重要的 答不上来也没啥 但要让面试官知道你比较努力。。。（这一点面几家就会有感觉了）\n5. 社招和校招还是不太一样的 社招背景契合会更容易进一些 背景不契合，比如区块链那些都是一面卒 自动驾驶直接简历关都没过。。。 对自己的职业规划要有点B数 虽然也不一定会一直当打工人\n6. 就是要广撒网 这样才会心态平和 不会患得患失 如果很自闭 就休息休息 或者找朋友倾诉倾诉 然后继续战斗！\n7. 关于面经 其实我是在准备阶段 就是几个月前看了几篇 开始面试后一篇都没看过。。。这个仁者见仁 智者见智 不过我的面经还是可以看一看的 嘿嘿（觉得自己好有文采.jpg\n\n最后的最后 祝大家可以拿到自己心仪的offer！！！\n\n7.7更新 大家有问题可以评论区留言 我一天会看一两次 有可能回答的我就会尽量解答\n\n7.10更新 更新了微众二面和蚂蚁二面 最终决定去平安啦 完结撒花！后续就不怎么会看帖子啦\n\n# 字节商业化技术提前批一面\n> 作者：印1第安老斑鸠\n链接：https://www.nowcoder.com/discuss/679452?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n1. 项目中遇到的难点。\n2. 有没有用过授权登录，第三方登录，了解过OAuth2.0吗？\n3. 线程的生命周期。\n4. sleep()和wait()方法的区别是什么？如何唤醒？\n5. 数据库的索引结构是什么？\n6. 为什么使用B+树？对比B树，哈希。\n7. 聚簇索引和非聚簇索引的区别。\n8. sql优化。\n9. 项目中缓存使用过哪些数据结构？\n10. redis数据结构如何实现的？是否了解过底层代码。\n11. synchronized和lock的区别？\n12. 发生异常时 synchronized和lock锁的占用情况？\n13. 可重入锁的特性。\n14. 创建线程池的参数有哪些？\n15. 任务队列一般选用阻塞队列还是非阻塞队列？为什么使用阻塞队列？\n16. 有没有实现过所有的数据结构？\n17. 代码题，两道二选一。\n    1. n个节点的有向无环图，找到所有从0→n-1的路径。\n    2. 找出字符串中最长的回文子串。\n# 百度2022提前批 一、二、三面面经——Java研发工程师\n> 作者：我一直都很浪\n链接：https://www.nowcoder.com/discuss/679889?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 一面\n电话面，一上来自我介绍，然后全是问基础，时长40分钟左右，提问的顺序记不清了，大致内容是下面这些。\n1. java语言的特点\n2. synchronized关键字使用方法，修饰实例方法和静态方法区别，锁升级过程\n3. 如何判断一个对象是垃圾对象\n4. 什么语言使用引用计数法（不知道）\n5. 垃圾回收算法\n6. 算法题：如何判断链表有环\n7. 算法题：如何找到链表三等分点\n8. 算法题：如何在一堆数字中找到重复数字，只有一个数字重复。（本来以为是剑指offer原题，结果刚去看又不是，不知道是不是当时听错题了...）\n9. mysql隔离级别\n10. mysql3种日志作用（redo log、undo log、bin log，bin log用途忘了）\n11. redis list底层实现（不会）\n12. 跳表了解吗（不会）\n13. spring aop实现\n14. 介绍ConcurrentHashMap\n15. HashMap为什么每次扩容为2倍\n16. 介绍OSI七层模型\n17. 传输层有哪些协议\n18. TCP、UDP区别\n19. UDP使用场景\n20. redis过期策略\n21. redis淘汰策略、默认淘汰策略（默认淘汰策略不知道）\n22. TCP如何保证有序\n23. TCP四次挥手，为什么是四次挥手\n\n总结\n\n应该主要是照着简历上技术栈问的，所以还是要多熟悉简历上的技术栈。另外就是最好把剑指offer多刷几遍，多么痛的领悟...\n## 二面\n7.8号，腾讯会议，首先自我介绍，然后问项目，然后问了一些场景题\n\n1. 项目相关\n2. 项目难点介绍\n3. 为什么用Ehcache，它的缓存过期策略、淘汰策略，和caffine、guava区别\n4. 其他\n5. 场景题,100万个电话号码如何快速去重，用什么数据结构存储节省空间\n6. redis什么结构适合存储100万个电话号码\n7. redis中list、set区别\n8. sql语句，一张表三个字段（姓名、课程、成绩），如何查询至少有两门课程在60分以上的学生姓名\n9. mysql建表、建索引有什么规范\n10. redis集群、哨兵了解吗\n11. 算法题：“I am a student”反转每个单词，单词之间顺序不变\n12. 还有一个linux相关的，我直接说不会…\n## 三面\n7.19（等了10天的三面...），时间40min左右，如流\n1. 自我介绍\n2. 项目相关的深入聊了挺久，面试官指出了一些项目存在的问题和可优化点\n3. 本科和研究生学过哪些课程\n4. 你觉得你的优势和劣势是什么\n5. 有可能实习吗\n6. 算法题：爬台阶，一个问题是爬到目标台阶有几种走法，一个问题是把所有可能的路径打印出来\n\n# 滴滴社招java面经\n\n> 作者：牛客4733855号\n链接：https://www.nowcoder.com/discuss/679905?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n1. 介绍项目\n2. 介绍redis缓存，雪崩击穿等\n3. redis执行原理，没明白猜测可能是数据结构map啥的。\n4. ioc三级缓存\n5. 服务划分策略\n6. dubbo底层协议\n7. kafka 吞吐量，最大存储空间\n8. 问你项目数据量\n9. 线程池参数，怎么确定核心线程大小\n10. cpu突然增大怎么排查，网络io突然增大呢。\n11. fullgc怎么解决\n12. rpc中使用事务该咋办\n\n# CVTE Java开发 社招 已拿offer\n> 作者：七里翔\n链接：https://www.nowcoder.com/discuss/679983?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n本人二本菜鸡，两年工作经验。一毕业就在一家小公司，感觉技术达到瓶颈了，所以想去一些大点的公司提升提升技术。\n半个月前面了 CVTE ，一共四面，两轮技术面两轮HR面，最后入职了（虽然号称广州四坑之一，但这已经是我拿到的offer里面最好的了，而且入职后感觉还不错，没网上说的那么坑），大概整理一下 面经 ，两轮HR面就不整理了，跟其它的 面经 差不多，都是问一些常规的问题：性格、爱好、家里人什么的。\n\n## 一面 差不多70分钟\n1. 从 项目 开始问， 项目 架构、SpringCloud Alibaba常用组件、 redis zset底层实现、 redis 常用数据结构， 项目 中可能出现的问题，怎么解决\n2. MySQL：哈希索引和B+树索引、聚簇索引和非聚簇索引、回表怎么优化（覆盖索引）、索引最左前缀原则、假如有个组合索引ABC，以A为查询条件，走不走索引，以B为条件呢？组合索引的索引结构是什么样子的？事务隔离级别有哪几种？分别解决了什么问题？慢查询优化流程、MySQL有哪些锁\n3. 什么是NIO？IO多路复用是什么？\n4. Mybatis的#和$区别\n5. 线上OOM怎么排查\n6. 过滤器和拦截器区别、SpringBoot自动配置原理、SPI机制\n\n## 二面  50分钟左右\n1. 简单聊了聊我的一个python 项目\n2. 聊了聊我的一个Spring Cloud 项目 架构、非对称加密、安全性问题\n3. Spring的AOP怎么实现、JDK动态代理和cglib动态代理有什么区别？\n4. MySQL大分页优化、隔离级别、MVCC实现原理\n5. JVM优化\n6. redis 分布式锁原理、分布式锁问题：死锁(加过期时间)、超时释放(看门狗，自动续期)\n7. 我做组长优化了什么流程，具体是怎样\n\n# 记录2021字节客户端提前批面经\n> 作者：穿牛仔裤的小牛仔\n链接：https://www.nowcoder.com/discuss/680673?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n转眼就又到了暑假了，把去年找工作时字节跳动客户端提前批的面试给翻了出来，给今年的同学一些参考，希望大家快速上岸。\n\n## 一.字节跳动一面（6.30）\n1. 项目介绍\n2. 说一说retrofit\n3. okhttp中的责任链模式讲一下\n4. TCP三次握手四次挥手\n5. Synchronized和volatiled的区别\n6. 乐观锁和悲观锁\n7. Synchronized的锁升级机制\n8. handler机制\n9. App的启动过程\n10. CAS\n11. Activity的启动过程和启动模式，分别的应用场景\n12. Tcp/Udp的区别\n13. tcp可靠性的保证\n14. 动态代理的实现原理\n15. 算法题：根据前中序重构二叉树\n\n## 二.字节跳动二面（7.7）\n二面只想起来一部分内容\n\n1. hashmap\n2. 能否自己写出一个求hash值的函数（算是半个算法题吧）\n3. 讲一讲okhttp\n4. http中如何实现缓存\n5. 那在okhttp中如何实现缓存\n6. DNS的解析过程\n7. 算法题：场景题，能否写一个函数给定安卓界面上最顶层的view得到安卓界面中view得深度\n\n## 三.字节跳动三面（7.13）\n1. 项目介绍\n2. ==和equals的区别，Stringbuilder和StringBuffer的区别等等java基础\n3. 项目中的难点\n4. 项目中学到了什么\n5. 算法：螺旋矩阵\n\n不知道为何三面突然问起了java基础，项目里的内容也没有深究，可能是因为非科班，安卓端又非常缺人。\n\n# 商汤Java二面面经\n> 作者：SKY技术修炼指南\n链接：https://www.nowcoder.com/discuss/681206?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n更新进度：已挂，正式批不准备投商汤了\n\n开场和面试官聊了几分钟，气氛还挺轻松，后面面试官一直追着某一个点深挖，挖的实在答不上来了才换别的话题，压力山大\n1. 介绍一下项目实现的功能，技术栈等\n2. 项目是单体服务还是微服务，UI是用什么做的\n3. 介绍一下Kafka，有什么功能\n4. Kafka里面有哪些专业的概念？术语名词？\n5. Kafka中partition越多，吞吐量越大吗？\n6. 消息发送到Kafka中，是存在topic还是partition还是哪里\n7. topic和partition是什么关系，可以没有partition只有topic吗？\n8. 把消息发到topic上，消息存储是有序的吗？\n9. 怎么保证消息发到一个partition里面\n10. 怎么保证Kafka的冗余性，使数据不丢失？\n11. 消息写到partition里面是同时写到主副partition里面还是写完主partition再同步到副partition？\n12. partition副本太多会有什么影响？\n13. 可以在同一个服务器上部署多个Broker吗，可以不放在Docker里面吗\n14. Kafka的默认端口是什么\n15. 你是怎么看ES的\n16. 项目的Spring Boot的版本是多少\n17. 项目过程中学到什么？有什么心得体会\n18. 你是怎么衡量并发数的\n19. jmeter需要配置哪些参数\n20. 了解过restful api吗\n21. https比http在连接过程中多了哪些环节\n22. 典型的对称加密和非对称加密算法有哪些\n23. Java 8里面有哪些新特性\n24. lamda表达式有哪些使用场景\n25. 比较器Comparator类需要重写什么方法呢\n26. compare方法和compareTo有什么区别\n27. 假设服务器里面有A.txt的文件，文件里有许多英文字符，说一下B.java实现读出A.txt中内容的思路\n28. Java里面处理文件的常见的工具类有哪些\n29. BufferedReader和BufferedInputStream之间的区别\n30. 刚才读文件的场景应该用字符流还是字节流呢\n31. 如何将字符流中的byte转成字符呢\n32. 读取文件这个逻辑中还需要注意什么\n33. 读文件可能发生哪些Exception呢\n34. 异常捕获可以写多个catch吗？\n35. 可以只写try不写catch吗，能编译成功吗？\n36. finally是必要的吗？\n37. 可以只有try和finally没有catch吗？\n38. 假如我们上面的B.java写完了，用什么命令运行起来？\n39. 假如B.java用到了第三方的类，在别的目录下，要怎么编译？\n40. 怎么判断服务器的端口是否被占用？\n41. 怎么查看linux当前目录的大小\n42. Maven的生命周期\n43. 用Maven编译的命令是什么\n44. 用过哪些版本控制软件，创建git分支的命令是什么\n45. 反问环节\n\n二面面了50几分钟，感觉明显没有一面轻松了，还是希望面试官高抬贵手放我进三面\n\n# Shopee深圳后端提前批一面凉经 \n> 作者：素炒年糕\n链接：https://www.nowcoder.com/discuss/682083?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n结局反转了家人们，收到复试邀约了\n_________________________________________________________\n\n真真正正的一问三不知，估计给面试官也整吐了。楼主是Java，没问语言\n## Linux\n1. 查看cpu占用情况的命令，内存呢\n2. 查看磁盘占用情况的命令\n3. 怎么查看文件描述符\n4. BufferCache是什么？有什么作用？\n## 网络\n1. TCP四次挥手的具体过程\n2. 为什么四次，三次可能出现什么情况\n3. 服务端收到客户端FIN报文后，可以把它的ACK和FIN一起发出去，变成三次挥手吗\n4. Keepalive是什么，具体过程\n5. 除了TCP，应用层用到了Keepalive吗\n6. 为什么要额外实现Keepalive，有什么好处？\n7. 用户层面实现这个心跳检测机制有什么用，有什么是TCP的Keepalive做不到的但用户层面可以做到的？\n8. 滑动窗口是什么，为什么要有滑动窗口\n9. 如果没有滑动窗口，客户端也可以一下发很多包出来，会出现什么问题\n## 数据库\n1. 一个sql语句，分析一下，怎么建索引\n2. 为什么要建联合索引，最左前缀\n3. 稍微改一下，能命中索引吗，怎么优化\n4. 联合索引失效，索引下推\n5. Join知道吗？Join索引呢\n6. 事务隔离级别，具体怎么实现的\n7. MVCC是什么，作用呢，怎么实现的\n8. undo log怎么实现的MVCC\n9. LRU是什么，怎么实现的，mysql用到了吗？做了什么优化，为什么这么做\n## 手撕代码\n一个购物车算最接近余额的题，我以为是背包，想错了没做出来，后来面试官说直接穷举，我也太蠢了，直接凉透\n\n一些细节想不起来了，基本追问的问题就没答出来过\n\n# shopee后端一面面经\n> 作者：牛客566012114号\n链接：https://www.nowcoder.com/discuss/682164?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n1. 项目\n2. 怎么用redis缓存，redis做缓存主从不一致的问题\n3. http长连接和短连接\n4. http各个返回码的意思，302？400?500?\n5. tcp 为什么是四次分手 状态转移\n6. 如果不等待2rtt的话会有什么后果，等待2rtt有什么不好\n7. 三次握手第三次丢失会怎么样\n8. MSL\n9. Linux 基本指令  top指令的作用\n10. 如何查看一个文件倒数100行，敲出来 tail\n11. 假设一个进程占用cpu特别多的资源，如何分析\n12. 操作系统底层结构组成\n13. 分级存储器？\n14. 进程的通信方式、用过哪些通信方式?死锁的条件、如何避免死锁\n15. 什么是僵尸进程？如何预防僵尸进程？什么是孤儿进程、如何避免孤儿进程？产生孤儿进程怎么处理？\n16. init进程的端口号是啥？init进程有社么作用？\n17. 索引的底层结构，为什么用B+树，用B树行么？红黑树行么?\n18. hashmap扩容方式？为什么是*2？其他扩容方式比如指数扩容有什么问题？\n19. 了解什么高级数据结构？跳表？redis为什么用跳表\n20. 手撕代码：一个链表，长度为n，给定整数k，k\n\n# 2022虾皮提前批后端一面和二面HR面已意向\n> 作者：blrfcwwwwwwwwwwww\n链接：https://www.nowcoder.com/discuss/682261?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 虾皮shopee后端一面，二面，HR面，base深圳\n7.11一面总共花了35分钟，个人觉得有点短，面试官说赶时间，最后一个算法题没让我撕就让我说思路\n\n1. 自我介绍\n2. 进程、线程、协程\n3. TCP的特点和要怎么改进\n4. 跳表数据结构，redis中哪里用到了跳表\n5. B+树特点\n6. mysql索引\n7. 哈希索引\n8. 联合索引，最左前缀匹配规则，sql优化器在其应用\n9. 死锁必要条件和预防的基本方法，检测死锁\n10. 检测死锁的表可以用什么实现，我说的hashmap，面试官提醒我图可不可以，答有向无环图可以\n11. redis集群模式，什么情况下需要集群模式，redis主从复制原理，什么时候全量复制，什么时候增量复制\n12. 智力题，9000g面粉，有50g和200g面粉，一个天平，怎么样三次内获得2000g面粉\n13. 代码：链表反转\n14. 代码：单链表，排序，时间复杂度为O（nlogn），快慢指针加归并排序\n\n\n## 7.18二面  45分钟左右\n1. 自我介绍\n2. 项目。项目包括数据库（kafka，redis，mysql），分布式，分布式锁实现等。\n3. 根据项目中用到的技术栈问八股和自己的理解：包括：\n4. kafka的作用，业务中依据什么划分数据重要性，为什么要用kafka，为什么设置分区，是否有序，消息队列的作用和应用场景（与kafka的作用类似，只是更抽象一点）。\n5. 为什么要用缓存redis，对于redis的理解，redis基本数据结构string，redis的cluster模式和主从模式分别是干什么用的（本来想扯一下hash槽和一致性hash算法但是搞忘了）。\n6. 分布式锁是怎么实现的，setnx是怎么设置过期时间的，如果时间过短和时间过长分别会有什么影响。\n7. mysql和redis数据一致性是怎么保证的，mysql中数据量多大，redis中数据保存多少条，redis缓存淘汰策略是什么，lfu和lru的区别，lfu中频率相同的数据是什么淘汰策略。\n8. 根据项目问完相关技术选型开始传统八股：\n9. 集合有哪些，treeset和treemap的底层实现（答不知道！！）\n10. hashset的本质原理\n11. hashmap的rehash过程，扩容过程，put函数的底层\n12. mysql索引失效场景有那些\n13. 最左匹配原则的题（给了6个sql语句让判断那些命中索引那些没命中，以及为什么，这里要根据b+树底层的存储原理来答比较好）\n14. mysql的事务，ACID是怎么实现的，undolog和redolog的作用，历史读和当前读的区别等等。\n15. 场景题：一亿个url怎么样找到其中重复top100的url（这个我答的不太好，一开始想着使用bitmap去处理大体量数据，没想到hash冲突怎么办，这个答得最不好）\n16. 算法题：数组的奇数放在奇数位，偶数放在偶数位，先说思路再敲，大概用了3分钟，简单双指针的题。\n\n\n## 7.29  HR面\n1. 为什么选虾皮\n2. 对工作比较看重什么\n3. 硕士毕业的东西准备的怎么样了\n4. 未来规划\n\n# 7月10日 蔚来提前批 后端开发1、2、3面 面经\n> 作者：SYFsf\n链接：https://www.nowcoder.com/discuss/682318?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 一面 7-10 早上10点 37min\n1. Netty（项目中用到了，大概聊了一下），简单介绍，IO多路复用\n2. BIO、NIO的区别。（上个问题中我说到了NIO）\n3. NIO、AIO的区别。\n4. IO多路复用底层在Linux中的原理（用的select、poll、epoll）\n5. redis有哪些特性使它能作为缓存（在内存中，单线程）\n6. redis为什么单线程还能快（忘了，没答上）\n7. 数据一致性（忘了，没答上）\n8. HTTPS如何保证传输安全性（说了下HTTPS的加密过程）\n9. HashMap如何处理Hash冲突\n10. 其他的处理Hash冲突的方式\n11. Java8中相对于Java7，对HashMap做了哪些优化\n12. ConcurrentHashMap如何保证线程安全\n13. HashMap能不能存空键和空值\n14. ConcurrentHashMap能不能存空键和空值\n15. MySQL中InnoDB的索引为什么使用B+树而不用别的结构\n16. 聚簇索引和非聚簇索引\n17. user表，有id、name、age等信息，去查的时候，索引是怎么用的\n18. 算法题\n```给出一个仅包含字符'(',')','{','}','['和']',的字符串，判断给出的字符串是否是合法的括号序列\n括号必须以正确的顺序关闭，\"()\"和\"()[]{}\"都是合法的括号序列，但\"(]\"和\"([)]\"不合法。\n```\n19. 项目中用消息中间件主要解决哪些问题、起到什么作用\n20. 如何保证消息丢失的情况\n21. MySQL事务隔离级别\n22. 每种隔离级别解决了什么问题\n23. 使用start()启动线程和run()去启动线程有什么区别\n24. ThreadLocal为什么用完之后要手动去remove，如果不去remove会有什么问题（面试官看我不会，问我项目中有没有用到ThreadLocal，我说没有用到，就跳过这个问题了）\n25. sleep()和wait()的区别\n26. wait()方法为什么要放在Object类中（不会）\n27. 一个任务提交到线程池，说一下执行流程\n28. 线程池中达到最大线程数，之后任务量小了，核心线程数到最大线程数之间的这些线程也会去队列中竞争任务吗\n29. 反问环节\n\n一面结束之后，没几分钟就收到二面的邮件，确认参加后就进入面试房间等着了\n\n## 二面 10点55左右 30min\n介绍了一下自己做过的项目，然后问我更想聊哪个项目。然后就围绕着项目中的业务和技术进行场景拓展和深入，问题大多是如果xxxxx情况会怎么做然后逐步深入。深入到你不会就换别的。项目聊了10来分钟。\n\n1. Spring中常用的注解\n2. @Autowired和@Resource有什么区别\n3. Mybatis中的@MapperScan中的路径有什么注意事项\n4. AOP可以做什么\n5. AOP的实现原理，两种动态代理的区别\n6. 选择Spring的原因，它的优点是什么\n7. IOC的作用是什么，相比于非Spring的项目，优点在哪\n8. JVM的内存区域\n9. SQL优化方法\n10. MySQL中in和exist有什么区别\n11. volatile的作用\n12. 什么情况下会用到volatile\n13. 有没有想过会从事后端开发中的哪些业务功能和方向\n\n14. 反问环节\n\n反问的时候面试官说我过了，让我保持在线等下一轮面试。\n二面完也很快收到三面的邮件，点了确认参加然后就进房间等\n\n## 三面 11点40左右 43min\n1. 线程池有哪些参数，工作原理\n2. \n```\npublic void method1() {\nmethod2();\n}\n@Transactional\npublic void method2() {   \n}\n```\nmethod1没有事务注解，2有。这个类中1调用2，1会不会开启事务，为什么。\n3. 动态数据中求中位数，数据一直在动态增加，顺序也不固定，说设计思路\n4. 用数组实现一个队列类，包含以下方法：入队、出队、size()。写出代码后继续不断升级要求，循环使用数组空间，加锁等等。这一个题就搞了好久，写代码->调试->加要求->写代码->调试->加要求，循环。\n5. 买卖一次股票，最大利润。leetcode原题。\n   最后一个题写完就直接结束了，啥也没说。\n\n蔚来的面试体验整体来说非常好，面试官很和善，不会的题目也会给出正确的答案或者引导你去思考，回答中有不太准确的地方也会帮你纠正。一早上面三轮实在有点刺激。\n\n# 华为软开消费者云三面面经(已过) \n> 作者：一位爱分享的小白\n链接：https://www.nowcoder.com/discuss/682412?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n无论你是想内推还是想被内推的小伙伴都欢迎进入！！ 群里有超全面经答案与算法题讲解，up主都可以答疑的哦！随带巩固自己的知识，一举两得!! 哈哈哈！ QQ群：725936761\n\n## 一面：\n1. 股票问题\n2. StringBuff和StringBuilder\n3. compator和compatable的区别\n4. 线程池的类型，固定线程\n5. Arraylist和LinkedList\n6. 熔断机制， redis和mysql如何同步， kafka如果中途宕机了，\n7. 压测如何，承受压力如何\n8. 结算服务，描述，首先前端传入结算信息列表，加上消费券结算微服务。\n9. Redis宕机后怎么办  Redis有自己的保护机制：RDB和AOF，  还有kafka里也有消费信息，保证消息能正确消费。 如果没有将消息传入到kafka，Redis启动后会去看是否kafka里面有该消息。\n10. 这个是多点吗？\n11. 熔断机制。怎么解决\n12. 如果服务只能1000请求，来了1500个请求怎么办\n\n## 二面：\n1. 为什么用springcloud\n2. ngix和springcloud路由的区别分发\n3. mysql分库分表怎么处理，如果有大表\n4. 为什么要使用微服务\n5. 动态规划，数字匹配字符\n\n## 三面：\n1. 问实习经历\n2. 聊一些性格相关\n3. 聊部门\n\n# 4399 Java后端实习一面\n\n> 作者：魚禾\n链接：https://www.nowcoder.com/discuss/684030?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n1. 在你的项目中遇到的棘手的问题\n2. 跨域\n3. 同源策略\n4. 你的课程表怎么设计的\n5. 数据库查询很慢怎么办\n6. 那怎么分库分表的呢\n7. 别人能随便访问你的接口吗，你有做什么处理吗\n8. 你做了权限控制，那别人用命令（比如：SQL注入）来攻击，怎么办\n9. 你知道还有什么Web的攻击方式吗\n10. 怎么保存用户的登录状态的\n11. 用过什么缓存\n12. Redis挂了，数据会丢失吗\n13. Redis的持久化机制说下，RDB+AOF\n14. 一般用哪种\n15. 你的网站访问慢，优化策略\n16. 讲下nginx怎么做负载均衡\n17. nginx如何替换url（不是alias和proxy_pass，搞错了，太尴尬了）\n18. Linux下如何查找nginx进程并将它杀死\n19. Linux下，nginx访问日志，查访问量最高的10个ip\n\n\n基本都是场景题😅\n\n# 字节提前批后端开发——一、二面面经\n> 作者：我一直都很浪\n链接：https://www.nowcoder.com/discuss/684077?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 一面\n7.14，面了70分钟左右，大致内容是这些，还有一些忘记了\n1. 自我介绍\n2. 介绍下跳表实现，扯到了平衡二叉树，让简单对比下\n3. 介绍下哈希表实现\n4. 如何实现线程安全的哈希表，简单介绍下\n5. 联合索引相关场景，给了个sql，问能不能用上索引\n6. InnoDb针对数据库缓冲池管理使用LRU算法，做了哪些优化（太久没看搞忘了）\n7. redis的LRU淘汰策略做了哪些优化（没了解过）\n8. JVM垃圾回收算法\n9. TCP3次握手，socket系统调用中如何完成3次握手（面试官引导了半天但实在不会…）\n10. TCP拥塞控制相关\n11. 算法题：力扣82\n## 二面\n7.16，时长60min，问题基本上都是围绕着项目展开的，全是设计题，麻了...\n1. 自我介绍\n2. 项目简单介绍\n3. 介绍主流的工作流引擎、规则引擎\n4. 如果让你设计一个工作流引擎，该怎么设计\n5. 看你项目用到了线程池，介绍下线程池的拒绝策略\n6. Java线程如何创建、销毁（销毁没了解过）\n7. Java线程生命周期\n8. 如果让你设计一个线程池，该怎么设计\n9. 你项目中用到了Ehcache，介绍下它的内存存储策略\n10. 让你设计一个缓存框架，该怎么设计？\n11. 了解I/O模型吗，介绍下\n12. 介绍下select、poll、epoll区别\n13. 100块钱分给6个人，每个人不少于10块，该怎么分？\n14. 算法题：跟leetcode718差不多只不过数组换成了字符串，不知道是不是第1062题，办不起会员看不了…\n\n设计题回答的有点烂，下来想了想感觉直接从数据结构上来答相对好点（虽然面的时候面试官已经给了提示了，但当时脑子处于懵逼状态...）\n\n# 字节后端一面凉经\n> 作者：zttttttt\n链接：https://www.nowcoder.com/discuss/684153?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n1. java的内存模型知道吗 有什么  ？（求问这个问题怎么回答比较好 😥 😥）\n2. 可见性是什么\n3. java中声明的变量 哪一些会在工作内存 哪一些在主内存\n4. 换一个问法 在JVM当中哪一些区域是线程共享哪一些是私有\n5. new声明的变量那就是共有的对吗\n6. 本地方法栈存放的是什么\n7. 说一下java线程不安全的问题，还有如何实现线程安全\n8. 两个线程对一个变量同时写不安全，那在保证有序的情况下即（一个线程写一个线程读保证交替执行）这样会导致线程不安全吗\n9. volatile关键字的作用是什么\n10. volatile和synchronized reentrantlock的区别是什么\n11. mysql的事务隔离级别有哪些\n12. 脏读和幻读分别是什么，如何解决脏读和幻读\n13. OSI七层模型是什么\n14. TCP协议位于哪一层\n15. 说一下TCP的建立连接和断开连接的过程\n16. 如果有大量请求从客户端发送给服务端，但是不发送ack确认会怎样 （半连接队列->超时重传->删除），会造成服务器什么后果\n17. 如果大量恶意请求发送过来不发ack，要怎么处理\n18. 算法：不递归实现树的后序遍历\n19. 反问：表现及总共多少轮面试\n\n# 百度提前批一面凉经 (哭唧唧) \n> 作者：牛客971687418号\n链接：https://www.nowcoder.com/discuss/684327?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n1. JVM中锁的关键字\n2. JVM内存模型\n3. 类的生命周期和类加载\n4. 给对象开辟内存空间后，将相应的地址赋值给引用，有几种方式\n5. jvm类加载\n6. Synchronized的锁升级\n   1. 轻量级锁和偏向锁的区别\n   2. 锁升级过程\n   3. Synchronized加锁的类型，类锁和对象锁\n   4. Synchronized的锁类型，独占锁，可重入锁等\n   5. Synchronized经历过怎样的优化........\n7. Java的Exception\n8. Spring的IOC\n   1. 什么是IOC\n   2. 好处\n9. Mybatis，Jdbc，Hibernate之间的区别\n   1. 给jdbc封装了哪些功能\n   2. Mybatis原理\n   3. Mybatis如何实例化对象过程？？？？？\n10. MyIsam和Memmry区别\n11. InnoDB如何存数据的\n12. 聚簇索引和非聚簇索引的区别，非聚簇索引，如果数据不在叶子节点上，如何找到磁盘上的数据\n13. 一个表中有一列数据是id，并且是索引，找到id>2的遍历过程\n14. redis常用的数据结构\n15. redis的HashMap底层？扩容原理？缩容呢？\n16. redis集群有哪几种\n    1. 在一主一从中\n    2. 如何同步\n    3. 如果主从服务器数据相差太多，是如何同步\n17. Linux命令如何查找占用某个端口的线程\n18. [1,1,2,2,3,3,4,4,5]如何不开辟新空间，找到这个的单一的数字\n# 货拉拉后端研发一面面经\n> 作者：牛客246750689号\n链接：https://www.nowcoder.com/discuss/684427?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n1. 自我介绍\n2. synchronized？lock锁了解吗？\n3. 线程池的参数？\n4. 垃圾回收算法？\n5. CMS？\n6. springboot启动类上的注解？\n7. mybatis中的$和#区别？\n8. mysql索引？\n9. 索引优化？\n10. 隔离级别？\n11. redis？\n12. linux指令：\n13. 项目里服务器怎么分配内存资源的？？\n14. 查找文件指定内容？\n15. 输出文件内容？\n16. top？awk?\n17. 反问\n\n# 腾讯java后端实习（深圳腾讯CSIG教育部门）\n> 作者：马路小狮子🐯\n链接：https://www.nowcoder.com/discuss/684567?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n### 上来先抛出两道编程题\n手撕代码\n1. 编程题一：对角线排序\n链接： https://leetcode-cn.com/problems/sort-the-matrix-diagonally/\n2. 编程题二：格雷编码\n链接： https://leetcode-cn.com/problems/gray-code/\n### 然后问问题：\n1. 自我介绍\n2. 说一下hashmap扩容机制\n3. 了解linux IO模型吗(BIO, NIO, AIO, java.io)\n4. 说一下linux中的零拷贝\n5. 说一下TCP三次握手过程\n6. TCP为什么三次握手，四次挥手\n7. 说一下滑动窗口和流量控制\n8. java.util.concurrent包中都有哪些类\n9. 说一下mysql四种隔离级别\n10. 说一下mysql中如何看select用到了哪些索引（explain）\n11. 说一下java高并发应用场景（比如：五个线程同时运行， CountDownLatch, CyclicBarrier）\n12. 讲一下的科研情况\n13. 你用到的神经网络模型及落地情况\n14. 反问\n\n## 二面（2021/7/20）\n1. 先自我介绍\n2. 抛出一个代码手撕（不能用sort, hashset，要求时间空间复杂度）\n3. 链接： https://leetcode-cn.com/problems/find-all-numbers-disappeared-in-an-array/\n4. 提问问题：\n5. http请求行，请求头中的内容\n6. DNS服务器如何work\n7. 状态码500, 502, 504的区别，什么时候需要关注\n8. 状态码403,404,302区别，什么时候需要关注\n9. 说一下XSS攻击和CSRF攻击\n10. 了解zookeeper和nginx吗\n11. 问项目\n12. 对linux理解到哪种程度\n\n# 2022百度提前批java后端二面\n> 作者：牛客677390903号\n链接：https://www.nowcoder.com/discuss/684679?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n刚面完百度二面，感觉面的不太好，在此先许愿一下有三面吧。\n1. 给一个二位数组，从左上角走到右下角，怎么走的路径加起来的值最小。想了用递归加动态规划，但是没做出来，就换了一道题。\n2. 二叉树按层输出，想了半天，后面才在前辈的引导下做出来。浪费太多前辈的时间了，真的很抱歉\n3. 说说虚拟内存和物理内存？   操作系统基础有点差，没学习到位，没答上。\n4. Transactional注解底层怎么实现的？spring只用过，没去看底层实现。没答上\n5. bean的生命周期？ 同上\n6. new一个对象的过程？\n7. 接着就问类加载过程？\n8. 再接着问双亲委派模型？\n9. 双亲委派模型的好处？怎么样会打破双亲委派模型？\n10. 类加载器有哪些？\n11. 知道tomcat的类加载器吗？\n12. 说说你了解的数据结构？在java中对应哪些类？详细说说这些类？\n13. 什么地方用到了树？\n14. http请求完一次TCP层会立即关闭吗？\n15. 了解redis吗？说说看\n16. 对称加密和非对称加密？什么地方用到了？\n17. 项目中用redis做什么？\n18. 知道http请求头中有哪些信息？\n19. java中用到了哪些锁？ReentrantLock的实现原理？\n20. 项目中怎么用的rabbitmq? 考虑过消息丢失吗或者消息传得太快来不及处理？\n\n# 虾皮一面面经\n> 作者：牛客899173605号\n链接：https://www.nowcoder.com/discuss/684696?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n1. 进程 线程的区别\n2. 协程？\n3. Linux 用过哪些命令\n4. 查看日志 tail怎么用\n5. 计算机网络层次 各层协议\n6. ping用的什么协议 ICMP\n7. http状态码 怎么分类的\n8. 1开头的状态吗\n9. 三次握手 为什么不是两次\n10. time-wait\n11. MSL是什么\n12. 短连接和长连接，短链接存在的问题？\n13. 最左前缀匹配\n14. 联合索引和唯一索引 走哪个索引\n15. 事务隔离级别\n16. 索引底层\n17. redis 有哪几种数据类型\n18. RDB AOF 默认用哪种？\n19. 怎么做分布式锁？\n20. setnx\n21. 算法 链表有没有环\n22. 快排 时间复杂度\n23. 青蛙跳\n24. 前k个最大的数\n\n# 美团社招(1年)java一面面经(过了)\n\n> 作者：。。201806192026925\n链接：https://www.nowcoder.com/discuss/684709?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n1. sleep 和 wait区别\n2. ThreadLocal，原理以及使用的有什么注意的地方(内存泄漏)\n3. 堆里面为什么有2个survivor区\n4. young gc 和 full gc的触发条件\n5. 讲一下对线程池的理解\n6. volatile\n7. 单核cpu用volatile有用吗\n8. 不稳定复现的bug怎么调试\n9. cpu使用率高怎么排查\n10. oom怎么排查\n11. 设计一个电梯调度系统，不需要具体实现，只需要给接口的入参和返回\n12. Ioc和AOP简单讲下\n13. 缓存常用的替代算法\n14. LRU怎么优化（讲了下mysql缓存池的LRU设计）\n15. 系统变慢了怎么排查\n16. 算法题\n```\n现在有一个只包含数字的字符串，将该字符串转化成IP地址的形式，返回所有可能的情况。\n    例如：\n    给出的字符串为\"25525522135\",\n    返回[\"255.255.22.135\", \"255.255.221.35\"]. (顺序没有关系)\n```\n# 阿里Java开发社招一面\n> 作者：Arealy仁辰\n链接：https://www.nowcoder.com/discuss/684765?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n一年经验，Java开发\n时间：2021年7月15日（1小时53分钟，电话）\n1. 自我介绍；\n2. 项目延申；（延展的非常非常非常深）；\n3. new一个对象，从底层来说会发生什么；\n4. new一个HashMap，for循环put进入10000个数据会发什么；\n5. 快速排序和归并排序的原理，以及时间、空间复杂度解释一下，并且让手撕代码；\n6. 10亿个数据对象（其中包含属性最早创建时间createTime），求其中最早的十个有什么方法？那自己设计一个数据结构该怎么设计？\n7. 手撕 NC50 链表中的节点每k个一组翻转（原题稍有变更）；\n8. 手撕模拟多线程实现银行转帐（未同步与同步）；\n9. 你平时是怎么学习新技术的？\n10. 你还有什么想问的吗？\n# shoppe虾皮深圳提前批Java一面面经\n> 作者：lmwis\n链接：https://www.nowcoder.com/discuss/685037?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n一个小时问的很基础但我都玩忘了\n1. 介绍下二叉树和AVL树\n2. UDP和TCP的区别\n3. UDP和TCP的使用场景\n4. TCP如何做到可靠的\n5. HTTP和HTTPS的区别\n6. 线程和进程的区别\n7. IO多路复用的实现，三种的区别和优劣势\n8. redis数据结构和底层数据结构\n9. 讲一下跳表\n10. mysql有哪些引擎，哪些是支持事务的\n11. 事务的隔离级别\n12. 慢sql 分析优化\n13. 消息队列的作用\n14. JVM内存区域，哪些私有哪些公有\n15. JVM内存分配和回收策略\n16. 算法题： 最长不含重复字符的子字符串\n\n# 字节提前批-一面面经-后端开发 \n> 作者：暂停丶算不算放弃\n链接：https://www.nowcoder.com/discuss/685356?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n大概内容是这个，问的八股文比较多有的可能没想起来。\n1. 自我介绍\n2. 稍微问了问项目里面的一些实现细节？\n3. Redis如何实现分布式锁？\n4. 获取锁的线程宕机了怎么办？\n5. Redis的数据类型？\n6. Redis中sort set如何实现？\n7. Redis为什么速度快？（只说了是在内存中，面试官说还有就是redis不需要锁）\n8. MySQL的InnoDB索引的数据结构？\n9. MySQL有什么锁？\n10. 说一下脏读和幻读？\n11. MySQL如何防止发生幻读？\n12. 介绍一下GC\n13. 新生代中为什么有两个 Survivor区？\n14. 类加载流程\n15. 双亲委派机制\n16. 僵尸进程和孤儿进程是什么区别？（不会）\n17. 说一下HashMap实现原理？\n18. 为什么链表长度为8要转化为红黑树？(不会)\n19. 扩容因子默认为什么是0.75？\n20. 说一下CurrentMap如何实现线程安全的？\n21. 什么是CAS？ \n22. synchronized是可重入锁么？\n\n还有一些想不起来了，都是一些老八股文了。下面开始问算法。\n\n1.一共有1000个小球分别装入10个盒子中，每个盒子可以放任意个小球。然后一个人需要1-1000任意的个数的小球，但是他只能拿1个或者多个盒子，请问每个盒子中应该放多少个小球才能满足要求？（这个只是手算了一下每个放多少，说了一下，并没有然写具体代码）\n\n2.一个链表右移k位，返回移动后的链表。（这个让代码实现了\n\n# 百度暑期实习面经三轮 后端 \n> 作者：明光村乔建永\n链接：https://www.nowcoder.com/discuss/685677?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 一面：\n1. 算法题：二叉树的左视图\n2. 算法题：三数之和\n3. HTTP状态码有哪些，分别有什么含义\n4. HTTP是如何复用tcp连接的，还是每次请求都重新握手\n5. 讲一下你对HTTP代理的理解，正向代理反向代理\n6. 讲一下CORS\n7. HTTP服务如何优雅的重启\n\n## 二面：\n1. 抽象类和接口的区别，接口可以继承吗，类可以多继承吗？接口可以多继承吗？\n2. JAVA基本数据类型和占几个字节，java的三种注释方法解释一下\n3. jvm的结构，运行时数据区有什么，非运行时数据区有什么，堆栈各放什么，说一下栈帧\n4. int i =1，integer i=1，在jvm各放在什么位置，基本类型如何进行包装的，integer的缓冲区\n5. 类加载机制，几种类加载器，双亲委派，可以自己写一个java.lang.string吗\n6. cas的unsafe类用到了自旋锁讲一下\n7. hashmap 1.7 1.8中数据结构 put 扩容\n8. 用过什么注册中心，nacos和eureka区别，自己实现注册中心注意什么，如何监控服务是否在线\n9. 熔断限流用的什么，底层是如何实现的\n10. mysql的bin log、redo log、undo log讲一下\n11. 存储引擎inodb和myisam区别\n12. 建一张表要考虑什么方面\n13. 性别列可以选择建索引吗，为什么\n14. having、group by、order by、limit、desc顺序，limit 2,3什么意思\n15. 大数据量使用limit分页时如何进行优化\n16. redis的key的过期策略\n17. redis是单线程的吗，为什么这么快\n18. git的常用命令，git add/commit分别是把文件放到了什么地方\n19. docker的优势，讲一下沙箱隔离机制，为什么docker比虚拟机快，了解k8s吗\n20. nginx的负载均衡策略，一致性hash了解吗\n21. 定时任务Quartz中解释cron表达式含义\n22. 讲一下之前做过的一个项目\n23. http状态码\n24. https和http区别，https建立连接过程，SSL/TSL是哪一层协议，是跟https绑定的吗，可以用于其他协议吗\n25. 算法题\n    1. 在整数数组中，如果一个整数的出现频次和它的数值大小相等，我们就称这个整数为「幸运数」。 给你一个整数数组 arr，请你从中找出并返回一个幸运数。如果数组中存在多个幸运数，只需返回 最大 的那个。如果数组中不含幸运数，则返回 -1 。\n    2. 一个列表n个视频，属于不同的m类，需要让相邻的视频不能属于同一类\n\n## 三面：\n1. 挑一个你觉得最成功的的项目讲一下具体的细节实现（半小时）\n2. 算法题：包含‘（’和‘）’、数字、字母的字符串中，找出最长有效（包含在一对括号中，中间不出现括号）子串的长度，例如(ss)(v89)，max=3；(ss)(v8)9)，max=2\n3. 程序设计题：司机开动火车，考察oo思想\n4. sql题：查出成绩大于等于60分的男女比例\n5. redis的数据结构，list的pop命令时间复杂度，怎样实现一个消息队列，怎么实现一个排行榜\n6. http状态码\n7. linux如何查看进程，如何查看日志，定位行数\n\n# 百度校招一面面经 \n> 作者：心有庭树\n链接：https://www.nowcoder.com/discuss/685831?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n先自我介绍\n1. 先说说集合吧，又问了hashmap的原理，把与集合有关的都回答了。\n2. JVM区域划分，每一部分再细讲一下。\n3. 有哪些垃圾回收算法，详细说了3个GC算法。\n4. 类加载的步骤，还有内存回收的过程。\n5. 讲一下G1垃圾回收器，这个还需要再往深的学一下。\n6. 死锁\n7. 讲一下volatile这个关键字，面试官说我讲的有点少。\n8. 描述一下什么是乐观锁，悲观锁。\n9. volatile和synchronize的区别。\n10. 面向对象的六大原则，这个没有回答出来，只知道单一职责原则和开闭原则。\n11. mysql常见的索引方式。再把每种索引方式讲一下。\n12. 事务的四种隔离机制，每种隔离机制分别会引发什么并发问题。\n13. 介绍一下MVCC。\n14. 说一下你熟悉的设计模式。我讲了单例模式、工厂模式、代理模式，可能讲的有点少。\n15. 说说AOP吧。\n16. 讲一下Spring中的IOC吧。\n\n# 字节后端提前批一面面经 \n> 作者：lmwis\n链接：https://www.nowcoder.com/discuss/685872?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n45min，问的贼多，数据库给我问穿了\n\n1. 自我介绍+讲项目\n2. java并发你熟是吧，讲讲synchronized和volatile，原理啥的\n3. 原子类你用过吗，讲一讲\n4. ABA是啥，咋解决\n5. 事务ACID分别啥意思\n6. 隔离级别啥的bulabula\n7. MVCC有啥优缺点\n8. mysql事务ACID是咋实现的\n9. mysql锁你知道哪些，有啥逻辑\n10. 间隙锁是啥\n11. next-key是啥\n12. 为什么索引B+树\n13. 为什么B+树高度就小，是因为什么，B树为什么就高一些\n14. 千万行的数据这个B+树索引大概多高\n15. redis你也熟是吧，这个zset是咋实现的（我不是我没有😭 😭）\n16. 什么用跳表不用B树，红黑树这些\n17. redis AOF和RDB有啥优缺点\n18. AOF重写你了解过吗\n19. redis主从架构你了解吗，怎么做数据同步的\n20. mysql主从同步咋做的\n21. redis缓存穿透，缓存击穿，缓存雪崩，什么概念和解决方案\n\n还有几个不会的问题忘记了，因为都没有听过这个概念，什么mysql什么模式，有什么物理还有什么什么来着\n凉了凉了\n\n# 社招一年半经验后台开发岗美团一面面经分享\n> 链接：https://www.nowcoder.com/discuss/685985?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n\n社招一年半经验后台开发岗美团一面面经分享\n\n蓝的天白的云\n编辑于 2021-07-19 10:48:57APP内打开赞 1 | 收藏 58 | 回复3 | 浏览5193社招一年半经验后台开发岗美团一面面经分享APP内打开 1 58 3 分享\n美团\n## 一面\n1. 线程安全的类有哪些，平时有使用么，用来解决什么问题\n2. mysql日志文件有哪些，分别介绍下作用\n3. 你们项目为什么用redis，快在哪，怎么保证高性能，高并发的\n4. redis字典结构，hash冲突怎么办，rehash，负载因子\n5. jvm了解哪些参数，用过哪些指令\n6. zookeeper的基本原理，数据模型，znode类型，应用场景有哪些\n7. 一个热榜功能怎么设计，怎么设计缓存，如何保证缓存和数据库的一致性\n8. 容器化技术了解么，主要解决什么问题，原理是什么\n9. 算法：对于一个字符串，计算其中最长回文子串的长度\n项目介绍\n\n美团\n因为之前的部门一面通过后，该部门没有hc了，就给我推荐到其他部门了，大厂hc还是挺紧张的\n\n## 一面\n1. redis集群，为什么是16384，哨兵模式，选举过程，会有脑裂问题么，raft算法，优缺点\n2. jvm类加载器，自定义类加载器，双亲委派机制，优缺点，tomcat类加载机制\n3. tomcat热部署，热加载了解么，怎么做到的\n4. cms收集器过程，g1收集器原理，怎么实现可预测停顿的，region的大小，结构\n5. 内存溢出，内存泄漏遇到过么，什么场景产生的，怎么解决的\n6. 锁升级过程，轻量锁可以变成偏向锁么，偏向锁可以变成无锁么，自旋锁，对象头结构，锁状态变化过程\n7. kafka重平衡，重启服务怎么保证kafka不发生重平衡，有什么方案\n8. 怎么理解分布式和微服务，为什么要拆分服务，会产生什么问题，怎么解决这些问题\n9. 你们用的什么消息中间件，kafka，为什么用kafka，高吞吐量，怎么保证高吞吐量的，设计模型，零拷贝\n10. 算法1：给定一个长度为N的整形数组arr，其中有N个互不相等的自然数1-N，请实现arr的排序，但是不要把下标0∼N−1位置上的数通过直接赋值的方式替换成1∼N\n11. 算法2：判断一个树是否是平衡二叉树\n\n# 社招一年半经验后台开发岗美团二面、三面面经分享\n> 作者：蓝的天白的云\n链接：https://www.nowcoder.com/discuss/685990?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 二面\n1. Innodb的结构了解么，磁盘页和缓存区是怎么配合，以及查找的，缓冲区和磁盘数据不一致怎么办，mysql突然宕机了会出现数据丢失么\n2. redis字符串实现，sds和c区别，空间预分配\n3. redis有序集合怎么实现的，跳表是什么，往跳表添加一个元素的过程，添加和获取元素，获取分数的时间复杂度，为什么不用红黑树，红黑树有什么特点，左旋右旋操作\n4. io模型了解么，多路复用，selete，poll，epoll，epoll的结构，怎么注册事件，et和lt模式\n5. 怎么理解高可用，如何保证高可用，有什么弊端，熔断机制，怎么实现\n6. 对于高并发怎么看，怎么算高并发，你们项目有么，如果有会产生什么问题，怎么解决\n7. 项目介绍\n算法：给定一个二叉树，请计算节点值之和最大的路径的节点值之和是多少，这个路径的开始节点和结束节点可以是二叉树中的任意节点\n\n## 三面\n1. 项目介绍\n2. 算法：求一个float数的立方根，牛顿迭代法\n3. 什么时候能入职，你对岗位的期望是什么\n4. 你还在面其他公司么，目前是一个什么流程\n\n\n# 阿里开发岗一面面经（社招）\n\n> 作者：ce、欢笙\n链接：https://www.nowcoder.com/discuss/685998?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n阿里\n## 一面\n1. synchronized原理，怎么保证可重入性，可见性，抛异常怎么办，和lock锁的区别，2个线程同时访问synchronized的静态方法，2个线程同时访问一个synchronized静态方法和非静态方法，分别怎么进行\n2. volatile作用，原理，怎么保证可见性的，内存屏障\n3. 你了解那些锁，乐观锁和悲观锁，为什么读要加锁，乐观锁为什么适合读场景，写场景不行么，会有什么问题，cas原理\n4. 什么情况下产生死锁，怎么排查，怎么解决\n5. 一致性hash原理，解决什么问题，数据倾斜，为什么是2的32次方，20次方可以么\n6. redis缓存穿透，布隆过滤器，怎么使用，有什么问题，怎么解决这个问题\n7. redis分布式锁，过期时间怎么定的，如果一个业务执行时间比较长，锁过期了怎么办，怎么保证释放锁的一个原子性，你们redis是集群的么，讲讲redlock算法\n8. mysql事务，acid，实现原理，脏读，脏写，隔离级别，实现原理，mvcc，幻读，间隙锁原理，什么情况下会使用间隙锁，锁失效怎么办，其他锁了解么，行锁，表锁\n9. mysql索引左前缀原理，怎么优化，哪些字段适合建索引，索引有什么优缺点\n10. 线上遇到过慢查询么，怎么定位，优化的，explain，using filesort表示什么意思，产生原因，怎么解决\n11. 怎么理解幂等性，有遇到过实际场景么，怎么解决的，为什么用redis，redis过期了或者数据没了怎么办\n\n\n> 作者：ce、欢笙\n链接：https://www.nowcoder.com/discuss/686002?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 二面\n1. hashmap原理，put和get，为什么是8转红黑树，红黑树节点添加过程，什么时候扩容，为什么是0.75，扩容步骤，为什么分高低位，1.7到1.8有什么优化，hash算法做了哪些优化，头插法有什么问题，为什么线程不安全\n2. arraylist原理，为什么数组加transient，add和get时间复杂度，扩容原理，和linkedlist区别，原理，分别在什么场景下使用，为什么\n3. 了解哪些并发工具类\n4. reentrantlock的实现原理，加锁和释放锁的一个过程，aqs，公平和非公平，可重入，可中断怎么实现的\n5. concurrenthashmap原理，put，get，size，扩容，怎么保证线程安全的，1.7和1.8的区别，为什么用synchronized，分段锁有什么问题，hash算法做了哪些优化\n6. threadlocal用过么，什么场景下使用的，原理，hash冲突怎么办，扩容实现，会有线程安全问题么，内存泄漏产生原因，怎么解决\n7. 垃圾收集算法，各有什么优缺点，gc roots有哪些，什么情况下会发生full gc\n8. 了解哪些设计模式，工厂，策略，装饰者，桥接模式讲讲，单例模式会有什么问题\n9. 对spring aop的理解，解决什么问题，实现原理，jdk动态代理，cglib区别，优缺点，怎么实现方法的调用的\n10. mysql中有一个索引(a,b,c)，有一条sql，where a = 1 and b > 1 and c =1;可以用到索引么，为什么没用到，B+树的结构，为什么不用红黑树，B树，一千万的数据大概多少次io\n11. mysql聚簇索引，覆盖索引，底层结构，主键索引，没有主键怎么办，会自己生成主键为什么还要自定义主键，自动生成的主键有什么问题\n12. redis线程模型，单线程有什么优缺点，为什么单线程能保证高性能，什么情况下会出现阻塞，怎么解决\n13. kafka是怎么保证高可用性的，讲讲它的设计架构，为什么读写都在主分区，这样有什么优缺点\n14. 了解DDD么，不是很了解\n15. 你平时是怎么学习的\n16. 项目介绍\n\n> 作者：ce、欢笙\n链接：https://www.nowcoder.com/discuss/686004?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 三面\n1. 线程有哪些状态，等待状态怎么产生，死锁状态的变化过程，中止状态，interrupt()方法\n2. 你怎么理解线程安全，哪些场景会产生线程安全问题，有什么解决办法\n3. mysql多事务执行会产生哪些问题，怎么解决这些问题\n4. 分库分表做过么，怎么做到不停机扩容，双写数据丢失怎么办，跨库事务怎么解决\n5. 你们用的redis集群么，扩容的过程，各个节点间怎么通信的\n6. 对象一定分配在堆上么，JIT，分层编译，逃逸分析\n7. es的写入，查询过程，底层实现，为什么这么设计\n8. es集群，脑裂问题，怎么产生的，如何解决\n9. while(true)里面一直new thread().start()会有什么问题\n10. socket了解么，tcp和udp的实现区别，不了解，用的不多\n11. 设计一个秒杀系统能承受千万级并发，如果redis也扛不住了怎么办\n12. 项目介绍\n\n## 四面\n1. 讲讲你最熟悉的技术，jvm，mysql，redis，具体哪方面\n2. new Object[100]对象大小，它的一个对象引用大小，对象头结构\n3. mysql主从复制，主从延时怎么解决\n4. 怎么保证redis和mysql的一致性，redis网络原因执行超时了会执行成功么，那不成功怎么保证数据一致性\n5. redis持久化过程，aof持久化会出现阻塞么，一般什么情况下使用rdb，aof\n6. 线上有遇到大流量的情况么，产生了什么问题，为什么数据库2000qps就撑不住了，有想过原因么，你们当时怎么处理的\n7. 限流怎么做，如果让你设计一个限流系统，怎么实现\n8. dubbo和spring cloud区别，具体区别，分别什么场景使用\n9. 给了几个场景解决分布式事务问题\n10. 项目介绍\n11. 你觉得你们的业务对公司有什么实际价值，体现在哪，有什么数据指标么\n\n\n> 作者：ce、欢笙\n链接：https://www.nowcoder.com/discuss/686008?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 五面\nhr面完后又来了一面，说是交叉面\n1. 怎么理解用户态，内核态，为什么要分级别，有几种转换的方式，怎么转换的，转换失败怎么办\n2. 怎么理解异常，它的作用是什么，你们工作中是怎么使用的\n3. 你们用redis么，用来做什么，什么场景使用的，遇到过什么问题，怎么解决的\n4. jvm元空间内存结构，永久代有什么问题\n5. 你平时开发中怎么解决问题，假如现在线上有一个告警，你的解决思路，过程\n6. 你们为什么要用mq，遇到过什么问题么，怎么就解决的\n7. 你觉得和友商相比，你们的优势在哪\n8. 聊天：炒股么，为什么买B站，天天用，看好他\n\n# 京东软件开发岗面经（社招）\n> 作者：牛客895370106号\n链接：https://www.nowcoder.com/discuss/686024?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 京东\n## 一面\n1. tcp和udp的区别，tcp怎么保证可靠连接的，出现网络拥塞怎么解决\n2. tcp和udp的报文结构了解么\n3. 给了一个业务场景写sql语句\n4. 你们建表会定义自增id么，为什么，自增id用完了怎么办\n5. 一般你们怎么建mysql索引，基于什么原则，遇到过索引失效的情况么，怎么优化的\n6. jvm内存结构，堆结构，栈结构，a+b操作数栈过程，方法返回地址什么时候回收，程序计数器什么时候为空\n7. redis实现分布式锁，还有其他方式么，zookeeper怎么实现，各有什么有缺点，你们为什么用redis实现\n8. 算法：返回一个树的左视图\n\n## 二面\n1. spring你比较了解哪方面，讲讲，生命周期，bean创建过程\n2. 使用过事务么，遇到过事务失效的情况么，原因是什么\n3. springboot是怎么加载类的，通过什么方式\n4. 什么对象会进入老年代，eden和survivor比例可以调整么，参数是什么，调整后会有什么问题\n5. 微信朋友圈设计，点赞，评论功能实现，拉黑呢，redis数据没了怎么办\n6. 项目介绍\n7. 算法：给你两个非空的链表，表示两个非负的整数。它们每位数字都是按照逆序的方式存储的，并且每个节点只能存储一位数字。 请你将两个数相加，并以相同形式返回一个表示和的链表\n\n## 三面\n1. 感觉面试官对es很熟悉，一直问es问题\n2. es倒排索引，原理，lucene，分词，分片，副本\n3. es写数据原理，数据实时么，为什么不实时，会丢数据么，segment，cache，buffer，translog关系\n4. es深度分页，优化\n5. 项目介绍\n6. 算法：验证二叉搜索树\n\n# 字节Java开发社招一面 \n> 作者：Arealy仁辰\n链接：https://www.nowcoder.com/discuss/686123?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n一年经验，Java开发\n时间：2021年7月18日（53分钟，视频）\n1. 自我介绍；\n2. 项目介绍；（问的比较浅）\n3. Http和Https的区别？\n4. 了解哪些加密、解密算法？\n5. Redis的数据结构有哪些？为什么Redis那么快？\n6. Java1.7和1.8的区别，仔细讲讲HashMap和CurrentHashMap在1.7和1.8的区别。\n7. 乐观锁和悲观锁，以及CAS是什么，怎么体现在HashMap中？\n8. TCP三次握手、四次挥手的具体过程，以及TCP有哪些保护机制，具体是怎么样的？\n9. JVM内存结构了解多少，GC垃圾回收呢？新生代和老年代的区别是怎么样？\n10. 手撕算法：NC54 数组中相加和为0的三元组（顺便手撕快排和归并）；\n\n总结：问的都是基础八卦文，但问的很细，如果只是了解会吃亏。\n一点Java相关的知识都没问，基本都是要转Go了。\n\n# 虾皮上海7.18 后台一面面经\n> 作者：筱原沐\n链接：https://www.nowcoder.com/discuss/686449?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n1. 计算机cache缓存 寄存器一般多大，cpu一级缓存一般多大\n2. 为什么线程的切换比进程的切换简单\n3. 进程间是如何进行通信的，说详细一点，具体实现\n4. 死锁是什么，条件，避免\n5. CPU调度方式，时间片是什么\n6. 线程的切换需要什么操作\n7. 七层网络模型，Nginx在哪一层\n8. Http协议说一下（2.0多路复用如何实现）报文格式\n9. TCP在哪一层，如何确保可靠传输，拥塞控制细节，校验如何实现，编码如何实现\n10. 编程：字符串匹配KMP\n11. 编程：一个数组删除重复元素\n12. LinkedList，Arraylist区别，存储1个int的话用什么比较省空间\n13. HashMap存储的过程，负载因子是多少，为什么大于8会转成 红黑树，为什么是8不是7，\n14. 智力题，1000个铅球，一个是空心的，如何最快找出来\n\n\n基本就这些吧，很多东西细节我也没有咋确认，先发出来，面试官很好，面试体验很好，答得有些不太好，希望有好的结果。\n\n# 阿里大神的电话面---21.7.20\n> 作者：德鲁的菜鸟\n链接：https://www.nowcoder.com/discuss/686939?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n1. 简单介绍项目\n2. 知道哪些数据结构以及他们的特点\n3. 链表增删快，那如何提高其查询效率，有没有什么想法？\n4. B+树了解吗？B+树如何范围查询？B+树退化的极端情况是什么？（竟然是链表,那不就是上面如何优化链表查询了吗？果然还是我太菜了，面试官一步一步引导我）\n5. 跳表了解吗？\n6. 大顶堆、小顶堆了解吗？\n7. 实现长地址请求到服务端，然后服务端重定向短地址给客户端，如何实现长短地址的互相映射？\n8. 那我现在有10份数据，有1000个线程来争抢，你要怎么处理？\n9. 分布式是什么？为什么要分布式？分布式又会有哪些问题？分布式系统是如何实现事物的？\n10. Redis集群了解吗？如何处理宕机的情况？Redis的同步策略？\n11. LRU算法了解吗？你会如何实现它？这个算法可以应用在哪些场景下？\n12. TCP为什么是三次握手？两次行不行？多次行不行？\n13. TCP的安全性是如何实现的？两台服务器之间可以同时建立多条TCP链接吗？怎么实现的？\n14. 客服端输入一个网址后，是如何拿到客服想要的数据的，是怎样在网络中传输的？\n15. cookie和session\n16. java有哪些锁？共享锁是什么？CAS？乐观锁和悲观锁？synchronied的底层原理？锁升级？死锁怎么形成的？如何破解死锁？\n\n大概是一个半小时的时间吧，有些问题忘记了，没有记得很全。不愧是阿里的面试，很多情景问题需要去思考，而不是死板的八股文问题，还是我太菜了呀。\n\n# 蔚来一二三面-校招提前批-Java（已OC）\n> 作者：codeMonkey·\n链接：https://www.nowcoder.com/discuss/686990?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n蔚来相比于大厂感觉稍微简单点，算法考的也不难\n## 一面：\n面试官就考了两个简单算法题，一个判断带大小写字母的回文串，一个二分查找，最后问了问实习经历（半年没写算法题了，写了好多小bug，丢人，然而最丢人的还在后面。。。）\n## 二面：\n面试官就按照我简历写的开始问八股文\n1. 问了HashMap源码\n2. JMM，volatile作用，双亲委派模型\n3. MySQL索引底层，最左匹配原则，B树B+树的区别\n4. 问了问TCP三次握手，OSI七层网络模型都是啥\n5. Spring的IOC,AOP，还有SpringMVC的@RequestMapping的底层原理（这个没看过。。。哭）\n6. 最后做了一个快排（最丢人的来了，我竟然不会写快排了，我吐。。。写了老半天，他还提示我，最后他还提示错了一处，磕磕绊绊还是写出来了）\n\n二面一度以为凉了，毕竟快排都不会，还面个锤锤，不过可能是托八股文的光，竟然给过了\n\n## 三面：\n三面和二面之间本来只有半个小时，但是到点之后面试官一直没上线，我又等了一个半点！！！有点不开心，差点就下线了\n三面也是根据简历问的，问二面没有问的点，\n1. 问了ConcurrentHashMap的源码，问了问JUC包用过的并发类，还问了forkjoin多线程框架（完全没听过，菜鸡实锤了。。。）\n2. 问了JVM的垃圾收集算法，垃圾收集器，G1相较于CMS的缺点\n3. JDK8都有哪些新特性（我说了stream，lambda表达式，date类有变化（这个其实我已经不太了解了）），然后问我stream都有哪些函数，lambda支持什么样的参数,date有什么改变（除了stream的函数都没答上）\n4. 问了MySQL的索引优化，事务隔离级别\n5. 计网问了请求的组成，响应的组成，HTTP1.0，HTTP1.1，HTTP2.0，HTTP3.0的区别，HTTPS的请求响应过程\n6. Redis基本数据类型，高级用法（布隆过滤器啥的），持久化\n7. 最后问了问Linux常用指令（我说了说我常用的）\n8. 时间超时了，两道算法题让我选一道，一个是力扣原题爬楼梯，一个是用加减以及位运算实现乘法函数 double mul(double d, int n)，要求时间复杂度O(n)一下，给了我点提示，但是还是没想出来，果断A了爬台阶\n\n\n面试体验感觉还是不错的，面试官人都不错，虽然三面迟到了好久，但是面试官人很健谈，聊的挺嗨\n\n# 字节提前批后端开发一、二、三面面经，已意向书\n> 作者：旋风少男\n链接：https://www.nowcoder.com/discuss/687099?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n楼主是周天下午4点到7点连续三面，第二天早上发的意向书，效率很快\n\n感觉全部是怼着简历问的，八股文问的比较少，全部是基于项目 实习的基础上去问的！感觉参考价值不是特别高，还是发出来给大家看看！\n\n## 一面：\n1. 因为项目做了基于netty的rpc框架，针对这个进行展开提问\n2. Reactor线程模型\n3. netty怎么实现reactor线程模型的\n4. rpc调用的时候调用远程方法像调用本地方法一样是用了什么（这里我回答了网络连接的底层，结果面试官问的代码层面的动态代理）\n5. 动态代理怎么实现的？有哪两种动态代理（JDK、cglib）？有什么区别？\n6. IO多路复用\n7. select、poll、epoll\n8. 由于自己简历上写了看过rocketmq源码，接下来对mq展开提问\n9. rocketmq和市面上常见的mq有什么区别，都有什么优缺点\n10. rocketmq事务消息底层\n11. 一个数组，从输入中找一个数看看在不在这里面（开放题，任何你想到的都能说）：我回答了排序二分、遍历、用set、用hashmap、hashcode、用布隆过滤器。比较开放题\n12. 自增id有什么好处（我回答了和uuid相比，节省磁盘空间，作为聚簇索引提升查询效率）\n13. select * from user where id >= 多少 order by phone 这个sql有什么问题可以优化的\n14. 算法题：选定一个链表，返回环的入口节点，没有则返回空节点\n\n\n## 二面：\n1. 怼项目（支付宝实习项目）\n2. rocketmq延时消息底层实现，应用场景\n3. epoll 水平触发和边缘触发\n4. 常见的json序列化工具有哪些？\n5. 看到你写netty ，知道protobuf吗？和json比有什么好处呢？\n6. 那你能说说dubbo是怎么实现的吗？\n7. dubbo的序列化方式是什么呢？\n8. 微服务zookeeper、eureka、consul、nacos对比\n9. zookeeper讲讲？CP还是AP？eruka呢？ 服务调用需要ap还是cp？分析一下场景？\n10. 为什么mysql单表最多不放超过2000w行数据呢？\n11. 算法题：两个字符串找最长公共子串\n\n\n\n## 三面：\n\n1. 怼项目（商汤实习项目、数学建模项目）\n2. 认证、授权、熔断、限流都是怎么实现的？\n3. 常见的限流算法？（令牌桶等）\n4. 常见的限流方式？（nginx、网关）\n5. JWT了解吗？\n6. 进程通信方式？哪种通信方式最快？\n7. 开发中怎么解决线程安全问题？\n8. 如果你在浏览器上输入一个网址返回error怎么排查？（ping对应的ip）\n9. 如果你ping出来的ip是128.0.0.1怎么办？（肯定是对应的浏览器缓存映射、或者本级host被修改，面试官说就是这个）\n10. 你前面两面还有没被问到的吗？（不知道没有，别问了）\n11. 算法题：两个有序数组找中位数\n\n# 蘑菇街后台开发岗面试经验分享（社招）\n> 作者：这对丑情侣\n链接：https://www.nowcoder.com/discuss/687200?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n1. 先让你自我介绍，后来问了好多java的基础问题，我能记得的是以下这些，可能有些记得不是很准确，大家可以酌情参考。\n2. ArrayList、LinkedList、Vector的区别。\n3. HashMap和ConcurrentHashMap的区别。\n4. HashMap和LinkedHashMap的区别。\n5. wait方法和sleep方法的区别。\n6. synchronized、Lock、ReentrantLock、ReadWriteLock。\n7. 介绍下CAS(无锁技术)。\n8. 先问你熟悉哪些设计模式，然后再具体问你某个设计模式具体实现和相关扩展问题。\n9. 什么是ThreadLocal。\n10. 创建线程池的4种方式。\n11. ThreadPoolExecutor的内部工作原理。\n12. 分布式环境下，怎么保证线程安全。\n13. Mysql索引的数据结构。\n14. SQL怎么进行优化。\n15. SQL关键字的执行顺序。\n16. 有哪几种索引。\n17. 什么时候该（不该）建索引。\n18. Spring用了哪些设计模式。\n19. Spring中AOP主要用来做什么。\n20. Spring注入bean的方式。\n21. 什么是IOC，什么是依赖注入。\n22. 介绍下B树、二叉树。\n23. ajax的4个字母分别是什么意思。\n24. xml全称是什么。\n25. 分布式锁的实现。\n26. 分布式session存储解决方案。\n27. 常用的linux命令。\n28. HashMap是线程安全的吗。\n29. ConcurrentHashMap是怎么实现线程安全的。\n30. 类加载的过程。双亲委派模型。\n31. 有哪些类加载器。\n32. 能不能自己写一个类叫java.lang.String。\n33. Spring是单例还是多例，怎么修改。\n34. Spring事务隔离级别和传播性。\n35. 介绍下Mybatis/Hibernate的缓存机制。\n36. ==和equals的区别。\n37. 重载和重写的区别。\n38. String和StringBuilder、StringBuffer的区别。\n39. Explain包含哪些列。\n40. Explain的Type列有哪几种值。\n41. Mybatis的mapper文件中#和$的区别。\n42. 除了以上技术问题之外，其他技术问题都是根据项目来问的。\n43. 情景问题，例如：你的一个功能上了生产环境后，服务器压力骤增，该怎么排查。\n44. 你有什么想问面试官的\n\n# 美团crm到店一面面经\n\n> 作者：Jessin\n链接：https://www.nowcoder.com/discuss/688058?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n1. kafka积压优化。怎么评估你的优化效果。 \n   1. 主要看业务监控。\n2. cms的原理，是否发生stop the world，有什么缺点。\n3. 老年代垃圾回收放不下了，会发生什么。\n4. 答了会继续发生fullgc，触发oom。说不对\n5. 老年代用标记的主要原因是什么。\n6. 用标记，标记垃圾。如果用复制算法，需要两倍的空间，而且需要复制很多存活的对象。\n7. 数据库sql的执行过程\n8. 索引为什么用b+树，而不是用b树，为什么不用红黑树。\n9. innodeb和myisam中索引的区别。\n   1. myisam：表锁、无事务、无外键\n   2. https://mp.weixin.qq.com/s/sfSS-CaXxH7RdgPcrrgGMA\n10. 代理模式、适配器模式、桥接模式、装饰器模式，本质区别是什么。\n    1. https://cloud.tencent.com/developer/article/1082047\n11. spring aop的原理。cglib和jdk动态代理的原理，有什么区别。为什么jdk动态代理不继承类。\n12. 数据库的隔离级别，可重复读是否解决了幻读的问题，发生幻读举个例子。mysql怎么解决幻读的问题。\n13. mysql有哪些锁。mvcc怎么实现。当前读怎么操作。\n14. 实现split子串。\n15. jvm运行时数据区有哪些。\n16. 线程池有哪些参数。默认的拒绝策略有哪些。最大线程数有什么用。\n17. threadlocal用过么，原理是什么。\n18. 强引用、软引用、弱引用、虚引用有什么区别。\n19. b+树叶子指针存的是什么？地址么？\n    1. 存数据。辅助索引叶子节点存的是主键的数据（不是指针）\n\n# 百度java一面凉经 \n> 作者：节操粉碎机\n链接：https://www.nowcoder.com/discuss/688087?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n周日下午电话面的，感觉和普通面试不太一样的。。。上来也没让自我介绍然后就开始了。\n1. 先问线程池作用和七个参数。\n2. 实际工程中怎么查看线程池状态并决定需不需要修改相关参数，怎么改。\n3. java有内存泄漏吗。\n4. threadlocal用在线程池中会不会泄漏。\n5. 怎么在项目中找到一句慢sql。\n6. 如何优化。\n7. 怎么查看是否命中索引，看explain以后的哪些字段。\n8. spring怎么在容器启动的时候就把数据库中的数据写到内存中去。\n9. 怎么理解分布式。\n10. 在分布式架构中客户端需要实现哪些功能。\n11. 负载均衡有哪些算法。\n12. redis有哪些数据类型。\n13. zset是怎么实现的。\n14. 算法题：爬楼梯问动态规划和递归怎么写分别的时空间复杂度是多少。\n15. 生产消费者模型了解吗，怎么写。\n16. synchronized和reentrantlock啥区别，实际工程中应该用哪个，为什么，底层原理是啥。\n\n就记得起来这些了，顺序不太对，反正答的很差。。分布式根本简历就没写被抓着问。最后反问都没给就结束了。\n# Java端点面经\n> 作者：牛客335399388号\n链接：https://www.nowcoder.com/discuss/688165?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 一面\n端点面试，电话面试，面了大概40分钟+\n1. 自我介绍\n2. Zookeeper分布式锁怎么实现（监听+Znode 、项目上写的）\n3. Zookeeper怎么保证事务一致性（2PC）\n4. Zookeeper怎么实现ID生成器\n5. HashMap八股文 （扩容机制、结构）\n6. 问了一个红黑树基本定义（说了一遍）\n7. HashMap八股文与CourrentHashMap八股文（也问了ConcurrentLinkedList不太会）\n8. gc的整体流程\n9. 调用System.gc（），会立马GC吗？会执行GC吗？\n10. gc算法、判断对象是否存活、清理阶段算法\n11. synchronize底层实现\n12. volatile关键字作用\n13. JMM内存模型、Java内存模型（我顺便说了一下happen-befroe原则）\n14. Java的乐观锁\n15. Lock的实现原理\n16. 对象怎么到老年代\n17. 创建对象的整体流程\n18. CAS的原理\n19. ThreadLocal使用过吗？使用要避免啥？（键是软引用，可能会内存泄漏）\n20. Spring的类加载器和JDK的加载器有什么区别 ？ 不会\n21. Class.forName和ClassLoader的区别？ 不会\n22. 并发编程方向 具体有点忘了\n23. 堆排序具体流程\n24. MapReduce的整体流程\n\n有些问题具体忘记了，主要还是JVM和并发编程方面。。\n\n# 字节住小帮一二面凉经\n> 作者：每天起床OFFER+1\n链接：https://www.nowcoder.com/discuss/688542?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n字节的面试流程真的太快了，7月16找hr投递的简历，当天晚上hr就打电话约面试，约到了周日。\n## 一面（7月18号 约的晚上五点）\n1. 进程线程区别\n2. 乐观锁、悲观锁、读锁、写锁、自旋锁\n3. 怎么才能线程安全\n4. cookie是什么\n5. cookie怎么做到同一个域名都可见\n6. TIMEWAIT为什么要等待两个MSL\n7. 算法题：完全平方和\n8. sql题：第n高的薪水\n9. JVM内存划分\n10. 栈和堆的结构是什么？为什么这样设计？\n11. 对象一定是在堆上分配的吗？\n\n还有些问题不太记得了。。。\n面试官很友好，完全平方和在写转移方程的时候漏了一项，找了半天错误，最后面试官提醒才通过的。\n第一次面大厂，很紧张，然后面了一个小时，又很饿，答得自我感觉不是很好。\n感觉要凉凉，结果第二天中午接到了hr的电话，说一面通过了，约二面的时间。\n\n## 二面（7月21号 约的晚上六点半）\n自我介绍，简单说一下项目，nacos的服务中心和注册中心是用来做什么的？\n1. redis的数据结构有哪些？\n2. hyperloglog可以remove吗？\n3. 用redis实现一个队列怎么做？\n4. redis的del操作时间复杂度是多少？\n5. mysql事务的acid特性是什么？\n6. mysql的持久化是怎么做的？\n7. 策略模式和模板模式的区别？\n8. 为什么要使用策略模式？为什么不直接if else？\n9. 线程的状态，以及怎么流转的？\n10. 线程池的参数？\n\n做题，面试官自己出题，\n1. 面试写了一个类，让我重写equals方法和hashcode方法\n2. 面试官给了一个文件，里面有\\t IP 登入时间 登出时间，然后问我会不会shell，我不会，应该跟shell编程有关，于是换了个题。\n3. 面试官写了一个类Line，类里面是刚才那三个属性：IP、登入时间、登出时间，现在有一个List<Line>，问怎么求出ip数量最多的前10个ip，说思路就行\n4. 登入时间和登出时间是从当天零时开始记的秒数，如何找到用户数量最大的时刻（秒为单位），说一下思路\n5. 跟面试官讨论了一会，应该怎么做，然后让我把代码写出来。(面试官叫停了我，让我下去再研究研究)\n\n刚刚收到面试调查问卷，然后没过几分钟就收到了感谢信。\n\n# 字节后端开发一面记录\n> 作者：不学无墅墅\n链接：https://www.nowcoder.com/discuss/689175?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 一面\n1. 自我介绍\n2. 介绍项目\n3. 项目压测过没有，通过压测发现了系统瓶颈在哪里（这个确实没思考过）\n4. 进程与线程的区别（面试官让我尽可能多说）\n5. 多线程哪些内存是共享的，哪些不是共享的？（没答出来，现在想来应该就是问java内存模型，堆和方法区就是共享的，虚拟机栈，本地方法栈，程序计数器就是非共享的，当时不知道再想啥）\n6. 线程的地址空间什么的？（我太菜了，都没听过）\n7. 一个线程能访问到另一个线程的局部变量吗？\n8. 进程切换与线程切换哪个代价比较大？为什么（不会，后面学一下）\n9. 操作系统的内存管理（我说我操作系统不太懂）\n10. 一个对象在内存中的存放位置？字符串呢？\n11. LRU算法的实现，口述\n12. 常用排序算法及时间复杂度\n13. 如果一个超大文件有10亿行数据，每行一个整数，内存放不下，怎么排序？不能只答归并排序，要落实到代码应该怎么写（思考了一会，没想出来，后面学一下）\n14. 计算机网络常规知识...\n15. 说一下数据库事务的事务\n16. 分别讲一下ACID是什么意思（发现我对一致性了解不深，没有搞懂一致性的概念）\n17. 怎么保证持久性的？（感觉没回答好）\n18. 写了redo log，但事务还没提交，突然系统崩溃了会怎么样？（也没回答好）\n19. 给一个数组，用最快的排序算法，进行排序\n    1. 我答那就是快排了，他说有更快的吗？针对这个场景，有更快的吗？\n    我又提了一基数排序，但好像不适用，面试官就说那就用你觉得最快的写吧\n    然后，啪啦啪啦...\n\n反问，因为我晓得我答得太烂了，就跟面试官说我这是第一次面试，发挥的不是很好，面试官说不要因为某个问题打得好或者不好，就觉得怎么怎么样，我们都是整体来看的。\n\n关于java的一点没问，在问LRU算法的时候，我说Map里面放双向链表的节点，他问放的是节点，还是节点的引用，给我整蒙了，java里面除了基本类型其他应该都是引用，但当时头脑空白，在想怎么才能放节点而不是节点的引用，没想出来，就回答的应该放的是引用，感觉很多问题回答的都不是很确定，以为已经凉了，过了几分钟，HR打来电话预约二面时间\n\n# 友塔游戏开发岗7.22一面电话50min\n> 作者：安东尼的小不二\n链接：https://www.nowcoder.com/discuss/689301?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n1. BIO、NIO区别\n2. Kyro序列化比JDK自带的效率高多少\n3. Zookeeper注册中心的实现？主节点挂了做法？节点选举办法\n4. Redis扣减库存做法，Redis加锁效率低，怎么解决\n5. 多级缓存，缓存不一致问题（删缓存，再写数据库）我谈到任务队列方法，面试官说性能太差\n6. RocketMq异步下单问题\n7. Java判断对象是否被回收（存活）\n8. 讲一下AOP、IOC\n9. 单例模式\n10. Mysql事务ACID\n11. 原子性怎么实现（undo log日志保证，记录事务回滚）\n12. 事务的隔离级别（具体讲了可重复读）\n13. 可重复读是用MVCC，MVCC怎么做的\n14. Innodb和MyISAM的区别\n15. Btree和B+tree区别\n16. 讲述一下前序和中序遍历还原二叉树\n17. HTTP和HTTPS区别\n18. HTTP 2.0了解过吗\n19. 进程和线程区别\n20. 进程通讯方式\n21. 讲了讲没做出来的笔试题\n\n# 京东提前批一面面经（44min)\n> 作者：Yyyilia\n链接：https://www.nowcoder.com/discuss/689420?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n1. 自我介绍\n2. 看您有两个项目，框架用的是Spring吗，是微服务的项目吗\n3. 能说一下您在项目中做了什么吗，负责哪些模块，能体现技术能力的\n4. Java 中有个 Object 基类，里面有两个方法：hashcode()、equals()，他们是做什么的\n5. 再问一下 List 集合，ArrayList 和 LinkedList 有什么区别？\n6. LinkedList不支持随机访问，但是我们 List 接口肯定有通过下标去 get()，LinkedList 想实现 get() 怎么实现呢（每个节点有前后指针，根据这个找）\n7. 这两个 List 都是线程安全的吗，有没有线程安全的 List？（Collections.synchronizedList、CopyOnWriteArrayList）\n8. 你刚才提到了 ConcurrentHashMap，说一下它是怎么保证线程安全的，用的什么数据结构和锁机制\n9. 刚才提到了 CAS，能详细说说吗\n10. CAS 的 ABA 问题了解吗，怎么解决\n11. 用 synchronized 实现一个简单的死锁，你会怎么实现\n12. ThreadPoolExecutor 有哪些参数，每个参数大概都是干什么的，整个工作流程是怎样的，workQueue 满了会怎样\n13. 说一下 Spring 的 IOC\n14. Spring 会帮你创建实例，那它整个的流程是怎样的，启动的时候是怎么个流程，最后怎么就让程序员能直接获取并使用实例\n15. 如果我想在整个项目启动之前，初始化一个全局的线程池，或者打印日志，要怎么实现（启动过程中实现 Aware 接口）\n16. Spring 中用到了哪些设计模式\n17. 你们的项目里有用到什么设计模式\n18. Sql 语句：只有一列 name，里面有重复的，怎么把重复的名字找出来（Group by...Having）\n19. 索引最左匹配原则\n20. 中间件有了解吗，比如 Redis、消息队列\n21. Redis 为什么快\n22. Redis 的单进程单线程用到了什么模式/方式，其他的中间件也有用到的（IO多路复用）\n23. 反问：部门是京东零售技术与数据平台中心，技术栈\n\n# 7.23 陌陌Java 秋招 一面 52分钟 （已过一面）\n\n> 作者：tongji4m3\n链接：https://www.nowcoder.com/discuss/689548?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n1. 自我介绍\n2. SpringMVC前台发一个请求到后端的处理过程（不会，扯到项目）\n3. 布隆过滤器设计原理\n4. HashMap底层原理\n5. 多线程相关，创建线程的几种方式\n6. 手动创建线程池的参数、线程池调度过程确定\n7. 网站首页调用10个接口，希望100ms拿到结果，对超时的丢弃，这个场景怎么通过多线程实现（提示CountDownLautch，还是不会）\n8. 操作系统信号量\n9. ThreadLocal原理、使用场景、注意事项\n10. 计算机网络一道题，IP地址、子网掩码、CIDR相关\n11. Linux常用命令\n12. 服务上线，日志报错信息的排查\n13. 刷题：找出字符串中的数字\n14. 刷题：二叉树的之字形层次遍历\n15. 反问环节\n\n# JD 基础架构部门一面面经\n> 作者：Napo1eon\n链接：https://www.nowcoder.com/discuss/689601?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 背景\n昨天下午（2021年7月22日）收到面试邮件通知说今日（2021年7月23日） 下午5点面试，邮件中提到是视频面，还特意下了个JoyMeeting，然后今天4：30接到电话说可以提前吗，于是就开始电话面试。总时长27mins。\n\n## 问题\n自我介绍\n\n## Java\n1. java 8种数据类型\n2. int几个字节，最大多少\n3. Map接口的实现类，HashMap底层数据结构\n4. Hashmap的容量,为什么是2的n次方\n5. Hashmap什么时候会由链表转为红黑树\n6. 为什么红黑树的阈值是8\n7. 为什么采用红黑树\n8. 红黑树特点\n9. 调用什么方法会使一个线程进入就绪状态。\n10. 共享变量通信\n11. 垃圾回收算法\n12. wait和sleep区别\n13. Java存储中文用什么\n\n## 网络\n1. TCP保证可靠性（三次握手），为什么不是四次\n2. TCP三次握手后为了交流什么\n3. TCP UDP区别\n4. UDP能实现可靠传输吗\n5. TCP保证可靠性的方法\n6. Cookie和Session区别\n7. Cookie如何保存敏感信息，用对称还是非对称\n8. 打开京东网站过程发生了什么（八股文）\n9. RPC框架知道哪些\n\n## 数据库\n1. 索引有什么用\n2. mysql innodb引擎用的什么数据结构\n3. 为什么采用B+ 树\n\n## 反问\n1. 公司哪个部门：基础架构，主要是做服务注册服务发现。\n2. JD的后端是用什么写的，大部分是java\n\n## 总结\n1. 面试官人很好，会给提示~\n2. 有些事情还是需要深入学习，一个问题需要再往深处想一想。\n\n许愿二面~，给个Offer吧，救救孩子……\n\n\n# shopee后端提前批一面二面hr面\n> 作者：独钓清水河\n链接：https://www.nowcoder.com/discuss/689683?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 一面7月11号     64分钟\n1. 自我介绍\n2. 讲下cms垃圾回收\n3. cms第二个步骤是清理还是标记？\n4. cms哪个步骤会stop the world\n5. 讲下https\n6. 数字证书原理\n7. 如果没有第三方我自己可不可以发布一个数字证书？\n8. 数字证书怎么验证的？\n9. 公钥和私钥？\n10. 非对称加密原理有了解吗？\n11. 公钥和私钥怎么生成的？\n12. 生成私钥的使用算法是什么？\n13. 你知道rsa吗？\n14. mysql特性？隔离级别？\n15. 四种隔离级别的特点\n16. mysql在哪个级别解决幻读问题的？\n17. 讲下间隙锁  加锁范围  具体说下\n18. 索引使用的数据结构\n19. b+树和b树区别？\n20. 范围查询 b+树怎么实现的？\n21. redis主从同步了解吗？\n22. 讲下redis数据类型\n23. 有序集合怎么实现的？\n24. 一致性哈希了解吗？\n25. 极限编程挑战赛做的什么？你做了什么？\n26. 你做出多少题？\n27. 软件精英挑战赛做了什么？\n28. 100万 0到1万随机乱序数，o(n)下，找中位数\n29. 算法题: 最长公共子串 （耗时22分钟）十分钟思考 12分钟写\n\n反问\n\n## 二 面7月18号     58分钟\n1. 自我介绍\n2. 看你有实习经历，介绍下\n3. 实习的公司做啥的\n4. 项目中你具体做的什么？\n5. 订单表怎么实现的？\n6. 订单id怎么生成的？\n7. 表的主键是什么？\n8. 为什么要做软删除？\n9. 订单id除了自增主键还有什么生成方式？\n10. 项目上线了没有？用户量怎么样？\n11. sql运行过程？详细点\n12. 缓存的原理是什么？\n13. mysql对sql语句怎么优化的？\n14. 你刚刚提到哈希表，聊聊哈希表数据结构和实现原理\n15. 算法题： 有三个子节点的树的层序遍历，并且实现测试用例编写和测试输出。  这道题半小时写完，既写层序遍历，还需要手写测试用例。\n16. 你自己的职业规划\n17. 你去年参加过shopee面试？\n18. 反问\n\n## hr面7月23号    25分钟\n1. 自我介绍\n2. 先确认本科研究生学历，什么时候毕业，能不能拿到双证\n3. 在学校成绩怎么样？ 排名\n4. 研究生研究方向\n5. 为什么没选算法选择后端呢？\n6. 介绍下项目\n7. 项目中遇到的挑战 怎么解决的\n8. 后期有没有优化改进？\n9. 做项目有人带你吗 还是自己从0到1的？\n10. 怎么学习这些知识的呢？\n11. 挑选公司最看重什么？ 优先级列三点\n12. 对我们公司的了解\n13. 有拿到哪些offer?或者还有哪些在流程中？\n14. 将来阿里 腾讯 字节会投吗？ 会去吗？\n15. 反问\n\n# 大华提前批java1，2面+HR面（已OC）\n\n> 作者：最肯忘却故人诗\n链接：https://www.nowcoder.com/discuss/689950?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 7.20 19:00-19:30一面\n1. 自我介绍\n2. Redis缓存点赞数怎么存的\n3. hasmap数据结构以及get和put过程\n4. hashmap是无序，想要实现存储和取出顺序一致（比如页面下拉页表），使用什么数据结构(Linkedhasmap)\n5. 多线程中的闭锁和栅栏\n6. 线程池7大参数\n7. SpringAop如何理解\n8. mysql隔离级别及默认隔离级别\n9. A,B同时开启一个事务，B的插入已经提交了，那么A能否查到B插入的数据\n10. mysql主键索引和普通索引的区别\n11. 双亲委派机制及缺点\n12. 打破双亲委派机制的案列\n\n## 7.22 19:40-20:25 二面\n1. 自我介绍\n2. 项目的架构设计\n3. 注册登录怎么做的\n4. 注册时的激活流程\n5. 缓存点赞数如何实现\n6. 取消点赞如何实现\n7. 同步消息的堆积怎么实现\n8. 如何理解缓存\n9. 缓存雪崩，缓存击穿，缓存穿透\n10. 为什么选择ES\n11. 如何创建一个SpringBoot应用\n12. 怎么理解Restful风格，好处\n13. Post和Get\n14. http协议，是否有状态\n15. Cookie和Session，Cookie的限制\n16. 访问一个网址发生的事情\n17. JDK8的Stream\n18. mysql的事务\n19. mysql中的MVCC\n20. JVM的内存模型\n21. 线程池七大参数\n22. 如何学习的JAVA\n\n## 7.27 HR面20分钟\n过了大概一周收到意向书。\n\n# 滴滴一二面凉经20210725\n> 作者：peonyX\n链接：https://www.nowcoder.com/discuss/690450?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 一面\n两个算法，求最近公共祖先；给一个对象list，里面有userid,logintime,logouttime，查出哪个时间点在线人数最多，这个时长是多少\n\n## 二面\n1. 有界队列和无界队列区别，阻塞队列怎么阻塞的，怎么实现的\n2. aqs的队列怎么实现的，怎么实现的节点唤醒\n3. synchronized原理，mutex是什么\n4. Public static synchronized的方法，分别用new A().method和A.method调用，他们会锁竞争吗\n5. unsafe.park和unpark计算机底层怎么实现的\n6. volatile原理怎么实现的\n7. 工作内存和主内存在计算机硬件中是分别对应什么\n8. 算法和一面一样，求最近公共祖先\n9. 网络和MySQL都没有，就这两个我熟悉，还不问，这些并发包涉及到的计算机原理也不会，凉了也算意料之中。但是吸取教训，把底层原理看一遍，面阿里的时候就不怕了。juc原理加上操作系统原理。\n\n# 阿里java研发面经，三面共25个问题\n> 作者：link8888\n链接：https://www.nowcoder.com/discuss/690638?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n首先说下我的感受，阿里非常重视基础，问的不仅宽泛而且比较深入，java基础、数据结构、操作系统、计算机网络、数据库都有涉及；相较于头条，阿里算法问的比较少也比较简单。ps:文末有福利哈\n废话不多说，直接上干货：\n1. Java容器：List,Set,Map\n2. Map的遍历方式\n3. HashMap扩容为什么是扩为两倍？\n4. Java线程同步机制（信号量，闭锁，栅栏）\n5. 对volatile的理解：常用于状态标记\n6. 八种基本数据类型的大小以及他们的封装类（顺带了解自动拆箱与装箱）\n7. 线程阻塞几种情况？如何自己实现阻塞队列？\n8. Java垃圾回收\n9. 可达性分析->引用级别->二次标记（finalize方法）->垃圾收集 算法（4个）->回收策略（3个）->垃圾收集器（GMS、G1）。 可达性分析的根结点：\n10. java内存模型\n11. TCP/IP的理解\n12. 进程和线程的区别\n13. http状态码含义\n14. ThreadLocal（线程本地变量），如何实现一个本地缓存\n15. JVM内存区哪里会出现溢出？\n16. 双亲委派模型的理解，怎样将两个全路径相同的类加载到内存中？\n17. CMS收集器和G1收集器\n18. TCP流量控制和拥塞控制\n19. 服务器处理一个http请求的过程\n20. 例举几个Mysql优化手段\n21. 数据库死锁定义，怎样避免死锁\n22. spring的aop是什么？如何实现的\n23. 面向对象的设计原则\n24. 策略模式的实现\n25. 操作系统的内存管理的页面淘汰 算法 ，介绍下LRU（最近最少使用 算法 ）\n26. B+树的特点与优势\n\n# 百度Java一面面经 上海\n> 作者：汇源锅子\n链接：https://www.nowcoder.com/discuss/690660?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n百度一面：\n1. 自我介绍，项目\n2. 项目中的难点，怎么解决，主要讲java方面的\n3. JVM介绍一下\n4. 垃圾回收器介绍一下\n5. CMS和G1回收时的具体步骤详细说一下\n6. CMS回收时如果老年代中有引用指向新生代，这种时候怎么避免回收器重写扫描新生代（不会，面试官后面说用card table，让我后面去了解一下）\n7. JVM调优会吗，具体用过哪些指令\n8. concurrenthashmap讲一下，底层实现原理是什么\n9. concurrenthashmap的get（key）方法加锁吗，这个方法是怎么保证线程安全的（不会，面试官说是将value用volatile修饰，保证其可见性）\n10. AQS知道吗，底层的实现\n11. 公平锁的情况下，CLH队列是怎么操作的\n12. 线程池用过吗，参数讲一下\n13. 当线程池需要回收线程时，流程是什么（不会）\n14. Innodb的底层结构是什么样的\n15. 有过数据库调优吗（explain）\n16. redis中zset可以用来实现什么功能\n17. zset的底层原理是什么（ziplist+skiplist）\n18. redis的持久化（rdb+aof）\n19. 写题：二叉树的层序遍历\n\n# 字节住小帮一面凉经\n> 作者：代码界的小白\n链接：https://www.nowcoder.com/discuss/690980?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n1. 自我介绍\n2. Spring和SpringBoot的区别\n3. Spring中的IOC与Aop，说区别，说怎么实现的。\n4. 你了解的常见注解有哪些？\n5. 计算机网络了解吗？\n6. 七层网络，然后问http、tcp和ip位于什么层\n7. Tcp和Udp的区别和使用场景\n8. tcp的拥塞控制\n9. cookie和session的区别\n10. cookie如何实现登陆的。。。\n11. 用户态与内核态的区别\n12. 进程与线程的区别\n13. 平衡二叉树说一下？\n14. 平衡二叉树的遍历、特性和构建？\n15. 了解红黑树吗？\n16. B+树的特性\n17. mysql的事务隔离级别\n18. redis你了解哪些\n19. redis 的数据类型\n20. redis持久化方式，RDB和AOF的区别\n21. java多线程中的线程池参数和拒绝策略说一下\n22. static修饰的属性有哪些\n23. static修饰的东西存在哪\n24. 非static修饰方法中的变量存在哪\n25. 一道sql题，给一个学生表，四个字断，查询语文成绩和平均分都大于60的学生id\n26. 一道算法题，最长递增子序列。\n\n算法题写的时间有点长，早晨还看过，唉，还是不够熟练，兄弟们多刷题吧！\n\n面试结束后问内推的人，他说问hr，hr说没过，说算法需要提升一下，基础还行。\n\n# 字节，美团，B站后端社招一面面经\n> 作者：嘟嘟果实123\n链接：https://www.nowcoder.com/discuss/691139?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 字节跳动（45分钟），已过：\n1. 介绍项目\n2. 设计模式：写一写单例模式-懒汉模式，饿汉模式，线程安全型，双重校验锁型。\n3. 为什么要使用双重校验锁的单例模式？\n4. volatile关键字的作用，怎么起作用的\n5. 锁相关：sychornized和可重入锁\n6. 1.8之后对sychornized有了优化，有了解过吗。\n7. 线程池相关：说一说线程池，怎样创建线程池，有哪几个参数，分别有什么含义，应该怎么设置\n8. 数据库相关 什么是脏读，幻读，不可重复读。\n9. 索引：索引的分类，聚簇索引的概念\n10. B+树的原理和结构\n11. 算法题：股票的无限次交易 动态规划解决。\n\n## 哔哩哔哩（50分钟左右），已过：\n1. 自我介绍\n2. Hashmap,concurrenthashmap,hashtable各自的特点区别。 jdk1.8都做了哪些改进？\n3. 可重入锁reentrantlock（因为上一题提到了concurrenthashmap的segement继承了这个）。\n4. volatile和transient关键字。\n5. hashcode与equal。\n6. juc中的countdownlatch，其概念，使用场景。\n7. 扩展：java中如何查看线程状态，你知道那些java自带的命令。\n8. java内存模型，及jvm内存分区，各有存了什么。\n9. jvm类的加载过程\n10. 线程池\n11. 设计模式，说一下策略模式和装饰器模式。\n12. mysql的索引分类，如何创建联合索引，有什么原则（最左前缀匹配原则）\n13. 有哪些编码格式(GBK,UTF-8,ISO-),有没有想过为什么会有这么多的编码格式。\n\n感觉面试的广度是有的，不过都没有太深入。\n\n## 美团（70分钟左右），回答问题的时候语速比较慢，思考题耽搁的比较久。从晚上九点视频面试面到快十点半。最后还是凉了。：\n1. 一个线程不安全的类要用到线程安全的场景中，要做什么？\n2. sychornized和threadlocal\n3. string,stringbuilder和stringbuffer,string为什么要用final修饰？\n4. final和static的作用。\n5. 抽象类和接口的区别。\n6. 重写了equals后为什么要重写hashcode，如果不重写，会有什么影响？\n7. TCP和UDP\n8. TCP为什么是面向连接的，有哪些机制？\n9. TCP的三次握手，为什么是三次，两次会有什么情况？\n10. 思考题:100只试管里有一只是有毒的，现在有10个小白鼠，如何最快速地判断出那只试管有毒。\n11. 编程：指定区间反转链表。\n\n面试官评价：会缺乏一些开放性的思考。\n\n# 字节一，二，三面（已意向书）\n> 作者：敲代码的小白\n链接：https://www.nowcoder.com/discuss/691263?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 一面（7.14）\nSql语句：找到第n高的薪水。\nhttps://www.nowcoder.com/practice/4a052e3e1df5435880d4353eb18a91c6?tpId=82&&tqId=29764&rp=1&ru=/activity/oj&qru=/ta/sql/question-ranking\n代码题：https://leetcode-cn.com/problems/perfect-squares/\n\n1. JVM的运行时数据区，分成堆和栈有什么好处？只有栈没有堆会怎样？\n2. JDK8的Stream\n3. Tcp的Time-wait状态\n4. Http中的Keep-aLive，http长连接如何实现的。\n5. SSH连接中输入一个很长时间处理的命令，把客户端关了，命令会不会继续执行。https://www.runoob.com/linux/linux-comm-nohup.html?ivk_sa=1024320u\n6. ES如何搜索，为什么快。如何分词？\n7. Redis的内存淘汰策略\n8. Redis pipeline\n9. Cookie，客户端能不能该Cookie，服务器能不能改Cookie。\n10. 进程与线程的区别，进程间能不能共享资源？线程同步的方式\n11. 为什么要有GC？C++没有？没有GC会怎样\n12. GC怎么判断内存是否可回收？哪些对象可以作为GC roots。\n13. HashMap的put流程，解决hash冲突的方法，除了链地址法有没有别的方法？\n14. MVCC是什么？如何实现？\n15. 什么是自旋锁？\n## 二面（7.16）\n1. HTTPS的加密流程\n2. Spring IOC的设计理念\n3. 策略模式和模板方法的区别\n4. 进程、线程与协程\n5. 进程间通信方式\n6. 堆的设计（堆排序中的保持堆特性的代码操作）\n7. 慢sql优化，using filesort什么意思\n8. 线程的状态，以及各状态之间的转化关系\n9. 项目中的一些问题\n## 三面（7.20）\n由于在字节实习过，问的都是项目问题，无八股文。\n\n1. 订单有哪些状态，如何实现订单状态的扭转，补偿任务如何工作的，为什么用jesque和job？\n2. Es的分词以及搜索流程，介绍了下倒排索引。\n3. 慢sql如何优化的，举个例子。说了下索引下推的例子，using index condition\n4. 实习做了什么工作内容？ \n5. 算法题：https://leetcode-cn.com/problems/merge-intervals/\n\n部门是在杭州，字节面试效率还是非常快的。\n\n# 字节国际化电商一面二面\n> 作者：菜鸡只想要个offer\n链接：https://www.nowcoder.com/discuss/691427?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 一面7.27\n1. 自我介绍\n2.  怼项目（深度学习项目，问了做了哪写工作，怎么实现的） 因为是导师的一个CV项目，好像面试官硕士也是做这个方向的 简单聊了15min.。 然后全程全程怼八股文：\n3.  static关键字（修饰什么，应用场景）\n4.  final关键字（能修饰什么，应用场景）\n5.  volatile关键字（从volatile扯到了JMM。。。又扯到了线程和进程。。。）\n6.  hashmap底层原理 扩容 （从hashmap底层不安全扯到了扩容形成环状链表，concurrenthashmap，，，）\n7.  gc root(先回答了有哪写，，然后跟他疯狂扯GC的整个过程，，叨叨了好久)\n\n最后留了20分钟左右让我A一道题。。leetcode 33   。。\n\n我有什么想要问他的。。。 问了语言的问题。。。 ans：进去之后要转go。不过会给留充足的时间学习。\n\n## 二面8.3\n1. 自我介绍\n2. 怼项目（大概写了点自己的论文和科研的项目还有一个辣鸡web项目，然后面试官问我为啥不去搞算法，吓得我一身冷汗，赶紧说自己被老师逼着发论文，其实对这一块不感兴趣）\n3. 问我学过操作系统么，我说本科的时候学过，然后问进程与线程的区别\n4. 进程间上下文的切换与线程上下文的切换\n5. 线程上下文切换的时候都保存了哪些东西（没答上来）\n6. mysql索引，说了hash索引和b+树索引，比较了他们之间的区别，面试官也没用追问。\n7. 数据库事务隔离级别\n8. MVCC\n9. 幻读问题的解决\n10. ACID原则与对应的实现\n\n算法题：leetcode 378 明明之前刷过这个题，一开始脑子里还是暴力解法（可能太紧张了），然后面试官让我考虑一下有没有其他解法，然后想了想之前拿小顶堆写的，试了试，还是有bug\n\n# 京东提前批一面Java开发\n\n> 作者：牛客590954564号\n链接：https://www.nowcoder.com/discuss/691858?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n1. 自我介绍\n2. 问项目\n3. 有看过Feign实现吗\n4. 看过Spring Cloud全家桶里面哪些源码\n5. 怎么写一个Feign接口（客户端和服务端），这里问的非常非常详细，具体到Controller里面用了什么注解，参数需要添加什么注解\n6. 用Post方法，服务端Post JSON数据，怎么让Spring把Body变成一个对象\n7. 接上面，PostMapping需要指定什么，参数怎么写\n8. Spring IoC，AOP是什么？原理\n9. JDK动态代理涉及到哪几个类\n10. Spring Boot对于Spring而言里面增加了哪些新的注解？\n11. SpringBoot的starter实现原理是什么？为什么不需要再像原来配置那么多Bean对象出来\n12. 知道怎么写一个starter吗？\n13. Spring事务了解不\n14. Spring事务传播行为\n15. 上面提到了ThreadLocal，应用场景是什么\n16. ThreadLocalMap里面的key一般是谁（ThreadLocal用get方法的时候怎么取到当前线程下的对象的？）\n17. 平常自己有没有用过ThreadLocal\n18. Bean的生命周期和作用域\n19. 集合类都有哪些？从接口上面往下说\n20. List里面几个实现类区别和特点\n21. Map里面实现类的各自区别和特点，数据结构\n22. 并发包里面的Map和List看过没\n23. 线程池用过吗？线程池一般用来做什么\n24. 线程池参数\n25. 饱和策略说一下有哪些\n26. workQueue阻塞队列主要实现有几种？\n27. Synchronized和Lock的区别，主要实现原理\n28. Synchronized修饰静态方法和非静态方法有什么区别\n29. ClassLoader用过吗？用在哪些方面（依赖冲突，热加载，热部署，加密保护）\n30. 做微服务的时候用过Tomcat没\n31. 数据库三大范式\n32. 一个SQL题目，一个表有学生学号成绩学科，找所有科目对应的最高成绩和学生学号\n33. 了解联合索引吗\n34. 慢查询如何定位\n35. 建立A，B联合索引和A，B单列索引有什么区别\n36. 了解MQ吗？MQ能做什么\n37. 异步的目的是什么\n38. RabbitMQ的死信队列了解吗\n39. RabbitMQ消息模型\n40. NoSQL了解吗\n41. Redis数据结构\n42. HTTP报文结构\n43. Header里面有哪些常用的\n44. 和缓存相关的Header是哪些？（Expires，Cache-Control，Last-Modified...）\n45. HTTP响应码（3开头的有哪些）\n46. 跨域了解吗\n47. 网络攻击行为了解吗？csrf攻击原因是什么\n48. 七层网络模型？应用层有哪些协议？TCP哪层的？\n49. Nginx用过吗\n50. JVM内存模型\n51. GC原理\n52. G1搜集其了解吗\n53. CDN了解吗？\n54. CDN工作机制清楚吗\n55. 现在是怎么学习的？看什么书？\n56. 平常有进行一些总结吗？\n57. 有进过职场吗？有参与过一些开源项目吗\n58. 目前的一些职业规划，未来三年到五年的目标\n59. 自身的缺点有哪些（技术和生活方面）\n60. 团队里面出现分歧怎么解决的\n61. 遇到过哪些困难和挑战吗（学业和生活）\n62. 一道算法：有序数组中数字两两一对，找出只出现一次的数字（写了位运算，面试官说再优化一下---二分）\n\n# 字节提前批后端123面面经\n> 作者：感谢信收割机QAQ\n链接：https://www.nowcoder.com/discuss/692070?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 1面 7.15 50min\n1. 怎么理解微服务\n2. 微服务的缺点\n3. 微服务之间怎么做负载均衡\n4. Oauth2基本流程、原理\n5. 登录模块是怎么做的\n6. cookie和session的区别\n7. 购物车为什么用Redis存，是永久存储吗\n8. 为什么购物车多读多写\n9. Redis怎样清除过期key，有哪些策略\n10. lru是怎样的过程\n11. Redis字典底层怎么实现的\n12. hashtable是怎样实现的\n13. ziplist怎样实现的\n14. 普通的哈希表怎样实现的\n15. 哈希表怎么扩容\n16. 使用MQ的好处\n17. MQ解耦和微服务解耦的区别\n18. 算法：最长回文子串\n19. https建立连接的过程（SSL/TLS协商的过程）\n20. 对称加密和非对称加密的优缺点\n21. 为什么要区分内核态和用户态\n22. 什么时候从用户态切换到内核态\n23. 你编程的情况下，系统调用什么时候会发生\n24. 反问：业务，开发语言，表现，对应届生的要求（重点是基础和算法）\n25. 面试体验不错，但是项目挖的有点深\n## 2面 7.19 1h\n1. 手写单例模式\n2. volatile什么作用\n3. 多线程的几种实现方式\n4. 四种方式的区别\n5. 锁用过哪些\n6. 排它锁什么意思\n7. 自旋锁什么意思\n8. CAS相关\n9. MySQL可以不指定主键建表吗，背后的逻辑是什么\n10. 聚簇索引和其他索引有什么区别\n11. 建唯一索引，插入数据时是怎么处理的\n12. 重复插入会报错，是怎么处理的\n13. 不同事物隔离级别的实现\n14. 以前没有实习过吗\n15. lc40 组合总和II\n反问：部门怎样培养新人，刚进来做什么（学基础，语言和中间件，做demo），大概多久做需求（1周到1个月不等，看学习情况），框架和中间件以开源的为主还是以自研的为主（自研的）\n## 3面 7.26 1h\n1. 有在实习吗\n2. 面试通过后可以实习吗\n3. 做项目的过程中遇到过什么问题\n4. 内存泄露具体发生在哪\n5. 什么情况下会出现多线程访问\n6. 缓存穿透，怎么解决 （好像一紧张说成缓存击穿了，面试复盘的时候才发现。。。）\n7. 缓存雪崩，怎么解决\n8. 缓存与数据库数据一致性\n9. 超卖问题怎么解决的\n10. 集群环境下，Redis内存里的数据怎么保证一致\n11. 算法：给定一个字符数组，和一个字符串，在字符串里找到任意一个完全由字符数组组成的子串，字符顺序无所谓（滑动窗口）\n12. 反问：面试通过还有面试吗，新人入职有培训吗，技术氛围怎么样\n\n\n# 字节2022年秋招提前批123面面经，JAVA后端开发岗位 \n> 作者：爱考拉爱生活\n链接：https://www.nowcoder.com/discuss/692243?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n今天刚收到字节的offer意向书，来和小伙伴们分享下面经，希望能对大家有所帮助！\n\n传送门：\n实验室大佬面经：秒杀项目大佬，啥都会，你们想请教秒杀项目可以找他，比我强的太多了。\nhttps://www.nowcoder.com/discuss/698548\n华为秋招sp面经： https://www.nowcoder.com/discuss/692506\n阿里面经由于朋友不想公开，所以删了，很抱歉了\n\n通知书镇楼，证明下本面经的真实性。\n\n\n\n先简单介绍下本人情况：\n交大本硕，绩点3.85/4，通信专业，6月开始在阿里实习。\n\n重点来了：\n\n## 一面：\n1. 自我介绍\n2. JAVA SDK起到的作用\n3. 项目\n4. 数据流（项目）\n5. 排序（介绍下你知道的排序和复杂度）\n6. Arrays.sort底层的排序算法（有三种策略）\n7. 堆排序基本思路\n8. linux，操作系统的开机流程（这题我不会。）\n9. 进程和线程的区别\n10. 进程切换会发生什么\n11. 进程调度算法有哪些\n12. TCP、udp区别\n13. java锁，关键字区别\n14. 公平锁、非公平锁解释一下\n\n## 二面：\n1. 算法题：由前序遍历中序遍历重建子树；\n2. 为什么静态类中不能使用非静态类（从类加载过程回答）；\n3. java类加载过程；\n   1. 加载阶段中，为什么要有自定义的类加载器；\n   2. 双亲委派原则的机制；\n4. HashMap数据结构；\n   1. 为什么小于6是链表，大于8变成红黑树；\n   2. HashMap扩容机制;\n   3. HashMap是否线程安全，例子；\n   4. ConcurrentHashMap和HashTable的区别；\n   5. ConcurrentHashMap如何保证高效，为什么是线程安全，为什么比HashTable优秀，分段锁机制；\n   6. CAS能保证线程安全吗（我回答能，面试官说不能。。估计想考ABA问题），volatile关键字能保证线程安全吗；\n\n5. 随机数求根，比如根号二的值。（二分查询）\n6. 有n个筐，筐固定，每个筐内有不同数目的苹果，移动苹果，使每个筐苹果平均（移动的代价：1~2算1步，1~3算2步）使步数最小；\n\n## 三面：\n1. 自我介绍\n2. 解决什么问题，做了些什么？（项目）\n3. 多个接口，有失败怎么办（项目）\n4. redis分布式锁怎么实现\n5. 时间过期怎么办\n6. ArrayList怎么扩容，时间复杂度O（n）？插尾部O（1），平均是多少，答案O(2)需要考虑扩容，小伙伴们可以自己推一下。\n7. HashMap底层原理\n8. mysql索引什么原理、B+树\n9. mysql和redis区别（讲一下各自优缺点）\n10. 为什么不用redis存数据？\n\n# 杭州涂鸦Java开发岗位1面2面面经\n> 作者：星星上的louie\n链接：https://www.nowcoder.com/discuss/692329?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n2021.07.15\n1年经验, 社招涂鸦小程序Java开发岗位\n## 涂鸦一面\n1. 项目\n2. Java有哪些锁？Synchronized原理？ Synchronized 为什么要进行锁膨胀？ReentrantLock 原理？\n3. AQS还在哪有使用过？\n4. JDK源码中用到了哪些设计模式？说一说责任链模式在你读过的源码里在哪用过？(\n5. ExecutorService用过吗？场景？FixedSizeThreadPool有哪些问题？\n6. FixedSizeThreadPool为什么会有OOM问题？(\n7. CMS与Ｇ1的区别？\n8. JVM有调优经验吗？为什么会产生OOM？什么会引起年轻代频繁回收？\n9. 什么是内存泄漏？\n10. MySql底层的数据结构？主键索引和非主键索引有什么区别？\n11. 数据量大嘛？Mysql慢查询如何优化？例子？\n12. Redis有哪些数据结构？分别有什么应用场景？Redis怎么部署的？Sential模式有什么问题？Sentiential挂了怎么办？\n13. 为什么要用RocketMQ？RocketMQ有哪些组件？RocketMQ如何保证高可用性？\n14. 如何保证消息的幂等性？\n15. 为什么要使用Netty框架？Netty有哪些组件？如何保证发送包的有序性？\n## 涂鸦2面\n1. 项目\n2. NIO元素组件? NIO 和BIO区别? NIO如何转AIO?\n3. MongoDB跟Mysql的差别？\n4. Mysql数据为啥在千万级别会有性能问题?为啥不是百万级别或者上亿级别?\n5. Mybatis2级缓存的原理？使用场景？1级缓存的原理？缓存淘汰策略？\n6. LRU算法？设计LRU缓存？如何保证高并发？给哪些节点加锁？\n# 平安银行java面经\n> 作者：6Ningt\n链接：https://www.nowcoder.com/discuss/693176?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 一面\n主要是围绕项目提的问题，持续时间接近一小时\n1. 自我介绍\n2. 最熟悉的项目是哪个，有哪些技术难点\n3. 根据项目提出了两种方案，问各自的优缺点是什么\n4. 应用题，定时任务和接口在一个系统，某一段时间定时任务处理大量数据影响接口响应，应该怎么做\n5. 如何提高系统的可靠性／可用性，在实际项目中做了哪些工作\n6. 框架源码有没有看过？spring boot在程序启动时做了什么，datasoure初始化过程\n7. 事务隔离级别，什么是脏读，不可重复读，幻读\n8. mvcc的原理\n9. redis常用的数据结构，在项目中的应用场景\n10. redis集群中某一个主节点挂掉后续会发生什么\n11. 熟悉的设计模式有哪些？在程序中有哪些应用\n12. 有没有遇到过fullgc或者jvm的问题，解决的过程\n\n\n## 二面\n问了一些框架和中间件原理，大概二十分钟就完事了\n1. 自我介绍\n2. 换工作的原因\n3. 讲讲最熟悉的项目，技术难点，项目中用了什么框架，基于这个框架做了什么\n4. redis的持久化方式，各自的优缺点\n5. redis key的淘汰策略\n6. mysql的索引类型，优缺点是什么\n7. sql语句优化\n8. 线程池的实现原理，如果使用无限队列会出现什么问题\n9. tcp三次握手，四次挥手\n\n## 三面\n也是围绕项目问的问题，问的很细，还有一些开放性的问题，大约半个小时\n1. 两分钟介绍一下做过的项目\n2. 最成功的项目是什么，为什么这么觉得\n3. 项目中遇到的难题，解决问题的过程，其中涉及框架的原理\n4. 项目中关键算法的原理，以及其他常用的算法，各自的优缺点是什么\n5. 在这个项目中有什么收获\n6. 评价一下自己的技术水平\n\n# 字节电商直通终面面经，附春招实习面经\n\n> 作者：-吴下阿蒙-\n链接：https://www.nowcoder.com/discuss/693370?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 字节跳动电商7.29直通终面（50min）\n1. 自我介绍\n2. 问项目\n3. 问实习\n4. 为啥没来字节实习\n5. 来道题，最大公共子串\n6. 嘴撕：链表有无环\n7. 嘴撕：有环链表找入环节点，数学证明\n8. sql题：表T：id name salary city\n9. 查找符合以下条件的记录\n   1. 1） salary> 10000\n   2. 2.）city的平均salary > 5000\n10. hashmap, concurrenthashmap源码看过？讲下原理\n11. 双亲委派机制及其作用\n12. 线程池核心参数解释解释？如果让你实现一个线程池，你会怎么做，越详细越好。\n13. MySql innoDB引擎的默认隔离级别？底层实现原理？\n14. 这个级别能解决幻读么？怎么解决？\n15. 反问：部门做些啥？什么时候出结果？有什么学习上的建议？\n## 字节跳动3.23实习一面（1h）\n1. 自我介绍\n2. 主要是用什么语言？\n3. 操作系统、计网、数据结构这些都学过吧？\n4. DNS查询过程讲讲。\n5. 根域名是什么？有哪些？\n6. 虚拟内存的作用是什么？\n7. 你提到了缺页中断的情况，讲讲这种情况怎么处理。\n8. 虚拟内存怎么寻址？\n9. 只用查页表吗？不用查偏移量吗？怎么查偏移量？\n10. 用过Netty是吧，BIO NIO AIO讲讲。\n11. 数据库索引引擎的数据结构有哪些？B+和Hash的特点与区别是什么？\n12. 什么情况下用哈希索引？\n13. MySQL支持哈希索引吗？\n14. 你写个死锁的实例吧。\n15. 手撕：和大于target的连续子序列的最小长度。\n16. 概率题：每轮抛硬币，A先B后，先抛到正面的赢，A赢的概率？\n17. 反问。我给你过了，你先别走等下一面。\n## 字节跳动3.23实习二面(1h)\n1. 自我介绍下吧。\n2. 技术栈是Java是吧，跟我讲讲JVM的内存区域和垃圾回收吧。\n3. TCP四次挥手讲讲。\n4. TIME WAIT是什么情况？\n5. 接触过哪些设计模式？运用设计模式有哪些原则？里氏替换具体讲讲？\n6. MySQL innoDB隔离级别有哪些，默认的是什么？\n7. RR的实现原理？（MVCC相关的讲了一大坨)\n8. 你说快照读的幻读通过MVCC可解决，那当前读的咋办呢？（MVCC+行级锁）\n9. 加行锁就能解决问题了吗？\n10. IO模型有哪些？\n11. IO多路复用讲讲？\n12. 你提到了select,poll,epoll这些，你讲讲epoll的原理吧。\n13. 学过哪些数据结构？图学过吗？\n14. 邪魅一笑：要不要挑战一下图的算法？（打扰了）\n15. 算了还是树吧\n16. 手撕Lc1530小改编，基本没多大变化。\n17. 反问：我刚才那个解法对了吗？有没有更优的解法？\n18. 你没有其他问题了吗？你不问问我们部门和你进来做些什么吗？\n19. 回去等通知。\n## 字节跳动3.26实习三面(1h10min)\n1. 自我介绍\n2. 前面两次面试感觉如何\n3. 项目从哪来的\n4. 出于什么目的去做了这个RPC项目\n5. 研究过哪些RPC框架\n6. 你讲讲Dubbo有哪些做的比较好的地方吧\n7. 你参考了它哪些优点\n8. 为什么要用RPC而不用现成的协议呢？\n9. 心跳？你怎么做的心跳策略\n10. 你这个自定义协议都是什么layout\n11. 你用的序列化都有哪些\n12. Json，Hessian, Protobuf你更倾向于用哪种，为什么？\n13. 你提到PB压缩了数字类型，字符串能压吗？\n14. 服务发现业界都是怎么做的（主要从注册中心切入，说了服务实例的存储，服务端注册服务，客户端拉取服务列表等等）\n15. SQL 每个省份重名top1的名字\n16. 求二叉树的宽度。\n\n# 百度提前批一面面经\n\n> 作者：dzsam\n链接：https://www.nowcoder.com/discuss/693636?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n视频面\n时间：1小时27分钟\n面试官人很好，学到了很多，很棒的一次面试体验\n\n1. 自我介绍\n2. 项目用户有多少\n3. 读写分离介绍一下\n4. 什么是多态\n5. 说说十大排序算法 时间复杂度 空间复杂度 记得不好 还要再看\n6. 说说快排的实现过程，非递归能实现吗\n7. 你了解mysql什么索引，介绍索引\n8. 哈希表是什么样的结构\n9. hash值一样怎么处理\n10. Java里有哪些类使用了hash\n11. 哈希冲突1.8 链表转红黑树\n12. 什么是rpc\n13. rpc框架有做过性能测试吗\n14. rpc请求失败了，可能是什么原因\n15. 算法题：寻找第k大，用快排思想+二分实现 中途经过面试官提醒 磕磕绊绊做出来。。\n16. 写一下单元测试\n17. 了解过哪些设计模式\n18. 单例模式具体的场景\n19. 手写单例模式 写了DCL。。\n20. 用过socket吗 服务器使用socket c语言的 用了什么函数\n21. http协议 报文的结构\n22. http和https的区别 了解TLS握手的过程吗\n23. 一卡车在一个小时内通过某一路段的概率是96% ，半小时有卡车通过的概率是？\n24. 硬币正反两面，两个人先抛到正面的算赢，先抛的赢的概率是？，升级n个人呢 ，写出公式\n\n反问\n\n# 字节后端开发面经\n> 作者：SKY技术修炼指南\n链接：https://www.nowcoder.com/discuss/693706?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 一面：\n1. IM系统用户登录怎么实现的？\n2. 登录状态是怎么保存的？session是怎么获取的？sessionid是怎么识别的？整个流程是什么样的？有没有考虑分布式session？\n3. Redis的数据类型\n4. Redis数据类型的底层数据结构\n5. 三次握手、四次挥手\n6. Redis持久化机制\n7. MySQL的InnoDB索引数据结构\n8. 哪些SQL的关键字会让索引失效\n9. 队列、栈、数组、链表\n10. 算法题：leetcode 92题\n## 二面：\n1. 讲讲爬虫的构成\n2. 爬虫抓到的数据不清洗吗？不去重吗？\n3. 对爬虫有什么更多的了解吗？\n4. Linux进程间通信机制\n5. 进程和线程的区别\n6. 线程私有的数据有哪些？（不是Java线程）\n7. 讲一下堆排序，每次调整的时间复杂度？堆排序是稳定的吗？（一开始说错了，应该是不稳定的，后面面试官问稳定的定义是什么）\n8. 哈希表的原理，怎么减小哈希表的冲突，怎么保证哈希表的并行读写\n9. Kafka用过吗？说说Kafka的原理？怎么保证Kafka的高可用？Kafka怎么保证消息有序？\n10. 项目里的set实现点赞，zset实现关注，为什么？\n11. zset底层实现？说一下跳表？节点的高度是多少？怎么决定节点的高度？\n12. https了解吗？\n13. 中间人攻击知道吗？怎么做https的抓包？https怎么篡改？\n14. 虚拟地址到物理地址的映射过程\n15. 算法题：给一个数组，建一颗最小高度的二叉树（递归和非递归）\n## 三面：\n1. 介绍一下做过的项目，哪些挑战性比较大，比较有难度的\n2. IM项目怎么用Netty的，为什么要用Netty，长连接交互是怎样的\n3. 消息怎么存储，怎么发送，怎么知道消息已读和未读的\n4. 读了5条消息、又来5条消息，你是怎么去更新的，你的消息是幂等的吗？\n5. 项目里怎么用ES的，ES怎么支持搜索的\n6. 技术论坛网站的评论是怎么存储的\n7. 查询评论是在DB里扫表查询吗？怎么展示所有的评论？性能如何？想要查询更快可以做哪些优化？\n8. 结合缓存和DB的时候会出现哪些问题？要怎么解决？\n9. 快排了解吗？介绍一下快排？时间复杂度是多少？为什么会退化成O(n^2)？单链表可以做快排吗？快排最核心的逻辑是什么？写一下单链表快排\n\n# 京东Java一二面面经\n> 作者：牧狼人🐺\n链接：https://www.nowcoder.com/discuss/694264?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 一面，30min\n1. 自我介绍\n2. 面向对象编程的特征:\n3. 静态变量和实例变量的区别\n4. int和Integer的区别\n5. final关键字\n6. final修饰变量不能改变是指引用不能改变还是值不能改变\n7. 重载和重写\n8. 接口和抽象类的区别\n9. 创建对象的方式\n10. ==和equals()的区别；\n11. 深拷贝和浅拷贝\n12. static的用法\n13. 简单说一下垃圾回收\n14. 并行和并发\n15. 为什么要使用多线程\n16. 创建线程的方式\n17. 创建线程这两种方式的区别\n18. start()方法和run()的区别\n19. ThreadLocal\n20. 索引的优缺点\n21. 项目，redis如何和mysql保证一致性\n22. redis中写库存时是在什么情况下写的\n23. 项目，具体情况，具体问题\n24. 都了解那些MQ\n25. 说一下RabbitMQ的优缺点\n26. 如何保证RabbitMQ中的消息不被重复消息\n\n面试官：基础不错，可以再深入拔高一下，不出问题会有其他人联系\n没有反问\n\n\n## 二面：30min\n\n1. 自我介绍\n2. 介绍项目\n3. 项目部署了么，怎么部署的，使用的是什么框架\n4. 项目做了一个什么产品，APP还是小程序\n5. 锁的使用，为什么使用，怎么设计锁的，锁的维度，锁怎么实现的，有没有其他实现方式呢\n6. RabbitMQ的使用，为什么使用，怎么使用的\n7. springboot自动装配原理\n8. springboot中有很多starter，你了解的starter都有哪些，这些starter有什么作用\n9. mysql了解的数据引擎，InnoDB和MylSAM的区别\n10. mysql事务的隔离级别，默认是什么，各个都解决了什么问题\n11. mysql是以什么什么方式实现事务的\n12. redis的集群，都说一下\n13. redis数据恢复的方式，AOF和RDB的区别\n14. 设计模式都了解多少，记得单例怎么实现的，用了什么关键字，需要判断几次\n15. 在做项目中遇到了那些问题，怎么解决的\n16. 最近都在做什么事，有学什么新东西\n17. 平时怎么学习的，平时上下班时间都是怎么安排的\n18. 可以实习吗\n\n反问：是哪个部门，base\n\n零售部门，做拍卖\n\n# 字节提前批后端三面+hr面面经，已OC！\n> 作者：王小宁。\n链接：https://www.nowcoder.com/discuss/694338?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n## 一面\n首先问实习+项目\n### MySQL\n\n1. char和varchar区别\n2. 数据库你都用过什么索引\n3. 主键索引和唯一索引的区别\n4. 说一下聚集索引和非聚集索引\n5. 什么是回表，是不是每次都要回表\n6. 说一下覆盖索引\n\n### 网络\n1. tcp与udp区别\n2. tcp如何保证可靠传输的\n3. 说一下tcp拥塞控制\n\n### OS\n1. 有没有用过go语言之外其他语言的多线程\n2. 什么是死锁，条件，如何避免\n3. 了解哪些网络IO模型\n4. 说一下select，poll，epoll\n\n### 算法\n1. 最长递增子序列，输出具体序列\n2. 判断无向图是否存在环路\n\n## 二面\n1. 首先问实习+项目\n2. 消息队列为什么能支持这么大的吞吐量\n\n### go语言\n1. 说一下go的select\n2. slice和数组有什么区别\n3. 重复关闭channel会怎样？向已关闭的channel写数据会怎样？从已关闭的channel读数据会怎样？\n4. 说一下context\n\n### MySQL\n1. mysql支持哪些存储引擎\n2. innodb和myisam的区别\n3. 数据库隔离级别\n4. 什么是幻读，怎么解决\n\n### 网络\n1. url输入到显示网页的过程\n2. tcp长连接如何实现的\n3. 业务当中用不用tcp自己的保活机制实现长连接\n4. 说一下dns劫持\n\n### 算法\nk个一组反转链表，不足k个也要反转\n\n## 三面\n实习+项目 问了半小时，深挖各种细节\n\n### 场景题\n1. 有一个tcp服务器，在不改变它本身任何代码的情况下，如何及时发现服务器down了\n2. tcp长连接连接池有几百万个连接，如何及时找出并关闭空闲连接(假设超过N秒无数据收/发的连接为空闲连接)\n3. 手机微信扫码登录网页版微信的功能，如何实现\n4. 没有算法题，没有八股文\n\n## HR面\n基本是聊天，大概半小时，小姐姐说两到三个工作日打电话通知结果。\n\n部分题目记不住了，就把记得住的写了一下，攒攒人品！\n\n许愿秋招的第一个OC！！！ 许愿秋招的第一个OC！！！ 许愿秋招的第一个OC！！！ 许愿秋招的第一个OC！！！ 许愿秋招的第一个OC！！！ 许愿秋招的第一个OC！！！\n\n8月3日更新，已OC并收到意向书了！！\n\n# 菜鸟物流 后端暑期实习 一面\n\n> 作者：_Asuka_\n链接：https://www.nowcoder.com/discuss/694628?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n迟来的面经，6.16面的，虽说是简历面，还是记录一下叭\n\n1. 什么接触Java？项目背景是老师合作的还是自己发起的？\n2. 问了卓越计划班是啥，成绩咋样\n3. 进程和线程的理解\n4. 进程间的通讯方式\n5. 死锁的必要条件和预防方式\n6. Linux的IO模型、常用命令\n7. OSI模型、有几层\n8. TCP、UDP区别\n9. 有哪些保证TCP能正确收发数据的机制\n10. 浏览器输入URL到显示的过程\n11. 数据库索引的作用、具体实现\n12. 聚簇索引、一张表能有几个\n13. 红黑树了解吗？说下定义\n14. 数据库为什么不会用红黑树来做索引\n15. 哪些操作会导致索引失效，使用组合索引应该注意什么（建立时根据字段的区分度高低、使用时符合最左匹配原则）\n16. 常用的排序算法了解吗，选个熟悉的说一下（说了快排）\n17. 快排最坏和最好的时间复杂度；空间复杂度？忘了。。(后来查了下最好是O(logN)，最坏是O(N)\n18. 常用的算法思想，贪心、递归、动归的不同点和相同点？？（有点懵，零零散散说了一些，面试官也会慢慢引导。看来对这些思想还是要多总结\n19. 说下ArrayList的实现，什么时候扩容的\n20. 有删除过ArrayList中的元素吗？（说了在不同位置删除的效率不同）；会有什么问题？提示了下说如果有多个相同元素，会导致删除不完全。如果相同元素是相邻的，要怎么做呢？\n21. JVM内存布局分为哪些\n22. 哪些是线程共享和私有的\n23. 堆会分为哪几部分（说了下新生代和老年代的具体划分）\n24. 新生代和老年代的GC算法\n25. 确定GC对象有哪些方法？（说了可达性分析）\n26. 类加载机制\n27. 类加载器有哪些，组织方式是怎样的（双亲委派），为什么要这样做\n28. 选一个项目详细介绍\n29. 项目的目的，项目相关\n30. 反问\n\n之前面完说两周内会有二面面试官联系我，结果等了两周都莫得消息，被鸽辽o(╥﹏╥)o\n"
  },
  {
    "path": "面试解答/面试解答2021-09.md",
    "content": "# 点击[:rocket::rocket::rocket:]可以看到知识点在哪\n\n---\n\n* [9月28美团java后端二面总结](#9月28美团java后端二面总结)\n  * [mysql:](#mysql)\n    * [1.从联合索引出发，要求画出联合索引B+树索引图。](#1从联合索引出发要求画出联合索引b树索引图)\n    * [2.从mysql事务出发，要求画出RC和RR隔离级别下的MVCC。](#2从mysql事务出发要求画出rc和rr隔离级别下的mvcc)\n    * [3.从mysql update语句出发， 要求说出binlog 与 redolog一致性问题。](#3从mysql-update语句出发-要求说出binlog-与-redolog一致性问题)\n  * [java基础:](#java基础)\n    * [4.从可重入锁出发， 要求写出基于可重入锁的阻塞队列，怎么实现。](#4从可重入锁出发-要求写出基于可重入锁的阻塞队列怎么实现)\n    * [5.从volatile出发， 要求写出volatile解决可见性的代码。](#5从volatile出发-要求写出volatile解决可见性的代码)\n    * [6.从设计模式出发， 要求写出装饰者模式和策略模式代码。](#6从设计模式出发-要求写出装饰者模式和策略模式代码)\n    * [7.HashMap 多线程下，怎么个个不安全法，数据丢失问题。](#7hashmap-多线程下怎么个个不安全法数据丢失问题)\n  * [分布式方面:](#分布式方面)\n    * [8.从银行转账出发， 要求描述出两阶段提交基于（RocketMQ）](#8从银行转账出发-要求描述出两阶段提交基于rocketmq)\n  * [redis相关:](#redis相关)\n    * [9.缓存穿透， 缓存击穿， 缓存雪崩基本概念及解决方案。](#9缓存穿透-缓存击穿-缓存雪崩基本概念及解决方案)\n    * [10.redis哨兵集群相关问题（怎么主从同步，怎么哨兵选举， 怎么master选举）](#10redis哨兵集群相关问题怎么主从同步怎么哨兵选举-怎么master选举)\n* [快手 秋招 Java开发 一二三面面经【已意向】](#快手-秋招-java开发-一二三面面经已意向)\n  * [1. 自我介绍](#1-自我介绍)\n  * [2. 实习项目、背景、需求介绍](#2-实习项目背景需求介绍)\n  * [3. InnoDB优点](#3-innodb优点)\n  * [4. MyISAM索引底层是什么结构](#4-myisam索引底层是什么结构)\n  * [5. B树和B+树区别](#5-b树和b树区别)\n  * [6. 为什么选择B+树不选择B树](#6-为什么选择b树不选择b树)\n  * [7. MySQL如何支持事务](#7-mysql如何支持事务)\n  * [8. undo log如何保证原子性](#8-undo-log如何保证原子性)\n  * [9. MySQL隔离级别、存在的问题](#9-mysql隔离级别存在的问题)\n  * [10. MySQL如何解决脏读、不可重复读、幻读](#10-mysql如何解决脏读不可重复读幻读)\n  * [11. 如何解决脏读？（读已提交）MySQL如何判断事务有没有提交？事务A中对id=1进行修改，不提交；事务B中读取id=1的数据，如何判断这个数据有没有被提交？](#11-如何解决脏读读已提交mysql如何判断事务有没有提交事务a中对id1进行修改不提交事务b中读取id1的数据如何判断这个数据有没有被提交)\n  * [12. InnoDB可重复读是否存在幻读问题](#12-innodb可重复读是否存在幻读问题)\n  * [13. 如果对记录修改，是否会读到修改的值？](#13-如果对记录修改是否会读到修改的值)\n  * [14. LeetCode：8. 字符串转换整数](#14-leetcode8-字符串转换整数)\n  * [15. HashMap和HashTable区别](#15-hashmap和hashtable区别)\n  * [16. synchronized如何实现HashTable线程安全](#16-synchronized如何实现hashtable线程安全)\n  * [17. 线程之间如何知道已经有线程在put（Mark word）](#17-线程之间如何知道已经有线程在putmark-word)\n  * [18. Mark word是什么](#18-mark-word是什么)\n  * [19. synchronized的锁优化](#19-synchronized的锁优化)\n  * [20. 出于目的写博客；什么时间写博客](#20-出于目的写博客什么时间写博客)\n  * [21. 反问](#21-反问)\n  * [22. 其他offer](#22-其他offer)\n  * [23. 自我介绍](#23-自我介绍)\n  * [24. 项目问题](#24-项目问题)\n  * [25. 实习有什么体感](#25-实习有什么体感)\n  * [26. 假设有1,2,3,4,5,6,7,8,9,10 在B+树中存储，是什么样子](#26-假设有12345678910-在b树中存储是什么样子)\n  * [27. 为什么1和2之间是链表](#27-为什么1和2之间是链表)\n  * [28. MySQL有哪些索引](#28-mysql有哪些索引)\n  * [29. 为什么会有覆盖索引](#29-为什么会有覆盖索引)\n  * [30. table 有a b c d四列，(b c d) 联合索引，selct c,d from table where c = 1会使用这个联合索引吗？不会，最左匹配](#30-table-有a-b-c-d四列b-c-d-联合索引selct-cd-from-table-where-c--1会使用这个联合索引吗不会最左匹配)\n  * [31. 为什么覆盖索引存在最左匹配原则](#31-为什么覆盖索引存在最左匹配原则)\n  * [32. select c,d from table where b = 1 and d = 2会走索引吗？我：行。面试官：这个可以行，也可以不行…分情况，MySQL中有一些优化，比如ICP，就会将索引下推（我没懂…）](#32-select-cd-f况mysql中有一些优化比如icp就会将索引下推我没懂)\n  * [33. 算法题：LeetCode 34. 在排序数组中查找元素的第一个和最后一个位置](#33-算法题leetcode-34-在排序数组中查找元素的第一个和最后一个位置)\n  * [34. HashMap底层数据结构是什么](#34-hashmap底层数据结构是什么)\n  * [35. HashMap先不考虑红黑树，手写一个底层数据结构，存储key value](#35-hashmap先不考虑红黑树手写一个底层数据结构存储key-value)\n  * [36. Java 线程的状态；time-waiting时间到了，进入什么；调用sleep()进入什么状态？time_waiting，那这个time_waiting状态会释放锁吗？不会；锁等待是什么状态？blocked](#36-java-线程的状_waiting状态会释放锁吗不会锁等待是什么状态blocked)\n  * [37. wait() notify() 以及线程状态转换](#37-wait-notify-以及线程状态转换)\n  * [38. Java线程状态和操作系统线程有什么不同？Java线程的 runable=ready+running，操作系统线程分为 running和 ready，并不是合在一起的](#38-java线程状态和操作系统线程有什么不同java线程为-running和-ready并不是合在一起的)\n  * [39. 为什么Java把这两个状态放在一起？](#39-为什么java把这两个状态放在一起)\n  * [40. 反问](#40-反问)\n  * [41. 自我介绍](#41-自我介绍)\n  * [42. JVM内存结构](#42-jvm内存结构)\n  * [43. 堆如何分代](#43-堆如何分代)\n  * [44. 为什么要分代](#44-为什么要分代)\n  * [45. 回收算法](#45-回收算法)\n  * [46. 回收算法有哪些具体实现？垃圾回收器](#46-回收算法有哪些具体实现垃圾回收器)\n  * [47. TCP三次握手](#47-tcp三次握手)\n  * [48. TCP 四次挥手](#48-tcp-四次挥手)\n  * [49. 为什么建立三次、断开是四次](#49-为什么建立三次断开是四次)\n  * [50. 四次挥手套接字的状态转移](#50-四次挥手套接字的状态转移)\n  * [51. 输入url的流程](#51-输入url的流程)\n  * [52. http的request、response的具体格式](#52-http的requestresponse的具体格式)\n  * [53. 你们的服务是如何部署的？SpringBoot中的Tomcat](#53-你们的服务是如何部署的springboot中的tomcat)\n  * [54. LRU 如何实现？在哪用过](#54-lru-如何实现在哪用过)\n  * [55. LRU put get 时复](#55-lru-put-get-时复)\n* [招银java一面](#招银java一面)\n  * [1.==和equals的区别（自我感觉良好）](#1和equals的区别自我感觉良好)\n  * [2.一个类的两个对象怎么进行比较（感到一丝茫然，是问重写equals还是问实现comparable接口啊）](#2一个类的两个对象怎么进行比较感到一丝茫然是问重写equals还是问实现comparable接口啊)\n  * [3.既然说到hashcode，有没有可能两个对象equals但是hashcode不同（开始懵逼，自然情况下没可能吧？对叭？）](#3既然说到hashcode有没有可能两个对象equals但是hashcode不同开始懵逼自然情况下没可能吧对叭)\n  * [4.如果出现了上述这种情况，有可能发生什么情况？（hashset没法覆盖？）](#4如果出现了上述这种情况有可能发生什么情况hashset没法覆盖)\n  * [5.用过多线程吗，怎么实现的多线程？（答了自己用线程池，还可以用其他三种方法创建新线程）](#5用过多线程吗怎么实现的多线程答了自己用线程池还可以用其他三种方法创建新线程)\n  * [6.那线程池的线程具体在什么时候创建一个线程或者销毁一个线程？（痛苦面具）](#6那线程池的线程具体在什么时候创建一个线程或者销毁一个线程痛苦面具)\n  * [7.你能手动实现一个死锁的情况吗（说思路）](#7你能手动实现一个死锁的情况吗说思路)\n  * [8.有ABC三个线程，怎么编程让B在C前面执行，A在B前面执行（之前看过这题，说了思路被diss太麻烦但是逻辑可行）](#8有abc三个线程怎么编程让b在c前面执行a在b前面执行之前看过这题说了思路被diss太麻烦但是逻辑可行)\n  * [9.问一下数据结构，你了解哪些二叉树的种类和他们的具体使用场景（已经有点崩溃了，说了搜索和完全，其他就想不起来了，所以面试官开始引导我）](#9问一下数据结构你了解哪些二叉树的种类和他们的具体使用场景已经有点崩溃了说了搜索和完全其他就想不起来了所以面试官开始引导我)\n  * [10.AVL树了解吗（宕机了，想不起来应用场景了，就说不太记得了）](#10avl树了解吗宕机了想不起来应用场景了就说不太记得了)\n  * [11.红黑树了解吗（简单说了一下）](#11红黑树了解吗简单说了一下)\n  * [12.红黑树的具体应用场景，举个例子（说了hashmap和1.8的concurrent hashmap）](#12红黑树的具体应用场景举个例子说了hashmap和18的concurrent-hashmap)\n  * [13.为什么用红黑树不一直用链表](#13为什么用红黑树不一直用链表)\n  * [14.为什么用红黑树不用普通二叉树（说了普通二叉树会导致一侧树的深度太深）](#14为什么用红黑树不用普通二叉树说了普通二叉树会导致一侧树的深度太深)\n  * [15.普通二叉树深度太深会导致什么？（…）](#15普通二叉树深度太深会导致什么)\n  * [16.B树和B+树知道吗？区别是什么？](#16b树和b树知道吗区别是什么)\n  * [17.B树和B+树的应用场景说一下（mysql的索引）](#17b树和b树的应用场景说一下mysql的索引)\n  * [18.给字段加索引最好怎么加？](#18给字段加索引最好怎么加)\n  * [19.什么情况下使用复合索引更好？](#19什么情况下使用复合索引更好)\n  * [20.什么情况下会导致索引失效？（到这里都信心满满）](#20什么情况下会导致索引失效到这里都信心满满)\n  * [21.为什么使用模糊匹配会失效，你能给我解释一下底层原理吗？（？？？？？？？）](#21为什么使用模糊匹配会失效你能给我解释一下底层原理吗)\n  * [22.网络协议有了解吗，为什么Tcp是三次握手四次挥手不是四四或者三三？](#22网络协议有了解吗为什么tcp是三次握手四次挥手不是四四或者三三)\n  * [23.平时做项目用http还是https?](#23平时做项目用http还是https)\n  * [24.SSL套接字的过程？（啊？？？？？）](#24ssl套接字的过程啊)\n  * [25.SSL在历史上有一次心脏流血漏洞，这个漏洞怎么出现的？（啊？？？？？？？？？？orz）](#25ssl在历史上有一次心脏流血漏洞这个漏洞怎么出现的啊orz)\n  * [26.设计模式用过吗？（说用过工厂模式）](#26设计模式用过吗说用过工厂模式)\n  * [27.那我们来聊聊单例模式（？？？？？？），单例模式有几种实现方式？（这里有一个地方说错了，说成饿汉是编译时期生成了）](#27那我们来聊聊单例模式单例模式有几种实现方式这里有一个地方说错了说成饿汉是编译时期生成了)\n  * [28.你再想想，是编译时期吗？我问下你，你写的代码如何运行，这个过程你说一下（对不起！！！！）](#28你再想想是编译时期吗我问下你你写的代码如何运行这个过程你说一下对不起)\n  * [29.为什么双重校验，一次校验不行吗（这题我会！）](#29为什么双重校验一次校验不行吗这题我会)\n  * [30.那怎么用一次校验实现线程安全？（我忘了orz开始胡言乱语，没有自信的问静态内部类可以吗）](#30那怎么用一次校验实现线程安全我忘了orz开始胡言乱语没有自信的问静态内部类可以吗)\n  * [31.静态内部类效率也不太好，你能有什么优化方法吗（对不起！！！我真的没用过我不会！！！）](#31静态内部类效率也不太好你能有什么优化方法吗对不起我真的没用过我不会)\n  * [32.再来问问网络安全吧，Sql注入…（慌张打断，说我不了解网络安全，没有学过这方面）](#32再来问问网络安全吧sql注入慌张打断说我不了解网络安全没有学过这方面)\n  * [33.没关系，那接着聊，刚才说的hashmap，hashmap怎么解决hash冲突](#33没关系那接着聊刚才说的hashmaphashmap怎么解决hash冲突)\n  * [34.除了链地址法还有其他的解决hash冲突的方法吗（开放定址和再哈希）](#34除了链地址法还有其他的解决hash冲突的方法吗开放定址和再哈希)\n  * [35.如果hashmap溢出了怎么办（建立公共溢出区？）](#35如果hashmap溢出了怎么办建立公共溢出区)\n  * [36.公共溢出区也满了怎么办？（啊…？这我真的盲区了，我说hashmap也会扩容吧…？）](#36公共溢出区也满了怎么办啊这我真的盲区了我说hashmap也会扩容吧)\n  * [37.说一下hashmap扩容的过程？](#37说一下hashmap扩容的过程)\n  * [38.你对jvm有了解吗？说一下jvm的内存分区？](#38你对jvm有了解吗说一下jvm的内存分区)\n  * [39.堆里面怎么分区的？（这题真不会，只说知道为了方便垃圾回收所以分了新生代区和老年代区，其他的真不知道）](#39堆里面怎么分区的这题真不会只说知道为了方便垃圾回收所以分了新生代区和老年代区其他的真不知道)\n  * [40.没关系，那你知道一个对象怎么从新生代变成老年代吗？（懵逼，对不起，不知道，只简单的知道两个区的定义）](#40没关系那你知道一个对象怎么从新生代变成老年代吗懵逼对不起不知道只简单的知道两个区的定义)\n* [深圳今日头条--JAVA后端开发](#深圳今日头条--java后端开发)\n  * [1. MyISAM 和 InnoDB 比较；](#1-myisam-和-innodb-比较)\n  * [2. mysql都有哪些索引类型；为什么b+树，红黑树、b树为什么不好；](#2-mysql都有哪些索引类型为什么b树红黑树b树为什么不好)\n  * [3. mysql的主键，唯一索引区别，怎么建索引；](#3-mysql的主键唯一索引区别怎么建索引)\n  * [4. 一条sql怎么优化？](#4-一条sql怎么优化)\n  * [5. 数据库的范式？【三大范式】](#5-数据库的范式三大范式)\n  * [6. 数据库事务，ACID，mvcc](#6-数据库事务acidmvcc)\n  * [7. mysql怎么实现主从复制？ 【binlog】](#7-mysql怎么实现主从复制-binlog)\n  * [8. redis持久化机制](#8-redis持久化机制)\n  * [9. redis的基础数据类型，以及他们如何实现](#9-redis的基础数据类型以及他们如何实现)\n  * [10. redis缓存问题-雪崩，击穿](#10-redis缓存问题-雪崩击穿)\n  * [11. redis数据一致性问题，如何解决？](#11-redis数据一致性问题如何解决)\n  * [12. 谈一谈http，https](#12-谈一谈httphttps)\n  * [13. tcp怎么实现可靠传输，udp可以可靠传输吗？](#13-tcp怎么实现可靠传输udp可以可靠传输吗)\n  * [14. stmp，ftp了解吗【我都没看过，三面考的】](#14-stmpftp了解吗我都没看过三面考的)\n  * [15. tcp拥塞控制，滑动窗口](#15-tcp拥塞控制滑动窗口)\n  * [16. tcp的sync攻击，为什么三次握手](#16-tcp的sync攻击为什么三次握手)\n  * [17. tcp listen backlog【当时一脸懵，三面考的】](#17-tcp-listen-backlog当时一脸懵三面考的)\n  * [18. OSI七层协议](#18-osi七层协议)\n  * [19. 输入URL到页面加载过程](#19-输入url到页面加载过程)\n  * [20. linux 执行二进制文件过程。。。【三面考的，我当场就裂开了】](#20-linux-执行二进制文件过程三面考的我当场就裂开了)\n  * [21. linux 创建进程啥的【也裂开】](#21-linux-创建进程啥的也裂开)\n  * [22. 内核，用户态，内核态，怎么切换](#22-内核用户态内核态怎么切换)\n  * [23. 进程线程协程](#23-进程线程协程)\n  * [24. 进程通信方式，哪种最高效](#24-进程通信方式哪种最高效)\n  * [25. 进程同步方式](#25-进程同步方式)\n  * [26. 谈谈虚拟内存【听到谈谈就麻】](#26-谈谈虚拟内存听到谈谈就麻)\n  * [27. 谈谈使用过的几种设计模式，以及优缺点【真的太高频了，我每次都被考】](#27-谈谈使用过的几种设计模式以及优缺点真的太高频了我每次都被考)\n  * [28. jvm内存模型，如何分配内存](#28-jvm内存模型如何分配内存)\n  * [29. 垃圾回收算法](#29-垃圾回收算法)\n  * [30. 类加载机制](#30-类加载机制)\n  * [31. 锁都有哪些，区别](#31-锁都有哪些区别)\n  * [32. RPC相关](#32-rpc相关)\n  * [33. 消息中间件相关，MQ](#33-消息中间件相关mq)\n  * [34. 多路io复用](#34-多路io复用)\n* [深圳转转 Java 一、二面 2021.9.15（已意向书）](#深圳转转-java-一二面-2021915已意向书)\n  * [1. 自我介绍](#1-自我介绍-1)\n  * [2. 介绍一下项目？](#2-介绍一下项目)\n  * [3. Spring的两个核心。说一下](#3-spring的两个核心说一下)\n  * [4. AOP主要用到的Java的哪些技术呢？](#4-aop主要用到的java的哪些技术呢)\n  * [5. MySQL的索引有哪些了解？](#5-mysql的索引有哪些了解)\n  * [6. 主键索引和普通索引有什么区别？](#6-主键索引和普通索引有什么区别)\n  * [7. 事务的隔离级别有哪些？](#7-事务的隔离级别有哪些)\n  * [8. 不同的隔离级别解决了哪些问题？](#8-不同的隔离级别解决了哪些问题)\n  * [9. 可重复读有没有解决这个幻读的问题？](#9-可重复读有没有解决这个幻读的问题)\n  * [10. 可重复读如何解决不可重复读的一个问题？](#10-可重复读如何解决不可重复读的一个问题)\n  * [11. ACID四个特性有了解过吗？](#11-acid四个特性有了解过吗)\n  * [12. 怎么保证这ACID四个特性？](#12-怎么保证这acid四个特性)\n  * [13. MVCC有没有起到作用？](#13-mvcc有没有起到作用)\n  * [14. 介绍一下集合](#14-介绍一下集合)\n  * [15. HashMap是线程安全的还是线程不安全的？](#15-hashmap是线程安全的还是线程不安全的)\n  * [16. HashMap线程不安全会出现什么问题？](#16-hashmap线程不安全会出现什么问题)\n  * [17. 有没有了解过线程安全的HashMap？](#17-有没有了解过线程安全的hashmap)\n  * [18. ConcurrentHashMap怎么保证线程安全？](#18-concurrenthashmap怎么保证线程安全)\n  * [19. 有没有实际用过多线程的东西？](#19-有没有实际用过多线程的东西)\n  * [20. 线程的创建有哪几种方式？](#20-线程的创建有哪几种方式)\n  * [21. Thread类和Runable接口的最大区别是什么？](#21-thread类和runable接口的最大区别是什么)\n  * [22. 线程池最核心的是哪些？](#22-线程池最核心的是哪些)\n  * [23. 线程池的执行顺序是怎么样的呢？](#23-线程池的执行顺序是怎么样的呢)\n  * [24. 运行的时候，核心线程数能不能修改？](#24-运行的时候核心线程数能不能修改)\n  * [25. JVM的内存结构？](#25-jvm的内存结构)\n  * [26. 对象在哪个区？](#26-对象在哪个区)\n  * [27. Class文件在哪个地方存？](#27-class文件在哪个地方存)\n  * [28. 垃圾回收会发生在哪几个区域？](#28-垃圾回收会发生在哪几个区域)\n  * [29. OOM会发生在哪个区域？](#29-oom会发生在哪个区域)\n  * [30. 虚拟机栈会不会溢出？](#30-虚拟机栈会不会溢出)\n  * [31. GC算法有没有了解过？](#31-gc算法有没有了解过)\n  * [32. 怎么确定一个对象是垃圾？](#32-怎么确定一个对象是垃圾)\n  * [33. 哪些对象可以做为CG Root？](#33-哪些对象可以做为cg-root)\n  * [34. Java有哪些锁？](#34-java有哪些锁)\n  * [35. 有没有除了Synchronize和ReentrantLock之外还有没有可以上锁的？](#35-有没有除了synchronize和reentrantlock之外还有没有可以上锁的)\n  * [36. volatile怎么理解？](#36-volatile怎么理解)\n  * [37. 类加载器有哪些？](#37-类加载器有哪些)\n  * [38. 双亲委派模型](#38-双亲委派模型)\n  * [39. 怎么打破双亲委派模型？（面试的时候答了不会）（面试官告诉我说Tomcat有）](#39-怎么打破双亲委派模型面试的时候答了不会面试官告诉我说tomcat有)\n  * [40. 类加载的过程，Class文件](#40-类加载的过程class文件)\n  * [41. 有没有自己使用过算法](#41-有没有自己使用过算法)\n  * [42. 有哪些排序？](#42-有哪些排序)\n  * [43. 快排的原理。时间复杂度，最坏情况。](#43-快排的原理时间复杂度最坏情况)\n  * [44. TCP连接和断开。](#44-tcp连接和断开)\n  * [45. 三次握手过程](#45-三次握手过程)\n  * [46. TCP连接CloseWait和TimeWait状态](#46-tcp连接closewait和timewait状态)\n  * [47. 网络的拥塞控制有没有了解过？](#47-网络的拥塞控制有没有了解过)\n  * [48. 输入一个网址的调用流程。](#48-输入一个网址的调用流程)\n  * [49. 有没有了解过HTTPS](#49-有没有了解过https)\n  * [50. HTTPS的加密算法是哪些？](#50-https的加密算法是哪些)\n  * [51. 你感觉你比较擅长哪方面？](#51-你感觉你比较擅长哪方面)\n  * [52. 有没有刷过题？](#52-有没有刷过题)\n  * [53. 你觉得动态规划的关键是什么？](#53-你觉得动态规划的关键是什么)\n  * [54. 反问环节。](#54-反问环节)\n  * [55. 数据库索引](#55-数据库索引)\n  * [56. 单核情况下，多线程为什么会比单线程的情况下去使用情况会更好？（面试官提醒后，会了）](#56-单核情况下多线程为什么会比单线程的情况下去使用情况会更好面试官提醒后会了)\n  * [57. TCP协议了解吗？](#57-tcp协议了解吗)\n  * [58. HTTP长连接还是短连接？](#58-http长连接还是短连接)\n  * [59. 服务端主动发起关闭还是客户端主动发起关闭TCP？](#59-服务端主动发起关闭还是客户端主动发起关闭tcp)\n  * [60. 心跳机制说一下？（几乎没答对）](#60-心跳机制说一下几乎没答对)\n  * [61. 单例模式说一下。](#61-单例模式说一下)\n  * [62. 线程安全的单例模式是怎么样的？](#62-线程安全的单例模式是怎么样的)\n  * [63. 那为什么要使用volatile呢？](#63-那为什么要使用volatile呢)\n  * [64. 为什么要使用双段锁呢？](#64-为什么要使用双段锁呢)\n  * [65. 算法题：8个人乒乓球比赛，A赢B，B赢C，可以默认A赢C。那么最少比赛多少次可以获得冠亚季军（当时没想出来，心态崩了。）](#65-算法题8个人乒乓球比赛a赢bb赢c可以默认a赢c那么最少比赛多少次可以获得冠亚季军当时没想出来心态崩了)\n  * [66. 你怎么学习技术？](#66-你怎么学习技术)\n  * [67. 你觉得什么最重要？](#67-你觉得什么最重要)\n  * [68. 你在团队开发的项目里面学到了什么？](#68-你在团队开发的项目里面学到了什么)\n  * [69. 反问。](#69-反问)\n* [顺丰二面 20210915](#顺丰二面-20210915)\n  * [1. ThreadLocal的底层原理以及其应用](#1-threadlocal的底层原理以及其应用)\n  * [2. Mybatis框架了解哪些](#2-mybatis框架了解哪些)\n  * [3. Mybatis中的#和$的区别](#3-mybatis中的和的区别)\n  * [4. Mybatis执行一条语句的底层原理，如何对他对他进行一些优化排序](#4-mybatis执行一条语句的底层原理如何对他对他进行一些优化排序)\n  * [5. 熟悉哪些设计模式](#5-熟悉哪些设计模式)\n  * [6. hashmap的底层结构，put和get的整个的执行过程](#6-hashmap的底层结构put和get的整个的执行过程)\n  * [7. 如何在分布式的情况下实现事务一致性](#7-如何在分布式的情况下实现事务一致性)\n  * [8. 类的排序根据某个字段](#8-类的排序根据某个字段)\n  * [9. 分布式情况下如何保证数据一致性](#9-分布式情况下如何保证数据一致性)\n  * [10. synchronized实际中的使用，底层原理](#10-synchronized实际中的使用底层原理)\n  * [11. 缓存穿透、击穿、雪崩](#11-缓存穿透击穿雪崩)\n* [携程Java面经20210913](#携程java面经20210913)\n  * [1. 自我介绍](#1-自我介绍-2)\n  * [2. 聊两个项目](#2-聊两个项目)\n  * [3. MySQL事务隔离机制，如何实现](#3-mysql事务隔离机制如何实现)\n  * [4. Spring如何避免循环依赖](#4-spring如何避免循环依赖)\n  * [5. 线程的创建方式](#5-线程的创建方式)\n  * [6. 线程池参数](#6-线程池参数)\n  * [7. 当所有线程处理完毕打印一条日志怎么做](#7-当所有线程处理完毕打印一条日志怎么做)\n  * [8. 两个千万级别的int数组，输出交集，说一下你认为时间复杂度最低的方案](#8-两个千万级别的int数组输出交集说一下你认为时间复杂度最低的方案)\n  * [9. 反问：表现和建议。建议我应该多做项目，很多东西书里没有……但是我工作一年多，简历四个项目啊？](#9-反问表现和建议建议我应该多做项目很多东西书里没有但是我工作一年多简历四个项目啊)\n* [网易云音乐Java开发岗秋招8.30一二面面经](#网易云音乐java开发岗秋招830一二面面经)\n  * [1. 描述一下项目的技术架构](#1-描述一下项目的技术架构)\n  * [2. 有没有了解过阿里云的分布式数据库?单点数据库的话能够支撑目前项目的数据吗?有了解阿里云的分库分表技术吗?](#2-有没有了解过阿里云的分布式数据库单点数据库的话能够支撑目前项目的数据吗有了解阿里云的分库分表技术吗)\n  * [3. ES如何使用?为何ES查询会变快?目前用的哪款分词器?ES和数据库数据不一致该如何处理?](#3-es如何使用为何es查询会变快目前用的哪款分词器es和数据库数据不一致该如何处理)\n  * [4. DDD是如何实践的，起到了什么作用?](#4-ddd是如何实践的起到了什么作用)\n  * [5. 讲一下对索引的理解](#5-讲一下对索引的理解)\n  * [6. innoDB和MyISAM的区别](#6-innodb和myisam的区别)\n  * [7. b+树的结构，叶子节点设计成有序链表的好处](#7-b树的结构叶子节点设计成有序链表的好处)\n  * [8. 描述下双向链表的数据结构](#8-描述下双向链表的数据结构)\n  * [9. Explain进行查询性能优化，需要关注哪些字段?](#9-explain进行查询性能优化需要关注哪些字段)\n  * [10. 举一个Redis的使用场景](#10-举一个redis的使用场景)\n  * [11. 跳表是如何实现的?](#11-跳表是如何实现的)\n  * [12. Redis的基本数据类型](#12-redis的基本数据类型)\n  * [13. Redis的数据淘汰策略，Iru如何实现?](#13-redis的数据淘汰策略iru如何实现)\n  * [14. Rocket事务消息的实现机制](#14-rocket事务消息的实现机制)\n  * [15. RocketMQ和Kafka的区别](#15-rocketmq和kafka的区别)\n  * [16. 你对什么技术有沉淀或者学习收获?](#16-你对什么技术有沉淀或者学习收获)\n  * [17. 你有什么擅长点没有被问到，讲一下](#17-你有什么擅长点没有被问到讲一下)\n  * [18. 你这个数据中台好像没什么用啊，客户不能自己做吗?功能好像不是很充分啊?](#18-你这个数据中台好像没什么用啊客户不能自己做吗功能好像不是很充分啊)\n  * [19. 对接不同客户有哪些通用的模块?](#19-对接不同客户有哪些通用的模块)\n  * [20. 你觉得项目业务复杂在什么地方?为什么客户会选择你们来做?](#20-你觉得项目业务复杂在什么地方为什么客户会选择你们来做)\n  * [21. 用什么进行存储?](#21-用什么进行存储)\n  * [22. MySQL联合索引的结构，在b+树上体现?](#22-mysql联合索引的结构在b树上体现)\n  * [23. 进程间的通信方式](#23-进程间的通信方式)\n  * [24. Kafka的原理](#24-kafka的原理)\n  * [25. Kafka的消息丢失问题，考虑主从同步](#25-kafka的消息丢失问题考虑主从同步)\n  * [26. 你有什么方面擅长](#26-你有什么方面擅长)\n  * [27. Redis集群的部署方式](#27-redis集群的部署方式)\n  * [28. Redis的基本数据类型](#28-redis的基本数据类型)\n  * [29. JVM新生代和老年代垃圾回收的方式](#29-jvm新生代和老年代垃圾回收的方式)\n* [蚂蚁金服一面20210909-被打得面目全非](#蚂蚁金服一面20210909-被打得面目全非)\n  * [1.redis为什么快？](#1redis为什么快)\n  * [2.epoll你知道吗](#2epoll你知道吗)\n  * [3.nio你知道多少？和bio区别，零拷贝，netty](#3nio你知道多少和bio区别零拷贝netty)\n  * [4.你知道有nio哪些组件吗？selector，buffer，channel](#4你知道有nio哪些组件吗selectorbufferchannel)\n  * [5.io和nio区别是什么？io是面向流的，nio是面向什么的？](#5io和nio区别是什么io是面向流的nio是面向什么的)\n  * [6.redis和db一致性怎么设计](#6redis和db一致性怎么设计)\n  * [7.如果订阅binlog，会有什么问题？](#7如果订阅binlog会有什么问题)\n  * [8.如果延时双删，删除失败了怎么办，阻塞放入队列里面吗，那用户岂不是得一直等着？](#8如果延时双删删除失败了怎么办阻塞放入队列里面吗那用户岂不是得一直等着)\n  * [9.如果先更新数据库，再删除缓存，删除缓存失败了怎么办？](#9如果先更新数据库再删除缓存删除缓存失败了怎么办)\n  * [10.缓存热点会造成什么问题？](#10缓存热点会造成什么问题)\n  * [11.redis的内存淘汰策略你知道哪些？](#11redis的内存淘汰策略你知道哪些)\n  * [12.redis数据结构？](#12redis数据结构)\n  * [13.threadLocal知道吗，如果不remove，出问题了怎么补救？](#13threadlocal知道吗如果不remove出问题了怎么补救)\n  * [14.countDownLatch和semaphore是什么，怎么用](#14countdownlatch和semaphore是什么怎么用)\n  * [15.Java线程池有哪些，线程池数量怎么配](#15java线程池有哪些线程池数量怎么配)\n  * [16.多线程需要从哪些方面考虑](#16多线程需要从哪些方面考虑)\n  * [17.spi知道吗？](#17spi知道吗)\n  * [18.怎么定义一个注解？](#18怎么定义一个注解)\n  * [19.怎么提高查询db性能](#19怎么提高查询db性能)\n  * [20.db容灾有哪几种？](#20db容灾有哪几种)\n  * [21.十亿用户，怎么设计db？](#21十亿用户怎么设计db)\n  * [22.什么时候fullgc](#22什么时候fullgc)\n  * [23.订单延时取消怎么做？](#23订单延时取消怎么做)\n  * [设计一个内部的rpc接口，你觉得需要从哪些方面考虑？](#设计一个内部的rpc接口你觉得需要从哪些方面考虑)\n* [转转-Java后端开发一、二面+hr面](#转转-java后端开发一二面hr面)\n  * [1. 项目的超卖如何解决的？](#1-项目的超卖如何解决的)\n  * [2. 排序算法有哪些？](#2-排序算法有哪些)\n  * [3. 堆排序的时间复杂度？说说堆排序的排序过程，是怎么得到这个时间复杂度的？](#3-堆排序的时间复杂度说说堆排序的排序过程是怎么得到这个时间复杂度的)\n  * [4. Object类中有哪些方法？](#4-object类中有哪些方法)\n  * [5. wait()和sleep()的区别？](#5-wait和sleep的区别)\n  * [6. 线程池的参数和作用？线程池的执行流程/原理？](#6-线程池的参数和作用线程池的执行流程原理)\n  * [7. JVM内存模型，垃圾回收算法？](#7-jvm内存模型垃圾回收算法)\n  * [8. HashMap的容量为什么要初始化为2的n次幂？](#8-hashmap的容量为什么要初始化为2的n次幂)\n  * [9. HashMap和ConcurrentHashMap的区别？](#9-hashmap和concurrenthashmap的区别)\n  * [10. ConcurrentHashMap的扩容过程，源码有没有看过？](#10-concurrenthashmap的扩容过程源码有没有看过)\n  * [11. 说说你对Synchronized的理解，底层原理？](#11-说说你对synchronized的理解底层原理)\n  * [12. 除了Synchronized，还知道Java中的其他锁吗？](#12-除了synchronized还知道java中的其他锁吗)\n  * [13. 说说你对Lock/ReentraLock的理解？有没有看过源码？](#13-说说你对lockreentralock的理解有没有看过源码)\n  * [14. 说说你对MySQL索引的理解，什么是聚簇索引和非聚簇索引？](#14-说说你对mysql索引的理解什么是聚簇索引和非聚簇索引)\n  * [15. 如何根据索引查找数据的，索引执行的流程/原理？](#15-如何根据索引查找数据的索引执行的流程原理)\n  * [16. MySQL的事务隔离级别？](#16-mysql的事务隔离级别)\n  * [17. MySQL是如何解决幻读的？](#17-mysql是如何解决幻读的)\n  * [18. Redis有哪些了解，基本数据类型有哪些，底层实现知道吗？](#18-redis有哪些了解基本数据类型有哪些底层实现知道吗)\n  * [19. Redis的缓存淘汰策略、持久化机制说一下？](#19-redis的缓存淘汰策略持久化机制说一下)\n  * [20. 项目如何限流？](#20-项目如何限流)\n  * [21. 添加购物车时，数据库层面是如何操作的？](#21-添加购物车时数据库层面是如何操作的)\n  * [22. 知道接口的幂等性和非幂等性吗？](#22-知道接口的幂等性和非幂等性吗)\n  * [23. 项目里面有没有考虑幂等性？](#23-项目里面有没有考虑幂等性)\n  * [24. 自定义线程池需要关注的参数有哪些？](#24-自定义线程池需要关注的参数有哪些)\n  * [25. 线程池的运行原理？](#25-线程池的运行原理)\n  * [26. 阻塞队列满了以后，新进来的线程是执行队列头部的任务还是队尾的任务？](#26-阻塞队列满了以后新进来的线程是执行队列头部的任务还是队尾的任务)\n  * [27. 如果阻塞队列满了以后，系统重启/宕机，需要考虑什么情况？如何做？](#27-如果阻塞队列满了以后系统重启宕机需要考虑什么情况如何做)\n  * [28. synchronized锁的底层原理？](#28-synchronized锁的底层原理)\n  * [29. synchronized锁和lock锁的区别](#29-synchronized锁和lock锁的区别)\n* [渤海银行提前批面试9.6](#渤海银行提前批面试96)\n  * [1.多线程中run()和start()方法的区别？](#1多线程中run和start方法的区别)\n  * [2.synchronized和lock的区别？](#2synchronized和lock的区别)\n  * [3.final fianaly finanize](#3final-fianaly-finanize)\n  * [4.数据库三范式](#4数据库三范式)\n  * [5.索引](#5索引)\n  * [6.如何打破死锁](#6如何打破死锁)\n  * [7.框架用过啥？](#7框架用过啥)\n  * [8.配置文件啥类型？](#8配置文件啥类型)\n  * [9.持久层框架 mybatis](#9持久层框架-mybatis)\n  * [10.mybatis特性？](#10mybatis特性)\n  * [11.linux文件的属性怎么看？属主、属组和其他用户。](#11linux文件的属性怎么看属主属组和其他用户)\n* [阿里9/2 Java后端 57min](#阿里92-java后端-57min)\n  * [1. tcp和udp区别](#1-tcp和udp区别)\n  * [2. TCP/IP协议涉及哪几层架构](#2-tcpip协议涉及哪几层架构)\n  * [3. 4次挥手为什么是4次](#3-4次挥手为什么是4次)\n  * [4. 为什么要4次挥手](#4-为什么要4次挥手)\n  * [5. 学生表和成绩表sql选出没考试的学生](#5-学生表和成绩表sql选出没考试的学生)\n  * [6. sql选出参加2次考试的学生](#6-sql选出参加2次考试的学生)\n  * [7. 计算机插上电源操作系统做了什么](#7-计算机插上电源操作系统做了什么)\n  * [8. 操作系统设备文件有哪些](#8-操作系统设备文件有哪些)\n  * [9. 多线程同步有哪些方法](#9-多线程同步有哪些方法)\n  * [10. 两个方法加 synchronized，一个线程进去sleep，另一个线程可以进入到另一个方法吗](#10-两个方法加-synchronized一个线程进去sleep另一个线程可以进入到另一个方法吗)\n  * [11. 如何让可重入变成不可重入](#11-如何让可重入变成不可重入)\n  * [12. 创建线程的三个方法分别什么时候使用](#12-创建线程的三个方法分别什么时候使用)\n  * [13. 怎么获取线程的返回值](#13-怎么获取线程的返回值)\n  * [14. 线程池怎么创建](#14-线程池怎么创建)\n  * [15. 线程池参数如何设计](#15-线程池参数如何设计)\n  * [16. 拒绝策略有哪些](#16-拒绝策略有哪些)\n  * [17. 如何设计线程数量](#17-如何设计线程数量)\n  * [18. 5个任务，4个最大线程数，线程池里面同时运行几个任务](#18-5个任务4个最大线程数线程池里面同时运行几个任务)\n  * [19. 给用户发消息任务超出队列，你用哪个拒绝策略](#19-给用户发消息任务超出队列你用哪个拒绝策略)\n  * [20. 有其他方法吗](#20-有其他方法吗)\n  * [21. JMM](#21-jmm)\n  * [22. 什么时候用多线程、为什么要设计多线程](#22-什么时候用多线程为什么要设计多线程)\n  * [23. 多线程越多效率越高吗](#23-多线程越多效率越高吗)\n  * [24. 多线程会产生哪些并发问题](#24-多线程会产生哪些并发问题)\n  * [25. dom是什么](#25-dom是什么)\n  * [26. 前端有哪些标签](#26-前端有哪些标签)\n  * [27. 前端input参数如何获取](#27-前端input参数如何获取)\n  * [28. 前端参数传到后端，并获取的流程](#28-前端参数传到后端并获取的流程)\n  * [29. mybatis如何将对象转换成sql](#29-mybatis如何将对象转换成sql)\n  * [30. jvm内存结构](#30-jvm内存结构)\n  * [31. 栈会溢出吗什么时候，方法区会溢出吗](#31-栈会溢出吗什么时候方法区会溢出吗)\n  * [32. jvm如何加载的](#32-jvm如何加载的)\n  * [33. 自己写个String类能加载吗，之前的String是什么时候加载进去的](#33-自己写个string类能加载吗之前的string是什么时候加载进去的)\n  * [34. ThreadLocal为什么要设计key值](#34-threadlocal为什么要设计key值)\n  * [35. 如何理解微服务，什么时候使用微服务](#35-如何理解微服务什么时候使用微服务)\n\n## 9月28美团java后端二面总结\n> 作者：求个实习啊\n链接：https://www.nowcoder.com/discuss/761069?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n### mysql:\n#### 1.从联合索引出发，要求画出联合索引B+树索引图。\n[:rocket::rocket::rocket:](../数据库/MySQL.md#联合索引的底层组织方式)\n#### 2.从mysql事务出发，要求画出RC和RR隔离级别下的MVCC。\n\n#### 3.从mysql update语句出发， 要求说出binlog 与 redolog一致性问题。\n[:rocket::rocket::rocket:](../数据库/MySQL.md#redo-log和binlog一致性问题)\n### java基础:\n#### 4.从可重入锁出发， 要求写出基于可重入锁的阻塞队列，怎么实现。\nlock-condition [:rocket::rocket::rocket:](../场景设计/场景设计.md#java实现blockqueue)\n#### 5.从volatile出发， 要求写出volatile解决可见性的代码。\n[:rocket::rocket::rocket:](../Java-多线程/volatile.md#volatile解决可见性的代码)\n#### 6.从设计模式出发， 要求写出装饰者模式和策略模式代码。\n装饰者模式[:rocket::rocket::rocket:](../设计模式/装饰者模式.md)\n\n策略模式[:rocket::rocket::rocket:](../设计模式/策略模式.md)\n#### 7.HashMap 多线程下，怎么个个不安全法，数据丢失问题。\nJDK1.7 HashMap线程不安全体现在：死循环、数据丢失\n\nJDK1.8 HashMap线程不安全体现在：数据覆盖\n\n[:rocket::rocket::rocket:](../Java-基础/容器-collection.md#map)\n### 分布式方面:\n#### 8.从银行转账出发， 要求描述出两阶段提交基于（RocketMQ）\n//TODO\n### redis相关:\n#### 9.缓存穿透， 缓存击穿， 缓存雪崩基本概念及解决方案。\n[:rocket::rocket::rocket:](../Redis/Redis.md#常问故障场景)\n#### 10.redis哨兵集群相关问题（怎么主从同步，怎么哨兵选举， 怎么master选举）\n[:rocket::rocket::rocket:](../Redis/Redis.md#sentinel哨兵模式)\n\n## 快手 秋招 Java开发 一二三面面经【已意向】\n\n> 作者：julia_\n链接：https://www.nowcoder.com/discuss/752971?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n### 1. 自我介绍\n### 2. 实习项目、背景、需求介绍\n### 3. InnoDB优点\n- 支持事务\n- 支持外键\n- 支持崩溃修复能力和并发控制\n- 支持行锁\n### 4. MyISAM索引底层是什么结构\nb树\n### 5. B树和B+树区别\n[:rocket::rocket::rocket:](../数据库/MySQL.md#b树)\n### 6. 为什么选择B+树不选择B树\n减少IO，更快的查找数据\n### 7. MySQL如何支持事务\n事务的ACID \n- 原子性 undo log\n- 一致性 其他特性保证了一致性\n- 隔离性 锁，mvcc\n- 持久性 redo log\n### 8. undo log如何保证原子性\n[:rocket::rocket::rocket:](../数据库/MySQL.md#undo-log回滚日志)\n### 9. MySQL隔离级别、存在的问题\n[:rocket::rocket::rocket:](../数据库/MySQL.md#acid)\n### 10. MySQL如何解决脏读、不可重复读、幻读\n### 11. 如何解决脏读？（读已提交）MySQL如何判断事务有没有提交？事务A中对id=1进行修改，不提交；事务B中读取id=1的数据，如何判断这个数据有没有被提交？\n### 12. InnoDB可重复读是否存在幻读问题\n### 13. 如果对记录修改，是否会读到修改的值？\n### 14. LeetCode：8. 字符串转换整数\n### 15. HashMap和HashTable区别\n[:rocket::rocket::rocket:](../Java-基础/容器-collection.md#hashtable)\n### 16. synchronized如何实现HashTable线程安全\nsynchronized的语义原理 [:rocket::rocket::rocket:](../Java-多线程/锁机制.md#synchronized)\n### 17. 线程之间如何知道已经有线程在put（Mark word）\n### 18. Mark word是什么\n### 19. synchronized的锁优化\n### 20. 出于目的写博客；什么时间写博客\n### 21. 反问\n### 22. 其他offer\n### 23. 自我介绍\n### 24. 项目问题\n### 25. 实习有什么体感\n### 26. 假设有1,2,3,4,5,6,7,8,9,10 在B+树中存储，是什么样子\n```java\n                1\n            1-5 6-10\n1<->2<->3<->4<->5<->6<->7<->8<->9<->10  \n```\n### 27. 为什么1和2之间是链表\n### 28. MySQL有哪些索引\n[:rocket::rocket::rocket:](../数据库/MySQL.md#索引)\n### 29. 为什么会有覆盖索引\n提高查询效率避免回表\n### 30. table 有a b c d四列，(b c d) 联合索引，selct c,d from table where c = 1会使用这个联合索引吗？不会，最左匹配\n### 31. 为什么覆盖索引存在最左匹配原则\n索引的底层组织形式是以最左的字段开始排序的\n### 32. select c,d from table where b = 1 and d = 2会走索引吗？我：行。面试官：这个可以行，也可以不行…分情况，MySQL中有一些优化，比如ICP，就会将索引下推（我没懂…）\nICP: [:rocket::rocket::rocket:](../数据库/MySQL.md#icpindex-condition-pushdown)\n### 33. 算法题：LeetCode 34. 在排序数组中查找元素的第一个和最后一个位置\n### 34. HashMap底层数据结构是什么\nNode数组\n### 35. HashMap先不考虑红黑树，手写一个底层数据结构，存储key value\n```java\nNode<K,V>(){\n  int hash;  \n  K key;\n  E value;\n  Node<K,V> next;\n  \n  Node(K key,E value,int hash,Node<K,V> next){\n      this.key = key;\n      this.value = value;\n      this.hash = hash;\n      this.next = next;\n  }\n}\nNode<K,V> [] tab;\n\nput(K key,E value){\n    int n = tab.length;\n    int index = hash(key) & (n-1);\n    if(n == 0){\n        扩容\n    }\n    if(tab[index] == null){\n        tab[index] = new Node(key,value,hash,null);\n    }else{\n        if(tab[index].hash == hash){\n            覆盖\n        }else if(tab[index] instenceof TreeNode){\n            红黑树插入\n        }else{\n            while(tab[index].next != null){\n                遍历链表\n        }\n        }\n    }\n    \n}\n```\n### 36. Java 线程的状态；time-waiting时间到了，进入什么；调用sleep()进入什么状态？time_waiting，那这个time_waiting状态会释放锁吗？不会；锁等待是什么状态？blocked\n[:rocket::rocket::rocket:](../Java-多线程/线程.md)\n\ntime-waiting时间到了，进入可运行\n\n调用sleep()进入期限等待\n\n那这个time_waiting状态会释放锁吗？不会\n\n锁等待是什么状态？blocked 阻塞\n### 37. wait() notify() 以及线程状态转换\n[:rocket::rocket::rocket:](../Java-多线程/线程.md)\n### 38. Java线程状态和操作系统线程有什么不同？Java线程的 runable=ready+running，操作系统线程分为 running和 ready，并不是合在一起的\n[:rocket::rocket::rocket:](../Java-多线程/线程.md#java线程和操作系统的线程区别)\n### 39. 为什么Java把这两个状态放在一起？\n而对不同的操作系统，由于本身设计思路不一样，对于线程的设计也存在种种差异，所以JVM在设计上，就已经声明:虚拟机中的线程状态，不反应任何操作系统线程状态\n\n我觉得这个问题可能是问为什么这么设计，可能是为了屏蔽底层的差异\n### 40. 反问\n### 41. 自我介绍\n### 42. JVM内存结构\n[:rocket::rocket::rocket:](../Java-JVM/内存结构.md)\n### 43. 堆如何分代\n### 44. 为什么要分代\n### 45. 回收算法\n[:rocket::rocket::rocket:](../Java-JVM/垃圾回收.md)\n### 46. 回收算法有哪些具体实现？垃圾回收器\n[:rocket::rocket::rocket:](../Java-JVM/垃圾回收.md)\n### 47. TCP三次握手\n[:rocket::rocket::rocket:](../计算机网络/TCP_IP.md#三次握手)\n### 48. TCP 四次挥手\n[:rocket::rocket::rocket:](../计算机网络/TCP_IP.md#四次挥手)\n### 49. 为什么建立三次、断开是四次\n[:rocket::rocket::rocket:](../计算机网络/TCP_IP.md#为什么需要三次握手)\n### 50. 四次挥手套接字的状态转移\n### 51. 输入url的流程\n[:rocket::rocket::rocket:](../计算机网络/HTTP面试题.md#在浏览器中输入url地址显示主页的过程)\n### 52. http的request、response的具体格式\n[:rocket::rocket::rocket:](../计算机网络/HTTP.md#http的request和response格式)\n### 53. 你们的服务是如何部署的？SpringBoot中的Tomcat\n### 54. LRU 如何实现？在哪用过\n### 55. LRU put get 时复\n\n## 招银java一面\n\n> 作者：兽兽今天也在被占用\n链接：https://www.nowcoder.com/discuss/752898?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n### 1.==和equals的区别（自我感觉良好）\n[:rocket::rocket::rocket:](../Java-基础/Object.md#equals)\n### 2.一个类的两个对象怎么进行比较（感到一丝茫然，是问重写equals还是问实现comparable接口啊）\n重写equals和hashcode\n### 3.既然说到hashcode，有没有可能两个对象equals但是hashcode不同（开始懵逼，自然情况下没可能吧？对叭？）\nequals相等hashcode相等\n\nhashcode相等，equals不一定相等\n### 4.如果出现了上述这种情况，有可能发生什么情况？（hashset没法覆盖？）\n[:rocket::rocket::rocket:](../Java-基础/Object.md#重写了equals后为什么要重写hashcode如果不重写会有什么影响)\n### 5.用过多线程吗，怎么实现的多线程？（答了自己用线程池，还可以用其他三种方法创建新线程）\n[:rocket::rocket::rocket:](../Java-多线程/线程.md#使用线程)\n### 6.那线程池的线程具体在什么时候创建一个线程或者销毁一个线程？（痛苦面具）\n[:rocket::rocket::rocket:](../Java-多线程/线程池.md#如何释放线程)\n### 7.你能手动实现一个死锁的情况吗（说思路）\n[:rocket::rocket::rocket:](../Java-多线程/锁机制.md#死锁代码举例)\n### 8.有ABC三个线程，怎么编程让B在C前面执行，A在B前面执行（之前看过这题，说了思路被diss太麻烦但是逻辑可行）\n[:rocket::rocket::rocket:](../Java-多线程/线程.md#怎么让多个线程有序执行)\n### 9.问一下数据结构，你了解哪些二叉树的种类和他们的具体使用场景（已经有点崩溃了，说了搜索和完全，其他就想不起来了，所以面试官开始引导我）\n[:rocket::rocket::rocket:](../数据结构和算法/二叉树.md)\n### 10.AVL树了解吗（宕机了，想不起来应用场景了，就说不太记得了）\n### 11.红黑树了解吗（简单说了一下）\n### 12.红黑树的具体应用场景，举个例子（说了hashmap和1.8的concurrent hashmap）\n### 13.为什么用红黑树不一直用链表\n链表的查询效率较低\n### 14.为什么用红黑树不用普通二叉树（说了普通二叉树会导致一侧树的深度太深）\n- 使用普通二叉树可能会导致一侧的节点太多，深度太深\n- 不使用平衡二叉树是因为红黑树只是要求基本平衡没有强要求，所以在平衡的效率上较高\n\n### 15.普通二叉树深度太深会导致什么？（…）\n查询效率降低\n### 16.B树和B+树知道吗？区别是什么？\n[:rocket::rocket::rocket:](../数据库/MySQL.md#b树)\n### 17.B树和B+树的应用场景说一下（mysql的索引）\n[:rocket::rocket::rocket:](../数据库/MySQL.md#b树)\n### 18.给字段加索引最好怎么加？\n多个使用的字段可以添加联合索引\n### 19.什么情况下使用复合索引更好？\n查询多个字段\n### 20.什么情况下会导致索引失效？（到这里都信心满满）\n[:rocket::rocket::rocket:](../数据库/MySQL.md#b树#mysql-索引失效)\n### 21.为什么使用模糊匹配会失效，你能给我解释一下底层原理吗？（？？？？？？？）\n[:rocket::rocket::rocket:](../数据库/MySQL.md#b树#like索引失效原理)\n### 22.网络协议有了解吗，为什么Tcp是三次握手四次挥手不是四四或者三三？\n[:rocket::rocket::rocket:](../计算机网络/TCP_IP.md#三次握手)\n### 23.平时做项目用http还是https?\n### 24.SSL套接字的过程？（啊？？？？？）\n其实就是问HTTPS的加密过程\n### 25.SSL在历史上有一次心脏流血漏洞，这个漏洞怎么出现的？（啊？？？？？？？？？？orz）\nhttps://zh.wikipedia.org/wiki/%E5%BF%83%E8%84%8F%E5%87%BA%E8%A1%80%E6%BC%8F%E6%B4%9E\n### 26.设计模式用过吗？（说用过工厂模式）\n[:rocket::rocket::rocket:](../设计模式/工厂模式.md)\n### 27.那我们来聊聊单例模式（？？？？？？），单例模式有几种实现方式？（这里有一个地方说错了，说成饿汉是编译时期生成了）\n[:rocket::rocket::rocket:](../设计模式/单例模式.md)\n### 28.你再想想，是编译时期吗？我问下你，你写的代码如何运行，这个过程你说一下（对不起！！！！）\n### 29.为什么双重校验，一次校验不行吗（这题我会！）\n### 30.那怎么用一次校验实现线程安全？（我忘了orz开始胡言乱语，没有自信的问静态内部类可以吗）\n### 31.静态内部类效率也不太好，你能有什么优化方法吗（对不起！！！我真的没用过我不会！！！）\n### 32.再来问问网络安全吧，Sql注入…（慌张打断，说我不了解网络安全，没有学过这方面）\n[:rocket::rocket::rocket:](../计算机网络/网络攻击行为.md)\n### 33.没关系，那接着聊，刚才说的hashmap，hashmap怎么解决hash冲突\n链地址法:[:rocket::rocket::rocket:](../Java-基础/容器-collection.md#map)\n### 34.除了链地址法还有其他的解决hash冲突的方法吗（开放定址和再哈希）\n[:rocket::rocket::rocket:](../场景设计/场景设计.md#解决哈希冲突的方法)\n### 35.如果hashmap溢出了怎么办（建立公共溢出区？）\n### 36.公共溢出区也满了怎么办？（啊…？这我真的盲区了，我说hashmap也会扩容吧…？）\n### 37.说一下hashmap扩容的过程？\n[:rocket::rocket::rocket:](../Java-基础/容器-collection.md#扩容)\n### 38.你对jvm有了解吗？说一下jvm的内存分区？\n[:rocket::rocket::rocket:](../Java-JVM/内存结构.md)\n### 39.堆里面怎么分区的？（这题真不会，只说知道为了方便垃圾回收所以分了新生代区和老年代区，其他的真不知道）\n### 40.没关系，那你知道一个对象怎么从新生代变成老年代吗？（懵逼，对不起，不知道，只简单的知道两个区的定义）\n\n## 深圳今日头条--JAVA后端开发\n\n> 作者：幸运鹅lucky\n链接：https://www.nowcoder.com/discuss/751429?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n### 1. MyISAM 和 InnoDB 比较；\n[:rocket::rocket::rocket:](../数据库/MySQL.md#myisam和innodb的区别总结)\n### 2. mysql都有哪些索引类型；为什么b+树，红黑树、b树为什么不好；\n[:rocket::rocket::rocket:](../数据库/MySQL.md#b树)\n### 3. mysql的主键，唯一索引区别，怎么建索引；\n[:rocket::rocket::rocket:](../数据库/MySQL.md#主键和唯一索引的区别)\n### 4. 一条sql怎么优化？\n- explain\n- 慢查询\n- 索引\n\n[:rocket::rocket::rocket:](../数据库/MySQL.md#sql优化)\n### 5. 数据库的范式？【三大范式】\n[:rocket::rocket::rocket:](../数据库/MySQL.md#数据库三范式)\n### 6. 数据库事务，ACID，mvcc\n[:rocket::rocket::rocket:](../数据库/MySQL.md#事务)\n### 7. mysql怎么实现主从复制？ 【binlog】\n[:rocket::rocket::rocket:](../数据库/MySQL.md#主从复制)\n### 8. redis持久化机制\n[:rocket::rocket::rocket:](../Redis/Redis.md#持久化方式)\n### 9. redis的基础数据类型，以及他们如何实现\n[:rocket::rocket::rocket:](../Redis/Redis.md#数据类型)\n### 10. redis缓存问题-雪崩，击穿\n[:rocket::rocket::rocket:](../Redis/Redis.md#常问故障场景)\n### 11. redis数据一致性问题，如何解决？\n[:rocket::rocket::rocket:](../场景设计/场景设计.md#什么是延迟双删)\n### 12. 谈一谈http，https\n[:rocket::rocket::rocket:](../计算机网络/HTTP.md)\n### 13. tcp怎么实现可靠传输，udp可以可靠传输吗？\ntcp怎么实现可靠传输:[:rocket::rocket::rocket:](../计算机网络/TCP_IP.md#tcp怎么保障可靠传输)\n\nudp可以可靠传输:[:rocket::rocket::rocket:](../计算机网络/TCP_IP.md#如何实现可靠udp传输)\n### 14. stmp，ftp了解吗【我都没看过，三面考的】\n[:rocket::rocket::rocket:](../计算机网络/网络协议分层.md#常见的协议)\n### 15. tcp拥塞控制，滑动窗口\n[:rocket::rocket::rocket:](../计算机网络/TCP_IP.md#tcp怎么保障可靠传输)\n### 16. tcp的sync攻击，为什么三次握手\n[:rocket::rocket::rocket:](../计算机网络/网络攻击行为.md#syn-flood攻击)\n### 17. tcp listen backlog【当时一脸懵，三面考的】\n已完成TCP连接由一个complete connection queue维护，其最大长度为listen函数的参数backlog\n### 18. OSI七层协议\n[:rocket::rocket::rocket:](../计算机网络/网络协议分层.md#osi-7层)\n### 19. 输入URL到页面加载过程\n[:rocket::rocket::rocket:](../计算机网络/HTTP面试题.md#在浏览器中输入url地址显示主页的过程)\n### 20. linux 执行二进制文件过程。。。【三面考的，我当场就裂开了】\n//TODO\n### 21. linux 创建进程啥的【也裂开】\n[:rocket::rocket::rocket:](../Linux/linux.md#创建进程的方式)\n### 22. 内核，用户态，内核态，怎么切换\n[:rocket::rocket::rocket:](../Linux/linux.md#用户态和内核态)\n### 23. 进程线程协程\n[:rocket::rocket::rocket:](../Linux/linux.md#线程进程协程)\n### 24. 进程通信方式，哪种最高效\n[:rocket::rocket::rocket:](../Linux/linux.md#进程间8种通信方式详解)\n### 25. 进程同步方式\n### 26. 谈谈虚拟内存【听到谈谈就麻】\n[:rocket::rocket::rocket:](../Linux/linux.md#linux物理内存和虚拟内存)\n### 27. 谈谈使用过的几种设计模式，以及优缺点【真的太高频了，我每次都被考】\n[:rocket::rocket::rocket:](../设计模式)\n### 28. jvm内存模型，如何分配内存\n[:rocket::rocket::rocket:](../Java-JVM/内存结构.md)\n### 29. 垃圾回收算法\n[:rocket::rocket::rocket:](../Java-JVM/垃圾回收.md)\n### 30. 类加载机制\n[:rocket::rocket::rocket:](../Java-JVM/类加载机制.md)\n### 31. 锁都有哪些，区别\n[:rocket::rocket::rocket:](../Java-多线程/锁机制.md)\n### 32. RPC相关\n[:rocket::rocket::rocket:](../场景设计/场景设计.md#什么是rpc)\n### 33. 消息中间件相关，MQ\n### 34. 多路io复用\n[:rocket::rocket::rocket:](../Linux/linux.md#io模型)\n\n## 深圳转转 Java 一、二面 2021.9.15（已意向书）\n\n> 作者：天川透流\n链接：https://www.nowcoder.com/discuss/746064?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n### 1. 自我介绍\n### 2. 介绍一下项目？\n### 3. Spring的两个核心。说一下\n[:rocket::rocket::rocket:](../Spring/Spring.md)\n### 4. AOP主要用到的Java的哪些技术呢？\n动态代理[:rocket::rocket::rocket:](../Spring/Spring.md#AOP)\n### 5. MySQL的索引有哪些了解？\n[:rocket::rocket::rocket:](../数据库/MySQL.md#索引)\n### 6. 主键索引和普通索引有什么区别？\n主键索引也就是聚集索引，叶子节点包含数据，普通索引叶子节点是主键ID，如果走普通索引会根据叶子节点的主键ID去聚集索引查询数据\n### 7. 事务的隔离级别有哪些？\n- 读未提交\n- 读已提交\n- 可重复读\n- 串行化\n\n[:rocket::rocket::rocket:](../数据库/MySQL.md#事务)\n### 8. 不同的隔离级别解决了哪些问题？\n- 读未提交 \n- 读已提交 解决脏数据\n- 可重复读 解决不可重复读、幻读\n- 串行化 解决幻读\n### 9. 可重复读有没有解决这个幻读的问题？\nmvcc解决幻读\n[:rocket::rocket::rocket:](../数据库/MySQL.md#事务)\n### 10. 可重复读如何解决不可重复读的一个问题？\n### 11. ACID四个特性有了解过吗？\n- 原子性\n- 一致性\n- 隔离性\n- 持久化\n\n[:rocket::rocket::rocket:](../数据库/MySQL.md#事务)\n### 12. 怎么保证这ACID四个特性？\n- 原子性 undo log\n- 一致性 事务，加锁，MVCC ，undo log\n- 隔离性 事务，加锁，MVCC ，undo log\n- 持久化 redo log\n### 13. MVCC有没有起到作用？\n隔离性\n### 14. 介绍一下集合\n[:rocket::rocket::rocket:](../Java-基础/容器-collection.md#list)\n### 15. HashMap是线程安全的还是线程不安全的？\n不安全\n### 16. HashMap线程不安全会出现什么问题？\n- jdk1.7环链\n- 1.8虽然解决了环链，但是本身不是设计为并发，比如put 源码++size\n### 17. 有没有了解过线程安全的HashMap？\nConcurrentHashMap\n[:rocket::rocket::rocket:](../Java-基础/容器-collection.md#concurrenthashmap)\n### 18. ConcurrentHashMap怎么保证线程安全？\nsynchronized+CAS[:rocket::rocket::rocket:](../Java-基础/容器-collection.md#concurrenthashmap)\n### 19. 有没有实际用过多线程的东西？\n线程池执行批量任务\n### 20. 线程的创建有哪几种方式？\n[:rocket::rocket::rocket:](../Java-多线程/线程.md)\n### 21. Thread类和Runable接口的最大区别是什么？\n[:rocket::rocket::rocket:](../Java-多线程/线程.md)\n### 22. 线程池最核心的是哪些？\n[:rocket::rocket::rocket:](../Java-多线程/线程池.md)\n### 23. 线程池的执行顺序是怎么样的呢？\n[:rocket::rocket::rocket:](../Java-多线程/线程池.md)\n### 24. 运行的时候，核心线程数能不能修改？\n[:rocket::rocket::rocket:](../Java-多线程/线程池.md#运行时能修改线程数么)\n### 25. JVM的内存结构？\n[:rocket::rocket::rocket:](../Java-JVM/内存结构.md)\n### 26. 对象在哪个区？\n堆或者栈(JIT优化)\n### 27. Class文件在哪个地方存？\n方法区\n### 28. 垃圾回收会发生在哪几个区域？\n堆、元空间\n### 29. OOM会发生在哪个区域？\n堆、栈、直接内存、方法区\n### 30. 虚拟机栈会不会溢出？\n会\n### 31. GC算法有没有了解过？\n[:rocket::rocket::rocket:](../Java-JVM/内存结构.md#回收算法)\n### 32. 怎么确定一个对象是垃圾？\n[:rocket::rocket::rocket:](../Java-JVM/内存结构.md#判断一个对象能否被回收)\n### 33. 哪些对象可以做为CG Root？\n[:rocket::rocket::rocket:](../Java-JVM/内存结构.md#哪些可以作为是根节点)\n### 34. Java有哪些锁？\n[:rocket::rocket::rocket:](../Java-多线程/锁机制.md)\n### 35. 有没有除了Synchronize和ReentrantLock之外还有没有可以上锁的？\n自旋锁CAS\n### 36. volatile怎么理解？\n[:rocket::rocket::rocket:](../Java-多线程/volatile.md)\n### 37. 类加载器有哪些？\n[:rocket::rocket::rocket:](../Java-JVM/类加载机制.md)\n### 38. 双亲委派模型\n[:rocket::rocket::rocket:](../Java-JVM/类加载机制.md)\n### 39. 怎么打破双亲委派模型？（面试的时候答了不会）（面试官告诉我说Tomcat有）\n[:rocket::rocket::rocket:](../Java-JVM/类加载机制.md#怎么打破双亲委派模型)\n### 40. 类加载的过程，Class文件\n[:rocket::rocket::rocket:](../Java-JVM/类加载机制.md)\n### 41. 有没有自己使用过算法\n### 42. 有哪些排序？\n[:rocket::rocket::rocket:](../数据结构和算法/排序算法.md)\n### 43. 快排的原理。时间复杂度，最坏情况。\n### 44. TCP连接和断开。\n[:rocket::rocket::rocket:](../计算机网络/TCP_IP.md)\n### 45. 三次握手过程\n[:rocket::rocket::rocket:](../计算机网络/TCP_IP.md)\n### 46. TCP连接CloseWait和TimeWait状态\n[:rocket::rocket::rocket:](../计算机网络/TCP_IP.md)\n### 47. 网络的拥塞控制有没有了解过？\n[:rocket::rocket::rocket:](../计算机网络/TCP_IP.md)\n### 48. 输入一个网址的调用流程。\n[:rocket::rocket::rocket:](../计算机网络/HTTP面试题.md)\n### 49. 有没有了解过HTTPS\n[:rocket::rocket::rocket:](../计算机网络/HTTP.md#https)\n### 50. HTTPS的加密算法是哪些？\n[:rocket::rocket::rocket:](../计算机网络/HTTP.md#https)\n### 51. 你感觉你比较擅长哪方面？\n### 52. 有没有刷过题？\n### 53. 你觉得动态规划的关键是什么？\n### 54. 反问环节。\n### 55. 数据库索引\n[:rocket::rocket::rocket:](../数据库/MySQL.md#索引)\n### 56. 单核情况下，多线程为什么会比单线程的情况下去使用情况会更好？（面试官提醒后，会了）\n单核-多线程并不会加快处理速度，反而因为上下文切换速度会更慢\n### 57. TCP协议了解吗？\n[:rocket::rocket::rocket:](../计算机网络/TCP_IP.md)\n### 58. HTTP长连接还是短连接？\n[:rocket::rocket::rocket:](../计算机网络/TCP_IP.md#http长连接还是短连接)\n### 59. 服务端主动发起关闭还是客户端主动发起关闭TCP？\n[:rocket::rocket::rocket:](../计算机网络/TCP_IP.md#四次挥手)\n### 60. 心跳机制说一下？（几乎没答对）\n### 61. 单例模式说一下。\n[:rocket::rocket::rocket:](../设计模式/单例模式.md)\n### 62. 线程安全的单例模式是怎么样的？\n### 63. 那为什么要使用volatile呢？\n### 64. 为什么要使用双段锁呢？\n### 65. 算法题：8个人乒乓球比赛，A赢B，B赢C，可以默认A赢C。那么最少比赛多少次可以获得冠亚季军（当时没想出来，心态崩了。）\n### 66. 你怎么学习技术？\n### 67. 你觉得什么最重要？\n### 68. 你在团队开发的项目里面学到了什么？\n### 69. 反问。\n\n\n\n## 顺丰二面 20210915\n\n> 作者：java小白进化\n链接：https://www.nowcoder.com/discuss/745452?type=2&order=3&pos=3&page=0&source_id=discuss_center_2_nctrack&channel=1009&ncTraceId=a90e472cf19c4bcb830627a78fb06c69.135.16317017484279121\n来源：牛客网\n\n### 1. ThreadLocal的底层原理以及其应用\n[:rocket::rocket::rocket:](../Java-多线程/ThreadLocal.md)\n### 2. Mybatis框架了解哪些\n[:rocket::rocket::rocket:](../Mybatis/mybatis.md)\n### 3. Mybatis中的#和$的区别\n[:rocket::rocket::rocket:](../Mybatis/mybatis.md#mybatis中和的区别)\n### 4. Mybatis执行一条语句的底层原理，如何对他对他进行一些优化排序\n### 5. 熟悉哪些设计模式\n[:rocket::rocket::rocket:](../设计模式)\n### 6. hashmap的底层结构，put和get的整个的执行过程\n[:rocket::rocket::rocket:](../Java-基础/容器-collection.md#map)\n### 7. 如何在分布式的情况下实现事务一致性\n[:rocket::rocket::rocket:](../分布式相关/分布式事务.md)\n### 8. 类的排序根据某个字段\ncompare?\n### 9. 分布式情况下如何保证数据一致性\n[:rocket::rocket::rocket:](../分布式相关/一致性算法.md)\n### 10. synchronized实际中的使用，底层原理\n[:rocket::rocket::rocket:](../Java-多线程/锁机制.md#synchronized)\n### 11. 缓存穿透、击穿、雪崩\n[:rocket::rocket::rocket:](../Redis/Redis.md#常问故障场景)\n\n\n## 携程Java面经20210913\n\n> 作者：擅长丢人的seki同学\n链接：https://www.nowcoder.com/discuss/742356?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n### 1. 自我介绍\n### 2. 聊两个项目\n### 3. MySQL事务隔离机制，如何实现\n[:rocket::rocket::rocket:](../数据库/MySQL.md#acid)\n### 4. Spring如何避免循环依赖\n[:rocket::rocket::rocket:](../Spring/Spring.md#循环依赖问题)\n### 5. 线程的创建方式\n[:rocket::rocket::rocket:](../Java-多线程/线程.md#使用线程)\n### 6. 线程池参数\n[:rocket::rocket::rocket:](../Java-多线程/线程池.md#参数含义)\n### 7. 当所有线程处理完毕打印一条日志怎么做\n- countDownLatch\n- 阻塞队列\n### 8. 两个千万级别的int数组，输出交集，说一下你认为时间复杂度最低的方案\n\n### 9. 反问：表现和建议。建议我应该多做项目，很多东西书里没有……但是我工作一年多，简历四个项目啊？\n\n\n\n## 网易云音乐Java开发岗秋招8.30一二面面经\n> 作者：windwj000\n链接：https://www.nowcoder.com/discuss/738717?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n### 1. 描述一下项目的技术架构\n### 2. 有没有了解过阿里云的分布式数据库?单点数据库的话能够支撑目前项目的数据吗?有了解阿里云的分库分表技术吗?\n### 3. ES如何使用?为何ES查询会变快?目前用的哪款分词器?ES和数据库数据不一致该如何处理?\n[:rocket::rocket::rocket:](../数据库/Elasticsearch.md)\n\n数据不一致？es是做缓存么？如果是 参考缓存和数据库不一致问题[:rocket::rocket::rocket:](../场景设计/场景设计.md#什么是延迟双删)\n### 4. DDD是如何实践的，起到了什么作用?\n[:rocket::rocket::rocket:](../场景设计/场景设计.md#什么是ddd)\n### 5. 讲一下对索引的理解\n索引主要是提高查找效率的\n### 6. innoDB和MyISAM的区别\n[:rocket::rocket::rocket:](../数据库/MySQL.md#储存引擎-1)\n### 7. b+树的结构，叶子节点设计成有序链表的好处\n页加载，提高io效率\n### 8. 描述下双向链表的数据结构\n```java\nclass Node{\n  var data;\n  Node pre;\n  Node next;\n}\n```\n### 9. Explain进行查询性能优化，需要关注哪些字段?\n[:rocket::rocket::rocket:](../数据库/MySQL.md#explain)\n### 10. 举一个Redis的使用场景\n[:rocket::rocket::rocket:](../Redis/Redis.md#常见使用场景)\n### 11. 跳表是如何实现的?\n[:rocket::rocket::rocket:](../Redis/Redis.md#ziplist压缩列表)\n### 12. Redis的基本数据类型\n[:rocket::rocket::rocket:](../Redis/Redis.md#数据类型)\n### 13. Redis的数据淘汰策略，Iru如何实现?\n[:rocket::rocket::rocket:](../Redis/Redis.md#内存回收策略)\n### 14. Rocket事务消息的实现机制\n[:rocket::rocket::rocket:](../消息队列/RocketMQ.md#rocket的事务实现机制)\n### 15. RocketMQ和Kafka的区别\n### 16. 你对什么技术有沉淀或者学习收获?\n### 17. 你有什么擅长点没有被问到，讲一下\n### 18. 你这个数据中台好像没什么用啊，客户不能自己做吗?功能好像不是很充分啊?\n### 19. 对接不同客户有哪些通用的模块?\n### 20. 你觉得项目业务复杂在什么地方?为什么客户会选择你们来做?  \n### 21. 用什么进行存储?\n### 22. MySQL联合索引的结构，在b+树上体现?\n[:rocket::rocket::rocket:](../数据库/MySQL.md#联合索引的树存储结构)\n### 23. 进程间的通信方式\n[:rocket::rocket::rocket:](../Linux/linux.md#进程间8种通信方式详解)\n### 24. Kafka的原理\n[:rocket::rocket::rocket:](../消息队列/Kafka.md)\n### 25. Kafka的消息丢失问题，考虑主从同步\n### 26. 你有什么方面擅长\n### 27. Redis集群的部署方式\n[:rocket::rocket::rocket:](../Redis/Redis.md#集群)\n### 28. Redis的基本数据类型\n[:rocket::rocket::rocket:](../Redis/Redis.md#数据类型)\n### 29. JVM新生代和老年代垃圾回收的方式\n[:rocket::rocket::rocket:](../Java-JVM/垃圾回收.md#gc定义)\n\n\n\n## 蚂蚁金服一面20210909-被打得面目全非\n>作者：peonyX\n链接：https://www.nowcoder.com/discuss/736939?type=2&channel=-1&source_id=discuss_terminal_discuss_hot_nctrack\n来源：牛客网\n\n### 1.redis为什么快？\n[:rocket::rocket::rocket:](../Redis/Redis.md#redis为什么这么快)\n### 2.epoll你知道吗\n[:rocket::rocket::rocket:](../Linux/linux.md#selectpollepoll)\n### 3.nio你知道多少？和bio区别，零拷贝，netty\nnio:[:rocket::rocket::rocket:](../Java-基础/JavaIO.md#nio)\n\nnetty:[:rocket::rocket::rocket:](../Netty/netty.md)\n### 4.你知道有nio哪些组件吗？selector，buffer，channel\nnio:[:rocket::rocket::rocket:](../Java-基础/JavaIO.md#nio)\n### 5.io和nio区别是什么？io是面向流的，nio是面向什么的？\nnio:[:rocket::rocket::rocket:](../Java-基础/JavaIO.md#nio)\n### 6.redis和db一致性怎么设计\nredis使用的是hash槽，hash一致性算法[:rocket::rocket::rocket:](../Redis/Redis.md#数据分区方式)\n### 7.如果订阅binlog，会有什么问题？\n造成主库的压力增大？\n### 8.如果延时双删，删除失败了怎么办，阻塞放入队列里面吗，那用户岂不是得一直等着？\n[:rocket::rocket::rocket:](../场景设计/场景设计.md#什么是延迟双删)\n### 9.如果先更新数据库，再删除缓存，删除缓存失败了怎么办？\n### 10.缓存热点会造成什么问题？\n缓存雪崩，如果同一时间缓存都失效了，请求全打在了数据库上\n\n[:rocket::rocket::rocket:](../Redis/Redis.md#常问故障场景)\n### 11.redis的内存淘汰策略你知道哪些？\n[:rocket::rocket::rocket:](../Redis/Redis.md#内存回收策略)\n### 12.redis数据结构？\n[:rocket::rocket::rocket:](../Redis/Redis.md#数据类型)\n### 13.threadLocal知道吗，如果不remove，出问题了怎么补救？\nthreadLocal和thread是绑定的，生命周期相同，那么，kill掉这个线程可以释放ThreadLocalMap\n### 14.countDownLatch和semaphore是什么，怎么用\n[:rocket::rocket::rocket:](../Java-多线程/AQS.md#同步工具类)\n### 15.Java线程池有哪些，线程池数量怎么配\n[:rocket::rocket::rocket:](../Java-多线程/线程池.md)\n### 16.多线程需要从哪些方面考虑\n- 资源是否充足\n- 业务处理时间是否很长\n- 线程死锁\n### 17.spi知道吗？\n[:rocket::rocket::rocket:](../场景设计/场景设计.md#什么是spi)\n### 18.怎么定义一个注解？\n[:rocket::rocket::rocket:](../Spring/Spring.md#怎么定义一个注解)\n### 19.怎么提高查询db性能\n- 服务器方面 + 内存（[狗头]）\n- sql 优化\n- 索引优化\n\n[:rocket::rocket::rocket:](../数据库/MySQL.md)\n### 20.db容灾有哪几种？\n- 主从分离\n- 读写分离\n### 21.十亿用户，怎么设计db？\n- 按业务主键分表，比如用户ID\n- 如果查手机号 可为 用户ID + 手机号单独设计存储表\n### 22.什么时候fullgc\n[:rocket::rocket::rocket:](../Java-JVM/垃圾回收.md#GC定义)\n### 23.订单延时取消怎么做？\n[:rocket::rocket::rocket:](../商城类问题/商城类问题.md#订单延时取消怎么做)\n### 设计一个内部的rpc接口，你觉得需要从哪些方面考虑？\n[:rocket::rocket::rocket:](../场景设计/场景设计.md#一个优秀的RPC框架需要考虑的问题)\n\n## 转转-Java后端开发一、二面+hr面\n> 作者：Todo_\n链接：https://www.nowcoder.com/discuss/732376?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n### 1. 项目的超卖如何解决的？\n[:rocket::rocket::rocket:](../商城类问题/商城类问题.md)\n### 2. 排序算法有哪些？\n[:rocket::rocket::rocket:](../数据结构和算法/排序算法.md)\n### 3. 堆排序的时间复杂度？说说堆排序的排序过程，是怎么得到这个时间复杂度的？\n- n log n \n- 每个堆有排序，整体有排序 \n### 4. Object类中有哪些方法？\n[:rocket::rocket::rocket:](../Java-基础/Object.md)\n### 5. wait()和sleep()的区别？\n- wait是等待锁对象唤醒 如果加了时间，是等待时间后解除等待 是object的方法\n- sleep 是阻塞有限时间后自动解除\n### 6. 线程池的参数和作用？线程池的执行流程/原理？\n[:rocket::rocket::rocket:](../Java-多线程/线程池.md)\n### 7. JVM内存模型，垃圾回收算法？\n内存结构:[:rocket::rocket::rocket:](../Java-JVM/内存结构.md)\n\n垃圾回收:[:rocket::rocket::rocket:](../Java-JVM/垃圾回收.md)\n### 8. HashMap的容量为什么要初始化为2的n次幂？\n[:rocket::rocket::rocket:](../Java-基础/容器-collection.md#hashmap)\n### 9. HashMap和ConcurrentHashMap的区别？\n[:rocket::rocket::rocket:](../Java-基础/容器-collection.md#hashmap)\n### 10. ConcurrentHashMap的扩容过程，源码有没有看过？\n[:rocket::rocket::rocket:](../Java-基础/容器-collection.md#ConcurrentHashMap)\n### 11. 说说你对Synchronized的理解，底层原理？\n[:rocket::rocket::rocket:](../Java-多线程/锁机制.md#synchronized)\n### 12. 除了Synchronized，还知道Java中的其他锁吗？\n[:rocket::rocket::rocket:](../Java-多线程/锁机制.md)\n### 13. 说说你对Lock/ReentraLock的理解？有没有看过源码？\n[:rocket::rocket::rocket:](../Java-多线程/锁机制.md#ReentrantLock)\n### 14. 说说你对MySQL索引的理解，什么是聚簇索引和非聚簇索引？\n[:rocket::rocket::rocket:](../数据库/MySQL.md#索引)\n### 15. 如何根据索引查找数据的，索引执行的流程/原理？\n[:rocket::rocket::rocket:](../数据库/MySQL.md#索引)\n### 16. MySQL的事务隔离级别？\n[:rocket::rocket::rocket:](../数据库/MySQL.md#事务)\n### 17. MySQL是如何解决幻读的？\n[:rocket::rocket::rocket:](../数据库/MySQL.md#事务)\n### 18. Redis有哪些了解，基本数据类型有哪些，底层实现知道吗？\n[:rocket::rocket::rocket:](../Redis/Redis.md)\n### 19. Redis的缓存淘汰策略、持久化机制说一下？\n内存回收策略:[:rocket::rocket::rocket:](../Redis/Redis.md#内存回收策略)\n\n持久化机制:[:rocket::rocket::rocket:](../Redis/Redis.md#持久化方式)\n### 20. 项目如何限流？\n持久化机制:[:rocket::rocket::rocket:](../场景设计/场景设计.md#如何解决这些问题)\n### 21. 添加购物车时，数据库层面是如何操作的？\n### 22. 知道接口的幂等性和非幂等性吗？\n持久化机制:[:rocket::rocket::rocket:](../场景设计/场景设计.md#如何保证接口的幂等性)\n### 23. 项目里面有没有考虑幂等性？\n### 24. 自定义线程池需要关注的参数有哪些？\n持久化机制:[:rocket::rocket::rocket:](../Java-多线程/线程池.md#参数含义)\n### 25. 线程池的运行原理？\n持久化机制:[:rocket::rocket::rocket:](../Java-多线程/线程池.md)\n### 26. 阻塞队列满了以后，新进来的线程是执行队列头部的任务还是队尾的任务？\n- 头\n### 27. 如果阻塞队列满了以后，系统重启/宕机，需要考虑什么情况？如何做？\n- 需要保存任务状态 [:rocket::rocket::rocket:](../Java-多线程/线程池.md)\n### 28. synchronized锁的底层原理？\n[:rocket::rocket::rocket:](../Java-多线程/锁机制.md#synchronized)\n### 29. synchronized锁和lock锁的区别\n[:rocket::rocket::rocket:](../Java-多线程/锁机制.md#synchronized锁和lock锁的区别)\n\n---\n\n## 渤海银行提前批面试9.6\n> 作者：牛客844244425号\n链接：https://www.nowcoder.com/discuss/733148?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n### 1.多线程中run()和start()方法的区别？\n- start() 方法则是 Thread 类的方法，用来异步启动一个线程，然后主线程立刻返回。该启动的线程不会马上运行，会放到等待队列中等待 CPU 调度，只有线程真正被 CPU 调度时才会调用 run() 方法执行。\n- run方法是线程实际运行的方法\n\n### 2.synchronized和lock的区别？\n- 区别在与实现机制不同，synchronized是使用指令实现，lock是主要用CAS实现\n\n[:rocket::rocket::rocket:](../Java-多线程/锁机制.md)\n\n### 3.final fianaly finanize\n\n[:rocket::rocket::rocket:](../Java-基础/Java关键字.md#final fianaly finanize)\n\n### 4.数据库三范式\n- 第一范式：要求有主键，并且要求每一个字段原子性不可再分\n- 第二范式：要求所有非主键字段完全依赖主键，不能产生部分依赖\n  - 比如一个表有 学生编号、学生姓名、教师编号、教师姓名，但是学生姓名却没有依赖教师编号\n- 第三范式：所有非主键字段和主键字段之间不能产生传递依赖\n  - 上一个例子教师姓名依赖于教师编号\n\n### 5.索引\n[:rocket::rocket::rocket:](../数据库/MySQL.md#索引)\n\n### 6.如何打破死锁\n[:rocket::rocket::rocket:](../Java-多线程/锁机制.md#死锁)\n### 7.框架用过啥？\n- spring\n- orm：mybatis\n- springcloud\n### 8.配置文件啥类型？\n- xml\n- yml\n- json\n- porperties\n### 9.持久层框架 mybatis\n[:rocket::rocket::rocket:](../Mybatis/mybatis.md)\n### 10.mybatis特性？\n[:rocket::rocket::rocket:](../Mybatis/mybatis.md)\n### 11.linux文件的属性怎么看？属主、属组和其他用户。\n在 Linux 中我们可以使用 ll 或者 ls –l 命令来显示一个文件的属性以及文件所属的用户和组\n```shell\n[root@www /]# ls -l\ntotal 64\ndr-xr-xr-x   2 root root 4096 Dec 14  2012 bin\ndr-xr-xr-x   4 root root 4096 Apr 19  2012 boot\n```\n--- \n\n## 阿里9/2 Java后端 57min\n> 作者：蛋蛋超人。\n> 链接：https://www.nowcoder.com/discuss/729256?source_id=discuss_experience_nctrack&channel=-1\n> 来源：牛客网\n\n#### 1. tcp和udp区别\n[:rocket::rocket::rocket:](../计算机网络/TCP_IP.md#udp-和-tcp-的特点)\n#### 2. TCP/IP协议涉及哪几层架构\n- TCP/UDP 传输层 ; IP 网络层\n[:rocket::rocket::rocket:](../计算机网络/网络协议分层.md)\n#### 3. 4次挥手为什么是4次\n[:rocket::rocket::rocket:](../计算机网络/TCP_IP.md#四次挥手)\n#### 4. 为什么要4次挥手\n#### 5. 学生表和成绩表sql选出没考试的学生\n- 因为不知道表结构，想象写的，应以实际为准\n```sql\nselect stno from stu  \n    where stno not in \n        (select stno from gra group by stno having sum(\"分数\") = 0)\n```\n#### 6. sql选出参加2次考试的学生\n- 因为不知道表结构，想象写的，应以实际为准\n- 应该是成绩表的每科成绩有两个吧\n```sql\nselect stno from gra group by stno,\"科目\" having count(*) = 2\n```\n#### 7. 计算机插上电源操作系统做了什么\n1. BIOS自检后，启动磁盘上的活动分区的引导记录，分区引导记录读取执行操作系统的系统文件，比如Windows，\n2. Windows开始初始化一些重要的系统数据，然后进行DOS部分和GUI部分的引导和初始化工作\n3. 完成后跳出登录界面，允许用户交互\n#### 8. 操作系统设备文件有哪些\n- Linux 中的设备有2种类型\n  - `字符设备(无缓冲且只能顺序存取)` 字符设备是指每次与系统传输1个字符的设备。这些设备节点通常为传真、虚拟终端和串口调制解调器之类设备提供流通信服务，它通常不支持随机存取数据。 字符设备在实现时，大多不使用缓存器。系统直接从设备读取／写入每一个字符。\n  - `块设备(有缓冲且可以随机存取))` 块设备是指与系统间用块的方式移动数据的设备。这些设备节点通常代表可寻址设备，如硬盘、CD-ROM和内存区域。\n- 在基于Linux的系统中，设备节点一般在/dev下，通常使用如下的前缀\n  - fb：frame缓冲\n  - fd：软盘\n  - hd：IDE硬盘或光驱\n  - lp：打印机\n  - par：并口\n  - pt：伪终端\n  - s：SCSI设备\n    - scd：SCSI音频光驱\n    - sd：SCSI硬盘\n    - sg：SCSI通用设备\n    - sr：SCSI数据光驱\n    - st：SCSI磁带\n  - tty：终端\n    - ttyS：串口\n#### 9. 多线程同步有哪些方法\n- synchronized\n  - 加锁\n- volatile\n  - 变量对多个线程可见\n- lock\n  - 加锁\n- ThreadLocal\n  - 每个线程都有自己的私有数据互不干扰\n- 阻塞队列 如：LinkedBlockingQueue\n  - 在队列里排队\n- 使用原子变量实现线程同步如：AtomicInteger\n  - CAS解决多线程竞争问题\n#### 10. 两个方法加 synchronized，一个线程进去sleep，另一个线程可以进入到另一个方法吗\n- 问的是synchronized锁方法、锁方法块、锁类的区别\n  - `锁方法`是锁住对象，多个对象不干扰\n  - `锁方法块`是锁住加锁的对象 synchronized(obj){} 锁住是obj\n  - `锁类`是锁住类，那么类的多个实例也是会同步的\n- 所以答案是：如果是同一个对象，是进不去的，不同对象实例是ok的\n\n[:rocket::rocket::rocket:](../Java-多线程/锁机制.md)\n#### 11. 如何让可重入变成不可重入\n- Synchronize和ReentrantLock都是可重入锁，表示如果是当前线程再次进入获取锁时，只是计数+1，退出时计数-1，既拥有锁的对象可再进入。\n- 如果变为不可重入可考虑，在tryLock的时候不判断是否为当前线程，状态变量计数也只有0和1，尝试获取锁时直接让CAS，成功置为1，退出置为0，即直接让线程参与竞争\n#### 12. 创建线程的三个方法分别什么时候使用\n\n[:rocket::rocket::rocket:](../Java-多线程/线程.md#使用线程)\n#### 13. 怎么获取线程的返回值\n[:rocket::rocket::rocket:](../Java-多线程/线程.md#Callable如何返回值的)\n#### 14. 线程池怎么创建\n[:rocket::rocket::rocket:](../Java-多线程/线程池.md#创建线程池的方式)\n#### 15. 线程池参数如何设计\n[:rocket::rocket::rocket:](../Java-多线程/线程池.md#参数含义)\n#### 16. 拒绝策略有哪些\n[:rocket::rocket::rocket:](../Java-多线程/线程池.md#handler)\n#### 17. 如何设计线程数量\n[:rocket::rocket::rocket:](../Java-多线程/线程池.md#如何设置线程数)\n#### 18. 5个任务，4个最大线程数，线程池里面同时运行几个任务\n4\n#### 19. 给用户发消息任务超出队列，你用哪个拒绝策略\n- DiscardOldestPolicy抛弃头部最早任务，适用于消息更新\n- 重写方案\n  - `executor.getQueue().offer(r, 60, TimeUnit.SECONDS);`再等待一段时间\n  - `final Thread t = new Thread(r, \"Temporary task executor\"); t.start();`新建线程去处理\n  - `executor.getQueue().put(r);`put阻塞提交\n#### 20. 有其他方法吗\n#### 21. JMM\n[:rocket::rocket::rocket:](../Java-多线程/volatile.md#JMM)\n#### 22. 什么时候用多线程、为什么要设计多线程\n提交任务处理速度\n#### 23. 多线程越多效率越高吗\n不是，过多线程会严重影响CPU切换上下文的速度，使速度更慢\n#### 24. 多线程会产生哪些并发问题\n- 超卖，与预期结果不符（原子性）\n- 当读操作和写操作在不同的线程中执行时，我们无法确保执行读操作的线程能实时看到其他线程写入的值。（可见性）\n- 多线程（有序性）\n  - 指令重排在单线程环境下是安全的，在多线程环境下就可能出现问题。比如：\n  ```java\n   线程A:\n   s=new String(\"sssss\");//指令1\n   flag=false;//指令2\n  \n   线程B:\n   while(flag){\n      doSome();\n   }\n   s.toUpperCase();//指令3\n   ```\n  如果线程A顺序执行，即执行指令1，再执行指令2，线程B的执行不会出现问题。指令重排后，假如线程A先执行指令2，\n  这是flag=true，切换到线程2，终止循环，执行指令3，由于s对象尚未创建就会出现空指针异常。\n\n#### 25. dom是什么\n#### 26. 前端有哪些标签\n#### 27. 前端input参数如何获取\n#### 28. 前端参数传到后端，并获取的流程\n小的request.getParam 大了可以回答springmvc的流程\n[:rocket::rocket::rocket:](../Spring/SpringMVC.md)\n#### 29. mybatis如何将对象转换成sql\n[:rocket::rocket::rocket:](../Mybatis/mybatis.md#mybatis封装参数执行SQL)\n#### 30. jvm内存结构\n[:rocket::rocket::rocket:](../Java-JVM/内存结构.md)\n#### 31. 栈会溢出吗什么时候，方法区会溢出吗\n- 达到最大栈深度栈会溢出\n- 方法区也会溢出，jdk1.7方法区也叫永久代，永久代在逻辑上和堆内存是分开的，物理上是连续的，永久代的GC和老年代是捆绑在一起的，所以无论谁满了都会触发full GC,永久代的参数`-XX:PermSize`和`-XX：MaxPermSize`,\n  jdk1.8把永久代移除，取代的是元空间，元空间不再和堆连续，而是储存与本地内存，本地内存是供JVM进程使用的，所以也是会有上限的，也会发生溢出\n#### 32. jvm如何加载的\n[:rocket::rocket::rocket:](../Java-JVM/JVM的启动过程.md)\n#### 33. 自己写个String类能加载吗，之前的String是什么时候加载进去的\n- 自己写的也能加载，但是默认是双亲委派，会交给父类加载\n[:rocket::rocket::rocket:](../Java-JVM/类加载机制.md#双亲委派模型)\n#### 34. ThreadLocal为什么要设计key值\nThreadLocal其实内部封装了ThreadLocalMap，ThreadLocalMap是存储数据的真正结构，ThreadLocalMap类似于map，可以用k-v键值对来保存数据，key是线程对象\n#### 35. 如何理解微服务，什么时候使用微服务\n微服务的对立就是单体服务，我们可以来先看下单体服务的特点：\n- 系统间以API方式访问，耦合很高\n- 各领域之间采用相同的技术栈，难以快速应用新技术\n- 对系统的任何修改都必须整个系统一起部署，运维成本高\n- 系统负载增加时，难以水平扩展\n- 当系统某一处出现问题，会影响整个系统\n\n为了解决这些问题，微服务架构应运而生，微服务是将一个大的服务拆分为多个小的服务，比如一个商城服务，可以拆为，鉴权服务、查询服务、登录服务、用户信息服务、订单服务、商品信息服务、支付服务等等；\n\n**微服务的主要特点**：\n- 单一职责 每个服务只提供一种服务\n- 自治 一个微服务是一个独立的实体，它自己独立部署升级，对其他微服务不影响（理想情况）\n- 可扩展、高可用  可以水平扩展，多个服务示例来达成高可用\n- 灵活组合 多个服务之间可以组合，功能重用，比如鉴权服务可以供所有系统需要鉴权的场景使用\n- 技术异构 不同的功能有不同的特点，使用的技术可以不相同\n\n**缺点：**\n- 复杂度高\n- 运维复杂\n\n---"
  },
  {
    "path": "面试解答/面试解答2021-10.md",
    "content": "# 点击[:rocket::rocket::rocket:]可以看到知识点在哪\n\n---\n\n* [面试复盘 | 字节番茄小说二面(已通过)](#面试复盘--字节番茄小说二面已通过)\n  * [1. 自我介绍](#1-自我介绍)\n  * [2. 项目相关](#2-项目相关)\n  * [3. Java中有个String类，如果我们自己写一个java.lang.String类，会出现问题吗](#3-java中有个string类如果我们自己写一个javalangstring类会出现问题吗)\n  * [4. Java里的Map有哪几种实现](#4-java里的map有哪几种实现)\n  * [5. TreeMap的使用场景，底层数据结构，红黑树的存取复杂度](#5-treemap的使用场景底层数据结构红黑树的存取复杂度)\n  * [6. LinkedHashMap的使用场景](#6-linkedhashmap的使用场景)\n  * [7. Java中有个volatile关键字用过吗，用volatile修饰的变量来记录访问次数，需要其他同步操作吗](#7-java中有个volatile关键字用过吗用volatile修饰的变量来记录访问次数需要其他同步操作吗)\n  * [8. Java有哪些同步方案，如果不加锁呢，加锁会不会太重了](#8-java有哪些同步方案如果不加锁呢加锁会不会太重了)\n  * [9. CAS的ABA问题是什么，要怎么解决](#9-cas的aba问题是什么要怎么解决)\n  * [10. 有没有用过工具，怎么查看Java堆的统计信息](#10-有没有用过工具怎么查看java堆的统计信息)\n  * [11. 我们来问问网络吧，DNS是哪层的协议](#11-我们来问问网络吧dns是哪层的协议)\n  * [12. tcp有个状态是Time_Wait，这个具体是在哪儿，作用是什么](#12-tcp有个状态是time_wait这个具体是在哪儿作用是什么)\n  * [13. get和post的区别，用get和post传输的时候有个编码，那个编码的作用是什么](#13-get和post的区别用get和post传输的时候有个编码那个编码的作用是什么)\n  * [14. 有没有面过别的公司 | 哪儿的人](#14-有没有面过别的公司--哪儿的人)\n  * [15. 给了个C++的结构体，有char、int等3个属性，问占多少内存，换了个顺序，又问占多少内存](#15-给了个c的结构体有charint等3个属性问占多少内存换了个顺序又问占多少内存)\n  * [16. 算法题：](#16-算法题)\n  * [17. 做完题开始问数据库相关的，聚集索引和非聚集索引](#17-做完题开始问数据库相关的聚集索引和非聚集索引)\n  * [18. 事务隔离级别](#18-事务隔离级别)\n  * [19. 怎么实现的可重复读](#19-怎么实现的可重复读)\n  * [20. 反问](#20-反问)\n* [面试复盘 | 字节番茄小说一面(已通过)](#面试复盘--字节番茄小说一面已通过)\n  * [1. 自我介绍](#1-自我介绍-1)\n  * [2. 项目相关](#2-项目相关-1)\n  * [3. Java中常用的集合类](#3-java中常用的集合类)\n  * [4. 有一组数据，需要按照顺序对它进行加密，如果用集合做的话你会选哪个集合？比如一个数组，其元素本身无序，每个元素是字母或者数字都行，对它从小到大进行排序，你会选哪个集合去存储？](#4-有一组数据需要按照顺序对它进行加密如果用集合做的话你会选哪个集合比如一个数组其元素本身无序每个元素是字母或者数字都行对它从小到大进行排序你会选哪个集合去存储)\n  * [5. Java中线程同步的方案有哪些](#5-java中线程同步的方案有哪些)\n  * [6. Lock接口的实现类](#6-lock接口的实现类)\n  * [7. MySQL查询比较慢的话，通过什么方式来优化](#7-mysql查询比较慢的话通过什么方式来优化)\n  * [8. http协议](#8-http协议)\n  * [9. 算法题：lc726 原子的数量](#9-算法题lc726-原子的数量)\n  * [10. 反问](#10-反问)\n\n\n---\n\n# 面试复盘 | 字节番茄小说二面(已通过)\n\n> 作者：Yyyilia\n链接：https://www.nowcoder.com/discuss/766918?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n### 1. 自我介绍\n### 2. 项目相关\n### 3. Java中有个String类，如果我们自己写一个java.lang.String类，会出现问题吗\n(参考《深入理解Java虚拟机》第7章 虚拟机类加载机制，主要讲了类加载器的种类和双亲委派模型)\n\n[:rocket::rocket::rocket:](../Java-JVM/类加载机制.md#双亲委派模型)\n### 4. Java里的Map有哪几种实现\n(HashMap | TreeMap | HashTable | LinkedHashMap | ConcurrentHashMap)\n\n[:rocket::rocket::rocket:](../Java-基础/容器-collection.md#map)\n### 5. TreeMap的使用场景，底层数据结构，红黑树的存取复杂度\n[:rocket::rocket::rocket:](../Java-基础/容器-collection.md#treemap)\n### 6. LinkedHashMap的使用场景\n(用LinkedHashMap实现lru，按插入顺序(默认) accessOrder = false | 按访问顺序 accessOrder = true，HashMap+双向链表)\n### 7. Java中有个volatile关键字用过吗，用volatile修饰的变量来记录访问次数，需要其他同步操作吗\n(场景：单例模式等，参考《深入理解Java虚拟机》第12章12.3.3“对于volatile型变量的特殊规则”，从Java内存模型JMM的角度讲volatile，说明指令中的lock前缀和将esp寄存器的值+0这个空操作)\n\n所以用volatile修饰的变量来记录访问次数，需要同步操作，因为volatile不能保证原子性类似i++是不安全的\n\n[:rocket::rocket::rocket:](../Java-多线程/volatile.md)\n### 8. Java有哪些同步方案，如果不加锁呢，加锁会不会太重了\n(参考《深入理解Java虚拟机》第13章第2节，从“阻塞同步方案 | 非阻塞同步方案 | 无同步方案”三部分说明，面试官提到加锁会不会太重的时候，我提了一下synchronized锁升级)\n\n- synchronized\n- lock\n- CAS\n\n[:rocket::rocket::rocket:](../Java-多线程/锁机制.md)\n### 9. CAS的ABA问题是什么，要怎么解决\n(参考《深入理解Java虚拟机》P477)\n\n可以使用版本控制\n### 10. 有没有用过工具，怎么查看Java堆的统计信息\n(参考《深入理解Java虚拟机》第4章，没用过工具，只说了常用了JVM参数)\n\n- jstack\n- jmap\n- jconsole\n- jvisualvm\n- arthas\n### 11. 我们来问问网络吧，DNS是哪层的协议\n应用层\n### 12. tcp有个状态是Time_Wait，这个具体是在哪儿，作用是什么\n[:rocket::rocket::rocket:](../计算机网络/TCP_IP.md#四次挥手)\n### 13. get和post的区别，用get和post传输的时候有个编码，那个编码的作用是什么\n(一开始没听懂问题，说的是Accept-Encoding，后来面试官说是想问http请求中特殊字符的转义)\n### 14. 有没有面过别的公司 | 哪儿的人\n### 15. 给了个C++的结构体，有char、int等3个属性，问占多少内存，换了个顺序，又问占多少内存\n(这个不大会，有会的老哥欢迎评论)\n### 16. 算法题：\n1. [算法1]：求最大长度的自然序子数组，输出长度和子数组下标(从1开始)，不要求连续，自然序是指“123456...”，即3的后面必须是4，以此类推如，3344567，输出应为4，[2,3,4,5]，即数组的第2/3/5/6个元素“3456”\n\n2. [算法2]：二叉树的层序遍历(面试官：再写个常规的吧)\n### 17. 做完题开始问数据库相关的，聚集索引和非聚集索引\n[:rocket::rocket::rocket:](../数据库/MySQL.md#索引)\n### 18. 事务隔离级别\n[:rocket::rocket::rocket:](../数据库/MySQL.md#隔离级别)\n### 19. 怎么实现的可重复读\n(主要从锁机制+MVCC的角度讲，详细说明了当前要访问的版本的事务id和ReadView中活跃事务id列表的关系，如果大于最大值怎么样，小于最小值怎么样，介于最大值最小值之间要怎么办)\n\n[:rocket::rocket::rocket:](../数据库/MySQL.md#隔离级别)\n### 20. 反问\n问的内容比较多，算法题让写了两个，用面试官的话来说一个不大常规，一个常规题目，也可能是因为一面的算法拉胯了所以二面问了俩？算法题一边讲思路一边做的，都还算顺利。面试官很和善，很多问题之间都是有联系的，回答的时候没按八股文来，基本都是说自己的理解+书籍上的内容。面试结果很快就出了(大概当天或者第二天)，效率很高，约了节后三面。\n\n--- \n\n# 面试复盘 | 字节番茄小说一面(已通过)\n\n> 作者：Yyyilia\n链接：https://www.nowcoder.com/discuss/766791?source_id=discuss_experience_nctrack&channel=-1\n来源：牛客网\n\n### 1. 自我介绍\n### 2. 项目相关\n(团队人数 | 项目应用到的框架等 | 表设计)\n### 3. Java中常用的集合类\n(先说Collection和Map接口，再分别说子接口和实现类，以及大概的区别，没让细讲源码)\n\n[:rocket::rocket::rocket:](../Java-基础/容器-collection.md)\n### 4. 有一组数据，需要按照顺序对它进行加密，如果用集合做的话你会选哪个集合？比如一个数组，其元素本身无序，每个元素是字母或者数字都行，对它从小到大进行排序，你会选哪个集合去存储？\n(我当时好像选的TreeMap？忘记了...)\n\n有序：\n- list\n- TreeSet\n- TreeMap\n- LinkedHashMap\n### 5. Java中线程同步的方案有哪些\n(参考《深入理解Java虚拟机》第13章第2节，从“阻塞同步方案 | 非阻塞同步方案 | 无同步方案”三部分说明)\n### 6. Lock接口的实现类\n(参考《Java并发编程的艺术》第5章，Lock接口的实现基本都是通过聚合了一个同步器的子类来完成线程访问控制的，引出了队列同步器AQS，实现类讲了ReentrantLock和ReentrantReadWriteLock)\n### 7. MySQL查询比较慢的话，通过什么方式来优化\n情况①：偶尔很慢，可能是数据库在查询脏页，或者没拿到锁\n\n情况②：一直很慢，可能是没有索引，或者有索引但没走索引，或者表数据量太大需要分库分表)\n### 8. http协议\n(http是哪一层的协议，其传输层协议是什么，三次握手和四次挥手的完整过程)\n### 9. 算法题：lc726 原子的数量\n(稍微变型，不必统计所有原子的数量，统计输入的目标原子的数量)\n### 10. 反问\n部门和技术栈 其实是面试体验不太好的，不是面试官的面试过程有问题，是他没关消息提示，整个面试过程听了可能上百声“叮咚”的消息提示音，一开始没适应的时候会打断思路。最后的算法做的不咋地，给了大概20min，说了思路没全写完，面试官说思路应该可以，他还有下一场面试。这回算法拉胯了，以为凉了结果过了。"
  }
]