面试二问了整个项目的整体的流程,为什么选择某些技术或者基础组件
1.自我介绍
2.挑一个项目,画项目的架构图
Kafka工作流程:
ACK
有三个参数配置:参数是0,1,-1.SYN攻击:TCP连接建立是需要三次握手,假设攻击者短时间伪造不同 IP 地址的 SYN 报文,服务端每接收到一个 SYN 报文,就进入 SYN_RCVD 状态,但服务端发送出去的 ACK+SYN 报文,无法得到未知 IP 主机的 ACK 应答,久而久之就会占满服务端的 SYN 接收队列(未连接队列),使得服务器不能为正常用户服务。
避免 SYN 攻击方式:
方式一、增大半连接队列。通过修改Linux内核参数,控制队列大小和当队列满时应做什么处理。
net.core.netdev_max_backlog
net.ipv4.tcp_max_syn_backlog
net.ipv4.tcp_abort_on_overflow
增⼤ tcp_max_syn_backlog 和 somaxconn 的⽅法是修改 Linux 内核参数:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xYzNEmOl-1632587894643)(D:\Desktop\知识整理\图片\image-20210724152822889.png)]
增⼤ backlog 的⽅式,每个 Web 服务都不同,⽐如 Nginx 增⼤ backlog 的⽅法如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AuCUjO2P-1632587894647)(D:\Desktop\知识整理\图片\image-20210724152859337.png)]
方式二:开启tcp_syncookies功能。Linux内核 SYN(未完成连接建立)队列与 Accept (已完成连接建立)队列是如何工作的:
产生问题的环节:
如果应用程序过慢时,就会导致「 Accept 队列」被占满。
如果不断受到SYN攻击,就会导致「 SYN 队列」被占满。tcp_syncookies的方式可以应对SYN攻击的方法:
net.ipv4.tcp_syncookies = 1
#(0值表示关闭该功能;1值表示仅当SYN半连接队列放不下时,再启用它;2值表示无条件开启功能)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1AK2jT6O-1632587894649)(D:\Desktop\知识整理\图片\image-20210724152705338.png)]
方式三:减少SYN+ACK重传次数。当服务端受到SYN攻击时,就会有大量处于SYN_REVC状态的TCP连接,处于这个状态的TCP会重传SYN+ACK,当重传超过次数达到上限后,就会断开连接。那么针对SYN攻击的场景,我们可以减少SYN+ACK的重传次数,以加快处于SYN_REVC状态的TCP连接断开。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qc3PRBlP-1632587894651)(D:\Desktop\知识整理\图片\image-20210724152635912.png)]
重放攻击:Session ID和Session Ticket都不具备向前安全性,一旦加密的会话密钥的密钥被破解或者服务器泄露会话密钥 ,前面劫持的通信密文都会被破解。
解决办法:避免重发攻击的方式就是需要对话密钥设定一个合理的过期时间。
重放攻击的危险之处在于,如果中间人截获了某个客户端的Session ID或Session Ticket以及POST报文,而一般POST请求会改变数据库的数据,中间人就可以利用此截获的报文,不断向服务器发送该报文,这样就会导致数据库的数据被中间人改变了,而客户是不知情的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zYTshBBH-1632587894653)(D:\Desktop\知识整理\图片\image-20210825165554969.png)]
Cookie和Session都是用来跟踪浏览器用户身份的会话技术,但两者有所区别:
由于Session是基于Cookie的。浏览器发起请求会携带SessonID到服务器,服务器根据这个id来判断当前访问的是哪个Session。
然而浏览器被关闭后由于浏览器的Cookie文件还未设置MaxAge值,所以在此时浏览器的Cookie是会话级别的,是存在浏览器的内存中,当浏览器被关闭时,浏览器的内存被释放,临时文件被清除,这时的Cookie也随之销毁,则当前这个请求中并没有之前的那个SessionID值,服务器就当是第一次访问,给浏览器创建一个新的Session值并返回一个null。
但是之前的那个Session并没有被干掉,只是浏览器找不到这个SessionID了。这样一来,此时服务器就存在了两个Session了。服务器会一直保留这个会话对象直到它一直处于非活动状态并超过设定的间隔为止。所以我们没必要重新添加Session。解决方法如下:结合Cookie来实现
我们可以手动为Cookie中添加JSESSIONID信息,此时不管你的浏览器是否关闭,我的Cookie中都会携带JSESSION信息,这样的话,只要Session没有消亡,服务器就一定能够找到对应的Session,而不会重新建立一个新的Session。
//登录成功后 ? 手动添加cookie,保存JSESSIONID信息
Cookie cookie = new Cookie("JSESSIONID", session.getId());
cookie.setMaxAge(1800); //设置cookie 和 session生命周期同步.
response.addCookie(cookie);
滑动窗口,慢启动算法,越传越快。
ssthresh(slow start threshold),是一个上限,当cwnd >= ssthresh时,就会进入“拥塞避免算法”。一般来说ssthresh的值是65535,单位是字节,当cwnd达到这个值+后,算法如下:
收到一个ACK时,cwnd = cwnd + 1/cwnd
当每过一个RTT时,cwnd = cwnd + 1
这样就可以避免增长过快导致网络拥塞,慢慢的增加调整到网络的最佳值。很明显,是一个线性上升的算法。
无连接:是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。当请求时建立连接、请求完释放连接,以尽快将资源释放出来服务其他客户端,可以加KeepAlive弥补无连接的问题
无状态:是指协议对于事务处理没有记忆能力,服务器不知道客户端是什么状态。即我们给服务器发送 HTTP 请求之后,服务器根据请求,会给我们发送数据过来,但是,发送完,不会记录任何信息。 可以通过Cookie和Session来弥补这个问题。
CPU有可能会把相应代码的CPU指令进行一次重排序,这虽然能提高运行的效率,但是也可能会影响一些数据的可见性,而volatile通过 内存屏障 这个指令,来保证了相应代码块的执行顺序。内存屏障还会强制更新一次CPU缓存。加载最新的内容。
TCP粘包是指:发送方发送的若干包数据到接收方接收时粘成一包
发送方原因:
TCP默认使用Nagle*[ˈneɪgəl]*算法(主要作用:减少网络中报文段的数量)。
收集多个小分组,在一个确认到来时一起发送、导致发送方可能会出现粘包问题
接收方原因:
TCP将接收到的数据包保存在接收缓存里,如果TCP接收数据包到缓存的速度大于应用程序从缓存中读取数据包的速度,多个包就会被缓存,应用程序就有可能读取到多个首尾相接粘到一起的包。
解决粘包问题:
最本质原因在于接收方无法分辨消息与消息之间的边界在哪,通过使用某种方案给出边界,例如:
发送定长包。每个消息的大小都是一样的,接收方只要累计接收数据,直到数据等于一个定长的数值就将它作为一个消息。
包尾加上\r\n标记。FTP协议正是这么做的。但问题在于如果数据正文中也含有\r\n,则会误判为消息的边界。
包头加上包体长度。包头是定长的4个字节,说明了包体的长度。接收方先接收包体长度,依据包体长度来接收包体。
⼀开始,客户端和服务端都处于 CLOSED 状态。先是服务端主动监听某个端⼝,处于 LISTEN 状态;
SYN_SENT
**状态;SYN_RCVD
状态;ESTABLISHED
状态。服务端收到客户端的应答报文后,也进入 ESTABLISHED
状态。
可通过
netstat -napt
命令查看TCP的连接状态
什么是TCP连接:用于保证可靠性和流量控制维护的某些状态信息,这信息的组合,包括 Socket、序列号和窗口大小称为连接。
三次握手才可以避免历史连接(主要原因);
我们来看看 RFC 793 指出的 TCP 连接使⽤三次握⼿的⾸要原因:
The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion.
简单来说,三次握⼿的⾸要原因是为了防⽌旧的重复连接初始化造成混乱。
客户端连续发送多次 SYN 建⽴连接的报⽂,在⽹络拥堵情况下:
三次握手才可以同步双方的初始序列号;
TCP 协议的通信双⽅, 都必须维护⼀个「序列号」, 序列号是可靠传输的⼀个关键因素,它的作⽤:
三次握手才可以避免资源浪费;
如果只有「两次握⼿」,当客户端的 SYN 请求连接在⽹络中阻塞,客户端没有接收到 ACK 报⽂,就会重新发送 SYN ,由于没有第三次握⼿,服务器不清楚客户端是否收到了⾃⼰发送的建⽴连接的 ACK 确认信号,所以每收到⼀个 SYN 就只能先主动建⽴⼀个连接,这会造成什么情况呢?
如果客户端的 SYN 阻塞了,重复发送多次 SYN 报⽂,那么服务器在收到请求后就会建⽴多个冗余的⽆效链接,造成不必要的资源浪费。
无法防止历史连接的建立;会造成双方资源的浪费;也无法可靠的同步双方序列号。
FIN_WAIT_1
状态;CLOSED_WAIT
**状态;客户端收到服务端的 ACK 应答报文后,之后进入 FIN_WAIT_2
状态;LAST_ACK
状态;TIME_WAIT
状态;服务器收到了 ACK 应答报文后,就进入 CLOSED
状态,至此服务器已经完成连接的关闭;客户端在经过 2MSL*(Maximum Segment Lifetime)* 时间后,自动进入 CLOSED
状态,至此客户端也完成连接的关闭。
这⾥⼀点需要注意是:主动关闭连接的,才有 TIME_WAIT 状态。
再来回顾下四次挥⼿双⽅发 FIN 包的过程,就能理解为什么需要四次了。
从上⾯过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK 和 FIN ⼀般都会分开发送,从⽽⽐三次握⼿导致多了⼀次。
首先,主动发起关闭连接的一方,才会有 TIME_WAIT
状态。
需要**TIME_WAIT
**状态,主要有两个原因:
防止具有相同 四元组 的旧数据包被收到;
TCP 就设计出了这么⼀个机制,经过 2MSL 这个时间,⾜以让两个⽅向上的数据包都被丢弃,使得原来连接的数据包在⽹络中都⾃然消失,再出现的数据包⼀定都是新建⽴连接所产⽣的。
保证 被动关闭连接的一方能被正确的关闭,即保证最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭;
ack,超时重传,接收缓冲,快速重传
dns(首先查询本地HOST文件,没有则查询网络),ip,路由
通过 校验和
、序列号
、确认应答
、重发控制(又分为超时重传、快速重传、SACK、D-SACK)
、连接管理
、流量控制
、拥塞控制
等。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XCLcjG8K-1632587894655)(D:\Desktop\知识整理\图片\image-20210826145130394.png)]
校验和:在数据传输过程中,将发送的数据段都当做一个16位的整数,将这些整数加起来,并且前面的进位不能丢弃,补在最后,然后取反,得到校验和。
发送方:在发送数据之前计算校验和,并进行校验和的填充。
接收方:收到数据后,对数据以同样的方式进行计算,求出校验和,与发送方进行比较。
序列号:TCP 传输时将每个字节的数据都进行了编号,这就是序列号。序列号的作用不仅仅是应答作用,有了序列号能够将接收到的数据根据序列号进行排序,并且去掉重复的数据。
确认应答:TCP 传输过程中,每次接收方接收到数据后,都会对传输方进行确认应答,也就是发送 ACK 报文,这个 ACK 报文中带有对应的确认序列号,告诉发送方,接收了哪些数据,下一次数据从哪里传。
重发控制:
连接管理:就是指三次握手、四次挥手的过程。
流量控制:如果发送方的发送速度太快,会导致接收方的接收缓冲区填充满了,这时候继续传输数据,就会造成大量丢包,进而引起丢包重传等等一系列问题。TCP 支持根据接收端的处理能力来决定发送端的发送速度,这就是流量控制机制。
具体实现方式:接收端将自己的接收缓冲区大小放入 TCP 首部的『窗口大小』字段中,通过 ACK 通知发送端。
拥塞控制:TCP 传输过程中一开始就发送大量数据,如果当时网络非常拥堵,可能会造成拥堵加剧。所以 TCP 引入了慢启动机制
,在开始发送数据的时候,先发少量的数据探探路。
慢启动:当发送⽅每收到⼀个ACK,拥塞窗⼝cwnd 的⼤⼩就会加 1。
有⼀个叫慢启动⻔限 ssthresh (slow start threshold)状态变量。
当cwnd < ssthresh 时,使⽤慢启动算法。
当 cwnd >= ssthresh 时,就会使⽤「拥塞避免算法」。
拥塞避免算法:每当收到一个ACK时,cwcd增加1/cwnd。
拥塞发生:当⽹络出现拥塞,也就是会发⽣数据包重传,重传机制主要有两种:超时重传和快速重传。
当发生了 超时重传,就会使用拥塞发生算法,这个时候,ssthresh 和 cwnd 的值会发⽣变化:ssthresh 设为 cwnd/2 ;cwnd 重置为 1
当发生快速重传时,TCP 认为这种情况不严重,因为⼤部分没丢,只丢了⼀⼩部分,则 ssthresh 和 cwnd 变化如下:cwnd = cwnd/2 ,也就是设置为原来的⼀半;ssthresh = cwnd ;进⼊快速恢复算法。
快速恢复算法:快速重传和快速恢复算法⼀般同时使⽤,快速恢复算法是认为,你还能收到 3 个重复 ACK 说明⽹络也不那么糟糕,所以没有必要像 RTO 超时那么强烈。然后,进⼊快速恢复算法如下:
先说一下 IP 的基本特点:
IP 地址主要分为A、B、C三类及特殊地址D、E这五类,甩一张图:
A类:(1.0.0.0-126.0.0.0)一般用于大型网络。
B类:(128.0.0.0-191.255.0.0)一般用于中等规模网络。
C类:(192.0.0.0-223.255.255.0)一般用于小型网络。
D类:是多播地址,地址的网络号取值于224~239之间,一般用于多路广播用户。
E类:是保留地址。地址的网络号取值于240~255之间。
HTTP1.0
性能问题:每发送一个请求,都需要新建立一次TCP连接(三次握手),而且是串行请求,增加了通信开销。
HTTP1.1
性能提升:
提出了长连接的通信方式,也叫持久连接,减少了TCP连接的重复建立和断开所造成的额外开销,减轻了服务器端的负载。长连接特点:只要任意一端没有明确提出断开连接,则保持TCP连接状态。
管道网络传输:在同一个TCP连接里面,客户端可以发起多个请求,只要第一个请求发送出去了,不必等其回来,就可以发送第二个请求出去,可以减少整体的响应时间。
因此出现的新问题:队头阻塞,即服务器还是按照顺序对请求进行回应,如果前面的回应特别慢,后面就会有许多请求排队等着。
性能瓶颈:
HTTP 2:HTTP 2 协议基于HTTPS
性能提升:
性能瓶颈:
HTTP 3:HTTP 3 把HTTP下层的TCP协议改成了UDP
性能提升:
HTTPS
特点
解决安全方案
SSL/TLS协议基本流程
上述前两步就是SSL/TLS的建立过程,也就是握手阶段:①ClientHello;②ServerHello;③客户端回应;④服务器的最后回应。注:SSL/TLS 1.3经过优化只需要3次握手。
HTTPS优化
HTTPS性能损耗的两个环节
优化方案
针对第一个环节:
HTTPS协议是计算密集型,而不是IO密集型,优化重点在CPU,首选支持AES-NI特性的CPU;
软件优化:软件升级;
协议优化:尽可能选择ECDHE密钥交换算法替换RSA算法,TLS升级到1.3,简化握手的步骤,完成TLS握手只需要1个RTT延时;
证书优化:
证书传输:减小证书的大小,选择ECDSA证书,而不是RSA这个念书;
证书验证:使用OCSP Stapling(在线证书状态协议)来查询证书的有效性;
会话复用:TLS会话的目的就是为了协商出会话密钥,就是对称加密密钥,如果将首次的TLS握手协商的对称加密密钥缓存启来,下次连接时就可以直接复用,分为Session ID 和 Session Ticket;
针对第二个环节:现在主流的对称加密算法 AES、ChaCha20 性能都是不错的,⽽且⼀些 CPU ⼚商还针对它们做了硬件级别的优化,因此这个环节的性能消耗可以说⾮常地⼩。
HTTP 是超⽂本传输协议,信息是明⽂传输,存在安全⻛险的问题;
HTTPS 则解决 HTTP 不安全的缺陷,在TCP 和 HTTP ⽹络层之间加⼊了 SSL/TLS 安全协议,使得报⽂能够加密传输。
HTTP 连接建⽴相对简单,是无状态的,TCP 三次握⼿之后便可进⾏ HTTP 的报⽂传输;
⽽ HTTPS 在 TCP 三次握⼿之后,还需进⾏ SSL/TLS 的握⼿过程,才可进⼊加密报⽂传输。
HTTP 的端⼝号是 80;HTTPS 的端⼝号是 443。
HTTPS 协议需要向 CA(证书权威机构)申请数字证书,来保证服务器的身份是可信的。
对称密钥加密是指加密和解密使用同一个密钥的方式,这种方式存在的最大问题就是密钥发送问题,即如何安全地将密钥发给对方;
而非对称加密是指使用一对非对称密钥,即公钥和私钥,公钥可以随意发布,但私钥只有自己知道。发送密文的一方使用对方的公钥进行加密处理,对方接收到加密信息后,使用自己的私钥进行解密。
由于非对称加密的方式不需要发送用来解密的私钥,所以可以保证安全性;但是和对称加密比起来,它比较慢,所以我们还是要用对称加密来传送消息,但对称加密所使用的密钥我们可以通过非对称加密的方式发送出去。
cookie,请求时传递给服务端的cookie信息
set-cookie,响应报文首部设置要传递给客户端的cookie信息
allow,支持什么HTTP方法
last-modified,资源的最后修改时间
expires,设置资源缓存的失败日期
content-language,实体的资源语言
content-encoding,实体的编码格式
content-length,实体主体部分的大小单位是字节
content-range,返回的实体的哪些范围
content-type,哪些类型
accept-ranges,处理的范围请求
age,告诉客户端服务器在多久前创建了响应
vary,代理服务器的缓存信息
location,用于指定重定向后的
URI If-Match,值是资源的唯一标识
User-Agent,将创建请求的浏览器和用户代理名称等信息传递给服务器
Transfer-Encoding,传输报文的主体编码方式
connection,管理持久连接,keep-alive ,
close Cache-Control,控制浏览器的强缓存
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tIV6vQYb-1632587894656)(D:\Desktop\知识整理\图片\image-20210826164833271.png)]
方法 | 描述 |
---|---|
GET | 向特定资源发送请求,查询数据,并返回实体 |
POST | 向指定资源提交数据进行处理请求,可能会导致新的资源建立、已有资源修改 |
PUT | 向服务器上传新的内容 |
HEAD | 类似GET请求,返回的响应中没有具体的内容,用于获取报头 |
DELETE | 请求服务器删除指定标识的资源 |
OPTIONS | 可以用来向服务器发送请求来测试服务器的功能性 |
TRACE | 回显服务器收到的请求,用于测试或诊断 |
CONNECT | HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器 |
(1)在浏览器中输入www.baidu.com
域名,操作系统会先检查自己本地的 hosts 文件是否有这个网址映射关系,如果有就先调用这个IP地址映射,完成域名解析。
(2)如果 hosts 里没有这个域名的映射,则查找本地 DNS 解析器缓存,是否有这个网址映射关系,如果有直接返回,完成域名解析。
(3)如果 hosts 与 本地 DNS 解析器缓存都没有相应的网址映射关系,首先会找 TCP/IP 参数中设置的首选 DNS 服务器,在此我们叫它本地 DNS 服务器,此服务器收到查询时,如果要查询的域名,包含在本地配置区域资源中,则返回解析结果给客户机,完成域名解析,此解析具有权威性。
(4)如果要查询的域名,不由本地 DNS 服务器区域解析,但该服务器已缓存了此网址映射关系,则调用这个 IP 地址映射,完成域名解析,此解析不具有权威性。
(5)如果本地 DNS 服务器本地区域文件与缓存解析都失效,则根据本地 DNS 服务器的设置(是否设置转发器)进行查询,如果未用转发模式,本地 DNS 就把请求发至13台根 DNS ,根 DNS 服务器收到请求后会判断这个域名(.com)是谁来授权管理,并会返回一个负责该顶级域名服务器的一个IP。本地 DNS 服务器收到IP信息后,将会联系负责 .com 域的这台服务器。这台负责 .com 域的服务器收到请求后,如果自己无法解析,它就会找一个管理.com域的下一级DNS服务器地址(baidu.com)给本地 DNS 服务器。当本地 DNS 服务器收到这个地址后,就会找 baidu.com 域服务器,重复上面的动作,进行查询,直至找到 www.baidu.com 主机。
(6)如果用的是转发模式,此 DNS 服务器就会把请求转发至上一级 DNS 服务器,由上一级服务器进行解析,上一级服务器如果不能解析,或找根 DNS 或把转请求转至上上级,以此循环。不管是本地 DNS 服务器转发,还是根提示,最后都是把结果返回给本地 DNS 服务器,由此 DNS 服务器再返回给客户机。
域名解析 -> 建立TCP连接(三次握手)-> 发起http请求 -> 服务器响应http请求,浏览器得到html代码 -> 浏览器解析html代码,并请求html代码中的资源(如 js、css、图片等)-> 浏览器对页面进行渲染呈献给用户。
XSS 即(Cross Site Scripting)中文名称为:跨站脚本攻击。XSS的重点不在于跨站点,而在于脚本的执行。
XSS的原理是:
恶意攻击者在web页面中会插入一些恶意的 script代码。当用户浏览该页面的时候,那么嵌入到web页面中script代码会执行,因此会达到恶意攻击用户的目的。
XSS攻击最主要有如下分类:反射型、存储型、及 DOM-based型。反射型 和 DOM-baseed型可以归类为 非持久性XSS攻击。存储型可以归类为 持久性XSS攻击。
CSRF(Cross Site Request Forgery,跨站域请求伪造)是一种网络的攻击方式,它在 2007 年曾被列为互联网 20 大安全隐患之一,也被称为『One Click Attack』或者 『Session Riding』,通常缩写为CSRF
或者XSRF
,是一种对网站的恶意利用。
听起来像跨站脚本(XSS),但它与XSS非常不同,并且攻击方式几乎相左。
XSS利用站点内的信任用户,而CSRF则通过伪装来自受信任用户的请求来利用受信任的网站。与XSS攻击相比,CSRF攻击往往不大流行(因此对其进行防范的资源也相当稀少)和难以防范,所以被认为比XSS更具危险性。
死锁是指两个或两个以上进程在执行过程中,因争夺资源而造成的下相互等待的现象;
死锁的条件:
**解决方法:**破坏死锁的任意一条件
我说可以用netstat查看一下两端的连接状态,比如可以判断B是否被SYN攻击了,还是单纯网络问题。 另外也可能B端的文件描述符用完了,他表示能想到文件描述符这个层面很好。
当一个线程需要调用对象的 wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的notify()方法。同样的,当一个线程需要调用对象的 notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。
Thread 类的 sleep()和 yield()方法将在当前正在执行的线程上运行。所以在其他处于等待状态的线程上调用这些方法是没有意义的。这就是为什么这些方法是静态的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其他非运行线程调用这些方法。
如果是实例方法,就可以实现在一个线程里随时让其它线程在执行中休眠,而且这种休眠是不放弃锁的。那么这种简单粗暴就很容易引起混乱,因此目前这种机制其实就是一种限制:你可以选择给自己吃安眠药,而不能随便给正在工作的别人吃安眠药。
该代码只有在某个A线程执行时会被执行,这种情况下通知某个B线程yield是无意义的(因为B线程本来就没在执行)。因此只有当前线程执行yield才是有意义的。通过使该方法为static,你将不会浪费时间尝试yield 其他线程。就是说,如果是和线程实例绑定的话,你可能会在当前线程中尝试调用otherThread.yeild()/sleep(), 而这使没有意义的
首先wait()是操作监视器对象的; sleep()是由线程调用的,Thread.sleep是 使当前执行的线程休眠(就是 Thread.sleep所在代码片段的线程)。
如果sleep是实例方法,则在JVM中可以拿到thread实例的引用,因此就会出现别的线程强制另外一个线程睡眠的方法,这样就出现了线程的执行逻辑以及内存模型的不可控,所以只能把目标设定为当前的线程。
如果sleep不是静态的, 只对当前进程作用. 而是实例方法, 那么应该和suspend有同样的问题, 死锁.
sleep()是让某个线程暂停运行一段时间,其控制范围是由当前线程决定,也就是说,在线程里面决定.好比如说,我要做的事情是 “点火->烧水->煮面”,而当我点完火之后我不立即烧水,我要休息一段时间再烧.对于运行的主动权是由我的流程来控制.
而wait(),首先,这是由某个确定的对象来调用的,将这个对象理解成一个传话的人,当这个人在某个线程里面说"暂停!",也是 thisOBJ.wait(),这里的暂停是阻塞,还是"点火->烧水->煮饭",thisOBJ就好比一个监督我的人站在我旁边,本来该线 程应该执行1后执行2,再执行3,而在2处被那个对象喊暂停,那么我就会一直等在这里而不执行3,但整个流程并没有结束,我一直想去煮饭,但还没被允许, 直到那个对象在某个地方说"通知暂停的线程启动!",也就是thisOBJ.notify()的时候,那么我就可以煮饭了,这个被暂停的线程就会从暂停处 继续执行.
Java 的每个对象中都有一个锁(monitor,也可以成为监视器) 并且 wait(),notify()等方法用于等待对象的锁或者通知其他线程对象的监视器可用。在Java 的线程中并没有可供任何对象使用的锁和同步器。这就是为什么这些方法是 Object类的一部分,这样 Java 的每一个类都有用于线程间通信的基本方法。
读的时候相应的段会加锁,共有16段,所以并发读就是16。
在Linux/unix下,中止一个Java进程有两种方式,一种是kill -9 pid,一种是kill -15 pill(默认)。
SIGNKILL(9) 的效果是立即杀死进程. 该信号不能被阻塞, 处理和忽略。
SIGNTERM(15) 的效果是正常退出进程,退出前可以被阻塞或回调处理。并且它是Linux缺省的程序中断信号(默认是15)。
**阻塞IO:**在应用调用recvfrom
函数(经Socket接收数据)读取数据时,其系统调用直到数据包到达且被复制到应用缓冲区中或者发送错误时才返回,自此期间一直等待,进程从调用到返回这段时间内都是被阻塞的。
1、应用进程向内核发起recfrom
读取数据。
2、准备数据报(应用进程阻塞)。
3、将数据从内核负责复制到应用空间。
4、复制完成后,返回成功提示。
注:阻塞等待的是 内核数据准备好 和 数据从内核态拷贝到用户态 这两个过程。
**非阻塞IO:**在应用调用recvfrom
读取数据时,如果该缓冲区没有数据的话,就会直接返回一个EWOULDBLOCK错误,不会让应用一直等待中。在没有数据的时候会即刻返回错误标识,那也意味着如果应用要读取数据就需要不断地调用recvfrom
请求,直到读取到要的数据为止。
1、应用进程向内核发起recvfrom读取数据。
2、没有数据报准备好,即刻返回EWOULDBLOCK错误码。
3、应用进程向内核发起recvfrom读取数据。
4、已有数据包准备好就进行一下步骤,否则还是返回错误码。
5、将数据从内核拷贝到用户空间。
6、完成后,返回成功提示。
注:从内核拷贝到用户空间这一步骤是一个同步的过程,是需要等待的过程。
**IO多路复用模型:**进程通过将一个fd传递给select,阻塞在select操作上,select帮助侦测多个fd是否准备就绪,当有fd准备就绪时,select返回数据可读状态,应用程序再调用recvfrom
读取数据。
注:fd(file descriptor):文件描述符,Linux把所有网络请求以一个fd来标识。数据从内核态拷贝到用户态的过程也是一个同步的过程,需要等待。
**信号驱动IO模型:**首先开启套接口信号驱动IO功能,并通过系统调用sigaction执行一个信号处理函数,此时请求即刻返回,当数据准备就绪时,就生成对应的SIGIO信号,通过信号回调通知应用线程调用recvfrom
来读取数据。
注: IO复用模型里面的select虽然可以监控多个fd了,但select其实现的本质上还是通过不断的轮询fd来监控数据状态, 因为大部分轮询请求其实都是无效的,所以信号驱动IO意在通过这种建立信号关联的方式,实现了发出请求后只需要等待数据就绪的通知即可,这样就可以避免大量无效的数据状态轮询操作。
**异步IO:**应用告知启动某个操作,并让内核在整个操作完成之后,通知应用,这种模型与信号驱动模型的主要区别在于,信号驱动IO只是由内核通知我们合适可以开始下一个IO操作,而异步IO模型是由内核通知我们操作什么时候完成。
注:真正的异步I/O是 内核数据准备好 和 数据从内核拷贝到用户态 这两个过程都不用等待。
BIO:同步阻塞;
NIO:同步非阻塞和异步阻塞;
AIO:异步非阻塞;
IO多路复用技术出现的背景:为每个请求分配一个进程/线程的方式太浪费资源,所有就想办法只使用一个进程来维护多个Socket,整个技术就是IO多路复用技术。
什么时多路复用? 一个进程虽然任一时刻只能处理一个请求,但是处理每个请求的事件时,耗时控制在1毫秒以内,这样1秒内就可以处理上千个请求,把时间拉长来看,多个请求复用了一个进程,这就是多路复用,这种思想很类似一个CPU并发多个进程,所以也叫做 时分多路复用。
select==>时间复杂度O(n):它仅仅知道了,有I/O事件发生了,却并不知道是哪几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据或者需要写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。
实现方式:
1.将已连接的Socket都放到一个文件描述符集合,然后调用select函数将文件描述符集合拷贝到内核里;
2.让内核来检查是否有网络事件产生,检查的方式很粗暴,就是通过遍历文件描述符集的方式,当检查到有事件产生时,将此Socket标记为可读或可写;
3.接着再把整个文件描述符集合拷贝回用户态里,然后用户态还需要再通过遍历的方法找到可读或可写的Socket,然后再对其处理。
注:两次遍历文件描述符集合,两次拷贝文件描述符集合。select使用固定长度的BitsMap表示文件描述符集合,支持的文件描述符个数由内核中的
FD_SETSIZE
限制,默认最大值为1024,只能监听 0~1023的文件描述符。
poll (轮询)==>时间复杂度O(n):poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd(file descriptor)对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.
实现方式:
poll不是使用BitsMap来存储所关注的文件描述符,取而代之用动态数组,以链表的形式来组织,突破了select的文件描述符个数限制,当然还会受到系统文件描述符限制。但是 poll 和 select 并没有太⼤的本质区别,都是使⽤「线性结构」存储进程关注的Socket集合,因此都需要遍历⽂件描述符集合来找到可读或可写的Socket,时间复杂度为O(n),⽽且也需要在⽤户态与内核态之间拷⻉⽂件描述符集合,这种⽅式随着并发数上来,性能的损耗会呈指数级增⻓。
epoll==>时间复杂度O(1):epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))
实现方式:
epoll在内核里使用红黑树来跟踪进程所有待检测的文件描述符,把需要监控的Socket通过epoll_ctl()
函数加入内核中的红黑树里,红黑树是个高效的数据结构,增删查时间复杂度一般都是O(logn) ,通过对这颗红黑树进行操作,这样就不需要像 select/poll 每次操作时都传⼊整个 socket 集合,只需要传⼊⼀个待检测的 socket,减少了内核和⽤户空间⼤量的数据拷⻉和内存分配。
epoll使用事件驱动的机制,内核里维护了一个链表来记录就绪事件,当某个Socket有事件发生时,通过回调函数内核会将其加入到这个就绪事件列表中,当用户调用 epoll_wait()
函数时,只会返回有事件发生的文件描述符的个数,不需要像select/poll那样轮询扫描整个Socket集合,大大提高了检测的效率。
epoll支持两种 事件触发模式,分别是边缘触发(dege-triggered,ET)和水平触发(level-triggered,LT)。默认触发模式是水平触发。
使⽤边缘触发模式时,当被监控的 Socket 描述符上有可读事件发⽣时,服务器端只会从 epoll_wait
中苏醒⼀次,即使进程没有调⽤ read 函数从内核读取数据,也依然只苏醒⼀次,因此我们程序要保
证⼀次性将内核缓冲区的数据读取完;
使⽤⽔平触发模式时,当被监控的 Socket 上有可读事件发⽣时,服务器端不断地从 epoll_wait 中苏
醒,直到内核缓冲区数据被 read 函数读完才结束,⽬的是告诉我们有数据需要读取;
**补充:**select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说整个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里也都能够支持,其中epoll是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现
C10K问题:C是Client单词首字母缩写,C10K就是单机同时处理1万个请求得问题。从硬件资源角度看,对于2GB内存千兆网卡得服务器,如果每个请求处理占用不到200KB得内存和100Kbit得网络带宽就可以满足并发1万个请求。不过,要想真正实现C10K得 服务器,要考虑的地方在于服务器得I/O模型,效率低的模型会加重系统开销,从而会离C10K得目标越来越远。
**演变:**多进程模型(通过fork()
函数创建子进程) --> 多线程模型(通过pthread_create()
函数创建线程) --> I/O多路复用(又称作 时分多路复用,select/poll/epoll)
基于I/O多路复用编写网络程序是面向过程的方式写代码的,这样开发的效率不高。于是,大佬们对I/O多路复用做了一层封装,让使用者不用考虑底层网络API的细节,只需要关注应用代码的编写。
Reactor,意思是反应堆
,但这里的反应指的是对事件反应,也就是来了一个事件,Reactor就有相对应的反应/响应。 Reactor模式也叫Dispatcher*(调度员)*模式,即I/O多路复用监听事件,收到事件后,根据事件类型分配(Dispatch)给某个进行/线程。
Reactor 模式主要由 Reactor 和处理资源池这两个核⼼部分组成,它俩负责的事情如下:
Reactor 模式是灵活多变的,可以应对不同的业务场景,灵活在于:
将上⾯的两个因素排列组设⼀下,理论上就可以有 4 种⽅案选择:
C语言「单 Reactor 单进程」、Java 「单 Reactor 单线程」 --Redis采用此方案
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o765MudI-1632587894658)(D:\Desktop\知识整理\图片\image-20210730174334589.png)]
可以看到进程⾥有 Reactor、Acceptor、Handler 这三个对象:
对象⾥的 select、accept、read、send 是系统调⽤函数,dispatch 和 「业务处理」是需要完成的操作,其中 dispatch 是分发事件操作。
方案实现:
优点:
单 Reactor 单进程的⽅案因为全部⼯作都在同⼀个进程内完成,所以实现起来⽐较简单,不需要考虑进程间通信,也不⽤担⼼多进程竞争。
缺点:
所以,单 Reactor 单进程的⽅案不适⽤计算机密集型的场景,只适⽤于业务处理⾮常快速的场景。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yiAjU1Ki-1632587894659)(D:\Desktop\知识整理\图片\image-20210730175048969.png)]
方案实现:
优点:单 Reator 多线程的⽅案优势在于能够充分利⽤多核CPU 的性能。
缺点:带来了多线程竞争资源的问题。
单 Reactor 多进程相⽐单 Reactor 多线程实现起来很麻烦,主要因为要考虑⼦进程和⽗进程的双向通信,并且⽗进程还需要知道⼦进程要将数据发送给哪个客户端。⽽多线程间可以共享数据,虽然要额外考虑并发问题,但是这远⽐进程间通信的复杂度低得多,因此实际应⽤中也看不到单 Reactor 多进程的模式。
「单 Reactor」的模式还有个问题,因为⼀个 Reactor 对象承担所有事件的监听和响应,⽽且只在主线程中运⾏,在⾯对瞬间⾼并发的场景时,容易成为性能的瓶颈的地⽅。
多 Reactor 单进程 / 线程; --复杂而且没有性能优势,因此实际中并没有应用。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-obJW0Aw2-1632587894660)(D:\Desktop\知识整理\图片\image-20210730175747097.png)]
方案实现:
多 Reactor 多线程的⽅案虽然看起来复杂的,但是实际实现时⽐单 Reactor 多线程的⽅案要简单的多,原因如下:
⼤名鼎鼎的两个开源软件 Netty 和 Memcache 都采⽤了「多 Reactor 多线程」的⽅案。
采⽤了「多 Reactor 多进程」⽅案的开源软件是 Nginx,不过⽅案与标准的多 Reactor 多进程有些差异。
具体差异表现在主进程中仅仅⽤来初始化 socket,并没有创建 mainReactor 进行 accept 连接,⽽是由⼦进程的 Reactor 进行 accept 连接,通过锁来控制⼀次只有⼀个⼦进程进⾏ accept(防⽌出现惊群现象),⼦进程 accept 新连接后就放到⾃⼰的 Reactor 进⾏处理,不会再分配给其他⼦进程。
拓展:Reactor 和 Proactor 的区别
总结:Reactor 模式是基于「待完成」的 I/O 事件,⽽ Proactor 模式则是基于「已完成」的 I/O 事件。
C10K问题
非阻塞IO,耗时任务丢给计算线程
什么是线程安全?
从概念上解释:线程安全是编程中的术语,指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成
从内存上解释:”线程安全“不是指线程的安全,而是指内存的安全。目前主流操作系统都是多任务的,即多个进程同时运行。为了保证安全,每个进程只能访问分配给自己的内存空间,而不能访问别的进程的,这是由操作系统保障的。
在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存)。进程内的所有线程都可以访问到该区域,这就是造成问题的潜在原因。
假设某个线程把数据处理到一半,觉得很累,就去休息了一会,回来准备接着处理,却发现数据已经被修改了,不是自己离开时的样子了。可能被其它线程修改了。
所以线程安全指的是,在堆内存中的数据由于可以被任 线程访问到,在没有限制的情况下存在被意外修改的风险。即堆内存空间在没有保护机制的情况下,对多线程来说是不安全的地方,因为放进去的数据,可能被别的线程”破坏“。
如何做到线程安全?
解决的过程就是一个取舍的过程,不同的解决方案有不同的侧重点。
方法的局部变量是安全的。
分配给每个线程同样的局部变量,比如ThreadLocal类,但从所在”位置”的角度来讲,这些ThreadLocal数据是分配在公共区域的堆内存中的,ThreadLocal就是,把一个数据复制N份,每个线程认领一份,各玩各的,互不影响。
final关键字修饰的不变量都是线程安全的。只能看,不能摸。
如果公共区域(堆内存)的数据,要被多个线程操作时,为了确保数据的安全(或一致)性,需要在数据旁边放一把锁,想要操作数据,先获取锁再说。 --(互斥)锁
CAS --读多写少,线程数目特别少,ABA问题 --解决办法就是增加版本号或者时间戳。(乐观)锁,更倾向于认为数据不会被意外修改,如果修改了,就放弃,从头再来;悲观锁持悲观态度,倾向于认为数据一定会被修改,那干脆直接加锁得了。
除此之外还有:使用原子类(atomic concurrent classes),实现并发锁,使用 volatile 关键字保证内存可见性和禁止指令重排和用于多线程环境下的单次操作(单次读或者单次写)。
数组 | 链表 |
---|---|
随机访问速度比较快,增删慢 | 随机访问比较慢,增删比较快 |
(静态)数组从栈中分配空间,对于程序员方便快捷,但是自由度小 | 链表从堆中分配空间,自由度大 |
数组必须事先定义固定的长度(元素个数),不能适应数据动态地增减的情况 | 链表动态地进行存储分配,可以适应数据动态地增减的情况 |
在内存中必须连续 | 在内存中不需要连续,可以存在任何地方 |
插入、删除O(n) | 插入、删除O(1) |
读取O(1) | 读取O(n) |
补充:除了访问、插入、删除的不同外,还有在操作系统内存管理方面也有不同。正因为数组与链表的物理存储结构不同,在内存预读方面,内存管理会将连续的存储空间提前读入缓存(局部性原理),所以数组往往会被都读入到缓存中,这样进一步提高了访问的效率,而链表由于在内存中分布是分散的,往往不会都读入到缓存中,这样本来访问效率就低,这样效率反而更低了。在实际应用中,因为链表带来的动态扩容的便利性,在做为算法的容器方面,用的更普遍一点。
哈希表简单来说可以看作是对数组的升级,哈希表和数组的联系和区别:
堆是基于树抽象数据类型的一种特殊的数据结构,用于许多算法和数据结构中。如果父节点大于子节点,那么就称为最大堆,如果父节点小于子节点,则称为最小堆。
管道队列:管道就是内核里面的一串缓存,通信方式是单向的、低效的,不适合进程间频繁的交换数据,缓存保存在内核内存中。管道又分匿名管道和命名管道 --字节流数据
对于匿名管道,它的通信范围是存在父子关系的进程;对于命名管道,它可以在不相关的进程间也能相互通信。
在 shell ⾥⾯执⾏ A | B 命令的时候,A 进程和 B 进程都是 shell 创建出来的⼦进程,A 和 B 之间不存在
⽗⼦关系,它俩的⽗进程都是 shell。所以说,在 shell ⾥通过「 | 」匿名管道将多个命令连接在⼀起,实际上也就是创建了多个⼦进程,那么在我们编写 shell 脚本时,能使⽤⼀个管道搞定的事情,就不要使用多个管道,这样可以减少创建⼦进程的系统开销。
消息队列:消息队列是保存在内核中的消息链表,消息不及时,有大小限制,不适合比较大数据传输,消息队列通信过程中,存在用户态与内核态之间的数据拷贝开销,消息链表保存在内核中 --数据单元即消息体,都是固定大小的存储快,如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。
共享内存:拿出一块虚拟地址空间来,映射到相同的物理内存中,同时修改一个共享内存可能引起冲突。
信号量:保护机制,是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据,PV操作。
信号:信号量方式的进程间通信,是常规状态下的工作模式;
异常情况下的工作模式,需要信号的方式来通知进程,信号是进程间通信机制中唯一的异步通信机制,因为可以在任何时候发送信号给某一进程,一旦有信号产生就有下面这几种,用户进程对信号的处理方式。
Socket:管道、队列、共享内存、信号量、信号都是在同一台主机上进行进程间通信,Socket通信可以跨网络与不同主机上的进程之间通信(同主机也可以通信)
当进程访问的虚拟地址在⻚表中查不到时,系统会产⽣⼀个缺⻚异常。
在MySQL5.0之后,支持的存储引擎有十多个,但是我们常用的就那么几种,而且,默认支持的也是 InnoDB。
功能 | MyISAM | Memory | InnoDB |
---|---|---|---|
存储限制 | 256TB | RAM | 64TB |
支持事务 | No | No | Yes |
支持全文索引 | Yes | No | No |
支持 B 树索引 | Yes | Yes | Yes |
支持哈希索引 | No | Yes | No |
支持集群索引 | No | No | Yes |
支持数据索引 | No | Yes | Yes |
支持数据压缩 | Yes | No | No |
空间使用率 | 低 | N/A | 高 |
支持外键 | No | No | Yes |
InnoDB是默认的数据库存储引擎,主要特点有:
可以自动增长列,方法是:auto_increament
支持事务。默认的事务隔离级别是可重复读,通过MVCC(并发版本控制)来实现。互联网公司为了追求更高的并发设置的隔离级别主要为:Read Commit。像Repeatable Read 隔离级别有可能因为“间隙锁”导致死锁问题。
使用的锁粒度为行级锁,可以支持更高的并发。
支持外键约束,外键约束其实降低了表的查询速度,但是增加了表之间的耦合度。
配合一些热备工具可以支持在线热备份。
在InnoDB中存在着缓冲池,将索引和数据全部缓存起来,加快查询的速度。
对于InnoDB类型的表,其数据的物理组织形式是聚簇表。所有的数据按照逐渐来组织,数据和索引放在一块,都位于B+树的叶子节点上。
当然,InnoDB 的存储表和索引也有下面两种形式:
(1)使用共享表空间存储:所有的表和索引存放在同一个表空间中。
(2)使用多表空间存储:表结构放在frm文件,数据和索引放在IBD文件中。分区表的话,每个分区对应单独的IBD文件。使用分区表的好处在于提升查询效率。
对于InnoDB来说,最大的特点在于支持事务。但是这是以损失效率来换取的。
补充:
- DDL(Data Definition【释义、定义】 Language):数据定义语言
用来定义数据库对象:数据库,表,列等。关键字:create(创建), drop(删除),alter(修改) 等
- DML(Data Manipulation【操纵】 Language):数据操作语言
用来对数据库中表的数据进行增删改。关键字:insert(插入、增加), delete(删除), update(更新) 等
- DQL(Data Query【查询】 Language):数据查询语言
用来查询数据库中表的记录(数据)。关键字:select(查询), where 等
- DCL(Data Control Language):数据控制语言(了解)
用来定义数据库的访问权限和安全级别,及创建用户。关键字:GRANT, REVOKE 等
MyISAM,使用这个存储引擎,每个 MyISAM 在磁盘上存储形成3个文件:
frm 和 MYI 可以存放在不同的目录下。MYI 文件用来存储索引,但仅保存记录所在页的指针,索引的结构是B+树结构。下面这张图就是MYI文件保存的机制:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mP0qUcEq-1632587894661)(D:\Desktop\知识整理\图片\1254814-20200329145326651-2112224516.png)]
从这张图可以发现,这个存储引擎通过MYI的B+树结构来查找记录页,再根据记录页查找记录。并且支持全文索引、B树索引和数据压缩。支持数据的类型也有三种:
(1)静态固定长度表:这种方式的优点在于存储速度非常快,容易发生缓存,而且表发生损坏后也容易修复。缺点是占空间。这也是默认的存储格式。
(2)动态可变长表:优点是节省空间,但是一旦出错恢复起来比较麻烦。
(3)压缩表:上面说到支持数据压缩,说明肯定也支持这个格式。在数据文件发生错误时候,可以使用check table工具来检查,而且还可以使用repair table工具来恢复。
MyISAM有一个重要的特点那就是不支持事务,但是这也意味着他的存储速度更快,如果你的读写操作允许有错误数据的话,只是追求速度,可以选择这个存储引擎。
Memory,将数据存在内存,为了提高数据的访问速度,每一个表实际上和一个磁盘文件关联。文件是frm。
上文讨论过InnoDB的索引实现,InnoDB使用聚集索引,数据记录本身被存于主索引(一颗B+Tree)的叶子节点上。这就要求同一个叶子节点内(大小为一个内存页或磁盘页)的各条数据记录按主键顺序存放,因此每当有一条新的记录插入时,MySQL会根据其主键将其插入适当的节点和位置,如果页面达到装载因子(InnoDB默认为15/16),则开辟一个新的页(节点)。
如果表使用自增主键,那么每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,当一页写满,就会自动开辟一个新的页。这样就会形成一个紧凑的索引结构,近似顺序填满。由于每次插入时也不需要移动已有数据,因此效率很高,也不会增加很多开销在维护索引上。
B树(B-tree):B-树是一种平衡多路查找树,它在文件系统中很有用。
下图是一个M=4 阶的B树:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sqQ997eO-1632587894662)(D:\Desktop\知识整理\图片\290047064066682.png)]
特点:B树相对于平衡二叉树的不同是,每个节点包含的关键字增多了,特别是在B树应用到数据库中的时候,数据库充分利用了磁盘块的原理(磁盘数据存储是采用块的形式存储的,每个块的大小为4K,每次IO进行数据读取时,同一个磁盘块的数据可以一次性读取出来)把节点大小限制和充分使用在磁盘快大小范围;把树的节点关键字增多后树的层级比原来的二叉树少了,减少数据查找的次数和复杂度;
B+树:B+树是B树的一个升级版,相对于B树来说B+树更充分的利用了节点的空间,让查询速度更加稳定,其速度完全接近于二分法查找。
如下图,是一个B+树:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-csWdMeXI-1632587894665)(D:\Desktop\知识整理\图片\290050048129679.png)]
特点:
B树相对于B+树的优点是,如果经常访问的数据离根节点很近,而B树的非叶子节点本身存有关键字其数据的地址,所以这种数据检索的时候会要比B+树快。
B 树和B+树的区别图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4mxO7SWp-1632587894666)(D:\Desktop\知识整理\图片\290050088914733.png)]
总结:
事务的四个特性:ACID
事实上,原子性、隔离性、持久性都是为了保证一致性。
MySQL事务的原子性是通过undo log来实现的:Undo Log的原理很简单,为了满足事务的原子性,在操作任何数据之前,首先将数据备份到一个地方(这个存储数据备份的地方称为Undo Log)。然后进行数据的修改。如果出现了错误或者用户执行了ROLLBACK语句,系统可以利用Undo Log中的备份将数据恢复到事务开始之前的状态。
MySQL事务的持久性是通过redo log
来实现的。
MySQL的三种持久化方式:
参数 innodb_flush_log_at_trx_commit
,这个参数主要控制InnoDB将log buffer中的数据写入OS buffer,并刷到磁盘的时间点,取值分别为0,1,2,默认是1。这三个值的意思分别如下:
注意事项:
0:把数据先写入log buffer再写入OS buffer再写入磁盘,每秒写入磁盘一次,所以效率比1更高。
1:也就是每次commit后直接写入OS buffer,并且调用系统函数fsync()
把日志写到磁盘上。就保证数据一致性的角度来说,这种方式无疑是最安全的。但是我们都知道,安全大多数时候意味着效率偏低。每次提交都直接写入OS buffer并且写到磁盘,无疑会导致单位时间内IO的次数过多而效率低下。
2:每次commit写入OS buffer,再写入磁盘,少了一次数据拷贝的过程(从log buffer到OS buffer),所以比0 更加高效,每秒写入磁盘一次,所以效率比1更高。
数据库系统崩溃后重启,此时数据库处于不一致的状态,必须先执行一个crash recovery的过程:首先读取redo log,把成功提交但是还没来得及写入磁盘的数据重新写入磁盘,保证了持久性。再读取undo log将还没有成功提交的事务进行回滚,保证了原子性。crash recovery结束后,数据库恢复到一致性状态,可以继续被使用。
redo log
上图中的write pos
表示redo log当前记录的日志序列号LSN
(log sequence number),写入还未刷盘,循环往后递增;check point
表示redo log中的修改记录已刷入磁盘后的LSN,循环往后递增,这个LSN之前的数据已经全落盘。
write pos
到check point
之间的部分是redo log空余的部分(绿色),用来记录新的日志;check point
到write pos
之间是redo log已经记录的数据页修改数据,此时数据页还未刷回磁盘的部分。当write pos
追上check point
时,会先推动check point
向前移动,空出位置(刷盘)再记录新的日志。
注意:redo log日志满了,在擦除之前,需要确保这些要被擦除记录对应在内存中的数据页都已经刷到磁盘中了。擦除旧记录腾出新空间这段期间,是不能再接收新的更新请求的,此刻MySQL的性能会下降。所以在并发量大的情况下,合理调整redo log的文件大小非常重要。
因为redo log的存在使得Innodb
引擎具有了crash-safe
的能力,即MySQL宕机重启,系统会自动去检查redo log,将修改还未写入磁盘的数据从redo log恢复到MySQL中。
MySQL启动时,不管上次是正常关闭还是异常关闭,总是会进行恢复操作。会先检查数据页中的LSN
,如果这个 LSN 小于 redo log 中的LSN,即write pos
位置,说明在redo log
上记录着数据页上尚未完成的操作,接着就会从最近的一个check point
出发,开始同步数据。
简单理解,比如:redo log的LSN
是500,数据页的LSN
是300,表明重启前有部分数据未完全刷入到磁盘中,那么系统则将redo log中LSN
序号300到500的记录进行重放刷盘。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0SMyOGnC-1632587894667)(D:\Desktop\知识整理\图片\680f1897da4f4f109d8fec4f978d4de5~tplv-k3u1fbpfcp-zoom-1.image)]
磁盘中,那么系统则将redo log中LSN
序号300到500的记录进行重放刷盘。
在启动innodb的时候,不管上次是正常关闭还是异常关闭,总是会进行恢复操作。
因为redo log记录的是数据页的物理变化,因此恢复的时候速度比逻辑日志(如二进制日志)要快很多。而且,innodb自身也做了一定程度的优化,让恢复速度变得更快。
重启innodb时,checkpoint表示已经完整刷到磁盘上data page上的LSN,因此恢复时仅需要恢复从checkpoint开始的日志部分。例如,当数据库在上一次checkpoint的LSN为10000时宕机,且事务是已经提交过的状态。启动数据库时会检查磁盘中数据页的LSN,如果数据页的LSN小于日志中的LSN,则会从检查点开始恢复。
还有一种情况,在宕机前正处于checkpoint的刷盘过程,且数据页的刷盘进度超过了日志页的刷盘进度。这时候一宕机,数据页中记录的LSN就会大于日志页中的LSN,在重启的恢复过程中会检查到这一情况,这时超出日志进度的部分将不会重做,因为这本身就表示已经做过的事情,无需再重做。
另外,事务日志具有幂等性,所以多次操作得到同一结果的行为在日志中只记录一次。而二进制日志不具有幂等性,多次操作会全部记录下来,在恢复的时候会多次执行二进制日志中的记录,速度就慢得多。例如,某记录中id初始值为2,通过update将值设置为了3,后来又设置成了2,在事务日志中记录的将是无变化的页,根本无需恢复;而二进制会记录下两次update操作,恢复时也将执行这两次update操作,速度比事务日志恢复更慢。
undo log(回滚日志)
undo log
也是属于MySQL存储引擎InnoDB的事务日志。
undo log
属于逻辑日志,如其名主要起到回滚的作用,它是保证事务原子性的关键。记录的是数据修改前的状态,在数据修改的流程中,同时会记录一条与当前操作相反的逻辑日志到undo log
中。
我们举个栗子:假如更新ID=1记录的name字段,name原始数据为小富,现改name为程序员内点事
事务执行update X set name = 程序员内点事 where id =1
语句时,先会在undo log
中记录一条相反逻辑的update X set name = 小富 where id =1
记录,这样当某些原因导致服务异常事务失败,就可以借助undo log
将数据回滚到事务执行前的状态,保证事务的完整性。
那可能有人会问:同一个事物内的一条记录被多次修改,那是不是每次都要把数据修改前的状态都写入undo log
呢?
答案是不会的!
undo log
只负责记录事务开始前要修改数据的原始版本,当我们再次对这行数据进行修改,所产生的修改记录会写入到redo log
,undo log
负责完成回滚,redo log
负责完成前滚。
回滚
未提交的事务,即事务未执行commit
。但该事务内修改的脏页中,可能有一部分脏块已经刷盘。如果此时数据库实例宕机重启,就需要用回滚来将先前那部分已经刷盘的脏块从磁盘上撤销。
前滚
未完全提交的事务,即事务已经执行commit
,但该事务内修改的脏页中只有一部分数据被刷盘,另外一部分还在buffer pool
缓存上,如果此时数据库实例宕机重启,就需要用前滚来完成未完全提交的事务。将先前那部分由于宕机在内存上的未来得及刷盘数据,从redo log
中恢复出来并刷入磁盘。
数据库实例恢复时,先做前滚,后做回滚。
undo log
、redo log
、bin log
三种日志都是在刷脏页之前就已经刷到磁盘了的,相互协作最大限度保证了用户提交的数据不丢失。
bin log(归档日志)
bin log
是一种数据库Server层(和什么引擎无关),以二进制形式存储在磁盘中的逻辑日志。bin log
记录了数据库所有DDL
和DML
操作(不包含 SELECT
和 SHOW
等命令,因为这类操作对数据本身并没有修改)。
默认情况下,二进制日志功能是关闭的。可以通过以下命令查看二进制日志是否开启:
mysql> SHOW VARIABLES LIKE 'log_bin';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| log_bin | OFF |
+---------------+-------+
bin log
也被叫做归档日志
,因为它不会像redo log
那样循环写擦除之前的记录,而是会一直记录日志。一个bin log
日志文件默认最大容量1G
(也可以通过max_binlog_size
参数修改),单个日志超过最大值,则会新创建一个文件继续写。
mysql> show binary logs;
+-----------------+-----------+
| Log_name | File_size |
+-----------------+-----------+
| mysq-bin.000001 | 8687 |
| mysq-bin.000002 | 1445 |
| mysq-bin.000003 | 3966 |
| mysq-bin.000004 | 177 |
| mysq-bin.000005 | 6405 |
| mysq-bin.000006 | 177 |
| mysq-bin.000007 | 154 |
| mysq-bin.000008 | 154 |
bin log
日志的内容格式其实就是执行SQL命令的反向逻辑,这点和undo log
有点类似。一般来说开启bin log
都会给日志文件设置过期时间(expire_logs_days
参数,默认永久保存),要不然日志的体量会非常庞大。
mysql> show variables like 'expire_logs_days';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| expire_logs_days | 0 |
+------------------+-------+
1 row in set
mysql> SET GLOBAL expire_logs_days=30;
Query OK, 0 rows affected
bin log
主要应用于MySQL主从模式(master-slave
)中,主从节点间的数据同步;以及基于时间点的数据还原。
主从同步
通过下图MySQL的主从复制过程,来了解下bin log
在主从模式下的应用。
master
执行DDL
和DML
操作,修改记录顺序写入bin log
;slave
的I/O线程连接上Master,并请求读取指定位置position
的日志内容;Master
收到从库slave
请求后,将指定位置position
之后的日志内容,和主库bin log文件的名称以及在日志中的位置推送给从库;relay log
文件最末端,并将读取到的主库bin log文件名和位置position
记录到master-info
文件中,以便在下一次读取用;relay log
中内容更新后,读取日志并解析成可执行的SQL语句,这样就实现了主从库的数据一致;基于时间点还原
我们看到bin log
也可以做数据的恢复,而redo log
也可以,那它们有什么区别?
crash recovery
),保证MySQL宕机也不会影响持久性;bin log 用于时间点恢复(point-in-time recovery
),保证服务器可以基于时间点恢复数据和主从复制。Page
;bin log的内容是二进制,可以根据binlog_format
参数自行设置。bin log 与 redo log 功能并不冲突而是起到相辅相成的作用,需要二者同时记录,才能保证当数据库发生宕机重启时,数据不会丢失。
relay log(中继日志)
relay log
日志文件具有与bin log
日志文件相同的格式,从上边MySQL主从复制的流程可以看出,relay log
起到一个中转的作用,slave
先从主库master
读取二进制日志数据,写入从库本地,后续再异步由SQL线程
读取解析relay log
为对应的SQL命令执行。
slow query log
慢查询日志(slow query log
): 用来记录在 MySQL 中执行时间超过指定时间的查询语句,在 SQL 优化过程中会经常使用到。通过慢查询日志,我们可以查找出哪些查询语句的执行效率低,耗时严重。
出于性能方面的考虑,一般只有在排查慢SQL、调试参数时才会开启,默认情况下,慢查询日志功能是关闭的。可以通过以下命令查看是否开启慢查询日志:
mysql> SHOW VARIABLES LIKE 'slow_query%';
+---------------------+--------------------------------------------------------+
| Variable_name | Value |
+---------------------+--------------------------------------------------------+
| slow_query_log | OFF |
| slow_query_log_file | /usr/local/mysql/data/iZ2zebfzaequ90bdlz820sZ-slow.log |
+---------------------+--------------------------------------------------------+
通过如下命令开启慢查询日志后,发现 iZ2zebfzaequ90bdlz820sZ-slow.log
日志文件里并没有内容啊,可能因为执行的 SQL 都比较简单没有超过指定时间。
mysql> SET GLOBAL slow_query_log=ON;
Query OK, 0 rows affected
上边提到超过 指定时间
的查询语句才算是慢查询,那么这个时间阈值又是多少呐?我们通过 long_query_time
参数来查看一下,发现默认是 10 秒。
mysql> SHOW VARIABLES LIKE 'long_query_time';
+-----------------+-----------+
| Variable_name | Value |
+-----------------+-----------+
| long_query_time | 10.000000 |
+-----------------+-----------+
这里我们将 long_query_time
参数改小为 0.001秒再次执行查询SQL,看看慢查询日志里是否有变化。
mysql> SET GLOBAL long_query_time=0.001;
Query OK, 0 rows affected
果然再执行 SQL 的时,执行时间大于 0.001秒,发现慢查询日志开始记录了。
general query log
一般查询日志(general query log
):用来记录用户的所有操作,包括客户端何时连接了服务器、客户端发送的所有SQL
以及其他事件,比如 MySQL
服务启动和关闭等等。MySQL
服务器会按照它接收到语句的先后顺序写入日志文件。
由于一般查询日志记录的内容过于详细,开启后 Log 文件的体量会非常庞大,所以出于对性能的考虑,默认情况下,该日志功能是关闭的,通常会在排查故障需获得详细日志的时候才会临时开启。
我们可以通过以下命令查看一般查询日志是否开启,命令如下:
mysql> show variables like 'general_log';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| general_log | OFF |
+---------------+-------+
下边开启一般查询日志并查看日志存放的位置。
mysql> SET GLOBAL general_log=on;
Query OK, 0 rows affected
mysql> show variables like 'general_log_file';
+------------------+---------------------------------------------------+
| Variable_name | Value |
+------------------+---------------------------------------------------+
| general_log_file | /usr/local/mysql/data/iZ2zebfzaequ90bdlz820sZ.log |
+------------------+---------------------------------------------------+
执行一条查询 SQL 看看日志内容的变化。
mysql> select * from t_config;
+---------------------+------------+---------------------+---------------------+
| id | remark | create_time | last_modify_time |
+---------------------+------------+---------------------+---------------------+
| 1325741604307734530 | 我是广播表 | 2020-11-09 18:06:44 | 2020-11-09 18:06:44 |
+---------------------+------------+---------------------+---------------------+
我们看到日志内容详细的记录了所有执行的命令、SQL、SQL的解析过程、数据库设置等等。
error log
错误日志(error log
): 应该是 MySQL 中最好理解的一种日志,主要记录 MySQL 服务器每次启动和停止的时间以及诊断和出错信息。
默认情况下,该日志功能是开启的,通过如下命令查找错误日志文件的存放路径。
mysql> SHOW VARIABLES LIKE 'log_error';
+---------------+----------------------------------------------------------------+
| Variable_name | Value |
+---------------+----------------------------------------------------------------+
| log_error | /usr/local/mysql/data/LAPTOP-UHQ6V8KP.err |
+---------------+----------------------------------------------------------------+
注意:错误日志中记录的可并非全是错误信息,像 MySQL 如何启动 InnoDB
的表空间文件、如何初始化自己的存储引擎,初始化 buffer pool
等等,这些也记录在错误日志文件中。
时间 | 事务A | 事务B |
---|---|---|
T1 | 开启事务 | 开启事务 |
T2 | 查询账户余额:500元 | 查询账户余额:500元 |
T3 | 取走100元,剩余400元 | 取走100元,剩余400元 |
T4 | 提交事务,账户余额:400元 | - |
T5 | - | 提交事务,账户余额:400元 |
脏读:一个事务读到了另一个事务未提交的更新数据
幻读:一个事务读到了另一个事务已提交的新增数据
不可重复读:一个事务读到了另一个事务已提交的更新数据
四种隔离级别
各种隔离级别能解决的问题对应如下:
隔离级别 |
是否出现第一类丢失更新 |
是否出现脏读 |
是否出现不可重复读 |
是否出现幻读/虚读 |
是否出现第二类丢失更新 |
---|---|---|---|---|---|
Read Uncommited | 否 | 是 | 是 | 是 | 是 |
Read Commited | 否 | 否 | 是 | 是 | 是 |
Repeatable Read | 否 | 否 | 否 | 是 | 否 |
Serializable | 否 | 否 | 否 | 否 | 否 |
事务的隔离级别是通过 读写锁+MVCC(多版本并发控制) 实现的
实现方式就是事务开始时,第一条select语句查询结果集会生成一个快照(snapshot),并且这个事务结束前,同样的select语句返回的都是这个快照的结果,而不是最新的查询结果,这就是MySQL在Repeatable Read隔离级别对普通select语句使用的快照读(snapshot read)。
快照读和MVCC是什么关系呢?
MVCC是多版本并发控制,快照就是其中的一个版本。所以可以说MVCC实现了快照读,具体的实现方式涉及到MySQL的隐藏列。MySQL会给每个表自动创建三个隐藏列
由于undo log
中记录了各个版本的数据,并且通过DB_ROLL_PTR
可以找到各个历史版本,并且由DB_TRX_ID
决定使用哪个版本(快照)。所以相当于undo log
实现了MVCC,MVCC实现了快照读。MySQL对insert
、update
和delete
语句所使用的当前读(current read)。因为涉及到数据的修改,所以MySQL必须拿到最新的数据才能修改,所以涉及到数据的修改肯定不能使用快照读(snapshot read)。
那么在Repeatable Read
隔离级别是怎么解决幻读的呢?
是通过间隙锁(Gap Lock)来解决的。我们都知道InnoDB支持行锁,并且行锁是锁住索引。而间隙锁用来锁定索引记录间隙,确保索引记录的间隙不变。间隙锁是针对事务隔离级别为Repeatable Read或以上级别而生效的,间隙锁和行锁一起组成了Next-Key Lock
。当InnoDB扫描索引记录的时候,会首先对索引记录加上行锁,再对索引记录两边的间隙加上间隙锁(Gap Lock)。加上间隙锁之后,其他事务就不能在这个间隙插入记录。这样就有效的防止了幻读的发生。
加锁规则:两个”原则“、两个”优化“ 和一个”bug“
Redis是由C语言开发的一个开源的(遵从BSD协议)高性能键值对(key-value)的内存数据库,可以用作数据库、缓存、消息中间件等。它是一种Nosql(not-only-sql,泛指非关系型数据库)的数据库。Redis作为一个内存数据库的特点:
速度快,完全基于内存,使用C语言实现,网络层使用epoll解决高并发问题,单线程模型避免了不必要的上下文切换及竞争条件。
基础数据结构如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2wnaVYYS-1632587894672)(D:\Desktop\知识整理\图片\image-20210722164747573.png)]
String(字符串)
在Redis
中String
是可以修改的,称为动态字符串
(Simple Dynamic String
简称 SDS
),说是字符串但它的内部结构更像是一个 ArrayList
,内部维护着一个字节数组,并且在其内部预分配了一定的空间,以减少内存的频繁分配。
Redis
的内存分配机制是这样:
这样既保证了内存空间够用,还不至于造成内存的浪费,字符串最大长度为 512MB
.。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n5e5SuhF-1632587894673)(D:\Desktop\知识整理\图片\image-20210722164822608.png)]
上图就是字符串的基本结构,其中 buf
里面保存的是字符串内容,0x\0
作为结束字符不会被计算len
中。
字符串的数据结构:
/*
* 保存字符串对象的结构
*/
struct sdshdr {
int len; // buf 中已占用空间的长度
int free; // buf 中剩余可用空间的长度
char buf[]; // 数据空间
};
字符串(String)常用的命令:
set [key] [value] //给指定key设置值(set 可覆盖老的值)
get [key] //获取指定key 的值
del [key] //删除指定key
exists [key] //判断是否存在指定key
mset [key1] [value1] [key2] [value2] ...... //批量存键值对
mget [key1] [key2] ...... //批量取key
expire [key] [time] //给指定key 设置过期时间 单位秒
setex [key] [time] [value] //等价于 set + expire 命令组合
setnx [key] [value] //如果key不存在则set 创建,否则返回0
incr [key] //如果value为整数 可用 incr命令每次自增1
incrby [key] [number] //使用incrby命令对整数值 进行增加 number
Redis
中的list
和Java
中的LinkedList
很像,底层都是一种链表结构, list
的插入和删除操作非常快,时间复杂度为 0(1),不像数组结构插入、删除操作需要移动数据。
像归像,但是redis
中的list
底层可不是一个双向链表那么简单。
当数据量较少的时候它的底层存储结构为一块连续内存,称之为ziplist(压缩列表)
,它将所有的元素紧挨着一起存储,分配的是一块连续的内存;当数据量较多的时候将会变成quicklist(快速链表)
结构。
可单纯的链表也是有缺陷的,链表的前后指针 prev
和 next
会占用较多的内存,会比较浪费空间,而且会加重内存的碎片化。在redis 3.2之后就都改用ziplist+链表
的混合结构,称之为 quicklist(快速链表)
。
下面具体介绍下两种链表:
ziplist(压缩列表),先看一下ziplist
的数据结构:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DRdjK8Et-1632587894673)(D:\Desktop\知识整理\图片\image-20210722164922453.png)]
struct ziplist<T>{
int32 zlbytes; //压缩列表占用字节数
int32 zltail_offset; //最后一个元素距离起始位置的偏移量,用于快速定位到最后一个节点
int16 zllength; //元素个数
T[] entries; //元素内容
int8 zlend; //结束位 0xFF
}
压缩列表为了支持双向遍历,所以才会有 ztail_offset
这个字段,用来快速定位到最后一
个元素,然后倒着遍历
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bxmPadpz-1632587894674)(D:\Desktop\知识整理\图片\aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8yMTEwNTgwNi01NjAyMDNkYmNmNTNjMmIwLnBuZw)]
entry
的数据结构:
struct entry{
int<var> prevlen; //前一个 entry 的长度
int<var> encoding; //元素类型编码
optional byte[] content; //元素内容
}
entry
它的 prevlen
字段表示前一个 entry
的字节长度,当压缩列表倒着遍历时,需要通过这个字段来快速定位到下一个元素的位置。
quicklist
后续版本对列表数据结构进行了改造,使用 quicklist 代替了 ziplist 和 linkedlist。
quicklist 是 ziplist 和 linkedlist 的混合体,它将 linkedlist 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串接起来。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5CW0Rfc7-1632587894675)(D:\Desktop\知识整理\图片\image-20210722164956444.png)]
skipList 跳跃表
sorted set 类型的排序功能便是通过「跳跃列表」数据结构来实现。
跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
跳表在链表的基础上,增加了多层级索引,通过索引位置的几个跳转,实现数据的快速定位,如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QwjYRSnd-1632587894675)(D:\Desktop\知识整理\图片\image-20210722165014765.png)]
整数数组(intset)
当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis 就会使用整数集合作为集合键的底层实现,节省内存。
应用场景:
由于list它是一个按照插入顺序排序的列表,所以应用场景相对还较多的,例如:
lpop
和rpush
(或者反过来,lpush
和rpop
)能实现队列的功能;lpush
命令和lrange
命令能实现最新列表的功能,每次通过lpush
命令往列表里插入新的元素,然后通过lrange
命令读取最新的元素列表。list操作的常用命名:
rpush [key] [value1] [value2] ...... //链表右侧插入
rpop [key] //移除右侧列表头元素,并返回该元素
lpop [key] //移除左侧列表头元素,并返回该元素
llen [key] //返回该列表的元素个数
lrem [key] [count] [value] //删除列表中与value相等的元素,count是删除的个数。 count>0 表示从左侧开始查找,删除count个元素,count<0 表示从右侧开始查找,删除count个相同元素,count=0 表示删除全部相同的元素
//(PS: index 代表元素下标,index 可以为负数, index= 表示倒数第一个元素,同理 index=-2 表示倒数第二 个元素。)
lindex [key] [index] //获取list指定下标的元素 (需要遍历,时间复杂度为O(n))
lrange [key] [start_index] [end_index] //获取list 区间内的所有元素 (时间复杂度为 O(n))
ltrim [key] [start_index] [end_index] //保留区间内的元素,其他元素删除(时间复杂度为 O(n))
Redis
中的 Hash
和 Java的 HashMap
更加相似,都是数组+链表
的结构,当发生 hash 碰撞时将会把元素追加到链表上,值得注意的是在 Redis
的 Hash
中 value
只能是字符串.
hset books java "Effective java" (integer) 1
hset books golang "concurrency in go" (integer) 1
hget books java "Effective java"
hset user age 17 (integer) 1
hincrby user age 1 //单个 key 可以进行计数 和 incr 命令基本一致 (integer) 18
Hash 和String都可以用来存储用户信息 ,但不同的是Hash可以对用户信息的每个字段单独存储;String存的是用户全部信息经过序列化后的字符串,如果想要修改某个用户字段必须将用户信息字符串全部查询出来,解析成相应的用户信息对象,修改完后在序列化成字符串存入。而 hash可以只对某个字段修改,从而节约网络流量,不过hash内存占用要大于 String,这是 hash 的缺点。
应用场景:
hset [key] [field] [value]
命令, 可以实现以用户Id
,商品Id
为field
,商品数量为value
,恰好构成了购物车的3个要素。hash
类型的(key, field, value)
的结构与对象的(对象id, 属性, 值)
的结构相似,也可以用来存储对象。hash常用的操作命令:
hset [key] [field] [value] //新建字段信息
hget [key] [field] //获取字段信息
hdel [key] [field] //删除字段
hlen [key] //保存的字段个数
hgetall [key] //获取指定key 字典里的所有字段和值 (字段信息过多,会导致慢查询 慎用:亲身经历 曾经用过这个这个指令导致线上服务故障)
hmset [key] [field1] [value1] [field2] [value2] ...... //批量创建
hincr [key] [field] //对字段值自增
hincrby [key] [field] [number] //对字段值增加number
Redis
中的 set
和Java
中的HashSet
有些类似,它内部的键值对是无序的、唯一 的。它的内部实现相当于一个特殊的字典,字典中所有的value都是一个值 NULL。当集合中最后一个元素被移除之后,数据结构被自动删除,内存被回收。
应用场景:
好友、关注、粉丝、感兴趣的人集合:
sinter
命令可以获得A和B两个用户的共同好友;sismember
命令可以判断A是否是B的好友;scard
命令可以获取好友数量;smove
命令可以将B从A的粉丝集合转移到A的好友集合首页展示随机:美团首页有很多推荐商家,但是并不能全部展示,set类型适合存放所有需要展示的内容,而srandmember
命令则可以从中随机获取几个。
存储某活动中中奖的用户ID ,因为有去重功能,可以保证同一个用户不会中奖两次。
set的常用命令:
sadd [key] [value] //向指定key的set中添加元素
smembers [key] //获取指定key 集合中的所有元素
sismember [key] [value] //判断集合中是否存在某个value
scard [key] //获取集合的长度
spop [key] //弹出一个元素
srem [key] [value] //删除指定元素
zset
也叫SortedSet
一方面它是个 set
,保证了内部 value 的唯一性,另方面它可以给每个 value 赋予一个score
,代表这个value的排序权重。它的内部实现用的是一种叫作“跳跃列表
”的数据结构。
应用场景:
zset
可以用做排行榜,但是和list
不同的是zset
它能够实现动态的排序,例如: 可以用来存储粉丝列表,value 值是粉丝的用户 ID,score 是关注时间,我们可以对粉丝列表按关注时间进行排序。zset
还可以用来存储学生的成绩, value
值是学生的 ID, score
是他的考试成绩。 我们对成绩按分数进行排序就可以得到他的名次。zset有序集合的常用操作命令:
zadd [key] [score] [value] //向指定key的集合中增加元素
zrange [key] [start_index] [end_index] //获取下标范围内的元素列表,按score 排序输出
zrevrange [key] [start_index] [end_index] //获取范围内的元素列表 ,按score排序 逆序输出
zcard [key] //获取集合列表的元素个数
zrank [key] [value] //获取元素再集合中的排名
zrangebyscore [key] [score1] [score2] //输出score范围内的元素列表
zrem [key] [value] //删除元素
zscore [key] [value] //获取元素的score
策略 | 描述 |
---|---|
volatile-LRU | 从已设置过期时间的KV集中优先对最近最少使用(less recently used)的数据淘汰。把 Redis 既当缓存,又做持久化的时候使用这种策略。 |
volatile-ttl | 从已设置过期时间的KV集中优先对剩余时间短(time to live)的数据淘汰 |
volatile-random | 从已设置过期时间的KV集中随机选择数据淘汰 |
volatile-LFU | 从已设置过期时间的KV集中优先对最不经常使用(Least Frequently Used)的数据淘汰 |
allkeys-LRU | 从所有KV集中优先对最近最少使用(less recently used)的数据淘汰。只把 Redis 既当缓存是使用这种策略。(推荐) |
allkeys-random | 从所有KV集中随机选择数据淘汰。(应该没人用吧) |
noeviction | 不淘汰策略,若超过最大内存,返回错误信息(使用这个策略,疯了吧) |
allkeys-LFU | 从所有KV集中预先对最不经常使用(Least Frequently Used)的数据淘汰 |
Redis采用的是定期删除+惰性删除策略。
定期删除策略
Redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,默认每 100ms 进行一次过期扫描:
为什么不扫描所有的key?
Redis 是单线程,全部扫描岂不是卡死了。而且为了防止每次扫描过期的 key 比例都超过 1/4,导致不停循环卡死线程,Redis 为每次扫描添加了上限时间,默认是 25ms。
**从库的过期策略:**从库不会进行过期扫描,从库对过期的处理是被动的。主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key。因为指令同步是异步进行的,所以主库过期的 key 的 del 指令没有及时同步到从库的话,会出现主从数据的不一致,主库没有的数据在从库里还存在。
懒惰删除策略,Redis 为什么要懒惰删除(lazy free)?
删除指令 del 会直接释放对象的内存,大部分情况下,这个指令非常快,没有明显延迟。不过如果删除的 key 是一个非常大的对象,比如一个包含了千万元素的 hash,又或者在使用 FLUSHDB 和 FLUSHALL 删除包含大量键的数据库时,那么删除操作就会导致单线程卡顿。redis 4.0 引入了 lazyfree 的机制,它可以将删除键或数据库的操作放在后台线程里执行, 从而尽可能地避免服务器阻塞。
微服务架构是一种架构模式或者说是一种架构风格,它提倡将单一应用程序划分成一组小的服务,每个服务运行在其独立的自己的进程中,服务之间互相协调、互相配合,为用户提供最终价值。 服务之间采用轻量级的通信机制互相沟通(通常是基于HTTP的RESTful API)。每个服务都围绕着具体业务进行构建,并且能够被独立地部署到生产环境、类生产环境等。另外,应尽量避免统一的、集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言、工具对其进行构建,可以有一个非常轻量级的集中式管理来协调这些服务,可以使用不同的语言来编写服务,也可以使用不同的数据存储。
从技术维度来说:
微服务化的核心就是将传统的一站式应用,根据业务拆分成一个一个的服务,彻底地去耦合,每一个微服务提供单个业务功能的服务,一个服务做一件事,从技术角度看就是一种小而独立的处理过程,类似进程概念,能够自行单独启动或销毁,拥有自己独立的数据库。
与树有关的术语:
节点深度:对任意节点x,x节点的深度表示为根节点到x节点的路径长度。所以根节点深度为0,第二层节点深度为1,以此类推
节点高度:对任意节点x,叶子节点到x节点的路径长度就是节点x的高度
树的深度:一棵树中节点的最大深度就是树的深度,也称为高度
父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点
子节点:一个节点含有的子树的根节点称为该节点的子节点
节点的层次:从根节点开始,根节点为第一层,根的子节点为第二层,以此类推
兄弟节点:拥有共同父节点的节点互称为兄弟节点
节点的度:节点的子树数目就是节点的度(degree)
叶子节点:度为零的节点就是叶子节点
祖先:对任意节点x,从根节点到节点x的所有节点都是x的祖先(节点x也是自己的祖先)
后代:对任意节点x,从节点x到叶子节点的所有节点都是x的后代(节点x也是自己的后代)
森林:m颗互不相交的树构成的集合就是森林
注:其实对于祖先和后代的定义,不同的资料有不同的解释,争论在于节点本身是否是本身的祖先或者后代,我这里的定义取得是《数据结构与算法( Java 描述)-邓俊辉》。维基百科中对于祖先和后代的定义是:
Descendant:A node reachable by repeated proceeding from parent to child.
Ancestor:A node reachable by repeated proceeding from child to parent.
树的种类:
无序树: 树的任意节点的子节点没有顺序关系
有序树:树的任意节点的子节点有顺序关系
二叉树:树的任意节点至多包含两棵子树
满二叉树:叶子节点都在同一层并且除叶子节点外的所有节点都有两个子节点
完全二叉树:对于一颗二叉树,假设其深度为d(d>1)。除第d层外的所有节点构成满二叉树,且第d层所有节点从左向右连续地紧密排列,这样的二叉树被称为完全二叉树
平衡二叉树(AVL):它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树,同时,平衡二叉树必定是二叉搜索树
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d3jrK488-1632587894676)(D:\Desktop\知识整理\图片\v2-28e39093993f673de576f57ea614d604_720w.jpg)]
排序二叉树(二叉搜索树、二叉查找树、BST):是一种特殊结构的二叉树,通过它可以非常方便底对树中的所有节点进行排序和检索,排序二叉树要么是一棵空二叉树,要么是具有下列性质的二叉树
任意节点的没有键值相等的节点。
哈夫曼树:带权路径最短的二叉树称为哈夫曼树或最优二叉树。哈夫曼树是二叉树的一种应用,在信息检索中很常用。在构建哈弗曼树时,要使树的带权路径长度最小,只需要遵循一个原则,那就是:权重越大的结点离树根越近。在图中,因为结点 a 的权值最大,所以理应直接作为根结点的孩子结点。
介绍下一些相关概念:
树的带权路径长度为树中所有叶子结点的带权路径长度之和。通常记作 “WPL” 。例如图 1 中所示的这颗树的带权路径长度为:
WPL = 7 * 1 + 5 * 2 + 2 * 3 + 4 * 3
红黑树:虽然排序二叉树可以快速检索,但在最坏的情况下,如果插入的节点集本身就是有序的,要么是由小到大排列,要么是由大到小排列,那么最后得到的排序二叉树将变成链表:所有节点只有左节点或者所有得节点只有右节点,此时得检索效率就会很低。为了改变排序二叉树存在得不足,Rudolf Bayerr于1972年发明了另一种改进后的排序二叉树------红黑树,红黑树是一颗特殊的排序二叉树,红黑树在原有的排序二叉树上增加了如下几个要求:
根据“性质5”,红黑树从根节点到每个叶子节点的路径都包含相同数量的黑色节点,因此从根节点到叶子节点的路径中包含的黑色节点数被称为树的“黑色高度(black-height)”。
“性质4”则保证了从根节点到叶子节点的最长路径的长度不会超过任何其他路径的2倍。假如有一棵黑色高度为3的红黑树,从根节点到叶子节点的最短路径长度是2,该路径上全是黑色节点(黑色节点----黑色节点----黑色节点)。最长路径也只可能为4,在每个黑色节点之间插入一 个红色节点(黑色节点----红色节点----黑色节点----红色节点----黑色节点), “性质4”保证绝不可能插入更多的红色节点。由此可见,红黑树中最长的路径就是一条红黑交 替的路径。
由此可以得出结论:对于给定的黑色高度为N的红黑树,从根到叶子节点的最短路径长度为N-1, 最长路径长度为2*(N- 1)。
红黑树通过上面这种限制来保证它大致是平衡的----因为红黑树的高度不会无限增高,这样能保证红黑树在最坏的情况下都是高效的,不会出现普通排序二叉树的情况。
由于红黑树只是一棵特殊的排序二叉树,因此对红黑树上的只读操作与普通排序二叉树上的只读操作完全相同,只是红黑树保证了大致平衡,因此检索性能更好。
但在红黑树进行插入操作和删除操作会导致树不在符合红黑树的特征,一次插入操作和删除操作都需要进行一定的维护,以保证插入节点、删除节点后的树依然是红黑树。
Kafka学习之Kafka选举机制简述
Kafka是一个高性能,高容错,多副本,可复制的分布式消息系统。在整个系统中,涉及到多处选举机制,被不少人搞混,这里总结一下,本篇文章大概会从三个方面来讲解。
1、控制器(Broker)选举
所谓控制器就是一个Borker,在一个kafka集群中,有多个broker节点,但是它们之间需要选举出一个leader,其他的broker充当follower角色。集群中第一个启动的broker会通过在zookeeper中创建临时节点/controller
来让自己成为控制器,其他broker启动时也会在zookeeper中创建临时节点,但是发现节点已经存在,所以它们会收到一个异常,意识到控制器已经存在,那么就会在zookeeper中创建watch
对象,便于它们收到控制器变更的通知。
那么如果控制器由于网络原因与zookeeper断开连接或者异常退出,那么其他broker通过watch收到控制器变更的通知,就会去尝试创建临时节点/controller
,如果有一个broker创建成功,那么其他broker就会收到创建异常通知,也就意味着集群中已经有了控制器,其他broker只需创建watch
对象即可。
如果集群中有一个broker发生异常退出了,那么控制器就会检查这个broker是否有分区的副本leader,如果有那么这个分区就需要一个新的leader,此时控制器就会去遍历其他副本,决定哪一个成为新的leader,同时更新分区的ISR
集合。
如果有一个broker加入集群中,那么控制器就会通过Broker ID去判断新加入的broker中是否含有现有分区的副本,如果有,就会从分区副本中去同步数据。
集群中每选举一次控制器,就会通过zookeeper创建一个controller epoch
,每一个选举都会创建一个更大,包含最新信息的epoch
,如果有broker收到比这个epoch
旧的数据,就会忽略它们,kafka也通过这个epoch
来防止集群产生“脑裂”。
2、分区副本选举机制
在kafka的集群中,会存在着多个主题topic,在每一个topic中,又被划分为多个partition,为了防止数据不丢失,每一个partition又有多个副本,在整个集群中,总共有三种副本角色:
follower需要从leader中同步数据,但是由于网络或者其他原因,导致数据阻塞,出现不一致的情况,为了避免这种情况,follower会向leader发送请求信息,这些请求信息中包含了follower需要数据的偏移量offset,而且这些offset是有序的。
如果有follower向leader发送了请求1,接着发送请求2,请求3,那么再发送请求4,这时就意味着follower已经同步了前三条数据,否则不会发送请求4。leader通过跟踪 每一个follower的offset来判断它们的复制进度。
默认的,如果follower与leader之间超过10s内没有发送请求,或者说没有收到请求数据,此时该follower就会被认为“不同步副本”。而持续请求的副本就是“同步副本”,当leader发生故障时,只有“同步副本”才可以被选举为leader。其中的请求超时时间可以通过参数replica.lag.time.max.ms
参数来配置。
我们希望每个分区的leader可以分布到不同的broker中,尽可能的达到负载均衡,所以会有一个优先leader,如果我们设置参数auto.leader.rebalance.enable
为true,那么它会检查优先leader是否是真正的leader,如果不是,则会触发选举,让优先leader成为leader。
在kafka的消费端,会有一个消费者协调器以及消费组,组协调器GroupCoordinator
需要为消费组内的消费者选举出一个消费组的leader,那么如何选举的呢?
如果消费组内还没有leader,那么第一个加入消费组的消费者即为消费组的leader,如果某一个时刻leader消费者由于某些原因退出了消费组,那么就会重新选举leader,如何选举?
private val members = new mutable.HashMap[String, MemberMetadata]
leaderId = members.keys.headOption
上面代码是kafka源码中的部分代码,member是一个hashmap的数据结构,key为消费者的member_id
,value是元数据信息,那么它会将leaderId选举为Hashmap中的第一个键值对,它和随机基本没啥区别。
异步,非阻塞,使用了epoll 和大量的底层代码优化。
如果一个server采用一个进程负责一个request的方式,那么进程数就是并发数。正常情况下,会有很多进程一直在等待中。
而nginx采用一个master进程,多个woker进程的模式。
Nginx 的异步非阻塞工作方式正把当中的等待时间利用起来了。在需要等待的时候,这些进程就空闲出来待命了,因此表现为少数几个进程就解决了大量的并发问题。
每进来一个request,会有一个worker进程去处理。但不是全程的处理,处理到什么程度呢?处理到可能发生阻塞的地方,比如向上游(后端)服务器转发request,并等待请求返回。那么,这个处理的worker很聪明,他会在发送完请求后,注册一个事件:“如果upstream返回了,告诉我一声,我再接着干”。于是他就休息去了。此时,如果再有request 进来,他就可以很快再按这种方式处理。而一旦上游服务器返回了,就会触发这个事件,worker才会来接手,这个request才会接着往下走。
为什么不采用多线程模型管理连接?
为什么不采用多线程处理逻辑业务?
并且,如果进程中有阻塞的处理逻辑,应该由各个业务进行解决,比如OpenResty中利用了lua协程,对阻塞业务进行了优化。
Nginx: 采用单线程来异步非阻塞处理请求(管理员可以配置Nginx主进程的工作进程的数量)(epoll),不会为每个请求分配cpu和内存资源,节省了大量资源,同时也减少了大量的CPU的上下文切换。所以才使得Nginx支持更高的并发。
RST位为1时,表示TCP连接中出现异常必须强制断开连接。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UxfIJ2LS-1632587894678)(D:\Desktop\知识整理\图片\image-20210830153355314.png)]
心跳机制:定时发送一个自定义的结构体(心跳包),让对方知道自己还活着,让对方知道自己还活着,以确保连接的有效性的机制。
心跳检测步骤:
网络中的接收和发送数据都是使用操作系统中的SOCKET进行实现。
心跳包的发送,通常有两种技术
两种地址的概念:
操作系统引⼊了虚拟内存,进程持有的虚拟地址会通过 CPU 芯⽚中的内存管理单元(MMU)的映射关系,来转换变成物理地址,然后再通过物理地址访问内存,如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-amaVgMrK-1632587894679)(D:\Desktop\知识整理\图片\image-20210830160742563.png)]
系统管理虚拟地址与物理地址的方式:内存分段 和 内存分页,分段是比较早提出的。
程序是由若⼲个逻辑分段组成的,如可由代码分段、数据分段、栈段、堆段组成。不同的段是有不同的属性的,所以就⽤分段(Segmentation)的形式把这些段分离出来。
分段机制下,虚拟地址和物理地址是如何映射的?
分段机制下的虚拟地址由两部分组成,段选择⼦和段内偏移量。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SieK2V04-1632587894679)(D:\Desktop\知识整理\图片\image-20210830161110102.png)]
有点:解决了程序本身不需要关系具体的物理内存的问题;
缺点:
解决外部内存碎⽚的问题就是内存交换。
可以把⾳乐程序占⽤的那 256MB 内存写到硬盘上,然后再从硬盘上读回来到内存⾥。不过再读回的时候,我们不能装载回原来的位置,⽽是紧紧跟着那已经被占⽤了的 512MB 内存后⾯。这样就能空缺出连续的 256MB 空间,于是新的 200MB 程序就可以装载进来。
这个内存交换空间,在 Linux 系统⾥,也就是我们常看到的 Swap 空间,这块空间是从硬盘划分出来的,⽤于内存与硬盘的空间交换。
分段的好处就是能产生连续的内存空间,但是会出现内存碎片和内存交换的空间太大的问题。
要解决这些问题,那么就要想出能少出现⼀些内存碎⽚的办法。另外,当需要进⾏内存交换的时候,让需要交换写⼊或者从磁盘装载的数据更少⼀点,这样就可以解决问题了。这个办法,也就是内存分⻚(Paging)。
分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小。这样⼀个连续并且尺⼨固定的内存空间,我们叫⻚(Page)。在 Linux 下,每⼀⻚的⼤⼩为 4KB 。
虚拟地址与物理地址之间通过⻚表来映射,如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ayAnfapZ-1632587894680)(D:\Desktop\知识整理\图片\image-20210830163425847.png)]
⻚表是存储在内存⾥的,内存管理单元 (MMU)就做将虚拟内存地址转换成物理地址的⼯作。
⽽当进程访问的虚拟地址在⻚表中查不到时,系统会产⽣⼀个缺⻚异常,进⼊系统内核空间分配物理内存、更新进程⻚表,最后再返回⽤户空间,恢复进程的运⾏。
分⻚是怎么解决分段的内存碎⽚、内存交换效率低的问题?
由于内存空间都是预先划分好的,也就不会像分段会产⽣间隙⾮常⼩的内存,这正是分段会产⽣内存碎⽚的原因。⽽采⽤了分⻚,那么释放的内存都是以⻚为单位释放的,也就不会产⽣⽆法给进程使⽤的⼩内存。
如果内存空间不够,操作系统会把其他正在运⾏的进程中的「最近没被使⽤」的内存⻚⾯给释放掉,也就是暂时写在硬盘上,称为换出(Swap Out)。⼀旦需要的时候,再加载进来,称为换⼊(Swap In)。所以,⼀次性写⼊磁盘的也只有少数的⼀个⻚或者⼏个⻚,不会花太多时间,内存交换的效率就相对⽐较⾼。
更进⼀步地,分⻚的⽅式使得我们在加载程序的时候,不再需要⼀次性都把程序加载到物理内存中。我们完全可以在进⾏虚拟内存和物理内存的⻚之间的映射之后,并不真的把⻚加载到物理内存⾥,⽽是只有在程序运⾏中,需要⽤到对应虚拟内存⻚⾥⾯的指令和数据时,再加载到物理内存⾥⾯去。
分⻚机制下,虚拟地址和物理地址是如何映射的?
在分⻚机制下,虚拟地址分为两部分,⻚号和⻚内偏移。⻚号作为⻚表的索引,⻚表包含物理⻚每⻚所在物理内存的基地址,这个基地址与⻚内偏移的组合就形成了物理内存地址,⻅下图。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LsVdAw3h-1632587894681)(D:\Desktop\知识整理\图片\image-20210830163828032.png)]
总结⼀下,对于⼀个内存地址转换,其实就是这样三个步骤:
为解决单级页表占用内存大问题,分页方式采用了⼀种叫作多级⻚表(Multi-Level Page Table)的解决⽅案。
Linux内存主要采用的是页式内存管理,但同时也不可避免的涉及了段机制。
通过表格来说明一下,如下:
引用类型 | 被垃圾回收时间 | 用途 | 生存时间 |
---|---|---|---|
强引用 | 从来不会 | 对象的一般状态 | JVM停止运行时终止 |
软引用 | 在内存不足时 | 对象缓存 | 内存不足时终止 |
弱引用 | 在垃圾回收时 | 对象缓存 | gc运行后终止 |
虚引用 | 任何时候 | 跟踪对象被垃圾回收器回收的活动 | Unknown |
RFU算法的精妙所在是使用一种叫作 哈希链表 的数据结构。
我们都知道,哈希表是由若干个Key-Value组成的。在“逻辑”上, 这些Key-Value是无所谓排列顺序的,谁先谁后都一样。
在哈希链表中,这些Key-Value不再是彼此无关的存在,而是被一 个链条串了起来。每一个Key-Value都具有它的前驱Key-Value、后继 Key-Value,就像双向链表中的节点一样。
让我们以用户信息的需求为例,来演示一下LRU算法的基本思路。
假设使用哈希链表来缓存用户信息,目前缓存了4个用户,这4个 用户是按照被访问的时间顺序依次从链表右端插入的。
如果这时业务方访问用户5,由于哈希链表中没有用户5的数据, 需要从数据库中读取出来,插入到缓存中。此时,链表最右端是最新被 访问的用户5,最左端是最近最少被访问的用户1。
接下来,如果业务方访问用户2,哈希链表中已经存在用户2的数 据,这时我们把用户2从它的前驱节点和后继节点之间移除,重新插入 链表的最右端。此时,链表的最右端变成了最新被访问的用户2,最左 端仍然是最近最少被访问的用户1。
接下来,如果业务方请求修改用户4的信息。同样的道理,我们 会把用户4从原来的位置移动到链表的最右侧,并把用户信息的值更 新。这时,链表的最右端是最新被访问的用户4,最左端仍然是最近最 少被访问的用户1。
后来业务方又要访问用户6,用户6在缓存里没有,需要插入哈希链表中。假设这时缓存容量已经达到上限必须先删除最近最少被访问 的数据,那么位于哈希链表最左端的用户1就会被删除,然后再把用户6 插入最右端的位置。
什么是go语言?
Go也称为Golang,是一种编程语言。作为一种开源编程语言,Go可以轻松构建可靠,简单和高效的软件。
Go是键入的静态编译语言。Go语言提供垃圾收集,CSP风格的并发性,内存安全性和结构类型。
什么是java?
Java是一种用于一般用途的计算机编程语言,它是基于类的,并发的和面向对象的。Java专门设计为包含很少的实现依赖项。Java应用程序在JVM(Java虚拟机)上运行。它是当今最着名和最着名的编程语言之一。
Java是一种用于为多个平台开发软件的编程语言。Java应用程序上的编译代码或字节码可以在大多数操作系统上运行,包括Linux,Mac操作系统和Linux。Java的大部分语法都源自C ++和C语言。
go语言和Java之间的区别
1、函数重载
Go上不允许函数重载,必须具有方法和函数的唯一名称。java允许函数重载。
2、速度
go的速度比java快
3、多态
Java默认允许多态。而,Go没有。
4、路由配置
Go语言使用HTTP协议进行路由配置;而
java使用Akka.routing.ConsistentHashingRouter和Akka.routing.ScatterGatherFirstCompletedRouter进行路由配置。
5、可扩展性
Go代码可以自动扩展到多个核心;而,Java并不总是具有足够的可扩展性。
6、继承
Go语言的继承通过匿名组合完成:基类以Struct的方式定义,子类只需要把基类作为成员放在子类的定义中,支持多继承。
二面主要问项目和业务,所以要对自己的项目非常熟悉。
面试官一上来就让讲一下项目,整体架构怎么样的,讲一下自己负责的最有挑战性的地方,有什么亮点。
之后就是让讲清楚业务细节,面临哪些问题,用什么方案解决的,为什么要这么做,为什么要选这个不要选那个。
等他理解你的业务之后,会出题:要实现某个功能,设计一下方案。
算法题
最长不重复子串
描述:
给定一个字符串,找出不含有重复字符的最长子串的长度。示例:
给定 “abcabcbb” ,没有重复字符的最长子串是 “abc” ,那么长度就是3。
给定 “bbbbb” ,最长的子串就是 “b” ,长度是1。
给定 “pwwkew” ,最长子串是 “wke” ,长度是3。请注意答案必须是一个子串,”pwke” 是 子序列 而不是子串。
public static String getMaxsubHuisu(String s) {
if (s == null || s.length() == 0) {
return null;
}
int start = 0;//滑动窗口的开始值
int maxlen = 0;
int len = 0;
int startMaxIndex = 0;//最长子串的开始值
Map<Character, Integer> map = new HashMap<>();//存储窗口内字符跟位置
int i;
for (i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
Integer value = map.get(ch);
if (map.containsKey(ch)) {//map中包含字符,则出现重复字符
start = value + 1;//下一次开始的位置是,存在map中重复字符的下一个位置
len = 0;//重新开始新的窗口,len置为0
map = new HashMap<>();//map置空
i=value;//下次从重复的值开始回溯
} else {
map.put(ch, i);//不存在重复的,就存入map
len++;//每次进来长度自增1
if (len > maxlen) {//如果当前的窗口长度>最长字符串则,更新最长串,跟最长子串开始位置
maxlen = len;
startMaxIndex = start;
}
}
}
return s.substring(startMaxIndex, (startMaxIndex + maxlen));//截取字符串,substring为左闭右开
}
static void swap(int[] array, int x, int y) {
int temp = array[x];
array[x] = array[y];
array[y] = temp;
}
public static void sort(int[] array, int len) {
int zero = 0, one = 0, two = len - 1;
while (array[zero] == 0) {
zero++;
}
while (array[two] == 2) {
two--;
}
one = zero;
while (one <= two) {
if (array[one] == 2) {
swap(array, one, two);
if (array[one] == 0) {
swap(array, zero, one);
zero++;
}
two--;
while (array[two] == 2) {
two--;
}
} else if (array[one] == 0) {
swap(array, one, zero);
zero++;
}
one++;
}
}
public static TreeNode getParent(TreeNode root, TreeNode node1,TreeNode node2) {
if(root == null || node1 == null || node2 == null) return null;
//这里可以换成if(root == node1 || root == node2),我只是为了方便测试才这样写
if(root.val == node1.val || root.val == node2.val) return root;
TreeNode left = getParent(root.left,node1,node2);
TreeNode right = getParent(root.right,node1,node2);
//如果左右子树都能找到,那么当前节点就是最近的公共祖先节点
if(left != null && right != null) return root;
//如果左子树上没有,那么返回右子树的查找结果
if(left == null) return right;
//否则返回左子树的查找结果
else return left;
}