Java高频面试题(2023版)

Java高频面试题(2023最新版)

Java基础

1、JDK 和 JRE 有什么区别?

JDK(Java Development Kit),Java开发工具包
JRE(Java Runtime Environment),Java运行环境
JDK中包含JRE,JDK中有一个名为jre的目录,里面包含两个文件夹bin和lib,bin就是JVM,lib就是JVM工作所需要的类库。

2、== 和 equals 的区别是什么?

1、对于基本类型,比较的是值;
2、对于引用类型,比较的是地址;
3、equals不能用于基本类型的比较;
4、如果没有重写equals,equals就相当于==;
5、如果重写了equals方法,equals比较的是对象的内容;

3、final 在 java 中有什么作用?

(1)用来修饰一个引用
如果引用为基本数据类型,则该引用为常量,该值无法修改;
如果引用为引用数据类型,比如对象、数组,则该对象、数组本身可以修改,但指向该对象或数组的地址的引用不能修改。
如果引用时类的成员变量,则必须当场赋值,否则编译会报错。
(2)用来修饰一个方法
当使用final修饰方法时,这个方法将成为最终方法,无法被子类重写。但是,该方法仍然可以被继承。
(3)用来修饰类
当用final修改类时,该类成为最终类,无法被继承。比如常用的String类就是最终类。

4、java 中的 Math.round(-11.3) 等于多少?

-11
Math提供了三个与取整有关的方法:ceil、floor、round
(1)ceil:向上取整;
Math.ceil(11.3) = 12;
Math.ceil(-11.3) = -11;
(2)floor:向下取整;
Math.floor(11.3) = 11;
Math.floor(-11.3) = -12;
(3)round:四舍五入;加0.5然后向下取整。
Math.round(11.3) = 11;
Math.round(11.8) = 12;
Math.round(-11.3) = -11;
Math.round(-11.8) = -12;

5、String str="i"与 String str=new String(“i”)一样吗?

两个语句都会先去字符串常量池中检查是否已经存在 “xyz”,如果有则直接使用,如果没有则会在常量池中创建 “xyz” 对象。
另外,String s = new String(“xyz”) 还会通过 new String() 在堆里创建一个内容与 “xyz” 相同的对象实例。
所以前者其实理解为被后者的所包含。

6、new String(“a”) + new String(“b”) 会创建几个对象?

如果字符串常量池中没有“a”和“b”,就是6个,否则就是4个
对象1:new StringBuilder()
对象2:new String(“a”)
对象3:常量池中的"a"
对象4:new String(“b”)
对象5:常量池中的"b"
深入剖析:StringBuilder中的toString():
对象6:new String(“ab”)
强调一下,toString()的调用,在字符串常量池中,没有生成"ab"
字符串常量池: JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。

7、如何将字符串反转?

将对象封装到stringBuilder中,调用reverse方法反转。

8、String 类的常用方法都有那些?

(1)常见String类的获取功能

length:获取字符串长度;
charAt(int index):获取指定索引位置的字符;
indexOf(int ch):返回指定字符在此字符串中第一次出现处的索引;
substring(int start):从指定位置开始截取字符串,默认到末尾;
substring(int start,int end):从指定位置开始到指定位置结束截取字符串;

(2)常见String类的判断功能

equals(Object obj): 比较字符串的内容是否相同,区分大小写;
contains(String str): 判断字符串中是否包含传递进来的字符串;
startsWith(String str): 判断字符串是否以传递进来的字符串开头;
endsWith(String str): 判断字符串是否以传递进来的字符串结尾;
isEmpty(): 判断字符串的内容是否为空串"";

(3)常见String类的转换功能

byte[] getBytes(): 把字符串转换为字节数组;
char[] toCharArray(): 把字符串转换为字符数组;
String valueOf(char[] chs): 把字符数组转成字符串。valueOf可以将任意类型转为字符串;
toLowerCase(): 把字符串转成小写;
toUpperCase(): 把字符串转成大写;
concat(String str): 把字符串拼接;

(4)常见String类的其他常用功能

replace(char old,char new) 将指定字符进行互换
replace(String old,String new) 将指定字符串进行互换
trim() 去除两端空格
int compareTo(String str) 会对照ASCII 码表 从第一个字母进行减法运算 返回的就是这个减法的结果,如果前面几个字母一样会根据两个字符串的长度进行减法运算返回的就是这个减法的结果,如果连个字符串一摸一样 返回的就是0。

9、String、String buffer和String builder区别?

String字符串对象是不可变对象,虽然可以共享常量对象,但是对于频繁字符串的修改和拼接操作,效率极低
后面两者都是可变字符串对象
StringBuffer:线程安全的(因为它的方法有synchronized修饰)效率低
StringBuilder:线程不安全的 性能高
小结:
(1)如果要操作少量的数据用 String;
(2)多线程操作字符串缓冲区下操作大量数据用 StringBuffer;
(3)单线程操作字符串缓冲区下操作大量数据用 StringBuilder。
String不可变的真正原因?
1、字符串的本质是char数组(jdk9以后是byte[]),被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。
2、String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。

10、 字符串拼接问题

(1)常量+常量:结果是常量池(常量优化,因为编译期间就可以确定结果)
(2)常量与变量 或 变量与变量:结果是堆
(3)拼接后调用intern方法:结果在常量池

11、什么是自动拆装箱? int和Integer有什么区别?以及以下程序运行结果。

基本数据类型,如int,float,double,boolean,char,byte,不具备对象的特征,不能调用方法。
装箱:将基本类型转换成包装类对象
拆箱:将包装类对象转换成基本类型的值
java为什么要引入自动装箱和拆箱的功能?主要是用于java集合中,List list=new ArrayList();
list集合如果要放整数的话,只能放对象,不能放基本类型,因此需要将整数自动装箱成对象。
实现原理:javac编译器的语法糖,底层是通过Integer.valueOf()和Integer.intValue()方法实现。
区别:
(1)Integer是int的包装类,int则是java的一种基本数据类型
(2)Integer变量必须实例化后才能使用,而int变量不需要
(3)Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值
(4)Integer的默认值是null,int的默认值是0

包装类型的缓存机制

Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。
Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 True or False。
如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡。
两种浮点数类型的包装类 Float,Double 并没有实现缓存机制。

1.  public class Test01 {
    
      
2.     public static void main(String[] args){
    
      
3.        Integer a = 127;  
4.        Integer b = 127;  
5.        Integer c = 128;  
6.        Integer d = 128;  
7.        System.out.println(a==b); //true  
8.        System.out.println(c==d); //false 
		  
		  Float i11 = 333f;
		  Float i22 = 333f;
		  System.out.println(i11 == i22);// 输出 false

	      Double i3 = 1.2;
	      Double i4 = 1.2;
		  System.out.println(i3 == i4);// 输出 false

9.    }  
10.} 

建议:所有整型包装类对象之间值的比较,建议使用 equals 方法比较。

12、类和对象的关系

类是对同一类事物的描述,是抽象的
对象是一类事物的实现,是具体的
类是模板,对象是类的实例

13、怎么理解面向对象?

继承:描述的是事物之间的所属关系,is-a,子类继承父类的特征和行为,复用性 扩展性
封装:内部属性私有化,对外提供公共的访问方式,高内聚,低耦合
多态:同一个行为有不同的表现形式,多态存在3个条件:①继承②重写③父类引用指向子类对象 如List list = new ArrayList();

14、接口与抽象类的区别?

  1、成员变量
     	抽象类:既可以是常量也可以是变量
     	接口:一定是常量
  2、构造
      	抽象类:有构造
      	接口:没有构造
  3、成员方法
    	抽象类:既可以是普通方法,也可以是抽象方法
    	接口:JDK1.8之前,必须是抽象方法(JDK8之后出现了默认方法和静态方法,JDK9出现了私有方法)
  4、设计理念
      	抽象类:作为一个继承体系顶层,将共性行为和属性被继承下去,体现is-a的关系 单继承
      	接口:作为一个功能进行扩展 多实现(子类可以实现多个接口)

15、重载与重写有什么区别?

1、重载发生在本类,重写发生在父类与子类之间;
2、重载的方法名必须相同,重写的方法名相同且返回值类型必须相同;
3、重载的参数列表不同,重写的参数列表必须相同。
4、重写的访问权限不能比父类中被重写的方法的访问权限更低。
5、构造方法不能被重写

16、为什么要使用克隆?如何实现对象克隆?深拷贝和浅拷贝区别是什么?

(1)什么要使用克隆?
想对一个对象进行复制,又想保留原有的对象进行接下来的操作,这个时候就需要克隆了。
(2)如何实现对象克隆?
实现Cloneable接口,重写clone方法;
实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深克隆。
BeanUtils,apache和Spring都提供了bean工具,只是这都是浅克隆。
(3)深拷贝和浅拷贝区别是什么?
浅拷贝:仅仅克隆基本类型变量,不克隆引用类型变量;
深拷贝:既克隆基本类型变量,又克隆引用类型变量;

17、java 中 IO 流分为几种?

所谓的IO就是实现数据从磁盘的读取和写入。
实际上,除了磁盘以外,内存、网络都可以作为 I/O 流的数据来源和目的地。
在 Java 里面,提供了字符流和字节流两种方式来实现数据流的操作。
Java 里面提供了 Socket的方式来实现数据的网络传输。

在这里插入图片描述

18、 常见的IO模型:BIO、NIO、AIO

BIO

定义:同步阻塞IO,传统的IO模型,实现数据从磁盘中的读取以及写入。
特点:简单使用方便,但并发处理能力低。

NIO

定义:同步非阻塞 IO,是传统 IO 的升级,它是支持面向缓冲的,基于通道的 I/O 操作方法。了特点:多路复用,减少CPU的小号,适用于高并发。

AIO

定义:异步非阻塞IO,是 NIO 的升级,也叫 NIO2,异步 IO 的操作基于事件和回调机制。
特点:异步非阻塞

19、什么是 java 序列化?怎么实现序列化

序列化:把内存中的java对象转换为二进制字节流,用来实现存储或传输
反序列化:将从文件或者网络上获取到的对象的字节流转化为对象
序 列 化 的 实 现 :
只有实现了Serializable和Externalizeble的接口的类才能实现序列化。
Java.IO.ObjectOutputStream代表对象输出流。writeObject(Object obj)方法可对参数指定的obj对象进行序列化。
Java.IO.ObjectInputStream代表对象输入流,它的readObject()方法可以反序列化。
如果类中每个成员变量不想被序列化,可以用transient关键字修饰。

20、final、finally、finalize的区别?

final:修饰符(关键字)有三种用法:修饰类、变量和方法。修饰类时,意味着它不能再派生出新的子类,即不能被继承,因此它和abstract是反义词。修饰变量时,该变量使用中不被改变,必须在声明时给定初值,在引用中只能读取不可修改,即为常量。修饰方法时,也同样只能使用,不能在子类中被重写。
finally:通常放在try…catch的后面构造最终执行代码块,这就意味着程序无论正常执行还是发生异常,这里的代码只要JVM不关闭都能执行,可以将释放外部资源的代码写在finally块中。
finalize:Object类中定义的方法,Java中允许使用finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在销毁对象时调用的,通过重写finalize() 方法可以整理系统资源或者执行其他清理工作。

21、Object中有哪些方法?

1protected Object clone()--->创建并返回此对象的一个副本。 
(2boolean equals(Object obj)--->指示某个其他对象是否与此对象“相等”。 
(3protected void finalize()--->当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。 
(4Class<? extendsObject> getClass()--->返回一个对象的运行时类。 
(5int hashCode()--->返回该对象的哈希码值。 
(6void notify()--->唤醒在此对象监视器上等待的单个线程。 
(7void notifyAll()--->唤醒在此对象监视器上等待的所有线程。 
(8String toString()--->返回该对象的字符串表示。 
(9void wait()--->导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法。 
	void wait(long timeout)--->导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll()方法,或者超过指定的时间量。 
	void wait(long timeout, int nanos)--->导致当前的线程等待,直到其他线程调用此对象的 notify()

22、异常相关

在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类:
Exception :程序本身可以处理的异常,可以通过 catch 来进行捕获。Exception 又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (非受检查异常,可以不处理)。
Error:Error 属于程序无法处理的错误 ,我们没办法通过 catch 来进行捕获。例如 Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。

1、编译时异常:
IOException 输入输出流异常
FileNotFoundException 文件找不到的异常
ClassNotFoundException 类找不到异常
DataFormatException 数据格式化异常
NoSuchFieldException 没有匹配的属性异常
NoSuchMethodException 没有匹配的方法异常
SQLException 数据库操作异常
TimeoutException 执行超时异常
2、运行时异常(RuntimeException 及其子类都统称为非受检查异常)
ArrayIndexOutofBoundsException 数组越界异常
ClassCastException 类型转换异常
NullPointerException 空指针异常
IllegalAccessException 非法的参数异常
NumberFormatException 字符串转换为数字异常;出现原因:字符型数据中包含非数字型字符。
ArithmeticException 算术异常

23、hashcode是什么?有什么作用?

hashcode是一种编码方式,在Java中,每个对象都会有一个hashcode,Java可以通过这个hashcode来识别一个对象。hashcode方法返回该对象的哈希码值,该方法为哈希表提供一些优点,就是通过这个哈希实现快速查找键对象
1、如果equals相等,hashcode一定相同
2、但hashcode相等,equals不一定相等(hash碰撞)
3、重写equals方法一定要重写hashcode方法(两个相等的对象hashcode一定相同)
HashCode的存在主要是为了查找的快捷性,HashCode是用来在散列存储结构中确定对象的存储地址的。如hashmap、hashtable

24、谈谈你对反射的理解?

(1)反射机制:
所谓的反射机制就是java语言在运行时拥有一项自观的能力。通过这种能力可以彻底的了解自身的情况为下一步的动作做准备。
Java的反射机制的实现要借助于4个类:class,Constructor,Field,Method;
其中class代表的时类对 象,Constructor-类的构造器对象,Field-类的属性对象,Method-类的方法对象。通过这四个对象我们可以粗略的看到一个类的各个组 成部分。
(2)Java反射的作用:
在Java运行时环境中,对于任意一个类,可以知道这个类有哪些属性和方法。对于任意一个对象,可以调用它的任意一个方法。这种动态获取类的信息以及动态调用对象的方法的功能来自于Java 语言的反射(Reflection)机制。
(3)Java 反射机制提供功能
在运行时判断任意一个对象所属的类。
在运行时构造任意一个类的对象。
在运行时判断任意一个类所具有的成员变量和方法。
在运行时调用任意一个对象的方法

优点

  • 增加程序的灵活性,可以在运行的过程中动态对类进行修改和操作
  • 提高代码的复用率,比如动态代理,就是用到了反射来实现

缺点:

  • 破坏了代码本身的抽象性
  • 反射会涉及到动态类型的解析,所以 JVM 无法对这些代码进行优化,导致性能要比非反射调用更低。

25、linux常用命令

三个简单
ls:列出目录中的目录
cd:切换目录
mkdir :创建文件夹
cp:复制文件
mv:移动文件
cat、more:查看文件
三个复杂
ifconfig:查询当前网卡配置信息
tail -f xx.out 动态查看日志
netstat -anp | grep 端口号:查看端口号
top:查看内存
ps aux:查看进程
kill -9 进程号:杀死进程

26、docker常用命令

systemctr start/stop/restart docker 开启/停止/重启docker
docker images 查看所有镜像
docker pull 拉取镜像
docker rmi -f 删除镜像

docker ps 查看正在运行的容器
docker ps -a 查看所有的容器
docker run 启动容器
docker stop 停止容器
docker exec -it ‘容器名称或容器ID’ bash

docker logs -f 容器id/容器名称 实时查看日志
docker logs --tail=500 [容器id] 查看最后500行日志
docker logs -f --since “2022-06-22” [容器id或服务名称] 查看某个时间至今的日志

集合

1、数组和集合的区别

  • 相同点:都是容器,可以存储数据
  • 不同点:
    1、数组长度是不可变的,集合长度可变
    2、数组可以存储基本数据类型和引用数据类型,集合只能存储引用数据类型,如果要存基本数据类型,要转换为其对应的包装类
    3、数组转集合:Arrasys.asList()
    4、集合转数组:list.toArray(new String[list.size()])

2、说一下集合体系?

在这里插入图片描述

3、说一下ArrarList和LinkedList区别?

ArrayList

  • ArrayList:底层是动态数组,与Java中的数组相比,它的容量能动态增长。查询效率高(有索引)
  • 时间复杂度:查询时O(1),增删是O(n)

LinkedList

  • linkedList:底层是双向链表,增删效率高,也要看实际情况,对于单条数据的增删,ArrayList的效率反而优于linkedList。对于批量增删,linkedList大大优于ArrayList
  • 时间复杂度:查询O(n),增删O(1)
  • 都不是线程安全的

4、ArrayList和Vector的底层原理和扩容机制

  • ArrayList:是线程不安全的动态数组,JDK1.7后初始化为空数组,在添加第一个元素时初始化为长度为10的数组,如果容量满了,按照1.5倍扩容。支持foreach和Iterator遍历。
  • Vector:是线程安全的动态数组,初始化为长度为10的数组,如果容量满了,按照2.0倍扩容。除了支持foreach和Iterator遍历,还支持Enumeration迭代。

5、HashMap底层原理?

1、当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标 。
2、存储时,如果出现hash相同的key,有可能会先两种情况,

  • 如果key之前是存在的,则覆盖原始值;
  • 如果key不同则将当前的key-value放入链表中 。(jdk1.7头插法,jdk1.8尾插法)

3、获取时,通过hash直接找到对应的index,在进一步判断key是否相同,从而找到对应的值

JDK1.8之前

拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。

JDK1.8

jdk1.8是数组+链表+红黑树。当链表长度大于阈值(默认为8) 时且数组长度大于64时,将链表转化为红黑树,以减少搜索时间。当链表长度大于8且数组长度小于64时开始扩容 ,红黑树拆分成的树的结点数小于等于临界值6个,则退化成链表。

6、HashMap中put方法具体流程?

HashMap的底层结构在jdk1.7中由数组+链表实现,在jdk1.8中由数组+链表+红黑树实现,以数组+链表的结构为例。

JDK1.8之前Put方法:

在这里插入图片描述
(1)当第一次put时,数组初始化为一个长度为16的Entry数组
(2)特殊考虑:如果key为null,index直接是[0],hash也是0
(3)如果key不为null,会对key的hashCode()值做一个hash(key)再次哈希的运算,这样可以使得Entry对象更加散列的存储到table中
(4)计算当前对象的元素在数组中的下标index = table.length-1 & hash;
(5)如果table[index]为空,创建Entry对象,把value存到数组中,size++
(6)如果table[index]不为空,判断是否出现hash值相同的key

  • 如果key相同,则覆盖原始值;
  • 如果key不同(出现冲突),则将当前的key-value放入链表中 (头插法)

JDK1.8之后Put方法:
在这里插入图片描述

(1)当第一次put时,数组初始化为一个长度为16的table数组
(2)特殊考虑:如果key为null,index直接是[0],hash也是0
(3)如果key不为null,会对key的hashCode()值做一个hash(key)再次哈希的运算
(4)计算当前对象的元素在数组中的下标index = table.length-1 & hash;

(5) 如果table[index]==null,那么直接创建一个Node结点存储到table[index]中即可,size++
(6)如果table[index]不为空,判断是否出现hash值相同的key

  • 如果key相同,则覆盖原始值;
  • 如果key不同(出现冲突),则将当前的key-value放入链表中 (尾插法)
    与jdk1.7不同的是,当链表的长度达到8并且数组长度达到64时,此时将链表转化为红黑树去存储,可以提高性能和减少搜索时间。
    如果链表长度大于8,并且数组长度小于64时,并不会树化,而是数组扩容,因为红黑树涉及到左旋、右旋、变色等操作。

7、HashMap扩容机制?

jdk1.7:

if(size >= threshold  &&  table[index]!=null){
    
    
	①会扩容
	②会重新计算key的hash
	③会重新计算index
}

jdk1.8:

if(size > threshold ){
    
    
     ①会扩容
     ②会重新计算key的hash
     ③会重新计算index
 }

扩容之后会重新计算key的hash,会重新计算index
rehash: 扩容时在转移元素的过程中,
如 rehash为true,那么该元素转移之后,就有可能被放在新数组任意一个位置。
若 rehash为false,那么该元素转移之后,就会和老数组在同一个位置或者会被转移到元素在老数组位置所在索引 +老数组的长度

8、HashMap的寻址算法?

1、计算出key的hashCode赋值给h
2、hash = key.hashCode() ^ (h >>> 16) 按位异或
3、index = hash & (table.length-1) 按位与
​&(按位与运算):相同的二进制数位上,都是1的时候,结果为1,否则为0。
​4)^(按位异或运算):相同的二进制数位上,数字相同,结果为0,不同为1。

9、HashMap 多线程操作导致死循环问题

JDK1.7 及之前版本的 HashMap 在多线程环境下扩容操作可能存在死循环问题,这是由于当一个桶位中有多个元素需要进行扩容时,多个线程同时对链表进行操作,头插法可能会导致链表中的节点指向错误的位置,从而形成一个环形链表,进而使得查询元素的操作陷入死循环无法结束。
为了解决这个问题,JDK1.8 版本的 HashMap 采用了尾插法而不是头插法来避免链表倒置,使得插入的节点永远都是放在链表的末尾,避免了链表中的环形结构。
但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在数据覆盖的问题。并发环境下,推荐使用 ConcurrentHashMap 。

10、哈希表的底层数组长度为什么是 2 的 n 次方?

因为 2 的 n 次方-1 的二进制值是前面都 0,后面几位都是 1,这样的话,与 hash 进行 &运算的结果就能保证在[0,table.length-1]范围内,而且是均匀的。
保证index可以更散列均匀分布[0,table.length-1],减少hash冲突。

11、HashMap和HashTable区别?

1、HashMap是线程不安全的,HashTable是线程安全的(synchronized修饰的);
2、HashMap底层是数组+链表(jdk1.7)数组+链表+红黑树(jdk1.8),HashTable底层是数组+链表
3、HashMap中允许键和值为null,HashTable不允许;
4、HashMap的默认容器是16,为2倍扩容,HashTable默认是11,为2N+1扩容;
HashMap的线程安全问题可以使用Collections的synchronizedMap(Map<K,V> m) 方法解决。
红黑树:左旋、右旋、变色

12、哪些集合类是线程安全的?

Vector:就比Arraylist多了个同步化机制(线程安全)。
Stack:栈,也是线程安全的,继承于Vector。
Hashtable:就比Hashmap多了个线程安全。
CopyAndWriteList:写时复制,采用ReentrantLock
ConcurrentHashMap:是一种高效且线程安全的集合。

13、Hashmap树化链表长度为什么是8?负载因子为什么是0.75?

链表长度符合泊松分布,各个长度的命中概率逐渐递减,当长度为8时,hash碰撞的概率为千万分之6.
负载因子是计算扩容的阈值,作用就是节省时间和空间,当加载因子为0.75的时候,空间利用率很高,而且避免了相当多的hash冲突,使得底层的链表和红黑树的长度更低,提升了空间利用率。

14、JDK7与JDK8中HashMap的不同点?

1、数据结构
jdk7是数组+链表,jdk8是数组+链表+红黑树
2、链表插入方式
jdk7:头插法,扩容转移元素的时候也是使用的头插法,头插法速度更快,无需遍历链表,但是在多线程扩容的情况下使用头插法会出现循环链表的问题,导致CPU飙升
jdk8:尾插法,反正要去计算链表当前结点的个数,反正要遍历的链表的,所以直接使用尾插法
3、hash算法
jdk7hash算法更复杂,这样hash值越散列,通过二次hash,jdk8没有这个逻辑,jdk8还可以通过红黑树降低hash冲突,提升查询效率
4、扩容机制
jdk7:size >= threshold && table[index]!=null&&老数组的容量没有达到integer最大值。JDK7是每次转移一个元素
jdk8:size >= threshold ,JDK8是先算出来当前位置上哪些元素在新数组的低位上,哪些在新数组的高位上,然后在一次性转移

15、ConcurrentHashMap是怎么保证并发安全的?

jdk1.7 :采用Segments 分段锁,底层是由Segments + HashEntry 链表实现,Segment 继承了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。
jdk1.8:数据结构和hashmap类似。取消了 Segment 分段锁的设计,采用 Node + CAS + synchronized 保证线程安全,锁粒度更细,synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升

16、fail-safe和fail-fast有了解吗?

是多线程并发操作集合时的一种失败处理机制
fail-fast:表示快速失败,在集合遍历中,一旦发现容器中数据修改了,就会抛出异常
java.util 包下的集合类都是快速失败机制的,常见的的使用 fail-fast 方式遍历的容器有 HashMap 和 ArrayList 等。

fail-safe:表示失败安全,在集合遍历中,集合中元素被修改,也不会抛出异常。是因为采用这种机制的集合在遍历时,是先复制原有的集合,在拷贝上的集合进行遍历。
java.util.concurrent 包下的容器都是安全失败的,可以在多线程下并发使用,并发修改。
使用 fail-safe方式有ConcurrentHashMap 和CopyOnWriteArrayList

JavaWeb

1、Javaweb的三大组件

1、Servlet是用来接收处理客户端请求的动态资源,响应数据
2、filter主要负责拦截请求和放行。如:权限控制、统一编码处理、敏感字符处理等等.
3、Listener表示服务器的事件监听器,用于监听三个域对象的状态(对象、对象的属性)变化

2、JSP 和 Servlet 的区别

  • JSP的本质就是Servlet,Web容器将JSP的代码编译成JVM能够识别的Servlet
  • jsp更擅长表现于页面显示,servlet更擅长于逻辑控制.
  • Servlet中没有内置对象,Jsp中的内置对象都是必须通过HttpServletRequest对象,HttpServletResponse对象以及HttpServlet对象得到.
  • Jsp是Servlet的一种简化,使用Jsp只需要完成程序员需要输出到客户端的内容,Jsp中的Java脚本如何镶嵌到一个类中,由Jsp容器完成。而Servlet则是个完整的Java类,这个类的Service方法用于生成对客户端的响应。

3、拦截器和过滤器的区别

1、实现原理不同
过滤器 是基于函数回调的,拦截器 则是基于Java的反射机制==(动态代理)==实现的。自定义的过滤器中都会实现一个 doFilter()方法,这个方法有一个FilterChain 参数,而实际上它是一个回调接口
2、使用范围不同
过滤器依赖于servlet容器,只能在web程序中
拦截器是spring提供的,不依赖于servlet容器,可以单独使用,可以获取ioc容器中的各个bean
3、触发时机不同
过滤器Filter是在请求进入容器后,但在进入servlet之前进行预处理,请求结束是在servlet处理完以后。
拦截器 Interceptor 是在请求进入servlet后,在进入Controller之前进行预处理的,Controller 中渲染了对应的视图之后请求结束。
4、拦截的请求范围不同
拦截器只拦截action请求,而过滤器则几乎拦截所有的请求起作用。

4、Http 常见的状态码有哪些?

200 OK //客户端请求成功
301 Moved Permanently(永久移除),请求的 URL 已移走。Response 中应该包含一个 Location URL, 说明资源现在所处的位置
302 found 重定向
400 Bad Request //客户端请求有语法错误,不能被服务器所理解
401 Unauthorized //请求未经授权,这个状态代码必须和 WWW-Authenticate 报头域一起使用
403 Forbidden //服务器收到请求,但是拒绝提供服务 权限/跨域问题
404 Not Found //请求资源不存在,eg:输入了错误的 URL
405:请求方式错误
500 Internal Server Error //服务器发生不可预期的错误
503 Server Unavailable //服务器当前不能处理客户端的请求,一段时间后可能恢复正常

5、GET 和POST 的区别?

① 浏览器和表单的默认提交方式是get,get请求效率比post高
② get请求参数在url地址后拼接,所以有以下特点:
请求报文没有请求体
少了和请求体相关的请求头参数
参数在url地址中拼接,上传参数大小有限制,不能用来上传文件,相对post请求不安全
③ post请求参数在请求报文的请求体中携带,有以下特点:
请求报文有请求体,相对安全
请求头多了和请求体相关的参数
请求体数据没有大小限制可以用来上传文件

6、Cookie 和Session 的区别

session 的工作原理是客户端登录完成之后,服务器会创建对应的 session,session 创建完之后,会把 session 的 id 发送给客户端,客户端再存储到浏览器的cookie中。这样客户端每次访问服务器时,都会带着 sessionid,服务器拿到 sessionid 之后,在内存找到与之对应的 session 这样就可以正常工作了。
Cookie:cookie是服务端生成发送给客户端,保存在客户端的临时的少量的数据。下次请求同一网站时会携带cookie
Token:Token是服务端生成的一串字符串,当作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token并将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。

Cookie 和session 的不同点:

  • 存储位置 :cookie在客户端浏览器;session在服务器;
  • 存储容量:cookie一般不超过4K,一个站点最多保留20个cookie;session存储在服务器上可以任意存储数据。当 session存储数据太多时,服务器可选择进行清理。
  • 数据类型 :cookie只能保存ASCII字符串,并需要通过编码方式存储为Unicode字符或者二进制数据;session中能存储任何类型的数据,包括并不局限于String、integer、list、map等;
  • 安全性 :cookie对客户端是可见的,可以被删除和篡改,不安全;session存储在服务器上,安全;
  • 存储多样性 :session 可以存储在Redis中、数据库中、应用程序中;而 cookie 只能存储在浏览器中
  • 跨域支持cookie支持跨域,session不支持跨域;

7、转发和重定向的区别?(至少写3个)

1、请求次数: 转发一次,重定向两次
2、浏览器地址栏:转发不变,重定向改变
3、使用request域共享数据:转发是一次请求共享数据,重定向两次请求,不能共享数据
4、相对路径: 转发地址不变会造成转发后的页面中的相对位置发生改变引起相对路径失效,重定向不会
5、效率:转发浏览器一次请求效率高,重定向效率低
6、WEB-INF下资源:转发可以访问,重定向不可以
7、跳转限制:重定向可以跳转到任意URL,转发只能跳转本站点资源
8、发生行为:重定向是客户端行为,转发是服务器行为

8、说出几种vue当中的指令及其用法?

v-model双向数据绑定;
v-for循环;
v-if v-show 显示与隐藏;
v-on绑定事件;
v-once: 只绑定一次。

9、说出vue的声明周期?(vue2、vue3)

当Vue对象创建之前触发的函数(beforeCreate)
Vue对象创建完成触发的函数(Created)
当Vue对象开始挂载数据的时候触发的函数(beforeMount)
当Vue对象挂载数据的完成的时候触发的函数(Mounted)
Vue对象中的data数据发生改变之前触发的函数 (beforeUpdate)
Vue对象中的data数据发生改变完成触发的函数(Updated)
Vue对象销毁之前触发的函数 (beforeDestroy)
Vue对象销毁完成触发的函数(Destroy)

10、thymeleaf是什么?常用的标签

Thymeleaf是一个模板引擎,可以替代jsp。开箱即用
Th:text:文本替换
Th:value:属性赋值
Th:href:链接地址
Th:if:条件判断
Th:action:表单提交的地址

11、什么是 XSS 攻击,如何避免?

XSS 攻击:即跨站脚本攻击,它是 Web 程序中常见的漏洞。原理是攻击者往 Web 页面里插入恶意的脚本代码(css 代码、Javascript 代码等),当用户浏览该页面时,嵌入其中的脚本代码会被执行,从而达到恶意攻击用户的目的,如盗取用户 cookie、破坏页面结构、重定向到其他网站等。
预防 XSS 的核心是必须对输入的数据做过滤处理。

12、 什么是 CSRF 攻击,如何避免?

CSRF:Cross-Site Request Forgery(中文:跨站请求伪造),可以理解为攻击者盗用了你的身份,以你的名义发送恶意请求,比如:以你名义发送邮件、发消息、购买商品,虚拟货币转账等。
防御手段:

  • 验证请求来源地址;
  • 关键操作添加验证码;
  • 在请求地址添加 token 并验证。

Spring

1、谈谈你对Spring的理解

Spring是一个IOC和AOP 开源的轻量级容器框架,为简化企业级应用开发而生
Spring 容器的主要核心组件是:BeanFactory
控制反转(IOC),传统的 java 开发模式中,当需要一个对象时,我们会自己使用 new 或者 getInstance 等直接或者间接调用构造方法创建一个对象。而在 spring 开发模式中,spring 容器使用了工厂模式为我们创建了所需要的对象,不需要我们自己创建了,直接调用spring 提供的对象就可以了,这是控制反转的思想。对于 spring 框架来说,就是由 spring 来负责控制对象的生命周期和对象间的关系。
依赖注入(DI),spring 使用 javaBean 对象的 set方法或者带参数的构造方法为我们在创建所需对象时将其属性自动设置所需要的值的过程,就是依赖注入的思想。
面向切面编程(AOP),是面向切面编程的一种思想,是对OOP进行增强,可以将与业务无关的,却为业务模块所共同调用的非核心代码封装成(比如事务管理、日志管理、权限控制等等)一个个切面,然后在运行时通过动态代理注入到核心业务功能中。减少系统中的重复代码,降低了模块间的耦合度,同时 提高了系统的可维护性。AOP 底层是动态代理,如果是接口采用 JDK 动态代理,如果是类采用CGLIB 方式实现动态代理。
在我们的项目中我们自己写AOP的场景其实很少 , 但是我们使用的很多框架的功能底层都是AOP , 例如 :

1、统一日志处理

2、spring中内置的事务处理

2、SpringAOP和AspectJ的区别?

Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。 Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单,如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比 Spring AOP 快很多

3、介绍一下Spring bean 的生命周期?

(1)默认情况下,IOC容器中bean的生命周期分为五个阶段:

  • 调用构造器 或者是通过工厂的方式创建Bean对象(实例化)
  • 给bean对象的属性注入值
  • 调用初始化方法,进行初始化, 初始化方法是通过init-method来指定的.
  • 使用
  • IOC容器关闭时, 销毁Bean对象.

(2)当加入了Bean的后置处理器后,IOC容器中bean的生命周期分为七个阶段:

  • 调用构造器 或者是通过工厂的方式创建Bean对象
  • 给bean对象的属性注入值
  • 执行Bean后置处理器中的 postProcessBeforeInitialization
  • 调用初始化方法,进行初始化, 初始化方法是通过init-method来指定的.
  • 执行Bean的后置处理器中 postProcessAfterInitialization
  • 使用
  • IOC容器关闭时, 销毁Bean对象

4、介绍一下Spring bean 的依赖注入方式?

  • setter 属性注入
  • 构造方法注入
  • 注解方式注入

5、介绍一下Spring bean 的作用域?

理论上来说,常规的生命周期只有两种:
singleton:spring ioc 容器中只存在一个 bean 实例,bean 以单例模式存在,是系统默认值;
prototype:每次从容器调用 bean 时都会创建一个新的实例,既每次 getBean()相当于执行 new Bean()操作;
但基于Spring在Web 环境下,增加了一个会话维度来控制Bean的生命周期,主要有以下三种:
request:每次 http 请求都会创建一个 bean;
session:同一个session 共享一个 bean 实例;
global-session:针对全局 session 纬度,共享同一个 Bean 实例
「注意:」 使用 prototype 作用域需要慎重的思考,因为频繁创建和销毁 bean 会带来很大的性能开销。

6、请描述一下Spring 的事务管理

(1)声明式事务管理:,在 Spring 配置文件中声明或注解@Transactiona来处理事务。其本质是通过AOP实现,将事务管理代码从业务方法中抽离了出来,非侵入式的编程方式
(2)编程式事务控制:需要使用TransactionTemplate来进行实现,这种方式实现对业务代码有侵入性,因此在项目中很少被使用到。

7、Spring事务的传播方式?

首选,所谓的事务传播行为,就是多个声明了事务的方法相互调用的时候,这个事务应该如何传播。
比如说,methodA()调用 methodB(),两个方法都开启了事务。那么 methodB()是开启一个新事务,还是继续在 methodA()这个事务中执行?
PROPAGATION_REQUIRED:默认的事务传播行为,指的是如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
PROPAGATION_REQUIRES_NEW:不管是否存在事务,都会创建一个新的事务,如果当前存在事务,则把当前事务挂起。

8、Spring事务失效场景?

在这里插入图片描述

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

事务不生效(7种)

1、自定义的事务方法(即目标方法)访问权限是非public的
在这里插入图片描述

2、方法用final或static修饰,不会生效
在这里插入图片描述
3、同一个类中的方法直接内部调用,会导致事务失效
4、@Transactional注解所在的类未被Spring管理
5、多线程调用事务会失效。spring的事务是通过数据库连接来实现的。当前线程中保存了一个map,key是数据源,value是数据库连接。在多线程中获取到的数据库连接不一样,是不同的事务。
6、数据表(MyISAM)不支持事务
7、未开启事务,springboot通过DataSourceTransactionManagerAutoConfiguration类默认开启事务

事务不回滚(5种)

1、指定的传播特性错误。目前只有这三种传播特性才会创建新事务:REQUIRED,REQUIRES_NEW,NESTED。
2、手动 try…catch 捕获了异常
3、手动抛出别的异常。
①开发者没有手动捕获异常,但是抛得异常不正确,spring 事务也不会回滚
②开发捕获异常,又手动抛出了异常:Exception,事务也不会回滚。

4、自定义了回滚异常。
如 @Transactional(rollbackFor = BusinessException.class),而BusinessException是我们自定义的异常,报错的异常不属于BusinessException,所以事务也不会回滚。
5、嵌套事务回滚多了

8、spring中的BeanFactory和FactoryBean的区别是什么?

BeanFactory

  • Spring 里面的核心功能是 IOC 容器,所谓 IOC 容器,本质上就是一个 Bean 工厂。
  • 它能够根据 xml 里面声明的 Bean 配置进行 bean 的加载和初始化,然后BeanFactory 来生产我们需要的各种各样的 Bean
  • BeanFactory它是一个工厂类(接口),用于管理 Bean 的一个工厂。在 Spring 中,BeanFactory 是 IOC 容器的核心接口,它为 Spring 的容器定义了一套规范,并提供像 getBean 这样的方法从容器中获取指定的 Bean 实例。
  • BeanFactory 在产生 Bean 的同时,还提供了解决 Bean 之间的依赖注入的能力,也就是所谓的 DI。

FactoryBean

  • FactoryBean 是一个工厂 Bean,它是一个接口,主要的功能是动态生成某一个类型的 Bean 的实例,也就是说,我们可以自定义一个 Bean 并且加载到 IOC 容器里面。
  • 它里面有一个重要的方法叫== getObject()==,这个方法里面就是用来实现动态构建Bean 的过程。
  • Spring Cloud 里 面 的 OpenFeign 组 件 , 客 户 端 的 代 理 类 , 就 是 使 用 了FactoryBean 来实现的。

9、Spring的Bean对象的定义方式有几种?

1、通过xml配置bean
2、@CompontScan+@Component
3、@Configuration+@Bean
4、@Import:导入配置类或者普通的Bean
5、使用FactoryBean工厂bean ,动态构建一个Bean 实例
6、实现ImportBeanDefinitionRegistrar接口,可以动态注入Bean实例
7、实现ImportSelector接口,动态批量注入配置类或者Bean对象

10、什么是BeanDefinition?BeanDefinition中有哪些属性?

简单说就是对Bean信息的定义。
描述一个bean的全部信息,比如他的class类型、Bean的作用域、是否懒加载…
spring中每一个被扫描到的bean都会生成一个BeanDefinition。
BeanDefinition的主要作用是为了在只解析一次类的情况下,最大程度的拿到这类的信息。防止重复解析导致效率变低。
spring采用ASM(字节码解析的工具)技术去得到BeanDefinition。
常用的属性:
beanClass:表示Bean类型,未加载类的时候存放Bean的名字,加载类后存放Bean的class信息。
scope:表示Bean的作用域,一般值为单例或者原型。
lazyInit:表示Bean是否是懒加载。
initMethodName:Bean初始化需要执行的方法。
destroyMethodName:Bean销毁时要执行的方法。
factoryBeanName:创建当前Bean的工厂。

11、@Autowired和@Resource有什么区别?

这两个注解都能完成Bean对象依赖注入
1、来源不同:@Autowired 来自 Spring 框架,而 @Resource 来自于(Java)JSR-250;
2、依赖查找的顺序不同:

  • @Autowired 先ByType,如果需要ByName,要结合@Primary 或者@Qualifier
  • @Resource 先ByName再ByType;

3、支持的参数不同:@Autowired 只支持设置 1 个参数,而 @Resource 支持设置 7 个参数;
4、依赖注入的用法支持不同:@Autowired 既支持构造方法注入,又支持属性注入和 Setter 注入,而 @Resource 只支持属性注入和 Setter 注入;
5、编译器 IDEA 的提示不同:当注入 Mapper 对象时,使用 @Autowired 注解编译器会提示错误,而使用 @Resource 注解则不会提示错误。

12、Spring中常用的设计模式?

(1)代理模式——spring 中两种代理方式,若目标对象实现了若干接口,spring 使用jdk 的java.lang.reflect.Proxy类代理。若目标兑现没有实现任何接口,spring 使用 CGLIB 库生成目标类的子类。
(2)单例模式——在 spring 的配置文件中设置 bean 默认为单例模式。
(3)模板方式模式——用来解决代码重复的问题。
比如:RestTemplate、JdbcTemplate、JpaTemplate
(2)工厂模式——在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用同一个接口来指向新创建的对象。Spring 中使用 beanFactory 来创建对象的实例。

13、Spring循环依赖

1、什么是循环依赖?

简单的来说就是A依赖B的同时,B依赖A。在创建A对象的同时需要使用的B对象,在创建B对象的同时需要使用到A对象。
有两种情况会造成循环依赖:
①属性注入=>spring通过三级缓存解决
②@DependsOn=>spring没有解决

2、三级缓存?

Spring解决循环依赖是通过三级缓存(Map)

对象名 含义
一级缓存 (singletonObjects) 单例池:通过非懒加载的且已经完成整个生命周期的bean
二级缓存(earlySingletonObjects) 存放早期暴露出来的Bean对象,Bean的生命周期未结束(属性还未填充完)
三级缓存(singletonFactories) 缓存的是对象工厂,创建Bean对象

缓存—Map
一级缓存—singletonObjects==Map<key,value>=Map<String,Object>—key:beanName value:对象----通过非懒加载的且经过完整的Bean的生命周期所形成的单例Bean对象
二级缓存—earlySingletonObjects
Map<key,value>===Map<String,Object>—key:beanName value:放一个代理对象或者原始对象

二级缓存的作用为了提高查询对象的效率(x)

二级缓存的本质作用是:专门用来存放由于出现了循环依赖所得到的没有经过完整的bean的生命周期的一个对象。

三级缓存—singletonFactories====Map<key,value>====Map<String,接口>====key:beanName value:是一个lambda表达式。

三级缓存中的作用就是为了存放进行Aop需要的原材料(目标对象),然后来打破循环。

3、为什么在java中对象中属性依赖不会出现循环依赖的现象而在spring就会出现循环依赖?

对于java中的对象来说,只需要一步简单的实例化,就会产生一个对象,而一旦这个对象被产生,就可以让别人使用。
而在spring中的bean对象来说,需要经过完整的生命周期之后(而实例化一个对象只是生命周期),才能将该bean对象放到单例池中,别人才能使用。
说白了,如果bean的生命周期能够像普通的java对象的生命周期来说,就不会出现循环依赖。

4、如何解决循环依赖的?

第一,先实例A对象,同时会创建ObjectFactory对象存入三级缓存singletonFactories

第二,A在初始化的时候需要B对象,这个走B的创建的逻辑

第三,B实例化完成,也会创建ObjectFactory对象存入三级缓存singletonFactories

第四,B需要注入A,通过三级缓存中获取ObjectFactory来生成一个A的对象同时存入二级缓存,这个是有两种情况,一个是可能是A的普通对象,另外一个是A的代理对象,都可以让ObjectFactory来生产对应的对象,这也是三级缓存的关键

第五,B通过从通过二级缓存earlySingletonObjects 获得到A的对象后可以正常注入,B创建成功,存入一级缓存singletonObjects

第六,回到A对象初始化,因为B对象已经创建完成,则可以直接注入B,A创建成功存入一次缓存singletonObjects

第七,二级缓存中的临时对象A清除

5、那如果只有一级缓存和三级缓存是否可行?

不行的,每次从三级缓存中拿到ObjectFactory对象,执行getObject()方法又会产生新的代理对象,因为A是单例的,所有这里我们要借助二级缓存来解决这个问题,将执行了objectFactory.getObject()产生的对象放到二级缓存中去,后面去二级缓存中拿,没必要再执行一遍objectFactory.getObject()方法再产生一个新的代理对象,保证始终只有一个代理对象。

所以如果没有AOP的话确实可以两级缓存就可以解决循环依赖的问题,如果加上AOP,两级缓存是无法解决的,不可能每次执行objectFactory.getObject()方法都给我产生一个新的代理对象,所以还要借助另外一个缓存来保存产生的代理对象。

SpringMvc

1、简单的谈一下SpringMVC的工作流程?

  • 用户发送请求至前端控制器DispatcherServlet
  • DispatcherServlet收到请求调用HandlerMapping处理器映射器。
  • DispatcherServlet再把请求提交到对应的Controller
  • Controller进行业务逻辑处理后返回一个ModelAndView
  • DispatcherServlet将ModelAndView传给ViewReslover视图解析器
  • ViewReslover解析后返回具体View 对象
  • DispatcherServlet根据View进行渲染视图
  • 响应用户

2、说出Spring或者SpringMVC中常用的5个注解,并解释含义

@Component 基本注解,标识一个受Spring管理的组件
@Controller 标识为一个表示层的组件
@Service 标识为一个业务层的组件
@Repository 标识为一个持久层的组件
@Autowired 自动装配
@Qualifier(“”) 具体指定要装配的组件的id值
@RequestMapping() 完成请求映射
@PathVariable 从请求路径下中获取请求参数(/user/{id}),传递给方法的形式参数
@RequestBody:注解实现接收http请求的json数据,将json转换为java对象。
@ResponseBody:注解实现将controller方法返回对象转化为json对象响应给客户端。
@RequestParam:指定请求参数的名称

3、简述SpringMVC中如何返回JSON数据

Step1:在项目中加入json转换的依赖,例如jackson,fastjson,gson等
Step2:在请求处理方法中将返回值改为具体返回的数据的类型, 例如数据的集合类List等
Step3:在请求处理方法上使用@ResponseBody注解

4、Spring MVC怎么实现统一异常处理?

开发一个全局异常处理器需要使用到两个注解:@Controlleradvice 、@ ExceptionHandler
在这里插入图片描述

Mybatis

1、MyBatis中 #{}和${}的区别是什么?

#{}是预编译处理,KaTeX parse error: Expected 'EOF', got '#' at position 21: …串替换; Mybatis在处理#̲{}时,会将sql中的#{}替…{}时,就是把${}替换成变量的值;
使用#{}可以有效的防止SQL注入,提高系统安全性。

${}使用场景:
1、根据参数获取表名
2、Group by 分组列
3、Order by 排序列和排列类型
防止sql注入?
1、检查变量数据类型和格式
2、过滤特殊符号
3、绑定变量,使用预编译语句

2、Mybatis 中一级缓存与二级缓存?

(1)MyBatis的缓存分为一级缓存和 二级缓存。
一级缓存是基于 PerpetualCache 的 HashMap 本地缓存,默认开启。作用域是sqlsession级别的,同一个sqlsession中执行相同的sql查询(相同的sql和参数),第一次会去查询数据库并写到缓存中,第二次从一级缓存中取。
二级缓存是基于NameSpace和Mapper级别的缓存,多个 SqlSession 去操作同一个 Mapper 映射的 sql 语句,手动开启。针对同一个sqlSessionFactory。
多个sqlsession执行sql
(2)缓存的查找顺序:二级缓存 => 一级缓存 => 数据库

3、MyBatis如何获取自动生成的(主)键值?

在mapper的标签中使用 useGeneratedKeys 和 keyProperty 两个属性来获取自动生成的主键值。
示例:
1.
2. insert into names (name) values (#{name})
3.
MyBatis框架提供了insert标签的属性:
useGeneratedKeys:是否使用自动增长主键
keyProperty:获取自动增长的主键值,储存在Employee对象的哪个字段中

4、简述Mybatis的动态SQL,列出常用的6个标签及作用

动态SQL是MyBatis的强大特性之一 基于功能强大的OGNL表达式。
动态SQL主要是来解决查询条件不确定的情况,在程序运行期间,根据提交的条件动态的完成查询
常用的标签:
: 进行条件的判断
:在判断后的SQL语句前面添加WHERE关键字,并处理SQL语句开始位置的AND 或者OR的问题
:可以在SQL语句前后进行添加指定字符 或者去掉指定字符.
: 主要用于修改操作时出现的逗号问题
:类似于java中的switch语句.在所有的条件中选择其一
:迭代操作
:sql片段,可以把条件和查询结果的字段抽取出来

5、Mybatis 如何完成MySQL的批量操作,举例说明

MyBatis完成MySQL的批量操作主要是通过标签来拼装相应的SQL语句.
例如:
1.
2. insert into tbl_employee(last_name,email,gender,d_id) values
3.
4. (#{curr_emp.lastName},#{curr_emp.email},#{curr_emp.gender},#{curr_emp.dept.id})
5.
6.
这个在项目中也用过,为了提升性能,可以采用在映射文件中通过forEach标签遍历集合,获取每一个元素作为insert语句的参数值

6、mybatis的分页

逻辑分页:使用 MyBatis 自带的 RowBounds 进行分页,它是一次性查询很多数据,然后在数据中再进行检索。
消耗大量内存,对数据库压力较大
物理分页:自己手写 SQL 分页或使用分页插件 PageHelper,去数据库查询指定条数的分页数据的形式

7、Mybatis的工作流程

1、加载配置文件mybatis-config.xml
2、创建会话工厂。MyBatis通过读取配置文件的信息来构造出会话工厂得到SqlSessionFactory对象。
3、创建会话对象。sqlSessionFactory.openSession(),得到sqlSession对象
4、sqlsession对象.getMapper(XXX接口),得到这个接口的代理对象。
5、调用代理对象去执行代理逻辑(执行sql),返回查询结果集

8、Mybatis是否支持延迟加载?延迟加载的原理是什么?

1、mybatis 是否支持延迟加载?
延迟加载就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据
两次查询,第一次查主表,第二次查关联表,第二次查询条件是第一次查询结果的列值
是支持的,不过默认是关闭的,如果想要使用,需要添加配置开启。延迟加载可以实现先查询主表,按需实时做关联查询,返回关联表结果集,一定程度上提高了效率。
mybatis仅支持关联对象association和关联集合对象collection的延迟加载,association是一对一,collection是一对多查询,在mybatis配置文件中可以配置lazyloadingEnable=true/false。
2、延迟加载的原理是什么?
使用CGLIB为目标对象建立代理对象,当调用目标对象的方法时进入拦截器方法。
比如调用a.getB().getName(),拦截器方法invoke()发现a.getB()为null,会单独发送事先准备好的查询关联B对象的sql语句,把B查询出来然后调用a.setB(b),也是a的对象的属性b就有值了,然后调用getName(),这就是延迟加载的原理。

9、Mybatis有哪些执行器?

(1)SimpleExecutor 简单执行器
会进行两次预编译,每次都会构造一个PreparmentStatement对象------效率较低
(2)ReuseExecutor 可重用执行器
执行相同的sql语句只会进行一次预编译,效率更高
(3)BatchExecutor 批量执行器
查询时与SimpleExecutor相同会进行多次编译,更新或删除时,会批量进行,需要手动提交
原理:初始化sqlSession会读取配置文件,若配置了执行器则使用对应的执行器,未配置则使用SimpleExecutor
配置:在Mybatis配置文件中配置执行器

SpringBoot/SpringCloud

1、谈谈怎么理解SpringBoot框架?

Spring Boot 是 Spring 开源组织下的子项目,是 Spring 组件一站式解决方案,主要是简化了使用 Spring 的难度,简省了繁重的配置,提供了各种启动器,开发者能快速上手。
Spring Boot的优点
1、快速构建项目,可以选一些必要的组件;
2、对主流框架的无配置集成;
3、内嵌Tomcat容器,项目可独立运行;
4、删除了繁琐的xml配置文件
5、极大地提高了开发和部署效率;
6、提供starter,简化maven配置
7、约定大于配置
Spring Boot缺点:
1.版本迭代速度快,一些模块改动很大;
2.由于无须配置,报错时很难定位;
约定大约配置的体现:
1、starter启动器,管理jar包,简化maven配置
2、默认加载application.yml配置文件
3、通过扫描约定路径下的 spring.factories文件来识别配置类,实现 Bean 的自动装配。

2、Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的?

启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解:
@SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。
@EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,
如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。
@ComponentScan:Spring组件扫描。

3、Spring Boot 自动配置原理是什么?

自动配置:扫描启动类所在的包以及子包所有Bean组件并注册到IOC容器中
1、大致流程通过@SpringBootApplication进行实现,这个注解在启动类上
2、@SpringBootApplication注解由三个注解共同完成自动装配,各个注解作用如下
@SpringBootConfiguration: 标记启动类为一个spring配置类
@EnableAutoConfiguration: 实现自动装配的核心注解
@ComponentScan: spring组件扫描
3、@EnableAutoConfiguration主要是通过@Import注解导入AutoConfigurationImportSelector类自动配置选择器开启自动装配。
4、AutoConfigurationImportSelector类实现了ImportSelector 接口,重写了selectImports方法,我们就可以得到需要自动配置的类的全限定类名数组
5、通过Spring提供的SpringFactoriesLoader机制,扫描classpath下的META-INF/spring.factories文件,读取需要自动装配的配置类
6、依据@Conditional条件筛选的方式按需加载配置类,最终完成自动装配

4、SpringBoot配置文件有哪些?加载顺序?怎么实现多环境配置?

  • 两种前缀开头

bootstarp>application[加载顺序]

  • 3种后缀结尾

.yml>.yaml>.properties

  • 组合起来其实有6种可能

5、SpringBoot和SpringCloud是什么关系

Spring Boot 是 Spring 的一套快速配置脚手架,可以基于Spring Boot 快速开发单个微服务,简化了spring开发。约定大于配置。
Spring Cloud是一个基于Spring Boot的微服务全局治理框架,提供了快速构建分布式的常用组件,如服务的配置管理、服务注册与发现,服务调用与熔断降级等。
而真正的实现目前有两套体系用的比较多
一个是 Spring Cloud Netflix,其中常用组件有

  • Ribbon——负载均衡
  • Hystrix——服务熔断
  • Zuul——网关
  • Eureka——服务注册与发现
  • Feign——服务调用

一个是 Spring Cloud Alibaba

  • Dubbo——消息通讯
  • Nacos——注册中心/配置中心
  • Seata——分布式事务
  • Sentinel——熔断降级
  • XXL-job:分布式任务调度

6、SpringCloud都用过哪些组件?介绍一下作用

早期我们一般认为的Spring Cloud五大组件是

  • Eureka : 注册中心
  • Ribbon : 负载均衡
  • Feign : 远程调用
  • Hystrix : 服务熔断
  • Zuul/Gateway : 网关
    随着SpringCloudAlibba在国内兴起 , 我们项目中使用了一些阿里巴巴的组件
  • 注册中心/配置中心 Nacos
  • 负载均衡 Ribbon
  • 服务调用 openFeign
  • 服务保护 sentinel
  • 服务网关 Gateway

7、nacos、eureka的区别?

① Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式
② 临时实例心跳不正常会被剔除,非临时实例则不会被剔除
③ Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
④ Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;

  • Eureka采用AP方式
  • naocs默认是AP模式,可以采用CP模式

8、Nacos作用以及注册中心的原理

Nacos英文全称Dynamic Naming and Configuration Service,Na为naming/nameServer即注册中心,co为configuration即注册中心,service是指该注册/配置中心都是以服务为核心。
Nacos注册中心分为server与client,server采用Java编写,为client提供注册发现服务与配置服务。而client可以用多语言实现,client与微服务嵌套在一起,nacos提供sdk和openApi,如果没有sdk也可以根据openApi手动写服务注册与发现和配置拉取的逻辑。

服务注册原理
服务注册方法:以Java nacos client v1.0.1 为例子,服务注册的策略的是每5秒向nacos server发送一次心跳,心跳带上了服务名,服务ip,服务端口等信息。同时 nacos server也会向client 主动发起健康检查,支持tcp/http检查。如果15秒内无心跳且健康检查失败则认为实例不健康,如果30秒内健康检查失败则剔除实例。

9、服务注册和发现是什么意思?Spring Cloud 如何实现服务注册发现?

我理解的是主要三块大功能,分别是服务注册 、服务发现、服务状态监控

  1. 服务注册 : 服务启动的时候会将服务的信息注册到注册中心, 比如: 服务名称 , 服务的IP , 端口号等
  2. 服务发现 : 服务调用方调用服务的时候, 根据服务名称从注册中心拉取服务列表 , 然后根据负载均衡策略 , 选择一个服务, 获取服务的IP和端口号, 发起远程调用
  3. 服务状态监控 : 服务提供者会定时向注册中心发送心跳 , 注册中心也会主动向服务提供者发送心跳探测, 如果长时间没有接收到心跳, 就将服务实例从注册中心下线或者移除

使用的话, 首先需要部署注册中心服务 , 然后在我们自己的微服务中引入注册中心依赖, 然后再配置文件中配置注册中心地址 就可以了

10、Feign工作原理

Feign底层依赖于Java的动态代理机制,对原生Java Socket或者Apache HttpClient进行封装,实现了基于Http协议的远程过程调用。当然,Feign还在此基础上实现了负载均衡、熔断等机制。
主程序入口添加了@EnableFeignClients注解开启对FeignClient扫描加载处理。根据Feign Client的开发规范,定义接口并加@FeignClient注解。当程序启动时,会进行包扫描,扫描所有@FeignClient的注解的类,并且讲这些信息注入Spring IOC容器中,当定义的的Feign接口中的方法被调用时,通过JDK的代理方式,来生成具体的RequestTemplate。当生成代理时,Feign会为每个接口方法创建一个RequestTemplate对象,该对象封装HTTP请求需要的全部信息,如请求参数名,请求方法等信息都是在这个过程中确定的。然后RequestTemplate生成Request,然后把Request交给Client去处理,这里指的时Client可以是JDK原生的HTTPURLConnection,Apache的HttpClient,或OKhttp,最后Client被封装到LoadBalanceClient类,这个类结合Ribbon负载均衡发器服务之间的调用。

11、服务熔断和服务降级的区别

  • 服务降级:指的是业务代码【下单】出现问题(异常 出错了 响应很慢),接下开通过fallback去执行兜底方法,【返回服务页面 给一个默认
    查询缓存】 将核心业务降级到非核心业务
  • 服务熔断:当触发了服务的熔断阈值之后,也会调用fallback去执行兜底方法。但是服务熔断来说只会去调用兜底方法,不会在去执行业务方法。【半开转态】
    我们项目中涉及到服务调用得地方都会定义降级, 一般降级逻辑就是返回默认值 , 降级的实现也非常简单 , 就是创建一个类实现FallbackFactory接口 , 然后再对应的Feign客户端接口上面 , 通过@FeignClient指定降级类

12、你们项目中微服务之间是如何通讯的?

1.同步通信:通过openFeign发送http请求调用
2.异步:消息队列,如RabbitMq、KafKa等

13、你们项目的配置文件是怎么管理的 ?

大部分的固定的配置文件都放在服务本地 , 一些根据环境不同可能会变化的部分, 放到Nacos中
Naocs中主要存放的是各个微服务共享的配置,需要随着需求动态变更的配置。

14、你们项目中有没有做过限流 ? 怎么做的 ?

常见的限流算法:漏桶算法令牌桶算法

漏桶算法:漏桶算法其实很简单,可以粗略的认为就是注水漏水过程,往桶中以一定速率流出水,以任意速率流入水,当水超过桶流量则丢弃,因为桶容量是不变的,保证了整体的速率。

令牌桶算法:令牌桶是一个存放固定容量令牌的桶,按照固定速率r往桶里添加令牌;桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃;当一个请求达到时,会尝试从桶中获取令牌;如果有,则继续处理请求;如果没有则排队等待或者直接丢弃;可以发现,漏桶算法的流出速率恒定,而令牌桶算法的流出速率却有可能大于r;

分布式事务

1、什么是分布式事务?

事务就是为了保证一组数据库操作要么全部成功,要么全部失败,从而保证数据的原子性、一致性、隔离性、持久性。在以前单体项目中,整合了spring的话,我们直接用@Transactional解决。
在分布式系统中,一个业务因为跨越不同数据库或者跨越不同微服务而包含多个子事务,要求所有子事务同时成功或失败,这就是分布式事务。即在跨数据源的多个服务调用下产生的。
一组sql语句操作不同微服务上的不同数据库。
场景1:跨库事务

跨库事务指的是,一个应用某个功能需要操作多个库,不同的库中存储不同的业务数据。如下所示:
在这里插入图片描述
场景二:分库分表

通常一个库数据量比较大或者预期未来的数据量比较大,都会进行水平拆分,也就是分库分表。如下图,将数据库B拆分成了2个库:
在这里插入图片描述
场景三:跨服务事务

跨服务事务指的是,一个应用某个功能需要调用多个微服务进行实现,不同的微服务操作的是不同的数据库。如下所示:
在这里插入图片描述

2、什么是CAP理论(分布式事务特点)?

1、一致性(Consistency) : 更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致(强一致性),不能存在中间状态。

2、可用性(Availability) : 系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。

3、分区容错性(Partition tolerance) : 分布式系统在遇到任何网络分区故障时,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障。

3、什么是BASE?

BASE:BASE模型是传统ACID模型的反面,不同于ACID,BASE强调牺牲高一致性,从而获得可用性,数据允许在一段时间内的不一致,只要保证最终一致就可以了

4、怎么解决分布式事务?

我先说一下我的理解吧,说的不一定对,我再说说一下在实际工作中是怎么解决的?
一般来说呢,解决分布式事务归根结底就是协调事务(强一致性)和最终一致性方案。
强一致性:通过事务协调器来协调多个节点的事务性。保证每一个节点的事务同时成功或同时失败如:2PC、3PC、XA
最终一致性:多个网络节点数据允许出现不一致,但最终在某个时间点会达成数据一致性。如:本地消息表、TCC、Saga、分布式消息队列

5、什么是seata?

AT 模式,是一种基于本地事务+二阶段协议来实现的最终数据一致性方案,也是Seata 默认的解决方案
TCC 模式,TCC 事务是 Try、Confirm、Cancel 三个词语的缩写,简单理解就是把一个完整的业务逻辑拆分成三个阶段,然后通过事务管理器在业务逻辑层面根据每个分支事务的执行情况分别调用该业务的 Confirm 或者 Cacel 方法。
Saga 模式,Saga 模式是 SEATA 提供的长事务解决方案,在 Saga 模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者。
XA 模式,XA 可以认为是一种强一致性的事务解决方法,它利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种事务模式。
Seata事务管理中有三个重要的角色:
1、TC (Transaction Coordinator) -事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚。
2、TM (Transaction Manager) -事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务。
3、RM (Resource Manager) -资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

6、分布式全局唯一id

1、UUID
比较长、占用空间大;无序且不利于索引,在实际项目中不建议使用
2、雪花算法
分布式id生成算法,结果时是一个 64 位长度的 long类型数据。
其中这 64 位的数据,由 4 个部分组成

  • 第一个 bit 位是符号位,因为 id 不会是负数,所以它一般是 0
  • 接着用 41 个 bit 位来表示毫秒单位的时间戳
  • 再用 10 个 bit 位来表示工作机器 id
  • 最后 12 个 bit 位表示递增的序列号

2、redis的increby

Mysql

1、事务

1、SQL 的select 语句完整的执行顺序

(1)from 表名;
(2)join on 表名 ;
(3)where 条件 ;
(4)group by 分组字段;
(5)使用聚集函数进行计算;
(6)having 条件;
(7)select 字段;
(8)DISTINCT 去重
(9)order by 排序
(10)limit 分页

2、说一下MySQL的事务和事务特性?

事务:同一组的SQL语句(多条)执行,多条SQL语句要么都执行成功,要么都执行失败,不能出现部分成功,部分失败

事务的基本要素(ACID):

  • 原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。
  • 一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能A扣了钱,B却没收到。
  • 隔离性(Isolation):多个用户并发访问数据库时,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。
  • 持久性(Durability):是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的

3、说一下事务的并发问题?

事务的并发问题:如果不考虑隔离性,事务存在3种并发访问问题。

  • 脏读:一个事务读到了另一个事务未提交的数据.
  • 不可重复读:一个事务A读到了另一个事务B已经提交(update)的数据。导致事务A多次查询同一数据时结果不一致。
  • 幻读:一个事务A读到了另一个事务B已经提交(insert/delete)的数据。导致事务A多次查询同一数据时的结果不一致。

小结:

不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表
在 InnoDB 中设计了一种间隙锁,它的主要功能是锁定一段范围内的索引记录,解决幻读。
当对查询范围 id>4 and id<7 加锁的时候,会针对 B+树中(4,7)这个开区间范围的索引加间隙锁。
意味着在这种情况下,其他事务对这个区间的数据进行插入、更新、删除都会被锁住。

4、说一下 MySQL事务隔离级别?

  • 读未提交(read-uncommitted):一个事务读到另一个事务没有提交的数据。
  • 读已提交(read-committed):一个事务读到另一个事务已经提交的数据。解决脏读
  • 可重复读(repeatable-read):在一个事务中多次读取同一数据始终保持一致,无论另一个事务是否提交。解决了脏读和不可重复读
  • 串行化(serializable):同时只能执行一个事务,相当于事务中的单线程。解决了脏读、不可重复读和幻读。通过加悲观锁实现。
    安全性(数据一致性):serializable > repeatable read > read committed > read uncommitted
    性能(并发性) : `serializable < repeatable read < read committed < read uncommitted

常见数据库的默认隔离级别:

MySql:repeatable read
Oracle:read committed

1、查询数据库的隔离级别
show variables like '%isolation%';或select @@tx_isolation;
2、设置数据库的隔离级别
set session transaction isolation level` 级别字符串

2、索引

1、谈谈你对索引的理解?

索引是一种数据结构,能够提高查询效率。开源理解为一个目录。对谁建立索引就是对谁排序

按数据结构分类:B+tree索引、Hash索引、Full-text索引。
按物理存储分类:聚集索引、非聚集索引(也叫二级索引、辅助索引)。
按字段特性分类:主键索引、唯一索引、普通索引、全文索引。
按字段个数分类:单列索引(单值索引)、联合索引(也叫复合索引、组合索引)

2、索引的底层实现是什么?什么情况下会索引失效?

InnoDB 存储引擎是用 B+Tree 实现其索引结构

失效条件:

1.如果or前的条件中的列有索引,而后面的列中没有索引,那么涉及的索引都不会被用到。
2.范围查询右边的列,不能使用索引 。
3.like查询以%开头(覆盖索引可以解决)
4.如果列类型是字符串,字符串不加单引号,造成索引失效。
5.在索引列上进行运算操作, 索引将失效
6. 组合索引要遵循 最左匹配原则
7. 如果MySQL评估使用索引比全表更慢,则不使用索引。
8. in 走索引, not in 索引失效
9. 尽量使用覆盖索引,尽量避免select *

3 、索引创建原则有哪些?

哪些情况要建立索引

  1. 针对于常作为查询条件(where)、排序(order by)、分组(group by)操作的字段建立索引。
  2. 针对于数据量较大,且查询比较频繁的表建立索引。
  3. 尽量选择区分度高的列作为索引,尽量建立唯一索引,区分度越高,使用索引的效率越高。
  4. 尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间,避免回表,提高查询效率。
  5. 要控制索引的数量,索引并不是多多益善,索引越多,维护索引结构的代价也就越大,会影响增删改的效率。

4、聚簇索引和非聚簇索引的区别?

如果没有定义主键,innodb会选择非空的唯一索引代替。如果没有这样的索引,innodb会隐式的定义一个主键来作为聚簇索引。

聚簇索引

  • 聚集索引就是基于主键创建的索引,一个表中只能有一个
  • 叶子节点存储整张表的行记录数据(数据页)
  • 数据和索引放在一起,可以直接获取数据,效率高
  • 排序查询和范围查询效率高,因为其数据是按照大小排列的

非聚簇索引(二级索引、辅助索引)

  • 聚簇索引是除了主键索引以外的其他索引,一个表有多个
  • 叶子节点不存储数据、存储的是数据行地址(主键值)
  • 非聚簇索引获取数据需要第二次查询(最终还是通过主键索引来检索)

5、了解回表查询吗?

这种先到二级索引中查找数据,找到主键值,然后再到聚集索引中根据主键值,获取数据的方式,就称之为回表查询。

6、什么是左前缀原则嘛?

最左前缀法则指的是查询从索引的最左列开始,并且不跳过索引中的列。如果跳跃某一列,索引将会部分失效(后面的字段索引失效)。
比如有一个user表,给里面的字段创建了一个复合索引,顺序是name,age,email
当查询索引不包含name的时候会失效的,当然查询name和age则不会失效,查询name和email跳过了age,则只有name会命中索引

7、什么是覆盖索引?

覆盖索引是指 查询使用了索引,并且需要返回的列,在该索引中已经全部能够找到 。

比如有一个user表,给里面的字段创建了一个复合索引,顺序是name,age,email

当查询索引只select后面只包含name,age,email的时候就算是覆盖索引了

8、简述MyISAM和InnoDB的区别?

InnoDB存储引擎(MySQL5.5后默认版本)
主要面向OLTP(Online Transaction Processing,在线事务处理)

MyISAM存储引擎
主要面向OLAP(Online Analytical Processing,在线分析处理)方面的应用。

  1. 外键支持
    MyISAM不支持,InnoDB支持
  2. 事务支持
    MyISAM不支持,InnoDB支持
  3. 行表锁
    MyISAM支持表锁,InnoDB支持表锁、行锁(默认)
  4. 文件类型
    MyISAM:.frm、.myd、.myi InnoDB:.frm、.ibd
  5. 索引
    MyISAM:非聚簇索引InnoDB:聚簇索引
  6. 性能
    MyISAM:性能:节省资源、消耗少、简单业务InnoDB:事务:并发写、事务、更大资源
  7. 删除表
    innodb是逐行删除,而myisam是重新创建一张表

9、B+tree 与 B-tree区别

B+Tree是基于B-Tree的,大部分数据结构相同,但也有一些区别:
1、B+Tree非叶节点不保存数据信息,只保存关键字和节点的引用;
2、B 树的数据存储在每个节点上,而 B+树中的数据是存储在叶子节点,并且叶子节形成一个单向链表
3、B+Tree叶子节点是顺序排列的,并且相邻节点具有顺序引用关系;
4、因为数据的不同,导致查询过程也不同,B树的查找只需找到匹配元素即可,最好情况下查找到根节点,最坏情况下查找到叶子结点,所以性能很不稳定,而B+树每次必须查找到叶子结点,性能稳定;
5、B+Tree通常有两个指针,一个指向根结点,另一个指向关键字最小的叶子结点,因此可以对B+Tree进行两种查找运算:一种是对于关键字的范围查找和分页查找,另一种是从根节点开始,进行随机查找;

3、调优

1、MySQL 如何定位慢查询?

开启慢查询日志,查看慢查询的 SQL。
慢查询日志记录了所有执行时间超过指定参数(long_query_time,单位:秒,默认10秒)的所有SQL语句的日志。

如果要开启慢查询日志,需要在MySQL的配置文件(/etc/my.cnf)中配置如下信息:

# 开启MySQL慢日志查询开关
slow_query_log=1
# 设置慢日志的时间为2秒,SQL语句执行时间超过2秒,就会视为慢查询,记录慢查询日志
long_query_time=2

配置完毕之后,通过以下指令重新启动MySQL服务器进行测试,查看慢日志文件中记录的信息 /var/lib/mysql/localhost-slow.log。

如果这个时候有一条sql执行的时间超过2秒,则会记录到慢日志文件中

2、如何处理慢查询?

使用 explain 命令查询 SQL 语句执行计划。
在这里插入图片描述

主要可以根据几个字段,判断sql是否需要优化,特别是是否能命中索引或命中索引的情况

  • type 通过sql的连接的类型进行优化
  • possible_key 通过它查看是否可能会命中索引
  • key 当前sql实际命中的索引
  • key_len 索引占用的大小
  • Extra 额外的优化建议

3、数据库分表操作

可以说使用Mycat或者ShardingSphere等中间件来做,具体怎么做就要结合具体的场景进行分析了。
可以参考:https://database.51cto.com/art/201809/583857.htm

4、Sql优化经验

①选择表合适存储引擎

②数据库表的设计

  • 选择合适的字段类型
  • 优先考虑逻辑删除
  • 添加通用字段(createTime、updateTime、version)
  • 尽量使用not null定义字段
  • 不用严格遵守三范式,字段合理冗余
  • 合理添加索引

③索引优化

  • 表的主键、外键必须有索引;
  • 数据量大的表应该有索引;
  • 经常与其他表进行连接的表,在连接字段上应该建立索引;
  • 经常出现在Where子句中的字段,特别是大表的字段,应该建立索引;
  • 索引应该建在选择性高的字段上; (sex 性别这种就不适合)
  • 索引应该建在小字段上,对于大的文本字段甚至超长字段,不要建索引;
  • 频繁进行数据操作的表,不要建立太多的索引;
  • 删除无用的索引,避免对执行计划造成负面影响;

④sql语句优化

  • 避免直接使用select *
  • SQL语句要避免造成索引失效的写法
  • SQL语句中IN包含的值不应过多
  • 当只需要一条数据的时候,使用limit 1
  • 如果排序字段没有用到索引,就尽量少排序
  • 如果限制条件中其他字段没有索引,尽量少用or
  • 尽量用union all代替union
  • 避免在where子句中对字段进行null值判断
  • 不建议使用%前缀模糊查询
  • 避免在where子句中对字段进行表达式操作
  • Join优化 能用innerjoin 就不用left join right join,如必须使用 一定要已小表为驱动

⑤主从复制、读写分离

如果数据库的使用场景读的操作比较的时候,为了避免写的操作所造成的性能影响 可以采用读写分离的架构,读写分离,解决的是,数据库的写入,影响了查询的效率。读写分离的基本原理是让主数据库处理事务性增、改、删操作(INSERT、UPDATE、DELETE),而从数据库处理SELECT查询操作。 数据库复制被用来把事务性操作导致的变更同步到集群中的从数据库。

⑥mysql的分库分表

当单表的数据量太大或单库的数据量太大的话,性能会降的比较厉害,这个时候就要考虑选择合适的拆分策略,比如垂直分库和水平分库

4、锁

1、锁的分类?

MySQL中按照锁的粒度分有三类:

  • 全局锁:锁定数据库中的所有表。
  • 表级锁:每次操作锁住整张表。
  • 行级锁:每次操作锁住对应的行数据。

2、悲观锁和乐观锁的怎么实现?

  • 悲观锁:每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻止,直到这个锁被释放。
    select…for update是MySQL提供的实现悲观锁的方式。 例如:select price from item
    where id=100 for update select…for
    update语句执行中所有扫描过的行都会被锁上,因此在MySQL中用悲观锁务必须确定走了索引,而 不是全表扫描,否则将会将整个数据表锁住。
  • 乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据。
    数据库的乐观锁可以用版本号机制和CAS算法实现。在表里面添加一个 version 字段,每次修改成功值加1,这样每次修改的时候先对比一下,自己拥有的 version 和数据库现在的 version是否一致,如果不一致就不修改,这样就实现了乐观锁。
//1: 查询出商品信息
select (quantity,version) from items where id=100;
//2: 根据商品信息生成订单
insert into orders(id,item_id) values(null,100);
//3: 修改商品的库存
update items set quantity=quantity-1,version=version+1 where id=100 and version=#{
    
    version};

3、说一下 MySQL 的行锁和表锁?

MyISAM 只支持表锁,InnoDB 支持表锁和行锁,默认为行锁。

表锁

  • 定义: 即使操作一条记录也会锁住整个表,不适合高并发的操作
  • 好处:开销小,加锁快,不会出现死锁。锁定粒度大,发生锁冲突的概率最高,并发量最低。

行锁

  • 定义: 操作时只锁某一行,不对其它行有影响,适合高并发的操作
  • 好处:开销大,加锁慢,会出现死锁。锁力度小,发生锁冲突的概率小,并发度最高。

4、解释一下MVCC?

MVCC:多版本并发控制,用于在多个并发事务同时读写数据库时保持数据的一致性和隔离性。它是通过创建数据的多个版本和使用快照读取来实现并发控制。当一个事务要对数据库中的数据进行修改时,MVCC 会为该事务创建一个数据快照,而不是直接修改实际的数据行。不加锁,从而提升系统性能。
MVCC 通过创建数据的多个版本和使用快照读取来实现并发控制。读操作使用旧版本数据的快照,写操作创建新版本,并确保原始版本仍然可用。这样,不同的事务可以在一定程度上并发执行,而不会相互干扰,从而提高了数据库的并发性能和数据一致性。

MVCC的实现依赖于:(版本链)三个隐式字段、undo log日志、readView。在内部实现中,InnoDB 通过数据行的 DB_TRX_ID 和 Read View 来判断数据的可见性,如不可见,则通过数据行的 DB_ROLL_PTR 找到 undo log 中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建 Read View 之前已经提交的修改和该事务本身做的修改

版本链

一个事务修改了某条数据,不管有没有提交,那么这个事务的本次修改的记录都会被记录在版本链中,但是读事务,那么这个事务就不会被记录在版本链中。

我们在查看表结构的时候,就显式的看到id、name、age等字段。 实际上除了这三个字段以外,InnoDB还会自动给每个数据行添加三个隐藏字段:
在这里插入图片描述

undolog(回滚日志)

  • 是相同事务或不同事务对同一条记录进行修改,这个记录的undo log日志会生成一条记录版本链。方便回滚

ReadView(读视图)

在一个事务开启之后,当我在执行select查询的时候,我会生成一个ReadView对象,且该对象中维护了一个属性叫做mids,记录并维护系统当前活跃的事务(未提交的)id。
ReadView中包含了四个核心字段:
在这里插入图片描述
而在readview中就规定了版本链数据的访问规则:

trx_id 代表当前undolog版本链对应事务ID。在这里插入图片描述

不同的隔离级别,生成ReadView的时机不同:

  • READ COMMITTED :在事务中开始后每次select前都会生成一个ReadView。
  • REPEATABLE READ:仅在事务开始后第一次select数据时生成一个ReadView,后续复用该ReadView。

5、间隙锁

解决了幻读

5、你们公司有哪些数据库设计规范

1、命名规范(表名、字段名、索引名 要具有规范性、易读性)

2、选择合适的字段类型(占用尽量少的空间,字段长度一般设置为 2^n )

  • 整数(tinyint、smallint、int、bigint)
  • 小数(尽量使用 decimal,少用 float 或 double)
  • 时间(datetime)
  • 字符串(char、varchar、text、longtext)

3、优先考虑逻辑删除,而不是物理删除
4、添加通用字段

  • id:主键(尽量与业务逻辑无关)
  • gmt_create:创建时间(必须)
  • gmt_modified:修改时间(必须)
  • version:版本号,用于乐观锁(非必须)
  • remark:数据记录备注(非必须)

5、单表字段数量不宜过多(一般不超过 20 个)
6、尽量使用 not null 定义字段

7、合理添加索引(单表索引数量一般不超过 5 个)

8、不需要严格遵守 3NF(数据库三范式),字段合理冗余

  • 第一范式:对属性的原子性,要求属性具有原子性,不可再分解;
  • 第二范式:对记录的唯一性,要求记录有唯一标识,即实体的唯一性,不存在部分依赖;
  • 第三范式:对字段的冗余性,要求任何字段不能由其它字段派生出来,即字段无冗余,不存在传递依赖。

9、避免使用 MySQL 保留字(select、insert…)

10、尽量不使用外键关联(一般通过业务逻辑保证关联)

11、选择合适的字符集(utf8、utf8mb4、GBK、latin1)

12、字段尽量添加注释

Redis

数据类型

1、介绍下Redis?Redis有哪些数据类型?

Redis全称(Remote Dictionary Server)本质上是一个基于内存的Key-Value类型的非关系型数据库数据库,支持多种数据结构,单线程,性能高。

数据类型 数据结构
string:字符串最大存储容量为512M 简单动态字符串(SDS)
list :有序可重复集合 LinkedList(双向链表)
set:无序不可重复集合 Dict(哈希表/字典)、Intset(整数集合)
hash:类似于Map<String,Map<String,String>> Dict、ZipList(叶索列表)
zset(sorted set):有序不可重复集合 ZipList、SkipList(跳跃表)
GEO:推算地理位置 Zset
HyperLogLog:基数统计
bitmap:位图 二进制数组

持久化

1、Redis提供了哪几种持久化方式?

RDB:每隔一段时间,将redis存储的数据生成内存快照并存储到磁盘
优点:数据文件大小对于AOF较小,数据恢复快
缺点:容易丢数据
AOF:记录redis执行过得所有写操作,当redis重启的时候会重新执行这些命令来恢复数据,
优点:安全,不容易丢失数据
缺点:相对于RDB,更占空间,恢复数据慢
aof重写:AOF是通过记录所有写命令来持久化的,会造成文件过大,带来IO性能问题。所以当aof文件大小到达一定程度的时候,后台会自动的去执行aof重写,此过程不会影响主进程,重写完成后,新的写入将会写到新的aof中,旧的就会被删除掉。

其实RDB和AOF两种方式也可以同时使用,在这种情况下,如果redis重启的话,则会优先采用AOF方式,因为AOF方式的数据恢复完整度更高。
如果你没有数据持久化的需求,也完全可以关闭RDB和AOF方式,这样的话,redis将变成一个纯内存数据库,就像memcache一样。

Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。

官方推荐:
如果对数据不敏感,可以选单独用RDB;不建议单独用AOF,因为可能出现Bug;如果只是做纯内存缓存,可以都不用

分布式锁

1、什么是分布式锁?

锁:控制多个线程对同一个资源共享问题
本地锁 :控制一个 JVM 进程内的多个线程对本地共享资源的访问。
分布式锁:控制多个JVM进程共享同一份资源的访问。
分布式系统下,不同的服务/客户端通常运行在独立的 JVM 进程上。如果多个 JVM 进程共享同一份资源的话,使用本地锁就没办法实现资源的互斥访问了。于是,分布式锁 就诞生了。

2、分布式锁应该满足哪些条件?

在这里插入图片描述

3、分布式锁的实现方案?

在这里插入图片描述

4、setnx的缺点

1、如果用户在请求时一个服务器宕机了,其他服务器去访问一直拿不到锁,就会产生死锁现象。 (给锁加一个过期时间)
2、为了避免业务时长超过锁的过期时间 ,这样就会释放其他线程的锁。
加长锁的过期时间, 如果加长的时间还不够,增加一个兜底方案,在业务代码中添加一个子线程每10s去确认主线程是否持有锁,如果在线将锁的时间续期。 给锁增加一个唯一id,并且要利用lua脚本保证删除锁是原子操作,这样就能保证释放的是当前线程的锁。
保证加锁【站位+过期时间】、删除锁【判断+shanchu 锁】的原子性
由此看出来实现起来比较麻烦,还要保证代码的健壮性,否则某一个细节不注意就会出现bug,刚好redis提供了一个redisson组件。

主从和集群

使用问题

1、使用Redis作为缓存,Redis数据和MySQL数据库的一致性如何实现?

1、双写模式

写入数据库之后 同时将数据写入到redis缓存,单线程下重试写缓存即可
在多线程的情况下,无论先写数据库还是先写缓存 都有可能失败,导致数据不一致

2、失效模式

先写数据库再删除缓存 单线程下重试删除缓存即可
在多线程下,无论先删缓存还是先写数据库都都会导致数据不一致

3、延时双删模式

在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。具体步骤是:
1)先删除缓存
2)再写数据库
3)休眠500毫秒(根据具体的业务时间来定)
4)再次删除缓存。
结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时。

4、MQ消息队列

    (1)更新数据库数据; 
    (2)缓存因为种种问题删除失败;
    (3)将需要删除的key发送至消息队列;
    (4)redis监听消息队列并消费消息,获得需要删除的key;
    (5)继续重试删除操作,直到成功。
    然而,该方案有一个缺点,对业务线代码造成大量的侵入。

5、Cancel

接通过 Canal 组件,监控 Mysql 中 binlog 的日志,把更新后的数据同步到 Redis 里面。

2、缓存穿透,缓存击穿,缓存雪崩的原因和解决方案?

  1. 缓存穿透:

是指查询一个不存在的数据,由于缓存无法命中,将去查询数据库,但是数据库也无此记录,并且出于容错考虑,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
解决方案:
①空结果也进行缓存,可以设置一个空对象,但它的过期时间会很短,最长不超过五分钟。
②布隆过滤器:将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对DB的查询

  1. 缓存击穿

是指对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:如果这个key在大量请求同时进来之前正好失效,那么所有对这个key的数据查询都落到DB,我们称为缓存击穿。
解决方案:
①互斥机制(加锁)
本地锁:synchronized、lock
分布式锁:redis的setnx、redisson、zk的临时顺序节点
在你当前的业务场景,我是商品首页用到的锁,在该场景下就不涉及到数据的修改,只是在读数据而已,所以本地锁和分布式锁都可以使用,而我们优先建议使用本地锁,因为该锁的粒度小,并发请求就高。
我们日后随着用户量的增加,下把基础架构,指的是我采用分布式锁来搭建。
②热点数据不设置过期时间

  1. 缓存雪崩:

①缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
②redis服务器宕机,请求全部转发到数据库
解决方案:
①原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
②针对Redis解决宕机的导致的缓存雪崩,可以提前搭建好Redis的主从服务器进行数据同步,并配置哨兵机制,这样在Redis服务器因为宕机而无法提供服务时,可以由哨兵将Redis从服务器设置为主服务器,继续提供服务。
客户端

使用场景

1、redis的适用场景

String

热点数据缓存
session共享
分布式锁
计数器:点赞数

list

消息队列
微博或者微信的消息流推送。

set

抽奖活动
社交关系网模型(共同关注的人、可能认识的人)
微信点赞,收藏,标签
商品筛选

hash

购物车:key:用户id;field:商品id;value:商品数量。

zset

单日排行榜

其他

1、Redis为什么快?

1)完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)
2)数据结构简单,对数据操作也简单,Redis中的数据结构(散列表和跳跃表)是专门进行设计的
3)采用单线程,避免了多线间的竞争,避免不必要的上下文切换
4)使用I/O多路复用模型,非阻塞IO、一个线程处理多个IO流

  • bgsave 在后台执行rdb的保存,不影响主线程的正常使用,不会产生阻塞
  • bgrewriteaof 在后台执行aof文件的保存,不影响主线程的正常使用,不会产生阻塞

2、Redis为什么是单线程的?

因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了
1)绝大部分请求是纯粹的内存操作
2)采用单线程,避免了不必要的上下文切换和竞争条件

3、Redis真的是单线程吗?

Redis 是单线程,主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程。但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。所以,严格意义上来说,redis并不是单线程。

4、为什么Redis的操作是原子性的,怎么保证原子性的?

对于Redis而言,命令的原子性指的是:一个操作的不可以再分,操作要么执行,要么不执行。
Redis的操作之所以是原子性的,是因为Redis是单线程的。
Redis本身提供的所有API都是原子操作,Redis中的事务其实是要保证批量操作的原子性。
多个命令在并发中也是原子性的吗?
不一定, 将get和set改成单命令操作,incr 。使用Redis的事务,或者使用Redis+Lua==的方式实现.

5、MySQL里有大量数据,如何保证Redis中的数据都是热点数据?(Redis内存淘汰策略)

使用 allkeys-lru (挑选最近最少使用的数据淘汰)淘汰策略,那留下来的都是经常访问的热点数据

redis内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。
数据淘汰策略:
①设置过期时间的key
volatile-lru:从已设置过期时间的数据集中挑选最近最少使用
volatile-ttl:从已设置过期时间的数据集中挑选将要过期
volatile-random:从已设置过期时间的数据集(server. db[i]. expires)中任意选择数据淘汰。
②所有key
allkeys-lru:从数据集(server. db[i]. dict)中挑选最近最少使用的数据淘汰。
allkeys-random:从数据集(server. db[i]. dict)中任意选择数据淘汰。
no-enviction(驱逐):禁止驱逐数据。

6、redis过期策略

因为内存是有限的,如果缓存中的所有数据都是一直保存的话,分分钟直接 Out of memory。
数据删除策略:Redis中可以对数据设置数据的有效时间,数据的有效时间到了以后,就需要将数据从内存中删除掉。而删除的时候就需要按照指定的规则进行删除,这种删除规则就被称之为数据的删除策略。
注意:Redis 中除了字符串类型有自己独有设置过期时间的命令 setex 外,其他方法都需要依靠 expire 命令来设置过期时间 。另外, persist 命令可以移除一个键的过期时间。
惰性删除:只会在取出 key 的时候才对数据进行过期检查。这样对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。
定期删除:每隔一段时间抽取一批 key 执行删除过期 key 操作。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。
定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,所以 Redis 采用的是 定期删除+惰性/懒汉式删除 。

RabbitMQ

1、你们项目中哪里用到了RabbitMQ ?

我们项目中很多地方都使用了RabbitMQ , RabbitMQ 是我们项目中服务通信的主要方式之一 , 我们项目中服务通信主要有二种方式实现 :

  1. 通过Feign实现服务调用
  2. 通过MQ实现服务通信

基本上除了查询请求之外, 大部分的服务调用都采用的是MQ实现的异步调用 , 例如 :

  1. 发布内容的异步审核
  2. 验证码的异步发送
  3. 用户行为数据的异步采集入库
  4. 搜索历史记录的异步保存
  5. 用户信息修改的异步通知(用户修改信息之后, 同步修改其他服务中冗余/缓存的用户信息)
  6. 静态化页面的生成
  7. MYSQL和Redis , ES之间的数据同步

2、为什么会选择使用RabbitMQ ? 有什么好处 ?

选择使用RabbitMQ是因为RabbitMQ的功能比较丰富 , 支持各种消息收发模式(简单队列模式, 工作队列模式 , 路由模式 , 直接模式 , 主题模式等) , 支持延迟队列 , 惰性队列而且天然支持集群, 保证服务的高可用, 同时性能非常不错 , 社区也比较活跃, 文档资料非常丰富

这个好处很多了~~ 主要体现在

  • 吞吐量提升:无需等待订阅者处理完成,响应更快速
  • 故障隔离:服务没有直接调用,不存在级联失败问题
  • 调用间没有阻塞,不会造成无效的资源占用
  • 耦合度极低,每个服务都可以灵活插拔,可替换
  • 流量削峰:不管发布事件的流量波动多大,都由Broker接收,订阅者可以按照自己的速度去处理事件

当然,使用MQ也有很多缺点,比如:

  • 架构复杂了,业务没有明显的流程线,不好管理
  • 需要依赖于Broker的可靠、安全、性能

3、使用RabbitMQ如何保证消息不丢失 ?

在使用RabbitMQ发送消息的时候,从消息发送,到消费者接收,会经历多个过程 , 其中的每一步都可能导致消息丢失

  1. 生产者确认
    发布者确认机制publisher-confirm:确认消息到达交换机
    发布者回执机制publisher-return:确认消息从交换机到队列
  2. 消息队列本身
    可以进行消息持久化(交换机持久化, 队列持久化 , 消息持久化), 即使rabbitMQ挂了,重启后也能恢复数据
  3. 消费者
    消费者确认机制 , 消费者失败重试机制或者人工介入

4、消息的重复消费问题如何解决的 ?

在使用RabbitMQ进行消息收发的时候, 如果发送失败或者消费失败会自动进行重试, 那么就有可能会导致消息的重复消费

解决方案主要有两种方式

第一个是:每条消息设置一个唯一的标识id

第二个是:幂等方案

  • token+redis
  • 分布式锁
  • 数据库锁(悲观锁、乐观锁)

5、如何解决消息堆积?
第一:提高消费者的消费能力 ,可以使用多线程消费任务

第二:增加更多消费者,提高消费速度

​ 使用工作队列模式, 设置多个消费者消费消费同一个队列中的消息

第三:扩大队列容积,提高堆积上限

可以使用RabbitMQ惰性队列,惰性队列的好处主要是

①接收到消息后直接存入磁盘而非内存

②消费者要消费消息时才会从磁盘中读取并加载到内存

③支持数百万条的消息存储

5、RabbitMQ如何保证消费的顺序性 ?

这个是比较简单的,可以让一个队列只设置一个消费者消费即可 , 多个消费者之间是无法保证消息消费顺序性的

6、RabbitMQ的延迟队列有了解过嘛 ?

RabbitMQ的延迟队列有二种实现方案 : 延迟队列的视线方案

  1. 利用两个特性: Time To Live(TTL)、Dead Letter Exchanges(DLX)
  2. 利用rabbitmq中的插件x-delay-message

7、什么情况下消息会成为死信 ?
第一,当消费者使用basic.reject或 basic.nack声明消费失败,并且消息的requeue参数设置为false

第二,消息是一个过期消息,超时无人消费

第三,要投递的队列消息满了,无法投递

当一个队列中出现这些情况之一时,就会成为死信(dead letter):

多线程

线程基础知识

1、进程和线程

1、一个进程可以有多个线程,但至少有一个线程
2、资源分配给进程,同一个进程的所有线程共享该进程所有资源
3、进程是资源分配的基本单位,线程是CPU调度和分派的基本单位

2、并发和并行

并发:同一时刻多个线程在访问同一个资源,多个线程对一个点
并行:同时进行多件事,多项工作一起执行,之后再汇总

3、线程的创建方式

1)继承Thread类创建线程
2)实现Runnable接口创建线程
3)使用Callable和Future创建线程
4)使用线程池创建线程

4、线程的状态转换有什么?(生命周期)

在这里插入图片描述
(1)新建状态(New) :线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
(2)就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
(3)运行状态(Running):线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
(4)阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

  • 等待阻塞 – 通过调用线程的wait()方法,让线程等待某工作的完成。
  • 同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
  • 其他阻塞 --通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
    (5)死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

5、sleep和wait在线程里有什么区别?

(1)sleep() 方法是 Thread 类中的方法,而 wait() 方法是 Object 类中的方法。
(2)sleep() 方法不会释放 lock,但是 wait() 方法会释放,而且会加入到等待队列中。
(3)sleep() 方法不依赖于同步器 synchronized(),但是 wait() 方法 需要依赖 synchronized 关键字。
(4)线程调用 sleep() 之后不需要被唤醒(休眠时开始阻塞,线程的监控状态依然保持着,当指定的休眠时间到了就会自动恢复运行状态),但是 wait() 方法需要被重新唤醒(不指定时间需要被别人中断)。

6、notify()和notifyAll()有什么区别?

都是用来唤醒调用wait()方法进入等待锁资源队列的线程
notify()随机唤醒一个对象的等待池中的一个线程
notifyAll()唤醒对象的等待池中所有的线程

7、Thread 类中的start() 和 run() 方法有什么区别?

1、start方法用来启动相应的线程;
2、run方法只是thread的一个普通方法,在主线程里执行;
3、需要并行处理的代码放在run方法中,start方法启动线程后自动调用run方法;
4、run方法必须是public的访问权限,返回类型为void。

8、怎么理解线程安全?

线程安全问题体现在三个方面,原子性、有序性、可见性
1、原子性:和数据库中原子性一样,是一段程序只能由一个线程完
整的执行完成,而不能存在多个线程干扰。
CPU 的 上 下 文 切 换 , 是 导 致 原 子 性 问 题 的 核 心 , 而 JVM 里 面 提 供 了Synchronized 关键字来解决原子性问题
2、可见性:在多线程环境下,由于读和写是发生在不同的线程里面,有可能出现某个线程对共享变量的修改,对其他线程不是实时可见的。
导致可见性问题的原因有很多,比如 CPU 的高速缓存、CPU 的指令重排序、编译器的指令重排序。
3、有序性:指的是程序编写的指令顺序和最终 CPU 运行的指令顺序可能出现不一致的现象,这种现象也可以称为指令重排序,所以有序性也会导致可见性问题。
可见性和有序性可以通过 JVM 里面提供了一个 Volatile 关键字来解决

9、如何中断一个线程?

1、Java Thread 里面提供了一个 stop 方法可以强行终止,但是这种方式是不安全的,因为有可能线程的任务还没有,导致出现运行结果不正确的问题。
2、在 Java Thread 里面提供了一个 interrupt()方法,这个方法配合
isInterrupted()方法使用,就可以实现安全的中断机制。
这种实现方法并不是强制中断,而是告诉正在运行的线程,你可以停止了,不过是否要中断,取决于正在运行的线程,所以它能够保证线程运行结果的安全性。

并发锁

1、讲一下synchronized关键字的底层原理?

synchronized 底层使用的JVM级别中的Monitor 来决定当前线程是否获得了锁,如果某一个线程获得了锁,在没有释放锁之前,其他线程是不能或得到锁的。synchronized 属于悲观锁。
synchronized 因为需要依赖于JVM级别的Monitor ,是重量级锁,相对性能也比较低。但在jdk1.8之后做了优化,CAS自旋、轻量级锁、偏向锁等。

2、CAS 你知道吗?

CAS是Java 中 Unsafe 类里面的方法,它的全称是 CompareAndSwap,比较并交换的意思。它的主要功能是能够保证在多线程环境下,保证共享变量的修改的原子性。
在CAS中有3个操作数:内存值V预期值A要修改的新值B。当且仅当预期值A和内存值V相等时,将内存值V修改为B并返回true,否则什么都不做,并返回false。

通常情况下自旋锁都是通过这种方式来完成的。
CAS 主要用在并发场景中,比较典型的使用场景有两个。
第一个是 J.U.C 里面 Atomic 的原子实现,比如 AtomicInteger,AtomicLong。
第 二 个 是 实 现 多 线 程 对 共 享 资 源 竞 争 的 互 斥 性 质 , 比 如 在 AQS 、ConcurrentHashMap、ConcurrentLinkedQueue 等都有用到。

3、什么是AQS?

AQS的话,其实就一个jdk提供的类AbstractQueuedSynchronizer,抽象队列同步器
①AQS 内部维护了一个同步标志位 state,用来实现同步加锁控制:
初始值state为0,表示没有获取锁,state等于1的时候表明获取到了锁。state 实际上表示的是已获得锁的线程进行加锁操作的次数
②在它的内部还提供了基于 FIFO(先进先出) 的等待队列(CLH队列),来表示排队等待锁的线程,当线程争抢锁失败后会封装成 Node 节点加入 CLH 队列中去。
是一个双向列表,有两个节点

  • tail 指向队列最后一个元素
  • head 指向队列中最久的一个元素

4、ReentrantLock的实现原理

ReentrantLock:可重入锁,获得锁的线程在释放锁之前再次调用一个需要获取锁的方法时,不需要加锁,直接统计加锁次数。避免死锁
分布式可重入锁思路:统计加锁次数
一个线程获取锁时,如果第一次获取锁成功 锁重入次数为1
如果该线程调用了另一个需要获取锁的方法 锁获取的次数再+1
如果该线程释放锁,对获取锁的次数-1,减完后的值为0表示锁完全释放 否则释放失败

实现原理

ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似

构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。

5、synchronized和Lock有什么区别 ?

来源

  • synchronized 是java中关键字,源码在 jvm 中,用 c++ 语言实现
  • Lock 是接口,源码由 jdk 提供,用 java 语言实现

释放锁

  • synchronized会自动释放,不会死锁
  • lock需要手动调用 unlock 方法释放锁,很可能死锁,因此使用Lock时需要在finally块中释放锁;

功能

  • 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
  • Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量
  • lock是轻量级锁。synchronized是重量级锁

性能

  • 竞争不激烈,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖;两者效率差不多
  • 竞争资源非常激烈时,此时Lock的性能要远远优于synchronized。

7、锁的分类?

独占锁:同一时刻只有一个线程能够获取锁
读写锁(ReentrantReadWriteLock ):允许多个线程同时读取共享数据,但是一次只允许一个线程对共享数据进行更新。适用读多写少
读锁:共享锁(多个读线程之间共享)
写锁:排它锁(只能有一个线程操作写锁、独占锁)
读写锁允许读读共享, 读写互斥,写写互斥。

6、什么是死锁?怎么解决?怎么避免?

死锁:多个线程争抢同一个资源时互相等待对方释放锁
比如重启服务,或者杀掉某个线程。
我们只需要通过jdk自动的工具就能搞定

1、jps命令来查看当前java程序运行的进程id

2、通过jstack命令来查看这个进程id,就能展示出来死锁的问题,并且,可以定位代码的具体行号范围,我们再去找到对应的代码进行排查就行了。

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

7、请谈谈你对 volatile 的理解?

volatile 是一个关键字,是Java提供的最轻量级的同步机制,可以修饰类的成员变量、类的静态成员变量,主要有两个功能

第一:保证了共享变量的可见性,被volatile关键字修饰的变量,如果值发生了变化,其他线程立刻可见,避免出现脏读现象。

第二: 通过增加内存屏障防止指令重排,可以保证代码执行有序性

8、那你能聊一下ConcurrentHashMap的原理吗?

ConcurrentHashMap 是一种线程安全的高效Map集合,jdk1.7和1.8也做了很多调整。

  • JDK1.7的底层采用是分段的数组+链表 实现
  • JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。

在jdk1.7中 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一 种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构 的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修 改时,必须首先获得对应的 Segment的锁。

Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元 素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁

在jdk1.8中的ConcurrentHashMap 做了较大的优化,性能提升了不少。首先是它的数据结构与jdk1.8的hashMap数据结构完全一致。其次是放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保 证并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲 突,就不会产生并发 , 效率得到提升

9、怎么保证多线程的执行安全

线程的安全性问题体现在:

  • 原子性:一个或者多个操作在 CPU 执行的过程中不被中断的特性
  • 可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到
  • 有序性:程序执行的顺序按照代码的先后顺序执行

导致原因:

  • 缓存导致的可见性问题
  • 线程切换带来的原子性问题
  • 编译优化带来的有序性问题

解决办法:

  • JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
  • synchronized、volatile、LOCK,可以解决可见性问题
  • Happens-Before 规则可以解决有序性问题

10、常见线程安全的并发容器有哪些?

CopyOnWriteArrayList、CopyOnWriteArraySet:采用写时复制
ConcurrentHashMap:采用分段锁的方式实现线程安全

线程池

1、谈谈你对线程池的理解?

首先,线程池本质上是一种池化技术,而池化技术是一种资源复用的思想,比较常见的有连接池、内存池、对象池。
而线程池里面复用的是线程资源,它的核心设计目标,我认为有两个:
1、减少线程的频繁创建和销毁带来的性能开销,因为线程创建会涉及到 CPU 上下文切换、内存分配等工作。
2、线程池本身会有参数来控制线程创建的数量,这样就可以避免无休止的创建线程带来的资源利用率过高的问题,起到了资源保护的作用。

2、线程的核心参数

在线程池中一共有7个核心参数:

  1. corePoolSize :核心线程数
  2. maximumPoolSize :最大线程数
  3. keepAliveTime: 多余的空闲线程的存活时间
  4. unit: 存活时间单位
  5. workQueue:任务队列,5.被提交但尚未被执行的任务
  6. threadFactory: 线程工厂 ,表示生成线程池中工作线程的线程工厂, 用于创建线程,一般默认的即可
  7. handler :拒绝策略, 表示当队列满了,并且工作线程大于 等于线程池的最大线程数(maximumPoolSize)时会触发拒绝策略

在拒绝策略中又有4中拒绝策略
当线程数过多以后,第一种是抛异常、第二种是由调用者执行任务、第三是丢弃当前的任务,第四是丢弃最早排队任务。默认是直接抛异常。

阻塞队列

阻塞队列中能够容纳的元素个数,通常情况下是有界的,比如我们实例化一个 ArrayBlockingList,可以在构造方法中传入一个整形的数字,表示这个基于数组的阻塞队列中能够容纳的元素个数。这种就是有界队列。
而无界队列,就是没有设置固定大小的队列,不过它并不是像我们理解的那种元素没有任何限制,而是它的元素存储量很大,像LinkedBlockingQueue,它的默认队列长度是 Integer.Max_Value,所以我们感知不到它的长度限制。
无界队列存在比较大的潜在风险,如果在并发量较大的情况下,线程池中可以几乎无限制的添加任务,容易导致内存溢出的问题!

3、线程池的种类?

在jdk中默认提供了4中方式创建线程池

第一个是:newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回 收空闲线程,若无可回收,则新建线程。

第二个是:newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列 中等待。

第三个是:newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

第四个是:newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任 务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

4、线程池工作原理?

在这里插入图片描述

1.在创建了线程池后,线程池中的线程数为零。
2.当调用execute()方法添加一个请求任务时,线程池会做出如下判断:
1.如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
2.如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
3.如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
4.如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
3.当一个线程完成任务时,它会从队列中取下一个任务来执行。
4.当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。

5、为什么不建议使用Executors创建线程池呢?

主要原因是如果使用Executors创建线程池的话,它允许的请求队列默认长度是Integer.MAX_VALUE,这样的话,有可能导致堆积大量的请求,从而导致OOM(内存溢出)。

所以,我们一般推荐使用ThreadPoolExecutor来创建线程池,这样可以明确规定线程池的参数,避免资源的耗尽。

6、线程池的拒绝策略

ThreadPoolExecutor自带的拒绝策略如下:
1.AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
2.CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
3.DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加人队列中 尝试再次提交当前任务。
4.DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。 如果允许任务丢失,这是最好的一种策略。
以上内置的策略均实现了RejectedExecutionHandler接口,也可以自己扩展RejectedExecutionHandler接口,定义自己的拒绝策略

7、谈谈你对ThreadLocal的理解?

ThreadLocal 是一种线程隔离机制,它提供了多线程环境下对于共享变量访问的安全性。
在多线程访问共享变量的场景中,一般的解决办法是对共享变量加锁,从而保证在同一时刻只有一个线程能够对共享变量进行操作,但是加锁会带来性能的下降。

实现原理

ThreadLocal 用了一种空间换时间的设计思想,在 Thread 类 里 面 有 一 个 成 员 变 量ThreadLocalMap,它专门用来存储当前线程的共享变量副本,后续这个线程直接ThreadLocalMap 中的共享变量副本的操作,不会影响全局共享变量的值。这样既解决了线程安全问题,又避免了多线程竞争加锁的开销

防止内存泄漏

1、每次使用完 ThreadLocal 以后,主动调用 remove()方法移除数据
2、把ThreadLocal声明称全局变量,使得它无法被回收。

8、线程池如何知道一个线程的任务已经执行完成?

在线程池内部,当我们把一个任务丢给线程池去执行,线程池会调度工作线程来执行这个任务的 run 方法,run 方法正常结束,也就意味着任务完成了。
在线程池中,有一个 submit()方法,它提供了一个 Future 的返回值,我们通过Future.get()方法来获得任务的执行结果,当线程池中的任务没执行完之前,future.get()方法会一直阻塞,直到任务执行结束。因此,只要 future.get()方法正常返回,也就意味着传入到线程池中的任务已经执行完成了!

JVM

1、JVM组成

1、 说一下 JVM 的主要组成部分?及其作用?

在这里插入图片描述

主要组成部分

1、类加载器(ClassLoader)
2、运行时数据区(Runtime Data Area)
3、执行引擎(Execution Engine)
4、本地库接口(Native Interface)

作用(运行流程)

1、类加载器:把 Java 代码转换成字节码
2、运行时数据区:再把字节码加载到内存中,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎
3、执行引擎:将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口来实现整个程序的功能。

2、你能详细说一下 JVM 运行时数据区吗?

在这里插入图片描述

java虚拟机主要分为以下几个区:

线程共享区域:它是随着虚拟机的开启而创建,关闭而销毁;

(1)方法区(永久代)
①jdk1.7的时候在堆中,jdk1.8中移到了本地内存上,重新开辟了一块空间,叫做元空间。避免OOM出现
②很少发生垃圾回收,在这里进行的GC主要是对方法区里的常量池和对类型的卸载
③主要用来存储已被虚拟机加载的类的信息、常量、静态变量和即时编译器编译后的代码等数据。

(2)
堆解决的是对象实例、数组存储的问题,垃圾回收器管理的主要区域。当堆中没有内存空间可分配给实例,也无法再扩展时,则抛出OutOfMemoryError异常。

线程私有区域:依赖用户的线程创建而创建、销毁而销毁

(1)虚拟机栈:
栈里面存的是栈帧,用于存储局部变量表、操作数栈、动态链接和方法出口等信息。

  • 栈帧:每个方法从调用到执行的过程就是一个栈帧在虚拟机栈中入栈到出栈(相当于清空了数据)的过程。不需要GC
  • 局部变量表:用于保存函数的参数和局部变量。
  • 操作数栈:又称操作栈,大多数指令都是从这里弹出数据,执行运算,然后把结果压回操作数栈。

(2)本地方法栈
本地方法栈和虚拟机栈类似,只是本地方法执行的是本地方法,底层是c语言编写的
(3)程序计数器(PC寄存器)
程序计数器中存放的是当前线程所执行的字节码的行数。JVM工作时就是通过改变这个计数器的值来选取下一个需要执行的字节码指令。

3、详细的介绍Java堆吗?

在这里插入图片描述

jdk1.7下:

①新生代:主要用来存放新生的对象。
②老年代:存放应用中生命周期长的内存对象。
③方法区(永久代):永久保存区域

jdk1.8下:

Java中的堆术语线程共享的区域。主要用来保存对象实例,数组等,当堆中没有内存空间可分配给实例,也无法再扩展时,则抛出OutOfMemoryError异常。

​ 在JAVA8中堆内会存在年轻代、老年代

​ 1)Young区被划分为三部分,Eden区和两个大小严格相同的Survivor区,其中,Survivor区间中,某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制对象用。在Eden区变满的时候, GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到Tenured区间。

​ 2)Tenured区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在Young复制转移一定的次数以后,对象就会被转移到Tenured区。
新生代和老生代是1:2的关系
①新生代:内部又被分为了三个区域。Eden区,S0区,S1区【8:1:1】
②老年代

4、堆和栈的区别是什么?

1、栈内存一般会用来存储局部变量和方法调用,但堆内存是用来存储Java对象和数组的的。堆会GC垃圾回收,而栈不会。

2、栈内存是线程私有的,而堆内存是线程共有的。

3,、两者异常错误不同,但如果栈内存或者堆内存不足都会抛出异常。

栈空间不足:java.lang.StackOverFlowError。

堆空间不足:java.lang.OutOfMemoryError。

2、垃圾回收

1、GC是什么?为什么要GC?

GC:自动的垃圾回收机制
为了让程序员更专注于代码的实现,而不用过多的考虑内存释放的问题

2、Java中垃圾收集的方法有哪些?

1)复制算法 :将原有的内存空间一分为二,每次只用其中的一块在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,
优点: 效率高,无内存碎片,
缺点:需要内存容量大,比较耗内存

2)标记-清除 :根据可达性分析算法标记无用对象,然后进行清除回收
优点:效率快
缺点:会产生内存碎片
3)标记-整理 :标记无用对象,让存活的对象都向一端移动,然后清除掉边界以外的垃圾。
优点:不会产生内存碎片
缺点:需要移动对象,效率慢

4)分代收集算法:根据对象的存活周期的不同将内存划分为几块,一般就分为新生代和老年代,根据各个年代的特点采用不同的收集算法,新生代基本采用复制算法,老年代采用标记整理算法。
在java8时,堆被分为了两份:新生代和老年代,它们默认空间占用比例是1:2

对于新生代,内部又被分为了三个区域。Eden区,S0区,S1区默认空间占用比例是8:1:1

具体的工作机制是有些情况:

1)当创建一个对象的时候,那么这个对象会被分配在新生代的Eden区。当Eden区要满了时候,触发YoungGC。

2)当进行YoungGC后,此时在Eden区存活的对象被移动到S0区,并且当前对象的年龄会加1,清空Eden区。

3)当再一次触发YoungGC的时候,会把Eden区中存活下来的对象和S0中的对象,移动到S1区中,这些对象的年龄会加1,清空Eden区和S0区。

4)当再一次触发YoungGC的时候,会把Eden区中存活下来的对象和S1中的对象,移动到S0区中,这些对象的年龄会加1,清空Eden区和S1区。

5)对象的年龄达到了某一个限定的值(默认15岁 ),那么这个对象就会进入到老年代中。

当然也有特殊情况,如果进入Eden区的是一个大对象,在触发YoungGC的时候,会直接存放到老年代

当老年代满了之后,触发FullGCFullGC同时回收新生代和老年代,当前只会存在一个FullGC的线程进行执行,其他的线程全部会被挂起。 我们在程序中要尽量避免FullGC的出现。

3、 说一下 JVM 有哪些垃圾回收器

1、新生代回收器:
Serial:最早的单线程串行垃圾回收器
ParNew:是 Serial 的多线程版本。
Parallel 和 ParNew 收集器类似是多线程的,但 Parallel 是吞吐量优先的收集器,可以牺牲等待时间换取系统的吞吐量。
2、老年代回收器:
Serial Old:Serial 垃圾回收器的老年版本,同样也是单线程的,可以作为 CMS 垃圾回收器的备选预案。
Parallel Old 是 Parallel 老生代版本,Parallel 使用的是复制的内存回收算法,Parallel Old 使用的是标记-整理的内存回收算法。
CMS:一种以获得最短停顿时间为目标的收集器,非常适用 B/S 系统。(标记清除算法)
G1(jdk9):一种兼顾吞吐量和停顿时间的 GC 实现,是 JDK 9 以后的默认 GC 选项。

4、简述java回收策略以及Minor GC和Major GC(full GC)

嗯,其实它们指的是不同代之间的垃圾回收
Minor GC 发生在新生代的Eden区垃圾回收,这个区的对象生存时间短,发送GC频率高,回收速度快,
Major GC 发生在老年代区域的垃圾回收,老年代空间不足时,手动配置的情况下会先尝试触发Minor GC。Minor GC之后空间还不足,则会触发Major GC,Major GC速度比较慢,暂停时间长

==Full GC ==新生代 + 老年代完整垃圾回收,暂停时间长,应尽力避免

5、Java 中都有哪些引用类型?

强引用:表示对象处于有用且必须的状态,内存不足发生OOM也不会GC
软引用:表示对象处于有用非必须的状态,在发生内存不足发生OOM异常之前会被回收。
弱引用:表示对象处于可能有用非必须的状态,在下一次GC时会被回收。
虚引用(幽灵引用/幻影引用):表示对象处于无用的状态

6、如何判断一个对象是否存活?(或者GC对象的判定方法)

判断一个对象是否存活有两种方法:
(1)引用计数法
一个对象被引用了一次,在当前的对象头上递增一次引用次数,如果这个对象的引用次数为0,代表这个对象可回收
优点:实现简单,判断效率高
缺点:无法解决循环引用问题,也就是说当对象A引用对象B,对象B又引用者对象A,那么此时A,B对象的引用计数器都不为零,也就造成无法完成垃圾回收,
(2)==可达性分析算法(引用链法) ==
扫描堆中的对象,看是否能够沿着 GC Root 对象 为起点的引用链找到该对象,找不到,表示可以回收
在java中可以作为GC Roots的对象有以下几种:虚拟机栈中引用的对象、方法区类静态属性引用的对象、方法区常量池引用的对象、本地方法栈JNI引用的对象。

6、StackOverflowError(栈溢出)和OutOfMemoryError(堆溢出)常见原因?怎么排查?

StackOverFlowError 的常见原因:

1、无限递归循环调用(最常见)。
2、执行了大量方法,导致线程栈空间耗尽。
3、方法内声明了海量的局部变量。
4、native 代码有栈上分配的逻辑,并且要求的内存还不小

OutOfMemoryError的常见原因:

1、内存中加载的数据量过于庞大,如一次从数据库取出过多数据。
集合类中有对对象的引用,使用完后未清空,使得JVM不能回收。
代码中存在死循环或循环产生过多重复的对象实体。
启动参数内存值设定的过小。
排查:可以通过jvisualvm进行内存快照分析,参考https://www.cnblogs.com/boboooo/p/13164071.html

3、类加载

1、Java类加载过程?

编译的过程就是把.java 文件编译成.class 文件。
类加载的过程,就是把 class 文件装载到 JVM 内存中,装载完成以后就会得到一个 Class 对象,我们就可以使用 new 关键字来实例化这个对象。

类从加载到虚拟机中开始,直到卸载为止,它的整个生命周期包括了:加载连接(包括验证、准备、解析)初始化使用卸载

1.加载:查找和导入class文件

2.验证:保证加载类的准确性

3.准备:为类变量分配内存并设置类变量初始值

4.解析:把类中的符号引用转换为直接引用

5.初始化:对类的静态变量,静态代码块执行初始化操作

6.使用:JVM 开始从入口方法开始执行用户的程序代码

7.卸载:当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存

2、什么是类加载器,类加载器有哪些?

JVM只会运行二进制文件,而类加载器(ClassLoader)的主要作用就是将把Java代码转换为字节码文件加载到JVM中
常见的类加载器有4个:

1、启动类加载器(BootStrap ClassLoader):其是由C++编写实现。核心类库的加载,也就是JAVA_HOME/jre/lib目录下的类库。
2、扩展类加载器(ExtClassLoader):主要加JAVA_HOME/jre/lib/ext目录中的类库。
3、应用类加载器(AppClassLoader):,主要用于加载当前应用classpath下的类,也就是加载开发者自己编写的Java类。
4、自定义类加载器:开发者自定义类继承ClassLoader,实现自定义类加载规则。

3、简述双亲委派机制

双亲委托机制:按照类加载器的层级关系,逐层进行委派。
比如当需要加载一个 class 文件的时候,首先会把这个 class 的查询和加载委派给父加载器去执行,如果父加载器都无法加载,再尝试自己来加载这个 class。
因为这种机制,所以加载所有类,优先给系统加载器加载,那对于核心类库中的类没办法破坏,比如自己写一个 java.lang.String,最终还是会交给启动类加载器,在JRE/lib目录的rt.jar找到了String.class,将其加载到内存中,那么自己写的 java.lang.String就不会被加载,不会被使用。

作用

避免类的重复加载,防止内存中有同样的字节码

怎么打破双亲委派机制

  • 自定义类加载器,继承 ClassLoader 抽象类,重写 loadClass 方法,在这个方法可以自定义要加载的类使用的类加载器。
  • tomcat服务器就是打破了双亲委派机制

4、调优

1、说一下 JVM 调优的工具

JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下
1、jps 输出JVM中运行的进程状态信息
2、jstack查看java进程内线程的堆栈信息。
3、jmap 用于生成堆转存快照
4、jstat用于JVM统计监测工具
5、还有一些可视化工具,像jconsole和VisualVM等

2、常用的 JVM 调优的参数都有哪些?

Xms2g:初始化堆大小为 2g;
-Xmx2g:堆最大内存为 2g;
-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
-XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
–XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
-XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
-XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
-XX:+PrintGC:开启打印 gc 信息;
-XX:+PrintGCDetails:打印 gc 详细信息。

我记得当时我们设置过堆的大小,像-Xms和-Xmx
还有就是可以设置年轻代中Eden区和两个Survivor区的大小比例
还有就是可以设置使用哪种垃圾回收器等等。具体的指令还真记不太清楚。

3、服务器CPU持续飙高,你的排查方案与思路?

CPU 是整个电脑的核心计算资源,对于一个应用进程来说,CPU 的最小执行单元是线程。
cpu上下文切换过多。对于 CPU 来说,同一时刻下每个 CPU 核心只能运行一个线程,如果有多个线程要执行,CPU 只能通过上下文切换的方式来执行不同的线程。上下文切换需要做两个事情
1、保存运行线程的执行状态
2、让处于等待中的线程执行
这两个过程需要 CPU 执行内核相关指令实现状态保存,如果较多的上下文切换会占据大量 CPU 资源,从而使得 cpu 无法去执行用户进程中的指令,导致响应速度下降。
在 Java 中,文件 IO、网络 IO、锁等待、线程阻塞等操作都会造成线程阻塞从而触发上下文切换

1、使用top命令找到cpu利用率较高的进程,记录这个进程id
2、找到这个进程中 CPU 消耗过高的线程
3、通过jstack命令 获得线程的 Dump 日志,定位到线程日
志后就可以找到问题的代码。

设计模式

1、单例模式

单例模式的特点:

1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。

单例的四大原则:

1、构造私有
2、以静态方法或者枚举返回实例
3、确保实例只有一个,尤其是多线程环境
4、确保反序列换时不会重新构建对象

①饿汉式:线程安全,一开始就初始化。

8.public class Singleton {
    
      
9.  
10.    /** 
11.     * 私有构造 
12.     */  
13.    private Singleton() {
    
      
14.        System.out.println("构造函数Singleton1");  
15.    }  
16.  
17.    /** 
18.     * 初始值为实例对象 
19.     */  
20.    private static Singleton single = new Singleton();  
21.  
22.    /** 
23.     * 静态工厂方法 
24.     * @return 单例对象 
25.     */  
26.    public static Singleton getInstance() {
    
      
27.        System.out.println("getInstance");  
28.        return single;  
29.    }
30. }  

懒汉式:非线程安全,延迟初始化。多线程环境下会产生多个Singleton对象

8.public class Singleton2 {
    
      
9.  
10.    /** 
11.     * 私有构造 
12.     */  
13.    private Singleton2() {
    
      
14.        System.out.println("构造函数Singleton2");  
15.    }  
16.  
17.    /** 
18.     * 初始值为null 
19.     */  
20.    private static Singleton2 single = null;  
21.  
22.    /** 
23.     * 静态工厂方法 
24.     * @return 单例对象 
25.     */  
26.    public static Singleton2 getInstance() {
    
      
27.        if(single == null){
    
      
28.            System.out.println("getInstance");  
29.            single = new Singleton2();  
30.        }  
31.        return single;  
32.    }
33. }  

双重检锁(volatile):线程安全,延迟初始化。提高同步锁的效率,避免整个方法被锁,只需要锁代码的部分
volatile关键字是为了防止创建对象时的指令重排问题,导致其他线程使用对象时造成空指针问题。

7.public class Singleton4 {
    
      
8.  
9.    /** 
10.     * 私有构造 
11.     */  
12.    private Singleton4() {
    
    }  
13.  
14.    /** 
15.     * 初始值为null 
16.     * 加volatile关键字是为了防止 创建对象时的指令重排问题,导致其他线程使用对象时造成空指针问题。
17.     */  
18.    Private volatile static Singleton4 single = null;  
19.  
20.    /** 
21.     * 双重检查锁 
22.     * @return 单例对象 
23.     */  
24.    public static Singleton4 getInstance() {
    
      
25.        if (single == null) {
    
      
26.            synchronized (Singleton4.class) {
    
      
27.                if (single == null) {
    
      
28.                    single = new Singleton4();  
29.                }  
30.            }  
31.        }  
32.        return single;  
33.    }  
34.}  

2、工厂模式

工厂设计模式,顾名思义,通过工厂就是用来生产对象的,在java中,如果创建的时候直接new该对象,就会对该对象耦合严重,假如我们要更换对象,所有new对象的地方都需要修改一遍,这显然违背了软件设计的开闭原则,如果我们使用工厂来生产对象,我们就只和工厂打交道就可以了,彻底和对象解耦,如果要更换对象,直接在工厂里更换该对象即可,达到了与对象解耦的目的;所以说,工厂模式最大的优点就是:解耦
1、简单工厂
定义:一个工厂方法,依据传入的参数,生成对应的对象;

1.public class FruitFactory {
    
      
2.  
3.    public Fruit createFruit(String type) {
    
      
4.  
5.        if (type.equals("apple")) {
    
    //生产苹果  
6.            return new Apple();  
7.        } else if (type.equals("pear")) {
    
    //生产梨  
8.            return new Pear();  
9.        }  
10.  
11.        return null;  
12.    }  
13.}  

缺点:每添加一种产品,就要修改工厂类,违反了开闭原则。
适用:只适合于产品对象较少,且产品固定的需求,对于产品变化无常的需求来说显然不合适。
2、工厂方法模式
定义:将工厂提取成一个接口或抽象类,具体生产什么产品由子类决定;

//抽象工厂接口
1.public interface FruitFactory {
    
      
2.    Fruit createFruit();//生产水果  
3.}  
//苹果工厂
1.public class AppleFactory implements FruitFactory {
    
      
2.    @Override  
3.    public Apple createFruit() {
    
      
4.        return new Apple();  
5.    }  
6.}  

优点:虽然解耦了,也遵循了开闭原则
缺点:每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度。
3、抽象工厂模式
定义:为创建一组相关或者是相互依赖的对象提供的一个接口,而不需要指定它们的具体类。

在这里插入代码片

优点:抽象工厂可以解决一系列的产品生产的需求,对于大批量,多系列的产品,用抽象工厂可以更好的管理和扩展。

三种工厂方式总结
1、对于简单工厂和工厂方法来说,两者的使用方式实际上是一样的,如果对于产品的分类和名称是确定的,数量是相对固定的,推荐使用简单工厂模式;
2、抽象工厂用来解决相对复杂的问题,适用于一系列、大批量的对象生产。

3、代理模式

代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用
动态代理:使用jdk的反射机制,创建代理对象,并动态的指定代理的目标类

jdk动态代理:

使用jdk反射包下的Proxy和InvocationHandler实现代理对象的动态创建(jdk动态代理要求目标对象必须实现接口)

CGLib动态代理:

cglib通过继承目标类,创建它的子类,在子类中重写父类中的方法,实现功能的修改(要求目标类不能是final,且目标类的方法不能是final修饰的)

场景题

1、为什么要保证接口幂等性?

所谓幂等,是指一个方法或接口被多次调用,保证重复调用结果和单次调用的结果相同。
在restful风格中,get请求是查询操作,天然幂等性。

需要保证幂等性的场景:

1、 网络波动,可能会引起请求重复
2、MQ消息重复,
3、用户重复提交,可能会误操作多次提交,或因为长时间么有响应去多点几次

解决方案

1、 数据库唯一索引
使用数据库提供的唯一索引来保证数据重复插入,避免脏数据产生
解决场景:新增
2、token+redis
第一次请求生成一个token(key:userId,value:UUID)存储到redis中,将token返回给客户端
以后每次请求都会携带这个token,查询当前token在redis中是否存在,如果存在则正常出路业务,如果不存在,则操作失败。
场景:新增、删除、修改
3、分布式锁
在分布式锁使用的时候,要注意粒度
在操作数据时,先添加一个分布式锁,当操作完成后再释放掉这把锁,同时在操作过程中,如果有人来抢锁,应当抛出异常,如

2、上传数据的安全性你们怎么控制?

1、对称加密
文件加密和解密使用相同的秘钥
优点:算法公开、计算量小、加密速度快、加密效率高。
缺点:安全性不高
用途:一般用于保存用户手机号、身份证等敏感但能解密的信息
常用算法:AES、DES、3DES、Blowfish、IDEA、RC4、RC5、RC6、HS256

2、非对称加密
文件加密和解密是不同的秘钥。共有秘钥加密,私有秘钥解密。
优点:安全
缺点:加密解密时间长,速度慢,只适合少量数据加密
用途:一般用于签名和认证(token)
常见算法:RSA、DSA(数字签名用)、ECC(移动设备用)、RS256 (采用SHA-256 的 RSA 签名)

3、权限认证是如何实现的?

五张表:用户、角色、权限
shiro、spring security、token

4、你遇到过哪些问题?

可以从内存溢出、cpu飙高、mysql调优、设计模式回答

5、生产环境遇到问题怎么排查?

1、如果不是紧急问题,我们可以在测试环境上看看能不能复现这个bug,如果能复现就在测试环境去分析然后解决,然后再同步到生产环境上。发版即可。
1,先分析日志,通常在业务中都会有日志的记录,或者查看系统日志,或者查看日志文件,然后定位问题,解决问题

2,如果问题较为复杂,情况就可能会有多种,可能是代码的问题,也有可能是数据的问题

6、怎么去查看日志?

(1)tomcat查看实时日志

  • 实时监控日志:tail -f catalina.out
  • 查询最后100行日志:tail -n 100 -f catalina.out

(2)doeker容器实时查看日志

  • 实时监控日志:docker logs -f 容器id/容器名称
  • 查询最后100行日志:docker logs -n 100 -f 容器id/容器名称

(3)查看日志文件

  • 在test.log文件中搜索”exception”:cat -n test.log | grep “exception”

  • 分页查看日志文件:more test.log

  • 使用 >xxx.txt 将查询到的日志保存到文件中,可以下载这个文件分析

    cat -n test.log |grep "debug" >debug.txt

通常的使用思路:先尝试监控实时日志,看看能不能监控到想要的信息,如果不能则需要查看日志文件,从海量日志信息中找出自己想要的错误信息。

7、怎么解决内存溢出?

内存溢出也就是当我们内存不足时,会发生OOM(堆溢出)异常。

OOM异常常见原因

  • 对象使用完未清空,JVM不能回收
  • 内存中的数据量过大,如一次性从数据库查询过大的数据
  • 启动参数设置过小

内存溢出问题虽然很棘手,我们也有解决办法,
1、修改JVM的启动参数,增加内存。-Xms,-Xmx参数
2、检查错误日志,找出可能发生内存溢出的位置。重点排查以下:

  • 检查代码中是否有死循环或递归调用。
  • 检查是否有大循环重复产生新对象实体。
  • 检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。
  • 检查List、MAP等集合对象是否有使用完后,未清除的问题。
    3、使用内存查看工具动态查看内存使用情况。某个项目上线后,每次系统启动两天后,就会出现内存溢出的错误。这种情况一般是代码中出现了缓慢的内存泄漏,用上面三个步骤解决不了,这就需要使用内存查看工具了。
    实现步骤:
    1、通过jmap指定打印他的内存快照 dump
    2、通过工具, VisualVM(Ecplise MAT)去分析 dump文件
    3、通过分析dump文件可以查看堆信息的情况,可以大概定位内存溢出是哪行代码出了问题
    4、找到对应的代码,进行修复

命令

jps:查看JVM中运行的进程信息
jstack:

jstack pid  查看java进程下线程信息

jmap是jdk自带的jvm内存分析的工具,位于jdk的bin目录

jmap -histo <pid>在屏幕上显示出指定pid的jvm内存状况
map -dump:file=c:\dump.txt pid  将jvm的堆中内存信息输出到一个文件中  pid是指java进程的pid

可视化工具

Jconsole:是jdk自带的一个内存分析工具,它提供了图形界面。
打开方式:java 安装目录 bin目录下 直接启动 jconsole.exe 就行

jvisualvm:能够监控线程,内存情况,查看方法的CPU时间和内存中的对 象,已被GC的对象,反向查看分配的堆栈
打开方式:java 安装目录 bin目录下 直接启动 jvisualvm.exe就行

8、跨域问题?

浏览器的同源策略:域名、端口号、协议必须一致。
跨域是浏览器的同源策略造成的。
跨域是指一个域名的网页去访问另一个域名的网页时,域名不同、端口号不同、ip协议不同。

解决方案:Cors(跨域资源共享)
1、@CrossOrigin加载控制器上
2、网关全局处理跨域问题CrossWebFilter

@Configuration
public class GatewayConfig {
    
    
    /*
        网关项目引入的是webflux,解决跨域使用:CorsWebFilter
        微服务引入的是web,解决跨域问题: @CrossOrigin
     */
    @Bean
    public CorsWebFilter corsWebFilter(){
    
    
        //可以手动配置允许跨域的参数: 哪些路径需要跨域校验,允许的请求方式 哪些服务器允许跨域  请求头...
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);//允许携带cookie
        config.addAllowedHeader("*");//允许携带的头
        config.addAllowedMethod("*"); //允许的请求方式
        config.addAllowedOrigin("http://manager.gmall.com");//允许跨域访问的前端服务器
        config.addAllowedOrigin("http://www.gmall.com");//允许跨域访问的前端服务器
        config.addAllowedOrigin("http://item.gmall.com");//允许跨域访问的前端服务器
        source.registerCorsConfiguration("/**",config);
        return new CorsWebFilter(source);
    }

}

9、CPU飙高怎么处理?

1.使用top命令查看占用cpu的情况
在这里插入图片描述

2.通过top命令查看后,可以查看是哪一个进程占用cpu较高,上图所示的进程为:30978
3.查看当前进程中的线程信息
ps H -eo pid,tid,%cpu | grep 30978 pid进程id,tid进程中线程id,%cpu 使用率

在这里插入图片描述
4.通过上图分析,在进程id30978中的线程id30979占用cpu较高
我们得到的线程id是十进制,我们需要把这个线程id转换为16进制才行,因为通常在日志中展示的都是16进制的线程id名称
转换方式:在linux中执行命令printf "%x\n" 30979
5.可以根据线程 id 找到有问题的线程,进一步定位到问题代码的源码行号
执行命令:jstack 30978 此处是进程id
在这里插入图片描述

10、开发流程

11、服务正在发布,怎么不影响用户使用?

1.使用 nginx 故障转移即可。
2.灰度发布 先发布一小部分 如果没有问题 在让所有用户都可以访问。
灰度发布 nginx+nacos gateway+nacos(推荐) 或者是 k8s 实现。

猜你喜欢

转载自blog.csdn.net/m0_48134604/article/details/129554578