Java面试宝典1.0
2022-06-18 14:46:48 3 举报
AI智能生成
Java面试大全1.0版本。
作者其他创作
大纲/内容
Java基础
介绍一下 java 吧
java 是一门开源的跨平台的面向对象的计算机语言.
跨平台是因为 java 的 class 文件是运行在虚拟机上的,其跨平台是指虚拟机在不同平台有不同版本」,所以说 java 是跨平台的.
面向对象有几个特点:
1.「封装」
两层含义:一层含义是把对象的属性和行为看成一个密不可分的整体,将这两者'封装'在一个不可分割的「独立单元」(即对象)中
另一层含义指'信息隐藏,把不需要让外界知道的信息隐藏起来,有些对象的属性及行为允许外界用户知道或使用,但不允许更改,而另一些属性或行为,则不允许外界知晓,或只允许使用对象的功能,而尽可能「隐藏对象的功能实现细节」。
「优点」:
1.良好的封装能够「减少耦合」,符合程序设计追求'高内聚,低耦合'
2.「类内部的结构可以自由修改」
3.可以对成员变量进行更「精确的控制」
4.「隐藏信息」实现细节
两层含义:一层含义是把对象的属性和行为看成一个密不可分的整体,将这两者'封装'在一个不可分割的「独立单元」(即对象)中
另一层含义指'信息隐藏,把不需要让外界知道的信息隐藏起来,有些对象的属性及行为允许外界用户知道或使用,但不允许更改,而另一些属性或行为,则不允许外界知晓,或只允许使用对象的功能,而尽可能「隐藏对象的功能实现细节」。
「优点」:
1.良好的封装能够「减少耦合」,符合程序设计追求'高内聚,低耦合'
2.「类内部的结构可以自由修改」
3.可以对成员变量进行更「精确的控制」
4.「隐藏信息」实现细节
2.「继承」
继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
「优点」:
1.提高类代码的「复用性」
2.提高了代码的「维护性」
继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
「优点」:
1.提高类代码的「复用性」
2.提高了代码的「维护性」
3.「多态」
1.「方法重载」:在一个类中,允许多个方法使用同一个名字,但方法的参数不同,完成的功能也不同。
2.「对象多态」:子类对象可以与父类对象进行转换,而且根据其使用的子类不同完成的功能也不同(重写父类的方法)。
多态是同一个行为具有多个不同表现形式或形态的能力。Java语言中含有方法重载与对象多态两种形式的多态:
「优点」
「消除类型之间的耦合关系」
「可替换性」
「可扩充性」
「接口性」
「灵活性」
「简化性」
1.「方法重载」:在一个类中,允许多个方法使用同一个名字,但方法的参数不同,完成的功能也不同。
2.「对象多态」:子类对象可以与父类对象进行转换,而且根据其使用的子类不同完成的功能也不同(重写父类的方法)。
多态是同一个行为具有多个不同表现形式或形态的能力。Java语言中含有方法重载与对象多态两种形式的多态:
「优点」
「消除类型之间的耦合关系」
「可替换性」
「可扩充性」
「接口性」
「灵活性」
「简化性」
面向对象和面向过程的区别?
面向过程:是分析解决问题的步骤,然后用函数把这些步骤一步一步地实现,然后在使用的时候一
一调用则可。性能较高,所以单片机、嵌入式开发等一般采用面向过程开发
一调用则可。性能较高,所以单片机、嵌入式开发等一般采用面向过程开发
面向对象:是把构成问题的事务分解成各个对象,而建立对象的目的也不是为了完成一个个步骤,
而是为了描述某个事物在解决整个问题的过程中所发生的行为。面向对象有封装、继承、多态的特
性,所以易维护、易复用、易扩展。可以设计出低耦合的系统。 但是性能上来说,比面向过程要
低。
而是为了描述某个事物在解决整个问题的过程中所发生的行为。面向对象有封装、继承、多态的特
性,所以易维护、易复用、易扩展。可以设计出低耦合的系统。 但是性能上来说,比面向过程要
低。
JDK 和 JRE 有什么区别?
JDK:Java Development Kit 的简称,java 开发工具包,提供了 java 的开发环境和运行环境。
JRE:Java Runtime Environment 的简称,java 运行环境,为 java 的运行提供了所需环境。
具体来说 JDK 其实包含了 JRE,同时还包含了编译 java 源码的编译器 javac,还包含了很多 java 程序调试和分析的工具。简单来说:如果你需要运行
java 程序,只需安装 JRE 就可以了,如果你需要编写 java 程序,需要安装 JDK。
java 程序,只需安装 JRE 就可以了,如果你需要编写 java 程序,需要安装 JDK。
八种基本数据类型的大小,以及他们的封装类?
1.int是基本数据类型,Integer是int的封装类,是引用类型。int默认值是0,而Integer默认值
是null,所以Integer能区分出0和null的情况。一旦java看到null,就知道这个引用还没有指向某个
对象,再任何引用使用前,必须为其指定一个对象,否则会报错。
是null,所以Integer能区分出0和null的情况。一旦java看到null,就知道这个引用还没有指向某个
对象,再任何引用使用前,必须为其指定一个对象,否则会报错。
2.基本数据类型在声明时系统会自动给它分配空间,而引用类型声明时只是分配了引用空间,
必须通过实例化开辟数据空间之后才可以赋值。数组对象也是一个引用对象,将一个数组赋值给另
一个数组时只是复制了一个引用,所以通过某一个数组所做的修改在另一个数组中也看的见。
必须通过实例化开辟数据空间之后才可以赋值。数组对象也是一个引用对象,将一个数组赋值给另
一个数组时只是复制了一个引用,所以通过某一个数组所做的修改在另一个数组中也看的见。
虽然定义了boolean这种数据类型,但是只对它提供了非常有限的支持。在Java虚拟机中没有
任何供boolean值专用的字节码指令,Java语言表达式所操作的boolean值,在编译之后都使用Java
虚拟机中的int数据类型来代替,而boolean数组将会被编码成Java虚拟机的byte数组,每个元素
boolean元素占8位。这样我们可以得出boolean类型占了单独使用是4个字节,在数组中又是1个字
节。使用int的原因是,对于当下32位的处理器(CPU)来说,一次处理数据是32位(这里不是指的
是32/64位系统,而是指CPU硬件层面),具有高效存取的特点。
任何供boolean值专用的字节码指令,Java语言表达式所操作的boolean值,在编译之后都使用Java
虚拟机中的int数据类型来代替,而boolean数组将会被编码成Java虚拟机的byte数组,每个元素
boolean元素占8位。这样我们可以得出boolean类型占了单独使用是4个字节,在数组中又是1个字
节。使用int的原因是,对于当下32位的处理器(CPU)来说,一次处理数据是32位(这里不是指的
是32/64位系统,而是指CPU硬件层面),具有高效存取的特点。
什么是包装类?为什么需要包装类?
「Java 中有 8 个基本类型,分别对应的 8 个包装类」
byte -- Byte
boolean -- Boolean
short -- Short
char -- Character
int -- Integer
long -- Long
float -- Float
double -- Double
byte -- Byte
boolean -- Boolean
short -- Short
char -- Character
int -- Integer
long -- Long
float -- Float
double -- Double
「为什么需要包装类」:
基本数据类型方便、简单、高效,但泛型不支持、集合元素不支持
不符合面向对象思维
包装类提供很多方法,方便使用,如 Integer 类 toHexString(int i)、parseInt(String s) 方法等等
基本数据类型方便、简单、高效,但泛型不支持、集合元素不支持
不符合面向对象思维
包装类提供很多方法,方便使用,如 Integer 类 toHexString(int i)、parseInt(String s) 方法等等
Integer a = 1000,Integer b = 1000,a==b 的结果是什么?那如果 a,b 都为1,结果又是什么?
Integer a = 1000,Integer b = 1000,a==b 结果为「false」
Integer a = 1,Integer b = 1,a==b 结果为「true」
这道题主要考察 Integer 包装类缓存的范围,「在-128~127之间会缓存起来」,比较的是直接缓存的数据,在此之外比较的是对象
Integer a = 1,Integer b = 1,a==b 结果为「true」
这道题主要考察 Integer 包装类缓存的范围,「在-128~127之间会缓存起来」,比较的是直接缓存的数据,在此之外比较的是对象
3*0.1 == 0.3返回值是什么?
false,因为有些浮点数不能完全精确的表示出来.
a=a+b与a+=b有什么区别吗?
+= 操作符会进行隐式自动类型转换,此处a+=b隐式的将加操作的结果类型强制转换为持有结果的类
型,而a=a+b则不会自动进行类型转换
型,而a=a+b则不会自动进行类型转换
标识符的命名规则?
标识符的含义: 是指在程序中,我们自己定义的内容,譬如,类的名字,方法名称以及变量名称等
等,都是标识符。
命名规则:(硬性要求) 标识符可以包含英文字母,0-9的数字,$以及_ 标识符不能以数字开头 标
识符不是关键字
命名规范:(非硬性要求) 类名规范:首字符大写,后面每个单词首字母大写(大驼峰式)。 变量
名规范:首字母小写,后面每个单词首字母大写(小驼峰式)。 方法名规范:同变量名。
等,都是标识符。
命名规则:(硬性要求) 标识符可以包含英文字母,0-9的数字,$以及_ 标识符不能以数字开头 标
识符不是关键字
命名规范:(非硬性要求) 类名规范:首字符大写,后面每个单词首字母大写(大驼峰式)。 变量
名规范:首字母小写,后面每个单词首字母大写(小驼峰式)。 方法名规范:同变量名。
Java自动装箱与拆箱
装箱就是自动将基本数据类型转换为包装器类型(int-->Integer);调用方法:Integer的
valueOf(int) 方法
拆箱就是自动将包装器类型转换为基本数据类型(Integer-->int)。调用方法:Integer的
intValue方法
valueOf(int) 方法
拆箱就是自动将包装器类型转换为基本数据类型(Integer-->int)。调用方法:Integer的
intValue方法
try catch finally,try里有return,finally还执行么?
1、不管有木有出现异常,finally块中代码都会执行;
2、当try和catch中有return时,finally仍然会执行;
3、finally是在return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的
值保存起来,管finally中的代码怎么样,返回的值都不会改变,任然是之前保存的值),所以函数
返回值是在finally执行前确定的;
4、finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值。
2、当try和catch中有return时,finally仍然会执行;
3、finally是在return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的
值保存起来,管finally中的代码怎么样,返回的值都不会改变,任然是之前保存的值),所以函数
返回值是在finally执行前确定的;
4、finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值。
== 和 equals 的区别是什么?
== 对于基本类型来说是值比较,对于引用类型来说是比较的是引用;而 equals 默认情况下是引用比较,只是很多类重新了 equals 方法,比如 String、Integer 等把它变成了值比较,所以一般情况下 equals 比较的是值是否相等。
hashcode的作用?
java的集合有两类,一类是List,还有一类是Set。前者有序可重复,后者无序不重复。当我们在set
中插入的时候怎么判断是否已经存在该元素呢,可以通过equals方法。但是如果元素太多,用这样
的方法就会比较满。
于是有人发明了哈希算法来提高集合中查找元素的效率。 这种方式将集合分成若干个存储区域,每
个对象可以计算出一个哈希码,可以将哈希码分组,每组分别对应某个存储区域,根据一个对象的
哈希码就可以确定该对象应该存储的那个区域。
hashCode方法可以这样理解:它返回的就是根据对象的内存地址换算出的一个值。这样一来,当
集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理
位置上。如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如
果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了,不相
同就散列其它的地址。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。
中插入的时候怎么判断是否已经存在该元素呢,可以通过equals方法。但是如果元素太多,用这样
的方法就会比较满。
于是有人发明了哈希算法来提高集合中查找元素的效率。 这种方式将集合分成若干个存储区域,每
个对象可以计算出一个哈希码,可以将哈希码分组,每组分别对应某个存储区域,根据一个对象的
哈希码就可以确定该对象应该存储的那个区域。
hashCode方法可以这样理解:它返回的就是根据对象的内存地址换算出的一个值。这样一来,当
集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理
位置上。如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如
果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了,不相
同就散列其它的地址。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。
为什么重写equals方法也要重写hashcode方法
final有哪些用法?
1.被final修饰的类不能被继承
2.被final修饰的方法不能被重写
3.被final修饰的变量不能被更改,引用类型是引用不能改
4.被final修饰的常量会在编译阶段放入常量池
2.被final修饰的方法不能被重写
3.被final修饰的变量不能被更改,引用类型是引用不能改
4.被final修饰的常量会在编译阶段放入常量池
static都有哪些用法?
1.静态变量
2.静态方法
3.静态代码块
4.静态内部类
5.静态导包
2.静态方法
3.静态代码块
4.静态内部类
5.静态导包
普通内部类和静态内部类的区别?
1、普通内部类和静态内部类的区别:
a)普通内部类持有对外部类的引用,静态内部类没有持有外部类的引用。
b)普通内部类能够访问外部类的静态和非静态成员,静态内部类不能访问外部类的非静态成员,他只能访问外部类的静态成员。
c)一个普通内部类不能脱离外部类实体被创建,且可以访问外部类的数据和方法,因为他就在外部类里面。
2、区别还有
a)第一,内部类可以访问其所在类的属性(包括所在类的私有属性),内部类创建自身对象需要先创建其所在类的对象
b)第二,可以定义内部接口,且可以定义另外一个内部类实现这个内部接口
c)第三,可以在方法体内定义一个内部类,方法体内的内部类可以完成一个基于虚方法形式的回调操作
d)第四,内部类不能定义static元素
e)第五,内部类可以多嵌套
f)static内部类是内部类中一个比较特殊的情况,Java文档中是这样描述static内部类的:一旦内部类使用static修饰,那么此时这个内部类就升级为顶类。也就是说,除了写在一个类的内部以外,static内部类具备所有外部类的特性(和外部类完全一样)。
a)普通内部类持有对外部类的引用,静态内部类没有持有外部类的引用。
b)普通内部类能够访问外部类的静态和非静态成员,静态内部类不能访问外部类的非静态成员,他只能访问外部类的静态成员。
c)一个普通内部类不能脱离外部类实体被创建,且可以访问外部类的数据和方法,因为他就在外部类里面。
2、区别还有
a)第一,内部类可以访问其所在类的属性(包括所在类的私有属性),内部类创建自身对象需要先创建其所在类的对象
b)第二,可以定义内部接口,且可以定义另外一个内部类实现这个内部接口
c)第三,可以在方法体内定义一个内部类,方法体内的内部类可以完成一个基于虚方法形式的回调操作
d)第四,内部类不能定义static元素
e)第五,内部类可以多嵌套
f)static内部类是内部类中一个比较特殊的情况,Java文档中是这样描述static内部类的:一旦内部类使用static修饰,那么此时这个内部类就升级为顶类。也就是说,除了写在一个类的内部以外,static内部类具备所有外部类的特性(和外部类完全一样)。
java中操作字符串都有哪些类,他们的区别?
String,StringBuffer,StringBuilder
String 和 StringBuffer、StringBuilder 的区别在于 String 声明的是不可变的对象,每次操作都会生成新的 String 对象,然后将指针指向新的 String 对象,而 StringBuffer、StringBuilder 可以在原有对象的基础上进行操作,所以在经常改变字符串内容的情况下最好不要使用 String。
StringBuffer 和 StringBuilder 最大的区别在于,StringBuffer 是线程安全的,而 StringBuilder 是非线程安全的,但 StringBuilder 的性能却高于 StringBuffer,所以在单线程环境下推荐使用 StringBuilder,多线程环境下推荐使用 StringBuffer。
String str="i"与 String str=new String("i")一样吗?
不一样,因为内存的分配方式不一样。String str="i"的方式,java 虚拟机会将其分配到常量池中;而 String str=new String("i") 则会被分到堆内存中。
如何将字符串反转?
使用 StringBuilder 或者 stringBuffer 的 reverse() 方法。
String 类的常用方法都有那些?
indexOf():返回指定字符的索引。
charAt():返回指定索引处的字符。
replace():字符串替换。
trim():去除字符串两端空白。
split():分割字符串,返回一个分割后的字符串数组。
getBytes():返回字符串的 byte 类型数组。
length():返回字符串长度。
toLowerCase():将字符串转成小写字母。
toUpperCase():将字符串转成大写字符。
substring():截取字符串。
equals():字符串比较。
charAt():返回指定索引处的字符。
replace():字符串替换。
trim():去除字符串两端空白。
split():分割字符串,返回一个分割后的字符串数组。
getBytes():返回字符串的 byte 类型数组。
length():返回字符串长度。
toLowerCase():将字符串转成小写字母。
toUpperCase():将字符串转成大写字符。
substring():截取字符串。
equals():字符串比较。
java 有哪些数据类型?
1.「基本数据类型」
byte,short,int,long属于数值型中的整数型
float,double属于数值型中的浮点型
char属于字符型
boolean属于布尔型
基本数据有「八个」,
byte,short,int,long属于数值型中的整数型
float,double属于数值型中的浮点型
char属于字符型
boolean属于布尔型
基本数据有「八个」,
「引用数据类型」
引用数据类型有「三个」,分别是类,接口和数组
引用数据类型有「三个」,分别是类,接口和数组
抽象类必须要有抽象方法吗?
不需要,抽象类不一定非要有抽象方法。
接口和抽象类有什么区别?
1.接口是抽象类的变体,「接口中所有的方法都是抽象的」。而抽象类是声明方法的存在而不去实现它的类。
2.接口可以多继承,抽象类不行。
3.接口定义方法,不能实现,默认是 「public abstract」,而抽象类可以实现部分方法。
4.接口中基本数据类型为 「public static final」 并且需要给出初始值,而抽类象不是的。
2.接口可以多继承,抽象类不行。
3.接口定义方法,不能实现,默认是 「public abstract」,而抽象类可以实现部分方法。
4.接口中基本数据类型为 「public static final」 并且需要给出初始值,而抽类象不是的。
普通类和抽象类有什么区别
普通类不能包含抽象方法,抽象类可以包含抽象方法。
抽象类不能直接实例化,普通类可以直接实例化。
抽象类不能直接实例化,普通类可以直接实例化。
重载和重写什么区别?
重写:
1.参数列表必须「完全与被重写的方法」相同,否则不能称其为重写而是重载.
2.「返回的类型必须一致与被重写的方法的返回类型相同」,否则不能称其为重写而是重载。
3.访问「修饰符的限制一定要大于被重写方法的访问修饰符」
4.重写方法一定「不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常」。
1.参数列表必须「完全与被重写的方法」相同,否则不能称其为重写而是重载.
2.「返回的类型必须一致与被重写的方法的返回类型相同」,否则不能称其为重写而是重载。
3.访问「修饰符的限制一定要大于被重写方法的访问修饰符」
4.重写方法一定「不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常」。
重载:
1.必须具有「不同的参数列表」;
2.可以有不同的返回类型,只要参数列表不同就可以了;
3.可以有「不同的访问修饰符」;
4.可以抛出「不同的异常」;
1.必须具有「不同的参数列表」;
2.可以有不同的返回类型,只要参数列表不同就可以了;
3.可以有「不同的访问修饰符」;
4.可以抛出「不同的异常」;
Excption与Error包结构?
Java可抛出(Throwable)的结构分为三种类型:被检查的异常(CheckedException),运行时异常
(RuntimeException),错误(Error)。
(RuntimeException),错误(Error)。
常见的异常有哪些?
NullPointerException 空指针异常
ArrayIndexOutOfBoundsException 索引越界异常
InputFormatException 输入类型不匹配
SQLException SQL异常
IllegalArgumentException 非法参数
NumberFormatException 类型转换异常 等等....
ArrayIndexOutOfBoundsException 索引越界异常
InputFormatException 输入类型不匹配
SQLException SQL异常
IllegalArgumentException 非法参数
NumberFormatException 类型转换异常 等等....
异常要怎么解决?
Java标准库内建了一些通用的异常,这些类以Throwable为顶层父类。
Throwable又派生出「Error类和Exception类」。
错误:Error类以及他的子类的实例,代表了JVM本身的错误。错误不能被程序员通过代码处理,Error很少出现。因此,程序员应该关注Exception为父类的分支下的各种异常类。
异常:Exception以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被Java异常处理机制使用,是异常处理的核心。
Throwable又派生出「Error类和Exception类」。
错误:Error类以及他的子类的实例,代表了JVM本身的错误。错误不能被程序员通过代码处理,Error很少出现。因此,程序员应该关注Exception为父类的分支下的各种异常类。
异常:Exception以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被Java异常处理机制使用,是异常处理的核心。
处理方法:
1.try catch
2.throw
3.throws
1.try catch
2.throw
3.throws
java 中 IO 流分为几种?
按功能来分:输入流(input)、输出流(output)。
按类型来分:字节流和字符流。
按类型来分:字节流和字符流。
字节流和字符流的区别是:字节流按 8 位传输以字节为单位输入输出数据,字符流按 16 位传输以字符为单位输入输出数据。
BIO、NIO、AIO 有什么区别?
BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。
NIO:New IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。
AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。
NIO:New IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。
AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。
Files的常用方法都有哪些?
Files.exists():检测文件路径是否存在。
Files.createFile():创建文件。
Files.createDirectory():创建文件夹。
Files.delete():删除一个文件或目录。
Files.copy():复制文件。
Files.move():移动文件。
Files.size():查看文件个数。
Files.read():读取文件。
Files.write():写入文件。
Files.createFile():创建文件。
Files.createDirectory():创建文件夹。
Files.delete():删除一个文件或目录。
Files.copy():复制文件。
Files.move():移动文件。
Files.size():查看文件个数。
Files.read():读取文件。
Files.write():写入文件。
java 容器都有哪些?
Collection
List
Queue
Set
Map
HashMap
Collection 和 Collections 有什么区别?
java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。
Collections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。
List、Set、Map 之间的区别是什么?
HashMap 和 Hashtable 有什么区别?
hashMap去掉了HashTable 的contains方法,但是加上了containsValue()和containsKey()方法。
hashTable同步的,而HashMap是非同步的,效率上逼hashTable要高。
hashMap允许空键值,而hashTable不允许。
hashTable同步的,而HashMap是非同步的,效率上逼hashTable要高。
hashMap允许空键值,而hashTable不允许。
如何决定使用 HashMap 还是 TreeMap?
对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历。
说一下HashMap的put方法?
1. 根据Key通过哈希算法与与运算得出数组下标
2. 如果数组下标位置元素为空,则将key和value封装为Entry对象(JDK1.7中是Entry对象,JDK1.8中是 Node对象)并放⼊该位置
3. 如果数组下标位置元素不为空,则要分情况讨论
--a. 如果是JDK1.7,则先判断是否需要扩容,如果要扩容就进⾏扩容,如果不⽤扩容就⽣成Entry对 象,并使⽤头插法添加到当前位置的链表中
--b. 如果是JDK1.8,则会先判断当前位置上的Node的类型,看是红⿊树Node,还是链表Node
---i. 如果是红⿊树Node,则将key和value封装为⼀个红⿊树节点并添加到红⿊树中去,在这个过 程中会判断红⿊树中是否存在当前key,如果存在则更新value
---ii. 如果此位置上的Node对象是链表节点,则将key和value封装为⼀个链表Node并通过尾插法插 ⼊到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历链表的过程中会判断是否
2. 如果数组下标位置元素为空,则将key和value封装为Entry对象(JDK1.7中是Entry对象,JDK1.8中是 Node对象)并放⼊该位置
3. 如果数组下标位置元素不为空,则要分情况讨论
--a. 如果是JDK1.7,则先判断是否需要扩容,如果要扩容就进⾏扩容,如果不⽤扩容就⽣成Entry对 象,并使⽤头插法添加到当前位置的链表中
--b. 如果是JDK1.8,则会先判断当前位置上的Node的类型,看是红⿊树Node,还是链表Node
---i. 如果是红⿊树Node,则将key和value封装为⼀个红⿊树节点并添加到红⿊树中去,在这个过 程中会判断红⿊树中是否存在当前key,如果存在则更新value
---ii. 如果此位置上的Node对象是链表节点,则将key和value封装为⼀个链表Node并通过尾插法插 ⼊到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历链表的过程中会判断是否
说一下 HashSet 的实现原理?
HashSet底层由HashMap实现
HashSet的值存放于HashMap的key上
HashMap的value统一为PRESENT
HashSet的值存放于HashMap的key上
HashMap的value统一为PRESENT
ArrayList 和 LinkedList 的区别?
1.ArrayList 是实现了基于「数组」的,存储空间是连续的。LinkedList 基于「链表」的,存储空间是不连续的。(LinkedList 是双向链表)
2.对于「随机访问」 get 和 set ,ArrayList 觉得优于 LinkedList,因为 LinkedList 要移动指针。
3.对于「新增和删除」操作 add 和 remove ,LinedList 比较占优势,因为 ArrayList 要移动数据。
4.同样的数据量 LinkedList 所占用空间可能会更小,因为 ArrayList 需要「预留空间」便于后续数据增加,而 LinkedList 增加数据只需要「增加一个节点」
2.对于「随机访问」 get 和 set ,ArrayList 觉得优于 LinkedList,因为 LinkedList 要移动指针。
3.对于「新增和删除」操作 add 和 remove ,LinedList 比较占优势,因为 ArrayList 要移动数据。
4.同样的数据量 LinkedList 所占用空间可能会更小,因为 ArrayList 需要「预留空间」便于后续数据增加,而 LinkedList 增加数据只需要「增加一个节点」
ArrayList 和 Vector 的区别是什么?
Vector是同步的,而ArrayList不是。然而,如果你寻求在迭代的时候对列表进行改变,你应该使用CopyOnWriteArrayList。
ArrayList比Vector快,它因为有同步,不会过载。
ArrayList更加通用,因为我们可以使用Collections工具类轻易地获取同步列表和只读列表
ArrayList比Vector快,它因为有同步,不会过载。
ArrayList更加通用,因为我们可以使用Collections工具类轻易地获取同步列表和只读列表
Array 和 ArrayList 有何区别?
Array可以容纳基本类型和对象,而ArrayList只能容纳对象。
Array是指定大小的,而ArrayList大小是固定的。
Array没有提供ArrayList那么多功能,比如addAll、removeAll和iterator等
Array是指定大小的,而ArrayList大小是固定的。
Array没有提供ArrayList那么多功能,比如addAll、removeAll和iterator等
如何实现数组和 List 之间的转换?
List转换成为数组:调用ArrayList的toArray方法。
数组转换成为List:调用Arrays的asList方法。
数组转换成为List:调用Arrays的asList方法。
在 Queue 中 poll()和 remove()有什么区别?
poll() 和 remove() 都是从队列中取出一个元素,但是 poll() 在获取元素失败的时候会返回空,但是 remove() 失败的时候会抛出异常。
迭代器 Iterator 是什么?
迭代器是一种设计模式,它是一个对象,它可以遍历并选择序列中的对象,而开发人员不需要了解该序列的底层结构。迭代器通常被称为“轻量级”对象,因为创建它的代价小。
Iterator 怎么使用?有什么特点?
Java中的Iterator功能比较简单,并且只能单向移动:
(1) 使用方法iterator()要求容器返回一个Iterator。第一次调用Iterator的next()方法时,它返回序列的第一个元素。注意:iterator()方法是java.lang.Iterable接口,被Collection继承。
(2) 使用next()获得序列中的下一个元素。
(3) 使用hasNext()检查序列中是否还有元素。
(4) 使用remove()将迭代器新返回的元素删除。
Iterator是Java迭代器最简单的实现,为List设计的ListIterator具有更多的功能,它可以从两个方向遍历List,也可以从List中插入和删除元素。
(1) 使用方法iterator()要求容器返回一个Iterator。第一次调用Iterator的next()方法时,它返回序列的第一个元素。注意:iterator()方法是java.lang.Iterable接口,被Collection继承。
(2) 使用next()获得序列中的下一个元素。
(3) 使用hasNext()检查序列中是否还有元素。
(4) 使用remove()将迭代器新返回的元素删除。
Iterator是Java迭代器最简单的实现,为List设计的ListIterator具有更多的功能,它可以从两个方向遍历List,也可以从List中插入和删除元素。
Iterator 和 ListIterator 有什么区别?
Iterator可用来遍历Set和List集合,但是ListIterator只能用来遍历List。
Iterator对集合只能是前向遍历,ListIterator既可以前向也可以后向。
ListIterator实现了Iterator接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引,等等。
Iterator对集合只能是前向遍历,ListIterator既可以前向也可以后向。
ListIterator实现了Iterator接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引,等等。
hashMap 1.7 和 hashMap 1.8 的区别?
hashMap 线程不安全体现在哪里?
在 「hashMap1.7 中扩容」的时候,因为采用的是头插法,所以会可能会有循环链表产生,导致数据有问题,在 1.8 版本已修复,改为了尾插法
在任意版本的 hashMap 中,如果在「插入数据时多个线程命中了同一个槽」,可能会有数据覆盖的情况发生,导致线程不安全。
在任意版本的 hashMap 中,如果在「插入数据时多个线程命中了同一个槽」,可能会有数据覆盖的情况发生,导致线程不安全。
那么 hashMap 线程不安全怎么解决?
一.给 hashMap 「直接加锁」,来保证线程安全
二.使用 「hashTable」,比方法一效率高,其实就是在其方法上加了 synchronized 锁
三.使用 「concurrentHashMap」 , 不管是其 1.7 还是 1.8 版本,本质都是「减小了锁的粒度,减少线程竞争」来保证高效.
二.使用 「hashTable」,比方法一效率高,其实就是在其方法上加了 synchronized 锁
三.使用 「concurrentHashMap」 , 不管是其 1.7 还是 1.8 版本,本质都是「减小了锁的粒度,减少线程竞争」来保证高效.
介绍一下 hashset 吧
set 继承于 Collection 接口,是一个「不允许出现重复元素,并且无序的集合」.
HashSet 是「基于 HashMap 实现」的,底层「采用 HashMap 来保存元素」
元素的哈希值是通过元素的 hashcode 方法 来获取的, HashSet 首先判断两个元素的哈希值,如果哈希值一样,接着会比较 equals 方法 如果 equls 结果为 true ,HashSet 就视为同一个元素。如果 equals 为 false 就不是同一个元素。
HashSet 是「基于 HashMap 实现」的,底层「采用 HashMap 来保存元素」
元素的哈希值是通过元素的 hashcode 方法 来获取的, HashSet 首先判断两个元素的哈希值,如果哈希值一样,接着会比较 equals 方法 如果 equls 结果为 true ,HashSet 就视为同一个元素。如果 equals 为 false 就不是同一个元素。
什么是泛型?
泛型:「把类型明确的工作推迟到创建对象或调用方法的时候才去明确的特殊的类型」
使用泛型的好处?
以集合来举例,使用泛型的好处是我们不必因为添加元素类型的不同而定义不同类型的集合,如整
型集合类,浮点型集合类,字符串集合类,我们可以定义一个集合来存放整型、浮点型,字符串型
数据,而这并不是最重要的,因为我们只要把底层存储设置了Object即可,添加的数据全部都可向
上转型为Object。 更重要的是我们可以通过规则按照自己的想法控制存储的数据类型。
以集合来举例,使用泛型的好处是我们不必因为添加元素类型的不同而定义不同类型的集合,如整
型集合类,浮点型集合类,字符串集合类,我们可以定义一个集合来存放整型、浮点型,字符串型
数据,而这并不是最重要的,因为我们只要把底层存储设置了Object即可,添加的数据全部都可向
上转型为Object。 更重要的是我们可以通过规则按照自己的想法控制存储的数据类型。
泛型擦除是什么?
因为泛型其实只是在编译器中实现的而虚拟机并不认识泛型类项,所以要在虚拟机中将泛型类型进行擦除。也就是说,「在编译阶段使用泛型,运行阶段取消泛型,即擦除」。擦除是将泛型类型以其父类代替,如String 变成了Object等。其实在使用的时候还是进行带强制类型的转化,只不过这是比较安全的转换,因为在编译阶段已经确保了数据的一致性。
说说进程和线程的区别?
进程是程序的⼀次执⾏,是系统进⾏资源分配和调度的独⽴单位,他的作⽤是是程序能够并发执⾏提⾼
资源利⽤率和吞吐率。
由于进程是资源分配和调度的基本单位,因为进程的创建、销毁、切换产⽣⼤量的时间和空间的开销,
进程的数量不能太多,⽽线程是⽐进程更⼩的能独⽴运⾏的基本单位,他是进程的⼀个实体,可以减少
程序并发执⾏时的时间和空间开销,使得操作系统具有更好的并发性。
线程基本不拥有系统资源,只有⼀些运⾏时必不可少的资源,⽐如程序计数器、寄存器和栈,进程则占
有堆、栈。
资源利⽤率和吞吐率。
由于进程是资源分配和调度的基本单位,因为进程的创建、销毁、切换产⽣⼤量的时间和空间的开销,
进程的数量不能太多,⽽线程是⽐进程更⼩的能独⽴运⾏的基本单位,他是进程的⼀个实体,可以减少
程序并发执⾏时的时间和空间开销,使得操作系统具有更好的并发性。
线程基本不拥有系统资源,只有⼀些运⾏时必不可少的资源,⽐如程序计数器、寄存器和栈,进程则占
有堆、栈。
反射的作用和原理?
反射机制是在运行时,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意个对象,
都能够调用它的任意一个方法。在java中,只要给定类的名字,就可以通过反射机制来获得类的所
有信息。
都能够调用它的任意一个方法。在java中,只要给定类的名字,就可以通过反射机制来获得类的所
有信息。
反射的实现方式:
第一步:获取Class对象,有4中方法: 1)Class.forName(“类的路径”); 2)类名.class 3)对象
名.getClass() 4)基本类型的包装类,可以调用包装类的Type属性来获得该包装类的Class对象
第一步:获取Class对象,有4中方法: 1)Class.forName(“类的路径”); 2)类名.class 3)对象
名.getClass() 4)基本类型的包装类,可以调用包装类的Type属性来获得该包装类的Class对象
反射机制的优缺点:
优点: 1)能够运行时动态获取类的实例,提高灵活性; 2)与动态编译结合 缺点: 1)使用反射
性能较低,需要解析字节码,将内存中的对象进行解析。 解决方案: 1、通过setAccessible(true)
关闭JDK的安全检查来提升反射速度; 2、多次创建一个类的实例时,有缓存会快很多 3、
ReflectASM工具类,通过字节码生成的方式加快反射速度 2)相对不安全,破坏了封装性(因为通
过反射可以获得私有方法和属性)
优点: 1)能够运行时动态获取类的实例,提高灵活性; 2)与动态编译结合 缺点: 1)使用反射
性能较低,需要解析字节码,将内存中的对象进行解析。 解决方案: 1、通过setAccessible(true)
关闭JDK的安全检查来提升反射速度; 2、多次创建一个类的实例时,有缓存会快很多 3、
ReflectASM工具类,通过字节码生成的方式加快反射速度 2)相对不安全,破坏了封装性(因为通
过反射可以获得私有方法和属性)
创建对象有哪些方式
有「五种创建对象的方式」
1、new关键字
Person p1 = new Person();
2.Class.newInstance
Person p1 = Person.class.newInstance();
3.Constructor.newInstance
Constructor<Person> constructor = Person.class.getConstructor();
Person p1 = constructor.newInstance();
4.clone
Person p1 = new Person();
Person p2 = p1.clone();
5.反序列化
Person p1 = new Person();
byte[] bytes = SerializationUtils.serialize(p1);
Person p2 = (Person)SerializationUtils.deserialize(bytes);
1、new关键字
Person p1 = new Person();
2.Class.newInstance
Person p1 = Person.class.newInstance();
3.Constructor.newInstance
Constructor<Person> constructor = Person.class.getConstructor();
Person p1 = constructor.newInstance();
4.clone
Person p1 = new Person();
Person p2 = p1.clone();
5.反序列化
Person p1 = new Person();
byte[] bytes = SerializationUtils.serialize(p1);
Person p2 = (Person)SerializationUtils.deserialize(bytes);
讲讲单例模式懒汉式吧
// 懒汉式
public class Singleton {
// 延迟加载保证多线程安全
Private volatile static Singleton singleton;
private Singleton(){}
public static Singleton getInstance(){
if(singleton == null){
synchronized(Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
使用 volatile 是「防止指令重排序,保证对象可见」,防止读到半初始化状态的对象
第一层if(singleton == null) 是为了防止有多个线程同时创建
synchronized 是加锁防止多个线程同时进入该方法创建对象
第二层if(singleton == null) 是防止有多个线程同时等待锁,一个执行完了后面一个又继续执行的情况
public class Singleton {
// 延迟加载保证多线程安全
Private volatile static Singleton singleton;
private Singleton(){}
public static Singleton getInstance(){
if(singleton == null){
synchronized(Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
使用 volatile 是「防止指令重排序,保证对象可见」,防止读到半初始化状态的对象
第一层if(singleton == null) 是为了防止有多个线程同时创建
synchronized 是加锁防止多个线程同时进入该方法创建对象
第二层if(singleton == null) 是防止有多个线程同时等待锁,一个执行完了后面一个又继续执行的情况
深拷贝、浅拷贝是什么?
浅拷贝并不是真的拷贝,只是「复制指向某个对象的指针」,而不复制对象本身,新旧对象还是共享同一块内存。
深拷贝会另外「创造一个一模一样的对象」,新对象跟原对象不共享内存,修改新对象不会改到原对象。
多线程
并行和并发有什么区别?
并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如hadoop分布式集群。
并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如hadoop分布式集群。
线程和进程的区别?
简而言之,进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。线程是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小的能独立运行的基本单位。同一进程中的多个线程之间可以并发执行。
线程和协程的区别?
线程是操作系统调度执行的最小单位,协程是比线程更加轻量级的存在,区别在于协程不是被操作系统内核所管理,而完全被程序进行控制。协程在一个线程中执行,消除了线程切换的开销,性能优势明显。协程不需要多线程的锁机制,具有极高的执行效率
1) 一个线程可以多个协程,一个进程也可以单独拥有多个协程。
2) 线程进程都是同步机制,而协程则是异步。
3) 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态。
4)线程是抢占式,而协程是非抢占式的,所以需要用户自己释放使用权来切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能 力。
5)协程并不是取代线程, 而且抽象于线程之上, 线程是被分割的CPU资源, 协程是组织好的代码流程, 协程需要线程来承载运行, 线程是协程的资源, 但协程不会直接使用线程, 协程直接利用的是执行器(Interceptor), 执行器可以关联任意线程或线程池, 可以使当前线程, UI线程, 或新建新程.。
6)线程是协程的资源。协程通过Interceptor来间接使用线程这个资源
2) 线程进程都是同步机制,而协程则是异步。
3) 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态。
4)线程是抢占式,而协程是非抢占式的,所以需要用户自己释放使用权来切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能 力。
5)协程并不是取代线程, 而且抽象于线程之上, 线程是被分割的CPU资源, 协程是组织好的代码流程, 协程需要线程来承载运行, 线程是协程的资源, 但协程不会直接使用线程, 协程直接利用的是执行器(Interceptor), 执行器可以关联任意线程或线程池, 可以使当前线程, UI线程, 或新建新程.。
6)线程是协程的资源。协程通过Interceptor来间接使用线程这个资源
守护线程是什么?
守护线程(即daemon thread),是个服务线程,准确地来说就是服务其他的线程。
创建线程有哪几种方式?
继承Thread类创建线程类
定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
创建Thread子类的实例,即创建了线程对象。
调用线程对象的start()方法来启动该线程。
创建Thread子类的实例,即创建了线程对象。
调用线程对象的start()方法来启动该线程。
通过Runnable接口创建线程类
定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
调用线程对象的start()方法来启动该线程。
创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
调用线程对象的start()方法来启动该线程。
通过Callable和Future创建线程
创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
使用FutureTask对象作为Thread对象的target创建并启动新线程。
调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
使用FutureTask对象作为Thread对象的target创建并启动新线程。
调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
如何查看线程死锁?
命令: jstack pid
说一下 runnable 和 callable 有什么区别?
Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;
Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
线程有哪些状态?
线程通常都有五种状态,创建、就绪、运行、阻塞和死亡。
创建状态。在生成线程对象,并没有调用该对象的start方法,这是线程处于创建状态。
就绪状态。当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。
运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。
阻塞状态。线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait等方法都可以导致线程阻塞。
死亡状态。如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪
就绪状态。当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。
运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。
阻塞状态。线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait等方法都可以导致线程阻塞。
死亡状态。如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪
sleep() 和 wait() 有什么区别?
sleep():方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间。因为sleep() 是static静态的方法,他不能改变对象的锁,当一个synchronized块中调用了sleep() 方法,线程虽然进入休眠,但是对象的锁没有被释放,其他线程依然无法访问这个对象。
wait():wait()是Object类的方法,当一个线程执行到wait方法时,它就进入到一个和该对象相关的等待池,同时释放对象的机锁,使得其他线程能够访问,可以通过notify,notifyAll方法来唤醒等待的线程
notify()和 notifyAll()有什么区别?
如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争。
优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
为什么wait, notify 和 notifyAll这些方法不在thread类里
面?
面?
明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线
程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线
程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所
以把他们定义在Object类中因为锁属于对象
程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线
程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所
以把他们定义在Object类中因为锁属于对象
Java中interrupted 和 isInterruptedd方法的区别?
interrupted() 和 isInterrupted()的主要区别是前者会将中断状态清除而后者不会。Java多线程的中
断机制是用内部标识来实现的,调用Thread.interrupt()来中断一个线程就会设置中断标识为true。
当中断线程调用静态方法Thread.interrupted()来检查中断状态时,中断状态会被清零。而非静态方
法isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识。简单的说就是任何抛出
InterruptedException异常的方法都会将中断状态清零。无论如何,一个线程的中断状态有有可能
被其它线程调用中断来改变。
断机制是用内部标识来实现的,调用Thread.interrupt()来中断一个线程就会设置中断标识为true。
当中断线程调用静态方法Thread.interrupted()来检查中断状态时,中断状态会被清零。而非静态方
法isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识。简单的说就是任何抛出
InterruptedException异常的方法都会将中断状态清零。无论如何,一个线程的中断状态有有可能
被其它线程调用中断来改变。
线程的 run()和 start()有什么区别?
每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,方法run()称为线程体。通过调用Thread类的start()方法来启动一个线程。
start()方法来启动一个线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码; 这时此线程是处于就绪状态, 并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, 这里方法run()称为线程体,它包含了要执行的这个线程的内容, Run方法运行结束, 此线程终止。然后CPU再调度其它线程。
run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。
创建线程池有哪几种方式?
newFixedThreadPool(int nThreads)
创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程
newCachedThreadPool()
创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程
newSingleThreadExecutor()
这是一个单线程的Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它;它的特点是能确保依照任务在队列中的顺序来串行执行。
newScheduledThreadPool(int corePoolSize)
创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。
线程池都有哪些状态?
线程池有5种状态:Running、ShutDown、Stop、Tidying、Terminated。
1.RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。线程池的初始化状态是RUNNING。线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0。
2.SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。调用线程池的shutdown()方法时,线程池由RUNNING -> SHUTDOWN。
3.STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。调用线程池的shutdownNow()方法时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
4.TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。因为terminated()在ThreadPoolExecutor类中是空的,所以用户想在线程池变为TIDYING时进行相应的处理;可以通过重载terminated()函数来实现。
当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。
当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
5.TERMINATED:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。
2.SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。调用线程池的shutdown()方法时,线程池由RUNNING -> SHUTDOWN。
3.STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。调用线程池的shutdownNow()方法时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
4.TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。因为terminated()在ThreadPoolExecutor类中是空的,所以用户想在线程池变为TIDYING时进行相应的处理;可以通过重载terminated()函数来实现。
当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。
当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
5.TERMINATED:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。
线程池中 submit()和 execute()方法有什么区别?
接收的参数不一样
submit有返回值,而execute没有
submit方便Exception处理
submit有返回值,而execute没有
submit方便Exception处理
线程池的原理/执行流程/拒绝策略?
1. 最⼤线程数maximumPoolSize
2. 核⼼线程数corePoolSize
3. 活跃时间keepAliveTime
4. 阻塞队列workQueue
5. 拒绝策略RejectedExecutionHandler
2. 核⼼线程数corePoolSize
3. 活跃时间keepAliveTime
4. 阻塞队列workQueue
5. 拒绝策略RejectedExecutionHandler
当提交⼀个新任务到线程池时,具体的执⾏流程如下:
1. 当我们提交任务,线程池会根据corePoolSize⼤⼩创建若⼲任务数量线程执⾏任务
2. 当任务的数量超过corePoolSize数量,后续的任务将会进⼊阻塞队列阻塞排队
3. 当阻塞队列也满了之后,那么将会继续创建(maximumPoolSize-corePoolSize)个数量的线程来执
⾏任务,如果任务处理完成,maximumPoolSize-corePoolSize额外创建的线程等待
keepAliveTime之后被⾃动销毁
4. 如果达到maximumPoolSize,阻塞队列还是满的状态,那么将根据不同的拒绝策略对应处理
1. 当我们提交任务,线程池会根据corePoolSize⼤⼩创建若⼲任务数量线程执⾏任务
2. 当任务的数量超过corePoolSize数量,后续的任务将会进⼊阻塞队列阻塞排队
3. 当阻塞队列也满了之后,那么将会继续创建(maximumPoolSize-corePoolSize)个数量的线程来执
⾏任务,如果任务处理完成,maximumPoolSize-corePoolSize额外创建的线程等待
keepAliveTime之后被⾃动销毁
4. 如果达到maximumPoolSize,阻塞队列还是满的状态,那么将根据不同的拒绝策略对应处理
主要有4种拒绝策略:
1. AbortPolicy:直接丢弃任务,抛出异常,这是默认策略
2. CallerRunsPolicy:只⽤调⽤者所在的线程来处理任务
3. DiscardOldestPolicy:丢弃等待队列中最旧的任务,并执⾏当前任务
4. DiscardPolicy:直接丢弃任务,也不抛出异常
1. AbortPolicy:直接丢弃任务,抛出异常,这是默认策略
2. CallerRunsPolicy:只⽤调⽤者所在的线程来处理任务
3. DiscardOldestPolicy:丢弃等待队列中最旧的任务,并执⾏当前任务
4. DiscardPolicy:直接丢弃任务,也不抛出异常
Java线程池中队列常用类型有哪些?
ArrayBlockingQueue 是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则
对元素进行排序。
LinkedBlockingQueue 一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元
素,吞吐量通常要高于 ArrayBlockingQueue 。
SynchronousQueue 一个不存储元素的阻塞队列。
PriorityBlockingQueue 一个具有优先级的无限阻塞队列。 PriorityBlockingQueue 也是基于
最小二叉堆实现
DelayQueue
只有当其指定的延迟时间到了,才能够从队列中获取到该元素。
DelayQueue 是一个没有大小限制的队列,
因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费
者)才会被阻塞。
对元素进行排序。
LinkedBlockingQueue 一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元
素,吞吐量通常要高于 ArrayBlockingQueue 。
SynchronousQueue 一个不存储元素的阻塞队列。
PriorityBlockingQueue 一个具有优先级的无限阻塞队列。 PriorityBlockingQueue 也是基于
最小二叉堆实现
DelayQueue
只有当其指定的延迟时间到了,才能够从队列中获取到该元素。
DelayQueue 是一个没有大小限制的队列,
因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费
者)才会被阻塞。
说一下线程之间是如何通信的?
线程之间的通信有两种方式:wait和notify或者共享变量
在 java 程序中怎么保证多线程的运行安全?
线程安全在三个方面体现:
原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);
有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。
原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);
有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。
多线程锁的升级原理是什么?
在Java中,锁共有4种状态,级别从低到高依次为:无状态锁,偏向锁,轻量级锁和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。
什么是死锁?
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。是操作系统层面的一个错误,是进程死锁的简称,最早在 1965 年由 Dijkstra 在研究银行家算法时提出的,它是计算机操作系统乃至整个并发程序设计领域最难处理的问题之一。
怎么防止死锁?
互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源
请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放
不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放
环路等待条件:是指进程发生死锁后,若干进程之间形成一种头尾相接的循环等待资源关系
请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放
不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放
环路等待条件:是指进程发生死锁后,若干进程之间形成一种头尾相接的循环等待资源关系
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之 一不满足,就不会发生死锁。
知道synchronized原理吗?
synchronized是java提供的原⼦性内置锁,这种内置的并且使⽤者看不到的锁也被称为监视器锁,使⽤
synchronized之后,会在编译之后在同步的代码块前后加上monitorenter和monitorexit字节码指令,
他依赖操作系统底层互斥锁实现。他的作⽤主要就是实现原⼦性操作和解决共享变量的内存可⻅性问
题。
执⾏monitorenter指令时会尝试获取对象锁,如果对象没有被锁定或者已经获得了锁,锁的计数器+1。
此时其他竞争锁的线程则会进⼊等待队列中。
执⾏monitorexit指令时则会把计数器-1,当计数器值为0时,则锁释放,处于等待队列中的线程再继续
竞争锁。
synchronized是排它锁,当⼀个线程获得锁之后,其他线程必须等待该线程释放锁后才能获得锁,⽽且
由于Java中的线程和操作系统原⽣线程是⼀⼀对应的,线程被阻塞或者唤醒时时会从⽤户态切换到内核
态,这种转换⾮常消耗性能。
从内存语义来说,加锁的过程会清除⼯作内存中的共享变量,再从主内存读取,⽽释放锁的过程则是将
⼯作内存中的共享变量写回主内存。
synchronized之后,会在编译之后在同步的代码块前后加上monitorenter和monitorexit字节码指令,
他依赖操作系统底层互斥锁实现。他的作⽤主要就是实现原⼦性操作和解决共享变量的内存可⻅性问
题。
执⾏monitorenter指令时会尝试获取对象锁,如果对象没有被锁定或者已经获得了锁,锁的计数器+1。
此时其他竞争锁的线程则会进⼊等待队列中。
执⾏monitorexit指令时则会把计数器-1,当计数器值为0时,则锁释放,处于等待队列中的线程再继续
竞争锁。
synchronized是排它锁,当⼀个线程获得锁之后,其他线程必须等待该线程释放锁后才能获得锁,⽽且
由于Java中的线程和操作系统原⽣线程是⼀⼀对应的,线程被阻塞或者唤醒时时会从⽤户态切换到内核
态,这种转换⾮常消耗性能。
从内存语义来说,加锁的过程会清除⼯作内存中的共享变量,再从主内存读取,⽽释放锁的过程则是将
⼯作内存中的共享变量写回主内存。
如果再深⼊到源码来说,synchronized实际上有两个队列waitSet和entryList。
1. 当多个线程进⼊同步代码块时,⾸先进⼊entryList
2. 有⼀个线程获取到monitor锁后,就赋值给当前线程,并且计数器+1
3. 如果线程调⽤wait⽅法,将释放锁,当前线程置为null,计数器-1,同时进⼊waitSet等待被唤醒,
调⽤notify或者notifyAll之后⼜会进⼊entryList竞争锁
4. 如果线程执⾏完毕,同样释放锁,计数器-1,当前线程置为null
1. 当多个线程进⼊同步代码块时,⾸先进⼊entryList
2. 有⼀个线程获取到monitor锁后,就赋值给当前线程,并且计数器+1
3. 如果线程调⽤wait⽅法,将释放锁,当前线程置为null,计数器-1,同时进⼊waitSet等待被唤醒,
调⽤notify或者notifyAll之后⼜会进⼊entryList竞争锁
4. 如果线程执⾏完毕,同样释放锁,计数器-1,当前线程置为null
那锁的优化机制了解吗?
从JDK1.6版本之后,synchronized本身也在不断优化锁的机制,有些情况下他并不会是⼀个很重量级的
锁了。优化机制包括⾃适应锁、⾃旋锁、锁消除、锁粗化、轻量级锁和偏向锁。
锁的状态从低到⾼依次为⽆锁->偏向锁->轻量级锁->重量级锁,升级的过程就是从低到⾼,降级在⼀定条
件也是有可能发⽣的
锁了。优化机制包括⾃适应锁、⾃旋锁、锁消除、锁粗化、轻量级锁和偏向锁。
锁的状态从低到⾼依次为⽆锁->偏向锁->轻量级锁->重量级锁,升级的过程就是从低到⾼,降级在⼀定条
件也是有可能发⽣的
⾃旋锁:由于⼤部分时候,锁被占⽤的时间很短,共享变量的锁定时间也很短,所有没有必要挂起线
程,⽤户态和内核态的来回上下⽂切换严重影响性能。⾃旋的概念就是让线程执⾏⼀个忙循环,可以理
解为就是啥也不⼲,防⽌从⽤户态转⼊内核态,⾃旋锁可以通过设置-XX:+UseSpining来开启,⾃旋的默
认次数是10次,可以使⽤-XX:PreBlockSpin设置。
程,⽤户态和内核态的来回上下⽂切换严重影响性能。⾃旋的概念就是让线程执⾏⼀个忙循环,可以理
解为就是啥也不⼲,防⽌从⽤户态转⼊内核态,⾃旋锁可以通过设置-XX:+UseSpining来开启,⾃旋的默
认次数是10次,可以使⽤-XX:PreBlockSpin设置。
⾃适应锁:⾃适应锁就是⾃适应的⾃旋锁,⾃旋的时间不是固定时间,⽽是由前⼀次在同⼀个锁上的⾃
旋时间和锁的持有者状态来决定。
旋时间和锁的持有者状态来决定。
锁消除:锁消除指的是JVM检测到⼀些同步的代码块,完全不存在数据竞争的场景,也就是不需要加
锁,就会进⾏锁消除。
锁,就会进⾏锁消除。
锁粗化:锁粗化指的是有很多操作都是对同⼀个对象进⾏加锁,就会把锁的同步范围扩展到整个操作序
列之外。
列之外。
偏向锁:当线程访问同步块获取锁时,会在对象头和栈帧中的锁记录⾥存储偏向锁的线程ID,之后这个
线程再次进⼊同步块时都不需要CAS来加锁和解锁了,偏向锁会永远偏向第⼀个获得锁的线程,如果后
续没有其他线程获得过这个锁,持有锁的线程就永远不需要进⾏同步,反之,当有其他线程竞争偏向锁
时,持有偏向锁的线程就会释放偏向锁。可以⽤过设置-XX:+UseBiasedLocking开启偏向锁。
线程再次进⼊同步块时都不需要CAS来加锁和解锁了,偏向锁会永远偏向第⼀个获得锁的线程,如果后
续没有其他线程获得过这个锁,持有锁的线程就永远不需要进⾏同步,反之,当有其他线程竞争偏向锁
时,持有偏向锁的线程就会释放偏向锁。可以⽤过设置-XX:+UseBiasedLocking开启偏向锁。
轻量级锁:JVM的对象的对象头中包含有⼀些锁的标志位,代码进⼊同步块的时候,JVM将会使⽤CAS⽅
式来尝试获取锁,如果更新成功则会把对象头中的状态位标记为轻量级锁,如果更新失败,当前线程就
尝试⾃旋来获得锁。
式来尝试获取锁,如果更新成功则会把对象头中的状态位标记为轻量级锁,如果更新失败,当前线程就
尝试⾃旋来获得锁。
简单点说,偏向锁就是通过对象头的偏向线程ID来对⽐,甚⾄都不需要CAS了,⽽轻量级锁主要就是通
过CAS修改对象头锁记录和⾃旋来实现,重量级锁则是除了拥有锁的线程其他全部阻塞。
过CAS修改对象头锁记录和⾃旋来实现,重量级锁则是除了拥有锁的线程其他全部阻塞。
那对象头具体都包含哪些内容?
在我们常⽤的Hotspot虚拟机中,对象在内存中布局实际包含3个部分:
1. 对象头
2. 实例数据
3. 对⻬填充
1. 对象头
2. 实例数据
3. 对⻬填充
对象头包含两部分内容,Mark Word中的内容会随着锁标志位⽽发⽣变化,所以只说存储结构就好
了。
1. 对象⾃身运⾏时所需的数据,也被称为Mark Word,也就是⽤于轻量级锁和偏向锁的关键点。具体
的内容包含对象的hashcode、分代年龄、轻量级锁指针、重量级锁指针、GC标记、偏向锁线程
ID、偏向锁时间戳。
2. 存储类型指针,也就是指向类的元数据的指针,通过这个指针才能确定对象是属于哪个类的实例。
如果是数组的话,则还包含了数组的⻓度(占用4字节)
了。
1. 对象⾃身运⾏时所需的数据,也被称为Mark Word,也就是⽤于轻量级锁和偏向锁的关键点。具体
的内容包含对象的hashcode、分代年龄、轻量级锁指针、重量级锁指针、GC标记、偏向锁线程
ID、偏向锁时间戳。
2. 存储类型指针,也就是指向类的元数据的指针,通过这个指针才能确定对象是属于哪个类的实例。
如果是数组的话,则还包含了数组的⻓度(占用4字节)
实例数据: 对象实际数据,对象实际数据包括了对象的所有成员变量,其大小由各个成员变量的大小决定。(这里不包括静态成员变量,因为其是在方法区维护的)
对齐填充:Java 对象占用空间是 8 字节对齐的,即所有 Java 对象占用 bytes 数必须是 8 的倍数,是因为当我们从磁盘中取一个数据时,不会说我想取一个字节就是一个字节,都是按照一块儿一块儿来取的,这一块大小是 8 个字节,所以为了完整,padding 的作用就是补充字节,「保证对象是 8 字节的整数倍」。
对于加锁,那再说下ReentrantLock原理?他和
synchronized有什么区别?
synchronized有什么区别?
相⽐于synchronized,ReentrantLock需要显式的获取锁和释放锁,相对现在基本都是⽤JDK7和JDK8的
版本,ReentrantLock的效率和synchronized区别基本可以持平了。他们的主要区别有以下⼏点:
1. 等待可中断:当持有锁的线程⻓时间不释放锁的时候,等待中的线程可以选择放弃等待,转⽽处理
其他的任务。
2. 公平锁:synchronized和ReentrantLock默认都是⾮公平锁,但是ReentrantLock可以通过构造函
数传参改变。只不过使⽤公平锁的话会导致性能急剧下降。
3. 绑定多个条件:ReentrantLock可以同时绑定多个Condition条件对象。
ReentrantLock基于AQS(AbstractQueuedSynchronizer 抽象队列同步器)实现
版本,ReentrantLock的效率和synchronized区别基本可以持平了。他们的主要区别有以下⼏点:
1. 等待可中断:当持有锁的线程⻓时间不释放锁的时候,等待中的线程可以选择放弃等待,转⽽处理
其他的任务。
2. 公平锁:synchronized和ReentrantLock默认都是⾮公平锁,但是ReentrantLock可以通过构造函
数传参改变。只不过使⽤公平锁的话会导致性能急剧下降。
3. 绑定多个条件:ReentrantLock可以同时绑定多个Condition条件对象。
ReentrantLock基于AQS(AbstractQueuedSynchronizer 抽象队列同步器)实现
CAS的原理呢?
CAS叫做CompareAndSwap,⽐较并交换,主要是通过处理器的指令来保证操作的原⼦性,它包含三个
操作数:
1. 变量内存地址,V表示
2. 旧的预期值,A表示
3. 准备设置的新值,B表示
当执⾏CAS指令时,只有当V等于A时,才会⽤B去更新V的值,否则就不会执⾏更新操作。
操作数:
1. 变量内存地址,V表示
2. 旧的预期值,A表示
3. 准备设置的新值,B表示
当执⾏CAS指令时,只有当V等于A时,才会⽤B去更新V的值,否则就不会执⾏更新操作。
那么CAS有什么缺点吗?
CAS的缺点主要有3点:
ABA问题:ABA的问题指的是在CAS更新的过程中,当读取到的值是A,然后准备赋值的时候仍然是A,
但是实际上有可能A的值被改成了B,然后⼜被改回了A,这个CAS更新的漏洞就叫做ABA。只是ABA的问
题⼤部分场景下都不影响并发的最终效果。
Java中有AtomicStampedReference来解决这个问题,他加⼊了预期标志和更新后标志两个字段,更新
时不光检查值,还要检查当前的标志是否等于预期标志,全部相等的话才会更新。
循环时间⻓开销⼤:⾃旋CAS的⽅式如果⻓时间不成功,会给CPU带来很⼤的开销。
只能保证⼀个共享变量的原⼦操作:只对⼀个共享变量操作可以保证原⼦性,但是多个则不⾏,多个可
以通过AtomicReference来处理或者使⽤锁synchronized实现。
ABA问题:ABA的问题指的是在CAS更新的过程中,当读取到的值是A,然后准备赋值的时候仍然是A,
但是实际上有可能A的值被改成了B,然后⼜被改回了A,这个CAS更新的漏洞就叫做ABA。只是ABA的问
题⼤部分场景下都不影响并发的最终效果。
Java中有AtomicStampedReference来解决这个问题,他加⼊了预期标志和更新后标志两个字段,更新
时不光检查值,还要检查当前的标志是否等于预期标志,全部相等的话才会更新。
循环时间⻓开销⼤:⾃旋CAS的⽅式如果⻓时间不成功,会给CPU带来很⼤的开销。
只能保证⼀个共享变量的原⼦操作:只对⼀个共享变量操作可以保证原⼦性,但是多个则不⾏,多个可
以通过AtomicReference来处理或者使⽤锁synchronized实现。
那多线程环境怎么使⽤Map呢?ConcurrentHashmap了
解过吗?
解过吗?
多线程环境可以使⽤Collections.synchronizedMap同步加锁的⽅式,还可以使⽤HashTable,但是同步
的⽅式显然性能不达标,⽽ConurrentHashMap更适合⾼并发场景使⽤。
ConcurrentHashmap在JDK1.7和1.8的版本改动⽐较⼤,1.7使⽤Segment+HashEntry分段锁的⽅式实
现,1.8则抛弃了Segment,改为使⽤CAS+synchronized+Node实现,同样也加⼊了红⿊树,避免链表
过⻓导致性能的问题
的⽅式显然性能不达标,⽽ConurrentHashMap更适合⾼并发场景使⽤。
ConcurrentHashmap在JDK1.7和1.8的版本改动⽐较⼤,1.7使⽤Segment+HashEntry分段锁的⽅式实
现,1.8则抛弃了Segment,改为使⽤CAS+synchronized+Node实现,同样也加⼊了红⿊树,避免链表
过⻓导致性能的问题
1.7分段锁
从结构上说,1.7版本的ConcurrentHashMap采⽤分段锁机制,⾥⾯包含⼀个Segment数组,Segment
继承与ReentrantLock,Segment则包含HashEntry的数组,HashEntry本身就是⼀个链表的结构,具有
保存key、value的能⼒能指向下⼀个节点的指针。
实际上就是相当于每个Segment都是⼀个HashMap,默认的Segment⻓度是16,也就是⽀持16个线程
的并发写,Segment之间相互不会受到影响。
从结构上说,1.7版本的ConcurrentHashMap采⽤分段锁机制,⾥⾯包含⼀个Segment数组,Segment
继承与ReentrantLock,Segment则包含HashEntry的数组,HashEntry本身就是⼀个链表的结构,具有
保存key、value的能⼒能指向下⼀个节点的指针。
实际上就是相当于每个Segment都是⼀个HashMap,默认的Segment⻓度是16,也就是⽀持16个线程
的并发写,Segment之间相互不会受到影响。
1.8CAS+synchronized
1.8抛弃分段锁,转为⽤CAS+synchronized来实现,同样HashEntry改为Node,也加⼊了红⿊树的实
现
1.8抛弃分段锁,转为⽤CAS+synchronized来实现,同样HashEntry改为Node,也加⼊了红⿊树的实
现
put流程
1. ⾸先计算hash,遍历node数组,如果node是空的话,就通过CAS+⾃旋的⽅式初始化
2. 如果当前数组位置是空则直接通过CAS⾃旋写⼊数据
3. 如果hash==MOVED,说明需要扩容,执⾏扩容
4. 如果都不满⾜,就使⽤synchronized写⼊数据,写⼊数据同样判断链表、红⿊树,链表写⼊和
HashMap的⽅式⼀样,key hash⼀样就覆盖,反之就尾插法,链表⻓度超过8就转换成红⿊树
1. ⾸先计算hash,遍历node数组,如果node是空的话,就通过CAS+⾃旋的⽅式初始化
2. 如果当前数组位置是空则直接通过CAS⾃旋写⼊数据
3. 如果hash==MOVED,说明需要扩容,执⾏扩容
4. 如果都不满⾜,就使⽤synchronized写⼊数据,写⼊数据同样判断链表、红⿊树,链表写⼊和
HashMap的⽅式⼀样,key hash⼀样就覆盖,反之就尾插法,链表⻓度超过8就转换成红⿊树
get查询
get很简单,通过key计算hash,如果key hash相同就返回,如果是红⿊树按照红⿊树获取,都不是就遍
历链表获取。
get很简单,通过key计算hash,如果key hash相同就返回,如果是红⿊树按照红⿊树获取,都不是就遍
历链表获取。
那么说说你对JMM内存模型的理解?为什么需要JMM?
JMM 就是 「Java内存模型」(java memory model)。因为在不同的硬件生产商和不同的操作系统下,内存的访问有一定的差异,所以会造成相同的代码运行在不同的系统上会出现各种问题。所以java内存模型(JMM)「屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果」。
Java内存模型规定所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数。每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行。「线程不能直接读写主内存中的变量」。
每个线程的工作内存都是独立的,「线程操作数据只能在工作内存中进行,然后刷回到主存」。这是 Java 内存模型定义的线程基本工作方式。
Java内存模型规定所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数。每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行。「线程不能直接读写主内存中的变量」。
每个线程的工作内存都是独立的,「线程操作数据只能在工作内存中进行,然后刷回到主存」。这是 Java 内存模型定义的线程基本工作方式。
原⼦性:Java内存模型通过read、load、assign、use、store、write来保证原⼦性操作,此外还有lock
和unlock,直接对应着synchronized关键字的monitorenter和monitorexit字节码指令。
可⻅性:Java保证可⻅性可以认为通过volatile、synchronized、
final来实现。
有序性:由于处理器和编译器的重排序导致的有序性问题,Java通过volatile、synchronized来保证。
和unlock,直接对应着synchronized关键字的monitorenter和monitorexit字节码指令。
可⻅性:Java保证可⻅性可以认为通过volatile、synchronized、
final来实现。
有序性:由于处理器和编译器的重排序导致的有序性问题,Java通过volatile、synchronized来保证。
happen-before规则
(1)程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
(2)监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
(3)volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
(4)传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
(5)start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
(6)Join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
(7)程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。
(8)对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。
(2)监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
(3)volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
(4)传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
(5)start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
(6)Join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
(7)程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。
(8)对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。
volatile原理知道吗?
相⽐synchronized的加锁⽅式来解决共享变量的内存可⻅性问题,volatile就是更轻量的选择,他没有上
下⽂切换的额外开销成本。使⽤volatile声明的变量,可以确保值被更新的时候对其他线程⽴刻可⻅(缓存一致性协议)。
volatile使⽤内存屏障来保证不会发⽣指令重排,解决了内存可⻅性的问题。
下⽂切换的额外开销成本。使⽤volatile声明的变量,可以确保值被更新的时候对其他线程⽴刻可⻅(缓存一致性协议)。
volatile使⽤内存屏障来保证不会发⽣指令重排,解决了内存可⻅性的问题。
说内存屏障的问题,volatile修饰之后会加⼊不同的内存屏障来保证可⻅性的问题能正确执⾏。这⾥
写的屏障基于书中提供的内容,但是实际上由于CPU架构不同,重排序的策略不同,提供的内存屏障也
不⼀样,⽐如x86平台上,只有StoreLoad⼀种内存屏障。
1. StoreStore屏障,保证上⾯的普通写不和volatile写发⽣重排序
2. StoreLoad屏障,保证volatile写与后⾯可能的volatile读写不发⽣重排序
3. LoadLoad屏障,禁⽌volatile读与后⾯的普通读重排序
4. LoadStore屏障,禁⽌volatile读和后⾯的普通写重排序
写的屏障基于书中提供的内容,但是实际上由于CPU架构不同,重排序的策略不同,提供的内存屏障也
不⼀样,⽐如x86平台上,只有StoreLoad⼀种内存屏障。
1. StoreStore屏障,保证上⾯的普通写不和volatile写发⽣重排序
2. StoreLoad屏障,保证volatile写与后⾯可能的volatile读写不发⽣重排序
3. LoadLoad屏障,禁⽌volatile读与后⾯的普通读重排序
4. LoadStore屏障,禁⽌volatile读和后⾯的普通写重排序
介绍一下四种引用类型?
「强引用 StrongReference」
Object obj = new Object();
//只要obj还指向Object对象,Object对象就不会被回收
垃圾回收器不会回收被引用的对象,哪怕内存不足时,JVM 也会直接抛出 OutOfMemoryError,除非赋值为 null。
Object obj = new Object();
//只要obj还指向Object对象,Object对象就不会被回收
垃圾回收器不会回收被引用的对象,哪怕内存不足时,JVM 也会直接抛出 OutOfMemoryError,除非赋值为 null。
「软引用 SoftReference」
软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。
软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。
「弱引用 WeakReference」
弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。
弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。
「虚引用 PhantomReference」
虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用,NIO 的堆外内存就是靠其管理
虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用,NIO 的堆外内存就是靠其管理
ThreadLocal 是什么?有哪些使用场景?
线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。
ThreadLocal 有一个「静态内部类 ThreadLocalMap」,ThreadLocalMap 又包含了一个 Entry 数组,「Entry 本身是一个弱引用」,他的 key 是指向 ThreadLocal 的弱引用,「弱引用的目的是为了防止内存泄露」,如果是强引用那么除非线程结束,否则无法终止,可能会有内存泄漏的风险。
但是这样还是会存在内存泄露的问题,假如 key 和 ThreadLocal 对象被回收之后,entry 中就存在 key 为 null ,但是 value 有值的 entry 对象,但是永远没办法被访问到,同样除非线程结束运行。「解决方法就是调用 remove 方法删除 entry 对象」。
说一下 atomic 的原理?
Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。
Atomic系列的类中的核心方法都会调用unsafe类中的几个本地方法。我们需要先知道一个东西就是Unsafe类,全名为:sun.misc.Unsafe,这个类包含了大量的对C代码的操作,包括很多直接内存分配以及原子操作的调用,而它之所以标记为非安全的,是告诉你这个里面大量的方法调用都会存在安全隐患,需要小心使用,否则会导致严重的后果,例如在通过unsafe分配内存的时候,如果自己指定某些区域可能会导致一些类似C++一样的指针越界到其他进程的问题。
JVM
说一下 jvm 的主要组成部分?及其作用?
类加载器(ClassLoader)
运行时数据区(Runtime Data Area)
执行引擎(Execution Engine)
本地库接口(Native Interface)
运行时数据区(Runtime Data Area)
执行引擎(Execution Engine)
本地库接口(Native Interface)
「Class loader(类装载):」 根据给定的全限定名类名(如:java.lang.Object)来装载class文件
到运行时数据区的方法区中。
「Execution engine(执行引擎)」:执行class的指令。
「Native Interface(本地接口):」 与native lib交互,是其它编程语言交互的接口。
「Runtime data area(运行时数据区域)」:即我们常说的JVM的内存。
到运行时数据区的方法区中。
「Execution engine(执行引擎)」:执行class的指令。
「Native Interface(本地接口):」 与native lib交互,是其它编程语言交互的接口。
「Runtime data area(运行时数据区域)」:即我们常说的JVM的内存。
说说 JVM 内存区域
「1.程序计数器」
程序计数器是「程序控制流的指示器,循环,跳转,异常处理,线程的恢复等工作都需要依赖程序计数器去完成」。程序计数器是「线程私有」的,它的「生命周期是和线程保持一致」的,我们知道,N 个核心数的 CPU 在同一时刻,最多有 N个线程同时运行,在我们真实的使用过程中可能会创建很多线程,JVM 的多线程其实是通过线程轮流切换,分配处理器执行时间来实现的。既然涉及的线程切换,所以每条线程必须有一个独立的程序计数器。
程序计数器是「程序控制流的指示器,循环,跳转,异常处理,线程的恢复等工作都需要依赖程序计数器去完成」。程序计数器是「线程私有」的,它的「生命周期是和线程保持一致」的,我们知道,N 个核心数的 CPU 在同一时刻,最多有 N个线程同时运行,在我们真实的使用过程中可能会创建很多线程,JVM 的多线程其实是通过线程轮流切换,分配处理器执行时间来实现的。既然涉及的线程切换,所以每条线程必须有一个独立的程序计数器。
「2.虚拟机栈」
虚拟机栈,其描述的就是线程内存模型,「也可以称作线程栈」,也是每个「线程私有」的,「生命周期与线程保持一致」。在每个方法执行的时候,jvm 都会同步创建一个栈帧去存储局部变量表,操作数栈,动态连接,方法出口等信息。一个方法的生命周期就贯彻了一个栈帧从入栈到出栈的全部过程。
虚拟机栈,其描述的就是线程内存模型,「也可以称作线程栈」,也是每个「线程私有」的,「生命周期与线程保持一致」。在每个方法执行的时候,jvm 都会同步创建一个栈帧去存储局部变量表,操作数栈,动态连接,方法出口等信息。一个方法的生命周期就贯彻了一个栈帧从入栈到出栈的全部过程。
「3.本地方法栈」本地方法栈的概念很好理解,我们知道,java底层用了很多c的代码去实现,而其调用c端的方法上都会有native,代表本地方法服务,而本地方法栈就是为其服务的
「4.堆」堆可以说是jvm中最大的一块儿内存区域了,它是所有线程共享的,不管你是初学者还是资深开发,多少都会听说过堆,毕竟几乎所有的对象都会在堆中分配。
「5.方法区」
方法区也是所有「线程共享」的区域,它「存储」了被 jvm 加载的「类型信息、常量、静态变量等数据」。运行时常量池就是方法区的一部分,编译期生成的各种字面量与符号引用就存储在其中。
方法区也是所有「线程共享」的区域,它「存储」了被 jvm 加载的「类型信息、常量、静态变量等数据」。运行时常量池就是方法区的一部分,编译期生成的各种字面量与符号引用就存储在其中。
「6.直接内存」
这部分数据并「不是 jvm 运行时数据区的一部分」,nio 就会使用到直接内存,也可以说「堆外内存」,通常会「配合虚引用一起去使用」,就是为了资源释放,会将堆外内存开辟空间的信息存储到一个队列中,然后GC会去清理这部分空间。堆外内存优势在 IO 操作上,对于网络 IO,使用 Socket 发送数据时,能够节省堆内存到堆外内存的数据拷贝,所以性能更高。看过 Netty 源码的同学应该了解,Netty 使用堆外内存池来实现零拷贝技术。对于磁盘 IO 时,也可以使用内存映射,来提升性能。另外,更重要的几乎不用考虑堆内存烦人的 GC 问题。但是既然是内存。也会受到本机总内存的限制,
这部分数据并「不是 jvm 运行时数据区的一部分」,nio 就会使用到直接内存,也可以说「堆外内存」,通常会「配合虚引用一起去使用」,就是为了资源释放,会将堆外内存开辟空间的信息存储到一个队列中,然后GC会去清理这部分空间。堆外内存优势在 IO 操作上,对于网络 IO,使用 Socket 发送数据时,能够节省堆内存到堆外内存的数据拷贝,所以性能更高。看过 Netty 源码的同学应该了解,Netty 使用堆外内存池来实现零拷贝技术。对于磁盘 IO 时,也可以使用内存映射,来提升性能。另外,更重要的几乎不用考虑堆内存烦人的 GC 问题。但是既然是内存。也会受到本机总内存的限制,
说一下堆栈的区别?
栈是运行时单位,代表着逻辑,内含基本数据类型和堆中对象引用,所在区域连续,没有碎片;堆
是存储单位,代表着数据,可被多个栈共享(包括成员中基本数据类型、引用和引用对象),所在
区域不连续,会有碎片。
1、功能不同
栈内存用来存储局部变量和方法调用,而堆内存用来存储Java中的对象。无论是成员变量,局部变
量,还是类变量,它们指向的对象都存储在堆内存中。
2、共享性不同
栈内存是线程私有的。 堆内存是所有线程共有的。
3、异常错误不同
如果栈内存或者堆内存不足都会抛出异常。 栈空间不足:java.lang.StackOverFlowError。 堆空间
不足:java.lang.OutOfMemoryError。
4、空间大小
栈的空间大小远远小于堆的。
是存储单位,代表着数据,可被多个栈共享(包括成员中基本数据类型、引用和引用对象),所在
区域不连续,会有碎片。
1、功能不同
栈内存用来存储局部变量和方法调用,而堆内存用来存储Java中的对象。无论是成员变量,局部变
量,还是类变量,它们指向的对象都存储在堆内存中。
2、共享性不同
栈内存是线程私有的。 堆内存是所有线程共有的。
3、异常错误不同
如果栈内存或者堆内存不足都会抛出异常。 栈空间不足:java.lang.StackOverFlowError。 堆空间
不足:java.lang.OutOfMemoryError。
4、空间大小
栈的空间大小远远小于堆的。
队列和栈是什么?有什么区别?
队列和栈都是被用来预存储数据的。
队列允许先进先出检索元素,但也有例外的情况,Deque 接口允许从两端检索元素。
栈和队列很相似,但它运行对元素进行后进先出进行检索。
队列允许先进先出检索元素,但也有例外的情况,Deque 接口允许从两端检索元素。
栈和队列很相似,但它运行对元素进行后进先出进行检索。
知道new⼀个对象的过程吗?
当虚拟机遇⻅new关键字时候,实现判断当前类是否已经加载,如果类没有加载,⾸先执⾏类的加载机
制,加载完成后再为对象分配空间、初始化等
制,加载完成后再为对象分配空间、初始化等
加载:根据查找路径找到相应的 class 文件然后导入;
验证:检查加载的 class 文件的正确性;
准备:给类中的静态变量分配内存空间;
解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
初始化:对静态变量和静态代码块执行初始化工作,如果存在⽗类,先对⽗类进⾏初始化
Ps:静态代码块是绝对线程安全的,只能隐式被java虚拟机在类加载过程中初始化调⽤!
验证:检查加载的 class 文件的正确性;
准备:给类中的静态变量分配内存空间;
解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
初始化:对静态变量和静态代码块执行初始化工作,如果存在⽗类,先对⽗类进⾏初始化
Ps:静态代码块是绝对线程安全的,只能隐式被java虚拟机在类加载过程中初始化调⽤!
当类加载完成之后,紧接着就是对象分配内存空间和初始化的过程
1. ⾸先为对象分配合适⼤⼩的内存空间
2. 接着为实例变量赋默认值
3. 设置对象的头信息,对象hash码、GC分代年龄、元数据信息等
4. 执⾏构造函数(init)初始化
2. 接着为实例变量赋默认值
3. 设置对象的头信息,对象hash码、GC分代年龄、元数据信息等
4. 执⾏构造函数(init)初始化
介绍一下双亲委派模型,它的好处是什么?
类加载器⾃顶向下分为:
1. Bootstrap ClassLoader启动类加载器:默认会去加载JAVA_HOME/lib⽬录下的jar
2. Extention ClassLoader扩展类加载器:默认去加载JAVA_HOME/lib/ext⽬录下的jar
3. Application ClassLoader应⽤程序类加载器:⽐如我们的web应⽤,会加载web程序中ClassPath
下的类
4. User ClassLoader⽤户⾃定义类加载器:由⽤户⾃⼰定义
1. Bootstrap ClassLoader启动类加载器:默认会去加载JAVA_HOME/lib⽬录下的jar
2. Extention ClassLoader扩展类加载器:默认去加载JAVA_HOME/lib/ext⽬录下的jar
3. Application ClassLoader应⽤程序类加载器:⽐如我们的web应⽤,会加载web程序中ClassPath
下的类
4. User ClassLoader⽤户⾃定义类加载器:由⽤户⾃⼰定义
当我们在加载类的时候,⾸先都会向上询问⾃⼰的⽗加载器是否已经加载,如果没有则依次向上询问,
如果没有加载,则从上到下依次尝试是否能加载当前类,直到加载成功。
如果没有加载,则从上到下依次尝试是否能加载当前类,直到加载成功。
「好处:」
说这个问题前我要先和大家说一个概念,「Jvm 中类的唯一性是由类本身和加载这个类的类加载器决定的」,简单的说,如果有个a类,如果被两个不同的类加载器加载,那么他们必不相等。你看到这里会不会想到所有类的父类都是 Object 是怎么实现的了吗?是因为无论哪一个类加载器加载 Object 类,都会交给最顶层的启动类加载器去加载,这样就「保证了 Object 类在 Jvm 中是唯一的」
说这个问题前我要先和大家说一个概念,「Jvm 中类的唯一性是由类本身和加载这个类的类加载器决定的」,简单的说,如果有个a类,如果被两个不同的类加载器加载,那么他们必不相等。你看到这里会不会想到所有类的父类都是 Object 是怎么实现的了吗?是因为无论哪一个类加载器加载 Object 类,都会交给最顶层的启动类加载器去加载,这样就「保证了 Object 类在 Jvm 中是唯一的」
如何自定义类加载器?
在自定义ClassLoader的子类时,两种方式:
重写loadClass方法(是实现双亲委派逻辑的地方,修改他会破坏双亲委派机制,不推荐)
重写findClass方法 (推荐)
重写loadClass方法(是实现双亲委派逻辑的地方,修改他会破坏双亲委派机制,不推荐)
重写findClass方法 (推荐)
垃圾对象是怎么找到的?
「1.引用计数算法」
「2.根可达算法」
这也是「「JVM 默认使用」」的寻找垃圾算法它的原理就是定义了一系列的根,我们把它称为 「「"GC Roots"」」 ,从 「「"GC Roots"」」 开始往下进行搜索,走过的路径我们把它称为 「「"引用链"」」 ,当一个对象到 「「"GC Roots"」」 之间没有任何引用链相连时,那么这个对象就可以被当做垃圾回收了。
GC Roots 有哪些?
两个栈: Java栈 和 Native 栈中所有引用的对象;
两个方法区:方法区中的常量和静态变量;
所有线程对象;
所有跨代引用对象;
和已知 GCRoots 对象同属一个CardTable 的其他对象
两个方法区:方法区中的常量和静态变量;
所有线程对象;
所有跨代引用对象;
和已知 GCRoots 对象同属一个CardTable 的其他对象
常见的垃圾回收器?
Serial是一个「「单线程」」的垃圾回收器,「「采用复制算法负责新生代」」的垃圾回收工作,可以与 CMS 垃圾回收器一起搭配工作。
ParNew:Serial的多线程版本,⽤于和CMS配合使⽤
CMS收集器是以获取最短停顿时间为⽬标的收集器,相对于其他
的收集器STW的时间更短暂,可以并⾏收集是他的特点,同时他基于标记-清除算法,整个GC的过程分
为4步。
1. 初始标记:标记GC ROOT能关联到的对象,需要STW
2. 并发标记:从GCRoots的直接关联对象开始遍历整个对象图的过程,不需要STW
3. 重新标记:为了修正并发标记期间,因⽤户程序继续运作⽽导致标记产⽣改变的标记,需要STW
4. 并发清除:清理删除掉标记阶段判断的已经死亡的对象,不需要STW
从整个过程来看,并发标记和并发清除的耗时最⻓,但是不需要停⽌⽤户线程,⽽初始标记和重新标记
的耗时较短,但是需要停⽌⽤户线程,总体⽽⾔,整个过程造成的停顿时间较短,⼤部分时候是可以和
⽤户线程⼀起⼯作的。
的收集器STW的时间更短暂,可以并⾏收集是他的特点,同时他基于标记-清除算法,整个GC的过程分
为4步。
1. 初始标记:标记GC ROOT能关联到的对象,需要STW
2. 并发标记:从GCRoots的直接关联对象开始遍历整个对象图的过程,不需要STW
3. 重新标记:为了修正并发标记期间,因⽤户程序继续运作⽽导致标记产⽣改变的标记,需要STW
4. 并发清除:清理删除掉标记阶段判断的已经死亡的对象,不需要STW
从整个过程来看,并发标记和并发清除的耗时最⻓,但是不需要停⽌⽤户线程,⽽初始标记和重新标记
的耗时较短,但是需要停⽌⽤户线程,总体⽽⾔,整个过程造成的停顿时间较短,⼤部分时候是可以和
⽤户线程⼀起⼯作的。
「1.初始标记」
初始标记只是标记出来「「和 GC Roots 直接关联」」的对象,整个速度是非常快的,为了保证标记的准确,这部分会在 「「STW」」 的状态下运行。
「2.并发标记」
并发标记这个阶段会直接根据第一步关联的对象找到「「所有的引用」」关系,这一部分时刻用户线程「「并发运行」」的,虽然耗时较长,但是不会有很大的影响。
「3.重新标记」
重新标记是为了解决第二步并发标记所导致的标错情况,这里简单举个例子:并发标记时a没有被任何对象引用,此时垃圾回收器将该对象标位垃圾,在之后的标记过程中,a又被其他对象引用了,这时候如果不进行重新标记就会发生「「误清除」」。这部分内容也是在「「STW」」的情况下去标记的。
「4.并发清除」
这一步就是最后的清除阶段了,将之前「「真正确认为垃圾的对象回收」」,这部分会和用户线程一起并发执行。
初始标记只是标记出来「「和 GC Roots 直接关联」」的对象,整个速度是非常快的,为了保证标记的准确,这部分会在 「「STW」」 的状态下运行。
「2.并发标记」
并发标记这个阶段会直接根据第一步关联的对象找到「「所有的引用」」关系,这一部分时刻用户线程「「并发运行」」的,虽然耗时较长,但是不会有很大的影响。
「3.重新标记」
重新标记是为了解决第二步并发标记所导致的标错情况,这里简单举个例子:并发标记时a没有被任何对象引用,此时垃圾回收器将该对象标位垃圾,在之后的标记过程中,a又被其他对象引用了,这时候如果不进行重新标记就会发生「「误清除」」。这部分内容也是在「「STW」」的情况下去标记的。
「4.并发清除」
这一步就是最后的清除阶段了,将之前「「真正确认为垃圾的对象回收」」,这部分会和用户线程一起并发执行。
CMS的「「三个缺点」」:
「1.影响用户线程的执行效率」
CMS默认启动的回收线程数是(处理器核心数 + 3)/ 4 ,由于是和用户线程一起并发清理,那么势必会影响到用户线程的执行速度,并且这个影响「「随着核心线程数的递减而增加」」。所以 JVM 提供了一种 "「「增量式并发收集器」」"的 CMS 变种,主要是用来减少垃圾回收线程独占资源的时间,所以会感觉到回收时间变长,这样的话「「单位时间内处理垃圾的效率就会降低」」,也是一种缓和的方案。
「2.会产生"浮动垃圾"」
之前说到 CMS 真正清理垃圾是和用户线程一起进行的,在「「清理」」这部分垃圾的时候「「用户线程会产生新的垃圾」」,这部分垃圾就叫做浮动垃圾,并且只能等着下一次的垃圾回收再清除。
「3.会产生碎片化的空间」
CMS 是使用了标记删除的算法去清理垃圾的,而这种算法的缺点就是会产生「「碎片化」」,后续可能会「「导致大对象无法分配」」从而触发「「和 Serial Old 一起配合使用」」来处理碎片化的问题,当然这也处于 「「STW」」的情况下,所以当 java 应用非常庞大时,如果采用了 CMS 垃圾回收器,产生了碎片化,那么在 STW 来处理碎片化的时间会非常之久。
「1.影响用户线程的执行效率」
CMS默认启动的回收线程数是(处理器核心数 + 3)/ 4 ,由于是和用户线程一起并发清理,那么势必会影响到用户线程的执行速度,并且这个影响「「随着核心线程数的递减而增加」」。所以 JVM 提供了一种 "「「增量式并发收集器」」"的 CMS 变种,主要是用来减少垃圾回收线程独占资源的时间,所以会感觉到回收时间变长,这样的话「「单位时间内处理垃圾的效率就会降低」」,也是一种缓和的方案。
「2.会产生"浮动垃圾"」
之前说到 CMS 真正清理垃圾是和用户线程一起进行的,在「「清理」」这部分垃圾的时候「「用户线程会产生新的垃圾」」,这部分垃圾就叫做浮动垃圾,并且只能等着下一次的垃圾回收再清除。
「3.会产生碎片化的空间」
CMS 是使用了标记删除的算法去清理垃圾的,而这种算法的缺点就是会产生「「碎片化」」,后续可能会「「导致大对象无法分配」」从而触发「「和 Serial Old 一起配合使用」」来处理碎片化的问题,当然这也处于 「「STW」」的情况下,所以当 java 应用非常庞大时,如果采用了 CMS 垃圾回收器,产生了碎片化,那么在 STW 来处理碎片化的时间会非常之久。
G1作为JDK9之后的服务端默认收集器,且不再区分年轻代和⽼年代进⾏垃圾回收,他把内存划分为多个
Region,每个Region的⼤⼩可以通过-XX:G1HeapRegionSize设置,⼤⼩为1~32M,对于⼤对象的存
储则衍⽣出Humongous的概念,超过Region⼤⼩⼀半的对象会被认为是⼤对象,⽽超过整个Region⼤
⼩的对象被认为是超级⼤对象,将会被存储在连续的N个Humongous Region中,G1在进⾏回收的时候
会在后台维护⼀个优先级列表,每次根据⽤户设定允许的收集停顿时间优先回收收益最⼤的Region。
Region,每个Region的⼤⼩可以通过-XX:G1HeapRegionSize设置,⼤⼩为1~32M,对于⼤对象的存
储则衍⽣出Humongous的概念,超过Region⼤⼩⼀半的对象会被认为是⼤对象,⽽超过整个Region⼤
⼩的对象被认为是超级⼤对象,将会被存储在连续的N个Humongous Region中,G1在进⾏回收的时候
会在后台维护⼀个优先级列表,每次根据⽤户设定允许的收集停顿时间优先回收收益最⼤的Region。
G1的回收过程分为以下四个步骤:
1. 初始标记:标记GC ROOT能关联到的对象,需要STW
2. 并发标记:从GCRoots的直接关联对象开始遍历整个对象图的过程,扫描完成后还会重新处理并发
标记过程中产⽣变动的对象
3. 最终标记:短暂暂停⽤户线程,再处理⼀次,需要STW
4. 筛选回收:更新Region的统计数据,对每个Region的回收价值和成本排序,根据⽤户设置的停顿
时间制定回收计划。再把需要回收的Region中存活对象复制到空的Region,同时清理旧的
Region。需要STW
1. 初始标记:标记GC ROOT能关联到的对象,需要STW
2. 并发标记:从GCRoots的直接关联对象开始遍历整个对象图的过程,扫描完成后还会重新处理并发
标记过程中产⽣变动的对象
3. 最终标记:短暂暂停⽤户线程,再处理⼀次,需要STW
4. 筛选回收:更新Region的统计数据,对每个Region的回收价值和成本排序,根据⽤户设置的停顿
时间制定回收计划。再把需要回收的Region中存活对象复制到空的Region,同时清理旧的
Region。需要STW
总的来说除了并发标记之外,其他⼏个过程也还是需要短暂的STW,G1的⽬标是在停顿和延迟可控的情
况下尽可能提⾼吞吐量。
况下尽可能提⾼吞吐量。
什么情况下内存对象会从新生代进入老年代?
年龄达到15岁
默认是15岁,年龄在对象头中记录
动态年龄判断
当前放对象的Survivor区域里,一批对象的总大小大于这块区域的50%,那么此时大于等于这批年龄的对象直接进入老年代
大对象直接进入老年代
老年代空间分配担保机制
java 有哪四种引用类型?
「1.强引用」
"Object o = new Object()" 就是一种强引用关系,这也是我们在代码中最常用的一种引用关系。无论任何情况下,只要强引用关系还存在,垃圾回收器就不会回收掉被引用的对象。
"Object o = new Object()" 就是一种强引用关系,这也是我们在代码中最常用的一种引用关系。无论任何情况下,只要强引用关系还存在,垃圾回收器就不会回收掉被引用的对象。
「2.软引用」
当内存空间不足时,就会回收软引用对象。
当内存空间不足时,就会回收软引用对象。
「3.弱引用」
弱引用要比软引用更弱一点,它「「只能够存活到下次垃圾回收之前」」。也就是说,垃圾回收器开始工作,会回收掉所有只被弱引用关联的对象。
弱引用要比软引用更弱一点,它「「只能够存活到下次垃圾回收之前」」。也就是说,垃圾回收器开始工作,会回收掉所有只被弱引用关联的对象。
「4.虚引用」
虚引用是最弱的一种引用关系,它的唯一作用是用来作为一种通知。如零拷贝(Zero Copy),开辟了堆外内存,虚引用在这里使用,会将这部分信息存储到一个队列中,以便于后续对堆外内存的回收管理。
虚引用是最弱的一种引用关系,它的唯一作用是用来作为一种通知。如零拷贝(Zero Copy),开辟了堆外内存,虚引用在这里使用,会将这部分信息存储到一个队列中,以便于后续对堆外内存的回收管理。
说一说分代收集理论
大多数的垃圾回收器都遵循了分代收集的理论进行设计,它建立在两个分代假说之上:
「「弱分代假说」」:绝大多数对象都是朝生夕灭的。
「「强分代假说」」:熬过越多次数垃圾回收过程的对象就越难消亡。
「「弱分代假说」」:绝大多数对象都是朝生夕灭的。
「「强分代假说」」:熬过越多次数垃圾回收过程的对象就越难消亡。
这两种假说的设计原则都是相同的:垃圾收集器「「应该将jvm划分出不同的区域」」,把那些较难回收的对象放在一起(一般指老年代),这个区域的垃圾回收频率就可以降低,减少垃圾回收的开销。剩下的区域(一般指新生代)可以用较高的频率去回收,并且只需要去关心那些存活的对象,也不用标记出需要回收的垃圾,这样就能够以较低的代价去完成垃圾回收。
什么时候会触发FullGC?
除了system.gc()外;
1.老年代内存不足或者达到阈值
2.方法区内存不足
3.survivor放不下,老年代也放不下
4.young gc时统计得到进入老年代的平均大小,大于老年代的剩余大小
1.老年代内存不足或者达到阈值
2.方法区内存不足
3.survivor放不下,老年代也放不下
4.young gc时统计得到进入老年代的平均大小,大于老年代的剩余大小
垃圾收集算法有哪些?
「1.标记清除算法」
这种算法的实现是很简单的,有两种方式
1.标记出垃圾,然后清理掉
2.标记出存货的对象,回收其他空间
这种算法的实现是很简单的,有两种方式
1.标记出垃圾,然后清理掉
2.标记出存货的对象,回收其他空间
这种算法有两个「缺点」
1.随着对象越来越多,那么所需要消耗的时间就会越来越多
2.标记清除后会导致碎片化,如果有大对象分配很有可能分配不下而出发另一次的垃圾收集动作
1.随着对象越来越多,那么所需要消耗的时间就会越来越多
2.标记清除后会导致碎片化,如果有大对象分配很有可能分配不下而出发另一次的垃圾收集动作
「2.标记复制算法」
这种算法解决了第一种算法碎片化的问题。就是「「开辟两块完全相同的区域」」,对象只在其中一篇区域内分配,然后「「标记」」出那些「「存活的对象,按顺序整体移到另外一个空间」」,如下图,可以看到回收后的对象是排列有序的,这种操作只需要移动指针就可以完成,效率很高,「「之后就回收移除前的空间」」。
这种算法解决了第一种算法碎片化的问题。就是「「开辟两块完全相同的区域」」,对象只在其中一篇区域内分配,然后「「标记」」出那些「「存活的对象,按顺序整体移到另外一个空间」」,如下图,可以看到回收后的对象是排列有序的,这种操作只需要移动指针就可以完成,效率很高,「「之后就回收移除前的空间」」。
这种算法的缺点也是很明显的
浪费过多的内存,使现有的「「可用空间变为」」原先的「「一半」」
浪费过多的内存,使现有的「「可用空间变为」」原先的「「一半」」
「3.标记整理算法」
这种算法可以说是结合了前两种算法,既有标记删除,又有整理功能。
这种算法可以说是结合了前两种算法,既有标记删除,又有整理功能。
这种算法就是通过标记清除算法找到存活的对象,然后将所有「「存活的对象,向空间的一端移动」」,然后回收掉其他的内存。
调优命令有哪些?
Sun JDK监控和故障处理命令有jps jstat jmap jhat jstack jinfo
jps,JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。
jstat,JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟
机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
jmap,JVM Memory Map命令用于生成heap dump文件
jhat,JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内
置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看
jstack,用于生成java虚拟机当前时刻的线程快照。
jinfo,JVM Configuration info 这个命令作用是实时查看和调整虚拟机运行参数。
jps,JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。
jstat,JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟
机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
jmap,JVM Memory Map命令用于生成heap dump文件
jhat,JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内
置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看
jstack,用于生成java虚拟机当前时刻的线程快照。
jinfo,JVM Configuration info 这个命令作用是实时查看和调整虚拟机运行参数。
常用的 jvm 调优的参数都有哪些?
-Xms2g:初始化推大小为 2g;
-Xmx2g:堆最大内存为 2g;
-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
-XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
–XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
-XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
-XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
-XX:+PrintGC:开启打印 gc 信息;
-XX:+PrintGCDetails:打印 gc 详细信息。
-Xmx2g:堆最大内存为 2g;
-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
-XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
–XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
-XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
-XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
-XX:+PrintGC:开启打印 gc 信息;
-XX:+PrintGCDetails:打印 gc 详细信息。
项目如何排查JVM的问题?
分两种情况:
1.系统还在运行:
通过公司的监控平台查看jvm的运行情况,比如gc频率,线程数等等
--每次gc回收的垃圾多不,如果回收的多,可以尝试把新生代的内存调大
--系统此时的访问量大不大,系统压力如何
--每次fullgc只回收一点内存,看下dump文件,看下内存里都是有哪些对象,看下代码是不是有问题,比如说system.gc()
具体场景具体分析
2.系统挂了:
一般都会设置当OOM生成dump文件,然后找运维要dump⽂件(生产机器开发一般没权限);然后把dump文件放到mat工具中进行分析,mat工具挺好用的,会直接提示哪里的代码可能会有问题,先按提示排查问题
总之,调优不是⼀蹴⽽就的,需要分析、推理、实践、总结、再分析,最终定位到具体的问题
1.系统还在运行:
通过公司的监控平台查看jvm的运行情况,比如gc频率,线程数等等
--每次gc回收的垃圾多不,如果回收的多,可以尝试把新生代的内存调大
--系统此时的访问量大不大,系统压力如何
--每次fullgc只回收一点内存,看下dump文件,看下内存里都是有哪些对象,看下代码是不是有问题,比如说system.gc()
具体场景具体分析
2.系统挂了:
一般都会设置当OOM生成dump文件,然后找运维要dump⽂件(生产机器开发一般没权限);然后把dump文件放到mat工具中进行分析,mat工具挺好用的,会直接提示哪里的代码可能会有问题,先按提示排查问题
总之,调优不是⼀蹴⽽就的,需要分析、推理、实践、总结、再分析,最终定位到具体的问题
常见调优工具有哪些
常用调优工具分为两类,jdk自带监控工具:jconsole和jvisualvm,第三方有:MAT(Memory
Analyzer Tool)、GChisto。
jconsole,Java Monitoring and Management Console是从java5开始,在JDK中自带的java监
控和管理控制台,用于对JVM中内存,线程和类等的监控
jvisualvm,jdk自带全能工具,可以分析内存快照、线程快照;监控内存变化、GC变化等。
MAT,Memory Analyzer Tool,一个基于Eclipse的内存分析工具,是一个快速、功能丰富的
Java heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗
GChisto,一款专业分析gc日志的工具
Analyzer Tool)、GChisto。
jconsole,Java Monitoring and Management Console是从java5开始,在JDK中自带的java监
控和管理控制台,用于对JVM中内存,线程和类等的监控
jvisualvm,jdk自带全能工具,可以分析内存快照、线程快照;监控内存变化、GC变化等。
MAT,Memory Analyzer Tool,一个基于Eclipse的内存分析工具,是一个快速、功能丰富的
Java heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗
GChisto,一款专业分析gc日志的工具
什么是 STW ?
Java 中「「Stop-The-World机制简称 STW」」 ,是在执行垃圾收集算法时,Java 应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。「Java 中一种全局暂停现象,全局停顿」,所有 Java 代码停止,native 代码可以执行,但不能与 JVM 交互。
为什么需要 STW?
在 java 应用程序中「「引用关系」」是不断发生「「变化」」的,那么就会有会有很多种情况来导致「「垃圾标识」」出错。想想一下如果 Object a 目前是个垃圾,GC 把它标记为垃圾,但是在清除前又有其他对象指向了 Object a,那么此刻 Object a 又不是垃圾了,那么如果没有 STW 就要去无限维护这种关系来去采集正确的信息。再举个例子,到了秋天,道路上洒满了金色的落叶,环卫工人在打扫街道,却永远也无法打扫干净,因为总会有不断的落叶。
如何排查 OOM 的问题?
1.增加两个参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof,当 OOM 发生时自动 dump 堆内存信息到指定目录;
2.同时 jstat 查看监控 JVM 的内存和 GC 情况,先观察问题大概出在什么区域;
3.使用工具载入到 dump 文件,分析大对象的占用情况。
2.同时 jstat 查看监控 JVM 的内存和 GC 情况,先观察问题大概出在什么区域;
3.使用工具载入到 dump 文件,分析大对象的占用情况。
垃圾回收器是怎样寻找 GC Roots 的?
我们在前面说明了根可达算法是通过 GC Roots 来找到存活的对象的,也定义了 GC Roots,那么垃圾回收器是怎样寻找GC Roots 的呢?首先,「「为了保证结果的准确性,GC Roots枚举时是要在STW的情况下进行的」」,但是由于 JAVA 应用越来越大,所以也不能逐个检查每个对象是否为 GC Root,那将消耗大量的时间。一个很自然的想法是,能不能用空间换时间,在某个时候把栈上代表引用的位置全部记录下来,这样到真正 GC 的时候就可以直接读取,而不用再一点一点的扫描了。事实上,大部分主流的虚拟机也正是这么做的,比如 HotSpot ,它使用一种叫做 「「OopMap」」 的数据结构来记录这类信息。
什么是Stop The World ? 什么是OopMap?什么是安全
点?
点?
进行垃圾回收的过程中,会涉及对象的移动。为了保证对象引用更新的正确性,必须暂停所有的用
户线程,像这样的停顿,虚拟机设计者形象描述为「Stop The World」。也简称为STW。
户线程,像这样的停顿,虚拟机设计者形象描述为「Stop The World」。也简称为STW。
在HotSpot中,有个数据结构(映射表)称为「OopMap」。一旦类加载动作完成的时候,
HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,记录到OopMap。在即时编译过
程中,也会在「特定的位置」生成 OopMap,记录下栈上和寄存器里哪些位置是引用。
HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,记录到OopMap。在即时编译过
程中,也会在「特定的位置」生成 OopMap,记录下栈上和寄存器里哪些位置是引用。
这些特定的位置主要在:
1.循环的末尾(非 counted 循环)
2.方法临返回前 / 调用方法的call指令后
3.可能抛异常的位置
这些位置就叫作「安全点(safepoint)。」 用户程序执行时并非在代码指令流的任意位置都能够在
停顿下来开始垃圾收集,而是必须是执行到安全点才能够暂停。
1.循环的末尾(非 counted 循环)
2.方法临返回前 / 调用方法的call指令后
3.可能抛异常的位置
这些位置就叫作「安全点(safepoint)。」 用户程序执行时并非在代码指令流的任意位置都能够在
停顿下来开始垃圾收集,而是必须是执行到安全点才能够暂停。
从线程角度看,安全点可以理解成是在「「代码执行过程中」」的一些「「特殊位置」」,当线程执行到这些位置的时候,说明「「虚拟机当前的状态是安全」」的。比如:「「方法调用、循环跳转、异常跳转等这些地方才会产生安全点」」。如果有需要,可以在这个位置暂停,比如发生GC时,需要暂停所有活动线程,但是线程在这个时刻,还没有执行到一个安全点,所以该线程应该继续执行,到达下一个安全点的时候暂停,等待 GC 结束。那么如何让线程在垃圾回收的时候都跑到最近的安全点呢?这里有「「两种方式」」:
「抢先式中断」
抢先式中断:就是在stw的时候,先让所有线程「「完全中断」」,如果中断的地方不在安全点上,然后「「再激活」」,「「直到运行到安全点的位置」」再中断。
「主动式中断」
主动式中断:在安全点的位置打一个标志位,每个线程执行都去轮询这个标志位,如果为真,就在最近的安全点挂起。
抢先式中断:就是在stw的时候,先让所有线程「「完全中断」」,如果中断的地方不在安全点上,然后「「再激活」」,「「直到运行到安全点的位置」」再中断。
「主动式中断」
主动式中断:在安全点的位置打一个标志位,每个线程执行都去轮询这个标志位,如果为真,就在最近的安全点挂起。
安全区域是什么?解决了什么问题
刚刚说到了主动式中断,但是如果有些线程处于sleep状态怎么办呢?
为了解决这种问题,又引入了安全区域的概念安全区域是指「「在一段代码片中,引用关系不会发生改变」」,实际上就是一个安全点的拓展。当线程执行到安全区域时,首先标识自己已进入安全区域,那样,当在这段时间里 JVM 要发起 GC 时,就不用管标识自己为“安全区域”状态的线程了,该线程只能乖乖的等待根节点枚举完或者整个GC过程完成之后才能继续执行。
为了解决这种问题,又引入了安全区域的概念安全区域是指「「在一段代码片中,引用关系不会发生改变」」,实际上就是一个安全点的拓展。当线程执行到安全区域时,首先标识自己已进入安全区域,那样,当在这段时间里 JVM 要发起 GC 时,就不用管标识自己为“安全区域”状态的线程了,该线程只能乖乖的等待根节点枚举完或者整个GC过程完成之后才能继续执行。
说说三色标记
这里我们又提到了一个概念叫做 「「SATB 原始快照」」,关于SATB会延伸出有一个概念,「「三色标记算法」」,也就是垃圾回收器标记垃圾的时候使用的算法,这里我们简单说下:将对象分为「「三种颜色」」:
白色:没被 GC 访问过的对象(被 GC 标记完后还是白色代表是垃圾)
黑丝:存活的对象
灰色:被 GC 访问过的对象,但是对象引用链上至少还有一个引用没被扫描过
我们知道在 「「并发标记」」 的时候 「「可能会」」 出现 「「误标」」 的情况,这里举两个例子:
1.刚开始标记为 「「垃圾」」 的对象,但是在并发标记过程中 「「变为了存活对象」」
2.刚开始标记为 「「存活」」 的对象,但是在并发标记过程中 「「变为了垃圾对象」」
第一种情况影响还不算很大,只是相当于垃圾没有清理干净,待下一次清理的时候再清理一下就好了。第二种情况就危险了,正在使 「「用的对象的突然被清理掉」」 了,后果会很严重。那么 「「产生上述第二种情况的原因」」 是什么呢?
1.「「新增」」 一条或多条 「「黑色到白色」」 对象的新引用
2.删除 「「了」」 灰色 「「对象」」 到该白色对象 「「的直接」」 引用或间接引用。
当这两种情况 「「都满足」」 的时候就会出现这种问题了。所以为了解决这个问题,引入了 「「增量更新」」 (Incremental Update)和 「「原始快照」」 (SATB)的方案:
增量更新破坏了第一个条件:「「增加新引用时记录」」 该引用信息,在后续 STW 扫描中重新扫描(CMS的使用方案)。
原始快照破坏了第二个条件:「「删除引用时记录下来」」,在后续 STW 扫描时将这些记录过的灰色对象为根再扫描一次(G1的使用方案)。
白色:没被 GC 访问过的对象(被 GC 标记完后还是白色代表是垃圾)
黑丝:存活的对象
灰色:被 GC 访问过的对象,但是对象引用链上至少还有一个引用没被扫描过
我们知道在 「「并发标记」」 的时候 「「可能会」」 出现 「「误标」」 的情况,这里举两个例子:
1.刚开始标记为 「「垃圾」」 的对象,但是在并发标记过程中 「「变为了存活对象」」
2.刚开始标记为 「「存活」」 的对象,但是在并发标记过程中 「「变为了垃圾对象」」
第一种情况影响还不算很大,只是相当于垃圾没有清理干净,待下一次清理的时候再清理一下就好了。第二种情况就危险了,正在使 「「用的对象的突然被清理掉」」 了,后果会很严重。那么 「「产生上述第二种情况的原因」」 是什么呢?
1.「「新增」」 一条或多条 「「黑色到白色」」 对象的新引用
2.删除 「「了」」 灰色 「「对象」」 到该白色对象 「「的直接」」 引用或间接引用。
当这两种情况 「「都满足」」 的时候就会出现这种问题了。所以为了解决这个问题,引入了 「「增量更新」」 (Incremental Update)和 「「原始快照」」 (SATB)的方案:
增量更新破坏了第一个条件:「「增加新引用时记录」」 该引用信息,在后续 STW 扫描中重新扫描(CMS的使用方案)。
原始快照破坏了第二个条件:「「删除引用时记录下来」」,在后续 STW 扫描时将这些记录过的灰色对象为根再扫描一次(G1的使用方案)。
什么情况下会发生栈内存溢出?
Java 栈内存溢出可能抛出两种异常,两种异常虽然都发生在栈内存,但是两者导致内存溢出的根本原因是不一样的:
1.「如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量的时候」,Java 虚拟机将抛出一个 StackOverFlowError 异常。
2.如果 Java 虚拟机栈可以动态拓展,并且扩展的动作已经尝试过,但是目前「无法申请到足够的内存去完成拓展,或者在建立新线程的时候没有足够的内存去创建对应的虚拟机栈」,那 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。
1.「如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量的时候」,Java 虚拟机将抛出一个 StackOverFlowError 异常。
2.如果 Java 虚拟机栈可以动态拓展,并且扩展的动作已经尝试过,但是目前「无法申请到足够的内存去完成拓展,或者在建立新线程的时候没有足够的内存去创建对应的虚拟机栈」,那 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。
对象一定分配在堆中吗?有没有了解逃逸分析技术?
不一定的,JVM通过「逃逸分析」,那些逃不出方法的对象会在栈上
分配。
分配。
什么是逃逸分析?
逃逸分析是指分析指针动态范围的方法,它同编译器优化原理的指针分析和外形分析相关联。当变
量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他方法或者
线程所引用,这种现象称作指针(或者引用)的逃逸(Escape)。通俗点讲,如果一个对象的指针被
多个方法或者线程引用时,那么我们就称这个对象的指针发生了逃逸。
量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他方法或者
线程所引用,这种现象称作指针(或者引用)的逃逸(Escape)。通俗点讲,如果一个对象的指针被
多个方法或者线程引用时,那么我们就称这个对象的指针发生了逃逸。
「逃逸分析的好处」
栈上分配,可以降低垃圾收集器运行的频率。
同步消除,如果发现某个对象只能从一个线程可访问,那么在这个对象上的操作可以不需要同
步。
标量替换,把对象分解成一个个基本类型,并且内存分配不再是分配在堆上,而是分配在栈
上。这样的好处有,一、减少内存使用,因为不用生成对象头。二、程序内存回收效率高,并
且GC频率也会减少。
栈上分配,可以降低垃圾收集器运行的频率。
同步消除,如果发现某个对象只能从一个线程可访问,那么在这个对象上的操作可以不需要同
步。
标量替换,把对象分解成一个个基本类型,并且内存分配不再是分配在堆上,而是分配在栈
上。这样的好处有,一、减少内存使用,因为不用生成对象头。二、程序内存回收效率高,并
且GC频率也会减少。
说一说对象的栈上分配吧?
如果所有对象都分配在堆中那么会给 GC 带来许多不必要的压力,比如有些对象的生命周期只是在当前线程中,为了减少临时对象在堆内分配的数量,就「可以在在栈上分配」,随着线程的消亡而消亡。当然栈上空间必须充足,否则也无法分配,在判断是否能分配到栈上的另一条件就是要经过逃逸分析,
「逃逸分析(Escape Analysis)」:
简单来讲就是:Java Hotspot 虚拟机判断这个新对象是否只会被当前线程引用,并且决定是否能够在 Java 堆上分配内存。
「逃逸分析(Escape Analysis)」:
简单来讲就是:Java Hotspot 虚拟机判断这个新对象是否只会被当前线程引用,并且决定是否能够在 Java 堆上分配内存。
什么是指针碰撞?
一般情况下,JVM的对象都放在堆内存中(发生逃逸分析除外)。当类加载检查通过后,Java虚拟
机开始为新生对象分配内存。如果Java堆中内存是绝对规整的,所有被使用过的的内存都被放到一
边,空闲的内存放到另外一边,中间放着一个指针作为分界点的指示器,所分配内存仅仅是把那个
指针向空闲空间方向挪动一段与对象大小相等的实例,这种分配方式就是 指针碰撞。
机开始为新生对象分配内存。如果Java堆中内存是绝对规整的,所有被使用过的的内存都被放到一
边,空闲的内存放到另外一边,中间放着一个指针作为分界点的指示器,所分配内存仅仅是把那个
指针向空闲空间方向挪动一段与对象大小相等的实例,这种分配方式就是 指针碰撞。
什么是空闲列表?
如果Java堆内存中的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,不可以进
行指针碰撞啦,虚拟机必须维护一个列表,记录哪些内存是可用的,在分配的时候从列表找到一块
大的空间分配给对象实例,并更新列表上的记录,这种分配方式就是空闲列表。
行指针碰撞啦,虚拟机必须维护一个列表,记录哪些内存是可用的,在分配的时候从列表找到一块
大的空间分配给对象实例,并更新列表上的记录,这种分配方式就是空闲列表。
什么是TLAB?
可以把内存分配的动作按照线程划分在不同的空间之中进行,每个线程在Java堆中预先分配一小块
内存,这就是TLAB(Thread Local Allocation Buffer,本地线程分配缓存) 。虚拟机通过 -
XX:UseTLAB 设定它的。
内存,这就是TLAB(Thread Local Allocation Buffer,本地线程分配缓存) 。虚拟机通过 -
XX:UseTLAB 设定它的。
spring
什么是spring?
Spring 是个java企业级应用的开源开发框架。Spring主要用来开发Java应用,但是有些扩展是针对
构建J2EE平台的web应用。Spring 框架目标是简化Java企业级应用开发,并通过POJO为基础的编程
模型促进良好的编程习惯。
构建J2EE平台的web应用。Spring 框架目标是简化Java企业级应用开发,并通过POJO为基础的编程
模型促进良好的编程习惯。
spring的启动流程?
笼统的说法
启动Spring时:
1. ⾸先会进⾏扫描,扫描得到所有的BeanDefinition对象,并存在⼀个Map中
2. 然后筛选出⾮懒加载的单例BeanDefinition进⾏创建Bean,对于多例Bean不需要在启动过程中去 进⾏创建,对于多例Bean会在每次获取Bean时利⽤BeanDefinition去创建
3. 利⽤BeanDefinition创建Bean就是Bean的创建⽣命周期,这期间包括了合并BeanDefinition、推 断构造⽅法、实例化、属性填充、初始化前、初始化、初始化后等步骤,其中AOP就是发⽣在初始 化后这⼀步骤中
4. 单例Bean创建完了之后,Spring会发布⼀个容器启动事件
5. Spring启动结束
启动Spring时:
1. ⾸先会进⾏扫描,扫描得到所有的BeanDefinition对象,并存在⼀个Map中
2. 然后筛选出⾮懒加载的单例BeanDefinition进⾏创建Bean,对于多例Bean不需要在启动过程中去 进⾏创建,对于多例Bean会在每次获取Bean时利⽤BeanDefinition去创建
3. 利⽤BeanDefinition创建Bean就是Bean的创建⽣命周期,这期间包括了合并BeanDefinition、推 断构造⽅法、实例化、属性填充、初始化前、初始化、初始化后等步骤,其中AOP就是发⽣在初始 化后这⼀步骤中
4. 单例Bean创建完了之后,Spring会发布⼀个容器启动事件
5. Spring启动结束
你们项目中为什么使用Spring框架?
轻量:Spring 是轻量的,基本的版本大约2MB。
控制反转:Spring通过控制反转实现了松散耦合,对象们给出它们的依赖,而不是创建或查找
依赖的对象们。
面向切面的编程(AOP):Spring支持面向切面的编程,并且把应用业务逻辑和系统服务分开。
容器:Spring 包含并管理应用中对象的生命周期和配置。
MVC框架:Spring的WEB框架是个精心设计的框架,是Web框架的一个很好的替代品。
事务管理:Spring 提供一个持续的事务管理接口,可以扩展到上至本地事务下至全局事务
(JTA)。
异常处理:Spring 提供方便的API把具体技术相关的异常(比如由JDBC,Hibernate or JDO抛
出的)转化为一致的unchecked 异常。
控制反转:Spring通过控制反转实现了松散耦合,对象们给出它们的依赖,而不是创建或查找
依赖的对象们。
面向切面的编程(AOP):Spring支持面向切面的编程,并且把应用业务逻辑和系统服务分开。
容器:Spring 包含并管理应用中对象的生命周期和配置。
MVC框架:Spring的WEB框架是个精心设计的框架,是Web框架的一个很好的替代品。
事务管理:Spring 提供一个持续的事务管理接口,可以扩展到上至本地事务下至全局事务
(JTA)。
异常处理:Spring 提供方便的API把具体技术相关的异常(比如由JDBC,Hibernate or JDO抛
出的)转化为一致的unchecked 异常。
spring 中都用到了哪些设计模式?
工厂模式
比如通过 BeanFactory 和 ApplicationContext 来生产 Bean 对象
单例模式
Spring 中的 Bean 默认都是单例的
原型模式
代理模式
AOP 的实现方式就是通过代理来实现,Spring主要是使用 JDK 动态代理和 CGLIB 代理
观察者模式
Spring 中的 Event 和 Listener。spring 事件:ApplicationEvent,该抽象类继承了
EventObject 类,JDK 建议所有的事件都应该继承自 EventObject。spring 事件监听器:
ApplicationListener,该接口继承了 EventListener 接口,JDK 建议所有的事件监听器都应该继承
EventListener。
EventObject 类,JDK 建议所有的事件都应该继承自 EventObject。spring 事件监听器:
ApplicationListener,该接口继承了 EventListener 接口,JDK 建议所有的事件监听器都应该继承
EventListener。
适配器模式
Spring 中的 AOP 中 AdvisorAdapter 类,它有三个实现:
MethodBeforAdviceAdapter、AfterReturnningAdviceAdapter、ThrowsAdviceAdapter。Spring
会根据不同的 AOP 配置来使用对应的 Advice,与策略模式不同的是,一个方法可以同时拥有多个
Advice。Spring 存在很多以 Adapter 结尾的,大多数都是适配器模式。
MethodBeforAdviceAdapter、AfterReturnningAdviceAdapter、ThrowsAdviceAdapter。Spring
会根据不同的 AOP 配置来使用对应的 Advice,与策略模式不同的是,一个方法可以同时拥有多个
Advice。Spring 存在很多以 Adapter 结尾的,大多数都是适配器模式。
模板方法模式
Spring 中 jdbcTemplate 等以 Template 结尾的对数据库操作的类,都会使用到模板方法设计模式,一些通用的功能
包装器模式
我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源
spring 中有哪些核心模块?
1.「Spring Core」:Spring核心,它是框架最基础的部分,提供IOC和依赖注入DI特性
2.「Spring Context」:Spring上下文容器,它是 BeanFactory 功能加强的一个子接口
3.「Spring Web」:它提供Web应用开发的支持
4.「Spring MVC」:它针对Web应用中MVC思想的实现
5.「Spring DAO」:提供对JDBC抽象层,简化了JDBC编码,同时,编码更具有健壮性
6.「Spring ORM」:它支持用于流行的ORM框架的整合,比如:Spring + Hibernate、Spring + iBatis、Spring + JDO的整合等
7.「Spring AOP」:即面向切面编程,它提供了与AOP联盟兼容的编程实现
说一下你理解的 IOC 是什么?
首先 IOC 是一个「容器」,是用来装载对象的,它的核心思想就是「控制反转」
那么究竟「什么是控制反转」?
控制反转就是说,「把对象的控制权交给了 spring,由 spring 容器进行管理」,我们不进行任何操作
那么为「什么需要控制反转」?
我们想象一下,没有控制反转的时候,我们需要「自己去创建对象,配置对象」,还要「人工去处理对象与对象之间的各种复杂的依赖关系」,当一个工程的量起来之后,这种关系的维护是非常令人头痛的,所以就有了控制反转这个概念,将对象的创建、配置等一系列操作交给 spring 去管理,我们在使用的时候只要去取就好了
那么究竟「什么是控制反转」?
控制反转就是说,「把对象的控制权交给了 spring,由 spring 容器进行管理」,我们不进行任何操作
那么为「什么需要控制反转」?
我们想象一下,没有控制反转的时候,我们需要「自己去创建对象,配置对象」,还要「人工去处理对象与对象之间的各种复杂的依赖关系」,当一个工程的量起来之后,这种关系的维护是非常令人头痛的,所以就有了控制反转这个概念,将对象的创建、配置等一系列操作交给 spring 去管理,我们在使用的时候只要去取就好了
spring 中的 IOC 容器有哪些?有什么区别?
spring 主要提供了「两种 IOC 容器」,一种是 「BeanFactory」,还有一种是 「ApplicationContext」
它们的区别就在于,BeanFactory 「只提供了最基本的实例化对象和拿对象的功能」,而 ApplicationContext 是继承了 BeanFactory 所派生出来的产物,是其子类,它的作用更加的强大,比如支持注解注入、国际化等功能
它们的区别就在于,BeanFactory 「只提供了最基本的实例化对象和拿对象的功能」,而 ApplicationContext 是继承了 BeanFactory 所派生出来的产物,是其子类,它的作用更加的强大,比如支持注解注入、国际化等功能
ApplicationContext 继承了 BeanFactory,BeanFactory 是 Spring 中比较原始的
Factory,它不支持 AOP、Web 等 Spring 插件。而 ApplicationContext 不仅包含了 BeanFactory
的所有功能,还支持 Spring 的各种插件,还以一种面向框架的方式工作以及对上下文进行分层和实
现继承。
BeanFactory 是 Spring 框架的基础设施,面向 Spring 本身;而 ApplicationContext 面向使用
Spring 的开发者,相比 BeanFactory 提供了更多面向实际应用的功能,几乎所有场合都可以直接使
用 ApplicationContext,而不是底层的 BeanFactory。
Factory,它不支持 AOP、Web 等 Spring 插件。而 ApplicationContext 不仅包含了 BeanFactory
的所有功能,还支持 Spring 的各种插件,还以一种面向框架的方式工作以及对上下文进行分层和实
现继承。
BeanFactory 是 Spring 框架的基础设施,面向 Spring 本身;而 ApplicationContext 面向使用
Spring 的开发者,相比 BeanFactory 提供了更多面向实际应用的功能,几乎所有场合都可以直接使
用 ApplicationContext,而不是底层的 BeanFactory。
那 BeanFactory 和 FactoryBean 又有什么区别?
这两个是「不同的产物」
「BeanFactory 是 IOC 容器」,是用来承载对象的
「FactoryBean 是一个接口」,为 Bean 提供了更加灵活的方式,通过代理一个Bean对象,对方法前后做一些操作。
「BeanFactory 是 IOC 容器」,是用来承载对象的
「FactoryBean 是一个接口」,为 Bean 提供了更加灵活的方式,通过代理一个Bean对象,对方法前后做一些操作。
@Repository、@Service、@Compent、@Controller它们有什么区别?
这四个注解的「本质都是一样的,都是将被该注解标识的对象放入 spring 容器当中,只是为了在使用上区分不同的应用分层」
@Repository:dao层
@Service:service层
@Controller:controller层
@Compent:其他不属于以上三层的统一使用该注解
@Repository:dao层
@Service:service层
@Controller:controller层
@Compent:其他不属于以上三层的统一使用该注解
那么 DI 又是什么?
DI 就是依赖注入,其实和 IOC 大致相同,只不过是「同一个概念使用了不同的角度去阐述」
DI 所描述的「重点是在于依赖」,我们说了 「IOC 的核心功能就是在于在程序运行时动态的向某个对象提供其他的依赖对象」,而这个功能就是依靠 DI 去完成的,比如我们需要注入一个对象 A,而这个对象 A 依赖一个对象 B,那么我们就需要把这个对象 B 注入到对象 A 中,这就是依赖注入
spring 中有三种注入方式
接口注入
构造器注入
set注入
DI 所描述的「重点是在于依赖」,我们说了 「IOC 的核心功能就是在于在程序运行时动态的向某个对象提供其他的依赖对象」,而这个功能就是依靠 DI 去完成的,比如我们需要注入一个对象 A,而这个对象 A 依赖一个对象 B,那么我们就需要把这个对象 B 注入到对象 A 中,这就是依赖注入
spring 中有三种注入方式
接口注入
构造器注入
set注入
依赖注入的方式有几种,各是什么?
构造器注入
setter注入
接口注入
setter注入
接口注入
说说 AOP 是什么?
AOP 意为:「面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术」。
AOP 是 「OOP(面向对象编程) 的延续」,是 Spring 框架中的一个重要内容,是函数式编程的一种衍生范型。利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
「AOP 实现主要分为两类:」
「静态 AOP 实现」, AOP 框架「在编译阶段」对程序源代码进行修改,生成了静态的 AOP 代理类(生成的 *.class 文件已经被改掉了,需要使用特定的编译器),比如 AspectJ
「动态 AOP 实现」, AOP 框架「在运行阶段」对动态生成代理对象(在内存中以 JDK 动态代理,或 CGlib 动态地生成 AOP 代理类),如 SpringAOP
AOP 是 「OOP(面向对象编程) 的延续」,是 Spring 框架中的一个重要内容,是函数式编程的一种衍生范型。利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
「AOP 实现主要分为两类:」
「静态 AOP 实现」, AOP 框架「在编译阶段」对程序源代码进行修改,生成了静态的 AOP 代理类(生成的 *.class 文件已经被改掉了,需要使用特定的编译器),比如 AspectJ
「动态 AOP 实现」, AOP 框架「在运行阶段」对动态生成代理对象(在内存中以 JDK 动态代理,或 CGlib 动态地生成 AOP 代理类),如 SpringAOP
spring 中 AOP 的实现是「通过动态代理实现的」,如果是实现了接口就会使用 JDK 动态代理,否则就使用 CGLIB 代理。
「有 5 种通知类型:」
「@Before」:在目标方法调用前去通知
「@AfterReturning」:在目标方法返回或异常后调用
「@AfterThrowing」:在目标方法返回后调用
「@After」:在目标方法异常后调用
「@Around」:将目标方法封装起来,自己确定调用时机
「@Before」:在目标方法调用前去通知
「@AfterReturning」:在目标方法返回或异常后调用
「@AfterThrowing」:在目标方法返回后调用
「@After」:在目标方法异常后调用
「@Around」:将目标方法封装起来,自己确定调用时机
动态代理和静态代理有什么区别?
「静态代理」
由程序员创建或由特定工具自动生成源代码,再对其编译。在程序运行前,代理类的.class文件就已经存在了
静态代理通常只代理一个类
静态代理事先知道要代理的是什么
「动态代理」
在程序运行时,运用反射机制动态创建而成
动态代理是代理一个接口下的多个实现类
动态代理不知道要代理什么东西,只有在运行时才知道
由程序员创建或由特定工具自动生成源代码,再对其编译。在程序运行前,代理类的.class文件就已经存在了
静态代理通常只代理一个类
静态代理事先知道要代理的是什么
「动态代理」
在程序运行时,运用反射机制动态创建而成
动态代理是代理一个接口下的多个实现类
动态代理不知道要代理什么东西,只有在运行时才知道
如何实现AOP,项⽬哪些地⽅⽤到了AOP?
利⽤动态代理技术来实现AOP,⽐如JDK动态代理或Cglib动态代理,利⽤动态代理技术,可以针对某个类 ⽣成代理对象,当调⽤代理对象的某个⽅法时,可以任意控制该⽅法的执⾏,⽐如可以先打印执⾏时间, 再执⾏该⽅法,并且该⽅法执⾏完成后,再次打印执⾏时间。
项⽬中,⽐如事务、权限控制、⽅法执⾏时⻓⽇志都是通过AOP技术来实现的,凡是需要对某些⽅法做统 ⼀处理的都可以⽤AOP来实现,利⽤AOP可以做到业务⽆侵⼊。
项⽬中,⽐如事务、权限控制、⽅法执⾏时⻓⽇志都是通过AOP技术来实现的,凡是需要对某些⽅法做统 ⼀处理的都可以⽤AOP来实现,利⽤AOP可以做到业务⽆侵⼊。
JDK 动态代理和 CGLIB 代理有什么区别?
JDK 动态代理时业务类「必须要实现某个接口」,它是「基于反射的机制实现的」,生成一个实现同样接口的一个代理类,然后通过重写方法的方式,实现对代码的增强。
CGLIB 动态代理是使用字节码处理框架 ASM,其原理是通过字节码技术为一个类「创建子类,然后重写父类的方法」,实现对代码的增强。
CGLIB 动态代理是使用字节码处理框架 ASM,其原理是通过字节码技术为一个类「创建子类,然后重写父类的方法」,实现对代码的增强。
Spring AOP 和 AspectJ AOP 有什么区别?
Spring AOP 是运行时增强,是通过「动态代理实现」的
AspectJ AOP 是编译时增强,需要特殊的编译器才可以完成,是通过「修改代码来实现」的,支持「三种织入方式」
「编译时织入」:就是在编译字节码的时候织入相关代理类
「编译后织入」:编译完初始类后发现需要 AOP 增强,然后织入相关代码
「类加载时织入」:指在加载器加载类的时候织入
AspectJ AOP 是编译时增强,需要特殊的编译器才可以完成,是通过「修改代码来实现」的,支持「三种织入方式」
「编译时织入」:就是在编译字节码的时候织入相关代理类
「编译后织入」:编译完初始类后发现需要 AOP 增强,然后织入相关代码
「类加载时织入」:指在加载器加载类的时候织入
主要区别 Spring AOP AspecjtJ AOP
增强方式 运行时增强 编译时增强
实现方式 动态代理 修改代码
编译器 javac 特殊的编译器 ajc
效率 较低(运行时反射损耗性能) 较高
织入方式 运行时 编译时、编译后、类加载时
增强方式 运行时增强 编译时增强
实现方式 动态代理 修改代码
编译器 javac 特殊的编译器 ajc
效率 较低(运行时反射损耗性能) 较高
织入方式 运行时 编译时、编译后、类加载时
spring 中 Bean 的生命周期是怎样的?
(1)实例化Bean:
对于BeanFactory容器,当客户向容器请求一个尚未初始化的bean时,或初始化bean的时候需要注
入另一个尚未初始化的依赖时,容器就会调用createBean进行实例化。对于ApplicationContext容
器,当容器启动结束后,通过获取BeanDefinition对象中的信息,实例化所有的bean。
(2)设置对象属性(依赖注入):
实例化后的对象被封装在BeanWrapper对象中,紧接着,Spring根据BeanDefinition中的信息 以 及 通过BeanWrapper提供的设置属性的接口完成依赖注入。
(3)处理Aware接口:
接着,Spring会检测该对象是否实现了xxxAware接口,并将相关的xxxAware实例注入给Bean:
①如果这个Bean已经实现了BeanNameAware接口,会调用它实现的setBeanName(String
beanId)方法,此处传递的就是Spring配置文件中Bean的id值;
②如果这个Bean已经实现了BeanFactoryAware接口,会调用它实现的setBeanFactory()方法,传
递的是Spring工厂自身。
③如果这个Bean已经实现了ApplicationContextAware接口,会调用
setApplicationContext(ApplicationContext)方法,传入Spring上下文;
(4)BeanPostProcessor:
如果想对Bean进行一些自定义的处理,那么可以让Bean实现了BeanPostProcessor接口,那将会
调用postProcessBeforeInitialization(Object obj, String s)方法。
(5)InitializingBean 与 init-method:
如果Bean在Spring配置文件中配置了 init-method 属性,则会自动调用其配置的初始化方法。
(6)如果这个Bean实现了BeanPostProcessor接口,将会调用
postProcessAfterInitialization(Object obj, String s)方法;由于这个方法是在Bean初始化结束时调
用的,所以可以被应用于内存或缓存技术;
以上几个步骤完成后,Bean就已经被正确创建了,之后就可以使用这个Bean了。
(7)DisposableBean: 当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean这个接口,会调用其实现
的destroy()方法;
(8)destroy-method:
最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方
法。
对于BeanFactory容器,当客户向容器请求一个尚未初始化的bean时,或初始化bean的时候需要注
入另一个尚未初始化的依赖时,容器就会调用createBean进行实例化。对于ApplicationContext容
器,当容器启动结束后,通过获取BeanDefinition对象中的信息,实例化所有的bean。
(2)设置对象属性(依赖注入):
实例化后的对象被封装在BeanWrapper对象中,紧接着,Spring根据BeanDefinition中的信息 以 及 通过BeanWrapper提供的设置属性的接口完成依赖注入。
(3)处理Aware接口:
接着,Spring会检测该对象是否实现了xxxAware接口,并将相关的xxxAware实例注入给Bean:
①如果这个Bean已经实现了BeanNameAware接口,会调用它实现的setBeanName(String
beanId)方法,此处传递的就是Spring配置文件中Bean的id值;
②如果这个Bean已经实现了BeanFactoryAware接口,会调用它实现的setBeanFactory()方法,传
递的是Spring工厂自身。
③如果这个Bean已经实现了ApplicationContextAware接口,会调用
setApplicationContext(ApplicationContext)方法,传入Spring上下文;
(4)BeanPostProcessor:
如果想对Bean进行一些自定义的处理,那么可以让Bean实现了BeanPostProcessor接口,那将会
调用postProcessBeforeInitialization(Object obj, String s)方法。
(5)InitializingBean 与 init-method:
如果Bean在Spring配置文件中配置了 init-method 属性,则会自动调用其配置的初始化方法。
(6)如果这个Bean实现了BeanPostProcessor接口,将会调用
postProcessAfterInitialization(Object obj, String s)方法;由于这个方法是在Bean初始化结束时调
用的,所以可以被应用于内存或缓存技术;
以上几个步骤完成后,Bean就已经被正确创建了,之后就可以使用这个Bean了。
(7)DisposableBean: 当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean这个接口,会调用其实现
的destroy()方法;
(8)destroy-method:
最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方
法。
解释Spring支持的几种bean的作用域?
Spring容器中的bean可以分为5个范围:
(1)singleton:默认,每个容器中只有一个bean的实例,单例的模式由BeanFactory自身来维
护。
(2)prototype:为每一个bean请求提供一个实例。
(3)request:为每一个网络请求创建一个实例,在请求完成以后,bean会失效并被垃圾回收器回
收。
(4)session:与request范围类似,确保每个session中有一个bean的实例,在session过期后,
bean会随之失效。
(5)global-session:全局作用域,global-session和Portlet应用相关。当你的应用部署在Portlet
容器中工作时,它包含很多portlet。如果你想要声明让所有的portlet共用全局的存储变量的话,那
么这全局变量需要存储在global-session中。全局作用域与Servlet中的session作用域效果相同。
(1)singleton:默认,每个容器中只有一个bean的实例,单例的模式由BeanFactory自身来维
护。
(2)prototype:为每一个bean请求提供一个实例。
(3)request:为每一个网络请求创建一个实例,在请求完成以后,bean会失效并被垃圾回收器回
收。
(4)session:与request范围类似,确保每个session中有一个bean的实例,在session过期后,
bean会随之失效。
(5)global-session:全局作用域,global-session和Portlet应用相关。当你的应用部署在Portlet
容器中工作时,它包含很多portlet。如果你想要声明让所有的portlet共用全局的存储变量的话,那
么这全局变量需要存储在global-session中。全局作用域与Servlet中的session作用域效果相同。
spring 是怎么解决循环依赖的?
循环依赖就是说两个对象相互依赖,形成了一个环形的调用链路
spring 使用三级缓存去解决循环依赖的,其「核心逻辑就是把实例化和初始化的步骤分开,然后放入缓存中」,供另一个对象调用
「第一级缓存」:用来保存实例化、初始化都完成的对象
「第二级缓存」:用来保存实例化完成,但是未初始化完成的对象
「第三级缓存」:用来保存一个对象工厂,提供一个匿名内部类,用于创建二级缓存中的对象
spring 使用三级缓存去解决循环依赖的,其「核心逻辑就是把实例化和初始化的步骤分开,然后放入缓存中」,供另一个对象调用
「第一级缓存」:用来保存实例化、初始化都完成的对象
「第二级缓存」:用来保存实例化完成,但是未初始化完成的对象
「第三级缓存」:用来保存一个对象工厂,提供一个匿名内部类,用于创建二级缓存中的对象
当 A、B 两个类发生循环引用时 大致流程
1.A 完成实例化后,去「创建一个对象工厂,并放入三级缓存」当中
如果 A 被 AOP 代理,那么通过这个工厂获取到的就是 A 代理后的对象
如果 A 没有被 AOP 代理,那么这个工厂获取到的就是 A 实例化的对象
2.A 进行属性注入时,去「创建 B」
3.B 进行属性注入,需要 A ,则「从三级缓存中去取 A 工厂代理对象」并注入,然后删除三级缓存中的 A 工厂,将 A 对象放入二级缓存
4.B 完成后续属性注入,直到初始化结束,将 B 放入一级缓存
5.「A 从一级缓存中取到 B 并且注入 B」, 直到完成后续操作,将 A 从二级缓存删除并且放入一级缓存,循环依赖结束
1.A 完成实例化后,去「创建一个对象工厂,并放入三级缓存」当中
如果 A 被 AOP 代理,那么通过这个工厂获取到的就是 A 代理后的对象
如果 A 没有被 AOP 代理,那么这个工厂获取到的就是 A 实例化的对象
2.A 进行属性注入时,去「创建 B」
3.B 进行属性注入,需要 A ,则「从三级缓存中去取 A 工厂代理对象」并注入,然后删除三级缓存中的 A 工厂,将 A 对象放入二级缓存
4.B 完成后续属性注入,直到初始化结束,将 B 放入一级缓存
5.「A 从一级缓存中取到 B 并且注入 B」, 直到完成后续操作,将 A 从二级缓存删除并且放入一级缓存,循环依赖结束
spring 解决循环依赖有两个前提条件:
1.「不全是构造器方式」的循环依赖(否则无法分离初始化和实例化的操作)
2.「必须是单例」(否则无法保证是同一对象)
1.「不全是构造器方式」的循环依赖(否则无法分离初始化和实例化的操作)
2.「必须是单例」(否则无法保证是同一对象)
为什么要使用三级缓存,二级缓存不能解决吗?
不可以,主要是为了⽣成代理对象。
因为三级缓存中放的是⽣成具体对象的匿名内部类,他可以⽣成代理对象,也可以是普通的实例对象。
使⽤三级缓存主要是为了保证不管什么时候使⽤的都是⼀个对象。
假设只有⼆级缓存的情况,往⼆级缓存中放的显示⼀个普通的Bean对象, BeanPostProcessor 去⽣成
代理对象之后,覆盖掉⼆级缓存中的普通Bean对象,那么多线程环境下可能取到的对象就不⼀致了。
因为三级缓存中放的是⽣成具体对象的匿名内部类,他可以⽣成代理对象,也可以是普通的实例对象。
使⽤三级缓存主要是为了保证不管什么时候使⽤的都是⼀个对象。
假设只有⼆级缓存的情况,往⼆级缓存中放的显示⼀个普通的Bean对象, BeanPostProcessor 去⽣成
代理对象之后,覆盖掉⼆级缓存中的普通Bean对象,那么多线程环境下可能取到的对象就不⼀致了。
Spring中后置处理器的作⽤?
Spring中的后置处理器分为BeanFactory后置处理器和Bean后置处理器,它们是Spring底层源码架构设计 中⾮常重要的⼀种机制,同时开发者也可以利⽤这两种后置处理器来进⾏扩展。
BeanFactory后置处理器 表示针对BeanFactory的处理器,Spring启动过程中,会先创建出BeanFactory实例,然后利⽤ BeanFactory处理器来加⼯BeanFactory,⽐如Spring的扫描就是基于BeanFactory后置处理器来实现的;
Bean后置处理器也类似,Spring在创建⼀个Bean的过程中,⾸先会实例化得到⼀个对象,然后再 利⽤Bean后置处理器来对该实例对象进⾏加⼯,⽐如我们常说的依赖注⼊就是基于⼀个Bean后置处理器 来实现的,通过该Bean后置处理器来给实例对象中加了@Autowired注解的属性⾃动赋值,还⽐如我们常 说的AOP,也是利⽤⼀个Bean后置处理器来实现的,基于原实例对象,判断是否需要进⾏AOP,如果需 要,那么就基于原实例对象进⾏动态代理,⽣成⼀个代理对象。
BeanFactory后置处理器 表示针对BeanFactory的处理器,Spring启动过程中,会先创建出BeanFactory实例,然后利⽤ BeanFactory处理器来加⼯BeanFactory,⽐如Spring的扫描就是基于BeanFactory后置处理器来实现的;
Bean后置处理器也类似,Spring在创建⼀个Bean的过程中,⾸先会实例化得到⼀个对象,然后再 利⽤Bean后置处理器来对该实例对象进⾏加⼯,⽐如我们常说的依赖注⼊就是基于⼀个Bean后置处理器 来实现的,通过该Bean后置处理器来给实例对象中加了@Autowired注解的属性⾃动赋值,还⽐如我们常 说的AOP,也是利⽤⼀个Bean后置处理器来实现的,基于原实例对象,判断是否需要进⾏AOP,如果需 要,那么就基于原实例对象进⾏动态代理,⽣成⼀个代理对象。
@Autowired 和 @Resource 有什么区别?
「@Resource 是 Java 自己的注解」,@Resource 有两个属性是比较重要的,分是 name 和 type;Spring 将 @Resource 注解的 name 属性解析为 bean 的名字,而 type 属性则解析为 bean 的类型。所以如果使用 name 属性,则使用 byName 的自动注入策略,而使用 type 属性时则使用 byType 自动注入策略。如果既不指定 name 也不指定 type 属性,这时将通过反射机制使用 byName 自动注入策略。
「@Autowired 是spring 的注解」,是 spring2.5 版本引入的,Autowired 只根据 type 进行注入,「不会去匹配 name」。如果涉及到 type 无法辨别注入对象时,那需要依赖 @Qualifier 或 @Primary 注解一起来修饰。
「@Autowired 是spring 的注解」,是 spring2.5 版本引入的,Autowired 只根据 type 进行注入,「不会去匹配 name」。如果涉及到 type 无法辨别注入对象时,那需要依赖 @Qualifier 或 @Primary 注解一起来修饰。
说一下spring的事务机制?
1. Spring事务底层是基于数据库事务和AOP机制的
2. ⾸先对于使⽤了@Transactional注解的Bean,Spring会创建⼀个代理对象作为Bean
3. 当调⽤代理对象的⽅法时,会先判断该⽅法上是否加了@Transactional注解
4. 如果加了,那么则利⽤事务管理器创建⼀个数据库连接
5. 并且修改数据库连接的autocommit属性为false,禁⽌此连接的⾃动提交,这是实现Spring事务⾮常重 要的⼀步
6. 然后执⾏当前⽅法,⽅法中会执⾏sql
7. 执⾏完当前⽅法后,如果没有出现异常就直接提交事务
8. 如果出现了异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务
9. Spring事务的隔离级别对应的就是数据库的隔离级别
10. Spring事务的传播机制是Spring事务⾃⼰实现的,也是Spring事务中最复杂的
11. Spring事务的传播机制是基于数据库连接来做的,⼀个数据库连接⼀个事务,如果传播机制配置为需要 新开⼀个事务,那么实际上就是先建⽴⼀个数据库连接,在此新数据库连接上执⾏sql
2. ⾸先对于使⽤了@Transactional注解的Bean,Spring会创建⼀个代理对象作为Bean
3. 当调⽤代理对象的⽅法时,会先判断该⽅法上是否加了@Transactional注解
4. 如果加了,那么则利⽤事务管理器创建⼀个数据库连接
5. 并且修改数据库连接的autocommit属性为false,禁⽌此连接的⾃动提交,这是实现Spring事务⾮常重 要的⼀步
6. 然后执⾏当前⽅法,⽅法中会执⾏sql
7. 执⾏完当前⽅法后,如果没有出现异常就直接提交事务
8. 如果出现了异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务
9. Spring事务的隔离级别对应的就是数据库的隔离级别
10. Spring事务的传播机制是Spring事务⾃⼰实现的,也是Spring事务中最复杂的
11. Spring事务的传播机制是基于数据库连接来做的,⼀个数据库连接⼀个事务,如果传播机制配置为需要 新开⼀个事务,那么实际上就是先建⽴⼀个数据库连接,在此新数据库连接上执⾏sql
spring 事务隔离级别有哪些?
DEFAULT:采用 DB 默认的事务隔离级别
READ_UNCOMMITTED:读未提交
READ_COMMITTED:读已提交
REPEATABLE_READ:可重复读
SERIALIZABLE:串行化
READ_UNCOMMITTED:读未提交
READ_COMMITTED:读已提交
REPEATABLE_READ:可重复读
SERIALIZABLE:串行化
spring 事务的传播机制有哪些?
1.「propagation_required」
当前方法「必须在一个具有事务的上下文中运行」,如有客户端有事务在进行,那么被调用端将在该事务中运行,否则的话重新开启一个事务。(如果被调用端发生异常,那么调用端和被调用端事务都将回滚)
当前方法「必须在一个具有事务的上下文中运行」,如有客户端有事务在进行,那么被调用端将在该事务中运行,否则的话重新开启一个事务。(如果被调用端发生异常,那么调用端和被调用端事务都将回滚)
2.「propagation_supports」
当前方法不必需要具有一个事务上下文,但是如果有一个事务的话,它也可以在这个事务中运行
当前方法不必需要具有一个事务上下文,但是如果有一个事务的话,它也可以在这个事务中运行
3.「propagation_mandatory」
表示当前方法「必须在一个事务中运行」,如果没有事务,将抛出异常
表示当前方法「必须在一个事务中运行」,如果没有事务,将抛出异常
4.「propagation_nested」
如果当前方法正有一个事务在运行中,则该方法应该「运行在一个嵌套事务」中,被嵌套的事务可以独立于被封装的事务中进行提交或者回滚。如果封装事务存在,并且外层事务抛出异常回滚,那么内层事务必须回滚,反之,内层事务并不影响外层事务。如果封装事务不存在,则同propagation_required的一样
如果当前方法正有一个事务在运行中,则该方法应该「运行在一个嵌套事务」中,被嵌套的事务可以独立于被封装的事务中进行提交或者回滚。如果封装事务存在,并且外层事务抛出异常回滚,那么内层事务必须回滚,反之,内层事务并不影响外层事务。如果封装事务不存在,则同propagation_required的一样
5.「propagation_never」
当方法务不应该在一个事务中运行,如果「存在一个事务,则抛出异常」
当方法务不应该在一个事务中运行,如果「存在一个事务,则抛出异常」
6.「propagation_requires_new」
当前方法「必须运行在它自己的事务中」。一个新的事务将启动,而且如果有一个现有的事务在运行的话,则这个方法将在运行期被挂起,直到新的事务提交或者回滚才恢复执行。
当前方法「必须运行在它自己的事务中」。一个新的事务将启动,而且如果有一个现有的事务在运行的话,则这个方法将在运行期被挂起,直到新的事务提交或者回滚才恢复执行。
7.「propagation_not_supported」
方法不应该在一个事务中运行。「如果有一个事务正在运行,他将在运行期被挂起,直到这个事务提交或者回滚才恢复执行」
方法不应该在一个事务中运行。「如果有一个事务正在运行,他将在运行期被挂起,直到这个事务提交或者回滚才恢复执行」
事务三要素是什么?
数据源:表示具体的事务性资源,是事务的真正处理者,如MySQL等。
事务管理器:像一个大管家,从整体上管理事务的处理过程,如打开、提交、回滚等。
事务应用和属性配置:像一个标识符,表明哪些方法要参与事务,如何参与事务,以及一些相关属
性如隔离级别、超时时间等。
事务管理器:像一个大管家,从整体上管理事务的处理过程,如打开、提交、回滚等。
事务应用和属性配置:像一个标识符,表明哪些方法要参与事务,如何参与事务,以及一些相关属
性如隔离级别、超时时间等。
spring事务失效场景
1.访问权限问题
说白了,在AbstractFallbackTransactionAttributeSource类的computeTransactionAttribute方法中有个判断,如果目标方法不是public,则TransactionAttribute返回null,即不支持事务。
2. 方法用final修饰
为什么?如果你看过spring事务的源码,可能会知道spring事务底层使用了aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。但如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能。注意:如果某个方法是static的,同样无法通过动态代理,变成事务方法。
3.方法内部调用
4.未被spring管理
5.多线程调用
6.表不支持事务
7.未开启事务
spring事务不回滚场景
1.错误的传播特性
2.自己吞了异常
3.手动抛了别的异常
4.自定义了回滚异常
5.嵌套事务回滚多了
说说你对Spring MVC的理解
MVC:MVC是一种设计模式
M-Model 模型(完成业务逻辑:有javaBean构成,service+dao+entity)
V-View 视图(做界面的展示 jsp,html……)
C-Controller 控制器(接收请求—>调用模型—>根据结果派发页面)
V-View 视图(做界面的展示 jsp,html……)
C-Controller 控制器(接收请求—>调用模型—>根据结果派发页面)
工作原理:
1、 用户发送请求至前端控制器DispatcherServlet。
2、 DispatcherServlet收到请求调用HandlerMapping处理器映射器。
3、 处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及处理器
拦截器(如果有则生成)一并返回给DispatcherServlet。
4、 DispatcherServlet调用HandlerAdapter处理器适配器。
5、 HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。
6、 Controller执行完成返回ModelAndView。
7、 HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。
8、 DispatcherServlet将ModelAndView传给ViewReslover视图解析器。
9、 ViewReslover解析后返回具体View。
10、DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。
11、 DispatcherServlet响应用户。
2、 DispatcherServlet收到请求调用HandlerMapping处理器映射器。
3、 处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及处理器
拦截器(如果有则生成)一并返回给DispatcherServlet。
4、 DispatcherServlet调用HandlerAdapter处理器适配器。
5、 HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。
6、 Controller执行完成返回ModelAndView。
7、 HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。
8、 DispatcherServlet将ModelAndView传给ViewReslover视图解析器。
9、 ViewReslover解析后返回具体View。
10、DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。
11、 DispatcherServlet响应用户。
前端控制器(DispatcherServlet):接收请求,响应结果,相当于电脑的CPU。
处理器映射器(HandlerMapping):根据URL去查找处理器。
处理器(Handler):需要程序员去写代码处理逻辑的。
处理器适配器(HandlerAdapter):会把处理器包装成适配器,这样就可以支持多种类型的处理
器,类比笔记本的适配器(适配器模式的应用)。
视图解析器(ViewResovler):进行视图解析,多返回的字符串,进行处理,可以解析成对应的页
面。
处理器映射器(HandlerMapping):根据URL去查找处理器。
处理器(Handler):需要程序员去写代码处理逻辑的。
处理器适配器(HandlerAdapter):会把处理器包装成适配器,这样就可以支持多种类型的处理
器,类比笔记本的适配器(适配器模式的应用)。
视图解析器(ViewResovler):进行视图解析,多返回的字符串,进行处理,可以解析成对应的页
面。
SpringMVC常用的注解有哪些?
@RequestMapping:用于处理请求 url 映射的注解,可用于类或方法上。用于类上,则表示类中
的所有响应请求的方法都是以该地址作为父路径。
@RequestBody:注解实现接收http请求的json数据,将json转换为java对象。
@ResponseBody:注解实现将conreoller方法返回对象转化为json对象响应给客户。
的所有响应请求的方法都是以该地址作为父路径。
@RequestBody:注解实现接收http请求的json数据,将json转换为java对象。
@ResponseBody:注解实现将conreoller方法返回对象转化为json对象响应给客户。
为什么要用SpringBoot?
一、独立运行
Spring Boot而且内嵌了各种servlet容器,Tomcat、Jetty等,现在不再需要打成war包部署到容器
中,Spring Boot只要打成一个可执行的jar包就能独立运行,所有的依赖包都在一个jar包内。
二、简化配置
spring-boot-starter-web启动器自动依赖其他组件,简少了maven的配置。
三、自动配置
Spring Boot能根据当前类路径下的类、jar包来自动配置bean,如添加一个spring-boot-starterweb启动器就能拥有web的功能,无需其他配置。
四、无代码生成和XML配置
Spring Boot配置过程中无代码生成,也无需XML配置文件就能完成所有配置工作,这一切都是借助
于条件注解完成的,这也是Spring4.x的核心功能之一。
五、应用监控
Spring Boot提供一系列端点可以监控服务及应用,做健康检测。
Spring Boot而且内嵌了各种servlet容器,Tomcat、Jetty等,现在不再需要打成war包部署到容器
中,Spring Boot只要打成一个可执行的jar包就能独立运行,所有的依赖包都在一个jar包内。
二、简化配置
spring-boot-starter-web启动器自动依赖其他组件,简少了maven的配置。
三、自动配置
Spring Boot能根据当前类路径下的类、jar包来自动配置bean,如添加一个spring-boot-starterweb启动器就能拥有web的功能,无需其他配置。
四、无代码生成和XML配置
Spring Boot配置过程中无代码生成,也无需XML配置文件就能完成所有配置工作,这一切都是借助
于条件注解完成的,这也是Spring4.x的核心功能之一。
五、应用监控
Spring Boot提供一系列端点可以监控服务及应用,做健康检测。
说说常⽤的SpringBoot注解,及其实现
1. @SpringBootApplication注解:这个注解标识了⼀个SpringBoot⼯程,它实际上是另外三个注解的组 合,这三个注解是:
--a. @SpringBootConfiguration:这个注解实际就是⼀个@Configuration,表示启动类也是⼀个配 置类
--b. @EnableAutoConfiguration:向Spring容器中导⼊了⼀个Selector,⽤来加载ClassPath下 SpringFactories中所定义的⾃动配置类,将这些⾃动加载为配置Bean
--c. @ComponentScan:标识扫描路径,因为默认是没有配置实际扫描路径,所以SpringBoot扫描的 路径是启动类所在的当前⽬录
2. @Bean注解:⽤来定义Bean,类似于XML中的<bean>标签,Spring在启动时,会对加了@Bean注解 的⽅法进⾏解析,将⽅法的名字做为beanName,并通过执⾏⽅法得到bean对象
3. @Controller、@Service、@ResponseBody、@Autowired都可以说
--a. @SpringBootConfiguration:这个注解实际就是⼀个@Configuration,表示启动类也是⼀个配 置类
--b. @EnableAutoConfiguration:向Spring容器中导⼊了⼀个Selector,⽤来加载ClassPath下 SpringFactories中所定义的⾃动配置类,将这些⾃动加载为配置Bean
--c. @ComponentScan:标识扫描路径,因为默认是没有配置实际扫描路径,所以SpringBoot扫描的 路径是启动类所在的当前⽬录
2. @Bean注解:⽤来定义Bean,类似于XML中的<bean>标签,Spring在启动时,会对加了@Bean注解 的⽅法进⾏解析,将⽅法的名字做为beanName,并通过执⾏⽅法得到bean对象
3. @Controller、@Service、@ResponseBody、@Autowired都可以说
springBoot 自动装配原理?
1.容器在启动的时候会调用 EnableAutoConfigurationImportSelector.class 的 selectImports方法「获取一个全面的常用 BeanConfiguration 列表」
2.之后会读取 spring-boot-autoconfigure.jar 下面的spring.factories,「获取到所有的 Spring 相关的 Bean 的全限定名 ClassName」
3.之后继续「调用 filter 来一一筛选」,过滤掉一些我们不需要不符合条件的 Bean
4.最后把符合条件的 BeanConfiguration 注入默认的 EnableConfigurationPropertie 类里面的属性值,并且「注入到 IOC 环境当中」
2.之后会读取 spring-boot-autoconfigure.jar 下面的spring.factories,「获取到所有的 Spring 相关的 Bean 的全限定名 ClassName」
3.之后继续「调用 filter 来一一筛选」,过滤掉一些我们不需要不符合条件的 Bean
4.最后把符合条件的 BeanConfiguration 注入默认的 EnableConfigurationPropertie 类里面的属性值,并且「注入到 IOC 环境当中」
最后,说说Spring Boot 启动流程吧?
1. 准备环境,根据不同的环境创建不同的Environment
2. 准备、加载上下⽂,为不同的环境选择不同的Spring Context,然后加载资源,配置Bean
3. 初始化,这个阶段刷新Spring Context,启动应⽤
4. 最后结束流程
2. 准备、加载上下⽂,为不同的环境选择不同的Spring Context,然后加载资源,配置Bean
3. 初始化,这个阶段刷新Spring Context,启动应⽤
4. 最后结束流程
MyBatis
什么是MyBatis
(1)Mybatis是一个半ORM(对象关系映射)框架,它内部封装了JDBC,开发时只需要关注SQL
语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。程序员直
接编写原生态sql,可以严格控制sql执行性能,灵活度高。
(2)MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO映射成数据库中的记录,避
免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。
(3)通过xml 文件或注解的方式将要执行的各种 statement 配置起来,并通过java对象和
statement中sql的动态参数进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果
映射为java对象并返回。(从执行sql到返回result的过程)
语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。程序员直
接编写原生态sql,可以严格控制sql执行性能,灵活度高。
(2)MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO映射成数据库中的记录,避
免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。
(3)通过xml 文件或注解的方式将要执行的各种 statement 配置起来,并通过java对象和
statement中sql的动态参数进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果
映射为java对象并返回。(从执行sql到返回result的过程)
说说MyBatis的优点和缺点
优点:
(1)基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写 在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并
可重用。
(2)与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连
接;
(3)很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据
库MyBatis都支持)。
(4)能够与Spring很好的集成;
(5)提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象
关系组件维护。
缺点
(1)SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有
一定要求。
(2)SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。
(1)基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写 在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并
可重用。
(2)与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连
接;
(3)很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据
库MyBatis都支持)。
(4)能够与Spring很好的集成;
(5)提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象
关系组件维护。
缺点
(1)SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有
一定要求。
(2)SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。
#{}和${}的区别是什么?
#{}是预编译处理,${}是字符串替换。
Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值;
Mybatis在处理${}时,就是把${}替换成变量的值。
使用#{}可以有效的防止SQL注入,提高系统安全性。
Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值;
Mybatis在处理${}时,就是把${}替换成变量的值。
使用#{}可以有效的防止SQL注入,提高系统安全性。
当实体类中的属性名和表中的字段名不一样 ,怎么办 ?
第1种: 通过在查询的sql语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。
第2种: 通过来映射字段名和实体类属性名的一一对应的关系。
Mybatis是如何进行分页的?分页插件的原理是什么?
Mybatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分
页。可以在sql内直接拼写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物
理分页,比如:MySQL数据的时候,在原有SQL后面拼写limit。
分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截
待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。
页。可以在sql内直接拼写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物
理分页,比如:MySQL数据的时候,在原有SQL后面拼写limit。
分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截
待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。
Mybatis是如何将sql执行结果封装为目标对象并返回的?都有
哪些映射形式?
哪些映射形式?
第一种是使用标签,逐一定义数据库列名和对象属性名之间的映射关系。
第二种是使用sql列的别名功能,将列的别名书写为对象属性名。
有了列名与属性名的映射关系后,Mybatis通过反射创建对象,同时使用反射给对象的属性逐一赋
值并返回,那些找不到映射关系的属性,是无法完成赋值的。
第二种是使用sql列的别名功能,将列的别名书写为对象属性名。
有了列名与属性名的映射关系后,Mybatis通过反射创建对象,同时使用反射给对象的属性逐一赋
值并返回,那些找不到映射关系的属性,是无法完成赋值的。
Xml映射文件中,除了常见的select|insert|updae|delete
标签之外,还有哪些标签?
标签之外,还有哪些标签?
加上动态sql的9个标签,其中为sql片段标签,通过标签引入sql片段,为不支持自增的主键生成策
略标签
略标签
MyBatis实现一对一有几种方式?具体怎么操作的?
有联合查询和嵌套查询,联合查询是几个表联合查询,只查询一次, 通过在resultMap里面配置
association节点配置一对一的类就可以完成;
嵌套查询是先查一个表,根据这个表里面的结果的 外键id,去再另外一个表里面查询数据,也是通过
association配置,但另外一个表的查询通过select属性配置。
association节点配置一对一的类就可以完成;
嵌套查询是先查一个表,根据这个表里面的结果的 外键id,去再另外一个表里面查询数据,也是通过
association配置,但另外一个表的查询通过select属性配置。
Mybatis是否支持延迟加载?如果支持,它的实现原理是什
么?
么?
Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一
对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载
lazyLoadingEnabled=true|false。
它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调
用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的
查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成
a.getB().getName()方法的调用。这就是延迟加载的基本原理。
当然了,不光是Mybatis,几乎所有的包括Hibernate,支持延迟加载的原理都是一样的。
对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载
lazyLoadingEnabled=true|false。
它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调
用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的
查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成
a.getB().getName()方法的调用。这就是延迟加载的基本原理。
当然了,不光是Mybatis,几乎所有的包括Hibernate,支持延迟加载的原理都是一样的。
说说Mybatis的缓存机制
一级缓存localCache
在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的 SQL,
MyBatis 提供了一级缓存的方案优化这部分场景,如果是相同的 SQL 语句,会优先命中一级缓存,
避免直接对数据库进行查询,提高性能。
每个 SqlSession 中持有了 Executor,每个 Executor 中有一个 LocalCache。当用户发起查询时,
MyBatis 根据当前执行的语句生成 MappedStatement,在 Local Cache 进行查询,如果缓存命中
的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入 Local Cache,最后
返回结果给用户
在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的 SQL,
MyBatis 提供了一级缓存的方案优化这部分场景,如果是相同的 SQL 语句,会优先命中一级缓存,
避免直接对数据库进行查询,提高性能。
每个 SqlSession 中持有了 Executor,每个 Executor 中有一个 LocalCache。当用户发起查询时,
MyBatis 根据当前执行的语句生成 MappedStatement,在 Local Cache 进行查询,如果缓存命中
的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入 Local Cache,最后
返回结果给用户
1. MyBatis 一级缓存的生命周期和 SqlSession 一致。
2. MyBatis 一级缓存内部设计简单,只是一个没有容量限定的 HashMap,在缓存的功能性上有
所欠缺。
3. MyBatis 的一级缓存最大范围是 SqlSession 内部,有多个 SqlSession 或者分布式的环境下,
数据库写操作会引起脏数据,建议设定缓存级别为 Statement。
2. MyBatis 一级缓存内部设计简单,只是一个没有容量限定的 HashMap,在缓存的功能性上有
所欠缺。
3. MyBatis 的一级缓存最大范围是 SqlSession 内部,有多个 SqlSession 或者分布式的环境下,
数据库写操作会引起脏数据,建议设定缓存级别为 Statement。
二级缓存
在上文中提到的一级缓存中,其最大的共享范围就是一个 SqlSession 内部,如果多个 SqlSession
之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用 CachingExecutor 装饰
Executor,进入一级缓存的查询流程前,先在 CachingExecutor 进行二级缓存的查询
在上文中提到的一级缓存中,其最大的共享范围就是一个 SqlSession 内部,如果多个 SqlSession
之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用 CachingExecutor 装饰
Executor,进入一级缓存的查询流程前,先在 CachingExecutor 进行二级缓存的查询
二级缓存开启后,同一个 namespace 下的所有操作语句,都影响着同一个 Cache,即二级缓存被
多个 SqlSession 共享,是一个全局的变量。
当开启缓存后,数据的查询执行的流程为:
二级缓存 -> 一级缓存 -> 数据库
1. MyBatis 的二级缓存相对于一级缓存来说,实现了 SqlSession 之间缓存数据的共享,同时粒度
更加细,能够到 namespace 级别,通过 Cache 接口实现类不同的组合,对 Cache 的可控性
也更强。
2. MyBatis 在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件
比较苛刻。
3. 在分布式环境下,由于默认的 MyBatis Cache 实现都是基于本地的,分布式环境下必然会出现
读取到脏数据,需要使用集中式缓存将 MyBatis 的 Cache 接口实现,有一定的开发成本,直
接使用 Redis、Memcached 等分布式缓存可能成本更低,安全性也更高。
多个 SqlSession 共享,是一个全局的变量。
当开启缓存后,数据的查询执行的流程为:
二级缓存 -> 一级缓存 -> 数据库
1. MyBatis 的二级缓存相对于一级缓存来说,实现了 SqlSession 之间缓存数据的共享,同时粒度
更加细,能够到 namespace 级别,通过 Cache 接口实现类不同的组合,对 Cache 的可控性
也更强。
2. MyBatis 在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件
比较苛刻。
3. 在分布式环境下,由于默认的 MyBatis Cache 实现都是基于本地的,分布式环境下必然会出现
读取到脏数据,需要使用集中式缓存将 MyBatis 的 Cache 接口实现,有一定的开发成本,直
接使用 Redis、Memcached 等分布式缓存可能成本更低,安全性也更高。
JDBC 编程有哪些步骤?
1.装载相应的数据库的 JDBC 驱动并进行初始化:
2.建立 JDBC 和数据库之间的 Connection 连接:
Class.forName("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test?
characterEncoding=UTF-8", "root", "123456");
3.创建 Statement 或者 PreparedStatement 接口,执行 SQL 语句。
4.处理和显示结果。
5.释放资源
2.建立 JDBC 和数据库之间的 Connection 连接:
Class.forName("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test?
characterEncoding=UTF-8", "root", "123456");
3.创建 Statement 或者 PreparedStatement 接口,执行 SQL 语句。
4.处理和显示结果。
5.释放资源
MyBatis 中比如 UserMapper.java 是接口,为什么没有实
现类还能调用?
现类还能调用?
使用JDK动态代理+MapperProxy。本质上调用的是MapperProxy的invoke方法。
mysql
说一说三大范式
「第一范式」:数据库中的字段具有「原子性」,不可再分,并且是单一职责
「第二范式」:「建立在第一范式的基础上」,第二范式要求数据库表中的每个实例或行必须「可以被惟一地区分」。为实现区分通常需要为表加上一个列,以存储各个实例的惟一标识。这个惟一属性列被称为主键
「第三范式」:「建立在第一,第二范式的基础上」,确保每列都和主键列直接相关,而不是间接相关不存在其他表的非主键信息
「第二范式」:「建立在第一范式的基础上」,第二范式要求数据库表中的每个实例或行必须「可以被惟一地区分」。为实现区分通常需要为表加上一个列,以存储各个实例的惟一标识。这个惟一属性列被称为主键
「第三范式」:「建立在第一,第二范式的基础上」,确保每列都和主键列直接相关,而不是间接相关不存在其他表的非主键信息
但是在我们的日常开发当中,「并不是所有的表一定要满足三大范式」,有时候冗余几个字段可以少关联几张表,带来的查询效率的提升有可能是质变的
MyISAM 与 InnoDB 的区别是什么?
「InnoDB 必须有唯一索引(主键)」,如果没有指定的话 InnoDB 会自己生成一个隐藏列Row_id来充当默认主键,「MyISAM 可以没有」
「InnoDB支持事务,MyISAM不支持」。
「InnoDB 支持外键,而 MyISAM 不支持」。
「InnoDB是聚集索引」,使用B+Tree作为索引结构,数据文件是和索引绑在一起的,必须要有主键。「MyISAM是非聚集索引」,也是使用B+Tree作为索引结构,索引和数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。
「InnoDB 不保存表的具体行数」。「MyISAM 用一个变量保存了整个表的行数」。
Innodb 有 「redolog」 日志文件,MyISAM 没有
「InnoDB 支持表、行锁,而 MyISAM 支持表级锁」
「Innodb存储文件有frm、ibd,而Myisam是frm、MYD、MYI」
Innodb:frm是表定义文件,ibd是数据文件
Myisam:frm是表定义文件,myd是数据文件,myi是索引文件
Innodb:frm是表定义文件,ibd是数据文件
Myisam:frm是表定义文件,myd是数据文件,myi是索引文件
为什么推荐使用自增 id 作为主键?
1.普通索引的 B+ 树上存放的是主键索引的值,如果该值较大,会「导致普通索引的存储空间较大」
2.使用自增 id 做主键索引新插入数据只要放在该页的最尾端就可以,直接「按照顺序插入」,不用刻意维护
3.页分裂容易维护,当插入数据的当前页快满时,会发生页分裂的现象,如果主键索引不为自增 id,那么数据就可能从页的中间插入,页的数据会频繁的变动,「导致页分裂维护成本较高」
执行一条 select 语句,期间发生了什么?
图解:
总结:
连接器:建立连接,管理连接、校验用户身份;
查询缓存:查询语句如果命中查询缓存则直接返回,否则继续往下执行。MySQL 8.0 已删除该模块;
解析 SQL:通过解析器对 SQL 查询语句进行词法分析、语法分析,然后构建语法树,方便后续模块读取表名、字段、语句类型;
执行 SQL:执行 SQL 共有三个阶段:
**预处理阶段:检查表或字段是否存在;将 select * 中的 * 符号扩展为表上的所有列。
**优化阶段:基于查询成本的考虑, 选择查询成本最小的执行计划;
**执行阶段:根据执行计划执行 SQL 查询语句,从存储引擎读取记录,返回给客户端;
查询缓存:查询语句如果命中查询缓存则直接返回,否则继续往下执行。MySQL 8.0 已删除该模块;
解析 SQL:通过解析器对 SQL 查询语句进行词法分析、语法分析,然后构建语法树,方便后续模块读取表名、字段、语句类型;
执行 SQL:执行 SQL 共有三个阶段:
**预处理阶段:检查表或字段是否存在;将 select * 中的 * 符号扩展为表上的所有列。
**优化阶段:基于查询成本的考虑, 选择查询成本最小的执行计划;
**执行阶段:根据执行计划执行 SQL 查询语句,从存储引擎读取记录,返回给客户端;
使用 Innodb 的情况下,一条更新语句是怎么执行的?
用以下语句来举例,c 字段无索引,id 为主键索引
update T set c=c+1 where id=2;
update T set c=c+1 where id=2;
1.执行器先找引擎取 id=2 这一行。id 是主键,引擎直接用树搜索找到这一行
如果 id=2 这一行所在的数据页本来就「在内存中」,就「直接返回」给执行器
「不在内存」中,需要先从磁盘「读入内存」,然后再「返回」
如果 id=2 这一行所在的数据页本来就「在内存中」,就「直接返回」给执行器
「不在内存」中,需要先从磁盘「读入内存」,然后再「返回」
2.执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口「写入这行新数据」
3.引擎将这行新数据更新到内存中,同时将这个更新操作「记录到 redo log 里面」,此时 redo log 处于 「prepare」 状态。然后告知执行器执行完成了,随时可以提交事务
4.执行器「生成这个操作的 binlog」,并把 binlog 「写入磁盘」
5.执行器调用引擎的「提交事务」接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,「更新完成」
Innodb 事务为什么要两阶段提交?
先写 redolog 后写binlog。假设在 redolog 写完,binlog 还没有写完的时候,MySQL 进程异常重启,这时候 binlog 里面就没有记录这个语句。然后你会发现,如果需要用这个 binlog 来恢复临时库的话,由于这个语句的 「binlog 丢失」,这个临时库就会少了这一次更新,恢复出来的这一行 c 的值就是 0,与原库的值不同。
先写 binlog 后写 redolog。如果在 binlog 写完之后 crash,由于 redolog 还没写,崩溃恢复以后这个事务无效,所以这一行c的值是0。但是 binlog 里面已经记录了“把c从0改成1”这个日志。所以,在之后用 binlog 来恢复的时候就「多了一个事务出来」,恢复出来的这一行 c 的值就是 1,与原库的值不同。
可以看到,「如果不使用“两阶段提交”,那么数据库的状态就有可能和用它的日志恢复出来的库的状态不一致」。
什么是索引?以及好处和坏处
索引是一种帮助快速查找数据的数据结构,可以把它理解为书的目录,通过索引能够快速找到数据所在位置。
索引数据结构有:
Hash表(通过hash算法快速定位数据,但不适合范围查询,因为需要每个key都进行一次hash)、二叉树(查找和修改效率都比较高),但是在InnoDB引擎中使用的索引是B+Tree,相较于二叉树,B+Tree这种多叉树,更加矮宽,更适合存储在磁盘中。使用索引增加了数据查找的效率,但是相对的由于索引也需要存储到磁盘,所以增加了存储的压力,并且新增数据时需要同步维护索引。但是合理的使用索引能够极大提高我们的效率!
Hash表(通过hash算法快速定位数据,但不适合范围查询,因为需要每个key都进行一次hash)、二叉树(查找和修改效率都比较高),但是在InnoDB引擎中使用的索引是B+Tree,相较于二叉树,B+Tree这种多叉树,更加矮宽,更适合存储在磁盘中。使用索引增加了数据查找的效率,但是相对的由于索引也需要存储到磁盘,所以增加了存储的压力,并且新增数据时需要同步维护索引。但是合理的使用索引能够极大提高我们的效率!
索引的分类?
按「数据结构」分类:B+tree索引、Hash索引、Full-text索引。
按「物理存储」分类:聚簇索引(主键索引)、二级索引(辅助索引)。
按「字段特性」分类:主键索引、唯一索引、普通索引、前缀索引。
按「字段个数」分类:单列索引、联合索引。
按「物理存储」分类:聚簇索引(主键索引)、二级索引(辅助索引)。
按「字段特性」分类:主键索引、唯一索引、普通索引、前缀索引。
按「字段个数」分类:单列索引、联合索引。
什么时候适合用索引?什么时候不适合?
适合:
字段有唯一性
where条件经常用到的字段
group by和order by经常用到的字段
where条件经常用到的字段
group by和order by经常用到的字段
不适合:
字段频繁变化
字段数据大量重复
不经常用的字段
数据太少
字段数据大量重复
不经常用的字段
数据太少
索引失效的场景有哪些?
联合索引非最左匹配
对索引使用函数/表达式计算
不能继续使用索引中范围条件(bettween、<、>、in等)右边的列
索引字段上使用(!= 或者 < >)判断时,会导致索引失效而转向全表扫描
索引字段上使用 is null / is not null 判断时,会导致索引失效而转向全表扫描。
索引字段使用like以通配符开头(‘%字符串’)时,会导致索引失效而转向全表扫描,也是最左前缀原则。
索引字段是字符串,但查询时不加单引号,会导致索引失效而转向全表扫描
索引字段使用 or 时,会导致索引失效而转向全表扫描
执行计划的参数有哪些?
possible_keys 字段表示可能用到的索引;
key 字段表示实际用的索引,如果这一项为 NULL,说明没有使用索引;
key_len 表示索引的长度;
rows 表示扫描的数据行数。
type 表示数据扫描类型
key 字段表示实际用的索引,如果这一项为 NULL,说明没有使用索引;
key_len 表示索引的长度;
rows 表示扫描的数据行数。
type 表示数据扫描类型
type 字段就是描述了找到所需数据时使用的扫描方式是什么,常见扫描类型的执行效率从低到高的顺序为:
ALL(全表扫描);
index(全索引扫描);
range(索引范围扫描);
ref(非唯一索引扫描);
eq_ref(唯一索引扫描);
const(结果只有一条的主键或唯一索引扫描)。
ALL(全表扫描);
index(全索引扫描);
range(索引范围扫描);
ref(非唯一索引扫描);
eq_ref(唯一索引扫描);
const(结果只有一条的主键或唯一索引扫描)。
count(*)和count(1)有什么区别?
count(*)=count(1)>count(主键)>count(字段)
为什么采用 B+ 树,而不是 B-树
B+ 树只在叶子结点储存数据,非叶子结点不存具体数据,只存 key,查询更稳定,增大了广度,而一个节点就是磁盘一个内存页,内存页大小固定,那么相比 B 树,B- 树这些「可以存更多的索引结点」,宽度更大,树高矮,节点小,拉取一次数据的磁盘 IO 次数少,并且 B+ 树只需要去遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,效率更高。
WAl 是什么?有什么好处?
WAL 就是 Write-Ahead Logging,其实就是「所有的修改都先被写入到日志中,然后再写磁盘」,用于保证数据操作的原子性和持久性。
好处:
1.「读和写可以完全地并发执行」,不会互相阻塞
2.先写入 log 中,磁盘写入从「随机写变为顺序写」,降低了 client 端的延迟就。并且,由于顺序写入大概率是在一个磁盘块内,这样产生的 io 次数也大大降低
3.写入日志当数据库崩溃的时候「可以使用日志来恢复磁盘数据」
1.「读和写可以完全地并发执行」,不会互相阻塞
2.先写入 log 中,磁盘写入从「随机写变为顺序写」,降低了 client 端的延迟就。并且,由于顺序写入大概率是在一个磁盘块内,这样产生的 io 次数也大大降低
3.写入日志当数据库崩溃的时候「可以使用日志来恢复磁盘数据」
mysql的索引有哪些,聚簇和⾮聚簇索引⼜是什么?
普通索引 最基本的索引,仅加速查询 CREATE INDEX idx_name ON table_name(filed_name)
唯一索引 加速查询,列值唯一,允许为空;
组合索引则列值的组合必须唯一 CREATE UNIQUE INDEX idx_name ON table_name(filed_name_1,filed_name_2)
组合索引则列值的组合必须唯一 CREATE UNIQUE INDEX idx_name ON table_name(filed_name_1,filed_name_2)
主键索引 加速查询,列值唯一,
一个表只有1个,不允许有空值 ALTER TABLE table_name ADD PRIMARY KEY ( filed_name )
一个表只有1个,不允许有空值 ALTER TABLE table_name ADD PRIMARY KEY ( filed_name )
组合索引 加速查询,多条件组合查询 CREATE INDEX idx_name ON table_name(filed_name_1,filed_name_2);
覆盖索引 索引包含所需要的值,不需要“回表”查询,
比如查询 两个字段,刚好是 组合索引 的两个字段
比如查询 两个字段,刚好是 组合索引 的两个字段
全文索引 对内容进行分词搜索,仅可用于Myisam, 更多用ElasticSearch做搜索 ALTER TABLE table_name ADD FULLTEXT ( filed_name )
聚簇索引:B+树是左⼩右⼤的顺序存储结构,节点只包含id索引列,⽽叶⼦节点包含索引列和数据,这种数据和索
引在⼀起存储的索引⽅式叫做聚簇索引,⼀张表只能有⼀个聚簇索引。假设没有定义主键,InnoDB会选
择⼀个唯⼀的⾮空索引代替,如果没有的话则会隐式定义⼀个主键作为聚簇索引。
引在⼀起存储的索引⽅式叫做聚簇索引,⼀张表只能有⼀个聚簇索引。假设没有定义主键,InnoDB会选
择⼀个唯⼀的⾮空索引代替,如果没有的话则会隐式定义⼀个主键作为聚簇索引。
非聚簇索引:⾮聚簇索引(⼆级索引)保存的是主
键id值,这⼀点和myisam保存的是数据地址是不同的
键id值,这⼀点和myisam保存的是数据地址是不同的
什么是回表?
回表就是先通过数据库索引扫描出该索引树中数据所在的行,取到主键 id,再通过主键 id 取出主键索引数中的数据,即基于非主键索引的查询需要多扫描一棵索引树.
什么是索引下推?
如果存在某些被索引的列的判断条件时,MySQL 将这一部分判断条件传递给存储引擎,然后由存储引擎通过判断索引是否符合 MySQL 服务器传递的条件,「只有当索引符合条件时才会将数据检索出来返回给 MySQL 服务器」 。
什么是覆盖索引?
覆盖索引(covering index)指一个查询语句的执行只用从索引中就能够取得,不必从数据表中读取,可以减少回表的次数
什么是最左前缀原则?
最左前缀其实说的是,在 where 条件中出现的字段,「如果只有组合索引中的部分列,则这部分列的触发索引顺序」,是按照定义索引的时候的顺序从前到后触发,最左面一个列触发不了,之后的所有列索引都无法触发。
普通索引和唯一索引该怎么选择?
查询
当普通索引为条件时查询到数据会一直扫描,直到扫完整张表
当唯一索引为查询条件时,查到该数据会直接返回,不会继续扫表
当普通索引为条件时查询到数据会一直扫描,直到扫完整张表
当唯一索引为查询条件时,查到该数据会直接返回,不会继续扫表
更新
普通索引会直接将操作更新到 change buffer 中,然后结束
唯一索引需要判断数据是否冲突
普通索引会直接将操作更新到 change buffer 中,然后结束
唯一索引需要判断数据是否冲突
所以「唯一索引更加适合查询的场景,普通索引更适合插入的场景」
什么是事务?其特性是什么?
事务是指是程序中一系列操作必须全部成功完成,有一个失败则全部失败。
A原⼦性由undo log⽇志保证,它记录了需要回滚的⽇志信息,事务回滚时撤销已经执⾏成功的sql
C⼀致性⼀般由代码层⾯来保证
I隔离性由MVCC来保证
D持久性由内存+redo log来保证,mysql修改数据同时在内存和redo log记录这次操作,事务提交的时
候通过redo log刷盘,宕机的时候可以从redo log恢复
候通过redo log刷盘,宕机的时候可以从redo log恢复
事务的隔离级别?
1.「读提交」:即能够「读取到那些已经提交」的数据
2.「读未提交」:即能够「读取到没有被提交」的数据
3.「可重复读」:可重复读指的是在一个事务内,最开始读到的数据和事务结束前的「任意时刻读到的同一批数据都是一致的」
4.「可串行化」:最高事务隔离级别,不管多少事务,都是「依次按序一个一个执行」
2.「读未提交」:即能够「读取到没有被提交」的数据
3.「可重复读」:可重复读指的是在一个事务内,最开始读到的数据和事务结束前的「任意时刻读到的同一批数据都是一致的」
4.「可串行化」:最高事务隔离级别,不管多少事务,都是「依次按序一个一个执行」
「脏读」
脏读指的是「读到了其他事务未提交的数据」,未提交意味着这些数据可能会回滚,也就是可能最终不会存到数据库中,也就是不存在的数据。读到了并一定最终存在的数据,这就是脏读
「不可重复读」
对比可重复读,不可重复读指的是在同一事务内,「不同的时刻读到的同一批数据可能是不一样的」。
「幻读」
幻读是针对数据插入(INSERT)操作来说的。假设事务A对某些行的内容作了更改,但是还未提交,此时事务B插入了与事务A更改前的记录相同的记录行,并且在事务A提交之前先提交了,而这时,在事务A中查询,会发现「好像刚刚的更改对于某些数据未起作用」,但其实是事务B刚插入进来的这就叫幻读
脏读指的是「读到了其他事务未提交的数据」,未提交意味着这些数据可能会回滚,也就是可能最终不会存到数据库中,也就是不存在的数据。读到了并一定最终存在的数据,这就是脏读
「不可重复读」
对比可重复读,不可重复读指的是在同一事务内,「不同的时刻读到的同一批数据可能是不一样的」。
「幻读」
幻读是针对数据插入(INSERT)操作来说的。假设事务A对某些行的内容作了更改,但是还未提交,此时事务B插入了与事务A更改前的记录相同的记录行,并且在事务A提交之前先提交了,而这时,在事务A中查询,会发现「好像刚刚的更改对于某些数据未起作用」,但其实是事务B刚插入进来的这就叫幻读
binlog 是做什么的?
binlog 是归档日志,属于 Server 层的日志,是一个二进制格式的文件,用于「记录用户对数据库更新的SQL语句信息」。
主要作用
主从复制
数据恢复
主从复制
数据恢复
undolog 是做什么的?
undolog 是 InnoDB 存储引擎的日志,用于保证数据的原子性,「保存了事务发生之前的数据的一个版本,也就是说记录的是数据是修改之前的数据,可以用于回滚」,同时可以提供多版本并发控制下的读(MVCC)。
主要作用
事务回滚
实现多版本控制(MVCC)
事务回滚
实现多版本控制(MVCC)
relaylog 是做什么的?
relaylog 是中继日志,「在主从同步的时候使用到」,它是一个中介临时的日志文件,用于存储从master节点同步过来的binlog日志内容。
master 主节点的 binlog 传到 slave 从节点后,被写入 relay log 里,从节点的 slave sql 线程从 relaylog 里读取日志然后应用到 slave 从节点本地。从服务器 I/O 线程将主服务器的二进制日志读取过来记录到从服务器本地文件,然后 SQL 线程会读取 relay-log 日志的内容并应用到从服务器,从而「使从服务器和主服务器的数据保持一致」。
redolog 是做什么的?
redolog 是 「InnoDB 存储引擎所特有的一种日志」,用于记录事务操作的变化,记录的是数据修改之后的值,不管事务是否提交都会记录下来。
可以做「数据恢复并且提供 crash-safe 能力」
当有增删改相关的操作时,会先记录到 Innodb 中,并修改缓存页中的数据,「等到 mysql 闲下来的时候才会真正的将 redolog 中的数据写入到磁盘当中」。
redolog 是怎么记录日志的?
InnoDB 的 redo log 是固定大小的,比如可以配置为一组4个文件,每个文件的大小是1GB,那么总共就可以记录4GB的操作。「从头开始写,写到末尾就又回到开头循环写」。
所以,如果数据写满了但是还没有来得及将数据真正的刷入磁盘当中,那么就会发生「内存抖动」现象,从肉眼的角度来观察会发现 mysql 会宕机一会儿,此时就是正在刷盘了。
redolog 和 binlog 的区别是什么?
1.「redolog」 是 「Innodb」 独有的日志,而 「binlog」 是 「server」 层的,所有的存储引擎都有使用到
2.「redolog」 记录了「具体的数值」,对某个页做了什么修改,「binlog」 记录的「操作内容」
3.「binlog」 大小达到上限或者 flush log 「会生成一个新的文件」,而 「redolog」 有固定大小「只能循环利用」
4.「binlog 日志没有 crash-safe 的能力」,只能用于归档。而 redo log 有 crash-safe 能力。
说一说 mvcc 吧,有什么作用?
MVCC:多版本并发控制,是现代数据库(包括 MySQL、Oracle、PostgreSQL 等)引擎实现中常用的处理读写冲突的手段,目的在于「提高数据库高并发场景下的吞吐性能」。
在 MVCC 协议下,每个读操作会看到一个一致性的快照,「这个快照是基于整个库的」,并且可以实现非阻塞的读,用于「支持读提交和可重复读隔离级别的实现」
MVCC 允许数据具有多个版本,这个版本可以是时间戳或者是全局递增的事务 ID,在同一个时间点,不同的事务看到的数据是不同的,这个修改的数据是「记录在 undolog 中」的。
失效场景:两个连续的快照读中间有个当前读
当前读和快照读?
快照读就是最普通的Select查询语句
当前读指执行insert,update,delete,select 。。。for update,select。。。lock in share mode语句时进行读取数据的方式
那你知道什么是间隙锁吗?
间隙锁是可重复读级别下才会有的锁,结合MVCC和间隙锁可以解决幻读的问题。
需要注意的是唯⼀索引是不会有间隙索引的。
一条 Sql 语句查询一直慢会是什么原因?
「1.没有用到索引」
比如函数导致的索引失效,或者本身就没有加索引
比如函数导致的索引失效,或者本身就没有加索引
「2.表数据量太大」
考虑分库分表吧
考虑分库分表吧
「3.优化器选错了索引」
「考虑使用」 force index 强制走索引
「考虑使用」 force index 强制走索引
一条 Sql 语句查询偶尔慢会是什么原因?
「1. 数据库在刷新脏页」
比如 「redolog 写满了」,「内存不够用了」释放内存如果是脏页也需要刷,mysql 「正常空闲状态刷脏页」
比如 「redolog 写满了」,「内存不够用了」释放内存如果是脏页也需要刷,mysql 「正常空闲状态刷脏页」
「2. 没有拿到锁」
Mysql 主从之间是怎么同步数据的?
1.master 主库将此次更新的事件类型「写入到主库的 binlog 文件」中
2.master 「创建 log dump 线程通知 slave」 需要更新数据
3.「slave」 向 master 节点发送请求,「将该 binlog 文件内容存到本地的 relaylog 中」
4.「slave 开启 sql 线程」读取 relaylog 中的内容,「将其中的内容在本地重新执行一遍」,完成主从数据同步
2.master 「创建 log dump 线程通知 slave」 需要更新数据
3.「slave」 向 master 节点发送请求,「将该 binlog 文件内容存到本地的 relaylog 中」
4.「slave 开启 sql 线程」读取 relaylog 中的内容,「将其中的内容在本地重新执行一遍」,完成主从数据同步
「同步策略」:
1.「全同步复制」:主库强制同步日志到从库,等全部从库执行完才返回客户端,性能差
2.「半同步复制」:主库收到至少一个从库确认就认为操作成功,从库写入日志成功返回ack确认
1.「全同步复制」:主库强制同步日志到从库,等全部从库执行完才返回客户端,性能差
2.「半同步复制」:主库收到至少一个从库确认就认为操作成功,从库写入日志成功返回ack确认
主从延迟要怎么解决?
1.MySQL 5.6 版本以后,提供了一种「并行复制」的方式,通过将 SQL 线程转换为多个 work 线程来进行重放
2.「提高机器配置」(王道)
3.在业务初期就选择合适的分库、分表策略,「避免单表单库过大」带来额外的复制压力
4.「避免长事务」
5.「避免让数据库进行各种大量运算」
6.对于一些对延迟很敏感的业务「直接使用主库读」
2.「提高机器配置」(王道)
3.在业务初期就选择合适的分库、分表策略,「避免单表单库过大」带来额外的复制压力
4.「避免长事务」
5.「避免让数据库进行各种大量运算」
6.对于一些对延迟很敏感的业务「直接使用主库读」
删除表数据后表的大小却没有变动,这是为什么?
在使用 delete 删除数据时,其实对应的数据行并不是真正的删除,是「逻辑删除」,InnoDB 仅仅是将其「标记成可复用的状态」,所以表空间不会变小
为什么 VarChar 建议不要超过255?
当定义varchar长度小于等于255时,长度标识位需要一个字节(utf-8编码)
当大于255时,长度标识位需要两个字节,并且建立的「索引也会失效」
分布式式事务怎么实现?
1.「本地消息表」
2.「消息事务」
3.「二阶段提交」
4.「三阶段提交」
5.「TCC」
6.「最大努力通知」
7.「Seata 框架」
2.「消息事务」
3.「二阶段提交」
4.「三阶段提交」
5.「TCC」
6.「最大努力通知」
7.「Seata 框架」
Mysql 中有哪些锁?
mysql锁分为共享锁和排他锁,也叫做读锁和写锁。
读锁是共享的,可以通过lock in share mode实现,这时候只能读不能写。
写锁是排他的,它会阻塞其他的写锁和读锁。从颗粒度来区分,可以分为表锁和⾏锁两种。
表锁会锁定整张表并且阻塞其他⽤户对该表的所有读写操作,⽐如alter修改表结构的时候会锁表。
⾏锁⼜可以分为乐观锁和悲观锁,悲观锁可以通过for update实现,乐观锁则通过版本号实现。
读锁是共享的,可以通过lock in share mode实现,这时候只能读不能写。
写锁是排他的,它会阻塞其他的写锁和读锁。从颗粒度来区分,可以分为表锁和⾏锁两种。
表锁会锁定整张表并且阻塞其他⽤户对该表的所有读写操作,⽐如alter修改表结构的时候会锁表。
⾏锁⼜可以分为乐观锁和悲观锁,悲观锁可以通过for update实现,乐观锁则通过版本号实现。
为什么不要使用长事务?
1.并发情况下,数据库「连接池容易被撑爆」
2.「容易造成大量的阻塞和锁超时」
长事务还占用锁资源,也可能拖垮整个库,
3.执行时间长,容易造成「主从延迟」
4.「回滚所需要的时间比较长」
事务越长整个时间段内的事务也就越多
5.「undolog 日志越来越大」
长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间
2.「容易造成大量的阻塞和锁超时」
长事务还占用锁资源,也可能拖垮整个库,
3.执行时间长,容易造成「主从延迟」
4.「回滚所需要的时间比较长」
事务越长整个时间段内的事务也就越多
5.「undolog 日志越来越大」
长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间
buffer pool 是做什么的?
buffer pool 是一块内存区域,为了「提高数据库的性能」,当数据库操作数据的时候,把硬盘上的数据加载到 buffer pool,不直接和硬盘打交道,操作的是 buffer pool 里面的数据,数据库的增删改查都是在 buffer pool 上进行
buffer pool 里面缓存的数据内容也是一个个数据页
其中「有三大双向链表」:
「free 链表」
用于帮助我们找到空闲的缓存页
「flush 链表」
用于找到脏缓存页,也就是需要刷盘的缓存页
「lru 链表」
用来淘汰不常被访问的缓存页,分为热数据区和冷数据区,冷数据区主要存放那些不常被用到的数据
「free 链表」
用于帮助我们找到空闲的缓存页
「flush 链表」
用于找到脏缓存页,也就是需要刷盘的缓存页
「lru 链表」
用来淘汰不常被访问的缓存页,分为热数据区和冷数据区,冷数据区主要存放那些不常被用到的数据
预读机制:
Buffer Pool 有一项特技叫预读,存储引擎的接口在被 Server 层调用时,会在响应的同时进行预判,将下次可能用到的数据和索引加载到 Buffer Pool
Buffer Pool 有一项特技叫预读,存储引擎的接口在被 Server 层调用时,会在响应的同时进行预判,将下次可能用到的数据和索引加载到 Buffer Pool
说说你的 Sql 调优思路吧
1.「表结构优化」
1.1拆分字段
1.2字段类型的选择
1.3字段类型大小的限制
1.4合理的增加冗余字段
1.5新建字段一定要有默认值
1.1拆分字段
1.2字段类型的选择
1.3字段类型大小的限制
1.4合理的增加冗余字段
1.5新建字段一定要有默认值
2.「索引方面」
2.1索引字段的选择
2.2利用好mysql支持的索引下推,覆盖索引等功能
2.3唯一索引和普通索引的选择
2.1索引字段的选择
2.2利用好mysql支持的索引下推,覆盖索引等功能
2.3唯一索引和普通索引的选择
3.「查询语句方面」
3.1避免索引失效
3.2合理的书写where条件字段顺序
3.3小表驱动大表
3.4可以使用force index()防止优化器选错索引
3.1避免索引失效
3.2合理的书写where条件字段顺序
3.3小表驱动大表
3.4可以使用force index()防止优化器选错索引
4.「分库分表」
分表后⾮sharding_key的查询怎么处理呢?
1. 可以做⼀个mapping表,⽐如这时候商家要查询订单列表怎么办呢?不带user_id查询的话你总不
能扫全表吧?所以我们可以做⼀个映射关系表,保存商家和⽤户的关系,查询的时候先通过商家查
询到⽤户列表,再通过user_id去查询。
2. 打宽表,⼀般⽽⾔,商户端对数据实时性要求并不是很⾼,⽐如查询订单列表,可以把订单表同步
到离线(实时)数仓,再基于数仓去做成⼀张宽表,再基于其他如es提供查询服务。
3. 数据量不是很⼤的话,⽐如后台的⼀些查询之类的,也可以通过多线程扫表,然后再聚合结果的⽅
式来做。或者异步的形式也是可以的。
能扫全表吧?所以我们可以做⼀个映射关系表,保存商家和⽤户的关系,查询的时候先通过商家查
询到⽤户列表,再通过user_id去查询。
2. 打宽表,⼀般⽽⾔,商户端对数据实时性要求并不是很⾼,⽐如查询订单列表,可以把订单表同步
到离线(实时)数仓,再基于数仓去做成⼀张宽表,再基于其他如es提供查询服务。
3. 数据量不是很⼤的话,⽐如后台的⼀些查询之类的,也可以通过多线程扫表,然后再聚合结果的⽅
式来做。或者异步的形式也是可以的。
针对线上的数据库,你会做哪些监控,业务性能 + 数据安全 角度分析
业务性能
1、应用上线前会审查业务新增的sql,和分析sql执行计划 比如是否存在 select * ,索引建立是否合理
2、开启慢查询日志,定期分析慢查询日志
3、监控CPU/内存利用率,读写、网关IO、流量带宽 随着时间的变化统计图
4、吞吐量QPS/TPS,一天内读写随着时间的变化统计图
1、应用上线前会审查业务新增的sql,和分析sql执行计划 比如是否存在 select * ,索引建立是否合理
2、开启慢查询日志,定期分析慢查询日志
3、监控CPU/内存利用率,读写、网关IO、流量带宽 随着时间的变化统计图
4、吞吐量QPS/TPS,一天内读写随着时间的变化统计图
数据安全
1、短期增量备份,比如一周一次。 定期全量备份,比如一月一次
2、检查是否有非授权用户,是否存在弱口令,网络防火墙检查
3、导出数据是否进行脱敏,防止数据泄露或者黑产利用
4、数据库 全量操作日志审计,防止数据泄露
5、数据库账号密码 业务独立,权限独立控制,防止多库共用同个账号密码
6、高可用 主从架构,多机房部署
1、短期增量备份,比如一周一次。 定期全量备份,比如一月一次
2、检查是否有非授权用户,是否存在弱口令,网络防火墙检查
3、导出数据是否进行脱敏,防止数据泄露或者黑产利用
4、数据库 全量操作日志审计,防止数据泄露
5、数据库账号密码 业务独立,权限独立控制,防止多库共用同个账号密码
6、高可用 主从架构,多机房部署
redis
什么是 redis?它能做什么?
redis: redis 即 Remote Dictionary Server,用中文翻译过来可以理解为远程数据服务或远程字典服务。其是使用 C 语言的编写的key-value存储系统
应用场景:缓存,数据库,消息队列,分布式锁,点赞列表,排行榜等等
应用场景:缓存,数据库,消息队列,分布式锁,点赞列表,排行榜等等
redis 有哪八种数据类型?有哪些应用场景?
redis 总共有八种数据结构,五种基本数据类型和三种特殊数据类型。
字符串:redis没有直接使⽤C语⾔传统的字符串表示,⽽是⾃⼰实现的叫做简单动态字符串SDS的
抽象类型。C语⾔的字符串不记录⾃身的⻓度信息,⽽SDS则保存了⻓度信息,这样将获取字符串
⻓度的时间由O(N)降低到了O(1),同时可以避免缓冲区溢出和减少修改字符串⻓度时所需的内存重
分配次数。
抽象类型。C语⾔的字符串不记录⾃身的⻓度信息,⽽SDS则保存了⻓度信息,这样将获取字符串
⻓度的时间由O(N)降低到了O(1),同时可以避免缓冲区溢出和减少修改字符串⻓度时所需的内存重
分配次数。
链表linkedlist:redis链表是⼀个双向⽆环链表结构,很多发布订阅、慢查询、监视器功能都是使
⽤到了链表来实现,每个链表的节点由⼀个listNode结构来表示,每个节点都有指向前置节点和后
置节点的指针,同时表头节点的前置和后置节点都指向NULL。
⽤到了链表来实现,每个链表的节点由⼀个listNode结构来表示,每个节点都有指向前置节点和后
置节点的指针,同时表头节点的前置和后置节点都指向NULL。
字典hashtable:⽤于保存键值对的抽象数据结构。redis使⽤hash表作为底层实现,每个字典带有
两个hash表,供平时使⽤和rehash时使⽤,hash表使⽤链地址法来解决键冲突,被分配到同⼀个
索引位置的多个键值对会形成⼀个单向链表,在对hash表进⾏扩容或者缩容的时候,为了服务的可
⽤性,rehash的过程不是⼀次性完成的,⽽是渐进式的。
两个hash表,供平时使⽤和rehash时使⽤,hash表使⽤链地址法来解决键冲突,被分配到同⼀个
索引位置的多个键值对会形成⼀个单向链表,在对hash表进⾏扩容或者缩容的时候,为了服务的可
⽤性,rehash的过程不是⼀次性完成的,⽽是渐进式的。
跳跃表skiplist:跳跃表是有序集合的底层实现之⼀,redis中在实现有序集合键和集群节点的内部
结构中都是⽤到了跳跃表。redis跳跃表由zskiplist和zskiplistNode组成,zskiplist⽤于保存跳跃表
信息(表头、表尾节点、⻓度等),zskiplistNode⽤于表示表跳跃节点,每个跳跃表的层⾼都是1-
32的随机数,在同⼀个跳跃表中,多个节点可以包含相同的分值,但是每个节点的成员对象必须是
唯⼀的,节点按照分值⼤⼩排序,如果分值相同,则按照成员对象的⼤⼩排序。
结构中都是⽤到了跳跃表。redis跳跃表由zskiplist和zskiplistNode组成,zskiplist⽤于保存跳跃表
信息(表头、表尾节点、⻓度等),zskiplistNode⽤于表示表跳跃节点,每个跳跃表的层⾼都是1-
32的随机数,在同⼀个跳跃表中,多个节点可以包含相同的分值,但是每个节点的成员对象必须是
唯⼀的,节点按照分值⼤⼩排序,如果分值相同,则按照成员对象的⼤⼩排序。
整数集合intset:⽤于保存整数值的集合抽象数据结构,不会出现重复元素,底层实现为数组。
压缩列表ziplist:压缩列表是为节约内存⽽开发的顺序性数据结构,他可以包含多个节点,每个节
点可以保存⼀个字节数组或者整数值。
点可以保存⼀个字节数组或者整数值。
基于这些基础的数据结构,redis封装了⾃⼰的对象系统,包含字符串对象string、列表对象list、哈希对
象hash、集合对象set、有序集合对象zset,每种对象都⽤到了⾄少⼀种基础的数据结构。
象hash、集合对象set、有序集合对象zset,每种对象都⽤到了⾄少⼀种基础的数据结构。
redis通过encoding属性设置对象的编码形式来提升灵活性和效率,基于不同的场景redis会⾃动做出优
化。不同对象的编码如下:
1. 字符串对象string:int整数、embstr编码的简单动态字符串、raw简单动态字符串
2. 列表对象list:ziplist、linkedlist
3. 哈希对象hash:ziplist、hashtable
4. 集合对象set:intset、hashtable
5. 有序集合对象zset:ziplist、skiplist
化。不同对象的编码如下:
1. 字符串对象string:int整数、embstr编码的简单动态字符串、raw简单动态字符串
2. 列表对象list:ziplist、linkedlist
3. 哈希对象hash:ziplist、hashtable
4. 集合对象set:intset、hashtable
5. 有序集合对象zset:ziplist、skiplist
五种基本数据类型:
1.string:字符串类型,常被用来存储计数器,粉丝数等,简单的分布式锁也会用到该类型
2.hashmap:key - value 形式的,value 是一个map
3.list:基本的数据类型,列表。在 Redis 中可以把 list 用作栈、队列、阻塞队列。
4.set:集合,不能有重复元素,可以做点赞,收藏等
5.zset:有序集合,不能有重复元素,有序集合中的每个元素都需要指定一个分数,根据分数对元素进行升序排序。可以做排行榜
1.string:字符串类型,常被用来存储计数器,粉丝数等,简单的分布式锁也会用到该类型
2.hashmap:key - value 形式的,value 是一个map
3.list:基本的数据类型,列表。在 Redis 中可以把 list 用作栈、队列、阻塞队列。
4.set:集合,不能有重复元素,可以做点赞,收藏等
5.zset:有序集合,不能有重复元素,有序集合中的每个元素都需要指定一个分数,根据分数对元素进行升序排序。可以做排行榜
三种特殊数据类型:
1.geospatial: Redis 在 3.2 推出 Geo 类型,该功能可以推算出地理位置信息,两地之间的距离。
2.hyperloglog:基数:数学上集合的元素个数,是不能重复的。这个数据结构常用于统计网站的 UV。
3.bitmap: bitmap 就是通过最小的单位 bit 来进行0或者1的设置,表示某个元素对应的值或者状态。一个 bit 的值,或者是0,或者是1;也就是说一个 bit 能存储的最多信息是2。bitmap 常用于统计用户信息比如活跃粉丝和不活跃粉丝、登录和未登录、是否打卡等。
1.geospatial: Redis 在 3.2 推出 Geo 类型,该功能可以推算出地理位置信息,两地之间的距离。
2.hyperloglog:基数:数学上集合的元素个数,是不能重复的。这个数据结构常用于统计网站的 UV。
3.bitmap: bitmap 就是通过最小的单位 bit 来进行0或者1的设置,表示某个元素对应的值或者状态。一个 bit 的值,或者是0,或者是1;也就是说一个 bit 能存储的最多信息是2。bitmap 常用于统计用户信息比如活跃粉丝和不活跃粉丝、登录和未登录、是否打卡等。
redis的数据结构?
总览:
SDS(简单动态字符串)
len,记录了字符串长度。这样获取字符串长度的时候,只需要返回这个成员变量值就行,时间复杂度只需要 O(1)。
alloc,分配给字符数组的空间长度。这样在修改字符串的时候,可以通过 alloc - len 计算出剩余的空间大小,可以用来判断空间是否满足修改需求,如果不满足的话,就会自动将 SDS 的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以使用 SDS 既不需要手动修改 SDS 的空间大小,也不会出现前面所说的缓冲区溢出的问题。
flags,用来表示不同类型的 SDS。一共设计了 5 种类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64,后面在说明区别之处。
buf[],字符数组,用来保存实际数据。不仅可以保存字符串,也可以保存二进制数据。
优点:
O(1)复杂度获取字符串长度
二进制安全
因为 SDS 不需要用 “\0” 字符来标识字符串结尾了,而是有个专门的 len 成员变量来记录长度,所以可存储包含 “\0” 的数据。但是 SDS 为了兼容部分 C 语言标准库的函数, SDS 字符串结尾还是会加上 “\0” 字符。
因此, SDS 的 API 都是以处理二进制的方式来处理 SDS 存放在 buf[] 里的数据,程序不会对其中的数据做任何限制,数据写入的时候时什么样的,它被读取时就是什么样的。
通过使用二进制安全的 SDS,而不是 C 字符串,使得 Redis 不仅可以保存文本数据,也可以保存任意格式的二进制数据。
因此, SDS 的 API 都是以处理二进制的方式来处理 SDS 存放在 buf[] 里的数据,程序不会对其中的数据做任何限制,数据写入的时候时什么样的,它被读取时就是什么样的。
通过使用二进制安全的 SDS,而不是 C 字符串,使得 Redis 不仅可以保存文本数据,也可以保存任意格式的二进制数据。
不会发生缓冲区溢出
C 语言的字符串标准库提供的字符串操作函数,大多数(比如 strcat 追加字符串函数)都是不安全的,因为这些函数把缓冲区大小是否满足操作需求的工作交由开发者来保证,程序内部并不会判断缓冲区大小是否足够用,当发生了缓冲区溢出就有可能造成程序异常结束。
所以,Redis 的 SDS 结构里引入了 alloc 和 len 成员变量,这样 SDS API 通过 alloc - len 计算,可以算出剩余可用的空间大小,这样在对字符串做修改操作的时候,就可以由程序内部判断缓冲区大小是否足够用。
而且,当判断出缓冲区大小不够用时,Redis 会自动将扩大 SDS 的空间大小(小于 1MB 翻倍扩容,大于 1MB 按 1MB 扩容),以满足修改所需的大小。
在扩展 SDS 空间之前,SDS API 会优先检查未使用空间是否足够,如果不够的话,API 不仅会为 SDS 分配修改所必须要的空间,还会给 SDS 分配额外的「未使用空间」。
这样的好处是,下次在操作 SDS 时,如果 SDS 空间够的话,API 就会直接使用「未使用空间」,而无须执行内存分配,有效的减少内存分配次数。
所以,使用 SDS 即不需要手动修改 SDS 的空间大小,也不会出现缓冲区溢出的问题。
#节省内存空间
所以,Redis 的 SDS 结构里引入了 alloc 和 len 成员变量,这样 SDS API 通过 alloc - len 计算,可以算出剩余可用的空间大小,这样在对字符串做修改操作的时候,就可以由程序内部判断缓冲区大小是否足够用。
而且,当判断出缓冲区大小不够用时,Redis 会自动将扩大 SDS 的空间大小(小于 1MB 翻倍扩容,大于 1MB 按 1MB 扩容),以满足修改所需的大小。
在扩展 SDS 空间之前,SDS API 会优先检查未使用空间是否足够,如果不够的话,API 不仅会为 SDS 分配修改所必须要的空间,还会给 SDS 分配额外的「未使用空间」。
这样的好处是,下次在操作 SDS 时,如果 SDS 空间够的话,API 就会直接使用「未使用空间」,而无须执行内存分配,有效的减少内存分配次数。
所以,使用 SDS 即不需要手动修改 SDS 的空间大小,也不会出现缓冲区溢出的问题。
#节省内存空间
双向链表
优点:
listNode 链表节点的结构里带有 prev 和 next 指针,获取某个节点的前置节点或后置节点的时间复杂度只需O(1),而且这两个指针都可以指向 NULL,所以链表是无环链表;
list 结构因为提供了表头指针 head 和表尾节点 tail,所以获取链表的表头节点和表尾节点的时间复杂度只需O(1);
list 结构因为提供了链表节点数量 len,所以获取链表中的节点数量的时间复杂度只需O(1);
listNode 链表节使用 void* 指针保存节点值,并且可以通过 list 结构的 dup、free、match 函数指针为节点设置该节点类型特定的函数,因此链表节点可以保存各种不同类型的值;
list 结构因为提供了表头指针 head 和表尾节点 tail,所以获取链表的表头节点和表尾节点的时间复杂度只需O(1);
list 结构因为提供了链表节点数量 len,所以获取链表中的节点数量的时间复杂度只需O(1);
listNode 链表节使用 void* 指针保存节点值,并且可以通过 list 结构的 dup、free、match 函数指针为节点设置该节点类型特定的函数,因此链表节点可以保存各种不同类型的值;
缺点:
链表每个节点之间的内存都是不连续的,意味着无法很好利用 CPU 缓存。能很好利用 CPU 缓存的数据结构就是数组,因为数组的内存是连续的,这样就可以充分利用 CPU 缓存来加速访问。
还有一点,保存一个链表节点的值都需要一个链表节点结构头的分配,内存开销较大。
还有一点,保存一个链表节点的值都需要一个链表节点结构头的分配,内存开销较大。
未完。。。
redis为什么这么快?
1:完全基于内存操作
2:使用单线程模型来处理客户端的请求,避免了上下文的切换
3:IO 多路复用机制
4:自身使用 C 语言编写,有很多优化机制,比如全局哈希表,动态字符串 sds
2:使用单线程模型来处理客户端的请求,避免了上下文的切换
3:IO 多路复用机制
4:自身使用 C 语言编写,有很多优化机制,比如全局哈希表,动态字符串 sds
这是IO模型的一种,即经典的Reactor设计模式,
I/O 多路复用,简单来说就是通过监测文件的读写事件再通知线程执行相关操作,保证 Redis 的非阻塞 I/O 能够顺利执行完成的机制。
多路指的是多个socket连接,
复用指的是复用一个线程。多路复用主要有三种技术:select,poll,epoll。
epoll是最新的也是目前最好的多路复用技术。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。
I/O 多路复用,简单来说就是通过监测文件的读写事件再通知线程执行相关操作,保证 Redis 的非阻塞 I/O 能够顺利执行完成的机制。
多路指的是多个socket连接,
复用指的是复用一个线程。多路复用主要有三种技术:select,poll,epoll。
epoll是最新的也是目前最好的多路复用技术。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。
Redis的线程模型?
Redis 内部使用文件事件处理器 file event handler ,这个文件事件处理器是单线程的,所以
Redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket ,根据 socket 上的事
件来选择对应的事件处理器进行处理。
Redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket ,根据 socket 上的事
件来选择对应的事件处理器进行处理。
文件事件处理器的结构包含 4 个部分:
1. 多个 socket 。 2. IO 多路复用程序。
3. 文件事件分派器。
4. 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)。
1. 多个 socket 。 2. IO 多路复用程序。
3. 文件事件分派器。
4. 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)。
多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会
监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事
件,把该事件交给对应的事件处理器进行处理。
监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事
件,把该事件交给对应的事件处理器进行处理。
redis到底是单线程还是多线程?
Redis 6.0版本之前的单线程指的是其网络I/O和键值对读写是由一个线程完成的。
Redis6.0引入的多线程指的是网络请求过程采用了多线程,而键值对读写命令仍然是单线程 处理的,所以Redis依然是并发安全的。
听说 redis 6.0之后又使用了多线程,不会有线程安全的问题吗?
不会
其实 redis 还是使用单线程模型来处理客户端的请求,只是使用多线程来处理数据的读写和协议解析,执行命令还是使用单线程,所以是不会有线程安全的问题。
之所以加入了多线程因为 redis 的性能瓶颈在于网络IO而非CPU,使用多线程能提升IO读写的效率,从而整体提高redis的性能。
其实 redis 还是使用单线程模型来处理客户端的请求,只是使用多线程来处理数据的读写和协议解析,执行命令还是使用单线程,所以是不会有线程安全的问题。
之所以加入了多线程因为 redis 的性能瓶颈在于网络IO而非CPU,使用多线程能提升IO读写的效率,从而整体提高redis的性能。
Redis底层数据是如何用跳表来存储的?
跳表:将有序链表改造为支持近似“折半查找”算法,可以进行快速的插入、删除、查找操 作。
redis 的持久化机制有哪些?优缺点说说
redis持久化⽅案分为RDB和AOF两种。
AOF和RDB不同,AOF是通过保存redis服务器所执⾏的写命令来记录数据库状态的。
AOF通过追加、写⼊、同步三个步骤来实现持久化机制。
1. 当AOF持久化处于激活状态,服务器执⾏完写命令之后,写命令将会被追加append到aof_buf缓冲
区的末尾
2. 在服务器每结束⼀个事件循环之前,将会调⽤flushAppendOnlyFile函数决定是否要将aof_buf的内
容保存到AOF⽂件中,可以通过配置appendfsync来决定。
如果不设置,默认选项将会是everysec,因为always来说虽然最安全(只会丢失⼀次事件循环的写命
令),但是性能较差,⽽everysec模式只不过会可能丢失1秒钟的数据,⽽no模式的效率和everysec相
仿,但是会丢失上次同步AOF⽂件之后的所有写命令数据。
AOF通过追加、写⼊、同步三个步骤来实现持久化机制。
1. 当AOF持久化处于激活状态,服务器执⾏完写命令之后,写命令将会被追加append到aof_buf缓冲
区的末尾
2. 在服务器每结束⼀个事件循环之前,将会调⽤flushAppendOnlyFile函数决定是否要将aof_buf的内
容保存到AOF⽂件中,可以通过配置appendfsync来决定。
如果不设置,默认选项将会是everysec,因为always来说虽然最安全(只会丢失⼀次事件循环的写命
令),但是性能较差,⽽everysec模式只不过会可能丢失1秒钟的数据,⽽no模式的效率和everysec相
仿,但是会丢失上次同步AOF⽂件之后的所有写命令数据。
AOF重写:
1 # auto‐aof‐rewrite‐min‐size 64mb //aof文件至少要达到64M才会自动重写,文件太 小恢复速度本来就很快,重写的意义不大
2 # auto‐aof‐rewrite‐percentage 100 //aof文件自上一次重写后文件大小增长了100% 则再次触发重写
1 # auto‐aof‐rewrite‐min‐size 64mb //aof文件至少要达到64M才会自动重写,文件太 小恢复速度本来就很快,重写的意义不大
2 # auto‐aof‐rewrite‐percentage 100 //aof文件自上一次重写后文件大小增长了100% 则再次触发重写
RDB持久化可以⼿动执⾏也可以根据配置定期执⾏,它的作⽤是将某个时间点上的数据库状态保存到
RDB⽂件中,RDB⽂件是⼀个压缩的⼆进制⽂件,通过它可以还原某个时刻数据库的状态。由于RDB⽂
件是保存在硬盘上的,所以即使redis崩溃或者退出,只要RDB⽂件存在,就可以⽤它来恢复还原数据库
的状态。
可以通过SAVE或者BGSAVE来⽣成RDB⽂件。
SAVE命令会阻塞redis进程,直到RDB⽂件⽣成完毕,在进程阻塞期间,redis不能处理任何命令请求,
这显然是不合适的。
BGSAVE则是会fork出⼀个⼦进程,然后由⼦进程去负责⽣成RDB⽂件,⽗进程还可以继续处理命令请
求,不会阻塞进程。
RDB⽂件中,RDB⽂件是⼀个压缩的⼆进制⽂件,通过它可以还原某个时刻数据库的状态。由于RDB⽂
件是保存在硬盘上的,所以即使redis崩溃或者退出,只要RDB⽂件存在,就可以⽤它来恢复还原数据库
的状态。
可以通过SAVE或者BGSAVE来⽣成RDB⽂件。
SAVE命令会阻塞redis进程,直到RDB⽂件⽣成完毕,在进程阻塞期间,redis不能处理任何命令请求,
这显然是不合适的。
BGSAVE则是会fork出⼀个⼦进程,然后由⼦进程去负责⽣成RDB⽂件,⽗进程还可以继续处理命令请
求,不会阻塞进程。
Redis持久化RDB、AOF、混合持久化是怎么回事?
Redis的过期键的删除策略有哪些?
定时删除:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
惰性删除:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
定期删除:默认每隔100ms,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
Redis的内存满了怎么办?
实际上Redis定义了「8种内存淘汰策略」用来处理redis内存满的情况:
1.noeviction:直接返回错误,不淘汰任何已经存在的redis键
2.allkeys-lru:所有的键使用lru算法进行淘汰
3.volatile-lru:有过期时间的使用lru算法进行淘汰
4.allkeys-random:随机删除redis键
5.volatile-random:随机删除有过期时间的redis键
6.volatile-ttl:删除快过期的redis键
7.volatile-lfu:根据lfu算法从有过期时间的键删除
8.allkeys-lfu:根据lfu算法从所有键删除
1.noeviction:直接返回错误,不淘汰任何已经存在的redis键
2.allkeys-lru:所有的键使用lru算法进行淘汰
3.volatile-lru:有过期时间的使用lru算法进行淘汰
4.allkeys-random:随机删除redis键
5.volatile-random:随机删除有过期时间的redis键
6.volatile-ttl:删除快过期的redis键
7.volatile-lfu:根据lfu算法从有过期时间的键删除
8.allkeys-lfu:根据lfu算法从所有键删除
Redis淘汰Key的算法LRU与LFU区别?
LRU 算法(Least Recently Used,最近最少使用):淘汰很久没被访问过的数据,以最近一 次访问时间作为参考。
LFU 算法(Least Frequently Used,最不经常使用):淘汰最近一段时间被访问次数最少的 数据,以次数作为参考。
绝大多数情况我们都可以用LRU策略,当存在大量的热点缓存数据时,LFU可能更好点。
LFU 算法(Least Frequently Used,最不经常使用):淘汰最近一段时间被访问次数最少的 数据,以次数作为参考。
绝大多数情况我们都可以用LRU策略,当存在大量的热点缓存数据时,LFU可能更好点。
删除Key的命令会阻塞Redis吗?
有可能的,我们看下DEL Key命令的时间复杂度:
删除单个字符串类型的 key ,时间复杂度为 O(1)。
删除单个列表、集合、有序集合或哈希表类型的 key ,时间复杂度为 O(M), M 为以上数据结构内的元素数量。
删除单个字符串类型的 key ,时间复杂度为 O(1)。
删除单个列表、集合、有序集合或哈希表类型的 key ,时间复杂度为 O(M), M 为以上数据结构内的元素数量。
如果删除的是列表、集合、有序集合或哈希表类型的 key,如果集合元素过多,是会阻塞 Redis的。对于这种情况我们可以借助scan这样的命令循环删除元素。
如果删除的是字符串类型的 key,但是key对应value比较大,比如有几百M,那么也是会 阻塞Redis的。这种bigkey是我们要尽量减少出现的情况。
知道什么时热key吗?热 key 问题怎么解决?
所谓热key问题就是,突然有⼏⼗万的请求去访问redis上的某个特定key,那么这样会造成流量过于集
中,达到物理⽹卡上限,从⽽导致这台redis的服务器宕机引发雪崩。
中,达到物理⽹卡上限,从⽽导致这台redis的服务器宕机引发雪崩。
解决方案:
可以将结果缓存到本地内存中
将热 key 分散到不同的服务器中
设置永不过期
可以将结果缓存到本地内存中
将热 key 分散到不同的服务器中
设置永不过期
缓存击穿、缓存穿透、缓存雪崩是什么?怎么解决呢?
缓存击穿:就是单个key并发访问过⾼,过期时导致所有请求直接打到db上,这个和热key的问题⽐
较类似,只是说的点在于过期导致请求全部打到DB上⽽已。
较类似,只是说的点在于过期导致请求全部打到DB上⽽已。
1. 加锁更新,⽐如请求查询A,发现缓存中没有,对A这个key加锁,同时去数据库查询数据,写⼊缓
存,再返回给⽤户,这样后⾯的请求就可以从缓存中拿到数据了。(分布式锁/sychronized单机锁)
2. 永不过期
存,再返回给⽤户,这样后⾯的请求就可以从缓存中拿到数据了。(分布式锁/sychronized单机锁)
2. 永不过期
缓存穿透:缓存穿透是指用户请求的数据在缓存中不存在并且在数据库中也不存在,导致用户每次请求该数据都要去数据库中查询一遍,然后返回空。
1.加锁(类似击穿)
2.返回空对象
2.返回空对象
缓存雪崩:当某⼀时刻发⽣⼤规模的缓存失效的情况,⽐如你的缓存服务宕机了,会有⼤量的请求进来直接打到DB
上,这样可能导致整个系统的崩溃,称为雪崩。雪崩和击穿、热key的问题不太⼀样的是,他是指⼤规模
的缓存都过期失效了。
上,这样可能导致整个系统的崩溃,称为雪崩。雪崩和击穿、热key的问题不太⼀样的是,他是指⼤规模
的缓存都过期失效了。
1. 针对不同key设置不同的过期时间,避免同时过期
2. 限流,如果redis宕机,可以限流,避免同时刻⼤量请求打崩DB
3. ⼆级缓存,同热key的⽅案。
2. 限流,如果redis宕机,可以限流,避免同时刻⼤量请求打崩DB
3. ⼆级缓存,同热key的⽅案。
Redis 有哪些部署方式?
单机模式:这也是最基本的部署方式,只需要一台机器,负责读写,一般只用于开发人员自己测试
主从复制:在主从复制这种集群部署模式中,我们会将数据库分为两类,第一种称为主数据库(master),另一种称为从数据库(slave)。主数据库会负责我们整个系统中的读写操作,从数据库会负责我们整个数据库中的读操作。其中在职场开发中的真实情况是,我们会让主数据库只负责写操作,让从数据库只负责读操作,就是为了读写分离,减轻服务器的压力。
哨兵模式:哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。它具备自动故障转移、集群监控、消息通知等功能。
cluster集群模式:在redis3.0版本中支持了cluster集群部署的方式,这种集群部署的方式能自动将数据进行分片,每个master上放一部分数据,提供了内置的高可用服务,即使某个master挂了,服务还可以正常地提供。
哨兵的作用和选举过程是怎么样的?
哨兵可以同时监视多个主从服务器,并且在被监视的master下线时,⾃动将某个slave提升为master,
然后由新的master继续接收命令
然后由新的master继续接收命令
整个过程如下:
1. 初始化sentinel,将普通的redis代码替换成sentinel专⽤代码
2. 初始化masters字典和服务器信息,服务器信息主要保存ip:port,并记录实例的地址和ID
3. 创建和master的两个连接,命令连接和订阅连接,并且订阅sentinel:hello频道
4. 每隔10秒向master发送info命令,获取master和它下⾯所有slave的当前信息
5. 当发现master有新的slave之后,sentinel和新的slave同样建⽴两个连接,同时每个10秒发送info
命令,更新master信息
6. sentinel每隔1秒向所有服务器发送ping命令,如果某台服务器在配置的响应时间内连续返回⽆效回
复,将会被标记为下线状态
7. 选举出领头sentinel,领头sentinel需要半数以上的sentinel同意
8. 领头sentinel从已下线的的master所有slave中挑选⼀个,将其转换为master
9. 让所有的slave改为从新的master复制数据
10. 将原来的master设置为新的master的从服务器,当原来master重新回复连接时,就变成了新
master的从服务器
sentinel会每隔1秒向所有实例(包括主从服务器和其他sentinel)发送ping命令,并且根据回复判断是
否已经下线,这种⽅式叫做主观下线。当判断为主观下线时,就会向其他监视的sentinel询问,如果超过
半数的投票认为已经是下线状态,则会标记为客观下线状态,同时触发故障转移。
1. 初始化sentinel,将普通的redis代码替换成sentinel专⽤代码
2. 初始化masters字典和服务器信息,服务器信息主要保存ip:port,并记录实例的地址和ID
3. 创建和master的两个连接,命令连接和订阅连接,并且订阅sentinel:hello频道
4. 每隔10秒向master发送info命令,获取master和它下⾯所有slave的当前信息
5. 当发现master有新的slave之后,sentinel和新的slave同样建⽴两个连接,同时每个10秒发送info
命令,更新master信息
6. sentinel每隔1秒向所有服务器发送ping命令,如果某台服务器在配置的响应时间内连续返回⽆效回
复,将会被标记为下线状态
7. 选举出领头sentinel,领头sentinel需要半数以上的sentinel同意
8. 领头sentinel从已下线的的master所有slave中挑选⼀个,将其转换为master
9. 让所有的slave改为从新的master复制数据
10. 将原来的master设置为新的master的从服务器,当原来master重新回复连接时,就变成了新
master的从服务器
sentinel会每隔1秒向所有实例(包括主从服务器和其他sentinel)发送ping命令,并且根据回复判断是
否已经下线,这种⽅式叫做主观下线。当判断为主观下线时,就会向其他监视的sentinel询问,如果超过
半数的投票认为已经是下线状态,则会标记为客观下线状态,同时触发故障转移。
cluster集群模式是怎么存放数据的?
一个cluster集群中总共有16384个节点,集群会将这16384个节点平均分配给每个节点
Redis集群数据hash分片算法是怎么回事?
Redis Cluster 将所有数据划分为 16384 个 slots(槽位),每个节点负责其中一部分槽位。 槽位的信息存储于每个节点中。 当 Redis Cluster 的客户端来连接集群时,它也会得到一份集群的槽位配置信息并将其缓存 在客户端本地。这样当客户端要查找某个 key 时,可以根据槽位定位算法定位到目标节 点
槽位定位算法:
Cluster 默认会对 key 值使用 crc16 算法进行 hash 得到一个整数值,然后用这个整数值对 16384 进行取模来得到具体槽位。 HASH_SLOT = CRC16(key) mod 16384 再根据槽位值和Redis节点的对应关系就可以定位到key具体是落在哪个Redis节点上的。
Cluster 默认会对 key 值使用 crc16 算法进行 hash 得到一个整数值,然后用这个整数值对 16384 进行取模来得到具体槽位。 HASH_SLOT = CRC16(key) mod 16384 再根据槽位值和Redis节点的对应关系就可以定位到key具体是落在哪个Redis节点上的。
Redis执行命令竟然有死循环阻塞Bug?
如果你想随机查看 Redis 中的一个 key,Redis里有一个 RANDOMKEY 命令可以从 Redis 中随机取出一个 key,这个命令可能导致Redis死循环阻塞。
前面的面试题讲过Redis对于过期Key的清理策略是定时删除与惰性删除两种方式结合来做 的,而 RANDOMKEY 在随机拿出一个 key 后,首先会先检查这个 key 是否已过期,如果 该 key 已经过期,那么 Redis 会删除它,这个过程就是惰性删除。但清理完了还不能结 束,Redis 还要找出一个没过期的 key,返回给客户端。 此时,Redis 则会继续随机拿出一个 key,然后再判断它是否过期,直到找出一个没过期的 key 返回给客户端。 这里就有一个问题了,如果此时 Redis 中,有大量 key 已经过期,但还未来得及被清理 掉,那这个循环就会持续很久才能结束,而且,这个耗时都花费在了清理过期 key 以及寻 找不过期 key 上,导致的结果就是,RANDOMKEY 执行耗时变长,影响 Redis 性能。
以上流程,其实是在 master 上执行的。 如果在 slave 上执行 RANDOMEKY,那么问题会更严重。 slave 自己是不会清理过期 key,当一个 key 要过期时,master 会先清理删除它,之后 master 向 slave 发送一个 DEL 命令,告知 slave 也删除这个 key,以此达到主从库的数据
一致性。 假设Redis 中存在大量已过期还未被清理的 key,那在 slave 上执行 RANDOMKEY 时,就 会发生以下问题: 1、slave 随机取出一个 key,判断是否已过期。 2、key 已过期,但 slave 不会删除它,而是继续随机寻找不过期的 key。 3、由于大量 key 都已过期,那 slave 就会寻找不到符合条件的 key,此时就会陷入死循 环。也就是说,在 slave 上执行 RANDOMKEY,有可能会造成整个 Redis 实例卡死。 这其实是 Redis 的一个 Bug,这个 Bug 一直持续到 5.0 才被修复,修复的解决方案就是在 slave中最多找一定的次数,无论是否能找到,都会退出循环。
以上流程,其实是在 master 上执行的。 如果在 slave 上执行 RANDOMEKY,那么问题会更严重。 slave 自己是不会清理过期 key,当一个 key 要过期时,master 会先清理删除它,之后 master 向 slave 发送一个 DEL 命令,告知 slave 也删除这个 key,以此达到主从库的数据
一致性。 假设Redis 中存在大量已过期还未被清理的 key,那在 slave 上执行 RANDOMKEY 时,就 会发生以下问题: 1、slave 随机取出一个 key,判断是否已过期。 2、key 已过期,但 slave 不会删除它,而是继续随机寻找不过期的 key。 3、由于大量 key 都已过期,那 slave 就会寻找不到符合条件的 key,此时就会陷入死循 环。也就是说,在 slave 上执行 RANDOMKEY,有可能会造成整个 Redis 实例卡死。 这其实是 Redis 的一个 Bug,这个 Bug 一直持续到 5.0 才被修复,修复的解决方案就是在 slave中最多找一定的次数,无论是否能找到,都会退出循环。
一次线上事故,Redis主从切换导致了缓存雪崩
我们假设,slave 的机器时钟比 master 走得快很多。 此时,Redis master里设置了过期时间的key,从 slave 角度来看,可能会有很多在 master 里没过期的数据其实已经过期了。 如果此时操作主从切换,把 slave 提升为新的 master。 它成为 master 后,就会开始大量清理过期 key,此时就会导致以下结果: 1. master 大量清理过期 key,主线程可能会发生阻塞,无法及时处理客户端请 求。2. Redis 中数据大量过期,引发缓存雪崩。 当 master与slave 机器时钟严重不一致时,对业务的影响非常大。 所以,我们一定要保证主从库的机器时钟一致性,避免发生这些问题。
cluster的故障恢复是怎么做的?
判断故障的逻辑其实与哨兵模式有点类似,在集群中,每个节点都会定期的向其他节点发送ping命令,通过有没有收到回复来判断其他节点是否已经下线。
如果长时间没有回复,那么发起ping命令的节点就会认为目标节点疑似下线,也可以和哨兵一样称作主观下线,当然也需要集群中一定数量的节点都认为该节点下线才可以
1.当A节点发现目标节点疑似下线,就会向集群中的其他节点散播消息,其他节点就会向目标节点发送命令,判断目标节点是否下线
2.如果集群中半数以上的节点都认为目标节点下线,就会对目标节点标记为下线,从而告诉其他节点,让目标节点在整个集群中都下线
2.如果集群中半数以上的节点都认为目标节点下线,就会对目标节点标记为下线,从而告诉其他节点,让目标节点在整个集群中都下线
Redis集群网络抖动导致频繁主从切换怎么处理?
真实世界的机房网络往往并不是风平浪静的,它们经常会发生各种各样的小问题。比如网络 抖动就是非常常见的一种现象,突然之间部分连接变得不可访问,然后很快又恢复正常。 为解决这种问题,Redis Cluster 提供了一种选项clusternodetimeout,表示当某个 节点持续 timeout 的时间失联时,才可以认定该节点出现故障,需要进行主从切换。如果没有这个选项,网络抖动会导致主从频繁切换 (数据的重新复制)
Redis集群为什么至少需要三个master节点?
因为新master的选举需要大于半数的集群master节点同意才能选举成功,如果只有两个 master节点,当其中一个挂了,是达不到选举新master的条件的。
Redis集群为什么推荐奇数个节点?
因为新master的选举需要大于半数的集群master节点同意才能选举成功,奇数个master节 点可以在满足选举该条件的基础上节省一个节点,比如三个master节点和四个master节点 的集群相比,大家如果都挂了一个master节点都能选举新master节点,如果都挂了两个 master节点都没法选举新master节点了,所以奇数的master节点更多的是从节省机器资源 角度出发说的。
Redis集群支持批量操作命令吗?
对于类似mset,mget这样的多个key的原生批量操作命令,redis集群只支持所有key落在 同一slot的情况,如果有多个key一定要用mset命令在redis集群上操作,则可以在key的前 面加上{XXX},这样参数数据分片hash计算的只会是大括号里的值,这样能确保不同的key 能落到同一slot里去,示例如下: 1 mset {user1}:1:name zhuge {user1}:1:age 18 假设name和age计算的hash slot值不一样,但是这条命令在集群下执行,redis只会用大括 号里的 user1 做hash slot计算,所以算出来的slot值肯定相同,最后都能落在同一slot。
Lua脚本能在Redis集群里执行吗?
Redis官方规定Lua脚本如果想在Redis集群里执行,需要Lua脚本里操作的所有Redis Key 落在集群的同一个节点上,这种的话我们可以给Lua脚本的Key前面加一个相同的hash tag,就是{XXX},这样就能保证Lua脚本里所有Key落在相同的节点上了。
主从同步原理是怎样的?
1.当一个从数据库启动时,它会向主数据库发送一个SYNC命令,master收到后,在后台保存快照,也就是我们说的RDB持久化,当然保存快照是需要消耗时间的,并且redis是单线程的,在保存快照期间redis收到的命令会缓存起来
2.快照完成后会将缓存的命令以及快照一起打包发给slave节点,从而保证主从数据库的一致性。
3.从数据库接受到快照以及缓存的命令后会将这部分数据写入到硬盘上的临时文件当中,写入完成后会用这份文件去替换掉RDB快照文件,当然,这个操作是不会阻塞的,可以继续接收命令执行,具体原因其实就是fork了一个子进程,用子进程去完成了这些功能。
2.快照完成后会将缓存的命令以及快照一起打包发给slave节点,从而保证主从数据库的一致性。
3.从数据库接受到快照以及缓存的命令后会将这部分数据写入到硬盘上的临时文件当中,写入完成后会用这份文件去替换掉RDB快照文件,当然,这个操作是不会阻塞的,可以继续接收命令执行,具体原因其实就是fork了一个子进程,用子进程去完成了这些功能。
因为不会阻塞,所以,这部分初始化完成后,当主数据库执行了改变数据的命令后,会异步的给slave,这也就是我们说的复制同步阶段,这个阶段会贯穿在整个主从同步的过程中,直到主从同步结束后,复制同步才会终止。
无硬盘复制是什么?
1.master禁用了RDB快照时,发生了主从同步(复制初始化)操作,也会生成RDB快照,但是之后如果master发成了重启,就会用RDB快照去恢复数据,这份数据可能已经很久了,中间就会丢失数据
2.在这种一主多从的结构中,master每次和slave同步数据都要进行一次快照,从而在硬盘中生成RDB文件,会影响性能
2.在这种一主多从的结构中,master每次和slave同步数据都要进行一次快照,从而在硬盘中生成RDB文件,会影响性能
为了解决这种问题,redis在后续的更新中也加入了无硬盘复制功能,也就是说直接通过网络发送给slave,避免了和硬盘交互,但是也是有io消耗
Redis主从复制风暴是怎么回事?
如果Redis主节点有很多从节点,在某一时刻如果所有从节点都同时连接主节点,那么主节 点会同时把内存快照RDB发给多个从节点,这样会导致Redis主节点压力非常大,这就是所 谓的Redis主从复制风暴问题。
Redis 常见性能问题和解决方案有哪些?
Master 最好不要做任何持久化工作,如 RDB 内存快照和 AOF 日志文件;
如果数据比较重要,某个 Slave 开启 AOF 备份数据,策略设置为每秒同步一次;
为了主从复制的速度和连接的稳定性,Master 和 Slave 最好在同一个局域网内;
尽量避免在压力很大的主库上增加从库;
主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <-
Slave3….;这样的结构方便解决单点故障问题,实现 Slave 对 Master 的替换。如果 Master 挂
了,可以立刻启用 Slave1 做 Master,其他不变。
如果数据比较重要,某个 Slave 开启 AOF 备份数据,策略设置为每秒同步一次;
为了主从复制的速度和连接的稳定性,Master 和 Slave 最好在同一个局域网内;
尽量避免在压力很大的主库上增加从库;
主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <-
Slave3….;这样的结构方便解决单点故障问题,实现 Slave 对 Master 的替换。如果 Master 挂
了,可以立刻启用 Slave1 做 Master,其他不变。
网络
谈⼀谈你对TCP/IP四层模型,OSI七层模型的理解?
为了增强通⽤性和兼容性,计算机⽹络都被设计成层次机构,每⼀层都遵守⼀定的规则。
因此有了OSI这样⼀个抽象的⽹络通信参考模型,按照这个标准使计算机⽹络系统可以互相连接。
物理层:通过⽹线、光缆等这种物理⽅式将电脑连接起来。传递的数据是⽐特流,0101010100。
数据链路层: ⾸先,把⽐特流封装成数据帧的格式,对0、1进⾏分组。电脑连接起来之后,数据都经过
⽹卡来传输,⽽⽹卡上定义了全世界唯⼀的MAC地址。然后再通过⼴播的形式向局域⽹内所有电脑发送
数据,再根据数据中MAC地址和⾃身对⽐判断是否是发给⾃⼰的。
⽹络层:⼴播的形式太低效,为了区分哪些MAC地址属于同⼀个⼦⽹,⽹络层定义了IP和⼦⽹掩码,通
过对IP和⼦⽹掩码进⾏与运算就知道是否是同⼀个⼦⽹,再通过路由器和交换机进⾏传输。IP协议属于
⽹络层的协议。
传输层:有了⽹络层的MAC+IP地址之后,为了确定数据包是从哪个进程发送过来的,就需要端⼝号,通
过端⼝来建⽴通信,⽐如TCP和UDP属于这⼀层的协议。
会话层:负责建⽴和断开连接
表示层:为了使得数据能够被其他的计算机理解,再次将数据转换成另外⼀种格式,⽐如⽂字、视频、
图⽚等。
应⽤层:最⾼层,⾯对⽤户,提供计算机⽹络与最终呈现给⽤户的界⾯
因此有了OSI这样⼀个抽象的⽹络通信参考模型,按照这个标准使计算机⽹络系统可以互相连接。
物理层:通过⽹线、光缆等这种物理⽅式将电脑连接起来。传递的数据是⽐特流,0101010100。
数据链路层: ⾸先,把⽐特流封装成数据帧的格式,对0、1进⾏分组。电脑连接起来之后,数据都经过
⽹卡来传输,⽽⽹卡上定义了全世界唯⼀的MAC地址。然后再通过⼴播的形式向局域⽹内所有电脑发送
数据,再根据数据中MAC地址和⾃身对⽐判断是否是发给⾃⼰的。
⽹络层:⼴播的形式太低效,为了区分哪些MAC地址属于同⼀个⼦⽹,⽹络层定义了IP和⼦⽹掩码,通
过对IP和⼦⽹掩码进⾏与运算就知道是否是同⼀个⼦⽹,再通过路由器和交换机进⾏传输。IP协议属于
⽹络层的协议。
传输层:有了⽹络层的MAC+IP地址之后,为了确定数据包是从哪个进程发送过来的,就需要端⼝号,通
过端⼝来建⽴通信,⽐如TCP和UDP属于这⼀层的协议。
会话层:负责建⽴和断开连接
表示层:为了使得数据能够被其他的计算机理解,再次将数据转换成另外⼀种格式,⽐如⽂字、视频、
图⽚等。
应⽤层:最⾼层,⾯对⽤户,提供计算机⽹络与最终呈现给⽤户的界⾯
TCP/IP则是四层的结构,相当于是对OSI模型的简化。
1. 数据链路层,也有称作⽹络访问层、⽹络接⼝层。他包含了OSI模型的物理层和数据链路层,把电
脑连接起来。
2. ⽹络层,也叫做IP层,处理IP数据包的传输、路由,建⽴主机间的通信。
3. 传输层,就是为两台主机设备提供端到端的通信。
4. 应⽤层,包含OSI的会话层、表示层和应⽤层,提供了⼀些常⽤的协议规范,⽐如FTP、SMPT、
HTTP等。
1. 数据链路层,也有称作⽹络访问层、⽹络接⼝层。他包含了OSI模型的物理层和数据链路层,把电
脑连接起来。
2. ⽹络层,也叫做IP层,处理IP数据包的传输、路由,建⽴主机间的通信。
3. 传输层,就是为两台主机设备提供端到端的通信。
4. 应⽤层,包含OSI的会话层、表示层和应⽤层,提供了⼀些常⽤的协议规范,⽐如FTP、SMPT、
HTTP等。
总结下来,就是物理层通过物理⼿段把电脑连接起来,数据链路层则对⽐特流的数据进⾏分组,⽹络层
来建⽴主机到主机的通信,传输层建⽴端⼝到端⼝的通信,应⽤层最终负责建⽴连接,数据格式转换,
最终呈现给⽤户。
来建⽴主机到主机的通信,传输层建⽴端⼝到端⼝的通信,应⽤层最终负责建⽴连接,数据格式转换,
最终呈现给⽤户。
说说TCP 3次握⼿的过程?
建⽴连接前server端需要监听端⼝,所以初始状态是LISTEN。
1. client端建⽴连接,发送⼀个SYN同步包,发送之后状态变成SYN_SENT
2. server端收到SYN之后,同意建⽴连接,返回⼀个ACK响应,同时也会给client发送⼀个SYN包,发
送完成之后状态变为SYN_RCVD
3. client端收到server的ACK之后,状态变为ESTABLISHED,返回ACK给server端。server收到之后状
态也变为ESTABLISHED,连接建⽴完成。
1. client端建⽴连接,发送⼀个SYN同步包,发送之后状态变成SYN_SENT
2. server端收到SYN之后,同意建⽴连接,返回⼀个ACK响应,同时也会给client发送⼀个SYN包,发
送完成之后状态变为SYN_RCVD
3. client端收到server的ACK之后,状态变为ESTABLISHED,返回ACK给server端。server收到之后状
态也变为ESTABLISHED,连接建⽴完成。
为什么要3次?2次,4次不⾏吗?
因为TCP是双⼯传输模式,不区分客户端和服务端,连接的建⽴是双向的过程。
如果只有两次,⽆法做到双向连接的建⽴,从建⽴连接server回复的SYN和ACK合并成⼀次可以看出来,
他也不需要4次。
挥⼿为什么要四次?因为挥⼿的ACK和FIN不能同时发送,因为数据发送的截⽌时间不同。
如果只有两次,⽆法做到双向连接的建⽴,从建⽴连接server回复的SYN和ACK合并成⼀次可以看出来,
他也不需要4次。
挥⼿为什么要四次?因为挥⼿的ACK和FIN不能同时发送,因为数据发送的截⽌时间不同。
那么四次挥⼿的过程呢?
1. client端向server发送FIN包,进⼊FIN_WAIT_1状态,这代表client端已经没有数据要发送了
2. server端收到之后,返回⼀个ACK,进⼊CLOSE_WAIT等待关闭的状态,因为server端可能还有没
有发送完成的数据
3. 等到server端数据都发送完毕之后,server端就向client发送FIN,进⼊LAST_ACK状态
4. client收到ACK之后,进⼊TIME_WAIT的状态,同时回复ACK,server收到之后直接进⼊CLOSED状
态,连接关闭。但是client要等待2MSL(报⽂最⼤⽣存时间)的时间,才会进⼊CLOSED状态。
2. server端收到之后,返回⼀个ACK,进⼊CLOSE_WAIT等待关闭的状态,因为server端可能还有没
有发送完成的数据
3. 等到server端数据都发送完毕之后,server端就向client发送FIN,进⼊LAST_ACK状态
4. client收到ACK之后,进⼊TIME_WAIT的状态,同时回复ACK,server收到之后直接进⼊CLOSED状
态,连接关闭。但是client要等待2MSL(报⽂最⼤⽣存时间)的时间,才会进⼊CLOSED状态。
第四次挥手为什么要等待2MSL(60s)
1. 为了保证连接的可靠关闭。如果server没有收到最后⼀个ACK,那么就会重发FIN。
2. 为了避免端⼝重⽤带来的数据混淆。如果client直接进⼊CLOSED状态,⼜⽤相同端⼝号向server建
⽴⼀个连接,上⼀次连接的部分数据在⽹络中延迟到达server,数据就可能发⽣混淆了。
2. 为了避免端⼝重⽤带来的数据混淆。如果client直接进⼊CLOSED状态,⼜⽤相同端⼝号向server建
⽴⼀个连接,上⼀次连接的部分数据在⽹络中延迟到达server,数据就可能发⽣混淆了。
TCP怎么保证传输过程的可靠性?
校验和:发送⽅在发送数据之前计算校验和,接收⽅收到数据后同样计算,如果不⼀致,那么传输有
误。
确认应答,序列号:TCP进⾏传输时数据都进⾏了编号,每次接收⽅返回ACK都有确认序列号。
超时重传:如果发送⽅发送数据⼀段时间后没有收到ACK,那么就重发数据。
连接管理:三次握⼿和四次挥⼿的过程。
流量控制:TCP协议报头包含16位的窗⼝⼤⼩,接收⽅会在返回ACK时同时把⾃⼰的即时窗⼝填⼊,发
送⽅就根据报⽂中窗⼝的⼤⼩控制发送速度。
拥塞控制:刚开始发送数据的时候,拥塞窗⼝是1,以后每次收到ACK,则拥塞窗⼝+1,然后将拥塞窗⼝
和收到的窗⼝取较⼩值作为实际发送的窗⼝,如果发⽣超时重传,拥塞窗⼝重置为1。这样做的⽬的就是
为了保证传输过程的⾼效性和可靠性。
误。
确认应答,序列号:TCP进⾏传输时数据都进⾏了编号,每次接收⽅返回ACK都有确认序列号。
超时重传:如果发送⽅发送数据⼀段时间后没有收到ACK,那么就重发数据。
连接管理:三次握⼿和四次挥⼿的过程。
流量控制:TCP协议报头包含16位的窗⼝⼤⼩,接收⽅会在返回ACK时同时把⾃⼰的即时窗⼝填⼊,发
送⽅就根据报⽂中窗⼝的⼤⼩控制发送速度。
拥塞控制:刚开始发送数据的时候,拥塞窗⼝是1,以后每次收到ACK,则拥塞窗⼝+1,然后将拥塞窗⼝
和收到的窗⼝取较⼩值作为实际发送的窗⼝,如果发⽣超时重传,拥塞窗⼝重置为1。这样做的⽬的就是
为了保证传输过程的⾼效性和可靠性。
负载均衡有哪些实现⽅式?
DNS:这是最简单的负载均衡的⽅式,⼀般⽤于实现地理级别的负载均衡,不同地域的⽤户通过DNS的
解析可以返回不同的IP地址,这种⽅式的负载均衡简单,但是扩展性太差,控制权在域名服务商。
Http重定向:通过修改Http响应头的Location达到负载均衡的⽬的,Http的302重定向。这种⽅式对性
能有影响,⽽且增加请求耗时。
反向代理:作⽤于应⽤层的模式,也被称作为七层负载均衡,⽐如常⻅的Nginx,性能⼀般可以达到万
级。这种⽅式部署简单,成本低,⽽且容易扩展。
IP:作⽤于⽹络层的和传输层的模式,也被称作四层负载均衡,通过对数据包的IP地址和端⼝进⾏修改
来达到负载均衡的效果。常⻅的有LVS(Linux Virtual Server),通常性能可以⽀持10万级并发。
按照类型来划分的话,还可以分成DNS负载均衡、硬件负载均衡、软件负载均衡。
其中硬件负载均衡价格昂贵,性能最好,能达到百万级,软件负载均衡包括Nginx、LVS这种。
解析可以返回不同的IP地址,这种⽅式的负载均衡简单,但是扩展性太差,控制权在域名服务商。
Http重定向:通过修改Http响应头的Location达到负载均衡的⽬的,Http的302重定向。这种⽅式对性
能有影响,⽽且增加请求耗时。
反向代理:作⽤于应⽤层的模式,也被称作为七层负载均衡,⽐如常⻅的Nginx,性能⼀般可以达到万
级。这种⽅式部署简单,成本低,⽽且容易扩展。
IP:作⽤于⽹络层的和传输层的模式,也被称作四层负载均衡,通过对数据包的IP地址和端⼝进⾏修改
来达到负载均衡的效果。常⻅的有LVS(Linux Virtual Server),通常性能可以⽀持10万级并发。
按照类型来划分的话,还可以分成DNS负载均衡、硬件负载均衡、软件负载均衡。
其中硬件负载均衡价格昂贵,性能最好,能达到百万级,软件负载均衡包括Nginx、LVS这种。
说说BIO/NIO/AIO的区别?
BIO:同步阻塞IO,每⼀个客户端连接,服务端都会对应⼀个处理线程,对于没有分配到处理线程的连
接就会被阻塞或者拒绝。相当于是⼀个连接⼀个线程。
接就会被阻塞或者拒绝。相当于是⼀个连接⼀个线程。
NIO:同步⾮阻塞IO,基于Reactor模型,客户端和channel进⾏通信,channel可以进⾏读写操作,通
过多路复⽤器selector来轮询注册在其上的channel,⽽后再进⾏IO操作。这样的话,在进⾏IO操作的时
候再⽤⼀个线程去处理就可以了,也就是⼀个请求⼀个线程。
过多路复⽤器selector来轮询注册在其上的channel,⽽后再进⾏IO操作。这样的话,在进⾏IO操作的时
候再⽤⼀个线程去处理就可以了,也就是⼀个请求⼀个线程。
AIO:异步⾮阻塞IO,相⽐NIO更进⼀步,完全由操作系统来完成请求的处理,然后通知服务端开启线程
去进⾏处理,因此是⼀个有效请求⼀个线程。
去进⾏处理,因此是⼀个有效请求⼀个线程。
那么你怎么理解同步和阻塞?
⾸先,可以认为⼀个IO操作包含两个部分:
1. 发起IO请求
2. 实际的IO读写操作
同步和异步在于第⼆个,实际的IO读写操作,如果操作系统帮你完成了再通知你,那就是异步,否则都
叫做同步。
1. 发起IO请求
2. 实际的IO读写操作
同步和异步在于第⼆个,实际的IO读写操作,如果操作系统帮你完成了再通知你,那就是异步,否则都
叫做同步。
阻塞和⾮阻塞在于第⼀个,发起IO请求,对于NIO来说通过channel发起IO操作请求后,其实就返回了,
所以是⾮阻塞。
所以是⾮阻塞。
谈⼀下你对Reactor模型的理解?
Reactor模型包含两个组件:
1. Reactor:负责查询、响应IO事件,当检测到IO事件时,分发给Handlers处理。
2. Handler:与IO事件绑定,负责IO事件的处理
1. Reactor:负责查询、响应IO事件,当检测到IO事件时,分发给Handlers处理。
2. Handler:与IO事件绑定,负责IO事件的处理
单线程Reactor
这个模式reactor和handler在⼀个线程中,如果某个handler阻塞的话,会导致其他所有的handler⽆法
执⾏,⽽且⽆法充分利⽤多核的性能。
这个模式reactor和handler在⼀个线程中,如果某个handler阻塞的话,会导致其他所有的handler⽆法
执⾏,⽽且⽆法充分利⽤多核的性能。
单Reactor多线程
由于decode、compute、encode的操作并⾮IO的操作,多线程Reactor的思路就是充分发挥多核的特
性,同时把⾮IO的操作剥离开。
但是,单个Reactor承担了所有的事件监听、响应⼯作,如果连接过多,还是可能存在性能问题。
由于decode、compute、encode的操作并⾮IO的操作,多线程Reactor的思路就是充分发挥多核的特
性,同时把⾮IO的操作剥离开。
但是,单个Reactor承担了所有的事件监听、响应⼯作,如果连接过多,还是可能存在性能问题。
多Reactor多线程
为了解决单Reactor的性能问题,就产⽣了多Reactor的模式。其中mainReactor建⽴连接,多个
subReactor则负责数据读写。
为了解决单Reactor的性能问题,就产⽣了多Reactor的模式。其中mainReactor建⽴连接,多个
subReactor则负责数据读写。
介绍一下 HTTP 协议吧
HTTP 协议是基于 TCP 协议实现的,它是一个超文本传输协议,其实就是一个简单的请求-响应协议,它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。
它主要是负责点对点之间通信的。
超文本就是用超链接的方法,将各种不同空间的文字信息组织在一起的网状文本。比如说html,内部定义了很多图片视频的链接,放在浏览器上就呈现出了画面。
协议就是约定俗称的东西,比如说 moon 要给读者送一本书,读者那里只接受顺丰快递,那么 moon 觉得可以,发快递的时候选择的顺丰,那么我们彼此之间共同约定好的就叫做协议。
传输这个就很好理解了,比如刚才举的例子,将书发给读者,要通过骑车或者飞机的方式,传递的这个过程就是运输。
它主要是负责点对点之间通信的。
超文本就是用超链接的方法,将各种不同空间的文字信息组织在一起的网状文本。比如说html,内部定义了很多图片视频的链接,放在浏览器上就呈现出了画面。
协议就是约定俗称的东西,比如说 moon 要给读者送一本书,读者那里只接受顺丰快递,那么 moon 觉得可以,发快递的时候选择的顺丰,那么我们彼此之间共同约定好的就叫做协议。
传输这个就很好理解了,比如刚才举的例子,将书发给读者,要通过骑车或者飞机的方式,传递的这个过程就是运输。
GET 和 POST有什么区别?
GET 和 POST 本质上就是 TCP 链接,并无差别。
但是由于 HTTP 的规定和浏览器/服务器的限制,导致他们在应用过程中体现出一些不同。
但是由于 HTTP 的规定和浏览器/服务器的限制,导致他们在应用过程中体现出一些不同。
PING 的作用?
PING 主要的作用就是测试在两台主机之间能否建立连接,如果 PING 不通就无法建立连接。
它其实就是向目的主机发送多个 ICMP 回送请求报文
如果没有响应则无法建立连接
如果有响应就可以根据目的主机返回的回送报文的时间和成功响应的次数估算出数据包往返时间及丢包率
它其实就是向目的主机发送多个 ICMP 回送请求报文
如果没有响应则无法建立连接
如果有响应就可以根据目的主机返回的回送报文的时间和成功响应的次数估算出数据包往返时间及丢包率
常见的 HTTP 状态码有哪些
1xx 信息,服务器收到请求,需要请求者继续执行操作
2xx 成功,操作被成功接收并处理
3xx 重定向,需要进一步的操作以完成请求
4xx 客户端错误,请求包含语法错误或无法完成请求
5xx 服务器错误,服务器在处理请求的过程中发生了错误
2xx 成功,操作被成功接收并处理
3xx 重定向,需要进一步的操作以完成请求
4xx 客户端错误,请求包含语法错误或无法完成请求
5xx 服务器错误,服务器在处理请求的过程中发生了错误
HTTP1.1 和 HTTP1.0 的区别有哪些?
1.长链接
早期 HTTP1.0 的每一次请求都伴随着一次三次握手的过程,并且是串行的请求,增加了不必要的性能开销
HTTP1.1 新增了长链接的通讯方式,减少了性能损耗
2.管道
HTTP1.0 只有串行发送,没有管道
HTTP1.1 增加了管道的概念,使得在同一个 TCP 链接当中可以同时发出多个请求
3.断点续传
HTTP1.0 不支持断点续传
HTTP1.1 新增了 range 字段,用来指定数据字节位置,开启了断点续传的时代
4.Host头处理
HTTP1.0 任务主机只有一个节点,所以并没有传 HOST
HTTP1.1 时代,虚拟机技术越来越发达,一台机器上也有可能有很多节点,故增加了 HOST 信息
5.缓存处理
在HTTP1.0中主要使用header里的If-Modified-Since,Expires来做为缓存判断的标准
HTTP1.1则引入了更多的缓存控制策略例如Entity tag,If-Unmodified-Since, If-Match, If-None-Match等更多可供选择的缓存头来控制缓存策略。
6.错误状态响应码
在HTTP1.1中新增了24个错误状态响应码,如410(Gone)表示服务器上的某个资源被永久性的删除等。
早期 HTTP1.0 的每一次请求都伴随着一次三次握手的过程,并且是串行的请求,增加了不必要的性能开销
HTTP1.1 新增了长链接的通讯方式,减少了性能损耗
2.管道
HTTP1.0 只有串行发送,没有管道
HTTP1.1 增加了管道的概念,使得在同一个 TCP 链接当中可以同时发出多个请求
3.断点续传
HTTP1.0 不支持断点续传
HTTP1.1 新增了 range 字段,用来指定数据字节位置,开启了断点续传的时代
4.Host头处理
HTTP1.0 任务主机只有一个节点,所以并没有传 HOST
HTTP1.1 时代,虚拟机技术越来越发达,一台机器上也有可能有很多节点,故增加了 HOST 信息
5.缓存处理
在HTTP1.0中主要使用header里的If-Modified-Since,Expires来做为缓存判断的标准
HTTP1.1则引入了更多的缓存控制策略例如Entity tag,If-Unmodified-Since, If-Match, If-None-Match等更多可供选择的缓存头来控制缓存策略。
6.错误状态响应码
在HTTP1.1中新增了24个错误状态响应码,如410(Gone)表示服务器上的某个资源被永久性的删除等。
HTTPS 和 HTTP 的区别是什么?
1.SSL安全协议
HTTP 是超⽂本传输协议,信息是明⽂传输,存在安全⻛险的问题。
HTTPS 则解决 HTTP 不安全的缺陷,在TCP 和 HTTP ⽹络层之间加⼊了 SSL/TLS 安全协议,使得报⽂能够加密传输。
2.建立连接
HTTP 连接建⽴相对简单, TCP 三次握⼿之后便可进⾏ HTTP 的报⽂传输。
HTTPS 在 TCP 三次握⼿之后,还需进⾏ SSL/TLS 的握⼿过程,才可进⼊加密报⽂传输。
3.端口号
HTTP 的端⼝号是 80。
HTTPS 的端⼝号是 443。
4.CA证书
HTTPS 协议需要向 CA(证书权威。机构)申请数字证书来保证服务器的身份是可信的。
HTTP 是超⽂本传输协议,信息是明⽂传输,存在安全⻛险的问题。
HTTPS 则解决 HTTP 不安全的缺陷,在TCP 和 HTTP ⽹络层之间加⼊了 SSL/TLS 安全协议,使得报⽂能够加密传输。
2.建立连接
HTTP 连接建⽴相对简单, TCP 三次握⼿之后便可进⾏ HTTP 的报⽂传输。
HTTPS 在 TCP 三次握⼿之后,还需进⾏ SSL/TLS 的握⼿过程,才可进⼊加密报⽂传输。
3.端口号
HTTP 的端⼝号是 80。
HTTPS 的端⼝号是 443。
4.CA证书
HTTPS 协议需要向 CA(证书权威。机构)申请数字证书来保证服务器的身份是可信的。
知道HTTPS的⼯作原理吗?
1. ⽤户通过浏览器请求https⽹站,服务器收到请求,选择浏览器⽀持的加密和hash算法,同时返回
数字证书给浏览器,包含颁发机构、⽹址、公钥、证书有效期等信息。
2. 浏览器对证书的内容进⾏校验,如果有问题,则会有⼀个提示警告。否则,就⽣成⼀个随机数X,
同时使⽤证书中的公钥进⾏加密,并且发送给服务器。
3. 服务器收到之后,使⽤私钥解密,得到随机数X,然后使⽤X对⽹⻚内容进⾏加密,返回给浏览器
4. 浏览器则使⽤X和之前约定的加密算法进⾏解密,得到最终的⽹⻚内容
数字证书给浏览器,包含颁发机构、⽹址、公钥、证书有效期等信息。
2. 浏览器对证书的内容进⾏校验,如果有问题,则会有⼀个提示警告。否则,就⽣成⼀个随机数X,
同时使⽤证书中的公钥进⾏加密,并且发送给服务器。
3. 服务器收到之后,使⽤私钥解密,得到随机数X,然后使⽤X对⽹⻚内容进⾏加密,返回给浏览器
4. 浏览器则使⽤X和之前约定的加密算法进⾏解密,得到最终的⽹⻚内容
HTTP2 和 HTTP1.1 的区别是什么?
1.头部压缩
在 HTTP2 当中,如果你发出了多个请求,并且它们的头部(header)是相同的,那么 HTTP2 协议会帮你消除同样的部分。(其实就是在客户端和服务端维护一张索引表来实现)
2.二进制格式
HTTP1.1 采用明文的形式
HTTP/2 全⾯采⽤了⼆进制格式,头信息和数据体都是⼆进制
3.数据流
HTTP/2 的数据包不是按顺序发送的,同⼀个连接⾥⾯连续的数据包,可能属于不同的回应。(对数据包做了标记,标志其属于哪一个请求,其中规定客户端发出的数据流编号为奇数,服务器发出的数据流编号为偶数。客户端还可以指定数据流的优先级,优先级⾼的请求,服务器就先响应该请求)
4.IO多路复用
如:在⼀个连接中,服务器收到了客户端 A 和 B 的两个请求,但是发现在处理 A 的过程中⾮常耗时,索性就先回应 A 已经处理好的部分,再接着回应 B 请求,最后再回应 A 请求剩下的部分。
HTTP/2 可以在⼀个连接中并发多个请求或回应。
5.服务器推送
服务器可以主动向客户端发送请求
在 HTTP2 当中,如果你发出了多个请求,并且它们的头部(header)是相同的,那么 HTTP2 协议会帮你消除同样的部分。(其实就是在客户端和服务端维护一张索引表来实现)
2.二进制格式
HTTP1.1 采用明文的形式
HTTP/2 全⾯采⽤了⼆进制格式,头信息和数据体都是⼆进制
3.数据流
HTTP/2 的数据包不是按顺序发送的,同⼀个连接⾥⾯连续的数据包,可能属于不同的回应。(对数据包做了标记,标志其属于哪一个请求,其中规定客户端发出的数据流编号为奇数,服务器发出的数据流编号为偶数。客户端还可以指定数据流的优先级,优先级⾼的请求,服务器就先响应该请求)
4.IO多路复用
如:在⼀个连接中,服务器收到了客户端 A 和 B 的两个请求,但是发现在处理 A 的过程中⾮常耗时,索性就先回应 A 已经处理好的部分,再接着回应 B 请求,最后再回应 A 请求剩下的部分。
HTTP/2 可以在⼀个连接中并发多个请求或回应。
5.服务器推送
服务器可以主动向客户端发送请求
HTTP3 和 HTTP2 的区别是什么?
1.协议不同
HTTP2 是基于 TCP 协议实现的
HTTP3 是基于 UDP 协议实现的
2.QUIC
HTTP3 新增了 QUIC 协议来实现可靠性的传输
3.握手次数
HTTP2 是基于 HTTPS 实现的,建立连接需要先进行 TCP 3次握手,然后再进行 TLS 3次握手,总共6次握手
HTTP3 只需要 QUIC 的3次握手
HTTP2 是基于 TCP 协议实现的
HTTP3 是基于 UDP 协议实现的
2.QUIC
HTTP3 新增了 QUIC 协议来实现可靠性的传输
3.握手次数
HTTP2 是基于 HTTPS 实现的,建立连接需要先进行 TCP 3次握手,然后再进行 TLS 3次握手,总共6次握手
HTTP3 只需要 QUIC 的3次握手
TCP 滑动窗⼝是什么?
TCP 是每发送⼀个数据,都要进⾏⼀次确认应答。只有上一个收到了回应才发送下一个,这样效率会非常低,因此引进了滑动窗口的概念.
其实就是在发送方设立一个缓存区间,将已发送但未收到确认的消息缓存起来,假如一个窗口可以发送 5 个 TCP 段,那么发送方就可以连续发送 5 个 TCP 段,然后就会将这 5 个 TCP 段的数据缓存起来,这 5 个 TCP 段是有序的,只要后面的消息收到了 ACK ,那么不管前面的是否有收到 ACK,都代表成功,窗⼝⼤⼩是由接收方决定的。
窗⼝⼤⼩就是指不需要等待应答,还可以发送数据的大小。
其实就是在发送方设立一个缓存区间,将已发送但未收到确认的消息缓存起来,假如一个窗口可以发送 5 个 TCP 段,那么发送方就可以连续发送 5 个 TCP 段,然后就会将这 5 个 TCP 段的数据缓存起来,这 5 个 TCP 段是有序的,只要后面的消息收到了 ACK ,那么不管前面的是否有收到 ACK,都代表成功,窗⼝⼤⼩是由接收方决定的。
窗⼝⼤⼩就是指不需要等待应答,还可以发送数据的大小。
发送方一直发送数据,但是接收方处理不过来怎么办?(流量控制)
如果接收方处理不过来,发送方就会触发重试机制再次发送数据,然而这个是有性能损耗的,为了解决这个问题,TCP 就提出了流量控制,为的就是让发送方知道接受方的处理能力。
也就是说,每次接收方接受到数据后会将剩余可处理数据的大小告诉发送方。
比如接受方滑动窗口可用大小为400字节,发送方发送过来100字节的数据,那么接收方剩余可用滑动窗口大小就为300字节,这是发送方就知道下次返送数据的大小范围了。
但是这里有一个问题,数据会存放在缓冲区,但是这个缓冲区是操作系统控制的,当系统繁忙的时候,会缩减缓冲区减小,可能就会造成丢包的问题。
也就是说,每次接收方接受到数据后会将剩余可处理数据的大小告诉发送方。
比如接受方滑动窗口可用大小为400字节,发送方发送过来100字节的数据,那么接收方剩余可用滑动窗口大小就为300字节,这是发送方就知道下次返送数据的大小范围了。
但是这里有一个问题,数据会存放在缓冲区,但是这个缓冲区是操作系统控制的,当系统繁忙的时候,会缩减缓冲区减小,可能就会造成丢包的问题。
TCP 半连接队列和全连接队列是什么?
服务端收到客户端发出的 SYN 请求后,会把这个连接信息存储到半链接队列(SYN 队列)。
服务端收到第三次握⼿的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到全连接队列(accept 队列),等待进程调⽤ accept 函数时把连接取出来。
这两个队列都是有大小限制的,当超过容量后就会将链接丢弃,或者返回 RST 包。
服务端收到第三次握⼿的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到全连接队列(accept 队列),等待进程调⽤ accept 函数时把连接取出来。
这两个队列都是有大小限制的,当超过容量后就会将链接丢弃,或者返回 RST 包。
粘包/拆包是怎么发生的?怎么解决这个问题?
TCP 发送数据时会根据 TCP 缓冲区的实际情况进行包的划分,一个完整的包可能会被 TCP 拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是 TCP 粘包和拆包问题。
发生 TCP 粘包的原因:
1.发送的数据小于 TCP 缓冲区大小,TCP将缓冲区中的数据(数据属于多条业务内容)一次发送出去可能就会发生粘包。
2.接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。
发生 TCP 拆包的原因:
1.待发送数据大于最大报文长度,TCP 在传输前将进行拆包。
2.发送的数据大于 TCP 发送缓冲区剩余空间大小,将会发生拆包。
1.发送的数据小于 TCP 缓冲区大小,TCP将缓冲区中的数据(数据属于多条业务内容)一次发送出去可能就会发生粘包。
2.接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。
发生 TCP 拆包的原因:
1.待发送数据大于最大报文长度,TCP 在传输前将进行拆包。
2.发送的数据大于 TCP 发送缓冲区剩余空间大小,将会发生拆包。
1.发送端给每个数据包添加包首部,首部中包含数据包的长度,这样接收端在接收到数据后,通过该字段就可以知道每个数据包的实际长度了。
2.发送端将每个数据包设置固定长度,这样接收端每次从读取固定长度的数据把每个数据包拆分开。
3.可以在数据包之间设置边界,如添加特殊符号,接收端可以通过这个特殊符号来拆分包。
2.发送端将每个数据包设置固定长度,这样接收端每次从读取固定长度的数据把每个数据包拆分开。
3.可以在数据包之间设置边界,如添加特殊符号,接收端可以通过这个特殊符号来拆分包。
浏览器地址栏输入网站按回车后发生了什么?
1:解析网址,生成 HTTP 请求信息
2:根据 DNS 服务器查询真实请求的 IP 地址,如果本地服务器有缓存则直接返回
3:得到了 IP 以后,向服务器发送 TCP 连接,TCP 连接经过三次握手。
4:接受 TCP 报文后,对连接进行处理,对 HTTP 协议解析
5:服务器返回响应
6:浏览器接受响应,显示页面,渲染页面
2:根据 DNS 服务器查询真实请求的 IP 地址,如果本地服务器有缓存则直接返回
3:得到了 IP 以后,向服务器发送 TCP 连接,TCP 连接经过三次握手。
4:接受 TCP 报文后,对连接进行处理,对 HTTP 协议解析
5:服务器返回响应
6:浏览器接受响应,显示页面,渲染页面
MQ
MQ有什么用?
消息队列有很多使用场景,比较常见的有3个:解耦、异步、削峰。
解耦:传统的软件开发模式,各个模块之间相互调用,数据共享,每个模块都要时刻关注其他模块的是否更改或者是否挂掉等等,使用消息队列,可以避免模块之间直接调用,将所需共享的数据放在消息队列中,对于新增业务模块,只要对该类消息感兴趣,即可订阅该类消息,对原有系统和业务没有任何影响,降低了系统各个模块的耦合度,提高了系统的可扩展性。
异步:消息队列提供了异步处理机制,在很多时候应用不想也不需要立即处理消息,允许应用把一些消息放入消息中间件中,并不立即处理它,在之后需要的时候再慢慢处理
削峰:在访问量骤增的场景下,需要保证应用系统的平稳性,但是这样突发流量并不常见,如果以这类峰值的标准而投放资源的话,那无疑是巨大的浪费。使用消息队列能够使关键组件支撑突发访问压力,不会因为突发的超负荷请求而完全崩溃。消息队列的容量可以配置的很大,如果采用磁盘存储消息,则几乎等于“无限”容量,这样一来,高峰期的消息可以被积压起来,在随后的时间内进行平滑的处理完成,而不至于让系统短时间内无法承载而导致崩溃。在电商网站的秒杀抢购这种突发性流量很强的业务场景中,消息队列的强大缓冲能力可以很好的起到削峰作用。
缺点:
系统可用性降低
系统的复杂度提高了
一致性问题
系统的复杂度提高了
一致性问题
说一说生产者与消费者模式
所谓生产者-消费者问题,实际上主要是包含了两类线程。一种是生产者线程用于生产数据,另一种是消费者线程用于消费数据,为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库。生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为。而消费者只需要从共享数据区中去获取数据,就不再需要关心生产者的行为。但是,这个共享数据区域中应该具备这样的线程间并发协作的功能:
1.如果共享数据区已满的话,阻塞生产者继续生产数据放置入内;
2.如果共享数据区为空的话,阻塞消费者继续消费数据。
在Java语言中,实现生产者消费者问题时,可以采用三种方式:
1.使用 Object 的 wait/notify 的消息通知机制;
2.使用 Lock 的 Condition 的 await/signal 的消息通知机制;
3.使用 BlockingQueue 实现。
1.如果共享数据区已满的话,阻塞生产者继续生产数据放置入内;
2.如果共享数据区为空的话,阻塞消费者继续消费数据。
在Java语言中,实现生产者消费者问题时,可以采用三种方式:
1.使用 Object 的 wait/notify 的消息通知机制;
2.使用 Lock 的 Condition 的 await/signal 的消息通知机制;
3.使用 BlockingQueue 实现。
消息队列如何保证顺序消费?
在生产中经常会有一些类似报表系统这样的系统,需要做 MySQL 的 binlog 同步。比如订单系统要同步订单表的数据到大数据部门的 MySQL 库中用于报表统计分析,通常的做法是基于 Canal 这样的中间件去监听订单数据库的 binlog,然后把这些 binlog 发送到 MQ 中,再由消费者从 MQ 中获取 binlog 落地到大数据部门的 MySQL 中。
在这个过程中,可能会有对某个订单的增删改操作,比如有三条 binlog 执行顺序是增加、修改、删除。消费者愣是换了顺序给执行成删除、修改、增加,这样能行吗?肯定是不行的。不同的消息队列产品,产生消息错乱的原因,以及解决方案是不同的。
在这个过程中,可能会有对某个订单的增删改操作,比如有三条 binlog 执行顺序是增加、修改、删除。消费者愣是换了顺序给执行成删除、修改、增加,这样能行吗?肯定是不行的。不同的消息队列产品,产生消息错乱的原因,以及解决方案是不同的。
那你们使用什么mq?基于什么做的选型?
RocketMq
由于我们系统的qps压力比较大,所以性能是首要考虑的要素。
开发语言,由于我们的开发语言是java,主要是为了方便二次开发。
对于高并发的业务场景是必须的,所以需要支持分布式架构的设计。
功能全面,由于不同的业务场景,可能会用到顺序消息、事务消息等。
由于我们系统的qps压力比较大,所以性能是首要考虑的要素。
开发语言,由于我们的开发语言是java,主要是为了方便二次开发。
对于高并发的业务场景是必须的,所以需要支持分布式架构的设计。
功能全面,由于不同的业务场景,可能会用到顺序消息、事务消息等。
消息队列如何保证消息不丢?
消息丢失可能发生在生产者发送消息、MQ本身丢失消息、消费者丢失消息3个方面。
RabbitMQ:
RabbitMQ丢失消息分为如下几种情况:
生产者丢消息:
生产者将数据发送到RabbitMQ的时候,可能在传输过程中因为网络等问题而将数据弄丢了。
RabbitMQ自己丢消息:
如果没有开启RabbitMQ的持久化,那么RabbitMQ一旦重启数据就丢了。所以必须开启持久化将消息持久化到磁盘,这样就算RabbitMQ挂了,恢复之后会自动读取之前存储的数据,一般数据不会丢失。除非极其罕见的情况,RabbitMQ还没来得及持久化自己就挂了,这样可能导致一部分数据丢失。
消费端丢消息:
主要是因为消费者消费时,刚消费到还没有处理,结果消费者就挂了,这样你重启之后,RabbitMQ就认为你已经消费过了,然后就丢了数据。
生产者丢消息:
生产者将数据发送到RabbitMQ的时候,可能在传输过程中因为网络等问题而将数据弄丢了。
RabbitMQ自己丢消息:
如果没有开启RabbitMQ的持久化,那么RabbitMQ一旦重启数据就丢了。所以必须开启持久化将消息持久化到磁盘,这样就算RabbitMQ挂了,恢复之后会自动读取之前存储的数据,一般数据不会丢失。除非极其罕见的情况,RabbitMQ还没来得及持久化自己就挂了,这样可能导致一部分数据丢失。
消费端丢消息:
主要是因为消费者消费时,刚消费到还没有处理,结果消费者就挂了,这样你重启之后,RabbitMQ就认为你已经消费过了,然后就丢了数据。
针对上述三种情况,RabbitMQ可以采用如下方式避免消息丢失:
生产者丢消息:
可以选择使用RabbitMQ提供是事务功能,就是生产者在发送数据之前开启事务,然后发送消息,如果消息没有成功被RabbitMQ接收到,那么生产者会受到异常报错,这时就可以回滚事务,然后尝试重新发送。如果收到了消息,那么就可以提交事务。这种方式有明显的缺点,即RabbitMQ事务开启后,就会变为同步阻塞操作,生产者会阻塞等待是否发送成功,太耗性能会造成吞吐量的下降。
可以开启confirm模式。在生产者那里设置开启了confirm模式之后,每次写的消息都会分配一个唯一的id,然后如何写入了RabbitMQ之中,RabbitMQ会给你回传一个ack消息,告诉你这个消息发送OK了。如果RabbitMQ没能处理这个消息,会回调你一个nack接口,告诉你这个消息失败了,你可以进行重试。而且你可以结合这个机制知道自己在内存里维护每个消息的id,如果超过一定时间还没接收到这个消息的回调,那么你可以进行重发。
事务机制是同步的,你提交了一个事物之后会阻塞住,但是confirm机制是异步的,发送消息之后可以接着发送下一个消息,然后RabbitMQ会回调告知成功与否。 一般在生产者这块避免丢失,都是用confirm机制。
RabbitMQ自己丢消息:
设置消息持久化到磁盘,设置持久化有两个步骤:
创建queue的时候将其设置为持久化的,这样就可以保证RabbitMQ持久化queue的元数据,但是不会持久化queue里面的数据。
发送消息的时候讲消息的deliveryMode设置为2,这样消息就会被设为持久化方式,此时RabbitMQ就会将消息持久化到磁盘上。 必须要同时开启这两个才可以。
而且持久化可以跟生产的confirm机制配合起来,只有消息持久化到了磁盘之后,才会通知生产者ack,这样就算是在持久化之前RabbitMQ挂了,数据丢了,生产者收不到ack回调也会进行消息重发。
消费端丢消息:
使用RabbitMQ提供的ack机制,首先关闭RabbitMQ的自动ack,然后每次在确保处理完这个消息之后,在代码里手动调用ack。这样就可以避免消息还没有处理完就ack。
生产者丢消息:
可以选择使用RabbitMQ提供是事务功能,就是生产者在发送数据之前开启事务,然后发送消息,如果消息没有成功被RabbitMQ接收到,那么生产者会受到异常报错,这时就可以回滚事务,然后尝试重新发送。如果收到了消息,那么就可以提交事务。这种方式有明显的缺点,即RabbitMQ事务开启后,就会变为同步阻塞操作,生产者会阻塞等待是否发送成功,太耗性能会造成吞吐量的下降。
可以开启confirm模式。在生产者那里设置开启了confirm模式之后,每次写的消息都会分配一个唯一的id,然后如何写入了RabbitMQ之中,RabbitMQ会给你回传一个ack消息,告诉你这个消息发送OK了。如果RabbitMQ没能处理这个消息,会回调你一个nack接口,告诉你这个消息失败了,你可以进行重试。而且你可以结合这个机制知道自己在内存里维护每个消息的id,如果超过一定时间还没接收到这个消息的回调,那么你可以进行重发。
事务机制是同步的,你提交了一个事物之后会阻塞住,但是confirm机制是异步的,发送消息之后可以接着发送下一个消息,然后RabbitMQ会回调告知成功与否。 一般在生产者这块避免丢失,都是用confirm机制。
RabbitMQ自己丢消息:
设置消息持久化到磁盘,设置持久化有两个步骤:
创建queue的时候将其设置为持久化的,这样就可以保证RabbitMQ持久化queue的元数据,但是不会持久化queue里面的数据。
发送消息的时候讲消息的deliveryMode设置为2,这样消息就会被设为持久化方式,此时RabbitMQ就会将消息持久化到磁盘上。 必须要同时开启这两个才可以。
而且持久化可以跟生产的confirm机制配合起来,只有消息持久化到了磁盘之后,才会通知生产者ack,这样就算是在持久化之前RabbitMQ挂了,数据丢了,生产者收不到ack回调也会进行消息重发。
消费端丢消息:
使用RabbitMQ提供的ack机制,首先关闭RabbitMQ的自动ack,然后每次在确保处理完这个消息之后,在代码里手动调用ack。这样就可以避免消息还没有处理完就ack。
Kafka:
Kafka丢失消息分为如下几种情况:
生产者丢消息:
生产者没有设置相应的策略,发送过程中丢失数据。
Kafka自己丢消息:
比较常见的一个场景,就是Kafka的某个broker宕机了,然后重新选举partition的leader时。如果此时follower还没来得及同步数据,leader就挂了,然后某个follower成为了leader,它就少了一部分数据。
消费端丢消息:
消费者消费到了这个数据,然后消费之自动提交了offset,让Kafka知道你已经消费了这个消息,当你准备处理这个消息时,自己挂掉了,那么这条消息就丢了。
生产者丢消息:
生产者没有设置相应的策略,发送过程中丢失数据。
Kafka自己丢消息:
比较常见的一个场景,就是Kafka的某个broker宕机了,然后重新选举partition的leader时。如果此时follower还没来得及同步数据,leader就挂了,然后某个follower成为了leader,它就少了一部分数据。
消费端丢消息:
消费者消费到了这个数据,然后消费之自动提交了offset,让Kafka知道你已经消费了这个消息,当你准备处理这个消息时,自己挂掉了,那么这条消息就丢了。
你说到消费者消费失败的问题,那么如果一直消费失败导致消息积压怎么处理?
因为考虑到时消费者消费⼀直出错的问题,那么我们可以从以下⼏个⻆度来考虑:
1. 消费者出错,肯定是程序或者其他问题导致的,如果容易修复,先把问题修复,让consumer恢复
正常消费
2. 如果时间来不及处理很麻烦,做转发处理,写⼀个临时的consumer消费⽅案,先把消息消费,然
后再转发到⼀个新的topic和MQ资源,这个新的topic的机器资源单独申请,要能承载住当前积压的
消息
3. 处理完积压数据后,修复consumer,去消费新的MQ和现有的MQ数据,新MQ消费完成后恢复原
状
1. 消费者出错,肯定是程序或者其他问题导致的,如果容易修复,先把问题修复,让consumer恢复
正常消费
2. 如果时间来不及处理很麻烦,做转发处理,写⼀个临时的consumer消费⽅案,先把消息消费,然
后再转发到⼀个新的topic和MQ资源,这个新的topic的机器资源单独申请,要能承载住当前积压的
消息
3. 处理完积压数据后,修复consumer,去消费新的MQ和现有的MQ数据,新MQ消费完成后恢复原
状
那如果消息积压达到磁盘上限,消息被删除了怎么办?
最初,我们发送的消息记录是落库保存了的,⽽转发发送的数据也保存了,那么我们就可以通过这部分
数据来找到丢失的那部分数据,再单独跑个脚本重发就可以了。如果转发的程序没有落库,那就和消费
⽅的记录去做对⽐,只是过程会更艰难⼀点。
数据来找到丢失的那部分数据,再单独跑个脚本重发就可以了。如果转发的程序没有落库,那就和消费
⽅的记录去做对⽐,只是过程会更艰难⼀点。
说了这么多,那你说说RocketMQ实现原理吧?
RocketMQ由NameServer注册中心集群、Producer生产者集群、Consumer消费者集群和若干Broker(RocketMQ进程)组成,它的架构原理是这样的:
Broker在启动的时候去向所有的NameServer注册,并保持长连接,每30s发送一次心跳
Producer在发送消息的时候从NameServer获取Broker服务器地址,根据负载均衡算法选择一台服务器来发送消息
Conusmer消费消息的时候同样从NameServer获取Broker地址,然后主动拉取消息来消费
Broker在启动的时候去向所有的NameServer注册,并保持长连接,每30s发送一次心跳
Producer在发送消息的时候从NameServer获取Broker服务器地址,根据负载均衡算法选择一台服务器来发送消息
Conusmer消费消息的时候同样从NameServer获取Broker地址,然后主动拉取消息来消费
为什么RocketMQ不使用Zookeeper作为注册中心呢?
我认为有以下⼏个点是不使⽤zookeeper的原因:
1. 根据CAP理论,同时最多只能满⾜两个点,⽽zookeeper满⾜的是CP,也就是说zookeeper并不能
保证服务的可⽤性,zookeeper在进⾏选举的时候,整个选举的时间太⻓,期间整个集群都处于不
可⽤的状态,⽽这对于⼀个注册中⼼来说肯定是不能接受的,作为服务发现来说就应该是为可⽤性
⽽设计。
2. 基于性能的考虑,NameServer本身的实现⾮常轻量,⽽且可以通过增加机器的⽅式⽔平扩展,增
加集群的抗压能⼒,⽽zookeeper的写是不可扩展的,⽽zookeeper要解决这个问题只能通过划分
领域,划分多个zookeeper集群来解决,⾸先操作起来太复杂,其次这样还是⼜违反了CAP中的A
的设计,导致服务之间是不连通的。
3. 持久化的机制来带的问题,ZooKeeper 的 ZAB 协议对每⼀个写请求,会在每个 ZooKeeper 节点上
保持写⼀个事务⽇志,同时再加上定期的将内存数据镜像(Snapshot)到磁盘来保证数据的⼀致性
和持久性,⽽对于⼀个简单的服务发现的场景来说,这其实没有太⼤的必要,这个实现⽅案太重
了。⽽且本身存储的数据应该是⾼度定制化的。
4. 消息发送应该弱依赖注册中⼼,⽽RocketMQ的设计理念也正是基于此,⽣产者在第⼀次发送消息
的时候从NameServer获取到Broker地址后缓存到本地,如果NameServer整个集群不可⽤,短时
间内对于⽣产者和消费者并不会产⽣太⼤影响。
1. 根据CAP理论,同时最多只能满⾜两个点,⽽zookeeper满⾜的是CP,也就是说zookeeper并不能
保证服务的可⽤性,zookeeper在进⾏选举的时候,整个选举的时间太⻓,期间整个集群都处于不
可⽤的状态,⽽这对于⼀个注册中⼼来说肯定是不能接受的,作为服务发现来说就应该是为可⽤性
⽽设计。
2. 基于性能的考虑,NameServer本身的实现⾮常轻量,⽽且可以通过增加机器的⽅式⽔平扩展,增
加集群的抗压能⼒,⽽zookeeper的写是不可扩展的,⽽zookeeper要解决这个问题只能通过划分
领域,划分多个zookeeper集群来解决,⾸先操作起来太复杂,其次这样还是⼜违反了CAP中的A
的设计,导致服务之间是不连通的。
3. 持久化的机制来带的问题,ZooKeeper 的 ZAB 协议对每⼀个写请求,会在每个 ZooKeeper 节点上
保持写⼀个事务⽇志,同时再加上定期的将内存数据镜像(Snapshot)到磁盘来保证数据的⼀致性
和持久性,⽽对于⼀个简单的服务发现的场景来说,这其实没有太⼤的必要,这个实现⽅案太重
了。⽽且本身存储的数据应该是⾼度定制化的。
4. 消息发送应该弱依赖注册中⼼,⽽RocketMQ的设计理念也正是基于此,⽣产者在第⼀次发送消息
的时候从NameServer获取到Broker地址后缓存到本地,如果NameServer整个集群不可⽤,短时
间内对于⽣产者和消费者并不会产⽣太⼤影响。
Master和Slave之间是怎么同步数据的呢?
消息在master和slave之间的同步是根据raft协议来进⾏的:
1. 在broker收到消息后,会被标记为uncommitted状态
2. 然后会把消息发送给所有的slave
3. slave在收到消息之后返回ack响应给master
4. master在收到超过半数的ack之后,把消息标记为committed
5. 发送committed消息给所有slave,slave也修改状态为committed
1. 在broker收到消息后,会被标记为uncommitted状态
2. 然后会把消息发送给所有的slave
3. slave在收到消息之后返回ack响应给master
4. master在收到超过半数的ack之后,把消息标记为committed
5. 发送committed消息给所有slave,slave也修改状态为committed
你知道RocketMQ为什么速度快吗?
是因为使⽤了顺序存储、Page Cache和异步刷盘。
1. 我们在写⼊commitlog的时候是顺序写⼊的,这样⽐随机写⼊的性能就会提⾼很多
2. 写⼊commitlog的时候并不是直接写⼊磁盘,⽽是先写⼊操作系统的PageCache
3. 最后由操作系统异步将缓存中的数据刷到磁盘
1. 我们在写⼊commitlog的时候是顺序写⼊的,这样⽐随机写⼊的性能就会提⾼很多
2. 写⼊commitlog的时候并不是直接写⼊磁盘,⽽是先写⼊操作系统的PageCache
3. 最后由操作系统异步将缓存中的数据刷到磁盘
什么是事务、半事务消息?怎么实现的?
事务消息就是MQ提供的类似XA的分布式事务能⼒,通过事务消息可以达到分布式事务的最终⼀致性。
半事务消息就是MQ收到了⽣产者的消息,但是没有收到⼆次确认,不能投递的消息。
实现原理如下:
1. ⽣产者先发送⼀条半事务消息到MQ
2. MQ收到消息后返回ack确认
3. ⽣产者开始执⾏本地事务
4. 如果事务执⾏成功发送commit到MQ,失败发送rollback
5. 如果MQ⻓时间未收到⽣产者的⼆次确认commit或者rollback,MQ对⽣产者发起消息回查
6. ⽣产者查询事务执⾏最终状态
7. 根据查询事务状态再次提交⼆次确认
最终,如果MQ收到⼆次确认commit,就可以把消息投递给消费者,反之如果是rollback,消息会保存
下来并且在3天后被删除。
半事务消息就是MQ收到了⽣产者的消息,但是没有收到⼆次确认,不能投递的消息。
实现原理如下:
1. ⽣产者先发送⼀条半事务消息到MQ
2. MQ收到消息后返回ack确认
3. ⽣产者开始执⾏本地事务
4. 如果事务执⾏成功发送commit到MQ,失败发送rollback
5. 如果MQ⻓时间未收到⽣产者的⼆次确认commit或者rollback,MQ对⽣产者发起消息回查
6. ⽣产者查询事务执⾏最终状态
7. 根据查询事务状态再次提交⼆次确认
最终,如果MQ收到⼆次确认commit,就可以把消息投递给消费者,反之如果是rollback,消息会保存
下来并且在3天后被删除。
简述Kafka的架构设计?
总的来说,Kafka是分为三个角色:Producer、Kafka集群以及Consumer,生产者将消息发送到Kafka集群,然后消费者再去Kafka集群进行消息的消费
Consumer Group:消费者组,消费者组内每个消费者负责消费不同分区的数据,提高消费能力。逻
辑上的一个订阅者。
Topic:可以理解为一个队列,Topic 将消息分类,生产者和消费者面向的是同一个 Topic。
Partition:为了实现扩展性,提高并发能力,一个Topic 以多个Partition的方式分布到多个 Broker
上,每个 Partition 是一个 有序的队列。一个 Topic 的每个Partition都有若干个副本(Replica),一个
Leader 和若干个 Follower。生产者发送数据的对象,以及消费者消费数据的对象,都是 Leader。
Follower负责实时从 Leader 中同步数据,保持和 Leader 数据的同步。Leader 发生故障时,某个
Follower 还会成为新的 Leader。
Offset:消费者消费的位置信息,监控数据消费到什么位置,当消费者挂掉再重新恢复的时候,可以从
消费位置继续消费。
Zookeeper:Kafka 集群能够正常工作,需要依赖于 Zookeeper,Zookeeper 帮助 Kafka 存储和管理
集群信息。
辑上的一个订阅者。
Topic:可以理解为一个队列,Topic 将消息分类,生产者和消费者面向的是同一个 Topic。
Partition:为了实现扩展性,提高并发能力,一个Topic 以多个Partition的方式分布到多个 Broker
上,每个 Partition 是一个 有序的队列。一个 Topic 的每个Partition都有若干个副本(Replica),一个
Leader 和若干个 Follower。生产者发送数据的对象,以及消费者消费数据的对象,都是 Leader。
Follower负责实时从 Leader 中同步数据,保持和 Leader 数据的同步。Leader 发生故障时,某个
Follower 还会成为新的 Leader。
Offset:消费者消费的位置信息,监控数据消费到什么位置,当消费者挂掉再重新恢复的时候,可以从
消费位置继续消费。
Zookeeper:Kafka 集群能够正常工作,需要依赖于 Zookeeper,Zookeeper 帮助 Kafka 存储和管理
集群信息。
Kafka什么时候会出现消息丢失以及解决方案?
丢失的情况:
生产者
消费者
broker
生产者
消费者
broker
生产者:
1、ack=0,不重试 producer发送消息完,不管结果了,如果发送失败也就丢失了。
2、ack=1,leader crash producer发送消息完,只等待lead写入成功就返回了,leader crash了,这时follower没来及同步,消 息丢失。
3、unclean.leader.election.enable 配置true 允许选举ISR以外的副本作为leader,会导致数据丢失,默认为false。producer发送异步消息完,只等待 lead写入成功就返回了,leader crash了,这时ISR中没有follower,leader从OSR中选举,因为OSR 中本来落后于Leader造成消息丢失。
解决方案:
1、配置:ack=all / -1,tries > 1,unclean.leader.election.enable : false producer发送消息完,等待follower同步完再返回,如果异常则重试。副本的数量可能影响吞吐量。 不允许选举ISR以外的副本作为leader。
2、配置:min.insync.replicas > 1 副本指定必须确认写操作成功的最小副本数量。如果不能满足这个最小值,则生产者将引发一个异常(要么是 NotEnoughReplicas,要么是NotEnoughReplicasAfterAppend)。 min.insync.replicas和ack更大的持久性保证。确保如果大多数副本没有收到写操作,则生产者将引发异 常。
3、失败的offset单独记录 producer发送消息,会自动重试,遇到不可恢复异常会抛出,这时可以捕获异常记录到数据库或缓存,进行 单独处理。
1、ack=0,不重试 producer发送消息完,不管结果了,如果发送失败也就丢失了。
2、ack=1,leader crash producer发送消息完,只等待lead写入成功就返回了,leader crash了,这时follower没来及同步,消 息丢失。
3、unclean.leader.election.enable 配置true 允许选举ISR以外的副本作为leader,会导致数据丢失,默认为false。producer发送异步消息完,只等待 lead写入成功就返回了,leader crash了,这时ISR中没有follower,leader从OSR中选举,因为OSR 中本来落后于Leader造成消息丢失。
解决方案:
1、配置:ack=all / -1,tries > 1,unclean.leader.election.enable : false producer发送消息完,等待follower同步完再返回,如果异常则重试。副本的数量可能影响吞吐量。 不允许选举ISR以外的副本作为leader。
2、配置:min.insync.replicas > 1 副本指定必须确认写操作成功的最小副本数量。如果不能满足这个最小值,则生产者将引发一个异常(要么是 NotEnoughReplicas,要么是NotEnoughReplicasAfterAppend)。 min.insync.replicas和ack更大的持久性保证。确保如果大多数副本没有收到写操作,则生产者将引发异 常。
3、失败的offset单独记录 producer发送消息,会自动重试,遇到不可恢复异常会抛出,这时可以捕获异常记录到数据库或缓存,进行 单独处理。
消费者:
自动commit;
解决方案:
手动commit
自动commit;
解决方案:
手动commit
broker:
刷盘
解决方案:
减小刷盘间隔
刷盘
解决方案:
减小刷盘间隔
Kafka是pull还是push?
pull,默认一次最多500条
Kafka中zk的作用?
/brokers/ids:临时节点,保存所有broker节点信息,存储broker的物理地址、版本信息、启动时间
等,节点名称为brokerID,broker定时发送心跳到zk,如果断开则该brokerID会被删除
/brokers/topics:临时节点,节点保存broker节点下所有的topic信息,每一个topic节点下包含一个固
定的partitions节点,partitions的子节点就是topic的分区,每个分区下保存一个state节点、保存着当
前leader分区和ISR的brokerID,state节点由leader创建,若leader宕机该节点会被删除,直到有新的
leader选举产生、重新生成state节点
/consumers/[group_id]/owners/[topic]/[broker_id-partition_id]:维护消费者和分区的注册关系
/consumers/[group_id]/offsets/[topic]/[broker_id-partition_id]:分区消息的消费进度Offset
client通过topic找到topic树下的state节点、获取leader的brokerID,到broker树中找到broker的物理
地址,但是client不会直连zk,而是通过配置的broker获取到zk中的信息
等,节点名称为brokerID,broker定时发送心跳到zk,如果断开则该brokerID会被删除
/brokers/topics:临时节点,节点保存broker节点下所有的topic信息,每一个topic节点下包含一个固
定的partitions节点,partitions的子节点就是topic的分区,每个分区下保存一个state节点、保存着当
前leader分区和ISR的brokerID,state节点由leader创建,若leader宕机该节点会被删除,直到有新的
leader选举产生、重新生成state节点
/consumers/[group_id]/owners/[topic]/[broker_id-partition_id]:维护消费者和分区的注册关系
/consumers/[group_id]/offsets/[topic]/[broker_id-partition_id]:分区消息的消费进度Offset
client通过topic找到topic树下的state节点、获取leader的brokerID,到broker树中找到broker的物理
地址,但是client不会直连zk,而是通过配置的broker获取到zk中的信息
简单的说就是zk负责存储broker信息,topic信息,partitions信息,消费者和分区的注册关系,分区的消费进度
Kafka的reblance机制?
rebalance(重平衡)其实就是重新进行 partition 的分配,从而使得 partition 的分配重新达到平衡状态
触发条件:
当消费者组内的消费者数量发生变化(增加或者减少),就会产生重新分配patition
分区数量发生变化时(即 topic 的分区数量发生变化时)
当消费者组内的消费者数量发生变化(增加或者减少),就会产生重新分配patition
分区数量发生变化时(即 topic 的分区数量发生变化时)
Kafka的性能好在哪里?
kafka不基于内存,而是硬盘存储,因此消息堆积能力更强
顺序写:利用磁盘的顺序访问速度可以接近内存,kafka的消息都是append操作,partition是有序的,
节省了磁盘的寻道时间,同时通过批量操作、节省写入次数,partition物理上分为多个segment存储,
方便删除
节省了磁盘的寻道时间,同时通过批量操作、节省写入次数,partition物理上分为多个segment存储,
方便删除
零拷贝:
直接将内核缓冲区的数据发送到网卡传输
使用的是操作系统的指令支持
直接将内核缓冲区的数据发送到网卡传输
使用的是操作系统的指令支持
对应零拷贝技术有mmap及sendfile
mmap:小文件传输快
sendfile:大文件传输比mmap快
mmap:小文件传输快
sendfile:大文件传输比mmap快
kafka不太依赖jvm,主要理由操作系统的pageCache,如果生产消费速率相当,则直接用pageCache
交换数据,不需要经过磁盘IO
交换数据,不需要经过磁盘IO
Kafka如何应对大量的客户端连接?
Reactor多路复用
简单来说,就是搞⼀个 acceptor 线程,基于底层操作系统的⽀持,实现连接请求监听。 如果有某个设备发送了建⽴连接的请求过来,那么那个线程就把这个建⽴好的连接交给 processor 线程。 每个 processor 线程会被分配 N 多个连接,⼀个线程就可以负责维持 N 多个连接,他同样会基 于底层操作系统的⽀持监听 N 多连接的请求。 如果某个连接发送了请求过来,那么这个 processor 线程就会把请求放到⼀个请求队列⾥去。 接着后台有⼀个线程池,这个线程池⾥有⼯作线程,会从请求队列⾥获取请求,处理请求,接 着将请求对应的响应放到每个 processor 线程对应的⼀个响应队列⾥去。 最后,processor 线程会把⾃⼰的响应队列⾥的响应发送回给客户端。
Kafka的ISR机制是什么?
ISR 全称是 “In-Sync Replicas”,也就是保持同步的副本,他的含义就是,跟 Leader 始终保持同 步的 Follower 有哪些
Kafka与生产者的网络通信优化?
batch机制:多条消息打包成一个batch(默认16kb的大小)
request机制:多个batch打包成一个request
分布式
分布式幂等性如何设计?
在高并发场景的架构里,幂等性是必须得保证的。比如说支付功能,用户发起支付,如果后台没有
做幂等校验,刚好用户手抖多点了几下,于是后台就可能多次受到同一个订单请求,不做幂等很容
易就让用户重复支付了,这样用户是肯定不能忍的。
做幂等校验,刚好用户手抖多点了几下,于是后台就可能多次受到同一个订单请求,不做幂等很容
易就让用户重复支付了,这样用户是肯定不能忍的。
解决方案:
1,查询和删除不在幂等讨论范围,查询肯定没有幂等的说,删除:第一次删除成功后,后面来删
除直接返回0,也是返回成功。
2,建唯一索引:唯一索引或唯一组合索引来防止新增数据存在脏数据 (当表存在唯一索引,并发
时新增异常时,再查询一次就可以了,数据应该已经存在了,返回结果即可)。
3,token机制:由于重复点击或者网络重发,或者nginx重发等情况会导致数据被重复提交。前端
在数据提交前要向后端服务的申请token,token放到 Redis 或 JVM 内存,token有效时间。提交后
后台校验token,同时删除token,生成新的token返回。redis要用删除操作来判断token,删除成
功代表token校验通过,如果用select+delete来校验token,存在并发问题,不建议使用。
4,悲观锁
悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选用(另外还要考
虑id是否为主键,如果id不是主键或者不是 InnoDB 存储引擎,那么就会出现锁全表)。
select id ,name from table_# where id='##' for update;
5,乐观锁,给数据库表增加一个version字段,可以通过这个字段来判断是否已经被修改了
update table_xxx set name=#name#,version=version+1 where version=#version#
6,分布式锁,比如 Redis 、 Zookeeper 的分布式锁。单号为key,然后给Key设置有效期(防止支
付失败后,锁一直不释放),来一个请求使用订单号生成一把锁,业务代码执行完成后再释放锁。
7,保底方案,先查询是否存在此单,不存在进行支付,存在就直接返回支付结果。
1,查询和删除不在幂等讨论范围,查询肯定没有幂等的说,删除:第一次删除成功后,后面来删
除直接返回0,也是返回成功。
2,建唯一索引:唯一索引或唯一组合索引来防止新增数据存在脏数据 (当表存在唯一索引,并发
时新增异常时,再查询一次就可以了,数据应该已经存在了,返回结果即可)。
3,token机制:由于重复点击或者网络重发,或者nginx重发等情况会导致数据被重复提交。前端
在数据提交前要向后端服务的申请token,token放到 Redis 或 JVM 内存,token有效时间。提交后
后台校验token,同时删除token,生成新的token返回。redis要用删除操作来判断token,删除成
功代表token校验通过,如果用select+delete来校验token,存在并发问题,不建议使用。
4,悲观锁
悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选用(另外还要考
虑id是否为主键,如果id不是主键或者不是 InnoDB 存储引擎,那么就会出现锁全表)。
select id ,name from table_# where id='##' for update;
5,乐观锁,给数据库表增加一个version字段,可以通过这个字段来判断是否已经被修改了
update table_xxx set name=#name#,version=version+1 where version=#version#
6,分布式锁,比如 Redis 、 Zookeeper 的分布式锁。单号为key,然后给Key设置有效期(防止支
付失败后,锁一直不释放),来一个请求使用订单号生成一把锁,业务代码执行完成后再释放锁。
7,保底方案,先查询是否存在此单,不存在进行支付,存在就直接返回支付结果。
简单一次完整的 HTTP 请求所经历的步骤?
1、 DNS 解析(通过访问的域名找出其 IP 地址,递归搜索)。 2、HTTP 请求,当输入一个请求时,建立一个 Socket 连接发起 TCP的 3 次握手。
如果是 HTTPS 请求,会略微有不同。等到 HTTPS 小节,我们在来讲。
3.1、客户端向服务器发送请求命令(一般是 GET 或 POST 请求)。
这个是补充内容,面试一般不用回答。
客户端的网络层不用关心应用层或者传输层的东西,主要做的是通过查找路由表确定如何
到达服务器,期间可能经过多个路由器,这些都是由路由器来完成的工作,我不作过多的
描述,无非就是通过查找路由表决定通过那个路径到达服务器。
客户端的链路层,包通过链路层发送到路由器,通过邻居协议查找给定 IP 地址的 MAC 地
址,然后发送 ARP 请求查找目的地址,如果得到回应后就可以使用 ARP 的请求应答交换
的 IP 数据包现在就可以传输了,然后发送 IP 数据包到达服务器的地址。
3.2、客户端发送请求头信息和数据。
4.1、服务器发送应答头信息。
4.2、服务器向客户端发送数据。
5、服务器关闭 TCP 连接(4次挥手)。
这里是否关闭 TCP 连接,也根据 HTTP Keep-Alive 机制有关。
同时,客户端也可以主动发起关闭 TCP 连接。
6、客户端根据返回的 HTML 、 CSS 、 JS 进行渲染。
如果是 HTTPS 请求,会略微有不同。等到 HTTPS 小节,我们在来讲。
3.1、客户端向服务器发送请求命令(一般是 GET 或 POST 请求)。
这个是补充内容,面试一般不用回答。
客户端的网络层不用关心应用层或者传输层的东西,主要做的是通过查找路由表确定如何
到达服务器,期间可能经过多个路由器,这些都是由路由器来完成的工作,我不作过多的
描述,无非就是通过查找路由表决定通过那个路径到达服务器。
客户端的链路层,包通过链路层发送到路由器,通过邻居协议查找给定 IP 地址的 MAC 地
址,然后发送 ARP 请求查找目的地址,如果得到回应后就可以使用 ARP 的请求应答交换
的 IP 数据包现在就可以传输了,然后发送 IP 数据包到达服务器的地址。
3.2、客户端发送请求头信息和数据。
4.1、服务器发送应答头信息。
4.2、服务器向客户端发送数据。
5、服务器关闭 TCP 连接(4次挥手)。
这里是否关闭 TCP 连接,也根据 HTTP Keep-Alive 机制有关。
同时,客户端也可以主动发起关闭 TCP 连接。
6、客户端根据返回的 HTML 、 CSS 、 JS 进行渲染。
说说你对分布式事务的了解
ACID
指数据库事务正确执行的四个基本要素:
1. 原子性(Atomicity) 2. 一致性(Consistency) 3. 隔离性(Isolation) 4. 持久性(Durability)
1. 原子性(Atomicity) 2. 一致性(Consistency) 3. 隔离性(Isolation) 4. 持久性(Durability)
CAP
CAP原则又称CAP定理,指的是在一个分布式系统中,一致性(Consistency)、可用性
(Availability)、分区容忍性(Partition tolerance)。CAP 原则指的是,这三个要素最多只能同
时实现两点,不可能三者兼顾。
一致性:在分布式系统中的所有数据备份,在同一时刻是否同样的值。
可用性:在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。
分区容忍性:以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数
据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。
(Availability)、分区容忍性(Partition tolerance)。CAP 原则指的是,这三个要素最多只能同
时实现两点,不可能三者兼顾。
一致性:在分布式系统中的所有数据备份,在同一时刻是否同样的值。
可用性:在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。
分区容忍性:以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数
据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。
BASE理论
BASE理论是对CAP中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到
强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。
Basically Available(基本可用)
Soft state(软状态)
Eventually consistent(最终一致性)
强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。
Basically Available(基本可用)
Soft state(软状态)
Eventually consistent(最终一致性)
你知道哪些分布式事务解决方案?
1. 两阶段提交(2PC)
2. 三阶段提交(3PC)
3. 补偿事务(TCC=Try-Confirm-Cancel)
4. 本地消息队列表(MQ)
1. 两阶段提交(2PC)
2. 三阶段提交(3PC)
3. 补偿事务(TCC=Try-Confirm-Cancel)
4. 本地消息队列表(MQ)
什么是二阶段提交?
两阶段提交2PC是分布式事务中最强大的事务类型之一,两段提交就是分两个阶段提交:
第一阶段询问各个事务数据源是否准备好。
第二阶段才真正将数据提交给事务数据源。
为了保证该事务可以满足ACID,就要引入一个协调者(Cooradinator)。其他的节点被称为参与者
(Participant)。协调者负责调度参与者的行为,并最终决定这些参与者是否要把事务进行提交。
第一阶段询问各个事务数据源是否准备好。
第二阶段才真正将数据提交给事务数据源。
为了保证该事务可以满足ACID,就要引入一个协调者(Cooradinator)。其他的节点被称为参与者
(Participant)。协调者负责调度参与者的行为,并最终决定这些参与者是否要把事务进行提交。
阶段一
a) 协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待答复。
b) 各参与者执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)。
c) 如参与者执行成功,给协调者反馈 yes,否则反馈 no。
阶段二
如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(rollback)消息;否则,
发送提交(commit)消息。两种情况处理如下:
情况1:当所有参与者均反馈 yes,提交事务
a) 协调者向所有参与者发出正式提交事务的请求(即 commit 请求)。
b) 参与者执行 commit 请求,并释放整个事务期间占用的资源。
c) 各参与者向协调者反馈 ack(应答)完成的消息。
d) 协调者收到所有参与者反馈的 ack 消息后,即完成事务提交。
情况2:当有一个参与者反馈 no,回滚事务
a) 协调者向所有参与者发出回滚请求(即 rollback 请求)。
b) 参与者使用阶段 1 中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源。
c) 各参与者向协调者反馈 ack 完成的消息。
d) 协调者收到所有参与者反馈的 ack 消息后,即完成事务。
a) 协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待答复。
b) 各参与者执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)。
c) 如参与者执行成功,给协调者反馈 yes,否则反馈 no。
阶段二
如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(rollback)消息;否则,
发送提交(commit)消息。两种情况处理如下:
情况1:当所有参与者均反馈 yes,提交事务
a) 协调者向所有参与者发出正式提交事务的请求(即 commit 请求)。
b) 参与者执行 commit 请求,并释放整个事务期间占用的资源。
c) 各参与者向协调者反馈 ack(应答)完成的消息。
d) 协调者收到所有参与者反馈的 ack 消息后,即完成事务提交。
情况2:当有一个参与者反馈 no,回滚事务
a) 协调者向所有参与者发出回滚请求(即 rollback 请求)。
b) 参与者使用阶段 1 中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源。
c) 各参与者向协调者反馈 ack 完成的消息。
d) 协调者收到所有参与者反馈的 ack 消息后,即完成事务。
问题
1) 性能问题:所有参与者在事务提交阶段处于同步阻塞状态,占用系统资源,容易导致性能瓶颈。
2) 可靠性问题:如果协调者存在单点故障问题,或出现故障,提供者将一直处于锁定状态。
3) 数据一致性问题:在阶段 2 中,如果出现协调者和参与者都挂了的情况,有可能导致数据不一
致。
优点:尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能100%保证
强一致)。
缺点:实现复杂,牺牲了可用性,对性能影响较大,不适合高并发高性能场景。
1) 性能问题:所有参与者在事务提交阶段处于同步阻塞状态,占用系统资源,容易导致性能瓶颈。
2) 可靠性问题:如果协调者存在单点故障问题,或出现故障,提供者将一直处于锁定状态。
3) 数据一致性问题:在阶段 2 中,如果出现协调者和参与者都挂了的情况,有可能导致数据不一
致。
优点:尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能100%保证
强一致)。
缺点:实现复杂,牺牲了可用性,对性能影响较大,不适合高并发高性能场景。
什么是三阶段提交?
三阶段提交是在二阶段提交上的改进版本,3PC最关键要解决的就是协调者和参与者同时挂掉的问
题,所以3PC把2PC的准备阶段再次一分为二,这样三阶段提交。
题,所以3PC把2PC的准备阶段再次一分为二,这样三阶段提交。
阶段一
a) 协调者向所有参与者发出包含事务内容的 canCommit 请求,询问是否可以提交事务,并等待所
有参与者答复。
b) 参与者收到 canCommit 请求后,如果认为可以执行事务操作,则反馈 yes 并进入预备状态,否
则反馈 no。
阶段二
协调者根据参与者响应情况,有以下两种可能。
情况1:所有参与者均反馈 yes,协调者预执行事务
a) 协调者向所有参与者发出 preCommit 请求,进入准备阶段。
b) 参与者收到 preCommit 请求后,执行事务操作,将 undo 和 redo 信息记入事务日志中(但不
提交事务)。
c) 各参与者向协调者反馈 ack 响应或 no 响应,并等待最终指令。
情况2:只要有一个参与者反馈 no,或者等待超时后协调者尚无法收到所有提供者的反馈,即中断
事务
a) 协调者向所有参与者发出 abort 请求。
b) 无论收到协调者发出的 abort 请求,或者在等待协调者请求过程中出现超时,参与者均会中断事
务。
阶段三
该阶段进行真正的事务提交,也可以分为以下两种情况。
情况 1:所有参与者均反馈 ack 响应,执行真正的事务提交
a) 如果协调者处于工作状态,则向所有参与者发出 do Commit 请求。
b) 参与者收到 do Commit 请求后,会正式执行事务提交,并释放整个事务期间占用的资源。
c) 各参与者向协调者反馈 ack 完成的消息。
d) 协调者收到所有参与者反馈的 ack 消息后,即完成事务提交。
情况2:只要有一个参与者反馈 no,或者等待超时后协调组尚无法收到所有提供者的反馈,即回滚
事务。
a) 如果协调者处于工作状态,向所有参与者发出 rollback 请求。
b) 参与者使用阶段 1 中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源。
c) 各参与者向协调组反馈 ack 完成的消息。
d) 协调组收到所有参与者反馈的 ack 消息后,即完成事务回滚。
优点:相比二阶段提交,三阶段提交降低了阻塞范围,在等待超时后协调者或参与者会中断事务。
避免了协调者单点问题。阶段 3 中协调者出现问题时,参与者会继续提交事务。
缺点:数据不一致问题依然存在,当在参与者收到 preCommit 请求后等待 do commite 指令时,
此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造
成数据不一致。
a) 协调者向所有参与者发出包含事务内容的 canCommit 请求,询问是否可以提交事务,并等待所
有参与者答复。
b) 参与者收到 canCommit 请求后,如果认为可以执行事务操作,则反馈 yes 并进入预备状态,否
则反馈 no。
阶段二
协调者根据参与者响应情况,有以下两种可能。
情况1:所有参与者均反馈 yes,协调者预执行事务
a) 协调者向所有参与者发出 preCommit 请求,进入准备阶段。
b) 参与者收到 preCommit 请求后,执行事务操作,将 undo 和 redo 信息记入事务日志中(但不
提交事务)。
c) 各参与者向协调者反馈 ack 响应或 no 响应,并等待最终指令。
情况2:只要有一个参与者反馈 no,或者等待超时后协调者尚无法收到所有提供者的反馈,即中断
事务
a) 协调者向所有参与者发出 abort 请求。
b) 无论收到协调者发出的 abort 请求,或者在等待协调者请求过程中出现超时,参与者均会中断事
务。
阶段三
该阶段进行真正的事务提交,也可以分为以下两种情况。
情况 1:所有参与者均反馈 ack 响应,执行真正的事务提交
a) 如果协调者处于工作状态,则向所有参与者发出 do Commit 请求。
b) 参与者收到 do Commit 请求后,会正式执行事务提交,并释放整个事务期间占用的资源。
c) 各参与者向协调者反馈 ack 完成的消息。
d) 协调者收到所有参与者反馈的 ack 消息后,即完成事务提交。
情况2:只要有一个参与者反馈 no,或者等待超时后协调组尚无法收到所有提供者的反馈,即回滚
事务。
a) 如果协调者处于工作状态,向所有参与者发出 rollback 请求。
b) 参与者使用阶段 1 中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源。
c) 各参与者向协调组反馈 ack 完成的消息。
d) 协调组收到所有参与者反馈的 ack 消息后,即完成事务回滚。
优点:相比二阶段提交,三阶段提交降低了阻塞范围,在等待超时后协调者或参与者会中断事务。
避免了协调者单点问题。阶段 3 中协调者出现问题时,参与者会继续提交事务。
缺点:数据不一致问题依然存在,当在参与者收到 preCommit 请求后等待 do commite 指令时,
此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造
成数据不一致。
什么是补偿事务?
TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补
偿(撤销)操作。
它分为三个步骤:
Try 阶段主要是对业务系统做检测及资源预留。
Confirm 阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默
认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。
举个例子,假入你要向 老田 转账,思路大概是: 我们有一个本地方法,里面依次调用步骤: 1、
首先在 Try 阶段,要先调用远程接口把 你 和 老田 的钱给冻结起来。 2、在 Confirm 阶段,执行远
程调用的转账的操作,转账成功进行解冻。 3、如果第2步执行成功,那么转账成功,如果第二步执
行失败,则调用远程冻结接口对应的解冻方法 (Cancel)。
优点:
性能提升:具体业务来实现控制资源锁的粒度变小,不会锁定整个资源。
数据最终一致性:基于 Confirm 和 Cancel 的幂等性,保证事务最终完成确认或者取消,保证数据
的一致性。
可靠性:解决了 XA 协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动
管理器也变成多点,引入集群。
缺点:TCC 的 Try、Confirm 和 Cancel 操作功能要按具体业务来实现,业务耦合度较高,提高了开
发成本。
偿(撤销)操作。
它分为三个步骤:
Try 阶段主要是对业务系统做检测及资源预留。
Confirm 阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默
认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。
举个例子,假入你要向 老田 转账,思路大概是: 我们有一个本地方法,里面依次调用步骤: 1、
首先在 Try 阶段,要先调用远程接口把 你 和 老田 的钱给冻结起来。 2、在 Confirm 阶段,执行远
程调用的转账的操作,转账成功进行解冻。 3、如果第2步执行成功,那么转账成功,如果第二步执
行失败,则调用远程冻结接口对应的解冻方法 (Cancel)。
优点:
性能提升:具体业务来实现控制资源锁的粒度变小,不会锁定整个资源。
数据最终一致性:基于 Confirm 和 Cancel 的幂等性,保证事务最终完成确认或者取消,保证数据
的一致性。
可靠性:解决了 XA 协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动
管理器也变成多点,引入集群。
缺点:TCC 的 Try、Confirm 和 Cancel 操作功能要按具体业务来实现,业务耦合度较高,提高了开
发成本。
分布式ID生成有几种方案?
分布式ID的特性
唯一性:确保生成的ID是全网唯一的。
有序递增性:确保生成的ID是对于某个用户或者业务是按一定的数字有序递增的。
高可用性:确保任何时候都能正确的生成ID。
带时间:ID里面包含时间,一眼扫过去就知道哪天的交易。
唯一性:确保生成的ID是全网唯一的。
有序递增性:确保生成的ID是对于某个用户或者业务是按一定的数字有序递增的。
高可用性:确保任何时候都能正确的生成ID。
带时间:ID里面包含时间,一眼扫过去就知道哪天的交易。
UUID
算法的核心思想是结合机器的网卡、当地时间、一个随记数来生成UUID。
优点:本地生成,生成简单,性能好,没有高可用风险
缺点:长度过长,存储冗余,且无序不可读,查询效率低
算法的核心思想是结合机器的网卡、当地时间、一个随记数来生成UUID。
优点:本地生成,生成简单,性能好,没有高可用风险
缺点:长度过长,存储冗余,且无序不可读,查询效率低
数据库自增ID
使用数据库的id自增策略,如 MySQL 的 auto_increment。并且可以使用两台数据库分别设置不同
步长,生成不重复ID的策略来实现高可用。
优点:数据库生成的ID绝对有序,高可用实现方式简单
缺点:需要独立部署数据库实例,成本高,有性能瓶颈
使用数据库的id自增策略,如 MySQL 的 auto_increment。并且可以使用两台数据库分别设置不同
步长,生成不重复ID的策略来实现高可用。
优点:数据库生成的ID绝对有序,高可用实现方式简单
缺点:需要独立部署数据库实例,成本高,有性能瓶颈
批量生成ID
一次按需批量生成多个ID,每次生成都需要访问数据库,将数据库修改为最大的ID值,并在内存中
记录当前值及最大值。
优点:避免了每次生成ID都要访问数据库并带来压力,提高性能
缺点:属于本地生成策略,存在单点故障,服务重启造成ID不连续
一次按需批量生成多个ID,每次生成都需要访问数据库,将数据库修改为最大的ID值,并在内存中
记录当前值及最大值。
优点:避免了每次生成ID都要访问数据库并带来压力,提高性能
缺点:属于本地生成策略,存在单点故障,服务重启造成ID不连续
Redis生成ID
Redis的所有命令操作都是单线程的,本身提供像 incr 和 increby 这样的自增原子命令,所以能保
证生成的 ID 肯定是唯一有序的。
优点:不依赖于数据库,灵活方便,且性能优于数据库;数字ID天然排序,对分页或者需要排
序的结果很有帮助。
缺点:如果系统中没有Redis,还需要引入新的组件,增加系统复杂度;需要编码和配置的工作
量比较大。
考虑到单节点的性能瓶颈,可以使用 Redis 集群来获取更高的吞吐量。假如一个集群中有5台
Redis。可以初始化每台 Redis 的值分别是1, 2, 3, 4, 5,然后步长都是 5。
Redis的所有命令操作都是单线程的,本身提供像 incr 和 increby 这样的自增原子命令,所以能保
证生成的 ID 肯定是唯一有序的。
优点:不依赖于数据库,灵活方便,且性能优于数据库;数字ID天然排序,对分页或者需要排
序的结果很有帮助。
缺点:如果系统中没有Redis,还需要引入新的组件,增加系统复杂度;需要编码和配置的工作
量比较大。
考虑到单节点的性能瓶颈,可以使用 Redis 集群来获取更高的吞吐量。假如一个集群中有5台
Redis。可以初始化每台 Redis 的值分别是1, 2, 3, 4, 5,然后步长都是 5。
Twitter的snowflake算法(重点) Twitter 利用 zookeeper 实现了一个全局ID生成的服务 Snowflake
如上图的所示,Twitter 的 Snowflake 算法由下面几部分组成:
1位符号位:
由于 long 类型在 java 中带符号的,最高位为符号位,正数为 0,负数为 1,且实际系统中所使用
的ID一般都是正数,所以最高位为 0。
41位时间戳(毫秒级):
需要注意的是此处的 41 位时间戳并非存储当前时间的时间戳,而是存储时间戳的差值(当前时间
戳 - 起始时间戳),这里的起始时间戳一般是ID生成器开始使用的时间戳,由程序来指定,所以41
位毫秒时间戳最多可以使用 (1 << 41) / (1000x60x60x24x365) = 69年 。
10位数据机器位:
包括5位数据标识位和5位机器标识位,这10位决定了分布式系统中最多可以部署 1 << 10 = 1024
s个节点。超过这个数量,生成的ID就有可能会冲突。
12位毫秒内的序列:
这 12 位计数支持每个节点每毫秒(同一台机器,同一时刻)最多生成 1 << 12 = 4096个ID
加起来刚好64位,为一个Long型。
优点:高性能,低延迟,按时间有序,一般不会造成ID碰撞
缺点:需要独立的开发和部署,依赖于机器的时钟
如上图的所示,Twitter 的 Snowflake 算法由下面几部分组成:
1位符号位:
由于 long 类型在 java 中带符号的,最高位为符号位,正数为 0,负数为 1,且实际系统中所使用
的ID一般都是正数,所以最高位为 0。
41位时间戳(毫秒级):
需要注意的是此处的 41 位时间戳并非存储当前时间的时间戳,而是存储时间戳的差值(当前时间
戳 - 起始时间戳),这里的起始时间戳一般是ID生成器开始使用的时间戳,由程序来指定,所以41
位毫秒时间戳最多可以使用 (1 << 41) / (1000x60x60x24x365) = 69年 。
10位数据机器位:
包括5位数据标识位和5位机器标识位,这10位决定了分布式系统中最多可以部署 1 << 10 = 1024
s个节点。超过这个数量,生成的ID就有可能会冲突。
12位毫秒内的序列:
这 12 位计数支持每个节点每毫秒(同一台机器,同一时刻)最多生成 1 << 12 = 4096个ID
加起来刚好64位,为一个Long型。
优点:高性能,低延迟,按时间有序,一般不会造成ID碰撞
缺点:需要独立的开发和部署,依赖于机器的时钟
常见负载均衡算法有哪些?
轮询,加权轮询,随机,最少连接,源地址hash
你知道哪些限流算法?
计数器算法(固定窗口)
滑动窗口
漏桶算法
令牌桶算法
滑动窗口
漏桶算法
令牌桶算法
计数器算法是使用计数器在周期内累加访问次数,当达到设定的限流值时,触发限流策略。下一个
周期开始时,进行清零,重新计数。
此算法在单机还是分布式环境下实现都非常简单,使用redis的incr原子自增性和线程安全即可轻松
实现。
这个算法通常用于QPS限流和统计总访问量,对于秒级以上的时间周期来说,会存在一个非常严重
的问题,那就是临界问题
假设10S内服务器的负载能力为100,因此一个周期的访问量限制在100,然而在第一个周期的最
后5秒和下一个周期的开始5秒时间段内,分别涌入100的访问量,虽然没有超过每个周期的限制
量,但是整体上10秒内已达到200的访问量,已远远超过服务器的负载能力,由此可见,计数器算
法方式限流对于周期比较长的限流,存在很大的弊端。
周期开始时,进行清零,重新计数。
此算法在单机还是分布式环境下实现都非常简单,使用redis的incr原子自增性和线程安全即可轻松
实现。
这个算法通常用于QPS限流和统计总访问量,对于秒级以上的时间周期来说,会存在一个非常严重
的问题,那就是临界问题
假设10S内服务器的负载能力为100,因此一个周期的访问量限制在100,然而在第一个周期的最
后5秒和下一个周期的开始5秒时间段内,分别涌入100的访问量,虽然没有超过每个周期的限制
量,但是整体上10秒内已达到200的访问量,已远远超过服务器的负载能力,由此可见,计数器算
法方式限流对于周期比较长的限流,存在很大的弊端。
滑动窗口算法是将时间周期分为N个小周期,分别记录每个小周期内访问次数,并且根据时间滑动
删除过期的小周期。
当滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。
此算法可以很好的解决固定窗口算法的临界问题。但是不能完全解决
删除过期的小周期。
当滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。
此算法可以很好的解决固定窗口算法的临界问题。但是不能完全解决
漏桶算法是访问请求到达时直接放入漏桶,如当前容量已达到上限(限流值),则进行丢弃(触发
限流策略)。漏桶以固定的速率进行释放访问请求(即请求通过),直到漏桶为空。
限流策略)。漏桶以固定的速率进行释放访问请求(即请求通过),直到漏桶为空。
令牌桶算法是程序以r(r=时间周期/限流值)的速度向令牌桶中增加令牌,直到令牌桶满,请求到
达时向令牌桶请求令牌,如获取到令牌则通过请求,否则触发限流策略
达时向令牌桶请求令牌,如获取到令牌则通过请求,否则触发限流策略
如何提高系统的并发能力?
使用分布式系统。
部署多台服务器,并做负载均衡。
使用缓存(Redis)集群。
数据库分库分表 + 读写分离。
引入消息中间件集群。
部署多台服务器,并做负载均衡。
使用缓存(Redis)集群。
数据库分库分表 + 读写分离。
引入消息中间件集群。
注册中心
注册表结构
eureka
ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>
nacos
Map(namespace, Map(group::serviceName, Service))
Service-->Map<String, Cluster> clusterMap
Cluster
-->Set<Instance> persistentInstances
-->Set<Instance> ephemeralInstances
-->Set<Instance> persistentInstances
-->Set<Instance> ephemeralInstances
服务注册
eureka
服务端
1.修改客户端续约数量和预期每分钟的续约阈值
2.实例信息放入注册表
3.将变动放入最近变化队列
4.清除读写缓存
2.实例信息放入注册表
3.将变动放入最近变化队列
4.清除读写缓存
nacos
客户端
注册数据
serviceName,groupName,ip,port,clusterName,weight,enable,healthy,ephemeral,metadata
1.构建注册数据,并且为临时节点启动定时续约任务,若续约表示未注册,则发起注册请求
2.向服务端发起注册请求
2.向服务端发起注册请求
服务端
1.第一次注册会先创建service,同时service内部启动一个定时任务,每5秒检查一次心跳,若15秒未续约则把健康状态改为false,若30秒为续约则删除实例
2.待续
2.待续
服务续约
eureka
客户端
注册时,启动一个异步任务,每30秒向服务端发送一次心跳
nacos
客户端
注册时,启动一个异步任务,每5秒向服务端发送一次心跳
收藏
0 条评论
下一页