1. Java基础篇
1.1. 怎样理解oop面向对象
面向对象具有以下特征:
继承:继承已有的类,创建新类的过程,通过继承,子类可以重用父类的代码,减少代码冗余
封装:是指把数据和操作数据的方法绑定到一起,对于数据的访问只能通过接口来得到
多态性:指不同子类型的对象对同一消息作出的不同响应
1.2. 重载与重写的区别
重载是发生在同类中,重写则是发生在父类和子类之间
重载的方法名必须相同,重写的方法名相同并且返回值类型必须相同
重载的参数列表不同,重写的参数列表必须相同
构造方法不能被重写
1.3. 接口与抽象类的区别
抽象类要被子类继承,接口要被实现;
接口可以多继承接口,但是类只能是去单继承;
抽象类可以有构造器,但是接口不能有构造器;
抽象类可以成员变量,但是接口只能申明常量;
抽象方法可以用public、protected、default这些修饰符,接口只能是public
1.4. 深拷贝与浅拷贝的区别
深拷贝和浅拷贝就是指对象的拷贝,一个对象中存在两种类型的属性,一种是基本数据类型,一种是实例对象的引用。
浅拷贝是指,只会拷贝基本数据类型的值,以及实例对象的引用地址,并不会复制一份引用地址所指向的对象,也就是浅拷贝出来的对象,内部类属性指向的是同一个对象
深拷贝是指,既会拷贝基本数据类型的值,也会针对实例对象的引用地址所指向的对象进行复制,其拷贝出来的对象,内部的类执行指向的不是同一个对象
1.5. sleep和wait方法区别
sleep方法
属于Thread类中的方法
释放cpu给其它线程 不释放锁资源
sleep(1000) 等待超过1s被唤醒
wait方法
属于Object类中的方法
释放cpu给其它线程,同时释放锁资源
wait(1000) 等待超过1s被唤醒,默认是ms,所以1000是1秒
wait() 一直等待需要通过notify或者notifyAll进行唤醒
wait 方法必须配合 synchronized 一起使用,不然在运行时就会抛出IllegalMonitorStateException异常
1.6. 什么是自动装箱和拆箱,为什么要用呢
装箱:将基本类型转换成包装类对象
拆箱:将包装类型转换成基本类型的值
java为什么要引入自动装箱和拆箱的功能?
主要是用于java集合中,List<Inteter> list=new ArrayList<Integer>();
其中的list集合如果要放整数的话,只能放对象,不能放基本类型,因此需要将整数自动装箱成对象。
自动装箱代码示例:
int num = 10;
Integer boxedNum = num; // 自动装箱
// Integer boxedNum = Integer.valueOf(num); // 自动装箱的底层
自动拆箱代码示例:
Integer boxedNum = 10;
int num = boxedNum; // 自动拆箱
// int num = boxedNum.intValue(); // 自动拆箱的底层
实现原理:javac编译器的语法糖,底层是通过Integer.valueOf()和lnteger.intValue()方法实现。
区别:
1. Integer是int的包装类,int 是java的一种基本数据类型
2.Integer变量必须实例化后才能使用,而int变量不需要
3. Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int是直接存储数据值
4.Integer的默认值是null,int的默认值是0
1.7. int和integer有什么区别
说白了就是基本数据类型和包装数据类型的区别
- 基本数据类型VS包装数据类型
-
- int是基本数据类型,用于存储整数值,直接在内存中存储实际的数据
- integer是包装类,属于Java的对象类型,是
java.lang.Integer
类的实例,包装了一个int
类型的值。
- 存储方式
-
- int直接存储数值,存储在栈内存中,效率高,适合进行高效计算
- integer是一个对象,存储在堆内存中,封装了一个int值,适合在需要对象类型的场合使用
- 默认值
-
- int在没有初始化的时候,默认值是0
- integer在没有初始化的时候,默认值是null
- 应用场景
-
int
通常用于数学运算或性能要求较高的场景,因为它直接存储数值,占用的内存小,处理速度快。Integer
作为对象类型,常用于需要对象参数的场合,比如 Java 集合类(List
,Set
,Map
),因为它们只能存储对象类型,不能直接存储基本类型。
- 比较方式
-
- int作为基本类型,用“==”比较两个值的大小,直接比较数值本身
- integer作为对象,用“equals”方法来比较
- 性能差异
-
int
操作比Integer
更高效,因为int
是基本类型,不需要对象的内存分配和管理。Integer
作为对象类型,涉及对象的创建和销毁,操作较为缓慢,在频繁使用时可能带来性能开销。
- 自动拆箱和自动装箱
-
- 通过Java的语法糖实现自动转换
1.8. 包装数据类型和引用数据类型
- 定义
-
- 包装数据类型是Java中基本数据类型对应的对象类型,每个基本数据类型都有一个相应的包装类型 ,如
Integer
(对应int
)、Double
(对应double
)、Boolean
(对应boolean
)等。包装数据类型用于将基本类型转换为对象,以便在需要对象的地方使用(例如集合类)。
- 包装数据类型是Java中基本数据类型对应的对象类型,每个基本数据类型都有一个相应的包装类型 ,如
-
- 引用数据类型 是指所有可以通过引用指向对象的类型,除了包装类,还包括自定义类、数组、接口等。引用类型的变量存储的是对象的内存地址(引用),而不是直接存储对象本身。 常见的引用数据类型包括用户定义的类、接口、数组、String 等。
- 关系
-
- 包装类是对基本数据类型的封装,每个基本数据类型(如
int
,char
等)都有对应的包装类(如Integer
,Character
等)。包装类允许基本类型的值以对象的形式出现,可以在泛型、集合等需要对象的场景中使用。
- 包装类是对基本数据类型的封装,每个基本数据类型(如
int num = 10; // 基本数据类型
Integer boxedNum = num; // 自动装箱,包装类类型
-
- 与基本数据类型不同,引用数据类型存储的是对象的引用(内存地址),而不是对象本身。引用类型的变量指向一个对象实例,该实例存储在堆内存中。
String str = "Hello"; // 引用数据类型
Person person = new Person(); // 自定义类,也是引用数据类型
- 存储方式
-
- 包装类作为对象,存储在堆(heap)内存中。当创建包装类实例时,程序分配堆内存来存储它们。
- 引用类型变量存储在栈(stack)内存中,但它指向的对象本身存储在堆(heap)内存中。引用变量包含对象的内存地址,通过这个地址访问实际的对象。
- 默认值
-
- 当包装类没有被显式赋值时,它们的默认值为
null
。这是因为包装类是对象,而对象的默认值是null
。 - 引用类型变量的默认值也是
null
,因为它们也是指向对象的引用。
- 当包装类没有被显式赋值时,它们的默认值为
- 比较方式
-
- 对于包装类,
==
比较的是引用地址,只有在比较两个同一对象引用时,才会返回true
。要比较包装类中的实际值,需要使用equals()
方法。
- 对于包装类,
Integer a = new Integer(10);
Integer b = new Integer(10);
System.out.println(a == b); // false,因为它们是不同的对象
System.out.println(a.equals(b)); // true,比较的是实际的值
-
- 引用数据类型的
==
比较的是对象的内存地址,equals()
方法通常被重写来比较对象的内容或逻辑相等。
- 引用数据类型的
String str1 = new String("Hello");
String str2 = new String("Hello");
System.out.println(str1 == str2); // false,不同的对象地址
System.out.println(str1.equals(str2)); // true,内容相等
- 常见用途
-
- 常用于泛型、集合类(如
List
,Set
,Map
)等需要对象的场合,也用于需要通过对象对基本数据类型进行更多操作的场合(如使用Integer
的parseInt()
方法)。 - 除了包装类外,引用类型还包括字符串(
String
)、自定义对象(如Person
类)和数组等,用于保存更复杂的数据结构和对象关系。
- 常用于泛型、集合类(如
- 性能差异
-
- 包装类由于是对象,需要在堆内存中创建实例,并伴随垃圾回收机制,因此在性能上比基本数据类型略低。频繁使用包装类可能带来额外的性能开销。
- 引用类型的性能取决于其实现和使用方式。虽然引用变量本身存储在栈中,但对象存储在堆中,因此对象的创建、销毁和引用会影响性能。
- 总结
特性 | 包装数据类型(Wrapper Classes) | 引用数据类型(Reference Types) |
定义 | 基本数据类型的对象封装类,如
| 所有非基本类型的对象类型,如类、数组、接口等 |
与基本类型的关系 | 是基本数据类型的对象版本,支持自动装箱/拆箱 | 不与基本类型直接相关 |
存储位置 | 引用存储在栈中,实例(对象)存储在堆中 | 引用存储在栈中,实例存储在堆中 |
默认值 |
|
|
比较方式 |
比较引用地址, 比较实际数值 |
比较引用地址, 比较对象内容或逻辑 |
使用场景 | 适用于需要对象的场合,如集合类、泛型等 | 用于表示所有对象类型,如类实例、接口、数组等 |
性能 | 较慢,因为涉及对象的创建和垃圾回收 | 性能依赖于实现和使用方式,通常涉及更多复杂操作 |
举例 |
| 自定义类、 、数组、接口等 |
1.9. ==和equals区别
- ==
如果比较的是基本数据类型,那么比较的是变量的值
如果比较的是引用数据类型,那么比较的是地址值(两个对象是否指向同一块内存)
- equals
如果没重写equals方法比较的是两个对象的地址值
如果重写了equals方法后我们往往比较的是对象中的属性的内容
equals方法是从Object类中继承的,默认的实现就是使用==
1.10. String buffer和String builder区别
①当对字符串进行修改的时候,需要使用 StringBuffer 和 StringBuilder 类。
②和 String 类不同的是,StringBuffer 和 StringBuilder类的对象能够被多次的修改,并且不产生新的未使用对象。
③StringBuilder 类在 Java 5 中被提出,它和 StringBuffer 之间的最大不同在于 StringBuilder 的方法不是线程安全的(不能同步访问)。
④由于 StringBuilder 相较于 StringBuffer 有速度优势,多数情况下建议使用 StringBuilder类。然而在应用程序要求线程安全的情况下,则必须使用 StringBuffer 类。
public static void main(String[] args) {
StringBuilder sb1 = new StringBuilder("hello");
StringBuilder sb2 = sb1;
// 追加:即尾插-->字符、字符串、整形数字
sb1.append(' '); // hello
sb1.append("world"); // hello world
sb1.append(123); // hello world123
System.out.println(sb1); // hello world123
System.out.println(sb1 == sb2); // true
System.out.println(sb1.charAt(0)); // 获取0号位上的字符 h
System.out.println(sb1.length()); // 获取字符串的有效长度14
System.out.println(sb1.capacity()); // 获取底层数组的总大小
sb1.setCharAt(0, 'H'); // 设置任意位置的字符 Hello world123
sb1.insert(0, "Hello world!!!"); // Hello world!!!Hello world123
System.out.println(sb1);
System.out.println(sb1.indexOf("Hello")); // 获取Hello第一次出现的位置
System.out.println(sb1.lastIndexOf("hello")); // 获取hello最后一次出现的位置
sb1.deleteCharAt(0); // 删除首字符
sb1.delete(0, 5); // 删除[0, 5)范围内的字符
String str = sb1.substring(0, 5); // 截取[0, 5)区间中的字符以String的方式返回
System.out.println(str);
sb1.reverse(); // 字符串逆转
str = sb1.toString(); // 将StringBuffer以String的方式返回
System.out.println(str);
}
StringBuffer 与 StringBuilder 中的方法和功能完全是等价的,
只是StringBuffer 中的方法大都采用了 synchronized 关键字进行修饰,因此是线程安全的,而 StringBuilder 没有这个修饰,可以被认为是线程不安全的。
在单线程程序下,StringBuilder效率更快,因为它不需要加锁,不具备多线程安全而StringBuffer则每次都需要判断锁,效率相对更低
1.11. final、finally、finalize
- final:修饰符(关键字)有三种用法:修饰类、变量和方法。修饰类时,意味着它不能再派生出新的子类,即不能被继承,因此它和abstract是反义词。修饰变量时,该变量使用中不被改变,必须在声明时给定初值,在引用中只能读取不可修改,即为常量。修饰方法时,也同样只能使用,不能在子类中被重写。
- finally:通常放在try…catch的后面构造最终执行代码块,这就意味着程序无论正常执行还是发生异常,这里的代码只要JVM不关闭都能执行,可以将释放外部资源的代码写在finally块中。
- finalize:Object类中定义的方法,Java中允许使用finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在销毁对象时调用的,通过重写finalize() 方法可以整理系统资源或者执行其他清理工作。
1.12. 集合体系
1.13. ArraryList和LinkedList区别
- arraylist是实现了基于动态数组的数据结构,linkedlist基于链表的数据结构
- 对于随机访问get和set,arraylist效率要高于linkedlist,因为有数组下标可以访问,而linkedlist需要去移动指针
- 对于新增和删除操作add和remove,linkedlist占据优势更大一点,除非是只对单条数据进行插入和删除操作,arraylist的速度快于linkedlist,如果是随机批量的插入数据,linkedlist要优于arraylist,因为arraylist每插入一条数据,要移动插入点以及之后的所有数据
1.14. HashMap的底层结构,和HashTable的区别
- 数组+链表+红黑树,链表的作用是去解决hash冲突,将hash值取模之后的对象存放在一个链表中置于对应的槽位,红黑树代替超过8个节点的链表,把原来的O(n)简化成O(logn)
- HashMap和HashTable的区别如下:
-
- 线程安全不同,HashMap是线程不安全的,HashTable是线程安全的,其中的方法是Synchronized,在多线程并发的情况下,可以直接使用HashTable,但是使用HashMap时必须自己加上同步处理
- key和value是否允许空值:HashTable中,key和value不允许出现null值,HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。
- HashTable的默认容量是11,HashMap是16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。
- Hash Table在扩容的时候会在原来的基础上变成2倍并加1,HashMap则是扩容为原来容量的2倍
1.15. 线程的创建方式
- 继承Thread类创建线程
- 实现Runnable接口创建线程
- 使用Callable和Fulture来创建线程,有返回值
- 使用线程池来创建线程
import java.util.concurrent.*;
public class threadTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 继承Thread类的线程
ThreadClass thread = new ThreadClass();
thread.start(); // 启动线程
Thread.sleep(100); // 主线程睡眠,等待子线程执行
System.out.println("#####################");
// 实现Runnable接口的线程
RunnableClass runnable = new RunnableClass();
new Thread(runnable).start(); // 创建新线程并启动
Thread.sleep(100); // 主线程睡眠,等待子线程执行
System.out.println("#####################");
// 实现Callable接口的线程
FutureTask<String> futureTask = new FutureTask<>(new CallableClass());
futureTask.run(); // 执行任务
System.out.println("callable返回值:" + futureTask.get()); // 获取并输出返回值
Thread.sleep(100); // 主线程睡眠,等待任务完成
System.out.println("#####################");
// 线程池示例
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
1, // 核心线程池大小
1, // 最大线程池大小
2, // 空闲线程最大存活时间
TimeUnit.SECONDS, // 时间单位
new ArrayBlockingQueue<>(10) // 阻塞队列,用于保存待执行任务
);
// 提交任务到线程池
threadPoolExecutor.execute(thread); // 执行继承Thread类的任务
threadPoolExecutor.shutdown(); // 关闭线程池
Thread.sleep(100); // 主线程睡眠,等待线程池任务完成
System.out.println("#####################");
// 使用并发包Executors创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(5); // 创建一个固定大小的线程池,线程数为5
executorService.execute(thread); // 提交任务到线程池
executorService.shutdown(); // 关闭线程池
}
}
// 继承Thread类创建线程
class ThreadClass extends Thread {
@Override
public void run() {
System.out.println("我是继承thread形式:" + Thread.currentThread().getName());
}
}
// 实现Runnable接口创建线程
class RunnableClass implements Runnable {
@Override
public void run() {
System.out.println("我是实现runnable接口:" + Thread.currentThread().getName());
}
}
// 实现Callable接口创建线程,并返回结果
class CallableClass implements Callable<String> {
@Override
public String call() {
System.out.println("我是实现callable接口:");
return "我是返回值,可以通过get方法获取";
}
}
1.16. 线程的状态转化
- 新建状态(New) :线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
- 就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
- 运行状态(Running):线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
- 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
-
- 等待阻塞 -- 通过调用线程的wait()方法,让线程等待某工作的完成。
- 同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
- 其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
- 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
1.17. Java中几种类型的流
1.18. 请写出你最常见的5个RuntimeException
- java.lang.NullPointerException
-
- 空指针异常,出现原因:调用了未经过初始化的对象或者是不存在的对象
- java.lang.ClassNotFoundException
-
- 指定路径找不到,出现原因:类的名称或者路径加载错误;通常都是程序试图通过某个字符串来加载某个类时引发的异常
- java.lang.NumberFormatException
-
- 字符串转化成数字异常,出现原因:字符型数据中包含非数字型字符
- java.lang.IndexOutOfBoundsException
-
- 数组越界异常
- java.lang.IllegalArgumentException
-
- 方法传递参数错误
- java.lang.ClassCastException
-
- 数据类型转换异常
编译时异常 (Checked Exception):
- 定义: 在编译时就能被检查到的异常。
- 特点:
-
- 必须用
try...catch
块捕获或使用throws
关键字声明抛出。 - 通常是由程序员的错误导致的,例如文件找不到、数据库连接失败等。
- 必须用
- 常见子类:
-
IOException
:输入/输出错误SQLException
:数据库操作错误ClassNotFoundException
:类找不到FileNotFoundException
:文件找不到
运行时异常 (Runtime Exception):
- 定义: 在运行时才会发生的异常。
- 特点:
-
- 不需要用
try...catch
块捕获,也不需要使用throws
关键字声明抛出。 - 通常是程序逻辑错误导致的,例如数组越界、空指针异常等。
- 不需要用
- 常见子类:
-
ArithmeticException
:算术错误(除数为零)ArrayIndexOutOfBoundsException
:数组越界NullPointerException
:空指针异常IllegalArgumentException
:非法参数异常ClassCastException
:类型转换异常
1.19. Java反射的理解
反射机制
所谓的反射机制就是java语言在运行时拥有一项自观的能力。通过这种能力可以彻底了解自身的情况为下一步的动作做准备。
Java反射的实现主要借助四个类:class(类对象)、constructor(类构造器对象)、field(类的属性对象)、method(类的方法对象),
Java反射的作用
在Java运行时环境中,对于任意一个类,可以知道这个类有哪些属性和方法。对于任意一个对象,可以调用它的任意一个方法。这种动态的获取类的信息以及动态调用对象的方法的功能便是反射
Java反射机制提供的功能
在运行时判断任意一个对象所属的类。
在运行时构造任意一个类的对象。
在运行时判断任意一个类所具有的成员变量和方法。
在运行时调用任意一个对象的方法
1.20. Java序列化
- 序列化是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。序列化是为了解决在对对象流进行读写操作时所引发的问题。
- 序列化的实现:将需要被序列化的类实现Serializable接口,该接口没有需要实现的方法 , implements Serializable 只是为了标注该对象是可被序列化的,然后使用一个输出流(如:FileOutputStream)来构造一个ObjectOutputStream(对象流)对象,接着,使用创建号的这个ObjectOutputStream 对象的 writeObject(Object obj)方法就可以将参数为 obj 的对象写出(即保存其状态),要恢复的话则用输入流。
1.21. Http常见状态码
常见的HTTP状态码可以分为五类:1xx(信息性状态码)、2xx(成功状态码)、3xx(重定向状态码)、4xx(客户端错误状态码)和5xx(服务器错误状态码)
- 200 OK //客户端请求成功
- 301 Permanently Moved (永久移除),请求的 URL 已移走。Response 中应该包含一个 Location URL, 说明资源现在所处的位置
- 302 Temporarily Moved 临时重定向
- 400 Bad Request //客户端请求有语法错误,不能被服务器所理解
- 401 Unauthorized //请求未经授权,这个状态代码必须和 WWW-Authenticate 报头域一起使用
- 403 Forbidden //服务器收到请求,但是拒绝提供服务
- 404 Not Found //请求资源不存在,eg:输入了错误的 URL
- 500 Internal Server Error //服务器发生不可预期的错误
- 503 Server Unavailable //服务器当前不能处理客户端的请求,一段时间后可能恢复正常
1.22. GET和POST的区别
- get请求的数据会附在URL后面(也就是直接放在HTTP协议头中),以?分割URL和传输数据,参数之间以&相连,如:login.action?name=zhagnsan&password=123456。POST 把提交的数据放置在HTTP的请求体中
- 理论上GET和POST提交的数据是没有限制的,但是GET是通过URL提交数据的,那么GET可提交的护士距离就会受到URL的长度有直接关系,实际上,URL长度没有上限问题,但是特定的浏览器以及服务器会对它进行限制
- POST比GET更加安全,POST会把信息放到请求体中,信息相对来说安全一点,因为通过GET提交的数据,用户和密码明文会出现在URL上,容易泄露,除此之外,使用GET 提交数据还可能会造成 Cross-site request forgery 攻击。
- 幂等性:GET是幂等的,重复调用不会影响服务器的资源状态,比如多次查询数据,结果是一样的;POST不是幂等的,重复调用可能会产生不同的结果,通常用于提交数据或者更改服务器资源状态
- GET请求会被浏览器自动缓存,因此适合取获取静态资源或查询操作;默认情况下,浏览器不会缓存POST请求
1.23. Cookie和Session的区别
定义区别
- Cookie:Cookie是web服务器发送给浏览器的一块信息,浏览器会在本地一个文件中给每个web服务器存储cookie。以后再给特定的web服务器发送请求的时候,同时会发送存储的cookie
- Session:Session是存储在web服务器的一块信息,session对象存储特定的用户会话配置信息和属性。当用户在应用程序的web页之间跳转的时候,存储在Session对象中的变量不会丢失,而是在整个会话中存储下去
存储位置
- Cookie:存储在客户端(浏览器)中。服务器通过
Set-Cookie
头部将Cookie发送到客户端,客户端每次请求都会将Cookie包含在请求头中发送回服务器。 - Session:存储在服务器端。服务器为每个会话创建一个Session对象,并生成一个唯一的Session ID,该ID存储在客户端的Cookie中,客户端通过Session ID与服务器交互,服务器通过这个ID找到相应的Session数据。
安全性
- Cookie:由于存储在客户端,容易被用户修改或窃取,尤其是如果Cookie没有设置为安全的
HttpOnly
或Secure
属性时。攻击者可能通过XSS(跨站脚本攻击)来窃取Cookie。 - Session:存储在服务器端,相对更安全。即使攻击者获取了Session ID,也无法直接获取Session的具体内容,除非他们能访问服务器。
生命周期
- Cookie:可以设置过期时间。如果设置了
Expires
或Max-Age
属性,Cookie可以在浏览器关闭后仍然存在,成为持久性Cookie;否则Cookie将在会话结束时(浏览器关闭时)被删除。 - Session:通常在会话结束或用户关闭浏览器时过期,默认情况下Session的生命周期比Cookie短,也可以通过服务器设置Session过期时间。
适用场景
- Cookie:适合保存一些简单的数据或状态,比如用户偏好、主题颜色、语言选择等;它也可以用来实现“记住我”功能。
- Session:适用于存储敏感数据或需要与服务器端紧密交互的数据,比如用户的登录状态、购物车信息等。
容量限制
- Cookie:通常每个Cookie的大小限制为4KB,浏览器对单个域名的Cookie数量也有一定的限制。
- Session:Session存储在服务器端,理论上只受服务器内存或存储的限制,可以存储更多的数据。
服务器压力
- Cookie:由于Cookie存储在客户端,服务器不需要额外存储和管理Cookie,因此对服务器资源没有直接影响。
- Session:每个用户都会有一个对应的Session,随着用户数量的增加,服务器端需要更多的内存或存储来管理这些Session。
传输方式
- Cookie:每次请求都会自动带上,增加网络请求的大小,尤其是当Cookie包含大量数据时。
- Session:仅通过Session ID进行关联,减少了每次请求的传输数据量。
数据量方面
- Cookie:只能存储String类型的对象
- Session:能够存储任意的Java对象,cookie只能存储String类型的对象
2. Java高级篇
2.1. JVM内存分为哪几个区,每个区的作用是什么
2.2. Java垃圾收集方法
- 复制算法
-
- 年轻代一般采用的是GC算法,这种GC算法采用的就是复制算法
- 优点:效率高;缺点:需要内存容量大,比较耗内存
- 使用在占用空间较少、刷新次数比较多的新生区
- 标记-清除算法
-
- 老年代一般是由标记清除或者是标记清除与标记整理的混合实现
- 率比较低,会差生碎片
- 标记-整理算法
-
- 老年代一般是由标记清除或者是标记清除与标记整理的混合实现
- 效率低速度慢,需要移动对象,但不会产生碎片。
2.3. 垃圾回收的时候判断对象是否存活
- 引用计数法
-
- 判断一个对象的引用次数是否为零,如果为零那么就没有被引用
- 可达性分析算法
-
- 该算法的基本思路就是通过一些被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(即从GC Roots节点到该节点不可达),则证明该对象是不可用的。
- Java中可以作为引用链的对象有几种:虚拟机栈中用到的对象、方法区类静态属性引用的对象、方法区常量池中引用的对象、本地方法栈JNI引用的对象
2.4. 什么情况下会栈溢出,什么情况下会堆溢出
- 引发 StackOverFlowError(栈溢出) 的常见原因有以下几种
-
- 无限递归循环调用(最常见)
- 执行了大量方法,导致线程栈空间耗尽
- 方法内声明了海量的局部变量
- native 代码有栈上分配的逻辑,并且要求的内存还不小,比如 java.net.SocketInputStream.read0 会在栈上要求分配一个 64KB 的缓存(64位 Linux)
- 引发 OutOfMemoryError的常见原因有以下几种
-
- 内存中加载的数据量庞大,比如一次性从数据库中取出大量数据
- 集合类中有对对象的引用,使用完后没有清空,使得JVM不能回收
- 代码中存在死循环或循环产生过多重复的对象实体
- 启动参数内存设置的值过于小
2.5. 线程池创建的有哪些
- newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。这种类型的线程池特点是:
工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。
- newFixedThreadPool
创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。
- newSingleThreadExecutor
创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。
- newScheduleThreadPool
创建一个定长的线程池,而且支持定时的以及周期性的任务执行。例如延迟3秒执行。
2.6. 为什么要用到线程池
- 线程池的主要工作是要控制运行的线程数量,处理过程中将任务放在队列中,然后在线程创建后启动这些任务,如果线程数量超过了最大线程数量,超出的线程进入等待队列,等其他线程完成后再去执行
- 主要特点是:线程服用,线程最大并发量控制,管理线程
2.7. 线程池底层原理
2.8. 线程池对象有哪些参数?怎么设定核心线程数和最大线程数?拒绝策略有哪些
总共7个参数:
- corePoolSize:核心线程数
在ThreadPoolExecutor中有一个与它相关的配置:allowCoreThreadTimeOut(默认为false),当allowCoreThreadTimeOut为false时,核心线程会一直存活,哪怕是一直空闲着。而当allowCoreThreadTimeOut为true时核心线程空闲时间超过keepAliveTime时会被回收。
- maximumPoolSize:最大线程数
线程池能容纳的最大线程数,当线程池中的线程达到最大时,此时添加任务将会采用拒绝策略,默认的拒绝策略是抛出一个运行时错误(RejectedExecutionException)。值得一提的是,当初始化时用的工作队列为LinkedBlockingDeque时,这个值将无效。
- keepAliveTime:存活时间,
当非核心空闲超过这个时间将被回收,同时空闲核心线程是否回收受allowCoreThreadTimeOut影响。
- unit:keepAliveTime的单位。
- workQueue:任务队列
常用有三种队列,即SynchronousQueue,LinkedBlockingDeque(无界队列),ArrayBlockingQueue(有界队列)。
- threadFactory:线程工厂,
ThreadFactory是一个接口,用来创建worker。通过线程工厂可以对线程的一些属性进行定制。默认直接新建线程。
- RejectedExecutionHandler:拒绝策略
也是一个接口,只有一个方法,当线程池中的资源已经全部使用,添加新线程被拒绝时,会调用RejectedExecutionHandler的rejectedExecution法。默认是抛出一个运行时异常。
2.9. synchronized 和 lock 有什么区别
- Lock的加锁和解锁都是由java代码配合native方法(调用操作系统的相关方法)实现的,而synchronize的加锁和解锁的过程是由JVM管理的
- 当一个线程使用synchronize获取锁时,若锁被其他线程占用着,那么当前只能被阻塞,直到成功获取锁。而Lock则提供超时锁和可中断等更加灵活的方式,在未能获取锁的条件下提供一种退出的机制,更加灵活
- synchronize对线程的同步仅提供独占模式,而Lock即可以提供独占模式,也可以提供共享模式
2.10. 了解volatile关键字不
- volatile是Java提供的最轻量级的同步机制,保证了共享变量的可见性,被volatile修饰的变量,如果值发生了变化,其他线程立即可见,避免出现脏读的情况
- volatile禁止了指令重排,可以保证程序执行的有序性,但是由于禁止了指令重排,所以JVM相关的优化没了,效率会偏弱
2.11. synchronized和volatile有什么区别
- volatile本质是告诉JVM当前变量值不能直接去拿寄存器的值,也就是缓存,需要去从主存中读取,synchronized则是锁定当前变量,只有当前线程才可以访问,其它线程被阻塞
- volatile只能被用在变量级别,synchronized可以使用在变量、方法、类级别中
- volatile只能实现变量的修改可见性,synchronized则可以保证变量的修改可见性和原子性
- volatile不会造成线程阻塞,synchronized可能会造成线程阻塞
- volatile标记的变量不会被编译器优化,synchronized标记的变量可以被编译器优化
2.12. 简述java内存分配与回收策略以及Minor GC和Major GC(full GC)
- 内存分配
-
- 栈区:Java栈分为虚拟机栈和本地方法栈
- 堆区:堆被所有线程共享区域,在虚拟机启动时创建,唯一目的存放对象实例。堆区是gc的主要区域,通常情况下分为两个区块:年轻代和年老代,默认比例为1:2。更细一点年轻代又分为Eden区,主要放新创建对象,From survivor 和 To survivor 保存gc后幸存下的对象,默认情况下各自占比 8:1:1。
- 方法区:被所有线程共享区域,用于存放已被虚拟机加载的类信息,常量,静态变量等数据。被Java虚拟机描述为堆的一个逻辑部分。习惯是也叫它永久代(permanment generation)
- 程序计数器:当前线程所执行的行号指示器。通过改变计数器的值来确定下一条指令,比如循环,分支,跳转,异常处理,线程恢复等都是依赖计数器来完成。线程私有的。
- 回收策略以及Minor GC(新生代垃圾回收)和Major GC(老年代垃圾回收)
-
- 对象优先在堆的Eden区分配
- 大对象直接进入老年代
- 长期存活的年轻代对象进入老年代
2.13. Java死锁如何避免
- 造成死锁的原因
-
- 一个资源只能被一个线程使用
- 一个线程在阻塞等待某个资源的时候,不释放已占有的资源
- 一个线程已经获得的资源,在未使用完之前,不能被强行剥夺
- 若干线程形成头尾相接的循环等待资源关系
- 这是造成死锁必须要达到的4个条件,如果要避免死锁,只需要不满足其中某一个条件即可。而其中前3个条件是作为锁要符合的条件,所以要避免死锁就需要打破第4个条件,不出现循环等待锁的关系。
- 在开发过程中:
-
- 要注意加锁的顺序,保证每个线程按同样的顺序进行加锁
- 要注意加锁的时限,可以设置一个超时时间
- 定期检查是否会出现死锁,预防机制的处理,确保第一时间发现并解决
3. Java框架篇
3.1. 简述SpringMVC的工作流程
以上就是SpringMVC的流程图
1、用户将请求发送,由前端控制器DispatcherServlet来拦截并接收请求
2、前端控制器DispatcherServlet收到请求之后调用处理器映射器(HanlderMapping),去查找与请求对应的Handler
3、处理器映射器(HanlderMapping)找到具体的处理器(根据xml配置、注解等方式进行查找),返回一个处理器执行链(是一个包含处理器对象和拦截器(Interceptor)的对象【如果有拦截器的话】)
4、然后前端控制器DispatcherServlet调用了处理器适配器HandlerAdapter
5、处理器适配器会找到具体Handler的具体方法,并将获取到的参数执行完成之后将结果继续返回给DispatcherServlet(结果通常是ModelAndView)
6、然后前端控制器(DispatcherServlet)会调用视图解析器,并将ModelAndView传给它(ViewResolver)
7、视图解析器(ViewResolver)将获得的参数从逻辑视图转换为物理视图对象(View)返回给前端控制器(DispatcherServlet)
8、前端控制器(DispatcherServlet)调用物理视图进行渲染并返回。
9、前端控制器(DispatcherServlet)将渲染完毕的页面响应给用户
3.2. Spring或者SpringMVC中常用的常用注解
- @Component 基本注解,标识一个受Spring管理的组件
- @Controller 标识为一个表示层的组件
- @Service 标识为一个业务层的组件
- @Repository 标识为一个持久层的组件
- @Autowired 自动装配
- @RequestMapping() 完成请求映射
- @PathVariable 映射请求URL中占位符到请求处理方法的形参
3.3. SpringMVC中如何返回JSON数据
Step1:在项目中加入json转换的依赖,例如jackson,fastjson,gson等
Step2:在请求处理方法中将返回值改为具体返回的数据的类型, 例如数据的集合类List<Employee>等
Step3:在请求处理方法上使用@ResponseBody注解
3.4. 谈谈你对Spring的理解
Spring 容器的主要核心是:
控制反转(IOC),传统的 java 开发模式中,当需要一个对象时,我们会自己使用 new 或者 getInstance 等直接或者间接调用构造方法创建一个对象。而在 spring 开发模式中,spring 容器使用了工厂模式为我们创建了所需要的对象,不需要我们自己创建了,直接调用spring 提供的对象就可以了,这是控制反转的思想。
@Component
public class UserService {
public void addUser() {
System.out.println("User added!");
}
}
public class MainApp {
public static void main(String[] args) {
// 获取Spring容器
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
// 使用容器来获取对象,而不是直接 new
UserService userService = context.getBean(UserService.class);
userService.addUser();
}
}
依赖注入(DI),spring 使用 javaBean 对象的 set 方法或者带参数的构造方法为我们在创建所需对象时将其属性自动设置所需要的值的过程,就是依赖注入的思想。
@Component
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void addUser() {
userRepository.save(new User());
}
}
@Component
public class UserRepository {
public void save(User user) {
System.out.println("User saved!");
}
}
面向切面编程(AOP),在面向对象编程(oop)思想中,我们将事物纵向抽成一个个的对象。而在面向切面编程中,我们将一个个的对象某些类似的方面横向抽成一个切面,对这个切面进行一些如权限控制、事物管理,记录日志等公用操作处理的过程就是面向切面编程的思想。AOP 底层是动态代理,如果是接口采用 JDK 动态代理,如果是类采用CGLIB 方式实现动态代理。
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.UserService.*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("方法调用前: " + joinPoint.getSignature().getName());
}
@After("execution(* com.example.service.UserService.*(..))")
public void logAfter(JoinPoint joinPoint) {
System.out.println("方法调用后: " + joinPoint.getSignature().getName());
}
}
@Service
public class UserService {
public void addUser() {
System.out.println("用户已添加");
}
}
public class MainApp {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = context.getBean(UserService.class);
userService.addUser();
}
}
3.5. Spring中常用的设计模式
3.5.1. 继承和实现
继承就是说我拿到父类的方法直接去用,只能是单继承,继承后的子类可以使用父类中的非私有成员方法,可以直接用这些方法和功能,如果需要,也可以重写父类的方法来增强或修改行为
实现则是我对接口的一种处理方式和逻辑,实现方法的签名(方法签名是指方法的名称以及它的参数类型和顺序,不包括方法的返回类型或访问修饰符), 需要按照接口的要求提供具体功能,同时可以自由发挥,进行扩展和定制
3.5.2. 抽象类中实现类的特点
在Java中,抽象类既可以包含抽象方法(没有实现的方法),也可以包含已实现的方法(有具体实现的方法)。因此,抽象类可以实现某些具体功能。
- 抽象方法:只声明方法签名,不提供具体实现。抽象类的子类必须提供这些抽象方法的实现。
- 具体方法:抽象类也可以有普通的、已实现的方法,子类可以直接使用这些方法,或者根据需要进行重写。
特点:
- 可以包含实现方法:抽象类可以为某些方法提供默认实现,这样子类可以继承这些方法而无需重新实现。
- 不能实例化:虽然抽象类可以实现一些方法,但抽象类本身不能被实例化。你不能通过
new
关键字创建一个抽象类的对象。 - 可以包含非抽象方法:这些方法可以被子类直接使用,或者子类也可以选择重写它们。
- 必须由子类实现所有抽象方法:如果抽象类中有抽象方法,子类必须实现这些抽象方法,除非子类本身也是抽象类。
abstract class Animal {
// 抽象方法,子类必须实现
public abstract void makeSound();
// 已实现的方法,子类可以直接使用
public void sleep() {
System.out.println("The animal is sleeping");
}
}
// 具体的子类必须实现抽象方法
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Woof!");
}
}
public class Main {
public static void main(String[] args) {
Animal dog = new Dog(); // 使用多态
dog.makeSound(); // 调用子类的具体实现方法
dog.sleep(); // 调用抽象类中的已实现方法
}
}
3.5.3. 类适配器模式
3.5.3.1. 架构
使用继承的方式,将需要适配的类转换为目标接口的子类,实现目标接口的所有方法,同时继承适配类的实现,用以完成一些适配逻辑。
3.5.3.2. 目标接口方法
/**
* @author Created by njy on 2023/6/8
* 目标接口(target)
*/
public interface Target {
/**
* 翻译
* @param source 母语
* @param target 要翻译成的语种
* @param words 内容
*/
void translate(String source,String target,String words);
}
3.5.3.3. 源对象类
/**
* @author Created by njy on 2023/6/8
* 源对象(source):充当翻译
*/
public class Translator {
//英——》汉
public void translateInZh(String words){
if("hello world!".equals(words)){
System.out.println("翻译成中文:”你好世界!“");
}
}
//汉——》英
public void translateInEn(String words){
if("你好世界!".equals(words)){
System.out.println("Translate in English:”hello world!“");
}
}
}
3.5.3.4. 子类继承父类(源对象类)并且实现接口
/**
* @author Created by njy on 2023/6/11
* 类适配器:通过多重继承目标接口和被适配者类方式来实现适配
*/
public class ClassAdapter extends Translator implements Target {
@Override
public void translate(String source, String target, String words) {
if("中文".equals(source) && "英文".equals(target)) {
//汉--》英
this.translateInEn(words);
} else {
//英--》汉
this.translateInZh(words);
}
}
}
3.5.3.5. 测试类适配器的设计
/**
* @author Created by njy on 2023/6/8
*/
@SpringBootTest
public class TestAdapter {
//类适配器
@Test
void classAdapter(){
//创建一个类适配器对象
ClassAdapter adapter=new ClassAdapter();
adapter.translate("中文", "英文", "你好世界!");
adapter.translate("英语","中文","hello world!");
}
}
3.5.4. 对象适配器模式
3.5.4.1. 架构
通过组合的方式,将适配对象与目标接口组合,实现目标接口的所有方法,并在适配类中调用需要适配对象的方法。
3.5.4.2. 目标接口方法
/**
* @author Created by njy on 2023/6/8
* 目标接口(target)
*/
public interface Target {
/**
* 翻译
* @param source 母语
* @param target 要翻译成的语种
* @param words 内容
*/
void translate(String source,String target,String words);
}
3.5.4.3. 源对象类
/**
* @author Created by njy on 2023/6/8
* 源对象(source):充当翻译
*/
public class Translator {
//英——》汉
public void translateInZh(String words){
if("hello world!".equals(words)){
System.out.println("翻译成中文:”你好世界!“");
}
}
//汉——》英
public void translateInEn(String words){
if("你好世界!".equals(words)){
System.out.println("Translate in English:”hello world!“");
}
}
}
3.5.4.4. 对象适配器,采用组合的方式
/**
* @author Created by njy on 2023/6/11
* 对象适配器:使用组合的方式
*/
public class ObjectAdapter implements Target {
private Translator translator=new Translator();
@Override
public void translate(String source, String target, String words) {
if("中文".equals(source) && "英文".equals(target)) {
//汉--》英
translator.translateInEn(words);
} else {
//英--》汉
translator.translateInZh(words);
}
}
}
3.5.4.5. 测试
/**
* @author Created by njy on 2023/6/8
*/
@SpringBootTest
public class TestAdapter {
//对象适配器
@Test
void ObjectAdapter(){
ObjectAdapter adapter=new ObjectAdapter();
adapter.translate("中文", "英文", "你好世界!");
adapter.translate("英语","中文","hello world!");
}
}
3.5.5. 接口适配器模式
3.5.5.1. 架构
主要适用于需要被适配的接口中,只有用到个别接口,也就是说不需要实现它的全部接口。通过一个中间抽象类或接口实现。
3.5.5.2. 目标接口方法,用于解释接口适配器
/**
* @author Created by njy on 2023/6/11
* target2:用于解释接口适配器
*/
public interface target2 {
/**
* 翻译
* @param source 母语
* @param target 要翻译成的语种
* @param words 内容
*/
void translate(String source,String target,String words);
//无用方法,仅仅用来说明接口适配器
void a();
}
3.5.5.3. 源对象类
/**
* @author Created by njy on 2023/6/8
* 源对象(source):充当翻译
*/
public class Translator {
//英——》汉
public void translateInZh(String words){
if("hello world!".equals(words)){
System.out.println("翻译成中文:”你好世界!“");
}
}
//汉——》英
public void translateInEn(String words){
if("你好世界!".equals(words)){
System.out.println("Translate in English:”hello world!“");
}
}
}
3.5.5.4. AbstractAdapter抽象类
/**
* @author Created by njy on 2023/6/8
* AdapterTranslate抽象类
*/
public abstract class AbstractAdapter implements target2 {
private Translator translator=new Translator();
@Override
public void translate(String source, String target, String words) {
if("中文".equals(source) && "英文".equals(target)) {
//汉--》英
translator.translateInEn(words);
} else {
//英--》汉
translator.translateInZh(words);
}
}
@Override
public void a() {
}
}
3.5.5.5. InterfaceAdapter:接口适配器
/**
* @author Created by njy on 2023/6/11
* 接口适配器:当不需要全部实现接口方法时。
* 可以先设计一个抽象类实现接口AdapterTranslate
* AdapterTranslate实现,不用去实现b()方法
*/
public class InterfaceAdapter extends AbstractAdapter {
public void translate(String source, String target, String words) {
super.translate(source,target,words);
}
}
3.5.5.6. 测试类
/**
* @author Created by njy on 2023/6/8
*/
@SpringBootTest
public class TestAdapter {
//接口适配器
@Test
void interfaceAdapter(){
InterfaceAdapter adapter=new InterfaceAdapter();
adapter.translate("中文", "英文", "你好世界!");
adapter.translate("英语","中文","hello world!");
}
}
3.5.6. 对比总结
类适配器模式:
- 实现方式:通过继承实现适配器功能。
- 适配器类需要继承目标类(或者说源类),并且实现目标接口中的方法。
- 特点:
-
- 适配器与被适配者之间的关系是继承关系。
- 由于 Java 是单继承,所以类适配器模式无法适配多个类(即只适配一个目标类)。
- 适用场景:适配器与适配的类之间有很强的关系,且适配器只需要适配一个类。
对象适配器模式:
- 实现方式:通过组合实现适配器功能。
- 适配器类实现目标接口,并且持有一个被适配者对象,通过调用该对象的方法来实现接口功能。
- 特点:
-
- 适配器与被适配者之间的关系是组合关系。
- 可以适配多个目标类,因为组合方式可以随时替换被适配的对象。
- 适用场景:当我们希望适配的类是灵活可变的,而不是固定的继承结构时。
接口适配器模式(或缺省适配器模式):
- 实现方式:通过一个抽象类来实现接口,并为接口中的方法提供空实现,然后子类可以选择性地重写所需的方法。
- 特点:
-
- 接口适配器模式的重点是简化接口的实现,当接口有很多方法时,我们不需要实现所有方法,而只需实现我们关心的方法。
- 适用于当我们不想全部实现接口中的所有方法时,可以通过继承抽象类,选择性地实现需要的功能。
- 适用场景:当接口有很多方法,而子类只需要部分实现时,可以使用该模式避免不必要的方法实现。
对比总结:
- 类适配器模式:通过继承源类(或目标类)和实现接口来实现适配,耦合度高,不能适配多个类。
- 对象适配器模式:通过组合源类和实现接口来实现适配,灵活性高,可以适配多个类。
- 接口适配器模式:通过抽象类实现接口并提供默认实现,子类可以选择性地实现接口中的部分方法,适合处理接口中方法过多的情况。
3.6. MyBatis中 #{}和${}的区别是什么
#{}
- 预编译处理:
-
- 在 MyBatis 中,
#{}
是用于预编译的占位符,会将 SQL 中的#{}
替换为?
,然后 MyBatis 会通过 JDBC 的PreparedStatement
来执行 SQL 语句。具体来说,它会使用PreparedStatement.setXxx()
方法将参数值绑定到 SQL 语句中的占位符?
上。 - 预编译的好处:
- 在 MyBatis 中,
-
-
- 防止 SQL 注入:由于
#{}
使用的是参数绑定方式,输入的参数值不会直接拼接到 SQL 语句中,因此可以有效防止 SQL 注入攻击。 - 提高性能:使用
PreparedStatement
预编译的 SQL 语句可以在数据库中缓存,避免多次解析,提升性能。
- 防止 SQL 注入:由于
-
${}
- 字符串替换:
-
${}
是直接将表达式中的值按字符串拼接到 SQL 语句中,相当于普通的字符串替换。例如,如果使用${id}
,MyBatis 会将${id}
替换为对应的变量值(例如1
),最终生成的 SQL 语句可能是SELECT * FROM user WHERE id = 1
。- 使用场景:
-
-
${}
适合用于动态生成 SQL 语句的一部分,比如动态表名、列名等场景。但应谨慎使用,尤其是在涉及用户输入时,容易导致 SQL 注入问题。
-
<!-- 使用 #{}, 安全防注入 -->
<select id="getUserById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
<!-- 使用 ${}, 直接替换变量值,容易导致 SQL 注入 -->
<select id="getUserByName" resultType="User">
SELECT * FROM user WHERE name = '${name}'
</select>
如果用户输入了特殊字符(如 name = 'abc' OR '1'='1'),使用 ${} 可能会生成危险的 SQL 语句:
SELECT * FROM user WHERE name = 'abc' OR '1'='1',从而引发 SQL 注入攻击。
- 优先使用
#{}
:除非确实需要动态拼接 SQL 语句的一部分,否则应始终使用#{}
进行参数传递,以防止 SQL 注入风险。 - 动态 SQL 场景:在使用 MyBatis 的动态 SQL 特性(如
<if>
、<foreach>
)时,通常也是结合#{}
来安全地传递参数。 - 安全使用
#{}
:用于 SQL 语句中的参数传递,防止 SQL 注入。 - 谨慎使用
${}
:只应在动态 SQL 场景下拼接表名、列名等 SQL 结构内容,且要确保输入经过充分的验证
3.7. Mybatis中的一级缓存和二级缓存
3.8. 如何再mybatis获取自动生成的主键值
在<insert>标签中使用 useGeneratedKeys和keyProperty 两个属性来获取自动生成的主键值。
<insert id=”insertname” usegeneratedkeys=”true” keyproperty=”id”>
insert into names (name) values (#{name})
</insert>
3.9. 简述Mybatis的动态SQL,列出常用的6个标签及作用
3.9.1. <if>
标签
用途:用于判断某个条件是否为真,如果为真则生成 SQL 语句的一部分。
<select id="findUser" resultType="User">
SELECT * FROM users
<where>
<if test="username != null">
AND username = #{username}
</if>
<if test="age != null">
AND age = #{age}
</if>
</where>
</select>
在上述示例中,如果 username 和 age 不为 null,将会生成相应的 SQL 条件。
3.9.2. <where>
标签
用途:在条件查询中自动添加 WHERE
关键字,并处理 AND 或 OR 的问题。它会自动处理条件前的 AND/OR,以确保生成的 SQL 语句的正确性。
<select id="findUser" resultType="User">
SELECT * FROM users
<where>
<if test="username != null">
username = #{username}
</if>
<if test="age != null">
AND age = #{age}
</if>
</where>
</select>
这里的 <where> 标签会自动在 SQL 中添加 WHERE,并去掉第一个 AND。
3.9.3. <trim>
标签
用途:可以在 SQL 语句前后添加或去掉指定的字符,类似于字符串的 trim 功能。
<update id="updateUser">
UPDATE users
<trim prefix="SET" suffixOverrides=",">
<if test="username != null">
username = #{username},
</if>
<if test="age != null">
age = #{age},
</if>
</trim>
WHERE id = #{id}
</update>
在这个示例中,<trim> 标签会去掉最后的多余逗号,并在 SQL 语句前添加 SET 关键字。
3.9.4. <set>
标签
用途:主要用于 UPDATE
语句,自动去除多余的逗号。
<update id="updateUser">
UPDATE users
<set>
<if test="username != null">
username = #{username},
</if>
<if test="age != null">
age = #{age},
</if>
</set>
WHERE id = #{id}
</update>
<set> 标签会自动去除最后多余的逗号。
3.9.5. <choose>
<when>
<otherwise>
标签
用途:类似于 Java 中的 switch-case
语句。<when>
相当于 case
,<otherwise>
相当于 default
。
<select id="findUser" resultType="User">
SELECT * FROM users
<where>
<choose>
<when test="username != null">
username = #{username}
</when>
<when test="age != null">
age = #{age}
</when>
<otherwise>
status = 'ACTIVE'
</otherwise>
</choose>
</where>
</select>
在这个示例中,<choose> 标签会根据条件进行选择,只会生成一个条件。如果前面条件不满足,则执行 <otherwise> 中的语句。
3.9.6. <foreach>
标签
用途:用于遍历集合,如 List
或 数组
,生成 IN 查询或批量插入的 SQL 语句。
<select id="findUserByIds" resultType="User">
SELECT * FROM users WHERE id IN
<foreach item="id" collection="idList" open="(" separator="," close=")">
#{id}
</foreach>
</select>
<foreach> 标签会遍历 idList 集合,生成类似于 id IN (1, 2, 3) 的 SQL 语句。
3.10. Mybatis如何完成MySQL的批量操作
3.10.1. 使用 foreach
标签
MyBatis 提供了 foreach
标签,可以用来进行批量插入、更新或删除。foreach
标签会遍历传入的集合,并生成相应的 SQL 语句。
- 批量插入示例:
假设你有一个 User
对象列表,并希望将这些用户批量插入到数据库中。
<insert id="batchInsertUsers">
INSERT INTO users (id, name, email)
VALUES
<foreach collection="list" item="user" index="index" separator=",">
(#{user.id}, #{user.name}, #{user.email})
</foreach>
</insert>
void batchInsertUsers(List<User> userList);
INSERT INTO users (id, name, email)
VALUES
(1, 'Alice', 'alice@example.com'),
(2, 'Bob', 'bob@example.com');
3.10.2. 配置 JDBC
的批处理
MyBatis 的批处理机制依赖于 JDBC 的批量操作特性。在执行大批量数据操作时,你可以配置 MyBatis 使用批处理模式。
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
for (User user : userList) {
userMapper.insertUser(user);
}
sqlSession.commit();
} catch (Exception e) {
sqlSession.rollback();
e.printStackTrace();
} finally {
sqlSession.close();
}
在这个例子中,通过将 ExecutorType 设置为 BATCH,
MyBatis 会将多个 insertUser 操作批量提交到数据库,而不是一条一条执行。
3.10.3. 直接执行批量 SQL
你也可以在 Mapper 中直接编写批量操作的 SQL 语句,例如批量更新或者删除。
<update id="batchUpdateUsers">
<foreach collection="list" item="user" index="index" separator=";">
UPDATE users
SET name = #{user.name}, email = #{user.email}
WHERE id = #{user.id}
</foreach>
</update>
<delete id="batchDeleteUsers">
DELETE FROM users WHERE id IN
<foreach collection="list" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
3.11. 如何理解SpringBoot框架
Spring Boot 是 Spring 开源组织下的子项目,是 Spring 组件一站式解决方案,主要是简化了使用 Spring 的难度,简省了繁重的配置,提供了各种启动器,开发者能快速上手。
优点:
- 独立运行
Spring Boot而且内嵌了各种servlet容器,Tomcat、Jetty等,现在不再需要打成war包部署到容器中,Spring Boot只要打成一个可执行的jar包就能独立运行,所有的依赖包都在一个jar包内。
- 简化配置
spring-boot-starter-web启动器自动依赖其他组件,简少了maven的配置。除此之外,还提供了各种启动器,开发者能快速上手。
- 自动配置
Spring Boot能根据当前类路径下的类、jar包来自动配置bean,如添加一个spring-boot-starter-web启动器就能拥有web的功能,无需其他配置。
- 无代码生成和XML配置
Spring Boot配置过程中无代码生成,也无需XML配置文件就能完成所有配置工作,这一切都是借助于条件注解完成的,这也是Spring4.x的核心功能之一。
- 应用监控
Spring Boot提供一系列端点可以监控服务及应用,做健康检测。
3.12. Spring Boot 的核心注解是哪个,它主要由哪几个注解组成的
启动类上面的注解是@SpringBootApplication,主要组合包含了以下 3 个注解:
@SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。
@EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。
@ComponentScan:Spring组件扫描
3.13. Spring Boot自动配置原理是什么
注解 @EnableAutoConfiguration, @Configuration, @ConditionalOnClass 就是自动配置的核心,首先它得是一个配置文件,其次根据类路径下是否有这个类去自动配置。
@EnableAutoConfiguration是实现自动配置的注解
@Configuration表示这是一个配置文件
3.14. SpringBoot配置文件有哪些 怎么实现多环境配置
Spring Boot 的核心配置文件是 application 和 bootstrap 配置文件。
application 配置文件这个容易理解,主要用于 Spring Boot 项目的自动化配置。
application和bootstrap的区别:
加载顺序不同:
bootstrap.properties
:
-
- 由 Spring Cloud 相关组件使用,通常在应用程序初始化阶段加载,优先于
application.properties
。它被用于配置与外部系统(如配置中心、服务注册与发现)相关的属性。 - 加载时机更早,用于加载配置源,比如从外部配置中心(如 Spring Cloud Config Server)获取配置信息。
- 加载过程中,一些系统的环境变量(如
spring.application.name
)可能已经生效。
- 由 Spring Cloud 相关组件使用,通常在应用程序初始化阶段加载,优先于
application.properties
:
-
- 由 Spring Boot 使用,加载顺序稍后,在应用程序上下文创建时加载,用于定义应用程序的主要配置。
- 加载时机稍后,通常用于配置应用程序的业务相关属性,比如数据库连接信息、端口号、日志配置等。
作用范围不同:
bootstrap.properties
:
-
- 主要用于 Spring Cloud 中的初始化配置,包括从远程配置中心获取配置、设置应用的名称(
spring.application.name
)以及连接配置中心的 URL 等信息。 - 这些属性在 Spring Boot 的应用上下文初始化之前使用,因此必须在
bootstrap.properties
中配置。
- 主要用于 Spring Cloud 中的初始化配置,包括从远程配置中心获取配置、设置应用的名称(
application.properties
:
-
- 主要用于配置应用程序的业务逻辑和环境,如数据库连接、缓存、端口、日志等。它加载的时机是在 Spring Boot 完成了基本的启动步骤之后,因此适用于应用程序的业务配置。
使用场景不同:
bootstrap.properties
:
-
- 通常用于 Spring Cloud 环境,适合场景包括从配置中心拉取配置、服务注册和发现等。典型的场景是微服务架构中,配置中心用于统一管理各个服务的配置信息。
- 例如,Spring Cloud Config 配置中心的相关配置应该放在
bootstrap.properties
中。
application.properties
:
-
- 主要用于本地和应用程序级别的配置,不依赖外部系统。所有与业务相关的配置都可以放在这个文件中,例如数据库配置、消息队列配置等。
application和bootstrap的联系:
- 层次关系:
bootstrap.properties
和application.properties
共同作用于同一个应用程序,它们的配置可以互补使用。bootstrap.properties
的配置通常用于系统级的外部配置,而application.properties
则用于应用级的配置。 - 配置继承:
bootstrap.properties
加载后,其配置会被application.properties
中的配置继承。例如,从配置中心加载的配置可以覆盖本地的application.properties
中的同名属性。
-
- 如果在
application.properties
中没有配置相关属性,Spring Boot 会回退到bootstrap.properties
的配置中。 - 但如果在
application.properties
和bootstrap.properties
中都配置了相同的属性,优先使用application.properties
中的配置,除非配置中心的属性有更高的优先级(比如配置中心设置了强制覆盖策略)。
- 如果在
3.15. SpringBoot和SpringCloud是什么关系
Spring Boot 是 Spring 的一套快速配置脚手架,可以基于Spring Boot 快速开发单个微服务,Spring Cloud是一个基于Spring Boot实现的开发工具;Spring Boot专注于快速、方便集成的单个微服务个体,Spring Cloud关注全局的服务治理框架; Spring Boot使用了默认大于配置的理念,很多集成方案已经帮你选择好了,能不配置就不配置,Spring Cloud很大的一部分是基于Spring Boot来实现,必须基于Spring Boot开发。
可以单独使用Spring Boot开发项目,但是Spring Cloud离不开 Spring Boot。
3.16. SpringCloud都用过哪些组件 介绍一下作用
- Spring Cloud Config(配置管理中心)
-
- 作用:Spring Cloud Config 提供集中化的外部配置管理方案,允许多个微服务共享统一的配置文件,并且支持动态刷新配置。开发者可以通过它将配置存储在 Git、SVN 等远程仓库中,微服务可以在运行时从配置中心获取最新的配置。
- 使用场景:当你需要动态地改变微服务的配置而无需重启应用时,Config 是非常有用的。
- Spring Cloud Netflix (Eureka、Ribbon、Hystrix 等)(服务注册与发现、负载均衡、断路器)
-
- 作用:
-
-
- Eureka:服务注册与发现。Eureka 是一个服务注册中心,允许微服务动态注册和发现其他服务。
- Ribbon:客户端负载均衡器,用于在多个服务实例之间分发请求,实现软负载均衡。
- Hystrix:断路器,提供容错机制,在远程服务调用失败时避免服务雪崩效应。
-
-
- 使用场景:当多个微服务之间需要互相调用,并且需要负载均衡和容错机制时,Netflix 组件非常有用。
- Spring Cloud Gateway(API 网关)
-
- 作用:Spring Cloud Gateway 是微服务架构中的 API 网关,主要用于请求路由和过滤。它可以将外部请求路由到具体的微服务,并且可以在请求到达后端服务之前进行权限验证、负载均衡、限流等操作。
- 使用场景:在微服务架构中,网关是所有外部请求的入口,Gateway 可以帮助简化路由规则,提供安全保障以及流量控制。
- Spring Cloud OpenFeign(声明式服务调用)
-
- 作用:OpenFeign 是一种声明式的 HTTP 客户端,用于简化微服务之间的通信。开发者只需定义接口并注解,无需手动编写 HTTP 客户端代码即可实现微服务之间的远程调用,OpenFeign 也集成了 Ribbon 以实现负载均衡。
- 使用场景:当多个微服务之间需要频繁通信且希望代码简洁时,OpenFeign 可以显著减少开发者的工作量。
- Spring Cloud Sleuth + Zipkin(分布式链路追踪)
-
- 作用:
-
-
- Sleuth:为微服务请求链路增加唯一的追踪 ID,帮助开发者在分布式系统中跟踪请求的流转过程。
- Zipkin:集中展示和分析 Sleuth 生成的链路追踪数据,帮助开发者定位性能瓶颈和问题。
-
-
- 使用场景:在分布式系统中,一个请求往往会经过多个微服务的处理,Sleuth 和 Zipkin 的配合能够帮助开发者清晰地看到整个调用链条,发现潜在问题。
这五大组件帮助开发者高效地构建和管理微服务架构,简化了配置、服务发现、通信、负载均衡、容错以及监控等重要功能的实现。
3.17. Nacos作用以及注册中心原理
(一)Nacos 的作用
- 服务发现
-
- 在分布式系统中,服务数量众多且动态变化。Nacos 作为服务发现工具,能够让服务消费者轻松地找到服务提供者的网络位置(如 IP 地址和端口号)。例如,一个电商系统中,订单服务需要调用库存服务来检查商品库存,订单服务通过 Nacos 可以快速定位库存服务的位置,从而进行调用。
- 服务注册
-
- 服务提供者在启动时,将自己的服务信息(包括服务名称、IP 地址、端口号等)注册到 Nacos 服务器。这样,当新的服务实例上线或者旧的服务实例下线时,Nacos 能够及时感知并更新服务列表。以微服务架构中的用户服务为例,当启动一个新的用户服务实例时,它会向 Nacos 发送注册请求,告知自己的相关信息。
- 配置管理
-
- Nacos 可以集中管理微服务的配置信息。它支持动态配置更新,当配置发生变化时,Nacos 可以通知相关的服务实例重新加载配置。例如,对于一个具有多个服务实例的推荐系统,推荐算法的参数配置可以在 Nacos 中统一管理,当需要调整算法参数时,只需在 Nacos 中修改配置,各个服务实例就能自动更新。
(二)Nacos 注册中心原理
- 服务注册流程
-
- 服务提供者在启动后,会主动向 Nacos 服务器发送一个 HTTP 请求(默认是 POST 请求),请求的内容包含服务的基本信息,如服务名称(例如 “user - service”)、服务实例的 IP 地址(如 “192.168.1.100”)、端口号(如 “8080”)等。
- Nacos 服务器收到注册请求后,会将服务实例信息存储在内存中的一个数据结构(通常是一个双层的 Map,外层 Map 的键是服务名称,内层 Map 的键是服务实例的唯一标识,值是服务实例的详细信息)中。同时,Nacos 还可能将这些信息持久化到磁盘或者数据库中,以防止服务器重启后数据丢失。
- 服务发现流程
-
- 服务消费者在需要调用服务提供者时,会向 Nacos 服务器发送一个服务发现请求(通常是 GET 请求),请求中包含要查找的服务名称。
- Nacos 服务器根据服务名称,从内存中的服务实例列表中查找对应的服务实例信息。它可以采用多种负载均衡策略(如轮询、随机、权重等)来选择一个合适的服务实例返回给服务消费者。例如,如果有多个库存服务实例,Nacos 可以按照轮询策略依次返回不同的实例,以均衡服务调用负载。
- 心跳机制和健康检查
-
- 服务提供者会定期(通过发送心跳请求)向 Nacos 服务器报告自己的健康状态。Nacos 服务器如果在一定时间内没有收到服务提供者的心跳,就会将该服务实例标记为不健康状态。
- 当服务消费者进行服务发现时,Nacos 会尽量避免返回不健康的服务实例。同时,Nacos 也可以通过主动探测(如发送 HTTP 请求检查服务端点是否正常响应)等方式来检查服务实例的健康状况。
3.18. Fegin工作原理
(一)Feign 简介
Feign 是一个声明式的 Web 服务客户端,它让编写 Web 服务客户端变得更加简单。它基于 Netflix Feign 实现,在 Spring Cloud 中被广泛用于微服务之间的调用。
(二)工作原理
- 接口定义
-
- 首先,开发人员需要定义一个接口,这个接口使用 Feign 的注解来描述对远程服务的调用。例如,定义一个接口来调用用户服务的获取用户信息方法:
@FeignClient(name = "user - service")
public interface UserServiceClient {
@GetMapping("/user/{id}")
User getUserById(@PathVariable("id") Long id);
}
- 这里
@FeignClient
注解指定了要调用的服务名称为 “user - service”,接口中的方法使用@GetMapping
注解定义了远程服务的 HTTP 请求路径和方法类型(这里是 GET 请求)。
- 动态代理生成
-
- 在 Spring 容器启动时,Feign 会根据定义的接口生成一个动态代理类。这个动态代理类会在方法被调用时,根据接口中的注解信息构建一个 HTTP 请求。例如,当调用
getUserById
方法时,动态代理类会根据@GetMapping
注解中的路径"/user/{id}"
和方法参数构建一个完整的 HTTP 请求,请求的 URL 可能是 “http://user - service/user/1”(假设id = 1
)。
- 在 Spring 容器启动时,Feign 会根据定义的接口生成一个动态代理类。这个动态代理类会在方法被调用时,根据接口中的注解信息构建一个 HTTP 请求。例如,当调用
- HTTP 请求发送
-
- 动态代理类构建好 HTTP 请求后,会通过底层的 HTTP 客户端(如 OkHttp 或 Spring 的 RestTemplate)将请求发送到指定的服务。在发送请求时,还会处理请求头、请求参数等信息。例如,可能会添加认证信息到请求头中,以确保服务调用的合法性。
- 响应处理
-
- 当收到服务端的响应后,Feign 会将响应数据进行反序列化(如果是 JSON 数据,会使用相应的 JSON 反序列化库),并将反序列化后的结果返回给调用者。例如,如果服务端返回的是一个 JSON 格式的用户对象,Feign 会将其反序列化为一个
User
类型的 Java 对象并返回。
- 当收到服务端的响应后,Feign 会将响应数据进行反序列化(如果是 JSON 数据,会使用相应的 JSON 反序列化库),并将反序列化后的结果返回给调用者。例如,如果服务端返回的是一个 JSON 格式的用户对象,Feign 会将其反序列化为一个
3.19. Spring循环依赖问题
(一)什么是循环依赖
在 Spring 中,循环依赖是指两个或多个 Bean 之间互相依赖。例如,Bean A 依赖 Bean B,同时 Bean B 又依赖 Bean A。这就形成了一个循环引用的关系。
(二)Spring 解决循环依赖的原理(以单例 Bean 为例)
- 三级缓存机制
-
- Spring 使用了三级缓存来解决循环依赖问题。这三级缓存分别是:
- singletonObjects:一级缓存,用于存放完全初始化好的单例 Bean。这个缓存中的 Bean 是可以直接使用的,它的键是 Bean 的名称,值是 Bean 实例。
- earlySingletonObjects:二级缓存,用于存放早期暴露出来的单例 Bean。所谓早期暴露,是指 Bean 还没有完全初始化,但已经创建了实例,并且可以将这个实例提前暴露出来,供其他 Bean 引用。
- singletonFactories:三级缓存,用于存放创建 Bean 的工厂对象。这个工厂对象可以用于创建早期暴露的 Bean 实例。
- 循环依赖解决过程示例
-
- 假设存在两个 Bean:A 和 B,A 依赖 B,B 依赖 A。
- 当创建 Bean A 时,Spring 首先会检查一级缓存
singletonObjects
,发现没有 A 的实例。然后开始创建 A,在 A 的实例化后,还没有进行属性注入和初始化方法调用时,会将 A 的创建工厂对象放入三级缓存singletonFactories
。 - 接着,在进行 A 的属性注入时,发现 A 依赖 B,于是开始创建 B。同样,B 在实例化后,将其创建工厂对象放入三级缓存。在 B 的属性注入时,发现 B 依赖 A,此时会先检查一级缓存,没有 A;再检查二级缓存,也没有 A;然后检查三级缓存,发现有 A 的创建工厂对象,通过这个工厂对象可以获取早期暴露的 A 的实例,将这个实例注入到 B 中。
- B 完成属性注入和初始化后,将其放入一级缓存
singletonObjects
,然后将早期暴露的 B 的实例注入到 A 中。A 完成属性注入和初始化后,也放入一级缓存。这样就解决了 A 和 B 之间的循环依赖问题。
- 注意事项
-
- Spring 的循环依赖解决机制主要是针对单例 Bean 的。对于原型 Bean,Spring 默认是不解决循环依赖的,因为原型 Bean 每次获取都是一个新的实例,无法像单例 Bean 那样通过提前暴露来解决循环依赖。而且,循环依赖可能会导致一些难以发现的问题,如 Bean 的初始化顺序不符合预期等,所以在设计 Bean 之间的依赖关系时,应尽量避免循环依赖。
3.20. 介绍一下Spring bean 的生命周期、注入方式和作用域
注入方式:通过setter方法注入,或者是通过构造方法注入
Bean的作用域:总共四种作用域,分别为Singleton、Request、Session、Prototype(原型的)
4. MySQL
4.1. select语句的完整执行顺序
SQL Select 语句完整的执行顺序:
(1)from 子句组装来自不同数据源的数据;
(2)where 子句基于指定的条件对记录行进行筛选;
(3)group by 子句将数据划分为多个分组;
(4)使用聚集函数进行计算;
(5)使用 having 子句筛选分组;
(6)计算所有的表达式;
(7)select 的字段;
(8)使用order by 对结果集进行排序。
4.2. MySQL事务
事务的基本要素(ACID)
- 原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位
- 一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能A扣了钱,B却没收到。
- 隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。
- 持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。
事务的并发问题
- 脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
- 不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致
- 幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
如何解决脏读、幻读、不可重复读
- 脏读: 隔离级别为 读提交、可重复读、串行化可以解决脏读
- 不可重复读:隔离级别为可重复读、串行化可以解决不可重复读
- 幻读:隔离级别为串行化可以解决幻读、通过MVCC + 区间锁可以解决幻读
小结:
不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表
4.3. MyISAM和InnoDB的区别
- 事务支持上
-
- MyISAM:不支持事务,如果在操作过程中发生错误,MyISAM不能回滚到之前的操作
- InnoDB:支持事务,提供了事务的四大特性,原子性、一致性、隔离性和持久性,能够保持数据的完整性
- 表级锁和行级锁
-
- MyISAM:使用表级锁,这意味着如果在执行插入、更新或者删除操作时,整个表都会被锁住,导致并发性能较差
- InnoDB:使用行级锁,在大多数情况下,InnoDB锁定的只是相关的行,而不是整个表,更加适用于高并发的场景,性能更好
- 外键支持
-
- MyISAM:不支持外键约束
- InnoDB:支持外键约束,能够维护表与表之间的关系,确保数据的一致性和完整性
- 崩溃修复能力
-
- MyISAM:崩溃后恢复能力交叉,需要手动进行修复,可能导致数据丢失
- InnoDB:具备自动崩溃恢复功能,依赖于事务日志和回滚日志,能保证即使出现了崩溃异常的错误后,也能确保数据一致
- 全文索引
-
- MyISAM:原生支持全文索引
- InnoDB:MySQL版本以后才开始支持全局索引
- 表的存储结构
-
- MyISAM:将数据文件和索引文件分开存储。表的数据存储在‘.MYD’文件中,索引存储在‘.MYI’文件中
- InnoDB:数据和索引放在同一个表空间中,在磁盘上有更好的存储管理
- 表的行数
-
- MyISAM:使用静态计数来存储表的总行数,因此在进行‘select count(*)’查询效率很高,尤其适用与频繁的大表
- InnoDB:不保存表的行数,需要遍历统计,‘select count(*)’查询会相对较慢
- 数据存储大小
-
- MyISAM:因为不支持事务和行级锁,MyISAM的表结构相对简单,通常占用空间较小
- InnoDB:支持更多功能(如事务、行级锁),因此占用空间相对较大
- 使用场景
-
- MyISAM:适用于读多写少的场景,或者对事务和数据一致性要求不高的应用,如博客、内容管理系统
- InnoDB:适用于是事务处理,数据一致性高、并发量大的应用,如金融系统、在线交易平台等
总结
- MyISAM适用于读密集型、对事务不敏感、数据量大但不频繁修改的场景。
- InnoDB则更适合对事务和数据一致性要求高的场景,尤其是在多用户高并发操作时表现更好。
4.4. 悲观锁和乐观锁的区别
悲观锁和乐观锁是两种常见的并发控制机制,分别用于应对多线程或多进程环境下的数据竞争问题。
4.4.1. 悲观锁(Pessimistic Lock)
悲观锁假设每次数据操作都会发生冲突,因此在对数据进行修改前,会先锁住数据以防止其他线程修改。悲观锁的核心思想是“假定最坏的情况”,通过加锁来确保只有一个线程可以修改数据,其他线程必须等待。
实现方式:
- 数据库中的悲观锁:使用数据库的行锁机制,常见于
SELECT ... FOR UPDATE
语句。例如:
-- 获取悲观锁
SELECT * FROM table WHERE id = 1 FOR UPDATE;
在查询时加上 FOR UPDATE
,会锁住对应的行,直到事务提交后才会释放锁。其他事务在锁释放之前无法读取或修改该行数据。
- Java中的悲观锁:可以使用
synchronized
或ReentrantLock
来实现。
public class PessimisticLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void doSomething() {
lock.lock(); // 获取锁
try {
// 操作共享资源
} finally {
lock.unlock(); // 释放锁
}
}
}
优点:
- 可以有效避免并发冲突。
缺点:
- 可能导致性能下降,尤其是在并发量大但实际冲突不频繁的情况下,线程可能长期处于等待状态。
4.4.2. 乐观锁 (Optimistic Lock)
乐观锁假设数据操作通常不会发生冲突,因此不会在每次操作前锁住数据。乐观锁的核心思想是“假设最好的情况”,在更新数据时通过版本号或条件检查来确保数据一致性。
实现方式:
- 版本号机制:每次读取数据时,都会带上一个版本号,更新时检查当前版本号是否和读取时一致。如果一致,则说明没有其他线程修改过,操作成功;否则,说明有并发操作,更新失败。
在数据库中可以使用类似以下方式:
-
- 首先查询时获取版本号:
SELECT version FROM table WHERE id = 1;
-
- 更新时根据版本号进行校验:
UPDATE table SET value = 'newValue', version = version + 1
WHERE id = 1 AND version = oldVersion;
只有当版本号匹配时,才会更新成功。否则,表示数据已经被其他线程修改过。
- Java中的乐观锁:可以使用
Atomic
类,比如AtomicInteger
、AtomicLong
等,或者手动维护一个版本号字段。
使用 CAS
(Compare-And-Swap) 操作实现,例如:
public class OptimisticLockExample {
private AtomicInteger version = new AtomicInteger(0);
public void update() {
int oldVersion = version.get();
// 尝试更新
if (version.compareAndSet(oldVersion, oldVersion + 1)) {
// 成功更新
} else {
// 更新失败,进行重试或抛出异常
}
}
}
优点:
- 不会产生锁竞争,性能更高,适用于读多写少的场景。
缺点:
- 如果并发冲突频繁,可能会导致大量的重试或失败操作,性能不如悲观锁。
4.5. 聚簇索引和非聚簇索引区别
两者都是B+树的数据结构
- 聚簇索引:将数据存储与索引放到了一块、并且是按照一定的顺序组织的,找到索引也就找到了数据,数据的物理存放顺序与索引顺序是一致的,即:只要索引是相邻的,那么对应的数据一定也是相邻地存放在磁盘上的
- 非聚簇索引叶子节点不存储数据、存储的是数据行地址,也就是说根据索引查找到数据行的位置再取磁盘查找数据,这个就有点类似一本书的目录,比如我们要找第三章第一节,那我们先在这个目录里面找,找到对应的页码后再去对应的页码看文章。
优势:
- 查询通过聚簇索引可以直接获取数据,相比非聚簇索引需要第二次查询(非覆盖索引的情况下)效率要高
- 聚簇索引对于范围查询的效率很高,因为其数据是按照大小排列的
- 聚簇索引适合用在排序的场合,非聚簇索引不适合
劣势:
- 维护索引很昂贵,特别是插入新行或者主键被更新导至要分页(pagesplit)的时候。建议在大量插入新行后,选在负载较低的时间段,通过OPTIMIZETABLE优化表,因为必须被移动的行数据可能造成碎片。使用独享表空间可以弱化碎片
- 表因为使用uuId(随机ID)作为主键,使数据存储稀疏,这就会出现聚簇索引有可能有比全表扫面更慢,所以建议使用int的auto_increment作为主键
- 如果主键比较大的话,那辅助索引将会变的更大,因为辅助索引的叶子存储的是主键值,过长的主键值,会导致非叶子节点占用占用更多的物理空间
4.6. 什么情况下会索引失效
失效条件:
- where 后面使用函数
- 使用or条件
- 模糊查询 %放在前边
- 类型转换
- 组合索引 (最佳左前缀匹配原则)
#查询条件用到了计算或者函数
explain SELECT * from test_slow_query where age = 20
explain SELECT * from test_slow_query where age +10 = 30
#模糊查询
EXPLAIN SELECT * from test_slow_query where NAME like '%吕布'
EXPLAIN SELECT * from test_slow_query where NAME like '%吕布%'
EXPLAIN SELECT * from test_slow_query where NAME like '吕布&'
#用到了or条件
EXPLAIN SELECT * from test_slow_query where NAME = '吕布' or name = "aaa"
#类型不匹配查询
explain SELECT * from test_slow_query where NAME = 11
explain SELECT * from test_slow_query where NAME = '11'
4.7. B+tree和B-tree
B-tree(B树)
- 定义:B树是一种自平衡的多路搜索树,确保数据始终保持排序,并且在插入、删除操作后依然平衡。每个节点可以包含多个子节点和关键字
- 特点:
-
- 节点中的关键字:每个节点可以包含多个关键字,节点之间按照关键字有序排列
- 子树:每个内部节点都有k+1个子树(k是指关键字的个数),这些子树的关键字范围按照父节点的关键字划分
- 搜索路径长度较短:通过增加每个节点中的关键字的数量,减少树的深度,减少访问磁盘的次数
- 所有节点包含关键字:B树的每个节点(包括非叶子节点)都包含数据
B+树
- 定义:与B树相比,B+树的非叶子节点不会存储实际的数据,只用于索引,所有数据都存储在叶子节点上,且叶子节点通过指针相互连接
- 特点:
-
- 非叶子节点只存储索引:B+树的非叶子节点只负责索引,不存储具体的数据信息
- 叶子节点包含所有数据:所有的数据都存储在叶子节点上,叶子节点之间通过指针连接,形成了链表结构,可以方便地进行范围查询
- 更适合范围查询:由于叶子节点之间通过指针连接,B+树可以非常高效地进行范围查询
两者的区别:
数据存储位置、范围查询、树的高度、磁盘读写率
4.8. SQL语句分析(explain)
type:连接类型
key: MYSQL使用的索引
rows:显示MYSQL执行查询的行数,简单且重要,数值越大越不好,说明没有用好索引
extra:该列包含MySQL解决查询的详细信息。
4.9. MySQL优化
(1)尽量选择较小的列
(2)将where中用的比较频繁的字段建立索引
(3)select子句中避免使用‘*’
(4)避免在索引列上使用计算、not in 和<>等操作
(5)当只需要一行数据的时候使用limit 1
(6)保证单表数据不超过200W,适时分割表。针对查询较慢的语句,可以使用explain 来分析该语句具体的执行情况。
(7)避免改变索引列的类型。
(8)选择最有效的表名顺序,from字句中写在最后的表是基础表,将被最先处理,在from子句中包含多个表的情况下,你必须选择记录条数最少的表作为基础表。
(9)避免在索引列上面进行计算。
(10)尽量缩小子查询的结果
4.10. sql优化案例
例1:where 子句中可以对字段进行 null 值判断吗?
可以,比如 select id from t where num is null 这样的 sql 也是可以的。但是最好不要给数据库留NULL,尽可能的使用 NOT NULL 填充数据库。不要以为 NULL 不需要空间,比如:char(100) 型,在字段建立时,空间就固定了, 不管是否插入值(NULL 也包含在内),都是占用 100 个字符的空间的,如果是 varchar 这样的变长字段,null 不占用空间。可以在 num 上设置默认值 0,确保表中 num 列没有 null 值,然后这样查询:select id from t where num= 0。
例2:如何优化?下面的语句?
select * from admin left join log on admin.admin_id = log.admin_id where log.admin_id>10
优化为:select * from (select * from admin where admin_id>10) T1 lef join log on T1.admin_id = log.admin_id。
使用 JOIN 时候,应该用小的结果驱动大的结果(left join 左边表结果尽量小如果有条件应该放到左边先处理, right join 同理反向),同时尽量把牵涉到多表联合的查询拆分多个 query(多个连表查询效率低,容易到之后锁表和阻塞)。
例3:limit 的基数比较大时使用 between
例如:select * from admin order by admin_id limit 100000,10
优化为:select * from admin where admin_id between 100000 and 100010 order by admin_id。
例4:尽量避免在列上做运算,这样导致索引失效
例如:select * from admin where year(admin_time)>2014
优化为: select * from admin where admin_time> '2014-01-01′