面试题经典版

面试题

一、JAVA基础

1. ==和 equals 的区别?
  • ==是判断两个变量或实例是不是指向同一个内存空间,equals是判断两个变量或实例所指向的内存空间的值是不是相同
  • ==是指对内存地址进行比较 , equals()是对字符串的内容进行比较
  • ==指引用是否相同, equals()指的是值是否相同
2. 说说抽象类和接口

相同点:

① 抽象类和接口都不能被实例化

② 抽象类和接口都可以定义抽象方法,子类/实现类必须覆写这些抽象方法

不同点:

① 抽象类有构造方法,接口没有构造方法

② 抽象类可以包含普通方法,接口中只能是public abstract修饰抽象方法(Java8之后可以)

③ 抽象类只能单继承,接口可以多继承

④ 抽象类可以定义各种类型的成员变量,接口中只能是public static final修饰的静态常量

抽象类和接口的使用场景

1.抽象类的使用场景

既想约束子类具有共同的行为(但不再乎其如何实现),又想拥有缺省的方法,又能拥有实例变量

如:模板方法设计模式,模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中某些步骤的具体实现。

2.接口的应用场景

① 约束多个实现类具有统一的行为,但是不在乎每个实现类如何具体实现

② 作为能够实现特定功能的标识存在,也可以是什么接口方法都没有的纯粹标识。

③ 实现类需要具备很多不同的功能,但各个功能之间可能没有任何联系。

④ 使用接口的引用调用具体实现类中实现的方法(多态)

3.重写和重载的区别

重载(Overload)是让类以统一的方式处理不同类型数据的一种手段,实质表现就是多个具有不同的参数个数或者类型的同名函数(返回值类型可随意,不能以返回类型作为重载函数的区分标准)同时存在于同一个类中,是一个类中多态性的一种表现(调用方法时通过传递不同参数个数和参数类型来决定具体使用哪个方法的多态性)。

重写(Override)是父类与子类之间的多态性,实质是对父类的函数进行重新定义,如果在子类中定义某方法与其父类有相同的名称和参数则该方法被重写,不过子类函数的访问修饰权限不能小于父类的;若子类中的方法与父类中的某一方法具有相同的方法名、返回类型和参数表,则新方法将覆盖原有的方法,如需父类中原有的方法则可使用 super 关键字。

4.public,protected,private的作用范围

public公共的。权限最大,外界可以引用

private 私有的。只能被本类自己调用,类外都不可以调用,子类也不可以

protected 受保护的。只能被子类(子类可以在其他包下面)或者同一个包下的其他类引用。其他的都不可以

5.常见的异常类有哪些?

java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类 Exception(异常)和 Error(错误)。Exception 能被程序本身处理(try-catch), Error 是无法处理的(只能尽量避免)。

Exception 和 Error 二者都是 Java 异常处理的重要子类,各自都包含大量子类。

  • Exception :程序本身可以处理的异常,可以通过 catch 来进行捕获。Exception 又可以分为 受检查异常(必须处理) 和 不受检查异常(可以不处理)。

  • Error :Error 属于程序无法处理的错误 ,我们没办法通过 catch 来进行捕获 。例如,Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。

6. 什么是反射?有什么作用?

Java 反射,就是在运行状态中

  • 获取任意类的名称、package 信息、所有属性、方法、注解、类型、类加载器、modifiers(public、static)、父类、现实接口等
  • 获取任意对象的属性,并且能改变对象的属性
  • 调用任意对象的方法
  • 判断任意一个对象所属的类
  • 实例化任意一个类的对象

Java 的动态就体现在反射。通过反射我们可以实现动态装配,降低代码的耦合度;动态代理等。反射的过度使用会严重消耗系统资源。

JDK 中 java.lang.Class 类,就是为了实现反射提供的核心类之一。

一个 jvm 中一种 Class 只会被加载一次。

7.你知道java8的新特性吗,请简单介绍一下?
  • Lambda 表达式 − Lambda允许把函数作为一个方法的参数(函数作为参数传递进方法中。
  • 方法引用− 方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器。与lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码。
  • 默认方法− 默认方法就是一个在接口里面有了一个实现的方法。
  • 新工具− 新的编译工具,如:Nashorn引擎 jjs、 类依赖分析器jdeps。
  • Stream API −新添加的Stream API(java.util.stream) 把真正的函数式编程风格引入到Java中。
  • Date Time API − 加强对日期与时间的处理。
  • Optional 类 − Optional 类已经成为 Java 8 类库的一部分,用来解决空指针异常。
  • Nashorn, JavaScript 引擎 − Java 8提供了一个新的Nashorn javascript引擎,它允许我们在JVM上运行特定的javascript应用。
8.String,StringBuffer和StringBuilder。

相同点:

  • 都可以储存和操作字符串
  • 都使用 final 修饰,不能被继承
  • 提供的 API 相似

区别:

  • String 是只读字符串,String 对象内容是不能被改变的
  • StringBuffer 和 StringBuilder 的字符串对象可以对字符串内容进行修改,在修改后的内存地址不会发生改变
  • StringBuilder 线程不安全;StringBuffer 线程安全
    方法体内没有对字符串的并发操作,且存在大量字符串拼接操作,建议使用 StringBuilder,效率较高。

二、JAVA容器

1. 集合了解吧,说说集合有几大类,分别介绍一下

Java 集合, 也叫作容器,主要是由两大接口派生而来:一个是 Collecton接口,主要用于存放单一元素;另一个是 Map 接口,主要用于存放键值对。对于Collection 接口,下面又有三个主要的子接口:List、Set 和 Queue。

  • List(对付顺序的好帮手): 存储的元素是有序的、可重复的。
  • Set(注重独一无二的性质): 存储的元素是无序的、不可重复的。
  • Queue(实现排队功能的叫号机): 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。
  • Map(用 key 来搜索的专家): 使用键值对(key-value)存储,类似于数学上的函数 y=f(x),“x” 代表 key,“y” 代表 value,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。
2. hashMap底层实现了解过吗?具体讲讲
  • HashMap 基于 Hash 算法实现,通过 put(key,value) 存储,get(key) 来获取 value
  • 当传入 key 时,HashMap 会根据 key,调用 hash(Object key) 方法,计算出 hash 值,根据 hash 值将 value 保存在 Node 对象里,Node 对象保存在数组里
  • 当计算出的 hash 值相同时,称之为 hash 冲突,HashMap 的做法是用链表和红黑树存储相同 hash 值的 value
  • 当 hash 冲突的个数:小于等于 8 使用链表;大于 8 且 tab length 大于等于 64 时,使用红黑树解决链表查询慢的问题

ps:

  • 上述是 JDK 1.8 HashMap 的实现原理,并不是每个版本都相同,比如 JDK 1.7 的 HashMap 是基于数组 + 链表实现,所以 hash 冲突时链表的查询效率低
  • hash(Object key) 方法的具体算法是 (h = key.hashCode()) ^ (h >>> 16),经过这样的运算,让计算的 hash 值分布更均匀
3. HashMap 和 hashTable的区别?

JDK 1.8 中 HashMapHashtable 主要区别如下:

  • 线程安全性不同。HashMap 线程不安全;Hashtable 中的方法是 synchronized 的。
  • key、value 是否允许 null。HashMap 的 key 和 value 都是可以是 null,key 只允许一个 null;Hashtable 的 key 和 value 都不可为 null。
  • 迭代器不同。HashMap 的 Iterator 是 fail-fast 迭代器;Hashtable 还使用了 enumerator 迭代器。
  • hash的计算方式不同。HashMap 计算了 hash值;Hashtable 使用了 key 的 hashCode方法。
  • 默认初始大小和扩容方式不同。HashMap 默认初始大小 16,容量必须是 2 的整数次幂,扩容时将容量变为原来的2倍;Hashtable 默认初始大小 11,扩容时将容量变为原来的 2 倍加 1。
  • 是否有 contains 方法。HashMap 没有 contains 方法;Hashtable 包含 contains 方法,类似于 containsValue。
  • 父类不同。HashMap 继承自 AbstractMap;Hashtable 继承自 Dictionary。
4. 说说ConcurrentHashMap的底层实现

ConcurrentHashMap1.7 实现原理
ConcurrentHashMap 采用分段锁设计、将一个大的 HashMap 集合拆分成 n 多个不同的小的 HashTable(Segment),默认的情况下是分成 16 个不同的 Segment,每个Segment 中都有自己独立的 HashEntry<K,V>[] table
数组+Segments 分段锁+HashEntry 链表实现
使用 Lock 锁+CAS 乐观锁+UNSAFE 类
PUT 方法流程

  • 第一次需要计算出:key 出存放在那个 Segment 对象中
  • 还需要计算 key 存放在 Segment 对象中具体 index 位置。

ConcurrentHashMap1.8 实现原理
Put 原理 锁的粒度非常小,对每个数组 index 位置上锁 对 1.7ConcurrentHashMap 实现优化

  1. 取消 segment 分段设计,使用 synchronized 锁
  2. synchronized 在 JDK1.6 开始做了优化 默认实现锁的升级过程

JDK 1.7 到 JDK 1.8 中的 ConcurrentHashMap 最大的改动:

链表上的 Node 超过 8 个改为红黑树,查询复杂度 O(logn)
ReentrantLock 显示锁改为 synchronized,说明 JDK 1.8 中 synchronized 锁性能赶上或超过 ReentrantLock

5. Jdk中map的实现都有什么:

HashMap、TreeMap、Hashtable、LinkedHashMap。

6.JUC包下面的集合类了解哪些

在这里插入图片描述

三、多线程

1. java实现多线程的方式有几种?

有4种方式可以用来创建线程:

  • 继承Thread类
class MyThread extends Thread{
	public void run(){
		System.out.println("线程运行");
	}
}
public class Test{
	public static void main(String[] args){
		MyThread thread=new MyThread();
		thread.start();//开启线程
	}
}
  • 实现Runnable接口
class MyThread implements Runnable
{
	public void run(){
		System.out.println("线程运行");
	}
}
public class Test{
	public static void main(String[] args){
		MyThread thread=new MyThread();
		Thread t=new Thread(thread);
		t.start();//开启线程
	}
}
  • 还有一种方式是实现Callable接口
    实现Runnable接口这种方式更受欢迎,因为这不需要继承Thread类。在应用设计中已经继承了别的对象的情况下,这需要多继承(而Java不支持多继承),只能实现接口。同时,线程池也是非常高效的,很容易实现和使用。
import java.util.concurrent.*;
public class CallableAndFuture{
	//创建线程
	public static class CallableTest implements Callable<String>{
		public String call() throws Exception{
			return "Hello World";
		}
	}
	public static void main(String[] args){
		ExecutorService threadPool=Executors.newSingleThreadExecutor();
		//启动线程
		Future<String> future=threadPool.submit(new CallableTest());
		try{
			System.out.println("等待线程执行完成");
			System.out.println(future.get());//等待线程结束,并获取返回结果
		}
		catch(Exception e){
			e.printStackTrace();
		}
	}
}
2. Runnable和Callable有什么区别?

主要区别

  • Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,支持泛型
  • Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息
3. 线程和进程的区别
  • 进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即为一个进程的创建、运行以及消亡的过程。

  • 线程是比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程,多个线程共享进程的堆和方法区内存资源,每个线程都有自己的程序计数器、虚拟机栈和本地方法栈。由于线程共享进程的内存,因此系统产生一个线程或者在多个线程之间切换工作时的负担比进程小得多,线程也称为轻量级进程。

  • 进程和线程最大的区别是,各进程是独立的,而各线程则不一定独立,因为同一进程中的多个线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护,进程则相反

4.进程之间的通信方式

1、管道,匿名管道,命名管道
2、信号
3、信号量
4、消息队列
5、共享内存
6、socket

5. 线程间通讯方式

1:同步(synchronized)

2:共享变量(volatile)

2:wait/notify()机制

3:管道通信就是使用java.io.PipedInputStream 和 java.io.PipedOutputStream进行通信

6. java的线程大概有几种状态?

线程在运行的生命周期中的任何时刻只能是5 种不同状态的其中一种。

  • 初始状态(NEW):线程已经构建,尚未启动。
  • 运行状态(RUNNABLE):包括就绪(READY)和运行中(RUNNING)两种状态,统称为运行状态。
  • 阻塞状态(BLOCKED):线程被锁阻塞。
  • 等待状态(WAITING):线程需要等待其他线程做出特定动作(通知或中断)。
  • 终止状态(TERMINATED):当前线程已经执行完毕。
7. sleep 和 wait方法的区别?
  • sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,把执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。

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

8. 死锁的四个条件?
  • 互斥条件:一个锁一次只能由一个进程占有
  • 不可剥夺条件:一个进程占有的资源在使用完之前不可以被其他进程剥夺,只能由该进程释放之后才能被其他进程获取。
  • 请求和保持条件:一个进程在申请资源的同时保持已经占有的资源不释放。
  • 循环等待条件:同时需要A、B两个资源的进程分别占有了A和B,形成了两个进程都阻塞并等待对方释放资源的状态。
9.线程安全是什么?如何保证线程安全?

当多个线程访问某个方法时,不管你通过怎样的调用方式或者说这些线程如何交替的执行,我们在主程序中不需要去做任何的同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类是线程安全的。

synchronized关键字,就是用来控制线程同步的,保证我们的线程在多线程环境下,不被多个线程同时执行,确保我们数据的完整性,使用方法一般是加在方法上。

就是我们在需要的时候去手动的获取锁和释放锁,甚至我们还可以中断获取以及超时获取的同步特性,但是从使用上说Lock明显没有synchronized使用起来方便快捷。

10. ThreadLocal有什么作用?有哪些使用场景?

ThreadLocal 是线程本地存储,在每个线程都创建了一个ThreadLocalMap对象,每个线程可以访问自己内部ThreadLocalMal对象内的value. 通过这种方式,避免资源在多线程见共享。

如果使用ThreadLocal管理变量,则每一个使用该变量的线程都会获得该变量的副本
副本之间相互独立,这样每一个线程都可以随意更改自己的变量副本,而不会对其他线程产生影响。

经典的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的 Connection; 还有 Session 管理 等问题。

11. synchronized 和 volatile 的区别是什么?

作用:

  • synchronized 表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程。
  • volatile 表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序。

区别:

  • synchronized 可以作用于变量、方法、对象;volatile 只能作用于变量。
  • synchronized 可以保证线程间的有序性(个人猜测是无法保证线程内的有序性,即线程内的代码可能被 CPU 指令重排序)、原子性和可见性;volatile 只保证了可见性和有序性,无法保证原子性。
  • synchronized 线程阻塞,volatile 线程不阻塞。
  • volatile 本质是告诉 jvm 当前变量在寄存器中的值是不安全的需要从内存中读取;sychronized 则是锁定当前变量,只有当前线程可以访问到该变量其他线程被阻塞。
  • volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。
12什么是线程池?

Java线程池主要用于管理线程组及其运行状态,以便Java虚拟机更好地利用CPU资源。Java线程池的工作原理为:JVM先根据用户的参数创建一定数量的可运行的线程任务,并将其放入队列中,在线程创建后启动这些任务,如果正在运行的线程数量超过了最大线程数量(用户设置的线程池大小),则超出数量的线程排队等候,在有任务执行完毕后,线程池调度器会发现有可用的线程,进而再次从队列中取出任务并执行。
线程池的主要作用是线程复用、线程资源管理、控制操作系统的最大并发数,以保证系统高效(通过线程资源复用实现)且安全( 通过控制最大线程并发数实现)地运行。

13.线程池的参数

corePoolSize(核心线程数)
maxPoolSize(最大线程数)
keepAliveTime(线程存活时间)
unit(时间单位)
workQueue(任务队列)
threadFactory (线程工厂)
handler(任务拒绝策略)

14.线程池的拒绝策略

若线程池中的核心线程数被用完且阻塞队列已排满,则此时线程池的线程资源已耗尽,线程池将没有足够的线程资源执行新的任务。为了保证操作系统的安全,线程池将通过拒绝策略处理新添加的线程任务。JDK内置的拒绝策略有AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy 、DiscardPolicy 这4种,默认的拒绝策略在ThreadPoolExecutor中作为内部类提供。在默认的拒绝策略不能满足应用的需求时,可以自定义拒绝策略。
AbortPolicy
线程池的默认拒绝策略为AbortPolicy,即丢弃任务并抛出RejectedExecutionException异常
CallerRunsPolicy
由调用线程处理该任务,如果任务被拒绝了,则由调用线程(提交任务的线程)直接执行此任务,
DiscardPolicy
丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。
DiscardOldestPolicy
丢弃队列最前面的任务,然后重新提交被拒绝的任务。

15.Java中的锁了解哪些

公平锁
    是指多个线程申请锁的顺序来获取锁,类似排队,先来后到。
    在并发环境中,每个线程在获取锁时会查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。

非公平锁
    是指多个线程获取锁的顺序不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁;在高并发的情况下,有可能会造成优先级反转或者接现象
    非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式
    
可重入锁(递归锁)
指的是统一线程外层函数获取锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。也就是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块。
ReentrantLock/Synchronized 就是典型的可重入锁,可重入锁最大的作用是避免死锁

自旋锁
    自旋锁(spinlock):是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少上下文切换的消耗,缺点就是循环会消耗CPU

独占锁(写锁)/共享锁(读锁)/互斥锁
独占锁:指该锁一次只能被一个线程所持有。对 ReentrantLock/Synchronized 而言都是独占锁
共享锁:指该锁可被多个线程所持有。对 ReentrantReadWriteLock 其读锁是共享锁,其写锁是独占锁。读锁的共享锁可保证并发读是非常高效的,读写、写读、写写的过程是互斥的。

16. 介绍一下AQS和CAS

AQS 是一个抽象的队列同步器通过维护一个共享资源状态(Volatile Int State) 和一个先进先出(FIFO)的线程等待队列来实现一个多线程访问共享资源的同步框架。

AQS 的原理

AQS为每个共享资源都设置一个共享 资源锁,线程在需要访问共享资源时首先需要获取共享资源锁,如果获取到了共享资源锁,便可以在当前线程中使用该共享资源,如果获取不到,则将该线程放入线程等待队列,等待下一次资源调度,具体的流程如图3-14所示。许多同步类的实现都依赖于AQS,例如常用的ReentrantLock 、Semaphore 和CountDownLatch。

CAS的概念:比较并交换

CAS ( Compare And Swap)指比较并交换。CAS算法CAS(V,E,N)包含3个参数,V表示要更新的变量,E表示预期的值,N表示新值。在且仅在V值等于E值时,才会将V值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,当前线程什么都不做。最后,CAS返回当前V的真实值。

CAS 的特性:乐观锁

CAS操作采用了乐观锁的思想,总是认为自己可以成功完成操作。在有多个线程同时使用CAS操作-一个变量时,只有一个会胜出并成功更新,其余均会失败。失败的线程不会被挂起,仅被告知失败,并且允许再次尝试,当然,也允许失败的线程放弃操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。

17.什么是ABA问题

对CAS算法的实现有一个重要的前提:需要取出内存中某时刻的数据,然后在下一时刻进行比较、替换,在这个时间差内可能数据已经发生了变化,导致产生ABA问题。ABA问题指第1个线程从内存的V位置取出A,这时第2个线程也从内存中取出A,并将v位置的数据首先修改为B,接着又将v位置的数据修改为A,这时第1个线程在进行CAS操作时会发现在内存中仍然是A,然后第1个线程操作成功。尽管从第1个线程的角度来说,CAS操作是成功的,但在该过程中其实V位置的数据发生了变化,只是第1个线程没有感知到罢了,这在某些应用场景下可能出现过程数据不–致的问题。

部分乐观锁是通过版本号( version)来解决ABA问题的,具体的操作是乐观锁每次在执行数据的修改操作时都会带上一个版本号,在预期的版本号和数据的版本号一致时就可以执行修改操作,并对版本号执行加操作,否则执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题,因为版本号只会增加,不会减少。

四、网络编程

1. TCP和UDP的区别?

TCP,Transmission Control Protocol 的缩写,即传输控制协议。

  • 面向连接,即必须在双方建立可靠连接之后,才会收发数据
  • 信息包头 20 个字节
  • 建立可靠连接需要经过3次握手
  • 断开连接需要经过4次挥手
  • 需要维护连接状态
  • 报文头里面的确认序号、累计确认及超时重传机制能保证不丢包、不重复、按序到达
  • 拥有流量控制及拥塞控制的机制

UDP,User Data Protocol 的缩写,即用户数据报协议。

  • 不建立可靠连接,无需维护连接状态
  • 信息包头 8 个字节
  • 接收端,UDP 把消息段放在队列中,应用程序从队列读消息
  • 不受拥挤控制算法的调节
  • 传送数据的速度受应用软件生成数据的速率、传输带宽、源端和终端主机性能的限制
  • 面向数据报,不保证接收端一定能收到
2. 输入一次url过程,用到哪些协议?

  1. DNS 解析:浏览器查询 DNS,获取域名对应的 IP 地址:具体过程包括浏览器搜索自身的 DNS 缓存、搜索操作系统的 DNS 缓存、读取本地的 Host 文件和向本地 DNS 服务器进行查询等。对于向本地 DNS 服务器进行查询,如果要查询的域名包含在本地配置区域资源中,则返回解析结果给客户机,完成域名解析(此解析具有权威性);如果要查询的域名不由本地 DNS 服务器区域解析,但该服务器已缓存了此网址映射关系,则调用这个 IP 地址映射,完成域名解析(此解析不具有权威性)。如果本地域名服务器并未缓存该网址映射关系,那么将根据其设置发起递归查询或者迭代查询;

  2. TCP 连接:浏览器获得域名对应的 IP 地址以后,浏览器向服务器请求建立链接,发起三次握手;

  3. 发送 HTTP 请求:TCP 连接建立起来后,浏览器向服务器发送 HTTP 请求;

  4. 服务器处理请求并返回 HTTP 报文:服务器接收到这个请求,并根据路径参数映射到特定的请求处理器进行处理,并将处理结果及相应的视图返回给浏览器;

  5. 浏览器解析渲染页面:浏览器解析并渲染视图,若遇到对 js 文件、css 文件及图片等静态资源的引用,则重复上述步骤并向服务器请求这些资源;浏览器根据其请求到的资源、数据渲染页面,最终向用户呈现一个完整的页面。

  6. 连接结束。

3. 说一说HTTP和HTTPS的区别。
  • 安全性上,HTTPS是安全超文本协议,在HTTP基础上有更强的安全性。简单来说,HTTPS是使用TLS/SSL加密的HTTP协议
  • 申请证书上,HTTPS需要使用ca申请证书
  • 传输协议上, HTTP是超文本传输协议,明文传输;HTTPS是具有安全性的 SSL 加密传输协议
  • 连接方式与端口上,http的连接简单,是无状态的,端口是 80; https 在http的基础上使用了ssl协议进行加密传输,端口是 443
4. TCP的三次握手,四次挥手

客户端–发送带有 SYN 标志的数据包–一次握手–服务端
服务端–发送带有 SYN/ACK 标志的数据包–二次握手–客户端
客户端–发送带有带有 ACK 标志的数据包–三次握手–服务端

客户端-发送一个 FIN,用来关闭客户端到服务器的数据传送
服务器-收到这个 FIN,它发回一 个 ACK,确认序号为收到的序号加 1 。和 SYN 一样,一个 FIN 将占用一个序号
服务器-关闭与客户端的连接,发送一个 FIN 给客户端
客户端-发回 ACK 报文确认,并将确认序号设置为收到序号加 1

5. 什么是 Cookie 和 Session ?

什么是 Cookie

HTTP Cookie(也叫 Web Cookie或浏览器 Cookie)是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器,如保持用户的登录状态。Cookie 使基于无状态的 HTTP 协议记录稳定的状态信息成为了可能。

Cookie 主要用于以下三个方面:

  • 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
  • 个性化设置(如用户自定义设置、主题等)
  • 浏览器行为跟踪(如跟踪分析用户行为等)

什么是 Session

Session 代表着服务器和客户端一次会话的过程。Session 对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当客户端关闭会话,或者 Session 超时失效时会话结束。

6. Cookie和Session的区别?
  • 作用范围不同,Cookie 保存在客户端(浏览器),Session 保存在服务器端。
  • 存取方式的不同,Cookie 只能保存 ASCII,Session 可以存任意数据类型,一般情况下我们可以在 Session 中保持一些常用变量信息,比如说 UserId 等。
  • 有效期不同,Cookie 可设置为长时间保持,比如我们经常使用的默认登录功能,-Session 一般失效时间较短,客户端关闭或者 Session 超时都会失效。
  • 隐私策略不同,Cookie 存储在客户端,比较容易遭到不法获取,早期有人将用户的登录名和密码存储在 Cookie 中导致信息被窃取;Session 存储在服务端,安全性相对 Cookie 要好一些。
  • 存储大小不同, 单个 Cookie 保存的数据不能超过 4K,Session 可存储数据远高于 Cookie。
7. SQL注入是什么,如何避免SQL注入?

SQL 注入就是在用户输入的字符串中加入 SQL 语句,如果在设计不良的程序中忽略了检查,那么这些注入进去的 SQL 语句就会被数据库服务器误认为是正常的 SQL 语句而运行,攻击者就可以执行计划外的命令或访问未被授权的数据。

SQL注入的原理主要有以下 4 点

  • 恶意拼接查询
  • 利用注释执行非法命令
  • 传入非法参数
  • 添加额外条件

避免SQL注入的一些方法:

  • 限制数据库权限,给用户提供仅仅能够满足其工作的最低权限。
  • 对进入数据库的特殊字符(’”\尖括号&*;等)转义处理。
  • 提供参数化查询接口,不要直接使用原生SQL。
8.forward和redirct的区别

1.forward是什么
forward是服务器请求资源,服务器直接访问目标地址的URL,把那个URL的响应内容读取过来,然后把这些内容在发给浏览器,浏览器根本不知道服务器发送的内容从哪里来,所以它的地址栏还是原来的地址。

2.redirect是什么
redirect是服务器根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址,所以地址栏显示的是新的URL。所以redirct等于客户端向服务器发出两次request,同时也接收两次response。

3.应用的差别
forward:一般用于用户登录的时候,根据角色转发到响应的模块。

redirect:一般用于用户注销登录时返回和跳转到其他的网站等。

9.什么是Servlet?

Servlet(Servlet Applet),全称Java Servlert .是用Java编写的服务器端程序。其主要功能在与交互式的浏览和修改数据,生成动态Web内容。狭义的servlet是指Java语言实现的一个接口,广义的Servlet是指任何实现了这个Servlet的类,一般情况下,人们将Servlet理解为后者。比如HttpServlet类继承自Servlet类,可以利用继承Http Servlet 来实现Http请求,当不是Http请求的时候,也可以定义其他形式的Servlet。

10.常见常态码

200 OK:表示从客户端发送给服务器的请求被正常处理并返回;

204 No Content:表示客户端发送给客户端的请求得到了成功处理,但在返回的响应报文中不含实体的主体部分(没有资源可以返回);

206 Patial Content:表示客户端进行了范围请求,并且服务器成功执行了这部分的GET请求,响应报文中包含由Content-Range指定范围的实体内容。

301 Moved Permanently:永久性重定向,表示请求的资源被分配了新的URL,之后应使用更改的URL;

302 Found:临时性重定向,表示请求的资源被分配了新的URL,希望本次访问使用新的URL;

303 See Other:表示请求的资源被分配了新的URL,应使用GET方法定向获取请求的资源;

304 Not Modified:表示客户端发送附带条件(是指采用GET方法的请求报文中包含if-Match、If-Modified-Since、If-None-Match、If-Range、If-Unmodified-Since中任一首部)的请求时,服务器端允许访问资源,但是请求为满足条件的情况下返回改状态码;

307 Temporary Redirect:临时重定向,与303有着相同的含义,307会遵照浏览器标准不会从POST变成GET;(不同浏览器可能会出现不同的情况);

400 Bad Request:表示请求报文中存在语法错误;

401 Unauthorized:未经许可,需要通过HTTP认证;

403 Forbidden:服务器拒绝该次访问(访问权限出现问题)

404 Not Found:表示服务器上无法找到请求的资源,除此之外,也可以在服务器拒绝请求但不想给拒绝原因时使用;

500 Inter Server Error:表示服务器在执行请求时发生了错误,也有可能是web应用存在的bug或某些临时的错误时;

503 Server Unavailable:表示服务器暂时处于超负载或正在进行停机维护,无法处理请求;

五、JVM

1. 说说JVM内存区域分为几大块,分别讲一下

Java 虚拟机在执行 Java 程序的过程中会把他所管理的内存划分为若干个不同的数据区域:

  • 程序计数器:可以看作是当前线程所执行的字节码文件(class)的行号指示器,它会记录执行痕迹,是每个线程私有的
  • 方法区:主要存储已被虚拟机加载的类的信息、常量、静态变量和即时编译器编译后的代码等数据,该区域是被线程共享的,很少发生垃圾回收
  • :栈是运行时创建的,是线程私有的,生命周期与线程相同,存储声明的变量
  • 本地方法栈:为 native 方法服务,native 方法是一种由非 java 语言实现的 java 方法,与 java 环境外交互,如可以用本地方法与操作系统交互
  • :堆是所有线程共享的一块内存,是在 java 虚拟机启动时创建的,几乎所有对象实例都在此创建,所以经常发生垃圾回收操作

JDK8 之前,Hotspot 中方法区的实现是永久代(Perm)

JDK8 开始使用元空间(Metaspace),以前永久代所有内容的字符串常量移至堆内存,其他内容移至元空间,元空间直接在本地内存分配。

2. Java中类加载过程是什么样的?

类加载的步骤为,加载 -> 验证 -> 准备 -> 解析 -> 初始化。

1、加载:

  • 获取类的二进制字节流
  • 将字节流代表的静态存储结构转化为方法区运行时数据结构
  • 在堆中生成class字节码对象

2、验证:连接过程的第一步,确保 class 文件的字节流中的信息符合当前 JVM 的要求,不会危害 JVM 的安全

3、准备:为类的静态变量分配内存并将其初始化为默认值

4、解析:JVM 将常量池内符号引用替换成直接引用的过程

5、初始化:执行类构造器的初始化的过程

4. JVM 如何确定垃圾对象:

JVM 采用的是可达性分析算法,通过 GC Roots 来判定对象是否存活,从 GC Roots 向下追溯、搜索,会产生 Reference Chain。当一个对象不能和任何一个 GC Root 产生关系时,就判定为垃圾。

软引用和弱引用,也会影响对象的回收。内存不足时会回收软引用对象;GC 时会回收弱引用对象。

5. Java中的垃圾回收算法有哪些?

Java中有四种垃圾回收算法,分别是标记清除法、标记整理法、复制算法、分代收集算法;
标记清除法
第一步:利用可达性去遍历内存,把存活对象和垃圾对象进行标记;
第二步:在遍历一遍,将所有标记的对象回收掉;
特点
效率不行,标记和清除的效率都不高;
标记和清除后会产生大量的不连续的空间分片,
可能会导致之后程序运行的时候需分配大对象而找不到连续分片而不得不触发一次GC;

标记整理法
第一步:利用可达性去遍历内存,把存活对象和垃圾对象进行标记;
第二步:将所有的存活的对象向一段移动,将端边界以外的对象都回收掉;
特点
适用于存活对象多,垃圾少的情况;需要整理的过程,无空间碎片产生;

复制算法
将内存按照容量大小分为大小相等的两块,每次只使用一块,当一块使用完了,就将还存活的对象移到另一块上,然后在把使用过的内存空间移除;
特点
不会产生空间碎片;内存使用率极低;

分代收集算法
根据内存对象的存活周期不同,将内存划分成几块,java虚拟机一般将内存分成新生代和老生代,在新生代中,有大量对象死去和少量对象存活,所以采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集;老年代中因为对象的存活率极高,没有额外的空间对他进行分配担保,所以采用标记清理或者标记整理算法进行回收;

6.常见的垃圾回收器

直达

6. 什么是双亲委派模型?为什么需要双亲委派模型?

双亲委派的意思是如果一个类加载器需要加载类,那么首先它会把这个类请求委派给父类加载器去完成,每一层都是如此。一直递归到顶层,当父加载器无法完成这个请求时,子类才会尝试去加载。

为了防止内存中出现多个相同的字节码;因为如果没有双亲委派的话,用户就可以自己定义一个java.lang.String类,那么就无法保证类的唯一性。

7. 说一下 JVM 调优的命令?
  • jps:JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。
  • jstat:jstat(JVM statistics Monitoring)是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
  • jmap:jmap(JVM Memory Map)命令用于生成heap dump文件,如果不使用这个命令,还阔以使用-XX:+HeapDumpOnOutOfMemoryError参数来让虚拟机出现OOM的时候·自动生成dump文件。 jmap不仅能生成dump文件,还阔以查询finalize执行队列、Java堆和永久代的详细信息,如当前使用率、当前使用的是哪种收集器等。
  • jhat:jhat(JVM Heap Analysis Tool)命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看。在此要注意,一般不会直接在服务器上进行分析,因为jhat是一个耗时并且耗费硬件资源的过程,一般把服务器生成的dump文件复制到本地或其他机器上进行分析。
  • jstack:jstack用于生成java虚拟机当前时刻的线程快照。jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。 如果java程序崩溃生成core文件,jstack工具可以用来获得core文件的java stack和native stack的信息,从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题。
8. 强引用、软引用、弱引用、虚引用是什么,有什么区别?
  • 强引用,就是普通的对象引用关系,如 String s = new String(“ConstXiong”)

  • 软引用,用于维护一些可有可无的对象。只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。SoftReference 实现

  • 弱引用,相比软引用来说,要更加无用一些,它拥有更短的生命周期,当 JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。WeakReference 实现

  • 虚引用是一种形同虚设的引用,在现实场景中用的不是很多,它主要用来跟踪对象被垃圾回收的活动。PhantomReference 实现

9. 谈谈对 OOM 的认识?如何排查 OOM 的问题?

除了程序计数器,其他内存区域都有 OOM 的风险。

  • 栈一般经常会发生 StackOverflowError,比如 32 位的 windows 系统单进程限制 2G 内存,无限创建线程就会发生栈的 OOM

  • Java 8 常量池移到堆中,溢出会出 java.lang.OutOfMemoryError: Java heap space,设置最大元空间大小参数无效;

  • 堆内存溢出,报错同上,这种比较好理解,GC 之后无法在堆中申请内存创建对象就会报错;

  • 方法区 OOM,经常会遇到的是动态生成大量的类、jsp 等;

  • 直接内存 OOM,涉及到 -XX:MaxDirectMemorySize 参数和 Unsafe 对象对内存的申请。

排查 OOM 的方法:

  • 增加两个参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof,当 OOM 发生时自动 dump 堆内存信息到指定目录;

  • 同时 jstat 查看监控 JVM 的内存和 GC 情况,先观察问题大概出在什么区域;

  • 使用 MAT 工具载入到 dump 文件,分析大对象的占用情况,比如 HashMap 做缓存未清理,时间长了就会内存溢出,可以把改为弱引用 。

10.什么时候会发生内存泄漏

内存泄漏是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成内存空间的浪费称为内存泄漏。内存泄露有时不严重且不易察觉,这样开发者就不知道存在内存泄露,但有时也会很严重,会提示你Out of memory。

长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期持有它的引用而导致不能被回收,这就是Java中内存泄漏的发生场景。具体主要有如下几大类:

  1. 静态集合类引起内存泄漏:像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,因为他们也将一直被Vector等引用着。
  2. 当集合里面的对象属性被修改后,再调用remove()方法时不起作用。
  3. 监听器:在java编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。
  4. 各种连接:数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的。
  5. 内部类和外部模块的引用:内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。外部模块不经意的引用也会引起内存泄漏。
  6. 单例模式:不正确使用单例模式是引起内存泄漏的一个常见问题,单例对象在初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么这个对象将不能被JVM正常回收,导致内存泄漏,
11.JVM调优过程
12. 生产环境服务器变慢,如何诊断处理?

使用 top 指令,服务器中 CPU 和 内存的使用情况,-H 可以按 CPU 使用率降序,-M 内存使用率降序。排除其他进程占用过高的硬件资源,对 Java 服务造成影响。

如果发现 CPU 使用过高,可以使用 top 指令查出 JVM 中占用 CPU 过高的线程,通过 jstack 找到对应的线程代码调用,排查出问题代码。

如果发现内存使用率比较高,可以 dump 出 JVM 堆内存,然后借助 MAT 进行分析,查出大对象或者占用最多的对象来自哪里,为什么会长时间占用这么多;如果 dump 出的堆内存文件正常,此时可以考虑堆外内存被大量使用导致出现问题,需要借助操作系统指令 pmap 查出进程的内存分配情况、gdb dump 出具体内存信息、perf 查看本地函数调用等。

如果 CPU 和 内存使用率都很正常,那就需要进一步开启 GC 日志,分析用户线程暂停的时间、各部分内存区域 GC 次数和时间等指标,可以借助 jstat 或可视化工具 GCeasy 等,如果问题出在 GC 上面的话,考虑是否是内存不够、根据垃圾对象的特点进行参数调优、使用更适合的垃圾收集器;分析 jstack 出来的各个线程状态。如果问题实在比较隐蔽,考虑是否可以开启 jmx,使用 visualmv 等可视化工具远程监控与分析

13.生产环境 CPU 占用过高,你如何解决?
  1. top + H 指令找出占用 CPU 最高的进程的 pid

  2. top -H -p在该进程中找到,哪些线程占用的 CPU 最高的线程,记录下 tid

  3. jstack -l >threads.txt,导出进程的线程栈信息到文本,导出出现异常的话,加上 -F 参数

  4. 将 tid 转换为十六进制,在 threads.txt 中搜索,查到对应的线程代码执行栈,在代码中查找占 CPU 比较高的原因。其中
    tid 转十六进制,可以借助 Linux 的 printf “%x” tid 指令

我用上述方法查到过,jvm 多条线程疯狂 full gc 导致的CPU 100% 的问题和 JDK1.6 HashMap 并发 put 导致线程 CPU 100% 的问题

六、数据库

1. 说说mysql的存储引擎

InnoDB

  • 默认事务型引擎,被广泛使用的存储引擎
  • 数据存储在共享表空间,即多个表和索引都存储在一个表空间中,可通过配置文件修改
  • 主键查询的性能高于其他类型的存储引擎
  • 内部做了很多优化,如:从磁盘读取数据时会自动构建hash索引,插入数据时自动构建插入缓冲区
  • 通过一些机制和工具支持真正的热备份
  • 支持崩溃后的安全恢复
  • 支持行级锁
  • 支持外键

MyISAM

  • 拥有全文索引、压缩、空间函数
  • 不支持事务和行级锁、不支持崩溃后的安全恢复
  • 表存储在两个文件:MYD 和 MYI
  • 设计简单,某些场景下性能很好,例如获取整个表有多少条数据,性能很高
2. 讲下索引以及应用场景
  • 当我们使用order by将查询结果按照某个字段排序时,如果该字段没有建立索引,那么执行计划会将查询出的所有数据使用外部排序,这个操作是很影响性能的。但是如果我们对该字段建立索引,那么由于索引本身是有序的,因此直接按照索引的顺序和映射关系逐条取出数据即可。
  • 对join语句匹配关系(on)涉及的字段建立索引能够提高效率。
  • 查找符合where条件的记录时
  • 如果要查询的字段都建立过索引,那么引擎会直接在索引表中查询而不会访问原始数据(否则只要有一个字段没有建立索引就会做全表扫描),这叫索引覆盖。因此我们需要尽可能的在select后只写必要的查询字段,以增加索引覆盖的几率。
3. 索引的作用?索引有什么缺点?

索引是一种用于快速查询和检索数据的数据结构。常见的索引结构有: B 树, B+树和 Hash。

索引的作用就相当于目录的作用。打个比方: 我们在查字典的时候,如果没有目录,那我们就只能一页一页的去找我们需要查的那个字,速度很慢。如果有目录了,我们只需要先去目录里查找字的位置,然后直接翻到那一页就行了。

优点 :

  • 使用索引可以大大加快 数据的检索速度(大大减少检索的数据量), 这也是创建索引的最主要的原因。
  • 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。

缺点 :

  • 创建索引和维护索引需要耗费许多时间。当对表中的数据进行增删改的时候,如果数据有索引,那么索引也需要动态的修改,会降低 SQL 执行效率。
  • 索引需要使用物理文件存储,也会耗费一定空间。
4. 索引的设计原则

索引虽好,但也不是无限制的使用,最好符合一下几个原则

1) 最左前缀匹配原则,组合索引非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。

2)较频繁作为查询条件的字段才去创建索引

3)更新频繁字段不适合创建索引

4)若是不能有效区分数据的列不适合做索引列(如性别,男女未知,最多也就三种,区分度实在太低)

5)尽量的扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。

6)定义有外键的数据列一定要建立索引。

7)对于那些查询中很少涉及的列,重复值比较多的列不要建立索引。

8)对于定义为text、image和bit的数据类型的列不要建立索引。

6. MySQL索引使用的什么数据结构,B树和B+树的区别

目前大部分数据库系统及文件系统都采用 B-Tree 或其变种 B+Tree 作为索引结构。

在B树中,你可以将键和值存放在内部节点和叶子节点;但在B+树中,内部节点都是键,没有值,叶子节点同时存放键和值。

B+树的叶子节点有一条链相连,而B树的叶子节点各自独立。

B树可以在内部节点同时存储键和值,因此,把频繁访问的数据放在靠近根节点的地方将会大大提高热点数据的查询效率。这种特性使得B树在特定数据重复多次查询的场景中更加高效。

由于B+树的内部节点只存放键,不存放值,因此,一次读取,可以在内存页中获取更多的键,有利于更快地缩小查找范围。 B+树的叶节点由一条链相连,因此,当需要进行一次全数据遍历的时候,B+树只需要使用O(logN)时间找到最小的一个节点,然后通过链进行O(N)的顺序遍历即可。而B树则需要对树的每一层进行遍历,这会需要更多的内存置换次数,因此也就需要花费更多的时间

7. 索引为什么采用B+树的数据结构,而不使用二叉树或者红黑树

红黑树也叫平衡二叉树,它不仅继承了二叉树的优点,而且解决了上面二叉树遇到的自增整形索引的问题,而且红黑树会左旋、右旋对结构进行调整,始终保证左子节点数 < 父节点数 < 右子节点数的规则。但在数据量大的时候,深度也很大。如果我们有很多数据,那么树的深度依然会很大,可能就会超过十几二十层以上,对我们的磁盘寻址不利,依然会花费很多时间查找。

B+树存储结构,只有叶子节点存储数据。B+树结构没有在所有的节点里存储记录数据,而是只在最下层的叶子节点存储,上层的所有非叶子节点只存放索引信息,这样的结构可以让单个节点存放下更多索引值,提高命中目标记录的几率。这种结构会在上层非叶子节点存储一部分冗余数据,但是这样的缺点都是可以容忍的,因为冗余的都是索引数据,不会对内存造成大的负担。

8. 聚簇索引和非聚簇索引这两个概念怎么理解?

聚簇索引就是按照每张表的主键构造一颗B+树,同时叶子节点中存放的就是整张表的行记录数据,也将聚集索引的叶子节点称为数据页。这个特性决定了索引组织表中数据也是索引的一部分,每张表只能拥有一个聚簇索引。

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

在聚簇索引之上创建的索引称之为辅助索引,辅助索引访问数据总是需要二次查找。辅助索引叶子节点存储的不再是行的物理位置,而是主键值。通过辅助索引首先找到的是主键值,再通过主键值找到数据行的数据页,再通过数据页中的Page Directory找到数据行。

Innodb辅助索引的叶子节点并不包含行记录的全部数据,叶子节点除了包含键值外,还包含了相应行数据的聚簇索引键。

辅助索引的存在不影响数据在聚簇索引中的组织,所以一张表可以有多个辅助索引。在innodb中有时也称辅助索引为二级索引。

9. mysql索引优化相关方法,联合索引应该把什么字段放在第一个位置?

索引优化的方法有以下,第一:字段选择性。查询条件含有多个字段时,不要在选择性很低字段上创建索引,可通过创建组合索引来增强低字段选择性和避免选择性很低字段创建索引带来副作用。正确索引会提高sql查询速度,过多索引会增加优化器选择索引的代价,不要滥用索引;第二:Explain优化查询检测。EXPLAIN可以帮助开发人员分析SQL问题,explain显示了mysql如何使用索引来处理select语句以及连接表,可以帮助选择更好的索引和写出更优化的查询语句。

在建立联合索引的时候应该注意索引列的顺序,一般情况下,将查询需求频繁或者字段选择性高的列放在前面。

10. mysql的最左原则吗?
  • 顾名思义,就是最左优先,在创建多列索引时,要根据业务需求,where子句中使用最频繁的一列放在最左边。
  • 最左前缀匹配原则,非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。
  • =和in可以乱序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式
11. 脏读、幻读、不可重复读指什么?
  • 脏读:一个事务读取另外一个事务还没有提交的数据。
  • 不可重复读:一个事务内,两次相同条件的查询返回了不同的结果。
  • 幻读:同一个事务中,一条数据出现在这次查询的结果集里,却没有出现在之前的查询结果集中。例如,在一个事务中进行了同一个查询运行了两次,期间被另外一个事务提交插入一行或修改查询条件匹配的一行。它比不可重复读更难防范,因为锁定第一个查询结果集的所有行并不能阻止导致幻象出现的更改。
12. 数据库事务的四个特性:

事务具备ACID四种特性,ACID是Atomic(原子性)、Consistency(一致性)、Isolation(隔离性)和Durability(持久性)的英文缩写。

  • 原子性(Atomicity)
    事务最基本的操作单元,要么全部成功,要么全部失败,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样。

  • 一致性(Consistency)
    事务的一致性指的是在一个事务执行之前和执行之后数据库都必须处于一致性状态。如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于有效状态。如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态。

  • 隔离性(Isolation)
    指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务查看数据更新时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。

  • 持久性(Durability)
    指的是只要事务成功结束,它对数据库所做的更新就必须永久保存下来。即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。

13. 说说sql的事务隔离级别,具体的应用场景
  • 读未提交(Read Uncommitted):是最低的事务隔离级别,它允许另外一个事务可以看到这个事务未提交的数据。会出现脏读,幻读,不可重复读,所有并发问题都可能遇到。
  • 读已提交(Read Committed):保证一个事物提交后才能被另外一个事务读取。另外一个事务不能读取该事物未提交的数据。不会出现脏读现象,但是会出现幻读,不可重复读。
  • 可重复读(Repeatable Read):这种事务隔离级别可以防止脏读,不可重复读,但是可能会出现幻象读。它除了保证一个事务不能被另外一个事务读取未提交的数据之外还避免了不可重复读。
  • 串行化(Serializable):这是花费最高代价但最可靠的事务隔离级别。事务被处理为顺序执行。防止脏读、不可重复读、幻象读。
14. 说说数据库的乐观锁和悲观锁?

1.乐观锁

乐观锁在读数据时,认为别人不会去写其所读的数据:悲观锁就刚好相反,觉得自己读数据时,别人可能刚好在写自己刚读的数据,态度比较保守;时间戳在操作数据时不加锁,而是通过时间戳来控制并发出现的问题。

2.悲观锁

悲观锁指在其修改某条数据时,不允许别人读取该数据,直到自己的整个事务都提交并释放锁,其他用户才能访问该数据。悲观锁又可分为排它锁(写锁)和共享锁(读锁)。

15.数据库怎么进行优化

1、优化索引、sql语句、分析慢查询
2、设计表的时候严格根据数据的设计规范来设计数据库
3、使用缓存,吧经常访问到的数据而且不需要变化的数据放到缓存中
4、使用固态硬盘
5、采用MYSQL内部自带的表分区技术,吧数据分层到不同的文件中,能够提高餐盘的读写效率
6、垂直分表,吧一些不经常用到的数据放到一个表中,节约磁盘的I/O
7、主从分离读写,采取主从复制把数据库的读操作和写操作分离出来
8、数据库分表分机器(数据特变大的),主要的原理就是数据路由
9、选择合适的表引擎,对参数地上的优化
10、进行加购级别的缓存,静态化和分布式。
11、不采用全文索引吗,用什么搜什么
12、采用更快的处分方式,例如NoSql储存经常访问的数据

18.主键索引和唯一索引的区别

1.主键不允许空值,唯一索引允许空值
2.主键只允许一个,唯一索引允许多个
3.主键产生唯一的聚集索引,唯一索引产生唯一的非聚集索引

19.介绍一下MVCC
20.索引失效的条件?
  • 如果条件中有 or,即使其中有部分条件是索引字段,也不会使用索引
  • 复合索引,查询条件不使用索引前面的字段,后续字段也将无法使用索引
  • 以 % 开头的 like 查询
  • 索引列的数据类型存在隐形转换
  • where 子句里对索引列有数学运算
  • where 子句里对索引列使用函数
  • MySQL 引擎估算使用全表扫描要比使用索引快,则不使用索引
21.主键索引和唯一索引的区别?哪个快?

七、设计模式

1. 你最熟悉的设计模式 ?

单例模式
保证一个类只有一个实例,并且提供一个访问该全局访问点
2.那些地方用到了单例模式
网站的计数器,一般也是采用单例模式实现,否则难以同步。
应用程序的日志应用,一般都是单例模式实现,只有一个实例去操作才好,否则内容不好追加显示。
多线程的线程池的设计一般也是采用单例模式,因为线程池要方便对池中的线程进行控制
Windows的(任务管理器)就是很典型的单例模式,他不能打开俩个
windows的(回收站)也是典型的单例应用。在整个系统运行过程中,回收站只维护一个实例。
5.单例创建方式
(主要使用懒汉和懒汉式)
饿汉式:类初始化时,会立即加载该对象,线程天生安全,调用效率高。
懒汉式: 类初始化时,不会初始化该对象,真正需要使用的时候才会创建该对象,具备懒加载功能。
静态内部方式:结合了懒汉式和饿汉式各自的优点,真正需要对象的时候才会加载,加载类是线程安全的。
枚举单例: 使用枚举实现单例模式 优点:实现简单、调用效率高,枚举本身就是单例,由jvm从根本上提供保障!避免通过反射和反序列化的漏洞, 缺点没有延迟加载。
双重检测锁方式 (因为JVM本质重排序的原因,可能会初始化多次,不推荐使用)

2. 懒汉式你会怎么写,懒汉式实例化在哪,构造函数的权限?

1.饿汉式
饿汉式:类初始化时,会立即加载该对象,线程天生安全,调用效率高。

package com.lijie;

//饿汉式
public class Demo1 {

    // 类初始化时,会立即加载该对象,线程安全,调用效率高
    private static Demo1 demo1 = new Demo1();

    private Demo1() {
        System.out.println("私有Demo1构造参数初始化");
    }

    public static Demo1 getInstance() {
        return demo1;
    }

    public static void main(String[] args) {
        Demo1 s1 = Demo1.getInstance();
        Demo1 s2 = Demo1.getInstance();
        System.out.println(s1 == s2);
    }
}

2.懒汉式
懒汉式: 类初始化时,不会初始化该对象,真正需要使用的时候才会创建该对象,具备懒加载功能。

package com.lijie;

//懒汉式
public class Demo2 {

    //类初始化时,不会初始化该对象,真正需要使用的时候才会创建该对象。
    private static Demo2 demo2;

    private Demo2() {
        System.out.println("私有Demo2构造参数初始化");
    }

    public synchronized static Demo2 getInstance() {
        if (demo2 == null) {
            demo2 = new Demo2();
        }
        return demo2;
    }

    public static void main(String[] args) {
        Demo2 s1 = Demo2.getInstance();
        Demo2 s2 = Demo2.getInstance();
        System.out.println(s1 == s2);
    }
}
3. 单例模式的饿汉式和懒汉式及区别

饿汉式:类初始化时,会立即加载该对象,线程天生安全,调用效率高。
懒汉式: 类初始化时,不会初始化该对象,真正需要使用的时候才会创建该对象,具备懒加载功能。

5. 简单工厂和抽象工厂有什么区别?
  • 简单工厂模式
    是由一个工厂对象创建产品实例,简单工厂模式的工厂类一般是使用静态方法,通过不同的参数的创建不同的对象实例
    可以生产结构中的任意产品,不能增加新的产品

  • 抽象工厂模式
    提供一个创建一系列相关或相互依赖对象的接口,而无需制定他们具体的类,生产多个系列产品
    生产不同产品族的全部产品,不能新增产品,可以新增产品族

6. 说一说设计模式中的代理模式?

代理模式指为对象提供-种通过代理的方式来访问并控制该对象行为的方法。在客户端不适合或者不能够直接引用一-个对象时,可以通过该对象的代理对象来实现对该对象的访问,可以将该代理对象理解为客户端和目标对象之间的中介者。

在现实生活也能看到代理模式的身影,比如企业会把五险一金业务交给第三方人力资源公司去做,因为人力资源公司对五险一金 业务更加熟悉。

在代理模式下有两种角色,一种是被代理者,一种是代理( Proxy),在被代理者需要做一项工作时,不用自己做,而是交给代理做。比如企业在招人时,不用自己去人才市场上找,可以通过代理(猎头公司)去找,代理有候选人池,可根据企业的需求筛选出合适的候选人去返回给企业。

8. 说一说设计模式中的适配器模式?

我们常常在开发中遇到各个系统之间的对接问题,然而每个系统的数据模型或多或少均存在差别,因此可能存在改变现有对象模型的情况,这将影响到系统的稳定。若想在不改变原有代码结构(类的结构)的情况下完成友好对接,就需要用到适配器模式。

适配器模式(Adapter Pattern)通过定义一个适配器类作为两个不兼容的接口之间的桥梁,将一个类的接口转换成用户期望的另一个接口,使得两个或多个原本不兼容的接口可以基于适配器类一起工作。

适配器模式主要通过适配器类实现各个接口之间的兼容,该类通过依赖注人或者继承实现各个接口的功能并对外统一提供服务。

在适配器模式的实现中有三种角色: Source、 Targetable、 Adapter。 Source 是待适配的类,Targetable 是目标接口,Adapter 是适配器。我们在具体应用中通过Adapter 将Source的功能扩展到Targetable,以实现接口的兼容。适配器的实现主要分为三类:类适配器模式、对象适配器模式、接口适配器模式。

八、框架

1. Spring的特点?

Spring 是一款开源的轻量级 Java 开发框架,旨在提高开发人员的开发效率以及系统的可维护性。

我们一般说 Spring 框架指的都是 Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发。

比如说 Spring 自带 IoC(Inverse of Control:控制反转) 和 AOP(Aspect-Oriented Programming:面向切面编程)、可以很方便地对数据库进行访问、可以很方便地集成第三方组件(电子邮件,任务,调度,缓存等等)、对单元测试支持比较好、支持 RESTful Java 应用程序的开发。

2.谈谈自己对于 Spring IoC 的了解

Spring通过一个配置 文件描述Bean 和Bean之间的依赖关系,利用Java的反射功能实例化Bean并建立Bean之间的依赖关系。Spring 的IoC容器在完成这些底层工作的基础上,还提供了Bean 实例缓存管理、Bean 生命周期管理、Bean实例代理、事件发布和资源装载等高级服务。

Spring 在启动时会从XML配置文件或注解中读取应用程序提供的Bean配置信息,并在Spring 容器中生成一份相应的Bean配置注册表:然后根据这张注册表实例化Bean.装配好Bcan 之间的依赖关系,为上层业务提供基础的运行环境。其中Bean缓存池为HashMap实现。

3.Spring AOP的实现原理?具体应用在哪些方面?举个例子?

AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。

Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用 Cglib ,这时候 Spring AOP 会使用 Cglib 生成一个被代理对象的子类来作为代理.

4.如何实现动态代理

Spring提供了JDK 和CGLib 2种方式来生成代理对象,具体生成代理对象的方式由AopProxyFactory根据AdvisedSupport 对象的配置来决定。Spring 默认的代理对象生成策略为:如果是目标类接口,则使用JDK动态代理技术,否则使用CGLib动态代理技术。

JDK动态代理: JDK动态代理主要通过java.lang.reflect 包中Proxy类和InvocationHandler接口来实现。InvocationHandler 是一个接口,不同的实现类定义不同的横切逻辑,并通过反射机制调用目标类的代码,动态地将横切逻辑和业务逻辑编制在一起。Proxy类利用InvocationHandler 动态创建一个符合某一 .接口的实例,生成目标类的代理对象。JDK 1.8中Proxy类的定义如下。

CGLib 动态代理: CGLib 即Code Generation Library, 它是一个高性能的代码生成类库,可以在运行期间扩展Java类和实现Java 接口。CGLib包的底层通过字节码处理框架ASM来实现,通过转换字节码生成新的类。

CGLib动态代理和JDK动态代理的区别: JDK只能为接口创建代理实例,而对于没有通过接口定义业务方法的类,则只能通过CGLib创建动态代理来实现。

4.Springboot的启动流程

第一步:获取并启动监听器
第二步:构造容器环境
第三步:创建容器
第四步:实例化SpringBootExceptionReporter.class,用来支持报告关于启动的错误
第五步:准备容器
第六步:刷新容器
第七步:刷新容器后的扩展接口

5. Springboot的自动装配原理

SpringBoot自动装配的本质就是通过Spring去读取META-INF/spring.factories中保存的配置类文件然后加载bean定义的过程。
如果是标了@Configuration注解,就是批量加载了里面的bean定义
如何实现”自动“:通过配置文件获取对应的批量配置类,然后通过配置类批量加载bean定义,只要有写好的配置文件spring.factories就实现了自动。

6.Springboot的常用注解

1、@SpringBootApplication

包含@Configuration、@EnableAutoConfiguration、@ComponentScan

通常用在主类上。

2、@Repository

用于标注数据访问组件,即DAO组件。

3、@Service

用于标注业务层组件。

4、@RestController

用于标注控制层组件(如struts中的action),包含@Controller和@ResponseBody

5、@ResponseBody

表示该方法的返回结果直接写入HTTP response body中

一般在异步获取数据时使用,在使用@RequestMapping后,返回值通常解析为跳转路径,加上@responsebody后返回结果不会被解析

为跳转路径,而是直接写入HTTP response body中。比如异步获取json数据,加上@responsebody后,会直接返回json数据。

6、@Component

泛指组件,当组件不好归类的时候,我们可以使用这个注解进行标注。

7、@ComponentScan

组件扫描。相当于,如果扫描到有@Component @Controller @Service等这些注解的类,则把

这些类注册为bean。

8、@Configuration

指出该类是 Bean 配置的信息源,相当于XML中的,一般加在主类上。

9、@Bean

相当于XML中的,放在方法的上面,而不是类,意思是产生一个bean,并交给spring管理。

10、@EnableAutoConfiguration

让 Spring Boot 根据应用所声明的依赖来对 Spring 框架进行自动配置,一般加在主类上。

11、@AutoWired

byType方式。把配置好的Bean拿来用,完成属性、方法的组装,它可以对类成员变量、方法及构造函数进行标注,完成自动装配的工作。
当加上(required=false)时,就算找不到bean也不报错。

12、@Qualifier

当有多个同一类型的Bean时,可以用@Qualifier(“name”)来指定。与@Autowired配合使用

13、@Resource(name=“name”,type=“type”)

没有括号内内容的话,默认byName。与@Autowired干类似的事。

14、@RequestMapping

RequestMapping是一个用来处理请求地址映射的注解,可用于类或方法上。用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径。

5.Spring的Bean的创建流程

Spring 在启动时会从XML配置文件或注解中读取应用程序提供的Bean配置信息,并在Spring 容器中生成一份相应的Bean配置注册表:然后根据这张注册表实例化Bean.装配好Bcan 之间的依赖关系,为上层业务提供基础的运行环境。其中Bean缓存池为HashMap实现。

6.Spring的bean的生命周期

(1)实例化一个Bean

(2)按照Spring上下文对实例化的Bean进行配置。

(3)如果这个Bean 实现了BeanNameAware 接口,则会调用它实现的setBeanName(String)方法,该方法传递的参数是Spring配置文件中Bean的id值。

(4)如果这个Bean实现了BeanFactoryAware 接口,则会调用它实现的setBeanFactory(BeanFactory)方法,该方法传递的参数是Spring 工厂自身。

(5)如果这个Bean 实现了ApplicationContextAware 接口,则会调用setApplicationContext(ApplicationContext)方法,该方法传入的参数是Spring上下文。

( 6)如果该Bean关联了BeanPostProcessor 接口,则会调用postProcessBeforeInitialization(Object obj, String s)方法,该方法在Bean初始化前调用,常用于定义初始化Bean的前置工作,比如系统缓存的初始化。

(7)如果Bean在Spring 配置文件中配置了init-method属性, 则会自动调用其配置的初始化方法。

(8)如果某个Bean 关联了BeanPostrocesor 接口,将会调用postProcessterehitaizatin(Objeat obj, String s)方法。至此,Bean的初始化工作就完成了,应用程序就可以开始使用Bean实例了。

( 9)当Bean不再被需要时,会在清理阶段被清理掉。如果Bean实现了DisposableBean接口,则Spring会在退出前调用实现类的destroy()方法。

(10) 如果某个Bean的Spring 配置文件中配置了destroy-method 属性,在Bean 被铺毁前会自动调用其配置的销毁方法。

7.SpringMVC的原理

SpringMVC框架是以请求为驱动,围绕Servlet设计,将请求发给控制器,然后通过模型对象,分派器来展示请求结果视图。其中核心类是DispatcherServlet,它是一个Servlet,顶层是实现的Servlet接口。
在这里插入图片描述
流程说明:

(1)客户端(浏览器)发送请求,直接请求到DispatcherServlet。

(2)DispatcherServlet根据请求信息调用HandlerMapping,解析请求对应的Handler。

(3)解析到对应的Handler后,开始由HandlerAdapter适配器处理。

(4)HandlerAdapter会根据Handler来调用真正的处理器开处理请求,并处理相应的业务逻辑。

(5)处理器处理完业务后,会返回一个ModelAndView对象,Model是返回的数据对象,View是个逻辑上的View。

(6)ViewResolver会根据逻辑View查找实际的View。

(7)DispaterServlet把返回的Model传给View。

(8)通过View返回给请求者(浏览器)

9.Mybatis的$和#的区别

#{ }可以防止Sql 注入,它会将所有传入的参数作为一个字符串来处理。
$ {} 则将传入的参数拼接到Sql上去执行,一般用于表名和字段名参数,$ 所对应的参数应该由服务器端提供,前端可以用参数进行选择,避免 Sql 注入的风险

九、Redis

1. redis都有哪些数据结构?
  • String字符串:字符串类型是 Redis 最基础的数据结构,首先键都是字符串类型,而且 其他几种数据结构都是在字符串类型基础上构建的,我们常使用的 set key value 命令就是字符串。常用在缓存、计数、共享Session、限速等。

  • Hash哈希:在Redis中,哈希类型是指键值本身又是一个键值对结构,哈希可以用来存放用户信息,比如实现购物车。

  • List列表(双向链表):列表(list)类型是用来存储多个有序的字符串。可以做简单的消息队列的功能。

  • Set集合:集合(set)类型也是用来保存多个的字符串元素,但和列表类型不一 样的是,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。利用 Set 的交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。

  • Sorted Set有序集合(跳表实现):Sorted Set 多了一个权重参数 Score,集合中的元素能够按 Score 进行排列。可以做排行榜应用,取 TOP N 操作。

2. Redis的应用场景
  • 缓存
  • 共享Session
  • 消息队列系统
  • 分布式锁
3. redis的延时队列怎么实现?

1、使用zset数据结构存储,订单号为key,时间为score。
2、新增订单的时候,将订单号插入zset。
3、设定轮询,每分钟轮询一次zset,找出score小于当前秒数的数据,进行处理,然后将key在zset内删除。

4. Redis如何实现持久化?

Redis支持RDB和AOF两种持久化方式。

(1) RDB (Redis DataBase): RDB在指定的时间间隔内对数据进行快照存储。RDB的特点在于:文件格式紧凑,方便进行数据传输和数据恢复;在保存.rdb快照文件时父进程会fork 出一个子进程,由子进程完成具体的持久化工作,所以可以最大化Redis 的性能;同时,与AOF相比,在恢复大的数据集时会更快一些。

(2) AOF ( Append Of Flie): AOF记录对服务器的每次写操作,在Redis重启时会重放这些命令来恢复原数据。AOF命令以Redis 协议追加和保存每次写操作到文件末尾,Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大。AOF的特点有:可以使用不同的fsync 策略(无fsync、每秒fsync、每次写的时候fsync )将操作追加命令到文件中,操作效率高;同时,AOF文件是日志的格式,更容易被理解和操作。

7. Redis缓存淘汰策略?是失效时间到了就立即淘汰吗?淘汰控制?

FIFO (First In First Out)先进先出原则
最先进入的缓存数据在缓存空间不够的情况下(超出最大元素限制时)会首先被清理出去

LFU (Less Frequently Uesd)最少使用原则
一直以来最少被使用的元素会被清理掉。意味着,要求缓存的元素有一个hit属性,在缓存空间不够的情况下,hit值最小的将会被清理出去

LRU (Least Recently Used)最近最少使用原则
缓存的元素有个时间戳,当缓存容量满了,而又要腾出新地方来缓存新的元素的时候,则现有缓存元素中时间戳离当前时间最远的元素将被清除出去

8. 什么是缓存雪崩和缓存穿透?

缓存雪崩
缓存雪崩指在同一时刻由于大量缓存失效,导致大量原本应该访问缓存的请求都去查询数据库,而对数据库的CPU和内存造成巨大压力,严重的话会导致数据库宕机,从而形成一系列连锁反应,使整个系统崩溃。

缓存穿透
缓存穿透指由于级存系统故陈或者用户频繁查询系统中不存在(在系统中不存在,在自然数据库和级存中都不存在)的数据,而这时请求穿过缓存不断被发送到数据库,导致数据库过载,进而引发一连串非发问题。

9. 如何解决 Redis 缓存雪崩问题
  • 请求加锁:对于并发量不是很多的应用,使用请求加锁排队的方案防止过多请求数据库。
  • 失效更新:为每一个缓存数据都增加过期标记来记录缓存数据是否失效,如果缓存标记失效,则更新数据缓存。
  • 设置不同的失效时间:为不同的数据设置不同的缓存失效时间,防止在同-时刻有大量的数据失效。
10. 如何解决 Redis 缓存穿透问题

常用的解决级存穿透问题的方法有布隆过滤器eache null策略。

  • 布隆过滤器: 指将所有可能存在的数据都映射列一个足够大的Bitmap中,在用户发起请求时首先经过布隆过滤器的拦截,一个一定不存在的数据会被这个布隆过泄器拦做,从而避免对底层存储系统业来企询上的压力。

  • cache null 策略:指如果一个查询返回的结果为null (可能见数据不存在,也可能是系统故陈),我们仍然级存这个nul结果,但它的过期时间会很短,通常不旭过5分钟;在用户再次青水该数据时血接返回nul,而不会继续访问数据库,从而有效保陈数据库的安全。其实cache null 策略的核心原理是:在级存中记录一个短暂的(数据过则时间内)数据在系统中是否存在的状态,如果不存在,则直接返回nl,不再在询数据内,从而避免复存穿遇列数据库上。

12. Redis如何淘汰过期数据?

定期删除。在设置了过期时间的数据集中,隔一段时间挑几个检测,看是不是过期了,过期就删了;惰性删除。在get获取值的时候,会检测你要get的数据是否过期,过期就删了,返回null。如果定期删除漏掉了很多过期key,然后也没及时去查,也就没走惰性删除,如果大量过期key堆积在内存里,导致redis内存块耗尽了,则进行内存淘汰机制。
一共有六种淘汰策略:
1从设置过期时间(不一定已经过期)的数据集中挑选出最近最少使用的数据淘汰。
2从设置过期时间的数据集中挑选将要过期的数据淘汰,优先删除剩余时间短的key。
3从设置过期时间的数据集中任意选择数据淘汰。当内存达到限制无法写入非过期时间的数据集时,可以通过该淘汰策略在主键空间中随机移除某个key。
4从整个数据集中挑选最近最少使用的数据淘汰,该策略要淘汰的key面向的是全体key集合,而非过期的key集合。
5从数据集中选择任意数据淘汰。
6禁止驱逐数据,也就是当内存不足以容纳新入数据时,新写入操作就会报错,请求可以继续进行,线上任务也不能持续进行,采用该策略可以保证数据不被丢失,这也是系统默认的一种淘汰策略。

15. zset的底层数据结构?什么是跳表?

Redis的zset为有序自动去重的集合数据类型,它的结构使用的是hash加上跳表的设计。hash结构用来存储value和score的关系,但是zset可以根据score的范围获取value的列表,这个实现就要用到跳跃表的设计。
跳表(skip List)是一种随机化的数据结构,基于并联的链表,实现简单,插入、删除、查找的复杂度均为O(logN)。简单说来跳表也是链表的一种,只不过它在链表的基础上增加了跳跃功能,正是这个跳跃的功能,使得在查找元素时,跳表能够提供O(logN)的时间复杂度。

十、Kafka

1. Kafka原理?

broker:中间的kafka cluster,存储消息,是由多个server组成的集群。
topic:kafka给消息提供的分类方式。broker用来存储不同topic的消息数据。
producer:往broker中某个topic里面生产数据。
consumer:从broker中某个topic获取数据。

2.kafaka为什么那么快

四大特点

  1. 顺序读写

在 Kafka 中一个概念叫做 Partition ,分区的意思用来存储消息,生产者生产的消息都是从末尾进行添加。

这里设计到一个新的问题,就是消费过的消息消费了之后进行删除不就破坏了消息的顺序关系了吗。这里 Kafka 做了个折中的处理,不进行实时的消息删除,而是在某个时间进行批量删除的,这个比单个删除效率要高。

  1. 页缓存
    页缓存相对来说比较简单,页缓存在操作系统层面是保存数据的一个基本单位,Kafka 避免使用 JVM,直接使用操作系统的页缓存特性提高处理速度,进而避免了JVM GC 带来的性能损耗。

  2. 零拷贝
    作为 Kafka 运行在 Linux 操作系统,作为 Linux 操作系统,它有一个特性叫做零拷贝。就是在用户态与内核态不再发生拷贝。

接下来我使用更加形象化进行解释,来看下图。假设现在有一条数据需要应用程序进行操作,但是他现在存储在磁盘上。操作系统层面会将磁盘信息加载到页缓存,之后再 copy 到应用层面的应用内存,需要发送的时候再加载到 socket 缓冲区。

但 Kafka 不是这么做的,来看下图。当数据需要操作会加载到内核态的页缓存中,需要发送是再加载到 socket 缓冲区中。其中就少了与用户态之间的 copy 动作,如果再处理海量数据的时候,效率就提高了很多。

  1. 批量操作

最后一个呢是批处理,同学可以想一下在 JDBC 中数据库的操作,会有些批量处理操作,它用来提高网络利用率与数据库执行效率。

在 kafka 中页提高了大量批处理的 API ,可以对数据进行统一的压缩合并,通过更小的数据包在网络中进行数据发送,再进行后续处理,这在大量数据处理中,效率提高是非常明显的。

2. Kafka三大特点

1.高吞吐量:可以满足每秒百万级别消息的生产和消费。

2.持久性:有一套完善的消息存储机制,确保数据高效安全且持久化。

3.分布式:基于分布式的扩展;Kafka的数据都会复制到几台服务器上,当某台故障失效时,生产者和消费者转而使用其它的Kafka。

2.Kafka 与传统 MQ 消息系统之间有三个关键区别

(1).Kafka 持久化日志,这些日志可以被重复读取和无限期保留

(2).Kafka 是一个分布式系统:它以集群的方式运行,可以灵活伸缩,在内部通过复制数据提升容错能力和高可用性

(3).Kafka 支持实时的流式处理

3.Kafka怎么保证顺序性?

Kafka 分布式的单位是 partition,同一个 partition 用一个 write ahead log 组织,所以可以保证 FIFO 的顺序。不同 partition 之间不能保证顺序。但是绝大多数用户都可以通过 message key 来定义,因为同一个 key 的 message 可以保证只发送到同一个 partition。

Kafka 中发送 1 条消息的时候,可以指定(topic, partition, key) 3 个参数。partiton 和 key 是可选的。如果你指定了 partition,那就是所有消息发往同 1个 partition,就是有序的。并且在消费端,Kafka 保证,1 个 partition 只能被1 个 consumer 消费。或者你指定 key( 比如 order id),具有同 1 个 key 的所有消息,会发往同 1 个 partition。

4. Kafka怎么避免数据丢失?

通过request.required.acks属性进行配置,有三个选项:

0代表:不进行消息接收是否成功的确认(默认值);

1代表:当Leader副本接收成功后,返回接收成功确认信息;

-1代表:当Leader和Follower副本都接收成功后,返回接收成功确认信息;acks设置为0时,不和Kafka集群进行消息接受确认,当网络发生异常等情况时,存在消息丢失的可能;想要不丢失消息数据就选:同步、ack=-1的策略。

5. Kafka怎么避免重复消费?

同消息不丢失伴生问题,如何避免重复消费:数据重复消费的情况,如果处理
去重:将消息的唯一标识保存到外部介质中,每次消费处理时判断是否处理过;
不管:大数据场景中,报表系统或者日志信息丢失几条都无所谓,不会影响最终的统计分析结果。

在kafka下游经常出现系统崩溃,需要回滚的问题,如何做到消息不重复消费是项目中很重要的一部分。可以修改offet从制定位置消费,也可以根据消息内容,从头消费toptic。对唯一字段进行过滤,做到消费过的字段不再消费。

6. Zookeeper对于Kafka的作用是什么?

答:

Zookeeper是一个开放源码的、高性能的协调服务,它用于Kafka的分布式应用。Zookeeper主要用于在集群中不同节点之间进行通信在Kafka中,它被用于提交偏移量,因此如果节点在任何情况下都失败了,它都可以从之前提交的偏移量中获取除此之外,它还执行其他活动,如:leader检测、分布式同步、配置管理、识别新节点何时离开或连接、集群、节点实时状态等等。

7. 说一下kafka的内部结构,消息队列存在的意义?

一个典型的Kafka体系架构有多个Producer(可以是服务器日志,业务数据,页面前端产生的page view等等),多个Broker,多个Consumer,每个Producer可以对应多个Topic,每个Consumer只能对应一个Consumer Group,以及一个Zookeeper集群。Kafka通过Zookeeper管理集群配置,选举leader,以及在消费者组发生变化时进行rebalance。Producer使用push(推)模式将消息发布到broker,Consumer使用pull(拉)模式从broker订阅并消费消息。

消息队列意义:
解耦:生产者只负责把消息放在队列中,而不用关心谁去使用它。
异步:生产者把消息放在队列中后即可返回,而不用一个个的通知消费者去执行,消费者是异步的获取消息的。
限流:生产者一次性产生大量的数据时,不会给消费者造成压力,消费者可以根据自身的能力,去消息队列中取数据。
在项目中,发帖事件,站内通知事件以及删帖事件是分别用不同的topic来存放,里面有且只有一个partiton,而Kafka 本身是保证 partiton 中消息的顺序性的,所以单分区下不用特别考虑顺序性问题。

十一、ES

1. ES的底层数据结构?

构造一个查询对象searchQuery,加入参数包括查询位置,查询顺序,分页信息,高亮设置。接着获取命中数据,创建一个集合,在集合中构建一个实体,根据命中的数据去构造这个实体,最终返回。使用消息队列(kafka)的方式,实现发帖/删帖后ES数据库的自动更新。最后创建一个控制类SearchController,这里先调用service实现数据的搜索,再用一个map聚合数据。最后返回到前端模板。
Elasticsearch使用倒排索引的数据结构,该结构支持非常快速的全文本搜索。倒排序在索引时创建,序列化到磁盘,全文搜索非常快,但不适合做排序,适合查询和全文检索。倒排索引是单词到文档映射关系的最佳实现形式。倒排索引列出了出现在任何文档中的每个唯一单词,并标识了每个单词出现的所有文档。索引可以认为是文档的优化集合,每个文档都是字段的集合,这些字段是包含数据的键值对。

2. ES倒排索引为什么能够加速搜索?

倒排索引列出了出现在任何文档中的每个唯一单词,并标识了每个单词出现的所有文档。索引可以认为是文档的优化集合,每个文档都是字段的集合,这些字段是包含数据的键值对。默认情况下,Elasticsearch 对每个字段中的所有数据建立索引,并且每个索引字段都具有专用的优化数据结构。例如,文本字段存储在倒排索引中,数字字段和地理字段存储在BKD树中。不同字段具有属于自己字段类型的特定优化数据结构,并具备快速响应返回搜索结果的能力使得 Elasticsearch 搜索飞快。

十二、数据结构与算法

1. 稳定排序有哪些?

不稳定:快排,堆排序,希尔排序,直接选择排序。
稳定:直接插入排序,冒泡,归并排序,基数排序

2. 排序的时空复杂度

在这里插入图片描述

3. 快速排序

选一个关键值作为基准值(一般选第一个元素),
从后向前比较,直到找到比基准值小的则交换位置;
从前向后比较,直到找到比基准值大的则交换位置;
重复执行,直到从前向后比较的索引大于等于从后向前比较的索引。


    public static int[] quickSort(int[] arr,int low,int high) {
        int start = low;//从前向后比较的索引
        int end= high;//从后向前比较的索引
             //基准数,默认设置为第一个值
        int key=arr[low];

        //循环
        while (end > start) {
            //从后往前比较
            while (end > start && arr[end] >= key) {
                end--;
            }
            //如果没有比基准值小的,则比较下一个,直到有比基准值小的,则交换位置,然后又从前向后比较
            if (arr[end] <= key) {
           		int temp = arr[end];
                arr[end] = arr[start];
                arr[start] = temp;
            }
            //从前往后比较
            while (end > start && arr[end] <= key) {
                start++;
            }
            //如果没有比基准值大的,则比较下一个,直到有比基准值大的,则交换位置
            if (arr[end] >= key) {
           		int temp = arr[end];
                arr[end] = arr[start];
                arr[start] = temp;
            }
        }
        //递归左边序列
        if(start>low)
        	sort(arr,low,start-1);
        //递归右边序列
      	if(end<high)
        	sort(arr,end+1,high);
        return arr;
    }

4. 冒泡排序

从左开始,依次比较相邻的两个元素,如果左边的元素大于右边的元素,二者进行交换位置,如此重复。


    public static int[]  bubbleSort(int[] arr){
    	//外层循环控制排序趟数
        for (int i = 0; i < arr.length-1; i++) { 
        	//内层循环控制每一趟排序次数
            for (int j = 0; j < arr.length-1-i; j++) { 
                if (arr[j] > arr[j+1] ){
                    int temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                }
            }
        }
        return arr;
    }

5.选择排序

第一次从arr[0]到arr[n-1]中选取最小值,与arr[0]交换。第二次从arr[1]到arr[n-1]中选取最小值,与arr[1]交换…第i次从arr[i-1]到arr[n-1]中选取最小值,与arr[i-1]交换…第n-1次从arr[n-2]到arr[n-1]中选取最小值,与arr[i-2]交换。总共通过n-1次,得到一个按排序从小到大排列的有序序列。

public static int[] selectionSort(int[] sourceArray) {

        // 首先判断数组长度是否小于2, 小于2直接返回
        if (sourceArray == null || sourceArray.length < 2) {
            return sourceArray;
        }

        // 最小变量下标
        int min, temp;
        // 比较次数
        int compareNum = 0;
        // 交换次数
        int swapNum = 0;

        // 除了最后一个数,每个数都需要跟他后面的数进行比较
        for (int i = 0; i < sourceArray.length - 1; i++) {

            // 假设第i位为最小的数
            min = i;

            // 由于最小的数都排在第 i 个数前面
            // 所以只需和第 i+1 个数及后续的数进行比较
            for (int j = i + 1; j < sourceArray.length; j++) {
                compareNum++;
                if (sourceArray[j] < sourceArray[min]) {
                    // 记录目前能找到的最小值元素的下标
                    min = j;
                }
            }

            // 将找到的最小值和i位置所在的值进行交换
            if (min != i) {
                // 交换位置
                swapNum++;
                temp = sourceArray[i];
                sourceArray[i] = sourceArray[min];
                sourceArray[min] = temp;
                System.out.println("第" + swapNum + "次交换后,数组为:" + JSON.toJSONString(sourceArray));
            }

        }

        System.out.println("比较次数:" + compareNum);
        System.out.println("交换次数:" + swapNum);
        return sourceArray;
    }

6. 平衡二叉树

平衡二叉树(Balanced Binary Tree),具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

7. LRU

LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

双向链表+Hash表
用哈希表存储链表结点和key值,能够做到O(1)访问链表任意结点,每次调用函数后将该结点放到链表最前方表示权重最大,最常访问,每次删除链表最后一个结点。要实现这个操作,我们需要的是有头结点和尾结点的双向链表。

public class Solution {
	//双向链表
    static class Node{
        int key , value;
        Node prev,next;
        public Node(int key , int value){
            this.key = key;
            this.value = value;
        }
    }
    
    private Map<Integer,Node> map = new HashMap<>();
    private Node head = new Node(-1,-1); //设置一个头
    private Node tail = new Node(-1,-1);//设置一个尾
    private int k;
    
    
    public int[] LRU (int[][] operators, int k) {
        // write code here
        this.k = k;
        head.next = tail;
        tail.prev = head;/先将链表首位相接,便于插入与删除
        ArrayList<Integer> list = new ArrayList<>();
        int cnt = 0;
        for(int i=0;i < operators.length ;i++){
            if(operators[i][0] == 1){
                set(operators[i][1],operators[i][2]);
            }else{
                list.add(get(operators[i][1]));
            }
        }
        int[] res = new int[list.size()];
        int i = 0;
        for(int val:list){
            res[i] = list.get(i);
            i++;
        }
        return res;
    }
    
    //插入数据
    public void set(int key,int value){
        //插入的数据已经存在,更新p节点的值,get()方法自动将节点位置调整到第一
        if(get(key) > -1){
            map.get(key).value = value;
        }else{
            if(map.size() == k ){
                int rk = tail.prev.key;
                tail.prev.prev.next = tail;
                tail.prev = tail.prev.prev;
                map.remove(rk);
            }
            Node node = new Node(key,value);
            map.put(key,node);
            removeToHead(node);
        }
    }
    
     //访问数据
    public int get(int key){
        if(map.containsKey(key)){//哈希表找到数据更新节点,并返回
            Node node = map.get(key);
            //将节点从原位置删除
            node.prev.next = node.next;
            node.next.prev = node.prev;
            //将节点插入到第一个位置
            removeToHead(node);
            return node.value;
        }
        return -1;
    }
    
    //将访问的节点放在第一个位置
    public void removeToHead(Node node){
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
        node.prev = head;
    }
    
}
8.红黑树

在这里插入图片描述

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蒙面侠1024

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值