当前位置:   article > 正文

java面试题_java bigdecimal 面试题

java bigdecimal 面试题

数据结构

红黑树和二叉树的区别

二叉树

二叉树只有一个根节点、最多两个叶子节点,叫做左子树、右子树,它可以进行前、中、后序遍历,有几种特殊的状态:

img

二叉搜索树

性质:

  • 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  • 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  • 它的左右子树也分别为二叉搜索树

红黑树

是一种二叉查找树,但在每个节点上增加了一个存储位来表示节点的颜色。为了确保平衡,从任何一条根到叶子的路径长度不会比其他的路径长出两倍,所以对节点的颜色做了限制。限制如下:

根节点为黑色

如果一个节点为红色,则其两个孩子节点都为黑色

对于每个结点,从该结点到其所有后代叶节点的简单路径上,均包含相同数目的黑色结点

每个叶子节点都是黑色的(此处的叶子接待你指的是空结点)

二叉平衡树

左右子节点高度差不高于2

二叉树

可能发展成链表,查询的时间复杂度为O(n)

java基础

谈谈你对java的理解

解析:从java跨平台、类加载、垃圾回收、类库

**回答模板:**java语言有着”一次编译、到处运行“的特性,其中java的跨平台与java虚拟机有密不可分的关系,其可以在不同的环境中运行。比如Windows平台和Linux平台都有相应的平台,安装好JDK后就有java的运行环境。对于我们开发的java代码,java的编译器会编译成.Class字节码文件,然后通过JVM进行类加载、解释或者执行。同时java有着”java是解释执行“的说法,我看来不是很准确,常用的jvm是由Oracle JDK提供的Hotspot JVM,提供了JIT(Just-In-Time)动态编译器,JIT也叫热点代码编译器,它能够将热点代码编译成机器码,这部分代码就是编译执行了。

日常的编译模式之外还有一种新的编译方式,即AOT(Ahead-of-TimeCompilation)直接将字节码编译成机器代码,省去了JIT等代码的预热开销。

同时在C语言中,因为其调用操作系统层面的API,不同的操作系统API一般是不同的,为了支持多平台,C语言的源文件是需要根据不同平台进行修改多次。所以焦点是对源文件的修改上。

基本数据类型

Integer和int的初始化区别

Integer是包装类,内部还是采用int的构造存储,如果在实例化的时候不赋值,默认初始值是为null

int默认的初始值为0

Double类型之前的加减

因为java语言在编译时会将基本数据类型由十进制转为二进制,而double由两个部分组成:指数和尾数

在double由十进制转成二进制时,由于存在整数部分和小数部分,整数转成二进制时进行整除2,小数位乘2,直到尾数为0。

img

而在转换成二进制时,会将指数位得出十进制的指数再转换为二进制存储指数位

这个过程就会发生精度丢失问题。

主要原因就是计算机中所有的数字都是二进制表示的,但是浮点数没有办法完全用二进制表示,用二进制表示浮点数会损失一定的精度,就像上面0.1用二进制表示时是无限接近于0.1,就变成了0.2+0.1000000000004。

解决办法

使用数据类型BigDecimal解决精度问题

通过double转bigdecimal的时候,可以使用三个方法:

有一种静态方法

// 方式一 public BigDecimal(double val) { this(val,MathContext.UNLIMITED);}
// 方式二 public BigDecimal(String val) { this(val.toCharArray(), 0, val.length());}
// 方式三(其实底层就是方式二)public static BigDecimal valueOf(double val) {
new BigDecimal(val.toString());
}
  • 1
  • 2
  • 3
  • 4
  • 5

所以将一个double转为bigdecimal时可以调用public BigDecimal(String val)方法进行计算

BigDecimal

能用来处理有效位超过16位有效数字的运算,里面建议用valueof来转换double格式的数据,直接new BigDecimal(double val),new出来的数值是增加了小数位的数据,原数据被修改了不精准。

BigDecimal如何保证精度的?

浮点型发生精度丢失的原因就是在转换成二进制存储时,十进制的小数在转化成二进制浮点数时会产生精度问题,小数可能会产生无穷接近的小数,则会产生精度丢失的问题。

解决方法:将小数部分单独拿来扩大为2的n次方倍后转换成整数进行运算,同时一般调用构造方法传入的值为String类型最好。

基础概念

java的三大特征是什么?

封装:通过私有化,将类中的函数、属性进行隐藏,只保留一些对外的方法与外界进行交互,用户不必知道具体的实现。例如:bean的封装

继承:继承是从已有的类中派生出新的类, 新的类能吸收已有类的数据属性和行为,并能扩展新的能力,被继承的类叫父类(parent class)或超类(superclass), 继承父类的类叫子类(subclass)或派生类(derivedclass)。 因此, 子类是父类的一个专门用途的版本, 它继承了父类中定义的所有实例变量和方法, 并且增加了独特的元素 。

方法的重写:

方法重写是在继承关系下,子类拥有与父类方法名、参数(个数、顺序、 类型)、 返回值类型完全相同,访问修饰符只能扩大或相等,不可缩小,但实现过程与父类不同的方法。方法重写也是多态的一种变现形式。

多态: 是指一个方法可以有多种实现版本,即“一种定义, 多种实现”。 利用多态可以设计和实现可扩展的系统, 只要新类也在继承层次中。 新的类对程序的通用部分只需进行很少的修改, 或不做修改。 类的多态性表现为方法的多态性,方法的多态性主要有方法的重载和方法的覆盖。

重载:

方法重载(overload)是指在同一个类中的多个方法可以同名但参数列表必须不同。 重载表现为同一个类中方法的多态性。

Java的引用类型有哪些?强引用在内存满了如何被回收?

强引用、软引用、弱引用、虚引用。

如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。如果不使用时,要通过如下方式来弱化引用,如下:

显式地设置o为null,或超出对象的生命周期范围,则gc认为该对象不存在引用,这时就可以回收这个对象。具体什么时候收集这要取决于gc的算法。

虚引用

虚引用必须与引用队列联合使用,主要用于跟踪对象被垃圾回收的活动,当回收器发现对象存在虚引用时,会在回收对象的内存之前将这个虚引用加入到引用队列中,程序通过判断引用队列是否加入了虚引用来了解被引用的对象是否要被垃圾回收。过程是怎么样的呢

当垃圾回收器回收一个对象后,虚引用会进入引用队列,在其虚引用出队前,不会彻底销毁该对象,可以通过检查引用队列是否存在该引用来判断对象是否完全被回收

强软弱虚对象在哪里用

ThreadLocal 中的Map里面的Entry就是使用的弱引用

为什么要使用弱引用?

img

同样假设在业务代码中使用完ThreadLocal ,threadLocal Ref被回收了。

由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向threadlocal实例, 所以threadlocal就可以顺利被gc回收,此时Entry中的key=null。

但是在没有手动删除这个Entry以及CurrentThread依然运行的前提下,也存在有强引用链 threadRef->currentThread->threadLocalMap->entry -> value ,value不会被回收, 而这块value永远不会被访问到了,导致value内存泄漏。

也就是说,ThreadLocalMap中的key使用了弱引用, 也有可能内存泄漏。

内存泄漏的两个前提:

  • 没有手动remove这个entry
  • 当前线程仍然在运行
为什么使用弱引用

根据刚才的分析, 我们知道了: 无论ThreadLocalMap中的key使用哪种类型引用都无法完全避免内存泄漏,跟使用弱引用没有关系。

要避免内存泄漏有两种方式:

  1. 使用完ThreadLocal,调用其remove方法删除对应的Entry
  2. 使用完ThreadLocal,当前Thread也随之运行结束

同时为了更好的回收Entry,ThreadLocal中定义了expungeStaleEntry方法用于清理key为null的value。expungeStaleEntry在remove中方法中调用。

注意:虽然ThreadLocal有expungeStaleEntry方法清除key为null的元素。但是可以看出循环退出的条件为遇到null的元素,因此null之后的并且key为null的元素无法被清除。并且这种清除方式是不及时的。所以在一定情况下依然会发生内存泄漏,最好的办法就是每次调用完之后及时使用remove方法。

类初始化顺序

父类静态代码块和静态成员变量-> 子类静态代码块和静态成员变量-> 父类代码块和普通成员变量->父类构造方法->子类代码块和普通成员变量->子类构造方法

final关键字

作用于不同地方有不同的特征:

标记类:表示这个类不能被继承,修饰的类所有成员方法都将被隐式修饰为final方法。

标记方法:表示这个方法不能重写,另外一个作用是在编译器对方法进行内联, 提升效率

标记变量:表示常量,在初始化时需,要赋予初始值,如果值为基本数据类型,则值不改变

如果为引用类型,引用在初始化后将永远指向一个内存地址, 不可修改。但是该内存地址中保存的对象信息, 是可以进行修改的。

Integer的缓存机制

Byte、Short、Integer、Long、Character都是具有缓存机制的类。缓存工作都是在静态块中完成,在类生命周期的初始化阶段执行。

缓存的范围:

Byte、Short、Integer、Long缓存的范围为-128 - 127

唯有Character的范围为0-127

Integer可以通过jvm参数指定缓存范围,其它类都不行。

Integer的缓存上界high可以通过jvm参数-XX:AutoBoxCacheMax=size指定,取指定值与127的最大值并且不超过Integer表示范围,而下

界low不能指定,只能为-128。

包装类的自动装箱拆箱

自动装箱: 就是将基本数据类型自动转换成对应的包装类。

自动拆箱:就是将包装类自动转换成对应的基本数据类型。

实现原理

通过包装类的Valueof方法实现自动装箱

通过包装类的xxValue()方法实现自动拆箱

抽象类和接口的区别

对于抽象类,他是描述了一个类,接口是描述这个类有什么功能,同时抽象类中能存在抽象方法,

不能存在抽象方法的实现,能存在普通方法且方法的修饰符只能是public或者protect,可以包含构

造方法、成员变量,只是不能实例化,

接口中不能存在构造方法、成员变量、普通方法实现,可以存在抽象方法

接口能被多实现,但是抽象类只能被其他的类单继承,但是接口能够实现多继承(通过实现多个接口)

JDK1.8中对接口增加了新的特性:
(1)默认方法(default method):JDK 1.8允许给接口添加非抽象的方法实现,
但必须使用default关键字修饰;定义了default的方法可以不被实现子类所实现,
但只能被实现子类的对象调用;如果子类实现了多个接口,并且这些接口包含一样的默认方法
则子类必须重写默认方法;
(2)静态方法(static method):JDK 1.8中允许使用static关键字修饰一个方法,

StackOverFlowError原因(java基础的题目 涉及线程、堆栈空间)

每个线程都有自己的堆栈,每个堆栈都有多个堆栈帧,线程按照执行顺序将其执行的方法、数据类型、对象指针和返回值都存入了堆栈中占用堆栈的内存,一旦超过堆栈的容量JVM即抛出错误
例如:
	类里面调用自己的main方法,参数为new的字符串,执行一段时间即报错,堆栈内存错误。
  • 1
  • 2
  • 3

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rEufy5zh-1686838790604)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20220807150030791.png)]

String、StringBuffer、StringBuilder的区别

String
是一个不可变类,一旦创建,在这个对象内的字符序列即不可更改,直至对象被销毁
string a = 123;
a = 456;
下图为该代码的内存存储空间图
  • 1
  • 2
  • 3
  • 4

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BpxGNjPR-1686838790604)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20220807151045370.png)]

根据图片可以看出,给a进行重新赋值时,内存中会重新创建一个对象实例来进行存储字符序列,然后在栈中的引用重新指向新对象,原来的实例对象仍然存在,如果没有再次被引用,则会被gc回收
  • 1
Stringbuilder和StringBuffer
两者都是字符序列可变的字符串,在内存中重新赋值是针对于原来的字符序列,也就是是说跟String类重新赋值是创建新的对象不同,StringBuffer是对同一个序列(transient char[]源代码的字节数组)重新添加或者赋值。不同的是StringBuffer是线程安全的,他的实现方法是在每个基础方法上添加同步锁,这也意味着效率降低
  • 1

== 和 equals的区别

== 对于基本数据类型比较的是值,对于引用类型比较的是地址
equals:比较的是对象是否相等(在包装类中先进行类型的比较,再进行值的比较)

equals未被重写时,object中的方法其实也是==判断,比较的还是地址,

但string、integer类中对equals进行了重写,判断的是值是否相等。

为什么重写equals要重写hashcode

因为equals比较是对象的地址,而hashcode计算的是哈希码

如果重写equals不重写hashcode,就可能导致equals相等而hash值不相等的现象,那么这个只重写了equals的对象在使用散列结合进行存储的时候就会出现问题,如果两个相同的对象存在不同的hash值,则会导致对象存储在不同的位置,导致异常的发生。

总而言之,重写equals后判断的是值本身,相同的equals值可能不是一个对象,而hash不重写可能导致hash值相同,对象不同。所以可能导致hash值不同。

那么,只重写了equals()而没有重写hashcode(),会出现什么问题呢?
  只重写了equals()而没有重写hashcode(),那么两个对象的hashcode就是从内存地址转化而来,一定不相同,即使是equals的。这就会导致这两个equals的对象被存到了哈希表中不同的位置上,这就违反了key的唯一性。

反过来,只重写了hashcode()而没有重写equals(),又会有什么问题?

只重写了hashcode()而没有重写equals(),那么equals()比较的相当于就是内存地址,两次new出来的对象一定是不同的,但如果他们的成员属性的值都相同,那么他们的hashcode就是相同的,会存在同一个链表(或红黑树)中。致命的是,再使用get(key)来获取值时,只要这时传入的key和set(key, value)时使用的key不是同一个对象,即使每个成员属性的值都一样,也不能得到想要的结果,因为这两个key是不equals的,会被当做不同key。

反射

java的反射可以在一个程序运行时获得一个类的成员变量、成员方法、属性。通过反射,可以动态调用某个类的方法以及获取类的信息,通过java.lang.reflect包来调用相应的类库:Construct、Field、Method

应用场景

比如动态代理中通过反射创建代理类,以及spring中通过反射实例化bean对象

如何对一个对象的方法进行增强

1)继承
2)装饰者模式

1、重新创建需要增强的类的接口的子类

2、将接口类私有化并作为构造函数的参数

3、实现接口内的方法,并在需要增强的方法内调用接口.方法()以及之后添加增强逻辑 即可

1 //接口
2 public interface Cat {
3     //吃的方法
4     void eat();
5     //玩的方法
6     void play();
7 }

1 //需要增强的类
 2 public class HomeCat  implements Cat{
 3 
 4     @Override
 5     public void eat() {
 6         // TODO Auto-generated method stub
 7         System.out.println("吃鱼的方法");
 8     }
 9 
10     @Override
11     public void play() {
12         // TODO Auto-generated method stub
13         
14     }
15 
16 }

1 //进行增强的类
 2 public class NewCat implements Cat{
 3     private Cat c;
 4     public  NewCat(Cat c){
 5         this.c=c;
 6     }
 7     //进行增强
 8     @Override
 9     public void eat() {
11         System.out.println("增强前");
12         c.eat();
13         System.out.println("增强后");
14     }
15 
16     @Override
17     public void play() {
19         c.play();
20     }
22 }

1 //测试类
2 public class Demo1 {
3     public static void main(String[] args) {
4         NewCat newcat=new NewCat(new HomeCat());
5         newcat.eat();
6     }
7 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
3、动态代理

概念:在程序运行期间,创建目标对象的代理对象,并对目标对象的方法进行功能性的增强的一种技术。

缺点:只能对接口进行代理。如果要代理的类为一个普通类、没有接口,那么Java动态代理就没法使用了。

实现:

1、基于jdk动态代理(运行时生成代理对象 )

调用java.lang.reflect.Prxy类中的newProxyInstance()

3个参数:类加载器,增强方法所在类实现的接口,实现invocationHandler接口,创建代理对象,填写增强方法

实现过程:

通过invocationHandler接口创建调用处理器

通过Proxy类指定类加载对象和接口创建代理类对象

通过反射获得代理类对象的构造函数,方便调用处理器参数类型

通过构造函数构建代理类实例,构造时调用处理器对象作为参数被传入

2、cglib动态代理(javac时生成代理)

1)添加cgliib依赖
2)创建增强器Enhancer类对象
3)调用Enhancer类对象的setSuperclass(Class cls)方法设置需要增强类的对象,参数为需要增强的类。
4)调用Enhancer类对象的回调方法 enhancer.setCallback(MethodInterceptor interceptor),与jdk动态代理一样,有两种方法。
5)获取增强之后的代理对象

动态代理和静态代理的区别

静态代理模式类似于装饰模式(具体模式可见40题)

静态代理代理的是一个具体的类,而动态代理对象为接口下的多个实现类

静态代理必须要事先知道代理的是什么,而动态代理则无须知道,只有在运行时才知道。

动态代理的底层分析

1)测试类(调用代理对象proxy的sayHello方法)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O7TYe6Z0-1686838790605)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20220818091031100.png)]

2)生成的代理对象proxy字节码文件 中的sayHello方法的invoke方法进行注入

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3B4ZIKiL-1686838790605)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20220818091138303.png)]

3)invocationHandler类(在创建代理对象需要实现的类,其中填写增强的方法)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ApBphBqN-1686838790606)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20220818091315985.png)]

小结:在测试类中调用的sayHello方法是指调用代理对象的sayHello方法,其中的super.h的h其实就是InvocationHandler类,通过构造方法进行注入。如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xAl4N6pf-1686838790607)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20220818091611519.png)]

其中参数每个是:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wRFkC1jT-1686838790608)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20220818092723901.png)]

更加详细的介绍:https://baijiahao.baidu.com/s?id=1678997916718732701&wfr=spider&for=pc

java属于编译型语言还是解释型语言?

编译型语言指通过编译器将代码编译成可执行文件,计算机执行文件即可。属于一次编译即可。

解释型语言是指通过解释器将代码转成机器码,并立马交由计算机执行,属于边翻译边执行。

java语言则是两者相结合的方式:java的编译器将代码编译成中间产物字节码文件永久保存,并交由jvm进行解释成机器指令进而在计算机中运行。

intern方法

String str = new String("ab");
str.intern(); // 在常量池中创建ab字符串
  • 1
  • 2

各个类修饰符的访问权限

类内部本包子类外部包
public
protected×
default××
private

Session和Cookie以及token的问题

Cookie存储在浏览器。

产生过程:当浏览器第一次访问服务端时,服务端会记录身份信息,格式为key=value,放入Http请求头的Set-Cookie字段中,随响应报文一并发给浏览器,浏览器接收并保持这个身份标识。下次请求会自动将key=value字段发给服务端。

img

缺点:cookie不安全(敏感数据需要加密,通过SSL),只能保存少量数据,同时浏览器存储的cookie数量也有限制。

优点:

可以控制cookie的生命期

分担了服务器存储的压力

Session(会话控制):

  • 存储在服务端的内存中

产生过程:(某服务端程序(如Servlet)调用**HttpServletRequest.getSession(true)**这样的语句时才会被创建)当浏览器第一次请求服务端时,服务端自动创建一个Session对象并附带一个Session_ID与之对应,id值会返回到浏览器,之后的请求中,Cookie会携带这个id来识别id。当会话过期或放弃后,服务端将终止会话。

Cookie和Session的区别

    cookie机制采用的是在客户端保持状态的方案,而session机制采用的是在服务器端保持状态的方案。 由于在服务器端保持状态的方案在客户端也需要保存一个标识,所以session机制可能需要借助于cookie机制来达到保存标识的目的,但实际上还有其他选择(url方案)。
  • 1

Token

由服务端生成的一段字符串,作为客户端进行请求的令牌,在第一次登录时服务端产生。token不存储数据,只是作为一个key,与之相关的数据还是存储在服务器上。

  • 存放在客户端(浏览器)
    • 存储在LocalStorage中,每次调用接口都把它当成一个字段传给后台
    • 存储在cookie中,自动发送,缺点不能进行跨域

token与Cookie的区别

Cookie不允许跨域,token支持跨域,前提传输的用户信息是HTTP头传输的

相对优势

1、无状态化,服务端无需存储token,只需要验证token数据,而session需要存储在服务端,通过cookie的sessionid查找对应的session。
2、避免CSRF跨站伪造攻击。
  • 1
  • 2
什么是跨域

1.当一个请求url的**协议、域名、端口**三者之间任意一个与当前页面url不同即为跨域。

Session与Token的区别

身份认证方面,token比session更安全。

重定向和请求转发的区别

1、请求转发是服务器行为、重定向是客户端浏览器行为
2、请求转发是request对象调用方法、重定向是response对象调用方法
3、请求转发只有一次请求所以可以实现request域对象中的数据共享,而重定向是多次请求、多次响应
4、请求转发的效率要高于重定向
5、请求转发url地址栏不变,而重定向会发生变化
6、既然请求转发是服务器内部的行为,所以只能访问服务器内部资源!而重定向既然是浏览器行为,地址栏会变,所以可以访问服务器外部资源!

异常体系

Throwable类为所有的异常类的根类

其中又包括了两个大类:Error和Exception

其中Error一般是jvm层面所产生的错误,一旦发生则程序崩溃

而Exception又分为检查型异常和非检查型:

检查型异常(编译时异常):例如输入输出的IO异常、SQLException、FileNotFoundException

非检查型(运行时异常):空指针异常、classNotFoundException、数组越界

NoClassDefFoundError和ClassNotFoundException 的区别

NoClassDefFoundError 是个Error,是指一个class在编译时存在,在运行时找不到了class文件了;

造成该问题的原因可能是打包过程漏掉了部分类,或者jar包出现损坏或者篡改。解决这个问题的办法是查找那些在开发期间存在于类路径下但在运行期间却不在类路径下的类。

ClassNotFoundException 是个Exception

Java支持使用Class.forName方法来动态地加载类,任意一个类的类名如果被作为参数传递给这个方法都将导致该类被加载到JVM内存中,如果这个类在类路径中没有被找到,那么此时就会在运行时抛出ClassNotFoundException异常。

Throws和Throw的区别

Throws用于方法声明后面,跟的是异常类名

throws XXException抛出异常 在声明变量或某类时
  • 1

Throw用于方法体,用来手动抛出异常

throw new XXException(""); 抛出异常,例如在catch中抛出
  • 1

如何自定义一个异常类

可以通过继承RuntimeException类实现,并在类中提供一个有参和无参的构造方法

使用java异常处理机制的一些开销
  • try-catch代码会产生额外的性能开销,往往会影响JVM对代码的优化,所以try不要包住整段代码;
  • java每实例化一个Exception,都会对当前的栈进行快照,这是一个相当比较重的操作。

IO流体系

种类

根据数据流的方法不同分类:输入流,输出流

根据处理数据单位不同分类:字节流,字符流

按照处理角色来分类:节点流(从一个特定的数据源中读取或写入数据的流)、处理流(已存在的流的连接和封装)

字节流:最小单位为字节,基类为inputStream和outputStream 处理字节byte(8位数据)

字符流:最小单位为字符,基类为Reader和Writer 处理字符char(16位数据,也就是中文)

两者先有字节流再有字符流,但是字符流更符合人操作,但是还是需要转换成字节流供机器读取。

这也意味着,在操作字符流时,需要传入字节流的数据

InputStream OutputStream 是字节流所以在输出的时候需要考虑 String转为byte 然后byte转为String

InputStreamReader OutputStreamReader 是字符流 输出的时候不需要考虑互相转换 直接就可以输出String。Reader/Writer 则是用于操作字符,增加了字符编解码等功能,适用于类似从文件中读取或者写入文本信息。本质上计算机操作的都是字节,不管是网络通信还是文件读取,Reader/Writer 相当于构建了应用逻辑和原始数据之间的桥梁。

字节流直接操作文件本身,字符流则是针对内存开辟一个缓冲区

并发

并行和并发的区别

并行是指多核的CPU在同一时间同时可以执行多个线程的一个能力。

并发是指同一个资源被不同的线程交替地执行,也就是一个CPU在一个时间段可以执行的任务量

线程的启动

继承Thread
	new Thred().start() // 即可启动

实现Runnable接口(启动方法:启动需要创建线程Runnable的实例,并把这个实例作为线程类的target)
	new Thread(new RunableImpl).start();

实现Callable(启动方法:)
	 CallThread callThread = new CallThread();
     FutureTask<String> futureTask = new FutureTask<>(callThread);
     Thread thread = new Thread(futureTask);
     
	该方式的优点;该接口方法为call()作为线程执行体(有返回值,抛出异常、Callable接受一个泛型,在call方法中返回这一类型的值。Runnable的run无返回值)

线程池创建
    //创建线程池
    ExecutorService executorService = new ThreadPoolExecutor(3, 10,
    50, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
    //创建
    Callable myCallable = new Callable() {
        @Override
        public String call() throws Exception {
            Thread.sleep(3000);
            System.out.println("call方法执行了");
            return "call方法返回值";
        }
    };
    Future future = executorService.submit(myCallable);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

线程运行的三个状态是什么?线程的生命周期?

三大状态:就绪、运行、阻塞

五大状态:创建、就绪、运行、阻塞、终止

生命周期:图结构如下, 对应五个状态。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pqPce0PD-1686838790609)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20220722090913848.png)]

线程中start方法和run方法的区别

Start运行之后会重新启动一个新的线程去处理run里面的方法。

run启动则程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码。

当一个线程启动之后,不能重复调用start(),否则会报IllegalStateException异常。但是可以重复调用run()方法。

sleep(),wait(),join(),yield()的区别

锁池:所有需要竞争锁的线程都会被放入到锁池当中,比如a对象的锁已被其中一个线程拿到,其他竞争线程都会进入到锁池中,等待a对象释放锁,当某个对象得到锁之后会进入到就绪状态。

等待池:线程调用wait方法之后,会进入到等待池当中,等待池中的线程不会去竞争同步锁的,只有调用notify或者notifyAll之后,线程才会开始竞争锁,notify是随机将等待池中的一个线程放到锁池当中,而notifyAll是将等待池中所有的线程放入到锁池当中。

1,sleep只是将线程进行休眠,不会释放锁,而wait会释放锁,使得其他线程可以竞争锁。

2,sleep是可以在任何地方使用的,但是wait,notify,notifyAll只能在同步块中使用。

3,sleep必须捕获异常,wait不用。

yield:调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间。让CPU选出一个优先级最高的线程进行执行,也就是当前线程有可能执行

join:当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的join线程执行完为止。

join()方法通常由使用线程的程序调用,以将大问题划分成许多小问题,每个小问题分配一个线程。当所有的小问题都得到处理后,再调用主线程来进一步操作。

如何保证线程的执行顺序

Join方法
 // 线程t2依赖于t1的执行结果,所以通过在t2中调用t1.join()方法后,t1会强占资源锁,直至t1运行完再运行t2,这是保证线程有序执行的一种方法
 public static void main(String[] args) throws Exception{
      Thread t1 =  new Thread(()->{
          try {
              Thread.sleep(500);
              System.out.println("线程1醒了");
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
            for(int i=0;i<100;i++){
                System.out.println("线程1 i:"+i);
 
            }
      });
      t1.setName("线程1");
 
      Thread t2 = new Thread(()->{
          try {
              t1.join();
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
         for(int i=0;i<100;i++){
             System.out.println("线程2 i:"+i);
             try {
                 Thread.sleep(100);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
      });
      t2.setName("线程2");
 
        t2.start();
        t1.start();
 
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
LockSupport.park和unpark方法

unpark函数为线程提供“许可(permit)”,线程调用park函数则等待“许可”,但是无论unpark重复调用多少次,也只能提供一个许可,也就是一个park就能消耗掉许可。

比如线程B连续调用了三次unpark函数,当线程A调用park函数就使用掉这个“许可”,如果线程A再次调用park,则进入等待状态。

注意,unpark函数可以先于park调用。比如线程B调用unpark函数,给线程A发了一个“许可”,那么当线程A调用park时,它发现已经有“许可”了,那么它会马上再继续运行。

public void printA(Thread thread){
        try {
            Thread.sleep(20l);
            System.out.println("A");
            LockSupport.unpark(thread);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void printB(Thread thread){
        try {
            Thread.sleep(10l);
            LockSupport.park();
            System.out.println("B");
            LockSupport.unpark(thread);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void printC(){
        try {
            Thread.sleep(20l);
            LockSupport.park();
            System.out.println("C");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
   //控制台输出: A B C
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
使用线程的CountDownLatch(倒计数)方法
/**
     * 用于判断线程一是否执行,倒计时设置为1,执行后减1
     */
    private static CountDownLatch c1 = new CountDownLatch(1);

    public static void main(String[] args) {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程1");
                c1.countDown();
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    //等待c1倒计时,倒计时为0向下执行
                    c1.await();
                    System.out.println("线程2");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread2.start();
        thread1.start();
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
使用CyclicBarrier(回环栅栏)实现线程按顺序运行

通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。我们暂且把这个状态就叫做barrier,当调用await()方法之后,线程就处于barrier了。

应用场景:公司组织春游,等待所有的员工到达集合地点才能出发,每个人到达后进入barrier状态。都到达后,唤起大家一起出发去旅行

线程如何中止

线程因为是协作的,中断信号不保证一定执行,所以调用interpret方法后需要查询isinterpret是否中断进而进行其他操作

每个线程都有一个boolean标识,代表着是否有中断请求,使用 interrupt 来通知线程停止运行,并设置标志位为true,而不是强制停止

interpreted静态方法,返回当前线程的中断标记位,同时清除中断标记,改为false。比如当前线程已中断,调用interrupted(),返回true, 同时将当前线程的中断标记位改为false, 再次调用interrupted(),会发现返回false

线程池(必须了解)

线程池实现

线程池java存在四种:

支持可回收缓存 newCachedThreadPool

支持定时任务与周期性任务的线程池 newScheduleThreadPool

单线程线程池 newSingleThreadPool

可控线程数量 newfixedThreadPool

可以使用单线程线程池来实现:串行执行所有任务。

线程池的核心参数(ThreadPoolExecutor基类)

CorePoolSize 核心线程数

maxnumPoolSize 最大线程数

keepAliveTime 救急线程生存时间

unit 时间单位(救急线程)

workQueue 工作队列 默认的阻塞队列为LinkedBlockingQueue

handler 拒绝策略

threadFactory 线程工厂

线程池使用流程

img

线程池拒绝策略

1、中止策略,直接抛出异常以及捕获异常进行二次处理

2、抛弃策略,对该线程什么都不做

3、抛弃最老策略,替换掉等待队列中最先进入的线程

4、调用者运行策略,该策略既不会抛弃任务,也不会抛出异常,而是将任务回退给调用者。

线程池的实现原理

ThreadPoolExecutor 实现的顶层接口是Executor,顶层接口 Executor提供了一种思想:将任务提交和任务执行进行解耦。用户无需关注如何创建线程,如何调度线程来执行任务,用户只需提供 Runnable对象,将任务的运行逻辑提交到执行器(Executor)中,由 Executor框架完成线程的调配和任务的执行部分。
ExecutorService接口增加了一些能力:(1)扩充执行任务的能力,补充可以为一个或一批异步任务生成 Future的方法;(2)提供了管控线程池的方法,比如停止线程池的运行。
AbstractExecutorService则是上层的抽象类 ,将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法即可。最下层的实现类 ThreadPoolExecutor实现最复杂的运行部分。
ThreadPoolExecutor将会一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务。

ThreadPoolExecutor 执行 execute 方法分下面4中情况:
1)、如果当前运行的线程少于 corePoolSize,则创建新线程来执行任务(执行需要获取全局锁)。
2)、如果运行的线程等于或大余 corePoolSize,则将任务加入 BlockingQueue。
3)、如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(需要获取全局锁)
4)、如果创建新线程将使当前运行的线程超过 maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecutor() 方法。

ThreadLocal

ThreadLoacl中存放着一个ThreadLocalMap,再里面就是使用了Entry对象进行存储Key-value,Entry里面存放的是当前ThreadLocal对象,value为要隔离的变量,在get和set中,会查看map是否存在当前ThreadLoacl的key,存在即更新,不存在创建当前线程实例并插入,get值存在即返回,不存在更新map的key为当前ThradLoacl,value为null,适用于每个线程都有自己单独的实例,实例方法中共享但是线程不共享。

多个线程共同持有ThreadLocal对象时,每个ThreadLocal所拥有的ThreadLoaclMap对象是独一无二的,也就是不同线程之间Map中的Key相同,但是隔离的变量副本是不同的,达到隔离变量的目的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ngx3JY5v-1686838790613)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230223091240903.png)]

同时,Entry继承了WeakReference类,也就是key为弱引用,在下一次GC发生时会被回收当前的Key,所以ThreadLocal类定义了expungeStaleEntry方法用于清理key为null的value。expungeStaleEntry在remove中方法中调用。

为了避免内存泄漏的问题,在使用完ThreadLocal对象后应该及时进行remove

如何在线程池的核心线程满了后任务不进入队列?

构造线程池时,可以选择阻塞队列的类型,

可以使用SynchronousQueue队列,该队列不能存储任何元素的一个阻塞队列,特性就是每有一任务就需要指派给一个线程进行执行,否则阻塞生产者。

阻塞队列在异步消费如何保证顺序

首先,阻塞队列本身是符合FIFO特性的队列,也就是存储进去的元素符合先进先出的规则。

其次,在阻塞队列里面,使用了condition条件等待来维护了两个等待队列,一个是队列为空的时候存储被阻塞的消费者另一个是队列满了的时候存储被阻塞的生产者并且存储在等待队列里面的线程,都符合FIFO的特性。
最后,对于阻塞队列的消费过程,有两种情况。

第一种,就是阻塞队列里面已经包含了很多任务,这个时候启动多个消费者去消费的时候,它的有序性保证是通过加锁来实现的,也就是每个消费者线程去阻塞队列获取任务的时候必须要先获得排他锁。

第二种,如果有多个消费者线程因为阻塞队列中没有任务而阻塞,这个时候这些线程是按照FIFO的顺序存储到condition条件等待队列中的。 当阻塞队列中开始有任务要处理的时候,这些被阻塞的消费者线程会严格按照FIFO的顺序来唤醒,从而保证了消费的顺序型。

线程池是如何实现线程复用

利用的是Worker 这个类,Worker 本身就是一个线程类,里面有一个成员变量task,这个 task 是 Runnable 类型的,Worker 首先会去执行这个 task 的 run 方法,如果这个属性为 null,就去队列里面取出任务然后赋值给 task。而线程池在启动线程执行的时候就是去启动这个 Worker 线程类,而 Worker 里面又会去执行 task 的 run 方法,或者去队列里面去取任务,然后再执行 run 方法。所以线程池实现线程复用,主要是Worker 这个线程类可以不断地改变内部的任务,而达到核心线程反复利用。

阻塞队列

阻塞队列就是一个支持两个附加操作的队列,这两个队列附加的操作支持阻塞的插入和移除方法

  • 支持阻塞的插入方法:当队列满时,会阻塞向队列插入元素的线程,直到队列不满为止
  • 支持阻塞的移除方法:当队列为空时,获取元素的线程会等待队列变为非空

某种意义上这个队列就类似于一种容器,常用于生产者和消费者场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。

常用的阻塞队列有其中:

ArrayBlockingQueue:数组结构组成的有界阻塞队列,按照FIFO的原则进行排序

LinkedBlockingQueue:由链表结构组成的有界阻塞队列,也是按照FIFO原则排序

LinkedTransferQueue:由链表结构组成的无界阻塞队列,相对于 其他阻塞队列,此队列多了 tryTransfer 和 transfer 方法。

    tryTransfer 方法是用来试探生产者传入的元素是否能直接传给消费者。
  • 1

如果没有消费者等待接收元素,则返回 false。和 transfer 方法的区别是 tryTransfer 方法无论消费者是否接收,方法立即返回,而 transfer 方法是必须等到消费者消费了才返回。
对于带有时间限制的 tryTransfer(E e,long timeout,TimeUnit unit)方法,试图把生产者传入的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间 再返回,如果超时还没消费元素,则返回 false,如果在超时时间内消费了元素,则返回 true。

LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列,双向队列指的是可以从队列的两端进行插入和移除元素。

PriorityBlockingQueue:支持优先级排序的无界阻塞队列,采取自然顺序升序排列,也可以自定义类实现compareTo()方法来指定元素的排序规则

DelayQueue:支持延时获取元素的无界阻塞队列。队列元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。

SynchronousQueue:一个不存储元素的阻塞队列,每一个put操作必须等待一个take操作

产生死锁的解决方案

条件:

互斥条件:同一个资源只能一个线程持有

循环等待:我等你,你等我

不可抢占:不可以强制抢占资源

请求与保持:我等待x不释放y,我等待y不释放x

如何解决呢?

互斥不可能被破坏,其他三个可以

文件拷贝的方式

  • 通过inputstream流的方式进行拷贝
  • 通过自带的File.copy进行拷贝
  • 通过java.nio包下的库,使用transfer或transfrom方法实现

集合

Arraylist是什么?和list有什么不同?

Arraylist是实现接口list的实现类,实现类list中的所有方法并加入了自己特有的方法,因此,List接口不能被构造,也就是我们说的不能创建实例对象,但是我们可以像下面那样为List接口创建一个指向自己的对象引用,而ArrayList实现类的实例对象就在这充当了这个指向List接口的对象引用。

例如:List list = new ArrayList(); 这句创建了一个ArrayList实现类的对象后把它上溯到了List接口。此时它就是一个List对象了,它有些ArrayList类具有的,但是List接口没有的属性和方法,它就不能再用了。

list的集合有哪些是线程安全,vetor线程安全

Arraylist不安全在哪:(1)在add的时候,由于进行的是先判断当前数组是否需要进行扩容然后再在下一个位置存入值,那假如有两个线程,一个线程已经判断完不需要扩容后又挂起了,后一个线程也是,但是继续执行注入值的过程,此时容量已经达到最大值需要扩容,但是下一个线程过来注值的时候就会报数组越界的异常(add方法原子性的问题)。

(2)同时在add方式的时候会存在一个原子性问题

elementData[size++] = e;

//不是一个原子操作,是分两步执行的。
elementData[size] = e;
size++;
  • 1
  • 2
  • 3
  • 4
  • 5

这里会导致一个问题:

  • 可能一个线程会覆盖另一个线程的值。
  • 可能会发生并发修改异常

那如何解决这种多线程下的多写多读产生的问题呢?

可以借鉴CopyOnWriteArrayList容器在写时复制,读写分离的思路

写的时候会将原数组进行复制一份,对新数组进行一个注值,再将原引用指向新的容器

CopyOnWriteArrayList的add方法(添加了ReentrantLock的写锁)

public boolean add(E e) {
     //1、先加锁
    final ReentrantLock lock = this.lock;
    lock.lock();
     try {
        Object[] elements = getArray();
         int len = elements.length;
         //2、拷贝数组
         Object[] newElements = Arrays.copyOf(elements, len + 1);
        //3、将元素加入到新数组中
       newElements[len] = e;
        //4、将array引用指向到新数组
        setArray(newElements);
         return true;
    } finally {
       //5、解锁
       lock.unlock();
     }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

避免在多线程并发add的时候,复制多个副本出来把数据搞乱了。

并发的写通过锁来控制,如果存在并发的读,则根据写的状态来进行划分

当写操作未完成,直接读取原数组数据

当写操作完成,引用未指向新的数组,那么直接从新数组中读取数组。

树(Tree):

二叉树:

​ 高度:从某节点出发,到叶子节点为止,最长的简单路径上边的条数

​ 深度:从根节点出发,到某节点边的条数。

平衡二叉树(有三条性质):

​ (1)树的左右高度差不能超过1

​ (2)任何往下递归的左子树和右子树,必须符合第一条性质

​ (3)没有任何节点的空树或者只有根节点的树也是平衡二叉树

二叉查找树:

​ (1)对任意节点,它的左子树节点值都小于自己,它的右子树节点值都大于自己

引出遍历二叉树的方式:

​ (1)前序(根节点、左节点、右节点)、中序(左节点、根节点、右节点)、后序遍历(左节点、右节点、根节点)

​ (2)任何递归子树中,左节点一点在右节点之前先遍历

​ (3)三种遍历顺序指的是根节点在遍历时的位置顺序

TreeMap:

​ 按照key的自然顺序进行排序,改变了map的散乱无序的存储结构。

​ 实现SortedMap接口:key有序不可重复,支持获取头尾key-value元素,或者根据key指定范围获取子集合

​ 其排序依靠Comparator或Comparable来实现key的去重

final int compare(Object k1, Object k2) {
  return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)
      : comparator.compare((K)k1, (K)k2);
}
该方法为treemap的排序方法
解析为:如果comparator不为null,则优先使用比较器comparator的compare方法,反之。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

HashMap:

​ 相比concurrentHashMap的缺点:

​ 死链问题、扩容数据丢失问题

​ 对hashmap进行并发的优化:

​ 使用concurrentHashMap替换hashmap

​ 使用Collections.synchronizedMap方法将hashmap包装成同步集合

​ 对方法进行同步处理

HashMap:(底层是什么样的?1.7和1.8有什么不同?)

底层结构和原理是什么?

​ HashMap是用key-value键值对进行存储的数据结构,在jdk1.7之前使用数组+链表的结构,Entry类进行存储key+value;jdk1.8中使用数组+链表/红黑树,使用node类进行存储key和value;其中node类存储自身的hash、key和value以及下一个节点。

使用链表的原因?

​ 因为数组的长度是有限的,在有限的数组上面使用哈希,那么哈希冲突不可避免的,如果出现了,则会采 取拉链法进行解决,也就是把hash相同的值放在同一条链表上。

那么红黑树有什么用呢?

​ 当hash冲突严重时,会导致链表的长度过于长,因为链表查询数据需要遍历,则会影响查询的效率,当链表的长度大于阈值时,会自动转换为红黑树(不过在转换前会先查看table的长度是否超过了64,如果小于64则会先进行扩容resize)

新的节点插入链表是如何插入的?

​ 在jdk1.7时,进行的是头插法。

​ 在jdk1.8时,为避免循环链表的问题,采用了尾插法进行存储。

扩容resize是如何进行的?

​ 扩容分为两步:

​ 1)扩容 创建一个阈值、容量为原数组2倍(newThr = oldThr << 1)的新数组

​ 2)再hash 将所有的节点重新hash到新数组

为什么要再hash而是不直接进行拷贝?

​ 因为hashmap的索引计算方法是根据数组容量来进行计算的。当容量改变时,就需要重新计算。

1.7的插入方法是如何产生循环链表的?

​ 这个需要看一下视频

hashmap的初始数组长度是多少?为啥?

​ 默认是16,但可以自定义长度但要是2的次幂。选择16也是因为经验问题

如何保证传入的容量一定是二次幂呢?

​ tableSizeFor方法即可了解,通过对容量值不断地右移1、2、4、8、16位来达到取当前值最近的二次幂值的效果

为什么是2的次幂呢(原因有两个)?

​ 一、关系到元素在桶的位置计算问题:

​ 一个元素放到哪个位置,是由 hash值 % 容量 取模运算的余数确定的,但hashmap不用&的方式,而用位运算来替换:(容量 - 1 ) & hash 为保证结果一致,需要确保容量为2的次幂

​ 二、扩容后元素在新数组newCap中的放置问题

​ 要把旧元素放到新数组中的一般实现方法是:遍历所有的node。然后put到新的table中,会涉及新桶位置、处理hash碰撞等处理。有个不可忽视的问题就是哈希碰撞。处理方法是哈希值一样但通过equals比较不同的元素会在同一个桶中形成链表,当链表长度大于等于8时,链表转换为红黑树;扩容时需要重新处理这些元素的哈希碰撞问题,jdk1.8使用了方法处理扩容后元素的放置问题(resize方法)。

​ 1)如果当前桶内只有一个节点(后面没有链表也没有树 e.next == null),则直接做e.hash & (newCap - 1) 的运算,将元素节点放到newCap的相应位置

​ 2)如果桶上是链表,则链表上的所有节点做“ e.hash & oldCap ” 运算(这里没有把容量-1),会得到一个帮助定位的值,该值要么为0,要么是小于capacity的正整数 。这个是规律,得出此规律的原因是和容量caoacity的取值为2的次幂有直接关系。

​ 根据定位值的不同,会将链表一分为二得到两个子链表,两个子链表的头节点直接放到newcap中,

​ 子链表的定位值== 0;则链表在oldCap是什么位置,就将子链表的头节点直接放到newCap的什么位置**

子链表的定位值 小于 容量值capacity的正整数;则将子链表的头节点放到newCap的oldCap + 定位值的位置;**

​ 最后总结:要想hashmap查询快,那元素分布均匀就很重要,保证这个的方式有两种:1、元素的hash值更随机、散列;2、通过hash&oldcap中的oldcap再次增加元素放置位置的随机性

​ 3)如果桶上是红黑树:将红黑树重新放到newCap中的逻辑和将链表放到newCap的逻辑是差不多的。不同之处在于,重新放后会将红黑树拆分成两条由TreeNode组成的子链表。

​ 如果子链表的长度 <= UNTREEIFY_THRESHOLD (即 <= 6)则将由TreeNode组成的子链表转换成Node组成的普通子链表,然后根据定位值将子链表的头节点放到newCap中。

final Node<K,V>[] resize() { //扩容方法
     
//---------------- -------------------------- 1.计算新容量(新桶) newCap 和新阈值 newThr: -------------------------------------------
  
        …… //此处源码见前文“一、3.”
         
//---------------------------------------------------------2.扩容:------------------------------------------------------------------
         
        …… //此处源码见前文“一、3.”
         
//--------------------------------------------- 3.将键值对节点重新放到新的桶数组里:------------------------------------------------
         
        if (oldTab != null) {//容量已经初始化过了:
            for (int j = 0; j < oldCap; ++j) {//一个桶一个桶去遍历,j 用于记录oldCap中当前桶的位置
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {//当前桶上有节点,就赋值给e节点
                    oldTab[j] = null;//把该节点置为null(现在这个桶上什么都没有了)
                    if (e.next == null)//e节点后没有节点了:在新容器上重新计算e节点的放置位置《===== ①桶上只有一个节点
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)//e节点后面是红黑树:先将红黑树拆成2个子链表,再将子链表的头节点放到新容器中《===== ②桶上是红黑树
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {  //遍历链表,并将链表节点按原顺序进行分组《===== ③桶上是链表
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {//“定位值等于0”的为一组:
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {//“定位值不等于0”的为一组:
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
 
              //将分好的子链表放到newCap中:
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;//原链表在oldCap的什么位置,“定位值等于0”的子链表的头节点就放到newCap的什么位置
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead; //“定位值不等于0”的子节点的头节点在newCap的位置 = 原链表在oldCap中的位置 + oldCap
                        }
                    }
                }
            }
        }
        return newTab;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58

HashMap为什么重写 equals 方法的时候还需要重写 hashCode 方法呢?

HashMap是通过hashCode(key)去计算寻找index的,考虑如果多个key哈希得到的index值一样会形成链表,那如何在相同hashCode的链表上找对象呢?

那就是重写equals方法比较两个对象的值。

比较过程如下:hashMap先比较hashCode得出的hash值是否相等,若相等则再用equals比较两个值,若相等则相等。

HashMap不安全在哪?

前面不是说使用了尾插法解决了环形链表的问题,但它仍然是不安全的。例如:

源码中的put方法如果判断到没有hash冲突时直接进行插入

if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
  • 1
  • 2

这个发生的情况就是:当两个线程同时进行put操作时,1线程抢到资源进行查询之后发现没有冲突还没插入就挂起了,2线程同时也发现没有冲突,则进行插入,1线程恢复后则会覆盖2线程的数据。则会导致不安全的现象。

总结:不安全的问题有两个:

  • jdk1.7:由于采用头插法改变了链表上的元素顺序,并发扩容可能导致循环链表的问题
  • jdk1.8:由于put操作没有锁,并发环境下可能发送某个线程插入数据被覆盖的问题

HashMap边安全的方法:

  • 使用Collection类的synchronizedMap方法 包装HashMap,改类的原理是对HashMap的所有操作都加上synchronized锁
  • 使用线程安全的ConcurrentHashMap类代替。该类在jdk1.7采用数组 + 链表存储数据,采用分段式Segment保证线程安全;jdk1.8采用数组+链表/红黑树存储数据,使用CAS+synchronized锁保证线程安全

HashMap和HashTable的区别

1、初始容量不同 前者容量为16,后者容量为11
2、底层数据结构不同 1.7之前相同 jdk1.8之后map加入了红黑树
3、map运行键值为null,table不行
4、线程安全性的问题 后者加入了同步锁(整个hashtable对应一把锁) 但降低了执行效率 单线程下map更占优势 并发情况下考虑CurrentHashMap,加入同步锁 每个数组节点对应一把锁
5、扩容的机制:前者达到负载因子的3/4即容量翻倍,后置翻倍+1
  • 1
  • 2
  • 3
  • 4
  • 5

HashMap的put流程

1、HashMap是懒惰创建数组的,首次使用才会创建数组
2、根据key计算索引
3、如果索引没有占用,则直接创建node占位返回
4、占用了的话
	1、如果是普通的节点,走链表的添加或者更新逻辑,如果链表的长度超过阈值,则树化
	2、走红黑树添加或更新逻辑
5、返回前检查容量是否超过阈值,一旦超过则扩容
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

Put方法:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //判断当前数据是否为空,为空则调用resize进行初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        //得到当前节点在数组中是否不存在值,不存在则创建节点占据当前桶
        //此处易产生并发问题的值覆盖现象
        tab[i] = newNode(hash, key, value, null);
    else {
        //存在hash冲突
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            //比较hash值,如果当前节点的值相等则为同一个对象,直接覆盖
            e = p;
        else if (p instanceof TreeNode)
            //执行红黑树插入逻辑
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
             // 此时为链表,则循环链表进行判断
            for (int binCount = 0; ; ++binCount) {
                //找到链表的尾部,直接插入
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        //链表树化
                        treeifyBin(tab, hash);
                    break;
                }
                //判断链表是否存在相同对象,通过hash值与equals进行判断
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        
        if (e != null) { // existing mapping for key
            // 返回旧的值
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    //判断数组容量+1是否需要扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55

更加详细的:

总结

传入put方法的当前键的hash值是调用本类的hash方法的扰动函数处理之后得hash值

更仔细地可以看下面的问题

①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行初始化;
②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;
③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值
对,否则转向⑤;
⑤.遍历table[i],并记录遍历长度,如果遍历过程中发现key值相同的,则直接覆盖value,没有相同的key则在链表尾部插入结点,插入后判断该链表长度是否大等于8,大等于则考虑树化,如果数组的元素个数小于64,则只是将数组resize,大等于才树化该链表;
⑥.插入成功后,判断数组中的键值对数量size是否超过了阈值threshold,如果超过,进行扩容。

HashMap 为什么在获取 hash 值时要进行位运算

换种问法:能不能直接使用key的hashcode值计算下标存储?

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  • 1
  • 2
  • 3
  • 4

如果使用直接使用hashCode对数组大小取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任何作用的

所以我们的思路就是让 hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动。
(h >>> 16)是无符号右移16位的运算,右边补0,得到 hashCode 的高16位。
(h = key.hashCode()) ^ (h >>> 16) 把 hashCode 和它的高16位进行异或运算,可以使得到的 hash 值更加散列,尽可能减少哈希冲突,提升性能。
而这么来看 hashCode 被散列 (异或) 的是低16位,而 HashMap 数组长度一般不会超过2的16次幂,那么高16位在大多数情况是用不到的,所以只需要拿 key 的 HashCode 和它的低16位做异或即可利用高位的hash值,降低哈希碰撞概率也使数据分布更加均匀。

HashMap的扩容方法

1、计算出扩容后的数组大小和阈值,数组大小是左移一位翻倍确定的,阈值的确定要分清理:

扩容前阈值小于16,

扩容前阈值>=16,且newCap小于数组最大值,则扩容阈值为当前阈值的翻倍

扩容前数组大小已经达到最大值阈值,不扩容,设置扩容条件为int最大值

2、创建新的数组,并将元素一个个移入新数组中

当前节点不存在下一个节点(未发生碰撞),则直接计算(hash & newCap - 1)出新存入的位置

当前节点为红黑树

当前节点为链表:创建了两个链接,低位链表存放与当前下标位置一致,高位链表,存放在当前数组下标位置为:当前数组下班位置 + 扩容之前数组的长度

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QoSwaKnG-1686838790615)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230228095339907.png)]

如何实现:

通过hash&oldCap 得出最高位的一个值是0 还是 1,如 1 111 & 1000 = 1 000 或者 0 111 & 1000 = 0 000

然后分别根据高低位进行存进高低链表中,后面就将高低位链表分别存放到新数组中对应位置

源码解析:

 final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length; // 原数组大小
        int oldThr = threshold;// 原数组阈值
        int newCap, newThr = 0;
        if (oldCap > 0) {
            // 当数组大小超过最大容量,则取int的最大值为阈值
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 获取扩容之后的阈值,原数组容量左移乘2,阈值为原来的两倍(左移一位)
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
     	// 原容量不大于0 并且原阈值大于0,则代表此时数组赋予了阈值但是没有初始化
        else if (oldThr > 0) // initial capacity was placed in threshold
            //将原阈值直接赋值给信容量,由之前的构造可以知道,原阈值存储的大小就是调用构造函数时指定的容量大小
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            //原数组容量不大于0,阈值也不大于0,则赋值默认的容量16和阈值 16*0.75
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            //原容量不大于0 并且原阈值大于0的情况进行处理,对新阈值进行赋值 也就是新容量*负载因子
            float ft = (float)newCap * loadFactor;
            //正式赋值
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        //数组新阈值
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
     	//正式扩容
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        //当前节点没有下一个节点,则没有冲突直接在新数组中使用 新容量newCap - 1 * hash值得到放置位置
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        //红黑树插入逻辑
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        //链表重新排列逻辑
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            //通过hash值 & 旧容量得到一个最高位值
                            if ((e.hash & oldCap) == 0) {
                                //等于0则放在低位
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                //等于1则放在高位
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
     	//返回新数组
        return newTab;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89

ConcurrentHashMap

1.7版本 使用数组Segment+链表的存储结构

其中Segment数组中包含着HashEntry数组,其本身也继承了ReentrantLock类,在有多个线程进行并发访问数组时,就会对Segment进行加锁。

其get方法利用Unsafe直接进行Volatile的可见性Access根据key来获取对于的Segment

put方法。首先利用二次哈希避免哈希冲突,然后调用Unsafe获取对应的Segment,拿到后正式进行put

同时1.7版本的ConcurrentHashMap会存在一个Segment的扩容问题以及分离锁的一个副作用:

不进行同步计算所有Segment的总值会因为并发put导致结果不准确,直接锁定所有的Segment进行计算。同时分离锁也限制了map初始化(不是懒加载)等操作

1.8的修改

采用数组(Entry)+链表(Node)/红黑树的数据结构,同时内部还是存在Segment数组,只是为了兼容序列化。

初始化为懒加载,减去了初始化的开销,Node内对节点值val和nextval添加了volatile进行修饰,

get方法因为可见性已经进行保证,并未进行同步操作

put方法是重点:

sizeCtl是一个非常重要的参数,值对应有几种情况:

等于-1 :表示当前map数组正在初始化

小于 -1: 表示数组正在扩容

0:表示数组还没初始化

正数:如果还没有初始化,代表要初始化的长度,如果已经初始化了,则代表扩容的阈值
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 计算hash值,对已经计算hash值的key进行高16位的hash运算以及判断当前数组内是否存在冲突
//然后
for (Node<K,V>[] tab = table;;) {
         Node<K,V> f; int n, i, fh;
         //当数组为空时
         if (tab == null || (n = tab.length) == 0)
         //懒加载初始化数组
             tab = initTable(); // 初始化数组,通过一个参数sizeCtl表示数组的状态(是否扩容、初始化),在cas修改sizeCtl前后进行判断数组是否为null,此处的源码省略
         else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//数组中未产生冲突 数组长度 - 1 & hash值
             if (casTabAt(tab, i, null,
                          new Node<K,V>(hash, key, value, null))) //cas的方式创建Node节点绑定值
                 break;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

如果出现了哈希冲突,会对当前的Node进行加锁(synchronized):

  • 如果是链表,拿到当前桶位置,首选判断hash值以及key的equals是否相等,相等则修改否则在链表末尾进行添加(尾插法)

  • 如果是红黑树,则执行红黑树添加逻辑

  • 最后会判断是否因为冲突进行添加元素的链表长度是否大于8,大于则需要树化。

  • treeifyBin(tab, i);
    
    • 1

链表树化的阈值为8的原因:泊松分布,统计学算出来的

树化方法treeifyBin:

​ 首先会判断是否需要扩容,也就是总容量没有超过64时需要扩容,通过扩容(tryPresize方法)来提高查询效率。

​ tryPresize(扩容)方法:首先还是确认扩容后的容量,再判断数组是否初始化

private final void tryPresize(int size) {
        //tableSizeFor找到容量为二的次幂的数,并通过右移1位+原容量得到新的容量
        int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
            tableSizeFor(size + (size >>> 1) + 1);
        int sc;
        while ((sc = sizeCtl) >= 0) {
            Node<K,V>[] tab = table; int n;
             //数组未初始化,需要初始化数组,并进行插入
            if (tab == null || (n = tab.length) == 0) {
                n = (sc > c) ? sc : c;
                if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                    try {
                        if (table == tab) {
                            @SuppressWarnings("unchecked")
                            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                            table = nt;
                            sc = n - (n >>> 2);
                        }
                    } finally {
                        sizeCtl = sc;
                    }
                }
            }
            //判断扩容长度是否小于扩容阈值以及数组长度已经大于最大长度时,直接退出
            else if (c <= sc || n >= MAXIMUM_CAPACITY)
            //此处为保证并发操作的安全
                break;
            else if (tab == table) {
            //正式扩容,得到一个扩容戳rs(长度32位的数值,高16位做扩容标识,低16位做扩容线程数)
                int rs = resizeStamp(n);
                //如果sizeCtl的值小于0,说明已经开始扩容,本线程帮助扩容
                if (sc < 0) {
                    Node<K,V>[] nt;

                                        if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                //暂时没有线程扩容,先设置SizeCtl为+2
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    //真正的扩容方法
                    transfer(tab, null);
            }
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

最后

链表升级成红黑树的条件

链表长度大于8时才会考虑升级成红黑树,是有一个条件是 HashMap 的 Node 数组长度大于等于64(不满足则会进行一次扩容替代升级)。

遍历一个集合的方式

for循环、迭代器、递归、stream流的foreach方法、while循环

xx.stream().foreach(p -> System.out.println§);

网络

Restful风格开发

get:请求从服务器获取资源

put:请求从服务器更新资源

post:请求从服务器新建资源

delete:请求从服务器删除特定资源

patch:是对put的一种补充,也是更新资源

Maven

常见的一些标签

-- 定义父类属性
<parent>
     <groupId>包名</groupId>
     <artifactId>项目名</artifactId>
     <version>1.0-SNAPSHOT</version>
     <relativePath/> 
 </parent>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

导入父类依赖时所需要注意添加的注解

</relativePath> 
这个注解的作用是什么呢?
加入此注解能够帮助找到父类parent的pom目录。
如果不写</relativePath>,那么默认就是../pom.xml,会从本地路径获取parent的pom
加了</relativePath> ,那就是从仓库中获取。
查找顺序:relativePath元素中的地址–本地仓库–远程仓
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

父类定义的内所有的依赖会直接被子类所继承,直接定义则不会被子类继承,需要继承时需要特别申明

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
</dependencies>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

版本控制标签

管理父子项目的依赖版本号的方式。在dependencyManagement元素中声明所依赖的jar包等信息,那么子类引入此依赖jar包则无需显式列出版本号。maven会沿着父子层级向上寻找标签,然后使用此标签定义的版本号

进程是操作系统分配资源的单位,线程是操作系统调度的单位,线程之间共享进程资源

框架

Spring

spring的IOC和AOP

ioc容器类似于一个map,在程序进行运行的时候会自动读取xml的各个bean节点以及注册的注解,利用反射创建对应的对象存在容器中

控制反转:将对象与对象之间的依赖关系由主动的关联关系变成由ioc容器被动注入的关系

DI(依赖注入):对需要使用到某对象的类,通过注入(注解注入)到该类进行使用

AOP的原理:动态代理(有接口,则jdk动态代理,无接口,则cglib动态代理)

AOP是一种面向切面的编程方式,通过代理的方式将重复代码抽取出来,横向地切入需要使用的地方,和javaoop传统地继承实现地竖向实现不同,降低了程序的依赖性。

将与业务无关的对多个对象产生影响的模块进行封装成一个可重复使用的模块,这个模块叫切面,凡是可能需要被增强的方法叫做连接点,切入点指我们明确针对的增强的方法。通知又叫增强,也就是需要对切入点做的事情,有前置、后置和环绕。

Spring的IOC实现原理

IOC的机制是基于反射进行实现,通过spring启动时加载xml的配置读取扫描成一个注册表,然后根据注册表将bean的信息也就是全限定类名和id放入进去,然后根据这个注册表去实例化bean,最后将spring的bean放到一级缓存中。

Spring的依赖注入

构造方法注入:也就是初始化的时候被注入。

set方法注入:调用成员变量的set方法注入,但是对象在初始化时还没注入

接口注入:实现指定接口,以及实现接口的某个函数

Spring为什么使用三级缓存解决呢?

通过上面的解释我们大概明白了循环依赖的解决方案,明明采用二级缓存就能够解决循环依赖,但是Spring为什么使用了三级缓存呢?

我们先来了解一下Spring每个缓存的名字及其作用:

「singletonObjects」:单例池,我们去存放已经创建完成,并且属性也注入完毕的对象!

「earlySingletonObjects」:提前暴露的对象,存放已经创建完成,但是没有注入好的对象!

「singletonFactories」:提前暴露的对象,存放已经创建完成,但是还没有注入好的对象的工厂对象!通过这个工厂可以返回这个对象!

为什么明明使用二级缓存就能够解决的问题,spring偏偏要使用三级缓存去解决呢?

上面的设计方案二级缓存是能够很好的解决循环依赖所带来的问题,但是请大家思考一个问题:

我们创建的bean所依赖的对象是一个需要被Aop代理的对象,怎么办?遇到这种情况,我们肯定不能够直接把创建完成的对象放到缓存中去的!为什么,因为我们期望的注入的是一个被代理后的对象,而不是一个原始对象! 所以这里并不能够直接将一个原始对象放置到缓存中,我们可以直接进行判断,如果需要Aop的话进行代理之后放入缓存!

但是Aop的操作是在哪里做的?是在Spring声明周期的最后一步来做的!如果我们进行判断创建的话,Aop的代理逻辑就会在创建实例的时候就进行Aop的代理了,这明显是不符合Spring对于Bean生命周期的定义的! 所以,Spring重新定义了一个缓存【「singletonFactories」】用来存放一个Bean的工厂对象,创建的对象之后,填充属性之前会把创建好的对象放置到【「singletonFactories」】缓存中去,并不进行实例化,只有在发生了循环引用,或者有对象依赖他的时候,才会调用工厂方法返回一个代理对象,从而保证了Spring对于Bean生命周期的定义!

以下是spring关于三级缓存的定义:

主要原理是利用三级缓存机制:

Map<String, Object> singletonObjects: 一级缓存,也就是我们平常理解的单例池,存放已经完整经历了完整生命周期的bean对象。

Map<String, Object> earlySingletonObjects: 二级缓存,存储早期暴露出来的bean对象,bean的生命周期未结束。(属性还未填充完)

Map<String,ObjectFactory<?> > singletonFactories: 三级缓存,存储生成bean的工厂。

注意:只有单例bean会通过三级缓存提前暴露出来解决循环依赖的问题,而非单例的bean,每次从容器中获取都是一个新的对象,都会重新创建,所以非单例bean是没有缓存的,不会将期放到三级缓存中。

spring解决循环依赖的过程

循环依赖:也就是一个(自己依赖自己)或多个bean的实现之间存在一个直接或者间接的依赖关系,而导致一个循环依赖。

三级缓存解决:分别来存放不同的bean。

一级缓存存放完全初始化好的bean,可以直接被使用。

二级缓存中存放的是原始bean对象,也就是还没初始化的对象

三级缓存中存放的是bean工厂,它回去生产原始的bean并放入二级缓存

BeanA和BeanB的循环依赖进行依赖注入的一个过程

也就是beanA、BeanB先分别在第三级缓存工厂中创建对象,然后由beanB去第三级缓存中查找BeanA,找到了之后,BeanB放入二级缓存并移除三级缓存,将不完整的beanA放入BeanB,并放入一级缓存中,然后BeanA就会注入BeanB的实例去一级缓存中查询,查找到了就完成BeanA的初始化以及放入一级缓存

总结:

A在创建过程中需要B,于是A先将自己放到三级缓存里面,去实例化B

B实例化的时候发现需要A,于是B先查一级缓存,没有再查二级缓存,还是没有,再查三级缓存,找到了A;然后把三级缓存里面的这个A放到二级缓存里面,并删除三级缓存里面的A

B顺利初始化完毕,将自己放到一级缓存里面(此时B里面的A依然是创建中状态);然后回来接着创建A,此时B已经创建结束,直接从一级缓存里面拿到B,然后完成创建,并将A放入到一级缓存中

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ShCx42oG-1686838790626)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230213093144817.png)]

这个解决的是单实例存在循环引用的问题,除此之外,需要人工进行干预,代码实现的话可以@DependOn注解导致的依赖,打破循环依赖和@Lazy解决构造器注入导致的依赖。单例的Setter注入导致的循环依赖,也可以使用@Lazy或@DependsOn注解注明加载先后关系

springBean的bean的作用域

Spring容器中Bean包含五种作用域(scope的值):

singleton:单例bean,配置的bean自始至终只会有一个实例

prototype:多例bean,每次get或者从IOC容器种获取bean的时候会创建一个新的实例对象

但是在Spring框架下的Web应用种存在一个会话维度。

request:每次http请求会创建一个bean,并且作用域在http请求内

session:每次http请求会创建一个bean,但是在session失效前有效

globalSession:一般少用

Spring的生命周期(?)

首先会调用createBean来完成实例化bean并放入BeanDefintion集合中

第二步给属性赋值:会从BeanDefintion中查找对应的bean,通过bean提供的一些设置属性的方法进行赋值

第三步处理aware接口,通过aware接口拿到一部分spring的信息,比如说beanNameAware

第四步是判断有没有实现beanPostprocess接口,这个接口有个beforeInit方法,可以在初始化bean之前执行

第五步判断有没有实现initalizingbean接口,第六步判断有没有声明init-method注解。

Spring自动装配的方法

contructor:构造器注入,类似于bytype

byType:根据类型查询进行注入

byName:根据参数名进行匹配

no-默认方式不自动装配,根据ref属性进行装配

相关注解

@PathVariable:获取请求路径除了参数的资源
@RequestParam:获取请求中的参数

@ResponseBody注解

注解的作用是将java对象转换成json对象。在controller的方法中将对象数据直接写入流中,返回结果封装在Http reponse body中,一般是异步获取数据时使用

该注解作用在方法上

Spring对应数据库的隔离级别

img

脏读:a正在写的同时b在读; 不可重复读:a读,b写,a再读导致数据不一致; 幻读:b在写时a在读,a以为没有。

数据库的四种隔离级别:未提交读、已提交读、可重复读、可序列化

Spring的事务注解@Transactional运行原理

@Transactional的工作机制是基于AOP实现的,如果目标对象实现了某个接口,则采用jdk动态代理,否则采用CGLB代理。

强制使用CGLib代理的方式:@EnableAspectJAutoProxy(proxyTargetClass = true)

当一个类或者方法被标记@Transactional注解时,spring容器在启动为其创建一个容器,在调用该方法的public方法时,实际调用的是TransactionInterceptor的invoke()方法,这个方法的作用是在执行方法之前开启事务,方法执行过程中遇异常回滚事务,调用方法之后提交事务。

@Transactional事务注解失效

同一个类的其他未被@Transactional注解标注的方法调用有@Transactional注解标注的方法时,@Transactional标注的方法事务会失效

原因:由于AOP代理造成的,因为只有当@Transactional注解的方法在类外调用的时候,spring的事务管理才生效

解决办法:避免同一个类调用或者使用AspectJ取代Spring AOP代理

@Transactional注解使用总结

1)@Transactional只有作用在public方法上事务才会生效,不推荐在接口中使用

2)避免同一个类中自调用@Transactional标注的方法

3)正确设置@Transactional的rollbackFor(事务的回滚)和propagation(事务的传递)属性,否则事务可能回滚失败

4)@Transactional注解的方法所在的类必须被spring容器管理

5)底层数据库必须支持事务机制

spring中的隔离级别

TransactionDefinition 接口中定义了五个表示隔离级别的常量:

public interface TransactionDefinition {
    ......
    int ISOLATION_DEFAULT = -1;
    int ISOLATION_READ_UNCOMMITTED = 1;
    int ISOLATION_READ_COMMITTED = 2;
    int ISOLATION_REPEATABLE_READ = 4;
    int ISOLATION_SERIALIZABLE = 8;
    ......
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

同时定义了一个枚举类,方便取隔离级别:Isolation

public enum Isolation {

  DEFAULT(TransactionDefinition.ISOLATION_DEFAULT),

  READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED),

  READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED),

  REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ),

  SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE);

  private final int value;

  Isolation(int value) {
    this.value = value;
  }

  public int value() {
    return this.value;
  }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

1)ISOLATION_DEFAULT:使用后端默认的隔离级别。(相对于mysql的四大隔离级别加入了默认判断)mysql默认采用可重复读,Oracle采用读已提交读(可能导致幻读不可重复读,阻止了脏读)

2)ISOLATION_READ_UNCOMMITTED 未提交读

3)ISOLATION_READ_COMMITTED 已提交读

4)ISOLATION_SERIALIZABLE 不可重复读

5)ISOLATION_SERIALIZABLE 序列化,所有的事务依次执行,事物之间不可能产生干扰

Spring事务的类型

1、编程式事务(通过TransactionTemplate或者TransactionManager手动管理事务,可以通过spring容器注入)

@Autowired
private TransactionTemplate transactionTemplate;
public void testTransaction() {

        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {

                try {

                    // ....  业务代码
                } catch (Exception e){
                    //回滚
                    transactionStatus.setRollbackOnly();
                }

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

2、声明式事务(@Transactional注解)

事物的传播行为

TransactionDefinition定义中包括了传播行为的常量

public interface TransactionDefinition {
    int PROPAGATION_REQUIRED = 0;
    int PROPAGATION_SUPPORTS = 1;
    int PROPAGATION_MANDATORY = 2;
    int PROPAGATION_REQUIRES_NEW = 3;
    int PROPAGATION_NOT_SUPPORTED = 4;
    int PROPAGATION_NEVER = 5;
    int PROPAGATION_NESTED = 6;
    ......
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

同时spring为了方便使用,定义了一个枚举类来获取级别Propagation

public enum Propagation {
    REQUIRED(0),
    SUPPORTS(1),
    MANDATORY(2),
    REQUIRES_NEW(3),
    NOT_SUPPORTED(4),
    NEVER(5),
    NESTED(6);

    private final int value;

    private Propagation(int value) {
        this.value = value;
    }

    public int value() {
        return this.value;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

1)PROPAGATION_REQUIRED

默认的传播行为,如果当前没有事务,则新建一个事务,且事务之间不会相互影响

2)PROPAGATION_SUPPORTS

支持当前事务,如果当前没有事务,就以非事务方式执行。

3)PROPAGATION_MANDATORY

支持当前事务,如果当前没有事务,就抛出异常。

4)PROPAGATION_REQUIRES_NEW

新建事务,如果当前存在事务,把当前事务挂起。

5)PROPAGATION_NOT_SUPPORTED

以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

6) PROPAGATION_NEVER

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

7)PROPAGATION_NESTED

如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。

总结,两个是基于支持当前事务和非事务方式执行,如果存在当前事务,则会抛出异常、或挂起当前事务,特例是嵌套执行

一个类没有被@Configuration标注,而直接用@Bean注解,该类能被加载成bean吗?

生效的前提是该类能被spring扫描到,如果手动配置扫描到该类的话,@Bean是能够生效的,但是生成的Bean是不遵循spring的作用域的。

@Configuration的作用就是使类被spring的作用域所限制。

事务失效的场景及解决办法

1、抛出检查(check)异常导致事务不能正确回滚(也就是编译器编译不能通过,需要手动进行修改代码的异常)

使用@Transaction的rollbackFor属性

2、业务方法内自己try、catch解决了异常的问题,没有在catch中抛出异常,事务不会回滚

异常原样抛出

手动设置TransactionStatus.setRollbackOnly

catch

3、aop切面顺序导致事务不能正确回滚

事务切面的优先级最低,但如果自定义的切面优先级跟他一样,则自定义切面在内层,内部进行try catch导致异常不能抛出,则和第二个一样,进行手动设置

调整切面顺序 @Order注解的orderd的属性 - 1

4、非public方法导致事务失效

一、修改源代码的方法修饰符为public

二、使用动态代理AspectJ(如果是static、final的,事务同样不能生效)

5、父子容器导致事务失效(不重要)

6、调用本类的方法导致传播行为失效(本类的非事务方法调用本类的事务方法)

img

由于Transaction注解的实现原理是AOP也就是动态代理,自己调用自己的过程,并不会产生动态代理去为Transaction注解设置配置的参数,这也就意味着事务失效

7、没有被spring容器所加载成Bean失效

加上能够被容器所加载Bean的注解

8、传播类型不支持事务

事务的传播机制有一种不以事务运行,当前事务挂起,则会导致事务失效

9、数据源没有配置事务管理器

10、多线程调用导致事务不能回顾,事务失效

SpringMVC

拦截器和过滤器的区别

相同:两者都是用来处理请求,只是出现的时机不同

拦截器是springmvc提供的一个组件,拦截的是到controller层的请求

过滤器是tomcat的servlet提供的组件,用来过滤到web资源的请求。

拦截器的执行顺序:
1.在请求到来的时候,拦截器会拦截,执行preHandle方法。如果该方法的返回值为true,就继续往下执行,否则,就结束执行,往下就不再执行任何方法。该方法在目标方法执行之前执行。可以考虑做权限、日志或者事务。
2.在preHandle的返回值为true的情况下,就继续执行请求的jsp页面或者controller。
3.执行完请求后,执行postHandle方法。该方法在目标方法执行之后执行,但在渲染视图之前执行,可以对请求域中的属性或视图做出修改。
4.最后执行afterComplete方法,做一些资源清理的工作。

SpringMVC模式是什么?

它简化传统的servlet+jsp模式的web开发,对MVC设计模式也进行了相应的扩展和升级:

1、对于控制层,springmvc将它分成了DispatcherServlet前端控制器和Controller

2、对于模型层,它被拆分成了业务层service和数据访问层

3、对于视图层,它可以支持多种不同的视图

spirngmvc的具体工作流程:

由前端控制器接收到请求后,调用handlemapping处理器映射器去查找处理器,handlemapping会找到对应的处理器后,生成处理器对象以及处理器拦截器返回给前端控制器,前端控制器会再找到处理器适配器,由适配器对象调用自定义的控制层。

img

SpringBoot

Springboot的自动装配原理

启动类的@SpringBootApplication注解由@SpringBootConfiguration,@EnableAutoConfiguration,@ComponentScan三个注解组成,三个注解共同完成自动装配;

@SpringBootConfiguration 标记启动类为配置类
@ComponentScan 实现启动时扫描启动类所在的包路径以及子包下所有标记为bean的类的IOC容器注册为bean
@EnableAutoConfiguration通过 @Import 注解导入 AutoConfigurationImportSelector类,然后通过AutoConfigurationImportSelector 类的 selectImports 方法去读取需要被自动装配的组件依赖下的spring.factories文件配置的组件的类全名,并按照一定的规则过滤掉不符合要求的组件的类全名,将剩余读取到的各个组件的类全名集合返回给IOC容器并将这些组件注册为bean

更加充分的回答:该项配置涉及三个核心部分,引入starter,装配依赖的时候,需要有一个@configuraction注解标志配置类,同时这个配置类中使用@Bean注解注明需要被ioc容器所加载的bean对象,然后这个配置类的类全限定类名会存放在一个文件中,这个文件的存放的位置根据springboot的约定大于配置的设计原理,会存放在metinfo下的spring.factories。然后springboot会利用@import的importSelector接口去找到这个文件并拿到类全限定类名路径,并交由springboot去加载。

springboot读取yml配置文件的方法

1)@value注解直接读取

2)@Autoawire 注解注入Environment对象,这个对象读取yml所有的配置文件,通过对象的getproperty方法进行读取

3)@ConfigurationProperties注解,将配置读取到包装类中

数据库

MySQL

一条查询SQL语句是如何执行的

1、mysql客户端与服务端间建立连接,客户端发送一条查询给服务器

2、服务器先根据语句检查查询缓存,key-value存储,key是查询的语句,value是查询的结果,如果命中了缓存则立刻返回存储在缓存中的结果,否则进入下一个阶段

3、服务端进行SQL解析(词法、语法分析)、预处理,生成合法的解析树

4、再由优化器(决定使用哪个索引,或者关联表时选择各个表的连接顺序)生成对应的执行计划

5、MYSQL根据优化器生成的执行计划,调用相应的存储引擎的API来执行,并将执行结果进行返回给客户端。

详细过程:(逻辑架构如下,参考资料《高性能mysql》)

img

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cSTsR7xa-1686838790631)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230224210319263.png)]

一条SQL更新语句是如何执行的?

与查询语句执行的流程一样,但加入了两个日志模块,也就是redo log(重做日志)、binlog(归档日志)

其中redo log (固定4个文件,总共4G存储空间,innoDB特有的物理日志,也就是记录了在某数据页上修改了什么,循环写入,当空间不足时,会将记录先更新到数据文件在擦除记录)充当的角色也是缓存,更新一条记录会现在redo log中记录,再去数据库中更新。

binlog(Server层的归档日志,逻辑日志,给某数据页的某记录 加减 ,追加写入日志文档,也就是当前文件写到一定大小则会切换到下一个,并不覆盖一起的日志)

具体更新过程

  1. 执行器先找引擎取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
  2. 执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
  3. 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
  4. 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
  5. 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。

其中最后redo log执行的时候是分为prepare和commit“两阶段提交”

redolog起到的作用是mysql异常重启,binlog是用来备份

怎样让数据库恢复到半个月内任意一秒的状态?

当然是去查找binlog中存放的所有逻辑操作,并且备份系统一定会保存最近半个月的所有binlog,系统也会定期正库备份

当需要恢复到指定的某一秒时,比如某天下午两点发现中午十二点有一次误删表,需要找回数据,那你可以这么做:

  • 首先,找到最近的一次全量备份,如果你运气好,可能就是昨天晚上的一个备份,从这个备份恢复到临时库;
  • 然后,从备份的时间点开始,将备份的 binlog 依次取出来,重放到中午误删表之前的那个时刻。

如果不用两阶段提交,要么就是先写完 redo log 再写 binlog,或者采用反过来的顺序。

我们看看这两种方式会有什么问题。

仍然用前面的 update 语句来做例子。假设当前 ID=2 的行,字段 c 的值是 0,再假设执行 update 语句过程中在写完第一个日志后,第二个日志还没有写完期间发生了 crash,会出现什么情况呢?

  1. 先写 redo log 后写 binlog。假设在 redo log 写完,binlog 还没有写完的时候,MySQL 进程异常重启。由于我们前面说过的,redo log 写完之后,系统即使崩溃,仍然能够把数据恢复回来,所以恢复后这一行 c 的值是 1。 但是由于 binlog 没写完就 crash 了,这时候 binlog 里面就没有记录这个语句。因此,之后备份日志的时候,存起来的 binlog 里面就没有这条语句。 然后你会发现,如果需要用这个 binlog 来恢复临时库的话,由于这个语句的 binlog 丢失,这个临时库就会少了这一次更新,恢复出来的这一行 c 的值就是 0,与原库的值不同。
  2. 先写 binlog 后写 redo log。如果在 binlog 写完之后 crash,由于 redo log 还没写,崩溃恢复以后这个事务无效,所以这一行 c 的值是 0。但是 binlog 里面已经记录了“把 c 从 0 改成 1”这个日志。所以,在之后用 binlog 来恢复的时候就多了一个事务出来,恢复出来的这一行 c 的值就是 1,与原库的值不同。

那么,如果不用两阶段提交,那么就会出现不一致的问题

那么对于此种情况,在主从数据库的从数据库就是利用全量备份加上binlog实现的

即使数据库发生重启也不怕记录丢失,这个过程叫crash-safe

小结:

redo log 用于保证 crash-safe 能力。innodb_flush_log_at_trx_commit 这个参数设置成 1 的时候,表示每次事务的 redo log 都直接持久化到磁盘。这个参数我建议你设置成 1,这样可以保证 MySQL 异常重启之后数据不丢失。

sync_binlog 这个参数设置成 1 的时候,表示每次事务的 binlog 都持久化到磁盘。这个参数我也建议你设置成 1,这样可以保证 MySQL 异常重启之后 binlog 不丢失。

定期全量备份的周期“取决于系统重要性,有的是一天一备,有的是一周一备”。那么在什么场景下,一天一备会比一周一备更有优势呢?或者说,它影响了这个数据库系统的哪个指标?

  1. 数据是否重要?如果重要,那么最好还是一天一备份。
  2. 如果数据量产生的很大,那么还是建议一天一备份,毕竟备份是全量式的,如果一周一备份,但是备份过程就要持续一周。

那么备份的时候,会影响数据库哪些指标呢?

RTO(恢复目标时间)

MySQL的逻辑架构可以分为Server层和存储引擎层:

​ 1)大多数MysSQL的核心服务都在Server层,例如:查询解析、分析、优化、缓存以及内置函数,所有跨存储引擎的功能全在这一层实现:存储过程、触发器、视图等。

​ 2)第二层是存储引擎。其负责MySQL中数据的存储和提取,响应上层服务器的请求。每个存储引擎都有其优势和劣势,不同引擎之间无法相互通信。

​ 服务器通过API与存储引擎进行通信。这些接口屏蔽了不同存储引擎之间的差异。

连接器

​ MySQL为客户端提供MySQL服务器的连接。总共做了两件事:一、MySQL连接,二、权限验证(验证用户是否具有某个查询的权限,之后的权限判断逻辑都依赖与此连接所读到的权限,也就意味着不切换连接进行修改权限会不生效)

查询缓存

官方给的介绍是:查询缓存存储了select语句的文本以及响应给客户端的相应结果,这样,如果收到相同的查询语句,则会从缓存中取结果。查询缓存在session之间共享,因此可以发送一个客户端生成的结果集以响应另一个客户端发出的相同查询。

当期查询命中查询缓存时,会在返回结果之前MySQL会检查一次用户权限。(涉及到缓存就得考虑缓存一致性问题了,但缓存不会返回陈旧数据:当表被修改时,查询缓存的任何相关条目会被flushed 清空而不是刷新)

MySql的隔离级别和隔离性

读未提交读:另一个事务能够看到一个事务还没提交的变更

读提交读:一个事务提交后,变更才能被其他事务看到

可重复读:一个事务在执行(Runing)看到的数据和这个事务启动时看到的数据是一致的。当然未提交的变更是不可见的

串行化:只允许一个事务执行。写会加写锁,读会加读锁

你可能会问那什么时候需要“可重复读”的场景呢?我们来看一个数据校对逻辑的案例。

假设你在管理一个个人银行账户表。一个表存了每个月月底的余额,一个表存了账单明细。这时候你要做数据校对,也就是判断上个月的余额和当前余额的差额,是否与本月的账单明细一致。你一定希望在校对过程中,即使有用户发生了一笔新的交易,也不影响你的校对结果。

这时候使用“可重复读”隔离级别就很方便。事务启动时的视图可以认为是静态的,不受其他事务更新的影响。

怎么产生长事务?如何避免长事务?

在连接数据库之后,将commit设置为false,当此连接为长连接时,就会产生长事务。默认都是28800 秒,8 小时。

如何避免?在系统层面,可以使用set autocommit = 1,并用begin显式启动事务,如果执行commit则提交事务。如果需要频繁使用事务,则可以通过commit work and chain来提交事务后并自动启动下一个事务。

业务层面,首先是确认使用了set autocommit = 0,这个确认工作可以在测试环境中开展,把mysql的general_log开起来,通过日志来确认长事务。

确认不存在不必要的只读事务,也就是把好几个select语句放进事务中,这种只读事务可以去掉

根据业务本身的预估,通过set_MAX_EXECUTION_TIME命令来控制每个语句执行的最长时间,避免单个语句意外执行太长时间。

数据库端看:可以监控 information_schema.Innodb_trx,也就是长事务阈值,超过就报警或者kill

Percona的pt-kill可以支持

按阶段输出所有的general_log,分析日志行为提前发现问题;

如何避免回表查询?

可以使用联合索引(覆盖索引),当当前的非主键树找到目标的值时,则会自动返回相应的值,不会回表查询

覆盖索引也就是非聚集索引,是联合索引的一种运用,在联合索引中已经存在需要查询的字段,就不需要找到主键id,再根据主键回表查询聚集索引所带来的开销

例如:

select * from T where k between 3 and 5
select ID from T where k between 3 and 5  -- 优化后只查询了主键ID,而主键ID已经在k索引树上了。要知道我们首先找的就是非聚集索引树,当找到要求的元素后就不会去聚集索引找了。
  • 1
  • 2

最左前缀原则

联合索引所遵循的,其索引存放的规律是以左边元素进行分组排列,所以建立索引需要考虑安排字段顺序。

不满足最左前缀的查询会怎么样呢

在Mysql5.6之前,只能从第一个匹配的元素开始一个个灰白哦找到主键索引上进行比对字段值

在Mysql4.6引入了索引下推优化,可以在遍历索引过程中,对索引包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。

是这两个过程的执行流程图。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GCG7sOLK-1686838790632)(https://learn.lianglianglee.com/%E4%B8%93%E6%A0%8F/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2/assets/b32aa8b1f75611e0759e52f5915539ac.jpg)]

图 3 无索引下推执行流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oe2a0Fpi-1686838790633)(https://learn.lianglianglee.com/%E4%B8%93%E6%A0%8F/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2/assets/76e385f3df5a694cc4238c7b65acfe1b.jpg)]

图 4 索引下推执行流程

图 4 跟图 3 的区别是,InnoDB 在 (name,age) 索引内部就判断了 age 是否等于 10,对于不等于 10 的记录,直接判断并跳过。在我们的这个例子中,只需要对 ID4、ID5 这两条记录回表取数据判断,就只需要回表 2 次。

总结:覆盖索引、前缀索引、索引下推。这三个就是为了尽量避免回表的策略

全局锁

对整个数据库实例加锁,加全局读锁的方法:Flush tables with read lock,整个数据库处于只读状态

使用场景:对数据库进行全库逻辑备份,也就是存进binlog中,但是备份不加锁会怎么样呢?

不加锁可能导致逻辑备份和物理备份数据不一致

比较一下set global readonly=true和FTWRL 方式对备份不同的处理:

  • 一是,在有些系统中,readonly 的值会被用来做其他逻辑,比如用来判断一个库是主库还是备库。因此,修改 global 变量的方式影响面更大,我不建议你使用。
  • 二是,在异常处理机制上有差异。如果执行 FTWRL 命令之后由于客户端发生异常断开,那么 MySQL 会自动释放这个全局锁,整个库回到可以正常更新的状态。而将整个库设置为 readonly 之后,如果客户端发生异常,则数据库就会一直保持 readonly 状态,这样会导致整个库长时间处于不可写状态,风险较高。

表级锁

表锁、元数据锁(MDL)。

表锁的语法是lock tables … read/write,与FTWRL相类似,可以用unlock tables主动释放锁,也可以在客户端断开的时候自动释放。需要注意,lock tables 语法除了会限制别的线程的读写外,也限定了本线程接下来的操作对象。

举个例子, 如果在某个线程 A 中执行 lock tables t1 read, t2 write; 这个语句,则其他线程写 t1、读写 t2 的语句都会被阻塞。同时,线程 A 在执行 unlock tables 之前,也只能执行读 t1、读写 t2 的操作。连写 t1 都不允许,自然也不能访问其他表。

表锁一般是在数据库引擎不支持行锁的时候才会被用到的一般是在数据库引擎不支持行锁的时候才会被用到的。MySQL 5.5 版本中引入了 MDL,当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁。

  • 读锁之间不互斥,因此你可以有多个线程同时对一张表增删改查。
  • 读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性。因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。

如何安全地给小表加字段?

首先我们要解决长事务,事务不提交,就会一直占着 MDL 锁。在 MySQL 的 information_schema 库的 innodb_trx 表中,你可以查到当前执行中的事务。如果你要做 DDL 变更的表刚好有长事务在执行,要考虑先暂停 DDL,或者 kill 掉这个长事务。

但考虑一下这个场景。如果你要变更的表是一个热点表,虽然数据量不大,但是上面的请求很频繁,而你不得不加个字段,你该怎么做呢?

这时候 kill 可能未必管用,因为新的请****************************求马上就来了。比较理想的机制是,在 alter table 语句里面设定等待时间,如果在这个指定的等待时间里面能够拿到 MDL 写锁最好,拿不到也不要阻塞后面的业务语句,先放弃。之后开发人员或者 DBA 再通过重试命令重复这个过程。

行锁

两阶段锁协议:行锁在事务需要的时候加上,在事务结束的时候释放,

产生死锁的条件:互斥、循环等待、请求与保持、不剥夺条件

死锁解决:

  • 直接进入等待,直接超时,方案并不会,不可控的超时时间的设置
  • 发起死锁检测,发现死锁后主动回滚死锁链中的某一个事务,让其他事务得以继续执行。innodb_deadlock_detect 设置为on,表示开启检测

如何高效删除一个表中的前10000条数据?

  • 直接删除10000条
  • 在一个连接中循环执行20次删除 500行的语句
  • 在20个连接中执行删除500行语句

第一种方式(即:直接执行 delete from T limit 10000)里面,单个语句占用时间长,锁的时间也比较长;而且大事务还会导致主从延迟。

第三种方式(即:在 20 个连接中同时执行 delete from T limit 500),会人为造成锁冲突。

什么时候建普通索引、什么时候建唯一索引呢?

例如身份证号,就不能考虑主键索引,只能考虑普通、唯一索引了

因为业务已经能够支持插入的身份证号是唯一的,所以从插入和更新的性能进行分析

假设,执行查询的语句是 select id from T where k=5。这个查询语句在索引树上查找的过程,先是通过 B+ 树从树根开始,按层搜索到叶子节点,也就是图中右下角的这个数据页,然后可以认为数据页内部通过二分法来定位记录。

  • 对于普通索引来说,查找到满足条件的第一个记录 (5,500) 后,需要查找下一个记录,直到碰到第一个不满足 k=5 条件的记录。
  • 对于唯一索引来说,由于索引定义了唯一性,查找到第一个满足条件的记录后,就会停止继续检索。

对于查询所带来的性能损耗,可以说是微乎其微的。

但是在更新所带来的损耗可能会有很多影响,一般还是选择普通索引。

当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InooDB 会将这些更新操作缓存在 change buffer 中,这样就不需要从磁盘中读入这个数据页了。在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行 change buffer 中与这个页有关的操作。通过这种方式就能保证这个数据逻辑的正确性。

需要说明的是,虽然名字叫作 change buffer,实际上它是可以持久化的数据。也就是说,change buffer 在内存中有拷贝,也会被写入到磁盘上。

对于唯一索引来说,所有的更新操作都要先判断这个操作是否违反唯一性约束。比如,要插入 (4,400) 这个记录,就要先判断现在表中是否已经存在 k=4 的记录,而这必须要将数据页读入内存才能判断。如果都已经读入到内存了,那直接更新内存会更快,就没必要使用 change buffer 了。

因此,唯一索引的更新就不能使用 change buffer,实际上也只有普通索引可以使用。

如果要在这张表中插入一个新记录 (4,400) 的话,InnoDB 的处理流程是怎样的。

第一种情况是,这个记录要更新的目标页在内存中。这时,InnoDB 的处理流程如下:

  • 对于唯一索引来说,找到 3 和 5 之间的位置,判断到没有冲突,插入这个值,语句执行结束;
  • 对于普通索引来说,找到 3 和 5 之间的位置,插入这个值,语句执行结束。

这样看来,普通索引和唯一索引对更新语句性能影响的差别,只是一个判断,只会耗费微小的 CPU 时间。

但,这不是我们关注的重点。

第二种情况是,这个记录要更新的目标页不在内存中。这时,InnoDB 的处理流程如下:

  • 对于唯一索引来说,需要将数据页读入内存,判断到没有冲突,插入这个值,语句执行结束;
  • 对于普通索引来说,则是将更新记录在 change buffer,语句执行就结束了。

将数据从磁盘读入内存涉及随机 IO 的访问,是数据库里面成本最高的操作之一。change buffer 因为减少了随机磁盘访问,所以对更新性能的提升是会很明显的。

Order by 是如何执行的

排序有两种算法:全字段排序和rowid排序

自增ID用完了怎么办?

  1. 表的自增 id 达到上限后,再申请时它的值就不会改变,进而导致继续插入数据时报主键冲突的错误。
  2. row_id 达到上限后,则会归 0 再重新递增,如果出现相同的 row_id,后写的数据会覆盖之前的数据。
  3. Xid 只需要不在同一个 binlog 文件中出现重复值即可。虽然理论上会出现重复值,但是概率极小,可以忽略不计。
  4. InnoDB 的 max_trx_id 递增值每次 MySQL 重启都会被保存起来,所以我们文章中提到的脏读的例子就是一个必现的 bug,好在留给我们的时间还很充裕。
  5. thread_id 是我们使用中最常见的,而且也是处理得最好的一个自增 id 逻辑了。

MySql的引擎有哪些?

MySQL的体系结构:

**1、InnoDB 存储引擎。**InnoDB是一种兼顾高可靠性和高性能的通用存储引擎,在 MySQL 5.5 之后,InnoDB是默认的 MySQL 存储引擎。

特点:

(1) 支持自动增长列AUTO_INCREMENT。自动增长列的值不能为空,且值必须唯一。MySQL中规定自增列必须为主键。

(2) 支持外键,保证数据的完整性和正确性。外键所在表为子表,外键所依赖的表为父表。父表中被子表外键关联的字段必须为主键。

(3) DML操作遵循ACID(原子性、一致性、隔离性、持久性)模型,支持事务。

(4) 行级锁 ,提高并发访问性能。

行级锁和表级锁的区别

1、 表级锁,一般是指表结构共享锁,是不可对该表执行DDL操作,但对DML操作都不限制。 行级锁之前需要先加表结构共享锁。锁定整个表,限制对于其他用户对表的访问。

2、行级锁,一般是指排它锁,即被锁定行不可进行修改,删除,只可以被其他会话select。行级锁之前需要先加表结构共享锁。对目前被修改的行进行锁定,其它用户可访问被锁定的行以外的行。
  • 1
  • 2
  • 3

2、 MyISAM 存储引擎

特点:

(1) 不支持事务,不支持外键
(2) 支持表锁,不支持行锁
(3) 占用空间小,访问速度快

(4)表结构也是B+树

(5)存在非聚集索引

适用于系统读多写少,不需要事务的操作,插入多更新少,读取频繁,适用于频繁的统计

3 、Memory 存储引擎

Memory引擎的表数据时存储在内存中的,由于受到硬件问题、或断电问题的影响,只能将这些表作为临时表或缓存使用。

特点:

(1) 内存存放
(2) hash索引(默认)

第二三范式的区别

第二范式要求所有的非主键与主键进行直接的关联,不能与主键的某一部分相关。即每一张表只能存一种数据
第三范式在二范式基础上要求所有非主键列之间不存在直接或间接的依赖关系
  • 1
  • 2

B数的存储结构

以一棵最大度数(max-degree,指一个节点的子节点个数)为5(5阶)的 b-tree 为例(每个节点最多存储4个key,5个指针)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YwxPNjQm-1686838790633)(https://dhc.pythonanywhere.com/media/editor/B-Tree结构_20220316163813441163.png)]

动画插入演示地址:https://www.cs.usfca.edu/~galles/visualization/BTree.html

B+数存储结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aEnTnwq2-1686838790634)(https://dhc.pythonanywhere.com/media/editor/B+Tree结构图_20220316170700591277.png)]

演示地址:https://www.cs.usfca.edu/~galles/visualization/BPlusTree.html

与 B-Tree 的区别:

  • 所有的数据都会出现在叶子节点
  • 叶子节点形成一个单向链表

MySQL 索引数据结构对经典的 B+Tree 进行了优化。在原 B+Tree 的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的 B+Tree,提高区间访问的性能。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6BPVOjEh-1686838790635)(https://dhc.pythonanywhere.com/media/editor/结构图_20220316171730865611.png)]

mysql的最左匹配

对于创建的索引为联合索引:例如(a1,a2)

进行查询时,如果查询条件只存在a1,则查询索引树,如果只知道a2,则不查询索引树

原因:B+树的叶子节点存储普通索引时,存储是以左边元素为标准进行分组存放的,例如:a1=1的所有组合放在一起,a2的位置会分散

索引相关

索引按结构分为聚集索引和二级索引

​ 存在主键则主键为聚集索引,无主键则第一个UNIQUE键为聚焦索引

二级索引叶子节点存储的数据为聚集索引的id值,聚集索引的叶子节点存储的是指定行的所有数据

也就是由二级索引找聚集索引的id值,再找到聚集索引上指定的数据(聚集索引上的叶子节点存储的是所有数据)

二级索引查询的过程

​ 在二级索引中查询出相应的id值,再往聚焦索引中查询对应的列

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YBdXGSlS-1686838790635)(https://dhc.pythonanywhere.com/media/editor/原理图_20220318194454880073.png)]

mysql的索引类型有哪些?

按字段特性分类为:主键索引、唯一索引、普通索引和全文索引。

按数据类型分类为**:B+tree索引、Hash索引、Full-text索引**

索引提高数据的读写速度,提高并发能力和抗压能力。索引相当于图书的目录,根据目录可以快速找到所需的内容

**主键索引:**唯一性索引

**唯一索引:**索引列的所有值只能出现一次,且必须唯一,值可以为空。

**普通索引:**普通的索引类型,值可以为空,没有唯一性的限制

**全文索引:**索引类型位fulltext,它可以在varchar、char、text类型的列上创建。可以通过alter table或create index命令创建。

B+tree索引:非叶子节点上仅存储键值,不存储数据;数据记录均存储在叶子节点上,并且数据是按照顺序排列的。

a:图示如下(B+tree的优点):

**a.非叶子节点上可以存储更多的键值(阶数-1),相应的树的阶数(指针数n)(节点的子节点树)就会更大,树也就会变得更矮更胖。这样一来我们查找数据进行磁盘I/O的次数就会大大减少,数据查询的效率也会更快。

b. 所有数据记录都有序存储在叶子节点上,就会使得范围查找,排序查找,分组查找以及去重查找变得异常简单。

**c.**数据页之间、数据记录之间都是通过链表链接的,有了这个结构的支持就可以方便的在数据查询后进行升序或者降序操作。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VqHdde7P-1686838790635)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20220722090928494.png)]

B树(图示):B树每个节点都会存储数据和关键字,而且叶子节点没有任何关系

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iuLm9RlK-1686838790636)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20220722090945467.png)]

B+树与B树对比:

1. **B+树的磁盘读写代价更低 。**B+树的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说I/O读写次数也就降低了。

2. **B+树的查询效率更加稳定。**由于内部结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。

3. **B+树更有利于对数据库的扫描。**B树在提高了磁盘IO性能的同时并没有解决元素遍历的效率低下的问题,而B+树只需要遍历叶子节点就可以解决对全部关键字信息的扫描,所以对于数据库中频繁使用的range query,B+树有着更高的性能。

为什么添加索引之后查询速度变快?

传统的查询方式为遍历查询,而索引查询则是通过btree算法生成一个索引文件,查询数据库时,找到索引文件进行遍历(折半查找),找到相应的键从而获取数据。

InnoDB采用的是一个B+树的存储结构,树的层高代表着IO的次数,同时相对于B树来说更有优势,同时B+树的层高一般在2-4层,在3层高的时候就可以存储两千万的数据量。这样的一个结构使得索引查询的更快。

在哪些列上使用索引?

a) 查询较频繁的字段应该创建索引

b) 唯一性太差的字段不适合创建索引,例如gender

c) 更新非常频繁的字段不适合索引

d) 不会出现在where子句的字段不该创建索引

总结如下:

a: 肯定在where条经常使用

b: 该字段的内容不是唯一的几个值

c: 字段内容不是频繁变化。

索引失效的几种情况

1、索引列发生了类型转换

2、对索引列进行了各种运算

3、使用了索引列但查询的是所有的情况,则索引失效

4、分区表导致索引失效

5、move操作导致索引失效

6、long列调整导致索引失效

什么情况下使用索引?

1、表的主关键字

2、表的唯一约束

3、直接条件查询的字段

4、查询中与其他表关联的字段

5、需要排序的字段

6、统计和分组统计的字段

索引的开销

1、热块竞争

2、回表消耗

3、更新消耗

4、建立开销

53、索引的优缺点

优点

​ 提高数据检索的速度,降低io成本

​ 降低对数据进行排序的成本,降低CPU的消耗

缺点

​ 占据磁盘空间

​ 降低更新的速度

54、MySql优化方向:

(1) 选择合适的字段属性

(2) 使用连接(JOIN)代替子查询

(3) 使用联合(UNION)来代替创建临时表

(4) 事务

(5) 锁定表

(6) 使用外键

(7) 使用索引

(8) 优化查询语句

(9)数字型的字段不能设置成字符型,因为字符型需要逐个字符进行比较

对于单表数据量过百万、千万的查询

1)数据库分区()

分区就是将数据量大的表的数据均摊到不同的硬盘、系统或不同服务器存储介质中(从物理上分成若干个小表),本质还是同一张表,有利于查询的时候通过不同的硬件分摊查询压力

1、水平分区

-背景:数据量庞大

-介绍:对表的行进行分区,不同物理不同分组里面的物理分割数据集得以组合,从而进行个体分割或集体分割。所有在表中定义的列在每个数据中都能找到。所以表的特性依然得以保持

2、垂直分区

-背景:每行数据字段多,但有些字段包含大text且不经常被访问,这些字段就需要被切割出去。

-介绍:对表的垂直划分来减少目标表的宽度,使某些特定的列被划分到特定的分区,每个列都包含了其中的列所对应的行。

使用场景:

* 一张表的查询速度已经慢到影响使用;
* SQL经过优化还是很慢;
* 数据量大;
* 表中的数据是分段的;
* 对数据的操作往往只涉及一部分,而不是所有的数据。
  • 1
  • 2
  • 3
  • 4
  • 5

优点

1、相对于单个文件系统或是硬盘,分区可以存储更多的数据;
2、数据管理比较方便,比如要清理或废弃某年的数据,就可以直接删除该日期的分区数据即可;
3、精准定位分区查询数据,不需要全表扫描查询,大大提高数据检索效率;
4、可跨多个分区磁盘查询,来提高查询的吞吐量;
5、在涉及聚合函数查询时,可以很容易进行数据的合并;
  • 1
  • 2
  • 3
  • 4
  • 5
2)数据库分表

就是把一张表按一定的规则分解成N个具有独立存储空间的实体表。系统读写时需要根据定义好的规则得到对应的字表明,然后操作它。

与分区的区别

* 分区只是一张表中的数据的存储位置发生改变,分表是将一张表分成多张表。
* 当访问量大且表数据比较大时,两种方式可以互相配合使用。
* 当访问量不大,但表数据比较多时,可以只进行分区。
  • 1
  • 2
  • 3

使用场景:

* 一张表的查询速度慢到影响使用时;
* SQL经过优化;
* 数据量大;
* 当插入数据或联合查询速度变慢时。
  • 1
  • 2
  • 3
  • 4

union和union All的区别

对两个数据库进行操作
Union:对两个结果集进行并集操作,不包括重复行,同时进行默认规则的排序;

Union All:对两个结果集进行并集操作,包括重复行,不进行排序;
  • 1
  • 2
  • 3
  • 4

Mysql的主从同步

主要关键日志就是binary log日志

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LQ9r12gK-1686838790636)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230226084435216.png)]

MySQL的日志

一、重做日志(redo log)
redo日志的作用一句话概括就是:保证事务的持久性
重做日志是一种基于磁盘的数据结构,因为REDO日志会在磁盘的相应位置产生记录。
它的主要作用在于在崩溃恢复期间纠正不完整事务写入的数据。 在正常操作期间,重做日志对由 SQL 语句或低级 API 调用产生的更改表数据的请求进行记录。
在初始化期间和接受连接之前,会自动重做在意外关闭之前未完成更新数据文件的修改。

redo log可以简单分为以下两个部分:
一是内存中重做日志缓冲 (redo log buffer),是易失的,在内存中
二是重做日志文件 (redo log file),是持久的,保存在磁盘

什么时候写redo?
在数据页修改完成之后,在脏页刷到磁盘之前,先写入redo日志。注意的是先修改数据(数据在内存中的变化,并提交),后写日志(提交后把日志写会到磁盘里)。
但是这里需要注意的是,在未提交(commit)前,redo日志在内存的日志缓冲区中,并没有写回到磁盘上。

什么时候释放?
当对应事务的脏页写入到磁盘之后,redo log的使命也就完成了,重做日志占用的空间就可以重用(被覆盖)。

关于文件的大小和数量,由一下两个参数配置
innodb_log_file_size 重做日志文件的大小。
innodb_mirrored_log_groups 指定了日志镜像文件组的数量,默认1

redo日志空间管理
Redo log文件以ib_logfile[number]命名,Redo log 以顺序的方式写入文件,写满时则回溯到第一个文件,进行覆盖写。

MYSQL 8.0归档日志
在MySQL中,redo log是一个文件组,一般是3个文件,循环写入,写满的时候会做redo log层面的checkpoint,然后覆盖之前的redo log;而binlog是有归档功能的,每个binlog写满之后,都会重新开启下一个binlog开始写入,这也是为什么可以使用binlog来进行数据恢复的一个原因,就是因为它的归档功能。

MySQL8.0.17中引入了redo log的归档功能,如果我们开启归档功能,redo log会持续不断的生成,而不会覆盖掉之前的redo log。这个功能主要在哪种场景下应用呢?
试想这样一种情况,在对一个高并发的数据库进行备份的时候,备份速度很慢而redo log生成的速度很快,备份的速度跟不上redo log的生成速度,导致redo log被覆盖了,此时备份的一致性就无法得到保证了。有了redo log的归档功能,就可以在备份启动的时候同步启动redo log 归档,而在备份结束的时候同步停止redo log归档,这样就可以避免这个备份的问题了。备份结束之后,依旧可以利用这个期间产生的redo log进行数据恢复。

先检查一下是否开启了归档:
使用show variables like ‘%archive%’;
redo log的归档路径有如下限制:
(1)、目录必须存在,而且其他用户不可访问,最好是700的权限模式
(2)、该用户目录不能和datadir、innodb_tmpdir、以及其他mysqld的运行目录重合,需要单独创建

1.首先在系统下创建一个目录,并且赋予相关权限
[root@mysql80 ~]# mkdir /var/mysql_arch
[root@mysql80 ~]# chown -R mysql.mysql /var/mysql_arch
[root@mysql80 ~]# chmod 700 /var/mysql_arch
2.MYSQL数据库中设置相关参数。
(1) set global innodb_redo_log_archive_dirs=‘tmp_redo_dir:/var/mysql_arch’ ;
(2)生成归档测试:
do innodb_redo_log_archive_start('tmp_redo_dir‘);
检查归档是否生成:
关闭归档就一句话:do innodb_redo_log_archive_stop();

二、回滚日志(undo log)
undo日志用来保证事务的原子性以及innodb的mvcc(多版本并发控制)

undo数据是:事务行为的记录、每次更改数据的前镜像(旧数据)记录、至少保留到事务结束
用于支持:回滚操作、MVCC机制的重要保障、在实例的恢复时起到重要作用

内容:逻辑格式的日志,在执行undo的时候,仅仅是将数据从逻辑上恢复至事务之前的状态,而不是从物理页面上操作实现的,这一点是不同于redo log的。

undo log的写入时机?
undo是在事务开始之前保存的被修改数据的一个版本,产生undo日志的时候,同样会伴随类似于保护事务持久化机制的redolog的产生。

undo什么时候释放?
事务提交之后,undo log并不能立刻被删除,而是放入待清理的链表,由purge线程判断是否有其他事务在使用undo段中的上一个事务之前的版本信息,再决定是否可以清理UNDO段的空间。
如果事务 rollback,innodb 通过执行 undo log 中的所有反向操作,实现事务中所有操作的回滚,随后就会删除该事务关联的所有undo段。

undo文件
默认情况下undo文件是保存在共享表空间的,即ibdatafile文件中,当数据库中发生一些大的事务性操作的时候,要生成大量的undo信息,全部保存在共享表空间中。
因此共享表空间可能会变的很大,默认情况下,也就是undo 日志使用共享表空间的时候,被“撑大”的共享表空间是不会也不能自动收缩的。
因此,mysql5.7之后的“独立undo 表空间”的配置就显得很有必要了。
MYSQL 8.0已经把UNDO表空间给独立出来了,不需要我们再进行额外操作了。

三、二进制日志(binlog)
用来记录操作MySQL数据库中的写入性操作(增删改,但不包括查询)
MySQL 的二进制日志(binary log)是一个二进制文件,主要用于记录修改数据或有可能引起数据变更的 SQL 语句。
二进制日志(binary log)中记录了对 MySQL 数据库执行更改的所有操作,并且记录了语句发生时间、执行时长、操作数据等其它额外信息,
但是它不记录 SELECT、SHOW 等那些不修改数据的 SQL 语句。二进制日志(binary log)主要用于数据库恢复和主从复制,以及审计(audit)操作
二进制日志(Binary log) 的操作语句
/删除所有二进制日志文件:/
reset master
reset slave

/删除部分二进制日志文件:/
purge master logs to/befor ‘args’;
例如:
PURGE MASTER LOGS TO ‘mysql-bin.010’;
PURGE MASTER LOGS BEFORE ‘2021-06-02 22:46:26’;

/查看是否启用二进制日志:/
show variables like ‘%log_bin%’;

/查看所有的二进制日志参数/
show variables like ‘%binlog%’;

/查看文件的位置/
show variables like ‘%datadir%’;

/查看当前服务器所有的二进制日志文件/
show binary logs;
show master logs;

四、错误日志(errorlog)
启动、运行或停止 mysqld 时遇到的问题
MySQL 错误日志记录 MySQL 运行过程中较为严重的警告和错误信息,以及 MySQL 每次启动和关闭的详细信息。
MySQL 错误日志默认是开启的。可以通过 MySQL 配置文件中的 log-error=/var/log/mysqld.log 配置,修改错误日志的配置信息。
可以通过如下 SQL 查看错误日志的详细信息:
show variables like ‘%log_err%’;

五、慢查询日志(slow query log)
执行时间超过 long_query_time 秒的查询(默认是10s)
记录所有执行时间超过 long_query_time 秒的查询 SQL 或者没有使用索引的查询 SQL,默认情况下,MySQL 不开启慢查询日志,
long_query_time值查询语句: show variables like ‘long_query_time’;
long_query_time值修改语句: set long_query_time = 秒数;

/查看当前慢查询日志的开启情况:/
show variables like ‘%query%’;

slow_query_log:ON 表示开启慢查询日志,OFF 表示关闭慢查询日志
slow_query_log_file:记录慢查询日志的文件地址(默认为主机名.log)
long_query_time:指定了慢查询的阈值,单位是秒,即执行语句的时间若超过这个值则为慢查询语句
log_queries_not_using_indexes:ON 表示会记录所有没有利用索引来进行查询的语句,前提是 slow_query_log 的值也是 ON,否则,不会奏效,OFF 表示不会记录所有没有利用索引来进行查询的语句。

六、一般查询日志(general log)
从已建立的客户端连接收到的语句
记录已连接MYSQL数据库的客户端所执行的语句。

可以通过如下 SQL 查看当前的通用日志是否开启:
SHOW VARIABLES LIKE ‘%general%’;
开启通用查询日志:
set global general_log = on;
关闭通用查询日志:
set global general_log = off;

七、中继日志(relay log)
从复制源服务器收到的数据更改
当日志时间和系统时间不一致时的处理方式
在MySQL 5.7.2 新增了 log_timestamps 这个参数,该参数主要是控制 error log、 General query log 等记录日志的显示时间参数,并且数据库安装后这些日志时间戳默认为UTC,因此会造成与系统时间不一致,与北京时间相差8个小时
查看当前日志时间戳:
SHOW GLOBAL VARIABLES LIKE ‘log_timestamps’;
因为log_timestamps 是一个GLOBAL的全局参数,所以直接在登录后去set全局参数,重启后就会直接失效因此需要在mysql的配置文件中[mysqld]中增加一条:
log_timestamps=SYSTEM
并且重启MYSQL后生效。

MySQL的SQL优化

重点关键字:explain

"id":id值相同,从上往下顺序执行,同时顺序受表中行数影响,数据越少越先执行;id不同:越大越优先
"select_type":查询类型
"table":表名
"type":类型(重要)system>const>eq_ref>ref>range>index>ALL(效率高-》低)
system(只有一条数据的系统表或子表只有一条数据的主查询)>const(仅能查询到一条数据,必须用于主键或者唯一索引)为理想,实际能达到ref的效率
eq_ref:唯一性索引:对于每个索引键的查询,匹配到唯一一行数据(也就是name为张三的行只有一行),常见于唯一索引或者主键索引
ref:非唯一性索引,对于每个索引键的查询(也就是索引键name为张三的有多个),会返回匹配的所有行(0或者多个,不能是一个)
range:检索索引键指定范围的行,where后面是一个范围查询(between,<,> )in可能会失效
index:查询全部数据(根据索引)
all:查询表中全部数据
"possible_keys":预测用到的索引
"key":实际使用的索引列(重要)
"key_len":索引长度
"ref":表之间的引用
"rows":通过索引查询到的数据量(重要)
"Extra":扩展信息

                              
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

MyBatis

运行原理

  • 加载全局配置文件mybatis-config.xml中的运行环境等信息
  • 加载映射文件也就是xml层,配置跟dao接口进行匹配的数据库操作语句
  • **创建会话工厂:**通过加载的环境构建会话工厂SqlSessionFactory
  • 创建会话对象:由会话工厂进行创建SqlSession对象,该对象包含了执行SQL语句的所有方法,是一个既可以发送sql执行并返回结果,也可以获取mapper接口。
  • Executor执行器:根据Sqlsession传递的参数动态生成需要执行的sql语句,同时负责查询缓存的维护。
  • **MapperStatement对象:**在Executor接口的执行方法中有一个MappedStatement类型的参数,这个参数是对映射信息的封装,用于存储要映射的SQL语句的id、参数等信息
  • **输入参数映射:**输入参数类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。
  • **输出类型映射:**输出结果也可以是Map、List等集合类型。类似JDBC对结果集的解析过程

img

基本组件

SqlSessionFactoryBuilder建造者对象

img

SqlSessionFactory对象

创建sqlSession,sqlSession是一个会话,相当于jdbc中的Connection对象

每次访问数据库就要通过SqlSessionFactory创建sqlSession,所以SqlSessionFactory会在mybatis整个生命周期都会使用
如果多次创建同一个数据库的SqlSessionFactory,每次创建SqlSessionFactory会打开更多的数据库连接资源,连接资源会被耗尽,所以这个采用单例模式。一个数据库只对应一个SqlSessionFactory

SqlSession对象

sqlSession是一个外观模式,提供基本API:增删改查,还有辅助API,也就是提交、关闭会话

  • 每次创建SqlSession都必须即时关闭,否则数据库连接池的活动资源少。
    SqlSession提供了增删改查,使用mapper接口,其中有映射器,映射器作为一个动态代理,进入到mapperMethod的方法就能简单找到SqlSession的增删改查。其实就是通过动态代理记录,让接口跑起来,使用命令模式,最后采用sqlSession的方法执行sql语句
Executor执行器

一共有三个执行器实现类

  • SimpleExecutor:每执行一次更新或查询就会开启一个Statement对象,用完立刻关闭Statement对象。
  • ReuseExecutor:执行查询或者更新,以sql作为key去查询Statement对象,用完不关闭Statement对象,而是放置于Map<String,Statement>内,供下一次使用
  • BatchExecutor:执行更新(批量处理不支持select),将所有sql添加到批处理对象(addBatch())等待统一执行(ExecuteBatch()),它缓存了多个Statement对象。
Mapper对象

Mapper是一个接口,没有任何实现类,作用是发送SQL返回需要的结果,或者执行SQL从而修改数据库的数据,因此应该在一个SqlSession事务方法之内。

在这里插入图片描述

mybatis的层次结构

img

SqlSession:作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能
Executor:MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护

StatementHandler :封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合。

ParameterHandler: 负责对用户传递的参数转换成JDBC Statement 所需要的参数,

ResultSetHandler:负责将JDBC返回的ResultSet结果集对象转换成List类型的集合;

TypeHandler :负责java数据类型和jdbc数据类型之间的映射和转换

MappedStatement: MappedStatement维护了一条<select|update|delete|insert>节点的封装,

SqlSource:负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回

BoundSql :表示动态生成的SQL语句以及相应的参数信息

Configuration:MyBatis所有的配置信息都维持在Configuration对象之中

mybatis的接口和mapper.xml如何进行映射的

mybatis里所有mapper接口的实现类都可以看做是mapperProxy,mapper代理类,然后调用MapperProxy.invoke()方法,invoke()方法会执行相应sql语句,并将结果返回。

**那么是如何调用的mapperProxy代理类呢?**且看一下分解:

img

1、程序会在SqlSession中调用Configuration的getMapper方法

 @Override
  public <T> T getMapper(Class<T> type) {
    return getConfiguration().getMapper(type, this);
  }
  • 1
  • 2
  • 3
  • 4

2、当时使用configuration的getMapper方法时,会调用mapperRegistry的getMapper方法

 public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
  }
  • 1
  • 2
  • 3

3、当使用mapperRegistry的getMapper方法时,会调用mapperProxyFactory.newInstance(sqlSession)方法,得到一个MapperProxy对象

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
      // 转移到mapperProxyFactory
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

4、在这里,先通过T newInstance(SqlSession sqlSession)方法会得到一个MapperProxy对象,然后调用T newInstance(MapperProxy mapperProxy)生成代理对象然后返回。

  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
    //所有动态代理类的方法调用,都会交由InvocationHandler接口实现类里的invoke()方法去处理。这是动态代理的关键所在。
    //动态代理我们写的dao接口,第一个参数是类加载器,第二个参数是mapper接口,第三个参数是mapper代理类
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }
  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

5、而查看MapperProxy的代码,可以看到如下内容:

public class MapperProxy<T> implements InvocationHandler, Serializable {

  //mapper动态代理实现InvocationHandler接口,重写invoke方法
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        //此处表示InvocationHandler的invoke方法,动态代理最后都会调用InvocationHandler的invoke方法
        return method.invoke(this, args);
      } else if (isDefaultMethod(method)) {
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    //sqlSession我们在上一篇博客以获取,现在有得到了mapper接口的代理类,所以此处开始执行sql
    return mapperMethod.execute(sqlSession, args);
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

该MapperProxy类实现了InvocationHandler接口,并且实现了该接口的invoke方法。通过这种方式,我们只需要编写Mapper.java接口类,当真正执行一个Mapper接口的时候,就会转发给MapperProxy.invoke方法,而该方法则会调用后续的sqlSession.cud>executor.execute>prepareStatement,mybatis会利用Statement对象读取xml映射文件的映射信息(类全限定名和id),并通过mapperProxy类的invoke方法来注入接口类的全限定类名找到对应的Statement对象。通过以上的mapperProxy代理类,我们就可以方便的使用mapper.java和mapper.xml了。

mybatis的一对多查询

tips:查询某市的所有学校,学校表中有关联市编号

//第一种方案 查询两次,根据某市(Urban)的编号调用查询学校(School)的传入市编号 
<mapper namespace="com.yh.mybatis.dao.mapper.SchoolMapper">
    <select id="urbanSchool" resultType="com.yh.mybatis.dao.pojo.School">
        select * from school where urban_id = #{urbanId}
    </select>
</mapper>
        
<resultMap id="findAllUrbanSandH" type="com.yh.mybatis.dao.pojo.Urban">
        <collection property="schools" javaType="java.util.List" ofType="com.yh.mybatis.dao.pojo.School"
                    select="com.yh.mybatis.dao.mapper.SchoolMapper.urbanSchool"
                    column="{urbanId=id}">
        </collection>
</resultMap>
        <select id="findAllUrbanSandH" resultMap="findAllUrbanSandH">
        	select * from urban
        </select>
// 第二种方案 查询一次
<resultMap id="findAllUrbanSandH2" type="com.yh.mybatis.dao.pojo.Urban">
        <id property="id" column="id"/>
        <result property="cityId" column="city_id"/>
        <result property="urbanName" column="urban_name"/>
<!--这上面这几个字段就是urban表中,自带的那几个字段-->
        
        <collection property="schools" javaType="java.util.List" ofType="com.yh.mybatis.dao.pojo.School">
            <id property="id" column="sid"/>
            <result property="urbanId" column="surban_id"/>
            <result property="schoolName" column="school_name"/>
            <result property="people" column="speople"/>
        </collection>
<!--这上面就是school表中的字段
		javaType是urban类中定义的school的类型  可以不写
		ofType就是泛型,这个还是很有必要的,接下来的id result 就是这个类中定义的各种字段,要写全
		如果涉及到的任何表中,在数据库中有重复的字段名,那就必须要起别名。(例如各个表中的id)
		起别名直接在下面的sql中就可以。
-->
        <select id="findAllUrbanSandH2" resultMap="findAllUrbanSandH2">
        select  urban.city_id
                ,urban.id
                ,urban.urban_name
                ,school.id sid
                ,school.urban_id surban_id
                ,school.school_name
                ,school.people speople
        from urban
            inner join school on urban.id = school.urban_id
    </select>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

也可以使用Association标签进行嵌套关联和查询关联,具体跟collection类似

Mybatis的动态SQL

一些标签:

if(条件判断)

**choose 、 when otherwise(多条件分支)**相当于java的case when

where(处理sql语句拼接问题)

foreach(循环)

/*collection:类型,如果你传入的参数是数组,就用array,是集合就用list
        item:数组中每个元素赋值的变量名
        open: 以谁开始
        close:以谁结束
        separator:分割符*/
<select id="findByVid" resultType="TickDTO">
        select v.vid,vd.time from voyage v,voyage_detail vd where v.vid = vd.vid
        <foreach collection="array" open="and v.vid in ( " close=")" separator="," item="item">
            #{item}
        </foreach>
    </select>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

trim

<update id="testTrim" parameterType="com.mybatis.pojo.User">
        update user
        <trim prefix="set" suffixOverrides=",",suffix = " end">
            <if test="cash!=null and cash!=''">cash= #{cash},</if>
            <if test="address!=null and address!=''">address= #{address},</if>
        </trim>
        <where>id = #{id}</where>
</update>

-- 该语句中的prefix,suffix表示trim包裹的内容不为空则加上set以及 end
-- 语句中的suffixOverrides表示trim包裹的内容不为空则删除最后一个,
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

Mybatis的常见的使用${}的情况:

  • ```
    #{}会在预编译时将#{}替换成占位符?。然后再调用预编译对象进行赋值
    只是单纯的文本替换,将 {}只是单纯的文本替换,将 只是单纯的文本替换,将{}的值替换成变量的值
    ${} 使用之后不会带上单引号 而#{} 会自动加上单引号
    1.当sql中表名是从参数中取的情况
    2.order by排序语句中,因为order by 后边必须跟字段名,这个字段名不能带引号,如果带引号会被识别会字符串,而不是字段。
    
    
    • 1

Mybatis是如何解决实体类的属性名和表中的字段名不一致的问题?

(1)在resultMap标签中一一定义实体类属性和表列之间的映射关系

<resultMap id="" type="返回的实体类">
	<id property="实体类属性名" column = "列名">
  • 1
  • 2

(2)在查询的结果中为每个列定义一个别名,别名与实体类的属性名一一对应。

MyBatis传递多个参数到xml的方法

(1)使用@param注解,一一声明要传入的参数

(2)使用实体类,每一个属性代表一个参数

(3)使用Map集合

MyBatis的缓存

第一层缓存为sqlsession缓存,每一个用户在执行查询的时候,都需要使用到sqlsession来查询数据库,为避免重复的请求多次直接请求数据库,所以在本地建立了sqlsession的缓存存储查询到的数据,后续会首先查询缓存的数据,如果需要实现跨sqlsession的缓存就需要用到二级缓存,也就是在多个用户访问sqlsession的时候,只要有一个用户查询到相应的数据就会存放到二级缓存中,其他的sqlsession就会从二级缓存中加载数据。

实现原理:一级缓存中存在一个Executor对象,内存放着LocalCache对象,当我们进行查询的时候,mybatis会根据查询语句去localCache中找到对应的数据,命中则返回,在分布式的系统中可能导致脏读的现象。

二级缓存则是在原有的基础上新建了一个CachingExecutor对象,查询时会先通过这个对象首先进行二级缓存的查询。则查询的流程为:先查二级再查一级再查数据库

分页插件注意事项

方法1:startPage(int pageNum, int pageSize)或offsetPage(int offset, int limit)
tips:只有紧跟在方法后的第一个 Mybatis 的查询(Select)方法会被分页

Redis

redis的存储结构

redis是一个key-value形式的键值数据库,key的数据格式是string,value的类型有五大数据格式:

1、String字符串格式,string类型是二进制安全的,可以包含任何数据。比如图片或者序列化的对象,最大存储512MB。

2、Hash哈希,是一个键值对的集合,适合存储对象

3、List列表,字符串列表,按照插入顺序排序。

4、Set集合,String类型的无序不重复集合,内部实现是一个value为null的Hash Map,使用场景:共同好友

5、zset(sorted set:有序集合)

redis为什么这么快

1、redis的操作完成是基于内存的

2、redis是单线程的操作,省去了多线程的上下文切换带来的开销

3、redis的数据结构简单,对数据操作也简单

4、采用基于IO多路复用机制的线程模型,处理并发的连接

Redis基于Reactor(多路复用) 模式开发了自己的网络事件处理器,这个处理器就是单线程的。所以叫单线程的模型,但是它采用了IO多路复用监听多个Socket,并将Socket选择对应事件处理器进行处理并放到队列中排队。交由单线程的网络处理器进行处理

多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,然后程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。

这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个客户端的网络IO连接请求(尽量减少网络 IO 的时间消耗)
  • 1
  • 2
  • 3

Redis6.0 为什么要引入多线程呢?

因为Redis的瓶颈不在内存,而是在网络I/O模块带来CPU的耗时,所以Redis6.0的多线程是用来处理网络I/O这部分,充分利用CPU资源,减少网络I/O阻塞带来的性能损耗

Redis6.0 如何开启多线程?

默认情况下Redis是关闭多线程的,可以在conf文件进行配置开启:

io-threads-do-reads yes

io-threads 线程数

## 官方建议的线程数设置:4核的机器建议设置为2或3个线程,8核的建议设置为6个线程,线程数一定要小于机器核数,尽量不超过8个。
  • 1
  • 2
  • 3
  • 4
  • 5

Redis的持久化

redis持久化是指在指定的时间间隔内将内存中的数据集快照(snapshotting)写入磁盘,恢复时是将快照文件读入内存 redis提供了两种持久化方式:RDB(Redis DataBase)  AOF(Append of File)

redis会单独创建一个子线程来进行持久化,会将数据写入一个临时文件中,待持久化过程都结束了,再将这个临时的文件替换上次持久化好的文件,整个过程,主进程不进行io操作。

RDB的缺点是:
1、在一定时间间隔做一次备份,如果redis意外down机,最后一次持久化后的数据可能丢失
2、RDB方式持久化数据没办法做到秒级/实时地持久化。因为bgsave每次运行都要fork操作子进程,属于重量级操作,如果不采用压缩算法,执行成本过高。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
1) RDB持久化过程:

img

2)AOF持久化过程

以日志地形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读不记录),只许追加文件不改写文件,redis启动时读取该文件重构数据。重启的过程:根据日志文件的内容将写指令从前往后执行一次以完成数据的恢复工作。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1Gydh527-1686838790637)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230225214118350.png)]

(1)客户端的请求写命令会被append追加到AOF缓冲区内;
(2)AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中;
(3)AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;
(4)Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的;

img

aof的文件体积大如何解决?

有一个bgrewriteaof命令,让多个对同一个key进行操作的语句,会进行合并,并只有最后一步操作是有效的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iJaIVuTa-1686838790637)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230225214702251.png)]

AOF和RDB两者的对比

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Lwz9pcnR-1686838790638)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230225215044327.png)]

两者同时结合使用时,一般会以aof作为主要,因为aof文件更为完整

Redis的主从集群、读写分离

解决redis的高并发读的一个问题,内存设置不能太高,因为内存同步中的rdb或者aof占有IO较高

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vRmmaKDG-1686838790638)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230225215303983.png)]

配置集群的主节点:slaveof <主节点ip> <主节点端口号>

5.0后用replcaof替换了slaveof

数据同步原理

主从第一次同步为全量同步。过程为:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GVOl2Q7B-1686838790638)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230225220634647.png)]

由于bgsave是异步进行的,所以需要一个log缓存记录rdb时候执行的记录

master如何判断slave是第一次同步数据?

  • Replication id 简称repid,是数据集的标记,id一致说明是同一个数据集
  • offset 偏移量,随着记录在repl_baklog中的记录增加而增大。所以slave在同步的时候也会检查偏移量,当小于master的偏移量时,则需要更新

全量同步的全过程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sYQm2F79-1686838790639)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230225221149556.png)]

增量同步发生时机:slave重启后同步

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dUolYhZJ-1686838790639)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230225221247857.png)]

注意:repl_baklog大小有上限,写满后会覆盖最早的数据。如果slave断开时间过久,导致尚未备份的数据被覆盖则无法基于log做增量同步,只能再次全量同步

哨兵机制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iuDOtCOB-1686838790640)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230226084736296.png)]

监控:基于心跳机制检测,每隔1s向集群的每个实例发送ping,

  • 主观下线:某Sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线
  • 客观下线:也就是其他节点也发现了,进行投票,数量超过Sentinel的一半时,则认为下线

选择salve的一个节点作为master,依据是这样:

首先会判断子节点与主节点断开的时间长短,超过指定值就会排除

然后判断slave的slave-priority值,值小优先级高,但是为0不参与选举

如果slave-priority值一样,则会判断偏移量,越大优先级越高

最后会判断slave的允许id大小,越小优先级越低

自动故障恢复、通知

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2u00OFPy-1686838790640)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230226085613478.png)]

java实现读写分离配置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MgXlg8DH-1686838790641)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230226085930533.png)]

Redis的分片集群

解决的是redis主节点内存不足无法应对数据量多的写时。

哨兵模式基本已经实现了高可用,但是每个节点都存储相同的内容,很浪费内存。而且,哨兵模式没有解决master写数据的压力。为了解决这些问题,就有了集群模式,实现分布式存储,每个节点存储不同的内容。集群部署的方式能自动将数据进行分片,每个master上放一部分数据,提供了内置的高可用服务,即使某个master宕机了,服务还可以正常地提供

img

集群模式中数据通过数据分片的方式被自动分割到不同的master节点上,每个Redis集群有16384个哈希槽,进行set操作时,每个key会通过CRC16校验后再对16384取模来决定放置在哪个槽。数据在集群模式中是分开存储的,那么节点之间想要知道其他节点的状态信息,包括当前集群状态、集群中各节点负责的哈希槽、集群中各节点的master-slave状态、集群中各节点的存活状态等是通过建立TCP连接,使用gossip协议来进行集群信息传播。

故障判断方法:判断故障的逻辑其实与哨兵模式有点类似,在集群中,每个节点都会定期的向其他节点发送ping命令,通过有没有收到回复来判断其他节点是否已经下线。具体方法是采用半数选举机制,当A节点发现目标节点疑似下线,就会向集群中的其他节点散播消息,其他节点就会向目标节点发送命令,判断目标节点是否下线。如果集群中半数以上的节点都认为目标节点下线,就会对目标节点标记为下线,从而告诉其他节点,让目标节点在整个集群中都下线。

缓存同步策略

设置有效期:给缓存设置有效期,到期后自动删除。

**同步双写:**修改数据库时直接修改缓存

**异步通知:**修改数据库时发生事件通知,相关服务监听到通知后修改缓存数据

解决缓存和数据双写一致性的问题

什么问题?

为了提高数据查询的性能,会将从数据库查询到的数据存放到缓存中,方便下一次进行查询,在进行更新数据库时同时需要同步更新缓存中的数据,但是这个过程会受到另外线程查询或者更新的影响。

解决策略:

(1)实时性一致要求不高的系统中,可以对缓存的数据加上过期时间,定期对数据进行更新。

(2)先删缓存中的数据,再删除数据库中的数据。

但这样会带来一个线程切换导致的问题:当删除缓存还未更新时,线程切换,缓存中存入了旧数据,线程切回,对数据库进行更新,导致缓存数据库数据不一致

(3)先删除缓存,再更新数据库,再删除缓存

基于redis的高可用高并发

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0N9w6f8U-1686838790641)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230225210245279.png)]

redis的缓存淘汰策略

noeviction

当内存到达配置的最大值时新数据不会被保存,这个策略就是noeviction,可以说是非常坑的。
allkeys-lru

关于最近最少使用Least Recently Used,其实是按最后一次访问时间进行的排序。可以在逻辑上用一个队列来建模,左边代表可以清理的,右边代表需要保留的。当内存不足时,从左到右清除队列里的数据。那么当一个元素被使用时,就移动到最右边。所以这个算法对跟使用次数是没关系的,只和最后一次的使用时间有关系,但是翻译为“最近最少使用”,这个最少就容易让人误解为要使用次数最少。其实他的英文Least没有加the,说明不是形容词的最高级,真正的意思是至少、起码,所以准确的翻译应该为“起码最近使用”。而allkeys是覆盖了所有的key,与通过redis命令加在key上面的过期时间无关。所以这个策略也是不建议使用的。这个算法常见的实现是LinkedHashMap,用Hash快速找到使用的key,用LinkedList维护淘汰队列。
allkeys-lfu

LFU的翻译是最不经常使用frequently used,但是这个翻译是意译,中文算是准确了,就是访问次数最少的被淘汰。算法很简单,使用一个小顶堆来实现,使用次数最少的在堆顶,然后每次内存不足时从堆顶移除。随便插一句,Java的堆为什么叫堆?其实这是个历史名词,因为以前的时代,就是用小顶堆来维护内存的,内存的使用次数最小的(一般为0)在堆顶,所以每次要空闲内存的时候,就从堆顶拿内存就可以了,后来的内存分配算法已经不用堆了,但是堆这个历史名词还是保留了下来。同allkeys-lru一样,allkeys-lfu也是忽略key的过期时间,所以也不建议使用。
volatile-lru

从设置了超时的key,也就是expire=true的key中使用LRU算法进行淘汰,不再赘述。但是要注意的是这个时候key不一定是过期的key,极有可能是还存活者的key。
volatile-lfu

从设置了超时的key中使用LFU算法进行淘汰,不再赘述。
allkeys-random

这从字面就可以理解,随机选个key淘汰,这也太坑了吧、
volatile-random

从设置了超时的key里随机淘汰,还不算太坑。
volatile-ttl

从设置了超时的key里淘汰剩余存活时间time-to-live最小的key,TTL就是time-to-live的缩写。
近似LRU算法

Redis的LRU算法不是精确的LRU算法,也就是说并不是对所有的key进行最后一次访问时间管理,只是取一定的样本数据来进行粗略估算,所以样本数量取得越大,算法就越精确,具体需要用户自己根据实际情况配置。
近似LFU算法

Redis的LFU算法虽然同LRU一样使用的是近似的抽样统计算法,但是原理有点不一样,内部使用了莫里斯计数器Morris counter进行次数统计。其默认是这样,100万次请求后,将莫里斯计数器设置为最大值,也就是使莫里斯计数器饱和Saturate ,然后每隔一个衰减周期decay time减少一次计数器的值。
发生缓存淘汰的时机:redis的内存占据达到了maxmemory最大存储阈值时,这个值默认值也就是服务器的最大内存值。就会从redis中选出key进行淘汰。其中共有8中策略:

可以划分为5大种:

LRU 把不经常使用的key直接淘汰

LFU 在LRU基础上增加了对key的使用频率进行记录并比较,保证不去掉热点key

ttl 选出过期时间最近的key

随机淘汰某个key

报错
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

redis的缓存穿透、击穿、雪崩

穿透:由于key不存在于redis缓存中,请求直接来到数据库。

解决办法

  • 在请求到数据库未找到信息时,缓存空信息

  • 布隆过滤器

    虽然缓存空值可以解决缓存穿透问题,但仍然需要查询一次数据库才能确定是否有数据,如果有用户恶意攻击,高并发地使用系统不存在的数据id进行查询,所有的查询都要经过数据库,这样仍然会给数据库带来很大的压力。

击穿:热点数据大量过期,导致请求量直接来到数据库

解决办法:

  • 对热点数据不设置过期时间。如果我们能提前知道某个数据是热点数据,那么就可以不设置这些数据的过期,从而避免缓存击穿问题

  • 互斥锁

    提前知道某些数据会有大量访问,我们当然可以设置不过期,但更多时候,我们并不能提前预知,这种情况要怎么处理呢?

    我们来分析一下缓存击穿的情况:

    正常情况下,当某个Redis缓存数据过期时,如果有对该数据的请求,则重新到数据库中查询并再写入缓存,让后续的请求可以命中该缓存而无须再去数据库中查询。

    而热点数据过期时,由于大量请求,当某个请求无法命中缓存时,会去查询数据库并重新把数据写入Redis,也就是在写入Redis之前,其他请求进来,也会去查询数据库。

    好了,我们知道热点数据过期后,很多请求会去查询数据库,那么我们可以给去查询数据库的业务逻辑加个互斥锁,只有获得锁的请求才能去查询数据库并把数据写回Redis,而其他没有获得锁的请求只能等待数据就绪。

  • 设置逻辑过期时间

    由于互斥锁影响性能,所有对业务数据添加一个冗余过期时间,但数据在Redis不设置过期时间

    当一个请求拿到Redis中的数据时,判断逻辑过期时间是否到期,如果没有到期,直接返回,如果到期则开启另一个线程获得锁后去查询数据库并将查询的最新数据写回Redis,而当前请求返回已经查询的数据。

雪崩:大量的请求无法在缓存中找到,直接来到数据库进行查询

产生原因

  • 由于大量数据从缓存中过期,导致无法在redis中命中数据
  • redis宕机了,无法请求redis

解决办法:

  • 在原有的过期时间基础上,再加上一个较短的随机时间,尽量保证不在同一时间过期
  • 使用redis的高可用模式,搭载主从、哨兵、集群架构

分布式

分布式与微服务的区别

我理解的微服务是用来解决系统架构之间耦合度过高不便于维护和迭代,需要根据模块进行拆分,可以部署在同一台服务器也可以是多台服务器,微服务是可以包含分布式的,但是分布式不一定是微服务。

分布式系统解决的是性能瓶颈问题,是多个单独的服务器所支撑的架构集合,它强调物理层面的分别部署,进而提高系统的吞吐量,不同系统之间通过接口进行沟通。

总体框架

前台的请求首次会到达服务网关进行处理,网关能够验证请求所需要对应得微服务,同时能够预防短时间大量的请求进行限流,由于gateway是基于spring5中提供的WebFlux响应式实现,相比较于zuul基于servlet的实现具有更好的性能。同时经过dubbo进行负载均衡找到同类型微服务的一个服务,微服务会从注册中心和配置中心进行拉去配置信息。

SpringCloud的重要组件

Eureka注册中心(不同服务之间实例信息是不能共享的,所以需要一个中间站,将实例信息存在注册中心以便服务之间调取。同时注册中心也应该有探测服务状态的功能)

Ribbon负载均衡对于同一个服务的集群由此组件按规则进行分发请求给服务处理。

Hystria容错框架(对请求的量以及服务内部进行管理)

Zuul网关(类似于nginx反向代理的功能)(对请求权限与请求进行接收)

Config配置管理(集中配置相同的文件)

跨域的解决方案

1、服务端设置Response Header(响应头部)的Access-Controller-Allow-Origin

2、在需要跨域访问的类和方法中设置允许跨域访问(spring的CrossOrigin注解)

3、继承Spring Web的CorsFilter(适用于Springboot、springmvc)

4、实现WebMVcConfigurer接口的addCorsMappings方法(适用于springboot方法)

// 实现WebMVcConfigurer接口
@Configuration
public class AppConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**") // 拦截所有请求
                .allowedOrigins("*"); // 可跨域的域名,可以为 *
    }
}

// 注解配置方案,跨域部分接口
@CrossOrigin
@RequestMapping("/user")
public class UserController {

    @GetMapping("/{id}")
    public User get(@PathVariable Long id) {
        
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

5、网关GrateWay的跨域配置,设置允许跨域的源

spring: 
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]':
            # 允许跨域的源(网站域名/ip),设置*为全部
            # 允许跨域请求里的head字段,设置*为全部
            # 允许跨域的method, 默认为GET和OPTIONS,设置*为全部
            allow-credentials: true
            allowed-origins:
              - "http://xb.abc.com"
              - "http://sf.xx.com"
            allowed-headers: "*"
            allowed-methods:
              - OPTIONS
              - GET
              - POST
              - DELETE
              - PUT
              - PATCH
            max-age: 3600
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

分布式事务

事务

事务是用户定义的一系列数据库操作,这些操作可以看作一个完成的逻辑处理工作单元,要么全部执行、要么全部不执行,是不可分割的单元。

分布式事务

分布式架构中每个服务都建立在不同的服务器中,服务向数据库发起的请求是不同的,对于这种跨服务的事务我们一般无法通过数据库来进行统一的管理,所以产生了分布式事务。其本质就是保证数据库的数据一致性问题

CAP原理

Consistency(一致性)用户访问分布式系统的任意节点,得到的数据必须一致,也就是及时同步

Availability(可用性)用户访问任何节点,必须得到回应。节点能不能呗正常访问

Partition tolerance(分区容错性)

分区:因为网络等原因导致分布式系统的部分节点与其他节点失去连接,形成独立分区

容错:集群出现分区时,整个系统持续对外提供服务

其中分布式系统通过网络进行连接,一定会出现分区问题

但是当分区问题出现时,就只能满足其他两个条件之一

es集群在出现网络问题时,会将故障节点剔除出集群,并将数据分摊到其余节点保证了一致性

BASE理论,包含三个思想:

Basically Available(基本可用)运行牺牲部分可用性。

Soft State(软状态)一定时间内,允许出现中间状态,比如临时的不一致状态

Eventually Consistent(最终一致性)在软状态结束后,最终达到数据一致。

分布式事务就是解决子事务之间的一致性问题。解决方案

AP模式:子事务之间分别提交,允许结果不一致,保证高可用,采用弥补措施比如增库存成功就减库存恢复数据,实现最终一致

CP模式:各个子事务之间执行后互相等待,同时提交,同时回滚,达成强一致。但是事务等待过程中,处于弱可用状态

Seata架构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bmgdVcnt-1686838790642)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230225200550034.png)]

使用Seata首先会在nacos中注册TC角色

并在微服务通过nacos找到TC

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E9r8T9kN-1686838790643)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230225201221239.png)]

解决模式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FBZvJOoI-1686838790643)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230225200812942.png)]

XA模式

通过协调者,检测所有的事务的执行状态,协调者检测到事务全部成功则成功,存在失败则全部失败回滚,在检测期间事务一直连接

Seata的XA做了调整,加入了RM角色

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KYAnqo2X-1686838790643)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230225201619824.png)]

代码使用:全局事务的入口方法添加@GlobalTransaction注解

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LMZzzY1A-1686838790643)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230225201902989.png)]

AT模式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sSQZ06ez-1686838790644)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230225202301217.png)]

基本思路在正式执行sql提交前需要记录更新前后的快照,就是当发生事务执行失败,可以利用undolog来回滚事务

相对于XA模式最大的区别是什么?

一个是直接提交,一个是等待,资源不浪费

XA利用数据库机制实现回滚,AT利用数据库快照实现数据回滚

同时AT的全局锁锁住的不是整行的记录,只是与当前事务相关的列的锁定:例如要改订单id为1的状态,全局锁锁的就是id为1的状态列,订单的其他列,其他事务还是照常进行修改。

XA的是采用的行锁,锁住的整行的数据。

AT模式的脏写问题

在第一个事务执行时,当协调者需要回滚时会覆盖第二个事务所执行的结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7XMiYGC8-1686838790644)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230225202825759.png)]

解决方案:全局锁,由TC记录当前正在操作某行数据的事务,该事务持有全局锁,具有执行权

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TiyJ8NP0-1686838790645)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230225203027145.png)]

该锁会拒绝第二个事务修改id为1的行的数据.直到事务一释放全局锁,但此时事务二持有了DB锁,事务一又在等待事务二释放DB锁,这样就会产生循环等待的死锁的锁,=

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-52qhtc67-1686838790645)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230225203215764.png)]

解决方案为等待时间的不同。获取全局锁的重试次数。

对于不被Seata所管理的事务1去修改全局锁锁住的列时,事务1就不会去尝试获取锁,则就会产生脏读现象,为了处理这个问题,在记录快照时会记录修改前后的记录,方便后续进行比对

TCC模式

两阶段提交,也就是AP模式的实现

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E7ev6hQD-1686838790645)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230225204413040.png)]

总结:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MCZ2gxV8-1686838790646)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230225204546958.png)]

Saga模式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KdtoNwgK-1686838790646)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230225204746308.png)]

四种模式对比

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0EQcmLW6-1686838790646)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230225204828005.png)]

Nacos原理

img

nacos的server和客户端client之间的关系采取推拉结合,一方面client会通过定时任务每隔10s向nacos发起查询请求,nacos会通过阻塞队列异步地处理这些请求,如果列表改变则会返回新列表。另一方面,当本地实例发送变化时(也就是心跳停止断开连接),nacos会主动通过UDP协议推送到Client,并实时地通过UDP推送到Client,为防止UDP数据丢失,client会通过定时任务每隔10s向nacos发送拉取请求,当服务列表改变nacos再返回

具体的源码可以先放着,参考链接:https://blog.csdn.net/qq_40011574/article/details/127343825

Nacos的注册表结构是什么样的?

是一个多级存储模型,最外层通过namespace来实现隔离,然后通过group分组,分组下面就是具体的服务,一个服务可以分为不同的集群,集群有多个实例

Map<String,Map<String,Service>>
  • 1

最外层的key时namespace_id,内层key是group+serviceName

Service内部维护了一个Map,结构是Map<String,Cluster>,key是clusterName,值是集群信息

Cluster内部维护了一个Set集合,元素代表集群的多个实例

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ArtxXr1V-1686838790647)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230226092622153.png)]

Nacos如何保证并发写时的安全?

注册实例时,会对service加锁,不同service就不存在相互影响,并且更新实例列表时,会通过一个异步的单例线程池来完成更新。

Nacos如何保证并发读写的冲突?

Nacos在更新实例列表时,会采用CopyOnWrite技术,将Old实例列表拷贝一份,然后更新拷贝的实例列表,再用新的实例列表覆盖旧的实例列表。

Nacos的热部署注解实现

使用@RefreshScope注解写在类上,该类中使用@Value注解的变量会随nacos配置文件中的值变化而即时变化。

注意:如果采用该方式直接在一个service或者component中定义动态配置,并且仅在service或者component内使用类内部定义的动态配置,则会出现失效问题! 即在controller,service,mapper中@RefreshScope注解失效,此时需要单独创建一个实体类,使用@RefreshScope+@Component注解,在需要使用的地方直接@Autowired注入即可。

使用@ConfigurationProperties注解标注配置类,在通过Nacos发布配置后,配置类会被重新rebind,此时变量动态修改生效,使用的地方通过@Autowired注入。

nacos与Eureka相比优势如下

nacos在自动或手动下线服务,使用消息机制通知客户端,服务实例的修改很快响应;Eureka只能通过任务定时剔除无效的服务。
nacos可以根据namespace命名空间,DataId,Group分组,来区分不同环境(dev,test,prod),不同项目的配置。

链路追踪

链路追踪是指在分布式架构下实现请求链路可视化监控的一种技术。核心目的是了解分布式系统中的请求调用的一个行为,从而从整体到局部的详细展示各个系统的一个指标。

MQ

MQ一条消息的生命周期

1、消息生产阶段:消息被生产者生产之后提交给MQ以及将消息持久化的过程中,只要能正常收到MQ Broker的ack确认响应,就表示发送成功,所有只要处理好返回值和异常,这个阶段是不会出现消息丢失

2、消息存储阶段:这个阶段交由MQ消息中间件来保证,比如将消息进行持久化,存储在日志中。

3、消息消费阶段:消费端从Broker上拉取消息,只要消费端在收到消息后,不立即发送消费确认给Broker,而是等执行完业务逻辑后,再发送消费确认,也能保证消息不丢失。如果mq没有收到确认消息或者消费失败,则重新发送

总结:producer生产者生产消息,发送到绑定了queue队列的exchange交换机上,并进入到指定的queue队列(队列和交换机进行绑定的介质是路由键),最后推送到consumer消费端。

img

MQ交换机路由的方式

Direct

交换机根据routingKey进行完全匹配,如果匹配失败则丢弃消息

Fanout

忽略路由键,进行广播到与交换机绑定的所有队列

Topic

对路由键进行模糊匹配,如果匹配失败则丢弃消息,如果匹配多个结果,则往多个结果都发送消息。

类似于正则表达式, 它有两种语法:“*”,“#”

“**“只匹配当前级别的词,如"person.*“可以匹配"person.name”,无法匹配"person.name.jerry”。无法匹配空,如"person”。

“#“能匹配多个子级别的词,如"person.#“可以匹配"person.name”,也可以匹配"person.name.jerry”。可以匹配空,如"person”。

如何确保消息不丢失

**考虑角度:**要思考消息丢失在哪个阶段,如何去检测到消息真正丢失了

消息发送到mq时丢失:开启confirm确认机制

  • 消息成功发送到交换机 返回ack
  • 消息未投递到交换机,返回nack

发生者回执:

  • 消息投递到交换机了,但没有路由到队列,返回ack以及路由失败的原因

生产者设置confirm模式后,会对每一个消息分配一个唯一ID,如果成功写入rabbitmq中,会回传一个ack消息,如果mq没能成功处理消息,则会回调一个nack接口,告诉生产者消息接受失败,需要重传。根据每个消息id的状态,在一定时间进行判断是否需要重发。

spring:
  rabbitmq:
    publisher-confirms: true #开启confirm机制
    publisher-confirm-type:correlated 确认机制类型 异步回调,定义ConfirmCallback,也就是消息返回结果到这个类进行处理
                           simple 同步等待confirm结果,直到超时
   	template:
   		mandatory:true 路由失败原因
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

CustomMessageSender方法

@Component
public class CustomMessageSender implements RabbitTemplate.ConfirmCallback {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private RedisTemplate redisTemplate;

    private static final String MESSAGE_CONFIRM_KEY="message_confirm_";

	//构造方法,为当前rabbitTemplete添加ConfirmCallback处理类
    public CustomMessageSender(RabbitTemplate rabbitTemplate) {
        this.rabbitTemplate = rabbitTemplate;
        rabbitTemplate.setConfirmCallback(this);
    }

    /**
     * 回调方法
     * @param correlationData 本次操作的唯一标识
     * @param ack 成功/失败标识
     * @param cause
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack){
            //持久化成功
            //删除临时存储空间的内容
            redisTemplate.delete(correlationData.getId());
            redisTemplate.delete(MESSAGE_CONFIRM_KEY+correlationData.getId());
        }else{
            //持久化失败
            //消息重新发送
            Map<String,String> entries = redisTemplate.opsForHash().entries(MESSAGE_CONFIRM_KEY + correlationData.getId());
            String exchange = entries.get("exchange");
            String routingKey = entries.get("routingKey");
            String message = entries.get("message");
            //重新发送
            rabbitTemplate.convertAndSend(exchange,routingKey,message,correlationData);
        }
    }

    //在发送数据的同时,先向redis存一份数据,等到将来confirm方法返回持久化成功后,再将redis数据删除,否则就重新发送
    //自定义消息发送发送
    public void sendMessage(String exchange,String routingKey,String message){

        //向存储空间中存放本次消息的内容
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());

        redisTemplate.boundValueOps(correlationData.getId()).set(message);

        Map<String,String> map = new HashMap<>();
        map.put("exchange",exchange);
        map.put("routingKey",routingKey);
        map.put("message",message);
        redisTemplate.opsForHash().putAll(MESSAGE_CONFIRM_KEY+correlationData.getId(),map);

        //发送消息
        rabbitTemplate.convertAndSend(exchange,routingKey,message,correlationData);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61

消息持久化(mq接收到消息后消息丢失):将消息写入到日志中(前提队列持久化:将 durable 参数置为 true 实现)

如何实现持久化:指定deliveryMode为2或者设置MessageProperties.PERSISTENT_TEXT_PLAIN

必须同时设置队列持久化和消息持久化,再结合生产者的confrim模式,才能保证消息准确投递到broker并保证进入磁盘。

mq确保持久化消息能从服务器重启中恢复的方式是,将它们写入磁盘上的一个持久化日志文件,当发布一条持久性消息到持久交换器上时,Rabbit 会在消息提交到日志文件后才发送响应。一旦消费者从持久队列中消费了一条持久化消息,RabbitMQ 会在持久化日志中把这条消息标记为等待垃圾收集。如果持久化消息在被消费之前 RabbitMQ 重启,那么 Rabbit 会自动重建交换器和队列(以及绑定),并重新发布持久化日志文件中的消息到合适的队列。

消费者丢失消息:

在消费者收到消息的时候,不自动发送确认收到的ack,而是需要等待任务完成之后,后台再返回ack。

ACK确认机制

acknowledge-mode: manual 手动ack 调用api进行发送ack
acknowledge-mode: none 关闭ack 队列投递消息后,不管结果直接删除消息
acknowledge-mode: auto 自动ack 对消费者消费失败的消息进行重复投递 未限制投递次数
  • 1
  • 2
  • 3

MQ消费失败重新入队重试解决方案

利用Spring的retry机制,在消费者出现异常利用本地重试,而不是返回ack或者nack

相关配置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a1AlPFZy-1686838790647)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230225143305359.png)]

当本地失败达到一定次数之后处理策略:

RejectAndDontRequeueRecoverer 丢弃消息,返回reject(拒绝)消息

返回nack,消息重新入队(队列还是原来的队列)

RepublishMessageRecoverer失败消息投递到指定交换机(交换机为error交换机,会发送到指定的消费失败队列进行后续的处理)

RepublishMessageRecoverer具体实例

配置交换机(error.direct)、队列以及相应的本地失败重试机制返回MessageRecovery接口实现类RepublishMessageRecoverer发送一个新的消息到交换机,路由键(error)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SyvMfMvm-1686838790647)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230225144550735.png)]

对于发送到错误队列的消息,其中的head包含了具体的报错信息以及原消息内容

mq死信队列相关

消息到达队列后,消息就开始计时,当无人消费时就进行失效交换机也就是进入死信交换机进入死信队列。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ufkBQF6E-1686838790648)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230225145510646.png)]

当消息本身和队列两者都设置了ttl的超时时间,则以最短的为准

对队列进行设置超时时间的相关配置以及绑定死信交换机

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wAk7Buuu-1686838790648)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230225150112619.png)]

相关参数解读:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bXfz7bUS-1686838790648)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230225150941716.png)]

声明死信交换机、队列监听过期消息

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9ThavBSY-1686838790648)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230225150302608.png)]

如果要设置动态的超时时间,则可以对每个消息进行设置不同的超时时间,具体时间可以根据不同业务来设置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-12kY4MHq-1686838790649)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230225151027366.png)]

mq官方对延迟消息做了一个插件DelayExchange,与原生的Exchange做了功能的升级:

  • DelayExchange接收的消息可以暂存在内存中(Exchange是无法存储消息的)
  • DelayExchange中计时,超时后才投递消息到队列

配置延迟队列

  • 基于注解的方式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IYgF5C3m-1686838790649)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230225152457300.png)]

  • 基于Bean的方式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gdK5Rb8y-1686838790650)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230225152528705.png)]

发送消息时,需要对消息的头部加上一个x-delay的属性 单位为毫秒

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XiMoLgER-1686838790650)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230225152557558.png)]

如何保证rabbitMq顺序消费

在Mq发送消息到交换机以及路由到绑定的队列上时,需要保证的是在生产发送消息的有序性,在消费者从队列中消费消息的时候,保证消费者是有序进行消费消息的

1)RocketMQ 会为每个消息队列建一个对象锁,这样只要线程池中有该消息队列在处理,则需等待处理完才能进行下一次消费,保证在当前 Consumer 内,同一队列的消息进行串行消费。

2)向 Broker 端请求锁定当前顺序消费的队列,防止在消费过程中被分配给其它消费者处理从而打乱消费顺序。

项目使用mq解决的问题

候补订单监控退单事件,当发生退单事件之后,会异步调用mq的查询以及符合条件进行下单的业务。

通过mq解除了候补订单和退单之间的耦合,让退单事件发送之后无需关注mq的内部细节和级联失败的问题。

72、为什么选择mq

市面上存在的消息中间件,常见的就是kafka、Rabbitmq、RocketMQ,对比其中的一些性能,可以发现kafka注重的是吞吐量换消息的可靠性,rabbitmq则在消息的可靠性和消息延迟性能的优势要高于其他两种,kafka和rocketmq支持自定义协议,rabbitmq是固定的协议:amqp等

56、MQ消息重复消费

在消息消费过程中,如果出现失败的情况,会通过补偿的机制,生产者会重复发送一条消息,这个时候就不合理了,就需要保证幂等性

什么是幂等性:比如一个转账系统,A 要转给 B 100 元,当 A 发出消息后,B 接收成功,然后给 MQ 确认的时候出现网络波动,MQ 并没有接收到 ack 确认,那 MQ 为了保证消息被消费,就会继续给消费者投递之前的消息,如果再重复投递 5 次,则 B 在处理 5 次,加上之前的一次,B 的余额增加了 600 元,很明显是不合理的。
解决方法:在真正消费消息之前,加入一个全局ID检测过程,查询ID是否存在在redis中

不存在,则正常消费,消费完毕后写入redis中

存在则直接丢弃消息,不做处理。

备注:生成全局ID的各种方法对比,建议使用Sonwflake算法生成。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cJx4v3Tu-1686838790650)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20220819144940202.png)]

MQ消息(堆积)积压的问题

当生产者发送消息的速度超过了消费者处理消息的速度,就会导致队列中的消息堆积,直到队列存储消息达到上限。最早接收到的消息,可能就会成为死信,会被丢弃,这就是消息堆积问题。

考虑角度:消息积压不就是性能的问题,想要解决的话,就是要知道哪个阶段出现了积压,然后再去解决。

因为消息发送之后才会出现积压的问题,所以和消息生产端没有关系,又因为绝大部分的消息队列单节点都能达到每秒钟几万的处理能力,相对于业务逻辑来说,性能不会出现在中间件的消息存储上面。毫无疑问,出问题的肯定是消息消费阶段。

那么如何解决

  • 如果是线上突发的话,进行临时扩容,增加消费端的数量,降级一些非核心的业务。

排查解决异常问题,通过监控、日志等手段分析是否消费端的业务逻辑代码出现了问题。

  • 扩大队列容积、提高堆积上限
  • 在消费者端开启线程池加快消息处理速度(适合当前业务执行耗时比较长,因为cpu切换线程上下文浪费资源)

惰性队列解决消息堆积

特征:

  • 接收消息后存入磁盘
  • 消费者消费消息时从磁盘加载到内存
  • 支持数百万条消息

运行中的队列变成惰性队列

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GCCyNHdW-1686838790651)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230225154139510.png)]

新建一个惰性队列

系统面对接口突发的大流量保护机制

服务降级和熔断(预防)

降级是指将一些服务临时关闭,用户访问的话则给出一些默认地请求,例如:服务繁忙等提示。

服务器压力剧增的情况下,根据业务情况以及流量对一些服务和页面有策略地降级,以此来缓解服务器地压力。

熔断是指是调用外部接口出现故障而断绝与外部接口的关系。系统发生异常或者延迟或者流量太大,都会触发该服务的服务熔断措施,链路熔断,返回兜底方法。这是对局部的一种保险措施。

限流

限流:需要参考一些指标(QPS服务端每秒能够响应的客户端查询请求数量、HPS服务端每秒能够收到的请求数量)

概念是:对请求量设置一个阈值,当达到阈值会根据限流算法进行不同的限制策略

常见的限流算法有漏桶算法、计数器算法、令牌桶算法。

计数器算法是最简单的:核心原理就是利用一个int值

Sentinel vs Hystrix

Hystrix的侧重点在于:隔离和熔断为主,并提供fallback机制

Sentinel 的侧重点在于:多样化的流量控制,熔断降级、系统负载保护、实时监控和控制台

JVM

一个Object对象的存储结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7GBpwuUp-1686838790651)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230226154416077.png)]

对其填充中,java规定一个对象的大小为8的倍数的字节,为了避免伪共享的问题。

类加载的过程加载、连接、初始化

加载:将类文件读入内存并创建一个对象实例(通过类的全限定类名获取定义此类的二进制字节流并将这个二进制流转化成方法区的运行区数据结构并在内存中生成一个class对象作为方法区的入口),这个过程是可控的

在该过程中,并没有指明去哪个类中获取二进制流数据和如何获取,所以在这个基础上衍生了很多的相关技术,例如压缩文件、网络中获取Web Applet

连接:

​ 验证:确认加载的class文件的字节流信息符合jvm规范的全部约束(包含文件格式验证、元数据验证、字节码验证、符号引用验证)

​ 准备:为类的静态变量分配内存空间并初始化。

​ 解析:将常量池的符号引用(任何形式的字面量)替换成直接引用(直接指向的指针、偏移量)的一个过程。

初始化:正式执行java内的方法

触发Class初始化时机

只有在首次调用一个类或接口时才会初始化

1、创建类的实例

2、调用静态方法

3、为类的静态变量赋值

4、初始化某个子类

5、通过反射调用类

类加载器有哪些

主要负责的是将.class文件加载到内存中并生成一个class对象

1、根类加载器:负责java核心类库的加载,例如String

2、扩展类加载器:负责JRE的扩展目录中jar包的加载

3、系统类(应用类)加载器:负责JVM启动时加载来自java命令的class文件(加载自定义的一些类)

反射

通过反射,我们可以在运行时某个类的所有方法、属性以及调用某个对象的所有方法和属性

通过反射获取Class对象的三种方式

一、调用Object类的getClass方法

二、调用某个类的class属性来获取该类的Class对象

Class s = Student.class
  • 1

三、调用Class的静态方法forName(String 类的全限定类名)

Class s = Class.forName("com.nl.nilei.Student")
  • 1

双亲委派机制

源码解析:ClassLoader.class
/*加载具有指定二进制名称的类。该方法的默认实现按以下顺序搜索类: 调用findloaddclass(String)检查类是否已经加载。 在父类装入器上调用loadClass方法。如果父类为空,则使用虚拟机内置的类装入器。 调用findClass(String)方法来查找类。 如果使用上述步骤找到该类,并且resolve标志为true,则该方法将在结果class对象上调用resolveClass(class)方法。 ClassLoader的子类被鼓励重写findClass(String),而不是这个方法。 除非重写,否则该方法在整个类加载过程中同步getClassLoadingLock方法的结果。*/
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

总结:类加载器加载类时,会向上委派类去上一级类加载器去找类,这个过程中会查找缓存中是否已经加载了该类否则直到委托到核心类加载器,也就是核心类加载器查找核心类库中是否存在该类,如果没有找到则向下查找,也就是扩展类加载器,加载JRE中jar包是否存在,否则向下查找系统类加载器,未找到则报错

img

起到的作用:防止篡改核心类库,以及类重复加载

如何打破双亲委派机制

自定义类加载器打破,原理:自定义一个ClassLoader,重写findClass和LoadClass方法

全盘委托机制:

也就是说当一个类加载器加载某类时,会存在可以被其他类所加载的其他类,但是这个类还是会被本类的加载器负责加载。其原本的加载器也会加载。

意思也就是一个类的其他类会被当前类加载器和所属类加载器都进行加载,例如,本类中存在一个String类,本类会被应用类加载器加载也会被核心类加载器加载

运行时数据区

img

其中的元空间(方法区),在1.8之前叫做永久代,1.8之后叫元空间,元空间不使用JVM的内存,而使用本地内存。本地方法区存放的是本地的方法

通过一个类的加载过程来描述运行时数据区的组成:

一个类被类加载器加载后,未被加载过的类的相关信息(常量池,类信息)都会存放在元空间,创建的类实例会存放在堆中,当前线程的每个方法会创建自己的虚拟机栈栈帧,存放的是方法的局部变量、数据,每个栈帧存放着:

局部变量表:存放着方法的形参、局部变量值、返回值,其大小编译期确定

**操作数栈:**后进先出的栈,用来保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间

动态链接:指向运行时常量池(也就是在加载时所产生的在内存中存放类的字节码信息)中的方法引用

程序计数器存放着指针表示当前程序执行的位置,方便线程切换后恢复程序执行。

对于热点方法调用,或者频繁的循环代码,由 JIT 即时编译器将这些代码编译成机器码缓存,提高执行性能

其中,容易发生内存溢出的区域

outofMemoryError:

  • 堆内存 - 对象越来越多又一直在使用,不能垃圾回收
  • 方法区内存耗尽:加载的类越来越多
  • 虚拟机栈累计:创建的线程个数越来越多又不能销毁

StackOverFlowError:

  • 虚拟机栈:虚拟机栈有方法递归调用未正确结束

元空间的类相关信息何时被清理

当类的引用长时间未被使用以及相关的类加载器不被使用时,整个元空间内相关类的信息才会被清理

JVM内存参数

-Xmx10240m 最大内存空间 10G

-Xms10240m 最小内存空间 10G

-Xmn5120m 新生代内存空间 5G

-XX:SurvivorRatio = 3 意思就是eden占 6 份,survivor区占4份

GC垃圾回收算法

标记清除

  • 产生内存碎片

标记整理

  • 需要移动元素,执行效率慢

标记复制

  • 需要双倍的存储空间

分代回收算法:

堆中内存分为两块:新生代和老年代

其中新生代又分为三块,伊甸园区、from、to区

当新生代内存不足时触发minor gc,经过gc处理后存活的对象会通过标记复制算法存放到from区,存活年龄+1,如果再次触发minor gc时,from区存活的对象再次放到to区,在这两个区来回存放,直到对象存活年龄达到15时移到老年代。老年代内存不足时触发full gc

三色标记法

对对象进行标记的过程中,根据gc root 引用链进行标记,分为三个状态:

白色:未被标记

灰色:正在被标记

黑色:已经被标记

内存溢出和内存泄漏的区别

内存溢出:超出规定的内存区域而导致报错

内存泄漏:内存中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费

虚拟机栈相关知识

1、每个线程在创建时,会创建一个虚拟机栈并分配内存空间,一旦线程的个数过多导致内存无法进行分配空间,则抛出outofmemoryError。其内部存着一个个的栈帧,对应一个个的方法,每个栈帧会占据一定的空间,一旦超出虚拟机栈空间大小,会抛出stackoverError。
2、虚拟机栈中主要管的是运行时的存储,存放的是方法的局部变量(8种基本的数据类型)以及对象的引用、部分的结果、参与方法的调用与返回等。
3、栈不存在垃圾回收

方法区

类加载只能同时加载一个类,也就是单例加载。

关闭JVM即关闭这个区域

存储内容:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XmoHH1l2-1686838790651)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20220812231719554.png)]

运行时常量池(运行时产生的字面量、符号引用等)与字符串常量池为两个概念(jdk1.7之前放置在元空间,jdk1.8之后存放在堆中,方便进行gc回收)

字符串常量池和运行时常量池

运行时常量池:每个class文件只有一份,存放的是经过类加载器加载之后产生的符号引用值和字面量,在经过解析过程之后,符号引用会被替换成直接引用,以保持与字符串常量池一致

字符串常量池(String Pool):存放的是字符串常量的引用值

堆按对象的生存时间分为新生代、老年代和永久代。

新生代、老年代的垃圾回收机制:

new对象会先放在伊甸园区,伊甸园区的内存填满了之后,程序是首先进行minor gc回收伊甸园区的内存,经历垃圾回收之后存活的对象会进入会进入f0。当再次进行垃圾回收时,存活的对象会进入f1。循环网速直到存活的对象年龄达到阈值。则会进入老年代。当老年代的对象存满后会触发major gc回收机制
  • 1

新生代进入老年代的机制:

当年龄达到阈值
对象占据内存大的可能直接进入老年代
动态对象年龄判断机制(年龄1的占用了33%,年龄2的占用了33%,累加和超过默认的TargetSurvivorRatio(50%),年龄2和年龄3的对象都要晋升。)
空间担保机制(是指minor gc之后老年代的内存空间 与 以往新生代minor gc后进入老年代对象的平均大小相比)
  • 1
  • 2
  • 3
  • 4

编译器

编译器由编译器和JIT编译器(即时编译器)组成。

JIT编译器 = 热点代码编译器,将热点代码的二进制码转换成机器码,供主编译器使用。

编译器的优化:逃逸分析和标量替换、锁消除。

逃逸分析:即一个对象只在方法内部进行使用,不被方法之外进行引用的话,编译器进行逃逸分析之后会将对象保存在栈中,所以对象不一定只保存在堆中。

永久代为什么变为元空间(即将其从堆内移到了堆外)

为了能够存储更多的类信息,移到堆外占用的是整个系统的内存

为了统一版本

运行时常量池、字符串常量池移到堆内?

为了更方便gc回收常量池,堆外的元空间回收频率低。

以jvm的角度看创建对象的过程

1、虚拟机遇到一条new指令时,首先会检查这个类的参数能不能在常量池中定位到一个类的符号引用,并检查这个符号引用所代表的类是否被加载、解析、初始化

2、类加载检查过后,虚拟机会为新的对象分配内存空间,分配的方式可以为指针碰撞法

3、内存分配完成之后,虚拟机将分配的内存空间初始化成零值,其中不包括对象头,这一步操作保证对象能不初始化就能被使用

4、对对象进行设置对象头的相关属性,以便存储对象信息

5、执行初始化的方法,为属性赋值和执行构造方法

JVM内存结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f2VQlrQQ-1686838790652)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20230226100300757.png)]

GC判断对象是否存活的算法

1)循环引用计数算法(对引用对象赋值一个标识,但对于循环引用有不可回收的缺点,如图)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eDtGQ3MZ-1686838790652)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20220815090740709.png)]

2)可达性分析法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XlcRIYwj-1686838790652)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20220815090932105.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GGni5VNG-1686838790653)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20220815091042876.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sRwShhx1-1686838790653)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20220815091003266.png)]

34、GC垃圾回收算法

标记清除法

复制算法

标记压缩法

分区算法

分代算法

增量算法

35、垃圾回收器(分类)

串行回收:同一时间段内只允许有一个CPU用于执行垃圾手术操作,此时工作线程被暂停,直至垃圾收集工作结束。

并行回收:可以运行多个CPU同时执行垃圾回收,提升了应用的吞吐量,但仍然采用独占式,工作线程等待GC执行完成。

并发:垃圾和应用程序 交替运行

独占:只有垃圾线程运行

JMM概述

JMM(java内存模型)定义了线程与内存之间的关系,规定了每个线程拥有自己的工作内存,其中保存了主内存共享变量的副本,线程对共享变量的所有操作都直接针对工作内存进行,而不能直接读取主内存。主内存和工作内存之间进行同步。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成

那么工作内存(本地内存)是存放在哪呢?其实是JMM的抽象概念,并不真实存在。本地内存涵盖了缓存,写缓存区、寄存器以及其他的硬件和编译器优化后的一个数据存放位置。

举例说明

本地内存A和B有主内存中共享变量x的副本。假设初始时,这三个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。

从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。

计算机网络

36、TCP/IP协议的三次握手、四次挥手

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wnVCoNKs-1686838790654)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20220815134412771.png)]

三次握手的过程,由客户端向服务端发送syn包请求连接,服务端携带syn并加ACK确认包返回到客户端,客户端回复ack确认该连接有效,三次握手完成建立连接

不用二次握手的原因:在发送第一次发送syn包之后可能因为未知的问题导致不能到达服务端产生了滞留,客户端超时重新发送syn包,服务端接收到请求并返回确认ack包建立连接,但此时信道恢复,服务端接受的syn包以为需要再次建立连接,经过二次握手之后,服务端进入等待消息状态,而客户端则不知道此连接,导致双方建立的不可靠的连接。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cr9gVk8u-1686838790654)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20220815135030636.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tIlSiQ2J-1686838790654)(C:\Users\nilei\AppData\Roaming\Typora\typora-user-images\image-20220815135839709.png)]

四次挥手的过程:

1、客户端发送fin包请求,自己进入中止等待状态

2、服务端收到请求并发送ack包确认请求,服务端进入关闭等待状态,此时服务端还可以发送未发生的数据,客户端可以接收数据

3、待服务端发送完所有数据后,发送fin包到客户端,进入最后的确认状态

4、客户端接收fin包并返回ack包进入超时等待状态,客户端接收ack直接关闭

最后的超时等待存在的原因:避免ack包丢失,服务端不能正常接收,服务端没有接收ack包的确认,则会重新发起fin包,则客户端接收并刷新超时等待时间

为什么不是三次挥手?

三次挥手考虑将Ack+Fin一起发送。在服务端收到客户端发送过来的关闭请求时,这时服务端的数据未传输完,为避免客户端因传输数据过大等待超时,则要先发一个fin包通知客户端已经收到,然后继续传输文件才会保证不会超时。如果只有三次挥手,则客户端可能会超时,不断重发fin包。

37、计算机网络的五层结构

1、物理层:建立设备之间的物理连接,同时传输的是电平信息(也就是0,1的电信号)

设备:网线、集线器

2、数据链路层:传输的单位为帧,也称为以太网数据包,设备之间通过MAC地址以及处于同一子网络进行广播的方式进行传输数据。

以太网数据包格式:头部head(18字节) + 数据部分Data(最短46字节,最长1500字节)

设备:网卡

3、网络层:单位为数据包,产生了ip地址以及子网掩码,确定是否为同一子网络。ip地址总共32为,前24位地址为网络,后8位为主机。

IP数据包:标头(20-60字节) + 数据包(最大65535字节)

ip协议规定了IP数据包的格式,发送出去是放在以太网数据包的数据部分中,则需要包括两个部分:对方的ip地址和mac地址

如何确定对方的MAC地址:第一种情况:处于同一子网络,可以根据ARP协议来得到MAC地址(具体过程:ARP协议发出一个数据包放在以太网数据包中,其中包含了对方的ip地址,MAC地址中存的是广播地址,所处的所有子网络都能收到,并根据ip地址相比较,如果相同则做出回复,报告自己的MAC地址,否则丢失包,通过ARP协议的数据包就可以得知对方的MAC地址了)

第二种情况:不处于同一子网络,将数据包传到两个子网络连接的网关,网关进行处理

子网掩码:判断两个ip地址是否处于同一子网络,将ip地址与子网掩码与操作,得到结果一致与否

设备:路由器

4、传输层

该层作用是建立程序之间端口对端口的通讯

该层加入了两个协议:UDP和TCP协议 其数据包内嵌在ip数据包的数据部分

UDP数据包:标头(发出端口和接收端口 8字节)+数据包(具体的内容 65535字节)

TCP数据包:无长度限制,理论无限长,但是保证传输效率,通常不超过IP数据包的长度

5、应用层

规定应用程序的数据格式,也就是HTTP协议的具体实现

66、HTTP常见的状态码

1)1XX(信息类)
100 接受的请求正在处理
  • 1
2)2XX(成功类)
2XX 成功处理了请求
  • 1
3)3XX(重定向)
301,永久性重定向,标识资源已被分配了新的URL
302,临时性重定向,标识资源临时被分配了新的URL
303,表示资源存在另一个URL,用GET方法获取资源
304,自从上次请求后,请求网页未修改过,服务器返回此响应前,不会返回网页内容
  • 1
  • 2
  • 3
  • 4
4)4xx(客户端错误)
400,服务器不理解请求的语法,(请求参数出现问题)
401、表示发送的请求需要有通过HTTP认证的认证信息
403(禁止)、服务器拒绝请求
404、服务器找不到网页
  • 1
  • 2
  • 3
  • 4
5)5XX(服务器错误)
500,服务器遇到错误,无法完成请求
503,服务器停机维护或超负载
  • 1
  • 2

前端

40、js的定时任务

setInterval  开启定时任务
clearInterval  关闭定时任务
  • 1
  • 2
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/羊村懒王/article/detail/422360
推荐阅读
相关标签
  

闽ICP备14008679号