2021-09-26

面试二问了整个项目的整体的流程,为什么选择某些技术或者基础组件

1.自我介绍

2.挑一个项目,画项目的架构图

Kafka发送数据之后立即返回还是等待ACK之后才返回?

Kafka工作流程:

    1. producter先从Zookeeper获取分区的leader
    1. producter将消息发送给leader
    1. Leader将消息写入本地文件
    1. followers从leader pull消息
    1. followers将消息写入本地后向leader发送ACK
    1. leader收到所有副本的ACK后向producer发送ACK(或者leader自己收到消息后就发送ACK)。ACK有三个参数配置:参数是0,1,-1.
    • 0: producter不等待broker的ACK,这种操作提供了最低的延迟,broker还没有写入磁盘就已经返回了,当broker故障的时候丢失数据(相当于异步发送)
    • 1: producter等待broker的ACK,partition的leader落盘成功后返回ACK,如果follower同步数据之前leader故障,此时会丢失数据。此时follower需要同步leader中的数据,但是leader宕机了,挂了之后Kafka集群会重新选举leader,选举出leader之后,并没有同步到原有的数据,就会造成数据的丢失
    • -1或all: prodecter等待broker的ACK和partition的leader和follower全部落盘成功后,才会返回ACK,但是如果follower同步完成之后,在broker发送ACK之前,leader发生故障,那么会出现数据的重复,但不会造成数据丢失

有了解过网络攻击吗?黑客频繁发起二次握手导致连接占满,如何解决恶意二次握手?如果黑客二次握手后也不下线,通过 tcp_syncookies 发送请求时,也回应,怎么办?

SYN攻击:TCP连接建立是需要三次握手,假设攻击者短时间伪造不同 IP 地址的 SYN 报文,服务端每接收到一个 SYN 报文,就进入 SYN_RCVD 状态,但服务端发送出去的 ACK+SYN 报文,无法得到未知 IP 主机的 ACK 应答,久而久之就会占满服务端的 SYN 接收队列(未连接队列),使得服务器不能为正常用户服务。

避免 SYN 攻击方式:

方式一、增大半连接队列。通过修改Linux内核参数,控制队列大小和当队列满时应做什么处理。

  • 当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包。控制该队列的最大值如下参数:net.core.netdev_max_backlog
  • SYN_RCVD 状态连接的最大个数:net.ipv4.tcp_max_syn_backlog
  • 超出处理能力时,对新的SYN直接回RST,丢弃连接: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 (已完成连接建立)队列是如何工作的:

  • 当服务端接收到客户端的SYN报文时,会将其加⼊到内核的「 SYN 队列」;
  • 接着发送 SYN + ACK 给客户端,等待客户端回应 ACK 报⽂;
  • 服务端接收到 ACK 报⽂后,从「 SYN 队列」移除放⼊到「 Accept 队列」;
  • 应⽤通过调⽤ accpet() socket 接⼝,从「 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 队列」满之后,后续服务器收到 SYN 包,不进⼊「 SYN 队列」;
    • 计算出一个 cookie 值,再以SYN + ACK中的「序列号」返回客户端,
    • 服务端接收到服务端的应答报文时,服务器会检查这个ACK包的合法性。如果合法,直接放入到「 Accept队列」。
    • 最后应用通过调用accept() socket接口,从「 Accept 队列」取出的连接。

方式三:减少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请求会改变数据库的数据,中间人就可以利用此截获的报文,不断向服务器发送该报文,这样就会导致数据库的数据被中间人改变了,而客户是不知情的。

Https的核心是什么以及客户端与服务端如何交互的?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zYTshBBH-1632587894653)(D:\Desktop\知识整理\图片\image-20210825165554969.png)]

  • 信息加密:交互信息无法被窃取,采用混合加密(对称加密和非对称加密结合)
    • 在通信建⽴前采⽤⾮对称加密的⽅式交换「会话秘钥」,后续就不再使⽤⾮对称加密。
    • 在通信过程中全部使⽤对称加密的「会话秘钥」的⽅式加密明⽂数据。
  • 校验机制:无法篡改通信内容 ,采用摘要算法
  • 身份证书:证明网站的真实性,将服务器公钥放入到数字证书中(CA-数字证书认证机构)

Cookie,Session的区别?

Cookie和Session都是用来跟踪浏览器用户身份的会话技术,但两者有所区别:

  • Cookie数据保存在客户端,Session数据保存在服务端;
  • Cookie是客户端保持HTTP会话状态的技术;Session是服务端保持HTTP会话状态的技术;
  • Cookie不是很安全,别人可以分析存放在本地的Cookie并进行欺骗,考虑到安全应当使用Session;
  • Cookie有大小限制以及浏览器存Cookie的数量也有限制,Session没有大小限制和服务器的内存大小有关,Session保存在服务器端存在一段时间才会消失,如果Session过多会增加服务器的压力;
  • Cookie一般用来保存用户信息,Session的主要作用就是通过服务端记录用户的状态。
  • session 的运行依赖 session id,而 session id 是存在 cookie 中的,也就是说,如果浏览器禁用了 cookie ,同时 session 也会失效(但是可以通过其它方式实现,比如在 url 中传递 session_id);
  • session 默认被存储在服务器的一个文件里(不是内存);
  • session 可以放在文件、数据库、或内存中都可以;
  • 用户验证这种场合一般会用 session;

Session的SessonID存储到浏览器之后,关闭浏览器再启动还能拿到SessionID吗?

由于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

这样就可以避免增长过快导致网络拥塞,慢慢的增加调整到网络的最佳值。很明显,是一个线性上升的算法。

HTTP 无状态 无连接

无连接:是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。当请求时建立连接、请求完释放连接,以尽快将资源释放出来服务其他客户端,可以加KeepAlive弥补无连接的问题

无状态:是指协议对于事务处理没有记忆能力,服务器不知道客户端是什么状态。即我们给服务器发送 HTTP 请求之后,服务器根据请求,会给我们发送数据过来,但是,发送完,不会记录任何信息。 可以通过Cookie和Session来弥补这个问题。

volatile 内存屏障

CPU有可能会把相应代码的CPU指令进行一次重排序,这虽然能提高运行的效率,但是也可能会影响一些数据的可见性,而volatile通过 内存屏障 这个指令,来保证了相应代码块的执行顺序。内存屏障还会强制更新一次CPU缓存。加载最新的内容。

TCP和UDP的不同?

  • 连接
    • TCP是面向连接的传输层协议,传输数据前先要建立连接。
    • UDP是不需要连接的,即刻传输数据。
  • 服务对象
    • TCP是一对一的两点服务,即一条连接只有两个端点。
    • UDP支持一对一、一对多、多对多的交互通信。
  • 可靠性
    • TCP是可靠交付数据的,数据可以无差错、不丢失、不重复、按需到达。
    • UDP是尽最大努力交付,不保证可靠交付数据。
  • 拥塞控制、流量控制
    • TCP有拥塞控制和流量控制机制,保证数据传输的安全性
    • UDP则没有,即使网络非常拥堵了,也不会影响UDP的发送速率。
  • 首部开销
    • TCP首部长度较长,会有一定的开销,首部在没有使用 选项 字段时是 20 个字节,如果使用了选项字段则会更长的。
    • UDP ⾸部只有 8 个字节,并且是固定不变的,开销较⼩。
  • 传输方式
    • TCP 是流式传输,没有边界,但保证顺序和可靠。
    • UDP 是⼀个包⼀个包的发送,是有边界的,但可能会丢包和乱序。
  • 分片不同
    • TCP的数据大小如果大于MSS大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装TCP数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片。
    • UDP的数据大小如果大于MTU*(Maximum Transmission Unit)*大小,则会在IP层进行分片,目标主机收到后,在IP层组装完数据,接着再传输给传输层,但是如果中途丢了一个分片,在实现可靠传输的UDP时则就需要重传所有的数据包,这样传输效率非常差,所以通常UDP的报文应该小于MTU。

TCP 和 UDP 应⽤场景

  • 由于TCP是面向连接,能保证数据的可靠性交付,因此经常用于:FTP文件传输,HTTP/HTTPS、SMTP
  • 由于UDP面向无连接,它可以随时发送数据,再加上UDP本身的处理既简单又高效,因此经常用于:
    • 包总量较少的通信,如DNS、SNMP、RIP等
    • 视频、音频等多媒体通信
    • 广播通信

TCP粘包原因和解决方法

TCP粘包是指:发送方发送的若干包数据到接收方接收时粘成一包

发送方原因:

TCP默认使用Nagle*[ˈneɪgəl]*算法(主要作用:减少网络中报文段的数量)。

收集多个小分组,在一个确认到来时一起发送、导致发送方可能会出现粘包问题

接收方原因:

TCP将接收到的数据包保存在接收缓存里,如果TCP接收数据包到缓存的速度大于应用程序从缓存中读取数据包的速度,多个包就会被缓存,应用程序就有可能读取到多个首尾相接粘到一起的包。

解决粘包问题:

最本质原因在于接收方无法分辨消息与消息之间的边界在哪,通过使用某种方案给出边界,例如:

  • 发送定长包。每个消息的大小都是一样的,接收方只要累计接收数据,直到数据等于一个定长的数值就将它作为一个消息。

  • 包尾加上\r\n标记。FTP协议正是这么做的。但问题在于如果数据正文中也含有\r\n,则会误判为消息的边界。

  • 包头加上包体长度。包头是定长的4个字节,说明了包体的长度。接收方先接收包体长度,依据包体长度来接收包体。

TCP三次握手

⼀开始,客户端和服务端都处于 CLOSED 状态。先是服务端主动监听某个端⼝,处于 LISTEN 状态;

  • 客户端随机初始化序列号 client_isn ,将序列号置于TCP首部的 序列号 字段中,同时把 SYN 标制位置为 1,表示 SYN 报文。接着把第一个SYN报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 **SYN_SENT**状态;
  • 服务端收到客户端的 SYN报文后,首先服务端也随机初始化自己的序列号 server_isn,并填入 TCP 首部的 序列号 字段中,其次把TCP首部的 确认应答号 字段填入 client_isn + 1,接着把 SYNACK 标制置为 1,最后把该报文发送给客户端,该报文也不包含应用层数据,之后服务端处于 SYN_RCVD 状态;
  • 客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文TCP首部 ACK 标志位置为 1,其次 确认应答号 字段填入 server_isn + 1,最后把报文发送给服务端,这次报文可以携带客户到服务器的数据,之后客户端处于 ESTABLISHED 状态。

服务端收到客户端的应答报文后,也进入 ESTABLISHED 状态。

可通过 netstat -napt 命令查看TCP的连接状态

什么是TCP连接:用于保证可靠性和流量控制维护的某些状态信息,这信息的组合,包括 Socket、序列号和窗口大小称为连接。

TCP握手为什么三次

  • 三次握手才可以避免历史连接(主要原因);

    我们来看看 RFC 793 指出的 TCP 连接使⽤三次握⼿的⾸要原因

    The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion.

    简单来说,三次握⼿的⾸要原因是为了防⽌旧的重复连接初始化造成混乱。

    客户端连续发送多次 SYN 建⽴连接的报⽂,在⽹络拥堵情况下:

    • ⼀个「旧 SYN 报⽂」⽐「最新的 SYN 」 报⽂早到达了服务端;
    • 那么此时服务端就会回⼀个 SYN + ACK 报⽂给客户端;
    • 客户端收到后可以根据⾃身的上下⽂,判断这是⼀个历史连接(序列号过期或超时),那么客户端就会发送 RST 报⽂给服务端,表示中⽌这⼀次连接。
  • 三次握手才可以同步双方的初始序列号;

    TCP 协议的通信双⽅, 都必须维护⼀个「序列号」, 序列号是可靠传输的⼀个关键因素,它的作⽤:

    • 接收⽅可以去除重复的数据;
    • 接收⽅可以根据数据包的序列号按序接收;
    • 可以标识发送出去的数据包中, 哪些是已经被对⽅收到的;
  • 三次握手才可以避免资源浪费;

    如果只有「两次握⼿」,当客户端的 SYN 请求连接在⽹络中阻塞,客户端没有接收到 ACK 报⽂,就会重新发送 SYN ,由于没有第三次握⼿,服务器不清楚客户端是否收到了⾃⼰发送的建⽴连接的 ACK 确认信号,所以每收到⼀个 SYN 就只能先主动建⽴⼀个连接,这会造成什么情况呢?

    如果客户端的 SYN 阻塞了,重复发送多次 SYN 报⽂,那么服务器在收到请求后就会建⽴多个冗余的⽆效链接,造成不必要的资源浪费。

为什么两次不行?

无法防止历史连接的建立;会造成双方资源的浪费;也无法可靠的同步双方序列号。

TCP四次挥手

  • 客户端打算关闭连接,此时会发送一个TCP首部 FIN 标制位被置为 1 的报文,也即 FIN 报文,之后客户端进入 FIN_WAIT_1 状态;
  • 服务端收到该报文后,就向客户端发送 ACK 应答报文,接着服务端进入 **CLOSED_WAIT**状态;客户端收到服务端的 ACK 应答报文后,之后进入 FIN_WAIT_2 状态;
  • 等到客户端处理完数据后,也向客户端发送 FIN 报文,之后服务端进入 LAST_ACK 状态;
  • 客户端收到服务端的 FIN 报文后,回一个 ACK 应答报文,之后进入 TIME_WAIT 状态;服务器收到了 ACK 应答报文后,就进入 CLOSED 状态,至此服务器已经完成连接的关闭;

客户端在经过 2MSL*(Maximum Segment Lifetime)* 时间后,自动进入 CLOSED 状态,至此客户端也完成连接的关闭。

这⾥⼀点需要注意是:主动关闭连接的,才有 TIME_WAIT 状态。

TCP回收为什么四次

再来回顾下四次挥⼿双⽅发 FIN 包的过程,就能理解为什么需要四次了。

  • 关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据。
  • 服务器收到客户端的 FIN 报⽂时,先回⼀个 ACK 应答报⽂,⽽服务端可能还有数据需要处理和发送,等服务端不再处理数据时,才发送 FIN 报⽂给客户端来表示同意现在关闭连接。

从上⾯过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK 和 FIN ⼀般都会分开发送,从⽽⽐三次握⼿导致多了⼀次。

为什么需要 TIME_WAIT 状态?

首先,主动发起关闭连接的一方,才会有 TIME_WAIT 状态。

需要**TIME_WAIT**状态,主要有两个原因:

  • 防止具有相同 四元组 的旧数据包被收到;

    TCP 就设计出了这么⼀个机制,经过 2MSL 这个时间,⾜以让两个⽅向上的数据包都被丢弃,使得原来连接的数据包在⽹络中都⾃然消失,再出现的数据包⼀定都是新建⽴连接所产⽣的。

  • 保证 被动关闭连接的一方能被正确的关闭,即保证最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭;

TCP 怎么处理丢包的问题的

ack,超时重传,接收缓冲,快速重传

知道一个域名,怎么和他通信

dns(首先查询本地HOST文件,没有则查询网络),ip,路由

TCP是如何实现数据的可靠传输的

通过 校验和序列号确认应答重发控制(又分为超时重传、快速重传、SACK、D-SACK)连接管理流量控制拥塞控制等。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XCLcjG8K-1632587894655)(D:\Desktop\知识整理\图片\image-20210826145130394.png)]

  • 校验和:在数据传输过程中,将发送的数据段都当做一个16位的整数,将这些整数加起来,并且前面的进位不能丢弃,补在最后,然后取反,得到校验和。

    发送方:在发送数据之前计算校验和,并进行校验和的填充。

    接收方:收到数据后,对数据以同样的方式进行计算,求出校验和,与发送方进行比较。

  • 序列号:TCP 传输时将每个字节的数据都进行了编号,这就是序列号。序列号的作用不仅仅是应答作用,有了序列号能够将接收到的数据根据序列号进行排序,并且去掉重复的数据。

  • 确认应答:TCP 传输过程中,每次接收方接收到数据后,都会对传输方进行确认应答,也就是发送 ACK 报文,这个 ACK 报文中带有对应的确认序列号,告诉发送方,接收了哪些数据,下一次数据从哪里传。

  • 重发控制

    • 超时重传:在发送数据时,设定一个定时器,当超过指定的时间后,没有收到对方的 ACK 确认应答报文,就会重发该数据。TCP会在以下两种情况发生超时重传:数据包丢失 和 确认应答号丢失。超时时间应略大于RTT(Round-Trip Time 往返时延)。
    • 快速重传:当收到三个相同的ACK报文时,会在定时器过期之前,重传丢失的报文段。
    • SACK(Selective Acknowledgment 选择性确认):快速重传机制解决了超时时间的问题,但是没有解决当重传的时候,是重传之前的一个,还是重传所有的问题。该方式需要在TCP头部选项字段里加一个SACK的东西,它可以将缓存的地图发送给发送方,这样发送方就可以知道哪些数据收到了,哪些数据没有收到,知道了这些数据,就可以只重传丢失的数据。
    • D-SACK(Duplicate SACK):使用SACK主要来告诉 发送方 有哪些数据被重复接收了。
  • 连接管理:就是指三次握手、四次挥手的过程。

  • 流量控制:如果发送方的发送速度太快,会导致接收方的接收缓冲区填充满了,这时候继续传输数据,就会造成大量丢包,进而引起丢包重传等等一系列问题。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 超时那么强烈。然后,进⼊快速恢复算法如下:

      • 拥塞窗口 cwnd = ssthresh + 3 ( 3 的意思是确认有 3 个数据包被收到了);
      • 重传丢失的数据包;
      • 如果再收到重复的 ACK,那么 cwnd 增加 1;
      • 如果收到新数据的 ACK 后,把 cwnd 设置为第⼀步中的 ssthresh 的值,原因是该 ACK 确认了新的数据,说明从 duplicated ACK 时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进⼊拥塞避免状态;

IP地址是怎样分类的?

先说一下 IP 的基本特点:

  • IP地址由四段组成,每个字段是一个字节,8位,最大值是255。
  • IP地址由两部分组成,即网络地址和主机地址。网络地址表示其属于互联网的哪一个网络,主机地址表示其属于该网络中的哪一台主机。

IP 地址主要分为A、B、C三类及特殊地址D、E这五类,甩一张图:

2021-09-26_第1张图片

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、http1.1、https、http2、http3演变

HTTP1.0

性能问题:每发送一个请求,都需要新建立一次TCP连接(三次握手),而且是串行请求,增加了通信开销。

HTTP1.1

性能提升:

  • 提出了长连接的通信方式,也叫持久连接,减少了TCP连接的重复建立和断开所造成的额外开销,减轻了服务器端的负载。长连接特点:只要任意一端没有明确提出断开连接,则保持TCP连接状态。

  • 管道网络传输:在同一个TCP连接里面,客户端可以发起多个请求,只要第一个请求发送出去了,不必等其回来,就可以发送第二个请求出去,可以减少整体的响应时间。

    因此出现的新问题:队头阻塞,即服务器还是按照顺序对请求进行回应,如果前面的回应特别慢,后面就会有许多请求排队等着。

性能瓶颈:

  • Header(请求/响应头部)未经压缩就发送,首部信息越多延迟越大。只能压缩Body部分;
  • 发送冗长的首部,每次互相发送相同的首部造成的浪费较多;
  • 队头阻塞;
  • 没有请求优先级控制;
  • 请求只能从客户端开始,服务器只能被动响应;

HTTP 2:HTTP 2 协议基于HTTPS

性能提升:

  • 头部压缩:HPACK算法,在客户端和服务器同时维护一张头部信息表;
  • 二进制格式:全面采用二进制格式,头信息和数据体都是二进制,统称为帧(frame),分为头信息帧和数据帧;
  • 数据流:每个请求或回应的所有数据包,称为数据流(Stream),每个数据流都标记着一个独一无二的编号,其中规定客户端发送的数据流为奇数,服务端发出的数据流编号为偶数,客户端还可以指定数据流的优先级;
  • 多路复用:一个链接中并发多个请求或响应,而不是顺序一一对应;
  • 服务器推送:服务器可以主动向客户端发送信息;

性能瓶颈:

  • 多路复用,一旦发生了丢包现象,就会触发TCP的重传机制,这样在一个TCP连接中的所有HTTP请求都必须等待这个丢了的包被重传回来;
  • 握手时延迟:需要经过TCP三次握手和TLS四次握手(1.2版本)的过程,需要3个RTT时延才能发送数据;

HTTP 3:HTTP 3 把HTTP下层的TCP协议改成了UDP

性能提升:

  • UDP是不可靠传输,但基于QUIC协议可以实现类似TCP的可靠传输,当某个流丢包时,只会阻塞这个流,其他流不受影响;
  • TLS/1.3 的头部压缩算法升级成了QPACK;
  • QUIC把以往的TCP和TLS/1.3 的6次交互合并成了3次,减少了交互次数。QUIC是一个在UDP之上的伪TCP+TLS+HTTP/2的多路复用协议。

HTTPS

特点

  • 在TCP和HTTP层间加入了SSL/TLS安全协议,使得报文能够加密传输;
  • HTTPS在TCP三次握手之后,还需要进行SSL/TLS的握手过程,才可进入加密报文传输;
  • HTTPS协议需要向CA(证书权威机构)申请数字证书,来保证服务器的身份是可信的;
  • 端口号是443;

解决安全方案

  • 信息加密:混合加密的方式实现信息的机密性,解决了窃听的风险;
  • 校验机制:摘要算法的方式来实现完整性,它能为数据生成独一无二的指纹,指纹用于校验数据的完整性,解决了篡改的风险;
  • 身份证书:将服务器公钥放入到数字证书中,解决了冒充的风险;

SSL/TLS协议基本流程

  • 第一步,客户端向服务器索要并验证服务器的公钥;
  • 第二步,双方协商生产会话密钥;
  • 第三步,双方采用会话密钥进行加密通信;

上述前两步就是SSL/TLS的建立过程,也就是握手阶段:①ClientHello;②ServerHello;③客户端回应;④服务器的最后回应。注:SSL/TLS 1.3经过优化只需要3次握手。

HTTPS优化

HTTPS性能损耗的两个环节

  • 第一个环节是TLS协议握手过程,不仅增加了2RTT的网络延迟,而且握手过程中的一些步骤也会产生性能损耗(ECDHE算法需要临时生成椭圆曲线公钥、访问CA、双方计算对称加密密钥等);
  • 第二个环节就是握手后的对称加密报文传输;

优化方案

针对第一个环节:

  • 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 是超⽂本传输协议,信息是明⽂传输,存在安全⻛险的问题;

    HTTPS 则解决 HTTP 不安全的缺陷,在TCP 和 HTTP ⽹络层之间加⼊了 SSL/TLS 安全协议,使得报⽂能够加密传输。

  • HTTP 连接建⽴相对简单,是无状态的,TCP 三次握⼿之后便可进⾏ HTTP 的报⽂传输;

    ⽽ HTTPS 在 TCP 三次握⼿之后,还需进⾏ SSL/TLS 的握⼿过程,才可进⼊加密报⽂传输。

  • HTTP 的端⼝号是 80;HTTPS 的端⼝号是 443。

  • HTTPS 协议需要向 CA(证书权威机构)申请数字证书,来保证服务器的身份是可信的。

对称加密和非对称加密的区别和原理

对称密钥加密是指加密和解密使用同一个密钥的方式,这种方式存在的最大问题就是密钥发送问题,即如何安全地将密钥发给对方;

而非对称加密是指使用一对非对称密钥,即公钥私钥,公钥可以随意发布,但私钥只有自己知道。发送密文的一方使用对方的公钥进行加密处理,对方接收到加密信息后,使用自己的私钥进行解密。

由于非对称加密的方式不需要发送用来解密的私钥,所以可以保证安全性;但是和对称加密比起来,它比较慢,所以我们还是要用对称加密来传送消息,但对称加密所使用的密钥我们可以通过非对称加密的方式发送出去。

常见的状态码有哪些?

  • 1×× : 请求处理中,请求已被接受,正在处理;
  • 2×× : 请求成功,请求被成功处理 200 OK、204:与 200 OK 基本相同,但响应头没有 body 数据、206:是应⽤于 HTTP 分块下载或断点续传,表示响应返回的 body 数据并不是资源的全部,⽽是其中的⼀部分,也是服务器处理成功的状态;
  • 3×× : 重定向,要完成请求必须进行进一步处理。301 : 永久性转移,需改⽤新的 URL 再次访问 、302 :暂时性转移、 304 :已缓存;
  • 4×× : 客户端错误,请求不合法。 400:Bad Request,请求有语法问、 403:拒绝请求,并不是客户端的请求出错 、404:客户端所访问的页面不存在,所以⽆法提供给客户端;
  • 5×× : 服务器端错误,服务器不能处理合法请求 500 :服务器内部错误 、501:客户端请求的功能还不⽀持、502:通常是服务器作为⽹关或代理时返回的错误码,表示服务器⾃身⼯作正常,访问后端服务器发⽣了错误、503 :服务不可用,稍等;

http中常见的header字段有哪些

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的区别?

  • GET 一般用来从服务器上获取资源;POST 一般用来创建资源;
  • GET 是幂等的,即读取同一个资源,总是得到相同的数据;而 POST 不是幂等的。GET 不会改变服务器上的资源;而 POST 会对服务器资源进行改变;
  • 从请求参数形式上看,GET 请求的数据会附在URL之后;而 POST 请求会把提交的数据则放置在是HTTP请求报文的请求体中。
  • POST 的安全性要比 GET 的安全性高,因为 GET 请求提交的数据将明文出现在 URL 上;而 POST 请求参数则被包装到请求体中,相对更安全。
  • GET 请求的长度受限于浏览器或服务器对URL长度的限制,允许发送的数据量比较小;而POST请求则是没有大小限制的。

http除了常用的get和post还有什么方法

方法 描述
GET 向特定资源发送请求,查询数据,并返回实体
POST 向指定资源提交数据进行处理请求,可能会导致新的资源建立、已有资源修改
PUT 向服务器上传新的内容
HEAD 类似GET请求,返回的响应中没有具体的内容,用于获取报头
DELETE 请求服务器删除指定标识的资源
OPTIONS 可以用来向服务器发送请求来测试服务器的功能性
TRACE 回显服务器收到的请求,用于测试或诊断
CONNECT HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器

DNS 的寻址过程

(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 服务器再返回给客户机。

在浏览器中输入一个www.baidu.com后执行的全部过程

域名解析 -> 建立TCP连接(三次握手)-> 发起http请求 -> 服务器响应http请求,浏览器得到html代码 -> 浏览器解析html代码,并请求html代码中的资源(如 js、css、图片等)-> 浏览器对页面进行渲染呈献给用户。

有哪些 web 性能优化技术

  • DNS查询优化
  • 客户端缓存
  • 优化TCP连接
  • 避免重定向
  • 网络边缘的缓存
  • 条件缓存
  • 压缩和代码极简化
  • 图片优化

什么是 XSS 攻击

XSS 即(Cross Site Scripting)中文名称为:跨站脚本攻击。XSS的重点不在于跨站点,而在于脚本的执行。

XSS的原理是:

恶意攻击者在web页面中会插入一些恶意的 script代码。当用户浏览该页面的时候,那么嵌入到web页面中script代码会执行,因此会达到恶意攻击用户的目的。

XSS攻击最主要有如下分类:反射型、存储型、及 DOM-based型。反射型 和 DOM-baseed型可以归类为 非持久性XSS攻击。存储型可以归类为 持久性XSS攻击。

什么是跨站攻击CSRF

CSRF(Cross Site Request Forgery,跨站域请求伪造)是一种网络的攻击方式,它在 2007 年曾被列为互联网 20 大安全隐患之一,也被称为『One Click Attack』或者 『Session Riding』,通常缩写为CSRF或者XSRF,是一种对网站的恶意利用。

听起来像跨站脚本(XSS),但它与XSS非常不同,并且攻击方式几乎相左。

XSS利用站点内的信任用户,而CSRF则通过伪装来自受信任用户的请求来利用受信任的网站。与XSS攻击相比,CSRF攻击往往不大流行(因此对其进行防范的资源也相当稀少)和难以防范,所以被认为比XSS更具危险性。

死锁条件、解决方式。

​ 死锁是指两个或两个以上进程在执行过程中,因争夺资源而造成的下相互等待的现象;

死锁的条件:

  • 互斥条件:进程对所分配到的资源不允许其他进程访问,若其他进程访问该资源,只能等待至占有该资源的进程释放该资源;
  • 请求与保持条件:进程获得一定的资源后,又对其他资源发出请求,阻塞过程中不会释放自己已经占有的资源
  • 不可剥夺条件:进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用后自己释放
  • 循环等待条件:系统中若干进程组成环路,环路中每个进程都在等待相邻进程占用的资源

​ **解决方法:**破坏死锁的任意一条件

  • 破坏资源互斥条件:乐观锁,CAS
  • 破坏剥夺请求和保持条件:资源一次性分配、tryLock。
  • 破坏不可剥夺资源:即当进程新的资源未得到满足时,释放已占有的资源,从而破坏不可剥夺的条件,数据库deadlock超时,死锁检测。
  • 破坏循环等待条件:资源有序分配法。系统给每类资源赋予一个序号,每个进程按编号递增的请求资源,从而破坏环路等待的条件,转账场景

A要用到B服务器的服务,开了多个连接,但是只有一小部分连接成功了,怎么排查原因?

我说可以用netstat查看一下两端的连接状态,比如可以判断B是否被SYN攻击了,还是单纯网络问题。 另外也可能B端的文件描述符用完了,他表示能想到文件描述符这个层面很好。

Object有哪些方法?

  • Object():默认构造方法
  • clone():创建并返回此对象的一个副本
  • equals():指示某个其他对象是否与此对象相等
  • finalize():当垃圾回收器确定不存在该对象的更多引用时,由对象的垃圾回收器调用此方法
  • getClass():返回一个对象运行时类
  • hashCode():返回该对象的哈希值
  • notify():唤醒此对象监视器上等待的单个线程,如果所有线程都在此同步监视器上等待,则会选择唤醒其中一个线程,选择是任意性的
  • notifyAll():唤醒此对象监视器上等待的所有线程
  • toString():返回该对象的字符串表示
  • wait():导致当前的线程等待,直到其他线程调用该同步监视器的notify()或notifyAll()方法唤醒该线程
  • wait(long timeout):导致当前的线程等待调用此对象notify()或notifyAll(),等待指定时间后自动苏醒
  • wait(long timeout, int nanos):导致当前的线程等待,直到其他线程调用此对象的notify()或notifyAll(),或其他某个线程中断当前线程,或者以超过某个时间时间量
  • registerNatives():对本地方法进行注册

sleep和wait的区别

  • sleep()不释放锁,wait()释放锁
  • sleep()在Thread类中声明的,wait()在Object类中声明
  • sleep()是静态方法,是Thread.sleep();wait()是非静态方法,必须由“同步锁”对象调用
  • sleep()方法导致当前线程进入阻塞状态后,当时间到或interrupt()醒来;wait()方法导致当前线程进入阻塞状态后,由notify或notifyAll()唤醒

为什么wait(),notify()和notifyAll()必须在同步方法或者同步块中被调用?

当一个线程需要调用对象的 wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的notify()方法。同样的,当一个线程需要调用对象的 notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。

和wait()比较一下,thread中的sleep()和 yield ()方法为什么被设计为静态方法?

Thread 类的 sleep()和 yield()方法将在当前正在执行的线程上运行。所以在其他处于等待状态的线程上调用这些方法是没有意义的。这就是为什么这些方法是静态的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其他非运行线程调用这些方法。

如果是实例方法,就可以实现在一个线程里随时让其它线程在执行中休眠,而且这种休眠是不放弃锁的。那么这种简单粗暴就很容易引起混乱,因此目前这种机制其实就是一种限制:你可以选择给自己吃安眠药,而不能随便给正在工作的别人吃安眠药。

该代码只有在某个A线程执行时会被执行,这种情况下通知某个B线程yield是无意义的(因为B线程本来就没在执行)。因此只有当前线程执行yield才是有意义的。通过使该方法为static,你将不会浪费时间尝试yield 其他线程。就是说,如果是和线程实例绑定的话,你可能会在当前线程中尝试调用otherThread.yeild()/sleep(), 而这使没有意义的

首先wait()是操作监视器对象的; sleep()是由线程调用的,Thread.sleep是 使当前执行的线程休眠(就是 Thread.sleep所在代码片段的线程)。

如果sleep是实例方法,则在JVM中可以拿到thread实例的引用,因此就会出现别的线程强制另外一个线程睡眠的方法,这样就出现了线程的执行逻辑以及内存模型的不可控,所以只能把目标设定为当前的线程。

如果sleep不是静态的, 只对当前进程作用. 而是实例方法, 那么应该和suspend有同样的问题, 死锁.

wait为什么是Object的方法,sleep为什么是Thread的方法

sleep()是让某个线程暂停运行一段时间,其控制范围是由当前线程决定,也就是说,在线程里面决定.好比如说,我要做的事情是 “点火->烧水->煮面”,而当我点完火之后我不立即烧水,我要休息一段时间再烧.对于运行的主动权是由我的流程来控制.
​而wait(),首先,这是由某个确定的对象来调用的,将这个对象理解成一个传话的人,当这个人在某个线程里面说"暂停!",也是 thisOBJ.wait(),这里的暂停是阻塞,还是"点火->烧水->煮饭",thisOBJ就好比一个监督我的人站在我旁边,本来该线 程应该执行1后执行2,再执行3,而在2处被那个对象喊暂停,那么我就会一直等在这里而不执行3,但整个流程并没有结束,我一直想去煮饭,但还没被允许, 直到那个对象在某个地方说"通知暂停的线程启动!",也就是thisOBJ.notify()的时候,那么我就可以煮饭了,这个被暂停的线程就会从暂停处 继续执行.

  • wait和notify不仅仅是普通方法或同步工具,更重要的是他们是Java中两个线程之间的通信机制。对语言设计者而言, 如果不能通过 Java 关键字(例如 synchronized)实现通信此机制,同时又要确保这个机制对每个对象可用, 那么 Object 类则是的合理的声明位置。同步等待通知是两个不同的领域,不要把它们看成是相同的或相关的。同步是提供互斥并确保 Java 类的线程安全,而 wait 和 notify 是两个线程之间的通信机制
  • 每个对象都可上锁,这是在 Object 类而不是 Thread 类中声明 wait 和 notify 的另一个原因。
  • 在 Java 中,为了进入代码的临界区,线程需要锁定并等待锁,他们不知道哪些线程持有锁,而只是知道锁被某个线程持有, 并且需要等待以取得锁, 而不是去了解哪个线程在同步块内,并请求它们释放锁。
  • Java 是基于 Hoare 的监视器的思想(http://en.wikipedia.org/wiki/…)。在Java中,所有对象都有一个监视器。
  • 在 Java 设计中,线程不能被指定,它总是运行当前代码的线程。但是,我们可以指定监视器(这是我们称之为等待的对象)。这是一个很好的设计,因为如果我们可以让任何其他线程在所需的监视器上等待,这将导致“入侵”,影响线程执行顺序,导致在设计并发程序时会遇到困难。请记住,在 Java 中,所有在另一个线程的执行中造成入侵的操作都被弃用了(例如 Thread.stop 方法)。

Java 的每个对象中都有一个锁(monitor,也可以成为监视器) 并且 wait(),notify()等方法用于等待对象的锁或者通知其他线程对象的监视器可用。在Java 的线程中并没有可供任何对象使用的锁和同步器。这就是为什么这些方法是 Object类的一部分,这样 Java 的每一个类都有用于线程间通信的基本方法。

concurrentHashMap1.7 这个最多支持多少的并发读?

读的时候相应的段会加锁,共有16段,所以并发读就是16。

LINUX kill命令

在Linux/unix下,中止一个Java进程有两种方式,一种是kill -9 pid,一种是kill -15 pill(默认)。

SIGNKILL(9) 的效果是立即杀死进程. 该信号不能被阻塞, 处理和忽略

SIGNTERM(15) 的效果是正常退出进程,退出前可以被阻塞或回调处理。并且它是Linux缺省的程序中断信号(默认是15)。

非阻塞IO和阻塞IO的区别?

**阻塞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:异步非阻塞;

  • BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
  • NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
  • AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。

IO多路复用:epoll和select和poll

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)

reactor模式介绍?

基于I/O多路复用编写网络程序是面向过程的方式写代码的,这样开发的效率不高。于是,大佬们对I/O多路复用做了一层封装,让使用者不用考虑底层网络API的细节,只需要关注应用代码的编写。

Reactor,意思是反应堆,但这里的反应指的是对事件反应,也就是来了一个事件,Reactor就有相对应的反应/响应。 Reactor模式也叫Dispatcher*(调度员)*模式,即I/O多路复用监听事件,收到事件后,根据事件类型分配(Dispatch)给某个进行/线程。

Reactor 模式主要由 Reactor处理资源池这两个核⼼部分组成,它俩负责的事情如下:

  • Reactor负责监控和分发事件,事件类型包含连接事件、读写事件;
  • 处理资源池 负责处理事件,如read -> 业务裸机 -> send;

Reactor 模式是灵活多变的,可以应对不同的业务场景,灵活在于:

  • Reactor的数量可以只有一个,也可以有多个;
  • 处理资源池 可以是单个进程/线程,也可以是多个进程/线程;

将上⾯的两个因素排列组设⼀下,理论上就可以有 4 种⽅案选择:

单 Reactor 单进程 / 线程;

C语言「 Reactor 单进程」、Java 「 Reactor 单线程」 --Redis采用此方案

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o765MudI-1632587894658)(D:\Desktop\知识整理\图片\image-20210730174334589.png)]

​ 可以看到进程⾥有 Reactor、Acceptor、Handler 这三个对象:

  • Reactor 对象的作⽤是监听和分发事件;
  • Acceptor 对象的作⽤是获取连接;
  • Handler 对象的作⽤是处理业务;

对象⾥的 select、accept、read、send 是系统调⽤函数,dispatch 和 「业务处理」是需要完成的操作,其中 dispatch 是分发事件操作。

方案实现

  • Reactor对象通过select(IO多路复用接口)监听事件,收到事件后通过dispatch进行分发,具体分发给Acceptor对象还是Handler对象,还要看收到的事件类型;
  • 如果是连接建⽴的事件,则交由 Acceptor 对象进⾏处理,Acceptor 对象会通过 accept ⽅法 获取连接,并创建⼀个 Handler 对象来处理后续的响应事件;
  • 如果不是连接建⽴事件, 则交由当前连接对应的 Handler 对象来进⾏响应;
  • Handler 对象通过 read -> 业务处理 -> send 的流程来完成完整的业务流程。

优点

单 Reactor 单进程的⽅案因为全部⼯作都在同⼀个进程内完成,所以实现起来⽐较简单,不需要考虑进程间通信,也不⽤担⼼多进程竞争。

缺点

  • 第⼀个缺点,因为只有⼀个进程,⽆法充分利⽤ 多核 CPU 的性能
  • 第⼆个缺点,Handler 对象在业务处理时,整个进程是⽆法处理其它连接的事件的,如果业务处理耗时⽐较⻓,那么就造成响应的延迟

所以,单 Reactor 单进程的⽅案不适⽤计算机密集型的场景,只适⽤于业务处理⾮常快速的场景

单 Reactor 多进程 / 线程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yiAjU1Ki-1632587894659)(D:\Desktop\知识整理\图片\image-20210730175048969.png)]

方案实现

  • Reactor 对象通过 select (IO 多路复⽤接⼝) 监听事件,收到事件后通过 dispatch 进⾏分发,具体分发给 Acceptor 对象还是 Handler 对象,还要看收到的事件类型;
  • 如果是连接建⽴的事件,则交由 Acceptor 对象进⾏处理,Acceptor 对象会通过 accept ⽅法 获取连接,并创建⼀个 Handler 对象来处理后续的响应事件;
  • 如果不是连接建⽴事件, 则交由当前连接对应的 Handler 对象来进⾏响应;
  • Handler 对象不再负责业务处理,只负责数据的接收和发送,Handler 对象通过 read 读取到数据后,会将数据发给⼦线程⾥的 Processor 对象进⾏业务处理;
  • ⼦线程⾥的 Processor 对象就进⾏业务处理,处理完后,将结果发给主线程中的 Handler 对象,接着由 Handler 通过 send ⽅法将响应结果发送给 client;

优点:单 Reator 多线程的⽅案优势在于能够充分利⽤多核CPU 的性能

缺点:带来了多线程竞争资源的问题。

单 Reactor 多进程相⽐单 Reactor 多线程实现起来很麻烦,主要因为要考虑⼦进程和⽗进程的双向通信,并且⽗进程还需要知道⼦进程要将数据发送给哪个客户端。⽽多线程间可以共享数据,虽然要额外考虑并发问题,但是这远⽐进程间通信的复杂度低得多,因此实际应⽤中也看不到单 Reactor 多进程的模式

「单 Reactor」的模式还有个问题,因为⼀个 Reactor 对象承担所有事件的监听和响应,⽽且只在主线程中运⾏,在⾯对瞬间⾼并发的场景时,容易成为性能的瓶颈的地⽅。

多 Reactor 单进程 / 线程; --复杂而且没有性能优势,因此实际中并没有应用。

多 Reactor 多进程 / 线程;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-obJW0Aw2-1632587894660)(D:\Desktop\知识整理\图片\image-20210730175747097.png)]

方案实现

  • 主线程中的 MainReactor 对象通过 select 监控连接建⽴事件,收到事件后通过 Acceptor 对象中的accept 获取连接,将新的连接分配给某个⼦线程;
  • ⼦线程中的 SubReactor 对象将 MainReactor 对象分配的连接加⼊ select 继续进⾏监听,并创建⼀个Handler ⽤于处理连接的响应事件。
  • 如果有新的事件发⽣时,SubReactor 对象会调⽤当前连接对应的 Handler 对象来进⾏响应。
  • Handler 对象通过 read -> 业务处理 -> send 的流程来完成完整的业务流程。

多 Reactor 多线程的⽅案虽然看起来复杂的,但是实际实现时⽐单 Reactor 多线程的⽅案要简单的多,原因如下:

  • 主线程和⼦线程分⼯明确,主线程只负责接收新连接,⼦线程负责完成后续的业务处理。
  • 主线程和⼦线程的交互很简单,主线程只需要把新连接传给⼦线程,⼦线程⽆须返回数据,直接就可以在⼦线程将处理结果发送给客户端

⼤名鼎鼎的两个开源软件 NettyMemcache 都采⽤了「多 Reactor 多线程」的⽅案。

采⽤了「多 Reactor 多进程」⽅案的开源软件是 Nginx,不过⽅案与标准的多 Reactor 多进程有些差异。

具体差异表现在主进程中仅仅⽤来初始化 socket,并没有创建 mainReactor 进行 accept 连接,⽽是由⼦进程的 Reactor 进行 accept 连接,通过锁来控制⼀次只有⼀个⼦进程进⾏ accept(防⽌出现惊群现象),⼦进程 accept 新连接后就放到⾃⼰的 Reactor 进⾏处理,不会再分配给其他⼦进程。

拓展:Reactor 和 Proactor 的区别

  • Reactor 是⾮阻塞同步⽹络模式,感知的是就绪可读写事件。在每次感知到有事件发⽣(⽐如可读就绪事件)后,就需要应⽤进程主动调⽤ read ⽅法来完成数据的读取,也就是要应⽤进程主动将socket 接收缓存中的数据读到应⽤进程内存中,这个过程是同步的,读取完数据后应⽤进程才能处理数据。
  • Proactor 是异步⽹络模式, 感知的是已完成的读写事件。在发起异步读写请求时,需要传⼊数据缓冲区的地址(⽤来存放结果数据)等信息,这样系统内核才可以⾃动帮我们把数据的读写⼯作完成,这⾥的读写⼯作全程由操作系统来做,并不需要像 Reactor 那样还需要应⽤进程主动发起 read/write来读写数据,操作系统完成读写⼯作后,就会通知应⽤进程直接处理数据。

总结: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)

补充:除了访问、插入、删除的不同外,还有在操作系统内存管理方面也有不同。正因为数组与链表的物理存储结构不同,在内存预读方面,内存管理会将连续的存储空间提前读入缓存(局部性原理),所以数组往往会被都读入到缓存中,这样进一步提高了访问的效率,而链表由于在内存中分布是分散的,往往不会都读入到缓存中,这样本来访问效率就低,这样效率反而更低了。在实际应用中,因为链表带来的动态扩容的便利性,在做为算法的容器方面,用的更普遍一点。

哈希表?

哈希表简单来说可以看作是对数组的升级,哈希表和数组的联系和区别:

  • 联系:哈希表是由数组实现的。
  • 区别:数组中存储的元素和数组下标没有确定的关系,而哈希表中存储的元素和数组的下标有一个确定的关系,我们将这个确定的关系称之为哈希函数(Hash)。

什么是堆,堆排序?

堆是基于树抽象数据类型的一种特殊的数据结构,用于许多算法和数据结构中。如果父节点大于子节点,那么就称为最大堆,如果父节点小于子节点,则称为最小堆。

进程通信方式

  • 管道队列:管道就是内核里面的一串缓存,通信方式是单向的、低效的,不适合进程间频繁的交换数据,缓存保存在内核内存中。管道又分匿名管道和命名管道 --字节流数据

    • 对于匿名管道,它的通信范围是存在父子关系的进程;对于命名管道,它可以在不相关的进程间也能相互通信。

    • 在 shell ⾥⾯执⾏ A | B 命令的时候,A 进程和 B 进程都是 shell 创建出来的⼦进程,A 和 B 之间不存在

      ⽗⼦关系,它俩的⽗进程都是 shell。所以说,在 shell ⾥通过「 | 」匿名管道将多个命令连接在⼀起,实际上也就是创建了多个⼦进程,那么在我们编写 shell 脚本时,能使⽤⼀个管道搞定的事情,就不要使用多个管道,这样可以减少创建⼦进程的系统开销。

  • 消息队列:消息队列是保存在内核中消息链表,消息不及时,有大小限制,不适合比较大数据传输,消息队列通信过程中,存在用户态与内核态之间的数据拷贝开销,消息链表保存在内核中 --数据单元即消息体,都是固定大小的存储快,如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。

  • 共享内存:拿出一块虚拟地址空间来,映射到相同的物理内存中,同时修改一个共享内存可能引起冲突。

  • 信号量:保护机制,是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据,PV操作。

  • 信号:信号量方式的进程间通信,是常规状态下的工作模式;

    • Ctrl+C 产⽣ SIGINT 信号,表示终⽌该进程;
    • Ctrl+Z 产⽣ SIGTSTP 信号,表示停⽌该进程,但还未结束;

    异常情况下的工作模式,需要信号的方式来通知进程,信号是进程间通信机制中唯一的异步通信机制,因为可以在任何时候发送信号给某一进程,一旦有信号产生就有下面这几种,用户进程对信号的处理方式。

    • 1.执⾏默认操作。Linux 对每种信号都规定了默认操作,例如, SIGTERM 信号,就是终⽌进程的意思。
    • 2.捕捉信号。我们可以为信号定义⼀个信号处理函数。当信号发⽣时,我们就执⾏相应的信号处理函数。
    • 3.忽略信号。当我们不希望处理某些信号的时候,就可以忽略该信号,不做任何处理。有两个信号是应⽤进程⽆法捕捉和忽略的,即 SIGKILL 和 SEGSTOP ,它们⽤于在任何时候中断或结束某⼀进程。
  • Socket:管道、队列、共享内存、信号量、信号都是在同一台主机上进行进程间通信,Socket通信可以跨网络与不同主机上的进程之间通信(同主机也可以通信)

虚拟地址的好处

  • 程序可以用一系列相邻的虚拟地址访问实际物理内存中不相邻的大内存缓冲区;
  • 程序可以用一系列虚拟地址访问大于可用物理内存的内存缓冲区;
  • 不同进程操作的虚拟地址分隔开,即不同进程不会操作同一个物理地址,造成程序崩溃;

什么是缺页异常

当进程访问的虚拟地址在⻚表中查不到时,系统会产⽣⼀个缺⻚异常

mysql的存储引擎

在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来说,最大的特点在于支持事务。但是这是以损失效率来换取的。

补充:

  1. DDL(Data Definition【释义、定义】 Language):数据定义语言

用来定义数据库对象:数据库,表,列等。关键字:create(创建), drop(删除),alter(修改) 等

  1. DML(Data Manipulation【操纵】 Language):数据操作语言

用来对数据库中表的数据进行增删改。关键字:insert(插入、增加), delete(删除), update(更新) 等

  1. DQL(Data Query【查询】 Language):数据查询语言

用来查询数据库中表的记录(数据)。关键字:select(查询), where 等

  1. DCL(Data Control Language):数据控制语言(了解)

用来定义数据库的访问权限和安全级别,及创建用户。关键字:GRANT, REVOKE 等

MyISAM,使用这个存储引擎,每个 MyISAM 在磁盘上存储形成3个文件:

  • frm文件:存储表的定义数据;
  • MYD文件:存放表具体记录的数据;
  • MYI文件:存储索引;

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。

  • 支持的数据类型有限制,比如:不支持TEXT和BLOB类型,对于字符串类型的数据,只支持固定长度的行,VARCHAR会被自动存储为CHAR类型;
  • 支持的锁粒度为表级锁。所以,在访问量比较大时,表级锁会成为MEMORY存储引擎的瓶颈;
  • 由于数据是存放在内存中,一旦服务器出现故障,数据都会丢失;
  • 查询的时候,如果有用到临时表,而且临时表中有BLOB,TEXT类型的字段,那么这个临时表就会转化为MyISAM类型的表,性能会急剧降低;
  • 默认使用hash索引。
  • 如果一个内部表很大,会转化为磁盘表。

MyISAM和InnoDB差别

  • MyISAM不支持事务,InnoDB支持。MyISAM不支持行锁,只支持表锁,InnoDB支持行锁。
  • InnoDB没有保存具体的行数,所以在统计行数的时候回扫描全表,MyISAM有保存。
  • myisam的索引以表名+.MYI文件分别保存。innodb的索引和数据一起保存在表空间里。

InnoDB 自增主键

上文讨论过InnoDB的索引实现,InnoDB使用聚集索引,数据记录本身被存于主索引(一颗B+Tree)的叶子节点上。这就要求同一个叶子节点内(大小为一个内存页或磁盘页)的各条数据记录按主键顺序存放,因此每当有一条新的记录插入时,MySQL会根据其主键将其插入适当的节点和位置,如果页面达到装载因子(InnoDB默认为15/16),则开辟一个新的页(节点)。

如果表使用自增主键,那么每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,当一页写满,就会自动开辟一个新的页。这样就会形成一个紧凑的索引结构,近似顺序填满。由于每次插入时也不需要移动已有数据,因此效率很高,也不会增加很多开销在维护索引上。

索引的数据结构,与B树的区别?

B树(B-tree):B-树是一种平衡多路查找树,它在文件系统中很有用。

  • 排序方式:所有节点关键字是按递增次序排列,并遵循左小右大原则;
  • 子节点数:非叶节点的子节点数>1,且<=M ,且M>=2,空树除外(注:M阶代表一个树节点最多有多少个查找路径,M=M路,当M=2则是2叉树,M=3则是3叉);
  • 关键字数:枝节点的关键字数量大于等于ceil(m/2)-1个且小于等于M-1个(注:ceil()是个朝正无穷方向取整的函数 如ceil(1.1)结果为2);
  • 其他节点至少有M/2个子节点
  • 所有叶子节点均在同一层、叶子节点除了包含了关键字和关键字记录的指针外也有指向其子节点的指针只不过其指针地址都为null对应下图最后一层节点的空格子;

下图是一个M=4 阶的B树:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sqQ997eO-1632587894662)(D:\Desktop\知识整理\图片\290047064066682.png)]

特点:B树相对于平衡二叉树的不同是,每个节点包含的关键字增多了,特别是在B树应用到数据库中的时候,数据库充分利用了磁盘块的原理(磁盘数据存储是采用块的形式存储的,每个块的大小为4K,每次IO进行数据读取时,同一个磁盘块的数据可以一次性读取出来)把节点大小限制和充分使用在磁盘快大小范围;把树的节点关键字增多后树的层级比原来的二叉树少了,减少数据查找的次数和复杂度;

B+树:B+树是B树的一个升级版,相对于B树来说B+树更充分的利用了节点的空间,让查询速度更加稳定,其速度完全接近于二分法查找。

  • B+跟B树不同B+树的非叶子节点不保存关键字记录的指针,只进行数据索引,这样使得B+树每个非叶子节点所能保存的关键字大大增加;
  • B+树叶子节点保存了父节点的所有关键字记录的指针,所有数据地址必须要到叶子节点才能获取到。所以每次数据查询的次数都一样;
  • B+树叶子节点的关键字从小到大有序排列,左边结尾数据都会保存右边节点开始数据的指针。
  • 非叶子节点的子节点数=关键字数(来源百度百科)(根据各种资料 这里有两种算法的实现方式,另一种为非叶节点的关键字数=子节点数-1(来源维基百科),虽然他们数据排列结构不一样,但其原理还是一样的Mysql 的B+树是用第一种方式实现);

如下图,是一个B+树:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-csWdMeXI-1632587894665)(D:\Desktop\知识整理\图片\290050048129679.png)]

特点:

  • B+树的层级更少:相较于B树B+每个非叶子节点存储的关键字 数更多,树的层级更少所以查询数据更快;
  • B+树查询速度更稳定:B+所有关键字数据地址都存在叶子节点上,所以每次查找的次数都相同所以查询速度要比B树更稳定;
  • B+树天然具备排序功能:B+树所有的叶子节点数据构成了一个有序链表,在查询大小区间的数据时候更方便,数据紧密性很高,缓存的命中率也会比B树高。
  • B+树全节点遍历更快:B+树遍历整棵树只需要遍历所有的叶子节点即可,,而不需要像B树一样需要对每一层进行遍历,这有利于数据库做全表扫描。

B树相对于B+树的优点是,如果经常访问的数据离根节点很近,而B树非叶子节点本身存有关键字其数据的地址,所以这种数据检索的时候会要比B+树快。

B 树和B+树的区别图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4mxO7SWp-1632587894666)(D:\Desktop\知识整理\图片\290050088914733.png)]

总结

  • 相同思想和策略:从平衡二叉树、B树、B+树、B*树总体来看它们的贯彻的思想是相同的,都是采用二分法和数据平衡策略来提升查找数据的速度;
  • 不同的方式的磁盘空间利用:不同点是他们一个一个在演变的过程中通过IO从磁盘读取数据的原理进行一步步的演变,每一次演变都是为了让节点的空间更合理的运用起来,从而使树的层级减少达到快速查找数据的目的。

事务的特性? 原子性怎么实现

事务的四个特性:ACID

  • 原子性(Atomicity):个事物必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作,这就是事物的原子性。
  • 一致性(Consistency):事务应确保数据库的状态从一个一致状态变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束。(又可以解释为:无论事务提交还是回滚,不会破坏数据的完整性)
  • 隔离性(Isolation):通常来说,一个事物所做的修改在最终提交以前,对其他事务是不可见的。
  • 持久性(Durability):一旦事务提交,则其所做的修改就会永久保存到数据库中。此时计时系统崩溃,修改的数据也不会丢失。

事实上,原子性、隔离性、持久性都是为了保证一致性

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。这三个值的意思分别如下:

在这里插入图片描述 log file刷盘策略

注意事项:

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结束后,数据库恢复到一致性状态,可以继续被使用。

MySQL日志

redo log

img

​ 上图中的write pos表示redo log当前记录的日志序列号LSN(log sequence number),写入还未刷盘,循环往后递增;check point表示redo log中的修改记录已刷入磁盘后的LSN,循环往后递增,这个LSN之前的数据已经全落盘。

write poscheck point之间的部分是redo log空余的部分(绿色),用来记录新的日志;check pointwrite pos之间是redo log已经记录的数据页修改数据,此时数据页还未刷回磁盘的部分。当write pos追上check point时,会先推动check point向前移动,空出位置(刷盘)再记录新的日志。

注意:redo log日志满了,在擦除之前,需要确保这些要被擦除记录对应在内存中的数据页都已经刷到磁盘中了。擦除旧记录腾出新空间这段期间,是不能再接收新的更新请求的,此刻MySQL的性能会下降。所以在并发量大的情况下,合理调整redo log的文件大小非常重要。

crash-safe

​ 因为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的记录进行重放刷盘。

2021-09-26_第2张图片

在启动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将数据回滚到事务执行前的状态,保证事务的完整性。

2021-09-26_第3张图片

那可能有人会问:同一个事物内的一条记录被多次修改,那是不是每次都要把数据修改前的状态都写入undo log呢?

答案是不会的!

undo log只负责记录事务开始前要修改数据的原始版本,当我们再次对这行数据进行修改,所产生的修改记录会写入到redo logundo log负责完成回滚,redo log负责完成前滚。

回滚

未提交的事务,即事务未执行commit。但该事务内修改的脏页中,可能有一部分脏块已经刷盘。如果此时数据库实例宕机重启,就需要用回滚来将先前那部分已经刷盘的脏块从磁盘上撤销。

前滚

未完全提交的事务,即事务已经执行commit,但该事务内修改的脏页中只有一部分数据被刷盘,另外一部分还在buffer pool缓存上,如果此时数据库实例宕机重启,就需要用前滚来完成未完全提交的事务。将先前那部分由于宕机在内存上的未来得及刷盘数据,从redo log中恢复出来并刷入磁盘。

数据库实例恢复时,先做前滚,后做回滚。

undo logredo logbin log三种日志都是在刷脏页之前就已经刷到磁盘了的,相互协作最大限度保证了用户提交的数据不丢失。

bin log(归档日志)

bin log是一种数据库Server层(和什么引擎无关),以二进制形式存储在磁盘中的逻辑日志。bin log记录了数据库所有DDLDML操作(不包含 SELECTSHOW等命令,因为这类操作对数据本身并没有修改)。

默认情况下,二进制日志功能是关闭的。可以通过以下命令查看二进制日志是否开启:

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在主从模式下的应用。

2021-09-26_第4张图片

  • 用户在主库master执行DDLDML操作,修改记录顺序写入bin log;
  • 从库slave的I/O线程连接上Master,并请求读取指定位置position的日志内容;
  • Master收到从库slave请求后,将指定位置position之后的日志内容,和主库bin log文件的名称以及在日志中的位置推送给从库;
  • slave的I/O线程接收到数据后,将接收到的日志内容依次写入到relay log文件最末端,并将读取到的主库bin log文件名和位置position记录到master-info文件中,以便在下一次读取用;
  • slave的SQL线程检测到relay log中内容更新后,读取日志并解析成可执行的SQL语句,这样就实现了主从库的数据一致;

基于时间点还原

我们看到bin log也可以做数据的恢复,而redo log也可以,那它们有什么区别?

  • 层次不同:redo log 是InnoDB存储引擎实现的;bin log 是MySQL的服务器层实现的,但MySQL数据库中的任何存储引擎对于数据库的更改都会产生bin log。
  • 作用不同:redo log 用于碰撞恢复(crash recovery),保证MySQL宕机也不会影响持久性;bin log 用于时间点恢复(point-in-time recovery),保证服务器可以基于时间点恢复数据和主从复制。
  • 内容不同:redo log 是物理日志,内容基于磁盘的页Page;bin log的内容是二进制,可以根据binlog_format参数自行设置。
  • 写入方式不同:redo log 采用循环写的方式记录;binlog 通过追加的方式记录,当文件大小大于给定值后,后续的日志会记录到新的文件上。
  • 刷盘时机不同:bin log在事务提交时写入;redo log 在事务开始时即开始写入。

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秒,发现慢查询日志开始记录了。

2021-09-26_第5张图片

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的解析过程、数据库设置等等。

2021-09-26_第6张图片

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 等等,这些也记录在错误日志文件中。

2021-09-26_第7张图片

和隔离级别分别对应的三个问题

  • 第二类丢失更新:一个事务在提交的时候,覆盖了另一个事务已提交的更新数据
时间 事务A 事务B
T1 开启事务 开启事务
T2 查询账户余额:500元 查询账户余额:500元
T3 取走100元,剩余400元 取走100元,剩余400元
T4 提交事务,账户余额:400元 -
T5 - 提交事务,账户余额:400元
  • 脏读:一个事务读到了另一个事务未提交的更新数据

  • 幻读:一个事务读到了另一个事务已提交的新增数据

  • 不可重复读:一个事务读到了另一个事务已提交的更新数据

四种隔离级别

  • read uncommitted(未提交读):事务中的修改,即使没有提交,对其他事务也是可见的。事务可以读取未提交的数据,这也被称为脏读(Dirty Read)。
  • read committed(提交读):大部分数据库系统的默认隔离级别都是 read committed(但MySql不是)。一个事务从开始知道提交之前,所做的任何修改对其他事务都是不可见的。这个级别有时候也叫做不可重复读(nonrepeatable read),因为两次执行同样的查询,可能会得到不一样的结果。
  • repeatable read(可重复读):解决了脏读的问题。该级别保证了在同一个事务中多次读取同样记录的结果是一致的。但理论上,可重复读隔离级别还是无法解决幻读(Phantom Read)的问题。幻读就是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行(Phantom Row)。 可重复度时MySql的默认事务隔离级别。
  • serializable(可串行化):是最高的隔离级别。它通过强制事务串行执行,避免了前面说的幻读的问题。serializable会在读取的每一行数据上都加锁,所以可能导致大量的超时和锁争用得问题。实际应用中也很少用到这个隔离级别,只有在非常需要确保数据的一致性而且可以接受没有并发的情况下,才考虑采用该级别。

各种隔离级别能解决的问题对应如下:

隔离级别 是否出现第一类丢失更新 是否出现脏读 是否出现不可重复读 是否出现幻读/虚读 是否出现第二类丢失更新
Read Uncommited
Read Commited
Repeatable Read
Serializable

隔离级别是怎么实现的

事务的隔离级别是通过 读写锁+MVCC(多版本并发控制) 实现的

实现方式就是事务开始时,第一条select语句查询结果集会生成一个快照(snapshot),并且这个事务结束前,同样的select语句返回的都是这个快照的结果,而不是最新的查询结果,这就是MySQL在Repeatable Read隔离级别对普通select语句使用的快照读(snapshot read)。

快照读MVCC是什么关系呢?

​ MVCC是多版本并发控制,快照就是其中的一个版本。所以可以说MVCC实现了快照读,具体的实现方式涉及到MySQL的隐藏列。MySQL会给每个表自动创建三个隐藏列

  • DB_TRX_ID:事务ID,记录操作(增、删、改)该数据事务的事务ID
  • DB_ROLL_PTR:回滚指针,记录上一个版本的数据在undo log中的位置
  • DB_ROW_ID:隐藏ID ,创建表没有合适的索引作为聚簇索引时,会用该隐藏ID创建聚簇索引

​ 由于undo log中记录了各个版本的数据,并且通过DB_ROLL_PTR可以找到各个历史版本,并且由DB_TRX_ID决定使用哪个版本(快照)。所以相当于undo log实现了MVCC,MVCC实现了快照读。MySQL对insertupdatedelete语句所使用的当前读(current read)。因为涉及到数据的修改,所以MySQL必须拿到最新的数据才能修改,所以涉及到数据的修改肯定不能使用快照读(snapshot read)。

那么在Repeatable Read隔离级别是怎么解决幻读的呢?

​ 是通过间隙锁(Gap Lock)来解决的。我们都知道InnoDB支持行锁,并且行锁是锁住索引。而间隙锁用来锁定索引记录间隙,确保索引记录的间隙不变。间隙锁是针对事务隔离级别为Repeatable Read或以上级别而生效的,间隙锁和行锁一起组成了Next-Key Lock。当InnoDB扫描索引记录的时候,会首先对索引记录加上行锁,再对索引记录两边的间隙加上间隙锁(Gap Lock)。加上间隙锁之后,其他事务就不能在这个间隙插入记录。这样就有效的防止了幻读的发生。

加锁规则:两个”原则“、两个”优化“ 和一个”bug“

  • 原则1:加锁的基本单位是next-key lock。next-key lock是前开后闭区间。
  • 原则2:查找过程中访问到的对象才会加锁。
  • 优化1:索引上的等值查询,给唯一索引加锁的时候,next-key lock退化为行锁。
  • 优化2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock退化为间隙锁。
  • 一个bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。

redis的五种基本数据结构?

Redis是由C语言开发的一个开源的(遵从BSD协议)高性能键值对(key-value)的内存数据库,可以用作数据库、缓存、消息中间件等。它是一种Nosql(not-only-sql,泛指非关系型数据库)的数据库。Redis作为一个内存数据库的特点:

​ 速度快,完全基于内存,使用C语言实现,网络层使用epoll解决高并发问题,单线程模型避免了不必要的上下文切换及竞争条件。

  • 性能优秀,数据在内存中,读写速度非常快,支持并发10W QPS;
  • 单进程单线程,是线程安全的,采用IO多路复用机制;
  • 丰富的数据类型,支持字符串(String)、散列(Hash)、列表(List)、集合(Set)、有序集合(Sorted Set);
  • 支持数据持久化。可以将内存中数据保存在磁盘中,重启时加载;
  • 主从复制,哨兵,高可用;
  • 可以用作分布式锁;
  • 可以作为消息中间件使用,支持发布订阅。

基础数据结构如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2wnaVYYS-1632587894672)(D:\Desktop\知识整理\图片\image-20210722164747573.png)]

  • String(字符串)

    RedisString是可以修改的,称为动态字符串(Simple Dynamic String 简称 SDS),说是字符串但它的内部结构更像是一个 ArrayList,内部维护着一个字节数组,并且在其内部预分配了一定的空间,以减少内存的频繁分配。

    Redis的内存分配机制是这样:

    • 当字符串的长度小于 1MB时,每次扩容都是加倍现有的空间。
    • 如果字符串长度超过 1MB时,每次扩容时只会扩展 1MB 的空间。

    这样既保证了内存空间够用,还不至于造成内存的浪费,字符串最大长度为 512MB.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n5e5SuhF-1632587894673)(D:\Desktop\知识整理\图片\image-20210722164822608.png)]

上图就是字符串的基本结构,其中 buf 里面保存的是字符串内容,0x\0作为结束字符不会被计算len中。

字符串的数据结构:

/*  
 * 保存字符串对象的结构  
 */  
struct sdshdr {  
    int len;  // buf 中已占用空间的长度  
    int free;  // buf 中剩余可用空间的长度
    char buf[];  // 数据空间  
};
  • SDS 中 len 保存这字符串的长度,O(1) 时间复杂度查询字符串长度信息。
  • 空间预分配:SDS 被修改后,程序不仅会为 SDS 分配所需要的必须空间,还会分配额外的未使用空间。
  • 惰性空间释放:当对 SDS 进行缩短操作时,程序并不会回收多余的内存空间,而是使用 free 字段将这些字节数量记录下来不释放,后面如果需要 append 操作,则直接使用 free 中未使用的空间,减少了内存的分配。

字符串(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
  • List(列表)

Redis中的listJava中的LinkedList很像,底层都是一种链表结构, list的插入和删除操作非常快,时间复杂度为 0(1),不像数组结构插入、删除操作需要移动数据。

像归像,但是redis中的list底层可不是一个双向链表那么简单。

当数据量较少的时候它的底层存储结构为一块连续内存,称之为ziplist(压缩列表),它将所有的元素紧挨着一起存储,分配的是一块连续的内存;当数据量较多的时候将会变成quicklist(快速链表)结构。

可单纯的链表也是有缺陷的,链表的前后指针 prevnext 会占用较多的内存,会比较浪费空间,而且会加重内存的碎片化。在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它是一个按照插入顺序排序的列表,所以应用场景相对还较多的,例如:

  • 消息队列lpoprpush(或者反过来,lpushrpop)能实现队列的功能;
  • 朋友圈的点赞列表、评论列表、排行榜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))
  • Hash(字典)

Redis 中的 Hash和 Java的 HashMap 更加相似,都是数组+链表的结构,当发生 hash 碰撞时将会把元素追加到链表上,值得注意的是在 RedisHashvalue 只能是字符串.

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商品Idfield,商品数量为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

  • Set(集合)

Redis 中的 setJava中的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(有序集合)

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

redis的过期策略?

策略 描述
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)的数据淘汰

lru是属于主动淘汰还是被动淘汰?

Redis采用的是定期删除+惰性删除策略。

  • 定期删除策略

    Redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,默认每 100ms 进行一次过期扫描:

    • 1.随机抽取20个key
    • 2.删除这20个key中过期的key
    • 3.如果过期的key比例超过1/4,就重复步骤1,继续删除。

为什么不扫描所有的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.

树的种类:

  • 无序树: 树的任意节点的子节点没有顺序关系

  • 有序树:树的任意节点的子节点有顺序关系

  • 二叉树:树的任意节点至多包含两棵子树

  • 满二叉树:叶子节点都在同一层并且除叶子节点外的所有节点都有两个子节点

    img

  • 完全二叉树:对于一颗二叉树,假设其深度为d(d>1)。除第d层外的所有节点构成满二叉树,且第d层所有节点从左向右连续地紧密排列,这样的二叉树被称为完全二叉树

    img

  • 平衡二叉树(AVL):它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树,同时,平衡二叉树必定是二叉搜索树

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d3jrK488-1632587894676)(D:\Desktop\知识整理\图片\v2-28e39093993f673de576f57ea614d604_720w.jpg)]

  • 排序二叉树(二叉搜索树、二叉查找树、BST):是一种特殊结构的二叉树,通过它可以非常方便底对树中的所有节点进行排序和检索,排序二叉树要么是一棵空二叉树,要么是具有下列性质的二叉树

    • 若它的左子树不空,则左子树上所有节点的值均小于它的根节点的值;

    • 若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值;

    • 它的左、右子树也分别为排序二叉树;

      2021-09-26_第8张图片

    任意节点的没有键值相等的节点。

  • 哈夫曼树:带权路径最短的二叉树称为哈夫曼树或最优二叉树。哈夫曼树是二叉树的一种应用,在信息检索中很常用。在构建哈弗曼树时,要使树的带权路径长度最小,只需要遵循一个原则,那就是:权重越大的结点离树根越近。在图中,因为结点 a 的权值最大,所以理应直接作为根结点的孩子结点。

    介绍下一些相关概念:

    • 路径:在一棵树中,一个结点到另一个结点之间的通路,称为路径。图中,从根结点到结点 a 之间的通路就是一条路径。
    • 路径长度:在一条路径中,每经过一个结点,路径长度都要加 1 。例如在一棵树中,规定根结点所在层数为1层,那么从根结点到第 i 层结点的路径长度为 i - 1 。图中从根结点到结点 c 的路径长度为 3。
    • 结点的权:给每一个结点赋予一个新的数值,被称为这个结点的权。例如,图中结点 a 的权为 7,结点 b 的权为 5。
    • 结点的带权路径长度:指的是从根结点到该结点之间的路径长度与该结点的权的乘积。例如,图中结点 b 的带权路径长度为 2 * 5 = 10 。

    树的带权路径长度为树中所有叶子结点的带权路径长度之和。通常记作 “WPL” 。例如图 1 中所示的这颗树的带权路径长度为:

    WPL = 7 * 1 + 5 * 2 + 2 * 3 + 4 * 3

    2021-09-26_第9张图片

  • 红黑树:虽然排序二叉树可以快速检索,但在最坏的情况下,如果插入的节点集本身就是有序的,要么是由小到大排列,要么是由大到小排列,那么最后得到的排序二叉树将变成链表:所有节点只有左节点或者所有得节点只有右节点,此时得检索效率就会很低。为了改变排序二叉树存在得不足,Rudolf Bayerr于1972年发明了另一种改进后的排序二叉树------红黑树,红黑树是一颗特殊的排序二叉树,红黑树在原有的排序二叉树上增加了如下几个要求:

    • 性质1:每个节点要么是红色,要么是黑色;
    • 性质2:根节点永远是黑色的;
    • 性质3:所有的叶子节点都是空姐点(即null),并且都是黑色的;
    • 性质4:每个红色节点的两个子节点都是黑色的。(从每个叶子到根的路径上不会有两个连续的红色节点。)
    • 性质5:从任一节点到其子树中每个叶子节点的路径都包含相同数量黑色节点。
红黑树图例

​ 根据“性质5”,红黑树从根节点到每个叶子节点的路径都包含相同数量的黑色节点,因此从根节点到叶子节点的路径中包含的黑色节点数被称为树的“黑色高度(black-height)”。

​ “性质4”则保证了从根节点到叶子节点的最长路径的长度不会超过任何其他路径的2倍。假如有一棵黑色高度为3的红黑树,从根节点到叶子节点的最短路径长度是2,该路径上全是黑色节点(黑色节点----黑色节点----黑色节点)。最长路径也只可能为4,在每个黑色节点之间插入一 个红色节点(黑色节点----红色节点----黑色节点----红色节点----黑色节点), “性质4”保证绝不可能插入更多的红色节点。由此可见,红黑树中最长的路径就是一条红黑交 替的路径。
​ 由此可以得出结论:对于给定的黑色高度为N的红黑树,从根到叶子节点的最短路径长度为N-1, 最长路径长度为2*(N- 1)。

​ 红黑树通过上面这种限制来保证它大致是平衡的----因为红黑树的高度不会无限增高,这样能保证红黑树在最坏的情况下都是高效的,不会出现普通排序二叉树的情况。

​ 由于红黑树只是一棵特殊的排序二叉树,因此对红黑树上的只读操作与普通排序二叉树上的只读操作完全相同,只是红黑树保证了大致平衡,因此检索性能更好。

​ 但在红黑树进行插入操作和删除操作会导致树不在符合红黑树的特征,一次插入操作和删除操作都需要进行一定的维护,以保证插入节点、删除节点后的树依然是红黑树。

kafka可靠性实现,kafka选举controller机制(集群启动时、controller宕机之后)

Kafka学习之Kafka选举机制简述

Kafka是一个高性能,高容错,多副本,可复制的分布式消息系统。在整个系统中,涉及到多处选举机制,被不少人搞混,这里总结一下,本篇文章大概会从三个方面来讲解。

  • 控制器(Broker)选主
  • 分区多副本选主
  • 消费组选主

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又有多个副本,在整个集群中,总共有三种副本角色:

  • leader副本:也就是leader主副本,每个分区都有一个leader副本,为了保证数据一致性,所有的生产者与消费者的请求都会经过该副本来处理。
  • follower副本:除了首领副本外的其他所有副本都是follower副本,follower副本不处理来自客户端的任何请求,只负责从leader副本同步数据,保证与首领保持一致。如果leader副本发生崩溃,就会从这其中选举出一个leader。
  • 优先副本:创建分区时指定的优先leader。如果不指定,则为分区的第一个副本。

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。

3、消费组选主

在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中的第一个键值对,它和随机基本没啥区别。

Nginx 是如何实现高并发的?

异步,非阻塞,使用了epoll 和大量的底层代码优化。

如果一个server采用一个进程负责一个request的方式,那么进程数就是并发数。正常情况下,会有很多进程一直在等待中。

而nginx采用一个master进程,多个woker进程的模式。

  • master进程主要负责收集、分发请求。每当一个请求过来时,master就拉起一个worker进程负责处理这个请求。
  • 同时master进程也负责监控woker的状态,保证高可靠性
  • woker进程一般设置为跟cpu核心数一致。nginx的woker进程在同一时间可以处理的请求数只受内存限制,可以处理多个请求。

Nginx 的异步非阻塞工作方式正把当中的等待时间利用起来了。在需要等待的时候,这些进程就空闲出来待命了,因此表现为少数几个进程就解决了大量的并发问题。

2021-09-26_第10张图片

每进来一个request,会有一个worker进程去处理。但不是全程的处理,处理到什么程度呢?处理到可能发生阻塞的地方,比如向上游(后端)服务器转发request,并等待请求返回。那么,这个处理的worker很聪明,他会在发送完请求后,注册一个事件:“如果upstream返回了,告诉我一声,我再接着干”。于是他就休息去了。此时,如果再有request 进来,他就可以很快再按这种方式处理。而一旦上游服务器返回了,就会触发这个事件,worker才会来接手,这个request才会接着往下走。

多线程单进程代表 Nginx 为什么不使用多线程?

为什么不采用多线程模型管理连接?

  • 无状态服务,没有必要进行共享进程内存
  • 采用独立的进程,可以让互相之间不会影响。一个进程异常崩溃,其它进程的服务不会中断,提升了价格的可靠性
  • 进程之间不共享资源,不需要加锁,所以省掉了锁带来的开销

为什么不采用多线程处理逻辑业务?

  • 进程数已经等于核心数,再新建线程处理任务,只会抢占现有进程,增加切换代价
  • 作为接入层,基本上都是数据转发业务,网络IO任务的等待耗时部分,已经被处理为非阻塞,全异步,事件驱动模式,在没有更多cpu的情况下,再利用多线程处理,意义不大

并且,如果进程中有阻塞的处理逻辑,应该由各个业务进行解决,比如OpenResty中利用了lua协程,对阻塞业务进行了优化。

Nginx: 采用单线程来异步非阻塞处理请求(管理员可以配置Nginx主进程的工作进程的数量)(epoll),不会为每个请求分配cpu和内存资源,节省了大量资源,同时也减少了大量的CPU的上下文切换。所以才使得Nginx支持更高的并发。

TCP什么时候发送复位包

RST位为1时,表示TCP连接中出现异常必须强制断开连接。

  • 三次连接中,如果客户端收到服务端发来的因网络延迟的旧的连接,客户端会发送RST报文终止历史连接;
  • SYN攻击中,如果超出处理能力时,可对新的SYN直接回报文RST,丢弃连接;
  • 当服务器端处于LASE_ACK状态时,收到新的连接,服务端会发送RST报文给客户端,连接建立的过程就会被终止。等等

TCP头部字段

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UxfIJ2LS-1632587894678)(D:\Desktop\知识整理\图片\image-20210830153355314.png)]

TCP心跳包机制

心跳机制:定时发送一个自定义的结构体(心跳包),让对方知道自己还活着,让对方知道自己还活着,以确保连接的有效性的机制。

心跳检测步骤:

  1. 客户端每隔一个时间间隔发送一个探测包(心跳或者心跳帧)给服务器;
  2. 客户端发包时启动一个超时定时器;
  3. 服务器端接收到检测包,应该回应一个包;
  4. 如果客户端接收到检测包,则说明服务器正常,删除超时定时器;
  5. 如果客户端的超时定时器超时,依然没有收到应答包,则说明服务器挂了。

网络中的接收和发送数据都是使用操作系统中的SOCKET进行实现。

心跳包的发送,通常有两种技术

  • 方法1:应用层自己实现的心跳包
    由应用程序自己发送心跳包来检测连接是否正常,大致的方法是:服务器在一个 Timer事件中定时 向客户端发送一个短小精悍的数据包,然后启动一个低级别的线程,在该线程中不断检测客户端的回应, 如果在一定时间内没有收到客户端的回应,即认为客户端已经掉线;同样,如果客户端在一定时间内没 有收到服务器的心跳包,则认为连接不可用。
  • 方法2:TCP的KeepAlive保活机制
    因为要考虑到一个服务器通常会连接多个客户端,因此由用户在应用层自己实现心跳包,代码较多 且稍显复杂,而利用TCP/IP协议层为内置的KeepAlive功能来实现心跳功能则简单得多。 不论是服务端还是客户端,一方开启KeepAlive功能后,就会自动在规定时间内向对方发送心跳包, 而另一方在收到心跳包后就会自动回复,以告诉对方我仍然在线。 因为开启KeepAlive功能需要消耗额外的宽带和流量,所以TCP协议层默认并不开启KeepAlive功 能,尽管这微不足道,但在按流量计费的环境下增加了费用,另一方面,KeepAlive设置不合理时可能会 因为短暂的网络波动而断开健康的TCP连接。并且,默认的KeepAlive超时需要7,200,000 MilliSeconds, 即2小时,探测次数为5次。对于很多服务端应用程序来说,2小时的空闲时间太长。因此,我们需要手工开启KeepAlive功能并设置合理的KeepAlive参数。

虚拟地址和物理地址转换

两种地址的概念:

  • 我们程序所使⽤的内存地址叫做虚拟内存地址Virtual Memory Address
  • 实际存在硬件⾥⾯的空间地址叫物理内存地址Physical Memory Address)。

操作系统引⼊了虚拟内存,进程持有的虚拟地址会通过 CPU 芯⽚中的内存管理单元(MMU)的映射关系,来转换变成物理地址,然后再通过物理地址访问内存,如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-amaVgMrK-1632587894679)(D:\Desktop\知识整理\图片\image-20210830160742563.png)]

系统管理虚拟地址与物理地址的方式:内存分段内存分页,分段是比较早提出的。

内存分段

程序是由若⼲个逻辑分段组成的,如可由代码分段、数据分段、栈段、堆段组成。不同的段是有不同的属性的,所以就⽤分段(Segmentation)的形式把这些段分离出来。

分段机制下,虚拟地址和物理地址是如何映射的?

分段机制下的虚拟地址由两部分组成,段选择⼦段内偏移量

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SieK2V04-1632587894679)(D:\Desktop\知识整理\图片\image-20210830161110102.png)]

  • 段选择子就是保存在段寄存器里面。段选择子里面最重要的是短号,用作段表的索引。段表里面保存的是这个段的基地址、段的界限和特权等级等。
  • 虚拟地址中的段内偏移量应该位于0段和段界限之间,如果段内偏移量是合法的,就将段基地址加上段内偏移量得到物理内存地址。

有点:解决了程序本身不需要关系具体的物理内存的问题;

缺点:

  1. 内存碎片;
    • 外部内存碎片,也就是产生了多个不连续的小物理内存,导致新的程序无法被装载;
    • 内部内存碎片,程序所有的内存都被装载到了物理内存,但是这个程序有部分的内存可能并不是很常使用,这也导致内存的浪费。
  2. 内存交换的效率低。

解决外部内存碎⽚的问题就是内存交换

可以把⾳乐程序占⽤的那 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内存主要采用的是页式内存管理,但同时也不可避免的涉及了段机制。

进程和线程区别

  • 进程是资源分配的最小单位;线程是程序执行的最小单位(资源调度的最小单位)。
  • 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵;而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。
  • 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行;不过如何处理好同步与互斥是编写多线程程序的难点。
  • 但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。

Java中的弱引用、强引用、软引用和虚引用是什么,他们分别在哪些场景中使用

  • 强引用(StrongReference),我们平常典型编码 Object obj=newObject() 中的obj就是强引用。通过关键字new创建的对象所关联的引用就是强引用。强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当JVM 内存空间不足,JVM 宁愿抛出OutOfMemoryError运行时错误(OOM),使程序异常终止,也不会靠随意回收具有强引用的“存活”对象来解决内存不足的问题。只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。对于一个普通的对象,如果没有其他的引用关系,只要超出对象的生命周期范围或者显式地将相应(强)引用赋值为null,就是可以被垃圾收集的了,当然具体回收时机还是要看垃圾收集策略。
  • 软引用(SoftReference),是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,**只有当JVM 认为内存不足时,才会去试图回收软引用指向的对象。**JVM 会确保在抛出OutOfMemoryError之前,清理软引用指向的对象。软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。后续,我们可以调用ReferenceQueue的poll()方法来检查是否有它所关心的对象被回收。如果队列为空,将返回一个null,否则该方法返回队列中前面的一个Reference对象。【应用场景】:软引用通常用来实现内存敏感的缓存。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
  • 弱引用通过WeakReference类实现。弱引用的生命周期比软引用短。在垃圾回收器线程扫描它所管辖的内存区域的过程中,**一旦发现了具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。**由于垃圾回收器是一个优先级很低的线程,因此不一定会很快回收弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。【应用场景】:弱应用同样可用于内存敏感的缓存。
  • 虚引用,你不能通过它访问对象。幻象引用仅仅是提供了一种确保对象被finalize以后,做某些事情的机制。虚引用只是用来得知对象是否被GC。**如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用必须和引用队列(ReferenceQueue)联合使用。**当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。【应用场景】:可用来跟踪对象被垃圾回收器回收的活动,当一个虚引用关联的对象被垃圾收集器回收之前会收到一条系统通知。

通过表格来说明一下,如下:

引用类型 被垃圾回收时间 用途 生存时间
强引用 从来不会 对象的一般状态 JVM停止运行时终止
软引用 在内存不足时 对象缓存 内存不足时终止
弱引用 在垃圾回收时 对象缓存 gc运行后终止
虚引用 任何时候 跟踪对象被垃圾回收器回收的活动 Unknown

如何设计一个LRU缓存?如何将时间复杂度降到O(1)

RFU算法的精妙所在是使用一种叫作 哈希链表 的数据结构。

我们都知道,哈希表是由若干个Key-Value组成的。在“逻辑”上, 这些Key-Value是无所谓排列顺序的,谁先谁后都一样。

在哈希链表中,这些Key-Value不再是彼此无关的存在,而是被一 个链条串了起来。每一个Key-Value都具有它的前驱Key-Value、后继 Key-Value,就像双向链表中的节点一样。

让我们以用户信息的需求为例,来演示一下LRU算法的基本思路。

  1. 假设使用哈希链表来缓存用户信息,目前缓存了4个用户,这4个 用户是按照被访问的时间顺序依次从链表右端插入的。

  2. 如果这时业务方访问用户5,由于哈希链表中没有用户5的数据, 需要从数据库中读取出来,插入到缓存中。此时,链表最右端是最新被 访问的用户5,最左端是最近最少被访问的用户1。

  3. 接下来,如果业务方访问用户2,哈希链表中已经存在用户2的数 据,这时我们把用户2从它的前驱节点和后继节点之间移除,重新插入 链表的最右端。此时,链表的最右端变成了最新被访问的用户2,最左 端仍然是最近最少被访问的用户1。

  4. 接下来,如果业务方请求修改用户4的信息。同样的道理,我们 会把用户4从原来的位置移动到链表的最右侧,并把用户信息的值更 新。这时,链表的最右端是最新被访问的用户4,最左端仍然是最近最 少被访问的用户1。

  5. 后来业务方又要访问用户6,用户6在缓存里没有,需要插入哈希链表中。假设这时缓存容量已经达到上限必须先删除最近最少被访问 的数据,那么位于哈希链表最左端的用户1就会被删除,然后再把用户6 插入最右端的位置。

java和go的区别

什么是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为左闭右开
}

一道算法题:数组中只有0,1,2三种数字,将其排序,立刻想到计数排序,面试官说如果只允许空间复杂度O(1),时间复杂度O(n),且只能遍历一遍,怎么做?

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;
    }

你可能感兴趣的:(大厂面经,kafka,zookeeper)