Java面试总结
2021-02-05 09:30:43 268 举报
AI智能生成
Java面试总结:在这次面试中,我展示了对Java核心概念的深入理解,如面向对象、异常处理和多线程。我还谈到了自己的项目经验,包括使用Spring框架进行开发和数据库连接池优化。此外,我还讨论了JVM内存管理和垃圾回收算法。面试官对我的回答表示满意,并提出了关于设计模式和RESTful API的问题。最后,我谈到了如何提高代码质量和进行单元测试。总体来说,这次面试让我更加自信地展示了自己的Java技能和解决问题的能力。
作者其他创作
大纲/内容
计算机基础
cpu使用率和cpu负载
cpu使用率指的是程序在运行期间实时占用的CPU百分比,这是对一个时间段内CPU使用状况的统计,通过这个指标可以看出在某一个时间段CPU被占用的情况
https://www.cnblogs.com/zhangwangvip/p/12626400.html
https://blog.csdn.net/jackliu16/article/details/79382993
操作系统
进程和线程
1.进程是操作系统资源分配的最小单元,线程是CPU任务调度的最小单位,一个进程可以包含多个线程,所以进程和线程都是一个时间段的描述,是CPU工作时间段的描述,不过是粒度大小不同
2.不同进程间数据很难共享,同一进程下不同线程间数据很容易共享
3.每个进程都有独立的代码和数据空间,进程要比线程消耗更多的计算机资源。线程可以看作轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器,线程之间切换的开销小
4.进程间不会相互影响,一个线程挂掉将导致整个进程挂掉
5.系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源
多线程和单线程
线程不是越多越好,假如你的业务逻辑全部是计算型的(CPU密集型),不涉及到IO,并且只有一个核心,那肯定一个线程最好,多一个线程就多一点线程切换的计算,CPU不能完完全全的把计算能力放在业务计算上,线程越多就会造成CPU利用率(用在业务计算的时间/总的时间)下降。但是在WEB场景下,业务并不是CPU密集型任务,而是IO密集型的任务,一个线程是不合适的,如果一个线程在等待数据,把CPU的计算能力交给其他线程,这样也能充分的利用CPU资源。但是线程数量也要有个限度,一般线程数有一个公式:最佳启动线程数=[任务执行时间/(任务执行时间-IO等待时间)]*CPU内核数。超过这个数量CPU要进行多余的线程切换从而浪费计算能力,低于这个数量,CPU要进行IO等待从而造成计算能力不饱和。总之就是要尽可能地榨取CPU地计算能力,如果你的CPU处于饱和状态,并且没有多余地线程切换浪费,那么此时就是你服务地完美状态,如果再加大并发量,势必会造成性能上地下降
进程地组成部分
进程由进程控制块(PCB)、程序段、数据段三部分组成
进程地通信方式
进程间五种通信方式地比较
内存管理有哪几种方式
页面置换算法
操作系统中进程调度策略有哪几种
死锁地4个必要条件
如何避免(预防)死锁
计算机网络
Get和Post区别
1.Get是不安全的,因为在传输过程,数据被放在请求的URL中;Post的所有操作对用户来说都是不可见的
2.Get传送的数据量较小,这主要是因为受URL长度限制;Post传送的数据量较大,一般被默认为不受限制
3.Get限制Form表单的数据集的值必须为ASCII字符;而Post支持整个ISO10646字符集
4.Get执行效率比Post方法好,Get是Form提交的默认方法
5.Get产生一个TCP数据包;Post产生两个TCP数据包(非必然,客户端可灵活决定)
Http请求地完全过程
1.浏览器根据域名解析IP地址(DNS),并查DNS缓存
2.浏览器与WEB服务器建立一个TCP连接
3.浏览器给WEB服务器发送一个HTTP请求(GET/POST):一个HTTP请求报文由请求行(request line)、请求头部(headers)、空行(blank line)和请求数据(request body)四个部分组成
4.服务器响应HTTP响应报文,报文由状态行(status line)、响应头部(headers)、空行(blank line)和响应数据(response body)四个部分组成
5.浏览器解析渲染
计算机网络地五层模型
1.应用层:为操作系统或网络应用程序提供访问网络服务的接口,通过应用进程间的交互完成特定网络应用。应用层定义的是应用进程间通信和交互的规则。(HTTP,FTP,SMTP,RPC)
2.传输层:负责向两个主机中进程之间的通信提供通用数据服务(TCP,UDP)
3.网络层:负责对数据包进行路由选择和存储转发(IP,ICMP(ping命令))
4.数据链路层:两个相邻节点之间传送数据时,数据链路层将网络层交下来的IP数据组装成帧,在两个相邻的链路上传送帧(frame)。每一帧包括数据和必要的控制信息。
5.物理层:物理层所传数据单位是比特(bit)。物理层要考虑用多大的电压代表1或0,以及接收方如何识别发送方所发送的比特
tcp和udp区别
1.TCP面向连接,UDP是无法连接的,即发送数据之前不需要建立连接
2.TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付
3.TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流,UDP是面向报文的,UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)
4.每一条TCP连接只能是点到点的,UDP支持一对一,一对多,多对一和多对多的交互通信
5.TCP首部开销20字节,UDP的首部开销小,只有8个字节
6.TCP的逻辑通信信道使全双工的可靠信道,UDP则是不可靠信道
tcp和udp地优点
TCP的优点:可靠,稳定。TCP的可靠性体现在TCP在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完后,还会断开连接用来节约系统资源。
TCP的缺点:慢,效率低,占用系统资源高,易被攻击。TCP在传递数据之前,要先建连接,这会消耗时间,而且在数据传递时,确认机制、重传机制、拥塞控制机制等都会消耗大量的时间,而且要在每台设备上维护所有的传输连接,事实上,每个连接都会占用系统的CPU、内存等硬件资源。而且,因为TCP有确认机制、三次握手机制,这些也导致TCP容易被人利用,实现DOS、DDOS、CC等攻击
UDP的优点:快,比TCP稍安全 UDP没有TCP的握手、确认、窗口、重传、拥塞控制等机制,UDP是一个无状态的传输协议,所以它在传递数据时非常快。没有TCP的这些机制,UDP较TCP被攻击者利用的漏洞就要少一些。但UDP也是无法避免攻击的,比如:UDP Flood攻击……
UDP的缺点: 不可靠,不稳定 因为UDP没有TCP那些可靠的机制,在数据传递时,如果网络质量不好,就会很容易丢包。
基于上面的优缺点,那么: 什么时候应该使用TCP: 当对网络通讯质量有要求的时候,比如:整个数据要准确无误的传递给对方,这往往用于一些要求可靠的应用,比如HTTP、HTTPS、FTP等传输文件的协议,POP、SMTP等邮件传输的协议。 在日常生活中,常见使用TCP协议的应用如下: 浏览器,用的HTTP FlashFXP,用的FTP Outlook,用的POP、SMTP Putty,用的Telnet、SSH QQ文件传输。什么时候应该使用UDP: 当对网络通讯质量要求不高的时候,要求网络通讯速度能尽量的快,这时就可以使用UDP。 比如,日常生活中,常见使用UDP协议的应用如下: QQ语音 QQ视频 TFTP。
三次握手
第一次握手:建立连接时,客户端发送syn包(syn=x)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(syn=y),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手
为什么不能两次握手
TCP是一个双向通信协议,通信双方都有能力发送消息,并接收响应。如果只是两次握手,至多只有连接发起方的起始序列号能被确认,另一方选择的序列号则得不到确认
四次挥手
1.客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时客户端进入FIN-WAIT-1(终止等待1)状态。TCP规定,FIN报文段即使不携带数据,也要消耗一个序号
2.服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受,这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间
3.客户端收到服务器的确认请求后,此时客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)
4.服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号seq=,此时服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认
5.客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2**MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态
6.服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些
为什么连接的时候是三次握手,关闭地时候却是四次握手
因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,“你发的FIN报文我收到了”,只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四部握手。
TCP的拥塞控制
TCP的拥塞控制是什么,请简单说说:
TCP通过一个定时器(timer)采样了RTT并计算RTO,但是,如果网络上的延时突然增加,那么,TCP对这个事做出的应对之后重传数据,然而重传会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这就导致了恶行循环,最终形成“网络风暴”,TCP的拥塞控制机制就是对于应对这种情况
首先需要了解一个概念,为了在发送端调节所要发送的数据量,定义了一个“拥塞窗口”(Congestion Window),在发送数据时,将拥塞窗口的大小与接收端ack的窗口大小做比较,取较小者作为发送数据量的上限。
拥塞控制主要是四个算法:
1.慢启动:意思是刚刚加入网络的连接,一点一点地提速,不要一上来就把路占满。
连接建好的开始先初始化cwnd = 1,表明可以传一个MSS大小的数据。
每当收到一个ACK,cwnd++; 呈线性上升
每当过了一个RTT,cwnd = cwnd*2; 呈指数让升
阈值ssthresh(slow start threshold),是一个上限,当cwnd >= ssthresh时,就会进入“拥塞避免算法”
2.拥塞避免:当拥塞窗口 cwnd 达到一个阈值时,窗口大小不再呈指数上升,而是以线性上升,避免增长过快导致网络拥塞。
每当收到一个ACK,cwnd = cwnd + 1/cwnd
每当过了一个RTT,cwnd = cwnd + 1
拥塞发生:当发生丢包进行数据包重传时,表示网络已经拥塞。分两种情况进行处理:
等到RTO超时,重传数据包
sshthresh = cwnd /2
cwnd 重置为 1
3.进入慢启动过程
在收到3个duplicate ACK时就开启重传,而不用等到RTO超时
sshthresh = cwnd = cwnd /2
进入快速恢复算法——Fast Recovery
4.快速恢复:至少收到了3个Duplicated Acks,说明网络也不那么糟糕,可以快速恢复。
cwnd = sshthresh + 3 * MSS (3的意思是确认有3个数据包被收到了)
重传Duplicated ACKs指定的数据包
如果再收到 duplicated Acks,那么cwnd = cwnd +1
如果收到了新的Ack,那么,cwnd = sshthresh ,然后就进入了拥塞避免的算法了
TCP通过一个定时器(timer)采样了RTT并计算RTO,但是,如果网络上的延时突然增加,那么,TCP对这个事做出的应对之后重传数据,然而重传会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这就导致了恶行循环,最终形成“网络风暴”,TCP的拥塞控制机制就是对于应对这种情况
首先需要了解一个概念,为了在发送端调节所要发送的数据量,定义了一个“拥塞窗口”(Congestion Window),在发送数据时,将拥塞窗口的大小与接收端ack的窗口大小做比较,取较小者作为发送数据量的上限。
拥塞控制主要是四个算法:
1.慢启动:意思是刚刚加入网络的连接,一点一点地提速,不要一上来就把路占满。
连接建好的开始先初始化cwnd = 1,表明可以传一个MSS大小的数据。
每当收到一个ACK,cwnd++; 呈线性上升
每当过了一个RTT,cwnd = cwnd*2; 呈指数让升
阈值ssthresh(slow start threshold),是一个上限,当cwnd >= ssthresh时,就会进入“拥塞避免算法”
2.拥塞避免:当拥塞窗口 cwnd 达到一个阈值时,窗口大小不再呈指数上升,而是以线性上升,避免增长过快导致网络拥塞。
每当收到一个ACK,cwnd = cwnd + 1/cwnd
每当过了一个RTT,cwnd = cwnd + 1
拥塞发生:当发生丢包进行数据包重传时,表示网络已经拥塞。分两种情况进行处理:
等到RTO超时,重传数据包
sshthresh = cwnd /2
cwnd 重置为 1
3.进入慢启动过程
在收到3个duplicate ACK时就开启重传,而不用等到RTO超时
sshthresh = cwnd = cwnd /2
进入快速恢复算法——Fast Recovery
4.快速恢复:至少收到了3个Duplicated Acks,说明网络也不那么糟糕,可以快速恢复。
cwnd = sshthresh + 3 * MSS (3的意思是确认有3个数据包被收到了)
重传Duplicated ACKs指定的数据包
如果再收到 duplicated Acks,那么cwnd = cwnd +1
如果收到了新的Ack,那么,cwnd = sshthresh ,然后就进入了拥塞避免的算法了
http和https的区别
1.HTTPS协议需要用到CA(Certificate Authority,证书颁发机构),申请证书,一般免费证书较少,因而需要一定费用
2.HTTP是超文本传输协议,信息是明文传输,HTTPS则是具有安全性的SSL加密传输协议
3.HTTP和HTTPS使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443
4.HTTP的连接很简单,是无状态的。HTTPS协议是有SSL和HTTP协议构建的可进行加密传输、身份认证的网络协议,比HTTP协议安全。(无状态的意思是其数据包的发送、传输和接收都是相互独立的。无连接的意思是指通信双方都不长久的维持对方的任何信息)
https的调用流程
1.客户端向服务端发起Https请求,连接到服务器端的443端口上
2.服务端有一个密钥对,即公钥核私钥,是用来进行非加密使用的,服务端保存着私钥,将公钥发送给客户端
3.客户端收到服务端的公钥之后,对公钥进行检查,验证其合法性,如果发现公钥有问题,那么HTTPS传输无法继续;如果合格,那么客户端会生成一个随机值,这个随机值就是用于进行对称加密的密钥,即客户端密钥,然后用服务端的公钥对客户端密钥进行非对称加密,这样客户端密钥就变成了密文了,至此,https中的第一次HTTP请求结束
4.客户端发起HTTPS的第二个请求,将加密之后的客户端密钥发送给服务器
5.服务器接收到客户端发来的密文后,会用自己的私钥对其进行非对称解密,解密之后的明文就是客户端密钥,然后用客户端密钥对数据进行对称加密,这样数据就变成了密文
6.然后服务器端将加密后的密文发送给客户端
7.客户端收到服务器发来的密文,用客户端密钥对其进行对称解密,得到服务器发来的数据
数据结构与算法
1.冒泡排序
2.选择排序:选择排序与冒泡排序有点像,只不过选择排序每次都是在确定了最小数的下标之后再进行交换,大大减少了交换的次数
3.插入排序:将一个记录插入到已排序的有序表中,从而得到一个新的,记录数增1的有序表
4.快速排序:通过一趟排序将序列分成左右两部分,其中左半部分的值均比右半部分的值小,然后再分别对左右部分的记录进行排序,直到整个序列有序
5.堆排序:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了
6.希尔排序:先将整个待排记录序列分割成为若干序列分别进行插入排序,待整个序列中的记录基本有序时再对全体记录进行一次直接插入排序
7.归并排序:把有序表划分成元素个数尽量相等的两半,把两半元素分别排序,两个有序表合并成一个
其他
高并发系统的设计与实现
在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流
缓存:缓存比较好理解,在大型高并发系统中,如果没有缓存数据库将分分钟被爆,系统也会瞬间瘫痪。使用缓存不单单能够提升系统访问速度、提高并发访问量,也是保护数据库、保护系统的有效方式。大型网站一般主要是“读”,缓存的使用很容易被想到。在大型“写”系统中,缓存也常常扮演着非常重要的角色。比如累计一些数据批量写入,内存里面的缓存队列(生产消费),以及HBase写数据的机制等等也都是通过缓存提升系统的吞吐量或者实现系统的保护措施。甚至消息中间件,你也可以认为是一种分布式的数据缓存
降级:服务降级时当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略地降级,以此释放服务器资源以保证核心任务地正常运行。降级往往会指定不同的级别,面临不同的异常等级执行不同的处理。根据服务方式:可以拒接服务,可以延迟服务,也有时候可以随即服务。根据服务范围:可以砍掉某个功能,也可以砍掉某些模块。总之服务降级需要根据不同地业务需求采用不同地降级策略。主要的目的就是服务虽然有损但是总比没有好。
限流:限流可以认为服务降级地一种,限流就是限制系统地输入和输出流量已达到保护系统地目的。一般来说系统地吞吐量是可以被测算地,为了保证系统地稳定运行,一旦达到需要限制地阈值,就需要限制流量并采取一些措施以完成限制流量地目的。比如:延迟处理,拒绝处理,或者部分拒绝处理等等
负载均衡算法
1.轮询
2.加权轮询
3.随机算法
4.一致性Hash
常见地限流算法
常见地限流算法有计数器、漏桶和令牌桶算法。漏桶算法在分布式环境中消息中间件或者Redis都是可选地方案。发放令牌的频率增加可以提升整体数据处理的速度,而通过每次获取令牌地个数增加或者放慢令牌的发放速度和降低整体数据处理速度。而漏桶不行,因为它地流出速率是固定的,程序处理速度也是固定地
秒杀并发情况下库存为负数问题
1.for update显式加锁
2.把update语句写在前边,先把数量-1,之后select出库存如果>-1就commit,否则rollback
update products set quantity = quantity -1 where id=3;
select quantity from products where id=3 for update;
update products set quantity = quantity -1 where id=3;
select quantity from products where id=3 for update;
3.update语句在更新地同时加上一个条件
quantity = select quantity from products where id=3;
update products set quantity = ($quantity-1) where id=3 and quantity = $quantity
quantity = select quantity from products where id=3;
update products set quantity = ($quantity-1) where id=3 and quantity = $quantity
Redis
应用场景
1.缓存
2.共享session
3.消息队列系统
4.分布式锁
单线程的Redis为什么会快
1.纯内存操作
2.单线程操作,避免了频繁的上下文切换
3.合理高效的数据结构
4.采用了非阻塞I/o多路复用机制(epoll,有一个文件描述符同时监听多个文件描述符是否有数据到来)
Redis的数据结构及适用场景
1.String字符串:字符串类型是redis最基础的数据结构,首先键都是字符串类型,而且其他几种数据结构都是在字符串基础上构建的,常用在缓存、计数、共享session、限速等
2.hash哈希:在redis中,哈希类型是指键值本身又是一个键值对结构,哈希可以用来存放用户信息,比如实现购物车
3.List列表(双向链表):列表类型是用来存储多个有序的字符串,可以做简单的消息队列功能
4.set集合:集合set类型也是用来保存多个的字符串元素,但和列表类型不一样的是,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素,利用set的交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能
5.sorted set有序集合(跳表实现):zset多了一个权重参数score,集合中的元素能够按score进行排列,可以做排行榜应用,去top N操作
Redis的数据过期策略
redis中数据过期策略采用定期删除+惰性删除策略
定期删除策略:Redis启用一个定时器定时监视所有的key,判断key是否过期,过期的话就删除,这种策略可以保证过期的key最终都会被删除,但是也存在严重的缺点:每次都遍历内存中所有的数据,非常消耗cpu资源,并且当key已过期,但是定时器还处于未唤起状态,这段时间内key依然可以用
惰性删除策略:在获取key时,先判断key是否过期,如果过期则删除。这种策略存在一个缺点:如果这个key一直未被使用,那么它一直在内存中,其实它已经过期了,会浪费大量的空间
这两种策略天然的互补,结合起来之后,定时删除策略就发生了一些改变,不再是每次扫描全部的key了,而是随机抽取一部分key进行检查,这样就降低了对cpu资源的消耗,惰性删除策略互补了未检查到的key,基本上满足了所有的要求。但是有时候就是这么的巧,既没有被定时器抽取到,又没有被使用,这些数据又如何从内存中消失?这种时候就要用内存淘汰机制。
Redis内存淘汰策略
1.当内存不足以容纳新写入数据时,新写入操作会报错。(redis默认策略)
2.当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(LRU推荐使用)
3.当内存不足以容纳新写入数据时,在键空间中,随机移除某个key
4.当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key,这种情况一般是把redis既当缓存,又做持久化存储的时候采用
5.当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某些key
6..当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除
Redis的set和setnx
Redis中的setnx不支持设置过期时间,做分布式锁时要想避免某一客户端中断导致死锁,需要设置lock过期时间,在高并发时setnx与expire不能实现原子操作,如果要用,得在程序代码上显示的加锁。使用set代替setnx,相当于setnx+expire实现了原子性,不必担心setnx成功,expire失败的问题
Redis的LRU具体实现
传统的LRU是使用栈的形式,每次都将最新使用的移入栈顶,但是用栈的形式会导致执行select *的时候大量非热点数据占领头部数据,所以需要改进,redis每次按key获取一个值的时候,都会更新value中的lru字段为当前秒级别的时间戳。redis初始的实现算法很简单,随机从dict中取出五个key,淘汰一个lru字段值最小的。在3.0的时候,又改进了一版算法,首先第一次随机选取的key都会放入一个pool中(pool的大小为16),pool中的key是按lru大小顺序排列的,接下来每次随机选取的key lru值必须小于pool中最小的lru才会继续放入,直到将pool放满,放满之后每次如果有新的key需要放入,需要将pool中lru最大的一个key取出,淘汰的时候,直接从pool中选取一个lru最小的值然后将其淘汰
Redis如何发现热点key
1.凭借经验,进行预估:例如提前知道了某个活动的开启,那么就将此key作为热点key
2.服务端收集:在操作redis之前,加入一行代码进行数据统计
3.抓包进行评估:redis使用tcp协议与客户端进行通信,通信协议使用的是RESP,所以自己写程序监听端口也能进行拦截包进行解析
4.在proxy层,对每一个redis请求进行收集上报
5.redis自带命令查询:redis4.0.4版本提供了redis-cli-hotkeys就能找出热点key。(如果要用redis自带命令查询命令时,要注意需要先把内存逐出策略设置为allkeys-lru或者volatile-lru,否则会返回报错。进入redis中使用config set maxmemory-policy allkeys-lru即可)
Redis的热点key解决方案
1.服务端缓存:即将热点数据缓存至服务端的内存中(利用redis自带的消息通知机制来保证redis和服务端热点key的数据一致性,对于热点key客户端建立一个监听,当热点key有更新操作的时候,服务端也随之更新)
2.备份热点key:即将热点key+随机数,随机分配至redis其他节点中,这样访问热点key的时候就不会全部命中到一台机器上了
如何解决redis缓存雪崩问题
1.使用redis高可用架构:使用redis集群来保证redis服务不会挂掉
2.缓存时间不一致,给缓存的失效时间,加上一个随机值,避免集体失效
3.限流降级策略:有一定的备案,比如个性推荐服务不可用了,换成热点数据推荐服务
如何解决Redis缓存穿透问题
1.在接口做校验
2.存null值,缓存击穿加锁,或设置不过期
3.布隆过滤器拦截:将所有可能的查询key先映射到布隆过滤器中,查询时先判断key是否存在布隆过滤器中,存在才继续向下执行,如果不存在,则直接返回。布隆过滤器将值进行多次哈希bit存储,布隆过滤器说某个元素在,可能会被误判,布隆过滤器说某个元素不在,那么一定不在
Redis的持久化机制
redis为了保证效率,数据缓存在了内存中,但是会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件中,以保证数据的持久化。Redis的斥候话持久化策略有两种
1.RDB:快照形式是直接把内存中的数据保存到一个dump的文件中,定时保存,保存策略。当redis需要做持久化时,redis会fork一个子进程,子进程将数据写入到磁盘上一个临时RDB文件中。当子进程完成写临时文件后,将原来的RDB替换掉
2.AOF:把所有的对Redis的服务器进行修改的命令都存到一个文件里,命令的集合
使用AOF做持久化,每一个写命令都通过write函数追加到appendonly.aof中,aof的默认策略时每秒钟fsync一次,在这种配置下,就算发生故障停机,也最多丢失一秒钟的数据,缺点是对于相同数据集来说,AOF的文件体积通常要大于RDB文件的体积,根据使用的fsync策略,AOF的素服速度可能会慢于RDB,Redis默认是快照RDB的持久化方式,对于主从同步来说,主从刚刚连接的时候,进行全量同步(RDB),全同步结束后,进行增量同步AOF
Redis的事务
1.Redis事务的本质是一组命令的集合,事务支持依次执行多条命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。总结:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令
2.Redis事务没有隔离级别的概念,批量操作在发送EXEC命令前被放入队列缓存,并不会被实际执行,也就不存在事务内的查询要看到事务里的更新,事务外查询不能看到
3.Redis中,单条命令是原子性的,但事务不保证原子性,且没有回滚,事务中任意命令执行失败,其余的命令仍会被执行
Redis事务相关命令
1.watch key1 key2...:监视一或多个key,如果在事务执行之前,被监视的key被其他命令改动,则事务被打断(类似乐观锁)
2.multi:标记一个事务块的开始(queued)
3.exec:执行所有事务块的命令(一旦执行exec后,之前加的监控锁都会被取消掉)
4.discard:取消事务,放弃事务块中的所有命令
5.unwatch:取消watch对所有key的监控
Redis和memcached的区别
1.存储方式上:memcache会把数据全部存在内存中,断电后会挂掉,数据不能超过内存大小,redis有部分数据存在硬盘上,这样能保证数据的持久性
2.数据支持类型上:memcache对数据类型的支持简单,只支持简单的key-value,而redis支持五种数据类型
3.用底层模型不同:他们之间底层实现方式以及与客户端之间通信的应用协议不一样,redis直接自己构建了VM机制,因为一般的系统调用系统的话,会浪费一定的时间去移动和请求
4.value的大小:redis可以达到512MB,而memcache只有1MB
Redis的几种集群模式
1.主从复制
2.哨兵模式
3.cluster模式
Redis的哨兵模式
哨兵是一个分布式系统,在主从复制的基础上你可以在一个架构中运行多个哨兵进程,这些进程使用流言协议来接收关于master是否下线的信息,并使用投票协议来决定是否执行自动故障转移,以及选择哪个slave作为新的master
每个哨兵会向其他哨兵、master、slave定时发送消息,以确认对方是否活着,如果发现对方在指定时间(可配置)内未回应,则暂时认为对方已挂(所谓的“主观认为宕机”)
若“哨兵群”中的多数sentinel,都报告某一master没响应,系统才认为该master“彻底死亡”(即:客观上的真正down机),通过一定的vote算法,从剩下的slave节点中,选一台提升为master,然后自动修改相关配置
Redis的rehash
Redis的rehash操作并不是一次性、集中式完成的,而是分多次、渐进式地完成的,,redis会维护维持一个索引计数器变量rehashidx来标示rehash地进度
这种渐进式地rehash避免了集中式rehash带来地庞大计算量和内存操作,但是需要注意的是redis在进行rehash地时候,正常访问请求可能需要做多访问两次hashtable(ht[0],ht[1]),例如键值被rehash到心ht1,则需要先访问ht0,如果ht0中找不到,则取ht1中找
Redis的hash表被扩展地条件
1.哈希表中保存地key数量超过了哈希表地大小
2.Redis服务器目前没有在执行BGSAVE命令(rdb)或BGREWRITEAOF命令,并且哈希表地负载因子大于等于1
3.Redis服务器目前在执行BGSAVE命令(rdb)或BGRESWRITEAOF命令,并且哈希表地负载因子大于等于5(负载因子=哈希表已保存节点数量/哈希表大小,当哈希表地负载因子小于0.1时,对哈希表执行收缩操作)
Redis并发竞争key的解决方案
1.分布式锁+时间戳
2.利用消息队列
Redis与MySQL双写一致性方案
先更新数据库,再删缓存,数据库的读操作的速度远快于写操作的,所以脏数据很难出现,可以对异步延时删除策略,保证读请求完成以后,再进行删除操作
Redis的管道pipeline
对于单线程阻塞式Redis,pipeline可以满足批量的操作,把多个命令连续的发送给Redis server,然后意义解析响应结果,pipelining可以提高批处理性能,提升的原因主要是TCP连接中减少了“交互往返”的时间,pipeline底层时通过把所有的操作封装成流,redis有定义自己的输入输出流。再sync()方法执行操作,每次请求放到队列里面,解析相应包
RedLock
https://www.cnblogs.com/baichunyu/p/11631750.html
MySQL
事务的基本要素
1.原子性:事务是一个原子操作单元,其对数据的修改,要么全部执行,要么全部不执行
2.一致性:事务开始前和结束后,数据库的完整性约束没有被破坏
3.隔离性:同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰
4.持久性:事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚
MySQL的存储引擎
1.InnoDB存储引擎:InnoDB存储引擎支持事务,其设计目标主要面向在线事务处理(OLTP)的应用,其特点时行锁设计,支持外键,并支持非锁定锁,即默认读取操作不会产生锁。从MySQL5.5.8版本开始,InnoDB存储引擎时默认的存储引擎
2.MyISAM存储引擎:不支持事务、表锁设计,支持全文索引,主要面向一些OLAP数据库应用。InnoDB的数据文件本身就是主索引文件,而MyISAM的主索引和数据是分开的
3.NDB存储引擎:
4.Memory存储引擎
5.Archive存储引擎
6.Maria存储引擎
事务的并发问题
1.脏读:事务A读取了事务B未提交更新的数据,然后B回滚操作,那么A读取道德到的数据是脏数据
2.不可重复读:事务A多次读取同一数据,事务B在事务A多次读取过程中,对数据做了更新并提交,导致数据A多次读取同一数据时,结果不一致
3.幻读:A事务读取了B事务已经提交的新增数据,注意和不可重复读的区别,这里是新增,不可重复读是更改(或删除)。select某记录是否存在,不存在,准备插入此纪录,但执行insert时发现此记录已存在,无法插入,此时就发生了幻读
MySQL事务隔离级别
事务隔离级别
MySQL的逻辑结构
最上层的服务类似其他CS结构,比如连接处理,授权处理
第二层是Mysql的服务层,包括SQL的解析分析优化,存储过程触发器视图等也在这一层实现
最后一层是存储引擎的实现,类似于Java接口的实现,Mysql的执行器在执行SQL的时候只会关注API的调用,完全屏蔽了不同引擎实现间的差异,比如select语句,先会判断当前用户是否拥有权限,其次到缓存(内存)查询是否有相应的结果集,如果没有再执行解析sql,检查sql语句语法是否正确,在优化生成执行计划,调用api执行
SQL执行顺序
from---where---group by---having---select---order by
MVCC,redolog,undolog,binlog
MVCC:多版本并发控制是Mysql中基于乐观锁理论实现隔离级别的方式,用于读已提交和可重复读取隔离级别的实现。在Mysql中,会在表中每一条数据后面添加两个字段;最近修改该行数据的事务ID,指向该行(undolog表中)回滚段的指针。Read View判断行的可见性,创建一个新事务时,copy一份当前系统中的活跃事务列表。意思是,当前不应该被本事务看到的其他事务id列表。已提交读隔离级别下的事务在每次查询的开始都会生成一个独立的ReadView,而可重复读隔离级别则在第一次读的时候生成一个ReadView,之后的读都复用之前的ReadView
undolog:也就是我们常说的回滚日志文件,主要用于事务中执行失败,进行回滚,以及MVCC中对于数据历史版本的查看。由引擎层的InnoDB引擎实现,是逻辑日志,记录数据修改被修改前的值,比如“把id=B”修改未id=B2,那么undo日志就会用来存放id=B的记录。当一条数据需要更新前,会先把修改前的记录存储到undolog中,如果这个修改出现异常,则会使用undo日志来实现回滚操作,保证事务的一致性。当事务提交之后,undolog并不能立马被删除,而是会被放到待清理链表中,待判断没有事务用到该版本的信息时才可以清理相应undolog。它保存了事务发生之前的数据的一个版本,用于回滚,同时可以提供多版本并发控制下的读(MVCC),也即非锁定读
redolog:重做日志文件是记录数据修改之后的值,用于持久化到磁盘中,redolog包括两部分:一是内存中的日志缓冲(redo log buffer),该部分日志是易失性的;二是磁盘上的重做日志文件(redo log file),该部分日志是持久性的。由引擎层的InnoDB引擎实现,是物理日志,记录的是物理数据页修改的信息,比如某个数据页上的内容发生了哪些改动。当一条数据需要更新时,InnoDB会先将数据更新,然后记录redolog在内存中,然后找个时间将redolog的操作执行到磁盘上的文件上,不管是否提交成功我都记录,你要是回滚了,那我连回滚的修改也记录。它确保了事务的持久性,每个InnoDB存储引擎至少有一个重做日志文件组(group),每个文件组下至少有2个重做日志文件,如默认的ib_logfile0和ib_logfile1。为了得到更高的可靠性,用户可以设置多个的镜像日志组(mirrored log groups),将不同的文件组放在不同的磁盘上,以此提高重做日志的高可用性。在日志组中每个重做日志文件的大小一致,并以循环写入的方式运行。InnoDB存储引擎先写重做日志文件1,当达到日志的最后时,会切换至重做日志文件2,再当重做日志文件2也被写满时,会在切换到重做日志文件1中
binlog和redolog的区别
1.redolog是在InnoDB存储引擎层产生,而binlog是MySQL数据库的上层服务层产生的
2.两种日志记录的内容形式不同。mysql的binlog是逻辑日志,其记录是对应的sql语句,对应的事务。而innodb存储引擎层面的重做日志是物理日志,是关于每个页(Page)的更改的物理情况
3.两种日志于记录写入磁盘的时间点不同,binlog日志只在事务提交完成后进行一次写入。而innodb存储引擎的重做日志在事务进行中不断地被写入,并且日志不是随事务提交的顺序进行写入的
4.binlog不是循环使用,在写满或者重启之后,会生成新的binlog文件,redolog是循环使用
5.binlog可以作为恢复数据使用,主从复制搭建,redolog作为异常宕机或者介质故障后的数据恢复使用
MySQL读写分离以及主从同步
1.原理:主库将变更写binlog日志,然后从库连接到主库后,从库有一个IO线程,将主库的binlog日志拷贝到自己本地,写入一个中继日志中,接着从库中有一个sql线程会从中继日志 读取binlog,然后执行binlog日志中的内容,也就是在自己本地再执行一遍sql,这样就可以保证自己跟主库的数据一致
2.问题:这里有很重要的一点,就是从库同步主库数据的过程是串行化的,也就是说主库上并行操作,在从库上会串行化执行,由于从库从主库拷贝日志以及串行化执行sql特点,在高并发情况下,从库数据一定比主库慢一点,是有延迟的,所以经常出现,刚写入主库的数据可能读不到了,要过几十毫秒,甚至几百毫秒才能读取到。还有一个问题,如果突然主库宕机了,然后恰巧数据还没有同步到从库,那么有些数据可能在从库上是没有的,有些数据可能就丢失了。所以mysql实际上由两个机制,一个是半同步复制,用来解决主库数据丢失问题,一个是并行复制,用来解决主从同步延迟问题
3.半同步复制:semi-sync复制,指的是主库写入binlog日志后,就会将强制此时立即将数据同步到从库,从库将日志写入自己本地的relaylog后,接着会返回一个ack给主库,主库接收到至少一个从库ack之后才会认为写完成
4.并发复制:指的是从库开启多个线程,并行读取relaylog中不同库的日志,然后并行重放不同库的日志,这样库级别的并行。(将主库分库也可缓解延迟问题)
Next-Key Lock
InnoDB采用Next-Key Lock解决幻读问题。在insert into test(xid) values (1),(3),(5),(8),(11);后,由于xid上是有索引的,该算法总是会取锁住索引记录。现在,该索引可能被锁住的范围如下:(-∞, 1], (1, 3], (3, 5], (5, 8], (8, 11], (11, +∞)。Session A(select * from test where id = 8 for update)执行后会锁住的范围:(5, 8], (8, 11]。除了锁住8所在的范围,还会锁住下一个范围,所谓Next-Key
InnoDB的关键特性
1.插入缓存:对于非聚集性索引的插入或更新操作,不是每一次直接插入到索引页中,而是先判断插入的非聚集性索引页是否在缓存池中,若在,则直接插入;若不在,则先放入到一个Insert Buffer对象中。然后再以一定的频率和情况进行Insert Buffer和辅助索引页子节点的merge(合并)操作,这时通常能将多个插入合并到一个操作中(因为在一个索引页中),这就大大提高了对于非聚集索引插入的性能。
2.两次写:两次写带给InnoDB存储引擎的是数据页的可靠性,有经验的DBA也许会想,如果发生写失效,可以通过重做日志进行恢复。这是一个办法,但是必须清楚的认识到,如果这个页本身已经发生了损坏(物理到page页的物理日志成功页内逻辑日志失败),再对其进行重做是没有意义的。这就是说,在应用(apply)重做日志前,用户需要一个页的副本,当写入失效发生时,先通过页的副本来还原该页,再进行重做。在对缓冲池的脏页进行刷新时,并不直接写磁盘,而是会通过memcpy函数将页先复制到内存中的doublewrite buffer,之后通过doublewrite buffer再分两次,每次1MB顺序地写入共享表空间地物理磁盘上,这就是doublewrite
3.自适应哈希索引:InnoDB存储引擎会监控对表上各索引页地查询。如果观察到建立哈希索引可以带来速度提升,则建立哈希索引,称之为自适应哈希索引。
4.异步IO:为了提高磁盘操作性能,当前的数据库系统都采用异步IO(AIO)地方式来处理磁盘操作,AIO地另一个优势是可以进行IO Merge操作,也就是将多个IO合并为一个IO,这样可以提高IOPS地性能
5.刷新邻接页:当刷新一个脏页时,InnoDB存储引擎会检测该页所在区(extent)地所有页,如果是脏页,那么一起进行刷新,这样做的好处显而易见,通过AIO可以将多个IO写操作合并为一个IO操作,故该工作机制在传统机械磁盘下有着显著地优势
Mysql如何保证一致性和持久性
mysql为了保证ACID中地一致性和持久性,使用了WAL(Write-Ahead Logging,先写日志再写磁盘)。redolog就是一种WAL地应用。当数据库忽然断电,再重新启动时,MySQL可以通过Redo log还原数据,也就是说,每次事务提交时,不用同步刷新磁盘数据文件,只需要同步刷新Redo log就足够了。
InnoDB的行锁模式
共享锁(S):用法lock in share mode,又称读锁,允许一个事务去读一行,阻止其他事务获得相同数据集地排他锁。若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上地S锁,这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改
排他锁(X):用法for update,又称写锁,允许获取排他锁的事务更新数据,阻止其他事务取得相同的数据集共享读锁和排他写锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。在没有索引的情况下,InnoDB只能使用表锁。
为什么选择B+树作为索引结构
Hash索引:Hash索引底层是哈希表,哈希表是一种以key-value存储数据的结构,所以多个数据在存储关系上是完全没有任何顺序关系的,所以,对于区间查询是无法通过索引查询的,就需要全表扫描,所以,哈希索引只适用于等值查询的情景。而B+树是一种多路平衡查询树,所以他的节点是天然有序的(左子节点小于父节点、父节点小于右子节点,所以对于范围查询的时候不需要做全表扫描)
二叉查找树:解决了排序的基本问题,但是由于无法保证平衡,可能退化为链表
平衡二叉树:通过旋转解决了平衡的问题,但是旋转操作效率太低
红黑树:通过舍弃严格的平衡和引入红黑节点,解决了AVL旋转效率过低的问题,但是在磁盘等场景下,树依然太高,IO次数太多
B+树:在B树的基础上,将非叶节点改造为不存储数据纯索引节点,进一步降低了树的高度;此外将叶节点使用指针连接成链表,范围查询更加高效
B+树的叶子节点都可以存哪些东西
可能存储的是整行数据,也有可能是主键的值。B+树的叶子节点存储了整行数据的是主键索引,也被称之为聚簇索引。而索引B+Tree的叶子节点存储了主键的值的是非主键索引,也被称为非聚簇索引
索引覆盖
指一个查询语句的执行只用从索引中就能够取得,不必从数据表中读取,也可以称之为索引覆盖
查询在什么时候不走(预期中的)索引
1.模糊查询 % like
2.索引列参与计算,使用了函数
3.非最左前缀顺序
4.where对null判断
5.where不等于
6.or操作有至少一个字段没有索引
7.需要回表的查询结果集过大(超过配置的范围)
explain命令概要
1.id:select选择标识符
2.select_type:标识查询的类型
3.table:输出结果集的表
4.partitions:匹配的分区
5.type:表示表的连接类型
6.possible_keys:表示查询时,可能使用的索引
7.key:表示实际使用的索引
8.key_len:索引字段的长度
9.ref:列与索引的比较
10.rows:扫描出的行数(估算的行数)
11.filtered:按表条件过滤的行百分比
12:Extra:执行情况的描述和说明
explain中的select_type(查询的类型)
1.SIMPLE(简单select,不使用union或子查询等)
2.PRIMARY(子查询中最外层查询,查询中若包含任何复杂的子部分,最外层的select被标记为PRIMARY)
3.UNION(UNION中的第二个或后面的select语句)
4.DEPENDENT UNION(UNION中的第二个或后面的select语句,取决于外面的查询)
5.UNION RESULT(UNION的结果,union语句中弟哥哥select开始后面所有select)
6.SUBQUERY(子查询中的第一个select,结果不依赖于外部查询)
7.DEPENDENT SUBQUERY(子查询中的第一个select,依赖外部查询)
8.DERIVED(派生表的select,from子句的子查询)
9.UNCACHEABLE SUBQUERY(一个子查询的结果不能被缓存,必须重新评估外链接的第一行)
explain中的type(表的连接类型)
1.system:最快,主键或唯一索引查找常量值,只有一条记录,很少能出现
2.const:PK或者unique上的等值查询
3.eq_ref:PK或者unique上的join查询,等值匹配,对于前表的每一行(row),后表只有一行命中
4.ref:非唯一索引,等值匹配,可能有多行命中
5.range:索引上的范围扫描,例如:between/in
6.index:索引上的全集扫描,例如:InnoDB的count
7.ALL:最慢,全表扫描(full table scan)
explain中的Extra(执行情况的描述和说明)
1.Using where:不用读取表中所有信息,仅通过索引就可以获取所需数据,这发生在对表的全部的请求列都是同一个索引的部分的时候,表示mysql服务器将在存储引擎检索行后再进行过滤
2.Using temporary:表示MySQL需要使用临时表来存储结果集,常见于排序和分组查询,常见 group by ; order by
3.Using filesort:当Query中包含 order by 操作,而且无法利用索引完成的排序操作称为“文件排序”
4.Using join buffer:改值强调了在获取连接条件时没有使用索引,并且需要连接缓冲区来存储中间结果。如果出现了这个值,那应该注意,根据查询的具体情况可能需要添加索引来改进能。
5.Impossible where:这个值强调了where语句会导致没有符合条件的行(通过收集统计信息不可能存在结果)。
6.Select tables optimized away:这个值意味着仅通过使用索引,优化器可能仅从聚合函数结果中返回一行
7.No tables used:Query语句中使用from dual 或不含任何from子句
数据库优化指南
1.创建并使用正确的索引
2.只返回需要的字段
3.减少交互次数(批量提交)
4.设置合理的Fetch Size(数据每次返回给客户端的条数)
数据库锁
https://blog.csdn.net/qq_35642036/article/details/89554721
dubbo
https://www.bilibili.com/video/BV1Gb411T7Ha
工作原理
第一层:service层,接口层,给服务提供者和消费者来实现的
第二层:config层,配置层,主要对dubbo进行各种配置的
第三层:proxy层,服务代理层,透明生成客户端的stub和服务单的skeleton
第四层:registry层,服务注册层,负责服务的注册与发现
第五层:cluster层,集群层,封装多个服务提供者的路由以及负载均衡,将多个实例组合成一个服务
第六层:monitor层,监控层,对rpc接口的调用次数和调用时间进行监控
第七层:protocol层,远程调用层,封装rpc调用
第八层:exchange层,信息交换层,封装请求响应模式,同步转异步
第九层:transport层,网络传输层,抽象mina和netty为统一接口
第十层:serialize层,数据序列化层
工作流程
第一步:provider向注册中心去注册
第二步:consumer从注册中心订阅服务,注册中心会通知consumer注册好的服务
第三步:consumer调用provider
第四步:consumer和provider都异步的通知监控中心
注册中心宕机了后还可以继续通信吗?
可以,因为刚开始初始化的时候,消费者会将提供者的地址等信息拉取到本地缓存,所以注册中心挂了可以继续通信
Dubbo支持哪些通信协议?支持哪些序列化协议?
dubbo协议
默认就是走dubbo协议,单一长连接,NIO异步通信,基于hessian作为序列化协议
适用场景:传输数据量很小(每次请求在100kb以内),但是并发量很高
rmi协议
走java二进制序列化,多个短连接
适合消费者和提供者数量差不多,适用于文件的传输,一般较少用
hessian协议
走hessian序列化协议,多个短连接
适用于提供者数量比消费者数量还多,适用于文件的传输,一般较少用
http协议
走json序列化
webservice
走SOAP文本序列化
Dubbo负载均衡策略和集群容错策略都有哪些?
random loadbalance
基于权重的随机调用负载均衡策略
roundrobin loadbalance
基于权重的轮询调用负载均衡策略
leastactive loadbalance
根据监控获取到的调用反应时间快的机器
consistanthhash loadbalance
一致性Hash算法,相同参数的请求一定分发到一个provider上去
dubbo集群容错策略
failover cluster模式
失败自动切换,自动重试其他机器,默认就是这个,常见于读操作
failfast cluster模式
一次调用失败就立即失败,常见于写操作
failsafe cluster模式
出现异常时忽略掉,常用于不重要的接口调用,比如记录日志
failback cluster模式
失败了后台自动记录请求,然后定时重发,比较适合于写消息队列这种
forking cluster模式
并行调用多个provider,只要一个成功就立即返回
broadcacst cluster模式
逐个调用所有的provider
dubbo动态代理策略
默认使用javassist动态字节码生成,创建代理类
但是可以通过spi扩展机制配置自己的动态代理策略
dubbo的spi思想是什么
service provider interface,Protocol接口
基于dubbo如何做服务治理、服务降级和重试
服务治理
调用链路自动生成
服务访问压力以及时长统计
其他:服务分层(避免循环依赖),调用链路失败监控和报警,服务鉴权,每个服务的可用性的监控(接口的调用成功率)
服务降级
创建Mock类
失败重试和超时重试
timeout + retries
dubbo的优缺点
优点
支持RPC调用,服务之间的调用性能好
支持多种序列化协议,如Hession、HTTP、WebService
Dubbo Admin后台管理功能强大,提供了路由规则、动态配置、访问控制、权重调节、负载均衡的功能
缺点
Registry严重依赖第三方组件(zookeeper或者redis),当这些组件出现问题时,服务调用会很快中断
dubbo只支持RPC调用,使得服务提供方(抽象接口)与调用方在代码上产生了强依赖,服务提供者需要不断将包含抽象接口的jar包打包出来供消费者使用。一旦打包出现问题,就会导致服务调用出错,并且以后发布部署会成很大问题(太强的依赖关系)
dubbo只是实现了服务治理,其他微服务框架并未包含,如果需要使用,需要结合第三方框架实现,开发成本较高,且风险较大
Dubbo提供的线程池
1.fixed:固定大小线程池,启动时建立线程,不关闭,一直持有
2.cached:缓存线程池,空闲一分钟自动删除,需要时重建
3.limited:可伸缩线程池,但池中的线程数只会增长不会收缩(为避免收缩时突然来了大流量引起的性能问题)
zookeeper
CAP定理
一个分布式系统不可能在满足分区容错性(P)的情况下同时满足一致性(C)和可用性(A)。在此Zookeeper保证的是CP,Zookeeper不能保证每次服务请求的可用性,在极端环境下,Zookeeper可能会丢弃一些请求,消费者程序需要重新请求才能获得结果,另外在进行leader选举时集群都是不可用的,所以说,ZooKeeper不能保证服务可用性
BASE理论
BASE理论是基本可用,软状态,最终一致性三个短语的缩写。BASE理论是对CAP中一致性和可用性(CA)权衡的结果,其来源对大规模互联网系统分布式实践的总结,是基于CAP定理逐步演化而来的,它大大降低了我们对系统的要求
1.基本可用:基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性。但是,这绝不等价于系统不可用。比如正常情况下,一个在线搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但是由于出现故障,查询结果的响应时间增加了1~2秒
2.软状态:软状态指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程中存在延时
3.最终一致性:最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态,因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性
ZooKeeper特点
1.顺序一致性:同一客户端发起的事务请求,最终会严格的按照顺序被应用到ZooKeeper中去
2.原子性:所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,也就是说,要么整个集群中所有的机器都成功应用了某一个事务,要么都没有应用
3.单一系统映像:无论客户端连到哪一个ZooKeeper服务器上,其看到的服务数据模型都是一致的
4.可靠性:一旦一次更改请求被应用,更改的结果就会被持久化,直到被下一次更改覆盖
ZAB协议
ZAB协议包括两种基本的模式:崩溃恢复和消息广播,当整个zk集群刚刚启动或者Leader服务器宕机、重启或者网络故障不存在过半的服务器与Leader服务器保持正常通信时,所有服务器进入崩溃恢复模式,首先选举产生新的Leader服务器,然后集群中Follower服务器开始与新的Leader服务器进行数据同步,当集群中超过半数机器与该Leader服务器完成同步数据之后,退出恢复模式进入消息广播模式,Leader服务器开始接收客户端的事务请求生成事务提案(超过半数同意)来进行事务请求处理
选举算法和流程:FastLeaderElection(默认提供的选举算法)
https://www.bilibili.com/video/BV1YK4y1k7Ny?from=search&seid=893231275237811897
全新选举机制
半数胜出
非全新选举机制
比较逻辑时钟(投票次数)、数据ID、服务器ID,大的胜出
zk中的监控原理
zk类似于linux中的目录节点树方式的数据存储,即分层命名空间,zk并不是专门存储数据的,它的作用主要是维护和监控存储数据的变化,通过监控这些数据状态变化,从而达到基于数据的集群管理,zk中的节点的数据上限是1M,client端会对某个znode建立一个watcher事件,当该znode发生变化时,这些client会收到zk的通知,然后client可以根据znode变化做出业务上的改变等
zk实现分布式锁
zk实现分布式锁主要利用其临时顺序节点,实现分布式锁的步骤如下:
1.创建一个目录mylock
2.线程A想获取锁就在mylock目录下创建临时顺序节点
3.获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序要最小,获取锁
4.线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点
5.线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小节点,如果是则获得锁
1.创建一个目录mylock
2.线程A想获取锁就在mylock目录下创建临时顺序节点
3.获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序要最小,获取锁
4.线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点
5.线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小节点,如果是则获得锁
JVM
运行时数据区域
1.程序计数器:程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器,在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值获取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。是线程私有的内存。
2.Java虚拟机栈:与程序计数器一样,Java虚拟机也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈桢在虚拟机栈中入栈到出栈的过程
3.本地方法栈:本地方法栈与虚拟机栈所发挥的作用非常相似,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务的
4.Java堆:对于大多数应用来说,Java堆是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
5.方法区:方法区用于存储已被虚拟机加载的类信息、常量、静态变量,如static修饰的变量加载类的时候就被加载到方法区中。运行时常量池是方法区的一部分,class文件除了有类的字段、接口、方法等描述信息之外,还有常量池用于存放编译期间生成的各种字面量和符号引用。在老版jdk,方法区也被称为永久代。在1.8之后,由于永久代内存经常不够用或发生内存泄漏,爆出异常java.lang.OutOfMemoryError,所以在1.8之后废弃永久代,引入元空间的概念。元空间是方法区的HotSpot jvm中的实现,元空间的本质和永久代类似,都是堆JVM规范方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。理论上取决于32位/64位系统可虚拟的内存大小,可见也不是无限制的,需要配置参数。
分代回收
HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果依然存活,将会被移到Survivor区,对象在Survivor区中每熬过一次Minor GC,年龄就会加1岁,当它的年龄加到一定程度时,就会被移动到年老代中。
因为年轻代的对象基本都是朝生夕死的,所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另一块上面。复制算法不会产生内存碎片。
在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区域是空的,MinorGC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,就会将所有对象移动到年老代中
动态年龄计算
HotSpot在遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了suivivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值。
JVM引入动态年龄计算,主要基于如下两点考虑:
1.如果固定按照MaxTenuringThreshold设定的阈值作为晋升条件:a)MaxTenuringThreshold设置的过大,原本应该晋升的对象一直停留在Survivor区,直到Survivor区溢出,一旦溢出发生,Eden+Survivor中对象将不在依据年龄全部提升到老年代,这样对象老化的机制就失效了。b)MaxTenuringThreshold设置的过小,“过早晋升”即对象不能在新生代充分被回收,大量短期对象被晋升到老年代,老年代空间迅速增长,引起频繁的Major GC,分代回收失去了意义,严重影响GC性能。
2.相同应用在不同时间的表现不同:特殊任务的执行或者流量成分的变化,都会导致对象的生命周期分布发生波动,那么固定的阈值设定, 因为无法动态适应变化,会造成和上面相同的问题
常见的垃圾回收机制
1.引用计数法:引用计数法是一种简单但速度很慢的垃圾回收技术。每个对象都含有一个引用计数器,当有引用连接至对象时,引用计数加1,当引用离开作用域被置为null时,引用计数减1。虽然管理引用计数的开销不大,但这项开销在整个程序生命周期中将持续发生。垃圾回收器会在含有全部对象的列表上遍历,当发现某个对象引用计数为0时,就释放其占用的空间。
2.可达性分析算法:这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
CMS的执行过程
1.初始标记(STW initial mark):这个过程从垃圾回收的“根对象”开始,只扫描到能够和“根对象”直接关联的对象,并作标记。所以这个过程虽然暂停了整个JVM,但是很快就完成了。
2.并发标记(Concurrent marking):这个阶段紧随初始标记阶段,在初始标记的基础上继续向下追溯标记。并发标记阶段,应用程序的线程和并发标记的线程并发执行,所以用户不会感受到停顿。
3.并发预清理(Concurrent precleaning):并发预清理阶段仍然是并发的。在这个阶段,虚拟机查找在执行并发标记阶段新进入老年代的对象(可能会有一些对象从新生代晋升到老年代,或者有一些对象被分配到老年代)。通过重新扫描,减少下一个阶段“重新标记”的工作,因为下一个阶段会Stop The World。
4.重新标记(STW remark):这个阶段会暂停虚拟机,收集器线程扫描在CMS堆中剩余的对象,扫描从“根对象”开始向下追溯,并处理对象关联。
5.并发清理(Concurrent sweeping):清理垃圾对象,这个阶段收集器线程和应用程序线程并发执行。
6.并发重置(Concurrent reset):这个阶段,重置CMS收集器的数据结构状态,等待下一次垃圾回收
G1的执行过程
1.标记阶段:首先是初始标记(Initial-Mark),这个阶段也是停顿的(stop-the-word),并且会捎带触发一次young GC
2.并发标记:这个过程在整个堆中进行,并且和应用程序并发运行。并发标记过程可能被young GC中断。在并发标记阶段,如果发现区域对象中的所有对象都是垃圾,那么这个区域会立即回收,同时并发标记过程中,每个区域的对象活性(区域中存活对象的比例)被计算
3.再标记:这个阶段用来补充收集并发标记阶段产生的新的垃圾,与之不同的是,G1中采用了更快的算法SATB
4.清理阶段:选择活性低的区域(同时考虑停顿时间),等待下次young GC一起收集,这个过程也会有停顿(STW)
5.回收/完成:新的young GC清理被计算好的区域。但是有一些区域还是可能存在垃圾对象,可能是这些区域中对象活性比较高,回收不划算,也可能是为了迎合用户设置的时间,不得不舍弃一些区域的收集
G1和CMS的比较
1.CMS收集器是获取最短回收停顿时间为目标的收集器,因为CMS工作时,GC工作线程与用户线程可以并发执行,以此来达到降低停顿时间的目的(只有初始标记和重新标记会STW)。但是CMS收集器对CPU资源非常敏感,在并发阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致应用程序变慢,总吞吐量下降
2.CMS仅作用于老年代,是基于标记清除算法,所以清理的过程中会有大量的空间碎片
3.CMS收集器无法处理浮动垃圾,由于CMS并发清理阶段用户线程还在运行,伴随程序的运行会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理它们,只好留待下一次GC时将其清理掉
4.G1是一款面向服务端应用的垃圾收集器,适用于多核处理器、大内存容量的服务端系统。G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或CPU核心)来缩短STW的停顿时间,它满足短时间停顿的同时达到一个高的吞吐量。
5.从JDK9开始,G1称为默认的垃圾收集器,当应用有以下任何一种特性时非常适合用G1:Full GC持续时间太长或者太频繁;对象的创建速率和存活率变动很大;应用不希望停顿时间长(长于0.5甚至1s)
6.G1将空间划分成很多块(Region),然后他们各自进行回收。堆比较大的时候可以采用,采用复制算法,碎片化问题不严重。整体上看属于标记整理算法,局部(region之间)属于复制算法
7.G1需要记忆集来记录新生代和老年代之间的引用关系看,这种数据结构在G1中需要占用大量的内存,可能达到整个堆内存容量的20%甚至更多。而且G1中维护记忆集的成本较高,带来了更高的执行负载,影响效率。所以CMS在小内存应用上的表现要优于G1,而大内存应用上G1更有优势,大小内存的界限时6GB到8GB。
哪些对象可以作为GC Roots
1.虚拟机栈(栈帧中的本地变量表)中引用的对象
2.方法区中静态属性引用的对象
3.方法区中常量引用的对象
4.本地方法栈中JNI(即一般说的Native方法)引用的对象
GC中Stop The World(STW)
在执行垃圾收集算法时,Java应用程序的其他所有除了垃圾收集收集器线程之外的线程都被挂起。此时,系统只能允许GC线程进行运行,其他线程则会全部暂停,等待GC线程执行完毕后才能再次运行。这些工作都是由虚拟机在后台自动发起和自动完成的,是在用户不可见的情况下把用户正常工作的线程全部停下来,这对于很多的应用程序,尤其是那些对于实时性要求很高的程序来说是难以接受的。
但不是说GC必须STW,你也可以选择降低运行速度但是可以并发执行的垃圾算法,这取决于你的业务
垃圾回收算法
1.停止-复制:先暂停程序的运行,然后将所有存活的对象从当前堆复制到另一个堆,没有被复制的对象全部都是垃圾。当对象被复制到新堆时,它们是一个挨着一个的,所以新堆保持紧凑排列,然后就可以按前述方法简单直接地分配了,缺点一是浪费空间,两个堆之间要来回倒腾,二是当程序进入稳定态时,可能只会产生极少地垃圾,甚至不产生垃圾,尽管如此,复制式回收器仍会将所有内存自一处复制到另一处
2.标记-清除:同样是从堆栈和静态存储区出发,遍历所有地引用,进而找出所有存活的对象,每当它找到一个存活地对象,就会给对象一个标记,这个过程不会回收任何对象。只有全部标记工作完成地时候,清理动作才会开始,在清理过程中,没有标记地对象会被释放,不会发生任何复制动作。所以剩下的堆空间是不连续地,垃圾回收器如果要希望得到连续空间地话,就得重新整理剩下地对象
3.标记-整理:它的第一个阶段与标记/清除算法是一摸一样地,均是遍历GC Roots,然后将存活地对象标记。移动所有存活地对象,且按照内存地址依次排列,然后将末端内存地址以后地内存全部回收,因此,第二阶段才称为整理阶段
4.分代收集算法:把Java堆分为新生代和老年代,然后根据各个年代地特点采用最合适地收集算法,新生代中,对象地存活率比较低,所以选用复制算法,老年代中对象存活率高且没有额外空间对它进行分配担保,所以使用“标记-清除”或“标记-整理”算法进行回收
Minor GC和Full GC触发条件
Minor GC触发条件:当Eden区满时,触发Minor GC
Full GC触发条件:
1.调用System.gc()时,系统建议执行Full GC,但是不必然执行
2.老年代空间不足
3.方法区空间不足
4.通过Minor GC后进入老年代的平均大小大于老年代的可用内存
5.由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
对象什么时候进入老年代
1.大对象直接进入老年代。虚拟机提供了一个阈值参数,令大于这个设置值的对象直接在老年代中分配,如果大对象进入新生代,新生代采用的复制算法收集内存,会导致在Eden区和两个Survivor区之间发生大量的内存复制,应该避免这种情况
2.长期存活的对象进入老年代。虚拟机给每个对象定义了一个年龄计数器,对象在Eden区出生,经过一次Minor GC后仍然存活,并且能被Survivor区容纳的话,将被移动到Survivor区中,此时对象的年龄设为1,然后对象在Survivor区中每熬过一次Minor GC,年龄就增加1,当年龄超过设定的阈值时,就会被移动到老年代中。
3.动态对象年龄判定:如果在Survivor空间中所有相同年龄的对象,大小总和大于Survivor空间的一半,那么年龄大于或等于该年龄的对象就直接进入老年代,无需等到阈值中要求的年龄
4.空间分配担保:如果老年代中最大可用的连续空间大于新生代所有对象的总空间,那么Minor GC是安全的,如果老年代中最大可用的连续空间大于历代晋升到老年代的对象的平均大小,就进行一次有风险的Minor GC,如果小于平均值,就进行Full GC来让老年代腾出更多的空间。因为新生代使用的是复制算法,为了内存利用率,只是使用其中一个Survivor空间来做轮换备份,如果大量对象在Minor GC后仍然存活,导致Survivor空间不够用,就会通过分配担保机制,将多出来的对象提前转到老年代,但老年代要进行担保的前提是自己本身还有容纳这些对象的剩余空间,由于无法提前知道会有多少对象存活下来,所以取之前每次晋升到老年代的对象的平均大小作为经验值,与老年代的剩余空间做比较
TLAB
在Java中,典型的对象不在堆上分配的有两种:TLAB和栈上分配(通过逃逸分析)。JVM在内存新生代Eden Space中开辟了一小块线程私有的区域,称为TLAB(Thread-local allocation buffer)。默认设定为占用Eden Space的1%。在Java程序中很多对象都是小对象且用过即丢,它们不存在线程共享也适合被快速GC,所以对于小对象通常JVM会优先分配在TLAB上,并且TLAB上的分配由于是线程私有所以没有锁开销,因此在实践中分配多个小对象的效率通常比分配一个大对象的效率要高。也就是说,Java中每个线程都会有自己的缓冲区称作TLAB,但是每个TLAB都只有一个线程可以操作,TLAB结合bump-the-pointer技术可以实现快速的对象分配,而不需要任何的锁进行同步,也就是说,在对象分配的时候不用锁住整个堆,而只需要在自己的缓冲区分配即可
Java对象分配的过程
1.编译器通过逃逸分析,确定对象是在栈上分配还是在堆上分配。如果是在堆上分配,则进入2.
2.如果tlab_top+size<=tlab_end,则在TLAB上直接分配对象并增加tlab_top的值,如果现有的TLAB不足以存放当前对象则3.
3.重新申请一个TLAB,并再次尝试存放当前对象。如果放不下,则4.
4.在Eden区加锁(这个区是多线程共享的),如果eden_top+size<eden_end则将对象存放在Eden区,增加eden_top的值,如果Eden区不足以存放,则5
5.执行一次Young GC(minor collection)
6.经过Young GC之后,如果Eden区仍然不足以存放当前对象,则直接分配到老年代
对象内存分配的两种方法
1.指针碰撞(Serial、ParNew等带Compact过程的收集器):假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配的内存就仅仅是那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump the Pointer)
2.空闲列表(CMS这种基于Mark-Sweep算法的收集器):如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单的进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)
JVM类加载过程
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段
1.加载:通过一个类的全限定名来获取定义此类的二进制字节流,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口
2.验证:验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
3.准备:准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配到Java堆中
4.解析:解析阶段时虚拟机将常量池中的符号(Class文件内的符号)引用替换为直接引用(指针)的过程
5.初始化:初始化时类加载过程的最后一步,开始执行类中定义的Java程序代码(字节码),init
双亲委派模型
双亲委派的意思是如果类加载器需要加载类,那么首先它会把这个类请求委派给父类加载器去完成,每一层都是如此,一直递归到顶层,当父加载器无法完成这个请求时,子类才会尝试去加载
双亲委派模型的“破坏”
一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动器去加载(在JDK1.3时放进去的rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用独立厂商实现并部署在应用程序ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)的代码,但启动类加载器不可能认识“这些代码”那该怎么办?
为了解决这个问题,Java设计团队只好引入了一个不太优雅的的设计:线程上下文加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个加载器默认就是应用程序类加载器。
有了线程上下文加载器,就可以做一些“舞弊”的事情了,JNDI服务使用这个线程上下文加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB、JBI等
JVM锁优化和膨胀过程
1.自旋锁:自旋锁其实就是在拿锁时发现已经有线程拿了锁,自己如果去拿会阻塞自己,这个时候会选择一次忙循环尝试。也就是不停循环看是否能等到上个线程自己释放锁。自适应自旋锁指的是例如第一次设置最多自旋10次,结果在自旋的过程中成功获得了锁,那么下一次就可以设置成最多自旋20次
2.锁粗化:虚拟机通过适当扩大加锁的范围以避免频繁的拿锁释放锁的过程
3.锁消除:通过逃逸分析发现其实根本就没有别的线程产生竞争的可能(别的线程没有临界值的引用),或者同步块内进行的是原子操作,而“自作多情”的给自己加上了锁,有可能虚拟机会直接去掉这个锁
4.偏向锁:在大多数的情况下,锁不仅不存在多线程的竞争,而是总是由同一个线程获得,因此为了让线程获得锁的代价更低而引入了偏向锁的概念,偏向锁的意思是如果一个线程获得了一个偏向锁,如果在接下来的一段时间中没有其他线程来竞争锁,那么持有锁的线程再次进入或者退出同一个同步代码块,不需要再次进行抢占锁和释放锁的操作
5.轻量级锁:当存在超过一个线程在竞争同一个同步代码块时,会发生偏向锁的撤销。当前线程会尝试使用CAS来获取锁,当自旋超过指定次数(可以自定义),此时锁会膨胀升级为重量级锁
6.重量级锁:重量级锁依赖对象内部的monitor锁来实现,而monitor又依赖操作系统的MutexLock(互斥锁)。当系统检查到是重量级锁之后,会把等待想要获取锁的线程阻塞,被阻塞的线程不会消耗CPU,但是阻塞或者唤醒一个线程,都需要通过操作系统来实现。
什么情况下需要开始类加载过程的第一个阶段加载
1.遇到new、getstatic、putstatic、或invokestatic这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这四条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候
2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
3.当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
4.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类
i++操作的字节码指令
1.将int类型常量加载到操作数栈顶
2.将int类型数值从操作数栈顶取出,并存储到局部变量表的第一个slot中
3.将int类型变量从局部变量表的第一个slot中取出,并放到操作数栈顶
4.将局部变量表的第一个slot中的int类型变量加1
5.表示将int类型数值从操作数栈顶取出,并存储到局部变量表的第一个slot中,即i中
JVM性能监控
1.JDK的命令行工具
jps(虚拟机进程状况工具):jps可以列出正在运行的虚拟机的进程,并显示虚拟机执行主类(Main Class,main()函数所在的类)名称,以及这些进程的本地虚拟机唯一ID(Local Virtual Machine Identifier,LVMID)
jstat(虚拟机统计信息监视工具):jstat是用来监视虚拟机各种运行状态信息的命令行工具,它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据
jinfo(Java配置信息工具):jinfo的作用是实时地查看和调整虚拟机各项参数
jmap(java内存映像工具):命令用于生成堆转储快照(一般称为heapdump或dump文件),如果不使用jmap命令,要想获取java堆转储快照,还有一些比较“爆力”的手段:譬如-XX:+HeapDumpOnOutOfMemoryError参数,可以让虚拟机在OOM异常出现之后自动生成dump文件,jmap的作用并不仅仅是为了获取dump文件,它还可以查询finalize执行队列、Java堆和永久代的详细信息,如空间使用率、当前用的是哪种收集器等
jhat(虚拟机堆转储快照分析工具):jhat命令与jmap搭配使用,来分析jmap生成的堆存储快照。jhat内置了一个微型的Http/Html服务器,生成dump文件的分析结果后,可以在浏览器中查看
jstack(Java堆栈跟踪工具):jstack命令用于生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的常见原因。线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道响应的线程到底在后台做些什么事情,或者等待着什么资源
2.JDK的可视化工具
Jconsole
VisualVM
JVM常见参数
1.-Xms20M:表示设置JVM启动内存的最小值为20M,必须以M为单位
2.-Xmx20M:表示设置JVM启动内存的最大值为20M,必须以M为单位,将-Xmx和-Xms设置为一样可以避免JVM内存自动扩展,大的项目-Xmx和-Xms一般都要设置到10G、20G甚至更高
3.-verbose:gc:表示输出虚拟机中GC的详细情况
4.-Xss128k:表示可以设置虚拟机栈的大小为128k
5.-Xoss128k:表示设置本地方法栈的大小为128k。不过HotSpot并不区分虚拟机栈和本地方法栈,因此对于HotSpot来说这个参数是无效的
6.-XX:PermSize=10M:表示JVM初始分配的永久代(方法区 )的容量,必须以M为单位
7.-XX:MaxPermSize=10M:表示JVM允许分配的永久代(方法区)的对最大容量,必须以M为单位,大部分情况下这个参数默认为64M
8.-Xnoclassgc:表示关闭JVM堆类的垃圾回收
9.-XX:+TraceClassLoading:表示查看类的加载信息
10.-XX:+TraceClassUnLoading:表示查看类的卸载信息
11.-XX:NewRatio=4:表示设置年轻代(包括Eden和两个Survivor区)/老年代的大小比例为1:4,这意味着年轻代占整个堆的1/5
12.-XX:SurvivorRatio=8:表示设置2个Survivor区:1个Eden区的大小比值为2:8,这意味着Survivor区占整个年轻代的1/5,这个参数默认为8
13.-Xmn20M:表示设置年轻代的大小为20M
14.-XX:+HeapDumpOnOutOfMemoryError:表示可以让虚拟机在出现内存溢出异常时Dump出当前的堆内存转储快照
15.-XX:+UseG1GC:表示让JVM使用G1垃圾收集器
16.-XX:+PrintGCDetails:表示在控制台上打印出GC具体细节
17.-XX:+PrintGC:表示在控制台上打印出GC信息
18.-XX:PretenureSizeThreshold=3145728:表示对象大于3145728(3M)时直接进入老年代分配,这里只能以字节作为单位
19.-XX:MaxTenuringThreshold=1:表示对象年龄大于1,自动进入老年代,如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代,对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代的存活时间,增加在年轻代被回收的概率
20.-XX:CompileThreshold=1000:表示一个方法被调用1000次之后,会被认为是热点代码,并触发即使编译
21.-XX:+PrintHeapAtGC:表示可以看到每次GC前后堆内存布局
22.-XX:+PrintTLAB:表示可以看到TLAB的使用情况
23.-XX:+UseSpining:开启自旋锁
24.-XX:PreBlockSpin:更改自旋锁的自选次数,使用这个参数必须先开启自旋锁
25.-XX:UseSerialGC:表示使用JVM的串行垃圾回收机制,该机制适用于单核CPU的环境下
26.-XX:+UseParallelGC:表示使用JVM的并行垃圾回收机制,该机制适用于多cpu机制,同时对响应时间无强硬要求的环境下,使用-XX:ParllelGCThreads=设置并行垃圾回收的线程数,此值可以设置与机器处理器数量相等
27.-XX:+UseParallelOldGC:表示年老代使用并行的垃圾回收机制
28.-XX:UseConcMarkSweepGC:表示使用并发模式的垃圾回收机制,该模式适用于对响应时间要求高,具有多CPU的环境下
29.-XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值
30.-XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低响应时间或者收集频率等,此值建议使用并行收集器,一直打开
JVM调优目标-何时需要做JVM调优
1.Heap内存(老年代)持续上涨达到设置的最大内存值
2.Full GC次数频繁
3.GC停顿时间过长(超过1s)
4.应用出现OutOfMemory等内存异常
5.应用中有使用本地缓存且占用大量内存空间
6.系统吞吐量与性能不高或下降
JVM调优实战
1.Major GC和Minor GC频繁
首先优化Minor GC频繁问题,通常情况下,由于新生代空间较小,Eden区很快被填满,就会频繁Minor GC,因此可以通过增大新生代空间来降低Minor GC的频率,例如在相同的内存分配率的前提下,新生代的Eden区增加一倍,Minor GC的此时就会减少一半
扩容Eden区虽然可以减少Minor GC的此时,但会增加单次Minor GC时间么?扩容后,Minor GC时增加了T1(扫描时间),但省去T2(复制对象)的时间,更重要的是对于虚拟机来说,复制对象的成本要远高于扫描成本,所以单次Minor GC时间更多取决于GC后存活对象的数量,而非Eden区的大小。因此如果堆中短期对象很多,那么扩容新生代,单次Minor GC时间不会显著增加
2.请求高峰期发生GC,导致服务可用性下降
由于跨代引用的存在,CMS在Remark阶段必须扫描整个堆,同时为了避免扫描时新生代有很多对象,增加了可中断的预清理阶段用来等待Minor GC的发生,只是该阶段有时间限制,如果超时等不到MinorGC,Remark时新生代仍然有很多对象,我们的调优策略是,通过参数强制Remark前进行一次Minor GC,从而降低Remark阶段的时间。另外,类似的JVM是如何避免Minor GC时扫描全堆的?通过统计信息显示,老年代持有新生代对象引用的情况不足1%,根据这一特性JVM引入了卡表(card table)来实现这一目的。卡表的具体策略时将老年代的空间分成大小为512B的若干张卡(card)。卡表本身是单字节数组,数组中的每个元素对应着一张卡,当发生老年代引用新生代时,虚拟机将该卡对应的卡表元素设置为适当的值。之后Minor GC时通过扫描卡表就可以很快的识别那些卡中存在老年代指向新生代的引用。这样虚拟机通过空间换时间的方式,避免了全堆扫描
3.STW过长的GC
对于性能要求很高的服务,建议将MaxPermSize和MinPermSize设置成一致(JDK8开始,Perm区完全消失,转而使用元空间,而元空间时直接存在内存中,不在JVM中),Xms和Xmx也设置为相同,这样可以减少内存自动扩容和收缩带来的性能损失。虚拟机启动的时候就会把参数锁设定的内存全部化为私有,即使扩容前有一部分内存不会被用户代码用到,这部分内存存在虚拟机中被标识为虚拟内存,也不会交给其他进程使用
4.外部命令导致系统缓慢
5.由windows虚拟内存导致的长时间停顿
CMS GC问题分析与解决
场景一:动态扩容引起的空间震荡
场景二:显式GC的去与留
场景三:MetaSpace区OOM
场景四:过早晋升
场景五:CMS Old GC频繁
场景六:单次CMS Old GC耗时长
场景七:内存碎片&收集器退化
场景八:对外内存OOM
场景九:JNI引发的GC问题
Java基础
HashMap和ConcurrentHashMap
由于HashMap是线程不同步的,虽然处理数据的效率高,但是在多线程的情况下存在着安全问题,因此设计了CurrentHashMap来解决多线程安全问题
HashMap在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的
HashMap的环:若当前线程此时获得entry节点,但是被线程中断无法继续执行,此时线程二进入transfer函数,并把函数顺利执行,此时新表中的某个位置有了节点,之后线程一获得执行权继续执行,因为并发transfer,所以两者都是扩容的同一个链表,当线程一执行到e.next=new table[i]的时候,由于线程二之前数据迁移的原因导致此时new table[i]上就有entry存在,所以线程一执行的时候,会将next节点,设置为自己,导致自己互相使用next引用对方,因此产生链表,导致死循环
在JDK1.7版本中,ConcurrentHashMap维护了一个Segment数组,Segment这个类继承了重入锁ReentrantLock,并且该类里面维护了一个HashEntry<K,V>[] table数组,在写操作put,remove,扩容的时候,会对Segment加锁,所以仅仅影响这个Segment,不同的segment还是可以并发的,所以解决了线程的安全问题,同时又采用分段锁也提升了并发的效率,在JDK1.8版本中,ConcurrentHashMap摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的hashMap
HashMap如果我想要让自己的Object作为k应该怎么办
1.重写HashCode()是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但是可能会导致更多的hash碰撞
2.重写equals()方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这个特性,目的是为了保证key在哈希表中的唯一性(Java建议重写equals方法的时候重写hashcode的方法)
HashMap和TreeMap的区别
对象的内存分配
ThreadLocalContext
volatile
volatile在多处理器开发中保证了共享变量的可见性。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值(线程内存,私有内存)
Atomic类的CAS操作
CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并交换。CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做,整个比较并替换的操作是一个原子操作。如Intel处理器,比较并交换通过指令的cmpxchg系列实现
CAS操作ABA问题
如果这段期间它的值曾经被修改为了B,后来又改回了A,那CAS操作就会误以为它从来没有被改变过,Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性
Synchronized和Lock的区别
1.首先synchronized是java内置关键字在jvm层面,Lock是个java类
2.synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁,并且可以主动尝试去获取锁
3.synchronized会自动释放锁(a线程执行完同步代码会释放锁;b线程执行过程中发生异常会释放锁),Lock需要在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁
4.用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了
5.synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可中断、可公平(两者皆可)
6.Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题
AQS理论的数据结构
AQS内部有3个对象,一个是state(用于计数器,类似gc的回收计数器),一个是线程标记(当前线程是谁加锁的),一个是阻塞队列
AQS是自旋锁,在等待唤醒的时候,经常会使用自旋的方式,不停地尝试获取锁,直到被其他线程获取成功
AQS有两个队列,同步队列和条件队列。同步队列依赖一个双向链表来完成同步状态的管理,当前线程获取同步状态失败后,同步器会将线程构建成一个节点,并将其加入同步队列中,通过signal或signalAll将条件队列中的节点转移到同步队列
如何指定多个线程的执行顺序
1.设定一个orderNum,每个线程执行结束之后,更新orderNum,指明下一个要执行的线程。并且唤醒所有的等待线程
2.在每一个线程的开始,要while判断orderNum是否等于自己的要求值,不是,则wait,是则执行本线程
为什么要使用线程池
1.减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务
2.可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存,而把服务器累趴下
核心线程池ThreadPoolExecutor内部参数
1.corePoolSize:指定了线程池中的核心线程数量
2.maximumPoolSize:指定了线程池中的的最大线程数量
3.keepAliveTime:线程池维护线程所允许的空闲时间
4.unit:keepAliveTime的单位
5.workQueue:任务队列,被提交但尚未被执行的任务
6.threadFactory:线程工厂,用于创建线程,一般用默认的即可
7.handler:拒绝策略,当任务太多来不及处理,如何拒绝任务
线程池的执行流程
1.如果正在运行的线程数量小于corePoolSize,那么马上创建线程执行这个任务
2.如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放到队列
3.如果这时候队列满了,而且正在运行的线程数量小于maximumPoolSize,那么还是要创建非核心线程立即运行这个任务
4.如果队列满了,而且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会执行拒绝策略
线程池都有哪几种工作队列
1.ArrayBlockingQueue:底层是数组,有界队列,如果我们要使用生产者-消费者模式,这是非常好的选择
2.LinkedBlockingQueue:底层是链表,可以当作无界和有界队列来使用,所以大家不要以为它就是无界队列
3.SynchronousQueue:本身不带有空间来存储任何元素,使用上可以选择公平模式和非公平模式
4.PriorityBlockingQueue:无界队列,基于数组,数据结构为二叉堆,数组第一个也是数的根节点总是最小值
举例ArrayBlockingQueue实现并发同步的原理:原理就是读操作和写操作都需要获取到AQS独占锁才能进行操作。如果队列为空,这个时候读操作的线程进入到读线程队列排队,等待写线程写入新的元素,然后唤醒读线程队列的第一个等待线程。如果队列已满,这个时候写操作的线程进入到写线程队列排队,等待读线程将队列元素移除腾出空间,然后唤醒写线程队列的第一个等待线程
线程池的拒绝策略
1.ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常
2.ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常
3.ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务
4.ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务
线程池的线程数量怎么确定
1.一般来说,如果是CPU密集型应用,则线程池大小设置为N+1
2.一般来说,如果是IO密集型应用,则线程池大小设置为2N+1
3.在IO优化中,线程等待时间所占比例越高,需要越多线程,线程CPU时间所占比例越高,需要越少线程。这样的估算公式可能更适合:最佳线程数目=((线程等待时间+线程CPU时间)/线程CPU时间)*CPU数目
如何实现一个带优先级的线程池
利用priority参数,继承ThreadPoolExecutor使用PriorityBlockingQueue优先级队列
ThreadLocal的原理和实现
ThreadLocal变量,线程局部变量,同一个ThreadLocal所包含的对象,在不同的Thread中有不同的副本。ThreadLocal变量通常被private static修饰。当一个线程结束时,它所使用的所有ThreadLocal相对的实例副本都可被回收
一个线程内可以存放多个ThreadLocal对象,所以其实是ThreadLocal内部维护了一个Map,这个Map不是直接使用的HashMap,而是ThreadLocal实现的一个叫做ThreadLocalMap的静态内部类。而我们使用的get()、set()方法其实都是调用了这个ThreadLocalMap类对应的get()、set()方法。这个储值的Map并非ThreadLocal的成员变量,而是java.lang.Thread类的成员变量。ThreadLocalMap实例时作为java.lang.Thread的成员变量存储的,每个线程有唯一的一个threadLocalMap。这个map以ThreadLocal对象为key,“线程局部变量”为值,所以一个线程下可以保存多个“线程局部变量”。对ThreadLocal的操作,实际委托给当前Thread,每个Thread都会有自己的独立的ThreadLocalMap实例,存储的仓库是Entry[] table;Entry的key为ThreadLocal,value为存储内容;因此在并发环境下,对ThreadLocal的set或get,不会有任何问题。由于Tomcat线程池的原因,最初使用的“线程局部变量”保存的值,在下一次请求依然存在(同一个线程处理),这样每次请求都是在本线程中取值,所以在线程池的情况下,处理完成后主动调用该业务threadLocal的remove()方法,将“线程局部变量”清空,避免本线程下次处理的时候依然存在旧数据。
ThreadLocal为什么要使用弱引用和内存泄漏问题
jre扩展
Java提供的运行环境只是核心类,不能满足用户的多种需求,用户可以自定义类。Java运行环境提供了扩展【\jre\lib\ext】,只需将类打包成jar文件,放入扩展中,就可以直接在程序中import使用了
HashSet和HashMap
HashSet的value存的是一个static finial PRESENT = new Object()。而HashSet的remove是使用HashMap实现,则是map.remove,而map的移除会返回value,如果底层value都是存null,显然将无法分辨是否移除成功
Boolean占几个字节
未精确定义字节。Java语言表达式所操作的boolean值,在编译之后都使用Java虚拟机中的int数据类型来代替,而boolean数组将会被编码成Java虚拟机的byte数组,每个元素boolean元素占8位
阻塞非阻塞与同步异步的区别
1.同步和异步关注的是消息通知机制,所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用成功,就得到返回值了。而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果,而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。
2.阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。阻塞调用是指调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会返回,非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
Java SPI
由于双亲委派模型损失了一丢丢灵活性,就比如java.sql.Drive这个东西。JDK只能提供一个规范接口,而不能提供实现。提供实现的是实际的数据库提供商。提供商的库总不能放JDK目录里吧。Java从1.6搞出了SPI就是为了优雅的解决这类问题——JDK提供接口,供应商提供服务,编程人员编码时面向接口编程,然后JDK能够自动找到合适的实现
需要一个目录
META/service
放到ClassPath下面
目录下面放置一个配置文件
文件名是要扩展的接口全名
文件内部为要实现的接口实现类
文件必须是UTF-8编码
如何使用
ServiceLoad.load(xx.class)
ServiceLoad<HelloInterface> loads = ServiceLoad.load(HelloInterface.class)
设计模式
原型模式的应用场景
创建大对象,初始化比较繁琐的对象,通过克隆
hashcode算法
Spring
什么是三级缓存
1.第一级缓存:单例缓存池singletonObjects
2.第二级缓存:早期提前暴露的对象缓存earlySingletonObjects(属性还没有值,对象也没有被初始化)
3.第三级缓存:singletonFactories单例对象工厂缓存
三级缓存详解:根据 Spring 源码写一个带有三级缓存的 IOC
Spring如何解决循环依赖问题
Spring使用了三级缓存解决了循环依赖的问题。在populateBean()给属性赋值阶段里面Spring会解析你的属性,并且赋值,当发现,A对象里面依赖了B,此时又会走getBean方法,但这个时候,你去缓存中是可以拿得到的。因为我们在对createBeanInstance对象创建完成以后已经放入了缓存中,所以创建B的时候发现依赖A,直接就从缓存中去拿,此时B创建完,A也创建完,一共执行了4次,至此Bean的创建完成,最后将创建好的Bean放入单例缓存池中
BeanFactory和ApplicationContext的区别
1.BeanFactory是Spring里面最低层的接口,提供了最简单的容器的功能,只提供了实例化对象和拿对象的功能
2.ApplicationContext应用上下文,继承BeanFactory接口,它是Spring的一个更高级的容器,提供了更多的有用的功能,如国际化,访问资源,载入多个(有继承关系)上下文,使得每一个上下文都专注于一个特定的层次,消息发送、响应机制、AOP等
3.BeanFactory在启动的时候不会去实例化Bean,只有从容器中拿Bean的时候才会去实例化。ApplicationContext在启动的时候就把所有的Bean全部实例化了,它还可以为Bean配置lazy-init=true来让Bean延迟实例化
动态代理的实现方式,AOP的实现方式
1.JDK动态代理:利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理
2.CGlib动态代理:利用ASM(开源的Java字节码编辑库,操作字节码)开源包,将代理对象类的class文件加载进来,通过修改其字节码生成子类来处理
3.区别:JDK代理只能对实现接口的类生成代理;CGlib是针对类实现代理,对指定的类生成一个子类,并覆盖其中的方法,这种通过继承类的实现方式,不能代理final修饰的类
@Transactional错误使用失效场景
1.@Transaction在private上:当标记在protected、private、package-visible方法上时,不会产生错误,但也不会表现出为它指定的事务配置。可以认为它作为一个普通方法参与到一个public方法的事务中。
2.@Transaction的事务传播方式配置错误
3.@Transaction注解属性rollbackFor设置错误:Spring默认抛出了未检查unchecked异常(继承自RuntimeException的异常)或者Error才回滚事务;其他异常不会触发回滚事务
4.同一个类中方法调用,导致@Transaction失效:由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理
5.异常被catch捕获导致@Transaction失效
6.数据库引擎不支持事务
Spring中的事务传播机制
1.REQUIRED(默认,常用):支持使用当前事务,如果当前事务不存在,创建一个新事务。eg:方法B用REQUIRED修饰,方法A调用方法B,如果方法A当前没有事务,方法B就新建一个事务(若还有C则B和C在各自的事务中独立执行),如果方法A有事务,方法B就加入到这个事务中,当成一个事务
2.SUPPORTS:支持使用当前事务,如果当前事务不存在,则不使用事务
3.MANDATORY:强制,支持使用当前事务,如果当前事务不存在,则抛出Exception
4.REQUIRES_NEW(常用):创建一个新事务,如果当前事务存在,把当前事务挂起。eg:方法B用REQUIRES_NEW修饰,方法A调用方法B,不管方法上有没有事务方法B都新建一个事务,在该事务中执行
5.NOT_SUPPORTED:无事务执行,如果当前事务存在,把当前事务挂起
6.NEVER:无事务执行,如果当前有事务则抛出Exception
7.NESTED:嵌套事务,如果当前事务存在,那么在嵌套的事务中执行,如果当前事务不存在,则表现跟REQUIRED一样
Spring中Bean的是生命周期
1.实例化Instantiation
2.属性赋值Populate
3.初始化Initialization
4.销毁Destruction
Spring的后置处理器
1.BeanPostProcessor:Bean的后置处理器,主要在bean初始化前后工作(before和after两个回调中间只处理了init-method)
2.InstantiationAwareBeanPostProcessor:继承于BeanPostProcessor,主要在实例化bean前后工作(TargetSource的AOP创建代理对象就是通过该接口实现)
3.BeanFactoryPostProcessor:Bean工厂的后置处理器,在bean定义(bean definitions)加载完成后,bean尚未初始化前执行
4.BeanDefinitionRegistryPostProcessor:继承于BeanFactoryPostProcessor。其自定义的方法postProcessBeanDefinitionRegistry会在bean定义(bean definitions)将要加载,bean尚未初始化前执行,即在BeanFactoryPostProcessor的postProcessBeanFactory方法前被调用
Spring MVC的工作流程(源码层面)
参考文章
消息队列
为什么需要消息队列
解耦,异步处理,削峰/限流
Kafka的文件存储机制
Kafka中消息是以topic进行分类的,生产者通过topic向Kafka broker发送消息,消费者通过topic读取数据。然而topic在物理层面上又能以partition为分组,一个topic可以分成若干个partition。partition还可以细分为segment,一个partition物理上由多个segment组成,segment文件由两部分组成,分别为“.index”文件和“.log”文件,分别表示segment索引文件和数据文件。这两个文件的命令规则为:partition全局的第一个segment从0开始,后续每个segment文件名为上一个segment文件最后一条消息的offset值
Kafka如何保证可靠性
如果我们要往Kafka对应的主题发送消息,我们需要通过Producer完成。前面讲过Kafka主题对应了多个分区,每个分区下面又对应了多个副本;为了让用户设置数据可靠性,Kafka在Producer里面提供了消息确认机制。也就是我们可以通过配置来决定消息发送到对应分区的几个副本才算消息发送成功。可以在定义Producer时通过acks参数指定。这个参数支持一下三种值:
acks=0:意味着如果生产者能够通过网络把消息发送出去,那么就认为消息已成功写入Kafka。在这种情况下还是有可能发生错误,比如发送的对象无法被序列化或者网卡发生故障,但如果是分区离线或整个集群长时间不可用,那就不会收到任何错误。在acks=0模式下的运行速度是非常快的(这就是为什么很多基准都是基于这个模式),可以得到惊人的吞吐量和快带利用率,不够如果选择了这种模式,一定会丢失一些消息
acks=1:意味着若Leader在收到消息并把它写入到分区数据文件(不一定同步到磁盘上)时会返回确认或错误响应。在这个模式下,如果发生正常的Leader选举,生产者会在选举时收到一个LeaderNotAvailableException异常,如果生产者能恰当地处理这个错误,它会重试发送消息,最终消息会安全到达新的Leader那里,不过在这个模式下仍然有可能丢失数据,比如消息已经成功写入Leader,但在消息被复制到follower副本之前Leader发生崩溃
acks=all(这个和request.required.asks=-1含义一样):意味着Leader在返回确认或错误响应之前,会等待所有副本都收到消息。如果和min.insync.replicas参数结合起来,就可以决定在返回确认前至少由多少个副本能够收到消息,生产者会一直重试直到消息被成功提交。不过这也是最慢地做法,因为生产者在继续发送其他消息之前需要等待所有副本都收到当前的消息
Kafka消息时采用Pull模式,还是Push模式
Kafka最初考虑地问题是:customer应该从brokers拉取消息还是brokers将消息推送到consumer,也就是pull还是push。在这方面,Kafka遵循了一种大部分消息系统共同地传统的涉及:producer将消息推送到broker,consumer从broker拉取消息。push模式下,当broker推送地速率远大于consumer消费地速率时,consumer恐怕就要崩溃。最终Kafka还是采取了传统地pull模式。Pull模式的另外一个好处是consumer可以自主决定是否批量从broker拉取数据。Pull有个缺点,如果broker没有可供消费的消息,将导致consumer不断在循环中轮询,直到新消息到达。为了避免这点,Kafka有个参数可以让consumer阻塞直到新消息到达。
Kafka是如何实现高吞吐率的
1.顺序读写:Kafka的消息是不断追加到文件中的,这个特性使Kafka可以充分利用磁盘的顺序读写性能
2.零拷贝:跳过“用户缓冲区”的拷贝,建立一个磁盘空间和内存的直接映射,数据不再复制到“用户态缓冲区”
3.文件分段:Kafka的队列topic被分为了多个区partition,每个partition又分为多个段segment,所以一个队列中的消息实际上是保存在N多个片段文件中
4.批量发送:Kafka允许进行批量发送消息,先将消息缓存在内存中,然后一次请求批量发送出去
5.数据压缩:Kafka还支持对消息集合进行压缩,Producer可以通过GZIP或Snappy格式对消息集合进行压缩
Kafka判断一个节点还活着的两个条件
1.节点必须可以维护和ZooKeeper的连接,ZooKeeper通过心跳机制检查每个节点的连接
2.如果节点是个follower,他必须能及时地同步leader的写操作,延时不能太久
0 条评论
下一页