Java后端开发八股文
2023-08-29 16:38:16 21 举报
AI智能生成
登录查看完整内容
Java后端开发主要涉及使用Java语言进行服务器端程序的开发,包括设计、编写、测试和优化代码。这需要对Java语言有深入的理解,包括但不限于面向对象编程、异常处理、多线程、集合框架等。同时,还需要熟悉常用的Java开发框架,如Spring、Hibernate等,以及数据库技术,如MySQL、Oracle等。此外,对于网络协议、操作系统、数据结构和算法等也有一定的了解。在实际开发中,需要能够根据需求设计和实现高效、稳定、可扩展的系统。同时,也需要有良好的问题解决能力,能够在遇到问题时迅速定位并解决问题。
作者其他创作
大纲/内容
多态
抽象 封装 继承 多态
面向对象的特征
被private修饰的属性和方法,不能被其他类访问,子类不能继承也不能访问。只能在所在类内部访问。
private
缺省变量或者方法前没有访问修饰符时,可以被所在类访问,可以被同一包内的其他类访问或者继承。但是不能被其他包访问
缺省
被protected修饰的方法和属性,在同一包内可被访问和继承。不同包内,子类可继承,非子类不能访问
protected
方法和属性前有public修饰,可以被任意包内的类访问。 另外,类要想被其他包导入,必须声明为public。被public修饰的类,类名必须与文件名相同
public
访问修饰符
static 关键字用来声明独立于对象的静态变量,无论一个类实例化多少对象,它的静态变量只有一份拷贝。 静态变量也被称为类变量。局部变量不能被声明为 static 变量
静态变量(类变量)
在静态方法中不能访问非静态成员方法和非静态成员变量但是在非静态成员方法中是可以访问静态成员方法/变量
静态方法内不能访问非静态方法
可以在没有创建任何对象的前提下,仅仅通过类本身来调用static方法。这实际上正是static方法的主要用途被static关键字修饰的方法或者变量不需要依赖于对象来进行访问,只要类被加载了,就可以通过类名去进行访问。static可以用来修饰类的成员方法、类的成员变量,另外可以编写static代码块来优化程序性能。想在不创建对象的情况下调用某个方法,就可以将这个方法设置为static。我们最常见的static方法就是main方法,至于为什么main方法必须是static的,现在就很清楚了。因为程序在执行main方法的时候没有创建任何对象,因此只有通过类名来访问
可以直接通过 类.静态方法 调用,不需要this和对象
静态方法
用来修饰方法和变量
static
用来修饰类、方法和变量,final 修饰的类不能够被继承,修饰的方法不能被继承类重新定义,修饰的变量为常量,是不可修改的
final
用来创建抽象类和抽象方法
abstract
synchronized 和 volatile
非访问修饰符
修饰符
作用在变量,不能被修改作用在方法,可以被重载,不能重写作用在类,不能被继承
由fianl域的读写重排序规则,可以保证正在创建中的对象不能被其他线程访问到
写final域重排序规则:禁止对final域的写 重排序到构造方法之外编译器会在fianl域写之后,构造方法的return语句之前插入一个storestore屏障
读final域的重排序规则为:在一个线程中,初次读取对象引用和初次读取该对象包含的final域,JMM会禁止这两个的重排序操作。 处理器会在这两个操作前面插上LoadLoad内存屏障
final在多线程并发下的应用
final
内部类的创建不依赖外部类的实例对象
静态内部类
内部类实例的创建必须依赖外部类的实例对象,所以普通的内部类不允许有静态的成员
非静态内部类
根据静态与否划分
组成:静态的内部类可以有静态成员,非静态的内部类不能有静态成员
访问:静态内部类可以访问外部类的静态变量,不可以访问外部类的非静态变量;非静态内部类可以访问外部类的成员
创建:静态内部类的创建不依赖外部类,非静态内部类的创建必须依赖外部类的实例
成员内部类
不能用public,private等修饰,只能方法内部使用和访问
局部内部类可以访问外部类的成员变量,但该成员必须声明为 final,并内部不允许修改该变量的值。(这句话并不准确,因为如果不是基本数据类型的时候,只是不允许修改引用指向的对象,而对象本身是可以被就修改的)
区别
局部内部类: 定义和使用只存在于方法内部的内部类
匿名内部类是没有访问修饰符的
匿名内部类必须继承一个抽象类或者实现一个接口
匿名内部类是没有构造方法的,因为它没有类名
约束
匿名内部类: 一般用于函数参数,用于实现一个抽象类或者接口
根据位置划分
内部类可以访问该类定义所在作用域中的数据,包括被private修饰的私有数据
内部类可以对同一个包中的其他类隐藏
内部类可以实现java单继承的缺点
当我们想要定义一个回调函数却不想写大量代码的时候我们可以选择使用匿名内部类来实现
作用
内部类
创建并返回此对象的一个副本
clone()
equals相同,hashcode一定相同equals不相同,hashcode可能相同hashcode不同,equals一定不同
指示某个其他对象是否与此对象“相等”
equals(Object obj)
当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法
finalize()
返回一个对象的运行时类。
getClass()
返回该对象的哈希码值。
hashCode()
唤醒在此对象监视器上等待的单个线程
notify()
唤醒在此对象监视器上等待的所有线程
notifyAll()
返回该对象的字符串表示
toString()
导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法
导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量
wait(long timeout)
导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量。
wait()
Object类
Class classobj1 = Class . forName (\"com.fanshe.student\") ;
1、forName(全限定路径)
Class classobj2 = Student.class;
2、类名.class
Student stu = new StudentO ; Class classobj3 = stu. getClassO;
3、对象.getClass()
创建方式
通过反射机制,可以在运行时访问 Java 对象的属性,方法,构造方法等
应用场景:开发通用框架、动态代理(AOP)、注解、可扩展性功能
反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题
破坏封装性
由于反射涉及动态解析的类型,因此无法执行某些jvm优化。反射操作的性能要比非反射操作的性能要差,应该在性能敏感的应用且频繁调用的代码段中避免
性能开销
缺点
面试题:如何在Java程序运行时不停机动态加载一个函数进来?
动态加载类
1、创建前提不同:newInstance创建类是这个类必须已经加载过且已经连接,new创建类是则不需要这个类加载过
2、创建对象的方式不同:newInstance是实用类的加载机制,new则是直接创建一个类
3、创建对象类型不同:newInstance: 弱类型(GC回收对象的限制条件很低,容易被回收)、低效率、只能调用无参构造)new 强类型(GC不会自动回收,只有所有的指向对象的引用被移除是才会被回收,若对象生命周期已经结束,但引用没有被移除,经常会出现内存溢出)
newInstance()和new()的区别
反射
抽象类含无实现的方法,不能创建对象抽象类不必须含有抽象方法抽象类不能设private,生来就为了继承子类必须实现父类抽象类方法,否则自己也设抽象类
抽象类
接口冲突:1.如果一个类实现了A,B两个接口,这两个接口有相同的默认方法,则实现类需要重写这个默认方法
超类优先:2.如果一个类的超类中方法和该类的实现接口默认方法相同,则这个类继承的方法以超类的方法为准
类优先:3.如果一个类扩展了超类的方法,并且该方法与实现接口中默认方法同名同参,则该方法以类优先
解决默认方法冲突
default方法:接口方法提供的默认实现,必须使用 default 修饰符标记这个方法
静态方法:java8开始,允许接口中包含静态方法
接口中的域(即常量): 自动地设置为 public static final
A对象 instanceof B接口:判断A类是否是B接口的实例
接口
重写:抽象类中的抽象方法必须实现,非抽象方法不用,接口必须实现所有非default方法
成员变量:抽象类可以是各种类型,接口只能是public static final类型
方法:抽象类有方法实现细节,接口只有public abstract方法
静态方法/代码块:抽象类有,接口可以有静态方法(java8的新特性),无静态代码块
继承/实现:只能继承一个抽象类,但可以实现多个接口
抽象类的设计目的,是代码复用。当不同的类具有某些相同的行为(记为行为集合A),且其中一部分行为的实现方式一致时(A的非真子集,记为B),可以让这些类都派生于一个抽象类。在这个抽象类中实现了B,避免让所有的子类来实现B,这就达到了代码复用的目的。而A减B的部分,留给各个子类自己实现。正是因为A-B在这里没有实现,所以抽象类不允许实例化出来(否则当调用到A-B时,无法执行)
接口的设计目的,是对类的行为进行约束(更准确的说是一种“有”约束,因为接口不能规定类不可以有什么行为),也就是提供一种机制,可以强制要求不同的类具有相同的行为。它只约束了行为的有无,但不对如何实现行为进行限制。对“接口为何是约束”的理解,我觉得配合泛型食用效果更佳。
接口和抽象类
浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝
深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容
详解
方法中定义的基本数据类型局部变量的具体内容是存储在栈中但基本类型成员变量也是随类一起储存在堆里
基本数据类型
类(对象)、接口 、数组
引用数据类型变量的具体内容都是存放在堆中的,地址存放在栈中
引用数据类型
扩展
区别:对引用数据类型是否需要创建一个新的对象
深拷贝&浅拷贝
public boolean createNewFile() throws IOException创建文件:当且仅当不存在具有此抽象路径名指定的名称的文件时,原子地创建由此抽象路径名指定的一个新的空文件。
public boolean mkdir() 创建目录:创建此抽象路径名指定的目录
public boolean delete() 删除此抽象路径名表示的文件或目录
public String getName()返回由此抽象路径名表示的文件或目录的名称
public String getParent() / getParentFile() 返回此抽象路径名的父路径名的路径名字符串 / 抽象路径名,如果此路径名没有指定父目录,则返回 null
public boolean exists()测试此抽象路径名表示的文件或目录是否存在
public long length()返回由此抽象路径名表示的文件的长度
public String toString() 返回此抽象路径名的路径名字符串
public boolean equals(Object obj)测试此抽象路径名与给定对象是否相等
...
File类
标识符由数字(0~9)和字母(A~Z 和 a~z)、美元符号($)、下划线(_)以及 Unicode 字符集中符号大于 0xC0 的所有符号组合构成(各符号之间没有空格)。标识符的第一个符号为字母、下划线和美元符号,后面可以是任何字母、数字、美元符号或下划线。另外,Java 区分大小写,因此 myvar 和 MyVar 是两个不同的标识符。提示:标识符命名时,切记不能以数字开头,也不能使用任何 Java 关键字作为标识符,而且不能赋予标识符任何标准的方法名
标识符
ps:一般\\大写字母表示小写字母的反义
. 表示匹配任意的字符
[]代表匹配中括号中其中任一个字符,如[abc]匹配a或b或c
\\d 表示数字
\\D 表示非数字
\\s表示由空字符组成,[ \\t\\\\x\\f]
\\S表示由非空字符组成,[^\\s]
\\w表示字母、数字、下划线,[a-zA-Z0-9_]
\\W表示不是由字母、数字、下划线组成
?: 表示出现0次或1次
+表示出现1次或多次
JAVA调用正则表达式的类是java.util.regex.Matcher 和 java.util.regex.Pattern
常用符号
正则表达式
重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法
重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写
重写(Override)
重载就是同样的一个方法能够根据传入参数的不同,做出不同的处理
发生在同一个类中(或者父类和子类之间),方法名必须相同,参数个数or类型必须不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同
重载(Overload)
重载Overload&重写Override
不能被继承、不能被重写Override、可以被重载
继承(extends)的含义其实是“扩展”,子类完全没必要扩展父类的构造函数,因为反正每次调子类的时候都会“自动运行”它父类的构造函数,如果真的需要子类构造函数特殊的形式,子类直接修改或重载自己的构造函数就好了
1、在子类继承父类的时候,子类必须调用父类的构造函数;2、在父类有默认构造函数,子类实例化时自动调用,在父类没有默认构造函数,即无形参构造函数,子类构造函数必须通过super调用父类的构造函数;3、在java的继承当中子类是不可以继承父类的构造函数,只能调用父类的构造函数
构造器
String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[],所以String 对象是不可变的
StringBuffer 与 StringBuilder 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串char[]value 但是没有用 final 关键字修饰,所以这两种对象都是可变的
StringBuffer是字符串变量,它的对象是可以扩充和修改的。StringBuilder是一个可变的字符序列
可变性
String不可变,线程安全;String的修改会生成一个新对象,性能最低
StringBuffer和StringBuilder都是对本身修改,相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
StringBuffer 对方法加了同步锁,线程安全StringBuilder 没对方法加同步锁,线程不安全
线程安全性和性能
说说String&StringBuffer&StringBuilder区别
因为要保证string的不可变性
给一个已有字符串\"abcd\"第二次赋值成\"abcedl\",不是在原内存地址上修改数据,而是重新指向一个新对象,新地址。
什么是不可变性
string的主要成员字段value是个char[ ]数组,而且是用private final修饰的,再加上string自身的final,为的就是安全性
为什么不可变
当String支持不可变性的时候,它们的值很好确定,不管调用哪个方法,都互不影响
安全
在并发场景下,多个线程同时读一个资源,是不会引发竟态条件的。只有对资源做写操作才有危险。不可变对象不能被写,所以线程安全。
不可变性支持线程安全
例如:字符串 one 和 two 都用字面量 \"something\" 赋值。它们其实都指向同一个内存地址。
在大量使用字符串的情况下,这样可以节省内存空间,提高效率。但之所以能实现这个特性,String的不可变性是最基本的一个必要条件。要是内存里字符串内容能改来改去,这么做就完全没有意义了
不可变性支持字符串常量池
不可变有什么好处?
为什么String 设计为final?
final 修饰的类叫最终类,该类不能被继承。
final 修饰的方法不能被重写。
final 修饰的变量叫常量,常量必须初始化,初始化之后值就不能被修改。
final 在 Java 中有什么作用?
错误理解一:值传递和引用传递,区分的条件是传递的内容,如果是个值,就是值传递。如果是个引用,就是引用传递
错误理解二:Java是引用传递
错误理解三:传递的参数如果是普通类型,那就是值传递,如果是对象,那就是引用传递
错误概念
实际参数是调用有参方法的时候真正传递的内容,而形式参数是用于接收实参内容的参数当我们调用一个有参函数的时候,会把实际参数传递给形式参数。但是,在程序语言中,这个传递过程中传递的两种情况,即值传递和引用传递
值传递(pass by value):是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数
引用传递(pass by reference):是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
什么是值/引用传递
Java为什么是值传递
由于Integer变量实际上是对一个Integer对象的引用,所以两个通过new生成的Integer变量永远是不相等的(因为new生成的是两个对象,其内存地址不同)
两个new的Integer(False)
对于两个非new生成的Integer对象,进行比较时,如果两个变量的值在区间-128到127之间,则比较结果为true,如果两个变量的值不在此区间,则比较结果为false(因为当值不在-128和127间时,非new生成Integer变量时,java API中最终会按照new Integer(i)生成)
两个非new的Integer(True/False)
非new生成的Integer变量和new Integer()生成的变量比较时,结果为false,(因为 ①当变量值在-128~127之间时,非new生成的Integer变量指向的是java常量池中的对象,而new Integer()生成的变量指向堆中新建的对象,两者在内存中的地址不同;②当变量值在-128~127之间时,非new生成Integer变量时,java API中最终会按照new Integer(i)进行处理,最终两个Interger的地址同样是不相同的)
非new的Integer和new的Integer(False)
Integer变量和int变量比较时,只要两个变量的值是向等的,则结果为true(因为包装类Integer和基本数据类型int比较时,java会自动拆箱为int,然后进行比较,实际上就变为两个int变量的比较)
new的Integer和基本类型int(True)
==比较基本数据类型时比较的是字面量,比较引用数据类型时比较的是在内存中的地址
Object的equals默认是比较对象的内存地址值,但String一般会重写equals,使其变为比较字面量ps:equals在比较基本类型的时候会自动装箱成
不同对象的equals具体举例
==和equals的区别
“如果两个对象相同,那么他们的hashcode应该相等”
重写equals是为了修改equals功能为 只比较字面值 ,如果不重写hashcode方法,那么在hashset中会出现equals调用结果相同,但hashcode不同导致出现重复元素
如何重写hashcode?
首先要明白hashcode的作用是在集合里用来匹配位置查找对象
为什么重写 equals 时必须重写 hashCode 方法
hashcode和equals的区别
链地址法(Java hashmap就是这么做的)
开放定址法(线性探测再散列,二次探测再散列,伪随机探测再散列)
再哈希法
建立一个公共溢出区
如何解决hashcode冲突
编译期&运行期区别
通过javac编译器进行编译,从Java源码 ---> Java 字节码通过 javac xxx.java 即可以编译该源码,javac编译器位于jdk --> bin -->javac
语法&词法分析
在词法分析之后我们需要把数据存起来,以供后续流程使用,编译器会以key-value的形式存储数据,以符号地址为key符号信息为value,具体形式没做限制可以是树状符号表或者有序符号表等。在语义分析(第三步)中,根据符号表所登记的内容 语义检查和产生中间代码,在目标代码生成阶段,当对符号表进行地址分配时,该符号表是检查的依据
填充符号表
1、分析和填充符号表
注解与普通的Java代码一样在运行期间发挥作用。可以把它看做是一组编译器的插件,在这些插件里面,可以读取、修改、添加抽象语法树中的任意元素。如果这些插件在处理注解期间对语法树进行了修改,编译器将回到解析及填充符号表的过程重新处理,直到所有插入式注解处理器都没有再对语法树进行修改为止。换句话说当我们处理注解时如果修改了语法树的话会重新执行分析以及符号填充过程,把注解也填充进来,直到处理完所有注解
2、注解处理器
语法分析以及处理注解之后,编译器获得了程序代码的抽象语法树,语法树能表示一个结构正确的源程序的抽象,但无法保证源程序是符合逻辑的。即可能出现语法树上的内容单个来说是合法的但是结合到上下文语义则未必是合法的,例如:
3、语义分析
Java 中最常用的语法糖主要有泛型、变长参数、条件编译、自动拆装箱、内部类等。虚拟机并不支持这些语法,它们在编译阶段就被还原回了简单的基础语法结构,这个过程称为解语法糖。换句话说,不论是否使用Java的语法糖,最终到jvm哪里的时候都是一样的基础语法结构,jvm不支持语法糖,所以需要编译阶段解语法糖,语法糖的初衷是用来提升开发效率(方便开发者开发),而不是代码性能。
4、解语法糖
字节码生成阶段主要工作就是将前面各个步骤所生成的信息(语法树、符号表)转化成字节码写到磁盘中字节码生成是Javac编译过程的最后一个阶段,在Javac源码里面由com.sun.tools.javac. jvm.Gen类来完成。
5、字节码生成
javac(编译期)具体操作
1、编译
跳转JVM部分
2、加载并执行
java写一个Demo类执行main函数到打印hello world时都经历了哪些流程
待补充
创建对象的方式不一样,newInstance是使用类加载机制,new是创建一个新类因为软件的可伸缩、可扩展和可重用等软件设计思想,所以延申出两种创建方式
java的new和newInstance()创建对象的区别
这个需要结合 JVM 的相关知识,静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,然后通过类的实例对象去访问。在类的非静态成员不存在的时候静态成员就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。
为什么在一个静态方法内调用一个非静态成员为什么是非法的?
“static” 关键字表明一个成员变量或者是成员方法可以在没有所属的类的实例变量情况下访问。Java中static方法不能被覆盖,因为方法覆盖是基于运行时动态绑定的,而static方法是编译时静态绑定的。static方法跟类的任何实例都不相关,所以概念上不适用
“static”关键字是什么意思?Java中是否可以覆盖(override)一个private或者是static的方法?
面试题
Java基础
无参、参为另一个Map、参为容量、参为容量+加载因子
四个构造方法
key -> hashCode -> 扰动 -> (n-1)&hash -> 存在则比较key是否相同 -> 相同就覆盖不同就拉链法
1.7
key -> hashCode -> 扰动 -> (n-1)&hash -> 存在则比较key是否相同 -> 相同就覆盖不同就添加到树/链表 -> 可能树化
1.8
put
get 主要讲下要判断树和链表再get
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4 :容量默认16
当HashMap中元素数超过容量*加载因子时,HashMap会进行扩容
若扩容前容量为2的n次方,每个元素的索引由后n-1位决定,扩容后则由后n位决定,多了一位扩容时进行(n-1)&hash之后,若多的这一位为0则 新索引=原索引,为1则 新索引=原索引+原容量
1、扩容:创建一个新的Entry空数组,长度是原数组的2倍
2、ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组hash具体操作:新index = HashCode(Key) & (Length - 1)ps:%和/ 比 & 慢10倍左右,因此用(n-1)&hash
扩容步骤
扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f; :加载因子默认为0.75
threshold = capacity * loadFactor,当 Size>=threshold的时候就扩容loadFactor 太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 0.75f 是官方给出的一个比较好的临界值
补充
思路:默认容量,负载因子,扩容阈值,红黑树与链表转换阈值,JDK1.7与1.8的区别,为什么引入红黑树
JDK7:new HashMap() 时,底层创建 size 为 16 的数组Entry[]数据的储存位置一般是通过 hash(key.hashCode())%len 获得,也就是元素的 key 的哈希值对数组长度取模得到。例如12%16=12;28%16=12;108%16=12;140%16=12。所以 12、28、108 以及 140 都存储在数组下标为 12
JDK8:首次调用 put() 时,底层创建 size 为 16 的数组Node[]。
创建
JDK7:数组+链表
JDK8:数组+链表+红黑树。当数组某个索引位置的元素以链表形式存在的数据个数 >8 且当前数组的长度 >64 时,该索引位置上的所有数据改为使用红黑树存储。当链表长度大于8时,由单链表转化为红黑树;而当链表长度小于6时,又由红黑树转化为单链表
储存形式
底层
扩展:头插法
JDK7:头插法
扩展:尾插法
JDK8:尾插法
1.7hashmap因使用头插法,多线程扩容时会死循环,1.8改成尾插法
实际上hashmap还是线程不安全,还存在其他并发问题,例如源码中的put/get方法未加同步锁无法保证上一秒put的值,下一秒get的时候还是上一秒put的原值(可能在get前被别的线程又put了)
新节点插入顺序
JDK1.7和1.8的区别
hashmap(线程不安全)
segment数组,每个segment有一个hashEntry数组,每个hashEntry都是一条链表采用锁分离技术:分段锁保证读写线程安全
每一个 Segment 类似于 HashMap 的结构,所以每一个 HashMap 的内部可以进行扩容。但是 Segment 的个数一旦初始化就不能改变,默认 Segment 的个数是 16 个,意思ConcurrentHashMap默认支持最多 16 个线程并发
储存结构
初始化 并发级别 concurrencyLevel 大小,无参默认16
初始化 容量大小=concurrencyLevel 之上最近的 2 的幂次方,无参默认16
记录 segmentMask,默认是 ssize - 1 = 16 -1 = 15
初始化 segments[0],默认大小为 2,负载因子 0.75,扩容阀值是 2*0.75=1.5,插入第二个值时才会进行扩容
初始化
计算要 put 的 key 的位置,获取指定位置的 Segment,如果指定位置的 Segment 为空,则初始化这个 Segment
检查segment是否为空
使用 Segment[0] 的容量和负载因子创建一个 HashEntry 数组
使用创建的 HashEntry 数组初始化这个 Segment
cas检查segment是否为空
初始化segment流程
不存在:大于阈值就扩容+头插法插入
不存在一样的key:大于阈值就扩容+头插法插入
存在就覆盖并返回旧值
存在:遍历链表找到一样的key然后覆盖
自旋tryLock()上锁(segment继承了reentrantLock) -> 计算index -> 找到hashEntry并遍历
先判断count是否等于0,然后遍历链表,最后判断如果value等于null,就说明发生重排序,就加锁再获取这个值,最后解锁。
get
segment永远是16个(初始化可以改),只能扩容hashEntry数组,扩容机制和hashmap相同:容量*2,新索引=原索引 或 新索引=原索引+原容量
扩容(rehash)
Node 数组 + 链表 / 红黑树
自旋+cas。sizeCtl的值决定着当前的初始化状态
代表数组未初始化,容量大小默认为16
0
如果数组未初始化,则记录的是数组的初始容量如果已初始化,则记录的是数组的扩容阈值(初始容量*加载因子)
n(大于0)
数组正在进行初始化(调用put方法里的initTable())同时其他线程不能再初始化
-1
表示数组正在扩容,-(1+n)表示当前正有n个线程共同完成数组的扩容任务(有争议)
-n(小于0,但不是-1)
sizeCtl
CAS+自旋,没有用到锁
初始化:不是new的时候,而是调用put()方法里的putVal()里的initTable()
DEFAULT_CAPACITY = 16; 容量大小默认为16
无参
tableSizeFor用于设置容量大小,为传入参数+传入参数右移1位+1后向上取整2的幂次位数例如32,则位32+16+1=49,然后向上取2的6次方:64
initialCapacity
1个参数
但是内部方法实际上会传入三个参数,第三个为concurrencyLevel,默认传入为1
loadFactor
2个参数
initialCapacity (int)
loadFactor (float)
concurrencyLevel (int)
3个参数
new的时候传入参数
key -> hashCode -> cas写入 -> 若hashcode=-1则为正在扩容 -> 失败就用synchronized写 -> 大于阈值转树
找位置 -> 若hashcode=-1则为正在扩容 -> 遍历链表/树
1.7:Segment + HashEntry + ReentrantLock(Unsafe)
1.8:移除Segment,使锁的粒度更小,Synchronized + CAS + Node数组 + Unsafe
整体结构
1.7:先定位Segment,再定位桶,put全程加锁,没有获取锁的线程提前找桶的位置,并最多自旋64次获取锁,超过则挂起。
1.8:由于移除了Segment,类似HashMap,可以直接定位到桶,拿到first节点后进行判断,1、为空则CAS插入;2、为-1则说明在扩容,则跟着一起扩容;3、else则加锁put(类似1.7)
put()
1、jdk7和8的区别
2.1、为什么要使用synchronized而不是如ReentranLock这样的可重入锁?
2、jdk8为什么用cas+synchronized代替分段锁?
3、jdk8为什么舍弃segment的原因?
segment主要是为了分段锁1.如果使用reentrantlock则每个节点都会继承AQS获取同步支持,增加内存开销。而jdk8中只有链表或树根需要同步支持。2.synchronized是JVM自带的,所以运行时JVM对synchronized有一定的调优
concurrenthashmap(线程安全)
都实现了list
Vector古老、线程安全。ArrayList常用、线程不安全
与Vector
都实现了list,都线程不安全
数组和链表的区别
与LinkedList(双向链表)(并发版ConcurrentLinkedQueue
对比
无参、参为容量、参为另一个Collection
构造方法
1.7中类似饿汉式初始化数组,1.8类似懒汉式延迟加载
无参构造时是个空数组,向其中加入第一个元素时,才扩容为10
当我们要 add 进第 1 个元素到 ArrayList 时,elementData.length 为 0 (因为还是一个空的 list),因为执行了 ensureCapacityInternal() 方法 ,所以 minCapacity 此时为 10。此时,minCapacity - elementData.length > 0成立,所以会进入 grow(minCapacity) 方法。
当 add 第 2 个元素时,minCapacity 为 2,此时 e lementData.length(容量)在添加第一个元素后扩容成 10 了。此时,minCapacity - elementData.length > 0 不成立,所以不会进入 (执行)grow(minCapacity) 方法。
添加第 3、4···到第 10 个元素时,依然不会执行 grow 方法,数组容量都为 10。
直到添加第 11 个元素,minCapacity(为 11)比 elementData.length(为 10)要大。进入 grow 方法进行扩容
grow方法
ArrayList(并发版CopyOnWriteArrayList
set的底层结构是hashmap,它add()里的Put()方法使用的是hashmap的put,所以是当key值相同时,覆盖旧值返回之前的对象值present!=null返回false用空间换取一些信息
Set
Java容器
图示
class文件基本组织结构
Class文件中的常量池详解(上)
Class文件中的常量池详解(下)
1.常量池中,小于等于32767的int型常量不会保存,为1的long型数据不出现在常量池
字节码文件(.class文件)
数组类型不通过类加载器创建,它由JVM直接创建
加载(loading):将class字节码文件存放到方法区,在堆中创建Class对象
验证(verification):校验字节码文件的正确性,文件格式是否符合class文件规范;元数据验证,符号引用验证
静态变量=类变量实例变量=普通变量=非static变量
准备(preparation):为类的静态类变量分配内存,并初始化为默认值0值(不是指定值) 而实例变量会随着new了个实例对象后,在堆内分配空间 特例:加了final static的会在这个阶段直接赋指定值
符号引用和直接引用的区别
解析(resolution):把类中的符号引用转换成直接引用
链接(linking)
初始化(initialization):对类的静态变量初始化赋值,并执行静态代码块
类加载的过程(加载、验证、准备、解析、初始化)
Java中Class对象详解
参考文献
类加载过程
类加载底层调用流程(所有的类都由类加载器加载,加载的作用就是将 .class文件加载到内存)
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。这样避免了在lib目录下的文件被替换
保证加载类的唯一性:当父加载器已经加载了该类时,就没有必要子加载器再加载一次
Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?——CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。所有的Java程序运行都可以指定沙箱,可以定制安全策略
沙箱安全机制:自己写的java.lang.String.class不会被加载,防止核心api库被篡改
BootstrapClassLoader用c++写的
负责加载用户自定义路径下的jar包和类
userDefinedClassLoader自定义类加载器
在双亲委派机制下自定义类加载器不打破双亲委派模型,继承ClassLoader类+重写ClassLoader类中的findClass()方法,无法被父加载器加载的类最终会通过这个方法被加载想打破双亲委派模型,继承ClassLoader类+ 重写findClass方法+重写loadClass方法或者使用上下文加载器
自定义类加载器有什么用?jvm自带的三个加载器只能加载指定路径下的类字节码自定义类加载器可以加载本机或网络上的某个类文件
打破双亲委派机制的场景?Tomcat。一个tomcat容器可能运行多个应用程序,每个应用需要同一种类库的不同版本,因此应用之间需要独立和隔离
双亲委派机制
扩展类、应用类和自定义类加载器都是java.lang.ClassLoader的子类实例自定义类加载器直接继承java.lang.ClassLoader
AppClassLoader的父类加载器输出为ExtClassLoaderExtClassLoader的父类加载器输出为nullnull并不代表ExtClassLoader没有父类加载器,而是 BootstrapClassLoader
由于应用类加载器和扩展类加载器都继承了ClassLoader,而ClassLoader类中有parent变量,用于指示向上委托的方向,而不是通过下层继承上层实现
创建一个classloader,parent设置成null
Java 类加载器怎么实现将同一个对象加载两次?
类加载器
Java安全模型
沙箱安全机制
存储对象,字符串常量池,静态变量
堆
存储包括类定义,结构,字段,方法(数据及代码)以及常量在内的类相关数据(类的信息不是Class对象)
jdk7永久代
元空间代替了永久代,存储在本地内存(本地内存包含元空间和直接内存),存放已被虚拟机加载的类信息、常量、静态变量,即时编译器编译后的代码
jdk8元空间
1.永久代的大小不好设置(类信息放在永久代的话,加载时不知道有多少个类),而且如果类加载过多很容易导致OOM2.永久代的回收效率低
永久代被代替的原因
运行时常量池:一直在方法区,除了上述还包括字面量和符号引用
字符串常量池:1.7在方法区,1.8在堆
class文件常量池(非运行时常量池)
详解:https://www.jianshu.com/p/cf78e68e3a99待补充https://www.processon.com/view/5dec50d7e4b0e2c298a6bd60?fromnew=1
常量池
因为永久代的回收效率很低,通常需要fullGC才进行回收,而fullGC是老年代触发majorCG后空间还不够或永久代快满才触发。放入堆中能够及时回收。
为什么jdk7之后字符串常量池要放入堆中呢?
Java官方的HotSpot 虚拟机中,Java8版本以后,是用元空间来实现的方法区;在Java8之前的版本,则是用永久代实现的方法区;也就是说,“元空间” 和 “方法区”,一个是HotSpot 的具体实现技术,一个是JVM规范的抽象定义;
方法区
存放基本数据类型和对象引用
Solt槽是局部变量表的最小单位每个变量槽可以存放32位长度的内存空间如果要放64位的数据就找两个槽
solt槽
局部变量表
用来存放操作数
操作数栈
每个栈帧都有一个指向运行时常量池该栈帧所属方法的引用。目的是当调用其它方法时,从运行时常量池找到对应的符号引用,并转成直接引用找到该方法。
动态链接
记录PC寄存器的值
当一个方法开始执行后,只有2种方式可以退出这个方法 : 方法返回指令 : 执行引擎遇到一个方法返回的字节码指令,这时候有可能会有返回值传递给上层的方法调用者,这种退出方式称为正常完成出口。 异常退出 : 在方法执行过程中遇到了异常,并且没有处理这个异常,就会导致方法退出。 无论采用任何退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息。 一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中会保存这个计数器值。 而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。
方法返回地址
锁记录
栈帧
虚拟机栈
简单来讲就是Java调用非Java代码接口为虚拟机使用的native方法服务,方法通常是使用C/C++编写,然后编译成.dll或者.so文件,再由JNI(Java Native Interface)调用执行。
本地方法栈
它的作用主要是通过程序计数器指针指向常量池的下一条偏移地址,执行引擎根据偏移地址获取机器指令,再交给CUP执行
程序计数器(唯一不会OOM的区域)
运行时数据区(java内存结构)
直接内存并不是jvm运行时数据区的一部分,也不是jvm规范中定义的内存区域。在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式它使用native 函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。本机直接内存的分配不会受到Java 堆大小的限制,受到本机总内存大小限制配置虚拟机参数时,不要忽略直接内存 防止出现OutOfMemoryError异常解决了NIO进行管道传输buffer时,发生full GC导致buffer位置发生改变。
直接内存创建和销毁更费性能,而IO读写的性能要优于普通的堆内存
和堆内存对比
显式调用System.gc()强制执行FullGC进行回收
每个DirectByteBuffer对象在初始化时,都会创建一个对应的Cleaner对象这个Cleaner对象会在合适的时候执行unsafe.freeMemory(address),从而回收这块堆外内存当directByteBuffer设为null后,指向它的虚引用cleaner就会进入pending队列,当发现队列里有cleaner对象,就会调用方法把堆外的buffer给清掉
直接内存是否会被GC?会
有很大的数据需要存储,它的生命周期很长
适合频繁的IO操作,例如网络并发场景
直接内存使用场景
加快了复制的速度。因为堆内在flush到远程时,会先复制到直接内存(堆外内存),然后在发送;而直接内存(堆外内存)相当于省略掉了这个工作
优点
直接内存难以控制,如果内存泄漏,那么很难排查,且不适合储存很复杂的对象
直接内存(堆外内存)
句柄 优势:reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改对象实例数据(堆):对象中各个实例字段的数据对象类型数据(方法区):对象的类型、父类、实现的接口、方法等静态区(也在方法区中)用来存放静态变量,静态块
直接指针 优势:速度快,它节省了一次指针定位的时间开销
对象自身的运行时数据(哈希码、GC 分代年龄、锁状态标志等等
markword
ps:并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说查找对象的元数据信息并不一定要经过对象本身
类型指针:指向自己是哪个类的实例
数组长度(只有数组对象才有)
对象头
指定的数据
实例数据
jvm要求对象大小为8字节整数倍,对象头大小为8字节的1~2倍,只有实例数据可能没对齐,所以要填充补齐
对齐填充
对象的内存布局
对象的访问定位
第一步:类加载检查先检查要new的对象对应的类在运行时常量池(方法区里)里有没有符号引用,如果有就检查这个符号引用代表的类有没有被加载、解析或初始化过ps:class文件常量池主要存放两大常量:字面量和符号引用,class常量池加载到内存中是运行时常量池
①指针碰撞:前提要求是JAVA堆中的内存对象是绝对完整的,所有的内存都放在一边,空闲的放在另一边,中间放着一个指针作为分界点,分配时即把指针移动类大小即可②空闲列表:这时的堆内存是相互交错的,虚拟机维护了一个列表记录了堆中哪里还有足够大的空闲内存可以使用,然后分配一块类需要的内存大小。这两种方法和垃圾收集算法选取有关系,比如使用Serial\\Parnew等带Compact过程的系统采用①,CMS基于Mark-Sweep(标记清除)的使用②
分配内存空间时可能存在线程安全问题解决办法:①对内存分配采取同步处理,实际上是虚拟机采用CAS算法失败重试的方式保证更新操作的原子性②TLAB(本地线程缓冲),为每个线程在Java堆中预先分配一小块内存,每一个线程预先在 Eden 区分配一块内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
第二步:分配内存为对象在堆内存分配一块空间,该空间大小在类加载完成后确定分配空间根据堆是否规整有两种方法:1、指针碰撞 2、空闲列表
至此,从虚拟机视角来看,一个新的对象已经产生了。但是在Java程序视角来看,执行new操作后会接着执行如下步骤
对象的init() : Java 在编译之后会在字节码文件中生成 init 方法,称之为实例构造器,该实例构造器会将语句块,变量初始化,调用父类的构造器等操作收敛到 init 方法中,收敛顺序为
new一个对象的完整过程
new对象的过程
如果字符串常量池有abc则在堆上创建1次对象(ps:字符串常量池则存在于方法区)
变式: String str1 = new String(\"A\"+\"B\")String str2 = new String(\"ABC\") + \"ABC\"
String s1 = new String(\"abc\");这句话创建了几个字符串对象?
jdk6之前:查看str1的字面量在字符串常量池中有没有,有则直接返回在常量池的值,没有则新建再返回。
jdk7之后:查看str1的字面量在字符串常量池中有没有,有则直接返回在常量池的值,没有则将堆中引用添加到字符串常量池,返回堆中值。
str1.intern() 过程
对象的init方法和类的clinit方法区别
问题
对象
JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证(如果A线程的写操作a与B线程的读操作b之间存在happens-before关系,尽管a操作和b操作在不同的线程中执行,但JMM向程序员保证a操作将对b操作可见)
在同一个线程中,书写在前面的操作happen-before后面的操作。
单线程的hb规则
同一个锁的unlock操作happen-before此锁的lock操作
监视器锁的hb规则
对一个volatile变量的写操作happen-before对此变量的任意操作(包括写操作)
volatile的hb规则
如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作
hb的传递性
同一个线程的start方法happen-before此线程的其它方法
线程启动hb规则
对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码
线程中断hb规则
线程中的所有操作都happen-before线程的终止检测
线程终结hb原则
一个对象的初始化完成先于他的finalize方法调用
对象创建hb原则
happens-before原则
可能导致线程安全问题,如DCL问题
重排序
《java并发编程实战》之java内存模型
8大happen-before原则超全面详解
volatile内存可见性和禁止指令重排序
jmm
java内存模型(JMM)定义了多线程和共享内存之间的并发模型
解释器:输入的代码 -> [ 解释器 解释执行 ] -> 执行结果
JIT编译器:输入的代码 -> [ 编译器 编译 ] -> 编译后的代码 -> [ 执行 ] -> 执行结果
JIT执行编译后的代码比解释器快,但如果代码只执行一次(如只被调用一次,例如类的构造器(class initializer或者没有循环的代码)则解释器比JIT编译器快
将代码一行行解析,启动直接运行,速度慢,而且会有重复代码
interpreter
在解析器在解析代码时,当虚拟机发现某些代码运行比较频繁时,(也就是热点代码)。JIT即时编译器就会把这些代码片段全部编译打包成可执行文件。开始执行时间会比较晚
回边计数器--也就是计算循环体执行的次数
方法调用计数器
检测热点代码
逃逸分析、 锁消除、 锁膨胀、 方法内联、 空值检查消除、 类型检测消除、 公共子表达式消除
优化代码
通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上
通俗来讲当对象的指针被多个方法或线程引用,称为逃逸分析
JIT判断同步块里的锁对象如果只被一个线程调用,那么同步锁取消。
同步省略
JIT发现当一个对象不会被外界访问到,就会将对象分成若干个标量(标量就是最小不可分割的单位)比如基本数据类型
标量替换
由于JIT将对象进行标量替换后,对象变成基本类型,存储在栈帧里,变量随着方法运行完而释放,减少了垃圾回收器的压力。
栈上分配
开启逃逸分析:-XX:+DoEscapeAnalysis 关闭逃逸分析:-XX:- DoEscapeAnalysis从jdk1.7版本开始默认开启逃逸分析
逃逸分析
HotSpot集成了两个JIT compiler — C1及C2(或称为Client及Server,C++实现)目前主流的HotSpot虚拟机中默认是采用解释器与其中一个编译器直接配合的方式工作。程序使用哪个编译器,取决于虚拟机运行的模式。HotSpot虚拟机会根据自身版本与宿主机器的硬件性能自动选择运行模式,用户也可以使用“-client”或“-server”参数去强制指定虚拟机运行在Client模式或Server模式。
运行在客户端,编译速度快
C1(Client Complier)
运行在服务端,编译质量好
C2(Server Complier)
Graal
JIT的C1和C2
分类
JVM即时编译器
JIT(Just In Time) compiler
并从from(to)区的数据移到to(from)区,年龄+1,当年龄=15就会晋升到老年代-XX:MaxTenuringThreshold=15至于为什么是 15次,原因是 HotSpot会在对象头的中的标记字段里记录年龄,分配到的空间只有4位,所以最多只能记录到15
Minor GC触发条件:当Eden区满时,触发Minor GC
显式调用System.gc时,系统建议执行Full GC,但是不必然执行
老年代/方法区满
通过Minor GC后进入老年代的平均大小大于老年代的可用内存
Full GC触发条件:
面试题:为什么s区要划分为from和to? 因为 避免碎片+使用标记复制算法如果只有Eden和from,Eden满,发生minorGC第一次minorGC之后,Eden为空,from有从Eden复制过来的存活对象第二次minorGC之后,Eden又会向from复制转移,但此时from也有死亡的和存活需要复制的对象,那么from无法使用标记复制(无处可去)如果有from和to区,那么Eden和from就可以复制到to,然后交换from和to,使to区永远为空。(to区满后转移到老年代)
MGC、YGC、OGC、FGC等差别
Eden:from:to = 8:1:1 -XX:survivorRatio新生代:老年代 = 1:2 -XX:Ratio
如果将要放入s0区的对象大小超过s0区容量的50%,那么其中年龄最大的对象就会被转移到老年代(一般在minor gc后触发)
对象动态年龄判断
1.如果Eden区MinorGC后,s区也放不下,则通过 分配担保机制 把新生代的对象提前转移到老年代中去2.大对象直接放入老年代,为了避免为大对象分配内存时由于 分配担保机制 带来的复制而降低效率。
分层编译模式
老年代空间分配担保机制
引用计数法 无法解决循环依赖问题
可作为gc roots的对象:虚拟机栈(栈帧中的本地变量表)中引用的对象本地方法栈(Native 方法)中引用的对象方法区中类静态属性引用的对象方法区中常量引用的对象所有被同步锁持有的对象
可达性分析
想中断或者回收强引用对象,可以显式地将引用赋值为null,变为软引用,这样的话JVM就会在合适的时间,进行垃圾回收或者指向另一对象:obj= newObject();
强引用 一定不收即使OOM
使用较多,可以同时保证 垃圾回收速度 和 不OOM
可以和引用队列一起用
软引用 空间不够才收
弱引用和软引用的区别在于:弱引用的对象拥有更短的生命周期,只要垃圾回收器扫描到它,不管内存空间充足与否,都会回收它的内存
弱引用 扫描到就收
如果一个对象没有强引用和软引用,对于垃圾回收器而言便是可以被清除的,在清除之前,会调用其finalize方法,如果一个对象已经被调用过finalize方法但是还没有被释放,它就变成了一个虚可达对象(如果一个对象与GC Roots之间仅存在虚引用,则称这个对象为虚可达对象)
虚引用通常与引用队列结合使用一个对象将要被回收之前将虚引用放入队列,程序从队列中检测到这个对象将要回收之前可以做些事情
在于跟踪垃圾回收过程,在对象被收集器回收时收到一个系统通知。 当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在垃圾回收后,将这个虚引用加入引用队列,在其关联的虚引用出队前,不会彻底销毁该对象。 所以可以通过检查引用队列中是否有相应的虚引用来判断对象是否已经被回收
虚引用 不影响对象生命周期,随时被收
当引用的对象将要被JVM回收时,会将其加入到引用队列中。应用:通过引用队列可以了解JVM垃圾回收情况
引用队列
对象持有的四种引用
常量池中的常量若没被引用便属于废弃常量
废弃类:需同时满足 实例被回收+ClassLoader被回收+类没被引用
对象是否死亡
原地清除垃圾,会产生碎片
标记清除
空间一分为二。先把有用的复制到另一边,再清空这边
标记复制
标记整理耗时>=标记复制虽然整理与复制都涉及移动对象,但取决于具体算法,整理可能要 计算对象目标地址,修正指针,移动对象;复制则可以把这几件事情合为一体来做,所以可以快一些
直接靠一端放,即使出现覆盖
标记整理
新生代存活率低用标记复制,只有少量会复制
老年代存活率高且都是大对象没有足够空间对它担保,用标记清除或标记整理
分代收集
效率:复制算法>标记整理>标记清除内存整齐度:复制=标记整理>标记清除内存使用率:标记整理=标记清除>复制
垃圾回收算法
jvm垃圾收集图示链接
年轻代 串行回收 标记复制 -XX:UseSerialGC (开启后会使用Serial + Serial Old的组合收集器)
serial
老年代 串行回收 标记整理
serial old
年轻代 并行回收 标记复制 -XX:UseParNewGC(开启后会使用ParNew + Serial Old的组合收集器)
parNew
年轻代 并行回收 标记复制 (对应的JVM参数 -XX:UseParallelGC 或 -XX:UseParallelOldGC (可互相激活))(开启后会使用Parallel + Parallel Old的组合收集器)
parllerl Scavenge
老年代 并行回收 标记整理
parllerl old
老年代 并发回收 标记清除
初始标记:标记GCRoot直接关联的对象并发标记:从GCRoot关联的对象开始,往下遍历整棵树重新标记:因为并发标记是并发的,会新增一些需标记的对象,所以重新标记并发清理:清理未被标记的对象
优点:最耗时的并发标记和并发清除是并发进行的,所以低停顿,低延时。
1.old gen使用标记清除算法,会造成内存碎片
2.并发标记和并发清理,会占用线程资源,降低吞吐量(吞吐量 = 运行用户代码时间 /(运行用户代码时间+垃圾收集时间))
3.并发清除时候,其他线程产生的垃圾对象,无法被清理会造成浮动垃圾,只能下次GC清理
4.当触发FullGC时,CMS会尝试通过一次串行的完整垃圾收集来回收碎片化的堆内存,这个过程会持续很长时间,造成STW
CMS
适用于多处理器和大容量内存服务器的垃圾收集器,满足 GC停顿时间要求 和 高吞吐量
局部看是标记复制,整体看是标记整理
并发+并行回收 分代收集 可预测停顿
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)
为什么可以预测停顿时间
如果短期的大对象放入老年代,会影响效率
为何设置H区?
扫描根和本地变量
根扫描
处理dirty card队列,更新Rset
更新Rset
检测年轻代指向老年代的对象
处理Rset
年轻代复制到survival/old区
复制对象
处理软引用、弱引用、虚引用
处理引用
youngGC
标记GCRoot直接可达的对象,此过程会触发一个youngGC,因为要跟新Rset引用
初始化标记stw
扫描初始化标记的存活区域对老年区的引用,并标记被引用的对象
根区域扫描
扫描标记在整个堆中存活的对象
并发标记
标记并发标记时新产生的对象,修改SATB缓存
最终标记stw
清理stw
使用写屏障,每次修改引用时都会将旧值写入long buffer。在最终标记时会修改快照
G1如何解决并发标记时对象的引用发生改变?如何解决存活的对象与快照不一样?
三色算法
三色标记算法
Rset
card table
全局并发标记
拷贝存活对象
mixedGC
回收流程
优点:1.G1是一个有整理内存过程的收集器,不会产生很多碎片2.stw可以控,G1在停顿时间上添加了预测机制,用户可以根据预期设置停顿时间3.region可以不连续,所以对内存负担小
优缺点
G1(JDK9默认)
与 CMS 中的 ParNew 和 G1 类似,ZGC 也采用标记-复制算法,不过 ZGC 对该算法做了重大改进在 ZGC 中出现 Stop The World 的情况会更少
颜色指针
读屏障
多重映射
ZGC jdk11
epsilon jdk11
垃圾回收器
GC
执行引擎
每次垃圾回收的时间越来越长,由之前的10ms延长到50ms左右,FullGC的时间也有之前的0.5s延长到4、5sFullGC的次数越来越多,最频繁时隔不到1分钟就进行一次FullGC年老代的内存越来越大并且每次FullGC后年老代没有内存被释放之后系统会无法响应新的请求,逐渐到达OutOfMemoryError的临界值,这个时候就需要分析JVM内存快照dump。
1.监控GC的状态2.生成堆的dump文件3.分析dump文件4.分析结果,判断是否需要优化5.调整GC类型和内存分配6.不断分析和调整
为什么调优?除了为了解决已出现的问题,更重要的是为了减少full gc次数(即减少stw时间)
亿级流量,日活500万用户,付费转化率10%,日均50万单,大促的时候每秒1000多单,有3台订单系统集群,配置均为4核8G,每秒可处理300单一个订单中有几十个字段int为4字节,long为8字节,假设每个订单对象大小为1kb,每秒有300kb对象生成加上围绕订单还需要一些库存、优惠券、积分对象,再扩大20倍,加上订单查询、修改、退款,再扩大10倍即每秒产生300kb*200=60MB对象,这些对象在生成后1秒就会变成垃圾,因为订单对象后面不再需要按4核8G来算,一般是留4G给系统,4G给jvm,jvm其他部分1G,给堆3G,Eden:s0:s1=800M:100M:100M,老年代2G因此在大概14s后,Eden区已满(全是垃圾),发生MinorGC后,最后这次没生成完的订单不会在这次gc中被清除,因为还有外部引用着,且不能停留在s0区,因为这里没生成完的对象大小超过了100M的50%(对象动态年龄判断)于是被放入老年代,也就是说,每15s就有50+MB对象被送入老年代,且这些对象都会在1s后变为垃圾,老年代很快就满,FullGC次数猛增
解决方案:调高新生代内存,使得大多数订单对象都可以在新生代自生自灭,不进入老年代
调优案例1
单机几十万请求。Eden:s0:s1=32G:4G:4G,老年代24G,为了应对初始对象过多,将新生代调大。但是在这种情况下,MinorGC一次实际上比FullGC还慢,因为新生代太大需要很长时间标记
解决方案:使用G1收集器的-XX:MaxGCPauseMillis(目标最大暂停时间,默认200ms。每200ms就发生一次MinorGC,不会等到Eden区满),发生MinorGC时只回收Eden区较少的空间,比如一次只回收4G,解决一次stw时间过长的问题
调优案例2
某个高QPS的服务重启的时候load会非常高,CPU使用率过高,在一段时间后负载才降下来。启动后高负载的原因大致是由于启动,随着代码的执行,jvm的JIT编译器会将部分热点代码编译为目标机器代码,这时产生的编译线程会占用大量的cpu 导致系统负载高。JIT编译器需要代码执行一定的频率才会进行编译优化,系统刚启动的时候大部分的代码只是解释执行,解释执行的性能比编译执行的性能当然要差很多,所以系统的负载高,等到主要热点代码都是编译执行,系统负载就降下来。 那接下来如何对这个问题调优
解决方案:JIT分层编译模式
效果:线上环境一台机器加入分层编译参数-XX:+TieredCompilation之后,在大多数情况下启动之后负载都不会升高,有时候即使有会升高,也比默认的恢复快很多。
调优案例3
调优指令
live 只dump存活的对象,如果不加则会dump所有对象format=b 表示以二进制格式file=filepath 输出到某个文件中
jmap使用
除了程序计数器,都可能发生OOM
调大堆:-Xmx
堆满
永久代满
内存泄漏
使用 -XX:-UseGCOverheadLimit 禁用这个提示功能
GC 开销超过限制由于某种原因,垃圾收集器占用了过多的时间(默认为进程所有CPU时间的98%),每次运行时恢复的内存非常少(默认为堆的2%)这实际上意味着您的程序停止执行任何进度,并且一直忙于只运行垃圾收集
数组过大
发生OOM的情况
由于 Full GC 的成本要远远高于 Minor GC ,因此尽可能将对象分配在新生代,在JVM 调优中,可以为应用程序分配一个合理的新生代空间,以最大限度避免新对象直接进去老年代。注意:由于新生代垃圾回收的速度高于老年代回收,因此,将年轻对象预留在新生代有利于提高整体的 GC 效率
1、将新对象预留在新生代
大对象占用空间多,如果直接放入新生代中会导致新生代空间不足,这样将会把大量的较小的年轻代对象移入到老年代中如果有短命大对象,原本存放于老年代的永久对象会被短命大对象塞满,扰乱了分代内存回收的基本思路
解决方法:-XX:PretenureSizeThreshold 设置大对象直接进入老年代的阀值,当对象超过这个阀值时,将直接在老年代中分配。PS:-XX:PretenureSizeThreshold 只对串行收集器和新生代并行收集器有效,并行回收收集器不识别这个参数。
需要占用大量连续内存空间的java对象是大对象,比如很长的字符串和数组。1.需要占用大量非连续空间的java对象不能称为大对象2.一个对象有很多属性也不能成为大对象
扩展:大对象
2、大对象进入老年代
因为对象满15岁后将被移到老年代,可以通过XX:MaxTenuringThreshold:默认值是15,这个参数是指定进入老年代的最大年龄值,对象实际进入老年代的年龄是 JVM 在运行时根据内存使用情况动态计算的。如果希望对象尽可能长地留在新生代中,可以设置一个较大的阀值。
3、设置对象进入老年代的年龄
稳定的堆大小对垃圾回收是有利的,获得一个稳定堆大小的方法就是设置 -Xmx 和 -Xms 一样的值。不稳定的堆也不是木有用处,让堆大小在一个区间内震荡,在系统不需要使用大内存时压缩堆空间,使 GC 应对一个较小的堆,可以加快单次 GC 的速度。基于这种思想,JVM 提供了两个参数用于压缩和扩展堆空间
4、稳定与震荡的堆大小
5、吞吐量优先设置
使用大的内存分页可以增强 CPU 的内存寻址能力,从而提高系统的性能
6、使用大页案例
为了降低应用软件在垃圾回收时的停顿,首先考虑的使用关注系统停顿的 CMS 回收器,为了减少 Full GC 的次数,应尽可能将对象预留在新生代,新生代 Minor GC 的成本远远小于老年代的 Full GC
7、降低停顿案例
8、分层编译模式
jvm调优
面试题:JVM的调优方式有哪些
JVM调优
JVM
在一操作或多个操作中要么全部执行成功,否则全部失败
原子性
一个线程修改了共享变量数据,其它线程能知道
可见性
按代码的循序执行
指令重排:jvm为了程序的效率会将代码循序重排,单线程不影响结果,多线程有影响。避免使用Executors 工具类创建
有序性
并发的三大特性
认为别的线程不会去修改值,不加锁。如果发现值被修改了,可以再次重试。乐观锁可以使用版本号机制和CAS算法实现。在 Java 语言中 java.util.concurrent.atomic包下的原子类就是使用CAS 乐观锁实现的。
乐观锁
认为别的线程会修改值,会加锁。在 Java 语言中 synchronized 和 ReentrantLock等就是典型的悲观锁,还有一些使用了 synchronized 关键字的容器类如 HashTable 等也是悲观锁的应用。
悲观锁
乐观锁适合读多写少的场景,因为不用上锁、释放锁,省去了锁的开销,从而提升了吞吐量。
写多读少的场景,即冲突比较严重,线程间竞争激励,使用乐观锁就是导致线程不断进行重试,这样可能还降低了性能,这种场景下使用悲观锁就比较合适
比较
乐观锁/悲观锁
锁一次只能被一个线程所持有。如果一个线程对数据加上排他锁后,那么其他线程不能再对该数据加任何类型的锁。获得独占锁的线程即能读数据又能修改数据。JDK中的synchronized和java.util.concurrent(JUC)包中Lock的实现类就是独占锁。
独占锁
锁可被多个线程所持有。如果一个线程对数据加上共享锁后,那么其他线程只能对数据再加共享锁,不能加独占锁。获得共享锁的线程只能读数据,不能修改数据。在 JDK 中 ReentrantReadWriteLock 就是一种共享锁。
共享锁
独占锁/共享锁
互斥锁是独占锁的一种常规实现,是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。互斥锁一次只能一个线程拥有互斥锁,其他线程只有等待。
互斥锁
是共享锁的一种具体实现。读写锁管理一组锁,一个是只读的锁,一个是写锁。读写和写写,写读都是互斥的
读写锁
互斥锁/读写锁
公平锁是指多个线程按照申请锁的顺序来获取锁。在 java 中可以通过构造函数初始化公平锁Lock lock = new ReentrantLock(true);
公平锁
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序在 java 中 synchronized 关键字是非公平锁,ReentrantLock默认也是非公平锁。
非公平锁
公平锁/非公平锁
可重入锁又称之为递归锁,是指同一个线程在外层方法获取了锁,在进入内层方法会自动获取锁。
可重入锁
可重入锁/不可重入锁
是一种锁的设计,并不是具体的一种锁。
分段锁
synchronized 关键字内部实现原理就是锁升级的过程:无锁 --> 偏向锁 --> 轻量级锁 --> 重量级锁。
无锁/偏向锁/轻量锁/重量锁
当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环
概念
CAS
实现
1、自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
2、非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)
1、如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高
2、上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题
自旋锁
图解java中的18把锁
15种锁
synchronized是一种同步锁,用于修饰类、实例方法、静态方法和代码块,保证原子性、可见性、有序性和可重入性
同一线程在调用自己类中其他synchronized方法/块或调用父类的synchronized方法/块都不会阻碍该线程的执行,就是说同一线程对同一个对象锁是可重入的,而且同一个线程可以获取同一把锁多次,也就是可以多次重入
介绍
synchronized有两种形式上锁:对 方法 和 代码块 上锁同步方法和同步代码块都是在进入同步代码之前先获取锁,拿到锁则计数器+1,执行完-1如果获取失败就阻塞式等待锁的释放。
对方法上锁:在方法的flags里面多了一个ACC_SYNCHRONIZED标志,这标志用来告诉JVM这是一个同步方法,在进入该方法之前先获取相应的锁,锁的计数器加1,方法结束后计数器-1,如果获取失败就阻塞住,直到该锁被释放
对代码块上锁:同步块是由monitorenter指令进入,然后monitorexit释放锁,在执行monitorenter之前需要尝试获取锁,如果这个对象没有被锁定,或者当前线程已经拥有了这个对象的锁,那么就把锁的计数器加1。当执行monitorexit指令时,锁的计数器也会减1
识别方式
加锁原理
Mark Word 存储对象的HashCode,分代年龄和锁标志位信息
Klass Point 对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
EntryList
Owner 会指向持有 Monitor 对象的线程
WaitSet
Monitor
扩展:对象头
synchronized对java对象加锁实际上是对对象头里的markword操作,Mark Word里默认存储对象的HashCode,分代年龄和锁标记位,运行期间Mark Word里存储的数据和锁的状态会随着锁标志位的变化而变化
底层实现(内存空间操作)
markword内 锁状态标记 001
无锁 001
减少同一线程获取锁的代价。在大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得,那么此时就是偏向锁
偏向锁 101
一个对象持有偏向锁时,又有另外线程来获取锁的时候,就会将101改为00,并且把hashcode记录在栈帧里的lock record中。对象头就记录着lock record的地址和状态。 当存在第二个线程申请同一个锁对象时,偏向锁就会立即升级为轻量级锁。注意这里的第二个线程只是申请锁,不存在两个线程同时竞争锁,可以是一前一后地交替执行同步块
轻量级锁 00
当该对象持有轻量级锁的时候,有另一线程thread-1来获取锁时,获取失败自旋几次后还失败的话就会进入锁膨胀,向系统申请一个monitor锁,对象头改为记录着monitor地址,状态从00改为10.(但此时还是thread-0线程获取着锁).所以升级后thread-1就会进入entryList阻塞。\t当thread-0解锁时就要用monitor的解锁方式了,就要根据monitor锁的地址找到monitor,然后将owner设为null,然后唤醒EntryList中阻塞的线程。 当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大。重量级锁一般使用场景会在追求吞吐量,同步块或者同步方法执行时间较长的场景
重量级锁 10
锁状态
无锁->偏向锁->轻量级锁->重量级锁(方向不可逆)
锁的膨胀方向
锁膨胀
这种优化得更彻底,在JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁
锁消除
博客
锁粗化是虚拟机对另一种极端情况的优化处理,通过扩大锁的范围,避免反复加锁和释放锁。(例如while循环内执行100次append,没有锁粗化的就要进行100次加锁/解锁,如果加在while循环体外,则只加锁一次即可)
锁粗化
获取轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。 自旋锁:许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得,通过让线程执行循环等待锁的释放,不让出CPU。如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式。但是它也存在缺点:如果锁被其他线程长时间占用,一直不释放CPU,会带来许多的性能开销。 自适应自旋锁:这种相当于是对上面自旋锁优化方式的进一步优化,它的自旋的次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的缺点。
自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快 非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)
自旋锁和自适应自旋锁
JVM对Synchronized的优化
Synchronized
如果锁计数器为0,则直接cas上锁
若锁是自己这个线程拿的,那么计数器+1,并执行setState方法
用cas抢占锁
return nonfairTryAcquire
tryAcquire
面试题:addWaiter()为什么要从尾部遍历?新节点pre指向tail,tail指向新节点,这里后继指向前驱的指针是由CAS操作保证线程安全的。而cas操作之后t.next=node之前,可能会有其他线程进来。所以出现了问题,从尾部向前遍历是一定能遍历到所有的节点
获取锁失败的线程如何安全的加入同步队列:addWaiter()
addWaiter
线程加入同步队列后会做什么:acquireQueued()
acquireQueued
acquire()三个最重要的方法
非公平模式加锁流程
调用unlock()方法,unlock方法调用了release()方法
release()方法调用tryrelease()方法释放锁,并调用unparkSuccessor(h)唤醒同步队列里阻塞的线程
unparkSuccessor(h)
非公平模式解锁流程
NonfairSync
hasQueuedPredecessors(公平比非公平只多了这一个方法)
如果是当前持有锁的线程 可重入
FairSync
ReentrantLock的state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的
可重入性
公平锁类型与非公平锁类型
getState()
setState()
compareAndSetState()
state(volatile修饰的int类型)
负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常
CANCELLED(1) 表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化
SIGNAL(-1) 表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL
CONDITION(-2) 表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
PROPAGATE(-3) 共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
0 新结点入队时的默认状态
结点状态waitStatus
CLH双向队列(多线程争用资源被阻塞时会进入此队列)
入队 出队
头结点设计
共享和独享的实现
unsafe.
原理
实际应用
cpu开销
AtomicReference
只能保证一个共享变量原子操作
标志位 时间戳
解决:使用版本号如AtomicStampedReference类最后cas两个pair对象
ABA
存在的问题
AQS详解
AbstractQueuedSynchronizer(AQS)
导航博客
Reentranlock
ReentrantReadWriteLock
Lock
ThreadLocal意为线程变量,ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
简介
1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。2、线程间数据隔离3、进行事务操作,用于存储线程事务信息。4、数据库连接,Session会话管理。
使用场景
继承结构
结构
code
Entry
ThreadLocalMap(关键静态内部类)
SuppliedThreadLocal(继承了ThreadLocal)
源码
ThreadLocal什么使用弱引用
内存泄漏问题
ThreadLocal
就绪
运行
阻塞
等待
超时等待
终止
线程状态
继承Thread类来创建并启动多线程1、先定义一个类继承Thread类并重写run()方法(run()方法是线程的执行体)2、创建Thread子类的实例,也就是创建了线程对象3、启动线程,即调用线程的start()方法
实现Runnable接口创建并启动线程1、定义Runnable接口的实现类并重写run()方法(run()方法是线程的执行体)2、创建Runnable实现类的实例,并用这个实例作为Thread的target来创建thread对象,这个thread对象才是真正的线程对象
使用Callable和Future创建线程1、创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)2、使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值3、使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)4、调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
使用线程池例如用Executor框架
线程如果只是实现Runnable或实现Callable接口,还可以继承其他类如果继承Thread类则无法继承其他类(java单继承)Run/Call 多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。但是编程稍微复杂,如果需要访问当前线程,必须调用Thread.currentThread()方法
Thread&Runnable&Callable区别
如何创建一个线程
如何创建一个线程?
线程
降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。提高响应速度:任务到达时,无需等待线程创建即可立即执行。提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。
执行最快,核心线程数为0,最大线程数为Integer.MAX_VALUE,使用同步队列SynchronousQueue,队列中一旦有元素,就会调入非核心线程区执行如果执行100个任务,每个任务执行时间短,就会产生线程复用,提升速度,如果执行时间过长会在非核心线程区不断创建新线程
不推荐使用:cpu100%+OOM
newCachedThreadPool()
执行慢,核心线程数用户自定且固定,最大线程数也等于这个值,使用LinkedBlockQueue当核心线程数太少,则任务会一组一组的执行
不推荐使用:OOM
newFixedThreadPool()
执行最慢,FixedThreadPool的单一版本
newSingleThreadPool()
增加功能的线程池,可以用做定时任务
不推荐使用:这个在实际项目中基本不会被用到,因为有其他方案选择比如quartz
newScheduleThreadPool()
四种类型
Runnable 接口不会返回结果或抛出检查异常,Callable会
Runnable vs Callable
execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否
并且可以通过 Future 的 get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完
submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功
execute() vs submit()
shutdown() 关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务了,但是队列里的任务得执行完毕。
shutdownNow() 关闭线程池,线程的状态变为 STOP。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。
shutdown() vs shutdownNow()
isTerminated 当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true
isShutDown 当调用 shutdown() 方法后返回为 true。
isTerminated() vs isShutdown()
关键字对比
runState(运行状态)
workCount(线程数量)
运行状态由内部维护,不由用户显性设置
能接受新提交的任务,也能处理阻塞队列里的任务
1、running
不能接受新提交的任务,但能处理阻塞队列里的人物
2、shutdonw
不能接受新提交的任务,也不能处理阻塞队列里的任务,还会中断正在执行任务的线程
3、stop
如果所有的任务都已终止了,workerCount (有效线程数) 为0,线程池进入该状态后会调用 terminated() 方法进入TERMINATED 状态
4、tidying
在terminated() 方法执行完后进入该状态,默认terminated()方法中什么也没有做
5、terminated
状态转换
线程池状态
线程池运行状态(生命周期)
基础
线程池中会维护一个核心线程数量(线程池的基本大小),corePoolSize即为指定核心线程数量大小,核心线程会一直存活,即使没有任务需要执行,并且只有在工作队列满了的情况下才会创建超出这个数量的线程,除非设置了allowCoreThreadTimeOut,设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭。
1、corePoolSize 线程池核心线程大小
先判断是否超Corepoolsize,超了再判断工作队列满没满,满了最后判断maximumPoolSize超了没,超了就走拒绝策略
2、maximumPoolSize 线程池最大线程数量
一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定
3、keepAliveTime 空闲线程存活时间
keepAliveTime的计量单位
4、unit 空闲线程存活时间单位
基于数组的有界阻塞队列,可以防止资源耗尽问题
ArrayBlockingQueue
基于链表的无界阻塞队列
LinkedBlockingQuene
不缓存任务的阻塞队列
SynchronousQuene
具有优先级的无界阻塞队列,优先级通过参数Comparator实现。
PriorityBlockingQueue
新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列
5、workQueue 工作队列
创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程(守护线程)等等
6、threadFactory 线程工厂
该策略下,直接丢弃任务,并抛出RejectedExecutionException异常(拒绝执行异常)
Abort Policy(默认)
任务被拒绝添加后,会调用当前线程池的所在的线程直接去执行被拒绝的任务的run()方法如果线程池已经shutdown,则直接抛弃任务缺点:可能导致主线程阻塞
CallerRuns Policy
该策略下,直接丢弃任务,什么都不做
Discard Policy
该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列
DiscardOldest Policy
当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,则需要拒绝策略处理。jdk中提供了4种拒绝策略(又称饱和策略)
7、handler 拒绝策略
ps:Run.get.avai..()返回的是可用的计算资源(线程数),而不是CPU物理核心数超线程的CPU来说,单个物理处理器相当于拥有两个逻辑处理器,能够同时执行两个线程。例如物理计算机有4个处理器核心,返回值则为4x2=8
1、获取CPU核心线程数CPU核数=Runtime.getRuntime().availableProcessors()
ps:IO密集型(某大厂实践经验) 核心线程数 = CPU核数 / (1-阻塞系数) 例如阻塞系数 0.8,CPU核数为4 则核心线程数为20
ps:io密集型 2* cpu核数只是经验值,实际要找出最优线程数就需要进行压测,不同环境不同机器表现也不同
ps:最大线程数一般是核心的2倍左右
2、分析线程池处理的程序类型是CPU密集型,还是IO密集型
1、核心线程数该怎么设置合适
corePoolSize:核心线程数;maximunPoolSize:最大线程数每当有新的任务到线程池时,第一步: 先判断线程池中当前线程数量是否达到了corePoolSize,若未达到,则新建线程运行此任务,且任务结束后将该线程保留在线程池中,不做销毁处理,若当前线程数量已达到corePoolSize,则进入下一步;第二步: 判断工作队列(workQueue)是否已满,未满则将新的任务提交到工作队列中,满了则进入下一步;第三步: 判断线程池中的线程数量是否达到了maxumunPoolSize,如果未达到,则新建一个工作线程来执行这个任务,如果达到了则使用饱和策略来处理这个任务。注意: 在线程池中的线程数量超过corePoolSize时,每当有线程的空闲时间超过了keepAliveTime,这个线程就会被终止。直到线程池中线程的数量不大于corePoolSize为止。(由第三步可知,在一般情况下,Java线程池中会长期保持corePoolSize个线程。)
2、核心线程数和最大线程数区别
线程池7个参数
线程池参数
存放优先级:corePollSize -> queue -> maxPoolSize
执行优先级:corePollSize -> maxPoolSize -> queue
abortPolicy:抛出RejectedExecutionException
callerRunsPolicy:如果线程没关闭,让调用者执行此任务
DiscardPolicy:放弃本次任务
DiscardOldestPolicy:丢弃阻塞队列中最老的任务,并将新任务加入
放不下时:拒绝策略如果线程达到maximumPoolSize任有新任务,就会执行拒绝策略。
任务调度
1.快速响应用户请求(响应速度>吞吐量)比如说用户要查看一个商品的信息,那么我们需要将商品维度的一系列信息如商品的价格、优惠、库存、图片等等聚合起来,展示给用户从用户体验角度看,这个结果响应的越快越好,如果一个页面半天都刷不出,用户可能就放弃查看这个商品了这种场景最重要的就是获取最大的响应速度去满足用户,所以应该不让队列去缓冲并发任务,调高corePoolSize和maxPoolSize去尽可能创造多的线程快速执行任务PS:使用同步队列:SynchronousQueue而不是缓冲队列,如:有界阻塞队列ArrayBlockingQueue、无界阻塞队列:LinkedBlockingQueue
2.快速处理批量任务(吞吐量>响应速度)比如说统计某个报表,需要计算出全国各个门店中有哪些商品有某种属性,用于后续营销策略的分析这种场景需要执行大量的任务,我们也会希望任务执行的越快越好,但这类场景任务量巨大,并不需要瞬时的完成,而是关注如何使用有限的资源,尽可能在单位时间内处理更多的任务,也就是吞吐量优先的问题所以应该设置队列去缓冲并发任务,调整合适的corePoolSize去设置处理任务的线程数。在这里,设置的线程数过多可能还会引发线程上下文切换频繁的问题,也会降低处理任务的速度,降低吞吐量
如何判断?CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上
CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。
如何根据类型决定线程数大小?
CPU密集型任务还是IO密集型任务?
事故1:事故描述:2018年XX页面展示接口产生大量调用降级,数量级在几十到上百。事故原因:该服务接口内部逻辑使用线程池做并行计算,由于没有预估好调用的流量,导致最大核心数设置偏小,大量抛出RejectedExecutionException
事故2:事故描述:2018年XX业务提供的服务执行时间过长,作为上游服务整体超时,大量下游服务调用失败。事故原因:该服务处理请求内部逻辑使用线程池做资源隔离,由于队列设置过长,最大线程数设置失效,导致请求数量增加时,大量任务堆积在队列中,任务执行时间过长,最终导致下游服务的大量调用超时失败
美团故障场景
能否不用线程池?不行,因为目前其他技术在java领域不够成熟
有没有通用计算公式设置参数?没有,cpu密集型和io密集型执行结果差距较大
简化线程池配置:线程池构造参数有8个,但是最核心的是3个:corePoolSize、maximumPoolSize,workQueue
参数可动态修改
增加线程池监控
动态配置线程池参数?可以
故障后的解决方案
业务场景
使用 ThreadPoolExecutor 的构造函数声明线程池
监测线程池运行状态。比如 SpringBoot 中的 Actuator 组件除此之外,还可以用ThreadPoolExecutor提供了获取线程池当前的线程数和活跃线程数、已经执行完成的任务数、正在排队中的任务数等等
事故3:假如我们线程池的核心线程数为 n,父任务数量为 n,父任务下面有两个子任务,其中一个已经执行完成,另外一个被放在了任务队列中。由于父任务把线程池核心线程资源用完,所以子任务因为无法获取到线程资源无法正常执行,一直被阻塞在队列中。父任务等待子任务执行完成,而子任务等待父任务释放线程池资源,这也就造成了 \"死锁\"。
建议不同类别的业务用不同的线程池
给线程池命名。默认情况下创建的线程名字类似 pool-1-thread-n 这样的,没有业务含义,不利于我们定位问题
正确配置线程池参数(最难)
线程池最佳实践
线程池
轻量级同步机制,具有可见性,有序性。每次使用被volatile修饰的变量都从主存里获取
在加了volatile的变量之后的变量都从主存里读取,并且之后的代码不会重排序到volatile前面
加了volatile的变量之前的赋值操作都会同步到主存中,并且之前的代码不会重排序到volatile后面
写屏障
底层原理:内存屏障
因为volatile是轻量级机制,而原子操作是一整个操作完成或失败,若遇到阻塞就会等很久。
为什么没有原子性?
MESI协议
1.状态标记量
2.double check
volatile
ABA问题
自旋CAS循环时间长开销大
只能保证一个共享变量的原子操作
CAS可能的问题
JDK中对CAS的支持 — Unsafe类
AtomicInteger
AtomicIntegerArray
atomic: JDK中的相关原子操作类
CAS机制
1、线程的创建
首先通过对象.run()方法可以执行方法,但是不是使用的多线程的方式,就是一个普通的方法,要想实现多线程的方式,一定需要通过对象.start()方法。调用start()里面的start0()方法该线程并不一定会立马执行,只是将线程变成了可运行状态(NEW ---> RUNNABLE)。具体什么时候执行,取决于 CPU ,由 CPU 统一调度
为什么用start()方法而不用run()方法
Java的线程是不允许启动两次的,第二次调用必然会抛出IllegalThreadStateException,这是一种运行时异常,多次调用start被认为是编程错误。
两次start()
一个线程两次调用start()方法会出现什么情况?
2、线程的启动
join:t.join()方法只会使主线程(或者说调用t.join()的线程)进入等待池并等待t线程执行完毕后才会被唤醒。并不影响同一时刻处在运行状态的其他线程
3、线程的方法
共享内存
消息传递
4、线程间通信
synchronized+wait/notify
cas
信号量
reentrantlock
5、线程间同步
6、线程返回值
volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
volatile和synchronized的区别
ReentrantLock 是 API 级别的,synchronized 是 JVM 级别的
ReentrantLock 显示的获得、释放锁,synchronized 隐式获得释放锁
ReentrantLock 可以实现公平锁
synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁。
ReentrantLock和Synchronized的区别和原理
Object.wait()和Thread.sleep()
1.所属类不同
2.对待锁的方式不同
wait() 通常被⽤于线程间交互/通信, sleep() 通常被⽤于暂停执⾏。
3.使用场景不同
wait() ⽅法被调⽤后,线程不会⾃动苏醒,需要别的线程调⽤同⼀个对象上的 notify() 或 者 notifyAll() ⽅法。 sleep() ⽅法执⾏完成后,线程会⾃动苏醒。或者可以使⽤ wait(longtimeout) 超时后线程会⾃动苏醒。
4.阻塞和等待唤醒方式不同
wait()和sleep()区别
sleep(int time)方法,会是线程从Running状态进入阻塞状态,不释放锁,带sleep时间结束,线程从阻塞状态进入Runable状态,等待操作系统分配时间片
yield()方法,会主动让出执行,不释放锁,使线程从Running状态进入Runable状态,等待操作系统分配时间片
sleep()让出cpu时间,让其他线程执行,不区分线程优先级;yield让出时间,给相同优先级的线程执行
sleep()方法申明抛出InterruptedException异常,而yield方法则没有申明抛出任何异常
yield() 和sleep() 区别
处理这多个请求的方式可以是并行的,这样就要求硬件需要有多个计算核心
也可以是只有一个计算资源,处理完一个请求再处理第二个 / 在不同请求间不停切换运行,意味着同时可以有许多线程存在,但每一时刻正在跑的线程只有一个
并发是一种机制,指在多个请求同时发起支持并发意思就是包容同时来到的多个请求,或说包容同时存在的多个线程
并行:并行是一种处理方式(或可以说是一种架构方式),支持并行即指“有多个计算核心,可以在同一时间同时处理多个任务 / 把一个任务分割成小块,在同一时间同时处理多个部分
同步:同步是一种要求,即指“在支持并发的情况下,不同请求之间(线程之间)不能发生数据冲突或资源抢占冲突”同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为
异步:调用者无需等待第一个方法返回,就可以开始其他的任务异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。而异步方法通常会在另外一个线程中,“真实”地执行着。整个过程,不会阻碍调用者的工作
同步&异步:注重的是结果,同步就是一直等待。异步:时不时地去看有没有结果
并发&并行&同步&异步
线程的几种状态
多线程和并发
也就是说属性完全依赖主键,主键能唯一表示这些属性
第三范式:建立在第二范式基础上,非主键只能依赖主键,不能依赖非主键
如果通过A属性(属性组)(主键),可以确定唯一B属性,那么B依赖A
依赖
B属性需要完全依赖A属性组(主键)才能确定唯一,比如语文成绩需要完全依赖学号和科目,不可以只通过学号或者科目获得
完全依赖
B属性需要部分依赖A属性组(主键)才能确定唯一,比如姓名需要依赖属性组(学号,班级)其中的学号就能行
部分依赖
A确定B,B确定C,则C传递依赖A
传递依赖
能唯一表示一条数据的属性或属性集,比如(学号),(学号,性别)
超键
能唯一表示一条数据的属性或属性集,b style=\
候选键
能唯一表示一条数据的b style=\
主键
候选码的并集
主属性
非候选码
非主属性
数据库三大范式
先需求分析,再画E-R模型,然后根据模型设计数据库,最后就是写代码、测试和部署
数据库如何设计的?
负责和客户端建立连接,获取并验证用户权限以及维持和管理连接(在用户建立连接后,即使管理员改变连接用户的权限,也不会影响到已连接的用户)
连接为长连接,连接时长由wait_timeout控制,默认值8小时
使用show processlist 可以查看所有连接的信息
基于TCP协议
连接器(建立管理连接,验证权限)
因为实际缓存命中率低,所以被mysql8后被废弃
但在实际情况下,查询缓存一般没有必要设置。因为在查询涉及到的表被更新时,缓存就会被清空。所以适用于静态表即因为实际缓存命中率低,所以被废除
查询缓存(查询到缓存直接返回结果)
扫描字符流,根据构词规则识别单个单词,如 select,表名,列名,判断其是否存在等。
词法分析
判断语句是否符合 MySQL 语法,在词法分析的基础上将单词序列组成语法短语,最后生成语法树,提交给优化器,
语法分析
分析器(语法、词法分析)
基于开销的优化器,以确定例如:索引的使用,join 表的连接顺序以及处理查询等的最优解方式,也就是说执行查询之前,都会先选择一条自以为最优的方案,然后执行这个方案来获取结果
优化器(选择最优的执行方案)
在具体执行语句前,会先进行权限的检查,通过后使用数据引擎提供的接口,进行查询。如果设置了慢查询,会在对应日志中看到 rows_examined 来表示扫描的行数。在一些场景下(索引),执行器调用一次,但在数据引擎中扫描了多行,所以引擎扫描的行数和 rows_examined 并不完全相同。
执行器(操作存储引擎,返回结果)
server层
buffer pool
内存管理
非聚簇索引
非唯一索引
使用条件
定义:只对于非聚集索引(非唯一)的插入、删除和更新有效,对于每一次的插入不是写到索引页中,而是先判断插入的非聚集索引页是否在缓冲池中,如果在则直接插入,若不在,则先放到Insert Buffer 中,再按照一定的频率进行合并操作,再写回disk。这样通常能将多个插入合并到一个操作中,目的还是为了减少随机IO带来性能损耗。
只对insert有效
Insert Buffer
对insert、delete、update(实际参数叫change)、purge有效
默认,缓存全部
all
不缓存
none
缓存insert
inserts
缓存delete
deletes
缓存delete+insert操作(delete+insert实现update操作)
changes
缓存后台执行的物理删除操作
purges
innodb_change_buffering参数
Change Buffer
插入缓存
二次写
自动监控并为某被频繁访问的二级缓存设置哈希索引
自适应哈希
将下一个extent提前读取到buffer pool中
线性预读
将下一个extent剩余的page提前读取到buffer pool中
随机预读(5.5废弃,但可以启用)
预读
特性
Innodb
InnoDB 存储引擎以页(默认为16KB)为基本单位存储
innodb索引的组织方式
InnoDB支持行级锁,外键,事务,崩溃后的安全恢复,MVCC。
InnoDB和MyISAM对比
存储引擎层
保证持久性
记录事务对数据库做了哪些修改
刷盘时机
确定恢复的起点
确定恢复的终点
怎样恢复
崩溃恢复
应用场景
redo log(重做日志)
保证原子性
事务回滚
在事务中对表中的记录做改动时
生成时机
服务器在内存中维护一个全局变量,且事务id分配之后自动+1
每当变量值为256的倍数时,会将变量值刷新到系统表空间页号为5的页面中
生成策略
事务id(唯一、递增)
INSERT操作对应的undo日志
DELETE操作对应的undo日志
UPDATE操作对应的undo日志
日志格式
undo log(回滚日志)
bin log
日志系统
mysql整体架构
水平分库
水平分表
垂直分库
将一个表按照字段分成多表,每个表存储其中一部分字段。它带来的提升是:1.为了避免IO争抢并减少锁表的几率,查看详情的用户与商品信息浏览互不影响2.充分发挥热门数据的操作效率,商品信息的操作的高效率不会被商品描述的低效率所拖累
垂直分表
分库分表
主从复制,是用来建立一个和主数据库完全一样的数据库环境,称为从数据库;主数据库一般是准实时的业务数据库
1、架构的扩展。业务量越来越大,I/O访问频率过高,单机无法满足,此时做多库的存储,物理服务器增加,能承载的负荷增加
S锁(共享锁 shared lock)读锁是共享的,或者说是相互不阻塞的又称读锁,若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
X锁(排他锁 exclusive lock)写锁是排他的,一个写锁会阻塞其他的写锁和读锁又称写锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。这保证了其他事务在T释放A上的锁之前不能再读取和修改A。
2、读写分离,使数据库能支撑更大的并发。主从只负责各自的写和读,极大程度的缓解X锁和S锁争用。在报表中尤其重要。由于部分报表sql语句非常的慢,导致锁表,影响前台服务。如果前台使用master,报表使用slave,那么报表sql将不会造成前台锁,保证了前台速度
3、做数据的热备份,作为后备数据库,主数据库服务器故障后,可切换到从数据库继续工作,避免数据丢失。
原理(重要)
主从复制
b树或红黑树都太高
B树搜索有可能会在非叶子结点结束,越靠近根节点的记录查找时间越短,只要找到关键字即可确定记录的存在,其性能等价于在关键字全集内做一次二分查找
B+树中,顺序检索比较明显,随机检索时,任何关键字的查找都必须走一条从根节点到叶节点的路,所有关键字的查找路径长度相同,导致每一个关键字的查询效率相当
b树查找不稳定,红黑树增删费时
没有利用局部性原理
b+树非叶子节点只储存索引,一次性读入内存中可以查找的关键字也就越多,降低IO读写次数
b+树磁盘读写代价更低
b+树遍历一遍叶子节点完成整棵树遍历,b树不行
索引为什么用b+树而不用b树或红黑树实现?
无法范围查找,无法排序,不支持最左匹配原则,有大量重复键值效率低
哈希索引适合等值查询,但是无法进行范围查询 哈希索引没办法利用索引完成排序 哈希索引不支持多列联合索引的最左匹配规则 如果有大量重复键值的情况下,哈希索引的效率会很低,因为存在哈希碰撞问题
B+ Tree索引和Hash索引区别?
B+树的叶子节点上有指针进行相连,因此在做数据遍历的时候,只需要对叶子节点进行遍历即可,这个特性使得B+树非常适合做范围查询而Mongodb是做单一查询比较多,数据遍历操作比较少,所以用B树作为索引结构
为什么 MongoDB 选用 B 树作为索引实现?
红黑树性能更高,因为其利用了“缓存”,算法一书中说过,红黑树相当于2-3树。2节点等价于普通平衡二叉树的节点,而3节点本质上是“非平衡的缓存”,当数据随机性强时,2节点向3节点转化可以吸收一些非平衡性性,减少旋转次数,快速完成平衡
红黑树和AVL树(平衡二叉树)比较
InnoDB存储引擎的最小存储单元是页,默认一个数据页大小为16kb,一个页中key假设为bigint占8byte,指向下个页的地址指针占6byte,16kb/(8+6)b=1170个对象(关键字-页指针)
假设数据记录大小1KB -> 叶子节点(页)可以存 16kb/1kb = 16条数据
高度为2的B+树:1170 * 16 = 18720,约存2万条数据记录。
高度为3的B+树:1170 * 1170 * 16 = 21902400,约存2千万条数据记录。
如果是b树,那么2000万数据的树高为log16 2000万 远远大于3层
计算多少数据的树高/三层可以放多少数据
b+树结构
案例
or中至少有一个字段无索引
不符合最左前缀原则
模糊匹配%在最前
not in
mysql自动调优选择了不走索引
索引失效场景
mysql会找到一个值都不相同的列,为它加上UNIQUE INDEX
如果没有具有唯一值的特征列,那么InnoDB会自动生成一个不可见的名为ROW_ID的列,名为GEN_CLUST_INDEX的聚簇索引该列是一个6字节的自增数值,随着插入而自增
如果没有主键,还会不会有有聚簇索引?有
递增才能支持范围查找
递增下,新增节点直接往后双链表后放。不递增的话,会因插入新节点造成大量页分裂
为什么主键推荐设为自增?
简单说就是以表的主键构建的一棵B+树的索引数据结构,如果没有主键的话innodb会选择唯一非空的主键。好坏处就是B+树的好坏处。
聚集索引通常是表的主键,若无主键则为表中第一个非空的唯一索引,还是没有就采用innodb存储引擎为每行数据内置的ROWID作为聚集索引。
聚簇索引和非聚簇索引(innodb)(回表查询就是非聚簇查询)(1)先通过非聚簇索引定位到主键值id=14;(2)在通过聚集索引14定位到行记录Eillson
主键索引和辅助索引是没啥区别的,都指向叶子节点的数据。因为索引树都是独立的,所以辅助所以无须通过主键索引来查询数据。
myisam默认非聚簇索引
聚簇索引和非聚簇索引
常见的方法是:将被查询的字段,建立到联合索引里去
如何实现索引覆盖?
场景1:全表count查询优化
场景2:列查询回表优化
场景3:分页查询
哪些场景可以利用索引覆盖来优化SQL?
索引覆盖
a
a and b
b and a
虽然explain显示用了联合索引,但实际上只用到了a的索引,c并没有用到
a and c
a and b and c
用到了联合索引
b
c
只有ab都有索引,才会都用索引,其他情况(a和b至少有一个没索引)都用不到索引因为假设ab中b没索引的话,那么a or b的执行情况很可能是a走了索引扫了一遍没找到,最后还是要全表扫描,还不如直接全表扫描
a or b
b and c
没用到索引
只用到a索引(abc都可以用,只不过mysql优化器只选其中最有效的一个或几个)
只用到b(和上同理)
a和b都用到了
给a、b、c分别创建单列索引
结论:多个单列索引底层会建立多个B+索引树,比较占用空间,也会浪费一定搜索效率,故如果只有多条件联合查询时最好建联合索引
where查询是按照从左到右的顺序,所以筛选力度大的条件尽量放前面?错误。实际上mysql优化器会根据索引自己选择,与顺序无关
数据重复性最小的
联合索引在设置的时候应该以什么标准把哪一列的数据放在前面
联合索引创建多个单列索引好还是一个联合索引好?
给具有唯一值的列建唯一性索引(避免重复)给经常查询、排序、分组的列建索引对于过长的字段尽量使用前缀索引尽量使用最左前缀匹配原则删除不用的索引
最左匹配原则
建立索引要注意什么?
索引面试题
索引
0.先运行看看是否真的很慢,注意设置SQL_NO_CACHE
这句话的意思是把查询语句的where都应用到 表中返回的记录数最小的表 开始查起,单表每个字段分别查询,看哪个字段的区分度最高(区分度的公式是count(distinct col)/count(*),表示字段不重复的比例)
1.where条件单表查,锁定最小返回记录表
2.explain查看执行计划,是否与1预期一致(从锁定记录较少的表开始查询)
开启:set global slow_query_log=1;只对当前数据库生效,重启MySQL后失效;想永久开启就修改my.cnf文件
查看慢查询开启命令:show variables like '%slow_query_log%'默认关闭
但设置后再查看会发现没改变?这时重新开个会话窗口就好了。或者show global variables like '%long_query_time%';
查看慢查询阈值时间:show variables like 'long_query_time';默认10秒修改 set global long_query_time=x ; x秒
查看日志
show status like '%slow_queries%';查询整个系统的话就加global
查询当前表有多少条慢SQL
mysqldumpslow
一个语句的生命周期,SQL语句在上张图片
语句和花费的时间
showprofile
慢查询优化步骤
加limit优化
对索引进行优化。1.不要加过多的索引,考虑给where和order by加 2.避免在where子句中用NULL,会造成全表扫描 3.字符字段最好不做主键,只加前缀索引 4.使用覆盖索引,(例如通过添加联合索引实现覆盖索引)
join
SELECT 字段 FROM table WHERE EXISTS(subquery);
SELECT * FROM A WHERE id IN (SELECT id FROM B);
in和exists
order by
group by
mysql大表优化查询效率
Mysql调优
事务中的操作要么全部完成,要么全部失败
原子性(atomicity)
事务的执行结果必须是使数据库从一个一致性状态到另一个一致性状态
一致性(consistency)
通过MVCC或锁保证隔离性
扩展:四种隔离级别和各自的底层实现
一个事务的执行不受其它事务的干扰
隔离性(isolation)
事务一旦提交,它对数据库的改变就是永久性的
持久性(durability)
ACID
事务特性
读取到未提交的事务
读未提交
读取已经提交的事务
读已提交(实际用的最多)
在事务A中读取数据时,事务B更改数据并提交,但事务A未提交事务,读的还是和之前一样的数据
可重复读(mysql默认)
这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。
串行化
底层实现
隔离级别(强度由上往下增强)
事务
读-读 不存在线程安全问题
读取到另一个事务未提交的数据
脏读
SQL规范中,可重复读不能解决幻读,但mysql使用gap锁解决了幻读问题
两次读到的数据(行数)不一致。第一次读,然后有事务插入或删除了数据,第二次读与之前不一致。
幻读
两次读取到的数据不一致。读一次,然后有另一个事务更新了数据,第二次读就不一致了
不可重复读
并发事务带来的问题
读-写:有线程安全问题,可能会造成事务隔离性问题
后一个事务的更新覆盖了前一个事务更新的情况
丢失更新
由于某个事务的回滚操作,参与回滚的旧数据将其他事务的数据更新覆盖标准定义的所有隔离界别都不允许第一类丢失更新发生。基本上数据库的使用者不需要关心此类问题
第一类丢失更新:回滚覆盖
解决:乐观锁或悲观锁
多个事务同时更新一行数据,导致一个事务更新被另一个事务覆盖
第二类丢失更新:更新覆盖
写-写:有线程安全问题,可能会存在丢失更新问题
并发场景
什么是MVCC?
MVCC在可重复读下实现原理:每行记录后面隐藏了两列:创建时间和删除时间(实际上是系统版本号)每开始一个事务,版本号就+1。查询时只查出同时符合1.行的系统版本号<=事务的系统版本号,这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的2.行的删除版本要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除
insert:InnoDB为新插入的每一行保存当前系统版本号作为行版本号
delete:InnoDB为删除的每一行保存当前系统版本号作为行删除标识
MVCC只在读已提交和可重复读两个隔离级别下工作。MVCC可以使用 乐观锁 和 悲观锁来实现
row_id
用来存储的每次对某条聚簇索引记录进行修改的时候的事务id
trx_id
旧版本串成单链表,版本链的头结点是当前记录最新的值
roll_pointer
三个隐藏列
版本链
ReadView
判断版本链中哪些版本对当前事务可见
版本链访问记录判断逻辑
核心问题
每一次进行普通select操作前都会生成一个ReadView
Read Commited
只在第一次进行普通select操作前生成一个ReadView,之后的select重复使用
Repeated Read
事务提交即释放
insert undo
需要支持MVCC,不能立即删除
update undo
记录打删除标记(逻辑删除)
delete mark
undo
MVCC
id:选择标识符,值越高,优先级越高
select_type:表示查询的类型
span style=\
partitions:匹配的分区
type:表示表的连接类型
possible_keys:表示查询时,可能使用的索引
ref:列与索引的比较
rows:扫描出的行数(估算的行数)rows是核心指标,绝大部分rows小的语句执行一定很快(有例外)。所以优化语句基本上都是在优化rows。
using where:使用了where
using index:使用了覆盖索引
表示在查询过程中产生了临时表用于保存中间结果。mysql在对有不是索引的字段进行group by,会出现临时表group by的实质是先排序后分组.
using temporay:
Using filesort通常出现在order by,当试图对一个不是索引的字段进行排序时,mysql就会自动对该字段进行排序,这个过程就称为“文件排序”
using filesort
Extra:执行情况的描述和说明
用于sql语句前面查看sql语句的执行计划select,update,delete,insert都可以使用,explain字段相同,不过增删改在内部都被重写为 执行增删改+select
explain
每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据(版本号或者时间戳)
每次(开启事务)在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。
按加锁机制
锁粒度小,开销大,加锁慢,会发生死锁,并发度高,发生锁冲突概率低
对当前操作的行加锁
行级锁
锁粒度大,开销小,加锁块,不会发生死锁,并发度低,发生锁冲突概率高
对当前操作的表上锁
表级锁
会发生死锁,并发度一般
处于行级锁和表级锁之间
页级锁
按粒度
用法:lock in share mode
读取数据时,任何事务不能修改数据
共享锁(读锁)
用法:for update
修改数据时,任何事务不能读取和写数据
排他锁(写锁)
按兼容性
单个行记录上的锁
Record Lock
会对键值条件内但并不存在的数据加锁。(行锁是前提)它是innoDB在可重复读的级别下防止幻读引入的锁
Gap Lock(间隙锁)
Next-key Lock(临键锁)
按模式
InnoDB锁
全局唯一,不能重复,(基本要求)
递增,下一个ID大于上一个ID;(可选,某些需求)
信息安全,非连续ID,避免恶意用户/竞争对手发现ID规则,从而猜出下一个ID或者根据ID总量猜出业务总量
高可用,不能故障,可用性4个9或者5个9;(99.99%、99.999%)
高QPS,性能不能太差,否则容易造成线程堵塞;
平均延迟尽可能低;
分布式ID生成系统需求
MySQL中,可通过数据列的auto_increment属性来自动生成自动递增的唯一编号
简单,代码方便,性能可以接受
数字ID天然排序,对分页或者需要排序的结果很有帮助
不同数据库语法和实现不同,数据库迁移的时候或多数据库版本支持的时候需要处理
ID生成依赖数据库单机的读写性能;(高并发条件下性能不是很好)
在单个数据库或读写分离或一主多从的情况下,只有一个主库可以生成。有单点故障的风险
在性能达不到要求的情况下,比较难于扩展
如果遇见多个系统需要合并或者涉及到数据迁移会相当复杂
分表分库的时候无法依靠auto_increment属性来唯一标识一条记录
1、mysql主键自增
简单,代码方便
生成ID性能非常好,基本不会有性能问题
全球唯一,在遇见数据迁移,系统数据合并,或者数据库变更等情况下,可以从容应对
没有排序,无法保证趋势递增
UUID往往是使用字符串存储,查询的效率比较低
传输数据量大
存储空间比较大,如果是海量数据库,就需要考虑存储量的问题
不可读
2、uuid
ps:比较适合使用Redis来生成每天从0开始的流水号。比如订单号=日期+当日自增长号。可以每天在Redis中生成一个Key,使用INCR进行累加
3、redis固定步长生成
Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0
https://github.com/twitter/snowflake
4、Twitter的雪花算法
方案一:通过持久顺序节点实现;
方案二:通过节点版本号;
5、Zookeeper生成分布式唯一ID
主键生成策略
sql
1、ORDER BY x 对结果集根据x来进行排序(默认从小到大排序)
2、DESC 就是将默认排序改为从大到小降序排序
查找最晚入职员工的所有信息
计算出现了多少次
count()
求和
sum()
子主题
HAVING
GROUP BY
查找薪水记录超过15次的员工号emp_no
Sql语句练习
MySQL
IoC 容器控制了外部资源获取(不只是对象包括文件等),由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象
Bean之间的解耦
不关心bean的底层实现
换dao的时候只要更改配置类
单例
为什么需要ioc
ioc
aop利用静态代理(AspectJ aop)和动态代理(JDK动态代理、CGLIB动态代理)实现。静态代理是在编译时增强代码,动态代理是在运行时增强代码。静态代理性能更高
jdk动态代理和cglib代理的区别
aop具体的底层实现
aop在项目中怎么用,做什么
jdk动态代理是基于invocationHandler接口和Proxy类实现,invocationHandler接口通过invoke方法反射调用目标类中的方法
aop
IOC是什么
IOC容器初始化过程
依赖注入的实现方法
依赖注入的相关注解
依赖注入的过程
Bean的生命周期
Bean的作用范围
使用Spring XML方式配置,该方式用于在纯Spring 应用中,适用于简单的小应用,当应用变得复杂,将会导致XMl配置文件膨胀 ,不利于对象管理。<bean id=\"xxxx\" class=\"xxxx.xxxx\"/>
XML
这种方式用在Spring Boot 应用中。@Configuration 标识这是一个Spring Boot 配置类,其将会扫描该类中是否存在@Bean 注解的方法,比如如下代码,将会创建User对象并放入容器中。@ConditionalOnBean 用于判断存在某个Bean时才会创建User Bean.这里创建的Bean名称默认为方法的名称user。也可以@Bean(\"xxxx\")定义。
2、@Bean注解
3、@Import注解
4、使用ImportSelector或者ImportBeanDefinitionRegistrar接口,配合@Import实现。
注解
这种方式的应用场景是为接口创建动态代理对象,并向SPRING容器注册。比如MyBatis中的Mapper接口,Mapper没有实现类,启动时创建动态代理对象,将该对象注册到容器中,使用时只要@Autowired注入即可使用,调用接口方法将会被代理拦截,进而调用相关的SqlSession执行相关的SQL业务逻辑
手动注入Bean容器
Bean创建的两种方式
如何通过注解配置文件
BeanFactory & FactoryBean & ApplicationContext
AOP是什么
AOP相关注解
AOP相关术语
AOP的过程
IOC和AOP面试
Spring Boot启动的时候会通过@EnableAutoConfiguration注解找到META-INF/spring.factories配置文件中的所有自动配置类,并对其进行加载,而这些自动配置类都是以AutoConfiguration结尾来命名的,它实际上就是一个JavaConfig形式的Spring容器配置类,它能通过以Properties结尾命名的类中取得在全局配置文件中配置的属性如:server.port,而XxxxProperties类是通过@ConfigurationProperties注解与全局配置文件中对应的属性进行绑定的。
springboot自动配置原理
用户发送请求至前端控制器DispatcherServlet;DispatcherServlet收到请求调用HandlerMapping;HandlerMapping根据请求的url找到具体的HandlerMapping处理器,生成并返回处理器执行链HandlerExecutionChain。(拦截器链里装有handler对象,拦截器)根据handler拿到对应的适配器。处理拦截器的prehandler方法。适配器执行handler,返回ModelAndView;处理拦截器的poshandler方法DispatcherServlet将ModelAndView传给ViewReslover视图解析器ViewReslover解析后返回具体View。(主要是提供视图缓存的支持,对redirect和forward前缀的处理)DispatcherServlet对View进行渲染视图(即将模型数据model填充至视图中)。DispatcherServlet响应用户
spring mvc运行原理↑
提供方 @Autowired是Spring的注解,@Resource是javax.annotation注解
注入方式 @Autowired先按照Type 注入,如果有多个bean,则按照Name注入;@Resource默认按Name自动注入,也提供按照Type 注入
@Autowired与@Resource的区别
编程式事务:在代码中手动的管理事务的提交、回滚等操作,代码侵入性比较强
基于TX和AOP的xml配置文件方式
作用在类:表示所有该类的public方法都配置相同的事务属性信息
作用在方法:方法上的事务 > 类的事务,方法上的优先
作用在接口(不推荐):因为注解是不能继承的,第一个实现接口的类会有事务属性,但这个类再去调用别的方法,就会失效
作用位置
spring在TransactionDefinition接口中定义了七个事务传播行为:propagation_requierd:如果当前没有事务,就新建一个事务,如果已存在一个事务中,加入到这个事务中,这是最常见的选择。 propagation_supports:支持当前事务,如果没有当前事务,就以非事务方法执行。 propagation_mandatory:使用当前事务,如果没有当前事务,就抛出异常。 propagation_required_new:新建事务,如果当前存在事务,把当前事务挂起。 propagation_not_supported:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 propagation_never:以非事务方式执行操作,如果当前事务存在则抛出异常。 propagation_nested:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与propagation_required类似的操作
propagation 事务传播行为
isolation 事务的隔离级别
readOnly 是否只读
rollbackFor :用于指定能够触发事务回滚的异常类型,可以指定多个异常类型
常用属性
基于@Transactional注解
声明式事务
介绍spring事务
最常见:含事务的B方法的异常被A方法的catch捕获,导致B方法不能正常回滚,进而事务失效
因为事务拦截器需要在目标方法执行前后进行拦截
@Transactional 应用在非 public 修饰的方法上(且不会报错)
因为基于AOP代理,只有从类的外部调用类中含事务的方法才会生效
同一个类中的不含事务的A方法调用含事务的B方法(不论B是public还是private)
最少见:数据库引擎不支持事务,InnoDB支持,MyISAM不支持
@Transactional失效场景
@Service
@Controller
@Repository
@Component
@PostConstruct
所有的Aware方法都是在初始化阶段之前调用的
生命周期(singleton才有)
加载过程
singleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的
prototype : 每次请求都会创建一个新的 bean 实例
request : 每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效
session : 每一次HTTP请求都会产生一个新的 bean,该bean仅在当前 HTTP session 内有效
作用域
BeanFactory源码
FactoryBean源码
BeanFactory是接口,提供了IOC容器最基本的形式,给具体的IOC容器的实现提供了规范, FactoryBean也是接口,为IOC容器中Bean的实现提供了更加灵活的方式,FactoryBean在IOC容器的基础上给Bean的实现加上了一个简单工厂模式和装饰模式(如果想了解装饰模式参考:修饰者模式(装饰者模式,Decoration) 我们可以在getObject()方法中灵活配置。其实在Spring源码中有很多FactoryBean的实现类.
BeanFactory和FactoryBean区别?
当我们尝试按name从BeanFactory.getBean(beanname)一个Bean时,返回的一定是A类对应的实例吗?答案是否, 当A需要需要创建代理对象时,我们getBean 得到是 代理对象的引用
bean
A绑定到ObjectFactory 注册到工厂缓存singletonFactory中,B在填充A时,先查成品缓存有没有,再查半成品缓存有没有,最后看工厂缓存有没有单例工厂类,有A的ObjectFactory。调用getObject ,执行扩展逻辑,可能返回的代理引用,也可能返回原始引用。成功获取到A的早期引用,将A放入到半成品缓存中,B填充A引用完毕。代理问题, 循环依赖问题都解决了
singletonObjects:第一级缓存,里面存放的都是创建好的成品Bean
earlySingletonObjects : 第二级缓存,里面存放的都是半成品的Bean(实例化,未完成初始化的单例对象(未完成属性注入的对象))
singletonFactories :第三级缓存, 不同于前两个存的是 Bean对象引用,此缓存存的bean 工厂对象,也就存的是 专门创建Bean的一个工厂对象。此缓存用于解决循环依赖(存放ObjectFactory对象)
为什么要有二级缓存?
所有循环依赖都可以用三级缓存解决吗?
三级缓存
循环依赖
Target(目标)
Proxy(代理)
JoinPoint(连接点)
PointCut(切点)
Advice(增强)
Advisor(切面)
Weaving(织入)
Introdution(引入)
相关术语
Before Advice(前置增强)
After Advice(后置增强)
Around Advice(环绕增强)
Throws Advice(抛出增强)
Introdution Advice(引入增强)
增强类型
代理类除了是客户类和委托类的中介之外,我们还可以通过给代理类增加额外的功能来扩展委托类的功能,这样做我们只需要修改代理类而不需要再修改委托类,符合代码设计的开闭原则。代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后对返回结果的处理等。代理类本身并不真正实现服务,而是通过调用委托类的相关方法,来提供特定的服务。真正的业务功能还是由委托类来实现,但是可以在业务功能执行的前后加入一些公共的服务。例如我们想给项目加入缓存、日志这些功能,我们就可以使用代理类来完成,而没必要打开已经封装好的委托类。
增加功能
代理类不让客户类访问委托类(例如 商家不让顾客直接与厂家交易),其特征是代理类和委托类实现相同的接口
控制访问
作用:
<tx:annotation-driven transaction-manager=\"txManager\" proxy-target-class=\"true\"/>
基于注解(@Aspect)
<aop:config expose-proxy=\"true\" proxy-target-class=\"false\"></aop:config>默认false,true使用cglib代理模式,false或者省略使用JDK代理
基于xml配置(aop:config)
Spring+AspectJ
使用
获取和匹配增强器
静态代理类是手动实现的,创建一个java类,并和委托类实现同一个接口表示该类为代理类优点:可以做到在符合开闭原则的情况下对目标对象进行功能扩展。缺点:我们得为每一个服务都得创建代理类,工作量太大,不易管理。同时接口一旦发生改变,代理类也得相应修改。
静态代理
动态代理无需手动创建代理类,代理对象直接由代理生成工具动态生成(在程序运行期间由JVM根据反射等机制动态的生成源码,类似工厂模式
用户类通过调用java.lang.reflect.Proxy类下的newProxyInstance()方法来创建代理对象优点:因为有接口,所以使系统更加松耦合缺点:JDK动态代理必须要有接口
JDK动态代理
优点:因为代理类与目标类是继承关系,所以不需要有接口的存在缺点:因为没有使用接口,所以系统的耦合性比使用JDK的动态代理高
CGLIB动态代理
动态代理Code
动态代理
AOP
它是一个接口,在spring用作一个标识作用比如beanNameAware继承aware接口自定义一个类实现这个接口,重写它的setBeanNameAware方法当容器bean初始化时发现这个bean属于BeanNameAawre这个类那么就会去调用它的setBeanName以及其它重写的方法
Aware作用
案例1:隐式扫描不到Bean的定义
案例2:Bean中自定义的构造方法缺少隐式依赖
案例3:原型Bean被固定为单例
SpringBean定义常见错误
案例1:@Autowire 过多赠与,无所适从
案例2:显示引用的Bean的首字母忽略大小写
案例3:引用内部类的Bean遗忘类名
案例4:@Value没有注入期望值
案例5:错乱的注入集合
SpringBean依赖注入常见错误
案例1:Bean的构造器内的变量抛NullPointerExeception
SpringBean生命周期常见错误
SpringCore篇
SpringWeb篇
@Transactional注解,默认情况下只会对RuntimeException和error 的异常进行事务回滚,要想也对Exception的异常回滚,需要加rollbackFor = Exception.class
异常与事务回滚(rollbackFor)
只用被事务注解修饰的public方法才能支持回滚
试图给private方法添加事务
调用事务的方法,必须是被aop代理的方法,也就是不能通过内部this调用,需要注入对象
属性propagation
默认propagation = Propagation.REQUIRED,父事务和子事务合并成一个事务,子事务如果想独立事务,设置REQUIRED_NEW
嵌套事务
多数据源切换之谜
Spring事务常见错误
Spring Test
补充篇
Spring编程常见错误50例
Spring
Redis 的全称是:Remote Dictionary.Server,本质上是一个 Key-Value 类型的内存数据库,很像 memcached,整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据 flush 到硬 盘上进行保存。 因为是纯内存操作,Redis 的性能非常出色,每秒可以处理超过 10 万次读写操作,是已知性能最快的 Key-Value DB。 Redis 的出色之处不仅仅是性能,Redis 最大的魅力是支持保存多种数据结构,此外单个 value 的最大 限制是 1GB,不像 memcached 只能保存 1MB 的数据,因此 Redis 可以用来实现很多有用的功能。 比方说用他的 List 来做 FIFO 双向链表,实现一个轻量级的高性 能消息队列服务,用他的 Set 可以做 高性能的 tag 系统等等。 另外 Redis 也可以对存入的 Key-Value 设置 expire 时间,因此也可以被当作一 个功能加强版的 memcached 来用。 Redis 的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能 读写,因此 Redis 适合的场景主要局限在较小数据量的高性能操作和运算上
redis优缺点
热点数据缓存
限时业务
分布式锁
其他
排行榜
set操作
共同关注
点赞/收藏(标签)
抽奖
微博/公众号消息流
购物车
具体功能
计数器
微信小程序中的喜欢作者和踩一下,采用incrby xx即可
String
小型购物车
hlen获取商品总数
hgetall勾选所有商品
Hash
消息队列
微信订阅号中消息的推送
List
微信小程序的开奖
srandmember xxx
spop xxx
Zset
五种基本数据类型
签到
Bitmap
基于loglog算法用于基数统计
Hyperloglog
定位
附近的人
GEO
三种特殊数据类型
数据类型对应应用
Redis可以实现的项目功能
注意:在Redis中,键总是一个字符串对象,而值可以是字符串、列表、集合等对象,所以我们通常说的键为字符串键,表示的是这个键对应的值为字符串对象,我们说一个键为集合键时,表示的只是这个键对应的值为集合对象,键本身为字符串。
int编码保存的是可以用 long 类型表示的整数值。例如 1 、 233 、1000000
cpucacheline每次读是64byte(64操作系统),redisObject一般只占20byte,所以剩下空间缓存embstr的字符串
embstr 编码保存长度小于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)
raw 编码保存长度大于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)
1、计数器
2、分布式锁
3、储存对象(不常变化)
string
当同时满足下面两个条件时,使用ziplist(压缩列表)编码: 1、列表保存元素个数小于512个 2、每个元素长度小于64字节不能满足上面两个条件的时候使用 linkedlist 编码。上面两个条件可以在redis.conf 配置文件中的 list-max-ziplist-value选项和 list-max-ziplist-entries 选项进行配置
ziplist是Redis为了节省内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构,一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。原理:压缩列表并不是对数据利用某种算法进行压缩,而是将数据按照一定规则编码在一块连续的内存区域,目的是节省内存
ziplist(压缩列表) quickList(3.2之后)
ziplist+linkedlist(双端链表)(3.2之前)
1、排行榜
使用场景:
list
储存方式和list的ziplist一样
ziplist
hashtable 编码的哈希表对象底层使用字典数据结构,哈希对象中的每个键值对都使用一个字典键值对
hashtable
1、购物车 以用户id为key,商品id为field,商品信息为value
2、储存对象(经常变化)
hash
intset 编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合中
intset(整数集合)(会排序不重复)
hashtable 编码的集合对象使用 字典作为底层实现,字典的每个键都是一个字符串对象,这里的每个字符串对象就是一个集合中的元素,而字典的值则全部设置为 null。这里可以类比Java集合中HashSet 集合的实现,HashSet 集合是由 HashMap 来实现的,集合中的元素就是 HashMap 的key,而 HashMap 的值都设为 null
set
分值小的靠近表头,分值大的靠近表尾
ziplist
skiplist 编码的有序集合对象使用 zset 结构作为底层实现,一个 zset 结构同时包含一个字典和一个跳跃表字典的键保存元素的值,字典的值则保存元素的分值;跳跃表节点的 object 属性保存元素的成员,跳跃表节点的 score 属性保存元素的分值 说明:其实有序集合单独使用字典或跳跃表其中一种数据结构都可以实现,但是这里使用两种数据结构组合起来,原因是假如我们单独使用 字典,虽然能以 O(1) 的时间复杂度查找成员的分值,但是因为字典是以无序的方式来保存集合元素,所以每次进行范围操作的时候都要进行排序;假如我们单独使用跳跃表来实现,虽然能执行范围操作,但是查找操作有 O(1)的复杂度变为了O(logN)。因此Redis使用了两种数据结构来共同实现有序集合
优点:跳表的线程安全是通过cas锁实现的,跳表的构建相对简单,同时支持范围查找
缺点:相对于红黑树,空间消耗增加
skiplist
zset
redis5种数据类型
redis五种数据类型
Expire 设置过期时间
及时删除对内存好,但是偶尔过多需要删除的话对cpu不好
定时删除:出生的key自带一个定时器,到期就删
对cpu好,但是对内存不好,还有可能内存泄漏
惰性删除:要用这个key的时候发现过期了才删
兼容上面两种优点,但缺点是频率不好把握,频率过高就跟定时删除一样,过低就跟惰性删除一样更致命的是还可能返回已经过期的key的值
定期删除:定期随机抽取一部分key看是否过期,过期就删
Redis的过期删除策略就是:惰性删除和定期删除两种策略配合使用
过期删除策略
先设置redis最大内存,默认无限制,一般设3/4
策略有很多种,通常用allkeys-lru 利用LRU算法移除任何key(包括过期和不过期
内存淘汰策略
场景:热门话题的评论,答案排序,大v的粉丝列表
整存整取:多redis实例拆分。只要部分:hash+filed片段访问
本地缓存
大key问题
本地缓存:Ehcache、hashmap
集群+随机数:给请求加随机数以分配到集群中不同的redis
最好提前发现:监控热key+通知系统去做本地缓存
热key问题
解决:设置过期时间(expire)
setnx拿到锁后 删锁前 宕机,死锁
解决:拿锁和设置过期必须原子(setnx ex+Lua脚本)
setnx拿到锁后 设置过期时间前 宕机,死锁
解决:删锁必须原子(uuid+Lua脚本)
setnx ex拿到锁后 锁因已过期 现在删了别的锁
每过1/3的过期时间就判断一次锁状态,锁还在就续满
即使哨兵模式也无法解决主从复制的时延问题,redis主机一崩就会丢失锁信息
Redisson的看门狗
记录个毫秒级的开始时间。加锁时轮询所有redis服务器,如果某台连接时间过长则放弃它
>=n/2+1台已加锁 && 当前时间 - 开始时间 < 锁的超时时间 则创建成功
RedLock
业务层校验
为不存在的数据设置短过期时间
布隆过滤器
用户不断发起缓存和数据库都不存在的请求
缓存穿透
热点key失效的瞬间大量请求进来
缓存击穿
过期时间均匀分布一下
数据预热
key大面积同时失效或redis宕机
缓存雪崩
缓存问题
把当前内存中的数据集快照写入磁盘,也就是 Snapshot 快照(数据库中所有键值对数据)。恢复时是将快照文件直接读到内存里。Redis 服务器在载入 RDB 文件期间,会一直处于阻塞状态,直到载入工作完成为止。
优点:文件适合备份和恢复,大数据集恢复速度比AOF快 生成RDB时会让主进程fork()一个子进程来做,主进程不需要磁盘IO缺点:不能实时持久化会丢失最后一次修改产生的数据
RDB
通过保存Redis服务器所执行的写命令来记录数据库状态。
AOF重写: AOF 文件重写并不是对原文件进行重新整理,而是直接读取服务器现有的键值对,然后用一条命令去代替之前记录这个键值对的多条命令,生成一个新的文件后去替换原来的 AOF 文件
优点:每秒同步一次 文件格式可读性强可手动修改一些命令缺点:同等数据量下AOF比RDB文件更大 每秒同步一次导致性能比RDB低一些 AOF保存的是指令容易出bug
AOF
RDB-AOF混合持久化(redis4.0以后)
持久化策略
谁说NoSql都不支持事务,虽然redis的事务提供的并不是严格的ACID的事务(比如一串用EXEC提交执行的命令,在执行中服务器宕机,那么会有一部分命令执行了,剩下的没执行),但是这个事务还是提供了基本的命令打包执行的功能(在服务器不出问题的情况下,可以保证一连串的命令是顺序在一起执行的,中间会有其他客户端命令插进来执行)。redis还提供了一个watch功能,你可以对一个key进行watch,然后再执行事务,在这个过程中,如果这个watch的值进行了修改,那么这个事务会发现并拒绝执行
redis支持事务吗
写:先更新 DB然后直接删除 cache
读 :从 cache 中读取数据,读取到就直接返回cache中读取不到的话,就从 DB 中读取数据返回再把数据放到 cache 中
缺陷1:首次请求数据一定不在 cache 的问题解决办法:可以将热点数据可以提前放入cache 中。
缺陷2:写操作比较频繁的话导致cache中的数据会被频繁被删除,这样会影响缓存命中率 。解决办法:数据库和缓存数据强一致场景 :更新DB的时候同样更新cache,不过我们需要加一个锁/分布式锁来保证更新cache的时候不存在线程安全问题。可以短暂地允许数据库和缓存数据不一致的场景 :更新DB的时候同样更新cache,但是给缓存加一个比较短的过期时间,这样的话就可以保证即使数据不一致的话影响也比较小。
问题1:在写数据的过程中,可以先删除 cache ,后更新 DB 吗?答案: 不行 因为这样可能会造成数据库(DB)和缓存(Cache)数据不一致的问题。例如:请求1先把cache中的A数据删除 -> 请求2从DB中读取数据->请求1再把DB中的A数据更新请求2读取的数据和DB里的数据就不一致了
可能存在的问题
旁路缓存模式
读写穿透模式
异步缓存写入
三种常用读写策略
发布/订阅模式
主从复制一致性问题
redis和mysql区别
redis-benchmark
redis压测
redis详解
Redis
异步、解耦、削峰、数据分发
作用就是优点
缺点:引入外部依赖,系统稳定性降低,复杂度提高还要考虑消息一致性问题
微信qq就是典型的mq应用。超时订单自动关闭
RocketMQ和RabbitMQ对比
nameserver 无状态的含义:每台nameserver之间没有连接无信息同步,都会收到broker上报的信息,和producer、consumer的询问,搭建无状态集群比较方便
broker主从部署:nameserver将两台broker分为一组,其中broker的nameserverId为0的是主,其它的都是从,比如1
nameserver(broker的路由中心)、producer、broker、consumer
组成
集群消费:每个group只有一个consumer能消费
广播消费:每个group每个consumer能消费
消费模式
consumer消费后发送的ack因网络原因导致broker没及时收到,让consumer再消费了一遍
每条消息都会有一条唯一的消息ID,消费者接收到消息会存储消息日志,如果日志中存在相同ID的消息,就证明这条消息已经被处理过了
重复消费问题
单个队列:天然FIFO顺序没有问题
缺点:降低并发和吞吐量
多个队列:限制1个topic里只有1个queue,且被1个consumer消费
顺序消费问题
发送失败后重试(一般设最多3次)
采取send()同步发消息,发送结果是同步感知的
producer
默认异步刷盘,修改成同步刷盘
集群部署
broker
consumer:消费正常后再进行手动ack确认
消息丢失问题
添加consumer
加topic
决定是否丢弃
消息堆积问题
场景:一般应用在当正常业务处理时出现异常时,将消息拒绝则会进入到死信队列中,有助于统计异常数据并做后续处理
死信队列
把每种延迟时间段的消息都存放到同一个队列中,然后通过一个定时器进行轮询这些队列,查看消息是否到期,如果到期就把这个消息发送到指定topic的队列中
订单超时未支付,自动取消
支付后24小时未评论自动好评
应用
延时队列
心跳机制
RocketMQ如何实现分布式事务?
RocketMQ
概述
1.权限控制使用在节点上2.一个节点可以有多个权限3.权限对子节点无影响
所有人可用 id固定是anyone
world
对ip认证
ip
对已添加认证的用户认证
auth
使用用户名密码认证
digest
secheme策略
permission权限:cdrwa
acl权限控制scheme:id:permission
可以监听节点的状态和是否发生了变化。只能使用一次。如果想要继续监听就要再回调时重新设置watch
getData()
exists()
setData()
create()
DataWathch监听node节点的数据变化
getChildren()
create()
ChildWatch监听孩子节点发生变化
两种数据类型
watch
依赖数据版本号生成
分布式唯一id
1.每个客户端与zookeeper连接时,都会再lock/目录下创建临时节点,并存储该客户带信息2.按cZxid大小排序lock/目录下的节点(leader接收到消息请求后,将消息赋予一个全局唯一的64位自增id,叫:zxid事务id,通过zxid的大小比较就可以实现因果有序这个特征)3. 判断排在第一的目录 是不是 自己,是的话就获取锁,不是的话就监听排在自己前面的节点
如果拿着锁的客户端宕机了,与zookeeper断开连接,那么它的临时节点就会被删除,监听这个节点的节点就会被通知
崩溃恢复之数据恢复
1》leader接收到消息请求后,将消息赋予一个全局唯一的64位自增id,叫:zxid,通过zxid的大小比较就可以实现因果有序这个特征。2》leader为每个follower准备了一个FIFO队列(通过TCP协议来实现,以实现全局有序这一个特点)将带有zxid的消息作为一个提案(proposal)分发给所有的 follower。 3》当follower接收到proposal,先把proposal写到磁盘,写入成功以后再向leader回复一个ack。 4》当leader接收到合法数量(超过半数节点)的ack后,leader就会向这些follower发送commit命令,同时会在本地执行该消息。 5》当follower收到消息的commit命令以后,会提交该消息。
简化版2PC
消息广播之原子广播
ZAB协议包含两种基本模式
zab协议zookeeper原子广播
监听原理
同步数据
寻找leader状态
looking
领导者状态
leading
跟随着状态
following
观察者状态,不投票
observing
服务器状态
当有两个服务器时,每个服务器都给自己投票,平票然后就会比较zxid,如果相等,再比较myid,选择大的最后票数大于一半服务器就成功选出leader
选举机制大于半数机制
断开连接后,创建的节点自己删除
临时节点
断开连接,创建的节点不删除
永久节点
znode
zookeeper
1、维护配置信息
2、分布式锁场景
3、集群管理
4、生成分布式唯一ID
应用场景:
1、高性能 zk的全量数据放在内存里
2、高可用 只要集群内超过一半机器正常工作,整个集群就能正常对外提供服务
3、严格顺序访问 对客户端的更新请求分配全局唯一的递增编号,用于反映操作先后顺序
设计目标
树状结构(目录),被称为znode(zookeeper node),一个znode可以有多个子节点使用路径来定位某个znode节点:/ns-1/icast/mysql/schema1/table1
1、数据:znode-data,类似map中的key-value关系
属性说明(ACL权限列表)
具体属性(get命令查看)
2、状态 stat 用来描述当前节点的创建,修改记录,包括cZxid、ctime等ps:写(增删改)操作会在zk服务器内部自动维护一个事务,读不会
3、子节点:children
znode内容
节点的类型在创建时被确定,且不能改变
生命周期依赖会话,会话(Session)结束后,临时节点自动删除,也可以手动删除ps:虽然临时znode创建后会被绑定到一个客户端会话,但它对所有客户端也可见pss:临时节点不允许拥有子节点
生命周期不依赖会话,只有在客户端执行删除操作的时候才会被删除
持久节点
znode类型
数据类型
待补充:watch、client、zab协议、acl权限控制、监听原理
Zookeeper第二版
ZooKeeper
简介图
动词:相当于mysql的insert在es里插入一条数据即称为索引一条数据到es
名词:相当于mysql的databasemysql里的database内存储一张张table(表)对应es里index里存储一个个Type
Index(索引)
在Index里可以定义一个或多个Type(类型)类似mysql里的table,每一种类型的数据放在一起es的数据(Document)存在某个索引的某个类型下
Type(类型)
相当于mysql里的数据,格式为Json,文档内每个记录称为属性保存在某个Index(索引)下的某个Type(类型)里的一个数据(文档)相当于mysql 某个database下的某个table里的一个记录
Document(文档)
在 5.X 版本中,一个 index 下可以创建多个 type;在 6.X 版本中,一个 index 下只能存在一个 type;在 7.X 版本中,直接去除了 type 的概念,就是说 index 不再会有 type
为什么移除:es基于lucene的倒排索引,倒排索引的生成基于index而非type,多个type会影响倒排索引性能
PS:最新版本es移除了type字段
基本概念
1、维护一个倒排索引表
2、分词
3、根据保存的数据分词情况在索引表里添加记录
4、相关性得分
5、检索
查询过程
ES存储的是一个JSON格式的文档,其中包含多个字段,每个字段会有自己的倒排索引
B+树实现
单词词典(Term Dictionary)
倒排列表记录了单词对应的文档集合,有倒排索引项(Posting)组成
倒排索引项主要包含如下信息:1.文档id用于获取原始信息2.单词频率(TF,Term Frequency),记录该单词在该文档中出现的次数,用于后续相关性算分3.位置(Posting),记录单词在文档中的分词位置(多个),用于做词语搜索(Phrase Query),分词所在位置从0开始计算4.偏移(Offset),记录单词在文档的开始和结束位置,也是从0开始,用于高亮显示
倒排列表(Posting List)
不需要锁,提升并发能力,避免锁的问题数据不变,一直保存在os cache中,只要cache内存足够filter cache一直驻留在内存,因为数据不变可以压缩,节省cpu和io开销
倒排索引不可变的好处
倒排索引
docker pull elasticsearch:7.4.2docker pull kibana:7.4.2 可视化检索数据界面,可装可不装ps:如果访问拒绝则在最前面加上sudo
docker安装es
(sudo)mkdir -p /mydata/elasticsearch/config(sudo)mkdir -p /mydata/elasticsearch/data(sudo)echo \"http.host:0.0.0.0\" >> /mydata/elasticsearch/config/elasticsearch.ymlhttp.host:0.0.0.0表示允许任何服务器端口访问 >>表示写入到某个文件
创建实例
1、暴露端口9200用于发送接收请求 9300分布式集群通信端口 2、指定单节点模式 3、重要:指定es初始和最大内存大小,不指定会占用全部内存导致卡死
略
启动并指定参数
安装&启动
es主要通过PUT/GET/POST命令发送请求到对应端口,es就会返回相关json数据或者操作
查看命令 _cat
GET /_cat/nodes :查看所有节点
GET /_cat/health :查看es的健康状况
GET /_cat/master :查看主节点
GET /_cat/indices :查看所有索引 (类似mysql的show database)
查
索引一个文档(保存)保存一个数据到某个索引的某个类型下,并可以指定一个唯一标识
新增:不带id,或带id之前没数据
修改:带id且之前有数据
不指定id会自动生成id,如果指定id就会修改这个数据并新增版本号
POST(主要用于新增)
新增:带id之前没数据,因为必须带id导致每次新增要换新id
PUT必须指定id,不指定会保错,一般用于修改操作(更新同一个id)
PUT(主要用于修改)
新增 PUT请求/POST请求
返回值ps:PUT:如果同样的请求发了2次或以上,version++,result从create变为updated,PUT发送多次即为更新操作POST:如果指定id则和PUT没区别而如果不指定id,则发送多次都会是新增操作,version不变,result为create不变,id会变另一个随机生成的id
PUT A/B/1 :在A索引的B类型下保存了一个自定的json同时指定id为1POST A/B :同理,POST可以不指定id,会自动生成一个
增
增删改查
实战
Elasticsearch
Netty是一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端,同时也是基于NIO(封装了jdk的nio),让我们使用起来更加方法灵活
Netty 是一款基于 NIO(Nonblocking IO,非阻塞IO)开发的网络通信框架,对比于 BIO(Blocking I/O,阻塞IO),他的并发性能得到了很大提高。
高并发
Netty 的传输依赖于零拷贝特性,尽量减少不必要的内存拷贝,实现了更高效率的传输。
传输快
Netty 封装了 NIO 操作的很多细节,提供了易于使用调用接口
封装好
特点
封装了 NIO 的很多细节,使用更简单
使用简单
预置了多种编解码功能,支持多种主流协议,如 FTP,SMTP,HTTP 以及各种二进制和基于文本的传统协议
功能强大
可以通过 ChannelHandler 对通信框架进行灵活地扩展。
定制功能强
Netty 是活跃的开源项目,版本迭代周期短,bug 修复速度快
社区活跃
Netty 修复了已经发现的NIO的 bug,让开发人员可以专注于业务本身
稳定
很多开源项目都使用到了 Netty, 比如我们经常接触的 Dubbo、RocketMQ 等等
经历大型项目考验
IO 线程模型:同步非阻塞,用最少的资源做更多的事。
内存零拷贝:尽量减少不必要的内存拷贝,实现了更高效率的传输。
内存池设计:申请的内存可以重用,主要指直接内存。内部实现是用一颗二叉查找树管理内存分配情况。
串形化处理读写:避免使用锁带来的性能开销
高性能序列化协议:支持 protobuf 等高性能序列化协议
通过与其他业界主流的 NIO 框架对比,如mina,Grizzly之类,Netty 的综合性能最优
性能高
优势
RPC 框架的网络通信工具
实现一个自定义的HTTP 服务器(类似tomcat)
即时通讯系统
消息推送系统
FileChannel 文件数据传输通道
DatagramChannel UDP网络数据传输通道
SocketChannel 客户端/服务端TCP网络数据传输通道
ServerSocketChannel 服务端TCP网络数据传输通道
Channel(双向通道,输入输出数据)
MappedByteBuffer
DirectByteBuffer
HeapByteBuffer
1、channel向buffer写入数据(channel.read(buffer))
3、从buffer读取数据(buffer.get())
4、切换到写模式(buffer.clear()&buffer.compact())
5、向buffer里写入数据(buffer.put(byte)) (参数可以是byte也可以是byte数组)
从写切换到读,position会指向0(即从0开始读),limit会指向最后一个写的位置
1、position(当前修改的位置)
2、Limit(写入限制)
3、capacity(buffer容量)
ByteBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
CharBuffer
Buffer(缓冲读写数据)
Selector详解
Selector(配合一个线程管理多个Channel)
1、三大组件
NIO基础
八股
Netty 网络操作(读写等操作)抽象类,包括基本的 I/O 操作,如 bind()、connect()、read()、write()
Channel
本质上 是单线程执行器(同时维护了一个selector),里面有run方法处理channel上源源不断的io事件ps:一个eventloop就是一个线程功能为 负责监听网络事件并调用事件处理器注册到其上的Channel 进行相关 I/O 操作的处理
NioEventLoop
EventLoop
EventLoopGroup是一组EventLoop,Channel调用Eventloopgroup的register方法来绑定其中的一个EventLoop后续这个channel上的io事件都由这个eventloop来处理,保证了io事件处理时的线程安全问题
EventLoopGroup是一个接口,需要具体实现,例如NioEventLoopGroup功能最全,支持io事件,普通任务,定时任务
NioEventLoopGroup 类的无参构造函数设置线程数量的默认值就是 CPU 核心数 *2
主要管理 eventLoop 的生命周期,可以理解为一个线程池,内部维护了一组线程,每个线程(NioEventLoop)负责处理多个 Channel 上的事件,而一个 Channel 只对应于一个线程。
NioEventLoopGroup
相比NioEventLoopGroup,不支持io事件,只能处理普通任务和定时任务
DefaultEventLoopGroup
EventLoopGroup
核心组件
客户端
服务端
channel:数据的通道msg:流动的数据handle:数据的处理工序 handle合在一起就是pipelinepipeline:加工流水线 加工msgeventloop:处理数据的工人
相关解析
helloworld
eventloop的基本使用
学习
Netty
\t降低维护带来的新风险
对扩展开放,对修改关闭
开闭原则
更利于代码结构的升级扩展
高层不应该依赖低层,要面向接口编程
依赖倒置原则
便于理解,提高代码的可读性
一个类只干一件事,实现类要单一
单一职责原则
\t功能解耦,高聚合、低耦合
一个接口只干一件事,接口要精简单一
接口隔离原则
只和朋友交流,不和陌生人说话,减少代码臃肿
不该知道的不要知道,一个类应该保持对其它对象最少的了解,降低耦合度
迪米特法则
\t防止继承泛滥
不要破坏继承体系,子类重写方法功能发生改变,不应该影响父类方法的含义
里氏替换原则
降低代码耦合
尽量使用组合或者聚合关系实现代码复用,少使用继承
合成复用原则
七大原则
饿汉式
懒汉式
双重校验锁
最优
枚举类
单例模式:Bean默认为单例模式应用:工具类、共享数据、单例线程池
只有一个具体的工厂类,非接口或抽象方法,getinstance获取实例时,通过if-else或switch来判断new出的是接口对应的哪个实例
简单工厂
定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行
抽象工厂:声明了工厂方法的接口。具体产品工厂:实现工厂方法的接口,负责创建产品对象。产品抽象类或接口:定义工厂方法所创建的产品对象的接口。具体产品实现:具有统一父类的具体类型的产品。
进程切换和线程切换的过程
工厂方法
工厂模式:BeanFactory
提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类
产品族难扩展,产品等级易扩展
设计模式:简单工厂、工厂方法、抽象工厂之小结与区别
抽象工厂模式
指挥者
产品
抽象建造者
具体建造者
一个复杂的对象分解为多个简单的对象,然后一步一步构建而成。产品的组成部分是不变的,但每一部分是可以灵活选择的
建造者模式注重零部件的组装过程,而工厂方法模式更注重零部件的创建过程,但两者可以结合使用
建造者模式
当存在大量相同或相似对象的创建问题,用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象
原型模式
创建型模式
缺点:会产生大量的代理类
优点:被代理类和代理类均不需要实现接口
基于继承: 被代理类不需要实现接口,代理类继承被代理类,扩展方法
缺点:被代理的类必须实现接口
优点:可以代理所有实现接口的类
基于接口:被代理类和代理类需要实现相同接口,代理类接口属性在实例化时候需要传入对应被代理的实例
两种实现方法
优点:可以生成所有实现接口的代理对象
JDK动态代理:通过java提供的Proxy类帮我们创建代理对象
需要引入外部依赖
CGLIB动态代理:cglib生成代理是被代理对象的子类
Spring AOP:底层使用cglib
实现方法
代理模式
(对象)适配器模式
桥接模式
装饰模式
外观模式
享元模式
组合模式
结构型模式
策略模式
命令模式
介绍:避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止
应用:JDK的java.util.logging.Logger#log()和javax.servlet.Filter#doFilter()Spring Security 使用责任链模式,可以动态地添加或删除责任(处理 request 请求)Spring AOP 通过责任链模式来管理 Advisor、Netty 中的 Pipeline 和 ChannelHandler 通过责任链设计模式来组织代码逻辑Mybatis 中的 Plugin 机制使用了责任链模式,配置各种官方或者自定义的 Plugin,与 Filter 类似,可以在执行 Sql 语句的时候做一些操作
责任链模式
状态模式
观察者模式(Spring中listener的实现:ApplicationListener):定义对象键一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知被制动更新,有时又称作发布-订阅模式、模型-视图模式
中介者模式(MVC 框架中,控制器(C)就是模型(M)和视图(V)的中介者;QQ 聊天程序的“中介者”是 QQ 服务器)定义一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。中介者模式又叫调停模式,它是迪米特法则的典型应用。
迭代器模式
访问者模式
备忘录模式
行为型模式
java.lang.Proxy#newProxyInstance()
java.lang.Object#toString()
java.lang.Class#newInstance()
java.lang.Class#forName()
工厂模式
java.util.logging.Logger#log()
javax.servlet.Filter#doFilter()
责任链模式:
JDK里用到的设计模式
GoF的23种设计模式
设计模式
程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的。当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器 等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)
什么是进程?
一个进程之内可以分为一到多个线程。一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行 。Java 中,线程作为小调度单位,进程作为资源分配的小单位。 在 windows 中进程是不活动的,只是作为线程的容器
什么是线程?
1.进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位2.每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;而线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小
进程和线程的区别
线程每次创建和销毁都要调用操作系统,是很昂贵的资源而协程就像是用户态的轻量级线程,协程的创建、切换发生在用户态。是一个在线程的基础上,针对某些应用场景发展出来的功能,由编程语言直接创建协程按照组织好的代码流程,并发地执行一些操作,代替一个线程,虽然时间上慢几毫秒,但省了创建线程过程
协程应用场景:线程池
线程和协程的区别
切换页目录以使用新的地址空间地址空间切换主要是针对用户进程而言
第1步、进程地址空间切换
主要为切换内核栈和硬件上下文处理器状态切换对应于所有的调度单位
第2步、处理器状态切换
为什么进程间切换很慢 因为切换虚拟地址空间后页面缓存失效,内存访问低效每个进程对应一块虚拟地址空间,虚拟地址空间内有一个页表(页目录)记录虚拟地址空间对应的物理地址空间把虚拟地址转换为物理地址需要查找页表,页表查找是一个很慢的过程,因此通常使用一个叫TLB的Cache来缓存常用的地址映射,这样可以加速页表查找。一旦切换进程后,页表也要切换,导致已缓存的TLB全部失效,缓存命中率降低,虚拟地址查找页表对应物理地址变慢,导致内存的访问在一段时间内相当的低效,因此进程间切换很慢
就绪态、运行态、阻塞态(待补充)
进程间切换
时间片
指的是每条线程执行的时间、线程的切换都由系统控制,系统控制指的是在系统某种运行机制下,可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片。在这种机制下,一个线程的堵塞不会导致整个进程堵塞Java中线程会按优先级分配CPU时间片运行,且优先级越高越优先执行,但优先级高并不代表能独自占用执行时间片,可能是优先级高得到越多的执行时间片,反之,优先级低的分到的执行时间少但不会分配不到执行时间
抢占式调度(java线程切换模式)
指某一线程执行完后主动通知系统切换到另一线程上执行,这种模式就像接力赛一样,一个人跑完自己的路程就把接力棒交接给下一个人,下个人继续往下跑。线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃。
协同式调度
1、当前运行线程主动放弃CPU,JVM暂时放弃CPU操作(基于时间片轮转调度的JVM操作系统不会让线程永久放弃CPU,或者说放弃本次时间片的执行权),例如调用yield()方法。
2、当前运行线程因为某些原因进入阻塞状态,例如阻塞在I/O上
3、线程执行完成,例如执行完run()里面方法
线程主动让出cpu
系统调度
1、挂起当前任务(线程/进程),将这个任务在 CPU 中的状态(上下文)存储于内存中的某处2、恢复一个任务(线程/进程),在内存中检索下一个任务的上下文并将其在 CPU 的寄存器中恢复3、跳转到程序计数器所指向的位置(即跳转到任务被中断时的代码行),以恢复该进程在程序中
上下文切换
当前执行任务(线程)的时间片用完之后,系统CPU正常调度下一个任务
时间片用完
中断处理,在中断处理中,其他程序”打断”了当前正在运行的程序。当CPU接收到中断请求时,会在正在运行的程序和发起中断请求的程序之间进行一次上下文切换。中断分为硬件中断和软件中断,软件中断包括因为IO阻塞、未抢到资源或者用户代码等原因,线程被挂起。
中断(硬件中断、软件中断)
用户态切换
多个任务抢占锁资源,在多任务处理中,CPU会在不同程序之间来回切换,每个程序都有相应的处理时间片,CPU在两个时间片的间隔中进行上下文切换
抢占锁资源
为什么引起上下文切换
线程间调度
线程间切换
1、进程间切换需要切换虚拟地址空间,而线程间切换不用这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。
2、进程的切换会扰乱处理器的缓存机制,让处理器已经缓存的内存地址全部失效,同时虚拟地址空间的改变导致TLB被刷新,缓存命中率降低而线程切换没有这些问题
切换
用于具有亲缘关系的父子进程间或者兄弟进程之间的通信
比如:ls | grep 1 就是将ls的输出结果作为grep 1的输入,实现进程间通信
匿名管道(pipe)
有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信
有名管道
比如按ctrl+c会发送(2)SIGINT、kill pid会发送(15)SIGTERM(中断信号)结果为Terminated、kill -9 pid会发送(9)SIGKILL结果为killed
信号(signal)
每个进程都有一个虚拟地址到物理地址的映射,一般情况虚拟地址可能相同,但物理地址不同这时将物理地址改为相同就可以共同访问同一块内存,利用共享内存实现进程间通信
内核创建了一个消息队列,进程可以对其发送或接收数据
支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点
套接字(socket)
进程间通信
临界区(CriticalSection)
事件(Event)
互斥量(Mutex)
信号量(Semphore)
操作系统线程同步
操作系统线程间同步
套接字
操作系统线程间通信
通信
1、最短工作优先(SJF)
2、最短剩余时间优先(SRTF)
非抢占式
3、最高响应比优先(HRRF)
4、优先级调度(Priority)
5、轮转调度(RR)
抢占式
进程调度算法
时间上,创建线程需要分配内存和列入调度,切换线程时将会内存换页和cpu缓存会被清空,在切换回来时,又需要重新从内存中读取信息,破坏了数据的局部性
空间上,线程所占用空间一般不受Java程序控制,而受系统资源限制。一般分配给其1MB堆栈空间
线程消耗哪些资源,要多少开销?
产生死锁的条件:资源互斥、请求与保持、不可剥夺、循环等待
银行家算法
避免死锁
资源剥夺法。 挂起某些死锁进程,抢占它资源,分配给其他的死锁进程
进程回退法。 让一部分进程回退到足以避免死锁的地步,进程回退时资源被释放而不是被剥夺
进程撤销法。 强制撤销并剥夺一部分进程的资源
解除死锁
死锁
子进程由父进程创建,子进程再创建新的进程。父子进程是一个异步过程,父进程永远无法预测子进程的结束,所以,当子进程结束后,它的父进程会调用wait()或waitpid()取得子进程的终止状态,回收掉子进程的资源。
一般进程
子进程退出了,但是父进程没有用wait或waitpid去获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中,这种进程称为僵尸进程
僵尸进程
父进程结束了,而它的一个或多个子进程还在运行,那么这些子进程就成为孤儿进程(father died)。子进程的资源由init进程(进程号PID = 1)回收
孤儿进程
产生原因
在每个进程退出的时候,内核会释放所有的资源,包括打开的文件,占用的内存等。但是仍保留一部分信息(进程号PID,退出状态,运行时间等)。直到父进程通过wait或waitpid来取时才释放
前提:unix提供了一种机制保证父进程知道子进程结束时的状态信息
导致:如果父进程不调用wait或waitpid的话,那么保留的信息就不会被释放,其进程号就会被一直占用,但是系统所能使用的进程号是有限的,如果大量产生僵死进程,将因没有可用的进程号而导致系统无法产生新的进程,这就是僵尸进程的危害孤儿线程没什么危害
任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程的数据结构,等待父进程去处理。如果父进程在子进程exit()之后,没有及时处理,出现僵尸进程,并可以用ps命令去查看,它的状态是“Z”。
问题危害
1、通过信号机制,在处理函数中调用wait,回收资源
严格的说,僵尸进程并不是问题的根源,罪魁祸首是产生大量僵死进程的父进程。因此,我们可以直接除掉元凶,通过kill发送SIGTERM或者SIGKILL信号。元凶死后,僵尸进程进程变成孤儿进程,由init充当父进程,并回收资源。
2、kill杀死元凶父进程(一般不用)
父进程通过wait或waitpid等函数去等待子进程结束,但是不好,会导致父进程一直等待被挂起,相当于一个进程在干活,没有起到多进程的作用。
3、父进程用wait或waitpid去回收资源(方案不好)
解决方案
1、僵尸进程和孤儿进程
进程和线程
会浪费空间,会有碎片
块式管理
连续分配
段式管理
分页是建立在分段的基础上,继续把段内的内存划分,页式管理通过页表对应逻辑地址和物理地址
页式管理
把主存先分成若干段,每个段又分成若干页,也就是说 段页式管理机制 中段与段之间以及段的内部的都是离散的。有段表+页表
段页式管理
页的大小是固定的,由操作系统决定;而段的大小不固定,取决于我们当前运行的程序。分页仅仅是为了满足操作系统内存管理的需求,而段是逻辑信息的单位,在程序中可以体现为代码段,数据段,能够更好满足用户的需要。
段和页区别
非连续分配
内存管理机制
快表相当于页表的cache,原本需要两次访问内存,现在只要一次cache一次内存
多级页表的主要目的是避免把全部页表一直放在内存中占用过多空间
快表和多级页表
64位的话就是2^64bit的寻址空间,数值远远大于1亿GB
32位操作系统会为每个进程分配多大的内存空间?4G,因为32位cpu的寻址空间最多4G,因此32位的cpu或操作系统都最多支持4G内存
建立在分页管理之上,为了支持虚拟内存功能而增加了请求调页功能和页面置换功能
请求分页管理
请求分段管理
请求段页式管理
虚拟内存实现
局部性原理,保证了虚拟内存不至于效率太低
OPT(不可能实现,只作为目标)
FIFO
LRU
LFU
页面置换算法
虚拟内存
内核态(管态)
不能直接使用系统资源,也不能改变CPU的工作状态,并且只能访问这个用户程序自己的存储空间
用户态(目态)
分为Ring0(内核态)、Ring1、Ring2、Ring3(用户态),每种特权等级可以使用不同的指令集合Ring设计的初衷是将系统权限与程序分离出来,使之能够让OS更好的管理当前系统资源,也使得系统更加稳定。内核态运行在R0特权级别上,可以使用特权指令,控制中断、修改页表、访问设备等等特权级别决定现在是内核态还是用户态
(Linux)特权级别
1、系统调用(用户主动切换为内核态)这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,比如linux中fork()实际上就是执行了一个创建新进程的系统调用。而系统调用的机制核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断
2、异常(被动)当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。
3、外围设备的中断(被动)外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到 内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。
唯一途径是通过中断、异常、陷入机制(访管指令)
用户态——>内核态
PSW
设置程序状态字PSW
内核态——>用户态
工作状态的切换
1、内核态与用户态是操作系统的两种运行级别,当程序运行在3级特权级上时,就可以称之为运行在用户态。因为这是最低特权级,是普通的用户进程运行的特权级,大部分用户直接面对的程序都是运行在用户态;当程序运行在0级特权级上时,就可以称之为运行在内核态。
2、处于用户态执行时,进程所能访问的内存空间和对象受到限制,其所处于占有的处理机是可被抢占的 ; 处于核心态执行中的进程,则能访问所有的内存空间和对象,且所占有的处理机是不允许被抢占的。
内核态与用户态区别
内核态和用户态
CPU的工作状态
拷贝描述的是CPU不执行拷贝数据从一个存储区域到另一个存储区域的任务,这通常用于通过网络传输一个文件时以减少CPU周期和内存带宽
优点:减少CPU和内存的占用
应用场景:RocketMQ持久化时用的是mmap+write()
在内存到硬件的直接工作
读:DMA会把硬盘数据拷贝缓冲区写:DMA将缓冲区的数据拷贝到网卡
DMA直接内存访问硬件
4次用户空间与内核空间的上下文切换+2次CPU拷贝和2次DMA拷贝
传统I/O
4次用户空间与内核空间的上下文切换+1次CPU拷贝和2次DMA拷贝
通过mmap实现的零拷贝I/Ommap(内存映射)是一个比sendfile昂贵但优于传统I/O的方法。mmap就是把内核空间的读缓冲区与用户空间的缓冲区映射到同一物理地址,用户空间与内核空间读缓冲区共享着物理内存。写的话要读了之后拷贝到socket
2次用户空间与内核空间的上下文切换+1次CPU拷贝和2次DMA拷贝
通过sendfile实现的零拷贝I/O
2次用户空间与内核空间的上下文切换+1次DMA拷贝和1次DMA gather拷贝
带有DMA收集拷贝功能的sendfile实现的I/O
\"传统I/O” VS “sendfile零拷贝I/O”
零拷贝
磁盘可以说是计算机系统最慢的硬件之一,读写速度相差内存 10 倍以上,所以针对优化磁盘的技术非常的多,比如零拷贝、直接 I/O、异步 I/O 等等,这些优化的目的就是为了提高系统的吞吐量,另外操作系统内核中的磁盘高速缓存区,可以有效的减少磁盘的访问次数(减少上下文切换次数)。
整个数据的传输过程,都要需要 CPU 亲自参与搬运数据的过程,而且在这个过程中CPU 是不能做其他事情的。简单的搬运几个字符数据那没问题,但是如果我们用千兆网卡或者硬盘传输大量数据的时候,都用 CPU 来搬运的话,肯定忙不过来
未引入DMA技术的传统IO
在进行 I/O 设备和内存的数据传输的时候,数据搬运的工作全部交给 DMA 控制器,而 CPU 不再参与任何与数据搬运相关的事情,这样 CPU 就可以去处理别的事务。
早期 DMA 只存在在主板上,如今由于 I/O 设备越来越多,数据传输的需求也不尽相同,所以每个 I/O 设备里面都有自己的 DMA 控制器。
DMA( 直接内存访问 Direct Memory Access)
整个数据传输的过程,CPU 不再参与数据搬运的工作,而是全程由 DMA 完成,但是 CPU 在这个过程中也是必不可少的,因为传输什么数据,从哪里传输到哪里,都需要 CPU 来告诉 DMA 控制器。
引入DMA技术后的传统IO
出现的背景
文件传输:将磁盘上的文件读取出来,然后通过网络协议发送给客户端。
传统IO下的一次文件传输: 4 次用户态与内核态的上下文切换:两次系统调用:read()、write():每次系统调用都得先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态 4 次数据拷贝,其中2次是DMA的拷贝,2次CPU 拷贝
传统IO的工作方式:数据读取和写入是从用户空间到内核空间来回复制,而内核空间的数据是通过操作系统层面的 I/O 接口从磁盘读取或写入
系统调用会导致上下文切换,所以要尽可能减少系统调用的次数
因为文件传输的应用场景中,在用户空间并不会对数据「再加工」,所以数据实际上可以不用搬运到用户空间,因此用户的缓冲区是没有必要存在的去掉用户缓冲区可以减少内存拷贝的次数
传统IO下的一次文件传输,原本只搬运一份数据,结果却搬运了 4 次,过多的数据拷贝无疑会消耗 CPU 资源,大大降低了系统性能简单又传统的文件传输方式,存在冗余的上文切换和数据拷贝,在高并发系统里是非常糟糕的,多了很多不必要的开销,会严重影响系统性能所以,要想提高文件传输的性能,就需要减少「用户态与内核态的上下文切换」和「内存拷贝」的次数。
传统IO下的文件传输
1、把read()系统调用替换成mmap()系统调用,mmap() 系统调用函数会直接把内核缓冲区里的数据「映射」到用户空间应用进程调用了 mmap() 后,DMA 会把磁盘的数据拷贝到内核的缓冲区里。接着,应用进程跟操作系统内核「共享」这个缓冲区
2、应用进程再调用 write(),操作系统直接将内核缓冲区的数据拷贝到 socket 缓冲区中,这一切都发生在内核态,由 CPU 来搬运数据;
3、最后,把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程是由 DMA 搬运的。
这样,操作系统内核与用户空间就不需要再进行任何的数据拷贝操作,相比传统IO少了一次CPU拷贝,因为直接由内核空间的缓冲区搬运到socket缓冲区但这还不是最理想的零拷贝,因为仍然需要通过 CPU 把内核缓冲区的数据拷贝到 socket 缓冲区里,而且仍然需要 4 次上下文切换,因为系统调用还是 2 次。
过程解析
4次上下文切换+3次拷贝(2次DMA拷贝和1次CPU拷贝)
mmap+write
在 Linux 内核版本 2.1 中,提供了一个专门发送文件的系统调用函数 sendfile()它可以替代前面的 read() 和 write() 这两个系统调用,这样就可以减少一次系统调用,也就减少了 2 次上下文切换的开销
2次上下文切换+3次拷贝(2次DMA拷贝和1次CPU拷贝)
sendfile
零拷贝下的文件传输
从文件传输中看零拷贝
传统文件传输 4次上下文切换+4次拷贝(2次DMA+2次CPU)
mmap+write 4次上下文切换+3次拷贝(2次DMA+1次CPU)
sendfile 2次上下文切换+3次拷贝(2次DMA+1次CPU)
直观对比
零拷贝第二版
文件 touch rm [-rf] vi/vim cat最后一屏/more百分比显示/less翻页查看/tail
目录 mkdir rm [-rf] mv(剪切)/cp(拷贝) find cd(目录切换) ls(目录查看)
查看tcp连接状态: netstat -napt
ps所有进程,aux运行中的,grep xxx搜索xxx的结果
进程 ps aux | grep redis
-9:强制
杀进程 kill -9 进程pid
显示第3行到第9行的内容
查看第几行/一些行
压缩 tar -zcvf
grep 查找文件里符合条件的字符串
top Linux下常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况,类似于Windows的任务管理器
chmod 改变权限,-R是目录下所有文件,777就是高权限(读、写、执行)chmod -R 777 * 意思就是将当前目录下所有文件都给予777权限可能会带来巨大的安全风险,建议如果你的Web服务器遇到权限问题,请将文件的所有权更改为运行应用程序的用户并将文件的权限设置为644,将目录的权限设置为755,而不是递归地将权限设置为777
linux常用命令
Linux常用命令
操作系统
ARP
1.DNS解析并返回ip地址(DNS2.tcp三次握手建立连接(TCP、IP、OSPF、ARP3.浏览器发送http请求(HTTP4.服务器处理请求并返回http报文5.浏览器渲染数据并显示6.tcp四次挥手断开连接
游览器搜索自己的缓存有没有被解析过的这个域名对应的ip地址,如果有,解析结束。同时域名被缓存的时间也可通过TTL属性来设置
1、游览器检查自己缓存
Windows DNS缓存的默认值是 MaxCacheTTL,默认值是86400s,一天
2、检查操作系统的缓存
如果在这里指定了一个域名对应的ip地址,那浏览器会首先使用这个ip地址
存在问题:域名劫持 这种操作系统级别的域名解析规程也被很多黑客利用,通过修改hosts文件里的内容把特定的域名解析到指定的ip地址上,造成域名劫持。所以在windows7中将hosts文件设置成了readonly,防止被恶意篡改
3、检查操作系统里的host文件
这台服务器一般在你的城市的某个角落,距离你不会很远,并且这台服务器的性能都很好,一般都会缓存域名解析结果,大约80%的域名解析到这里就完成了
4、请求本地域名服务器(LDNS)来解析这个域名
1、如果LDNS仍然没有命中,LDNS就直接去Root Server 域名服务器请求解析
2、根域名服务器返回给LDNS一个所查询域的主域名服务器(gTLD Server,国际顶尖域名服务器,如.com .cn .org等)地址(根域名服务器告诉LDNS一个gTLD地址,让LDNS去gTLD里查)
3、此时LDNS再发送请求给上一步返回的gTLD(LDNS向gTLD发起查询)
4、接受请求的gTLD查找并返回这个域名对应的Name Server的地址,这个Name Server就是网站注册的域名服务器
5、Name Server根据映射关系表找到目标ip,返回给LDNS
5、跳转root server进行查询
6、LDNS把gTLD解析的结果返回给用户,同时缓存这个域名和对应的ip,用户根据TTL值缓存到操作系统缓存中
7、操作系统缓存后,返回IP到游览器,域名解析过程结束
1、游览器向DNS请求解析并返回ip地址
2、三次握手建立TCP连接
3、游览器发起HTTP请求
4、服务器接受并解析HTTP请求,查找指定资源,并返回HTTP响应消息
5、客户端解析html代码,并请求html代码中的资源
6、客户端渲染展示页面
7、四次挥手关闭TCP连接
输入url到显示界面,经历了什么?用了什么协议?
应用层:通过应用进程间的交互来完成特定网络应用
运输层:负责向两台主机进程之间的通信提供通用的数据传输服务
网络层:在 计算机网络中进行通信的两个计算机之间可能会经过很多个数据链路,也可能还要经过很多通信子网。网络层的任务就是选择合适的网间路由和交换结点, 确保数据及时传送
数据链路层:两台主机之间的数据传输,总是在一段一段的链路上传送的,这就需要使用专门的链路层的协议。 在两个相邻节点之间传送数据时,数据链路层将网络层交下来的 IP 数据报组装成帧
物理层:实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异
OSI七层/五层协议
查找过程:本地dns -> 根dns -> 主dns -> 下级dns(递归查找) -> 已找到就缓存到本地
dns协议同时利用了tcp和udp
dns解析
http明文传输,https密文传输
https要用CA证书验证
http响应更快,因为https除了tcp还需要ssl(或tls)连接
http在tcp端口80,https为443
http和https区别
速度慢,只适合加密少量数据。私钥自己保存,公钥可随意分发。
密钥包括:公钥和私钥。如公钥加密,则私钥解密;如私钥加密,则公钥解密。
常用加密算法:RSA
非对称加密
加密速度快,适合大量数据的处理。但密钥的管理和分发安全性要求高。
加密和解密使用相同的密钥
常用加密算法:DES,3DES
对称加密
摘要的计算是单向的,其不是加密技术。
通过哈希(散列)函数对不同长度的报文计算出相同长度的特征值。报文如有修改,则特征值会变化。
常用签名算法:MD5,SHA-1,SHA-256
报文摘要
定义
1、网站拥有用于非对称加密的公钥A、私钥A 2‘浏览器向网站服务器请求,服务器把公钥A明文给传输浏览器。 3、浏览器随机生成一个用于对称加密的密钥X,用公钥A加密后传给服务器。 4、服务器拿到后用私钥A’解密得到密钥X。 5、这样双方就都拥有密钥X了,且别人无法知道它。之后双方所有数据都通过密钥X加密解密即可。
1、某网站有用于非对称加密的公钥A、私钥A。2、浏览器向网站服务器请求,服务器把公钥A明文给传输浏览器。3、中间人劫持到公钥A,保存下来,把数据包中的公钥A替换成自己伪造的公钥B(它当然也拥有公钥B对应的私钥B)。4、浏览器生成一个用于对称加密的密钥X,用公钥B(浏览器无法得知公钥被替换了)加密后传给服务器。5、中间人劫持后用私钥B解密得到密钥X,再用公钥A加密后传给服务器。服务器拿到后用私钥A’解密得到密钥X
中间人攻击
CA证书,确保服务端传的公钥A和游览器收到的公钥是同一个
如何防止中间人攻击
漏洞
非对称&对称加密
整个非对称加密过程都是为了最后的对称加密服务的,最终目的是证明证书中的公钥是安全且未篡改。使用公钥来加密一个秘钥,并把秘钥传给后端,后端使用私钥解密秘钥,这样两端都拥有同一个秘钥,从而进行对称加密
证书内容:颁发机构信息+公钥+公司信息+域名+有效期+指纹
如何验证合法性:验证域名和有效期等信息是否正确+判断证书来源是否合法+判断证书是否被篡改+判断证书是否已吊销
签名哈希算法又称指纹算法,通过对比证书的hash值和传递的哈希值确认证书的安全性
签名算法用于加密签名哈希算法,防止证书内的hash值被修改
签名算法&签名哈希算法
CA证书认证流程
CA证书
SSL记录协议
SSL握手协议
SSL警报协议
SSL
https加密机制
在一个TCP连接上可以传送多个HTTP请求和响应,为了减少建立和关闭的消耗和延迟
长连接
只传输头部信息,若有权限继续传输body
节约宽带
可存在多个虚拟主机共享一个IP地址
host域
引入了更多的缓存控制策略
缓存处理
http1.0&1.1
一个连接处理多个请求
多路复用
HPACK算法压缩header信息
头部压缩
当客户端请求资源时,服务器把多个资源都传输给你
服务器推送
二进制协议
http1.1&2.0
放弃使用TCP协议,而使用基于UDP的QUIC协议
http2.0&3.0
请求报文
响应报文
HTTP的报文格式
服务端生成的一串字符串,以作客户端进行请求的一个令牌
减少服务器和数据库查询压力
用设备号/设备mac地址作为Token(推荐)
用session值作为Token
Token
Session是在服务端保存的一个数据结构,用来标识用户和跟踪用户的状态,这个数据可以保存在集群、数据库、文件中
Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现Session的一种方式( Cookie 里面记录一个Session ID)如果cookie被禁用,session无法标识,则会用url重写方式,在url链接里带上sid=xxxx
cookie和session区别
Cookie和Session和Token
在tcp链接中,http请求必须等待前一个请求响应之后,才能发送,后面的依次类推,由此可以看出,如果在一个tcp通道中如果某个http请求的响应因为某个原因没有及时返回,后面的响应会被阻塞
对头阻塞
Http报文首部
HTTP
DHCP
告诉 TCP 协议应该把报文发给哪个进程,最大端口数目为65535(2的16次方)(哪个进程在侦听这个端口,就发哪个进程)
源端口和目的端口
第一个报文的序号在第一次交互时由系统随机生成变化过程:初始值+偏移量即为下一个报文的序号值
序号
数据被接收后,接收端给发送端回馈确认的机制。 若接收端接收到2000,则回复2001。还能够处理重复的报文段,一旦接收到相同的序号就丢弃
确认号
头部长度
数据偏移
占6位,保留为今后使用,目前应设置为0
保留
当UGR置1时,发送应用进程就告诉发送方的TCP有紧急数据要传送。于是发送方的TCP就把紧急数据插入到本报文段数据的最前面,而在紧急数据后面的数据仍是普通数据
紧急数据URG
确认报文段,仅当ACK=1时确认号字段才有效。当ACK=0时,确认号无效。
确认报文ACK
当两个应用进程进行交互式的通信时,有时在一端的应用进程希望在键入一个命令后立即就能够收到对方响应。在这种情况下,TCP可以使用PSUH(推送操作)。这时,发送方TCP把PSH置1,并立即创建一个报文段发送出去。接收方TCP收到PSH=1的报文段,就尽快(推送)交付给接收应用进程,而不在等整个缓存都填满了再向上交付
尽快推送PSH
当RST=1时,表明TCP连接出现了严重差错,必须释放连接,然后重新建立新运输连接。**RST=1还可以用来拒接一个非法报文段或拒绝打开一个连接例如:0窗窗口探测3次都无ack返回,time_wait状态结束
差错释放RST
同步信号SYN
用来释放一个连接,当FIN=1时,表明此报文段的发送方数据已经发送完毕,并要求释放运输连接。
终止信号FIN
控制位
窗口
校验和
紧急指针
TCP报文首部
TCP的可靠连接是靠 seq( sequence numbers 序列号)来达成的,TCP 设计中一个基本设定就是:通过TCP 连接发送的每一个包,都有一个sequence number。而因为每个包都是有序列号的,所以都能被对方确认收到这些包
1、序列号
为什么握手要三次
三次握手时ACK丢失怎么办
三次握手
为什么连接的时候是三次握手,关闭的时候却是四次握手?因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,\"你发的FIN报文我收到了\"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可能最后一个ACK丢失。如果ACK丢失,可以在TIME_WAIT状态的2MSL内,等对方再次发送FIN,然后重新给对方发ACKPS:2MSL中,MSL是报文最大生存时间,可以自定义MSL为30秒,1分钟,2分钟,默认为2分钟,2MSL是指2倍MSL
为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?
time_wait状态结束后才会释放端口,当并发请求过多无法及时断开的话,会占用大量的端口资源和服务器资源
内核里有两个hashtable:一个既包含time_wait状态的连接,也包含其他状态的连接。不同内核的hashtable大小设置不同另一个用来保存所有的bound ports,用于遍历找到一个可用端口或者随机端口,占用CPU不过占用内存很少很少。 一个tcp socket占用不到4k。1万条time_wait的连接,也就多消耗1M左右的内存
1、客户端改用长连接需要客户端的改动比较大,但能彻底解决问题,高并发的场景下,长连接的性能也明显好于短连接。
2、修改linux内核减小MSL时间能够降低出问题的概率,需要修改linux内核,难度和风险都较大。
解决:优化TCP/IP 的内核参数(有难度和风险),及时将time_wait状态的端口清理掉
出现大量time_wait会发生什么?怎么办?
客户端发送最后一次ACK之后,自身进入time_wait状态,如果ack在网络中丢失则服务端将再次发送FIN报文,如果没超过2MSL,则客户端重发ACK,如果超过2MSL,接受FIN的时候客户端已经关闭,则客户端发送RST,服务端收到后认为该连接出现异常
可靠的实现TCP全双工连接的终止
如果刚关闭连接就立刻建立新连接,可能会出现上一个连接发送较慢的数据包被新连接接收,破坏了新连接
允许老的重复节点在网络中消逝
为什么一定要time_wait?
TIME_WAIT
四次挥手
TCP 将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段
滑动窗口协议详解
TCP 连接的每一方都有固定大小的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议。(窗口字段包含在确认报文中,为0的时候表示不能发送数据)
滑动窗口中,窗口关闭导致的死锁问题,用0窗口探测解决
流量控制(被接收方要求发送方降低速率
当网络拥塞时,减少数据的发送。
一开始立即把大量数据字节注入到网络,可能会引起网络阻塞,因为现在还不知道网络的符合情况经验表明,较好的方法是先探测一下,即由小到大逐渐增大发送窗口
cwnd初始值为1,每经过一个传播轮次,cwnd加倍
慢开始
cwnd>=门限就会触发拥塞发生算法
每经过一个往返时间就把发送方的cwnd加1
拥塞避免
门限减半,起点为1,然后执行慢开始
超时重传
通过快重传引起的拥塞发生算法才会进入快恢复
门限减半,起点减半
快重传算法首先要求接收方每收到一个失序的报文段就立即发出重复确认(为的是使发送方及早的知道有报文段没有到达对方)而不要等到自己发送数据时才捎带确认。快重传算法规定,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待为其设置的重传计时器到期。
概率问题TCP按序发送,但TCP包是封装在IP包内,IP包在传输时乱序,意味着TCP包到达接收端也是乱序
1、为什么要设置为3个重复确认
SACK
2、重传的时候,是只重传丢失的报文,还是重传在重复确认时发送的所有报文
快重传是啥
快速重传
拥塞发生算法的两种触发方式和算法
快速重传和快速恢复算法一般同时使用,快速恢复算法是认为,你还能收到 3 个重复 ACK 说明网络也不那么糟糕,所以没有必要像 RTO 超时那么强烈
在门限减半和起点减半之后,cwnd=门限+3( 3 的意思是确认有 3 个数据包被收到了),并进入拥塞避免
快恢复
拥塞控制(被网络太拥堵要求发送方降低速率
它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组。如果发送方在发送后一段时间之内没有收到确认帧,它通常会重新发送
优点: 简单缺点: 信道利用率低,等待时间长
当 TCP 发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段
超时重传机制
A向B发送消息,B收到后给A发确认,但是确认丢失了,但A不知道,超时后A就继续发消息这时B会做两件事 1.把A重发的消息丢弃 2.再次向A发确认消息
确认丢失机制
A向B发送消息,B收到后给A发确认,但确认迟到,但A不知道,超时后A就继续发消息B又会收到重复消息,又发了个确认。结果是A发了两次消息,B发了两次确认处理如下:1.A丢弃第二次确认消息 2.B丢弃第二次收到的消息
确认迟到机制
停止等待ARQ协议
连续 ARQ 协议 可提高信道利用率。发送维持一个发送窗口,凡位于发送窗口内的分组可连续发送出去,而不需要等待对方确认。接收方一般采用累积确认,对按序到达的最后一个分组发送确认,表明到这个分组位置的所有分组都已经正确收到了
优点: 信道利用率高,容易实现,即使确认丢失,也不必重传。缺点: 不能及时向发送方反映出接收方已经正确收到的所有分组的信息。
连续ARQ协议
ARQ协议(自动重传请求协议
tcp如何保证可靠传输
1、要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。
2、待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包
3、要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。
4、接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包
发生原因
1、发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了
2、发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来
3、可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开
粘包&拆包
TCP
TCP(传输控制协议)
UDP(用户数据报协议)报文首部
三挥四握
TCP面向连接,UDP无连接
三挥四握,确认、窗口、重传、流量阻塞控制等
TCP可靠,UDP不可靠
TCP传输慢,UDP传输快
TCP占用资源多,UDP占用资源少
三次挥手四次握手
tcp和udp区别
TCP和UDP
IP报文首部
MAC 的作用则是实现「直连」的两个设备之间通信IP 则负责在「没有直连」的两个网络之间进行通信传输。
IP(网络层)和MAC(数据链路层)的区别
IP
阻塞IO
非阻塞IO
fd:file description 文件描述符。fds:存放fd的数组。
虚线以上内容:创建socket服务端+fd+fds
max:最大fd编号,标记轮询的范围
rset(fd_set):实际上是一个默认1024的bitmap
单线程下多个网络请求被系统(DMA)识别成多个fd,形成fds数组,再将fds数组中的编号存放至fd_set(编号是几,从0开始bitmap从左往右数第几位就是1),并在内核态下检查哪些位置是1,也就是有数据的
bitmap会从用户态放入内核态,对有数据的位置进行置位(做个标记),当有数据时select有返回值,然后回到用户态下的for遍历哪些位置被置位过,并读取该数据。否则没有返回值,select会阻塞
成功调用返回结果大于 0,超时返回结果为 0,出错返回结果为 -1
默认最大1024的bitmap虽然可调大小,但仍旧有上限
fdset不可重用,在内核中被置位了
每次都需要重新将fdset从用户态拷贝进内核态,有一定开销
O(n)再次遍历问题。因为rset里的fd被置位后,select函数并不知道哪个被置位了,需要从头遍历到尾,逐个对比
四个缺点
1. 最好不要频繁的进入内核区,不但危险而且实际上是很慢的。2. 对于每一个系统级的函数粒度越细越好
个人思考
select(windows/linux)
好处是不用一次申请足够大的空间,而可以分批次去申请空间。
解决缺点1:存储结构:以页为单位的链表结构,页中包含有pollfd这个数据。由poll_list(链表结构)指向pollfds数组,然后pollfd结构体存放在pollfds数组里
解决缺点2revents在每次为1时,又会恢复为0再执行读取等后续操作
基于结构体存储fd\tstruct pollfd{ int fd; //fd编号 short events; //将做读还是写 short revents; //可重用\t}
epoll_create调用时:1.内核在epoll文件系统里建了个file结点,建立一个rdllist双向链表,用于存储准备就绪的事件 2.内核在cache里建了个红黑树用于存储以后epoll_ctl传来的socket
epoll_ctl 用于向内核注册新的或修改文件描述符,已注册的描述符在内核中的红黑树上
epoll_wait调用时,仅仅观察这个rdllist双向链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效
LT(水平触发)模式下,只要有数据就触发,缓冲区剩余未读尽的数据会导致 epoll_wait都会返回它的事件;ET(边缘触发)模式下,只有新数据到来才触发,不管缓存区中是否还有数据,缓冲区剩余未读尽的数据不会导致epoll_wait返回。
epoll两种模式
解决缺点1:在epoll中对于每一个事件都会建立一个epitem结构体,无上限问题
解决缺点2:epitem结构体可重用
解决缺点3:只在while(1)循环外通过epoll_ctl将fd添加进内核一次,无需重复拷贝
解决缺点4:epoll_wait检查到rdllist有数据并执行完后,会返回告知用户有几个fd,那么for遍历时走几步即可,时间O1
使用场景:redis、nginx、(linux下)javaNIO
epoll(基于Linux)解决select的1,2,3,4
select,poll,epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
当客户处理多个描述符时(一般是交互式输入和网络套接口),必须使用I/O复用
如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用
多路复用就是在单线程里利用一个监听机制实现对多个客户端的监听,若客户端有反应,则代表有读写事件
总结
IO多路复用
IO多路复用第二版
信号驱动IO
异步IO
打开一个文件时,内核会返回一个文件描述符应用程序进程拿到的文件描述符ID == 进程文件描述符表的索引,通过索引拿到文件指针,指向系统级文件描述符表的文件偏移量,再通过文件偏移量找到inode指针,最终对应到真实的文件
文件描述符(FD)
I/O模型
注重的是结果,同步就是一直等待。异步:时不时地去看有没有结果
同步&异步
注重的是过程
阻塞&非阻塞
BIO&NIO&AIO
网络
c是指consistency一致性,保证读时能返回最新写的数据;a指available可用性,表示服务器可以在合理时间内返回合理结果(不是错误或者超时的响应);p指partition tolerance分区容错性,表示出现网络分区后仍能够对外提供服务。
很多人认为cap只能任意三选二,不对,实际上出现网络分区后,p是前提,也就是必须保证能对外提供服务,再从ca中2选1,为什么ca只能2选1呢?因为一致性相当于在写的时候要让读等待,就保证不了可用性,也就是不能及时返回结果。
应用方面,zk可以保证cp,无法保证ap,因为zk在选举leader或者超过一半节点故障时,整个服务器将不可用。Eureka保证ap,因为节点都是平等的,但无法保证cp可能会返回旧数据。而Nacos可以保证cp和ap。总结:在没出现网络分区时尽量思考如何保证ca,出现网络分区时,考虑保证cp还是ap
简述CAP理论
ap状态会产生一些损耗,如时间上慢些,非必须功能不可用
BA基本可用
数据暂时不一致,但最终会一致,如银行转账延迟
S软状态
强一致性(实时一致性)
弱一致性(不确定什么时候一致)
最终一致性(确定未来某个时间会一致)(业界推崇)
分布式一致性三个级别
E最终一致性
BASE理论(本质上是对ap的延伸)
分布式原理
Seata
TCC
2PC
3PC
分布式事务
所有功能都在一个机器上部署,扩展性低,不利于维护
orm单一应用架构
多个模块系统分开部署,但公用模块无法重复利用,开发性的浪费
mvc垂直应用架构
在垂直架构基础上,把公共模块抽取出来
RPC分布式服务架构
soa流动计算架构
发展演变
分布式
高并发,考虑redis
大容量,考虑分库分表
redis/db2,考虑同步
有状态,考虑hash
消息顺序,考虑单线程
实时,考虑同步,延时,考虑异步
注意点:
微信红包实现
多线程交替打印奇偶数
场景题
为什么用单点登录:多系统中session不共享问题
SSO系统生成一个token,并将用户信息存到Redis中,并设置过期时间其他系统请求SSO系统进行登录,得到SSO返回的token,写到Cookie中每次请求时,Cookie都会带上,拦截器得到token,判断是否已经登录
单点登录(SSO)
单点登录SSO
在一个多系统共存的环境下,用户在一处登录后,就不用再其他系统中登录,也就是用户的一次登录能得到其他所有系统的信任。
单点登录在大型网站使用非常频繁,例如阿里巴巴网站,在网站的背后是成白上千的子系统,用户的一次操作可能涉及到几十个子系统的协作,如果每个子系统都需要用户验证会导致系统效率非常低
解决如何产生和存储信任,系统如何验证这个信任的有效性(1.存储信任 2.验证信任)
需要解决的问题
创建一个cookies
通过注解的方式获得cookies
cookie 一般是由与用户访问页面而被创建的 , 可是并不是只有在创建 cookie 的页面才可以访问这个cookie。在默认情况下,出于安全方面的考虑,只有与创建 cookie 的页面处于同一个目录或在创建cookie页面的子目录下的网页才可以访问。那么此时如果希望其父级或者整个网页都能够使用cookie,就需要进行路径的设置
共享Cookie当我们的子系统都在一个父级域名下时,我们可以将Cookie种在父域下,这样浏览器同域名下的Cookie则可以共享,这样可以通过Cookie加解密的算法获取用户SessionID,从而实现SSO。但是,后面我们发现这种方式有几种弊端:a. 所有同域名的系统都能获取SessionID,易被修改且不安全;b. 跨域无法使用。
路径
解决
cookies存在跨域问题
1、cookie
1
2
Controller只是发布服务。接收三个参数,一个是要校验的数据,一个数据类型,一个是callback。调用Service校验,返回json数据,需要支持jsonp,需要判断callback (扩展:@PathVariable在备注里)
Service接收两个参数,一个是要校验的数据,一个是数据类型。根据不同的数据类型生成不同的查询条件,到user表中进行查询如果查询到结果返回false,查询结果为空返回true
数据校验接口
Controller接收一个表单,请求的方法为post。使用TbUser接收表单的内容。调用Service插入数据,返回
Service接收TbUser参数,对数据进行校验,校验成功,插入数据,返回结果
用户注册接口
用户注册
jedisClient用于设置和更新session过期时间
准备jedisClient接口
Controller接收两个参数,一个是用户名,一个是密码,请求的方法为post。调用Service方法返回登录处理结果,响应json数据
共享Session共享Session可谓是实现单点登录最直接、最简单的方式。将用户认证信息保存于Session中,即以Session内存储的值为用户凭证,这在单个站点内使用是很正常也很容易实现的,而在用户验证、用户信息管理与业务应用分离的场景下即会遇到单点登录的问题,在应用体系简单,子系统很少的情况下,可以考虑采用Session共享的方法来处理这个问题
这个架构使用了基于Redis的Session共享方案。将Session存储于Redis上,然后将整个系统的全局Cookie Domain设置于顶级域名上,这样SessionID就能在各个子系统间共享。这个方案存在着严重的扩展性问题,首先,ASP.NET的Session存储必须为SessionStateItemCollection对象,而存储的结构是经过序列化后经过加密存储的。并且当用户访问应用时,他首先做的就是将存储容器里的所有内容全部取出,并且反序列化为SessionStateItemCollection对象。这就决定了他具有以下约束:1、 Session中所涉及的类型必须是子系统中共同拥有的(即程序集、类型都需要一致),这导致Session的使用受到诸多限制;2、 跨顶级域名的情况完全无法处理;
Service接收用户名、密码。校验密码是否正确,生成token,向redis中写入用户信息,把token写入cookie,并在返回结果中包含token。
用户登录接口
Controller从url中取token的内容,调用Service取用户信息,响应json数据。
Service接收token,根据token查询redis,查询到结果返回用户对象,更新过期时间。如果查询不到结果,返回Session已经过期,状态码400
通过token查询用户信息
用户登录
展示注册和登录页面
当用户在首页点击登录或者注册的时候需要跳转到sso系统。进行相应的操作。登录成功跳转到首页。首页应该显示当前登录的用户
门户登录
有些页面是需要登录之后才能访问的,比如订单页面,当用户查看订单页面时此时必须要求用户登录,可以使用拦截器来实现。拦截器的处理流程为拦截请求url从cookie中取token如果没有token跳转到登录页面。取到token,需要调用sso系统的服务查询用户信息。如果用户session已经过期,跳转到登录页面如果没有过期,放行
拦截器配置
扩展:HandlerInterceptor拦截器
在springmvc中实现HandlerInterceptor接口开始拦截->取token->验证token->调用相关服务
其对应的Service作用为:根据token取用户信息,如果取到返回TbUser对象,如果取不到,返回null
登录拦截器
其他系统整合SSO
流程
单点登录(SSO)扩展
登录功能如何实现
个人商城系统,后台对购物车数据进行“半持久化”。因为购物车增删改的操作很频繁,如果使用mysql效率会很低,所以使用redis进行存储。如果担心redis会挂,可使用redis集群,还是很靠谱的
用户可以在登录状态下将商品加入在线购物车,在未登录状态下加入离线购物车
登录后会将未登录时缓存的购物车数据合并到账号中,并清空原先缓存
退出登录后原来的离线购物车数据也不存在
核心思路
hset key filed value(hset cart:userid id count)(多加个cart: 是因为要区分别的功能)
添加功能:假设用户id为1001,放3个商品,产品id为10021,10025,10079,各放1个
删除商品:hdel
加减商品数量:hincrby(减时用负数)
商品数量:hlen
全选功能:hgetall
redis存入购物车商品的数据结构
购物车功能如何实现
由于浏览器同源策略产生跨域问题
使用nginx代理配置为同一域
利用jsonp在<script>标签里添加外源地址
可以向@RequestMapping注解处理程序方法添加一个@CrossOrigin注解,以便启用CORS(默认情况下,@CrossOrigin允许在@RequestMapping注解中指定的所有源和HTTP方法)
@CrossOrigin中的2个参数:origins: 允许可访问的域列表maxAge:准备响应前的缓存持续的最大时间(以秒为单位)
@CrossOrigin
请求方式为 HEAD、GET、POST 这三种方式之一HTTP头信息中开发者添加的信息不超过以下几种
对于简单请求,浏览器直接发出 CORS 请求,具体来说,就是在头信息之中,添加一个 Origin 字段.
Origin 字段用来说明,本次请求来自来个源,服务器根据这个值决定是否同意这个请求。如果该值在许可范围(即允许跨域访问),服务器就会返回一个正常的 HTTP 回应,会多出几个头信息字段:Access-Control-Allow-Origin : 该字段必须的,表示接受该值对应的域名的请求。Access-Control-Allow-Credentials : 该值是一个布尔值,表示是否允许发送 Cookie
简单请求
原理:CORS 请求分为两类:简单请求和复杂请求
CORS
解决跨域问题
分片上传、断点续传、分片导出、HTTP 请求头、响应头的字段(待补充)
文件上传问题
手写RPC框架
项目
Clone:克隆,就是将远程仓库复制到本地
Push:推送,就是将本地仓库代码上传到远程仓库
Pull:拉取,就是将远程仓库代码下载到本地仓库
基本操作
工作流程如下:1.从远程仓库中克隆代码到本地仓库2.从本地仓库中checkout代码然后进行代码修改3.在提交前先将代码提交到暂存区4.提交到本地仓库。本地仓库中保存修改的各个历史版本5.修改完成后,需要和团队成员共享代码时,将代码push到远程仓库
工作流程
当安装Git后首先要做的事情是设置用户名称和email地址。这是非常重要的,因为每次Git提交都会使用该用户信息设置用户信息git config --global user.name “itcast”git config --global user.email “kinggm520@163.com”查看配置信息git config --listgit config user.name通过上面的命令设置的信息会保存在~/.gitconfig文件中
环境配置
获取Git仓库
git revert是用一次新的commit来回滚之前的commit,git reset是直接删除指定的commit。在回滚这一操作上看,效果差不多。但是在日后继续merge以前的老版本时有区别。因为git revert是用一次逆向的commit“中和”之前的提交,因此日后合并老的branch时,导致这部分改变不会再次出现,但是git reset是之间把某些commit在某个branch上删除,因而和老的branch再次merge时,这些被回滚的commit应该还会被引入。git reset 是把HEAD向后移动了一下,而git revert是HEAD继续前进,只是新的commit的内容和要revert的内容正好相反,能够抵消要被revert的内容。
Git中的revert(撤消操作)和reset(版本撤回)
常用命令
Git
Maven
工具
测试
阶(度)
红黑树
平衡二叉树(AVL)
二叉排序树(二叉查找树)BST
二叉树
图
2-3树
2-3-4树
B树(balance tree)
B+树
B*树
多路查找树
树
数据结构
排序算法(8大排序算法)
算法
快速排序((快速排序的最差时间复杂度是 O(n^2)),堆排序,归并排序 时间复杂度O(n*logn)
手写递归快排,非递归快排,堆排,归并,Dijkstra,Kruskal
手写5种单例,枚举类
时空复杂度及其优化
手写生产者/消费者模型
手写LRU
手写前中后序遍历的迭代写法
B树:1.叶子节点和非叶子节点都存数据。2.数据无链指针。B+树:1.只有叶子节点存数据。2.数据有链指针。B树优势:1.靠近根节点的数据,访问速度快。B+树优势:1.一页内存可以容纳更多的键,访问数据需要更少的缓存未命中。2.全面扫描只需要扫描叶子节点
红黑树是什么
LSM是什么
数据结构与算法
CPU
总线
南北桥
内存
计算机结构
java后端八股文
收藏
0 条评论
回复 删除
下一页