面经
Java开发面经
一、Java基础
Java中八种基本数据类型?
byte(字节型8位)、short(短整型16位)、int(整型32位)、long(长整型64位)、float(单精度浮点型32位) double (双精度浮点型64位)、char(字符型16位)、boolean(布尔型1位)
static关键字?
“static”关键字表明一个成员变量或者是成员方法可以在没有所属的类的实例变量的情况下被访问。面向对象三大特性?
- 封装。就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
- 继承。使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。通过继承创建的新类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”或“超类”。
- 多态。它是指在父类中定义的属性和方法被子类继承之后,可以具有不同的数据类型或表现出不同的行为,这使得同一个属性或方法在父类及其各个子类中具有不同的含义。
重载和重写的区别?
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。
- 重写发生在子类与父类之间, 重写方法返回值和形参都不能改变,与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分。即外壳不变,核心重写!
- 重载是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。最常用的地方就是构造器的重载。
抽象类和接口的区别?
- 抽象类可以提供成员方法的实现细节,而接口中只能存在public abstract 方法;
- 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的;
- 一个类只能继承一个抽象类,而一个类却可以实现多个接口。
- 抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象。
==和equals的区别?
==常用于相同的基本数据类型之间的比较(值),也可用于相同类型的对象之间的比较(比较的是两个对象的引用,也就是判断两个对象是否指向了同一块内存区域);
equals方法主要用于两个对象之间,检测一个对象是否等于另一个对象。有两种情况:- 情况1,类没有覆盖equals()方法。则通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象,比较的是引用。
- 情况2,类覆盖了equals()方法。比较内容是否相等。
error和Exception的区别?
Exception:程序本身可以处理的异常,可以通过catch来进行捕获,通常遇到这种错误,应对其进行处理,使应用程序可以继续正常运行。
error:属于程序无法处理的错误 。例如,系统崩溃,内存不足,堆栈溢出等,编译器不会对这类错误进行检测,一旦这类错误发生,通常应用程序会被终止。BIO、NIO、AIO区别?
- BIO:同步并阻塞,在服务器中实现的模式为一个连接一个线程。也就是说,客户端有连接请求的时候,服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然这也可以通过线程池机制改善。BIO一般适用于连接数目小且固定的架构,这种方式对于服务器资源要求比较高,而且并发局限于应用中,是JDK1.4之前的唯一选择,但好在程序直观简单,易理解。
- NIO:同步并非阻塞,在服务器中实现的模式为一个请求一个线程,也就是说,客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到有连接IO请求时才会启动一个线程进行处理。NIO一般适用于连接数目多且连接比较短(轻操作)的架构,并发局限于应用中,编程比较复杂,从JDK1.4开始支持。
- AIO:异步并非阻塞,在服务器中实现的模式为一个有效请求一个线程,也就是说,客户端的IO请求都是通过操作系统先完成之后,再通知服务器应用去启动线程进行处理。AIO一般适用于连接数目多且连接比较长(重操作)的架构,充分调用操作系统参与并发操作,编程比较复杂,从JDK1.7开始支持。
浅拷贝和深拷贝的区别?
浅拷贝会复制对象的引用,而不是对象本身的内容。这意味着当对被复制对象进行修改时,原对象和复制对象都会受到影响。简单来说,浅拷贝只是复制了指针,而没有复制指针所指向的对象。
深拷贝则是完全复制了对象及其内容。这意味着当对被复制对象进行修改时,原对象和复制对象是互相独立的,互不影响。深拷贝会递归地复制指针所指向的对象,直到所有的对象都被复制。面向对象的五大基本原则?
- 单一职责原则:一个类只负责完成一个单一的职责或功能,不要把多个不同的职责耦合到一个类中,以便于提高代码的可读性、可维护性和可扩展性。
- 开闭原则:软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。即在不修改原有代码的前提下,通过扩展来增加新的功能。
- 里氏替换原则:子类对象能够替换其父类对象,并且在不影响程序正确性的前提下,扩展父类的功能。
- 接口隔离原则:客户端不应该被迫依赖于它们不使用的接口。
- 依赖倒置原则:高层模块不应该依赖底层模块,它们应该都依赖于抽象接口或抽象类。即面向接口编程,而不是面向实现编程。
值传递和引用传递?
- 值传递是指传递的参数是原始数据类型或者不可变对象,传递的是参数值的副本。这样在函数内部对参数进行修改不会影响到函数外部的变量值。在 Java 中,基本数据类型和字符串都是采用值传递的方式传递参数
- 引用传递是指传递的参数是可变对象或者引用类型,传递的是参数引用的副本,也就是指向堆内存中的同一对象。这样在函数内部对参数进行修改会影响到函数外部的变量值。在 Java 中,数组、集合、自定义对象等都是采用引用传递的方式传递参数。
需要注意的是,Java 中的引用传递并不是真正的引用传递,而是传递引用的副本。也就是说,实际传递的还是值,只不过这个值是指向对象的引用。
反射?
反射是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 Java 语言的反射机制。优缺点:能够运行时动态获取类的实例,提高灵活性;但使用反射性能较低,需要解析字节码,将内存中的对象进行解析。
线程安全和不安全的集合?
线程安全的集合:HashTable, ConcurrentHashMap, Vector, Stack
线程不安全的集合:HashMap, ArrayList, LinkedList, HashSet, TreeSet, TreeMapArrayList和LinkedList异同?
- 是否线程安全:ArrayList和LinkedList都不安全
- 底层数据结构:ArrayList底层是数组,LinkedList底层是双向循环链表
- 是否支持随机访问:ArrayList可以随机访问,LinkedList不能随机访问
- 插入和删除元素是否受位置影响:ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 LinkedList 采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似 O(1)而数组为近似 O(n)。
ArrayList和Vector的区别?
- Vector是线程安全的,ArrayList不是线程安全的。Vector在关键性的方法前面都加了synchronized关键字,来保证线程的安全性。如果有多个线程会访问到集合,那最好是使用 Vector,因为不需要我们自己再去考虑和编写线程安全的代码。
- ArrayList在底层数组不够用时在原来的基础上扩展0.5倍,Vector是扩展1倍,这样ArrayList就有利于节约内存空间。
ArrayList的扩容机制?
ArrayList新建初始化时容量为0,当插入第一个元素时容量初始化为10,扩容时容量为原来的1.5倍。Collections.sort和Arrays.sort的区别?
- 参数类型不同:Collections.sort()方法的参数是List类型,而Arrays.sort()方法的参数是数组类型。
- 实现方式不同:Collections.sort()方法是对List类型进行排序的,它内部使用了归并排序(Merge Sort)算法,而Arrays.sort()方法是对数组类型进行排序的,它内部使用了快速排序(Quick Sort)算法。
- 排序效率不同:由于采用了不同的排序算法,因此在数据量较小的情况下,快速排序算法比归并排序算法更快,而在数据量较大的情况下,归并排序算法比快速排序算法更快。
- 可以排序的元素类型不同:Collections.sort()方法可以对包含任何类型的元素的List进行排序,而Arrays.sort()方法只能对基本类型和实现了Comparable接口的类进行排序。
HashMap的底层数据结构是什么?
在JDK1.7 中,HashMap由“数组+链表”组成,在JDK1.8 中,由“数组+链表+红黑树”组成。当链表超过 8 且数组长度超过 64 才会转红黑树。
HashMap中key的存储索引怎么计算的?
首先根据key的值计算出hashcode的值,然后根据hashcode计算出hash值,最后通过hash&(length-1)计算得到存储的位置。HashMap扩容机制?
HashMap 在容量超过负载因子(0.75)所定义的容量之后,就会扩容。Java 里的数组是无法自动扩容的,方法是将 HashMap 的大小扩大为原来数组的两倍,并将原来的对象放入新的数组中。HashMap为什么线程不安全?
多线程的put可能导致元素的丢失。多线程同时执行 put 操作,如果计算出来的索引位置是相同的,那会造成前一个 key 被后一个 key 覆盖,从而导致元素的丢失。怎么使HashMap线程安全?
可以使用线程安全的 ConcurrentHashMap 类,或者在使用 HashMap 时采用同步机制,如使用 Collections.synchronizedMap 方法包装 HashMap 实例,使用对象锁来保证多线程场景下线程安全,但是这样会导致一定的性能损失。红黑树?
红黑树是一种自平衡的二叉查找树,是一种高效的查找树。红黑树具有良好的效率,它可在 O(logN) 时间内完成查找、增加、删除等操作。每个节点或者是红色的,或者是黑色的。根节点是黑色的。每个叶节点(NIL节点,空节点)是黑色的。如果一个节点是红色的,则它的子节点必须是黑色的。从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。通过这些特点,红黑树可以保证树的高度是平衡的,从而提高了树的搜索、插入、删除等操作的效率。
红黑树放弃了追求完全平衡,追求大致平衡,在与平衡二叉树的时间复杂度相差不大的情况下,保证每次插入最多只需要三次旋转就能达到平衡,实现起来也更为简单。而平衡二叉树追求绝对平衡,条件比较苛刻,实现起来比较麻烦,每次插入新节点之后需要旋转的次数不能预知。
ConcurrentHashMap底层数据结构?
JDK1.7之前是分段数组+链表,JDK1.8之后和HashMap一样,为数组+链表+红黑树。
JDK1.7之前使用分段锁,JDK1.8抛弃了原有的 Segment 分段锁,采用CAS + synchronized实现更加低粒度的锁,在链表头结点或红黑树根节点加锁。JDK动态代理
动态代理是什么?
动态代理是Java编程语言提供的一种机制,用于在运行时创建代理对象并动态处理方法调用。它是Java的反射机制的一种应用。动态代理可以在不事先知道具体接口的情况下创建代理对象,并将方法调用分派给一个或多个实际的对象。它主要用于在不修改现有代码的情况下为现有对象添加额外的功能,例如日志记录、性能监测、事务管理等。jdk动态代理,必须有接口,目标类必须实现接口, 没有接口时,需要使用cglib动态代理。动态代理的实现?
在JDK中,通过使用java.lang.reflect.Proxy
类和java.lang.reflect.InvocationHandler
接口来实现动态代理。
实现JDK动态代理的一般步骤:- 定义一个接口:首先需要定义一个接口,该接口定义了被代理对象(目标对象)的方法。
- 实现InvocationHandler接口:创建一个实现
InvocationHandler
接口的类,它负责实现代理对象的具体操作。该接口只有一个方法invoke()
,在代理对象的方法被调用时会被调用。
public Object invoke(Object proxy, Method method, Object[] args){}
参数:
—-Object proxy: jdk创建的代理对象,无需赋值。
—-Method method: 目标类中的方法,jdk提供method对象的
—-Object[] args: 目标类中方法的参数, jdk提供的。 - 创建代理对象:使用
Proxy.newProxyInstance()
方法创建代理对象。该方法接受三个参数:类加载器、要实现的接口列表和InvocationHandler
实例。- ClassLoader loader 类加载器,负责向内存中加载对象的。 使用反射获取对象的ClassLoader
类a , a.getCalss().getClassLoader(), 目标对象的类加载器 - Class<?>[] interfaces: 接口, 目标对象实现的接口,也是反射获取的。
- InvocationHandler h : 我们自己写的,代理类要完成的功能。
- ClassLoader loader 类加载器,负责向内存中加载对象的。 使用反射获取对象的ClassLoader
- 通过代理对象调用方法:通过代理对象调用方法时,实际的方法调用将被转发给
InvocationHandler
接口的实现类中的invoke()
方法。
在
invoke()
方法中,可以根据需要对方法调用进行处理,例如在方法调用前后添加日志记录、执行特定的逻辑等。在invoke()
方法中,可以使用Method
类的方法获取被调用的方法的信息,并使用Method.invoke()
方法来调用实际的方法。下面是一个简单的示例代码,演示了如何使用JDK动态代理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
interface Hello {
void sayHello();
}
class HelloImpl implements Hello {
public void sayHello() {
System.out.println("Hello, world!");
}
}
class MyInvocationHandler implements InvocationHandler {
private Object target;
public MyInvocationHandler(Object target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before method invocation");
Object result = method.invoke(target, args);
System.out.println("After method invocation");
return result;
}
}
public class Main {
public static void main(String[] args) {
Hello hello = new HelloImpl();
InvocationHandler handler = new MyInvocationHandler(hello);
Hello proxyHello = (Hello) Proxy.newProxyInstance(
hello.getClass().getClassLoader(),
hello.getClass().getInterfaces(),
handler);
proxyHello.sayHello();
}
}在这个示例中,我们首先定义了接口
Hello
和实现该接口的类HelloImpl
,其中HelloImpl
类包含了要执行的具体方法sayHello()
。然后,我们创建了实现了
InvocationHandler
接口的类MyInvocationHandler
,它在方法调用前后添加了日志记录。在
Main
类的main()
方法中,我们首先创建了被代理对象hello
,然后实例化了MyInvocationHandler
类,将hello
对象传递给它。接下来,我们使用
Proxy.newProxyInstance()
方法创建了代理对象proxyHello
。该方法接受类加载器、要实现的接口列表和InvocationHandler
实例作为参数。最后,我们通过代理对象
proxyHello
调用sayHello()
方法,实际的方法调用将被转发给MyInvocationHandler
的invoke()
方法,在该方法中添加了额外的处理逻辑。当我们运行这段代码时,将输出:
1
2
3Before method invocation
Hello, world!
After method invocation
二、Java并发
进程和线程的区别?
- 进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。
- 每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
- 一个进程内有多个线程,线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
- 同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的。
- 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
创建线程的三种方式?
- 实现Runnable接口:重写run()方法,没有返回值
- 实现Callable接口:重写call()方法,有返回值
- 继承Thread类
sleep()和wait()方法的异同?
sleep():是Thread类的静态方法,当前线程将睡眠n毫秒,线程进入阻塞状态。当睡眠时间到了,会解除阻塞,进入可运行状态,等待CPU的到来。睡眠不释放锁(如果有的话)。
wait():是Object的方法,必须与synchronized关键字一起使用,线程进入阻塞状态,当notify或者notifyall被调用后,会解除阻塞。但是,只有重新占用互斥锁之后才会进入可运行状态。睡眠时,会释放互斥锁。Thread类中yield方法的作用?
yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行。volatile的使用及原理?
volatile保证变量对所有线程的可见性:当volatile变量被修改,新值对所有线程会立即更新。或者理解为多线程环境下使用volatile修饰的变量的值一定是最新的。原理:volatile 通过编译器在生成字节码时,在指令序列中添加” 内存屏障 “来禁止指令重排序的。
volatile的内存屏障指的是在使用volatile变量时,JVM需要插入一些特殊的指令,以确保多个线程之间的内存可见性和有序性。这些指令通常被称为内存屏障(Memory Barrier)。
具体来说,volatile的内存屏障包括以下两种:- 写屏障(Write Barrier):当一个线程写入一个volatile变量时,JVM会在写操作之后插入一条写屏障指令,以确保该变量的修改能够被其他线程立即看到。
- 读屏障(Read Barrier):当一个线程读取一个volatile变量时,JVM会在读操作之前插入一条读屏障指令,以确保该变量的值是最新的(即保证了前面的写操作已经完成)。
使用volatile的内存屏障可以确保多个线程之间的内存可见性和有序性,避免了出现数据不一致的问题。但需要注意的是,volatile并不能保证原子性,如果需要保证操作的原子性,需要使用其他的同步机制,如synchronized关键字或者java.util.concurrent包中提供的原子类。
CAS?
CAS:全称 Compare and swap,即比较并交换,它是一条 CPU 同步原语。是一种硬件对并发的支持,针对多处理器操作而设计的一种特殊指令,用于管理对共享数据的并发访问。
CAS 包含了 3 个操作数:旧的预期值 A(刚开始读的),需要读写的内存值 V(修改前一刻读的),要修改的更新值 B。只有当V=A时才修改(说明没有被动过)。如果不等,重新进行比较和更新。ABA问题:并发环境下,假设初始条件是A,去修改数据时,发现是A就会执行修改。但是看到的虽然是A,中间可能发生了A变B,B又变回A的情况。此时A已经非彼A,数据即使成功修改,也可能有问题。可以通过AtomicStampedReference解决ABA问题,它,一个带有标记的原子引用类,通过控制变量值的版本来保证CAS的正确性。
ThreadLocal?
ThreadLocal,即线程本地变量。如果创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题。实现原理:通过一个ThreadLocalMap来实现的,每个线程都有一个ThreadLocalMap实例。在ThreadLocalMap中,每个ThreadLocal对象都有一个对应的Entry对象,Entry对象中保存了该线程对应的变量值。每个线程在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
ThreadLocal内存泄漏问题?
ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,⽽ value 是强引⽤。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。 假如我们不做任何措施的话,value 永远⽆法被GC 回收,这个时候就可能会产⽣内存泄露。如何解决内存泄漏问题?
使用完ThreadLocal后,及时调用remove()方法释放内存空间。AQS是什么?
AQS全称是AbstractQueuedSynchronizer,是Java并发包中用于实现锁、同步器等高级同步功能的基础框架。它提供了一种基于FIFO等待队列的锁获取与释放的机制,同时也提供了一些方法供开发者自定义同步器。AQS的原理可以简单概括为以下三个方面:
- 状态管理:AQS通过维护一个状态变量来管理同步器的状态,状态可以是任意Java数据类型,如int、boolean等,通常表示锁的持有次数或资源的数量等。
- 等待队列:AQS通过维护一个等待队列来管理处于阻塞状态的线程,等待队列是一个FIFO队列,由多个Node节点构成,Node节点保存了线程的引用和状态等信息。
- CAS操作:AQS使用CAS操作来保证并发安全性,CAS操作可以在不使用锁的情况下,实现对内存中的值的原子操作。AQS中使用CAS操作来实现锁的获取和释放操作,在获取锁时,它会使用CAS操作尝试获取锁,如果CAS操作失败,线程会被阻塞,并加入到等待队列中,等待其他线程释放锁并通知它,在释放锁时,它会使用CAS操作更新锁的状态,并从等待队列中唤醒一个或多个线程,使它们尝试重新获取锁。
线程池核心参数?
- corePoolSize:核心线程大小。
- maximumPoolSize :表示线程池中允许存在的最大线程数量。
- keepAliveTime:线程空闲时间,表示空闲线程的存活时间。
- unit:线程空闲时间的单位。
- workQueue:任务队列,用于保存尚未执行的任务。
- threadFactory:线程工厂,用于创建新的线程。
- handler:拒绝策略。
为什么使用线程池?
降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行。
提高线程的可管理性。可以进行统一的分配,调优和监控。线程池的拒绝策略有哪些?
- AbortPolicy:默认饱和策略。直接丢弃任务,并抛出RejectedExecutionException异常。
- DiscardPolicy:直接丢弃任务,什么都不做。
- CallerRunsPolicy:线程池之外的线程直接调用run方法执行。
- DiscardOldestPolicy:将workQueue队首任务丢弃,将最新线程任务重新加入队列执行。
线程池的工作方式?
- 初始化线程池:在创建线程池时,需要设置线程池的核心线程数、最大线程数、线程空闲时间、任务队列等参数,并初始化线程池中的线程。
- 提交任务:将需要执行的任务提交给线程池,线程池会根据一定的策略将任务分配给空闲线程或新建线程执行。
- 执行任务:线程池中的线程会从任务队列中获取任务并执行,执行完成后会返回线程池并等待下一次任务。
- 管理线程:线程池会根据线程使用情况动态调整线程池中的线程数量,以保证线程池中的线程数量始终处于一个合适的范围内。
- 销毁线程池:在程序结束时,需要调用线程池的shutdown()方法来销毁线程池,同时释放线程池中的线程资源。
常用的线程池?
- FixedThreadPool:线程池中的线程数量固定不变,适用于执行时间较短的任务,可以避免线程的频繁创建和销毁,提高了系统的性能。
- CachedThreadPool:线程池中的线程数量不固定,适用于执行时间较短的任务,当任务量增加时,会动态增加线程数量;当任务减少时,会动态减少线程数量。
- ScheduledThreadPool:线程池中的线程数量固定不变,适用于需要定时执行任务的场景,比如定时发送邮件、定时备份数据等。
- SingleThreadPool:线程池中只有一个线程,适用于需要顺序执行任务的场景,确保所有任务按照指定的顺序执行。
- WorkStealingPool:Java8 新增的线程池,线程数量可以动态调整,每个线程都有自己的任务队列,可以从其他线程的队列中“窃取”任务,提高线程的利用率。
怎么创建线程池?
方式一:通过ThreadPoolExecutor构造函数来创建(推荐)
方式二:通过 Executor 框架的工具类 Executors 来创建。线程池常用的阻塞队列有哪些?
- LinkedBlockingQueue : FixedThreadPool 和 SingleThreadExector 使用,容量为 Integer.MAX_VALUE ,可以认为是无界队列。由于 FixedThreadPool 线程池的线程数是固定的,所以没有办法增加特别多的线程来处理任务,这时就需要 LinkedBlockingQueue 这样一个没有容量限制的阻塞队列来存放任务。
- SynchronousQueue:CachedThreadPool使用。
- DelayedWorkQueue: ScheduledThreadPool 和 SingleThreadScheduledExecutor使用。
负载均衡算法?
轮询法:将请求按照顺序轮流分配到服务器上
随机法:随机选一台
哈希法:一致性哈希算法
一致性哈希算法:一致性哈希算法是一种用于解决分布式环境下的负载均衡问题的算法。它的核心思想是将所有的服务器节点和所有可能的请求都映射到一个固定大小的哈希环上,然后通过计算请求的哈希值来确定它所要访问的服务器节点。具体来说,一致性哈希算法将哈希环分成多个虚拟节点,并将每个服务器节点映射到多个虚拟节点上,从而实现负载均衡和高可用性。当有新的请求到达时,计算请求对应的哈希值,并将该值映射到哈希环上的一个位置,然后沿着环顺时针方向查找到第一个服务器节点,将请求发送到该节点上。
在一致性哈希算法中,当一个服务器节点宕机或者新增一个服务器节点时,只需要重新计算受影响的节点周围的请求,而不需要重新计算整个哈希环,从而大大减少了计算量和数据迁移的成本。
一致性哈希算法被广泛应用于分布式缓存、负载均衡、分布式存储等领域。它具有简单、高效、容错性强等优点,可以有效地提高系统的性能和可伸缩性。
三、JVM
JVM内存区域?
- 程序计数器:线程私有的,是一块很小的内存空间,作为当前线程的行号指示器,用于记录当前虚拟机正在执行的线程指令地址;
- 虚拟机栈:线程私有的,每个方法执行的时候都会创建一个栈帧,用于存储局部变量表、操作数、动态链接和方法返回等信息,当线程请求的栈深度超过了虚拟机允许的最大深度时,就会抛出StackOverFlowError;
- 本地方法栈:线程私有的,保存的是native方法的信息,当一个jvm创建的线程调用native方法后,jvm不会在虚拟机栈中为该线程创建栈帧,而是简单的动态链接并直接调用该方法;
- 堆:java堆是所有线程共享的一块内存,几乎所有对象的实例和数组都要在堆上分配内存,因此该区域经常发生垃圾回收的操作;
堆被划分为两个区域:新生代和老年代。新生代又划分为三个区域:一个Eden区和两个Survivor区(From Survivor和To Survivor)。新的对象先放在新生代的Eden区,Survivor区作为Eden区和老年代区的缓冲,在survivor区的对象经历若干次收集仍然存活的,就会被放入老年代中。
进入老年代的条件:大对象直接进入老年代,长期存活的对象将进入老年代:虚拟机给每个对象一个对象年龄(Age)计数器,大部分情况,对象都会首先在 Eden 区域分配。如果对象在 Eden 出生并经过第一次 Minor GC(Minor GC只收集新生代GC)后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间,并将对象年龄设为 1(Eden 区->Survivor 区后对象的初始年龄变为 1)。对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。 - 方法区:存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据。即永久代,在jdk1.8中不存在方法区了,被元数据区替代了,原方法区被分成两部分;1:加载的类信息,2:运行时常量池;加载的类信息被保存在元数据区中,运行时常量池保存在堆中;
垃圾回收的算法?
- 标记清楚法:利用可达性去遍历内存,把存活对象和垃圾对象进行标记;在遍历一遍,将所有标记的对象回收掉; 特点:效率不行,标记和清除的效率都不高;标记和清除后会产生大量的不连续的空间碎片,可能会导致之后程序运行的时候需分配大对象而找不到连续分片而不得不触发一次GC;
- 标记整理法:利用可达性去遍历内存,把存活对象和垃圾对象进行标记; 将所有的存活的对象向一段移动,将端边界以外的对象都回收掉; 特点:适用于存活对象多,垃圾少的情况;需要整理的过程,无空间碎片产生;标记整理法需要将所有存活的对象复制到一段新的内存区域中,这会产生额外的开销,并且可能会导致频繁的内存分配和释放操作。
- 复制算法:将内存按照容量大小分为大小相等的两块,每次只使用一块,当一块使用完了,就将还存活的对象移到另一块上,然后在把使用过的内存空间移除; 特点:不会产生空间碎片;内存使用率极低;
- 分代收集法:根据内存对象的存活周期不同,将内存划分成几块,java虚拟机一般将内存分成新生代和老生代,在新生代中,有大量对象死去和少量对象存活,所以采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集;老年代中因为对象的存活率极高,没有额外的空间对他进行分配担保,所以采用标记清理或者标记整理算法进行回收;
如何判断对象是否存活?
- 引用计数法: 给每一个对象设置一个引用计数器,当有一个地方引用该对象的时候,引用计数器就+1,引用失效时,引用计数器就-1;当引用计数器为0的时候,就说明这个对象没有被引用,也就是垃圾对象,等待回收; 缺点:无法解决循环引用的问题,当A引用B,B也引用A的时候,此时AB对象的引用都不为0,此时也就无法垃圾回收,所以一般主流虚拟机都不采用这个方法;
- 可达性分析法: 从一个被称为GC Roots的对象向下搜索,如果一个对象到GC Roots没有任何引用链相连接时,说明此对象不可用,在java中可以作为GC Roots的对象有以下几种:
- 虚拟机栈中引用的对象
- 方法区类静态属性引用的变量
- 方法区常量池引用的对象
- 本地方法栈JNI引用的对象
CMS垃圾回收器?
一种以低停顿时间为目标的垃圾回收器。初始标记阶段:该阶段是 STW 阶段,暂停所有其他线程,用于标记 GC Root 能直接关联到的对象。
并发标记阶段:该阶段GC与应用程序线程并发执行,标记所有 GC Root 可达的对象,并将这些对象标记为“存活”。
重新标记阶段:该阶段是 STW 阶段,用于标记在并发标记阶段中因应用程序线程的执行而产生的新对象,以及在并发标记阶段中发生变化的对象。标记完成后,GC 线程会立即停止所有应用程序的线程,进入下一阶段。
并发清除阶段:该阶段与应用程序线程并发执行,对未被标记为“存活”的对象进行清除。
问题:无法清理浮动垃圾,内存碎片问题(CMS是一款基于“标记-清除”算法实现的回收器)。
G1回收器?
通过将整个堆空间划分为多个 Region 区域,并采用增量式回收方式初始标记:标记GC ROOT能关联到的对象,需要STW(停顿线程);
并发标记:从GCRoots的直接关联对象开始遍历整个对象图的过程,扫描完成后还会重新处理并发标记过程中产生变动的对象;
最终标记:短暂暂停用户线程,再处理一次,需要STW;
筛选回收:更新Region的统计数据,对每个Region的回收价值和成本排序,根据用户设置的停顿时间制定回收计划。再把需要回收的Region中存活对象复制到空的Region,同时清理旧的Region。需要STW。
类加载?类加载过程?
类加载:虚拟机把描述类的数据加载到内存里面,并对数据进行校验、解析和初始化,最终变成可以被虚拟机直接使用的class对象;
过程:- 加载,加载分为三步: 1、通过类的全限定性类名获取该类的二进制流; 2、将该二进制流的静态存储结构转为方法区的运行时数据结构; 3、在堆中为该类生成一个class对象;
- 验证:验证该class文件中的字节流信息复合虚拟机的要求,不会威胁到jvm的安全;
- 准备:为class对象的静态变量分配内存,初始化其初始值;
- 解析:该阶段主要完成符号引用转化成直接引用;
- 初始化:到了初始化阶段,才开始执行类中定义的java代码;初始化阶段是调用类构造器的过程;
什么是双亲委派模型?
双亲委派模型是一种类加载机制。当一个类加载器需要加载一个类时,它会先向其父类加载器发起请求。如果父类加载器可以加载该类,则直接返回Class对象。如果父类加载器无法加载该类,则将任务再次委派给它的父类加载器,一直到顶层的启动类加载器,如果仍然无法完成加载任务,那么这个类加载器会尝试自己去加载这个类。好处:使用双亲委派的好处是,保证同一个类在不同的类加载器中只会被加载一次,避免了重复加载,同时也可以保证类的安全性。
JVM调优命令?
- jps:查看程序对应的 PID。
- jmap:查找线程内存使用情况。
- jstack:查看线程情况。
- jstat:调优重点,查看 GC 情况。用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据
四、操作系统
协程?
协程是一种用户态的轻量级线程,它由程序员控制,可以在同一线程中实现多个协程的切换。协程之间的切换比线程之间的切换更快,因为协程的切换只需要保存少量的状态信息,不需要切换上下文。
协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态中执行)。
线程进程都是同步机制,而协程则是异步机制;
线程是抢占式,而协程是非抢占式的。
协程能保留上一次调用的状态,每次重入时,就相当于进入上一次调用的状态;进程间通信方式有哪些?
- 管道:半双工的通信,数据只能单向流动,只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。速度慢,容量有限;匿名管道:单向的,只能在有亲缘关系的进程间通信;命名管道:以磁盘文件的方式存在,可以实现本机任意两个进程通信。
- 消息队列:消息队列是一种在进程间传递数据的方式,它通过消息缓冲区实现数据的交换。不同进程之间可以通过消息队列发送和接收消息,并且可以设置优先级等参数。
- 信号量:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。因此,主要作为进程之间和线程之间的同步手段。
- 共享内存:共享内存是一种最快的进程间通信方式,它可以实现进程之间的数据共享。不同进程可以访问同一个共享内存区域,并且可以通过信号量等机制进行同步和互斥。
- 套接字:套接字是一种在网络上进行进程间通信的方式,它可以在不同的机器和进程之间传递数据。套接字可以实现不同进程之间的数据传输和通信,是网络编程中常用的通信方式。
死锁产生的四个必要条件?
- 互斥条件:一个资源一次只能被一个进程使用
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得资源保持不放
- 不剥夺条件:进程获得的资源,在未完全使用完之前,不能强行剥夺
- 循环等待条件:若干进程之间形成一种头尾相接的环形等待资源关系
什么是虚拟内存?
虚拟内存就是让物理内存扩充成更大的逻辑内存,从而让程序获得更多的可用内存。虚拟内存使用部分加载的技术,让一个进程或者资源的某些页面加载进内存,从而能够加载更多的进程,甚至能加载比内存大的进程,这样看起来好像内存变大了,这部分内存其实包含了磁盘或者硬盘,并且就叫做虚拟内存。实现方式:请求分页存储管理,请求分段存储管理,请求段页式存储管理。
IO多路复用?
O多路复用是一种高效的IO模型,可以同时监视多个IO流,当有数据到来时,系统可以通知相应的进程或线程进行处理。常见的IO多路复用技术包括select、poll、epoll等。IO多路复用的实现原理是利用操作系统内核提供的select、poll、epoll等系统调用,将多个IO流的状态(读/写/异常)注册到一个等待队列中,然后进入等待状态,直到有数据到来或者超时。当有数据到来时,操作系统会通知应用程序哪些IO流已经准备好,然后应用程序可以进行相应的读写操作。
select、poll 和 epoll 之间的区别?
处理的最大文件描述符数量不同: select和poll的最大文件描述符数量都是固定的,通常为1024,而epoll没有这个限制,可以支持大量的文件描述符。
检查I/O流状态的效率不同:select和poll在处理大量的文件描述符时,效率会随着文件描述符数量的增加而下降,因为每次系统调用都要检查所有的文件描述符。而epoll使用了一种基于事件驱动的机制,内核里维护了一个「链表」来记录就绪事件。当某个 socket 有事件发生时,通过回调函数内核会将其加入到这个就绪事件列表中,这样可以大大减少系统调用的次数,提高效率。
对于内核空间和用户空间的交互方式不同:select和poll每次系统调用都需要将所有的文件描述符集合从用户空间复制到内核空间,这样会带来一定的开销。而epoll 在内核里使用「红黑树」来关注进程所有待检测的 Socket.
支持的I/O事件类型不同: select和poll只支持可读、可写和异常事件的监视,而epoll支持这三种事件以及ET模式和LT模式(边缘触发和水平触发模式),ET模式只有在状态发生变化时才会通知用户程序,而LT模式则是在有数据可读时就会通知。
总体而言,epoll相对于select和poll具有更高的性能和更灵活的可扩展性,能够处理大量的文件描述符并且支持多种I/O事件类型,因此在高并发的网络编程中被广泛应用。
常用的linux命令
ls:列出目录中的文件和子目录。
mkdir:创建一个新目录。
cp:复制文件或目录
rm:删除文件或目录。
mv:移动或重命名文件或目录。
cat:连接文件并打印到标准输出。
tail:输出文件的末尾内容(查看日志)。
grep:在文件中搜索指定的模式。
ps:显示当前系统中运行的进程。
kill:向指定进程发送信号以终止其执行。
五、计算机网络
TCP和UDP的区别?
- TCP是面向连接的可靠传输,UDP是无连接不可靠传输
- TCP有序,UDP无序
- TCP传输速度慢,UDP快
- UDP支持一对一,一对多,多对一和多对多交互通信,TCP只能是一对一通信
- UDP面向报文传输,TCP面向字节流传输
应用场景:
TCP:HTTP、FTP、SMTP、TELNETUDP:DNS、TFTP、SNMP、NFS
TCP三次握手?
① 第一次握手:客户端请求建立连接,向服务端发送一个同步报文(SYN=1),同时选择一个随机数 seq = x 作为初始序列号,并进入SYN_SENT状态,等待服务器确认。
② 第二次握手:服务端收到连接请求报文后,如果同意建立连接,则向客户端发送同步确认报文(SYN=1,ACK=1),确认号为 ack = x + 1,同时选择一个随机数 seq = y 作为初始序列号,此时服务器进入SYN_RECV状态。
③ 第三次握手:客户端收到服务端的确认后,向服务端发送一个确认报文(ACK=1),确认号为 ack = y + 1,序列号为 seq = x + 1,客户端和服务器进入ESTABLISHED状态,完成三次握手。注意:第三次握手是可以携带数据的,前两次握手是不可以携带数据的
TCP为什么需要三次握手,而不是两次?
- 防止已过期的连接请求报文突然又传送到服务器,因而产生错误和资源浪费;
- 三次握手才能让双方均确认自己和对方的发送和接收能力都正常:第一次服务端确定客户端发送和服务端接受正常,第二次客户端确定客户端发送和接受正常以及服务端发送和接收正常,第三次服务端确定自己发送和接收正常以及客户端发送和接收正常。
- 同步双方初始序列号。去除重复的数据,根据数据包的序列号按序接收。
TCP 的四次挥手过程?
① 第一次挥手:客户端向服务端发送连接释放报文(FIN=1,ACK=1),初始序列号seq = m,主动关闭连接,同时等待服务端的确认。
② 第二次挥手:服务端收到连接释放报文后,立即发出确认报文(ACK=1),序列号 seq = k,确认号 ack = m + 1。
③ 第三次挥手:服务端向客户端发送连接释放报文(FIN=1,ACK=1),序列号 seq = w,主动关闭连接,同时等待服务端的确认。
④ 第四次挥手:客户端收到服务端的连接释放报文后,立即发出确认报文(ACK=1),序列号 seq = m + 1,确认号为 ack = w + 1。此时,客户端就进入了 TIME-WAIT 状态。注意此时客户端到 TCP 连接还没有释放,必须经过 2*MSL(最长报文段寿命)的时间后,才进入 CLOSED 状态。为什么连接的时候是三次握手,关闭的时候却是四次握手?
服务器在收到客户端的 FIN 报文段后,可能还有一些数据要传输,所以不能马上关闭连接,但是会做出应答,返回 ACK 报文段。接下来可能会继续发送数据,在数据发送完后,服务器会向客户单发送 FIN 报文,表示数据已经发送完毕,请求关闭连接。TCP四次挥手后,为什么要time_wait 2MSL?等1MSL为啥不可以?
确保 ACK 报文能够到达服务端,从而使服务端正常关闭连接。第四次挥手时,客户端第四次挥手的 ACK 报文不一定会到达服务端。服务端会超时重传 FIN/ACK 报文,此时如果客户端已经断开了连接,那么就无法响应服务端的二次请求,这样服务端迟迟收不到 FIN/ACK 报文的确认,就无法正常断开连接。MSL 是报文段在网络上存活的最长时间。客户端等待 2MSL 时间,即「客户端 ACK 报文 1MSL 超时 + 服务端 FIN 报文 1MSL 传输」
防止已失效的连接请求报文段出现在之后的连接中。
TCP如何保证传输可靠性?
- 建立连接:通过三次握手建立连接,保证连接实体真实存在;
- 序列号机制:保证数据是按序、完整到达;
- 数据校验:TCP报文头有校验和,用于校验报文是否损坏;
- 超时重传:如果发送一直收不到应答,可能是发送数据丢失,也可能是应答丢失,发送方再等待一段时间之后都会进行重传。
- 流量控制:当接收方来不及处理发送方的数据,能通过滑动窗口,提示发送方降低发送的速率,防止包丢失。
- 拥塞控制:网络层拥堵造成的拥塞,包括慢启动,拥塞避免,快速重传三种机制
HTTP1.0和HTTP1.1的区别?
- HTTP1.0使用短连接,HTTP1.1使用长连接,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟。
- 缓存处理:HTTP 1.1中缓存处理更加优化,增加了缓存控制机制,如Cache-Control、If-Modified-Since等,使得缓存机制更加灵活,有助于提高网络传输效率。
- 错误处理:HTTP 1.1中增加了更多的状态码,使得错误处理更加细致,例如401表示未授权,403表示禁止访问等。
- 带宽优化:HTTP 1.1支持断点续传,可以在传输过程中动态调整数据块的大小,使得带宽利用更加高效。
HTTP1.1和HTTP2.0和区别?
- 新的传输格式:2.0使用二进制格式报文,1.1依然使用基于文本格式。
- 多路复用:HTTP 2.0支持多路复用,即可以在单个连接上同时传输多个请求和响应。这可以避免HTTP 1.1中的队头阻塞问题,提高了网络传输效率。
- 头部压缩:HTTP 2.0引入了头部压缩机制,可以将请求和响应的头部信息压缩,减少了网络传输的开销,提高了网络传输效率。
- 服务器推送:HTTP 2.0支持服务器推送,即服务器可以主动将客户端需要的资源推送到客户端,减少了客户端的请求次数,提高了页面加载速度。
HTTP3?
HTTP/3是一种基于QUIC协议的新一代HTTP协议,旨在提高网络传输效率和安全性。它相对于HTTP/1.1和HTTP/2.0有以下特点:- 基于UDP协议:HTTP/3是基于QUIC协议的,而QUIC协议是基于UDP协议的。与TCP协议相比,UDP协议的连接建立速度更快,且支持多路复用和流量控制,同时减少了TCP协议的队头阻塞问题。
- 快速连接建立:HTTP/3使用了0-RTT(零往返时间)连接建立,可以在不需要等待握手过程的情况下建立连接,从而减少了网络延迟。
- 快速恢复:HTTP/3支持快速恢复机制,可以在网络丢失数据包时更快地进行恢复。
TCP的拥塞控制?
TCP的拥塞控制是一种在TCP协议中用于控制网络拥塞的机制。当网络发生拥塞时,拥塞控制机制可以自适应地降低发送方的数据传输速率,以避免网络拥塞问题的发生。- 慢启动:在TCP建立连接时,发送方会以较慢的速度发送数据,每经过一个往返时间,就将发送窗口的大小翻倍,直到达到一个阈值。
- 拥塞避免:在发送方的数据发送速度达到一个阈值时,将会进入拥塞避免状态。此时,每经过一个往返时间,发送方将仅仅增加发送窗口的大小1个单位,以避免网络拥塞的问题。
- 快速重传:当发送方接收到接收方的重复确认消息时,说明可能发生了数据包的丢失。发送方将快速重传这个丢失的数据包,以避免等待超时,从而降低网络拥塞的程度。
- 快速恢复:当发送方接收到接收方的重复确认消息时,说明可能发生了数据包的丢失。发送方将立即进入快速恢复状态,将发送窗口的大小减半,并发送丢失数据包之后的所有数据包,以提高数据传输的速度。
状态码301和302区别?
301适合永久重定向,表示所请求的资源已经永久地转移到新的位置。302用来做临时重定向,表示所请求的资源临时地转移到新的位置。HTTP的常见状态码?
- 1xx(信息性状态码):表示服务器已经接收到了客户端的请求,并且正在处理中,比如100(Continue)表示客户端应该继续发送请求。
- 2xx(成功状态码):表示服务器已经成功处理了请求,比如200(OK)表示请求成功,201(Created)表示请求成功并且服务器创建了新的资源。
- 3xx(重定向状态码):表示需要客户端进一步的操作才能完成请求,比如301(Moved Permanently)表示请求的资源已经永久移动到新的URL,需要客户端更新书签。
- 4xx(客户端错误状态码):表示客户端发送的请求有问题,比如404(Not Found)表示请求的资源不存在,400(Bad Request)表示请求格式有误。
- 5xx(服务器错误状态码):表示服务器处理请求时发生了错误,比如500(Internal Server Error)表示服务器内部错误,无法完成请求。
GET和POST请求的区别?
- 参数传递方式:GET请求通过URL参数传递参数,而POST请求通过请求体传递参数。
- 安全性:GET请求的参数暴露在URL中,可能被缓存或者记录在日志中,安全性较低;而POST请求的参数在请求体中,相对安全一些。
- 请求的数据大小:GET请求由于参数在URL中,所以传递的数据大小有限制,一般不超过2KB,而POST请求则没有这个限制。
- GET和POST最大的区别主要是GET请求是幂等性的,POST请求不是,这个是它们本质区别。也就是说GET多次请求得到的结果都是一样的,不会对服务器造成影响,因此可以被缓存;而POST请求一般不会被缓存,因为会对服务器造成副作用,如重复提交表单等。
HTTP和HTTPS的区别?
HTTP HTTPS 安全性 明文传输,不安全 采用SSL/TSL加密技术,安全 端口号 80 443 证书 不需要 服务端需要向客户端提供数字证书,证书中包含了服务器的公钥,
客户端会根据证书来验证服务器的身份是否合法,可以防止中间人攻击。协议 TCP TCP,SSL(运行在TCP之上) 资源消耗、性能 较少 加密处理,资源消耗更多,握手时间更长 HTTTPS加密流程?
① 客户端向服务器发起HTTPS请求。
② 服务器向客户端返回数字证书,证书中包含了服务器的公钥。
③ 客户端会验证服务器返回的证书是否合法。
④ 客户端生成一个随机的对称加密密钥,用于对数据进行加密和解密。
⑤ 客户端使用服务器的公钥对生成的随机密钥进行加密,然后将加密后的密钥发送给服务器。
⑥ 服务器使用自己的私钥对客户端发送的加密后的密钥进行解密,得到随机密钥。
⑦ 客户端和服务器使用随机密钥对通信数据进行加密和解密,保证了数据在传输过程中的安全性。浏览器中的一个网址如何去返回给你界面,在网络中经历了什么?
- DNS域名解析:将域名解析为对应的IP地址
- 浏览器通过IP地址和端口号建立与服务器的TCP连接,进行数据传输。
- 建立TCP连接后,浏览器会向服务器发送HTTP请求
- 服务器处理请求并返回响应:服务器接收到请求后,会根据请求内容,处理请求并生成响应。
- 服务器返回响应后,浏览器会解析响应,并呈现给用户。
DNS的域名解析过程?
- 浏览器首先会检查本地缓存中是否已经有该域名的解析结果。如果有,就直接使用本地缓存中的IP地址,不需要进行后续的DNS解析。
- 如果本地缓存中没有该域名的解析结果,浏览器会向本地DNS服务器发起请求,要求解析该域名的IP地址。
- 本地DNS服务器会先检查自己的缓存,如果没有,就向根DNS服务器查询。
- 根DNS服务器会返回一个顶级域DNS服务器的IP地址给本地DNS服务器。
- 本地DNS服务器再向顶级域DNS服务器查询,顶级域DNS服务器会返回一个权威DNS服务器的IP地址给本地DNS服务器。
- 本地DNS服务器再向权威DNS服务器查询,权威DNS服务器会返回该域名对应的IP地址给本地DNS服务器,并告知其可以缓存一段时间。
- 本地DNS服务器将收到的IP地址返回给浏览器,并将其缓存起来以备后用。
- 浏览器收到IP地址后,就可以与目标主机建立连接并交换数据。
拆包粘包?
粘包是指多个数据块被组合成一个大的数据包发送到接收方,而接收方只能一次性接收到所有数据块,导致接收方无法区分多个数据块之间的边界,从而导致数据的解析和处理出现问题。拆包是指一个数据块被分成多个小的数据包发送到接收方,接收方需要将这些小的数据包组合成一个完整的数据块,但由于小数据包之间可能存在网络延迟或丢失,从而导致接收方在组合数据块时缺少部分数据或者将多个数据块错误组合。
造成粘包和拆包的原因?
- 发送方发送数据速度过快,接收方没有及时接收到数据,从而导致多个数据包被组合成一个大的数据包。
- 接收方处理数据速度过慢,导致接收方在处理完一个数据包后还没有及时处理下一个数据包,从而多个数据包被组合成一个大的数据包。
解决方式:
- 消息定长:发送方在发送数据时,将每个数据块的大小固定为一个固定值,接收方在接收数据时,按照这个固定值进行分包,从而避免了粘包和拆包问题。
- 在包尾部增加回车或者空格符等特殊字符进行分割。
- 使用消息边界:在传输协议中,明确定义消息边界,发送方和接收方都遵循这个边界来分割数据块,从而避免了粘包和拆包问题。
- 使用应用层协议。
八种HTTP请求?
GET 获取资源,用于请求指定的资源 POST 提交数据,用于提交指定资源的数据 PUT 更新资源,用于更新指定资源 DELETE 删除资源,用于删除指定资源 HEAD 获取报头,用于获取与GET请求相同的报头,但不返回正文部分 OPTIONS 获取支持的请求方式,用于获取服务器支持的请求方法 CONNECT 建立连接隧道,用于建立与资源的双向连接 TRACE 跟踪路径,用于将服务器收到的请求信息返回给客户端,用于测试和调试 Session和Cookie的区别?
Cookie:服务器发送到用户浏览器并保存在本地的一小块数据,客户端每次向服务器发送请求时都将Cookie附加到请求头中。这样服务器就可以使用Cookie中的信息来标识客户端。
Session:是服务器端的一种机制,它可以在服务器端存储客户端的状态信息。在客户端第一次请求服务器时,服务器会为该客户端创建一个Session,并分配一个唯一的Session ID。然后服务器会将该Session ID 发送到客户端,并在客户端的浏览器中创建一个Cookie来存储该Session ID。客户端在每次向服务器发送请求时,都会携带该Cookie,这样服务器就可以根据Session ID 来识别客户端,并获取客户端的状态信息。区别:
- 作用范围不同,Cookie 保存在客户端(浏览器),Session 保存在服务器端。
- 生命周期不同:Cookie有过期时间,可以设置在一段时间之后自动失效,而Session一般在用户关闭浏览器或者一定时间内没有访问后就会失效。
- 安全性不同:Cookie数据存储在客户端,如果被恶意获取,那么用户的信息就会暴露。而Session存储在服务端,只有服务端可以访问,相对来说更加安全。
- 存储容量不同:Cookie一般只能存储4K左右的数据,而Session可以存储更多的数据。
如何考虑分布式 Session 问题?
在互联网公司为了可以支撑更大的流量,后端往往需要多台服务器共同来支撑前端用户请求,那如果用户在 A 服务器登录了,第二次请求跑到服务 B 就会出现登录失效问题。分布式 Session 一般会有以下几种解决方案:
- 客户端存储:直接将信息存储在cookie中,cookie是存储在客户端上的一小段数据,客户端通过http协议和服务器进行cookie交互,通常用来存储一些不敏感信息
- Session 复制:将 Session 复制到多个服务器,每个服务器都能够访问相同的数据,以保证 Session 在集群中的一致性。这种方案简单易实现,但是当集群规模增大时,Session 复制的网络负载也会增加,影响性能。
- 共享 Session:将 Session 数据统一存储在外部缓存中,比如 Redis、Memcached 等。每个服务器都可以从外部缓存中获取 Session 数据,以保证 Session 在集群中的一致性。这种方案可以减少网络负载,但是需要对缓存系统进行额外的部署和维护。
输入网址,服务端这边怎么处理请求的?
- 解析请求:服务端会解析浏览器发送的请求,包括HTTP请求头部和请求正文(如果有),以确定请求的方法、URL、HTTP版本等信息。
- 处理请求:根据请求的方法和URL,服务端会根据自己的业务逻辑进行处理,比如读取数据库、生成动态页面、获取文件等。如果请求的是静态资源,服务端会直接将该资源返回给客户端。
- 生成响应:服务端会生成一个HTTP响应,包括HTTP响应头部和响应正文。响应头部包括响应状态码、响应类型等信息,响应正文包括服务端返回的数据(如HTML、CSS、JavaScript等)。
- 发送响应:服务端将生成的HTTP响应发送给浏览器,浏览器会解析响应并显示相应的页面或数据。
SYN泛洪?
SYN 泛洪是一种常见的网络攻击方式,它利用 TCP 协议中的三次握手机制来消耗网络带宽和服务器资源,从而导致服务不可用或响应变慢。
在 TCP 协议中,连接建立时需要进行三次握手,即客户端向服务器发送 SYN 报文,服务器收到后返回 SYN+ACK 报文,最后客户端再发送 ACK 报文,连接才能建立。而在 SYN 泛洪攻击中,攻击者向服务器发送大量的 SYN 报文,由于服务器需要维护每个连接的状态信息,因此会占用大量的服务器资源,从而导致服务器响应变慢或者崩溃。DDoS?
DDoS(Distributed Denial of Service)是指分布式拒绝服务攻击,它是一种通过利用大量分布在不同地方的计算机向目标服务器发起大量请求,从而使目标服务器瘫痪的攻击方式。
最基本的DOS攻击过程如下:- 客户端向服务端发送请求链接数据包。
- 服务端向客户端发送确认数据包。
- 客户端不向服务端发送确认数据包,服务器一直等待来自客户端的确认。
预防方法有:减少SYN timeout时间,限制同时打开的SYN半连接数目。
负载均衡算法有哪些?
多台服务器以对称的方式组成一个服务器集合,每台服务器都具有等价的地位,能互相分担负载。
轮询法:将请求按照顺序轮流的分配到服务器上。不能发挥某些高性能服务器的优势。
随机法:随机获取一台,和轮询类似。
哈希法:通过ip地址哈希化来确定要选择的服务器编号。
加权轮询:根据服务器性能不同加权。
六、MySQL
什么是关系型数据库,什么是非关系型数据库,这里的关系指的是什么?
关系型数据库(RDBMS)是一种基于关系模型的数据库系统。在关系型数据库中,数据以表格的形式组织,其中表格由行和列组成。每个表格都有一个唯一的标识符,称为主键,用于唯一地标识表格中的每一行。关系型数据库使用结构化查询语言(SQL)来管理和查询数据。
非关系型数据库(NoSQL)是一种不基于关系模型的数据库系统。它们采用了不同的数据组织方式,如键值对、文档、列族或图形等。非关系型数据库通常更加灵活,能够处理大量结构化和非结构化数据,适用于分布式环境和大规模数据存储。关系指的是数据之间的关系。在关系型数据库中,不同表格之间可以通过共享相同的列或键来建立关系。这些关系可以用于在多个表格之间进行连接和查询,从而实现数据的一致性和完整性。需要注意的是,”关系”在这里指的是数据之间的关联关系,而不是指数据库中存储的关系数据。
关系型数据库和非关系型数据库之间的主要区别在于数据的组织方式和操作方式。关系型数据库更适用于需要强调数据一致性和复杂查询的应用场景,例如金融系统或企业管理系统。非关系型数据库则更适合需要处理大量动态数据、具有高度扩展性和灵活性的应用场景,例如社交媒体分析或实时日志处理。
mysql执行一条select语句是如何执行的?
- 客户端通过 TCP 连接发送连接请求到 MySQL 连接器,连接器会对该请求进行权限验证及连接资源分配。
- 查缓存。(判断缓存是否命中时,MySQL 不会进行解析查询语句,而是直接使用 SQL 语句和客户端发送过来的其他原始信息)
- 语法分析(SQL 语法是否写错了)。 如何把语句给到预处理器,检查数据表和数据列是否存在,解析别名看是否存在歧义。
- 优化。是否使用索引,生成执行计划。
- 交给执行器,将数据保存到结果集中,同时会逐步将数据缓存到查询缓存中,最终将结果集返回给客户端。
MyISAM 和 InnoDB 的区别有哪些?
- InnoDB 支持事务,MyISAM 不支持
- InnoDB 支持外键,MyISAM 不支持
- InnoDB 是聚集索引,MyISAM 是非聚集索引
- Innodb 不支持全文索引,而 MyISAM 支持全文索引,查询效率上 MyISAM 要高
- InnoDB 不保存表的具体行数,MyISAM 用一个变量保存了整个表的行数
- InnoDB 支持行级锁和表级锁,默认为行级锁,MyISAM 采用表级锁
MySQL的聚蔟索引和非聚蔟索引?
聚簇索引:将数据存储与索引放到了一块,将表的数据存放在索引的叶子节点中,一个表只能有一个聚簇索引。
非聚簇索引:将数据存储于索引分开结构,非聚族索引的叶子节点存储的是数据位置 。
通常情况下, 主键索引(聚簇索引)查询只会查一次,而非主键索引(非聚簇索引)需要回表查询多次。当然,如果是覆盖索引的话,查一次即可(一个索引包含(覆盖)所有需要查询字段的值,被称之为”覆盖索引”)什么是索引下推?
索引下推是MySQL在执行查询时的一种优化方式,它可以将查询条件下推到存储引擎层面进行计算,减少不必要的数据读取和传输,从而提高查询效率。
索引下推就是在查询时利用MySQL的最左前缀匹配原则,在索引中找到能够匹配最左前缀的部分,将这部分条件下推到存储引擎层面进行计算,从而减少MySQL的内存过滤操作和数据读取和传输,提高查询效率,减少回表次数。怎么查看MySQL语句有没有用到索引?
通过explain查看SQL语句。select_type:查询类型,表示当前被分析的sql语句的查询的复杂度。
table :表示当前访问的表的名称。
possible_keys:这个字段显示的是sql在查询时可能使用到的索引,但是不一定真的使用,只是一种可能。
key:sql执行中真正用到的索引字段。建索引的原则有哪些?
最左前缀匹配原则,索引列不能参与计算,尽量的扩展索引,不要新建索引。索引失效的场景?
- 对索引使用左或者左右模糊匹配
- 对索引使用函数
- 对索引进行表达式计算
- 对索引隐式类型转换
- 联合索引非最左匹配
- where 字句中的or,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效。
事务的四个特性?
- 原子性
- 一致性
- 隔离性
- 持久性:每提交一个事务必须先将该事务的所有日志写入到重做日志文件进行持久化,数据库就可以通过重做日志来保证事务的原子性和持久性。
四种隔离级别?
- Read Uncommitted(读取未提交内容):脏读
- Read Committed(读取提交内容):
- Repeatable Read(可重读):MySQL 的默认事务隔离级别,幻读
- Serializable(可串行化):通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。
InnoDB 存储引擎在 分布式事务 的情况下一般会用到**SERIALIZABLE(可串行化)**隔离级别。
MySQL事务的实现原理?
事务是基于重做日志(redo log)和回滚日志(undo log)实现的。
每提交一个事务必须先将该事务的所有日志写入到重做日志文件进行持久化,数据库就可以通过重做日志来保证事务的原子性和持久性。
每当有修改事务时,还会产生 undo log,如果需要回滚,则根据 undo log 的反向语句进行逻辑操作,比如 insert 一条记录就 delete 一条记录。undo log 主要实现数据库的一致性。binlog日志里面写了什么内容,怎么写的,什么时候写有什么用?
MySQL的 binlog 是记录所有数据库表结构变更(例如 CREATE、ALTER TABLE)以及表数据修改(INSERT、UPDATE、DELETE)的二进制日志。binlog 不会记录 SELECT 和 SHOW 这类操作,因为这类操作对数据本身并没有修改。
MySQL binlog 以事件形式记录,还包含语句所执行的消耗的时间,MySQL 的二进制日志是事务安全型的。binlog 的主要目的是复制和恢复。MVCC实现原理?
MVCC是一种并发控制机制,用于解决多个事务同时访问数据库时的并发问题。相比于传统的锁机制,MVCC可以在不加锁的情况下实现并发访问,提高了系统的并发能力。实现原理:InnoDB 每一行数据都有一个隐藏的回滚指针,用于指向该行修改前的最后一个历史版本,这个历史版本存放在 undo log 中。如果要执行更新操作,会将原记录放入 undo log 中,并通过隐藏的回滚指针指向 undo log 中的原记录。其它事务此时需要查询时,就是查询 undo log 中这行数据的最后一个历史版本。
乐观锁和悲观锁?
乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。在修改数据的时候把事务锁起来,通过version的方式来进行锁定。一般会使用版本号机制或CAS算法实现。悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。在查询完数据的时候就把事务锁起来,直到提交事务。
乐观锁实现?
使用版本号机制实现乐观锁:
- 在数据库表中增加一个版本号字段version。
- 当线程读取数据时,同时取出该数据的当前版本号version。
- 在更新时,如果当前数据库版本号和一开始的版本号一致,则更新version+1,否则失败。
使用CAS(Compare and Swap)实现乐观锁:
- 查询出当前需要更新的数据的版本号version,并保存在一个变量old_version中。
- 对该数据进行修改,然后使用CAS操作将version字段更新为old_version+1。
- 如果CAS操作失败,说明该数据已经被其他线程修改过了,需要重新查询数据并重复上述操作。
MySQL主从复制?
主从同步使得数据可以从一个数据库服务器复制到其他服务器上,在复制数据时,一个服务器充当主服务器(master),其余的服务器充当从服务器(slave)。原理:
- 第一步:主库master在每个事务更新数据完成之前,将数据变更记录在二进制日志binlog文件中。包括数据增删改操作,以及创建和删除数据库等操作。
- 从库salve开启一个I/O Thread,从库(Slave)连接主库,将主库的二进制日志复制到自己的中继日志(Relay Log)中。
- SQL Thread会读取中继日志,并顺序执行该日志中的SQL事件,从而与主数据库中的数据保持一致。
MySQL主从同步延时问题如何解决?
- 优化网络连接
- 使用半同步复制:主库写入 binlog 日志之后,立即将数据同步到从库,从库将日志写入自己本地的 relay log 之后,接着会返回一个 ack 给主库,主库接收到至少一个从库的 ack 之后才会认为写操作完成了。
- 用并行复制:多个从服务器同时复制主服务器的数据。通过并行复制,可以减少从服务器上的数据与主服务器上的数据的延迟,提高同步速度。
七、Redis
Redis数据类型?
- String:String的底层是动态字符串SDS(Simple Dynamic String),常规key-value缓存应用。常规计数: 微博数, 粉丝数。
- Hash:Hash的底层是dict,hash 特别适合用于存储对象
- Set:无序的天然去重的集合,点赞、共同好友、共同关注什么的功能
- List:List的底层是链表,
- Zset:底层是压缩列表ziplist或者字典+跳表,是Set的可排序版,支持优先级排序,维护了一个score的参数来实现。适用于排行榜和带权重的消息队列等场景。
- HyperLogLogs:统计基数的数据集合类型。百万级网页 UV 计数
- Bitmap:以位为单位数组,数组中的每个单元只能存0或者1
- Geospatial:用于存储地理位置信息
redis快的原因?
- 内存存储:Redis是使用内存(in-memeroy)存储,没有磁盘IO上的开销。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是O(1)。
- 单线程实现( Redis 6.0以前):Redis使用单个线程处理请求,避免了多个线程之间线程切换和锁资源争用的开销。注意:单线程是指的是在核心网络模型中,网络请求模块使用一个线程来处理,即一个线程处理所有网络请求。
- IO多路复用:Redis使用多路复用IO技术,将epoll作为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间。
Redis如何保持高并发的?
- 单线程模型:Redis采用单线程模型,通过异步I/O和事件驱动机制来处理请求,减少了上下文切换和锁竞争等开销,从而可以支持高并发的请求。
- 基于内存的数据存储:Redis将数据存储在内存中,而不是磁盘中,从而大大提高了数据的读写效率。
- 持久化机制:Redis提供了两种持久化机制,即RDB(Redis DataBase)和AOF(Append Only File),可以将内存中的数据定期或实时保存到磁盘上,保证了数据的持久性和可靠性。
- 集群模式:Redis提供了集群模式,通过分片技术将数据分散到多个节点上,实现数据的分布式存储和高可用性,进一步提高了系统的并发能力和性能。
- 数据压缩:Redis支持对数据进行压缩,可以减少网络传输的数据量,提高数据传输的效率,从而提高并发能力。
Redis是单线程的吗?
Redis采用单线程的方式来处理网络请求和数据操作,这是因为Redis的瓶颈在于CPU的处理能力,而不是网络带宽或者磁盘I/O等。因此采用单线程可以避免线程上下文切换等开销,提高CPU的利用率。不过,Redis的单线程也会限制其并发能力,无法充分利用多核CPU的优势。因此,Redis引入了多线程机制来提高并发能力。多线程主要用于以下两个方面:
- 网络I/O多路复用:Redis可以将网络I/O操作交给一个线程池来处理,从而避免在单线程中阻塞等待I/O操作完成的情况,提高网络I/O处理能力。
- 子进程的Fork操作:Redis在进行持久化操作时,会fork一个子进程来进行,这个子进程需要拷贝父进程的内存数据,如果数据量很大,这个操作会很耗时,而采用多线程的方式可以将这个操作并行化,提高持久化操作的效率。
Redis的多线程机制仅用于网络I/O和持久化操作,Redis仍然采用单线程的方式来处理数据操作,这也是Redis能够保证数据一致性和高性能的关键之一。
Redis是怎么解决线程安全问题的?
Redis采用单线程的方式来处理网络请求和数据操作,因此不需要考虑多线程并发访问的线程安全问题。但是,在多个客户端同时连接Redis的情况下,仍然可能存在并发访问共享数据的问题,因此Redis采用了以下几种方式来保证数据的线程安全性:- 原子操作:Redis提供了很多原子操作,如INCR、LPUSH、SETNX等,这些操作是不可分割的,可以保证数据的一致性。
- 乐观锁:Redis支持CAS(Compare And Swap)指令,通过CAS指令可以实现乐观锁机制,即先读取数据,然后在执行更新操作时检查数据是否被其他客户端修改过,如果没有修改过则更新,否则重新读取数据并重试。
- 分布式锁:Redis提供了分布式锁的实现方式,可以通过SETNX和EXPIRE指令来实现分布式锁的获取和释放,从而保证在分布式环境下对共享资源的互斥访问。
Redis的持久化机制?
将Redis中的数据写入到磁盘空间中,即持久化。
Redis提供了两种不同的持久化方法可以将数据存储在磁盘中,一种叫快照RDB,另一种叫只追加文件AOF。- RDB:RDB是Redis默认的持久化方式。是将Redis在内存中的数据周期性地保存到磁盘中。适合大规模的数据恢复,对数据完整性和一致性要求不高。
- AOF:将Redis的每个写操作追加到一个日志文件中,当Redis重启时,会通过重新执行日志文件中的写操作来恢复数据。
Redis过期键的删除策略?
Redis使用了两种策略来删除过期键,分别是定期删除和惰性删除。- 定期删除:Redis默认每隔一段时间就会随机抽取一些过期键并删除。这种方式简单,但可能会导致一些过期键一直存在,直到下一次定期删除时才会被删除。
- 惰性删除:当客户端尝试读取某个键时,Redis会检查该键是否过期,如果过期则立即删除。这种方式保证了过期键被及时删除,但也会增加读取操作的时间开销。
Redis默认使用惰性删除方式,同时也会每隔一段时间启动定期删除。如果需要修改过期键的删除策略,可以通过修改配置文件或者使用命令来实现。
如何保证缓存与数据库双写时的数据一致性?
先删除缓存,后更新数据库:删除缓存–>更新数据库–>sleep N毫秒–>再次删除缓存(延时双删)
先更新数据库,后删除缓存:什么是缓存击穿?
缓存击穿是某个热点的key失效,大并发集中对其进行请求,就会造成大量请求读缓存没读到数据,从而导致高并发访问数据库,引起数据库压力剧增。这种现象就叫做缓存击穿。解决:
- 设置热点数据的永久缓存:对于访问量较大、变化不频繁的热点数据,可以将其设置为永久缓存,避免因为缓存失效而导致的缓存击穿问题。
- 使用互斥锁:过互斥锁来控制读数据写缓存的线程数量
什么是缓存穿透?
缓存穿透是指访问的数据在缓存和数据库中都不存在,这种情况下大量的请求会直接打到数据库上,导致数据库压力过大,影响系统性能。
解决:
- 对空值做缓存,将无效的key存放进Redis中;
- 布隆过滤器:可以在缓存之前再加一个布隆过滤器,将数据库中的所有key都存储在布隆过滤器中,在查询Redis前先去布隆过滤器查询 key 是否存在,如果不存在就直接返回,不让其访问数据库,从而避免了对底层存储系统的查询压力。
布隆过滤器原理?
使用 k 个不同的哈希函数对元素进行哈希,并将得到的哈希值对一个大小为 m 的位数组进行标记,置为 1。当需要判断某个元素是否在集合中时,将该元素经过 k 个哈希函数后的结果对位数组进行查找,若所有位置均被标记,则该元素可能在集合中;否则,该元素一定不在集合中。
什么是缓存雪崩?
缓存雪崩是指缓存中的大量key同时失效或者缓存节点宕机,导致大量请求直接落到数据库上,引起数据库压力过大,进而影响系统性能甚至瘫痪的情况。解决:
- 均匀过期:设置不同的过期时间,让缓存失效的时间尽量均匀,避免相同的过期时间导致缓存雪崩。
- 使用多级缓存架构,将热点数据缓存在多个缓存节点上,避免单一节点的宕机导致缓存雪崩。
- 实现熔断机制,当数据库压力过大时,关闭缓存或降级处理,保证系统的稳定性。
Redis事务的三个阶段?
multi 开启事务–>大量指令入队–>exec执行事务块内命令。
Redis 事务的执行并不是原子性的。Redis事务中如果有某一条命令执行失败,之前的命令不会回滚,其后的命令仍然会被继续执行。redis的主从架构和主从哨兵区别?
Redis主从架构是将一台Redis服务器设置为主节点,其他Redis服务器设置为从节点,主节点可以进行读写操作并同步数据到从节点,从节点只能进行读操作。当主节点宕机时,从节点会选出(手动选取)一个新的主节点进行数据同步,保证服务的可用性。Redis主从哨兵则是在Redis主从架构的基础上引入哨兵节点进行监控和自动故障转移。哨兵节点会监控主节点和从节点的状态,当主节点宕机时,哨兵节点会主动选举新的 master,并将其升级为主节点。同时,哨兵节点还会更新从节点的配置,使其成为新主节点的从节点,从而实现自动故障转移。
Redis集群?
Redis集群是由多个Redis节点组成的分布式系统,每个节点都存储部分数据,通过节点之间的协调和通信,实现数据的分布式存储和查询。集群模式适用于大规模分布式应用场景,可以实现高可用、高性能的数据存储和读取。Redis如何应对主从数据不一致问题?
自动重同步:Redis从3.2版本开始支持自动重同步(Auto Resynchronization)机制。当从节点重新连接到主节点时,如果从节点的复制偏移量(offset)比主节点的复制偏移量还要旧,那么主节点就会自动执行完整重同步,以确保从节点的数据与主节点一致。Redis如何实现分布式锁?
① 使用SETNX命令尝试在Redis中设置一个键值对,其中键表示锁的名称,值为一个随机生成的唯一标识,表示锁的拥有者。
② 如果SETNX命令返回1,表示设置成功,当前线程获得了锁。如果SETNX命令返回0,表示设置失败,锁已经被其他线程占用。
③ 锁变量需要设置过期时间,以免客户端拿到锁后发生异常,导致锁一直无法释放,所以,我们在 SET 命令执行时加上 EX/PX 选项,设置其过期时间;
④ 释放锁时,使用DEL命令删除Redis中的键值对,将锁释放掉。为了避免当前线程误删其他线程的锁,需要在释放锁时通过锁的唯一标识符来判断当前线程是否持有该锁,可以使用Lua脚本实现原子性的判断和删除操作。
八、Spring
SpringBoot相对于Spring有什么优势?
- 简化配置:Spring Boot 提供了许多默认的配置,减少了开发者的配置工作量。
- 内嵌容器:Spring Boot 支持内嵌 Tomcat、Jetty 和 Undertow 等 Web 容器,使得开发人员不需要安装和配置外部 Web 服务器即可运行应用程序。
- 简化依赖管理:Spring Boot 提供了一种依赖管理的方式,它会自动将应用程序所需的依赖包括在内,开发人员无需手动添加依赖。
- 易于集成其他框架和技术:Spring Boot 集成了许多常用的框架和技术,如 Spring Data、Spring Security 等,使得开发人员可以更加容易地集成其他的技术和框架。
SpringMVC的工作原理?
① 客户端(浏览器)发送请求,直接请求到 DispatcherServlet。
② DispatcherServlet 根据请求信息调用 HandlerMapping,HandlerMapping 根据请求的 URL,查找对应的 Handler(Controller)。
③ 找到 Handler 后,DispatcherServlet 将请求交给 HandlerAdapter 进行处理,HandlerAdapter 负责将请求映射到对应的方法上,处理相应的业务逻辑。
④ 方法处理完请求后,会返回一个 ModelAndView 对象。Model 是返回的数据对象,View 是逻辑视图。
⑤ ViewResolver 会根据逻辑 View 查找实际的 View。
⑥ 找到 View 后,DispatcherServlet 将 Model 数据传递给 View,View 根据数据生成 HTML 页面,并将其返回给浏览器。Spring 中常用的注解?
- @Component:通用的注解,可用于任何类,使其成为 Spring 容器管理的 bean。
- @Autowired:自动装配,可用于构造器、属性、方法和参数上,自动注入依赖的 bean。
- @Controller:标注控制层组件,处理 HTTP 请求。
- @Service:标注服务层组件,用于事务处理和业务逻辑。
- @Repository:标注数据访问层组件,负责访问数据库。
- @Bean:标注在方法上,用于声明 bean,将方法的返回值注入 Spring 容器。
- @Value:注入属性值,支持从 properties 文件、环境变量、JVM 系统属性中获取。
- @Configuration:配置类注解,用于定义应用上下文的 bean。
- @Transactional:事务注解,用于控制事务的提交和回滚。
- @RequestMapping:处理 HTTP 请求的注解,映射 URL 和方法。
Spring中bean的生命周期?
在 Spring 容器中,每个 Bean 都有一个完整的生命周期,包括实例化、属性赋值、初始化和销毁四个阶段。Spring 容器管理 Bean 的生命周期,我们可以通过在 Bean 上加入生命周期相关的注解或者实现相应的接口来自定义 Bean 的生命周期。① 实例化:Spring 使用反射机制实例化 Bean 对象。调用Bean的构造函数创建Bean的实例,实例化 Bean 的属性,注入 Bean 的依赖。
② 属性赋值:Spring 将 Bean 实例中的属性值注入到相应的属性中。
③ 初始化:Spring容器会调用Bean的init方法,完成一些初始化工作,例如建立数据库连接、加载配置文件等。
④ 使用:Bean 实例已经准备好可以被其他对象使用。
⑤ 销毁:Spring容器会调用Bean的destroy方法,完成一些清理工作,例如关闭数据库连接、释放资源等。Bean的作用域?
在 Spring 中,Bean 的作用域用于指定 Bean 实例的生命周期及其在应用程序中的可见范围。Spring 提供了以下五种标准作用域:- Singleton(单例):在整个应用程序中,只创建一个 Bean 的实例,所有请求都将共享同一个实例。
- Prototype(原型):每次请求时,都会创建一个新的 Bean 实例。
- Request(请求):在一次 HTTP 请求中,创建一个 Bean 实例,并在该请求的所有处理过程中共享该实例。
- Session(会话):在用户会话期间,创建一个 Bean 实例,并在整个会话过程中共享该实例。
- Global Session(全局会话):在 Portlet 环境中使用,作用于整个 Portlet 应用程序中的所有用户会话。
Spring怎么解决循环依赖的?
Spring通过三级缓存解决循环依赖问题:Spring使用一种缓存机制。这个缓存机制分为三个层级,分别是单例Bean工厂缓存、单例Bean缓存和早期Bean缓存。一级缓存:单例Bean缓存,用于存放完全初始化好的单例Bean。这个缓存是一个ConcurrentHashMap,用来存储已经创建的单例Bean实例。
二级缓存:早期Bean缓存,实例化了但还未完成初始化的Bean。
三级缓存:单例Bean工厂缓存,用于存放Bean工厂,即创建Bean实例的工厂。当容器需要创建一个Bean时,会先从这个缓存中获取Bean的定义信息,以便于后续的Bean创建工作。具体过程如下:
- 首先从单例Bean缓存中获取Bean实例,如果获取到了则直接返回。
- 如果没有获取到,则尝试从早期Bean中获取早期暴露出来的Bean实例,找到了就返回该示例。
- 如果还没有获取到,则尝试从Bean工厂中获取对应的ObjectFactory,并调用getObject()方法创建Bean实例。
- 创建完成后将Bean实例放入二级缓存早期Bean中,并从Bean工厂中移除对应的ObjectFactory。
- 最后将完全初始化好的Bean实例放入单例Bean缓存中。
这种解决循环依赖的方式只适用于单例 Bean。对于原型 Bean,Spring 无法使用缓存来解决循环依赖的问题,只能抛出异常。此外,循环依赖可能导致死锁,因此需要慎重使用。
哪些情况下无法解决循环依赖问题?
- 单例Bean依赖于一个非单例Bean:如果一个单例Bean依赖于一个非单例Bean,那么Spring无法通过三级缓存来解决它们之间的循环依赖问题,因为非单例Bean每次注入时都会创建一个新的实例。
- 构造函数循环依赖:如果两个Bean的构造函数相互依赖,那么Spring无法通过三级缓存来解决它们之间的循环依赖问题,因为在构造函数中还没有创建对象实例,无法将其放入缓存中。
- 如果循环依赖的Bean中存在prototype作用域,那么Spring无法通过三级缓存来解决循环依赖问题,因为Spring只会为每个prototype Bean创建一个新的实例,无法将其放入缓存中。
Spring IOC?
Spring IOC(Inversion of Control)即控制反转,是一种编程思想,它通过将控制权从代码中移出来,将对象的创建、依赖注入和生命周期管理等工作交给Spring容器来完成,从而达到了松耦合的目的。其原理是通过反射、JavaBean和配置文件等方式,将对象实例化并保存在容器中,当需要使用某个对象时,从容器中获取即可。原理:Spring IOC的实现原理主要是基于Java的反射机制和XML配置文件来实现的。在Spring IOC容器启动时,会读取配置文件中定义的Bean的配置信息,通过反射机制实例化Bean对象,并根据Bean的依赖关系将相应的依赖注入到对象中,最终将所有的Bean对象保存到Spring IOC容器中,供其他组件使用。
SpringAOP?
Spring AOP(Aspect-Oriented Programming,面向切面编程)是Spring框架的一个重要组成部分,它通过在不改变原有代码的基础上,将一些横切关注点(如日志、事务、安全等)与业务逻辑分离,提高了代码的复用性、可维护性和可扩展性。Spring AOP的实现原理主要是基于动态代理和字节码增强机制。在运行时,Spring会通过动态代理为需要被增强的目标对象创建代理对象,然后在代理对象的指定位置(切点)插入增强代码,从而实现横切关注点的功能。
Join point:连接点,即目标对象中可以被增强的方法。
Pointcut:切入点,定义了一组连接点的集合,通常使用表达式来描述。
Advice:通知,即增强的具体实现,例如前置通知、后置通知等。
Aspect:切面,将切入点和通知组合起来,形成一个切面。
Weaver:织入器,将切面应用到目标对象上,生成代理对象。AOP记录日志是怎么做的?
通常情况下,我们可以通过在方法执行前后进行日志记录的方式,来实现对方法调用的拦截和增强。
可以通过定义一个切面类,在切面类中定义一个前置通知和一个后置通知方法来实现日志记录。在前置通知方法中可以记录方法调用前的相关信息,如方法名、参数等,而在后置通知方法中可以记录方法调用后的相关信息,如返回值等。Spring单例bean线程安全吗,怎么解决的?
单例 Bean 并不总是线程安全的,这主要是因为单例 Bean 的属性是共享的,当多个线程同时访问 Bean 的属性时可能会产生竞争条件。
解决:Spring使用ThreadLocal解决线程安全问题。
九、Kafka
Kafka 是什么,有什么特点?
Kafka 是一种高吞吐量的分布式发布订阅消息系统,它具有高可靠性、高并发、高可扩展性、低延迟等特点。Kafka 的主要设计目标是提供一个持久化的、高吞吐量的、低延迟的平台,以处理大量的实时数据流。Kafka 的核心组件有哪些?
Kafka 的核心组件包括生产者、消费者和 Kafka 集群。生产者将消息发布到 Kafka 集群,消费者从 Kafka 集群中读取消息。Kafka 的消息如何被存储?
Kafka的消息是以Topic为单位进行存储的,每个Topic被分为一个或多个Partition,每个Partition又被划分为多个Segment。每个Segment是一个单独的文件,消息被追加到文件的末尾。Kafka使用多个Segment来存储Partition的消息,当一个Segment达到一定的大小或者时间限制时,会被关闭并创建一个新的Segment。Kafka 如何保证消息的可靠性?
Kafka 使用副本机制来保证消息的可靠性。每个分区都有多个副本,其中一个副本被指定为领导者(leader),其余副本被指定为追随者(follower)。生产者将消息发布到领导者,领导者将消息写入本地磁盘,并将消息复制到所有追随者。只有在所有追随者都成功复制消息后,领导者才会确认消息发布成功。Kafka 如何保证消息的顺序性?
在一个Partition中,数据一定是有顺序的。Kafka会为每个Partition维护一个消息的偏移量(offset),每个消息都有一个唯一的偏移量。当一个Consumer消费一个Partition的消息时,它会按照消息的顺序来处理,并将每个消息的偏移量保存下来。当Consumer再次消费该Partition的消息时,它可以使用保存的偏移量来恢复上次消费的位置。Kafka 如何保证高可用性?
Kafka 使用副本机制和选举算法来保证高可用性。当一个分区的领导者宕机时,Kafka 会通过选举算法选出一个新的领导者。在新领导者选举完成前,这个分区将暂停对外服务,以保证数据的一致性。Kafka 中的 ISR、AR 指的是什么?
ISR:In-Sync Replicas 副本同步队列,指当前所有已经复制了消息的副本集合。
AR:Assigned Replicas 所有副本Kafka的Producer如何处理消息发送失败的情况?
当Producer发送消息失败时,它会根据配置的重试策略进行重试。如果消息在经过多次重试后仍然发送失败,Producer会将消息发送到配置的死信队列(DLQ)中,以便后续的处理。此外,Kafka还提供了一些钩子函数,允许开发者在发送消息失败或重试时执行一些自定义的逻辑。Kafka持久化?
Kafka进行持久化的方式是通过将消息写入磁盘中的日志文件来实现的,这也是Kafka的核心设计思想之一。具体来说,Kafka将消息以追加的方式写入主题(topic)的一个或多个分区(partition)中的日志文件(log file)中。每个日志文件都会被分成多个段(segment),每个段的大小通常为1GB或1小时,这样可以方便地进行文件的切换和管理。Kafka怎么保证消息不被重复消费?
Kafka 通过消费者组(consumer group)的概念来保证消息不被重复消费。Kafka 的消费者组采用的是广播的方式,也就是说,同一个主题的消息只会被一个消费者组中的一个消费者消费。topic—consumer group
Kafka 会为每个主题和消费者组组合分配一个唯一的消费者组 ID。消费者会从分配给自己的分区中消费消息,并将消费的 offset 提交到 Kafka。Kafka 会记录每个消费者组消费的 offset,并根据这些 offset 将每个分区的消息发送到对应的消费者中。如果一个消费者在处理消息时失败了,Kafka 会重新将该分区的消息分配给该消费者或者其他消费者来消费。如果一个新的消费者加入到消费者组中,Kafka 会自动将一些分区分配给新的消费者,确保每个分区只有一个消费者消费。
十、设计模式
单例模式
单例模式是一种创建型设计模式,它确保类只有一个实例,并提供了全局访问点来访问该实例。
单例模式有两种实现方式:饿汉式和懒汉式。饿汉式是指在程序启动时就创建一个单例对象,而懒汉式是指在第一次使用该单例对象时才创建它。饿汉式:
1
2
3
4
5
6
7
8
9
10
11
12
13public class SingletonHungry {
// 在类加载时就创建对象
private static SingletonHungry instance = new SingletonHungry();
// 私有构造函数,避免被其他类实例化
private SingletonHungry() {
}
// 全局访问点,获取单例对象
public static SingletonHungry getInstance() {
return instance;
}
}懒汉式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14public class SingletonLazy {
private static SingletonLazy instance; // 没创建
private SingletonLazy() {
}
public static synchronized SingletonLazy getInstance() {
// 第一次使用时再创建对象
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
}工厂模式
工厂模式是一种常用的创建型设计模式,用于将对象的创建过程封装起来,使得客户端无需关心具体的创建细节。它的核心思想是将对象的创建与使用分离开来,通过一个工厂类来负责创建对象,从而实现对对象的解耦和复用。