JAVA面试问题大全

188 阅读1小时+

一、Spring SE

1. 重写(override) VS 重载(overload)

区别点重载重写
发生范围同一个类子类
参数列表必须修改不可修改
返回类型可修改子类方法返回值要小于等于父类
异常可修改子类异常应小于等于父类
访问修饰符可修改小于等于父类
发生阶段编译器运行期
方法的重写要遵循“两同两小一大”:

-   “两同”即方法名相同、形参列表相同;
-   “两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;
-   “一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等。

 2. static

①、static成员变量

java中可以通过static关键字修饰变量达到全局变量的效果。static修饰的变量(静态变量)属于类,在类第一次通过类加载器到jvm时被分配内存空间。

②、static成员方法

static修饰的方法属于类方法,不需要创建对象就可以调用。static方法中不能使用this和super等关键字,不能调用非static方法,只能访问所属类的静态成员变量和静态方法。

③、static 代码块

JVM在加载类时会执行static代码块,static代码块常用于初始化静态变量,static代码只会在类被加载时执行且执行一次。

④、static内部类

static内部类可以不依赖外部类实例对象而被实例化,而内部类需要在外部类实例化后才能被实例化。

静态内部类不能访问外部类的普通变量,只能访问外部类的静态成员变量和静态方法。

3. final关键字有什么用

①、当 final 修饰一个类时,表明这个类不能被继承。比如,String 类、Integer 类和其他包装类都是用 final 修饰的。

②、当 final 修饰一个方法时,表明这个方法不能被重写(Override)。也就是说,如果一个类继承了某个类,并且想要改变父类中被 final 修饰的方法的行为,是不被允许的。

③、当 final 修饰一个变量时,表明这个变量的值一旦被初始化就不能被修改。

如果是基本数据类型的变量,其数值一旦在初始化之后就不能更改;如果是引用类型的变量,在对其初始化之后就不能再让其指向另一个对象

4. stream map和flatmap有什么区别

  1. 核心区别

    • map 用于 一对一 的转换。它接受一个 Function<T, R>,为流中的每个元素应用这个函数,生成一个新的元素
    • flatMap 用于 一对多 的转换并合并。它接受一个 Function<T, Stream<R>>,为每个元素生成一个,然后将所有这些流扁平化成一个单一的流。
  2. 关键比喻

    • 把 map 想象成工厂里的加工机器,每个产品进去,出来一个加工后的新产品。
    • 把 flatMap 想象成一个拆包和合并的流水线,你投入一个个箱子(每个箱子内含多个物品),流水线会拆开所有箱子,把所有物品放到同一条传送带上。
  3. 经典应用场景

    • map:转换数据类型(如 String -> Integer)、提取对象字段、进行简单计算。

    • flatMap

      • 处理嵌套集合(如 List<List<T>> 转 List<T>),这是最常见的场景。
      • 拆分字符串(如将句子流拆分成单词流)。
      • 过滤并展开(如先过滤掉无效数据,再将有效数据展开)。

5. 抽象类和普通类的区别

特性抽象类(Abstract Class)普通类(Concrete Class)
实例化不能被实例化(不能直接创建对象)。可以被实例化(可以直接创建对象)。
方法实现可以包含抽象方法(没有方法体)和具体方法。所有方法都必须有实现(不能有抽象方法)。
继承必须通过子类继承并实现抽象方法。可以直接使用,也可以被继承。
构造函数可以有构造函数,但不能直接调用。可以有构造函数,用于实例化对象。
设计目的用于定义模板或部分实现,供子类扩展。用于创建具体的对象和实现完整功能。

6. 接口和抽象类的区别

特性接口(Interface)抽象类(Abstract Class)
实例化不能被实例化。不能被实例化。
方法实现Java 8 之前只能有抽象方法;Java 8 开始可以有默认方法(default)和静态方法。可以包含抽象方法和具体方法。
字段只能有常量(默认是 public static final)。可以有普通字段和常量。
继承支持多继承(一个类可以实现多个接口)。单继承(一个类只能继承一个抽象类)。
构造函数不能有构造函数。可以有构造函数。
设计目的定义行为规范(契约),强调“能做什么”。定义模板或部分实现,强调“是什么”。

二、集合框架

1. HashMap vs HashTable

HashMapHashTable
线程安全HashMap中的方法在默认情况下是非同步的HashTable 中的方法是同步的,是线程安全
继承关系HashMap继承的抽象类AbstractMap实现了Map接口HashTable是基于陈旧的Dictionary类继承来的
允不允许null值HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为nullHashTable中,key和value都不允许出现null值,否则会抛出NullPointerException异常
默认初始容量和扩容机制HashMap中hash数组的默认大小是16,而且一定是2的指数HashTable中的hash数组初始大小是11,增加的方式是 old*2+1
哈希值的使用不同HashMap重新计算hash值HashTable直接使用对象的hashCode
遍历方式的内部实现上不同Hashtable、HashMap都使用了 Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式HashMap 实现 Iterator,支持fast-fail,Hashtable的 Iterator 遍历支持fast-fail,用 Enumeration 不支持 fast-fail

2. HashMap 1.7 和 HashMap 1.8 的区别?

不同点hashMap 1.7hashMap 1.8
数据结构数组+链表数组+链表+红黑树
插入数据的方式头插法尾插法
hash 值计算方式9次扰动处理(4次位运算+5次异或)2次扰动处理(1次位运算+1次异或)
扩容策略插入前扩容插入后扩容

3. ConcurrentHashMap 1.7 vs 1.8

不同点concurrentHashMap 1.7concurrentHashMap 1.8
锁粒度基于segment基于entry节点
reentrantLocksynchronized
底层结构Segment + HashEntry + UnsafeSynchronized + CAS + Node + Unsafe

4. 为什么 HashMap 链表转红黑树的阈值为 8 呢? 

红黑树节点的大小大概是普通节点大小的两倍,所以转红黑树,牺牲了空间换时间,更多的是一种兜底的策略,保证极端情况下的查找效率。

阈值为什么要选 8 呢?和统计学有关。理想情况下,使用随机哈希码,链表里的节点符合泊松分布,出现节点个数的概率是递减的,节点个数为 8 的情况,发生概率仅为0.00000006

至于红黑树转回链表的阈值为什么是 6,而不是 8?是因为如果这个阈值也设置成 8,假如发生碰撞,节点增减刚好在 8 附近,会发生链表和红黑树的不断转换,导致资源浪费。

5. ArrayList 的扩容机制

ArrayList是动态数组,因为它的底层是通过数组来实现的,当往 ArrayList 中添加元素时,会先检查是否需要扩容,如果当前容量+1 超过数组长度,就会进行扩容。

扩容后的新数组长度是原来的 1.5 倍,然后再把原数组的值拷贝到新数组中。

6. HashMap默认加载因子是多少?为什么是 0.75,不是 0.6 或者 0.8

这跟统计学里的一个很重要的原理——泊松分布有关。0.75是在“哈希冲突”与“空间利用率”取舍时候的结果

7. ArrayList VS LinkedList

比较维度ArrayListLinkedList
数据结构基于数组实现基于链表实现
用途ArrayList 更利于查找,get(int index)时间复杂度O(1);get(E element)时间复杂度O(n)LinkedList 更利于增删,get(int index)时间复杂度O(n);get(E element)时间复杂度O(n)
增删性能尾部增删时间复杂度O(1),涉及扩容时为O(n);中间增删可能涉及元素移动,时间复杂度O(n)头部、尾部增删时间复杂度O(1);中间增删需遍历链表,时间复杂度O(n);增删主要是改变引用,效率相对高
是否支持随机访问支持,实现了RandomAccess接口,可通过下标直接获取元素不支持,未实现RandomAccess接口,无法根据下标直接获取元素
内存占用内存占用紧凑,扩容时重新分配为原空间1.5倍,存在一定空间浪费每个节点有指向前置和后置节点的引用,单个节点占用内存稍大
使用场景随机访问频繁;读取操作远多于写入操作;末尾添加元素场景频繁插入和删除;顺序访问多于随机访问;实现队列(FIFO)和栈(LIFO)场景

8. 哪些集合是线程安全的,怎么保证安全的;

线程安全的集合包括VectorHashtableConcurrentHashMap,通过同步机制保证线程安全。

如何保证

  • 使用SynchronizedList类:使用Collections.synchronizedListCollections.synchronizedMap将非线程安全的集合转换为线程安全的集合。
  • 加锁synchronized

三、线程

1. 说说线程有几种创建方式?

①、创建一个类继承 Thread 类,并重写 run 方法。

②、创建一个类实现 Runnable 接口,并重写 run 方法。

③、实现 Callable 接口,重写 call 方法,这种方式可以通过 FutureTask 获取任务执行的返回值。

④、创建线程池

2. join 和 yield 的用法

  • yield 是使当前线程让出时间片,让cup给同等时间片的线程使用,若无其他线程yield不起作用

  • join 让”主线程”等待“子线程”结束之后才能继续运行

3. notify和notifyall的区别?

调用wait的线程的唤醒,一般通过notify和notifyAll。notify只释放一个等待池中的线程,优先级的高的机会大些。而notifyAll则释放等待池中所有的线程。

4. wait()和 sleep 的区别

sleepwait
是否释放锁NOYES
作用用于线程间交互/通信同于暂停执行
苏醒方式线程会自动苏醒自动苏醒或需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法
方法来源Thread 类的静态本地方法wait() 是 Object 类的本地方法
针对对象针对线程针对对象

5. synchronized [ˈsɪŋkrəˌnaɪz]

主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行

1) 原理

Java 对象底层都会关联一个 monitor,使用 synchronized 时 JVM 会根据使用环境找到对象的 monitor,根据 monitor 的状态进行加解锁的判断。如果成功加锁就成为该 monitor 的唯一持有者,monitor 在被释放前不能再被其他线程获取。

synchronized在JVM编译后会产生monitorenter 和 monitorexit 这两个字节码指令,获取和释放 monitor。这两个字节码指令都需要一个引用类型的参数指明要锁定和解锁的对象,对于同步普通方法,锁是当前实例对象;对于静态同步方法,锁是当前类的 Class 对象;对于同步方法块,锁是 synchronized 括号里的对象。

执行 monitorenter 指令时,首先尝试获取对象锁。如果这个对象没有被锁定,或当前线程已经持有锁,就把锁的计数器加 1,执行 monitorexit 指令时会将锁计数器减 1。一旦计数器为 0 锁随即就被释放。

2) synchronized 和 volatile 有什么区别

volatile 关键字用于修饰变量,确保该变量的更新操作对所有线程是可见的,即一旦某个线程修改了 volatile 变量,其他线程会立即看到最新的值。

synchronized 关键字用于修饰方法或代码块,确保同一时刻只有一个线程能够执行该方法或代码块,从而实现互斥访问。

3) 如何使用:

  • 修饰实例方法:锁当前对象实例
  • 修饰静态方法:锁当前类
  • 修饰代码块:锁指定对象/类

6. 悲观锁和乐观锁

悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程synchronized、ReentrantLock高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。并且,悲观锁还可能会存在死锁问题,影响代码的正常运行
乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。AtomicInteger、LongAdder高并发的场景下,乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,也不会有死锁的问题,在性能上往往会更胜一筹。但是,如果冲突频繁发生(写占比非常多的情况),会频繁失败和重试,这样同样会非常影响性能,导致 CPU 飙升。

7. 线程池工作流程

image.png

8. 线程池五大参数

思维导图.png

9. 线程的生命周期

线程的生命周期可以分为五个主要阶段:新建、可运行、运行中、阻塞/等待、和终止

image.png

10. 线程池的阻塞队列

①、ArrayBlockingQueue:一个有界的先进先出的阻塞队列,底层是一个数组,适合固定大小的线程池。

②、LinkedBlockingQueue:底层数据结构是链表,如果不指定大小,默认大小是 Integer.MAX_VALUE,相当于一个无界队列。

③、PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。任务按照其自然顺序或通过构造器给定的 Comparator 来排序。

适用于需要按照给定优先级处理任务的场景,比如优先处理紧急任务。

④、DelayQueue:类似于 PriorityBlockingQueue,由二叉堆实现的无界优先级阻塞队列。

⑤、SynchronousQueue:实际上它不是一个真正的队列,因为没有容量。每个插入操作必须等待另一个线程的移除操作,同样任何一个移除操作都必须等待另一个线程的插入操作。

Executors.newCachedThreadPool() 就使用了 SynchronousQueue,这个线程池会根据需要创建新线程,如果有空闲线程则会重复使用,线程空闲 60 秒后会被回收。

11. 线程池异常怎么处理

image.png

12. 线程池类型:

  • newCachedThreadPool 可缓存线程池,可设置最小线程数和最大线程数,线程空闲1分钟后自动销毁。
  • newFixedThreadPool 指定工作线程数量线程池。
  • newSingleThreadExecutor 单线程Executor。
  • newScheduleThreadPool 支持定时任务的指定工作线程数量线程池。
  • newSingleThreadScheduledExecutor 支持定时任务的单线程Executor

推荐阅读:能说一下四种常见线程池的原理吗

四、JVM

1. 强引用、软引用、弱引用、虚引用

  • 强引用:必不可少的生活用品 当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

  • 软引用:可有可无的生活用品如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。[软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。]

  • 弱引用:可有可无的生活用品在垃圾回收器线程扫描弱引用所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

  • 虚引用:形同虚设虚引用主要用来跟踪对象被垃圾回收的活动。

2. 垃圾收集有哪些算法,各自的特点

什么是垃圾回收:垃圾回收就是对内存堆中已经死亡的或者长时间没有使用的对象进行清除或回收。

  • 标记-清除(Mark-and-Sweep)算法

标记清除算法,首先会标记出所有需要回收的对象,然后统一回收这些被标记的对象。优点是实现简单,缺点是容易产生内存碎片

  • 复制(Copying)收集算法

复制算法可以解决标记-清除算法的内存碎片问题,因为它将内存空间划分为两块,每次只使用其中一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后清理掉这一块。缺点是浪费了一半的内存空间

  • 标记-整理(Mark-and-Compact)算法

标记-整理算法是标记-清除复制算法的升级版,它不再划分内存空间,而是将存活的对象向内存的一端移动,然后清理边界以外的内存。缺点是移动对象的成本比较高。

  • 分代收集算法

分代收集算法是目前主流的垃圾收集算法,它根据对象存活周期的不同将内存划分为几块,一般分为新生代和老年代。新生代用复制算法,因为大部分对象生命周期短。老年代用标记-整理算法,因为对象存活率较高。

3. 如何判断对象是否死亡

  • 引用计数法:

    • 每当有一个地方引用它,计数器就加 1;
    • 当引用失效,计数器就减 1;
    • 任何时候计数器为 0 的对象就是不可能再被使用的。

    这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间循环引用的问题。

  • 可达性分析算法:这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。

4. 类加载机制

JVM 的操作对象是 Class 文件,JVM 把 Class 文件中描述类的数据结构加载到内存中,并对数据进行校验、解析和初始化,最终形成可以被 JVM 直接使用的类型,这个过程被称为类加载机制。

其中最重要的三个概念就是:类加载器、类加载过程和类加载器的双亲委派模型。

  • 类加载器:负责加载类文件,将类文件加载到内存中,生成 Class 对象。
  • 类加载过程:加载、验证、准备、解析和初始化。
  • 双亲委派模型:当一个类加载器收到类加载请求时,它首先不会自己去尝试加载这个类,而是把请求委派给父类加载器去完成,依次递归,直到最顶层的类加载器,如果父类加载器无法完成加载请求,子类加载器才会尝试自己去加载。

5. 类加载器

类加载器(ClassLoader)用于动态加载 Java 类到 Java 虚拟机中。主要有四种类加载器:

①、启动类加载器(Bootstrap ClassLoader)负责加载 JVM 的核心类库,如 rt.jar 和其他核心库位于JAVA_HOME/jre/lib目录下的类。

②、扩展类加载器(Extension ClassLoader):由sun.misc.Launcher$ExtClassLoader(或其它类似实现)实现。负责加载JAVA_HOME/jre/lib/ext目录下,或者由系统属性java.ext.dirs指定位置的类库。

③、应用程序类加载器(Application ClassLoader):由sun.misc.Launcher$AppClassLoader(或其它类似实现)实现。

负责加载系统类路径(classpath)上的类库,通常是我们在开发 Java 应用程序时的主要类加载器。

我们编写的任何类都是由应用程序类加载器加载的,除非显式使用自定义类加载器。

④、用户自定义类加载器 (User-Defined ClassLoader),我们可以通过继承java.lang.ClassLoader类来创建自己的类加载器。

6. 类的生命周期

一个类从被加载到虚拟机内存中开始,到从内存中卸载,整个生命周期需要经过七个阶段:加载 (Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化 (Initialization)、使用(Using)和卸载(Unloading)。

7. 双亲委派模型

一个类加载器收到类加载请求之后,首先判断当前类是否被加载过。已经被加载的类会直接返回,如果没有被加载,首先将类加载请求转发给父类加载器,一直转发到启动类加载器,只有当父类加载器无法完成时才尝试自己加载。

加载类顺序:BootstrapClassLoader -> ExtensionClassLoader -> AppClassLoader -> CustomClassLoader

检查类是否加载顺序: CustomClassLoader -> AppClassLoader -> ExtensionClassLoader -> BootstrapClassLoader

双亲委派机制的优点

  • 避免类的重复加载。相同的类被不同的类加载器加载会产生不同的类,双亲委派保证了Java程序的稳定运行。
  • 保证核心API不被修改。
  • 如何破坏双亲委派机制
  • 重载loadClass()方法,即自定义类加载器

8. Java 对象的创建过程

①、类加载检查:检查new执行的参数是否能在常量池中定位到这个类的符号引用,并检查这个符号引用代表的类是否被加载过、解析过和初始化。如果没有,那必须先执行相应的类加载过程。

②、分配内存:虚拟机将为新生对象分配内存。分配方式有 “指针碰撞” 和 “空闲列表” 两种

③、初始化零值:虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)

④、设置对象头:虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。

⑤、执行init方法:在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,<init> 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 <init> 方法,把对象按照程序员的意愿进行初始化

9. 内存溢出和内存泄漏及其场景

内存泄漏内存溢出 OOM
是指程序在运行过程中由于未能正确释放已分配的内存,导致内存无法被重用,从而引发内存耗尽等问题也就是 Out of Memory,是指当程序请求分配内存时,由于没有足够的内存空间满足其需求,从而触发的错误
由于 ThreadLocal 没有及时清理导致出现了内存泄漏问题是因为创建了大量的对象,且长时间无法被垃圾收集器回收,导致堆内存耗尽。

10. 内存区域

①、程序计数器:为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,并且不能互相干扰,否则就会影响到程序的正常执行次序。也就是说,我们要求程序计数器是线程私有的。

②、Java 虚拟机栈:

Java 虚拟机栈的特点如下:
i. 线程私有: 每个线程都有自己的 JVM 栈,线程之间的栈是不共享的。
ii. 栈溢出: 如果栈的深度超过了 JVM 栈所允许的深度,将会抛出 StackOverflowError

③、本地方法栈(Native Method Stack)

④、堆:在 JVM 中,堆是可供各条线程共享的运行时内存区域,也是供所有类实例和数据对象分配内存的区域。

⑤、方法区:JDK 8 开始,使用元空间取代了永久代。方法区是 JVM 中的一个逻辑区域,用于存储类的结构信息,包括类的定义、方法的定义、字段的定义以及字节码指令。不同的是,元空间不再是 JVM 内存的一部分,而是通过本地内存(Native Memory)来实现的。

其中方法区是线程共享的,虚拟机栈本地方法栈程序计数器是线程私有的

10.1 堆和栈的区别是什么

堆属于线程共享的内存区域,几乎所有 new 出来的对象都会堆上分配,生命周期不由单个方法调用所决定,可以在方法调用结束后继续存在,直到不再被任何变量引用,最后被垃圾收集器回收。

栈属于线程私有的内存区域,主要存储局部变量、方法参数、对象引用等,通常随着方法调用的结束而自动释放,不需要垃圾收集器处理。

五、Spring

1. 常用设计模型

①、单例模式:

  • a. 饿汉式
  • b. 懒汉式

②、工厂模式:

③、建造者模式:

④、策略模式:

⑤、抽象工厂模式

⑥、适配器模式

1.1 懒汉模式下如何解决线程安全性问题

(1) 暴力法:增加synchronized

(2) volatile + synchronized

(3) 使用枚举

2. Spring Bean 生命周期

  • 实例化:Spring 首先使用构造方法或者工厂方法创建一个 Bean 的实例。在这个阶段,Bean 只是一个空的 Java 对象,还未设置任何属性。
  • 属性赋值:Spring 将配置文件中的属性值或依赖的 Bean 注入到该 Bean 中。这个过程称为依赖注入,确保 Bean 所需的所有依赖都被注入。
  • 初始化:Spring 调用 afterPropertiesSet 方法,或通过配置文件指定的 init-method 方法,完成初始化。
  • 使用中:Bean 准备好可以使用了。
  • 销毁:在容器关闭时,Spring 会调用 destroy 方法,完成 Bean 的清理工作。

3. @SpringBootApplication 注解

大概可以把 @SpringBootApplication看作是 @Configuration@EnableAutoConfiguration@ComponentScan 注解的集合。根据 SpringBoot 官网,这三个注解的作用分别是:

  • @EnableAutoConfiguration:启用 SpringBoot 的自动配置机制
  • @Configuration:允许在上下文中注册额外的 bean 或导入其他配置类
  • @ComponentScan:扫描被@Component (@Service,@Controller)注解的 bean,注解默认会扫描启动类所在的包下所有的类 ,可以自定义不扫描某些 bean。如下图所示,容器中将排除TypeExcludeFilterAutoConfigurationExcludeFilter

4. @EnableAutoConfiguration 自动装配

自动装配核心功能的实现实际是通过AutoConfigurationImportSelector加载自动装配类。

AutoConfigurationImportSelector实现了ImportSelector接口,该接口的作用是收集需要导入的配置类,配合 @Import() 将相应的类导入到 Spring 容器中。

获取注入类的方法是 selectImports(),它实际调用的是getAutoConfigurationEntry(),这个方法是获取自动装配类的关键。

5. 事务隔离级别以及各自解决的问题

①、ISOLATION_DEFAULT:使用数据库默认的隔离级别,MySQL 默认的是可重复读,Oracle 默认的读已提交。

②、ISOLATION_READ_UNCOMMITTED:读未提交,允许事务读取未被其他事务提交的更改。这是隔离级别最低的设置,可能会导致“脏读”问题。

③、ISOLATION_READ_COMMITTED:读已提交,确保事务只能读取已经被其他事务提交的更改。这可以防止“脏读”,但仍然可能发生不可重复读和幻读问题。

④、ISOLATION_REPEATABLE_READ:可重复读,确保事务可以多次从一个字段中读取相同的值,即在这个事务内,其他事务无法更改这个字段,从而避免了“不可重复读”,但仍可能发生“幻读”问题。

⑤、ISOLATION_SERIALIZABLE:串行化,这是最高的隔离级别,它完全隔离了事务,确保事务序列化执行,以此来避免“脏读”、“不可重复读”和“幻读”问题,但性能影响也最大。

a. 幻读、胀读分别是什么

  • 幻读是指在一个事务中,前后两次查询同一范围的数据时,由于其他事务插入或删除了符合该范围的数据,导致两次查询结果不一致。

  • 脏读是指一个事务读取了另一个未提交事务的修改数据。如果未提交事务回滚,则读取到的数据是无效的。

6. 事务失效场景

image.png

7. 一个事务方法调用另一个事务方法,事务是否会生效

事务是否生效取决于事务的传播行为 1. REQUIRED(默认)

  • 如果当前存在事务,则加入该事务;如果不存在事务,则创建一个新事务。

  • 示例:

    @Transactional(propagation = Propagation.REQUIRED)
    public void methodA() {
        methodB(); // 加入 methodA 的事务
    }
    
    @Transactional(propagation = Propagation.REQUIRED)
    public void methodB() {
        // 业务逻辑
    }
    

2. REQUIRES_NEW

  • 无论当前是否存在事务,都会创建一个新事务。

  • 示例:

    @Transactional(propagation = Propagation.REQUIRED)
    public void methodA() {
        methodB(); // 创建一个新事务
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void methodB() {
        // 业务逻辑
    }
    

3. NESTED

  • 如果当前存在事务,则在嵌套事务中执行;如果不存在事务,则创建一个新事务。
  • 嵌套事务可以独立回滚,而不影响外部事务。

4. SUPPORTS

  • 如果当前存在事务,则加入该事务;如果不存在事务,则以非事务方式执行。

5. NOT_SUPPORTED

  • 以非事务方式执行,如果当前存在事务,则挂起该事务。

6. MANDATORY

  • 如果当前存在事务,则加入该事务;如果不存在事务,则抛出异常。

7. NEVER

  • 以非事务方式执行,如果当前存在事务,则抛出异常。

8. 事务机制使用的什么设计模式

1. 模板方法模式(Template Method Pattern)

  • Spring 的事务管理通过 PlatformTransactionManager 和 TransactionTemplate 实现。
  • TransactionTemplate 定义了事务的执行流程(如开启事务、提交事务、回滚事务),而具体的数据库操作由用户实现。

2. 代理模式(Proxy Pattern)

  • Spring 通过 AOP(面向切面编程)实现声明式事务管理。
  • 使用动态代理(JDK 动态代理或 CGLIB 代理)在目标方法前后添加事务管理逻辑。

3. 工厂模式(Factory Pattern)

PlatformTransactionManager 是一个工厂接口,具体的实现类(如 DataSourceTransactionManagerHibernateTransactionManager)负责创建和管理事务。

9. controller层的异常如何捕获

使用 @ControllerAdvice + @ExceptionHandler 这两个注解 。

@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {

    @ExceptionHandler(BaseException.class)
    public ResponseEntity<?> handleAppException(BaseException ex, HttpServletRequest request) {
      //......
    }

    @ExceptionHandler(value = ResourceNotFoundException.class)
    public ResponseEntity<ErrorReponse> handleResourceNotFoundException(ResourceNotFoundException ex, HttpServletRequest request) {
      //......
    }
}

10. aop的使用及其原理

AOP用于日志记录、权限校验和事务管理,解决了代码重复和耦合度高的问题。

SpringAOP实现原理其实很简单,就是通过动态代理实现的。如果我们为Spring的某个bean配置了切面,那么Spring在创建这个bean的时候,实际上创建的是这个bean的一个代理对象,我们后续对bean中方法的调用,实际上调用的是代理类重写的代理方法。而SpringAOP使用了两种动态代理,分别是JDK的动态代理,以及CGLib的动态代理(默认)

切面: 拦截器类,其中会定义切点以及通知

切点: 具体拦截的某个业务点。

通知: 切面当中的方法,声明通知方法在目标业务层的执行位置,通知类型如下:

前置通知:@Before 在目标业务方法执行之前执行
后置通知:@After 在目标业务方法执行之后执行
返回通知:@AfterReturning 在目标业务方法返回结果之后执行
异常通知:@AfterThrowing 在目标业务方法抛出异常之后
环绕通知:@Around 功能强大,可代替以上四种通知,还可以控制目标业务方法是否执行以及何时执行

六、Redis

1. Redis 有哪些数据类型,分别使用什么场景

数据类型描述使用场景
String(字符串)最基本的数据类型,可以存储字符串、整数或浮点数。- 缓存数据(如用户信息、配置)。
- 计数器(如文章阅读量、点赞数)。
- 分布式锁(通过 SETNX 实现)。
Hash(哈希)类似于 Java 的 Map,存储字段和值的映射。- 存储对象(如用户信息、商品信息)。
- 适合存储需要频繁修改部分字段的场景。
List(列表)有序的字符串列表,支持从头部或尾部插入和删除元素。- 消息队列(如任务队列)。
- 最新消息列表(如最新评论、最新文章)。
Set(集合)无序且不重复的字符串集合。- 去重(如用户标签、IP 黑名单)。
- 交集、并集、差集运算(如共同好友、推荐系统)。
Sorted Set(有序集合)类似于 Set,但每个元素关联一个分数(score),可以根据分数排序。- 排行榜(如热门文章、用户积分)。
- 范围查询(如按时间范围查询数据)。
Bitmap(位图)基于字符串的位操作,可以看作是一个由二进制位组成的数组。- 用户签到、活跃度统计。
- 布隆过滤器(用于去重)。
HyperLogLog用于基数统计(估算集合中不重复元素的数量)。- 统计 UV(独立访客数)。
- 大数据量下的去重统计。
GEO(地理空间)存储地理位置信息,支持地理位置查询和计算。- 附近的人、附近的商家。
- 地理位置范围查询。
Stream类似于消息队列,支持多消费者组和消息持久化。- 消息队列(如日志收集、事件流处理)。
- 实时数据处理。

2. Redis 如何实现高可用

Redis 实现高可用主要有三种方式:主从复制、哨兵模式,以及 Redis 集群。

1)主从复制

将从前的一台 Redis 服务器,同步数据到多台从 Redis 服务器上,即一主多从的模式,这个跟 MySQL 主从复制的原理一样

2)哨兵模式

使用 Redis 主从服务的时候,会有一个问题,就是当 Redis 的主从服务器出现故障宕机时,需要手动进行恢复,为了解决这个问题,Redis 增加了哨兵模式(因为哨兵模式做到了可以监控主从服务器,并且提供自动容灾恢复的功能)。

3)Redis Cluster(集群)

Redis Cluster 是一种分布式去中心化的运行模式,是在 Redis 3.0 版本中推出的 Redis 集群方案,它将数据分布在不同的服务器上,以此来降低系统对单主节点的依赖,从而提高 Redis 服务的读写性能。

3. 使用哨兵模式在数据上有副本数据做保证,在可用性上又有哨兵监控,一旦 master 宕机会选举 salve 节点为 master 节点,这种已经满足了我们的生产环境需要,那为什么还需要使用集群模式呢

哨兵模式归根 节点还是主从模式,在主从模式下我们可以通过增加 salve 节点来扩展读并发能力,但是没办法扩展写能力和存储能力,存储能力只能是 master 节点能够承载的上限。所以为了扩展写能力和存储能力,我们就需要引入集群模式。

4. 缓存雪崩、缓存穿透、缓存击穿

缓存穿透 缓存击穿 缓存雪崩
大量请求的 key 是不合理的,根本不存在于缓存中,也不存在于数据库中 。这就导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。 请求的 key 对应的是 热点数据 ,该数据 存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期) 。这就可能会导致瞬时大量的请求直接打到了数据库上,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。 缓存在同一时间大面积的失效,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力
  • 做好参数校验;
  • 缓存无效 key;
  • 布隆过滤器;
  • 接口限流;
  • 永不过期(不推荐):设置热点数据永不过期或者过期时间比较长。 - 提前预热(推荐):针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期
  • 加锁(看情况):在缓存失效后,通过设置互斥锁确保只有一个请求去查询数据库并更新缓存
  • 针对 Redis 服务不可用的情况:
    1. Redis 集群:采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。
    2. 多级缓存:设置多级缓存,例如本地缓存+Redis 缓存的二级缓存组合,当 Redis 缓存出现问题时,还可以从本地缓存中获取到部分数据。

    针对大量缓存同时失效的情况:

    1. 设置随机失效时间(可选):为缓存设置随机的失效时间,例如在固定过期时间的基础上加上一个随机值,这样可以避免大量缓存同时到期,从而减少缓存雪崩的风险。
    2. 提前预热(推荐):针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。
    3. 持久缓存策略(看情况):虽然一般不推荐设置缓存永不过期,但对于某些关键性和变化不频繁的数据,可以考虑这种策略。

    5. Redis 过期 key 删除策略

    • 惰性删除:只会在取出/查询 key 的时候才对数据进行过期检查。这种方式对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。

    • 定期删除:周期性地随机从设置了过期时间的 key 中抽查一批,然后逐个检查这些 key 是否过期,过期就删除 key。相比于惰性删除,定期删除对内存更友好,对 CPU 不太友好。

    • 延迟队列:把设置过期时间的 key 放到一个延迟队列里,到期之后就删除 key。这种方式可以保证每个过期 key 都能被删除,但维护延迟队列太麻烦,队列本身也要占用资源。

    • 定时删除:每个设置了过期时间的 key 都会在设置的时间到达时立即被删除。这种方法可以确保内存中不会有过期的键,但是它对 CPU 的压力最大,因为它需要为每个键都设置一个定时器

    => Redis 采用的是 定期删除+惰性/懒汉式删除 结合的策略

    6. Redis 为什么早期选择单线程,而且那么快

    选择单线程个人觉得主要是使用简单,不存在锁竞争,可以在无锁的情况下完成所有操作,不存在死锁和线程切换带来的性能和时间上的开销,但同时单线程也不能完全发挥出多核 CPU 的性能。

    至于为什么单线程那么快我觉得主要有以下几个原因:

    • Redis 的大部分操作都在内存中完成,内存中的执行效率本身就很快,并且采用了高效的数据结构,比如哈希表和跳表。
    • 使用单线程避免了多线程的竞争,省去了多线程切换带来的时间和性能开销,并且不会出现死锁。
    • 采用 I/O 多路复用机制处理大量客户端的 Socket 请求,因为这是基于非阻塞的 I/O 模型,这就让 Redis 可以高效地进行网络通信,I/O 的读写流程也不再阻塞。

    7. Redis6.0 使用多线程是怎么回事

    Redis 不同版本之间采用的线程模型是不一样的,在 Redis4.0 版本之前使用的是单线程模型,在 4.0 版本之后增加了多线程的支持。

    在 4.0 之前虽然我们说 Redis 是单线程,也只是说它的网络 I/O 线程以及 Set 和 Get 操作是由一个线程完成的。但是 Redis 的持久化、集群同步还是使用其他线程来完成。

    4.0 之后添加了多线程的支持,主要是体现在大数据的异步删除功能上,例如 unlink key、flushdb async、flushall async 等

    8. Redis的持久化机制有哪些?各自的优缺点是什么?

    使用缓存的时候,我们经常需要对内存中的数据进行持久化也就是将内存中的数据写入到硬盘中。大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了做数据同步(比如 Redis 集群的主从节点通过 RDB 文件同步数据)。

    Redis 不同于 Memcached 的很重要一点就是,Redis 支持持久化,而且支持 3 种持久化方式:

    • 快照(snapshotting,RDB)Redis 默认
    • 只追加文件(append-only file, AOF)Redis6后默认开启
    • RDB 和 AOF 的混合持久化(Redis 4.0 新增)

    9. Redis为什么那么快呢

    • Redis 基于内存,内存的访问速度比磁盘快很多;
    • Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型,主要是单线程事件循环和 IO 多路复用;
    • Redis 内置了多种优化过后的数据类型/结构实现,性能非常高。
    • Redis 通信协议实现简单且解析高效。

    七、Mybatis & MYSQL

    1. SQL 查询语句的执行顺序

    image.png

    2. 优化 SQL

    image.png

    3. explain

    image.png

    4. Mysql索引有哪些

    ①、 B+Tree索引(默认)

    • 主键索引(Primary Key):唯一且非空,表必须有一个主键。
    • 唯一索引(Unique Key):列值必须唯一,允许NULL。
    • 普通索引(Key/Index):无唯一性约束。
    • 联合索引(Composite Index):多个列组合的索引,遵循最左匹配原则。

    ②、 哈希索引(Memory引擎支持):精确查询速度快,不支持范围查询和排序。

    ③、 全文索引(FULLTEXT):用于文本内容的模糊搜索(如MATCH AGAINST语句)。

    ④、 空间索引(SPATIAL):用于地理空间数据类型(如GEOMETRY),支持空间查询。

    ⑤、 前缀索引:对长文本字段(如VARCHAR)前N个字符建立索引,节省空间。

    5. 主键索引个唯一索引的区别?普通索引和主键索引的区别,叶子点中的数据有什么不一样

    特性主键索引 (Primary Key)唯一索引 (Unique Key)普通索引 (Key/Index)
    唯一性约束唯一且非空唯一,允许一个NULL无唯一性约束,允许重复和NULL
    索引数量每张表仅1个每张表可多个每张表可多个
    索引类型聚集索引非聚集索引非聚集索引
    叶子节点数据存储完整数据行存储主键值(需回表)存储主键值(需回表)
    物理存储顺序数据按主键顺序物理存储独立B+树结构独立B+树结构
    适用场景唯一标识行(如自增ID)业务唯一约束(如手机号、邮箱)加速高频查询条件(如状态字段)
    写入性能影响高(唯一性+非空校验)中(唯一性校验)低(无约束校验)
    是否允许修改不可直接修改(需删除重建)可修改可修改
    覆盖索引优化天然覆盖(数据与索引一体)需包含主键字段才可能覆盖需包含主键字段才可能覆盖

    叶子节点数据差异的核心原理

    ①、主键索引:

    • 叶子节点直接存储数据行的全部内容,因此主键查询无需回表。
    • 例:SELECT * FROM user WHERE id=1 直接命中数据页。

    ②、唯一索引 & 普通索引:

    • 叶子节点存储主键值,查询时需通过主键值回表到主键索引获取完整数据。
    • SELECT * FROM user WHERE email='a@example.com'(email是唯一索引):
      • 在email索引树中找到对应主键值id=1。
      • 通过id=1回表到主键索引树获取完整数据行。
    • 若查询字段全部在索引中(如 SELECT id FROM table WHERE email='xxx'),唯一索引无需回表。

    6. 索引优化

    核心优化原则:

    • 选择合适的索引列:高频查询条件、高区分度的列(如用户ID)。
    • 避免冗余索引:联合索引覆盖单列索引(如已有(a,b),无需单独建a的索引)。
    • 短索引优先:减少索引存储空间,提升查询效率。
    • 覆盖索引:查询列全部在索引中,避免回表(如SELECT a,b FROM table WHERE a=1,若索引是(a,b))。

    优化技巧

    • 索引下推(ICP):MySQL 5.6+支持,将WHERE条件过滤下推到存储引擎,减少回表次数。
    • 避免索引失效
    • 定期分析索引使用情况
    • 索引维护:定期优化表(OPTIMIZE TABLE)减少碎片。

    7. 创建索引有哪些注意点

    • 数据量少的表,不适合加索引
    • 更新比较频繁的也不适合加索引
    • 区分度低的字段不适合加索引(如性别)
    • where、group by、order by等后面没有使用到的字段,不需要建立索引
    • 已经有冗余的索引的情况(比如已经有a,b的联合索引,不需要再单独建立a索引)

    8. 如果一张表没有设置主键,是否还会有主键索引

    MySQL 的默认存储引擎是InnoDB,如果没有主键,会尝试使用唯一且非空的列作为主键;否则生成隐藏主键。

    9. MYSQL的索引引擎

    10.索引失效

    • 查询条件包含or,可能导致索引失效
    • 如果字段类型是字符串,where时一定用引号括起来,否则索引失效
    • like通配符可能导致索引失效。
    • 联合索引,查询时的条件列不是联合索引中的第一个列,索引失效。
    • 在索引列上使用 mysql 的内置函数,索引失效。
    • 对索引列运算(如,+、-、*、/),索引失效。
    • 索引字段上使用(!= 或者 < >,not in)时,可能会导致索引失效。
    • 索引字段上使用is null, is not null,可能导致索引失效。
    • 左连接查询或者右连接查询查询关联的字段编码格式不一样,可能导致索引失效。
    • mysql 估计使用全表扫描要比使用索引快,则不使用索引。

    11. 删除外键需要注意什么

    • 外键用于维护表之间的引用完整性,删除外键可能会导致数据不一致。
    • 确保删除外键不会破坏业务逻辑。

    12. B+树和B树的区别

    特性B+树B树
    数据存储位置数据只存储在叶子节点。数据可以存储在任意节点(包括非叶子节点)。
    叶子节点连接叶子节点通过指针连接,形成有序链表。叶子节点之间没有连接。
    查询性能适合范围查询,因为叶子节点有序连接。适合随机查询,但范围查询性能较差。
    磁盘 I/O更适合磁盘存储,因为内部节点只存储键值。磁盘 I/O 较高,因为数据可能存储在非叶子节点。
    应用场景数据库索引(如 MySQL 的 InnoDB)。文件系统、某些数据库索引。

    13.

    14. 使用in会走索引吗

    • 如果 IN 子句中的值是常量

      • MySQL 通常会使用索引。
      • 例如:SELECT * FROM 表名 WHERE id IN (1, 2, 3);
    • 如果 IN 子句中的值是子查询

      • 取决于子查询的复杂性和优化器的选择。
      • 如果子查询返回的结果集较小,可能会使用索引。
      • 如果子查询返回的结果集较大,可能不会使用索引

    15. 数据库三大范式

    第一范式,确保表的每一列都是不可分割的基本数据单元。

    第二范式,要求表中的每一列都和主键直接相关。

    第三范式,非主键列应该只依赖于主键列。

    16. MySQL最左匹配

    联合索引(a,b,c)中,查询条件必须从最左列开始,且连续使用索引列,否则后续列无法使用索引

    -- 1. 完全匹配
    WHERE a=1 AND b=2 AND c=3 -- 使用全部三列索引
    
    -- 2. 部分匹配
    WHERE a=1 AND b=2 -- 使用a和b两列索引
    WHERE a=1 -- 仅使用a列索引
    
    -- 3. 跳过列
    WHERE b=2 AND c=3 -- 无法使用索引(未包含a
    WHERE a=1 AND c=3 → 仅使用a列索引(跳过了b)
    
    -- 4. 范围查询
    WHERE a>1 AND b=2 -- 仅使用a列索引(范围查询后索引失效)
    WHERE a=1 AND b>2 AND c=3 -- 使用a和b列索引,c无法使用
    

    mysql 为什么要遵循最左原则?

    **最左前缀原则是由 B+Tree 索引的数据结构和索引的构建方式决定的。

    • 联合索引的构建方式是按照索引定义的字段顺序,从左到右进行排序的(先按第一列排序,第一列相同再按第二列,以此类推)。
    • 因此,查询必须从最左边的列开始,才能利用索引的有序性进行快速查找,这就像查字典必须知道第一个字母一样。
    • 如果跳过了左边的列,直接查询后面的列,这些列在全局上是无序的,索引就失去了其快速定位的优势,数据库只能进行低效的全表扫描。

    额外补充

    范围查询(><BETWEENLIKE 'A%')之后的列,索引会失效。

    17. MySQL锁资源竞争的三种情况

    锁资源竞争的三种情况包括行锁竞争、表锁竞争和死锁。

    18. 说下JDBC连接数据库的6个步骤?

    ①、注册驱动
    ②、获取连接
    ③、创建一个Statement语句对象
    ④、执行SQL语句
    ⑤、处理结果集
    ⑥、关闭资源

    19. MySQL的行级锁到底锁的是什么?

    MySQL 的行级锁本质上锁定的是表中符合条件的行记录对应的索引项,通过索引实现精准锁定,从而支持高并发读写。若操作不基于索引,行锁会退化为表锁,因此在实际开发中,确保更新 / 删除操作使用有效索引是避免锁冲突、提升性能的关键。

    20. char vs verchar

    对比维度CHARVARCHAR
    数据存储方式固定长度存储:无论实际数据长度是否达到定义长度,都会占用全部定义长度的存储空间。例:定义CHAR(10),存储 “abc” 时,会用空格(默认填充字符)补足 10 个字符的空间。可变长度存储:仅占用实际数据长度 + 1 字节(或 2 字节)长度标识的存储空间(长度标识用于记录数据实际长度)。例:定义VARCHAR(10),存储 “abc” 时,仅占用 3 字节(数据)+1 字节(标识)=4 字节。
    长度限制最大长度为255 个字符(MySQL 5.0 + 中,若使用utf8mb4编码,每个字符占 4 字节,实际存储上限仍受 255 字符限制)。最大长度为65535 个字符(但受表总行大小限制:MySQL 行总长度默认不超过 65535 字节,若VARCHARutf8mb4编码,实际最大可用长度约为 16383 字符,因需预留其他字段空间)。
    填充与截断- 存储时:若数据长度小于定义长度,会自动用空格填充到定义长度。- 查询时:默认会自动去除末尾的填充空格(需注意:若业务需保留末尾空格,CHAR会丢失,需用BINARY属性或VARCHAR)。- 存储时:不会填充任何字符,直接存储实际数据。- 查询时:完全保留数据原始内容(包括末尾空格),不会自动截断。
    性能表现1. 读取速度快:固定长度存储使数据在磁盘上的位置更规整,MySQL 可通过计算快速定位数据,无需解析长度标识。2. 更新效率低:若更新后数据长度变化(如CHAR(10)从 “abc” 改为 “abcd”),需重新调整存储位置,可能引发页分裂。1. 读取速度稍慢:需先解析长度标识,再读取实际数据,比CHAR多一步操作。2. 更新效率高:若更新后数据长度未超过原长度,可直接覆盖;若超过,仅需扩展少量空间(避免大规模页分裂)。
    适用场景适合长度固定、短字符串场景,如:- 性别(“男”“女”,固定 2 字符)- 手机号(11 位数字,固定 11 字符)- 身份证号(18 位,固定 18 字符)- 状态码(如 “0 - 正常”“1 - 禁用”,固定 1-2 字符)适合长度不固定、字符串长度差异大场景,如:- 用户名(2-20 字符)- 文章标题(10-100 字符)- 商品描述(长度可变,可能短至 10 字、长至 1000 字)- 备注信息(长度不确定,需灵活存储)

    21. MYSQL vs 达梦 vs 人大金仓差异

    语法场景MySQL达梦(DM)人大金仓(Kingbase)
    分页查询特有语法:LIMIT [偏移量,] 条数(如 SELECT * FROM t LIMIT 10 OFFSET 20),不支持 Oracle 的ROWNUM完全兼容 Oracle:支持ROWNUM(如 SELECT * FROM (SELECT t.*, ROWNUM rn FROM t) WHERE rn BETWEEN 21 AND 30),同时也支持LIMIT(兼容 MySQL)。同达梦:兼容ROWNUM(Oracle 方式)和LIMIT(MySQL 方式),迁移时无需修改分页逻辑。
    递归查询(层级数据)仅支持 MySQL 8.0 + 的WITH RECURSIVE(标准 SQL 递归),不支持 Oracle 的CONNECT BY [START WITH]。例:查询部门层级:WITH RECURSIVE dept_rec AS (SELECT * FROM dept WHERE parent_id=0 UNION ALL SELECT d.* FROM dept d JOIN dept_rec r ON d.parent_id=r.dept_id) SELECT * FROM dept_rec;完全兼容 Oracle 的CONNECT BYSELECT * FROM dept START WITH parent_id=0 CONNECT BY PRIOR dept_id=parent_id;,同时支持WITH RECURSIVE同达梦:同时支持CONNECT BY(Oracle)和WITH RECURSIVE(标准 SQL),适配不同迁移场景。
    合并数据(新增 / 更新)不支持 Oracle 的MERGE INTO,需用INSERT ... ON DUPLICATE KEY UPDATE(依赖唯一键)实现类似功能:INSERT INTO t (id, name) VALUES (1, 'a') ON DUPLICATE KEY UPDATE name='a';完全兼容 Oracle 的MERGE INTOMERGE INTO t USING (SELECT 1 id, 'a' name FROM DUAL) s ON (t.id=s.id) WHEN MATCHED THEN UPDATE SET t.name=s.name WHEN NOT MATCHED THEN INSERT (id, name) VALUES (s.id, s.name);同达梦:支持MERGE INTO(Oracle 语法),同时兼容 MySQL 的INSERT ... ON DUPLICATE KEY UPDATE
    存储过程 / 函数语法支持存储过程,但语法与 Oracle 差异大:- 无PACKAGE(包)概念;- 变量声明用DECLARE,无需DECLARE ... IS;- 异常处理仅支持DECLARE EXIT HANDLER,不支持EXCEPTION块。完全兼容 Oracle 的 PL/SQL 语法:- 支持PACKAGE(包)、PROCEDUREFUNCTION;- 异常处理用EXCEPTION WHEN ... THEN;- 支持FORALL(批量操作)、BULK COLLECT(批量获取)。同达梦:深度兼容 Oracle PL/SQL,支持PACKAGEEXCEPTIONFORALL等,Oracle 存储过程可直接迁移(修改率 < 5%)。
    数据类型场景MySQL达梦(DM)人大金仓(Kingbase)
    字符串类型基础类型:CHAR/VARCHAR/TEXT(无长度限制差异);不支持 Oracle 的NVARCHAR2(Unicode 字符串),需用VARCHAR(..., utf8mb4)替代。兼容 Oracle:支持CHAR/VARCHAR2/NVARCHAR2(区分字符集)、CLOB(大文本);VARCHAR2长度默认按 “字符” 计算(与 Oracle 一致),避免字符集转换问题。同达梦:支持VARCHAR2/NVARCHAR2/CLOBVARCHAR2长度规则与 Oracle 完全一致,迁移时无需调整字段长度。
    日期时间类型基础类型:DATE(仅存年月日)、DATETIME(年月日时分秒)、TIMESTAMP(带时区);不支持 Oracle 的TIMESTAMP WITH TIME ZONE(带时区的时间戳)。兼容 Oracle:支持DATE(年月日时分秒,与 Oracle 一致)、TIMESTAMPTIMESTAMP WITH TIME ZONE;默认DATE类型含时分秒(区别于 MySQL),迁移时需注意字段含义匹配。同达梦:DATE类型含时分秒,支持TIMESTAMP WITH TIME ZONE,与 Oracle 日期类型完全对齐。
    数值类型基础类型:INT/BIGINT/DECIMAL;不支持 Oracle 的NUMBER(p,s)(任意精度数值),需用DECIMAL(p,s)替代(功能一致,但语法名称不同)。完全兼容 Oracle 的NUMBER(p,s):支持NUMBER(10,2)(如金额字段),精度范围与 Oracle 一致(p 最大 38),迁移时字段类型可直接复用。同达梦:支持NUMBER(p,s),同时兼容 MySQL 的DECIMAL,语法灵活。

    八、RabbitMQ

    1. RabbitMQ的核心组件

    ①、Producer(生产者)

    • 生产者是发送消息的应用程序。
    • 它将消息发布到 RabbitMQ 的 Exchange 中。

    ②、Exchange(交换机)

    • 交换机负责接收生产者发送的消息,并根据规则将消息路由到队列。

    • 常见的交换机类型:

      • Direct Exchange:根据路由键(Routing Key)精确匹配队列。
      • Fanout Exchange:将消息广播到所有绑定的队列。
      • Topic Exchange:根据路由键的模式匹配队列。
      • x-delayed-message:延迟队列

    ③、Queue(队列)

    • 消费者是接收和处理消息的应用程序。
    • 它从队列中订阅消息并进行处理。

    ④、1.5 Binding(绑定)

    • 绑定是 Exchange 和 Queue 之间的连接规则。
    • 它定义了 Exchange 如何将消息路由到 Queue。

    1. 如何解决消费堆积问题

    消费堆积是指消息生产速度远大于消费速度,导致消息在队列中大量积压。解决方案需要从生产端优化消费端优化队列管理三个层面入手。

    1)消费端优化(核心)

    增加消费者实例数量(水平扩展)、优化消费逻辑,提升处理性能、使用批量消费模式

    2)生产端优化:

    消息批量发送,减少网络IO、合理设置消息过期时间(TTL)、异步发送避免阻塞主线程

    3)队列管理:

    设置队列最大长度、启用死信队列处理异常消息、监控告警机制

    2. RabbitMQ如何确保消息的可靠性

    ①、事务机制。在生产者发送消息之前,开启事务,而后发送消息,如果消息发送至 RabbitMQ Server 失败后,进行事务回滚,重新发送。如果 RabbitMQ Server 接收到消息,则提交事务。

    ②、开启生产者确认机制,RabbitMQ只有在完成消息持久化之后才会给生产者返回ACK

    ③、开启生产者返回机制,实现当交换机路由到指定队列失败后回调方法,拿到被退回的消息信息,进行相应的处理如记录日志或重传等等。

    ④、3.12后默认队列是持久化的。lazyQueue会将所有消息都持久化

    3. 消费者如何保证消息一定被消费了呢

    消费者通过手动确认机制异常处理死信队列重试机制来确保消息被成功消费。核心在于业务处理成功后才确认消息,处理失败时进行重试或转入死信队列。

    1. 手动确认模式

    • 业务处理成功后手动发送ACK
    • 处理失败时发送NACK或拒绝消息
    • 避免使用自动确认模式

    2. 异常处理机制

    • try-catch包裹业务逻辑
    • 根据异常类型选择重试或死信
    • 记录消费失败日志

    3. 重试策略

    • 最大重试次数限制
    • 指数退避策略避免频繁重试
    • 重试队列或延迟队列

    4. 死信队列

    • 处理多次重试失败的消息
    • 人工介入处理异常消息
    • 监控告警机制

    4. 各个MQ的区别

    image.png

    5. 幂等性是什么?怎么设计

    幂等性是指一个操作无论执行多少次,结果都是一样的。

    设计幂等性的方法:

    1. 通过业务代码控制
    2. 唯一标识符/版本号

    6. 如何实现消息的延迟队列?

    延迟消息:生产者发送消息时指定一个时间,消费者不会立刻收到消息,而是在指定时间之后才收到消息

    实现

    • 实现MessagePostProcessor
    • 重写postProcessMessage
    • message.getMessageProperties().setDelayLong(delay);然后message

    7. 死信队列?

    当队列中的消息满足以下情况之一,就会成为死信:

    • 消费者使用basic.reject 或 basic.nack 声明消息失败,并且设置requeue参数为false;
    • 消息时一个过期的消息(达到队列或消息本身设置的过期时间),超时无人消费
    • 要投递的队列堆积满了。最早的消息可能成为死信

    如果队列通过dead-letter-exchange属性制定了一个交换机,那么该队列中的死信就会投递到这个交换机中。这个交换机成为死信交换机(Dead Letter Exchange DLX)

    九、ES

    1. 使用ES的好处

    • 高性能搜索:ES基于倒排索引,能够快速进行全文搜索。
    • 实时性:数据几乎可以实时被搜索到。
    • 灵活的数据模型:支持结构化、非结构化数据的存储和搜索。
    • 强大的查询语言:支持复杂的查询和聚合操作。

    2. 如何与DB保证一致性

    • 双写机制:在写入数据库的同时写入ES,但需要处理双写的一致性问题。
    • 异步同步:通过消息队列(如Kafka)异步同步数据到ES,确保最终一致性。
    • 定时同步:通过定时任务定期将数据库的数据同步到ES。

    3. 倒排索引

    倒排索引(Inverted Index)是搜索引擎中非常重要的一种数据结构,用于实现高效的全文检索。它的核心思想是将文档内容中的单词作为索引,将包含该词的文档ID作为记录的结构。倒排索引的优势是查询速度快,适合全文检索和关键词搜索场景。

    Doc 1:Java is the best programming language 
    Doc 2:PHP is the best programming language 
    Doc 3:Javascript is the best programming language
    

    为了创建索引,ES引擎通过分词器将每个文档的内容拆成单独的词(称之为词条,或term),再将这些词条创建成不含重复词条的排序列表,然后列出每个词条出现在哪个文档,结果如下:

    termDoc 1Doc 2Doc 3
    Java
    is
    the
    best
    programming
    language
    PHP
    Javascript

    这种结构由文档中所有不重复的词的列表构成,对于其中每个词都有至少一个文档与与之关联。这种由属性值来确定记录的位置的结构就是倒排索引,带有倒排索引的文件被称为倒排文件。

    4. ES和mysql的查询有什么不一样

    • 查询语言:MySQL使用SQL,ES使用DSL(基于JSON的查询语言)。
    • 索引方式:MySQL使用B+树索引,ES使用倒排索引。
    • 实时性:ES支持近实时搜索,MySQL的查询是基于当前数据库状态的。
    • 分布式支持:ES天生支持分布式,MySQL需要额外配置(如分库分表)。

    5. 字段类型?

    • 字符串类型
      • text:支持分词,全文检索,支持模糊、精确查询,不支持聚合,排序操作;text类型的最大支持的字符长度无限制,适合大字段存储;
      • keyword:不进行分词,直接索引、支持模糊、支持精确匹配,支持聚合、排序操作。keyword类型的最大支持的长度为——32766个UTF-8类型的字符,可以通过设置ignore_above指定支持字符长度,超过给定长度后的数据将不被索引,无法通过term精确匹配检索返回结果。
    • 数值型
      • long、Integer、short、byte、double、float、half float、scaled float
    • 日期类型:date
    • te布尔类型:boolean
    • 二进制类型:binary

    6. IK分词器?是否自定义词库?

    IK分词器的2种模式:细粒度模式ik_max_word和智能ik_smart

    可以实现自定义词库:在elasticsearch/plugins/ik/configIKAnalyzer.cfg.xml可以增加本地或远程词库。

    7. 如何处理ES的深分页问题?

    深分页问题是指在大数据集中查询靠后的分页时,性能急剧下降。原因是from + size需要遍历所有匹配的文档,计算量较大。

    from+size查询方式在 10000-50000 条数据以内的时候还是可以的

    • 解决深分页问题的方法包括:
      • 使用scroll:适合一次性获取大量数据的场景,但不适合实时查询。
      • 使用search_after[官方的建议不用于实时的请求]:适合实时查询,通过排序字段和游标实现分页。
      • 限制分页深度:避免用户查询过深的分页。

    8. 如何优化索引

    1. 设置合理的索引分片数和副本数。
    - 索引分片数建议设置为集群节点的整数倍,初始数据导入时副本数设置为 0,生产环境副本数建议设置为 1(设置 1 个副本,集群任意 1 个节点宕机数据不会丢失;设置更多副本会占用更多存储空间,操作系统缓存命中率会下降,检索性能不一定提升)。
    - 单节点索引分片数建议不要超过 3 个,每个索引分片推荐 10-40GB 大小,索引- 分片数设置后不可以修改,副本数设置后可以修改。  
    - Elasticsearch6.X 及之前的版本默认索引分片数为 5、副本数为 1,从 
    - Elasticsearch7.0 开始调整为默认索引分片数为 1、副本数为 1。
    

    2. 使用批量请求。

    使用批量请求将产生比单文档索引请求好得多的性能。写入数据时调用批量提交接口,推荐每批量提交 5~15MB 数据。
    例如单条记录 1KB 大小,每批次提交 10000 条左右记录写入性能较优;
    单条记录 5KB 大小,每批次提交 2000 条左右记录写入性能较优。
    

    3. 通过多进程/线程发送数据。

    单线程批量写入数据往往不能充分利用服务器 CPU 资源,可以尝试调整写入线程数或者在多个客户端上同时向 Elasticsearch 服务器提交写入请求。
    与批量调整大小请求类似,只有测试才能确定最佳的 worker 数量。
    可以通过逐渐增加工作任务数量来测试,直到集群上的 I/O 或 CPU 饱和。
    

    4. 调大refresh interval。

    在 Elasticsearch 中,写入和打开一个新段的轻量的过程叫做 refresh 。
    默认情况下每个分片会每秒自动刷新一次。
    这就是为什么我们说 Elasticsearch 是近实时搜索: 文档的变化并不是立即对搜索可见,但会在一秒之内变为可见。  
    并不是所有的情况都需要每秒刷新。
    可能你正在使用 Elasticsearch 索引大量的日志文件,你可能想优化索引速度而不是近实时搜索,可以通过设置 refresh_interval,降低每个索引的刷新频率。
    refresh_interval 可以在已经存在的索引上进行动态更新,在生产环境中,当你正在建立一个大的新索引时,可以先关闭自动刷新,待开始使用该索引时,再把它们调回来。
    

    5. 配置事务日志参数。

    事务日志 translog 用于防止节点失败时的数据丢失。
    它的设计目的是帮助 shard 恢复操作,否则数据可能会从内存 flush 到磁盘时发生意外而丢失。
    事务日志 translog 的落盘(fsync)是 ES 在后台自动执行的,默认每 5 秒钟提交到磁盘上,或者当 translog 文件大小大于 512MB 提交,或者在每个成功的索引、删除、更新或批量请求时提交。  
    索引创建时,可以调整默认日志刷新间隔 5 秒,例如改为 60 秒,index.translog.sync_interval: "60s"。
    创建索引后,可以动态调整 translog 参数,"index.translog.durability":"async" 相当于关闭了 index、bulk 等操作的同步 flush translog 操作,仅使用默认的定时刷新、文件大小阈值刷新的机制。
    

    6. 设计mapping配置合适的字段类型

    十、序列化和反序列化

    1. 什么是序列化和反序列化?它们的应用场景是什么?

    • 序列化:将数据结构或对象转换成可以存储或传输的形式,通常是二进制字节流,也可以是 JSON, XML 等文本格式
    • 反序列化:将在序列化过程中所生成的数据转换为原始数据结构或者对象的过程

    2. 你使用过哪些序列化框架?各自的优缺点是什么?

    3. 如何优化序列化的性能?

    4. 如何避免序列化安全问题?

    十一、Linux

    1. 查看日志的命令有哪些

    # tail:查看文件的末尾内容,常用实时查看日志文件的更新。
    tail -f logfile.log
    
    # cat:显示整个日志文件内容
    cat logfile.log 
    
    # grep:查找包含 "error" 的日志行
    grep "error" logfile.log  
    
    # less:分页查看文件内容,支持向下滚动。
    less logfile.log  
    

    2. 查看最新十行内容用什么命令

    tail -n 10 logfile.log  # 查看文件的最后 10 行
    tail -f -n 10 logfile.log  # 实时查看文件的最后 10 行
    

    3. 编辑文本的命令?编辑查找时用什么命令

    编辑命令:

    vim filename.txt  # 使用 vim 编辑文件
    

    编辑查找命令:

    /search_term  # vim中查找 "search_term"
    

    十二、前端

    1. ES6特点

    • 声明方式:新增let(块级作用域变量)和const(常量),解决var的作用域问题。
    • 箭头函数:简化函数定义,如(x) => x*2,改变this指向规则。
    • 类与继承:引入class语法糖,支持extends继承,更接近传统面向对象编程。
    • 模块化:通过importexport实现模块导入导出,规范代码组织。
    • 解构赋值:便捷提取数组或对象中的值,如const {a, b} = obj
    • 模板字符串:用反引号()定义字符串,支持多行和变量插入(${变量}`)。
    • 默认参数:函数参数可设置默认值,如function fn(x=1) {}
    • 扩展运算符...用于数组 / 对象的展开或收集,如[...arr1, ...arr2]
    • Promise:解决异步回调地狱问题,提供更优雅的异步处理方式。

    2. ES6的箭头函数和JS有什么区别

    特性箭头函数普通函数
    this绑定继承父作用域的this有自己的this,取决于调用方式
    arguments没有arguments对象有arguments对象
    构造函数不能作为构造函数可以作为构造函数
    原型没有prototype属性有prototype属性
    简写语法更简洁相对冗长

    3. VUE的生命周期

    阶段生命周期钩子函数说明
    创建阶段setupVue3 新增,组件初始化前执行,替代 Vue2 的beforeCreatecreated部分功能
    beforeCreate(Vue2 已废弃)Vue2 中实例刚创建,数据和事件未初始化,Vue3 中不再使用
    created实例创建完成,可访问数据和方法,Vue3 中仍可使用但推荐在setup中处理
    挂载阶段beforeMount模板编译完成,即将挂载到 DOM,此时虚拟 DOM 已生成
    mountedDOM 挂载完成,可操作真实 DOM
    更新阶段beforeUpdate数据更新后,DOM 未重新渲染前触发
    updatedDOM 重新渲染完成后触发
    销毁阶段beforeUnmount组件销毁前触发,可清理资源(如定时器、事件监听)
    unmounted组件销毁完成,所有绑定和子组件被移除
    错误捕获阶段errorCaptured捕获子组件抛出的错误时触发,可返回false阻止错误向上传播

    image.png

    4. 块级元素 vs 行级元素

    特性块级元素(Block-level)行级元素(Inline-level)
    排列方式独占一行,默认从上到下排列与其他行级元素在同一行排列,宽度由内容决定
    宽度 / 高度可通过width/height设置,默认占满父容器宽度不可设置width/height,由内容撑开
    内边距 / 外边距marginpadding的上下左右都有效marginpadding仅左右有效,上下无效
    包含关系可包含块级元素和行级元素通常只能包含行级元素或文本

    十三、ngnix

    📢其他

    1. RESTful API

    2. DDD模型

    3. 本地缓存和redis缓存的区别;本地缓存更新策略?

    4. 消息幂等?如果处理两次如何实现?

    5. 分布式唯一ID消息生成方案?

    6. 物理内存和虚拟内存?

    📢场景题

    1. 你的查询触发了索引扫描但仍涉及十万条数据时,sql如何优化

    2. 你有一个A服务部署在acould上,有一个B服务部署在bcould上,如何在A中调用B的方法/API

    方案实现方式适用场景
    HTTP REST APISpring Cloud OpenFeign通用性最强
    RPC框架Dubbo + Nacos高性能要求
    消息队列RabbitMQ/RocketMQ异步解耦
    API网关Spring Cloud Gateway统一入口

    具体实现(推荐OpenFeign)

    java

    // 1. 服务B提供HTTP接口
    @RestController
    public class ServiceBController {
        @PostMapping("/api/process")
        public ResponseDTO process(@RequestBody RequestDTO request) {
            // 业务逻辑
            return new ResponseDTO("success");
        }
    }
    
    // 2. 服务A声明Feign客户端
    @FeignClient(
        name = "service-b",
        url = "http://bcloud-service:8080",
        configuration = FeignConfig.class
    )
    public interface ServiceBClient {
        @PostMapping("/api/process")
        ResponseDTO callServiceB(@RequestBody RequestDTO request);
    }
    
    // 3. 安全配置
    @Configuration
    public class FeignConfig {
        @Bean
        public BasicAuthRequestInterceptor basicAuth() {
            return new BasicAuthRequestInterceptor("user", "password");
        }
        
        @Bean
        public RequestInterceptor headerInterceptor() {
            return template -> template.header("X-Service-Token", "encrypted-token");
        }
    }
    
    // 4. 服务A调用
    @Service
    public class ServiceA {
        @Autowired
        private ServiceBClient serviceBClient;
        
        public void processBusiness() {
            ResponseDTO response = serviceBClient.callServiceB(request);
            // 处理响应
        }
    }
    

    3. 搜索如果要实现体现热词功能,怎么设计

    4. 给前端提供的接口怎么保证是安全的

    • 通过HTTPS加密传输、接口鉴权和参数校验保证接口的安全性。
    • 防止恶意调用的方法包括限流、IP白名单和验证码。
    • 使用过OAuth2.0和JWT鉴权,OAuth2.0适合第三方授权,JWT适合无状态鉴权。
    • 防止数据泄露的方法包括数据脱敏、权限控制和日志审计。

    5. 导入文档的应用场景

    6. 如何找出一张表中名字重复的数据

    SELECT name, COUNT(*) AS count
    FROM 表名
    GROUP BY name
    HAVING COUNT(*) > 1;
    

    7. A B两张表,通过ID连接,如何把A表中的名字更新到B中

    UPDATE B
    JOIN A ON B.id = A.id
    SET B.name = A.name;
    

    8. 如何查询并解决MYSQL死锁

    9. MySQL数据库cpu飙升的话,要怎么处理

    10. 口述冒泡排序怎么做

    比较nums[i]nums[i + 1],如果nums[i] > nums[i + 1],nums[i]nums[i + 1]互换位置,每次完成后都len - 1

    11. 如何保证服务间的调用安全

    12.