目录
一、基础
1.继承、封装、多态
继承
将若干类中相同的属性、方法抽取出来创建一个父类,其他的类不用在单独写共同的属性、方法。而只需要继承父类即可,继承可以使得子类具有父类的属性和方法或者重新定义、追加属性和方法等
封装就是把抽象出的数据[属性]和对数据的操作[方法]封装在一起,数据被保护在内部,程序的其他部分只有通过被授权的操作[方法],才能对数据进行操作。
封装的实现
将属性进行私有化private
提供一个公开的set方法,用于对属性判断并赋值
提供一个公开的get方法,用于获取属性的值
多态:就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会出现不同的状态。
多态的实现条件:
1.必须在继承体系下
2.子类必须要对父类中方法进行重写
3.通过父类的引用调用重写的方法
2.== 和 equals 的区别
== :基本类型:比较的是值是否相同;引用类型:比较的是引用是否相同;
equals :重写前比较值,重写后比较内存地址
3.String、StringBuffer、StringBuilder。
String 和 StringBuffer、StringBuilder 的区别在于 String 声明的是不可变的对象,每次操作都会生成新的 String 对象,然后将指针指向新的 String 对象,而 StringBuffer、StringBuilder 可以在原有对象的基础上进行操作,所以在经常改变字符串内容的情况下最好不要使用 String。
StringBuffer 和 StringBuilder 最大的区别在于,StringBuffer 是线程安全的,而 StringBuilder 是非线程安全的,但 StringBuilder 的性能却高于 StringBuffer,所以在单线程环境下推荐使用 StringBuilder,多线程环境下推荐使用 StringBuffer。
4.. 普通类和抽象类有哪些区别?
普通类不能包含抽象方法,抽象类可以包含抽象方法。
抽象类不能直接实例化,普通类可以直接实例化。
5.接口和抽象类有什么区别?
实现:抽象类的子类使用 extends 来继承;接口必须使用 implements 来实现接口。
构造函数:抽象类可以有构造函数;接口不能有。
实现数量:类可以实现很多个接口;但是只能继承一个抽象类。
访问修饰符:接口中的方法默认使用 public 修饰;抽象类中的方法可以是任意访问修饰符。
6.BIO、NIO、AIO 的区别?
BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。
NIO:Non IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。
AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。
BIO以流的方式处理数据,而NIO以块的方式处理数据,块I/O的效率比流I/O高很多
BIO是阻塞的,NIO则是非阻塞的
BIO基于字节流和字符流进行操作,而NIO基于Channel(通道)和Buffer(缓冲区)进行操作,数据总是从通道
读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道
与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可,这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序
即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。在JDK1.7中,这部分内容被称作NIO 2.0,主要在Java.nio.channels包下增加了下面四个异步通道:
AsynchronousSocketChannel
AsynchronousServerSocketChannel
AsynchronousFileChannel
AsynchronousDatagramChannel
IO多路复用是一种高效的IO处理方式,它允许一个进程或线程同时监视多个文件描述符(通常是套接字),并在有数据可读或可写时进行相应的处理,而不需要阻塞等待。这种方式可以大大提高系统的并发性能。
常见的IO多路复用机制有以下几种:
1. select:select函数是最早出现的IO多路复用机制,它通过一个位图来表示需要监视的文件描述符集合,并通过轮询的方式检查每个文件描述符的状态变化。
2. poll:poll函数是select的改进版本,它使用链表来存储需要监视的文件描述符集合,避免了select中位图大小的限制。单进程监听的 FD 存在限制,默认1024
3. epoll:epoll是Linux特有的IO多路复用机制,它使用事件驱动的方式来处理IO事件。通过epoll_ctl函数注册需要监视的文件描述符,并通过epoll_wait函数等待事件发生。
使用IO多路复用可以有效地减少系统调用次数和线程切换次数,提高系统的并发性能和响应速度。
6.Java 容器分为 Collection 和 Map 两大类,其下又有很多子类,如下所示:
Collection
List
ArrayList
LinkedList
Vector(Vector 使用了 Synchronized 来实现线程同步,是线程安全的)
Stack
Set
HashSet
LinkedHashSet
TreeSet
Map
HashMap
LinkedHashMap
TreeMap
ConcurrentHashMap
Hashtable
7.HashMap 的实现原理?
1、在JDK7之前,hashmap底层采用数组+链表的数据结构来存储数据
2、插入数据采用头插法,头插法效率更高,不需要去遍历链表。插入结点后将头结点移到数组下标的位置
3、扩容不同,在jdk7中,发生扩容,它会把原来所有的元素重新计算hash。再插入到新的位置。而jdk8中则是直接copy过去,要么位置不变,要么位置更改为索引+原数组长度
HashMap 基于 Hash 算法实现的,我们通过 put(key,value)存储,get(key)来获取。当传入 key 时,HashMap 会根据 key. hashCode() 计算出 hash 值,根据 hash 值将 value 保存在 bucket 里当计算出的 hash 值相同时,我们称之为 hash 冲突,HashMap 的做法是用链表和红黑树存储相同 hash 值的 value。当 hash 冲突的个数比较少时,使用链表否则使用红黑树。
1、JDK8以后hashmap的底层数据结构由数据+链表+红黑树实现
2、jdk8以后插入数据采用尾插法。因为引入了树形结构,总是要遍历的 当进行put操作的时候,当链表的长度大于或等于8时,会将链表转化为红黑树 在进行remove操作的时候,
当红黑树的节点个数小于或者等于6时,会将红黑树转化为链表
初始化数组长度为 0,元素数量大于 12 时, 就会进行扩容。
并允许使用null值和null键
HashMap 在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据hash算法找到其在数组中的存储位置,再根据equals方法
开放定址法:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中
再哈希法:同时构造多个不同的哈希函数,当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。
链地址法:这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。
建立公共溢出区:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。
Hashtable原理:
Hashtable 是一个散列表,它存储的内容是键值对(key-value)映射。
Hashtable 继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口。
Hashtable 的函数都是同步的,这意味着它是线程安全的。它的key、value都不可以为null。
HashTable采用"拉链法"实现哈希表,它定义了几个重要的参数:table、count、threshold、loadFactor、modCount。
table:为一个Entry[]数组类型,Entry代表了“拉链”的节点,每一个Entry代表了一个键值对,哈希表的"key-value键值对"都是存储在Entry数组中的。
count:HashTable的大小,注意这个大小并不是HashTable的容器大小,而是他所包含Entry键值对的数量。
threshold:Hashtable的阈值,用于判断是否需要调整Hashtable的容量。threshold的值="容量*加载因子"。
loadFactor:加载因子。
modCount:用来实现“fail-fast”机制的(也就是快速失败)。所谓快速失败就是在并发集合中,其进行迭代操作时,若有其他线程对其进行结构性的修改,这时迭代器会立马感知到,并且立即抛出ConcurrentModificationException异常,而不是等到迭代完成之后才告诉你
在HashTabel中存在5个构造函数。通过这5个构造函数我们构建出一个我想要的HashTable 默认构造函数,容量为11,加载因子为0.75
从该位置上的链表中取出该Entry。
8. HashSet 的实现原理?
HashSet 是基于 HashMap 实现的,HashSet 底层使用 HashMap 来保存所有元素,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,HashSet 不允许重复的值
9.sleep() 和 wait() 有什么区别?
类的不同:sleep() 来自 Thread,wait() 来自 Object。
释放锁:sleep() 不释放锁;wait() 释放锁。
用法不同:sleep() 时间到会自动恢复;wait() 可以使用 notify()/notifyAll()直接唤醒。
10.线程池都有哪些状态?
RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。
SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。
STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。
TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。
TERMINATED:terminated()方法结束后,线程池的状态就会变成这个。
11. 线程池中 submit() 和 execute() 方法有什么区别?
execute():只能执行 Runnable 类型的任务。
submit():可以执行 Runnable 和 Callable 类型的任务。
多线程中 synchronized 锁升级的原理是什么?
12.synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。
锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。
13.synchronized 底层实现原理?
synchronized 是由一对 monitorenter/monitorexit 指令实现的,monitor 对象是同步的基本实现单元。在 Java 6 之前,monitor 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作,性能也很低。但在 Java 6 的时候,Java 虚拟机 对此进行了大刀阔斧地改进,提供了三种不同的 monitor 实现,也就是常说的三种不同的锁:偏向锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。
14.synchronized 和 Lock 的区别?
synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
synchronized
用的锁是存在Java对象头里的。那么什么是 Java 对象头呢?Hotspot 虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。其中:
-
Klass Point 是是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
Mark Word 用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等
ArrayList 提供了三种方式的构造器:
可以构造一个默认初始容量为 10 的空列表(在添加第一个元素的时候默认初始化容量为10,而不是再创建对象的时候就初始化容量为10,,JDK1.7创建对象的时候就会默认初始化创建容量为10,jdk8为0 的数组)
构造一个指定初始容量的空列表
构造一个包含指定 collection 的元素的列表
每次数组容量的增长是其原容量的 1.5 倍。同时需要将原有数组中的数据复制到新的数组中。
如果是list.addAll(Collection)添加元素,如果容量不够,像扩容1.5倍, 如果仍然不够,则将原来数组的长度与新添加的集合的长度相加作为新的集合的长度
jdk7和jdk8中通过Vector()构造器创建对象时,底层都创建了长度为10的数组。在扩容方面,默认扩容为原来的数组长度的2倍。
1.1.继承Tread类实现线程
步骤:创建一个类继承Tread类--->重写Tread类中的run()方法---->用继承类创建一个个对象(每个对象代表一个线程)---->调用线程对象的start()方法---->多个线程启动了
2.实现Runnable接口实现线程
步骤:创建一个类实现Runnable接口---->实现Runnable接口的run()方法----->创建继承类对象---->采用代理的方式启动线程(new Tread( 继承类对象 ).start() )---->线程启动了
3.实现Callable接口实现线程 创建一个类实现Callable接口---->实现Callable接口的call()方法--->将继承类对象作为参数传入FutureTask对象中---->采用代理的方式启动线程(new Tread( FuturnTask对象 ).start() )---->线程启动了
volatile的三大特性
1. 可见性
保证不同线程对这个变量进行操作时的可见性,即变量一旦改变所有线程立即可见
使用volatile修饰共享变量,被volatile修改的变量有以下特点:
- 1.线程中读取的时候,每次读取都会去主内存中读取共享变量最新的值,然后将其复制到工作内存
- 2.线程中修改了工作内存中变量的副本,修改之后会立即刷新到主内存
- 从volatile变量的读写过程分析 要use(使用)一个变量的时候必需load(载入),要载入的时候必需从主内存read(读取)这样就解决了读的可见性。写操作是把assign和store做了关联(在assign(赋值)后必需store(存储)),store(存储)后write(写入)。 也就是做到了给一个变量赋值的时候一串关联指令直接把变量值写到主内存。 就这样通过用的时候直接从主内存取,在赋值到直接写回主内存做到了内存可见性
- 1.volatile修饰的变量进行写操作的时候,JVM就会向CPU发送LOCK#前缀指令,此时当前处理器的缓存行就会被锁定,通过缓存一致性机制确保修改的原子性,然后更新对应的主存地址的数据。
- 2.处理器会使用嗅探技术保证在当前处理器缓存行,主存和其他处理器缓存行的数据的在总线上保持一致。在JVM通过LOCK前缀指令更新了当前处理器的数据之后,其他处理器就会嗅探到数据不一致,从而使当前缓存行失效,当需要用到该数据时直接去内存中读取,保证读取到的数据时修改后的值。
2.有序性:volatile通过禁止指令重排序来保证程序的执行顺序。这意味着编译器和处理器不会对volatile变量的读写指令进行重排序,确保了程序代码在并发执行时的执行顺序与编写时的一致性。
3.原子性:对于单个volatile修饰的变量,其读写操作是原子的。但是,对于复合操作如i++,volatile并不能保证其原子性。这意味着volatile不能保证整个复合操作的原子性,但可以保证单个变量的读写是原子的
IO多路复用:全网最详细的 I/O 多路复用解析_io多路复用-CSDN博客
AQS和CAS:两个重要的实现模型 AQS和CAS_cas模型-CSDN博客
java锁
悲观锁和乐观锁
悲观锁: 指的是数据对外界的修改采取保守策略,它认为线程很容易会把数据修改掉,因此在整个数据被修改的过程中都会采取锁定状态,直到一个线程使用完,其他线程才可以继续使用。
Java 中的乐观锁大部分都是通过 CAS(Compare And Swap,比较并交换)操作实现的,CAS 是一个多线程同步的原子指令,CAS 操作包含三个重要的信息,即内存位置、预期原值和新值。如果内存位置的值和预期的原值相等的话,那么就可以把该位置的值更新为新值,否则不做任何修改。
CAS 可能会造成 ABA 的问题,ABA 问题指的是,线程拿到了最初的预期原值 A,然而在将要进行 CAS 的时候,被其他线程抢占了执行权,把此值从 A 变成了 B,然后其他线程又把此值从 B 变成了 A,然而此时的 A 值已经并非原来的 A 值了,但最初的线程并不知道这个情况,在它进行 CAS 的时候,只对比了预期原值为 A 就进行了修改,这就造成了 ABA 的问题。
ABA 的常见处理方式是添加版本号或者时间戳,每次修改之后更新版本号,每次修改之前校验版本号是否被修改过,修改过则需要重新获取值再修改,这样就解决了 ABA 的问题。
JDK 在 1.5 时提供了 AtomicStampedReference 类也可以解决 ABA 的问题,此类维护了一个“版本号” Stamp,每次在比较时不止比较当前值还比较版本号,这样就解决了 ABA 的问题。
可重入锁
可重入锁也叫递归锁,指的是同一个线程,如果外面的函数拥有此锁之后,内层的函数也可以继续获取该锁。在 Java 语言中 ReentrantLock 和 synchronized 都是可重入锁。
可重入锁的实现原理,是在锁内部存储了一个线程标识,用于判断当前的锁属于哪个线程,并且锁的内部维护了一个计数器,当锁空闲时此计数器的值为 0,当被线程占用和重入时分别加 1,当锁被释放时计数器减 1,直到减到 0 时表示此锁为空闲状态。
共享锁和独占锁
只能被单线程持有的锁叫独占锁,可以被多线程持有的锁叫共享锁。
独占锁指的是在任何时候最多只能有一个线程持有该锁,比如 synchronized 就是独占锁,而 ReadWriteLock 读写锁允许同一时间内有多个线程进行读操作,它就属于共享锁。
独占锁可以理解为悲观锁,当每次访问资源时都要加上互斥锁,而共享锁可以理解为乐观锁,它放宽了加锁的条件,允许多线程同时访问该资源。
自适应自旋锁
JDK 1.5 在升级为 JDK 1.6 时,HotSpot 虚拟机团队在锁的优化上下了很大功夫,比如实现了自适应式自旋锁、锁升级等。
JDK 1.6 引入了自适应式自旋锁意味着自旋的时间不再是固定的时间了,比如在同一个锁对象上,如果通过自旋等待成功获取了锁,那么虚拟机就会认为,它下一次很有可能也会成功 (通过自旋获取到锁),因此允许自旋等待的时间会相对的比较长,而当某个锁通过自旋很少成功获得过锁,那么以后在获取该锁时,可能会直接忽略掉自旋的过程,以避免浪费 CPU 的资源,这就是自适应自旋锁的功能。
锁升级
锁升级其实就是从偏向锁到轻量级锁再到重量级锁升级的过程,这是 JDK 1.6 提供的优化功能,也称之为锁膨胀。
序号 | 锁名称 | 应用 |
---|---|---|
1 | 乐观锁 | CAS |
2 | 悲观锁 | synchronized、vector、hashtable |
3 | 自旋锁 | CAS |
4 | 可重入锁 | synchronized、Reentrantlock、Lock |
5 | 读写锁 | ReentrantReadWriteLock,CopyOnWriteArrayList、CopyOnWriteArraySet |
6 | 公平锁 | Reentrantlock(true) |
7 | 非公平锁 | synchronized、reentrantlock(false) |
8 | 共享锁 | ReentrantReadWriteLock中读锁 |
9 | 独占锁 | synchronized、vector、hashtable、ReentrantReadWriteLock中写锁 |
10 | 重量级锁 | synchronized |
11 | 轻量级锁 | 锁优化技术 |
12 | 偏向锁 | 锁优化技术 |
13 | 分段锁 | concurrentHashMap |
14 | 互斥锁 | synchronized |
15 | 同步锁 | synchronized |
16 | 死锁 | 相互请求对方的资源 |
17 | 锁粗化 | 锁优化技术 |
18 | 锁消除 | 锁优化技术 |
加锁的完整流程
分布式锁
https://blog.csdn.net/a745233700/article/details/88084219
zookeeper分布式锁的实现
zookeeper就是通过临时节点和节点有序来实现分布式锁的。
1、每个获取锁的线程会在zk的某一个目录下创建一个临时有序的节点。
2、节点创建成功后,判断当前线程创建的节点的序号是否是最小的。
3、如果序号是最小的,那么获取锁成功。
4、如果序号不是最小的,则对他序号的前一个节点添加事件监听。如果前一个节点被删了(锁被释放了),那么就会唤醒当前节点,则成功获取到锁。
zookeeper
优点:
1、不用设置过期时间
2、事件监听机制,加锁失败后,可以等待锁释放
缺点:
1、性能不如redis
2、当网络不稳定时,可能会有多个节点同时获取锁问题。例:node1由于网络波动,导致zk将其删除,刚好node2获取到锁,那么此时node1和node2两者都会获取到锁。
Redis
优点:性能上比较好,天然的支持高并发
缺点:
1、获取锁失败后,得轮询的去获取锁
2、大多数情况下redis无法保证数据强一致性
具体流程
1.一把分布式锁通常使用一个 Znode 节点表示;如果锁对应的 Znode 节点不存在,首先创建 Znode 节点。这里假设为 /test/lock,代表了一把需要创建的分布式锁。
2.抢占锁的所有客户端,使用锁的 Znode 节点的子节点列表来表示;如果某个客户端需要占用锁,则在 /test/lock 下创建一个临时顺序的子节点。比如,如果子节点的前缀为 /test/lock/seq-,则第一次抢锁对应的子节点为 /test/lock/seq-000000001,第二次抢锁对应的子节点为 /test/lock/seq-000000002,以此类推。
3.当客户端创建子节点后,需要进行判断:自己创建的子节点,是否为当前子节点列表中序号最小的子节点。如果是,则加锁成功;如果不是,则监听前一个 Znode 子节点变更消息,等待前一个节点释放锁。
4.一旦队列中的后面的节点,获得前一个子节点变更通知,则开始进行判断,判断自己是否为当前子节点列表中序号最小的子节点,如果是,则认为加锁成功;如果不是,则持续监听,一直到获得锁。
5.获取锁后,开始处理业务流程。完成业务流程后,删除自己的对应的子节点,完成释放锁的工作,以方面后继节点能捕获到节点变更通知,获得分布式锁。
二、Redis
数据结构的存储:
* String
1 | SET key value 设置指定 key 的值。 |
2 | GET key 获取指定 key 的值。 |
3 | GETRANGE key start end 返回 key 中字符串值的子字符 |
4 | GETSET key value 将给定 key 的值设为 value ,并返回 key 的旧值(old value)。 |
5 | GETBIT key offset 对 key 所储存的字符串值,获取指定偏移量上的位(bit)。 |
6 | MGET key1 [key2..] 获取所有(一个或多个)给定 key 的值。 |
7 | SETBIT key offset value 对 key 所储存的字符串值,设置或清除指定偏移量上的位(bit)。 |
8 | SETEX key seconds value 将值 value 关联到 key ,并将 key 的过期时间设为 seconds (以秒为单位)。 |
9 | SETNX key value 只有在 key 不存在时设置 key 的值。 |
10 | SETRANGE key offset value 用 value 参数覆写给定 key 所储存的字符串值,从偏移量 offset 开始。 |
11 | STRLEN key 返回 key 所储存的字符串值的长度。 |
12 | MSET key value [key value ...] 同时设置一个或多个 key-value 对。 |
13 | MSETNX key value [key value ...] 同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。 |
14 | PSETEX key milliseconds value 这个命令和 SETEX 命令相似,但它以毫秒为单位设置 key 的生存时间,而不是像 SETEX 命令那样,以秒为单位。 |
15 | INCR key 将 key 中储存的数字值增一。 |
16 | INCRBY key increment 将 key 所储存的值加上给定的增量值(increment) 。 |
17 | INCRBYFLOAT key increment 将 key 所储存的值加上给定的浮点增量值(increment) 。 |
18 | DECR key 将 key 中储存的数字值减一。 |
19 | DECRBY key decrement key 所储存的值减去给定的减量值(decrement) 。 |
20 | APPEND key value 如果 key 已经存在并且是一个字符串, APPEND 命令将指定的 value 追加到该 key 原来值(value)的末尾。 |
* Hash
* List
* Set
* SortedSet
* Bitmap
8种内存淘汰策略供应用程序选择,用于我遇到内存不足时该如何决策:
1、noeviction:不进行淘汰数据。一旦缓存被写满,再有写请求进来,Redis就不再提供服务,而是直接返回错误。Redis 用作缓存时,实际的数据集通常都是大于缓存容量的,总会有新的数据要写入缓存,这个策略本身不淘汰数据,也就不会腾出新的缓存空间,我们不把它用在 Redis 缓存中。
2、volatile-ttl:在设置了过期时间的键值对中,移除即将过期的键值对。
3、volatile-random:在设置了过期时间的键值对中,随机移除某个键值对。
4、volatile-lru:在设置了过期时间的键值对中,移除最近最少使用的键值对。
5、volatile-lfu:在设置了过期时间的键值对中,移除最近最不频繁使用的键值对
6、allkeys-random:在所有键值对中,随机移除某个key。
7、allkeys-lru:在所有的键值对中,移除最近最少使用的键值对。
8、allkeys-lfu:在所有的键值对中,移除最近最不频繁使用的键值对
1.缓冲穿透
redis中没有这样的数据,无法进行拦截,直接被穿透到数据库,导致数据库压力过大宕机。
解决方案
对不存在的数据缓存到redis中,设置key,value值为null(不管是数据未null还是系统bug问题),并设置一个短期过期时间段,避免过期时间过长影响正常用户使用。
拉黑该IP地址
对参数进行校验,不合法参数进行拦截
布隆过滤器 将所有可能存在的数据哈希到一个足够大的bitmap(位图)中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
2.缓存击穿
某一个热点key,在不停地扛着高并发,当这个热点key在失效的一瞬间,持续的高并发访问就击破缓存直接访问数据库,导致数据库宕机。
解决方案
设置热点数据"永不过期"
加上互斥锁:上面的现象是多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个互斥锁来锁住它
其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后将数据放到redis缓存起来。后面的线程进来发现已经有缓存了,就直接走缓存
击穿:redis中有需要的数据但是过期或者被淘汰了,导致直接全部访问数据库;
穿透:redis中没有需要的数据,而且数据库中也没有需要的数据。
缓存雪崩:
缓存雪崩是指大量的应用请求无法在 Redis 缓存中进行处理,紧接着,应用将大量请求发送到数据库层,导致数据库层的压力激增。
1、避免给大量的数据设置相同的过期时间,设置随机的过期时间
2、通过服务降级(针对不同的数据采取不同的处理方式)
Redis四种部署方式
单机模式:单机模式的缺点比较明显,高性能受限于CPU的处理能力;可靠性不强,如果出现宕机,则造成服务不可用,一般情况下不建议使用。
主从模式:
优点
读写分离:分担服务器压力,从节点可以拓展主库节点的读能力。
高可靠性:主库发生故障,可以进行主备切换,保证服务平稳运行;合理备份,可以解决数据丢失。
缺点
故障恢复复杂:如果没有HA系统,主库故障,先需要手动将一个节点晋升为主节点,再需要通知业务方变更配置,其次让其他从节点复制新主库节点。
主库的写与存储受单机限制。
redis-Sentinel(哨兵模式)是Redis官方推荐的高可用性(HA)解决方案,当用Redis做Master-slave的高可用方案时,假如master宕机了,Redis本身(包括它的很多客户端)都没有实现自动进行主备切换,而Redis-sentinel本身也是一个独立运行的进程,它能监控多个master-slave集群,发现master宕机后能进行切换。哨兵集群节点数为奇数个,最少3个。
缺点:当一台主节点的机子因为某些原因无法进行连接了,哨兵以为它挂了,就重新选举出一台新的机子作为主节点,但后续之前那台机子又恢复过来了,这样整个集群就有了两个主节点了,需要将之前的节点降级为从节点。
哨兵模式 sentinel
以上单实例存redis会存在几个问题:
1)写并发: Redis单实例读写分离可以解决读操作的负载均衡,但对于写操作,仍然是全部落在了master节点上面,在海量数据高并发场景,一个节点写数据容易出现瓶颈,造成master节点的压力上升。
2)海量数据的存储压力: 单实例Redis本质上只有一台Master作为存储,如果面对海量数据的存储,一台Redis的服务器就应付不过来了,而且数据量太大意味着持久化成本高,严重时可能会阻塞服务器,造成服务请求成功率下降,降低服务的稳定性。
针对以上的问题, 提供了较为完善的方案,解决了存储能力受到单机限制,写操作无法负载均衡的问题。
哨兵的主要工作任务:
(1)监控:哨兵会不断地检查你的Master和Slave是否运作正常。
(2)提醒:当被监控的某个Redis节点出现问题时,哨兵可以通过 API 向管理员或者其他应用程序发送通知。
(3)自动故障迁移:当一个Master不能正常工作时,哨兵会进行自动故障迁移操作,将失效Master的其中一个Slave升级为新的Master,并让失效Master的其他Slave改为复制新的Master;当客户端试图连接失效的Master时,集群也会向客户端返回新Master的地址,使得集群可以使用新Master代替失效Master
cluster集群模式:在redis3.0版本中支持了cluster集群部署的方式,这种集群部署的方式能自动将数据进行分片,每个master上放一部分数据,提供了内置的高可用服务,即使某个master挂了,服务还可以正常地提供。
1、redis主从全量复制的流程:
Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份,具体步骤如下:
(1)slave发送sync到master服务器
(2)master收到sync命令之后,执行bgsave命令生成RDB快照文件并使用缓存区记录此后执行的所有写命令
如果master收到了多个slave并发连接请求,它只会进行一次持久化,而不是每个连接都执行一次,然后再把这一份持久化的数据发送给多个并发连接的slave。
如果RDB复制时间超过60秒(repl-timeout),那么slave服务器就会认为复制失败,可以适当调节大这个参数
(3)master执行完bgsave之后,就会向所有Slava服务器发送快照文件,并在发送期间继续在缓冲区内记录被执行的写命令
(4)slave收到RDB快照文件后,会将接收到的数据写入磁盘,然后清空所有旧数据,在从本地磁盘载入收到的快照到内存中,同时基于旧的数据版本对外提供服务。
(5)master发送完RDB快照文件之后,便开始向slave服务器发送缓冲区中的写命令
(6)slave服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;
(7)如果slave node开启了AOF,那么会立即执行BGREWRITEAOF,重写AOF
二者区别
RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程就是有一个fork子进程,先将数据集写入到临时文件中,写入成功后,再替换之前的文件,用二进制压缩存储。
AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。
RDB的优点:简称“3更”
1.体积更小:相同的数据量rdb数据比aof的小,因为rdb是紧凑型文件
2.恢复更快:因为rdb是数据的快照,基本上就是数据的复制,不用重新读取再写入内存
3.性能更高:父进程在保存rdb时候只需要fork一个子进程,无需父进程的进行其他io操作,也保证了服务器的性能。
缺点:
1.故障丢失:因为rdb是全量的,我们一般是使用shell脚本实现30分钟或者1小时或者每天对redis进行rdb备份,(注,也可以是用自带的策略),但是最少也要5分钟进行一次的备份,所以当服务死掉后,最少也要丢失5分钟的数据。
2.耐久性差:相对aof的异步策略来说,因为rdb的复制是全量的,即使是fork的子进程来进行备份,当数据量很大的时候对磁盘的消耗也是不可忽视的,尤其在访问量很高的时候,fork的时间也会延长,导致cpu吃紧,耐久性相对较差。
aof的优点
1.数据保证:我们可以设置fsync策略,一般默认是everysec,也可以设置每次写入追加,所以即使服务死掉了,咱们也最多丢失一秒数据
2.自动缩小:当aof文件大小到达一定程度的时候,后台会自动的去执行aof重写,此过程不会影响主进程,重写完成后,新的写入将会写到新的aof中,旧的就会被删除掉。但是此条如果拿出来对比rdb的话还是没有必要算成优点,只是官网显示成优点而已。
缺点呢:和rdb相反嘛,毕竟只有两种。
1.性能相对较差:它的操作模式决定了它会对redis的性能有所损耗
2.体积相对更大:尽管是将aof文件重写了,但是毕竟是操作过程和操作结果仍然有很大的差别,体积也毋庸置疑的更大。
3.恢复速度更慢:
Redis的高并发和快速原因
1.redis是基于内存的,内存的读写速度非常快;
2.redis是单线程的,省去了很多上下文切换线程的时间;
3.redis使用多路复用技术,可以处理并发的连接。非阻塞IO 内部实现采用epoll,采用了epoll+自己实现的简单的事件框架。epoll中的读、写、关闭、连接都转化成了事件,然后利用epoll的多路复用特性,绝不在io上浪费一点时间。
Redisson的看门狗机制
如果你想让Redisson启动看门狗机制,你就不能自己在获取锁的时候,定义超时释放锁的时间,无论,你是通过lock() (void lock(long leaseTime, TimeUnit unit);)还是通过tryLock获取锁,只要在参数中,不传入releastime,就会开启看门狗机制,
就是这两个方法不要用: boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException
和void lock(long leaseTime, TimeUnit unit);,因为它俩都传release
但是,你传的leaseTime是-1,也是会开启看门狗机制的,具体在源码部分解释
不传leaseTime或者传-1时,Redisson配置中默认给你的30秒
默认情况下,看门狗的续期时间是30s,也可以通过修改Config.lockWatchdogTimeout来另行指定。
watch dog 在当前节点存活时每10s给分布式锁的key续期 30s;
watch dog 机制启动,且代码中没有释放锁操作时,watch dog 会不断的给锁续期;
如果释放锁操作本身异常了,watch dog 还会不停的续期吗?不会,因为无论释放锁操作是否成功,EXPIRATION_RENEWAL_MAP中的目标 ExpirationEntry 对象已经被移除了,watch dog 通过判断后就不会继续给锁续期了。
watch dog 机制启动,且代码中没有释放锁操作时,watch dog 会不断的给锁续期;
如果程序释放锁操作时因为异常没有被执行,那么锁无法被释放,所以释放锁操作一定要放到 finally {} 中;
watchlog的延时时间 可以由 lockWatchdogTimeout指定默认延时时间,但是不要设置太小。如100
watchdog 会每 lockWatchdogTimeout/3时间,去延时。
watchdog 通过 类似netty的 Future功能来实现异步延时
watchdog 最终还是通过 lua脚本来进行延时
redis数据结构:
redis选举:
哨兵的选举采用的是Raft算法,Raft是一个用户管理日志一致性的协议,它将分布式一致性问题分解为多个子问题:Leader选举、日志复制、安全性、日志压缩等。Raft将系统中的角色分为领导者(Leader)、跟从者(Follower)和候选者(Candidate):
Leader:接受客户端请求,并向Follower同步请求日式,当日志同步到大多数节点上后告诉Follower提交日志。
Follower:接受并持久化Leader同步的日志,在Leader告知日志可以提交之后,提交日志。
Candidate:Leader选举过程中的临时角色。
1、先过滤故障节点
2、划分选举周期(raft算法可以随机定义一个时间周期,这个周期就是选举的周期,如果该周期中没有选出leader,则进入下个时间周期)
3、每个follow几点的选举时钟不一样,这样就可以避免所有的follow节点向其它节点同时发送选举指令
4、首先follow节点选投票给自己,然后发送请求给其它节点给自己投票
5、其它节点收到投票请求之后,先比较 优先级,如果自己的大,则给自己投票,否则投票给别人。如果 优先级相同,则比较复制偏移量(从主节点同步的数据偏移量),如果自己大,则给自己投票,否则投票给别人,如果偏移量相同,则比较runid(redis的标识,随机生成),如果自己的小,则投票给自己,否则投票给别人,每个节点只能投票一次,而每个节点有自己的投票箱
6、最后比较投票的数量,数量最多,则成功leader,如果数量相同,则进入下一个选举周期,重新投票
三、线程:
1、 corePoolSize 线程池核心线程大小
corePoolSize 是线程池中的一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut。
2、maximumPoolSize 线程池最大线程数量
线程池能够容纳同时执行的最大线程数,此值大于等于1。
3、keepAliveTime 多余的空闲线程存活时间
当线程空闲时间达到keepAliveTime值时,多余的线程会被销毁直到只剩下corePoolSize个线程为止。默认情况下:只有当线程池中的线程数大于corePoolSize时keepAliveTime才会起作用,直到线程中的线程数不大于corepoolSIze,
4、unit 空闲线程存活时间单位
keepAliveTime的计量单位
5、workQueue 工作队列
任务被提交给线程池时,会先进入工作队列,任务调度时再从工作队列中取出。
6、threadFactory 线程工厂
创建一个线程工厂用来创建线程,可以用来设定线程名、是否为daemon线程等等
7、handler 四种拒绝策略
AbortPolicy:丢弃任务并抛出 RejectedExecutionException 异常。(默认这种)
DiscardPolicy:丢弃任务,但是不抛出异常
DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程) 。也就是当任务被拒绝添加时,会抛弃任务队列中最旧的任务也就是最先加入队列的,再把这个新任务从队尾添加进去,等待执行。
CallerRunsPolicy:谁调用,谁处理。由调用线程(即提交任务给线程池的线程)处理该任务,如果线程池已经被shutdown则直接丢弃略
一个任务被提交到线程池以后,首先会找有没有空闲并且存活线程,如果有则直接将任务交给这个空闲线程来执行,如果没有则会放到工作队列中,直到工作队列满了,才会创建一个新线程,然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。工作队列满,且线程数等于最大线程数,此时再提交任务则会调用拒绝策略。
常用工作队列有以下几种
BlockingQueue,如果BlockQueue是空的,从BlockingQueue取东西的操作将会被阻断进入等待状态,直到 BlockingQueue进了东西才会被唤醒.同样,如果BlockingQueue是满的,任何试图往里存东西的操作也会被阻断进入等待状态,直到 BlockingQueue里有空间才会被唤醒继续操作.
使用BlockingQueue的关键技术点如下:
1.BlockingQueue定义的常用方法如下:
1)add(anObject):把anObject加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则报异常
2)offer(anObject):表示如果可能的话,将anObject加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false.
3)put(anObject):把anObject加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断直到BlockingQueue里面有空间再继续.
4)poll(time):取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null
5)take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到Blocking有新的对象被加入为止
1. ArrayBlockingQueue(数组的有界阻塞队列)
特点:
1.初始化一定容量的数组
2.使用一个重入锁,默认使用非公平锁,入队和出队共用一个锁,互斥
3.是有界设计,如果容量满无法继续添加元素直至有元素被移除
4.使用时开辟一段连续的内存,如果初始化容量过大容易造成资源浪费,过小易添加失败
ArrayBlockingQueue 在创建时必须设置大小,按FIFO排序(先进先出)。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
2. LinkedBlockingQueue(链表的无界阻塞队列)
特点:
1.内部使用节点关联,会产生多一点内存占用
2.使用两个重入锁分别控制元素的入队和出队,用Condition进行线程间的唤醒和等待
3.有边界的,在默认构造方法中容量是Integer.MAX_VALUE
非连续性内存空间 按 FIFO 排序任务,可以设置容量(有界队列),不设置容量则默认使用 Integer.Max_VALUE 作为容量 (无界队列)。该队列的吞吐量高于 ArrayBlockingQueue。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。有两个快捷创建线程池的工厂方法 Executors.newSingleThreadExecutor、Executors.newFixedThreadPool,使用了这个队列,并且都没有设置容量(无界队列)。
3.SynchronousQueue(一个不缓存任务的阻塞队列)
特点:
1.内部容量是0
2.每次删除操作都要等待插入操作
3.每次插入操作都要等待删除操作
4.一个元素,一旦有了插入线程和移除线程,那么很快由插入线程移交给移除线程,这个容器相当于通道,本身不存储元素
5.在多任务队列,是最快的处理任务方式。
生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
其 吞 吐 量 通 常 高 于LinkedBlockingQueue。 快捷工厂方法 Executors.newCachedThreadPool 所创建的线程池使用此队列。与前面的队列相比,这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
4. PriorityBlockingQueue(具有优先级的无界阻塞队列)
特点:
1.无边界设计,但容量实际是依靠系统资源影响
2.添加元素,如果超过1,则进入优先级排序
优先级通过参数Comparator实现。
5. DelayQueue(这是一个无界阻塞延迟队列)
特点:
1无边界设计
2添加(put)不阻塞,移除阻塞
3元素都有一个过期时间
4取元素只有过期的才会被取出
底层基于 PriorityBlockingQueue 实现的,队列中每个元素都有过期时间,当从队列获取元素(元素出队)时,只有已经过期的元素才会出队,而队列头部的元素是过期最快的元素。快捷工厂方法 Executors.newScheduledThreadPool 所创建的线程池使用此队列。
Java 中的阻塞队列(BlockingQueue)与普通队列相比,有一个重要的特点:在阻塞队列为空时,会阻塞当前线程的元素获取操作。具体来说,在一个线程从一个空的阻塞队列中取元素时,线程会被阻塞,直到阻塞队列中有了元素;当队列中有元素后,被阻塞的线程会自动被唤醒(唤醒过程不需要用户程序干预)。
3种数据删除策略
1.定时删除(时间换空间)
定时删除时给key都设置一个过期的时间,当达到删除时间节点时,立即执行对key的删除。
2.惰性删除(空间换时间)
惰性删除的含义是:当要删除的数据到达给定时间时,先不进行删除操作;等待下一次访问时,若数据已过期则进行删除,客户端返回不存在,数据未过期,则返回数据。
3.定期删除(折中)
定期删除是对CPU和内存消耗取得一个折中方案,通过每隔一段时间执行一次删除过期key的操作,并且通过限制删除操作执行的时长和频率来减少删除操作对CPU造成的影响,但是限制删除操作执行的时长和频率需要合理地设置,否则可能会退化为成定时删除或惰性删除。
四、MSYQL
1.MYSQ的锁锁的是行记录还是索引
在MySQL中,行级锁并不是直接锁记录,而是锁索引
InnoDB的行锁是针对索引加的锁,不是针对记录加的锁。并且该索引不能失效,否则都会从行锁升级为 表锁。
回表 索引覆盖 索引下推
通过非主键索引查询数据时,我们先通过非主键索引树查找到主键值,然后再在主键索引树搜索一次(根据rowid再次到数据块里取数据的操作),这个过程称为回表
当SQL语句的所有查询字段(select列)和查询条件字段(where子句)全都包含在一个索引中,便可以直接使用索引查询而不需要回表,故称索引覆盖
查询字段(select列)不是/不全是联合索引的字段,查询条件为多条件查询且查询条件子句(where/order by)字段全是联合索引。MySQL 5.6引入了索引下推优化,可以在索引遍历过程中,对索引中包含的字段先做判断,过滤掉不符合条件的记录,减少回表字数。索引下推
回表
根据上面的索引结构说明,主键索引和普通索引的查询区别
如果语句是 select * from T where ID=500,即主键查询方式,则只需要搜索 ID 这棵 B+ 树;
如果语句是 select * from T where k=5,即普通索引查询方式,则需要先搜索 k 索引树,得到 ID 的值为 500,再到 ID 索引树搜索一次。这个过程称为回表
覆盖索引
如果执行的语句是 select ID from T where k between 3 and 5,这时只需要查 ID 的值,而 ID 的值已经在 k 索引树上了,因此可以直接提供查询结果,不需要回表。也就是说,在这个查询里面,索引 k 已经“覆盖了”我们的查询需求,我们称为覆盖索引。
2.什么是事务
事务(Transaction)是访问并可能更新数据库中各项数据项的一个程序执行单元(unit)
3.事务的四大特性acid
事务有四个特性:acid,原子性,一致性,隔离性,持久性。
原子性:事务的原子性指的是一个事务中的所有操作是不可分割的,必须是同一个逻辑单元,要么全部执行,要么全部不执行
一致性:事务前后数据的完整性保持一致
隔离性:多个事务在同时触发的时候,彼此互不干扰,当事务出现并发操作的时候要相互隔离
持久性:事务一旦提交,对数据库的产生的效果是永久的,其他事务的执行或者故障都不会对本次事务的操作结果有影响
事务的四个性质是怎么实现的?
原子性:原子性依靠undolog回滚日志实现,每次对数据做修改或者删除插入的操作都会生成一条undolog来记录操作之前的数据状态,使用rollback的语句能够将所有执行成功的SQL语句产生的效果撤销。回滚日志先于数据持久化到硬盘上。回滚就是逆向操作。
一致性:依靠其他三个性质实现,一致性指的是数据的完整性,为了保证数据的有意义状态
隔离性:通过锁机制实现,当事务操作数据的时候加锁,让事务执行前后看到的数据时候一致的,并行执行事务和串行执行事务产生的效果一样。本质有两种,悲观锁和乐观锁
持久性:持久性通过redolog重做日志实现,redolog记录的是对数据库的操作。MySQL先把存放在硬盘上的数据加载到内存中,在内存中做修改再刷回磁盘,redolog使得在事务提交的时候将数据刷回磁盘。
当数据库宕机之后,先通过redolog来恢复数据库,然后根据undolog和binlog的记录决定是要回滚还是提交
4、四种隔离级别
读未提交:在这种隔离级别下,别的事务在提交之前对数据进行修改,所有事务能够读到修改后的数据。读取未提交的数据会导致脏读,脏读指的是如果事务A读到事务B修改后的数据,但是事务B执行的回滚操作,那么事务A读到的数据就是脏数据。读未提交的隔离级别几乎不会用到。可能会出现脏读、不可重复读和幻读等问题
读提交:只有别的事务提交之后,本事务才能看到修改后的数据,提交之前只能看到未修改的数据。这是大多数数据库默认的隔离级别,但是不是MySQL的默认隔离级别。导致不可重复读:在当前事务中只能看见已经提交事务的执行结果,当同一事务在读取期间出现新的commit操作,读到不一样的数据,可能出现不可重复读和幻读问题
可重复读:可重复读是MySQL的默认隔离级别。 对于同一个字段事务在执行过程中读到数据,除非自己修改。保证当前事务不会读取到其他事务的update操作,因此会导致幻读。
幻读指的是select 某记录是否存在,不存在,准备插入此记录,但执行 insert 时发现此记录已存在,无法插入,此时就发生了幻读。
序列化:串行化,对于同一行记录,读会加读锁,写会加写锁,出现读写冲突的时候,后访问的事务必须等前一个事务执行完成才能继续执行。
通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。
7种事务传播性:
1、PROPAGATION_REQUIRED
表示当前方法必须运行在事务中。如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。
2、PROPAGATION_SUPPORTS
如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行。
单纯的调用methodB时,methodB方法是非事务的执行的。当调用methdA时,methodB则加入了methodA的事务中,事务地执行。
3、PROPAGATION_MANDATORY
该方法必须在事务中运行,如果当前事务不存在,则抛出异常。
如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常。
4、PROPAGATION_MANDATORY
它会开启一个新的事务。如果一个事务已经存在,则先将这个存在的事务挂起。
两个独立的事务,互不影响。
5、PROPAGATION_NOT_SUPPORTED
该方法不应该运行在事务中,如果存在当前事务,在改方法运行期间,当前事务会被挂起。
6、PROPAGATION_NEVER
总是非事务地执行,如果存在一个活动事务,则抛出异常。
7、PROPAGATION_NESTED
如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行提交或回滚。
如果当前事务不存在那么其行为与 PROPAGATION_REQUIRED 相同。
5.mysql的锁怎么理解 间隙锁 行锁 表锁
共享锁、排他锁、行级锁、表级锁、间隙锁 悲观锁forupdate 乐观锁(版本号)
基于锁的粒度分类,有表锁、行锁、记录锁、间隙锁、临键锁
还有全局锁,页锁等
全局锁:全局锁就是对整个数据库实例加锁,MySQL提供了一个加全局读锁的方法,命令是 Flush tables with read lock (FWRL)。当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。
全局锁的应用:对全库做逻辑备份,备份过程中主从库都是处于只读的状态。如果只在主库上备份,那么更新期间不能有操作,业务会停摆,而只在从库上备份,备份期间从库不能执行主库同步过来的binlog,导致主从延迟。
表级锁:有两种,一种是表锁,另一种是元数据锁mdl
表级锁特点:开销小,加锁快;不会出现死锁;粒度大,容易出现锁冲突;数据库引擎总是一次性获取所有的锁,并且按照同样的顺序获取表锁,避免死锁
元数据锁:防止并发执行ddl、mdl阻塞:用于隔离dml(数据操纵语言,对表数据进行修改)和ddl(数据定义语言,对表结构进行修改)语句,每执行一条dml或者是ddl语句的时候都会申请mdl锁,DML操作需要MDL读锁,DDL操作需要MDL写锁(MDL加锁过程是系统自动控制,无法直接干预,读读共享,读写互斥,写写互斥)
表锁有两种:表的读锁和表的写锁
语句:lock tables t red:innodb存储引擎会对t表加表级别S锁。
lock tables t write:innodb存储引擎会对t表加表级别X锁。
表锁是MySQL中最基本的锁策略,不依赖存储引擎,开销小,粒度大,最大的负面问题是锁竞争概率高,并发性差
行锁
Innodb支持行锁,这是代替原生的myisam引擎的重要原因
行锁分为共享锁(s锁)和排他锁(x锁)
共享锁又称为读锁,多个事务对于同一个数据能共享一把锁,都能访问到最新的数据
多个事务的查询可以共享一把锁,单个事务拿到共享锁之后,该事务可以进行修改,但是多个事务拿到共享锁,所有事务都不能修改,会导致死锁
排他锁(写锁),对某一样的数据排他锁只能有一个事务拿到,其余事务不能再获取该数据行的锁,基于该数据行的操作会被阻塞,直到锁释放。
共享/排它锁的使用场景
共享锁:确保某个事务查到最新的数据;这个事务不需要对数据进行修改、删除等操作;也不允许其它事务对数据进行修改、删除等操作;其它事务也能确保查到最新的数据。
排它锁:确保某个事务查到最新的数据;并且只有该事务能对数据进行修改、删除等操作。
6.当前读 快照读
1.快照读(普通读)
普通的 select 语句。执行方式是生成 readview,直接利用 MVCC 机制来读取,并不会对记录进行加锁。它是基于多版本并发控制即 MVCC机制,既然是多版本,那么快照读读到的数据不一定是当前最新的数据,有可能是之前历史版本的数据。
2、当前读(锁定读)
它读取的记录都是数据库中当前的最新版本,会对当前读取的数据进行加锁,防止其他事务修改数据,这种锁是一种悲观锁。当前读的规则,就是要能读到所有已经提交的记录的最新值。
7.mysql语句执行流程
连接器判断用户登录以及用户权限;
1.客户端发起一个SQL的查询;
2.缓存命中,走缓存,直接返回查询结果;
3.缓存没命中,到达分析器,对SQL语句进行分析,包括预处理与解析过程;
4.优化器,对SQL语句进行优化;
5.执行器,调用存储引擎,执行具体的SQL操作;
6.将操作记录在undo log中,并存储回滚段指针和事务ID。
7.通过索引查找数据
8.写入redo log
9.写binlog
10.提交事务
(连接器、查询缓存、词法分析器、优化器、执行器、Bin-log归档)
两段式提交
8.WAL机制
https://blog.csdn.net/L_IK_Y/article/details/121792251
WAL全称为Write-Ahead Logging,预写日志系统。其主要是指MySQL在执行写操作的时候并不是立刻更新到磁盘上,而是先记录在日志中,之后在合适的时间更新到磁盘中。日志主要分为undo log、redo log、binlog。
当内存数据页跟磁盘数据页内容不一致的时候,我们成这个内存页为“脏页”。内存数据写入磁盘后,内存和磁盘上的数据页内容就一致了,称为“干净页”
MySQL真正使用WAL的原因是:磁盘的写操作是随机IO,比较耗性能,所以如果把每一次的更新操作都先写入log中,那么就成了顺序写操作,实际更新操作由后台线程再根据log异步写入。这样对于client端,延迟就降低了。并且,由于顺序写入大概率是在一个磁盘块内,这样产生的IO次数也大大降低。所以WAL的核心在于将随机写转变为了顺序写,降低了客户端的延迟,提升了吞吐量。
9.buffer pool undolog redolog binlog
MySQL为了提供性能,读写不是直接操作的磁盘文件,而是在内存中开辟了一个叫做buffer pool的缓存区域,更新数据的时候会优先更新到buffer pool,之后再由I/O线程写入磁盘。同时为了InnoDB为了保证宕机不丢失buffer pool中的数据,实现crash safe(服务器宕机重启后),还引入了一个叫做redo log的日志模块。另外还有处于MySQL Server层的用于备份磁盘数据的bin log,用于事务回滚和MVCC的undo log等。
https://blog.csdn.net/huangzhilin2015/article/details/115396599
redolog和binlog的区别: redolog是物理日志,循环写,记录了某个数据页上做了什么修改,是innodb独有的。
binlog是逻辑日志,追加写,记录了一条mysql的原始逻辑 即原始语句
10MVCC
https://blog.csdn.net/lans_g/article/details/124232192
11red view 关键参数
基于undolog、版本链、readview。
https://blog.csdn.net/lans_g/article/details/124232192
12.索引 聚族索引 普通索引
InnoDB中,表数据文件本身就是按B+Tree组织的一个索引结构,聚簇索引就是按照每张表的主键构造一颗B+树,同时叶子节点中存放的就是整张表的行记录数据,也将聚集索引的叶子节点称为数据页。这个特性决定了索引组织表中数据也是索引的一部分;索引和数据都存在主节点
一般建表会用一个自增主键做聚簇索引,没有的话MySQL会默认创建,但是这个主键如果更改代价较高,故建表时要考虑自增ID不能频繁update这点。
我们日常工作中,根据实际情况自行添加的索引都是辅助索引,辅助索引就是一个为了需找主键索引的二级索引,现在找到主键索引再通过主键索引找数据;
非聚簇索引:将数据存储于索引分开结构,索引结构的叶子节点指向了数据的对应行,myisam通过key_buffer把索引先缓存到内存中,当需要访问数据时(通过索引访问数据),在内存中直接搜索索引,然后通过索引找到磁盘相应数据。主节点保存索引,叶子结点保存数据。
13.回表 索引覆盖
回表:回表查询顾名思义就是在数据查询过程中 MySQL 内部需要两次查询。即先定位查询数据所在表的主键值,再根据主键定位行记录。
索引覆盖:如果一个索引覆盖(包含)了所有需要查询的字段的值,这个索引就是覆盖索引。因为索引中已经包含了要查询的字段的值,因此查询的时候直接返回索引中的字段值就可以了,不需要再到表中查询,避免了对主键索引的二次查询,也就提高了查询的效率。
14.存储引擎
InnoDB存储引擎
InnoDB已经开发了十余年,遵循CNU通用公开许可(GPL)发行。
InnoDB给MySQL的表提供了事务、回滚、崩溃修复能力和多版本并发控制的事务安全。
InnoDB是MySQL上第一个提供外键约束的表引擎。而且InnoDB对事务处理的能力,也是MySQL其他存储引擎所无法与之比拟的。
InnoDB存储引擎中支持自动增长列AUTO_INCREMENT。InnoDB存储引擎中支持外键(FOREIGN KEY)。
InnoDB存储引擎的优势在于提供了良好的事务管理、崩溃修复能力和并发控制。
缺点是其读写效率稍差,占用的数据空间相对比较大。
My ISAM存储引擎
MyISAM存储引擎是MySQL中常见的存储引擎,曾是MySQL的默认存储引擎。
MyISAM存储引擎是基于ISAM存储引擎发展起来的,它解决了ISAM的很多不足。My ISAM增加了很多有用的扩展。 MyISAM插入速度快,对空间和内存的使用较低。
My ISAM不支持事务,对数据的完整性、并发性支持不够。
MyISAM创建表时,表存储成三个文件,文件名与表名相同,分别为sdi、MYD和MYI。其中sdi文件用于存储表的元数据信息,MYD存储的是数据信息,而MYI则是索引信息
MEMORY存储引擎
MEMORY存储引擎是MySQL中的一类特殊的存储引擎。
其使用存储在内存中的内容来创建表,而且所有数据也放在内存中,存取速度快。
这些特性都与InnoDB存储引擎、MyISAM存储引擎不同。但MEMORY存储引擎的数据安全性较低,且不能创建过大的表。
五、框架
springmvc工作流程
1. 用户通过浏览器发起 HttpRequest 请求到前端控制器 (DispatcherServlet)。
2. DispatcherServlet 将用户请求发送给处理器映射器 (HandlerMapping)。
3. 处理器映射器 (HandlerMapping)会根据请求,找到负责处理该请求的处理器,并将其封装为处理器执行链 返回 (HandlerExecutionChain) 给 DispatcherServlet
4. DispatcherServlet 会根据 处理器执行链 中的处理器,找到能够执行该处理器的处理器适配器(HandlerAdaptor) --注,处理器适配器有多个
5. 处理器适配器 (HandlerAdaptoer) 会调用对应的具体的 Controller
6. Controller 将处理结果及要跳转的视图封装到一个对象 ModelAndView 中并将其返回给处理器适配器 (HandlerAdaptor)
7. HandlerAdaptor 直接将 ModelAndView 交给 DispatcherServlet ,至此,业务处理完毕
8. 业务处理完毕后,我们需要将处理结果展示给用户。于是DisptcherServlet 调用 ViewResolver,将 ModelAndView 中的视图名称封装为视图对象
9. ViewResolver 将封装好的视图 (View) 对象返回给 DIspatcherServlet
10. DispatcherServlet 调用视图对象,让其自己 (View) 进行渲染(将模型数据填充至视图中),形成响应对象 (HttpResponse)
11. 前端控制器 (DispatcherServlet) 响应 (HttpResponse) 给浏览器,展示在页面上。
springboot启动流程
一、运行 SpringApplication.run() 方法
new springApplication()调用构造方法
1.确定应用程序类型
利用WebApplicationType.deduceFromClasspath()方法判断当前应用程序的容器,默认使用的是Servlet 容器,除了servlet之外,还有NONE 和 REACTIVE (响应式编程);
2.加载所有的初始化器
3.加载所有的监听器
从META-INF/spring.factories,自带有2个,分别在源码的jar包的 spring-boot-autoconfigure 项目 和 spring-boot 项目里面各有一个
org.springframework.context.ApplicationContextInitializer 接口就是初始化器了
org.springframework.context.ApplicationListener 接口就是监听器了
这两个作用(https://blog.csdn.net/weixin_46666822/article/details/124619117)
4.设置程序运行的主类
deduceMainApplicationClass(); 这个方法仅仅是找到main方法所在的类,为后面的扫包作准备,deduce是推断的意思,所以准确地说,这个方法作用是推断出主方法所在的类;
二、开启计时器
// 实例化计时器
StopWatch stopWatch = new StopWatch();
// 开始计时
stopWatch.start();
将java.awt.headless设置为true 没有显示器器和鼠标键盘的模式下照样可以工作
三、.获取并启用监听器
getRunLiisteners(args)
创建所有 Spring 运行监听器并发布应用启动事件
启用监听器
四、设置应用程序参数
将执行run方法时传入的参数封装成一个对象
五、准备环境变量
准备环境变量,包含系统属性和用户配置的属性,执行的代码块在 prepareEnvironment 方法内将maven和系统的环境变量都加载进来
六、打印 banner 信息
自定义banner信息 在resources目录下添加一个 banner.txt 的文件即可
七、创建应用程序的上下文
调用 createApplicationContext() 方法创建应用程序的上下文
八、实例化异常报告器
异常报告器是用来捕捉全局异常使用的,当springboot应用程序在发生异常时,异常报告器会将其捕捉并做相应处理,在spring.factories 文件里配置了默认的异常报告器,
这个异常报告器只会捕获启动过程抛出的异常
九、准备上下文环境
1.实例化单例的beanName生成器,在 postProcessApplicationContext(context); 方法里面。使用单例模式创建了BeanNameGenerator 对象,其实就是beanName生成器,用来生成bean对象的名称
2.执行初始化方法 其实是执行加载出来的所有初始化器,实现了ApplicationContextInitializer 接口的类
3.将启动参数注册到容器中 这里将启动参数以单例的模式注册到容器中,是为了以后方便拿来使用,参数的beanName 为 :springApplicationArguments
十、刷新上下文
调用spring的refresh加载IOC容器、自动配置类,并创建bean、servlet容器等信息
https://blog.csdn.net/m0_63437643/article/details/123089891
十一、刷新上下文后置处理
afterRefresh 方法是启动后的一些处理,留给用户扩展使用,目前这个方法里面是空的
十二、结束计时器
十三、发布上下文准备就绪事件
十四、启动完成
@SpringBootApplication注解的代码如下,这些注解中 有关SpringBoot的注解只有三个,分别是:
SpringBootConfiguration
EnableAutoConfiguration
ComponentScan()
自动装载
实际上@SpringBootApplication这个注解是这三个注解的复合注解。每次写三个会显得极其麻烦,将其整合。
首先、@SpringBootConfiguration等同于@Configuration,带有spring的标志,是属于spring的一个配置类;
其次、@EnableAutoConfiguration是开启自动配置功能;
@EnableAutoConfiguration 可以帮助 SpringBoot 应用将所有符合条件的 @Configuration 配置都加载到当前 SpringBoot 创建并使用的 IoC 容器
@SpringFactoriesLoader详解,其主要功能就是从指定的配置文件 META-INF/spring.factories 加载配置,spring.factories 是一个典型的 java properties 文件,配置的格式为 Key=Value 形式,只不过 Key 和 Value 都是 Java 类型的完整类名
@ComponentScan 的功能其实就是自动扫描并加载符合条件的组件或 bean 定义,最终将这些 bean 定义加载到容器中。
SpringBoot使用设计模式:单例、策略、工厂
Eureka:
Eureka Server 的数据存储分了两层:数据存储层和缓存层。数据存储层记录注册到 Eureka Server 上的服务信息,缓存层是经过包装后的数据,可以直接在 Eureka Client 调用时返回。我们先来看看数据存储层的数据结构。
Eureka缓存机制_wh柒八九的博客-CSDN博客_eureka本地缓存
工作流程
1)Eureka Server 启动成功,等待服务注册,每个Eureka Server 都存在独立完整的服务注册表信息,注册时,它提供自身的元数据,比如IP地址、端口,运行状况指示符URL,主页等。
2)Eureka Client 启动时根据配置的 Eureka Server 地址去注册中心注册服务
3)Eureka Client 会每 30s 向 Eureka Server 发送一次心跳请求,证明客户端服务正常(服务续约,获取注册列表信息)
4)当 Eureka Server 90s 内没有收到 Eureka Client 的心跳,注册中心则认为该节点失效,会注销该实例(服务剔除)
5)单位时间内 Eureka Server 统计到有大量的 Eureka Client 没有上送心跳(15%),则认为可能为网络异常,进入自我保护机制,不再剔除没有上送心跳的客户端
6)当 Eureka Client 心跳请求恢复正常之后,Eureka Server 自动退出自我保护模式
7)Eureka Client 定时全量或者增量从注册中心获取服务注册表,并且将获取到的信息缓存到本地
8)服务调用时,Eureka Client 会先从本地缓存找寻调取的服务。如果获取不到,先从注册中心刷新注册表,再同步到本地缓存(看源码是没有这部分功能的 同步注册表依靠心跳)
9)Eureka Client 获取到目标服务器信息,发起服务调用
10)Eureka Client 程序关闭时向 Eureka Server 发送取消请求,Eureka Server 将实例从注册表中删除(服务下线)
11)Eureka Server 之间通过复制的方式完成数据的同步,Eureka还提供了客户端缓存机制,即使所有的Eureka Server 都挂掉,客户端依然可以利用缓存中的信息消费其他服务的API。综上,Eureka通过心跳检查、客户端缓存等机制,确保了系统的高可用性、灵活性和可伸缩性。
3.Feign
Feign是一个HTTP请求的轻量级客户端框架。通过 接口+注解 的方式发起HTTP请求的调用,而不是像Java中通过封装HTTP请求报文的方式直接调用。
Feign执行流程:
在主程序上添加@EnableFeignClients注解开启对已经加@FeignClient注解的接口进行扫描加载
调用接口中的方法时,基于JDK动态代理的方式,通过InvokeHandler调用处理器分发远程调用,生成具体的RequestTemplate
RequestTemplate生成Request请求,结合Ribbon实现服务调用负载均衡策略
Feign最核心的就是动态代理,同时整合了Ribbon和Hystrix,具备负载均衡、隔离、熔断和降级功能
4.Ribbon
Ribbon是一个客户端的负载均衡器,他提供对大量的HTTP和TCP客户端的访问控制
Ribbon负载均衡策略:简单轮询、权重、随机、重试等多种策略
5.Hystrix
Hystrix提供两个命令,分别是HystrixCommand、HystrixObservableCommand,通常是在Spring中通过注解和AOP来实现对象的构造
熔断:简单来说,就是我们生活中的“保险丝”。如果熔断器打开,就会执行短路,直接进行降级;如果熔断器关闭,就会进入隔离逻辑。默认触发熔断器的条件为:最近一个时间窗口(默认为10秒),总请求次数大于等于circuitBreakerRequestVolume Threshold(默认为20次)并且异常请求比例大于circuitBreakerError ThresholdPercentage(默认为50%),此时熔断器触发
隔离:一般分为两种:线程池隔离和信号量隔离。线程池隔离指的是每个服务对应一个线程池,线程池满了就会降级;信号量隔离是基于tomcat线程池来控制的,当线程达到某个百分比,走降级流程
降级:作为Hystrix中兜底的策略,当出现业务异常、线程池已满、执行超时等情况,调用fallback方法返回一个错误提示
1:什么是熔断
A–>B时,B持续失败,于是A判断B出了故障,然后A决定不再调用B了,此时A熔断了。熔断期间,A或者抛出异常(例如:CircuitException),或者A返回某个默认值
注意:熔断发生在主调方
2:什么是降级
我们在系统内部预先埋好开关,在某个时刻打开开关隐藏某些功能,降级后系统提供有损服务。例如:为了保障核心交易链路,黑五期间我们通过预先埋好的开关隐藏退货按钮并显示友好提示;又或者,黑五期间,为了保障数据库性能,关闭掉B端系统的某些报表导出功能(B端和C端共有同一个数据库)。
注意:降级一般是早早埋好的开关。等时x机成熟了就把开关打开
6.Zull
网关相当于一个网络服务架构的入口,所有网络请求必须通过网关转发到具体的服务。
主要功能:统一管理微服务请求、负载均衡、动态路由、权限控制、监控、静态资源处理等
负载均衡:为每一种负载类型分配对应的容量,并弃用超出限定值的请求
动态路由:根据需要动态的将路由请求到后台不同的集群
权限控制:可以识别认证需要的信息和拒绝不满足条件的请求
监控:追踪有意义的数据和统计结果
静态资源处理:直接在Zull中处理静态资源,从而避免其转发到内部集群
服务注册最长延迟 90s
Eureka Server 维护每 30s 更新数据存储层数据的响应缓存
Eureka Client 对已经获取到的注册信息也做了 30s 缓存
负载均衡组件 Ribbon 也有 30s 缓存
六、消息中间件
一、RockertMQ
1. 基础概念
Producer: 消息生产者,负责产生消息,一般由业务系统负责产生消息
Producer Group:消息生产者组,简单来说就是多个发送同一类消息的生产者称之为一个生产者
Consumer:消息消费者,负责消费消息,一般是后台系统负责异步消费
Consumer Group:消费者组,和生产者类似,消费同一类消息的多个 Consumer 实例组成一个消费者组
Topic:主题,用于将消息按主题做划分,Producer将消息发往指定的Topic,Consumer订阅该Topic就可以收到这条消息
Message:消息,每个message必须指定一个topic,Message 还有一个可选的 Tag 设置,以便消费端可以基于 Tag 进行过滤消息
Tag:标签,子主题(二级分类)对topic的进一步细化,用于区分同一个主题下的不同业务的消息
Broker:Broker是RocketMQ的核心模块,负责接收并存储消息,同时提供Push/Pull接口来将消息发送给Consumer。Broker同时提供消息查询的功能,可以通过MessageID和MessageKey来查询消息。Borker会将自己的Topic配置信息实时同步到NameServer
Queue:Topic和Queue是1对多的关系,一个Topic下可以包含多个Queue,主要用于负载均衡,Queue数量设置建议不要比消费者数少。发送消息时,用户只指定Topic,Producer会根据Topic的路由信息选择具体发到哪个Queue上。Consumer订阅消息时,会根据负载均衡策略决定订阅哪些Queue的消息
Offset:RocketMQ在存储消息时会为每个Topic下的每个Queue生成一个消息的索引文件,每个Queue都对应一个Offset记录当前Queue中消息条数
NameServer:NameServer可以看作是RocketMQ的注册中心,它管理两部分数据:集群的Topic-Queue的路由配置;Broker的实时配置信息。其它模块通过Nameserv提供的接口获取最新的Topic配置和路由信息;各 NameServer 之间不会互相通信, 各 NameServer 都有完整的路由信息,即无状态。
Producer/Consumer :通过查询接口获取Topic对应的Broker的地址信息和Topic-Queue的路由配置
Broker : 注册配置信息到NameServer, 实时更新Topic信息到NameServer
2.RocketMQ 消费模式
2.1 广播模式
一条消息被多个Consumer消费,即使这些Consumer属于同一个Consumer Group,消息也会被Consumer Group中的每一个Consumer都消费一次。
//设置广播模式
consumer.setMessageModel(MessageModel.BROADCASTING);
与集群消费不同的是,consumer 的消费进度是存储在各个 consumer 实例上,这就容易造成消息重复。还有很重要的一点,对于广播消费来说,是不会进行消费失败重投的,所以在 consumer 端消费逻辑处理时,需要额外关注消费失败的情况。
2.2 集群模式
一个Consumer Group中的所有Consumer平均分摊消费消息(组内负载均衡),每条消息只会被 consumer 集群内的任意一个 consumer 实例消费一次
//设置集群模式,也就是负载均衡模式
consumer.setMessageModel(MessageModel.CLUSTERING);
consumer 的消费进度是存储在 broker 上,consumer 自身是不存储消费进度的。消息进度存储在 broker 上的好处在于,当你 consumer 集群是扩大或者缩小时,由于消费进度统一在broker上,消息重复的概率会被大大降低了
集群工作流程:
1、启动NameServer,NameServer起来后监听端口,等待Broker、Producer、 Consumer连上来,相当于一个路由控制中心。
2、Broker启动,跟所有的NameServer保持长连接,定时发送心跳包。心跳包中包含当前Broker信息(IP+端口等)以及存储所有Topic信息。注册成功后,NameServer集群中就有Topic跟Broker的映射关系。
3、收发消息前,先创建Topic,创建Topic时需要指定该Topic要存储在哪些Broker上,也可以在发送消息时自动创建Topic。
4、Producer发送消息,启动时先跟NameServer集群中的其中一台建立长连接,并从NameServer中获取当前发送的Topic存在哪些Broker上,轮询从队列列表中选择一个队列,然后与队列所在的Broker建立长连接从而向Broker发消息。
5、Consumer跟Producer类似,跟其中一台NameServer建立长连接,获取当前订阅Topic存在哪些Broker上,然后直接跟Broker建立连接通道,开始消费消息。
3.Broker 的存储结构
3.1 存储文件简介
Commit log:消息存储文件,rocket Mq会对commit log文件进行分割(默认大小1GB),新文件以消息最后一条消息的偏移量命名。(比如 00000000000000000000 代表了第一个文件,第二个文件名就是 00000000001073741824,表明起始偏移量为 1073741824)
Consumer queue:消息消费队列(也是个文件),可以根据消费者数量设置多个,一个Topic 下的某个 Queue,每个文件约 5.72M,由 30w 条数据组成;ConsumeQueue 存储的条目是固定大小,只会存储 8 字节的 commitlog 物理偏移量,4 字节的消息长度和 8 字节 Tag 的哈希值,固定 20 字节;消费者是先从 ConsumeQueue 来得到消息真实的物理地址,然后再去 CommitLog 获取消息
IndexFile:索引文件,是额外提供查找消息的手段,通过 Key 或者时间区间来查询对应的消息
3.2当有消息发送到mq 先将消息写入到内存 然后将消息第储到commitLog文件(topic queueId message)无序的 同时commitlog会将消息分发给两个索引文件(consumerQueue ,indexFile)consumerQueue记录的是队列的消息消费程度 为了保证效率它不记录commitlog里面的消息内容 只存commitlog里面的索引 indexFile文件是在consumerQueue基础之上提供的一个索引记录功能文件
回溯消费
回溯消费是指Consumer已经消费成功的消息,由于业务上需求需要重新消费,要支持此功能,Broker在向Consumer投递成功消息后,消息仍然需要保留。并且重新消费一般是按照时间维度,例如由于Consumer系统故障,恢复后需要重新消费1小时前的数据,那么Broker要提供一种机制,可以按照时间维度来回退消费进度。RocketMQ支持按照时间回溯消费,时间维度精确到毫秒。
事务消息(https://zhuanlan.zhihu.com/p/115553176)
RocketMQ事务消息(Transactional Message)是指应用本地事务和发送消息操作可以被定义到全局事务中,要么同时成功,要么同时失败。RocketMQ的事务消息提供类似 X/Open XA 的分布事务功能,通过事务消息能达到分布式事务的最终一致。
消息重试
Consumer消费消息失败后,要提供一种重试机制,令消息再消费一次。Consumer消费消息失败通常可以认为有以下几种情况:
1、 由于消息本身的原因,例如反序列化失败,消息数据本身无法处理(例如话费充值,当前消息的手机号被注销,无法充值)等。这种错误通常需要跳过这条消息,再消费其它消息,而这条失败的消息即使立刻重试消费,99%也不成功,所以最好提供一种定时重试机制,即过10秒后再重试。
2、由于依赖的下游应用服务不可用,例如db连接不可用,外系统网络不可达等。遇到这种错误,即使跳过当前失败的消息,消费其他消息同样也会报错。这种情况建议应用sleep 30s,再消费下一条消息,这样可以减轻Broker重试消息的压力。
RocketMQ什么时候删除过期消息?
默认是72小时后删除文件,可以通过配置文件的filereservetime属性更改这个过期时间。
RocketMQ事务消息流程概要
分为两个流程:正常事务消息的发送及提交、事务消息的补偿流程。
1.事务消息发送及提交:
(1) 发送消息(half消息)。
(2) 服务端响应消息写入结果。
(3) 根据发送结果执行本地事务(如果写入失败,此时half消息对业务不可见,本地逻辑不执行)。
(4) 根据本地事务状态执行Commit或者Rollback(Commit操作生成消息索引,消息对消费者可见)
2.补偿流程:
(1) 对没有Commit/Rollback的事务消息(pending状态的消息),从服务端发起一次“回查”
(2) Producer收到回查消息,检查回查消息对应的本地事务的状态
(3) 根据本地事务状态,重新Commit或者Rollback
其中,补偿阶段用于解决消息Commit或者Rollback发生超时或者失败的情况。
如果消费者消费失败或超时则可以进行重试机制保证消费者消费成功
生产者在发送消息时,同步消息失败会重投,异步消息有重试,oneway没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但可能会造成消息重复,消息重复在RocketMQ中是无法避免的问题。消息重复在一般情况下不会发生,当出现消息量大、网络抖动,消息重复就会是大概率事件。另外,生产者主动重发、consumer负载变化也会导致重复消息。如下方法可以设置消息重试策略:
1、 retryTimesWhenSendFailed:同步发送失败重投次数,默认为2,因此生产者会最多尝试发送retryTimesWhenSendFailed + 1次。不会选择上次失败的broker,尝试向其他broker发送,最大程度保证消息不丢。超过重投次数,抛出异常,由客户端保证消息不丢。当出现RemotingException、MQClientException和部分MQBrokerException时会重投。
2、retryTimesWhenSendAsyncFailed:异步发送失败重试次数,异步重试不会选择其他broker,仅在同一个broker上做重试,不保证消息不丢。
3、retryAnotherBrokerWhenNotStoreOK:消息刷盘(主或备)超时或slave不可用(返回状态非SEND_OK),是否尝试发送到其他broker,默认false。十分重要消息可以开启。
死信队列
死信队列用于处理无法被正常消费的消息。当一条消息初次消费失败,消息队列会自动进行消息重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列 不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。
同步刷盘和异步刷盘
消息发给mq之后会存硬盘,存的时候也有区别,如果为了保证存储的效率,通常会先数据存到PAGECACHE缓存,mq将消息存到PAGECACHE缓存之后,就给broker一个响应结果, 然后另外有一个线程专门去做把PAGECACHE的数据写到磁盘里面,这个就是异步刷盘机制,
如果是同步刷盘的话,就是RocketMQ将消息写入内存的PAGECACHE后,立马就通知刷盘线程刷盘, 然后等待刷盘完成,刷盘线程执行完成后唤醒等待的线程,返回消息写 成功的状态。
同步刷盘:
在返回写成功状态时,消息已经被写入磁盘。具体流程是,消息写入内存的PAGECACHE后,立刻通知刷盘线程刷盘, 然后等待刷盘完成,刷盘线程执行完成后唤醒等待的线程,返回消息写成功的状态。
异步刷盘:
在返回写成功状态时,消息可能只是被写入了内存的PAGECACHE,写操作的返回快,吞吐量大.当内存里的消息量积累到一定程度时,统一触发写磁盘动作,快速写入。
同步刷盘和异步刷盘区别
异步刷盘吞吐量更高,但是会有丢失消息的可能,因为你消息是会暂存到PAGECACHE这个内存,如果你有一些消息还没来得及写到硬盘中,此时这个broker服务宕机了,那你还没来得及写入到硬盘的消息就丢失了.
同步刷盘虽然吞吐量低,吞吐量比异步刷盘差很多,但是每过来一条数据都会立马写到磁盘上,这样就不会丢失消息.
刷盘机制配置
刷盘方式是通过Broker配置文件里的flushDiskType 参数设置的,这个参数被配置成SYNC_FLUSH (同步刷盘)、ASYNC_FLUSH (异步刷盘)中的 一个。
二、kafka
Producer:消息⽣产者,向 Kafka Broker 发消息的客户端。
Consumer:消息消费者,从 Kafka Broker 取消息的客户端。Kafka支持持久化,生产者退出后,未消费的消息仍可被消费。
Consumer Group:消费者组(CG),消费者组内每个消费者负责消费不同分区的数据,提⾼消费能⼒。⼀个分区只能由组内⼀个消费者消费,消费者组之间互不影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的⼀个订阅者。
Broker:⼀台 Kafka 机器就是⼀个 Broker。⼀个集群(kafka cluster)由多个 Broker 组成。⼀个 Broker 可以容纳多个 Topic。
Controller:由zookeeper选举其中一个Broker产生。它的主要作用是在 Apache ZooKeeper 的帮助下管理和协调整个 Kafka 集群。
Topic:可以理解为⼀个队列,Topic 将消息分类,⽣产者和消费者⾯向的是同⼀个 Topic。
Partition:为了实现扩展性,提⾼并发能⼒,⼀个⾮常⼤的 Topic 可以分布到多个 Broker上,⼀个 Topic 可以分为多个 Partition,同⼀个topic在不同的分区的数据是不重复的,每个 Partition 是⼀个有序的队列,其表现形式就是⼀个⼀个的⽂件夹。不同Partition可以部署在同一台机器上,但不建议这么做。
Replication:每⼀个分区都有多个副本,副本的作⽤是做备胎。当主分区(Leader)故障的时候会选择⼀个备胎(Follower)上位,成为Leader。在kafka中默认副本的最⼤数量是10个,且副本的数量不能⼤于Broker的数量,follower和leader绝对是在不同的机器,同⼀机器对同⼀个分区也只可能存放⼀个副本(包括⾃⼰)。
Message:每⼀条发送的消息主体。
Leader:每个分区多个副本的“主”副本,⽣产者发送数据的对象,以及消费者消费数据的对象,都是 Leader。
Follower:每个分区多个副本的“从”副本,使用发布订阅模式主动拉取Leader的数据(与redis不同),实时从 Leader 中同步数据,保持和 Leader 数据的同步。Leader 发⽣故障时,某个 Follower 还会成为新的 Leader。
Offset:消费者消费的位置信息,监控数据消费到什么位置,当消费者挂掉再重新恢复的时候,可以从消费位置继续消费。
ZooKeeper:Kafka 集群能够正常⼯作,需要依赖于 ZooKeeper,ZooKeeper 帮助 Kafka存储和管理集群信息。
High Level API 和Low Level API :高水平API,kafka本身定义的行为,屏蔽细节管理,使用方便;低水平API细节需要自己处理,较为灵活但是复杂。
生产者发送数据
producer 是生产者,也是数据的入口,Producer 在写入数据时(需要指定broker地址),写入leader , 不会将数据写入 follower。leader会维持一个与其保持同步的follower(副本)集合,该集合就是ISR,每一个partition都有一个ISR,它由leader动态维护
存储机制
由于⽣产者⽣产的消息会不断追加到 log ⽂件末尾,为防⽌ log ⽂件过⼤导致数据定位效率低下,Kafka 采取了分⽚和索引机制。它将每个 Partition 分为多个 Segment,每个 Segment 对应两个⽂件:“.index” 索引⽂件和“.log” 数据⽂件
Kafka 有三种分配策略:
RoundRobin策略的原理是将消费组内所有消费者以及消费者所订阅的所有topic的partition按照字典序排序,然后通过轮询算法逐个将分区以此分配给每个消费者。 逐个分完
默认为Range,Range分配策略是面向每个主题的,首先会对同一个主题里面的分区按照序号进行排序,并把消费者线程按照字母顺序进行排序。然后用分区数除以消费者线程数量来判断每个消费者线程消费几个分区。如果除不尽,那么前面几个消费者线程将会多消费一个分区。 各自分区/消费者线程=各个消费者消费个数 余数为第一个消费者多消费数 每个topic区分开来分配。
Sticky分配策略的原理比较复杂,它的设计主要实现了两个目的:
分区的分配要尽可能的均匀;
分区的分配尽可能的与上次分配的保持相同。
如果这两个目的发生了冲突,优先实现第一个目的。
kafka设定了默认的消费逻辑:一个分区只能被同一个消费组(ConsumerGroup)内的一个消费者消费。
这三种情况其实就是kafka进行分区分配的前提条件:
1.同一个 Consumer Group 内新增消费者;
2.订阅的主题新增分区;
3.消费者离开当前所属的Consumer Group,包括shuts down 或 crashes
kafka消费模式:
1)点对点模式(一对一,消费者主动拉取数据,消息收到后消息清除)
消息生产者生产消息发送到Queue中,然后消息消费者从Queue取出并且消费消息。消息被消费以后,queue中不再有存储,所以消息消费者不可能消费到已经被消费的消息。Queue支持存在多个消费者,但是对一个消息而言,只会有一个消费者可以消费。
2)发布/订阅模式(一对多,消费者消费数据之后不会清除消息)
消息生产者(发布)将消息发布到topic中,同时多个消息消费者(订阅)消费该消息。和点对点方式不同,发布到topic的消息会被所有订阅者消费。
ack应答机制
对于某些不太重要的数据,对数据的可靠性要求不是很高,能够容忍数据的少量丢失,所以没有必要等ISR中的follower全部接收成功。所以Kafka提供了三种可靠性级别,用户可以根据对可靠性和延迟的要求进行权衡。acks:
0: producer不等待broker的ack,这一操作提供了一个最低的延迟,broker一接收到还没写入磁盘就已经返回,当broker故障时可能丢失数据;
1: producer等待leader的ack,partition的leader落盘成功后返回ack,如果在follower同步成功之前leader故障,那么将会丢失数据;
-1(all):producer等待broker的ack,partition的leader和ISR里的follower全部落盘成功后才返回ack。但是如果在follower同步完成后,broker发送ack之前,leader发生故障,那么会造成重复数据。(极端情况下也有可能丢数据:ISR中只有一个Leader时,相当于1的情况)。
kafka消息物理存储
文件管理
kafka不会一直保留数据,也不会等到所有消费者都读取了消息之后才删除消息。kafka会为每个主题配置了数据保留期限,规定数据被删除之前可以保留多长时间,或者清理数据之前可以保留的数据量大小。
因为在一个大文件中查找和删除消息很费时,所以我们把分区分成若干个片段。默认情况下,每个片段包含1GB或一周的数据,以较小的那个为准。在broker往分区写入数据时,如果达到片段上限,就关闭当前文件,并打开一个新文件。当前正在写入数据的片段叫做活跃片段。活跃片段永远不会被删除。
文件格式
kafka的消息和偏移量保存在文件里。保存在磁盘上的数据格式与从生产者发送过来或者发送给消费者的消息格式是一样的。因为使用了相同的消息格式进行磁盘存储和网络传输,kafka可以使用零复制技术给消费者发送消息,同时避免了对生产者已经压缩过的消息进行解压和在压缩。
除了键,值和偏移量外,消息里还包含了消息大小、校验和、消息格式版本号、压缩算法和时间戳。时间戳可以是生产者发送消息的时间,也可以是消息达到broker的时间,这个可以配置。
消息保留机制(消息删除策略)
消息保留有两个维度:根据时间保留数据和根据字节大小。无论哪个条件先满足,消息都会被删除。这两个维度通过下面配置参数来控制:
log.retention.ms,log.retention.hours和log.retention.minutes
根据时间来决定数据可以被保留多久。这三个配置的效果是一样的,只是时间单位不一样,默认使用log.retention.hours配置,默认参数是168小时也就是一周。如果指定了不止一个参数,kafka优先使用具有最小值的那个参数。
根据时间保留数据是通过检查磁盘上日志片段文件的最后修改时间来实现的。一般来说,最后修改时间指定的就是日志片段的关闭时间,也就是文件里的最后一个消息的时间戳。不过,在某些特别的情况下,最后修改时间并不是最后一个消息的时间戳,例如:分区移动。
log.retention.bytes
通过保留的消息字节数来判断消息是否过期。这个是作用在每个分区上的。例如:一个主题有8个分区,此参数设置1G,那么这个主题最多可以保留8G的数据,所以当分区增加时,整个主题可以保留的数据也会随之增加。
以上两个设置都作用在日志片段上,而不是作用在单个消息上。当消息到达broker时,它们被追加到分区的当前日志片段上。当日志片段大小达到log.segment.bytes设置的上限(默认是1G)时,当前日志片段就会被关闭,一个新的日志片段被打开。如果一个日志片段被关闭,就开始等待过期。log.segment.bytes值越小,就会越频繁的关闭和分配新文件,磁盘写入整体效率就会降低。另外还有一个参数log.segment.ms通过指定了多长时间之后日志片段会被关闭。日志片段会在大小或者时间达到上限时被关闭,就看哪个条件先满足。默认log.segment.ms没有设定值,所以只根据大小来关闭日志片段。
要特别注意,日志片段被关闭之前消息是不会过期的,因此日志片段在关闭之前也不会被删除。要等到日志片段的最后一个消息过期此日志片段才能被删除。
RocketMQ与kafka的不同
1、数据可靠性
2、性能对比
3、单机支持的队列数
4、消息投递的实时性
5、消费失败重试
6、严格保证消息有序
7、定时消息
8、分布式事务消息
9、消息查询
10、消息回溯
11、消息并行度
12、开发语言
13、消息堆积能力
14、开源社区活跃度
15、应用领域成熟度
三、总结
kafka相比RocketMQ的优势
1、单机吞吐量TPS可上百万,远高于RocketMQ的TPS7万每秒,适用于日志类消息。
2、kafka支持多语言的客户端
RocketMQ相比kafka的优势
**1、保证消息不丢( 数据可靠性达10个9)
2、可严格保证消息有序
3、支持分布式事务消息
4、支持按时间做消息回溯(可精确到毫秒级)
5、支持按标识和内容查询消息,用于排查丢消息
6、支持消费失败重试
7、可支持更多的partition, 即更多的消费线程数
RocketMQ与kafka的区别_rocketmq和kafka区别_Shi Peng的博客-CSDN博客
七、JVM
JVM详解_Black_Me_Bo的博客-CSDN博客_jvm详解
类装载器(ClassLoader)(用来装载.class文件)
类装载的执行过程
加载:根据查找路径找到相应的 class 文件然后导入;
验证:检查加载的 class 文件的正确性;
准备:给类中的静态变量分配内存空间;
解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为 一个标示,而直接引用直接指向内存中的地址;
初始化:对静态变量和静态代码块执行初始化工作
启动类加载器(Bootstrap ClassLoader),是虚拟机自身的一部分,用来加载 Java_HOME/lib/目录中的,或者被 -Xbootclasspath 参数所指定的路径中并且被虚 拟机识别的类库;
其他类加载器:
扩展类加载器(Extension ClassLoader):负责加载\lib\ext目录或Java. ext. dirs系统变量指
定的路径中的所有类库;
应用程序类加载器(Application ClassLoader)。负责加载用户类路径 (classpath)上的指
定类库,我们可以直接使用这个类加载器。一般情况,如果我 们没有自定义类加载器默认就
是用这个加载器
双亲委派模型
如果一个类加载器收到了类加载的请求,它首先不会自己去加载 这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如 此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无 法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加 载类。
执行引擎(执行字节码,或者执行本地方法)
解释器(Interpreter)
当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行
JIT编译器(Just In Time Compiler)
就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言
执行引擎的工作过程
执行引擎在执行的过程中需要执行什么样的字节码指令完全取决于PC寄存器
每当执行完一项指令操作后PC寄存器就会更新下一条被执行的指令地址
当然在方法执行的过程中,执行引擎可能会通过存储在局部变量表中的对象引用准确定位到存储在Java堆区中的对象实例信息,以及通过对象头中的元数据指针定位到目标对象自的类型信息
从外观上来看,所有的Java虚拟机执行引擎的输入、输出、都是一致的:输入的是字节码为二进制流,处理过程是字节码解析执行的有效过程,输出的是执行结果
运行时数据区(方法区、堆、java栈、PC寄存器、本地方法栈)
栈
运行时的单位。
解决程序的运行问题,即程序如何执行,或者说如何处理数据。
存放基本数据类型的局部变量,以及引用数据类型的对象的引用。
堆
是存储的单位。
堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
对象主要都是放在堆空间的,是运行时数据区比较大的一块。
运行时数据区(程序计数器(PC寄存器)、方法区、堆、java栈、本地方法栈)
程序计数器(线程私有)
程序计数器是一块较⼩的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
另外,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
程序计数器的主要作用:
1.字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
2.在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时能够知道该线程上次运行到哪⼉了。(上下文切换)
注意:程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。(因为程序计算器仅仅只是一个运行指示器,它所需要存储的内容仅仅就是下一个需要待执行的命令的地址)
Java虚拟机栈(线程私有)
Java虚拟机栈的生命周期和线程相同(随着线程的创建而创建随着线程的死亡而死亡),描述的是 Java方法执行的内存模型,每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
Java内存可以粗糙的区分为堆内存(Heap)和栈内存(Stack),其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。(实际上,Java虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。)
局部变量表主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。
局部变量表、操作栈、动态链接、方法出口:https://www.cnblogs.com/TidalCoast1034/articles/14591981.html
本地方法栈
和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。在HotSpot虚拟机中和Java虚拟机栈合二为一。
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现StackOverFlowError和 OutOfMemoryError两种错误。
Java堆(线程共享)
Java堆是虚拟机所管理的内存中最大的一块,在虚拟机启动时创建,此块内存的唯一目的就是存放对象实例,几乎所有的对象实例都在对上分配内存。JVM规范中的描述是:所有的对象实例以及数据都要在堆上分配。但是随着JIT编译器的发展与逃逸分析技术的逐渐成熟,栈上分配(对象只存在于某方法中,不会逃逸出去,因此方法出栈后就会销毁,此时对象可以在栈上分配,方便销毁),标量替换(新对象拥有的属性可以由现有对象替换拼凑而成,就没必要真正生成这个对象)等优化技术带来了一些变化,目前并非所有的对象都在堆上分配了。
当java堆上没有内存完成实例分配,并且堆大小也无法扩展是,将会抛出OutOfMemoryError异常。Java堆是垃圾收集器管理的主要区域。
方法区(线程共享)
方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。
对象的创建
虚拟机遇到一条new指令时,先检查常量池是否已经加载过相应的类,如果没有,必须先执行相应的类加载
类加载过后,分配内存,若java对中南内存时绝对规整的,使用“指针碰撞”方式分配内存;如果是不规整的,就从空闲列表中分配
划分内存时需要考虑并发问题,两种方式:CAS同步处理,或者本地线程分配缓冲。
内存空间初始化操作,接着是做一些必要的对象设置(元信息、哈希码。。。。。。),后执行方法
为对象分配内存
类加载完成后,接着会在java堆中划分一块内存分配给对象。内存分配根据java堆是否规整,有两种方法
指针碰撞:如果java堆的内存时规整的,即所有用过的内存放在一边,而空闲的放在一边。分配内存时将位于中间的指针指示器向空闲内存移动一段与对象大小相等的距离,这样便完成分配内存工作
空闲列表:如果java堆的内存时不规整的,则需要由虚拟机维护一个列表来记录那些内存是可用的,这样在分配的时候可以从列表中查询到足够大的内存分配给对象,并在分配后更新列表
选择那种分配方法时由java堆是否规整来决定的,而Java堆是否规整又由才用的垃圾收集器是否带有整理功能决定
垃圾回收器
一、常见的垃圾收集器有串行垃圾回收器(Serial)、并行垃圾回收器(Parallel)、并发清除回收器(CMS)、G1回收器。
垃圾回收算法种类
复制(coping)算法;(发生在eden区和两个Survior区域)
标记-清除(Mark-Sweep)算法;(发生在老年代)
标记-整理(Mark-Compact)算法。(发生在老年代)
-Xss:每个线程的栈大小
-Xms:设置堆的初始可用大小,默认物理内存的1/64
-Xmx:设置堆的最大可用大小,默认物理内存的1/4
-Xmn:新生代大小
-XX:NewRatio:默认2表示新生代占年老代的1/2,占整个堆内存的1/3。
-XX:SurvivorRatio:默认8表示一个survivor区占用1/8的Eden内存,即1/10的新生代内存。
关于元空间的JVM参数有两个:-XX:MetaspaceSize=N和 -XX:MaxMetaspaceSize=N
-XX:MaxMetaspaceSize: 元空间最大值, 默认-1, 即不限制,或者说只受限于本地内存大小。
-XX:MetaspaceSize: 指定元空间触发Fullgc的初始阈值(元空间无固定初始大小), 以字节为单位,默认是21M左右,达到该值就会触发full gc进行类型卸载,
同时收集器会对该值进行调整: 如果释放了大量的空间, 就适当降低该值; 如果释放了很少的空间,会适当提高该值( 如果设置了-XX:MaxMetaspaceSize,不会超过其最大值 )。
这个跟早期jdk版本的-XX:PermSize参数意思不一样,-XX:PermSize代表永久代的初始容量。
1、GC root原理
GC root原理:通过对枚举GCroot对象做引用可达性分析,即从GC root对象开始,向下搜索,形成的路径称之为 引用链。如果一个对象到GC roots对象没有任何引用,没有形成引用链,那么该对象等待GC回收。
2、GC root对象是什么?
Java中可以作为GC Roots的对象
1、虚拟机栈(javaStack)(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
2、方法区中的类静态属性引用的对象。
3、方法区中常量引用的对象。
4、本地方法栈中JNI(Native方法)引用的对象。
垃圾回收触发机制
首先需要知道,GC又分为minor GC 和 Full GC(major GC)。Full GC包含minor GC 和major GC,Java堆内存分为新生代和老年代,新生代
中又分为1个eden区和两个Survior区域。一般情况下,新创建的对象都会被分配到eden区(特殊情况时eden区放不下会会先触发minor GC一次,执行完还是放不下就会把比较大的对象分放入老年代),这些对象经过一个minor gc后仍然存活将会被移动到Survior区域中,对象在Survior中每熬过一个Minor GC,年龄就会增加一岁,当他的年龄到达一定程度时(15),就会被移动到老年代中。
当eden区满时,还存活的对象将被复制到survior区,当一个survior区满时,此区域的存活对象将被复制到另外一个survior区,当另外一个也满了的时候,从前一个Survior区复制过来(全部复制过来复制过去,方便年岁+1)的并且此时还存活的对象,将可能被复制到老年代。
Minor GC:从年轻代回收内存,当jvm无法为一个新的对象分配空间时会触发Minor GC,比如当Eden区满了。当内存池被填满的时候,其中的内容全部会被复制,指针会从0开始跟踪空闲内存。Eden和Survior区不存在内存碎片,写指针总是停留在所使用内存池的顶部。执行minor操作时不会影响到永久代,从永久代到年轻代的引用被当成GC roots,从年轻代到永久代的引用在标记阶段被直接忽略掉(永久代用来存放java的类信息)。如果eden区域中大部分对象被认为是垃圾,永远也不会复制到Survior区域或者老年代空间。如果正好相反,eden区域大部分新生对象不符合GC条件,Minor GC执行时暂停的线程时间将会长很多。Minor may call “stop the world”;
Full GC:是清理整个堆空间包括年轻代和老年代。那么对于Minor GC的触发条件:大多数情况下,直接在eden区中进行分配。如果eden区域没有足够的空间,那么就会发起一次Minor GC;对于FullGC的触发条件:如果老年代没有足够的空间,那么就会进行一次FullGC在发生MinorGC之前,虚拟机会先检查老年代最大可利用的连续空间是否大于新生代所有对象的总空间。如果大于则进行Minor GC,如果小于则看HandlePromotionFailure设置是否是允许担保失败(不允许则直接FullGC)如果允许,那么会继续检查老年代最大可利用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则尝试minor gc (如果尝试失败也会触发Full GC),如果小于则进行Full GC。但是,具体什么时候执行,这个是由系统来进行决定的,是无法预测的。
PermGen space永久代 和MetaSpace元数据区的区别?
不管是PermGen space 还是 MetaSpace 他们都是Hotspot针对方法区的一种实现,两者最大的区别在于PermGen space 是在JVM虚拟机中分配内存的,而Metaspace则是在虚拟机之外的系统本地分配内存。
因为很多类是在运行期间加载的,这部分类加载的空间不可控,如果这部分内存是在JVM内存里分配的话,永久代分配太大那么JVM其他区域(比如说堆)的内存就会变小,反之如果设置太小,就容易出现方法区内存溢出,因为本身存储的类信息属于不确定大小,类信息在我们运行的时候可以动态加载(代理)。所以jdk1.8中选择把Metaspace内存分配在本地内存,如果这样做的好处是]Metaspace空间的大小不会受限于虚拟机分配的内存大小,只会受限于机器内存,可分配的内存大了那么就不会那么容易出现内存溢。
Serial收集器
ParNew收集器
Serial Old收集器(百搭)
Parallel Scavenge收集器
Parallel Old收集器
CMS收集器
G1收集器
ZGC收集器(取代G1)
问题点:
srping解决循环依赖:
这三级缓存分别指:
singletonObjects:单例对象的cache
earlySingletonObjects :提前暴光的单例对象的Cache
singletonFactories : 单例对象工厂的cache
Spring首先从一级缓存singletonObjects中获取。如果获取不到,并且对象正在创建中,就再从二级缓存earlySingletonObjects中获取。如果还是获取不到且允许singletonFactories通过getObject()获取,就从三级缓存singletonFactory.getObject()(三级缓存)获取,如果获取到了则:
从singletonFactories中移除,并放入earlySingletonObjects中。其实也就是从三级缓存移动到了二级缓存。
为什么需要三级缓存:【有料】Spring Bean 循环依赖为什么需要三级缓存_daobuxinzi的博客-CSDN博客
spring初始化bean的过程如下:
1>首先尝试从一级缓存中获取serviceA实例,发现不存在并且serviceA不在创建过程中;
2>serviceA完成了初始化的第一步(实例化:调用createBeanInstance方法,即调用默认构造方法实例化);
3>将自己(serviceA)提前曝光到singletonFactories中;
4>此时进行初始化的第二步(注入属性serviceB),发现自己依赖对象serviceB,此时就尝试去get(B),发现B还没有被实create,所以走create流程;
5>serviceB完成了初始化的第一步(实例化:调用createBeanInstance方法,即调用默认构造方法实例化);
6>将自己(serviceB)提前曝光到singletonFactories中;
7>此时进行初始化的第二步(注入属性serviceA);
8>于是尝试get(A),尝试一级缓存singletonObjects(肯定没有,因为A还没初始化完全),尝试二级缓存earlySingletonObjects(也没有),尝试三级缓存singletonFactories,由于A通过ObjectFactory将自己提前曝光了,所以B能够通过ObjectFactory.getObject拿到A对象(虽然A还没有初始化完全,但是总比没有好呀);
9>B拿到A对象后顺利完成了初始化阶段1、2、3,完全初始化之后将自己放入到一级缓存singletonObjects中;
10>此时返回A中,A此时能拿到B的对象顺利完成自己的初始化阶段2、3,最终A也完成了初始化,进去了一级缓存singletonObjects中;
MVCC
多版本并发控制
readview:四大核心参数
当前最小事务ID
当前最大的已提交事务ID
当前活跃的事务列表
当前的事务ID
通过和undolog的事务版本链进行对比选择需要读取的数据
Redis分布式锁实现
setnx命令:表示SET if Not eXists,即如果 key 不存在,才会设置它的值,否则什么也不做
锁的释放
当客户端1操作完后,释放锁资源,即删除try_lock。那么此时客户端2再次尝试获取锁时,则会获取锁成功。
可重入锁。
先判断如果锁名不存在,则加锁。接下来,判断如果锁名和 线程ID,如value=线程ID则可重入操作
请问你用 Redis 做分布式锁的时候,如果指定过期时间到了,把锁给释放了。但是任务还未执行完成,导致任务再次被执行,这种情况你会怎么处理呢
分布式锁问题:用redisson框架,可以api多,看门狗机制(Redisson提供的分布式锁是支持锁自动续期的,也就是说,如果线程仍旧没有执行完,那么redisson会自动给redis中的目标key延长超时时间(1/3),这在Redisson中称之为 Watch Dog 机制。)
CAS:
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。
CAS的优点
利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。其它原子操作都是利用类似的特性完成的。而整个J.U.C都是建立在CAS之上的,因此对于synchronized阻塞算法,J.U.C在性能上有了很大的提升。
CAS存在的三大问题
1.ABA问题 2.循环时间长开销大 3.只能保证一个共享变量的原子操作
利用版本号比较可以有效解决ABA问题。
https://www.cnblogs.com/shoshana-kong/p/10833837.html
AQS 原理:
1.AQS内部维护了一个int类型的变量state,用户表示加锁的状态。初始值为0,表示没有加锁
2.还维护一个关键变量,用来表示当前加锁的线程,初始值为null
3.如果此时有线程1过来加锁(比如调用实现类ReentrantLock的lock()方法尝试加锁),就会用CAS操作将state值从0改为1,如果修改成功,再把加锁线程设为自己。
4.如果此时,线程2过来加锁,会发现state不是0,再判断锁线程是否是自己(如果是,
直接将state加1),发现不是,所以此时线程2就会加锁失败。接着,线程2会
将自己加入AQS内部的一个等待队列,进行排队,等待线程1释放锁。
5.当线程1执行完操作,释放了锁(将state减一,如果是0,则彻底释放锁,将“加锁线程“设为null),然后,会从等待队列对头唤醒线程2重新获取锁。
6.线程2重新尝试获取锁,此时执行CAS操作,将state置为1,将加锁线程改为自己,同时线程2从等待队列中出队。
https://www.cnblogs.com/ericz2j/p/13445822.html
bean的存储:
bean对象最终存储在spring容器中,我们简单的、狭义上的spring容器,在spring源码底层就是一个map集合,这个map集合存储的key是当前bean的name,如果不指定,默认的是class类型首字母小写作为key,value是bean对象。存储bean的map在
当Spring容器扫描到Bean类时 , 会把这个类的描述信息, 以包名加类名的方式存到beanDefinitionMap 中,
Map<String,BeanDefinition> , 其中 String是Key , 默认是类名首字母小写 , BeanDefinition , 存的是类的定义(描述信息) , 我们通常叫BeanDefinition接口为 : bean的定义对象。
1.spring在将class文件转换为beanDefinition的时候,是没有额外的要求的,不管这个bean是单例的,还是原型的,还是懒加载的,都会扫描出来,统一放到beanDefinitionMap集合中。
2.在第二步去初始化bean的时候,才会判断当前bean是否是原型的、是否是懒加载的、是否是有依赖关系的,等
三级缓存的应用时机
Bean对象在被实例化后会放入第三级缓存(addSingletonFactory)
二级缓存的应用时机
在Bean对象被放入三级缓存后,以后的操作如果要进入缓存查询,就会将
三级缓存中的Bean对象移动到二级缓存**,此时放在三级缓存的Bean对象会被移除
一级缓存的应用时机
在Bean对象创建完毕后会放入一级缓存。(addSingleton)
在要用到一个Bean对象之前,都会尝试从缓存中获取,最先就是判断一级缓存中存不存在,所以称“一级”。
Zookeeper集群中节点之间数据同步:
1.⾸先集群启动时,会先进⾏领导者选举,确定 哪个节点是Leader ,哪些节点是 Follower 和 Observer
2.然后Leader会和其他节点进⾏ 数据同步 ,采⽤ 发送快照 和 发送Diff⽇志 的⽅式
3.集群在⼯作过程中,所有的写请求都会交给Leader节点来进⾏处理,从节点只能处理读请求
4.Leader节点收到⼀个写请求时,会通过 两阶段机制 来处理
5.Leader节点会将该写请求对应的⽇志发送给其他Follower节点并等待Follower节点持久化⽇志成功
6.Follower节点收到⽇志后会进⾏持久化,如果持久化成功则发送⼀个Ack给Leader节点
7.当Leader节点收到半数以上的 Ack 后,就会开始提交,先更新Leader节点本地的内存数据
8.然后发送 commit 命令给 Follower 节点,Follower节点收到commit命令后就会 更新各⾃本地内存数据
9.同时Leader节点还是将当前写请求直接发送给Observer节点,Observer节点收到Leader发过来的写请求后直接执⾏更新本地内存数据
10.最后Leader节点返回客户端写请求响应成功
11.通过 同步机制 和 两阶段提交机制 来达到 集群中节点数据⼀致
zk选举
服务刚起时:
ZAB选举中的每个节点都记录三个元素,节点ID、事务ID、选举轮数,投票信息包括节点ID(被选举节点)、事务ID(投票节点具有的)。
选举原则:事务ID最大当选,事务ID一致则节点ID最大的当选。
服务启动后:启动后先对比选举轮数后对比事物ID再节点ID(轮数最大其实事物ID也是最大)
ZAB选举涉及四种状态:
选举状态,选举过程中所有节点的状态,观察者节点除外
领导状态,领导节点的状态,领导节点可以向其他节点广播合同步信息。
跟随状态,非领导节点的状态
观察状态,观察者具有的状态,无投票合选举权
选举流程
ZAB选举中的每个节点都记录三个元素,节点ID、事务ID、选举轮数,投票信息包括节点ID(被选举节点)、事务ID(投票节点具有的)。
选举原则:事务ID最大当选,事务ID一致则节点ID最大的当选。
1.hashmap线程安全问题
2.分库分表及查询问题
方法一:全局视野法
(1)将order by time offset X limit Y,改写成order by time offset 0 limit X+Y
(2)服务层对得到的N*(X+Y)条数据进行内存排序,内存排序后再取偏移量X后的Y条记录
这种方法随着翻页的进行,性能越来越低。
方法二:业务折衷法-禁止跳页查询
(1)用正常的方法取得第一页数据,并得到第一页记录的time_max
(2)每次翻页,将order by time offset X limit Y,改写成order by time where time>$time_max limit Y
以保证每次只返回一页数据,性能为常量。
方法三:业务折衷法-允许模糊数据
(1)将order by time offset X limit Y,改写成order by time offset X/N limit Y/N
方法四:二次查询法
(1)将order by time offset X limit Y,改写成order by time offset X/N limit Y
(2)找到最小值time_min
(3)between二次查询,order by time between $time_min and $time_i_max
(4)设置虚拟time_min,找到time_min在各个分库的offset,从而得到time_min在全局的offset
(5)得到了time_min在全局的offset,自然得到了全局的offset X limit Y
3.垃圾回收器区别
CSM:
1、初始标记
初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。初始标记的过程是需要触发STW的,不过这个过程非常快,而且初试标记的耗时不会因为堆空间的变大而变慢,是可控的,因此可以忽略这个过程导致的短暂停顿。
2、并发标记
并发标记就是将初始标记的对象进行深度遍历,以这些对象为根,遍历整个对象图,这个过程耗时较长,而且标记的时间会随着堆空间的变大而变长。不过好在这个过程是不会触发STW的,用户线程仍然可以工作,程序依然可以响应,只是程序的性能会受到一点影响。因为GC线程会占用一定的CPU和系统资源,对处理器比较敏感。CMS默认开启的GC线程数是:(CPU核心数+3)/4,当CPU核心数超过4个时,GC线程会占用不到25%的CPU资源,如果CPU数不足4个,GC线程对程序的影响就会非常大,导致程序的性能大幅降低。
3、重新标记
由于并发标记时,用户线程仍在运行,这意味着并发标记期间,用户线程有可能改变了对象间的引用关系,可能会发生两种情况:一种是原本不能被回收的对象,现在可以被回收了,另一种是原本可以被回收的对象,现在不能被回收了。针对这两种情况,CMS需要暂停用户线程,进行一次重新标记。
4、并发清理
重新标记完成后,就可以并发清理了。这个过程耗时也比较长,且清理的开销会随着堆空间的变大而变大。不过好在这个过程也是不需要STW的,用户线程依然可以正常运行,程序不会卡顿,不过和并发标记一样,清理时GC线程依然要占用一定的CPU和系统资源,会导致程序的性能降低。
CMS收集器的优缺点:
1、对处理器敏感
并发标记、并发清理阶段,虽然CMS不会触发STW,但是标记和清理需要GC线程介入处理,GC线程会占用一定的CPU资源,进而导致程序的性能下降,程序响应速度变慢。CPU核心数多的话还稍微好一点,CPU资源紧张的情况下,GC线程对程序的性能影响非常大。
2、浮动垃圾
并发清理阶段,由于用户线程仍在运行,在此期间用户线程制造的垃圾就被称为“浮动垃圾”,浮动垃圾本次GC无法清理,只能留到下次GC时再清理。
3、并发失败
由于浮动垃圾的存在,因此CMS必须预留一部分空间来装载这些新产生的垃圾。CMS不能像Serial Old收集器那样,等到Old区填满了再来清理。在JDK5时,CMS会在老年代使用了68%的空间时激活,预留了32%的空间来装载浮动垃圾,这是一个比较偏保守的配置。如果实际引用中,老年代增长的不是太快,可以通过-XX:CMSInitiatingOccupancyFraction参数适当调高这个值。到了JDK6,触发的阈值就被提升至92%,只预留了8%的空间来装载浮动垃圾。
如果CMS预留的内存无法容纳浮动垃圾,那么就会导致「并发失败」,这时JVM不得不触发预备方案,启用Serial Old收集器来回收Old区,这时停顿时间就变得更长了。
4、内存碎片
由于CMS采用的是「标记清除」算法,这就意味这清理完成后会在堆中产生大量的内存碎片。内存碎片过多会带来很多麻烦,其一就是很难为大对象分配内存。导致的后果就是:堆空间明明还有很多,但就是找不到一块连续的内存区域为大对象分配内存,而不得不触发一次Full GC,这样GC的停顿时间又会变得更长。
针对这种情况,CMS提供了一种备选方案,通过-XX:CMSFullGCsBeforeCompaction参数设置,当CMS由于内存碎片导致触发了N次Full GC后,下次进入Full GC前先整理内存碎片,不过这个参数在JDK9被弃用了。
三色标记算法(重点)
介绍完CMS垃圾收集器后,我们有必要了解一下,为什么CMS的GC线程可以和用户线程一起工作。
JVM判断对象是否可以被回收,绝大多数采用的都是「可达性分析」算法,关于这个算法,可以查看笔者以前的文章:大白话理解可达性分析算法。
从GC Roots开始遍历,可达的就是存活,不可达的就回收。
CMS将对象标记为三种颜色:
标记的过程大致如下:
刚开始,所有的对象都是白色,没有被访问。
将GC Roots直接关联的对象置为灰色。
遍历灰色对象的所有引用,灰色对象本身置为黑色,引用置为灰色。
重复步骤3,直到没有灰色对象为止。
结束时,黑色对象存活,白色对象回收。
这个过程正确执行的前提是没有其他线程改变对象间的引用关系,然而,并发标记的过程中,用户线程仍在运行,因此就会产生漏标和错标的情况。
漏标、错标
CMS为了让GC线程和用户线程一起工作,回收的算法和过程比以前旧的收集器要复杂很多。究其原因,就是因为GC标记对象的同时,用户线程还在修改对象的引用关系。因此CMS引入了三色算法,将对象标记为黑、灰、白三种颜色的对象,并通过「写屏障」技术将用户线程修改的引用关系记录下来,以便在「重新标记」阶段可以修正对象的引用。
虽然CMS从来没有被JDK当做默认的垃圾收集器,存在很多的缺点,但是它开启了「GC并发收集」的先河,为后面的收集器提供了思路,光凭这一点,就依然值得记录下来。
G1收集器:
G1收集器采用“标记-复制”和“标记-整理”。从整体上看是基于“标记-整理”,从局部看,两个region之间是“标记-复制”。
它具备以下特点:
①分区收集:虽然G1可以不需要其他收集器配合就能独⽴管理整个GC堆,但是还是保留了分代的概念。G1最大的特点是引入分区的思路,弱化了分代的概念。
②并⾏与并发:G1能充分利⽤CPU、多核环境下的硬件优势,使⽤多个CPU(CPU或者CPU核⼼)来缩短Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执⾏的GC动作,G1收集器仍然可以通过并发的⽅式让java程序继续执⾏。
③算法,空间整合:与CMS的“标记–清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
④可预测的停顿:这是G1相对于CMS的另⼀个⼤优势,降低停顿时间是G1 和 CMS 共同的关注点,但G1 除了追求低停顿外,还能建⽴可预测的停顿时间模型,能让使⽤者明确指定在⼀个⻓度为M毫秒的时间⽚段内,消耗在垃圾收集上的时间不得超过N毫秒。
1)G1执行的第一阶段:初始标记(Initial Marking )
这个阶段是STW(Stop the World )的,所有应用线程会被暂停,标记出从GC Root开始直接可达的对象。
2)G1执行的第二阶段:并发标记
从GC Roots开始对堆中对象进行可达性分析,找出存活对象,耗时较长。当并发标记完成后,开始最终标记(Final Marking )阶段
3)最终标记(标记那些在并发标记阶段发生变化的对象,将被回收),这阶段需要停顿线程,但是可以并行执行。
4)筛选回收
暂停用户线程,筛选阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。
G1收集器在后台维护了⼀个优先列表,每次根据允许的收集时间,优先选择回收价值最⼤的Region(这也就是它的名字Garbage-First的由来)。这种使⽤Region划分内存空间以及有优先级的区域回收⽅式,保证了GF收集器在有限时间内可以尽可能⾼的收集效率(把内存化整为零)。
对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉个旧的Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
G1收集器只有并发标记不会stop the world,而CMS并发标记和并发清除不会stop the world
最后,G1中还提供了两种垃圾回收模式,Young GC和Mixed GC,两种都是Stop The World(STW)的。
4.b树 b+树
B树:
一棵m阶B树或为空树,或为满足如下特征的m叉树:
1)树中每个结点至多有m棵子树。(即至多含有m-1个关键字)(“两棵子树指针夹着一个关键字”)
2)若根结点不是终端结点(最底层非叶子结点),则至少含有两棵子树。
3)除根结点外的所有非叶子结点至少含有 ceil(m/2) 棵子树。(即至少含有ceil(m/2)-1个关键字)
4)所有叶结点的结构如下:
B+树
B+树是常用于数据库和操作系统的文件系统中的一种用于查找的数据结构。
m阶B+树与m阶的B树的主要差异在于:
(1)在B+树中,具有n个关键字的结点只含有n棵子树,即每个关键字对应一棵子树;而在B树中,具有n个结点的关键字含有(n+1)棵子树。
(2)在B+树中,每个结点(非根内部结点)关键字个数n的范围是ceil(m/2)<=n<=m(根节点1<=n<=m),在B树中,每个结点(非根内部结点)关键字个数n的范围是ceil(m/2)-1<=n<=m-1(根节点:1<=n<=m-1)。
(3)在B+树中,叶结点包含信息,所有非叶结点仅起到索引作用,非叶结点中的每个索引项只含有对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址。
(4)在B+树中,叶结点包含了全部关键字,即在非叶结点中出现的关键字也会出现在叶节点中;而在B树中,叶结点包含的关键字和其他节点包含的关键字是不重复的。
(5)在B+树中,有一个指针指向关键字最小的叶子结点,所有叶子结点连接成一个链表。