当前位置:   article > 正文

JAVA常见面试题整理(长期更新)_java基础面试

java基础面试

常见面试题

Java

Java基础

1、java跨平台原理

java中.java文件在执行编译后,产生.class文件,.class文件是在JVM虚拟机中运行的,而JVM虚拟机可以安装在各个主流系统上,所以编译后的.class文件可以运行在各个系统。

2、java中int占几个字节 int和integer的区别

在java中int是中基本类型的其中一种,占4字节,32位,定义后初始化数据为0。
Integer为int类型的包装类,里面定义了许多关于int类型的方法,同时也可以达到存储int类型数据的目的,拥有自动装箱,自动拆箱功能,定义后初始化数据为null,当存储数据在-128到127时,Integer对象是在IntegerCache.cache产生,会复用已有对象,此时使用进行判断,两个是同一个对象。当不在这个区间内,则会重新new一个Integer对象,使用判断后结果为false。

3、==与equals区别 equals和hashcode的区别与联系

实际上equals方法的底层是==去实现的,当比较的数据类型为基本类型时则不能使用基本类型.equals,需要使用==才能进行比较。
使用equals或==对类型为String的数据进行判断时比较的是字符串内容,当使用equals或==对引用数据进行判断时,比较的是地址在内存中的地址值,当重写equals后可以自己指定比较内容,同时也可以使用hashcode作为比较值。当需要比较大量数据时,使用重写equals方法并结合hashcode进行查找效率最高。

4、面向对象的几个特征

抽象: 将一类对象的共同特征总结出来构造类的过程,抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。
封装: 把一个对象的属性私有化,但是同时也提供一些可以被外界访问的属性的方法,如果不希望外界访问,则也不必提供方法给外界。
继承: 使用已存在的类的定义作为基础,建立新类的技术,继承父类的子类可以增加自己独有的属性和方法,同时也可以使用父类的属性和功能,但不能选择性的继承父类,通过继承可以减少代码冗余,增加代码复用。要注意的是java是单继承多实现。
子类拥有父类非private的属性和方法。
子类可以拥有自己的属性和方法,可以在父类的基础上进行扩展。
子类可以用自己的方式实现父类的方法,也就是方法重写。
多态: 指程序中定义的引用变量所指向的具体类型在编程时不确定,而是在运行期间才确定,即一个引用变量到底指向哪个实例对象,该引用变量发出的方法调用到底是那个类中的实现方法,必须由程序运行期间才能决定。

5、抽象类和接口的区别

抽象类是定义要被继承的子类的通用特性的,接口只能定义方法。
抽象类是对类的抽象,是一种模板,接口是对行为的抽象,是对行为的规范。
相同点:
接口和抽象类都不能实例化,只能被继承或实现。
都包含抽象方法,并且子类必须重写这些方法。(1.8以后接口引入默认方法和静态方法,默认方法不用被强制实现)
不同点:
声明关键字不同。
实现关键字不同。
抽象类可以有构造器,接口没有。
抽象类的方法可以有各种访问修饰符,接口访问修饰符必须是public
一个类最多继承一个抽象类,但是可以实现多个接口

6、什么是字符串常量池?

字符串常量池位于堆内存,专门存放字符串常量,可以提高内存使用率,避免多块空间存储相同字符串。在初始化字符串时会在内存中检查字符串常量池,如果常量池已存在该字符串,则返回它的引用,如果不存在,则实例化一个字符串放入池中,并返回其引用。

7、字符串是可变的吗?

字符串是不可变的,java中String类是用final修饰的一个常量char数组,所以不可修改。表面看上去的可以修改实际上是在字符串常量池中新建了一个字符串,并返回了该字符串的引用。

8、String、StringBuffer和StringBuilder的区别是什么?

String底层是被final修饰过的所以是不可变的,StringBuffer和StringBuilder虽然也是用char数组实现,但是没有用final修饰,所以是可变的,并且拥有一些独有的字符串处理方法
StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以它是线程安全的,而StringBuilder没有加锁,所以它不是安全的。
StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并引用,相同情况下使用StringBuilder要比StringBuffer效率要高。

9、访问权限修饰符的区别

修饰符当前类同包子类其他包
public
producted
default
private

10、&和&&的区别

&和&&都是逻辑运算符,从某种意义上来讲,两个效果是相同的,但是&&拥有短路特性,当有多个逻辑表达式进行运算时,如果第一个逻辑表达式就是false,那么不管后面是否正确,都不在执行。同理 | 和 || 也是一样的,只要第一个为true那么后面都不在执行了。

11、是否可以继承String

不可以,String类是被final修饰过的,并且继承String本身就是一种错误的行为。

12、当一个对象作为参数传入一个方法后,这个方法可以改变传入对象的属性值,那么这里对象的传递是值传递还是引用传递

是值传递,因为java中不存在引用传递,当一个对象实例被传入方法中,参数的值是该对象的引用,对象的属性可能会被改变,但是对象引用的改变是不会影响到调用者的。

13、重载和重写的区别

重载是相同的方法名,参数列表的类型、数量、顺序不同,并且与返回值无关,可以有不同修饰符。
重写是将继承的父类方法重写覆盖掉父类方法,所以要求,方法名,参数列表都与父类方法名和参数列表相同
注:构造方法不能被重写,被final修饰的方法不能被重写,声明为static的方法不能被重写,但是可以被再次声明

14、char类型中能否存入一个中文汉字

可以,因为java使用的默认编码为Unicode,直接使用字符在字符集的编号,一个charl类型占两字节,所以放一个中文汉字是没问题的。

15、抽象方法是否可以被static、native、synchronized修饰

都不可以
首先抽象方法是要被子类重写的,而static方法无法被重写,这是相互矛盾的。
native是有本地方法实现的,而抽象方法是没有实现的,所以也不可以。
synchronized和实现的细节有关,而抽象方法不设计实现细节,因此也是矛盾的。

16、什么是多态

多态是父类或接口定义的引用变量可以指向子类实现,而程序调用的方法在执行期间才会动态绑定,就是引用变量所指向的具体实例对象的方法,也就是对象内存中正在运行的那个对象,而不是引用变量的类型中定义的那个方法。

17、Java中的静态变量和实例变量有何区别?静态方法和实例方法有何区别?

静态变量和实例变量的区别:
静态变量属于类,实例变量属于对象;
静态变量只有一份,被所有对象共享,实例变量每个对象都有一份;
静态变量可以直接通过类名访问,实例变量需要通过对象名访问;

静态方法和实例方法的区别:
静态方法属于类,实例方法属于对象;
静态方法可以直接通过类名调用,实例方法需要通过对象名调用;
静态方法中不能使用 this 关键字,因为 this 表示当前对象,而静态方法没有对象;

静态对象和实例对象在存储方式上也是不同的
静态变量存储在方法区(Method Area)中,也称为永久代(PermGen),即在类加载时就已经被分配了内存空间,所有该类的对象共享同一份静态变量的内存空间。
而实例变量则存储在 Java 堆(Java Heap)中,即每个对象都有自己的实例变量的内存空间,当对象被创建时,实例变量也随之被分配内存空间。
需要注意的是,从 JDK 8 开始,永久代被移除了,取而代之的是元空间(Metaspace),静态变量的存储方式也变成了存储在元空间中。但是,与永久代不同,元空间并不是虚拟机运行时数据区域的一部分,而是使用本地内存来实现的。

18、Java中如何进行对象的序列化(Serialization)和反序列化(Deserialization)?

对象的序列化(Serialization)是指将对象转换为字节流的过程,而反序列化(Deserialization)则是指将字节流转换为对象的过程。通过序列化和反序列化,可以实现对象的持久化存储、网络传输等功能。
实现 Serializable 接口:要使一个类可序列化,需要让该类实现 Serializable 接口。Serializable 接口是一个标记接口,没有定义任何方法,只是作为标记告诉编译器这个类可以被序列化。
序列化:使用 ObjectOutputStream 将对象序列化为字节流,并将字节流写入文件或发送给网络。
进行反序列化:使用 ObjectInputStream 从字节流中读取数据,并将其反序列化为对象。

异常

java异常分为运行时异常和编译时异常
编译时异常是可以被手动处理掉的,大部分是可以预见性的异常,如果是提前知道怎么处理异常,则可以使用try…cache捕获并处理异常,如果不知道如何处理,则定义该方法是声明时抛出该异常。
运行时异常则是只有在代码运行时才发出的异常,比如类转换异常,数组下标越界异常、除数为0数学异常等,这种异常在出现时会被系统自动捕获,可以手动try…cache进行处理,或者直接交给程序自动处理。

try-catch 块:try-catch 块用于捕获和处理可能抛出异常的代码块。try 块中编写可能引发异常的代码,catch 块用于捕获并处理异常。

throws 关键字:throws 关键字用于声明方法可能抛出的异常,让调用该方法的代码去处理异常。

finally 块:finally 块用于定义无论是否发生异常都会执行的代码,通常用于释放资源或清理工作。

throw 关键字:throw 关键字用于在代码中手动抛出异常。

1、error和exception的区别

error和exception都继承于throwable类
error一般是虚拟机相关的问题,如系统崩溃,内存空间不足,方法调用栈溢出等,这种问题一旦出现,就代表这无法修复的错误,是非常严重的。
exception表示程序可处理的异常,遇见这种问题,应该尽可能的解决异常,使程序恢复运行。
exception又分为运行时异常和编译时异常,编译时异常表示语法都没用办法通过,运行时异常表示的是只有在程序运行时,才会出现的异常,比如数组下标越界,没找到类异常等。遇见异常时,尽量使用try…cache进行异常捕获并处理,保证程序的正常运行。

泛型

泛型(Generic)是一种在编译时期约束集合类接受的元素类型的机制。通过泛型,可以使代码更加通用、可重用,并提高代码的类型安全性。

使用泛型可以带来以下好处:
代码复用性:通过泛型,可以编写更通用的类和方法,适用于不同类型的数据,避免了重复编写相似的代码。
类型安全性:使用泛型可以在编译时发现类型错误,避免在运行时出现类型转换异常或其他类型相关的错误。
减少强制类型转换:使用泛型可以避免频繁进行类型转换操作,使代码更加简洁清晰。

在 Java 中,泛型主要应用于以下几个方面:
泛型类(Generic Class):定义一个泛型类可以接受任意类型的数据,例如 class MyGenericClass<T>
泛型方法(Generic Method):定义一个泛型方法可以接受任意类型的参数,例如 public <T> void myGenericMethod(T t)
泛型接口(Generic Interface):定义一个泛型接口可以让实现类指定具体的类型,例如 interface MyGenericInterface<T>
泛型通配符(Generic Wildcards):使用通配符 ? 可以表示未知类型,在一些情况下可以灵活地处理不同类型的数据。### 反射

1、什么是反射?

在程序运行状态时,都能够知道任何一个类的所有属性和方法,对于任意一个对象,都能调用它的任意一个方法和属性,这种动态获取对象类或对象信息,并动态调用对象的方法被称为反射。

2、反射的优缺点

优点:可以在运行期间就对类或对象进行判断,动态加载,提高代码灵活度。
缺点:相当于手动操作JVM进行操作,不如JVM自动操作效率高。

3、反射机制的应用场景

在平时的项目开发过程中,很少直接使用反射,但是在框架设计、模块化开发等都是通过反射调用相对应的字节码,在spring的动态代理中就使用到了反射机制,spring就是通过读取配置文件后,通过全限定类名和反射获得对象实例。

4、通过反射获取Clazz的几种方式

  1. 使用 Class.forName 静态方法,传入类的全限定类名
  2. 通过类使用.class方法
  3. 通过实例对象的.getClass()方法获取

5、通过反射创建实例对象几种方式

  1. 首先获取类的字节码文件,其次调用clazz.newInstance()获取实例
  2. 首先获取类的字节码文件,其次获取构造方法clazz.getConstructor(),通过constroctor.newInstance()创建实例对象

6、如何通过反射获取私用属性

首先通过反射获取class对象
通过class对象获取属性 getDeclaredField(filedName)
给属性设置可以访问 field.setAccessible(true);

集合

1 、简述集合体系

在这里插入图片描述

Collection接口 和 Map接口继承于Iterator接口
set接口 和 List接口继承于Collection接口
set接口下有HashSet、TreeSet和LinkedHashSet

 - HashSet:无序,集合内元素不可重复,线程不安全
 - TreeSet:指定排序算法后可以进行排序,线程不安全
 - LinkedHashSet:有序并且保证排序,线程不安全
  • 1
  • 2
  • 3

List接口下有ArrayList、LinkedList

 - ArrayList:查、改快,线程不安全,初始长度没有指定默认长度的时候,长度为0,并在第一次添加元素时初始化,初始长度为10,再次扩容会先copy数组,并扩容为原数组的1.5倍。
 - LinkedList:增、删快,线程不安全,插入元素是将元素置于链表末尾,属于尾插法。
  • 1
  • 2

Map接口下有HashMap、TreeMap、LinkedHashMap和HashTable

 - HashMap:无序双列集合,线程不安全
 - TreeMap:可排序双列集合,线程不安全
 - LinkedHashMap:有序并且保证排序的双列集合,线程不安全
 - HashTable:无序双列集合,线程安全
  • 1
  • 2
  • 3
  • 4

2、hashMap和hashTable区别

HashMap和HashTable都继承于Map接口,都是双列集合,两者的区别在于:

  1. HashMap线程不安全,hashTable线程安全
  2. HashMap的键和值允许为null值,而HashTable键和值都不允许为null。
  3. 解决hash冲突的方式不同

3、ArrayList和Vector的区别

两个类都继承了List接口,都属于单列有序集合。
Vector是Java出现的时候就存在的类,而ArrayList是后面更新后才出现的。java也推荐我们优先使用ArrayList,但是在使用前要考虑线程安全问题,因为ArrayList是线程不安全的,而Vector是线程安全的,如果没有多线程问题则选择ArrayList,这样效率高于Vector。
ArrayList和Vector在创建时都有一个初始容量大小,当存储的数据超过初始容量时,会自动对集合进行扩容,Vector默认每次增长为原来的2倍,ArrayList是增长为原来的1.5倍,但是Vector可以手动设置增长的空间大小,ArrayList不能手动设置增长空间大小。

4、Array和ArrayList有什么区别?什么时候应该用Array而不是ArrayList?

Array大小是固定的,ArrayList大小是动态变化的。
ArrayList处理固定大小的基本数据类型时,这种方式效率较慢。
在实际应用场景中,如果提前知道需要存储的数量,并且后期不会在改变大小的时候可以使用数组,如果后期对存储容量有动态变化的时候则使用ArrayList。

5、HashMap的底层原理

在jdk1.7之前HashMap的底层原理是由数组+链表实现的,当创建出来HashMap时,是一个数组,数组中的每一个元素是一个单向链表的头指针,指向一个entry键值对,当一个键值对放入hashMap时会根据键的hashcode选择放入哪一个数组元素,也就是选择放入哪一个单向链表中,如果出现两个entry的hash值一样,这样就产生了hash冲突,那么新放入的entry键值对则使用头插法,插入在表头。
jdk1.8之后采用数组+链表+红黑树实现,在基础思想上添加了红黑树进行优化,当链表长度大于等于阈值(8)时,链表转化为红黑树,利用红黑树的自平衡在查找性能上得到提升。当链表长度小于于等于阈值(6)时,红黑树转化为链表。HashMap的初始长度是16,每次自动扩展或手动扩展时,长度必须是2的幂,1.8之后遇到hash冲突后是尾插法。

6、set里的元素是不能重复的,那么底层是用什么方法区分重复与否呢?

set底层先使用hashcode值和将要加入的hashcode值进行比较,如果相同则继续使用equals方法进行比较。

7、HashSet和TreeSet的区别

HashSet底层是有HashMap实现的,在HashSet的构造方法中初始化了一个HashMap,利用HashMap的键值不唯一,使用HashMap的键来存储值,因此当存储的值重复时会返回false。
TreeSet是有树形结构,基于TreeMap实现的,所以存储是有序的,但是同样是线程不安全的。

8、LinkedHashMap的实现原理

LinkedHashMap也是基于HashMap实现的,只是它额外定义了一个Entry header,这个header是独立出来的一个链表头指针。每个entry添加了两个属性,before和after和header结合起来形成了一个双向链表,由此就实现了插入顺序或访问顺序排序。默认排序即插入的顺序。

9、ConcurrentHashMap与Hashtable的区别

两者都是线程安全的,但是底层对于线程安全实现方式不同,Hashtable是对表结构进行上锁,其他操作需要等待执行完毕后才能访问,1.8之前ConcurrentHashMap是采用分离锁形式,没有对整表进行锁定,而是对局部进行锁定,不影响其他线程对表的其他地方操作。1.8之后ConcurrentHashMap采用CAS算法进行安全实现线程安全。

线程

1、进程、线程的关系与区别

进程是包含多个线程的集合,每一个程序在执行时都是一个进程。
线程是进程中最小的数据单位,每个指令都是一个线程。

进程可以没有线程,但是线程必须要存在于进程,即线程依赖于进程。

2、创建线程的几种方式

  1. 继承Thread类,重写run方法,实例化线程,调用start执行。
  2. 实现Runable接口,实现run方法,实例化线程(需要借助Thread),调用start执行。(无返回值)
  3. 实现Callable接口,实例化FutureTask对象包装Callable的实例化对象,使用实例化FutureTask对象作为Thread对象的target创建并启动新线程,Callable接口的call()方法可以返回执行结果。
  4. 使用匿名内部类:可以通过创建匿名内部类来实现线程的创建和启动,这种方式在简单的场景下可以更加简洁

3、什么是守护线程

守护线程是为了服务用户线程的存在,比如jvm虚拟机会等待用户线程执行完成后关闭,但是不会等待GC线程执行完再关闭。

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

start()方法用来创建线程,底层也是调用了run()方法,这和直接调用run()方法不一样,当你调用run()方法时,是在当前线程调用,使用start()方法是启动一个新线程。

5、什么导致线程阻塞

导致线程阻塞的原因大体上来说是因为需要的资源没有就绪,所以会陷入阻塞状态,需要等待资源就绪后才会继续执行。常见的阻塞原因有以下几种

  • sleep():sleep() 允许 指定以毫秒为单位的一段时间作为参数,它使得线程在指定的时间内进入阻塞 状态,不能得到CPU 时间,指定的时间一过,线程重新进入可执行状态。
  • wait()和notify():两个方法配套使用,wait() 使得线程进入阻塞状态,它有两种形式,一种允许指定以毫秒为单位的一段时间作为参数,另一种没有参数,前者当对应的 notify() 被调用或者超出指定时间时线程重新进入可执行状态,后者则必须对应的 notify() 被调用。

6、wait()与sleep()的区别

  1. sleep()来自Thread类,和wait()来自Object类
  2. sleep()睡眠后不让出系统资源,wait()让其他线程可以占用CPU
  3. sleep需要指定一个睡眠时间,时间一到会自动唤醒.而wait()需要配合notify()使用

7、什么是线程局部变量ThreadLocal

线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享,是一种实现线程安全的方式

8、什么是乐观锁和悲观锁

  • 乐观锁:认为不是每一次操作都需要上锁,但是在执行操作之前会判断是否会对数据执行修改,一般会使用版本号机制或者CAS机制实现
  • 悲观锁,认为每一次操作都需要上锁,比如synchronized,不管三七二十一就给加上锁了

9、版本号机制和CAS机制的区别

  • 版本号机制会在取出记录时携带版本号,在修改版本号时会与版本号对比,如果版本号不一致,则说明出现问题,抛出异常。如果更新操作执行成功则版本号自增。
  • CAS机制在更新前先取出值作为一个缓存,在更新时,会对更新的值做比较 ,如果更新前的值和更新后的值相同则执行更新,否则抛出异常。

10、volatile关键字

被volatile修饰的共享变量保证了不同线程对该变量操作的内存可见性,禁止指令重排序。
就是当你写一个 volatile 变量之前,会插入一个写屏障,读一个 volatile 变量之前,会插入一个读屏障。在你写一个 volatile 变量时,能保证任何线程都能看到你写的值,在写之前,也能保证任何数值的更新对所有线程是可见的

11、线程池的类别

  1. FixedThreadPool(固定大小线程池):固定大小的线程池,线程数量固定不变。适用于需要控制并发线程数量的场景。
  2. CachedThreadPool(缓存线程池):线程数量可动态调整,新任务到来时会创建新线程。适用于执行大量短期异步任务的场景
    3.SingleThreadExecutor(单线程线程池):只有一个工作线程的线程池,保证所有任务按照指定顺序执行。适用于需要顺序执行任务且在单线程中执行的场景。
  3. ScheduledThreadPool(定时任务线程池):定时执行任务或者周期性执行任务的线程池。适用于需要定时执行任务的场景。
  4. WorkStealingPool(工作窃取线程池):JDK 1.8 引入的一种线程池,使用 ForkJoinPool 实现。每个线程都有自己的任务队列,当自己的任务执行完毕后,会去其他线程的队列中窃取任务执行。
  5. CustomThreadPool(自定义线程池):可以根据具体需求自定义线程池,如设置核心线程数、最大线程数、任务队列类型等参数。

12、如何正确创建一个线程池

创建线程池的常见方法包括使用 Executors 工厂类和直接使用 ThreadPoolExecutor 类。
根据阿里巴巴的《Java 开发手册》,在企业开发中,推荐使用 ThreadPoolExecutor 类直接创建线程池,而不建议使用 Executors 工厂类来创建线程池。这是因为 Executors 工厂类虽然提供了一些便捷的方法来创建线程池,但在某些情况下可能会引发一些意想不到的问题。

具体来说,Executors 工厂类创建的线程池存在一些问题:
FixedThreadPool 和 CachedThreadPool 的风险:Executors.newFixedThreadPool() 和 Executors.newCachedThreadPool() 使用的是无界队列,如果任务提交速度过快,可能导致内存溢出。
SingleThreadExecutor 的风险:Executors.newSingleThreadExecutor() 使用的也是无界队列,如果任务提交速度过快,也可能导致内存溢出。
相比之下,使用 ThreadPoolExecutor 类可以更加灵活地配置线程池的参数,包括核心线程数、最大线程数、工作队列类型等,从而更好地控制线程池的行为,避免出现意外情况。因此,在阿里巴巴企业中,建议使用 ThreadPoolExecutor 类来创建线程池,以确保线程池的稳定性和可靠性。

13、线程池的核心参数有哪些

创建一个线程池时,可以通过设置一些核心参数来配置线程池的行为。以下是线程池的一些核心参数:

corePoolSize(核心线程数):
线程池中同时执行的核心线程数量。
核心线程会一直存活,即使没有任务需要执行。
当有新任务提交时,如果核心线程数还没有达到限制,将创建新的核心线程来执行任务。

maximumPoolSize(最大线程数):
线程池中允许的最大线程数量。
当任务提交数量超过核心线程数时,线程池可以创建新的线程来执行任务,直到达到最大线程数。
若任务继续增加,超出最大线程数的任务会被拒绝执行。

keepAliveTime(线程空闲时间):
当线程池中的线程数量超过核心线程数,并且空闲时间达到 keepAliveTime,多余的线程会被销毁。
设置时间单位,如 TimeUnit.MILLISECONDS。

workQueue(任务队列):
用于存储待执行任务的阻塞队列。
不同的队列类型可选择,如 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue 等。
当任务提交数量超过核心线程数时,新任务会被添加到队列中等待执行。

threadFactory(线程工厂):
用于创建新的线程。
可以自定义线程的命名、优先级等属性。

handler(拒绝策略):
当线程池已达到最大线程数并且队列也已满时,新任务无法被提交时的处理策略。
常见的处理策略有 AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy 等。

除了上面提到的几个核心参数外,线程池还有其他一些可选参数可以根据实际情况进行配置。以下是一些常用的可选参数:

allowCoreThreadTimeOut(允许核心线程超时):
若为 true,则核心线程也会在 keepAliveTime 时间内超时并被回收。
默认为 false。

rejectedExecutionHandler(拒绝策略):
当任务无法被提交时的处理策略。
ThreadPoolExecutor 中提供了 4 种拒绝策略,分别是 AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy。
在使用自定义拒绝策略时,需要实现 RejectedExecutionHandler 接口,并重写 rejectedExecution() 方法。
keepAliveTime 和 TimeUnit 的组合设置:

keepAliveTime 参数的单位可以通过 TimeUnit 枚举值进行设置,包括 NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS、MINUTES、HOURS 和 DAYS。
不同的 TimeUnit 设置对应的 keepAliveTime 值不同,如 TimeUnit.SECONDS 对应的 keepAliveTime 值为秒数。

threadFactory 和 rejectedExecutionHandler 的默认实现:
ThreadPoolExecutor 提供了默认的线程工厂和拒绝策略实现,如果不需要自定义,可以直接使用默认实现。

setMaximumPoolSize() 方法:
在运行过程中,可以通过 setMaximumPoolSize() 方法动态地修改最大线程数。这个方法可能会影响到线程池的并发性能,需要慎重使用。

14、创建线程池中的workQueue(任务队列)有哪些可供选择,他们的区别是什么

ArrayBlockingQueueLinkedBlockingQueueSynchronousQueue 是 Java 中常用的三种阻塞队列实现,它们有以下区别:

ArrayBlockingQueue:
ArrayBlockingQueue 是一个基于数组的有界阻塞队列。
它在构造时需要指定容量,即队列中可以存储的元素数量。
如果队列已满,则插入操作将被阻塞,直到队列中有空闲位置。
如果队列为空,则移除操作将被阻塞,直到队列中有元素可供移除。
ArrayBlockingQueue 是线程安全的,适用于固定大小的线程池。

LinkedBlockingQueue:
LinkedBlockingQueue 是一个基于链表的可选界阻塞队列。
在构造时,可以选择不指定容量,或者指定一个上限。如果不指定,则容量默认为 Integer.MAX_VALUE,即无界队列。
插入操作将一直成功,除非队列已满。
移除操作将一直成功,除非队列为空。
LinkedBlockingQueue 是线程安全的,适用于无限制大小或大容量的线程池。

SynchronousQueue:
SynchronousQueue 是一个不存储元素的阻塞队列。
每个插入操作必须等待一个对应的移除操作,反之亦然。
插入和移除操作是成对的,无法独立进行。
SynchronousQueue 是线程安全的,适用于线程池中的任务移交。

总结:
ArrayBlockingQueue 是一个有界阻塞队列,固定大小,适用于固定大小的线程池。
LinkedBlockingQueue 是一个可选界阻塞队列,默认情况下无界,适用于无限制大小或大容量的线程池。
SynchronousQueue 是一个不存储元素的阻塞队列,仅用于线程之间直接传输任务。

15、创建线程池中的handler(拒绝策略)有哪些可供选择,他们的区别是什么

AbortPolicy(默认策略):
当线程池无法执行新任务时,会抛出 RejectedExecutionException 异常。
这是默认的拒绝策略。

CallerRunsPolicy:
当线程池无法执行新任务时,会使用调用线程来执行该任务。
这可能会降低整体的处理速度,但可以保证不会丢失任务。

DiscardPolicy:
当线程池无法执行新任务时,会丢弃被拒绝的任务,不做任何处理。
使用这个策略可能会导致任务丢失,不建议在需要保证任务不丢失的情况下使用。

DiscardOldestPolicy:
当线程池无法执行新任务时,会丢弃队列中最旧的任务,然后尝试重新提交新任务。
这样可以腾出空间给新任务,但可能会丢失一些等待时间较长的任务。

自定义拒绝策略:
可以通过实现 RejectedExecutionHandler 接口来自定义拒绝策略,重写 rejectedExecution() 方法来定义具体的处理逻辑。

JavaEE

1、http get post的区别

http是超文本传输协议,它规定通过浏览器访问服务器时要遵循请求发送的规则,要求请求的格式要有请求行,请求头,请求体。get方法没有请求体

get和post则是发送请求的两种不同的方法

get请求可以被缓存,可以保存在历史浏览记录中,可以被收藏为书签,响应速度比post快,但是缺乏安全性,请求的数据会在地址栏中显示,请求长度限制最大4k。

post请求不可以被缓存,不会保留在历史浏览记录中,不能被收藏为书签,请求长度没有限制,请求数据不会显示在地址栏上,请求头比get更大。

2、Servlet生命周期

servlet包含四个生命周期

  1. 加载和实例化:当Servlet容器监听到有请求时,会实例化相应的servlet。
  2. 初始化:当示例化后,将会调用init()方法对servlet对象进行一定的初始化处理
  3. 处理请求:当成功初始化后,开始处理用户的请求,并返回给用户
  4. 销毁服务:当Servlet容器检测到已经执行完,就会调用destory()方法释放这个Servlet实例对象。

3、jsp和Servlet的相同点和不同点

jsp是servlet的技术加强,所有的jsp文件都会被解析成一个继成HttpServlet的类,这个类可以对外访问,并且可以动态的生成HTML,XML,或其他格式的web文档返回给用户。

servlet是应用在java文件中,处理用户请求,以HTML,XML,或其他格式返回给用户。

jsp多侧重于页面展示,servlet侧重处理业务逻辑。

4、内置对象和四大作用域

jsp拥有9个内置对象,4个作用域

内置对象

  1. request: 代表对象的请求对象,包含form表单的数据,浏览器信息等。
  2. response: 代表对客户端的相应对象,封装返回给用户的数据,发送重定向编码。
  3. session: 代表用户的一个连接对象,用于追踪用户会话,同时可以将用户的一些数据放入session域中,只要用户不断开此次连接,就可以一直读取session域中的数据。
  4. pageContext: 代表当前页面的一切属性,同时可以通过这个对象获取其他8个对象。。
  5. application: 提供了关于服务器版本,应用参数等和资源的绝对路径等。
  6. out: 代表输出流对象,用作向浏览器返回文本一级的数据,可以直接动态生成html文件。
  7. config: 代表servlet的配置信息对象,可以获取servlet的配置信息。
  8. page: 代表当前页面自身对象。
  9. exception: 代表jsp运行期间产生的异常对象。

作用域

  1. application: web程序的全局范围 自启动就存在,直到服务器关闭才销毁。
  2. session: 用作同一个会话中使用,会话关闭后,session域销毁。
  3. request: 只能在同一个一个请求中转发。
  4. pageContext: 只能在当前jsp页面中使用的数据。

5、Session和Cookie的区别

两者同是追踪回话的一种方式,最直观的区别就在于Session是在服务器端存储用户的登录信息,Cookie是在客户端浏览器存储用户的登录信息。

Seesion的机制决定了用户只能获取自己的session,其他用户的seesion不可见,各客户的session相互独立不可见。seesion的使用比cookie要方便,但是会对服务器产生一定的压力。

6、MVC模式和MVC各部分实现

MVC是目前B/S架构中最常见的设计思想,利用分层思想将程序的整个运行流程分为三层,方便开发中的逻辑处理,实现每一层处理不同的业务。
M: 用于处理应用程序数据逻辑的部分。通常模型对象负责在数据库中存取数据。
V: view代表向用户展示的页面数据。
C: 是应用程序中处理用户交互的部分。通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据。

JVM虚拟机

1、java类加载过程

Java类的生命周期为:加载、验证、准备、解析、初始化、使用、卸载七个生命周期,其中,加载、验证、准备、解析、初始化可以称之为类的加载过程。

首先将class文件转为二进制流,接着验证是否为class文件和文件的正确性,验证完毕后在内存中生成该类对象,接着对类中的属性进行预定义,在局部变量表中进行分配存储。解析主要完成符号引用到直接引用的转换动作,有可能在初始化之后开始解析。初始化是对类中的变量进行初始化,加载构造方法,剩余的都又jvm进行初始,之后才开始执行类中定义的java程序代码。开始使用,最后卸载。

2、描述JVM加载Class文件的原理机制

当运行指定程序时JVM会按照一定规则编译并加载class文件,组织成为一个完整的java应用程序,这个过程由类加载器完成。一般有隐式加载(使用new的方式创建对象)和显示加载(利用class.forName()创建对象)两种方式。
类的加载是动态的,它不会一次性将所有的类加载完后再执行,而是保证基础类的加载,至于其他类则在需要时加载。

3、java内存分配

在这里插入图片描述
java内存分为以下5个区域:

  1. 堆:是jvm中最大的一块内存区域,用于存放对象实例,数组等。
    堆中内存又分为新生代老年代,内存分配占比为 1:2。
    新生代又分为Eden区和两块Survior区,内存占比为 8:1:1
  2. 栈:也叫虚拟机栈,可以理解为一个线程,在这个线程里执行的每个方法都是一个栈帧,而一个栈帧又包含了【局部变量表,操作数栈,动态链接,返回地址】。
  3. 方法区 :方法区中又分为静态方法区和非静态方法区。静态方法区用来存储已经被JVM加载的常量,静态变量等,是可以被多个线程共享的。非静态方法区是用于存储需要实体对象调用的方法。
  4. 本地方法栈:非Java语言实现的本地方法的堆栈,提供jvm和本机操作系统的功能接口。
  5. 程序计数器:保存有当前正在执行的JVM指令的地址。

4、GC是什么?为什么要有GC?

GC是JVM虚拟机中的垃圾回收器,可以自动监测堆内存中是否存在垃圾对象,从而达到自动回收内存的目的,尽量避免内存溢出。Java 语言没有提供释放已分配内存的显示操作方法。

5、简述java垃圾回收机制

在 Java 中垃圾回收由虚拟机自行执行。JVM 有一个垃圾回收线程,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫描垃圾对象,将他们回收。

6、如何判断一个对象是否存活

判断一个对象是否存活有两种方法:
引用计数法:给每一个对象设置一个引用计数器,当有一个地方引用这个对象时,计数器加一,引用失效时,计数器减一。当一个对象的引用计数器为零时,说明此对象没有被引用,也就是“死对象”,将会被垃圾回收。
引用计数法有一个缺陷就是无法解决循环引用问题,所以主流的虚拟机都没有采用这种算法。
可达性算法:从一个被称为 GC Roots 的对象开始向下搜索,如果一个对象到 GC Roots 没有任何引用链相连时,则说明此对象不可用。当一个对象不可达 GC Root 时,这个对象并不会立马被回收,若要被真正的回收需要经历两次标记。当经历两次标记后该对象将被移除” 即将回收”集合,等待回收。

7、垃圾回收的优点以及原理

在程序开发过程中,最让人头疼的就是内存回收,但是java引入了垃圾回收机制,使此类为题迎刃而解,使开发人员在开发中不用再考虑内存管理。
垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。不可预知的情况下对内存中已经死亡的或者长时间没有使用的对象进行清除和回收,开发者不能手动的调用垃圾回收器对某个对象进行垃圾回收。
回收机制有分代复制垃圾回收标记垃圾回收增量垃圾回收

8、垃圾回收器可以马上回收内存吗?有什么办法可以主动通知JVM进行垃圾回收?

当程序员创建对象时,GC 就开始监控这个对象的地址、大小以及使用情况。GC 采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式判断是不是不可引用的垃圾对象,当确定是垃圾对象后GC会回收这些内存空间。而程序员虽然可以手动执行System.gc()但是java不能保证一定会销毁对象。

9、java中存在内存泄漏吗?

内存泄露就是指一个不再被程序使用的对象或变量一直被占据在内存中。Java 中有垃圾回收机制,它可以保证一个对象不再被引用的时候,对象将被垃圾回收器从内存中清除。
而java中使用有向图进行垃圾回收管理,可以消除引用循环的问题。
当然java也存在内存泄漏的可能,比如创建了一个对象,以后一直不再使用这个对象,这个对象却一直被引用,即这个对象无用但是却无法被垃圾回收器回收的,这就是 java中可能出现内存泄露的情况。

10、谈谈深copy和浅copy

深拷贝就是更改copy后的对象数据,原对象数据不变。浅拷贝就是更改copy后的对象数据原对象数据也跟着改变。简单来说就是深拷贝复制对象的值,浅拷贝是复制对象的地址。

11、如果对象的引用被置为null,垃圾收集器是否会立即释放对象占用的内存

如果对象的引用被置为null,只是断开了当前线程栈帧中对该对象的引用关系,而垃圾收集器是运行在后台的线程,只有当用户线程运行到安全点或者安全区域才会扫描对象引用关系,扫描到对象没有被引用则会标记对象,这时候仍然不会立即释放该对象内存,因为有些对象是可恢复的(在 finalize方法中恢复引用 )。只有确定了对象无法恢复引用的时候才会清除对象内存。

12、对象什么时候可以被垃圾回收

当对象没有变量引用的时候,这个对象就可以被回收了。

13、简述java内存分配与回收测率以及MinorGC

• 对象优先在堆的 Eden 区分配
• 大对象直接进入老年代
• 长期存活的对象将直接进入老年代
当 Eden 区没有足够的空间进行分配时,虚拟机会执行一次Minor GC 。Eden 区的对象生存期短,所以可能会频繁触发MinorGC,触发MinorGC后会将未被释放掉的对象放入S0或S1内存,如果要放入对象大于S0或S1内存的50%,则跳过S1或S0直接放入老年代。

14、JVM的永久代中会发生垃圾回收吗?

垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发Full GC
注:Java 8 中已经移除了永久代,新加了一个叫做元数据区的native内存区。

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

类加载器是一个用来加载类文件的类。Java源代码通过javac编译器编译成类文件。然后JVM执行加载后的字节码。类加载器负责加载文件系统、网络或其他来源的类文件。

加载器说明
启动类加载器(BootstrapClassLoader)用来加载 Java 核心类库,无法被 Java 程序直接引用。
扩展类加载器(ExtensionsClassLoader)用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
系统类加载器(SystemClassLoader)根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java应用的类都是由它来完成加载的。
用户自定义类加载器通过继承 java.lang.ClassLoader 类的方式实现。

计算机网络

网络基础知识

1、什么是HTTP和HTTPS?它们有什么区别?

HTTP(Hypertext Transfer Protocol)是一种用于在Web浏览器和Web服务器之间传输数据的协议。它是基于客户端-服务器模型的,使用TCP作为传输协议,通过URL来定位资源,并使用请求-响应的方式进行通信。

HTTPS(Hypertext Transfer Protocol Secure)是在HTTP的基础上添加了安全性支持的协议。它通过使用SSL(Secure Sockets Layer)或TLS(Transport Layer Security)协议对通信进行加密,从而确保传输过程中的数据安全性。

主要区别如下:
安全性:HTTP是明文协议,数据传输过程中的内容是不加密的,易受到窃听和篡改的风险。而HTTPS通过使用SSL/TLS协议对数据进行加密,确保传输过程中数据的机密性和完整性,提供更高的安全性。
端口号:HTTP默认使用端口号80进行通信,而HTTPS默认使用端口号443。这样可以使得网络设备能够根据端口号来区分HTTP和HTTPS流量,从而进行相应的处理。
证书:在使用HTTPS时,服务器需要拥有一个有效的数字证书。该证书由受信任的第三方机构颁发,用于验证服务器的身份。这样可以防止中间人攻击,确保通信的安全性。

HTTPS相比于HTTP提供了更高的安全性,适用于需要保护用户隐私和敏感数据的场景,如登录、支付等。而HTTP则适用于不涉及敏感信息传输的普通网页浏览等场景。为了确保数据的安全性,使用HTTPS仍然需要注意其他方面的安全措施,如防止跨站脚本攻击(XSS)、点击劫持等。

2、TCP和UDP之间有什么区别?你能举出一些使用TCP和UDP的具体应用场景吗?

连接性:TCP是面向连接的协议,通信前需要建立连接、传输数据、然后释放连接;而UDP是无连接的协议,通信时不需要建立连接,直接发送数据包。
可靠性:TCP提供可靠的数据传输,通过确认和重传机制来确保数据的完整性和顺序性;UDP不提供数据传输的可靠性,数据包可能丢失或乱序,并不关心对方是否收到数据包的情况。
面向字节流和面向报文:TCP是面向字节流的协议,将数据视为字节流进行传输;UDP是面向报文的协议,每个数据包都是一个完整的报文。
拥塞控制:TCP具有拥塞控制机制,可以根据网络情况动态调整传输速率;UDP没有拥塞控制,数据包只能以发送者设定的速率传输。

TCP常用于需要可靠传输的场景,如网页浏览、电子邮件传输、文件下载等。
UDP常用于对实时性要求较高、容忍少量数据丢失的场景,如直播的音视频流媒体传输、在线游戏、VoIP通话等。

3、解释一下OSI模型中每一层的功能以及它们之间的关系。

物理层(Physical Layer):负责传输比特流,主要涉及物理介质、数据传输速率、电压等物理特性。

数据链路层(Data Link Layer):负责在相邻节点之间传输数据帧,提供可靠的数据传输服务。包括物理寻址、错误检测与纠正等功能。

网络层(Network Layer):负责在不同网络之间传输数据包,实现数据的路由和转发。包括逻辑寻址、路由选择、拥塞控制等功能。

传输层(Transport Layer):负责端到端的数据传输,提供可靠的数据传输服务。包括数据分段、流量控制、错误恢复等功能。常见的协议有TCP和UDP。

会话层(Session Layer):负责建立、管理和终止会话连接,确保数据的顺序传输和同步。处理会话层的协议如SIP、NetBIOS等。

表示层(Presentation Layer):负责数据格式转换、加密解密、数据压缩等操作,确保数据在不同系统之间的兼容性和可靠性。

应用层(Application Layer):提供用户接口和网络应用服务,包括HTTP、FTP、SMTP等协议,实现用户与网络的交互。

4、什么是IP地址?IPv4和IPv6有何不同?

IP地址(Internet Protocol address)是用来标识网络上设备的数字地址,它是计算机在网络上的唯一标识,类似于现实世界中的门牌号码。IP地址可以分为IPv4和IPv6两种版本。

IPv4是目前广泛使用的IP地址协议版本,它由32位二进制数表示,通常以每8位二进制数为一组,用十进制数表示
IPv6是下一代IP地址协议版本,它由128位二进制数表示,通常以每16位二进制数为一组,用十六进制数表示

IPv4和IPv6主要的不同点在于地址长度和表示方式上的差异,IPv6拥有更大的地址空间和更多的功能特性,能够更好地支持互联网的持续发展和扩张

5、什么是DNS?它的作用是什么?描述DNS解析过程。

DNS(Domain Name System)是互联网中用于将域名解析为对应IP地址的分布式数据库系统,它提供了域名和IP地址之间的映射关系,使得用户可以通过使用易记的域名来访问互联网上的各种服务和资源。
DNS的主要作用包括以下几个方面:

域名解析:将用户输入的域名转换成对应的IP地址,以便能够在网络中定位到特定的主机或服务器。
逆向解析:将IP地址反向解析成对应的域名,用于确定某个IP地址对应的主机名或域名。

6、什么是路由器?它在计算机网络中扮演什么样的角色?

路由器(Router)是一种网络设备,用于在不同网络之间传输数据包,并根据目标地址选择合适的路径进行转发。路由器在计算机网络中扮演着非常重要的角色,它主要用于实现不同网络之间的互联互通,确保数据能够在网络中准确、快速地传输。

路由器在计算机网络中的主要角色包括:
数据包转发:路由器接收到数据包后,会根据数据包中的目标IP地址,查找路由表确定最佳路径,然后将数据包转发到相应的下一跳路由器或目标主机。
网络分割:路由器可以将一个大的网络划分为多个子网,实现对不同子网的管理和控制,提高网络性能和安全性。
网络连接:路由器可以连接不同类型的网络,如LAN(局域网)与WAN(广域网),实现不同网络之间的通信交换。
数据包过滤:路由器可以根据配置的访问控制列表(ACL)等规则,对数据包进行过滤和检查,保护网络安全。
负载均衡:路由器可以根据负载情况,动态调整数据包的传输路径,实现负载均衡,提高网络性能和可靠性。

7、什么是网络拓扑结构?列举几种常见的网络拓扑结构并比较它们的优缺点。

网络拓扑结构是指计算机网络中各个节点之间连接的方式和布局方式。不同的网络拓扑结构对于数据传输、网络容错性和成本等方面有不同的影响。

常见的网络拓扑结构包括:
星型拓扑结构:
优点:易于管理和维护,故障定位简单,适合小型网络。
缺点:中心节点故障会导致整个网络瘫痪,扩展性受限。

总线型拓扑结构:
优点:易于布线,成本较低,适合小型网络。
缺点:如果主干线路出现问题,整个网络将受到影响,扩展性受限。

环型拓扑结构:
优点:对称性强,适合小型网络。
缺点:当某个节点或线路出现问题时,整个环型结构会受到影响。

网状型拓扑结构:
优点:具有高度的冗余性和容错性,适合大型网络。
缺点:成本较高,管理和维护较为复杂。

树型拓扑结构:
优点:结构清晰,扩展性较好,适合中等规模的网络。
缺点:如果根节点出现问题,整个网络将受到影响。

8、解释一下ARP协议的作用和原理。

ARP(地址解析协议)是一种用于在IPv4网络中将IP地址解析为对应的物理MAC地址的协议。
ARP的作用是通过查询局域网内的设备,获取目标设备的MAC地址,从而实现数据包的传输。当主机需要发送数据包给同一局域网内的另一个主机时,它会首先检查目标主机的IP地址是否存在于其本地的ARP缓存表中。如果存在,则直接使用对应的MAC地址进行通信;如果不存在,则需要发送ARP请求广播,询问目标主机的MAC地址。

9、什么是三次握手和四次挥手?描述TCP连接的建立和断开过程。

TCP连接的建立过程(三次握手):
客户端向服务器发送一个带有SYN标志的数据包,表示请求建立连接。
服务器接收到客户端的SYN请求后,会回复一个带有ACK和SYN标志的数据包,表示同意建立连接,并确认客户端的SYN。
客户端收到服务器的确认后,也会发送一个带有ACK标志的数据包,表示连接建立成功。

TCP连接的断开过程(四次挥手):
客户端向服务器发送一个带有FIN标志的数据包,表示请求断开连接。
服务器收到客户端的FIN后,会回复一个带有ACK标志的数据包,表示确认收到客户端的请求。
服务器在完成当前发送的数据传输后,也会向客户端发送一个带有FIN标志的数据包,表示自己也准备断开连接。
客户端收到服务器的FIN后,会回复一个带有ACK标志的数据包,表示确认收到服务器的请求。此时客户端和服务器的连接就断开了。

网络安全

1、什么是SQL注入攻击?如何防范SQL注入攻击?

SQL注入攻击是一种利用Web应用程序对用户输入数据的处理不当,(如:用户输入未经验证或过滤、动态构建SQL语句等),从而向后端数据库中插入恶意SQL语句的安全漏洞。通过SQL注入攻击,黑客可以执行未经授权的数据库操作,如删除数据、修改数据、获取敏感信息等。

防范SQL注入攻击的方法包括:

  1. 使用参数化查询:使用参数化查询可以将用户输入的数据作为参数传递给SQL查询,而不是将用户输入直接拼接到SQL语句中,从而避免了SQL注入攻击。
  2. 输入验证和过滤:对用户输入的数据进行验证和过滤,确保只接受符合预期格式和类型的数据。例如,可以限制输入的长度、格式、特殊字符等。
  3. 使用ORM框架:ORM(对象关系映射)框架可以帮助开发人员更安全地与数据库交互,避免手动编写SQL语句,从而减少SQL注入的风险。
  4. 最小权限原则:在数据库访问控制上,给予应用程序最小必要的权限,避免应用程序具有对数据库执行敏感操作的权限,从而降低SQL注入攻击造成的危害。
  5. 定期更新和维护:定期更新数据库系统和应用程序,及时修补已知的SQL注入漏洞,以保障系统的安全性。

2、什么是跨站脚本攻击(XSS攻击)?如何防范XSS攻击?

XSS攻击)是一种利用Web应用程序对用户输入数据的处理不当,将恶意脚本注入到网页中,从而在用户的浏览器上执行恶意脚本的安全漏洞。通过XSS攻击,攻击者可以窃取用户的会话信息、篡改网页内容、重定向用户浏览器等,造成一系列安全问题。

防范XSS攻击的方法包括:

  1. 输入验证和过滤:对用户输入的数据进行验证和过滤,确保只接受符合预期格式和类型的数据,同时移除或转义特殊字符,避免恶意脚本的注入。
  2. 输出编码:在Web应用程序输出用户输入的数据时,使用适当的编码方式进行转义,以确保浏览器不会将用户输入的数据解释为可执行的脚本。
  3. HTTP头部设置:通过设置HTTP响应头部来加强浏览器的安全策略,如设置X-Content-Type-Options、X-XSS-Protection、Content-Security-Policy等头部,以帮助防范XSS攻击。
  4. 使用HttpOnly标记:在设置敏感的cookie时,使用HttpOnly标记,这样可以防止JavaScript访问该cookie,减少XSS攻击的可能性。
  5. 安全性审查与培训:开发人员应该接受关于XSS攻击的培训,了解如何编写安全的代码,并且进行代码审查,及时发现和修复潜在的XSS漏洞。
  6. 使用内容安全策略(CSP):内容安全策略是一种强大的工具,可以帮助网站管理员控制页面上能够执行的脚本资源,从而有效地防范XSS攻击。

3、请解释一下什么是DDoS攻击,以及如何应对这种攻击?

DDoS攻击是一种分布式拒绝服务攻击,它通过大量的合法或非法的请求来淹没目标服务器或网络,导致目标系统无法正常工作或服务中断。攻击者通常会使用大量的计算机、服务器或物联网设备等构成一个“僵尸网络”,利用这些设备来发起攻击。

应对DDoS攻击的方法包括:

  1. 流量清洗:流量清洗是一种将所有进入的网络流量进行分类和过滤的技术。在DDoS攻击时,反向代理或专业的DDoS防护设备可以根据预定义的规则,对网络流量进行分类和过滤,从而保证只有合法的流量到达目标服务器。
  2. 增加带宽:增加带宽是一种增强目标服务器的处理能力的方法。通过增加网络带宽,可以使目标服务器能够更快地处理大量的请求,并且减少DDoS攻击的影响。
  3. 负载均衡:负载均衡技术可以将用户请求分散到多个服务器上,从而减轻单个服务器受到DDoS攻击的压力。这样可以保证即使某个服务器被攻击,其他服务器仍然能够正常提供服务。
  4. 云防护:云防护是一种将DDoS攻击流量引导到云端进行处理的方法。通过将目标服务器的IP地址更改为云防护服务商的IP地址,可以将恶意请求引导到云端,并进行流量清洗、过滤和拦截,最终只将合法请求传递到目标服务器。
  5. 监测与预警:及时监测网络流量和服务器负载变化,并设置预警机制可以帮助管理员发现并及时应对DDoS攻击。
  6. 安全培训:加强网络安全意识培训,提高员工防范DDoS攻击的意识。

4、什么是加密算法?请列举几种常见的加密算法,并简要说明其特点和适用场景。

加密算法是一种通过对数据进行转换和处理,使其在未授权的情况下无法被读取或理解的算法。加密算法可以用于保护数据的机密性、完整性和可用性,确保数据在传输和存储过程中不被窃取或篡改。

以下是几种常见的加密算法以及它们的特点和适用场景:
对称加密算法:
DES(Data Encryption Standard):DES是一种对称加密算法,使用56位密钥对数据进行加密和解密。DES已经被认为安全性较低,通常用于保护低级别的敏感数据。
AES(Advanced Encryption Standard):AES是目前最常用的对称加密算法之一,支持128位、192位和256位三种密钥长度,安全性高,适用于保护各种类型的数据,如金融数据、个人信息等。
非对称加密算法:
RSA(Rivest-Shamir-Adleman):RSA是一种非对称加密算法,使用公钥和私钥进行加密和解密。RSA广泛应用于数字签名、数据加密等场景,适用于保护通信数据的安全性。
ECC(Elliptic Curve Cryptography):ECC是一种基于椭圆曲线数学原理的非对称加密算法,相比RSA在相同安全级别下具有更小的密钥长度和更高的性能,适用于移动设备、物联网设备等资源受限的场景。
哈希算法:
MD5(Message Digest Algorithm 5):MD5是一种哈希算法,用于将任意长度的数据映射为固定长度的哈希值。由于MD5存在碰撞攻击风险,已不推荐用于安全性要求高的场景。
SHA-256(Secure Hash Algorithm 256-bit):SHA-256是一种安全性更高的哈希算法,生成256位的哈希值,广泛应用于数字签名、数据完整性验证等场景。

5、请解释一下什么是公钥基础设施(PKI),以及PKI在网络安全中的作用。

公钥基础设施(PKI)是一种由多个组件和协议组成的框架,用于管理和分发数字证书、公钥和密钥对等安全凭证,以确保通信双方的身份验证和数据加密。

PKI包括以下四个主要组件:
数字证书:数字证书是一个电子文档,其中包含了用户或实体的公钥、身份信息以及数字签名等信息,由认证机构(CA)颁发并验证真实性和合法性。
公钥:公钥是由数字证书中提取出来的一个加密算法所需的公钥,用于加密数据以保证数据的机密性和完整性。
私钥:私钥是与公钥配对的一种加密算法所需的秘密密钥,用于解密数据和签名数据以保证数据的真实性和完整性。
认证机构(CA):认证机构是负责数字证书颁发和管理的机构,负责验证用户或实体身份信息,颁发数字证书,并在数字证书过期或被吊销时撤销数字证书。

PKI在网络安全中的作用主要有以下三个方面:
身份验证:PKI通过数字证书和公钥的方式,验证通信双方的身份信息,确保通信双方是合法的、授权的实体,并减少身份欺骗的风险。
数据保护:PKI通过公钥加密和私钥解密的方式,确保通信数据在传输和存储过程中的机密性和完整性,防止数据泄露和篡改等风险。
数字签名:PKI通过私钥签名和公钥验证的方式,确保通信数据的真实性和完整性,防止数据被篡改和伪造,保证通信数据的可信性。

6、如何进行网络漏洞扫描和评估?请列举几种常见的漏洞扫描工具。

进行网络漏洞扫描和评估是保障网络安全的重要步骤,主要包括以下几个步骤:
确定扫描范围:首先确定需要扫描的网络和系统范围,包括内部和外部网络设备、应用程序、操作系统等。
选择合适的漏洞扫描工具:根据实际需求选择适合的漏洞扫描工具进行扫描,可以是开源工具或商业工具,确保工具能够覆盖到目标系统的各种漏洞类型。
进行漏洞扫描:运行选定的漏洞扫描工具对目标系统进行扫描,识别系统中存在的漏洞、弱点和安全风险。
分析扫描结果:对扫描结果进行分析和评估,确定哪些漏洞是真实的、危险程度如何,以及如何修复这些漏洞。
制定漏洞修复计划:根据漏洞扫描结果,制定漏洞修复计划,按照严重性级别和紧急性进行漏洞修复。

常见的漏洞扫描工具包括但不限于:
Nessus:Nessus是一款商业漏洞扫描工具,功能强大,支持广泛的漏洞检测和报告功能。
OpenVAS:OpenVAS是一款开源的漏洞扫描工具,提供漏洞检测、资产管理和报告功能,适合中小型组织使用。
Nmap:Nmap是一款开源的网络扫描工具,可以用于发现主机、服务和漏洞,并提供灵活的扫描选项和脚本功能。
Acunetix:Acunetix是一款专注于Web应用程序漏洞扫描的商业工具,用于检测Web应用程序中的漏洞和安全问题。
QualysGuard:QualysGuard是一款云端的漏洞扫描工具,提供自动化的漏洞扫描和管理解决方案,适用于企业级网络安全评估。

7、请解释一下什么是网络嗅探(sniffing)攻击?如何避免自己成为网络嗅探者的目标?

网络嗅探(sniffing)攻击是指黑客利用网络嗅探工具监视和拦截网络传输的数据包,以获取敏感信息(如账号、密码、信用卡信息等)的一种攻击方式。黑客可以通过网络嗅探工具截取经过网络的数据包,并分析其中的信息,从而获取目标系统或用户的机密信息。

为了避免成为网络嗅探者的目标,可以采取以下几点防范措施:
加密通信数据:使用加密通信协议(如HTTPS、SSH等)保护数据在传输过程中的安全性,避免数据被窃取和篡改。
使用虚拟专用网络(VPN):通过使用VPN可以建立加密的隧道通信,保护数据在公共网络上的传输安全,降低被网络嗅探者截取数据的风险。
定期更新安全补丁:及时更新操作系统、应用程序和网络设备的安全补丁,修复潜在漏洞,减少黑客利用漏洞进行网络嗅探攻击的可能性。
禁用不必要的网络服务:关闭或禁用不必要的网络服务和端口,减少黑客进行嗅探攻击的入口,提高网络安全性。
使用网络入侵检测系统(IDS):部署网络入侵检测系统来监控网络流量,及时发现异常活动和潜在的嗅探攻击行为,加强网络安全防护。
加强访问控制:设置强密码、多因素认证等访问控制措施,限制未授权用户的访问权限,避免敏感信息被窃取。

8、请解释一下什么是端口扫描(Port scanning)?列举几种常见的端口扫描技术和工具。

端口扫描(Port scanning)是指黑客或安全测试人员用来识别目标主机开放的网络端口和服务的过程。通过端口扫描,可以确定目标主机上哪些端口处于监听状态,并根据端口开放情况做进一步的漏洞分析、攻击或安全评估。

以下是几种常见的端口扫描技术和工具:
TCP 扫描:使用 TCP 协议的端口扫描技术,发送 TCP SYN、ACK 或 RST 包到目标主机的端口,根据返回的响应来确定端口的开放状态。常用工具包括 Nmap、Hping等。
UDP 扫描:使用 UDP 协议的端口扫描技术,发送 UDP 数据包到目标主机的端口,根据返回的响应来确定端口的开放状态。常用工具包括 Nmap、NetScanTools等。
SYN 扫描:也称为半开放扫描,通过发送 TCP SYN 包到目标主机的端口,根据返回的响应来确定端口的开放状态。常用工具包括 Nmap、Masscan等。
FIN 扫描:通过发送 TCP FIN 包到目标主机的端口,根据返回的响应来确定端口的开放状态。FIN 扫描通常用于绕过防火墙和入侵检测系统。常用工具包括 Nmap、NetScanTools等。
XMAS 扫描:通过发送 TCP FIN、URG、PSH 标志位都置为 1 的数据包到目标主机的端口,根据返回的响应来确定端口的开放状态。XMAS 扫描通常用于检测主机是否处于活跃状态。常用工具包括 Nmap、NetScanTools等。

常用框架

Spring框架

1、什么是spring?spring有哪些主要模块?

spring框架是一个轻量级,一站式的开源java框架,它提供了综合广泛的基础性支持平台。拥有简单性,可测试性,松耦合性,任何java程序都能从中收益。
spring中已经集成了20多个模块,比如常用的web,jdbc,aop,tx等。

2、使用spring框架有什么好处?

spring框架提供了IoC(控制翻转)和DI(依赖注入)实现了松耦合的依赖关系
spring框架提供了AOP切面编程,对指定功能进行增强,减少冗余代码,提高代码复用性。
spring框架提供了简单的声明式事务,使开发中能够专注逻辑代码而不用花费过多心思管理事务。
spring框架是按照模块的形式来进行组装的,使用的时候一目了然,直接导入使用的模块即可。

3、什么是IoC?什么是DI?

IoC(控制翻转)将对象交给spring进行创建、初始化、管理、销毁,并存放在IoC容器中。
DI(依赖注入)在程序使用IoC容器中的某个对象时,从容器中获取对象。有三种注入方式:set方法注入,构造方法注入,工厂方法注入,静态方法注入,接口注入。

4、spring中的单例Bean是线程安全的吗?

spring框架没有对单例Bean进行任何的线程封装处理,关于单例Bean的线程安全问题需要开发者自己解决。但是在实际开发中,大部分bean都没有可变状态,所以从某种情况上考虑,spring的单例是安全的,如果bean有多种状态的话,就需要自己保证线程安全。最常用的办法就是将bean的作用域由singleton变更为prototype。

5、Spring中都用到了哪些设计模式?

spring中用到了大量的设计模式具体如下:

  • 代理模式:在AOP中使用,使用jdk的动态代理和cglib动态代理。
  • 单例模式:在spring配置文件中定义的bean默认是单例模式。
  • 工厂模式:BeanFactory用来创建对象的实例。
  • 模板模式:用来解决代码重复的问题,比如RestTemplate,JDBCTemplate。

6、IoC容器中Bean的生命周期

  1. 通过构造器或工厂方法创建bean实例。
  2. 为bean的属性设置值和对其他bean的引用。
  3. 将bean实例传递给bean后置处理器的postProcessBeforeInitialization方法
  4. 调用bean的初始化方法。
  5. 将bean实例传递给bean后置处理器的postProcessAfterInitialization方法
  6. bean可以使用了。
  7. 当容器关闭时,调用bean的销毁方法(destroy-method)。

7、解释Spring支持的几种Bean的作用域

作用域说明
singletonbean在每个spring ioc容器中只有一个实例,配置文件配置后默认是单例
prototype一个bean的定义可以有多个实例
request每次http请求都会创建一个bean,该作用域仅在基于web的Spring ApplicationContext情形下有效
session在一个HTTP Session中,一个bean定义对应一个实例,该作用域仅在基于web的Spring ApplicationContext情形下有效
global-session在一个HTTP Session中,一个bean定义对应一个实例,该作用域仅在基于web的Spring ApplicationContext情形下有效,缺省的Spring bean的作用域是Singleton

8、Spring几种属性装配方式

  1. no:默认的方式是不进行自动装配,通过显示的ref属性进行装配。
  2. byName:通过配置的id进行自动装配,如果在配置文件中bean的autowire属性被设置为byname,那么在进行匹配时,装配和该bean的属性具有相同名字的bean。
  3. byType:通过参数类型自动装配,如果再配置文件中bean的的autowire设置为bytype,在进行匹配时,装配和该bean的属性具有相同类型的bean,如果有多个bean符合条件则抛出异常。
  4. constructor:这个方式类似于byType,但是要提供构造器参数,如果没有确定的带参数的构造器,则会抛出异常。
  5. autodetect:首先尝试使用constructor来自动装配,如果无法工作,则用byType。

9、什么是AOP?

AOP是面向切面编程,是一种编程技术,允许对功能做横向切割,对目标方法做增强,可以有效的增加代码复用性,减少冗余代码。
AOP底层是以jdk动态代理和cglib动态代理技术实现的,jdk的动态代理是对接口方法进行增强,所以要求切入点至少要实现一个接口,否则spring会使用另一个cglib动态代理进行增强,而cglib是在程序运行期间找到继承的父类,并生成代理后的子类。
切面是我们要织入的功能,可以是一个类中的一个方法,也可以是一个类中的多个方法。
切入点是我们要对哪个功能进行增强,在开发中,我们通常理解为一个切入点就是一个方法。
增强的方式也有前置增强,返回前增强,返回后增强,异常增强,环绕增强,总共5中方式,具体使用哪种方法还要参考具体业务逻辑需求。

10、简单说明Spring的声明式事务?

spring有自己的一套事务管理机制,一般使用TransactionMananger进行管理,可以通过配置文件进行注入来完成此功能,通过配置可以实现对事务级别和传播性的控制。而底层也是使用到了spring的AOP思想。

11、Spring中BeanFactory和ApplicationContext的区别

ApplicationContext是BeanFactory的子类,两者都有getBean这个核心方法
Beanfactory只用基本只有跟Bean相关的功能,而ApplicationContext则在此基础上有增加了很多bean的细节,且进行了一定程度的封装。

springMVC框架

1、SpringMVC运行流程

  1. 用户请求发送给DispatcherServlet,DispatcherServlet调用HandlerMapping处理器映射器;

  2. HandlerMapping根据xml或注解找到对应的处理器,生成处理器对象返回给DispatcherServlet;

  3. DispatcherServlet会调用相应的HandlerAdapter;

  4. HandlerAdapter经过适配调用具体的处理器去处理请求,生成ModelAndView返回给DispatcherServlet

  5. DispatcherServlet将ModelAndView传给ViewReslover解析生成View返回给DispatcherServlet;

  6. DispatcherServlet根据View进行渲染视图;

2、什么是DispatcherServlet

Spring的MVC框架是围绕DispatcherServlet来设计的,它用来处理所有的HTTP请求和响应。

3、Spring MVC的控制器是不是单例模式

是单例模式,所以在多线程访问的时候有线程安全问题,不要用同步,会影响性能的,解决方案是在控制器里面不能写字段。

4、Spring MVC怎么和AJAX相互调用的

通过Jackson框架就可以把Java里面的对象直接转化成Js可以识别的Json对象。
这样就可以直接返回一个json字符串形式的json对象,但是要注意方法上要加上@ResponseBody注解,或者类上加@RestController注解。

Mybatis

1、什么是ORM

是对象关系映射,是一种为了解决关系型数据库与java对象的映射技术,ORM是通过使用描述对象和数据库之间的映射关系,将程序中的对象自动持久化到关系型数据库中。

2、MyBatis的工作原理

  1. 读取mybatis配置文件
  2. 加载映射文件
  3. 创建SqlessionFactory(构造会话工厂)
  4. 创建会话对象(SqlSession)
  5. 调用sql执行器
  6. 输入参数映射
  7. 输出结果映射

3、为什么需要预编译

  1. 预编译是指在发送sql语句进行执行之前,对sql语句先进行编译,这样数据库在执行时就不需要再次编译
  2. 还可以预防sql注入

4、Mybatis是否支持延迟加载?

mybatis支持一对一、一对多的延迟加载
当调用方法进行查询时,进入拦截器方法,当判断到关联查询的值为null时,会去查找相对应的sql语句进行二次查询,然后在赋予值。

5、#{}和${}的区别

  1. #{}是占位符,预编译处理;${}是拼接符,字符串替换,没有预编译处理。
  2. Mybatis在处理#{}时,#{}传入参数是以字符串传入,会将SQL中的#{}替换为?号,调用PreparedStatement的set方法来赋值。
  3. #{} 可以有效的防止SQL注入,提高系统安全性;${} 不能防止SQL 注入
  4. #{} 的变量替换是在DBMS 中;${} 的变量替换是在 DBMS 外

6、模糊查询like语句该怎么写

  1. '%${question}%' 可能引起SQL注入,不推荐
  2. "%"#{question}"%"
  3. CONCAT(’%’,#{question},’%’)

7、mapper中如何对应多个值

  1. 顺序传参法:#{}里的数字代表接口形参中的传入顺序
  2. @Param注解:在接口参数前使用@Param注解指定,#{}中传入的变量名
  3. map传参:#{}中的内容是map中的key值
  4. 对象传参:将传递的参数封装成一个java对象,#{}中传入的是对象的属性

8、Dao接口的工作原理是什么?Dao接口里的方法,参数不同时,方法能重载吗

dao接口工作原理是JDK动态代理,在运行时会通过动态代理产生代理对象,代理对象会拦截接口方法,执行所对应的sql语句,随后返回执行结果。

dao接口方法是不能重载的,因为要靠全限定名和方法名去进行拦截处理

9、Mybatis是如何将sql执行结果封装为目标对象并返回的

首先判断返回结果是resultType还是resultMap,如果是ResultType会根据全限定类名查找实体类通过反射创建对象,调用set方法给对象赋值。如果是ResultMap则会直接根据配置的名称和数据库查询后的别名进行映射处理。

10、Mybatis动态sql

Mybatis动态sql可以让我们在Xml映射文件内,以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的功能
比如<foreach><if><where>

11、Mybatis的缓存机制

mybatis拥有一级和二级缓存

  1. 一级缓存:在操作数据库时需要创建SqlSession对象,在内存中有一个hashMap缓存数据,每一个SqlSession的缓存互不影响,当同一个SqlSession两次执行相同的sql,则会直接去缓存中获取数据,当SqlSession关闭时,缓存清除。
  2. 二级缓存:首先二级缓存是跨session的,多个session可以共享二级缓存,当一个namespace下执行两次同一个语句,并传递相同的参数,则不在从数据库进行查询,直接从二级缓存中获取,mybatis默认开启一级缓存,但是二级缓存默认关闭。

springBoot框架

1、请简要介绍一下 Spring Boot 的核心特性和优点。

简化配置:Spring Boot 提供了自动化配置(Auto-configuration)功能,大大减少了开发人员对应用程序的配置工作量。通过约定大于配置的原则,开发者可以快速搭建和启动应用程序。
内嵌容器:Spring Boot 支持内嵌容器(Embedded Container),如 Tomcat、Jetty 等,使得应用程序可以独立运行,无需外部应用服务器的支持。
依赖管理:Spring Boot 提供了 Starter 依赖,简化了项目中各种依赖库的管理,开发者只需添加相关 Starter 依赖即可快速集成所需功能模块。
集成测试支持:Spring Boot 提供了良好的测试支持,开发者可以轻松编写单元测试和集成测试,保证应用程序的质量。
监控与管理:Spring Boot Actuator 模块提供了丰富的监控和管理功能,如健康检查、性能指标、应用信息等,方便开发者监控应用程序的运行状态。
生态系统:Spring Boot 集成了大量的第三方库和插件,支持各种应用场景的开发,如数据库访问、安全认证、缓存等,使得开发更加高效。
微服务支持:Spring Boot 非常适合构建微服务架构,支持 RESTful Web 服务开发,同时提供了微服务治理的解决方案。

2、Spring Boot 如何实现自动化配置(Auto-configuration)?

当应用程序启动时,Spring Boot 会扫描项目中的类路径,并加载所有的配置类、组件和依赖,通过 @EnableAutoConfiguration 注解,Spring Boot 启用自动配置功能。这个注解通常放在主配置类(如 SpringApplication 的入口类)上。Spring Boot 会加载所有在类路径下的 META-INF/spring.factories 文件中声明的 EnableAutoConfiguration 实现类。这些实现类包含了自动配置的逻辑和条件。Spring Boot 根据条件化配置的规则(@Conditional 注解)来判断是否需要自动配置某些组件或功能。这些条件可以基于环境变量、系统属性、类路径上的资源等。如果项目中引入了 Starter 依赖,Spring Boot 会根据 Starter 中所包含的自动配置来决定需要加载哪些组件。Starter 依赖通常包含了一组相关的库和自动配置,以便快速集成常用功能。根据条件化配置和 Starter 依赖,Spring Boot 开始自动装配各种组件和功能。它会根据约定大于配置的原则,尝试自动配置数据库连接、消息队列、Web 服务器等常见的组件,以及其他必要的配置。自动装配过程中,Spring Boot 将创建并注册各种 Bean 到 Spring 上下文中。这些 Bean 可能是各种服务、控制器、数据库连接池等组件,开发者无需手动编写配置文件或代码。自动配置的顺序也是有优先级的,高优先级的配置会覆盖低优先级的配置。这样,开发者可以通过自定义配置来覆盖默认的自动配置。

3、什么是 Starter 依赖?Spring Boot Starter 的作用是什么?

Spring Boot Starter 的作用包括:
简化依赖管理:Starter 依赖将相关的库和配置打包在一起,开发者只需引入一个 Starter 依赖,就能够快速集成所需的功能,而不必手动管理多个依赖项。
自动配置:Starter 依赖中通常包含了自动配置类,可以根据约定自动配置应用程序所需的组件和功能,简化了配置过程。
约定大于配置:Spring Boot Starter 遵循约定大于配置的原则,提供了一种标准化的方式来组织和使用依赖,降低了集成和配置的复杂度。

自定义的 Spring Boot Starter,可以按照以下步骤进行:

编写自动配置类:编写一个自动配置类,使用 @Configuration 注解标记,并通过条件化注解(如 @ConditionalOnClass)来指定在何种情况下启用自动配置。在自动配置类中,你可以通过 @Bean 注解来定义需要被 Spring 容器管理的 Bean。
创建 spring.factories 文件:创建一个名为 META-INF/spring.factories 的资源文件,在该文件中声明你的自动配置类
提供默认配置:为 Starter 提供默认的配置文件,如 application.properties 或 application.yml,定义一些默认属性值。
编写 Starter 类:创建一个类,继承自 Spring Boot 的 org.springframework.boot.autoconfigure.EnableAutoConfiguration 类,用于启用自动配置。

4、 Log4j、Logback和Slf4j的区别?

常用的日志框架(如 Log4j、Logback)提供了丰富的功能和配置选项,包括日志级别控制、日志输出格式定制、日志滚动策略等。它们具有较高的灵活性和可扩展性,适用于各种日志需求。
Slf4j 是一个日志框架的抽象层,它只定义了一组统一的日志接口,不提供具体的日志实现。Slf4j 的主要目的是在应用程序中使用统一的日志接口,并支持根据需要切换底层的日志实现框架。
常用的日志框架提供了丰富的功能和灵活性,适用于各种日志需求。而 Slf4j 则是一个日志框架的抽象层,提供统一的日志接口,并允许根据需要切换底层的日志实现框架。使用 Slf4j 可以减少对具体日志框架的直接依赖,提高代码的灵活性和可维护性

5、如何在 Spring Boot 中实现跨域请求处理(CORS)?

使用 @CrossOrigin 注解
在控制器类或方法上使用 @CrossOrigin 注解可以实现对特定请求处理跨域访问。例如,在控制器类上添加 @CrossOrigin 注解,可以允许该控制器下所有方法处理跨域请求
全局配置
另一种方式是在 Spring Boot 应用的配置类中进行全局配置,以允许所有请求处理跨域。可以通过 WebMvcConfigurer 接口的 addCorsMappings 方法来配置全局跨域支持

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowedHeaders("*")
                .allowCredentials(true);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

6、Spring Boot 中如何实现异常处理?

实现异常处理可以通过使用@ControllerAdvice注解定义全局异常处理器,或者在@Controller内部使用@ExceptionHandler注解处理局部异常。常用全局异常处理的方式:

全局异常处理器(Global Exception Handler):
创建一个全局异常处理器类,使用@ControllerAdvice注解标记,然后在类中定义异常处理方法,并使用@ExceptionHandler注解指定处理的异常类型。这样可以统一处理应用中抛出的各种异常。

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleException(Exception e) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An error occurred: " + e.getMessage());
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

7、如何在 Spring Boot 中进行单元测试和集成测试?

创建一个类,类上增加@SpringBootTest注解和@RunWith(SpringRunner.class),编写方法,在方法上增加@Test注解
最新版本的 JUnit5 中,不再需要@RunWith(SpringRunner.class)注解,

8、Spring Boot 中如何实现定时任务(Scheduled Task)?

在 Spring Boot 中实现定时任务(Scheduled Task)可以通过@Scheduled注解来实现。

  1. 在 Spring Boot 应用的主类中添加 @EnableScheduling 注解,以启用定时任务支持
  2. 在方法上增加@Scheduled) 注解,还可以通过fixedRatefixedDelayinitialDelaycron来控制不同的用法
	@Scheduled(fixedRate = 5000) // 每隔5秒执行一次
    @Scheduled(fixedDelay = 3000) // 上一次任务完成后延迟3秒再执行下一次
    @Scheduled(initialDelay = 2000, fixedRate = 5000) // 延迟2秒后首次执行,然后每隔5秒执行一次
    @Scheduled(cron = "0 0 12 * * ?") // 每天中午12点执行
  • 1
  • 2
  • 3
  • 4

9、如何在 Spring Boot 中进行配置文件的管理和加载?

  1. File
    使用 File 类来读取资源文件。需要提供完整的文件路径。
File file = new File("src/main/resources/file.txt");
InputStream inputStream = new FileInputStream(file);
  • 1
  • 2

需要注意的是,使用该方法需要提供完整的文件路径,因此需要知道文件所在的绝对路径。这种方式在idea上可以运行,但打成jar包是会报文件找不到异常。

  1. ClassLoader.getResourceAsStream()
    可以使用类加载器来获取资源文件的输入流。该方法接受一个资源文件路径参数,返回一个 InputStream 对象。
	InputStream inputStream = getClass().getClassLoader().getResourceAsStream("File.txt");
  • 1

注意,该方法返回的资源文件路径是相对于类加载器的根路径。因此,对于 resources 目录下的文件,需要在文件名前加上 “ClassPath:” 前缀。例如: “classpath:file.txt”。

  1. Class.getResourceAsStream()
    可以使用 Class 类的 getResourceAsStream() 方法来读取资源文件。该方法接受一个资源文件路径参数,返回一个 InputStream 对象。
	InputStream inputStream = getClass().getResourceAsStream("/file.txt");
  • 1

该方法返回的资源文件路径是相对于当前类的路径。因此,对于 resources 目录下的文件,需要在文件名前加上 “/” 前缀。例如: “/file.txt”。

  1. ResourceLoader
    可以使用 Spring 的 ResourceLoader 接口来加载资源文件。ResourceLoader 接口有一个 getResource() 方法,接受一个资源文件路径参数,返回一个 Resource 对象。DefaultResourceLoader: 默认实现类Resource:资源描述的策略接口ResourceLoader:加载资源的策略接口ResourceLoader 的实现类中,实现的各个getResource方法会将资源包装成不同的Resource接口实现对象进行返回,包括常见类:ClassPathResource,FileSystemResource,ServletContextResource,UrlResource(通过java.net.URL来访问资源,也支持File格式,如“file:”、“http:”。),以及Resource接口中继承体系中的各个实现!
	@Autowired
	private ResourceLoader resourceLoader;
	
	Resource resource = resourceLoader.getResource("classpath:file.txt");
	InputStream inputStream = resource.getInputStream();
  • 1
  • 2
  • 3
  • 4
  • 5
  1. ResourceUtils
    ResourceUtils 是 Spring 提供的一个工具类,用于加载资源文件。可以使用 ResourceUtils.getFile() 方法来获取文件对象。
	File file = ResourceUtils.getFile("classpath:file.txt");
  • 1

需要注意的是,该方法只适用于本地文件系统和 JAR 文件。对于 WAR 文件或者其他类型的文件,该方法可能无法正常工作。

  1. ApplicationContext
    可以使用 ApplicationContext 的 getResource() 方法来加载资源文件。该方法接受一个资源文件路径参数,返回一个 Resource 对象。
	@Autowired
	private ApplicationContext applicationContext;
	
	public void readResourceFile() throws IOException {
	    Resource resource = applicationContext.getResource("classpath:file.txt");
	    InputStream inputStream = resource.getInputStream();
	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  1. ServletContext
    可以使用 ServletContext 的 getResourceAsStream() 方法来读取资源文件。该方法接受一个资源文件路径参数,返回一个 InputStream 对象。
	@Autowired
	private ServletContext servletContext;
	
	public void readResourceFile() throws IOException {
	    InputStream inputStream = servletContext.getResourceAsStream("/WEB-INF/classes/file.txt");
	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  1. Paths 和 Files

可以使用 Java NIO 中的 Paths 和 Files 类来读取资源文件。该方法需要提供完整的文件路径。

Path path = Paths.get("src/main/resources/file.txt");
InputStream inputStream = Files.newInputStream(path);
  • 1
  • 2

需要注意的是,使用该方法需要提供完整的文件路径,因此需要知道文件所在的绝对路径。

  1. ClassPathResource

可以使用 Spring 提供的 ClassPathResource 类来读取资源文件。该方法需要提供资源文件的相对路径。

ClassPathResource resource = new ClassPathResource("file.txt");
InputStream inputStream = resource.getInputStream();
  • 1
  • 2

需要注意的是,ClassPathResource 会在类路径下查找资源文件,因此不需要提供完整的文件路径。

10、SpringBoot事务的使用

  1. 在 Spring Boot 主类中使用 @EnableTransactionManagement 注解,以启用事务管理功能。
  2. 通过使用 @Transactional 注解来管理事务。@Transactional 注解可以应用在方法级别或类级别上,用于标识希望启用事务管理的方法或类。

默认情况下,@Transactional注解只会对受检查异常(RuntimeException 的子类)进行回滚操作,如果需要对所有异常进行回滚,可以使用 rollbackFor 属性。

SpringCloud Netflix & SpringCloud Alibaba

SpringCloud基础

1、什么是微服务架构?它与传统单体架构有什么区别?

微服务架构是一种将应用程序拆分为多个小型、独立部署的服务的架构风格。每个微服务都专注于完成特定的业务功能,并通过轻量级的通信机制(比如 RESTful API)进行交互。这些微服务可以被独立地开发、部署和扩展,从而提高系统的灵活性、可维护性和可伸缩性。
与传统的单体架构相比,微服务架构具有以下几个显著的区别:
模块化:
单体架构通常是将整个应用程序作为一个单独的模块进行开发、部署和维护,所有功能都集中在同一个代码库中。
微服务架构将应用程序拆分为多个小型的服务,每个服务负责一个特定的业务功能,这样可以更好地实现模块化开发和团队自治。
独立部署:
在单体架构中,所有功能模块共享同一个部署单元,一次部署会影响整个应用程序。
微服务架构中,每个微服务都是独立部署的,可以独立更新和扩展,降低了对整个系统的影响范围。
技术多样性:
在单体架构中,通常会使用相同的技术栈和开发框架,不容易引入新的技术或语言。
微服务架构允许每个微服务选择适合自身需求的最佳技术栈,从而更好地解决特定问题。
弹性伸缩:
微服务架构可以根据需求对每个微服务进行独立的水平扩展,提高系统的弹性和可伸缩性。
在单体架构中,只能对整个应用程序进行水平扩展,无法针对特定功能进行精确调整。
复杂性:
微服务架构引入了服务间通信、服务发现、负载均衡等新的挑战,增加了系统的复杂性和运维管理成本。
单体架构相对来说更加简单直观,适用于小型项目或固定功能的应用。

2、Spring Cloud 是什么?它解决了哪些问题?

Spring Cloud是基于Spring Boot的微服务架构开发工具,它提供了一系列开发工具和库,用于快速构建分布式系统中的微服务架构。Spring Cloud为开发人员提供了一套简单易用的组件,帮助他们解决微服务架构中常见的问题。

服务注册与发现:
Spring Cloud集成了Netflix Eureka、Consul等服务注册中心,可以帮助微服务在动态环境下实现自动化的服务注册与发现,使得服务之间可以更加方便地通信。
负载均衡:
通过集成Netflix Ribbon等负载均衡组件,Spring Cloud可以实现对服务请求的负载均衡,提高系统的性能和可靠性。
断路器:
通过集成Netflix Hystrix等断路器组件,Spring Cloud可以实现服务之间的故障隔离和容错处理,防止因为某个服务故障导致整个系统的瘫痪。
网关:
Spring Cloud提供了Netflix Zuul等API网关组件,可以帮助开发人员统一管理和路由微服务的请求,提高系统的安全性和稳定性。
服务间调用:
Spring Cloud提供了Feign、OpenFeign实现服务间的通信和调用,简化了开发过程,提高了系统的可维护性和可扩展性。
配置管理:
Spring Cloud Config可以帮助开发人员集中管理应用程序的配置信息,实现配置的集中化管理和动态刷新,避免了修改代码重新部署的麻烦。
分布式跟踪:
通过集成Zipkin等分布式跟踪组件,Spring Cloud可以帮助开发人员跟踪和监控微服务之间的调用链路,帮助排查和解决分布式系统中的问题。

3、SOA架构和微服务架构的区别

规模和粒度:
SOA强调的是面向服务的架构,服务可以是较为庞大的、粗粒度的服务,通常是基于企业的业务功能划分而来的。
微服务则更强调将应用系统划分为多个小型、细粒度的服务,每个微服务关注一个特定的业务领域,并且可以独立部署和扩展。
通信方式:
在SOA中,服务之间的通信通常采用SOAP(Simple Object Access Protocol)或者基于消息队列的方式进行。
微服务架构更倾向于采用基于HTTP的轻量级通信机制,比如RESTful API。
数据一致性:
在SOA中,由于服务粒度较大,可能会存在多个服务共享同一数据库,因此需要注意数据一致性的问题。
微服务架构鼓励每个微服务拥有自己的数据库,从而避免了数据一致性的问题。
部署和管理:
SOA服务通常被部署在统一的应用服务器上,并由统一的管理框架进行管理。
微服务则更加强调独立部署和自治性,每个微服务都可以独立部署并运行,具有更高的灵活性和可替换性。

4、什么是CAP理论

CAP 理论是分布式系统领域的一个重要理论概念。CAP 理论指出,一个分布式系统不可能同时满足以下三个特性:一致性(Consistency)、可用性(Availability)和分区容忍性(Partition Tolerance)。

一致性(Consistency):所有节点在同一时间点看到的数据是相同的,即在进行写操作后,所有节点的数据应该保持一致。
可用性(Availability):系统能够保证每个请求都能够得到响应,即系统对外提供正常的服务并能够处理客户端的请求。
分区容忍性(Partition Tolerance):系统能够在网络发生分区的情况下仍然能够继续工作,即系统能够处理因网络故障而导致的节点之间通信失败的情况。

分布式系统在面临网络分区时,必须在一致性(Consistency)和可用性(Availability)之间做出权衡选择,即要么保证数据的一致性但牺牲一定的可用性,要么保证可用性但可能出现数据的不一致性。分区容忍性(Partition Tolerance)是必须要满足的基本条件,因为网络分区在分布式系统中是常见且不可避免的。

在实际设计分布式系统时,根据具体的业务需求和系统特点,需要权衡考虑如何在一致性、可用性和分区容忍性之间进行取舍,以满足系统的实际需求。 CAP 理论帮助开发人员更好地理解分布式系统设计中的抉择,并指导其进行合理的系统设计和架构选择。

Eureka

1、什么是Eureka?它的作用是什么?

Eureka是Netflix开源的一种服务注册与发现组件,用于在基于微服务架构的应用中实现服务的注册、发现和故障转移。它允许微服务在动态环境下注册自身,并能够发现其他服务的位置。
Eureka的核心组件包括
Eureka Server:
Eureka Server是服务注册中心的组件,负责管理服务的注册和发现。当一个微服务启动时,它会向Eureka Server注册自己的信息(比如服务名、IP地址、端口号等),并周期性地向Eureka Server发送心跳以表明自己的健康状态。同时,其他微服务可以通过Eureka Server查询到注册的服务信息,从而实现服务之间的通信。
Eureka Client:
Eureka Client是服务提供者和消费者的组件,用于注册自身并发现其他服务。当一个微服务作为客户端运行时,它会向Eureka Server注册自己,并且从Eureka Server获取其他服务的信息来进行调用。同时,Eureka Client会定期从Eureka Server获取注册表信息,并缓存在本地,以便在Eureka Server不可用时仍然能够提供服务。

2、Eureka的工作原理是什么?

服务注册:
当一个微服务启动时,它会向Eureka Server发送注册请求,将自身的信息(比如服务名、IP地址、端口号等)注册到Eureka Server上。
心跳与健康检查:
注册完成后,微服务会定期向Eureka Server发送心跳,以表明自己的健康状态。如果Eureka Server在一定时间内没有收到某个微服务的心跳,就会将该服务实例从注册表中移除,认为该服务不再可用。
服务发现:
其他微服务可以通过Eureka Server查询到已注册的服务信息,包括服务名、IP地址、端口号等。这样,微服务之间就可以相互发现并调用对方提供的服务。
负载均衡:
Eureka Server可以根据注册表中的服务实例信息,实现负载均衡功能,将请求分发到多个具体的服务实例上。
高可用性:
Eureka Server支持构建多节点的集群,在多个Eureka Server节点之间相互注册以实现高可用性,即使其中一个节点出现故障,其他节点仍然可以提供服务注册与发现的功能。

3、Eureka和ZooKeeper的区别是什么?

设计目的:
Eureka 是专门为基于微服务架构设计的服务注册与发现组件,着重于服务的动态注册、发现和负载均衡。
ZooKeeper 是一个分布式协调服务,提供的功能包括服务注册、协调、配置管理等多方面,适用于更广泛的分布式系统场景。
一致性协议:
Eureka 使用 eventual consistency(最终一致性)的策略,即服务注册信息可能存在一定时间的不一致,但最终会达到一致状态。
ZooKeeper 使用 ZAB(ZooKeeper Atomic Broadcast)协议,保证了强一致性,即数据变更会被所有节点同步,读操作能够获得最新的数据。
数据存储方式:
Eureka 使用内存存储注册信息,并且默认情况下不支持持久化,适用于相对较小规模的微服务架构。
ZooKeeper 使用磁盘存储数据,支持持久化存储,并且能够存储更复杂的数据结构,适用于更复杂的分布式系统场景。
生态系统支持:
Eureka 是 Netflix 提供的开源项目,与 Netflix 的其他组件(如 Ribbon、Hystrix 等)集成良好。
ZooKeeper 是 Apache 的顶级项目,有较大的社区支持,同时也被许多公司广泛应用于生产环境。

4、Eureka如何保证自身的高可用?

Eureka 内部实现高可用性的关键在于其自身的集群化和故障处理机制。
Eureka Server 集群:
Eureka 通过搭建多个 Eureka Server 实例组成集群的方式来实现高可用性。这些实例之间互为对等,彼此相互注册,共享服务注册信息。
客户端应用可以向任何一个 Eureka Server 实例注册服务或查询服务,这样就可以实现负载均衡和故障转移。
心跳和健康检查:
Eureka Server 会周期性地向集群中的其他节点发送心跳消息,以确保节点之间的存活状态。
同时,Eureka Server 会对注册在其上的服务进行健康检查,及时剔除不健康的服务实例。
自我保护机制:
Eureka Server 在面临网络分区或异常情况下,会进入自我保护模式,不轻易剔除健康的服务实例,以免造成更大的系统压力。
自我保护机制可以防止因网络问题导致整个集群中的实例被错误剔除,确保系统的稳定性。
数据同步和复制:
Eureka Server 可以配置跨区域或多数据中心的数据同步和复制机制,确保不同地域的 Eureka Server 数据一致性,提高整体的可用性。
负载均衡:
Eureka Server 集群可以通过负载均衡器进行流量分发,保证请求能够平衡地分布到各个节点,避免单点故障。

ZooKeeper

1、什么是 ZooKeeper?它的作用是什么?

ZooKeeper 是一个开源的分布式协调服务,提供高性能、高可用、有序的数据存储和访问服务。它主要用于解决分布式系统中的一致性问题,为分布式应用提供可靠的协调机制。
ZooKeeper 的主要作用包括:

  • 统一命名服务:ZooKeeper 提供了一个统一的命名空间,可以在其中注册和管理分布式系统中的各种资源,如配置信息、服务地址等。
  • 配置管理:分布式系统中的各个节点可以通过 ZooKeeper 来集中管理配置信息,实现配置的动态更新和同步。
  • 分布式锁:ZooKeeper 提供了分布式锁的机制,可以帮助实现分布式系统中的互斥访问控制。
  • 分布式协调:通过 ZooKeeper 的 Watcher 机制和临时节点特性,可以实现分布式系统中的协调和通知机制。
  • 集群管理:ZooKeeper 可以用于管理分布式系统的节点状态、健康检查等信息,帮助实现集群管理和监控。
2、ZooKeeper 是什么样的数据结构?它如何维护这些数据?

它的数据模型类似于一个分层的文件系统,但是它实际上是一种基于内存的树形数据结构。ZooKeeper 中的数据被组织成类似目录结构的节点(node),每个节点可以存储少量的数据。

具体来说,ZooKeeper 的数据结构是一颗树,每个节点都可以存储一个小于 1MB 大小的数据。每个节点都有一个路径(path),类似于文件系统中的路径,以 “/” 开头。节点之间的关系是父子关系,类似于文件系统中文件与文件夹的关系。

ZooKeeper 如何维护这些数据呢?ZooKeeper 使用 ZAB(ZooKeeper Atomic Broadcast)协议来保证数据的一致性和可靠性。当客户端向 ZooKeeper 发送写请求时,ZooKeeper 集群会将该请求转发给一个 Leader 节点,Leader 节点负责协调集群中的其他节点进行数据写入操作。只有当多数节点都写入成功后,写操作才会被提交,这样可以确保数据的一致性。

此外,ZooKeeper 还会在内存中维护一个数据快照(snapshot),用于快速恢复数据。ZooKeeper 会定期将内存中的数据快照持久化到磁盘,以防止数据丢失。当 ZooKeeper 重启时,它可以通过加载最近的数据快照来恢复数据。

3、什么是ZAB(ZooKeeper Atomic Broadcast)协议

是 ZooKeeper 中用于实现一致性和可靠性的关键协议。ZAB 协议主要用于保证分布式系统中的数据一致性,并确保在发生故障或节点重启时能够恢复数据的正确性。
主要包含以下几个功能
Leader 选举:ZAB 协议会在 ZooKeeper 集群中选择一个 Leader 节点来负责处理客户端的写请求。Leader 负责接收客户端的写请求,并将这些请求广播给其他节点。在初始启动时,ZooKeeper 集群中没有 Leader,因此需要进行 Leader 选举。
消息广播:一旦选出了 Leader,它可以接收来自客户端的写请求,并将这些请求作为提案(proposal)广播给集群中的其他节点。其他节点会接收并处理这些提案,然后向 Leader 发送确认信息。
多数派提交:ZAB 协议要求大部分节点都要确认某个提案,即超过半数的节点都要确认某个写操作后,Leader 才会将该提案确定为已提交。这样可以确保在网络分区或节点故障的情况下仍能保持数据的一致性。
事务日志:一旦 Leader 确认了一个提案,它会将该提案写入事务日志(transaction log)。这个事务日志会被持久化到磁盘上,以便在节点重启时能够恢复数据。
快照和状态同步:ZAB 协议还包括了快照机制,用于定期将内存中的数据快照持久化到磁盘,以便在节点重启时快速恢复数据。同时,ZAB 协议还包括了状态同步机制,用于在节点重启后与 Leader 进行状态同步,以确保数据的正确性。

4、ZooKeeper集群下Leader的选举过程

ZooKeeper 中 Leader 选举的基本过程:
初始化阶段:在初始启动时,所有的节点都处于初始状态,没有 Leader。此时,每个节点都会尝试成为 Leader。
投票选举:每个节点开始向其他节点发送选举通知,并宣布自己的候选身份。其他节点收到通知后会进行投票,根据一定的规则比较各个候选者的 zxid(事务 ID)来决定是否接受该节点的领导权。
选票比较:如果某个节点的 zxid 最大,并且得到了超过半数节点的赞成票,那么该节点就会成为 Leader。这样就确保了在网络分区或节点故障的情况下,集群可以快速选出新的 Leader,并继续处理客户端的请求。
Leader 宣告:一旦某个节点成为 Leader,它会向其他节点发送消息,宣称自己已成为 Leader,并开始处理客户端的写请求,同时将写请求广播给其他节点。

5、请解释 ZooKeeper 的 CAP 理论。

ZooKeeper 的 CAP 理论指的是分布式系统中的三个基本属性:一致性(Consistency)、可用性(Availability)和分区容忍性(Partition Tolerance)。根据 CAP 理论,一个分布式系统不可能同时满足这三个属性,最多只能同时满足其中两个。
ZooKeeper 在设计上选择保证一致性和分区容忍性,因此在面对网络分区时会牺牲一定的可用性。这种设计使得 ZooKeeper 能够成为一个强一致性的分布式协调服务,适用于多种分布式系统场景。

6、ZooKeeper 是如何处理分布式锁的?请描述一下 ZooKeeper 实现分布式锁的原理。

ZooKeeper 实现分布式锁的基本原理:
创建临时顺序节点:当一个客户端需要获取锁时,它在 ZooKeeper 中创建一个临时顺序节点(ephemeral sequential node),节点的路径包含一个有序的序号。这样可以确保每个客户端都会创建一个唯一的节点,并且节点会按照创建的顺序排列。
获取锁:客户端创建完临时顺序节点后,它会获取当前 ZooKeeper 中所有临时顺序节点的列表,并判断自己创建的节点是否是列表中序号最小的节点。如果是最小节点,则表示该客户端成功获取了锁;否则,客户端会监听自己前一个节点的变化(Watch),并等待前一个节点被删除或释放锁。
释放锁:当客户端不再需要锁时,它会删除自己创建的临时顺序节点。这会触发 ZooKeeper 发送通知给等待的客户端,让它们重新检查是否可以获取锁。

7、ZooKeeper 的 Watcher 机制是什么?它有什么作用?

ZooKeeper 的 Watcher 机制是一种事件通知机制,允许客户端在数据变化或状态变化时得到通知。客户端可以注册 Watcher 监听指定的节点,当节点被创建、删除或数据被更新时,ZooKeeper 会向客户端发送通知,客户端收到通知后可以做出相应的处理。

Watcher 机制的作用包括:
实时通知:客户端可以通过 Watcher 机制实时获取数据变化的通知,而不需要轮询检查节点的状态。
简化开发:Watcher 可以简化分布式系统中节点间的协调和同步,避免手动实现复杂的通知机制。
减少网络开销:相比频繁的轮询检查,Watcher 可以减少网络开销,提高系统性能。
支持分布式协同:Watcher 可以帮助客户端实现分布式锁、选举等机制,保证分布式系统的一致性和可靠性。

8、请解释 ZooKeeper 的节点类型和数据节点的特点。

在 ZooKeeper 中,主要有以下几种节点类型和数据节点的特点:
持久节点(Persistent Nodes):
持久节点在创建后会一直存在,直到显式删除。
当客户端与 ZooKeeper 断开连接后,持久节点不会被自动删除。
临时节点(Ephemeral Nodes):
临时节点在创建它的客户端会话失效或断开连接时被自动删除。
临时节点通常用于标识临时性状态或连接,例如临时性的服务注册信息。
顺序节点(Sequential Nodes):
顺序节点在节点路径后会附加一个递增的序号,确保节点在创建时有序排列。
顺序节点的序号是 ZooKeeper 服务器自动分配的,客户端无法指定。
临时顺序节点(Ephemeral Sequential Nodes):
临时顺序节点结合了临时节点和顺序节点的特点,即在客户端会话失效时会被自动删除,并且节点路径后会附加递增的序号。

数据节点的特点包括:
轻量级数据存储:ZooKeeper 用于存储小量的关键元数据和配置信息,每个数据节点的大小通常受到限制,适合存储少量的状态信息。
高可用性:ZooKeeper 数据存储是基于内存的,并通过多副本复制实现高可用性,可以提供可靠的数据访问和一致性保证。
数据监听机制:客户端可以通过 Watcher 机制监听数据节点的变化,一旦数据节点发生变化,客户端会收到通知,实现实时的数据同步和通知。
层级命名空间:ZooKeeper 的数据节点是以层级命名空间的方式组织的,类似于文件系统的目录结构,方便管理和查找数据节点。

9、ZooKeeper 保证了哪些 ACID 特性?

原子性(Atomicity)
ZooKeeper使用事务(Transaction)机制来确保操作的原子性,在一个事务中,一组操作要么全部成功,要么全部失败,不会存在部分成功的情况。如果在一个事务中的某个操作失败,整个事务将被回滚,以保持数据的一致性。
例如,创建节点、更新节点数据等操作都是原子性的,要么操作成功,要么失败,不会出现中间状态。
一致性(Consistency)
ZooKeeper 提供了强一致性,即在任意时刻,所有客户端看到的数据视图都是一致的。
当一个客户端对数据进行修改后,其他客户端会立即看到最新的数据状态,确保了数据的一致性。
隔离性(Isolation)
ZooKeeper 使用递增的版本号(zxid)和序列号等机制来实现客户端之间的操作隔离,即多个客户端并发操作时,它们彼此之间互不影响。
每个数据节点都有一个版本号,客户端在更新数据时需要指定该节点的当前版本号,如果版本号不匹配,则更新操作会失败,避免了并发操作的冲突。
持久性(Durability)
ZooKeeper 确保数据的持久性,即一旦数据被写入成功,数据将被持久化存储,不会因服务器故障或重启而丢失。
数据节点的持久性保证了系统的可靠性和数据的稳定性。

10、ZooKeeper 如何处理节点的故障恢复?

ZooKeeper 使用多副本复制(Replication)机制来处理节点的故障恢复,确保系统的可用性和数据的持久性。具体来说,ZooKeeper 采取以下措施来处理节点的故障恢复:

Leader 选举:
在 ZooKeeper 集群中,通过选举机制选出一个节点作为 Leader,负责处理客户端的请求和协调集群中的其他节点。
如果当前的 Leader 节点发生故障,ZooKeeper 会自动进行 Leader 选举,选出新的 Leader 节点来接管工作,确保系统的连续性。
多副本复制:
ZooKeeper 将数据存储在多个节点上,并使用多副本复制机制来保证数据的可靠性和持久性。通常情况下,每个数据节点都会有多个副本分布在不同的服务器上。
当某个节点发生故障时,ZooKeeper 会从其他副本中选择一个新的 Leader,并通过数据同步机制将数据恢复到故障节点上,确保数据的完整性和可用性。
数据恢复:
当一个节点因为故障而下线时,ZooKeeper 会自动触发数据恢复机制,尝试将该节点上的数据同步到其他正常运行的节点上,以确保数据的一致性和完整性。
一旦节点恢复正常,ZooKeeper 会将其重新加入到集群中,并进行数据同步,使其恢复到正常的工作状态。

11、什么是 ZooKeeper 的会话超时?它有什么作用?

会话超时是指客户端与 ZooKeeper 服务器之间的会话在一定时间内没有收到心跳信号而被认定为超时失效
会话超时具有以下作用:
心跳检测:
客户端与 ZooKeeper 服务器之间会周期性地进行心跳通信,以维持会话的活跃状态。
如果服务器在一定时间内没有收到客户端的心跳信号,就会认为客户端可能出现了故障或断连,将其会话标记为超时失效。
故障检测和恢复:
当客户端的会话超时被触发后,ZooKeeper 服务器会将该客户端标记为已断开连接,进而触发相应的故障检测和恢复机制。
断开连接的客户端可能是因为网络故障、客户端崩溃等原因,ZooKeeper 会通过重新选举 Leader 和数据同步等操作来处理这些故障情况,确保系统的可用性。
保持数据的一致性:
会话超时对于保持数据的一致性非常重要。当一个客户端的会话超时失效后,ZooKeeper 会将其之前创建的临时节点和所持有的锁等资源删除或释放,避免了因为客户端故障导致资源未被正确清理的问题。
这样可以确保分布式系统中的数据和资源状态始终保持一致,避免悬挂的临时节点和死锁等问题。

Consul

1、什么是 Consul?它的作用是什么?

Consul 是一种开源的服务网格和服务发现工具。它提供了一种简单、高效的方式来实现服务发现、健康检查、动态配置和分布式一致性等功能,帮助构建可靠的分布式系统。

Consul 的主要功能包括:
服务发现:Consul 允许服务注册和发现,使得各个微服务能够动态地找到彼此,从而实现服务之间的通信和协作。
健康检查:Consul 提供健康检查机制,定期检查服务的健康状态,并及时发现并处理不健康的服务实例,确保系统的稳定性。
KV 存储:Consul 提供 Key-Value 存储功能,可以用来存储配置信息、控制参数等,实现动态配置管理。
分布式锁:Consul 支持分布式锁的实现,用于协调分布式系统中的并发访问控制,避免竞争条件和数据冲突。
多数据中心支持:Consul 能够跨多个数据中心进行服务发现和通信,支持构建全球性的分布式系统。
Consul Connect:Consul 提供了 Consul Connect 功能,用于安全地管理服务之间的通信流量,实现服务之间的加密、认证和授权。

2、请简要介绍一下 Consul 的架构和核心组件。

Consul Server(Consul 服务器):
Consul Server 用于存储集群的状态信息、执行一致性协议,并处理来自客户端的查询请求。
Consul 集群通常包含多个 Consul Server,通过 Raft 协议实现一致性,选举出 Leader 节点负责处理写操作。
Consul Client(Consul 客户端):
Consul 客户端是与 Consul Server 进行通信的代理,用于注册服务、进行健康检查、查询服务信息等操作。
客户端会定期同步服务信息和健康状态,并将查询请求转发给 Consul Server 处理。
Service Catalog(服务目录):
服务目录用于存储已注册的服务信息,包括服务名称、IP 地址、端口号等。
通过服务目录,Consul 客户端可以轻松地发现和访问其他服务。
Health Checking(健康检查):
Consul 提供了健康检查功能,用于定期检查服务的健康状态,包括 HTTP、TCP、TLS 等各种类型的健康检查方式。
健康检查结果会影响服务的可用性,不健康的服务实例会被标记并从服务发现中移除。
Key-Value Store(键值存储):
Consul 提供了 Key-Value 存储功能,用于存储配置信息、控制参数等。
应用程序可以通过 Key-Value 存储动态获取配置,实现动态配置管理。
Consul Connect:
Consul Connect 是 Consul 的一部分,用于安全地管理服务之间的通信流量,提供加密、认证和授权功能。
通过 Consul Connect,服务之间的通信可以更加安全可靠。

3、什么是Raft 协议?

Raft 协议是一种分布式一致性协议,用于在分布式系统中实现状态机复制。Raft 协议通过领导者(Leader)选举、日志复制和安全性机制来确保分布式系统中各个节点之间的一致性。下面是 Raft 协议的一些核心概念:
Leader 选举:Raft 将节点划分为 Leader、Follower 和 Candidate 三种角色。在初始状态下,所有节点都是 Follower。如果 Follower 超时没有收到 Leader 的心跳,则会转变为 Candidate,并开始一轮选举过程。Candidate 会向其他节点发送投票请求,获得多数票的节点将成为新的 Leader。
日志复制:Leader 负责接收客户端请求并将操作追加到日志中,然后将日志条目发送给 Followers 进行复制。一旦大多数节点确认已复制该日志条目,Leader 就可以提交该操作并告知 Followers 执行。
安全性:Raft 通过定期选举、日志复制和持久化等机制来确保系统的安全性和一致性。每个节点都会定期向其他节点发送心跳消息以维持连接,确保系统正常运行。

4、Consul 是如何处理服务发现和健康检查的?

服务注册:
当一个服务启动时,它会向 Consul 注册自己的服务实例信息,包括服务名称、IP 地址、端口号等。服务注册可以由 Consul 客户端或者服务网格代理执行。
注册后的服务信息会被存储在 Consul 的服务目录中,用于服务发现和健康检查。
服务发现:
其他服务想要调用某个服务时,它们可以向 Consul 发送查询请求,请求特定服务名称的服务实例信息。
Consul 会返回与请求匹配的服务实例信息,包括 IP 地址和端口号,使得客户端能够直接访问所需的服务。
健康检查:
Consul 客户端会定期执行健康检查,以确保注册的服务实例的健康状态。健康检查可以包括 HTTP、TCP、TLS 等多种方式,用于检测服务是否可用。
如果服务实例被标记为不健康,Consul 将不再返回该实例的信息,避免客户端访问到不可用的服务。
故障转移:
如果一个服务实例变得不可用,Consul 会自动将其从服务目录中移除,从而避免客户端继续向其发送请求。
同时,Consul 还支持在服务实例故障时自动触发故障转移,将流量转移到其他健康的服务实例上,确保系统的高可用性。

5、Consul 支持哪些服务发现的方式?

DNS 服务发现:
Consul 提供了 DNS 接口,通过域名解析的方式来进行服务发现。客户端可以直接使用域名来访问服务,而无需显式地与 Consul API 进行交互。
通过 DNS 服务发现,可以使得现有的应用程序更容易地迁移到基于 Consul 的服务发现机制上。
HTTP API
Consul 提供了丰富的 HTTP API,允许开发者使用 RESTful 接口来查询和管理服务实例信息。通过调用这些 API,可以实现自动化的服务发现和管理。
RPC 接口:
Consul 还提供了支持多种编程语言的 RPC 接口,如 gRPC、Thrift、Protocol Buffers 等。这些接口可以与现有的 RPC 框架结合,使得服务发现过程更加方便和透明。
Consul Template:
Consul Template 是 Consul 的一个附加工具,它可以监视 Consul 中的服务信息变化,并根据模板生成对应的配置文件。这样可以实现动态更新应用程序的配置信息,从而无缝地适应服务实例的变化。

6、介绍一下 Consul 中的 Key-Value 存储功能以及用途。

Consul 中的 Key-Value 存储功能是指 Consul 提供的一种轻量级的分布式键值存储服务,允许用户在集群中存储和检索简单的键值对数据。Key-Value 存储在 Consul 中被广泛用于配置管理、动态参数设置、特性开关等场景,为分布式系统提供了便捷的配置管理工具。Consul提供了RESTful的HTTP API接口,你可以使用类似cURL、Postman这样的工具或者编程语言的HTTP客户端来进行访问。
配置管理:
开发人员可以将应用程序的配置信息存储在 Consul 的 Key-Value 存储中,如数据库连接信息、日志级别、调试标志等。这些配置可被动态更新,无需重启应用程序即可生效。
动态参数设置:
Key-Value 存储可以用于存储系统的参数设置,如超时时间、重试次数等。通过 Consul API 或者 Consul Template,可以实现参数的动态调整和更新。
特性开关:
通过在 Key-Value 存储中设置特性开关的值,可以实现系统中特定功能的开启或关闭。这种方式可以帮助开发人员进行 A/B 测试、灰度发布等功能控制。
服务注册元数据:
除了配置信息,Key-Value 存储还可以存储与服务注册相关的元数据,如服务版本、环境信息等。这些元数据可以帮助实现更灵活的服务发现和路由策略。
监控和告警配置:
将监控和告警配置信息存储在 Consul 的 Key-Value 存储中,可以实现监控系统的配置自动化管理,方便对监控报警规则进行调整。

7、Consul 是如何实现分布式锁的?

Consul实现分布式锁的机制通常使用基于Key-Value存储和Session的方式来实现。以下是Consul如何实现分布式锁的一般步骤:
创建锁:
通过向Consul的Key-Value存储中写入一个特定的键值对来创建一个锁。这个键通常表示一个资源或者一个任务,而值可以是任意值。
尝试获取锁:
当一个客户端(服务)想要获取这个锁时,它会尝试在Consul中创建一个与该键关联的Session。这个Session由Consul管理,可以设置过期时间。
竞争获取锁:
多个客户端同时尝试获取锁时,只有一个客户端的Session能够成功创建,并且这个客户端获得了锁。其它客户端的Session创建可能会失败,它们将不拥有锁。
持有锁:
客户端成功获取锁后,在Consul中的Session将保持活动状态,确保锁的有效性。只有持有锁的客户端才能对资源进行操作。
释放锁:
当客户端完成任务或者释放资源时,它会主动释放锁,即删除相应的键值对或者Session。这样其他客户端就有机会尝试获取锁了。

8、Consul 中的 Consul Connect 是用来做什么的?它有什么优势?

Consul Connect 是 Consul 提供的一种服务网格解决方案,用于管理和安全地连接服务之间的通信。它有以下主要功能和优势:

安全的服务通信:
Consul Connect 提供了基于TLS的加密通信机制,确保服务之间的通信是安全可靠的。它通过自动化证书颁发和轮换,简化了证书管理的复杂性,同时可以有效地防止窃听和篡改。
服务授权:
Consul Connect 支持细粒度的服务授权,可以根据服务的身份和属性对通信进行灵活的访问控制。这样可以确保只有经过授权的服务才能相互通信,提高了系统的安全性。
流量控制:
通过 Consul Connect,你可以轻松地实现流量控制和负载均衡,以确保服务之间的通信是稳定和可靠的。它支持多种代理模式,可以根据需要进行灵活配置。
动态服务发现:
Consul Connect 集成了 Consul 的服务发现功能,可以自动发现并连接服务,并提供了统一的服务注册表和命名解析。这样可以降低服务之间的耦合度,提高了系统的可扩展性和灵活性。
易于部署和管理:
Consul Connect 提供了丰富的命令行工具和 API,可以方便地部署和管理服务网格。同时,它与 Consul 的其他功能如健康检查、故障恢复等紧密集成,使得整个系统更加稳定和可靠。

9、如何在 Consul 中实现跨数据中心的服务发现和通信?

要在 Consul 中实现跨数据中心的服务发现和通信,可以使用 Consul 的 WAN 功能(Wide Area Network)。
启用 WAN 功能:

首先,在 Consul 的配置文件中启用 WAN 功能。确保每个数据中心的 Consul agent 都配置了正确的 WAN 地址,并且能够相互通信。
确保不同数据中心的 Consul 集群能够互相通信。这通常涉及到配置网络规则和防火墙规则,以允许数据中心之间的通信。
在数据中心之间配置服务同义词(Service Aliases),使得不同数据中心的服务可以通过相同的名称进行访问。这样可以简化跨数据中心服务发现的配置和管理。
为了确保跨数据中心通信的安全性,可以配置 Consul 的 WAN 加密功能,使用 TLS 加密数据传输。这样可以有效地保护数据中心之间的通信安全。
在各个数据中心的服务中注册服务信息,并使用 Consul 的服务发现功能来发现跨数据中心的服务。通过 Consul 的 DNS 接口或 HTTP API,可以轻松地在不同数据中心中查找和访问服务。
确保在跨数据中心通信时配置了适当的健康检查机制,以及故障恢复策略。这样可以及时发现并处理服务不可用的情况,保证系统的稳定性和可靠性。

Nacos

1、什么是 Nacos?它的主要功能是什么?

服务发现与注册:
Nacos 提供了服务注册和发现功能,服务提供者可以向 Nacos 注册自己的服务,并且消费者可以通过 Nacos 发现并调用这些服务,实现服务之间的动态发现和通信。
动态配置管理:
Nacos 可以集中管理应用程序的配置信息,支持动态更新配置,应用程序可以实时获取最新的配置信息,无需重启服务即可生效。
服务健康检查:
Nacos 提供了健康检查机制,可以定期检查服务的健康状态,并及时发现故障节点,确保服务的稳定性和可靠性。
动态 DNS:
Nacos 支持动态 DNS 解析,可以根据服务名称动态解析到具体的服务实例地址,简化服务调用过程。
流量管理:
Nacos 提供了流量管理功能,可以实现流量的动态调度、限流和熔断,保护系统免受突发流量影响。
多环境支持:
Nacos 支持多环境的管理,可以根据不同的环境(如开发、测试、生产)管理不同的配置信息,便于应用程序在不同环境中部署和运行。

2、Nacos 支持哪些注册中心和配置中心的功能?

Nacos 注册中心具有以下主要功能:
服务注册:
服务提供方可以通过 Nacos 注册中心将自己提供的服务注册到注册中心,包括服务名称、IP地址、端口号等信息。
服务发现:
服务消费方可以通过 Nacos 注册中心查询注册的服务列表,以便发现可用的服务实例,从而实现服务之间的通信与调用。
健康检查:
Nacos 注册中心会定期检查注册的服务实例的健康状态,如果某个服务实例出现故障或不可用,注册中心会及时发现并标记为不健康,确保服务的可靠性。
负载均衡:
Nacos 注册中心可以根据服务实例的健康状态和负载情况进行负载均衡,确保请求被均匀地分发到各个健康的服务实例上。
集群管理:
Nacos 注册中心支持集群部署,可以横向扩展以应对高并发和大规模的服务注册与发现需求,提高系统的可伸缩性和稳定性。
动态配置:
Nacos 注册中心还提供了动态配置管理功能,使得服务的配置信息可以动态更新,无需重启服务即可生效,方便应用程序灵活调整配置。

Nacos 配置中心具有以下主要功能:
动态配置管理:
Nacos配置中心允许用户集中管理应用程序的配置信息,包括数据库连接信息、日志级别、缓存配置等,这些配置信息可以在配置中心中进行统一管理。
配置分组:
Nacos配置中心支持将配置信息进行分组管理,可以按照不同的环境(如开发、测试、生产)或不同的应用程序进行分组,方便进行管理和区分。
动态更新配置:
配置中心允许用户实时更新配置信息,应用程序可以动态获取最新的配置信息,无需重启服务即可生效,提高了配置的灵活性和响应速度。
配置监听:
Nacos配置中心支持配置监听功能,应用程序可以注册配置监听器,一旦配置信息发生变化,配置中心会通知应用程序,从而实现配置的动态加载。
版本管理:
配置中心支持配置信息的版本管理,用户可以查看历史配置信息,并进行回滚操作,确保配置信息的安全性和可追溯性。
权限控制:
Nacos配置中心提供了权限控制机制,可以对不同用户或角色设置不同的权限,保障配置信息的安全性和隐私性。

3、请解释 Nacos 中的命名空间(Namespace)是什么,有什么作用?

在 Nacos 中,命名空间(Namespace)是用来隔离和管理配置信息的逻辑空间。每个命名空间都可以包含一组配置信息,并且可以单独进行管理和控制。命名空间为用户提供了一个独立的配置环境,可以在不同的命名空间中管理不同的配置信息,避免配置信息混乱和冲突。
配置隔离:
不同的应用程序或不同的环境可以使用不同的命名空间,通过命名空间的隔离,确保配置信息之间相互独立,避免配置信息的混乱和冲突。
权限控制:
命名空间可以设置不同的权限控制,可以对不同用户或角色进行授权,限制其对配置信息的访问和修改权限,保障配置信息的安全性。
版本管理:
每个命名空间都可以独立管理配置信息的版本,用户可以查看历史配置信息,并进行回滚操作,确保配置信息的可追溯性和安全性。
环境切换:
使用命名空间可以方便地进行不同环境(如开发、测试、生产)之间的配置切换,使得应用程序在不同环境中能够快速部署并加载相应的配置信息。

4、Nacos 中的服务发现是如何工作的?它有什么优势?

服务注册:

  • 服务提供者在启动时,向 Nacos 注册中心发送注册请求,包括服务名、IP 地址、端口等服务信息。
  • 注册中心将服务信息存储在自身的数据存储中,以便后续的服务发现和管理。
  • 注册中心定期与服务提供者进行心跳检测,确保服务实例的健康状态。
    服务发现:
  • 服务消费者在需要调用某个服务时,向 Nacos 注册中心发送发现请求,获取该服务的详细信息。
  • 注册中心根据服务名等信息查询对应的服务实例列表,并返回给服务消费者。
  • 服务消费者根据负载均衡策略选择合适的服务实例进行调用。

Nacos 的注册中心实现服务注册和发现的关键在于其内部的数据存储机制和通信协议。注册中心通过高效的数据存储和管理,实现了快速的服务注册和发现功能;同时通过与服务实例的心跳检测,保证了注册中心与服务实例之间的实时性和可靠性。

5、Nacos 如何高效管理数据存储,如何通保证实例的健康?

Nacos 使用分布式存储技术来存储服务注册信息、配置信息等数据。这使得 Nacos 具有良好的横向扩展性和高可用性,能够应对大规模数据存储和高并发访问的需求。同时Nacos 基于一致性协议(例如 Paxos 或 Raft)来保证数据的一致性和可靠性。这确保了注册中心存储的数据在各个节点之间的同步和一致,避免了数据不一致性和丢失的问题。
Nacos 会定期向服务实例发送心跳检测请求,以确保服务实例的健康状态。如果某个服务实例长时间未响应心跳检测,注册中心会将其标记为不可用,从而避免服务调用者调用到不健康的实例。同时Nacos 可以通过故障转移和负载均衡等机制,确保服务实例的健康状态和负载均衡。当某个实例出现故障时,注册中心会自动将请求转发给其他健康的实例,保证了系统的稳定性和可靠性。

6、Nacos 如何实现服务的动态配置管理?

当服务启动时,服务会向 Nacos 注册中心订阅所需的配置信息,Nacos 注册中心会将对应的配置信息发送给服务。同时,服务可以注册监听器,实时监测配置的变化情况。这样,一旦配置发生变化,Nacos 注册中心就会通知相应的服务,并更新配置信息,而服务无需重启即可应用新的配置。
Nacos 提供了 Web 控制台和 API 接口,用户可以通过这些界面来管理配置信息。用户可以在控制台上添加、修改、删除配置,并设置配置的数据类型、格式等,也可以通过 API 接口进行批量操作和自动化管理。
Nacos 还支持配置的分组和命名空间管理,用户可以对配置进行分类和归档,方便管理和查找。同时,Nacos 提供了配置的版本管理功能,用户可以查看历史配置记录,进行回滚操作,确保配置的可追溯性和稳定性。

7、Nacos 支持哪些数据持久化存储方式?它们之间有什么区别?

nacos支持以下几种方式的存储
Nacos 支持将数据持久化到文件系统中,这种方式简单直接,适合小规模部署和测试环境使用。
Nacos 支持将数据持久化到关系型数据库(如 MySQL、Oracle 等)中是一种常见且成熟的持久化方式。
Nacos 也支持将数据持久化到 NoSQL 数据库中,比如使用 Redis、Elasticsearch 等作为后端存储。

这些不同的数据持久化存储方式之间的区别主要体现在以下几个方面:
性能:NoSQL 数据库通常具有更高的性能和横向扩展能力,适合处理大规模数据和高并发访问;而关系型数据库在事务处理和一致性方面表现更为出色。
一致性:关系型数据库通常提供强一致性的特性,而 NoSQL 数据库在某些情况下可能会提供最终一致性或者柔性事务的特性。
可靠性:关系型数据库通常具有成熟的备份恢复机制,能够保证数据的可靠性和持久化;NoSQL 数据库的可靠性取决于具体的实现和配置方式。
横向扩展:NoSQL 数据库通常更容易实现横向扩展,可以方便地应对大规模数据和高并发访问的需求;关系型数据库在横向扩展方面相对较为复杂。

8、Nacos 的配置推送原理是什么?如何实现配置的即时更新?

Nacos 的配置推送原理主要基于长连接和监听器机制。当客户端向 Nacos 注册配置监听器后,Nacos 会在配置发生变化时通知客户端,从而实现配置的即时更新。
Nacos 的配置推送原理包括以下几个关键步骤:
注册配置监听器:
客户端通过 Nacos 提供的 API 向 Nacos 注册配置监听器,指定需要监听的配置信息的 Group、DataId 和 Namespace 等参数。客户端可以通过 SDK 或者 Nacos 的 REST API 来进行注册。
长连接:
Nacos 客户端与 Nacos 服务器之间建立长连接,以便及时接收配置变更的通知。这种长连接通常基于 WebSocket 或者长轮询等技术实现,保持客户端和服务器的实时通信。
配置变更通知:
当指定的配置信息发生变化时,Nacos 服务器会向注册了监听器的客户端发送配置变更通知。这可以是针对单个客户端的通知,也可以是广播给所有订阅了相同配置的客户端。
即时更新:
客户端收到配置变更通知后,立即执行相应的更新逻辑,将最新的配置应用到应用程序中。这样就实现了配置的即时更新,无需重启应用程序或者手动刷新配置。

Ribbon

Hystrix

Sentinel

Zull

Gateway

Seata

OpenFeign

Dubbo

1、Dubbo的核心架构模块有哪些?请简要描述每个模块的作用。
  • 服务提供者(Provider):服务提供者负责将具体的服务实现发布为Dubbo服务,以供消费者调用。它将自己的服务注册到注册中心,并对外提供接口。

  • 服务消费者(Consumer):服务消费者通过Dubbo框架调用远程的服务提供者,获取需要的业务功能。它通过注册中心获取可用的服务提供者列表,并通过负载均衡策略选择一个合适的服务提供者进行调用。

  • 注册中心(Registry):注册中心作为服务的管理和协调中心,负责服务的注册与发现。服务提供者在启动时将自己的服务信息注册到注册中心,服务消费者通过注册中心获取可用的服务列表。Dubbo支持多种注册中心,如ZooKeeper、Consul等,dubbo从2.7.0版本后支持nacos做注册中心。

  • 监控中心(Monitor):监控中心用于监控系统的运行状态和性能指标,提供可视化的监控界面。它收集服务提供者和消费者的调用统计数据,并进行展示和报警。

2、介绍一下Dubbo的工作原理及通信协议

Dubbo基本架构
当Dubbo应用启动时,服务提供者会将自己的服务信息注册到注册中心,并监听指定的端口等待消费者的调用。服务提供者需要配置服务接口的实现类、协议、端口号等参数。服务消费者在调用服务之前,首先需要从注册中心获取可用的服务提供者列表。消费者可以通过配置或注解指定要引用的服务接口、注册中心地址、负载均衡策略等参数。Dubbo内部会维护一套服务元数据,包括服务接口、版本、协议、负载均衡策略等信息。Dubbo会根据这些元数据生成相应的代理类,用于在消费者端与服务提供者进行通信。Dubbo的远程通信主要依赖于通信协议。Dubbo默认使用Dubbo协议,该协议基于Netty框架实现,使用NIO方式进行网络通信。Dubbo协议采用自定义的二进制编码和解码方式,以提高性能和减少网络传输的数据量。

3、如何在Dubbo中实现负载均衡?能否举例说明不同的负载均衡策略

在Dubbo中,可以通过在服务引用的配置中指定loadbalance属性来选择不同的负载均衡策略
例如:

<dubbo:reference id="userService" interface="com.example.UserService" loadbalance="random"/>
  • 1

或者

dubbo:
  provider:
    loadbalance: random
  consumer:
    loadbalance: random
  • 1
  • 2
  • 3
  • 4
  • 5

dubbo支持以下几种策略

  • random - 随机从可用的服务提供者列表中选择一个进行调用,每个提供者被选择的概率是相等的。适用于无状态服务的负载均衡,如查询类服务
  • roundrobin - 按照顺序依次调用可用的服务提供者,循环往复。适用于服务提供者性能相近的场景
  • leastactive - 选择活跃调用数最少的服务提供者进行调用,以达到最优的负载均衡效果。适用于存在长时间耗时任务的场景
  • consistenthash - (2.1.0以上版本)根据请求的key通过一致性哈希算法选择服务提供者,可以保证同一个请求始终落到同一个提供者上,适用于缓存类应用或需要保证一定的稳定性的场景。
  • shortestresponse - (2.7.7以上版本)基于最短响应时间的负载均衡策略。该策略会记录每个服务提供者的平均响应时间,然后选择平均响应时间最短的服务提供者来处理当前的请求。这样可以确保将请求发送给响应时间最短的服务提供者,从而提高整体系统的性能和响应速度。
4、什么是Dubbo的服务治理?包括哪些方面

Dubbo的服务治理是指通过一系列的机制和工具来管理和控制Dubbo服务架构中的各种组件,以确保服务的可靠性、稳定性、性能和可伸缩性。Dubbo的服务治理涵盖了多个方面,主要包括以下几个方面:

  • 服务注册与发现:Dubbo通过服务注册中心实现服务的注册和发现,服务提供者将自己的服务注册到注册中心,服务消费者从注册中心获取可用的服务提供者列表。这样可以动态管理服务的上下线、扩容和缩容。

  • 负载均衡:Dubbo提供了多种负载均衡策略,如随机、轮询、最少活跃调用等,用于在多个服务提供者之间分配请求负载,以提高系统的性能和稳定性。

  • 服务容错:Dubbo支持多种容错机制,如失败自动切换、快速失败、失败重试等,用于处理服务调用中可能出现的异常情况,保障服务的可靠性。

  • 并发控制:Dubbo提供了并发控制机制,可以限制服务提供者的并发访问量,防止系统被过多的请求压垮。

  • 隔离机制:Dubbo支持服务隔离,可以将不同的服务按照不同的隔离策略进行隔离,避免由于某个服务出现问题而影响整个系统的稳定性。

  • 限流与熔断:Dubbo支持限流和熔断功能,可以根据系统负载情况对服务进行限流,同时在服务异常情况下进行熔断,保护系统免受雪崩效应的影响。

  • 监控与报警:Dubbo提供了监控和报警机制,可以实时监控服务的运行状态、调用次数、响应时间等指标,并及时报警处理异常情况。

5、Dubbo框架提供了哪些服务容错机制,如何配置服务容错策略

Dubbo框架提供了多种服务容错机制,用于处理在分布式系统中可能出现的各种异常情况,确保系统的稳定性和可靠性。以下是Dubbo常用的几种服务容错策略:

  • Failover:失败自动切换:默认的容错机制,当调用服务失败时会自动切换到下一个可用的服务提供者,直到调用成功或者达到重试次数上限。
  • Failfast: 快速失败:快速失败机制,只会尝试一次调用,适用于对实时性要求较高的场景,不会进行重试。
  • Failback: 失败自动恢复:失败自动恢复机制,在失败后会定时重试调用,直到调用成功为止。
  • Failsafe: 失败安全:失败安全机制,调用失败时会直接返回空结果,适用于读操作,可以减少对系统的影响。
  • Failover + Failback: 失败自动切换 + 失败自动恢复:结合了失败自动切换和失败自动恢复两种机制,提高了系统的容错能力。
  • Forking: 并行调用:并行调用多个服务提供者,只要有一个调用成功即返回结果,适用于对结果要求较高的场景。
  • Broadcast: 广播调用:广播调用所有的服务提供者,将所有结果聚合起来,适用于广播通知等场景。

配置容错的话一般通过xml、注解、yaml配置来做
xml

<dubbo:service interface="com.example.DemoService" ref="demoService" cluster="failover"/>
<dubbo:reference id="demoService" interface="com.example.DemoService" cluster="failover"/>
  • 1
  • 2

java

	/**
	*提供者
	*/
	@Service(interfaceClass = DemoService.class, cluster = "failover")
	public class DemoServiceImpl implements DemoService {
	    // 服务实现代码
	}
	
	/**
	*消费者
	*/
	public class SomeConsumer {
	    @Reference(interfaceClass = DemoService.class, cluster = "failover")
	    private DemoService demoService;
	
	    // 使用 demoService 的代码
	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

yaml

dubbo:
  # 配置中心容错
  config-center:
    cluster: failover
  # 注册中心容错
  registry:
    cluster: failover
  # 消费者容错
  consumer:
    cluster: failover
  # 提供者容错
  provider:
    cluster: failover
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
6、Dubbo支持的注册中心有哪些?它们有何区别

Dubbo 支持多种注册中心,每种注册中心都有其特点和适用场景。以下是 Dubbo 支持的几种常见注册中心及它们的区别:

Zookeeper:
Zookeeper 是 Dubbo 最常用的注册中心之一,具有高可用、一致性和良好的性能特点。它适合用于生产环境,并且在大型分布式系统中表现优秀。

Nacos:
Nacos 是一个新一代的动态服务发现、配置管理和服务管理平台,具有服务注册、发现、配置管理等功能。与传统的注册中心相比,Nacos 具有更丰富的功能和更灵活的扩展性。

Consul:
Consul 是一个由 HashiCorp 公司开发的开源工具,提供了服务发现、健康检查、KV 存储等功能。它的特点是易于部署和维护,并且提供了丰富的 HTTP API 接口。

Etcd:
Etcd 是一个分布式的 Key-Value 存储系统,可以用于构建分布式系统中的注册中心。它具有高可用、一致性和可靠性等特点。

7、在Dubbo中如何处理服务的版本管理

当一个服务接口有新的实现或者接口定义发生变化时,您可以通过为该服务接口指定不同的版本号来进行版本管理。这样,在服务提供者和服务消费者之间就可以根据版本号来区分不同的服务实现,从而实现平滑的服务升级和版本切换。
在 Dubbo 的服务提供者和消费者配置中,您可以通过 @Service 或 @Reference 注解、XML 配置文件或者 Spring 配置文件中的 version 属性来指定服务的版本号。

// 服务提供者
@Service(version = "1.0.0")
public class SomeServiceImpl implements SomeService {
    // 实现代码
}

// 服务消费者
@Reference(version = "1.0.0")
private SomeService someService;

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
8、请描述一下Dubbo的SPI机制

在Dubbo中,SPI配置文件位于META-INF/dubbo目录下,使用接口名称加上后缀Adaptive的文件来定义扩展点的实现类。例如,如果有一个名为com.example.SomeInterface的接口需要扩展,可以创建一个名为com.example.SomeInterfaceAdaptive的配置文件,并在文件中指定具体的扩展实现类。
Dubbo允许通过@Adaptive注解标记接口或方法,实现自适应扩展。这意味着Dubbo根据运行时条件选择合适的扩展实现类。例如,当某个接口方法被标记为@Adaptive时,Dubbo会根据调用时的参数来选择合适的实现类。
Dubbo的SPI机制支持一个接口有多个实现类。通过在配置文件中指定不同的实现类,可以实现不同的功能扩展。Dubbo会根据实际情况选择合适的实现类。
Dubbo的SPI机制支持动态加载扩展实现类。这意味着无需重新编译和部署应用程序,可以通过更改配置文件来添加或替换扩展实现类。这提供了更大的灵活性和可维护性。
在加载扩展实现类时,Dubbo的SPI机制会根据配置文件中定义的加载顺序进行加载。如果某个实现类加载失败(例如找不到类或初始化失败),Dubbo会使用默认的实现类或者抛出异常。这种容错机制确保即使有些扩展实现类无法加载,系统仍能正常工作。

9、如何实现Dubbo过滤器

在 Dubbo 中,可以通过实现 Dubbo 提供的 Filter 接口来定义自己的过滤器功能。
需要实现 invoke 方法,该方法接收两个参数:Invoker<?> invoker 和 Invocation invocation,并返回一个 Result 对象。
在 invoke 方法中,您可以编写自己的逻辑来对调用进行过滤、增强或者其他操作,并最终返回处理结果。通过实现这个接口,您可以实现各种自定义的过滤器功能,比如权限控制、日志记录、异常处理等。
一旦实现了自定义的过滤器类,需要将其配置到 Dubbo 的配置文件中,以便 Dubbo 在启动时能够加载和应用这些过滤器。在配置文件中指定提供者和消费者的过滤器链,Dubbo 将按照配置的顺序依次调用这些过滤器。

gRPC

RabbitMQ

1、 MQ的优点

  1. 异步处理,提高了系统吞吐量
  2. 解耦:系统间通过消息通信,不用关心其他系统的处理。
  3. 削峰填谷:可以通过消息队列长度控制请求量;可以缓解短时间内的高并发请求.

2、RabbitMQ 的主要组件有哪些?它们各自的作用是什么?

  1. Broker:RabbitMQ 的核心,它接收和分派消息的组件。
  2. Exchange:用于接收来自生产者发送的消息,并将消息路由到一个或多个队列中。Exchange 的类型包括四种:direct、fanout、topic 和 headers。
  3. Queue:是存储消息的缓冲区,可以被消费者订阅并消费。消息从 Exchange 经过路由后最终被投递到 Queue 中。
  4. Binding:用于将 Exchange 和 Queue 之间建立桥梁,实现消息路由的功能。Binding 包含了 Exchange 的名称、Queue 的名称和 Routing Key。
  5. Connection:生产者和消费者与 RabbitMQ Broker 建立的 TCP 连接,用于传输消息。
  6. Channel:连接上的虚拟连接,用于完成消息的传输操作。在同一个连接中,可以创建多个 Channel,Channel 之间是相互独立且并发执行的。
  7. Producer:消息的发送者,将消息发送到 Exchange 中。
  8. Consumer:消息的接收者,从 Queue 中订阅并消费消息。
  9. Virtual Host:RabbitMQ 虚拟主机,用于隔离不同应用程序之间的消息队列。

3、 四种交换机对应的不同功能

Direct Exchange(直连交换机):
功能:将消息通过 Routing Key 直接路由到与之完全匹配的 Queue 中。
路由规则:Exchange 会根据消息的 Routing Key 将消息发送到对应的 Queue 中,只有当消息的 Routing Key 与 Queue 绑定时指定的 Routing Key 完全匹配时,消息才会被投递到该 Queue。

Fanout Exchange(扇形交换机):
功能:将消息广播到所有绑定的 Queue 中,忽略 Routing Key。
路由规则:Exchange 接收到消息后,会将消息发送到所有与之绑定的 Queue 中,无需考虑消息的 Routing Key。

Topic Exchange(主题交换机):
功能:根据通配符匹配将消息路由到一个或多个 Queue 中。
路由规则:Exchange 会根据消息的 Routing Key 和 Queue 绑定时指定的通配符规则进行匹配,将消息发送到符合条件的 Queue 中。其中,通配符符号 * 匹配一个单词,# 可以匹配零个或多个单词。

Headers Exchange(标题交换机):
功能:根据消息头信息进行匹配,而非 Routing Key。
路由规则:Exchange 会根据消息的 Headers 属性进行匹配,并根据匹配结果将消息发送到对应的 Queue 中。在绑定时需要指定一组键值对,只有当消息的 Headers 属性与这些键值对完全匹配时,消息才会被发送到对应的 Queue 中。

4、消息基于什么传输

RabbitMQ 使用信道的方式来传输数据。信道是建立在真实的 TCP 连接内的虚拟连接,且每条 TCP 连接上的信道数量没有限制

5、RabbitMQ 的消息确认机制和RabbitMQ 中的消息确认模式

消息确认机制可以保证消息不会因为网络故障、Broker 故障等原因而丢失,从而提高消息传递的可靠性。在 RabbitMQ 中,有两种消息确认模式:Publisher Confirm 模式和 Consumer Acknowledge 模式。

Publisher Confirm 模式(生产者确认):
在生产者发送消息到 Exchange 后,可以通过确认机制来确保消息已经被 Broker 成功接收。
生产者在发送消息时,可以将信道设置为 Confirm 模式,当消息成功到达 Broker 时,Broker 会返回一个确认信息给生产者。
生产者可以通过监听 Confirm 信号来获取消息是否成功发送的确认信息,以此来决定是否进行下一步操作。
这种确认模式适用于需要确保消息不丢失的场景,如重要业务消息的发送。

Consumer Acknowledge 模式(消费者确认):
在消费者接收并处理消息后,可以向 Broker 发送确认信息,告知 Broker 消息已经成功处理。
消费者可以使用手动确认(manual acknowledgment)或自动确认(automatic acknowledgment)两种方式来确认消息的处理情况。
手动确认需要消费者在完成消息处理后显式地发送确认信息,而自动确认则由 RabbitMQ 自动确认消息。
手动确认模式适用于消费者需要在处理消息后执行一些复杂操作,确保消息处理的完整性和准确性。

6、什么是 RabbitMQ 的死信队列(DLX)?它有什么作用?如何通过私信队列实现延迟队列

RabbitMQ 的死信队列(Dead Letter Exchange,DLX)是一种特殊的队列,用于存储那些无法被消费者成功消费并处理的消息。当消息满足一定条件而被标记为“死信”时,这些消息会被重新路由到死信队列中,以便进行后续处理。

下面是关于 RabbitMQ 死信队列的一些重要信息和作用:

触发条件:
消息成为死信的触发条件可以通过设置队列的 TTL(Time-To-Live)、消息过期时间、消息被拒绝等多种方式来定义。
当消息满足这些条件之一时,RabbitMQ 将会将该消息标记为死信,并将其路由到指定的死信交换机(Dead Letter Exchange)。
作用:
死信队列可以帮助处理那些无法被正常消费和处理的消息,从而避免这些消息一直占据队列资源或丢失。
通过使用死信队列,可以对处理失败的消息进行分类、记录和后续处理,有助于排查和解决消息处理失败的原因。
死信队列还可以用于实现延迟消息队列、消息重试机制等应用场景,提高消息处理的灵活性和可靠性。
除了使用死信队列实现延迟消息队列,还可以通过私信队列实现延迟队列。具体步骤如下:
如何实现
创建一个普通队列,并设置其属性,包括消息的 TTL、队列绑定的交换机及路由键等信息。
在队列绑定的交换机上创建一个私信队列(Private Queue),并将其绑定到一个专门用于处理延迟消息的交换机上。
当消息达到其 TTL 时,RabbitMQ 将会将该消息路由到私信队列中,而不是死信队列中。
私信队列再将延迟消息发送到专门用于处理延迟消息的交换机中,以便进行后续处理。

RocketMQ

Kafka

ElasticSearch

Quartz

XXL-Job

数据库

Mysql

1、数据库分类和常用数据库

数据库分为关系型数据库和非关系型数据库
关系型数据库:
Mysql,Oracle,sqlServer,DB2等

因为关系型数据库的原因,各表之间可以有关联,方便对用户请求数据的分模块、分功能管理,可以使用简单的sql语句就可以做非常复杂的数据查询。

非关系型数据库:
NoSql,Redis,MongoDB

非关系型数据库利用键值对的方式存储数据,不便于存储用户的大量数据,也正是因为这个机制,非关系型数据库经常被用做关系型数据库的缓存,以此减少关系型数据库的压力和资源消耗。

2、关系型数据库三大范式

第一范式: 数据表中的每个字段都具有不可拆分性。也是数据设计中最基本的要求。
第二范式: 在满足第一范式的基础上,消除了部分依赖,解决了删除异常,插入异常和修改异常。
第三范式: 在满足第二范式的基础上,消除了传递依赖。

3、事务的三大特征

  1. 原子性:保证事务中包含的操作为一个独立的数据单元。
  2. 隔离性:保证事务与事务之间不存在相互干扰,保证事务之间的相互独立性
  3. 一致性:保证事务执行结束后数据的一致状态,所有操作要么全部成功,要么全部失败。

4、mysql数据库最大连接数

mysql的最大值是16384,默认值是100,最大连接数同时也根据当前系统的内存、tcp连接数相关,但是一般不建议设置为最大值,这样很容易导致数据库崩溃,一般情况下我们会设置一个最大连接数,用来限制数据库的最大连接,防止访问量过大导致数据库崩溃。

5、MySQL或者Oracle分页语句

mysql

-- 0代表开始查找的索引值,5代表查找数量。
select * 
from your_table 
limit 0,5;
  • 1
  • 2
  • 3
  • 4

Oracle

-- 1代表开始查找的索引值,5代表查找数量。
SELECT *
FROM (
    SELECT your_columns, ROWNUM AS rn
    FROM your_table
    WHERE your_conditions
    ORDER BY your_order_column
)
WHERE ROWNUM BETWEEN 1 AND 5;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

6、简单说说jdbc的理解

JDBC(Java Database Connectivity)是 Java 语言访问数据库的标准 API。通过 JDBC,Java 程序可以与各种关系型数据库进行连接、查询和更新数据。
包括以下功能:
连接数据库:使用 JDBC,Java 应用程序可以通过驱动程序连接到各种数据库管理系统(如Oracle、MySQL、SQL Server 等)。
执行 SQL 语句:通过 JDBC,可以发送 SQL 查询和更新语句到数据库,并获取执行结果。
处理结果集:JDBC 可以帮助开发人员处理从数据库返回的结果集,包括读取查询结果、处理数据并在应用程序中进行展示。
事务管理:JDBC 支持事务管理,可以通过提交或回滚事务来确保数据的完整性和一致性。
异常处理:JDBC 提供了异常处理机制,可以捕获和处理与数据库连接、查询或更新相关的异常情况。
元数据访问:JDBC 允许开发人员访问数据库的元数据信息,如表结构、列属性等。

7、数据库连接池的作用

连接池极大的提高了数据库的性能,极大的节省了数据库资源。
提高获取数据库连接的速度,避免了频繁创建和销毁连接。
方便对数据库连接的管理,方便对数据库连接的调优。

8、mysql优化

  • 为搜索字段建立索引可以提高效率,但是要注意的是建立索引不一定会提高效率,当数据库数据比较少时,通过索引进行查找反而会降低效率,因为查找索引本身也是要消耗资源的。
  • MySQL集群、负载均衡、读写分离
  • 根据需要指定不同的存储引擎
  • 查询时避免多个范围条件
  • 优化关联查询,确保关联的列要有索引,确保group by分组是的列有索引

9、什么是索引,索引的类别

  • 索引也是一种文件,它包含着数据库中所有记录的指针
  • 索引是一种排序数据结构,为了实现快速查找,更新表数据,底层使用B+树进行实现
  • 索引也可以理解为目录,通过查找目录快速定位目标位置,索引本身也是需要占用磁盘空间的
  • 索引的原理是将内容进行排序,并生成倒排表,在倒排表上拼上地址链,在查询时先拿到倒排表,根据地址链指向的地址拿到具体数据。

Mysql中的索引类别有以下几种:
B-Tree索引:B-Tree索引是MySQL中最常用的索引类型,它可以用于几乎所有的数据类型。B-Tree索引是一种平衡树结构,它能够快速定位到某个key值对应的记录。

哈希索引:哈希索引适用于等值比较查询,例如使用“=”或“IN”条件进行查询。哈希索引使用哈希算法将key值转换为哈希码,然后使用哈希码来进行查询。相对于B-Tree索引,哈希索引查询速度更快,但不支持排序和范围查询。

全文索引:全文索引适用于文本类型的列,例如VARCHAR和TEXT类型的列。全文索引可以实现针对文本内容的关键字搜索,支持自然语言查询和布尔查询等功能。

空间索引:空间索引适用于存储地理位置信息、二维坐标点等类型的列,例如GEOMETRY类型的列。空间索引可以进行距离计算、区域查询等功能。

前缀索引:前缀索引可以只对索引列的前n个字符进行索引,从而减少索引的存储空间和提高查询性能。但是由于只索引了部分字符,可能存在重复值较多的情况,因此需要权衡存储空间和查询效率。

联合索引(Composite Index):联合索引是指将多个列组合起来创建的索引,可以同时对多个列进行索引,从而加快多列条件查询的速度。当查询条件涉及到联合索引的所有列或部分列时,数据库可以有效地利用联合索引来提高查询性能。在创建联合索引时,需要注意列的顺序,通常应该将最常用于查询的列放在联合索引的前面。

主键索引(Primary Key Index):主键索引是用来唯一标识每条记录的索引,它要求所有记录都有唯一的主键值,通常是表中的某个列或列组合。主键索引不允许有重复值或NULL值,并且自动创建主键索引也会自动创建主键约束,确保数据的完整性和唯一性。主键索引可以帮助数据库快速定位到指定记录,是表中最重要的索引之一。
唯一索引(Unique Index):唯一索引要求索引列的值是唯一的,可以用来保证数据的唯一性约束。与主键索引不同的是,唯一索引允许存在NULL值(只能有一个NULL值),并且可以有多个唯一索引。

10、索引失效的场景,如何避免索引失效

  1. 如果条件中有or,即使其中有条件带索引也不会使用,如果想使用or又想使用索引,就需要在条件的每个列上都添加索引
  2. 如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引
  3. 使用联合索引的时候要遵循最左匹配原则,否则索引会失效
  4. 如果一个列中的值具有很低的选择性(即,该列的唯一值数量很小),那么对该列进行索引可能是没有意义的。例如,性别列只有两个值“男”和“女”,对该列进行索引可能并不会提高查询性能。
  5. 如果在查询语句中对索引列进行了函数操作(如使用 UPPER() 函数进行大小写转换),那么索引可能会失效。因为索引是根据原始列值进行存储和排序的,而函数操作会改变原始列值,从而使得索引失效。
  6. 如果在查询语句中对索引列进行了数据类型转换(如将字符型列转换为数字型列),那么索引可能会失效。因为索引是根据原始列数据类型进行存储和排序的,而数据类型转换会改变原始列数据类型,从而使得索引失效。
  7. 如果在查询语句中对索引列进行了模糊查询(如LIKE查询以%开头),那么索引可能会失效。因为在一些情况下,数据库可能无法使用索引直接匹配查询条件,而需要进行全表扫描,从而导致查询性能下降。
  8. 如果在查询语句中对索引列进行了范围查询(如使用 BETWEEN 或 < >),那么索引可能会失效。因为在一些情况下,数据库可能无法使用索引直接匹配查询条件,而需要进行全表扫描,从而导致查询性能下降。

11、 B树和B+树的区别

首先B树上,内容节点可以存放键和值,但是B+树的内容节点都是键,所有的值都在叶子节点上
其次B树的每个叶子节点相互独立,B+树的叶子节点有一条链相连

B树可以将经常查询的热点数据放入内容节点中,有利于缩小查找范围,方便快速查找。
B+树因为在叶子节点上有一条链相连,所以查找时会根据链进行顺序查找,也更适合于范围查找。

12、mysql常用的存储引擎

  • MyISAM:MyISAM表是独立于操作系统的,可以轻松的从windows服务器移植到linux服务器,但是MyISAM引擎无法处理事务,这也意味着当需要处理和事务有关的表时,不能使用MyISAM引擎
  • InnoDB:InnoDB是一个强大的支持事务的引擎,并且有较高的并发读取频率,所以Mysql中默认的存储引擎就是InnoDB
  • MEMORY:从名字就可以知道是一个基于内存的存储引擎,为了得到最快的响应时间而在内存中存储表数据,以此达到很高的性能,但是当守护进程崩溃时,所有Memory数据会全部丢失。所以一般用于数据较小的热点数据存储,或者数据是临时的,可以使用该引擎。

13、主从复制、读写分离

当写操作影响数据库查询效率时,这时候就要用到读写分离。
读写分离的基本的原理是让主数据库处理事务性增、改、删操作,而从数据库处理查询操作。我们可以通过主从复制实现数据同步,通过读写分离提升并发负载能力。

14、主从复制的几种方式

  1. 基于语句的复制:在主数据库上执行sql语句,在从数据库上执行同样的语句,而mysql默认采用基于语句复制,并且执行效率高。
  2. 基于行的复制:将改变的内容复制到从服务器,不在进行语句的执行。
  3. 混合类型的复制:属于以上两种方法的合体,平时采用基于语句的复制,一旦发现基于语句无法精确复制时,就会采用基于行的复制。

15、主从复制的过程

首先在主数据库执行完事务操作后,将执行操作写入binlog日志文件,随后通知从数据库进行同步。从数据库会建立一个I/O线程,随后从主数据库中读取事件放入中继日志,如果已经将事件读取完毕,则陷入阻塞状态,等待新的事件到来。随后从数据库从中继日志读取事件进行数据的同步更新。

16、mysql分库分表

当读操作频繁时,我们可以增加从数据库进行读写分离来降低数据库压力,但是当从数据库也变多的时候,我们就要考虑分库。同样当写操作压力很大时,我们也要进行分库操作。

垂直分库:
我们可以按照业务进行拆分,将不同业务的表放在不同的库中,这样就可以减轻单库的操作压力。但是随着数据的增大同样会让数据库的能力达到瓶颈,服务器磁盘、内存也会受到影响,这时候就要将其拆分到多个服务器上。
垂直分表:
一般就是将大表拆成小表,将不常用的字段拆分出来放到扩展表中,避免查询的时候数据量太大造成跨页问题。

水平分表
类似于垂直分库,只是将表的内容做了一定限制,可以减少单表数据量,达到查询效率的提升。
水平分库分表
在水平分表的基础上将数据切片部署在多个服务器中,每个服务器具有相对应的库和表,但是表的数据不同,这样做可以有效缓解数据库的大部分压力。

17、分库分表策略

策略介绍
哈希取模根据用户的id进行取模,按照取模后的结果放入不同的数据库
范围分片比如根据id进行划分,1-10000划入表1,10001-20000划入表2
地理位置分片按照地理位置进行分片,华东区一个表,华北区一个表
时间分片按照时间进行分片,比如按年或者月进行分片

18、mysql日志

mysql总共有4种日志类型

日志类型介绍
Error Log(错误日志)用于几率sql错误信息
General Query Log(一般查询日志)记录目前sql正在做的事情
Binary Log(二进制日志)记录数据库的建表,改动等,可以通过该日志进行数据回滚和主从复制的数据同步
Slow Query Log(慢查询日志)记录查询时间超过阈值的sql语句,常用于数据库调优

19、mysql事务隔离级别

mysql拥有4种事务隔离界别从低到高分别是
读未提交、读已提交(解决脏读)、可重复读(解决不可重复读)、序列化(解决幻读)

20、mysql锁类别

  • 共享锁: 也叫读锁,在执行查询操作时,对数据上锁,同时可以多个线程拿到这把锁
  • 排它锁: 也叫写锁,在执行事务操作时,对数据上锁,但是只可有一个线程获取这把锁,别的线程必须等待释放锁后才可执行事务操作。
  • 行级锁:是mysql中锁粒度最细的一种锁,只对当前操作行进行加锁,执行事务操作时不会影响其他行的操作,所以能减少数据库操作的冲突,但是加锁的开销也是最大的。同时用可能出现死锁
  • 表级锁:是mysql中锁粒度最大的一种锁,表示对整张表加锁,相比行锁消耗资源较少,并且被大部分mysql引擎支持。
  • 页级锁:是mysql中锁粒度介于行锁和表锁中间的一种锁,因为行锁消耗大,页锁锁定太多,所以折中了这两个锁形成页锁,一次锁定相邻的一组记录。但是同样会出现死锁,锁定粒度介于行锁和表锁之间。
    在innoDB下的行锁是基于索引进行行锁,而主键自带索引,但是如果主键没有索引,那么也无法进行行锁,直接升级为表锁。

21、什么是游标,存储过程,触发器

  • 游标是系统为用户开设的一个数据缓冲区,存放SQL语句的执行结果,每个游标区都有一个名字。用户可以通过游标逐一获取记录并赋给主变量,交由主语言进一步处理

  • 存储过程是一个预编译的SQL语句,允许模块化的设计,只需要创建一次,以后在该程序中就可以调用多次。如果某次操作需要执行多次SQL,使用存储过程比单纯SQL语句执行要快。

  • 触发器是用户定义在关系表上的一类由事件驱动的特殊的存储过程。触发器是指一段代码,当触发某个事件时,自动执行这些代码。

22、常用触发器

  • Before Insert
  • After Insert
  • Before Update
  • After Update
  • Before Delete
  • After Delete

23、BTree 与 Hash 索引有什么区别

Hash索引是通过hash函数计算出hash值在表中查找对应的数据,所以在精确查找上hash索引要快

BTree索引底层使用B+树实现,要多次使用折半查找来找到对应的数据,所有数据都存放在叶子节点上,并且每个叶子节点之间都有链相连,所以BTree索引支持范围查找,模糊查询。

Redis

1、Redis的优点

  • 读写性能优异
  • 支持数据持久化
  • 支持事务
  • 数据结构丰富
  • 支持主从复制,读写分离,分片,集群

2、Redis为甚么执行效率快

  1. 单线程操作,不存在多线程切换小号cpu,不用考虑锁的问题,所以不会出现死锁
  2. 是基于内存实现的,避免大量的访问数据库,减少直接读取磁盘的数据
  3. 采用了非阻塞IO多路复用机制

3、redis事务的特征

  1. 开启事务后所有命令都将变为串行化进行执行,在执行期间,redis不再为其他客户端的请求提供服务,从而保证事务的原子性。
  2. 在执行期间如果出现某一条命令执行失败,后面的命令仍会继续执行。

4、redis的雪崩、穿透、击穿

  • 缓存雪崩(Cache Avalanche):指缓存中大量的key在同一时间失效,导致大量请求同时落在数据库上,引起数据库的崩溃。解决办法主要有两种:一是使用高可用架构(如Redis集群)来保证系统的可用性;二是对缓存的过期时间进行随机化(Jitter)来避免缓存同时失效。

  • 缓存穿透(Cache Penetration):指恶意攻击者故意查询不存在于缓存和数据库中的数据,导致每次查询都需要访问数据库,从而引起数据库压力过大而崩溃。解决办法主要有两种:一是在代码中对输入参数进行校验,拒绝不合法的请求;二是使用布隆过滤器(Bloom Filter)等技术,过滤掉一部分不存在于数据库中的请求。

  • 缓存击穿(Cache Miss):指一个不存在于缓存中的key,被大量并发请求访问,导致这些请求都落在了数据库上,从而引起数据库的压力过大,导致系统崩溃。解决办法一般是使用互斥锁(如Redis的setnx命令)或者在缓存中设置一个空对象(Null Object Pattern)来避免缓存穿透。

4、redis同步机制

全量同步

  1. 从节点第一次连接主节点时,主节点会生成rdb文件,期间所有写命令被放入缓存区,生成rdb后发送给从节点
  2. 从节点抛弃旧数据,执行rdb文件,随后执行主节点缓存区的命令
  3. 之后主节点的每一条写命令都会发送给从节点去执行。

增量同步
4. 如果出现网络问题导致的数据丢失情况,从节点自身已经保存了已复制的偏移量和主节点id
5. 主节点根据偏移量将缓存区的数据发送给从节点,保证主从复制状态的正常。

5、redis持久化机制

有RDB和AOF两种方式做持久化,RDB做快照持久,AOF做增量持久

RDB根据快照保存当前数据库中的信息,随后开启一个线程创建RDB文件,但是RDB机制依旧会存在一定问题,因为它是每隔一段时间进行快照,这种方式会造成上次快照时间到这一秒中的数据丢失。

AOF是遵循redis协议对执行的命令进行持久化,redis会记录下所有变更数据库状态的命令,redis在载入AOF文件的时候,把AOF中每一条命令都执行一遍,最终还原回数据库的状态,它的载入也是自动的。在RDB和AOF文件都有的情况下,redis会优先载入AOF文件

6、redis集群搭建

  1. 使用主从复制,达到一个主节点下挂多个从节点,使数据冗余,同时还可以实现读写分离,减轻redis压力,但是也存在一定问题,当主节点挂掉以后,相当于整个redis集群还是都挂掉了,并没有提升高可用,所以我们要引入哨兵机制。
  2. 利用哨兵对主节点和从节点进行监控,为了防止哨兵存在挂掉的情况,哨兵也是可以搭建集群的。而监控的方式是乒乓机制,每隔一段固定时间就会向所监控的节点发送ping,节点会回复pang,如果监控的节点没有进行回复,则哨兵会将此节点判定为下线状态,并通过投票选举出一个新的主节点。但是这样依旧存在一个问题,redis的容量受限于单机配置。
  3. cluster解决了redis分布式存储,也就是每个节点存储的不通的内容来解决在线扩容的问题,对数据进行分片处理,所有的在redis的节点上有个哈希槽,取值为0-16383,当存取key时会g恩局CRC16算法和取模运算得出一个余数,随后会放到对应的哈希槽中,这样每个值都对应一个槽,通过这个值可以去不同节点对相应的key值进行操作。同样为了保证高可用也可以引用主从和哨兵,集群搭建最少要配置6个节点,3主3从,主节点实现读写操作,从节点作为备份节点,同样哨兵也可以搭建集群避免哨兵宕机。

7、redis应用场景

  1. 缓存:将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率。
  2. 排行榜:ZSet 可以实现有序性操作,从而实现排行榜等功能。
  3. 计数器:可以对string进行自增自减运算,因为是内存型数据库,所以读写效率非常高,很适合频繁的读写

8、redis过期键删除策略

  1. 定时删除:每个key设定一个时间,到达时间后自动删除,可以达到实时删除的目的,对内存友好,但是需要占用cpu大量资源去监控定时器。
  2. 惰性删除:只有在访问一个key时,才回去判断是否过期,如果过期则删除,相比定时删除,对cpu友好,但是可能存在一个key一直不被访问,所以也没办法删除,导致占用内存。
  3. 定期删除:上面两种的这种方案,每隔一段时间会扫描一定数量的key,并清除中间已经过期的key值,这样达到了内存和cpu的最优平衡效果,

9、redis内存淘汰

全局的键空间选择删除策略:

  1. 当内存不足容纳新数据时,会执行失败
  2. 当内存不足容纳新数据时,会删除最近最少使用的key
  3. 当内存不足容纳新数据时,会随机删除某个key

设置过期时间的键空间选择性的删除:

  1. 当内存不足容纳新数据时,在设置了过期时间中,移除最少使用的key
  2. 当内存不足容纳新数据时,在设置了过期时间中,随机移除某个key
  3. 当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。

10 如何用redis实现分布式锁

使用SETNX命令,SETNX(SET if Not eXists)是 Redis 提供的一个原子性命令,用于设置指定键的值,仅当该键不存在时才设置成功。因此,可以利用 SETNX 命令来实现分布式锁。具体实现方式是将某个唯一标识作为锁的值,以锁的名字作为键,通过 SETNX 命令尝试获取锁。如果 SETNX 返回 1,表示成功获取到锁;否则,表示锁已经被其他客户端持有。
springboot环境中可以使用RedisTemplate来进行实现

	/**
	* 获取锁
	*/
	public boolean acquireLock(String lockKey, String uniqueValue, long expireTime) {
        ValueOperations<String, String> ops = redisTemplate.opsForValue();
        Boolean result = ops.setIfAbsent(lockKey, uniqueValue);
        if (result != null && result) {
            redisTemplate.expire(lockKey, expireTime, TimeUnit.MILLISECONDS);
            return true;
        }
        return false;
    }
    
	/**
	* 释放锁
	*/
    public void releaseLock(String lockKey, String uniqueValue) {
        String value = redisTemplate.opsForValue().get(lockKey);
        if (uniqueValue.equals(value)) {
            redisTemplate.delete(lockKey);
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

11、什么是布隆过滤器,原理是什么

布隆过滤器(Bloom Filter)是一种空间效率高、快速判断元素是否存在的概率型数据结构。它通过使用一个很长的二进制向量和一系列的哈希函数来表示集合,并可以高效地进行插入和查询操作。

布隆过滤器的原理如下:

初始化:布隆过滤器是一个长度为m的位数组(通常用二进制表示),初始时所有位都被置为0。

插入操作:当要将一个元素插入布隆过滤器时,使用k个不同的哈希函数对该元素进行哈希计算,得到k个哈希值。然后将位数组中对应这k个哈希值的位置置为1。

查询操作:当要查询一个元素是否存在于布隆过滤器时,同样使用k个哈希函数对该元素进行哈希计算,得到k个哈希值。然后检查位数组中对应这k个哈希值的位置是否都为1,若有任何一个位置为0,则说明该元素不存在;若所有位置都为1,则该元素可能存在,但可能存在误判的概率。

由于布隆过滤器采用了多个哈希函数进行计算,并且将结果映射到位数组中,因此可以有效地减少冲突的概率。但是布隆过滤器存在一定的误判率,即有时会判断某个元素存在于布隆过滤器中,但实际上并不存在。这是因为多个元素哈希到了位数组中的同一位置,导致冲突。为了降低误判率,可以适当增加位数组的长度和哈希函数的数量。

Linux服务器

设计模式

工厂方法模式

抽象工厂模式

建造者模式

原型模式

单例模式

适配器模式

桥接模式

组合模式

装饰者模式

外观模式

享元模式

代理模式

责任链模式

命令模式

解释器模式

迭代器模式

中介者模式

备忘录模式

观察者模式

状态模式

策略模式

模板方法模式

访问者模式

算法

排序算法

冒泡排序

通过两层循环遍历数组,每次比较相邻的两个元素,如果顺序不对则交换它们,直到整个数组排序完成

public static void bubbleSort(int[] arr) {
       int n = arr.length;
       for (int i = 0; i < n - 1; i++) {
           for (int j = 0; j < n - i - 1; j++) {
               if (arr[j] > arr[j + 1]) {
                   // 交换 arr[j] 和 arr[j+1]
                   int temp = arr[j];
                   arr[j] = arr[j + 1];
                   arr[j + 1] = temp;
               }
           }
       }
   }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

选择排序

通过两层循环遍历数组,在每一轮中找到未排序部分的最小元素的索引,然后将其与当前位置的元素进行交换

	public static void selectionSort(int[] arr) {
        int n = arr.length;
        for (int i = 0; i < n - 1; i++) {
            int minIndex = i;
            for (int j = i + 1; j < n; j++) {
                if (arr[j] < arr[minIndex]) {
                    minIndex = j;
                }
            }
            // 交换 arr[i] 和 arr[minIndex]
            int temp = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = temp;
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

插入排序

从数组的第二个元素开始,将当前元素插入到已经排序好的部分的合适位置。在插入过程中,需要将大于当前元素的元素依次向右移动。

	public static void insertionSort(int[] arr) {
        int n = arr.length;
        for (int i = 1; i < n; i++) {
            int key = arr[i];
            int j = i - 1;

            // 将大于 key 的元素都向右移动
            while (j >= 0 && arr[j] > key) {
                arr[j + 1] = arr[j];
                j--;
            }
            arr[j + 1] = key;
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

希尔排序

希尔排序是插入排序的一种改进,它通过使用不同的步长来对数组进行排序,从而减少逆序对的数量,提高了效率。

	public static void shellSort(int[] arr) {
        int n = arr.length;
        
        // 初始步长设定为数组长度的一半,然后逐步缩小步长直至 1
        for (int gap = n / 2; gap > 0; gap /= 2) {
            // 类似于插入排序,但是步长为 gap
            for (int i = gap; i < n; i++) {
                int temp = arr[i];
                int j;
                for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {
                    arr[j] = arr[j - gap];
                }
                arr[j] = temp;
            }
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

归并排序

归并排序采用分治策略,将数组不断拆分为更小的部分,然后合并已排序的部分,最终得到完全有序的数组。

 	public static void mergeSort(int[] arr, int left, int right) {
        if (left < right) {
            int mid = left + (right - left) / 2;
            mergeSort(arr, left, mid);
            mergeSort(arr, mid + 1, right);
            merge(arr, left, mid, right);
        }
    }

    public static void merge(int[] arr, int left, int mid, int right) {
        int n1 = mid - left + 1;
        int n2 = right - mid;

        int[] L = new int[n1];
        int[] R = new int[n2];

        for (int i = 0; i < n1; i++) {
            L[i] = arr[left + i];
        }
        for (int j = 0; j < n2; j++) {
            R[j] = arr[mid + 1 + j];
        }

        int i = 0, j = 0;
        int k = left;
        while (i < n1 && j < n2) {
            if (L[i] <= R[j]) {
                arr[k] = L[i];
                i++;
            } else {
                arr[k] = R[j];
                j++;
            }
            k++;
        }

        while (i < n1) {
            arr[k] = L[i];
            i++;
            k++;
        }

        while (j < n2) {
            arr[k] = R[j];
            j++;
            k++;
        }
    }
  • 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
  • 45
  • 46
  • 47
  • 48

快速排序

快速排序通过选取一个基准元素,将数组分割为比基准元素小和比基准元素大的两部分,然后分别对这两部分递归进行排序,最终得到完全有序的数组。

	public static void quickSort(int[] arr, int low, int high) {
        if (low < high) {
            int pi = partition(arr, low, high);
            quickSort(arr, low, pi - 1);
            quickSort(arr, pi + 1, high);
        }
    }

    public static int partition(int[] arr, int low, int high) {
        int pivot = arr[high];
        int i = low - 1;
        for (int j = low; j < high; j++) {
            if (arr[j] < pivot) {
                i++;
                int temp = arr[i];
                arr[i] = arr[j];
                arr[j] = temp;
            }
        }
        int temp = arr[i + 1];
        arr[i + 1] = arr[high];
        arr[high] = temp;
        return i + 1;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

堆排序

堆排序首先将数组构建成一个最大堆,然后逐步将堆顶元素(最大元素)与最后一个元素交换并调整堆,直到整个数组有序。

	public void heapSort(int[] arr) {
        int n = arr.length;

        // Build heap (rearrange array)
        for (int i = n / 2 - 1; i >= 0; i--) {
            heapify(arr, n, i);
        }

        // One by one extract an element from heap
        for (int i = n - 1; i > 0; i--) {
            // Move current root to end
            int temp = arr[0];
            arr[0] = arr[i];
            arr[i] = temp;

            // Call max heapify on the reduced heap
            heapify(arr, i, 0);
        }
    }

    void heapify(int[] arr, int n, int i) {
        int largest = i; // Initialize largest as root
        int left = 2 * i + 1;
        int right = 2 * i + 2;

        // If left child is larger than root
        if (left < n && arr[left] > arr[largest]) {
            largest = left;
        }

        // If right child is larger than largest so far
        if (right < n && arr[right] > arr[largest]) {
            largest = right;
        }

        // If largest is not root
        if (largest != i) {
            int swap = arr[i];
            arr[i] = arr[largest];
            arr[largest] = swap;

            // Recursively heapify the affected sub-tree
            heapify(arr, n, largest);
        }
    }
  • 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
  • 45

计数排序

	public void countingSort(int[] arr) {
        int n = arr.length;

        // Find the maximum element in the array
        int max = arr[0];
        for (int i = 1; i < n; i++) {
            if (arr[i] > max) {
                max = arr[i];
            }
        }

        // Create a count array to store the count of each unique element
        int[] count = new int[max + 1];

        // Store the count of each element in the count array
        for (int i = 0; i < n; i++) {
            count[arr[i]]++;
        }

        // Modify the count array to store the actual position of each element in the output array
        for (int i = 1; i <= max; i++) {
            count[i] += count[i - 1];
        }

        // Create an output array and fill it with the sorted elements
        int[] output = new int[n];
        for (int i = n - 1; i >= 0; i--) {
            output[count[arr[i]] - 1] = arr[i];
            count[arr[i]]--;
        }

        // Copy the sorted elements back to the original array
        for (int i = 0; i < n; i++) {
            arr[i] = output[i];
        }
    }
  • 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

桶排序

桶排序将元素分配到不同的桶中,然后对每个桶中的元素进行排序,最后将所有桶中的元素按顺序合并起来,从而完成排序。

	public void bucketSort(float[] arr) {
        int n = arr.length;
        @SuppressWarnings("unchecked")
        ArrayList<Float>[] buckets = new ArrayList[n];

        // Create empty buckets
        for (int i = 0; i < n; i++) {
            buckets[i] = new ArrayList<>();
        }

        // Add elements into the buckets
        for (int i = 0; i < n; i++) {
            int bucketIndex = (int) (n * arr[i]);
            buckets[bucketIndex].add(arr[i]);
        }

        // Sort the elements in each bucket
        for (int i = 0; i < n; i++) {
            Collections.sort(buckets[i]);
        }

        // Concatenate the sorted buckets
        int index = 0;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < buckets[i].size(); j++) {
                arr[index++] = buckets[i].get(j);
            }
        }
    }
  • 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

基数排序

基数排序从最低位到最高位依次对每个位进行计数排序,直到完成排序

	public void radixSort(int[] arr) {
        // Find the maximum element in the array
        int max = Arrays.stream(arr).max().getAsInt();

        // Perform counting sort for every digit
        for (int exp = 1; max / exp > 0; exp *= 10) {
            countingSort(arr, exp);
        }
    }

    private void countingSort(int[] arr, int exp) {
        int n = arr.length;
        int[] output = new int[n];
        int[] count = new int[10];

        // Initialize count array
        Arrays.fill(count, 0);

        // Store count of occurrences in count[]
        for (int i = 0; i < n; i++) {
            count[(arr[i] / exp) % 10]++;
        }

        // Change count[i] so that count[i] contains actual position of this digit in output[]
        for (int i = 1; i < 10; i++) {
            count[i] += count[i - 1];
        }

        // Build the output array
        for (int i = n - 1; i >= 0; i--) {
            output[count[(arr[i] / exp) % 10] - 1] = arr[i];
            count[(arr[i] / exp) % 10]--;
        }

        // Copy the output array to arr[]
        System.arraycopy(output, 0, arr, 0, n);
    }
  • 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

睡眠排序

睡眠排序(Sleep Sort)是一个有趣且不太实用的“排序算法”,它基于每个元素的大小来安排线程的睡眠时间,从而实现排序。这种排序方法仅适用于对正整数进行排序,并且对于大量数据来说效率非常低,不适合实际使用。

鸡尾酒排序

鸡尾酒排序(Cocktail Shaker Sort),也称作双向冒泡排序(Bidirectional Bubble Sort)或定向冒泡排序(Shuttle Sort),是冒泡排序的一种变体。它与传统的冒泡排序的区别在于,鸡尾酒排序是从左到右和从右到左交替进行的,而不是单向地从左到右进行。
鸡尾酒排序的基本思想是通过多次往返扫描列表来实现排序,每次扫描都会在两个方向上依次比较相邻的元素,并根据需要交换它们的位置。这样可以在一定程度上提高排序的效率,特别是对于某些接近排序好的列表。

查找算法

线性查找

	public static int linearSearch(int[] arr, int target) {
        for (int i = 0; i < arr.length; i++) {
            if (arr[i] == target) {
                return i;
            }
        }
        return -1;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

二分查找

二分查找(Binary Search),也称为折半查找,是一种高效的查找算法,要求数据集合必须是有序的。它通过将目标值与数据集合的中间值进行比较,以确定目标值在左侧还是右侧,并逐步缩小查找范围,直到找到目标值或确定不存在。

	public static int binarySearch(int[] arr, int target) {
        int left = 0;
        int right = arr.length - 1;

        while (left <= right) {
            int mid = left + (right - left) / 2;

            if (arr[mid] == target) {
                return mid;
            } else if (arr[mid] > target) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }

        return -1;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

插值查找

插值查找(Interpolation Search)是一种改进的二分查找算法,特别适用于数据量较大且分布均匀的有序数据集合。与二分查找不同,插值查找通过插值公式来预测目标值可能在数据集中的位置,从而更快地缩小查找范围。

	public static int interpolationSearch(int[] arr, int target) {
        int low = 0;
        int high = arr.length - 1;

        while (low <= high && target >= arr[low] && target <= arr[high]) {
            int mid = low + ((target - arr[low]) * (high - low) / (arr[high] - arr[low]));

            if (arr[mid] == target) {
                return mid;
            } else if (arr[mid] < target) {
                low = mid + 1;
            } else {
                high = mid - 1;
            }
        }

        return -1;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

哈希查找

哈希查找(Hash Search)是一种基于哈希表进行查找的查找算法。在哈希查找中,通过将数据存储在哈希表中,可以实现常数时间复杂度的查找操作(O(1))。哈希查找适用于需要快速查找元素的场景,但要求内存空间较大。
java中可以使用HashMap来进行实现

二叉查找树

二叉查找树(Binary Search Tree,BST)是一种基于二叉树的数据结构,它具有以下性质:
对于任意节点,其左子树中的所有节点的值都小于该节点的值。
对于任意节点,其右子树中的所有节点的值都大于该节点的值。
左子树和右子树也分别是二叉查找树。
从根节点开始,将要查找的值与当前节点的值进行比较。如果相等,则找到了目标节点;如果小于当前节点的值,则在左子树中继续查找;如果大于当前节点的值,则在右子树中继续查找。直到找到目标节点或遍历到叶子节点为止。

平衡二叉查找树

平衡二叉查找树(Balanced Binary Search Tree),也称为自平衡二叉查找树,是一种在插入和删除操作后能自动保持平衡的二叉查找树。平衡二叉查找树的目的是为了提高插入、删除和查找等操作的效率。
常见的平衡二叉查找树有红黑树(Red-Black Tree)、AVL树以及B树等。

B树

B树是一种多路搜索树,常被用于数据库和文件系统中,其具有良好的平衡性和高效的查找特性。B树的每个节点可以包含多个子节点,相比于二叉树,B树可以更好地利用磁盘块的特性,减少磁盘 I/O 操作,从而提高数据的读取速度。
B树的基本特点如下:

  • 每个节点最多有m个子节点。
  • 除了根节点外,每个非叶子节点至少包含ceil(m/2)个子节点。
  • 每个非叶子节点中包含的关键字个数比子节点少一个。
  • 所有叶子节点位于同一层次,并且不包含任何信息。
  • B树的查找操作通过从根节点开始,根据节点上的关键字信息决定前往哪个子节点继续查找,直到找到目标数据或者确定目标数据不存在为止。

跳表

跳表(Skip List)是一种支持快速查找的数据结构,它在有序链表的基础上增加了多层索引。跳表允许快速地进行插入、删除和查找操作,其时间复杂度为O(log n),与平衡二叉查找树类似。
跳表的基本思想是通过建立多级索引来加速搜索。跳表中的节点按照层级划分,最底层是原始链表,每个节点都有一个指向下一个节点的指针。除了原始链表外,还会建立其他层级的索引链表,其中每个节点只保留一个指向下一个节点的指针,该节点可能是下一层级的同一位置的节点,也可能是下一层级的前一个节点。
通过使用多级索引,跳表可以跳过部分节点,从而减少搜索的时间复杂度。在跳表中,每个节点的上方节点都是下方节点的快速访问入口点。索引层数越高,跨越的节点数量就越多,查询的速度也就越快。

斐波那契查找

斐波那契查找(Fibonacci Search)是一种利用斐波那契数列来进行搜索的算法,它在有序数组中进行查找操作。与二分查找不同,斐波那契查找使用黄金分割比例来确定要搜索的位置,以提高搜索效率。

斐波那契查找的基本思想如下:
首先,需要找到一个斐波那契数列中大于或等于数组长度的最小数。这个数被称为斐波那契数列的索引。
根据斐波那契数列中的两个相邻数相除的比例来划分数组,将数组分成两部分:前半部分的长度为前一个斐波那契数,后半部分的长度为前两个斐波那契数。
比较要查找的元素与当前位置的元素大小:
如果相等,返回当前位置;
如果要查找的元素大于当前位置的元素,说明在后半部分继续查找;
如果要查找的元素小于当前位置的元素,说明在前半部分继续查找。
重复以上步骤,直到找到要查找的元素或者遍历完整个数组。
斐波那契查找的时间复杂度为O(log n),比起传统的二分查找,在某些情况下具有更好的性能表现。

块查找

块查找(Block Search)是一种在静态有序表中进行查找的算法,它将表分成若干块,每个块中的元素也是有序的。块查找的基本思想是先确定待查找元素所在的块,然后在该块内进行顺序查找。

块查找的步骤如下:

  1. 将整个有序表分成若干块,每个块包含相等数量的元素,且每个块内的元素是有序排列的。
  2. 确定待查找元素所在的块,可以根据元素的大小和每个块的最大值、最小值来确定。
  3. 在确定的块内进行顺序查找,直到找到目标元素或者确定目标元素不在该块内。

块查找的优点是减少了比较次数,特别适用于对静态表进行查找操作。然而,在动态表中,由于元素的插入和删除可能导致块的重新划分,块查找的效率会降低。

Trie树查找

Trie树(也称为字典树或前缀树)是一种用于高效存储和查找字符串的数据结构。它通过将字符串拆分为字符,并将每个字符作为节点存储在树中,从而实现快速的字符串查找操作。

Trie树的基本思想是将字符串按照字符的顺序构建成一棵树。树的根节点表示空字符串,每个节点包含一个字符和指向子节点的指针。从根节点到叶子节点的路径表示一个完整的字符串。

Trie树的查找操作根据待查找的字符串逐个字符地在树中进行遍历,直到遍历完所有字符或者找不到匹配的节点为止。具体的查找过程如下:

  1. 从根节点开始,将待查找的字符串的第一个字符与根节点的子节点进行比较。
  2. 如果有相等的子节点,则继续比较下一个字符,进入该子节点继续查找。
  3. 如果没有相等的子节点,表示待查找的字符串不存在于Trie树中,查找失败。
  4. 重复以上步骤,直到查找完所有字符,如果最后一个字符所在的节点为叶子节点,则表示待查找的字符串存在于Trie树中,查找成功;否则,查找失败。

Trie树的时间复杂度取决于待查找字符串的长度,即为O(m),其中m是待查找字符串的长度。因此,Trie树在处理大量字符串查找时具有较高的效率。

后缀树查找

后缀树(Suffix Tree)是一种数据结构,用于存储一个字符串的所有后缀,并支持高效的字符串匹配和查找操作。后缀树可以在O(m)的时间复杂度内完成字符串匹配,其中m是待匹配字符串的长度。

后缀树的构建过程会将原始字符串的所有后缀添加到树中,形成一个压缩的树结构,其中每条从根节点到叶子节点的路径表示一个后缀。通过这种方式,后缀树可以快速地进行子串匹配和查找操作。

后缀树的基本思想是将所有后缀构建成一棵树,其中包含如下特点:

  1. 每个边都对应一个字符串片段。
  2. 每个子串都唯一地对应一条从根节点到叶子节点的路径。
  3. 每个叶子节点对应一个后缀。

后缀树的构建可以使用不同的算法,如Ukkonen算法或McCreight算法。在构建完后缀树后,可以利用后缀树进行各种字符串匹配和查找操作,例如查找某个模式串是否在原始字符串中出现。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/人工智能uu/article/detail/869903
推荐阅读
相关标签
  

闽ICP备14008679号