跳至主要內容

面试题全集

wangdx大约 107 分钟

JDK、JRE、JVM 之间的区别

JDK(Java SE Development Kit),Java 标准开发包,它提供了编译、运⾏ Java 程序所需的各种⼯具和资源,包括 Java 编译器、Java 运⾏时环境,以及常⽤的 Java 类库等

JRE( Java Runtime Environment) ,Java 运⾏环境,⽤于运⾏ Java 的字节码⽂件。JRE 中包括了 JVM 以及 JVM ⼯作所需要的类库,普通⽤户只需要安装 JRE 来运⾏ Java 程序,⽽程序开发者必须安装 JDK 来编译、调试程序。

JVM(Java Virtual Mechinal),Java 虚拟机,是 JRE 的⼀部分,它是整个 java 实现跨平台的最核⼼的部分,负责运⾏字节码⽂件。

我们写 Java 代码,⽤ txt 就可以写,但是写出来的 Java 代码,想要运⾏,需要先编译成字节码,那就需要编译器,⽽ JDK 中就包含了编译器 javac,编译之后的字节码,想要运⾏,就需要⼀个可以执⾏字节码的程序,这个程序就是 JVM(Java 虚拟机),专⻔⽤来执⾏ Java 字节码的。

如果我们要开发 Java 程序,那就需要 JDK,因为要编译 Java 源⽂件。如果我们只想运⾏已经编译好的 Java 字节码⽂件,也就是*.class ⽂件,那么就只需要 JRE。

JDK 中包含了 JRE,JRE 中包含了 JVM。

另外,JVM 在执⾏ Java 字节码时,需要把字节码解释为机器指令,⽽不同操作系统的机器指令是有可能不⼀样的,所以就导致不同操作系统上的 JVM 是不⼀样的,所以我们在安装 JDK 时需要选择操作系统。

另外,JVM 是⽤来执⾏ Java 字节码的,所以凡是某个代码编译之后是 Java 字节码,那就都能在 JVM 上运⾏,⽐如 Apache Groovy, Scala and Kotlin 等等。

hashCode()与 equals()之间的关系

在 Java 中,每个对象都可以调⽤⾃⼰的 hashCode()⽅法得到⾃⼰的哈希值(hashCode),相当于对象的指纹信息,通常来说世界上没有完全相同的两个指纹,但是在 Java 中做不到这么绝对,但是我们仍然可以利⽤ hashCode 来做⼀些提前的判断,⽐如:

  • 如果两个对象的 hashCode 不相同,那么这两个对象肯定不同的两个对象
  • 如果两个对象的 hashCode 相同,不代表这两个对象⼀定是同⼀个对象,也可能是两个对象
  • 如果两个对象相等,那么他们的 hashCode 就⼀定相同

在 Java 的⼀些集合类的实现中,在⽐较两个对象是否相等时,会根据上⾯的原则,会先调⽤对象的 hashCode()⽅法得到 hashCode 进⾏⽐较,如果 hashCode 不相同,就可以直接认为这两个对象不相同,如果 hashCode 相同,那么就会进⼀步调⽤ equals()⽅法进⾏⽐较。⽽ equals()⽅法,就是⽤来最终确定两个对象是不是相等的,通常 equals ⽅法的实现会⽐较重,逻辑⽐较多,⽽ hashCode()主要就是得到⼀个哈希值,实际上就⼀个数字,相对⽽⾔⽐较轻,所以在⽐较两个对象时,通常都会先根据 hashCode 想⽐较⼀下。

所以我们就需要注意,如果我们重写了 equals()⽅法,那么就要注意 hashCode()⽅法,⼀定要保证能遵守上述规则。

String、StringBuffer、StringBuilder 的区别

  1. String 是不可变的,如果尝试去修改,会新⽣成⼀个字符串对象,StringBuffer 和 StringBuilder 是可变的
  2. StringBuffer 是线程安全的,StringBuilder 是线程不安全的,所以在单线程环境下 StringBuilder 效率会更⾼

泛型中 extends 和 super 的区别

  1. <? extends T>表示包括 T 在内的任何 T 的⼦类
  2. <? super T>表示包括 T 在内的任何 T 的⽗类

==和 equals ⽅法的区别

  • ==:如果是基本数据类型,⽐较是值,如果是引⽤类型,⽐较的是引⽤地址
  • equals:具体看各个类重写 equals ⽅法之后的⽐较逻辑,⽐如 String 类,虽然是引⽤类型,但是 String 类中重写了 equals ⽅法,⽅法内部⽐较的是字符串中的各个字符是否全部相等。

重载和重写的区别

  • 重载(Overload): 在⼀个类中,同名的⽅法如果有不同的参数列表(⽐如参数类型不同、参数个数不同)则视为重载。
  • 重写(Override): 从字⾯上看,重写就是重新写⼀遍的意思。其实就是在⼦类中把⽗类本身有的⽅法重新写⼀遍。⼦类继承了⽗类的⽅法,但有时⼦类并不想原封不动的继承⽗类中的某个⽅法,所以在⽅法名,参数列表,返回类型都相同(⼦类中⽅法的返回值可以是⽗类中⽅法返回值的⼦类)的情况下, 对⽅法体进⾏修改,这就是重写。但要注意⼦类⽅法的访问修饰权限不能⼩于⽗类的。

List 和 Set 的区别

  • List:有序,按对象插⼊的顺序保存对象,可重复,允许多个 Null 元素对象,可以使⽤ Iterator 取出所有元素,在逐⼀遍历,还可以使⽤ get(int index)获取指定下标的元素
  • Set:⽆序,不可重复,最多允许有⼀个 Null 元素对象,取元素时只能⽤ Iterator 接⼝取得所有元素,在逐⼀遍历各个元素

ArrayList 和 LinkedList 区别

  1. ⾸先,他们的底层数据结构不同,ArrayList 底层是基于数组实现的,LinkedList 底层是基于链表实现的
  2. 由于底层数据结构不同,他们所适⽤的场景也不同,ArrayList 更适合随机查找,LinkedList 更适合删除和添加,查询、添加、删除的时间复杂度不同
  3. 另外 ArrayList 和 LinkedList 都实现了 List 接⼝,但是 LinkedList 还额外实现了 Deque 接⼝,所以 LinkedList 还可以当做队列来使⽤

谈谈 ConcurrentHashMap 的扩容机制

1.7 版本

  1. 1.7 版本的 ConcurrentHashMap 是基于 Segment 分段实现的
  2. 每个 Segment 相对于⼀个⼩型的 HashMap
  3. 每个 Segment 内部会进⾏扩容,和 HashMap 的扩容逻辑类似
  4. 先⽣成新的数组,然后转移元素到新数组中
  5. 扩容的判断也是每个 Segment 内部单独判断的,判断是否超过阈值

1.8 版本

  1. 1.8 版本的 ConcurrentHashMap 不再基于 Segment 实现
  2. 当某个线程进⾏ put 时,如果发现 ConcurrentHashMap 正在进⾏扩容那么该线程⼀起进⾏扩容
  3. 如果某个线程 put 时,发现没有正在进⾏扩容,则将 key-value 添加到 ConcurrentHashMap 中,然后判断是否超过阈值,超过了则进⾏扩容
  4. ConcurrentHashMap 是⽀持多个线程同时扩容的
  5. 扩容之前也先⽣成⼀个新的数组
  6. 在转移元素时,先将原数组分组,将每组分给不同的线程来进⾏元素的转移,每个线程负责⼀组或多组的元素转移⼯作

Jdk1.7 到 Jdk1.8 HashMap 发⽣了什么变化(底层)?

  1. 1.7 中底层是数组+链表,1.8 中底层是数组+链表+红⿊树,加红⿊树的⽬的是提⾼ HashMap 插⼊和查询整体效率
  2. 1.7 中链表插⼊使⽤的是头插法,1.8 中链表插⼊使⽤的是尾插法,因为 1.8 中插⼊ key 和 value 时需要判断链表元素个数,所以需要遍历链表统计链表元素个数,所以正好就直接使⽤尾插法
  3. 1.7 中哈希算法⽐较复杂,存在各种右移与异或运算,1.8 中进⾏了简化,因为复杂的哈希算法的⽬的就是提⾼散列性,来提供 HashMap 的整体效率,⽽ 1.8 中新增了红⿊树,所以可以适当的简化哈希算法,节省 CPU 资源

说⼀下 HashMap 的 Put ⽅法

先说 HashMap 的 Put ⽅法的⼤体流程:

  1. 根据 Key 通过哈希算法与与运算得出数组下标

  2. 如果数组下标位置元素为空,则将 key 和 value 封装为 Entry 对象(JDK1.7 中是 Entry 对象,JDK1.8 中是 Node 对象)并放⼊该位置

  3. 如果数组下标位置元素不为空,则要分情况讨论

    • a. 如果是 JDK1.7,则先判断是否需要扩容,如果要扩容就进⾏扩容,如果不⽤扩容就⽣成 Entry 对象,并使⽤头插法添加到当前位置的链表中

    • b. 如果是 JDK1.8,则会先判断当前位置上的 Node 的类型,看是红⿊树 Node,还是链表 Node

      • i.如果是红⿊树 Node,则将 key 和 value 封装为⼀个红⿊树节点并添加到红⿊树中去,在这个过程中会判断红⿊树中是否存在当前 key,如果存在则更新 value
      • ⅱ. 如果此位置上的 Node 对象是链表节点,则将 key 和 value 封装为⼀个链表 Node 并通过尾插法插⼊到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历链表的过程中会判断是否存在当前 key,如果存在则更新 value,当遍历完链表后,将新链表 Node 插⼊到链表中,插⼊到链表后,会看当前链表的节点个数,如果⼤于等于 8,那么则会将该链表转成红⿊树
      • ⅲ. 将 key 和 value 封装为 Node 插⼊到链表或红⿊树中后,再判断是否需要进⾏扩容,如果需要就扩容,如果不需要就结束 PUT ⽅法

深拷⻉和浅拷⻉

深拷⻉和浅拷⻉就是指对象的拷⻉,⼀个对象中存在两种类型的属性,⼀种是基本数据类型,⼀种是实例对象的引⽤。

  1. 浅拷⻉是指,只会拷⻉基本数据类型的值,以及实例对象的引⽤地址,并不会复制⼀份引⽤地址所指向的对象,也就是浅拷⻉出来的对象,内部的类属性指向的是同⼀个对象
  2. 深拷⻉是指,既会拷⻉基本数据类型的值,也会针对实例对象的引⽤地址所指向的对象进⾏复制,深拷⻉出来的对象,内部的属性指向的不是同⼀个对象

HashMap 的扩容机制原理

1.7 版本

  1. 先⽣成新数组
  2. 遍历⽼数组中的每个位置上的链表上的每个元素
  3. 取每个元素的 key,并基于新数组⻓度,计算出每个元素在新数组中的下标
  4. 将元素添加到新数组中去
  5. 所有元素转移完了之后,将新数组赋值给 HashMap 对象的 table 属性

1.8 版本

  1. 先⽣成新数组
  2. 遍历⽼数组中的每个位置上的链表或红⿊树
  3. 如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去
  4. 如果是红⿊树,则先遍历红⿊树,先计算出红⿊树中每个元素对应在新数组中的下标位置
    • a. 统计每个下标位置的元素个数
    • b. 如果该位置下的元素个数超过了 8,则⽣成⼀个新的红⿊树,并将根节点的添加到新数组的对应位置
    • c. 如果该位置下的元素个数没有超过 8,那么则⽣成⼀个链表,并将链表的头节点添加到新数组的对应位置
  5. 所有元素转移完了之后,将新数组赋值给 HashMap 对象的 table 属性

CopyOnWriteArrayList 的底层原理是怎样的

  1. ⾸先 CopyOnWriteArrayList 内部也是⽤数组来实现的,在向 CopyOnWriteArrayList 添加元素时,会复制⼀个新的数组,写操作在新数组上进⾏,读操作在原数组上进⾏
  2. 并且,写操作会加锁,防⽌出现并发写⼊丢失数据的问题
  3. 写操作结束之后会把原数组指向新数组
  4. CopyOnWriteArrayList 允许在写操作时来读取数据,⼤⼤提⾼了读的性能,因此适合读多写少的应⽤场景,但是 CopyOnWriteArrayList 会⽐较占内存,同时可能读到的数据不是实时最新的数据,所以不适合实时性要求很⾼的场景

什么是字节码?采⽤字节码的好处是什么?

编译器(javac)将 Java 源⽂件(*.java)⽂件编译成为字节码⽂件(*.class),可以做到⼀次编译到处运⾏,windows 上编译好的 class ⽂件,可以直接在 linux 上运⾏,通过这种⽅式做到跨平台,不过 Java 的跨平台有⼀个前提条件,就是不同的操作系统上安装的 JDK 或 JRE 是不⼀样的,虽然字节码是通⽤的,但是需要把字节码解释成各个操作系统的机器码是需要不同的解释器的,所以针对各个操作系统需要有各⾃的 JDK 或 JRE。

采⽤字节码的好处,⼀⽅⾯实现了跨平台,另外⼀⽅⾯也提⾼了代码执⾏的性能,编译器在编译源代码时可以做⼀些编译期的优化,⽐如锁消除、标量替换、⽅法内联等。

Java 中的异常体系是怎样的

  • Java 中的所有异常都来⾃顶级⽗类 Throwable。

  • Throwable 下有两个⼦类 Exception 和 Error。

  • Error 表示⾮常严重的错误,⽐如 java.lang.StackOverFlowError 和 Java.lang.OutOfMemoryError,通常这些错误出现时,仅仅想靠程序⾃⼰是解决不了的,可能是虚拟机、磁盘、操作系统层⾯出现的问题了,所以通常也不建议在代码中去捕获这些 Error,因为捕获的意义不⼤,因为程序可能已经根本运⾏不了了。

  • Exception 表示异常,表示程序出现 Exception 时,是可以靠程序⾃⼰来解决的,⽐如 NullPointerException、IllegalAccessException 等,我们可以捕获这些异常来做特殊处理

  • Exception 的⼦类通常⼜可以分为 RuntimeException 和⾮ RuntimeException 两类

    • RunTimeException 表示运⾏期异常,表示这个异常是在代码运⾏过程中抛出的,这些异常是⾮检查异常,程序中可以选择捕获处理,也可以不处理。这些异常⼀般是由程序逻辑错误引起的,程序应该从逻辑⻆度尽可能避免这类异常的发⽣,⽐如 NullPointerException、IndexOutOfBoundsException 等。
    • ⾮ RuntimeException 表示⾮运⾏期异常,也就是我们常说的检查异常,是必须进⾏处理的异常,如果不处理,程序就不能检查异常通过。如 IOException、SQLException 等以及⽤户⾃定义的 Exception 异常。

在 Java 的异常处理机制中,什么时候应该抛出异常,什么时候捕获异常?

异常相当于⼀种提示,如果我们抛出异常,就相当于告诉上层⽅法,我抛了⼀个异常,我处理不了这个异常,交给你来处理,⽽对于上层⽅法来说,它也需要决定⾃⼰能不能处理这个异常,是否也需要交给它的上层。

所以我们在写⼀个⽅法时,我们需要考虑的就是,本⽅法能否合理的处理该异常,如果处理不了就继续向上抛出异常,包括本⽅法中在调⽤另外⼀个⽅法时,发现出现了异常,如果这个异常应该由⾃⼰来处理,那就捕获该异常并进⾏处理。

Java 中有哪些类加载器

JDK ⾃带有三个类加载器:bootstrap ClassLoader、ExtClassLoader、AppClassLoader。

  • BootStrapClassLoader 是 ExtClassLoader 的⽗类加载器,默认负责加载%JAVA_HOME%lib 下的 jar 包和 class ⽂件。
  • ExtClassLoader 是 AppClassLoader 的⽗类加载器,负责加载%JAVA_HOME%/lib/ext ⽂件夹下的 jar 包和 class 类。
  • AppClassLoader 是⾃定义类加载器的⽗类,负责加载 classpath 下的类⽂件。

说说类加载器双亲委派模型

JVM 中存在三个默认的类加载器:

  1. BootstrapClassLoader
  2. ExtClassLoader
  3. AppClassLoader

AppClassLoader 的⽗加载器是 ExtClassLoader,ExtClassLoader 的⽗加载器是 BootstrapClassLoader。

JVM 在加载⼀个类时,会调⽤ AppClassLoader 的 loadClass ⽅法来加载这个类,不过在这个⽅法中,会先使⽤ ExtClassLoader 的 loadClass ⽅法来加载类,同样 ExtClassLoader 的 loadClass ⽅法中会先使⽤ BootstrapClassLoader 来加载类,如果 BootstrapClassLoader 加载到了就直接成功,如果 BootstrapClassLoader 没有加载到,那么 ExtClassLoader 就会⾃⼰尝试加载该类,如果没有加载到,那么则会由 AppClassLoader 来加载这个类。

所以,双亲委派指得是,JVM 在加载类时,会委派给 Ext 和 Bootstrap 进⾏加载,如果没加载到才由⾃⼰进⾏加载。

JVM 中哪些是线程共享区

堆区和⽅法区是所有线程共享的,栈、本地⽅法栈、程序计数器是每个线程独有的

运行时数据区===》方法区 ,虚拟机栈,本地方法栈,堆,程序计数器 执行引擎+++本地接口++本地方法库

你们项⽬如何排查 JVM 问题

对于还在正常运⾏的系统:

  1. 可以使⽤ jmap 来查看 JVM 中各个区域的使⽤情况
  2. 可以通过 jstack 来查看线程的运⾏情况,⽐如哪些线程阻塞、是否出现了死锁
  3. 可以通过 jstat 命令来查看垃圾回收的情况,特别是 fullgc,如果发现 fullgc ⽐较频繁,那么就得进⾏调优了
  4. 通过各个命令的结果,或者 jvisualvm 等⼯具来进⾏分析
  5. ⾸先,初步猜测频繁发送 fullgc 的原因,如果频繁发⽣ fullgc 但是⼜⼀直没有出现内存溢出,那么表示 fullgc 实际上是回收了很多对象了,所以这些对象最好能在 younggc 过程中就直接回收掉,避免这些对象进⼊到⽼年代,对于这种情况,就要考虑这些存活时间不⻓的对象是不是⽐较⼤,导致年轻代放不下,直接进⼊到了⽼年代,尝试加⼤年轻代的⼤⼩,如果改完之后,fullgc 减少,则证明修改有效
  6. 同时,还可以找到占⽤ CPU 最多的线程,定位到具体的⽅法,优化这个⽅法的执⾏,看是否能避免某些对象的创建,从⽽节省内存

对于已经发⽣了 OOM 的系统:

  1. ⼀般⽣产系统中都会设置当系统发⽣了 OOM 时,⽣成当时的 dump ⽂件(-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/base)
  2. 我们可以利⽤ jsisualvm 等⼯具来分析 dump ⽂件
  3. 根据 dump ⽂件找到异常的实例对象,和异常的线程(占⽤ CPU ⾼),定位到具体的代码
  4. 然后再进⾏详细的分析和调试

总之,调优不是⼀蹴⽽就的,需要分析、推理、实践、总结、再分析,最终定位到具体的问题

⼀个对象从加载到 JVM,再到被 GC 清除,都经历了什么过程?

  1. ⾸先把字节码⽂件内容加载到⽅法区
  2. 然后再根据类信息在堆区创建对象
  3. 对象⾸先会分配在堆区中年轻代的 Eden 区,经过⼀次 Minor GC 后,对象如果存活,就会进⼊ Suvivor 区。在后续的每次 Minor GC 中,如果对象⼀直存活,就会在 Suvivor 区来回拷⻉,每移动⼀次,年龄加 1
  4. 当年龄超过 15 后,对象依然存活,对象就会进⼊⽼年代
  5. 如果经过 Full GC,被标记为垃圾对象,那么就会被 GC 线程清理掉

怎么确定⼀个对象到底是不是垃圾?

  1. 引⽤计数算法: 这种⽅式是给堆内存当中的每个对象记录⼀个引⽤个数。引⽤个数为 0 的就认为是垃圾。这是早期 JDK 中使⽤的⽅式。引⽤计数⽆法解决循环引⽤的问题。
  2. 可达性算法: 这种⽅式是在内存中,从根对象向下⼀直找引⽤,找到的对象就不是垃圾,没找到的对象就是垃圾。

JVM 有哪些垃圾回收算法?

  1. 标记清除算法:

    • a. 标记阶段:把垃圾内存标记出来
    • b. 清除阶段:直接将垃圾内存回收。
    • c. 这种算法是⽐较简单的,但是有个很严重的问题,就是会产⽣⼤量的内存碎⽚。
  2. 复制算法:

为了解决标记清除算法的内存碎⽚问题,就产⽣了复制算法。复制算法将内存分为⼤⼩相等的两半,每次只使⽤其中⼀半。垃圾回收时,将当前这⼀块的存活对象全部拷⻉到另⼀半,然后当前这⼀半内存就可以直接清除。这种算法没有内存碎⽚,但是他的问题就在于浪费空间。⽽且,他的效率跟存活对象的个数有关。

  1. 标记压缩算法:

为了解决复制算法的缺陷,就提出了标记压缩算法。这种算法在标记阶段跟标记清除算法是⼀样的,但是在完成标记之后,不是直接清理垃圾内存,⽽是将存活对象往⼀端移动,然后将边界以外的所有内存直接清除。

什么是 STW?

STW: Stop-The-World,是在垃圾回收算法执⾏过程当中,需要将 JVM 内存冻结的⼀种状态。在 STW 状态下,JAVA 的所有线程都是停⽌执⾏的-GC 线程除外,native ⽅法可以执⾏,但是,不能与 JVM 交互。GC 各种算法优化的重点,就是减少 STW,同时这也是 JVM 调优的重点。

JVM 参数有哪些?

JVM 参数⼤致可以分为三类:

  1. 标注指令: -开头,这些是所有的 HotSpot 都⽀持的参数。可以⽤ java -help 打印出来。
  2. ⾮标准指令: -X 开头,这些指令通常是跟特定的 HotSpot 版本对应的。可以⽤ java -X 打印出来。
  3. 不稳定参数: -XX 开头,这⼀类参数是跟特定 HotSpot 版本对应的,并且变化⾮常⼤。

说说对线程安全的理解

线程安全指的是,我们写的某段代码,在多个线程同时执⾏这段代码时,不会产⽣混乱,依然能够得到正常的结果,⽐如 i++,i 初始化值为 0,那么两个线程来同时执⾏这⾏代码,如果代码是线程安全的,那么最终的结果应该就是⼀个线程的结果为 1,⼀个线程的结果为 2,如果出现了两个线程的结果都为 1,则表示这段代码是线程不安全的。

所以线程安全,主要指的是⼀段代码在多个线程同时执⾏的情况下,能否得到正确的结果。

对守护线程的理解

线程分为⽤户线程和守护线程,⽤户线程就是普通线程,守护线程就是 JVM 的后台线程,⽐如垃圾回收线程就是⼀个守护线程,守护线程会在其他普通线程都停⽌运⾏之后⾃动关闭。我们可以通过设置 thread.setDaemon(true)来把⼀个线程设置为守护线程。

ThreadLocal 的底层原理

  1. ThreadLocal 是 Java 中所提供的线程本地存储机制,可以利⽤该机制将数据缓存在某个线程内部,该线程可以在任意时刻、任意⽅法中获取缓存的数据
  2. ThreadLocal 底层是通过 ThreadLocalMap 来实现的,每个 Thread 对象(注意不是 ThreadLocal 对象)中都存在⼀个 ThreadLocalMap,Map 的 key 为 ThreadLocal 对象,Map 的 value 为需要缓存的值
  3. 如果在线程池中使⽤ ThreadLocal 会造成内存泄漏,因为当 ThreadLocal 对象使⽤完之后,应该要把设置的 key,value,也就是 Entry 对象进⾏回收,但线程池中的线程不会回收,⽽线程对象是通过强引⽤指向 ThreadLocalMap,ThreadLocalMap 也是通过强引⽤指向 Entry 对象,线程不被回收,Entry 对象也就不会被回收,从⽽出现内存泄漏,解决办法是,在使⽤了 ThreadLocal 对象之后,⼿动调⽤ ThreadLocal 的 remove ⽅法,⼿动清楚 Entry 对象
  4. ThreadLocal 经典的应⽤场景就是连接管理(⼀个线程持有⼀个连接,该连接对象可以在不同的⽅法之间进⾏传递,线程之间不共享同⼀个连接)

并发、并⾏、串⾏之间的区别

  1. 串⾏:⼀个任务执⾏完,才能执⾏下⼀个任务
  2. 并⾏(Parallelism):两个任务同时执⾏
  3. 并发(Concurrency):两个任务整体看上去是同时执⾏,在底层,两个任务被拆成了很多份,然后⼀个⼀个执⾏,站在更⾼的⻆度看来两个任务是同时在执⾏的

Java 死锁如何避免?

造成死锁的⼏个原因:

  1. ⼀个资源每次只能被⼀个线程使⽤
  2. ⼀个线程在阻塞等待某个资源时,不释放已占有资源
  3. ⼀个线程已经获得的资源,在未使⽤完之前,不能被强⾏剥夺
  4. 若⼲线程形成头尾相接的循环等待资源关系

这是造成死锁必须要达到的 4 个条件,如果要避免死锁,只需要不满⾜其中某⼀个条件即可。⽽其中前 3 个条件是作为锁要符合的条件,所以要避免死锁就需要打破第 4 个条件,不出现循环等待锁的关系。

在开发过程中:

  1. 要注意加锁顺序,保证每个线程按同样的顺序进⾏加锁
  2. 要注意加锁时限,可以针对所设置⼀个超时时间
  3. 要注意死锁检查,这是⼀种预防机制,确保在第⼀时间发现死锁并进⾏解决

线程池的底层⼯作原理

线程池内部是通过队列+线程实现的,当我们利⽤线程池执⾏任务时:

  1. 如果此时线程池中的线程数量⼩于 corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
  2. 如果此时线程池中的线程数量等于 corePoolSize,但是缓冲队列 workQueue 未满,那么任务被放⼊缓冲队列。
  3. 如果此时线程池中的线程数量⼤于等于 corePoolSize,缓冲队列 workQueue 满,并且线程池中的数量⼩于 maximumPoolSize,建新的线程来处理被添加的任务。
  4. 如果此时线程池中的线程数量⼤于 corePoolSize,缓冲队列 workQueue 满,并且线程池中的数量等于 maximumPoolSize,那么通过 handler 所指定的策略来处理此任务。
  5. 当线程池中的线程数量⼤于 corePoolSize 时,如果某线程空闲时间超过 keepAliveTime,线程将被终⽌。这样,线程池可以动态的调整池中的线程数

线程池为什么是先添加列队⽽不是先创建最⼤线程?

当线程池中的核⼼线程都在忙时,如果继续往线程池中添加任务,那么任务会先放⼊队列,队列满了之后,才会新开线程。这就相当于,⼀个公司本来有 10 个程序员,本来这 10 个程序员能正常的处理各种需求,但是随着公司的发展,需求在慢慢的增加,但是⼀开始这些需求只会增加在待开发列表中,然后这 10 个程序员加班加点的从待开发列表中获取需求并进⾏处理,但是某⼀天待开发列表满了,公司发现现有的 10 个程序员是真的处理不过来了,所以就开始新招员⼯了。

ReentrantLock 中的公平锁和⾮公平锁的底层实现

⾸先不管是公平锁和⾮公平锁,它们的底层实现都会使⽤ AQS 来进⾏排队,它们的区别在于:线程在使⽤ lock()⽅法加锁时,如果是公平锁,会先检查 AQS 队列中是否存在线程在排队,如果有线程在排队,则当前线程也进⾏排队,如果是⾮公平锁,则不会去检查是否有线程在排队,⽽是直接竞争锁。

不管是公平锁还是⾮公平锁,⼀旦没竞争到锁,都会进⾏排队,当锁释放时,都是唤醒排在最前⾯的线程,所以⾮公平锁只是体现在了线程加锁阶段,⽽没有体现在线程被唤醒阶段。

另外,ReentrantLock 是可重⼊锁,不管是公平锁还是⾮公平锁都是可重⼊的。

ReentrantLock 中 tryLock()和 lock()⽅法的区别

  1. tryLock()表示尝试加锁,可能加到,也可能加不到,该⽅法不会阻塞线程,如果加到锁则返回 true,没有加到则返回 false
  2. lock()表示阻塞加锁,线程会阻塞直到加到锁,⽅法也没有返回值

CountDownLatch 和 Semaphore 的区别和底层原理

CountDownLatch 表示计数器,可以给 CountDownLatch 设置⼀个数字,⼀个线程调⽤ CountDownLatch 的 await()将会阻塞,其他线程可以调⽤ CountDownLatch 的 countDown()⽅法来对 CountDownLatch 中的数字减⼀,当数字被减成 0 后,所有 await 的线程都将被唤醒。对应的底层原理就是,调⽤ await()⽅法的线程会利⽤ AQS 排队,⼀旦数字被减为 0,则会将 AQS 中排队的线程依次唤醒。

Semaphore 表示信号量,可以设置许可的个数,表示同时允许最多多少个线程使⽤该信号量,通过 acquire()来获取许可,如果没有许可可⽤则线程阻塞,并通过 AQS 来排队,可以通过 release()⽅法来释放许可,当某个线程释放了某个许可后,会从 AQS 中正在排队的第⼀个线程开始依次唤醒,直到没有空闲许可。

Sychronized 的偏向锁、轻量级锁、重量级锁

  1. 偏向锁:在锁对象的对象头中记录⼀下当前获取到该锁的线程 ID,该线程下次如果⼜来获取该锁就可以直接获取到了
  2. 轻量级锁:由偏向锁升级⽽来,当⼀个线程获取到锁后,此时这把锁是偏向锁,此时如果有第⼆个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻量级锁底层是通过⾃旋来实现的,并不会阻塞线程
  3. 如果⾃旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞
  4. ⾃旋锁:⾃旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就⽆所谓唤醒线程,阻塞和唤醒这两个步骤都是需要操作系统去进⾏的,⽐较消耗时间,⾃旋锁是线程通过 CAS 获取预期的⼀个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程⼀直在运⾏中,相对⽽⾔没有使⽤太多的操作系统资源,⽐较轻量。

Sychronized 和 ReentrantLock 的区别

  1. sychronized 是⼀个关键字,ReentrantLock 是⼀个类
  2. sychronized 会⾃动的加锁与释放锁,ReentrantLock 需要程序员⼿动加锁与释放锁
  3. sychronized 的底层是 JVM 层⾯的锁,ReentrantLock 是 API 层⾯的锁
  4. sychronized 是⾮公平锁,ReentrantLock 可以选择公平锁或⾮公平锁
  5. sychronized 锁的是对象,锁信息保存在对象头中,ReentrantLock 通过代码中 int 类型的 state 标识来标识锁的状态
  6. sychronized 底层有⼀个锁升级的过程

谈谈你对 AQS 的理解,AQS 如何实现可重⼊锁?

  1. AQS 是⼀个 JAVA 线程同步的框架。是 JDK 中很多锁⼯具的核⼼实现框架。
  2. 在 AQS 中,维护了⼀个信号量 state 和⼀个线程组成的双向链表队列。其中,这个线程队列,就是⽤来给线程排队的,⽽ state 就像是⼀个红绿灯,⽤来控制线程排队或者放⾏的。 在不同的场景下,有不⽤的意义。
  3. 在可重⼊锁这个场景下,state 就⽤来表示加锁的次数。0 标识⽆锁,每加⼀次锁,state 就加 1。释放锁 state 就减 1。

谈谈你对 IOC 的理解

通常,我们认为 Spring 有两⼤特性 IoC 和 AOP,那到底该如何理解 IoC 呢?

对于很多初学者来说,IoC 这个概念给⼈的感觉就是我好像会,但是我说不出来。

那么 IoC 到底是什么,接下来来说说我的理解,实际上这是⼀个⾮常⼤的问题,所以我们就把它拆细了来回答,IoC 表示控制反转,那么:

  1. 什么是控制?控制了什么?
  2. 什么是反转?反转之前是谁控制的?反转之后是谁控制的?如何控制的?
  3. 为什么要反转?反转之前有什么问题?反转之后有什么好处?

这就是解决这⼀类⼤问题的思路,⼤⽽化⼩。 那么,我们先来解决第⼀个问题:什么是控制?控制了什么? 我们在⽤ Spring 的时候,我们需要做什么:

  1. 建⼀些类,⽐如 UserService、OrderService
  2. ⽤⼀些注解,⽐如@Autowired

但是,我们也知道,当程序运⾏时,⽤的是具体的 UserService 对象、OrderService 对象,那这些对象是什么时候创建的?谁创建的?包括对象⾥的属性是什么时候赋的值?谁赋的?所有这些都是我们程序员做的,以为我们只是写了类⽽已,所有的这些都是 Spring 做的,它才是幕后⿊⼿。这就是控制:

  1. 控制对象的创建
  2. 控制对象内属性的赋值

如果我们不⽤ Spring,那我们得⾃⼰来做这两件事,反过来,我们⽤ Spring,这两件事情就不⽤我们做了,我们要做的仅仅是定义类,以及定义哪些属性需要 Spring 来赋值(⽐如某个属性上加@Autowired),⽽这其实就是第⼆个问题的答案,这就是反转,表示⼀种对象控制权的转移。

那反转有什么⽤,为什么要反转?

如果我们⾃⼰来负责创建对象,⾃⼰来给对象中的属性赋值,会出现什么情况?

⽐如,现在有三个类:

  1. A 类,A 类⾥有⼀个属性 C c;
  2. B 类,B 类⾥也有⼀个属性 C c;
  3. C 类

现在程序要运⾏,这三个类的对象都需要创建出来,并且相应的属性都需要有值,那么除开定义这三个类之外,我们还得写:

  1. A a = new A();
  2. B b = new B();
  3. C c = new C();
  4. a.c = c;
  5. b.c = c;

这五⾏代码是不⽤ Spring 的情况下多出来的代码,⽽且,如果类在多⼀些,类中的属性在多⼀些,那相应的代码会更多,⽽且代码会更复杂。所以我们可以发现,我们⾃⼰来控制⽐交给 Spring 来控制,我们的代码量以及代码复杂度是要⾼很多的,反⾔之,将对象交给 Spring 来控制,减轻了程序员的负担。

总结⼀下,IoC 表示控制反转,表示如果⽤ Spring,那么 Spring 会负责来创建对象,以及给对象内的属性赋值,也就是如果⽤ Spring,那么对象的控制权会转交给 Spring。

单例 Bean 和单例模式

单例模式表示 JVM 中某个类的对象只会存在唯⼀⼀个。

⽽单例 Bean 并不表示 JVM 中只能存在唯⼀的某个类的 Bean 对象。

Spring 事务传播机制

多个事务⽅法相互调⽤时,事务如何在这些⽅法间传播,⽅法 A 是⼀个事务的⽅法,⽅法 A 执⾏过程中调⽤了⽅法 B,那么⽅法 B 有⽆事务以及⽅法 B 对事务的要求不同都会对⽅法 A 的事务具体执⾏造成影响,同时⽅法 A 的事务对⽅法 B 的事务执⾏也有影响,这种影响具体是什么就由两个⽅法所定义的事务传播类型所决定。

  1. REQUIRED(Spring 默认的事务传播类型):如果当前没有事务,则⾃⼰新建⼀个事务,如果当前存在事务,则加⼊这个事务
  2. SUPPORTS:当前存在事务,则加⼊当前事务,如果当前没有事务,就以⾮事务⽅法执⾏
  3. MANDATORY:当前存在事务,则加⼊当前事务,如果当前事务不存在,则抛出异常。
  4. REQUIRES_NEW:创建⼀个新事务,如果存在当前事务,则挂起该事务。
  5. NOT_SUPPORTED:以⾮事务⽅式执⾏,如果当前存在事务,则挂起当前事务
  6. NEVER:不使⽤事务,如果当前事务存在,则抛出异常
  7. NESTED:如果当前事务存在,则在嵌套事务中执⾏,否则 REQUIRED 的操作⼀样(开启⼀个事务)

Spring 事务什么时候会失效?

spring 事务的原理是 AOP,进⾏了切⾯增强,那么失效的根本原因是这个 AOP 不起作⽤了!常⻅情况有如下⼏种

  1. 发⽣⾃调⽤,类⾥⾯使⽤ this 调⽤本类的⽅法(this 通常省略),此时这个 this 对象不是代理类,⽽是 UserService 对象本身!解决⽅法很简单,让那个 this 变成 UserService 的代理类即可!
  2. ⽅法不是 public 的:@Transactional 只能⽤于 public 的⽅法上,否则事务不会失效,如果要⽤在⾮ public ⽅法上,可以开启 AspectJ 代理模式。
  3. 数据库不⽀持事务
  4. 没有被 spring 管理
  5. 异常被吃掉,事务不会回滚(或者抛出的异常没有被定义,默认为 RuntimeException)

Spring 中的 Bean 创建的⽣命周期有哪些步骤

Spring 中⼀个 Bean 的创建⼤概分为以下⼏个步骤:

  1. 推断构造⽅法
  2. 实例化
  3. 填充属性,也就是依赖注⼊
  4. 处理 Aware 回调
  5. 初始化前,处理@PostConstruct 注解
  6. 初始化,处理 InitializingBean 接⼝
  7. 初始化后,进⾏ AOP

当然其实真正的步骤更加细致,可以看下⾯的流程图

alt text
alt text

Spring 中 Bean 是线程安全的吗

Spring 本身并没有针对 Bean 做线程安全的处理,所以:

  1. 如果 Bean 是⽆状态的,那么 Bean 则是线程安全的
  2. 如果 Bean 是有状态的,那么 Bean 则不是线程安全的

另外,Bean 是不是线程安全,跟 Bean 的作⽤域没有关系,Bean 的作⽤域只是表示 Bean 的⽣命周期范围,对于任何⽣命周期的 Bean 都是⼀个对象,这个对象是不是线程安全的,还是得看这个 Bean 对象本身。

ApplicationContext 和 BeanFactory 有什么区别

BeanFactory 是 Spring 中⾮常核⼼的组件,表示 Bean ⼯⼚,可以⽣成 Bean,维护 Bean.

ApplicationContext 继承了 BeanFactory,所以 ApplicationContext 拥有 BeanFactory 所有的特点,也是⼀个 Bean ⼯⼚,但是 ApplicationContext 除开继承了 BeanFactory 之外,还继承了诸如 EnvironmentCapable、MessageSource、ApplicationEventPublisher 等接⼝,从⽽ ApplicationContext 还有获取系统环境变量、国际化、事件发布等功能,这是 BeanFactory 所不具备的

Spring 中的事务是如何实现的

  1. Spring 事务底层是基于数据库事务和 AOP 机制的
  2. ⾸先对于使⽤了@Transactional 注解的 Bean,Spring 会创建⼀个代理对象作为 Bean
  3. 当调⽤代理对象的⽅法时,会先判断该⽅法上是否加了@Transactional 注解
  4. 如果加了,那么则利⽤事务管理器创建⼀个数据库连接
  5. 并且修改数据库连接的 autocommit 属性为 false,禁⽌此连接的⾃动提交,这是实现 Spring 事务⾮常重要的⼀步
  6. 然后执⾏当前⽅法,⽅法中会执⾏ sql
  7. 执⾏完当前⽅法后,如果没有出现异常就直接提交事务
  8. 如果出现了异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务
  9. Spring 事务的隔离级别对应的就是数据库的隔离级别
  10. Spring 事务的传播机制是 Spring 事务⾃⼰实现的,也是 Spring 事务中最复杂的
  11. Spring 事务的传播机制是基于数据库连接来做的,⼀个数据库连接⼀个事务,如果传播机制配置为需要新开⼀个事务,那么实际上就是先建⽴⼀个数据库连接,在此新数据库连接上执⾏ sql

Spring 中什么时候@Transactional 会失效

因为 Spring 事务是基于代理来实现的,所以某个加了@Transactional 的⽅法只有是被代理对象调⽤时,那么这个注解才会⽣效,所以如果是被代理对象来调⽤这个⽅法,那么@Transactional 是不会失效的。

同时如果某个⽅法是 private 的,那么@Transactional 也会失效,因为底层 cglib 是基于⽗⼦类来实现的,⼦类是不能重载⽗类的 private ⽅法的,所以⽆法很好的利⽤代理,也会导致@Transactianal 失效

Spring 容器启动流程是怎样的

  1. 在创建 Spring 容器,也就是启动 Spring 时:
  2. ⾸先会进⾏扫描,扫描得到所有的 BeanDefinition 对象,并存在⼀个 Map 中
  3. 然后筛选出⾮懒加载的单例 BeanDefinition 进⾏创建 Bean,对于多例 Bean 不需要在启动过程中去进⾏创建,对于多例 Bean 会在每次获取 Bean 时利⽤ BeanDefinition 去创建
  4. 利⽤ BeanDefinition 创建 Bean 就是 Bean 的创建⽣命周期,这期间包括了合并 BeanDefinition、推断构造⽅法、实例化、属性填充、初始化前、初始化、初始化后等步骤,其中 AOP 就是发⽣在初始化后这⼀步骤中
  5. 单例 Bean 创建完了之后,Spring 会发布⼀个容器启动事件
  6. Spring 启动结束
  7. 在源码中会更复杂,⽐如源码中会提供⼀些模板⽅法,让⼦类来实现,⽐如源码中还涉及到⼀些 BeanFactoryPostProcessor 和 BeanPostProcessor 的注册,Spring 的扫描就是通过 BenaFactoryPostProcessor 来实现的,依赖注⼊就是通过 BeanPostProcessor 来实现的
  8. 在 Spring 启动过程中还会去处理@Import 等注解

Spring ⽤到了哪些设计模式

  1. 工厂模式:BeanFactory,FactoryBean,ProxyFactory
  2. 原型模式:原型 Bean,PrototypeTargetSource,PrototypeAspectInstanceFactory
  3. 单例模式:原型 Bean,SingletonTargetSource,DefaultBeanNameGenerator
  4. 构造器模式:BeanDefinitionBuilder----BeanDefinition,StringBuilder
  5. 适配器模式:
    • ApplicationListenerMethodAdapter--将@EventListener 注解的方法适配 ApplicationListener
    • AdvisorAdapter--将 Advisor 适配 MethodInterceptor
  6. 访问者模式:
    • PropertyAccessor --- 属性访问器,用来访问和设置某个对象的属性
    • MessageAccessor === 国际化资源访问器
  7. 装饰器模式:
    • BeanWrapper===比单纯的 Bean 更加强大
    • HttpRequestWrapper
  8. 代理模式
    • 方法生成了代理对象的地方就用到
    • aop
    • @Configuration
    • @Lazy
  9. 观察者模式
    • ApplicationListener == 事件监听机制
    • AdvisedSupportListener==ProxyFactory 可以提交此监听器,用来监听 ProxyFactory 创建代理对象完成事件,添加 Advisor 事件
  10. 策略模式
    • instantiationStratege == spring 需要根据 BeanDefinition 来实例化 Bean,但是具体可以选择不同策略进行实体化
    • BeanNameGenerator- beanname 生成器
  11. 模板方法模式 AbstractApplicationContext
    • postProcessBeanFactory 子类可以继承处理 beanfactory
    • onRefresh 子类可以做一些额外的初始化
  12. 责任链模式
    • DefaultAdvisorChainFactory

Spring Boot 中常⽤注解及其底层实现

  1. @SpringBootApplication 注解:这个注解标识了⼀个 SpringBoot ⼯程,它实际上是另外三个注解的组合,这三个注解是:

    • a. @SpringBootConfiguration:这个注解实际就是⼀个@Configuration,表示启动类也是⼀个配置类
    • b. @EnableAutoConfiguration:向 Spring 容器中导⼊了⼀个 Selector,⽤来加载 ClassPath 下 SpringFactories 中所定义的⾃动配置类,将这些⾃动加载为配置 Bean
    • c. @ComponentScan:标识扫描路径,因为默认是没有配置实际扫描路径,所以 SpringBoot 扫描的路径是启动类所在的当前⽬录
  2. @Bean 注解:⽤来定义 Bean,类似于 XML 中的<bean>标签,Spring 在启动时,会对加了@Bean 注解的⽅法进⾏解析,将⽅法的名字做为 beanName,并通过执⾏⽅法得到 bean 对象

  3. @Controller、@Service、@ResponseBody、@Autowired 都可以说

Spring Boot 是如何启动 Tomcat 的

  1. ⾸先,SpringBoot 在启动时会先创建⼀个 Spring 容器
  2. 在创建 Spring 容器过程中,会利⽤@ConditionalOnClass 技术来判断当前 classpath 中是否存在 Tomcat 依赖,如果存在则会⽣成⼀个启动 Tomcat 的 Bean
  3. Spring 容器创建完之后,就会获取启动 Tomcat 的 Bean,并创建 Tomcat 对象,并绑定端⼝等,然后启动 Tomcat

Mybatis 的优缺点

优点:

  1. 基于 SQL 语句编程,相当灵活,不会对应⽤程序或者数据库的现有设计造成任何影响,SQL 写在 XML ⾥,解除 sql 与程序代码的耦合,便于统⼀管理;提供 XML 标签, ⽀持编写动态 SQL 语句, 并可重⽤。
  2. 与 JDBC 相⽐,减少了 50%以上的代码量,消除了 JDBC ⼤量冗余的代码,不需要⼿动开关连接;
  3. 很好的与各种数据库兼容( 因为 MyBatis 使⽤ JDBC 来连接数据库,所以只要 JDBC ⽀持的数据库 MyBatis 都⽀持)。
  4. 能够与 Spring 很好的集成;
  5. 提供映射标签, ⽀持对象与数据库的 ORM 字段关系映射; 提供对象关系映射标签, ⽀持对象关系组件维护。

缺点:

  1. SQL 语句的编写⼯作量较⼤, 尤其当字段多、关联表多时, 对开发⼈员编写 SQL 语句的功底有⼀定要求。
  2. SQL 语句依赖于数据库, 导致数据库移植性差, 不能随意更换数据库。

#{}和${}的区别是什么?

#{}是预编译处理、是占位符, ${}是字符串替换、是拼接符。

Mybatis 在处理#{}时,会将 sql 中的#{}替换为?号,调⽤ PreparedStatement 来赋值;

Mybatis 在处理${}时,会将sql中的${}替换成变量的值,调⽤ Statement 来赋值;

使⽤#{}可以有效的防⽌ SQL 注⼊, 提⾼系统安全性。

索引的基本原理

索引⽤来快速地寻找那些具有特定值的记录。如果没有索引,⼀般来说执⾏查询时遍历整张表。

索引的原理:就是把⽆序的数据变成有序的查询

  1. 把创建了索引的列的内容进⾏排序
  2. 对排序结果⽣成倒排表
  3. 在倒排表内容上拼上数据地址链
  4. 在查询的时候,先拿到倒排表内容,再取出数据地址链,从⽽拿到具体数据

索引设计的原则?

查询更快、占⽤空间更⼩

  1. 适合索引的列是出现在 where ⼦句中的列,或者连接⼦句中指定的列
  2. 基数较⼩的表,索引效果较差,没有必要在此列建⽴索引
  3. 使⽤短索引,如果对⻓字符串列进⾏索引,应该指定⼀个前缀⻓度,这样能够节省⼤量索引空间,如果搜索词超过索引前缀⻓度,则使⽤索引排除不匹配的⾏,然后检查其余⾏是否可能匹配。
  4. 不要过度索引。索引需要额外的磁盘空间,并降低写操作的性能。在修改表内容的时候,索引会进⾏更新甚⾄重构,索引列越多,这个时间就会越⻓。所以只保持需要的索引有利于查询即可。
  5. 定义有外键的数据列⼀定要建⽴索引。
  6. 更新频繁字段不适合创建索引
  7. 若是不能有效区分数据的列不适合做索引列(如性别,男⼥未知,最多也就三种,区分度实在太低)
  8. 尽量的扩展索引,不要新建索引。⽐如表中已经有 a 的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。
  9. 对于那些查询中很少涉及的列,重复值⽐较多的列不要建⽴索引。
  10. 对于定义为 text、image 和 bit 的数据类型的列不要建⽴索引。

事务的基本特性和隔离级别

事务基本特性 ACID 分别是:

原⼦性指的是⼀个事务中的操作要么全部成功,要么全部失败。

⼀致性指的是数据库总是从⼀个⼀致性的状态转换到另外⼀个⼀致性的状态。⽐如 A 转账给 B100 块钱,假设 A 只有 90 块,⽀付之前我们数据库⾥的数据都是符合约束的,但是如果事务执⾏成功了,我们的数据库数据就破坏约束了,因此事务不能成功,这⾥我们说事务提供了⼀致性的保证

隔离性指的是⼀个事务的修改在最终提交前,对其他事务是不可⻅的。

持久性指的是⼀旦事务提交,所做的修改就会永久保存到数据库中。

隔离性有 4 个隔离级别,分别是:

  • read uncommit 读未提交,可能会读到其他事务未提交的数据,也叫做脏读。⽤户本来应该读取到 id=1 的⽤户 age 应该是 10,结果读取到了其他事务还没有提交的事务,结果读取结果 age=20,这就是脏读。

  • read commit 读已提交,两次读取结果不⼀致,叫做不可重复读。不可重复读解决了脏读的问题,他只会读取已经提交的事务。⽤户开启事务读取 id=1 ⽤户,查询到 age=10,再次读取发现结果=20,在同⼀个事务⾥同⼀个查询读取到不同的结果叫做不可重复读。

  • repeatable read 可重复复读,这是 mysql 的默认级别,就是每次读取结果都⼀样,但是有可能产⽣幻读。

  • serializable 串⾏,⼀般是不会使⽤的,他会给每⼀⾏读取的数据加锁,会导致⼤量超时和锁竞争的问题。

什么是 MVCC

MVCC(Multi-Version Concurrency Control ,多版本并发控制)指的就是在使⽤ READCOMMITTD、REPEATABLE READ 这两种隔离级别的事务在执⾏普通的 SEELCT 操作时访问记录的版本链的过程。可以使不同事务的读-写、写-读操作并发执⾏,从⽽提升系统性能。READCOMMITTD、REPEATABLE READ 这两个隔离级别的⼀个很⼤不同就是:⽣成 ReadView 的时机不同,READ COMMITTD 在每⼀次进⾏普通 SELECT 操作前都会⽣成⼀个 ReadView,⽽ REPEATABLEREAD 只在第⼀次进⾏普通 SELECT 操作前⽣成⼀个 ReadView,之后的查询操作都重复使⽤这个 ReadView 就好了。

简述 MyISAM 和 InnoDB 的区别

MyISAM:

  • 不⽀持事务,但是每次查询都是原⼦的;
  • ⽀持表级锁,即每次操作是对整个表加锁;
  • 存储表的总⾏数;
  • ⼀个 MYISAM 表有三个⽂件:索引⽂件、表结构⽂件、数据⽂件;
  • 采⽤⾮聚集索引,索引⽂件的数据域存储指向数据⽂件的指针。辅索引与主索引基本⼀致,但是辅索引不⽤保证唯⼀性。

InnoDb:

  • ⽀持 ACID 的事务,⽀持事务的四种隔离级别;
  • ⽀持⾏级锁及外键约束:因此可以⽀持写并发;
  • 不存储总⾏数;
  • ⼀个 InnoDb 引擎存储在⼀个⽂件空间(共享表空间,表⼤⼩不受操作系统控制,⼀个表可能分布在多个⽂件⾥),也有可能为多个(设置为独⽴表空,表⼤⼩受操作系统⽂件⼤⼩限制,⼀般为 2G),受操作系统⽂件⼤⼩的限制;
  • 主键索引采⽤聚集索引(索引的数据域存储数据⽂件本身),辅索引的数据域存储主键的值;因此从辅索引查找数据,需要先通过辅索引找到主键值,再访问辅索引;最好使⽤⾃增主键,防⽌插⼊数据时,为维持 B+树结构,⽂件的⼤调整。

Explain 语句结果中各个字段分表表示什么

列名 描述 id 查询语句中每出现⼀个 SELECT 关键字,MySQL 就会为它分配⼀个唯⼀的 id 值,某些⼦查询会被优化为 join 查询,那么出现的 id 会⼀样

select_type SELECT 关键字对应的那个查询的类型

table 表名

partitions 匹配的分区信息

type 针对单表的查询⽅式(全表扫描、索引)

possible_keys 可能⽤到的索引

key 实际上使⽤的索引

key_len 实际使⽤到的索引⻓度

ref 当使⽤索引列等值查询时,与索引列进⾏等值匹配的对象信息

rows 预估的需要读取的记录条数

filtered 某个表经过搜索条件过滤后剩余记录条数的百分⽐

Extra ⼀些额外的信息,⽐如排序等

索引覆盖是什么

索引覆盖就是⼀个 SQL 在执⾏时,可以利⽤索引来快速查找,并且此 SQL 所要查询的字段在当前索引对应的字段中都包含了,那么就表示此 SQL ⾛完索引后不⽤回表了,所需要的字段都在当前索引的叶⼦节点上存在,可以直接作为结果返回了

最左前缀原则是什么

当⼀个 SQL 想要利⽤索引是,就⼀定要提供该索引所对应的字段中最左边的字段,也就是排在最前⾯的字段,⽐如针对 a,b,c 三个字段建⽴了⼀个联合索引,那么在写⼀个 sql 时就⼀定要提供 a 字段的条件,这样才能⽤到联合索引,这是由于在建⽴ a,b,c 三个字段的联合索引时,底层的 B+树是按照 a,b,c 三个字段从左往右去⽐较⼤⼩进⾏排序的,所以如果想要利⽤ B+树进⾏快速查找也得符合这个规则

Innodb 是如何实现事务的

Innodb 通过 Buffer Pool,LogBuffer,Redo Log,Undo Log 来实现事务,以⼀个 update 语句为例:

  1. Innodb 在收到⼀个 update 语句后,会先根据条件找到数据所在的⻚,并将该⻚缓存在 Buffer Pool 中
  2. 执⾏ update 语句,修改 Buffer Pool 中的数据,也就是内存中的数据
  3. 针对 update 语句⽣成⼀个 RedoLog 对象,并存⼊ LogBuffer 中
  4. 针对 update 语句⽣成 undolog ⽇志,⽤于事务回滚
  5. 如果事务提交,那么则把 RedoLog 对象进⾏持久化,后续还有其他机制将 Buffer Pool 中所修改的数据⻚持久化到磁盘中
  6. 如果事务回滚,则利⽤ undolog ⽇志进⾏回滚

B 树和 B+树的区别,为什么 Mysql 使⽤ B+树

B 树的特点:

  1. 节点排序
  2. ⼀个节点了可以存多个元素,多个元素也排序了

B+树的特点:

  1. 拥有 B 树的特点
  2. 叶⼦节点之间有指针
  3. ⾮叶⼦节点上的元素在叶⼦节点上都冗余了,也就是叶⼦节点中存储了所有的元素,并且排好顺序

Mysql 索引使⽤的是 B+树,因为索引是⽤来加快查询的,⽽ B+树通过对数据进⾏排序所以是可以提⾼查询速度的,然后通过⼀个节点中可以存储多个元素,从⽽可以使得 B+树的⾼度不会太⾼,在 Mysql 中⼀个 Innodb ⻚就是⼀个 B+树节点,⼀个 Innodb ⻚默认 16kb,所以⼀般情况下⼀颗两层的 B+树可以存 2000 万⾏左右的数据,然后通过利⽤ B+树叶⼦节点存储了所有数据并且进⾏了排序,并且叶⼦节点之间有指针,可以很好的⽀持全表扫描,范围查找等 SQL 语句。

Mysql 锁有哪些,如何理解

按锁粒度分类:

  1. ⾏锁:锁某⾏数据,锁粒度最⼩,并发度⾼
  2. 表锁:锁整张表,锁粒度最⼤,并发度低
  3. 间隙锁:锁的是⼀个区间

还可以分为:

  1. 共享锁:也就是读锁,⼀个事务给某⾏数据加了读锁,其他事务也可以读,但是不能写
  2. 排它锁:也就是写锁,⼀个事务给某⾏数据加了写锁,其他事务不能读,也不能写

还可以分为:

  1. 乐观锁:并不会真正的去锁某⾏记录,⽽是通过⼀个版本号来实现的
  2. 悲观锁:上⾯所的⾏锁、表锁等都是悲观锁

在事务的隔离级别实现中,就需要利⽤锁来解决幻读

Mysql 慢查询该如何优化?

  1. 检查是否⾛了索引,如果没有则优化 SQL 利⽤索引
  2. 检查所利⽤的索引,是否是最优索引
  3. 检查所查字段是否都是必须的,是否查询了过多字段,查出了多余数据
  4. 检查表中数据是否过多,是否应该进⾏分库分表了
  5. 检查数据库实例所在机器的性能配置,是否太低,是否可以适当增加资源

什么是 RDB 和 AOF

RDB:Redis DataBase,在指定的时间间隔内将内存中的数据集快照写⼊磁盘,实际操作过程是 fork ⼀个⼦进程,先将数据集写⼊临时⽂件,写⼊成功后,再替换之前的⽂件,⽤⼆进制压缩存储。

优点:

  1. 整个 Redis 数据库将只包含⼀个⽂件 dump.rdb,⽅便持久化。
  2. 容灾性好,⽅便备份。
  3. 性能最⼤化,fork ⼦进程来完成写操作,让主进程继续处理命令,所以是 IO 最⼤化。使⽤单独⼦进程来进⾏持久化,主进程不会进⾏任何 IO 操作,保证了 redis 的⾼性能
  4. 相对于数据集⼤时,⽐ AOF 的启动效率更⾼。

缺点:

  1. 数据安全性低。RDB 是间隔⼀段时间进⾏持久化,如果持久化之间 redis 发⽣故障,会发⽣数据丢失。所以这种⽅式更适合数据要求不严谨的时候
  2. 由于 RDB 是通过 fork ⼦进程来协助完成数据持久化⼯作的,因此,如果当数据集较⼤时,可能会导致整个服务器停⽌服务⼏百毫秒,甚⾄是 1 秒钟。

AOF:Append Only File,以⽇志的形式记录服务器所处理的每⼀个写、删除操作,查询操作不会记录,以⽂本的⽅式记录,可以打开⽂件看到详细的操作记录

优点:

  1. 数据安全,Redis 中提供了 3 中同步策略,即每秒同步、每修改同步和不同步。事实上,每秒同步也是异步完成的,其效率也是⾮常⾼的,所差的是⼀旦系统出现宕机现象,那么这⼀秒钟之内修改的数据将会丢失。⽽每修改同步,我们可以将其视为同步持久化,即每次发⽣的数据变化都会被⽴即记录到磁盘中。
  2. 通过 append 模式写⽂件,即使中途服务器宕机也不会破坏已经存在的内容,可以通过 redis-check-aof ⼯具解决数据⼀致性问题。
  3. AOF 机制的 rewrite 模式。定期对 AOF ⽂件进⾏重写,以达到压缩的⽬的

缺点:

  1. AOF ⽂件⽐ RDB ⽂件⼤,且恢复速度慢。
  2. 数据集⼤的时候,⽐ rdb 启动效率低。
  3. 运⾏效率没有 RDB ⾼

AOF ⽂件⽐ RDB 更新频率⾼,优先使⽤ AOF 还原数据,AOF ⽐ RDB 更安全也更⼤,RDB 性能⽐ AOF 好,如果两个都配了优先加载 AOF。

Redis 的过期键的删除策略

Redis 是 key-value 数据库,我们可以设置 Redis 中缓存的 key 的过期时间。Redis 的过期策略就是指当 Redis 中缓存的 key 过期了,Redis 如何处理。

  • 惰性过期:只有当访问⼀个 key 时,才会判断该 key 是否已过期,过期则清除。该策略可以最⼤化地节省 CPU 资源,却对内存⾮常不友好。极端情况可能出现⼤量的过期 key 没有再次被访问,从⽽不会被清除,占⽤⼤量内存。

  • 定期过期:每隔⼀定的时间,会扫描⼀定数量的数据库的 expires 字典中⼀定数量的 key,并清除其中已过期的 key。该策略是⼀个折中⽅案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得 CPU 和内存资源达到最优的平衡效果。

(expires 字典会保存所有设置了过期时间的 key 的过期时间数据,其中,key 是指向键空间中的某个键的指针,value 是该键的毫秒精度的 UNIX 时间戳表示的过期时间。键空间是指该 Redis 集群中保存的所有键。)

Redis 中同时使⽤了惰性过期和定期过期两种过期策略。

简述 Redis 事务实现

1、事务开始

MULTI 命令的执⾏,标识着⼀个事务的开始。MULTI 命令会将客户端状态的 flags 属性中打开 REDIS_MULTI 标识来完成的。

2、命令⼊队

当⼀个客户端切换到事务状态之后,服务器会根据这个客户端发送来的命令来执⾏不同的操作。如果客户端发送的命令为 MULTI、EXEC、WATCH、DISCARD 中的⼀个,⽴即执⾏这个命令,否则将命令放⼊⼀个事务队列⾥⾯,然后向客户端返回 QUEUED 回复

  • 如果客户端发送的命令为 EXEC、DISCARD、WATCH、MULTI 四个命令的其中⼀个,那么服务器⽴即执⾏这个命令。
  • 如果客户端发送的是四个命令以外的其他命令,那么服务器并不⽴即执⾏这个命令。⾸先检查此命令的格式是否正确,如果不正确,服务器会在客户端状态(redisClient)的 flags 属性关闭 REDIS_MULTI 标识,并且返回错误信息给客户端。如果正确,将这个命令放⼊⼀个事务队列⾥⾯,然后向客户端返回 QUEUED 回复事务队列是按照 FIFO 的⽅式保存⼊队的命令

3、事务执⾏

客户端发送 EXEC 命令,服务器执⾏ EXEC 命令逻辑。

  • 如果客户端状态的 flags 属性不包含 REDIS_MULTI 标识,或者包含 REDIS_DIRTY_CAS 或者 REDIS_DIRTY_EXEC 标识,那么就直接取消事务的执⾏。
  • 否则客户端处于事务状态(flags 有 REDIS_MULTI 标识),服务器会遍历客户端的事务队列,然后执⾏事务队列中的所有命令,最后将返回结果全部返回给客户端;

redis 不⽀持事务回滚机制,但是它会检查每⼀个事务中的命令是否错误。

Redis 事务不⽀持检查那些程序员⾃⼰逻辑错误。例如对 String 类型的数据库键执⾏对 HashMap 类型的操作!

  • WATCH 命令是⼀个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)⾏为。可以监控⼀个或多个键,⼀旦其中有⼀个键被修改(或删除),之后的事务就不会执⾏,监控⼀直持续到 EXEC 命令。
  • MULTI 命令⽤于开启⼀个事务,它总是返回 OK。MULTI 执⾏之后,客户端可以继续向服务器发送任意多条命令,这些命令不会⽴即被执⾏,⽽是被放到⼀个队列中,当 EXEC 命令被调⽤时,所有队列中的命令才会被执⾏。
  • EXEC:执⾏所有事务块内的命令。返回事务块内所有命令的返回值,按命令执⾏的先后顺序排列。当操作被打断时,返回空值 nil 。
  • 通过调⽤ DISCARD,客户端可以清空事务队列,并放弃执⾏事务, 并且客户端会从事务状态中退出。
  • UNWATCH 命令可以取消 watch 对所有 key 的监控。

Redis 主从复制的核⼼原理

通过执⾏ slaveof 命令或设置 slaveof 选项,让⼀个服务器去复制另⼀个服务器的数据。主数据库可以进⾏读写操作,当写操作导致数据变化时会⾃动将数据同步给从数据库。⽽从数据库⼀般是只读的,并接受主数据库同步过来的数据。⼀个主数据库可以拥有多个从数据库,⽽⼀个从数据库只能拥有⼀个主数据库。

全量复制:

  1. 主节点通过 bgsave 命令 fork ⼦进程进⾏ RDB 持久化,该过程是⾮常消耗 CPU、内存(⻚表复制)、硬盘 IO 的
  2. 主节点通过⽹络将 RDB ⽂件发送给从节点,对主从节点的带宽都会带来很⼤的消耗
  3. 从节点清空⽼数据、载⼊新 RDB ⽂件的过程是阻塞的,⽆法响应客户端的命令;如果从节点执⾏ bgrewriteaof,也会带来额外的消耗

部分复制:

  1. 复制偏移量:执⾏复制的双⽅,主从节点,分别会维护⼀个复制偏移量 offset

  2. 复制积压缓冲区:主节点内部维护了⼀个固定⻓度的、先进先出(FIFO)队列 作为复制积压缓冲区,当主从节点 offset 的差距过⼤超过缓冲区⻓度时,将⽆法执⾏部分复制,只能执⾏全量复制。

  3. 服务器运⾏ ID(runid):每个 Redis 节点,都有其运⾏ ID,运⾏ ID 由节点在启动时⾃动⽣成,主节点会将⾃⼰的运⾏ ID 发送给从节点,从节点会将主节点的运⾏ ID 存起来。 从节点 Redis 断开重连的时候,就是根据运⾏ ID 来判断同步的进度:

    • 如果从节点保存的 runid 与主节点现在的 runid 相同,说明主从节点之前同步过,主节点会继续尝试使⽤部分复制(到底能不能部分复制还要看 offset 和复制积压缓冲区的情况);
    • 如果从节点保存的 runid 与主节点现在的 runid 不同,说明从节点在断线前同步的 Redis 节点并不是当前的主节点,只能进⾏全量复制。

Redis 有哪些数据结构?分别有哪些典型的应⽤场景?

Redis 的数据结构有:

  1. 字符串:可以⽤来做最简单的数据,可以缓存某个简单的字符串,也可以缓存某个 json 格式的字符串,Redis 分布式锁的实现就利⽤了这种数据结构,还包括可以实现计数器、Session 共享、分布式 ID
  2. 哈希表:可以⽤来存储⼀些 key-value 对,更适合⽤来存储对象
  3. 列表:Redis 的列表通过命令的组合,既可以当做栈,也可以当做队列来使⽤,可以⽤来缓存类似微信公众号、微博等消息流数据
  4. 集合:和列表类似,也可以存储多个元素,但是不能重复,集合可以进⾏交集、并集、差集操作,从⽽可以实现类似,我和某⼈共同关注的⼈、朋友圈点赞等功能
  5. 有序集合:集合是⽆序的,有序集合可以设置顺序,可以⽤来实现排⾏榜功能

Redis 分布式锁底层是如何实现的?

  1. ⾸先利⽤ setnx 来保证:如果 key 不存在才能获取到锁,如果 key 存在,则获取不到锁
  2. 然后还要利⽤ lua 脚本来保证多个 redis 操作的原⼦性
  3. 同时还要考虑到锁过期,所以需要额外的⼀个看⻔狗定时任务来监听锁是否需要续约
  4. 同时还要考虑到 redis 节点挂掉后的情况,所以需要采⽤红锁的⽅式来同时向 N/2+1 个节点申请锁,都申请到了才证明获取锁成功,这样就算其中某个 redis 节点挂掉了,锁也不能被其他客户端获取到

Redis 主从复制的核⼼原理

Redis 的主从复制是提⾼ Redis 的可靠性的有效措施,主从复制的流程如下:

  1. 集群启动时,主从库间会先建⽴连接,为全量复制做准备
  2. 主库将所有数据同步给从库。从库收到数据后,在本地完成数据加载,这个过程依赖于内存快照 RDB
  3. 在主库将数据同步给从库的过程中,主库不会阻塞,仍然可以正常接收请求。否则,redis 的服务就被中断了。但是,这些请求中的写操作并没有记录到刚刚⽣成的 RDB ⽂件中。为了保证主从库的数据⼀致性,主库会在内存中⽤专⻔的 replication buffer,记录 RDB ⽂件⽣成收到的所有写操作。
  4. 最后,也就是第三个阶段,主库会把第⼆阶段执⾏过程中新收到的写命令,再发送给从库。具体的操作是,当主库完成 RDB ⽂件发送后,就会把此时 replocation buffer 中修改操作发送给从库,从库再执⾏这些操作。这样⼀来,主从库就实现同步了
  5. 后续主库和从库都可以处理客户端读操作,写操作只能交给主库处理,主库接收到写操作后,还会将写操作发送给从库,实现增量同步

Redis 集群策略

Redis 提供了三种集群策略:

  1. 主从模式:这种模式⽐较简单,主库可以读写,并且会和从库进⾏数据同步,这种模式下,客户端直接连主库或某个从库,但是但主库或从库宕机后,客户端需要⼿动修改 IP,另外,这种模式也⽐较难进⾏扩容,整个集群所能存储的数据受到某台机器的内存容量,所以不可能⽀持特⼤数据量
  2. 哨兵模式:这种模式在主从的基础上新增了哨兵节点,但主库节点宕机后,哨兵会发现主库节点宕机,然后在从库中选择⼀个库作为进的主库,另外哨兵也可以做集群,从⽽可以保证但某⼀个哨兵节点宕机后,还有其他哨兵节点可以继续⼯作,这种模式可以⽐较好的保证 Redis 集群的⾼可⽤,但是仍然不能很好的解决 Redis 的容量上限问题。
  3. Cluster 模式:Cluster 模式是⽤得⽐较多的模式,它⽀持多主多从,这种模式会按照 key 进⾏槽位的分配,可以使得不同的 key 分散到不同的主节点上,利⽤这种模式可以使得整个集群⽀持更⼤的数据容量,同时每个主节点可以拥有⾃⼰的多个从节点,如果该主节点宕机,会从它的从节点中选举⼀个新的主节点。

对于这三种模式,如果 Redis 要存的数据量不⼤,可以选择哨兵模式,如果 Redis 要存的数据量⼤,并且需要持续的扩容,那么选择 Cluster 模式。

缓存穿透、缓存击穿、缓存雪崩分别是什么

缓存中存放的⼤多都是热点数据,⽬的就是防⽌请求可以直接从缓存中获取到数据,⽽不⽤访问 Mysql。

  1. 缓存雪崩:如果缓存中某⼀时刻⼤批热点数据同时过期,那么就可能导致⼤量请求直接访问 Mysql 了,解决办法就是在过期时间上增加⼀点随机值,另外如果搭建⼀个⾼可⽤的 Redis 集群也是防⽌缓存雪崩的有效⼿段
  2. 缓存击穿:和缓存雪崩类似,缓存雪崩是⼤批热点数据失效,⽽缓存击穿是指某⼀个热点 key 突然失效,也导致了⼤量请求直接访问 Mysql 数据库,这就是缓存击穿,解决⽅案就是考虑这个热点 key 不设过期时间
  3. 缓存穿透:假如某⼀时刻访问 redis 的⼤量 key 都在 redis 中不存在(⽐如⿊客故意伪造⼀些乱七⼋糟的 key),那么也会给数据造成压⼒,这就是缓存穿透,解决⽅案是使⽤布隆过滤器,它的作⽤就是如果它认为⼀个 key 不存在,那么这个 key 就肯定不存在,所以可以在缓存之前加⼀层布隆过滤器来拦截不存在的 key

Redis 和 Mysql 如何保证数据⼀致

  1. 先更新 Mysql,再更新 Redis,如果更新 Redis 失败,可能仍然不⼀致
  2. 先删除 Redis 缓存数据,再更新 Mysql,再次查询的时候在将数据添加到缓存中,这种⽅案能解决 1 ⽅案的问题,但是在⾼并发下性能较低,⽽且仍然会出现数据不⼀致的问题,⽐如线程 1 删除了 Redis 缓存数据,正在更新 Mysql,此时另外⼀个查询再查询,那么就会把 Mysql 中⽼数据⼜查到 Redis 中
  3. 延时双删,步骤是:先删除 Redis 缓存数据,再更新 Mysql,延迟⼏百毫秒再删除 Redis 缓存数据,这样就算在更新 Mysql 时,有其他线程读了 Mysql,把⽼数据读到了 Redis 中,那么也会被删除掉,从⽽把数据保持⼀致

Redis 的持久化机制

RDB:Redis DataBase 将某⼀个时刻的内存快照(Snapshot),以⼆进制的⽅式写⼊磁盘。

⼿动触发:

  • save 命令,使 Redis 处于阻塞状态,直到 RDB 持久化完成,才会响应其他客户端发来的命令,所以在⽣产环境⼀定要慎⽤
  • bgsave 命令,fork 出⼀个⼦进程执⾏持久化,主进程只在 fork 过程中有短暂的阻塞,⼦进程创建之后,主进程就可以响应客户端请求了

⾃动触发:

  • save m n :在 m 秒内,如果有 n 个键发⽣改变,则⾃动触发持久化,通过 bgsave 执⾏,如果设置多个、只要满⾜其⼀就会触发,配置⽂件有默认配置(可以注释掉)
  • flushall:⽤于清空 redis 所有的数据库,flushdb 清空当前 redis 所在库数据(默认是 0 号数据库),会清空 RDB ⽂件,同时也会⽣成 dump.rdb、内容为空
  • 主从同步:全量同步时会⾃动触发 bgsave 命令,⽣成 rdb 发送给从节点

优点:

  1. 整个 Redis 数据库将只包含⼀个⽂件 dump.rdb,⽅便持久化。
  2. 容灾性好,⽅便备份。
  3. 性能最⼤化,fork ⼦进程来完成写操作,让主进程继续处理命令,所以是 IO 最⼤化。使⽤单独⼦进程来进⾏持久化,主进程不会进⾏任何 IO 操作,保证了 redis 的⾼性能
  4. 相对于数据集⼤时,⽐ AOF 的启动效率更⾼。

缺点:

  1. 数据安全性低。RDB 是间隔⼀段时间进⾏持久化,如果持久化之间 redis 发⽣故障,会发⽣数据丢失。所以这种⽅式更适合数据要求不严谨的时候)
  2. 由于 RDB 是通过 fork ⼦进程来协助完成数据持久化⼯作的,因此,如果当数据集较⼤时,可能会导致整个服务器停⽌服务⼏百毫秒,甚⾄是 1 秒钟。会占⽤ cpu

AOF:Append Only File 以⽇志的形式记录服务器所处理的每⼀个写、删除操作,查询操作不会记录,以⽂本的⽅式记录,可以打开⽂件看到详细的操作记录,调操作系统命令进程刷盘

  1. 所有的写命令会追加到 AOF 缓冲中。
  2. AOF 缓冲区根据对应的策略向硬盘进⾏同步操作。
  3. 随着 AOF ⽂件越来越⼤,需要定期对 AOF ⽂件进⾏重写,达到压缩的⽬的。
  4. 当 Redis 重启时,可以加载 AOF ⽂件进⾏数据恢复。同步策略:

每秒同步:异步完成,效率⾮常⾼,⼀旦系统出现宕机现象,那么这⼀秒钟之内修改的数据将会丢失

每修改同步:同步持久化,每次发⽣的数据变化都会被⽴即记录到磁盘中,最多丢⼀条不同步:由操作

系统控制,可能丢失较多数据

优点:

  1. 数据安全
  2. 通过 append 模式写⽂件,即使中途服务器宕机也不会破坏已经存在的内容,可以通过 redis-check-aof ⼯具解决数据⼀致性问题。
  3. AOF 机制的 rewrite 模式。定期对 AOF ⽂件进⾏重写,以达到压缩的⽬的

缺点:

  1. AOF ⽂件⽐ RDB ⽂件⼤,且恢复速度慢。
  2. 数据集⼤的时候,⽐ rdb 启动效率低。
  3. 运⾏效率没有 RDB ⾼

对⽐:

AOF ⽂件⽐ RDB 更新频率⾼,优先使⽤ AOF 还原数据。AOF ⽐ RDB 更安全也更⼤

RDB 性能⽐ AOF 好

如果两个都配了优先加载 AOF

Redis 单线程为什么这么快

Redis 基于 Reactor 模式开发了⽹络事件处理器、⽂件事件处理器 fileeventhandler。它是单线程的, 所以 Redis 才叫做单线程的模型,它采⽤ IO 多路复⽤机制来同时监听多个 Socket,根据 Socket 上的事件类型来选择对应的事件处理器来处理这个事件。可以实现⾼性能的⽹络通信模型,⼜可以跟内部其他单线程的模块进⾏对接,保证了 Redis 内部的线程模型的简单性。

⽂件事件处理器的结构包含 4 个部分:多个 Socket、IO 多路复⽤程序、⽂件事件分派器以及事件处理器(命令请求处理器、命令回复处理器、连接应答处理器等)。 多个 Socket 可能并发的产⽣不同的事件,IO 多路复⽤程序会监听多个 Socket,会将 Socket 放⼊⼀个队列中排队,每次从队列中有序、同步取出⼀个 Socket 给事件分派器,事件分派器把 Socket 给对应的事件处理器。

然后⼀个 Socket 的事件处理完之后,IO 多路复⽤程序才会将队列中的下⼀个 Socket 给事件分派器。⽂件事件分派器会根据每个 Socket 当前产⽣的事件,来选择对应的事件处理器来处理。

  1. Redis 启动初始化时,将连接应答处理器跟 AE_READABLE 事件关联。
  2. 若⼀个客户端发起连接,会产⽣⼀个 AE_READABLE 事件,然后由连接应答处理器负责和客户端建⽴连接,创建客户端对应的 socket,同时将这个 socket 的 AE_READABLE 事件和命令请求处理器关联,使 得客户端可以向主服务器发送命令请求。
  3. 当客户端向 Redis 发请求时(不管读还是写请求),客户端 socket 都会产⽣⼀个 AE_READABLE 事件,触发命令请求处理器。处理器读取客户端的命令内容, 然后传给相关程序执⾏。
  4. 当 Redis 服务器准备好给客户端的响应数据后,会将 socket 的 AE_WRITABLE 事件和命令回复处理器关联,当客户端准备好读取响应数据时,会在 socket 产⽣⼀个 AE_WRITABLE 事件,由对应命令回复处 理器处理,即将准备好的响应数据写⼊ socket,供客户端读取。
  5. 命令回复处理器全部写完到 socket 后,就会删除该 socket 的 AE_WRITABLE 事件和命令回复处理器的映射。

单线程快的原因:

  1. 纯内存操作
  2. 核⼼是基于⾮阻塞的 IO 多路复⽤机制
  3. 单线程反⽽避免了多线程的频繁上下⽂切换带来的性能问题

简述 Redis 事务实现

事务开始:MULTI 命令的执⾏,标识着⼀个事务的开始。MULTI 命令会将客户端状态的 flags 属性中打开 REDIS_MULTI 标识来完成的。

命令⼊队:当⼀个客户端切换到事务状态之后,服务器会根据这个客户端发送来的命令来执⾏不同的操作。如果客户端发送的命令为 MULTI、EXEC、WATCH、DISCARD 中的⼀个,⽴即执⾏这个命令,否则将命令放⼊⼀ 个事务队列⾥⾯,然后向客户端返回 QUEUED 回复,如果客户端发送的命令为 EXEC、DISCARD、WATCH、MULTI 四个命令的其中⼀个,那么服务器⽴即执⾏这个命令。如果客户端发送的是四个命令以外的其他命令,那么服务器并不⽴即执⾏这个命令。⾸先检查此命令的格式是否正确,如果不正确,服务器会在客户端状态(redisClient)的 flags 属性关闭 REDIS_MULTI 标识,并且返回错误信息给客户端。如果正确,将这个命令放⼊⼀个事务队列⾥⾯,然后向客户端返回 QUEUED 回复事务队列是按照 FIFO 的⽅式保存⼊队的命令

事务执⾏:客户端发送 EXEC 命令,服务器执⾏ EXEC 命令逻辑。如果客户端状态的 flags 属性不包含 REDIS_MULTI 标识,或者包含 REDIS_DIRTY_CAS 或者 REDIS_DIRTY_EXEC 标识,那么就直接取消事务的执⾏。 否则客户端处于事务状态(flags 有 REDIS_MULTI 标识),服务器会遍历客户端的事务队列,然后执⾏事务队列中的所有命令,最后将返回结果全部返回给客户端;Redis 不⽀持事务回滚机制,但是它会检查每⼀个事务中的命令是否错误。Redis 事务不⽀持检查那些程序员⾃⼰逻辑错误。例如对 String 类型的数据库键执⾏对 HashMap 类型的操作!

什么是 CAP 理论

CAP 理论是分布式领域中⾮常重要的⼀个指导理论,C(Consistency)表示强⼀致性,A(Availability)表示可⽤性,P(Partition Tolerance)表示分区容错性,CAP 理论指出在⽬前的硬件条件下,⼀个分布式系统是必须要保证分区容错性的,⽽在这个前提下,分布式系统要么保证 CP,要么保证 AP,⽆法同时保证 CAP。

分区容错性表示,⼀个系统虽然是分布式的,但是对外看上去应该是⼀个整体,不能由于分布式系统内部的某个结点挂点,或⽹络出现了故障,⽽导致系统对外出现异常。所以,对于分布式系统⽽⾔是⼀定要保证分区容错性的。

强⼀致性表示,⼀个分布式系统中各个结点之间能及时的同步数据,在数据同步过程中,是不能对外提供服务的,不然就会造成数据不⼀致,所以强⼀致性和可⽤性是不能同时满⾜的。

可⽤性表示,⼀个分布式系统对外要保证可⽤。

什么是 BASE 理论

由于不能同时满⾜ CAP,所以出现了 BASE 理论:

  1. BA:Basically Available,表示基本可⽤,表示可以允许⼀定程度的不可⽤,⽐如由于系统故障,请求时间变⻓,或者由于系统故障导致部分⾮核⼼功能不可⽤,都是允许的
  2. S:Soft state:表示分布式系统可以处于⼀种中间状态,⽐如数据正在同步
  3. E:Eventually consistent,表示最终⼀致性,不要求分布式系统数据实时达到⼀致,允许在经过⼀段时间后再达到⼀致,在达到⼀致过程中,系统也是可⽤的

什么是 RPC

RPC,表示远程过程调⽤,对于 Java 这种⾯试对象语⾔,也可以理解为远程⽅法调⽤,RPC 调⽤和 HTTP 调⽤是有区别的,RPC 表示的是⼀种调⽤远程⽅法的⽅式,可以使⽤ HTTP 协议、或直接基于 TCP 协议来实现 RPC,在 Java 中,我们可以通过直接使⽤某个服务接⼝的代理对象来执⾏⽅法,⽽底层则通过构造 HTTP 请求来调⽤远端的⽅法,所以,有⼀种说法是 RPC 协议是 HTTP 协议之上的⼀种协议,也是可以理解的。

数据⼀致性模型有哪些

强⼀致性:当更新操作完成之后,任何多个后续进程的访问都会返回最新的更新过的值,这种是对⽤户最友好的,就是⽤户上⼀次写什么,下⼀次就保证能读到什么。根据 CAP 理论,这种实现需要牺牲可⽤性。

弱⼀致性:系统在数据写⼊成功之后,不承诺⽴即可以读到最新写⼊的值,也不会具体的承诺多久后可以读到。⽤户读到某⼀操作对系统数据的更新需要⼀段时间,我们称这段时间为“不⼀致性窗⼝”。

最终⼀致性:最终⼀致性是弱⼀致性的特例,强调的是所有的数据副本,在经过⼀段时间的同步之后, 最终都能够达到⼀个⼀致的状态。因此,最终⼀致性的本质是需要系统保证最终数据能够达到⼀致,⽽ 不需要实时保证系统数据的强⼀致性。到达最终⼀致性的时间 ,就是不⼀致窗⼝时间,在没有故障发⽣的前提下,不⼀致窗⼝的时间主要受通信延迟,系统负载和复制副本的个数影响。最终⼀致性模型根据其提供的不同保证可以划分为更多的模型,包括因果⼀致性和会话⼀致性等。

分布式 ID 是什么?有哪些解决⽅案?

在开发中,我们通常会需要⼀个唯⼀ ID 来标识数据,如果是单体架构,我们可以通过数据库的主键,或直接在内存中维护⼀个⾃增数字来作为 ID 都是可以的,但对于⼀个分布式系统,就会有可能会出现 ID 冲突,此时有以下解决⽅案:

  1. uuid,这种⽅案复杂度最低,但是会影响存储空间和性能
  2. 利⽤单机数据库的⾃增主键,作为分布式 ID 的⽣成器,复杂度适中,ID ⻓度较之 uuid 更短,但是受到单机数据库性能的限制,并发量⼤的时候,此⽅案也不是最优⽅案
  3. 利⽤ redis、zookeeper 的特性来⽣成 id,⽐如 redis 的⾃增命令、zookeeper 的顺序节点,这种⽅案和单机数据库(mysql)相⽐,性能有所提⾼,可以适当选⽤
  4. 雪花算法,⼀切问题如果能直接⽤算法解决,那就是最合适的,利⽤雪花算法也可以⽣成分布式 ID,底层原理就是通过某台机器在某⼀毫秒内对某⼀个数字⾃增,这种⽅案也能保证分布式架构中的系统 id 唯⼀,但是只能保证趋势递增。业界存在 tinyid、leaf 等开源中间件实现了雪花算法。

分布式锁的使⽤场景是什么?有哪些实现⽅案?

在单体架构中,多个线程都是属于同⼀个进程的,所以在线程并发执⾏时,遇到资源竞争时,可以利⽤ ReentrantLock、synchronized 等技术来作为锁,来控制共享资源的使⽤。

⽽在分布式架构中,多个线程是可能处于不同进程中的,⽽这些线程并发执⾏遇到资源竞争时,利⽤ ReentrantLock、synchronized 等技术是没办法来控制多个进程中的线程的,所以需要分布式锁,意思就是,需要⼀个分布式锁⽣成器,分布式系统中的应⽤程序都可以来使⽤这个⽣成器所提供的锁,从⽽达到多个进程中的线程使⽤同⼀把锁。

⽬前主流的分布式锁的实现⽅案有两种:

  1. zookeeper:利⽤的是 zookeeper 的临时节点、顺序节点、watch 机制来实现的,zookeeper 分布式锁的特点是⾼⼀致性,因为 zookeeper 保证的是 CP,所以由它实现的分布式锁更可靠,不会出现混乱
  2. redis:利⽤ redis 的 setnx、lua 脚本、消费订阅等机制来实现的,redis 分布式锁的特点是⾼可⽤,因为 redis 保证的是 AP,所以由它实现的分布式锁可能不可靠,不稳定(⼀旦 redis 中的数据出现了不⼀致),可能会出现多个客户端同时加到锁的情况

什么是分布式事务?有哪些实现⽅案?

在分布式系统中,⼀次业务处理可能需要多个应⽤来实现,⽐如⽤户发送⼀次下单请求,就涉及到订单系统创建订单、库存系统减库存,⽽对于⼀次下单,订单创建与减库存应该是要同时成功或同时失败的,但在分布式系统中,如果不做处理,就很有可能出现订单创建成功,但是减库存失败,那么解决这类问题,就需要⽤到分布式事务。常⽤解决⽅案有:

  1. 本地消息表:创建订单时,将减库存消息加⼊在本地事务中,⼀起提交到数据库存⼊本地消息表,然后调⽤库存系统,如果调⽤成功则修改本地消息状态为成功,如果调⽤库存系统失败,则由后台定时任务从本地消息表中取出未成功的消息,重试调⽤库存系统

  2. 消息队列:⽬前 RocketMQ 中⽀持事务消息,它的⼯作原理是:

    • a. ⽣产者订单系统先发送⼀条 half 消息到 Broker,half 消息对消费者⽽⾔是不可⻅的
    • b. 再创建订单,根据创建订单成功与否,向 Broker 发送 commit 或 rollback
    • c. 并且⽣产者订单系统还可以提供 Broker 回调接⼝,当 Broker 发现⼀段时间 half 消息没有收到任何操作命令,则会主动调此接⼝来查询订单是否创建成功
    • d. ⼀旦 half 消息 commit 了,消费者库存系统就会来消费,如果消费成功,则消息销毁,分布式事务成功结束
    • e. 如果消费失败,则根据重试策略进⾏重试,最后还失败则进⼊死信队列,等待进⼀步处理
  3. Seata:阿⾥开源的分布式事务框架,⽀持 AT、TCC 等多种模式,底层都是基于两阶段提交理论来实现的

什么是 ZAB 协议

ZAB 协议是 Zookeeper ⽤来实现⼀致性的原⼦⼴播协议,该协议描述了 Zookeeper 是如何实现⼀致性的,分为三个阶段:

  1. 领导者选举阶段:从 Zookeeper 集群中选出⼀个节点作为 Leader,所有的写请求都会由 Leader 节点来处理
  2. 数据同步阶段:集群中所有节点中的数据要和 Leader 节点保持⼀致,如果不⼀致则要进⾏同步
  3. 请求⼴播阶段:当 Leader 节点接收到写请求时,会利⽤两阶段提交来⼴播该写请求,使得写请求像事务⼀样在其他节点上执⾏,达到节点上的数据实时⼀致

但值得注意的是,Zookeeper 只是尽量的在达到强⼀致性,实际上仍然只是最终⼀致性的。

为什么 Zookeeper 可以⽤来作为注册中⼼

可以利⽤ Zookeeper 的临时节点和 watch 机制来实现注册中⼼的⾃动注册和发现,另外 Zookeeper 中的数据都是存在内存中的,并且 Zookeeper 底层采⽤了 nio,多线程模型,所以 Zookeeper 的性能也是⽐较⾼的,所以可以⽤来作为注册中⼼,但是如果考虑到注册中⼼应该是注册可⽤性的话,那么 Zookeeper 则不太合适,因为 Zookeeper 是 CP 的,它注重的是⼀致性,所以集群数据不⼀致时,集群将不可⽤,所以⽤ Redis、Eureka、Nacos 来作为注册中⼼将更合适。

Zookeeper 中的领导者选举的流程是怎样的?

对于 Zookeeper 集群,整个集群需要从集群节点中选出⼀个节点作为 Leader,⼤体流程如下:

  1. 集群中各个节点⾸先都是观望状态(LOOKING),⼀开始都会投票给⾃⼰,认为⾃⼰⽐较适合作为 leader
  2. 然后相互交互投票,每个节点会收到其他节点发过来的选票,然后 pk,先⽐较 zxid,zxid ⼤者获胜,zxid 如果相等则⽐较 myid,myid ⼤者获胜
  3. ⼀个节点收到其他节点发过来的选票,经过 PK 后,如果 PK 输了,则改票,此节点就会投给 zxid 或 myid 更⼤的节点,并将选票放⼊⾃⼰的投票箱中,并将新的选票发送给其他节点
  4. 如果 pk 是平局则将接收到的选票放⼊⾃⼰的投票箱中
  5. 如果 pk 赢了,则忽略所接收到的选票
  6. 当然⼀个节点将⼀张选票放⼊到⾃⼰的投票箱之后,就会从投票箱中统计票数,看是否超过⼀半的节点都和⾃⼰所投的节点是⼀样的,如果超过半数,那么则认为当前⾃⼰所投的节点是 leader
  7. 集群中每个节点都会经过同样的流程,pk 的规则也是⼀样的,⼀旦改票就会告诉给其他服务器,所以最终各个节点中的投票箱中的选票也将是⼀样的,所以各个节点最终选出来的 leader 也是⼀样的,这样集群的 leader 就选举出来了

Zookeeper 集群中节点之间数据是如何同步的

  1. ⾸先集群启动时,会先进⾏领导者选举,确定哪个节点是 Leader,哪些节点是 Follower 和 Observer
  2. 然后 Leader 会和其他节点进⾏数据同步,采⽤发送快照和发送 Diff ⽇志的⽅式
  3. 集群在⼯作过程中,所有的写请求都会交给 Leader 节点来进⾏处理,从节点只能处理读请求
  4. Leader 节点收到⼀个写请求时,会通过两阶段机制来处理
  5. Leader 节点会将该写请求对应的⽇志发送给其他 Follower 节点,并等待 Follower 节点持久化⽇志成功
  6. Follower 节点收到⽇志后会进⾏持久化,如果持久化成功则发送⼀个 Ack 给 Leader 节点
  7. 当 Leader 节点收到半数以上的 Ack 后,就会开始提交,先更新 Leader 节点本地的内存数据
  8. 然后发送 commit 命令给 Follower 节点,Follower 节点收到 commit 命令后就会更新各⾃本地内存数据
  9. 同时 Leader 节点还是将当前写请求直接发送给 Observer 节点,Observer 节点收到 Leader 发过来的写请求后直接执⾏更新本地内存数据
  10. 最后 Leader 节点返回客户端写请求响应成功
  11. 通过同步机制和两阶段提交机制来达到集群中节点数据⼀致

Dubbo ⽀持哪些负载均衡策略

  1. 随机:从多个服务提供者随机选择⼀个来处理本次请求,调⽤量越⼤则分布越均匀,并⽀持按权重设置随机概率
  2. 轮询:依次选择服务提供者来处理请求, 并⽀持按权重进⾏轮询,底层采⽤的是平滑加权轮询算法
  3. 最⼩活跃调⽤数:统计服务提供者当前正在处理的请求,下次请求过来则交给活跃数最⼩的服务器来处理
  4. ⼀致性哈希:相同参数的请求总是发到同⼀个服务提供者

Dubbo 是如何完成服务导出的?

  1. ⾸先 Dubbo 会将程序员所使⽤的@DubboService 注解或@Service 注解进⾏解析得到程序员所定义的服务参数,包括定义的服务名、服务接⼝、服务超时时间、服务协议等等,得到⼀个 ServiceBean。
  2. 然后调⽤ ServiceBean 的 export ⽅法进⾏服务导出
  3. 然后将服务信息注册到注册中⼼,如果有多个协议,多个注册中⼼,那就将服务按单个协议,单个注册中⼼进⾏注册
  4. 将服务信息注册到注册中⼼后,还会绑定⼀些监听器,监听动态配置中⼼的变更
  5. 还会根据服务协议启动对应的 Web 服务器或⽹络框架,⽐如 Tomcat、Netty 等

Dubbo 是如何完成服务引⼊的?

  1. 当程序员使⽤@Reference 注解来引⼊⼀个服务时,Dubbo 会将注解和服务的信息解析出来,得到当前所引⽤的服务名、服务接⼝是什么
  2. 然后从注册中⼼进⾏查询服务信息,得到服务的提供者信息,并存在消费端的服务⽬录中
  3. 并绑定⼀些监听器⽤来监听动态配置中⼼的变更
  4. 然后根据查询得到的服务提供者信息⽣成⼀个服务接⼝的代理对象,并放⼊ Spring 容器中作为 Bean

Dubbo 的架构设计是怎样的?

Dubbo 中的架构设计是⾮常优秀的,分为了很多层次,并且每层都是可以扩展的,⽐如:

  1. Proxy 服务代理层,⽀持 JDK 动态代理、javassist 等代理机制
  2. Registry 注册中⼼层,⽀持 Zookeeper、Redis 等作为注册中⼼
  3. Protocol 远程调⽤层,⽀持 Dubbo、Http 等调⽤协议
  4. Transport ⽹络传输层,⽀持 netty、mina 等⽹络传输框架
  5. Serialize 数据序列化层,⽀持 JSON、Hessian 等序列化机制

各层说明

  • config 配置层:对外配置接⼝,以 ServiceConfig , ReferenceConfig 为中⼼,可以直接初始化配置类,也可以通过 spring 解析配置⽣成配置类
  • proxy 服务代理层:服务接⼝透明代理,⽣成服务的客户端 Stub 和服务器端 Skeleton, 以 ServiceProxy 为中⼼,扩展接⼝为 ProxyFactory
  • registry 注册中⼼层:封装服务地址的注册与发现,以服务 URL 为中⼼,扩展接⼝为 RegistryFactory , Registry , RegistryService
  • cluster 路由层:封装多个提供者的路由及负载均衡,并桥接注册中⼼,以 Invoker 为中⼼,扩展接⼝为 Cluster , Directory , Router , LoadBalance
  • monitor 监控层:RPC 调⽤次数和调⽤时间监控,以 Statistics 为中⼼,扩展接⼝为 MonitorFactory , Monitor , MonitorService
  • protocol 远程调⽤层:封装 RPC 调⽤,以 Invocation , Result 为中⼼,扩展接⼝为 Protocol , Invoker , Exporter
  • exchange 信息交换层:封装请求响应模式,同步转异步,以 Request , Response 为中⼼,扩展接⼝为 Exchanger , ExchangeChannel , ExchangeClient , - ExchangeServer
  • transport ⽹络传输层:抽象 mina 和 netty 为统⼀接⼝,以 Message 为中⼼,扩展接⼝为 Channel , Transporter , Client , Server , Codec
  • serialize 数据序列化层:可复⽤的⼀些⼯具,扩展接⼝为 Serialization , ObjectInput ,ObjectOutput , ThreadPool

关系说明

  • 在 RPC 中,Protocol 是核⼼层,也就是只要有 Protocol + Invoker + Exporter 就可以完成⾮透明的 RPC 调⽤,然后在 Invoker 的主过程上 Filter 拦截点。
  • 图中的 Consumer 和 Provider 是抽象概念,只是想让看图者更直观的了解哪些类分属于客户端与服务器端,不⽤ Client 和 Server 的原因是 Dubbo 在很多场景下都使⽤ Provider, Consumer,Registry, Monitor 划分逻辑拓普节点,保持统⼀概念。
  • ⽽ Cluster 是外围概念,所以 Cluster 的⽬的是将多个 Invoker 伪装成⼀个 Invoker,这样其它⼈只要关注 Protocol 层 Invoker 即可,加上 Cluster 或者去掉 Cluster 对其它层都不会造成影响,因为只有⼀个提供者时,是不需要 Cluster 的。
  • Proxy 层封装了所有接⼝的透明化代理,⽽在其它层都以 Invoker 为中⼼,只有到了暴露给⽤户使⽤时,才⽤ Proxy 将 Invoker 转成接⼝,或将接⼝实现转成 Invoker,也就是去掉 Proxy 层 RPC 是可以 Run 的,只是不那么透明,不那么看起来像调本地服务⼀样调远程服务。
  • ⽽ Remoting 实现是 Dubbo 协议的实现,如果你选择 RMI 协议,整个 Remoting 都不会⽤上,Remoting 内部再划为 Transport 传输层和 Exchange 信息交换层,Transport 层只负责单向消息传输,是对 Mina, Netty, Grizzly 的抽象,它也可以扩展 UDP 传输,⽽ Exchange 层是在传输层之上封装了 Request-Response 语义。
  • Registry 和 Monitor 实际上不算⼀层,⽽是⼀个独⽴的节点,只是为了全局概览,⽤层的⽅式画在⼀起。

负载均衡算法有哪些

1、轮询法:将请求按顺序轮流地分配到后端服务器上,它均衡地对待后端的每⼀台服务器,⽽不关⼼服务器实际的连接数和当前的系统负载。

2、随机法:通过系统的随机算法,根据后端服务器的列表⼤⼩值来随机选取其中的⼀台服务器进⾏访问。由概率统计理论可以得知,随着客户端调⽤服务端的次数增多,其实际效果越来越接近于平均分配调⽤量到后端的每⼀台服务器,也就是轮询的结果。

3、源地址哈希法:源地址哈希的思想是根据获取客户端的 IP 地址,通过哈希函数计算得到的⼀个数值,⽤该数值对服务器列表的⼤⼩进⾏取模运算,得到的结果便是客服端要访问服务器的序号。采⽤源地址哈希法进⾏负载均衡,同⼀ IP 地址的客户端,当后端服务器列表不变时,它每次都会映射到同⼀台后端服务器进⾏访问。

4、加权轮询法:不同的后端服务器可能机器的配置和当前系统的负载并不相同,因此它们的抗压能⼒也不相同。给配置⾼、负载低的机器配置更⾼的权重,让其处理更多的请;⽽配置低、负载⾼的机器,给其分配较低的权重,降低其系统负载,加权轮询能很好地处理这⼀问题,并将请求顺序且按照权重分配到后端。

5、加权随机法:与加权轮询法⼀样,加权随机法也根据后端机器的配置,系统的负载分配不同的权重。不同的是,它是按照权重随机请求后端服务器,⽽⾮顺序。

6、最⼩连接数法:最⼩连接数算法⽐较灵活和智能,由于后端服务器的配置不尽相同,对于请求的处理有快有慢,它是根据后端服务器当前的连接情况,动态地选取其中当前积压连接数最少的⼀台服务器来处理当前的请求,尽可能地提⾼后端服务的利⽤效率,将负责合理地分流到每⼀台服务器。

分布式架构下,Session 共享有什么⽅案

1、采⽤⽆状态服务,抛弃 session

2、存⼊ cookie(有安全⻛险)

3、服务器之间进⾏ Session 同步,这样可以保证每个服务器上都有全部的 Session 信息,不过当服务器数量⽐较多的时候,同步是会有延迟甚⾄同步失败;

4、 IP 绑定策略

使⽤ Nginx (或其他复杂均衡软硬件)中的 IP 绑定策略,同⼀个 IP 只能在指定的同⼀个机器访问,但是这样做失去了负载均衡的意义,当挂掉⼀台服务器的时候,会影响⼀批⽤户的使⽤,⻛险很⼤;

5、使⽤ Redis 存储

把 Session 放到 Redis 中存储,虽然架构上变得复杂,并且需要多访问⼀次 Redis ,但是这种⽅案带来的好处也是很⼤的:

  • 实现了 Session 共享;
  • 可以⽔平扩展(增加 Redis 服务器);
  • 服务器重启 Session 不丢失(不过也要注意 Session 在 Redis 中的刷新/失效机制);
  • 不仅可以跨服务器 Session 共享,甚⾄可以跨平台(例如⽹⻚端和 APP 端)。

如何实现接⼝的幂等性

  • 唯⼀ id。每次操作,都根据操作和内容⽣成唯⼀的 id,在执⾏之前先判断 id 是否存在,如果不存在则执⾏后续操作,并且保存到数据库或者 redis 等。
  • 服务端提供发送 token 的接⼝,业务调⽤接⼝前先获取 token,然后调⽤业务接⼝请求时,把 token 携带过去,服务器判断 token 是否存在 redis 中,存在表示第⼀次请求,可以继续执⾏业务,执⾏业务完成后,最后需要把 redis 中的 token 删除
  • 建去重表。将业务中有唯⼀标识的字段保存到去重表,如果表中存在,则表示已经处理过了
  • 版本控制。增加版本号,当版本号符合时,才能更新数据
  • 状态控制。例如订单有状态已⽀付 未⽀付 ⽀付中 ⽀付失败,当处于未⽀付的时候才允许修改为⽀付中等

简述 zk 的命名服务、配置管理、集群管理

命名服务:

通过指定的名字来获取资源或者服务地址。Zookeeper 可以创建⼀个全局唯⼀的路径,这个路径就可以作为⼀个名字。被命名的实体可以是集群中的机器,服务的地址,或者是远程的对象等。⼀些分布式服务框架(RPC、RMI)中的服务地址列表,通过使⽤命名服务,客户端应⽤能够根据特定的名字来获取资源的实体、服务地址和提供者信息等

配置管理:

实际项⽬开发中,经常使⽤.properties 或者 xml 需要配置很多信息,如数据库连接信息、fps 地址端⼝等等。程序分布式部署时,如果把程序的这些配置信息保存在 zk 的 znode 节点下,当你要修改配置,即 znode 会发⽣变化时,可以通过改变 zk 中某个⽬录节点的内容,利⽤ watcher 通知给各个客户端,从⽽更改配置。

集群管理:

集群管理包括集群监控和集群控制,就是监控集群机器状态,剔除机器和加⼊机器。zookeeper 可以⽅便集群机器的管理,它可以实时监控 znode 节点的变化,⼀旦发现有机器挂了,该机器就会与 zk 断开连接,对应的临时⽬录节点会被删除,其他所有机器都收到通知。新机器加⼊也是类似。

讲下 Zookeeper 中的 watch 机制

客户端,可以通过在 znode 上设置 watch,实现实时监听 znode 的变化

Watch 事件是⼀个⼀次性的触发器,当被设置了 Watch 的数据发⽣了改变的时候,则服务器将这个改变发送给设置了 Watch 的客户端

  1. ⽗节点的创建,修改,删除都会触发 Watcher 事件。
  2. ⼦节点的创建,删除会触发 Watcher 事件。

⼀次性:⼀旦被触发就会移除,再次使⽤需要重新注册,因为每次变动都需要通知所有客户端,⼀次性可以减轻压⼒,3.6.0 默认持久递归,可以触发多次

轻量:只通知发⽣了事件,不会告知事件内容,减轻服务器和带宽压⼒

Watcher 机制包括三个⻆⾊:客户端线程、客户端的 WatchManager 以及 ZooKeeper 服务器

  1. 客户端向 ZooKeeper 服务器注册⼀个 Watcher 监听,
  2. 把这个监听信息存储到客户端的 WatchManager 中
  3. 当 ZooKeeper 中的节点发⽣变化时,会通知客户端,客户端会调⽤相应 Watcher 对象中的回调⽅法。watch 回调是串⾏同步的

Zookeeper 和 Eureka 的区别

zk:CP 设计(强⼀致性),⽬标是⼀个分布式的协调系统,⽤于进⾏资源的统⼀管理。当节点 crash 后,需要进⾏ leader 的选举,在这个期间内,zk 服务是不可⽤的。

eureka:AP 设计(⾼可⽤),⽬标是⼀个服务注册发现系统,专⻔⽤于微服务的服务发现注册。Eureka 各个节点都是平等的,⼏个节点挂掉不会影响正常节点的⼯作,剩余的节点依然可以提供注册和查询服务。⽽ Eureka 的客户端在向某个 Eureka 注册时如果发现连接失败,会⾃动切换⾄其他节点,只要有⼀台 Eureka 还在,就能保证注册服务可⽤(保证可⽤性),只不过查到的信息可能不是最新的(不保证强⼀致性)

同时当 eureka 的服务端发现 85%以上的服务都没有⼼跳的话,它就会认为⾃⼰的⽹络出了问题,就不会从服务列表中删除这些失去⼼跳的服务,同时 eureka 的客户端也会缓存服务信息。eureka 对于服务注册发现来说是⾮常好的选择。

存储拆分后如何解决唯⼀主键问题

  • UUID:简单、性能好,没有顺序,没有业务含义,存在泄漏 mac 地址的⻛险

  • 数据库主键:实现简单,单调递增,具有⼀定的业务可读性,强依赖 db、存在性能瓶颈,存在暴露业务信息的⻛险

  • redis,mongodb,zk 等中间件:增加了系统的复杂度和稳定性

  • 雪花算法

雪花算法原理

第⼀位符号位固定为 0,41 位时间戳,10 位 workId,12 位序列号,位数可以有不同实现。

优点:每个毫秒值包含的 ID 值很多,不够可以变动位数来增加,性能佳(依赖 workId 的实现)。时间戳值在⾼位,中间是固定的机器码,⾃增的序列在低位,整个 ID 是趋势递增的。能够根据业务场景数据库节点布置灵活调整 bit 位划分,灵活度⾼。

缺点:强依赖于机器时钟,如果时钟回拨,会导致重复的 ID ⽣成,所以⼀般基于此的算法发现时钟回拨,都会抛异常处理,阻⽌ ID ⽣成,这可能导致服务不可⽤。

如何解决不使⽤分区键的查询问题

  • 映射:将查询条件的字段与分区键进⾏映射,建⼀张单独的表维护(使⽤覆盖索引)或者在缓存中维护
  • 基因法:分区键的后 x 个 bit 位由查询字段进⾏ hash 后占⽤,分区键直接取 x 个 bit 位获取分区,查询字段进⾏ hash 获取分区,适合⾮分区键查询字段只有⼀个的情况
  • 冗余:查询字段冗余存储

Spring Cloud 有哪些常⽤组件,作⽤是什么?

  1. Eureka:注册中⼼
  2. Nacos:注册中⼼、配置中⼼
  3. Consul:注册中⼼、配置中⼼
  4. Spring Cloud Config:配置中⼼
  5. Feign/OpenFeign:RPC 调⽤
  6. Kong:服务⽹关
  7. Zuul:服务⽹关
  8. Spring Cloud Gateway:服务⽹关
  9. Ribbon:负载均衡
  10. Spring CLoud Sleuth:链路追踪
  11. Zipkin:链路追踪
  12. Seata:分布式事务
  13. Dubbo:RPC 调⽤
  14. Sentinel:服务熔断
  15. Hystrix:服务熔断

如何避免缓存穿透、缓存击穿、缓存雪崩?

缓存雪崩是指缓存同⼀时间⼤⾯积的失效,所以,后⾯的请求都会落到数据库上,造成数据库短时间内承受⼤量请求⽽崩掉。

解决⽅案:

  • 缓存数据的过期时间设置随机,防⽌同⼀时间⼤量数据过期现象发⽣。
  • 给每⼀个缓存数据增加相应的缓存标记,记录缓存是否失效,如果缓存标记失效,则更新数据缓存。
  • 缓存预热互斥锁

缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受⼤量请求⽽崩掉。

解决⽅案:

  • 接⼝层增加校验,如⽤户鉴权校验,id 做基础校验,id<=0 的直接拦截;
  • 从缓存取不到的数据,在数据库中也没有取到,这时也可以将 key-value 对写为 key-null,缓存有效时间可以设置短点,如 30 秒(设置太⻓会导致正常情况也没法使⽤)。这样可以防⽌攻击⽤户反复⽤同⼀个 id 暴⼒攻击
  • 采⽤布隆过滤器,将所有可能存在的数据哈希到⼀个⾜够⼤的 bitmap 中,⼀个⼀定不存在的数据会被这个 bitmap 拦截掉,从⽽避免了对底层存储系统的查询压⼒

缓存击穿是指缓存中没有但数据库中有的数据(⼀般是缓存时间到期),这时由于并发⽤户特别多,同时读缓存没读到数据,⼜同时去数据库去取数据,引起数据库压⼒瞬间增⼤,造成过⼤压⼒。和缓存雪崩不同的是,缓存击穿指并发查同⼀条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从⽽查数据库。

解决⽅案:

  • 设置热点数据永远不过期。加互斥锁

分布式系统中常⽤的缓存⽅案有哪些

  • 客户端缓存:⻚⾯和浏览器缓存,APP 缓存,H5 缓存,localStorage 和 sessionStorage
  • CDN 缓存:内容存储:数据的缓存,内容分发:负载均衡
  • nginx 缓存:静态资源
  • 服务端缓存:本地缓存,外部缓存
  • 数据库缓存:持久层缓存(mybatis,hibernate 多级缓存),mysql 查询缓存
  • 操作系统缓存:PageCache、BufferCache

缓存过期都有哪些策略?

  • 定时过期:每个设置过期时间的 key 都需要创建⼀个定时器,到过期时间就会⽴即清除。该策略可以⽴即清除过期的数据,对内存很友好;但是会占⽤⼤量的 CPU 资源去处理过期的数据,从⽽影响缓存的响应时间和吞吐量
  • 惰性过期:只有当访问⼀个 key 时,才会判断该 key 是否已过期,过期则清除。该策略可以最⼤化地节省 CPU 资源,但是很消耗内存、许多的过期数据都还存在内存中。极端情况可能出现⼤量的过期 key 没有 再次被访问,从⽽不会被清除,占⽤⼤量内存。
  • 定期过期:每隔⼀定的时间,会扫描⼀定数量的数据库的 expires 字典中⼀定数量的 key(是随机的), 并清除其中已过期的 key。该策略是定时过期和惰性过期的折中⽅案。通过调整定时扫描的时间间隔和 每次扫描的限定耗时,可以在不同情况下使得 CPU 和内存资源达到最优的平衡效果。
  • 分桶策略:定期过期的优化,将过期时间点相近的 key 放在⼀起,按时间扫描分桶。

常⻅的缓存淘汰算法

  • FIFO(First In First Out,先进先出),根据缓存被存储的时间,离当前最远的数据优先被淘汰;
  • LRU(LeastRecentlyUsed,最近最少使⽤),根据最近被使⽤的时间,离当前最远的数据优先被淘汰;
  • LFU(LeastFrequentlyUsed,最不经常使⽤),在⼀段时间内,缓存数据被使⽤次数最少的会被淘汰。

布隆过滤器原理,优缺点

  • 位图:int[10],每个 int 类型的整数是 4*8=32 个 bit,则 int[10]⼀共有 320 bit,每个 bit ⾮ 0 即 1,初始化时都是 0
  • 添加数据时:将数据进⾏ hash 得到 hash 值,对应到 bit 位,将该 bit 改为 1,hash 函数可以定义多个,则⼀个数据添加会将多个(hash 函数个数)bit 改为 1,多个 hash 函数的⽬的是减少 hash 碰撞的概率
  • 查询数据:hash 函数计算得到 hash 值,对应到 bit 中,如果有⼀个为 0,则说明数据不在 bit 中,如果都为 1,则该数据可能在 bit 中

优点:

  • 占⽤内存⼩
  • 增加和查询元素的时间复杂度为:O(K), (K 为哈希函数的个数,⼀般⽐较⼩),与数据量⼤⼩⽆关哈希函数相互之间没有关系,⽅便硬件并⾏运算
  • 布隆过滤器不需要存储元素本身,在某些对保密要求⽐较严格的场合有很⼤优势 数据量很⼤时,布隆过滤器可以表示全集
  • 使⽤同⼀组散列函数的布隆过滤器可以进⾏交、并、差运算

缺点:

  • 误判率,即存在假阳性(False Position),不能准确判断元素是否在集合中不能获取元素本身
  • ⼀般情况下不能从布隆过滤器中删除元素

分布式缓存寻址算法

  • hash 算法:根据 key 进⾏ hash 函数运算、结果对分⽚数取模,确定分⽚ 适合固定分⽚数的场景,扩展分⽚或者减少分⽚时,所有数据都需要重新计算分⽚、存储
  • ⼀致性 hash:将整个 hash 值得区间组织成⼀个闭合的圆环,计算每台服务器的 hash 值、映射到圆环中。使⽤相同的 hash 算法计算数据的 hash 值,映射到圆环,顺时针寻找,找到的第⼀个服务器就是数据存储的服务器。新增及减少节点时只会影响节点到他逆时针最近的⼀个服务器之间的值存在 hash 环倾斜的问题,即服务器分布不均匀,可以通过虚拟节点解决
  • hash slot:将数据与服务器隔离开,数据与 slot 映射,slot 与服务器映射,数据进⾏ hash 决定存放的 slot,新增及删除节点时,将 slot 进⾏迁移即可

Spring Cloud 和 Dubbo 有哪些区别?

Spring Cloud 是⼀个微服务框架,提供了微服务领域中的很多功能组件,Dubbo ⼀开始是⼀个 RPC 调⽤框架,核⼼是解决服务调⽤间的问题,Spring Cloud 是⼀个⼤⽽全的框架,Dubbo 则更侧重于服务调⽤,所以 Dubbo 所提供的功能没有 Spring Cloud 全⾯,但是 Dubbo 的服务调⽤性能⽐ Spring Cloud ⾼,不过 Spring Cloud 和 Dubbo 并不是对⽴的,是可以结合起来⼀起使⽤的。

什么是服务雪崩?什么是服务限流?

  1. 当服务 A 调⽤服务 B,服务 B 调⽤ C,此时⼤量请求突然请求服务 A,假如服务 A 本身能抗住这些请求,但是如果服务 C 抗不住,导致服务 C 请求堆积,从⽽服务 B 请求堆积,从⽽服务 A 不可⽤,这就是服务雪崩,解决⽅式就是服务降级和服务熔断。
  2. 服务限流是指在⾼并发请求下,为了保护系统,可以对访问服务的请求进⾏数量上的限制,从⽽防⽌系统不被⼤量请求压垮,在秒杀中,限流是⾮常重要的。

什么是服务熔断?什么是服务降级?区别是什么?

  1. 服务熔断是指,当服务 A 调⽤的某个服务 B 不可⽤时,上游服务 A 为了保证⾃⼰不受影响,从⽽不再调⽤服务 B,直接返回⼀个结果,减轻服务 A 和服务 B 的压⼒,直到服务 B 恢复。
  2. 服务降级是指,当发现系统压⼒过载时,可以通过关闭某个服务,或限流某个服务来减轻系统压⼒,这就是服务降级。

相同点:

  1. 都是为了防⽌系统崩溃
  2. 都让⽤户体验到某些功能暂时不可⽤

不同点:熔断是下游服务故障触发的,降级是为了降低系统负载

SOA、分布式、微服务之间有什么关系和区别?

  1. 分布式架构是指将单体架构中的各个部分拆分,然后部署不同的机器或进程中去,SOA 和微服务基本上都是分布式架构的
  2. SOA 是⼀种⾯向服务的架构,系统的所有服务都注册在总线上,当调⽤服务时,从总线上查找服务信息,然后调⽤
  3. 微服务是⼀种更彻底的⾯向服务的架构,将系统中各个功能个体抽成⼀个个⼩的应⽤程序,基本保持⼀个应⽤对应的⼀个服务的架构

怎么拆分微服务?

拆分微服务的时候,为了尽量保证微服务的稳定,会有⼀些基本的准则:

  1. 微服务之间尽量不要有业务交叉。
  2. 微服务之前只能通过接⼝进⾏服务调⽤,⽽不能绕过接⼝直接访问对⽅的数据。
  3. ⾼内聚,低耦合。

怎样设计出⾼内聚、低耦合的微服务?

⾼内聚低耦合,是⼀种从上⽽下指导微服务设计的⽅法。实现⾼内聚低耦合的⼯具主要有 同步的接⼝调⽤ 和 异步的事件驱动 两种⽅式。

有没有了解过 DDD 领域驱动设计?

什么是 DDD: 在 2004 年,由 Eric Evans 提出了, DDD 是⾯对软件复杂之道。Domain-Driven-Design –Tackling Complexity in the Heart of Software

⼤泥团: 不利于微服务的拆分。⼤泥团结构拆分出来的微服务依然是泥团机构,当服务业务逐渐复杂,这个泥团⼜会膨胀成为⼤泥团。

DDD 只是⼀种⽅法论,没有⼀个稳定的技术框架。DDD 要求领域是跟技术⽆关、跟存储⽆关、跟通信⽆关。

什么是中台?

所谓中台,就是将各个业务线中可以复⽤的⼀些功能抽取出来,剥离个性,提取共性,形成⼀些可复⽤的组件 。 ⼤体上,中台可以分为三类 业务中台、数据中台和技术中台。⼤数据杀熟-数据中台

中台跟 DDD 结合: DDD 会通过限界上下⽂将系统拆分成⼀个⼀个的领域, ⽽这种限界上下⽂,天⽣就成了中台之间的逻辑屏障。

DDD 在技术与资源调度⽅⾯都能够给中台建设提供不错的指导。

DDD 分为战略设计和战术设计。 上层的战略设计能够很好的指导中台划分,下层的战术设计能够很好的指导微服务搭建

你的项⽬中是怎么保证微服务敏捷开发的?

  • 开发运维⼀体化。
  • 敏捷开发: ⽬的就是为了提⾼团队的交付效率,快速迭代,快速试错
  • 每个⽉固定发布新版本,以分⽀的形式保存到代码仓库中。快速⼊职。任务⾯板、站⽴会议。团队⼈员灵活流动,同时形成各个专家代表
  • 测试环境- ⽣产环境 -开发测试环境 SIT-集成测试环境-压测环境 STR-预投产环境-⽣产环境 PRD
  • 晨会、周会、需求拆分会

如何进⾏消息队列选型?

Kafka:

  • 优点: 吞吐量⾮常⼤,性能⾮常好,集群⾼可⽤。
  • 缺点:会丢数据,功能⽐较单⼀。
  • 使⽤场景:⽇志分析、⼤数据采集

RabbitMQ:

  • 优点: 消息可靠性⾼,功能全⾯。
  • 缺点:吞吐量⽐较低,消息积累会严重影响性能。erlang 语⾔不好定制。
  • 使⽤场景:⼩规模场景。

RocketMQ:

  • 优点:⾼吞吐、⾼性能、⾼可⽤,功能⾮常全⾯。
  • 缺点:开源版功能不如云上商业版。官⽅⽂档和周边⽣态还不够成熟。客户端只⽀持 java。
  • 使⽤场景:⼏乎是全场景。

RocketMQ 的事务消息是如何实现的

a. ⽣产者订单系统先发送⼀条 half 消息到 Broker,half 消息对消费者⽽⾔是不可⻅的

b. 再创建订单,根据创建订单成功与否,向 Broker 发送 commit 或 rollback

c. 并且⽣产者订单系统还可以提供 Broker 回调接⼝,当 Broker 发现⼀段时间 half 消息没有收到任何操作命令,则会主动调此接⼝来查询订单是否创建成功

d. ⼀旦 half 消息 commit 了,消费者库存系统就会来消费,如果消费成功,则消息销毁,分布式事务成功结束

e. 如果消费失败,则根据重试策略进⾏重试,最后还失败则进⼊死信队列,等待进⼀步处理

为什么 RocketMQ 不使⽤ Zookeeper 作为注册中⼼呢?

根据 CAP 理论,同时最多只能满⾜两个点,⽽ zookeeper 满⾜的是 CP,也就是说 zookeeper 并不能保证服务的可⽤性,zookeeper 在进⾏选举的时候,整个选举的时间太⻓,期间整个集群都处于不可⽤的状态,⽽这对于⼀个注册中⼼来说肯定是不能接受的,作为服务发现来说就应该是为可⽤性⽽设计。

基于性能的考虑,NameServer 本身的实现⾮常轻量,⽽且可以通过增加机器的⽅式⽔平扩展,增加集群的抗压能⼒,⽽ zookeeper 的写是不可扩展的,⽽ zookeeper 要解决这个问题只能通过划分领域,划分多个 zookeeper 集群来解决,⾸先操作起来太复杂,其次这样还是⼜违反了 CAP 中的 A 的设计,导致 服务之间是不连通的。

持久化的机制来带的问题,ZooKeeper 的 ZAB 协议对每⼀个写请求,会在每个 ZooKeeper 节点上保持写⼀个事务⽇志,同时再加上定期的将内存数据镜像(Snapshot)到磁盘来保证数据的⼀致性和持久性,⽽对于⼀个简单的服务发现的场景来说,这其实没有太⼤的必要,这个实现⽅案太重了。⽽且本身 存储的数据应该是⾼度定制化的。

消息发送应该弱依赖注册中⼼,⽽ RocketMQ 的设计理念也正是基于此,⽣产者在第⼀次发送消息的时候从 NameServer 获取到 Broker 地址后缓存到本地,如果 NameServer 整个集群不可⽤,短时间内对于⽣产者和消费者并不会产⽣太⼤影响。

RocketMQ 的实现原理

RocketMQ 由 NameServer 注册中⼼集群、Producer ⽣产者集群、Consumer 消费者集群和若⼲ Broker(RocketMQ 进程)组成,它的架构原理是这样的:

Broker 在启动的时候去向所有的 NameServer 注册,并保持⻓连接,每 30s 发送⼀次⼼跳

Producer 在发送消息的时候从 NameServer 获取 Broker 服务器地址,根据负载均衡算法选择⼀台服务器来发送消息

Conusmer 消费消息的时候同样从 NameServer 获取 Broker 地址,然后主动拉取消息来消费

RocketMQ 为什么速度快

因为使⽤了顺序存储、Page Cache 和异步刷盘。我们在写⼊ commitlog 的时候是顺序写⼊的,这样⽐随机写⼊的性能就会提⾼很多,写⼊ commitlog 的时候并不是直接写⼊磁盘,⽽是先写⼊操作系统的 PageCache,最后由操作系统异步将缓存中的数据刷到磁盘

消息队列如何保证消息可靠传输

消息可靠传输代表了两层意思,既不能多也不能少。

  1. 为了保证消息不多,也就是消息不能重复,也就是⽣产者不能重复⽣产消息,或者消费者不能重复消费消息
  2. ⾸先要确保消息不多发,这个不常出现,也⽐较难控制,因为如果出现了多发,很⼤的原因是⽣产者⾃⼰的原因,如果要避免出现问题,就需要在消费端做控制
  3. 要避免不重复消费,最保险的机制就是消费者实现幂等性,保证就算重复消费,也不会有问题,通过幂等性,也能解决⽣产者重复发送消息的问题
  4. 消息不能少,意思就是消息不能丢失,⽣产者发送的消息,消费者⼀定要能消费到,对于这个问题,就要考虑两个⽅⾯
  5. ⽣产者发送消息时,要确认 broker 确实收到并持久化了这条消息,⽐如 RabbitMQ 的 confirm 机制,Kafka 的 ack 机制都可以保证⽣产者能正确的将消息发送给 broker
  6. broker 要等待消费者真正确认消费到了消息时才删除掉消息,这⾥通常就是消费端 ack 机制,消费者接收到⼀条消息后,如果确认没问题了,就可以给 broker 发送⼀个 ack,broker 接收到 ack 后才会删除消息

消息队列有哪些作⽤

  1. 解耦:使⽤消息队列来作为两个系统之间的通讯⽅式,两个系统不需要相互依赖了
  2. 异步:系统 A 给消息队列发送完消息之后,就可以继续做其他事情了
  3. 流量削峰:如果使⽤消息队列的⽅式来调⽤某个系统,那么消息将在队列中排队,由消费者⾃⼰控制消费速度

死信队列是什么?延时队列是什么?

  1. 死信队列也是⼀个消息队列,它是⽤来存放那些没有成功消费的消息的,通常可以⽤来作为消息重试
  2. 延时队列就是⽤来存放需要在指定时间被处理的元素的队列,通常可以⽤来处理⼀些具有过期性操作的业务,⽐如⼗分钟内未⽀付则取消订单

如何保证消息的⾼效读写?

零拷⻉: kafka 和 RocketMQ 都是通过零拷⻉技术来优化⽂件读写。

传统⽂件复制⽅式: 需要对⽂件在内存中进⾏四次拷⻉。

零拷⻉: 有两种⽅式, mmap 和 transfile,Java 当中对零拷⻉进⾏了封装, Mmap ⽅式通过

MappedByteBuffer 对象进⾏操作,⽽ transfile 通过 FileChannel 来进⾏操作。Mmap 适合⽐较⼩的⽂件,通常⽂件⼤⼩不要超过 1.5G ~2G 之间。Transfile 没有⽂件⼤⼩限制。RocketMQ 当中使⽤ Mmap ⽅式来对他的⽂件进⾏读写。

在 kafka 当中,他的 index ⽇志⽂件也是通过 mmap 的⽅式来读写的。在其他⽇志⽂件当中,并没有使⽤零拷⻉的⽅式。Kafka 使⽤ transfile ⽅式将硬盘数据加载到⽹卡。

epoll 和 poll 的区别

  1. select 模型,使⽤的是数组来存储 Socket 连接⽂件描述符,容量是固定的,需要通过轮询来判断是否发⽣了 IO 事件
  2. poll 模型,使⽤的是链表来存储 Socket 连接⽂件描述符,容量是不固定的,同样需要通过轮询来判断是否发⽣了 IO 事件
  3. epoll 模型,epoll 和 poll 是完全不同的,epoll 是⼀种事件通知模型,当发⽣了 IO 事件时,应⽤程序才进⾏ IO 操作,不需要像 poll 模型那样主动去轮询

TCP 的三次握⼿和四次挥⼿

TCP 协议是 7 层⽹络协议中的传输层协议,负责数据的可靠传输。

在建⽴ TCP 连接时,需要通过三次握⼿来建⽴,过程是:

  1. 客户端向服务端发送⼀个 SYN
  2. 服务端接收到 SYN 后,给客户端发送⼀个 SYN_ACK
  3. 客户端接收到 SYN_ACK 后,再给服务端发送⼀个 ACK

在断开 TCP 连接时,需要通过四次挥⼿来断开,过程是:

  1. 客户端向服务端发送 FIN
  2. 服务端接收 FIN 后,向客户端发送 ACK,表示我接收到了断开连接的请求,客户端你可以不发数据了,不过服务端这边可能还有数据正在处理
  3. 服务端处理完所有数据后,向客户端发送 FIN,表示服务端现在可以断开连接
  4. 客户端收到服务端的 FIN,向服务端发送 ACK,表示客户端也会断开连接了

浏览器发出⼀个请求到收到响应经历了哪些步骤?

  1. 浏览器解析⽤户输⼊的 URL,⽣成⼀个 HTTP 格式的请求
  2. 先根据 URL 域名从本地 hosts ⽂件查找是否有映射 IP,如果没有就将域名发送给电脑所配置的 DNS 进⾏域名解析,得到 IP 地址
  3. 浏览器通过操作系统将请求通过四层⽹络协议发送出去
  4. 途中可能会经过各种路由器、交换机,最终到达服务器
  5. 服务器收到请求后,根据请求所指定的端⼝,将请求传递给绑定了该端⼝的应⽤程序,⽐如 8080 被 tomcat 占⽤了
  6. tomcat 接收到请求数据后,按照 http 协议的格式进⾏解析,解析得到所要访问的 servlet
  7. 然后 servlet 来处理这个请求,如果是 SpringMVC 中的 DispatcherServlet,那么则会找到对应的 Controller 中的⽅法,并执⾏该⽅法得到结果
  8. Tomcat 得到响应结果后封装成 HTTP 响应的格式,并再次通过⽹络发送给浏览器所在的服务器
  9. 浏览器所在的服务器拿到结果后再传递给浏览器,浏览器则负责解析并渲染

跨域请求是什么?有什么问题?怎么解决?

跨域是指浏览器在发起⽹络请求时,会检查该请求所对应的协议、域名、端⼝和当前⽹⻚是否⼀致,如果不⼀致则浏览器会进⾏限制,⽐如在www.baidu.com的某个⽹⻚中,如果使⽤ajax去访问www.jd.com是不⾏的,但是如果是img、iframe、script等标签的src属性去访问则是可以的,之所以浏览器要做这层限制,是为了⽤户信息安全。但是如果开发者想要绕过这层限制也是可以的:

  1. response 添加 header,⽐如 resp.setHeader("Access-Control-Allow-Origin", "*");表示可以访问所有⽹站,不受是否同源的限制
  2. jsonp 的⽅式,该技术底层就是基于 script 标签来实现的,因为 script 标签是可以跨域的
  3. 后台⾃⼰控制,先访问同域名下的接⼝,然后在接⼝中再去使⽤ HTTPClient 等⼯具去调⽤⽬标接⼝
  4. ⽹关,和第三种⽅式类似,都是交给后台服务来进⾏跨域访问

零拷⻉是什么

零拷⻉指的是,应⽤程序在需要把内核中的⼀块区域数据转移到另外⼀块内核区域去时,不需要经过先复制到⽤户空间,再转移到⽬标内核区域去了,⽽直接实现转移

上次编辑于: