2021秋招学习笔记
文章目录Java基础篇学习(7/3-7/4)数据类型泛型、反射、注解、序列化(加实例)1、reflect类方法2、泛型3、反射原理这里补充java四大引用类型:**反射获取方法总结**反射使用方法invoke4、注解原理这里顺便提一句Java动态代理机制:5、序列化原理6、Clone的原理?7、BigInteger高精度数据结构+java+操作系统+网络(查漏补缺的)1、完全二叉树定义2、Inod
PS:csdn上有很多图片加载不出来,有PDF版在我的资源。(如果没有1积分可以评论我,直接发给你邮箱)
文章目录
- Java基础篇学习(7/3-7/4)
- Java 集合学习(7/16)
- JVM 学习(7/17)
- 一、常见配置汇总
- 二、java内存模型
- 三、java文件的跨平台性
- 四、JVM如何加载.class文件
- 五、JVM内存模型
- 六、GC-java的垃圾回收机制
- 1、判断垃圾是否需要回收?
- 2、垃圾回收算法
- 3、分代回收算法
- 4、垃圾收集器
- 1、Stop-the-World
- 2、Safepoint:
- 3、JVM的运行模式:
- 4、垃圾收集器:
- 七、如何查看线程状态,线程卡在那个地方。(检测死锁位置?)
- 八、Java对象的创建对象的过程
- Redis学习(7/20)
- MySQL学习(7/22没看成,7/29号上午11点)
- Java中IO学习(10/16)
- HTTP相关协议学习(7/29)
- 计算机网络学习(8/4)
- Rocketmq学习(8/4-8/5)
- 数据结构学习(8/9)
- 设计模式学习(8/14)
- 并发进阶面试题复习(9/10-)
- Spring面试题复习(8/17)
- RPC介绍,RPC原理是什么?(9/16)
Java基础篇学习(7/3-7/4)
数据类型
注:包装类型中一般设有缓冲池,比如Integer、String。
1、Integer缓存池范围-128~127都是同一个地址,在缓存池范围内赋值不会创建新的对象,且不开辟新内存空间。该缓存池由源码Integer.class中的IntegerCache这个私有静态内部类定义。该缓存池与jvm关系是:缓存池创建缓存数据,jvm会在常量池中直接找到该值引用。不用创建新的对象。还可以在jvm中设置缓存池hi最大值。
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty(“java.lang.Integer.IntegerCache.high”);
2、String也有缓存池
String不可变性:所谓不可变性就是不可以手动修改已经分配内存空间的String。
//
String使用+拼接,不能改变原来的String,会创建新的String对象。创建和分配空间,所以+号耗时。
String str1 = “java”;
str1.concat(“c++”);
System.out.println("str1: " + str1);//仍然为java
String不可变性的好处就是:1、可以缓存hash值,不用重复计算hash值,例如String常被用于map的键key。2、String
pool中应用了String不可变性。3、安全性。String通常作为参数,例如,网络地址不需要被改变。4、线程安全性。String
不可变性天生具备线程安全,可以在多个线程中安全地使用。
String Pool字符串缓存池:
String缓存池中保存所有字符串字面量(literal
strings),这些在编译时就确定。可以使用intern()方法在运行时,将字符串加入到String
Pool中。**Intern()**方法就是将String Pool中没有的字符串加入,返回String
Pool中引用。如果已经存在直接返回String Pool中引用。
注意:例如,String str = new String(“x”)+new
String(“x“);str.inern();如果String
Pool之前没存在“xx”,调用的str引用会指向String Pool中字面量。【+号不会在String
Pool中创建”xx”对象,只会在Heap中创建对象。】
注:+号反编译之后是调用StringBuilder创建新的对象,拼接之后调用toString()方法:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bc7X1hrO-1606109192015)(media/ef627310e25938ebef4d94734a2ad132.png)]
Intern()方法详解:
https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html
经典问题:
String s = new String(“abc”)这个语句创建了几个对象的题目(前提是String
Pool中还没有abc)。
这种题目主要就是为了考察程序员对字符串对象的常量池掌握与否。上述的语句中是创建了2个对象,第一个对象是”abc”字符串存储在常量池(方法区,独立于java
heap)中,第二个对象在JAVA Heap中的 String 对象。
注:包装类的自动装箱、自动拆箱。
1、下表列出了原始类型及其对应的包装类,Java编译器将其用于自动装箱和拆箱:
Primitive type | Wrapper class |
---|---|
boolean | Boolean |
byte | Byte |
char | Character |
float | Float |
int | Integer |
long | Long |
short | Short |
double | Double |
2、(1)自动装箱,发生于存储在集合类中,基本数据类型不满足要求。或者基本数据类型传参给包装类方法。
(2)自动拆箱,容器中取出基本数据类型。传参给需要基本数据类型的方法。
泛型、反射、注解、序列化(加实例)
1、reflect类方法
Class 和 java.lang.reflect 一起对反射提供了支持,java.lang.reflect
类库主要包含了以下三个类:
-
Field :可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段;
-
Method :可以使用 invoke() 方法调用与 Method 对象关联的方法;
-
Constructor :可以用 Constructor 的 newInstance() 创建新的对象。
2、泛型
白话:
(1)让数据类型变得参数化(不支持基本类型),代替一部分参数类型。
(2)在编译期运用<?extends
List<Integer>>限定容器中存储的参数类型。而且限定类型之后能在编译之前就发现类型错误。List.add(“123”)是加不进去字符串的。
这里属于向上转型(子类对象(小范围)转化为父类对象(大范围)),java中自动转换。
向下转型(父类对象强制转换为子类对象)
把类型明确的工作推迟到创建对象或调用方法的时候才去明确特殊的类型。
容器编译时类型检查。
好处:
(1)通过泛型的语法定义,编译器可以在编译期提供一定的类型安全检查,过滤掉大部分因为类型不符而导致的运行时异常。//可以对参数进行类型限定。
(虽然类型不确定,但是必须是同一种类型的,防止在编译期能发现的问题,要运行时才发现)
(2)泛型可以让程序代码的可读性更高,并且由于本身只是一个语法糖,所以对于 JVM
运行时的性能是没有任何影响的
基本使用:泛型类、泛型方法、泛型变量。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nC8PUFeW-1606109192018)(media/a55440b93e525e376310e9ac3630f817.png)]
类型变量的限定
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FTeZNFRM-1606109192021)(media/45931fa23c6ab225339a514253561a3d.png)]
有如下用法
<T extends Comparable> //一个类型变量的一个类型限定
<T extends Comparable & Serializable> //一个类型变量的两个类型限定
<T extends Comparable,U extends Serializable> //两个类型变量的类型限定
<? extends T>:是指 “上界通配符(Upper Bounds Wildcards)”
<? super T>:是指 “下界通配符(Lower Bounds Wildcards)
基本原理:
编译器和虚拟机对泛型的处理是不一样的
(1)虚拟机不存在“泛型”概念
(2)只有编译器识别泛型,并对其操作。
泛型擦除(类型擦除):
Java中,无论定义了一个反型,在编译时都会自动生成一个相应的原始类型。
T变成Object,Integer擦除变成Object。但之后会插入一行checkcast指令用于强制类型转换。由Object到Integer成为泛型翻译。
注意:java默认泛型不可以向上转型,如果能够转型,说明类可以存储其他子类,这会造成逻辑上的混乱。List<List<Integer>>
re = new ArrayList<ArrayList<Integer>>是非法的。
3、反射原理
通过Java的反射机制,可以在运行期间调用对象的任何方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5ES8xLiw-1606109192024)(media/74b3d628f285e24b418354ee70174595.png)]
类加载的第一步即加载阶段,主要完成下面3件事情:
(1)通过全类名获取定义此类的二进制字节流。
(2)将字节流所代表的静态存储结构转化为方法区的运行时数据结构。
(3)在内存(堆)中生成一个代表该类的class对象,作为方法区这些数据的访问入口。
???cyc2018虚拟机类加载部分。类加载的第一个阶段加载阶段。
Class对象是加载的最终产品,类的方法代码,变量名,方法名,访问权限,返回值等都是方法区的。
Java中使用类java.lang.Class来指向一个类型信息,通过这个Class对象,我们可以得到该类的所有内部信息。
反射获取Method实例
直接从clazz.getDeclaredMethod();方法入手
首先Class类里面有一个软引用类型的成员变量
SoftReference<Class.ReflectionData<T>> reflectionData;
ReflectionData是用来缓存从JVM方法区中读取的类型信息的,比如字段、方法等。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lfYss5sl-1606109192026)(media/5a5bf2166bc9d681934c9e351bc7019b.png)]
该成员变量时SoftReference的,说明在内存紧张时会被GC回收,如果reflectionData被回收之后,又执行了反射方法,通过newReflectionData方法重新创建一个这样的对象。
这里补充java四大引用类型:
Java中根据其生命周期的长短,将引用分为4类:
1、强引用
最普遍的引用,通过new关键字创建的对象所关联的引用是强引用。当JVM内存空间不足,宁愿抛出OOM异常,也不会回收具有强引用对象。一个普通的对象,没有其他引用关系,或将强引用赋值为null,就代表可以GC了。
2、软引用
通过SoftReference类实现。软引用生命周期比强引用短一些。只有当JVM认为内存不足时,才会去回收软引用指向的对象,即JVM保证在抛出OOM之前回收软引用对象。
软引用用于内存敏感的缓存,没有空闲内存就需要被回收的。
3、弱引用
弱引用也是用来描述非必须对象的,他的强度比软引用更弱一些,被弱引用关联的对象,在垃圾回收时,如果这个对象只被弱引用关联(没有任何强引用关联他),那么这个对象就会被回收。
4、虚引用
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
https://www.jianshu.com/p/825cca41d962
继续讲反射实现的重要方法getDeclaredMethod()方法源码
(1)getDeclaredMethod()
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W1JiTAY0-1606109192027)(media/671d78081606f28a6f52a3451451dcb8.png)]
其中Method method = searchMethods(this.privateGetDeclaredMethods(false), name,
parameterTypes);为主要加载方法的步骤。
(2)SearchMethods
SearchMethods中调用的copyMethod每次返回新的method对象。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-afJa8LE1-1606109192029)(media/e2086d55c89fac6f787b7c5b7bf4c3d5.png)]
所次每次调用getDeclaredMethod方法返回的Method对象其实都是一个新的对象,且新对象的root属性都指向原来的Method对象,如果需要频繁调用,最好把Method对象缓存起来。
(3)privateGetDeclaredMethods()
里面又是Method[] privateGetDeclaredMethods() 方法很重要。源码为:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NXa3go2V-1606109192030)(media/9fe730e59a8f7f12bec77cdca68efdbb.png)]
这个方法确定了ReflectionData的获取逻辑:
(1)先查看缓存中对否存在。存在直接返回rd。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-68Zngf8w-1606109192032)(media/9587877781ce302f2fe3cf951800e867.png)]
(2)不存在调用newReflectionData方法从JVM中获取。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xB5W76wk-1606109192034)(media/1eb4c44bf9871fe711896fa917eda42c.png)]
反射获取方法总结
反射获取方法:getDeclaredMethod方法获取本类声明过的public、private等方法。其中先searchMethod找到与所求方法名相同的方法,复制一份返回。而找方法需要调用privateGetDeclaredMethod查看软引用类型缓存的所有类的信息ReflectionData。由此又使用reflectionData()方法判断软引用类的缓存有没有被GC掉,如果没有直接返回,如果被GC了需要从JVM中获取类信息。
反射使用方法invoke
Method.invoke()
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iI3fsi3S-1606109192036)(media/4add198be92a4e69e816314f35ae0ae4.png)]
最终执行的是 MethodAccessor.invoke(obj, args); 方法
该方法会交给一个叫DelegatingMethodAccessorImpl代理类, 最终执行。
4、注解原理
所有注解都继承自Annotation
如注解@Override 定义
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zrvxrtWi-1606109192037)(media/1122105f5fa4e82cc92b60ed5e70dffe.png)]
本质上就是Override接口继承Annotation类
一个注解的意义准确来说,只不过是一种特殊的注释而已,如果没有解析它的代码,它可
能连注释都不如。
解析一个类或者方法的注解有两种方式
(1)编译期直接扫描
(2)运行时反射
JDK提供的几种注解
元注解
用来定义注解**(运行期反射)**
Target:注解的作用目标(方法、类、字段…)
Retention:注解的生命周期(编译器、类加载、运行期)
Documented:注解是否应当被包含在 JavaDoc 文档中
Inherited:是否允许子类继承该注解
内置注解:(编译期直接扫描)
Override:方法重写
Deprecated:方法已过时
SuppressWarnings:压制警告(不希望编译器检查)
运行期反射:
虚拟机规范定义了一系列和注解相关的属性表, 如下:
RuntimeVisibleAnnotations:运行时可见的注解
RuntimeInVisibleAnnotations:运行时不可见的注解
RuntimeVisibleParameterAnnotations:运行时可见的方法参数注解
RuntimeInVisibleParameterAnnotations:运行时不可见的方法参数注解
AnnotationDefault:注解类元素的默认值
对于一个类或者接口来说,Class 类中提供了以下一些方法用于反射注解。
getAnnotation:返回指定的注解
isAnnotationPresent:判定当前元素是否被指定注解修饰
getAnnotations:返回所有的注解
getDeclaredAnnotation:返回本元素的指定注解
getDeclaredAnnotations:返回本元素的所有注解,不包含父类继承而来的
自定义注解Hello
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YRVELYER-1606109192038)(media/8aa37a6a46ef2d598c0258f6d02fdebe.png)]
进行反射调用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4dZAWuz2-1606109192039)(media/43b4b1865ee08588fbcd9f38c6339e44.png)]
注解本质上是继承了 Annotation 接口的接口,而当你通过反射,也就是我们这里的
getAnnotation 方法去获取一个注解类实例的时候,其实 JDK
是通过动态代理机制生成一个实现我们注解(接口)的代理类。
这里顺便提一句Java动态代理机制:
每一个动态代理类的调用处理程序都必须实现InvocationHandler接口,并且每个代理类的实例都关联到了实现该接口的动态代理类调用处理程序中,当我们通过动态代理对象调用一个方法时候,这个方法的调用就会被转发到实现InvocationHandler接口类的invoke方法来调用,看如下invoke方法:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OkXwAyqT-1606109192040)(media/954686e949499d0d612894e1e8ce7f0d.png)]
客户端使用动态代理时,调用Proxy.newProxyInstance()获取代理对象,也可以封装在getAnnotation
方法中。当然还必须编写一个workhandler实际代理对象使用被代理对象Teacher的所有方法。(java动态代理是invocationhandler是一个接口,需要有实际代理类实现这个接口)(相当于新建的一个类,使用反射机制调用Teacher类的方法。。。)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zt0RXh4r-1606109192041)(media/f2f7e7d0204c963ab93bf66b95728840.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iZvf6O5R-1606109192042)(media/f46cee1553cdcc72e1c75184502f6e18.png)]
在其他类使用时,创建workhandle对象调用它的invoke方法,就能既执行原来的方法,而且能执行其他扩展方法。
https://blog.csdn.net/yaomingyang/article/details/80981004
最后我们再总结一下整个反射注解的工作原理:
(1)首先,我们通过键值对的形式可以为注解属性赋值,像这样:@Hello(value =
“hello”)。
(2)接着,你用注解修饰某个元素,编译器将在编译期扫描每个类或者方法上的注解,会做一个基本的检查,你的这个注解是否允许作用在当前位置,最后会将注解信息写入元素的属性表。
(3)然后,当你进行反射的时候,虚拟机将所有生命周期在 RUNTIME
的注解取出来放到一个 map 中,并创建一个 AnnotationInvocationHandler 实例,把这个
map 传递给它。
(4)最后,虚拟机将采用 JDK
动态代理机制生成一个目标注解的代理类,并初始化好处理器。
那么这样,一个注解的实例就创建出来了,它本质上就是一个代理类,你应当去理解好
AnnotationInvocationHandler 中 invoke
方法的实现逻辑,这是核心。一句话概括就是,通过方法名返回注解属性值。
5、序列化原理
概念:
序列化:把Java对象转换为字节序列的过程。
反序列化:把字节序列恢复为Java对象的过程。
如果一个类想被序列化,需要实现Serializable接口。否则将抛出NotSerializableException
异常,这是因为,在序列化操作过程中会对类型进行检查,要求被序列化的类必须属于
Enum、Array和Serializable类型其中的任何一种。
-
在变量声明前加上transient关键字,可以阻止该变量被序列化到文件中。
-
在类中增加writeObject() 和 readObject()
方法可以实现自定义序列化策略(ArrayList就是这么干的)
总结如下
为什么序列化可以破坏单例了?
序列化会通过反射调用无参数的构造方法创建一个新的对象。
如何防止?
在要序列化的类中实现readResolve()方法, 在该方法中返回单例实例
当被反序列化时就会判断, 该类中是否实现了readResolve()方法, 若实现了,
则反射调用该方法
6、Clone的原理?
Clone的浅拷贝、深拷贝。
浅拷贝是将变量引用传递给新变量,在修改新的变量时,旧的变量值会被改变。
深拷贝是重新开辟一片内存空间,复制属性值,然后传递新的引用给新变量。
Clone可以对基本数据类型进行深拷贝,对引用类型则需要重写clone方法。(直到对象clone的都是基本数据类型)才可以实现深拷贝。
使用new和clone的区别:
new操作符的本意是分配内存。程序执行到new操作符时,
首先去看new操作符后面的类型,因为知道了类型,才能知道要分配多大的内存空间。分配完内存之后,再调用构造函数,填充对象的各个域,这一步叫做对象的初始化,构造方法返回后,一个对象创建完毕,可以把他的引用(地址)发布到外部,在外部就可以使用这个引用操纵这个对象。
而clone在第一步是和new操作符相似的,都是进行内存空间的分配,调用clone方法时分配的内存和源对象(即调用clone方法的对象)相同。然后再使用原对象中对应的各个域填充新对象的域,填充完成之后clone方法返回,一个新的相同的对象被创建,同样可以把这个新对象的引用发布到外部
https://www.cnblogs.com/shoshana-kong/p/10822379.html
7、BigInteger高精度
https://blog.csdn.net/sinat_34328764/article/details/79900883
API中描述:
不可变的任意精度的整数。所有操作中,都以二进制补码形式表示 BigInteger(如 Java
的基本整数类型)。BigInteger 提供所有 Java 的基本整数操作符的对应物,并提供
java.lang.Math 的所有相关方法。另外,BigInteger 还提供以下运算:模算术、GCD
计算、质数测试、素数生成、位操作以及一些其他操作。
其实我们所应该知道的就是BigInteger可以表示任意大小的整数,加减乘除的操作都换成了方法调用,Bigxxx类型的数都是不可变的,每次运算都会产生新的对象来进行计算,应该避免大规模的使用。
使用字符串来进行初始化操作String temp1 = “123”,BigInteger bg1 = new
BigInteger(temp1);
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bGocM6LE-1606109192043)(media/8c1ee3ec690308e8ddbde3ee5f9fdbde.png)]
数据结构+java+操作系统+网络(查漏补缺的)
1、完全二叉树定义
定义(百度百科):
一棵深度为k的有n个结点的二叉树,对树中的结点按从上至下、从左到右的顺序进行编号,如果编号为i(1≤i≤n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同,则这棵二叉树称为完全二叉树
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0aSgslH3-1606109192044)(media/078d16082147d85484b32ed11612e487.jpeg)]
只需要节点都集中在左边就行,不需要节点个数为偶数。
2、Inode元信息
inode 和 block 概述#
文件是存储在硬盘上的,硬盘的最小存储单位叫做扇区sector,每个扇区存储512字节。操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个块block。这种由多个扇区组成的块,是文件存取的最小单位。块的大小,最常见的是4KB,即连续八个sector组成一个block。
文件数据存储在块中,那么还必须找到一个地方存储文件的元信息,比如文件的创建者、文件的创建日期、文件的大小等等。这种存储文件元信息的区域就叫做inode,中文译名为索引节点,也叫i节点。因此,一个文件必须占用一个inode,但至少占用一个block。
题目参考标题8
元信息 → inode
数据 → block
参考链接:https://www.cnblogs.com/llife/p/11470668.html
java序列化原理
基本包装类一般实现了serializable接口,所以可以直接序列化为字节序列(字节数组),
自定义对象类需要实现该接口。序列化使用writeObject,反序列化使用readOject方法。
序列化就是使得在不同环境下,
插入排序
插入排序对于基本有序,或者已经有序的数组是效率最高的。(这里的有序不是指全部逆序)
for (int i = 1; i < nums.length; i++) {
if (nums[i - 1] > nums[i]) { //
只有右边数比左边数小时才排序,所以基本有序就占了优势。
int tmp = nums[i]; // 保存小的值
for (int j = i; j >= 0; j–) {
if (j > 0 && nums[j - 1] > tmp) {
nums[j] = nums[j - 1]; // 向后移动大值
} else {
// 找到j这个位置,它之前数要比tmp小,所以插入在这里
nums[j] = tmp;
break;
}
}
}
}
udp到达tcp功能
在网络7层协议中,如果想使用UDP协议达到TCP协议的效果,可以在哪层做文章?
从建立会话连接的角度去考虑。udp是无连接的,tcp通过三次握手建立连接。会话层可以为端到端建立连接,并且维护这个连接,结束时最后释放连接。所以在会话层做文章。
(1)应用层是应用程序与应用程序之间交互。
(2)表示层主要处理数据表示有关的,数据的加密、解密、压缩与解压缩。
(3)会话层主要利用传输层提供的服务,为会话实体之间建立连接、维持连接和释放连接。
(4)传输层提供了进程间的逻辑通信。控制交互通信功能。
(5)网络层向上只提供简单灵活的、无连接的、尽最大努力的交互的数据报服务。
(6)数据链路层以数据帧形式进行交互。
(7)物理层提供物理线路传输电信号。
拥塞控制-发送窗口大小问题
答案:
链接:https://www.nowcoder.com/questionTerminal/bf02156911654f2aaa146f8fd15bd9e0
来源:牛客网
从拥塞控制的角度出发,发生超时时,ssthresh被设定为8的一半,且拥塞窗口被设为1KB,此后再无拥塞,故而拥塞窗口经10个RTT依次变化为2、4(未超过ssthresh值之前以指数级增长)、5、6、7、8、9、10、11、12(超过ssthresh之后以数量级增长),最终达到12KB。
而流量控制的角度出发,接受窗口恒为10KB。
发送方的发送窗口取拥塞窗口和接收窗口的最小值,故最后答案是10KB。
7、Linux的inode
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mtQFh3Vk-1606109192046)(media/4c13d0a47b1fb446dcd73c0ace33e4d7.png)]
解答:
文件名和inode号码是"一一对应"关系,每个inode号码对应一个文件名。但是,Unix/Linux系统允许,多个文件名指向同一个inode号码。这意味着,可以用不同的文件名访问同样的内容;对文件内容进行修改,会影响到所有文件名;但是,删除一个文件名,不影响另一个文件名的访问。这种情况就被称为"硬链接"(hard
link)。
除了硬链接以外,还有一种特殊情况。文件A和文件B的inode号码虽然不一样,但是文件A的内容是文件B的路径。读取文件A时,系统会自动将访问者导向文件B。因此,无论打开哪一个文件,最终读取的都是文件B。这时,文件A就称为文件B的"软链接"(soft
link)或者"符号链接(symbolic link)。
这意味着,文件A依赖于文件B而存在,如果删除了文件B,打开文件A就会报错:“No such
file or
directory”。这是软链接与硬链接最大的不同:文件A指向文件B的文件名,而不是文件B的inode号码,文件B的inode"链接数"不会因此发生变化。
8、线程阻塞-不包括时间片切换
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bHKySnXh-1606109192047)(media/74ed197412b65b0097dd97799a3108de.png)]
一般线程中的阻塞:
A、线程执行了Thread.sleep(int
millsecond);方法,当前线程放弃CPU,睡眠一段时间,然后再恢复执行
B、线程执行一段同步代码,但是尚且无法获得相关的同步锁,只能进入阻塞状态,等到获取了同步锁,才能回复执行。
C、线程执行了一个对象的wait()方法,直接进入阻塞状态,等待其他线程执行notify()或者notifyAll()方法。
D、线程执行某些IO操作,因为等待相关的资源而进入了阻塞状态。比如说监听system.in,但是尚且没有收到键盘的输入,则进入阻塞状态。
9、instanceof运算符+statement
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T1WL6Xml-1606109192048)(media/b94c4e99d3b887c88ffb6e91648dc6cd.png)]
10、如果想列出当前目录以及子目录下所有扩展名为“.txt”的文件,那么可以使用以下哪个命令?
find . -name “*.txt”
11、访问主存上的数据,大概需要多少个机器时钟?
100个。
12、进程进入等待状态有哪几种方式
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BmOVLkgh-1606109192049)(media/ced70090a023c57e2ea49ad55c3f3e9a.png)]
来源:牛客网
进程分为基本的三个状态:运行、就绪、阻塞/等待。
A. 高优先级的抢占CPU,使得原来处于运行状态的进程转变为就绪状态。
B.
阻塞的进程等待某件事情的发生,一旦发生则它的运行条件已经满足,从阻塞进入就绪状态。
C.
时间片轮转使得每个进程都有一小片时间来获得CPU运行,当时间片到时从运行状态变为就绪状态。
D.
自旋锁(spinlock)是一种保护临界区最常见的技术。在同一时刻只能有一个进程获得自旋锁,其他企图获得自旋锁的任何进程将一直进行尝试(即自旋,不断地测试变量),除此以外不能做任何事情。因此没有获得自旋锁的进程在获取锁之前处于忙等(阻塞状态)。
13、同一进程下的线程可以共享以下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gN4GxZ3r-1606109192050)(media/d7efbb19674f759758a75570a5820349.png)]
同一进程内的线程共享 1代码段 2数据段 3打开文件列表 4堆
线程私有 1线程id 2寄存器 (用于暂时存放数据)3工作栈
14、正则表达式
* 匹配前面的字符0或多次。
+ 匹配前面的字符1或多次。
? 匹配前面的字符0或1次。
[1-9]* 表示匹配任意的数字字符串。
{n} n正整数,表示确定匹配n个字符。
15、docker底层隔离机制
Docker的隔离性主要运用Namespace 技术
16、递归的时间复杂度分析
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0OiviYQG-1606109192052)(media/121bd494a0c33dd1e2dc1c634017bee1.jpeg)]
//截止条件只有一个x
public static void fun(int x, int y, int z) {
if (x <= 0) {
System.out.println(y + “,” + z);
return;
}
fun(x - 1, y + 1, z);
fun(x - 1, y, z + 1);
}
只有一个调用fun(x - 1, y + 1, z);复杂度就是O(n)。
有两个调用fun(x - 1, y + 1, z);复杂度就是O(2^n)。
17、程序流程控制
1、流程控制三大结构:顺序、循环、分支!
2、改变程序执行方式:选择、循环、方法调用。
18、ThreadLocal的继承问题?
关于ThreadLocal以下说法正确的是:
A、ThreadLocal继承自Thread
B、ThreadLocal实现了Runnable接口
C、ThreadLocal重要作用在于多线程间的数据共享
D、ThreadLocal是采用哈希表的方式来为每个线程都提供一个变量的副本
E、ThreadLocal保证各个线程间数据安全,每个线程的数据不会被另外线程访问和破坏
**答案:**D和E正确的
http://www.tilaile.com/question/12616
四、谈谈你对Java的理解
1、平台无关性
2、GC
3、语言特性
4、面向对象
5、类库
6、异常处理
1、平台无关性
编译时、运行时。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Akw2AAnc-1606109192053)(media/11a7b37aae183bcbe3c8abaf5ce2cd32.png)]
为什么JVM不直接将源码解析成机器码去执行?
1、如果直接解析源码,需要每次都编译,每次执行都需要各种检查
2、兼容性:也可将别的语言解析成字节码
一、final关键字的几点注意点(9/17)
1、final修饰数据。
(1)final修饰基本类型变量,代表该变量是常量不能被再次初始化。(final修饰成员变量一定要进行初始化,否则编译报错,修饰一般变量时,在使用之前需要进行初始化。)
【final修饰的变量-常量,在类加载的准备阶段,进行赋初始值,而且是赋常量的值。】
Static修饰变量表示该变量属于类变量,可以修改内容。类加载准备阶段,赋的是零值。
https://blog.csdn.net/zxd8080666/article/details/78087646
(2)final修饰引用类型变量,代表该引用不能指向其他引用变量。
final变量有点类似C中的宏替换,但是这种替换只有在编译期能够确定final修饰的变量的值,编译器才能对变量直接进行替换。
例1:
public static void main(String[] args) {
String a = “hello2”;
final String b = “hello”;
String d = “hello”;
String c = b + 2;
String e = d + 2;
System.out.println((a == c));
System.out.println((a == e));
}
结果: true,false
因为第一个b是final修饰的,编译期可以确定的值。
例2:
public static void main(String[] args) {
String a = “hello2”;
final String b = getHello();
String c = b + 2;
System.out.println((a == c));
}
public static String getHello() {
return “hello”;
}
结果: false,false
因为b在编译期无法确定值。
2、final修饰方法。
final修饰的方法,代表该方法在子类中不可以被重写。子类中有相同的函数签名也不是对父类方法的重写。(同函数名的方法重载不受影响。)
3、final修饰类。
final修饰的类,不可以被继承。final不可以修饰abstract类,匿名内部类中所有变量必须是final变量。
Ps:
(1)final关键字容易与finalize()方法搞混,后者是在Object类中定义的方法,是在垃圾回收之前被JVM调用的方法。
(2)接口中声明的所有变量本身是final的。
(3)final和abstract这两个关键字是反相关的,final类就不可能是abstract的。
(4)final方法在编译阶段绑定,称为静态绑定(static binding)。
参考:https://www.jb51.net/article/157603.htm
二、finally的注意点(9/17)
首先,不管有没有异常抛出或者try-catch中有无return。finally中语句都会执行!
这是为了使得finally中的一些对资源的释放、关闭不受影响。
(1)finally中有return,编译器会有警告(finally block does not complete
normally,finally语句块没有正常完成)那么try-catch中无论return还是抛异常,finally中的return都会覆盖返回值。只返回finally中的return的值。
(2)finally中没有return,try-catch中有return,那么return和抛异常是正常输出的。如果finally中对return的变量进行修改,那么不会影响到返回值的大小。
try-catch-finally的执行顺序:
(1)try中执行到return(如果有return)之前所有语句,包括return中的运算语句。(相当于返回值已经确定了)。
(2)catch捕获try中运行语句的异常。程序执行catch块中return之前(包括return语句中的表达式运算)代码。
(3)执行finally中语句(没有return不能改变1、2的返回结果),如果有return覆盖1、2返回结果。
https://blog.csdn.net/u011277123/article/details/59074492
三、static关键字的初始化顺序(9/17)
静态变量和静态代码块初始化,先于实例变量和普通语句块。静态变量、静态代码块之间初始化顺序取决于自身的代码顺序。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7oODlNUU-1606109192054)(media/8901b7e6f3eb375411a6e21e92e5bad8.png)]
最后才是构造函数的初始化。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wJLZy7pU-1606109192055)(media/63239c268892ffb7fb56a6f7c5a59ff2.png)]
重点:存在继承的初始化顺序。
(1)父类的静态变量、静态代码块。
(2)父类的实例变量、普通语句块初始化。
(3)父类的构造函数初始化。
(4)子类的静态变量、静态代码块执行。
(5)子类的实例变量、普通语句块初始化。
(6)子类的构造函数初始化。
四、Java8学习(9/18)
1、接口的默认方法(Default Methods for Interfaces)
Java8使我们够通过default关键字向接口添加非抽象方法的实现。此功能也称为虚拟扩展方法。
不管是抽象类还是接口,都可以通过匿名内部类的访问方式访问。不能通过抽象类或者接口直接创建对象。对于匿名内部类方式访问接口,我们可以这样理解:一个内部类实现了接口里的抽象方法并且返回一个内部类对象,之后我们让接口的引用来指向这个对象。
2、Lambda表达式
-
List<String> names = Arrays.asList(“peter”, “anna”, “mike”, “xenia”);
-
Collections.sort(names, new Comparator<String>() {
-
@Override
-
public int compare(String o1, String o2) {
-
return o1.compareTo(o2);
-
}
-
});
-
names.sort((o1, o2) -> o1.compareTo(o2));
-
System.out.println(names);
(1)函数式接口(Functional Interfaces):
为了使现有函数友好地支持Lambda。最终采取的方法是:增加函数式接口的概念。“函数式接口”是指仅仅包含一个抽象方法,但是可以有多个非抽象方法(也就是上面提到的默认方法)的接口。像这样的接口,可以被隐式转换为Lambda表达式。
Java.lang.Runnable与java,util.concurrent.Callable是函数式接口最典型的两个例子。
Java
8增加了一种特殊的注解@FunctionalInterface,但是这个注解通常不是必须的(某些情况建议使用),只要接口只包含一个抽象方法,虚拟机会自动判断该接口为函数式接口。一般建议在接口上使用@FunctionalInterface
注解进行声明,这样的话,编译器如果发现你标注了这个注解的接口有多于一个抽象方法的时候会报错的,如下图所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NQU8nod1-1606109192056)(media/f9fdb55ef3006988cb215fe9881e9cd8.png)]
-
@FunctionalInterface
-
public interface Converter<F, T> {
-
T convert(F from);
-
}
-
// 函数式接口 将数字字符串转换为整数类型
-
Converter<String, Integer> converter = (from) -> Integer.valueOf(from);
-
Integer converted = converter.convert(“123”);
-
System.out.println(converted.getClass());// class java.lang.Integer
-
System.out.println(converted);
Lambda表达式无法访问接口的默认方法。
(2)Lambda作用域:
在lambda表达式中访问外层作用域和老版本的匿名对象中的方式很相似。你可以直接访问标记了final的外层局部变量,或者实例的字段以及静态变量。
-
// Lambda作用域,访问局部变量
-
// final int num = 1;
-
int num = 1;
-
// 函数式接口,将Integer->String
-
Converter<Integer, String> stringCovert = (from) -> String.valueOf(from +
num); -
System.out.println(stringCovert.convert(2));
-
// num = 3;
这里的num必须不可被后面的代码修改(即隐性的具有final的语义,在lambda表达式中修改num也是不对的)
(3)访问对象字段与静态变量
和本地变量不同的是,lambda内部对于实例的字段以及静态变量是即可读又可写。该行为和匿名对象是一致的:
void testScopes() {
Converter<Integer, String> stringConverter1 = (from) -> {
outerNum = 23;
return String.valueOf(from);
};
// 必须要调用方法
stringConverter1.convert(23);
Converter<Integer, String> stringConverter2 = (from) -> {
outerStaticNum = 72;
return String.valueOf(from);
};
stringConverter2.convert(72);
}
3、方法和构造函数引用(Method and Constructor References)
Java 8 允许你使用 ::
关键字来传递方法或者构造函数引用,上面的代码展示了如何引用一个静态方法,我们也可以引用一个对象的方法:
前一节中的代码还可以通过静态方法引用来表示:(双冒号引用方法)
-
Converter<String, Integer> converter = Integer::valueOf;
-
Integer converted = converter.convert(“123”);
-
System.out.println(converted.getClass()); //class java.lang.Integer
4、Streams(流)
Java.util.Stream表示能应用在一组元素上一次执行的操作序列。Stream操作分为中间操作或者最终操作两种,最终操作返回一特定类型的计算结果,而中间操作返回Stream本身,这样你就可以将多个操作依次串起来。Stream的创建需要指定一个数据源,比如java.util.Collection的子类,List或者Set,Map不支持。Stream的操作可以串行执行或者并行执行。
Filter(过滤)
过滤通过一个predicate接口来过滤并保留符合条件的元素,该操作属于中间操作,所以我们可以在过滤后的结果来应用其他Stream操作(forEach)。forEach需要一个函数来对过滤后的元素一次执行。forEach是一个最终操作,所以我们不能在forEach之后执行其他Stream操作。
-
List<String> stringList = new ArrayList<>();
-
stringList.add(“ddd2”);
-
stringList.add(“aaa2”);
-
stringList.add(“bbb1”);
-
stringList.add(“aaa1”);
-
stringList.add(“bbb3”);
-
stringList.add(“ccc”);
-
stringList.add(“bbb2”);
-
stringList.add(“ddd1”);
-
// 测试 Filter(过滤)
-
stringList.stream().filter((s) ->
s.startsWith(“b”)).forEach(System.out::println);// bbb1 bbb3 bbb2
http://www.planetb.ca/syntax-highlight-word
Sorted(排序)
排序是一个中间操作,返回的是排序好后的Stream。如果你不指定一个自定义的Comparator则会使用默认排序。
// 测试 Sort (排序)
stringList
.stream()
.sorted()
.filter((s) -> s.startsWith(“a”))
.forEach(System.out::println);// aaa1 aaa2
需要注意的是,排序只创建了一个排列好后的Stream,而不会影响原有的数据源,排序之后原数据顺序不会改变的。
System.out.println(stringList);// ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
Java 集合学习(7/16)
ArrayList
-
ArrayList基于数组访问的,所以支持随机访问。实现RandomAccess接口。【顺便提一嘴实现comparable接口意味着该类(或容器)是可以排序的】
-
数组默认大小为10.—Object【】elementData
Private static final int DEFAULT_CAPACITY=10
- ArrayList扩容:
ArrayList添加元素add(E
e)时,使用ensureCapacityInternal()方法保证容量足够,容量不够调用grow()方法扩容,新容量为oldCapacity+(oldCapacity>>1)右移/2,原来的1.5倍。
扩容时需要调用Arrays.copyOf()方法,将所有元素复制到新数组,时间复杂度为O(n)。
- ArrayList删除:
将index后面元素通过System.arraycopy()复制到index位置,时间复杂度O(n)
- 序列化
ArrayList基于数组,有动态扩容特性,不需要整个数组都序列化,所以加transient修饰。
ArrayList实现了writeObject()和readObject()控制只序列化有元素填充的那部分内容。
readObject将字节流反序列化成传入对象,writeObject()则将数组对象序列化成字节流。
- ArrayList的Fail-Fast机制,保证ArrayList在动态变化时的操作准确。
modCount记录ArrayList结构发生变化的次数。包括添加、删除、扩容时modCount都会改变。序列化和迭代时参考modCount,不相等抛出ConcurrentModificationExpection。
Vector
与ArrayList类似加了synchronized关键字修饰方法。
扩容时2倍.
Collections.synchronizedList(); 得到一个线程安全的 ArrayList
CopyOnWriteArrayList
-
List<String> list = new CopyOnWriteArrayList<>();
-
读写分离:
写操作是在一个复制的数组上进行,读操作还是在原始数组中进行。
写操作加ReentrantLock,防止并发写,发生写入丢失。
写操作结束后需要把原始数组指向新的复制数组。
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
//array—>复制数组newElements
final void setArray(Object[] a) {
array = a;
}
- 适用场景
因为其读写分离的,可以在写操作的同时允许读操作,大大提高写少读操作多的场景。只不过读到的未必是实时数据。
CopyOnWriteArrayList缺陷:
-
数据不一致:读写分离导致不能读取实时数据,部分写操作还未同步到数组中。
-
内存占用:写操作需要复制一个数组。内存占用为2倍。
所以CopyOnWriteArrayList不适合内存敏感和对实时性要求高的场景。
适用于读多写少:只需每日一次更新黑名单;监听器,迭代操作远多于修改操作。
补充:
迭代修改
ArrayList 在迭代期间如果修改集合的内容,会抛出 ConcurrentModificationException
异常。原因是,在 ArrayList 源码里的 ListItr 的 next 方法中有一个
checkForComodification 方法。
和 ArrayList 不同的是,CopyOnWriteArrayList
的迭代器在迭代的时候,如果数组内容被修改了,CopyOnWriteArrayList 不会报
ConcurrentModificationException
的异常,因为迭代器使用的依然是旧数组,只不过迭代的内容可能已经过时了。迭代修改过程中,读出的内容还是旧的内容。(一定要在读多写少的场景下,对实时性要求较低情况下)
LinkedList
双向链表
transient Node<E> first;
transient Node<E> last;
ArryList查询O(1),插入删除O(n) 尾插O(1)
LinkedList查询O(n),插入删除O(1)
HashMap
内部是由Entry类型(int类型,boolean型啊)的数组table。Entry<K,V>[] table。
Entry<K,V>类包含4个参数:(1)hashCode:int;(2)K key;(3)V
value;(4)next指针
由此发现是一个链表。每个链表存储【hash值对table长度取余值】相同的值。
拉链法解决冲突,采用头插法,时间复杂度与链长有关O(n)
JDK 1.8 开始,一个桶存储的链表长度大于等于 8 时会将链表转换为红黑树。
- 扩容-基本原理
和扩容相关的参数主要有:capacity、size、threshold 和 load_factor。
Capacity默认16,必须保证为2的次方。
Size,键值对数量。(是hash表的键值对达到阈值时,进行2倍的扩容操作)
Threshold:size的临界值,当size大于等于threshold就必须进行扩容。
LoadFactor:装载因子,table能够使用的比例,threshold = LoadFactor*capacity
负载因子为0.75,单个hash槽内的等于8才转换为红黑树,小于等于6才转链表。
链表长度大于8并且数组长度大于64则会转换为红黑树,否则先扩容。
2020/10/10补充:
参考:https://blog.csdn.net/the_one_and_only/article/details/81665098
HashMap重要成员:
(1)初始容量:默认为16,需要为2的整数倍。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
(2)装载因子:表示table内键值对占用比例,默认为0.75f。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
装载因子*容量 大于 Threshold阈值就需要进行扩容,将capacity容量变为两倍。
(3)链表长度到8,就转为红黑树
static final int TREEIFY_THRESHOLD = 8;
(4)树大小为6,就转回链表
static final int UNTREEIFY_THRESHOLD = 6;
注:hashset中的去重是直接调用hashmap中的put方法来进行处理的:
return map.put(key,PRESENT)==null;
没有重复的就直接插入,有重复的进行put逻辑,
Hashmap的put操作是需要调用自身的putVal函数,对key进行hash运算计算出结果后,比较hash&(table.length-1)对应位置是否为null,如果为null直接新建插入节点,否则对应位置是有值存在的,判断该位置及其链表上是否已经存在该元素,存在就会覆盖(但是没有改变set也是利用这个特点)。还会判断是否是红黑树节点,是的话就插入红黑树中。
扩容时,capacity变为2倍。
扩容使用resize()实现,扩容需要把原来的table全部重新插入新的table;
HashMap构造函数允许用户传入容量不是2的n次方,因为它可以自动传入的容量转化为大于该值的最小2次方数。
2、jdk1.7的concurrentHashMap,是由segment<K,V>组成(继承自ReentrantLock,底层是个HashEntry<K,V>[]
table),每个segment维护几个HashEntry桶,默认并发级别是16也就是默认16个segment。
每个segment维护一个count统计该 Segment 中键值对个数。
Size()方法是先不加锁遍历所有segment中count求和,连续两次值相等就认为该结果是正确的。尝试此时超过3次,就需要对每个segment加锁。
JDK1.8是对每一个桶HashEntr<K,V>修改时,执行CAS操作。支持比16更高的并发度
JDK 1.8 使用了 CAS 操作来支持更高的并发度,在 CAS 操作失败时使用内置锁
synchronized。
LinkedHashMap
LinkedHashMap继承自HashMap
public class LinkedHashMap<K,V> extends HashMap<K,V> implements
Map<K,V>
- 内部维护了一个双向链表,用来维护插入顺序or LRU顺序。
accessOrder决定了顺序,默认为false插入顺序。(即使节点被访问不需要移动节点位置)。
accessOrder设置为true就是为LRU顺序。(被访问过的节点会被移动到链表尾部tail)
LinkedHashMap 最重要的是以下用于维护顺序的函数,它们会在 put、get
等方法中调用。
void afterNodeAccess(Node<K,V> p) { } ------ get()方法中调用执行
void afterNodeInsertion(boolean evict) { } ------ put()方法调用后执行
afterNodeAccess方法在get()中执行,将被访问的节点移动到结尾。链表首部就是最近最久未被使用的节点。
afterNodeInsertion()方法在put()等操作之后执行,删除链表首部节点。
要实现LRU缓存不仅要在构造函数中修改accessOrder,还要重写removwEldesEntry函数让其返回true。
Class LRUCache<K,V> extends LinkedHashMap<K,V>{
Private static final int MAX_ENTRIES = 3;
Protected Boolean removeEldestEntry(Map.Entry eldest){
Return size()>MAX_ENTRIES;
}
LRUCache() {
super(MAX_ENTRIES, 0.75f, true);
}
}
JVM 学习(7/17)
一、常见配置汇总
-
堆设置
-
-Xms:初始堆大小
-
-Xmx:最大堆大小
-
-XX:NewSize=n:设置年轻代大小
-
**-XX:NewRatio=n:**设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
-
-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
-
-XX:MaxPermSize=n:设置持久代大小
-
-
收集器设置
-
-XX:+UseSerialGC:设置串行收集器
-
-XX:+UseParallelGC:设置并行收集器
-
-XX:+UseParalledlOldGC:设置并行年老代收集器
-
-XX:+UseConcMarkSweepGC:设置并发收集器
-
-
垃圾回收统计信息
-
-XX:+PrintGC
-
-XX:+PrintGCDetails
-
-XX:+PrintGCTimeStamps
-
-Xloggc:filename
-
-
并行收集器设置
-
-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
-
-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
-
-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
-
-
并发收集器设置
-
-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
-
-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。
-
二、java内存模型
JAVA内存模型和JVM内存划分:
(1)JVM内存结构与JVM中运行时数据区有关(RuntimeDataArea)
(2)Java内存模型与并发编程有关。
两者没有本质关系。见周志明《深入理解Java虚拟机》p442
https://www.zhihu.com/question/43519009/answer/197267028
Java内存模型(JMM)是和多线程相关的一组规范,作用就是让相同的java程序在不同的jvm上运行也能得到相同的结果(java跨平台特性)。JMM与处理器、缓存、并发、编译器有关。JMM解决了CPU多级缓存、处理器优化、指令重排序导致的问题。
JMM的3点内容:1、重排序。2、原子性(happens-before?)。3、内存可见性(主内存和工作内存关系)。
1、重排序
编译器、JVM和CPU对于java程序出于优化目的,可能进行实际指令执行顺序的重排序。
-
编译器优化。编译器(JVM、JIT编译器等)出于优化目的,保证单线程内的语义顺序下,改变指令执行顺序。
-
CPU重排序。与1)类似目的。
-
内存重排序。内存中有缓存,在JMM中表现为贮存和本地内存,主存和本地内存可能不一致。
-
cpu多级缓存
缓解cpu速度和内存处理速度不匹配,所以就有缓存层存在。
- 主内存和工作内存。
每个线程只能直接接触工作内存,不能直接操作主内存,工作内存中保存的变量时从主内存中对应变量拷贝下来的副本。修改需要线程同步到主内存中,这样其他线程才能看到修改。线程之间通信需要借助主内存进行中转。主内存和工作内存的通信是由JMM控制。
- Happens-before
描述的是可见性问题:第一个操作的内容,对于第二个操作而言是可见的。
-
是volatile使用场景,该关键字是保证可见性和禁止重排序的。
-
作为共享变量被各个线程读取或赋值(没有其他的操作,没有复合操作)。赋值操作具有原子性的,不用担心线程不安全。
-
作为触发器,保证可见性和禁止指令重排序
Volatile如何实现内存可见性?
volatile关键词能保证可见性,不能保证变量复合操作的原子性(如i++)
volatile通过加入内存屏障和禁止重排序来实现。
-
volatile变量执行写操作,在写操作后加一条store屏障指令。(store指令将写入数据写入主存,对其他线程可见)
-
volatile变量执行读操作,在读操作前加一条load屏障指令(load指令将缓存中的数据失效,强制从主存中加载数据)。
读操作之前加load指令,写操作之后加store指令。采用悲观锁的态度使用内存屏障。
Synchronized实现可见性?
原子性:由于加锁,线程之间不允许交叉执行,所以保证了原子性。
可见性:JMM关于synchronized规定:解锁前将变量值刷新到主内存。加锁时,清空工作内存中的变量值,从主内存中重新读取值。
单例模式使用检查锁存在的问题?
// 1、多线程就拉胯
// 2、方法加synchronized,多线程阻塞,效率低下
//
3、对SingletonDemo加锁,只允许一个线程创建实例,指令重排:线程1还未分配内存,该单例对象就被线程2使用。
//4、volatile关键词修饰禁止指令重排,同时synchronized关键词给singletonDemo加锁。
三、java文件的跨平台性
跨平台表现在java文件一次编译到处执行。是由于在各个平台上(Mac、Linux、Windows系统)都由不同平台的JVM解析成适用于不同平台执行的机器码指令。如java文件在win上编译过,到linux上直接执行java文件即可,会由jvm解析字节码文件。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zvxVjOeb-1606109192057)(media/f72692affabdede5f5d52b2934264ded.png)]
四、JVM如何加载.class文件
JVM是一个内存中的虚拟机,JVM的存储就是内存。
JVM主要由
(1)Class Loader:根据特定格式,加载class文件到内存。
(2)Excution Engine:对命令进行解析。
(3)RunTime Data Area:JVM内存空间结构模型。
(4)Native Interface四个部分组成:融合不同的开发语言的原生库为Java所用。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bjdT6jw2-1606109192058)(media/4c9f00f24beb7c67594ee2369fa56513.png)]
反射class.forName()加载类的时候,发现的trick:
-
加载主类class.forName(“com.xjr.reflect.Proxy”);
-
加载Proxy内部的静态类,需要加$符与主类名连接。
Class.forName(“com.xjr.reflect.Proxy$InnerClass”);
类从编译到执行的过程?
(1)编译器将Proxy.java源文件编译为Proxy.class字节码文件。
(2)ClassLoader将字节码转换为JVM中的Class<Proxy>对象。(字节码对象)
(3)JVM利用Class<Proxy>对象实例化为Proxy对象。
谈谈ClassLoader?
ClassLoader工作在Class装载的加载阶段,主要作用是从系统外部获得Class二进制数据流。并将Class文件中的符合格式的二进制数据流加载进系统,交给java虚拟机进行连接、初始化等操作。(所有的Class都是由ClassLoader进行加载的)
2020/08/29整合了一下整个类加载的过程:
第2点是类加载过程
1、首先是编译器先将.java文件编译成字节码文件.class。
2、类加载器经过5个阶段将字节码文件转换成使用的文件。
(1)加载阶段。(2)验证阶段。(3)准备阶段。(4)解析阶段。(5)初始化阶段。
(1)加载阶段:
*选择合适的类加载器,通过完全限定类名称获取二进制字节流byte[]。(FileInputSream加载的字节码文件)
*将字节流表示的静态存储结构转换为方法区的运行时存储结构。
*内存中生成一个代表该类的Class对象,作为方法区中访问该类数据的入口。
(2)验证阶段:验证字节流中包含信息符合虚拟机要求,并不会危害到虚拟机自身的安全。(从整体上看,验证阶段大致上会完成下面4个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。)
(3)准备阶段:为类变量使用方法区内存,分配内存并设置初始值。Static类型的int赋值0,static
final常量的直接设常量值。(final类型的常量,在这个阶段进行赋值)
(4)解析阶段:将(字节码文件中变量关系)符号引用转化为直接引用(直接指向目标的指针,句柄)。(可以在初始化后开始,支持动态绑定)
https://blog.csdn.net/maihilton/article/details/81531878
(5)初始化阶段:真正开始执行类中定义的java程序代码,虚拟机执行类构造器<clinit>()方法。根据程序员通过程序制定的主观计划取初始化类变量和其他资源。
(一个实例对象,在jvm分配内存是会被赋初值;声明实例变量时又会被赋值一次;代码块中可以对其再次进行初始化;构造函数中也可对其进行赋值)最多4次实例变量,不是类变量。初始化其他资源,这些资源有static{}块,构造函数,父类的初始化等。
3、使用该类
4、卸载(GC垃圾回收)
为什么要自定义类加载器?
(1)我们需要的类并不在已经设定好的classpath中,对自定义路径上的类需要有自己的类加载器。
(2)我们从网络流中读取类,需要有自定义的加密、解密。此时就需要自定义的类加载器。
双亲委派机制是为了?避免同样的字节码的类加载。
破坏双亲委派机制?受加载范围限制,不能使用父类加载器对类进行加载,某类的实现不在父类加载目录中,需要对实现类进行加载,则需要委派子类加载器。如jdk中的driverManager被启动类加载器加载,但是是由各个厂商实现类的,所以向下委派系统类加载器加载。
ClassLoader的种类:
(1)BootStrapClassLoader:C++编写,加载核心库java.*
(2)ExtClassLoader::java编写,加载扩展库javax.*
(3)AppClassLoader:java编写,加载开发者在classpath下所指定的类和jar。
(4)自定义ClassLoader:java编写,定制化加载。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AYRTdkz2-1606109192059)(media/cc315b2d9d7743649592b450cb259c34.png)]
可以通过自定义类加载器,对字节码进行加密等强化操作。
双亲委派机制:
(1)从底向上,向上委派,尝试查看类文件和jar是否已经加载。
(2)直到最顶层的Bootstrap ClassLoader都没加载过,在Boostrap
ClassLoder加载目录中加载类,如果没有就会向下委派。层层向下委派加载类文件,如果最终自定义类无法加载就返回类无法加载。
ExClassLoader的getParent是Boostrap ClassLoader因为是C编写的是null。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RYlcYQJ7-1606109192060)(media/5d968880289a25769dbd45a0ba922335.png)]
为什么要使用双亲委派机制去加载类?
(1)避免多份同样的字节码的加载。(内存中只有一份字节码对象)
为什么要破坏双亲委派机制去加载类?
因为在某些情况下父类加载器需要委托子类加载器去加载class文件。受到加载范围的限制,父类加载器无法加载到需要的文件,以Driver接口为例,由于Driver接口定义在jdk当中的,而其实现由各个数据库的服务商来提供,比如mysql的就写了MySQL
Connector,那么问题就来了,DriverManager(也由jdk提供)要加载各个实现了Driver接口的实现类,然后进行管理,但是DriverManager由启动类加载器加载,只能记载JAVA_HOME的lib下文件,而其实现是由服务商提供的,由系统类加载器加载,这个时候就需要启动类加载器来委托子类来加载Driver实现,从而破坏了双亲委派,这里仅仅是举了破坏双亲委派的其中一个情况。
自定义ClassLoader
如果不想不破坏双亲委派模型,只要去重写findClass方法
如果想要去破坏双亲委派模型,需要去重写loadClass方法
https://blog.csdn.net/zhouxcwork/article/details/81566636
区别一:
getClassLoader()是当前类加载器,而getContextClassLoader是当前线程的类加载器
区别二:
getClassLoader是使用双亲委派模型来加载类的,而getContextClassLoader就是为了避开双亲委派模型的加载方式的,也就是说它不是用这种方式来加载类
类的加载方式
(1)隐式加载:new
(2)显示加载:loadClass,forName(先获得的是字节码对象class对象,然后反射调用newInstance方法创建实例)
loadClass和forName的区别?
类的装载过程:
1、加载
通过ClassLoader加载class字节码文件,生成Class对象。
- 链接(jvm)
(1)校验:检查加载的class的正确性和安全性;(2)准备:为类变量分配存储空间并设置类变量初始值;(3)解析:JVM将常量池内的符号引用转换为直接引用。
- 初始化
执行类变量赋值和静态代码块。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HjisjS4g-1606109192061)(media/de79576663d1a2ecb845c64ad731001d.png)]
一、ClassLoader中的loadClass方法,决定是否需要解析resolve该类,然后会去调用链接。
因此Classloader.loadClass得到的class是还没有链接的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YLIpeBXl-1606109192061)(media/489074f60ab3176d320b9aad474a762c.png)]
运行后发现并没有执行静态代码块。说明类加载只到链接阶段。
二、Class.forName得到的class是已经初始化完成的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-28r9oF8U-1606109192062)(media/7a5ac4d22baa5ac380a79b0b98d3dca8.png)]
发现静态代码块static中的被打印出来了。说明class.foName能将类装载执行到初始化阶段。
两种加载方式都是有其存在价值的:
(1)Class.forName(”com.mysql.jdbc.Driver”)需要将Driver中的静态代码块执行
(2)IOC容器中大量使用loadClass,是为了让类加载变快速(懒加载),直到类需要使用时,再对其进行初始化。
总结:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QSjpuvSl-1606109192063)(media/0f14aad2b47da6785ea70892860c30fb.png)]
五、JVM内存模型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qblK1q9n-1606109192064)(media/a404b29dfa175cc27d50fa394a342024.png)]
1、程序计数器:
(1)当前线程多执行的字节码行号指示器(逻辑计数器)
(2)改变计数器的值来选取下一条需要执行的字节码指令。
(3)不会发生内存泄露
(4)native本地方法程序计数器值为Undefined
2、Java虚拟机栈(Stack)
(1)Java方法执行的内存模型
(2)包含多个栈帧
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fF74uufJ-1606109192066)(media/4931e3477d765c8b36ea28d0aad09a1f.png)]
Java栈如何运行?—每次调用方法都会加载到栈顶(所以递归过深就会导致stackOverFlow)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wzubwlJQ-1606109192067)(media/b29e5c702e382947f39e5e97b2d63e73.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yp6RiBTq-1606109192068)(media/259c97a6d259bb76eb37928239da5ed7.png)]
(1)先加载a,b参数到局部变量表。先将c=0加载到操作数栈,再加载进局部变量表。
(2)把a,b加载操作数栈,进行运算之后,再加载回局部变量c中。
(3)将局部变量c加载如操作数栈,然后return。
3、本地方法栈:
与java虚拟机啊栈相似,主要作用域标注了native的方法。
4、方法区(元空间)
元空间(MetaSpace)与永久代(PermGen)的区别?
(1)元空间和永久代都是方法区的实现,java
7之后方法区(元空间)中的字符串常量池,被移动到java堆中。Java
8中元空间替代永久代。
(2)元空间使用本地内存(不存在于jvm内存),而永久代使用的是jvm的内存。
(3)方法区存储类加载信息。
1)这个元空间是使用本地内存(Native
Memory)实现的,也就是说它的内存是不在虚拟机内的,所以可以理论上物理机器还有多个内存就可以分配,而不用再受限于JVM本身分配的内存了。
2)“元空间” 和 “方法区”,一个是HotSpot 的具体实现技术,一个是JVM规范的抽象定义;
https://www.zhihu.com/question/358312524
http://openjdk.java.net/jeps/122 删除永久代原因。
元空间的优势:
(1)字符串常量池存在永久代中,容易出现性能问题和内存溢出,利用的jvm内存。
(2)类和方法的信息大小难确定,给永久代的大小指定带来困难。
(3)永久代会为GC带来不必要的复杂性
https://blog.csdn.net/lehek/article/details/104448678
网上搞了图区分jvm内存和本地内存?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IYNLabLV-1606109192069)(media/15cbcf46507d234eec7a6e813a334786.png)]
5、Java堆(Heap)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0Ecd37G1-1606109192070)(media/2ac87b66c37b5676e1a9f17cd98654c9.png)]
GC管理的主要区域:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aoVbYTHu-1606109192071)(media/6616c1141375ac97cbbcf82e240f5616.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-43nXtgZo-1606109192072)(media/67796f9fc657b7b4dd0054f561248f5b.png)]
新生代和老年代的比值为1:2。Eden 8:10和survivor是2:10
JVM三大性能调优参数-Xms -Xmx -Xss的含义
(1)-Xss 规定了每个线程虚拟机栈的大小
(2)-Xms 堆初始大小
(3)-Xmx 堆能达到的最大值
Java内存模型中堆和栈的区别-内存分配策略
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XZYZ2mHL-1606109192073)(media/af63366588722609cb9f8d4bec9df88a.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xc8aqHFt-1606109192074)(media/dcbc2c87db8b787be1eb7860696e703f.png)]
类的各部分存储情况:
元空间:
(1)Class:HelloWorld -Method:sayHello\setName\main -Field:name
(2)Class:System
Java堆:
(1)Object:String(“test”)//字符串常量池、字面量
(2)Object:HelloWorld
线程独占:
(1)Parameter Reference:“test” to String object //test参数的引用
(2)Variable reference:“hw” to HelloWorld object
(3)Local Variables:a with 1,lineNo(行号)//局部变量
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zgoHTljC-1606109192075)(media/09b82d7fabde399adaf3f633569de718.png)]
Jdk6+
Intern()函数是将在堆中的字符串引用放到String
Pool中,返回的是同一个引用,但是如果String
Pool中已有该字符串则不会放入引用,直接返回常量池中引用。
new String("abc”)
Jdk6之前
只会将字符串对象添加到字符串常量池String s = “abc”,对于new
String(“abc”)intern方法只会拷贝副本进常量池。
通过intern()帮助理解Java 内存模型:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HmCljC5a-1606109192076)(media/a5845c2f03a7d2b6dd7d2165579b298f.jpeg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aFoL0XlM-1606109192077)(media/190cd5c06b19ee9eab4e549d777f6384.jpeg)]
6、运行时常量池
运行时常量池是方法区的一部分。
Class 文件中的常量池(编译器生成的字面量和符号引用)会在类加载后被放入这个区域。
除了在编译期生成的常量,还允许动态加载,例如 String 类的 intern()。
六、GC-java的垃圾回收机制
1、判断垃圾是否需要回收?
(1)引用计数算法
通过判断对象的引用数量来决定对象是否可以被回收。每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1。任何引用计数为0的对象实例可以被当做垃圾收集。
优点:执行效率高,程序执行受影响较小。(几乎不用打断程序执行)
缺点:无法检测出循环引用的情况,导致内存泄露。
主流GC并未采用这种算法。
(2)可达性分析算法
从GC root开始搜索,GC
root到达的路径叫做引用链,可达的对象都是存活的,不可达的对象可以被回收。
可作为GC Root的对象:
-
虚拟机栈中引用的对象(栈帧中的本地变量表)
-
方法区中的常量引用的对象
-
方法区中的类静态属性引用的对象
-
本地方法栈JNI(Native方法)的引用对象
-
活跃线程的引用对象。
方法区的垃圾回收:
1、常量池中废弃的常量:字面量和符号引用(没用被引用,则可以进行回收)
2、不再使用的类型(同时满足以下三个条件的类可被允许回收):
1)该类的所有实例都被回收了,即Java堆中不存在该类及其任何派生的子类的实例
2)该类的类加载器已经被回收了(除非精心设计,否则很难实现,如OSGI,JSP的重加载等)
3)该类对象对应的java.lang.Class对象没有在任何地方引用、无法在任何地方通过反射访问到该类的方法
关于是否要对类型进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class以及
-XX:+TraceClass-Loading、-XX:+TraceClassUnLoading查看类加载和卸载信息
https://www.cnblogs.com/jklixin/p/13457936.html#5、方法区的垃圾回收
2、垃圾回收算法
(1)标记-清除算法(Mark and Sweep)
标记:从根集合进行扫描,对存活的对象进行标记
清除:对堆内存从头到尾进行线性遍历,回收不可达对象内存。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G1s8rYr8-1606109192078)(media/0c6c3c4473a0f8d0e47c6408bfdf31cb.png)]
标记-清除算法缺点:
碎片化,内存没有被有效利用。
(2)标记-复制算法
分为对象面和空闲面,对象在对象面创建,存活的对象被从对象面复制到空闲面。然后将对象面所有对象内存清除。
标记-复制适用于对象生命周期短的对象,比如年轻代中对象。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3TvbHJfn-1606109192079)(media/1ef92bf5d8f2ffb6b6358297a873a1db.png)]
标记-复制优点:
1)解决碎片化问题;2)顺序分配内存,使用率高;3)适用于对象存活率低的场景。
缺点:
不适用老年代,会导致多次复制;空间利用率低,会有50%的空间作为空闲面。
(3)标记-整理算法
标记阶段:从根集合进行扫描,对存活对象进行标记。
清除阶段:移动所有存活的对象,且按照内存你地址次序依次排序,然后将末端内存地址以后的内存全部回收。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ofpDs9zQ-1606109192080)(media/76d8f5c89ee02379e7a4e8590c8d8abd.png)]
标记-整理算法优点:
1)避免内存的不连续性。2)不用设置两块内存空间互换。3)适用于存活率高的场景(适用于老年代的回收)。
缺点:需要移动内存
3、分代回收算法
垃圾回收算法的组合算法,按照对象生命周期的不同,划分区域以采用不同的垃圾回收算法。
提高JVM GC效率。
GC的分类:
Minor
GC:表示在新生代的垃圾回收,一般采用标记复制算法。因为这里的对象都是朝生夕灭的。
Major GC:由于业界发展,有可能是表示只对老年代的GC,也可能是对整个堆的回收。
Full GC:对整个堆进行回收。
老年代和年轻代:
(1)年轻代:尽可能快速地收集掉那些生命周期短的对象。
对象刚开始创建都是在年轻代的Eden区(Eden被挤满会触发一次Minor
GC),也有Eden区不够会创建在Survivor区。
Eden区+两个Survivor区
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vL72elD3-1606109192081)(media/3380e9cbe791840afa70f2376957b5e1.png)]
年轻代垃圾回收的过程演示:
(1)第一次Minor
GC:当Eden被填满,则会对Eden中存活的对象复制到Survivor区,存活时间+1,然后删除Eden所有对象。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bNn5VP9q-1606109192082)(media/7c087033c0ae990aaadbc13e13265c48.png)]
(2)Eden又满了,第二次Minor GC。
第一次Minor
GC将S0设为from,S1设为to。所以这次会将S0和Eden区所有的存活对象都复制到S1,并且存活时间+1。S1变成from,S0变成to区。Eden和S0被清空。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QziQkZSx-1606109192083)(media/fc6768cf8d1021be5a170f949a74dc19.png)]
(3)Eden满了,触发第三次Minor GC。
会将Eden和S1区所有的存活对象都复制到to区域S0,存活时间+1。然后将原来区域的对象全部清除。S0被设为from,S1被设为to。从此,周而复始。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CYdgOzFF-1606109192084)(media/3ab131e71930d4689a924a19e9d7f98b.png)]
对象晋升到老年代:
(1)-XX:MaxTenuringThreshold设置存活时间(经过的GC次数),存活时间达到一定数值后(默认为15),对象进入老年代。
(2)当然Survivor区都存放不下时,可能直接晋升老年代。
(3)新生成的大对象(-XX:+PretenuerSizeThreshold)超过这个size晋升老年代。
常用调优参数:
-XX:SurvivorRatio:Eden和Survivor的比值,默认8:1
-XX:NewRatio:老年代和年轻代内存大小的比例。
-XX:MaxTenuringThreshold对象从年轻代晋升到老年代经过的GC次数的最大阈值。
(2)老年代:存放生命周期长的对象。
Full GC和Major GC问清是否是老年代的回收,还是整个堆的回收。
Full GC比Minor GC慢,执行效率低。
触发Full GC的条件:
(1)老年代空间不足。
(2)永久代空间不足(对于jdk8之前才有的永久代)。
(3)CMS GC时出现promotion failed,concurrent mode failure(老年代空间不足)
(4)Minor GC 晋升到老年代的平均大小小于老年代的剩余空间
(5)调用System.gc()
4、垃圾收集器
了解垃圾收集器之前需要先了解的概念。
1、Stop-the-World
定义:JVM由于要执行GC而停止了应用程序的执行。(除了GC线程所有线程都处于等待状态,直到GC任务完成)
注意点
(1)Stop-the-world在任何一种GC算法中都会发生。
(2)多数GC优化就是通过减少stop-the-world发生的时间来提高程序性能。(提高吞吐,减少停顿)
2、Safepoint:
定义:就是程序停顿的时间点,各个对象引用关系不会发生变化的点。
产生Safepoint地方:方法调用;循环跳转;异常跳转等。
3、JVM的运行模式:
Server:重量级JVM,启动慢但运行效率高。
Client:轻量级JVM启动快
4、垃圾收集器:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4Nem1EUJ-1606109192085)(media/22ece680238cfa563a37e828d66ecddd.png)]
年轻代垃圾收集器
Serial收集器:(-XX:UseSerialGC,复制算法)表示启用该GC
(1)单线程收集,进行垃圾收集时,必须暂停所有工作线程。
(2)简单高效,Client模式下默认的年轻代收集器。
用户分配给GC的内存空间较少。
ParNew收集器(-XX:+UseParNewGC,复制算法)
(1)多线程收集,其余的行为、特点和Serial收集器一样
(2)单核效率不如Serial,多核下执行才有优势。(Server模式下默认收集器)
Parallel Scavenge收集器(-XX:+UseParallelGC,复制算法)
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
(1)运行在server模式下的默认的young GC
(2)比起用户线程停顿,更关注系统的吞吐量。
(3)在多核下执行才有优势
老年代的垃圾收集器:
Serial Old收集器(-XX:+UseSerialOld,标记-整理算法)
单线程收集,进行垃圾收集时,必须暂停所有工作线程stop-the-world
简单高效,Client模式下默认老年收集器。
Parallel Old收集器(-XX:+UseParallelOldGC,标记-整理算法)
多线程,吞吐量大
CMS收集器(-XX:+UseConcMarkSweepGC,标记-清除算法)
垃圾回收线程几乎能和用户线程同步工作,但是不能完全同步。
(1)初始标记:stop-the-world。从GC Root根集合进行扫描
(2)并发标记:并发追溯标记,程序不会停顿。
(3)并发预清理:查找、执行并发标记阶段中
从年轻代晋升到老年代的对象。(减少下一个阶段的标记)
(4)重新标记:stop-the-world暂停虚拟机,扫描CMS堆中的剩余对象。
(5)并发清理:清理垃圾对象,程序不会停顿。
(6)并发重置:重置CMS收集器的数据结构
最大的缺点就是使用标记-清除算法,如果有大的对象需要分配,则需要频繁的full
GC,因为碎片化的产生,还不一定能够分配空间。
G1收集器(-XX:+UseG1GC,复制+标记-整理算法)
在年轻代和老年代都可使用。
(1)并行和并发,使用多个CPU缩短stop-the-world的时间。
(2)分代收集,分年轻代和老年代的收集
(3)空间整合,基于标记-整理算法,不会发生空间碎片化的使用。
(4)可预测的停顿。收集时间可控制
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-59Oq1rXj-1606109192086)(media/7bebed7228a3323d9bbc18969afe8914.png)]
Garbage First收集器特点:
(1)将整个Java堆内存划分为多个大小相等的Region。
(2)年轻代和老年代不再是物理隔离的。分配空间不需要使连续的。
GC相关的面试题:
1、Object的finalize()方法作用是否与C++的析构函数作用相同。
(1)与C++析构函数不同,析构函数调用确定,而它的是不确定的。
(2)将未被引用的对象放置于F-Queue队列。
(3)方法优先级低会被终止
(4)finalize()方法只会执行一次,为对象提供最后一次存活的机会。
2、强引用、软引用、弱引用、虚引用。
(1)ReferenceQueue是在软、弱和虚引用被删除后,会被加入到引用队列中。无实际存储结构,存储逻辑依赖于内部节点之间的关系来表达。
(2)虚引用的定义:
虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。
(3)在SoftReference类中,有三个方法,两个构造方法和一个get方法(WeakReference类似):两个构造方法如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hrPTCgMg-1606109192086)(media/b904dc674a424b5b09ac8fc2b2618e32.png)]
get方法用来获取与软引用关联的对象的引用,如果该对象被回收了,则返回null。
应用场景:
(1)弱引用解决hashmap的OOM问题:
https://www.ibm.com/developerworks/cn/java/j-jtp11225/
(2)软引用解决图片缓存问题:
https://www.cnblogs.com/dolphin0520/p/3784171.html
(2)假如有一个应用需要读取大量的本地图片,如果每次读取图片都从硬盘读取,则会严重影响性能,但是如果全部加载到内存当中,又有可能造成内存溢出,此时使用软引用可以解决这个问题。
设计思路是:用一个HashMap来保存图片的路径 和
相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题。在Android开发中对于大量图片下载会经常用到。
private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String,
SoftReference<Bitmap>>();//将对象地址和缓存的对象,以键值对形式存入Map中即可。
总结何时使用软引用和弱引用:
(1)如果只是想避免OutOfMemory异常的发生,则可以使用软引用。如果对于应用的性能更在意,想尽快回收一些占用内存比较大的对象,则可以使用弱引用。
(2)还有就是可以根据对象是否经常使用来判断。如果该对象可能会经常使用的,就尽量用软引用。如果该对象不被使用的可能性更大些,就可以用弱引用。
七、如何查看线程状态,线程卡在那个地方。(检测死锁位置?)
方法1、使用jconsole连接到需要查看的进程。查看线程选项,点击检测死锁,点击查看详情。
方法2、jvisualvm连接远程主机加端口号。进入jvisualvm实时查看程序运行状态。
方法3、linux系统上,先ps -ef|grep java查看进程的中线程的pid,然后jstack -l pid
windows jps查看进程vmid,在cmd中输入 jstack -l 7412。
补充生成或导出dump文件:
导出整个JVM 中内存信息,导出堆存储文件
jmap -dump:format=b,file=文件名 [pid]
jstat查看堆内存,各部分使用情况,以及加载类的数量。
https://www.cnblogs.com/sxdcgaq8080/p/11089841.html
八、Java对象的创建对象的过程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PGbmhcvh-1606109192087)(media/1889aebcfa6bd846c5446d03d0248244.png)]
1、类加载检查。2、分配内存。3、初始化零值。4、设置对象头。5、执行init方法。
(1)类加载检查:虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
补充:符号引用和直接引用?
符号引用:以一组符号来描述所引用的目标,符号可以是任意形式的字面量,只要使用时能够无歧义的定位到引用目标即可。符号引用与虚拟机的内存布局无关,引用的目标不一定加载到内存。
直接引用:直接指向目标的指针、相对偏移量、定位到目标的句柄。
举例子:比如com.xjr.children类引用了com.xjr.parent类,这个com.xjr.parent类就是符号变量,编译时children可能不知道parent在哪,但是经过类加载的解析阶段,将符号引用转为直接引用(指针类型)。
通俗解释:
符号引用就是字符串,这个字符串包含足够的信息,以供实际使用时可以找到相应的位置。你比如说某个方法的符号引用,如:“java/io/PrintStream.println:(Ljava/lang/String;)V”。里面有类的信息,方法名,方法参数等信息。当第一次运行时,要根据字符串的内容,到该类的方法表中搜索这个方法。运行一次之后,符号引用会被替换为直接引用,下次就不用搜索了。直接引用就是偏移量,通过偏移量虚拟机可以直接在该类的内存区域中找到方法字节码的起始位置。
链接:https://www.zhihu.com/question/30300585/answer/51313752
(2)分配内存:
类加载检查通过后,接下来虚拟机为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,堆中划分内存。分配方式“指针碰撞”和“空闲列表”,分配方式有java堆是否规整决定,而java堆是否规整又由所采用的gc是否带有压缩整理功能决定。
标记-清除不规整,其他两种内存规整的。
方法一、指针碰撞:1、适用内存规整情况下。2、原理:用过的内存整合到一边,没有用过的内存放在另一边,中间有一个分界值指针,只需要向着没有使用的内存空间,移动对象大小的位置即可。—Serial、ParNew垃圾收集器
方法二、空闲列表:1、适用于堆内存不规整;2、原理:虚拟机会维护一个列表,记录哪些内存块是可用的,在分配时,找一块合适大小的划分给对象实例,最后更新列表记录。—CMS垃圾收集器
内存分配并发问题:
创建对象线程安全考虑,虚拟机采用两种方式来保证线程安全:
-
CAS+失败重试:虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。(自旋锁方式分配内存)
- TLAB:为每一个线程预先在Eden区分配一块内存,JVM在给线程中的对象分配内存时,首先在TLAB(栈上内存)中分配,当对象大于TLAB中剩余内存或TLAB内存用尽,采用CAS进行内存分配。
(3)初始化零值。
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一
步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型多对应的零值。
(4)设置对象头:
初始化零值之后,虚拟机对对象进行必要的设置,例如这个对象是哪个类的实例、元数据信息、对象哈希码、对象的GC分代年龄等信息。存放在对象头中。另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
对象头(9/22补充)
HotSpot虚拟机的对象头包括两部分信息:运行时数据和类型指针。
运行时数据:用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
类型指针:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8OGr4w8X-1606109192088)(media/0818b20b4864652cad4b9849961d9989.png)]
(5)执行init方法:
对于jvm来说新对象已经生成,对于java程序所有字段还为零。执行new指令之后接着执行init方法,把对象按照程序员意愿初始化。
Redis学习(7/20)
一、基础知识
1、redis默认有16个数据库,默认是第0个,通过select切换。
2、清除当前数据库内容:flushdb,清除所有数据库数据:FLUSHALL
3、redis是单线程的,CPU不是Redis性能瓶颈,Redis的瓶颈是根据机器的内存和网络带宽决定。
4、Redis是C语言写的,100000+的QPS(每秒查询率Queries-per-second)。
全称叫Remote Dictionary Server远程字典服务器。
5、redis中所有操作都是原子性的,不会出现像java中i++(被反编译出现分步计算的情况)
- redis所有数据,都是以字节数组byte[]形式存储的。
所以存储图片是可以的,以字节形式存储。
7、Redis缓存是指:电脑内存。
8、查看redis内存消耗:Info命令查看redis内存消耗指标:
Used_memory == redis分配器分配的内存总量
Used_memory_rss == 表示redis使用的物理内存总量
Mem_fragmentation_ratio:use_memory_rss / used_memory内存碎片率
大于1表示内存碎片过多,小于1表示旧数据,无效数据过多(需要进行内存交换)
9、redis存储都是key-value键值对形式,key唯一的,value的形式多种多样
String、list(列表)、hash(字典)、set(集合)、zset(有序集合);丰富的类型结果是redis具有突出优势的关键。
1、String:字符串,由字节数组byte[]组成。动态可修改,arraylist类似。
顺便复习一下char和byte的区别:
Char无符号型的占2字节,大小0-65535,能够表示中文,c = 65 ,print©—A。
Char和int转换,charint===char c1 = ’9’; int a = c1-‘0’;int char ===char c2 =
(char)a;
Byte有符号型占1字节,大小-128-127,不能表示中文。
- list链表。3、hash:redis的hash的value只能是字符串,rehash方式不同(新的hash生成时,会保留旧的hash,循序渐进的迁移到新的hash结构)渐进式hash。4、set:相当于hashset,value=null。
5、zset:有序列表,相当于是sortedSet和hashmap的集合体。一方面是一个set保证value的唯一性,另一方面value被赋予一个score(以double类型存储),代表value的排序权重。Zset的内部排序功能是通过“跳跃列表”数据结构实现的。Zset是由hash和skiplist构成的
1、Redis为什么单线程这么快?
-
误区1:高性能的服务器一定是多线程的?
- 误区2:多线程(CPU上下文切换)一定比单线程效率高?
速度:CPU>内存>硬盘
核心:redis是将所有的数据全部放在内存中,单线程操作效率最高。多线程(CPI上下文切换,耗时的操作),对于内存系统来说没有上下文切换,在同一个CPU上是最佳方案。
Worker线程时单线程的+多线程IO(IO是多线程的)?
-
纯内存操作,redis为了达到最快的读写速度,将数据都存在内存中,通过异步方式写入磁盘,如果不在内存中,磁盘io会严重影响速度。弊端:数据过多,占用内存空间。
-
Redis是单线程,避免线程上下文切换导致的额外开销。不存在多线程加锁释放锁的问题,避免死锁导致的性能消耗。
-
Redis独特的数据结构,操作简单快速。(memc只能存string,hash保存为json时,每次读数据必须全量查询,效率很低)
-
多路复用I/O模型。—(1)介绍了多路复用IO
-
将多个并发client连接抛给epoll异步执行,有事件发生调用回调函数,告诉redis有客户端io事件来临。(select的异步阻塞IO是需要不断对socket进行遍历轮询,哪些socket有事件来临)
-
多线程并行IO读取、写入。(服务器中一般不是单cpu,串行化IO读取不能完全利用多核cpu,所以就可以使用多线程并行IO读写)
-
Redis worker单线程计算(计算读取结果)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-psfaGh4V-1606109192089)(media/7acd2d844e555aade905f201647ee919.png)]
然后IO线程可以是多线程并行的。这样时间片就可以减少很多。
上面是传统redis串行化、下面是改进的redis充分利用多个cpu。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IWMGxMwJ-1606109192090)(media/0fafbebf21b828f9b1517b83caae5276.png)]
非阻塞IO中的多路复用IO(都属于系统调用):多路复用是用来应对client并发访问的
- select系统调用:
应用程序不断轮询(调用select,以对应的读写描述符作为参数)查找有哪些socket有读写事件的到来。弊端:应用程序需要循环遍历系统维护的socket列表,才能确定哪些有事件。比如:比如有10万个连接,
其中有事件的只有一个, 那就回有9999次无效的操作,
虽然这些无效的操作是由linux系统内核判断的。
8/20添加:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RucbIf8O-1606109192091)(media/a00d3e09ddabc82d41a9a3144619d2e1.png)]
(1)rset是一个长度为1024位的bitmap。表示哪个文件描述符被占用(哪些文件描述符需要被监听)。
(2)FD置位是指rset对应位置位。
文件描述符:
系统创建的每个进程默认会打开3个文件fd:标准输入(0)、标准输出(1)、标准错误(2)。跟select中自定义的fd[5]不冲突,声明的fd[5]意思是分配5个描述符给5个client使用。而自定义的fd[]的值就是文件描述符(0-2不可使用),文件描述符本质是file结构体里面的索引值。(所以才会从0开始)
- epoll系统调用:
应用程序不需要循环调用函数,只需要绑定一个系统回调,对端口监听进行绑定,在回调函数上阻塞等待事件来临。
epoll是将你需要监听的列表交给系统维护, 这样当有新数据来的时候,
系统知道这是你要的, 等你下次来拿的时候, 直接给你了, 少去了上面的系统遍历. 同时,
也没有select查询时那一大堆参数, 每次都只调用一次进行绑定即可。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5ibD69CI-1606109192091)(media/a72cb0f723cf28f62210cd8f1bb072af.png)]
关于《Redis深度历险》中的p90页指令队列和响应队列思考。
1、这个指令队列说的是redis的单线程的工作队列,先来先服务。
2、响应队列应该说的是IO多路复用,如果是epoll调用,socket给redis分配的write_fds,read_fds描述符,有数据就返回给回调函数。(他这个IO多路复用是双向的,redis到kernal是传数据回应回调函数;kernal到redis是读、写事件发生请求)
参考:
8/20号添加
epoll_create是在用户态和内核态创建一个共享区域。
epoll_wait 通过回调函数,内核会将 I/O
准备好的描述符加入到一个链表中管理,进程调用。
8/24号添加
为什么Redis用了自己新开发的SDS,而不用C语言的字符串?—这也是快的另一方面。
https://blog.csdn.net/qq_35190492/article/details/107039821
2、分布式锁?
对于分布式情况下,需要并发读并不关心会发生并发问题。但是需要进行并发修改时,则需要对先读取数据,再修改数据。那就会发生读取脏数据问题。这时需要对该条数据加分布式锁。
(1)Redis加锁命令:setnx key value就是setnx key value == set key value nx
其实 nx是个参数:代表key不存在就设置。
如果设置了 NX 或者 XX
,但因为条件没达到而造成设置操作未执行,那么命令返回空批量回复(NULL Bulk
Reply)。
(2)Redis释放锁:del key 或者 del key value
(3)为防止执行出错导致的死锁问题。加上超时释放expire lock:codehole 5
因为expire和setnx不是原子性操作。
将其结合set key value ex 5 nx
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kfKT0Fvz-1606109192092)(media/d43d240e0cb4296ae31f52bf9ad752d3.png)]
上述分布式锁无法解决超时任务问题。(如果有任务没在expire规定时间内执行完毕,锁就被释放了,不能严格串行化执行)
分布式锁需要解决的问题:
-
客户端修改互斥性
-
不同reids客户端修改,产生安全性。
-
有客户端没释放锁导致的死锁
-
容错
一、单机redis中,超时任务给出不是很完美的方案:
tag = random().nextInt();
//加个自旋
While(true){
if(Redis.set(key, tag, nx=True,3000)){ //获取锁成功
//进行任务
Do something();
//校验是否是本客户端key,如果是就释放锁
Redis.delifequals(key, tag);
}
Thread.sleep(100);
}
使用lua脚本保证(1、比较key和tag,2、释放锁)多个指令的原子性操作。
【保证校验删除的key是否是本客户端设置的key】
#delifquals
If redis.call(“get”,KEYS[1]) == ARGV[1] then
Return redis.call(“del”,KEY[1])
else
Return 0
End
redis锁的过期时间能够自动续期。(2020/8/11补充)
为了解决这个问题我们使用redis客户端redisson,redisson很好的解决了redis在分布式环境下的一些棘手问题,它的宗旨就是让使用者减少对Redis的关注,将更多精力用在处理业务逻辑上。
redisson对分布式锁做了很好封装,只需调用API即可。
RLock lock = redissonClient.getLock(“stockLock”);
redisson在加锁成功后,会注册一个定时任务监听这个锁,每隔10秒就去查看这个锁,如果还持有锁,就对过期时间进行续期。默认过期时间30秒。这个机制也被叫做:“看门狗”,
举例子:假如加锁的时间是30秒,过10秒检查一次,一旦加锁的业务没有执行完,就会进行一次续期,把锁的过期时间再次重置成30秒。
参考:https://www.cnblogs.com/youngdeng/p/12883790.html
二、Redis集群中分布式锁
Redis集群出现错误原因:主要发生在主从复制时,主从复制是异步的,也就是在ClientA获得锁后,在主redis复制数据到从redis时崩溃了,导致主redis数据没有复制到从redis中,在从redis仍然能够获取锁。
多节点redis实现的分布式锁算法(RedLock):有效防止单点故障。
假设有多个完全独立的redis服务器,没有主从之分。RedLock算法采用大多数机制。
1、获取当前时间戳
2、客户端尝试按顺序使用相同的key,value向所有的redis获取锁(获取锁时间远快于过期时间,超过获取时间走下一个)
3、client获取所有锁后,减去第一步时间戳,差值小于TTL(过期时间),并且有半数以上redis获取锁,代表加锁成功。
4、成功获取锁,有效时间需减去,获取所有锁时间和时钟漂移 。
5、client加锁失败,或任务完成释放锁时,向所有redis del lock释放锁。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5wt1FXhz-1606109192093)(media/fe8d2424c588d3215674f210d4b9023a.png)]
参考:https://www.cnblogs.com/rgcLOVEyaya/p/RGC_LOVE_YAYA_1003days.html
客户端竞争锁失败处理方法:
-
直接抛出特定异常,由用户决定是否重试。
-
Sleep当前请求
-
将该请求放入延时队列中(就是redis的list中,也可以用zset有序列表)。
redis用作简单的消息队列
redis中的list(列表)数据结构可用作消息队列,lpush、rpush表示左入队、右入队。
lpop、rpop表示左出队、右出队。一般lpush和rpop组合使用。
Redis作为消息队列使用时,使用阻塞读操作的blpop左阻塞出队、brpop右阻塞出队。防止在队列空的时候空轮询。导致QPS上升。
注意事项:(1)空闲时间过长,redis会断开连接,blpop和brpop就会抛出异常,需要手动捕捉异常。(2)如果使用的是sleep有可能会导致消息延迟增大。使得原来不需要等待的任务也等待了。
3、bitmap运用场景
0&0=0; 0&1=0; 1&0=0; 1&1=1;
0|0=0; 0|1=1; 1|0=1; 1|1=1;
0^0=0; 0^1=1; 1^0=1; 1^1=0;
Bitmaps并不属于Redis中数据结构的一种,它其实是使用了字符串类型,是set、get等一系列字符串操作的一种扩展,与其不同的是,它提供的是位级别的操作,从这个角度看,我们也可以把它当成是一种位数组、位向量结构。当我们需要存取一些boolean类型的信息时,Bitmap是一个非常不错的选择,在节省内存的同时也拥有很好的存取速度(getbit/setbit操作时间复杂度为O(1))。
- 常用命令
(1)Setbit key offset value
设置设置或者清空key的value(字符串)在offset处的bit值。当key不存在的时候,将新建字符串value。参数offset需要大于等于0,并且小于232(限制Bitmap大小为512MB)。没有setbit的位会默认设定为0.
(2)getbit key offset
返回key对应的string在offset处的bit值。当offset超出了字符串长度或key不存在时,返回0。
(3)BITCOUNT key [start end] 可用版本 >= 2.6.0
时间复杂度: O(N)
统计字符串被设置为1的bit数。需要注意的是,这里的start和end并不是位偏移,而是以字节(8位)为单位来偏移的,比如BITCOUNT
foo 0 1是统计key为foo的字符串中第一个到第二个字节中bit为1的总数。
(4)bitop operation res key1 key2 key3…
起始版本:2.6.0 复杂度是O(N)
对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 res 上。
BITOP 命令支持 AND 、 OR 、 NOT 、 XOR 这四种操作中的任意一种参数:
除了 NOT 操作之外,其他操作都可以接受一个或多个 key 作为输入。
- 应用场景
Bitmap实现统计活跃用户数:
可以给每个用户一个bitmap,名称就以userId命名。
-
设置userId的用户第一天签到:Setbit userId 1 1 、Setbit userId 364 1.
-
统计userId一年内签到次数:bitcount userId
-
统计第一季度(某个时间段)签到次数:bitcount userid 1 90
-
查看某一天签到:getbit userId 125
-
查看连续签到天数:byte[]取出来,后端使用二分查找方式找连续1的左端、右端相减。时间复杂度O(logn),或者一次遍历。
Bitmap实现记录用户的登录日志:
可以以时间作为key:userId存储用户信息,set 20200101:[userId]
(1)用户几天之内签到的情况。
三天之内签到的次数 bitop or threeor 20200101:userId 20200102:userid
20200103:userId
bitCount threeor
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6gRXUNRl-1606109192095)(media/c173bccae2cf64bd809e54b813ab64dd.png)]
Bitmap常见的应用场景之一就是用户签到了,在这里,我们以日期作为key,以用户ID作为位偏移(也就是索引),存储用户的签到信息(1为签到,0为未签到)。不过这个方法也是有前置条件的,那就是
userid 是整数连续的,并且活跃占比较高,否则可能得不偿失。
(这是一种比较节省的方案,所有用户按顺序排序只用一个bit位)
(1)给某一天用户签到:setbit 20200101:userid 1
(2)统计一天打卡情况:bitcount 20200101
(3)连续3天签到的用户数:bitop and threeand 20200101:userId 20200102:userid
20200103:userId
bitCount threeAnd
(4)三天之内签到的 bitop or threeor 20200101:userId 20200102:userid
20200103:userId
bitCount threeor
(5)一个月连续签到的,lua脚本写。
bitop(‘AND’, ‘monthActivities’’, $redis->keys(‘login:201903*’));
echo “连续一个月签到用户数量:” . $redis->bitCount(‘monthActivities’);
参考:
二、Redis 与 Memcached比较
二者都是非关系型内存键值数据库,主要有以下不同:
-
存储数据类型不同。redis支持5种数据类型存储string、list、hash、set、zset。Memc支持文本型、二进制型(字符串)
-
持久化。Redis可以通过RDB 快照和 AOF 日志持久化。Memc不支持。
-
分布式。服务器端Memcached
不支持分布式,只能通过在客户端使用一致性哈希来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。Redis Cluster 实现了分布式的支持。
-
内存管理机制。Redis会将很久不使用的value交换到磁盘。Memc一直在内存中。
Memc将内存分割成特定长度存储,会导致内存利用率不高。
三、LRU算法
LRU(Least Recently
Used)最近最少被使用,是一种页面置换算法,是为虚拟页式存储管理服务的。
算法思路:将访问的key放入队列中,队列已满时最早入队的就会被出队。如果入队的key被访问过
就会重新入队,删除原来位置上的key。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SRxfiicy-1606109192095)(media/09fd5096e48e54e833d7f3578278e707.png)]
一、LinkedHashMap实现:
获取LinkedHashMap中的头部元素(最早添加的元素):
时间复杂度O(1)
public <K, V> Entry<K, V> getHead(LinkedHashMap<K, V> map) {
return map.entrySet().iterator().next();
}
获取LinkedHashMap中的末尾元素(最近添加的元素):
时间复杂度O(n)
public <K, V> Entry<K, V> getTail(LinkedHashMap<K, V> map) {
Iterator<Entry<K, V>> iterator = map.entrySet().iterator();
Entry<K, V> tail = null;
while (iterator.hasNext()) {
tail = iterator.next();
}
return tail;
}
通过反射获取LinkedHashMap中的末尾元素:
时间复杂度O(1),访问tail属性
public <K, V> Entry<K, V> getTailByReflection(LinkedHashMap<K, V> map)
throws NoSuchFieldException, IllegalAccessException {
Field tail = map.getClass().getDeclaredField(“tail”);
tail.setAccessible(true);
return (Entry<K, V>) tail.get(map);
}
二、双链表+hashmap实现:
ListNode(int key,int value){
This.Key = key;
This.value = value;
Pre = null;
Next = null;
}
MoveToTail()
有点烦记不住的
redis中海量数据查询key前缀
1、使用命令查询:keys key1*
使用keys对线上的业务影响
Keys pattern:查找所有符合给定模式pattern的key
-
keys指令一次性返回所有匹配的key
-
键的数量过大会使服务卡顿
2、使用scan命令,无阻塞查询,返回部分结果
SCAN cursor [Match pattern][Count count]
-
基于游标的迭代器,需要上一次的游标延续之前的迭代过程。
-
以0作为游标开始一次新的迭代,直到命令返回游标0完成一次遍历。
-
不保证每次执行返回某个给定数量的元素,支持模糊查询
-
一次返回数量不可控,只能大概率符合count参数。
例如:scan 0 match k1* count 10
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ihFcf2ih-1606109192096)(media/fe4e2097f01adaa1e233d8593a6a8a96.png)]
111534336是游标值
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qiY9Ucfz-1606109192097)(media/349b31941f59761fcf386faaf80a7686.png)]
五、如何实现异步队列
使用list类型保存数据信息,rpush生产消息,lpop消费消息,当lpop没有消息时,可以sleep一段时间,然后再检查有没有信息,如果不想sleep的话,可以使用blpop,
在没有信息的时候,会一直阻塞,直到信息的到来(注意空闲时间长会断开连接)。redis可以通过pub/sub主题订阅模式实现一个生产者,多个消费者,当然也存在一定的缺点,当消费者下线时,生产的消息会丢失。
六、redis回收进程如何工作
1、当redis内存使用超过maxmemory(期望内存最大值),调用指定的淘汰策略。
2、直到淘汰的过期键值小于临界点。
淘汰策略:(1)不继续服务写请求。(del继续)默认,导致业务停顿。(2)volatile-lru:淘汰设置过期时间的key,最长最少被使用的淘汰(Linkedhashmap)。(3)volatile-ttl按过期时间,剩余寿命越小越先被淘汰。(4)volatile-random:随机淘汰过期集合key。(5)allkeys-lru全部key按lru规则淘汰。(6)allkeys-random所有key随机淘汰。
只对过期集合key淘汰volatile-xxx。对所有key淘汰allkeys-xxx
Redis过期策略:
-
Redis有一个过期key集合(独立字典),存储所有设置了过期时间的key。
-
删除策略:(1)定时删除(定时遍历过期key集合,然后进行删除);(2)惰性删除(等待客户端访问key时去判断key是否过期,过期就删除)
-
定时扫描过期集合(默认没秒10次)贪心(1)过期集合key随机取20个,删除超时key。(2)删除key超过1/4,继续取随机20个key判断删除。
1、为了防止同一时间有过多的过期key进行删除。我们可以手动在key的过期时间的基准上+random.randomint(86400)(一天的随机时间)
Redis.expire_at(key,random.randomint(86400) + expire_ts);
2、近似lru算法(只有惰性处理)
Redis的lru算法是一种近似lru算法,lru算法需要消耗大量的额外内存。
(在redis中写入新key时)近似lru给每个key加一个最后访问时间(时间戳24bit),然后随机采样5个(数量可以设置,越大越好10个)key,淘汰最旧的key,如果淘汰后内存还是超过maxmemory继续采样淘汰,直到小于阈值。
zset有序列表实现
zset是由hash和skiplist组成,value唯一,每个value增加了score(double)字段,按score从小到大排序,支持随机插入、删除,支持范围查询。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7aTGj0kD-1606109192098)(media/1fb33ac960693dc21b5856e734a4335a.png)]
1、Zset的使用命令:
(1)插入命令 :zadd books 9.0 “think in java”
【 ZADD key score1 member1 [score2 member2]】
(2)删除命令: zrem books “java”
(3)按score排序输出:
按score顺序输出,zrange books 0 -1(-1表示倒数第一个数)
按score逆序输出,zrevrange books 0 -1.
(4)输出个数:zcard books
(5)输出指定value的score:zscore books “java”
(6)获取按score的排名:zrank books “java”
(7)按score区间[0,8.91]遍历:zrangebyscore books 0 8.91
功能一、Zset支持随机插入、删除需要使用链表结构,有value和score组成的node节点。如果只是单链表结构时间复杂度为O(n)是否可以改进。
功能二、范围查询,支持范围查询需要使有序的并且前驱指针。
为了提高查找效率,我们采用多级索引。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Lg518YRA-1606109192099)(media/9cbe5f4a6f012c323cfa68f9cbb50544.png)]
以上链表加上多级索引就是skiplist。
2、查找节点思路:
从head节点最高层开始寻找,找到最后一个比node值小的数,然后到下一层继续找,直到找到node值相等的节点。还需要存储搜索路径。
Inode结构:
String value;
double score;
zsInode *[] forwards(level[]多层连接指针)
zsInode* backward 回溯指针
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xDbWDlWf-1606109192100)(media/bdc40f9ac9e143de370bd452bd450fa7.png)]
跳表查询时间复杂度:
有n个节点的链表,假如每两个节点构建索引,如上图。
第一层索引个数:n/2;
第二层索引个数:n/4;
…
第k层索引个数:n/2^k;
假设最高层2个索引,则k=logn
时间复杂度:
查找x过程:先从最高层向右遍历,发现没有比x小的值,走下一层遍历到y(最后一个小于x的值),向下走继续向右遍历就找到x节点。
因为从上一层下来的,每一层至多访问3个节点,k=logn层,时间复杂度为O(3*logn)
(看第一层只有3个索引,能确定一个范围,然后这个范围在下一次又只有3个索引,以此类推每一层最多访问3个节点。)
空间复杂度:
所有层的节点求和:等比数列求和。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bFI1avSv-1606109192101)(media/236e48e4153e16ab73e39e2147142d78.png)]
a1 = n/2,an = 2, q = 1/2, 求得 Sn = n-1,空间复杂度为O(n)
总之跳表是空间换时间的数据结构。
- 跳表的的插入操作(查询操作思路如上)
(1)先查询插入节点位置,存储搜索路径,计算跨度;
(2)然后根据随机算法生成一个随机层数,如果比skiplist最高层高,则更新之;
(3)创建新节点,自己的后向指针更新指向,更新新节点共同层数的之前节点的后向指针(因为插入节点了);
(4)更新搜索路径节点的高层节点的跨度,要加上插入的node(+1)
(5)更新node的回溯指针
- 删除操作
先找删除节点,将每个搜索路径上的节点,重排一下前向后向指针,同时注意更新skiplist的最高层数。
- 更新操作
如果修改的score值不改变排序顺序,不进行调整。否则采取先删除节点,再插入节点的操作。需要进行两次路径搜索。
3、跳表索引动态更新问题:
不停的对节点进行插入,但是不更新索引时,极端情况下可能会导致跳表退化成链表。
就像红黑树插入时需要动态调整树的平衡结构,以保证查询等的效率。跳表也需要为新插入的节点设置索引,维护跳表的查询、插入、删除的性能。跳表采取随机函数生成新节点所处于的索引层高度。例如,随机函数生成一个K层高度,意味着新节点添加到1-K级索引中。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ltqhm1Q7-1606109192102)(media/bb71319d4943bace34352f5dac38223f.png)]
4、跳表的rank排名如何实现的?
每个forward(向右指针)进行了优化,给forward指针都增加了span属性,span是“跨度”的意思,表示从当前指针跳跃到下一个节点中间会跳过多少个节点。Redis在删除、更新节点时会更新span的值。
计算一个元素的排名时,将搜索路径上所有节点的span’累加就可以计算出最终的rank值。
5、为什么Zset使用skiplist不使用红黑树?
1、skiplist不是很占用内存,具有灵活性,平衡树每个节点固定需要左右两个指针。而skiplist每个节点包含的指针数目平均为1/(1-p),具体取决于参数p的大小。如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。
2、范围查询可以直接按链表查询,平衡树操作相对困难(中序遍历)。
3、跳表易于实现和调试,Zrank只需要加上span属性,几乎不需要大的更改。
跳表小结:
-
查询、插入、删除效率logn级别
-
支持范围查询zrangebyscore books 0 8.91
-
跟红黑树crd性能一致,但红黑树不支持区间查找。
https://juejin.im/post/6844903446475177998#heading-2
zset实现延时队列:
使用sortedset,将当前时间戳作为score,消息内容作为key调用zadd来生产消息。消费者用zrangebyscore指令获取,当前时间N秒之前的数据,获取不到则轮询处理。
redis应用
缓存:redis做缓存。1、纯内存操作,查询、添加等操作效率很高;2、redis支持多种数据结构为业务中各种不同逻辑提供支持。
Redis应用中遇到的问题:
1、缓存穿透:
(大量查询一个不存在的key)查询数据库中不一定存在的数据。
如果每次都查询一个不存在value的key,由于缓存中没有数据,所以每次都会去查询数据库;当对key查询的并发请求量很大时,每次都访问DB,很可能对DB造成影响;并且由于缓存不命中,每次都查询持久层,那么也失去了缓存的意义。
解决方案:
-
对结果为空的情况也进行缓存,并且设置相对较短的过期时间防止太多空值占用内存。
-
限制对key的查询次数,redis中设置计数变量(简单的限流器)。
-
使用布隆过滤器防止缓存穿透,查询用户时先在布隆过滤器中查询,没有直接丢弃。
2、缓存雪崩:
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,都去数据库中查询,引起数据库压力过大甚至宕机。(有人说还有一种缓存击穿,是指并发查同一条数据)。
解决方案:
-
key的过期时间设置随机,防止同一时间出现大量key过期情况。
-
设置热点数据永不过期。
-
缓存失效后通过消息队列或加锁控制读数据库,其他写数据库等待。
3、缓存击穿:
缓存雪崩是由于大面积的key失效,打崩了DB。
缓存击穿是指,并发量非常高的热点key,在一瞬间过期了。然后非常大的并发量访问数据库。导致数据库承受不了。
缓存雪崩可以
(1)直接在接口处校验逻辑,比如做用户权限校验、参数做校验,不合法的参数直接return。比如id小于0的直接return。(对传入参数一定要保持不信任态度)
(2)也给这些k设置v为null,位置错误,稍后重试,这些问产品。但是过期时间设置短一些,比如30s。
(3)高级用法,使用布隆过滤器。布隆过滤是使用一个大型的位数组和几个不同的无偏hash函数(把元素hash值分的均匀,减少冲突)。将数据库中数据添加到布隆过滤器中。向里面添加值时,计算出各自的索引值往里面add。主要用来查询mysql中是否存在kv,非常快速。不存在直接返回,存在则去redis、数据库中查询,刷入KV中。
布隆过滤器的原理是,当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。
https://juejin.im/post/6844903982209449991
缓存击穿解决:
可以将缓存热点数据设置永远不过期。或者在过期的时候加上分布式锁。
参考:
redis持久化
RDB(快照)持久化:保存某个时间点的全量数据快照
AOF(Append-Only-File)持久化:保持写状态
1、RDB机制(快照,全量存储)
RDB其实就是把数据以快照的形式保存在磁盘上。什么是快照呢,你可以理解成把当前时刻的数据拍成一张照片保存下来。
RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘。也是默认的持久化方式,这种方式是就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为dump.rdb。如果系统发生故障,将会丢失最后一次创建快照之后的数据。如果数据量很大,保存快照的时间会很长。
RDB两种存储命令(手动触发指令):
-
SAVE:阻塞redis服务器进程,直到RDB文件被创建完毕(很少被使用)
-
BGSAVE:Fork出一个子进程来创建RDB文件,不阻塞服务器进程。(重要命令)后台保存的机制。
自动化触发RDB持久化的方式:
-
根据redis,conf配置里的SAVE m n定时触发(使用BGSAVE)
-
主从复制时,主节点自动触发
-
执行Debug Reload
-
执行shutdown且没有开启AOF持久化
分析BGSAVE原理:
-
执行BGSAVE后台只能有一个AOF/BGSAVE子进程,存在就返回错误。
-
没有子进程就触发持久化,调用rdbSaveBackground
-
执行fork指令,创建子进程执行rdb指令(fork进程负责写入一个rdb文件副本)。
-
主进程响应其他操作
Fork出来的子进程,快照持久化完全有子进程来执行,主进程继续处理客户端请求。
刚被fork出来的自己和父进程拥有共享内存里的代码段和数据段。
Copy-on-Write
防止主线程修改redis数据,造成子线程的持久化异常。Redis调用系统的COW机制,COW实际上就是当主线程需要对redis中的数据进行修改时,会复制这个页面下来,对这个页面进行修改。子进程还是原来的数据,所以持久化不受影响。
RDB缺点:
1、数据全量同步,数据量大会由于IO影响性能。
2、redis宕机就会失去正在持久化的数据。
2、AOF机制(默认是关闭的)
全量备份总是耗时的,有时候我们提供一种更加高效的方式AOF,工作机制很简单,redis会将每一个收到的写命令都通过write函数追加到文件中。通俗的理解就是日志记录。
问题:AOF日志在redis长期运行之后会变得很长。
日志重写解决AOF文件大小不断增大的问题,原理:
-
调用fork()创建子进程。
-
子进程把新的AOF写到临时文件,不依赖原来的AOF文件(对指令进行压缩写入)
-
主进程持续将新的变动同时写到内存和原来的AOF里
-
主进程获取子进程重写AOF的完成信号,往新的AOF同步增量变动。
5、使用新的AOF文件替换旧的AOF
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vX5M5j3v-1606109192103)(media/76aad585d40e14d1d3fc902ab8ab39b5.png)]
每当有一个写命令过来时,就直接保存在我们的AOF文件中。
Redis数据的恢复:
RDB和AOF文件共存情况下的恢复流程:
存在AOF情况下直接加载AOF,否则加载RDB。
RDB和AOF的优缺点:
RDB优点:全量数据快照,文件小(二进制数据),恢复快
AOF优点:可读性高,适合保存增量数据,数据不易丢失
AOF缺点:文件体积大,恢复时间长。
RDB-AOF混合持久化方式:
BGSAVE做镜像全量持久化,AOF做增量持久化(只对持久化开始-结束做AOF日志)
参考:https://baijiahao.baidu.com/s?id=1654694618189745916&wfr=spider&for=pc
RDB fork创建子进程。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W7xLt3uv-1606109192104)(media/b1eba20991b3af8c0610048132875229.png)]
双写一致性问题
1、那个双写一致性的删缓存、更新数据库,只是针对更新、插入操作多的。减少频繁修改缓存。要读取的时候直接去数据库查询。(删除时加分布式锁,防止发生重复修改)
2、但是读取的话没必要删缓存,直接读取即可。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-viWK1Fun-1606109192105)(media/e9d91d712a14effabce91907a3a1fc25.jpeg)]
参考: https://www.cnblogs.com/liuqingzheng/p/11080680.html
十一、Redis集群
1、是否使用过Redis集群,集群的高可用怎么保证,集群的原理是什么?
Redis
Sentinal着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。
Redis Cluster着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。
2、一致性哈希算法(9/20)
对于redis集群来说,如果保存文件的规则是随机分配的,我们需要遍历所有redis服务器,才能查询到。
所以我们采取按某一个字段值进行Hash值(取模)。但这种方式与服务器数量相关。
优点:简单性,(常用于数据库分库分表规则。一般采用预分区的方式,提前根据数据量规划好分区数。)
缺点:当节点变化的时候,如扩容或收缩节点,数据节点映射关系需要全部重新计算,会导致数据的重新迁移。
(自己理解:数据迁移只需迁移到前面一个服务器上,不需要将数据重新计算分配)
最后采用一致性hash算法(取模是对2^32取模,与服务器数量无关)。即,一致性Hash算法将整个Hash空间组织成一个虚拟的圆环,Hash函数的值空间为0~2^32-1。
优点:加入和删除节点只影响哈希环中顺时针方向的相邻节点,对其他节点无影响。
缺点:数据的分布和节点位置有关,这些节点不均匀的分布在哈希环上,所以数据存储不均匀。
第一步是排列服务器的位置。
整个圆环以顺时针方向组织,圆环正上方的点代表0,0点右侧的第一个点代表1,以此类推。
对各个服务器计算哈希值,具体可选择服务器ip或主机名作为关键字进行哈希,以确定服务器在哈希环上的位置。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6ieK01My-1606109192106)(media/4ff5cca5c6eaded35b632e1f692ae70b.png)]
第二步保存缓存数据
定位数据访问相应服务器,将数据Key使用相同的hash函数计算出哈希值,并确定此数据在环上的位置,从此位置沿环顺时针查找,遇到的第一个服务器就是定位到的服务器。
参考:https://blog.csdn.net/jnshu_it/article/details/84935964
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YnqwXRD2-1606109192108)(media/9da5903d6e392039b0225bcf9515d480.png)]
(1)一致性hash的容错性和扩展性:
当一个服务器节点宕机之后,只有hash环上一部分数据被重新定位到,前一个服务器节点。其他数据不受影响。
当我们新增加了一个服务器节点NodeX,那么服务器A、B上的数据无需变动,只需要将服务器B顺时针向前的,环上的部分数据重新定位到NodeX上即可。
**总结:**一致性Hash算法对于节点的增减都只需重定位环空间中的一小部分数据,有很好的容错性和可扩展性。
(2)数据倾斜问题
在一致性Hash算法服务节点太少的情况下,容易因为节点分布不均匀而造成数据倾斜(被缓存的对象大部分缓存在某一台服务器上)问题。比如,大量数据集中在节点A上,而节点B只有少量数据。为解决数据倾斜问题,一致性Hash算法引入了虚拟节点机制。即对每一个服务器节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。
具体操作可以为服务器IP或主机名后加入编号来实现。比如:Node B#1、Node B#2、Node
B#3。
数据定位算法不变,只需要增加一步:虚拟节点到实际点的映射。
所以加入虚拟节点之后,即使在服务节点很少的情况下,也能做到数据的均匀分布。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gwbcWY4f-1606109192109)(media/d5aa95d4174d6f4204f71f3f9c4a55d7.png)]
(3)redis集群中一致性hash算法的使用
一致性哈希算法的应用场景:
在做缓存集群时,为了缓解服务器的压力,会部署多台缓存服务器,把数据资源均匀的分配到每个服务器上,分布式数据库首先要解决把整个数据集按照分区规则映射到多个节点的问题,即把数据集划分到多个节点上,每个节点负责整体数据的一个子集。
Redis的集群模式使用的就是虚拟槽分区,一共有16383个槽位平均分布到节点上。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cDHx0QAc-1606109192110)(media/c666ba3e7fbce996d196aa8b4e70cec4.png)]
本质上还是第一种的普通哈希算法,把全部数据离散到指定数量的哈希槽中,把这些哈希槽按照节点数量进行了分区。这样因为哈希槽的数量的固定的,添加节点也不用把数据迁移到新的哈希槽,只要在节点之间互相迁移就可以了,即保证了数据分布的均匀性,又保证了在添加节点的时候不必迁移过多的数据。
MySQL学习(7/22没看成,7/29号上午11点)
MVCC多版本并发控制
MVCC(Multi-Version Concurrency
Control)多版本并发控制是MySQL的InnoDB存储引擎实现隔离级别的一种具体方式,用于实现读已提交、可重复读这两种隔离级别。读未提交级别总是读取最新的数据行,要求很低,无需使用MVCC。可串行化隔离级别需要对所有读取的行都加锁,单纯使用MVCC无法实现。
基本思想:
读写锁中读操作和写操作是互斥的,MVCC利用多版本思想,写操作更新最新的版本快照,而读操作去读旧版本快照,没有互斥关系,这点和java中CopyonWrite类似。
在MVCC中事务的修改(DELETE、INSERT、UPDATE)会为数据行增加一个版本快照。
脏读和不可重复读的根本原因是事务读取到其他事务未提交的修改。MVCC规定只能读取已经提交的快照。当然一个事务可以读取自身未提交的快照,不算脏读。
MVCC实现原理:
解决读写冲突,依赖3个隐式字段,undo日志,Read View实现的。
版本号、隐式字段:
-
系统版本号SYS_ID:是一个递增的数字,每开始一个新的事务,系统版本号自增。
-
事务版本号TRX_ID(越大越新!!!):事务开始时的系统版本号。
每行记录除了我们自定义的字段外,还有数据库隐式定义的字段:
DB_ROW_ID
6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引
DB_TRX_ID
6byte,最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务ID
DB_ROLL_PTR
7byte,回滚指针,指向这条记录的上一个版本(存储于rollback segment里)
实际还有一个删除flag隐藏字段,
既记录被更新或删除并不代表真的删除,而是删除flag变了
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LMYys31r-1606109192111)(media/e6008e4e43835787ad352f53caaabb9e.png)]
Undo日志:
MVCC多版本是指多个版本的快照,快照存储在Undo日志中,该日志通过回滚指针ROLL_PTR把一个数据行的所有快照连接起来。
Cyc20118上undo日志例子:1、插入t表,id和x字段;2、更新x=b;3、更新x=c。
MySQL如果没有使用START TRANSACTION,默认是AUTOCOMMIT自动提交。
没有commit,其他事务执行select语句看不到插入行。因为MySQL默认是RR事务隔离级别。
START TRANSACTION;
Sql
Commit;
INSERT、UPDATE、DELETE操作创建Undo日志,写入事务版本号TRX_ID。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xVHTuCPy-1606109192112)(media/8ed332da8073ef2090cb90ccaf45bf32.png)]
两种Undo日志:1、update undo log和2、insert undo log
Update和del时产生(事务回滚+快照读)1,insert时产生2.(事务回滚需)
对MVCC有用的是undate undo log
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LhJLK2SX-1606109192112)(media/18cc16accee0a4842809c14a570f04c2.png)]
ReadView(读视图)
Read View用来实现读快照,事务进行读操作时产生的读视图。Read
View遵循可见性判断,判断当前事务能够看到那个版本的数据,既可能是当前最新的数据,也有可能是undo
log里的某个版本的数据。
当前记录中TRX_ID不符合可见性,就通过ROLL_PTR回滚指针,取undo_log里最新版本。
Read
View维护的未提交的事务id列表(当前活跃事务id),有最小值TRX_ID_MIN和最大值TRX_ID_MAX。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BQMLrp2P-1606109192113)(media/8b6a6e8382538651f9224251c8284e39.png)]
进行select操作时[insert操作无关](select操作是快照读不加锁,需要验证TRX_ID排序,不包括select
for update,lock in share
mode等加锁操作,以及update\insert等),将当前行快照的TRX_ID与TRX_MIN和TRX_MAX关系进行比较,判断当前数据行快照是否可以读取:
-
TRX_ID < TRX_MIN_ID,表示该行数据在
所有未提交事务之前就更改了,因此是可用的。 -
TRX_ID > TRX_MAX_ID,表示该行数据 在所有未提交事务之后
才更改的,因此不可用。 -
TRX_ID_MIN <= TRX_ID <= TRX_ID_MAX,需要根据隔离级别再进行判断:
这里是MVCC实现提交读和可重复读的关键点。
-
读已提交:如果TRX_ID在事务id列表中存在,表明该事务还未提交,所以不可使用。不在列表中,表示当前事务已经提交,行数据可以使用。
-
可重复读:无论在不在列表中,都不可以使用行数据。因为未提交事务中可能会修改本行数据,当前事务再读取时发生不可重复读问题。
当前数据快照不可用时,根据ROLL_PTR回滚指针到undo
log中从大到小(从新到旧)找可用的版本快照。
因为这种机制只针对select快照读,不加锁,对于insert、update、delete不影响。所以可能发生幻读。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JCuynrW1-1606109192114)(media/66e5ad6c19e2265e2974256a83a08fab.png)]
快照读与当前读
快照读:就是纯select语句,不加锁。
当前读:MVCC
其它会对数据库进行修改的操作(INSERT、UPDATE、DELETE)需要进行加锁操作,从而读取最新的数据。可以看到
MVCC 并不是完全不用加锁,而只是避免了 SELECT 的加锁操作。
在进行 SELECT 操作时,可以强制指定进行加锁操作。以下第一个语句需要加 S
锁,第二个需要加 X 锁。
Select * from x where ? lock in share mode; //加读锁 |
---|
Select * from x where ? for update; //加写锁 |
参考:
-
concurrency control:https://scanftree.com/dbms/2-phase-locking-protocol
-
https://cyc2018.github.io/CS-Notes/#/notes
-
https://www.jianshu.com/p/8845ddca3b23(MVCC)
-
https://my.oschina.net/alchemystar/blog/1927425 (MVCC源码解析)
-
https://blog.csdn.net/fly43108622/article/details/99683621
(有书籍innodb引擎)
间隙锁gap lock类
Next-Key Locks 是 MySQL 的 InnoDB 存储引擎的一种锁具体实现。
MVCC 不能解决幻影读问题,Next-Key Locks
就是为了解决这个问题而存在的。在可重复读(REPEATABLE READ)隔离级别下,使用 MVCC
- Next-Key Locks 可以解决幻读问题。
1、Record Locks记录锁
锁定一个记录上的索引,而不是记录本身。(行锁,主键索引就是records
lock;键不明确就是全表锁(每一条聚集索引后加x锁))
SELECT c FROM t WHERE c = 1 FOR UPDATE;
如果表没有设置索引,InnoDB 会自动在主键上创建隐藏的聚簇索引,因此 Record Locks
依然可以使用。
2、Gap Locks间隙锁
可重复读隔离级别来解决幻读的问题。Gap
Lock在InnoDB的唯一作用就是防止其他事务的插入操作,以此防止幻读的发生。
锁定索引之间的间隙,但是不包含索引本身。例如当一个事务执行以下语句,其它事务就不能在
t.c 中插入 15。
SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;
3、Next-Key Locks(包括record和gap锁)
在默认情况下,mysql的事务隔离级别是可重复读,并且innodb_locks_unsafe_for_binlog参数为0,这时默认采用next-key
locks。所谓Next-Key Locks,就是Record lock和gap
lock的结合,即除了锁住记录本身,还要再锁住索引之间的间隙。
它是 Record Locks 和 Gap Locks
的结合在mysql中的具体应用,不仅锁定一个记录上的索引,也锁定索引之间的间隙。它锁定一个前开后闭区间,例如一个索引包含以下值:10,
11, 13, and 20,那么就需要锁定以下区间:
(-∞, 10]
(10, 11]
(11, 13]
(13, 20]
(20, +∞)
select * from user where user_id > 20 for update;(id=20也会被锁上)
1、即便是select * from user user_id=21 lock in share
mode;也会被阻塞,读锁竞争不过行锁。
2、update dept set LOC = “北京” where deptno = 3 。update操作也会被阻塞。
3、当然insert操作会被阻塞。
参考:
https://www.cnblogs.com/feiwuqianqi/p/13344476.html (讲锁的)
https://www.cnblogs.com/crazylqy/p/7773492.html
(总结)InnoDB中的record锁、gap锁、next-key锁。
1、对主键索引或者唯一索引会使用Gap锁吗?
(1)如果where条件全部命中,则不会用Gap锁,只会加记录锁。
当前读情况下,select from tb1 where id in(5,6,9,) lock int share mode
Where全部命中,会使用record锁。
Delete from tb1 where id=7顺利执行。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gIgRxC3K-1606109192115)(media/3dd8e58f3f285e9c6ea79057aef4c808.png)]
(2)如果where条件部分命中
当前读情况下,select from tb1 where id in(5,7,9,) lock int share mode
Id=7没有这条记录,会使用Gap锁,锁住(5,9)所有间隙。
Delete from tb1 where id=7会被block住。
2、Gap锁使用在非唯一索引或者不走索引的当前读中。防止幻读现象的发生。
在id=9周围的id=6和11会被锁住。
(1)非唯一索引
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hbgO4i36-1606109192116)(media/f9a5012fb0a429a1efe1e866765b553d.png)]
(2)不走索引
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OmsEivmZ-1606109192117)(media/b3396f585e356002951a3394cb847051.png)]
4、意向锁
①在mysql中有表锁,LOCK TABLE my_tabl_name READ;
用读锁锁表,会阻塞其他事务修改表数据。LOCK TABLE my_table_name WRITe;
用写锁锁表,会阻塞其他事务读和写。
②Innodb引擎又支持行锁,行锁分为共享锁,一个事务对一行的共享只读锁。排它锁,一个事务对一行的排他读写锁。
③这两中类型的锁共存的问题考虑这个例子:
事务A锁住了表中的一行,让这一行只能读,不能写。之后,事务B申请整个表的写锁。如果事务B申请成功,那么理论上它就能修改表中的任意一行,这与A持有的行锁是冲突的。
数据库需要避免这种冲突,就是说要让B的申请被阻塞,直到A释放了行锁。
数据库要怎么判断这个冲突呢?
step1:判断表是否已被其他事务用表锁锁表
step2:判断表中的每一行是否已被行锁锁住。
注意step2,这样的判断方法效率实在不高,因为需要遍历整个表。
于是就有了意向锁。在意向锁存在的情况下,事务A必须先申请表的意向共享锁,成功后再申请一行的行锁。在意向锁存在的情况下,
上面的判断可以改成
step1:不变
step2:发现表上有意向共享锁,说明表中有些行被共享行锁锁住了,因此,事务B申请表的写锁会被阻塞。
注意:申请意向锁的动作是数据库完成的,就是说,事务A申请一行的行锁的时候,数据库会自动先开始申请表的意向锁,不需要我们程序员使用代码来申请。
总结:为了实现多粒度锁机制(白话:为了表锁和行锁都能用)
对有行锁的表再申请表时需要每行检测行锁,为了提高检测效率,直接让行锁升级为表上的意向锁。
为了提高带有行锁的表的检测效率,在申请表锁时,让行锁升级为表上的意向锁(意向共享锁),申请表锁先检查意向锁,这样就提高效率。
隔离级别
读未提交(READ UNCOMMITTED)RU
事务中的修改即使没有提交,对其他事务也是可见的。
读已提交(READ COMMITTED)RC
一个事务只能读取已经提交的事务所做的修改。没有提交的事务,对于其他事务是不可见的。
其他事务可以update
可重复读(REPEATABLE READ)RR
保证同一个事务中多次读取同一个数据的结果是一致的。不让其他的事务修改update本事务处理的数据。
其他事务可以read、del、insert
可串行化(SERIALIZABLE)
强制所有事务串行化执行,多个事务互不干扰,不会出现并发一致性问题。
该隔离级别需要加锁实现,因为要使用加锁机制保证同一时间只有一个事务执行,也就是保证事务串行执行。
四、MySQL45讲学习(8/10、8/11、8/14)
01基础架构:一条SQL查询语句是如何执行的?
MySQL逻辑架构图:
MySQL 可以分为 Server 层和存储引擎层(提供读写接口)两部分。
Server层:包括连接器、查询缓存、分析器、优化器、执行器等,涵盖MySQL的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。
存储引擎:负责数据的存储和提取。
其架构模式是插件式的,支持InnoDB、MyISAM、Memory等多个存储引擎。现在最常用的存储引擎是InnoDB,它从MySQL5.5.5版本开始成为默认存储引擎。
比如在 create table 语句中使用 engine=memory, 来指定使用内存引擎创建表。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yd3mBbyb-1606109192118)(media/0e4ec250113d5d5d0596645f91ec3e2f.png)]
从图中不难看出,不同的存储引擎共用一个Server
层,也就是从连接器到执行器的部分。你可以先对每个组件的名字有个印象,接下来我会结合开头提到的那条
SQL 语句,带你走一遍整个执行流程,依次看下每个组件的作用。
连接器
第一步,你连接到这个数据库上,这时候接待你的就是连接器。连接器负责跟客户端建立连接、获取权限、维持和管理连接。客户端如果太长时间没有操作,连接器就会自动断开。时间参数wait_timeout控制默认是8小时。
长连接是为了防止频繁建立复杂连接。但是全部使用长连接之后MySQL占用内存特别快,因为MySQL在执行过程中临时使用的内存是管理在连接对象里面。只有连接断开时会释放资源。操作:定期断开长连接。
查询缓存
连接建立完成后,你就可以执行select语句了。执行逻辑就回来到第二步:查询缓存。
MySQL拿到一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。之前执行过的语句及其结果会以key-value对的形式,被直接缓存在内存中。Key是查询的语句,value是查询的结果。缓存中有key,value会被直接返回给客户端。
但是大多数情况下我会建议你不要使用查询缓存,为什么呢?因为查询缓存往往弊大于利。
除非查询表是静态表,很长时间才会更新一次。
好在 MySQL 也提供了这种“按需使用”的方式。你可以将参数 query_cache_type 设置成
DEMAND,这样对于默认的 SQL
语句都不使用查询缓存。而对于你确定要使用查询缓存的语句,可以用 SQL_CACHE
显式指定,像下面这个语句一样:
Select SQL_CACHE * from T where ID=10;
需要注意的是,MySQL 8.0 版本直接将查询缓存的整块功能删掉了,也就是说 8.0
开始彻底没有这个功能了。
分析器
没有命中查询缓存,真正执行语句。
分析器先会做“词法分析”。你输入的是由多个字符串和空格组成的一条SQL语句,MySQL需要识别出里面的字符串分别是什么,代表什么。
MySQL
从你输入的"select"这个关键字识别出来,这是一个查询语句。它也要把字符串“T”识别成“表名
T”,把字符串“ID”识别成“列 ID”。
然后做“语法分析”。根据词法分析结果,语法分析器会根据语法规则,判断你输入的这个SQL语句是否满足MySQL语法。
如果你的语法不对,就会收到“You have an error in your SQL syntax”的错误提醒。
优化器
经过了分析器,MySQL就知道你要做什么了。在开始执行之前,还要经过优化器的处理。
优化器是在表里有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。
但是执行的效率会有不同,而优化器的作用就是决定选择使用哪一个方案。
执行器
MySQL通过分析器知道了你要做什么,通过优化器知道了该怎么做,就进入执行器阶段,开始执行语句。
开始前先判断有没有相关权限。执行器就会根据表的引擎定义,去使用这个引擎提供的接口。
比如select * from T where ID=10;
-
调用InnoDB引擎接口取这个表的第一行,判断ID值是不是10,如果不是则跳过,如果是则将这行存在结果集中;
-
调用引擎接口取“下一行”,重复相同的逻辑判断,直到取到这个表的最后一行。
-
执行器将上述遍历过程中所有满足条件的行组成的记录集作为结果集返回给客户端。
rows_examined
的字段,表示这个语句执行过程中扫描了多少行。这个值就是在执行器每次调用引擎获取数据行的时候累加的。
Oracle、MySQL会在分析阶段判断语句是否正确,表是否存在,列是否存在等。
02基础架构:一条SQL更新语句执行流程?
1、执行语句连接数据库,使用连接器。
表有更新时,这个表的查询缓存会失效。
2、分析器经过词法分析、语法分析得知该语句是更新语句
3、优化器决定使用主键索引。
4、执行器负责具体执行,找到这一行然后更新。
更新流程涉及两个重要的日志模块,redo log(重做日志)和binlog(归档日志)。
每次更新都要磁盘IO,将修改直接写到磁盘上。这会浪费大量IO、查找时间。
MySQL使用WAL技术,WAL技术全称是Write-Ahead Logging,关键点是先写日志,再写磁盘。
当有一条记录需要更新的时候,InnoDB引擎就会先把记录写到redo
log(固定大小的)里面,并更新内存,这时就算是完成了。InnoDB引擎会在适当时候,将这个操作记录更新到磁盘里面。
InnoDB的redo
log是固定大小的,比如可以配置为一组4个文件,每个文件的大小是1GB,那么redo
log可以记录4GB的操作。从头开始写,写到末尾又回到开头循环写。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KeTBKvUZ-1606109192120)(media/7d52a3a655126e41a4e00db15c1e88c1.png)]
Write
pos是当前记录的位置,一边写一边后移,写到第3号文件末尾后就回到0号文件开头。checkpoint是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。Write
pos追上checkpoint时需要停下来擦除记录,先推进checkpoint更新到磁盘。
有了redo
log,InnoDB就可以保证即使数据库发生异常重启,之前提交的记录也不会丢失,这个能力称为crash-safe。
重要的日志模块:binlog
MysQL整体来看分为Server层,主要做的是MySQL功能层面的事情;还有一块是引擎层,负责存储相关的具体事宜。redo日志是InnoDB引擎特有的日志,Server层也有日志,称为binlog(归档日志)。
这两种日志的三点不同之处:
-
redo
log是InnoDB引擎特有的;binlog是MySQL的Server层的实现,所有引擎都可以使用。 -
redo
log是物理日志,记录的是“某个数据页上做了什么修改”;binlog是逻辑日志,记录的是语句逻辑。 -
redo
log循环写,空间固定会用完;binlog是可以追加写入的。**“追加写”**是指bonlog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
执行器和InnoDB引擎在执行这个简单的update语句时的内部流程。
-
执行器先找引擎取ID=2这一行。ID主键,引擎直接用树搜索找到这一行。如果ID=2这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
-
执行器拿到引擎给的行数据,把这个值加上1,比如原来是N,现在就是N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
-
引擎将这行新数据更新到内存中,同时将这个更新操作记录到redo log里面,此时redo
log处于prepare状态。然后告知执行器执行完成了,随时可以提交事务。 -
执行器生成这个操作的binlog,并把binlog写入磁盘。
-
执行器调用引擎的提交事务接口,引擎把刚刚写入的redo
log改成提交(commit)状态,更新完成。
将 redo log 的写入拆成了两个步骤:prepare 和 commit,这就是"两阶段提交"。
两阶段提交就是将redo log和binlog日志的写入放在一个事务中。先写入redo
log完成后redo
log处于prepare状态,而不是直接commit(如果两份日志分别提交,就有可能出现不一致性),等到binlog日志写入之后,再对redo
log进行提交。【维护了两份日志的逻辑一致性】
小结:
1、redo log 用于保证 crash-safe 能力。innodb_flush_log_at_trx_commit
这个参数设置成 1 的时候,表示每次事务的 redo log
都直接持久化到磁盘。这个参数我建议你设置成 1,这样可以保证 MySQL
异常重启之后数据不丢失。
【crash-safe能力就是MySQL异常重启之后数据能够从redo log恢复】
2、sync_binlog 这个参数设置成 1 的时候,表示每次事务的 binlog
都持久化到磁盘。这个参数我也建议你设置成 1,这样可以保证 MySQL 异常重启之后
binlog 不丢失。
3、redo
log记录数据页做了什么改动(实际修改的值);binlog有两种模式,statement格式是记录sql语句,row格式会记录行数据。并且记录更新前和更新后的。
4、redo
log存放了数据页的修改;undo日志是存放多个版本的快照,通过回滚指针ROLL_PTR串联在一起。
5、两阶段式提交,类似事务保证两次日志内容的一致性。先是redo
log写好之后,通知执行器自己处于prepare状态,不直接提交。等binlog写入之后,再对redo
log进行提交。
03事务隔离:为什么你改了我还看不见?
MySQL 是一个支持多引擎的系统,但并不是所有的引擎都支持事务。比如 MySQL 原生的
MyISAM 引擎就不支持事务,这也是 MyISAM 被 InnoDB 取代的重要原因之一。
为什么建议你尽量不要使用长事务?
长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,导致大量占据空间。
*(补充)MySQL如何实现事务的ACID特性的?
1、原子性(事务当中操作要么都做,要么都不做)
MySQL是利用innoDB的undo log。
Undo
log是回滚日志,是实现原子性的关键,当事务回滚时能够撤销所有已经执行成功的sql语句。并根据undo日志中的信息将数据回滚到修改之前的样子。
-
delete一条数据,需记录这条数据信息,回滚时insert这条数据。
-
update一条数据,记录旧数据信息,回滚时update旧数据信息。
-
insert之后,又要回滚时delete该条数据。
2、一致性(事务只会从一个一致性状态,到另一个一致性状态,也就是说事务从始至终都该是维护数据一致性的)
首先ACID中的一致性,需要原子性、隔离性、持久性满足的基础上再谈一致性。
如果原子性无法保证,则一个事务内的操作就不能保证数据前后一致。隔离性不满足就可能发生脏读等。持久性不满足可能在MySQL重启后,按日志恢复时,恢复数据前后不一致。
3、隔离性(事务之间互不相干,不可以操作另一个事务中的数据)
MySQL使用锁机制和MVCC多版本并发控制,实现隔离性。
(1)对数据进行修改、插入和删除,当前读时对数据加锁。
(2)(只针对快照读)MVCC通过判断当前事务id是否在Read
View读视图中(当前没提交的事务id集合中),如果在这集合最小事务id以前,说明已提交事务,当前快照的数据可以使用;如果大于集合最大事务id则不能使用。
特殊的就是在集合之内,分两种情况。1)读已提交。如果事务id在未提交集合内,数据不能使用。事务id不是集合内事务id则可以使用。2)
可重复读。不管事务id是否存在read
view维护的当前未提交事务集合中,都是不可以使用当前快照的数据的。
不能使用当前快照,就按回滚指针到undo log中向上回溯找新的一个快照,再进行判断。
4、持久性(数据从内存中持久化到磁盘)
**结论:**MySQL使用WAL技术,WAL技术全称是Write-Ahead
Logging,关键点是先写日志,再写磁盘。【redo日志+binlog日志】
**简述:**MySQL中使用InnoDB中的redo
log和server层的(在执行器过程中执行binlog)binlog,两种日志文件(两阶段提交),维护持久化的一致性。
**具体:**数据库需要从磁盘中读取数据,并在内存中修改,然后再写回磁盘。如果为了保证数据库宕机而,在事务结束前直接写入内存这种方式。
**直接刷盘的问题:**很耗时、耗费性能,每次都必须把数据页全部重新写回磁盘,数据页写入是随机IO速度慢。
(1)只修改一个页面里的一个字节,就要将整个页面刷入磁盘,太浪费资源了。毕竟一个页面16kb大小,你只改其中一点点东西,就要将16kb的内容刷入磁盘,也不合理。
(2)毕竟一个事务里的SQL可能牵涉到多个数据页的修改,而这些数据页可能不是相邻的,也就是属于随机IO。显然操作随机IO,速度会比较慢。
使用redo log来进行持久化。
首先Redo log存储的物理日志,数据页修改了什么,不需要存储整个数据页。其次redo
log是有固定大小,并且每次写入都是顺序写入修改。超过存储则将内容写入磁盘内。然后还和binlog存储逻辑日志sql语句等结合使用,并且先写redo
log
写完处于prepare状态,等待binlog执行完毕,再提交事务commit。两阶段提交保证一致性。
参考:https://www.cnblogs.com/youngdeng/p/12855672.html
04索引
-
哈希索引适用于等值查询。
-
有序数组适用于静态存储引擎。(插入、删除数据移动很麻烦)
3、搜索树—InnoDB的索引模型B+树
每一个索引在InnoDB里面对应一棵B+树。
比如,主键为ID,字段k,k上有索引。
Create table T(
Id int primary key,
K int not null,
Name varchar(16),
Index(k))engine=InnoDB;
R1-R5表示每一行的数据。(ID,k)对应(100,1)、(200,2)等。主键ID有一棵B+树,叶子节点包含每行的数据,是聚簇索引。字段k有索引所以也有一棵B+树,叶子节点是k对应的主键值。为非主键索引,二级索引。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1hvHE00S-1606109192120)(media/3f6e65b683043e499a727096fa47d9a9.png)]
基于主键索引和普通索引查询有什么区别?
(1)如果是语句select * from T where
ID=500,即主键查询的方式,则只需要搜索ID这课B+树;
(2)如果语句时select * from T where
k=5,普通索引查询方式,则需要先搜索索引k的B+树,得到ID值为500,再到ID索引树搜索一次。这个过程称为回表。
回到主键索引树搜索的过程,我们称为回表
面试题:数据量大,二级索引搜索会快?
一个innoDB引擎的表,数据量非常大,根据二级索引搜索会比主键搜索快,文章阐述的原因是主键索引和数据行在一起,非常大搜索慢,我的疑惑是:通过普通索引找到主键ID后,同样要跑一边主键索引?
答:数据量很大的时候,二级索引比主键索引更快”,这个结论是只有在使用覆盖索引时才成立,非覆盖索引还是要回表查询。(覆盖索引,查询结果是二级索引中叶子节点的主键ID,所以不需要回表查ID了)
索引维护:
插入新记录可能导致页分裂,影响数据页利用率。删除数据时也会页合并
自增主键or不使用自增主键?
自增主键是指自增列上定义的主键,在建表语句中一般是这么定义的: NOT NULL PRIMARY
KEY AUTO_INCREMENT。
-
自增主键,插入新记录,属于递增插入属于追加操作。使用自增主键可以使得二级索引的B+树的叶子节点存储量减少,叶子节点占4字节。
-
非自增主键,不保证有序插入,写成本高并且插入时可能导致页分裂。
显然,主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。
**什么时候使用业务字段做主键?**1、只有一个索引;2、该索引是唯一索引。典型的KV场景。
补充:为什么B+树可以减少磁盘访问?
查找数据时,需要从根节点向下访问,访问到一个节点就是一整个数据块,也就是一个索引,索引不只存在内存,还需要从磁盘中读取。如果我们是使用二叉树存,需要访问的数据块就很多。所以使用B+树,它是N叉树,每一节点好多个主键值,顺序排列的。磁盘IO读取一个数据块之后有更大概率找到所需值。B+树更矮搜索更快。
我思考了一个问题:
联合索引可以使用覆盖索引,到达减少回表,提高查询效率。
1、联合索引B+树长什么样子?
2、最左匹配原则在其中起到什么作用?
1、联合索引B+树(非叶节点上也是按顺序排列的索引字段)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fr0GlTFh-1606109192121)(media/a034057830dde9a9bd004df3238736e4.png)]
非叶子结点也应该是col1、col2、col3三个索引。该图是错误的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bQ86Yus7-1606109192122)(media/8ab4d7009ee85eeaaf798df32c33ef56.png)]
图5-2是正确的联合索引的B+树。
参考:https://www.jianshu.com/p/35588ecf33c1
1.1覆盖索引是啥?
如果执行的语句是 select ID from T where k between 3 and 5,这时只需要查 ID
的值,而 ID 的值已经在 k
索引树上了,因此可以直接提供查询结果,不需要回表。也就是说,在这个查询里面,索引
k 已经“覆盖了”我们的查询需求,我们称为覆盖索引。
由于覆盖索引可以减少树的搜索次数,显著提升查询性能,所以使用覆盖索引是一个常用的性能优化手段。
2、利用最左匹配原则,可以减少为特定查询建立冗余索引。
最左匹配原则不只是匹配索引的全部定义,只要满足最左前缀,就可以利用索引来加速检索。这个最左前缀可以是联合索引的最左
N 个字段,也可以是字符串索引的最左 M 个字符。
比如:有一个高频根据身份证号查询姓名请求,还有一个根据身份证号查询地址。只需要建立(身份证号,姓名)的索引即可,一来减少高频请求的回表查询,而来减少身份证号查地址的全表查询(现在只需要根据身份证号回表查询即可
)。
索引下推优化是在索引遍历过程中,先对索引内部含有的字段做筛选,直接过滤掉不满足条件的记录,减少回表次数。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1QwjEF0k-1606109192123)(media/92ac326e12a7827158f0144e5d8d922a.png)]
拓展:
CREATE TABLE `geek` (
`a` int(11) NOT NULL,
`b` int(11) NOT NULL,
`c` int(11) NOT NULL,
`d` int(11) NOT NULL,
PRIMARY KEY (`a`,`b`),
KEY `c` (`c`),
KEY `ca` (`c`,`a`),
KEY `cb` (`c`,`b`)
) ENGINE=InnoDB;
ca索引和cb索引有必要吗?
-
order by ca索引,表示先按c排序,再按a排序。
-
order by c,表示按c排序,之后a是主键还得按a排序。
-
order by cb索引,表示先按c排序,再按b排序。
所以cb索引保留,ca索引可以删除。
06索引、慢查询、锁等补充
全局锁就是锁数据库实例。使用场景:数据库全局逻辑备份。
1、为什么要使用索引
在进行数据查询时一定会根据其中的一个或者某几个字段的值来选择,首先假如不使用索引信息,也就是没有对数据的索引信息进行组织和管理,那么当我们想要从数据库中找到学号为xxxxxx的学生,需要与数据库表中的各条数据逐行对比,才能找到匹配的数据。这样的查询效率比较低,时间复杂度为O(n)。
因此,如果考虑对数据中的索引采取一定的组织方式的话,比如将数据按照学号的大小顺序排列,这样在查找学号为xxxxxx的学生时,就可以使用二分查找,查找的时间复杂度就变为O(logn),不过采用二分查找数据需要放在连续空间不利于数据的插入和删除操作。所以在数据库中的索引通常按照B+树的结构存放,查询任意一条数据的时间复杂度都为O(logn)。
建立索引是将O(n)复杂度降低到O(logn)复杂度。
使用索引就是为了在查询数据时避免全表扫描,提高查询效率
2、B树、B+树:
B树非叶子结点可能存储数据,所以一层存储的节点数不能过大。相对于B+树来说高度较高。
B+树非叶节点存储索引(指向叶子节点的指针,不是地址指针),叶节点存数据(不一定是具体data,可以是地址),那么非叶节点层就可以存储大量索引,因此B+树较矮,查询就快。(查询从根出发,到叶子节点结束)。
结论:B+树比B树支持范围查询。磁盘读写代价更低。B+查询效率稳定。
3、聚集、非聚集索引:(cluster聚簇索引、聚集索引翻译区别,一个意思。)
聚集索引:数据的物理排列方式与聚集索引的顺序相同,只能建立一个聚集索引。
聚集索引能够获取到全部列得到数据。(MySQL的MyISAM除外,此存储引擎的聚集索引和非聚集索引只多了个唯一约束,其他没什么区别)
地址 | id | username | score |
---|---|---|---|
0x01 | 1 | 小明 | 90 |
0x02 | 2 | 小红 | 80 |
0x03 | 3 | 小华 | 92 |
… | … | … | … |
0xff | 256 | 小英 | 70 |
非聚集索引:该索引中索引的逻辑顺序与磁盘上行的物理存储顺序不同,一个表中可以拥有多个非聚集索引。
一般指非主键索引,也有人把非主键索引建聚集索引。。。
注:除了聚集索引以外的索引都是非聚集索引,只是人们想细分一下非聚集索引,分成普通索引,唯一索引,全文索引。如果非要把非聚集索引类比成现实生活中的东西,那么非聚集索引就像新华字典的偏旁字典,他结构顺序与实际存放顺序不一定一致。
聚集索引相当于是新华字典的拼音排序,非聚集索引相当于是偏旁部首排序(不按实际顺序查,按逻辑顺序查的)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hm8u7egV-1606109192124)(media/4762afc27a2edab3f267ee7933d55ab3.png)]
参考:https://www.cnblogs.com/s-b-b/p/8334593.html
4、密集索引和稀疏索引区别:
密集索引文件中的每个搜索码值对应一个索引值。(每个索引指向一个数据)
(1)
对于聚集索引来说,如果存在具有相同搜索码值的记录,会顺序存储在第一条数据记录之后。那么在稠密索引中只需要记录相同搜索码值的第一条记录的指针即可,其余的可以依据物理位置找到。
(2)
对于非聚集索引,由于其索引的顺序与记录的物理存储顺序不一致,无法根据存储位置找到具有相同搜索码值的其它记录,因此即使多个记录的搜索码相同也要分别为它们建立索引项
稀疏索引文件只为索引码的某些值建立索引项。(首个索引指向一个数据,之后按数据排列顺序确定)
在稀疏索引中不会为每一个搜索码值都建立索引项,而是采用一种区间的方式,每个索引项对应的指针指向的是大于等于当前搜索码值的记录的位置,然后根据该位置再进行下一步搜索。这也就要求数据记录的物理存储必须按照当前索引的顺序存储,那么,也就是说只有聚集索引才能使用稀疏索引。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N3DBJapj-1606109192125)(media/acd5537de3538b8dfe150d8703445011.png)]
参考:https://www.cnblogs.com/sasworld/p/11517707.html
5、慢查询优化:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CcEysaOR-1606109192126)(media/e0113da9b7e350c55e9edc6ebbd89335.png)]
Explain分析查询语句:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FsCtgfKo-1606109192127)(media/8cfe4fc7c84307215757eb7caf5e86be.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l8ie4X7U-1606109192128)(media/023ed61067ba502e088e6678628d6d13.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xjSLj9B2-1606109192128)(media/fbb804235bb1a4f258d8de9ea56f17bc.png)]
Type中index_merge表示采用了多个索引合并优化的方法。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-icU29Jnx-1606109192129)(media/929a0735b7d73304274881a101106be6.png)]
其中先看key表示是否使用了索引,type联结类型是否是ALL(all的话说明是全表查询),再看possible_keys是否大于3条。是否发生覆盖索引(是否需要回表查询看Extra
是否是 using index)。
注:sid,cid的联合索引,如果where条件中只有cid是不走索引的;如果是使用or连接where条件也不走索引;like
“%”开头也不走索引。(必须要’xu%’才会走索引)
1、只查询testid=1001,不走联合索引。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mVfno95g-1606109192130)(media/94d9952d7a659961d2431ab416777624.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zSTqq4IR-1606109192131)(media/c426e9f23f718baeb5786865d87605d0.png)]
2、使用or连接不走索引。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SfDb30Qp-1606109192132)(media/5592f29b668db64830b8028c990730eb.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HtPyHtbF-1606109192133)(media/00503b2b8cee6fab53baefd25ccd0663.png)]
https://www.cnblogs.com/xpp142857/p/7373005.html
6、索引建立越多越好?
(1)数据量小不必要建立索引。O(n)、O(logn)没啥区别
(2)频繁更新的表和列,索引也需要更新、变动,因此开销大。
(3)维护更多的索引也会占用更多的空间。(有时数据量20G,索引有40G完全本末倒置了)
7、联合索引的最左匹配原则
如前所述,不仅单个字段可以成为索引,多个字段的组合也可以成为索引。但是在使用多个字段组成的索引时,需要满足最左匹配原则。
1、最左前缀匹配原则,MySQL会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a
= 3 and b = 4 and c > 6 and d = 6,如果建立的索引为(a, b, c,
d),那本次查询是不使用索引的(因为优化器把d=6放到c>6之前匹配了,所以匹配不到a,b,d的索引),如果建立的索引是(a,
b, d, c)则会使用该索引,a,b,d的顺序可以任意调整。
2、=和in可以乱序,比如a = 3 and b = 4 and d = 6 ,索引(a, b,
d)是可以使用的。(server端的优化器优化执行顺序的)
3、联合索引只要where中出现最左边的索引,就可以使用联合索引。
8、数据库锁的分类
读锁
读锁又被称为共享锁,因为一个会话对数据库进行读操作不会阻塞其它会话的读操作,但是会阻塞其它会话的写操作。
在进行select操作的后面加上for update将会添加排它锁,加lock in share
mode会添加共享锁
写锁
写锁又被称为排它锁,当一个会话给数据库加了写锁后会阻塞其它会话中所有的读和写操作。
显示添加读/写锁
lock tables TABLE_NAME read/write;
unlock tables 删除锁
页级锁
介于表级和行级锁之间,锁定位于被操作数据相邻的数据。
页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁.表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。BDB支持页级锁
意向锁
当事务A锁住了表中的某一行时,事务B如果想要申请整张表的写锁,这是因为事务A对数据行加了行锁,如果让B申请成功那么就会与A发生冲突。因此在给事务B分配表锁时需要判断当前数据库表有没有被其它事务加过行或者表锁,那么就需要进行如下判断:
1:判断表是否已被其他事务用表锁锁表
2:判断表中的是否有任意一行已被行锁锁住。
这样的话在第2步中需要对整张表中记录进行判断,效率比较低。于是就有了意向锁,在意向锁存在的情况下,任何事务对表或者行进行上锁前都必须先获取意向锁,有了意向锁,上面的检查过程就变为:
1:不变
2:获取表的意向锁
申请意向锁的动作是数据库完成的,事务进行上锁时数据库会自动先申请表的意向锁,不需要我们程序员使用代码来申请
悲观锁
当我们要对一个数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制。
乐观锁
乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。
参考:https://www.cnblogs.com/sasworld/p/11517707.html
行级锁与死锁
MyISAM中是不会产生死锁的,因为MyISAM总是一次性获得所需的全部锁,要么全部满足,要么全部等待。而在InnoDB中,锁是逐步获得的,就造成了死锁的可能。
在MySQL中,行级锁并不是直接锁记录,而是锁索引。索引分为主键索引和非主键索引两种,如果一条sql语句操作了主键索引,MySQL就会锁定这条主键索引;如果一条语句操作了非主键索引,MySQL会先锁定该非主键索引,再锁定相关的主键索引。
在UPDATE、DELETE操作时,MySQL不仅锁定WHERE条件扫描过的所有索引记录,而且会锁定相邻的键值,即所谓的next-key
locking。
当两个事务同时执行,一个锁住了主键索引在等待其他相关索引,一个锁定了非主键索引,在等待主键索引。这样就会发生死锁。
发生死锁后,InnoDB一般都可以检测到,并使一个事务释放锁回退,另一个获取锁完成事务。
数据库层面的避免死锁,这里只介绍常见的三种
1、如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。
2、在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率;
3、对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率;
参考:https://www.cnblogs.com/123-shen/p/11361664.html
9、数据库索引不命中情况
(1)模糊查询like ‘%abc’ 匹配字符出现在首位。
(2)where 子句中使用
or连接不会命中索引。(想要命中索引必须将or连接的所有关键词都加上索引)
(3)负向条件都不会使用索引。 例如:not in、not like、not exists。
(4)当B+索引中使用is NULL不会走索引,因为索引中不存NULL,is not
NULL会使用索引。
(5)where 字段是一个函数运算不会走索引。
等等
https://www.jianshu.com/p/19d329aee7f2
10、内连接和外连接和等值连接的区分?
内连接和等值连接:
-
Select * from t1 inner join t2 on
t1.id=t2.id;(inner不加也行),是将两个表在id上的交集输出。 -
当然 t1 join t2 on t1.id <> t2.id不等号也行。所以等值连接时内连接的子集。
外连接和内连接:
1、内连接是两个集合的交集,左外连接是两个集合的交集加上左边有的数值。
11、MyISAM与InnoDB关于锁方面的区别是什么?
1、MyISAM默认用的是表级锁,不支持行级锁。
2、InnoDB默认使用行级锁,也支持表级锁。
1、innodb和myisam中加上和解除读锁的命令:
Lock table videos read;
如果不加unlock,就会一直加着共享锁。
Update 会被block住。
InnoDB显式给select加共享锁、写锁。
Select * from video where id = 3 lock in share mode;
Select * from video where id = 3 for update;
2、InnoDB中的加锁过程。
(1)Innod中采用二段锁方式,加锁和解锁是分成两个步骤来进行。
先对一个事务里的同一批操作进行加锁。然后到commit的时候对加的锁统一的进行解锁。(InnoDB中默认事务自动提交,看起来和MyISAM一样)
(2)select操作中,如果where中使用的字段没有索引,则会对全表加锁。
(3)意向锁IS IX
MyISAM适合场景
(1)频繁执行全表count语句,一个变量保存rows
(2)对数据进行增删改的频率不高,查询频率高
(3)没有事务
InnoDB适合场景:
(1)增删改查,行锁
(2)事务
数据库锁的分类:
(1)锁的粒度划分,分为表级锁、行级锁、页级锁(bdb按块划分)
(2)锁的级别划分,可分为共享锁、排它锁。
(3)加锁方式划分,自动锁、显式锁。(自动锁增删改、意向锁)
(4)按操作划分,可划分为DML锁、DDL锁(alter table对表结构进行操作)
(5)使用方式划分,可分为乐观锁、悲观锁
Java中IO学习(10/16)
一、概览
Java的I/O大概可以分成以下几类:
磁盘操作:File
字节操作:InputStream和OutputStream
字符操作:Reader和Writer
对象操作:Serializable
网络操作:Socket
新的输入/输出:NIO
二、磁盘操作
HTTP相关协议学习(7/29)
一、HttpServletRequest
HttpServletRequest对象代表客户端的请求,当客户端通过HTTP协议访问服务器时,HTTP请求头中的所有信息都封装在这个对象中,通过这个对象提供的方法,可以获得客户端请求的所有信息。
1、获得客户端信息:
getRequestURL() | 返回客户端发出请求时的完整URL。 |
---|---|
getRequestURI() | 返回请求行中的参数部分。 |
getQueryString () | 方法返回请求行中的参数部分(参数名+值) |
getRemoteHost() | 返回发出请求的客户机的完整主机名。 |
getRemoteAddr() | 返回发出请求的客户机的IP地址。 |
2、获得客户端请求头:
方法 | |
---|---|
getHeader(string name) | 以 String 的形式返回指定请求头的值。如果该请求不包含指定名称的头,则此方法返回 null。如果有多个具有相同名称的头,则此方法返回请求中的第一个头。头名称是不区分大小写的。可以将此方法与任何请求头一起使用 |
getHeaders(String name) | 以 String 对象的 Enumeration 的形式返回指定请求头的所有值 |
getHeaderNames() | 返回此请求包含的所有头名称的枚举。如果该请求没有头,则此方法返回一个空枚举 |
3、获得客户机请求参数:
getParameter(String name) | 根据name获取请求参数(常用) |
---|---|
getParameterValues(String name) | 根据name获取请求参数列表(常用) |
getParameterMap() | 返回的是一个Map类型的值,该返回值记录着前端(如jsp页面)所提交请求中的请求参数和请求参数值的映射关系。(编写框架时常用) |
二、HTTP、RESTful
超文本传输协议(HTTP)是一种用户分布式、协作式和超媒体信息系统的应用层协议。HTTP是万维网的数据通信的基础。
表现层状态转换(REST)。RESTful 是目前最流行的 API 设计规范,用于 Web
数据接口的设计。
URI
统一资源标识符(Uniform Resource
Identifier,URI)是一个用于标识某一互联网资源名称的字符串。URI的最常见的形式是统一资源定位符(Uniform
Resource
Locator,URL),经常指定为非正式的网址。更罕见的用法是统一资源名称(Uniform
Resource
Name,URN),其目的是通过提供一种途径。用于在特定的名字空间资源的标识,以补充网址。在RESTful架构中
URI 不应该有动词,动词应该放在HTTP协议中。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N8AUI3Lu-1606109192134)(media/376bd7732a90331b7ff7f7d943ce7c17.png)]
HTTP请求报文分析:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zhLCRPR3-1606109192134)(media/cd86f8fbd1c2d3a0580e8917a607c343.png)]
请求行:1、method,客户端发送报文的方法GET方法,表示请求页面信息;POST请求服务器将指定内容作为请求的url中从属实体。2、URI,表示请求的页面地址,/doc表示doc目录下的test.html。3、http的版本。
请求头部:
1、HOST:连接的目标主机,连接的服务器是非标准端口,可能出现使用的非标准端口。
2、Accept:请求的对象类型。如果是“/”表示任意类型,如果是指定的类型,则会变成“type/”。
3、Accept-Language:使用的语言种类。
4、Accept-Encording:页面编码种类。
5、Accept-Charset:页面字符集。
6、User-Agent:提供了客户端浏览器的类型和版本。
7、Connection:对于HTTP连接的处理,keep-alive表示保持连接,如果是在响应报文中发送页面完毕就会关闭连接,状态变为close。
HTTP响应报文分析:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4DmbHmUb-1606109192135)(media/71c4b0d5df52bf4a27b525d9bd8c2c8f.png)]
响应报文也分为两部分。前两行称为状态行,状态行给出了服务器的http版本,以及一个响应代码。响应代码是服务器根据请求进行查找后得到的结果的一种反馈,共有5大类。分别以1、2、3、4、5开头。
1**表示接收到请求,继续进程,在发送post后可以收到该应答。
2**表示请求的操作成功,在发送get后返回。
3**表示重发,为了完成操作必须进一步动作。
4**表示客户端出现错误。
5**表示服务器出现错误。
Server表示服务器软件版本,date标注当前服务器时间,content-type表示了应答请求后返回的内容类型。Content还有内容长度和内容语言以及内容编码三个项,其中内容长度只有在请求报文中的connection值为keep-alive时才会用到。
GET和POST
GET用于获取资源,POST用于传输实体主体。
GET和POST都可以添加参数,GET添加在url内,POST添加在请求实体当中(request
body),不能认为POST比GET更安全都可以看到,中文字符需编码。
1、GET /test/demo_form.asp?name1=value1&name2=value2 HTTP/1.1
2、POST /test/demo_form.asp HTTP/1.1
Host: w3schools.com
name1=value1&name2=value2
Cookie产生:
当服务器收到(没有cookie的)请求报文后,针对request
method作出响应动作,在响应报文的实体部分,加入了set-cookie段,set-cookie段中给出了cookie的id,过期时间以及参数path,path是表示在哪个虚拟目录路径下的页面可以读取使用该cookie,将这些信息发回给客户端后,客户端在以后的http
request中就会将自己的cookie段用这些信息填充。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jr0HcPZQ-1606109192136)(media/59cf996361c2730cd32050f020f40450.jpeg)]
参考:https://www.cnblogs.com/wxf-h/p/10519670.html
URL的设计
1.1 动词 + 宾语(名词复数)
例如:GET /api/articles
错误示范:/getAllCars,/createNewCar
1.2避免多级URL,除了第一级,其他级别都用查询字符串表达
GET /authors/12/categories/2(不好)
GET /authors/12? categories=2(推荐)
例如:查询已发布的文章
GET /articles/published(不好)
查询字符串的写法明显更好。
GET /articles?published=true(推荐)
提供链接
API 的使用者未必知道,URL
是怎么设计的。一个解决方法就是,在回应中,给出相关链接,便于下一步操作。这样的话,用户只要记住一个
URL,就可以发现其他的 URL。这种方法叫做 HATEOAS。
HTTPS
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0nuiA7D5-1606109192137)(media/cb7f3d4ba96c5ce37f339bbfe42db675.jpeg)]
区别 | HTTP | HTTPS |
---|---|---|
协议 | 运行在 TCP 之上,明文传输,客户端与服务器端都无法验证对方的身份 | 身披 SSL( Secure Socket Layer )外壳的 HTTP,运行于 SSL 上,SSL 运行于 TCP 之上, 是添加了加密和认证机制的 HTTP。 |
端口 | 80 | 443 |
资源消耗 | 较少 | 由于加解密处理,会消耗更多的 CPU 和内存资源 |
开销 | 无需证书 | 需要证书,而证书一般需要向认证机构购买 |
加密机制 | 无 | 共享密钥加密和公开密钥加密并用的混合加密机制 |
安全性 | 弱 | 由于加密机制,安全性强 |
进行 HTTPS
通信时,服务器会把证书发送给客户端。客户端取得其中的公开密钥之后,先使用数字签名进行验证,如果验证通过,就可以开始通信了。
HTTPS具有加密+认证+完整性保护的功能:
1、采用混合加密,客户端先向服务端发送非对称加密的公钥,服务端使用公钥对传输私钥(对称加密)进行非对称加密,客户端使用自己的私钥解密出私钥(对称加密的)。
2、认证,服务端向CA机构(数字证书认证机构)购买数字认证,将证书和密钥放一起,发送给客户端,客户端通过数字签名进行验证,验证成功可开始通信。
3、完整性保护,SSL提供报文摘要功能。使得无法篡改报文内容。
HTTP2.0
1、为什么说HTTP是无连接?
无连接是指应用层的,每次客户端给服务端发送http请求,服务端给客户端回复http应答,之后就断开连接。采用这样的方式可以节省传输时间。
长连接和短连接也是指tcp连接的状态。Tcp连接的短连接也是成功传输一次就断开连接。http1.0协议默认采用短连接,提出长连接的概念。http1.1默认采用长连接。
tcp的长连接是客户端与服务端连接一直保持,不停地传输http数据。
长连接的响应头会加上Connection:keep-alive。
补充解释:
http的无连接是强调http的特性,tcp的短连接可以称之为一种实现。
具体解释:
原来采用http1.0时,无连接的实现便是一次http请求和回复之后,就会断开tcp连接。后来http1.1默认采用了长连接,使得客户端与服务端保持tcp连接,对于http来说即便是在同一个tcp连接中,不同http请求之间也没有联系的。发送一次请求收到一次请求从应用层面来说已经本次请求已经结束了。
参考:https://segmentfault.com/a/1190000015821798?utm_source=tag-newest
2、为什么说HTTP的是无状态的?
http无状态是指每一次请求都有自己的请求体,也获得对应的响应体。上一次请求和下一次请求并没有联系的,哪怕是在同一个tcp连接中。http协议下并不包括session、cookie和application这些存储状态和信息的单位,只是用session来弥补http的无状态。
从tcp是有状态角度来说,tcp分割大的数据包,并且会打上序号,在接收端接收到数据包后会按顺序拼接起来,这说明每次发送的数据包都是有前后顺序的。(http中尽管有chunked判断文件发送完毕一说,但应该是建立在tcp分割发送之上。)
3、HTTP2 可以提高网页的性能。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lW8NHRB1-1606109192138)(media/2fa67faeae4ddca75cfd5bbdcb82e787.png)]
(1)多路复用
在 HTTP1 中浏览器限制了同一个域名下的请求数量(Chrome
下一般是六个),当在请求很多资源的时候,由于队头阻塞当浏览器达到最大请求数量时,剩余的资源需等待当前的六个请求完成后才能发起请求。
HTTP2 中引入了多路复用的技术,这个技术可以只通过一个 TCP
连接就可以传输所有的请求数据。多路复用可以绕过浏览器限制同一个域名下的请求数量的问题,进而提高了网页的性能。
(2)首部压缩
HTTP/1.1 的首部带有大量信息,而且每次都要重复发送。
HTTP/2.0
要求客户端和服务器同时维护和更新一个包含之前见过的首部字段表,从而避免了重复传输。
不仅如此,HTTP/2.0 也使用 Huffman
编码对首部字段进行压缩。(哈夫曼编码:权值小的在底部,权值大的在顶部。权值小的相加,组成根节点)是采用了HPACK头部压缩算法
(3)二进制分帧(改进传输性能,减低延迟和高吞吐量)
在二进制分帧层中, HTTP/2
会将所有传输的信息分割为更小的消息和帧(frame),并对它们采用二进制格式的编码
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YsIc5nLL-1606109192139)(media/18fa4b8bbd7ec5ff75d6d99bb843f122.png)]
(4)服务端推送
HTTP/2.0
在客户端请求一个资源时,会把相关的资源一起发送给客户端,客户端就不需要再次发起请求了。
4、HTTP响应头keep-alive如何实现长连接(连接保活)?
*http使用content-length判断静态文本发送完毕,判断不了长度使用Transfer-Encoding。
(1)http中keep-alive
timeout时间(一般是处在服务器端响应报文头)表示服务器在使用tcp发送完最后一个http响应,再等待超时时间后httpd守护进程再关闭tcp连接。
(2)tcp协议的keep-alive保活机制。当客户端和服务端建立连接之后很长时间没有响应(空闲了keep_alive_time后),服务端就会发送空的心跳包(侦测包),没有收到ack回复就会继续等待一个逐渐增长的时间,继续发送心跳包。在发送了指定次数之后,客户端依然没有回答就断开tcp连接。
【书面化语言:如果没有收到对方的回答(ack包),则会在
tcp_keepalive_intvl后再次尝试发送侦测包,直到收到对方的ack,如果一直没有收到对方的ack,一共会尝试
tcp_keepalive_probes次,每次的间隔时间在这里分别是15s, 30s, 45s, 60s, 75s。】
参考:
-
https://blog.csdn.net/wwq0813/article/details/90256058 request详解
4、https://www.zhihu.com/question/34074946 http2.0改进
5、https://www.cnblogs.com/zxmbky/p/10281152.html keep-alive
输入url发生了什么?
http从浏览器(客户端)到服务端,发生了什么?
完整的URL格式包括:
协议名://用户名:密码@子域名.域名.顶级域名:端口号/目录/文件/文件名.后缀?参数=值#标志
输入网址:https://www.jianshu.com/p/4aa3bb16f36c
(1)Https标示使用协议名称。每个协议都有固定通信规则,协议规定浏览器通过何种内容格式获取想要的文件。
(2)网络地址:www一般是标识提供的是网络服务(可以不用管),www.jianshu.com(依次为
二级域名.顶级域名,整体是个二级域名)
(3)区分顶级域名(一级域名)、二级域名、三级域名,要用一句话概括:除去www以外,名前边没有点的就是顶级域名(http://xxx.com),有一个点就是二级域名(http://a.xxx.com),有两个点就是三级域名(http://b.a.xxx.com)。
(4)资源路径/p/4aa3bb16f36c,以”/”开始,表示服务器根目录下p文件夹中。
将上述地址输入完毕,浏览器会先在浏览器缓存中查找对应ip,如果没有就去系统缓存的host文件找,如果没有去路由器缓存找,如果还没有则通过查询DNS服务器转换域名为ip。
- 建立TCP连接
HTTP协议的底层依靠的是TCP连接,获得ip之后,浏览器跟服务器建立tcp连接,tcp连接时面向连接的,可靠的传输协议。需三次握手:(1)client主动发送连接请求给server。(2)server被动打开,如果能连接server返回响应报文(请求确认报文ack)。(3)client发送连接确认报文,建立连接。【三次保证client确实能够连接上,不会在第一次request后就不能连接,提高了连接的可靠性】
- 浏览器向服务器发送http请求
HTTP是应用层协议,浏览器会按http协议规则,生成http报文格式封装在tcp报文中。http报文一般分为请求头和请求体,头中包括协议版本HTTP1.1、请求方法GET、POST、目标URL、请求数据格式、客户端是否需要cookie等。
Session和Cookie区别?
Cookie在客户端,是服务端颁发给浏览器的认证信息(类似缓存),服务端会在响应http报文中body部分增加set-cookie字段,给出cookieId、expire过期时间、path访问目录地址。浏览器下次使用时,会在请求的cookie段使用这些信息填充。
(1)会话cookie只保存在内存中,浏览器关闭即消失。(2)持久是因为设置expire过期时间,存储到硬盘中。(3)cookie不能跨域名,浏览器访问淘宝不能使用qq的cookie。
Session在服务端,是服务端记录客户状态的机制,每个用户都有一个sessionId。比如使用httpServletRequest.getSession()方法获取session并且setAtrribute等。
传输时按照应用层http报文、传输层(封装成tcp报文,可能对数据包进行拆分发送,加上tcp报头)、网络层(加上ip报头)、数据链路层、物理层传输电信号(二进制比特流bit)。
秒杀项目中用户登录之后,就会携带token进行请求,token的作用是什么?
客户端频繁的想服务端请求数据,会导致服务端频繁地取数据库查询用户信息进行校验。这样对服务端性能造成影响。因此token产生,token是服务端生成的用户唯一凭证,发放给客户端,客户端每次只需要携带token来访问就可以校验该用户的身份。
Token是为了减轻服务端压力,减少频繁的查询数据库,是服务器更加健壮。
-
服务器接收HTTP请求
数据包到达服务器会按照物理层、数据链路层、网络层、传输层、应用层将数据包拆封恢复到http报文格式,再进行解析。
-
服务器响应HTTP请求并返回浏览器
服务器上操作系统发现Tomcat监听8080端口,所以会将http请求发给tomcat处理。Tomcat是一个servlet容器,servlet是运行在Web服务器上的程序,它是作为浏览器(客户端请求)和HTTP服务器数据库(应用程序)之间中间层。tomct解析http请求报文,会生成httpServletRequest和httpServletResponse对象。httpServletRequest封装请求资源,Tomcat将httpServletRequest转发给servlet,并通过servlet的service()方法处理请求(向数据库查询),生成的结果由httpServletResponse传给Tomcat,Tomcat获取内容生成HTTP响应报文(http协议版本、状态码、响应数据等),tomcat转交给服务端封装后,由tcp传输回客户端,层层解封装后,由浏览器渲染成页面。
5、浏览器完成一次完整的请求并受到回复,此时tcp等待下一次请求或者关闭连接。关闭连接通过4次挥手。
参考:
https://www.cnblogs.com/8023-CHD/p/11067141.html
https://www.cnblogs.com/l199616j/p/11195667.html
https://www.jianshu.com/p/21b5a26dc2cc
三次握手:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MhQecmdP-1606109192140)(media/ee43e6493e308138873e9bf4a97920a7.png)]
四次挥手:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NpA7tCNZ-1606109192141)(media/b269dbed1221a3ae1a9c1a34adcd077f.png)]
【问题1】为什么比三次握手多出一次?
因为在服务端发送关闭请求确认时,不会立刻关闭连接,而是需要将剩余的数据发送完毕,再断开连接。传送完毕之后,服务器会发送
FIN 连接释放报文。
也就是问为什么要close-wait?
【问题2】为什么client需要等待2MSL(MaximumSegmentLifetime,最大报文段生存时间)?
-
必须假想网络是不可靠的,有可能最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文,如果
server没收到 client 发送来的确认报文,那么就会重新发送连接释放请求报文。 -
等待一段时间是为了让本连接出现的所有报文都从网络中消亡,使得下一个新连接中不会出现旧连接的报文。
也就是问为什么client是time-wait状态,不直接是closed状态?
TCP拥塞控制:
附上udp报文格式、tcp报文格式:
TCP:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D1WHnY8Y-1606109192141)(media/af9f337f909ab02d9114451a96de4ccb.png)]
确认号是用来确认连接是否建立的。连接确立ACK=1,ACK始终为1.
UDP:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uvfwsyrA-1606109192142)(media/c39f511a297e016eaea02bbbd52553f4.gif)]
UDP没有号SequenceNumber这一项。
每个 UDP 报文分为 UDP 报头和 UDP 数据区两部分。报头由 4 个 16 位长(2
字节)字段组成,分别说明该报文的源端口、目的端口、报文长度和校验值(可以检验数据在传输过程中是否被损坏)。
IP报文:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J9jxaH8H-1606109192143)(media/6d9888589f706ca9cfa9e6a4d9f20a98.png)]
物理层:
-
单工通信:单向传输
-
半双工通信:双向交替传输
-
全双工通信:双向同时传输
参考:
https://www.cnblogs.com/shineyoung/p/10656914.html tcp
http://c.biancheng.net/view/6440.html udp
https://blog.csdn.net/bobozai86/article/details/87518617 ip
https://blog.csdn.net/ThinkWon/article/details/104903925 计算机网络面试题
计算机网络学习(8/4)
TCP滑动窗口
该窗口是缓存一部分,用来暂时存放字节流。
在发送方有一个发送窗口,接收方有一个接收窗口,发送方根据接收方在tcp报文段中的窗口字端设置自己的窗口大小。如果接收方设置为0,则不能发送。
发送窗口内的字节都允许被发送,接收窗口内的字节都允许被接收。发送窗口内的最左边
字节收到确认就向右滑动。接收方的窗口内最左边字节确认收到就向右滑动,接收字节只有接收到窗口最左边的字节才滑动,接收到其他部位的不滑动。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I3ylFgSQ-1606109192144)(media/6842decfaeefc7d3c59c821f74ec336e.png)]
二、流量控制
流量控制是为了控制发送速率,保证接收方来得及接收。
接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为
0,则发送方不能发送数据。
TCP拥塞控制
如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当控制发送方的速率。这与流量控制很像,但流量控制是为了是接收方来得及接收。而拥塞控制的控制发送速率是为了缓解拥塞程度。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ttjjLHTG-1606109192144)(media/b0a7e8dcd91c6008bb6e765627620a64.png)]
TCP通过四个算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。
发送方需要维护一个叫做拥塞窗口(cwnd)的状态变量,注意拥塞窗口与发送方窗口的区别:拥塞窗口只是一个状态变量,实际决定发送方能发送多少数据的是发送方窗口。
为了便于讨论,做如下假设:
(1)接收方有足够大的接收缓存,因此不会发生流量控制;
(2)虽然 TCP 的窗口基于字节,但是这里设窗口的大小单位为报文段。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-glKq0ntf-1606109192145)(media/5f6713436a428a48bce7d941f3706b78.png)]
- 慢开始与拥塞避免(针对的是发送端)
(1)发送的最初执行慢开始,令cwnd=1,发送方只能发送一个报文段;当收到确认后,将cwnd翻倍,因此之后发送报文段的大小为:2、4、8…
(2)慢开始每轮cwnd都是指数级增长,此时需要设置一个慢开始门限ssthresh,当cwnd>=ssthresh时,进入拥塞避免,每一轮只将cwnd增加1。
-
如果某一轮出现超时,则将cwnd/2重新执行慢开始阶段。
-
快重传与快恢复(针对是接收端)
(1)接收方接收到字节时会给发送方确认信息。当发送丢失时,接收方会连续给发送方发送三个已接收到的数据,重复确认信息。此时,发送方就会立即快速发送一个丢失数据(快重传)给接收方。
(2)当丢失个别报文时,而不是网络拥塞。则执行快恢复,令ssthresh=cwnd/2,cwnd=ssthresh,此时进入拥塞避免。
慢开始和快恢复的快慢指的是 cwnd 的设定值,而不是 cwnd 的增长速率。慢开始 cwnd
设定为 1,而快恢复 cwnd 设定为 ssthresh。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gnQkIJMT-1606109192146)(media/1673c28c35af3e1f7a48ad5ae2f120f5.png)]
四、SSL(Security Sockets Layer,安全套接字)
1、为网络通信提供安全及数据完整性的一种安全协议
2、是操作系统对外的API,SSL3.0之后更名为TLS
3、采用身份验证和数据加密保证网络通信的安全和数据的完整性
加密的方式有哪些
1、对称加密:加密和解密都使用同一个密钥
2、非对称加密:加密使用的密钥和解密使用的密钥是不相同的
3、哈希算法:将任意长度的信息转换为固定长度的值,算法不可逆
4、数字签名:证明某个消息或者文件是某人发出/认同的
SSL协议位于TCP/IP协议与各种应用层协议之间,为数据通讯提供安全支持。
SSL协议可分为两层: SSL记录协议(SSL Record
Protocol):它建立在可靠的传输协议(如TCP)之上,为高层协议提供数据封装、压缩、加密等基本功能的支持。
SSL握手协议(SSL Handshake
Protocol):它建立在SSL记录协议之上,用于在实际的数据传输开始前,通讯双方进行身份认证、协商加密算法、交换加密密钥等。
1、HTTPS数据传输流程:
(1)浏览器将支持的加密算法信息发送给服务器。
(2)服务器选择一套浏览器支持的加密算法,以证书形式回发给浏览器。
(3)浏览器验证证书合法性,并结合证书和公钥,公钥加密了信息发送给服务端(CA机构颁发的证书为了确认服务器的身份)。
(4)服务器使用私钥解密信息,验证哈希,加密响应消息回发浏览器。
(5)浏览器解密响应消息,并对消息进行验证,之后使用。
2、HTTPS和HTTP的区别?
(1)HTTPS需要到CA申请证书,HTTP不需要。
(2)HTTPS密文传输,HTTP明文传输。
(3)连接方式不同,HTTPS默认使用443端口,HTTP80
(4)HTTPS=HTTP+加密+认证+完整性保护。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SXQmwmsk-1606109192147)(media/7ebfb3df06626f10edf477d93ffc5b9f.png)]
HTTPS有如下特点:
内容加密:采用混合加密技术,中间者无法直接查看明文内容
验证身份:通过证书认证客户端访问的是自己的服务器
保护数据完整性:防止传输的内容被中间人冒充或者篡改
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZYakk92I-1606109192148)(media/b67c380f68c40ef8637eafeadade12ec.png)]
1、客户端共享公钥,只有认证的服务端有私钥,黑客服务端没有私钥无法解密。
2、客户端使用公钥将加密报文内容的密钥加密,发送给服务端,黑客服务端没有密钥无法解密报文内容。
3、报文内容使用散列算法,黑客无法给报文添加其他敏感内容。
参考:https://segmentfault.com/a/1190000018992153
3、GET请求和POST请求的区别?
三个层面来解答,
(1)HTTP报文层面:GET将请求信息放在URL(有长度限制),POST放在报文体中(没有长度限制)。(都不安全)
(2)数据库层面:GET请求是幂等性和安全性,POST不符合。
(3)GET请求能被缓存,而POST不行。(绝大部分GET请求可以被CDN缓存)
4、Session和Cookie的区别?
(1)Cookie数据存放在客户的浏览器上,Session数据放在服务器上。
(2)Session相对于Cookie更安全
(3)若考虑到减轻服务器负担,应当使用Cookie。
当服务器收到(没有cookie的)请求报文后,针对request
method作出响应动作,在响应报文的实体部分,加入了set-cookie段,set-cookie段中给出了cookie的id,过期时间以及参数path,path是表示在哪个虚拟目录路径下的页面可以读取使用该cookie,将这些信息发回给客户端后,客户端在以后的http
request中就会将自己的cookie段用这些信息填充。
Rocketmq学习(8/4-8/5)
MQ(Message
Queue)消息队列,是一种跨进程的通信方式,应用程序通过写入和检索出入队列的消息来通信,无需通过专用连接链接它们。MQ借助消息队列传递数据。
消息队列:
-
解耦:消息生产者和消费者解耦,交互系统之间没有直接的调用关系,只通过消息传输,耦合度低。
-
异步:消息进入队列后,不用立即处理,异步处理,加快系统响应速度。
-
削峰限流:流量巨大的秒杀业务,通过MQ、减缓数据库的压力。
Rocketmq是一款分布式、队列模型的消息中间件。
-
支持集群模型、负载均衡、水平扩展能力。
-
亿级别的消息堆积能力
-
采用零拷贝、顺序写盘、随机读。
-
丰富的API使用,支持同步、异步、顺序和事务型消息投递。
-
代码优秀,底层通信框架采用Netty NIO框架
-
NameServer代替Zookeeper(NameServer是更轻量级的网络路由的服务)
-
强调集群无单点,可扩展,任意一点高可用
-
消息失败重试机制、消息可查询
RocketMQ-概念模型
NameServer:主要负责对源数据管理,包括对于Topic和路由信息的管理。
NameServer是一个功能齐全的服务器,其角色类似Zookeeper,比zk更轻量级。
压力不会太大,主要开销在维持心跳和提供Topic-Broker的关系数据。
NameServer是几乎无状态的,可以横向扩展,节点之间相互无通信。
Broker:消息中转角色,负责存储消息,转发消息。
Producer消息生产者,业务系统负责产生消息。
Consumer:消息消费者,负责消费消息,一般是后台系统负责异步消费
Message:传输的信息
Topic:主题消息类型。
Tag:子主题,消息的第二类型。
架构:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VJyXjXh4-1606109192149)(media/9b17d6b17226ec89a61ca8a7843e3326.png)]
一次完整的通信流程是怎样的?
Producer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer获取Topic路由信息,并向提供Topic服务的Broker
Master建立长连接,且定时向Broker发送心跳。
Producer只能将消息发送到Broker master,但Consumer同时与Broker
Master和BrokerSalve建立长连接。可以从master、和salve中订阅消息。
优点:1、单机吞吐量大、可用性高分布式架构、消息可靠、支持消息堆积。
缺点:支持客户端语言不多
使用消息中间件之前需要先了解“同步”调用、“异步”调用?
-
同步调用意味着A、B、C三个系统,实现一个功能的调用链是:A调用B,B又调用C,A返回结果需要等待B,B要返回有需要C。
-
引入MQ之后,原来的依赖关系转移了,从系统之间的依赖,变成系统都依赖MQ。就是A调用B,只需要向MQ发送一条消息,A就认为自己的工作完成了。B只需要从MQ中取得A发送的消息。实现A异步调用B。
由此MQ是让系统之间由同步到异步:实现1、提升性能;2、系统解耦;3、流量削峰。
-
提升性能是指原来一个系统同步依赖每个系统,现在只需发送消息给MQ即可认为是任务完成。比如:A完成200ms,B完成20ms,请求需依赖A、B则同步情况下完成220ms,有了MQ都发给MQ即可。
-
系统解耦,原来系统A、系统B强依赖,一旦一个系统出现问题会立刻影响到另个系统。现在只需要依赖MQ即可。
-
流量削峰,高并发访问系统A,A需要调用数据库,那么系统瓶颈在数据库。(数据库系统复杂需要支持事务、锁、复杂SQL)同样的机器配置下,如果数据库可以抗每秒6000请求,MQ至少可以抗每秒几万请求。
引入MQ后,A系统依赖支持高并发的MQ,数据库也依赖MQ,可以以自己适合的速度读取消息。原来高并发流量在MQ被削峰了。整个系统性能由A决定而不是由B决定。
消息重复消费问题(幂等性)
面试题:如何保证消息不被重复消费?或者说,如何保证消息消费时的幂等性?
影响消息正常发送和消费的重要原因:网络的不确定性、人为的对服务端系统重启。
Consumer消费完消息后,应该发送COMSUME_SUCCESS确认消息通知broker,但因为网络原因,消息丢失。导致broker不知道consumer已消费过,又分发该消息给其他消费者。
补充:(RabbitMQ是发送ACK信息、Kafka有个offset的概念,Kafka 实际上有个 offset
的概念,就是每个消息写进去,都有一个 offset,代表消息的序号,然后 consumer
消费了数据之后,每隔一段时间(定时定期),会把自己消费过的消息的 offset
提交一下,表示“我已经消费过了,下次我要是重启啥的,你就让我继续从上次消费到的
offset 来继续消费吧“。)
解决方案:
幂等性就是保证消息不会被重复消费。生产者发送带有唯一凭证的消息(messageId或者uuid)。
- 强校验:消费者同步数据库时,从数据库查询该操作流水的主键id,校验是否存在(悲观锁方式)。也可以直接以uuid或者messageId为主键插入记录,处理异常直接忽略异常。(乐观锁方式);
注:悲观锁方式可能会有并发问题。
2、弱校验:消费时将消息记录存入redis(redis自带幂等性),使用setnx保证redis操作的并发安全,下次消费前先校验是否消费过。
二、如何保证消息的可靠性传输?或者说,如何处理消息丢失的问题?(漏消费)
MQ有个基本原则,就是数据不能多一条,也不能少一条,不能多就是幂等性,不能少就是不能少消费,少发送消息。
RocketMQ消息丢失场景:
1、生产者投放消息给MQ时,通信异常消息丢失。
2、RocketMQ持久化消息过程中,没来得及异步刷盘RocketMq宕机消息丢失,消息写入磁盘但是没备份消息丢失。
3、消费者异步消费消息,但是异步过程中消费者宕机。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5IRZul9M-1606109192150)(media/571cf5a4085afe31b107bb9448334bf0.png)]
关于第二步详细说明:
①RocketMQ为了减少磁盘的IO,会先将消息写入到os
cache中,而不是直接写入到磁盘中,消费者从os
cache中获取消息类似于直接从内存中获取消息,速度更快,过一段时间会由os线程异步的将消息刷入磁盘中,此时才算真正完成了消息的持久化。在这个过程中,如果消息还没有完成异步刷盘,RocketMQ中的Broker宕机的话,就会导致消息丢失
②如果消息已经被刷入了磁盘中,但是数据没有做任何备份,一旦磁盘损坏,那么消息也会丢失
解决方案:(如何保证消息零丢失)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MHPHKR0u-1606109192151)(media/e5397ccc3c44ba29bd56ea4813466600.png)]
-
场景1中保证传输过程中消息不丢失,直接开启RocketMq的事务型消息sendMessageInTransaction保证消息的投放成功。启用消息监听器,监听消费者返回的事务状态。
-
场景3中让消费者开启,自身消息监听器registerMessageListener,消费成功返回CONSUME_SUCCESS。
-
场景2中,os
cache的异步刷盘改用同步刷盘,这一步需要修改Broker的配置文件,将flushDiskType改为SYNC_FLUSH同步刷盘策略,默认的是ASYNC_FLUSH异步刷盘。一旦同步刷盘返回成功,那么就一定保证消息已经持久化到磁盘中了。对于硬盘损坏,我们保证磁盘锁坏不会丢失数据,对RocketMq采取主从架构,集群部署。Leader中的数据在多个Follower中都有备份,防止单点故障。
上面的一整套方案可以保证RocketMQ的消息零丢失。但是性能和吞吐量大大下降。
-
事务型消息传输,比普通消息传输多出很多步骤,消耗性能。
-
同步刷盘相比异步刷盘,一个存储在内存,一个存储在磁盘,速度差距大
-
主从架构,需要主从复制。
-
消费时无法异步消费只能等待消费完成再通知RocketMQ。
参考:https://blog.csdn.net/LO_YUN/article/details/103949317
三、RocketMQ保证消息顺序
这是使用MQ的时候必问话题,第一看你了不了解顺序这个事?第二看看你有没有办法保证消息是有顺序的?这是生产系统中常见的问题。
如果三个消息是对数据的增加、修改、删除。如果消息执行顺序搞错了,本来这个数据应该本删除,但是最后却被保留下来,导致数据不一致。
RabbitMQ中消息从queue中出来被不同的消费者拉取消费,导致顺序不一致。RocketMq者RabbitMQ类似,不过是在同一Topic不同的MessageQueue中,被不同的消费者消费导致的问题。
1、RabbitMq问题:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ECGZ7hi5-1606109192152)(media/29ea655479f826f9a6a5d9d005f46c7c.png)]
2、RocketMq问题:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yYfsZduA-1606109192153)(media/adaff707897ef4bc37f97a0942152428.png)]
(这个图的对应关系,是通过重写select方法之后,达到发送消息发送给对应的queue)
(单独说)消费者和topic以及其中的queue对应关系。
Producer向一些队列轮流发送消息,队列集合称为Topic,Consumer如果做广播消费,则一个consumer实例消费这个Topic对应的所有队列,如果做集群消费,则多个Consumer实例平均消费这个topic对应的队列集合。
参考:
http://jm.taobao.org/2017/01/12/rocketmq-quick-start-in-10-minutes/
https://blog.csdn.net/a417930422/article/details/51198531
解决方案:
RabbitMq是先划分多个queue,一个queue对应一个消费者,然后同一key的消息进入同一个queue,然后被相同的消费者消费。(不展开说)
RocketMq:只要将有序的消息按照顺序都放入同一个MessageQueue中,最后就能被同一个消费者顺序消费了。(消费者跟对应的topic一一对应需要使用集群消费,一个消费者组,对应一个topic)
在代码层面,我们可以在消息投放时,根据特定条件(相同的key,比如相同的订单号),将这类消息投放到相同的MessageQueue中。(相同的msg的id,可以通过orderId对length取模得到)
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
Integer id = (Integer) arg;
int index = id % mqs.size();
return mqs.get(index);
}
}, orderId);
还有一个问题,就是消费者在消费顺序消息失败了怎么办?
RocketMq默认是返回ConsumeConcurrentlyStatus.RECOSUME_LATER。表示将该消息放入重试队列中稍后重试。这显然违背了顺序执行条件。我们可以修改默认返回,返回一个ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT让当前队列等一会再消费,只有前面的消息消费结束,才轮到后面消息。【或者使用事务型消息,执行丢失、错误直接回滚,重新来一遍。这样的话吞吐量、性能就会下降】
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeOrderlyContext context) {
//业务代码
if (消费成功) {
return ConsumeOrderlyStatus.SUCCESS;
} else {
return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
}
}
四、消息积压如何处理?
如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时,说说怎么解决?
1、后台定时任务每隔72小时,删除旧的没有使用过的消息信息
2、根据不同的业务实现不同的丢弃任务,选择不同的策略淘汰任务,例如FIFO/LRU等
3、(1)临时紧急扩容,新建一个topic,临时建立好原来10倍的MessageQueue数量,(2)写一个临时的消费数据的consumer程序,采用多线程消费,消费后不做耗时处理,部署到服务器端。(3)快速消费完积压数据之后,恢复到原先部署的架构。(推荐)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MY1o5mmX-1606109192154)(media/5d04ac7b65a608d4539552c775025c62.png)]
消息堆积原因:(1)消费者故障宕机,出现bug。(2)消费者消费速度远比不上生产者生产速度。
8/13补充详细说明:
-
先查消费者有没有宕机,是否出现bug,修复不消费的问题之后再启动。
-
临时Topic队列扩容。增加新的topic,以RocketMQ为例一个topic有多个队列,是消息堆积情况,设置queue的个数。(堆积的topic里面的message
queue数量固定,过多的consumer不能分配到message queue出消费者现冗余)。 -
编写分发程序,让旧topic中的消息分发到新的topic中,再启动更多consumer在新的topic中消费(一般message
queue数量大于consumer) -
处理完堆积,恢复到正常机器数。
(附,不太在意执行结果,可以采用异步消费措施。(多线程、再开一个消息队列等、redis中的list))
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ztth44KO-1606109192155)(media/ec88125b6187b84634c8d686a71cf4f3.png)]
参考:https://blog.csdn.net/qq_38480786/article/details/105679723
MQ中的消息过期失效了?
采用消息批量重导策略。
使用RocketMQ的回溯消费机制,回溯消费是指Consumer已经消费成功的消息,由于业务上的需求需要重新消费,要支持此功能,Broker在向Consumer投递成功消息后,消息仍然需要保留。并且重新消费一般是按照时间维度。
例如由于Consumer系统故障,恢复后需要重新消费1小时前的数据,那么Broker要提供一种机制,可以按照时间维度来回退消费进度。
RocketMQ支持按照时间回溯消费,时间维度精确到毫秒,可以向前回溯,也可以向后回溯。
五、设计一个消息队列的思路?
其实聊到这个问题,一般面试官要考察两块:
你有没有对某一个消息队列做过较为深入的原理的了解,或者从整体了解把握住一个消息队列的架构原理。
看看你的设计能力,给你一个常见的系统,就是消息队列系统,看看你能不能从全局把握一下整体架构设计,给出一些关键点出来。
说实话,问类似问题的时候,大部分人基本都会蒙,因为平时从来没有思考过类似的问题,大多数人就是平时埋头用,从来不去思考背后的一些东西。类似的问题,比如,如果让你来设计一个
Spring 框架你会怎么做?如果让你来设计一个 Dubbo
框架你会怎么做?如果让你来设计一个 MyBatis 框架你会怎么做?
设计解答:
1、首先这个 mq
得支持可伸缩性吧,就是需要的时候快速扩容,就可以增加吞吐量和容量,那怎么搞?设计个分布式的系统呗,参照一下
kafka 的设计理念,broker -> topic -> partition,每个 partition
放一个机器,就存一部分数据。如果现在资源不够了,简单啊,给 topic 增加
partition,然后做数据迁移,增加机器,不就可以存放更多数据,提供更高的吞吐量了。(RocketMQ扩展成nameServer、broker集群的设计)
2、持久化机制,可以异步刷盘还可以同步刷盘。持久化可以顺序写,这样就没有磁盘随机读写的寻址开销,磁盘顺序读写的性能是很高的,这就是
kafka 的思路。
3、可用性。MQ可以有多个leader和follower,多副本保证broker的高可用。
4、支持0丢失。支持事务型消息、支持broker主从架构、支持磁盘备份。
六、常用消息中间件区别,使用场景?
提高性能、解耦系统、流量削峰。
https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/why-mq.md
提一嘴提高性能:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dw8s7emd-1606109192156)(media/2391fe31d5836c48b4f0a49b3615a12a.png)]
耗时:(用户感受到的耗时)3+300+450+200ms = 953ms
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zbMHPCT8-1606109192157)(media/903a0eb3fcc175a2d6806cf9f535d143.png)]
耗时:(用户感觉到的耗时)3+5=8ms
消息队列应用带来的缺点:
- 系统的复杂度高。
添加了MQ进来,使得系统所需要考虑的问题多了,保证消息没有重复消费?怎么处理消息丢失的情况?怎么保证消息传递的顺序性?
- 系统的可用性降低。
如果MQ不是集群,分布式部署,那么MQ宕机全部的系统都不可以工作。
- 一致性问题。
异步更新数据库时,用户看到处理成功,但是在B、C、D下游系统写入时,有一个出错了,保证数据一致性成为问题。
Kafka、ActiveMQ、RabbitMQ、RocketMQ 有什么优缺点?
特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka |
---|---|---|---|---|
单机吞吐量 | 万级,比 RocketMQ、Kafka 低一个数量级 | 同 ActiveMQ | 10 万级,支撑高吞吐 | 10 万级,高吞吐,一般配合大数据类的系统来进行实时数据计算、日志采集等场景 |
topic 数量增加对吞吐量的影响 | topic 可以达到几百/几千的级别,吞吐量会有较小幅度的下降,这是 RocketMQ 的一大优势,在同等机器下,可以支撑大量的 topic | topic 从几十到几百个时候,吞吐量会大幅度下降,在同等机器下,Kafka 尽量保证 topic 数量不要过多,如果要支撑大规模的 topic,需要增加更多的机器资源 | ||
时效性 | ms 级 | 微秒级,这是 RabbitMQ 的一大特点,延迟最低 | ms 级 | 延迟在 ms 级以内 |
可用性 | 高,基于主从架构实现高可用 | 同 ActiveMQ | 非常高,分布式架构 | 非常高,分布式,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 |
消息可靠性 | 有较低的概率丢失数据 | 基本不丢 | 经过参数优化配置,可以做到 0 丢失 | 同 RocketMQ |
功能支持 | MQ 领域的功能极其完备 | 基于 erlang 开发,并发能力很强,性能极好,延时很低 | MQ 功能较为完善,还是分布式的,扩展性好 | 功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用 |
ActiveMQ没有经过高吞吐量的场景的检验,也就是说吞吐量不如RocketMq,消息有较低概率会丢失,相比RocketMq不会丢失消息。开发者社区不活跃。
后来大家开始用 RabbitMQ,但是确实 erlang 语言阻止了大量的 Java
工程师去深入研究和掌控它,对公司而言,几乎处于不可控的状态,但是确实人家是开源的,比较稳定的支持,活跃度也高;
RocketMQ是使用java开发的,对于java开发者使用起来方便,高吞吐量、高可用分布式集群架构、相比kafka的Zookeeper使用轻量级的nameserver管理topic和路由信息。确实很不错(阿里出品),但社区可能有突然黄掉的风险,对自己公司技术实力有绝对自信的,推荐用
RocketMQ,否则回去老老实实用 RabbitMQ 吧,人家有活跃的开源社区,绝对不会黄。
参考:
https://blog.csdn.net/lo_yun/category_9480949.html
https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/why-mq.md
https://blog.csdn.net/dingshuo168/article/details/102970988
七、分布式事务(8/24补充)
1、2pc(两段式提交)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WGNsXzQJ-1606109192158)(media/770db56aa9b60ce7e7af257a7921eee7.png)]
就是消息中间件协调双方都锁定资源后,再通知各自提交事务。(出现问题:当提交错误还是会导致不一致性。)
2、半消息事务(最终一致性)【事务型消息?正解!】
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-imJ5P34P-1606109192159)(media/c0f5c72fa5e0868a3d82559a7a13042c.jpeg)]
(1)消息主动投递方,本地事务提交失败,被动接受方是收不到消息的。
(2)消息主动投递方,本地事务提交成功,消息服务会将消息投递到下游被动接受方,保证被动接收方一定能够成功消费。(消费成功与否,有一个最终状态。)
3、3pc
数据结构学习(8/9)
一、红黑树学习
什么是“平衡二叉查找树”?
平衡二叉树的严格定义是这样的:二叉树中任意一个节点的左右子树的高度相差不能大于
1。从这个定义来看,上一节我们讲的完全二叉树、满二叉树其实都是平衡二叉树,但是非完全二叉树也有可能是平衡二叉树。
为什么需要平衡二叉树?因为二叉查找树在频繁地插入、删除动态更新后,发生时间复杂度退化(不断插入最终变成单链表)。
红黑树(Red-Black Tree)简称R-B Tree:
-
根节点都是黑色的;
-
每个叶子节点都是黑色的空节点(NIL),也就是说,叶子节点不存储数据;
-
任何相邻节点都不能同时为红色,也就是红色节点是被黑色节点隔开的;
-
每个节点,从该节点到达其可达叶子节点的所有路径,都包含相同数目的黑色节点;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UYASOrnt-1606109192160)(media/903ee0dcb62bce2f5b47819541f9069a.jpeg)]
去掉所有红色节点,不会使红黑树的高度超过logn。只会使高度更矮一些。(相对黑色节点是平衡的,但对于红色节点是不平衡的,高度差超过1)整体高度2logn,近似AVL树。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hE0mzuTf-1606109192161)(media/3e20413244393a46ed9a694b62579239.png)]
为什么工程中倾向于使用R-H Tree而不是AVL树?
AVL树是一种高度平衡的二叉树,所以查找的效率非常高,但是维护平衡付出更高的代价。每次插入、删除必须都要做调整,就比较复杂、耗时。所以对于有频繁插入、删除的数据集合,使用AVL树的代价就很高。
红黑树只是做到了近似平衡,并不是严格的平衡,所以在维护平衡的成本上,要比AVL树要低。
红黑树的高度近似logn,所以它是近似平衡,插入、删除、查找操作的复杂度都是O(logn)。
散列表、跳表和红黑树比较:
散列表:插入删除查找都是O(1),
是最常用的,但其缺点是不能顺序遍历以及扩容缩容的性能损耗。适用于那些不需要顺序遍历,数据更新不那么频繁的。
跳表:插入删除查找都是O(logn),
并且能顺序遍历。缺点是空间复杂度O(n)。适用于不那么在意内存空间的,其顺序遍历和区间查找非常方便。
红黑树:插入删除查找都是O(logn),
中序遍历即是顺序遍历,稳定。缺点是难以实现,去查找不方便。其实跳表更佳,但红黑树已经用于很多地方了。
红黑树的调整
在正式开始之前,我先介绍两个非常重要的操作,左旋(rotate left)、右旋(rotate
right)。左旋全称其实是叫围绕某个节点的左旋,那右旋的全称估计你已经猜到了,就叫围绕某个节点的右旋。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5A7Ebybx-1606109192163)(media/db1c829d1ea61f1dad8efac3fdd6c2cb.jpeg)]
红黑树的插入和删除操作会破坏红黑树的定义,具体来说就是会破坏红黑树的平衡。
插入操作的平衡调整
红黑树规定,插入的节点必须是红色的。而且,二叉查找树中新插入的节点都是放在叶子节点上。
两种特殊情况号处理。
-
插入节点的父节点是黑色的,什么都不用做。
-
如果插入节点是根节点,把它变黑色。
除此以外需要:左右旋转和改变颜色。
(红黑树节点调整就跟魔方调整类似,看到符合条件的情况就做类似调整)
红黑树的平衡调整过程是一个迭代的过程。我们把正在处理的节点叫做**关注节点。**关注节点点会随着不停地迭代处理,而不断变化。最开始关注节点就是新插入的节点。
新节点插入后,一般有三种情况。我们根据每种情况的特点,不停调整。
(父节点的兄弟节点叫做叔叔节点,父节点的父节点叫做祖父节点。)
CASE1:如果关注点是a,它的叔叔节点d是红色,则:
-
将关注点a的父节点b、叔叔节点d的颜色都设置成黑色;
-
将关注点a的祖父节点c的颜色设置成红色;
-
关注节点变成变成a的祖父节点c;
-
调到CASE 2或者CASE 3.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NjM8H1FX-1606109192164)(media/0932deac9ac0a491ae9787ee45777b45.png)]
(该图表示插入a节点)
**CASE2:如果关注点是a,它的叔叔节点d是黑色,关注节点a是其父节点b的右子节点,**我们执行下面操作:
-
关注节点变成节点a的父节点b;
-
围绕新的关注节点b左旋;
-
调到CASE 3.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xYWFT8Fh-1606109192165)(media/4480a314f9d83c343b8adbb28b6782ad.jpeg)]
CASE 3:如果关注节点是 a,它的叔叔节点 d 是黑色,关注节点 a 是其父节点 b
的左子节点,我们就依次执行下面的操作:
-
围绕关注节点 a 的祖父节点 c 右旋;
-
将关注节点 a 的父节点 b、兄弟节点 c 的颜色互换。
-
调整结束。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jcAAJgkh-1606109192166)(media/04650d9470b1e67899f5b8b7b8e33212.jpeg)]
上述3个CASE是指在插入时,对插入情况进行判断。符合哪种就套哪个CASE,不是严格从CASE1到CASE2到CASE3的。
删除操作的平衡调整
删除操作的平衡调整分为两步,第一步是针对删除节点初步调整。初步调整只是保证整棵红黑树在一个节点删除之后,仍然满足最后一条定义的要求,也就是说,每个节点,从该节点到达其可达叶子节点的所有路径,都包含相同数目的黑色节点;第二步是针对关注节点进行二次调整,让它满足红黑树的第三条定义,即不存在相邻的两个红色节点。
- 针对删除节点初步调整
这里需要注意一下,红黑树的定义中“只包含红色节点和黑色节点”,经过初步调整之后,为了保证满足红黑树定义的最后一条要求,有些节点会被标记成两种颜色,“红
- 黑”或者“黑 - 黑”。如果一个节点被标记为了“黑 -
黑”,那在计算黑色节点个数的时候,要算成两个黑色节点。
在下面的讲解中,如果一个节点既可以是红色,也可以是黑色,在画图的时候,我会用一半红色一半黑色来表示。如果一个节点是“红
- 黑”或者“黑 - 黑”,我会用左上角的一个小黑点来表示额外的黑色。
CASE1:如果要删除的节点是a,它只有一个子节点b
-
删除节点a,并且把节点b替换到节点a的位置;
-
节点a只能是黑色,节点b只能是红色,其他情况不符合红黑树的定义。这种情况下,我们把节点b改为黑色;
-
调整结束,不需要进行二次调整。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2lZ7QCux-1606109192166)(media/82363b57e635fe3bd6ae7b72eb3ea99f.jpeg)]
(如果b是黑色非NIL节点,那么b下面还会跟两个NIL节点,不满足红黑树每个节点,到叶子点都包含相同个数个黑色节点的第4点定义。)
CASE
2:如果要删除的节点a有两个非空子节点,并且它的后继节点就是节点a的右子节点c。
-
如果节点 a 的后继节点就是右子节点 c,那右子节点 c 肯定没有左子树。我们把节点
a 删除,并且将节点 c 替换到节点 a
的位置。这一部分操作跟普通的二叉查找树的删除操作无异; -
然后把节点 c 的颜色设置为跟节点 a 相同的颜色;
-
如果节点 c 是黑色,为了不违反红黑树的最后一条定义,我们给节点 c 的右子节点 d
多加一个黑色,这个时候节点 d 就成了“红 - 黑”或者“黑 - 黑”; -
这个时候,关注节点变成了节点 d,第二步的调整操作就会针对关注节点来做。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9rVr2fy4-1606109192167)(media/714636146842664246115b1425454b77.png)]
CASE 3:如果要删除的是节点 a,它有两个非空子节点,并且节点 a
的后继节点不是右子节点,我们就依次进行下面的操作:
-
找到后继节点 d,并将它删除,删除后继节点 d 的过程参照 CASE 1;
-
将节点 a 替换成后继节点 d;
(3) 把节点 d 的颜色设置为跟节点 a 相同的颜色;
(4)如果节点 d 是黑色,为了不违反红黑树的最后一条定义,我们给节点 d 的右子节点
c 多加一个黑色,这个时候节点 c 就成了“红 - 黑”或者“黑 - 黑”;
(5)这个时候,关注节点变成了节点 c,第二步的调整操作就会针对关注节点来做。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qOpRqBVw-1606109192168)(media/53856649091e22556725a90bcb609f7f.png)]
剩余都是规则性的记忆不下。
红黑是叶节点是黑的空节点?
实现红黑树的方式更加简单一些,就是为了符合上述指定的规则。实现代码中黑色NIL节点只有一个节省存储空间。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LZBrlUEL-1606109192169)(media/d63231acb0e9d54c3469055d8dbdb366.jpeg)]
第三点,插入操作的平衡调整比较简单,但是删除操作就比较复杂。针对删除操作,我们有两次调整,第一次是针对要删除的节点做初步调整,让调整后的红黑树继续满足第四条定义,“每个节点到可达叶子节点的路径都包含相同个数的黑色节点”。但是这个时候,第三条定义就不满足了,有可能会存在两个红色节点相邻的情况。第二次调整就是解决这个问题,让红黑树不存在相邻的红色节点。
二、全排列、IP地址复原-DFS(回溯算法)
DFS做题时一定考虑一下把所有可能的图示画出来,画成一棵树。容易写出dfs
- 全排列树:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kABzrFOt-1606109192170)(media/826d85d2a8014eb086014568c0794e05.png)]
2、IP地址复原树:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2YsL2ypA-1606109192171)(media/e567d228c39f4995c2a824085340d3e3.png)]
都是使用void dfs并且在边界条件,对全局变量进行填充。
哈希树
哈希树的理论基础
【质数分辨定理】
简单地说就是:n个不同的质数可以“分辨”的连续整数的个数和他们的乘积相等。“分辨”就是指这些连续的整数不可能有完全相同的余数序列。
例如:
从2起的连续质数,连续10个质数就可以分辨大约M(10)
=2*3*5*7*11*13*17*19*23*29= 6464693230
个数,已经超过计算机中常用整数(32bit)的表达范围。连续100个质数就可以分辨大约M(100)
= 4.711930 乘以10的219次方。
插入
我们选择质数分辨算法来建立一棵哈希树。
选择从2开始的连续质数来建立一个十层的哈希树。第一层结点为根结点,根结点下有2个结点;第二层的每个结点下有3个结点;依此类推,即每层结点的子节点数目为连续的质数。到第十层,每个结点下有29个结点。
同一结点中的子结点,从左到右代表不同的余数结果。
例如:第二层结点下有三个子节点。那么从左到右分别代表:除3余0,除3余1,除3余2.
对质数进行取余操作得到的余数决定了处理的路径。
下面我们以随机的10个数的插入为例,来图解HashTree的插入过程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9ZfI20sC-1606109192172)(media/1e589753d7f9e4ea386050aac312ec58.png)]
四、各类排序算法时间复杂度及稳定性
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DwKZeovn-1606109192173)(media/49dbf83e6c67b7794a799160b965fbdc.jpeg)]
五、各种简单dp题目(类似爬楼梯的变形题)
1、用长度1、3、5长度的火材拼成11长。有多少种摆法?
类似能爬一步、三步、五步有多少种走法?
Base:dp[0]=1,dp[1]=1,dp[2]=1,dp[3]=2,dp[4]=2
dp[i] = dp[i-1]+dp[i-3]+dp[i-5];
2、现在小明瘸了,左腿只能走一步,右腿可以走一步或者2步,而且小明左腿不能连续走(即左腿用过一次,下一次必须用右腿)。对于走楼梯,走左腿和走右腿我们看做不同的走法,问多少种?代码实现
设0表示用的左腿,1表示用的右腿。
dp[i][0]表示用的左腿迈了一步到达了i,viceversa。
base:
dp[1][0] = 1,dp[1][1] = 1,dp[2][0] = dp[1][1],dp[2][1] =
dp[1][0]+dp[1][1]+dp[0][1];
状态转移方程:
I可以从2开始遍历
dp[i][0] = dp[i-1][1];
dp[i][1] = dp[i-1][0]+dp[i-1][1]+dp[i-2][1];
六、力扣刷题遇到的java语法问题
1、
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dC02jIP5-1606109192173)(media/2acb3947b183de341b7e29db573c4fb7.png)]
答:
通过定义public Object
peek()可以看出,peek()返回的是对象,对象不存在相等的概念,所以不能用==符号,
而int不是对象,是变量值的概念,值可以使用==进行比较,
值和对象间的转换涉及到装箱拆箱的动作,由程序自动完成,感兴趣可以去了解下。
2、约瑟夫环问题(本质是一道利用%位移问题)
利用最后一次只剩1个数时,下标为0做文章,倒数第二次剩下2个数,最后一个数的下标为1。
最后1个数(7:下标0)往后移动m个位置(在上一次数组总数范围下:2)就是上一次该数所在下标位置(7:下标1)。太难了。。。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N7dDij3l-1606109192174)(media/80f1161e0400b39f6ca2460112da3c6a.png)]
f(N,m) = (f(N-1,m)+m)%N //每次都向后移动m个位置
https://blog.csdn.net/u011500062/article/details/72855826
3、递归传递数值型int、boolean和引用数据类型。
return后数值型会恢复初始状态,引用数据类型需要remove状态或者传递new出来的对象。
七、链表排序为什么归并比快速排序更优?
快速排序效率的主要来源之一是引用的局部性,计算机硬件在这里得到了优化,因此访问批次彼此相邻的内存位置往往比访问分散在内存中的内存位置更快。快速排序中的分区步骤通常具有很好的局部性,因为它访问前后相邻的数组元素。因此,快速排序往往比其他排序算法(如堆排序)执行得好得多,尽管它通常执行的比较和交换次数大致相同,因为在堆排序情况下,访问更加分散。
此外,快速排序通常比其他排序算法快得多,因为它是就地操作的,不需要创建任何辅助数组来保存临时值。与归并排序相比,这是一个巨大的优势,因为分配和释放辅助数组所需的时间是显而易见的。就地操作还改进了快速排序的局部性。
当使用链表时,这两个优点都不一定适用。因为链表单元格经常分散在内存中,所以访问相邻的链表单元格没有局部性的好处。因此,快速排序的巨大性能优势之一被耗尽了。类似的就地工作的好处不再适用,因为merge
sort的链表算法不需要任何额外的辅助存储空间。
设计模式学习(8/14)
一、设计模式六大原则
- 单一职责原则
一个类、一个方法尽量只做一件事情。
按业务对象(BO business object)、业务逻辑(BL business logic)拆分;
- 开闭原则
对增加开放,对修改关闭。软件通过扩展来实现变化,而不是通过修改已有的代码来实现改变。
- 迪米特原则
最少知道原则,一个类对另一个类涉及的越少越好。减少类之间的耦合。
- 接口隔离原则
一个接口包含的方法过多可以进行拆分,拆分成小的接口。
简单理解:复杂的接口,根据业务拆分成多个简单接口;(对于有些业务的拆分多看看适配器的应用)
【接口的设计粒度越小,系统越灵活,但是灵活的同时结构复杂性提高,开发难度也会变大,维护性降低】
- 依赖倒置原则
面向接口编程
上层模块不应该依赖下层模块,它们都应该依赖接口。
- 里氏替换原则
子类可以扩展父类的功能,但不能改变父类原有的功能。(本质是多态)
目的:增强程序的健壮性,在实际项目中,每个子类对应不同的业务场景,使父类作为参数传递不同的子类完成业务逻辑。
https://www.cnblogs.com/Sam-2018/p/principle.html
二、设计模式
1、UML统一建模语言学习:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eXRDL1os-1606109192175)(media/ddd34f82670283eaf75a50ad2924beb6.png)]
- 依赖关系
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9DxfcF1V-1606109192175)(media/148625ef70040498423201c23277ef9c.png)]
满足下列条件都是依赖关系:
-
成员变量使用其他类。
-
某个方法的返回类型使用其他类。
-
某个方法的接收参数。
-
方法中局部变量使用到其他类。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SquytRZ1-1606109192176)(media/50a9b101355ddd20f7455997001aef7a.png)]
成员变量PersonDao可使用聚合关系,空心菱形箭头指向PersonServiceBean。
- 泛化关系(继承关系)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9BmrbDt7-1606109192177)(media/80929ed78b1ce5615cdb3a5fb8950236.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XnZU8EvZ-1606109192177)(media/6e394c80a97281d724374db4fa0c8ce4.png)]
泛化关系使用实线空心三角形箭头。
3、实现关系(实现接口)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nJjDWKo8-1606109192178)(media/5eef0ff2c301763b0aebb82642e1b0fb.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vOau8rWr-1606109192179)(media/f6815a3be71fd6c6b869cc848c4530c9.png)]
实现关系使用虚线空心三角形
4、关联关系(是将依赖关系更加细化==成员变量依赖,抽取出来成为关联关系)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ky2nAbfU-1606109192180)(media/9abcc901fe8eaa741b5bb83aff926921.png)]
关联关系是依赖关系的特例。使用实线的箭头表示。
一个类单方面使用另一个类是单向一对一关系,两个类互相使用就是双向一对一关系。
5、聚合关系(是关联关系一种特殊情况)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Uh6nA5dm-1606109192181)(media/d11ccefa56fed2a72e1b147a136edbe3.png)]
6、组合关系
类与类之间同生同死,一个类在另一个类的初始化方法里new出来。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QGWNJz3u-1606109192182)(media/87230eb43061b0132ba943ddd76032d4.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IF2l5ciJ-1606109192183)(media/3c574309bb2f929356ab83486b497dd3.png)]
2、常用设计模式
设计模式分类:
(1)创建型模式:对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离。外界对于这些对象只需要知道它们共同的接口,不用清楚具体实现细节。
—单例模式、工厂模式。(手写get)
(2)结构型模式:描述如何将类或者对象结合在一起形成更大的结构,类与类的组合,类与对象的组合。
—桥接模式、代理模式。
(3)行为型模式:对在不同的对象之间划分责任和算法的抽象化。
—责任链模式、策略模式。
单例模式:
双校验锁。减少频繁地创建与销毁对象。
单线程环境中单例模式的两种经典实现,饿汉式和懒汉式。
在介绍单线程环境中单例模式的两种经典实现之前,我们有必要先解释一下立即加载和延迟加载两个概念。
**立即加载:**在类加载初始化的时候就主动创建实例;
**延迟加载:**等到真正使用的时候才去创建实例,不用时不去主动创建。
单线程下的饿汉式是获取单例对象类的时候就已经完成对单例对象的类加载。
单线程下的懒汉式是在单例类被加载时,不会实例化对象,只有被使用时才会实例化对象。
https://blog.csdn.net/fuzhongmin05/article/details/71001857
装饰器模式:
动态的将新功能添加到对象上。(体现ocp开闭原则)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2EMbZg4I-1606109192184)(media/1dea0db5887470f34e45a9cfe5270cd5.png)]
是聚合+继承、继承箭头画反了(是装饰器继承Drink)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7reE2IT4-1606109192185)(media/981ea280cd1a9b4f2f03fca825bf2bca.png)]
后续添加新的咖啡和新的调味料只需要让class extends Coffe或extends Decorator
使用了继承加聚合的方式,将装饰器(可以是接口)继承抽象类,然后将这个抽象类作为装饰器参数传入。增加装饰器功能,并且可以使用主体类的内容,只需要新建类继承装饰器即可。
策略模式:
Intent:
定义一系列算法,封装每个算法,并使它们可以互换。
策略模式可以让算法独立于使用它的客户端。
Class Diagram
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LzcjwjJr-1606109192186)(media/4f47ec1bffde6d07269313bfa8b81262.png)]
设计鸭子类见eclipse代码:
设计一只鸭子它可以动态的改变叫声,这里的算法族是鸭子的叫声。
责任链模式:
什么是链?
1、链是一系列节点的集合。
2、链的各节点可以灵活拆分再重组。
责任链模式?
使用多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理业务为止。
角色:
抽象处理者角色(Handler):定义出一个处理请求的接口。接口可以定义出一个处理方法、设置后继和获取后继。通常是以java的接口或抽象类实现。
具体处理者角色(ConcreteHandler):具体处理者接到请求后,可以选择将请求处理掉,或者传递给下一家。
责任链模式好处:是具体处理者角色之间各司其职,具有良好的扩展性。整个责任链相当于一个黑盒请求者只需要发送请求给责任链头部即可,不需要关心内部实现。减少if-else的语句。
适配器模式:
将一个接口转换为客户所希望的另一个接口,将一个类在另一个接口中使用(适配另一个接口)。
适配器模式中,我们通过新增一个适配器类来解决接口不兼容的问题,使得原本没有关系的类可以协同工作。
根据适配器类和被适配类的关系不同可以分为:类适配器和对象适配器。类适配器就是适配器类继承目标类或者实现目标类,对象适配器就是将被适配类组合进适配器类。
如:Interface duck和interface
turkey,需要在duck中调用turkey的方法,是两个接口兼容。先写一个concreteTurkey
implements turkey声明特有的的叫声方法。然后写一个turkeyAdapter implements
duck,在类中组合上turkey turkeyExample,duck接口方法
用turkey的方法来实现。—这样就可以在duck接口中通过turkeyAdapter使用turkey接口的方法。
https://blog.csdn.net/wwwdc1012/article/details/82780560
并发进阶面试题复习(9/10-)
一、ThreadLocal结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zRiAEIEt-1606109192187)(media/824ce25a38f50bdd57582bee6700e781.jpeg)]
ThreadLocal以Entry<K,V>形式存放在ThreadLocalMap中,ThreadLocalMap是ThreadLocal中的一个静态类。相当于是Thread的局部变量,在thread类中有声明是一个成员变量。
(1)Thread和ThreadLocal、ThreadLocalMap关系。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8N46DKKP-1606109192188)(media/da9ab3725936456070b97dfcaf6efea4.png)]
(2)ThreadLocal和ThreadLocalMap的关系?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XzvdPDkw-1606109192190)(media/626fd882e494b13e475108e1a210a7df.png)]
1、为了防止ThreadLocal发生的内存溢出,使用ThreadLocal结束时加上ThreadLocal.remove()。
2、关于SimpleDateFormat对时间Date进行格式化时,会发生并发修改的问题。本质是共用一个SimpleDateFormat类,该类在format的执行过程当中会使用成员变量Calendar存储日期。而该成员变量没有加锁,也就会被其他线程修改,导致并发错误。
3、能跟我说一下对象存放在哪里么?
在Java中,栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存,而堆内存中的对象对所有线程可见,堆内存中的对象可以被所有线程访问。
二、Java内存模型
Java内存模型本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
JMM中的主内存存放数据:
-
所有线程创建的java实例对象。
-
包括成员变量、类信息、常量、静态变量等。
-
属于数据共享的区域,多线程并发操作时会引发线程安全问题。
JMM中的工作内存数据:
1、存储当前方法的所有本地变量信息,本地变量对其他线程不可见。
2、字节码行号指示器、Native方法信息。
3、属于线程私有数据区域,不存在线程安全问题。
指令重排序需要满足的条件:
1、在单线程环境下不能改变程序运行的结果。
2、存在数据依赖关系的不允许重排序
无法通过happens-before原则推导出来的,才能进行指令的重排序。
Java具体初始化规则:
String str = null;
//字符串赋值为null,表示str初始化了,并且有了引用,但是没有指向任何内存空间。
String str = new String(“123”);
//字符串常量池中建立对象赋值为123,并且分配了堆空间,引用指向“123”
Happens-before规则:
(1)程序的顺序性规则。
Happens
Before先行先发生。先发生和先执行时两码事。在同一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。注意,这里的控制流顺序不是程序代码顺序,因为要考虑分支、循环等结构。
(2)管程锁定规则:一个unlock操作先行发生于后面对于同一个锁的lock操作。
(3)volatile变量规则:对于一个volatile变量的写操作先行发生于后面对这个变量的读操作。
(4)线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
(5)线程终止规则:线程中的所有操作都先行于对于此线程的终止检测。
(6)传递性规则:操作A先行发生于B,操作B先行发生于C。能够退出操作A先行发生于C。
三、yield和sleep,wait和sleep的区别?
1、yield和sleep的区别。
调用Thread类的sleep方法时,调用线程会被阻塞挂起指定时间,在此期间内线程调度器,不会调用该线程。而调用Thread类的yield方法时,只是让出自己剩余时间片,线程处于就绪态,线程调度器还会调度到当前线程。
2、wait和sleep的区别。
Wait是object类的方法,sleep是Thread类的方法。调用Wait一定要持有锁,比如是在synchronized修饰下调用的,并且wait调用一定会释放锁。调用wait方法的线程会被阻塞挂起,直到其他线程调用notify或notifyall唤醒至Runnable状态。
wait时调用interrupt方法抛出InterruptedException,调用wait方法没获取锁,报IllegalMonitorStateException异常。
四、线程和协程(9/16)
1、首先比较单线程和多线程的区别?
(1)单线程就是一个进程运行时产生一个线程。
(2)多线程就是一个进程运行时产生多个线程。(jvm中包括用户线程和守护线程,典型的守护线程有GC回收线程,当JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作。)
**注意:**多线程不是为了提高运行效率,而是为了提高资源使用效率,提高系统的利用率。
多线程并不总是比单线程快。不是任何场景下都需要我们使用多线程去执行任务。
事实上我们创建的线程数量超过CPU核心数时,需要通过CPU分配时间片给线程,线程才能执行,。所以多个线程的线程状态:就绪态和运行态的来回切换会导致性能消耗问题。
创建合适的线程数量?
一般可由线程执行任务的性质来确定。
CPU密集型、IO密集型任务。
CPU密集型就是使用CPU进行计算的任务,比如神经网络训练时,只使用CPU时,利用算法对某些数据进行计算时。
IO密集型任务就是对硬盘数据进行访问时,网络传输的数据进行处理时。Web应用大部分都是IO密集型任务。
因此CPU密集型的线程数应该和CPU核心数保持一致。IO密集型任务可以创建多个线程来充分使用CPU资源。
实际项目中对多线程的使用?
异步任务:
(异步同步使用线程池,是为了发送尽可能多的发送消息,处理请求)异步同步数据库(并发使用连接池内连接,导致mysql服务器的cpu负载急剧升高)、对下单成功的反馈信息可以交给别的线程处理。
任务拆分计算并汇总(分治的思想):
多个任务没有直接依赖的情况下,则可以是串行变并行,并行中消耗时间最长的任务即为总消耗时间。
多任务并行处理:
处理批处理任务是,可以考虑使用多线程,比如通过定时任务每凌晨整理一些数据的时候。
关于topk问题如何选择合适的算法:
(1)单机+单核+足够大内存
直接将所有数据放入内存进行处理,如果需要查找10亿个查询次(每个占8B)中出现频率最高的10个,考虑到每个查询词占8B,则10亿个查询次所需的内存大约是10^9
* 8B=8GB内存。排序完之后进行查询,这种方式简单快速。
(2)单机+多核+足够大内存。
直接在内存中将所有数据hash方法划分为n个partition,每个partition交给一个线程处理,然后分别对各部分求topk,最后一个线程将结果归并。
(3)单机+单核+受限内存,这种情况下,需要将原数据文件切割成一个一个小文件,直到能够放入内存处理,依次对小文件进行排序然后取topk。然后依次对结果进行归并取topk。
https://blog.csdn.net/CSDN_WYL2016/article/details/107701099
2、协程是什么?
线程是cpu资源调度的基本单位。
进程是操作系统资源调度的基本单位。
协程是微线程,基本没有内核切换开销,可以不加锁的访问全局变量,拥有自己的寄存器和栈。
Java中使用Thread类具象表示抽象的线程。线程共享进程的内存空间。java虚拟机栈、本地方法栈、程序计数器都是线程私有的。
协程和线程的区别:(java还没实现协程)
(1)一个线程有多个协程,一个进程也可以有多个协程。
(2)线程进程都是同步机制,而协程还存在异步机制。
(3)协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态。
(4)线程时抢占式,而协程是非抢占式的,所以需要用户自己释放使用权来切换到其他协程。同一时间只有一个协程拥有运行权,相当于单线程的能力。
Spring面试题复习(8/17)
一、Spring IOC原理
IoC-Inversion of Control,控制反转。
定义:
控制反转就是把创建和管理bean的过程转移给了第三方。而这个第三方,就是Spring Ioc
Container,对于IoC来说,最重要的就是容器。
容器负责创建、配置和管理bean,也就是管理bean的生命,控制bean的依赖注入。
Bean其实就是包装了的Object,无论是控制反转还是依赖注入,它们的主语都是object,而bean就是第三方包装好了的object。
IoC容器
Spring如何设计IoC容器?
答:使用ApplicationContext,它是BeanFactory的子类,更好的补充并实现了BeanFactory
(BeanFactory简单粗暴,可以理解为HashMap:Key-bean name;Value-bean
object,一般只有get,put两个,称之为低级容器)
ApplicationContext继承了多个接口(接口可以多继承)所以多了很多功能。
ApplicationContext有两个具体的实现子类,用来读取配置文件的:
(1)ClassPathXmlApplicationContext-从class path中加载配置文件
(2)FileSystemXmlApplicationContext -
从本地文件中加载配置文件,不是很常用,如果再到 Linux
环境中,还要改路径,不是很方便。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xXT2hlA7-1606109192191)(media/20e8e81e457489c02173021f1b432543.png)]
DI依赖注入:
何为依赖,依赖什么?
程序运行需要依赖外部的资源,提供程序内对象的所需要的数据、资源。
何为注入,注入什么?
配置文件把资源从外部注入到内部,容器加载了外部的文件、对象、数据,然后把这些资源注入给程序内的对象,维护了程序内外对象之间的依赖关系。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yux9n3XB-1606109192192)(media/11d5b934e91c23dc20e6ee77fd1afd39.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tabqLecI-1606109192193)(media/575eb67740a1a612ea1786cae877dba8.png)]
为什么要用IoC思想?—解耦
IoC容器把对象之间的依赖关系转成用配置文件来管理,由Spring IoC
Container管理。对多个对象的合作管理很困难,所以统一交由IoC容器管理。
那么 Spring 是如何帮我们创建对象的呢?
ApplicationContext 是 IoC 容器的入口,其实也就是 Spring 程序的入口,
刚才已经说过了它的两个具体的实现子类,在这里用了从 class path
中读取数据的方式;使用该类中的getBean方法获取Bean对象。
Follow
up1,对象在容器中默认是单例的。(创建IoC容器时,加载配置文件如果注入了bean就会一起生成bean对象)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-onros7Xs-1606109192194)(media/e358ee35b0c5f524f1cf7cbddf5a8e68.png)]
使用prototype原型类型就可创建多个bean对象。(默认为单例的)
接着上一个点研究bean创建时间。
Follow up 2. 容器中的对象是什么时候创建的?
其实是每次启动容器的时候,就已经创建好容器中的所有对象了。(当然,这在 scope =
“prototype” 的时候不适用,只是 singleton 的时候。)
多说一句,其实最好应该一直保留一个无参的 constructor,因为这里 bean
对象的创建是通过反射,clazz.newInstance() 默认是调用无参的 constructor
不过,现在已经被弃用掉了,换用了这个:
clazz.getDeclaredConstructor().newInstance()
小结:
(1)Spring对象创建过程:
通过ApplicationContext这个IoC容器入口,使用两个具体实现子类,从class
path或者file path中读取配置,将bean和容器一起创建,之后用getBean()获取bean
Instance。
(2)Spring省略了我们new 的过程。把 new
的过程交给第三方来创建、管理,这就是「解耦」
- Spring也是使用的类中的set()方法,只不过不是由程序员来调用的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xh7RozEM-1606109192195)(media/0c96cac922f2a3e526c192865ed1d968.png)]
Spring将bean的定义解析成BeanDefinition,然后后一String,BeanDefinition键值对存入beanDefinitionMap中以便后续bean的实例化(这里的map是ConcurrentHashMap应对并发解析bean)
getBean方法的代码逻辑:
(1)转换beanName;(2)从缓存中加载实例;(3)实例化bean;(4)检测parentBeanFactory;(5)初始化Bean的相关的依赖;(6)创建Bean。
面试题:
一、Spring Bean的作用域。
(1)singleton:Spring默认作用域,容器里拥有唯一的Bean。
(2)propotype:针对每个getBean请求,容器都会创建一个Bean实例。
(3)request:会为每个http请求创建一个Bean实例
(4)会为每个session创建一个Bean实例
(5)globalSession:会为每个全局Http
Session创建一个Bean,该作用域仅对Portlet有效。(Porlet是个web容器)
二、Spring Bean的生命周期
(1)实例化Bean;
(2)Aware接口(一般情况下Bean对象不需要对ioc容器有所感知,但也有特殊要求而aware方法就是将容器信息注入Bean中,让Bean可以对容器进行操作)注入Bean
ID、BeanFactory和AppCtx。
(3)BeanPostProcessor(s)前置初始化方法,在Bean初始化之前有一些自定义处理方法。
postProcessBeforeInitialization
(4)InitialingBean(s)afterPropertiesSet
(5)定制的Bean init方法。
(6)BeanPostProcessor(s)后置初始化方法,在Bean初始化之后有一些自定义处理方法。
postProcessAfterInitialization
(7)Bean初始化相关。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mu3kAPkp-1606109192196)(media/bd8ce7cadd4b765a76c9951fd4180957.png)]
二、Spring AOP(面向切面编程)
关注点分离:不同的问题交给不同的部分去解决。
(1)面向切面编程AOP体现,关注点分离。
(2)通用化功能代码的实现,对应所谓的切面(aspect)
(3)业务功能代码和切面代码分开后,架构将更加高内聚低耦合。
(4)保证功能完成:切面合并到业务中(weave)
AOP的三种织入方式:
(1)编译时织入:需要特殊java编译器,如AspectJ
(2)类加载时织入:需要特使java编译器,如AspectJ和AspectWerkz
(3)运行时织入:Spring采用的方式,动态代理。实现简单。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p8P7pVkx-1606109192197)(media/aa2d2d72df5e3f8d5c175e8ade7b5c12.png)]
Advice的种类:
-
前置通知(Before)
-
后置通知(AfterReturning)
-
环绕通知(Round)
AOP的使用:(使用@AspectJ注解开发AOP)
1、编写controller及其中的方法。
生产中controller中有很多方法,如果手动在方法的调用前后加上logger.info很繁琐。
所以使用AOP,编写一个切面然后在切入点处使用。
2、编写切面RequestLogAspect.java
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pkVeCB9t-1606109192198)(media/c8adc72de1c4e5658b3d00a47e4b3b71.png)]
加上@Aspect注解表明是一个切面,并注入到Spring IoC容器中。初始化一个日志对象。
3、设定切入点PointCut,是那个方法运行时调用记录log方法。给切入点命名为webLog方法。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kKrQm91n-1606109192199)(media/388d5691d7b0a3a399df93bd526a572b.png)]
4、设定各种通知,在方法webLog()执行的各种时期执行自定义的方法。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xXuyHysS-1606109192200)(media/0c64d3fbb95d7a7416d7e96c4dbb197e.png)]
AOP的实现原理:
使用代理类来代替真实的类,调用代理类的方法就相当于执行真实类的方法(还可以在真实类的方法前后加上日志方法。)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vrpgC9iG-1606109192201)(media/1d356647d3de5e5ee809f8c8e2c7681a.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ei41ji1r-1606109192202)(media/8a3ef07ee2b29fcbfa0c2f1f8538b101.png)]
总结:
(1)如果真实对象实现了接口,则一般采用JDK的动态代理。
(2)JDK动态代理必须提供接口才能使用,在一些不能提供接口的环境中,只能采用其他第三方技术,比如CGLIB动态代理.它的优势在于不需要接口,只要一个非抽象类就能实现动态代理
JDK动态代理原理:
JDK动态代理是java.lang.reflect.*包提供的方式,它必须借助一个接口才能生成代理对象。先定义一个接口,并提供其实现类。然后,创建代理对象实现InnvocationHandler,建立代理对象和真实对象的关系(代理对象的构造方法传入Object真实对象),然后实现代理对象的代理逻辑的invoke方法。
Cglib代理原理:
JDK动态代理必须提供接口才能使用,在一些不能提供接口的环境中,只能采用其他第三方技术,比如CGLIB动态代理.它的优势在于不需要接口,只要一个非抽象类就能实现动态代理。
创建Cglib动态代理类实现MethodInterceptor,在定义的getProxy方法中将代理对象联系真实对象。然后要实现intercept方法,在intercept方法中使用反射的invokeSuper方法,调用真实对象的方法。
https://blog.csdn.net/qq_41649001/article/details/107021056
三、Spring容器对象可以用jvm自己创建的对象吗,并说明理由。
**解释问题:**Spring容器可以使用jvm自己创建的对象(手动new出来的对象,外部声明的对象)。本质是问spring外部对象的依赖注入?
这里有一个外部类User类,我们需要把它注入到Spring容器中,交由容器管理。
(1)在有AspectJ支持情况下,使用@Configurable注解将自己new出来的对象加入到spring启动容器中。这个对象里我们注入一个Spring的ApplicationContext。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jc1ZSUsa-1606109192203)(media/691344ae81993f8b829746e83c811088.png)]
参考:https://zhuanlan.zhihu.com/p/61587264
(2)第一点是从外部注入角度。第二点主要是强调spring容器只是被托管bean的创建、管理,初始化还是使用反射调用bean类自己的空构造函数。由于反射本质上还是需要去jvm中创建bean的,所以spring对象和jvm对象并无本质区别。
四、Spring创建的对象和jvm创建的对象有什么不同?
首先聊一聊Spring容器的生命周期,Spring容器会被GC掉吗?
Spring容器肯定不能在web项目跑的时候被GC掉,启动spring项目使用命令生成堆内存快照,通过MAT工具进行分析,找到spring
工厂,path to root。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VE3fENiz-1606109192204)(media/185533d5124384b0cff8be0c9931af49.png)]
发现spring容器的GC
root是Thread,该线程属于Tomcat,根据图中信息可推断此线程为Tomcat启动加载容器的线程,此线程未stop,即spring
资源无法被回收。
Spring创建的对象Bean则在Spring容器没有被回收时,遵循Spring容器中对Bean生命周期的管理。(只要Bean被使用,Spring容器会一直引用)如果需要销毁Bean对象可以调用removeBeanDefinition()方法将Bean对象与Spring容器中的引用删除,然后Bean由JVM的GC进行回收。
同时删除Bean有两种方法:
(1)在Spring容器中删除该Bean的定义。
(2)直接删除Spring容器,这样Bean随之被移除引用。
自定义一个方法:
(1)public static void unregisterBean(String beanId){
beanDefinitionRegistry.removeBeanDefinition(beanId);
}
(2)spring通过AbstractApplicateContext的close方法执行容器的关闭处理,
最终的关闭处理是通过调用doClose方法执行,在doClose方法中调用destroyBean()方法,其中获取一个该Bean的依赖集合,并删除所有依赖的引用。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zsptxdwL-1606109192204)(media/7714a2859318d868cd278820ef8cedad.png)]
https://blog.csdn.net/chongzhelu7371/article/details/100943540
Spring容器的GC
回到正题上,Spring对象和JVM创建的对象有何不同?
Spring对象是包装好的Bean对象,它的使用和管理交由Spring容器管理(当然Spring容器也是由JVM负责回收),而JVM创建的对象生命周期结束直接由JVM的GC进行回收。对象也分为强引用、软引用、弱引用、虚引用来区别回收。
比如:spring中的bean在项目启动被保存在了上下文的map中,也就是相当于被引用,所以不会被回收。
jdk动态代理和cglib有何区别?
1、jdk动态代理:这个bean必须实现了接口。利用拦截器(必须实现invocationHandler)加上反射机制生成一个代理接口的匿名类,在调用具体方法前,调用invocationHandler来处理。
2、cglib代理:利用ARM框架,加载对象类的class文件,修改字节码文件生成新的子类增强类。
Jdk动态代理是反射生成代理类。Cglib是使用ARM框架,字节码技术创建代理类效率高。
https://www.cnblogs.com/sandaman2019/p/12636727.html
springboot有什么作用?
B+树做索引而不用红黑树?
AVL
树(平衡二叉树)和红黑树(二叉查找树)基本都是存储在内存中才会使用的数据结构。在大规模数据存储的时候,红黑树往往出现由于树的深度过大而造成磁盘IO读写过于频繁,进而导致效率低下的情况。为什么会出现这样的情况,要获取磁盘上数据,必须先通过磁盘移动臂移动到数据所在的柱面,然后找到指定盘面,接着旋转盘面找到数据所在的磁道,最后对数据进行读写。磁盘IO代价主要花费在查找所需的柱面上,树的深度过大会造成磁盘IO频繁读写。根据磁盘查找存取的次数往往由树的高度所决定,所以,只要我们通过某种较好的树结构减少树的结构尽量减少树的高度,B树可以有多个子女,从几十到上千,可以降低树的高度。
https://www.cnblogs.com/loveLands/articles/11509069.html
https://blog.csdn.net/A_BlackMoon/article/details/80094814
https://www.cnblogs.com/coder2017/p/11681975.html#/cnblog/works/article/11681975
五、Bean的生命周期
主要生命周期包括4个阶段:
(1)instance实例化阶段,调用构造方法或工厂方法实例化bean。
(2)populateBean设置参数阶段,属性设值。
(3)initializeBean初始化阶段,自定义初始化方法。
(4)destory销毁阶段
1-3是包含在doCreate()方法中,4是在configurableApplicationContext的close()方法中。
生命周期中还有许多的扩展点,比如影响多个bean的接口:
BeanPostProcessor是在初始化前后的处理器。
InstantiationAwareBeanPostProcessor是在实例化阶段前后的处理。
Ps:类初始化和实例化区别?
类初始化是指,在类加载过程中的初始化阶段,按照程序员意图对变量的赋值操作。
实例化是指,当类完全加载到内存后,创建对象的过程。
https://blog.csdn.net/justloveyou_/article/details/72466416
Bean的作用域:
(1)Singleton单例作用域(默认值),就是ioc容器中只能存在一个实例bean对象。跟着ioc容器一起创建的。
(2)prototype原型作用域,一个ioc容器可以有多个bean对象。不跟ioc容器一起创建。
(3)request每次http请求生成一个bean。
(4)session是同一个session共享一个bean。
六、Spring 框架中都用到了哪些设计模式?
工厂模式:BeanFactory就是简单工厂模式的体现,用来创建对象的实例;(在创建一个对象时不向客户暴露内部细节,并提供一个创建对象的通用接口。)
单例模式:Bean默认为单例模式。
代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术;
模板方法:用来解决代码重复的问题。比如. RestTemplate, JmsTemplate, JpaTemplate。
观察者模式:定义对象键一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知被制动更新,如Spring中listener的实现–ApplicationListener。
责任链模式、适配器模式:springMVC
说说SpringMVC、SpringBoot和SpringCloud三者之间的联系和区别?
Springmvc是基于servlet实现的mvc框架,是属于spring中的web层一部分。用来处理路径映射、视图渲染。
Springboot是为了解决xml,config各种配置文件,重复写同样形式的代码,为了简化工作流程推出了starter整合包模式。
后面项目越来越庞大,单体应用不能满足需要,就出现springboot聚合工程,还有将业务进行拆分,所以就产生springcloud的微服务。
七、SpringMVC原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n8PQU1c0-1606109192205)(media/b473373735049b02526a53dbfa300434.png)]
https://blog.csdn.net/zero__007/article/details/88650174
1、DispatcherServlet前端控制器接收用户请求,然后调用getHandler()方法根据HandleMapping处理器映射器,获得执行链handlerExecutionChain(包括handle和HandlerInterceptor)—责任链模式,for循环选择合适的handler(Controller),根据handler,DispatcherServlet选择合适的适配器—适配器模式,AnnotationMethodHandlerAdapter注解适配器。
2、适配器调用handle()方法,填入request数据和入参,调用目标类的方法。返回一个ModelAndView对象,DispatcherServlet选择合适的ViewResolver进行解析。
3、ViewResolver以Model和View进行模型渲染,将数据填入request域中,返回给前端(浏览器)
八、Spring事务
Spring管理事务的方式:1、编程式事务,在代码中硬编码。2、声明式事务,在配置文件中配置。(xml配置、注解声明)
Spring事务中的隔离级别?
TransactionDefinition接口中定义5个常量:
(1)TransactionDefinition.ISOLATION_DEFAULT使用后端数据库默认的隔离级别。
(2)TransactionDefinition.ISOLATION_READ_UNCOMMITTED
(3)TransactionDefinition.ISOLATION_READ_COMMITTED
(4)TransactionDefinition.ISOLATION_REPEATABLE_READ
(5)TransactionDefinition.ISOLATION_SERIALIZATION串行化事务
Spring事务的事务传播行为?
支持当前事务的情况:
(1)TransactionDefinition.
PROPAGATION_REQUIRED:如果当前存在事务加入该事务;没有就创建一个新事务。
(2)TransactionDefinition.
PROPAGATION_SUPPRORTS:如果存在事务则加入事务,否则以非事务方式运行。
不支持当前事务的情况:
(3)TransactionDefinition.
PROPAGATION_REQUIRES_NEW:创建一个新事务,如果当前存在事务,则把当前事务挂起。(自己执行自己的事务)
九、如何使用JPA在数据库中非持久化一个字段?
想让字段并不被数据库存储怎么办?
给字段加上transient关键字,或者给字段加上@Transient注解。
RPC介绍,RPC原理是什么?(9/16)
什么是RPC?
RPC(Remote Procedure
Call)-远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。比如两个不同的服务
A、B 部署在两台不同的机器上,那么服务 A 如果想要调用服务 B
中的某个方法该怎么办呢?使用 HTTP请求
当然可以,但是可能会比较慢而且一些优化做的并不好。 RPC
的出现就是为了解决这个问题。
RPC原理是什么?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-drae3GP7-1606109192206)(media/e336575764ac335ea3a60154dc15ee1e.png)]
1、LSTM有一条主线和支线,输入控制根据支线输入的重要程度,按比例替换主线的内容。
2、分线中某些数据跟主线结论不一致,使用忘记门来替换主线的部分结论。
主线的更新取决于输入和忘记门来进行控制。
- 输出就是根据主线和分线数据的取舍,输出结果。
U好像是h0对h1的权重,w是x对h1的权重,B是x的截距。
RNN反向传播是误差会成w这个权重,乘的步骤多了,不是梯度弥散(w<1)就是梯度爆炸(w>1)
数据库中存储的DECIMAL是指?
DECIMAL(10,2)
总共能存10位数字,末尾2位是小数,字段最大值99999999.99(小数点不算在长度内)
如果提交表单是 提交1000000000000,最大还是会存99999999.99;
更多推荐
所有评论(0)