一、Java
基础知识
权限修饰符
Java 中 4 种修饰符分别为:public、protect(颇太可特)default、private
访问权限 类 包 子类 其他包
public √ √ √ √
protected √ √ √ x
default √ √ x x
private √ x x x
面向对象的特征
- 封装:
- 就是把对象的属性和行为(数据)结合成一个独立的整体,并尽可能隐藏对象的内部实现细节,就是把不想告诉或者不该告诉别人的东西隐藏起来,把可以告诉别人的公开,别人只能用我提供的功能实现需求,而不知道是如何实现的,增加安全性。
- 继承:
- 子类继承父类的数据属性和行为,并能根据自己的需求扩展出新的行为,提高了代码的复用性
- 多态:
- 指允许不同的对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式(发送消息就是函数调用)。封装和继承几乎都是为多态而准备的,在执行期间判断引用对象的实际类型,根据其实际的类型调用其相应的方法
- 抽象:
- 表示对问题领域进行分析、设计中得出的抽象的概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象。在 Java 中抽象用 abstract 关键字来修饰,用 abstract 修饰类时,此类就不能被实例化,从这里可以看出,抽象类(接口)就是为了继承而存在的
String 常用的方法有哪些
- indexOf():返回指定字符的索引
- charAt():返回指定索引处的字符
- replace():字符串替换
- trim():去除字符串两端空白
- split():分割字符串,返回一个分割后的字符串数组
- getBytes():返回字符串的 byte 类型数组
- length():返回字符串长度
- toLowerCase():将字符串转成小写字母
- toUpperCase():将字符串转成大写字母
- subString():截取字符串
- equals():字符串比较
String 的 equals 是如何实现的
- 先比较内存地址,如果内存地址一样,就返回 true
- 比较两个值的长度,如果长度不一样,就返回 false
- 两个值的长度一样,再逐字符比较
Java 中有几种类型的流
- 字节流
- InputStream 是所有字节输入流的祖先
- OutputStream 是所有字节输出流的祖先
- 字符流
- Reader 是所有读取字符串输入流的祖先
- Writer 是所有输出字符流的祖先
- 字节高效流:BufferedOutputStream、BufferedInputStream
- 字符高效流:BufferedWriter、BufferedReader
- 转换流:OutputStreamWriter、InputstReamReader
四类八种
- 整型:byte(1字节)、short(2)、int(4)、long(8)
- 浮点型:float(4)、long(8)
- 字符型:char(2)
- 布尔型:boolean(1)
重载和重写的区别
- 重载:发生在同一个类中,方法名必须相同,参数类型不同,个数不同,顺序不同,方法返回值和访问修饰符可以不同,发生在编译时,比如
- 重写:发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为 private 则子类就不能重写该方法
== 和 equals 的区别
- ==
- 基本类型:比较的就是值是否相同
- 引用类型:比较的就是地址值是否相同
- equals
- 引用类型:默认情况下,比较的是地址值
- 特殊情况下:String、Integer、Date 这些类库中 equals 被重写,比较的是内容而不是地址
请解释字符串比较之中 == 和 equals() 的区别
==:比较的是两个字符串内存地址(堆内存)的数值是否相等,属于数值比较;
equals():比较的是两个字符串的内容,属于内容比较
String、StringBuffer、StringBuilder 三者之间的区别
- String 字符串常量
- StringBuffer 字符串变量(线程安全)
- StringBuilder 字符串变量(非线程安全)
String 中的 String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[],String 对象是不可变的,也就可以理解为常量,线程安全
AbstractStringBuilder 是 StrignBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 append、insert、indexOf 等公共方法。
StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。
StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的
如果要操作少量的数据用 String
多线程操作字符串缓冲区下操作大量数据用 StringBuffer
单线程操作字符串缓冲区下操作大量数据用 StringBuilder
接口和抽象类的区别是什么?
-
实现:抽象类的子类用 extends 来继承;接口必须使用 implements 来实现接口
-
构造函数:抽象类可以有构造函数;接口不能有
-
main 方法:抽象类可以有 main 方法,并且我们能运行它;接口不能有 main 方法
-
实现数量:类可以实现很多个接口;但是只能继承一个抽象类
-
访问修饰符:接口中的方法默认使用 public 修饰;抽象类中的方法可以是任意访问修饰符
什么是单例模式?有几种?
单例模式:某个类的实例在多线程环境下只会被创建一次出来。
单例模式有饿汉式单例模式、懒汉式单例模式和双检索单例模式三种。
饿汉式:线程安全,一开始就初始化
懒汉式:非线程安全,延迟初始化
双检索:线程安全,延迟初始化
饿汉式(静态常量)【可用】
优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。
懒汉式(线程不安全)【不可用】
这种写法起到了Lazy Loading的效果,但是只能在单线程下使用。如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。
————————————————
特点
单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
单例模式保证了全局对象的唯一性,比如系统启动读取配置文件就需要单例保证配置的一致性
什么情况使用单例模式
单例模式(Singleton Pattern)是一种设计模式,它确保一个类只有一个实例,并提供全局访问点。
单例模式通常在以下情况下使用:
-
资源共享:当系统中的多个模块需要共享某个资源时,例如数据库连接池、线程池等。
-
配置信息:当系统中的多个模块需要访问相同的配置信息时,例如日志配置、应用程序配置等。
-
对象缓存:当某个对象需要被共享和复用时,例如数据访问对象(DAO)。
-
控制对实例的访问:当需要控制对某个实例的访问权限时,例如线程池的最大并发数限制。
-
频繁创建销毁代价高:当一个对象的创建和销毁过程非常耗费资源和时间时,使用单例模式可以减少资源的浪费。
使用单例模式时要考虑线程安全性和性能问题。可以使用懒加载、双重检查锁定、静态内部类等方式来实现线程安全的单例模式。同时,过度使用单例模式可能导致代码的耦合性增加和可测试性降低,因此需要权衡使用的场景和需求。
反射
在 Java 中的反射机制是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象的方法就称为 Java 语言的反射机制
获取 Class 对象的 3种方法
- 调用某个对象的 getClass() 方法
- 调用某个类的 class 属性来获取该类对应的 Class 对象
- 使用 Class 类中的 forName() 静态方法(最安全/性能最好)
jdk1.8 的新特性
- Lambda 表达式
- 方法引用
- 方法引用允许直接引用已有 Java 类或对象的方法或构造方法
- 函数式接口
- 有且仅有一个抽象方法的接口叫做函数式接口,函数式接口可以被隐式转换为 Lambda 表达式。通常函数式接口上会添加 @FunctionalInterface 注解
- 接口允许定义默认方法和静态方法
- 从 JDK8 开始,允许接口中存在一个或多个默认非抽象方法和静态方法
- Stream API
- 新增加的 Stream API 把真正的函数式编程风格引入到 Java 中。这种风格将要处理的元素集合看作一种流,流在管道中传输,并且可以在管道的节点上进行处理,比如筛选,排序,聚合等
- 日期/时间类改进
- 之前的 JDK 自带的日期处理类非常不方便,我们处理的时候经常是使用第三方工具包,不过 JDK8 出现之后这个改观了很多,比如日期时间的创建、比较、调整、格式化、时间间隔等,这些都在 java.time 包下,LocalDate/LocalTime/LocalDateTime
- Optional 类
- Optional 类是一个可以为 null 的容器对象。如果值存在则 isPresent() 方法会返回 true,调用 get() 方法会返回该对象
- Java8 Base64 实现
- Java8 内置了 Base64 编码的编码器和解码器
Java 的异常有哪些
Throwable 是所有 Java 程序中错误处理的父类,有两种子类:Error 和 Exception
Error:表示由 JVM 所侦测到的无法预期的错误,由于这是属于 JVM 层次的严重错误,导致 JVM 无法继续执行,因此,这是不可捕捉到的,无法采取任何恢复的操作,顶多只能显示错误信息
Exception:表示可恢复的例外,这是可捕捉到的
- 运行时异常:都是 RuntimeException 类及其子类异常,如 NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。运行时异常的特点是 Java 编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用 try-catch 语句捕获它,也没有用 throws 子句声明抛出它,也会编译通过
- 非运行时异常(编译异常):是 RuntimeException 以外的异常,类型上都属于 Exception 类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如 IOException,SQLException 等以及用户自定义的 Exception 异常,一般情况下不自定义检查异常
常见的 RunTime 异常几种如下:
- NullPointerException:空指针引用异常
- ClassCastException:类型强制转换异常
- IllegalArgumentException:传递非法参数异常
- ArithmeticException:算术运算异常
- ArrayStoreException:向数组中存放与声明类型不兼容对象异常
- IndexOutOfBoundsException:下标越界异常
BIO、NIO、AIO 有什么区别
- BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低
- NIO:New IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用
- AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非阻塞 IO,异步 IO 的操作基于事件和回调机制
同步锁、死锁、乐观锁、悲观锁
- 同步锁:
- 当多个线程同时访问同一个数据时,很容易出现问题。为了避免这种情况发生,我们要保证线程同步互斥,就是指并发执行多个线程,在同一时间内只允许一个线程访问共享数据。Java 中可以使用 synchronized 关键字来取得一个对象的同步锁
- 死锁:
- 何为死锁,就是多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放
- 乐观锁:
- 总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和 CAS 算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量。
- 悲观锁:
- 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。(传统的关系型数据库里边就用到了很多这种锁机制,比如行锁、表锁、读锁、写锁等,都是在做操作之前先上锁)。
什么是 Java 序列化,如何实现 Java 序列化
什么是 Java 序列化
- 序列化是将对象转台转换为可持续或传输的格式的过程。与序列化相对的是反序列化,它将流转换为对象。这两个过程结合起来,可以轻松的存储和传输数据
如何实现 Java 序列化
构造方法为什么不能被重写
重写是子类方法重写父类的方法,重写的方法名不变,而类的构造方法名必须与类名一致,父类的构造方法如果能够被子类重写则子类类名必须与父类类名一致才行,所以Java 的构造方法是不能被重写的。
而重载是针对同一个方法名的,所以Java构造方法可以被重载
。
构造方法可以被继承吗
构造方法 不会被继承,但子类在继承父类时,会先调用父类的构造函数来初始化继承的成员变量。构造函数在对象创建时被调用,负责对象的初始化,成员属性的初始化。它一般用来初始化成员属性和执行其他必要的初始化操作。
final关键字
当一个类被声明为final
时,可以保护该类的实现细节,防止其他类继承并修改其行为。当一个方法被声明为final
时,可以确保该方法的行为不会被子类修改,从而保持一致的逻辑。当一个变量被声明为final
时,可以在编译时进行常量折叠和优化,提高程序的执行效率
字符串可以被继承吗
字符串是被声明为final
的,意味着它不能被继承。
异常处理方式
try cath 有的异常捕获会加上finally,无论try块中异常有无捕获finally块中最后都会执行,除非try块代码中有 system.exit(0)(system.exit(0)的作用是退出虚拟机)。
throw是语句抛出异常,出现于函数内部,用来抛出一个具体异常实例,throw被执行后面的语句不起作用
lambda表达式
Lambda表达式,也可称为闭包。其本质属于函数式编程的概念,是Java8发布的最重要的新特性
- 避免匿名内部类定义过多
- 可以写出更简洁、更灵活的代码,只留下来核心的逻辑
- `public interface Runnable{`
`public abstract void run();`
`}`
- 对于函数式接口,我们可以通过lambda表达式来创建该接口的对象
- 任何接口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口。
stream流
果你提到的"stream"是指在编程中常见的"流"(也称为"流式处理"),那么它是一种对数据进行连续序列处理的概念。
流式处理是指将数据视为一系列连续的事件或对象,并且按照特定的顺序对它们进行处理。在流式处理中,数据被分割成特定的块,并且每个块都经过一系列的转换操作。这种处理方式有助于处理大量的数据集或者连续产生的数据,同时也能够实时处理数据。
流式处理常常用于以下场景:
- 大数据处理:在处理大规模数据集时,流式处理可以对数据进行分块处理,避免将整个数据集一次性加载到内存中。这可以提高处理效率并减少内存消耗。
- 实时数据处理:流式处理非常适合处理实时产生的数据,例如传感器数据、日志数据等。通过实时流式处理,可以对数据进行实时分析、过滤、聚合等操作,从而得到及时的结果。
集合
常见的数据结构
常见的数据结构有:数组,栈,队列,列表,树,散列,堆,图等
- 数组是最常用的数据结构,数组的特点是长度固定,数组的大小固定后就无法扩容了,数组只能存储一种类型的数据,添加,删除的操作慢,因为要移动其他的元素
- 栈是一种基于先进后出(FILO)的数据结构,是一种只能在一端进行插入和删除操作的特殊线性表。它按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)
- 队列是一种基于先进先出(FIFO)的数据结构,是一种只能在一端进行插入,在另外一端进行删除操作的特殊线性表,它按照先进先出的原则存储数据,先进入的数据,在读取数据时先被读取出来
- 链表是一种物理存储单元上非连续、非顺序的存储结构,其物理结构不能只表示数据元素的逻辑顺序,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列的节点(链表中的每一个元素称为节点)组成,节点可以在运行时动态生成。根据指针的指向,链表能形成不同的结构,例如单链表,双向链表,循环链表等
- 树是我们计算机中非常重要的一种数据结构,同时使用树这种数据结构,可以描述现实生活中的很多事务,例如家谱、单位的组织架构等等。有二叉树、平衡树、红黑树、B树、B+树
- 散列表,也叫哈希表,是根据关键码和值(key 和 value)直接进行访问的数据结构,通过 key 和 value 来映射到集合中的一个位置,这样就可以很快找到集合中的对应元素
- 堆是计算机学科中一类特殊的数据结构的统称,堆通常可以被看作是一颗完全二叉树的数组对象
- 图的定义:图是由一组顶点和一组能够将两个顶点相连的边组成的
List 和 Set、Map 的区别
-
List 和 Set 是存储单列数据的集合,Map 是存储键值对这样的双列数据的集合
-
List 中存储的数据是有顺序的,并且值允许重复
-
Set 中存储的数据是无顺序的,并且不允许重复,但元素在集合的位置是由元素的 hashcode 决定,即位置是固定的(Set 集合是根据 hashcode 来进行数据存储的,所以位置是固定的,但是这个位置不是用户可以控制的,所以对于用户来说 set 中的元素还是无序的)
-
Map 中存储的数据是无序的,它的键是不允许重复的,但是值是允许重复的
List 和 Set、Map 的实现类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7mirFniL-1689232139828)(C:\Users\A\AppData\Roaming\Typora\typora-user-images\1688009690196.png)]
- Collection 接口:
- List 有序,可重复
- ArrayList:
- 优点:底层数据结构是数组,查询快,增删慢
- 缺点:线程不安全,效率高
- LinkedList:
- 优点:底层数据结构是链表,查询慢,增删快
- 缺点:线程不安全,效率高
- Vector:
- 优点:底层数据结构是数组,查询快,增删慢
- 缺点:线程安全,效率低,已经舍弃了
- Set 无序,唯一
- HashSet:
- 底层数据结构是哈希表(无序,唯一),如何来保证元素唯一性?依赖两个方法:hashCode() 和 equals()
- LinkedHashSet:
- 底层数据结构是链表和哈希表。(FIFO 插入有序,唯一)
- 由链表保证元素有序
- 由哈希表保证元素唯一
- TreeSet
- 底层数据结构是红黑树(唯一,有序)
- 如何保证元素排序的呢?自然排序、比较器排序
- 如何保证元素唯一性的呢?根据比较的返回值是否是0来决定
- Map
- HashMap
- 基于 hash 表的 Map 接口实现,非线程安全,高效,支持 null 键和 null 值,线程不安全
- HashTable
- 线程安全,效率低,不支持 null 键和 null 值
- LinkedhashMap
- 线程不安全,是 HashMap 的一个子类,保存了记录的插入顺序
- TreeMap
- 能够把它保存的记录根据建排序,默认是键值的升序排序,线程不安全
ArrayList 和 LinkedList 的区别
1、ArrayList的实现是基于数组,LinkedList的实现是基于双向链表。
2、对于随机访问,ArrayList优于LinkedList
3、对于插入和删除操作,LinkedList优于ArrayList
4、LinkedList比ArrayList更占内存,因为LinkedList的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
map集合中有什么
(https://img-blog.csdnimg.cn/a6a85abf665d41c794c44a9f3dd17076.png#pic_center)
HashMap 底层原理
HashMap 在 JDK1.8 之前的实现方式是 数组+链表
但是在 jdk1.8 后对 HashMap 进行了底层优化,改为了由数组+链表+红黑树实现,主要的目的是提高查找效率
Map 的 put 流程
- 首先将 k,v 封装到 Node 对象当中
- 调用 k 的 hashCode() 方法得出哈希值,并通过哈希算法转换成数组的下标
- 下标位置上如果没有任何元素,就把 Node 添加到这个位置上。如果说下标对应的位置上有链表。此时,就会拿着 k 和链表上每个节点的 k 进行比较。如果所有的 equals 方法返回都是 false,那么这个新的节点将被添加到链表的末尾。如其中有一个 equals 返回了 true,那么这个节点的 value 将会被覆盖。当整个 Map 的元素个数大于等于64,链表的元素个数大于等于8以后,会将链表转换为红黑树,当红黑树的节点个数小于等于6时又会退化为链表
Map 的 get 流程
- 先调用 k 的 hashCode() 方法得出哈希值,并通过哈希算法转换成数组的下标。
- 再通过数组下标快速定位到某个位置上。如果这个位置上什么都没有,则返回 null。如果这个位置上有单向链表,那么它就会拿着参数 k 和单向链表上的每一个节点的 k 进行比较,如果所有比较方法都返回 false,则 get 方法返回 null。如果其中一个节点的 k 和参数 k 进行比较返回 true,那么此时该节点的 value 就是我们要找的 value 了,get 方法最终返回这个要找的 value。
hashMap 如何去解决 hash 冲突
HashMap 和 HashTable、ConcurrentHashMap 区别
HashMap 和 HashTable 区别
- HashMap 是非线程安全的,HashTable 是线程安全的
- HashMap 的键和值都允许有 null 值存在,而 HashTable 则不行
- 因为线程安全的问题,HashMap 效率比 HashTable 的要高
- HashTable 是同步的,而 HashMap 不是。因此,HashMap 更适合单线程环境,而 HashTable 适合于多线程环境。一般现在不建议用 HashTable,原因是:
- 1.HashTable 是遗留类,内部实现很多没优化和冗余
- 2.即使在多线程环境下,现在也有同步 ConcurrentHashMap 替代,没有必要因为是多线程而用 HashTable
HashTable 和 ConcurrentHashMap 区别
- HashTable 使用的是 Synchronized 关键字修饰,ConcurrentHashMap 是 JDK1.7 使用了锁分段技术来保证线程安全的。JDK1.8 ConcurrentHashMap 取消了 Segment 分段锁,采用 CAS 和 synchronized 来保证并发安全。数据结构跟 HashMap1.8 的结构类似,数组+链表/红黑树
- synchronized 只锁定当前链表或红黑树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍。
LinkedHashMap 和 HashMap 区别
- 实现方式不同:HashMap 底层使用哈希表实现,而 LinkedHashMap 继承自 HashMap,并基于双向链表实现了一个有序的 Map 集合
- 迭代顺序不同:HashMap 迭代时元素没有排序,而 LinkedHashMap 则按照插入顺序或者访问顺序(LRU算法)进行遍历。因此,如果需要保证迭代顺序与插入顺序一致,则应该使用 LinkedHashMap
ArrayList 的扩容
ArrayList 是一个数组结构的存储容器,默认情况下数组的长度是10个,随着在程序里面不断地往 ArrayList 里面添加数据,当添加的数据达到10个的时候,ArrayList 里面就没有足够的容量去存储新的数据,此时就会触发扩容,
- 它首先创建一个新的数组,这个新数组的长度是原来数组长度的1.5倍
- 然后使用 Arrays.conpyOf 方法把老数组里面的数据拷贝到新的数组里面
- 扩容完成之后再把当前需要添加的元素加入到新的数组里面
HashMap 扩容
当 HashMap 中的元素数量超过负载因子乘以当前容量时,就会触发扩容操作。扩容过程包括以下步骤:
- 创建一个新的两倍大小的数组。
- 遍历原数组中的每个元素,重新计算它们的哈希值,并将它们放入新数组的对应位置。
- 如果多个元素计算后的位置相同,则它们会形成一个链表。
- 扩容后,每个链表中的元素会被重新分布到新数组中。
- 元素的插入顺序可能会改变,因为新数组的位置不同。
- 当所有元素都被重新分布后,扩容完成。
简单来说,HashMap 的扩容就是将原来的元素重新放到一个更大的数组中,以避免过多的哈希冲突。这样可以提高 HashMap 的性能和效率。
CAS 机制
- CPU 开销大:在并发很高的情况下,如果多线程反复尝试更新一个变量,却一直不成功,循环往复,造成 CPU 压力大
- 不能保证原子性:多线程可能会互相影响
- 引发 ABA 问题:这是 CAS 机制的最大的问题,解决方案就是加 Version 版本号
多线程
线程和进程区别
进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位
线程的状态
线程在执行过程中可以处于不同的状态。Java中的线程状态可以由Thread类的getState()
方法获得,主要包括以下几种状态:
-
新建(New):线程对象被创建后,但还未调用start()
方法启动线程时处于新建状态。
-
运行(Runnable):线程调用start()
方法后,进入可运行状态。在可运行状态下,线程可能正在执行,也可能在等待CPU分配时间片。
-
阻塞(Blocked):线程在某些条件下被暂停执行,进入阻塞状态。常见的情况包括线程等待锁对象、调用sleep()
方法、等待输入输出等待。
-
等待(Waiting):线程处于等待状态,等待其他线程发出特定的通知或指定的时间到达。比如调用wait()
方法、join()
方法等。
-
计时等待(Timed Waiting):线程等待指定的时间或一段时间内的某个事件发生,处于计时等待状态。比如调用sleep()
方法、join(long millis)
方法等。
-
终止(Terminated):线程执行完毕或因异常退出后,进入终止状态。
需要注意的是,具体线程的状态可能会因操作系统调度、同步机制等因素而有所变化,这只是一种大致的分类。在实际应用中,使用合适的线程状态可以帮助管理线程的执行和同步。
什么是线程?线程和进程的区别?
**进程:**具有一定独立功能的程序关于某个数据集合上的一次运行活动,是操作系统进行资源分配和调度的一个独立单位
**线程:**是进程的一个实体,是 cpu 调度和分配的基本单位,是比进程更小的可以独立运行的基本单位。
**特点:**线程的划分尺度小于进程,这使多线程程序拥有高并发性,进程在运行时各自内存单元相互独立,线程之间内存共享,这使多线程编程可以拥有更好的性能和用户体验
反射是什么
反射是一种可以间接操作目标对象的机制。当使用反射时,JVM 在运行的时候才动态加载类,对于任意类,知道其属性和方法,并不需要提前在编译期知道运行的对象是谁,允许运行时的 Java 程序获取类的信息并对其进行操作。
对象的类型在编译期就可以确定,但程序运行时可能需要动态加载一些类(之前没有用到,故没有加载进 jvm),使用反射可以在运行期动态生成对象实例并对其进行操作。
2 反射的原理
在获取到 Class 对象之后,反向获取和操作对象的各种信息。
collection下面有什么
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wB7wa0ia-1689232139828)(C:\Users\29232\AppData\Roaming\Typora\typora-user-images\image-20230712081713425.png)]
创建线程有几种方式
- 继承 Thread 类并重写 run 方法创建线程,实现简单但不可以继承其他类
- 实现 Runnable 接口并重写 run 方法。避免了单继承局限性,编程更加灵活,实现解耦
- 实现 Callable 接口并重写 call 方法,创建线程。可以获取线程执行结果的返回值,并且可以抛出异常
- 使用线程池创建(使用 java.util.concurrent.Executor 接口)
Runnable 和 Callable 的区别
- Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,支持泛型
- Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以捕获异常信息
如何启动一个新线程、调用 start 和 run 方法的区别
- 线程对象调用 run 方法不开启线程。仅是对象调用方法。
- 线程对象调用 start 开启线程,并让 jvm 调用 run 方法在开启的线程中执行
- 调用 start 方法可以启动线程,并且使得线程进入就绪状态,而 run 方法只是 thread 的一个普通方法,还是在主线程中执行
线程的生命周期
- 创建:当程序使用 new 关键字创建了一个线程之后,该线程就处于一个新建状态
- 就绪:当线程对象调用了 Thread.start() 方法之后,该线程处于就绪状态
- 运行:添加到了线程列表中,如果 OS 调度选中了,就会进入到运行状态
- 阻塞:阻塞状态是线程因为某种原因放弃 CPU 使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况大概三种:
- 等待阻塞:运行的线程执行 wait() 方法,JVM 会把该线程放入等待池中
- 同步阻塞:运行的现场在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池中
- 其他阻塞:运行的线程执行 sleep() 或 join() 方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep() 状态超时、join() 等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态
- 死亡:线程会在一下三种方式之一结束,结束后就处于死亡状态
- run() 方法执行完成,线程正常结束
- 线程抛出一个未捕获的 Exception 或 Error
- 直接调用该线程的 stop() 方法来结束该线程 ------ 该方法容易导致死锁,通常不推荐使用
wait() 和 sleep() 的区别
- 来自不同的类
- wait() 来自 Object 类
- sleep() 来自 Thread 类
- 关于锁的释放
- wait() 在等待的过程中会释放锁
- sleep() 在等待的过程中不会释放锁
- 使用的范围
- wait() 必须在同步代码块中使用
- sleep() 可以在任何地方使用
- 是否需要捕获异常
- wait() 不需要捕获异常
- sleep() 需要捕获异常
- 参数不同
- wait() 不需要接收时间参数
- sleep() 需要接收时间参数
synchronized 的原理
synchronized 是基于对象实现的
通过编译可以看到 monitorenter(监视器入口)、monitorexit(监视器出口),线程在获取 monitor(监视器)前,会先查看 monitor(监视器)的进入数是否为 0,如果是 0 则进入,进入数+1,线程称为 monitor 的拥有者,其他线程再想进入的话,就会阻塞等待,此时如果拥有 monitor 监视器的线程再进入的话进入数就会再+1,所以,monitor 是可以重复进入的,进入的依据就是,线程在初次进入监视器的时候,监视器会记录线程 ID,之后线程再进入的时候,就会判断线程 ID 是否一致,一致可进入。monitorexit 只有 monitor 拥有者才可以调用,调用一次,monitor 进入数-1,当 monitor 为 0 时,释放锁
线程安全的集合
Collections 包装方法,Collections 工具类中提供了相应的包装方法把它们包装成线程安全的集合
List<E> synArrayList = Collections.synchronizedList(new ArrayList<E>());
Set<E> synHashSet = Collections.synchronizedSet(new HashSet<E>());
Map<K,V> synHashMap = Collections.synchronizedMap(new HashMap<K,V>());
什么是死锁?死锁产生的原因
- 什么是死锁
- 死锁是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待的现象
- 死锁产生的原因
- 系统资源的竞争:通常系统中拥有的不可剥夺资源,其数量不足以满足多个进程运行的需要,使得进程在运行过程中,会因争夺资源而陷入僵局,如磁带机、打印机等。只有对不可剥夺资源的竞争才可能产生死锁,对可剥夺资源的竞争是不会引起死锁的
- 进程推进顺序非法:进程在运行过程中,请求和释放资源的顺序不当,也同样会导致死锁。例如:并发进程 P1、P2 分别保持了资源 R1、R2,而进程 P1 申请资源 R2,进程 P2 申请资源 R1 时,两者都会因为所需资源被占用而阻塞
如何解决死锁问题
- 加锁顺序(线程按照一定的顺序加锁,只有获得了从顺序上排在前面的锁之后,才能获取后面的锁)
- 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
- 死锁检测(判断系统是否处于死锁状态)
- 死锁避免(指进程在每次申请资源时判断这些操作是否安全。例如银行家算法:在分配资源之前先看清楚,资源分配后是否会导致系统死锁。如果会死锁,则不分配。否则就分配)
二、Spring
Spring 是什么?
Spring 是一个分层的 JavaSE 轻量级开源框架
Spring 为不同的层都提供了企业级解决方案
- Web 层:Spring MVC
- Service 层:Spring
- Dao 层:Mybatis
Spring 的核心思想是 IOC(控制反转)和 AOP(面向切面编程)
Spring 的优势
- 方便解耦,简化开发
- Spring 就是一个大工厂(容器),用于创建对象(bean)和维护对象间的依赖关系
- AOP 编程的支持
- Spring 提供面向切面编程,可以实现对程序权限拦截、运行监控等功能
- 声明式事务的支持,只需要通过配置就可以完成对事务的管理,而无需手动编程
- 方便程序的调试
- Spring 对 Junit4 的支持,可以通过注解方便的测试 Spring 程序
- 方便集成各种优秀框架
- Spring 支持各种优秀框架(如:SpringMVC、MyBatis、SpringBoot、SpringCloud等)
- 降低 JavaEE API 的使用难度
- 封装了一些 API(JDBC、JavaMail 等)降低开发难度
IoC 和 DI
- ioc 全称叫控制反转,在传统的 Java 程序开发里面呢,我们只能通过 new 关键字去创建一个对象,这种方式会导致程序里面对象的依赖关系比较复杂,而且耦合度比较高,而 ioc 的作用就是实现了对象的管理,也就是把我们设计好的对象交给 ioc 容器来控制,然后在需要用到目标对象的时候直接从容器里面去获取,有了ioc容器来管理 bean 以后,相当于把对象的创建和查找依赖对象的控制交给了容器,这种设计理念使得对象和对象之间是一种松耦合的状态,极大的提升了程序的灵活性以及功能的复用性
- DI 表示依赖注入,也就是对于 ioc 容器中管理的 bean,如果 bean 之间存在依赖关系,那么 ioc 容器需要自动实现依赖对象的实例注入,通常我们有三种方式去描述 bean 和 bean 之间的依赖关系,第一种是接口注入,第二种是 setter 注入,第三种是构造器注入,另外为了更加灵活的去实现 bean 的实例的依赖注入,Spring 还提供了 @Resource、@Autowired 这样一个注解,分别去根据 bean 的name 和 bean 的类型去实现依赖注入
IOC 和 AOP
IOC:即控制反转,不是什么技术,而是一种设计思想,IOC 意味着将你设计好的对象交给容器控制,而不是 new
AOP:面向切面编程,他是一种编程思想,在不修改原业务逻辑的情况下,对原有业务方法进行增强
如何实现 IOC 和 AOP
实现 IOC:准备一个 Map,然后在项目启动的时候,通过反射,将对象放入 Map 中,未来在项目中需要使用对象的时候,就去 Map 中获取
实现 AOP:使用代理模式,代理模式分为静态代理和动态代理,动态代理的具体实现方式是 JDK 动态代理和 CGLIB 动态代理。
实现 AOP 必须使用动态代理,Spring 默认使用 JDK 动态代理,但是可以通过配置修改成 CGLIB 动态代理
那里用到了 AOP
常见的 AOP 应用常见:
- 记录操作日志:
- 缓存处理
- Spring 中内置的事务处理
我在项目中是这样用的,统一异常处理:
token 在 gateway 的校验,将 token 解析之后放入 ThreadLocal,未来需要使用用户信息的时候,直接从 ThreadLocal 中获取
springbuffer和springbuilder区别
StringBuffer
是线程安全的,所有的方法都被 synchronized
修饰,因此可以在多线程环境下使用,保证了线程安全,但也带来了一定的性能损失。
StringBuilder
不是线程安全的,它的方法没有使用同步修饰符,因此在单线程环境下使用效率更高。如果在单线程下进行字符串的操作,StringBuilder
是更好的选择。
总结一下,StringBuffer
是线程安全的,适用于多线程环境,但性能相对较低。StringBuilder
不是线程安全的,适用于单线程环境,性能较高。
需要根据具体的使用场景和线程安全的需求来选择合适的类。如果在多线程环境中进行字符串操作或需要线程安全保证,应使用 StringBuffer
;如果在单线程环境下进行字符串操作且追求更高的性能,应使用 StringBuilder
。
ThreadLocal 的原理
SpringMVC 中每一次请求都会开辟一条线程,ThreadLocal 为每个线程开辟独立的存储空间,多条线程间互不影响,互不干扰,在这条线程中,获取的用户数据是一致的
JDK 动态代理和 CGLIB 动态代理的区别是啥?
JDK 动态代理必须要有接口才能够代理
CGLIB 动态代理可以对任何类生成代理类
Spring 的常用注解
- 声明 Bean 的注解
- @Component(com 破嫩特):将修饰的资源交予 Spring 管理,value 属性:为资源命名(唯一标识)
- @Controller:衍生注解,与 @Component 作用和属性相同,特用于修饰表示层的资源
- @Service:衍生注解,与 @Compoent 作用和属性相同,特用于修饰业务逻辑层的资源
- @Repository(瑞趴日退):衍生注解,与 @Compoent 作用和属性相同,特用于修饰数据访问层的资源
- 注入 Bean 的注解
- @Autowired:由 Spring 提供,根据属性类型进行自动装配
- @Qualifier:根据名称进行注入,需要和 @Autowired 一起使用
- @Resource:可以根据类型注入,也可以根据名称注入
- 配置类相关注解
- @Configuration:声明当前类为配置类,内部组合了 @Compoent 注解,表明这个类是一个 Bean
- @Bean:注解在方法上,声明当前方法的返回值为一个 Bean
- @ComponentScan:用于对组件进行扫描
- 切面(AOP)相关注解
- @Aspect(啊死白特):声明一个切面(类上)
- @After:在方法执行之后执行(方法上)
- @Befor:在方法执行之前执行(方法上)
- @Around(额ruang得):在方法执行之前与之后执行(方法上)
- @Value
Spring 框架中都用到了哪些设计模式
- 工厂模式:BeanFactory 就是简单工厂模式的体现,用来创建对象的实例
- 单例模式:Bean 默认为单例模式
- 代理模式:Spring 的 AOP 功能用到了 JDK 的动态代理和 CGLIB 字节码生成技术
- 模板方法:用来解决代码重复的问题。比如:RestTemplate,JmsTemplate
Spring 事务的实现方式和实现原理
Spring 事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,Spring 是无法提供事务功能的,真正的数据库层的事务提交和回滚是通过 binlog 或者 redolog 实现的
Spring 事务实现主要有两种方式
- 编程式:beginTransaction()、commit()、rollback() 等事务管理相关的方法
- 声明式:利用注解 @Transactional 或者 aop 配置
Spring 中事务失效的场景有哪些?
- 如果方法上异常捕获处理,自己处理了异常,没有抛出,就会导致事务失效,所以一般处理了异常之后,别忘了抛出去就行了
- 如果方法抛出检查异常,如果报错也会导致事务失效,最后在 Spring 事务的注解上,也就是 @Transactional 上配置 rollbackFor 属性为 Exception,这样别管是什么异常,都会回滚事务
- 如果方法上不是 public 修饰的,也会导致事务失效
Spring 框架中的单例 bean 是线程安全的吗
不是线程安全的
- Spring 框架中有一个 @Scope 注解,默认的值就是 singleton,单例的。因为一般在 Spring 的 bean 中的都是注入无状态的对象,没有线程安全问题,如果在 bean 中定义了可修改的成员变量,是要考虑线程安全问题的,可以使用多例或者加锁来解决
Bean 的生命周期
- 生命周期
- Bean 生命周期
- 通过构造器创建 bean 实例(无参数构造)
- 为 bean 的属性设置值和对其他 bean 引用(调用 set 方法)
- 调用 bean 的初始化方法(需要进行配置)
- bean 可以使用了(对象获取到了)
- 当容器关闭的时候,调用 bean 的销毁方法(需要进行配置销毁的方法)
静态代理和动态代理的区别
- 静态代理需要手动编写代理类,而动态代理是在运行时生成的
- 静态代理在编译时已经确定,无法在运行时进行更改,而动态代理在运行时可以根据需要动态生成代理类
- 静态代理只能代理一个特定的接口或父类,而动态代理可以代理多个接口或父类
- 静态代理的代码相对固定,而动态代理的代码相对更加灵活和可扩展
三、SpringMVC
SpringMVC 执行流程
版本1:视图版本,jsp
-
当用户通过浏览器发起一个HTTP请求,请求直接到前端控制器DispatcherServlet;
-
前端控制器接收到请求以后调用处理器映射器HandlerMapping,处理器映射器根据请求的URL找到具体的Handler(汉得乐),并将它返回给前端控制器;
-
前端控制器调用处理器适配器HandlerAdapter去适配调用Handler;
-
处理器适配器会根据Handler去调用真正的处理器去处理请求,并且处理对应的业务逻辑;
-
当处理器处理完业务之后,会返回一个ModelAndView对象给处理器适配器,处理器适配器 HandlerAdapter再将该对象返回给前端控制器;这里的Model是返回的数据对象,View是逻辑上的View。
-
前端控制器DispatcherServlet将返回的ModelAndView对象传给视图解析器ViewResolver进行解析,解析完成之后就会返回一个具体的视图View给前端控制器。(ViewResolver根据逻辑的View查找具体的View)
-
前端控制器DispatcherServlet将具体的视图进行渲染,渲染完成之后响应给用户(浏览器显示)。
版本2:前后端开发,接口开发
- 用户发送请求到前端控制器(DispatcherServlet)
- 前端控制器收到请求调用处理器映射器(HandlerMapping)
- 处理器映射器找到具体的处理器,生成处理器对象及处理器拦截器(如果有),再一起返回给前端控制器
- 前端控制器调用处理器适配器
- 处理器适配器经过适配调用具体的处理器(Handler/Controller)
- 方法上添加了 @ResponseBody
- 通过 HttpMessageConverter 来返回结果转换为 JSON 并响应
讲一下 MVC
MVC 是一种常用的软件架构模式,它包含三个核心部分 Model、View 和 Controller
- Model(模型):表示应用程序中用于处理和管理数据的部分。通常包括数据模型及其相关操作,例如数据的存储、读取、更新等
- View(视图):是用户界面,在 MVC 架构中通常指的是用户界面设计及其显示效果。VIew 通常负责接收用户的输入,将数据传递给 Controller 处理,并显示结果给用户
- Controller(控制层):负责连接模型和视图,并协调它们之间的交互。Controller 通常会处理来自 View 的请求,对数据进行处理后将结果反馈给 View。
cookie 和 session 的区别
- 存储位置不同
- cookie 存放在客户端电脑,是一个磁盘文件。
- session 是存放在服务器内存中的一个对象。
- 存储容量不同
- 单个 cookie 保存的数据 <=4KB,一个站点最多保存 20个 Cookie
- 对于 session 来说并没有上限,但出于对服务器端的性能考虑,session 内不要存放过多的东西
- 存储方式不同:
- cookie 中只能保管 ASCII 字符串,并需要通过编码方式存储为 Unicode 字符或者二进制数据
- session 中能够存储任何类型的数据,包括且不限于 string,integer,list,map 等
- 隐私策略不同
- cookie 对客户端是可见的,别有用心的人可以分析存放在本地的 cookie 并进行 cookie 欺骗,所以它是不安全的
- session 存储在服务器上,不存在敏感信息泄漏的风险
@Controller和@RestController区别
@Controller 注解修饰的类,通常返回一个 View,即指定该请求的响应结果是一个逻辑视图名,它会被 ViewResolver 解析成物理视图名,再交给视图技术(如 JSP、Thymeleaf 等)进行渲染。
而 @RestController 注解修饰的类,通常返回 JSON、XML 等格式的数据,他会被直接写入 HTTP 响应的消息体中,而不是返回逻辑视图名。
总的来说,Controller 适用于需要返回 HTML、JSP 等视图页面的场景,而 RestController 适用于需要返回 JSON、XML 等数据格式的场景,它们之间的主要区别在于返回的数据类型不同。
SpringMVC 常用注解
- @RequestMapping:用于映射请求路径,可以定义在类上和方法上,用于类上,则表示类中的所有方法都是以该地址为父路径
- @RequestBody:注解实现接收 http 请求的 json 数据,将 json 转换为 java 对象
- @RequestParam:指定请求参数的名称
- @PathViriable:从请求路径下中获取请求参数,传递给方法的形式参数
- @ResponseBody:注解实现将 controller 方法返回对象转化为 json 对象响应给客户端
- @RequestHeader:获取指定的请求头数据
- @RestController:@Controller+@ResponseBody
get 请求和 post 请求的区别是什么
- get 在 url 中拼凑请求参数,post 请求参数在请求体中
- get 请求参数加载路径上,可见不安全,post 请求参数在请求体中,一般人不可见,较安全
- get 提交数据有限,post 无限
- get 重点在向服务器获取资源,post 重点在向服务器发送资源
四、SpringBoot
SpringBoot 是什么
是 Spring 的子项目,主要简化 Spring 开发难度,去掉了繁重的配置,提供各种启动器,可以让程序员更快上手,节省开发时间
SpringBoot 的优点
SpringBoot 对上述 Spring 的缺点进行改善和优化,基于约定优于配置的思想,可以让开发人员不必在配置和逻辑业务之间进行思维的切换,全身心的投入到逻辑业务的代码编写中,从而大大提高了开发效率,一定程度上缩短了项目周期
Spring Boot 的优点:
- 简化开发:Spring Boot 通过自动化配置和约定大于配置的原则,大大简化了应用程序的开发和部署,开发者可以更专注于业务逻辑而非底层配置
- 快速启动:Spring Boot 提供了快速启动和嵌入式服务器的特性,可以快速构建和运行独立的、自包含的应用程序
- 生态系统丰富:Spring Boot 与 Spring 框架无缝集成,并且拥有庞大的生态系统,提供了大量的插件和第三方库,可以满足各种需求
Spring Boot 的缺点:
- 不适用于大型系统:虽然 Spring Boot 适用于快速构建小型和中型应用程序,但对于大型系统和复杂的业务需求,可能需要更多的定制和扩展
- 依赖冲突问题:由于 Spring Boot 自动引入了许多依赖,可能会导致依赖冲突的问题,需要进行版本管理和解决
SpringBoot 常用的注解
SpringBoot 的核心注解是 @SpringBootApplication,它由几个注解组成:
- @SpringBootConfiguration:结合了 @Configuration 注解,实现配置文件的功能
- @EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项
- @CompontScan:Spring 组件扫描,默认扫描当前引导类所在包及其子包
SpringBoot 自动装配原理
- 在 Spring Boot 项目中的引导类上有一个注解 @SpringBootApplication,这个注解是对三个注解进行了封装,分别是:
- @SpringBootConfiguration
- @EnableAutoConfiguration
- @ComponentScan
- 其中 @EnableAutoConfiguration 是实现自动化配置的核心注解,该注解通过 @Import 注解导入对应的配置选择器。内部就是读取了该项目和该项目引用的 jar 包的 classpath 路径下的 META-INF/spring.factories(读fai可突瑞丝) 文件中的所配置的类的全类名,在这些配置类中所定义的 bean 会根据条件注解所指定的条件来决定是否需要将其导入到 Spring 容器中
- 条件判断会有像 @ConditionalOnClass 这样的注解,判断是否有对应的 class 文件,如果有则加载该类,把这个配置类的所有 bean 放入容器中使用
五、SpringCloud
SpringCloud 有哪些核心组件
早期我们一般认为的 SpringCloud 五大组件是
- Eureka:注册中心,服务注册和发现
- Ribbon:负载均衡,实现服务调用的负载均衡
- Hystrix:熔断器
- Feign:远程调用
- Zuul(肉):网关
随着 SpringCloudAlibaba 在国内的兴起
- Nacos:注册中心/配置中心
- 负载均衡:Ribbon
- 服务调用:Feign
- 服务保护:Sentinel
- 服务网关:Gateway
Ribbon 负载均衡策略有哪些
- RoundRobinRule:简单轮询服务列表来选择服务器
- WeightedResponseTimeRule:按照权重来选择服务器,响应时间越长,权重越小
- RndomRule:随机选择一个可用的服务器
- ZoneAvoidanceRule:区域铭感策略,以区域可用的服务器为基础进行服务器的选择
CAP定理
CAP 主要是分布式项目下的一个理论。包含了三项,一致性、可用性、分区容错性
- 一致性:用户访问分布式系统中的任意节点,得到的数据必须一致
- 可用性:用户访问集群中的任意健康节点,必须能得到响应,而不是超时或拒绝
- 分区容错性
- 分区:因为网络故障或其他原因导致分布式系统中部分节点与其他节点失去连接,形成独立分区
- 容错:在集群出现分区时,整个系统也要持续对外提供服务
这三个条件不能同时满足,最多只能满足其中两个,于是就产生了 BASE 理论
BASE 理论
BASE 理论是对 CAP 的一种解决思路,包含三个思想
- Basically Available(基本可用):分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用
- Soft State(软状态):在一定时间内,允许出现中间状态,比如临时的不一致状态
- Eventually Consistent(最终一致性):虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致
Seata 的事务模型有哪些?区别是啥?
XA、AT、TCC、SAGA
-
XA 模型的优点
- 事务的强一致性,满足 ACID 原则
- 常用的数据库都支持,实现简单,并且没有代码侵入
-
XA 模型的缺点
- 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差
- 依赖关系型数据库实现事务,如果我使用了不支持 XA 模式的数据库,比如 redis、mongoDB,这种类型的数据库,就无法使用 XA 模式进行分布式事务的控制
-
AT 模式的优点
- 一阶段完成直接提交事务,释放数据库资源,性能比较好
- 利用全局锁实现读写隔离
- 没有代码侵入,框架自动完成回滚和提交
-
AT 模式的缺点
- 两阶段之间属于软状态,属于最终一致,可能会引起脏写
- 框架的快照功能会影响性能,但比 XA 模式要好很多
-
TCC 模式的每个阶段是做什么的?
- Try:资源检查和预留
- Confirm:业务执行和提交
- Cancel:预留资源的释放
-
TCC 的优点
- 一阶段完成直接提交事务,释放数据库资源,性能好
- 相比 AT 模型,无需生成快照,无需使用全局锁,性能最强
- 不依赖数据库事务,而是依赖补偿操作,可以用于非事务性数据库
-
TCC 的缺点
- 有代码侵入,需要人为编写 Try、Confirm 和 cancel 接口,太麻烦
- 软状态,事务是最终一致
- 需要考虑 Confirm 和 Cancel 的失败情况,做好幂等处理
SAGA 了解比较少
Spring、SpringBoot、SpringCloud 区别
- Spring 是 JavaEE 的轻量级开发框架,主营 IoC 和 AOP,集成 JDBC、ORM、MVC 等功能便于开发
- SpringBoot 是一个基于 Spring 的组件,它帮我们预组装了 Spring 的一系列组件,以便以尽可能少的代码和配置来开发 Spring 的 Java 应用程序。
- 以汽车为例,如果我们想组装一辆汽车,我们需要发动机、座椅、内饰等各种不见,然后把它们装配起来。Spring 就相当于提供了一系列这样的部件,但是要装好汽车上路,还需要我们自己动手,而 SpringBoot 则相当于已经帮我们预装好了一辆可以上路的汽车,如果有特殊的要求,可以通过修改配置或编写少量代码完成。因此,SpringBoot 和 Spring 的关系就像是整车和零部件的关系,它们不是取代关系,试图跳过 Spring 直接学习 SpringBoot 是不可能的
- SpringBoot 专注于快速方便的开发单个个体服务,SpringCloud 是关注全局的微服务协调治理框架,它将 SpringBoot 开发的一个个单体微服务整合并管理起来,为各个微服务之间提供、配置管理、服务发现、熔断器、路由等服务。SpringBoot 可以单独使用,它不依赖于 SpringCloud;而 SpringCloud 必然依赖于 SpringBoot,属于依赖关系
Nacos 心跳机制
什么是心跳呢?就是为了证明“自己”的存在,会给服务端发送心跳包,nacos 的心跳机制是怎样的?
- 先启动我们的 nacos server
- 在启动微服务,微服务会向我们的 nacos 建立一次 http 连接,并且发送心跳请求。这么做是为了证明我们的网络和服务是正常的
- nacos 则将微服务发送过来的相关信息和本次的心跳时间记录下来
- 微服务向 nacos server 进行服务注册,并且每 5秒发送一次心跳包
- nacos 有一个定时线程,不断地监听所有注册上来的微服务,用我们 nacos 的系统时间减去最新一次的心跳时间,和我们本地设置的阈值进行对比。(默认阈值 15秒),如果大于15秒,nacos 则会认为,当前的服务处于一个不健康的状态
- 当 nacos 的系统时间减去最后的一次心跳时间,如果大于30秒,nacos 则会认为该服务已经下线了,会直接摘除该服务
Spring Cloud 的加载流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gZSDLfsz-1689232139829)(C:\Users\A\AppData\Roaming\Typora\typora-user-images\1688699618280.png)]
六、MyBatis
#{} 和 ${} 的区别是什么
- #{} 是预编译处理,${} 是字符串替换
- MyBatis 在处理 #{} 时,会对 SQL 语句进行预处理,将 SQL 的 #{} 替换为 ?号,调用 PreparedStatement 的 set 方法来赋值,MyBatis 在处理 ${} 时,就是把 ${} 直接替换成变量的值
- 在某些特定情况下必须使用 ${},比如在分表存储的情况下,对那张表的查询是不确定的,也就是 sql 语句不能写死,表名是动态的,查询条件是固定的,此时表名只能使用 ${} 方式进行字符串拼接
- 使用 #{} 可以有效地防止 SQL 注入,提高系统安全性
MyBatis 中的动态 SQL 是什么意思
在 xml 配置文件中,根据数据的不同,动态拼凑对应的 sql 语句
MyBatis-Plus 如何分页
MyBatis Plus 自带分页插件,只要简单的配置即可实现自动分页功能
准备工作:添加配置类
@Configuration
@MapperScan("com.atguigu.mybatisplus.mapper")
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new
PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
测试1:
@Test
public void testPage() {
Page<User> page = new Page<>(1, 5);
userMapper.selectPage(page, null);
List<User> list = page.getRecords();
list.forEach(System.out::println);
System.out.println("当前页: " + page.getCurrent());
System.out.println("每页显示的条数: " + page.getSize());
System.out.println("总记录数: " + page.getTotal());
System.out.println("总页数: " + page.getPages());
System.out.println("是否有上一页: " + page.hasPrevious());
System.out.println("是否有下一页: " + page.hasNext());
}
xml 自定义分页
1.到 mapper 接口中定义接口方法
@Mapper
public interface UserMapper extends BaseMapper<User> {
Page<User> selectPageVo(@Param("page") Page<User> page, @Param("age") Integer age);
}
2.在 xml 文件中编写 sql
<sql id="BaseColumns">id,username,age,emailsql>
<select id="selectPageVo" resultType="User">
SELECT <include refid="BaseColumns">include> FROM t_user WHERE age > # {age}
select>
3.测试
@Test
public void testSelectPageVo() {
Page<User> page = new Page<>(1, 5);
userMapper.selectPageVo(page, 20);
List<User> list = page.getRecords();
list.forEach(System.out::println);
System.out.println("当前页:" + page.getCurrent());
System.out.println("每页显示的条数:" + page.getSize());
System.out.println("总记录数:" + page.getTotal());
System.out.println("总页数:" + page.getPages());
System.out.println("是否有上一页:" + page.hasPrevious());
System.out.println("是否有下一页:" + page.hasNext());
}
七、RabbitMQ
RabbitMQ中的几种工作模式
- Hello World:简单模式,一个生产者发送消息到一个队列,一个消费者从该队列接收并处理消息
- work queues:工作队列模式,多个消费者从同一个队列接收消息进行处理,实现负载均衡
- pub/sub:发布订阅模式,一个生产者发送消息到一个交换机,交换机将消息广播给所有绑定的队列
- routing:路由模式,生产者发送消息到交换机,并使用路由键将消息发送到与之匹配的队列
- topics:通配符模式,生产者发送消息到交换机,并使用主题进行灵活的消息路由
RabbitMQ 实际运用场景
MQ 的优势和劣势
优势:
劣势:
RabbitMQ 如何保证消息不丢失
- 第一个是开启生产者确认机制,确保生产者的消息能到达队列,如果报错可以先记录到日志中,再去修复数据
- 第二个是开启持久化功能,确保消息未消费前在队列中不会丢失,其中的交换机、队列和消息都要做持久化
- 第三个是开启消费者确认机制为 auto,由 Spring 确定消息处理之后完成 ack,当然也需要设置一定的重试次数,我们当时设置了 3次,如果重试 3次还没有收到消息,就将失败后的消息投递到异常交换机,交由人工处理
MQ 如何防止消息重复消费
防止重复消费,就是要做幂等设计
- 生产者生产消息的时候,给消息多加一个全局唯一 ID,一并发送到 MQ 中
- 消费者去 MQ 中取出消息的时候,先根据全局唯一 ID 去 Redis 中查找消费记录,如果没有找到,就消费消息,并将消费记录保存到 Redis 中;如果找到了,就不能消费消息
八、ElasticSearch
ElasticSearch 原理?
倒排索引,在存文档/数据的时候,对数据进行分词,并记录每个分词下的文档编号,查找的时候通过分词找到对应的倒排列表的文档编号,然后根据文档编号去索引库里面找出对应的文档记录
为什么要使用 ElasticSearch
- 数据库 MySQL,Redis 无法使用分词搜索,ElasticSearch 可以实现分词搜索
- 数据量大+速度快:数据库 MySQL 单表最多放 1000w 条,如果数据量再多的话,搜索性能和速度就会下降,而使用 ElasticSearch,数据量没有上限,而且数据非常快
为什么要使用ik分词器?
ElasticSearch 默认单字分词,不符合项目需求,而 ik 中文分词器采用正向迭代最细粒度算法,每秒可以处理 160w 字分词,可以根据语义分词,所以使用ik分词器
ElasticSearch 是如何搜索的
使用 ElasticSearch,首先需要创建索引,然后 springboot-data-elasticsearch 提供了丰富的 API,不仅可以进行查找所有、分页查找、排序,还可以直接编写方法名,根据方法名搜索,如果直接编写方法名方式不足以完成功能,springboot-data-elasticsearch 还提供了很多查询方法,比如:
- matchQuery:先分词再查找
- termQuery:词条匹配
- fuzzyQuery:模糊匹配,允许错字
- booleanQuery:组合搜索
- rangeQuery:范围搜索
九、Redis
Redis 的数据类型
- String 类型:字符串类型,Redis 中最常用的类型,可以包含任何数据,一个 key 对应一个 value
- list:顺序链表,一键多值,值可以重复
- set:无序集合,一键多值,值不可以重复
- zset:有序不重复的集合,一键多值
- hash:键值对,key 后面跟 field 属性,从而确定一个值
Redis 各种数据类型应用场景
- String:粉丝数、点赞数
- List:消息队列、日志有序输出
- set:黑名单
- Hash:商城购物车,以用户 id 作为购物车的 key,商品的 id 作为 field,value 就是购物车中商品的详细信息,当加入购物车时候,根据用户 id 和商品 id 查找,如果找到了,直接更新商品数量,如果没有找到,直接将商品信息添加到购物车
- zSet:排行榜、热搜
Redis 缓存穿透、缓存击穿、缓存雪崩
- 缓存穿透
- 概述:缓存穿透指对于一个不存在于缓存中和数据库中的键进行查询,导致每次查询都要访问数据库,从而增加数据库负载
- 解决:可以使用布隆过滤器预先过滤掉一部分明显不存在的键
- 缓存击穿
- 概述:缓存击穿指的是一个热点数据过期或失效,同时又有大量并发请求访问该数据,导致请求直接击穿到数据库上,增加数据库负载
- 解决:缓存 null,当某一个请求去数据库查询的时候,同时在 redis 中将只设为 null,待它从数据库查到数据返回后,设置具体的值
- 缓存雪崩
- 概述:缓存雪崩指的是在某个时间点,缓存中的大量键同时过期或失效,导致大量请求直接落到数据库上,造成数据库瞬时压力过大甚至崩溃
- 解决:设置缓存过期时间随机化,避免大量键同时失效,比如固定的过期时间上再随机 1~10 分钟
Redis 作为缓存,MySQL 的数据如何与 Redis 进行同步
1.采用延时双删策略
2.采用阿里云的 canal 组件实现数据同步,不需要更改业务代码,部署一个 canal 服务。canal 服务把自己伪装成 MySQL 的一个从节点,当 MySQL 数据更新以后,canal 会读取 binlog 数据,然后再通过 canal 的客户端获取到数据,更新缓存即可
Redis 作为缓存,数据的持久化是怎么做的
Redis 中提供了两种数据持久化的方式:RDB、AOF
RDB 是一个快照文件,他是把 Redis 内存存储的数据写到磁盘上,当 Redis 实例宕机恢复数据的时候,方便从 RDB 的快照文件中恢复数据
AOF 的含义是追加文件,当 Redis 操作写命令的时候,都会存储在这个文件中,当 Redis 实例宕机恢复数据的时候,会对这个文件中再次执行命令以便来恢复数据
RDB 因为是二进制文件,在保存的时候体积也是比较小的,它恢复的比较快,但是它可能会丢数据,我们通常在项目中也会使用 AOF 来恢复数据,虽然 AOF 恢复的慢一些,但是它丢数据的风险要小很多,在 AOF 文件中可以设置刷盘策略,我们当时设置的就是每秒批量写入一次命令
RDB 备份中 save 和 bgsave 的区别
- save 会阻塞 redis 进程,知道 rdb 备份完成之后,才会释放资源,在进程阻塞期间,redis 不能处理任何命令请求
- bgsave 则是 fork 一个子进程,然后由子进程负责生成 rdb 文件,父进程继续处理命令请求,不会阻塞进程
Redis 的数据过期策略有哪些
Redis 中提供了两种数据过期删除策略
-
第一种是惰性删除,在设置该 key 过期时间后,我们不去管他,当需要该 key 的时候,我们再检查其是否过期,如果过期,我们就删掉它,反之返回该 key
-
第二种是定期删除,就是说每隔一段时间,我们就对一些 key 进行检查,删除里面过期的 key
- 定期清理的两种模式:
- SLOW 模式是定时任务,执行频率默认为 10hz,每次不超过 25ms,可以通过修改配置文件 redis.conf 的 hz 选项来调整这个次数
- FAST 模式执行频率不固定,每次事件循环都尝试执行,但两次间隔不低于 2ms,每次耗时不超过 1ms
Redis 的过期删除策略:惰性删除 + 定期删除两种策略进行配合使用
Redis 的数据淘汰策略有哪些
这个在 Redis 中提供了很多种,默认是 noeviction,不删除任何数据,内部不足直接报错
是可以在 Redis 的配置文件中进行设置的,里面有两个非常重要的概念,一个是 LRU,另外一个是 LFU
- LRU 的意思是最少最近使用,用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高
- LFU 的意思是最少频率使用,会统计每个 key 的访问频率,值越小淘汰优先级越高
我们在项目设置的 allkeys-lru,挑选最近最少使用的数据淘汰,把一些经常访问的 key 留在 Redis 中
面试题:数据库有 1000万条数据,Redis 只能缓存 20w数据,如何保证 Redis 中的数据都是热点数据?
可以使用 allkeys-lru(挑选最近最少使用的数据淘汰)淘汰策略,那留下来的就是经常访问的热点数据
Redis 的内存用完了会发生什么
这个要看 Redis 的数据淘汰策略是什么?如果是默认的配置,Redis 内存用完以后则直接报错,我们当时设置的 allkeys-lru 策略,把最近最长访问的数据留在缓存中
Redis 分布式锁如何实现
在 Redis 中提供了一个命令 setnx,由于 Redis 是单线程的,用了命令之后,只能有一个客户端对某一个 key 设置值,在没有过期或删除 key 的时候其他客户端是不能设置这个 key 的
如何控制 Redis 实现分布式锁有效时长
的确,Redis 的 setnx 指令不好控制这个问题,我们当时采用的 redis 的一个框架 redisson 实现的
在 redisson 中需要手动加锁,并且可以控制锁的失效时间和等待时间,当锁住的一个业务还没有执行完成的时候,在 redisson 中引入了一个看门狗机制,就是说每隔一段时间就检查当前业务是否还持有锁,如果持有就增加加锁的持有时间,当业务执行完成之后释放锁就可以了
还有一个好处就是,在高并发下,一个业务有可能会执行很快,先客户1持有锁的对象,客户2来了以后并不会马上拒绝,他会自旋不断尝试获取锁,如果客户1释放之后,客户2就可以马上持有锁,性能也得到了提升
Redis 读写速度非常快,原因是啥
- 使用内存进行读写
- Redis 是单线程+多路 IO复用技术
- 单线程避免了线程切换消耗的时间以及避免多线程的死锁问题的发生
- 为了加快速度,采用多路IO复用技术,多个黑窗口复用这条单线程,只有真正执行命令的时候,才会占用单线程的资源,其余时候不占用单线程资源,这样子提高了并发速度
Redis 的应用场景
- 缓存
- 限时业务的应用:如短信验证码,在有效时间结束后,自动删除数据
Redis 的优势
- 运行在内存,速度快
- 数据虽然在内存,但是提供了持久化的支持,即可以将内存中的数据异步写入到硬盘中
- 支持数据结构丰富
- String:字符串
- list:链表
- set:集合
- zset:有序集合
- hash:哈希类型
十、MySQL
数据库三范式
- 第一范式:保证每列的原子性,每列是不可再分割的单元
- 第二范式:保证每张表都有主键
- 第三范式,保证表中的每个字段都绝对依赖当前主键
说出不小于 3 种 mysql 支持的约束,并说明它们的应用场景
- 主键约束:在主键上使用
- 外键约束:建立两个表之间的关系时使用
- 唯一约束:保证数据表中某个字段的唯一性时使用
- 非空约束:需要保证数据库字段值不为空时使用
- 默认约束:当字段没有值的时候,设置默认值
- 检查约束:通过 check 检查插入的值是否合法
谈谈数据库连接池的作用
- 资源重用,避免了数据库连接频繁建立、关闭的开销
- 更快的系统响应速度,直接从连接池中获取连接,响应速度加快
- 控制资源的使用,连接池能自动维护池中的连接数量,提高资源的利用率
红黑树
- 必须保持黑高平衡:从任意节点触发,到其每个叶子节点的路径中包含相同数量的黑色节点
- 根节点必为黑色
- 叶子节点都为黑色,且为 null
- 连接红色节点的两个子节点都为黑色(红黑树不会出现相邻的红色节点)
- 新加入到红黑树的节点为红色节点
- 节点分为红色或者黑色
B树和B+树的区别
- 在 B 树中,非叶子节点和叶子节点都会存放数据,而 B+ 树的所有数据都会出现在叶子节点,在查询的时候,B+ 树查找效率更加稳定
- 在进行范围查询的时候,B+ 树效率更高,因为 B+ 树都在叶子节点存储,并且叶子节点是一个双向链表
索引是什么?
它是帮助 MySQL 高效获取数据的数据结构,主要是用来提高数据检索的效率,降低数据库的 IO 成本,同时通过索引列对数据进行排序,降低数据排序的成本,也能降低 CPU 的消耗
索引分类
- 主键索引:在数据表的主键字段创建的索引,这个字段必须被 primary key 修饰,每张表只能有一个主键
- 唯一索引:在数据表中的唯一列创建的索引(unique),此列的所有值只能出现一次,可以为 null
- 普通索引:在普通字段上创建的索引,没有唯一性的限制
- 组合索引:两个及以上字段组合起来创建的索引
什么是聚簇索引?什么是非聚簇索引?
- 聚簇索引主要是指数据与索引放在一块,B+ 树的叶子节点保存了整行数据,有且只有一个,一般情况下主键是作为聚簇索引的
- 非聚簇索引指的是数据与索引分开存储,B+ 树的叶子节点保存对应的主键,可以有多个,一般我们自己定义的索引都是非聚簇索引
聚簇索引选取规则
- 如果存在主键,主键索引就是聚簇索引
- 如果不存在主键,将使用第一个唯一(UNIQUE)索引作为聚簇索引
- 如果表没有主键,或没有合适的唯一索引,则 InnoDB 会自动生成一个 rowid 作为隐藏的聚集索引
什么是回表查询
其实跟聚簇索引和非聚簇索引是有关系的,回表的意思就是通过二级索引找到对应的主键值,然后再通过主键值找到聚簇索引中所对应的整行数据,这个过程就是回表
什么是索引覆盖
索引覆盖是指 select 查询语句使用了索引,在返回的列,必须在索引中全部能够找到,如果我们使用 id 查询,它会直接走聚簇索引查询,一次索引扫描,直接返回数据,性能高。
如果按照二级索引查询数据的时候,返回的列中没有创建索引,有可能会触发回标查询,尽量避免使用 select *,尽量在返回的列中都包含添加索引的字段
最左前缀原则
如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始并且不跳过索引中的列(口诀:带头大哥不能死,中间兄弟不能断)
索引的优缺点
- 优点
- 索引大大降低了数据库服务器在执行查询操作时扫描的数据数,提高查询效率
- 缺点
- 当数据表中数据发生 DML(添加、删除、修改)操作时,索引页需要更新
- 索引文件也会占用磁盘空间
索引的不适用场景
适用场景
- 加快条件的判断速度
- 在作为主键的列上,强制该列唯一
- 在经常需要排序的列上创建索引,因为索引以及排序
不适用场景
- 查询少的列
- 数据内容少的表
- text,image 和 bit 数据类型的列不应该增加索引
- 修改多于查询的列不能加索引
什么情况下索引会失效
- 索引在使用的时候没有遵循最左匹配法则
- 模糊查询,如果 % 号在前面也会导致索引失效
- 如果在添加索引的字段上进行了运算操作或者类型转换也都会导致索引失效
- 如果使用了复合索引,中间使用了范围查询,右边的条件索引也会失效
通常情况下,想要判断出这条 SQL 是否有索引失效的情况,可以使用 explain 执行计划来分析
事务的四大特性
数据库事务的四大特性为:ACID,分别是原子性、一致性、隔离性和持久性
- 原子性:一个事务中的所有操作,要么全部完成,要么全部不完成
- 一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏
- 隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致
- 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障数据也不会丢失
事务隔离级别
- 读未提交:一个事务可以读取到另一个事务未提交的数据,可能会导致出现脏读
- 不可重复读:一个事务只能读取到另一个事务已提交的数据,避免了脏读,但可能会导致出现虚读
- 可重复读:一个事务在结束之前其他事务不能修改对应的数据,避免了虚读,但可能会导致出现幻读
- 串行化:同时只允许一个事务对数据表进行操作;避免了脏读、虚读、幻读问题,但是该级别效率低,一般不使用
存储引擎
存储引擎有三种:
- InnoDB
- MyISAM:
- 不支持事务,不支持外键
- 支持表锁,不支持行锁
- 访问速度快
- Memory:表数据是存储在内存中的,由于受到硬件问题、或断电问题的影响,只能将这些表作为临时表或缓存使用
limit 分页
基本语法:limit m,n; 表示从 m 行开始(包含)查询,往下查询 n 条数据;
分页查询(查询第几页的数据):limit (m-1)*n, n;m 表示当前查询第几页,n 表示一页显示多少条数据
SQL 优化
- 加索引:增加索引是一种简单高效的手段,但是需要选择合适的列,同时避免导致索引失效的操作,比如 like、函数等
- 避免返回不必要的数据列,减少返回数据列可以增加查询的效率
- 根据查询分析器适当优化 SQL 的结构,比如是否走全表扫描,避免子查询等
- 分库分表:在单表数据量较大或者并发连接数过高的情况下,通过这种方式可以有效提升查询效率
- 读写分离:针对读多写少的常见,这样可以保证写操作的数据库承受更小的压力,也可以缓解独占锁和共享锁的竞争。
索引优化最佳实践:
- 全值匹配:MySQL 全值匹配是指在使用复合索引时,查询条件要包含索引的所有列,才能最大程度地利用索引
- 最左前缀法则:指的是查询从索引的最左前列开始并且不跳过索引中的列。(带头大哥不能死,中间兄弟不能断)
- 不在索引列上做任何操作(计算、函数、(自动 or 手动)类型转换),会导致索引失效而转向全表扫描
- 尽量使用覆盖索引(只访问索引的查询(索引列包含查询列)),减少 select * 语句。(覆盖索引不写星)
- mysql 在使用不等于(!= 或者 <>),not in,not exists 的时候无法使用索引会导致全表扫描,< 小于、> 大于、<=、>= 这些,mysql 内部优化器会根据检索比例、表大小等多个因素整体评估是否使用索引,is null、is not null 一般情况下也无法使用索引,少用 or 或 in,用它查询时,mysql 不一定使用索引,mysql 内部优化器会根据检索比例,表大小等多个因素整体评估是否使用索引,详情范围查询优化
- like 百分写最右
- 字符串不加单引号索引失效
SQL 优化最佳实践:
**避免使用 select ***
- 查询时需要先将星号解析成表的所有字段然后再查询,增加查询解析器的成本
- select * 查询一般不走覆盖索引会产生大量的回表查询
- 在实际应用中我们通常只需要使用某几个字段,其他不需要使用的也查出来浪费 CPU、内存资源
- 文本数据、大字段数据传输增加网络消耗
小表驱动大表
- 小表驱动大表就是指用数据量较小、索引比较完备的表,然后使用其索引和条件对大表进行数据筛选,从而减少数据计算量,提高查询效率。
用连接查询代替子查询
- mysql 需要在两张表以上获取数据的方式有两种:第一种通过链表查询获取,第二种通过子查询获取
- 因为子查询执行两次数据库查询,一次是外部查询,一次是嵌套子查询。因此,使用连接查询可以减少数据库查询的次数,提高查询的效率
批量操作
- 批量插入和批量删除数据,逐个处理会频繁的与数据库交互,损耗性能,但需要注意,不建议一次批量操作太多的数据,如果数据太多数据库响应也会很慢。批量操作需要把握一个度,建议每批数据尽量控制在 500 以内。如果数据多于 500,则分多批次处理。
使用 limit
- 提高查询效率:一个查询返回成千上万的数据行,不仅占用了大量的系统资源,也会占用更多的网络带宽,影响查询效率。使用 limit 可以限制返回的数据行数,减轻了系统负担,提高了查询效率
- 避免过度提取数据:对于大型数据库系统,从数据库中提取大量的数据可能会导致系统崩溃。使用 limit 可以限制提取的数据量,避免过度提取数据,保护系统不受影响。
- 优化分页查询:分页查询需要查询所有的数据才能进行分页处理,这会浪费大量的系统资源和时间。使用 limit 优化分页查询可以只查询需要的数据行,缩短查询时间,减少资源的浪费。
- 简化查询结果:有时我们只需要一小部分数据来得出决策,而不是整个数据集。使用 limit 可以使结果集更加精简和易于阅读和理解
限制行数非常有用,因为它可以提高查询性能、减少处理需要的时间,并且只返回我们关心的列
用 union all 代替 union
- union all:获取所有数据但是数据不去重,包含重复数据
- union:获取所有数据且去重,不包含重复数据
那么 union all 与 union 如果当然它业务数据容许出现重复的记录,我们更推荐使用 union all,因为 union 去重数据需要遍历、排序和比较,它更耗时,更消耗 cpu 资源,但是数据结果最完整。
join 的表不宜过多
- 查询效率下降:多表 JOIN 查询数据对比时间变长
- 系统负载增加:JOIN 操作需要进行大量的计算,因此会导致系统负载增加
- 维护难度加大:在一个连接了多个表的查询中,如果需要修改其中一个表的结构或内容,就可能会需要同时修改其他表的结构或内容
因此,在数据库设计时,应该尽量减小 JOIN 操作的使用频率,并且简化表之间的关系,以提高查询效率和系统的性能。
MySQL 的优化,大体可以分为三部分:
- 索引的优化
- 只要列中含有 NULL 值的字段,不使用索引
- 尽量使用短索引
- 经常在 where 子句中使用的列,尽量使用索引;有多个列 where 或者 order by 子句的,应该建立复合索引;对于 like 语句,以 %或者 - 开头的不会使用索引,以 % 结尾会使用索引
- 尽量不要在列上进行计算
- 尽量不要使用 not in 和 != 操作
- SQL 语句的优化
- SQL 优化
- 不要写select *
- 小表驱动大表
- 连接查询代替子查询
- 提升 group by 的效率:创建索引:如果你使用group by的列没有索引,那么查询可能会变得很慢,因此,可以创建一个或多个适当的索引来加速查询
- 批量操作:建议批量插入或批量删除数据,逐个处理会频繁地与数据库交互,损耗性能
- 使用limit
- 尽量少关联表
- 表的优化
- 表的字段尽可能用 NOT NULL
- 字段长度固定的表查询会更快
- 把数据库的大表按时间或一些标志分成小表
- 将表分区
MySQL 中,如何定位慢查询?
MySQL 中提供了慢日志查询的功能,可以在 MySQL 的系统配置文件中开启这个慢日志的功能,并且也可以设置 SQL 执行超过多长时间来记录到一个日志文件中,我记得上一个项目配置的是 2 秒,只要 SQL 执行的时间超过了 2 秒就会记录到日志文件中,我们就可以在日志文件找到执行比较慢的 SQL 了
那这个 SQL 语句执行很慢,如何分析呢?
- 如果一条 SQL 执行很慢的话,我们通常会使用 MySQL 自带的执行计划 explain 来去查看这条 SQL 的执行情况,比如在这里可以通过 key 和 key_len 检查是否命中了索引,如果本身已经添加了索引,也可以判断索引是否有失效的情况
- 第二个,可以通过 type 字段查看 sql 是否有进一步的优化空间,是否存在全索引扫描或全盘扫描,
- 第三个可以通过 extra 建议来判断,是否出现了回表的情况,如果出现了,可以尝试添加索引或修改返回字段来修复
explain 的结果中,那些信息值得注意呢
- type:本次查询表连接类型,从这里可以看到本次查询大概的效率
- all:执行full table scan,这是最差的一种方式
- index:执行 full index scan,并且可以通过索引完成结果扫描并且直接从索引中取得想要的结果数据,也就是可以避免回标,比 all 略好,因为索引文件通常比全部数据要来的小
- range:利用索引进行范围查询,比 index 略好
- index_subquery:子查询中可以用到索引
- unique_subquery:子查询中可以用到唯一索引,效率比 index_subquery 更高些
- index_merge:可以利用 index marge 特性用到多个索引,提高查询效率
- ref_or_null:表连接类型是 ref,但进行扫描的索引列中可能包含 null 值
- fulltext:全文检索
- ref:基于索引的等值查询,或者表间等值连接
- eq_ref:表连接时基于主键或非 null 的唯一索引完成扫描,比 ref 略好
- const:基于主键或唯一索引唯一性查询,最多返回一条结果,比 eq_ref 略好
- system:查询对象表只有一行数据,这时最好的情况
- key_len:本次查询用于结果过滤的索引实际长度
- key:最终选择得索引,如果没有索引的话,本次查询效果通常都很差
- rows:预计需要扫描的记录数,预计需要扫描的记录数越小越好
- extra:额外附加信息,主要确定是否出现 Using filesort、Using temporary 这两种情况
- Using filesort:将用外部排序而不是按照索引顺序排列结果,数据较少时从内存排序,否则需要在磁盘完成排序,代价非常高,需要添加合适的索引
- Using temporary:需要创建一个临时表来存储结果,这通常发生在对没有索引的列进行 GROUP BY 时,或者 ORDER BY 里的列不都在索引里,这里需要添加合适的索引
- Using index:表示 MySQL 使用覆盖索引避免全表扫描,不需要再到表中进行二次查找数据,这是比较好的结果之一。注意不要和 type 中的 index 类型混淆
- Using where:通常是进行了全表/全索引扫描后再用 where 子句完成结果过滤,需要添加合适的索引
- Impossible where:对 where 子句判断的结果总是 false 而不能选择任何数据,例如 where 1=0,无序过多关注
- select tables optimized away:使用某些聚合函数来访问存在索引的某个字段时,优化器会通过索引直接一次定位到所需要的数据行完成整个查询,例如 min、max、这种也是比较好的结果之一
执行顺序
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VgKLgnQx-1689232139829)(C:\Users\A\AppData\Roaming\Typora\typora-user-images\1688515638832.png)]
十一、MongoDB
MongoDB 数据库的好处
- MongoDB 使用内存+硬盘存储的模式,热数据放内存,冷数据放硬盘,从而提升查找速度
- 比 redis 稍慢,但是使用的内存比 redis 少;比 mysql 快很多,但是并发量比 mysql 高很多
MongoDB 存放那些数据
MongoDB 适合存储允许丢失的海量数据!读写速度比 redis 慢,比 mysql、oracle 快,适合存储海量数据,这种数据丢了一些也不影响整体项目的运行(抖音:点赞、收藏、评论)
十二、JVM
讲一讲 Java 内存的堆、栈和方法区
- 栈:栈内存存放引用(方法的定义)
- 堆:用于存放 new 的对象或数组
- 方法区:与 Java 堆一样,是各个线程共享的内存区域。存储已被 Java 虚拟机加载的类信息、常量池、静态变量、以及编译器编译后的代码等。
JVM 主要分为五大模块
- 类加载器
- 运行数据区
- 执行引擎
- 本地方法接口
- 垃圾收集模块
什么是程序计数器
线程私有的,每个线程一份,内部保证字节码的行号。用于记录正在执行的字节码指令的地址
你能给我详细的介绍 Java 堆吗
- 线程共享的区域:主要用来保存对象实例,数组等,内存不够则抛出 OutOfMemeryError 异常
- 组成:年轻代 + 老年代
- 年轻代被划分为三部分:伊甸区(Eden)和两个大小严格相同的幸存者(Survivor)区
- 老年代主要保存生命周期长的对象,一般是一些老的对象
什么是虚拟机栈
- 每个线程运行时所需要的内存,称为虚拟机栈,先进后出
- 每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
垃圾回收是否涉及栈内存
垃圾回收主要指就是堆内存,当栈帧弹栈以后,内存就会释放
栈内存分配越大越好吗
未必,默认的栈内存通常为 1024k
栈帧过大会导致线程数变少,例如,机器总内存为 512m,目前能活动的线程数则为 512个,如果把栈内存改为 2048k,那么能活动的栈帧就会减半
方法内的局部变量是否线程安全
- 如果方法内局部变量没有逃离方法的作用范围,他是线程安全的
- 如果是局部变量引用了对象,并逃离方法的作用范围(形参,有返回值),需要考虑线程安全
栈内存溢出情况
- 栈帧过多导致栈内存溢出,典型问题:递归调用
- 栈帧过大导致栈内存溢出
栈和堆的区别是什么
- 栈内存一般会用来存储局部变量和方法调用,但堆内存是用来存储 Java 对象和数组的,堆会 GC 垃圾回收,而栈不会
- 栈内存是线程私有的,而堆内存是线程共有的
- 两者异常错误不同,如果栈内存或者堆内存不足都会抛出异常
- 栈空间不足:java.lang.StackOverFlowError
- 堆空间不足:java.lang.OutOfMemoryError
能不能解释一下方法区
- 方法区(Method Area)是各个线程共享的内存区域
- 主要存储类的信息、运行时常量池
- 虚拟机启动的时候创建,关闭虚拟机时释放
- 如果方法区域中的内存无法满足分配,则会抛出 OutOfMemoryError:Metaspace
介绍一下运行时常量池
- 常量池:可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
- 当类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
你听过直接内存吗
- 并不属于 JVM 中的内存结构,不由 JVM 进行管理。是虚拟机的系统内存
- 常见于 NIO 操作时,用于数据缓冲区,它分配回收成本较高,但读写性能高,不受 JVM 内存回收管理
什么是类加载器?类加载器有哪些?
什么是类加载器:
JVM 只会运行二进制文件,类加载器的作用就是将字节码文件加载到 JVM 中,从而让 Java 程序能够启动起来
有四种:
- 启动类加载器:由 C++ 编写实现,加载 JAVA_HOME/jre/lib 目录下的库
- 扩展类加载器:主要加载 JAVA_HOME/jre/lib/ext 目录下的类
- 应用类加载器:用于加载 classPath 下的类
- 自定义类加载器:自定义类继承 ClassLoader,实现自定义类加载规则
什么是双亲委派模型
加载某一个类,先委托上一级的加载器进行加载,如果上级加载器也有上级,则会继续向上委托,如果该类委托上级没有被加载,子加载器尝试加载该类
JVM 为什么采用双亲委派机制
- 通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性
- 为了安全,保证类库 API 不会被修改
说一下类装载的执行过程
- 加载:查找和导入 class 文件
- 验证:保证加载类的准确性
- 准备:为类变量分配内存并设置类变量初始值
- 解析:把类中的符号引用转换为直接引用
- 初始化:对类的静态变量,静态代码块执行初始化操作
- 使用:JVM 开始对入口方法开始执行用户的程序代码
- 卸载:当用户程序代码执行完毕后,JVM 便开始销毁创建的 class 对象
对象什么时候可以被垃圾器回收
如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾,如果定位了垃圾,则有可能会被垃圾回收器回收
定位垃圾的方式
- 引用计数法
- 一个对象被引用了一次,在当前的对象头上递增一次引用次数,如果这个对象的引用次数为0,代表这个次数可回收
- 当对象间出现了循环引用的话,则引用计数法就会失效
- 可达性分析算法
- 现在的虚拟机采用的都是通过可达性分析算法来确定哪些内容是垃圾
- Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
- 扫描堆中的对象,看是否能够沿着 GC Root 对象为起点的引用链找到该对象,找不到,表示可以回收
哪些对象可以作为 GC Root?
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
JVM 垃圾回收算法有哪些
- 标记清除算法
- 垃圾回收分为2个阶段,分别是标记和清除,效率高,有磁盘碎片,内存不连续
- 标记整理算法
- 标记清除算法一样,将存活对象都向内存另一端移动,然后清理边界以外的垃圾,无碎片,对象需要移动,效率低
- 复制算法
- 将原有的内存空间一分为二,每次只用其中的一块,正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收;无碎片,内存使用率低
说一下 JVM 中的分代回收
一、堆的区域划分
堆被分为了两份:新生代和老年代【1:2】
对于新生代,内部又被分为了三个区域,Eden 区,幸存者区survivor(分为 from 和 to)【8:1:1】
二、对象回收分代回收策略
- 新创建的对象,都会先分配到伊甸区
- 当伊甸区的内存不足,标记伊甸区与 from(现阶段没有)的存活对象
- 将存活对象采用复制算法复制到 to中,复制完成后,伊甸区和 from 内存得到释放
- 经过一段时间后伊甸园的内存又出现不足,标记 eden 区域和 to 区存活的对象,将其复制到 from 区
- 当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会提前晋升)
MinorGC、MixedGC、FullGC 的区别是什么
- MinorGC 发生在新生代的垃圾回收,暂停时间短
- Mixed GC 新生代 + 老年代部分区域的垃圾回收,G1收集器持有
- FullGC:新生代 + 老年代完整垃圾回收,暂停时间长(STW),应尽力避免
说一下 JVM 有哪些垃圾回收器
在jvm中,实现了多种垃圾收集器,包括:
- 串行垃圾收集器,Serial GC(针对新生代),Serial Old GC(针对老年代)
- 并行垃圾收集器,Parallel Old GC(针对老年代),ParNew GC(针对新生代)
- CMS(并发)垃圾收集器, CMS GC,作用在老年代
- G1垃圾收集器,作用在新生代和老年代(重点)
详细聊一下G1垃圾收集器
- 应用于新生代和老年代,在JDK9之后默认使用G1
- 划分成多个区域,每个区域都可以充当 eden,survivor,old,humongous,其中 humongous 专为大对象准备
- 采用复制算法
- 响应时间与吞吐量兼顾
- 分为三个阶段:新生代回收(stw)、并发标记(重新标记stw)、混合收集
- 如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC
强引用、软引用、弱引用、虚引用的区别
- 强引用:只要所有GC Roots能找到,就不会被回收
- 软引用:需要配合SoftReference使用,当垃圾多次回收,内存依然不够的时候会回收软引用对象
- 弱引用:需要配合WeakReference使用,只要进行了垃圾回收,就会把弱引用对象回收
- 虚引用:必须配合引用队列使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存
常用的 JVM 调优的参数都有哪些?
- 设置堆空间大小
- 虚拟机栈的设置
- 设置垃圾回收收集器
- 年轻代中Eden区和两个Survivor区的大小比例
- 年轻代晋升老年代阈值
十三、区别
Redis 和 MySQL 的区别
- mysql 是关系型数据库,redis 是内存型数据库
- mysql 的并发能力较低,而 redis 的并发能力很强,redis 读的并发是 11w/s,写的并发是 8w/s
- 如果是在高并发情况下,不使用 redis,服务器很容易宕机
Spring、SpringBoot 与 SpringCloud 区别
- Spring 是 JavaEE 的一个轻量级开发框架,主营 IOC 和 AOP,集成 JDBC、ORM、MVC 等功能便于开发
- Spring Boot 是一个基于 Spring 的组件,它帮我们预组装了 Spring 的一系列组件,以便以尽可能少的代码和配置来开发基于 Spring 的 Java 应用程序。以汽车为例,如果我们想要组装一辆汽车,我们需要发动机、座椅、内饰等各种不见,然后把他们装配起来。Spring 就相当于提供了一系列这样的部件,但是要装好汽车上路,还需要我们自己动手。而 Spring Boot 则相当于已经帮我们预装好了一辆可以上路的汽车,如果有特殊的要求,可以通过修改配置或编写少量代码完成。因此,Spring Boot 和 Spring 的关系就是整车和零部件的关系,他们不是取代关系,试图跳过 Spring 直接学习 Spring Boot 是不可能的。
- SpringBoot 专注于快速方便的开发单个个体服务,SpringCloud 是关注全局的微服务协调治理框架,它将 SpringBoot 开发的一个个单体微服务整合并管理起来,为各个微服务之间提供,配置管理、服务发现、熔断器、路由等服务,SpringBoot 可以单独使用,他不依赖于 SpringCloud;而 SpringCloud 必然依赖于 SpringBoot,属于依赖关系
WebSocket 与 Http 请求的区别
- 数据传输方式不同:HTTP 请求使用请求-响应模式,客户端给服务器发送请求,服务器给客户端响应,服务器无法做到直接给客户端响应。而 WebSocket(是一种基于 Socket 的双向通信协议),允许客户端主动向服务器端发送消息,也允许服务器随时向客户端发送消息
- 场景不同:HTTP 请求主要适用于客户端-服务器请求响应模型,例如浏览器请求网页资源等;而 WebSocket 主要用于实时通信常见,如在线游戏、聊天室等。
- 连接维持时间不同:HTTP 请求完成后会断开连接,下次请求需要重新建立连接。而 WebSocket 使用长生命周期连接,在连接建立后保持打开状态,直到显式关闭或发生错误
总之,WebSocket 是一种更高效、更快捷的双向数据传输协议,特别适用于实时通信和 Web 应用程序中需要频繁更新数据的地方
SSM 和 SpringCloud 有什么区别
SpringCloud 是微服务框架,擅长服务发现、服务治理以及提供各种微服务框架,比如 feign、ribbon、nacos 等等
ssm 是具体的功能框架,SpringCloud 里面要实现业务功能,需要使用 SSM,让 SSM 和 SpringCloud 整合,如果不使用 SpringCloud 整合 SSM,直接开发 SSM,可能会比较麻烦
SpringCloud 的优点和缺点,对比 SpringBoot
SpringCloud 的优点:
- 分布式系统支持:Spring Cloud 提供了一系列的分布式系统开发工具和组件,如服务注册与发现、负载均衡、断路器、配置管理等,使得构建和管理分布式系统变得更加简单和高效。
- 微服务架构:Spring Cloud 鼓励使用微服务架构,通过将应用程序拆分成小而独立的服务,提高了系统的可扩展性、灵活性和容错性
- 生态系统和文档丰富:Spring Cloud 拥有庞大的开发者社区和活跃的生态系统,提供了丰富的文档和示例代码,可以帮助开发者快速入门和解决问题
SpringCloud 的缺点:
- 学习曲线较陡峭:Spring Cloud 是一个庞大的框架,涵盖了很多组件和概念,初学者可能需要花费一些时间来理解和掌握
- 复杂性增加:引入分布式系统和微服务架构会增加系统的复杂性,涉及到服务间通信、配置管理、故障处理等方面的考虑和实现
- 运维成本高:分布式系统和微服务架构的部署和运维相对复杂,需要管理多个服务示例和解决服务间的调用、故障处理等问题
Spring Boot 的优点:
- 简化开发:Spring Boot 通过自动化配置和约定大于配置的原则,大大简化了应用程序的开发和部署,开发者可以更专注于业务逻辑而非底层配置
- 快速启动:Spring Boot 提供了快速启动和嵌入式服务器的特性,可以快速构建和运行独立的、自包含的应用程序
- 生态系统丰富:Spring Boot 与 Spring 框架无缝集成,并且拥有庞大的生态系统,提供了大量的插件和第三方库,可以满足各种需求
Spring Boot 的缺点:
- 不适用于大型系统:虽然 Spring Boot 适用于快速构建小型和中型应用程序,但对于大型系统和复杂的业务需求,可能需要更多的定制和扩展
- 依赖冲突问题:由于 Spring Boot 自动引入了许多依赖,可能会导致依赖冲突的问题,需要进行版本管理和解决
十四、Dubbo
什么是 Dubbo
远程服务调用框架,跟 feign 的功能是一致的,但是效率比 feign 高
- 服务提供方通过 @DubboService 提供服务
- 服务调用方通过 @DubboReference(瑞福润死)调用服务
Dubbo 跟 Feign 的比较
- dubbo 适合数据量小、高并发和服务提供者远远小于消费者的场景;而 feign 不适合高并发的访问
- dubbo 支持的负载均衡策略只有四种(随机、轮询、活跃度、Hash 一致性);而 feign 支持 N 种
- dubbo 支持多种容错策略;而 feign 采用熔断器 hystrix 进行熔断降级处理,处理方式不一样