JVM指的是Java的虚拟机,Java程序需要运行在虚拟机上,不同的平台都有自己的虚拟机,所以Java语言实现了跨平台。
JRE指的是Java的运行时环境,包括需要的大量的类库和Java的虚拟机。
JDK指的运行时候的需要的一些工具类和运行时环境,比如包括javac.exe ,javadoxc.exe 一系列用于编译字节码工具 打包文档的工具
jdk1.5之前 byte、short、int、char
jdk5 ~ jdk1.7 加入了enum
jdk1.7之后 加入了String
*注意 不支持long类型
3.4 默认是浮点double类型的,如果赋值给float是向下转型,会出现精度缺失,,需要强制转换
用于修饰类,方法,变量(属性):
首先String是 private final char[]
首先他们都是继承AbstractStringBuilder,相对于String来说是可变的字符串,只不过Stringbuffer加了synchronized所以是线程安全的,而Stringbuilder是线程非安全的
== 默认是比较两个对象的引用地址的,而equals默认情况是==比较规则,但是不同的类会重写掉Object类的equals从而改变了equals的比较规则,比如String类的equals方法就是比较他们两个的内容是否相等
两个对象相等他们的hashCode和equals一定相等,但是hashCode相等的两个对象未必相等
面向过程:就是具体化的一步一步的去实现,优点 就是性能比较快因为不需要进行实例化。 缺点就是不容易进行维护,扩展和复用
面向对象:因为面向对象的三大特点就是 封装、继承和多态
数据结构:
HashMap通常是key-value键值对的方式进行存储,在JDK1.7的时候就是通过数组+链表的形式存储,每个数组存储的是一个一个的Entity组成的链表,这些链表上的数字都有一个特点就是Hash值相同,当数据量比较大的时候链表的长度比较大,这个时候查找的时间复杂度就比较大所以。在JDK1.8的时候引入了红黑树概念,当一个链表上的长度大于8的时候并且数组长度大于64这个时候链表就自动转换成红黑树,如果数组长度没有达到64那么此时就对数组进行1倍的扩容。
而且在JDK1.7和1.8还有一个改动就是插入的方式,由于JDK1.7的时候插入方式是头插法,在多线程的方式下会出选循环链表情况,所以1.8改为了尾插法方式。
扩容有三个地方
HashMap是线程不安全的,如果要保证线程安全那么可以使用ConcurrentHashMap
JDK 1.7 时候是使用分成16个Seagment段,每个Seagment里面存储着一个HashMap和一个锁,所以说1.7能支持最大的并发量也就是16个线程
JDK1.8采用的CAS + Synchronized,每次插入的时候判断是否是第一次插入,是就通过CAS插入,然后判断f.hash是否=-1,如果是的那么其他线程在扩容,当前线程也会参与扩容;删除方法用了synchronized修饰,保障并发下删除元素的安全
继承:抽象类是只能单继承的,接口是可以多实现的
属性:属性修饰符也不一样,抽象累可以是public protect 默认不写 还有final 修饰,但是接口只能是public static final修饰
构造方法:抽象类是可以进行实例化的,而接口是不能进行实例化
方法:抽象类既可以抽象的方法,也可以具体的方法,接口只有抽象的方法,而且子类必须实现
corePoolSize: 线程池核心线程数最大值
maximumPoolSize: 线程池最大线程数大小
keepAliveTime: 线程池中非核心线程空闲的存活时间大小
unit: 线程空闲存活时间单位
workQueue: 存放任务的阻塞队列
threadFactory: 用于设置创建线程的工厂,可以给创建的线程设置有意义的名字,可方便排查问题。
handler: 线程池的饱和策略事件,主要有四种类型。
当提交一个线程执行任务时候 ,先看有没有空闲的核心线程如果有就执行,如果没有就看阻塞队列中有没有满的状态,如果没有放满则放入队列中,否则就直接看线程数量是否大于非核心线程数量,如果没有就直接创建一个非核心线程,否则就是按照指定的拒绝策略给处理。。如果一个非核心线程在某个一段时间类是空闲的那么线程池就会把这个线程自动销毁掉。
newFixedThreadPool (固定数目线程的线程池)
newCachedThreadPool(可缓存线程的线程池)
newSingleThreadExecutor(单线程的线程池)
newScheduledThreadPool(定时及周期执行的线程池)
new ThreadPoolExecutor() 自定义的方式创建
会的,newFixedThreadPool使用了无界的阻塞队列LinkedBlockingQueue,如果线程获取一个任务后,任务的执行时间比较长(比如,上面demo设置了10秒),会导致队列的任务越积越多,导致机器内存使用不停飙升,最终导致OOM。
进程:进程是操作系统分配系统资源和内存空间的最小单位。进程是独立的一块空间,所以资源和内存空间的切换是特别消耗资源的。
线程:线程也叫做轻量级的进程,是操作系统调用执行的最小单位。线程的资源是依赖于他的父进程,所以他的资源是共享的,线程的切换需要转换到内核态开销相对于小一些。
协程:协程是一种轻量级的线程,协程是直接在用户态就可以控制,具有对内核态来说是不可见的,所以协程的上下文切换更加的节约资源消耗。
上下文的切换指的是CPU寄存器和程序计数器存储的数据进行切换,而内存数据的改变只有在内核态才能进行。所以上下文切换对系统来说是巨大的成本。
JMM屏蔽了各种硬件和操作系统的内存访问差异,实现让Java程序在各平台都能够达到一致的内存访问效果,它定义了Java如何将程序中的变量在主存中读取
具体定义:所有变量都在主存中,主存是线程的共享区域,每个线程都有自己独有的工作内存,线程想要操作变量必须从主存中copy一份到自己的工作区域,每个独立内存区域相互隔离。
所以这个时候读写存在延迟,且不是原子操作,所以就出现了一些列的线程安全操作。比如 原子性、可见性、有序性。
原子性:一次或多次操作在执行期间不被其他线程影响
可见性:当一个线程在工作内存修改了变量,其他线程能立刻知道**(利用内存原子操作解决或者内存屏障或者lock前缀)**
有序性:JVM对指令的优化会让指令执行顺序改变,有序性是禁止指令重排**(内存屏障)**
每一个线程创建变量的副本,不同线程间是不可见的,保证线程安全。每个线程都维护一个ThreadLocalMap,key为threadlocal实例,value是保存的副本。
但是使用ThreadLocal会存在内存泄漏问题,因为key 是弱引用,value是强引用,每次GC时key都会回收,而value不会被回收。所以为了解决内存泄漏问题,可以在每次使用完后删除value或者使用static修饰ThreadLocal,可以随时的获取value。
第二个会出现内存溢出问题,如果使用的是线程池的方式去使用ThreadLocal话,那么就会出现存储的value一直存在,这样就一直堆积。
CAS锁可以保证原子性,思想是更新内存是会判断其内存的值是否被修改了,如果没有被修改就直接更新,如果被修改了,就得重新去获取值,知道更新为止。这样是有缺点的:
解决ABA问题,可以通过加入版本控制
在JDK1.6以后Synchronized引入了偏向锁、轻量级锁、重量级锁、锁的粗化、锁消除的优化。并发性能基本和Lock持平的。
轻量级锁:当锁是偏向锁时,有另外一个线程来访问,会撤销掉偏向锁,然后升级为轻量级锁,这个线程会通过自旋方式不断获取锁,不会阻塞,提高性能
重量级锁:轻量级锁尝试去获取到锁,如果获取失败线程就会进入阻塞状态,该锁会升级为重量级锁,重量级锁时,来竞争锁的所有线程都会阻塞,性能降低
注意,锁只能升级不能降级
因为有一些线程可能发生中断 ,而发生中断时候就需要在同步阻塞队列中删除掉,这个时候就会用到prev和next方便删除掉中间的节点
互斥条件:进程要求对所匹配的资源进行排他性控制,即是一段时间内某资源仅为一进程所占有
请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:进程已获得的资源在未使用完之前不,不能剥夺,只能在使用完时有自己释放
环路等待条件:在发生死锁时,必须存在一个进程–资源得环星链
资源一次性得分配:一次性分配所有得资源,这样就不会有请求了(破坏请求条件);只要一个资源得不到分配,也不给这个进程分配其他得资源(破坏请求保持条件)
可剥夺条件:即当某个进程获得部分资源,得不到其他得资源,则释放已有得资源(破坏补课剥夺得条件)
资源有序分配法:系统分配资源编号,每一个进程按编号递增的顺序请求资源,释放则反之(环路破坏)
**剥夺资源:**从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;
**撤消进程:**可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等。
CPU 密集型:对于这样的任务最佳的线程数为CPU核心数的1~2倍,如果设置过多的线程,就会造成不必要的CPU上下文切换,性能相对于比较低
IO密集任务:由于IO读写是比较消耗时间的,并不会特别的消耗CPU的资源。对于这种任务最大的线程数一般大于CPU核心数的很多倍。
线程数 = CPU 核心数 *(1+平均等待时间/平均工作时间
强引用 :发生GC时候不会被回收
软引用:发生内存满(内存溢出的时候)会被回收(通常用于缓存)
弱引用:发生下一次GC时候会被回收
虚引用:无法通过虚引用去获取对象。用途:用于gc时返回一个通知
首先编译器通过把Java代码转换成字节码,类加载器把字节码加载到运行数据区域的方法区内,而字节码只是一套JVM规范指令,所以这个时候需要执行引擎将字节码翻译成底层系统指令,在交给CPU去执行,这个过程需要用到其他语言本地库接口
主要分为 加载 -> 链接 - > 初始化 三步骤:
加载:把字节码通过二进制的方式转化到方法区中的运行数据区
链接:分为三步骤:
验证:校验字节码的正确性
准备:给静态变量分配内存空间 和 赋值操作(赋0值 ,比如String 赋值 null int 赋值 0 ……)
注意 final修饰的变量不是这个时候赋值的而是在在编译期间就已经赋值了
解析:解析是将常量池中的符号引用替换成直接引用的过程
符号引用:以一组符号(可以是任何格式的字面量,无歧义即可)来描述所引用的目标。
直接引用:可以直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
初始化:初始化阶段可以看做是执行类的构造器()方法的过程。
注意:值得注意的是:加载阶段与连接阶段部分内容是交叉进行的,但开始时间有先后顺序。
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
new指令对应到语言层面上讲是,new关键词、对象克隆、对象序列化等
对象所需内存的大小在类 加载完成后便可完全确定,为对象分配空间的任务等同于把 一块确定大小的内存从Java堆中划分出来。
这个步骤有两个问题:
1.如何划分内存。
2.在并发情况下, 可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。
“指针碰撞”(Bump the Pointer)(默认用指针碰撞)
如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
“空闲列表”(Free List)
如果Java堆中的内存并不是规整的,已使用的内存和空 闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记 录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录
CAS(compare and swap)
虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。
本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)
把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。通过 XX:+/UseTLAB
参数来设定虚拟机是否使用TLAB(JVM会默认开启 XX:+UseTLAB
), XX:TLABSize 指定TLAB大小。
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头), 如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头Object Header之中。
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、 实例数据(Instance Data)
和对齐填充(Padding)。 HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时 间戳等。对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
执行方法,即对象按照程序员的意愿进行初始化。对应到语言层面上讲,就是为属性赋值(注意,这与上面的赋零值不同,这是由程序员赋的值),和执行构造方法。
在HotSpot虚拟机里对象内存布局分为3个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
- 第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这部分数据的长度在32位和64位的虚拟机(未开启压缩指针) 中分别为32个比特和64个比特, 官方称它为“Mark Word”。
- 另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
如果对象是一个 java 数组,那么在对象头中还有一块用于记录数组长度的数据。
1)创建一个maven项目,引入依赖
org.openjdk.jol
jol-core
0.16
2)新建一个类
@Data
public class BaseEntity {
private long id;
private double amount;
private int updateUserId;
private float f;
private char c;
private byte b;
private boolean bb;
private short ss;
private long[] list; //4byte
private String s; //4byte
private Long count; //4byte
}
3)测试
public class ObjectHeaderTest {
public static void main(String[] args) {
BaseEntity baseEntity = new BaseEntity();
String toPrintable = ClassLayout.parseInstance(baseEntity).toPrintable();
System.out.println(toPrintable);
}
}
打印结果
site.sunlong.obj.BaseEntity object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf800c143
12 4 int BaseEntity.updateUserId 0
16 8 long BaseEntity.id 0
24 8 double BaseEntity.amount 0.0
32 4 float BaseEntity.f 0.0
36 2 char BaseEntity.c
38 2 short BaseEntity.ss 0
40 1 byte BaseEntity.b 0
41 1 boolean BaseEntity.bb false
42 2 (alignment/padding gap)
44 4 long[] BaseEntity.list null
48 4 java.lang.String BaseEntity.s null
52 4 java.lang.Long BaseEntity.count null
Instance size: 56 bytes
Space losses: 2 bytes internal + 0 bytes external = 2 bytes total
4)测试结果解读
数据第1行(object header: mark):这个是Mark Word占用的字节数,64位机器上占用8个字节,32位机器上占用4个字节。
数据第2行(object header: class) :类型指针,在开启指针压缩的情况下占4个字节,未开启的情况下占8个字节,jdk1.6之后默认开启指针压缩。
数据第3-14行(除(alignment/padding gap)):BaseEntity对象属性类型占用字节数,一共占用39个字节。
其他数据行:对齐填充2个字节,由于Mark Word(8个字节)+类型指针(4个字节)+对象字节数(42个字节)=54个字节,54不是8的倍数,所以要填充2个字节凑够8的倍数。如果字节数之和刚好是8的倍数,则不需要对齐填充。
父类静态代码块和静态成员变量->子类静态代码块和静态成员变量->父类代码块和普通成员变量->父类构造方法->子类代码块和普成员变量->子类构造方法
就是不再使用的对象或者变量一直占用着内存中,而且GC也无法去回收。这种情况是因为长生命周期的对象持有短生命周期的对象的引用就会发生内存泄漏情况。
堆内存溢出:(1)当对象一直创建而不被回收时(2)加载的类越来越多时(3)虚拟机栈的线程越来越多时
栈溢出:方法调用次数过多,一般是递归不当造成
在Java中,程序员不需要去指定的释放对象。而是有JVM虚拟机自行的执行,有一个垃圾回收线程(守护线程)他的优先级比较低,他不会去主动的去执行,只有虚拟机空闲
或者当前的堆空间满
的时候,才会去触发,去扫描回收掉没有用的对象。
当创建一个对象的时候GC就会进行监控这个对象的地址、大小以及使用情况 。GC通过有向图的方式记录管理堆,通过这种方式来判断那些可达,哪些不可达,从而来判定是否回收。
程序员可以手动的进行System.gc()来通知gc运行,但是GC并不能保证GC一定会执行
两种方式:
标记清除: 标记需要清除的对象,对标记的对象进行清楚操作。缺点:产生了大量的不连续碎片化对象,不利于分配新的对象。
标记整理:标记需要清除的对象,对标记的对象进行清楚操作过程中,也将没有清除的对象进行整理到内存的一端。优点:解决了标记-清理算法存在的内存碎片问题 缺点:需要局部的移动对象,降低的效率
复制算法:将内存空间分称等大的两部分,每次遍历一个区域将存货的对象放入另一个区域,然后清除掉掉这个区域所有的对象,以此类推循环。 优点:效率高速度快,也没有内存碎片的问题 缺点:堆内存大小要求高
分代算法:就是把内存空间分配成不同的年轻代、老年代和永久代,根据不同的年代使用不同的算法。
比如说 年轻代空间大而且垃圾回收发生频繁 所以采用复制算法
,老年代就采用标记整理算法
新生代采用复制算法,老年代采用标记-整理算法。单线程
,所以在发生GC时候需要暂停所有线程的工作。
新生代采用复制算法,老年代采用标记-整理算法。多线程
,Parallel Scavenge收集器关注点是**吞吐量(**高效率的 利用CPU)
新生代采用复制算法,老年代采用标记-整理算法。和Parallel 相似主要用于配合CMS收集器使用。
CMS收集器是一种 “标记-清除”算法实现的。
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户 体验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用 户线程(基本上)同时工作。
直接能引用
的对象,速度很快。不需要停顿用户线程
, 可以与垃圾收集线程一起并发运行。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。对象的内存分配通常是在 Java堆
上分配(随着虚拟机优化技术的诞生,某些场 景下也会在栈
上分配,
后面会详细介绍),对象主要分配在新生代的 Eden
区, 如果启动了本地线程缓冲,将按照线程优先在
TLAB
上分配。少数情况下也会直 接在老年代
上分配。总的来说分配规则不是百分百固定的,其细节取
决于哪一种 垃圾收集器组合以及虚拟机相关参数有关,但是虚拟机对于内存的分配还是会遵 循以下几种
对象优先在 Eden 区分配
多数情况,对象都在新生代 Eden 区分配。当 Eden 区分配没有足够的空间进行 分配时,虚拟机将会发
起一次 Minor GC。如果本次 GC后还是没有足够的空 间,则将启用分配担保机制在老年代中分配内存。
年轻代每次minor gc之前JVM都会计算下老年代剩余可用空间,如果空间不够的话就直接进行FullGC
大对象直接进入老年代
所谓大对象是指需要大量连续内存空间的对象,频繁出现大对象是致命的,会导 致在内存还有不少空间
的情况下提前触发 GC 以获取足够的连续空间来安置新对 象。
前面我们介绍过新生代使用的是标记-清除算法来处理垃圾回收的,如果大对象 直接在新生代分配就会导
致 Eden 区和两个 Survivor 区之间发生大量的内存复制。因此对于大对象都会直接在老年代进行分配。
长期存活对象将进入老年代
虚拟机采用分代收集的思想来管理内存,那 么内存回收时就必须判断哪些对象应 该放在新生代,哪些对
象应该放在老年代。因此虚拟机给每个对象定义了一个对 象年龄的计数器,如果对象在 Eden 区出生,
并且能够被 Survivor 容纳,将被 移动到 Survivor 空间中,这时设置对象年龄为 1。对象在 Survivor 区
中每「熬 过」一次 Minor GC 年龄就加 1,当年龄达到一定程度(默认 15) 就会被晋升 到老年代。
空间分配担保
动态年龄判定
类加载器主要有:
双亲委派模型就是子加载器向父级加载器向上委托它加载,如果父加载器能加载那么子加载器就不用加载直接返回,如果不能加载就有子加载器进行加载。
这样做的好处就是:
**如何打破双亲委派模型:**重写loadClass(String, boolean)
方法
第一范式:每个列都不可以再拆分。
第二范式:在第一范式的基础上,非主键列完全依赖于主键,而不能是依赖于主键的一部分。
第三范式:在第二范式的基础上,非主键列只依赖于主键,不依赖于其他非主键。
在设计数据库结构的时候,要尽量遵守三范式,如果不遵守,必须有足够的理由。比如性能。事实上我 们经常会为了性能而妥协数据库的设计。
B树的数据即存在非叶子节点上也存在于叶子节点上,而B+树所有的数据都是存在叶子节点。就是因为这个原因所以B+树存储的数据比较多,因为一个页的大小是16KB而B树的非叶子节点又存储了数据data,所以一个页锁存储的节点少一些,故如果使用B树那么层数会相应提高很多。
B+树的叶子节点还采用了双向链表的结构,这样也可以可以方便使用范围查找和所有数据的遍历。而B树有的数据存储在非叶子节点就做不到这一点,必须经过不断的回溯IO大大降低了效率
存储的文件格式: MyISAM 存储索引MYI
存储数据MYD
存储表的结构frm
InnoDB存储的索引+数据都是IDB
存储表的结构frm
索引存储方式:MyISAM 存储的是那条记录的地址(非聚簇索引)
InnoDB存储的是那条记录的所有值(聚簇索引)
事务:InnoDB支持事务,MyISAM 不支持事务
外键:InnoDB支持外键,MyISAM 不支持外键
锁:InnoDB支持行锁和表锁,MyISAM 只支持表锁
聚簇索引:就是将索引和值放在一起,根据索引可以直接获取值,
非主键索引:叶子节点存放的是数据行地址,先根据索引找到数据地址,再根据地址找数据
覆盖索引:覆盖索引就是所查找的结果集,在所查找的索引树当中已经存在了不用再进行回表操作
联合索引:多个字段联合起来的索引
要遵循最左前缀法则
使用覆盖索引,就是查询字段需要指定明确,切忌不要使用 *
in 后面中的 字段尽量少,不宜过多,发现数据较少
注意范围查询语句,一般返回查询语句的后面字段会索引失效
不建议使用%前缀模糊查询
避免在where子句中,对字段进行null判断,,这样会造成不走索引
避免where子句中对字段表达式函数的操作,字符串不加单引号
尽量避免where子句使用or 判断,因为or左右只有有一方没有索引,就会导致全表扫描
尽量使用inner join 避免使用left join因为是小表为驱动表,而left join是左表驱动右表
被驱动表:
驱动表:
通常使用小表使用驱动表,被驱动表字段如果不存在索引使用BNL算法,否则时候NLJ算法,因为他们扫描次行数相等,这个时候BNL算法的磁盘扫描而且扫描次数更少,他是先从磁盘中放入join_buffer中去,而且计算快得多。
驱动表首先会根据where字段进行过滤,然后再去跟非驱动表进行join关联的。
大表 1000 小表 100行记录
关联过程:
NLJ算法:
这个通常是在被驱动表关联字段存在索引情况下触发,首先将驱动表查询好的字段取出来(100次),然后因为有索引所以每一行查询好关联的记录去被驱动表(100)次。总共 100+100 = 200次
BNL算法:
把驱动表筛选出来的数据读取到join_buffer中去(100次),将大表的每一行取出来拿出来join_buffer每一行进行对比(100 * 1000)。如果join_buffer存不下去,那么分多次磁盘的IO读写进行和被驱动表对比。
就是当一个联合索引,第一个是模糊查询,导致后面的字段不能够使用索引 ,这个时候将索引查询到的数据进行回表操作,这个时候的回标任务比较大。所以MySQL自动优化采用了索引下推的机制,在筛选模糊查询的时候也进一步去筛选模糊查询后面的字段,这样可以大大的减少了回表的成本。
原子性:就是一个事务一系列的操作命令要么全部执行,要么全部执行失败。通过Undolog日志解决
持久性:事务一旦提交就不可改变,而且必须持久化到磁盘当中。通过Redolog日志解决
一致性:再一个事务对数据的查询结果必须是一致的。MVCC,通过Undolog日志解决
隔离性:事务和事务之间互不影响。这个我们可以通过加锁来解决
脏读
、不可重复读
、幻读
不可重复读
、幻读
幻读
空间上的代价:每建立一个索引都要为它建立一棵B+树,每一棵B+树的每一个节点都是一个数据页,一个页默认会占用 16KB 的存储空间,一棵很大的B+树由许多数据页组成,那就是很大的一片存储空间。
**时间上的代价:**每次对表中的数据进行 增、删、改
操作时,所以存储引擎需要额外的时间进行一些 记录移位 , 页面分裂 、 页面回收
等操作来维护好节点和记录的排序。如果我们建了许多索引,每个索引对应的B+树都要进行相关的维护操作
,会给性能拖后腿。
防止实例挂了 或者宕机,这样就会丢失掉那一段时间的数据。有了redolog日志就直接降低了刷盘的频率。
三种输盘策略:redo log buffer -> page cache -> 磁盘
1
提交事务立马刷盘0
不刷盘,每个1s进行刷盘操作2
不刷盘,由OS操作系统决定刷盘基于属性:
基于粒度:
MVCC是多版本控制,我们在解决隔离级别产生问题的时候,一般有两种方式
在每次的事务当中都会生成一个新版本ReadView的数据:
SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY ... LIMIT...
这条sql 的执行步骤
FROM -> JOIN -> ON -> WHERE -> GROUP BY -> HAVING ->
SELECT -> DISTINCT -> ORDER BY -> LIMIT
客户端 -> 连接器 -> 词法分析器 -> 优化器 -> 执行器 -> 存储引擎
连接器:如果用户名密码认证通过,连接器会到权限表里面查出你拥有的权限。之后,这个连接里面的权限判断逻辑,都将依赖于此时读到的权限。
char列的长度是固定的,能存取的最大的长度是0~255之间,如果存储的是utf8那么就是3*255的字节;如果是GBK那么就是2*
255
varchar分为两个部分存储,一个是存储数据一个是存储长度范围的,以前的最大长度是255 ,mysql5版本后就是0~65535(2^16)个字节 ,例外还需要1-2个字节(长度超过255时需要2个字节)来存储字符串的实际长度。
查看系统性能参数
SHOW [GLOBAL|SESSION] STATUS LIKE '参数';
统计SQL的查询成本
SHOW STATUS LIKE 'last_query_cost';
开启慢查询日志参数
set global slow_query_log='ON';
设置慢查询的时间阈值
set global long_query_time = 1;
查询有多少条慢查询的记录
SHOW GLOBAL STATUS LIKE '%Slow_queries%';
查看 SQL 执行成本
SHOW PROFILE
分析查询语句
EXPLAIN
(1)where条件中有or,除非所有查询条件都有索引,否则失效
(2)like查询用%开头,索引失效
(3)索引列参与计算,索引失效
(4)违背最左匹配原则,索引失效
(5)索引字段发生类型转换,索引失效
(6)mysql觉得全表扫描更快时(数据少),索引失效
快照持久化RDB
在默认情况下, Redis 将内存数据库快照保存在名字为 dump.rdb 的二进制文件中。
redis的默认持久化机制,通过父进程fork一个子进程,子进程可以共享主线程的所有内存数据。bgsave 子进程 运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件。此时,如果主线程对这些数据也都是读操作, 那么,主线程和 bgsave 子进程相互不影响。但是,如果主线程要修改一块数据,那么,这块数据就会被复制 一份,生成该数据的副本。然后,bgsave 子进程会把这个副本数据写入 RDB 文件,而在这个过程中,主线程 仍然 可以直接修改原来的数据。
以日志形式记录每一次的写入和删除操作,策略有每秒同步、每次操作同步、不同步,优点是数据完整性高, 缺点是运行效率低,恢复时间长
redis单线程仅局限于 他的网络IO流,和他的键值对读写操作。但是redis对于其他的功能,比如持久化、异步的删除、集群同步等操作,都是额外的线程操作的。
因为redis所有数据都是在内存中,所有的运算都是内存级别的,而且单线程避免了多线程切换的性能损耗问题。正因为 Redis 是单线程,所以要小心使用 Redis 指令,对于那些耗时的指令(比如keys),一定要谨慎使用,一不小心就可能会导致 Redis 卡顿。
Redis的IO多路复用:redis利用epoll来实现IO多路复用,将连接信息和事件放到队列中,依次放到文件事件分派器,事件分派器将事件分发给事件处理器。
缓存穿透就是查询到一些不存在的数据,本来缓存层就是为了保护存储层的,而这样直接穿透到存储层就起不到保护的作用。
**如何解决:**在缓存存这是访问的key 为 一个null的值,并设置一个过期时间。。或者提前在接口层做好校验工作
由于大批量的缓存在同一时刻失效,大量请求过来就穿透缓存
到达存储层,可能造成存储层压力过大甚至挂掉。
**如何解决:**设置一个随机的过期时间,而不是同一时刻都过期。。或者加上互斥锁。。
缓存雪崩指的是缓存层支撑不住或宕掉后,流量会像奔逃的野牛一样, 打向后端存储层。由于缓存层承载着大量请求, 有效地保护了存储层, 但是如果缓存层由于某些原因不能提供服务(比如超大并发过来,缓存层支撑不住,或者由于缓存设计不好,类似大量请求访问bigkey,导致缓存能支撑的并发急剧下降), 于是大量请求都会打到存储层, 存储层的调用量会暴增, 造成存储层也会级联宕机的情况。
非核心数据
(例如电商商品属性,用户信息等)时,暂时停止从缓存中查询这些数据,而是直接返回预定义的默认降级信息、空值或是错误提示信息;当业务应用访问的是核心数据
(例如电商商品库存)时,仍然允许查询缓存,如果缓存缺失,也可以继续通过数据库读取。预案设定
。采用的定期过期 + 惰性过期
定期删除 :Redis 每隔一段时间从设置过期时间的 key 集合中,随机抽取一些 key ,检查是否过期,如果已经过期做删除处理。
惰性删除 :Redis 在 key 被访问的时候检查 key 是否过期,如果过期则删除。
在大并发下,同时操作数据库与缓存会存在数据不一致性问题
过期时间
,每隔一段时间触发读的主动更新即可。容忍短时间的缓存数据不一致
(如商品名称,商品分类菜单等),缓存加上过期时间
依然可以解决大部分业务对于缓存的要求。不能
容忍缓存数据不一致,可以通过加读写锁
保证并发读写或写写的时候按顺序排好队,读读的时候相当于无锁。canal
通过监听数据库的binlog日志及时的去修改缓存,但是引入了新的中间件,增加了系统的复杂度。(1)主从模式:多个master节点,多个slave节点,master节点宕机slave自动变成主节点
(2)哨兵模式:在主从集群基础上添加哨兵节点或哨兵集群,用于监控master节点健康状态,通过投票机制选择slave成为主节点
(3)分片集群:主从模式和哨兵模 式解决了并发读的问题,但没有解决并发写的问题,因此有了分片集群。分片集群有多个master节点并且不同master保存不同的数据,master之间通过ping相互监测健康状态。客户端请求任意一个节点都会转发到正确节点,因为每个master都被映射到0-16384个插槽上,集群的key是根据key的hash值与插槽绑定
主从同步第一次是全量同步:slave第一次请求master节点会根据replid判断是否是第一次同步,是的话master会生成RDB发送给slave。
主从复制(全量复制)流程图:
后续为增量同步:在发送RDB期间,会产生一个缓存区间记录发送RDB期间产生的新的命令,slave节点在加载完后,会持续读取缓存区间中的数据
主从复制(部分复制,断点续传)流程图:
新master的选举需要大于半数的集群master节点同意才能选举成功,如果只有两个master节点,当其中一个挂了,是达不到选举新master的条件的。
奇数个master节点可以在满足选举该条件的基础上节省一个节点,比如三个master节点和四个master节点的集群相比,大家如果都挂了一个master节点都能选举新master节点,如果都挂了两个master节点都没法选举新master节点了,所以奇数的master节点更多的是从节省机器资源角度出发说的。
当slave发现自己的master变为FAIL状态时,便尝试进行Failover,以期成为新的master。由于挂掉的master可能会有多个slave,从而存在多个slave竞争成为master节点的过程, 其过程如下:
1.slave发现自己的master变为FAIL
2.将自己记录的集群currentEpoch加1,并广播FAILOVER_AUTH_REQUEST 信息
3.其他节点收到该信息,只有master响应,判断请求者的合法性,并发送FAILOVER_AUTH_ACK,对每一个epoch只发送一次ack
4.尝试failover的slave收集master返回的FAILOVER_AUTH_ACK
5.slave收到超过半数master的ack后变成新Master(这里解释了集群为什么至少需要三个主节点,如果只有两个,当其中一个挂了,只剩一个主节点是不能选举成功的)
6.slave广播Pong消息通知其他集群节点。
Spring是个轻量级的框架,简化了应用的开发程序,提高开发人员的系统维护性,不过配置消息比较繁琐,所以后面才出选了SpringBoot的框架。
Spring的核心组件 : Spring Core 、 Spring Context 、 Spring Beans
IOC是Bean的一个容器,Context是每个Bean之间依赖的关系。正是因为各种Bean建立的关系我们才构成了IOC容器为我们服务,而Core就是维护、建立和维护所需要的的工具。
非安全的
原型Bean
对于原型Bean,每次创建一个新对象,也就是线程之间并不存在Bean共享,自然是不会有线程安全的问题。
单例Bean
对于单例Bean,所有线程都共享一个单例实例Bean,因此是存在资源的竞争。
如果单例Bean,是一个无状态Bean,也就是线程中的操作不会对Bean的成员执行查询以外的操作,那么这个单例Bean是线程安全的。比如Spring mvc 的 Controller、Service、Dao等,这些Bean大多是无状态的,只关注于方法本身。
但是如果Bean是有状态的 那就需要开发人员自己来进行线程安全的保证,最简单的办法就是改变bean的作用域 把 "singleton"改为’‘protopyte’ 这样每次请求Bean就相当于是 new Bean() 这样就可以保证线程的安全了。
Spring中的Bean默认是单例模式的,框架并没有对bean进行多线程的封装处理。
实际上大部分时间Bean是无状态的(比如Dao) 所以说在某种程度上来说Bean其实是安全的。
**IOC:**IoC就是控制反转,是一种思想而不是技术实现,A类对象中需要B类对象,传统是自己手动new就是主动控制,IoC思想就是交给IoC容器管理,不再自己手动new,容器管理好对象以及依赖关系,需要的时候从容器里拿就行。
**IOC原理:**就是在给每一个bean进行实例化后,都要对他的属性进行填充,大多数我们都是使用@Autowire直接的填充依赖注入的,在实例化和实例化后之间会调用一个MergedBeanDefinitionPostProcessor.postProcessMergedBeanDefinition
一个寻找注入点的方法,然后在属性填充时候,首先是按照注入点的类型去寻找如果有多个选择@Qualifier
注解寻找,如果还是有多个按照@Primary
,@Priority
,最后还是没有筛选出来一个唯一的注入就按照名字
来查找
**AOP:**是面向切面的编程,AOP就是可以将那些与业务不相关但是很多业务都要调用的代码抽取出来封装起来,减少重复代码,思想就是不侵入原有代码的情况下对功能进行增强。
**原理:**SpringAOP是基于动态代理的,其中SpringAOP的底层的动态代理有两种方式,一种是对于实现接口的JDK Proxy方式,另外一种就是CgLib
Aspect
:表示切面,比如被@Aspect注解的类就是切面,可以在切面中去定义Pointcut、Advice等等Join point
:表示连接点,表示一个程序在执行过程中的一个点,比如一个方法的执行,比如一个异常的处理,在Spring AOP中,一个连接点通常表示一个方法的执行。Advice
:表示通知,表示在一个特定连接点上所采取的动作。Advice分为不同的类型,后面详细讨论,在很多AOP框架中,包括Spring,会用Interceptor拦截器来实现Advice,并且在连接点周围维护一个Interceptor链Pointcut
:表示切点,用来匹配一个或多个连接点,Advice与切点表达式是关联在一起的,Advice将会执行在和切点表达式所匹配的连接点上循环依赖就是在创建A
实例的时候里面包含着B
属性实例,所以这个时候就需要去创建B
实例,而创建B
实例过程中也包含着A
实例。 这样A
实例还在创建的过程当中,所以就导致A
和B
实例都创建不出来。
一级缓存 就是我们所说的Bean存储的经过完整的生命周期的单例池
二级缓存 缓存未经过完整的声明周期的Bean
三级缓存 缓存的是ObjectFactory其实存储的是一个动态代理类的一个实现类的拉姆达表达式,执行这个拉姆达表达式就会生成这个类的代理类。
有了这三级缓存我们就来看一看,他是如何解决的。
如果出现了循环依赖的问题,我们在创建A
的过程中,需要创建B
。这个时候就需要将A
放入三级缓存并且不会执行那个拉姆达表达式,所以这个时候需要去创建B
在创建B
时候需要去依赖A
,就直接去三级缓存中查找,并且判断需不需要进行AOP
处理,如果需要就执行那个拉姆达表达式得到代理对象,如果不需要就不需要执行,直接取出原始的对象。将取出的对象放入二级缓存中,因为这个时候A
还未经过完整的生命周期所以不能放入一级缓存。这个时候其他需要依赖A
对象的直接从二级缓存中去获取即可。当B
创建完成,A
继续执行生命周期,当A
完成了属性的注入后,就可以放入一级缓存了。
DispatcherServlet
。DispatcherServlet
根据请求信息调用并且找到对应的HandlerMapping
,解析请求对应的 Handler
。ModelAndView
Model
是返回的数据对象,View
是个逻辑上的 View
ViewResolver
会根据逻辑 View
查找实际的 View
。DispaterServlet
把返回的 Model
传给 View
(视图渲染)。spring事务是在启动类上加一个@Configuration 和 @EnableTransactionManagement注解,然后再Service的方法上面加一个@Transactional注解就可以了。Spring使用的事务隔离级别默认是使用MySQL数据库的隔离级别。
当前不存在事务:
requiresd
新建一个事务
requires_new
新建一个事务
nested
新建一个事务
mandatory
抛异常
当前存在事务:
requiresd
加入这个事务
requires_new
把当前事务挂起,然后新建一个事务
nested
记录一个SavePoint,创建一个事务作为当前事务的嵌套事务来运行 外部主事务回滚的话,子事务也会回滚,而内部子事务可以单独回滚而不影响外部主事务和其他子事务。
mandatory
加入该事务
never
以非事务方式运行,如果当前存在事务,则抛出异常。
@configuration @handmapping @getmapping @postmapping @EnableTransactionManagement
@mapperScaner @commpoent @requestBody @restcontroller
${}
是properties文件的变量占位符,用于标签属性值和SQL内部,比如${driver}会被替换成com.mysql.jdbc.Driver。
#{}
是SQL的参数占位符,实际上会被替换成 ?
@SpringBootApplication
下有三个注解:
@SpringBootConfiguration:启用 SpringBoot 的配置类,相当于是Spring中的一个配置文件,
比如说可以在这个类去注册bean,或者导入其他的配置类
@ComponentScan:用于扫描启动类包下面的所有类的注解信息,并且注册
@EnableAutoConfiguration:启用 SpringBoot 的自动配置机制,获取需要自动装配的所有配置类,读取所有的Start中的META-INF/spring.factories
,按需加载到IOC容器中。
1、数据库引擎不支持事务
2、传播机制不支持 事务,就是不以事务的情况运行
3、没有配置事务管理器数据源
2、没有被 Spring 管理
3、方法不是 public 的
@Transactional 只能用于 public 的方法上,否则事务不会失效,如果要用在非 public 方法上,可以开启 AspectJ 代理模式。
4、自身调用问题
5、异常被捕获了
6、异常的类型不匹配,默认是运行时的异常
CAP:
一致性: 在分布式环境下,一致性是指数据在多个副本之间是否能够保持一致性的特性,等同于所有的节点访问同一份最新数据的副本。在一致性的需求下,当一个系统在数据一致的状态下执行更新操作后,应该保证数据仍然一致性的状态。
可用性:每次请求都能够保证正确的响应,但是不保证获取的数据为最新的数据。
分区容错性:分布式情况下在遇到任何的网络分区额故障的时候,仍然需要能够保证对外提供一致性和可用性的服务,除非是整个网络环境发生了故障。
BASE:
基本可用:在分布式系统出现故障,允许损失部分的可用性(服务降级、页面降级)
软可用:允许分布式出现中间状态。而且中间状态不影响系统的可用性。这里的中间状态指的是data replication(数据备份节点)之间的数据更新可以出现延迟的最终一致性。
最终一致性:data replications 经过一段时间达到一致性。
1、任意时刻,所有的节点数据都是一样的。
2、一个集群需要对外部提供强一致性,所以只要集群内部的某一个数据发生了改变,那么就需要等待集群内其他服务器的数据同步完成后,才能对外部提供访问。
3、保证了强一致性,务必会损耗可用性
1、系统中的某个数据被更新后,后续对该数据的读取操作可能会得到更新后的数值,也可能会得到更新前的数值
2、即使过来一段时间窗口,后续的读取也不能保证一致性
1、弱一致性的特殊形式,不保证在任意时刻节点上的同一份数据是相同的,但是随着时间的迁移,不同节点的同一份数据总是在趋向一致性变化
2、存储系统保证在没有新的更新的条件下,最终所有的访问都是最后更新的值
1、任意一次读都能读到某个数据的最近一次写的数据
2、对其他节点之前的修改是可见的(已同步)且确定的,并且新的写入建立在已经达到同步的基础上
高并发、高可用、高性能
分为应用层,传输层,网络层,数据链路层
网络的七层架构 :应用层,表示层,会话层,传输层,网络层,数据链路层,物理层
TCP/IP是面向连接的协议,发送数据前要先建立好连接,TCP提供了可靠的服务,也就是说通过TCP连接传输的数据是不会丢失,没有重复,并且按照顺序达到的。
UDP是无连接的协议,发送数据前是不需要建立连接的,没有可靠的协议。所以传输的过程中可以以任意的路径进行传输,并且传输的过程中是否能达到和到达的时间都是没有保障的。
不可以,如果采用两次握手的话,那么只要服务端确认就建立了连接。如果client发送了一个请求过来,延迟了一个小时,一个小时候服务端接受到消息确认,他们建立了连接,但是这个时候client已经关闭了。但是服务端不知道。就会造成服务端一直发送数据,等待客户端的回消息,白白浪费许多资源。所以必须需要第三次握手的客户端确认。
这个肯定可以。三次握手都可以保证连接成功了,何况是四次,但是会降低传输的效率。
第一次挥手:Client将FIN置为1,发送一个序列号seq给Server;进入FIN_WAIT_1状态;
第二次挥手:Server收到FIN之后,发送一个ACK=1,acknowledge number=收到的序列号+1;
进入CLOSE_WAIT状态。此时客户端已经没有要发送的数据了,但仍可以接受服务器发来的数据。
第三次挥手:Server将FIN置1,发送一个序列号给Client;进入LAST_ACK状态;
第四次挥手:Client收到服务器的FIN后,进入TIME_WAIT状态;接着将ACK置1,发送一个
acknowledge number=序列号+1给服务器;服务器收到后,确认acknowledge number后,变为
CLOSED状态,不再向客户端发送数据。客户端等待2*MSL(报文段最长寿命)时间后,也进入
CLOSED状态。完成四次挥手。
其实HTTPS就是从HTTP加上加密处理(一般是SSL安全通信线路)+认证+完整性保护
区别:
什么是cookie
cookie是由Web服务器保存在用户浏览器上的文件(key-value格式),可以包含用户相关的信
息。客户端向服务器发起请求,就提取浏览器中的用户信息由http发送给服务器
什么是session
session 是浏览器和服务器会话过程中,服务器会分配的一块储存空间给session。
服务器默认为客户浏览器的cookie中设置 sessionid,这个sessionid就和cookie对应,浏览器在向
服务器请求过程中传输的cookie 包含 sessionid ,服务器根据传输cookie 中的 sessionid 获取出
会话中存储的信息,然后确定会话的身份信息。
1、校验和(数据准确性)
2、序列号和确认应答(数据确认达到和有序)
3、超时重传(防止数据丢失)
(1)数据在传输过程中由于网络原因等直接全体丢包,接收方根本没有接收到。
(2)接收方接收到了响应的数据,但是发送的ACK报文响应却由于网络原因丢包了。
TCP在解决这个问题的时候引入了一个新的机制,叫做超时重传机制。**简单理解就是发送方在发送完数据后等待一个时间,时间到达没有接收到ACK报文,那么对刚才发送的数据进行重新发送。**如果是刚才第一个原因,接收方收到二次重发的数据后,便进行ACK应答。如果是第二个原因,接收方发现接收的数据已存在(判断存在的根据就是序列号,所以上面说序列号还有去除重复数据的作用),那么直接丢弃,仍旧发送ACK应答。
4、连接管理
连接管理就是三次握手与四次挥手的过程,保证可靠的连接,是保证可靠性的前提。
消息手动确认消费
RabbitMQ 持久化 消息持久化(发布确认方式) + 队列持久化(直接设置参数)
5、流量控制
6、拥塞控制
。