问题集锦

JVM-Class类文件结构

常量池:字面量(字符串和final常量)和符号引用(类和接口的全限定名、字段的名称和描述符、方法句柄和方法类型、方法的名称和描述符 )
字段表、方法表、属性表(code属性存放代码)

JVM-运行时数据区域

方法区(线程共享,可能会内存溢出):用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据
1、以前使用永久代来实现方法区导致Java应用更容易遇到内存溢出的问题,现在使用本地内存来实现方法区
2、JDK 7的HotSpot,已经把原本放在永久代的字符串常量池、静态变量等移出(移到堆内)
3、在JDK 8,完全废弃了永久代,使用本地内存中实现的元空间来代替,把JDK 7中永久代还剩余的内容(主要是类型信息)全部移到元空间中
4、运行时常量池:Class文件中常量池在类加载后存放到方法区的运行时常量池中(也存放直接引用)
堆(线程共享,可能会内存溢出):
1、存放对象实例, “几乎”所有的对象实例都在这里分配内存(由于即时编译技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配、标量替换优化手段已经导致对象可以不在堆内分配)
2、堆中可以划分出多个线程私有的分配缓冲区 ,以更好地回收内存,或者更快地分配内存
3、通过参数-Xmx和-Xms设定堆内存大小
直接内存(可能会内存溢出):
1、直接内存并不是虚拟机运行时数据区的一部分,也不是Java中的内存区域
2、应用:NIO是一种基于通道(Channel)与缓冲区 (Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的 DirectByteBuffer对象作为这块内存的引用进行操作,这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据(零拷贝)
栈(线程私有,可能会内存溢出和堆栈溢出):
1、每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法返回地址等信息
2、每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程
局部变量表(栈帧中,大小在编译期已经确定,code属性中存储大小):
1、用于存放方法参数和方法内部定义的局部变量(基本类型和对象引用)、returnAddress 类型(指向了一条字节码指令的地址),这些数据类型在局部变量表中的存储空间以局部变量槽来表示
2、当一个方法被调用时,使用局部变量表来完成实参到形参的传递
3、如果执行的是实例方法(没有被static修饰的方法),局部变量表中第0位存放this引用,在方法中直接访问
4、局部变量表中的变量槽是可以重用的;复用的时机是其他变量需要用到这块变量槽,因此即使该变量已经不被使用,也不会垃圾回收,除非赋值null(不建议,会被优化掉)
5、局部变量不像类变量那样存在“准备阶段”,类变量有两次赋初始值的过程,一次在准备阶段,赋予系统初始值;另外一次在初始化阶段,赋予代码定义的初始值,因此即使在初始化阶段代码没有为类变量赋值也没有关系,类变量仍然具有一个确定的初始值,不会产生歧义;但局部变量就不一样了,如果一个局部变量定义了但没有赋初始值,那它是完全不能使用的
操作数栈(栈帧中,大小在编译期已经确定,code属性中存储大小):
1、两个栈帧会出现一部分重叠,让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样做不仅节约了一些空间,更重要的是在进行方法调用时就可以直接共用一部分数据,无须进行额外的参数复制传递
动态连接(栈帧中):
1、每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接
2、Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数,这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析;另外一部分将在每一次运行期间都转化为直接引用,这部分就称为动态连接
方法返回地址(栈帧中):方法退出时使用
程序计数器(线程私有,不会内存溢出):字节码解释器工作时通过改变计数器的值来选取下一条需要执行的字节码指令(线程切换时为了标识每个线程执行位置)
执行引擎:
1、在不同的虚拟机实现中,执行引擎在执行字节码的时候,通常会有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种选择,也可能两者兼备,还可能会有同时包含几个不同级别的即时编译器一起工作的执行引擎
2、从外观上来看,所有的Java虚拟机的执行引擎输入、输出都是一致的:输入的是字节码二进制流,处理过程是字节码解析执行的等效过程,输出的是执行结果
方法调用:
1、方法调用并不等同于方法中的代码被执行,方法调用阶段唯一的任务就是确定被调用方法的版本 (即调用哪一个方法),暂时还未涉及方法内部的具体运行过程
2、Class文件的编译过程中不包含传统程序语言编译的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是直接引用
3、解析:所有方法调用的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,即调用目标在程序代码写好、编译器进行编译那一刻就已经确定下来,这类方法的调用被称为解析
(静态方法、私有方法、final方法、构造方法、父类方法)
4、分派
**静态分派:重载
引用类型和实际类型在程序中都可能会发生变化,区别是引用类型的变化仅仅在使用时发生,引用类型不会被改变,并且最终的引用类型是在编译期可知的;而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么

// 实际类型变化 
Human human = (new Random()).nextBoolean() ? new Man() : new Woman(); 
// 引用类型变化 
sr.sayHello((Man) human) ;
sr.sayHello((Woman) human);

虚拟机(或者准确地说是编译器)在重载时是通过参数的引用类型而不是实际类型作为判定依据的,由于引用类型在编译期可知,所以在编译阶段,Javac编译器就根据参数的引用类型决定了会使用哪个重载版本
所有依赖引用类型来决定方法执行版本的分派动作,都称为静态分派,静态分派的最典型应用表现就是方法重载,静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的
**动态分派 覆盖
在运行期根据实际类型确定方法执行版本的分派过程称为动态分派
**单分派和多分配派 重载+覆盖
方法的接收者与方法的参数统称为方法的宗量
单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择
编译阶段:根据方法接收者的引用类型+方法参数类型才能确定使用哪个类的哪个方法,因此静态分派属于多分派类型
运行阶段:根据方法接收者的实际类型就能确定使用哪个类(会覆盖静态分派的判断,使用哪个方法是静态分派确定),因此动态分派属于单分派类型

JVM-类加载机制

定义:Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型的过程
在Java语言里面,类型的加载、连接和初始化过程都是在程序运行期间完成的
类的生命周期:加载 、连接(验证、准备、解析)、初始化 、使用和卸载
类加载的时机
对于初始化阶段,有且只有6种情况必须对类进行初始化(主动引用),其余都是被动引用:
1、new对象、读写类型静态字段(final常量除外,编译期已放入常量池)、调用类型静态方法时
2、对类型反射调用时
3、初始化子类时,要先初始化父类
4、虚拟机启动时,初始化主类
5、方法句柄对应的类初始化
6、接口定义了默认方法,实现类初始化时,要先初始化接口
被动引用:
1、通过子类引用父类的静态字段,不会导致子类初始化(对于静态字段, 只有直接定义这个字段的类才会被初始化)
2、通过数组定义来引用类,不会触发此类的初始化(与数组是不同的类)
3、常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化(在编译阶段通过常量传播优化,已经将常量的值直接存储在调用类的常量池中,以后调用类对常量的引用,实际都被转化为对自身常量池的引用了;也就是说,实际上调用类的Class文件之中并没有原类的符号引用入口,这两个类在编译成 Class文件后就已经不存在任何联系了)
类加载的过程
加载:
1、通过类的全限定名获取定义类的二进制字节流(非数组类型的加载既可以使用引导类加载器来完成,也可以由用户自定义的类加载器去完成(重写一个类加载器的findClass或loadClass方法);数组类本身不通过类加载器创建,是由Java虚拟机直接在内存中动态构造出来的,但数组类与类加载器仍然有很密切的关系,因为数组类的元素类型最终还是要靠类加载器来完成加载)
2、将字节流所代表的静态存储结构转化为方法区的运行时数据结构
3、在内存中生成代表类的Class对象,作为方法区中类的各种数据的访问入口
验证:
1、文件格式验证
2、元数据验证(是否有父类、是否继承了不该继承的如final类、是否实现了抽象类的所有方法等)
3、字节码验证:对类的方法体(Class文件中的code属性)进行检验分析
4、符号引用验证:将符号引用转换为直接引用时验证(解析阶段),通过全限定名是否能找到对应的类等
准备:
1、准备阶段是正式为类变量分配内存并设置初始值(默认值)的阶段,这些类变量所使用的内存都在方法区(jdk1.8在堆中)中进行分配
2、实例变量将会在对象实例化时随着对象一起分配在Java堆中
3、类常量在这个阶段会被初始化(不是默认值)
解析:将常量池内的符号引用替换为直接引用
1、符号引用:符号引用与虚拟机实现的内存布局无关,引用的目标并不一定是已经加载到虚拟机内存当中的内容
2、直接引用:直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄,直接引用是和虚拟机实现的内存布局直接相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在
初始化:
1、初始化阶段就是执行类构造器方法(所有类变量的赋值动作和静态语句块(static{}块)中的语句,执行顺序是代码中的顺序)的过程,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问
2、父类的类构造器方法先执行,父类中定义的静态语句块要优先于子类的变量赋值操作
3、Java虚拟机必须保证一个类的类构造器方法在多线程环境中被正确地加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的类构造器方法,其他线程都需要阻塞等待,直到活动线程执行完毕类构造器方法,如果在一个类的类构造器方法中有耗时很长的操作,那就可能造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的
4、同一个类加载器下,一个类型只会被初始化一次
类加载器
1、类加载器通过一个类的全限定名来获取描述该类的二进制字节流
2、对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间,即比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等

线程-java内存模型

JUC-AQS

1、AQS支持独占锁(如ReentrantLock、ReadWriteLock的写锁)和共享锁(如CountDownLatch、ReadWriteLock的读锁)
2、state:表示资源数,volatile修饰,需要保证可见性、有序性、利用CAS保证原子性操作
3、队列节点等待状态:初始状态、取消状态(线程超时或被中断时,会进入取消状态,取消状态不会再往下执行)、唤醒状态(表示当前节点的后继节点正在等待获取资源,当前节点在release或cancel时需要执行unpark来唤醒后继节点)、条件状态(当前节点为条件队列节点,这个状态在同步队列里不会被用到)、传播状态(针对共享锁,设置在head节点releaseShared(释放共享锁)操作需要被传递到下一个节点,用来保证后继节点可以获取共享资源)
4、nextWaiter属性:连接下一个等待condition的节点,或者在共享模式下作为一个特殊节点保存,用来判断是否为共享模式
5、同步队列:双向链表,队尾插入时会有线程竞争(自旋+CAS插入,需要前驱有效节点唤醒,使用LockSupport.park/unpark阻塞释放线程);队首表示获取资源的线程节点
**acquire方法:调用子类的tryAcquire方法尝试CAS获取资源,成功直接返回;失败则阻塞线程,创建节点,自旋+CAS将节点放入同步队列尾部,当前驱有效节点是头节点时,自旋+CAS尝试获取资源(前驱有效节点不为头节点时,阻塞当前线程,直到被前驱节点唤醒;即使当前线程中间被中断过,也可以自旋+CAS尝试获取资源);如果自旋+CAS尝试获取资源出现异常,要将当前节点状态置为取消
**release方法:调用子类的tryRelease方法尝试自旋+CAS释放资源,唤醒同步队列中的后继节点(中间会移除取消状态的节点),后继节点自旋+CAS尝试获取资源,获取成功,则从acquire方法返回
**acquireShared方法:调用子类的tryAcquireShared方法尝试CAS获取资源,成功直接返回;失败则阻塞线程,创建节点,自旋+CAS将节点放入同步队列尾部,当前驱有效节点是头节点时,自旋+CAS尝试获取资源(前驱有效节点不为头节点时,阻塞当前线程,直到被前驱节点唤醒;即使当前线程中间被中断过,也可以自旋+CAS尝试获取资源),获取资源成功后,将当前节点设置为头节点,如果还有剩余资源,让后续节点也尝试获取资源;如果自旋+CAS尝试获取资源出现异常(超时),要将当前节点状态置为取消
**releaseShared方法:调用子类的tryReleaseShared方法尝试自旋+CAS释放资源,唤醒同步队列中的后继节点(中间会移除取消状态的节点),后继节点自旋+CAS尝试获取资源,获取成功,则从acquireShared方法返回
6、子类需要实现的方法:tryAcquire/tryRelease(独占)、tryAcquireShared/tryReleaseShared(共享)、isHeldExclusively(有用到Condition才需要实现)
7、条件队列:双向链表,只有在使用了Condition(AQS的内部类)才会存在条件队列,在使用Condition的方法之前需要先获取锁
**await方法:调用之前当前线程需要先获取资源;创建节点,自旋+CAS将节点放入条件队列尾部;调用release方法释放当前线程已经持有的资源,并移除同步队列节点,唤醒同步队列中的后继节点,如果释放资源出现异常(超时),要将当前节点状态置为取消;当当前线程没有加入到同步队列时,进行自旋,并阻塞当前线程,直到当前线程被唤醒(从条件队列移到同步队列)或期间被中断,并记录中断状态;在同步队列中自旋+CAS尝试获取资源;如果当前节点的nextWaiter不为空,说明节点在获取锁时由于异常或者被中断而被取消,此时需要移除等待队列中取消状态的节点;如果期间被中断过,抛出中断异常
**signal方法:自旋+CAS将条件队列中节点移除,并创建新节点,添加到同步队列尾部,并唤醒线程,在同步中自旋+CAS尝试获取资源,获取成功从await方法返回


JUC-ReentrantLock

lock是显式的获取锁,拥有锁获取与释放的可操作性、非阻塞获取锁、可中断的获取锁(获取到锁的线程能响应中断:当获取到锁的线程被中断时,锁也会被释放)以及超时获取锁(如果超时未获取锁,会返回)等多种synchronized关键字所不具备的同步特性;不能将获取lock锁的过程写在try块中,因为如果在获取锁时发生了异常(已经获取锁),异常抛出时,锁也会被释放
ReentrantLock是一个可重入的互斥锁,也被称为“独占锁”,“独占锁”在同一个时间点只能被一个线程持有,而“可重入锁”可以被单个线程多次获取;ReentrantLock又分为“公平锁”和“非公平锁”(默认),它们的区别体现在获取锁的机制上:在“公平锁”的机制下,线程依次排队获取锁;而“非公平锁”机制下,如果锁是可获取状态,不管自己是不是在队列的head节点都会去尝试获取锁
内部有一个Sync类继承自AQS,有两个子类FairSync和NonfairSync
1、lock方法:
非公平:调用NonfairSync中的lock方法;尝试CAS获取资源,成功,设置当前线程持有资源;失败,调用AQS的acquire方法获取资源;调用NonfairSync中的tryAcquire方法尝试CAS获取资源(不会判断当前节点前是否还有节点,与公平锁区别):如果没有线程获取资源,CAS尝试获取资源,设置当前线程持有资源,如果当前线程已经获取过资源(可重入),在原来基础上+1(不需要CAS,因为无竞争),成功直接返回;失败则阻塞线程,创建节点,自旋+CAS将节点放入同步队列尾部,当前驱有效节点是头节点时,自旋+CAS尝试获取资源(前驱有效节点不为头节点时,阻塞当前线程,直到被前驱节点唤醒;即使当前线程中间被中断过,也可以自旋+CAS尝试获取资源);如果自旋+CAS尝试获取资源出现异常,要将当前节点状态置为取消
公平:调用FairSync中的lock方法;调用AQS的acquire方法获取资源;调用FairSync中的tryAcquire方法尝试CAS获取资源(要判断当前节点前是否还有节点,与非公平锁区别):如果没有线程获取资源,CAS尝试获取资源,设置当前线程持有资源,如果当前线程已经获取过资源(可重入),在原来基础上+1(不需要CAS,因为无竞争),成功直接返回;失败则阻塞线程,创建节点,自旋+CAS将节点放入同步队列尾部,当前驱有效节点是头节点时,自旋+CAS尝试获取资源(前驱有效节点不为头节点时,阻塞当前线程,直到被前驱节点唤醒;即使当前线程中间被中断过,也可以自旋+CAS尝试获取资源);如果自旋+CAS尝试获取资源出现异常,要将当前节点状态置为取消
2、unLock方法:公平和非公平一致;调用AQS的release方法释放资源;调用Sync的tryRelease方法尝试释放资源:获取资源的次数必须要等于释放资源的次数,这样才算是真正释放了资源,才可以设置持有资源的线程为空,不需要CAS,因为无竞争;唤醒同步队列中的后继节点(中间会移除取消状态的节点),后继节点自旋+CAS尝试获取资源,获取成功,则从acquire方法返回
3、tryLock方法:调用Sync的nonfairTryAcquire方法尝试请求获取资源(不会判断当前节点前是否还有节点,非公平):如果没有线程获取资源,CAS尝试获取资源,设置当前线程持有资源,如果当前线程已经获取过资源(可重入),在原来基础上+1(不需要CAS,因为无竞争),成功直接返回;失败则阻塞线程,创建节点,自旋+CAS将节点放入同步队列尾部,当前驱有效节点是头节点时,自旋+CAS尝试获取资源(前驱有效节点不为头节点时,阻塞当前线程,直到被前驱节点唤醒;即使当前线程中间被中断过,也可以自旋+CAS尝试获取资源);如果自旋+CAS尝试获取资源出现异常,要将当前节点状态置为取消
4、等待通知机制:使用AQS的Condition实现

JUC-CountDownLatch

CountDownLatch是一个同步辅助类,是通过AQS实现的一个可重入的共享锁,可响应中断,会直接抛出中断异常;在其他线程完成操作之前,可以有一个或多个线程等待;内部有一个Sync类,继承自AQS,实现了tryAcquireShared和tryReleaseShared方法
1、await方法:调用AQS的acquireSharedInterruptibly方法获取共享资源;回调子类Sync的tryAcquireShared方法尝试获取资源(当state=0的时候才可以获取资源),成功直接返回;失败则阻塞线程,创建节点,自旋+CAS将节点放入同步队列尾部,当前驱有效节点是头节点时,自旋+CAS尝试获取资源(前驱有效节点不为头节点时,阻塞当前线程,直到被前驱节点唤醒;即使当前线程中间被中断过,也可以自旋+CAS尝试获取资源),获取资源成功后,将当前节点设置为头节点,如果还有剩余资源,让后续节点也尝试获取资源;如果自旋+CAS尝试获取资源出现异常(超时),要将当前节点状态置为取消;如果中间被中断,直接抛出中断异常
2、countDown方法:调用AQS的releaseShared方法释放共享资源;回调子类Sync的tryReleaseShared方法尝试自旋+CAS释放资源,唤醒同步队列中的后继节点(中间会移除取消状态的节点),后继节点自旋+CAS尝试获取资源,获取成功,则从acquireShared方法返回

JUC-ReentrantReadWriteLock

ReentrantReadWriteLock维护了一对相关的锁:共享锁readLock和独占锁writeLock
共享锁readLock用于读操作,能同时被多个线程获取;独占锁writeLock用于写入操作,只能被一个线程持有(支持锁降级:持有写锁的线程可以在写锁未释放之前获得读锁)
Condition只有在写锁中用到,读锁是不支持Condition的
内部有一个Sync类继承自AQS,有两个子类FairSync和NonfairSync:读锁和写锁共用一个状态,高16位标识读计数了,低16位标识写重入次数;内部有一个静态内部类继承自ThreadLocal用于读记录读线程重入次数
内部有两个静态内部类:ReadLock和WriteLock
1、ReadLock:内部有lock和unLock方法,参考CountDownLatch
**lock方法:调用AQS的acquireShared方法获取资源;回调Sync的tryAcquireShared方法尝试CAS获取资源(持有写锁的线程可以继续获取读锁,没有线程获取写锁,可以尝试CAS获取资源),成功,更新当前线程锁重入次数,直接返回;失败,则阻塞线程,创建节点,自旋+CAS将节点放入同步队列尾部,当前驱有效节点是头节点时,自旋+CAS尝试获取资源(前驱有效节点不为头节点时,阻塞当前线程,直到被前驱节点唤醒;即使当前线程中间被中断过,也可以自旋+CAS尝试获取资源),获取资源成功后,将当前节点设置为头节点,如果还有剩余资源,让后续节点也尝试获取资源;如果自旋+CAS尝试获取资源出现异常(超时),要将当前节点状态置为取消;如果中间被中断,直接抛出中断异常
**unLock方法:调用AQS的releaseShared方法释放共享资源;回调子类Sync的tryReleaseShared方法尝试自旋+CAS释放资源,更新线程重入次数;唤醒同步队列中的后继节点(中间会移除取消状态的节点),后继节点自旋+CAS尝试获取资源,获取成功,则从acquireShared方法返回
2、WriteLock:内部有lock和unLock方法,参考ReentrantLock

Spring-IOC

1、解析注册:使用Resource定位xml配置;使用BeanDefinitionReader读取配置,并封装成BeanDefinition;使用BeanDefinitionRegistry将BeanDefinition注册到BeanDefinitionMap中
2、BeanFatory中bean的加载过程
**转换对应的beanName:传入的参数可能不是bean的name,可能是别名、FactoryBean(&开头)
**如果是单例,尝试从缓存中获取单例bean,获取失败再尝试从singletonFactories中获取单例工厂,通过单例工厂去加载(在创建单例bean时为解决依赖注入,不等bean创建完成就将创建bean的工厂提早曝光并加入缓存中,其他单例bean创建时如果需要依赖该bean,直接从缓存中获取单例bean或获取工厂去创建)(调用工厂的getObject方法先获取实例化但还未初始化的单例bean,加入到earlySingletonObjects缓存中,将单例工厂从singletonFactories中移除,返回单例bean),单例bean的转换:获取的bean可能是原始状态(有可能获取的是Factorybean,需要调用FactoryBean中的getObject方法获取单例bean)
**如果缓存中没有,以下开始创建单例bean
**根据beanName尝试从beanDefinitionMap中获取对应的beanDefinition中的配置,如果获取不到配置,尝试递归根据parentBeanFactory去加载(调用父类工厂的getBean方法)
**前置处理:创建单例bean之前,记录单例bean正在创建状态,用于检测循环依赖
**通过单例工厂创建单例bean,调用单例工厂的getObject方法获取提早曝光的单例bean(实例化还未初始化):处理override属性:为了后面实例化单例bean时更好的处理,这里先预先判断一下是否需要覆盖或重载,后面处理的原理就是在实例化bean时如果检测到methodOverrides时,会动态地为当前bean生成代理并使用对应的拦截器为bean做增强处理;实例化单例bean前处理;短路处理:Spring AOP代理实现,如果需要使用代理bean且代理bean已经创建,直接返回;实例化单例bean(将BeanDefinition转换为BeanWrapper(对反射相关API的简单封装,使得上层使用反射完成相关的业务逻辑大大的简化),需要选择不同的实例化策略:如果有需要覆盖或动态替换的方法,需要使用cglib进行动态代理,因为可以在创建代理的同时将动态方法织入类中,否则可以直接用反射;构造函数注入循环依赖问题spring不能解决 循环依赖是在实例化后处理的);实例化bean后处理;如果需要解决循环依赖(满足3个条件:单例、允许循环依赖、当前bean正在创建),则提早曝光单例工厂(将单例工厂放入工厂缓存singletonFactories中,其他单例bean在创建时调用getObject方法可以获取未创建好的单例bean,getObject方法中实现Spring AOP 的advice动态织入;属性注入(填充)(递归初始化);激活aware方法(通过aware方法可以获取对应的资源:BeanNameAware 获取bean名称,BeanClassLoaderAware 获取bean的类加载器;BeanFactory 获取bean的工厂,即加载到IOC容器中);初始化单例bean前处理;激活用户自定义的init方法:如afterPropertiesSet方法、init-method,afterPropertiesSet先执行,init-method后执行;初始化单例bean后处理(spring AOP 在这里实现);检测循环依赖;注册destroy-method销毁方法);
**后置处理:创建单例bean之后,移除单例bean正在创建状态,用于检测循环依赖
**将单例bean放入单例缓存singletonObjects,从单例工厂缓存singletonFactories中移除单例工厂,从单例bean缓存earlySingletonObjects中移除单例bean,保存已注册的单例bean
**类型转换:将bean转换为需要的类型
3、ApplicationContext
**环境准备:如系统属性或环境变量的准备及验证
**加载BeanFactory,并读取配置文件:创建BeanFactory(DefaultListableBeanFactory);定制BeanFactory(在基本容器的基础上,增加了是否允许覆盖是否允许扩展的设置,并提供了对注解@Qualifier、@Autowired的支持);加载beanDefinition,读取配置文件(通过beanDefinitionReader加载beanDefinition(并注册到beanFactory的BeanDefinitionMap中));使用全局变量记录beanFactory
**对BeanFactory进行功能填充:如对@Qualifier和@Autowired注解的支持;增加AspectJ支持;增加属性注册编辑器(Spring DI 依赖注入时 Date类型是无法识别的)
**子类通过覆盖方法做额外处理
**激活(调用)BeanFactory处理器(容器级),可以有多个,通过排序依次处理;beanFactory处理器可以在实例化任何bean之前获得配置元数据并修改BeanDefinition(如${}替换);@ComponentScan就是在这里实现的;注册bean处理器(BeanFactory没有注册(因此不能使用),需要手动注册),在bean创建时调用
**注册拦截bean创建的bean处理器,这里只是注册,真正调用是在getBean方法中
**国际化处理
**初始化应用消息广播器
**子类覆盖方法处理
**在所有注册的bean中查找要监听的bean,注册到消息广播器中(注册监听器,并在合适的时候通知监听器)
**通过beanFactory加载bean(非延迟加载):ApplicationContext在启动时会加载所有的单例bean,调用getBean方法(上面Spring中BeanFactory加载bean的过程)
**完成刷新,通知生命周期管理器lifecycleProcessor刷新过程,并通过事件通知监听者


Spring-FactoryBean、BeanFactory、ObjectFactory

1、FactoryBean:
一般情况下,Spring通过反射机制来实例化bean,而这样可能需要很多配置,可以通过实现FactoryBean接口以编码的方式来代替
在IOC容器的基础上给Bean的实现加上了一个简单工厂模式和装饰模式,是一个可以生产对象和装饰对象的工厂bean
它是泛型的,只能固定生产某一类对象,而不像BeanFactory那样可以生产多种类型的Bean
当bean实现FactoryBean接口时,通过工厂的getBean方法返回的是FactoryBean中的getObject方法返回的实例,如果想要返回当前bean,需要以&开头
2、BeanFactory:对象工厂,用于实例化和保存对象
3、ObjectFactory:某个特定的工厂,用于在项目启动时,延迟实例化对象,解决循环依赖问题, 调用它的getObject方法时,才会触发 Bean 实例化


Spring单例下如何解决循环依赖(三级缓存)

Spring中循环依赖包括构造器依赖和setter注入依赖,Spring只能解决单例setter注入依赖(注入时会返回提前暴露的创建中的bean),构造器依赖无法解决(因为只有实例化之后才能曝光,实例化前曝光是有风险的),对于原型模式,循环依赖也是无法解决的(因为不使用缓存)
bean什么时候才会提早曝光:单例、创建中、允许循环依赖
1、尝试从singletionObjects中获取实例
2、尝试从earlySingletionObjects中获取实例
3、根据beanName尝试从singletonFactories中获取ObjectFactory,调用getObject方法创建bean(这里只是实例化了bean),放到earlySingletionObjects中,并从singletonFactories中移除ObjectFactory(互斥操作)这时已经可以通过容器的getBean方法获取到bean


Spring-AOP

1、静态代理、动态代理:静态代理直接调用目标类方法;动态代理通过反射调用目标类方法
2、JDK动态代理、CGLIB动态代理:JDK是在运行期间创建接口的实现类来完成对目标对象的代理;CGLIB采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类(生成代理类Class的二进制字节码;通过Class.forName加载二进制字节码,生成Class对象;通过反射机制获取代理类实例构造,并初始化代理类对象),并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑
3、连接点(方法执行处)、切入点(何处织入通知)、通知(处理时机及处理逻辑)、切面(包括切入点和通知)
4、源码:
**解析器解析AOP代理注解,生成BeanDefination,并注册到BeanDefinitionMap中
**创建自动代理创建器(用来实现AOP)(AnnotationAwareAspectJAutoProxyCreator)
**选择代理实现方式:默认如果目标对象有实现接口,则使用JDK动态代理;否则使用CGLIB代理(无法覆写final方法,可以通过proxy-target-class属性强制使用CGLIB代理);expose-proxy属性是为了解决有时候目标对象内部的自我调用无法实现切面增强的问题(强制暴露代理,在代码中可以获取这个代理进行显式调用)
**创建AOP动态代理:自动代理创建器实现了BeanPostProcessor接口,Spring在目标bean初始化完成之后会调用其postProcessAfterInitialization方法来创建AOP动态代理;获取增强,获取所有增强中目标bean可用的增强;创建代理工厂,根据配置设置JDK动态代理,或者CGLIB代理,将目标bean及其增强添加到代理工厂,通过代理工厂创建代理并返回(将增强组成拦截器链,执行目标方法时,执行拦截器链,中间会调用目标方法)


Spring事务失效的场景

1、注解@Transactional配置的方法非public权限修饰(可以开启 AspectJ 代理模式解决)
2、注解@Transactional所在类非Spring容器管理的bean
3、注解@Transactional所在类中,注解修饰的方法被类内部方法调用(无事务方法调用有事务方法,事务失效,使用代理对象调用解决,且要在启动类上加注解@EnableAspectJAutoProxy(exposeProxy = true)):Spring在扫描Bean的时候会自动为标注了@Transactional注解的类生成一个代理对象(proxy),当有注解的方法被调用的时候,实际上是代理对象调用的,代理对象在调用之前会开启事务,执行事务的操作,但是同类中的方法互相调用,相当于this.B(),此时的B方法并非是代理对象调用,而是直接通过原有的Bean直接调用,所以注解会失效)
4、业务代码抛出异常类型非RuntimeException,事务失效
5、业务代码中存在异常时,使用try…catch…语句块捕获,而catch语句块没有throw new RuntimeExecption异常(最难被排查到问题且容易忽略)
6、注解@Transactional中Propagation属性值设置错误即Propagation.NOT_SUPPORTED(一般不会设置此种传播机制)
7、mysql关系型数据库,且存储引擎是MyISAM而非InnoDB,则事务会不起作用(基本开发中不会遇到)


@Transactional原理

@Transactional是基于Spring AOP的,以@Transactional注解为连接点,@Transactional注解的切面逻辑类似于@Around


Spring Boot-启动原理

@SpringBootApplication注解:
1、@SpringBootConfiguration注解:继承Configuration,表示启动类是IOC容器的配置类
2、@EnableAutoConfiguration注解:通过@AutoConfigurationPackage注解获取自动配置包,返回当前主类的同级以及子级的中断自动配置组件;开启springboot的配置功能,借助@Import({EnableAutoConfigurationImportSelector.class})实现,将自动配置组件对应的bean定义都加载到IoC容器中,通过Spring的SpringFactoriesLoader(Spring工厂加载器)去读取META-INF/spring.factories中的配置类信息,通过反射生成一个配置类(里面有许多bean定义),最后将这些bean定义加载到IOC容器中(但是不是所有存在于spring.factories中的配置都进行加载,而是通过@ConditionalOnClass注解进行判断条件是否成立(只要导入相应的stater,条件就能成立),如果条件成立则加载配置类,否则不加载该配置类)
https://www.cnblogs.com/xiaopotian/p/11052917.html
3、@ComponentScan注解:自动扫描并加载符合条件的组件(如@Component和@Repository等)或者bean定义,最终将这些bean加载到IoC容器中,在beanFactoryProcessor中调用
run方法:
1、创建监听器
2、加载配置环境
3、创建ConfigurableApplicationContext
4、spring.factories加载,bean实例化

image.png


Spring Cloud

分布式事务

CAP:一致性、可用性、容错性
BASE:可用性、容错性、最终一致性(可能会存在中间状态如:处理中)
1、两阶段提交(2PC)(基于数据库,MySQL和Oracle支持):准备阶段(资源锁定,执行本地事务,并写日志undo(修改后数据)/redo日志(修改前数据))、提交/回滚阶段,中间由事务管理器控制全局事务,资源锁需要等到两个阶段结束才释放,性能较差,会出现死锁问题
2、seata改进2PC:事务管理器(事务发起服务引入,负责发起全局事务,发起全局提交或全局回滚的指令)、事务协调器(单独的服务,控制,维护全局事务状态,协调各分支事务提交/回滚)、资源管理器(控制每个分支事务,使用DataSourceProxy连接数据库,使用ConnectionProxy操作数据库,目的就是在第一阶段执行本地事务的同时,写入undo_log表(保存修改前和修改后的数据),因此第一阶段就能进行事务提交,并释放资源;第二阶段提交时只需要删除undo_log表数据,回滚时反向执行即可)
3、TCC:预处理Try(业务检查(一致性)及资源预留(隔离)执行)、确认 Confirm(确认提交)、撤销Cancel(回滚);如处理表(中间有状态、流水号)、被调用方保持幂等、并提供查询接口/回调
4、可靠消息最终一致性:本地消息表+消息中间件(通过本地事务保证数据业务操作和消息的一致性,然后通过定时任务将消息发送至消息中间件,待确认消息发送给消费方成功再将消息删除)
要解决以下问题:
**本地事务与消息发送的原子性问题
**事务参与方接收消息的可靠性(一定能够接收到消息)
**消息重复消费的问题(幂等性)
MQ的ack(即消息确认)机制,消费者监听MQ,如果消费者接收到消息并且业务处理完成后向MQ 发送ack(即消息确认),此时说明消费者正常消费消息完成,MQ将不再向消费者推送消息,否则消费者会不断重试向消费者来发送消息
5、最大努力通知


zookeeper

zookeeper是一个典型的分布式数据一致性解决方案
节点类型:持久节点、持久顺序节点、临时节点(客户端会话,非TCP连接,只能作为叶子节点)、临时顺序节点
集群:Leader、Follower、Observer(不参与Leader选举、也不参与写操作的“过半写成功”策略)
Leader选举:

分布式锁

Redis分布式锁:
1、加锁:set 命令要用 set key value px milliseconds nx,替代 setnx + expire 需要分两次执行命令的方式,保证了原子性;给锁加上一个过期时间,即使Redis客户端中间出现异常(来不及调用lua脚本释放锁)也可以保证过期时锁会自动释放(但是如果Redis服务端异常就没办法解决);超时问题解决:lua脚本+额外线程进行锁延时
2、解锁:将lua脚本传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey(锁标志),ARGV[1]赋值为requestId(Redis客户端标志);在执行的时候,首先会获取锁对应的value值,检查是否与requestId相等,如果相等则解锁(删除key);比较requestId是为了解决超时问题:如果在加锁和释放锁之间的逻辑执行的太长,以至于超出了锁的超时限制,就会出现问题,因为这时候锁过期了,第二个线程重新持有了这把锁,但是紧接着第一个线程执行完了业务逻辑,就把锁给释放了,第三个线程就会在第二个线程逻辑执行完之前拿到了锁
3、可重入性:对客户端的 set 方法进行包装,使用线程的 Threadlocal 变量存储当前线程持有锁的计数,还需要考虑内存锁计数的过期时间
问题:如果存储锁对应key的那个节点挂了的话,就可能存在丢失锁的风险,导致出现多个客户端持有锁的情况,这样就不能实现资源的独占了(即Redis服务端出现问题),即使是Redis主从也不能解决问题(Redis的主从同步通常是异步的)
解决:
1、Redlock算法:轮流尝试在每个节点上创建锁,过期时间较短,一般就几十毫秒,至少要在大多数节点上成功创建锁,才说明获取到锁,客户端计算创建锁的时间,如果创建锁的时间小于超时时间,就是创建成功了;如果创建锁失败了,那么就依次删除以前创建过的锁;如果其他客户端已经创建锁,就得不断轮询去尝试获取锁
2、Redisson:RedissonLock 同样没有解决节点挂掉的时候,存在丢失锁的风险的问题;Redisson 提供了实现了redlock算法的 RedissonRedLock,RedissonRedLock 真正解决了单点失败的问题,代价是需要额外的为 RedissonRedLock 搭建Redis环境
zookeeper分布式锁:
获取锁:客户端获取锁时,调用create方法创建临时顺序节点(Zookeeper会保证所有的客户端中,最终只有一个客户端能够创建成功,没有获取到锁的客户端需要创建一个节点去Watch监听锁节点)
释放锁:当前获取锁的客户端宕机/业务逻辑执行完都会移除临时顺序锁节点,并通知所有Watch节点去重新尝试获取锁

Redis-基础数据结构

1、String:类似于ArrayList,字节数组,用途:缓存用户信息(序列化和反序列化)
2、List:类似于LinkedList,链表+压缩列表(数据量少时,只用压缩列表),增删快,查询慢,用途:异步队列
3、Hash:类似于HashMap,数组+链表,渐进式reHash(中间新旧数据都会读),用途:缓存用户信息
4、Set:类似于HashSet,value值为空,用途:去重
5、ZSet: 类似于SortedSet 和 HashMap 的结合体,跳跃列表,既要随机增删,又要排序,用途:核心企业/供应商列表
6、跳跃列表:每一层是一个单向链表,每一层有一个额外的节点去定位每一层的头节点


image.png

Redis-缓存一致性

1、先删除缓存,再更新数据库(缓存设置过期时间)
问题:如果两个并发操作,一个读操作,一个写操作,写操作删除缓存,读操作从缓存读取数据失败,从数据库读取数据成功,然后更新缓存,写操作更新数据库,无法避免这种情况的缓存一致性
解决:延迟双删:写操作更新数据库成功后,sleep(睡眠时间不好控制)一段时间,再删除一次缓存
2、先更新数据库,再删除缓存(缓存设置过期时间)(推荐)
原理:更新数据库时,会加锁,其他操作不能操作这条数据
问题:写操作时,更新数据库成功,删除缓存失败,读操作仍读取缓存中的旧数据
解决:
**消息队列:更新数据库,插入本地消息表,删除缓存,消息队列重试去删除缓存(需要考虑消息队列的一些常见问题)
**消息队列+binlog日志:更新数据库时,会插入binlog日志,通过canal读取binlog日志,推送给消息队列,消息队列重试去删除缓存(与业务代码解耦)
3、为什么是删除缓存,而不是更新缓存
**问题1:如果两个并发操作,一个写操作a,一个写操作b,a更新数据库,释放锁,b更新数据库,释放锁,b更新缓存完成,a更新缓存完成,无法避免这种情况的缓存一致性
**问题2:每次都更新缓存,会导致性能消耗

Redis-缓存穿透、缓存雪崩、缓存击穿

缓存穿透
定义:查找一定不存在的数据(如恶意攻击),在缓存中根据key查不到,在数据库中也查询不到,所以不会存到缓存中,每次都要查询数据库
解决:
1、布隆过滤:将一切可能查询的key存到map中,请求过来时先去map中查找,不存在直接丢弃
2、即使在数据库中查询不到,空值也存到缓存中,设置过期时间(浪费空间,且在有效期内可能数据不一致)
缓存雪崩
定义:缓存中大量的数据同时失效(如服务挂掉和同时过期),直接去数据库中查询
解决:
1、过期时间设置均匀(避免同时过期)(事前)
2、缓存服务高可用(主从+哨兵)(事前)
3、限流:缓存失效时,通过加锁或者队列来控制读数据库写缓存的线程数量(如对某个key只允许一个线程查询数据和写缓存,其他线程等待),避免数据库崩掉(事中)
4、Redis 持久化:一旦重启,自动从磁盘上加载数据,快速恢复缓存数据(事后)
缓存击穿
定义:大量请求同时访问一个或多个热点key,key如果瞬间失效,会直接请求数据库,导致数据库崩掉
解决:
1、若缓存的数据是基本不会发生更新的,则可尝试将该热点数据设置为永不过期
2、若缓存的数据更新不频繁,且缓存刷新的整个流程耗时较少的情况下,则可以采用基于 Redis、zookeeper 等分布式中间件的分布式互斥锁,或者本地互斥锁以保证仅少量的请求能请求数据库并重新构建缓存,其余线程则在锁释放后能访问到新缓存
3、若缓存的数据更新频繁或者缓存刷新的流程耗时较长的情况下,可以利用定时任务在缓存过期前主动的重新构建缓存或者延后缓存的过期时间,以保证所有的请求能一直访问到对应的缓存

Redis-持久化

1、RDB全量快照(数据)直接复制,快
需要解决的问题:不能影响服务响应,如何保证边持久化边响应
解决:使用Copy On Write机制来实现快照持久化
原理:Redis 在持久化时会产生一个子进程,子进程刚刚产生时,和父进程共享内存里面的代码段和数据段,这是 Linux 操作系统的机制,在进程分离的一瞬间,内存的增长几乎没有明显变化;子进程只做数据持久化,不会修改现有的内存数据结构,只是对数据结构进行遍历读取,然后序列化写到磁盘中;当父进程对其中一个页面的数据进行修改时,会将被共享的页面复制一份分离出来,然后对这个复制的页面进行修改,这时子进程相应的页面是没有变化的,还是进程产生时那一瞬间的数据
2、AOF增量日志(指令)需要指令重放,慢,一般是先存到磁盘,然后再执行指令
需要解决的问题:文件会越来越大,需要定期进行AOF重写;服务宕机,数据丢失的问题
原理:创建子进程对内存遍历转换成指令,序列化到一个新的AOF日志文件中;序列化完毕后再将操作期间发生的增量AOF日志追加到这个新的AOF日志文件中,追加完毕后就立即替代旧的AOF日志文件,瘦身工作就完成了;AOF日志在内存缓存中,需要异步将数据刷回到磁盘,如果机器突然宕机,AOF日志内容可能还没有来得及完全刷到磁盘中,这个时候就会出现日志丢失,因此需要每隔 1s(可配置)左右执行一次 fsync 操作(强制刷新到缓存)
3、通常 Redis 的主节点不会进行持久化操作,持久化操作主要在从节点进行,从节点是备份节点,没有来自客户端请求的压力
4、混合持久化:重启 Redis 时,很少使用 RDB 来恢复内存状态,因为会丢失大量数据(重启期间的数据没有保存),通常使用 AOF 日志重放(重启期间的数据会追加),但是重放 AOF 日志性能相对 RDB 来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间;可以将RDB的内容和增量的AOF存放在一起,这里的AOF不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量 AOF日志,通常这部分AOF日志很小,于是在Redis重启的时候,开启AOF,先加载RDB的内容,然后再重放增量AOF日志就可以完全替代之前的AOF全量文件重放,重启效率因此大幅得到提升

Redis-过期策略

Redis是单线的,删除也会占用线程的时间,Redis采用的是定期删除 + 懒惰删除策略(定期删除是集中处理,惰性删除是零散处理)
定期删除策略
Redis单线程默认会每秒进行十次过期扫描,过期扫描不会遍历过期字典中所有的 key,而是采用了一种简单的贪心策略
1、从过期字典中随机 20 个 key
2、删除这 20 个 key 中已经过期的 key
3、如果过期的 key 比率超过 1/4,那就重复步骤 1
4、同时,为了保证过期扫描不会出现循环过度,导致线程卡死现象,算法还增加了扫描时间的上限,默认不会超过 25ms
为什么不扫描所有的过期key?
1、会导致线上读写请求出现明显的卡顿现象(所以要尽量避免大量key同时过期)
2、即使扫描有 25ms 的时间上限:假如有 101 个客户端同时将请求发过来了,然后前 100 个请求的执行时间都是25ms,那么第 101 个指令需要等待多久才能执行?2500ms(因为单线程),这个就是客户端的卡顿时间
从库的过期策略
1、从库不会进行过期扫描,从库对过期的处理是被动的,主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的key
2、因为指令同步是异步进行的,所以主库过期的 key 的 del 指令没有及时同步到从库的话,会出现主从数据的不一致,主库没有的数据在从库里还存在
懒惰删除策略
为什么要懒惰删除?
1、删除指令 del 会直接释放对象的内存,大部分情况下,这个指令非常快,没有明显延迟,不过如果删除的 key 是一个非常大的对象,比如一个包含了千万元素的 hash,那么删除操作就会导致单线程卡顿
2、Redis 内部实际上并不是只有一个主线程,它还有几个异步线程专门用来处理一些耗时的操作,可以用异步线程实现懒惰删除

Redis-内存淘汰机制

1、noeviction:当内存超出最大内存,写入请求会报错,但是删除和读请求可以继续(一般不使用,但是是默认的)
2、allkeys-lru:当内存超出最大内存,在所有的key中,移除最少使用的key,只把Redis当作缓存时使用(推荐)
3、allkeys-random:当内存超出最大内存,在所有的key中,随机移除某个key(一般不使用)
4、volatile-lru:当内存超出最大内存,在设置了过期时间key的字典中,移除最少使用的key(不会移除没有设置过期时间的),把Redis既当缓存,又做持久化的时候使用
5、volatile-random:当内存超出最大内存,在设置了过期时间key的字典中,随机移除某个key(不会移除没有设置过期时间的)
6、volatile-ttl:当内存超出最大内存,在设置了过期时间key的字典中,优先移除剩余时间ttl 最少的(不会移除没有设置过期时间的)
LRU算法:
1、实现 LRU 算法除了需要 key/value 字典外,还需要附加一个链表,链表中的元素按照一定的顺序进行排列
2、当空间满的时候,会踢掉链表尾部的元素
3、当字典的某个元素被访问时,它在链表中的位置会被移动到表头,所以链表的元素排列顺序就是元素最近被访问的时间顺序
4、位于链表尾部的元素就是不被重用的元素,所以会被踢掉;位于表头的元素就是最近刚刚被人用过的元素,所以暂时不会被踢(双向链表)
近似 LRU 算法
1、Redis 使用的是一种近似 LRU 算法,之所以不使用 LRU 算法,是因为需要消耗大量的额外的内存,需要对现有的数据结构进行较大的改造
2、近似LRU 算法则很简单,在现有数据结构的基础上使用随机采样法+额外字段(最后一次被访问的时间戳)来淘汰元素,能达到和 LRU 算法非常近似的效果
LFU算法
1、Redis 4.0 里引入了一个新的淘汰策略 —— LFU(最近最少使用)算法
2、LFU 表示按最近的访问频率进行淘汰,它比 LRU 更加精准地表示了一个 key 被访问的热度
3、如果一个 key 长时间不被访问,只是刚刚偶然被用户访问了一下,那么在使用 LRU 算法下它是不容易被淘汰的,因为 LRU 算法认为当前这个 key 是很热的
4、而 LFU 是需要追踪最近一段时间的访问频率,如果某个 key 只是偶然被访问一次是不足以变得很热的,它需要在近期一段时间内被访问很多次才有机会被认为很热

Redis-哨兵

1、负责持续监控主从节点的健康,当主节点挂掉时,自动选择一个最优的从节点切换为主节点
2、客户端来连接主从时,会首先连接哨兵,通过哨兵来查询主节点的地址,然后再去连接主节点进行数据交互
3、当主节点发生故障时,客户端会重新向哨兵获取主节点地址,哨兵会将最新的主节点地址告诉客户端,无需重启即可自动完成节点切换
4、主节点挂掉了,原先的主从复制也断开了,客户端和损坏的主节点也断开了,从节点被提升为新的主节点,其它从节点开始和新的主节点建立复制关系,客户端通过新的主节点继续进行交互
5、哨兵会持续监控已经挂掉了主节点,待它恢复后,集群会进行调整,原先挂掉的主节点现在变成了从节点,从现在的主节点那里建立复制关系
6、哨兵进行主从切换时,客户端如何知道地址变更了 ? 在建立连接的时候进行了主节点地址变更判断,查询主节点地址,然后跟内存中的主节点地址进行比对,如果变更了,就断开所有连接,重新使用新地址建立新连接;如果是旧的主节点挂掉了,那么所有正在使用的连接都会被关闭,然后在重连时就会用上新地址

Redis-应用

1、Scan:扫描海量数据(有游标)
2、HyperLogLog:统计UV
3、布隆过滤器:推荐去重(布隆过滤器能准确过滤掉那些已经看过的内容,那些没有看过的新内容,它也会过滤掉极小一部分 (误判)),当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在
原理:对key多次无偏hash,每个hash取模数组长度,确定位置,将该位置置为1;查询是否存在时,再次对key多次无偏hash,取模,确定位置是否为1;位置可以重复利用,因此会有误差

HashMap

MySQL-索引

B+树索引的优点:
1、索引按照顺序存储数据,可以用来做ORDER BY和GROUP BY操作
2、索引中存储了实际的索引列值,所以某些査询只使用索引就能够完成全部査询(非一级索引的叶子节点存储主键)
3、索引大大减少了服务器需要扫描的数据量
4、索引可以帮助服务器避免排序和临时表
5、索引可以将随机I/O变为顺序I/O
B+树索引的缺点:
1、如果不是按照索引的最左列开始査找,则无法使用索引
2、不能跳过索引中的列(只能使用跳过前的列)
3、如果查询中有某个列的范围査询,则其右边所有列都无法使用索引优化査找(但是可以作为值返回);如果范围査询列值的数量有限,那么可以使用多个等于条件来代替范围条件
索引策略
1、独立的列:索引列不能是表达式的一部分,也不能是函数的参数,因此要始终将索引列单独放在比较符号的一侧
2、前缀索引和索引选择性:前缀越长,选择性越好
3、聚合(多列)索引:
当出现服务器对多个索引做相交操作时(通常有多个AND条件)或对多个索引做联合操作时(通常有多个OR条件),通常意味着需要一个包含所有相关列的多列索引,而不是多个独立的单列索引(需要合并)
多列索引中索引的顺序(需要兼顾排序和分组):当不需要考虑排序和分组时,将选择性最高的列放在前面通常是很好的
4、聚簇索引:当表有聚簇索引时,它的数据行实际上存放在索引的叶子页中,因为无法同时把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引(覆盖索引可以模拟多个聚簇索引的情况)
5、二级索引:访问需要两次索引査找,而不是一次,因为二级索引叶子节点保存的不是指向行的物理位置的指针,而是行的主键值,这意味着通过二级索引查找行,存储引擎需要找到二级索引的叶子节点获得对应的主键值,然后根据这个值去聚簇索引中査找到对应的行,这里做了重复的工作:两次B+树査找而不是一次(回表查询)
6、普通索引和唯一索引:普通索引在查找到一条记录后会继续查找,而唯一索引会终止查找
使用索引排序
1、只有当索引的列顺序和ORDER BY子句的顺序完全一致,并且所有列的排序方向(倒序或正序)都一样时,MySQL才能够使用索引来对结果做排序
2、如果査询需要关联多张表,则只有当ORDER BY子句引用的字段全部为第一个表时,才能使用索引做排序
3、ORDER BY子句和查找型查询的限制是一样的:需要满足索引的最左前缀的要求;否则MySQL都需要执行排序操作,而无法利用索引排序
有一种情况下ORDER BY子句可以不满足索引的最左前缀的要求,就是前导列为常量的时候;即如果WHERE子句或者JOIN子句中对这些列指定了常量,就可以“弥补”索引的不足
B树与B+树的区别
1、B树可能在非叶子节点命中返回;B+不可能在非叶子结点命中
2、B+树叶子节点存放所有数据;B+树叶子节点之间又是一个链表

MySQL-事务

事务的特性
1、原子性:一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚
2、一致性:数据库总是从一个一致性的状态转换到另外一个一致性的状态
3、隔离性:通常来说(涉及到隔离级别),一个事务所做的修改在最终提交以前,对其他事务是不可见的
4、持久性:通常来说(涉及到持久级别),一旦事务提交,则其所做的修改就会永久保存到数据库中,此时即使系统崩溃,修改的数据也不会丢失
事务的隔离级别
1、读未提交:一个事务可以读取到其他事务未提交的数据(脏读)
2、读已提交(不可重复读):一个事务只能读取到其他事务此刻已提交的数据
3、可重复读:一个事务只能读取到其他事务在该事务开启时已提交的数据(快照读),但是无法避免幻读(单指插入,两次读取的结果不一致)
可重复读是MySQL的默认事务隔离级别,InnoDB通过MVCC(多版本并发控制)和next-key lock解决了幻读
4、串行读:强制事务串行执行,解决了幻读问题,在读取的每一行数据上都加锁,所以可能导致大量的超时和锁争用的问题
幻读:
1、幻读指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行(单指插入);在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插入的数据的(MVCC),幻读在“当前读”下仍会出现(加锁读时,只锁当前满足条件的行)
2、通过加间隙锁解决当前读导致的幻读,跟间隙锁存在冲突关系的,是跟 “往这个间隙中插入一个记录 ”这个操作,间隙锁之间都不存在冲突关系;间隙锁和行锁合称next-key lock,每个next-key lock是前开后闭区间
3、间隙锁是在可重复读隔离级别下才会生效的
4、一个并发问题:任意锁住一行,如果这一行不存在的话就插入,如果存在这一行就更新它的数据;因为锁的是间隙锁,并发时会锁竞争,发生死锁
MVCC-多版本并发控制(可重复读下)
查询(同时满足下面条件的,才作为结果返回):
a、査找版本早于当前事务版本的数据行(也就是行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的
b、行的删除版本要么未定义,要么大于当前事务版本号,这可以确保事务读取到的行,在事务开始之前未被删除
插入:为新插入的每一行保存当前事务版本号作为行版本号
删除:为删除的每一行保存当前事务版本号作为行删除标识
修改:插入一行新记录,保存当前事务版本号作为行版本号,同时保存当前事务版本号到原来的行作为行删除标识
优点:保存这两个额外系统版本号,使大多数读操作都可以不用加锁,这样设计使得读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行
缺点:每行记录都需要额外的存储空间,需要做更多的行检査工作,以及一些额外的维护工作
MySQL没有完全解决幻读问题
如:事务a先查询(MVCC),事务b插入(next-key),事务a更新(next-key,会加版本号),事务a查询(MVCC),两次查询的结果不同
意向锁(表级锁):意向锁是由数据库自己维护的,一般来说,给一行数据加上共享锁之前,数据库会自动在这张表上面加一个意向共享锁(IS锁);给一行数据加上排他锁之前,数据库会自动在这张表上面加一个意向排他锁(IX锁)
意向锁可以认为是共享锁和互斥锁在数据表上的标识,通过意向锁可以快速判断表中是否有记录被上锁,从而避免通过遍历的方式来查看表中有没有记录被上锁,提升加锁效率
如要加表级别的互斥锁,这时候数据表里面如果存在行级别的互斥锁或者共享锁的,加锁就会失败,此时直接根据意向锁就能知道这张表是否有行级别的X锁或者S锁

MySQL-查询性能优化

从下面几点进行优化:
1、减少扫描行数(索引)
2、减少返回的行数或列数(limit或避免*)
如:
1、证件号码、证件名称、证件类型(聚合索引,冗余索引,顺序问题,减少回表)
2、select a.id,a.name from user a inner join (select b.id from user order by a.userId limit 1000,10) b on a.id = b.id
(原始的写法:select a.id,a.name from user a order by a.userId limit 1000,10)
(使用一级索引,减少回表)
join(小表驱动大表,大表用索引)
1、在可以使用被驱动表的索引(join字段)情况下,使用join语句,性能比强行拆成多个单表执行SQL语句的性能要好;如果使用join语句的话,需要让小表(根据条件查询出来少的表)做驱动表
2、在判断要不要使用join语句时,就是看explain结果里面,Extra字段里面有没有出现“Block Nested Loop”字样(出现则不用join)
3、如果用left join的话,左边的表一定是驱动表吗?不是
4、如果两个表的join包含多个条件的等值匹配,是都要写到on里面呢,还是只把一个条件写到on里面,其他条件写到where部分?写到on里面
5、在MySQL里,NULL跟任何值执行等值判断和不等值判断的结果,都是NULL;select NULL =NULL 的结果,也是返回 NULL

MySQL-执行流程

MySQL执行一个査询的过程:
1、客户端发送一条査询给服务器
2、服务器先检査査询缓存,如果命中了缓存,则立刻返回存储在缓存中的结果,否则进入下一阶段
3、服务器端进行SQL解析、预处理,再由优化器生成对应的执行计划
4、MySQL根据优化器生成的执行计划,调用存储引擎的API来执行査询
5、将结果返回给客户端
更新:先找到要更新的数据,从磁盘读入内存;在执行器中执行语句,调用引擎先把记录写到redo log(覆盖写,磁盘中,物理日志)里面(原先的记录会写到undo log中,用于回滚),并更新内存,此时还未提交事务,在适当的时候,再将记录更新到磁盘;执行器写到binlog(不覆盖写,磁盘中,逻辑日志(语句));引擎将redo log改成提交状态,更新完成,即两阶段提交

ThreadLocal

1、一个线程对应多个ThreadLocal,但只有一个ThreadLocalMap(在当前线程内)
2、多个线程可以使用同一个ThreadLocal,但是是隔离的
3、ThreadLocalMap是ThreadLocal的内部类
4、ThreadLocalMap的key为ThreadLocal(弱引用),value为存储的值
5、内存泄漏:当ThreadLocal被回收时,ThreadLocalMap中就可能出现key为null的Entry,没有任何办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收(没有调用remove),造成内存泄漏
6、为什么key使用弱引用:如果key使用强引用,如果当前线程再迟迟不结束的话(如线程池中复用线程),可能会出现整个Entry对象都不会被回收,也会出现内存泄漏问题(更难解决);如果key使用弱引用,即使没有手动删除,key也会被回收,但是会出现value不会被回收
7、内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用
8、内存泄漏解决:使用static修饰ThreadLocal引用(这样保证ThreadLocal始终保持被引用,不会被回收),但是最后还是要调用remove方法(ThreadLocal不会被回收,value会回收)


你可能感兴趣的:(问题集锦)