面试最常被问的 Java 后端题¶
一、Java 基础篇¶
1. Object 有哪些常用方法?大致说一下每个方法的含义¶
java.lang.Object
下面是对应方法的含义。
clone 方法 保护方法,实现对象的浅复制,只有实现了 Cloneable 接口才可以调用该方法,否则抛出 CloneNotSupportedException 异常,深拷贝也需要实现 Cloneable,同时其成员变量为引用类型的也需要实现 Cloneable,然后重写 clone 方法。finalize 方法 该方法和垃圾收集器有关系,判断一个对象是否可以被回收的最后一步就是判断是否重写了此方法。equals 方法 该方法使用频率非常高。一般 equals 和 == 是不一样的,但是在 Object 中两者是一样的。子类一般都要重写这个方法。hashCode 方法
该方法用于哈希查找,重写了 equals 方法一般都要重写 hashCode 方法,这个方法在一些具有哈希功能的 Collection 中用到。
一般必须满足 obj1.equals (obj2)==true
。可以推出 obj1.hashCode ()==obj2.hashCode ()
,但是 hashCode 相等不一定就满足 equals。不过为了提高效率,应该尽量使上面两个条件接近等价。
- JDK 1.6、1.7 默认是返回随机数;
- JDK 1.8 默认是通过和当前线程有关的一个随机数 + 三个确定值,运用 Marsaglia’s xorshift scheme 随机数算法得到的一个随机数。
wait 方法 配合 synchronized 使用,wait 方法就是使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。wait () 方法一直等待,直到获得锁或者被中断。wait (long timeout) 设定一个超时间隔,如果在规定时间内没有获得锁就返回。
调用该方法后当前线程进入睡眠状态,直到以下事件发生。
- 其他线程调用了该对象的 notify 方法;
- 其他线程调用了该对象的 notifyAll 方法;
- 其他线程调用了 interrupt 中断该线程;
- 时间间隔到了。
此时该线程就可以被调度了,如果是被中断的话就抛出一个 InterruptedException 异常。notify 方法 配合 synchronized 使用,该方法唤醒在该对象上 等待队列 中的某个线程(同步队列中的线程是给抢占 CPU 的线程,等待队列中的线程指的是等待唤醒的线程)。notifyAll 方法 配合 synchronized 使用,该方法唤醒在该对象上等待队列中的所有线程。总结 只要把上面几个方法熟悉就可以了,toString 和 getClass 方法可以不用去讨论它们。该题目考察的是对 Object 的熟悉程度,平时用的很多方法并没看其定义但是也在用,比如说:wait () 方法,equals () 方法等。
Class Object is the root of the class hierarchy.Every class has Object as a superclass. All objects, including arrays, implement the methods of this class.
大致意思:Object 是所有类的根,是所有类的父类,所有对象包括数组都实现了 Object 的方法。面试扩散 上面提到了 wait、notify、notifyAll 方法,或许面试官会问你为什么 sleep 方法不属于 Object 的方法呢?因为提到 wait 等方法,所以最好把 synchronized 都说清楚,把线程状态也都说清楚,尝试让面试官跟着你的节奏走。
2. Java 创建对象有几种方式?¶
这题目看似简单,要好好回答起来还是有点小复杂的,我们来看看,到底有哪些方式可以创建对象?
-
- 使用 new 关键字,这也是我们平时使用的最多的创建对象的方式,示例:
-
- 反射方式创建对象,使用 newInstance (),但是得处理两个异常 InstantiationException、IllegalAccessException:
User user=User.class.newInstance();
Object object=(Object)Class.forName("java.lang.Object").newInstance()
-
- 使用 clone 方法,前面题目中 clone 是 Object 的方法,所以所有对象都有这个方法。
-
- 使用反序列化创建对象,调用 ObjectInputStream 类的 readObject () 方法。
我们反序列化一个对象,JVM 会给我们创建一个单独的对象。JVM 创建对象并不会调用任何构造函数。一个对象实现了 Serializable 接口,就可以把对象写入到文件中,并通过读取文件来创建对象。总结 创建对象的方式关键字:new、反射、clone 拷贝、反序列化。
3. 获取一个类对象的方式有哪些?¶
搞清楚类对象和实例对象,但都是对象。
- 第一种:通过类对象的 getClass () 方法获取,细心点的都知道,这个 getClass 是 Object 类里面的方法。
- 第二种:通过类的静态成员表示,每个类都有隐含的静态成员 class。
- 第三种:通过 Class 类的静态方法 forName () 方法获取。
- 面试扩散 可能面试官会问相关的题目,比如:
Class.forName 和 ClassLoader.loadClass 的区别是什么? 参考: 反射中 Class.forName () 和 ClassLoader.loadClass () 的区别
4. ArrayList 和 LinkedList 的区别有哪些?¶
ArrayList¶
- 优点 :ArrayList 是实现了基于动态数组的数据结构,因为地址连续,一旦数据存储好了,查询操作效率会比较高(在内存里是连着放的)。
- 缺点 :因为地址连续,ArrayList 要移动数据,所以插入和删除操作效率比较低。
LinkedList¶
- 优点 :LinkedList 基于链表的数据结构,地址是任意的,所以在开辟内存空间的时候不需要等一个连续的地址。对于新增和删除操作,LinkedList 比较占优势。LinkedList 适用于要头尾操作或插入指定位置的场景。
- 缺点 :因为 LinkedList 要移动指针,所以查询操作性能比较低。适用场景分析 - 当需要对数据进行对随机访问的时候,选用 ArrayList。
- 当需要对数据进行多次增加删除修改时,采用 LinkedList。
如果容量固定,并且只会添加到尾部,不会引起扩容,优先采用 ArrayList。
当然,绝大数业务的场景下,使用 ArrayList 就够了,但需要注意避免 ArrayList 的扩容,以及非顺序的插入。
5. 用过 ArrayList 吗?说一下它有什么特点?¶
只要是搞 Java 的肯定都会回答 “用过”。所以,回答题目的后半部分 ——ArrayList 的特点。可以从这几个方面去回答:
Java 集合框架中的一种存放相同类型的元素数据,是一种变长的集合类,基于定长数组实现,当加入数据达到一定程度后,会实行自动扩容,即扩大数组大小。
底层是使用数组实现,添加元素。
- 如果 add (o),添加到的是数组的尾部,如果要增加的数据量很大,应该使用 ensureCapacity () 方法,该方法的作用是预先设置 ArrayList 的大小,这样可以大大提高初始化速度。
- 如果使用 add (int,o),添加到某个位置,那么可能会挪动大量的数组元素,并且可能会触发扩容机制。
高并发的情况下,线程不安全。多个线程同时操作 ArrayList,会引发不可预知的异常或错误。
ArrayList 实现了 Cloneable 接口,标识着它可以被复制。注意:ArrayList 里面的 clone () 复制其实是浅复制。
6. 有数组了为什么还要搞个 ArrayList 呢?¶
通常我们在使用的时候,如果在不明确要插入多少数据的情况下,普通数组就很尴尬了,因为你不知道需要初始化数组大小为多少,而 ArrayList 可以使用默认的大小,当元素个数到达一定程度后,会自动扩容。
可以这么来理解:我们常说的数组是定死的数组,ArrayList 却是动态数组。
7. 说说什么是 fail-fast?¶
fail-fast 机制是 Java 集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生 fail-fast 事件。
例如:当某一个线程 A 通过 iterator 去遍历某集合的过程中,若该集合的内容被其他线程所改变了,那么线程 A 访问集合时,就会抛出 ConcurrentModificationException 异常,产生 fail-fast 事件。这里的操作主要是指 add、remove 和 clear,对集合元素个数进行修改。
解决办法:建议使用 “java.util.concurrent 包下的类” 去取代 “java.util 包下的类”。
可以这么理解:在遍历之前,把 modCount 记下来 expectModCount,后面 expectModCount 去和 modCount 进行比较,如果不相等了,证明已并发了,被修改了,于是抛出 ConcurrentModificationException 异常。
8. Hashtable 与 HashMap 的区别¶
本来不想这么写标题的,但是无奈,面试官都喜欢这么问 HashMap。
- 出生的版本不一样,Hashtable 出生于 Java 发布的第一版本 JDK 1.0,HashMap 出生于 JDK 1.2。
- 都实现了 Map、Cloneable、Serializable(当前 JDK 版本 1.8)。
- HashMap 继承的是 AbstractMap,并且 AbstractMap 也实现了 Map 接口。Hashtable 继承 Dictionary。
- Hashtable 中大部分 public 修饰普通方法都是 synchronized 字段修饰的,是线程安全的,HashMap 是非线程安全的。
- Hashtable 的 key 不能为 null,value 也不能为 null,这个可以从 Hashtable 源码中的 put 方法看到,判断如果 value 为 null 就直接抛出空指针异常,在 put 方法中计算 key 的 hash 值之前并没有判断 key 为 null 的情况,那说明,这时候如果 key 为空,照样会抛出空指针异常。
- HashMap 的 key 和 value 都可以为 null。在计算 hash 值的时候,有判断,如果
key==null
,则其hash=0
;至于 value 是否为 null,根本没有判断过。 - Hashtable 直接使用对象的 hash 值。hash 值是 JDK 根据对象的地址或者字符串或者数字算出来的 int 类型的数值。然后再使用除留余数法来获得最终的位置。然而除法运算是非常耗费时间的,效率很低。HashMap 为了提高计算效率,将哈希表的大小固定为了 2 的幂,这样在取模预算时,不需要做除法,只需要做位运算。位运算比除法的效率要高很多。
- Hashtable、HashMap 都使用了 Iterator。而由于历史原因,Hashtable 还使用了 Enumeration 的方式。
- 默认情况下,初始容量不同,Hashtable 的初始长度是 11,之后每次扩充容量变为之前的 2n+1(n 为上一次的长度)而 HashMap 的初始长度为 16,之后每次扩充变为原来的两倍。
另外在 Hashtable 源码注释中有这么一句话:
Hashtable is synchronized. If a thread-safe implementation is not needed, it is recommended to use HashMap in place of Hashtable . If a thread-safe highly-concurrent implementation is desired, then it is recommended to use ConcurrentHashMap in place of Hashtable.
大致意思:Hashtable 是线程安全,推荐使用 HashMap 代替 Hashtable;如果需要线程安全高并发的话,推荐使用 ConcurrentHashMap 代替 Hashtable。
这个回答完了,面试官可能会继续问:HashMap 是线程不安全的,那么在需要线程安全的情况下还要考虑性能,有什么解决方式?
这里最好的选择就是 ConcurrentHashMap 了,但面试官肯定会叫你继续说一下 ConcurrentHashMap 数据结构以及底层原理等。
9. HashMap 中的 key 我们可以使用任何类作为 key 吗?¶
平时可能大家使用的最多的就是使用 String 作为 HashMap 的 key,但是现在我们想使用某个自定义类作为 HashMap 的 key,那就需要注意以下几点:
- 如果类重写了 equals 方法,它也应该重写 hashCode 方法。
- 类的所有实例需要遵循与 equals 和 hashCode 相关的规则。
- 如果一个类没有使用 equals,你不应该在 hashCode 中使用它。
- 咱们自定义 key 类的最佳实践是使之为不可变的,这样,hashCode 值可以被缓存起来,拥有更好的性能。不可变的类也可以确保 hashCode 和 equals 在未来不会改变,这样就会解决与可变相关的问题了。
10.HashMap 的长度为什么是 2 的 N 次方呢?¶
为了能让 HashMap 存数据和取数据的效率高,尽可能地减少 hash 值的碰撞,也就是说尽量把数据能均匀的分配,每个链表或者红黑树长度尽量相等。
我们首先可能会想到 %
取模的操作来实现。
下面是回答的重点哟:
取余(%)操作中如果除数是 2 的幂次,则等价于与其除数减一的与(&)操作(也就是说
hash % length == hash &(length - 1)
的前提是 length 是 2 的 n 次方)。并且,采用二进制位操作&
,相对于%
能够提高运算效率。
这就是为什么 HashMap 的长度需要 2 的 N 次方了。
11.HashMap 与 ConcurrentHashMap 的异同¶
- 都是 key-value 形式的存储数据;
- HashMap 是线程不安全的,ConcurrentHashMap 是 JUC 下的线程安全的;
- HashMap 底层数据结构是数组 + 链表(JDK 1.8 之前)。JDK 1.8 之后是数组 + 链表 + 红黑树。当链表中元素个数达到 8 的时候,链表的查询速度不如红黑树快,链表会转为红黑树,红黑树查询速度快;
- HashMap 初始数组大小为 16(默认),当出现扩容的时候,以 0.75 * 数组大小的方式进行扩容;
- ConcurrentHashMap 在 JDK 1.8 之前是采用分段锁来现实的 Segment + HashEntry,Segment 数组大小默认是 16,2 的 n 次方;JDK 1.8 之后,采用 Node + CAS + Synchronized 来保证并发安全进行实现。
12. 红黑树有哪几个特征?¶
紧接上个问题,面试官很有可能会问红黑树,下面把红黑树的几个特征列出来:
如果面试官还要继续问红黑树具体是怎么添加节点和删除节点的,推荐看:
13. 说说你平时是怎么处理 Java 异常的¶
try-catch-finally
- try 块负责监控可能出现异常的代码
- catch 块负责捕获可能出现的异常,并进行处理
- finally 块负责清理各种资源,不管是否出现异常都会执行
- 其中 try 块是必须的,catch 和 finally 至少存在一个标准异常处理流程
抛出异常→捕获异常→捕获成功(当 catch 的异常类型与抛出的异常类型匹配时,捕获成功)→异常被处理,程序继续运行 抛出异常→捕获异常→捕获失败(当 catch 的异常类型与抛出异常类型不匹配时,捕获失败)→异常未被处理,程序中断运行
在开发过程中会使用到自定义异常,在通常情况下,程序很少会自己抛出异常,因为异常的类名通常也包含了该异常的有用信息,所以在选择抛出异常的时候,应该选择合适的异常类,从而可以明确地描述该异常情况,所以这时候往往都是自定义异常。
自定义异常通常是通过继承 java.lang.Exception 类,如果想自定义 Runtime 异常的话,可以继承 java.lang.RuntimeException 类,实现一个无参构造和一个带字符串参数的有参构造方法。
在业务代码里,可以针对性的使用自定义异常。比如说:该用户不具备某某权限、余额不足等。
14.finally 模块执行了吗?是先执行 return 还是先执行 finally 模块?返回什么?¶
public class FinallyDemo {
public String method111() {
String ret = "hello";
try {
return ret;
} finally {
ret = "world";
}
}
}
把 FinallyDemo.java 编译成 class 文件后,找到该 class 文件的当前目录,执行 cmd 命令:
然后打开 test.txt,关键部分内容如下:
发现在字节码指令中,将 hello 保存在本地变量 2 中,然后直到把本地变量 2 加载到操作数栈中,然后就直接出栈,return 回去了,所以本题的返回去的是 hello,但是 finally 代码块也执行了,执行完 finally 模块后再返回一个临时变量 2。
二、JVM 篇¶
1. Java 类加载器有几种?¶
2. 说一下有哪些类加载场景?¶
3. 说说 Java 类加载机制是什么?说说 new 创建一个普通对象的过程?¶
类加载的过程包括了:
加载、验证、准备、解析、初始化。
new 创建一个普通对象的过程如下:
- 检测类是否被加载过
- 为对象分配内存
- 为分配的内存空间初始化为 0
- 对对象进行其他相关设置
- 执行 init 方法
下面用一张图来描述:
4. 说说类的生命周期?¶
注意类生命周期和对象声明周期,类生命周期主要有以下几个阶段:
5. 什么是双亲委派模型?¶
6. 如何破坏双亲委派模型?¶
7. 能不能自己也写一个 java.lang.String 类?¶
可以写,能编译,但是不能 run。禁止使用包名:java. 开头的包名
。
定义一个普通类:
package java.lang;
public class MyTest {
public MyTest() {
}
public MyTest(String str, int a) {
}
public int length(){
return 10;
}
public static void main(String[] args) {
MyTest myTest =new MyTest("lang",1);
myTest.length();
}
}
运行:
具体校验的源码地方:
结论就是定义包目录的时候,不能以 java. 开头。
8. 说一下 JVM 运行时数据区有哪些?分别说一下它们的功能¶
此题我想用我的方法说,不像网上一堆一堆抄书上的,希望能对大家有所帮助,如果没多大帮助,那可以网上找个看看,只能说抱歉了。
下面我们直入主题:
- 每个线程单独拥有的:程序计数器、Java 虚拟机栈、本地方法栈
- 所有线程都共同有的:方法区、堆
如果你在 Java 代码里创建一个线程,相应 JVM 虚拟机中就创建与之对应的程序计数器、Java 虚拟机栈、本地方法栈,同时方法区和堆是在虚拟机启动就已经有了。
程序计数器 可以简单理解为:程序计数器是记录执行到你代码的的第几行了,每个线程各自对应自己的程序计数器。Java 虚拟机栈 虚拟机会为每个线程分配一个虚拟机栈,每个虚拟机栈中都有若干个栈帧,每个栈帧中存储了局部变量表、操作数栈、动态链接、返回地址等。一个栈帧就对应 Java 代码中的一个方法,当线程执行到一个方法时,就代表这个方法对应的栈帧已经进入虚拟机栈并且处于栈顶的位置,每一个 Java 方法从被调用到执行结束,就对应了一个栈帧从入栈到出栈的过程。一个线程的生命周期和与之对应的 Java 虚拟机栈的生命周期相同。
一个线程进来就创建虚拟机栈,该线程调用的方法就是栈帧,进入方法,栈帧就入栈(虚拟机栈),出方法就是出虚拟机栈。可以通过下面两张图进行理解:
本地方法栈 和 Java 虚拟机栈类似,Java 虚拟机栈针对的是 Java 方法,而本地方法栈针对的 native 修饰的方法。堆 JVM 几乎所有的对象的内存分配都在堆里。由于对象是有生命周期的,所以把堆又分成了新生代和老年代。
新生代和老年代大小比例 = 1:2(默认)。新生代又分为 Eden、S0、S1 区域,Ede:S0:S1=8:1:1。
大多数对象在 Eden 区出生和死亡。Eden 区存活下来的对象进入 S0 区,S0 区活下来的对象放到 S1,S1 区活下来的对象放到 S0 区,这过程中 S0 和 S1 至少有一个区域是空着的。并且对象每次倒腾一次自己的年龄就加 1,直达加到 15 岁的时候,就直接入老年代了。有的大对象可以直接进入老年代,条件是把该对象的大小以及达到了能直接进入老年代的条件了(阈值可以设置)。
方法区 先按照图中的关键字回答。但是方法区由于 JDK 版本有所变动。
回答的时候,一定要说一下方法区由于 JDK 版本有所变动。版本变动情况如下:
9. 方法区和永久代有什么区别?¶
之前有小伙伴也问过我,方法区和永久代到底是什么区别?
这么说吧:永久代又叫 Perm 区,只存在于 HotSpot JVM 中,并且只存在于 JDK 1.7 和之前的版本中,JDK 1.8 中已经彻底移除了永久代,JDK 1.8 中引入了一个新的内存区域叫 metaspace。
- 并不是所有的 JVM 中都有永久代,IBM 的 9,Oracle 的 JRocket 都没有永久代。
- 永久代是实现层面的东西。
- 永久代里面存的东西基本上就是方法区规定的那些东西。
因此,我们可以说,在 JDK 1.7 中永久代是方法区的一种实现,当然,在 HotSpot JDK 1.8 中 metaspace 可以看成是方法区的一种实现。
10. JVM 运行时数据区哪些地方会产生内存溢出?¶
简单回答就行: Java 虚拟机栈、本地方法栈、Java 堆、方法区,其实就是除了程序计数器以外的几个都会发生 OOM。
建议把阈值对应的几个区也简要的说一下:
11. 为什么要用 metaspace 替换 permspace 呢?¶
在 JDK 1.8 之前的 HotSpot 实现中,类的元数据如方法数据、方法信息(字节码、栈和变量大小)、运行时常量池、已确定的符号引用和虚方法表等被保存在永久代中,32 位默认永久代的大小为 64M,64 位默认为 85M,可以通过参数 -XX:MaxPermSize
进行设置,一旦类的元数据超过了永久代大小,就会抛出 OOM 异常。
虚拟机团队在 JDK 1.8 的 HotSpot 中,把永久代从 Java 堆中移除了,并把类的元数据直接保存在本地内存区域(堆外内存),称之为元空间 metaspace(本地内存)。即就是说 JDK 1.8 之前,永久代是在虚拟机中的,而 JDK 1.8 引入的元空间是系统的内存的一部分。理论上取决于 32 位 / 64 位系统可虚拟的内存大小,可见也不是无限制的,需要配置参数。
另外一方面,咱们在对永久代进行调优的时候是相当费劲,因为永久代的大小不好控制,涉及到很多因素,比如:类的总数、常量池的大小、方法数量等,最无语的是永久代的内存不够了可能会伴随着一次 Full GC。
下面是 JDK 几个版本中方法区和堆存储的信息的关系:
- 字符串存在永久代中,容易出现性能问题和内存溢出。
- 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
- 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
- 移除永久代是为融合 HotSpot JVM 与 JRockit VM 而做出的努力,因为 JRockit 没有永久代,不需要配置永久代。
12. 熟悉哪些 JVM 调优参数?¶
- 整个堆内存大小
-Xms
(初始堆大小)、-Xmx
(最大堆大小),为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,我们通常把最大、最小设置为相同的值。 - 新生代空间大小 NewRadio:年轻代和年老代将根据默认的比例(1:2)分配堆内存,建议设置为 2 到 4,可以通过调整二者之间的比率 NewRadio 来调整二者之间的大小。也可以针对回收代,比如年轻代,通过
-XX:newSize -XX:MaxNewSize
来设置其绝对大小。同样,为了防止年轻代的堆收缩,我们通常会把-XX:newSize -XX:MaxNewSize
设置一样大小。 - 方法区(元空间) JDK 1.8:
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m
,根据实际情况调整, 可以使用命令jstat -gcutil pid
查看当前使用率,M 对应的列,根据使用率来定制一个具体的值,建议两个值设置成同样大小。JDK 1.7:-XX:MaxPermSize=256m -XX:MaxPermSize=256m
永久带。 - GC 日志
-Xloggc:CATALINA_BASE/logs/gc.log
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
```
记录 GC 日志并不会特别地影响 Java 程序性能,推荐你尽可能记录日志。
- **GC 算法**
```log
-XX:+UseParNewGC
-XX:+CMSParallelRemarkEnabled
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=75
一般来说推荐使用这些配置,但是根据程序不同的特性,其他的也有可能更好。 任何一个 JVM 参数的默认值可以通过
获取,例如:
13. Java 对象的引用类型有哪些?¶
14. JVM 垃圾回收算法有哪些?¶
垃圾回收算法共四种:其实我更愿意说成三种,因为分代回收其实不是算法。
15. 垃圾收集器有哪些?¶
目前常见的有如下几种: Serial 收集器 ParNew 收集器 Parallel scavenge 收集器 Serial Old 收集器 CMS=Concurrent Mark Sweep 收集器 Parallel Old 收集器 G1=Garbage-First 收集器
- 垃圾收集器整合 - G1 是新生代和老年代一起搞,不和别人合伙。
- Serial:CMS 或者 Serial Old
- ParNew:CMS 或者 Serial Old
-
Parallel Scavenge:Parallel Old 或者 Serial Old
-
分代收集器对应 - 新生代收集器:Serial、ParNew、Parallel Scavenge
- 老年代收集器:Serial Old、Parallel Old、CMS
- 整堆收集器:G1
16. 说说 JVM 中内存的分配与回收策略¶
三、Dubbo 篇¶
其实关于 Dubbo 的面试题,我觉得最好的文档应该还是官网,因为官网有中文版,照顾了很多阅读英文文档吃力的小伙伴。但是官网内容挺多的,于是这里就结合官网和平时面试被问的相对较多的题目整理了一下。
1. 说说一次 Dubbo 服务请求流程?¶
2. 说说 Dubbo 工作原理¶
工作原理分 10 层:
- 第一层:service 层,接口层,给服务提供者和消费者来实现的(留给开发人员来实现);
- 第二层:config 层,配置层,主要是对 Dubbo 进行各种配置的,Dubbo 相关配置;
- 第三层:proxy 层,服务代理层,透明生成客户端的 stub 和服务单的 skeleton,调用的是接口,实现类没有,所以得生成代理,代理之间再进行网络通讯、负责均衡等;
- 第四层:registry 层,服务注册层,负责服务的注册与发现;
- 第五层:cluster 层,集群层,封装多个服务提供者的路由以及负载均衡,将多个实例组合成一个服务;
- 第六层:monitor 层,监控层,对 rpc 接口的调用次数和调用时间进行监控;
- 第七层:protocol 层,远程调用层,封装 rpc 调用;
- 第八层:exchange 层,信息交换层,封装请求响应模式,同步转异步;
- 第九层:transport 层,网络传输层,抽象 mina 和 netty 为统一接口;
- 第十层:serialize 层,数据序列化层。 这是个很坑爹的面试题,但是很多面试官又喜欢问,你真的要背么?你能背那还是不错的,我建议不要背,你就想想 Dubbo 服务调用过程中应该会涉及到哪些技术,把这些技术串起来就 OK 了。
面试扩散 如果让你设计一个 RPC 框架,你会怎么做?其实你就把上面这个工作原理中涉及的到技术点总结一下就行了。
3. Dubbo 支持哪些协议?¶
还有三种,混个眼熟就行:Memcached 协议、Redis 协议、Rest 协议。 上图基本上把序列化的方式也罗列出来了。 详细请参考:Dubbo 官网。
4. 注册中心挂了,consumer 还能不能调用 provider?¶
可以。因为刚开始初始化的时候,consumer 会将需要的所有提供者的地址等信息拉取到本地缓存,所以注册中心挂了可以继续通信。但是 provider 挂了,那就没法调用了。 关键字:consumer 本地缓存服务列表。
5. 怎么实现动态感知服务下线的呢?¶
- pull 模式需要客户端定时向注册中心拉取配置;
- push 模式采用注册中心主动推送数据给客户端。 Dubbo ZooKeeper 注册中心采用是事件通知与客户端拉取方式。服务第一次订阅的时候将会拉取对应目录下全量数据,然后在订阅的节点注册一个 watcher。一旦目录节点下发生任何数据变化,ZooKeeper 将会通过 watcher 通知客户端。客户端接到通知,将会重新拉取该目录下全量数据,并重新注册 watcher。利用这个模式,Dubbo 服务就可以做到服务的动态发现。 注意:ZooKeeper 提供了 “心跳检测” 功能,它会定时向各个服务提供者发送一个请求(实际上建立的是一个 socket 长连接),如果长期没有响应,服务中心就认为该服务提供者已经 “挂了”,并将其剔除。
6. Dubbo 负载均衡策略?¶
- 随机(默认):随机来
- 轮训:一个一个来
- 活跃度:机器活跃度来负载
- 一致性 hash:落到同一台机器上
7. Dubbo 容错策略¶
failover cluster 模式 provider 宕机重试以后,请求会分到其他的 provider 上,默认两次,可以手动设置重试次数,建议把写操作重试次数设置成 0。failback 模式 失败自动恢复会在调用失败后,返回一个空结果给服务消费者。并通过定时任务对失败的调用进行重试,适合执行消息通知等操作。failfast cluster 模式 快速失败只会进行一次调用,失败后立即抛出异常。适用于幂等操作、写操作,类似于 failover cluster 模式中重试次数设置为 0 的情况。failsafe cluster 模式 失败安全是指,当调用过程中出现异常时,仅会打印异常,而不会抛出异常。适用于写入审计日志等操作。forking cluster 模式 并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks="2"
来设置最大并行数。broadcacst cluster 模式 广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。
8. Dubbo 动态代理策略有哪些?¶
默认使用 javassist 动态字节码生成,创建代理类,但是可以通过 SPI 扩展机制配置自己的动态代理策略。
9. 说说 Dubbo 与 Spring Cloud 的区别?¶
这是很多面试官喜欢问的问题,本人认为其实他们没什么关联之处,但是硬是要问区别,那就说说吧。 回答的时候主要围绕着四个关键点来说:通信方式、注册中心、监控、断路器,其余像 Spring 分布式配置、服务网关肯定得知道。通信方式 Dubbo 使用的是 RPC 通信;Spring Cloud 使用的是 HTTP RestFul 方式。注册中心 Dubbo 使用 ZooKeeper(官方推荐),还有 Redis、Multicast、Simple 注册中心,但不推荐。Spring Cloud 使用的是 Spring Cloud Netflix Eureka。监控 Dubbo 使用的是 Dubbo-monitor;Spring Cloud 使用的是 Spring Boot admin。断路器 Dubbo 在断路器这方面还不完善,Spring Cloud 使用的是 Spring Cloud Netflix Hystrix。分布式配置、网关服务、服务跟踪、消息总线、批量任务等。Dubbo 目前可以说还是空白,而 Spring Cloud 都有相应的组件来支撑。
10. 说说 TCP 与 UDP 的区别,以及各自的优缺点¶
11. 说一下 HTTP 和 HTTPS 的区别¶
- 端口不同 :HTTP 和 HTTPS 的连接方式不同没用的端口也不一样,HTTP 是 80,HTTPS 用的是 443;
- 消耗资源 :和 HTTP 相比,HTTPS 通信会因为加解密的处理消耗更多的 CPU 和内存资源;
- 开销 :HTTPS 通信需要证书,这类证书通常需要向认证机构申请或者付费购买。
12. 说说 HTTP、TCP、Socket 的关系是什么?¶
- TCP/IP 代表传输控制协议 / 网际协议,指的是一系列协议族。
- HTTP 本身就是一个协议,是从 Web 服务器传输超文本到本地浏览器的传送协议。
- Socket 是 TCP/IP 网络的 API,其实就是一个门面模式,它把复杂的 TCP/IP 协议族隐藏在 Socket 接口后面。对用户来说,一组简单的接口就是全部,让 Socket 去组织数据,以符合指定的协议。 综上所述:
- 需要 IP 协议来连接网络。
- TCP 是一种允许我们安全传输数据的机制,使用 TCP 协议来传输数据的 HTTP 是 Web 服务器和客户端使用的特殊协议。
- HTTP 基于 TCP 协议,所以可以使用 Socket 去建立一个 TCP 连接。
13. 说一下 HTTP 的长连接与短连接的区别¶
HTTP 协议的长连接和短连接,实质上是 TCP 协议的长连接和短连接。短连接 在 HTTP/1.0 中默认使用短链接,也就是说,浏览器和服务器每进行一次 HTTP 操作,就建立一次连接,但任务结束就中断连接。如果客户端访问的某个 HTML 或其他类型的 Web 资源,如 JavaScript 文件、图像文件、CSS 文件等。当浏览器每遇到这样一个 Web 资源,就会建立一个 HTTP 会话。长连接 从 HTTP/1.1 起,默认使用长连接,用以保持连接特性。在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输 HTTP 数据的 TCP 连接不会关闭。如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接。Keep-Alive 不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如 Apache)中设定这个时间。
四、MyBatis 篇¶
1. 说说 MyBatis 的缓存¶
一级缓存 在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的 SQL,MyBatis 提供了一级缓存的方案优化这部分场景,如果是相同的 SQL 语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。 每个 SqlSession 中持有了 Executor,每个 Executor 中有一个 LocalCache。当用户发起查询时,MyBatis 根据当前执行的语句生成 MappedStatement,在 Local Cache 进行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入 Local Cache,最后返回结果给用户。具体实现类的类关系图如下图所示:
- MyBatis 一级缓存的生命周期和 SqlSession 一致。
- MyBatis 一级缓存内部设计简单,只是一个没有容量限定的 HashMap,在缓存的功能性上有所欠缺。
- MyBatis 的一级缓存最大范围是 SqlSession 内部,有多个 SqlSession 或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为 Statement。二级缓存 在上文中提到的一级缓存中,其最大的共享范围就是一个 SqlSession 内部,如果多个 SqlSession 之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用 CachingExecutor 装饰 Executor,进入一级缓存的查询流程前,先在 CachingExecutor 进行二级缓存的查询,具体的工作流程如下所示。 二级缓存开启后,同一个 namespace 下的所有操作语句,都影响着同一个 Cache,即二级缓存被多个 SqlSession 共享,是一个全局的变量。 当开启缓存后,数据的查询执行的流程为:
二级缓存 -> 一级缓存 -> 数据库
- MyBatis 的二级缓存相对于一级缓存来说,实现了 SqlSession 之间缓存数据的共享,同时粒度更加细,能够到 namespace 级别,通过 Cache 接口实现类不同的组合,对 Cache 的可控性也更强。
- MyBatis 在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻。
- 在分布式环境下,由于默认的 MyBatis Cache 实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将 MyBatis 的 Cache 接口实现,有一定的开发成本,直接使用 Redis、Memcached 等分布式缓存可能成本更低,安全性也更高。
2. JDBC 编程有哪些步骤?¶
-
- 装载相应的数据库的 JDBC 驱动并进行初始化:
-
- 建立 JDBC 和数据库之间的 Connection 连接:
Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test?characterEncoding=UTF-8", "root", "123456");
-
- 创建 Statement 或者 PreparedStatement 接口,执行 SQL 语句:
// 查询用户信息
public List<User> findUserList(){
String sql = "select * from t_user order by user_id";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
// 创建一个 List 用于存放查询到的 User 对象
List<User> userList = new ArrayList<>();
try {
conn = DbUtil.getConnection();
pstmt =(PreparedStatement) conn.prepareStatement(sql);
rs =(ResultSet) pstmt.executeQuery();
while(rs.next()){
int courseId = rs.getInt("user_id");
String courseName = rs.getString("user_name");
// 每个记录对应一个 User 对象
User user = new User();
user.setUserId(courseId);
user.setUserName(courseName);
// 将对象放到集合中
userList.add(course);
}
} catch(SQLException e) {
e.printStackTrace();
}finally{
// 资源关闭
DbUtil.close(pstmt);
DbUtil.close(conn);
}
return userList;
}
-
- 处理和显示结果。
-
- 释放资源。
3. 说一下 MyBatis 中使用的 ## 和 $ 有什么区别¶
动态 SQL 是 MyBatis 的主要特性之一,在 mapper 中定义的参数传到 xml 中之后,在查询之前 MyBatis 会对其进行动态解析。
MyBatis 为我们提供了两种支持动态 SQL 的语法:##{}
以及 ${}
。
##{}
是预编译处理,${}
是字符替换。在使用 ##{} 时,MyBatis 会将 SQL 中的 ##{}
替换成 ?
,配合 PreparedStatement 的 set 方法赋值,这样可以有效的防止 SQL 注入,保证程序的运行安全。建议能不要用就不要用,“常在河边走哪能不湿鞋” 。
4. MyBatis 中比如 UserMapper.java 是接口,为什么没有实现类还能调用?¶
UserMapper.xml 中:
反射生成 namespace 的对象:
JDK 动态代理:
Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);
总结:XML 中的 namespace="com.user.UserMapper"
接口 com.user.UserMapper 本身反射 JDK 动态代理实现接口。
5. MyBatis 中见过什么设计模式?¶
五、MySQL 篇¶
1. 简单说说在 MySQL 中执行依据查询 SQL 是如何执行的?¶
- 取得链接,使用使用到 MySQL 中的连接器。
- 查询缓存,key 为 SQL 语句,value 为查询结果,如果查到就直接返回。不建议使用次缓存,在 MySQL 8.0 版本已经将查询缓存删除,也就是说 MySQL 8.0 版本后不存在此功能。
- 分析器,分为词法分析和语法分析。此阶段只是做一些 SQL 解析,语法校验。所以一般语法错误在此阶段。
- 优化器,是在表里有多个索引的时候,决定使用哪个索引;或者一个语句中存在多表关联的时候(join),决定各个表的连接顺序。
- 执行器,通过分析器让 SQL 知道你要干啥,通过优化器知道该怎么做,于是开始执行语句。执行语句的时候还要判断是否具备此权限,没有权限就直接返回提示没有权限的错误;有权限则打开表,根据表的引擎定义,去使用这个引擎提供的接口,获取这个表的第一行,判断 id 是都等于 1。如果是,直接返回;如果不是继续调用引擎接口去下一行,重复相同的判断,直到取到这个表的最后一行,最后返回。 MySQL 的典型的三层结构(连接器 + Server + 执行器):
2. MySQL 有哪些存储引擎?¶
3. MySQL 中 varchar 与 char 的区别?varchar (30) 中的 30 代表的涵义?¶
- varchar 与 char 的区别,char 是一种固定长度的类型,varchar 则是一种可变长度的类型。
- varchar (30) 中 30 的涵义最多存放 30 个字符。varchar (30) 和 (130) 存储 hello 所占空间一样,但后者在排序时会消耗更多内存,因为 ORDER BY col 采用 fixed_length 计算 col 长度(memory 引擎也一样)。
- 对效率要求高用 char,对空间使用要求高用 varchar。
4. int (11) 中的 11 代表什么涵义?¶
int (11) 中的 11,不影响字段存储的范围,只影响展示效果。
5. 为什么 SELECT COUNT (*) FROM table 在 InnoDB 比 MyISAM 慢?¶
对于 SELECT COUNT (*) FROM table 语句,在没有 WHERE 条件的情况下,InnoDB 比 MyISAM 可能会慢很多,尤其在大表的情况下。因为,InnoDB 是去实时统计结果,会 全表扫描 ;而 MyISAM 内部维持了一个计数器,预存了结果,所以直接返回即可。面试扩散 此题还有另外一种问法:SELECT COUNT(*) FROM table
在使用存储引擎 InnoDB 和 MyISAM,谁更快,为什么?
6. 说说数据库的三范式和反模式¶
7. 在设计数据库表的时候,字段用于存储金额、余额时,选择什么类型比较好?¶
- 直接选择 int 或者 bigint 类型,但是得对金额进行乘 100,或者 1000;
- 使用 decimal 类型,避免精度丢失。如果使用 Java 语言时,需要使用 BigDecimal 进行对应,但是使用 BigDecimal 的时候也是容易出问题的,这是 Java 层面的,没遇到坑的,以后要留意点。
8. 大概说说 InnoDB 与 MyISAM 有什么区别?¶
- 在 MySQL 5.1 及之前的版本中,MyISAM 是默认的存储引擎,而在 MySQL 5.5 版本以后,默认使用 InnoDB 存储引擎。
- MyISAM 不支持行级锁,换句话说,MyISAM 会对整张表加锁,而不是针对行。同时,MyISAM 不支持事务和外键。MyISAM 可被压缩,存储空间较小,而且 MyISAM 在筛选大量数据时非常快。
- InnoDB 是事务型引擎,当事务异常提交时,会被回滚。同时,InnoDB 支持行锁。此外,InnoDB 需要更多存储空间,会在内存中建立其专用的缓冲池用于高速缓冲数据和索引。InnoDB 支持自动奔溃恢复特性。 建议:一般情况下,个人建议优先选择 InnoDB 存储引擎,并且尽量不要将 InnoDB 与 MyISAM 混合使用。
9. 什么是索引?¶
索引,类似于书籍的目录,想找到一本书的某个特定的主题,需要先找到书的目录,定位对应的页码。 MySQL 中存储引擎使用类似的方式进行查询,先去索引中查找对应的值,然后根据匹配的索引找到对应的数据行。
10. 索引有什么优缺点?¶
11. MySQL 索引类型有哪些?¶
12. 什么时候不要使用索引?¶
- 经常增删改的列不要建立索引;
- 有大量重复的列不建立索引;
- 表记录太少不要建立索引。
13. 使用 MySQL 的索引应该注意些什么?¶
14. 怎么知道一条查询语句是否用到了索引,用了什么类型的索引?¶
使用方法,在 SELECT 语句前加上 EXPLAIN 执行,查看其执行计划, 可以帮助选择更好的索引和写出更优化的查询语句,explain 执行计划对应每一列的详情,这里就不用再提了,网上一堆资料,但还是推荐看官网:
索引详解 英语好的就直接看官网的说明,英语不好的可以使用浏览器搞个自动翻译的小插件,结合英文同步来看。
15. 说说什么是 MVCC?¶
多版本并发控制(MVCC=Multi-Version Concurrency Control),是一种用来解决读 - 写冲突的无锁并发控制。也就是为事务分配单向增长的时间戳,为每个修改保存一个版本。版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照(复制了一份数据)。这样在读操作不用阻塞写操作,写操作不用阻塞读操作的同时,避免了脏读和不可重复读。
16. MVCC 可以为数据库解决什么问题?¶
在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能。同时还可以解决脏读、幻读、不可重复读等事务隔离问题,但不能解决更新丢失问题。
17. 说说 MVCC 的实现原理¶
MVCC 的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突,它的实现原理主要是依赖记录中的 3 个隐式字段、undo 日志、Read View 来实现的。
18. 什么是死锁?¶
生活中:吃饭使用一双筷子,但是如果你我各一支,你想吃饭我也想吃饭,你的那支不愿意给我,我的那支也不愿给你,这会就死锁了。 虽然进程在运行过程中,可能发生死锁,但死锁的发生也必须具备一定的条件。 死锁的发生必须具备以下四个必要条件:
- 互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用完释放。
- 请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
- 不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
- 环路等待条件:指在发生死锁时,必然存在一个进程 —— 资源的环形链,即进程集合 {P0,P1,P2,…,Pn} 中的 P0 正在等待一个 P1 占用的资源,P1 正在等待 P2 占用的资源,……,Pn 正在等待已被 P0 占用的资源。
19. MySQL 事务隔离级别?¶
- READ UNCOMMITTED(未提交读):事务中的修改,即使没有提交,对其他事务也都是可见的。会导致脏读。
- READ COMMITTED(提交读):事务从开始直到提交之前,所做的任何修改对其他事务都是不可见的。会导致不可重复读。这个隔离级别,也可以叫做 “不可重复读”。
- REPEATABLE READ(可重复读):一个事务按相同的查询条件读取以前检索过的数据,其他事务插入了满足其查询条件的新数据。产生幻行,会导致幻读。(MySQL 默认隔离级别)
- SERIALIZABLE(可串行化):强制事务串行执行。
20. 请说说 MySQL 数据库的锁?¶
关于 MySQL 的锁机制,可能会问很多问题,不过这也得看面试官在这方面的知识储备。 MySQL 中有共享锁和排它锁,也就是读锁和写锁。
- 共享锁:不堵塞,多个用户可以同一时刻读取同一个资源,相互之间没有影响。
- 排它锁:一个写操作阻塞其他的读锁和写锁,这样可以只允许一个用户进行写入,防止其他用户读取正在写入的资源。
- 表锁:系统开销最小,会锁定整张表,MyISAM 使用表锁。
- 行锁:容易出现死锁,发生冲突概率低,并发高,InnoDB 支持行锁(必须有索引才能实现,否则会自动锁全表,那么就不是行锁了)。
21. 说说什么是锁升级?¶
- MySQL 行锁只能加在索引上,如果操作不走索引,就会升级为表锁。因为 InnoDB 的行锁是加在索引上的,如果不走索引,自然就没法使用行锁了,原因是 InnoDB 是将 primary key index 和相关的行数据共同放在 B+ 树的叶节点。InnoDB 一定会有一个 primary key,secondary index 查找的时候,也是通过找到对应的 primary,再找对应的数据行。
- 当非唯一索引上记录数超过一定数量时,行锁也会升级为表锁。测试发现当非唯一索引相同的内容不少于整个表记录的二分之一时会升级为表锁。因为当非唯一索引相同的内容达到整个记录的二分之一时,索引需要的性能比全文检索还要大,查询语句优化时会选择不走索引,造成索引失效,行锁自然就会升级为表锁。
22. 说说悲观锁和乐观锁¶
悲观锁 说的是数据库被外界(包括本系统当前的其他事物以及来自外部系统的事务处理)修改保持着保守态度,因此在整个数据修改过程中,将数据处于锁状态。悲观的实现往往是依靠数据库提供的锁机制,也只有数据库层面提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统汇总实现了加锁机制,也是没有办法保证系统不会修改数据。 在悲观锁的情况下,为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其它事务无法修改这些数据。修改删除数据时也要加锁,其它事务无法读取这些数据。乐观锁 相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。 而乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本(Version)记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
23. 怎样尽量避免死锁的出现?¶
- 设置获取锁的超时时间,至少能保证最差情况下,可以退出程序,不至于一直等待导致死锁;
- 设置按照同一顺序访问资源,类似于串行执行;
- 避免事务中的用户交叉;
- 保持事务简短并在一个批处理中;
- 使用低隔离级别;
- 使用绑定链接。
六、RabbitMQ 篇¶
1. 看你简历上写了 RabbitMQ,通常会问:为什么要用 RabbitMQ?¶
异步 场景:需要把 A 系统信息发给 B、C、D 系统。 用户发起请求总耗时 380,调用 A 系统耗时 80ms,A 系统调用 B 系统 100ms,A 系统调用 C 系统耗时 110ms,A 系统调用 D 系统耗时 90ms。其中 A 系统只管把数据推送出去就行。 如果引入 RabbitMQ 后,该请求总耗时 = A 系统耗时 + 发送 RabbitMQ 耗时,总耗时从 380ms 到 120ms 了。 可以看到通过 RabbitMQ 的异步功能,可以大大提高接口的性能。
解耦 场景:业务需要把用户信息从 A 系统推送到 B 系统和 C 系统,流程如下: 但是这回业务变化了,还需要把 A 系统把用户信息推送给 D 系统和 E 系统。如果还按照上面系统那种模式,那么 A 系统得又得新开发。就变成下面这样: 如果引入 RabbitMQ 的话: 往后不管有多少个系统需要 A 系统推送用户信息,A 系统就不用变,只是对应系统去 RabbitMQ 取数据就可以搞定了。
削峰 场景:A 系统常规情况下,每秒并发 200,但是高峰时间可能会到几千或者上万,但是数据库每秒只能处理 1000 左右,多了会把数据库搞死。正常情况下: 如果引入 RabbitMQ 的话,就是先把消息给 RabbitMQ,然后慢慢入库,这样数据库就不会有太大压力了。 削峰就类似于银行办理业务,正常情况下,几个窗口够用,但是在节假日去银行办理业务的人太多了,几个窗口就支持不了,窗口边会站满了人。于是就可以采取取号制度,先拿号码去大厅坐着,我们一个一个来。(如有觉得场景不妥的,望谅解) 上面三种场景能回答上来,基本上就可以避免 “我们领导叫我们这么用的” 尴尬场面。 真实场景:
- 用户付款成功,发一条站内信,付款成功;
- 用户注册成功,发一个短信或者邮件,提示注册成功;
- 用户每次操作日志,记录好用户行为。
到这里相信大家面试的时候,就算你没用过,编也能编出一个你项目中的场景了吧。
2. 可能你讲了上面三个 RabbitMQ 的优点后,会继续问:使用 RabbitMQ 容易带来什么问题?¶
3. 那么多消息队列,为什么选 RabbitMQ 呢?¶
这里就得把常见的消息队列都列出来进行对比,如果是做技术选型的话,这也是必须要考察的。短期为了面试,长期为了在做架构时的技术选型。
4. RabbitMQ 中什么是死信队列?¶
DLX=Dead-Letter-Exchange。利用 DLX,当消息在一个队列中变成死信(dead message)之后,它能被重新 publish 到另一个 Exchange,这个 Exchange 就是 DLX。 从字面上就看得出,死信就是无法被消费的消息。producer 将消息传给 broker 或者 queue 中,consumer 从 queue 取出消息进行消费。但是在某些特殊情况下会导致 queue 中部分消息无法被 consumer 消费,这样一直没有被消费的消息就变成了私信,既然有私信了,相应也有了死信队列。 消息变成死信一般有以下几种情况:
- 消息被拒绝(basic.reject/basic.nack)并且 requeue=false。
- 消息 TTL 过期(消息超时进入死信队列)。
- 队列达到最大长度。
5. 如何处理死信队列?¶
关于死信的出现既然不可避免,那么就需要从实际业务场景去考虑这个问题。对这些私信怎么处理,常见的处理办法有以下几种:
- 简单粗暴的丢弃,这是针对那些不是很重要的消息,可以有选择地丢弃;
- 把死信记录在数据里,然后针对业务进行分析怎么处理这些死信;
- 通过死信队列,由负责监听死信队列的应用程序进行处理(推荐)。
6. 怎么保证消息不会被丢失?¶
有三种场景会导致消息丢失: 总结为:生产者搞丢数据、RabbitMQ 搞丢数据、消费者搞丢数据。
生产者搞丢数据 事务功能机制 使用 RabbitMQ 的事务功能,就是生产者发送数据之前开启 RabbitMQ 事务 channel.txSelect,然后发送消息,如果消息没有成功被 RabbitMQ 接收到,那么生产者会收到异常报错,此时就可以回滚事务 channel.txRollback,然后重试发送消息;如果收到了消息,那么可以提交事务 channel.txCommit。但是这种方案是存在问题的,即 RabbitMQ 事务机制(同步)一搞,基本上 吞吐量会下来,因为 太耗性能 。confirm 机制 在生产者那里设置开启 confirm 模式后,你每次写的消息都会分配一个唯一的 ID。如果写入 RabbitMQ 成功后会回传一个 ack 消息,告诉你这个消息已经到达 RabbitMQ 了;如果没收到你的消息或者失败了,则会回调你的一个 nack,告诉你这个消息 RabbitMQ 接受失败,然后你就可以继续重试发送,而且你可以结合这个机制在内存里维护一个 ID 的状态。如果超过一定时间没收到回调,那么就可以再次发送消息。所以一般在生产者这方避免数据丢失,都是使用 confirm 机制。事务机制 PK confirm 模式 事务机制是同步的,提交事务后会阻塞在那里等待。confirm 是异步的,发送这个消息后就可以发送下一个消息了。消息被 RabbitMQ 接收之后会异步回调一个通知,告知你这个消息已接收到了。
RabbitMQ 搞丢数据 RabbitMQ 接收到消息,默认是放在内存里,如果系统挂了或者重启,那对应的消息就会丢失。所以选择开启持久化,把消息写入磁盘中,这样就算系统挂了或者重启都不会丢失消息。 持久化的两个步骤:
- 创建 queue 的时候将其设置为持久化,这样就可以保证 RabbitMQ 持久化 queue 的元数据,但是它不会持久化 queue 里的数据。
- 将消息 deliveryMode=2,即将消息设置为持久化,此时 RabbitMQ 会将消息持久化到磁盘中去。 上面两个持久化必须同时设置才行。这个 RabbitMQ 就算挂了,再次重启的时候也会从磁盘上重启恢复 queue 和 queue 里的数据。 持久化和 confirm 模式一起使用,就算在消息持久化之前,RabbitMQ 挂了、数据丢了、生产者收不到 ack 的通知时,咱们也可以选择重新发送数据。
消费端搞丢消息数据 消费端代码中可能有 bug,异常没有处理导致消费失败,或者系统重启、挂了等,那么 RabbitMQ 认为咱们已经消费了,所以对应消息数据就会丢失了。 这时候我们就得使用 RabbitMQ 的 ack 机制。得把自动 ack 关闭,有个 api 直接调用,然后在自己代码里,确保消费者真的成功消费完成后,再进行一个手动 ack。
7. RabbitMQ 怎么高可用呢?¶
单机模式 不属于高可用,单机模式就是启动单个 RabbitMQ 节点,一般用于本地开发或者测试环境。实际生产上,基本不会使用这种单节点模式。
普通集群模式 不属于高可用,普通集群模式就是在多台机器上启动多个 RabbitMQ 实例,每个机器各自启动一个。你创建的 queue,只会放在一个 RabbitMQ 实例上,但是每个实例都是同步 queue 的元数据(元数据可以认为是 queue 的一些配置基本信息,通过元数据,可以找到 queue 所在的实例)。你消费的时候,如果连接到另外一个实例,那个实例就会从 queue 所在的实例上把数据拉取过来。
上面这种普通集群方式确实很麻烦,给人的感觉不是很好,没有做到真正的分布式,就是一个普通的集群。因为这导致要么消费者每次都链接一个实例然后拉取消息数据,要么固定连接那个 queue 所在实例消费数据。前者有数据拉取的开销,后者导致实例性能瓶颈。如果消息放的 queue 挂了,会导致接下来其他实例无法从该实例上拉取消息数据。如果开启了消息持久化,让 RabbitMQ 本地持久化,消息不一定会丢。得等到这个实例重启恢复后,才可以继续从这个 queue 上拉取消息数据。所以上面这种模式,就没有所谓的高可用。这方案主要是提高吞吐量的,就是说让集群中多个节点来服务 queue 的读写操作。
镜像集群模式 高可用模式,镜像集群模式才是所谓的 RabbitMQ 的高可用模式。跟普通集群模式不一样的是,在镜像集群模式下,你所创建的 queue,无论元数据还是 queue 里的消息数据都会存在于多个实例上 。也就是说,每个 RabbitMQ 都有 queue 的一个完整镜像,包含 queue 的全部数据。每次写消息到 queue 的时候,都会自动把消息数据同步到多个实例的 queue 上。
那么,要如何才能开启这个镜像集群模式呢?其实很简单,RabbitMQ 有很好的管理控制台,在后台新增一个策略,这个策略就是镜像集群模式的策略。指定的时候可以要求数据同步到所有节点,也可以要求同步到指定数量的节点上。再次创建 queue 的时候,应用这个策略,就会自动将数据同步到其他的结点上了。 可以看到,不管任何一个机器挂了、宕了,都没影响。其他机器或者节点上还包括这个 queue 的完整数据,其他消费者都可从其他节点上去拉取消息数据进行消费。但有坏处:
- 第一,性能开销太大,消息需要同步到所有机器或者很多机器上,会导致网络带宽压力和消耗很重;
- 第二,这样不是分布式,没有扩展性可言。如果每个 queue 负载很重,添加机器或新增的机器也包含这个 queue 的所有数据,并没办法做线性扩展。如果这个 queue 的数据量很大,大到这个机器上的容量无法容纳,此时问题就更多了。
所以,以上三种模式都不是绝对的高可用模式,只是相对的。
8. RabbitMq 怎么保证消息的顺序性?¶
与 Kafka 和 RocketMQ 不同,Kafka 不存在类似 Topic 的概念,而是真正的一条一条队列,并且每个队列可以被多个消费者拉取消息。这是一个非常大的差异。 在 RabbitMQ 中,一个 queue,多个 consumer。比如:生产者向 RabbitMQ 里发送了三条数据,顺序依次为是 data1/data2/data3,压入的是 RabbitMQ 的内存队列里。有三个消费者分别从 RabbitMQ 中消费三条数据中的一条,结果消费者 2 先执行完操作,把 data2 存入数据库,然后是 data1/data3。这不明显乱了,没有按照顺序消费消息。
应对上面的顺序消费问题有两种方案:
方案一 拆分多个 queue,每个 queue 对应一个 consumer,就是多一些 queue 而已,确实也麻烦了些。 这种方式,有点类似于 Kafka 和 RocketMQ 中 Topic 的概念。比如说,原先一个 queue 叫 aaa,那么多个 queue,我们就可以搞成 aaa-01,aaa-02,aaa-03 等,相同前缀,不同后缀。
方法二 就一个 queue,但对应一个 consumer,consumer 内部用内存做队列、做排队,然后分发给底层不同的 worker 来处理。这种方式就是将一个 queue 里的,相同 key 交给同一个 worker 来执行。因为 RabbitMQ 是可以单条消息来 ack,所以较为方便。
从上面两个方案可以看出,前提都是一个 queue 只能启动一个 consumer 对应。
9. 如果有大量消息持续积压在队列了,怎么处理?¶
通常遇到这种情况,只能是搞个临时扩容,具体步骤和思路如下:
- 先修复消费者的 bug,确保其回复消费速度,然后将现有的消费者都停了;
- 临时建好原先 10 倍或者更多的 queue;
- 写一个临时分发数据的消费者,这个程序部署消费积压的数据,消费后不做任何耗时的处理,直接均匀轮训写入临时建好的 queue 里;
- 临时启动对应 queue 数量的消费者,对新建的 queue 里的消息进行消费。比如说以前只有一台消费者,那么此时的消费者可以是 10 个或者更多,这样处理积压数据的速度会快很多;
- 等快速消费完积压消息数据后,把修复好的消费者继续使用(继续原来的架构),归还相应的服务器资源。 下面大致搞一个图来表示。
问题处理完了,还得把架构改成架构一。
七、Redis 篇¶
1. 为什么要用缓存¶
使用缓存的目的就是提升读写性能。而实际业务场景下,更多的是为了带来更好的性能、更高的并发量。Redis 的读写性能比 MySQL 好的多,我们就可以把 MySQL 中的热点数据缓存到 Redis 中,提升读取性能,同时也减轻了 MySQL 的读取压力。
2. 为什么 使用 Redis 而不是用 Memcached 呢?¶
这时候可以回答 Memcached 与 Redis 区别:
- Redis 和 Memcached 都是将数据存放在内存中,都是内存数据库。不过 Memcached 还可用于缓存其他东西,例如图片、视频等等。
- Memcached 仅支持 key-value 结构的数据类型,Redis 不仅仅支持简单的 key-value 类型的数据,同时还提供 list、set、hash 等数据结构的存储。
- 虚拟内存:Redis 当物理内存用完时,可以将一些很久没用到的 value 交换到磁盘。
- 分布式:设定 Memcached 集群,利用 magent 做一主多从;Redis 可以做一主多从。都可以一主一从
- 存储数据安全:Memcached 挂掉后,数据没了;Redis 可以定期保存到磁盘(持久化)
- Memcached 的单个 value 最大 1m,Redis 的单个 value 最大 512m。
- 灾难恢复:Memcached 挂掉后,数据不可恢复; Redis 数据丢失后可以通过 aof 恢复。
- Redis 原生就支持集群模式,Redis 3.0 版本中,官方便能支持 Cluster 模式了,Memcached 没有原生的集群模式,需要依赖客户端来实现,然后往集群中分片写入数据。
- Memcached 网络 IO 模型是多线程,非阻塞 IO 复用的网络模型,原型上接近于 nignx。而 Redis 使用单线程的 IO 复用模型,自己封装了一个简单的 AeEvent 事件处理框架,主要实现类 epoll、kqueue 和 select,更接近于 Apache 早期的模式。
3. 为什么 Redis 单线程模型效率也能那么高?¶
- C 语言实现,效率高
- 纯内存操作
- 基于非阻塞的 IO 复用模型机制
- 单线程的话就能避免多线程的频繁上下文切换问题
- 丰富的数据结构(全称采用 hash 结构,读取速度非常快。对数据存储进行了一些优化,比如亚索表、跳表等)
4. 说说 Redis 的线程模型¶
问这问题是因为前面回答的时候提到了 “Redis 是基于非阻塞的 IO 复用模型”。如果这个问题回答不上来,就相当于前面的回答是给自己挖坑,面试官对你的印象可能也会打点折扣。
Redis 内部使用 文件事件处理器 file event handler,这个文件事件处理器是 单线程的,所以 Redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。
文件事件处理器的结构包含 4 个部分:
- 多个 socket
- IO 多路复用程序
- 文件事件分派器
- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件。但是 IO 多路复用程序会监听多个 socket,会将 socket 产生的事件放入队列中排队。事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。
来看客户端与 Redis 的一次通信过程:
下面来大致说一下这个图:
-
客户端 Socket01 向 Redis 的 Server Socket 请求建立连接,此时 Server Socket 会产生一个 AE_READABLE 事件。IO 多路复用程序监听到 server socket 产生的事件后,将该事件压入队列中。文件事件分派器从队列中获取该事件,交给连接应答处理器。连接应答处理器会创建一个能与客户端通信的 Socket01,并将该 Socket01 的 AE_READABLE 事件与命令请求处理器关联。
-
假设客户端发送了一个 set key value 请求,此时 Redis 中的 Socket01 会产生 AE_READABLE 事件,IO 多路复用程序将事件压入队列,事件分派器从队列中获取到该事件。由于前面 Socket01 的 AE_READABLE 事件已经与命令请求处理器关联,因此事件分派器将事件交给命令请求处理器来处理。命令请求处理器读取 Socket01 的 set key value 并在自己内存中完成 set key value 的设置。操作完成后,它会将 Socket01 的 AE_WRITABLE 事件与令回复处理器关联。
-
如果此时客户端准备好接收返回结果了,那么 Redis 中的 Socket01 会产生一个 AE_WRITABLE 事件,同样压入队列中。事件分派器找到相关联的命令回复处理器,由命令回复处理器对 Socket01 输入本次操作的一个结果(比如 ok)之后解除 Socket01 的 AE_WRITABLE 事件与命令回复处理器的关联。 这样便完成了一次通信。不要怕这段文字,结合图看,一遍不行两遍,实在不行可以网上查点资料结合着看。一定要搞清楚,否则前面吹的牛逼就白费了。
-
说一下 Redis 有什么优点和缺点 优点 - 速度快:因为数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是 O (1)。
- 支持丰富的数据结构:支持 String 、List、Set、Sorted Set、Hash 五种基础的数据结构。
- 持久化存储:Redis 提供 RDB 和 AOF 两种数据的持久化存储方案,解决内存数据库最担心的 “万一 Redis 挂掉,数据会消失掉” 的问题。
- 高可用:内置 Redis Sentinel,提供高可用方案,实现主从故障自动转移。内置 Redis Cluster,提供集群方案,实现基于槽的分片方案,从而支持更大的 Redis 规模。
- 丰富的特性:Key 过期、计数、分布式锁、消息队列等。缺点 - 由于 Redis 是内存数据库,所以,单台机器存储的数据量,跟机器本身的内存大小有关。虽然 Redis 本身有 Key 过期策略,但还是需要提前预估和节约内存。如果内存增长过快,需要定期删除数据。
- 如果进行完整重同步,由于需要生成 RDB 文件,并进行传输。这会占用主机的 CPU,并会消耗现网的带宽。不过 Redis2.8 版本,已经有部分重同步的功能,但还是有可能有完整重同步的,比如:新上线的备机。
- 修改配置文件,进行重启,将硬盘中的数据加载进内存,时间比较久。在这个过程中,Redis 不能提供服务。
5. Redis 缓存刷新策略有哪些?¶
6. Redis 持久化方式有哪些?以及有什么区别?¶
Redis 提供两种持久化机制 RDB 和 AOF 机制: RDB 持久化方式 指用数据集快照的方式半持久化模式记录 redis 数据库的所有键值对,在某个时间点将数据写入一个临时文件。持久化结束后,用这个临时文件替换上次持久化的文件,达到数据恢复。优点: - 只有一个文件 dump.rdb,方便持久化 - 容灾性好,一个文件可以保存到安全的磁盘 - 性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以使 IO 最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 Redis 的高性能 - 相对于数据集大时,比 AOF 的启动效率更 缺点: 数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化期间 Redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候。
AOF=Append-only file 持久化方式 是指所有的命令行记录,以 Redis 命令请求协议的格式完全持久化存储,保存为 AOF 文件。
优点: - 1. 数据安全。AOF 持久化可以配置 appendfsync 属性,有 always,每进行一次命令操作就记录到 AOF 文件中一次。 - 2. 通过 append 模式写文件。即使中途服务器宕机,也可以通过 redis-check-aof 工具解决数据一致性问题。 - 3. AOF 机制的 rewrite 模式。AOF 文件没被 rewrite 之前(文件过大时会对命令进行合并重写),可以删除其中的某些命令(比如误操作的 flushall)。缺点: 1. AOF 文件比 RDB 文件大,且恢复速度慢。 2. 数据集大的时候,比 RDB 启动效率低。
7. 持久化有两种,那应该怎么选择呢?¶
- 不要仅仅使用 RDB,因为那样会导致丢失很多数据。
- 也不要仅仅使用 AOF,因为那样有两个问题:第一,通过 AOF 做冷备,没有 RDB 做冷备,来的恢复速度更快; 第二,RDB 每次简单粗暴生成数据快照,更加健壮,可以避免 AOF 这种复杂的备份和恢复机制的 bug。
- Redis 支持同时开启两种持久化方式。我们可以综合使用 AOF 和 RDB 两种持久化机制,用 AOF 来保证数据不丢失,作为数据恢复的第一选择;用 RDB 来做不同程度的冷备,在 AOF 文件都丢失或损坏不可用的时候,还可以使用 RDB 来进行快速的数据恢复。
- 如果同时使用 RDB 和 AOF 两种持久化机制,那在 Redis 重启的时候,会使用 AOF 来重新构建数据,因为 AOF 中的数据更加完整。
8. 怎么使用 Redis 实现消息队列?¶
一般使用 list 结构作为队列,rpush 生产消息,lpop 消费消息。当 lpop 没有消息的时候,要适当 sleep 一会再重试。
- 面试官可能会问可不可以不用 sleep 呢?list 还有个指令叫 blpop,在没有消息的时候,它会阻塞住直到消息到来。
- 面试官可能还问能不能生产一次消费多次呢?使用 pub /sub 主题订阅者模式,可以实现 1:N 的消息队列。
- 面试官可能还问 pub /sub 有什么缺点?在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列,如 RabbitMQ 等。
- 面试官可能还问 Redis 如何 实现延时队列? 我估计现在你很想把面试官一棒打死,怎么问的这么详细。但是你会很克制,然后神态自若地回答道:使用 sortedset,拿时间戳作为 score,消息内容作为 key 调用 zadd 来生产消息,消费者用 zrangebyscore 指令获取 N 秒之前的数据轮询进行处理。
面试扩散 : 很多面试官上来就直接这么问: Redis 如何 实现延时队列?
9. 熟悉哪些 Redis 集群模式?¶
- Redis Sentinel 体量较小时,选择 Redis Sentinel,单主 Redis 足以支撑业务。
- Redis Cluster Redis 官方提供的集群化方案,体量较大时,选择 Redis Cluster,通过分片,使用更多内存。
- Twemprox Twemprox 是 Twtter 开源的一个 Redis 和 Memcached 代理服务器,主要用于管理 Redis 和 Memcached 集群,减少与 Cache 服务器直接连接的数量。
- Codis Codis 是一个代理中间件,当客户端向 Codis 发送指令时,Codis 负责将指令转发到后面的 Redis 来执行,并将结果返回给客户端。一个 Codis 实例可以连接多个 Redis 实例,也可以启动多个 Codis 实例来支撑,每个 Codis 节点都是对等的,这样可以增加整体的 QPS 需求,还能起到容灾功能。
- 客户端分片 在 Redis Cluster 还没出现之前使用较多,现在基本很少人使用了。在业务代码层实现,启动几个毫无关联的 Redis 实例,在代码层,对 Key 进行 hash 计算,然后去对应的 Redis 实例操作数据。这种方式对 hash 层代码要求比较高,考虑部分包括节点、失效后的替代算法方案、数据震荡后的自动脚本恢复、实例的监控等等。
10. 缓存和数据库谁先更新呢?¶
解决方案¶
- 写请求过来,将写请求缓存到缓存队列中,并且开始执行写请求的具体操作(删除缓存中的数据、更新数据库、更新缓存)。
- 如果在更新数据库过程中,又来了个读请求,将读请求再次存入到缓存队列(可以搞 n 个队列,采用 key 的 hash 值进行队列个数取模 hash% n,落到对应的队列中,队列需要保证顺序性)中。顺序性保证等待队列前的写请求执行完成,才会执行读请求之前的写请求。删除缓存失败,直接返回。此时数据库中的数据是旧值,并且与缓存中的数据是一致的,不会出现缓存一致性的问题。
- 写请求删除缓存成功,则更新数据库,如果更新数据库失败,则直接返回,写请求结束,此时数据库中的值依旧是旧值,读请求过来后,发现缓存中没有数据, 则会直接向数据库中请求,同时将数据写入到缓存中,此时也不会出现数据一致性的问题。
- 更新数据成功之后,再更新缓存。如果此时更新缓存失败,则缓存中没有数据,数据库中是新值,写请求结束,此时读请求还是一样。发现缓存中没有数据,同样会从数据库中读取数据,并且存入到缓存中。其实这里不管更新缓存成功还是失败, 都不会出现数据一致性的问题。 上面这方案解决了数据不一致的问题,主要是使用了串行化,每次操作进来必须按照顺序进行。如果某个队列元素积压太多,可以针对读请求进行过滤,提示用户刷新页面,重新请求。
潜在的问题¶
留给大家自己去想了,因为这属于发散性问题。如下给出两点思考提示:
- 请求时间过长,大量的写请求堆压在队列中,一个读请求过来,得等都写完了才可以获取到数据。
- 热点数据路由问题,导致请求倾斜。
八、Spring Boot 篇¶
1.Spring Boot 提供了哪些核心功能?¶
独立运行 Spring 项目 Spring Boot 可以以 jar 包形式独立运行,运行一个 Spring Boot 项目只需要通过 java -jar xx.jar 来运行。内嵌 Servlet 容器 Spring Boot 可以选择内嵌 Tomcat、Jetty 或者 Undertow,这样我们无须以 war 包形式部署项目。提供 Starter 简化 Maven 配置 Spring 提供了一系列的 starter pom 来简化 Maven 的依赖加载,比如:spring-boot-starter-web。自动配置 Spring Bean Spring Boot 检测到特定类的存在,就会针对这个应用做一定的配置,进行自动配置 Bean,这样会极大地减少我们要使用的配置。当然,Spring Boot 只考虑大多数的开发场景,并不是所有的场景,若在实际开发中我们需要配置 Bean,而 Spring Boot 没有提供支持,则可以自定义自动配置进行解决。准生产的应用监控 Spring Boot 提供基于 HTTP、JMX、SSH 对运行时的项目进行监控。无代码生成和 XML 配置 Spring Boot 没有引入任何形式的代码生成,它使用的是 Spring 4.0 的条件 @Condition 注解以实现根据条件进行配置。同时使用了 Maven /Gradle 的依赖传递解析机制来实现 Spring 应用里面的自动配置。
2. Spring Boot 核心注解是什么?¶
package cn.tian.spring.boot.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
@SpringBootApplication 注解,就是 Spring Boot 的核心注解。 @SpringBootApplication 源码:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan( excludeFilters = {@Filter( type = FilterType.CUSTOM, classes = TypeExcludeFilter.class}),
@Filter( type = FilterType.CUSTOM,classes = {AutoConfigurationExcludeFilter.class})})
public @interface SpringBootApplication {
@AliasFor( annotation = EnableAutoConfiguration.class)
Class<?>[] exclude() default {};
@AliasFor(annotation = EnableAutoConfiguration.class)
String[] excludeName() default {};
@AliasFor(annotation = ComponentScan.class,attribute = "basePackages")
String[] scanBasePackages() default {};
@AliasFor(annotation = ComponentScan.class,attribute = "basePackageClasses")
Class<?>[] scanBasePackageClasses() default {};
}
这个注解主要由三个注解组合:
- @Configuration 注解,指定类是 Bean 定义的配置类。来自 spring-context 项目,用于 Java Config,不是 Spring Boot 新带来的。
- @ComponentScan 注解,扫描指定包下的 Beans。来自 spring-context 项目,用于 Java Config,不是 Spring Boot 新带来的。
- @EnableAutoConfiguration 注解,打开自动配置的功能。如果我们想要关闭某个类的自动配置,可以设置注解的 exclude 或 excludeName 属性。来自 spring-boot-autoconfigure 项目,它才是 Spring Boot 新带来的。
3. 说说 Spring Boot 的自动装配原理¶
- 使用 @EnableAutoConfiguration 注解,打开 Spring Boot 自动配置的功能。
- Spring Boot 在启动时扫描项目所依赖的 jar 包,寻找包含 spring.factories 文件的 jar 包。
- 根据 spring.factories 配置加载 AutoConfigure 类。
- 根据 @Conditional 等条件注解 的条件,进行自动配置并将 Bean 注入 Spring IoC 中。
4. Spring Boot 常用 starter 有哪些?¶
- spring-boot-starter-web:嵌入 Tomcat 和 web 开发需要的相关 jar 包
- spring-boot-starter-data-redis:redis 数据库支持
- mybatis-spring-boot-starter:第三方的 MyBatis 集成的 starter
- spring-boot -starter-test:用于测试 Spring 引导应用程序
- spring-boot-starter-AOP:这个 starter 用于使用 AspectJ 和 Spring AOP 进行面向方面的编程 太多了 starter 了,这里只是例举几个。
5. Spring 中的 starter 是什么?¶
依赖管理是任何复杂项目的关键部分。以手动的方式来实现依赖管理不太现实,你得花更多时间,这样你在项目其他重要的方面能付出的时间就会变得越少。Starter 主要用来简化依赖用的。 比如我们之前做 MVC 时要引入日志组件,那么需要去找到 log4j 的版本,然后引入。现在有了 Starter 之后,直接用这个之后,log4j 就自动引入了,也不用关心版本这些问题。 比如我们要在 Spring Boot 中引入 Web MVC 的支持时,我们通常会引入这个模块 spring-boot-starter-web,而这个模块如果解压出来会发现里面什么都没有,只定义了一些 POM 依赖。
6. Spring Boot 有什么优缺点?¶
7. 读取配置文件中配置项的有哪些方法?¶
九、Spring 篇¶
1. Spring 中 ApplicationContext 和 BeanFactory 的区别¶
-
包目录不同 - spring-beans.jar 中 org.springframework.beans.factory.BeanFactory
-
spring-context.jar 中 org.springframework.context.ApplicationContext
-
国际化 BeanFactory 是不支持国际化功能的,因为 BeanFactory 没有扩展 Spring 中 MessageResource 接口。相反,由于 ApplicationContext 扩展了 MessageResource 接口,因而具有消息处理的能力(i18N)。
- 强大的事件机制(Event) 基本上牵涉到事件(Event)方面的设计,就离不开观察者模式,ApplicationContext 的事件机制主要通过 ApplicationEvent 和 ApplicationListener 这两个接口来提供的,和 Java swing 中的事件机制一样。即当 ApplicationContext 中发布一个事件时,所有扩展了 ApplicationListener 的 Bean 都将接受到这个事件,并进行相应的处理。
- 底层资源的访问 ApplicationContext 扩展了 ResourceLoader(资源加载器)接口,从而可以用来加载多个 Resource,而 BeanFactory 是没有扩展 ResourceLoader。
- 对 Web 应用的支持 与 BeanFactory 通常以编程的方式被创建,ApplicationContext 能以声明的方式创建,如使用 ContextLoader。 当然你也可以使用 ApplicationContext 的实现方式之一,以编程的方式创建 ApplicationContext 实例。
- 延迟加载 1. BeanFactroy 采用的是延迟加载形式来注入 Bean 的,即只有在使用到某个 Bean 时 (调用 getBean ()),才对该 Bean 进行加载实例化。这样,我们就不能发现一些存在的 spring 的配置问题。而 ApplicationContext 则相反,它是在容器启动时,一次性创建了所有的 Bean。这样,在容器启动时,我们就可以发现 Spring 中存在的配置错误。
- BeanFactory 和 ApplicationContext 都支持 BeanPostProcessor、BeanFactoryPostProcessor 的使用。两者之间的区别是:BeanFactory 需要手动注册,而 ApplicationContext 则是自动注册。 可以看到,ApplicationContext 继承了 BeanFactory,BeanFactory 是 Spring 中比较原始的 Factory,它不支持 AOP、Web 等 Spring 插件。而 ApplicationContext 不仅包含了 BeanFactory 的所有功能,还支持 Spring 的各种插件,还以一种面向框架的方式工作以及对上下文进行分层和实现继承。 BeanFactory 是 Spring 框架的基础设施,面向 Spring 本身;而 ApplicationContext 面向使用 Spring 的开发者,相比 BeanFactory 提供了更多面向实际应用的功能,几乎所有场合都可以直接使用 ApplicationContext,而不是底层的 BeanFactory。
- 常用容器 BeanFactory 类型的有 XmlBeanFactory,它可以根据 XML 文件中定义的内容,创建相应的 Bean。 ApplicationContext 类型的常用容器有:
- ClassPathXmlApplicationContext:从 ClassPath 的 XML 配置文件中读取上下文,并生成上下文定义。应用程序上下文从程序环境变量中取得。
- FileSystemXmlApplicationContext:由文件系统中的 XML 配置文件读取上下文。
- XmlWebApplicationContext:由 Web 应用的 XML 文件读取上下文。例如我们在 Spring MVC 使用的情况。
2. 说一下你对 Spring IOC 的理解¶
3. Spring IOC 有什么优点?¶
4. Bean 的生命周期¶
这个题目在面试的时候被问到的概率很大,主要考察咱们对 bean 的整个生命周期的了解程度,其实平时工作中很少需要你关注 bean 的生命周期。 Bean 的声明周期为 bean 的创建、应用、销毁。
创建过程¶
- 实例化 Bean 对象;
- 设置 Bean 属性;
- 如果我们通过各种 Aware 接口声明了依赖关系,则会注入 Bean 对容器基础设施层面的依赖。具体包括 BeanNameAware、BeanFactoryAware 和 ApplicationContextAware,分别会注入 Bean ID、Bean Factory 或者 ApplicationContext;
- 调用 BeanPostProcessor 的前置初始化方法 postProcessBeforeInitialization;
- 如果实现了 InitializingBean 接口,则会调用 afterPropertiesSet 方法;
- 调用 Bean 自身定义的 init 方法;
- 调用 BeanPostProcessor 的后置初始化方法 postProcessAfterInitialization;
- 创建过程完毕。
应用销毁过程¶
销毁过程会依次调用 DisposableBean 的 destroy 方法和 Bean 自身定制的 destroy 方法。 网上找到一张图:
5. Spring Bean 的作用域有哪些?¶
注 :网络上很多文章说有 Global-session 级别,它是 Portlet 模块独有,目前已经废弃,在 Spring 5 中是找不到的。
6. Spring 是怎么管理事务的?¶
7. 说说你对 Spring AOP 的理解¶
AOP 的设计¶
- 每个 Bean 都会被 JDK 或者 CGlib 代理,取决于是否有接口;
- 每个 Bean 会有很多方法拦截器,注意这里的拦截器分为两层,外层有 Spring 内核控制流程,内层拦截器是用户设置,也就是 AOP;
- 当代理方法被调用时候,先经过外层拦截器,外层拦截器根据方法的各种信息判断该方法应该执行哪些 “内层拦截器”。内层拦截器的设计就是职责链的设计。
整体分析¶
-
代理的创建
- 首先,需要创建代理工厂,代理工厂需要 3 个重要的信息:拦截器数组、模板对象接口数组、目标对象。
- 创建代理工厂时,默认会在拦截器数组尾部再增加一个默认拦截器,用于最终的调用目标方法。
- 当调用 getProxy 方法的时候,会根据接口数量大于 0 条件,返回一个代理对象(JDK 或者 CGlib)。 注意:创建代理对象时,同时会创建一个外层拦截器,这个拦截器就是 spring 内核的拦截器,用于控制整个 AOP 流程。
-
代理的调用
8. Spring 中用到了哪些设计模式?¶
简单工厂模式 :Spring 中的 BeanFactory 就是简单工厂模式的体现。根据传入一个唯一的标识来获得 Bean 对象,但是在传入参数后创建还是传入参数前创建,要根据具体情况来定。工厂模式 :Spring 中的 FactoryBean 就是典型的工厂方法模式,实现了 FactoryBean 接口的 bean 是一类叫做 factory 的 bean。其特点是,spring 在使用 getBean () 调用获得该 bean 时,会自动调用该 bean 的 getObject () 方法,所以返回的不是 factory 这个 bean,而是这个 bean.getOjbect () 方法的返回值。单例模式 :在 spring 中用到的单例模式有:scope="singleton"
,注册式单例模式,bean 存放于 Map 中。bean name 当做 key,bean 当做 value。原型模式 :在 spring 中用到的原型模式有:scope="prototype"
,每次获取的是通过克隆生成的新实例,对其进行修改时对原有实例对象不造成任何影响。迭代器模式 :在 Spring 中有个 CompositeIterator 实现了 Iterator,Iterable 接口和 Iterator 接口,这两个都是迭代相关的接口。可以这么认为,实现了 Iterable 接口,则表示某个对象是可被迭代的。Iterator 接口相当于是一个迭代器,实现了 Iterator 接口,等于具体定义了这个可被迭代的对象时如何进行迭代的。代理模式 :Spring 中经典的 AOP,就是使用动态代理实现的,分 JDK 和 CGlib 动态代理。适配器模式 :Spring 中的 AOP 中 AdvisorAdapter 类,它有三个实现:MethodBeforAdviceAdapter、AfterReturnningAdviceAdapter、ThrowsAdviceAdapter。Spring 会根据不同的 AOP 配置来使用对应的 Advice,与策略模式不同的是,一个方法可以同时拥有多个 Advice。Spring 存在很多以 Adapter 结尾的,大多数都是适配器模式。观察者模式 :Spring 中的 Event 和 Listener。spring 事件:ApplicationEvent,该抽象类继承了 EventObject 类,JDK 建议所有的事件都应该继承自 EventObject。spring 事件监听器:ApplicationListener,该接口继承了 EventListener 接口,JDK 建议所有的事件监听器都应该继承 EventListener。模板模式 :Spring 中的 org.springframework.jdbc.core.JdbcTemplate 就是非常经典的模板模式的应用,里面的 execute 方法,把整个算法步骤都定义好了。责任链模式 :DispatcherServlet 中的 doDispatch () 方法中获取与请求匹配的处理器 HandlerExecutionChain,this.getHandler () 方法的处理使用到了责任链模式。
9. Spring 框架中的单例 Bean 是线程安全的么?¶
Spring 框架并没有对单例 Bean 进行任何多线程的封装处理。
- 关于单例 Bean 的线程安全和并发问题,需要开发者自行去搞定。
- 单例的线程安全问题,并不是 Spring 应该去关心的。Spring 应该做的是,提供根据配置,创建单例 Bean 或多例 Bean 的功能。 当然,但实际上,大部分的 Spring Bean 并没有可变的状态,所以在某种程度上说 Spring 的单例 Bean 是线程安全的。如果你的 Bean 有多种状态的话,就需要自行保证线程安全。最浅显的解决办法,就是将多态 Bean 的作用域(Scope)由 Singleton 变更为 Prototype。
10. Spring 是怎么解决循环依赖的?¶
- 首先 A 完成初始化第一步并将自己 提前曝光 出来(通过 ObjectFactory 将自己提前曝光),在初始化的时候,发现自己依赖对象 B,此时就会去尝试 get (B),这个时候发现 B 还没有被创建出来;
- 然后 B 就走创建流程,在 B 初始化的时候,同样发现自己依赖 C,C 也没有被创建出来;
- 这个时候 C 又开始初始化进程,但是在初始化的过程中发现自己依赖 A,于是尝试 get (A)。这个时候由于 A 已经添加至缓存中(一般都是添加至三级缓存 singletonFactories),通过 ObjectFactory 提前曝光,所以可以通过 ObjectFactory##getObject () 方法来拿到 A 对象。C 拿到 A 对象后顺利完成初始化,然后将自己添加到一级缓存中;
- 回到 B,B 也可以拿到 C 对象,完成初始化,A 可以顺利拿到 B 完成初始化。到这里整个链路就已经完成了初始化过程了。
十、ZooKeeper 篇¶
1. 说说 ZooKeeper 是什么?¶
ZooKeeper 是一个开放源码的分布式协调服务,它是集群的管理者,监视着集群中各个节点的状态。根据节点提交的反馈进行下一步合理操作,最终将简单易用的接口和性能高效、功能稳定的系统提供给用户。ZooKeeper 是 Chubby 的开源实现,使用 ZAB 协议(Paxos 算法的变种)。 分布式应用程序可以基于 ZooKeeper 实现诸如数据发布 / 订阅、负载均衡、命名服务、分布式协调 / 通知、集群管理、Master 选举、分布式锁和分布式队列等功能。
2. ZooKeeper 有哪些应用场景?¶
统一命名服务 命名服务是指通过指定的名字来获取资源或服务的地址,利用 ZooKeeper 创建一个全局的路径,即时唯一的路径。这个路径就可以作为一个名字,指向集群中机器或者提供服务的地址,又或者一个远程的对象等。分布式服务 相对来说,这个功能使用还是比较广泛的。ZooKeeper 实现的分布式锁的可靠性比 Redis 实现的高,当然相对性能来说,ZooKeeper 性能稍弱,但其实已经很牛了。配置管理 Spring Cloud Config ZooKeeper 就是基于 ZooKeeper 来实现的,提供配置中心的服务。注册与发现 是否有新的机器加入或者是有机器退出(挂了)。所有机器约定在父目录下创建临时节点,然后监听父节点下的子节点变化。一旦有机器挂机,该机器与 ZooKeeper 的链接断开,其所创建的临时目录节点也被删除,所有其他机器都收到对应的通知:某个结点被删除了。Dubbo 就是典型应用案例。Master 选举 基于 ZooKeeper 实现分布式协调,从而实现主从的选举。比如:Kafka、Elastic-job 等中间件都有使用到。队列管理 ZooKeeper 有两种类型的队列: 同步队列:当一个队列的成员都聚齐时,这个队列才可用,否则一直等待。在约定的目录下创建临时目录节点,再监听节点数目是否是我们要求的数据。队列按照先进先出 FIFO 方式进行入队和出队操作。和分布式锁服务中心的控制时序的场景基本原理相同,入列和出列都有编号。创建 PERSISTENT_SEQUENTIAL 节点,创建成功时 Watcher 通知等待的队列,队列删除序列号最小的节点以消费。此场景下,znode 用于消息存储,znode 存储的数据就是消息队列中的消息内容,SEQUENTIAL 序列号就是消息的编号,按序取出即可。由于创建的节点是持久化的,所以不必担心队列消息丢失的问题。分布式锁 有了 ZooKeeper 的一致性文件系统,锁的问题就变得简单多了。锁服务可以分为保持独占和控制时序。
- 保持独占。我们把 znode 看作是一把锁,通过 createZnode 的方式来实现。所有客户端都去创建 /distribute_lock 节点,最终成功创建的那个客户端也即拥有了这把锁,用完删除掉自己创建的 /distribute_lock 节点就释放出锁。
- 控制时序。/distribute_lock 已经预先存在,所有客户端在它下面创建临时顺序编号目录节点,和 Master 一样,编号最小的获得锁,用完删除,依次方便。
3. ZooKeeper 有哪些节点类型?¶
4. 请描述一下 ZooKeeper 的通知机制是什么?¶
ZooKeeper 允许客户端向服务端的某个 znode 注册一个 Watcher 监听,当服务端的一些指定事件触发了这个 Watcher,服务端会向指定客户端发送一个事件通知来实现分布式的通知功能,然后客户端根据 Watcher 通知状态和事件类型做出业务上的改变。 大致分为三个步骤: 客户端注册 Watcher
- 调用 getData、getChildren、exist 三个 API,传入 Watcher 对象;
- . 标记请求 request,封装 Watcher 到 WatchRegistration;
- 封装成 Packet 对象,发服务端发送 request;
- 收到服务端响应后,将 Watcher 注册到 ZKWatcherManager 中进行管理;
- 请求返回,完成注册。 服务端处理 Watcher
- 务端接收 Watcher 并存储;
- Watcher 触发;
- 调用 process 方法来触发 Watcher。 客户端回调 Watcher
- 客户端 SendThread 线程接收事件通知,交由 EventThread 线程回调 Watcher。
- 客户端的 Watcher 机制同样是一次性的,一旦被触发后,该 Watcher 就失效了。
5. ZooKeeper 对节点的 watch 监听通知是永久的吗?¶
不是,一次性 的。无论是服务端还是客户端,一旦一个 Watcher 被触发, ZooKeeper 都会将其从相应的存储中移除。这样的设计有效地减轻了服务端的压力,不然对于更新非常频繁的节点,服务端会不断地向客户端发送事件通知,无论对于网络还是服务端的压力都非常大。
6. ZooKeeper 集群中有哪些角色?¶
在一个集群中,最少需要 3 台。或者保证 2N+1 台,即奇数。为什么保证奇数?主要是为了选举算法。
7. ZooKeeper 集群中 Server 有哪些工作状态?¶
LOOKING 寻找 Leader 状态。当服务器处于该状态时,它会认为当前集群中没有 Leader,因此需要进入 Leader 选举状态。FOLLOWING 跟随者状态。表明当前服务器角色是 Follower。LEADING 领导者状态。表明当前服务器角色是 Leader。OBSERVING 观察者状态。表明当前服务器角色是 Observer。
8. ZooKeeper 集群中是怎样选举 leader 的?¶
当 Leader 崩溃了,或者失去了大多数的 Follower,这时候 ZooKeeper 就进入恢复模式,恢复模式需要重新选举一个新的 Leader,让所有的 Server 都恢复到一个状态 LOOKING 。 ZooKeeper 有两种选举算法:基于 basic paxos 实现和基于 fast paxos 实现。默认为 fast paxos。 由于篇幅问题,这里推荐:选举流程。
9. ZooKeeper 是如何保证事务的顺序一致性的呢?¶
- ZooKeeper 采用了递增的事务 id 来识别,所有的 proposal(提议)都在被提出的时候加上了 zxid 。zxid 实际上是一个 64 位数字。
- 高 32 位是 epoch 用来标识 Leader 是否发生了改变,如果有新的 Leader 产生出来,epoch 会自增。
- 低 32 位用来递增计数。
- 当新产生的 proposal 的时候,会依据数据库的两阶段过程,首先会向其他的 Server。 发出事务执行请求,如果超过半数的机器都能执行并且能够成功,那么就会开始执行。
10. ZooKeeper 集群中各服务器之间是怎样通信的?¶
Leader 服务器会和每一个 Follower/Observer 服务器都建立 TCP 连接,同时为每个 Follower/Observer 都创建一个叫做 LearnerHandler 的实体。
- LearnerHandler 主要负责 Leader 和 Follower/Observer 之间的网络通讯,包括数据同步,请求转发和 proposal 提议的投票等。
- Leader 服务器保存了所有 Follower/Observer 的 LearnerHandler。
11. ZooKeeper 分布式锁怎么实现的?¶
如果有客户端 1、客户端 2 等 N 个客户端争抢一个 ZooKeeper 分布式锁。大致如下:
- 大家都是上来直接创建一个锁节点下的一个接一个的临时有序节点;
- . 如果自己不是第一个节点,就对自己上一个节点加监听器;
- 只要上一个节点释放锁,自己就排到前面去了,相当于是一个排队机制。 而且用临时顺序节点的另外一个用意就是,如果某个客户端创建临时顺序节点之后,不小心自己宕机了也没关系。ZooKeeper 感知到那个客户端宕机,会自动删除对应的临时顺序节点,相当于自动释放锁,或者是自动取消自己的排队。
十一、并发编程篇¶
1. 通常创建线程有几种方式?¶
创建线程的常用四种方式:
- 继承 Thread 类
- 实现 Runnable 接口
- 实现 Callable 接口(JDK1.5>=)
- 线程池方式创建 通过继承 Thread 类或者实现 Runnable 接口、Callable 接口都可以实现多线程,不过实现 Runnable 接口与实现 Callable 接口的方式基本相同,只是 Callable 接口里定义的方法返回值,可以声明抛出异常而已。因此将实现 Runnable 接口和实现 Callable 接口归为一种方式。这种方式与继承 Thread 方式之间的主要差别如下:
采用实现 Runnable、Callable 接口的方式创建线程的优缺点¶
- 优点 :线程类只是实现了 Runnable 或者 Callable 接口,还可以继承其他类。这种方式下,多个线程可以共享一个 target 对象。所以非常适合多个相同线程来处理同一份资源的情况,从而可以将 CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
- 缺点 :编程稍微复杂一些,如果需要访问当前线程,则必须使用 Thread.currentThread () 方法
采用继承 Thread 类的方式创建线程的优缺点¶
- 优点 :编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread () 方法,直接使用 this 即可获取当前线程
- 缺点 :因为线程类已经继承了 Thread 类,Java 语言是单继承的,所以就不能再继承其他父类了。
2. 说说线程的生命周期¶
先来看一张图: 这六个状态就对应线程的生命周期。下图为线程对应状态以及状态出发条件:
3. 说说 synchronized 的使用和原理¶
- 对于普通同步方法,锁是当前实例对象
- 对于静态同步方法,锁是当前类 Class 对象
- 对于同步方法块,锁是 Synchronized 括号里配置的对象(对象为普通对象则锁定的是该对象,对象为 Class 对象则锁定的是 Class 对象) JVM 基于进入和退出 Monitor 对象来显示方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用 monitorenter 和 monitorexit 指令实现的,而方法同步是使用另外一种方式实现的。 monitorenter 指令是在编译后插入到同步代码块的开始位置,而 monitorexit 是插入到方法的结束处和异常处,JVM 保证每个 monitorenter 必须有对应的 monitorexit 与之配对。
4. synchronized 和 ReentrantLock 区别¶
- synchronized 是关键字,ReentrantLock 是 JUC 下面的一个类。
- JDK 1.5 之前同步锁只有 synchronized。
- 都是可重入的同步锁。
- synchronized 只有非公平锁,ReentrantLock 默认为非公平锁,但是可以手动设置为公平锁。
- ReentrantLock 需要手动释放锁
try {--objectLock.lock ();}---finally--{objectLock.unlock ();}
,synchronized 隐形释放(方法或者代码块执行完、异常)。 - ReentrantLock 可中断,synchronized 不可中断,一个线程引用锁的时候,别的线程只能阻塞等待。
- ReentrantLock 和 synchronized 持有的对象监视器不同。
- ReentrantLock 能够将 wait/notify/notifyAll 对象化。synchronized 中,锁对象的 wait 和 notify () 或 notifyAll () 方法可以实现一个隐含的条件。
- synchronized 和 ReentrantLock 的性能不能一概而论,早期版本 synchronized 在很多场景下性能相差较大,在后续版本进行了较多改进,在低竞争场景中表现可能优于 ReentrantLock。
- ReentrantLock 提供了很多实用的方法,能够实现很多 synchronized 无法做到的细节控制,比如可以控制 fairness,也就是公平性,或者利用定义条件等。但是,编码中也需要注意,必须要明确调用 unlock () 方法释放,不然就会一直持有该锁。
5. 什么是线程安全?¶
按照《Java 并发编程实战》(Java Concurrency in Practice) 的定义就是:线程安全是一个多线程环境下正确性的概念,也就是保证多线程环境下共享的、可修改的状态的正确性,这里的状态反映在程序中其实可以看作是数据。 通俗易懂的说法:当多个线程访问某个方法时,不管你通过怎样的调用方式或者说这些线程如何交替执行,我们在主程序中不需要做任何同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类是线程安全的。
6. 线程安全需要保证几个基本特征¶
- 原子性,简单说就是相关操作不会中途被其他线程干扰,一般通过同步机制实现。
- 可见性,是一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为将线程本地状态反映到主内存上,volatile 就是负责保证可见性的。
- 有序性,是保证线程内串行语义,避免指令重排等。
7. 说一下线程之间是如何通信的?¶
线程之间的通信有两种方式:共享内存和消息传递。
- 共享内存
在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写 - 读内存中的公共状态来隐式进行通信。典型的共享内存通信方式,就是通过共享对象进行通信。
例如上图线程 A 与 线程 B 之间如果要通信的话,那么就必须经历下面两个步骤:
- 线程 A 把本地内存 A 更新过的共享变量刷新到主内存中去;
-
线程 B 到主内存中去读取线程 A 之前更新过的共享变量。
-
消息传递
在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。在 Java 中典型的消息传递方式,就是 wait () 和 notify (),或者 BlockingQueue。
8. 说说你对 volatile 的理解¶
9. 说一下 volatile 和 synchronized 的区别?¶
- volatile 本质是在告诉 JVM 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取。synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
- volatile 仅能使用在变量级别。synchronized 则可以使用在变量、方法、和类级别的。
- volatile 仅能实现变量的修改可见性,不能保证原子性。而 synchronized 则可以保证变量的修改可见性和原子性。
- volatile 不会造成线程的阻塞。synchronized 可能会造成线程的阻塞。
- volatile 标记的变量不会被编译器优化。synchronized 标记的变量可以被编译器优化。
10. Thread 调用 run 方法和调 start 方法的区别?¶
调用 run 方法不会再启一个线程,跟着主线程继续执行,和调普通类的方法一样; 调用 start 方法表示启动一个线程。
面试扩散 下面代码将输出什么内容?不清楚的建议自己去试试。
public class ThreadDemo {
public static void main(String[] args) {
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("test start");
}
});
thread.start();
thread.start();
}
}
11. 说一下 Java 创建线程池有哪些方式?¶
通过 java.util.concurrent.Executors 来创建以下常见线程池: 也可以通过 java.util.concurrent.ThreadPoolExecutor 来创建自定义线程池,其中核心的几个参数:
int corePoolSize, // 核心线程数量
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 超时时间,超出核心线程数量以外的线程空余存活时间
TimeUnit unit, // 存活时间单位
BlockingQueue<Runnable> workQueue, // 保存执行任务的队列
ThreadFactory threadFactory,// 创建新线程使用的工厂
RejectedExecutionHandler handler // 当任务无法执行的时候的处理方式
12. 说说 ThreadLocal 底层原理是什么,怎么避免内存泄漏?¶
13. 说说你对 JUC 下并发工具类¶
-
Semaphore 是一种新的同步类,它是一个计数信号。从概念上讲,从概念上讲,信号量维护了一个许可集合。
-
如有必要,在许可可用前会阻塞每一个 acquire () 方法,然后再获取该许可。
- 每个 release () 方法,添加一个许可,从而可能释放一个正在阻塞的获取者。
-
但是,不使用实际的许可对象,Semaphore 只对可用许可的数量进行计数,并采取相应的行动。
-
CountDownLatch 字面意思是减小计数(CountDown)的门闩(Latch)。它要做的事情是,等待指定数量的计数被减少,意味着门闩被打开,然后进行执行。CountDownLatch 默认的构造方法是 CountDownLatch (int count),其参数表示需要减少的计数,主线程调用 await () 方法告诉 CountDownLatch 阻塞等待指定数量的计数被减少,然后其它线程调用 CountDownLatch 的 CountDown () 方法,减小计数 (不会阻塞)。等待计数被减少到零,主线程结束阻塞等待,继续往下执行。
-
CyclicBarrier 字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。 CyclicBarrier 默认的构造方法是 CyclicBarrier (int parties),其参数表示屏障拦截的线程数量,每个线程调用 await () 方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞,直到 parties 个线程到达,结束阻塞。
14. CyclicBarrier 和 CountdownLatch 有什么区别?¶
- CyclicBarrier 可以重复使用,而 CountdownLatch 不能重复使用。
CountdownLatch 其实可以把它看作一个计数器,只不过这个计数器的操作是原子操作。可以向 CountdownLatch 对象设置一个初始的数字作为计数值,任何调用这个对象上的 await () 方法都会阻塞,直到这个计数器的计数值被其他的线程减为 0 为止。所以在当前计数到达零之前,await 方法会一直受阻塞。之后,会释放所有等待的线程,await 的所有后续调用都将立即返回。这种现象只出现一次 —— 计数无法被重置。如果需要重置计数,请考虑使用 CyclicBarrier。
CountdownLatch 的一个非常典型的应用场景是:有一个任务想要往下执行,但必须要等到其他的任务执行完毕后才可以继续往下执行。假如我们这个想要继续往下执行的任务调用一个 CountdownLatch 对象的 ##await () 方法,其他的任务执行完自己的任务后调用同一个 CountdownLatch 对象上的 countDown () 方法,这个调用 ##await () 方法的任务将一直阻塞等待,直到这个 CountdownLatch 对象的计数值减到 0 为止。 CyclicBarrier 一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点(common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用,所以称它为循环的 barrier。
十二、设计模式篇¶
1. 你都熟悉哪些设计模式¶
这里肯定是巴拉巴拉一堆设计模式,但是记住最好说清楚,自己最熟悉那几个设计模式,这样面试官就可以问你最熟悉的了。 比如:
- 单例模式:唯一性、安全性、性能、是否懒加载。
- 工厂模式:根据输入的条件生产对象。
- 模板模式:指定一系列算法,算法的骨架是不变的。
- 装饰器模式:动态地给某个方法的功能进行变更。
- 代理模式:房屋中介,访问权限的控制。
- 监听者模式:监听与被监听,只要被监听对象发生变动,监听就立马收到其变动的消息。
- 策略模式:条条大路通北京,每一条大路都是一个策略。
- 享元模式……。 模板模式推荐:
快速理解模板模式 代理模式详解推荐: 细说从代理模式到动态代理 代理模式 VS 装饰模式: 代理模式 VS 装饰模式 工厂方法模式 VS 建造者模式: 工厂方法模式 VS 建造者模式 初中级应对的方法就是搞几个自己很熟悉的,然后面试的时候强调一下自己最熟悉的,然后结合 Spring 中或者 MyBatis 中的来说一下。高级可能就没那么简单的,至少得知道的多点,尤其是要学会对比,比如说:工厂方法模式 VS 建造者模式有什么区别。 建议熟悉一下常用设计模式实际使用案例,例如 Spring 篇、MyBatis 篇中。 推荐在线阅读: 设计模式
十三、其他篇¶
1. 有 8 个球(大小颜色都一模一样),其中一个球比其他 7 个球中的任何一个都重,使用天平秤最多几次能找到最重的那个球?¶
- 拿出两个球,剩下 6 个进行称,每一边 3 个。
- 如果天平秤平衡,那么拿出去的两个球中肯定有个是最重的。再对两个球称重,就能找到最大的。
- 如果天平秤不平衡,那么最重的球就在重的那边三个中的一个。然后再从三个中拿出两个来称,如果平衡,则最重的球就另外一个,否则就是重的那边那个。 所以一共称了 两次 。
1. 分布式幂等性如何设计?¶
在高并发场景的架构里,幂等性是必须得保证的。比如说支付功能,用户发起支付,如果后台没有做幂等校验,刚好用户手抖多点了几下,于是后台就可能多次收到同一个订单请求。不做幂等很容易就让用户重复支付了,这样用户是肯定不能忍的。
解决方案¶
- 查询和删除不在幂等讨论范围,查询肯定没有幂等的说法,删除:第一次删除成功后,后面来删除直接返回 0,也是返回成功。
- 建唯一索引:唯一索引或唯一组合索引来防止新增数据存在脏数据 (当表存在唯一索引,并发时新增异常时,再查询一次就可以了,数据应该已经存在了,返回结果即可)。
- token 机制:由于重复点击或者网络重发,或者 nginx 重发等情况会导致数据被重复提交。前端在数据提交前要向后端服务的申请 token,token 放到 Redis 或 JVM 内存,token 有效时间。提交后后台校验 token,同时删除 token,生成新的 token 返回。Redis 要用删除操作来判断 token,删除成功代表 token 校验通过,如果用 select+delete 来校验 token,存在并发问题,不建议使用。
-
悲观锁:
悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选用(另外还要考虑 id 是否为主键,如果 id 不是主键或者不是 InnoDB 存储引擎,那么就会出现锁全表)。 5. 乐观锁,给数据库表增加一个 version 字段,可以通过这个字段来判断是否已经被修改了。
-
分布式锁,比如 Redis、ZooKeeper 的分布式锁。单号为 key,然后给 Key 设置有效期(防止支付失败后,锁一直不释放),来一个请求使用订单号生成一把锁,业务代码执行完成后再释放锁。
- 保底方案:先查询是否存在此单,不存在此支付单据(上锁),存在就直接返回支付结果。
2. 简单一次完整的 HTTP 请求所经历的步骤?¶
-
DNS 解析(通过访问的域名找出其 IP 地址,递归搜索)。
-
HTTP 请求,当输入一个请求时,建立一个 Socket 连接发起 TCP 的 3 次握手。
如果是 HTTPS 请求,会略微有不同。等到 HTTPS 小节,我们再来讲。
-
客户端向服务器发送请求命令(一般是 GET 或 POST 请求),客户端发送请求头信息和数据。
这个是补充内容,面试一般不用回答。
客户端的网络层不用关心应用层或者传输层的东西,主要做的是通过查找路由表确定如何到达服务器,期间可能经过多个路由器,这些都是由路由器来完成的工作,我不做过多的描述,无非就是通过查找路由表决定通过哪个路径到达服务器。
客户端的链路层,包通过链路层发送到路由器,通过邻居协议查找给定 IP 地址的 MAC 地址,然后发送 ARP 请求查找目的地址。如果得到回应后就可以使用 ARP 的请求应答交换的 IP 数据包,然后发送 IP 数据包到达服务器的地址。
-
服务器发送应答头信息,服务器向客户端发送数据。
-
服务器关闭 TCP 连接(4 次挥手)。
这里是否关闭 TCP 连接,也根据 HTTP Keep-Alive 机制有关。
同时,客户端也可以主动发起关闭 TCP 连接。
-
客户端根据返回的 HTML、CSS、JS 进行渲染。
下面使用《图解 HTTP》里的一张图:
3. 说说分布式事务解决方案有哪些?¶
更多方案详情,推荐阅读:
4. 说说常用的 JVM 调优命令和工具有哪些?¶
常用 JVM 调优工具分为两类:
JDK 自带监控工具:jconsole 和 jvisualvm 2、第三方有:MAT(Memory Analyzer Tool)、GChisto。
- jconsole,Java Monitoring and Management Console 是从 JDK 1.5 开始,在 JDK 中自带的 Java 监控和管理控制台,用于对 JVM 中内存,线程和类等的监控。
- jvisualvm,JDK 自带全能工具,可以分析内存快照、线程快照;监控内存变化、GC 变化等。
- MAT,Memory Analyzer Tool,一个基于 Eclipse 的内存分析工具,是一个快速、功能丰富的 Java heap 分析工具,可以帮助我们查找内存泄漏和减少内存消耗。
- GChisto,一款专业分析 GC 日志的工具。
5. 说说你对 JVM 内存溢出和内存泄漏的理解¶
-
内存泄漏 内存使用后未得到及时释放,又不能被 GC 回收,导致虚拟机不能再次使用该内存,此时这段内存就泄露了。 常见避免方法:
- 尽早释放无用对象
- 尽量少用静态变量。因为静态变量是全局的,GC 不会回收
- 尽量运用对象池技术以提高系统性能
- 不要在经常调用的方法中创建对象,尤其是忌讳在循环中创建对象