一.java基础
(一).java反射
1.问:java反射的优缺点(24/02/20)
答:
java反射的优点:
增加程序的灵活性,可以在运行的过程中动态的对类进行修改和操作;
提高代码的复用率,比如动态代理,就是用反射实现的
可以在程序运行的过程中获取任意一个类的方法,属性,还可以通过反射进行动态调用;
java反射的缺点:
反射会涉及到动态的类型解析,所以JVM无法对这些代码进行优化,导致性能会比非反射调用更低;
使用反射后,代码的可读性可能会降低
反射可以绕过一些限制访问的属性或是方法,可能会破坏代码本身的抽象性
(二).集合
--来源:黑马程序员
1.问:数组的索引为什么从0开始,加入从1开始不可以吗(24/02/22)
答:
根据索引获取元素的时候,是通过索引和寻址公式来计算内存所对应的元素数据,选址公式是:数组的首地址+索引乘存储数据的类型大小,如果索引从1开始,寻址公式中,就需要增加一次减法操作,对应CPU来说就多了一条指令,性能不好;
2.问:ArrayList的底层实现原理(24/02/22)
答:
1.ArrayList的底层是用动态数组实现的
2.ArrayList的初始容量为0,第一次添加元素会扩容为10
3.ArrayList每次扩容会扩容为原来的1.5倍,每次扩容需要进行数组的拷贝
4.ArrayList在添加元素的时候,会确保数组的容量充足,不充足就会调用grow进行扩容为原来的1.5倍,当然第一次会扩容为10,成功添加新元素后会方法true;
(
ArrayList list=new ArrayList(10)中的list扩容了几次? 0次,有参的构造函数中是直接new 了一个大小为10的数组,没有进行扩容
)
3.问:数组和List之间的转换(24/02/22)
答:
数组转List: Arrays.asList;List转数组:toArray方法 ;
(
Arrays.asList转数组后,修改数组内容list受影响吗?
受影响,因为底层使用的是Arrays类中的一个内部类ArrayList的构造方法来构造的集合,在这个类的构造器中,只是把我们传入的集合进行了包装而已,它们指向的同一块内存地址
list用toArray转集合后,如果修改了list的内容,数组受影响吗?
不受影响,toArray中采用的是数组的拷贝,两者不互相影响
)
4.问:ArrayList和linkedList的区别?(24/02/22)
1.底层的数据结构
ArrayList是动态数组实现的,LinkedList是双向链表实现的
2.增删的效率
ArrayList增删慢,LinkList增删快;
ArrayList底层采用的动态数组,当插入节点时,就需要把插入位置以及之后的所有元素向后移动;当删除节点时,就需要把删除位置之后的所有节点向前移动;
LinkList底层采用的是双向链表,插入或是删除节点,十分迅速;
3.查询效率
ArrayList查询快,LinkList查询慢;
ArrayList底层数据是有数组存储的,因此支持通过下标随机范围元素,查询效率迅速;
LinkList底层采用链表,不支持通过下标查询元素,查询元素需要对链表进行遍历,查询效率缓慢;
4.内存的空间占用
ArrayList底层是数组,内存连续,节省内存
LinkedList底层双向链表,有头指针,尾指针,更占用内存
5.线程安全
ArrayList和LinkedList都不是线程安全的
如果需要保证线程安全,有两种方案:
(1)在方法的内部使用,局部变量是线程安全的
(2)使用Collections.synchronizedList方法得到线程安全的List对象
5.问:HashMap实现原理(24/02/22)
答:
HashMap的数据结构:底层使用hash表数据结构,即数组和链表或是红黑树
当向hashMap中put元素的时候,利用key的hashCode重新hash计算出当前元素在应该在数组的下标,存储时如果遇到hash冲突,就有两种情况:key值相同,覆盖原始值;key值不同,就会把key-value放入链表或是红黑树中
获取时,直接找到hash值对应的下标,再进一步去判断key是否相同,从而找到对应的值
6.问: HashMap的jdk1.7和jdk1.8的区别(24/02/22)
答:
在jdk1.8之前采用的是拉链法,拉链法:就是数组和链表相结合,也就是说创建了一个链表数组,数组的每一格就是一个链表,如果遇到哈希冲突,就会把冲突的值加到链表中
jdk1.8在解决这个哈希冲突时有了较大的变化,当链表的长度大于阈值8的时候且数组的长度达到64时,就会把链表转为红黑树,以减少搜索时间,扩容时,会把节点数小于等于6的红黑树退化为链表
7.问:HashMap的put方法的具体流程(24/02/22)
答:
1.判断键值数组table是否为空或是null,如果是的话就会执行resize()执行扩容(初始化)
2.根据键值key计算hash值得到数组索引i
3.判断table[i]是否等于null如果是的话,就会直接新建节点并添加
4.如果不是,判断table[i]的首个元素和key是否相同,如果相同就直接覆盖原始值,否则再判断当前table[i]是否为红黑树节点,如果是,在红黑树中插入键值对;如果不是,就遍历链表,如果找到key值相同的就覆盖原始值,否则插入到链表尾部;
5.判断链表的长度是否大于8,大于8并且HashMap的长度达到64就会把链表转为红黑树
8.问:HashMap的扩容机制(24/02/22)
在初始化或是元素个数得到了扩容阈值时就会调用resize方法进行扩容;初始化数组长度为16,后续扩容就会扩容为原来的两倍;
扩容的之后,就会创建一个新的数组,让后把老数组中的数据挪动到新数组中
对于没有hash冲突的节点,就直接通过节点的哈希值和新数组的容量-1做与运算(e.hash&(newCap -1))计算得到节点新的索引位置
如果是红黑树,就会走红黑树的添加
如果是链表,那么就需要遍历链表,可能需要拆分链表,判断节点的哈希值和老数组容量做与运算(e.hash&oldCap)的结果是否为0,如果是,那么该节点留在原始的索引位置,否则就需要移动到原始索引+老数组长度的位置
对于扩容后如果红黑树的节点数小于等于6那么会退化为链表
9.问:hashMap的寻址算法(24/02/22)
答:
调用对象的hashCode()获取hash值再调用HashMap的hash方法进行二次哈希,hash方法中就是对hashcode的值和hashcode的值逻辑右移了16位,让哈希更加的均匀;
10.问:为什么HashMap的数组长度一定为2的次幂(24/02/22)
答:
计算索引的效率更高,如果是2的次幂就可以通过与运算代替取模
扩容时重新计算索引的效率更高,如果哈希值和旧数组容量做与运算 (hash&oldCap)等于0那么元素就会在原来的索引位置,否则就在原来的索引加上旧数组容量的位置;
11.问:hashMap在jdk1.7下的多线程死循环问题(24/02/22)
答:
在jdk1.7中hashmap扩容的时候,由于链表采用的是头插法,在数据迁移的时候就有可能会导致死循环的产生
比如:现在有两个线程,在线程1对hashmap进行扩容的时候另个一线程2介入,也对hashmap进行扩容,由于采用的是头插法,所以链表的顺序会颠倒过来,比如开始的顺序为AB,扩容后就变成了BA;线程2执行结束,然后执行线程1,就有可能会出现死循环问题;线程1把A放入新的数组,在用头插法把B插入A的首部,由于线程2执行之后使B.next=A,所以又会把A插入B的首部,就形成了A->B->A这样的死循环;
在jdk1.8就对扩容算法进行了调整,链表采用了尾插法,就避免了jdk1.7的死循环问题
(三).juc
1.问: 线程和进程的区别(24/02/22)
答:
进程是正在运行的程序的实例,进程中包含了线程,每一个线程执行不同的任务
不同的进程使用不用的内存空间,而同一个进程下的所有线程共享内存空间
线程更加的轻量,线程的上下文切换成本比进程的上下文切换成本低
2.问:并行和并发的区别(24/02/22)
答:
现在都是多核CPU,在多核CPU下
并行指的是在同一时间做多件事的能力,例如4个CPU同时执行4个线程
并发是同一时间应对多件事的能力,多个线程轮流使用一个或是多个CPU
3.问:创建线程的方式(24/02/22)
答:
继承Thread类
实现Runnable接口
实现Callable接口
线程池创建线程
4.问:runnable和callable有什么区别(24/02/22)
runnable接口的run方法 是没有返回值的;callable接口的call方法是有返回值的,是一个泛型,和Future和FutureTask配合使用获取异步执行的结果
callable接口的call方法允许抛出异常,而runable接口的run方法的异常只能在内部处理,不能向上抛
5.问:run方法和start方法的区别(24/02/22)
答:
run方法封装了要被执行的业务逻辑,可以被多次调用;start方法是用来启动线程,该线程会调用run方法,执行内部的逻辑,start方法只能被调用一次
6.问:线程的状态(24/02/22)
答:
新建(NEW);
可运行(RUNNABLE);
阻塞(BLOCKED);
等待(WAITING);
计时等待(TIME_WAITING);
终止(TERMINATED)
7.问:线程之间的状态如何变化的(24/02/22)
答:
创建线程对象就是新建状态
调用线程对象的start方法就会变为可运行状态
线程获取CPU的执行权并执行结束后就变成了终止状态
在可执行状态时;
获取锁,如果没有获取成功就会进入阻塞状态,获取锁后会切换会可运行状态
线程调用了wait()就会进入等待状态,等其他线程调用notify()或是notifyAll()唤醒后就会切换为可运行状态
如果线程调用了sleep(n)就会进入计时等待状态;等时间到了之后就会切换为可运行状态
8.问:新建T1,T2,T3三个线程,如何让他们按照顺序执行(24/02/22)
答:
使用join方法
使用LockSupport的part,unpart方法
。。。
9.问:notify()和notifyAll()的区别(24/02/22)
答:
notify():只随机唤醒一个wait线程
notifyAll(): 唤醒所有wait线程
10.问:java中wait()和sleep()的区别(24/02/22)
答:
共同点:wait(),wait(long),sleep(long)的效果都是让CPU暂时放弃CPU的使用权,进入阻塞状态
不同点:
方法的归属不同: sleep(long)方法是Thread类的静态方法;wait(),wait(long)是Object的成员方法
醒来的时机不同:执行sleep(long),wait(long)的线程都会等待指定的毫秒后醒来;wait(long),wait()还可以被notify(),notifyAll()唤醒;wait如果不唤醒就会一直等下去;他们都可以被打断唤醒
锁的特性不同:wait方法的调用必须先获取对象的锁,而sleep方法没有这个限制;wait方法执行后会释放掉对象锁,允许其他线程获取锁;而sleep方法在syncronized代码块中执行后,并不会释放对象的锁;
11.问:如何停止一个正在运行的线程(24/02/22)
答:
使用退出标记,使线程正常退出;
使用stop方法强行终止(不推荐,可能会导致一些锁无法释放)
使用interrupt方法打断线程
打断阻塞的线程(sleep,wait,join)的线程,线程会抛出interruptException异常,
打断正常线程,可以根据打断状态来标记是否退出线程
12.问:synchronized关键字的底层原理(24/02/22)
答:
synchronized是采用互斥的方式使同一时刻最多只有一个线程只有对象锁
它的底层是采用monitor实现的,monitor是jvm级别的由C++编写的,线程获取锁需要使用对象关联monitor
在monitor内部有三个属性,owner,entrylist,waitset,其中owner是关联的获取锁的线程,并且只能关联一个线程;entrylist关联的是获取锁失败进入阻塞的线程;waitset关联的是获取锁后调用了wait方法进入等待状态的线程
(
在jdk1.6之后引入了两种新的锁机制,偏向锁和轻量级锁,他们的引入是为了解决在没有多线程竞争或是基本上没有竞争的场景下使用重量级锁带来的性能开销问题
synchronized有偏向锁和轻量级锁,重量级锁三种形式,分布对应了锁只被一个线程持有,不同的线程交替持有,多线程锁竞争的情况;
重量级锁 | 底层使用的是Monitor,涉及到用户空间和内核空间的切换,进程的上下文切换,成本较高 |
轻量级锁 | 线程加锁的时间是错开的,可以使用轻量级锁来进行优化,轻量级锁修改对象头的锁标记,相对重量级锁来说性能提升很多,每次修改都是CAS操作,保证 原子性 |
偏向锁 | 在很长一段时间都只被一个线程使用锁,就可以使用偏向锁,只有在第一次获取锁的时候,会有一个CAS操作在,之后再次获取锁,就只需要判断对象头的Mark Word是否是自己的线程id即可,不用进行开销相对较大的CAS操作 |
)
13.问:谈谈JMM(java内存模型)(24/02/22)
答:
JMM java内存模型,定义了共享内存中多线程程序的读写操作的行为规范,通过规定规范对内存的操作从而保证指令的正确性
JMM把内存分为了两块,一块是私有线程的工作区域,工作内存,一块是所有线程共享的区域,主内存
线程与线程之间相互隔离,线程根线程交互需要经过主内存
14.问:CAS(24/02/23)
答:
CAS:Compare And Swap (比较在交换),它体现的是乐观锁的思想,能在无锁的状态下保证线程操作数据的原子性
CAS使用的地方很多:如AQS框架,AtomicXXX类(原子类)
在操作共享变量的时候使用的是自旋锁,效率高一点,
CAS底层依赖于Unsafe类来直接调用操作系统底层的CAS指令
15.问:对volatile的理解(24/02/23)
答:
1.保证线程间的可见性:用volatile修饰的共享变量,能够防止编译器等优化的发送,保证一个线程对共享变量的修改对另一个线程可见
2.禁止进行指令重排序:用volatile修饰的共享变量,在共享变量的读写操作时加入屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果
(
写操作会加写屏障到volatile修饰变量的上方,阻止上方的其他写操作越过屏障,确保在写操作之前的,对共享变量的修改都会被同步到主存中
读操作会加读屏障到volatile修饰变量的下方,阻止下方的其他读操作越过屏障,确保在读操作之后的,对共享变量的读取都是加载的主存中最新的
)
16.问:AQS(24/02/23)
答:
AQS AbstractQueuedSychronizer ,抽象队列同步器,它是构建锁或是其他同步组件的此处框架,向ReentrantLock,Semaphore,CountDownLatch都是基于AQS实现的
AQS内部维护了一个先进先出的双向链表,队列中存储的是排队的线程;在AQS内部还有一个state属性,默认是0,如果某个线程成功的将state修改为了1,那么就相当于该线程成功的获取了锁;对state进行修改的时候使用的是cas操作,保证多个线程修改时的原子性
17.问:ReentrantLock的实现原理(24/02/23)
答:
ReentrantLock表示支持可重入的锁,调用lock方法获取锁之后之后,还可以再次调用lock获取锁;ReentrantLock是基于CAS+AQS队列来实现的;支持公平锁和非公平锁,在提供的构造器中无参构造器默认是非公平锁,也可以设置参数为true设置为公平锁
(线程抢锁时使用cas的方式修改state属性,如果成功修改为1,说明抢锁成功,让exclusiveOwnerThread属性指向当前线程;如果修改失败,则会进入先进先出的双向队列等待;当exclusiveOwnerThread为null的时候,就会唤醒在双向队列中等待的线程;公平锁体现在,如果队列中有线程那么,就会进入队列让队列中的线程获取锁;非公平锁体现在,没有进入队列的线程也可以抢锁)
18.问:synchronized和Lock的区别(24/02/23)
答:
语法层面:
synchronized是关键字,源码在jvm中是由c++实现的;
Lock是接口,源码有jdk提供,用java实现的
使用synchronized时,退出同步代码块会自动释放锁,而使用Lock时,需要手动调用unlock方法释放锁
功能层面:
二者都是悲观锁,都有基本的互斥,同步,可重入的功能
Lock提供了需要synchronized不具备的功能,比如:可打断,可设置为公平锁,可有多个条件变量;
Lock有许多不同场景的实现,比如ReentrantLock,ReentrantReadWriteLock(读写锁)
性能层面:
jdk1.6时synchronized引入了偏向锁,轻量级锁,没有竞争的时候性能不错;但是在竞争激烈的时候,Lock的实现通常能提供更好的性能
19.问:死锁产生的条件(24/02/23)
答:
死锁通常是两个或两个以上的线程互相等待彼此持有的资源导致,所有线程都是无法执行;产生死锁的四个必要条件:1.互斥,2.请求与保持,3.不可剥夺,4.循环等待;java中通常出现在一个线程需要同时获取多把锁的情况
20.问:如何诊断死锁(24/02/23)
答:
当程序出现了死锁的时候,我们可以使用jdk自带的工具,jps和jstack;
使用jsp查看JVM中运行的进程的进程信息,获取它的进程id,在通过jstack 使用jstack -l 进程id指令去查看进程的堆栈信息,查看日志中是否有死锁;
或是通过可视化工具jconsole,VisualVm来检查死锁
21.问:ConcurrentHashMap(24/02/23)
答:
底层的数据结构:
jdk1.7底层采用的是分段的数组+链表实现的
jdk1.8采用的是数组+链表/红黑树,和HashMap1.8一样
加锁的方式:
jdk1.7采用的是segment分段锁,底层使用的是ReentrantLock
jdk1.8在添加节点的时候使用的CAS自旋锁,采用synchronized锁定链表或红黑树的首节点,相比segment分段锁粒度更细,性能更优
22.问:导致并发程序出现问题的根本原因(java程序中如何保证多线程的执行安全)(24/02/23)
答:
1.原子性 synchronized lock
2.内存可见性 volatile synchronized lock
3.有序性 volatile
23.问:线程的核心参数(线程池的执行原理)(24/02/23)
答:
corePoolSize:核心线程数
maximumPoolSize:最大线程数 =(核心线程数+救急线程的最大数目)
keepAliveTime:生存时间-救急线程的生存时间,生存时间到了没有新的任务就会释放线程资源
unit 时间单位-救急线程的生存时间单位
workQueue 工作队列-当没有空闲的核心线程时,新来的任务就会加入到这个队列中进行排队,队列慢就会创建救急线程执行任务
threadFactroy 线程工厂-可以定制线程对象的创建,例如:线程的名字,是否为线程对象
handler 拒绝策略-当所有的线程都在忙的时候,workQueue也满了,就会触发拒绝策略
24.问:线程池中常见的阻塞队列(24/02/23)
答:
1.ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO(先进先出)
2.LinkedBlockingQueue:基于链表结构的阻塞队列,FIFO
3.DelayedWorkQueue:是一个优先级队列,它可以保证每次出队出队的任务都是当前队列中执行时间最靠前的
4.SynchronourQueue:不存储元素的阻塞队列,每个插入操作就需要等待一个移除操作
ArrayBlockingQueue | LinkedBlockingQueue |
强制有界 | 默认无界,支持有界 |
底层是数组 | 底层是单向链表 |
提前初始化好Node数组 | 是懒惰的,创建节点的时候添加数据 |
Node需要提前创建好 | 入队会生成新的Node |
一把锁 | 两把锁(头尾) |
25.问:如何确定核心线程数(24/02/23)
答:
(
IO密集型任务:一般来说 文件读写,DB读写,网络请求;
CPU密集任务:一般来说 计算型代码,Bitmap转换,Gson转换
)
高并发,任务执行时间短->cpu核数+1
并发不高,任务执行时间长->
io密集型任务->cpu核数*2+1
cpu密集型任务->cpu核数+1
并发高,任务执行时间长,解决这种类型的任务的关键不在意线程池,而在于整体的架构设计,看这些业务能不能做缓存或是添加服务器,至于线程值的设计,可以依旧
io密集型任务->cpu核数*2+1
cpu密集型任务->cpu核数+1
26.问:线程池的种类有哪些(24/02/23)
答:
1.newFiexedThreadPool:创建一个定长的线程池,可以控制线程的最大并发数,超出的任务会在队列中等待
核心线程数和最大线程数一样,没有救急线程;采用的是LinkedBlockingQueue,最大的容量为Integer.MAX_VALUE;
适用于任务数已知,相对耗时的任务
2.newCachedTheadPool:创建一个可缓存的线程池,如果线程池的长度超过处理需要就可以灵活的回收空闲线程,若无可回收,则新建线程
核心线程数为0,最大的线程数为integer.MAX_VALUE,阻塞队列为SynchronousQueue:不存储元素的阻塞队列,每一个插入操作就需要等待一个移除操作
适用于任务较密集,但是每个任务的执行时间短的情况
3.newScheduleThreadPool:可以执行延迟任务的线程池,支持定时及周期的任务执行
4.newSingleThreadExecutor:创建一个单线程的线程池,它只会用唯一的工作线程来执行任务,保证所有任务是按照顺序(FIFO)执行的
27.问:为什么不建议使用Executors创建线程池(24/02/23)
答:
Executors返回的线程池对象的弊端:
newFixedThreadPool和newSingThreadExecutor:队列长度为Integer.MAX_VALUE,可能会导致大量的任务堆积,导致OOM;
CachedThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM;
28.问:线程池的使用场景(24/02/23)
答:
批量导入:使用线程池+CountDownLatch批量把数据库的数据导入ES中,避免OOM
数据汇总:调用多个接口来汇总数据,如果所有接口或部分接口没有依赖关系,就可以线程池和+future来提升性能
异步线程:使用异步线程调用其他方法,提升方法的响应时间
29.问:如何控制某个方法允许访问的线程数(24/02/23)
答:
可以使用多线程中提供的一个工具类Semaphore,信号量;
创建一个Semaphore对象,给定一个允许访问的线程数;执行方法之前的时候通过调用aquire()方法请求一个信号量;方法执行结束再调用release()方法释放一个信号量
30.问:对ThreadLocal的理解(24/02/23)
答:
ThreadLocal可以实现资源对象的线程隔离,让每个线程各用各的资源对象,避免挣用而引发的线程 安全问题
ThreadLocal实现了线程内的资源共享
每一个线程都一个ThreadLocalMap类型的成员变量,用来存储资源对象
调用对象set方法,就是以ThreadLocal自己作为key,资源对象为vlaue,放入ThreadLocalMap集合中
调用get方法,就是以ThreadLocal为key,在ThreadLocalMap中查找关联的资源值
调用remove方法,就是以ThreadLocal为key,在ThreadLocalMap中移除关联的资源对象
(四).jvm
1.问:什么是程序计数器(24/02/24)
答:
线程私有的,记录下一条执行指令的地址
2.问:介绍java堆(24/02/24)+
答:
是一个线程共享的区域,主要是用来保存对象的实例,数组等,内存不够就会抛出OOM异常,它由年轻代和老年代组成,年轻代又划分为了三个区域,Eden区以及两个大小相同的Survivor区,老年代中主要保存生命周期较长的对象,一般都是一些老的对象;
jdk1.7中还有一个方法区,存储的是类的信息,静态变量,常量等以及编译后的代码;jdk1.8中移除了方法区,把数据存储到了本地内存的元空间中,避免内存溢出
3.问:什么是虚拟机栈(24/02/24)+
答:
虚拟机栈就是每个线程运行时所需要的内存,每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存,每个线程只能有一个活动栈帧,对应当前正在执行的那个方法
4.问:垃圾回收是否涉及栈内存(24/02/24)
答:
垃圾回收主要指的是堆内存,当栈帧弹栈之后,内存就会释放
5.问:栈内存分配越大越好吗(24/02/24)
答:
未必,栈内存过大会导致最大线程数减少,默认的栈内存通常为1024k,例如机器的总内存512m,目前能活动的线程数为512,如果把栈内存改为2048,那么能活动的栈帧就会减半
6.问:方法内的局部变量是否线程安全(24/02/24)
答:
如果方法内部的局部变量没有逃离方法的作用范围,那么它是线程安全的;如果方法内的局部变量引用了对象,并逃离了方法的作用范围,那么就需要考虑线程安全问题了
7.问:什么情况会导致栈内存溢出(24/02/24)
答:
栈帧过多导致栈内存溢出,典型的问题:递归调用;栈帧多大导致栈内存溢出
8.问:堆栈的区别是什么(24/02/24)+
答:
堆内存主要是用来存储变量和方法调用,而堆内存是用来存储java对象和数组的,堆会进行gc回收,二栈不会
栈内存是线程私有的,而堆内存是线程共有的
两者内存不足时抛出的异常不同:栈内存不足:StackOverFlowError;堆内存不足:OutOfMemoryError;
9.问:能不能解释一下方法区(24/02/24)
答:
方法区就是各个线程共享的内存区域
方法区主要存储类的信息,运行时常量池
虚拟机启动的时候创建,虚拟机关闭的时候释放
如果方法区的内存无法满足分配请求,就会抛出OutMemoryError:Metaspace
10.问:介绍一下运行时常量池(24/02/24)
答:
可以看做一张表,虚拟机指令执行的时候就是根据这张表找到要执行的类名,方法名,参数类型,字面量等信息;当类被加载,它的常量池信息就会放入运行时常量池,其中的符号地址变为真实的地址
11.问:你听说过直接内存吗(24/02/24)
答:
直接内存不属于JVM的内存结构,不用JVM进行管理,是虚拟机的系统内存;常见于NIO操作时,用于数据缓冲区,分配回收成本高,但读写性能高,不受JVM内存回收管理
12.问:什么是类加载器(24/02/24)
答:
JVM只会运行二进制文件,类加载器的作用就是将字节码文件加载到JVM中,从而让java程序运行起来
13.问:类加载器有哪些(24/02/24)
答:
启动类加载器(BootStrap ClassLoader):加载JAVA_HOME/jre/lib 目录下的库
扩展类加载器(ExtClassLoader):主要加载JAVA_HOME/jre/lib/ext目录下的库
应用类加载器(AppClassLoader):用于classPath下的类
自定义类加载器(CustomizedClassLoader):实现自定义的类加载规则
14.什么是双亲委派模型(24/02/24)
答:
当一个类加载器去加载某个类的时候,就会自底向上查找是否被加载过,如果加载过就直接返回,如果没有被记载,又会自顶向下的进行加载6
15.问:JVM为什么采用双亲委派机制(24/02/24)
答:
1.能够避免一个类被重复的加载
2.能够防止恶意代码替换jdk的核心类库
16.问:说一说类装载的执行过程(24/02/24)
答:
加载:查找和导入class文件
验证:保证加载类的准确性
准备:为变量分配内存并设置内的初始值
解析:把类的符号应用转为直接引用
初始化:对类的静态变量进行赋值,执行静态代码块的逻辑
使用:JVM开始从入口方法开始执行用户的程序代码
卸载:当用户程序代码执行完毕之后,JVM便开始卸载Class对象
17.问:对象什么时候可以被垃圾回收(24/02/24)
答:
如果一个对象或多个对象没有任何引用指向它了,那么这个对象就可以被垃圾回收器回收了;
定位垃圾的方式两种:引用计数法,可达性分析算法;
(
哪些对象可以被称为GC Root对象:
线程Thread对象;
系统类加载器加载的java.lang.Class对象,应用类的静态变量;
监视器对象,用来保存同步锁synchronized关键字持有的对象;
本地方法调用时使用的全局对象
)
18.问:垃圾回收算法有哪些?(24/02/24)
答:
1.标记清楚算法:垃圾回收分为两个阶段,分别时标记和清除,效率高,有磁盘碎片,内存不连续
2.标记整理算法:标记存活的对象将存活的对象移动到一端,然后清除边界以外的垃圾,无碎片,对象需要移动,效率低
3.赋值算法:将原有的内存空间一分为二,每次值使用其中的一块,把存活的对象移到另一个内空间中,让后把内存清空,交换两个内存的角色,完成垃圾的回收,无碎片,内存使用率低
19.问:分代回收算法(24/02/24)
答:
(
Minor GC 发生在新生代的垃圾回收,暂停时间短
Mixed GC 新生代+老年代 部分区域的垃圾回收,G1收集器持有
Full GC 新生代+老年代 完整的垃圾回收,暂停时间长,应该尽力避免
)
堆被分为了两份:新生代和老年代【1:2】对于新生代内部有分为了三个区域,Eden区,幸存者区survivor(分为了from和to)【8:1:1】
新建的对象,都会被先分配到eden区;当eden区内存不足,就会标记eden区和from区中存活的对象将存活的对象采用复制法赋值到to区;复制后,把eden区和from区的内存释放;当进过一段时间eden区内存有满了,就会标记eden区和to区存活的对象复制到from区;当幸存者区的对象熬过了几次(最多15次)垃圾回收,就会晋升到老年代(内存不足或是大对象会提前晋升);
20.问:JVM的垃圾回收器(24/02/24)
答:
(
串行垃圾回收器
Serial和Serial Old串行垃圾回收器,指的是使用单线程进行垃圾回收,适用于堆内存小,适合个人电脑
Serial 作用于新生代,采用复制算法;Serial Old作用于老年代,采用标记-整理算法;
垃圾回收时,只有一个线程在工作,且java应用中的所有线程都要暂停,等待垃圾回收完成
并行垃圾回收器
Parallel New 和Parallel Old是一个并行的垃圾回收器,jdk8默认使用的垃圾回收器
Parallel New作用于新生代,采用复制算法
Parallel Old作用于老年代,采用的标记-整理算法
垃圾回收时,多个线程工作,且java应用中的所有线程都要暂停,等待垃圾回收暂停
CMS(并发)垃圾回收器,
是一款并发的,使用标记清楚算法的垃圾回收器,该收集器是针对老年代的垃圾回收的,是一款以获取最短的停顿时间为目标的收集器,停顿时间短,用户体验好,最大的特点是在进行垃圾回收的时候,应用仍能正常的运行
)
串行垃圾回收器:Serial GC,Serial Old GC
并行垃圾回收器:Parallel NewGC, Parallel Old GC
CMS(并发垃圾回收器):CMS GC,作用于老年代
G1垃圾回收器,作用于新生代和老年代
21.问:G1垃圾回收器(24/02/24)
答:
(
将堆划分为多个区域
初始时,所有的区域都处于空闲状态
创建对象时,就会挑出一些空闲的区域作为eden区来存储对象
当eden区需要进行垃圾回收的时候,又会挑出一个空闲的区域作为幸存者区,使用复制算法复制对象,需要暂停用户的线程
当eden又不足了,将eden区的以及之前的幸存者者区的存活对象采用复制算法,复制到新的幸存者区,将比较老的对象晋升到老年代
当老年代占用内存超过阈值(默认45%)后,触发并发标记,这是无需暂停用户线程
并发标记阶段之后,会有重新标记阶段解决漏标问题,此时需要短暂的暂停用户线程,这些执行之后,就会知道老年代还有哪些存活的对象
随后进入混合收集阶段(参与复制的有eden,survivor,old),此时不会有对所有老年代进行回收,会根据暂停的时间 优先回收价值高(存活对象少)的区域;
复制完成后内存得到释放,进入下一轮新生代回收,并发标记混合收集
)
应用于新生代和老年代,在jdk9后默认采用g1
划分为了多个区域,每个区域都可以充当eden,survivor,old,humongours,其中humongours专为大对象准备
采用复制算法
响应时间与吞吐量
分成了三个阶段:新生代回收,并发标记,混合收集
如果并发失败(即回收速度赶不上创建新对象的速度),就会触发Full GC
21.问:强引用,软引用,弱引用,虚引用的区别(24/02/24)
答:
强引用:
软引用:需要配合SoftReference使用,当内存不足时,会回收软引用对象
弱引用:需要配合WeakReference使用,gc一碰到就会对弱引用进行回收
虚引用:多用于释放直接内存,必须配合引用队列使用,被引用对象被回收就会把虚引用插入队列,由Refrence Handler线程调用虚引用相关的api释放直接内存
22.问:CMS垃圾回收器(24/02/24)
答:
CMS(Concurrent Mark Sweep)的垃圾回收器是一种作用于老年代,采用标记清除算法的一种以获取最短的停顿时间为目标垃圾回收器,在进行垃圾回收时用户进程依旧可以正常运行;
GC的过程分为五个步骤:
初始标记:暂停所有线程,并记录GC Roots直接引用的对象,速度快;
并发标记:并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长,用户线程可以和垃圾回收线程一起的并发运行,该阶段由于用户线程的执行可能会导致一些标记过的对象的状态发送改变,产生多标和漏标的问题;
重新标记:该阶段的停顿时间一般会比初始标记阶段,重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记的对象状态发送变化的情况,该阶段的停顿时间一般比初始标记阶段的时间稍长,远远比并发标记阶段的时间短;主要用到了三色标记的增量更新算法做的重新标记
并发清理:GC线程开始前清理未标记的对象(垃圾对象),如果该阶段有新增的对象那么会被标记为黑色不做任何处理
并发重置:重置本次GC过程中的标记数据
23.问:JVM的内存结构(24/03/02)
答:
· 1.堆
2.虚拟机栈
3.本地方法栈:线程私有,和虚拟机栈的主要区别:虚拟机栈是虚拟机执行java的方法服务的,本地方法栈是为虚拟机使用到的native方法服务的;
4.程序计数器
5.方法区
(五).基础
1.问:为什么重写equals()方法就一定要重写hashcode()方法(24/03/01)
答:
对于只重写了equals方法的对象,在使用散列表集合进行存储的时候就会出现问题,在散列集合在存储两个相同的对象但是这两个相同的对象却有不同的hashcode就会导致存储到hash表的不同位置,可能会导致出现一些不可预料的错误
2.问:String,StringBuffer,StringBudier的区别(24/03/01)
答:
String是不可变类型,在进行拼接等操作的时候就会去创建一个新的String类型的对象,可能会影响性能,且String类型的对象线程安全的(因为值不可变);
StringBuffer是不可变类型,在进行拼接等操作时会在原本的对象上进行修改,不会产生新的对象;是线程安全的,由于线程安全的特性,执行速度可能相对较慢
StringBuilder是不可变类型,在进行修改时是在原有的对象上进行的修改,而不会创建新的对象,与StringBuffer相比,StringBuilder不是线程安全的,也正是因为不需要考虑线程安全问题,StirngBuilder的执行速度比StringBuffer快
3.问:BIO,NIO,AIO
答:
BIO,同步阻塞IO,服务器实现模式为一个连接一个线程,即客户端存在连接请求时服务器就需要启动一个线程来处理,如果这个链接不做任何事就会造成不必要的线程开销;
NIO,同步非阻塞IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求时启动一个线程进行处理,用户进程需要时不时的询问IO操作时候否就绪;
AIO,异步非阻塞IO,用户进程只需要发起一个IO操作然后立即返回,等IO操作执行完之后用户进程就会得到IO操作完成的通知,此时用户进程就只需要对数据进行处理就好了,不需要实际的IO读写操作,因为真正的IO读取或是写入操作有内核完成;
二.框架,数据库,中间件
(一).SpringBoot
1.问:SpringBoot的启动流程(24/02/20)
答:
SpringBoot的启动,其本质就是加载一些配置信息,然后初始化IOC容器并返回;
首先,当启动类执行SpringApplication.run这行代码的时候,它方法的内部会做两件事:
1.创建SpringApplication对象
2.执行该SpringApplication对象的run方法;
在创建SpringApplicaiton对象的时候,在它的构造函数内部主要做了3件事:
1.确定Web应用类型,一般情况下是Servlet类型,这个类型的应用,将来会启动一个tomcat
2.从spring.factories配置文件中加载默认的ApplicationContextInitalizer和ApplicationListener
3.记录当前的主启动类,后面做包扫描;
调用该SpringApplication对象的run方法时主要做了4件事:
1.准备Environment对象,会封装一些该应用运行环境的参数,比如环境变量等等
2.实例化容器,这里仅仅会创建一个ApplicaitonContext对象
3.容器创建之后就会为容器做一些准备工作,比如为容器设置Environment,BeanFactoryPostProcessor后置处理器,并加载主类对应的Definition;
4.刷新容器,在这里会真正的创建Bean的实例
2.问:IOC容器的初始化流程(24/02/20)
答:
IOC容器的初始化,核心工作是在AbstractApplicationContext.refresh方法中完成的,
refresh方法中主要做了这么几件事:
1.准备BeanFactory,这一块需要给BeanFactory设置很多属性,如类加载器,Environment等
2.执行BeanFactory后置处理器,这一阶段会扫描放到容器中的Bean信息,得到对应的BeanDefination
3.注册BeanPostProcessor,我们自定义的BeanPostPorcessor也会在这一阶段被加载,将来Bean对象实例化好之后会使用到
4.启动tomcat
5.实例化容器的非懒加载的单例Bean。多例Bean和非懒加载的单例Bean将来被使用的时候才会被创建;
6.当容器初始化完成之后,在做一些扫尾工作,比如清楚缓存等;
简单总结一下,IOC容器的初始化过程中,首先准备并执行BeanFactory后置处理器,然后注册Bean后置处理器,再启动tomcat,最后借助BeanFactory完成Bean的实例化
3.问:Bean的生命周期(24/02/20)
答:
Bean的生命周期总的来说有4个阶段,分别是创建对象,初始化对象,使用对象以及销毁对象;
首先,在创建对象阶段,会调用该对象的构造方法实例化对象,然后在执行依赖的注入;
创建完毕之后,就会执行一下初始化的操作:
1.执行3个Aware接口的回调(BeanNameAware,BeanFactoryAware,ApplicationContextAware)
2.执行Bean后置处理器的postProcessBeforeInitalization方法
3.执行InitalizingBean接口的回调,如果Bean有方法标注了@PostConstruct注解,那么会先执行该方法
4.执行Bean后置处理器的postProcessAfterInitalization方法
Bean的初始化就完成了;
接下来在Bean的使用阶段就是程序员从容器中获取Bean并使用;
在容器销毁之前,会执行DisposableBean接口的回调,如果Bean有方法标注了@PreDestroy接口的函数,那么会先执行该方法
4.问:Spring中Bean的循环依赖(24/02/20)
答:
Bean的循环依赖指的是A依赖B,B依赖A这样的依赖闭环问题,在Spring中是通过DefaultSingletonBeanRegistry中的三个缓冲区解决的,这三个缓冲区分别是singletonObjects用来存储创建完毕的Bean,earlySingletonObjects用来存储未完成依赖注入的Bean,SingletonFactories用来存储Bean的ObjectFactory,
假如现在A依赖B,B依赖A,那么他们的创建过程是这样的:
首先,调用A的构造方法实例化A,此时的A还没有进行依赖注入,暂且称他为半成品,此时就会被半成品A封装成ObjectFactory,并将ObjectFactory存储到singletonFactorys中;
接下来就要进行A的依赖注入了,可是此时还没有B,那么就会先去创建B,调用B的构造方法实例化B,把半成品B封装成ObjectFactory并存储到singletonFactorys中;
然后进行B的依赖注入,此时会从singletonFactorys中获取半成品A对应的ObjectFactory,调用它的getObject方法获取半成品A(如果此时需要的是代理对象,那么就会创建并返回对应的代理对象),把半成品A注入给B,并会把半成品的A存放至earlySingletonObjects中,如果还有其他的类循环依赖了A,就可以直接从earlySingletonObjects中获取半成品A,并把半成品A对应的ObjectFactory从singletonFactorys中删除;
B的依赖注入也完成了,就会把B存储到singletonObjects中,并把singletonFactorys中半成品B对应的ObjectFactory删掉了;
此时B创建完成了,就可以继续进行A的依赖注入了,把B注入给A,A也创建完成了,就会把A存储到singletonObjects中,并把earlySingletonObjects中的半成品A删除掉了;
好了,A,B都已经创建完成了,并存储到了singletonObjects中,将来从容器中获取对象,都是通过singletonObjects中获取;
总结一下就是,通过DefaultSingletonBeanRegistry的三个缓冲区解决的循环依赖问题;
5.问: SpringMvc的执行流程(24/02/20)
答:
使用了SpringMvc后,所有的请求都需要经过前端控制器,该类中有一个doDispath方法,有关请求的处理以及结果的响应的所有流程都是在该方法中完成的;
首先,通过处理器映射器得到处理器执行链,里面封装了HandlerMethod代表目标Controller的方法,同时会用一个集合记录需要执行的拦截器;
接下来,会根据HandMethod获取对应的处理器适配器,里面封装着参数解析器和结果处理器
然后执行拦截器的preHandler方法
接下来是核心,通过处理器适配器执行目标的Controller方法,在这个过程中会使用参数解析器和结果处理器来解析浏览器提交的数据以及处理Controller返回的结果
然后执行拦截器的postHandler方法
最后处理响应,在这个过程中如果有异常抛出,则会执行异常的逻辑,这里还会执行全局异常处理器的逻辑,并通过视图解析器解析视图,在渲染视图
最后执行拦截器的afterCompletion方法
6.问: Spring IOC的理解(24/02/20)
答:
Spring IOC 的全称是Inversion of Controller 控制反转,在传统的java程序开发中我们通常通过new关键字来创建对象,这样会使得程序中对象之间的依赖关系比较复杂且耦合度比较高,而IOC的主要作用就是实现了对象的管理,我们把设计好的对象交给IOC容器控制,然后再我们需要使用目标对象的时候再从容器中去获取,有了IOC容器来管理Bean以后呢相当于把对象的创建和查找依赖对对象的控制交给了容器,这样的设计理念使对象与对象之间处于松耦合状态,极大的提升了程序的灵活性以及功能的复用性;
7.问: Spring DI的理解(24/02/20)
答:
DI 依赖注入,对于IOC容器管理的Bean,如果Bean之间存在依赖关系那么需要IOC容器自动去实现依赖对象的实例注入,通常我们有三种方式去描述Bean和Bean之间的关系,接口注入,setter注入,构造器注入,另外为了更加灵活的去实现Bean实例的依赖注入,Spring还提供了@Resource,@AutoWired这样的注解分别去根据Bean的名称,类型进行依赖的注入;
8.问:Springboot 的自动配置原理(24/02/20)
答:
在SpringBoot项目中的引导类上有@SpringBootApplication这个注解,该注解是对:@SpringBootConfiguration,@EnableAutoConfiguration,@CompontentScan三个注解的封装,其中@EnableAutoCofiguration是实现自动化配置的核心注解,该注解通过@Import注解导入了对应的配置选择器,内部就是读取了该项目以及该项目导入的jar的classpath路径下的MATE-INF/spring.factories文件中所配置类的全类名,在这些配置类中所定义的Bean会根据条件注解所指定的条件来决定是否需要将其导入到SpringBoot容器中;
条件注解比如@ConditionalOnClass,会判断是否有对应的class文件,如果有就会加载该配置类,并把配置类中所有的Bean放至Spring容器中;
9.问:Spring框架中的单例Bean是线程安全的吗(24/02/21)
答:
不是线程安全的,当多个用户请求同一个服务的时候,容器会为每一个请求分配一个线程,多个线程就会并发的执行业务逻辑(成员方法),如果业务逻辑中有对单例bean状态(成员属性)的修改那么就需要考虑线程安全问题了;
spring框架并没与对单例bean进行任何多线程的封装处理,对应单例bean的线程安全问题需要开发者自己去解决;
但是呢,我们平时在项目中所使用的bean大多都是无状态(没有成员属性)的,比如Service类,Dao类等,所以可以从某种角度来说它们是线程安全的
如果使用的bean是多中状态的话(比如View Model对象),就需要去保证其线程安全,最简单的解决方法就是给这个bean添加@Scope注解,将bean设置为多例;
10.问:AOP(24/02/21)
答:
面向切面编程,将那些与业务无关,但对多个对象产生影响的公共行为和逻辑,抽取成公共的模块复用,降低耦合
11.问:Spring的事务是如何实现的(24/02/21)
答:
本质是通过AOP实现的,对方法前后进行拦截,在方法执行之前开启事务,在目标方法执行完之后根据情况提交或是回滚事务
12.问:Spring中事务失效的场景(24/02/21)
答:
1.异常捕获处理,自己把异常处理掉了,没有抛出,解决方法:手动抛出异常
2.抛出检查异常,因为@Transactional默认捕获的是非检查异常,解决方法:给@Transactional注解rollbackFor属性修改为Exception.class
3.非public方法导致事务失效,解决方法:改为public
13.问:SpringBoot和SpringMVC的区别(24/02/28)
答:
SpringBoot是一个自动化配置的工具,SpringMVC是一个web框架
在搭建项目时SpringMVC需要手动的配置xml文件,同时需要配置Tomcat服务器,而SpringBoot采用的是约定大于配置的方式,会进行自动的装配,同时内置服务器,打开就可以直接用;
14.问:Spirng,SpringBoot和SpringMVC的常见注解(24/02/28)
答:
Spring常用注解
1、@Configuration
2、@Import
3、@ImportResource
4、@Bean
5、@Value
6、@Primary
7、@Autowired
8、@Qualifier
9、@Resource(J2EE里面的注解)
10、@controller
11、@Service
12、@Repository
13、@Mapper(Mybatis的注解)
14、@Component
15、@Scope
16、@Lazy(true)
17、@PostConstruct
18、@PreDestory
19、@DependsOn
20、@Async
SpringMVC常用注解
1、@RestController
2、@RequestMapping
handler method 参数绑定常用的注解
3、@PathVariable(处理requet uri 部分)
4、@RequestHeader, @CookieValue(处理request header部分)
5、@RequestParam, @RequestBody(处理request body部分)
6、@ModelAttribute和 @SessionAttributes
7、@ResponseBody
SpringBoot常用注解
1、@SpringBootApplication
2、@SpringBootConfiguration
3、@EnableAutoConfiguration
4、@ComponentScan
5、RequestMapping简化注解
6、@Profiles
(二).MySql
1.问: mysql如何定位慢查询(24/02/21)
答:
1.开源工具: Arhas,Prometheus,Skywalking
2.mysql自带的慢日志
(出现慢查询的原因:1.聚合查询,2.多表查询,3.表数据量过大查询,4.深度分页查询)
2.问: SQL语句很慢如何分析(24/02/21)
答:可以采用mysql自带的分析工具explain,通过key和key_len检查是否命中了索引,是否出现了索引失效的情况,通过type字段查看sql还有没有进一步优化的空间,是否出现了全索引扫描,或是全盘扫描,通过extra建议判断,是否出现了回表的情况,如果出现了可以尝试添加索引或是修改返回字段来解决;
(
possiable_key:当前sql可能使用到的索引
key:当前sql实际命中的索引
key_len:索引占用的大小
Extra:额外的优化建议(Using where;Using Index:查找使用了索引,需要的数据在索引中都能找到,不需要回表操作;Using index condition:查找使用了索引,但是需要回表操作)
type:这条sql连接的类型,性能由好到差为 NULL,system,const,eq_ref,ref,range,index,all(system:查询系统的表,const:根据主键查询,eq_ref:主键索引查询或唯一索引查询,ref:索引查询,range:范围查询,index:索引树扫描,all:全盘索引)
)
3.问:什么是索引(24/02/21)
答:
索引就是帮助数据库高效获取数据的数据结构,主要是用来提高数据检索的效率的,降低数据库的IO成本,同时索引还能高效的进行数据的排序,降低数据的排序成本,也能降低CPU的消耗
4.问:索引底层的数据结构(24/02/21)
答:
mysql的默认存储引擎InnoDB采用的是B+树这种数据结构作为索引的,选择B+的原因有,阶数更多,路径更短;磁盘读写代价B+树更低且查询效率稳定,因为它只用叶子节点存储数据;B+树有利于扫库或是区间查询,因为叶子节点是一个双向链表
5.问:B树和B+树的区别(24/02/21)
答:
首先,B树的叶子节点和非叶子节点都会存储数据,而B+树只有叶子节点存储数据,因此B+树的查询效率更加稳定且磁盘读写代价更低
还有在进行范围查询的时候,B+树的效率更高,因为B+树都在叶子节点存储,且叶子节点还是一个双向链表;
6.问:聚集索引(聚簇索引),非聚集索引(非聚簇索引,二级索引)(24/02/21)
答:
聚集索引主要指的是数据和索引放到一块,B+树的叶子节点存储一整行的数据,有且只有一个,一般情况会以主键作为聚集索引
非聚集索引是指数据和索引分开存储,B+树的叶子节点只存储对应的主键,一个表能有多个,一般我们自定义的索引就是非聚集索引
(聚集索引的选取规则:如果存在主键,那么注解索引就是聚集索引;如果不存在主键,就会以第一个唯一索引作为聚集索引;如果不存在主键,也没有一个合适的唯一索引,那么InnoDB就会自动生成一个rowid作为隐藏的聚集索引)
7.问:回表查询(24/02/21)
答:
回表就是指通过非聚集索引查询到对应的主键,让后在通过主键找到聚集索引中对应的整行的数据,这个过程就是回表;
8.问: 覆盖索引(24/02/21)
答:
覆盖索引就是指select查询时使用了索引,且返回的列在索引中全部都能找到
9.问:mysql的超大分页如何处理(24/02/21)
答:
超大分页一般都是在数据量比较大的时候,使用了limit分页查询,且需要对数据继续排序,这时候的效率就会很低,我们可以通过覆盖索引和子查询的方式来解决
分页查询时查询数据的id字段,确定了id之后通过子查询进行过滤,查询id对应的数据就可以了,因为查询id的时候走的是覆盖索引,所有效率就可以提升很多
10.问:创建索引的原则(24/02/21)
答:
1.针对数据量较大,且查询比较频繁的表建立索引,单表超过10万数据(增加用户的体验)
2.针对常作为查询条件,排序,分组的字段建立索引
3.尽量选择区分度高的列作为索引,尽量建立唯一索引,区分度越高,使用索引的效率就越高
4.如果是字符串类型的字段,字段的长度较长,可以针对字段的特点建立前缀索引
5.尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间,避免回表,提升查询效率
6.也要控制索引的数量,所以不是多多益善,索引越多,维护索引的代价也越大,会影响增删改的效率
7.如果索引类不存储 null 值,在创建表的时候使用not null约束它,当优化器知道每列是否包含null值的时候,它可以更好的确定那个索引能更有效的进行查询
11.问:索引失效的情况(24/02/21)
答:
1.违反了最左前缀法则
2.范围查询右边的列,不能使用索引
3.在索引列上进行了算数运算
4.在索引列上进行了类型的转换(比如:字符串不加单引号)
5.以%开头的模糊查询
12.问:sql优化的经验(24/02/21)
答:
sql优化呢我们可以从这么几个方面进行考虑,比如:
1.表的建立(建表的时候为字段设定合适的类型,如:对boolean类型使用tingint;对于定长的字符串可以选择使用char,char定长效率高,varchar可变长度,效率偏低)
2.建立索引(见10.问:创建索引的原则)
3.sql语句的编写(1.select语句指明字段,避免使用select *;2.sql语句编写时要避免造成索引失效(见11.问:索引失效的情况);3.尽量使用union all代替 union,union会多过滤一次;4.join优化 能用inner join 就不用left join right join ,一定要使用小表驱动大表,inner join 会对两个表进行优化,优先把小表放到外面,把大表放到里面,left join,right join不会调整顺序)
4.主从复制,读写分离(对于数据库使用场景读较多时,为了避免写操作所造成的性能影响,可以采取读写分离的架构,避免数据库的写入影响查询的效率)
5.分库分表
13.问:ACID(24/02/21)
答:
原子性(Atomicity):事务时不可分割的最小操作单元,要么全部成功,要么全部失败
一致性(Consistency):事务完成时,必须要使所有数据的状态保持一致
隔离性(Isolation):数据库系统提供的隔离机制,要保证事务在不受外部并发操作影响的独立环境下运行
持久性(Durability):事务一旦提交或是回滚,那么它对数据库中的数据的改变将是永久的
(举个例子:张三向李四转500块,转账成功,张三扣除500,李四增加500;原子性的操作体现在这次账要么张三扣除500,李四增加500这件事要么都成功,要么都失败;一致性体现在:张三扣除了500,那么李四一定增加了500,;隔离性体现在:张三向李四转账的操作不会受到其他人转账的影响;持久性体现在:转账成功后,数据的改变是永久的)
14.问:并发事务带来的问题(24/02/21)
答:
脏读:当一个事务访问了某条数据并对数据进行了修改,而这个修改还没有提交到数据库的时候,另一个事务读取了这条还未提交的数据,那么另一个事务读到的就是脏数据,依照脏数据进行的后续操作可能不正确
不可重复读:一个事务内多次读取同一条数据,在这个事务还未结束时,有另一个事务访问了这条数据并对数据进行了修改,导致第一个事务两次读取数据不太一样;
幻读:幻读与不可重复读类似,发生在一个事务查询读取了几行数据,此时另一个事务插入了一些数据,导致第一个事务再次查询数据时发现多了几条数据,就像发生了幻影一样,所以称为幻读;
15.问:mysql的隔离级别(24/02/21)
答:
未提交读(会出现:脏读,不可重复读,幻读)
读已提交(解决了:脏读,会出现:不可重复读,幻读)
可重复读(解决了:脏读,不可重复读,会出现:幻读)(mysql默认隔离机制)
串行化(解决了:脏读,不可重复读,幻读)
但是由于串行化是让事务串行执行,性能较低;因此一般采用mysql的默认隔离机制,可重复读
16.问:undo log 和 redo log的区别(24/02/21)
答:
(redo log :重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性的,该日志文件是由两部分组成:redo log buffer(重做日志缓冲)以及redo log file (重做日志文件),前者在内存,后者在磁盘,当事务提交之后就会把所有的修改信息存到日志文件中,用于刷新脏页到磁盘时,发生错误时,进行数据恢复使用)
(undo log:回滚日志,记录数据被修改前的信息,作用包含两个提供回滚和MVCC,undo log 和redo log 记录物理日志不同,它记录的是逻辑日志,可以认为当delete一条数据的时候,undo log就会记录一条对应的insert语句,反之亦然;当update一条语句的时候,就会记录一条对应相反的update记录;当执行rollback(回滚)的时候,就可以从undo log中的逻辑记录中读取对应的内容进行回滚,它实现了事务的原子性和一致性)
redo log记录的是数据页的物理变化,服务器宕机时可以用来同步数据,而undo log 不同,它记录的是逻辑日志,当事务回滚的时候,可以通过逆操作恢复原来的数据,比如我们删除一条数据,就会在undo log中记录一条对应的insert语句,如果发生回滚就进行逆操作;redo log保证了事务的持久性,undo log保证了事务的原子性和一致性;
17.问:事务的隔离性是如何保证的(24/02/21)
答:
事务的隔离性是由锁和MVCC实现的
其中mvcc就是多版本并发控制,会维护一个数据的多个版本,使其读写操作不冲突;它的底层的实现分为了三个阶段,第一个是隐藏字段,第二个是undo log日志,第三个是readView读视图
隐藏字段是指,在mysql中会为每一个表设置隐藏字段,有一个trx_id(事务id)用来记录每次操作的事务id,是自增的;另一个事roll_point(回滚指针),执行上一个版本的版本记录地址
updo log主要是记录回滚日志,存储老版本的数据,内部会形成一个版本链,在多个事务并发操作某一行的数据,就会记录不同事务修改数据的版本,通过回滚指针形成一个版本链
readView解决的是事务查询选择版本的问题,内部定义一些匹配规则和当前的一些事务id判断该访问哪一个版本的数据,不同的隔离级别快照读是不一样的,最终的访问结果页不一样,如果是RC(读已提交)每次执行快照读的时候会生成ReadView;而RR(可重复读)仅仅会在事务第一个执行快照读的时候生成ReadView,后续复用
(
readView:是快照读sql执行时mvcc提取数据的依据,记录并维护系统的当前活跃的(未提交的)事务id
当前读:记录的是最新版本,读取时要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁,例如:select ...lock in share mode,select ... for update ,update ,insert,delete都是一种当前读
快照读:简单的select(不加锁)就是快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁,就是非阻塞读
Read Commited 每次select 生成一个快照读
Repeatable Read 开启事务后第一个select 生成快照读,后续复用
)
18.问:主从同步的原理(24/02/21)
答:
mysql主从复制的核心就是二进制日志文件binlog(记录DDL(数据定义语句),DML(数据库操作语句)语句)
1.主库在提交事务时会把数据的变更记录到二进制日志文件binlog文件中
2.从库就会去读取主库的二进制日志文件binlog,写入从库的中继日志文件Relay log中
3.从库就会重做中继日志文件中的事件,进行数据的同步
19.问:分库分表的拆分策略
答:
水平分库(将一个库的数据拆分到多个库中):可以解决海量的数据存储和高并发的问题
水平分表(将一个表的数据拆分到多个表中):解决单表存储和性能问题
垂直分库(以表为依据,根据业务将不同的表拆分到不同的库中):高并发下提高磁盘IO和网络连接数
垂直分表(以字段为依据,根据字段属性的不同拆分到不同的表中):冷热数据分离,多表互不影响;
20.问:mysql的存储引擎
答:
innoDB:是一种兼顾可靠性和高性能的通用存储引擎,在mysql5.5 之后,innoDB是默认的存储引擎;
特点: DML操作遵循ACID模型,支持事务;行级锁,提高并发访问性能;支持外键Foreign Key约束,保证数据的完整性和正确性;
MyISAM:是mysql早期的默认存储引擎
特点:不支持事务,不支持外键;支持表锁,不支持行锁,访问速度快
Memory:表数据存储子在内存中,由于受到硬件问题或断电问题的影响,只能将这些表作为临时表或是缓存使用
特点:内存存储,hash索引
(三).Redis
1.问:redis的使用场景(24/02/20)
答:
1.缓存 (穿透,击穿,雪崩,双写一致,持久化,数据过期,淘汰策略)
2.分布式锁 (setnx,redisson)
(3.消息队列,延时队列)
2.问:缓存穿透?解决方案?(24/02/20):
答:
缓存穿透:用户请求不存在的数据,就会每次都去数据库查询,对数据库造成压力;
解决方案:1.缓存空值,2.布隆过滤;
3.问: 缓存雪崩?解决方案?(24/02/20)
答:
缓存雪崩:同一时间大量的key同时过期,或是redis服务器宕机,导致大量请求查询数据库,对数据库造成压力
解决方案:1.给key设定ttl时添加随机值(大量的key同时过期),2.部署redis集群(redis服务器宕机)
4.问: 缓存击穿?解决方案?(24/02/20)
答:
缓存击穿:也叫做热点key问题,对应某个高并发且缓存重建比较缓慢的key过期了,导致大量的请求去查询数据库,对数据库造成压力;
解决方案:1.互斥锁(强一致,性能查)2.逻辑过期(高可用,性能优,不能保证数据绝对一致)
5.问: redis持久化(24/02/20)
答:
RDB是一个快照文件,它是把redis内存存储的数据写到磁盘上,当redis实例宕机恢复数据的时候,方便从RDB的快照文件中恢复速度;
(
RDB的执行原理?
redis执行bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据,完成fork之后子进程就会读取内存数据写入RDB文件;
fork采用的是copy-on-write技术:主进程执行读操作的时候,访问共享数据;当主进程执行写操作的时候,就会拷贝一份数据,执行写操作;
)
AOF的含义是追加文件,当redis执行写操作的时候,就会把写操作的指令存储到这个文件中,当redis宕机恢复数据的时候就会把AOF中的指令再执行一遍来恢复数据;
RDB,AOF各有优缺点,如果对数据安全性较高可结合两者使用;
RDB | AOF | |
持久化方式 | 定时对整个内存做快照 | 记录每一个执行的命令 |
数据完整性 | 不完整,两次备份之间会丢失 | 相对完整,取决于刷盘策略 |
文件大小 | 会有压缩,文件体积小 | 记录命令,文件体积大 |
宕机恢复速度 | 很快 | 慢 |
数据恢复优先级 | 低,因为数据完整性不如AOF | 高,因为数据完整性高 |
系统资源占用 | 高,大量CPU和内存消耗 | 低,主要是磁盘IO资源但AOF重写时会占用大量CPU和内存资源 |
使用场景 | 可以容忍数分钟的数据丢失,追求更快的启动速度 | 对数据安全性较高场景 |
6.问: redis数据过期(24/02/20)
答:
redis的数据删除策略有:惰性删除,定期删除
惰性删除:设置改key的过期时间后,不去管他,当使用该key的时候再去检查key是否过期,如果过期就删掉这个key,没有过期返回key的值;
(优点:对CPU友好,因为只有在使用key的时候才会进行过期检查,对于用不到的key不需要浪费时间进行过期检查
缺点:对内存不友好,如果一个key已经过期,可是一直没有使用,该key就会一直存在内存中,内存无法释放
)
定期删除:每隔一定时间,对一些key进行检查,删除其中过期的key;
定期删除有两种模式:
SLOW模式是定时任务,默认的参数是10hz(频率参数),也就是周期为100ms,每次执行的时间不会超过1/4周期,也就是25ms;
FAST模式执行的频率不固定,每次执行时间不超过1ms,两次执行间隔不小于2ms;
7.问: redis数据淘汰策略(24/02/20)
答:
当redis的内存不够用的时候,再向redis添加key,那么redis就会按照一定的规则淘汰key;
redis支持8种淘汰策略:
noeviction:不淘汰任何的key,但是内存满的时候就不允许添加新的数据,redis默认就是采用的这种策略;
volatile-ttl:对于设定了ttl的key,比较key的剩余ttl,ttl越小越先被淘汰
allkeys-random:对于全体的key,随机进行淘汰
volatile-random:对于设定了ttl的key,随机进行淘汰
allkeys-lru:对于全体的key,采用lru算法进行淘汰
volatile-lru:对应设定了ttl的可,采用lru算法进行淘汰
allkeys-lfu:对于全体的key,采用lfu算法进行淘汰
volatile-lfu:对于全体的key,采用lfu算法进行淘汰
(
lru:最近最少使用,当前时间减去上一次访问的时间,值越大淘汰的优先级越高(越长时间没有使用,淘汰的优先级越高);
lfu:最少频率使用,统计每个key的访问频率,值越小淘汰的优先级越高;
)
8.问: redis为什么这么快(24/02/20)
答:
1.redis是纯内存操作,执行速度非常快
2.redis采用的单线程,避免了不必要的上下文切换竞争条件,多线程还需要考虑线程安全问题
3.redis采用I/O多路复用模型,非阻塞IO
9.问: mysql,redis保证数据同步(24/02/20)
答:
(双写一致性:修改数据库数据的同时也要更新缓存的数据,保证数据库,缓存的数据一致;)
1.读写锁(强一致)
2.延迟双删(弱一致)
若允许延时一致
3.使用MQ更新数据后,通知缓存删除
4.使用canal中间件,不需要修改业务代码,伪装成mysql的从节点,canal通过读取mysql的binlog数据更新缓存;
9.问: 介绍布隆过滤器(24/02/20)
答:
布隆过滤器。。。
10.问: redis的集群方案(24/02/20)
答:
主从复制,哨兵模式,分片集群
11.问: redis的主从同步流程(24/02/20)
(主从同步:单节点的redis的并发能力是有限的,为了进一步提高redis的并发能力,就需要搭建主从集群,实现读写分离,一般是一主多从,主节点负责写数据,从节点负责读数据)
全量同步:
1.从节点携带replication id,offset请求主节点同步数据
2.主节点会去判断从节点的replication id和自己的时候一致,不一致的话说明是第一次同步,那么就会进行全量同步,或是一致但是主节点的repl_baklog文件中从节点offset的值已经被覆盖了(说明从节点宕机太久了)
3.主节点执行bgsave指令生成rdb文件,然后发送给从节点读取
4.在rdb文件生成的过程中,主节点收到的请求会记录到repl_baklog(一个日志文件)文件中;
5.最后再把repl_baklog文件发送给从节点
增量同步:
1.从节点携带replication id,offset向主节点发起同步请求
2.从节点的replication id和主节点一致,并且主节点repl_baglog文件中从节点offset的没有被覆盖,那么就会进行增量同步
3.主节点就会把repl_baglog文件中从节点offset之后的数据发送给从节点进行数据的同步
12.问:哨兵的作用(24/02/20)
答:
Redis提供了哨兵机制来实现主从集群的自动故障恢复,哨兵的作用有:监控,自动故障恢复,通知;
监控:哨兵会不断地检查主节点和从节点是否按预期进行工作
自动故障恢复:如果主节点发生了故障,哨兵就会把一个从节点选举为主节点,当老的主节点恢复之后,就会降为从节点,以新的主节点为主;
通知:哨兵会充当redis客户端的服务发现来源,当集群发送故障转移的时候,就会把新的信息推送给redis的客户端;
(哨兵是基于心跳机制检测服务状态的,每隔1s向集群的每一个实例发送一个ping命令;如果哨兵节点发现某个实例未在规定时间内响应,则会认为该节点主观下线;如果超过指定数量(quorum)的哨兵认为该实例主观下线,那么该实例客观下线)
13.哨兵的选主规则(24/02/20)
答:
1.首先去看从节点与主节点断开的时间长短,如果超过了指定值那么就会排除该从节点
2.让后去判断从节点的slave-priority值,值越小优先级越高
3.如果slave-priority的值相同,就比较从节点的offset的值,offset的值越大说明数据越新,优先级就越高
4.最后比较从节点的运行id,值越小优先级越高
14.问:如何保证redis的高并发可用(24/02/20)
答:
可以选择搭建主从集群,在加上redis中的哨兵模式,哨兵模式可以实现自动的故障恢复,其中包含了对主从集群的监控,自动故障恢复,通知;如果主节点发送了故障,哨兵就会选举新的主节点,当故障的实例恢复之后,老的主节点就会降为从节点,以新的主节点为主;同时哨兵会充当redis客户端的发现来源,当集群出现故障转移的时候,哨兵会把最新的信息推送给redis的客户端,所以一般项目都会采用哨兵模式来保证redis的高并发可用;
15.问: redis的集群脑裂?怎么解决?(24/02/20)
答:
集群脑裂就是由于网络问题使主节点,从节点和哨兵处于不同的网络分区,使的哨兵无法心跳感知到主节点,就会认为主节点客观下线,然后重新选举新的主节点,此时就存在两主节点,就像大脑分裂了一样,导致客户端还在老的主节点写入数据,新节点无法同步数据;等网络恢复之后,哨兵就会把老的主节点降为从节点,这是再向新的主节点同步数据,导致老的主节点中的大量数据丢失
解决方案:在redis的配置中设置:1.设置最少的从节点数,比如设置最少需要一个从节点才能进行写操作,2.设置主从复制和同步的延迟时间,达不到要求就拒绝请求,这样就可以避免大量的数据丢失;
16.问:分片集群的特征(作用)(24/02/20)
答:
1.集群有多个主节点,每个主节点存储不同的数据
2.每个主节点都可以有多个从节点
3.主节点之间同ping检测彼此的健康
4.客户端请求可以访问集群的任意节点,最终都会被转发到正确的节点;
17.问:redis分片集群中的数据如何存储和读取(24/02/20)
答:
1.redis分片集群引入了哈希槽的概念,redis集群共有16384个哈希槽
2.这16384个哈希槽会分配到不同的主节点
3.存储读取数据:根据key的有效部分计算哈希值,对16384取余,余数作为哈希槽的插槽,寻找插槽所在的主节点存储或读取数据;
18.解释一下I/O多路复用模型(24/02/20)
答:
redis除了持久化以外,基本上是纯内存操作的,执行速度非常快,因此它的性能瓶颈主要是网络延迟而不是执行速度,I/O多路复用模型就实现了高效的网络请求;
I/O多路复用模型就是利用单个线程来监听多个socket,并在某个或多个socket可读可写时得到通知,从而避免无效的等待,充分的利用CPU资源;通知的实现方式有:1.select,2.poll,3.epoll;
目前I/O多路复用都是采用的epoll模式,它在通知用户进程socket就绪的同时,还把就绪的socket写入了用户空间,不需要像select,poll一样遍历socket集合进行判断,提升了性能;
(
select的执行流程:
阶段一:
1.用户进程调用select,指定要监听的socket集合
2.内核就会监听对应的多个socket
3.当某个或是多个socket数据就绪的时候返回readable
4.在此过程中用户进程阻塞
阶段二:
1.用户线程找到就绪的socket
2.依次调用recvfrom读取数据
3.内核数据拷贝到用户空间
4.用户进程处理数据
差异:
select和poll只会通知用户进程Socket就绪了,但是不确定是哪个Socket,需要用户进线自己遍历来确认;
epoll在通知用户进程Socker就绪的同时,会把以及就绪的Socket写入用户空间
)
19.问:redis网络模型(24/02/20)
答:
就是使用I/O多路复用模型结合事件处理器来处理多个socket请求
连接应答处理器
命令回复处理器,在redis6.0之后,为了提升更好的性能,就是用了多线程来回复事件
命令请求处理器,在redis6.0之后,将命令的转换使用了多线程,增加了命令转换速度,在命令执行的时候依旧采用的单线程;
20.问:QuickList的特点(24/03/01)
答:
是一个节点为ZipList的双向链表;
节点采用的是ZipList,解决了传统链表的内存占用问题;
空调了ZipList大小,解决连续内存空间申请的效率问题
中间的节点可以压缩,进一步节省了内存
21.问:SkipList的特点(24/03/01)
答:
跳表是一个双向链表,每个节点都包含score和element的值;
节点根据scope值排序,score值一样则按照element字典排序;
每个节点都可以包含多层指针,层数是1-32之间的随机数(由redis底层的算法决定)
不同层指针到下一个节点的跨度不同,层级越高跨度越大
增删改查效率与红黑树基本一致,实现却更加简单
(四).Mybatis
1.问:mybatis的执行流程(24/02/21)
答:
1.读取Mybatis配置文件,mybatis-config.xml加载运行环境和映射文件
2.构造会话工厂SqlSessionFactory
3.会话工厂创建SqlSession对象(包括了执行Sql语句的所有方法)
4.操作数据库的接口,Executor执行器,同时维护查询缓存
5.Executor接口的执行方法中有一个MappedStatement类型的参数,封装了映射信息
6.输出参数映射
7.输出结果映射
2.问:mybatis是否支持延迟加载(24/02/21)
答:
mybaits支持一对一关联对象和一对多关联集合的延迟加载;在mybatis的配置文件中,可以配置是否启动延迟加载(lazyLoadingEnabled=true|false),默认 情况下是关闭的
3.问:延迟加载实现的原理(24/02/21)
答:
使用CGLB创建目标对象的代理对象;当调用目标方法的时候,会进入拦截器invoke方法,发现目标方法的值是null,就会执行sql进行查询;获取数据之后,调用set方法设置属性值,再次调用目标方法就有值了
4.问:mybaits的一级,二级缓存(24/02/21)
答:
一级缓存:基于PerpetualCache的HashMap本地缓存,其作用域为SqlSession,当SqlSession进行flush或是close之后该SqlSession中的所有缓存就会清空,默认打开了一级缓存
二级缓存:基于namespace和mapper的作用域起作用的,不依赖与SqlSession,默认也是采用的PerpetualCache的HashMap本地缓存,需要手动开启
(
注意:
1.对于缓存的更新机制,当某一个作用域(一级缓存Session/二级缓存Namespaces)进行了新增,修改,删除操作后,该默认作用域下所有的select中的缓存就清空了
2.二级缓存需要缓存的数据实现Serializable接口
3.只有会话提交或是结束之后,一级缓存中的数据才会转移到二级缓存中
)
5.mybaits中#{}和${}的区别
答:
mybaits中#{}和${}都是去实现动态SQL的一种方式,通过这两种方式可以把参数传递到XML中,在传递以后,在执行操作之前mybatis会对这两个占位符进行动态的解析
#{}占位符等同于jdbc中的?号占位符,它相当于向PreparedStatement的里面的预处理语句设置参数,而PreparedStatement里面的SQL语句是预编译的,那么SQL语句中使用了占位符,规定了SQL语句的一个结构,并且在设置参数的时候,如果有特殊字符会自动进行转义,所以#号占位符可以防止SQL注入,而使用$的方式呢,相当于直接把参数拼接到了原始SQL中,Mybatis不会对它进行任何的特殊处理,所以#和$最大的区别在于前者是占位符,后者是动态的参数,动态参数无法防止SQL注入的一个问题,所以在实际中应该尽可能的去使用#号占位符,另外$符号的动态传参可以适合应用在一些动态的SQL场景里面,比如说动态的传递表名,或是动态设置排序字段等等;
(五).SpringCloud
1.问:Spring Cloud 5大组件有哪些?(24/02/25)
答:
注册中心:Eureka
负载均衡:Ribbon
远程调用:Feign
服务熔断:Hystrix
网关:Gateway
2.问:服务的注册和发现是什么意思(SpringCould如何实现的服务的注册和发现)(24/02/25)
答:
当时的采用nacos来作为的注册中心;
服务的注册指的是 服务的提供者将自身的信息注册到nacos,由nacos来保存这些信息,比如服务的名称,ip,端口
服务的发现指的是 消费者向nacos拉取服务列表信息,如果服务提供者有多个的话,就会采用负载均衡算法选择一个发起调用
服务的提供者需要每隔三十秒向nacos发送一次心跳,报告健康状态,如果nacos 90秒没有收到某个服务提供者的心跳,那么就会把它从nacos服务列表中剔除
3.问:nacos和eureka的区别(24/02/25)
答:
nacos和eureka的共同点:
都能作为注册中心,支持服务的注册和发现
都支持服务提供者通过心跳的方式做健康检查
区别:
nacos支持服务端的主动检测服务提供者的状态,临时实例采用的是心跳模式,非临时实例采用的是主动检查;临时实例心跳不正常会被剔除,非临时实例不会被剔除
nacos支持服务列表变更是的消息推送模式,使服务列表更新的更加及时
nacos集群默认采用的AP模式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP模式
nacos还支持配置中心,eureka只能作为注册中心
4.问:Ribbon负载均衡的策略有哪些?(24/02/25)
答:
RoundRobinRule:简单的轮询服务列表选择服务器
WeightedResponseTimeRule:按照权重来进行选择服务器,响应越长,权重越小
RandomRule:随机选择一个可用的服务器
ZoneAvoidanceRule:以区域可用的服务器为基础进行服务器的选择,使用Zone对服务器进行分类,这个Zone可以理解为一个机房,对Zone内的多个服务进行轮询
5.问:你们项目的负载均衡如何实现的(24/02/25)
答:
我们在使用feign远程调用的过程中,底层的负载均衡就是使用的Ribbon
6.问:如何实现自定义的负载均衡策略(24/02/25)
答:
1.创建类实现IRule接口,可以指定负载均衡策略
2.在客户端的配置文件中,可以配置一个服务调用的负载均衡策略
7.问:什么是服务雪崩,怎么解决这个问题(24/02/25)
答:
服务雪崩:一个服务失败,导致整条服务链路都失败的情况
服务降级:服务自我保护的一种方式,或者保护下游服务的一种方式,用于确保服务不会受请求突增影响变得不可用,确保服务不会崩溃
服务熔断:默认关闭的,需要手动打开,如果检测到10s内请求的失败率超过50%,那么就会触发熔断机制,之后每隔5s重新请求微服务,如果请求可达,就会关闭熔断机制,恢复正常请求,否则就会继续走熔断机制
8.问:如何监控微服务(24/02/25)
答:
可以使用skywalking 来进行微服务的监控,skywalking可以监控接口,服务,物理实例的一些状态,特别是在进行压测的时候可以看到众多服务的哪些服务和接口比较慢,对其进行针对性的分析和优化;
可以设置告警规则,在服务上线后如果保存,可以设置给相关的负责人发送短信和邮件,就能第一时间知道项目的bug情况,第一时间进行修复
9.问:有没有做过限流?怎么做的?(24/02/25)
答:
。。。
nginx限流
控制速率,使用漏桶算法来实现过滤,让请求以固定的速率进行处理,可以应对突发流量
控制并发连接数,限制单个ip的连接数和并发连接的总数
gateway限流
在spring cloud gateway中支持局部过滤器RequestRateLimiter来做限流,使用的是令牌桶算法
在根据ip或路径进行限流,可以设置每秒的平均速率,和令牌桶对的总容量
10.问:解释一下CAP和BASE(24/02/25)
答:
CAP指的是一致性,可用性,分区容错性
分布式系统节点通过网络连接,一定会出现分区问题,当分区问题出现的时候,系统的一致性和可用性就无法同时满足
BASE理论:基本可用,软状态,最终一致,解决分布式问题的思想和模型:
最终一致思想:各分支事务分别执行并提交,如果有不一致的情况,再想办法恢复数据
强一致思想:各分支事务执行完业务不要提交,要等待彼此的结果,让后进行统一提交或是回滚
11.问:分布式事务的解决方案(24/02/25)
答:
Seata
XA模式:CP,需要等待各个分支事务提交,可以保证强一致,性能差
AT模式:AP,底层采用undo log实现,性能好
TCC模式:AP,性能好,不过需要进行手动编码
MQ:在A服务进行写数据的时候,在同一个事务内将消息发送到另一个事务,异步,性能好
12.问:xxl-job的路由策略有哪些(24/02/25)
答:
xxl-job提供了很多路由策略,我们平时使用较多的有:轮询,故障转移,分片广播
13.问:xxl-job任务执行失败怎么解决(24/02/25)
答:
路由策略选择故障转移,使用其他健康的实例来执行任务
设置重试次数
查看日志+邮件警告来通知相关负责人
13.问:如果有大数据量的任务同时都需要执行,怎么解决(xxl-job)(24/02/25)
答:
让多个实例一块去执行,路由策略为分片广播
在任务执行的代码中可以获取分片总数和当前分片数,按照取模的方式分摊到各个实例执行
(六).RabbitMq
1.问:RabbitMQ-如何确保消息不丢失(24/02/25)
答:
开启生产者确实机制,确保生产者的消息能够到达队列
开启持久化(交换机,队列,消息)功能,去报消息在队列中不会丢失
开启消费者确认机制为auto,由spring确认消息处理成功后完成ack
开启消费者失败重试机制,多次重试失败后将消息投递到异常交换机,交由人工处理
2.问:RabbitMQ的消息重复消费问题如何解决(24/02/25)
答:
给每条消息设置唯一的id,处理之前去数据库中查询是否存在,存在则不处理,不存在就处理消息,然后在写入数据库;
或是幂等方案(分布式锁,数据库锁(悲观锁,乐观锁))
3.问:RabbitMQ中的死信交换机(RabbitMQ延迟队列有了解嘛)(24/02/25)
答:
(
当一个队列中的消息满足下列情况之一时,可以成为死信:
消费者使用basic.reject 或 basic.nack声明消费失败。并且消息的requeue参数设置为false
消息是一个过期消息,超时无人消费
要投递的消息队列队列满了,最早的消息就会成为死信
如果该队列配置了dead-letter-exchange熟悉,指定了一个交换机,那么队列中的死信就会投递到这个交换机中,这个交换机就被称为死信交换机
TTL,也就是Time-To-Live,如果一个队列中的消息TTL结束任没消费,就会变成死信,TTL超时超时的情况:1.消息所在的队列设置了存活时间2.消息本身设置了存活时间
)
当时在。。。业务中使用了延迟队列,就是使用了RabbitMQ来实现 的
RabbitMQ中可以给队列绑定死信交换机,当消息超时未处理就会变成死信,让后发送到死信交换机,私信交换机又可以绑定其他队列;在我们发送消息的时候就可以给消息设定一个TTL,这样就实现了延迟队列了
当然RabbitMQ中还有一种实现延迟队列的方式,就是按照插件,然后在创建交换机的时候加一个x-delay的头指定这个队列为延迟队列,那么发送到这个交换机的消息就会延迟指定的时间才会被转发到队列,从而实现延迟队列的效果
4.问:RabbitMQ如果有100万消息堆积在MQ,如何解决(24/02/25)
答:
增加更多的消费者,提高消费速度
在消费者内开启线程池加快消息处理速度
扩大队列容积,提高堆积上线,采用惰性队列
在声明队列的时候设置属性x-queue-mode为lazy,即为惰性队列
基于磁盘存储,消息上限高
性能比较稳定,但受限于磁盘存储,受限于磁盘IO,时效性会降低
5.问:RabbitMQ五个常用的模式(24/02/25)
答:
-
简单模式(Simple Mode):
简单模式是最基本的消息传递模式,它包括一个生产者和一个消费者。生产者将消息发送到一个队列中,消费者从队列中接收并处理消息。这种模式适用于简单的应用场景,其中消息的顺序和可靠性不那么重要。 -
工作队列模式(Work Queue Mode):
工作队列模式也称为任务队列模式,它包括一个或多个生产者和多个消费者。生产者将消息发送到一个任务队列中,多个消费者并发地从队列中接收任务并进行处理。这种模式可以实现消息的负载均衡和并发处理,适用于多个消费者共同处理任务的场景。 -
发布/订阅模式(Publish/Subscribe Mode):
发布/订阅模式通过交换机(Exchange)来进行消息的广播。生产者将消息发送到交换机,交换机将消息广播到与之绑定的所有队列,每个队列都有一个对应的消费者进行接收和处理。这种模式适用于需要将消息广播给多个消费者的场景,如日志发布、实时广播等。 -
路由模式(Routing Mode):
路由模式通过交换机和路由键(Routing Key)来选择性地将消息发送到特定的队列。生产者将消息发送到交换机,并指定一个路由键,消费者通过绑定队列和路由键的方式来接收特定的消息。这种模式适用于根据消息的特性或目标进行有选择地消费的场景。 -
主题模式(Topic Mode):
主题模式是路由模式的一种扩展,它通过使用通配符来进行更灵活的消息路由。生产者将消息发送到交换机,并指定一个主题(Topic),消费者通过绑定队列和主题的方式来接收匹配的消息。通配符支持模糊匹配,可以根据消息的内容、属性等进行灵活的路由。这种模式适用于具有复杂路由规则的场景。
(七).Kafka
1.问:Kafka如何保证消息不会丢失(24/02/25)
答:
需要从三个层面去解决这个问题:
生产者发送消息到Brocker丢失:
设置异步发送,发送失败使用回调进行 记录或是重发;
失败重试,参数配置,可以设置重试次数
消息在Broker中存储丢失
发送确认acks,选择all,让所有的副本都参与保存数据后确认
消息者从Broker接收消息丢失
关闭自动提交偏移量,开启手动提交
提交方式,最好是同步+异步提交
2.问:Kafka是如何保证消费的顺序性(24/02/25)
答:
(
问题原因:一个topic的数据可能存储在不同的分区中,每一个分区都有一个按照顺序的存储的偏移量,如果消费者关联了多个分区就无法保证顺序性
)
kafa默认的存储和消费消息,由于一个topic数据可能存储在多个分区中,每一个分区都一个按照顺序存储的偏移量,如果消费者关联了多个分区就无法保证消息消费的顺序性
解决这个的话有两种方案:1.发送消息是指定分区号,2.对于相同业务的消息设置同样的key,最终也能是消息到达同一个分区
(八).计网
1.问:TCP三次握手和四次挥手(24/02/28)
答:
三次握手:
第一次握手:客户端发送一个SYN包给服务器,请求建立连接,发送后客户端就会进入SYN_SEND状态;
第二次握手:服务器收到了客户端的SYN包,就会发送一个SYN/ACK包进行应答,发送后客户端就会进入SYN_RECV状态
第三次握手:客户端收到服务器的SYN/ACK包之后,就会向服务器发送一个ACK包,表示握手成功,此时服务器和客户端就都进入了已建立连接(ESTABLISHED)状态了,就可以进行数据的传输了
四次挥手:
第一次挥手:客户端关闭连接时,会给服务器发送一个FIN包,然后进入FIN_WAIT_1状态;
第二次挥手:服务器收到FIN包后,会发送一个ACK包给客户端,然后服务器进入CLOSE_WAIT状态,客户端进入FIN_WAIT_2状态
第三次挥手:服务器关闭连接时会发送一个FIN包给客户端,然后服务器进入LAST_ACK状态
第四次挥手:客户端收到FIN包后,发送一个ACK包给服务器,此时客户端处于TIME_WAIT状态,然后会等待一段时间关闭,确保服务器收到ACK包,然后关闭
2.问:TCP和UDP的区别
答:
TCP是有连接的,UDP是无连接的
TCP是可靠的,UDP是不可靠的
TCP是基于点对点通信的,UDP支持一对一,一对多,多对多;
TCP的首部20个字节,UDP的首部8个字节
TCP有拥塞机制,UDP没有
TCP协议下双方发送接收缓冲器都有,UDP并无实际意义上的发送缓冲区,但是存在接收缓冲区;
3.问:TCP和HTTP的区别
答:
TCP是传输层的协议,主要解决的是数据如何在网络中传输,而HTTP是应用层的协议,主要解决的如何包装数据
4.问:HTTP和HTTPS的区别
答:
传输信息的安全性:
http是超文本传输协议,信息是明文传输的,攻击者截取了web浏览器和网站服务器之间的传输报文,就可以读懂其中的信息;
https是安全的ssl加密传输协议,为浏览器和服务器之间的通信加密,确保数据传输的安全;
连接方式不同:
http协议是无状态的
https是由ssl+http协议构建的可以进行加密传输,身份认证的网络协议;
端口不同:
http默认是80端口
https默认是443端口
证书的申请方式不同
http协议:免费申请
https协议需要导ca申请证书,一般免费证书很少,需要缴费