一、项目
1.知道你的项目到底是做什么的,有哪些功能。
2.知道你做的模块在整个项目中所处的位置及作用,并能清晰的阐述模块间的调用关系。
3.知道你项目的整体架构和使用到的中间件,并对中间件的原理有一定的了解。
4.能流畅阐述的自己在项目中解决过的比较复杂的问题(重点)。
二、Java基础知识
1.栈和队列的区别
栈:先进后出,后进先出;进栈和出栈在同一端;
队列:先进先出,后进后出,进队列和出队列在不同端;
2.接口和抽象类的区别
—>接口不可以被实例化,但是不代表不可以new 接口(); 以匿名内部类的方式,就可以new 接口();
3.int和Integer的区别,以及自动拆箱/装箱的相关问题
4.常量池相关问题
常量池有字节码常量池、运行时常量池,字符串常量池;重点在于字符串常量池:
5.==和equals的区别
6.重载和重写的区别
7.String、StringBuilder、StringBuffer的区别
三、集合框架
1.ArrayList、LinkedList、HashMap、LinkedHashMap、ConcurrentHashMap的底层实现原理。
以JDK8来说明:
ArrayList
底层是一个动态数组;当我们初始化一个ArrayList,即new ArrayList<>();时,会在创建一个Object类型的数组,这时这个数组是空的(长度为0)。 然后在对这个ArrayList的实例,第一次调用add()方法添加数据的时候,底层会创建长度为10的数组。并将数据放到数组下标0的位置。 后面继续添加数据,如果此次添加,超出了数组的设定好的长度,就是执行其扩容机制: 扩容时,默认扩容为原本长度的1.5倍,然后将原有的数据复制到新的数组中。
HashMap
以jdk8为例说明;HashMap底层为数组+链表或红黑树;
① 首先,初始化一个HashMap,即new HashMap时,创建了一个空数组;
② 然后,第一次调用其put()方法时,创建了一个长度为16的Node类型数组Node[]; (Node就是hashMap中的每个具体的节点,包含键、值以及下一个元素的引用);
put()方法:
我们调用put()方法时,会调用HashMap里面的putVal()方法,这个方法在接收参数时,会对key用hash()方法做处理后再作为参数,来看一下hash()方法:
hash()方法会用自己的算法计而出一个key的值,如果key为null,则值为0,否则键的值为key的哈希值^(key的哈希值 >>> 16);
再看看putVal()方法:
在putVal()方法中,做了如下事情:
如果是首次put元素(这时hashmap实例还是个空数组),那么会创建一个长度为16的数组;
首次之外,再进行put,则会根据key的hash值,定位到数组位置,数组的位置上没有元素,就直接插入;
如果定位到的数组位置已有元素,就和要插入的key进行比较,如果key相同(使用equals来比较),就进行覆盖;如果不同,则在原元素下面使用链表结构来存储这个元素,这时就是数组+链表的形式;
然后继续put的话,继续进行比较,当数组的某一个索引上的元素以链表形式存在的个数大于8,并且数组的长度超过了64的时候,这个是这个索引上的数据就改为使用红黑树存储了;(因为红黑树是平衡二叉树,在查找性能方面比链表要高);
在不断添加的过程中,会涉及到扩容问题,默认的扩容方式是扩容为原本容量的两倍,扩容完以后,将原有的数据赋值过来;扩容是根据DEFAULT_LOAD)FACTOR(默认加载因子,值为0.75),这个是用来计算扣荣临界值的,比如一开始数组长度为16,默认记载因子为0.75,那么当数组长度大于16*0.75=12时,就开始扩容;
2.JDK1.7版本和1.8版本的HashMap的区别。
3.JDK1.7版本和1.8版本的ConcurrentHashMap的区别。
4.HashMap能不能排序?HashMap的长度为什么要是2的幂次方?
四、多线程
1.创建线程的几种方式?wait、sleep分别是谁的方法?区别?线程间的通信方式?
/
创建和初始化线程的方式:* 1、继承Thread类;
// 方式一:继承Thread类
Thread thread = new Thread01();
thread.start(); // 启动线程
2、实现Runnable接口;
Runnable01 runnable01 = new Runnable01();
new Thread(runnable01).start();
3、实现Callable接口+FutureTask,可以拿到返回结果,可以处理异常;
FutureTask
// 阻塞等待整个线程执行完成,获取返回结果
Integer integer = futureTask.get();
4、通过线程池的方式;
给线程池直接提交任务
public static ExecutorService service = Executors.newFixedThreadPool(10);
service.execute(new Runnable01()); // execute()没有返回值;
Future
wait()和sleep():
相同点:
这两个方法都会使当前线程进入阻塞状态;
不同点:
wait()是Object类中定义的方法,而sleep()是Thread类的方法;
wait只能在synchronized方法或者synchronized块中使用,而sleep可以在任何地方使用;
Object类的wait()方法会让出CPU,释放已经占有同步锁的资源,而Thread类的sleep()方法,不会导致锁的行为改变;他们最大的区别就在这里,sleep()不释放同步锁,wait()释放同步锁;
-> sleep()方法导致了程序暂停执行指定的时间,让出cpu,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态,在调用sleep()方法的过程中,线程不会释放对象锁; 而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备。
线程间的通信方式:
https://blog.csdn.net/qq_42411214/article/details/107767326?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-1.pc_relevant_antiscanv2&spm=1001.2101.3001.4242.2&utm_relevant_index=4
线程的通信可以被定义为:当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以避免无效的资源争夺。
2.介绍下什么是死锁,遇见过死锁吗?你是怎么排查的。(可以通过jps排查)
什么是死锁 :
死锁就是当两个或两个以上的线程因竞争相同资源而处于无限期的等待,这样就导致了多个线程的阻塞,出现程序无法正常运行和终止的情况。
https://baijiahao.baidu.com/s?id=1721832902655524388&wfr=spider&for=pc
3.创建线程池的几种方式,线程池有什么好处。
创建线程池的方式:
方式一,通过线程池ThreadLoaclPool的构造方法,new出来来创建;
方式二:通过Executor框架的工具类Executors来创建
线程池的好处:
4.线程继承和接口的区别,接口有什么好处。
继承Thread类的话,资源类只能继承Thread这一个类,有单继承的局限;
而且,实现Runnable接口, Runnable内部的代码可以被多个线程共享,适用于多个线程处理同一资源的情况。
5.synchronized、Lock、ReentrantLock的区别,用法及原理。
synchronized和Lcok的区别:
synchronized和ReentrantLock的区别:
synchronized的用法:
synchronized关键字主要有三种用法:
1.修饰实例方法:
此时作用域当前对象实例加锁,进入同步代码前,需要获得当前对象实例的锁;
比如一个Book类下面的hello()方法被synchronized关键字修饰,那么synchronized的锁就是Book类的这个实例对象;
2.修饰静态方法:
此时,是给当前类加锁。会作用域当前类的所有实例,进入同步代码前,要获得当前class的锁。因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管 new 了多少个对象,只有一份)。所以,如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
3.修饰代码块:
指定加锁对象,对给定对象/类加锁。synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁。synchronized(类.class) 表示进入同步代码前要获得 当前 class 的锁
ReentrantLock的使用方法:
ReentrantLock使用起来,由于它是一个类,所以要先获取它的实例对象,在执行逻辑代码前,要先调用lock()方法加锁,要在finally代码块中使用unlock()方法释放锁;
还可以搭配Condition接口的实现类,通过对象的await()/sgnal()/signalAll()方法,来实现线程间的通信;
synchronized的原理:
ReentrantLock的原理:
6.CountDownLatch与CyclicBarrier用法
7.ThreadLocal的用法和原理
8.volatile关键字的作用和原理
9.乐观锁和悲观锁
10.对公平锁、非公平锁、可重入锁、自旋锁、读写锁的理解
11.CAS是什么及底层原理。
12.ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue等等堵塞队列的理解
13.ThreadPoolExecutor的传入参数及内部工作原理
14.给你一个具体的业务场景,让你使用ThreadPoolExecutor创建一个适合的线程池。
15.分布式环境下,怎么保证线程安全。
五、JVM相关问题
1.JVM内存机制
JVM的内存区域,主要划分为5个部分,分别是,方法区、堆、本地方法栈、虚拟机栈、程序计数器;
其中,方法区和堆内存,是各个线程共享的,而程序计数器、栈和本地方法栈是各个内存私有的;
方法区:方法区存储了每一个类的结构信息(也就是类的模板Class),例如运行时常量池、字段(即成员变量)和方法(成员方法)数据、构造函数和普通方法的字节码码内容。这是一个规范,在不同的虚拟机中是不一样,在jdk7中,是永久代,而在jdk8中是元空间;
堆:堆内存是各线程共享的,也就是一个jvm实例中,只有一个堆内存,堆内存的大小是可以调节的;它主要用来存储创建出的对象的实例;这也是垃圾回收的主要区域;
-> 堆内存在逻辑上分为新生代、老年代和永久代,不过永久代一般都是通过方法区去体现了,所以在
jvm的堆内存中,物理上堆内存分为了新生代和老年代,其中新生代又分为伊甸区、幸存者0区和幸存 者1区;新生代占了堆内存的1/3的空间,老年代占了堆内存的2/3的空间;新生代中,伊甸区占了新生 代8/10的空间,幸存者0区和1区分别占用1/10;
-> 可以通过-Xms 和-Xmx参数分别调节堆内存的初始大小和最大内存大小;其中-Xms默认值为物理内 存的1/64,而最大内存大小默认值为物理内存的1/4;
虚拟机栈(栈内存):它是线程私有的,主要保存局部变量,基本数据类型变量以及堆内存中某个对象的引用;每个方法在执行的过程中,都会创建一个栈帧(Stack Frame),栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。栈中的栈帧随着方法的进入和推出有条不紊的执行着出栈和入栈的操作;
程序计数器: 程序计数器是当前线程执行的字节码的位置指示器;字节码解释器工作时,通过改变这个计数器的值,来选取吓下一条需要执行的字节码指令,是内存区域中唯一一个不会发生OOM错误的区域;
本地方法栈:主要是为jvm提供使用native方法的服务;调用底层的C/C++方法;
2.介绍下垃圾收集机制,垃圾收集有哪些算法,各自的特点。
垃圾收集机制:
在java中,不用开发人员自己来管理内存,而是jvm对内存进行管理;
java中的垃圾收集机制,是使用分代收集算法,在次数上,频繁收集新生代,较少收集老年带,基本上不会去收集元空间。jvm在进行GC时,并不是你每次都对新生代、老年代、元空间一起回收,大部分都是回收新生代,因此GC按照回收的区域又可划分为普通minorGC和全局GC (FullGC 或者Major GC );
垃圾收集有哪些算法,各自的特点:
标记-清除算法:
该算法分为“标记”和“清除”阶段:首先标记处所有不需要回收的对象,在标记完成后,同意回收掉所有没有被标记的对象。
特点是它是最基础的收集算法,效率也很高后续的算法都是和其不足进行改进得到;
但同时也有问题就是:会有空间问题,标记清楚后,会产生大量不连续的碎片;
标记-复制算法:
为了解决效率问题,标记-复制算法出现了;标记-复制算法将内存分为大小相同的两块,每次使用其中的一块;当这一块的内存使用完后,就将还存活的对象复制到另一块区,然后再把使用的空间一次清理掉,(特点)这样就使每次的内存回收都是对内存区域的hi版进行回收;
标记-整理算法:
这是根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
分代收集算法:
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
3.聊聊GC,谈谈MajorGC、FullGc的区别,垃圾收集器有哪些,他们的区别?
一般我们说的GC,是指轻GC,也即Young GC或者说minor GC,它是对新生代进行的垃圾收集;
而major GC,也可以成为Old GC,是对老年代进行的垃圾收集;
Full FC,是指收集整个java堆和方法区;
普通GC(minor GC):只针对新生代区域的GC,是发生在新生代的垃圾收集,因为大多数java对象存活率都不高,所以minor GC非常频繁,一般回收的速度也比较快;
全局GC(major GC 或者说 Full GC):指发生在老年代/整个堆和方法区的垃圾收集动作,出现了full GC,一般会伴随至少一次的minor GC(但这并不是绝对的);full GC的速度一般比minor GC慢十倍以上;
4.OutOfMemoryError这个错误你遇到过吗?你是怎么解决处理的?
当 JVM 内存严重不足时,就会抛出 java.lang.OutOfMemoryError 错误。
https://blog.csdn.net/wan3964366/article/details/119425135
5.JVM调优有哪些参数,介绍下,线上环境上,你是怎么查看JVM的参数并进行调优的?
6.能不能自己写一个类叫java.lang.String(类加载的过程,双亲委派模型)
写是可以写的,但是这个类写出来,是没有用的;
双亲委派模型:
JVM中内置了三种类加载器,这类加载器之间是有层级关系的;
首先,是启动类加载器Bootstrap ClassLoader,它是最顶层的类加载器,由C++实现,负载加载%JAVA_HOME%/lib目录下的jar包和类,或者被-Xbootclasspath参数指定的路径中的所有类;
然后,是拓展类加载器Extension ClassLoader,它是第二层的类加载器,由java编写,主要负责加载拓展类,如%JRE_HOME%/lib/ext 目录下的 jar 包和类,或被 java.ext.dirs 系统变量所指定的路径下的 jar 包。
最后,是应用程序类加载器App ClassLoader,这是面向我们用户的类加载器,负责加载当前应用classpath下的所有jar包和类;
然后还有用户可以自己定义类加载器,但是自定义的加载器也是位于层级关系的最下层;
->基于此层级关系,类加载又有双亲委派机制:
系统中的类加载器在工作时,会默认使用双亲委派机制。当一个类在加载的时候,系统会首先判断它是否被加载过,已经被加载过的类会直接返回,否则才会尝试加载; 加载的时候,首先会把请求委派给上层的加载类处理,每一层的加载器都会这样,把请求委派给自己的上层加载器进行处理;因此所有的请求最终都会委派给顶层的启动类加载器进行处理,只有当顶层的加载器无法处理,才会自己处理。
也正因此,我们自己写的java.lang.String类,在加载的时候,实际会被启动类加载器加载,
而启动类加载器会找到jdk中已经定义的Stirng,因此还是会使用jdk中的String,我们自己写这个类是没有意义的。
类加载过程:
jvm加载class类型文件主要有三步,分别是加载、连接、初始化。
其中,连接的构成又分为:验证、准备、解析这三步。
加载:
加载是类加载过程的第一步,主要进行了这三件事:
1.通过全类名获取定义此类的二进制字节流;
2.将字节流所代表的静态存储结构转换为方法区的运行时数据结构;
3.在内存中生成一个代表该类的Class对象,作为方法区这些数据的访问入口;
加载阶段和连接阶段的部分内容是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了;
验证:
验证阶段包括文件格式验证、元数据验证、字节码验证、符号引用验证。
文件格式验证:验证字节流是否符合Class文件格式的规范,例如:是否以0xCAFEBABE开头,主次版本号是否在当前虚拟机的处理范围内,常量池中的常量是否有不被支持的类型;
元数据验证:对字节码描述的信息进行与语义分析,以保证其描述的信息符合java语言规范的要求;例如:这个类是否有父类(除java.lang.Object类,其他类都有父类),这个类是否继承了不允许继承的类(被final修饰的类)等等;
字节码验证:这时最复杂的一个阶段。通过数据流和控制流分析,确定程序语义是合法的,符合逻辑的。比如保证任意时刻操作数栈和指令代码序列都能配合工作;
符号引用验证:确保解析动作能正确执行;
准备:
准备阶段是正式为类变量分配内存,并设置类变量初始值的阶段;这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:
1.这时候进行内存分配的,仅包括类变量(Class Variables,即静态变量,被static关键字修饰的变量,只与类相关,因此被称为类变量),而不包括实例变量。实例变量会在对象实例化时随着对象一块分配在java堆中。
2.从概念上讲,类变量所使用的内存都应该在方法区中分配。不过,jdk7之前,HotSpot虚拟机使用永久代来实现方法区,其实现是完全符合这种逻辑概念的,但是jdk8开始,HotSpot已经把原本放在永久代的字符串常量池、静态变量等移动到堆中,这个时候,类变量则会随着Class对象一起放在Java堆中。
3.这里所设置的初始值,通常情况下时数据类型默认的零值(0,0L,null,false等),比如我们定义了public static int value = 111;那么value变量在准备阶段的初始值是0而不是111(要到初始化阶段才会赋值)。特殊情况:给value变量加上了关键字public static final int value = 111;那么准备阶段value的值就会被赋值为111;
解析:
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程;解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。
符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。在程序实际运行时,只有符号引用是不够的。举个例子:在程序执行方法时,系统需要明确知道这个方法所在的位置。 JVM为每个类都准备了一张方法表来存放类中所有的方法,当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法了。
->综上,解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或字段、方法在内存中的指针或偏移量。
初始化:
初始化阶段是执行初始化方法
(
对
对于初始化阶段,虚拟机严格规范了有且只有5种情况下,必须对类进行初始化(只有主动去使用类,才会初始化类):
(1)当遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,比如new一个类,读取一个静态字段(未被final修饰)、或调用一个类的景天方法时。
- 当jvm执行new指令时会初始化类;即当程序创建一个类的实例对象时;
- 当jvm执行getstatic指令时会初始化类;即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池);
- 当jvm执行putstatic指令时会初始化傀儡;即程序给类的静态变量赋值时;
当jvm执行invokestatic指令时会初始化类;即程序调用类的惊天方法时;
(2)使用java.lang.reflect包的方法对类进行反射调用时,如Class.forname(“…”),newInstance()等等,这时如果类没有初始化,需要触发其初始化;
(3)初始化一个类,如果其父类还没初始化,那么先触发其父类的初始化;
(4)当虚拟机启动时,用户需要定义一个要执行的主类(包含main方法的那个类),虚拟机会初始阿虎这个类;
(5)MethodHanle和VarHandle可以看作时轻量级的反射调用机制,而想要使用这两个调用,就必须先使用findStaticVarHandle来初始化要调用的类;
补充: (6)当一个接口中定义了JDK8新加入的默认方法(被default关键字修饰的接口方法时),如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化;
——-
类的卸载:
7、JVM如何判断一个对象可以被回收?
在jvm中判断一个对象是否可以被回收,最重要的是判断这个兑现是否还在被使用,只要没有被使用,那么这个对象就可以被回收;
有两种算法可以实现,是引用计数器算法和可达性算法和可达性分析算法;
引用计数器算法:
为每一个对象添加一个引用计数器,指向当前对象的引用次数,如果但该你这个对象存在引用的更新,引用的计数器就会进行会增加,一旦这个引用计数器变为0,就意味着它可以被回收了;
这种方法需要额外的空间来存储引用计数器,但是它的实现比较简单,而且效率比较高;不过它在处理一些复杂的循环引用,或者相互依赖的一些情况下的时候,可能会出现一些不再使用但是又无法回收的内存,造成内存泄漏的问题;
可达性分析算法:
它的主要思想是,首先确定一些列不能回收的对象,作为GC root,比如说虚拟机栈里面的一个引用对象,以及本地方法栈里面的一些对象等,以GC root作为起始节点,从这些节点开始向下搜索,去寻找它的直接或者间接的引用对象,当遍历完以后,如果发现一些对象是不可达的,那么就认为这些对象已经没有引用了,需要被回收;而在垃圾回收的时候,jvm首先会找到所有的GC root,这个过程中会暂停所有的用户线程,然后再从GC root这些根节点向下搜索,可达的对象保留,不可达的对象就会被回收;
主流JVM都是用可达性分析算法;六、框架相关问题
1.Spring用了哪些设计模式? Spring注入bean的方式?对SpringIOC和SpringAOP的理解?
2.Spring事务隔离级别和传播机制
3.Mybatis的缓存机制(一级缓存和二级缓存),Mybatis的mapper文件中#和$的区别
4.SpringMVC的流程
5.Spring和SpringBoot的区别?
6.对SpringBoot的理解。
7.RPC框架有哪些,他们的区别?
8.Dubbo的使用和理解
9.SpringCloud的使用和组件,谈谈你的理解。
七、消息中间件
1.你们公司是如何进行消息中间件的技术选型?
2.如何保证消息中间件的高可用?
3.如何保证消息中间件重复发送消息?
4.消息队列积压了大量的消息,你该怎么处理?
5.如何保证消费者消费消息是有顺序的?
6.让你来开发一个消息中间件,你会怎么架构?
八、Redis
1.你们公司为什么要使用Redis?Redis有几种数据类型?
为什么要使用Redis(使用Redis的好处):
1、读写性能优秀;因为数据是在内存中,因此读写性能是很快的;
2、支持数据持久化;
3、支持主从复制,主机可以自动将数据复制到从机上,可以实现读写分离;
4、数据结构丰富,支持多种数据的存储;
5、可以用redis来实现分布式锁
Redis有几种数据类型:
本身是5种基本数据类型,分别是String、list、hash、set以及Zset;
redis6版本以后,新增了三种:bitmaps、HyperLogLog、Geospatial;
String:
String数据结构是简单的key-value类型,它是二进制安全的,可以包含任何数据,比如jpg图片或者序列化对象;一个redis的String类型value最多可以是512M;
list:
list是redis中链表。这是一个双向链表,可以支持反向查找和遍历,更方便操作,但是带来了部分额外的内存开销;它是简单的字符串列表,按照插入顺序排序,可以添加一个元素到列表的头部或尾部;
hash:
hash类似于jdk8之前的HashMap,是一个键值对集合,内部使用数组加链表实现。hash是一个Stirng类型的field和value的映射表,特别适合用于存储对象,类似于Java中的Map
set:
set类似于java中的HashSet,它可以自动排重,是一个无序集合。如果需要存储一个列表数据,又不希望出现重复数据时,可以用set。并且set提供了判断某个成员是否在一个set集合内的接口,这是list所没有的。它底层是一个value为null的hash表,所以添加,删除,查找 的复杂度都是O(1);
Zset:
又称sorted set,有序集合,它与set非常相似,是一个没有重复元素的字符串集合,但是,有序集合的每个成员都关联了一个权重参数score,这个权重参数被用来按照从最低分到最高分的方式排序集合中的成员。集合中的成员是唯一不可重复的,但是评分是可以重复的。
2.Redis持久化机制?Redis的过期策略?
Redis的持久化机制:
Redis提供了两种持久化机制:一种是持RDB(snapshotting,快照),另一种是AOF(ppend-only file,只追加文件);
1.RDB(snapshoting,快照):(默认使用此持久化方式)
RDB是什么:
RDB是在指定的时间间隔中,将内存中的数据集快照写入磁盘,它恢复时是将快照文件直接读到内存中;
Redis可以通过创建快照来获取存储在内存里的数据在某个时间点上的副本。Redis创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis主从结构,主要用来提高Redis性能),还可以将快照留在原地以便重启服务器的时候使用;
(RDB 快照就是记录某一个瞬间的内存数据,记录的是实际数据,这里提一点,Redis 的快照是全量快照,也就是说每次执行快照,都是把内存中的「所有数据」都记录到磁盘中。所以可以认为,执行快照是一个比较重的操作,如果频率太频繁,可能会对 Redis 性能产生影响。如果频率太低,服务器故障时,丢失的数据会更多。)
RDB是如何进行的:
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能,如果需要进行大规模数据的恢复,且对数据恢复的完整性不是非常敏感,那RDB的方式要比AOF的方式更高效;
RDB的缺点是最后一次持久化后的数据可能丢失;
2.AOF(append-only file只追加文件持久化)
AOF是什么:
AOF是以日志的形式来加入每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读指令不记录),只许追加文件,但不可以更改文件,Redis启动之初,会读取该文件重新构建数据。也就是,Redis重启的话就根据日志文件的内容,将写指令从前到后执行一次以完成数据的恢复工作。
AOF持久化流程:
首先,客户端的请求写命令会被append追加到AOF缓冲区内;
然后,AOF缓冲区根据AOF持久化策略,将操作sync同步到磁盘的AOF文件中;
AOF文件大小超过重写策略或手动重写时,会对AOF文件重写,压缩AOF文件容量;
Redis服务重启式,会重新load加载AOF文件中的写操作,达到数据恢复的目的;
AOF细节:
与RDB相比,AOF持久化实时性更好,因此已成为主流持久化方案。默认情况下,redis没有开启AOF,可以通过addpendonly参数开启: appendonly yes;
开启AOF后,每执行一条会更改redis中数据的命令,redis就会将该命令写入到内存缓存erver.aof_buf中,然后在根据appendfync配置来决定何时将其同步到硬盘的AOF文件中;
在Redis的配置文件中,存在三种不同的AOF方式,他们分别是:
appendfsync always:
每次有数据修改发生时,都会写入AOF文件,这样会严重降低Redis的速度;
appendfsync everysec:
每秒钟同步一次,显示地将多个写命令同步到硬盘;
appendfsync no:
让操作系统决定何时进行同步;
->为了兼顾数据和写入性能,用户可以考虑appendfsync ererysec选项,让redis每秒同步一次aof文件,这样redis的性能几乎没有受到任何影响,而且这样即使出现系统崩溃,用户最多只丢失一秒内差生的数据。
注:
Redis的过期策略(也即过期数据的删除策略):
->引申问题:Redis如何判断数据是否过期的呢? 这个问题后续探究
你设置了一批key只能存活1分钟,那么一分钟后,redis是怎么对这批key进行删除的呢?
Redis采用的过期策略是定期删除+懒惰删除策略;
首先,常用的删除策略有三种:
定期删除:
每隔一段时间,抽取一批key执行删除过期key的操作;并且Redis’底层会通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响;对内存比较友好;
惰性删除(也称懒汉式删除):
只会在取出key的时候才会对数据进行过期检查。这样对cpu最友好,但可能会造成太多过期key没有被删除;
定时删除:
在设置key的过期时间的同时,为该key创建一个定时器,到时间立即执行删除操作。这方式对内存友好,因为能保证过期了立马删除,但是对cpu不友好:若过期key很多,删除这些key会占用很多cpu时间;
->Redis采用的是定期删除+惰性删除策略
定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,所以 Redis 采用的是 定期删除+惰性/懒汉式删除 。
参考:https://blog.csdn.net/suoyx/article/details/114741566
但是,仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这样就导致大量过期 key 堆积在内存里,然后就 Out of memory 了。
->如何解决这个问题: 通过Redis的内存淘汰机制:
(相关问题MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?)
Redis提供了六种数据淘汰策略:
1.volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰;
2.volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰;
3.volatile-random:从已设置过期时间的数据集中任意选择数据淘汰;
4.alleys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个最常用);
5.allkeys-random:从数据集中任意选择数据淘汰;
6.no-eviction:这个是默认的淘汰策略;禁止驱逐数据,也即当内存不足以容纳新写入数据时,新写入数据会报错(但这个策略应该没人用);
-> redis4.0版本后,新增以下两种
volatile-lfu(least frequently used):从已设置过期时间数据集中挑选最不经常使用的数据淘汰;
allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的key;
3.怎么保证Redis的高可用?
可以通过1、主从复制;2、哨兵机制;3、搭建集群的方式,来保证Redis的高可用
这三种方式的具体介绍,后面学习;
4.什么是缓存穿透?如何避免?什么是缓存雪崩?如何避免?
缓存穿透:
什么是缓存穿透:
缓存穿透是指查询一个一定不存在的数据;即大量请求的key,在缓存中根本不存在,这时候由于缓存未命中,导致请求直接到了数据库上,根本没有经过缓存这一层。这时数据库也没有这个记录,这就导致这个不存在的数据每次请求都要到数据库去查询,这就是缓存穿透。
缓存穿透的风险:
在流量大的时候,可能数据库就去挂掉了,如果有人利用不存在的key频繁攻击我的应用,这就是一种漏洞;
如何解决缓存穿透:
1.最基本的就是,首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端,比如查询到数据库id不能小于0,传入的字段格式不对的时候,直接返回错误消息到客户端;
2.然后,可以缓存无效key,如果这个key的数据在redis和数据库中都查不到,那就缓存这个key,给他存个空结果;当然这有可能导致redis中缓存大量无效的key,所以尽量设置一个比较短的过期时间,比如一分钟;
3.通过布隆过滤器 (这个后续学习)
缓存雪崩:
什么是缓存雪崩:
缓存在同一时间,大面积失效,导致后面的请求都直接转发到数据库上,造成数据库短时间内承受大量的请求,数据库瞬时压力过重而雪崩;
如何解决缓存雪崩:
1.可以给缓存,比如热点数据设置过期时间时,增加一个随机值,这样每个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件;
2.当然,也可以缓存永不失效 ;
3.针对redis服务不可用的情况,采用redis集群,避免单机redis出现问题,而导致整个缓存服务都不可用
4.针对redis服务不可用的情况,采取限流措施,避免同时处理大量的请求;
5.如何保证缓存与数据库的双写一致性?
1.可以采取双写模式:修改数据库后,就也把缓存中的数据做修改;
这样可能产生脏数据,但这个脏数据是暂时性的,在数据稳定,缓存过期后,又能得到最新的正确数据;
这时,可以在更新数据时,加锁来解决。一个线程更新时,得到锁,等这个线程全更新完了,释放锁,其他线程才能得到锁; 又或者,如果对数据的一致性要求不是特别高,可以在缓存设计时,设置一个过期时间,等缓存过期了,重新写入缓存数据,以此来保证数据的最终一致性即可;
2.采取失效模式:
数据库修改后,直接将缓存删掉,等待下次查询时进行更新缓存;
也可能产生脏数据,也可加锁来解决。
3.缓存失效时间设短(不推荐,治标不治本):
针对失效模式,即:更新DB,直接删除缓存的情况,如果更新DB成功,而删除缓存这一步失败:
我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据;另外,这种方式对于先操作缓存后操作数据库的场景不适用;
4.增加cache更新重试机制(常用):
针对失效模式,即:更新DB,直接删除缓存的情况,如果更新DB成功,而删除缓存这一步失败:
如果缓存服务当前不可用,导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数自己设定;如果多次重试还是失败的话,可以把当前更新失败的key存入队列中,等缓存服务可用之后,再将缓存中对应的key删除即可;
6.Redis单线程模型原理,为什么能支撑高并发?
7.Redis哨兵架构的理解和底层原理。
九、数据库
1.工作中你是怎么优化sql的?
- 避免使用SELECT :实际业务场景中,我们可能只使用其中几列,查了很多数据但是不用,白白浪费资源,比如内存,cpu,也会增加数据传输时间; 而且select * 不会走覆盖索引,性能很低;
- 一批数据需要插入时,使用批量操作插入:如果不这样,每次远程请求数据库,性能消耗很大;
- 用连接查询代替子查询:子查询使用in关键字实现,一个查询语句的条件落在另一个语句的查询结果中;但是mysql执行子查询时,需要创建临时表,查询完毕再删除临时表,有一些额外的性能消耗;
- join连接查询的表不要过多:过多的话,mysql在选择索引时会非常复杂,容易选错索引;
- 选择合理的字段类型:
还有其他很多,后面说。
2.索引的种类?
3.什么情况下,索引会失效?
4.数据库的存储存储引擎,比如:MySQL的MyISAM和InnoDB区别?
5.索引的最左原则
6.索引的底层原理
7.你们公司是怎么进行分库分表?分库分表的方案(主从库,Mycat)
十、其他
1.分布式事务是怎么解决的?
2.分布式session方案?
3.设计一个秒杀场景。
4.怎么防止表单多次提交?
5.Linux的基本操作命令
6.ElasticSearch的使用和原理
7.Zookeep的使用和原理
HR面试
1.简历中写的过去工作经历的离职原因
2.当前公司薪资待遇
3.期望能到怎样的一家公司
4.个人未来的发展方向