Java知识大全
前言大三即将结束了,最终还是决定不考研。因此,打算把之前学过的知识再梳理一遍,整理成笔记,并进行一些原理上的思考。1. 在Java中,int类型永远是32位因为Java虚拟机的存在,类型的定义也是跨平台的。2. Java适合于网络/分布式环境分布式的前提是网络环境,由于Java对CS模式的支持,因此对网络的支持也渗透到了方方面面。分布式分为数据分布和操作分布。可以通过url的形式,将数据分布在网络
目录
- 1. 在Java中,int类型永远是32位
- 2. Java适合于网络/分布式环境
- 3. Java的高性能
- 4. Java的健壮性
- 5. JVM、JRE、JDK、JMM
- 6. Java文件夹目录
- 7. 内存模型
- 8. 垃圾回收
- 9. 类加载
- 10. 类型与变量
- 11. 异常处理
- 12. 集合
- 13. IO流
- 14. JVM虚拟机
- 15. 多线程
- 16. 反射
- 17. IO模型
- 18. HashMap
- 19. docker核心原理
- 20. nginx要点
- 21. CAP原则
- 22. mysql核心知识点
- 23. Spring要点
- 24. mybatis要点
- 25. Git要点
- 26. SpringBoot要点
- 27. 设计模式
- 28. 泛型
- 29. Redis要点
- 30. 第三方接口
- 31. 微服务要点
- 32. 秒杀系统
- 33. 数据结构
- 34. 科班基础
- 999. 小知识点
您有任何建议或意见,请您在下方回复或者私信我,感谢!
1. 在Java中,int类型永远是32位
因为Java虚拟机的存在,类型的定义也是跨平台的。
Java的核心在于JVM,Java程序被编译为字节码后,在JVM中运行,与宿主机是隔离的(这也保证了一定的安全性)。
如果其他语言也能编译为字节码,也是可以在JVM中运行的,比如Scala。
这样来看,Java体系本身具有一定的“语言无关性”。
2. Java适合于网络/分布式环境
分布式的前提是网络环境,由于Java对CS模式的支持,因此对网络的支持也渗透到了方方面面。
分布式分为数据分布和操作分布。
可以通过url的形式,将数据分布在网络中,也可以通过分布式的数据库实现数据分布;
操作分布其实就是分布式计算,典型的就是MapReduce。
3. Java的高性能
回忆一下基础知识:
1.高级语言的通用运行原理:
–> 编写代码 : 编写源代码
–> 预处理 :展开头文件/宏替换/去掉注释/条件编译
–> 编译 :语法分析与处理,并生成汇编语言
–> 汇编 :将汇编语言转为机器码
–> 链接 :将库的内容链接到代码中
2.编译型语言与解释型语言
C语言就是典型的编译型语言,就是再执行代码之前,将代码完整的翻译成可执行文件。
python则是典形的解释型语言,遇到一行代码,就翻译一句并执行。
显然,对于编译型语言来说,在得到可执行代码之前,需要完成大量的工作,如果修改了任何一行代码,都需要重新进行编译,但是一旦编译完毕,执行的时候效率就会很高。即准备时间长,执行速度快。
而对于解释型语言,更加灵活,但是解释的过程掺杂在执行过程中,因此执行速度慢。
上述部分并不完整,后续复习到再补充
而Java,则是采取折中方案。
Java先将源代码预编译成平台无关的字节码文件(.class文件),生成的代码介于机器码与Java代码之间,这种代码就是JVM的“机器语言”。但是这种代码不能在机器上执行,依旧需要被翻译成真正的机器语言,JVM就是承担了对字节码文件的解释工作。
因此Java程序的执行速度比纯粹的解释型语言更快,但依旧赶不上编译型语言。
Java源代码到字节码的过程,为了与通用意义上的编译(生成汇编码、机器码)进行区分,最好是理解为预编译。
hotspot(热点编码)
Java利用热点编码技术,提高执行效率。(hotspot也是一种JVM的名称)
热点编码就是把反复多次执行的代码(比如循环体)编译成机器码(缓存),下一次执行的时候就可以直接执行而不再触发解释,以提高程序的运行效率。
大致原理:存在一个计数器,统计代码的执行次数,若超出设定阈值,则直接将该代码编译为机器码,下次进入该代码时,就不需要对其进行解释。
主要方法包括方法计数器以及回边计数器(循环跳转称为回边)
参考
https://zhuanlan.zhihu.com/p/71043114
https://www.cnblogs.com/yanggb/p/13205449.html
实现hotspot需要使用JIT技术。
JIT技术(JUST IN TIME,即时编译技术)
简单来说就是“懒加载”,只有遇到了需要的东西,才将对应的代码编译成机器码。
缺点:
1.这种加载动作散落在整个程序的生命周期中,累加起来花费了更多的时间。
2.增加了可执行代码的长度(字节码要比即时编译器展开后的本地机器码小的多),这导致页面调度,从而降低程序速度。
4. Java的健壮性
1.强类型,在编译和运行时进行了大量的类型检查,防止了数据类型不匹配的发生。
2.垃圾回收机制(详见 7. 内存模型与 8. 垃圾回收)
3.异常处理机制(详见 11. 异常处理)
5. JVM、JRE、JDK、JMM
JVM(Java Virtual Machine):用于执行bytecode字节码的虚拟计算机,定义了指令集、寄存器集、结构栈、垃圾收集堆、内存区域。负责将java字节码边解释边运行(影响一定的速度)。
不同的操作系统有不同的虚拟机。Java虚拟机屏蔽了底层平台的差异,实现一次编译,到处执行,这就是实现跨平台的核心机制。
JRE(Java Runtime Environment):运行时环境,包括JVM,库函数,运行java程序所必须的文件。
JDK(Java Development Kit):Java 开发工具箱,顾名思义,包含Java开发所必须的所有文件,包含JRE、JVM、编译器、调试器等。
若只需要运行Java程序,只需要安装JRE即可(实际非常小)
JMM:Java内存模型
6. Java文件夹目录
bin 可执行二进制文件
db 数据文件
include 包
lib 相关jar包
src.zip jdk相关java类的源码
7. 内存模型
Java程序从编码(.java文件)到编译(.class文件)再到jvm中,jvm首先字节码文件进行类加载,然后通过执行引擎进行执行,在执行过程中会创建一个内存区域,称为Runtime Data Area(运行时数据区),Java内存管理主要就是对该区域进行管理。
从运行的角度来看:
线程拥有各自的工作内存(线程栈),他们共享一个主内存(堆)。当线程需要操作主内存中的对象时,会先将其复制到自己的工作内存中,操作完毕后再写回主内存中。
从方法的角度来看:
主要分为五大区域,其中三个是线程私有:虚拟机栈、本地方法栈、程序计数器,两个线程共享:堆、方法区。
- 虚拟机栈:方法执行的内存模型,存储局部变量,操作数(被运算的对象),方法出口(调用方法之前的地方)等
- 本地方法栈:调用native时使用的内存模型
- 程序计数器:行号指示器,代码跳转(分支、循环、异常处理)都依赖程序计数器。
- 堆:保存对象实例
- 方法区:类相关信息,如代码、常量(运行时常量池)、静态变量、静态方法等
对象的内存分配
由于heap是线程共享的,因此是非线程安全的,所以JVM自然需要解决这个问题。
在Eden空间中,还有一个小空间(缺省占1%的Eden内存,可以调控),称为TLAB(本地线程分配缓冲区),该区域是线程私有的,能够保证线程安全。
因此,当字节码完成类装载后,首先尝试在TLAB中给对象分配空间,若失败了,则到Eden中进行分配(大对象直接去老年代分配)。
在共享区域,JVM通过加锁保证线程安全。
内存特性
原子性
对基本数据类型的变量读取和赋值的操作都是原子操作,即这些操作是不可中断,要么执行,要么不执行。
x = 10; //语句1
y = x; //语句2
x++; //语句3
x = x + 1; //语句4
以上语句只有语句1是原子性操作。
语句1是直接将数值10赋值给x,也就是说线程执行这个语句会直接将数值10写入到工作内存中。
语句2实际上包含2个操作,先读取x的值,再将x的值写入到工作内存中,虽然读取x的值及将x的值写入工作内存都是原子操作,但结合起来就不是原子性的操作了。
x++和x=x+1包含3个操作,读取x的值,进行加1操作,写入新的值。
也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。
Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。
有序性
编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
也就是说:在并发时,程序的执行可能会出现乱序。
可见性
一个线程修改了某一个共享变量的值,其他线程是否能够立刻知道这个修改。(可见会牺牲性能)。
通过volatile关键字保证可见性:
当一个共享变量被volatile修饰时,它会保证修改的值会立即更新到主存,当有其他线程需要读取时,它会去主存中读取最新的值。
普通的共享变量不能保证可见性,因为共享变量被修改后,什么时候被写入主存是不确定的,当其他线程去读取的时候,此时内存中可能还是原来的旧值,因此无法保证可见性。
通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
8. 垃圾回收
线程私有的区域,等到线程结束的时候,会自动释放,因此GC主要处理的是线程共享的区域,即堆与方法区。
-
引用计数法
给每个对象添加一个计数器,引用一次计数器就加一,反之减一,若在一定时间内计数器保持为0,则对其回收。
但这种算法无法解决循环引用问题。 -
可达性分析算法
利用一个GC Root节点,建立起一张引用图,凡是从GC Root根节点出发不可达的对象,则对其回收。该算法能够解决循环引用。
显然,GC Root对象不需要担心内存回收问题,因此该对象应该具有自动回收属性或者永久性的特点,因此,可以作为GC Root的对象也就可以猜测出来。
- 栈帧中的局部变量(包括虚拟机栈和native栈) ---- gc不需要回收他们,栈帧释放后他们会自动回收,因此适合作为GC Root
- 方法区中的静态对象或者常量 ---- 他们在运行时具有永久性的特点
垃圾回收算法之分代垃圾回收
垃圾回收算法:
- 标记回收:标记需要回收的对象,然后清除,造成内存碎片
- 标记清理:标记回收的基础上,进行内存紧凑
- 复制:from——to区域所用算法
- 分代:如下描述
堆内存分为新生代,老年代 和持久代
不同区的对象生命周期不同,因此回收算法也不同。
-
新对象优先在Eden区进行分配(大对象直接进入老年代,-XX:PretenureSizeThreshold=3145728 这个参数来定义多大的对象直接进入老年代),当Eden满后,触发一次Minor GC,清理无用对象,并将有用对象复制到Survivor区中。
-
在Survivor区中循环存放超过n次之后还未被回收的对象,会放到Tenured区,当Tenured区满后,会触发一次Major GC。
-
持久代,用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响。
JVM调优很大一部分工作就是对FullGC调优
如下原因可能导致Full GC:
- 1.年老代(Tenured)被写满
- 2.持久代(Perm)被写满
- 3.System.gc()被显式调用(程序建议GC启动,但不会直接调用GC)
- 4.上一次GC之后Heap的各域分配策略动态变化
Full GC代价非常高昂,所有程序都必须暂时中止。
之所以需要两个survivor区,是为了进行Copying算法。
当一个survivor满后,进行扫描,然后清除无用对象,此时会散布着多个有用对象,存在很多内存碎片,因此把有用对象复制到另一个survivor中,完全腾空当前survivor,能够避免内存碎片,提高后续内存利用效率。
引用与垃圾回收
引用分为强、软、弱、虚。
-
强引用(Strong Reference)
被new出来的对象,为强引用。
若没有显式设为null,则永不回收。
注意,对象声明为null,也仅仅是告知GC该对象可以被回收,而不是立即回收。 -
软引用(Soft Reference)
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj =null;
sf.get();//有时候会返回null
此时sf就是一个软引用对象。
若即将抛出OOM之前,软引用对象会被GC回收。
-
弱引用(Weak Reference)
用法与软引用类似。使用WeakReference对象实现。
弱引用对象在第二次GC时进行回收(无论内存是否充足)。 -
虚引用(Phantom Reference)
虚引用是每次垃圾回收的时候都会被回收,虚引用主要用于检测对象是否已经从内存中删除(通过虚引用对象的get方法,若返回null,则说明原对象已被回收)。
记忆:
强引用 n次GC
软引用 n-1次GC(OOM之前)
弱引用 2次GC
虚引用 1次GC
内存泄漏与内存溢出
内存泄露:内存空间脱离控制范围(比如只释放了链头导致其他节点丢失)
内存溢出:内存不足
可能导致内存泄露的情况如下:
- 使用静态集合类:静态集合类生命周期与程序一致,其内的对象不会被GC
- 单例模式:单例模式的生命周期与程序一致,若单例对象中持有其他非必须对象,也会常驻内存(可以采用非强引用来解决)
- 变量不合理的作用域
- 创建大量无用对象(比如用加号拼接字符串)
9. 类加载
类加载过程包括“加载字节码到内存”、“链接”、“初始化类”三个过程。
生命周期如下图:
- 加载:通过Class的全限定名加载字节流到方法区,并生成一个java.lang.Class对象表示该类
- 验证:进行格式校验安全校验等
- 准备:为Class对象中的static对象分配对象,并赋予默认值(0、null、false)
- 解析:将符号引用(可以理解为标识符)转为直接引用(可以理解为指针)
初始化(重点):
- 1.执行类构造器()方法的过程:由编译器自动收集类中的所有类变量和静态语句块
- 2.初始化一个类时,若其父类还没有进行初始化,则先对其父类发起初始化(继承树回溯初始化)
- 3.JVM会保证类构造器()在多线程中被正确加锁和同步
- 4.当访问一个Java类的静态域时,只有真正声明这个域的类才会被初始化
假设代码如下:
class Parent {
static {
System.out.println("父类被初始化");
}
}
class Son extends Parent{
static {
System.out.println("子类被初始化");
}
}
public class Test{
public static void main(String[] args) {
Son p1 = new Son();
}
}
结果
父类被初始化
子类被初始化
涉及知识点:
- 静态语句块被收集
- 继承树回溯初始化
假设代码如下:
package xmlStudy;
class Parent {
static String string="parent";
static {
System.out.println("父类被初始化");
}
}
class Son extends Parent{
static {
System.out.println("子类被初始化");
}
}
public class Test{
public static void main(String[] args) {
System.out.println(Son.string);
}
}
结果
父类被初始化
parent
若子类有父类重名的属性,则打印结果是子类的属性值
涉及知识点:
4.当访问一个Java类的静态域时,只有真正声明这个域的类才会被初始化,只有父类会被初始化
引用导致的初始化
-
类的主动引用,一定会发生类的初始化
-
new一个类对象
-
调用类的静态域(成员和方法),不包括final常量
-
使用java.lang.reflect包的方法堆类进行反射调用
-
虚拟机启动类,如命令行编译后执行 java Test ,则Test类一定会被初始化
-
继承树回溯初始化,当父类没有被初始化时,优先初始化父类。
-
类的被动引用,不会发生类的初始化
-
访问静态域时,真正声明这个域的类才会被初始化(通过子类引用父类的静态变量,不会导致子类初始化,参照上面代码)
-
通过数组定义类引用,不会导致类的初始化
-
引用常量不会触发初始化(常量在编译阶段就被放入method area中)
类加载器与双亲委派机制
类的加载需要类加载器来实现,而Java默认的类加载器使用双亲委派机制,即:如果一个类加载器收到了加载请求,它会层层向上递交给父类加载器去完成,若所有父类加载器均无法加载,则由其自己加载。
这种模式能够保证核心库的安全(比如不会出现用户定义Object对象的情况),但是tomcat则相反,使用子类的类加载器进行加载(tomcat下存在多个项目,但是这些项目中存在同路径同名的类,双亲委派就会导致加载覆盖)。
- bootstrap:引导类加载器,加载Java核心库
- extension:扩展类加载器,加载Java扩展库
- application:应用程序类加载器,一般的类都由其根据类路径进行加载(可用ClassLoader.getSystemClassLoader() 方法的返回,所以一般也称它为系统类加载器。)
- 其他:继承java.lang.ClassLoader然后重写findClass方法(loadClass方法在所有父加载器无法加载时会调用findClass方法)实现自定义类加载器,自定义类加载器能够实现一些类的加密解密。
双亲委派机制原理
// 代码摘自《深入理解Java虚拟机》
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 首先,检查请求的类是否已经被加载过了
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器抛出ClassNotFoundException
// 说明父类加载器无法完成加载请求
}
if (c == null) {
// 在父类加载器无法加载的时候
// 再调用本身的findClass方法来进行类加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
application类加载器加载classPath下的类,若出现class not found错误,考虑classPath下读取不到该类,可以通过System.getProperty(“java.class.path”)查看当前classPath
线程上下文类加载器
Java有很多东西只提供了接口,而具体实现由第三方厂商来完成(如JDBC)。
我们使用的时候,其实调用的是接口,由于双亲委派机制会导致API与实现无法匹配的问题(SPI(service provide interface)问题),那么JDBC的具体代码是如何被加载进来的?
在Java中使用mysql时,常常见到通过Class.forName()来加载mysql驱动,其实这就是调用了线程上下文类加载器对其进行加载。
10. 类型与变量
11. 异常处理
Try-catch-finally中的return
阿里巴巴Java开发手册建议不要在finally中使用return,因为finally的return会覆盖catch或try中的值(而非网上说的会将catch的值临时存在栈中再返回)。
测试如下:
输出结果
12. 集合
要点
-
1.List的元素有顺序,可重复,包括:
- a)ArrayList:查询效率高,增删效率低,线程不安全
- b)LinkedList:查询效率低,增删效率高,线程不安全
- c)Vector:ArrayList的线程安全版,效率低
-
2.Set的元素无顺序,不可重复,包括:
- a)HashSet:查询、增删效率高
- b)TreeSet:用TreeMap的Key实现的,内部需要对存储的元素进行排序,因此,对应的类需要实现Comparable接口(编写比较逻辑),这样才能根据compareTo()方法比较对象之间的大小,才能进行内部排序。
-
3.Map,key-value,key不可重复,value可以,包括
- a)HashMap:最常用,效率最高
- b)TreeMap:自动按照key升序排列
-
装载可能涉及到排序的对象时,需要对该对象重写equals、hashCode方法
13. IO流
标准代码
import java.io.*;
public class TestIO {
public static void main(String[] args) {
try (
FileInputStream fis = new FileInputStream("d:/a.txt");
)
{
StringBuilder sb = new StringBuilder();
int temp = 0;
//当temp等于-1时,表示已经到了文件结尾,停止读取
while ((temp = fis.read()) != -1) {
sb.append((char) temp);
}
System.out.println(sb);
} catch (Exception e) {
e.printStackTrace();
}
}
}
- 字符流 操作文字文件
- 字节流 操作所有文件(但是读取中文可能读取到半个中文导致出错,而且写入中文的时候需要先将String转为字节数组)
- 字节数组–>可以理解为内存,JVM拥有自己的内存区域,因此字节数组流(ByteArray…Stream)实际上是被JVM操纵的,能够实现自动回收,相关流的close其实是个空方法
- 所有Stream结尾的都是字节流,其他reader和writer是字符流,特殊情况:OutPutStreamWriter,将字符流转为字节流,InputStreamReader,将字节流转为字符流。
- 字节流直接操纵文件,没有缓冲区,字符流有缓冲区,写后需要flush(close后会自动flush)。
- 由于缓冲区其实就是一块内存,因此使用字节流更好(省内存,但是字符流因为有缓冲区效率高)
14. JVM虚拟机
Java的精华之处,就在于JVM。
Java源代码通过JVM编译成字节码,实现一次编码处处运行。
源代码到字节码的编译过程
- 词法分析:将源代码解析为Token语法文件,并通过Name对象将Token与源代码建立一一对应关系
- 语法分析:根据Token生成对应的语法树
- 语义分析:完善语法树(类型检查、异常检查、缺省配置等)
- 生成字节码:根据语法树生成字节码文件
通用的Token 包含“源代码”、“类型”、“语义值”;
比如 “32”–> Token(“32”,“整数”,“32”)
可以将javacc理解为“编译器的编译器”,可以实现词法分析、语法分析、语义分析等功能。
字节码(中间代码)的优点
在源代码中,不同的表达式语义可能是一致的(比如三元表达式与if语句),但若对其优化,他们的优化机制是完全不同的。中间代码具有语言无关性,转为中间代码后,只保留了语义,因此对于同语义的只需要一种优化方式。
在转为中间代码的过程中就可以进行优化,将优化工作提前能够减少后期优化工作量。
若直接对源代码进行优化,是十分复杂的,转为中间代码能够提高可读性、降低复杂性。
HotSpot JVM
分层编译策略
在HotSpot JVM中,当JVM启动后,首先发挥作用的是解释器,边翻译边执行;
随着时间的推进,编译器逐渐发挥作用,通过热点探测功能,将热点代码编译成本地机器码。当下一次遇到热点代码时,便不需要编译或解释,而是直接执行。
此外,编译器包含client compile和server compile两种,client compile会对代码进行简单可靠的优化,而server compile会进行耗时更长的编译优化
JVM优化
- JVM的优化点可以涉及到编译优化、解释优化(普通人做不了)、GC优化(常用,核心在于降低GC频率、缩短GC时间)
- 淘宝JVM就是对JVM的定制化,主要特点之一极大地提升了编译速度。
- 最简单的堆外分配(off heap)就是栈上分配,对于不发生逃逸的对象(逃逸:作用域仅在方法内,不作为返回值、不被外部引用、不在外部赋值),可以从堆内移出放在栈中。
- Eden区最好尽可能大:因为Eden区存在非常频繁的对象创建,足够大的话能够降低GC频率,能够减少新对象直接前往老年代的概率(但会增加GC时间)
编译优化
- 常量折叠: int A=2*1024,编译时直接求出实际值
- 代数简化:就是初高中学的,把式子简化
- 降低运算强度:把乘除转为加法,复杂运算通过数学进行展开
- 消除公共子表达式:上下文中存在共同的运算式,则将该运算式提取出来共用
- 消除无效语句:常见于调试用语句,实际无效
- 函数内联:比如某个函数,输入x,返回2x,则将该函数直接作为表达式放在调用处(输入x确定的时候,又可以通过常量折叠,直接将2x求出来)
STW优化
full cg造成的stop the world,其实就是full cg优化。减少cg频率,降低cg时间。
15. 多线程
程序、进程、线程
概念
- 程序:静态的概念
- 进程:资源分配的基本单位,运行的程序
- 线程:系统调度的基本单位,一个程序中有多个事件同时执行,每个事件一个线程,多个线程共享代码和数据空间(共享进程的资源,因为进程是资源分配的基本单位)
线程同步机制
- synchronized关键字
- Lock接口下的各种锁机制
- 信号量机制
- volatile关键字
创建线程的方法
- 1.继承thread类(由于类只能单继承,少用)
- 2.实现runnable接口(主要使用)
- 3.实现callable接口(juc下的)
每个线程都有优先级,但优先级只代表优先的概率更大
- 用户线程:必须等用户执行完才能停下,最高优先级
- 守护线程:jvm停止之前,一定会等用户线程,但不等守护线程,最低优先级(只用于服务用户线程)
- 默认状况下,所有线程都是用户线程
- 设置线程为守护线程(thread.setDaemon(true))必须保证线程还没有start
- 守护线程的子线程也是守护线程
- 守护线程的主要意义:主线程结束后,该线程没有意义的时候,可以用守护线程(比如Java垃圾回收线程)
lambda
new Thread(()->System.out.println(“多线程中的lambda”)).start().
线程状态
进入就绪状态的情况:
- 1.Start()
- 2.阻塞解除
- 3.运行时调用yield,(若没有其他线程在排队,则yield无效)
- 4.Jvm线程切换
进入阻塞状态的情况
- 1.Sleep:占用资源等待(不会释放锁),通常用于模拟延时,放大错误发生的可能性(检查线程不安全情况)
- 2.Wait:不占资源等待(会释放锁)
- 3.Jion:合并线程,当此线程执行完毕后,再执行其他线程(在A里面调用B.join(),B插队了,A被阻塞了,等B执行完了A再走)
- 4.IO流阻塞,Read、write:必须由操作系统去调度,因此也会阻塞
线程死亡(一般不要使用自带的stop、destroy摧毁)
- 1.在thread中加一个Boolean flag变量,当其为true的时候执行run。thread对外提供一个停止方法,更改flag的值为false,实现停止。
Thread.State
State state = t.getState(); (t为写好的线程实例)
- NEW:尚未启动
- RUNNABLE:在JVM中执行(包含就绪状态和运行状态)
- BLOCKED:被阻塞
- WAITING:正在等待另一个线程执行特定动作
- TIMED_WAITING:正在等待另一个线程达到指定运行时间
- TERMINATED:已退出
HappenBefore
当多行代码没有依赖的时候,为了减少“内存操作速度慢而CPU速度快导致CPU等待内存而影响效率“的情况,jvm和CPU会尝试重排指令顺序,加快运行速度。这就导致代码不一定按你写的顺序执行。
语句的执行的最后一步,从CPU中将计算好的值传回内存,在这个过程中,由于CPU速度快,内存速度慢,可能发生计算好的值还没传回内存,新的指令就使用了该值的旧值。
简单理解:语句A和语句B按AB排列,若A执行完毕会非常慢,而jvm判断B又与A没有依赖,那么为了保证CPU不闲置,jvm会提前执行B。
但是在这种情况下,如何保证一个操作对另一个操作可见?happenbefore就是为了保证可见性的定义的一系列规则。
最常见的例子:
- 多线程下,每个线程都有自己的工作空间,当线程A从主内存中拿到数据x之后,对其进行更改;当更改x过程还没结束的时候,线程B就拿走了x,此时就出现了使用旧值的情况。
但是,表面看似B对A没有依赖,但可能因为多线程的关系,在其他线程中AB存在依赖,这就导致不合理的结果。
public class HappenBefore {
static int a;
static boolean flag;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
a=0;
flag=false;
Thread t1 = new Thread(()-> {
a=1;
flag=true;
});
Thread t2 = new Thread(()-> {
if (flag) {
a*=1;
}
if (a==0) {
System.out.println("HappenBefore a-->"+a);
}
});
t1.start();
t2.start();
}
}
}
上面的代码,理想情况下,只会输出a–>0,但实际还会输出a–>1;
Volatile
为了避免多线程中的数据的HappenBefore,volatile保证了多线程之间变量的可见性。(轻量级的synchronize)
volatile会禁止重排序(内存屏障),内存屏障之后的代码,都不允许重排序到屏障之前,有内存屏障的地方,会保证数据被修改后被立即写回主存。
并发包里面有atomic对象提供原子操作。
核心思想:
- 当cpu写入数据时,如果发现该变量被其他线程共享,则通知其他线程,该变量无效
- 当其他线程访问该变量时,只从主存中获取。
自旋锁
让线程不断循环判断锁是否可用,处于忙等。
CAS操作
比较并替换,主要是在操作值的时候,查看值是否发生变化,如果没有发生变化,就认为对象没有被操作过。
但是会产生ABA问题,比如一个线程修改数据从A变成了B,随后该数据又被改成了A,这时候他看起来就像是没有变化过,通过了CAS校验。可以通过增加版本号来解决这个问题。
本地线程(重要)
ThreadLocal【本地线程】
ThreadLocal给每个线程一个独立的对象空间(一个ThreadLocal只能存放一个对象,重复放置会被覆盖)
实例:
1.Spring通过ThreadLocal保存每个数据库连接,保证单个线程使用的是同一个数据库连接
2.Shiro使用ThreadLocal保存当前用户信息
3.其实就是保存上下文信息
synchronized核心
早期synchronized是重量级锁,底层使用操作系统的互斥锁,每次获取锁和释放锁都需要用户态和内核态的切换。
现在的synchronized采取逐步升级的形式:
- 只有一个线程进入的时候,是偏向锁,进程执行完毕并不释放锁,下次线程进入的时候判断是否是同一个线程,如果是则直接放行。由于初次访问时加锁,以及后续的一次判断,单线程下几乎对性能没有影响。
- 当进入第二个线程,也就是出现锁竞争的时候,升级为自旋锁,抢不到锁的线程原地自旋,但超出自旋阈值之后,会升级为重量级锁,也就是互斥锁,抢不到锁的线程会进入等待队列,等待唤醒。
一定程度的自旋,能够降低用户态和内核态的切换开销。
synchronized的使用先说结论:
- this锁只有一个,锁当前对象
- class锁只有一个,锁静态资源
每次执行之前模拟以下过程:判断是否需要锁-->锁竞争-->得到锁-->执行-->释放锁
①synchronized修饰非静态方法-->锁this
-->1.1 多个线程访问同一个对象的该方法-->同步
-->1.2 同一个对象,一个线程访问synchronized方法,另一个对象访问非synchronized方法-->异步
-->1.3 同一个对象,一个线程访问synchronized方法,另一个对象访问另一个synchronized方法-->同步
结论:一个实例只有一个this锁,
对于1.1,存在锁竞争的过程,因此同步
对于1.2,非synchronized方法不需要锁竞争,因此异步
对于1.3,尽管是两个不同的synchronized方法,但是是同一个锁,也需要锁竞争,因此同步
②synchronized修饰静态方法-->锁class
-->2.1 一个线程访问synchronized静态方法,另一个对象访问非synchronized静态方法-->异步
-->2.2 一个线程访问synchronized静态方法,另一个对象访问另一个synchronized静态方法-->同步
对于2.1,非synchronized方法不需要锁竞争,因此异步
对于2.2,尽管是两个不同的synchronized方法,但是是同一个锁,也需要锁竞争,因此同步
③定义静态变量lock,通过synchronized(lock){}对代码块进行加锁-->锁lock变量
-->3.1 一个线程访问synchronized静态方法,另一个线程访问包含synchronized(lock){}的静态方法-->异步
-->3.2 一个线程访问synchronized静态方法,另一个线程访问包含synchronized(当前类名.class){}的静态方法-->同步
对于3.1,一个锁是class,一个锁是lock,不存在锁竞争,因此异步
对于3.2,两个锁都是当前类名.class,存在锁竞争,因此异步
注意,有种lock的写法是 private static 当前类名 lock = new 当前类名();
此时的lock与当前类名.class依旧不是同一个锁。
④定义静态变量int i,一个线程运行synchronized方法修改i,另一个线程运行非synchronized方法修改i,此时是异步,而且数据不安全。
--> synchronized没有锁住资源,只锁住了代码,在其他入口访问同一份资源依旧会出现数据不同步问题。
⑤定义静态代码块内的synchronized
static{
synchronized(xx){}
}
-->静态代码块会阻塞该类中的所有资源,因为加载静态代码块属于类的初始化过程
综上,其实synchronized的核心就在于加锁过程,我们需要判断当前锁是否存在、是否是同一个锁对象,进而判断是否存在锁竞争,从而得知是否是同步、异步。
sleep与wait的区别
1.sleep是Thread方法,wait是Object方法
2.sleep不会释放锁,wait会释放锁(两者都放弃CPU调度)
3.sleep不依赖于锁,wait的使用依赖于锁
4.sleep不需要主动唤醒,wait需要(wait(time)除外)
线程池
线程池组成
1.任务队列:所有执行线程去任务队列里面拿任务执行
2.线程队列:线程池有一个线程队列
3.拒绝策略(抛出异常、丢弃、阻塞、临时队列):当任务数量超过线程池设定的可接受数量时,进行拒绝的处理策略
4.容量:线程池的线程队列个数可以动态变化。
AQS
AQS指的是abstractQueuedSynchronized,抽象队列同步锁,其下有非常多的实现类,比如信号量、默认的线程池执行器。
其内维护了一个volatile对象state,表示资源状态,以及一个先入先出的队列。
AQS提供了独占以及共享两种锁机制,可以通过继承它然后自定义自己的锁。一般只需要重写state状态变化时的行为即可。
其他
- 避免并发问题:不可变对象(只读)、资源私有化(ThreadLocal)
- 提高并发:读写锁分离(读读不需要锁)、
16. 反射
动态修改注解的值
需要实现一个需求,动态修改springdata neo4j 的@RelationShip注解的type值。
网上搜索后,发现有方法可以解决。
但是实际上,似乎没有办法做到。
个人推测:JVM会将注解的值解释为一个常量(不可变对象),即便你改变了他的值,最终打印结果看似成功改变,但是创建的对象的注解实际值依旧是原来那个。
代码运行效果:
修改前的值:
期望通过运行时修改type的值
修改代码运行前:
修改代码运行后:
看似成功修改
查看neo4j数据库:
修改实际并没有生效。
我试过将修改注解值的操作尽可能提前,但是也没有用,应该是在类初始化完毕之后,该注解信息已经固定。
17. IO模型
IO模型大体分为五种,阻塞IO、非阻塞IO、多路复用IO、信号驱动IO以及异步IO。
IO:分为几个阶段:发起请求、等待数据准备、数据从内核拷贝到用户空间、返回请求响应
- 阻塞式IO:发起请求后等待拿到结果后再返回
- 非阻塞式IO:发起请求后,不断轮询是否有结果。
- 信号驱动IO:发起请求后,注册一个信号,数据准备好之后,内核利用该信号通知用户过来取数据
- 多路复用IO:存在一个代理对象,同时监控多路用户,若有一路处于可用状态,
- 异步IO:发起请求后去处理其他事情,内核完成IO后通知用户取结果。
Select
创建一个bitmap来标志文件描述符(用户线程)的可用状态(上限1024位),然后将bitmap交给select函数,select会将bitmap交给内核来检测是否有文件描述符可用,若有一个可用,则对其置位为1,然后select函数返回。接着遍历bitmap,得到标志位为1的文件描述符,处理它的IO操作。
缺点:
- 1.bitmap有上限
- 2.bitmap不可重用(完成一次IO后bitmap必须重置)
- 3.bitmap从用户态到内核态的切换开销
- 4.标志位扫描 O(n)
poll
原理与select基本一致,但是改了两个地方:
- 使用pollfd结构体的数组取代bitmap结构,因此没有上限
- 遍历pollfd数组的时候,只对单个pollfd结构体标志复位,因此pollfd可以重用
依旧保留了上述3-4的缺点
epoll
使用一个int整数来映射文件描述符(epfd),这个epfd由内核态和用户态共享,避免了切换开销,内核监控epfd的状态变化,并进行置位,此时的置位是将对应的文件描述符往前挪动,最终返回一个可操作文件的个数nfds,然后对对应文件描述符遍历nfds次,逐个操作,这样保证了只对可操作文件进行遍历,复杂度是O(1)
epoll解决了上述4个缺点。
- ET边沿触发:无论事件是否处理完毕,仅通知一次
- LT水平触发:只要事件没有处理完毕,每一次epoll_wait都会登记该事件
18. HashMap
哈希
定义:将任意长度的输入,转为固定长度的输出。
Java中hashCode的方法返回值是int,因此hashCode实际是32位;
特点:
- 不可反向推导
- 输入的微小变化,得到的Hash值不同,同一个Hash算法,输入相同则Hash结果一定相同。
- 执行效率要高效,长文本也能快速计算出哈希值。
- 冲突概率要小
冲突:由于哈希结果的范围比原值要小,因此发生抽屉原理(十个苹果放入九个抽屉,必定有一个抽屉放两个苹果)
HashMap实际是一个数组+链表+红黑树的数据结构,发生冲突时,节点追加到冲突节点之后形成链表,当达到树化阈值(链表长度超过8且HashMap中元素个数达到64),则会将该链表转为红黑树。(底层检查是否对象是否是String,如果是则按String排序,否则利用对象的类名按String排序)
- HashMap继承于AbstractMap,其上有个Map接口;
- HashMap初始化大小是16
- 默认负载因子大小0.75
- 扩容阈值等于当前大小负载因子(默认是160.75=12)
- 树化的链表,阈值8
- 树降级为链表,阈值6
- 树化的元素总个数,阈值64
- HashMap中有个静态内部类Node,包含int hash、K、V、Node next;
- 其中有好几个变量使用transient修饰(避免序列化),因为其他属性就足以还原出完整的HashMap
对HashMap的任意操作,只需要从他的结构出发即可(数组+链表+红黑树)。
比如插入时,计算出对应位置后,看他有没有碰撞,若碰撞了,再看看他是链表还是红黑树,再进行插入;
比如删除时,计算出对应位置后,看他的next是否为空,如果不为空,再看看他是链表还是红黑树,再进行删除;
其他原理类似。
HashMap中的算法
-
哈希算法:
哈希的目的是“给key和数组下标建立一一对应关系”,所以肯定用到了key相关的信息,这里使用的是key的hashCode;而又要保证任何微小的变化都要保证哈希结果不同,所以必须用上key的所有特征。
在计算机中,想要包含一个事物的所有特征,就是用上他的所有“位信息”。key的hashCode是一个32位的int,要用它所有的位信息,只需要它的高位异或上低位即可return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
-
路由算法:
路由结果肯定不能超过数组长度,一个简单方式就是将路由结果的位数控制在数组长度位数内即可。
所以路由算法为 hash & ( table.length - 1) (与算法,数组长度高位全为0,相当于抛弃了hash高位,而数组的长度肯定是二进制数,减一后为全1) -
扩容算法:
当实际元素>负载因子*当前长度时,触发扩容。
扩容后容量翻倍,旧的元素依旧根据路由算法存放。 -
节点相同判断
((k = p.key) == key || (key != null && key.equals(k))))
其中p是旧址,key是当前操作值
HashMap线程不安全
1.7中,扩容的时候,在transfer中有个reHash的过程,可能会改变原本链表的前后顺序,如果多线程恰好都进入resize,则可能因为链表节点的顺序颠倒导致成环。(一个线程先扩容,一个线程后放节点)
1.8移除了transfer方法,但1.8的HashMap可能导致数据覆盖,因为put方法并没有加锁。
保证线程安全需要使用ConcurrentHashMap 。
concurrentHashMap的内部节点类Node的value和next被volatile修饰,保证可见性(get的时候就不需要加锁了,因为一定是最新值,只需要put的时候获得锁)。
在put的时候,如果发现put的位置为空,则使用cas写入,否则的话,用synchronized写入。
19. docker核心原理
隔离特性
隔离可分为资源隔离(进程、文件、网络等)和物理资源隔离(CPU、内存等);
应用层面的隔离(不能产生跨容器的文件冲突)以及物理层面的隔离(不能产生跨容器的资源抢占)
docker利用linux的namespaces实现了文件系统、网络、宿主机间进程的隔离,利用linux的控制组实现物理资源(CPU、内存、磁盘IO等)的隔离。
namespaces是linux用来分离进程树、网络接口以及进程资源通信的方法;
控制组(CGroup)是linux用于隔离进程组使用物理资源的机制。
镜像
镜像本身是一个文件与linux根目录的目录结构差不多
分层存储
docker使用联合文件系统(UnionFS),使用dockerFile构建镜像的时候可以看到,docker容器其实是按层构建的,每层都是只读的,只有最上层的容器层提供给用户读写。
docker能够在不同容器之间进行层的复用(新建一个docker,若发现其中所需的某层之前下载过,那么不会再下载,而是直接拿来用)
数据卷挂载
持久化方案,当容器删除了,宿主机挂载的数据不会被删除
网络
利用namespaces隔离了网络,提供四种网络模式
- None:该模式关闭了容器的网络功能。
- host:容器将不会虚拟出自己的网卡,配置自己的IP等,而是使用宿主机的IP和端口。
- Container:创建的容器不会创建自己的网卡,配置自己的IP,而是和一个指定的容器共享IP、端口范围。
- Bridge:默认模式,此模式会为每一个容器分配、设置IP等,并将容器连接到一个docker0虚拟网桥,通过docker0网桥以及Iptables 配置与宿主机通信。
20. nginx要点
nginx是一个反向代理服务器,能够屏蔽内部系统的网络细节,并且可以在HTTP协议上做一些统一的配置(比如解决跨域,请求过滤),可以对用户的请求做负载均衡,可以做缓存,降低服务器压力,
高效性
nginx使用epoll模型;
nginx使用多线程,对外有个master线程,其下有多个worker进程,master转发请求到某个worker去处理。
负载均衡策略
- 轮询:按请求顺序挨个分配服务器(存在http连接浪费、session丢失、下载断片等问题)
- 指定权重:根据服务器的承受能力指定权重,访问压力也会按照权重进行分布
- IP哈希:将IP哈希到某个服务器上,能够实现session保持(同一个IP每次都是一台机器)
- fair:按照服务器响应时间长度来选择,谁快谁上
- url哈希:一般用于缓存命中需求高的服务器,比如文件下载服务器,文件实际保存在第三方云平台,那么每个文件下载地址(url)都被缓存到了各自的服务器上,此时若对url进行哈希,能够避免重复缓存,还能提高缓存命中率;
集群(负载均衡)导致的session丢失问题
session会话建立在一个客户端以及一个服务端之间,若用户使用途中因为负载均衡导致访问的服务器切换,则session也会丢失。
解决办法:
- session保持:保证用户访问的服务器不会被切换(IP哈希)
- session复制:session在服务器之间复制
- session共享:统一管理session,所有服务器共享session,比如使用redis
一致性哈希
一般的哈希不能保证当“桶”的个数发生变化后的一致性。
当我们使用哈希作为负载均衡策略的时候,
比如使用简单的取余,有十台服务器,那么就是通过 hash(x)%10找到对应服务器。
假设一台服务器挂掉了,那么算法变成了 hash(x)%9,这时候会导致之前的缓存、session等全部失效,原本跑到A服务器的请求,会全部被转移到B去。
增加一台服务器也是这种情况。
因此需要一个算法,增加或者删除容器个数,都不会影响原有的映射关系。也就是一致性哈希
一致性哈希将虚拟出非常多个桶位(比如有十台机器,但是虚拟出有100台),这些桶位形成一个环形链表,进行哈希计算的时候,通过模100,得到一个桶位,如果该桶位不存在,就顺序的向下找到最近的一个桶放进去。
这样,增加一个桶,只是将一个虚拟桶变成真实桶,减少一个桶,则是将一个真实桶变成虚拟桶。
21. CAP原则
consistency:一致性,分布式的数据备份能够保证写完之后的读都相同
availability:可用性,每个请求无论成功还是失败都有对应的响应
partition tolerance:分区容错性,允许有错,但系统任一部分的信息丢失不会影响系统运行
CAP原则就是最多满足CAP中的两个性质。
由于P是必须存在的(不可能存在某个系统不会发生错误),因此就剩下C和A进行抉择。
C与A的冲突
一致性与可用性冲突最简单的例子就是,网络故障(即满足分区容错),网络出现了故障,那么满足可用性则必定无法满足一致性(无法通过网络更新已经被修改、分布在各处的数据),满足一致性则必定不满足可用性(等待网络修复后(暂停服务),用户才可以查看到正确的数据)
22. mysql核心知识点
基础范式
后续范式的基础条件是满足前面的范式;
第一范式:数据项不可拆分
(比如某项是“学生信息”)
第二范式:每个非主属性都依赖于码
(比如学生表中出现了系主任名称,系主任名称并不依赖于学生ID,如果把某个学生删除,会导致系主任也被删除)
第三范式:每个非主属性之间相互独立
(比如学生表中包含了系主任表,想以此表达学生归哪个老师管)
基本术语
DML:data manipulation language(插入更新删除)
DDL:data define language(定义)
DQL:data query language(查询)
TCL:transaction control language
执行流程
基本组件
连接器、查询缓存(8已废弃,失效频繁,命中率太低,建议在客户端实现)、分析器、优化器、执行器
查询流程
建立连接
查询缓存(如有则返回,结束)
词法分析语法分析
优化,生成执行计划,索引选择
执行器执行(访问存储引擎接口获取数据),返回结果
更新流程(删除、插入)
先走查询流程
找到对应的数据行
在buffer pool更新数据行
写入redo log(prepare)、undolog、bin log
redo log commit,更新完成
存储引擎将buffer pool更新数据到内存(刷脏)
redolog保证在刷脏过程中断电后的恢复工作,
ACID
ACID是描述事务的特点。
A:原子性:一个事务要么全部成功、要么全部失败
C:一致性:在事务执行的前后,数据库保证数据一致性,不能因为事务的中断导致数据前后不一
I:隔离性:在并发环境下,并发的事务之间不会相互影响
D:持久性:提交成功的事务描述的数据状态一定能永久保存到数据库中(比如执行过程中断电,当恢复电力的时候,会从事务日志中继续完成未执行完毕的事务)
针对隔离性提出的隔离级别:
- 读未提交:事务之间能够脏读(事务A未提交的数据,事务B能看到变化)
- 读已提交:事务之间不可重复读(只有事务A完成的数据,事务B才能看到变化。当发生修改或删除时,重复读可能导致数据不一致,比如第二次读的时候被其他事务修改或删除了)
- 可重复读:当发生增加时,可能发生幻读,比如A第一次读取了3条数据(还未提交事务),然后B增加了一条数据主键id=4,A重复读的时候依旧是3条(因为保证重复读的一致性),但是如果此时A插入一条主键id=4,会报错。mysql的默认级别
- 串行化:事务排队执行
读未提交解决丢失更新(其实是锁阻塞),但是会有脏读
读已提交解决脏读,但是不可重复读,
可重复读解决不可重复读,但是幻读。
为什么默认RR?
历史遗留问题,以前mysql基于binlog实现主从复制,如果使用RC会出现数据不一致的情况,因此默认RR
幻读模拟:
A开启事务
A全表搜索有3条数据 B开启事务
B插入新数据
A再次全表搜索依旧是3条数据(MVCC保证了可重复读),但是插入数据,缺报主键重复(A看不到新的数据,但是实际存在一个新的数据)
加间隙锁就可以防止幻读(因为插入会被阻塞)
MVCC
MVCC:多版本并发控制。
在数据行中,还有两个隐藏列,一个是事务id (trx_id),一个是指向undo日志中上一版本的指针地址roll_pointer。
对数据行的修改,会在日志中形成一个类似单链表的结构,保存着数据行的所有版本。
通过MVCC,实现不同隔离级别读到不同的数据
比如读未提交,事务总是读到最新数据(当前读);
读已提交,事务会通过指针地址读到上一个提交过的记录上,如果有多次不同的提交那么总会读取到最新的一次,因此不可重复读(快照读);
可重复读,事务总是读到第一次读到的那个(快照读)
数据安全
日志
- undolog:记录了修改的反操作(比如之前是插入xx,则记录删除xx),保证回滚,MVVC的版本也是靠他,保证了原子性。
- redo:修改磁盘数据的时候,需要在一个buffer区域中操作,如果此时崩溃,就需要记录修改后的值,因此redolog记录了数据修改之后的值,保证数据一致性和持久性(之所以不直接操作磁盘,显然是因为性能问题)。redolog采取循环记录的模式,因此循环到头的时候,需要确保头部数据已完成刷盘。
- binlog:归档日志
执行顺序:
undolog–>redo prepare -->binlog -->redo commit
其中undo和redo在引擎中执行,binlog在server层执行
其他日志:error、slowquery等
crash-safe
mysql保障数据安全的基本功能有两个,一个是按时间点恢复数据库(binlog),一个是故障发生的时候,重启之后提交的记录不丢失(crash-safe,依赖redolog和undolog)
WAL:write ahead log,日志先行,写操作之前,先记录日志。
因此日志的开启需要视情况而定,比如binlog主要是用于按时间点恢复数据库(可选),crash-safe则必须保证。
两阶段提交、组提交以及数据恢复
在数据恢复的时候,有三个日志可选,如何选择日志进行恢复?
首先需要考虑日志的记录情况,也就是落盘情况。
两阶段提交就是为了解决redolog和binlog落盘顺序问题。
第一阶段:准备阶段 redolog prepare redolog落盘
第二阶段:提交阶段 binlog落盘, redolog commit
如果redolog落盘但binlog未落盘,执行undolog回滚
如果binlog落盘了,则认为数据未丢失
由于每个事务都需要保障落盘,因此存在性能瓶颈,提出组提交。
组提交就是指多个事务同时落盘redolog和binlog
索引
索引是排好序的、可快速查找的、数据结构。
排好序:影响插入和删除(逻辑删除的好处之一就是能够避免这种现象)
索引的分类:
- 单值索引:一个索引包含单个字段(一个表尽量不超过5个)
- 唯一索引:索引值必须唯一,可以为空
- 复合索引:一个索引包含多个字段
什么情况下建立索引:
- 主键自动建立唯一索引()
- 频繁作为查询条件的字段建立索引
- 外键建立索引
- 高并发下组合索引更好,而且组合索引更符合实际情况(索引的建立应该满足用户搜索需求,而用户的搜索,往往是多个字段条件)
- 查询中排序的字段,加上索引能够大大提高排序速度
- 查询中需要统计(分组)的字段(因为统计分组order by必先排序)
什么情况下不要创建索引:
- where条件用不到的字段
- 数十上百万记录,才有建立索引的价值
- 经常发生数据变动的表不应该建立
- 如果某个数据列包含很多重复内容,则索引基本没啥实际效果(数据的差异率不高)
索引效率计算:差异值/值总数=索引效率,越接近1越好
聚集索引与非聚集索引
- 聚集索引:找到聚集索引就相当于找到了数据,因为聚集索引与物理地址一一对应,一个表一个聚集索引
- 非聚集索引:索引结果存放的是数据的物理地址(或者主键),其实是二级索引,比如(id,name)这个表,id是主键肯定是聚集索引,即便给name加上了索引,然后查询条件是name,最终找到实际存放地,是通过name–>id–>物理存储地址实现的。
联合索引最左匹配原则
对于联合索引(a,b,c),在b树索引中,是按联合索引的顺序创建的b树,如果查询条件中不包含a,那么将无法使用索引。
而且如果存在范围查询,会导致后面的无效(比如 where a>10 && b=1 && c=1),只有a>10才会生效,因为找到a>10的索引范围之后,对于b=1和c=1来说,不满足最左匹配原则(a=?并不知道)。
索引覆盖
结果集只包含索引列
索引失效
- 条件使用函数或者表达式
- 条件包含or (使用union all 代替or)
- like以%开头且结果集不仅仅包含索引列(比如索引列是name,结果集要求id,name,查询条件是name like ‘%xx’ 不会走索引),如果只包含索引列,则会走索引
- 发生类型转换(比如列类型是字符串,但是查询的时候没有加,如 where type=10)
- 不符合最左匹配原则(第一个查询条件不是联合索引的第一个索引)
explain告知走了索引,但依旧全表扫描
索引失效(比如发生类型转换,走了索引,但是其实是类型转换后进行全表扫描)
慢查询
开启slowquery.log可以查看慢查询日志
导致慢查询的原因:
1.数据库层:索引失效、select *、sql未优化
2.业务层:数据解析或者数据处理逻辑延时高
3.前端:同上
4.其他:网络问题
解决办法:
xx层:合理xxx
业务层:缓存
limit慢查询
limit 100000,20 意思是扫描100020条数据,然后扔掉100000条,保留20条。前面的过程非常慢。
解决办法:覆盖索引、子查询优化
https://zhuanlan.zhihu.com/p/246125350
B树
- 二叉查找树:提高查询效率
- 平衡二叉树:降低树的高度,进一步提高查询效率
- B树:进一步降低树的高度,每个节点包含 KV和多个子节点指针
- B+树:将数据只保存在叶子节点上,降低了IO消耗(非叶子节点只保存K和指针,省去了data,节点内存变小了),复杂度是log(m)(N),m为叉树,N为关键字的个数
使用B+树的关键点还是在于基于IO的考量
锁
以下默认在InnoDB的RR模式下。
先说结论
以InnoDB为例。
- InnoDB的锁的对象其实是索引,如果查询对象没有索引,那么会升级为表锁。
- update、delete、insert都会加写锁,select默认不加锁,可以使用for update加写锁,for share加读锁。
- RR下读写锁都会加上next key lock(默认),next key lock=gap lock+record lock,gap lock范围与操作行相关的最小间隙(如果是等值条件,则是等值前后两个间隙,如果是范围,则锁定范围所有间隙(但并不是锁表)),都是为了避免插入,解决幻读
- 读写锁都支持其他事务读,区别在于排他性不同,加了读锁,只是告知其他事务,我需要在事务期间使用这个值,要保证这个值不发生变化(比如使用该值去更新另一个表),其他事务可以继续加读锁。加了写锁,则不允许再加任何锁。
- 行锁操作频繁(比如范围更新),会自动升级为表锁。
- 行锁开销大,并发度高,表锁反之。
非行锁
-
全局锁
对整个库变得只读,可以加全局读锁,比如做全库逻辑备份的时候可以用。
但是可能出现以下问题:
在主库上备份,数据无法更新,业务基本停摆
在从库上备份,主库的内容无法更新到从库,造成主从延迟 -
表级锁
mysql中表级锁有两种:一种是表锁,一种是元数据锁(MLD锁)。
表锁语法:lock tables xxx read/write.
MLD锁:访问一个表的时候会自动加上,保证读写的正确性。当对一个表内容进行CRUD的时候,加MDL读锁,当对表本身操作的时候,加MDL写锁。
全局锁用于备份,表锁用于修改表结构
两段锁协议
行锁是需要的时候加上的,但并不是不需要了就立刻释放,而是等到事务结束才释放。
比如说事务中包含A,B,C三条语句,其中B语句涉及的行是需要加锁的,那么在执行到B的时候就会加上行锁,但是不会在B执行完毕就释放锁,而是执行完C事务提交后才会释放行锁。
因此加锁时间是(B->C);
如果把B放到最后,那么加锁时间就只有B本身,这样能够减少锁时间。
行锁分为共享锁和排它锁,在加锁之前,必须先获得对应的意向锁。
意向锁之间是兼容的,其他情况只要出现了“排它”,都会发生冲突。
死锁
发生死锁后,InnoDB通过将持有最少行级排它锁的事务进行回滚。
死锁案例:
A对x行加读锁,B对x行加写锁(阻塞),A对x行加写锁(造成死锁)。
A对x行加读锁后,相当于A拿到了x的读锁,
然后B对x行加写锁,相当于B拿到了写锁但是需要等待A的读锁,
然后A对x行加写锁,就需要等待B的写锁,造成环路等待。
此时InnoDB会回滚A,并告知需要重新开始事务,B会成功拿到读锁。
行锁遵循请求和保持,且满足其他三个死锁条件,因此可能发生死锁。
而表锁不遵循请求和保持,即要么获得全部锁,要么一个也不持有全部等待。
乐观锁
假设不会发生并发冲突,只在提交数据时检查是否违反数据完整性。
每次拿数据的时候认为别人不会修改,但是在更新的时候会检查一下在此期间有没有人更新这个数据,可以使用版本号机制。
适合多读业务,能够提高并发度。
悲观锁
认为每次都发生了并发冲突,每次拿数据都上锁。
适合常写业务。
引擎
InnoDB和MyISAM
InnoDB是mysql默认引擎,支持行锁、外键、事务,而MyISAM不支持。
MyISAM会在拿到所有锁后执行语句,因此不会发生死锁。
InnoDB使用的是聚簇索引(找到索引就相当于找到了数据),而MyISAM是非聚簇索引(数据在磁盘中随机存放,还需要通过索引找到物理存储位置)
MyISAM适合大文件存储,支持全文索引。而InnoDB的事务机制,保证了ACID,适合并发,可靠性更高。
iMyISAM在查询的时候所做的维护工作远少于InnoDB(比如MVVC机制、),因此查询速度比InnoDB快。但在并发情况下,MyISAM是b的是表锁,因此效率远低于InnoDB。
23. Spring要点
Spring的核心:IOC、AOP、声明式事务;
为什么要使用IOC和DI
IOC:控制反转
DI:依赖注入
要解释控制反转,需要先考虑设计模式六大原则之一的“依赖倒置原则”。
依赖倒置原则,是指设计代码结构时,高层模块不应该依赖低层模块,二者都应该依赖其抽象。
假设汽车需要组件轮胎,那么下面是其依赖的方式:在Car中直接new出对应的Tire对象,称为Car直接依赖Tire对象
class Car {
TireOne tire;
Car() {
this.tire = new TireOne();
}
}
class TireOne {
private int size;
}
但是如果现在轮胎进行了改变,那么Car类也要进行相应的变动,这显然不合理;或者Car需要更换其他的轮胎,也无法直接实现。
依赖倒置,使Car依赖其抽象
class Car {
Tire tire;
Car(Tire tire) {
this.tire = tire;
}
}
interface Tire{
}
class TireOne implements Tire {
private int size;
}
这样,我们需要什么轮胎,就放入什么轮胎即可,这个放入的过程,就是DI 依赖注入,DI可以通过构造器,也可以通过setter方法。
但是要实现复杂类群体的初始化,可能涉及到十分复杂而且有顺序要求的依赖注入,这个操作其实很复杂,而控制反转,就是为了解决这个问题。
BeanFactory和ApplicationContext对比
BeanFactory 采取懒加载机制
ApplicationContext是对BeanFactory扩展,提供了更多功能,比如国际化处理。
加载Bean的方式
- 调用构造器(也就是最基本的配置)
- 静态工厂方法(在配置中指定工厂类的静态方法)
- 实例工厂方法(在配置中指定一个工厂类以及对应的工厂方法)
Bean注入属性的方式
构造器注入和set方法
Bean的创建方式(作用域)
- 单例
- 多例
- request:每个请求创建一个Bean
- session:同一个session使用同一个Bean
- globalSession:不知道(官方说用于portlet环境)
Bean的生命周期
详细版本
- instantiate bean对象实例化
执行静态块和空构造方法 - populate properties 封装属性
执行set方法 - 如果Bean实现BeanNameAware 执行 setBeanName
设置名字 - 如果Bean实现BeanFactoryAware 执行setBeanFactory
获取Spring容器 - 如果存在类实现 BeanPostProcessor ,执行postProcessBeforeInitialization
在初始化前执行 - 如果Bean实现InitializingBean 执行 afterPropertiesSet
在属性设置后执行 - 调用<bean init-method=“init”> 指定初始化方法 init
自定义初始化 - 如果存在类实现 BeanPostProcessor(处理Bean) ,执行postProcessAfterInitialization
初始化后执行 - 业务处理
- 如果Bean实现 DisposableBean 执行 destroy
- 调用指定销毁方法 customerDestroy
自定义销毁
简单版本
- 执行静态块和构造方法
- 执行set,属性设置
- 命名
- 设置Bean工厂
- 执行 “初始化前执行”
- 执行“属性设置后执行”
- 执行自定义init方法
- 执行“初始化后执行”
- 业务处理
- 执行自有销毁方法
- 执行自定义销毁方法
极简版本:
1.静态块收集
2.属性注入
3.初始化
4.业务处理
5.销毁
其中每个方法前后都有对应的钩子方法方便我们在创建过程中实现自定义
AOP
主要应用:事务、日志、权限安全、性能监控
实现原理:通过动态代理,在方法的前后植入代码;
JDK动态代理要求代理类必须实现了接口,而且代理编译时结果也是用接口接收。因为JDK动态代理实际上是创建了一个被代理类的兄弟类,兄弟类之间不能实现互转。
CGLib代理则不要求类必须有接口,而且效率比JDK动态代理更高。
通知点:前置、后置、环绕、异常、最终通知
事务
事务管理
Spring实际使用AOP来实现事务,本质上有个TransactionMananger,Spring不做具体实现,而是注入各个数据库平台各自的事务管理器,通过AOP将业务逻辑加入到事务中。
事务传播
事务传播是一个常见的业务行为,必须定义事务传播机制,来保证发生这些行为之后,有对应的处理办法
当A方法有事务,其内又调用了B方法,此时需要给B定义事务传播机制。
一共有七种,主要掌握:
- request:B必须要有事务(要么复用前面的,要么自己创新的)
- request-new:B必须要有自己的新事务(不管前面有没有,自己都创建一个新的)
- nested:外围事务回滚会带着内部事务一起一起回滚,内部事务自己回滚不影响外部
Spring IOC原理
Spring的容器就是ApplicationContext,这是一个顶级接口,其下有多个实现,比如ClassPathXMLApplicationContext就是扫描xml文件进行Bean的IOC,AnnotationConfigApplicationContext则是扫描对应注解的形式。
ApplicationContext继承了BeanFactory接口,因此它本身也是一个Bean工厂
Hierarchical:分等级的–> 可以启动多个分等级的Bean工厂
Listable:可列表化的–>可以同时获取多个Bean(BeanFactory是针对单个Bean)
在context中,核心方法就是refresh方法,用于初始化Bean容器(之所以不用init,是因为refresh支持重建容器,因此包含一些销毁已有资源的方法)
在refresh中,除了扫描对应注解,初始化相关Bean放入容器中以外,还需要检查用户是否实现了某些钩子方法(就是生命周期中对应的方法),然后逐一执行。
refresh会初始化所有除去懒加载的单例Bean。
Spring如何处理线程安全
Spring默认Bean都是单例,显然容易出现并发问题,但是其实只需要保证Bean无状态即可,所以避免单例Bean中使用成员变量,或者用ThreadLocal保存可能发生并发问题的变量。
ThreadLocal比线程同步机制实现简单,秉承空间换时间的思想。
Spring MVC流程
24. mybatis要点
执行逻辑
// 1.读取mybatis配置文件 ----- 通过类加载器获取
InputStream inputStream = Test.class.getClassLoader().getResourceAsStream("mybatis-config.xml");
// 2. 获取Sqlsession工厂
// 解析配置文件,生成Configuration对象,利用Configuration创建DefaultSqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 3. 打开sqlsession
// 从Configuration中获取defaultExecutorType,并通过Configuration创建Executor,
// 然后使用Configuration和Executor创建SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
// 4. 获取mapper接口对象
// 利用Configuration.getMapper(Class,Sqlsession)获取,其内调用的MapperRegistry获取,
// 最里面是用的jdk动态代理获得对应Mapper接口的代理对象
BlogMapper blogMapper = sqlSession.getMapper(BlogMapper.class);
// 5. 调用代理对象mapper进行查询
Blog resBlog = blogMapper.selectOne();
// 6. 业务处理
System.out.println(resBlog.toString());
关键对象:
- mybatis-config.xml:配置mybatis的所有信息,比如数据库连接、缓存、事务
- Configuration:解析mybatis-config.xml得到的对象
a) 功能:包含mybatis-comfig.xml文件中的所有配置信息 - SqlSessionFactory:解析Configuration,创建SqlSessionFactory
a) 持有Configuration - SqlSession:SqlSessionFactory利用Configuration、Executor、autoCommit参数创建
a) 创建SqlSession之前先创建了Executor:SqlSessionFactory利用Configuration创建Executor(并不是工厂创建的)
i. 持有一个装饰对象delegate(用于处理二级缓存)和事务管理器
b) 持有Configuration和Executor - xxxMapper:jdk动态代理对象,由Configuration根据xxxMapper的Class对象以及SqlSession进行创建(从这一步可以猜出,数据库操作由SqlSession发起,而SqlSession持有Executor,因此真正的数据库操作由Executor执行)
sql的创建(MappedStateMent对象),SqlSession将拿到的xxxMapper.方法名作为key传递给Configuration,Configuration调用
this.mappedStatements是一个Map(在Configuration创建过程中就通过MapperRegistry将mappedStatements装载到Configuration对象中了)
实现一个持久层框架的核心在于(从数据库往应用层):sql语句解析、参数映射、sql执行、结果集处理
执行器与缓存
概述:
一级缓存:
存在一个BaseExecutor,有一个query方法,能够执行MappedStatement ,在执行之前,会创建缓存,若用SimpleExecutor的doQuery方法,则不会创建缓存
二级缓存:
有个CacheExecutor,专门处理需要二级缓存的情况,他持有了一个delegate 执行器(委派对象),实际的query由委派对象完成,在delegate.query之前,会在缓存里面获取。
一级缓存是会话级别的,二级缓存是跨线程的。
SimpleExecutor 源码演示
对象准备
void before() throws SQLException {
// 调试基础类:Configuration和Transaction
final InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("mybatis-config.xml");
SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
final SqlSessionFactory sessionFactory = factoryBuilder.build(resourceAsStream);
configuration = sessionFactory.getConfiguration();
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/blog?serverTimezone=Hongkong","root","123456");
transaction = new JdbcTransaction(connection);
}
@Test
void test() throws SQLException {
// before注解失效了,我这里直接手动调用
before();
Executor simpleExecutor = new SimpleExecutor(configuration, transaction);
MappedStatement mappedStatement = configuration.getMappedStatement("com.ddw.mapper.BlogMapper.selectOne");
List<Object> query = simpleExecutor.query(mappedStatement,null, RowBounds.DEFAULT,SimpleExecutor.NO_RESULT_HANDLER);
System.out.println(query.get(0));
}
SimpleExecutor本身的doQuery没有缓存机制,但是如果用其父类BaseExecutor去接收SimpleExecutor对象,然后调用query方法,则会有一级缓存。
原理:BaseExecutor抽象了缓存处理逻辑(query),同时定义了子类不使用缓存的逻辑抽象(doQuery);如果我们直接使用子类的doQuery,则不会进行缓存,如果我们使用BaseExecutor的query,则他会先处理缓存,然后再调抽象方法doQuery(实际会去调子类实现)。
query相当于一个包装方法,对doQuery方法外面包了一层处理缓存的逻辑。
其他Executor原理类似。
一般情况下,我们没有直接使用Executor,而是通过SqlSession来操作。SqlSession作为门面,降低了使用复杂度。
缓存
可以看出,缓存只有一个核心实现,PerpetualCache,其内特别简单,就是一个HashMap的操作。
但是他有非常多的装饰器,比如LruCache(最少最近未使用)、FifoCache(先进先出)
一级缓存默认开启,是会话级的,但是sqlSession只持有了配置类对象和执行器对象,并没有持有缓存对象,因此缓存对象肯定在执行器中。
为什么说一级缓存是会话级的?
因为缓存对象在执行器中,而执行器被sqlSession持有,mybatis对于跟数据库的每次会话都会创建一个sqlSession,因此缓存对象是会话级的。
由于一级缓存是会话级的,因此跨会话访问同一个数据,可能出现不一致的问题。
上图是一级缓存实现原理,执行器直接与client接触。
下图是二级缓存实现原理,由于他面向所有SqlSession的Executor,命名空间级别,只要是同一个接口里面的相同方法,都可以共享。
查询顺序显然是二级–>一级–>数据库
其他(还没学完)
架构图-类图
这个作者对mybatis的一系列解读值得慢慢阅读。
四大过程(sql解析、参数映射、sql执行、结果集处理)都有一个interceptorChain.pluginAll(parameterHandler);
25. Git要点
主要分为三个区:
- 工作区:在这编码
- 本地仓库:add、commit
- 远程仓库:push
通过主干和分支的灵活切换、融合,实现版本迭代。
26. SpringBoot要点
自动配置
SpringBoot的核心在于自动配置,通过扫描META-INFO/spring.factories,实现各个组件的自动配置
优势
SpringBoot的核心优势并非脚手架的功能,而是
- Spring社区的支持
- 开源
- 庞大的用户群,非常多人去试错,遇到问题能够快速找到解决办法
profile
不同环境(测试、开发、生产)下加载不同的配置
通过 配置yml 可以实现
可以通过yml的 spring.profiles.active=dev设置激活,或者在启动的时候指定,或者用虚拟机参数指定。
27. 设计模式
目标:高内聚、低耦合、易扩展
六大原则
- 单一 职责原则
- 开闭原则,对修改关闭,对扩展开放
- 依赖倒置原则,使用接口来接收其子类的初始化(List a = ArrayList(),而非ArrayList a = ArrayList())
- 接口隔离原则,实现接口的类应该觉得没有多余的方法
- 里式替换原则,引用父类的地方,必须可以透明的使用其子类的对象(少用继承,当修改父类方法的时候,不得不考虑所有子类的实现)
- 迪米特法则,减少类的依赖,降低耦合
具体模式请参考菜鸟教程
28. 泛型
如果不用泛型,只能使用Object标识未知的对象,但是这要求程序员记住所有Object所指代的类别,不然会出现转型错误。
通过泛型(其实本质还是Object),能够在编译时统一元素类型,保证类型安全。
比如一个方法是操作Session的,Object实际是Session,只是我们不知道是具体哪个Session,但是我们也得记住这里只能放Session。但是用泛型,我们直接写个标识符表示就行(甚至直接命名泛型的标识符为Session)
泛型类型不支持基本类型。
同一个泛型类,不同的泛型参数,本质上还是同一类型=>泛型类本身
// intGeneric是<Integer>,strGeneric是<String>
System.out.println(intGeneric.getClass()==strGenenric.getClass()) // true
// intGeneric.getClass() ==>Generic
泛型不支持重载和多态
子类也是泛型类,则子类泛型标识至少要包含父类的泛型标识
class child<T,E,K> extends father<T>{}
子类不是泛型类,则父类的泛型类型需要显式声明
class child extends father<String>{}
此时子类使用父类的方法,方法的返回值都会变成明确的类型
泛型只在编译前有效,编译后被擦除
- 若仅仅无类型泛型,则擦除为Object A<T>
- 若有上限,则擦除为上限 A<T extends B> T编译后变成B
- 父类无类型,子类指定了类型,则会在子类中生成桥接方法,保持接口和类的实现关系,如下图:
29. Redis要点
Redis是基于键值对的NoSQL 内存 数据库,利用快照和日志的形式进行数据持久化。
特点:
- 单线程架构,避免了多线程竞争
- C语言实现
- 内存数据库(所以要考虑内存占用问题)
- 相对简单稳定
- 提供主从复制
应用:
- 缓存
- 排行榜
- 计数器
- 消息队列
- 共享session
- 限速(比如限制登录,key=“limit:uid”,value=“登录次数”,存活时间=“60s”,如果从redis中拿不到这个key的value或者value的值小于阈值,则通过)
数据结构与内部编码
五大数据结构:字符串(最大512MB,key:string)、哈希(key:{key:value})、列表(key:[a,b,c])、集合、有序集合
五种数据结构在redis内部其实会分情况的映射为不同结构以提高性能(类似HashMap)。
单线程架构
多端的命令,会先进入一个执行队列,然后排队完成。
redis使用单线程依旧很快,原因在于三点:1. 内存的高速、 2. 使用epoll作为IO多路复用的实现 、 3. 单线程避免了线程切换开销和竞争开销
redis中有批处理命令,能够将多次网络IO转为1次网络IO(但是依旧串行执行)
由于其单线程的架构,用在高并发场景下,就可能出现问题,可以使用集群,构建合理的拓扑,读写分离,合理配置复制相关的参数,来应用在高并发场景下。
持久化
提供rdb和aof两种方式。
rdb基于快照进行持久化,适合全量持久化,由于基于快照,做不到实时持久化,如果无法接受十几分钟的数据丢失,最好是用AOF
rdb最重要的优点就是对性能影响较小。
rdb可以使用save和bgsave命令开始,save会阻塞服务器进程。
aof则通过记录每一条写命令来实现几乎实时的持久化,
redis模式
单机模式
主从模式:相比单机模式提高了可用性,但是没有故障兜底方案,节点挂了需要手动修复。
主从复制方式:从服务器发送连接心跳,主服务器生成全量快照,交给从服务器,从服务器根据快照生成数据,随后主服务器会将所有写命令同步给从服务器。
哨兵:主从模式的升级版,通过额外的哨兵节点监控主从节点,哨兵直接也会互相监控,当主节点挂了,会选举一个从节点作为新的主节点。
热key问题
热点数据(秒杀信息),单机redis也扛不住,改集群,配置好一致性哈希,将请求分配到不同机器上。
大key问题
比如几十万的排行榜,一个key带着这么多的数据,载入太难。
切分,将一个key拆成多个key,可以通过hash,比如以1000为一个单位放在一个key中。
缓存
优点:加速读写、降低后端(数据库)压力
缺点:数据不一致风险,增加运维成本
应用场景:本质是加速查询,可以体现到两个方面,一个是加速响应,一个是避免开销大的计算。
更新策略:
- 随机删除(还可以只针对设置了过期时间的key进行随机删除,下同)
- 最近最少使用LRU
- 最近最不常用LFU
- 先入先出
- 超时
- 主动更新
缓存穿透:通过重复查询不存在的数据,导致数据库压力过大。
例子:发起无数次查询:查询id为-1的用户 --> 先到缓存中查询,不存在 --> 再到数据库查询 --> 使得数据库承受无数次查询压力。
解决方案:
1.将不存在的key也缓存起来
2.布隆过滤器
缓存击穿:缓存中没有,但是数据库有,当并发量较大的时候,瞬间大批量查询数据库,导致压力较大。
解决方案:
1.高并发查询同一个数据,一般是因为该数据是热点数据,因此设置热点数据永不过期。
2.缓存预热
缓存预热:一开始就将数据库的所有数据存为缓存,用户只允许查询缓存;如果缓存中没有,则说明没有,不必查询数据库(也能防止缓存穿透);当且仅当数据发生变更,再统一更新数据库与缓存。
实现方法:
1.存:实现InitializingBean接口,会在启动时调用(或者其他项目初始化方法),在其中查询数据库,并将数据加到缓存中。
2.取:查询在缓存中查,若不存在,则返回为空
3.改:修改数据后,更新对应缓存
缓存雪崩:“雪崩”,顾名思义即缓存崩溃;当缓存的过期时间接近导致同一时间大批量缓存过期,瞬间使得缓存类似失效的状态,且数据库在此时接收大批查询,压力大。
解决方案:
如果是缓存大面积过期,解决如下:
1.过期时间设置随机(或其他方案),避免同一时间过多缓存过期。
2.热点数据不过期
3.缓存预热
否则:
事前:设置合适的淘汰策略,尽量保证redis集群的稳定性
事中:增设本地缓存,服务降级限流
事后:利用redis持久化机制尽快恢复缓存
redis如何实现key的过期删除?
定期检查:隔一段时间从设置了过期的key中抽取一部分,检查是否过期,过期则删除。
惰性删除:获取key的时候检查是否过期,过期则删除。
为什么要两种删除方式:
假设只有定期检查,当检查到有大量对象需要删除,那么会导致redis疲于删除。
假设只有惰性删除,会导致某些不被访问的key永驻内存。
redis实现消息队列
redis自带发布订阅命令,publish和subscribe,
或者使用List模拟队列存储消息,lpush发送,rpop接收
30. 第三方接口
顺便说一下,调用第三方的某些接口(比如支付),在页面设计上需要满足他们设定的规范,比如logo大小之类的。
支付
以支付宝支付为例
支付宝申请正式服务:
进入支付宝手机端开发者中心,获取能力。
进入网站管理中心,管理能力。
添加应用,填写资料。
设置开发信息:
接口加密方式,使用支付宝平台开放助手,生成公钥密钥。然后将公钥粘贴到填入栏,保存。
沙箱环境
https://openhome.alipay.com/platform/appDaily.htm
然后填入支付宝平台开放助手生成的公钥即可。
支付宝支付官方流程图
简化说明:
1. 用户使用支付宝支付,
2. 服务端生成预支付订单,向支付平台提交订单信息,获取签名返回给前端
3. 前端使用该签名发起支付,完成支付,支付宝会调用配置好的回调地址
4. 回调地址中处理支付成功的逻辑
微信支付流程差不多,但是支付参数校验比较麻烦。
涉及到退款、商户转账,需要配置CA证书。(CA:证书机构)
登录
以支付宝登录为例
简单描述:
1. 用户点击使用第三方登录,获得授权
2. 授权返回auth_code,这个code交给后端
3. 后端使用auth_code获取相应的token和uid
4. 利用token和uid获取用户信息
微信登录与其类似。
OSS/OBS
不过阿里云接口不太友好,我最后选的是华为云OBS,看完上述视频配哪个云都可以。
31. 微服务要点
提前总结:
微服务就是将单体架构拆分成多个小的服务,各个服务直接独立部署,使用轻量通信机制,这些服务相互独立,可以是跨语言的,并保持最低限度的集中管理。微服务也带来了很多必要的需求,比如服务注册与发现、服务调用、服务降级、服务限流、服务熔断、服务配置、服务总线、分布式事务、链路监控等。
微服务技术栈:
或者直接使用一套SpringCloud alibaba
服务之间的通信
各个服务被拆分,首先考虑通信问题。
服务于服务之间可视为服务消费者与服务提供者;
提供者提供相应的接口(比如HTTP REST接口),消费者使用RestTemplate进行访问。
上述是最简单的微服务,但仅支持一对一供给。
若出现多个消费者、多个提供者,就涉及到接口的匹配、负载均衡、容错等问题,统称为服务治理。
服务之间的注册与发现
抽取出一个服务注册中心,所有的消费者和提供者都在其上注册,然后将服务治理(负载均衡)交给注册中心来管理。
注册中心本身也是一个服务,因此也可能崩溃,可以为其搭建集群环境。
服务容错
- 扇出: A调用B和C,B和C又调用其他,扇形扩散
- 服务雪崩:若在扇出过程中某个节点卡主,导致所有参与扇出的节点都卡主
- 服务降级:当服务器压力剧增的时候,将一些不重要的服务进行延迟调用或者临时停止,降级发生在服务端宕机后,一般由消费端实现。
- 服务熔断:扇出发生错误时,对对应服务降级,进而熔断,快速响应错误消息。实际上存在一个虚拟的熔断器,当n秒内错误率达到阈值时,熔断器打开,任何请求都直接返回错误信息。当n秒过去后,熔断器半开,尝试放过请求,若请求成功,则判定服务正常,熔断器关闭。熔断是一种自我保护机制,在服务端实现。
- 服务限流:限定访问个数,多余的排队等待。
路由网关
网关可以实现反向代理、负载均衡、路由、过滤、鉴权、流量控制、熔断、日志监控等。
是对外的统一访问入口。
配置与消息
将所有微服务的配置统一管理起来,比如在git仓库中的某个文件,然后修改该配置文件,能够实时提现到微服务上。
微服务中消息中间件必不可少,比如使用SpringCloud bus作为消息总线。
链路监控
在微服务中,一个请求可能涉及到多个服务,服务之间还可能扇出,任何一个节点的延迟或错误都可能造成崩溃,因此需要监控调用链路。
可以使用springcloud sleuth。
分布式带来的问题
分布式锁
对于集群共享的一个数据,需要加上分布式锁,比如红包奖金池,不能出现超额分发。
分布式锁肯定需要独立于所有服务之外,所有服务需要去抢到这个锁才可以执行后续逻辑。
可以使用redis或者zookeeper。
利用redis单线程的优势:
使用setnx,setnx,设置值,如果不存在则成功,否则失败。
让需要抢锁的服务去setnx key value。
如果成功了,则说明抢到锁,然后执行后续逻辑,如果失败了,则说明没有抢到。
后续逻辑执行完毕,还需要删除该值,表示释放锁。
如果节点在后续逻辑中挂了,未能删除该值,就造成了死锁,所以可以给该值设定一个超时时间。
利用zookeeper文件系统不可重名的特性:
谁先创建对应的文件节点,谁就抢到了锁,然后执行后续逻辑,其他节点只能监听该文件的变化。
当文件发生变化,说明锁被释放,然后又开始抢锁。
为了避免死锁(同样是处理后续逻辑的时候挂了,没能删除节点,其他节点都死锁了),可以使用临时性节点,如果服务挂了,那么该节点就会被删除。
还可以使用顺序临时性节点,让每个服务监听前一个节点的变化,避免惊群效应(一个节点变化,一堆服务冲上去)
分布式事务
两阶段提交模型:包含两类角色,一个是协调者,一个是参与者,第一阶段是所有参与者告知可以进行事务操作(prepare阶段),然后协调者通知所有参与者提交事务(commit阶段)。
缺点:协调者单点故障、参与者同步阻塞、协调者通知参与者commit的时候,如果部分节点未收到,会导致数据不一致
三阶段提交模型:引入超时机制,第一阶段,协调者询问参与者是否可执行(can-commit),接着通知所有节点进入pre-commit(执行事务,但未提交),最后所有节点do-commit提交事务。
每个阶段执行完毕后都需要向协调者提交执行反馈,任意阶段出现了超时或者某节点表示不参与,都会回滚。
以seata为例
1+3模型:一个全局唯一事务id:XID
三组件:
TC:事务协调器,维护全局事务状态,驱动全局事务回滚或提交
TM:事务管理器,定义全局事务范围,开启、提交、回滚事务
RM:资源管理器,管理分支事务处理,驱动分支事务回滚、提交
流程:
1.事务管理器向事务协调器申请开启全局事务,事务协调器返回全局事务XID
2.XID在微服务调用链路中传播
3.资源管理器向事务协调器注册分支事务,纳入对应的XID中
4.事务管理器向事务协调器提出全局提交或者全局回滚
5.事务协调器调度XID对应的所有分支事务完成提交或回滚。
32. 秒杀系统
秒杀系统的关键在于解决“微量的库存与大量请求的冲突”。
完美的方案就是,n个库存对应n个请求。
所以关键在于,如何过滤无效请求?
分为4层:浏览过程(浏览浏览器)、请求过程(发起请求)、服务过程(处理请求)、数据库过程(修改数据库)。
浏览过程优化
普通用户的典型行为就是狂点“抢购按钮”,因此,可以在页面进行控制,比如点击一次页面置灰(或者不置灰,但是仅第一次点击生效)。
并做好页面缓存。
请求过程优化
有的人可能通过接口直接访问,那么按钮控制会无效。
做好uid 的访问频度限制、限流,并做好负载均衡。
服务过程优化
首先是流量削峰,采用消息队列,服务端根据自己的QPS拉取请求进行处理。
对于读请求,走缓存,对于写请求,走队列。然后从队列中拿出库存个数个的操作。
数据库过程优化
写请求很少,锁机制保证数据一致性即可。
超卖问题解决:更新库存-1之前,加上库存必须大于0的条件。(更新操作会自动加行锁,这样能够保证库存不会变成负数)
上图是一个简单的秒杀流程,现在模拟一下。
40万个用户蹲守10个商品
- 浏览过程优化:尽量保证只有40万个有效请求通过,运行少数重复请求
- 请求过程优化:40万个请求,负载均衡到20个服务站点,每个服务站点2万请求。
- 服务过程优化:流量削峰(比较极端的例子就是消息队列只放行库存个请求)
- 具体服务过程:
1.检查Redis数据,判断是否满足抢购条件(比如只允许抢购一次(在此处被放行一次)),由于Redis的单线程机制(即操作原子性),即时通过n个同一个用户请求,也能够拒绝后续的重复请求。
2.检查Redis库存,n个相同请求通过检查
3.Redis减库存(原子操作)
4.生成订单,等待支付。 - 数据库过程优化:锁机制即可
抢红包算法
公平性:
首先要保证公平性,不能出现先抢的人抢的金额大。
可以采用线段切割,当红包创建的时候,制定了红包个数N,通过这个数,在红包总额上生成随机的N-1个点(如果有重复的点就再生成一次),将红包按照这些点进行区间划分,然后当其他人来拆的时候,按顺序领走即可。
并发:
当红包创建成功后,在数据库和redis中都插入一条记录。
当用户去抢的时候,在redis进行原子减操作,直到为零。
33. 数据结构
稀疏数组
稀疏数组用于处理数组的压缩存储。
保存一个“行列值”数组即可,如下图。
思路:
保存:遍历原始二维数组,遇到非零数据的时候,创建一个节点对象,保存该节点的行列值。注意第零行保存原数据的长宽以及有效数字个数
还原:根据第一个数组的数据,创建还原数组,然后遍历赋值即可。
队列
以下需要记住
普通数组模拟队列
front和rear初始值都为-1,full判断 rear== maxsize-1
由于初始为-1.所有put/get 都是先++
普通数组模拟循环队列
front和rear初始值都为0,full判断 (rear+1)%maxsize==front,
由于初值都是0,get时先get后 front = (front+1)maxsize
put时先put后rear = (rear+1)%maxsize
有效数据个数size=(rear+maxsize-front)%maxsize
所以遍历数据的时候范围是[front,front+size],当前值为 遍历中的 i%maxsize
使用数组模拟队列,会出现假溢出(可操作的数在front和rear之间,随着头指针往后移动,可操作的空间不断变小)。
顺序存储二叉树
左子树2n+1
右子树2n+2
当前节点的根节点 (n-1)/2
二叉排序树的删除
二叉排序树(二叉查找树)的节点删除需要先找到目标target的父节点parent,且分为三种情况:
- ①删除叶子节点–>将parent对应的left/right置空
- ②删除有一个子节点的节点–>用target的子节点代替target的位置
- ③删除有两个子节点的节点–>用target右子树值的左叶子(右子树最小值)替换target的位置(或者用左子树的右叶子(左子树的最大值)替换target的位置)
红黑树
特征:
- 每个节点要么红色要么黑色
- 根节点一定是黑色
- 不允许两个红色相连(红色的子节点一定都是黑的)
- 从任意节点到其叶子节点的所有路径,都包含相同个数的黑色节点
- 叶子节点都是黑色
一定是红插(红插能够最小程度的违反特征,修复代价低)
红黑树并不是严格的平衡二叉树,它通过颜色,降低了平衡的代价,满足部分平衡。
算法常用小点:
- 使用异或提取每位特征
- “”.matches("\d+")返回是否为数字
- char ‘3’ 转为 int 3 ,(int)‘3’-‘0’
- 判断二维数组是否在同一斜线:(列差的绝对值==行差的绝对值)则在同一斜线
- 获取数字每一位上的值(n=1表示个位,2表示百位…) number / (int) Math.pow(10,n-1) % 10,或者转成字符串用string.charAt(string.length()-n)-‘0’;
- 交换的代价远远高于移位(指数级差距)
34. 科班基础
计网
七层模型:
物理层:规定物理特性,透明传输比特流
数据链路层:帧定界、无差错传送帧
网络层:路由选择、分组转发
传输层:提供端到端逻辑通信
会话层:验证访问和会话管理(RPC、SQL)
表示层:信息格式和语法的转化
应用层:为用户进程提供服务接口
基础
集线器、交换机、路由器
集线器在物理层,交换机在数据链路层,路由器在网络层。
在带宽上,集线器是共享的,交换机是独享的,比如10M带宽,集线器连接了两台设备,那么两台各分5M,如果是交换机,则表现为每台最大带宽都是10M。
集线器是广播发送(安全性较差),而交换机是有目的的发送。
(因为集线器无法记住MAC地址,而交换机如果给新设备发消息,不知道对方MAC,第一次也是广播)
路由器是根据IP地址转发数据包的。
交换机主要是连接局域网内的设备,而路由器是连接不同的网段。
简单来说,路由器在网络层负责将本局域网与外网连接,而交换机主要负责局域网内的数据交换。如果没有路由器,交换机的数据也出不去。
子网掩码
子网掩码主要是为了将ip地址分为网络地址和主机地址两部分,配合IP地址使用。主机地址全一就是子网的广播地址。
三次握手 四次挥手
建立TCP连接时需要三次握手:
- 1.客户端发送带有SYN的请求,申请建立连接
- 2.服务端返回SYN与ACK,表示通过请求
- 3.客户端发送ACK,表示收到服务端的通过请求
开始数据传输
三次握手换成两次行不行?
不行,三次握手有两个好处:
- 1.当第二次握手,服务端返回SYN和ACK的时候,服务端并不知道客户端是否收到该信息,第三次握手能够告知服务端“我收到了你的同意”
- 2.(也是关键)三次握手能够避免滞留的请求造成的错误链接。比如客户端发送了一个SYN,但这请求滞留在了某个网络节点,直到连接被释放了才到达服务器,这时候如果是两次握手,那么服务器收到该SYN便立即以为连接已经建立,开始等待资源传输,但是实际上该SYN是无效的。如果是三次握手,当服务器收到SYN后,如果等不到第三次握手,那么连接不会建立。
释放TCP连接需要四次挥手:
以主动关闭方为客户端,被动关闭方为服务端为例
1.客户端发送FIN请求,表示关闭连接
2.服务端收到该FIN,但是他还有数据要发送,于是继续发送数据(进入close-wait状态)
3.服务端发送FIN,表示关闭连接(进入last-ack状态,等待最后一个ack)
4.客户端发送ACK,进入time-wait状态,time-wait结束后客户端才会关闭连接(如果服务端收到该ACK,则服务端连接关闭,如果在规定时间内没有收到,服务端会重新发送第三次挥手)
time-wait指的是主动关闭方在最后一次握手发送ACK之后进入的状态,会等待2MSL(2倍最大报文存活时间)才真正进行关闭。
为什么需要Time-Wait?
- 1.如果客户端发送ACK之后就直接关闭,但是服务端不一定收到了该ACK,因此需要一个时间进行超时重传验证。
- 2.time-wait能够保证失效的SYN请求度过存活时间。为了避免已失效的SYN请求出现在本次连接中。比如说客户端发送了一个SYN请求,但是该请求滞留在某个网点,假设没有Time-wait,客户端直接关闭当前连接,然后又开始了新的SYN,但是这时候上一次的SYN也出现了,这就造成了混乱。
断开连接一定是四次挥手吗?
并不是,第二次和第三次可能合并(比如没有数据要发送)
每次请求都是三次握手吗?
并不是,在HTTP1.1,浏览器HTTP请求默认开启Connection:Keep-alive,并通过定时心跳告诉服务端自己还活跃,下次就可以复用该链接。
大量的time wait
time wait是在第四次挥手的时候出现在主动关闭方的,如果出现大量的tw,则说明服务器主动关闭了大量的TCP连接。
主动关闭大量TCP连接,说明之前创建了大量的TCP连接,如果不是某些异常行为,那应该是高并发导致的。
应该有tw复用的配置,或者修改tw时长的配置,不过修改tw时长不太安全。如果是HTTP请求,可以开启keepAlive,改用长连接。
大量的close wait
注意close wait是服务端回应fin ack后进入的状态
出现大量close wait,首先就是close wait可能没有设置超时时间,其次多半是服务端代码问题,比如:
客户端请求某个接口,服务端处理十分耗时(比如30s),这时候客户端等待超时(比如只等待5s),就发起关闭连接,然后服务端回应fin/ack,但是他的接口还没完成,于是卡在closewait上。客户端不可能等待30秒,因此超时自动结束了挥手状态,等服务端结束接口调用再响应fin,客户端已经不认了。
解决办法就是,要么降低接口时长,要么修改close wait超时时间。
客户端与服务端同时关闭连接
双方随后互相对FIN进行ACK确认,收到对方的ACK后,都进入tw。
SYN攻击
服务端维护着两个队列,一个是半连接队列,当收到SYN并回复对应SYN的ACK时,相当于准备连接了,因此会创建一个半连接放入半连接队列。如果三次握手完成,半连接就变成了全连接,会放入对应的全连接队列,等待对应应用程序取走。
SYN攻击通过伪造大量不存在的IP发起SYN请求,服务端会为这些SYN请求会创建大量的半连接,导致半连接队列占满,无法响应正常的请求。
解决办法,收到SYN后不建立连接,而是生成对应的一个cookie,然后将cookie与SYN确认一起返回。客户端第三次握手的时候需要带上cookie,校验通过后才分配连接。
利用该cookie可以实现TFO(TCP fast open),后续的连接,客户端可以直接发送SYN+cookie+HTTP请求,然后服务端校验过cookie后,可以立即响应HTTP请求(当然,后续的两次握手也会继续完成,但是能够预处理一次HTTP请求,积累起来就很可观)
TCP&UDP
TCP与UDP区别
七层模型:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层
五层模型:物理层、数据链路层、网络层、传输层、应用层
TCP/IP四层模型:网络访问层、网络层、传输层、应用层
使用TCP的应用层协议:HTTP、SMTP、FTP、TELNET
使用UDP的应用层协议:DNS、DHCP(动态配置IP)、电话、视频
TCP
- 传输控制协议,是面向连接的、字节流的传输协议(三次握手、四次挥手),保证可靠传输。首部20B。
- TCP和UDP标识双方的都是源端口号和目的端口号,没有IP地址,IP地址在网络层加。存在一个紧急指针发送紧急数据。
UDP
- 用户数据报协议,非连接的、面向报文的传输协议,直接将应用层传下来的报文加上首部,然后交给网络层(因此应用层要控制好报文的大小,太大则会导致网络层对其分片,太小则导致有效数据率太低),尽最大努力交付(有差错控制),不保证可靠传输。(首部8B:源端口号+目的端口号+UDP长度+UDP检验和)
1、基于连接与无连接;
2、对系统资源的要求(TCP较多,UDP少);
3、UDP程序结构较简单;
4、字节流模式与数据报模式 ;
5、TCP保证数据正确性,UDP可能丢包;
6、TCP保证数据顺序,UDP不保证。
TCP如何进行可靠传输?
1.首部校验和
2.通过序号保证数据有序传输
3.通过确认机制保证数据确认接收
4.如果没有确认,就进行重传,比如超时重传和快速重传
TCP累计确认
如果确认了ACK10,那么说明10之前的都收到了。
TCP重传
两种方式:
一种是基于定时器,如果在指定时间内没有收到ACK确认,那就认为数据丢失,进行重传。
但是这种方式增加了时延,而且,如果在等待当前确认的过程中,没有响应后续的确认,那么对方也以为自己的数据都丢失了,造成不必要的重传。
另一种方式基于冗余ACK的快重传机制。
冗余ACK是指,如果接收端没有接收到期望的确认,就重复发送上一个收到的一个确认信息。
当收到3个重复的确认报文时,就认为出现了数据丢失,立即进行重传。
TCP拥塞控制
慢开始:
刚开始拥塞窗口设为一个最大报文,然后指数级增长到窗口阈值,接着窗口进行加法增大,直到发生拥塞,然后立即将拥塞窗口降为一个最大报文,新的窗口阈值减半。然后重复上述步骤
快恢复:
当发生拥塞的时候,不减为一个最大报文,而是减为原来的一半,然后进行加法增大
TCP连接个数
一个TCP连接由四元组唯一标识,本地ip,本地端口,客户端ip,客户端接口。
因此,TCP的连接数量不受限于端口,受限于硬件资源。(还受限于文件数量,比如linux下会为每个tcp连接创建一个文件,如果tcp连接超过最大文件允许个数,就会报错)
流量控制:滑动窗口
滑动窗口用于流量控制,分为两个窗口,一个是接收窗口,一个是发送窗口。流量其实是由接收窗口控制的。
比如说连接建立成功后,接收窗口设置好接收窗口大小后,会告知发送端,然后发送端也会限制自己的发送窗口跟接收窗口一样大,当发送端发送了窗口大小的数据之后,就会向前滑动,滑动时窗口大小会改变,改变值依旧取决于接收方传来的接收窗口大小。
GBN(GoBackN协议) 选择重传协议
GBN就是滑动窗口应用,发送方一次性发送窗口为N的数据包(假设N=10),如果接收方在接收第2个数据包的时候发生了错误,就要求发送方重新发送2之后的所有包(尽管发送方已经发送过一次)。
也就是说,GBN不对发送过来的数据做缓存,只要发生一个错误,就丢弃后续的所有数据。
选择重传协议就是用于解决这个问题,它缓存发送过来的数据,谁出错就重传谁。
TCP的keep alive
HTTP的keepalive——长连接
服务端/TCP的keepalive——心跳机制,定时检验另一端是否有效
TCP双工通信
由于TCP是双工通信,而且为了配合重传机制,有一个发送缓存和接收缓存。
发送的数据会保存在发送缓存中,方便重传所需;
接收的数据会保存在接收缓存中,方便拥塞情况下慢慢取出数据进行处理。
其他网络相关的发送缓存和接收缓存原理类似。
TCP粘包与拆包
粘包,TCP一次性将发送缓存中的数据发出,导致接收端接收缓存中多个数据包粘连在一起。
解决办法:每个数据包增加头部,指明数据包长度;或者其他定界功能
拆包:发送的数据大于TCP发送缓存,就会发生拆包
HTTP
301永久重定向,302临时重定向,主要是为了seo。
304,未修改,表示自从上次请求之后,页面并没有修改过,不会返回内容,节省资源。
HTTP是基于TCP的应用层的协议。
可以分为HTTP请求和HTTP响应,通过报文进行标识。
HTTP1.0和HTTP1.1和HTTP2.0
1.0默认使用短连接,1.1默认使用长连接,长连接分为流水线和非流水线模式,流水线就是指在未收到响应之前可以发新的请求,非流水线则是必须等到响应过后才能发下一个请求。
1.1新增了很多错误状态码。
1.1新增了缓存处理支持
1.1对断点续传提供了支持(Range参数)
2.0提供首部压缩、多路复用以及服务器推送(将响应主动退给客户端)。
HTTP缺点以及HTTPS
HTTP使用明文,不验证通信方身份,无法证明报文完整性,不知道是否被篡改。
HTTP升级为HTTPS需要先申请SSL证书,然后在服务端进行配置,HTTPS在443端口。
HTTPS流程:
1.客户端发起HTTPS请求
2.服务端响应数字证书以及公钥
3.客户端校验证书合法性,通过后生成一个对称密钥,并用服务端的公钥加密
4.服务端用私钥解密信息,得到对称密钥,随后使用对称密钥加密信息
5.后续都是使用该对称密钥进行数据的加密解密
TLS(传输层安全协议)是SSL(安全套接字层)的升级版,但习惯上还是以SSL统称TLS和SSL
常用协议
ICMP:ping命令 网络连通性、可达性
ARP:找到IP地址的MAC地址,广播发送,单播回传、RARP,MAC找IP
IPV6 128字节,16进制,去掉了校验和,由网络层保证可靠,不允许分片,NDP取代ARP
NAT:网络地址转换协议,用于解决分配了内网IP的主机访问外网的问题,分为如下三类:
- 静态转换:内网IP与外网IP一一对应,配置的外网IP是一成不变的。
- 动态转换:同上,只是配置的外网IP是随机的。
- 端口复用:所有内网共享同一个外网IP,只是映射表现的端口不同,能够屏蔽内网细节,节省IP资源
路由选择协议:
RIP:内部网关协议,用于动态路由的选择,基于距离向量,与16跳以内的路由器交换信息
OSPF:内部网关协议,基于最短路径优先,迪杰斯塔拉算法,
BGP:边界网关协议,交换不同自治系统间的可达信息。
输入url后的过程
操作系统
vfs
虚拟文件系统,就是一组文件调用接口,屏蔽了底层实现细节,各家厂商完成各自的具体实现。
协程
轻量级的用户态线程,拥有自己的工作空间,切换开销比线程切换小得多,由于是单线程,也不需要加锁,只需要对共享资源进行状态判断。
零拷贝
前置原理:
DMA:专门负责处理CPU与硬件IO之间的工作,可看做专用于IO的操作系统,缓解CPU与硬件的速度矛盾。
在linux下,当用户打算将一个文件通过网络发送出去的时候,实际上需要进行四次拷贝:从磁盘拷贝到内核buffer,从内核buffer拷贝到用户buffer,从用户buffer拷贝到socket buffer,从socket buffer拷贝到网卡上。包含4次拷贝和4次上下文切换。
零拷贝就是为了减少这种不必要的拷贝过程以及多余的上下文切换。
mmap:用户态的缓冲区与内核态的缓冲区映射到同一段物理内存上(虚拟内存的应用),减少一次内存拷贝。
sendFile:直接在内核空间进行拷贝,减少了上下文切换
NIO
NIO通过块(首地址+长度)来进行传输,为了避免GC造成的内存调整,导致块的内存参数错误,会先将块拷贝至堆外内存(DirectByteBuffer),堆外内存不归GC管控,但是它持有一个Cleaner对象,该对象是一个虚引用,当发生GC的时候,就会回收该对象,回收之前就会调用free方法释放堆外内存。
操作系统提供了很多零拷贝方案的接口,Java中NIO的transferTo方法,它可以直接将文件缓冲区的数据发送到目标Channel。
虚拟内存
虚拟内存建立在局部性原理上(时间~指令,空间~内存),内存中只驻留需要执行的部分,其他部分暂时放在外存上,根据执行所需对换内外存程序,这样用户就感觉似乎有大于内存的空间可以使用。
实现虚拟内存需要保证内存是离散式分配,因为要实现换入换出。
虚拟内存的最大值取min(计算机寻址范围,内外存之和)。
比如32位机器按字节编址,寻址范围是4g
死锁
发生死锁的四个必要条件(同时满足则发生,任意一个不满足则不发生):
互斥:抢夺互斥的资源
不剥夺:进程分配的资源不允许被剥夺
请求和保持:请求资源时会继续持有其他资源
循环等待:存在某种资源的循环等待
解决死锁的四个手段:
预防:破坏死锁的四个必要条件,或者确保资源有序分配
避免:安全序列,银行家算法
检测:资源分配图、死锁定理(图不可完全简化则死锁)
解除:资源剥夺、进程撤销、进程回退到安全序列
Mysql
优化
并没有做过实际的优化,但是会考虑如何做,才能尽量与mysql本身自带的优化相结合。
比如:
1.mysql是半双工通信,当客户端发送了一次请求,就只能等待结果返回,无法进行其他控制,因此,只要查询结果是集合,都尽量进行分页。
2.用BIgInt或者int表示小数,避免decimal精确计算代价高以及存储代价(128位)
3.由于聚集索引的性质,自增ID作为物理主键,UUID作为逻辑主键
4.mysql索引无法识别表达式/函数作为查询条件的查询语句,比如where id+1=5,显然id=4,但是并不会走索引,因此避免查询条件是表达式或者函数。另外,发生类型转换的时候也无法走索引
5.较长的字段,可以使用前缀索引
6.在应用层体现外键逻辑,去除物理外键
7.运行避免冗余列,减少join的使用,指定查询列避免select *
8.除非真的要保存NULL值,尽量设置NotNULL,因为索引需要对其进行特殊处理,一定程度上避免空指针
9.索引不是越多越好,尽量选择区分度高的列作为索引
B+树高度 1~3层,算出来的,innoDB最小存储单元是页,一页是16KB。
索引优化:区分度较高的、常读 联合查询优化:最左匹配
explain查看是否走索引(key不为null)
为什么使用B+树,树低、多路搜索、数据在叶子节点有序排列契合磁盘存储、预读磁盘、
Java
深拷贝与浅拷贝
浅拷贝:仅仅是引用拷贝,内部对象其实依旧指向同一个对象,内部对象的改变会影响被拷贝对象。
深拷贝:真正意义上的创建一个完全相同但引用不同(包括成员对象的应用)的拷贝对象。
浅拷贝只需要对象实现Cloneable接口并调用父类的clone方法即可。
深拷贝则要求对象以及对象内部的所有其他对象都实现Cloneable接口,然后重写深拷贝对象的clone方法,层层调用内部对象的克隆方法。
假设对象A持有一个对象B,对象B持有一个对象C,要对A进行深拷贝
@Override
public Object clone() {
//深拷贝
try {
// 直接调用父类的clone()方法
A a= (A) super.clone();
// 层层拷贝
a.b= (B) b.clone();
a.b.c = (C)b.c.clone();
return student;
} catch (CloneNotSupportedException e) {
return null;
}
}
finalize()
finalize()方法,在gc时会被调用,但是可能导致无法回收对象。因为finalize()方法执行后,gc不会回收该对象,而是再次检查该对象的可达性,如果该对象“复活”了,那么不会回收,这样可能出问题。
1.7和1.8的区别
接口默认方法
lambda表达式
函数式接口
日期API
Stream和Optional
哈希map 拉链改成数组+链表+红黑树
等。。。。
Java与C++的区别
- C++是编译型语言,而Java是通过预编译生成字节码,然后再编译解释生成机器码。
- C++没有Java的一次编译处处执行的特点,但是C++更快(编译更耗时)
- C++兼具面向过程与面向对象,而Java是纯面向对象。
- C++中有指针,Java则是引用
- C++支持多继承
- C++需要自己管理内存,Java有自动垃圾回收机制
对象头
其实就是对象的一个名片信息,包含mark word、Klass pointer、Array length(数组专有)。
也就是,标志信息、类指针、数组长度。
- mark word中包含锁状态(无锁、偏向锁、轻量级锁、重量级锁)、GC标记、GC分代年龄、线程ID、hashcode等…
- Klass pointer存放的是对象的类的指针,即指向方法区的类信息区域
- Array length ,随时记录数组长度,避免获取时遍历计算,空间换时间
fast fail
集合的快速失败,遍历/迭代集合元素的时候,如果删除或增加集合元素,就可能会抛出异常。
底层维护了一个modCount以及expectedModCount ,刚开始这两个值相同,如果对集合进行修改导致modCount增加,进而导致与expectedModCount 不相等,就会抛出 ConcurrentModificationException。
双重校验锁——单例
class Singleton {
private volatile static Singleton instance;
private Singleton(){ }
public static Singleton getInstance(){
if (instance==null){
synchronized (Singleton.class){
if (instance==null){
instance = new Singleton();
}
}
}
return instance;
}
}
注意要加上volatile,保证多线程进入同步块之后,第二次校验的时候能够立即感知到实例变化。
频繁发生full gc时的排查
通过jvm参数 HeapDumpBeforeFullGC,在stw之前将内存使用情况导出为日志,可以看到哪些对象占用了较大内存,进而检查代码。
服务器CPU占用过高排查
思路:查看进程占用(top命令)–>查看进程中的线程占用–>利用jstack查看线程的堆栈信息
IO占用过高同理排查。
Linux
常用命令
ln:创建链接,ln xx 默认硬链接 ,ln -s xx 创建软链接(符号链接)
find /usr -size +10M:查询usr目录下低于10M的文件
查看内存情况 free 或者 cat/proc/meminfo
修改权限 chmod 权限 文件
常见权限:777,公开;644(文件),所有者全权,其他只读;755(目录),所有者全权,其他可读可执行(目录的执行就是进入目录)。
特殊情况:拥有写文件的权限,却无法删除它。
因为删除文件本身其实是对目录项的修改,因此需要拥有对目录的写权限。
进程相关:
top:实时查看进程信息以及资源占用
ps:查看当前进程快照信息
pstree:树状查看
nice <优先级> <进程名>:指定优先级启动进程,renice是调整正在运行的
网络相关:
tcpdump:tcp抓包,-c 10 只显示10条 -q 精简模式 (当网络慢、丢包的时候可以用该命令排查)
netstat -anp |grep 8080 :查看端口占用进程
traceroute:利用ICMP查看访问对应ip所经过的路由
进程间的通信方式
- 管道:本质是一个pipe文件,一个进程在一端写,另一个进程在另一端读,保证互斥(要么读要么写)与同步(读完了再写,写完了再读),用于传输字节流。
- 消息队列:通过消息链表实现
- 共享内存:映射一段共享内存,让进程读取的时候仿佛读取自己的内存空间一样,省略了内核切换
- 信号量机制:通过信号量的变化,控制线程同步
- socket
- 远程过程调用
其中共享内存最快,因为共享内存映射到用户的地址空间,减少了内核态切换的干涉。
tmp文件权限
查看权限 ls -l xxx
权限位共10位,第一位表示文件类型,普通文件-,目录文件d,cbs分别代表字符设备、块设备、符号链接
前三位表示所有者权限
中三位表示群组权限
后三位表示其他人权限
r读w写x执行
tmp权限:drwxr-xr-x 所有者7,其他人不可修改
符号链接和硬链接
符号链接类似快捷方式,符号链接文件只保存了对应文件的地址。
硬链接创建的文件,不指向源文件,而是指向原文件的物理地址。相当于做了一个深拷贝。
硬链接创建的文件,不受原文件的影响,软链接则参考快捷方式。
进程的内存空间
1.stack:存放临时变量,方法出口等
2.heap:动态申请的变量 (堆和栈的内存增长方向是相反的)
3.data:存放初始化的全局变量和静态变量
4.bss:存放未初始化的的全局变量和静态变量
5.text:存放指令操作,只读
僵尸进程
僵尸进程,当子进程结束而父进程没有结束时,子进程就会变成一个僵尸进程,这是为了保留了自己的剩余信息,比如为什么结束,执行状态,执行时间等等,这些信息需要等待被收集。
ps状态为Z的就是僵尸进程。
僵尸进程不能直接杀死,可以通过杀死父进程,让其变成孤儿进程,随后由init接管。
如何避免僵尸进程,用两次fork,然后让子进程结束,这样孙子进程就会被init接管,负责它的回收问题。
init是linux所有进程的父进程,进程号为1,负责一些管理工作,比如回收孤儿线程,
SWAP
swap交换分区,当物理内存不足的时候,系统会释放一些不活跃的进程所占空间以供使用,被释放掉的进程数据放在swap分区中,当这些不活跃的进程变活跃的时候,就从swap中进行恢复。
协程
协程:协程是比线程更小的一个单元,它的好处在于它的控制不走内核,走的是程序,由程序控制它的切换以及阻塞,减少了开销,协程不支持并发,任意时刻只有一个协程在执行。
协程的优点:追求极限性能和优美代码结构的产物。
微信协程框架libco
操作系统原子性:基本的原子操作+常用非原子操作加锁
文件操作原理
-
创建文件:
进程:调用create,告知所需大小,存放路径,文件名
操作系统:找到所需空间,找到对应的文件目录,创建对应的目录项 -
删除文件:
进程:调用delete,告知存放路径,文件名
操作系统:根据存放路径找到目录项,根据目录项找到文件的内存信息,回收内存,最后删除目录项 -
打开文件:
进程:调用open,告知文件存放路径,文件名,操作类型(r、rw)
操作系统:根据存放路径,找到对应的目录项,检查操作权限;然后将目录项复制到“打开文件表” 中,并将文件描述符返回给用户,后续使用文件描述符访问文件即可,不再需要通过目录项。(打开文件表能够提升访问速度)
打开文件表分进程打开文件表(进程专属)以及 “系统打开文件表”(系统唯一),包含打开计数器。 -
读文件:
需要先打开文件,然后进程通过打开文件表访问文件,通过系统函数read ,告知文件描述符以及读取字节数,系统会将文件内容读到用户指定的内存区域中(用户buffer) -
写文件:
首先需要打开文件,然后通过xxxxxx,告知xxxx(与读文件相同),并告知写回外存的数据在内存的哪个地方(用户buffer)
进程、线程、协程切换
进程切换:保存当前进程上下文信息,更新换出进程执行状态,更新切入进程的执行状态,切换虚拟地址空间,载入切入进程的上下文信息。切换过程由内核完成。
线程切换:没有虚拟地址空间的切换,因为线程共用进程的地址空间。
协程切换:由线程(用户)控制,
信号量原理
void P(semaphore s)
{
s.value--; // 申请资源,边界是value已经为0了,那么现在变-1,表示有一个进程在等待
if(s.value < 0)
{
将此进程加入就绪队列,等待;
block(s.L);
}
}
void V(semaphore s)
{
s.value++;
if(s.value <= 0)
{
将进程P从就绪队列中移出;
wakeup(P);// 叫醒P,让它起来干活
}
}
大数据量算法
分治\哈希取模
大数据通常需要划分到小文件中逐一处理。
对于一亿条数据,如果直接按出现的顺序对其进行取模然后分配(比如按次序%1000分配到1000个文件里面),会导致同一条数据内容出现在不同的文件中。
一般先对其进行哈希,再对哈希结果进行取模,这样能够保证相同的数据必定在同一个文件中。
哈希统计
利用Map的键值对,键保存对象,值保存对象出现的次数,进行频度统计
寻找topK/ lowK
小顶堆或者大顶堆
重复、不重复
使用bitmap,一位表示一个数。
如果目标数是int类型,那么最大就是21亿出头,如果仅仅是去重,那么需要21亿位,大概是250M内存。
如果是判断重复,则需要两位表示一个数据,00表示没有,01表示出现一次,10表示出现多次,需要500M内存。
去重、交集
使用布隆过滤器
布隆过滤器将key通过若干个hash算法,将映射值散列到对应的一个bitmap里面,后续如果有key进来,通过布隆过滤器筛选,如果对应的位置都是1,则说明已存在。
示例算法:分治\哈希取模+哈希统计+快排\堆排\桶排:
① 十亿条用户数据,以及一亿条的浏览记录,找出浏览过该记录的用户以及对应的浏览次数。
1.哈希取模,将用户数据进行哈希取模,划分到小文件中,浏览记录采用同样的哈希取模算法。记用户数据小文件为A,浏览记录小文件为B
2.一一比较(a0,b0)、(a1,b1)…利用A初始化Map,然后用该Map遍历B,每发现一次就value+1
(因为哈希取模保证相同内容被分配到相同序号文件中)
② 十亿个数字,找出其中最大的十个。
方法1.哈希取模,将数字分布到小文件中,再对小文件进行快排
方法2.构建容量为10的小根堆,依次遍历所有文件的最小值,如果文件最小值比当前堆顶大,则替换。最终结果就是小根堆。(或者不对小文件进行排序,直接使用小根堆遍历所有文件的值)
常见的web攻击方式
XSS:跨站脚本攻击
- 存储型,通过提交文本中嵌入js代码,然后被保存到数据库中,等后端将该文本拼接成html后返回到前端,该html中被植入的代码就会被执行,可以以此获得用户的cookie
- 反射型,攻击者将恶意代码放在url中,用户点击该url后,服务端取得该url中的参数(实际是恶意代码)拼接成html返回给前端,然后恶意代码被浏览器执行
- Dom型,恶意代码也在url中,用户点击后,被浏览器解析执行(不涉及服务端,纯前端漏洞)
防范:对提交文本进行过滤
CSRF:跨站请求伪造(比如qq空间,发表说说的接口是post xxx?content=说说,然后黑客发送给你一个网址,那个网址实际就是请求这个接口,如果你点击了,就会调用这个接口,由于你的浏览器处于qq登录状态,因此会默认你是登录的)
防范:每个请求都需要token校验
SQL注入解决方案,采用预编译
其他
字节序
字节序指的是超过一个字节的数据的存放顺序,一般情况下不需要考虑字节序,如果涉及到跨平台,则需要考虑(由于jvm屏蔽了平台差异,因此java里面可以说是没有这个概念)
字节可以按照大端存放或者小端存放。
大端规则下,低位数据放在高位地址上,小端规则则相反。
(大端规则符合人从左至右的观看规则,比如int 1234,大端规则下,4是低位,放在高位地址上,我们从低位往高位走,看到的数据就是1234)
漏桶和令牌桶
限流算法:
漏桶:任意速率流入请求,请求以恒定的速度流出,到达服务端。桶容量优先,溢出的请求被拒绝。
令牌桶:服务端固定速率向桶内放入令牌,客户端拿到几个令牌就可以发送几个请求。高并发情况下令牌速度跟不上,多余的请求被拒绝。
区别:对于漏桶,服务端不管并发如何,处理速度恒定,而对于令牌桶,假设这种情况,桶满的时候,一瞬间被拿完,那么服务器就需要一瞬间处理桶容量大小的并发。
Raft算法
在分布式环境下解决一致性问题的协议。
raft协议中,任何一个节点都处于三种状态之一(leader、candidate、follower)
启动时,所有节点都是follower,如果收不到leader的心跳,那么follower会转为candidate,发起选举。如果收到大多数节点的投票(包括自己一票),则该candidate成为leader,其他candidate重新变为follower。
leader有任期,如果任期到了,就会转为follower,此时又变成了启动时的状态。
即便多个candidate同时发起选举,但是每个节点只有一票,投出去了就不能再投。(这防止了脑裂(多个leader))
如果出现平票,则只能重新发起(这会延长不可用时间,可以通过让选举时间随机化,降低平票概率)
leader开始工作后,接收到的请求会写入日志,然后同步到其他节点中。
LRU和LFU
LRU指最近最少使用(在时间上靠后)
LFU指的是最近使用频度最低(在频次上靠后)
所以LRU只需要一个双向链表,头插尾删即可,对访问的节点将其提到头部
对于LFU,则需要记录节点使用次数,按次数进行排序
如果有个用户喜欢频繁遍历缓存,会导致缓存长期处于“新鲜状态”,这样删除策略就显得不那么可靠。
可以创建多个缓存,将key哈希到各个缓存中,减少遍历影响。
个人觉得,其实频繁遍历,本身也符合缓存的本意。
完全二叉树必备公式:N个节点的二叉树的高度为 ceil( log2(N+1) )
精度
long类型的数组求均值,可能会出现越界
需要使用二分法常见的mid = left+(left+right)/2
public static void main(String[] args) {
long[] list = new long[]{4L,4L,4L,4L};
for (int i = 0; i < list.length-1; i++) {
list[i+1] = list[i]+(list[i+1]-list[i])/2;
}
System.out.println(list[list.length-1]);
}
double数组求和,尽量保证精度
每次选择最小的两个数进行求和。
消息队列
作用:削峰填谷(请求塞到队列里面排队处理)、异步解耦(系统间的消息塞到队列里,而不直接进行系统间的消息传递)
如何保证消息不重复消费?
校验消息消费的幂等性。
一般而言,幂等性的保证要靠操作本身的性质来决定(比如把5改成1,无论多少次都是幂等;把5加1,怎样都是非幂等),所以业务上的幂等,一般指的是只允许调用一次,所以只需要在调用前加个判断即可,比如判断5是否变动过,或者setnx加个锁,锁存在说明已经调用过了。
因此保证消息不重复消费,可以在消费前查看消费结果是否已出现(比如查数据库),或者加setnx
如何保证消息按序执行?
消息拿到后放到内存队列中,然后在内存队列中获取执行
如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时,说说怎么解决?
对于过期失效、消息队列满了,如果有条件的话,可以增开一个队列保存(死信队列)。此外,基于这个思想,可以增设多类功能性队列,比如延迟队列、重试队列等等。
消息积压的关键在于提高服务端消费消息的速度,如果是非正常积压,可以先考虑排查服务端消费接口是否正常,恢复正常消费速度。
如果还是积压严重,只能临时更改架构,比如增设多个队列,然后将积压信息分布到这些队列中,再增设对应个数的消费节点,与队列构成一一对应关系进行消费,加快消费速度。
如何保证消息的可靠传输?(避免消息丢失)
首先是前端与消息队列之间、消息队列与服务端之间的消息传输,参考TCP,采用应答机制。
其次是消息队列本身集群节点故障问题,采用常见的分布式一致性算法,比如raft。
999. 小知识点
- 数组元素为引用类型,若用Arrays.sort(),则该引用类型要实现接口Comparable才能进行比较排序。
- 引用数据类型的数组不会对引用数据类型进行初始化。
- continue+标签可以实现“goto”的功能
- StringBuffer (线程安全)和 StringBuilder (快)
- 加号连接string的时候,被隐式的转换为StringBuilder
- final:变量不可修改、方法不可重写但可重载(比如更改参数)、对类不可继承
- $可以作为标识符开头
- Java不采用ASCII,而是使用Unicode
- 静态导入 Import static java.lang.Math.PI; //导入静态属性PI
- 多态的三个必要条件:继承、重写、父类引用指向子类对象(Parent p = new Child()),多态只体现在非静态方法上。(比如父类和子类都有一个static S对象但值不同,则p.static打印的是父类的结果,又称为隐藏)
- 通过抽象类实现接口,向下作为抽象接口,能够减少必须方法的实现,并且可以额外拓展接口。
- 通过内部类可以实现多继承
- 集合的remove方法,仅仅是将对象的引用从集合中剔除,对象本身没有被删除。
- 不要存在无限while循环,应当在while中使用sleep(持有锁,但让出CPU)
- 只有POST是非幂等的
- 在SpringBoot下,我们的类没有实现序列化接口,依旧能够进行网络传输的原因是因为,SpringBoot会通过HttpMessageCoverter对我们的类转为JSON字符串(比如fastJson),而字符串String是实现了序列化接口的。(也可得知,服务器–>客户端,是先进行JSON字符串转换,再进入序列化层)
更多推荐
所有评论(0)