Java面试题整理
Java基础
Java是解释性还是编译性语言?
既是编译性语言(需要由编译器编译为.class字节码文件),又是解释性语言(需要由JVM读一行执行一行,由解释器解释为操作系统能执行的命令)
Java的编译器是javac.exe,解释器是java.exe
为什么引入Hash?好处是什么?
简称散列算法,是将一个大文件映射成一个小串字符。与指纹一样,就是以较短的信息来保证文件的唯一性的标志,这种标志与文件的每一个字节都相关,而且难以找到逆向规律。
好处:
1) 在庞大的数据库中,由于哈希值更为短小,被找到更为容易,因此,哈希使数据的存储与查询速度更快。
2) 哈希能对信息进行加密处理,使得数据传播更为安全。
什么是动态代理?
动态代理就是,在程序运行期,创建目标对象的代理对象,并对目标对象中的方法进行功能性增强的一种技术。在生成代理对象的过程中,目标对象不变,代理对象中的方法是目标对象方法的增强方法。可以理解为运行期间,对象中方法的动态拦截,在拦截方法的前后执行功能操作。
什么是java的反射机制?
反射是动态获取信息以及动态调用对象方法的一种机制。
Java反射就是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;并且能改变它的属性。而这也是Java被视为动态语言的一个关键性质。
Java反射的功能是在运行时判断任意一个对象所属的类,在运行时构造任意一个类的对象,在运行时判断任意一个类所具有的成员变量和方法,在运行时调用任意一个对象的方法,生成动态代理。
final
一、final修饰类
final修饰一个类的时候,这个类不能被继承,final类中的方法都会被隐式的指定为final方法,JDK中被设计为final类的有String、System等。
二、final修饰方法
被final修饰的方法不能被重写,可以被重载,一个类的private方***隐式的被指定为final方法。
三、final修饰成员变量
被final修饰的成员变量必须要赋初始值,而且只能初始化一次,可以直接赋值或在构造方法中赋初值。如果final修饰的成员变量是基本类型,则表示这个变量的值不能改变,如果修饰的成员变量是一个引用类型,则引用的地址不能改变,但是这个引用所指向的对象里面的内容可以改变。
static修饰的方法可以被重写吗?重载呢?
不可以重写。
当我们在子类中改变方法体时,子类的该方法只是将父类的方法进行了隐藏,而非重写,这两个方法没有关系。父类引用指向子类对象时,只会调用父类的静态方法,所以不具有多态性。
可以重载。
Java的异常体系及异常的捕获和处理?
一、Java的异常体系
Throwable(表示可抛出)是所有异常和错误的超类,两个直接子类为Error和Exception,分别表示错误和异常。异常又可分为运行时异常(不检查异常)和非运行时异常(检查异常)。
1、Error和Exception:
Error是程序无法处理的错误,是由JVM产生和抛出的,比如OutOfMemoryError、ThreadDeath等。Error发生时JVM会选择线程终止。
Exception是程序可以处理的异常,程序中应当尽可能去处理这些异常。
2、运行时异常和非运行时异常:
运行时异常都是RuntimeException类及其子类异常,如NullPointerException、IndexOutOfBoundsException等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。
非运行时异常是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常。
二、异常的捕获和处理
1、try catch finally
1)可以组成try...catch...finally、try...catch、try...finally三种结构,catch可以有一个或多个,finally最多一个。
2)try、catch、finally三个代码块中变量的作用域为代码块内部,分别独立而不能相互访问。如果要在三个块中都可以访问,则需要将变量定义到这些块的外面。
3)多个catch块时候,最多只会匹配其中一个异常类且只会执行该catch块代码,而不会再执行其它的catch块,且匹配catch语句的顺序为从上到下,也可能所有的catch都没执行
2、throw和throws
throw关键字用于方法体内部,用来抛出一个Throwable类型的异常。如果抛出了检查异常,则应该在方法头部声明方法可能抛出的异常类型,该方法的调用者必须处理或继续抛出异常。如果所有方法都层层上抛获取的异常,最终JVM会进行处理,处理方式就是打印异常消息和堆栈信息。
throws关键字用于方法体外部的方法声明部分,用来声明方法可能会抛出某些异常。仅当抛出了检查异常,该方法的调用者才必须处理或者重新抛出该异常。当方法的调用者无力处理该异常的时候,应该继续抛出。
Java的方法分派?
方法分派指的是虚拟机如何确定应该执行哪个方法。
静态分派(方法重载):编译器确定,根据调用者的声明类型和方法参数类型。
动态分派(方法重写):运行时确定,根据调用者的实际类型分派。
Serializable接口中serialVersionUID的作用?
在序列化的时候系统将serialVersionUID写入到序列化的文件中去,当反序列化的时候系统会先去检测文件中的serialVersionUID是否跟当前类的serialVersionUID是否一致,如果一致则反序列化成功,否则就说明当前类跟序列化后的类发生了变化,比如是成员变量的数量或者是类型发生了变化,那么在反序列化时就会发生crash,并且报错。
JVM
引用的几种方式?
- 强引用:强引用是在程序代码之中普遍存在的引用赋值,类似
Object o = new Object()
这种引用关系。无论任何情况下,只要强引用关系还在,垃圾收集器就永远不会回收掉被引用的对象。 - 软引用:用来描述一些还有用但非必须的对象。只被软引用关联的对象,在系统要发生内存溢出异常前,会把这些对象列进回收范围之中进行二次回收,如果这次回收还是没有足够的内存,才会抛出溢出异常。
应用场景:做缓存(浏览器的后退按钮) - 弱引用:也是用来描述那些非必须对象,但它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
- 虚引用:最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用的唯一目的只是为了在这个对象被收集器回收时收到一个系统通知。
minorGC和MajorGC分别发生在什么时候?
minorGC:
1)Eden区满了 2)新创建对象的大小大于Eden所剩余空间
majorGC:
1)每次晋升到老年代的对象平均大小超过了老年代剩余空间
2)minorGC后存活的对象超过了老年代剩余空间
minor GC和major GC的过程?
minor GC:在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
major GC:参考CMS的工作过程。
垃圾收集算法及各自的优缺点?
1、标记-清除算法
“标记-清除”算法是最基础的算法,分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
缺点:
- 执行效率不稳定;标记和清除两个过程的执行效率随对象数量增长而降低。
- 内存空间的碎片化问题;标记、清除之后会产生大量不连续的内存碎片,导致当需要分配较大对象时无法找到足够的连续空间而不得不提前触发另一次垃圾收集动作。
2、标记-复制算法(针对新生代)
标记-复制算法将可用内存按容量划分为大小相等的两块,每次使用其中的一块。当这块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。
优点:
- 分配内存时不用考虑空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。
缺点:
- 将可用内存缩小为了原来的一半,空间浪费多。
3、标记-整理算法(针对老年代)
复制算法在对象存活率较高时就需要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用复制算法。
根据老年代的特点提出了“标记-整理”算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向空间一端移动,然后直接清理掉边界以外的内存。
标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式的。是否移动对象都存在弊端,移动对象操作必须全程暂停用户应用程序才能进行("Stop The World"),不移动对象会影响应用程序的吞吐量。
CMS收集器是怎样的?
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,基于标记-清除算法实现,是一款老年代收集器,它非常符合那些集中在互联网站或者B/S系统的服务端上的Java应用,这些应用都非常重视服务的响应速度。
整个工作流程包括四个步骤:
- 初始标记:仅仅标记GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。
- 并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,耗时较长但不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
- 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要“Stop The World”。
- 并发清除:清理删除标记阶段判断的已经死亡的对象,不需要移动存活对象,可以与用户线程同时并发。
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
优点:并发收集、停顿低
缺点:
- 对CPU资源敏感,总吞吐量会降低
- 无法处理浮动垃圾
- 标记-清除算法导致空间碎片
什么情况下会出现OOM?
OOM--OutOfMemoryError,当JVM因为没有足够的内存来为对象分配空间,并且垃圾回收器也已经没有空间可回收时,就会抛出这个error。
一、原因:
1、为虚拟机分配的内存太少
2、应用用的太多,并且用完没释放,此时就会造成内存泄露或者内存溢出
- 内存泄露:申请的内存在被使用完后没有释放,导致虚拟机不能再次使用该内存
- 内存溢出:申请的内存超出了JVM能提供的内存大小
大量的内存泄露可能会导致内存溢出。
二、出现OOM时的分析方法
Java堆内存的OOM异常是实际应用中常见的内存溢出异常情况,要解决这个区域的异常,一般的手段是先通过内存映像分析工具(如Eclipse Memory Analyzer)对Dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清除到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。
三、OOM的解决方法
1、如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收他们的。掌握了泄漏对象的类型信息及GC Roots引用链的信息,就可以比较准确的定位出泄漏代码的位置。
2、如果不存在泄漏,换句话说,就是内存中的对象确实还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还有向上调整的空间。再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗。
Java类加载机制
一、类加载的时机
1、隐式加载:new创建类的实例
2、显示加载:loaderClass、forName
forName和loaderClass区别?
- Class.forName()得到的class是已经初始化完成的
- ClassLoader.loadClass()得到的class是还没有链接的
3、访问类的静态变量,或者为静态变量赋值
4、调用类的静态方法
5、使用反射创建某个类或者接口的Class对象
6、初始化某个类的子类
7、直接使用java.exe命令来运行某个类
二、类加载的过程
当需要某个类的时候,jvm会加载.class文件,并创建对应的class对象,将class文件加载到虚拟机的内存,这个过程被称为类的加载。
- 加载:ClassLoader通过全类名查找类的字节码文件并创建一个class对象
- 链接
- 验证
- 准备:为类变量(static修饰)分配内存并赋初始值
- static int i = 5这里只是将i赋值为0,初始化的阶段再把i赋值为5
- 不包含final修饰的static,因为final在编译的时候就已经分配了
- 解析
- 初始化:如果该类有父类就对父类进行初始化
三、双亲委派模式
1、原理:
双亲委派模式要求除了顶层的启动类加载器之外,其余的类加载器都应该有自己的父类加载器,但是在双亲委派模式中父子关系采取的并不是继承的关系而是组合来复用父类加载器的相关代码。
如果一个类收到了类加载的请求,它并不会自己先去加载,而是把这个请求委托给父类加载器去执行,如果父类加载器还有父类加载器,则进一步向上委托,依次递归,请求最后到达顶层的启动类加载器,如果父类能够完成类的加载任务,就会成功返回,如果父类加载器无法完成任务,子类加载器才会尝试自己去加载。
通俗理解:每个儿子都很懒,遇到类加载的活都给爸爸干,直到爸爸说我也做不来的时候,儿子才会想办法自己去加载。
2、优点:
- 避免类的重复加载:父类加载器已经加载该类后子类加载器就没必要再加载一次
- 安全性:防止核心API库被篡改
四、如何破坏双亲委派机制?
自定义的类加载器重写loadClass()方法,如果不想破坏双亲委派,那么重写findClass()方法。
五、NoClassDefFoundError 和 ClassNotFoundException 有什么区别?
1、ClassNotFoundException:当应用程序运行的过程中尝试使用类加载器去加载Class文件的时候,如果没有在classpath中查找到指定的类,就会抛出ClassNotFoundException。一般情况下,当我们使用Class.forName()或者ClassLoader.loadClass()以及使用ClassLoader.findSystemClass()在运行时加载类的时候,如果类没有被找到,那么就会导致JVM抛出ClassNotFoundException。
2、NoClassDefFoundError:当JVM在加载一个类的时候,如果这个类在编译时是可用的,但是在运行时找不到这个类的定义的时候,JVM就会抛出一个NoClassDefFoundError错误。比如当我们在new一个类的实例的时候,如果在运行时类找不到,则会抛出一个NoClassDefFoundError的错误。
Java对象的创建过程
1、分配内存
2、初始化
实例变量初始化和实例代码块初始化:
如果对实例变量赋值或者使用实例代码块赋值,那么编译器会将其中的代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后,构造函数本身的代码之前。
构造函数初始化:
Java要求在实例化类之前,必须先实例化其超类,以保证所创建实例的完整性。
Java内存分区?
1、程序计数器:执行字节码的行号指示器,线程私有,没有OOM
2、Java虚拟机栈:存局部变量表、栈帧,线程私有
3、本地方法栈
4、Java堆:GC堆,存放对象实例,所有线程共享
5、方法区:常量池存在方法区,所有线程共享
数据结构与算法
LinkedList和ArrayList的区别?
1、数据结构不同
ArrayList是基于数组的数据结构,LinkedList是基于链表的数据结构
2、效率不同
当随机访问List(get和set操作)时,ArrayList比LinkedList的效率更高,因为LinkedList是线性的数据存储方式,所以需要移动指针从前往后依次查找。
当对数据进行增加和删除的操作(add和remove操作)时,LinkedList比ArrayList的效率更高,因为ArrayList是数组,所以在其中进行增删操作时,会对操作点之后所有数据的下标索引造成影响,需要进行数据的移动。
3、自由性不同
ArrayList自由性较低,因为它需要手动的设置固定大小的容量(默认初始容量为10),但是它的使用比较方便,只需要创建,然后添加数据,通过调用下标进行使用;
LinkedList自由性较高,能够动态的随数据量的变化而变化,但是它不便于使用。
4、主要控件开销不同
ArrayList主要控件开销在于需要在List列表预留一定空间;
LinkList主要控件开销在于需要存储结点信息以及结点指针信息。
HashMap和Hashtable的区别?
1、Hashtable出现的时间早(JDK1.0),HashMap出现的时间晚(JDK1.2)
2、Hashtable继承Dictionary类(已废弃),HashMap实现Map接口
3、Hashtable线程安全但效率低,HashMap非线程安全但效率高
4、HashMap可以存储null键和null值,Hashtable不可以存储null键和null值
B树和B+树的区别?
1、B树的每个结点都存储了key和data,B+树的data存储在叶子节点上,节点不存储data,这样一个节点就可以存储更多的key。可以使得树更矮,所以IO操作次数更少。
2、B+树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录,由于数据顺序排列并且相连,所以便于区间查找和搜索。而B树则需要进行每一层的递归遍历。相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。
JDK1.8中HashMap的get()和put()方法如何实现的?
put方法:
- 判断当前桶是否为空,为空则需要初始化(resize可以初始化桶数组或者进行扩容)
- 根据当前key的hashcode定位到具体的桶中并判断是否为空,为空表明没有Hash冲突就直接在当前位置创建一个桶即可
- 如果当前桶有值(Hash冲突),那么就要比较当前桶中的key、key的hashcode与写入的key是否相等,相等就赋值给e,之后会统一进行赋值及返回
- 如果当前桶为红黑树,那就按照红黑树的方式写入数据
- 如果是链表,就需要将当前的key、value封装成一个新节点写入到当前桶的后面(形成链表)
- 接着判断当前链表的大小是否大于阈值,大于时要转换为红黑树
- 如果在遍历过程中找到key和hashcode均相同时直接退出遍历
- 如果e不为空则存在相同的key和hashcode,就需要将值覆盖
- 最后判断是否需要进行扩容
get方法:
- 将key hash之后取得所定位的桶
- 如果桶为空则直接返回null
- 否则判断桶的第一个位置(可能是链表或红黑树)的key和hashcode是不是要查找的,如果是返回该node
- 如果第一个node不匹配,则判断它的下一个是红黑树还是链表
- 红黑树就按照树的查找方式返回值
- 否则就按照链表的方式遍历匹配返回值
HashMap为什么非线程安全?
HashMap在多线程情况下,在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,同时存在其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的
JDK1.8对hash算法和寻址算法进行了哪些优化?
1、hash算法:JDK1.7中通过key.hashcode()得到关键字的hash值,JDK1.8将hash值的高16位与低16位进行异或运算,使得hash值的低16位同时保留高16位和低16位的特征,对于两个hash值低16位相等,高16位不等的情况,减少hash冲突。
2、寻址算法:JDK1.7位hash值对数组长度进行取模运算,JDK1.8变为hash&(n-1),n为数组长度。当数组长度是2的幂次时,hash值对数组长度取模和hash&(n-1)的效果是一样的,但是与运算的性能更高。
什么是红黑树?
满足以下规则的二叉搜索树:
1、每个节点不是红色就是黑色
2、根节点为黑色
3、如果节点为红色,其子节点必须为黑色
4、任意一个节点到到NULL(树尾端)的任何路径,所含之黑色节点数必须相同
红黑树是接***衡的。
Dijkstra算法和Floyd算法对比
Dijkstra算法与Floyd算法都是广度优先搜索的算法。Dijkstra计算的是单源最短路径,Floyd计算的是多源最短路径。
Dijkstra算法本质上是贪心算法,下一条路径都是由当前更短的路径派生出来的更长的路径。不存在回溯的过程,不适用于有负数权值的场景。时间复杂度为O(n2),用堆优化为O((m+n)logn),其中m为边的个数,n为节点的个数。
Floyd算法实际上是一个动态规划算法。每一个点对u和v之间的最短路径,可能会经过N个点,这些中间点记为k。假定u到k之间的最短路径已经找好,k到v之间的最短路径已经找好,那么求u到v之间的最短路径,就是遍历各个可能的k点,然后求(u,k)+(k,v)之间的最小值。所以这实际上将大规模的问题自顶向下划分为了小规模的问题,这就是动态规划思想。实现使用三层循环,第一层循环设置中间点k,第二层循环设置起始点i,第三层循环设置结束点j。Floyd算法支持负权值但不支持负权环。时间复杂度为O(n3)
问:为什么弗洛伊德算法支持负权值?
答:因为路径更新是根据新值和旧值比较获得的,最终的结果都是在最后一次迭代过程中对全局进行更新而得到的,中间的每次迭代只是一次局部调整而非最终结果。而不像迪杰斯特拉采用的贪心策略,每一次迭代都确定出一条最短路径,负权的出现使得不能保证每次迭代都是最优解。
各种排序算法及其适用场景?
稳定的排序:冒泡、插入、归并
不稳定的排序:选择、快排、堆排序
适用场景:
1、冒泡排序:数据量不大,对稳定性有要求,数据基本有序的情况下
2、选择排序:数据量不大,对稳定性没有要求
3、插入排序:数据量不大,对稳定性有要求,且数据局部或者整体有序的情况
堆排序的过程?
参考博客:https://blog.csdn.net/u010452388/article/details/81283998
1、将无序数组构造成一个大根堆:新插入的数据与其父节点比较,如果插入的数比父节点大,则与父节点交换,直到小于等于父节点或者来到顶端。这一步骤涉及到元素上升
2、固定一个最大值,将剩余的数重新构造成一个大根堆,重复此过程。这一步骤涉及到元素下降
堆排序的时间复杂度O(NlogN),额外空间复杂度O(1),是一个*不稳定性**的排序
KMP字符串匹配算法?
参考博客:https://www.cnblogs.com/dusf/p/kmp.html
提取加速匹配的信息,通过消除主串指针的回溯来提高匹配的效率,也就是利用已经部分匹配这个有效信息,保持i指针不回溯,通过修改j指针,让模式串尽量的移动到有效的位置。维护一个next数组,next[j]=k,表示当T[i] != P[j]时,j指针的下一个位置。另一个恒等的定义为:k值是j位前的子串的最大重复子串的长度。KMP算法的时间复杂度为O(m+n)
public static int[] getNext(String ps) { char[] p = ps.toCharArray(); int[] next = new int[p.length]; next[0] = -1; int j = 0; int k = -1; while (j < p.length - 1) { if (k == -1 || p[j] == p[k]) { if (p[++j] == p[++k]) { // 当两个字符相等时要跳过 next[j] = next[k]; } else { next[j] = k; } } else { k = next[k]; } } return next; }
框架
Listener,Filter,Servlet执行顺序和生命周期?
一、执行顺序
理(Listener)发(Filter)师(servlet)
二、生命周期
1、Listener生命周期:一直从程序启动到程序停止运行。
ServletRequestListener:每次访问一个Request资源前,都会执行requestInitialized()方法,方法访问完毕,都会执行requestDestroyed()方法。
HttpSessionListener:每次调用request.getSession(),都会执行sessionCreated()方法,执行session.invalidate()方法,都会执行sessionDestroyed()方法。
ServletRequestAttributeListener:每次调用request.setAttribute()都会执行attributeAdded()方法,如果set的key在request里面存在,就会执行attributeReplaced()方法,调用request.removeAttribute()方法,都会执行attributeRemoved()方法。
2、Filter生命周期:
- 程序启动调用Filter的init()方法(只调用一次)
- 程序停止调用Filter的destroy()方法(只调用一次)
- 每次的访问请求如果符合拦截条件都会调用doFilter()方法
- 程序第一次运行,会在servlet调用init()方法以后调用
- 不管第几次,都在调用doGet(),doPost()方法之前
3、Servlet生命周期:
- 程序第一次访问,会调用servlet的init()方法初始化(只调用一次)
- 每次程序执行都会根据请求调用doGet()或者doPost()方法
- 程序停止调用destory()方法(只调用一次)
spring的IOC/DI是什么?
控制翻转(和依赖注入是一回事),一种设计思想,将设计好的对象交给容器控制。
IOC容器就是具有依赖注入功能的容器,IOC容器负责实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。应用程序无需在代码中new相关的对象,应用程序由IOC容器进行组装。
spring如何实现依赖注入?
spring实现依赖注入有两种方式:构造函数注入和setter注入
构造函数注入 | setter 注入 |
---|---|
没有部分注入 | 有部分注入 |
不会覆盖 setter 属性 | 会覆盖 setter 属性 |
任意修改都会创建一个新实例 | 任意修改不会创建一个新实例 |
适用于设置很多属性 | 适用于设置少量属性 |
setter 注入使用的更多
spring如何解决循环依赖?
spring的三级缓存:
缓存 | 用途 |
---|---|
singletonObjects | 用于存放完全初始化好的 bean,从该缓存中取出的 bean 可以直接使用 |
earlySingletonObjects | 存放原始的 bean 对象(尚未填充属性),用于解决循环依赖 |
singletonFactories | 存放 bean 工厂对象,用于解决循环依赖 |
Spring 解决循环依赖的过程:
- 首先 A 完成初始化第一步并将自己提前曝光出来(通过 ObjectFactory 将自己提前曝光),在初始化的时候,发现自己依赖对象 B,此时就会去尝试 get(B),这个时候发现 B 还没有被创建出来
- 然后 B 就走创建流程,在 B 初始化的时候,同样发现自己依赖A,于是尝试 get(A),这个时候由于 A 已经添加至缓存中(一般都是添加至三级缓存
singletonFactories
),通过 ObjectFactory 提前曝光,所以可以通过ObjectFactory.getObject()
方法来拿到 A 对象,B拿到 A 对象后顺利完成初始化,然后将自己添加到一级缓存中 - 回到 A,A 也可以拿到 B 对象,完成初始化,到这里整个链路就已经完成了初始化过程了。
spring中bean的生命周期?作用域?
一、生命周期
实例化-->属性赋值-->初始化-->销毁
影响多个Bean的接口:BeanPostProcessor,与自动注入以及AOP的实现有关
只调用一次的接口:
- Aware类型的接口:从spring容器中拿到一些资源
- 生命周期的接口:让我们自己实现初始化和销毁
二、作用域
在spring配置文件定义Bean时,通过声明scope配置项,可以定义Bean的作用域,一共有五种:
1、singleton:IOC容器仅创建一个Bean实例,IOC容器每次返回的是同一个Bean实例。(默认的作用域)
2、prototype:IOC容器可以创建多个Bean实例,每次返回的都是一个新的实例。
3、request:该属性仅对HTTP请求产生作用,使用该属性定义Bean时,每次HTTP请求都会创建一个新的Bean,适用于WebApplicationContext环境。
4、 session:该属性仅用于HTTP Session,同一个Session共享一个Bean实例。不同Session使用不同的实例。
5、global-session:该属性仅用于HTTP Session,同session作用域不同的是,所有的Session共享一个Bean实例。
@Autowired和@Resource比较
相同点:作用相同,都可以标注在字段或属性的setter方法上
不同点:@Autowired默认按照类型装配,@Resource默认按照名称装配
spring的AOP是什么?实现原理?
面向切面编程,将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。
关于 AOP 的原理,概括来说就是通过代理模式为目标对象生产代理对象,并将横切逻辑插入到目标方法执行的前后。
实现AOP的技术,主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码。
spring事务中的传播行为?
事务行为 | 说明 |
---|---|
PROPAGATION_REQUIRED | 支持当前事务,假设当前没有事务,就新建一个事务 |
PROPAGATION_SUPPORTS | 支持当前事务,假设当前没有事务,就以非事务方式运行 |
PROPAGATION_MANDATORY | 支持当前事务,假设当前没有事务,就抛出异常 |
PROPAGATION_REQUIRES_NEW | 新建事务,假设当前存在事务,把当前事务挂起 |
PROPAGATION_NOT_SUPPORTED | 以非事务方式运行操作。假设当前存在事务,就把当前事务挂起 |
PROPAGATION_NEVER | 以非事务方式运行,假设当前存在事务,则抛出异常 |
PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。 |
spring中默认的事务传播行为是PROPAGATION_REQUIRED,ServiceA.methodA调用ServiceB.methodB,有一条sql执行失败了,ServiceA.methodA要回滚,无论ServiceB.methodB的事务是否被提交。
Mybatis的映射配置文件中,动态传递参数有两种方式:#{}和${},它们有什么区别?
1、#{}为参数占位符?即sql预编译,${}为字符串替换,即sql拼接
2、变量替换后,#{} 对应的变量自动加上单引号,${} 对应的变量不会加上单引号
3、#{}能防止sql注入,${}不能防止sql注入
什么是sql注入?
在实现定义好的查询语句的结尾添加额外的sql语句,欺骗数据库服务器进行非授权的任意查询,盗取数据库数据。
在Mybatis中尽量使用#{}
Mybatis如何根据映射器(mapper.xml文件)生成sql语句?
1、XMLConfigBuilder解析映射器xml文件时,会将每一个sql语句和其配置的内容保存起来
2、mybatis中一条SQL与它相关的配置信息是由MappedStatement、SqlSource和BoundSql三个部分组成
- MappedStatement的作用是保存一个映射器节点(select|insert|delete|update)的内容,它是一个类,包括许多我们配置的SQL、SQL的id、resultMap等重要配置内容,同时还有一个重要的属性sqlSource。mybatis通过读取MappedStatement来获得某条SQL配置的所有信息。
- SqlSource是提供BoundSql对象的地方,它是一个接口,使用它就可以得到一个BoundSql对象。
- BoundSql是一个结果对象,是建立SQL和参数的地方。
Mybatis一级缓存和二级缓存的区别
1、一级缓存:SqlSession范围的缓存,默认开启,在同一个SqlSession中,执行相同的SQL查询时,第一次会去查询数据库,并写在缓存中,第二次会直接从缓存中取。Mybatis的内部缓存使用一个HashMap,key为hashcode+statementId+sql语句,value为查询出来的结果集映射成的Java对象,两次查询sql中间如果有增删改操作会清空缓存。
2、二级缓存:Mapper级别的缓存,跨SqlSession,默认没有开启,SqlSession1第一次调用Mapper下的SQL进行查询后会将结果存放在Mapper对应的二级缓存区域,SqlSession2再调用Mapper中相同的SQL查询时,会去对应的二级缓存内取结果。如果SqlSession3执行commit提交,将会清空该Mapper映射下的二级缓存区域的数据。
SpringBoot的启动加载过程和自动配置流程?
一、自动配置:@SpringBootApplication
@SpringBootApplication中有三个注解:@Configuration、@EnableAutoConfiguration和@ComponentScan
1、@Configuration:启动类标注了@Configuration后相当于一个IOC容器的配置类,会在spring的XML配置文件applicationContent.xml中装配所有bean事务,提供一个spring的上下文环境
2、@EnableAutoConfiguration:帮助SpringBoot将所有符合条件的@Configuration配置都加载到当前SpringBoot创建并使用的IoC容器
3、@ComponentScan:组件扫描,可自动发现和装配Bean
二、启动加载:application.run()
主要创建了配置环境(environment)、事件监听(listeners)、应用上下文(applicationContext),并基于以上条件,在容器中开始实例化我们需要的Bean
并发编程
synchronized锁的底层原理?
JVM中对象是分成三部分存在的:对象头、实例数据、对其填充。(实例数据存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐;对其填充不是必须部分,由于虚拟机要求对象起始地址必须是8字节的整数倍,对齐填充仅仅是为了使字节对齐。)
对象头是synchronized实现锁的基础,因为synchronized申请锁、上锁、释放锁都与对象头有关。对象头主要结构是由Mark Word
和 Class Metadata Address
组成,其中Mark Word
存储对象的hashCode、锁信息或分代年龄或GC标志等信息,Class Metadata Address
是类型指针指向对象的类元数据,JVM通过该指针确定该对象是哪个类的实例。
JDK6后锁有四种状态,无锁状态、偏向锁、轻量级锁、重量级锁,其中无锁就是一种状态了。锁的类型和状态在对象头Mark Word
中都有记录,在申请锁、锁升级等过程中JVM都需要读取对象的Mark Word
数据。
每一个锁都对应一个monitor对象,在HotSpot虚拟机中它是由ObjectMonitor实现的(C++实现),当一个monitor被某个线程持有后,它便处于锁定状态。
Java中对象锁的四种状态及锁升级的过程?
一、对象锁的四种状态
1、无锁
2、偏向锁
多数情况下,锁不仅不存在竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁
3、轻量级锁
如果明显存在其他线程申请锁,那么偏向锁将很快升级为轻量级锁
4、重量级锁
指的是原始的Synchronized的实现,其他线程试图获取锁时,都会被阻塞,只有持有锁的线程释放锁之后才会唤醒这些线程
二、锁升级的场景
1、经常只有一个线程来加锁,使用偏向锁,偏向锁的执行流程如下:
- 线程首先检查该对象头的线程ID是否为当前线程
- 如果对象头的线程ID和当前线程ID一直,则直接执行代码
- 如果不是当前线程ID则使用CAS方式替换对象头中的线程ID
- 如果使用CAS替换不成功则说明有线程正在执行,存在锁的竞争,这时需要撤销偏向锁,升级为轻量级锁
- 如果CAS替换成功,则把对象头的线程ID改为自己的线程ID,然后执行代码
- 执行代码完成之后释放锁,把对象头的线程ID修改为空
2、有线程来参与锁的竞争,但是获取锁的冲突时间很短
当开始有锁的冲突了,那么偏向锁就会升级到轻量级锁;线程获取锁出现冲突时,线程必须做出决定是继续在这里等,还是回家等别人打电话通知,而轻量级锁就是采用继续在这里等的方式,当发现有锁冲突,线程首先会使用自旋的方式循环在这里获取锁,因为使用自旋的方式非常消耗CPU,当一定时间内通过自旋的方式无法获取到锁的话,那么锁就开始升级为重量级锁了。
3、有大量的线程参与锁的竞争,冲突性很高
当获取锁冲突多,时间长的时候,线程无法继续死等,只好先休息,然后等前面获取锁的线程释放了锁之后再开启下一轮的锁竞争,而这种形式就是重量级锁。
乐观锁和悲观锁
一、概念
乐观锁:所谓的乐观锁,指的是在操作数据的时候非常乐观,乐观地认为别人不会同时修改数据,因此乐观锁不会上锁,只有在执行更新的时候才会去判断在此期间别人是否修改了数据,如果别人修改了数据则放弃操作,否则执行操作。
悲观锁:所谓的悲观锁,指的是在操作数据的时候比较悲观,悲观地认为别人一定会同时修改数据,因此悲观锁在操作数据时是直接把数据上锁,直到操作完成之后才会释放锁,在上锁期间其他人不能操作数据。
二、实现方式
乐观锁:
1、CAS:比较并交换,如果等于预期值则更新,否则不进行操作。许多CAS操作都是自旋的,意思就是,如果操作不成功,就会一直重试,直到操作成功为止。
CAS的缺点?
1)ABA问题:其他线程将数据从A修改到B再改回A,由于数据与开始时一致,当前线程并不知道数据被修改过。
2)高竞争下的开销问题:CAS如果失败会一致重试,造成CPU开销大,可以引入退出机制,重试次数超过阈值就强制失败退出。
3)自身功能受限:只能保证单个变量操作的原子性。
2、版本号机制:版本号机制的基本思路,是在数据中增加一个version字段用来表示该数据的版本号,每当数据被修改版本号就会加1。当某个线程查询数据的时候,会将该数据的版本号一起读取出来,之后在该线程需要更新该数据的时候,就将之前读取的版本号与当前版本号进行比较,如果一致,则执行操作,如果不一致,则放弃操作。
悲观锁:
悲观锁的实现方式也就是加锁,加锁既可以在代码层面(比如Java中的synchronized
关键字),也可以在数据库层面(比如MySQL中的排他锁)。
三、适用场景
在竞争不激烈(出现并发冲突的概率比较小)的场景中,乐观锁更有优势。因为悲观锁会锁住代码块或数据,其他的线程无法同时访问,必须等待上一个线程释放锁才能进入操作,会影响并发的响应速度。另外,加锁和释放锁都需要消耗额外的系统资源,也会影响并发的处理速度。
在竞争激烈(出现并发冲突的概率较大)的场景中,悲观锁则更有优势。因为乐观锁在执行更新的时候,可能会因为数据被反复修改而更新失败,进而不断重试,造成CPU资源的浪费。
Synchronized 和 Lock 区别
1、Synchronized 内置的Java关键字, Lock 是一个Java类
2、Synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁
3、Synchronized 会自动释放锁,lock 必须要手动释放锁!如果不释放锁,死锁
4、Synchronized 线程 1(获得锁,阻塞)、线程2(等待,傻傻的等);Lock锁就不一定会等待下去;
5、Synchronized 可重入锁,不可以中断的,非公平;Lock ,可重入锁,可以 判断锁,非公平(可以自己设置);
6、Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码!
AQS的实现原理?
AbstractQueuedSynchronizer(AQS --简称同步器)是用来构建锁及其他同步组件的基础框架,它的实现主要是依赖一个int状态变量以及通过一个FIFO队列共同构成同步队列。当多个线程竞争共享资源时,一个线程竞争到共享资源后,其他请求资源的线程会被阻塞,进入同步队列,也就是说同步队列中存放的被阻塞的线程,这些线程等待cpu调度再次竞争共享资源。int状态的更新使用CAS,同步器既支持独占锁,也支持共享锁。AQS的设计使用模板方法设计模式,它将一些方法开放给子类进行重写。
wait和sleep的区别?
1、来自不同的类:wait->Object,sleep->Thread
2、关于锁的释放:wait 会释放锁,sleep 睡觉了,抱着锁睡觉,不会释放!
3、使用的范围不同:wait必须在同步代码块中,sleep可以在任何地方睡
4、是否需要捕获异常:wait不需要捕获异常,sleep必须捕获异常
Java如何实现多线程?
1、继承Thread类,重写run()方法,使用start()开启多线程
2、实现Runnable接口,Runnable 没有返回值、效率比Callable 相对较低
3、实现Callable接口(JUC下的)
volatile
一、volatile的作用?
1、保证可见性
如何保证可见性?
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立刻刷新到主内存中。
当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存读取共享变量。
2、不保证原子性
3、可以避免指令重排(内存屏障的存在)
volatile是线程安全的吗?
volatile不是线程安全的,volatile是一种弱的同步机制,保护的是变量安全,想要强线程安全需要用synchronized或lock锁。
ThreadLocal
一、ThreadLocal的原理?
ThreadLocal是线程本地存储,在每个线程中都创建了一个静态的ThreadLocalMap对象(key是当前线程的句柄,value是需要保持的值),每个线程可以访问自己内部ThreadLocalMap对象内的value。
ThreadLocal的作用是提供一种线程隔离,将变量与线程相绑定,保证线程安全。
二、ThreadLocal的缺点?
内存泄露。原因是ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除(remove()方法)对应key就会导致内存泄露。
三、ThreadLocalMap的key使用哪种引用?为什么?
弱引用,在使用完ThreadLocal,当前Thread依然运行的情况下,就算忘记调用remove方法,弱引用比强引用多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set/get/remove中的任一方法的时候会被清除,从而避免内存泄露。
Java线程池
一、线程池的实现原理
Java线程池的实现原理就是一个线程集合workerset和一个阻塞队列workqueue,当用户向线程池提交一个任务时,线程池会先将任务放入workqueue中,workerSet中的线程会不断的从workQueue中获取线程然后执行。当workQueue中没有任务的时候,worker就会阻塞直到队列中有任务了就取出来继续执行。
二、七大参数
三、submit一个task以后进行什么操作?
1、判断当前运行的worker数量是否超过corePoolSize,如果不超过corePoolSize就创建一个worker直接执行该任务(线程池最开始没有worker在运行)
2、如果正在运行的worker数量大于等于corePoolSize,那么就将该任务加入到workQueue中去
3、如果workQueue满了就检查当前运行的worker数量是否小于maximumPoolSize,如果小于就创建一个worker执行该任务
4、如果当前运行的worker数量大于等于maximumPoolSize,那么就执行拒绝策略
四、线程池的作用?
线程复用、控制最大并发数、管理线程
Thread中interrupt()interrupted()和isInterrupted()的区别
1、interrupt():非静态方法,作用范围线程实例,由线程实例对象调用,其作用是中断此线程实例(注:不一定是当前线程),但实际上只是给线程设置一个中断标志,线程仍会继续运行。
2、interrupted():静态方法,作用范围当前线程,作用是测试当前线程是否被中断(检查中断标志),返回一个boolean并清除中断状态,第二次再调用时中断状态已经被清除,将返回一个false(换句话说,复位线程状态,如果已经复位,再次复位则返回false)。
3、isInterrupted():非静态方法,作用范围线程实例,由线程实例对象调用,其作用是只测试此线程是否被中断 ,不清除中断状态。
数据库
索引
索引使用哪种数据结构?区别?Mysql使用哪种?
一、常见的索引有Hash索引和B+树索引,Mysql使用的是InnoDB引擎,默认是B+树索引。。
二、Hash索引和B+树索引的区别:
Hash索引适合等值查询,无法进行范围查询
Hash索引没法利用索引完成排序
Hash索引不支持联合索引的最左匹配原则
什么是联合索引的最左前缀匹配原则?
在创建联合索引时,根据业务需求,where子句中使用最频繁的一列放在最左边,因为MySQL索引查询会遵循最左前缀匹配原则,即最左优先,在检索数据时从联合索引的最左边开始匹配。创建一个联合索引(key1,key2,key3)相当于创建了(key1)、(key1,key2)、(key1,key2,key3)三个索引,能够减少开销。
如果有大量重复键值的情况下,哈希索引的效率会很低,因为存在哈希碰撞问题
三、聚簇索引和非聚簇索引?
1、B+树的叶子节点存储了整行数据的是主键索引,也叫聚簇索引
2、B+树的叶子节点存储了主键的值的是非主键索引,也叫非聚簇索引
二者查询数据时的区别:
聚簇索引(主键索引)查询会更快,因为主键索引树的叶子节点就是要查询的数据,而非主键索引的叶子节点是主键的值,还需要进行回表。
非主键索引一定会回表查询多次吗?
不是,通过覆盖索引也可以只查询一次。
覆盖索引:一个查询语句的执行只用从索引中就能够取得,不必从数据表中读取。
什么时候需要建索引?什么时候没必要建索引?
索引方便了查找的效率,但是会导致增删改的速率变慢(因为与主键绑定)。
一、需要建立索引的情况
1、主键自动建立唯一索引
2、频繁进行查询的字段应该创建索引
3、与其他表进行联合查询的字段,外键关系建立索引
主键和外键?
主键是能确定一条记录的唯一标识
外键用于与另一张表的关联,是能确定另一张表记录的字段,用于保持数据一致性
4、排序或分组要用到的字段
二、不需要创建索引的情况
1、表记录太少
2、字段经常进行增删改的操作
3、where条件用不到的字段
索引失效的七种情况?
1、条件中有or必须每个列都加上索引
2、复合索引未用左列字段
3、like以%开头
4、需要类型转换
5、where中索引列有运算
6、where中索引列使用了函数
7、如果mysql觉得全表扫描更快时(数据少)
Mysql5.6对于索引的优化?
引入了索引下推优化,可以在有like条件查询的情况下,减少回表次数。
通过什么方法来判断有没有走索引查询?
可以通过explain查看sql语句的执行计划,通过执行计划来分析索引使用情况。
如何用explain来分析一条sql的执行计划?
explain语句的各项输出如下:
列名 | 用途 |
---|---|
id | 每一个SELECT关键字查询语句都对应一个唯一id |
select_type | SELECT关键字对应的查询类型 |
table | 表名 |
partitions | 匹配的分区信息 |
type | 单表的访问方法 |
possible_keys | 可能用到的索引 |
key | 实际使用到的索引 |
key_len | 实际使用到的索引长度 |
ref | 当使用索引列等值查询时,与索引列进行等值匹配的对象信息 |
rows | 预估需要读取的记录条数 |
filtered | 某个表经过条件过滤后剩余的记录条数百分比 |
Extra | 额外的一些信息 |
重点说一下type
字段:
最优到最差分别为:system > const > eq_ref > ref > range > index > ALL
一般来说,得保证查询至少达到range级别,最好能达到ref。
system,const
:MySQL 能对查询的某部分进行优化并将其转化成一个常量。用于主键或唯一二级索引列与常数比较时,所以表最多有一个匹配行,读取1次,速度比较快。system
是const
的特例,表里只有一条记录匹配时为system
。eq_ref
:在连接查询时,如果被驱动表是通过主键或者唯一二级索引列等值匹配的方式进行访问的,则对该被驱动表的访问方法就是eq_ref
。这可能是在 const 之外最好的联接类型了。ref
:相比 eq_ref,不使用唯一索引,而是使用普通索引或者唯一性索引的部分前缀,索引要和某个值相比较,可能会找到多个符合条件的行。range
:使用索引获取范围区间的记录,通常出现在in, between ,> ,<, >=
等操作中。index
:扫描全表索引,这通常比ALL快一些。(index
是从索引中读取的,而ALL
是从硬盘中读取)。ALL
:即全表扫描,MySQL 需要从头到尾去查找表中所需要的行。通常情况下这需要增加索引来进行优化了。
再来说一下extra
字段:
Using index
:表示相应的select操作中使用了覆盖索引,查询的列被索引覆盖,不需要回表查询。Using temporary
:MySQL 中需要创建一张内部临时表来处理查询。Using filesort
:排序无法使用索引来排序。
当发现Extra
提示为 Using filesort
、Using temporary
时就需要格外注意了,考虑索引优化。
数据库引擎有哪几种?该如何选型?
主要有InnoDB和MyISAM。
如果数据表主要用来插入和查询记录,则MyISAM引擎能提供较高的处理效率,不支持外键,不支持事务。
如果要提供提交、回滚、崩溃恢复能力的事物安全(ACID兼容)能力,并要求实现并发控制,InnoDB是一个好的选择,支持外键和事务。
为什么MyISAM读比InnoDB快?
1、索引和数据分开存储2、不支持事务
为什么InnoDB写比MyISAM快?
InnoDB默认行锁,而MyISAM只有表锁
并发事务
事务的并发问题是如何发生的?
多个事务同时操作同一个数据库的相同数据
并发问题有哪些?
- 脏读:一个事务读取了其他事务还没有提交的数据,读到的是其他事务“更新”的数据
- 不可重复读:一个事务多次读取,结果不一样
- 幻读:一个事务读取了其他事务还没有提交的数据,读到的是其他事务“插入”的数据
如何解决并发问题?
设置隔离级别
事务的隔离级别有哪些?分别解决了什么问题?
脏读 | 不可重复读 | 幻读 | |
---|---|---|---|
read uncommitted(读未提交) | √ | √ | √ |
read committed(读已提交) | × | √ | √ |
repeatable read(可重复读) | × | × | √ |
serializable(串行化) | × | × | × |
mysql的默认隔离级别是可重复读。
mysql可重复读的实现原理?
使用一种叫MVCC的控制方式 ,即Mutil-Version Concurrency Control,多版本并发控制,类似于乐观锁的一种实现方式,为每行数据存储一个版本号,当数据被修改时,版本号加1
数据库的分库分表?
一、数据库瓶颈
数据库到达瓶颈时会导致数据库的活跃连接数增加,逼近数据库可承载活跃连接数的阈值,业务层来看就是数据库连接少甚至无连接可用,会导致并发量、吞吐量、数据库崩溃等问题
1、IO瓶颈
1)磁盘读IO瓶颈:热点数据太多,数据库缓存放不下,每次查询时会产生大量的IO,降低查询速度
解决方法:分库和垂直分表
2)网络IO瓶颈,请求的数据太多,网络带宽不够
解决方法:分库
2、CPU瓶颈
1)SQL问题:如SQL中包含join,group by,order by,非索引字段条件查询等,增加CPU运算的操作
解决方法:SQL优化,简历合适的索引
2)单表数据量太大,查询是扫描的行太多,SQL效率低,CPU率先出现瓶颈
解决方法:水平分表
二、分库分表
1、水平分库:以字段为依据,按照一定策略(hash、range等),将一个库中的数据拆分到多个库中。
结果:
- 每个库的结构都一样;
- 每个库的数据都不一样,没有交集;
- 所有库的并集是全量数据;
场景:系统绝对并发量上来了,分表难以根本上解决问题,并且还没有明显的业务归属来垂直分库。
分析:库多了,io和cpu的压力自然可以成倍缓解。
2、水平分表:以字段为依据,按照一定策略(hash、range等),将一个表中的数据拆分到多个表中。
结果:
- 每个表的结构都一样;
- 每个表的数据都不一样,没有交集;
- 所有表的并集是全量数据;
场景:系统绝对并发量并没有上来,只是单表的数据量太多,影响了SQL效率,加重了CPU负担,以至于成为瓶颈。
分析:表的数据量少了,单次SQL执行效率高,自然减轻了CPU的负担。
3、垂直分库:以表为依据,按照业务归属不同,将不同的表拆分到不同的库中。
结果:
- 每个库的结构都不一样;
- 每个库的数据也不一样,没有交集;
- 所有库的并集是全量数据;
场景:系统绝对并发量上来了,并且可以抽象出单独的业务模块。
分析:将相关的表拆到单独的库中,可以服务化。
4、垂直分表:以字段为依据,按照字段的活跃性,将表中字段拆到不同的表(主表和扩展表)中。
结果:
- 每个表的结构都不一样;
- 每个表的数据也不一样,一般来说,每个表的字段至少有一列交集,一般是主键,用于关联数据;
- 所有表的并集是全量数据;
场景:系统绝对并发量并没有上来,表的记录并不多,但是字段多,并且热点数据和非热点数据在一起,单行数据所需的存储空间较大。以至于数据库缓存的数据行减少,查询时会去读磁盘数据产生大量的随机读IO,产生IO瓶颈。
分析:垂直分表的拆分原则是将热点数据放在一起作为主表,非热点数据放在一起作为扩展表。这样更多的热点数据就能被缓存下来,进而减少了随机读IO。拆了之后,要想获得全部数据就需要关联两个表来取数据。但记住,千万别用join,因为join不仅会增加CPU负担并且会讲两个表耦合在一起(必须在一个数据库实例上)。关联数据,应该在业务Service层做文章,分别获取主表和扩展表数据然后用关联字段关联得到全部数据。
三、分库分表步骤
根据容量(当前容量和增长量)评估分库或分表个数 -> 选key(均匀)-> 分表规则(hash或range等)-> 执行(一般双写)-> 扩容问题(尽量减少数据的移动)
MySQL中的锁?什么情况下行锁会变成表锁?
行锁+表锁
没有索引或者索引失效时,InnoDB 的行锁变表锁
原因:Mysql 的行锁是通过索引实现的
操作系统
死锁
一、什么是线程死锁?
首先明确:进程死锁和线程死锁的区别就是死锁的基本单位不同。
线程死锁是当多个线程互相持有对方所需要的资源时,会互相等待对方释放资源,如果线程都不主动释放所占有的资源,将产生死锁。
二、死锁产生的四个条件?
1、互斥:一个资源只能被一个进程使用
2、不剥夺:进程已经获得的资源在使用完以前不能强行剥夺
3、请求和保持:一个进程因请求资源而阻塞时,对已获得的资源保持不放
4、循环等待:多个进程之间形成一种互相循环等待资源的关系
三、如何避免线程死锁?
1、控制加锁顺序(常用)
所有的线程都按照相同的顺序获得锁,需要事先知道所有可能会用到的锁
2、银行家算法
进程运行之前先声明对各种资源的最大需求量,当进程在执行中继续申请资源时,先测试进程已占用的资源数与本次申请的资源数之和是否超过该进程声明的最大需求量,若超过则拒绝分配资源,若未超过则再测试系统现存的资源能否满足该进城所需的最大资源量,若能满足则按当前的申请量分配资源,否则也要推迟分配。
四、怎么定位程序哪里发生了死锁?
1、使用 jps -l 定位进程号
2、使用 jstack 进程号 找到死锁问题
五、死锁接触的方法?
1、资源剥夺法:挂起某些死锁并抢夺它的资源,以便让其他进程继续推进
2、撤销进程法:强制撤销部分、甚至全部死锁进程并剥夺这些进程的资源
3、进程回退法:让一个或多个进程回退到足以回避死锁的地步
堆和栈的区别?
1)栈由系统自动分配,而堆是人为申请开辟;
2)栈获得的空间较小,而堆获得的空间较大;
3)栈由系统自动分配,速度较快,而堆一般速度比较慢;
4)栈是连续的空间,而堆是不连续的空间。
进程和线程的区别?
1、进程是一段程序,是资源分配的最小单元,线程是CPU调度的最小单元,一个进程中可以包含有多个线程
2、进程有独立的内存空间,多个线程共享相同的内存空间;线程有独立的栈,没有独立的堆,堆通常与进程相关,每一个线程有自己的栈,访问共同的堆
3、通信方式不同
进程间的通信方式及适用场景?
一、共享内存通信
共享内存是指多个进程共享一块内存,是专门用来解决不同进程之间的通信问题的,由于是直接对内存进行数据传输操作,所以是速度最快的IPC(inter-process communication)方式,因为是共享内存,所以需要配合信号量机制实现同步。
二、管道通信
1、无名管道
半双工,只能在具有亲缘关系的进程间使用。(进程的亲缘关系通常是指父子进程关系)
2、具名管道
半双工,允许无亲缘关系进程间的通信。
三、消息队列
消息队列是消息的链表,克服了信号传递消息少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
四、套接字通信
可用于不同机器间的进程通信。
什么是协程?
协程是一种比线程更加轻量级的存在,一个线程可以拥有多个协程。协程不是被操作系统内核所管理,而是完全由程序所控制(也就是在用户态执行),好处是性能得到了很大提升,不会像线程切换那样消耗资源。
JDK1.8后锁的几种状态?
无锁状态、偏向锁、轻量级锁、重量级锁
计算机网络
BIO、NIO、AIO的区别?
BIO:同步阻塞IO,一个请求对应一个线程,上下文切换占用的资源很重,无用的请求也会占用一个线程,没有数据到达也会阻塞。
改进:线程池机制
NIO:同步非阻塞IO,利用多路复用技术(select,poll,epoll),多个socket通道对应一个线程
AIO:一个有效请求一个线程,客户端的 I/O 请求都是由 OS 先完成了再通知服务器应用去启动线程进行处理
浏览器输入一个网址后发生了什么?
1、DNS解析:
浏览器会根据输入的URL去查找对应的IP,寻找的过程遵循就近原则,依次是:浏览器缓存 --> 操作系统缓存 --> 路由器缓存-->本地(ISP)域名服务器缓存 --> 根域名服务器
2、进行TCP连接
浏览器得到IP以后,向服务器发送TCP连接,TCP连接经过三次握手
3、浏览器发送HTTP请求
HTTP的请求方式为get,这个get请求包含了主机(Host)、用户代理(User-Agent),用户代理就是自己的浏览器,它是你的"代理人",Connection(连接属性)中的keep-alive表示浏览器告诉对方服务器在传输完现在请求的内容后不要断开连接,不断开的话下次继续连接速度就很快了。可能还会有Cookies,Cookies保存了用户的登陆信息,一般保存的是用户的JSESSIONID,在每次向服务器发送请求的时候会重复发送给服务器,服务器就知道是哪个浏览器。
4、服务器处理请求
服务器传回来响应头(包含状态码)和具体的要请求的页面内容
5、浏览器解析渲染页面
6、关闭TCP连接(四次挥手)
TCP三次握手
一、三次握手的过程?
1、客户端向服务端发送1个连接请求的报文段(SYN=1、seq=x)(客户端变成SYN-SENT)
什么是syn?
syn是TCP/IP建立连接时使用的握手信号,在客户端和服务端建立TCP网络连接时,客户端首先发送一个syn消息,服务端使用syn-ack应答表示接收到了这个消息,最后客户端再以ack消息响应,这样再客户端和服务器之间才能建立起可靠的TCP连接,数据才可以在客户端和服务器之间传递。
2、服务端收到请求连接报文段后,若同意建立连接,则向客户端发回连接确认的报文段(SYN=1、ACK=1、seq=y、ack=x+1)(服务器变成SYN-RECEIVED)
为什么要回传syn?
接收端传回发送端发送的syn是为了告诉发送端,我接收到的信息确实就是你所发送的信号了。
传了syn为啥还要传ack?
传了syn证明发送方到接收方的通道没有问题,但是接收方到发送方的通道还需要ack信号来进行验证。
3、客户端收到确认报文段后,向服务器再次发出连接确认报文段(ACK=1、seq=x+1、ack=y+1)(客户端和服务器变成ESTABLISHED)
二、为什么需要三次握手?
为了信息对等和防止出现请求超时导致脏连接。
1、信息对等就是确保两台机器都具备发报和收报的能力,也就是确保发送端到接收端和接收端到发送端的通道都没有问题。
2、为什么会出现脏连接?
网络报文的TTL往往都会超过TCP请求超时时间,如果两次握手就可以创建连接,那么传输数据并释放连接后,第一个超时的连接请求才到达B机器的话,B机器会以为是A创建新连接的请求,然后确认同意创建连接,引文A机器的状态不是SYN_SENT,所以直接丢弃了B的确认数据,以致最后只是B机器单方面创建连接完毕。三次握手就可以解决这个问题,因为需要A机器确认以后才真正建立了连接。
TCP四次挥手
一、四次挥手的过程?
1、客户端向服务器发送1个连接释放的报文段
2、服务器收到连接释放报文段后,向客户端返回连接释放确认报文段
3、若服务器已无要向客户端发送的数据,则发出释放连接的报文段
4、客户端收到连接释放报文段后,向服务器发回连接释放确认报文段
二、为什么需要四次挥手?
由于TCP的半关闭造成的,半关闭就是TCP提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。
第二次挥手后服务端进入CLOSE_WAIT(关闭等待)状态,此时的TCP处于半关闭状态,客户端到服务端的连接释放,此时客户端已无数据发送,而服务端还可以向客户端发送数据,所以第二次挥手不能发送FIN报文,只有当服务端也没有数据要发送了,才在第三次挥手时发送FIN报文,告诉客户端不再发送数据了。
三、为什么客户端关闭连接前要等待2MSL时间?
MSL(Maximum Segment Lifetime),为“报文最大生存时间”,是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。
1、保证客户端发送的最后一个ACK报文段能够到达服务端
这个ACK报文段有可能丢失,使服务端收不到确认,服务端超时重传FIN+ACK报文段,客户端能在2MSL时间内收到这个报文段,接着客户端重传一次确认,重新启动2MSL计时器,最后客户端和服务端都进入到CLOSED状态。
2、防止已失效的连接请求报文段出现在本连接中
客户端在发送完最后一个ACK报文段后,在经过2MSL就可以使本连接持续时间内所产生的所有报文段都从网络中 消失,使下一个新的连接中不会出现这种旧的连接请求报文段。
TCP和UDP的区别和应用场景
一、对比
UDP | TCP | |
---|---|---|
是否连接 | 无连接 | 面向连接 |
是否可靠 | 不可靠传输 | 可靠传输 |
流量控制 | 无 | 有(滑动窗口) |
拥塞控制 | 无 | 有(慢开始 拥塞避免 快重传 快恢复) |
连接对象个数 | 支持一对一,一对多,多对一和多对多交互通信 | 只能是一对一通信(全双工) |
传输方式 | 面向报文 | 面向字节流 |
首部开销 | 首部开销小,仅8字节 | 首部最小20字节,最大60字节 |
适用场景 | 适用于实时应用(IP电话、视频会议、直播等) | 适用于要求可靠传输的应用,例如文件传输 |
二、总结
1、TCP向上层提供面向连接的可靠服务 ,UDP向上层提供无连接不可靠服务。
2、虽然 UDP 并没有 TCP 传输来的准确,但是也能在很多实时性要求高的地方有所作为
3、对数据准确性要求高,速度可以相对较慢的,可以选用TCP
TCP怎么保证数据传输的可靠性?
1、校验和:将发送的数据段都当作一个16位的整数,相加,进位不丢弃补在后面,最后取反得到校验和
2、确认应答与序列号:TCP传输时对每个字节的数据都进行了编号,每次接收方收到数据后,都会对传输方进行确认应答(ACK),其中带有对应的确认序列号,告诉发送方接收到了哪些数据,下一次的数据从哪里发
3、超时重传:时间到达没有收到ACK进行超时重传
4、连接管理:三四握手+四次挥手
5、流量控制:滑动窗口
6、拥塞控制:慢开始、拥塞避免、快重传、快恢复
拥塞控制和流量控制的区别?TCP怎么实现拥塞控制的?
一、什么是拥塞?对资源的需求>可用资源时网络性能变坏。
二、拥塞控制和流量控制的区别?
拥塞控制:全局性过程,防止过多的数据注入网络中,使得网络中路由器或链路不过载。
流量控制:<stron>,抑制发送端发送数据的速率,便于接收端来得及接收。</stron>
三、拥塞控制方法:
主要有四种算法:慢开始、拥塞避免、快重传、快恢复
发送方维护一个拥塞窗口,先进行慢开始算法,一开始发送方发送一个字节,在收到接收方的确认,然后发送的字节数量增大一倍,按照指数逐步增大窗口大小,直到达到慢开始门限,然后使用拥塞控制算法,增长速率变为线性增长,直到出现超时,重新将窗口大小调整为1个字节,使用慢开始算法,同时慢开始门限调整为超时点的一半,达到门限后继续执行拥塞避免方法,如果收到3-ACK,可能是报文丢失,使用快重传算法发送缺失的报文段,同时执行快恢复算法将门限调整为此时窗口的一半,并执行拥塞避免算法。
TCP的滑动窗口解决了什么问题?滑动机制是什么?
滑动窗口解决的是流量控制的的问题,就是如果接收端和发送端对数据包的处理速度不同,如何让双方达成一致。接收端的缓存传输数据给应用层,但这个过程不一定是即时的,如果发送速度太快,会出现接收端数据overflow,流量控制解决的是这个问题。
TCP会话的双方都各自维护一个发送窗口
和一个接收窗口
。发送窗口只有收到发送窗口内字节的ACK确认,才会移动发送窗口的左边界。接收窗口只有在前面所有的段都确认的情况下才会移动左边界。当在前面还有字节未接收但收到后面字节的情况下,窗口不会移动,并不对后续字节确认。以此确保对端会对这些数据重传。发送方发的window size = 8192;就是接收端最多发送8192字节,这个8192一般就是发送方接收缓存的大小。
TCP和HTTP的队头阻塞?
一、TCP的队头阻塞
1、产生原因:一个TCP分节丢失,导致其后续分节不按序到达接收端的时候。该后续分节将被接收端一直保持直到丢失的第一个分节被发送端重传并到达接收端为止
2、解决方法:舍弃TCP协议
二、HTTP的队头阻塞
1、产生原因:HTTP管道化要求服务端必须按照请求发送的顺序返回响应,那如果一个响应返回延迟了,那么其后续的响应都会被延迟,直到队头的响应送达
2、解决方法:HTTP2实现了完全多路复用,只需一个连接即可实现并行
TCP的粘包问题及解决方法?
TCP粘包就是指发送方发送的若干包数据到达接收方时粘成了一包,从接收缓冲区来看,后一包数据的头紧接着前一包数据的尾
一、粘包问题产生的原因
1、当连续发送数据时,由于tcp协议的nagle算***将较小的内容拼接成大的内容,一次性发送到服务器端,因此造成粘包
2、当发送内容较大时,由于服务器端的recv(buffer_size)方法中的buffer_size较小,不能一次性完全接收全部内容,因此在下一次请求到达时,接收的内容依然是上一次没有完全接收完的内容,因此造成粘包现象
二、解决方法
1、对于第一种粘包产生方式可以在两次send()直接使用recv()阻止连续发送的情况发生
2、由于产生粘包的原因是接收方的无边界接收,因此发送端可以在发送数据之前向接收端告知发送内容的大小即可
DNS域名解析用的是什么协议?
同时使用TCP和UDP协议。
DNS在区域传输的时候使用TCP协议,其他时候使用UDP协议。
一、DNS区域传输的时候使用TCP协议:
DNS规范规定了两种DNS服务器:主DNS服务器和辅助DNS服务器,辅助域名服务器会定时向主域名服务器进行查询以便了解数据是否有变动,如有变动,会执行一次区域传送,进行数据同步。区域传送使用TCP而不是UDP,因为数据同步传送的数据量比一个请求应答的数据量要多得多。而TCP是一种可靠连接,保证了数据的准确性。
二、域名解析时使用UDP协议:
客户端向DNS服务器查询域名,一般返回的内容都不超过512字节,用UDP传输即可,不用经过三次握手,这样DNS服务器负载更低。
什么是socket?客户端和服务端的socket通信过程?
socket是传输层和应用层之间的一个抽象层,socket是一种打开--读/写--关闭模式的实现,服务器和客户端各自维护一个”文件“(Unix一切皆文件),在建立连接打开后,可以向文件写入内容供对方读取或者读取对方内容,通讯结束时关闭文件。
socket通信过程:
socket保证了不同计算机之间的通信,也就是网络通信。对于网站,通信模型是服务器与客户端之间的通信。两端都建立了一个Socket对象,然后通过Socket对象对数据进行传输。通常服务器处于一个无限循环,等待客户端的连接。
客户端过程:
创建socket,连接服务器,将socket与远程主机连接,发送数据,读取响应数据,直到数据交换完毕,关闭连接,结束TCP对话
服务端过程:
创建socket,与本机地址及端口进行绑定,然后通知TCP准备好接收连接,调用accept()阻塞,等待来自客户端的连接。如果客户端与服务器建立了连接,客户端发送数据请求,服务器接收请求并处理请求,然后把响应数据发送给客户端,客户端读取数据,直到数据交换完毕,最后关闭连接。
HTTP和HTTPS的区别?
1、最重要的区别是安全性,HTTP明文传输,不对数据进行加密安全性较差,HTTPS(HTTP+SSL/TLS)的数据传输过程是加密的,安全性较好
2、HTTP响应速度比HTTPS快,因为HTTPS多了一层安全层
3、HTTPS端口号443,HTTP端口号80
HTTPS的缺点:
- 相同环境下HTTPS的响应时间和需要的资源相比HTTP都大幅上升
- HTTPS的安全范围有限,在黑客攻击和服务器劫持等情况几乎起不到作用
SSL/TLS怎么加密?
(1) 客户端向服务器端索要并验证公钥。
(2) 双方协商生成"对话密钥"。
(3) 双方采用"对话密钥"进行加密通信。
HTTP1.0、1.1、2.0的区别?
HTTP1.X:线程阻塞,同一时间同一域名的请求有一定数量限制,超过限制数目的请求会被阻塞
HTTP1.0和HTTP1.1主要区别:HTTP1.0默认使用短连接,HTTP1.1默认使用长连接
HTTP2.0:
- 采用二进制格式而非文本格式
- 完全多路复用,而非有序并阻塞的、只需一个连接即可实现并行
- 使用报头压缩,降低开销
- 服务器推送
GET、POST的区别?
从幂等性、安全性、传参方式、URL长度限制等方面回答。
1、幂等性:GET用于无副作用(不改变服务器资源)、幂等的场景,POST用于副作用、不幂等的场景
2、传参方式:GET请求对应的参数放在URL中,而POST请求对应的参数放在请求体中
3、长度限制:浏览器会对URL最大字符长度做限制,导致GET请求的参数数量是有限的
4、安全性:GET请求的参数暴露在URL中,安全性没有POST高
Session、Cookie的区别?
Session是在服务端保存的一个数据结构,用来跟踪用户的状态
Cookie是在客户端保存用户信息的一种机制,也是实现Session的一种方式,
ping命令的原理?
PING是用于测试网络连接质量的程序。Ping发送一个ICMP(Internet Control Messages Protocol)即网络信报控制协议;
原理:利用网络上机器IP地址的唯一性,给目标IP地址发送一个数据包,再要求对方返回一个同样大小的数据包来确定两台网络机器是否连接相通,时延是多少。
如何实现负载均衡?有哪些算法?
一、实现负载均衡的几种技术
1、HTTP重定向协议实现负载均衡:根据用户的http请求计算出一个真实的web服务器地址,并将该web服务器地址写入http重定向响应中返回给浏览器,由浏览器重新进行访问。
2、DNS域名解析负载均衡:原理:在DNS服务器上配置多个域名对应IP的记录。例如一个域名www.baidu.com对应一组web服务器IP地址,域名解析时经过DNS服务器的算法将一个域名请求分配到合适的真实服务器上。
3、反向代理负载均衡:反向代理处于web服务器这边,反向代理服务器提供负载均衡的功能,同时管理一组web服务器,它根据负载均衡算法将请求的浏览器访问转发到不同的web服务器处理,处理结果经过反向服务器返回给浏览器。
4、IP负载均衡:在网络层通过修改目标地址进行负载均衡。
5、数据链路层负载均衡:在数据链路层修改Mac地址进行负载均衡。
二、常见的负载均衡算法
轮询法、随机法、加权轮询法、加权随机法
Linux
Linux的启动过程?
1、内核引导:开机自检,加载BIOS
2、运行init:init进程是所有进程的起点,没有这个进程,系统中任何进程都不会启动。根据运行级别确定要运行哪些程序
3、系统初始化:激活交换分区,检查磁盘,加载硬件模块
4、建立终端,以便用户登录系统
5、用户登录系统:命令行/ssh/图形界面登录
什么是僵尸进程?
僵尸进程是当子进程比父进程先结束,而父进程又没有回收子进程,释放子进程占用的资源,此时子进程将成为一个僵尸进程。如果父进程先退出,子进程被init接管,子进程推出后init会回收其占用的相关资源。
设计模式
单例
一个类在内存中只存在一个对象
步骤:1、构造函数私有化2、在类中创建一个本类对象3、提供一个方法可以获取到该对象
//饿汉式 class Single { private static final Single s = new Single(); private Single() {} public static Single getInstance() { return s; } }
//懒汉式:延时加载 class Single { private static Single s = null; private Single() {} public static Single getInstance() { if (s == null) { //Synchronized解决多线程安全问题 Synchronized(Single.class) { if (s == null) { //双重判断解决效率问题 s = new Single(); } } } return s; } }
高级知识
Redis
redis哨兵模式
1、什么是redis哨兵模式?
redis哨兵模式是redis的高可用架构的一种方式,它的出现是为了解决主从模式下主节点挂了不能自动故障转移的问题,哨兵架构图如下:
2、哨兵实现原理?(三个定时任务)(心跳机制+投票裁决)
问题一:sentinel没有配置从节点信息如何知道从节点信息的?
每隔10秒,sentinel向主节点发送info命令,用于发现新的slave节点
问题二、如何加入新的sentinel?
每隔2秒,向redis数据节点_ sentinel_:hello频道发送本sentinel节点的信息和对主节点的判断:这是进行对主节点进行客观下线和领导者选举的重要依据;也是发现新sentinel节点的重要依据
问题三:如何判断一个节点的需要主观下线的?
每隔1秒每个sentinel对其他的redis节点(master,slave,sentinel)执行ping操作,对于master来说,若超过down-after-milliseconds内没有回复,就对该节点进行主观下线并询问其他的Sentinel节点是否可以客观下线
3、哨兵的作用?
1)监控(Monitoring): Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。
2)提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。
3)自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作, 它会将失效主服务器的其中一个从服务器升级为新的主服务器, 并让失效主服务器的其他从服务器改为复制新的主服务器; 当客户端试图连接失效的主服务器时,集群也会向客户端返回新主服务器的地址,使得集群可以使用新主服务器代替失效服务器。
4、主管下线和客观下线?
主观下线:每隔1秒每个sentinel对其他的redis节点(master,slave,sentinel)执行ping操作,若master超过down-after-milliseconds内没有回复,就对该节点进行主观下线,每个sentinel节点对redis节点失败的“偏见”
客观下线:当sentinel主观下线的节点是主节点时,sentinel会通过命令sentinel is-master-down-by-addr来询问其sentinel对主节点的判断,如果超过quorum个数就认为主节点需要客观下线, 所有sentinel节点对redis节点失败达成共识
5、如何选举sentinel的leader?
使用raft协议(解决分布式系统一致性问题的协议)
1)每个做主观下线的sentinel节点向其他sentinel节点发送命令,要求将自己设置为leader
2)收到的sentinel可以同意或拒绝
3)如果该sentinel节点发现自己的票数已经超过了半数并且超过了quorum那么当选leader
4)如果此过程选举出了多个leader,那么将等待一段时间重新进行选举
6、sentinel的leader如何选举从节点成为主节点?
1)过滤故障节点
2)根据slave-priority优先级进行选择
3)选择复制偏移量大的从节点为主节点
4)选择runid最小的从成为主(说明重启的时间靠前)
7、redis集群中的某个节点挂了怎么办?
自动故障迁移
8、什么是redis的主从复制?
集群中的每个节点都有1-N个复制品,其中一个为主节点,其余的为从节点,如果主节点下线了,集群就会把这个主节点的一个从节点设置为新的主节点继续工作,这样集群就不会因为一个主节点的下线而无法正常工作
9、redis主从复制的方式?
全量同步和增量同步
10、如何解决主从架构的redis分布式锁主节点宕机锁丢失的问题?
使用redisson封装的redlock算法实现的分布式锁用法。
redlock算法思想:不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁,必须在大多数redis节点上都成功创建锁,才能算这个整体的RedLock加锁成功,避免仅仅在一个redis实例上加锁而带来的问题。
关键在于set的value要具有唯一性,redisson怎样保持value的一致性?
UUID+threadId
为什么使用redis?
1、速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)
2、支持丰富数据类型,支持string,list,set,sorted set,hash
3、支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行
4、丰富的特性:可用于缓存,消息,按key设置过期时间,过期后将会自动删除
redis为什么效率比较高?
1、redis是纯内存数据库,一般都是简单的存取操作,读取速度快
2、redis使用非阻塞IO,IO多路复用
3、redis采用单线程模型,保证每个操作的原子性,减少了线程的上下文切换和竞争
4、redis全程使用hash结构,读取速度快,再比如有序集合使用的跳表
除了redis还有什么分布式缓存数据库?
Memcached
Memcached是一个高性能的分布式内存对象缓存系统,用于动态Web应用以减轻数据库负载。Memcached通过在内存中缓存数据和对象来减少读取数据库的次数,从而提高动态、数据库驱动网站的速度。
特点:哈希方式存储;全内存操作;简单文本协议进行数据通信;只操作字符型数据;集群由应用进行控制,采用一致性哈希算法。
限制性:数据保存在内存当中的,一旦机器重启,数据会全部丢失;只能操作字符型数据,数据类型贫乏;以root权限运行,而且Memcached本身没有任何权限管理和认证功能,安全性不足;能存储的数据长度有限,最大键长250个字符,储存数据不能超过1M。
redis和mysql 的区别
1、类型上:
mysql是关系型数据库,redis是缓存数据库
2、作用上:
mysql用于持久化的存储数据到硬盘,功能强大,但是速度较慢
redis用于存储使用较为频繁的数据到缓存中,读取速度快
3、需求上:
mysql和redis因为需求的不同,一般都是配合使用
补充:
1、mysql支持sql查询,可以实现一些关联的查询以及统计;
2、redis对内存要求比较高,在有限的条件下不能把所有数据都放在redis;
3、mysql偏向于存数据,redis偏向于快速取数据,但redis查询复杂的表关系时不如mysql,所以可以把热门的数据放redis,mysql存基本数据
为什么用Redis不用Map?
1、Redis可以实现分布式的缓存,Map只存在创建它的程序里
2、Redis可以分配很大的内存来做缓存,而Map受到JVM的限制
3、Redis的缓存可以持久化,Map存在于内存中,重启后丢失
4、Redis可以处理每秒百万级的并发,而Map只是普通对象
5、Redis缓存有过期机制,而Map无此功能
6、Redis有丰富的API,Map简单太多了
Redis分布式锁的原理
1、加锁机制:某个客户端要加锁,如果该客户端面对的是一个redis cluster集群,首先会根据hash节点选择一台机器。紧接着,就会发送一段lua脚本到redis上,保证这段复杂业务逻辑执行的原子性。lua脚本中包含的参数有加锁的key,key的默认生存时间,加锁的客户端的ID。加锁的逻辑是SETNX,即不存在才加锁。
2、锁互斥机制:客户端2来尝试加锁时会发现锁已存在并且客户端ID不是自己的,此时会进入一个while循环,不停的尝试加锁。
3、watch dog自动延期机制:客户端1一旦加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端1还持有锁key,那么就会不断的延长锁key的生存时间。
4、可重入加锁机制:如果客户端1已经持有锁了还进行可重入的加锁那么就会对客户端1的加锁次数累加1。
5、释放锁机制:执行lock.unlock()就可以释放分布式锁,每次对加锁次数减1,当加锁次数为0时从redis里删除这个key,然后另外的客户端2就可以尝试完成加锁了。
Redis分布式锁的缺点?
redis cluster,或者是redis master-slave架构的主从异步复制导致的redis分布式锁的最大缺陷:在redis master实例宕机的时候,可能导致多个客户端同时完成加锁。
redis中的哈希一致性算法?
在面对redis集群时需要选择一个节点进行缓存,普通的hash算法是取模:hash(IP地址或文件名)%服务器数量,但这种方式在服务器数量变动的时候,所有缓存的位置都要发生改变。因此诞生了Hash一致性算法,区别是对2^ 32-1取模,一致性Hash算法将整个Hash值控件组织成一个虚拟的圆环,将数据key使用相同的函数Hash计算出哈希值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,第一台遇到的服务器就是其应该定位到的服务器。为解决数据倾斜问题还引入了虚拟节点机制,具有较好的容错性和可扩展性。
redis有序集合使用的跳表是什么?
redis有五种类型的对象:字符串、列表、哈希、集合、有序集合。
redis中有序集合的底层采用跳表作为数据结构,有序链表只能逐一查询,导致操作起来非常慢,跳表在此基础上增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位。
跳表的查找复杂度是O(logN)。
RabbitMQ
为什么选用RabbitMQ?
1、基于AMQP协议2、高并发3、高性能4、高可用5、支持多语言
Rabbit的相关概念及工作流程?
一、工作流程
生产者和消费者收发消息都需要与RabbitMQ建立一条长连接,所有的收发消息都在连接中开辟信道进行收发,消息有消息头和消息体,消息头是一些参数设置,消息体是真正的消息内容,最重要的一个消息头是路由键,决定了消息要发给谁。消息首先来到消息代理指定的虚拟主机所指定的交换机,交换机收到消息后根据消息的路由键通过交换机与其他队列的绑定关系来决定将消息发给哪个队列,消费者会监听这些队列并拿到消息。
二、交换机(Exchange)类型
direct、fanout(扇出)、topic、headers(几乎不用)
direct是点对点式、fanout和topic是发布订阅式
各种Exchange比较:
- direct实现路由键的精确匹配
- fanout采用广播的形式,不处理路由键,将消息发给所有绑定的队列
- topic实现路由键的模糊匹配,可以有通配符
三、RabbitMQ消息确认机制--可靠抵达
为保证消息不丢失,可靠抵达,可以使用事务消息,但性能下降250倍,为此引入确认机制
发送端确认:
1、ConfirmCallback:消息只要被broker接收到就会执行ConfirmCallback
2、ReturnCallback:消息未能投递到目标queue里将调用ReturnCallback
消费端确认(保证每个消息被正确消费才可以在队列中删除):
3、ack:分为自动(默认)和手动;
1)默认是自动确认的,只有消息接收到,客户端会自动确认,服务端就会移除这个消息
问题:收到很多条消息,自动回复给服务器ack,但只有一个消息处理成功后就宕机了,发生消息丢失,因此要有手动模式
消费者手动确认模式:只要没有明确告诉MQ货物被签收(没有ack),消息就一直是unacked状态,即使consumer宕机,消息也不会丢失,会重新变为ready状态,待有新的consumer连接进来再发送
2)如何签收
- basic.ack用于肯定确认,broker将移除此消息
- basic.nack用于否定确认,可以指定broker丢弃此消息或重新入队,可以批量
- basic.reject用于否定确认,同上,但不能批量
死信队列是什么?作用?
死信队列:DLX,Dead-Letter-Exchange,利用DLX,当消息在一个队列中变成死信(dead message,就是没有任何消费者消费)之后,他能被重新publish到另一个Exchange,这个Exchange就是DLX。
消息变为死信的几种情况:1)消息被拒绝同时不重回队列2)TTL过期3)队列达到最大长度
DLX也是一个正常的Exchange,和一般的Exchange没有任何的区别,他能在任何的队列上被指定,实际上就是设置某个队列的属性。 当这个队列出现死信的时候,RabbitMQ就会自动将这条消息重新发布到Exchange上去,进而被路由到另一个队列,可以监听这个队列中的消息作相应的处理。
用过RPC框架吗?
用到了基于http的feign,SpringCloud对feign已经有了比较好的封装,还了解基于私有tcp协议的dubbo(阿里的)
如果不使用rpc框架,那么调用服务走http需要配置请求head、body,然后才能发起请求。获得响应体后,还需解析等操作,十分繁琐。Feign是一个http请求调用的轻量级框架,以Java接口注解的方式调用Http请求。Feign通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,封装了http调用流程。
docker
一、docker是什么?
开源的应用容器引擎,让开发者打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何linux机器上,也可以实现虚拟化
关键:应用环境隔离;封装好的开发环境直接部署;集装箱思想
二、docker的进程隔离机制?
Linux Namespace 是操作系统内核在不同进程间实现的一种「环境隔离机制」。
举例来说:现在有两个进程A,B。他们处于两个不同的 PID Namespace 下:ns1, ns2。在ns1下,A 进程的 PID 可以被设置为1,在 ns2 下,B 进程的 PID 也可以设置为1。但是它们两个并不会冲突,因为 Linux PID Namespace 对 PID 这个资源在进程 A,B 之间做了隔离。A 进程在 ns1下是不知道 B 进程在 ns2 下面的 PID 的。
Linux 一共构建了 6 种不同的 Namespace,用于不同场景下的隔离:
Namespace | 系统调用参数 | 隔离内容 |
---|---|---|
UTS | CLONE_NEWUTS | 主机名与域名 |
IPC | CLONE_NEWIPC | 信号量、消息队列和共享内存 |
PID | CLONE_NEWPID | 进程编号 |
Network | CLONE_NEWNET | 网络设备、网络栈、端口等等 |
Mount | CLONE_NEWNS | 挂载点(文件系统) |
User | CLONE_NEWUSER | 用户和用户组 |
三、docker和虚拟机有什么区别?
docker是一种轻量级的虚拟机,比虚拟机更节省内存,启动更快。
Docker守护进程可以直接与主操作系统进行通信,为各个Docker容器分配资源;它还可以将容器与主操作系统隔离,并将各个容器互相隔离。虚拟机启动需要数分钟,而Docker容器可以在数毫秒内启动。由于没有臃肿的从操作系统,Docker可以节省大量的磁盘空间以及其他系统资源。
虚拟机更擅长于彻底隔离整个运行环境。例如,云服务提供商通常采用虚拟机技术隔离不同的用户。
Docker通常用于隔离不同的应用,例如前端,后端以及数据库。
git
git产生冲突的情况及解决方法?
一、产生冲突的情况
github上创建一个仓库,本地两个文件夹分别clone同一仓库,分别叫cangkuA、仓库B,在仓库A修改文件,提交到远端,在仓库B修改同一文件的同一行,同样保存修改后尝试提交到远端,会发生冲突。
二、解决方法(合并分支)
1、rebase:保留原分支上的每个commit
2、merge:只会生成一个commit
maven
maven的生命周期?
Maven的生命周期就是对所有的构建过程进行抽象和统一。包含了项目的清理、初始化、编译、测试、打包、集成测试、验证、部署和站点生成等几乎所有的构建步骤。
Maven的生命周期是抽象的,即生命周期不做任何实际的工作,实际任务由插件完成,类似于设计模式中的模板方法。
Maven有三套相互独立的生命周期,分别是clean、default和site。每个生命周期包含一些阶段(phase),阶段是有顺序的,后面的阶段依赖于前面的阶段。
1、Clean 生命周期:清理项目,包含三个 phase :
- pre-clean:执行清理前需要完成的工作。
- clean:清理上一次构建生成的文件。
- post-clean:执行清理后需要完成的工作
2、Default 生命周期:构建项目,重要的 phase 如下:
- validate:验证工程是否正确,所有需要的资源是否可用。
- compile:编译项目的源代码。
- test:使用合适的单元测试框架来测试已编译的源代码。这些测试不需要已打包和布署。
- package:把已编译的代码打包成可发布的格式,比如 jar、war 等。
- integration-test:如有需要,将包处理和发布到一个能够进行集成测试的环境。
- verify:运行所有检查,验证包是否有效且达到质量标准。
- install:把包安装到maven本地仓库,可以被其他工程作为依赖来使用。
- deploy:在集成或者发布环境下执行,将最终版本的包拷贝到远程的repository,使得其他的开发者或者工程可以共享。
3、Site 生命周期:建立和发布项目站点,phase 如下:
- pre-site:生成项目站点之前需要完成的工作
- site:生成项目站点文档
- post-site:生成项目站点之后需要完成的工作
- site-deploy:将项目站点发布到服务器
maven的scope属性有哪些?
- compile (编译范围): compile是默认的范围;如果没有提供一个范围,那该依赖的范围就是编译范围。编译范围依赖在所有的classpath 中可用,同时它们也会被打包。
- provided (已提供范围):类似compile,期望JDK、容器或使用者会提供这个依赖。如servlet.jar。
- runtime (运行时范围):只在运行时使用,如JDBC驱动,适用运行和测试阶段。
- test (测试范围):test范围依赖 在一般的编译和运行时都不需要,它们只有在测试编译和测试运行阶段可用。
- system (系统范围):(注意该范围是不推荐使用的)与provided 类似,但是你必须显式的提供一个对于本地系统中JAR 文件的路径。这么做是为了允许基于本地对象编译,而这些对象是系统类库的一部分。这样的构件应该是一直可用的,Maven 也不会在仓库中去寻找它。如果你将一个依赖范围设置成系统范围,你必须同时提供一个 systemPath 元素。
开放性问题
微博、朋友圈是怎么实现的?
这些产品都是Feed流类型产品,一般都是按照时间“从上往下流动”,Feed其实是一个信息单元,比如一条朋友圈状态、一条微博,只要关注某些发布者就能获取到源源不断的新鲜信息。
Feed流系统特点
Feed流本质上是一个数据流,是将“N个发布者的信息单元”通过“关注关系”传送给“M个接收者”。
从数据层面看,数据分为三类,分别是:
- 发布者的数据:微博的个人页面,朋友圈的个人相册
- 关注关系:微博中是关注,是单向流,朋友圈是好友,是双向流,但信息流动永远是单向
- 接收者的数据:按某种顺序(一般是时间)组织在一起,越新的数据越要排在前面
设计Feed流系统时最核心的是确定清楚产品层面的定义,需要考虑的因素包括:
- 产品用户规模:用户规模在十万、千万、十亿级时,设计难度和侧重点会不同
- 关注关系(单向、双写):如果是双向,那么就不会有大V,否则会有大V存在
Feed流系统设计
1、产品定义:关注关系是单向还是双向,排序是时间还是推荐
2、存储:数据可靠不丢失,易于水平扩展,在数据规模很大时可以采用分布式的NoSQL(可靠性高于关系型数据库,数据天然分布在多台机器上)
3、同步:
- 推模式(写扩散):发送者发送了一个消息后立即将这个消息推送给接收者,但是接收者此时不一定在线,需要在同步库存储这个数据
- 拉模式(读扩散):发送者发送一条消息后将消息写入自己的发件箱,粉丝上i西安后再去自己关注着的发件箱里面去读取。
- 推拉结合模式
4、元数据:
- 用户详情和列表:分布式NoSQL或关系型数据库都可
- 关注或好友关系:根据数据量来选择数据库类型
- 推送session池:分布式NoSQL或关系型数据库都可
5、评论、赞、搜索...
电脑版微信登录的二维码包含什么信息?
了解OAuth2.0原理,移动端微信已经登录,获得access_token,
再与扫描的二维码中的字符串进行组合编码,发送给服务器,
服务器给浏览器网页的监听器发送消息,网页解析消息,并得到access_token,实现登录。
二维码只是充当一个掩码作用...
如何计算两份代码的相似度?
diff逐行/逐字比较
重构代码时要考虑哪些方面?(面向对象角度)
面向接口编程、单一职责:一个类只做一件事、高内聚低耦合
全部评论
(7) 回帖