计算机操作系统
进程
进程与线程
进程:资源分配的基本单位
线程:cpu调度的基本单位
进程通信方式
管道pipe:是一种半双工的通信方式,数据只能单向流动,且只能在父子进程中流动
命名管道FIFO:也是半双工的,但允许无亲缘进程间通信
消息队列:消息组成的链表
共享存储:映射一段能被其他进程访问到的内存,这个内存由一个进程创建,但所有进程都能访问。
信号量Semaphore:类似锁机制,可控制进程对资源的使用。
套接字socket:可用于不同机器间进程的通信
信号量:用于通知进程某个事件已发生
进程调度算法
1) 先来先服务
2) 短作业优先
3) 优先级调度
4) 时间片轮转
5) 高响应比优先
系统调用与中断机制
进程的切换
死锁
死锁的条件
1) 资源互斥
2) 不可剥夺
3) 请求与保持
4) 环路等待
处理死锁方法
1) 预防
破坏四个条件
2) 避免
银行家算法,阻止系统进入不安全状态。不安全不一定产生死锁
3) 检测解除
内存
动态分区分配内存
1) 首次适应算法
2) 下次适应算法
3) 最佳适应算法
4) 最差适应算法
分页机制
虚拟内存与物理内存
页面置换算法
1) 最佳置换
2) 先进先出
3) 最近最少使用
4) Clock算法
Belady异常:随着物理块数增加效率降低
分页与分段
计算机网络
七层协议
物理层,数据链路层,网络层,传输层,会话层,表示层,应用层
数据链路层:比特流传输时可能出现错误,链路层使用差错检测、差错控制和流量控制等方法,向网络层提供高质量的数据传输服务。
网络层:对网络进行子网划分,使得每个主机在网络上都是唯一的,建立两个主机之间的连接,选择合适的路由建立通信
传输层:建立主机端到端的连接,屏蔽下层通信细节,直接将可靠数据交由端口对应服务
Tcp协议
三次握手
客户端向服务端发送连接请求:SYN=1,x;服务端回复:SYN=1,ACK=1,x+1,y;客户端回复:ACK=1,y+1
四次挥手
客户端向服务端发送断开请求:FIN=1,m;服务端回复:ACK=1,m+1;服务端发送:FIN=1,n,客户端回复:ACK=1,n+1
为什么要等2TimeWait,因为网络状态是不可靠的,可能会出现回复了最后一个ack但对方没收到,一直空等。等待这个时间能保证对方如果没收到便可以超时重发。
可靠传输
保证数据的不丢失、有序到达、正确
方法:校验机制保证数据正确性,重传和ACK机制保证数据不丢失,滑动窗口保证数据有序到达且可以进行流量控制。拥塞控制。
流量控制(滑动窗口机制)
1、停等协议
发送窗口=1,接收窗口=1
每次只移动一格,发送方发送一格消息后会等待这个消息的ack,收到后才会发送下一个。由于每次接收方收到的报文一定是上次发送方发送的,所以不需要序号。
2、后退N帧
发送窗口=n,接收窗口=1
发送方可以连续发送,接收方施行累计确认方式,即可以不用对每个报文进行确认,确认一个报文后,该报文之前的所有报文自动确认。如果发送方发送了1/2/3/4/5这些报文,但收到了报文3的确认,这就说明4/5号报文没有收到,下次就要从4号报文开始重新发送。
3、选择重传
发送窗口=n,接收窗口=n
gbkN的缺点显而易见:接收方无法存储报文,导致发送方重复发送了很多报文。选择重传解决了这个问题,接收方有一个缓冲区,可以存储已经收到的正确报文,等到集齐了所有连续的正确报文后一起交付。缓冲区有一个过期时间,时间到了就会清除。
拥塞控制
拥塞控制中有个发送窗口大小,这个大小由rwnd和cwnd确定:接收窗口(rwnd)是接收端给的限制;拥塞窗口(cwnd)是发送端的限制。cwnd=min(cwnd,rwnd)。
慢启动
在开始传输的一段时间,发送端每收到一个 ACK,拥塞窗口大小加1,也就是说,每经过一个 RTT,cwnd 翻倍。如果说初始窗口为 10,那么第一轮 10个报文传完且发送端收到 ACK 后,cwnd 变为 20,第二轮变为 40,第三轮变为 80,依次类推。
拥塞避免
在拥塞控制中有一个拥塞阈值,当发送窗口达到这个阈值后,就不会成倍的增长,而是每收到一个ack发送窗口只增加1/cwnd。也就是一轮窗口数据发送完,cwnd才增加1。
快重传与快恢复
快重传:在 TCP 传输的过程中,如果发生了丢包,即接收端发现数据段不是按序到达的时候,接收端的处理是重复发送之前的 ACK。比如第 5 个包丢了,即使第 6、7 个包到达的接收端,接收端也一律返回第 4 个包的 ACK。当发送端收到 3 个重复的 ACK 时,意识到丢包了,于是马上进行重传,不用等到超时才重传。
快恢复:发送端收到三次重复 ACK 之后,发现丢包,觉得现在的网络已经有些拥塞了,自己会进入快速恢复阶段。在这个阶段,发送端如下改变:(1)拥塞阈值降低为 cwnd 的一半;(2)cwnd 的大小变为拥塞阈值;(3)cwnd 恢复像慢启动一样的线性增加
nagle算法
糊涂窗口综合征:指当发送端应用进程产生数据很慢、或接收端应用进程处理接收缓冲区数据很慢,或二者兼而有之;就会使应用进程间传送的报文段很小,特别是有效载荷很小; 极端情况下,有效载荷可能只有1个字节;传输开销有40字节(20字节的IP头+20字节的TCP头)
解决方法:nagle算法,当第一次发送数据时不用等待,就算是 1byte 的小包也立即发送,后面发送满足下面条件之一就可以发了: (1)数据包大小达到最大段大小MSS(2)之前所有包的 ACK 都已接收到
tcp计时器和时间戳
计时器种类
(1)重传计时器
目的:为了控制丢失的报文段或者丢弃的报文段。这段时间为对报文段的等待确认时间。
创建时间:在TCP发送报文段时,会创建对次特定报文段的重传计时器。
可能发生的两种情况:在截止时间(通常为60秒)到之前,已经收到了对此特定报文段的确认,则撤销计时器;在截止时间到了,但为收到对此特定报文段的确认,则重传报文段,并且将计时器复位。
重传时间:2*RTT(Round Trip Time,为往返时间)。
(2)坚持计时器
目的:主要解决零窗口大小通知可能导致的死锁问题
死锁问题的产生:当接收端的窗口大小为0时,接收端向发送端发送一个零窗口报文段,发送端即停止向对端发送数据。此后,如果接收端缓存区有空间则会重新给发送端发送一个窗口大小,即窗口更新。但接收端发送的这个确认报文段有可能会丢失,而此时接收端不知道已经丢失并认为自己已经发送成功,则一直处于等待数据的状态;而发送端由于没有收到该确认报文段,就会一直等待对方发来新的窗口大小,这样一来,双方都处在等待对方的状态,这样就形成了一种死锁问题。如果没有应对措施,这种局面是不会被打破的。为了解决这种问题,TCP为每一个连接设置了坚持计时器。
工作原理:当发送端TCP收到接收端发来的零窗口通知时,就会启动坚持计时器。当计时器的期限到达时,发送端就会主动发送一个特殊的报文段告诉对方确认已经丢失,必须重新发送。【这个特殊的报文段就称为探测报文段,探测报文段只有1个字节的大小,里边只有一个序号,该序号不需要被确认,甚至在计算其他部分数据的确认时该序号会被忽略。】
截止期的设置:设置为重传时间的值。但如果没有收到接收端的响应,则会发送另一个探测报文段,并将计时器的值加倍并复位,直到大于门限值(一般为60秒)。在此之后,发送端会每隔60秒发送一个探测报文段,直到窗口重新打开。
(3)保活计时器
目的:主要是为了防止两个TCP连接出现长时间的空闲。当客户端与服务器端建立TCP连接后,很长时间内客户端都没有向服务器端发送数据,此时很有可能是客户端出现故障,而服务器端会一直处于等待状态。保活计时器就是解决这种问题而生的。
工作原理:每当服务器端收到客户端的数据时,都将保活计时器重新设置(通常设置为2小时)。过了2小时后,服务器端如果没有收到客户端的数据,会发送探测报文段给客户端,并且每隔75秒发送一个,当连续发送10次以后,仍没有收到对端的来信,则服务器端认为客户端出现故障,并会终止连接。
(4)Times-Wait计时器
时间等待计时器是在连接终止期间使用的。当TCP关闭连接时并不是立即关闭的,在等待期间,连接还处于过渡状态。这样就可以使重复的FIN报文段在到达终点之后被丢弃。
时间设置:一般为报文段寿命期望值的2倍。
时间戳作用
(1)计算往返时延 RTT(Round-Trip Time)
比如现在 a 向 b 发送一个报文 s1,b 向 a 回复一个含 ACK 的报文 s2 那么:
step 1: a 向 b 发送的时候,timestamp 中存放的内容就是 a 主机发送时的内核时刻 ta1。
step 2: b 向 a 回复 s2 报文的时候,timestamp 中存放的是 b 主机的时刻 tb, timestamp echo字段为从 s1 报文中解析出来的 ta1。
step 3: a 收到 b 的 s2 报文之后,此时 a 主机的内核时刻是 ta2, 而在 s2 报文中的 timestamp echo 选项中可以得到 ta1, 也就是 s2 对应的报文最初的发送时刻。然后直接采用 ta2 - ta1 就得到了 RTT 的值。
(2)防止序列号的回绕问题
这个问题可以在滑动窗口角度进行处理,也可以通过时间戳判断新旧报文。
超时重传时间(RTO)如何计算?
对于每个报文都会设置一个超时重传的计时器,这个超时的时间是要根据RTT来计算得到,但RTT是一直在变动的,所以需要一个实时计算RTO的算法。RTO的计算有两种方法:平滑往返时间(SRTT)和Jacobson/Karels算法。
SRTT:
α 是平滑因子,建议值是0.8,范围是0.8 ~ 0.9。
这个算法过程还是很简单的,但是也存在一定的局限,就是在 RTT 稳定的地方表现还可以,而在 RTT 变化较大的地方就不行了,因为平滑因子 α 的范围是0.8 ~ 0.9, RTT 对于 RTO 的影响太小。
第一步: 计算SRTT,公式如下:
注意这个时候的 α跟经典方法中的α取值不一样了,建议值是1/8,也就是0.125。
第二步: 计算RTTVAR(round-trip time variation)这个中间变量。
β 建议值为 0.25。这个值是这个算法中出彩的地方,也就是说,它记录了最新的 RTT 与当前 SRTT 之间的差值,给我们在后续感知到 RTT 的变化提供了抓手。
第三步: 计算最终的RTO:
μ建议值取1, ?建议值取4。
这个公式在 SRTT 的基础上加上了最新 RTT 与它的偏移,从而很好的感知了 RTT 的变化,这种算法下,RTO 与 RTT 变化的差值关系更加密切。
各类协议
Rip协议
路由信息协议,使用UDP,通过定时和相邻节点交换路由表来建立完整路由信息。维护的是到每个网络的跳数,16为不可达(防止出现环路)。坏消息传的慢:A邻居点C出现故障,B不知道就给A发了消息,A就以为B可以到达,就给B发,产生了环路。
DHCP协议
动态主机配置协议,使用UDP。向DhcpServer发送请求申请地址,选择收到的最好的地址广播给DhcpServer,DhcpServer回复“确认”
OSPF协议
开放式最短路径优先。链路状态变化时,给当前网络所有节点发信息(洪泛法)。可再分自治系统和自治区域,适用于大网络。分组类型:问候分组(发现维持邻站可达性)、数据库描述分组(发送数据库信息)、链路状态请求分组(请求路由信息)、链路状态更新分组(洪泛更新)、链路状态确认分组
ICMP协议
网际控制报文协议。报告错误情况,如:终点不可达(主机不能交付),源点抑制(拥塞)、时间超过(TTL为0)、参数问题(首部出错)、改变路由。对本机不使用,对icmp不使用,对组播不使用,对第一个分片之后的不使用。
IGMP协议
网际组管理协议,基于TCP。
ARP协议
地址解析协议。用于查询ip地址对应的mac地址。A想获取B的地址,(1)先查ARP表,(2)发现没有则广播分组,(3)B收到后单播给A自己的地址,(4)A收到后更新表并发送。
DNS协议
DNS协议就是用来将域名解析到IP地址的一种协议
域名服务器主要分为:根域名服务器、*域名服务器、权限域名服务器、本地域名服务器。
根域名服务器:负责全球互联网域名根服务器、域名体系和IP地址等的管理,全球共有13台根服务器。
*域名服务器:负责管理所有的二级域名
权限域名服务器:负责管理一个区。当一个权限域名服务器还不能给出最后的查询回答时,就会告诉查询请求的DNS客户进程,下一步应当找哪一个权限域名服务器
本地域名服务器:可以看成是默认域名服务器,DNS客户进程收到主机发送过来的域名后,就会最初向该域名服务器发送查询请求
查询过程:(1)首先会查找本机缓存,如果本地缓存查找失败则会发送请求给DNS服务器(2)本地DNS服务器收到查询请求后,会在所管理的区域记录中查找,失败后,发送到根域名服务器。(3)根域名服务器会解析一部分域名,返回给DNS服务器一个域名服务器的地址,这个服务器可以查找到下一级域名(4)依次往下查询,最后再目标域名服务器查找到对应的ip地址,返回给DNS服务器,再返回到浏览器进程。
纯ALOHA
随机发,信道冲突就随机等待时间重发
时隙ALOHA
只能在时隙开始时发送
CSMA协议
载波监听多路访问。在发送前进行侦听,如果空闲以以下概率发送:1-坚持(立刻发送),p-坚持(以p概率发送),非坚持(立刻发送,忙的话放弃侦听)
CSMA/CA
载波监听多路访问冲突检测。先听后发,边听边发,冲突停发。
从输入URL开始计算机做了什么
(1) 使用DNS协议解析地址
(2) 获得地址后通过TCP协议建立稳定连接(三次握手)
(3) 中间经历网络层的路由,
信息安全
https
https是经过SSL安全加密的http协议,因为http是明文传输,明文传输的过程中很可能被监听。
SSL是网景公司提出的传输层之上应用层之下的协议。SSL的功能:(1)可以对服务器进行认证(2)可以对用户进行认证(可选)(3)建立加密通信
SSL包括两部分,握手协议和记录协议。握手协议用于在双方建立可靠连接。记录协议用于传输数据块。
SSL握手过程:
(1)用户通过浏览器请求https网站,服务器收到请求,选择浏览器支持的加密和hash算法,同时返回数字证书给浏览器,包含颁发机构、网址、公钥、证书有效期等信息。
(2)浏览器对证书的内容进行校验,如果有问题,则会有一个提示警告。否则,就生成一个随机数X,同时使用证书中的公钥进行加密,并且发送给服务器。
(3)服务器收到之后,使用私钥解密,得到随机数X,然后使用X对网页内容进行加密,返回给浏览器
(4)浏览器则使用X和之前约定的加密算法进行解密,得到最终的网页内容
SQL注入
应用接收用户输入,该输入被构造成SQL语句并获得执行。比如在select语句中,需要输入用户名和密码,但我只输入:a or 1#,到mysql中会被拼接成where Username = ‘a’ or 1# ‘ and Password = ‘$password‘"; #将后面的密码判断语句给注释了,前面的or 1使得判断永远成立,造成登录异常。
防御方法:
- 过滤特殊符号,如#、’、--等
- 显示区分数据和命令,做预编译
跨站点请求伪造(CSRF)
Cookie是在客户端用于保存用户信息的数据结构。Cookie保存在浏览器,只要你没有关闭浏览器,且cookie还没有到自动销毁的时间,当打开另一个页面时,cookie也依然存在,记录着你的访问信息。此时如果在一个恶意网站中的某一张图片上,黑客引入了一个链接,点击后直接向服务器发送删除博客的信息,一般情况服务器会判断用户信息,但由于用户cookie已经被获取,服务器就会执行删除行为。
防御方法:
- 使用Referer,在http中打开referer字段,会验证请求是否是从合理页面发送出来的。
- CSRF防御的本质就是让黑客猜不到所需要的参数,那么在传送的数据中加一个隐藏字段,即token字段,该字段被服务器和客户端双方持有,足够随机,黑客无法猜到,也就无法伪造请求。
跨站脚本攻击(XSS)
通过在输入框输入js代码,注入到html中,浏览器会把输入的内容与静态页面拼接并解析,js代码就会被执行。黑客可以通过js获取当前用户的cookie,并将cookie发送出去,以此获得用户信息。
- 反射型XSS
只是把用户输入的数据反射给浏览器,比如点击一个恶意的url链接。
- 存储型XSS
存储型指的是恶意代码会被存储在服务器端,比如写一篇满是js代码的博客,所有访问该博客的用户都会执行其中的js代码。
防御方法:
- 使用黑名单,删除用户输入带<script>字段的内容
- 输出转移
- Httponly cookie,只有在使用http传输时才能使用cookie,xss无法读取
点击劫持
Dos
反复的向服务器发送大量连接请求,服务器每收到一个连接就会为其开辟空间,最后会导致资源耗尽,服务器瘫痪。
分布式、大数据与高并发
分布式
CAP理论
一致性、可用性、分区容错性三者只能得二,不可能完全达到三者兼得。
一致性:比如主数据库进行修改,其从数据库也能立刻同步数据变动。强一致性:数据库无论何时访问,都能保证数据的绝对一致;弱一致性:无法保证数据同步,数据库中最后的数据可能就是不同的;最终一致性:不能立即同步,但在一定时间内一定会同步。
可用性:指服务一直可用,而且是正常响应时间。不管什么时候访问,都可以正常的获取数据值。系统能够很好的为用户服务,不出现用户操作失败或者访问超时等用户体验不好的情况。
分区容错性:在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务
目前大部分网站使用的都是AP原则,即保证可用性和分区容错性,舍弃一部分一致性。比如12306的购票网站,系统不会出现无法买票的情况,这是高可用性的体现。但可能会出现网页显示余票充足,但实际购票时又显示无票的情况,这是因为舍弃了一部分一致性的原因,但最终所有的票量会趋于一致。
事务故障恢复
Redo
Undo
RedoUndo
分布式事务
2PC协议
2PC协议是为了解决分布式数据库的一致性问题。将本地原子性提交行为的效果扩展到分布式事务, 保证了分布式事务提交的原子性。分为两个阶段,第一阶段为表决阶段,第二阶段为执行阶段。其中参与的角色分为协调者(C)和参与者(P)。
在提交事务时,首先C启动begin_commit到日志,通知所有P准备提交事务,如果P准备好了则写ready到日志,并告知C,C判断是不是所有P都准备好了,如果是则写commit到日志并全局提交事务,所有P便会进行事务提交。当所有P都提交成功,C进行事务提交。
2PC的故障与恢复
故障发生时间为(1)P写ready前(2)P写ready后(3)C写prepare之后Gcommit之前(4)C写Gcommit之后commit之前。
还可能存在报文丢失,发生于(1)P的ready信息(2)C的prepare报文丢失(3)C的Gcommit丢失(4)P的ack丢失。
2PC无法保证非失效site的非阻断终结(允许事务在非失效的site终结,不必等待失效的site恢复),也不是独立的恢复协议(失效site恢复时,不必求助其他site)。
比如在ready状态的参与者发生了故障,当故障恢复后,它发现自己处于ready状态,但并不知道是否要继续提交事务,所以需要询问其他参与者或协调者。
3PC协议
非阻断协议的充要条件是:(1)不存在提交状态与夭折状态相邻(2)不存在提交状态与不可提交状态相邻(Ready、Wait)。
2PC中处于Ready状态的参与者的下一状态就是提交,因为它一直在等待提交命令的到来,一旦这个命令无法到达或者无法接受,参与者就失去指引了。所以在wait和commit或ready与commit之间增加一个缓冲状态prepare commit,表示预提交。
如果出现了2PC中参与者ready状态的超时(协调者失效),所以site选举新协调者,如果协调者状态在PC之前,则全局终止,在PC之后,全局提交。这个PC状态就解决了wait时不知道到底要不要提交的问题,无阻断。不过如果P在PC状态故障,恢复后依然不知道该怎么做,要询问其他站点,非独立。
强一致性算法
Poxos算法
思想演变
主从思想
Mster更新slave,但当某从机故障,就导致整个系统瘫痪
多数派思想
无法解决多线程下的同步一致,同样的两个指令在从机上可能会以不同的顺序执行
三个角色
Proposer:提案者;Accepter:接受者;Learner:学习者
算法前提
提案者提出一个提案<N,V>,N是一个数字,V是提案内容
接受者接受提案时,只接受比已接收提案N值更大的提案
Basic paxos(难实现,2轮RPC效率低,活锁)
(1)算法过程
Pro
(2)故障解决
接受者故障:少数接受者故障并不会影响最终结果
*故障:会有新*替代
活锁问题:*1提出提案1后,还没完成就宕机了,此时*2代替其提案,*2还没完成提案2,*1恢复了,于是*1继续提案3,*2发现自己被打断,提出提案4,如此循环往复。
解决方法:在提案被打断后,不立即发出新提案,而是随机的等待一段时间,在这等待的时间里另一个*的提案可能已经被完成,问题解决。
Multi paxos
简化过程,直接选出一个合适的提案者持续一段时间,该时间内只服从该提案者的提案。
Raft算法
https://raft.github.io/
三个角色
Leader:领导者,发送同步请求;Follower:服从领导的同步请求;Candidate:候选者,由follower到leader的过渡角色。
同步过程
假设有一群节点,一个节点想成为leader,会先发送报文申请,没收到回复前他是candidate状态,当收到回复且通过半数选举后他成为leader,并发送报文告诉所有人我是leader(随后的perm时间里面会不断发送心跳报文维持状态)
Leader在同步数据x=5时,会先发送请求给所有follower,当回复超过半数的follower说已经将x=5写入日志,leader会先将自己的日志commit,再通知其他所有人可以commit。
Timeout机制
当一个节点都timeout了还没收到leader的消息(leader会不停发消息维持心跳),会发消息竞争成为leader。
假设有四个节点,当两个节点同时竞争leader时,另外两个节点各选择其中一个回复,最后两个candidate节点各获得两票(包括自己),他们就意识到有竞争者,于是随机的等待一段时间再发送请求。
反向代理
反向代理服务器架设在服务器端,通过缓冲经常被请求的页面来缓解服务器的工作量,将客户机请求转发给内部网络上的目标服务器;并将从服务器上得到的结果返回给Internet上请求连接的客户端,此时代理服务器与目标主机一起对外表现为一个服务器。
也就是说,正向代理情况下,客户端知道自己发送的是代理服务器,而反向代理情况下,客户度并不知道自己发送的是谁,统一认为就是服务器。
反向代理可以防止外网对内网服务器的恶性攻击、缓存以减少服务器的压力和访问安全控制之外,还可以进行负载均衡,将用户请求分配给多个服务器
大数据与高并发
大量数据如何存放
大量数据处理场景
场景一:快速去重
2.5亿个整数中找出不重复的整数的个数,内存空间不足以容纳这2.5亿个整数。
首先,根据“内存空间不足以容纳这2.5亿个整数”我们可以快速的联想到Bit-map。下边关键的问题就是怎么设计我们的Bit-map来表示这2.5亿个数字的状态了。其实这个问题很简单,一个数字的状态只有三种,分别为不存在,只有一个,有重复。因此,我们只需要2bits就可以对一个数字的状态进行存储了,假设我们设定一个数字不存在为00,存在一次01,存在两次及其以上为11。那我们大概需要存储空间几十兆左右。
接下来的任务就是遍历一次这2.5亿个数字,如果对应的状态位为00,则将其变为01;如果对应的状态位为01,则将其变为11;如果为11,,对应的转态位保持不变。
最后,我们将状态位为01的进行统计,就得到了不重复的数字个数,时间复杂度为O(n)。
场景二:快速查询
利用Bit-map也可以进行快速查询,这种情况下对于一个数字只需要一个bit位就可以了,0表示不存在,1表示存在。假设上述的题目改为,如何快速判断一个数字是够存在于上述的2.5亿个数字集合中。
同之前一样,首先我们先对所有的数字进行一次遍历,然后将相应的转态位改为1。遍历完以后就是查询,由于我们的Bit-map采取的是连续存储(整型数组形式,一个数组元素对应32bits),我们实际上是采用了一种分桶的思想。一个数组元素可以存储32个状态位,那将待查询的数字除以32,定位到对应的数组元素(桶),然后再求余(%32),就可以定位到相应的状态位。如果为1,则代表改数字存在;否则,该数字不存在。
场景三:布隆过滤器
当一个元素被加入集合中时,通过k各散列函数将这个元素映射成一个位数组中的k个点,并将这k个点全部置为1.
有一定的误判率--在判断一个元素是否属于某个集合时,有可能会把不属于这个集合的元素误判为属于这个集合.因此,它不适合那些"零误判"的应用场合.在能容忍低误判的应用场景下,布隆过滤器通过极少的误判换区了存储空间的极大节省.
Bloom Filter使用k个相互独立的哈希函数(Hash Function),它们分别将集合中的每个元素映射到{1,…,m}的范围中。对任意一个元素x,第i个哈希函数映射的位置hi(x)就会被置为1(1≤i≤k)。注:如果一个位置多次被置为1,那么只有第一次会起作用,后面几次将没有任何效果。
在判断y是否属于这个集合时,对y应用k次哈希函数,若所有hi(y)的位置都是1(1≤i≤k),就认为y是集合中的元素,否则就认为y不是集合中的元素。
场景四:快速排序
,假设我们要对0-7内的5个元素(4,7,2,5,3)排序(这里假设这些元素没有重复)。那么我们就可以采用Bit-map的方法来达到排序的目的。要表示8个数,我们就只需要8个Bit(1Bytes),首先我们开辟1Byte的空间,将这些空间的所有Bit位都置为0。
然后遍历这5个元素,首先第一个元素是4,那么就把4对应的位置为1(可以这样操作 p+(i/8)|(0x01<<(i%8)) 当然了这里的操作涉及到Big-ending和Little-ending的情况,这里默认为Big-ending),因为是从零开始的,所以要把第五位置为一(如下图):
然后再处理第二个元素7,将第八位置为1,,接着再处理第三个元素,一直到最后处理完所有的元素,将相应的位置为1,这时候的内存的Bit位的状态如下:
然后我们现在遍历一遍Bit区域,将该位是一的位的编号输出(2,3,4,5,7),这样就达到了排序的目的。
常见限流算法
计数器法
滑动窗口
漏桶算法
漏桶算法,又称leaky bucket。
从图中我们可以看到,整个算法其实十分简单。首先,我们有一个固定容量的桶,有水流进来,也有水流出去。对于流进来的水来说,我们无法预计一共有多 少水会流进来,也无法预计水流的速度。但是对于流出去的水来说,这个桶可以固定水流出的速率。而且,当桶满了之后,多余的水将会溢出。
我们将算法中的水换成实际应用中的请求,我们可以看到漏桶算法天生就限制了请求的速度。当使用了漏桶算法,我们可以保证接口会以一个常速速率来处理请求。所以漏桶算法天生不会出现临界问题。可以使用消息队列来完成。
令牌桶
负载均衡方法
数据拆分及读写分离
大型网站优化策略
1、服务和数据分离
应用服务器需要处理大量的业务逻辑,因此需要更快更强大的CPU;
数据库服务器需要快速磁盘检索和数据缓存,因此需要更快的磁盘和更大的内存;
文件服务器需要存储大量用户上传的文件,因此需要更大的硬盘。
2、使用缓存改善性能
可能存在的问题:
redis集群的数据一致性
3、数据库读写分离
网站使用缓存后,大部分读数据不会通过数据库访问,但某些情况(缓存过期、未命中)和全部的写操作都会经过数据库。对数据库的访问如果压力过大也会导致系统瓶颈。可以使用读写分离来改善数据库负载。即应用服务器在写数据的时候,访问主数据库,主数据库通过主从复制机制将数据更新同步到从数据库,这样当应用服务器读数据的时候,就可以通过从数据库获得数据
一致性问题:使用缓存进行标记。当对某个数据进行写时,在缓存中对该数据做一个标记,表示该数据正在或刚刚被更新,标记设置一个失效时间,为预估主从同步的时间。读取数据时会先去缓存中查看标记,如果被标记,则去主库中读,如果未被标记,去从库中读。这个思想其实和mysql的mvcc思想很相似,即每次只读取完全提交的数据。
4、微服务
5、反向代理
反向代理可以防止外网对内网服务器的恶性攻击、缓存以减少服务器的压力和访问安全控制之外,还可以进行负载均衡,将用户请求分配给多个服务器
6、使用NoSQL和搜索引擎
比如对于数据库检索时,由于存在超大字段导致的检索速度慢的问题(因为mysql每次读取都要读取一整行数据),可以使用NoSQL辅助检索。比如Mysql中存储除大字段以外的其他字段,NoSQL中存储主键和该大字段。查找时先在mysql中查找其余字段,然后去NoSQL中查找大字段(NoSQL使用redis,直接在内存查找很快),完事拼接成最终数据。
设计模式与实践
设计原则
设计模式
创建性模式:
单例模式、工厂模式、抽象工厂模式、建造者模式、原型模式
结构型模式:
适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式,代理模式
行为型模式:
模板方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、接解释器模式、状态模式、策略模式、职责链模式、访问者模式。
单例模式
(1)单例类只有一个实例对象;
(2)该单例对象必须由单例类自行创建;
(3)单例类对外提供一个访问该单例的全局访问点。
工厂模式
定义一个创建产品对象的工厂接口,将产品对象的实际创建工作推迟到具体子工厂类当中
(1)工厂类包含必要的逻辑判断,可以决定在什么时候创建哪一个产品的实例。客户端可以免除直接创建产品对象的职责,很方便的创建出相应的产品。工厂和产品的职责区分明确。
(2)客户端无需知道所创建具体产品的类名,只需知道参数即可。
(3)也可以引入配置文件,在不修改客户端代码的情况下更换和添加新的具体产品类。
抽象工厂模式
建造者模式
原型模式
适配器模式
策略模式
策略模式的主要角色如下
(1)抽象策略(Strategy)类:定义了一个公共接口,各种不同的算法以不同的方式实现这个接口,环境角色使用这个接口调用不同的算法,一般使用接口或抽象类实现。
(2)具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现。
(3)环境(Context)类:内部有一个策略类的实例,可以提供给客户端调用。
优点
(1)多重条件语句不易维护,而使用策略模式可以避免使用多重条件语句,如 if...else 语句、switch...case 语句。
(2)策略模式提供了一系列的可供重用的算法,从而避免重复的代码。
(3)策略模式可以提供相同行为的不同实现,客户可以根据不同时间或空间要求选择不同的。
代理模式
数据库
三范式
先设计一张表用于模拟:
主属性为:(学号,课名)
非主属性为:姓名,系名,系主任,分数
依赖关系为:学号à姓名、系名、系主任 ;(学号,课名)à分数
1NF(第一范式)
第一范式是指数据库表中的每一列都是不可分割的基本数据项,同一列中不能有多个值,即实体中的某个属性不能有多个值或者不能有重复的属性。如果出现重复的属性,就可能需要定义一个新的实体,新的实体由重复的属性构成,新实体与原实体之间为一对多关系。第一范式的模式要求属性值不可再分裂成更小部分,即属性项不能是属性组合或是由一组属性构成。
简而言之,第一范式就是无重复的列。例如,由“职工号”“姓名”“电话号码”组成的表(一个人可能有一部办公电话和一部移动电话),这时将其规范化为1NF可以将电话号码分为“办公电话”和“移动电话”两个属性,即职工(职工号,姓名,办公电话,移动电话)。
2NF(第二范式)
消除所有非主属性对主属性的部分函数依赖,这里涉及“部分函数依赖”的概念:
函数依赖:如果一个X可以唯一确定一个Y,则说Y函数依赖于X,表示为XàY。比如学号à姓名
完全函数依赖:由于在实际数据库表中,主键不一定只有一个,比如(学号,课名)à分数,分数无法只依赖于学号或只依赖于课名,这就是完全函数依赖。
部分函数依赖:指不符合完全函数依赖的函数依赖。比如(学号,课名)à姓名。虽然学号和课名可以确定姓名,但只通过学号也可以确定姓名。
不符合2NF可能出现插入、删除、更新异常。比如此时新增了一门课程,但还没有学生选课,就会导致表中的课名有数据,而其他字段全都是空,而主键学号不能为空(插入异常),再比如由于一个学生可能会选很多课,那么系名、系主任就会重复很多次(数据冗余)。
3NF(第三范式)
消除所有非主属性到主属性的传递函数依赖。简单来说就是如果一个字段可以被推导来,就不应该设计一个单独的字段来存放,否则会出现冗余数据。比如学号à系名à系主任,系主任可以直接依赖于系名,但也可以由学号推导得来。不符合3NF会导致很多问题,比如:假如将某个系中所有学生相关的记录都删除,那么所有系与系主任的数据也就随之消失了(删除异常)。
按照3NF修改后的数据库表:
Jdbc使用步骤
1. 加载 jdbc 驱动程序
Class.forName("com.mysql.jdbc.Driver");
2. 拼接 jdbc 需要连接的 url
jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=gbk;
3. 创建数据库的连接
Connection con = DriverManager.getConnection(url , username , password);
4. 创建一个Statement
Statement stmt = con.createStatement();
5. 执行SQL语句
ResultSet rs = stmt.executeQuery("SELECT * FROM ...");
6. 处理执行完SQL之后的结果
7. 关闭使用的JDBC对象
存储引擎
InnoDB和MylSAM的区别:InnoDB支持事务和行级锁,MylSAM注重效率,只支持表级锁,且崩溃后无法恢复。InnoDB支持外键,MylSAM不支持。InnoDB支持MVCC实现并发控制。
MylSAM使用的是B+树索引,树的最底层存的是文件对应的地址。索引文件和数据文件分离。查找时先在B+树中查到索引对应的文件地址,然后再去查询数据文件。
InnoDB使用的也是B+树,但其数据文件就是按照B+树的结构存储的,本身就是个具备索引功能的文件(以主键为索引,称为主索引,其余索引叫辅助索引,辅助索引的叶子节点存的是主键的值),叶子节点存储的就是所需的数据。在根据主索引搜索时,直接找到key所在的节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,再走一遍主索引。 因此,在设计表的时候,不建议使用过长的字段作为主键,也不建议使用非单调的字段作为主键,这样会造成主索引频繁分裂。
锁
X锁:行写锁;S锁:行读锁;IX锁:意向写锁;IS锁:意向读锁
当前锁模式/是否兼容/请求锁模式 |
S |
X |
IS |
IX |
S |
兼容 |
冲突 |
兼容 |
冲突 |
X |
冲突 |
冲突 |
冲突 |
冲突 |
IS |
兼容 |
冲突 |
兼容 |
兼容 |
IX |
冲突 |
冲突 |
兼容 |
兼容 |
事务
四大特性ACID
原子性(Atomic):一个事务内的操作要么全执行,要么全不执行。
一致性(Consistency):一个事务完成后,数据库应该从一个正确的状态转移到另一个正确的状态。这个一致性指的是与现实世界事务逻辑的一致,也就是说原子性与隔离性可能都会影响到一致性。
隔离性(Isolation):事务之间是相互独立的,一个事务的执行不会影响另一个事务。
持久性(Durability):所有提交的事务对数据库中数据的改变是持久的,即使出现故障也不应该有影响。
并发事务可能会导致的问题
脏读:读到了没有提交的数据
丢失修改:修改的数据被另一个事务覆盖。比如事务1读A=20,事务2读A=20,事务1修改A=A-1,事务2修改A=A-1。最后A=19,导致事务1的修改丢失。
不可重复读:同一个事务读取一个两次数据,在两次读取之间可能有事务将数据修改,导致两次读取到的数据不一致。
幻读:与不可重复读类似,指的是两次读取之间,其他事务进行了数据新增或删除,导致事务1感觉像出现幻觉一样多出了些数据。
事务隔离级别
READ-UNCOMMITED:允许读取未提交的数据,可能导致脏读balabala
READ-COMMITED:只能读取提交数据,解决脏读,可能出现不可重复读问题
REPEATBLE-READ:可重复读,可能会出现幻读问题
SERILIZABLE:序列化,完成服从ACID特性,所有事务依次执行
锁
表级锁:粒度最大的锁,速度快,并发低,安全性高不会死锁
行级锁:粒度最小的锁,速度慢,并发高,可能会死锁
大表优化
读写分离:主库负责写,从库负责读
垂直分区:根据列将表拆分成多张表。其优点是可以将每行数据量减少,降低查询时读取的块数,减少IO。缺点是主键出现冗余,join操作会增加。
水平分区:根据属性值将表拆分成多张表,比如按照性别拆成两张表。解决了单一表数据量过大的问题。但如果表都还在同一台机器上,对于并发没什么意义,所以水平分片最好分库。
MVCC
索引
索引结构
为什么使用索引
- 唯一性索引可以保证数据唯一性
- 加快检索速度
- 将随机IO变成顺序IO
- 加快表连接
为什么不对每个列加索引
因为对数据操作的同时也要对索引操作,索引也会占用空间,随着索引量增加,创建维护索引所需时间也在增加。
索引失效的情况
- 使用or关键字,or的两边只有一个有索引
- 使用like关键字,出现“like %”,不会使用索引
- 对索引字段进行计算、函数操作
- 对索引字段使用NULL、NOT NULL
- 数据库判断不使用索引比使用快时,索引失效
覆盖索引
指的是所查询的字段,都在叶子节点中,不需要回表。比如:select username , age from user where username = ‘Java‘ and age = 22。如果辅助索引的B+树data域中就保存了username和age,那么这就是覆盖索引
索引小tips
顺序查找范围数据很快,因为(1)减少磁盘寻道(2)不需要进行额外排序操作
单行访问很慢,不划算,所以索引尽量建在包含多行的字段上
索引也要建在经常查询、条件、连接的字段上
覆盖索引很快,索引包含所有所需数据,无需回表,且避免了大量单行访问。
最左前缀匹配原则:
在联合索引中,如果索引最左侧一列或左侧几列精准匹配数据,则命中索引。
冗余索引:
(name,city)和name就是冗余的,因为这两个必定会同时命中。
SQL优化
Explain语句解析
Explain语句包括id、selectType、table、type、possible_keys、key、key_len、ref、rows、extra这些信息。
Id
Id表示执行组,id值相同可以认为是一组,从上往下顺序执行;在所有组中,id值越大,优先级越高,越先执行;也就是表的读取顺序。
selectType
常用值有六种:(1)SIMPLE:简单的查询,不包含子查或UNION(2)PRIMARY:查询中若包含子查询,最外层的为primary(3)SUBQUERY:在select或where列表中包含了子查询(4)DERIVED(5)UNION:若第二个select被标记在UNION之后,则被标记为UNION,若UNION包含在from子查询中,外层select被标记为derived(6)UNION RESULT
table
type
从最好到最差依次:system->const->eq_ref->ref->range->index->ALL。
possible_keys
key
key_len
ref
rows
extra
SQL语句执行过程
Mysql架构
分为server层和存储引擎层。Server层主要包括连接器、缓存、分析器、优化器、执行器等。存储引擎包负责数据的存储和读取,如InnoDB和MylSAM都是经典引擎。
连接器:主要与身份认证与权限相关功能。
查询缓存
分析器:用于分析sql语句,分为词法分析和语法分析。词法分析是对关键字进行提取,如select、update、where,据此来确定查询表、查询条件等内容。语法分析用于判断sql语句是否正确。
优化器:选择最优执行方案,比如如何选择索引,多表查询如何关联顺序等。
执行器:判断用户是否有权限,有权限则调用引擎接口进行查询。
sql语句执行过程
查询操作:权限校验->查询缓存->分析器->优化器->权限校验->执行器->引擎
更新操作:分析器----》权限校验----》执行器---》引擎---redo log(prepare 状态---》binlog---》redo log(commit状态)
mysql存储过程
sql高级语句
左外连接
返回指定左表的全部行+右表对应的行,如果左表中数据在右表中没有与其相匹配的行,则在查询结果集中显示为空值。
自然连接
不用指定连接列,也不使用on语句,默认比较两张表中相同的列
Select * from student NATURE JOIN score;
内连接
与自然连接相似,但可以指定属性名不同的列
Java基础
J2SE基础知识
集合
HashMap
底层结构
put过程
get过程
为什么链表长度到8后转树
为什么到6后转链表
扩容机制
hashmap为什么使用头插法
hashmap线程不安全的原因
ArrayList
LinkedList
LinkedHashMap
LinkedHashMap底层是数组 + 单向链表 + 双向链表。数组和单向链表做hashmap存储,双向链表记录插入顺序。LInkedHashMap重写了两个方法,一个是添加entry,一个是创建entry,这两种时刻都要维护双向链表。
创建entry时进行如下操作,保证有序性。
在使用迭代器时,会输出header的下一个节点
反射
泛型
代理
J2EE基础知识
RESTful接口规范
Resource Representation State Transfer资源表示层状态转移。
URI 统一资源标识符 URL统一资源定位符
- 动作:GET、POST、PUT、DELETE、PATCH
- 路径:(1)网址中不能含有动词,因为表示的是资源,只能使用名词来表示,动词可以放入HTTP协议中来请求。(2)不用大写字母,建议用中杠-而不是下杠_。
- 过滤信息:如果要添加条件,建议使用url参数的形式
- 状态码:(1)2xx:成功 (2)3xx:重定向(3)4xx:客服端错误(4)5xx:服务端错误
Get和Post
Get和Post本质上没什么区别,底层都是基于TCP连接。如果需要获取资源就使用GET,需要提交数据就可以使用Post。
转发和重定向
转发是服务器行为,重定向是客户端行为。转发是服务器请求资源,将资源读取过来后直接发给客户端,客户端不知道资源从哪来,所以URL还是以前那样。重定向是服务器交给客户端一个状态码告诉他要去哪个新地方,客户端就会去请求新的URL。转发一般可以在用户登录时,根据角色转发到相应模块,重定向可以用作页面跳转。
Cookie和Session
Cookie是存放在客户端的,Session存放在服务端。cookie一般用来保存用户信息,比如在登录一次网站后会在cookie中记录账户信息,下次就无需再重新登录。Session是通过服务端记录用户信息,比如购物车。
IO
同步和异步、阻塞与非阻塞
同步指的是在调用服务后,如果服务没有执行完,不会返回结果,如果返回了结果,就说明一定是执行完了。异步指的是调用服务后,不会立刻返回结果,而是当执行完之后发送一个通知说明执行结束,可以通过回调函数获取结果。
阻塞指的是当请求发送后,不管是同步还是异步请求,都会挂起当前线程,直到等到结果返回。非阻塞指的是请求发出后,无论CPU如何处理,线程并不会挂起,而是可以做其他事情。也就是说调用方在获取io的过程中会立刻返回而不进行挂起。
Io的分类
按照流向,可分为输入流和输出流
按照操作单元,可分为字节流和字符流
按照流的角色可分为节点流和处理流
Io与nio
Io是面向流的,nio是面向缓冲区的
Io是阻塞的,nio是非阻塞的
Io没有选择器,nio有选择器
BIO(同步并阻塞)
服务器实现一个连接一个线程,客户端有连接请求时服务器端就需要启动一个线程进行处理,没处理完之前此线程不能做其他操作
NIO(同步非阻塞)
服务器实现一个连接一个线程,客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理
AIO(异步非阻塞)
服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由操作系统先完成了再通知服务器应用去启动线程进行处理
IO初探:
在调用accept方法时,传统IO会一直阻塞式的获取连接,如果一直没有连接,则会卡在这个位置等待。同样的,在read时,会一直等待读取,如果一直没有信息传递过来,也会卡在这一直等待,这就是BIO。
IO的更迭:
1)初代,单线程执行,先accept连接,然后handler处理,这种叫做BIO操作。初代不适合多线程,上一个线程没执行完handler,下一个连接就无法获取,导致阻塞。
2)二代,多线程执行,对每个获取到的连接开个线程执行handler,这样就可以继续获取连接。这种方式对于c10k或c10m等情况无法处理,占用大量内存。
3)三代,使用线程池,限制线程数量。这种方式的问题在于线程数量受限,如果某个线程的handler使用执行不完,该线程就不会释放,那就又导致线程资源被浪费。
4)四代,使用NIO,将连接保存下来,即使accept时没有连接,或者read不到数据,程序也会继续执行。但这里有个问题,就是当连接数量巨大时,channellist中有太多空闲连接,即没有数据读写的连接,那么每次都得遍历,这样就导致效率低下。
5)五代,IO多路复用。在NIO基础上加入selector,selector会选择那些有效的行为进行执行,比如建立连接,读写等。通过一种新的系统调用,一个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是内核缓冲区可读/可写),内核kernel能够通知程序进行相应的IO系统调用。
零拷贝原理
传统IO过程中涉及多次内核与用户进程上下文的切换,内核缓存与用户缓存中也存在重复数据(存在数据拷贝)。因此为了提高传输效率,要减少状态切换,且保证不存在数据副本。Java中可以使用transferTo方法实现零拷贝。
传统IO
hard driverà(DMA) kernel bufferà(CPU) user bufferà(CPU) socket bufferà(DMA) protocol engine
Mmap
用户空间可以共享内核空间的数据,减少一次CPU拷贝
sendFile
使用sendFile系统调用,直接将内核空间数据发送到协议栈,又减少一次CPU拷贝
NIO
组件介绍
三大核心组件:通道,缓冲区,选择器
通道:Channel是一个对象,可以通过它读取和写入数据。通常我们都是将数据写入包含一个或者多个字节的缓冲区,然后再将缓存区的数据写入到通道中,将数据从通道读入缓冲区,再从缓冲区获取数据。其本质是在底层操作系统创建了一个fd(即文件描述符),相当于建立了一个用于网络通信的通道,通过这个通道我们可以和外部网络进行通信。
Channel 类似于原I/O中的流(Stream),但有所区别:
流是单向的,通道是双向的,可读可写。
流读写是阻塞的,通道可以异步读写。
缓冲区:是一个对象,内部包含所要读取和写入的数据
选择器:Selector可以称他为通道的集合,每次客户端来了之后我们会把Channel注册到Selector中并且我们给他一个状态,在用死循环来环判断(判断是否做完某个操作,完成某个操作后改变不一样的状态)状态是否发生变化,知道IO操作完成后在退出死循环
1)boostrap、serverBootStrap
2)future、ChannelFuture
3)Channel
4)Selector
NIO底层在JDK1.4版本是用linux的内核函数select()或poll()来实现,跟上面的NioServer代码类似,selector每次都会轮询所有的 sockchannel看下哪个channel有读写事件,有的话就处理,没有就继续遍历,JDK1.5开始引入了epoll基于事件响应机制来优化NIO。
如上图所示,nio底层主要利用Linux提供的epoll操作来实现,epoll与select、poll的区别如下:
|
select |
poll |
epoll |
操作方式 |
遍历 |
遍历 |
回调 |
底层实现 |
数组 |
链表 |
哈希表 |
IO效率 |
O(n) |
O(n) |
O(1) |
最大连接数 |
有上限 |
有上限 |
无上限 |
epoll实现原理
操作系统如何接收网络数据
数据从网卡接收到,就会被放入内存中,操作系统就去内存中读取数据。
如何知道接收了数据
当网卡把数据写入到内存后,网卡向cpu发出一个中断信号,操作系统便能得知有新数据到来,再通过网卡中断程序去处理数据。
如何同时监控多个socket
关于阻塞
首先要明确一点,进程的阻塞状态不会耗费cpu资源,其实我们常说的进程资源就是进程所占有的内存资源,进程拥有自己的进程栈,cpu会按照栈内指令执行,如果cpu不去执行某个进程,这个进程就相当于阻塞,其内部不会有指令的执行跳转,所以处于阻塞态的进程并不会占有cpu资源。但进程的切换是需要耗费cpu的,如果一个进程阻塞,cpu就要转去做其他事情,这个切换的过程是cpu消耗的主要原因。
等待队列与工作队列
当A进程执行到创建socket时,操作系统会创建一个由文件系统管理的socket对象。这个socket对象包含了发送缓冲区、接收缓冲区、等待队列等成员。等待队列是个非常重要的结构,它指向所有需要等待该socket事件的进程。
当A进程创建socket后,会被放入socket对象的等待队列中进行阻塞,此时cpu不会操作A进程,当socket对象的缓冲区中有就绪数据时,就会将A进程从等待队列中取出,放回工作队列中,cpu会调度执行A进程。
同时监视多个socket
如果进程A需要监视多个socket,就将A进程分别放入这些socket的等待队列中。当有任意一个socket可以进行读写时都会通知到该线程,然后从等待队列移除A线程,加入工作队列,同时遍历socket找出可以读写的那些。这就是select的实现思想。
select方法原理
select的实现思路很直接:
(1)假如程序同时监视sock1、sock2和sock3三个socket,那么在调用select之后,操作系统把进程A(包括了select逻辑)分别加入这三个socket的等待队列中(放的其实是进程A的引用)。
(2)当任何一个socket收到数据后,中断程序将唤起进程。将进程A从所有等待队列中移除,并加入到工作队列中。经由这些步骤,当进程A被唤醒后,它知道至少有一个socket接收了数据。
(3)程序只需遍历一遍socket列表,就可以得到就绪的socket。
select的缺点
(1)每次调用select都需要将进程加入到所有监视socket的等待队列,每次唤醒都需要从每个队列中移除。这里涉及了两次遍历(遍历进程A关心的所有socket,需要注意的是添加从等待队列头部添加,删除通过回调直接实现,所以每个socket的等待队列不用遍历),而且每次都要将整个fds列表传递给内核,有一定的开销。正是因为遍历操作开销大,出于效率的考量,才会规定select的最大监视数量,默认只能监视1024个socket。
(2)进程被唤醒后,程序并不知道哪些socket收到数据,还需要遍历一次(这一次遍历是在应用层)。
也就是说select执行了三次socket的遍历(添加、删除就绪队列,找出就绪socket)
epoll设计思路
epoll的使用
先用epoll_create创建一个epoll对象epfd,再通过epoll_ctl将需要监视的socket添加到epfd中,最后调用epoll_wait等待数据。将添加工作队列和阻塞等待的过程分开,这样就有优化空间了。
就绪列表
select的一个缺点就是检测到数据到来后,并不知道是哪个socket,还需要再遍历一次。epoll在内存中加入了一个就绪列表rdlist,读写就绪的socket会被加入到rdlist中,这就减少了一次遍历。
select优于epoll的情况
当数据活动比较活跃,大多数socket都要进行读写时,用select更好,因为反正都是要访问的,不如直接遍历。
epoll流程
(1)创建epoll对象
当A进程调用epoll_create时,内核会创建一个eventpoll对象(epfd),其中也含有等待队列(队列中是等待事件,比如epoll_wait)。
(2)维护监视列表
创建epoll对象后,可以用epoll_ctl添加或删除所要监听的socket。这一操作会将eventpoll对象加入到socket的等待队列中。此时与select不同,加入等待队列的并不是A进程,而是eventpoll对象的引用,这就是在socket和进程间加了一个中介,用于双方的通知。
(3)接收数据
当socket接收到数据,eventpoll中的rdlist就会加入该socket的引用。也就是说,不需要像select一样去将A进程移出等待队列,此时A进程只需要从eventpoll中的rdlist就可以直接感知到可以读写的socket,socket的数据接收并不直接影响进程,而是通过改变eventpoll的就绪列表来改变进程状态。
(4)阻塞和唤醒A线程
在某时刻进程A运行到了epoll_wait语句,内核会将进程A放入eventpoll的等待队列中,阻塞进程。
当socket接收到数据,中断程序一方面修改rdlist,另一方面唤醒eventpoll等待队列中的进程,进程A再次进入运行状态。也因为rdlist的存在,进程A可以知道哪些socket发生了变化。
epoll只执行了一次遍历(添加等待队列)
面试题:谈谈epoll方法
epoll是Linux操作系统底层的一个系统调用,是实现NIO的一个重要底层方法。
在网络IO传输过程中,早期的select方法需要反复的遍历socket列表,进程需要阻塞的等待数据的传输,效率低下。
epoll通过在进程和socket之间加入一个eventpoll对象,eventpoll会加入socket的等待队列中,当有socket对象读写就绪时,会被记录到eventpoll的就绪队列中。获取数据时会调用epoll_wait进行阻塞式的查看rdlist是否有socket数据可以读取。和select相比减少了一次移除队列的遍历和一次查找就绪socket的遍历。
select是进程直接在socket的等待队列中阻塞(等的是socket数据),epoll是进程在调用epoll_wait,也就是主动的需要获取数据时,在eventpoll的等待队列中阻塞(等的是rdlist)
Netty
Netty使用步骤
Netty工作架构
Java虚拟机
Jvm内存模型
(1) 程序计数器:指向下一行所要执行的字节码,涉及分支、循环、跳转等。
(2) Java虚拟机栈:存储各种数据,对象引用等。可能会出现*和outOfMemory问题
(3) 本地方法栈:存储native方法
(4) 堆(共享):对象的存放地,分为新生代、老年代、永久代(方法区,1.8后被取消)。新生代可以按照8:1:1被分为Eden区和Survivor区
(5) 方法区(共享,jdk1.8后被元空间取代):类信息、运行时常量池、静态变量、编译后代码等
为什么方法区替换成元空间?方法区有内存大小限制,会出现OutOfMemoryError:MetaSpace,替换成元空间使用的是机器直接内存,其限制是机器内存,出错概率更小。
常量池相关内容
字面量:实际值,比如int a=8,和string b=“123”,这个8和“123”都是字面量
符号引用:在声明变量时的变量名称,是一个字符串,这个字符串就会被存入常量池,后续如果用到,就将其解析成直接引用(指针)
字符串常量池在jdk1.7后被移入堆,剩下的还在方法区
对象创建过程
- 类加载检查:通过符号引用检查该类是否被加载、解析和初始化过,如果没有,需要先进行类加载
- 分配内存:为对象在java堆中开辟内存。使用的方法有指针碰撞(在空闲内存和已用内存中间有一个分界指针,分配对象时移动指针即可)和空闲列表(维护一个列表,列表中记录哪些块是空闲的,分配时找一块足够大的分配给对象)两种。使用哪种方法取决于内存是否规整,规整使用指针碰撞,不规整使用空闲列表。而内存是否规整取决于是使用“标记-清除”还是“标记-整理”方法。
- 初始化零值:内存分配完毕后,虚拟机将内存空间初始化为零值。保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
- 设置对象头:对对象信息进行补充,比如属于哪个类,如何获得类信息,对象的hashcode,对象的gc分代年龄信息
- 执行init方法:从虚拟机视角对象创建已经完毕,现在就按照程序员意愿对对象初始化,比如赋初值等。
对象的内容:对象头,实例数据,对齐填充
对象的访问定位:句柄:会在java堆中开辟一块内存作为句柄池,本地变量表中存的reference就是句柄地址,句柄中包含了对象实例数据和对象类型数据指针,通过操作句柄就可以对对象进行操作。直接指针:本地变量表中存的内容就是对象地址。
句柄的优点是当对对象进行移动时(垃圾回收),只会更改句柄中的实例指针,不用动reference。
直接指针的优点就是快,节省了一次指针定位开销。
垃圾回收相关
新生代中的对象经过一次垃圾回收后如果存活则会进入survivor区,并且年龄+1,默认年龄为15就会进入老年代,进入老年代的阈值可以通过-XX:MaxTenuringThreshold来调整。
为什么大对象直接进入老年代?为了避免大对象由于分配担保机制带来的复制而降低效率。
GC分类
Partial GC(部分收集):
新生代收集(minor gc):只对新生代收集
老年代收集(major gc):只对老年代收集
混合收集(mixed gc):对整个新生代和部分老年代进行收集,G1就是这样的。
Full GC(整堆收集):收集整个java堆和方法区
如何判断对象死亡
引用计数法:给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。方法简单效率高,但可能存在互相引用导致无法被回收的情况。
可达性分析:从一系列GC ROOTS作为起点向下遍历,走过的路径成为引用链。如果一个对象没有任何引用链,那么该对象不可用。
可作为GC ROOTS的对象:本地变量表中引用的对象,本地方法栈中引用的对象,常量引用的对象,静态属性引用的对象
引用的扩展:
(1)强引用:大部分对象都是强引用,这种引用是不会被回收的,比如使用new关键字创建的对象。
(2)软引用:可有可无的对象,如果内存足够,就可以不回收,如果内存不足就将其回收。可以用来使用内存敏感的高速缓存。
(3)弱引用:和软引用一样是可有可无的对象,区别在于一旦gc线程发现该软引用,就会回收它。
(4)虚引用:一个对象具有虚引用,那就更没有引用一样,随时会被回收。虚引用主要用来跟踪对象被垃圾回收的活动。
找书看:对象是否非死不可?:对象真正的宣告死亡,至少要经历两次标记
如何判断常量是废弃常量
假如在常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 "abc" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,"abc" 就会被系统清理出常量池。
如何判断类是无用的类
(1)该类所有的实例都被回收(2)该类的classloader被回收(3)该类的class文件没有任何地方有引用,不会出现反射的使用
垃圾收集算法
算法 |
介绍 |
优点 |
缺点 |
标记清除 |
首先通过可达性分析标记出需要回收的对象, 然后将这些对象一一清除。 |
|
(1)效率问题:在需要回收对象太多时,效率降低(2)空间问题:会产生大量不连续的内存碎片 |
复制算法 |
复制算法需要将内存划分成两块或多块。与清除算法一样,也要先在A区域中标记出回收对象,然后将存活对象复制到另一块区域B,再清除整个A区域。 |
在存活对象较少的情况下,效率很高,且不会产生内存碎片 |
|
标记整理 |
将要回收的对象标记,然后将存活对象全部移动到内存的一侧。 |
|
|
分代收集 |
根据不同时期选择不同算法。新生代中,每次收集都会有大量对象死去,所以可以选择复制算法。而老年代的对象存活几率是比较高的所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。 |
|
|
为什么要分新生代和老年代?:因为从垃圾回收的角度来看,新生代和老年代的区别就在于新生代对象死亡量很大,老年代大部分对象都能存活。这样就便于我们使用不同的回收算法进行回收,提高效率。
为什么新生代使用复制而不使用标记-整理?:因为新生代存活对象少,在可达性分析过程中直接将访问到的对象直接复制到新区域就行,使用标记-整理的话就得有个标记过程,因为整理的过程必须要考虑存活对象将要覆盖的区域是否是废弃对象。
垃圾收集器
Jdk1.8使用的是Parallel Scavenge和Parallel Old收集器。
- Serial收集器
串行收集器,是单线程的收集器。一方面是只有一条线程进行回收,另一方面指的是在收集时必须要“stop the world”,停止所有线程,直到收集结束。新生代采用复制算法,老年代采用标记-整理算法。
- ParNew收集器
是Serial收集器的多线程版本。使用多线程进行回收,其余和Serial一样。
- CMS收集器(看书)
CMS收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验应用上使用。CMS使用标记清除算法,过程会更加复杂,分为四个步骤:(1)初始标记,暂停所有线程,标记与root相连的对象,时间很短。(2)并发标记,同时开启gc线程和用户线程,对对象进行标记。但由于是并发执行的,可能会存在用户线程在gc标记后又产生了新的应用,所以gc无法保证可达性分析的实时性,所以算法会跟踪这些地方。(3)重新标记,修正并发标记阶段出现问题的那些对象,时间很短。(4)并发清除。
其优点在于:并发收集,停顿时间短。
其缺点在于:对CPU资源敏感,标记清除算法会产生大量内存碎片
- G1收集器(看书)
可以使用多核CPU来降低停止线程的时间
常用jvm参数
- 初始化堆大小:-Xms(最小值)-Xmx(最大值)
- 初始化新生代大小:-Xmn(年轻代大小)或-NewSize(最小值)-MaxNewSize(最大值)
- 设置新生代老年代比例:-XX:NewRatio=1(新生代和老年代各占一半)
- 设置Eden区和Survivor区比例:-XX:SurvivorRatio 默认是8
频繁 full gc的问题
类加载机制
类加载过程:加载==》连接==》初始化,连接过程分为:验证==》准备==》解析
类加载器种类:BootstrapClassLoader(启动类加载器),最*加载器;ExtensionClassLoader(扩展类加载器)AppClassLoader(应用程序加载器)
双亲委派机模型
在加载类的时候,先判断该类是否加载过,加载过则直接返回,否则尝试加载。加载的时候,会将加载请求交给父加载器执行,一直往上。因此所有的类加载都会到BootStrapClassLoader,然后如果无法处理才会交给子类加载。这样做是为了避免了类的重复加载,也保护了java核心类的api不被篡改。
Java多线程
Volatile关键字
内存模型中,有一块共享内存,每个线程自己拥有一块私有内存,当线程需要对共享内存的数据进行操作时,会先将数据拷贝到自己的内存中再进行操作。在并发中对线程的要求有三个原则:可见性、原子性、有序性。可见性指的是当一个线程对共享内存中的数据做了改动,其他线程也应该立即知道数据的改动;原子性指的是线程的操作是不可被加塞的,比如i++操作中间就可能被其他操作加塞导致结果错误;有序性指的是程序的执行是有序的,可以通过禁止指令重排保证。
Volatile关键字用于修饰成员变量,可以保证可见性和有序性,但不能保证原子性。
Synchonized关键字
synchronized的执行过程:
1. 检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁
2. 如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1
3. 如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。
4. 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁
5. 如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
6. 如果自旋成功则依然处于轻量级状态。
7. 如果自旋失败,则升级为重量级锁。
偏向锁
当锁对象第一次被线程获取的时候,将对象头中的标志位设为“01”,表示偏向模式。同时使用CAS获取锁线程的ID记录到对象的mark word中,如果CAS操作成功,以后该线程每次进入到同步代码块中的时候,比较线程ID即可,虚拟机可以不进行任何同步措施,提高了效率。
偏向锁的优点是当只有一个线程没有竞争的时候,可以提高线程进入同步代码块的效率。
轻量级锁
当使用偏向锁时出现了线程竞争,偏向锁不会直接变成重量级锁,会先升级为轻量级锁,是因为研究人员认为线程竞争锁的情况是很少出现的,使用轻量级锁可以提高效率。
在轻量级锁首次进入锁对象使用时,会在线程栈帧底部建立一块lock record,在其中复制锁对象的mark word,包括分代年龄、锁状态、hashcode等信息。然后虚拟机试图通过CAS操作将对象的markword更新为指向lockrecord的指针。如果该操作成功了,即该线程拥有了这个对象的锁,并把对象头的锁标志位改为“00”。如果这个更新失败了,就意味着有其他线程参与了竞争,检查锁对象的markrecord是否指向当前栈帧,如果是说明已经拥有了该锁,进入同步即可,否则就说明已经被其他线程抢占,此时必须膨胀为重量级锁,锁状态变为“10”。
锁粗化
比如两个同步代码块之间存在一些代码,他们执行的很快与同步的这些代码无关,那么就可以将两个锁合并成一个,减少锁开销
锁消除
锁消除理解起来很简单,它指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。
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锁适合代码少量的同步问题。
Synchronized和volatile的区别
1)volatile本质是在告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住.
2)volatile仅能使用在变量级别,synchronized则可以使用在变量,方法.
3)volatile仅能实现变量的修改可见性,而synchronized则可以保证变量的修改可见性和原子性.
4)volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞.
5)当一个域的值依赖于它之前的值时,volatile就无法工作了,如n=n+1,n++等。如果某个域的值受到其他域的值的限制,那么volatile也无法工作,如Range类的lower和upper边界,必须遵循lower<=upper的限制。
6)使用volatile而不是synchronized的唯一安全的情况是类中只有一个可变的域。
基本原子类
Volatile关键字不能保证原子性,比如对于i++的操作,对变量i加上volatile并不能保证其原子性,因为i++在内存层面其实分为三步,首先线程获取i,然后在私有内存进行i=i+1的操作,这个操作期间另一个线程可能已经对i做了修改,最后将i写入内存,这就导致之前的修改被覆盖了。
为解决这个问题juc.atomic包中提供了一系列原子类进行操作。JUC包中的原子类可以分为四类:(1)基本类型(2)数组类型(3)引用类型(4)对象的属性修改类型。其中比较常用且重要的有AtomicInteger类(其他的跟这个都很像),AtomicReference(可以实现自定义类型)和AtomicStampedReference(带有原子更新功能,解决ABA问题)。
AtomicInteger的getAndIncrement()方法实现获取当前值并自增。
Unsafe类
这是一个含有许多native方法的类,提供包括CAS在内的底层操作。getAndIncrement方法中的函数调用链路为:unsafe.getAndAddIntà unsafe.compareAndSwapInt。compareAndSwapInt就是利用CAS原理保证原子操作。
CAS原理
CAS即比较并交换(Compare And Swap)。比如要对i进行自增操作,首先获得i的值,再对其进行+1操作,操作时首先判断当前的i值是不是与之前获得的i值相同,如果是,则更新,如果不是,更新失败。可以使用自旋锁循环尝试直到成功。
ABA问题
即使使用CAS原理保证了更新数值的正确性,但依然存在问题,即ABA问题。指的是在compare时数值确实与之前相同,但可能在这之前已经经过了一系列操作,只是到最后使这个数又变回了之前的值,这并不是真正的原子操作。
并发容器
ConcurrentHashMap
HashMap是线程不安全的,多线程扩容时可能会出现死循环。因此使用ConcurrentHashMap来解决多线程情况。
Jdk1.7之前的ConcurrentHashMap底层使用的是Segment分段锁+Entry数组+链表实现的。Segment是一个数组,继承自ReentrantLock,每个segment内部都存有一个Entry数组,这个数组其实就和HashMap中的数组一样了,Entry数组中是链表。每次使用时,segment会进行加锁,保证当前segment下的所有元素是同步操作的,同时也允许了对其他segment 的操作。
Jdk1.8对其做了优化,降低了锁的粒度,原本的segment需要对一块数据进行锁,1.8通过synchronized+数组+链表+红黑树对单个元素进行锁。在put等操作时使用synchronized关键字进行加锁。
ConcurrentHashMap可以很好地解决多线程安全问题,但如果频繁的进行写,就会频繁的拷贝数组,这样的效率也会很低,所有ConcurrentHashMap适用于读多写少的场景。
CopyOnWriteArrayList
CopyOnWriteArrayList在使用时无需加锁或同步机制,其所有对底层数组的修改操作,都不会对原数组进行操作,而是拷贝出一个副本,在副本中写入,然后替换原数组。这就保证了写数据时也能同时进行读。不过为了保证读写时数据的一致性,这个数组需要用volatile进行声明。
读取操作
读取操作没有任何同步控制和锁操作,因为内部数组 array 不会发生修改,只会被另外一个 array 替换,因此可以保证数据安全。
写入操作
写入操作是 add() 方法在添加集合的时候加了锁,保证了同步,避免了多线程写的时候会 copy 出多个副本出来。
锁
1、乐观锁、悲观锁
2、独享锁、共享锁
3、公平锁、非公平锁
4、互斥锁、读写锁
5、可重入锁
6、分段锁
7、锁升级(无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁) JDK1.6
公平锁和非公平锁
公平锁:线程在获取锁时会先查看队列,如果队列为空,则直接占有锁,否则加入队列中等待安按照FIFO规则来占有锁
非公平锁:线程上来就试图获取锁,获取失败后再按照公平锁进行
ReentrantLock默认是非公平锁,优点是吞吐量大。Synchronized也是非公平锁
可重入锁
同一线程在外层获得锁后,内层递归函数依然能获得该锁的代码。同一线程在外层方法获得锁后,进入内层方法会自动获得锁。其最大用途是避免死锁。ReentrantLock和Synchronized都是可重入锁。
自旋锁(写代码)
尝试获取锁的线程不会立刻阻塞,而是采用循环的方式尝试获取锁。好处是减少线程切换的消耗,缺点是循环会导致cpu消耗。
do{
v5=this,getIntVolatile();
}while(!this.compareAndSwapInt(v1,v2,v5,v5+v4));
Return v5;
synchonized和lock的区别
(1)synchonized是jvm层面实现,lock是一个类,是api层面
(2)synchonized不需要手动释放锁,lock需要
(3)synchonized不可中断,除非抛出异常或执行完,lock可以中断(设置超时方法或者用interrupt方法)
(4)synchonized是非公平锁,lock都可以,默认非公平
(5)synchonized不能精确唤醒进程,只能随机或全部唤醒。Lock可以。
读写锁(写代码)
Condition(写代码)
线程池(写代码)
使用线程池的好处:
(1) 降低资源消耗,线程池可以重复使用已完成任务的线程,减少线程创建和销毁。
(2) 提高响应速度,当任务到达时,可以直接取出线程使用,无需等待线程创建。
(3) 提高线程可管理性,线程是稀缺性资源,使用线程池可以进行统一的分配,调优和监控。
Executor框架
ThreadPoolExecutor类
线程池的七大参数:
(1) corePoolSize:线程池常驻线程数
(2) maximumPoolSize:线程池能够容纳同时执行的最大线程数,>=1
(3) keepAliveTime:多余空闲线程的存活时间。当大于corePoolSize的线程处于空闲状态,时间达到keepAliveTime时,会销毁多余的线程
(4) unit:keepAliveTime的单位
(5) workQueue:阻塞队列,当没有足够的线程完成任务时,任务会进入阻塞队列等待
(6) threadFactory:生成线程的线程工厂,一般使用默认的即可
(7) handler:拒绝策略,表示当队列满了,且工作线程大于等于最大线程数
线程池在使用过程中,首先进来的任务会直接分配常驻线程,当任务量超出corePoolSize时,任务进入workQueue阻塞队列中等待,当阻塞队列也满了之后,线程池开始增加额外线程,最多增加到maximumPoolSize。(1)此时如果还有任务不断进来,启动拒绝策略。(2)此时如果任务量下降,不断有线程空闲出来,当这些线程空闲时间达到keepAliveTime时,销毁该多余线程。最后当线程池完成了所有的任务,最终会收缩到corePoolSize大小。
拒绝策略
(1) AbortPolicy(默认):直接抛出异常,阻止系统正常运行,无特殊场景。
(2) CallerRunsPolicy:调用者运行,不允许失败场景(对性能要求不高、并发量较小)。
(3) DiscardOldestPolicy:丢弃等待最久的任务,发布消息。
(4) DiscardPolicy:直接丢弃任务,不予处理也不报异常,无关紧要的任务(博客阅读量)。
线程池的快速使用
(1) FixedThreadPool
可重用固定线程数的线程池。
参数: corePoolSize=n
maximumPoolSize=n
keepAliveTime=0
常驻线程数量即为最大值,不会出现阻塞队列满后加线程的情况。适用于任务量比较固定但耗时长的任务。
(2) SingleThreadPool
只有一个线程的线程池。
参数: corePoolSize=1
maximumPoolSize=1
keepAliveTime=0
只有一个线程,任务会在阻塞队列中排队使用该线程。适用于多个任务顺序执行的场景。
(3) CachedThreadPool
参数: corePoolSize=0
maximumPoolSize=INTEGER.MAX_VALUE
keepAliveTime=60
unit=SECONDS
workQueue=SychronousQueue
常驻线程为0,空闲线程等待时间为60秒。使用该线程池时,首先会先执行SynchronousQueue的offer方法提交任务,并查询线程池中是否有空闲线程来执行SynchronousQueue的poll方法来移除任务。如果有,则配对成功,将任务交给这个空闲线程,否则会为其创建新线程执行。适用于耗时少,任务量大的情况。
不过一般我们都不会直接使用以上三种线程池,因为这FixedThreadPool 和SingleThreadPool两种线程池的阻塞队列长度为Integer.MAX_VALUE,几乎不可能用完。如果有大量请求过来,就会导致OOM错误。CachedThreadPool和ScheduledThreadPool允许创建线程数量为Integer.MAX_VALUE,也会导致OOM
自实现线程池代码如下:
ExecutorService threadPool=new ThreadPoolExecutor(
2, //corePoolSize
5, //maximunPoolSize
1L, //keepAliveTime
TimeUnit.SECONDS, //unit
new LinkedBlockingDeque<>(3), //workQueue
Executors.defaultThreadFactory(), //threadFactory
new ThreadPoolExecutor.DiscardPolicy());//handler
如何选择线程数量
线程池中线程数量过大或者过小都不好。因为如果线程过多会导致频繁的上下文切换,消耗cpu资源,线程过少会导致任务无法及时完成或者堆积任务太多致使OOM错误。所以根据任务类型一般如下划分:
计算密集型(排序):N+1,将线程数设置为CPU内核数+1,多出来的一个用于防止线程偶发缺页中断,或者其他导致线程停止的情况,多出来的一个线程就能充分利用CPU。
IO密集型(网络操作、文件读取):2N,这种任务大多数时间都在进行IO,CPU使用很少,那么线程在处理IO时就可以将CPU给其他线程使用,因此可以多配些线程。
AQS
AQS是什么
AQS是一个模板,可以通过实现这些模板的方法,来实现一个锁。它核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
AQS提供了一些方法用于实现:
isHeldExclusively() //该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int) //独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int) //独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int) //共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
同步队列的实现
ReentrantLock如何实现AQS的
ReentrantLock默认是非公平锁,非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。
公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。
Runnable和Callable常见对比
- Runnable和Callable
Runnable适用于没有返回值和不需要抛异常的场景,Callable可以
- execute( )和submit( )
excute不会返回值,无法判断线程执行成功与否,submit会返回一个Future类型对象,通过future.get( )方法获取返回值。
- shutdown( )和shutdownNow( )
shutdown会关闭线程池,状态变为shutDown,但阻塞队列中的任务会执行完毕。shutdownNow也会关闭线程池,状态变为stop,阻塞队列中任务也停止执行。
- isTerminate( )和isShutDown( )
isShutDown当调用shutdown关闭线程池后返回true
isTerminate当调用shutdown,并处理完所有阻塞队列中线程后返回true。
ThreadLocal
Linux
开发框架
Dubbo
Dubbo使用步骤
1、配置该服务在全局中的名称
dubbo.application.name=service-provider
2、配置注册中心地址
dubbo.registry.address=127.0.0.1:2181
3、配置注册中心协议
dubbo.registry.protocol=zookeeper
4、指定通信协议
dubbo.protocol.name=dubbo
dubbo.protocol.port=20880
5、配置监控中心
dubbo.monitor.protocol=registry
5、暴露服务
@Service类注解
6、调用服务
@Reference属性注解
7、开启dubbo注解
@EnableDubbo
关闭启动时检查
直接在reference中配置check属性,或配置consumer的统一缺省值
超时异常
如果在指定时间内没有返回结果则报异常
在reference中配置timeout属性,缺省为1000ms
配置超时覆盖级别
reference-method>service-method>reference>service>consumer>provider
重试次数
reference-retries:不算第一次请求的重试次数,如果有多个服务提供方,会均衡尝试其他服务器
多版本
Service指定的类一样,但ref实现类不同,使用version指定不同版本。在调用服务时声明版本即可。
Springboot整合dubbo的方式
1)导入dubbo-starter,在application.properties中配置属性,使用@Service暴露服务,使用@Reference调用服务。优点是使用方便,但无法在方法级别进行控制
2)保留dubbo的xml文件,使用标签进行配置,使用@ImportResource注解导入配置即可
优点是控制更精细,但配置繁琐,可能需要重复写很多内容
3)使用注解API的方式
将每一个组件手动创建到容器中,在@Configuration注解的类下进行配置
Zookeeper宕机与dubbo直连
注册中心全部宕机后,仍能通过本地缓存通信
没有注册中心也能通过dubbo直连来通信
负载均衡
在暴露或调用服务时更改负载均衡机制
1)权重随机 Random Loadbalance
按照权重值进行随机调用
2)基于权重的轮询 RoundRobin Loadbalance
基本策略是轮询,当某台机器轮询次数达到其权重,就会分配给其他机器
3)最小活跃数 LeastActive Loadbalance
每次请求前先获取上次请求的时间,选择响应最快的机器进行处理
4)一致性hash ConsistentHash Loadbalance
每台机器对应一个hash值,hash到谁就选择谁服务
服务降级
当服务器压力剧增时,根据实际业务情况及流量,对一些服务和页面有策略的不处理或简单处理,从而释放资源保证核心交易的正常高效运作。
1)mock=force:return+null 消费者对该服务的调用返回空,不发起远程调用
2)mock=fail:return+null 调用失败后返回空
服务容错
Hystrix
Springboot
@Trasactional注解
@Bean注解
@Component注解
Spring源码
beanFactory是什么
那么我们为什么需要Spring框架来给我们提供这个beanFactory的功能呢?原因是一般我们认为是,可以将原来硬编码的依赖,通过Spring这个beanFactory这个工厂来注入依赖,也就是说原来只有依赖方和被依赖方,现在我们引入了第三方——spring这个beanFactory,由它来解决bean之间的依赖问题,达到了松耦合的效果;这个只是原因之一,还有一个更加重要的原因:在没有spring这个beanFactory之前,我们都是直接通过new来实例化各种对象,现在各种对象bean的生产都是通过beanFactory来实例化的,这样的话,spring这个beanFactory就可以在实例化bean的过程中,做一些小动作——在实例化bean的各个阶段进行一些额外的处理,也就是说beanFactory会在bean的生命周期的各个阶段中对bean进行各种管理,并且spring将这些阶段通过各种接口暴露给我们,让我们可以对bean进行各种处理,我们只要让bean实现对应的接口,那么spring就会在bean的生命周期调用我们实现的接口来处理该bean。
bean是怎么产生的?
class描述的是普通java类,但在spring中,被spring容器进行管理的类被称作bean。那么一个类从普通类变成bean的过程是:class——beanDefinition——bean,beanDefinition就是在类的基础上加入一些bean特有的描述信息,对bean进行描述。
详细过程:
/**
1)scan扫描@service、@bean、@controller等注解注释的类
2)循环:使用GenericBeanDefinition类去初始化这些类,加入新的属性使之成为bean
3)将这些bean以("xxxn",genericBeanDefinition)的形式put到map中,将name存到List中
4)调用扩展(如果有的话)
5)遍历list,根据list中的字符串去map中取元素,根据BeanDefinition信息去决定一些操作(要不要new?有无参数?参数合理吗?等)
6)new
spring什么时候开始实例化单例的类?AbstractApplicationContext类中有refresh方法,其中有一系列操作,finishBeanFactoryInitialization操作开始实例化单例(非懒加载)类。
refresh方法
1、为IOC容器以及Bean的生命周期管理提供条件。
2、刷新Spring上下文信息,定义Spring上下文加载流程。其中ConfigurationClassParser解析各种标签比如@Bean
bean的生命周期
getBean方法
AbstractBeanFactory类中的核心方法——getBean( )——获取bean实例的方法
getBean方法调用doGetBean,doGetBean有四个参数:
name,requireType,args,typeCheckOnly(默认false)
提供了四种覆写方法:
name,name+requireType,name+args,全都有
getSingleton方法
getSingleton方法涉及到三个重要的map,也被称为解决循环依赖的三级缓存
1.singletonObjects:一级缓存,存储已生成的bean,即单例池
2.earlySingletonObjects:二级缓存,用于提前暴露那些已经生成实例但还未成为bean
3.singletonFactories:三级缓存,里面有一个objectFactory,可以生成bean
?
其实使用一三级缓存即可完成,使用第二级缓存earlySingletonObjects的原因是:从三级缓存获取对像时需要每次都通过工厂去拿,需要遍历所有的后置处理器、判断是否创建代理对象。二级缓存的存在避免循环依赖中再次通过工厂获取bean这一复杂流程,提升加载效率。比如A中需要注入B和C两个属性,而B和C也都依赖A,那么在B实例化完成后,A要继续进行C的注入,如果没有二级缓存,C就要再进行一次A工厂获取实例的过程,效率降低。
循环依赖
假设A与B那个对象,A依赖B,B依赖A
(1)通过getBean方法试图获取A,getBean会调用getSingleton方法,但由于单例池(一级缓存)中并不存在A,所以getSingleton返回空
(2)此时转而去创建A,调用doCreateBean方法创建A实例
(3)A实例创建后,populateBean注入属性,发现依赖B,此时转而去获取B
(4)获取B的流程同A一样,但也获取不到,转而开始创建B,但注入属性的过程发现依赖A
(5)注入属性时调用getSingleton方法,发现singletonObjects中没有A,但是A已经被放入“正在创建集合”当中,于是试图去earlySingletonObjects中获取A的半成品(A实例的引用),但也没有,于是尝试三级缓存
singletonFactories,由于A通过ObjectFactory将自己提前曝光了,所以B能够通过ObjectFactory.getObject拿到A对象。
(6)获取到后就可以继续进行B的bean的创建,返回后继续A的创建
//核心方法getSingleton
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory=this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
控制反转与依赖注入
依赖注入的三种方式:1.构造器注入 2.setter方法注入 3.接口注入
为什么无法解决构造器注入的循环依赖?
因为spring解决循环依赖的方式主要在于提前暴露早期bean,这时的bean不完全,只是一个普通的java对象实例,但即便如此,想要获取它的引用,也至少要通过构造器new出一个简单java对象出来,如果是构造器注入,这个对象就new不出来,因此无法提前进行暴露,就无法解决循环依赖
容器启动过程
容器启动主要步骤都在refresh方法的refreshBeanFactory中,具体步骤:(1)调用loadBeanDefinition方法,使用beanDefinitionReader去读取xml文件里的信息(2)获取到xml配置信息后,调用registerBeanDefinition注册BeanDeifinition(3)加载并执行BeanFactoryPostProcessor,具体是通过检查所有继承自BeanFactoryPostProcessor的实现类(此后置处理器在bean实例化前,可以对BeanDefinition进行修改)(4)注册BeanPostProcessor(该后置处理器是在bean实例化前后执行,可以对bean进行操作)(5)实例化bean
Bean实例化与初始化
Bean初始化的前后会执行BeanPostProcessor方法,初始化方法可自定义,可能给bean赋个值啥的。
Bean的实例化过程:(1)调用getBean方法(2)如果没有bean,调用createBean方法(3)利用反射创建没有属性的bean实例(4)为解决循环依赖问题,将实例化后的bean放进缓存中提前暴露(5)在设置bean属性之前,允许BeanPostProcessor修改属性值(6)根据BeanDefinition中的属性设置bean属性(7)执行bean的初始化方法并执行前后置处理器
BeanFactoryPostProcessor和BeanPostProcessor的区别
BeanFactoryProcessor操作的是BeanDefinition,此时bean还未实例化。BeanPostProcessor操作的是bean,此时bean已经完成属性的注入。
对于BeanFactoryProcessor来说,是直接执行的,BeanPostProcessor会先注册到map里,后续再取出来挨个执行。
Aop实现过程(怎么换成代理对象?怎么执行切面方法?)
注册过程: 找到所有匹配当前bean 的advisor(配置文件中配置)
实现过程: AOP就是利用已经实例化bean后,在BeanPostProcessor中实现。在实例化bean的第7步中会调用BeanPostProcessor的后置方法postProcessAfterInitialization,而代理对象会继承BeanPostProcessor并实现该方法,将bean对象换为代理对象返回。代理对象中有什么呢,其中存了需要覆盖执行的原方法(TargetSource),后续每当bean在执行方法时,都会判断该方法是不是需要覆盖的方法,如果是,则使用代理对象执行。
IOC中涉及的设计模式
工厂模式
BeanFactory和ApplicationContext提供了getBean方法,用于创建对象
单例模式
ConcurrentHashMap存储单例,每次获取对象前先去查表。
策略模式
一个类的行为或其算法可以在运行时更改。获取Resource有多种不同的方法,Path、URL或者File,调用Applicationcontext的getResource方法时,会根据你选择的读取类别执行响应的读取方法。
模板模式
Mybatis
Mybatis和传统JDBC的区别
1、优化获取和释放,JDBC需要进行一系列复杂操作,mybatis配置即可
2、jdbc的SQL语句分布在各个类中,mybatis可以对SQL语句进行统一集中的管理
3、mybatis生成动态sql很方便,if-test即可
4、结果集映射。Jdbc需要对返回的ResultSet自行封装,mybatis可以直接设置resultType获取响应的结果集。
使用步骤:
1.加入maven依赖
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.1</version>
</dependency>
2.创建Dao接口,定义了操作数据的方法
3.创建mapper文件(sql映射文件),写和接口方法对应的sql语句
4.创建mybatis主配置文件
(1)default属性中应该选取一个environment作为环境
(2)dataSource中填入四项基本参数
(3)mapper表中写入所有需要此配置的sql映射
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="mydev">
<environment id="mydev">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatisDemo?serverTimezone=GMT%2B8"/>
<property name="username" value="root"/>
<property name="password" value="gczxlx"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="com/my/dao/StudentDao.xml"/>
</mappers>
</configuration>
5.使用sqlSession获取并执行sql语句
//获取配置
String config="mybatis.xml";
InputStream in=Resources.getResourcesAsStream(config);
SqlSessionFactoryBuilder builder=new SqlSessionFactoryBuilder();
SqlSessionFactory factory=builder.build(in);
SqlSession sqlsession=factory.openSession();
?
//执行sql
String sqlId="com.my.dao.StudentDao"+"."+"selectAll";
List<entity> list=sqlSession.selectList(sqlId);
传参数方法
1.单个参数
使用#{}作为占位符,单个参数时里面内容可以随意填写
?
<select id="selectOne" returnType="com.my.entity.student">
select id,name,email,age from student where id=#{studentId}
</select>
2.多个参数
<select id="selectMultParam" returnType="com.my.entity.student">
select id,name,email,age from student where id=#{myId} or name=#{myName}
</select>
public Student selectMultParam(@Param("myId") integer id,
@Param("myName") String name);
3.传入对象,使用对象的属性作为参数
传统方法如下
<select id="selectMultObject" returnType="com.my.entity.student">
select id,name,email,age from student where
id=#{paramId,javaType=java.lang.Integer,jdbcType=INTEGER} or
name=#{paramName,javaType=java.lang.String,jdbcType=VARCHAR}
</select>
?
简化后只需要传入属性名称即可,javaType和jdbcType可以通过反射获取
<select id="selectMultObject" returnType="com.my.entity.student">
select id,name,email,age from student where
id=#{paramId} or
name=#{paramName}
</select>
模糊查询
1.直接将like字符串准备好,传入
String name="%李%";
dao.selectOne(name);
<select id="selectMultParam" returnType="com.my.entity.student">
select id,name,email,age from student where name like #{name}
</select>
2.在mapper文件中拼接like内容
<select id="selectMultParam" returnType="com.my.entity.student">
select id,name,email,age from student where name like "%"+#{name}+"%"
</select>
动态sql
1.if标签,判断条件,符合条件的加入到语句中
<select id="selectStudentIf" returnType="com.my.entity.student">
select id,name,email,age from student where
<if test="name!=null and name!=‘‘ ">
name = #{name}
</if>
<if test="age>0">
or age > #{age}
</if>
</select>
2.where标签
<select id="selectStudentIf" returnType="com.my.entity.student">
select id,name,email,age from student
<where>
<if test="name!=null and name!=‘‘ ">
name = #{name}
</if>
<if test="age>0">
or age > #{age}
</if>
</where>
</select>
Redis
基本数据结构
- String
常用命令: set,get,decr,incr,mget
String数据结构是简单的key-value类型,value其实不仅可以是String,也可以是数字。常规key-value缓存应用:常规计数,微博数,粉丝数等。
- List
常用命令: lpush,rpush,lpop,rpop,lrange
- list 就是链表,Redis list 的应用场景非常多,也是Redis最重要的数据结构之一,比如微博的关注列表,粉丝列表,消息列表等功能都可以用Redis的 list 结构来实现。 ? Redis list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。另外可以通过 lrange命令,就是从某个元素开始读取多少个元素,可以基于list实现分页查询,这个很棒的一个功能,基于redis实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西(一页一页的往下走),性能高。
- Hash
常用命令: hget,hset,hgetall
- hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以 hash 数据结构来存储用户信息,商品信息等等,或者通过记录cookie来做单点登录。
- Set
key= luxu
value={
"id":1,
"name":"luxu",
"age":18,
"location":"hangzhou"
}
常用命令: sadd,spop,smembers,sunion
set提供与List类似的功能,但set的特点是元素不得重复
当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。可以基于set轻易实现交集、并集、差集的操作。
比如:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程,具体命令如下:
sinterstore key1 key2 key3 将交集存在key1内
- ZSet
常用命令:zadd,zrange,zrem,zcard
和set相比,sorted set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。
举例: 在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息,适合使用redis 中的 Sorted Set 结构进行存储。
- 图
- bitMap
redis的过期策略以及内存淘汰机制
分析:这个问题其实相当重要,到底redis有没用到家,这个问题就可以看出来。比如你redis只能存5G数据,可是你写了10G,那会删5G的数据。怎么删的,这个问题思考过么?还有,你的数据已经设置了过期时间,但是时间到了,内存占用率还是比较高,有思考过原因么?
回答:
redis采用的是定期删除+惰性删除策略。
为什么不用定时删除策略?
定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略.
定期删除+惰性删除是如何工作的呢?
定期删除,redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。
于是,惰性删除派上用场。也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。
采用定期删除+惰性删除就没其他问题了么?
不是的,如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性删除也没生效。这样,redis的内存会越来越高。那么就应该采用内存淘汰机制。
在redis.conf中有一行配置
# maxmemory-policy + 策略名称
该配置就是配内存淘汰策略的(什么,你没配过?好好反省一下自己)
1)noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。应该没人用吧。
2)allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。推荐使用,目前项目在用这种。
3)allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。应该也没人用吧,你不删最少使用Key,去随机删。
4)volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。这种情况一般是把redis既当缓存,又做持久化存储的时候才用。不推荐
5)volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。依然不推荐
6)volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。不推荐
ps:如果没有设置 expire 的key, 不满足先决条件(prerequisites); 那么 volatile-lru, volatile-random 和 volatile-ttl 策略的行为, 和 noeviction(不删除) 基本上一致。
RDB和AOF
rdb全局维护一个文件,fork出一个子进程,将数据集写入临时文件中,写入成功后进行持久化,即直接覆盖旧文件,粒度较大
aof持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录,粒度较小。提供三种同步策略:每秒同步、每修改同步和不同步
|
优点 |
缺点 |
rdb |
数据紧凑,适用于灾难性恢复。性能好,避免复杂的IO进程 |
数据丢失比较多,save,shutdown,slave之前系统崩了,就没法恢复了。fork的时候,数据集如果太大就会暂停很长时间。 |
aof |
粒度小所以可用性高。写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容 |
aof文件较大。运行效率较低 |
缓存问题
缓存穿透
描述:
当一个请求在缓存内没有查到数据,便会去数据库中查,数据库也查不到就返回空。此时如果有恶意攻击,比如产生大量id=-1的查询,会导致数据库负荷过高崩掉。
?
解决方案:
1.布隆过滤器
2.做用户鉴权筛选
3.id做基础校验,如id>=0
缓存击穿
描述:
当缓存中没有数据而数据库中有数据时,比如缓存过期的情况。此时并发过高对数据库的访问压力瞬间增加,导致压力过大
?
解决方案:
1.热点数据永不过期
2.互斥锁:对数据库的访问上锁,这样可以限制单位时间访问数据库的进程数量
缓存雪崩
描述:
和缓存击穿类似,但此时不是一条数据反复查询,而是多条数据可能都过期了,同时并发查询多条数据,造成宕机
?
解决方案:
1.热点数据永不过期
2.缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生
3.分布式存储,将缓存数据分布到不同的缓存数据库中
缓存一致性问题
问题:如果数据更新的话是先更新数据库还是先更新缓存?若果先更新数据库再更新缓存会涉及什么问题?
策略一:先更新数据库,再更新缓存
这种方法不建议使用,有两个原因。
1、从线程安全角度,更新数据库和更新缓存是两个不同的操作,并不能保证这两个操作串行执行。如果同时有请求A和请求B进行更新操作,那么会出现:(1)线程A更新了数据库(2)线程B更新了数据库(3)线程B更新了缓存(4)线程A更新了缓存。这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存,这就导致了脏数据,因此不考虑。
2、从具体业务角度,如果业务是写多读少的情况,频繁的写入数据库然后更新缓存,但这些缓存并没有被读,或者说还没有被读到就被更新,这样的缓存纯属浪费时间和空间,没有实际意义。
策略二:先删除缓存,再更新数据库
该方案会导致不一致的原因是。同时有一个请求A进行更新操作,另一个请求B进行查询操作。那么会出现如下情形:(1)请求A进行写操作,删除缓存(2)请求B查询发现缓存不存在(3)请求B去数据库查询得到旧值(4)请求B将旧值写入缓存(5)请求A将新值写入数据库。这就又导致了缓存和数据库值的不一致。也就是在一个线程更新数据库的这段时间里,缓存中可能会产生脏数据。那么可以使用延时双删策略,即更新完数据库过一会再去删一次缓存。
策略三:先更新数据库,再删除缓存
这种策略比较好,基本不会出现并发问题。
redis底层原理
redis为什么快?
1)纯内存操作,比磁盘操作快
2)单线程,避免频繁的上下文切换
3)采用非阻塞的IO多路复用机制
redis中的IO多路复用
IO多路复用在NIO中已经有所解释。所谓的多路指的是多个网络连接,或者说多个socket。redis基于Reactor模式进行接受socket连接和读写,利用select、epoll等方法监控多个socket进行读写,实现了非阻塞的网络IO。所以效率很高。
redis集群的一致性问题
先说结论:(1)redis保证最终一致性(2)用最终一致性换取了高吞吐量(3)主节点挂了的时候,如果数据没有同步到备节点,是会出现数据丢失的情况(4)发生网络分区的时候也可能会丢数据,这个时候有个node timeout时间概念
强一致性、弱一致性和最终一致性
主备和主从
redis常见场景
1.热点数据缓存
2.限时业务运用
3.计数器使用,如限制某用户发多少条短信
4.排行榜
5.分布式锁
6.分页查找,使用list的lrange操作可以获得指定数量的数据
7.社交网络共同好友等功能,使用set的取交集等功能
消息的幂等处理
1.查找和删除操作本身就是幂等处理
2.唯一索引,对于新增操作可能会出现多次新增的问题。通过建立唯一索引可以避免重复创建记录。。
3.防止页面重复提交,首先页面上要做按钮屏蔽,但光这样不够,还需要使用token机制来保证不重复提交。首先第一次发送请求前会先去服务器获取token(redis中会同步生成一个token),在后续的请求中都带上这个token。请求到达服务端后会先在redis中查找token是否存在,存在说明该请求是第一次,否则说明是重复请求。然后去校验token是否是同一个,如果是则进入业务逻辑处理,否则说明这是某个请求的重复请求。如下图:
消息队列
Nginx
Springcloud
Eureka
Eureka是遵守AP原则
Zookeeper是遵守CP原则
Zookeeper保证CP原则:
1.1当向注册中心查询服务列表时,我们可以容忍注册中心返回的是几分钟以前的注册信息,但不能接受服务直接down掉不可用。也就是说,服务注册功能对一致性的要求高于可用性。但是zk会出现这一种情况,当master节点因为网络故障与其他节点失去联系时,剩余注册功能就会重新进行leader选举看。问题在于,选举leader的时间太长,30~120s,且选举期间整个zk集群都是不可用的,这就导致在选举期间注册服务瘫痪。在云部署的环境下,因网络问题使得zk集群失去master节点是较大概率会发生的事,虽然服务能够最终恢复,但是漫长的选举时间导致的注册长期不可用是不能容忍的。
1.2Eureka看明白了这一点,因此在设计时就优先保证可用性。Eureka各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余节点依然可以提供注册和查询服务。而Eureka的客户端在向某个Eureka注册或者如果发现链接失败时,则会自动切换至其他节点,只要有一台Eureka还在,就能保证注册服务可用(保证可用),只不过查到的信息可能不是最新的(不保证一致性)。除此以外,Eureka还有一种自我保护机制,如果在15分钟内超过85%的节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,此时会出现一下几种情况:
(1)Eureka不在从注册列表中移除因为长时间没收到心跳而应该过期的服务
(2)Eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其他节点上(保证当前节点依然可用)
(3)当网络稳定时,当前实例新的注册信息会被同步到其他节点中,因此,Eureka可以很好的应对因网络故障导致部分节点失去联系的情况,而不会像zk那样是整个注册服务瘫痪。
Zookeeper
Zk的数据模型是一棵树,和Unix文件系统很类似,每个节点称作一个ZNode,每个ZNode默认存储1MB数据,每个Znode都可以通过其路径唯一标识。
提供的服务:统一命名服务
统一配置管理(zk主动对客户端同步)
所有节点配置都是一致的,对配置文件的修改希望同步到所有节点上。对于zk来说,可以将所有配置信息写入Znode中,各个客户端监听Znode,一旦Znode被更改,zk通知所有客户端。
统一集群管理(客户端主动向zk同步)
分布式环境下实时掌握每个节点的状态,可以将客户端状态写入ZNode节点中,一旦客户端状态发生变化就写入Znode中。
服务器动态上下线(服务器主动向zk同步,客户端主动获取状态,zk作为中间人)
客户端能实时洞察到服务器上下线变化。Zk将服务器状态写入zk服务器,客户端观察zk服务器的在线服务目录,可以感知到所有服务器状态。
负载均衡
负载均衡
Zookeeper使用
(1)启动
sudo bin/zkServer.sh start
(2)查看进程是否启动
sudo bin/zkServer.sh status
(3)查看状态
sudo bin/zkServer.sh status
(4)启动客户端
sudo bin/zkCli.sh
(5)退出客户端
quit
(6)停止zookeeper
sudo bin/zkServer.sh stop
面试设计题
设计一个分布式环境下全局唯一的编号生成器
设计一个带有过期时间的LRU缓存
设计一个分布式锁
设计一个分布式环境下的统一配置中心
项目复习
秒杀项目架构
业务特点
瞬时并发量大
秒杀时会有大量用户在同一时间进行抢购,瞬时并发访问量突增 10 倍,甚至 100 倍以上都有。
库存量少
一般秒杀活动商品量很少,这就导致了只有极少量用户能成功购买到。
业务简单
流程比较简单,一般都是下订单、扣库存、支付订单。
技术难点
现有业务的冲击
秒杀是营销活动中的一种,如果和其他营销活动应用部署在同一服务器上,肯定会对现有其他活动造成冲击,极端情况下可能导致整个电商系统服务宕机。
一致性
由于秒杀业务涉及到多个操作,比如下单à减库存à返回信息等,这个过程可能会出现数据不一致的情况,比如库存被减成了负的,或者付款后没货了。
高性能
面对大量请求,要能够扛住并发,就需要对数据读取操作进行优化,比如使用缓存,数据库读写分离等。
高可用性
在秒杀开始后会有大量请求涌入服务器,为了避免后端负载过高导致的系统崩溃,这就涉及到流量削峰等问题。
架构设计
限流
由于活动库存量一般都是很少,对应的只有少部分用户才能秒杀成功。所以我们需要限制大部分用户流量,只准少量用户流量进入后端服务器。
削峰
秒杀开始的那一瞬间,会有大量用户冲击进来,所以在开始时候会有一个瞬间流量峰值。如何把瞬间的流量峰值变得更平缓,是能否成功设计好秒杀系统的关键因素。实现流量削峰填谷,一般的采用缓存和 MQ 中间件来解决。
异步
秒杀其实可以当做高并发系统来处理,在这个时候,可以考虑从业务上做兼容,将同步的业务,设计成异步处理的任务,提高网站的整体可用性。
缓存
秒杀系统的瓶颈主要体现在下订单、扣减库存流程中。在这些流程中主要用到 OLTP 的数据库,类似 MySQL、SQLServer、Oracle。由于数据库底层采用 B+ 树的储存结构,对应我们随机写入与读取的效率,相对较低。如果我们把部分业务逻辑迁移到内存的缓存或者 Redis 中,会极大的提高并发效率。
优化策略
一致性
减库存的方式
有三种方式:(1)下单即减库存:可能会导致取消订单的情况,这样又得回滚一次(2)付款即减库存:并发高的情况下可能会出现无法付款的情况,因为东西已经被买走(3)下单冻结库存,付款后再减库存,数据库表中有个冻结数量字段。
实际使用第三种是最常见的,但仍然存在(1)恶意下单但不付款(2)超卖。为了解决这些问题,有以下思路:
(1)恶意下单:结合安全和反作弊措施来制止。比如,识别频繁下单不付款的买家并进行打标,这样可以在打标买家下单时不减库存;再比如为大促商品设置单人最大购买件数(可以使用redis来限制),一人最多只能买 N 件商品;又或者对重复下单不付款的行为进行次数限制阻断等
(2)避免超卖:一种是在通过事务来判断,即保证减后库存不能为负,否则就回滚;二是直接设置数据库字段类型为无符号整数,这样一旦库存为负就会在执行 SQL 时报错;
高并发读
在高并发场景下,秒杀的业务适合CAP理论中的AP场景,即允许读场景下一定的脏数据,这样只会导致少量原本无库存的下单请求被误认为是有库存的,等到真正写数据时再保证最终一致性。
高性能
动静分离
网页展示出的内容一般除了秒杀倒计时是一直在变化的,其他的都是静态的,那么可以将动态和静态的数据进行分开存储和加载。
热点数据处理
首先要进行热点数据识别,尽可能的确定哪些数据是热点数据,比如秒杀活动中的这些商品就是热点数据。识别出后就要进行热点数据的隔离,避免这些操作对其他服务的影响。可以直接将这些数据的操作请求分流到独立的服务器上,使用单独的缓存数据库。
高可用
流量削峰方案
答题
通过答题来实现:
(1)防止作弊。早期秒杀器比较猖獗,存在恶意买家或竞争对手使用秒杀器扫货的情况,商家没有达到营销的目的,所以增加答题来进行限制。并且如果答题时间<1s 人为操作的可能性就很小,可以进一步防止机器答题的情况
(2)延缓请求。答题可以人为拉长峰值下单的时长,由之前的 <1s 延长到 <10s。这个时间对于服务端非常重要,会大大减轻高峰期并发压力;另外,由于请求具有先后顺序,答题后置的请求到来时可能已经没有库存了,因此根本无法下单,此阶段落到数据层真正的写也就非常有限了
排队
一般使用消息队列,
搜索引擎项目
遇到的问题
传感器的参数特别多,而且收集到的数据参差不齐,这些数据可能会用到,比如做匹配或者展示详细信息,但我们在展示传感器列表时并不想把这些都查出来。1.0版本:只有一张表,展示传感器简要信息时就只查需要的字段,展示传感器详细信息时就都查出来。问题就是这样效率不高,即便是查少量字段也需要读取这一整行,就浪费了很多不必要的时间。2.0在经常查找的字段上加索引,这样就可以减少,问题在于索引有时候会失效,还是要读取一整行再去取字段,有时甚至会全表扫描。3.0加覆盖索引,直接给所有简要信息都加上索引。最后索引文件太大,还不如之前。4.0分表,将传感器简要信息和详细信息分表,用外键关联。后来实习的时候发现公司的大型项目也是用这种方法。
个人博客项目
功能
浙商供应链
项目技术栈
前端
vue+hui
后端
RPC-dubbo
框架-springboot
数据库-mysql
负载均衡-nginx
缓存-redis
持久层-MyBatis
项目架构
项目是一个前后端分离的微服务项目,后端只提供服务接口,前端进行页面控制。项目分为仓储、拍卖、会员、挂牌、资金、公共基础几个模块。模块直接的调用:使用公司组件(CloudReference注解),类似feign,不需要自己构建http请求。然后就像是调用自身工程的方法调用,而感觉不到是调用远程方法。
单个模块包含外部接口和内部接口两个子模块,外部接口通过URL访问,内部接口仅供模块内使用。每个服务通过url进入到service层进行逻辑处理(鉴权,参数校验,),在dao层使用mybatis进行持久化。部分返回的结果使用ServiceResult类封装。
实现的功能点
货主仓单注册
仓单注册列表
Page方法
用户权限校验
判断品种参数是否存在
调用dao接口查询
注册申请
addRegister方法
用户权限校验
明细校验
调用内部方法addRegisterInfo
处理注册信息(用户信息,操作员信息,仓单基本信息)
处理仓单注册明细(类型,冻结量)
处理货品属性信息(主要是数量和种类)
注册详情
参数校验
权限校验
查询仓单注册基本信息
查询仓单注册明细信息
查询货品属性信息
封装返回结果
注册修改
传入注册对象
更新
注册提交
为什么要将仓单注册新增和提交分开?因为一方面减少用户误操作的情况,另一方面方便用户随时进行修改,因为一旦提交的仓单就无法再修改了,除非审核被打回。
参数、权限校验
根据id查询仓单
判断当前状态值是不是合法(已提交不能重复提交)
提交信息
冻结用户库存(只是冻结,并没有减少,管理员审核后才真正减少库存)
注册撤回
撤回
解冻库存
管理员审核
传入仓单id
参数校验
权限校验
仓单状态校验
更新仓单信息
解冻库存
减少库存
新增仓单变更日志
货主仓单管理
仓单查询
仓单详情
仓单下架
仓单删除
拍卖报价优化
项目介绍:
浙商供应链项目
包含仓储、仓单、拍卖、资金、挂牌等模块。每个模块独立开发。该系统可以给客户提供拍卖挂牌等交易服务,也可以作为仓储后台进行仓库管理。
主流程可以认为是,拍卖或者从挂牌系统购买到货物,货物会申请进入库存,此时可以进行货物的出库、过户、冻结,转仓单等操作,如果转仓单,又会进入到仓单的处理流程。
微服务架构,redis做缓存实现快速查询,mysql做数据库,使用mybatis与数据库交互。
权限校验:rbac理论,在dac基础上发展出的权限管理理论。三个元素:用户、角色、权限。用户拥有角色,角色拥有权限。解决了dac无法根据实际业务批量管理权限的缺点。
单体项目架构:包含两个子模块,一个api、一个server。api中定义对外暴露的接口,server中是对这些接口的具体实现,包括处理逻辑的service层(这里使用redis做缓存)和数据持久化的dao层(使用mybatis框架,mapper文件对应的接口),也包括内部使用的私有服务(很多时候内部服务包含多个原子服务,外部服务调用原子服务进行操作)。数据库使用mysql,短连接。数据库中按模块划分,每个模块都有自己的表。划分的基本逻辑是:主信息表、明细表、操作流水表,比如:仓单表、仓单明细表、仓单操作表
实习内容:我主要负责仓储仓单以及拍卖模块的后台接口开发,实现仓单注册、注销,仓储的冻结、拍卖的报价等功能。
实习收货
实习参与实际的企业级大型项目的开发,与自己学习时做的项目感觉十分不同。
(1) 单体项目结构上比我自己设计的更加合理,接口分为外部接口和内部接口,模块化的开发,只调用接口,不关心实现细节。涉及到后期可能需要扩展的功能时,将服务尽可能抽象出基本操作,从接到请求开始可以设计多个后置处理器以便满足多种功能。
(2) 使用dto作为对外传输的对象
(3) 大部分传参使用的是dto对象来统一接受而不是直接传变量
(4) 查询使用query对象,这样可以自定义查询内容,减少参数,如果需要加参数也无需改动pojo
(5) 分布式技术虽然实际工作涉及较少,但我为了更好的理解项目架构也自学了一些理论知识,有了较好的分布式理论基础
(6) 开发规范上:日志很重要,在开发时要常用日志记录,之前自己做单体项目开发很少加日志。在报错时通过日志定位问题
参与的项目业务十分繁杂,功能点很多。所以对大型项目的设计和开发关键技术(比如spring、微服务)有了更深入的学习。在项目架构设计时应该要降低耦合度,减少冗余代码。比如使用微服务,将服务拆分开发。在设计类和方法时要预留出可扩展的点,比如在以前开发时接口参数就是需要几个写几个,现在知道应该要使用对象来传参,这样便于统一的修改。对于变化频繁,扩展性高的方法,可以拆分原子操作,然后执行,或者使用模板方法来减少重复代码。
在技术上对微服务架构、redis缓存基本使用、mybatis使用都有了基本了解。项目也激发了我的好奇心,研究了spring