Java后端开发高频面试题

JVM

说一下垃圾回收机制?

Java 语言中一个显著的特点就是引入了垃圾回收机制,在编写程序的时候不再需要考虑内存管理。垃圾回收机制可以有效的防止内存泄露,提高内存的内存率。

垃圾回收器通常是作为一个单独的低级线程运行,不可预知的情况下对堆中已经死亡的或者长时间没有使用的对象进行清理和回收。

回收机制的算法有:标记清除算法、复制算法、标记压缩算法等等。

描述一下垃圾回收的流程?

首先有三个代,新生代、老年代、永久代。

在新生代有三个区域:一个Eden区和两个Survivor区。当一个实例被创建了,首先会被存储Eden 区中。

具体过程是这样的:

  • 一个对象实例化时,先去看Eden区有没有足够的空间。
  • 如果有,不进行垃圾回收,对象直接在Eden区存储。
  • 如果Eden区内存已满,会进行一次minor gc。
  • 然后再进行判断Eden区中的内存是否足够。
  • 如果不足,则去看存活区的内存是否足够。
  • 如果内存足够,把Eden区部分活跃对象保存在存活区,然后把对象保存在Eden区。
  • 如果内存不足,查询老年代的内存是否足够。
  • 如果老年代内存足够,将部分存活区的活跃对象存入老年代。然后把Eden区的活跃对象放入存活区,对象依旧保存在Eden区。
  • 如果老年代内存不足,会进行一次full gc,之后老年代会再进行判断 内存是否足够,如果足够 还是那些步骤。
  • 如果不足,会抛出OutOfMemoryError(内存溢出异常)。

解释一下JVM的内存模型?

Java内存模型决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,定义了线程和主内存之间的抽象关系。

线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存(并不真实存在),本地内存中存储的是在主内存中共享变量的副本。

有两条规定:

  1. 线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写。
  2. 线程的工作内存是私有的,其他线程无法访问,线程变量值的传递通过主内存来完成。

GC回收的是堆内存还是栈内存?

主要管理的是堆内存。

什么时候新生代会转换为老年代?

  • Eden区满时,进行Minor GC时。
  • 对象体积太大, 新生代无法容纳时。
  • 虚拟机对每个对象定义了一个对象年龄(Age)计数器。当年龄增加到一定的临界值时,就会晋升到老年代中。
  • 如果在Survivor区中相同年龄的对象的所有大小之和超过Survivor空间的一半,包括比这个年龄大的对象就都可以直接进入老年代。

创建占大内存的对象分配到哪一代?

如果新创建的对象占用内存很大,则直接分配到老年代

新生代2个Survivor区的好处?

解决了内存碎片化问题。整个过程中,永远有一个Survivor区是空的,另一个非空的Survivor区是无碎片的。

遇到过OOM怎么解决?

我们可以修改虚拟机的参数,获取Heap Dump的文件,后缀名是.hprof。

-XX:+HeapDumpOnOutOfMemoryError 
-XX:HeapDumpPath=d:\jvm

之后可以使用JDK自带的一个工具jvisualvm来进行排查和定位。

Java基础

int和Integer的区别?

  • int是基本数据类型,Integer是它的包装类。
  • Integer保存的是对象的引用,int保存的变量值。
  • Integer默认是null,int默认是0。
  • Integer变量必须实例化后才能使用,而int变量不需要。

Java的基本数据类型和大小?

单位:字节
boolean(1) = byte(1) < short(2) = char(2) < int(4) = float(4) < long(8) = double(8)

Java类冲突怎么解决(jar包冲突)?

  1. 使用mvn: dependency tree查看冲突的jar
  2. 然后在pom文件里边 使用exclusion标签排除掉这些冲突的jar包

接口和抽象类的区别?

  1. 接口中所有的方法隐含的都是抽象的。而抽象类则可以同时包含抽象和非抽象的方法。
  2. 类可以实现很多个接口,但是只能继承一个抽象类。
  3. Java接口中声明的变量默认都是final的。抽象类可以包含非final的变量。
  4. Java接口中的成员函数默认是public的。抽象类的成员函数可以是private,protected或者是public。

多线程

多线程的创建方式

  • 继承Thread类
  • 实现Runnable接口
  • 直接使用线程池

实现Runnable接口这种方式更受欢迎,已经继承别的类的情况下只能实现接口。

Sleep(0)表示什么?

触发操作系统立刻重新进行一次CPU竞争,竞争的结果也许是当前线程仍然获得CPU控制权,也许会换成别的线程获得CPU控制权。

线程有三个状态,就绪态,运行态,等待态。Sleep(n)方法是让当前线程在n秒内不会参与CPU竞争。线程进入等待队列,n秒之后再次进入就绪队列。

Sleep(0)是让线程直接进入就绪状态。

Sleep与Wait区别?

sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,把执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。

wait是Object类的方法,对象调用wait方法导致本线程放弃对象锁,进入等待池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入锁池准备抢夺对象锁。

Thread.Join方法

Thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。

比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。

Lock锁与synchronized的区别?

  • Lock能完成synchronized的所有功能。
  • Lock有比synchronized更精确的线程语义和更好的性能。synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且必须在finally从句中释放。
  • Lock可以知道是不是已经获取到锁,而synchronized无法知道。

线程不安全的问题在多线程怎么解决?

可以使用synchronized、lock、volatile来实现同步。

谈谈你对volatile的理解?

volatile是轻量级的synchronized,比它的执行成本更低,因为它不会引起线程的上下文切换,它保证了共享变量的可见性,可见性的意思是当一个线程修改一个变量时,另外一个线程能读到这个修改的值。如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。还有就是它通过添加内存屏障的方式禁止指令的重排序。

如何实现主线程等待子线程执行完后再继续执行?

  1. 我们可以使用join方法,在主线程内部调用子线程.join方法。
  2. CountDownLatch实现

这是一个属于JUC的工具类,从1.5开始。主要用到方法是countDown() 和 await()。

  • await()方法阻塞当前线程,直到计数器等于0。
  • countDown()方法将计数器减一。

思路:我们可以在创建CountDownLatch对象,然后将此对象通过构造参数传递给子线程,在开启子线程后主线程调用await()方法阻塞主线程,子线程调用countDown()方法计数器减一。

简单说一下synchronize

synchronize是java中的关键字,可以用来修饰实例方法、静态方法、还有代码块;主要有三种作用:可以确保原子性、可见性、有序性。

  • 原子性就是能够保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等该线程处理完数据后才能进行。
  • 可见性就是当一个线程在修改共享数据时,其他线程能够看到。
  • 有序性就是,被synchronize锁住后的线程相当于单线程,在单线程环境jvm的重排序是不会改变程序运行结果的,可以防止重排序对多线程的影响。

synchronried的底层原理

synchronized的底层原理是跟monitor有关,也就是视图器锁,每个对象都有一个关联的monitor,当Synchronize获得monitor对象的所有权后会进行两个指令:加锁指令跟减锁指令。

monitor里面有个计数器,初始值是从0开始的。如果一个线程想要获取monitor的所有权,就看看它的计数器是不是0,如果是0的话,那么就说明没人获取锁,那么它就可以获取锁了,然后将计数器+1,也就是执行monitorenter加锁指令;monitorexit减锁指令是跟在程序执行结束和异常里的,如果不是0的话,就会陷入一个堵塞等待的过程,直到为0等待结束。

有几种线程池?并且详细描述一下线程池的实现过程

  1. newFixedThreadPool创建一个指定大小的线程池。每当提交一个任务就创建一个线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到等待队列中。
  2. newCachedThreadPool创建一个可缓存的线程池。这种类型的线程池特点是:
  3. 工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
  4. 如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
  5. newSingleThreadExecutor创建一个单线程的Executor,即只创建唯一的工作者线程来执行任务,如果这个线程异常结束,会有另一个取代它,保证顺序执行(我觉得这点是它的特色)。
  6. newScheduleThreadPool创建一个定长的线程池,而且支持定时的以及周期性的任务执行,类似于Timer。(这种线程池原理暂还没完全了解透彻)

线程池的主要参数和处理流程

主要参数有:

  1. 线程池核心线程数大小
  2. 最大线程数
  3. 存储的队列
  4. 拒绝策略
  5. 空闲线程存活时长

当需要任务大于核心线程数时候,就开始把任务往存储任务的队列里,当存储队列满了的话,就开始增加线程池创建的线程数量,如果当线程数量也达到了最大,就开始执行拒绝策略,比如说记录日志,直接丢弃,或者丢弃最老的任务,或者交给提交任务的线程执行。

当一个线程完成时,它会从队列中取下一个任务来执行。当一个线程无事可做,且超过一定的时间(keepAliveTime)时,如果当前运行的线程数大于核心线程数,那么这个线程会停掉了。

集合

List,Set,Map的区别?

  • List是一个有序的集合,里面可以存储重复的元素。
  • Set是一个不能存储相同元素的集合。
  • Map是一个通过键值对的方式存储元素的,键不能重复。

HashMap具体如何实现的?

HashMap底层是基于数组+链表实现的,通过添加键的hashcode与上数组的长度来得到这个元素在数组中的位置,如果这个位置没有数据,那么就把这个数据当做第一个节点。如果这个位置有了链表,那么在JDK1.7的时候使用的是头插法,在JDK1.8的时候使用尾插法。

HashMap在JDK1.8的版本中引入了红黑树结构做优化,当链表元素个数大于等于8时,链表转换成树结构;链表元素个数小于等于6时,树结构还原成链表。

JDK1.8的时候为什么选择8和6作为转换点?

因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,显然树的效率更高一些。

链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是树和链表相互转换的时间也不会太短。还有选择6和8,中间有个差值7可以有效防止链表和树频繁转换。

HashMap的扩容机制?

HashMap底层是数组,在第一次put的时候会初始化,发生第一次扩容到16。它有一个负载因子是0.75,下一次扩容的时候就是当前数组大小*0.75。扩大容量为原来的2倍。

concurrentmap为什么是线程安全的?

ConcurrentHashMap大部分的逻辑代码和HashMap是一样的,主要通过synchronized和来保证节点在插入扩容的时候是线程安全的。

ConcurrentHashMap的扩容核心逻辑主要是给不同的线程分配不同的数组下标,然后每个线程处理各自下表区间的节点。同时处理节点复用了hashMap的逻辑,通过位运行,可以知道节点扩容后的位置,要么在原位置,要么在原位置+oldlength位置,最后直接赋值即可。

ConcurrentHashMap的原理?

ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry组成的。HashEntry封装的就是每一个键值对。,每一个Segment元素存储的是HashEntry数组 + 链表。Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,Segment本身可以充当锁的角色。ConcurrentHashMap在put的时候需要进行两次hash,第一次需要确定在Segment数组的位置,第二次hash是确定在HashEntry数组中的位置。同样在get的时候也需要经过两次hash。

框架

Springmvc的执行流程

  1. 客户端的所有请求都交给前端控制器DispatcherServlet来处理,它会负责调用系统的其他模块来真正处理用户的请求。
  2. DispatcherServlet收到请求后,将根据请求的信息(包括URL、HTTP协议方法、请求头、请求参数、Cookie等)以及HandlerMapping的配置找到处理该请求的Handler(任何一个对象都可以作为请求的Handler)。
  3. 在这个地方Spring会通过HandlerAdapter对该处理器进行封装。
  4. HandlerAdapter是一个适配器,它用统一的接口对各种Handler中的方法进行调用。
  5. Handler完成对用户请求的处理后,会返回一个ModelAndView对象给DispatcherServlet,ModelAndView顾名思义,包含了数据模型以及相应的视图的信息。
  6. ModelAndView的视图是逻辑视图,DispatcherServlet还要借助ViewResolver完成从逻辑视图到真实视图对象的解析工作。
  7. 当得到真正的视图对象后,DispatcherServlet会利用视图对象对模型数据进行渲染。
  8. 客户端得到响应,可能是一个普通的HTML页面,也可以是XML或JSON字符串,还可以是一张图片或者一个PDF文件。

SpringMVC的常用注解

@Component: 会被spring容器识别,并转为bean。

@Repository: 对Dao实现类进行注解。

@Service: 对业务逻辑层进行注解。

@Controller: 表明这个类是Spring MVC里的Controller,将其声明为Spring的一个Bean,Dispatch Servlet会自动扫描注解了此注解的类,并将Web请求映射到注解了@RequestMapping的方法上。

@RequestMapping: 用来映射Web请求(访问路径和参数)、处理类和方法的。它可以注解在类和方法上。注解在方法上的@RequestMapping路径会继承注解在类上的路径。

@RequestBody: 可以将整个返回结果以某种格式返回,如json或xml格式。

@PathVariable: 用来接收路径参数,如/news/001,可接收001作为参数,此注解放置在参数前。

@RequestParam:用于获取传入参数的值。

@RestController:是一个组合注解,组合了@Controller和@ResponseBody,意味着当只开发一个和页面交互数据的控制的时候,需要使用此注解。

Spring MVC怎么将数据存储到session中?

我一般都是使用Servlet-Api,在处理请求的方法参数列表中,添加一个HTTPSession对象,之后SpringMVC就可以自动注入进来了。在方法体中调用session.setAttribute就可以了。

过滤器和拦截器的区别?

  1. 拦截器是基于java的反射机制的,而过滤器是基于函数回调。
  2. 拦截器不依赖servlet容器,过滤器依赖servlet容器。
  3. 拦截器只能对action请求起作用,而过滤器则可以对几乎所有的请求起作用。
  4. 拦截器可以访问action上下文、值栈里的对象,而过滤器不能访问。
  5. 在action的生命周期中,拦截器可以多次被调用,而过滤器只能在容器初始化时被调用一次。
  6. 拦截器可以获取IOC容器中的各个bean,而过滤器就不行,这点很重要,在拦截器里注入一个service,可以调用业务逻辑。

请简要说明一下IOC和AOP是什么?

依赖注入的三种方式:

  1. 接口注入
  2. Construct注入
  3. Setter注入

控制反转与依赖注入是同一个概念,引入IOC的目的:

  1. 脱开、降低类之间的耦合
  2. 倡导面向接口编程、实施依赖倒换原则
  3. 提高系统可插入、可测试、可修改等特性。

具体做法:

  1. 将bean之间的依赖关系尽可能地转换为关联关系
  2. 将对具体类的关联尽可能地转换为对Java interface的关联,而不是与具体的服务对象相关联
  3. Bean实例具体关联相关Java interface的哪个实现类的实例,在配置信息的元数据中描述
  4. 由IoC组件(或称容器)根据配置信息,实例化具体bean类、将bean之间的依赖关系注入进来。

AOP是面向切面编程,可以说是面向对象编程的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,但是这些都是纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,但是这与核心的业务代码确没有关系。

AOP利用"横切"的技术,把那些与业务无关,但是却为业务模块所共同调用的逻辑部分封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并提高了系统的维护性。

AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如日志还有事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。

Spring拦截器的执行顺序

Springmvc的拦截器实现HandlerInterceptor接口后,会有三个抽象方法需要实现,分别为方法前执行preHandle,方法后postHandle,页面渲染后afterCompletion。

  1. 当两个拦截器都实现放行操作时,顺序为preHandle 1,preHandle 2,postHandle 2,postHandle 1,afterCompletion 2,afterCompletion 1
  2. 当第一个拦截器preHandle返回false,也就是对其进行拦截时,第二个拦截器是完全不执行的,第一个拦截器只执行preHandle部分。
  3. 当第一个拦截器preHandle返回true,第二个拦截器preHandle返回false,顺序为preHandle 1,preHandle 2 ,afterCompletion 1

总结:

preHandle 按拦截器定义顺序调用
postHandler 按拦截器定义逆序调用
afterCompletion 按拦截器定义逆序调用
postHandler 在拦截器链内所有拦截器返成功调用
afterCompletion 只有preHandle返回true才调用

Spring 核心功能

spring 框架中核心组件有三个:Core、Context 和 Beans。其中最核心的组件就是Beans, Spring提供的最核心的功能就是Bean Factory。

Spring 解决了的最核心的问题就是把对象之间的依赖关系转为用配置文件来管理,也就是Spring的依赖注入机制。这个注入机制是在Ioc 容器中进行管理的。

@Autowired 和@Resource区别是什么?

共同点:两者都可以写在字段和setter方法上。两者如果都写在字段上,那么就不需要再写setter方法。

@Autowired注解是按照类型(byType)装配依赖对象。当有且仅有一个匹配的Bean时,Spring将其注入@Autowired标注的变量中。如果我们想使用按照名称(byName)来装配,可以结合@Qualifier注解一起使用。

@Resource默认按照ByName自动注入。@Resource有两个重要的属性:name和type,而Spring将@Resource注解的name属性解析为bean的名字,而type属性则解析为bean的类型。所以,如果使用name属性,则使用byName的自动注入策略,而使用type属性时则使用byType自动注入策略。如果既不制定name也不制定type属性,这时将通过反射机制使用byName自动注入策略。

SpringBootApplication是怎么加载的?

SpringBoot配置多套环境

可以定义多个配置文件,比如开发,测试,上线。 我们可以在SpringBoot中定义多个application.properties。 我一般都用-名字做区别,比如:

application-dev.properties
application-test.properties
application-prod.properties

之后我们需要在默认的配置文件里面声明一下激活哪些配置文件。

spring.profiles.active=test

使用java -jar 方式启动的时候也可以添加参数指定配置文件启动

java -jar mm.jar --spring.profiles.active=dev

SpringBoot的启动方式

  1. 运行带有main方法类。
  2. 通过命令行 java -jar 的方式。
  3. 通过spring-boot-plugin的方式,这种方式需要安装Maven的插件,然后通过命令mvn spring-boot:run启动项目。

SpringBoot的启动方式

  1. 前台启动命令:java -jar XXX.jar
  2. 后台启动命令:java -jar xxx.jar &,后台启动同时也可以制定控制台的输出标准,将日志输出到制定文件
  3. 使用Docker的方式启动SpringBoot应用,需要将jar制作成docker镜像。

MyBatis核心类

Mybatis中dao层和xml配置怎么建立关系的

Mybatis常用标签

#{ } 和${ }的区别?

  • #{}:这种方式是使用的预编译的方式,一个#{}就是一个占位符。相当于jdbc的占位符PrepareStatement。设置值的时候会加上引号。
  • ${}:这种方式是直接拼接的方式,不对数值做预编译。存在sql注入的现象。设置值的时候不会加上引号。

MyBatis的二级缓存

MyBatis一级缓存最大的共享范围就是一个SqlSession内部,那么如果多个SqlSession需要共享缓存,则需要开启二级缓存,开启二级缓存后,会使用CachingExecutor装饰Executor进入一级缓存的查询流程前,先在CachingExecutor进行二级缓存的查询。

当二级缓存开启后,同一个命名空间(namespace) 所有的操作语句,都影响着一个共同的 cache,也就是二级缓存被多个 SqlSession 共享,是一个全局的变量。当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。默认二级缓存不开启,需要在MyBatis的全局配置文件中进行配置。

MySQL

Mysql的存储引擎

MySQL 支持多种类型的数据库引擎,可分别根据各个引擎的功能和特性为不同的数据库处理任务提供各自不同的适应性和灵活性。在 MySQL 中,可以利用 SHOW ENGINES 语句来显示可用的数据库引擎和默认引擎。

MySQL 提供了多个不同的存储引擎,包括处理事务安全表的引擎和处理非事务安全表的引擎。在 MySQL 中,不需要在整个服务器中使用同一种存储引擎,针对具体的要求,可以对每一个表使用不同的存储引擎。

MySQL 5.7 支持的存储引擎有 InnoDB、MyISAM、Memory、Merge、Archive、Federated、CSV、BLACKHOLE 等。

InnoDB和MyISAM的区别

  • InnoDB支持事务,MyISAM不支持
  • InnoDB支持行级锁而MyISAM仅仅支持表锁。但是InnoDB可能出现死锁。
  • InnoDB的关注点在于:并发写、事务、更大资源。而MyISAM的关注点在于:节省资源、消耗少、简单业务
  • InnoDB比MyISAM更安全,但是MyISAM的效率要比InnoDB高
  • 在MySQL5.7的时候,默认就是InnoDb作为默认的存储引擎了

Sql执行计划

使用EXPLAIN关键字可以模拟优化器执行SQL查询语句,从而知道MySQL是如何处理你的SQL语句的。分析查询语句或是表结构的性能瓶颈。

Explain + SQL语句

通过Explain,我们可以获取以下信息:

  • 表的读取顺序
  • 哪些索引可以使用
  • 数据读取操作的操作类型
  • 哪些索引被实际使用
  • 表之间的引用
  • 每张表有多少行被物理查询
EXPLAIN SELECT * FROM USER;

显示的结果一般不会全部去关注,比较关注的有:

id是查询的序列号,包含一组数字,表示查询中执行select子句或操作表的顺序。

  • id相同,执行顺序由上至下。
  • 如果是子查询,id的序号会递增。id越大优先级越高,越先被执行。
  • id如果相同,可以认为是一组,从上往下顺序执行。在所有组中,id值越大,优先级越高,越先执行。

id号每个号码,表示一趟独立的查询。一个sql的查询趟数越少越好。

第二个是type,显示的是访问类型。如果是All就代表是全表扫描。需要进行优化。一般来说,得保证查询至少达到range级别,最好能达到ref。

然后是possible_keys:sql所用到的索引

还有一个就是key,这个就是实际使用的索引。如果为NULL,则没有使用索引。

然后key_len表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度。key_len字段能够帮你检查where条件是否充分的利用上了索引。key_len越长,查询效率越高。

rows列显示MySQL认为它执行查询时必须检查的行数。行数越少,效率越高!

数据库优化

  • 选取最适用的字段属性
  • 使用连接查询代替子查询
  • 为合适的字段创建索引

在上线后数据库要增加几个字段,你们是怎么做的

可以使用alter添加列,这样原有的数据不会改变,新增的字段值是null。 还可以使用Navicat或者SQLyog这些可视化工具修改表的结构,效果和上面的一样

数据库索引的优化建议以及有哪些注意点

  • 使用复合索引的效果会大于使用单个字段索引(但是要注意顺序)
  • 查询条件时要按照索引中的定义顺序进行匹配。如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始并且不跳过索引中的列。
  • 不在索引列上做任何操作(计算、函数、(自动or手动)类型转换),会导致索引失效而转向全表扫描
  • 存储引擎不能使用索引中范围条件右边的列,范围查询的列在定义索引的时候,应该放在最后面。
  • mysql 在使用不等于(!= 或者<>)的时候无法使用索引会导致全表扫描
  • is not null 也无法使用索引,但是is null是可以使用索引的
  • like以通配符开头('%abc...')mysql索引失效会变成全表扫描的操作
  • 字符串不加单引号索引失效(类型转换导致索引失效)

MySQL锁

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

  • 表级锁:开锁小,加锁快,不会出现死锁。锁的粒度大,发生锁冲突的概率最高。并发量最低。
  • 行级锁:开销大,加锁慢,会出现死锁,锁的粒度小,容易发生冲突的概率小,并发度最高

并发事务处理带来的问题

  • 更新丢失:多个事务对同一行数据进行更新,最后提交的更新会覆盖其他事务的更新。
  • 脏读:事务A读取到事务B已经修改但未提交的数据,还在这个数据基础上做了修改。此时,如果事务B回滚了,事务A的数据无效,不符合一致性要求。
  • 不可重读:事务A读取到了事务B已经提交的修改数据,不符合隔离性。
  • 幻读:事务A读取了事务B提交的新增数据,不符合隔离性。

事务的隔离级别

Java后端开发高频面试题_第1张图片

MySQL的事务隔离级别

Redis

Redis雪崩,穿透产生原因及怎么解决

一般的缓存系统,都是按照 key 去缓存查询,如果不存在对应的 value,就应该去后端数据库查。一些恶意的请求会故意查询不存在的 key,请求量很大,就会对后端系统造成很大的压力。 解决穿透的一种办法是对接口做校验,然后也可以对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该 key 对应的数据 insert 了之后清理缓存。

缓存雪崩就是当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,会给后端系统带来很大压力。导致系统崩溃。我们可以做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期。或者我们对不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。

Redis的数据类型

  • String字符串:字符串类型是 Redis 最基础的数据结构,首先键都是字符串类型,而且 其他几种数据结构都是在字符串类型基础上构建的,我们常使用的 set key value 命令就是字符串。常用在缓存、计数、共享Session、限速等。
  • Hash哈希:在Redis中,哈希类型是指键值本身又是一个键值对结构,哈希可以用来存放用户信息,比如实现购物车。
  • List列表(双向链表):列表(list)类型是用来存储多个有序的字符串。可以做简单的消息队列的功能。
  • Set集合:集合(set)类型也是用来保存多个的字符串元素,但和列表类型不一 样的是,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。利用 Set 的交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。
  • Sorted Set有序集合(跳表实现):Sorted Set 多了一个权重参数 Score,集合中的元素能够按 Score 进行排列。可以做排行榜应用,取 TOP N 操作。

Redis的持久化

Redis为了保证效率,数据缓存在了内存中,但是会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件中,以保证数据的持久化。Redis的持久化策略有两种:1. RDB:快照形式是直接把内存中的数据保存到一个dump的文件中,定时保存,保存策略。当Redis需要做持久化时,Redis会fork一个子进程,子进程将数据写到磁盘上一个临时RDB文件中。 当子进程完成写临时文件后,将原来的RDB替换掉。1. AOF:把所有的对Redis的服务器进行修改的命令都存到一个文件里,命令的集合。使用AOF做持久化,每一个写命令都通过write函数追加到appendonly.aof中。aof的默认策略是每秒钟fsync一次,在这种配置下,就算发生故障停机,也最多丢失一秒钟的数据。 缺点是对于相同的数据集来说,AOF的文件体积通常要大于RDB文件的体积。根据所使用的fsync策略,AOF的速度可能会慢于RDB。Redis默认是快照RDB的持久化方式。对于主从同步来说,主从刚刚连接的时候,进行全量同步(RDB);全同步结束后,进行增量同步(AOF)。

redis是单线程,但为什么快

  1. 纯内存操作
  2. 单线程操作,避免了频繁的上下文切换
  3. 合理高效的数据结构
  4. 采用了非阻塞I/O多路复用机制(有一个文件描述符同时监听多个文件描述符是否有数据到来)

redis存的数据过期了,数据会立即删除吗

不会,其实有三种不同的删除策略:

  1. 立即删除。在设置键的过期时间时,创建一个定时器,当过期时间达到时,立即执行删除操作。
  2. 惰性删除。key过期的时候不删除,每次从数据库获取key的时候去检查是否过期,若过期,则删除,返回null。
  3. 定时删除。每隔一段时间,对全部的键进行检查,删除里面的过期键。

Redis 和 Mysql 数据库数据如何保持一致性

先更新数据库,再删缓存。数据库的读操作的速度远快于写操作的,所以脏数据很难出现。可以对异步延时删除策略,保证读请求完成以后,再进行删除操作。

Redis的应用场景

  • 缓存
  • 共享Session
  • 消息队列系统
  • 分布式锁

redis里的hash类型怎么模糊查询

可以使用Java连接Redis,获得指定hash的所有值,然后做正则验证。

Redis常用命令

key
    keys * 获取所有的key
    select 0 选择第一个库
    move myString 1 将当前的数据库key移动到某个数据库,目标库有,则不能移动
    flush db      清除指定库
    randomkey     随机key
    type key      类型
    
    set key1 value1 设置key
    get key1    获取key
    mset key1 value1 key2 value2 key3 value3
    mget key1 key2 key3
    del key1   删除key
    exists key      判断是否存在key
    expire key 10   10过期
    pexpire key 1000 毫秒
    persist key     删除过期时间

string
    set name cxx
    get name
    getrange name 0 -1        字符串分段
    getset name new_cxx       设置值,返回旧值
    mset key1 key2            批量设置
    mget key1 key2            批量获取
    setnx key value           不存在就插入(not exists)
    setex key time value      过期时间(expire)
    setrange key index value  从index开始替换value
    incr age        递增
    incrby age 10   递增
    decr age        递减
    decrby age 10   递减
    incrbyfloat     增减浮点数
    append          追加
    strlen          长度
    getbit/setbit/bitcount/bitop    位操作
    
hash
    hset myhash name cxx
    hget myhash name
    hmset myhash name cxx age 25 note "i am notes"
    hmget myhash name age note   
    hgetall myhash               获取所有的
    hexists myhash name          是否存在
    hsetnx myhash score 100      设置不存在的
    hincrby myhash id 1          递增
    hdel myhash name             删除
    hkeys myhash                 只取key
    hvals myhash                 只取value
    hlen myhash                  长度

list
    lpush mylist a b c  左插入
    rpush mylist x y z  右插入
    lrange mylist 0 -1  数据集合
    lpop mylist  弹出元素
    rpop mylist  弹出元素
    llen mylist  长度
    lrem mylist count value  删除
    lindex mylist 2          指定索引的值
    lset mylist 2 n          索引设值
    ltrim mylist 0 4         删除key
    linsert mylist before a  插入
    linsert mylist after a   插入
    rpoplpush list list2     转移列表的数据
    
set
    sadd myset redis 
    smembers myset       数据集合
    srem myset set1         删除
    sismember myset set1 判断元素是否在集合中
    scard key_name       个数
    sdiff | sinter | sunion 操作:集合间运算:差集 | 交集 | 并集
    srandmember          随机获取集合中的元素
    spop                 从集合中弹出一个元素
    
zset
    zadd zset 1 one
    zadd zset 2 two
    zadd zset 3 three
    zincrby zset 1 one              增长分数
    zscore zset two                 获取分数
    zrange zset 0 -1 withscores     范围值
    zrangebyscore zset 10 25 withscores 指定范围的值
    zrangebyscore zset 10 25 withscores limit 1 2 分页
    Zrevrangebyscore zset 10 25 withscores  指定范围的值
    zcard zset  元素数量
    Zcount zset 获得指定分数范围内的元素个数
    Zrem zset one two        删除一个或多个元素
    Zremrangebyrank zset 0 1  按照排名范围删除元素
    Zremrangebyscore zset 0 1 按照分数范围删除元素
    Zrank zset 0 -1    分数最小的元素排名为0
    Zrevrank zset 0 -1  分数最大的元素排名为0
    Zinterstore
    zunionstore rank:last_week 7 rank:20150323 rank:20150324 rank:20150325  weights 1 1 1 1 1 1 1
    
    
排序:
    sort mylist  排序
    sort mylist alpha desc limit 0 2 字母排序
    sort list by it:* desc           by命令
    sort list by it:* desc get it:*  get参数
    sort list by it:* desc get it:* store sorc:result  sort命令之store参数:表示把sort查询的结果集保存起来

RabbitMq

怎么防止重复消费

可能因为各种原因,导致了生产端发送了多条一样的消息给消费端,但是,消费端也只能消费一条,不会多消费。可以使用唯一ID + 指纹码机制防止消息被重复消费。

指纹码(就是时间戳 + 业务的一些规则, 来保证id + 指纹码在同一时刻是唯一的,不会出现重复)。

  1. 唯一ID + 指纹码机制,利用数据库主键去重
  2. select count(1) from t_order where id = 唯一ID + 指纹码
  3. 如果不存在,则正常消费,消费完毕后将【唯一ID + 指纹码】 写入数据库
  4. 如果存在,则证明消息被消费过,直接丢弃。

Rabbitmq怎么防止消息丢失

将信道设置成confirm模式(发送方确认模式),则所有在信道上发布的消息都会被指派一个唯一的ID。 一旦消息被投递到目的队列后,或者消息被写入磁盘后(可持久化的消息),信道会发送一个确认给生产者(包含消息唯一ID)。

为什么选择使用MQ来实现同步

通过使用消息队列,我们可以异步处理请求,从而缓解系统的压力。同样可以达到解耦的效果。

ElasticSearch

高亮你们是怎么做的

SpringBoot整合ElasticSearch有一个searchSourceBuilder,通过链式调用一个highlighter方法,传入一个HighlightBuilder对象并设置好查询的列和高亮的标签。

之后调用RestHighLevelClient对象的Search方法之后返回一个SearchResponse对象,之后可以调用response.getHits().getHits();获得击中的结果数组,数组中每一个对象除了包含原始内容还包含了一个高亮结果集,是一个Map集合。

SpringBoot怎么集成ElasticSearch

首先需要导入spring-boot-starter-data-elasticsearch,在Spring官网的data项目里面有详细的文档介绍,官方强烈建议使用 High Level REST Client来操作ES。之后需要添加一个配置类,在官方文档有介绍。之后我们就可以通过Spring容器来管理获取HighLevelRESTClient对象了。

其他

Maven的声明周期?

Maven有三套生命周期,分别是clean、default、site,每个生命周期都包含了一些阶段(phase)。三套生命周期相互独立,但各个生命周期中的phase却是有顺序的,且后面的phase依赖于前面的phase。执行某个phase时,其前面的phase会依顺序执行,但不会触发另外两套生命周期中的任何phase。

clean的生命周期:

pre-clean:执行清理前的工作;
clean:清理上一次构建生成的所有文件;
post-clean:执行清理后的工作

default的生命周期:default生命周期是最核心的,它包含了构建项目时真正需要执行的所有步骤。

validate
initialize
generate-sources
process-sources
generate-resources
process-resources    :复制和处理资源文件到target目录,准备打包;
compile    :编译项目的源代码;
process-classes
generate-test-sources
process-test-sources
generate-test-resources
process-test-resources
test-compile    :编译测试源代码;
process-test-classes
test    :运行测试代码;
prepare-package
package    :打包成jar或者war或者其他格式的分发包;
pre-integration-test
integration-test
post-integration-test
verify
install    :将打好的包安装到本地仓库,供其他项目使用;
deploy    :将打好的包安装到远程仓库,供其他项目使用;

site的生命周期:

pre-site
site    :生成项目的站点文档;
post-site
site-deploy    :发布生成的站点文档

cookie和session区别

  1. cookie数据存放在客户的浏览器上,session数据放在服务器上。
  2. cookie不是很安全,别人可以分析存放在本地的cookie并进行cookie欺骗,考虑到安全应当使用session。
  3. session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,考虑到减轻服务器性能方面,应当使用cookie。
  4. 单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。
  5. 可以考虑将登陆信息等重要信息存放为session,其他信息如果需要保留,可以放在cookie中。

TCP三次握手,4次挥手的过程描述一下

握手过程:

  1. 主机A向主机B发送请求连接数据报,其中包括A的序列号seq=x,请求连接的标志位SYN=1
  2. 主机B收到请求之后返回确认连接数据报,其中包括B的序列号seq=y,请求连接的标志位SYN=1,ACK=1还有一个确认号,ack=x+1
  3. 主机A收到了B的确认报文后再次做出确认,再发送一个数据报,其中包括:ACK=1,seq=x+1,ack=y+1

至于为什么要发送第三条是因为在发送第一条的时候,可能因为网络原因导致数据报滞留,那么超过一定时间主机A会再次发送请求连接的数据报文。之后主机B返回确认连接报文,如果主机A收到确认报文之后不发送第三条报文告诉主机B自己已经收到了,那么B其实是不知道的,这时候可能A第一次发送的原本滞留的报文突然正常了,B就再次收到了请求连接的报文,但是实际上A已经连接了。

挥手过程:

  1. 客户端向服务器发送一个请求断开连接的数据报,终止位FIN=1,序列号seq=u
  2. 服务器收到请求后返回ACK=1,seq=v,ack=u+1。之后客户端通往服务器的单向连接就断开了。
  3. 之后服务器也需要和客户端断开连接,也是发送了一个FIN
  4. 客户端收到FIN后返回ACK,并将确认号设置为收到的序号+1

其实在客户端断开和服务器的单向连接之后,服务器仍然可以往客户端发送数据,需要处理一下事情。

客户端需要最后等一段时间才能进入关闭状态是因为:客户端无法保证最后发送的ACK报文会一定被对方收到,所以有时候需要重发可能丢失的ACK报文。

 更多Java进阶学习资料、2022大厂面试真题,关注我,主页自取

Java后端开发高频面试题_第2张图片

 

你可能感兴趣的:(java,面试,经验分享)