您有任何建议或意见,请您在下方回复或者私信我,感谢!
因为Java虚拟机的存在,类型的定义也是跨平台的。
Java的核心在于JVM,Java程序被编译为字节码后,在JVM中运行,与宿主机是隔离的(这也保证了一定的安全性)。
如果其他语言也能编译为字节码,也是可以在JVM中运行的,比如Scala。
这样来看,Java体系本身具有一定的“语言无关性”。
分布式的前提是网络环境,由于Java对CS模式的支持,因此对网络的支持也渗透到了方方面面。
分布式分为数据分布和操作分布。
可以通过url的形式,将数据分布在网络中,也可以通过分布式的数据库实现数据分布;
操作分布其实就是分布式计算,典型的就是MapReduce。
回忆一下基础知识:
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.增加了可执行代码的长度(字节码要比即时编译器展开后的本地机器码小的多),这导致页面调度,从而降低程序速度。
1.强类型,在编译和运行时进行了大量的类型检查,防止了数据类型不匹配的发生。
2.垃圾回收机制(详见 7. 内存模型与 8. 垃圾回收)
3.异常处理机制(详见 11. 异常处理)
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内存模型
bin 可执行二进制文件
db 数据文件
include 包
lib 相关jar包
src.zip jdk相关java类的源码
Java程序从编码(.java文件)到编译(.class文件)再到jvm中,jvm首先字节码文件进行类加载,然后通过执行引擎进行执行,在执行过程中会创建一个内存区域,称为Runtime Data Area(运行时数据区),Java内存管理主要就是对该区域进行管理。
线程拥有各自的工作内存(线程栈),他们共享一个主内存(堆)。当线程需要操作主内存中的对象时,会先将其复制到自己的工作内存中,操作完毕后再写回主内存中。
主要分为五大区域,其中三个是线程私有:虚拟机栈、本地方法栈、程序计数器,两个线程共享:堆、方法区。
由于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能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
线程私有的区域,等到线程结束的时候,会自动释放,因此GC主要处理的是线程共享的区域,即堆与方法区。
引用计数法
给每个对象添加一个计数器,引用一次计数器就加一,反之减一,若在一定时间内计数器保持为0,则对其回收。
但这种算法无法解决循环引用问题。
可达性分析算法
利用一个GC Root节点,建立起一张引用图,凡是从GC Root根节点出发不可达的对象,则对其回收。该算法能够解决循环引用。
显然,GC Root对象不需要担心内存回收问题,因此该对象应该具有自动回收属性或者永久性的特点,因此,可以作为GC Root的对象也就可以猜测出来。
垃圾回收算法:
堆内存分为新生代,老年代 和持久代
不同区的对象生命周期不同,因此回收算法也不同。
新对象优先在Eden区进行分配(大对象直接进入老年代,-XX:PretenureSizeThreshold=3145728 这个参数来定义多大的对象直接进入老年代),当Eden满后,触发一次Minor GC,清理无用对象,并将有用对象复制到Survivor区中。
在Survivor区中循环存放超过n次之后还未被回收的对象,会放到Tenured区,当Tenured区满后,会触发一次Major GC。
持久代,用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响。
JVM调优很大一部分工作就是对FullGC调优
如下原因可能导致Full GC:
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
内存泄露:内存空间脱离控制范围(比如只释放了链头导致其他节点丢失)
内存溢出:内存不足
可能导致内存泄露的情况如下:
类加载过程包括“加载字节码到内存”、“链接”、“初始化类”三个过程。
初始化(重点):
假设代码如下:
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下存在多个项目,但是这些项目中存在同路径同名的类,双亲委派就会导致加载覆盖)。
双亲委派机制原理
// 代码摘自《深入理解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驱动,其实这就是调用了线程上下文类加载器对其进行加载。
参考
参考
Try-catch-finally中的return
阿里巴巴Java开发手册建议不要在finally中使用return,因为finally的return会覆盖catch或try中的值(而非网上说的会将catch的值临时存在栈中再返回)。
要点
1.List的元素有顺序,可重复,包括:
2.Set的元素无顺序,不可重复,包括:
3.Map,key-value,key不可重复,value可以,包括
装载可能涉及到排序的对象时,需要对该对象重写equals、hashCode方法
标准代码
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();
}
}
}
Java的精华之处,就在于JVM。
Java源代码通过JVM编译成字节码,实现一次编码处处运行。
通用的Token 包含“源代码”、“类型”、“语义值”;
比如 “32”–> Token(“32”,“整数”,“32”)
可以将javacc理解为“编译器的编译器”,可以实现词法分析、语法分析、语义分析等功能。
字节码(中间代码)的优点
在源代码中,不同的表达式语义可能是一致的(比如三元表达式与if语句),但若对其优化,他们的优化机制是完全不同的。中间代码具有语言无关性,转为中间代码后,只保留了语义,因此对于同语义的只需要一种优化方式。
在转为中间代码的过程中就可以进行优化,将优化工作提前能够减少后期优化工作量。
若直接对源代码进行优化,是十分复杂的,转为中间代码能够提高可读性、降低复杂性。
分层编译策略
在HotSpot JVM中,当JVM启动后,首先发挥作用的是解释器,边翻译边执行;
随着时间的推进,编译器逐渐发挥作用,通过热点探测功能,将热点代码编译成本地机器码。当下一次遇到热点代码时,便不需要编译或解释,而是直接执行。
此外,编译器包含client compile和server compile两种,client compile会对代码进行简单可靠的优化,而server compile会进行耗时更长的编译优化
full cg造成的stop the world,其实就是full cg优化。减少cg频率,降低cg时间。
程序、进程、线程
概念
线程同步机制
创建线程的方法
每个线程都有优先级,但优先级只代表优先的概率更大
- 用户线程:必须等用户执行完才能停下,最高优先级
- 守护线程:jvm停止之前,一定会等用户线程,但不等守护线程,最低优先级(只用于服务用户线程)
- 默认状况下,所有线程都是用户线程
- 设置线程为守护线程(thread.setDaemon(true))必须保证线程还没有start
- 守护线程的子线程也是守护线程
- 守护线程的主要意义:主线程结束后,该线程没有意义的时候,可以用守护线程(比如Java垃圾回收线程)
lambda
new Thread(()->System.out.println(“多线程中的lambda”)).start().
线程状态
进入阻塞状态的情况
线程死亡(一般不要使用自带的stop、destroy摧毁)
Thread.State
State state = t.getState(); (t为写好的线程实例)
HappenBefore
当多行代码没有依赖的时候,为了减少“内存操作速度慢而CPU速度快导致CPU等待内存而影响效率“的情况,jvm和CPU会尝试重排指令顺序,加快运行速度。这就导致代码不一定按你写的顺序执行。
语句的执行的最后一步,从CPU中将计算好的值传回内存,在这个过程中,由于CPU速度快,内存速度慢,可能发生计算好的值还没传回内存,新的指令就使用了该值的旧值。
简单理解:语句A和语句B按AB排列,若A执行完毕会非常慢,而jvm判断B又与A没有依赖,那么为了保证CPU不闲置,jvm会提前执行B。
但是在这种情况下,如何保证一个操作对另一个操作可见?happenbefore就是为了保证可见性的定义的一系列规则。
最常见的例子:
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对象提供原子操作。
核心思想:
自旋锁
让线程不断循环判断锁是否可用,处于忙等。
CAS操作
比较并替换,主要是在操作值的时候,查看值是否发生变化,如果没有发生变化,就认为对象没有被操作过。
但是会产生ABA问题,比如一个线程修改数据从A变成了B,随后该数据又被改成了A,这时候他看起来就像是没有变化过,通过了CAS校验。可以通过增加版本号来解决这个问题。
本地线程(重要)
ThreadLocal【本地线程】
ThreadLocal给每个线程一个独立的对象空间(一个ThreadLocal只能存放一个对象,重复放置会被覆盖)
实例:
1.Spring通过ThreadLocal保存每个数据库连接,保证单个线程使用的是同一个数据库连接
2.Shiro使用ThreadLocal保存当前用户信息
3.其实就是保存上下文信息
synchronized核心
早期synchronized是重量级锁,底层使用操作系统的互斥锁,每次获取锁和释放锁都需要用户态和内核态的切换。
现在的synchronized采取逐步升级的形式:
synchronized的使用先说结论:
每次执行之前模拟以下过程:判断是否需要锁-->锁竞争-->得到锁-->执行-->释放锁
①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状态变化时的行为即可。
其他
需要实现一个需求,动态修改springdata neo4j 的@RelationShip注解的type值。
网上搜索后,发现有方法可以解决。
但是实际上,似乎没有办法做到。
网上的解决办法代码
个人推测:JVM会将注解的值解释为一个常量(不可变对象),即便你改变了他的值,最终打印结果看似成功改变,但是创建的对象的注解实际值依旧是原来那个。
参考
代码运行效果:
期望通过运行时修改type的值
修改代码运行前:
修改代码运行后:
看似成功修改
我试过将修改注解值的操作尽可能提前,但是也没有用,应该是在类初始化完毕之后,该注解信息已经固定。
IO模型大体分为五种,阻塞IO、非阻塞IO、多路复用IO、信号驱动IO以及异步IO。
IO:分为几个阶段:发起请求、等待数据准备、数据从内核拷贝到用户空间、返回请求响应
参考视频,讲的非常好。
创建一个bitmap来标志文件描述符(用户线程)的可用状态(上限1024位),然后将bitmap交给select函数,select会将bitmap交给内核来检测是否有文件描述符可用,若有一个可用,则对其置位为1,然后select函数返回。接着遍历bitmap,得到标志位为1的文件描述符,处理它的IO操作。
缺点:
原理与select基本一致,但是改了两个地方:
依旧保留了上述3-4的缺点
使用一个int整数来映射文件描述符(epfd),这个epfd由内核态和用户态共享,避免了切换开销,内核监控epfd的状态变化,并进行置位,此时的置位是将对应的文件描述符往前挪动,最终返回一个可操作文件的个数nfds,然后对对应文件描述符遍历nfds次,逐个操作,这样保证了只对可操作文件进行遍历,复杂度是O(1)
epoll解决了上述4个缺点。
哈希
定义:将任意长度的输入,转为固定长度的输出。
Java中hashCode的方法返回值是int,因此hashCode实际是32位;
特点:
冲突:由于哈希结果的范围比原值要小,因此发生抽屉原理(十个苹果放入九个抽屉,必定有一个抽屉放两个苹果)
HashMap实际是一个数组+链表+红黑树的数据结构,发生冲突时,节点追加到冲突节点之后形成链表,当达到树化阈值(链表长度超过8且HashMap中元素个数达到64),则会将该链表转为红黑树。(底层检查是否对象是否是String,如果是则按String排序,否则利用对象的类名按String排序)
对HashMap的任意操作,只需要从他的结构出发即可(数组+链表+红黑树)。
比如插入时,计算出对应位置后,看他有没有碰撞,若碰撞了,再看看他是链表还是红黑树,再进行插入;
比如删除时,计算出对应位置后,看他的next是否为空,如果不为空,再看看他是链表还是红黑树,再进行删除;
其他原理类似。
哈希算法:
哈希的目的是“给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是当前操作值
1.7中,扩容的时候,在transfer中有个reHash的过程,可能会改变原本链表的前后顺序,如果多线程恰好都进入resize,则可能因为链表节点的顺序颠倒导致成环。(一个线程先扩容,一个线程后放节点)
1.8移除了transfer方法,但1.8的HashMap可能导致数据覆盖,因为put方法并没有加锁。
保证线程安全需要使用ConcurrentHashMap 。
concurrentHashMap的内部节点类Node的value和next被volatile修饰,保证可见性(get的时候就不需要加锁了,因为一定是最新值,只需要put的时候获得锁)。
在put的时候,如果发现put的位置为空,则使用cas写入,否则的话,用synchronized写入。
隔离特性
隔离可分为资源隔离(进程、文件、网络等)和物理资源隔离(CPU、内存等);
应用层面的隔离(不能产生跨容器的文件冲突)以及物理层面的隔离(不能产生跨容器的资源抢占)
docker利用linux的namespaces实现了文件系统、网络、宿主机间进程的隔离,利用linux的控制组实现物理资源(CPU、内存、磁盘IO等)的隔离。
namespaces是linux用来分离进程树、网络接口以及进程资源通信的方法;
控制组(CGroup)是linux用于隔离进程组使用物理资源的机制。
镜像
镜像本身是一个文件与linux根目录的目录结构差不多
分层存储
docker使用联合文件系统(UnionFS),使用dockerFile构建镜像的时候可以看到,docker容器其实是按层构建的,每层都是只读的,只有最上层的容器层提供给用户读写。
docker能够在不同容器之间进行层的复用(新建一个docker,若发现其中所需的某层之前下载过,那么不会再下载,而是直接拿来用)
数据卷挂载
持久化方案,当容器删除了,宿主机挂载的数据不会被删除
网络
利用namespaces隔离了网络,提供四种网络模式
nginx是一个反向代理服务器,能够屏蔽内部系统的网络细节,并且可以在HTTP协议上做一些统一的配置(比如解决跨域,请求过滤),可以对用户的请求做负载均衡,可以做缓存,降低服务器压力,
高效性
nginx使用epoll模型;
nginx使用多线程,对外有个master线程,其下有多个worker进程,master转发请求到某个worker去处理。
集群(负载均衡)导致的session丢失问题
session会话建立在一个客户端以及一个服务端之间,若用户使用途中因为负载均衡导致访问的服务器切换,则session也会丢失。
解决办法:
一般的哈希不能保证当“桶”的个数发生变化后的一致性。
当我们使用哈希作为负载均衡策略的时候,
比如使用简单的取余,有十台服务器,那么就是通过 hash(x)%10找到对应服务器。
假设一台服务器挂掉了,那么算法变成了 hash(x)%9,这时候会导致之前的缓存、session等全部失效,原本跑到A服务器的请求,会全部被转移到B去。
增加一台服务器也是这种情况。
因此需要一个算法,增加或者删除容器个数,都不会影响原有的映射关系。也就是一致性哈希
一致性哈希将虚拟出非常多个桶位(比如有十台机器,但是虚拟出有100台),这些桶位形成一个环形链表,进行哈希计算的时候,通过模100,得到一个桶位,如果该桶位不存在,就顺序的向下找到最近的一个桶放进去。
这样,增加一个桶,只是将一个虚拟桶变成真实桶,减少一个桶,则是将一个真实桶变成虚拟桶。
consistency:一致性,分布式的数据备份能够保证写完之后的读都相同
availability:可用性,每个请求无论成功还是失败都有对应的响应
partition tolerance:分区容错性,允许有错,但系统任一部分的信息丢失不会影响系统运行
CAP原则就是最多满足CAP中的两个性质。
由于P是必须存在的(不可能存在某个系统不会发生错误),因此就剩下C和A进行抉择。
C与A的冲突
一致性与可用性冲突最简单的例子就是,网络故障(即满足分区容错),网络出现了故障,那么满足可用性则必定无法满足一致性(无法通过网络更新已经被修改、分布在各处的数据),满足一致性则必定不满足可用性(等待网络修复后(暂停服务),用户才可以查看到正确的数据)
后续范式的基础条件是满足前面的范式;
第一范式:数据项不可拆分
(比如某项是“学生信息”)
第二范式:每个非主属性都依赖于码
(比如学生表中出现了系主任名称,系主任名称并不依赖于学生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是描述事务的特点。
A:原子性:一个事务要么全部成功、要么全部失败
C:一致性:在事务执行的前后,数据库保证数据一致性,不能因为事务的中断导致数据前后不一
I:隔离性:在并发环境下,并发的事务之间不会相互影响
D:持久性:提交成功的事务描述的数据状态一定能永久保存到数据库中(比如执行过程中断电,当恢复电力的时候,会从事务日志中继续完成未执行完毕的事务)
针对隔离性提出的隔离级别:
读未提交解决丢失更新(其实是锁阻塞),但是会有脏读
读已提交解决脏读,但是不可重复读,
可重复读解决不可重复读,但是幻读。
为什么默认RR?
历史遗留问题,以前mysql基于binlog实现主从复制,如果使用RC会出现数据不一致的情况,因此默认RR
幻读模拟:
A开启事务
A全表搜索有3条数据 B开启事务
B插入新数据
A再次全表搜索依旧是3条数据(MVCC保证了可重复读),但是插入数据,缺报主键重复(A看不到新的数据,但是实际存在一个新的数据)
加间隙锁就可以防止幻读(因为插入会被阻塞)
MVCC:多版本并发控制。
在数据行中,还有两个隐藏列,一个是事务id (trx_id),一个是指向undo日志中上一版本的指针地址roll_pointer。
对数据行的修改,会在日志中形成一个类似单链表的结构,保存着数据行的所有版本。
通过MVCC,实现不同隔离级别读到不同的数据
比如读未提交,事务总是读到最新数据(当前读);
读已提交,事务会通过指针地址读到上一个提交过的记录上,如果有多次不同的提交那么总会读取到最新的一次,因此不可重复读(快照读);
可重复读,事务总是读到第一次读到的那个(快照读)
日志
执行顺序:
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
索引是排好序的、可快速查找的、数据结构。
排好序:影响插入和删除(逻辑删除的好处之一就是能够避免这种现象)
索引的分类:
什么情况下建立索引:
什么情况下不要创建索引:
索引效率计算:差异值/值总数=索引效率,越接近1越好
聚集索引与非聚集索引
联合索引最左匹配原则
对于联合索引(a,b,c),在b树索引中,是按联合索引的顺序创建的b树,如果查询条件中不包含a,那么将无法使用索引。
而且如果存在范围查询,会导致后面的无效(比如 where a>10 && b=1 && c=1),只有a>10才会生效,因为找到a>10的索引范围之后,对于b=1和c=1来说,不满足最左匹配原则(a=?并不知道)。
索引覆盖
结果集只包含索引列
索引失效
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树
以下默认在InnoDB的RR模式下。
先说结论
以InnoDB为例。
非行锁
全局锁
对整个库变得只读,可以加全局读锁,比如做全库逻辑备份的时候可以用。
但是可能出现以下问题:
在主库上备份,数据无法更新,业务基本停摆
在从库上备份,主库的内容无法更新到从库,造成主从延迟
表级锁
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。
Spring的核心:IOC、AOP、声明式事务;
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扩展,提供了更多功能,比如国际化处理。
构造器注入和set方法
详细版本
简单版本
极简版本:
1.静态块收集
2.属性注入
3.初始化
4.业务处理
5.销毁
其中每个方法前后都有对应的钩子方法方便我们在创建过程中实现自定义
主要应用:事务、日志、权限安全、性能监控
实现原理:通过动态代理,在方法的前后植入代码;
JDK动态代理要求代理类必须实现了接口,而且代理编译时结果也是用接口接收。因为JDK动态代理实际上是创建了一个被代理类的兄弟类,兄弟类之间不能实现互转。
CGLib代理则不要求类必须有接口,而且效率比JDK动态代理更高。
通知点:前置、后置、环绕、异常、最终通知
Spring实际使用AOP来实现事务,本质上有个TransactionMananger,Spring不做具体实现,而是注入各个数据库平台各自的事务管理器,通过AOP将业务逻辑加入到事务中。
事务传播是一个常见的业务行为,必须定义事务传播机制,来保证发生这些行为之后,有对应的处理办法
当A方法有事务,其内又调用了B方法,此时需要给B定义事务传播机制。
一共有七种,主要掌握:
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默认Bean都是单例,显然容易出现并发问题,但是其实只需要保证Bean无状态即可,所以避免单例Bean中使用成员变量,或者用ThreadLocal保存可能发生并发问题的变量。
ThreadLocal比线程同步机制实现简单,秉承空间换时间的思想。
其他参考
// 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());
关键对象:
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,命名空间级别,只要是同一个接口里面的相同方法,都可以共享。
查询顺序显然是二级–>一级–>数据库
参考
StatementHandler
ParameterHandler
架构图-类图
这个作者对mybatis的一系列解读值得慢慢阅读。
插件开发
四大过程(sql解析、参数映射、sql执行、结果集处理)都有一个interceptorChain.pluginAll(parameterHandler);
主要分为三个区:
SpringBoot的核心在于自动配置,通过扫描META-INFO/spring.factories,实现各个组件的自动配置
SpringBoot的核心优势并非脚手架的功能,而是
不同环境(测试、开发、生产)下加载不同的配置
通过 配置yml 可以实现
可以通过yml的 spring.profiles.active=dev设置激活,或者在启动的时候指定,或者用虚拟机参数指定。
目标:高内聚、低耦合、易扩展
具体模式请参考菜鸟教程
如果不用泛型,只能使用Object标识未知的对象,但是这要求程序员记住所有Object所指代的类别,不然会出现转型错误。
通过泛型(其实本质还是Object),能够在编译时统一元素类型,保证类型安全。
比如一个方法是操作Session的,Object实际是Session,只是我们不知道是具体哪个Session,但是我们也得记住这里只能放Session。但是用泛型,我们直接写个标识符表示就行(甚至直接命名泛型的标识符为Session)
泛型类型不支持基本类型。
同一个泛型类,不同的泛型参数,本质上还是同一类型=>泛型类本身
// intGeneric是,strGeneric是
System.out.println(intGeneric.getClass()==strGenenric.getClass()) // true
// intGeneric.getClass() ==>Generic
泛型不支持重载和多态
子类也是泛型类,则子类泛型标识至少要包含父类的泛型标识
class child<T,E,K> extends father<T>{}
子类不是泛型类,则父类的泛型类型需要显式声明
class child extends father<String>{}
此时子类使用父类的方法,方法的返回值都会变成明确的类型
泛型只在编译前有效,编译后被擦除
Redis是基于键值对的NoSQL 内存 数据库,利用快照和日志的形式进行数据持久化。
特点:
应用:
数据结构与内部编码
五大数据结构:字符串(最大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中。
优点:加速读写、降低后端(数据库)压力
缺点:数据不一致风险,增加运维成本
应用场景:本质是加速查询,可以体现到两个方面,一个是加速响应,一个是避免开销大的计算。
更新策略:
缓存穿透:通过重复查询不存在的数据,导致数据库压力过大。
例子:发起无数次查询:查询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接收
顺便说一下,调用第三方的某些接口(比如支付),在页面设计上需要满足他们设定的规范,比如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获取用户信息
微信登录与其类似。
可以参考这个视频
不过阿里云接口不太友好,我最后选的是华为云OBS,看完上述视频配哪个云都可以。
微服务提出者的原文翻译
提前总结:
微服务就是将单体架构拆分成多个小的服务,各个服务直接独立部署,使用轻量通信机制,这些服务相互独立,可以是跨语言的,并保持最低限度的集中管理。微服务也带来了很多必要的需求,比如服务注册与发现、服务调用、服务降级、服务限流、服务熔断、服务配置、服务总线、分布式事务、链路监控等。
或者直接使用一套SpringCloud alibaba
各个服务被拆分,首先考虑通信问题。
服务于服务之间可视为服务消费者与服务提供者;
提供者提供相应的接口(比如HTTP REST接口),消费者使用RestTemplate进行访问。
上述是最简单的微服务,但仅支持一对一供给。
若出现多个消费者、多个提供者,就涉及到接口的匹配、负载均衡、容错等问题,统称为服务治理。
抽取出一个服务注册中心,所有的消费者和提供者都在其上注册,然后将服务治理(负载均衡)交给注册中心来管理。
注册中心本身也是一个服务,因此也可能崩溃,可以为其搭建集群环境。
网关可以实现反向代理、负载均衡、路由、过滤、鉴权、流量控制、熔断、日志监控等。
是对外的统一访问入口。
将所有微服务的配置统一管理起来,比如在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对应的所有分支事务完成提交或回滚。
秒杀系统的关键在于解决“微量的库存与大量请求的冲突”。
完美的方案就是,n个库存对应n个请求。
所以关键在于,如何过滤无效请求?
分为4层:浏览过程(浏览浏览器)、请求过程(发起请求)、服务过程(处理请求)、数据库过程(修改数据库)。
普通用户的典型行为就是狂点“抢购按钮”,因此,可以在页面进行控制,比如点击一次页面置灰(或者不置灰,但是仅第一次点击生效)。
并做好页面缓存。
有的人可能通过接口直接访问,那么按钮控制会无效。
做好uid 的访问频度限制、限流,并做好负载均衡。
首先是流量削峰,采用消息队列,服务端根据自己的QPS拉取请求进行处理。
对于读请求,走缓存,对于写请求,走队列。然后从队列中拿出库存个数个的操作。
写请求很少,锁机制保证数据一致性即可。
超卖问题解决:更新库存-1之前,加上库存必须大于0的条件。(更新操作会自动加行锁,这样能够保证库存不会变成负数)
上图是一个简单的秒杀流程,现在模拟一下。
40万个用户蹲守10个商品
公平性:
首先要保证公平性,不能出现先抢的人抢的金额大。
可以采用线段切割,当红包创建的时候,制定了红包个数N,通过这个数,在红包总额上生成随机的N-1个点(如果有重复的点就再生成一次),将红包按照这些点进行区间划分,然后当其他人来拆的时候,按顺序领走即可。
并发:
当红包创建成功后,在数据库和redis中都插入一条记录。
当用户去抢的时候,在redis进行原子减操作,直到为零。
稀疏数组
稀疏数组用于处理数组的压缩存储。
保存一个“行列值”数组即可,如下图。
思路:
保存:遍历原始二维数组,遇到非零数据的时候,创建一个节点对象,保存该节点的行列值。注意第零行保存原数据的长宽以及有效数字个数
还原:根据第一个数组的数据,创建还原数组,然后遍历赋值即可。
队列
以下需要记住
普通数组模拟队列
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,且分为三种情况:
红黑树
特征:
一定是红插(红插能够最小程度的违反特征,修复代价低)
红黑树并不是严格的平衡二叉树,它通过颜色,降低了平衡的代价,满足部分平衡。
算法常用小点:
七层模型:
物理层:规定物理特性,透明传输比特流
数据链路层:帧定界、无差错传送帧
网络层:路由选择、分组转发
传输层:提供端到端逻辑通信
会话层:验证访问和会话管理(RPC、SQL)
表示层:信息格式和语法的转化
应用层:为用户进程提供服务接口
集线器、交换机、路由器
集线器在物理层,交换机在数据链路层,路由器在网络层。
在带宽上,集线器是共享的,交换机是独享的,比如10M带宽,集线器连接了两台设备,那么两台各分5M,如果是交换机,则表现为每台最大带宽都是10M。
集线器是广播发送(安全性较差),而交换机是有目的的发送。
(因为集线器无法记住MAC地址,而交换机如果给新设备发消息,不知道对方MAC,第一次也是广播)
路由器是根据IP地址转发数据包的。
交换机主要是连接局域网内的设备,而路由器是连接不同的网段。
简单来说,路由器在网络层负责将本局域网与外网连接,而交换机主要负责局域网内的数据交换。如果没有路由器,交换机的数据也出不去。
子网掩码
子网掩码主要是为了将ip地址分为网络地址和主机地址两部分,配合IP地址使用。主机地址全一就是子网的广播地址。
建立TCP连接时需要三次握手:
三次握手换成两次行不行?
不行,三次握手有两个好处:
释放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?
断开连接一定是四次挥手吗?
并不是,第二次和第三次可能合并(比如没有数据要发送)
每次请求都是三次握手吗?
并不是,在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/IP四层模型:网络访问层、网络层、传输层、应用层
使用TCP的应用层协议:HTTP、SMTP、FTP、TELNET
使用UDP的应用层协议:DNS、DHCP(动态配置IP)、电话、视频
TCP
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发送缓存,就会发生拆包
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的主机访问外网的问题,分为如下三类:
路由选择协议:
RIP:内部网关协议,用于动态路由的选择,基于距离向量,与16跳以内的路由器交换信息
OSPF:内部网关协议,基于最短路径优先,迪杰斯塔拉算法,
BGP:边界网关协议,交换不同自治系统间的可达信息。
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本身自带的优化相结合。
比如:
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+树,树低、多路搜索、数据在叶子节点有序排列契合磁盘存储、预读磁盘、
深拷贝与浅拷贝
浅拷贝:仅仅是引用拷贝,内部对象其实依旧指向同一个对象,内部对象的改变会影响被拷贝对象。
深拷贝:真正意义上的创建一个完全相同但引用不同(包括成员对象的应用)的拷贝对象。
浅拷贝只需要对象实现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++的区别
对象头
其实就是对象的一个名片信息,包含mark word、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占用过高同理排查。
常用命令
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所经过的路由
进程间的通信方式
其中共享内存最快,因为共享内存映射到用户的地址空间,减少了内核态切换的干涉。
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的小根堆,依次遍历所有文件的最小值,如果文件最小值比当前堆顶大,则替换。最终结果就是小根堆。(或者不对小文件进行排序,直接使用小根堆遍历所有文件的值)
XSS:跨站脚本攻击
防范:对提交文本进行过滤
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。