Java面试常见问题总结
Java基础
Java 中的⼏种基本数据类型是什么?对应的包装类型是什么?各⾃占⽤多少字节呢?
Java 中有 8 种基本数据类型,分别为:
- 6 种数字类型:
- 4 种整数型:
byte
、short
、int
、long
- 2 种浮点型:
float
、double
- 4 种整数型:
- 1 种字符类型:
char
- 1 种布尔型:
boolean
。
这 8 种基本数据类型的默认值以及所占空间的大小如下:
基本类型 | 位数 | 字节 | 默认值 | 取值范围 |
---|---|---|---|---|
byte | 8 | 1 | 0 | -128 ~ 127 |
short | 16 | 2 | 0 | -32768(-2^15) ~ 32767(2^15 - 1) |
int | 32 | 4 | 0 | -2147483648 ~ 2147483647 |
long | 64 | 8 | 0L | -9223372036854775808(-2^63) ~ 9223372036854775807(2^63 -1) |
char | 16 | 2 | ‘u0000’ | 0 ~ 65535(2^16 - 1) |
float | 32 | 4 | 0f | 1.4E-45 ~ 3.4028235E38 |
double | 64 | 8 | 0d | 4.9E-324 ~ 1.7976931348623157E308 |
boolean | 1 | false | true、false |
String 、 StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的?
可变性
String
是不可变的(后面会详细分析原因)。
StringBuilder
与 StringBuffer
都继承自 AbstractStringBuilder
类,在 AbstractStringBuilder
中也是使用字符数组保存字符串,不过没有使用 final
和 private
关键字修饰,最关键的是这个 AbstractStringBuilder
类还提供了很多修改字符串的方法比如 append
方法。
线程安全性
String
中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder
是 StringBuilder
与 StringBuffer
的公共父类,定义了一些字符串的基本操作,如 expandCapacity
、append
、insert
、indexOf
等公共方法。StringBuffer
对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder
并没有对方法进行加同步锁,所以是非线程安全的。
性能
每次对 String
类型进行改变的时候,都会生成一个新的 String
对象,然后将指针指向新的 String
对象。StringBuffer
每次都会对 StringBuffer
对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder
相比使用 StringBuffer
仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
对于三者使用的总结:
- 操作少量的数据: 适用
String
- 单线程操作字符串缓冲区下操作大量数据: 适用
StringBuilder
- 多线程操作字符串缓冲区下操作大量数据: 适用
StringBuffer
String
真正不可变有下面几点原因:
- 保存字符串的数组被
final
修饰且为私有的,并且String
类没有提供/暴露修改这个字符串的方法。 String
类被final
修饰导致其不能被继承,进而避免了子类破坏String
不可变。
String s1 = new String(“abc”); 这段代码创建了⼏个字符串对象?
先说答案:会创建 1 或 2 个字符串对象。
- 字符串常量池中不存在 “abc”:会创建 2 个 字符串对象。一个在字符串常量池中,由
ldc
指令触发创建。一个在堆中,由new String()
创建,并使用常量池中的 “abc” 进行初始化。 - 字符串常量池中已存在 “abc”:会创建 1 个 字符串对象。该对象在堆中,由
new String()
创建,并使用常量池中的 “abc” 进行初始化。
下面开始详细分析。
1、如果字符串常量池中不存在字符串对象 “abc”,那么它首先会在字符串常量池中创建字符串对象 “abc”,然后在堆内存中再创建其中一个字符串对象 “abc”。
== 与 equals?hashCode 与 equals ?
==
对于基本类型和引用类型的作用效果是不同的:
- 对于基本数据类型来说,
==
比较的是值。 - 对于引用数据类型来说,
==
比较的是对象的内存地址。
equals()
不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals()
方法存在于Object
类中,而Object
类是所有类的直接或间接父类,因此所有的类都有equals()
方法。
equals()
方法存在两种使用情况:
- 类没有重写
equals()
方法:通过equals()
比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是Object
类equals()
方法。 - 类重写了
equals()
方法:一般我们都重写equals()
方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。
总结下来就是:
- 如果两个对象的
hashCode
值相等,那这两个对象不一定相等(哈希碰撞)。 - 如果两个对象的
hashCode
值相等并且equals()
方法也返回true
,我们才认为这两个对象相等。 - 如果两个对象的
hashCode
值不相等,我们就可以直接认为这两个对象不相等。
相信大家看了我前面对 hashCode()
和 equals()
的介绍之后,下面这个问题已经难不倒你们了。
总结:
equals
方法判断两个对象是相等的,那这两个对象的hashCode
值也要相等。- 两个对象有相同的
hashCode
值,他们也不一定是相等的(哈希碰撞)。
包装类型的缓存机制了解么?
Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。
Byte
,Short
,Integer
,Long
这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character
创建了数值在 [0,127] 范围的缓存数据,Boolean
直接返回 True
or False
。
如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡。
⾃动装箱与拆箱了解吗?原理是什么?
- 装箱:将基本类型用它们对应的引用类型包装起来;
- 拆箱:将包装类型转换为基本数据类型;
从字节码中,我们发现装箱其实就是调用了 包装类的valueOf()
方法,拆箱其实就是调用了 xxxValue()
方法。
因此,
Integer i = 10
等价于Integer i = Integer.valueOf(10)
int n = i
等价于int n = i.intValue()
;
注意:如果频繁拆装箱的话,也会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作。
深拷⻉和浅拷⻉区别了解吗?什么是引⽤拷⻉?
浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
深拷贝:深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
那什么是引用拷贝呢? 简单来说,引用拷贝就是两个不同的引用指向同一个对象。
谈谈对 Java 注解的理解,解决了什么问题?
(Annotations)是一种元数据(metadata)机制,它们提供了一种在 Java 代码中添加元数据的方式。注解可以应用于类、方法、字段和其他程序元素,以提供关于这些元素的额外信息。
Java 注解解决了代码中一些重要问题
注解是Java编程中强大且灵活的工具,它们可以提供额外的元数据,用于代码文档化、配置、代码生成和增强等方面。通过使用注解,可以改善代码的可读性、可维护性和扩展性。
Exception 和 Error 有什么区别?
在 Java 中,所有的异常都有一个共同的祖先 java.lang
包中的 Throwable
类。Throwable
类有两个重要的子类:
Exception
:程序本身可以处理的异常,可以通过catch
来进行捕获。Exception
又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。Error
:Error
属于程序无法处理的错误 ,我们没办法通过catch
来进行捕获不建议通过catch
捕获 。例如 Java 虚拟机运行错误(Virtual MachineError
)、虚拟机内存不够错误(OutOfMemoryError
)、类定义错误(NoClassDefFoundError
)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
Java 反射?反射有什么缺点?你是怎么理解反射的(为什么框架需要反射)?
Java反射是指在运行时检查、访问和修改类、方法、字段等程序元素的能力。它提供了一组API,允许程序在运行时获取类的信息、调用方法、访问和修改字段,以及创建类的实例。
- 获取类的信息
- 创建对象实例
- 调用方法
- 访问和修改字符
- 注解处理
- 动态加载类
尽管反射提供了很多优点,但它也有一些缺点:
性能开销:由于反射要在运行时进行动态的类信息检查和调用,它通常比直接调用方法或访问字段的方式要慢。反射操作需要进行额外的检查和处理,因此可能会导致性能下降。在对性能要求较高的场景下,过度使用反射可能会成为一个瓶颈。
安全性问题:反射可以绕过访问控制检查,使得程序可以访问和修改本来不应该访问和修改的类、方法、字段等。这可能导致潜在的安全漏洞,因此在使用反射时需要格外小心,并确保对敏感操作进行适当的权限验证。
对于为什么框架需要反射,反射提供了框架在运行时动态加载和操作类的能力,从而实现更灵活和可扩展的框架结构。框架可以通过反射来实现配置文件的解析、动态创建对象、调用对象的方法等功能。反射使得框架可以在不了解具体类实现的情况下进行操作,从而实现了框架的解耦和扩展性。例如,依赖注入框架(如Spring)可以使用反射来动态创建和注入对象,使得应用程序可以在不修改代码的情况下配置和更改对象的依赖关系。通过反射,框架可以在运行时根据配置和需求来加载和使用类,提供更大的灵活性和可定制性。
Java 泛型了解么?什么是类型擦除?介绍⼀下常⽤的通配符?
内部类了解吗?匿名内部类了解吗?
如果A类需要直接访问B类中的成员,而B类又需要建立A类的对象。这时,为了方便设计和访问,直接将A类定义在B类中。就可以了。A类就称为内部类。内部类可以直接访问外部类中的成员。而外部类想要访问内部类,必须要建立内部类的对象。
没有名字的内部类。就是内部类的简化形式。一般只用一次就可以用这种形式。匿名内部类其实就是一个匿名子类对象。想要定义匿名内部类:需要前提,内部类必须继承一个类或者实现接口。
BIO,NIO,AIO 有什么区别?
BIO 属于同步阻塞 IO 模型 。
同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。
在客户端连接数量不高的情况下,是没问题的。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
NIO 可以看作是 I/O 多路复用模型。也有很多人认为,Java 中的 NIO 属于同步非阻塞 IO 模型。
它是支持面向缓冲的,基于通道的 I/O 操作方法。 对于高负载、高并发的(网络)应用,应使用 NIO 。
AIO异步
简单总结一下 Java 中的 BIO、NIO、AIO。
Java集合框架
说说 List,Set,Map 三者的区别?三者底层的数据结构?
List
(对付顺序的好帮手): 存储的元素是有序的、可重复的。Set
(注重独一无二的性质): 存储的元素不可重复的。Map
(用 key 来搜索的专家): 使用键值对(key-value)存储,类似于数学上的函数 y=f(x),“x” 代表 key,“y” 代表 value,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。
List
ArrayList
的底层是一个动态数组。LinkedList
的底层是一个双向链表。
Set
-
HashSet
(无序,唯一): 基于
HashMap实现的,底层采用
HashMap` 来保存元素。 -
LinkedHashSet
:LinkedHashSet
是HashSet
的子类,并且其内部是通过LinkedHashMap
来实现的。 -
TreeSet
(有序,唯一): 红黑树(自平衡的排序二叉树)。
Map
-
HashMap
:JDK1.8 之前HashMap
由数组+链表组成的,数组是HashMap
的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。 -
LinkedHashMap
:LinkedHashMap
继承自HashMap
,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap
在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。 -
Hashtable
:数组+链表组成的,数组是Hashtable
的主体,链表则是主要为了解决哈希冲突而存在的。 -
TreeMap
:红黑树(自平衡的排序二叉树)。
有哪些集合是线程不安全的?怎么解决呢?
比如HashMap
JDK1.7 及之前版本,在多线程环境下,HashMap
扩容时会造成死循环和数据丢失的问题。
数据丢失这个在 JDK1.7 和 JDK 1.8 中都存在,这里以 JDK 1.8 为例进行介绍。
JDK 1.8 后,在 HashMap
中,多个键值对可能会被分配到同一个桶(bucket),并以链表或红黑树的形式存储。多个线程对 HashMap
的 put
操作会导致线程不安全,具体来说会有数据覆盖的风险。
举个例子:
- 两个线程 1,2 同时进行 put 操作,并且发生了哈希冲突(hash 函数计算出的插入下标是相同的)。
- 不同的线程可能在不同的时间片获得 CPU 执行的机会,当前线程 1 执行完哈希冲突判断后,由于时间片耗尽挂起。线程 2 先完成了插入操作。
- 随后,线程 1 获得时间片,由于之前已经进行过 hash 碰撞的判断,所有此时会直接进行插入,这就导致线程 2 插入的数据被线程 1 覆盖了。
⽐较 HashSet、LinkedHashSet 和 TreeSet 三者的异同
HashSet
、LinkedHashSet
和TreeSet
都是Set
接口的实现类,都能保证元素唯一,并且都不是线程安全的。HashSet
、LinkedHashSet
和TreeSet
的主要区别在于底层数据结构不同。HashSet
的底层数据结构是哈希表(基于HashMap
实现)。LinkedHashSet
的底层数据结构是链表和哈希表,元素的插入和取出顺序满足 FIFO。TreeSet
底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。- 底层数据结构不同又导致这三者的应用场景不同。
HashSet
用于不需要保证元素插入和取出顺序的场景,LinkedHashSet
用于保证元素的插入和取出顺序满足 FIFO 的场景,TreeSet
用于支持对元素自定义排序规则的场景。
HashMap 和 Hashtable 的区别?
线程是否安全: HashMap
是非线程安全的,Hashtable
是线程安全的,因为 Hashtable
内部的方法基本都经过synchronized
修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap
吧!);
效率: 因为线程安全的问题,HashMap
要比 Hashtable
效率高一点。另外,Hashtable
基本被淘汰,不要在代码中使用它;
对 Null key 和 Null value 的支持: HashMap
可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否则会抛出 NullPointerException
。
初始容量大小和每次扩充容量大小的不同: ① 创建时如果不指定容量初始值,Hashtable
默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap
默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable
会直接使用你给定的大小,而 HashMap
会将其扩充为 2 的幂次方大小(HashMap
中的tableSizeFor()
方法保证,下面给出了源代码)。也就是说 HashMap
总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。
底层数据结构: JDK1.8 以后的 HashMap
在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间(后文中我会结合源码对这一过程进行分析)。Hashtable
没有这样的机制。
哈希函数的实现:HashMap
对哈希值进行了高位和低位的混合扰动处理以减少冲突,而 Hashtable
直接使用键的 hashCode()
值。
HashMap 和 HashSet 区别?
如果你看过 HashSet
源码的话就应该知道:HashSet
底层就是基于 HashMap
实现的。(HashSet
的源码非常非常少,因为除了 clone()
、writeObject()
、readObject()
是 HashSet
自己不得不实现之外,其他方法都是直接调用 HashMap
中的方法。
HashMap | HashSet |
---|---|
实现了 Map 接口 | 实现 Set 接口 |
存储键值对 | 仅存储对象 |
调用 put() 向 map 中添加元素 | 调用 add() 方法向 Set 中添加元素 |
HashMap 使用键(Key)计算 hashcode | HashSet 使用成员对象来计算 hashcode 值,对于两个对象来说 hashcode 可能相同,所以equals() 方法用来判断对象的相等性 |
HashMap 和 TreeMap 区别?
TreeMap
和HashMap
都继承自AbstractMap
,但是需要注意的是TreeMap
它还实现了NavigableMap
接口和SortedMap
接口。
相比于HashMap
来说, TreeMap
主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力。
HashMap 的底层实现
JDK1.8 之前 HashMap
底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashcode
经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash
判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
HashMap
中的扰动函数(hash
方法)是用来优化哈希值的分布。通过对原始的 hashCode()
进行额外处理,扰动函数可以减小由于糟糕的 hashCode()
实现导致的碰撞,从而提高数据的分布均匀性。
相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。
所谓 “拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
HashMap 的⻓度为什么是 2 的幂次⽅
简单总结一下 HashMap
的长度是 2 的幂次方的原因:
- 位运算效率更高:位运算(&)比取余运算(%)更高效。当长度为 2 的幂次方时,
hash % length
等价于hash & (length - 1)
。 - 可以更好地保证哈希值的均匀分布:扩容之后,在旧数组元素 hash 值比较均匀的情况下,新数组元素也会被分配的比较均匀,最好的情况是会有一半在新数组的前半部分,一半在新数组后半部分。
- 扩容机制变得简单和高效:扩容后只需检查哈希值高位的变化来决定元素的新位置,要么位置不变(高位为 0),要么就是移动到新位置(高位为 1,原索引位置+原容量)。
ConcurrentHashMap 和 Hashtable 的区别?
ConcurrentHashMap
和 Hashtable
的区别主要体现在实现线程安全的方式上不同。
-
底层数据结构: JDK1.7 的
ConcurrentHashMap
底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8
的结构一样,数组+链表/红黑二叉树。Hashtable
和 JDK1.8 之前的HashMap
的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的; -
实现线程安全的方式(重要):
- 在 JDK1.7 的时候,
ConcurrentHashMap
对整个桶数组进行了分割分段(Segment
,分段锁),每一把锁只锁容器其中一部分数据(下面有示意图),多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 - 到了 JDK1.8 的时候,
ConcurrentHashMap
已经摒弃了Segment
的概念,而是直接用Node
数组+链表+红黑树的数据结构来实现,并发控制使用synchronized
和 CAS 来操作。(JDK1.6 以后synchronized
锁做了很多优化) 整个看起来就像是优化过且线程安全的HashMap
,虽然在 JDK1.8 中还能看到Segment
的数据结构,但是已经简化了属性,只是为了兼容旧版本; Hashtable
(同一把锁) :使用synchronized
来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
Hashtable 的内部结构
Java8 ConcurrentHashMap 存储结构
- 在 JDK1.7 的时候,
ConcurrentHashMap 线程安全的具体实现⽅式/底层具体实现
JDK1.8之前
ConcurrentHashMap
是由 Segment
数组结构和 HashEntry
数组结构组成。
JDK1.8之后
数据结构跟 HashMap
1.8 的结构类似,数组+链表/红黑二叉树。
Java 8 中,锁粒度更细,synchronized
只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升。
MySQL
MySQL存储引擎
MySQL ⽀持哪些存储引擎?默认使⽤哪个?
MySQL 支持多种数据库,比如:InnoDB、MyISAM、Memory等,可以通过 SHOW ENGINES
命令来查看 MySQL 支持的所有存储引擎。
MySQL 当前默认的存储引擎是 InnoDB。并且,所有的存储引擎中只有 InnoDB 是事务性存储引擎,也就是说只有 InnoDB 支持事务。
MySQL 5.5.5 之前,MyISAM 是 MySQL 的默认存储引擎。5.5.5 版本之后,InnoDB 是 MySQL 的默认存储引擎。
MyISAM 和 InnoDB 有什么区别?
MyISAM | InnoDB | |
---|---|---|
是否支持行级锁 | 否,只有表级锁的颗粒度 | 是,支持行级锁和表级锁(默认行级锁) |
是否支持事务 | 不支持 | 支持,实现了 SQL 标准定义了四个隔离级别 |
是否支持外键 | 不支持 | 支持 |
是否支持MVCC | 不支持 | 支持 |
索引实现 | 两者虽然都使用B+树,但是两者得实现方式不太一样 | |
数据异常崩溃后得安全恢复 | 不支持 | 支持 |
性能 | InnoDB 的性能比 MyISAM 更强大 |
MySQL事务
事务的四⼤特性了解么?
原子性(Atomicity
):事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
一致性(Consistency
):执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的;
隔离性(Isolation
):并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
持久性(Durability
):一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
并发事务带来了哪些问题?不可重复读和幻读有什么区别?
多个事务并发运行,经常会操作相同的数据来完成各自的任务
脏读(Dirty read)
一个事务读取数据并且对数据进行了修改,这个修改对其他事务来说是可见的,即使当前事务没有提交。这时另外一个事务读取了这个还未提交的数据,但第一个事务突然回滚,导致数据并没有被提交到数据库,那第二个事务读取到的就是脏数据,这也就是脏读的由来。
例如:事务 1 读取某表中的数据 A=20,事务 1 修改 A=A-1,事务 2 读取到 A = 19,事务 1 回滚导致对 A 的修改并未提交到数据库, A 的值还是 20。
丢失修改(Lost to modify)
在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。
例如:事务 1 读取某表中的数据 A=20,事务 2 也读取 A=20,事务 1 先修改 A=A-1,事务 2 后来也修改 A=A-1,最终结果 A=19,事务 1 的修改被丢失。
不可重复读(Unrepeatable read)
指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
例如:事务 1 读取某表中的数据 A=20,事务 2 也读取 A=20,事务 1 修改 A=A-1,事务 2 再次读取 A =19,此时读取的结果和第一次读取的结果不同。
幻读(Phantom read)
幻读与不可重复读类似。它发生在一个事务读取了几行数据,接着另一个并发事务插入了一些数据时。在随后的查询中,第一个事务就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
例如:事务 2 读取某个范围的数据,事务 1 在这个范围插入了新的数据,事务 2 再次读取这个范围的数据发现相比于第一次读取的结果多了新的数据。
不可重复读和幻读的区别
- 不可重复读的重点是内容修改或者记录减少比如多次读取一条记录发现其中某些记录的值被修改;
- 幻读的重点在于记录新增比如多次执行同一条查询语句(DQL)时,发现查到的记录增加了。
幻读其实可以看作是不可重复读的一种特殊情况,单独把幻读区分出来的原因主要是解决幻读和不可重复读的方案不一样。
MySQL 事务隔离级别?默认是什么级别?
SQL 标准定义了四个隔离级别:
- READ-UNCOMMITTED(读取未提交) :最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
- READ-COMMITTED(读取已提交) :允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
- REPEATABLE-READ(可重复读) :对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
- SERIALIZABLE(可串行化) :最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ-UNCOMMITTED | √ | √ | √ |
READ-COMMITTED | × | √ | √ |
REPEATABLE-READ | × | × | √ |
SERIALIZABLE | × | × | × |
MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)。
MySQL 的隔离级别是基于锁实现的吗?
MySQL 的隔离级别基于锁和 MVCC 机制共同实现的。
SERIALIZABLE 隔离级别是通过锁来实现的,READ-COMMITTED 和 REPEATABLE-READ 隔离级别是基于 MVCC 实现的。不过, SERIALIZABLE 之外的其他隔离级别可能也需要用到锁机制,就比如 REPEATABLE-READ 在当前读情况下需要使用加锁读来保证不会出现幻读。
InnoDB 对 MVCC 的具体实现
MySQL字段类型
char 和 varchar 的区别是什么?
CHAR 和 VARCHAR 是最常用到的字符串类型,两者的主要区别在于:CHAR 是定长字符串,VARCHAR 是变长字符串。
CHAR 在存储时会在右边填充空格以达到指定的长度,检索时会去掉空格;VARCHAR 在存储时需要使用 1 或 2 个额外字节记录字符串的长度,检索时不需要处理。
CHAR 更适合存储长度较短或者长度都差不多的字符串,例如 Bcrypt 算法、MD5 算法加密后的密码、身份证号码。VARCHAR 类型适合存储长度不确定或者差异较大的字符串,例如用户昵称、文章标题等。
CHAR(M) 和 VARCHAR(M) 的 M 都代表能够保存的字符数的最大值,无论是字母、数字还是中文,每个都只占用一个字符。
varchar(100)和 varchar(10)的区别是什么?
VARCHAR(100)和 VARCHAR(10)都是变长类型,表示能存储最多 100 个字符和 10 个字符。因此,VARCHAR (100) 可以满足更大范围的字符存储需求,有更好的业务拓展性。而 VARCHAR(10)存储超过 10 个字符时,就需要修改表结构才可以。
虽说 VARCHAR(100)和 VARCHAR(10)能存储的字符范围不同,但二者存储相同的字符串,所占用磁盘的存储空间其实是一样的,这也是很多人容易误解的一点。
不过,VARCHAR(100) 会消耗更多的内存。这是因为 VARCHAR 类型在内存中操作时,通常会分配固定大小的内存块来保存值,即使用字符类型中定义的长度。例如在进行排序的时候,VARCHAR(100)是按照 100 这个长度来进行的,也就会消耗更多内存。
decimal 和 float/double 的区别是什么?存储⾦钱应该⽤哪⼀种?
DECIMAL 和 FLOAT 的区别是:DECIMAL 是定点数,FLOAT/DOUBLE 是浮点数。DECIMAL 可以存储精确的小数值,FLOAT/DOUBLE 只能存储近似的小数值。
DECIMAL 用于存储具有精度要求的小数,例如与货币相关的数据,可以避免浮点数带来的精度损失。
为什么不推荐使⽤ text 和 blob?
在日常开发中,很少使用 TEXT 类型,但偶尔会用到,而 BLOB 类型则基本不常用。如果预期长度范围可以通过 VARCHAR 来满足,建议避免使用 TEXT。
数据库规范通常不推荐使用 BLOB 和 TEXT 类型,这两种类型具有一些缺点和限制,例如:
- 不能有默认值。
- 在使用临时表时无法使用内存临时表,只能在磁盘上创建临时表(《高性能 MySQL》书中有提到)。
- 检索效率较低。
- 不能直接创建索引,需要指定前缀长度。
- 可能会消耗大量的网络和 IO 带宽。
- 可能导致表上的 DML 操作变慢。
MySQL索引
为什么索引能提⾼查询速度?
索引的作用就相当于书的目录。打个比方: 我们在查字典的时候,如果没有目录,那我们就只能一页一页的去找我们需要查的那个字,速度很慢。如果有目录了,我们只需要先去目录里查找字的位置,然后直接翻到那一页就行了。
索引底层数据结构存在很多种类型,常见的索引结构有: B 树, B+树 和 Hash、红黑树。在 MySQL 中,无论是 Innodb 还是 MyIsam,都使用了 B+树作为索引结构。
聚集索引和⾮聚集索引的区别?⾮聚集索引⼀定回表查询吗?
聚簇索引(Clustered Index)即索引结构和数据一起存放的索引,并不是一种单独的索引类型。InnoDB 中的主键索引就属于聚簇索引。
聚簇索引的优缺点
优点:
- 查询速度非常快:聚簇索引的查询速度非常的快,因为整个 B+树本身就是一颗多叉平衡树,叶子节点也都是有序的,定位到索引的节点,就相当于定位到了数据。相比于非聚簇索引, 聚簇索引少了一次读取数据的 IO 操作。
- 对排序查找和范围查找优化:聚簇索引对于主键的排序查找和范围查找速度非常快。
缺点:
- 依赖于有序的数据:因为 B+树是多路平衡树,如果索引的数据不是有序的,那么就需要在插入时排序,如果数据是整型还好,否则类似于字符串或 UUID 这种又长又难比较的数据,插入或查找的速度肯定比较慢。
- 更新代价大:如果对索引列的数据被修改时,那么对应的索引也将会被修改,而且聚簇索引的叶子节点还存放着数据,修改代价肯定是较大的,所以对于主键索引来说,主键一般都是不可被修改的。
非聚簇索引(Non-Clustered Index)即索引结构和数据分开存放的索引,并不是一种单独的索引类型。二级索引(辅助索引)就属于非聚簇索引。MySQL 的 MyISAM 引擎,不管主键还是非主键,使用的都是非聚簇索引。
非聚簇索引的叶子节点并不一定存放数据的指针,因为二级索引的叶子节点就存放的是主键,根据主键再回表查数据。
非聚簇索引的优缺点
优点:
更新代价比聚簇索引要小 。非聚簇索引的更新代价就没有聚簇索引那么大了,非聚簇索引的叶子节点是不存放数据的。
缺点:
- 依赖于有序的数据:跟聚簇索引一样,非聚簇索引也依赖于有序的数据
- 可能会二次查询(回表):这应该是非聚簇索引最大的缺点了。 当查到索引对应的指针或主键后,可能还需要根据指针或主键再到数据文件或表中查询。
非聚簇索引不一定回表查询。
试想一种情况,用户准备使用 SQL 查询用户名,而用户名字段正好建立了索引。
SELECT name FROM table WHERE name='guang19';
那么这个索引的 key 本身就是 name,查到对应的 name 直接返回就行了,无需回表查询。
如果 SQL 查的就是主键呢?
SELECT id FROM table WHERE id=1;
主键索引本身的 key 就是主键,查到返回就行了。这种情况就称之为覆盖索引了。
索引这么多优点,为什么不对表中的每⼀个列创建⼀个索引呢?(使⽤索引⼀定能提⾼查询性能吗?)
索引底层的数据结构了解么?Hash 索引和 B+树索引优劣分析
B+树做索引⽐红⿊树好在哪⾥?
最左前缀匹配原则了解么?
什么是覆盖索引
如何查看某条 SQL 语句是否⽤到了索引?
MySQL锁
表级锁和⾏级锁有什么区别?
哪些操作会加表级锁?哪些操作会加⾏级锁?请简单举例说⼀下。
InnoDB 有哪⼏类⾏锁?
Next-Key Lock 的加锁范围?
当前读和快照读有什么区别?
MySQL 如何使⽤乐观锁和悲观锁?
MySQL日志
MySQL 中常⻅的⽇志有哪些?
MySQL 中常见的日志类型主要有下面几类(针对的是 InnoDB 存储引擎):
错误日志(error log) :对 MySQL 的启动、运行、关闭过程进行了记录。
二进制日志(binary log,binlog) :主要记录的是更改数据库数据的 SQL 语句。
一般查询日志(general query log) :已建立连接的客户端发送给 MySQL 服务器的所有 SQL 记录,因为 SQL 的量比较大,默认是不开启的,也不建议开启。
慢查询日志(slow query log) :执行时间超过 long_query_time秒钟的查询,解决 SQL 慢查询问题的时候会用到。
事务日志(redo log 和 undo log) :redo log 是重做日志,undo log 是回滚日志。
中继日志(relay log) :relay log 是复制过程中产生的日志,很多方面都跟 binary log 差不多。不过,relay log 针对的是主从复制中的从库。
DDL 日志(metadata log) :DDL 语句执行的元数据操作。
二进制日志(binlog)和事务日志(redo log 和 undo log)比较重要,需要我们重点关注。
慢查询⽇志有什么⽤?
慢查询日志记录了执行时间超过 long_query_time(默认是 10s,通常设置为1s)的所有查询语句,在解决 SQL 慢查询(SQL 执行时间过长)问题的时候经常会用到。找到慢 SQL 是优化 SQL 语句性能的第一步,然后再用EXPLAIN 命令可以对慢 SQL 进行分析,获取执行计划的相关信息。你可以通过 show variables like “slow_query_log”;命令来查看慢查询日志是否开启,默认是关闭的。
binlog 主要记录了什么?
主要记录了对 MySQL 数据库执行了更改的所有操作(数据库执行的所有 DDL 和 DML 语句),包括表结构变更(CREATE、ALTER、DROP TABLE…)、表数据修改(INSERT、UPDATE、DELETE…),但不包括 SELECT、SHOW 这类不会对数据库造成更改的操作。不过,并不是不对数据库造成修改就不会被记录进 binlog。即使表结构变更和表数据修改操作并未对数据库造成更改,依然会被记录进 binlog。
redo log 如何保证事务的持久性?
MySQL InnoDB 引擎使用 redo log 来保证事务的持久性。redo log 主要做的事情就是记录页的修改,比如某个页面某个偏移量处修改了几个字节的值以及具体被修改的内容是什么。redo log 中的每一条记录包含了表空间号、数据页号、偏移量、具体修改的数据,甚至还可能会记录修改数据的长度(取决于 redo log 类型)。在事务提交时,我们会将 redo log 按照刷盘策略刷到磁盘上去,这样即使 MySQL 宕机了,重启之后也能恢复未能写入磁盘的数据,从而保证事务的持久性。也就是说,redo log 让 MySQL 具备了崩溃恢复能力。
⻚修改之后为什么不直接刷盘呢?
很多人可能要问了:为什么每次修改 Buffer Pool 中的页之后不直接刷盘呢?这样不就不需要 redo log 了嘛!这种方式必然是不行的,性能非常差。最大的问题就是 InnoDB 页的大小一般为 16KB,而页又是磁盘和内存交互的基本单位。这就导致即使我们只修改了页中的几个字节数据,一次刷盘操作也需要将 16KB 大小的页整个都刷新到磁盘中。而且,这些修改的页可能并不相邻,也就是说这还是随机 IO。采用 redo log 的方式就可以避免这种性能问题,因为 redo log 的刷盘性能很好。首先,redo log 的写入属于顺序 IO。 其次,一行 redo log 记录只占几十个字节。另外,Buffer Pool 中的页(脏页)在某些情况下(比如 redo log 快写满了)也会进行刷盘操作。不过,这里的刷盘操作会合并写入,更高效地顺序写入到磁盘。
binlog 和 redolog 有什么区别?
binlog 主要用于数据库还原,属于数据级别的数据恢复,主从复制是 binlog 最常见的一个应用场景。redolog 主要用于保证事务的持久性,属于事务级别的数据恢复。redolog 属于 InnoDB 引擎特有的,binlog 属于所有存储引擎共有的,因为 binlog 是 MySQL 的 Server 层实现的。redolog 属于物理日志,主要记录的是某个页的修改。binlog 属于逻辑日志,主要记录的是数据库执行的所有 DDL 和 DML 语句。binlog 通过追加的方式进行写入,大小没有限制。redo log 采用循环写的方式进行写入,大小固定,当写到结尾时,会回到开头循环写日志。
undo log 如何保证事务的原⼦性?
MySQL性能优化
能⽤ MySQL 直接存储⽂件(⽐如图⽚)吗?
MySQL 如何存储 IP 地址?
如何分析 SQL 的性能?
有哪些常⻅的 SQL 优化⼿段?
简单说⼀下⼤表优化的思路。
读写分离如何实现?
为什么要分库分表?有哪些常⻅的分库分表⼯具?
深度分⻚如何优化?
数据冷热分离如何做?
常⻅的数据库优化⽅法有哪些?
Redis
Redis基础
Redis 为什么这么快?
Redis 内部做了非常多的性能优化,比较重要的有下面 3 点:
- Redis 基于内存,内存的访问速度比磁盘快很多;
- Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型,主要是单线程事件循环和 IO 多路复用(Redis 线程模式后面会详细介绍到);
- Redis 内置了多种优化过后的数据类型/结构实现,性能非常高。
- Redis 通信协议实现简单且解析高效。
分布式缓存常⻅的技术选型⽅案有哪些?说⼀下 Redis 和 Memcached 的区别和共同点
现在公司一般都是用 Redis 来实现缓存,而且 Redis 自身也越来越强大了!不过,了解 Redis 和 Memcached 的区别和共同点,有助于我们在做相应的技术选型的时候,能够做到有理有据!
共同点:
- 都是基于内存的数据库,一般都用来当做缓存使用。
- 都有过期策略。
- 两者的性能都非常高。
区别:
- 数据类型:Redis 支持更丰富的数据类型(支持更复杂的应用场景)。Redis 不仅仅支持简单的 k/v 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。Memcached 只支持最简单的 k/v 数据类型。
- 数据持久化:Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memcached 把数据全部存在内存之中。也就是说,Redis 有灾难恢复机制而 Memcached 没有。
- 集群模式支持:Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 Redis 自 3.0 版本起是原生支持集群模式的。
- 线程模型:Memcached 是多线程,非阻塞 IO 复用的网络模型;Redis 使用单线程的多路 IO 复用模型。 (Redis 6.0 针对网络数据的读写引入了多线程)
- 特性支持:Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持。并且,Redis 支持更多的编程语言。
- 过期数据删除:Memcached 过期数据的删除策略只用了惰性删除,而 Redis 同时使用了惰性删除与定期删除。
相信看了上面的对比之后,我们已经没有什么理由可以选择使用 Memcached 来作为自己项目的分布式缓存了。
本地缓存和分布式缓存有什么区别?如何选择?
分布式缓存是一种将数据缓存到多个节点上的技术,这些节点通常分布在不同的物理位置。通过将数据分散到多个节点上,可以减轻单一节点的负载,并提高系统的可扩展性和可靠性。分布式缓存通常用于处理大规模数据和高并发访问场景,例如电商网站、在线游戏和社交网络等。
本地缓存是指在同一个进程内的内存空间中缓存数据。数据读写都在同一个进程内完成,不需要跨网络传输。本地缓存通常用于减少对数据库的访问次数,提高应用程序的性能和响应速度。
总结:选择合适的缓存方案需要根据实际需求进行权衡。对于需要处理大规模数据和高并发访问的场景,分布式缓存具有更好的扩展性和可靠性;对于小规模数据和低并发访问场景,本地缓存可以提供更快的访问速度和减轻网络负担的效果。
说⼀下有缓存情况下查询数据和修改数据的流程。
查询数据流程
- 接收请求:应用程序接收到查询请求,通常是用户对某些数据的访问请求。
- 检查缓存:
- 首先,应用程序会检查Redis缓存中是否存在请求的数据(通常是通过特定的键进行查询)。
- 如果缓存中存在(缓存命中),则直接从Redis中获取数据并返回给用户。
- 缓存未命中:
- 如果缓存中不存在所请求的数据(缓存未命中),应用程序会查询后端数据库(如MySQL、PostgreSQL等)。
- 获取到数据后,将查询结果存入Redis缓存,同时设置合适的过期时间。
- 返回结果:将从后端数据库获取到的数据返回给用户。
修改数据流程
- 接收修改请求:应用程序接收到数据修改请求(如更新、删除等)。
- 更新数据库:
- 首先,应用程序会对后端数据库进行相应的修改操作(如INSERT、UPDATE或DELETE)。
- 更新缓存:
- 数据库修改成功后,接下来要更新Redis缓存。通常有两种做法:
- 更新缓存:如果数据在Redis中存在,直接更新对应的缓存值。
- 删除缓存:如果数据在Redis中存在,可以选择先删除缓存的键,以确保下次查询时会从数据库重新加载数据到缓存中。
- 数据库修改成功后,接下来要更新Redis缓存。通常有两种做法:
- 可选的失效策略:在一些情况下,应用程序可能会设置相应的策略,例如在一定条件下使缓存失效,以确保数据的一致性。
- 返回结果:最后,应用程序会将操作结果(如成功或失败)返回给用户。
常⻅的缓存读写策略有哪些?
Cache Aside Pattern(旁路缓存模式)
Read/Write Through Pattern(读写穿透)
什么是 Redis Module?有什么⽤?项⽬使⽤过吗?
redis 从 4.0 版本开始,支持通过 Module 来扩展其功能以满足特殊的需求。这些 Module 以动态链接库(so 文件)的形式被加载到 Redis 中,这是一种非常灵活的动态扩展功能的实现方式,值得借鉴学习!
我们每个人都可以基于 Redis 去定制化开发自己的 Module,比如实现搜索引擎功能、自定义分布式锁和分布式限流。
目前,被 Redis 官方推荐的 Module 有:
- RediSearch:用于实现搜索引擎的模块。
- RedisJSON:用于处理 JSON 数据的模块。
- RedisGraph:用于实现图形数据库的模块。
- RedisTimeSeries:用于处理时间序列数据的模块。
- RedisBloom:用于实现布隆过滤器的模块。
- RedisAI:用于执行深度学习/机器学习模型并管理其数据的模块。
- RedisCell:用于实现分布式限流的模块。
Redis数据结构
Redis 有哪些数据结构?
Redis持久化机制(重要)
宕机了,Redis 如何避免数据丢失?
为了避免 Redis 数据丢失,可以采取以下几种策略:
- 持久化机制:
- RDB(快照):定期将数据集快照保存到磁盘。可以通过配置
SAVE
命令指定快照的频率。不过,这种方式可能会在崩溃时丢失最近的数据更新。 - AOF(追加文件):记录每个写操作,以日志的形式保存到磁盘。相较于 RDB,AOF 能够提供更高的数据持久性。可以配置不同的策略(如每秒、每次写入等)以平衡性能和数据安全。
- RDB(快照):定期将数据集快照保存到磁盘。可以通过配置
- 配置优化:
- 在
redis.conf
中合理配置持久化参数,例如appendfsync
的选项可以设置为everysec
,以在每秒进行一次追加文件同步,提供相对较好的性能和数据安全。 - 可以开启
save
配置来指定 RDB 快照的频率。
- 在
- 主从复制:
- 通过设置 Redis 主从复制(Replication),可以在多个实例之间备份数据。如果主节点宕机,从节点可以提升为主节点,减少数据丢失风险。
- 分布式部署:
- 使用 Redis Sentinel 或 Cluster 来确保高可用性与容错能力。Sentinel 可以监控 Redis 实例,自动进行故障转移。
- 备份数据:
- 定期进行完全备份,包括 RDB 文件或AOF 文件。可以采用不同的存储介质和位置,增加数据恢复的可能性。
- 使用 Redis Streams:
- 如果使用 Redis Streams,可以实现更高的数据保留和日志管理,以便在业务逻辑中回溯到特定时间点的数据状态。
如何选择 RDB 和 AOF?
在使用 Redis 时,选择 RDB(快照)和 AOF(追加文件)作为持久化机制主要取决于您的应用场景、数据一致性要求和性能需求。以下是一些选择的考量因素:
RDB(快照)
优点:
- 高效性:RDB 生成的数据快照是二进制格式,文件较小,重启时加载速度快。
- 性能影响小:由于 RDB 持久化是周期性的快照,性能影响相对较小。
- 简单:配置相对简单,适合大多数使用场景。
缺点:
- 数据丢失:在 RDB 的快照间隔内,如果发生故障,期间的数据可能会丢失。
- 恢复时间长:恢复数据时,需要加载整个快照文件,时间可能较长。
AOF(追加文件)
优点:
- 数据完整性:AOF 记录每一条写操作,可以最大限度地减少数据丢失(可以设置为每个操作、每秒或者不定期写入)。
- 可读性:AOF 文件是以人类可读的形式保存的,便于调试。
缺点:
- 性能影响:AOF 默认每个写操作都追加到文件中,可能会导致性能下降,但可以通过不同的策略来缓解(如每秒写入一次)。
- 文件膨胀:AOF 文件可能会随着时间不断增大,需要定期重写。
综合考虑
- 数据一致性高的场景:如果您需要确保在系统崩溃时尽量减少数据丢失,可以选择 AOF。
- 性能优先的场景:如果您的应用场景对性能要求极高,并且可以容忍短暂的数据丢失,可以选择 RDB。
- 混合模式:Redis 也支持同时使用 RDB 和 AOF。您可以利用 RDB 快速恢复启动性能,同时用 AOF 保证更高的数据完整性。
结论
最终的选择应该根据您的具体需求和对性能、数据安全性的权衡来决定。考虑使用 Redis 的场景、您的数据处理能力以及可能的故障恢复策略,做出合理的选择。
Redis线程模型(重要)
Redis 是单线程,那怎么监听⼤量的客户端连接呢?
Redis6.0 之前为什么不使⽤多线程?
Redis6.0 之后为何引⼊了多线程?
Redis内存管理
Redis 给缓存数据设置过期时间有啥⽤? Redis 是如何判断数据是否过期的呢?过期数
据是如何被删除的?
Redis 内存满了怎么办?
Redis 内存淘汰算法除了 LRU 还有哪些?
Redis事务
Redis 事务⽀持原⼦性吗?
Redis 事务⽀持持久性吗?
Redis 事务有什么缺陷?如何解决?
Redis性能优化(重要)
Redis 批量操作的⽅式有哪些?
Redis ⼤ key 有什么危害?如何排查和处理?
如何发现 Redis 热 Key,有哪些解决⽅案?
为什么会有 Redis 内存碎⽚?如何清理 Redis 内存碎⽚?
为什么会有慢查询命令?为什么会有慢查询命令?
Redis生存问题(重要)
什么情况会出现缓存穿透?有哪些解决办法?
什么情况会出现缓存击穿?有哪些解决办法?
什么情况会出现缓存雪崩?有哪些解决办法?
缓存穿透和缓存击穿有什么区别?缓存雪崩和缓存击穿有什么区别?
缓存预热如何实现?
计算机网络
TCP与UDP(重要)
TCP 的三次握⼿与四次挥⼿的内容? TCP 为什么连接是三次握⼿⽽断开是四次握⼿?
建立一个 TCP 连接需要“三次握手”,缺一不可:
- 一次握手:客户端发送带有 SYN(SEQ=x) 标志的数据包 -> 服务端,然后客户端进入 SYN_SEND 状态,等待服务端的确认;
- 二次握手:服务端发送带有 SYN+ACK(SEQ=y,ACK=x+1) 标志的数据包 –> 客户端,然后服务端进入 SYN_RECV 状态;
- 三次握手:客户端发送带有 ACK(ACK=y+1) 标志的数据包 –> 服务端,然后客户端和服务端都进入ESTABLISHED 状态,完成 TCP 三次握手。
当建立了 3 次握手之后,客户端和服务端就可以传输数据啦。
断开一个 TCP 连接则需要“四次挥手”,缺一不可:
- 第一次挥手:客户端发送一个 FIN(SEQ=x) 标志的数据包->服务端,用来关闭客户端到服务端的数据传送。然后客户端进入 FIN-WAIT-1 状态。
- 第二次挥手:服务端收到这个 FIN(SEQ=X) 标志的数据包,它发送一个 ACK (ACK=x+1)标志的数据包->客户端 。然后服务端进入 CLOSE-WAIT 状态,客户端进入 FIN-WAIT-2 状态。
- 第三次挥手:服务端发送一个 FIN (SEQ=y)标志的数据包->客户端,请求关闭连接,然后服务端进入 LAST-ACK 状态。
- 第四次挥手:客户端发送 ACK (ACK=y+1)标志的数据包->服务端,然后客户端进入TIME-WAIT状态,服务端在收到 ACK (ACK=y+1)标志的数据包后进入 CLOSE 状态。此时如果客户端等待 2MSL 后依然没有收到回复,就证明服务端已正常关闭,随后客户端也可以关闭连接了。
只要四次挥手没有结束,客户端和服务端就可以继续传输数据!
TCP 与 UDP 的区别及使⽤场景?
是否面向连接:UDP 在传送数据之前不需要先建立连接。而 TCP 提供面向连接的服务,在传送数据之前必须先建立连接,数据传送结束后要释放连接。
是否是可靠传输:远地主机在收到 UDP 报文后,不需要给出任何确认,并且不保证数据不丢失,不保证是否顺序到达。TCP 提供可靠的传输服务,TCP 在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制。通过 TCP 连接传输的数据,无差错、不丢失、不重复、并且按序到达。
是否有状态:这个和上面的“是否可靠传输”相对应。TCP 传输是有状态的,这个有状态说的是 TCP 会去记录自己发送消息的状态比如消息是否发送了、是否被接收了等等。为此 ,TCP 需要维持复杂的连接状态表。而 UDP 是无状态服务,简单来说就是不管发出去之后的事情了(这很渣男!)。
传输效率:由于使用 TCP 进行传输的时候多了连接、确认、重传等机制,所以 TCP 的传输效率要比 UDP 低很多。
传输形式:TCP 是面向字节流的,UDP 是面向报文的。
首部开销:TCP 首部开销(20 ~ 60 字节)比 UDP 首部开销(8 字节)要大。
是否提供广播或多播服务:TCP 只支持点对点通信,UDP 支持一对一、一对多、多对一、多对多;
什么时候应该使用TCP:
当对网络通讯质量有要求的时候,比如:整个数据要准确无误的传送给对方,这往往用于一些可靠的应用,比如HTTP,HTTPS,FTP等传输文件的协议,POP,SMTP等邮件传输的协议。
什么时候使用UDP:
当对网络通讯质量要求不高的时候,要求网络通讯速度能尽量的快,这时就可以使用UDP。常见使用UDP协议的应用如下:QQ语音,QQ视频,TFTP等。
HTTP(重要)
看之前的文章
DNS(重要)
看之前的文章
Spring
什么是 Spring 框架?
Spring 是一款开源的轻量级 Java 开发框架,旨在提高开发人员的开发效率以及系统的可维护性。
我们一般说 Spring 框架指的都是 Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发,比如说 Spring 支持 IoC(Inversion of Control:控制反转) 和 AOP(Aspect-Oriented Programming:面向切面编程)、可以很方便地对数据库进行访问、可以很方便地集成第三方组件(电子邮件,任务,调度,缓存等等)、对单元测试支持比较好、支持 RESTful Java 应用程序的开发。
列举⼀些重要的 Spring 模块?
Core Container
Spring 框架的核心模块,也可以说是基础模块,主要提供 IoC 依赖注入功能的支持。Spring 其他所有的功能基本都需要依赖于该模块,我们从上面那张 Spring 各个模块的依赖关系图就可以看出来。
- spring-core:Spring 框架基本的核心工具类。
- spring-beans:提供对 bean 的创建、配置和管理等功能的支持。
- spring-context:提供对国际化、事件传播、资源加载等功能的支持。
- spring-expression:提供对表达式语言(Spring Expression Language) SpEL 的支持,只依赖于 core 模块,不依赖于其他模块,可以单独使用。
AOP
- spring-aspects:该模块为与 AspectJ 的集成提供支持。
- spring-aop:提供了面向切面的编程实现。
- spring-instrument:提供了为 JVM 添加代理(agent)的功能。 具体来讲,它为 Tomcat 提供了一个织入代理,能够为 Tomcat 传递类文 件,就像这些文件是被类加载器加载的一样。没有理解也没关系,这个模块的使用场景非常有限。
Data Access
- spring-jdbc:提供了对数据库访问的抽象 JDBC。不同的数据库都有自己独立的 API 用于操作数据库,而 Java 程序只需要和 JDBC API 交互,这样就屏蔽了数据库的影响。
- spring-tx:提供对事务的支持。
- spring-orm:提供对 Hibernate、JPA、iBatis 等 ORM 框架的支持。
- spring-oxm:提供一个抽象层支撑 OXM(Object-to-XML-Mapping),例如:JAXB、Castor、XMLBeans、JiBX 和 XStream 等。
- spring-jms : 消息服务。自 Spring Framework 4.1 以后,它还提供了对 spring-messaging 模块的继承。
Spring Web
- spring-web:对 Web 功能的实现提供一些最基础的支持。
- spring-webmvc:提供对 Spring MVC 的实现。
- spring-websocket:提供了对 WebSocket 的支持,WebSocket 可以让客户端和服务端进行双向通信。
- spring-webflux:提供对 WebFlux 的支持。WebFlux 是 Spring Framework 5.0 中引入的新的响应式框架。与 Spring MVC 不同,它不需要 Servlet API,是完全异步。
Messaging
spring-messaging 是从 Spring4.0 开始新加入的一个模块,主要职责是为 Spring 框架集成一些基础的报文传送应用
Spring Test
Spring 团队提倡测试驱动开发(TDD)。有了控制反转 (IoC)的帮助,单元测试和集成测试变得更简单。
Spring 的测试模块对 JUnit(单元测试框架)、TestNG(类似 JUnit)、Mockito(主要用来 Mock 对象)、PowerMock(解决 Mockito 的问题比如无法模拟 final, static, private 方法)等等常用的测试框架支持的都比较好。
谈谈⾃⼰对于 Spring IoC 和 AOP 的理解
IoC(Inversion of Control:控制反转) 是一种设计思想,而不是一个具体的技术实现。IoC 的思想就是将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理。
将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。 IoC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。
在实际项目中一个 Service 类可能依赖了很多其他的类,假如我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。如果利用 IoC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。
在 Spring 中, IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个 Map(key,value),Map 中存放的是各种对象。
Spring 时代我们一般通过 XML 文件来配置 Bean,后来开发人员觉得 XML 文件来配置不太好,于是 SpringBoot 注解配置就慢慢开始流行起来。
AOP
AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用 Cglib 生成一个被代理对象的子类来作为代理。
Spring Bean 的⽣命周期说⼀下
整体上可以简单分为四步:实例化 —> 属性赋值 —> 初始化 —> 销毁。
初始化这一步涉及到的步骤比较多,包含 Aware
接口的依赖注入、BeanPostProcessor
在初始化前后的处理以及 InitializingBean
和 init-method
的初始化操作。
销毁这一步会注册相关销毁回调接口,最后通过DisposableBean
和 destory-method
进行销毁。
Spring 中的 bean 的作⽤域有哪些?
singleton : IoC 容器中只有唯一的 bean 实例。Spring 中的 bean 默认都是单例的,是对单例设计模式的应用。
prototype : 每次获取都会创建一个新的 bean 实例。也就是说,连续 getBean()
两次,得到的是不同的 Bean 实例。
request (仅 Web 应用可用): 每一次 HTTP 请求都会产生一个新的 bean(请求 bean),该 bean 仅在当前 HTTP request 内有效。
session (仅 Web 应用可用) : 每一次来自新 session 的 HTTP 请求都会产生一个新的 bean(会话 bean),该 bean 仅在当前 HTTP session 内有效。
application/global-session (仅 Web 应用可用):每个 Web 应用在启动时创建一个 Bean(应用 Bean),该 bean 仅在当前应用启动时间内有效。
websocket (仅 Web 应用可用):每一次 WebSocket 会话产生一个新的 bean。
拦截器和过滤器了解么?
过滤器(Filter)是 [Java Servlet](https://so.csdn.net/so/search?q=Java Servlet&spm=1001.2101.3001.7020) 技术中的一个重要部分,主要用于在 Servlet 处理请求之前或响应之后对数据进行某些处理。过滤器可以实现诸如日志记录、请求数据修改、响应数据修改、权限控制等功能。
过滤器工作在 [Servlet 容器](https://so.csdn.net/so/search?q=Servlet 容器&spm=1001.2101.3001.7020)中,它拦截客户端的请求和服务器的响应。过滤器链(Filter Chain)是多个过滤器按照一定的顺序执行的集合,一个请求可以依次通过多个过滤器,然后到达目标 Servlet,响应也会按相反的顺序经过这些过滤器返回给客户端。
拦截器(Interceptor)是 Spring MVC 框架中的一个核心组件,用于在处理 HTTP 请求的过程中进行拦截和处理。拦截器主要用于实现跨切面(cross-cutting)的逻辑,如日志记录、性能统计、安全控制、事务处理等。
拦截器工作在 Spring 的 DispatcherServlet 和具体的 Controller 之间。当一个请求被发送到 Spring MVC 应用时,DispatcherServlet 首先接收到这个请求,然后根据配置的拦截器链对请求进行预处理,最后将请求转发到相应的 Controller 进行处理。
Spring 动态代理默认⽤哪⼀种
Spring 的动态代理主要有两种实现方式:基于 JDK 的动态代理和基于 CGLIB 的动态代理。
- JDK 动态代理:这是 Spring 的默认方式,适用于实现了至少一个接口的类。JDK 动态代理会根据接口生成代理类。
- CGLIB 动态代理:当要代理的类没有实现接口时,或者在某些配置中指定了使用 CGLIB,Spring 会使用 CGLIB 动态代理。CGLIB 是一个功能强大的字节码生成库,可以在运行时动态生成一个类的子类。
在 Spring 中,如果一个类实现了接口且没有特别配置,默认使用 JDK 动态代理。如果类没有实现接口,Spring 会自动切换到 CGLIB 代理。你也可以通过配置显式选择使用哪种代理方式。
hibernate 和 mybatis 区别
Hibernate 和 MyBatis 都是目前业界中主流的对象关系映射框架
1)sql 优化方面
- Hibernate 使用 HQL(Hibernate Query Language)语句,独立于数据库。不需要编写大量的 SQL,就可以完全映射,但会多消耗性能,且开发人员不能自主的进行 SQL 性能优化。提供了日志、缓存、级联(级联比 MyBatis 强大)等特性。
- MyBatis 需要手动编写 SQL,所以灵活多变。支持动态 SQL、处理列表、动态生成表名、支持存储过程。工作量相对较大。
2)开发方面
- MyBatis 是一个半自动映射的框架,因为 MyBatis 需要手动匹配 POJO 和 SQL 的映射关系。
- Hibernate 是一个全表映射的框架,只需提供 POJO 和映射关系即可。
3)缓存机制比较
Hibernate 的二级缓存配置在 SessionFactory 生成的配置文件中进行详细配置,然后再在具体的表-对象映射中配置缓存。
MyBatis 的二级缓存配置在每个具体的表-对象映射中进行详细配置,这样针对不同的表可以自定义不同的缓存机制。并且 Mybatis 可以在命名空间中共享相同的缓存配置和实例,通过 Cache-ref 来实现。
总的来说,MyBatis 是一个小巧、方便、高效、简单、直接、半自动化的持久层框架,Hibernate 是一个强大、方便、高效、复杂、间接、全自动化的持久层框架。
Spring Boot 和 Spring 的区别
Spring和Spring Boot都是Java开发中常用的框架,用于构建企业级应用程序。Spring是一个轻量级的框架,提供了丰富的功能和模块,而Spring Boot是基于Spring的快速开发框架,简化了配置和部署过程,提供了约定大于配置的开发体验。
说出使⽤ Spring Boot 的主要优点
Spring Boot是建立在Spring框架之上的微服务框架,旨在简化Spring应用的开发过程。与传统的Spring应用相比,Spring Boot采用约定优于配置的原则,通过提供默认配置和快速开发的特性,大大减少了开发者的工作量。Spring Boot还支持快速构建独立的、可执行的JAR包,使得应用的部署变得更加简单。
项目初始化快
什么是 Spring Boot Starter?
- 是依赖的集合,用来简化maven依赖配置
- 提供默认的配置starter使用注解自动配置的形式提供默认的配置,使开发项目更便捷。
- starter负责配置固定的版本以及依赖的整合问题。
- Spring Boot之所以能够帮我们简化项目的搭建和开发过程,主要是基于它提供的起步依赖和自动配置。
介绍⼀下 @SpringBootApplication 注解
这个注解标识了一个SpringBoot工程,它实际上是另外三个注解的组合,这三个注解是:
@SpringBootConfiguration:这个注解实际就是一个@Configuration,标识启动类也是一个配置类
@EnableAutoConfiguration:想SPring容器中导入了一个Selector,用来加载ClassPath下SpringFactories中所定义的自动配置类,将这些自动加载为配置Bean
@ComponentScan:标识扫描路径,因为默认是没有配置实际扫描路径,所以SpringBoot扫描的路径是启动类所在的当前目录
Spring Boot 的⾃动配置是如何实现的?
具体来说,Spring Boot的自动配置是通过扫描classpath下的各种配置类和标记了特定注解的类来实现的。这些配置类通常位于依赖jar包中,其中可能包含了一些默认的配置信息、Bean定义以及各种组件。Spring Boot会自动识别这些配置类,并根据特定的规则将它们加载到Spring IoC容器中。
在加载这些配置类时,Spring Boot会根据条件判断是否需要应用这些配置。例如,它会检查当前项目中的其他配置、环境变量、系统属性等,来决定是否应用某个配置类。这样一来,开发者可以根据自己的需求对自动配置进行定制,从而实现更灵活和个性化的配置。
总的来说,Spring Boot的自动配置使得开发者可以更加专注于业务逻辑的实现,而无需过多关注底层的配置细节,极大地提高了开发效率和代码质量。
Spring Boot ⽀持哪些嵌⼊式 web 容器?
Undertow、tomcat、jetty
是Spring Boot 默认集成的三大容器。
置的原则,通过提供默认配置和快速开发的特性,大大减少了开发者的工作量。Spring Boot还支持快速构建独立的、可执行的JAR包,使得应用的部署变得更加简单。
项目初始化快
什么是 Spring Boot Starter?
- 是依赖的集合,用来简化maven依赖配置
- 提供默认的配置starter使用注解自动配置的形式提供默认的配置,使开发项目更便捷。
- starter负责配置固定的版本以及依赖的整合问题。
- Spring Boot之所以能够帮我们简化项目的搭建和开发过程,主要是基于它提供的起步依赖和自动配置。
介绍⼀下 @SpringBootApplication 注解
这个注解标识了一个SpringBoot工程,它实际上是另外三个注解的组合,这三个注解是:
@SpringBootConfiguration:这个注解实际就是一个@Configuration,标识启动类也是一个配置类
@EnableAutoConfiguration:想SPring容器中导入了一个Selector,用来加载ClassPath下SpringFactories中所定义的自动配置类,将这些自动加载为配置Bean
@ComponentScan:标识扫描路径,因为默认是没有配置实际扫描路径,所以SpringBoot扫描的路径是启动类所在的当前目录
Spring Boot 的⾃动配置是如何实现的?
具体来说,Spring Boot的自动配置是通过扫描classpath下的各种配置类和标记了特定注解的类来实现的。这些配置类通常位于依赖jar包中,其中可能包含了一些默认的配置信息、Bean定义以及各种组件。Spring Boot会自动识别这些配置类,并根据特定的规则将它们加载到Spring IoC容器中。
在加载这些配置类时,Spring Boot会根据条件判断是否需要应用这些配置。例如,它会检查当前项目中的其他配置、环境变量、系统属性等,来决定是否应用某个配置类。这样一来,开发者可以根据自己的需求对自动配置进行定制,从而实现更灵活和个性化的配置。
总的来说,Spring Boot的自动配置使得开发者可以更加专注于业务逻辑的实现,而无需过多关注底层的配置细节,极大地提高了开发效率和代码质量。
Spring Boot ⽀持哪些嵌⼊式 web 容器?
Undertow、tomcat、jetty
是Spring Boot 默认集成的三大容器。
原文地址:https://blog.csdn.net/fjdjdj1/article/details/142631898
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!