**
面试题总结(简略回答,方便记忆以及面试回答)
**
计算机网络
-
什么时候选择 TCP,什么时候选 UDP?
答:
UDP 一般用于即时通信,比如: 语音、 视频 、直播等等。这些场景对传输数据的准确性要求不是特别高。
TCP 用于对传输准确性要求特别高的场景,比如文件传输、远程登录等等。 -
HTTP 基于 TCP 还是 UDP?
HTTP 3.0 之前是基于 TCP协议的,而 HTTP3.0 将弃用 TCP,改用 基于 UDP 的 QUIC 协议 。
此变化主要为了解决HTTP/2 中存在的队头阻塞问题。由于 HTTP/2 在单个 TCP 连接上使用了多路复用,受到 TCP 拥塞控制的影响,少量的丢包就可能导致整个 TCP 连接上的所有流被阻塞。 -
get和post区别
1.get请求一般是去取获取数据(其实也可以提交,但常见的是获取数据);
post请求一般是去提交数据。
2.get因为参数会放在url中,所以隐私性,安全性较差,请求的数据长度是有限制的,
不同的浏览器和服务器不同,一般限制在 2~8K 之间,更加常见的是 1k 以内;
post请求是没有的长度限制,请求数据是放在body中。
-
使用 TCP 的协议有哪些?使用 UDP 的协议有哪些?
运行于 TCP 协议之上的协议 :http,https,ssh
运行于 UDP 协议之上的协议 :dns -
ping命令
-
TCP三次握手和四次挥手
tcp三次握手和四次挥手相关问题 -
TCP与UDP的区别
是否面向连接 :UDP 在传送数据之前不需要先建立连接。而 TCP 提供面向连接的服务,在传送数据之前必须先建立连接,数据传送结束后要释放连接。
是否是可靠传输:远地主机在收到 UDP 报文后,不需要给出任何确认,并且不保证数据不丢失,不保证是否顺序到达。TCP 提供可靠的传输服务,TCP 在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制。通过 TCP 连接传输的数据,无差错、不丢失、不重复、并且按序到达。
是否有状态 :TCP 传输是有状态的,这个有状态说的是 TCP 会去记录自己发送消息的状态比如消息是否发送了、是否被接收了等等。为此 ,TCP 需要维持复杂的连接状态表。而 UDP 是无状态服务,简单来说就是不管发出去之后的事情了。
是否提供广播或多播服务 :TCP 只支持点对点通信,UDP 支持一对一、一对多、多对一、多对多。 -
TCP 如何保证传输的可靠性?
1.对失序数据包重新排序以及去重:TCP 为了保证不发生丢包,就给每个包一个序列号,有了序列号能够将接收到的数据根据序列号排序,并且去掉重复序列号的数据就可以实现数据包去重。
2.校验和 : TCP 将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段。
3.超时重传 : 当发送方发送数据之后,它启动一个定时器,等待目的端确认收到这个报文段。接收端实体对已成功收到的包发回一个相应的确认信息(ACK)。如果发送端实体在合理的往返时延(RTT)内未收到确认消息,那么对应的数据包就被假设为已丢失open in new window并进行重传。
4.流量控制 : TCP 连接的每一方都有固定大小的缓冲空间,TCP 的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议(TCP 利用滑动窗口实现流量控制)。
5.拥塞控制 : 当网络拥塞时,减少数据的发送。
- 流量控制
如果缓存区满了发送方还在狂发数据的话,接收方只能把收到的数据包丢掉。出现丢包问题的同时又疯狂浪费着珍贵的网络资源。因此,我们需要控制发送方的发送速率,让接收方与发送方处于一种动态平衡才好。
接收方会通过确认号和自己的接收窗口大小(在TCP报文中体现为确认号和窗口)来告知发送方,发送方会通过确认号和窗口大小来构造自己的发送窗口(容量有限,这会使得发送方不会随意发送消息)。
这里通过修改窗口大小就可以减慢发送方的流量输出。
- 拥塞控制
在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏。这种情况就叫拥塞。拥塞控制就是为了防止过多的数据注入到网络中,这样就可以使网络中的路由器或链路不致过载。
拥塞控制是一个全局性的过程,涉及到所有的主机,所有的路由器,以及与降低网络传输性能有关的所有因素。相反,流量控制往往是点对点通信量的控制,是个端到端的问题。
慢开始和拥塞控制
慢开始: 慢开始算法的思路是当主机开始发送数据时,如果立即把大量数据字节注入到网络,那么可能会引起网络阻塞,因为现在还不知道网络的符合情况。经验表明,较好的方法是先探测一下,即由小到大逐渐增大发送窗口,也就是由小到大逐渐增大拥塞窗口数值。cwnd 初始值为 1,每经过一个传播轮次,cwnd 加倍。
拥塞控制:当cwnd大于一个门限值的时候,会进行拥塞控制。拥塞避免算法的思路是让拥塞窗口 cwnd 缓慢增大,即每经过一个往返时间 RTT 就把发送方的 cwnd 加 1.。
如果出现了超时会接收到确认的情况,会重新进入慢开始阶段。
快重传和快恢复
快重传:如果接收方收到了一个不按顺序到答的报文(这意味着有前面报文的丢失),接收方为了怕发送方认为网络出现了阻塞,就会快速重传确认号(告诉发送方缺失的序号,核心是要快,尽快通知接收方不是出现了网络阻塞,而是个人数据丢失)。此时,如果发送方接收了3个相同的ACK消息,则认为接受方出现了快重传,即此时网络可能不是真的阻塞,而是只出现了个别数据丢失,因此发送方此时会进入快恢复阶段。
-
http状态码
-
http1.0和http2.0的区别
-
URI 和 URL 的区别是什么?
URI(Uniform Resource Identifier) 是统一资源标志符,可以唯一标识一个资源。
URL(Uniform Resource Locator) 是统一资源定位符,可以提供该资源的路径。它是一种具体的 URI,即 URL 可以用来标识一个资源,而且还指明了如何 locate 这个。
-
linux下常见命令
netstate 查看网络状态
ps查看进程状态
curl发送http命令
jstat可以在linux下查看gc情况
top可以查看CPU率高的进程
top -H -p pid可以查看pid进程下的线程CPU使用率 -
https进行SSL单向认证的全过程
-
DNS
查询过程:
- QPS和TPS
操作系统
-
进程的状态
-
PV操作
-
生产者消费者模型
acquire相当于P操作申请资源,若资源为0则等待;
release相当于V操作释放资源并唤醒等待的线程。
关键是需要两种信号量。
-
进程间的通信方式
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)
1.匿名管道(Pipes) :用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。
2.有名管道:有名管道严格遵循先进先出(first in first out)。有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。
3.信号量(Semaphores) :信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信方式主要用于解决与同步相关的问题并避免竞争条件。
4.共享内存(Shared memory) :使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作如信号量等。
5.套接字(Sockets) : 此方法主要用于在客户端和服务器之间通过网络进行通信。
6.信号(Signal) :信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
7.消息队列 -
线程间的同步的方式
1.互斥量(Synchronized或者Lock)
2.事件(wait/notify)
3.信号量 -
进程调度算法
先到先服务(FCFS)调度算法 : 从就绪队列中选择一个最先进入该队列的进程为之分配资源。
短作业优先(SJF)的调度算法 : 从就绪队列中选出一个估计运行时间最短的进程为之分配资源
时间片轮转调度算法 : 每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。
优先级调度 : 为每个流程分配优先级,首先执行具有最高优先级的进程。
多级反馈队列调度算法 :多级反馈队列调度算法既能使高优先级的作业得到响应又能使短作业(进程)迅速完成。,因而它是目前被公认的一种较好的进程调度算法,UNIX 操作系统采取的便是这种调度算法。 -
死锁的四个必要条件
-
解决死锁的方法
悲观锁机制:(银行家算法)当一个进程申请使用资源的时候,银行家算法 通过先 试探 分配给该进程资源,然后通过 安全性算法 判断分配后系统是否处于安全状态,若不安全则试探分配作废,让该进程继续等待,若能够进入到安全的状态,则就 真的分配资源给该进程。乐观锁机制:死锁的检测和解除,当死锁发生才去处理。逐个撤销涉及死锁的进程,回收其资源直至死锁解除。
-
操作系统内存管理机制
块式管理 : 将内存分为几个固定大小的块,每个块中只包含一个进程。如果程序运行需要内存的话,操作系统就分配给它一块,如果程序运行只需要很小的空间的话,分配的这块内存很大一部分几乎被浪费了。
页式管理 :把主存分为大小相等且固定的一页一页的形式,页较小,相比于块式管理的划分粒度更小,提高了内存利用率,减少了碎片。页式管理通过页表对应逻辑地址和物理地址。
段式管理 : 页式管理虽然提高了内存利用率,但是页式管理其中的页并无任何实际意义。 段式管理把主存分为一段段的,段是有实际意义的,每个段定义了一组逻辑信息,例如,有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。 段式管理通过段表对应逻辑地址和物理地址。
段页式存储思想:首先将进程按照逻辑分段,再将各段分页。再将内存划分为大小相同的内存块,将各页加载到内存中去。
-
虚拟内存
虚拟内存 使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。 -
页表
页表就是一个用于将虚拟地址转换为物理地址的工具。
转换的公式就是: 通过页表先找到页,在使用页内偏移地址找到最终对应的实际物理内存。
在页式内存管理中有两个重要的问题
1.虚拟物理地址到物理地址转换比较慢
2.当虚拟空间很大的时候页表也会变的很大
所以为了解决第一个问题就有了快表
所以为了解决第二个问题就有了多级页表
- 快表
我们可以把快表理解为一种特殊的高速缓冲存储器(Cache),其中的内容是页表的一部分或者全部内容。作为页表的 Cache,它的作用与页表相似,但是提高了访问速率。由于采用页表做地址转换,读写内存数据时 CPU 要访问两次主存。有了快表,有时只要访问一次高速缓冲存储器,一次主存,这样可加速查找并提高指令执行速度。
使用快表之后的地址转换流程是这样的:
①根据虚拟地址中的页号查快表;
②如果该页在快表中,直接从快表中读取相应的物理地址;
③如果该页不在快表中,就访问内存中的页表,再从页表中得到物理地址,同时将页表中的该映射表项添加到快表中;
④当快表填满后,又要登记新页时,就按照一定的淘汰策略淘汰掉快表中的一个页。
- 多级页表
如果只有一级页表那么4G的虚拟地址空间就需要4M的连续内存来存放页表,开销太大
这时候就出现了多级页表,用来节约内存
使用二级页表的话,就只需要一个4k的页表来管理1024个二级页表,这时候你会有疑问其实消耗的内存更大了,
其实不然,这里的内存不是都要放在内存里面的,根据局部性原理只有一级页表在内存,二级页表都存放在外部磁盘。只有当缺页中断的时候才会调用进去内存中。
- 页面置换算法
当发生缺页中断时,如果当前内存中并没有空闲的页面,操作系统就必须在内存选择一个页面将其移出内存,以便为即将调入的页面让出空间。用来选择淘汰哪一页的规则叫做页面置换算法,我们可以把页面置换算法看成是淘汰页面的规则。
①FIFO(First In First Out) 页面置换算法(先进先出页面置换算法) : 总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面进行淘汰。
②LRU (Least Recently Used)页面置换算法(最近最久未使用页面置换算法) :LRU 算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 T,当须淘汰一个页面时,选择现有页面中其 T 值最大的,即最近最久未使用的页面予以淘汰。
③LFU (Least Frequently Used)页面置换算法(最少使用页面置换算法) : 该置换算法选择在之前时期使用最少的页面作为淘汰页。 - 阻塞与非阻塞 I/O VS 同步与异步 I/O
I/O一般需要两个过程:
从硬件读过内核缓冲区(DMA);
从内核缓冲区读到应用缓冲区(操作系统线程)。
阻塞I/O:
线程同步等待两个过程。
非阻塞IO:
主动轮询第一个过程是否完成;第二个过程会同步等待。
更为高级的就是第一个过程直接进阻塞态,内核区数据准备好操作系统进程来唤醒第一个过程。第二个过程会同步等待。(这也是BIO的实际体现,与NIO的核心区别就是一个线程管理一个连接)
- IO多路复用(NIO实际体现)
- 真正的异步IO(AIO)
两个过程均不需要同步等待。
juc
-
同步和异步的区别
同步 : 发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。
异步 :调用在发出之后,不用等待返回结果,该调用直接返回。 -
线程的六种状态
NEW: 初始状态,线程被创建出来但没有被调用 start() 。
RUNNABLE: 运行状态,线程被调用了 start()等待运行的状态。
BLOCKED :阻塞状态,需要等待锁释放。
WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
TERMINATED:终止状态,表示该线程已经运行完毕。
-
线程和进程的根本区别
进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。每个进程都有独立的代码和数据空间,同一类线程共享代码和数据空间。 -
linux下查看线程
top命令可以查看占CPU最高的线程。 -
局部变量是否线程安全?
-
锁膨胀
sychronized
偏向锁->轻量锁->重量锁
(当第一次有线程加锁时,(没禁用偏向锁)偏向锁会指向该线程;然后如果该线程释放锁后,又有其他线程加锁,那么就会升级成轻量级锁;如果锁还没释放,有其他线程抢占锁,那么锁会变成重量锁。)
对象头mark word结构
当没禁用偏向锁:
当禁用偏向锁:
当加锁后直接变成了轻量锁。
- 偏向锁
偏向锁的引入主要是为了解决轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
批量重偏向:
当20个对象发生偏向锁撤销变成轻量级锁之后,jvm会采取批量重偏向,就是偏向锁并不升级成轻量级锁,而是只改变偏向锁的偏向(由原先的线程偏向现在加锁的线程)而已。
批量撤销:
当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的(也就是加锁直接加轻量级锁,当轻量级锁释放后,重新变为不可偏向状态)。
- 轻量级锁
加锁前:
加锁后:
使用cas替换:
-
重量级锁
自旋优化:
自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。 -
sleep(long n) 和 wait(long n) 的区别
- sleep 是 Thread 方法,而 wait 是 Object 的方法
- sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
- sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
- 它们状态 TIMED_WAITING
-
prak和unpark
-
检查死锁(CPU占用率高的线程)
-
活锁
-
interrupt
每个线程变量调用interrupt方法,比如(t1.interrupt())。
1.如果线程处于被wait或者time_wait状态(例如处于sleep, wait, join 等状态),那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常(isInterrupt仍为false)。
2.如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true(设置当前线程的isInterrupt为true)。被设置中断标志的线程将继续正常运行,不受影响。
- ReentrantLock
可重入
可打断
公平锁
公平锁一般没有必要,会降低并发度
锁超时
不能获取锁立刻失败
超时失败
条件变量
这两个函数的用法和wait,notify差不多。只不过把控制力度更精细。
- 指令重排
源代码–>编译器优化重排–>指令并行重排–>内存系统重排–>最终执行指令。
单线程环境中,可以确保最终执行结果和代码顺序执行的结果一致。
但是多线程环境中,线程交替执行,由于编译器优化重排的存在,两个线程使用的变量能否保持一致性是无法确定的,结果无法预测。
- volatile
volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
对 volatile 变量的写指令后会加入写屏障
对 volatile 变量的读指令前会加入读屏障
写屏障:
写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中。
写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后。
读屏障:
读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据。
读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前。
因此volatile可以保证可见性和有序性,但不能保证原子性。
sychronized保证可见性和有序性,原子性。
-
happens-before 原则理解
happens-before 原则的设计思想其实非常简单:
为了对编译器和处理器的约束尽可能少,只要不改变程序的执行结果(单线程程序和正确执行的多线程程序),编译器和处理器怎么进行重排序优化都行。
对于会改变程序执行结果的重排序,JMM 要求编译器和处理器必须禁止这种重排序。 -
cas无锁
为什么无锁效率高?
无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞,这都会耗费性能。(多核CPU才能发挥CAS优势,最好是线程数等于CPU数)
体现:
原子整数,原子引用,原子数组等。。
-
cas和锁(sychronized)的对比
cas是一种乐观锁的思想,认为其他线程修改了也没关系,就重试一次。(其更适合线程少,cpu数多的情况,如果重试经常出现,会极大影响效率)
sychronized是一种悲观锁的思想,拿锁直接不允许其他线程作任何修改。 -
线程池
实际的线程池类一般为ThreadPoolExecutor
-
常见线程池
1.定长线程池(FixedThreadPool)
构造函数
public static ExecutorService newFixedThreadPool(int nThreads) {return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>(),threadFactory);
}
只有核心线程,线程数量固定,当所有线程活跃时,新的任务会放在队列中等待(队列无限大)
2.定时线程池(ScheduledThreadPool )
private static final long DEFAULT_KEEPALIVE_MILLIS = 10L;public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {super(corePoolSize, Integer.MAX_VALUE,DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,new DelayedWorkQueue());
}public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory) {return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
public ScheduledThreadPoolExecutor(int corePoolSize,ThreadFactory threadFactory) {super(corePoolSize, Integer.MAX_VALUE,DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,new DelayedWorkQueue(), threadFactory);
}
核心线程数量固定,非核心线程数量无限,执行完闲置 10ms 后回收,任务队列为延时阻塞队列。
使用:
// 1. 创建 定时线程池对象 & 设置线程池线程数量固定为5
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){public void run() {System.out.println("执行任务啦");}
};
// 3. 向线程池提交任务
scheduledThreadPool.schedule(task, 1, TimeUnit.SECONDS); // 延迟1s后执行任务
scheduledThreadPool.scheduleAtFixedRate(task,10,1000,TimeUnit.MILLISECONDS);// 延迟10ms后、每隔1000ms执行任务
3.可缓存线程池(CachedThreadPool)
public static ExecutorService newCachedThreadPool() {return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>(),threadFactory);
}
无核心线程,非核心线程数量无限,执行完闲置 60s 后回收,任务队列为不存储元素的阻塞队列
4.单线程化线程池(SingleThreadExecutor)
public static ExecutorService newSingleThreadExecutor() {return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>(),threadFactory));
}
只有 1 个核心线程,无非核心线程
5.对比
- 线程池4种拒绝策略
1、AbortPolicy
当任务添加到线程池中被拒绝时,它将抛出 RejectedExecutionException 异常。(该策略下,直接丢弃任务,并抛出RejectedExecutionException异常)
2、DiscardPolicy
当任务添加到线程池中被拒绝时,默认情况下它将丢弃被拒绝的任务。(即该策略下,直接丢弃任务,什么都不做)
3、DiscardOldestPolicy
当任务添加到线程池中被拒绝时,线程池会放弃等待队列中最旧的未处理任务,然后将被拒绝的任务添加到等待队列中。
(该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列)
4、CallerRunsPolicy
不进入线程池执行,在这种方式(CallerRunsPolicy)中,任务将由调用者线程去执行。
(用于被拒绝任务的处理程序,它直接在 execute 方法的调用线程中运行被拒绝的任务;如果执行程序已关闭,则会丢弃该任务。) - 线程池的三种队列
1.SynchronousQueue
SynchronousQueue没有容量,是无缓冲等待队列,是一个不存储元素的阻塞队列,会直接将任务交给消费者,必须等队列中的添加元素被消费后才能继续添加新的元素。
(使用SynchronousQueue阻塞队列一般要求maximumPoolSizes为无界(Integer.MAX_VALUE),避免线程拒绝执行操作。)
2.LinkedBlockingQueue
LinkedBlockingQueue是一个无界缓存等待队列。当前执行的线程数量达到corePoolSize的数量时,剩余的元素会在阻塞队列里等待。
3.ArrayBlockingQueue
ArrayBlockingQueue是一个有界缓存等待队列,可以指定缓存队列的大小,当正在执行的线程数等于corePoolSize时,多余的元素缓存在ArrayBlockingQueue队列中等待有空闲的线程时继续执行,当ArrayBlockingQueue已满时,加入ArrayBlockingQueue失败,会开启新的线程去执行,当线程数已经达到最大的maximumPoolSizes时,再有新的元素尝试加入ArrayBlockingQueue时会执行拒绝策略。 - AQS
AQS框架借助于两个类:Unsafe(提供CAS操作)和LockSupport(提供park/unpark操作)
jvm
- 频繁fullGC
原因:
①而且每次Young GC过后存活对象太多,内存分配不合理,Survivor区过小,导致对象频繁进入老年代,频繁触发Full GC。
②一次性加载大数据进入内存,频繁有大对象进入老年代。
③系统发生了内存泄漏,莫名其妙创建大量的对象,始终无法回收,一直占用在老年代里。
排查:
系统在频繁的full gc,但并没有出现oom
1、如果每次gc之后剩余的空间不大,说明有一部分顽固对象一直没法被回收,导致可用内存变少。这种情况下很容易后续出现oom,比如说一次大对象的申请(这是需要考虑出现一些连接是否主动关闭了,因为不主动关闭,这些连接的大对象是不会被回收的)。
2、如果每次gc之后剩余的空间比较大,意味着大部分对象都被清理了,但是系统又在频繁的full gc,说明很快老年代又会涌入大量对象。这个时候就应该检查下jvm的参数配置,很有可能是新生代设置的太小了,导致很多应该在minor gc阶段就清理出去的对象留到了老年代。
- fullGC触发时机
1.调用 System.gc()。
2.老年代空间不足。
在执行Full GC后空间仍然不足,则抛出错误:java.lang.OutOfMemoryError: Java heap space
jstat可以在linux下查看gc情况
-
死亡对象判断方法
① 引用计数法
给对象中添加一个引用计数器:每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1。不能解决循环引用的问题。
② 可达性分析算法
这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。(GCRoots是一组活跃的引用) -
如何判断一个常量是废弃常量?
假如在字符串常量池中存在字符串 “abc”,如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 “abc” 就是废弃常量,如果这时发生内存回收的话而且有必要的话,“abc” 就会被系统清理出常量池了。 -
如何判断一个类是无用的类?
① 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
②加载该类的 ClassLoader 已经被回收。
③该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 -
HotSpot 为什么要分为新生代和老年代?
在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。 -
指定堆内存的jvm参数
① 显式指定堆内存–Xms和-Xmx
②显示新生代内存
③设置老年代与新生代内存的比值
④显式指定永久代/元空间的大小
⑤设置垃圾回收器
-
处理OOM
①将head写入物理文件中。 HeapDumpOnOutOfMemoryError 指示 JVM 在遇到 OutOfMemoryError 错误时将 heap 转储到物理文件中。HeapDumpPath 表示要写入文件的路径; 可以给出任何文件名;
②应该在 cmd args 空间中使用适当的命令。如果我们想在内存不足时重启服务器。
比如内存不足时重启服务器:
-
class文件结构
-
静态常量池和运行时常量池
静态常量池主要存放两大类常量:字面量(Literal) 和 符号引用(Symbolic References)。
字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。
符号引用则属于编译原理方面的概念
- 四种引用
强引用:
即使抛OOM也不会回收对象
软引用:
当内存不足的时候会优先回收掉,而不是抛出OOM。但如果内存够的话,垃圾回收时则不会回收。
弱引用
weakReference和softReference很类似,不同的是weekReference引用的对象只要垃圾回收执行,就会被回收,而不管是否内存不足。
- 虚引用
运行时常量池
运行时常量池是当Class文件被加载到内存后,Java虚拟机会 将Class文件常量池里的内容转移到运行时常量池里。
字符串(字面)常量池在jdk1.7之后由永久代(方法区)转移到了堆中;
剩下的一部分常量池(由符号引用转变的直接引用)则留在了元空间(原永久代)。
-
类加载的过程
加载->链接(验证+准备+解析)->初始化
加载:
首先通过一个类的全限定名来获取此类的二进制字节流;其次将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;最后在java堆中生成一个代表这个类的Class对象,作为方法区这些数据的访问入口。
获取二进制流的方式:
1)从本地系统直接加载
2)通过网络下载.class文件
3)从zip,jar等归档文件中加载.class文件
4)从专有数据库中提取.class文件
5)将Java源文件动态编译为.class文件(服务器)
验证
准备
为类的静态变量分配内存,并将其初始化为默认值;
静态变量会被分配在方法区之中。
解析
把类中的符号引用转换为直接引用
类的初始化
主要是执行类的初始化方法:()方法
-
类的初始化时机
创建类的实例,也就是new一个对象
访问某个类或接口的静态变量,或者对该静态变量赋值
调用类的静态方法
反射(Class.forName(“com.lyj.load”))
初始化一个类的子类(会首先初始化子类的父类)
JVM启动时标明的启动类,即文件名和类名相同的那个类 -
双亲委派机制
-
如何打破双亲委派机制
自定义加载器的话,需要继承 ClassLoader 。如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法。 -
雪花算法
snowflake算法的特性是有序、唯一,并且要求高性能,低延迟(每台机器每秒至少生成10k条数据,并且响应时间在2ms以内),要在分布式环境(多集群,跨机房)下使用保证唯一id。
优点:
(1)高性能高可用:不依赖第三方库或者中间件,完全在内存中生成,可用性强。
(2)容量大:每秒中能生成数百万的自增ID。
(3)ID自增:存入数据库中,索引效率高
缺点:
强依赖机器时钟
java基础
- Integer机制
Integer与int之间完全兼容,相互转换会有自动拆箱和自动装箱
-128–127之间实际上提前有放在常量池中的对象
①对于Integer a=10;这种方式a如果满足-128–127,实际指向的是常量池中的对象,否则则是自己new出对象
②对于Integer a=new Integer(10);这种方式a指向的则是自己new出的对象
Integer与int之间的==比较则会将Integer自动拆箱成int,然后int之间比较。
Integer与Integer比较建议用equals - String
- String,StringBuild,StringBuffer
- HashMap
①HashMap内部的bucket数组长度为什么一直都是2的整数次幂?
第一,可以通过(table.length - 1) & key.hash()这样的位运算快速寻址,
第二,在HashMap扩容的时候可以保证同一个桶中的元素均匀的散列到新的桶中,具体一点就是同一个桶中的元素在扩容后一半留在原先的桶中,一半放到了新的桶中。(均匀散列很好理解,因为容量扩大两倍后,在位置k上的可能放在k+length上)。
②HashMap默认的bucket数组是多大?
默认是16,即时指定的大小不是2的整数次幂,HashMap也会找到一个最近的2的整数次幂来初始化桶数组。
③ HashMap什么时候开辟bucket数组占用内存
在第一次put的时候调用resize方法。
④HashMap何时扩容?扩容成多大?
当HashMap中的元素熟练超过阈值时,阈值计算方式是capacity * loadFactor,在HashMap中loadFactor是0.75,扩大为2*capacity。
⑤桶中的元素链表何时转换为红黑树,什么时候转回链表,为什么要这么设计?
当同一个桶中的元素数量大于等于8的时候元素中的链表转换为红黑树,反之,当桶中的元素数量小于等于6的时候又会转为链表,这样做的原因是避免红黑树和链表之间频繁转换,引起性能损耗。
⑥HashMap如何处理key为null的键值对?
放置在桶数组中下标为0的桶中。 - ConcurrentHashMap
JDK1.8 中的ConcurrentHashMap 选择了与 HashMap 相同的Node数组+链表+红黑树结构;
将锁的级别控制在了更细粒度的哈希桶数组元素级别,也就是说只需要锁住这个链表头节点(红黑树的根节点),就不会影响其他的哈希桶数组元素的读写,大大提高了并发度。
①put方法主要流程:
根据 key 计算出 hash 值;
定位到 Node,拿到首节点 f,判断首节点 f:
如果为 null ,则通过 CAS 的方式尝试添加;
如果为 f.hash = MOVED = -1 ,说明其他线程在扩容,参与一起扩容;
如果都不满足 ,synchronized 锁住 f 节点,判断是链表还是红黑树,遍历插入;
②get方法不加锁
-
ConcurrentHashMap 和 Hashtable 的效率哪个更高?为什么?
ConcurrentHashMap 的效率要高于 Hashtable,因为 Hashtable 给整个哈希表加了一把大锁从而实现线程安全。而ConcurrentHashMap 的锁粒度更低。 -
SPI
Redis
- Redis五种数据结构
①String
没有使用c语言的字符串表示,自己构建了一种 简单动态字符串(Simple Dynamic String,SDS)。
优点:
1.O(1)获得字符串长度。
2.杜绝缓冲区溢出。(会通过检查修改SDS是否会溢出,自动对SDS扩容)。
应用场景
1.需要存储常规数据的场景
举例 :缓存 session、token、图片地址、序列化后的对象(相比较于 Hash 存储更节省内存)。
2.需要计数的场景
②List
Redis 的 List 的实现为一个 双向链表。
应用场景
1.信息流展示
举例 :最新文章、最新动态。
2.消息队列
Redis List 数据结构可以用来做消息队列,只是功能过于简单且存在很多缺陷,不建议这样做。
③Hash
特别适合用于存储对象,后续操作的时候,你可以直接修改这个对象中的某些字段的值。
应用场景
对象数据存储场景。
④Set
应用场景
1.需要存放的数据不能重复的场景
文章点赞、动态点赞等场景。
2.需要获取多个数据源交集、并集和差集的场景。
共同好友(交集)、共同粉丝(交集)、好友推荐(差集)。
⑤Sorted Set
Sorted Set 类似于 Set,但和 Set 相比,Sorted Set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列。
应用场景
需要随机获取数据源中的元素根据某个权重进行排序的场景
举例 :各种排行榜 - String 还是 Hash 存储对象数据更好呢?
String 存储的是序列化后的对象数据,存放的是整个对象。Hash 是对对象的每个字段单独存储,可以获取部分字段的信息,也可以修改或者添加部分字段。
如果对象中某些字段需要经常变动或者经常需要单独查询对象中的个别字段信息,Hash 就非常适合;
String 存储相对来说更加节省内存,如果系统对性能和资源消耗非常敏感的话,String 就非常适合。
绝大部分情况,我们建议使用 String 来存储对象数据即可! - zset
编码可以是ziplist或者skiplist。
同时满足以下条件时使用ziplist编码:
①元素数量小于128个
②所有member的长度都小于64字节。
1.ziplist
当数据量比较小时,采用ziplist。
ziplist 编码的 Zset 使用紧挨在一起的压缩列表节点来保存,第一个节点保存 member,第二个保存 score。ziplist 内的集合元素按 score 从小到大排序,其实质是一个双向链表。虽然元素是按 score 有序排序的, 但对 ziplist 的节点指针只能线性地移动,所以在REDIS_ENCODING_ZIPLIST 编码的 Zset 中, 查找某个给定元素的复杂度为 O(N)。
2.skiplist
这个结构体中包含一个字典和一个跳跃表。跳跃表按 score 从小到大保存所有集合元素,查找时间复杂度为平均 O(logN),最坏 O(N) 。字典则保存着从 member 到 score 的映射,这样就可以用 O(1)的复杂度来查找 member 对应的 score 值。
跳跃表:
上层的节点个数是下层的一半
查找,插入和删除的时间复杂度都应该是O(logN)。
实际上,按照上面生成链表的方式,上面每一层链表的节点个数,是下面一层的节点个数的一半,这样查找过程就非常类似于一个二分查找,使得查找的时间复杂度可以降低到O(log n)。但是,这种方法在插入数据的时候有很大的问题。新插入一个节点之后,就会打乱上下相邻两层链表上节点个数严格的2:1的对应关系。如果要维持这种对应关系,就必须把新插入的节点后面的所有节点(也包括新插入的节点)重新进行调整,这会让时间复杂度重新蜕化成O(n)。删除数据也有同样的问题。
skiplist为了避免这一问题,它不要求上下相邻两层链表之间的节点个数有严格的对应关系,而是为每个节点随机出一个层数(level)。比如,一个节点随机出的层数是3,那么就把它链入到第1层到第3层这三层链表中。
对于每个节点随机层数的算法:
其中
原文连接:https://blog.csdn.net/weichi7549/article/details/107335133/?ops_request_misc=&request_id=&biz_id=102&utm_term=zset%E5%8E%9F%E7%90%86&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduweb~default-3-107335133.142v73insert_down3,201v4add_ask,239v2insert_chatgpt&spm=1018.2226.3001.4187
- Redis 单线程模型
Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型 (Netty 的线程模型也基于 Reactor 模式,Reactor 模式不愧是高性能 IO 的基石)。
Redis 通过 IO 多路复用程序 来监听来自客户端的大量连接。(或者说是监听多个 socket) - Redis6.0之前为什么不使用多线程模型
- Redis 的性能瓶颈不在 CPU ,主要在内存和网络;
- 多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能;
- 单线程编程容易并且更容易维护。
Redis6.0 之后为何引入了多线程?
Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这个算是 Redis 中的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络)。
-
Redis过期的数据的删除策略?
Redis 通过一个叫做过期字典(hash)来保存过期数据,key是数据库中的键,value是其对应的过期时间。
1.惰性删除 :只会在取出 key 的时候才对数据进行过期检查。这样对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。
2.定期删除 : 每隔一段时间抽取一批 key 执行删除过期 key 操作。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。
Redis 采用的是 定期删除+惰性/懒汉式删除 。 -
Redis 内存淘汰机制
当内存不够,加入新的数据时需要考虑将原先的数据删掉(原则是尽量留下热点数据)。
-
Redis 持久化机制
①RDB 持久化:
1.RDB 文件存储的内容是经过压缩的二进制数据, 保存着某个时间点的数据集,文件很小
2.使用 RDB 文件恢复数据,直接解析还原数据即可,不需要一条一条地执行命令,速度非常快
3.RDB 的数据安全性不如 AOF,没有办法实时或者秒级持久化数据。生成 RDB 文件的过程是比繁重的。(虽然 BGSAVE 子进程写入 RDB 文件的工作不会阻塞主线程,但会对机器的 CPU 资源和内存资源产生影响,严重的情况下甚至会直接把 Redis 服务干宕机。)
②AOF持久化
1.AOF 文件存储的是每一次写命令,类似于 MySQL 的 binlog 日志,通常会必 RDB 文件大很多。当 AOF 变得太大时,Redis 能够在后台自动重写 AOF。新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。
2.AOF恢复文件需要依次执行每个写命令,速度非常慢
3.AOF 支持秒级数据丢失(取决 fsync 策略,如果是 everysec,最多丢失 1 秒的数据),仅仅是追加命令到 AOF 文件,操作轻量。 -
Redis事务
Redis 事务在运行错误的情况下,除了执行过程中出现错误的命令外,其他命令都能正常执行。并且,Redis 是不支持回滚(roll back)操作的。因此,Redis 事务其实是不满足原子性的。
除了不满足原子性之外,事务中的每条命令都会与 Redis 服务器进行网络交互,这是比较浪费资源的行为。明明一次批量执行多个命令就可以了,Redis 事务是不建议在日常开发中使用的。
- redis lua脚本
好处:
①减少网络开销。可以将多个请求通过脚本的形式一次发送,减少网络时延。
②原子操作。Redis会将整个脚本作为一个整体执行,中间不会被其他请求插入。因此在脚本运行过程中无需担心会出现竞态条件,无需使用事务。
如果 Lua 脚本运行时出错并中途结束,出错之后的命令是不会被执行的。并且,出错之前执行的命令是无法被撤销的。因此,严格来说,通过 Lua 脚本来批量执行 Redis 命令也是不满足原子性的。
- 分布式锁
第一阶段需要对锁设置过期时间。(解决问题:拿锁后,如果该线程挂掉,将会造成死锁。而如果设置过期时间,该锁则会自动删除)。
第二阶段需要对加锁和设置过期时间设置原子操作。(解决问题:为了防止这两个操作断掉而出现死锁)。
第三阶段需要每个客户端对redis上的锁设置自己的uuid(解决问题:为了防止因为业务时间过长,自己的锁已经失效,而此时其他客户端已经加锁,而将其他客户端的锁删除)对比uuid即可。
第四阶段需要把对比uuid以及删除锁设置为原子操作。(解决问题:为了防止这两个操作之间 redis的锁刚好失效且其他客户端加锁,而将其他客户端锁删除)。
(Redisson的实现中可以加入watchdog看门狗机制来定时对快要过期的锁延时。)
- 缓存穿透
缓存穿透说简单点就是大量请求的 key 是不合理的,根本不存在于缓存中,也不存在于数据库中 。这就导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
解决方法:
①缓存无效key
不足:如果每次都生成不同的key将使得redis缓存大量无效的key,不能根本解决问题。
②布隆过滤器
布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。
-
缓存击穿
缓存击穿中,请求的 key 对应的是 热点数据 ,该数据 存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期) 。这就可能会导致瞬时大量的请求直接打到了数据库上,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
解决方法:
①设置热点数据永不过期或者过期时间比较长。(比如说只需要在秒杀时间段不过期)
②请求数据库写数据到缓存之前,先获取互斥锁,保证只有一个请求会落到数据库上,减少数据库的压力。 -
缓存雪崩
缓存在同一时间大面积的失效,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力。
设置不同的失效时间比如随机设置缓存的失效时间。 -
缓存同步
①双写模式
并发下可能出现脏数据问题:
这需要等缓存过期,才会恢复一致性。
②失效模式
更新数据库直接删除掉缓存中的数据,等下次查询数据库时在更新到缓存。
这种情况也会出现脏数据问题。
③使用canal监听mysql binlog同步更新redis(一致性最强,当然也最耗资源和性能)
总结:
一致性从低到高:
直接设置缓存过期,更新mysql时不同步缓存(事实上大部分场景也可以,可以设置)。
双写模式&失效模式
使用binlog
加锁
事实上,放在缓存中的数据就不应该是实时性要求高的。缓存的初衷是为了减小时延,而为了保证过多的一致性而设计过于复杂的系统反而得不偿失。
使用缓存要么直接设置缓存过期时间(缓存不过期不同步),要么使用双写模式或者失效模式。
- redis三种集群
①主从模式
至少需要两台redis服务器,一台主节点(master)、一台从节点(slave),组成主从模式的Redis集群。通常来说,master主要负责写,slave主要负责读,主从模式实现了读写分离。
作用:
高可用,高并发
主从复制三个阶段:
连接建立阶段:在主从节点之间建立连接,为数据同步做准备。
数据同步阶段:执行数据的全量(或增量)复制(复制RDB文件)
命令传播阶段:主节点将已执行的命令发送给从节点,从节点接收命令并执行,从而实现主从节点的数据一致性
②哨兵模式(Sentinel)
Redis Sentinel是Redis的高可用实现方案,它可以实现对redis的监控、通知和自动故障转移,当redis master挂掉之后,可以自动拉起slave提供业务,从而实现redis的高可用。为了避免Sentinel本身出现单点故障,Sentinel自己也可采用集群模式。
当一个sentinel节点与master节点的心跳丢失时,这个sentinel节点就会认为master节点出现了故障,处于不可用的状态,这种判定叫作主观下线(即sentinel节点自己主观认为master下线了)
之后,这个sentinel节点会与其他sentinel节点交换信息,如果发现认为主节点发生故障的sentinel节点的个数超过了某个阈值(通常为sentinel节点总数的1/2+1,即超过半数),则sentinel会认为master节点已经处于客观下线的状态,即大家都认为master故障不可用了。
之后,sentinel节点中会选举处一个sentinel leader来执行redis主节点的故障转移。
③集群模式
哨兵模式基于主从模式,实现读写分离,它还可以自动切换,系统可用性更高。但是它每个节点存储的数据是一样的,浪费内存,并且不好在线扩容。因此,Reids Cluster集群(切片集群的实现方案)应运而生,它在Redis3.0加入的,实现了Redis的分布式存储。对数据进行分片,也就是说每台Redis节点上存储不同的内容,来解决在线扩容的问题。并且,它可以保存大量数据,即分散数据到各个Redis实例,还提供复制和故障转移的功能。
- redis扩容与收缩
作为分布式部署的缓存节点总会遇到缓存扩容和缓存故障的问题。这就会导致缓存节点的上线和下线的问题。由于每个节点中保存着槽数据,因此当缓存节点数出现变动时,这些槽数据会根据对应的虚拟槽算法被迁移到其他的缓存节点上。所以对于redis集群,集群伸缩主要在于槽和数据在节点之间移动。
为了安全删除节点,Redis集群只能下线没有负责槽的节点。因此如果要下线有负责槽的master节点,则需要先将它负责的槽迁移到其他节点。迁移的过程也与上线操作类似,不同的是下线的时候需要通知全网的其他节点忘记自己,此时通过命令 cluster forget {downNodeId} 通知其他的节点。
- 当发生数据迁移时,客户端会产生的影响
客户端实质会保留slot到redis节点的映射缓存。 - Redis集群中节点的通信机制:goosip协议
mysql
-
基础查询(单表)
聚合函数
distinct去重
-
连接查询
内连接
左外连接
右外连接
-
表数据的增删改
插入:
删除:
修改:
-
mysql事务
-
四大隔离级别
-
幻读
-
如何解决幻读
间隙锁会防止插入新的数据。 -
三大日志
-
redo log
为什么不直接写入mysql?
因为写redolog是顺序存储,顺序存储比随机存储快很多。 -
binlog
-
undo log
主要作用:
①提供回滚作用
②提供多版本控制(MVCC)
-
一致性非锁定读和锁定读
-
MVCC
每次生成的ReadView主要体现在以下参数:
-
mvcc只能解决可重复读下的部分幻读,下面这种情况mvcc解决不了
-
mysql中的页
mysql中和磁盘交互的最小单位称为页,页是mysql内部定义的一种数据结构,默认为16kb,相当于4个磁盘块,也就是说mysql每次从磁盘中读取一次数据是16KB,要么不读取,要读取就是16KB,此值可以修改的。 -
B+树对比B树的优点
①由于B+Tree所有的数据都在叶子结点,并且结点之间有指针连接,在范围查询的时候,B+Tree只需要找到该关键字然后沿着链表遍历就可以了,而B-Tree还需要遍历该关键字结点的根结点去搜索。
②由于B-Tree的每个结点(这里的结点可以理解为一个数据页)都存储主键+实际数据,而B+Tree非叶子结点只存储关键字信息,而每个页的大小有限是有限的,所以同一页能存储的B-Tree的数据会比B+Tree存储的更少。这样同样总量的数据,B-Tree的深度会更大,增大查询时的磁盘I/O次数,进而影响查询效率。
③B+树每次查询都会查询到叶子结点,而B树可能查询到非叶子结点就结束了,效率更稳定。 -
聚集索引和非聚集索引
聚集索引:
每个表有且一定会有一个聚集索引,整个表的数据存储在聚集索引中,mysql索引是采用B+树结构保存在文件中,叶子节点存储主键的值以及对应记录的数据,非叶子节点不存储记录的数据,只存储主键的值。当表中未指定主键时,mysql内部会自动给每条记录添加一个隐藏的rowid字段(默认4个字节)作为主键,用rowid构建聚集索引。
聚集索引在mysql中又叫主键索引。
非聚集索引(辅助索引):
也是b+树结构,不过有一点和聚集索引不同,非聚集索引叶子节点存储字段(索引字段)的值以及对应记录主键的值,其他节点只存储字段的值(索引字段)。
每个表可以有多个非聚集索引。 -
MyISAM 和 InnoDB 有什么区别?
1.是否支持行级锁
MyISAM 只有表级锁(table-level locking),而 InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。
2.是否支持事务
MyISAM 不提供事务支持。
InnoDB 提供事务支持,实现了 SQL 标准定义了四个隔离级别
3.是否支持数据库异常崩溃后的安全恢复
MyISAM 不支持,而 InnoDB 支持。使用 InnoDB 的数据库在异常崩溃后,数据库重新启动的时候会保证数据库恢复到崩溃前的状态。这个恢复的过程依赖于 redo log
4.是否支持 MVCCMyISAM 不支持,而 InnoDB 支持。
讲真,这个对比有点废话,毕竟 MyISAM 连行级锁都不支持。MVCC 可以看作是行级锁的一个升级,可以有效减少加锁操作,提高性能。
5.索引实现不一样。
虽然 MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构,但是两者的实现方式不太一样。InnoDB 引擎中,树的叶节点 data 域保存了完整的数据记录。而 MyISAM,索引文件和数据文件是分离的。 -
索引覆盖
-
索引下推
-
索引区分度
-
多个索引时查询如何走?
当多个条件中有索引的时候,并且关系是and的时候,会走索引区分度高的,显然name字段重复度很低,走name查询会更快一些。 -
索引失效的情况
1、like 以%开头,索引无效;当like前缀没有%,后缀有%时,索引有效。
2、or语句前后没有同时使用索引。
3、组合索引,不是使用第一列索引,索引失效。
4、如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引
5、在索引列上使用 IS NULL 或 IS NOT NULL操作。
6、在索引字段上使用not,<>,!=。
7、对索引字段进行计算操作、字段上使用函数。(索引为 emp(ename,empno,sal))
8、当全表扫描速度比索引速度快时,mysql会使用全表扫描,此时索引失效。 -
表级锁和行级锁对比 :
表级锁: MySQL 中锁定粒度最大的一种锁(全局锁除外),是针对非索引字段加的锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。不过,触发锁冲突的概率最高,高并发下效率极低。
行级锁: MySQL 中锁定粒度最小的一种锁,是 针对索引字段加的锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。(当我们执行 UPDATE、DELETE 语句时,如果 WHERE条件中字段没有命中唯一索引或者索引失效的话,就会导致扫描全表对表中的所有行记录进行加锁) -
S锁(读锁,共享锁)和X锁(写锁,排他锁)
(结束事务才会释放锁,普通的快照读和S锁或者X锁都是兼容的。走索引会加行锁,否则会加表锁。)
-
InnoDB 有哪几类行锁?
记录锁(Record Lock) :也被称为记录锁,属于单个行记录上的锁。
间隙锁(Gap Lock) :锁定一个范围,不包括记录本身。
临键锁(Next-Key Lock) :Record Lock+Gap Lock,锁定一个范围,包含记录本身。
在 InnoDB 默认的隔离级别 REPEATABLE-READ 下,行锁默认使用的是 Next-Key Lock。但是,如果操作的索引是唯一索引或主键,InnoDB 会对 Next-Key Lock 进行优化,将其降级为 Record Lock -
意向锁
如果需要用到表锁的话,如何判断表中的记录没有行锁呢,一行一行遍历肯定是不行,性能太差。我们需要用到一个叫做意向锁的东东来快速判断是否可以对某个表使用表锁。
此时若有其他事务加表级锁,则会被互斥。
(这意味着行锁的意向锁不会与行锁的意向锁互斥) -
数据库三范式
第一范式:
①要求有主键,每一个字段是原子性不能再分;
②第二范式是建立在第一范式基础上,要求数据库中所有非主键字段完全依赖主键,不能产生部分依赖;(不建议使用联合主键,因为这有可能出现某些字段只依赖于联合主键中的某个单一主键)
③第三范式是建立在第二范式上,要求要求数据库中所有非主键字段不能传递依赖于主键字段。
-
mysql读写分离和分库分表
-
尽量控制单表数据量的大小,建议控制在 500 万以内。
500 万并不是 MySQL 数据库的限制,过大会造成修改表结构,备份,恢复都会有很大的问题。可以用历史数据归档(应用于日志数据),分库分表(应用于业务数据)等手段来控制数据量大小。
- 限制每张表上的索引数量,建议单张表索引不超过 5 个
索引并不是越多越好!索引可以提高效率同样可以降低效率。索引可以增加查询效率,但同样也会降低插入和更新的效率,甚至有些情况下会降低查询效率。因为 MySQL 优化器在选择如何优化查询时,会根据统一信息,对每一个可以用到的索引来进行评估,以生成出一个最好的执行计划,如果同时有很多个索引都可以用于查询,就会增加 MySQL 优化器生成执行计划的时间,同样会降低查询性能。
Netty
- BIO,NIO,AIO的区别
BIO:同步并阻塞,服务实现模式为一个连接对应一个线程,即客户端发送一个连接,服务端要有一个线程来处理。如果连接多了,线程数量不够,就只能等待,即会发生阻塞。
NIO:同步非阻塞,服务实现模式是一个线程可以处理多个连接,即客户端发送的连接都会注册到多路复用器上,然后进行轮询连接,有I/O请求就处理。
AIO:异步非阻塞,引入了异步通道,采用的是proactor模式,特点是:有效的请求才启动线程,先有操作系统完成在通知服务端。 - BIO,NIO,AIO的应用场景:
BIO:适用连接数目比较小且固定的架构,对服务器要求比较高,并发局限于应用中。
NIO:适用连接数目多且连接比较短的架构,如:聊天服务器,弹幕系统等,编程比较复杂。
AIO:适用连接数目多且连接长的架构,如相册服务器(使用异步回调,实质是新开线程分别阻塞真正的读取操作)。
-
Netty对比jdk原生NIO
需要自己构建协议
解决 TCP 传输问题,如粘包、半包
epoll 空轮询导致 CPU 100%
对 API 进行增强,使之更易用。 -
黏包和半包
半包:TCP协议,存在一个缓冲区,如果发送的一个包大于这个缓冲区的大小,就会出现半包现象,即一个包分成二次发送了。
黏包:也是因为缓存区的存在,如果短时间发送很多小尺寸的包,那么tcp协议实现的底层会把这些小包一起发送。 -
黏包半包解决方案
1.短链接,发一个包建立一次连接,这样连接建立到连接断开之间就是消息的边界,缺点效率太低
2.每一条消息采用分隔符,例如 \n,缺点需要转义
3.每一条消息分为 head 和 body,head 中包含 body 的长度
Netty中有对应的处理器可以处理。
- ByteBuf优点
- EventLoop
每个EventLoop可以处理多个连接
这里表示的是一个NioEventLoopGroup(其中只有一个线程,也意味着只有一个selector)用来处理TCP连接,另一个线程来处理读写事件(这里有两个线程,有2个selector)。
Spring
- Bean的生命周期
BeanPostProcessor实现自定义注解(只有被spring管理的对象才实现自定义注解)就是围绕在初始化这个过程后实现的。
在BeanPostProcessor中可以实现对Bean进行AOP加强,可以将加强的代理对象取代原来的bean。
-
IOC和AOP
IOC,即控制反转,把对象的创建、初始化、销毁交给 Spring 来管理,而不是由开发者控制,实现控制反转。使得对象之间的耦合度降低,易于管理对象,提高了代码的可维护性和可扩展性。
-
spring循环依赖解决
-
bean的作用域
-
springboot自动装配
集成各种中间件和框架都可以快速启动。
RabbitMQ
- 消息可靠性
生产者确认机制:
可以在yaml中配置相应的成功失败回调。
消费者确认机制:
- 延迟队列
死信交换机实现:
给队列绑定一个死信交换机,没有消费者消费这个队列。然后给消息或者队列设置过期时间。
当然rabbitMQ支持原生实现延迟队列。 - 解决消息堆积
- 解决消息重复消费
如果消息天然幂等,则多次消费也无所谓。
如果消息消费是插入数据库,可以使用消息id当做主键,插入重复消息则自动失败。
通用的做法:
可以在内存或者redis维护消费过的消息id,每次消费时则判断是否存在记录。
- 消息顺序消费
Sentinel
- 服务雪崩
解决方法:
首先需要对被调用方(服务C)进行一定检测,如果超过阈值后,服务C触发熔断,并进行降级处理(一般是直接调用失败)。
这里的检测主要是三种:
①流量控制(限流降级)
检测服务C的QPS,QPS大于阈值则直接降级处理
②线程隔离
对某接口设置固定的执行线程数量。一旦线程满,还有任务来,则降级处理。
③熔断降级
熔断降级是解决雪崩问题的重要手段。其思路是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务,进行降级处理。
这种情况下还需要进行降级恢复。
- 流量控制算法
漏桶算法:
令牌桶算法: