一、网络面试核心知识
1.网络基础知识
OSI开放式互联参考模型
1.物理层 - 2.数据链路层 - 3.网络层 - 4.传输层 - 5.会话层 - 6.表示层 - 7.应用层
先自上而下,后自下而上处理数据头部

TCP/IP
OSI的“实现”:TCP/IP

2.TCP的三次握手
- 传输控制协议TCP简介
- 面向连接的,可靠的,基于字节流的传输层通信协议
- 将应用层的数据流分割成报文段病发送给目标节点的TCP层
- 数据包都有序号,对方收到则发送ACK确认,未收到则重传
- 使用校验和来检验数据在传输过程中是否有误
数据传输时应用层向TCP发送数据流,然后TCP把数据流分割成适当长度的报文段,报文段的长度呢,通常受该计算机连接的网络的数据链路成的最大传输单元及AMPU的限制。之后呢,TCP把结果包传给IP层,由它来通过网络将包传送给目标节点的TCP层。TP为了保证不丢失包,就给每个包一个序号及sequence number,同时呢序号也保证了传送到目标节点的包的按序处理,然后接收端子实体对已成功收到的包呢发回一个相应的确认及ACK确认。如果发送端实体在合理的往返及RCT内未收到确认,那么对应的数据包就会被假设为已丢失,并且呢将会对其进行重传。TCP用一个机有校验函数呢来检验数据是否有错误,在发送和接收时呢,都要计算校验。
- TCP报文头/头部结构:

- Source Port (源端口) 和 Destination Port (目的端口):用于标识通信中的源和目的端口。
- Sequence Number (序列号):指示TCP连接中传输的字节流中每个字节的顺序编号。
- Acknowledgment Number (确认号):表示期望收到对方下一个报文的第一个数据字节的序号。
- Offset (偏移):指出TCP头部长度,因为TCP头部长度是可变的,所以用该字段来确定数据开始的位置。
- Reserved (保留):保留用于将来使用,目前通常被置为0。
- Control Bits (控制位)
- SYN (同步):用于建立连接。
- ACK (确认):用于确认收到的数据。
- RST (重置):用于重置连接标志。
- FIN (结束):用于关闭连接。
- URG:紧急指针标志
- PSH:push标志
- TCP三次握手过程:
- 客户端向服务器发出连接请求:客户端发送一个带有SYN标志的数据包给服务器,同时选择一个初始序列号seq。
- 服务器接收连接请求并确认:服务器收到请求后,回应一个带有SYN和ACK标志的数据包给客户端,同时选择一个初始序列号seq,并确认收到客户端的序列号。
- 客户端确认服务器的回应:客户端收到服务器的回应后,发送一个带有ACK标志的数据包给服务器,确认收到服务器的序列号。

为什么需要三次握手才能建立起连接?
TCP需要进行三次握手来建立连接的主要原因是确保双方正确初始化序列号(Sequence Number)。通信双方需要相互通知对方自己的初始化序列号,这个序列号将作为以后数据通信的序号,以保证数据在传输过程中能够按正确顺序被接收。因此,在服务器回发其序列号后,客户端需要发送确认报文给服务器,告知服务器已经收到了初始化的序列号。
首次握手的隐患---SYN超时
在第一次握手时存在一个隐患,即超时问题。、
1.server收到Client的SYN,恢复Syn-ACK的时候未收到ACK确认。
如果服务器端收到客户端的SYN后回复了SYN+ACK,但客户端在回复ACK之前掉线,服务器端将不会收到客户端的确认。这将导致连接处于一个中间状态,即半连接状态,无法成功建立也无法失败。为了应对这个问题,服务器端会在一定时间内等待客户端的确认,如果超时,则会重新发送SYN+ACK,直到达到最大重试次数。
2.Server不断重试直至超时,Linux默认等待63秒才断开连接
在Linux系统下,默认重试次数为5次,重试间隔从1秒开始,每次翻倍,直到第5次发送后需要等待32秒才会判定为超时,共计等待63秒。这可能导致服务器受到SYN Flood攻击的风险,攻击者可以利用此漏洞消耗服务器的连接队列,使其无法处理正常的连接请求。
3.针对SYN Flood的防护措施
- SYN队列满后,通过tcp_syncookies参数回发SYN Cookie
- 若正常链接则Client会回发SYN Cookie,直接建立连接
为了解决这个问题,TCP引入了一个名为TCP Cookie的参数。当连接队列满时,服务器会回复一个特殊的序列号,称为新Cookie。如果是正常连接,则客户端会回复这个新Cookie,然后服务器可以通过Cookie建立连接。通过TCP Cookie,即使连接队列已满,依然可以建立连接,从而解决了这个问题。
此外,如果已经建立连接但客户端突然出现故障,TCP还设有保活机制。在一段饱和时间内,连接处于非活动状态,一端会向对方发送保活探测报文。如果发送端没有收到响应,将继续发送保活探测报文,直到达到饱和探测次数,对方主机将被确认为不可达,连接被中断。
3.TCP的四次挥手
挥手是为了终止连接。TCP四次挥手流程图如下

第一次挥手(FIN from Client):
- 客户端发送一个FIN包到服务器,用于关闭客户到服务器的数据传输。
- 客户端进入FIN_WAIT_1状态。
第二次挥手(ACK from Server):
- 服务器收到FIN后,发送一个ACK包作为响应。
- 服务器进入CLOSE_WAIT状态,客户端进入FIN_WAIT_2状态。
第三次挥手(FIN from Server):
- 服务器完成数据传输后,发送一个FIN包给客户端,请求关闭服务器到客户端的数据传输。
- 服务器进入LAST_ACK状态。
第四次挥手(ACK from Client):
- 客户端收到FIN后,发送一个ACK包响应。
- 客户端进入TIME_WAIT状态,等待足够的时间以确保服务器接收到最终的ACK包。
为什么需要四次挥手:
- TCP连接是全双工的,意味着每个方向的连接都必须单独终止,所以需要双方各自发送FIN和ACK包。
- 确保数据完整传输:通过等待足够的时间(2MSL,Maximum Segment Lifetime),确保对方接收到所有包,并且避免可能的包重复。
TIME_WAIT状态的原因:
- 确保ACK包被对方接收,如果服务器未收到最终的ACK,它将重发FIN包。(确保有足够的时间让对方收到ACK包)
- 防止旧连接的数据包在新连接中出现。(避免新旧连接混淆)
关于CLOSE_WAIT状态:
- 如果出现大量的CLOSE_WAIT状态,可能是因为服务器端没有正确关闭连接,或者是由于程序BUG导致连接没有被及时释放。
- 检查代码,提别是释放资源的代码
- 检查配置,特别是处理请求的线程配置
检查和处理CLOSE_WAIT状态:
- 使用
netstat命令检查网络连接状态,特别是CLOSE_WAIT状态的连接。 - 如果CLOSE_WAIT数量异常高,需要检查服务端的应用日志,定位和修复没有正确关闭TCP连接的问题。
4.TCP和UDP的区别
UDP报文结构
UDP(用户数据报协议)的报文结构
非常简单,主要由四个部分组成:源端口号、目的端口号、长度和校验和。这种设计旨在提供尽可能简洁的传输方式,以支持那些不需要TCP复杂控制机制的应用。下面是UDP报文的详细结构:
源端口号(16位):这是一个可选字段,用于指定发送端应用程序的端口号。如果未使用,则设置为零。这个字段使得接收端的应用程序能够将数据发回发送端的正确应用程序。
目的端口号(16位):这个字段指定接收端应用程序的端口号。这是数据包应该被送达的地方。
长度(16位):这个字段指定了UDP头部和数据总共占用的字节数。最小值是8字节(仅头部时)。由于这个字段的存在,UDP数据报的最大长度被限制为65535字节,但是由于IP层有自己的长度限制,实际上UDP数据报的实际最大长度通常会小于这个值。
校验和(16位):用于错误检测,包括头部和数据。它是一个可选字段,在IPv4中可以设置为零表示不使用,但在IPv6中则是必需的。校验和计算包括UDP报文自身和一个伪头部,伪头部包含了源地址、目的地址、协议类型和UDP长度信息,这样设计是为了提供额外的保护,确保数据报被送到正确的目的地。
这种简洁的结构使得UDP非常适合那些对时间敏感或可以容忍一定数据丢失的应用,例如视频播放、在线游戏和语音通信。UDP不像TCP那样提供可靠性保证,如数据排序、重传控制或连接状态的维护,但它的简洁性使得数据传输延迟更小,处理更高效。
特点:
- 面向非连接
- 不维护连接状态,支持同时向多个客户端传输相同的消息
- 数据包报头只有8个字节,额外开销较小
- 吞吐量只收与数据生成速率、传输速率以及机器性能
- 尽最大努力交付,不保证可靠交付,不需要维持复杂的链接状态表
- 面向报文,不对应用程序提交的报文信息进行拆分或者合并
连接方式:
- TCP:面向连接,必须先建立连接才能通信。
- UDP:无连接,发送数据前不需要建立连接。(单点向多点)
可靠性:
- TCP:提供高可靠性,通过序号、确认应答、重传等机制确保数据正确传输。
- UDP:尽最大努力交付,不保证数据的可靠传输。
有序性:
- TCP:保证数据按照发送顺序到达。
- UDP:数据到达顺序不定,可能乱序。
速度/效率:
- TCP:相对较慢,因为建立连接、保证数据可靠性和顺序性需要时间和资源。
- UDP:较快,适合需要快速传输的应用,如视频会议、直播等。
数据报头部大小:
- TCP:较大的头部,最少20字节。
- UDP:较小的头部,仅8字节。
用途:
- TCP:适用于需要可靠传输的应用,如网页浏览、文件传输、电子邮件等。
- UDP:适用于实时应用,如流媒体、在线游戏、语音和视频通信等,这些应用更注重速度而非每一个数据包的准确性。
拥塞控制:
- TCP:有拥塞控制机制,可以根据网络状况调整数据发送速率,防止网络拥塞。
- UDP:没有拥塞控制,应用需要自己处理可能的网络拥塞问题。
5.TCP的滑动窗口
RTT和RTO
- RTT(Round-Trip Time):指的是发送一个数据包到收到对应的确认(ACK)所花费的时间。
- RDT(Round-Trip Delay Time):是指从发送数据包到收到对应的确认所经历的时间。简而言之,就是发送数据包到接收确认ACK的时间间隔。
RTO(重传超时)与重传机制
- RTO(Retransmission Timeout):即重传超时,是指TCP在发送一个数据包之后启动的重传定时器时间。该时间由R(RDT)计算而来,用于确定重传的时间。
- 如果在RTO之前收到了ACK,重传定时器就会失效;如果未收到ACK而RTO时间到达,就会触发重传操作。
TCP滑动窗口的作用
- TCP滑动窗口用于流量控制和乱序重排,以提供可靠性和效率。
- 其主要作用包括提供TCP的可靠性和流量控制特性。
TCP窗口的计算过程
- 发送端和接收端各自有一个窗口,用于管理数据的发送和接收。
- 发送端窗口由last byte sent、last byte acked和last byte written组成。
- 接收端窗口由byte read、next byte expected和last byte received组成。
TCP滑动窗口及相关原理详解

接收端缓存值与窗口管理
- 接收的最大数据量:即接收端缓存值的大小,表示接收端能够缓存的最大数据量。
- last byte received 和 byte read:表示接收端已经接收到的数据或者预留给尚未接收到的数据的空间。
- 通过计算未被占据的缓存空间,可以确定接收端还能够接收的数据量,并将其通过advertise window通知发送端。
发送端窗口的管理与流量控制
- 发送端窗口可分为四类:已发送且已确认、已发送但未确认、未发送但允许发送、未发送且窗口已满。
- 发送窗口的大小由发送端管理,确保已发送但未确认的数据不超过接收端的window大小。
- 发送端根据接收端的advertise window来确定剩余可发送的数据量(effective window),以实现流量控制。
滑动窗口的基本原理
TCP会话的接收方

- 发送窗口和接收窗口均可滑动,滑动窗口是由已发送但未确认的数据和允许发送的数据组成的连续空间。
- 窗口只有在前面的数据被确认后才会移动,确保数据的传输可靠性和顺序性。
- TCP通过确认重传机制和窗口滑动来实现可靠的数据传输,发送窗口和接收窗口的移动均受到严格控制,以确保数据的正确性和可靠性。
动态调整窗口大小与流量控制
- 窗口大小可以根据策略进行动态调整,以适应网络环境的变化和端点的处理能力。
- 应用可以通过调整TCP接收窗口大小来限制发送端的发送速率,实现对发送端的流量控制。
6.HTTP相关
HTTP协议概述
HTTP(HyperText Transfer Protocol)是一个基于请求与响应模式的无状态的应用层协议,常基于TCP的连接方式。它是应用在Web开发中最为广泛的协议之一,用于客户端和服务器之间的通信。
主要特点:
支持客户服务模式:HTTP工作于客户端-服务器架构,浏览器作为HTTP客户端向服务器发送请求,服务器根据请求返回相应的数据。

简单快速:HTTP协议简单,传输速度快。客户端只需传送请求方法和路径,服务器只需传送响应状态和数据。
灵活:HTTP允许传输任意类型的数据对象,数据类型由Content-Type字段指定。
无连接:HTTP协议默认使用无连接的方式,即每次连接只处理一个请求。HTTP/1.1引入了持久连接机制(Keep-Alive),使得可以复用同一个连接来发送多个请求。
无状态:HTTP协议是无状态协议,即协议对于处理请求没有记忆能力,每个请求都是独立的。如果后续处理需要前面的信息,则必须重新传输。
HTTP请求结构:

HTTP请求由请求行、请求头部、空行和请求数据(可选)组成。
- 请求行:包括请求方法、URL和协议版本。
- 请求头部:包含若干个键值对,用于设置请求的一些参数。
- 空行:请求头部与请求数据之间必须有一个空行。
- 请求数据:请求的数据内容,比如POST请求中发送的数据。
HTTP响应结构:

HTTP响应由状态行、响应头部、空行和响应数据组成。
- 状态行:包括协议版本、状态码和状态描述。
- 响应头部:包含若干个键值对,用于设置响应的一些参数。
- 空行:响应头部与响应数据之间必须有一个空行。
- 响应数据:响应的数据内容,比如返回的HTML文档。
请求响应的步骤
- 客户端连接到web服务器
- 发送HTTP请求
- 服务器接收请求并返回HTTP响应
- 释放连接TCP连接
- 客户端浏览器解析HTML内容
HTTP状态码:
HTTP状态码由三位数字组成,第一个数字定义了响应的类别,常见的状态码有:
- 1xx:信息性状态码,表示请求已接收,继续处理。
- 2xx:成功状态码,表示请求已成功接收、理解并被接受。
- 3xx:重定向状态码,表示请求要完成需要进一步操作。(跳转)
- 4xx:客户端错误状态码,表示客户端出现错误请求。
- 5xx:服务器错误状态码,表示服务器未能实现合法的请求。
浏览器地址栏输入URL的经历流程:(重要)
- 浏览器查询DNS缓存,解析URL中的域名对应的IP地址。
- 建立TCP连接,连接到目标服务器的HTTP端口,默认是80端口。
- 发送HTTP请求报文,包括请求方法、URL、协议版本、请求头部和请求数据。
- 服务器接收并处理请求,返回HTTP响应报文,包括状态行、响应头部和响应数据。
- 浏览器接收响应,根据状态码判断请求是否成功,解析响应头部和响应数据。
- 渲染HTML内容并在浏览器窗口中显示。
- 释放TCP连接,关闭与服务器的连接。
GET请求和POST请求的区别
GET请求和POST请求的区别在于它们在HTTP报文中的传输方式和安全性方面有所不同。
- HTTP报文层面:GET将请求信息放在URL,POST放在报文体重
- 数据库层面:CET符合幂等性和安全性,POST不符合
- 其他层面:GET可以被缓存,被存储,而POST不行
GET请求信息放置在URL后面,参数和URL之间以问号隔开,因此请求信息直接暴露在URL中,易于被获取。由于数据在URL中传输,长度有限制。GET请求适合于查询操作,因为它不会改变服务器端的数据,符合幂等性和安全性。GET请求也可以被缓存,可以保存在浏览器历史记录和书签中,减少服务器负担。
POST请求则将请求信息放置在请求体中,因此参数不会暴露在URL中,相对更安全。POST请求没有长度限制,适合于提交数据,如登录表单等。但是由于每次请求都可能改变服务器端的数据,不符合幂等性和安全性。POST请求不能被缓存,也不能保存在浏览器历史记录或书签中。
GET请求适合于查询操作,并具有幂等性和安全性,可以被缓存;而POST请求适合于提交数据,不具有幂等性和安全性,不能被缓存。
Cookie和Session的区别?
Cookie简介:
- 是由服务器发给客户端的特殊信息,以文本的形式存放在客户端
- 客户端再次请求的时候,会把Cookie回发
- 服务器接收到后,会解析Cookie生成与客户端相对应的内容
Cookie技术是客户端的解决方案,它是由服务器发送给客户端的特殊信息,以文本文件的形式存放在客户端。每次客户端向服务器发送请求时,都会携带这些特殊信息。这些信息通常包括用户的个人信息等。Cookie的设置和发送过程可以分为四步:客户端发送HTTP请求到服务器;服务器发送HTTP响应到客户端,其中包括了Set-Cookie头部;客户端再次发送HTTP请求到服务器,包括了Cookie头部;服务器端发送HTTP响应到客户端。
Cookie的设置以及发送过程

Session简介
- 服务器端的机制,在服务器上保存的信息
- 解析客户端请求,并操作session id,按需保存状态信息即可
Session的实现方式
- 使用Cookie来实现
- 使用URL回写来实现

Session机制是一种服务器端的机制,服务器使用类似于散列表的结构来保存信息。当程序需要为某个客户端的请求创建一个Session时,服务器会首先检查客户端的请求是否包含了一个Session标识,称为Session ID。如果已包含一个Session ID,则说明以前已经为此客户端创建过Session,服务器就会检索出来使用。如果检索不到,则可能会新建一个。
Session的实现方式有两种:
一种是使用Cookie来实现,服务器给每个Session分配一个唯一的Session ID,并通过Cookie发送给客户端;
另一种是使用URL重写来实现,即在发送给浏览器的页面中,所有的链接都携带Session ID的参数。
Session的持久化通常在一定时间内保存在服务器上,但会占用服务器的资源。
Cookie和Session的区别主要有以下三点:
- 数据存放位置:Cookie数据存放在客户端的浏览器上,而Session数据存放在服务器上。
- 安全性:Cookie相对不太安全,因为存放在客户端,可以被分析和欺骗;而Session相对更安全,因为数据存储在服务器端。
- 存储时效性:Cookie可以设置过期时间,而Session会在一定时间内保存在服务器上。
7.HTTP和HTTPS的区别
HTTPS简介
**HTTPS(HyperText Transfer Protocol Secure)是一种基于HTTP的安全传输协议,它通过SSL(Secure Sockets Layer)或TLS(Transport Layer Security)协议来加密HTTP通信,提供了更安全的数据传输方式。**HTTPS在HTTP的基础上加入了加密、身份验证和数据完整性保护等安全机制,从而有效防止了数据被窃听、篡改或伪造的风险

SSL简介
SSL(Secure Sockets Layer)是一种用于保护网络通信安全的协议,它位于TCP/IP协议栈与应用层之间,为应用层提供了安全性和数据完整性保护。SSL协议的主要目的是确保在网络上进行的数据传输过程中,数据不被窃听、篡改或伪造。
- 为网络通信通过安全及数据完整性的一种安全协议
- 是操作系统对外提供的API,SSL3.0后更名为TLS
- 采用身份验证和数据加密保证网络通信的安全和数据的完整性
加密的方式
- 对称加密:加密和解密都使用同一个密钥
- 非对称加密:加密使用的密钥和解密使用的密钥是不相同的
- 哈希算法:将任意长度的信息转换为固定长度的值,算法不可逆
- 数字签名:证明某个消息或者文件是某人发出/认同的
HTTPS数据传输流程
- 浏览器将支持的加密算法信息发送给服务器
- 服务器选择一套浏览器支持的加密算法,以证书的形式回发浏览器
- 浏览器验证证书合法性,并结合证书公钥加密信息发送给浏览器
- 服务器使用私钥解密信息,验证哈希,加密响应消息回发浏览器
- 浏览器解密响应消息,并对消息进行验真,之后进行加密交互数据
浏览器首先将支持的加密算法信息发送给服务器。服务器选择一套浏览器支持的加密算法,并将其以证书的形式回发给浏览器。浏览器验证证书的合法性,并结合证书的公钥加密信息发送给服务器。服务器使用私钥解密信息,验证哈希,并加密响应消息回发给浏览器。最后,浏览器解密响应消息,并对消息进行验真,然后进行加密交互数据。
HTTP和HTTPS的区别?
- 安全性: HTTP是不安全的传输协议,数据以明文形式传输,容易被黑客截获和篡改;而HTTPS通过SSL/TLS加密传输数据,保护数据的隐私和完整性,提供更高的安全性。
- 加密方式:HTTPS = HTTP + 加密 + 认证 + 完整性保护,较HTTP安全
- 证书验证: HTTPS需要网站服务器使用SSL证书进行身份验证,确保通信双方的身份合法性;而HTTP没有证书验证的过程,容易受到中间人攻击。
- 端口和连接方式: HTTP默认使用80端口进行通信,连接方式简单,并且是无状态的;而HTTPS默认使用443端口进行通信,采用SSL/TLS协议进行加密传输,并且是有状态的。
- CA证书: HTTPS协议需要到CA机构申请SSL证书,而HTTP不需要
HTTPS真的很安全吗?
- 浏览器默认填充http://,请求需要进行跳转,有被劫持的风险
- 可以使用HSTS进行优化
Socket简介
Socket是对TCP和IP协议的抽象,是操作系统对外开放的接口

Socket的通信流程

当进行Socket通信时,首先需要创建Socket对象,客户端和服务器端各自创建一个。接着,客户端通过指定服务器端的IP地址和端口号,发起连接请求,而服务器端则监听指定的端口,等待客户端的连接请求。一旦建立了连接,双方就可以开始进行通信。数据的传输是通过输入流和输出流来实现的,客户端通过输出流向服务器端发送数据,服务器端通过输入流接收数据,反之亦然。通信完成后,双方可以关闭它们的Socket连接,释放网络资源,结束通信过程。这就是Socket通信的基本流程。
题目:编写一个网络应用程序,有客户端与服务端,客户想服务器发送一个字符串,服务器收到该字符串后将其打印到命令行上,然后向客户端返回该字符串的长度,最后,客户端输出服务器端返回的改字符串的长度,分别用TCP和UDP两种方式去实现。
TCP实现步骤:
- 客户端首先创建一个TCP Socket,并指定服务器的IP地址和端口号。
- 客户端向服务器发送待发送的字符串。
- 服务器端接收到客户端发送的字符串后,在命令行上打印该字符串。
- 服务器端获取字符串的长度,并将长度信息发送给客户端。
- 客户端接收服务器返回的字符串长度,并在命令行上打印。
TCP服务器端代码
// 服务器端代码
import java.io.*;
import java.net.*;
public class TCPServer {
public static void main(String[] args) {
try {
// 创建服务器Socket,指定端口号
ServerSocket serverSocket = new ServerSocket(8888);
System.out.println("服务器已启动,等待客户端连接...");
// 监听客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("客户端已连接");
// 获取客户端输入流
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
// 获取客户端输出流
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
// 读取客户端发送的数据
String message = in.readLine();
System.out.println("客户端发送的消息:" + message);
// 将消息长度发送给客户端
out.println(message.length());
// 关闭连接
in.close();
out.close();
clientSocket.close();
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
TCP客户端代码
// 客户端代码
import java.io.*;
import java.net.*;
public class TCPClient {
public static void main(String[] args) {
try {
// 创建客户端Socket,指定服务器地址和端口号
Socket clientSocket = new Socket("localhost", 8888);
// 获取客户端输入流
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
// 获取客户端输出流
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
// 向服务器发送数据
String message = "Hello, Server!";
out.println(message);
// 读取服务器返回的数据
int length = Integer.parseInt(in.readLine());
System.out.println("服务器返回的消息长度:" + length);
// 关闭连接
in.close();
out.close();
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
UDP实现步骤:
- 客户端首先创建一个UDP Socket。
- 客户端向服务器发送待发送的字符串。
- 服务器端接收到客户端发送的字符串后,在命令行上打印该字符串。
- 服务器端获取字符串的长度,并将长度信息发送给客户端。
- 客户端接收服务器返回的字符串长度,并在命令行上打印。
UDP服务器端代码
// 服务器端代码
import java.io.*;
import java.net.*;
public class UDPServer {
public static void main(String[] args) {
try {
// 创建服务器DatagramSocket,指定端口号
DatagramSocket serverSocket = new DatagramSocket(8888);
System.out.println("服务器已启动,等待客户端连接...");
// 创建接收数据的DatagramPacket对象
byte[] receiveData = new byte[1024];
DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
// 接收客户端发送的数据
serverSocket.receive(receivePacket);
String message = new String(receivePacket.getData(), 0, receivePacket.getLength());
System.out.println("客户端发送的消息:" + message);
// 创建发送数据的DatagramPacket对象
byte[] sendData = String.valueOf(message.length()).getBytes();
InetAddress clientAddress = receivePacket.getAddress();
int clientPort = receivePacket.getPort();
DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, clientAddress, clientPort);
// 发送数据到客户端
serverSocket.send(sendPacket);
// 关闭连接
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
UDP客户端代码
// 客户端代码
import java.io.*;
import java.net.*;
public class UDPClient {
public static void main(String[] args) {
try {
// 创建客户端DatagramSocket
DatagramSocket clientSocket = new DatagramSocket();
// 向服务器发送数据
String message = "Hello, Server!";
byte[] sendData = message.getBytes();
InetAddress serverAddress = InetAddress.getByName("localhost");
int serverPort = 8888;
DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, serverAddress, serverPort);
clientSocket.send(sendPacket);
// 创建接收服务器响应的DatagramPacket对象
byte[] receiveData = new byte[1024];
DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
// 接收服务器返回的数据
clientSocket.receive(receivePacket);
String response = new String(receivePacket.getData(), 0, receivePacket.getLength());
System.out.println("服务器返回的消息长度:" + response);
// 关闭连接
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
二、关系型数据库
1. 数据库架构
关系型数据库的主要考点

如何设计一个关系型数据库?

第一部分是存储部分,类似于一个文件系统,用于将数据持久化到存储设备中。然而,仅有存储是不够的,我们还需要另一部分,即程序实例模块,来对存储进行逻辑上的管理。
存储管理模块:
负责将数据的逻辑关系转换成物理存储关系,确保数据在存储设备上的有效管理和组织。
缓存模块:
用于优化执行效率,通过缓存机制减少IO操作,提高数据库的性能和响应速度。
SQL解析模块:
负责将SQL语句进行解析,将其转换为机器可识别的指令,以便执行数据库操作。
日志管理模块:
记录数据库操作的日志,包括增删改查等操作,以确保数据的完整性和一致性,并支持灾难恢复和数据备份。
权限划分模块:
用于进行多用户管理,对不同用户或角色进行权限划分,确保数据的安全性和保密性。
灾难恢复模块:
设计用于灾难恢复的机制和策略,以应对意外情况,确保数据库的可靠性和稳定性。
索引模块:
优化数据查询效率,通过索引结构加速数据检索,减少查询时间,提高数据库性能。
锁模块:
支持数据库的并发操作,通过锁机制控制数据的访问权限,避免数据的并发冲突和一致性问题。
数据库设计基本原则:
- 模块化设计:
- 将数据库拆分成不同的模块,例如存储模块、缓存模块、权限划分模块等,以便管理和优化。
- 存储和管理数据:
- 数据库主要功能是存储和管理数据,需要设计存储模块将数据持久化到存储设备,并有逻辑实例模块管理数据的逻辑结构和物理存储关系。
- 优化执行效率:
- 引入缓存机制,减少IO操作,提高程序执行效率。
- 设计合适的数据结构和算法以优化查询性能。
- 异常处理和容灾:
- 引入异常处理机制,设计灾难恢复模块,确保数据库在异常情况下的可靠性和稳定性。
面试问题示例:
1. 为什么要使用索引?
- 提升检索效率: 索引可以避免全表扫描,使得数据检索更加高效。
2. 什么样的信息能成为索引?
- 具备区分性的字段: 任何能够让数据具备一定区分性的字段都可以成为索引,如主键、唯一键等。
3. 索引的数据结构
- 主流数据结构:
- B+树: 主流的索引数据结构,适用于大多数场景,能够提供快速的检索和范围查询。
- 哈希结构: 通过哈希函数将索引键值映射为索引位置,适用于等值查询,但不支持范围查询。
- 位图索引: 当索引的字段值具有固定的几种值时,可用于高效统计,但目前较少数据库支持。
4. 密集索引和稀疏索引的区别
- 密集索引: 每个搜索码值都对应一个索引值,叶子节点保存键值和其他列的信息,一般用于主键索引。
- 稀疏索引: 只为索引码的某些键建立索引项,叶子节点仅保存键位信息和数据地址,适用于辅助键索引。
总结: 索引是提高数据库检索效率的重要工具,选择合适的索引数据结构和类型能够有效优化数据库性能。
2.优化索引 二叉树查找

二叉查找树是每个节点最多有两个子树的树结构,每个节点的值都小于其右子树中的节点,大于其左子树中的节点。利用二叉查找树作为索引可以提升查询效率。然而,索引的存储块并非一一对应于数据库的存储单位,而是以关键字和指针组成的结构来组织数据。虽然二叉查找树有助于提高查询效率,但其结构容易因增删操作而失衡,导致查询效率下降。为了解决这一问题,可以利用B树结构,使每个节点能存储更多的数据,从而降低查询的时间复杂度和IO次数,提高数据库的性能。
3.优化索引 B-Tree

B树是一种平衡多路查找树,其中每个节点最多有M个孩子,因此称为MB树。通常情况下,M是一个较大的值,取决于节点的容量和数据库的配置。B树的特征包括:根节点至少有两个孩子;每个节点最多含有M个孩子,且孩子数不能少于2个;对于非根非叶子节点,其孩子数至少为M÷2的上限;所有叶子节点位于同一层;除根节点和叶子节点外,其他节点至少含有M÷2-1个关键字信息,且关键字个数不超过M-1;非叶子节点的指针分别指向小于其关键字值和大于其关键字值的子节点,以及介于其相邻关键字值之间的子节点;其他指针指向关键字值属于某个区间范围的子节点。B树具有高效的查询效率,查找数据的时间复杂度为大O(logN),并通过合并、分裂、上移和下移节点等策略来维持其平衡特性。在面试中,B树的实现常作为考察候选人数据结构知识的重要内容。
定义:
根节点至少包括两个孩子
书中每个节点最多含有m个孩子(m>2)
除根节点和叶节点外,其他每个节点至少有ceil(m/2)个孩子
所有叶子节点都位于同一层
假设每个非终端节点中包含有n个关键字信息,其中
a) KI(i = 1...n)为关键字,且关键字按顺序升序排序k(i - 1)< ki
b) 关键字的个数n必须满足:[ceil(m/2)-1]<=n <= m-1
c) 非叶子节点的指针:P[1] ,[p2], ....,p[M]; 其中P[1] 指向关键字小于K[1] 的子树,p[M]指向关键字大于K[M-1]的子树,其他P[i]指向关键字属于(K[i-1],K[i])的子树。
4.优化索引 B+ Tree
B+树是一种基于B树的变体,具有更好的性能和适用性。它与B树的主要区别在于:
- 非叶子节点的指针与关键字个数相同: 在B+树中,非叶子节点除了第一个节点外,其余节点的关键字数量与指针数量相同。这使得B+树内部节点相对更小,有助于减少IO读写次数。
- 非叶子节点指针的指向: 非叶子节点的指针指向的是关键字的范围,而不是具体的关键字值。这个范围规定了该指针所指向的子树中关键字的取值范围,保证了B+树的有序性。
- 数据仅存储在叶子节点中: 所有的数据都存储在叶子节点中,而非叶子节点仅用于索引。这种设计使得所有的搜索都必须在叶子节点中结束,从而保证了查询效率的稳定性。
- 叶子节点之间的链接: 所有的叶子节点通过链表按照大小顺序链接在一起,方便进行范围查询。这个特性使得B+树在数据库中的扫描操作更加高效。
结论:B+Tree更适合用来做存储索引
- B+树的磁盘读写代价更低
- B+树的查询效率更加稳定
- B+树更有利于对数据库的扫描
5.优化索引 运用Hash以及BitMap

缺点:
- 仅仅能满足“=” , “IN” ,不能使用范围查询
- 无法被用来避免数据的排序操作
- 不能利用部分索引键查询
- 不能避免表扫描
- 遇到大量Hash值相等的情况后,性能并不一定就会比B-Tree索引高
BitMap索引 Oracle数据库用

位图索引的结构类似于B+树,但是针对字段值只有几种固定值的情况,位图索引可以是一个较好的选择。位图索引将每种字段值的出现情况用位图的形式进行存储,每个位代表一个行是否包含对应的字段值。例如,如果某一行的字段值是红色,则对应的位图中的位置为1,否则为0。由于只需要存储是或否的信息,因此每个位通常只需要一个bit来表示。
位图索引在统计时非常快速,因为它几乎是纯CPU的位操作,而且可以存储大量的行信息在一个叶子块中。然而,位图索引只适用于字段的值有限且固定的情况,例如颜色、状态等。
尽管位图索引有其优势,但也存在一些缺点。其中一个主要缺点是锁的力度较大。由于位图索引中的位与行的顺序对应,因此在发生数据的增删改查操作时,位图索引需要锁定与之相关的所有位图,以防止数据的位置顺序发生改变导致查询错误。因此,位图索引不适合于高并发的事务处理系统,而更适合于统计运算较多的OLAP系统。
综上所述,数据库中常见的索引数据结构包括B+树、哈希结构和位图索引。其中,B+树是主流索引结构,而哈希结构和位图索引则在特定场景下发挥重要作用。
6.密集索引和稀疏索引的区别

密集索引和稀疏索引是数据库中常见的两种索引类型,它们在索引结构和存储方式上有所不同。
密集索引:
- 文件中每个搜索码值都对应一个索引值。
- 叶子节点保存键值以及同行记录的其他列信息。
- 确定了表的物理排列顺序,一个表只能有一个密集索引。
- 适用于MyISAM存储引擎,在B+树的叶子节点中存储了完整的行数据,索引与数据存储在同一个文件中。
稀疏索引:
- 文件中只为索引码的某些键建立索引项。
- 叶子节点仅保存键位信息以及该行数据的地址或组件信息。
- 表数据存储在独立的地方,索引与数据分开存储。
- 适用于InnoDB存储引擎,索引和数据分开存储,通过索引查找到主键或者辅助键,再根据主键查询到行数据。
在MySQL中,InnoDB存储引擎使用稀疏索引,而MyISAM存储引擎使用密集索引。对于InnoDB引擎,如果没有定义主键,则会生成一个隐藏的主键作为密集索引。密集索引用于主键索引和唯一非空索引,而辅助索引则属于稀疏索引。
对于密集索引,在查询时只需一次检索即可获得完整的行数据;而对于稀疏索引,则需要两次检索,一次查找索引键,一次查找主键获取完整的行数据。
在MySQL中,可以通过查看数据文件的存储位置来区分索引类型。对于InnoDB引擎,索引和数据存储在不同的文件中;而对于MyISAM引擎,索引和数据存储在同一个文件中。

7.索引额外的问题-调优SQL
如何定位并优化慢查询的SQL?
根据慢日志定位慢查询SQL
1.show variables like '%quer%'; 执行后查看 慢日志开关slow_query_log 参数 OFF 关闭的,需要将他打开 慢日志文件slow_query_log_file 参数 【文件路径】 慢日志文件路径 慢日志花费时间 long_query_time 参数 10.000 2.show status like '%Slow_queries%';(了解系统的状态) 执行后查看 慢查询数量 Slow_queries 参数 0 3.打开慢日志开关: set global slow_query_log = on; 查看是否打开show variables like '%quer%'; 慢日志开关slow_query_log 参数 ON 打开了 4.设置慢查询时间为1秒: set global long_query_time = 1;(需要关掉客户端重新连接才能看到是修改是否成功) 执行语句:SELECT username FROM users ORDER BY username DESC; 5.慢日志中的数据 # Time: 2024-03-23T11:14:04.266819Z # User@Host: root[root] @ localhost [::1] Id: 558 # Query_time: 5.940174 Lock_time: 0.000003 Rows_sent: 2397680 Rows_examined: 4795360 SET timestamp=1711192438; SELECT username FROM users ORDER BY username DESC; 解释: 这个日志条目显示了一个慢查询,查询语句是从 users 表中选择 username 字段,并按照 username 的降序进行排序。查询的执行时间是 5.940174 秒,期间没有进行锁定操作。在查询期间发送了 2397680 行数据,并且检查了 4795360 行数据。 建议优化这个查询,可能的优化包括: 索引优化: 如果 username 字段上没有索引,可以考虑添加一个以加快排序操作。 分页查询: 如果可能,可以考虑对结果进行分页,以减少一次性返回的行数。 缓存: 如果查询的结果不经常变化,可以考虑将结果缓存起来,以减少数据库查询的频率。 查询优化: 考虑查询是否可以被进一步优化,例如是否可以只选择需要的列,而不是全部列。使用explain等工具分析SQL
EXPLAIN SELECT username FROM users ORDER BY username DESC;type:

extra中出现以下2项意味extra着MYSQL根本不能使用索引,效率会收到重大影响。应尽可能对此进行优化。

修改SQL或者尽量让SQL走索引
- 给email字段添加了索引
EXPLAIN SELECT email FROM users ORDER BY email DESC;日志对比: # Time: 2024-03-23T11:14:04.266819Z # User@Host: root[root] @ localhost [::1] Id: 558 # Query_time: 5.940174 Lock_time: 0.000003 Rows_sent: 2397680 Rows_examined: 4795360 SET timestamp=1711192438; SELECT username FROM users ORDER BY username DESC; # Time: 2024-03-23T11:33:09.311001Z # User@Host: root[root] @ localhost [::1] Id: 558 # Query_time: 2.810996 Lock_time: 0.000003 Rows_sent: 2397680 Rows_examined: 2397680 SET timestamp=1711193586; SELECT email FROM users ORDER BY email DESC;
8.联合索引的最左匹配原则的成因?
1.最左前缀匹配原则,非常重要的原则,mysql会一直向右匹配直到遇到范围査询(>、<、between、like)就停止匹配,比如a=3 and b=4 andc>5 and d=6如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。
2.=和in可以乱序,比如a=1and b=2 andc=3建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式

联合索引的最左匹配原则是指在使用联合索引进行查询时,如果查询条件涉及到联合索引的多个列,那么这些列的顺序必须与创建索引时的顺序保持一致,而且查询条件中的列必须从联合索引的最左边的列开始,依次向右逐个列进行匹配。如果查询条件中跳过了联合索引的某个列或者列的顺序与索引不一致,那么该索引将不会被使用,无法实现最优的查询性能。
这一原则的成因主要是由索引的数据结构决定的。在 B-Tree 或 B+Tree 索引结构中,数据按照索引的列顺序存储,并且索引的节点以及叶子节点都是按照这个顺序构建的。因此,如果查询条件不符合最左匹配原则,就无法充分利用索引的数据结构,导致数据库无法高效地定位和检索数据。
- 索引是建立的越多越好吗?
答案:不是,数据量小的表不需要建立索引,建立会增加额外的索引开销。数据变更需要维护索引,因此更多的索引意味着更多的维护成本,更多的索引也意味着需要更多的空间。
- 数据量小的表不需要建立索引,建立会增加额外的索引开销。
- 数据变更需要维护索引,因此更多的索引意味着更多的维护成本,更多的索引也意味着需要更多的空间。
希望大家课下多花些时间好好复习一下所以相关的知识,并学习一下主外键、唯一键之类的约束,同时去研究一个特定的数据库的关于索引方面的特性。这里推荐mySQL的inno DB以及MyISAM引擎。
9.锁模块
(1)MyISAM与Inno DB关于锁方面的区别是什么?
MyISAM默认的用的是表级锁,不支持行级锁。
InnoDB默认的是行级锁,也支持表级锁
一般来说,MyISAM 的读锁是共享锁,写锁是排它锁。对表A而言,进程1给表A加了共享锁,进程2只能对表A加共享锁;若进程1加了排它锁,那进程2只能等待进程1解锁后才能查询或加锁。
InnoDB采用的是二段锁,即加锁和解锁(commit,数据库默认打开自动提交);但是在非SQL加共享锁时,若没改变某行数据,那另一进程亦可修改该行记录。InnoDB的SQL没用到索引时,用的是表级锁;用到时则是行级锁。
MyISAM适用场景
- 频繁执行全表count语句。
- 对数据进行增删改的频率不高,查询非常频繁。
- 没有事务
InnoDB适用场景
数据库增删改查都相当频繁。
可靠性要求比较高,要求支持事务。
MyISAM在对数据进行select会对数据加上一个表级别读锁,而对数据增删改的时候,他会对操作表增加一个表级别的写锁。
锁知识
按照粒度划分:表级锁,行级锁、页级锁;
按锁级别划分:共享锁,排他锁;
按加锁方式划分:自动锁,显式锁;
按操作划分:DML锁,DDL锁;
按使用方式划分,可分为乐观锁,悲观锁,
行锁:对表操作时对操作行上锁。
表锁:对表操作是锁上整个表的数据。
共享锁:对于多个不同的事务,对同一个资源共享同一个锁。但是对于insert ,update,delete事务则会自动加上排它锁。在执行语句后面加上lock in share mode就代表对某些资源加上共享锁。 eg: 可同时执行多个select语句。
排它锁:对于多个不同的事务,对同一个资源只能有一把锁。只需在执行语句后+for update 即可。eg:只能一个个进行操作。
悲观锁:在操作数据的时候,认为此操作会发生数据冲突,所以在进行每次操作时都要通过获取锁才能进行相同数据的操作。与Java的synchronized很相似,所以悲观锁需要耗费较多的时间。
乐观锁:操作数据库时,认为此操作不会导致数据冲突,在操作数据时,不进行加锁处理,在进行更新后,再去判断是否有冲突。例如hibernate中的乐观锁两种实现,就是分别基于version和timestamp来实现的。eg: update user set name = ‘John’ ,version=version+1 where id =13 and version = #{version};

锁的释放
- 自动提交事务时,锁在事务结束时释放。
- 手动控制事务时,需要显式地提交或回滚事务来释放锁。
添加表级读锁: lock tables 表名 read;
释放锁: unlock tables;
查看事务是否是自动提交的
show variables like 'autocommit';
set autocommit =0 ; 关闭自动提交 (仅限于当前会话)
10.数据库事务
四大特性ACID
- 原子性(Atomicity):事务是不可分割的最小工作单位,要么全部执行成功,要么全部执行失败。如果事务中的任何一个操作失败,整个事务都会被回滚到事务开始前的状态,保持数据的一致性。
- 一致性(Consistency):事务执行的结果必须使数据库从一个一致性状态转变到另一个一致性状态。在事务开始前和结束后,数据库的完整性约束必须得到保持,确保数据不会因为事务的执行而处于不一致的状态。
- 隔离性(Isolation):事务的执行不受其他事务的影响,每个事务都感觉不到其他事务在同时执行。事务之间的操作彼此隔离,避免了并发操作时的数据竞争和不一致性。
- 持久性(Durability):一旦事务提交,其对数据库的修改就是永久性的,即使系统发生故障,数据库系统也能够保证事务的提交结果不会丢失。已经提交的事务对数据库的修改都将被持久化到数据库中,即使数据库发生故障,数据也能够被恢复到最近的一致状态。
1.事务隔离级别以及各级别下的并发访问问题?
查看事务隔离级别:
SELECT @@transaction_isolation;
结果:REPEATABLE-READ
REPEATABLE-READ 是 MySQL 中的默认事务隔离级别。
在这个级别下,一个事务执行过程中所读取的数据集合是固定的,即使在其他事务对数据进行修改时,也不会影响当前事务的查询结果。这意味着在同一个事务中进行的多次读取操作会返回一致的结果。

设置事务的隔离级别
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;这将设置当前会话的事务隔离级别为 `READ COMMITTED`。
开始事务时直接设置隔离级别
START TRANSACTION ISOLATION LEVEL READ COMMITTED;
这将在开始新的事务时设置隔离级别为 `READ COMMITTED`。
- READ UNCOMMITTED(未提交读): 这是最低的隔离级别。在该级别下,一个事务可以读取到另一个事务尚未提交的数据,可能会读取到未提交的、脏数据。这种隔离级别的特点是读取的数据可能是不一致的。
- READ COMMITTED(提交读): 在这个级别下,一个事务只能读取到已经提交的数据,这样可以避免脏读,但是仍然可能会发生不可重复读和幻读的问题。其他事务对数据的修改只有在提交后才会被当前事务看到。
- REPEATABLE READ(可重复读): 这个级别下,一个事务在执行过程中多次读取同一数据时,得到的结果是一致的。即使其他事务对数据进行了修改,对于同一数据,当前事务多次读取时的结果是一致的。但是,仍然可能发生幻读问题,即在同一个事务中,查询同一范围的数据时,得到的结果集合可能是不一致的。
- SERIALIZABLE(串行化): 这是最高的隔离级别,它通过对事务进行串行化执行来避免幻读问题。在这个级别下,所有的事务都会被按顺序执行,每个事务都会排队等待其他事务执行完毕。虽然可以解决幻读的问题,但是会导致系统的并发性能大幅下降。
事务并发访问引起的问题以及如何避免:
更新丢失 —— mysql所有事务隔离级别在数据库层面上均可避免

脏读 —— READ - COMMITTED 事务隔离级别以上可避免
在数据库事务中,脏读(Dirty Read)指的是一个事务读取了另一个事务尚未提交的数据。当一个事务开始对某些数据进行修改,但尚未提交时,其他事务如果读取了这些尚未提交的数据,就可能发生脏读。由于尚未提交的数据可能会被回滚或修改,所以其他事务读取到的数据可能是不准确或无效的。 脏读可能导致数据的不一致性和错误的结果。为了避免脏读,通常可以使用事务隔离级别中的“读已提交”(Read Committed)级别,该级别保证事务只能读取已经提交的数据,从而避免了脏读的问题。不可重复读 —— REPEATABLE READ事务隔离级别以上可避免
不可重复读(Non-Repeatable Read): 不可重复读指的是在一个事务内的两次读取操作之间,由于其他事务对数据进行了修改或删除,导致两次读取的结果不一致。换句话说,在同一个事务中,同一行数据可能在第一次读取时与第二次读取时不一样。这种情况下,事务在读取数据时,由于其他事务的干扰,出现了不一致的现象。幻读 —— SERIALIZABLE事务隔离级别可避免
幻读(Phantom Read): 幻读指的是在一个事务内的两次查询操作之间,由于其他事务插入了新的数据行,导致两次查询的结果不一致。与不可重复读不同的是,幻读通常发生在范围查询(Range Query)中。例如,在一个事务中,第一次查询时返回了一些数据行,然后另一个事务插入了新的数据行,接着在同一个事务中再次进行相同的查询时,会发现结果集合中出现了额外的数据行,这种情况就被称为幻读。
2.InnoDB可重复读隔离级别下如何避免幻读?
表象:快照读(非阻塞读) -- 伪MVCC
通过伪MVCC机制实现的快照读和非阻塞读,在RR级别下可以避免出现幻行。
InnoDB 使用多版本并发控制(MVCC)机制来实现可重复读隔离级别。在这种机制下,读取数据时会生成一个快照,该快照包含了事务开始时数据库的状态。因此,即使其他事务对数据进行了修改或插入操作,当前事务在可重复读隔离级别下仍然会看到事务开始时的数据状态,从而避免了幻读问题。这种快照读是一种非阻塞的读取方式,不会阻塞其他事务对数据的访问。当前读和快照读
当前读:select ... lock in share mode , select ... for update
当前读:update , delete ,insert
就是在操作数据的同时给数据加了锁。就是当前读。看到的都是实时数据

快照读:不加锁的非阻塞读,select
内在:next - key锁 (行锁 + gap锁)
Next-Key 锁:InnoDB 使用行锁和间隙锁(gap lock)来防止幻读。当一个事务对某行数据进行读取或者修改时,会给该行数据加上行锁,阻止其他事务对同一行数据进行修改。而当一个事务对范围数据进行查询时,例如使用范围条件查询,InnoDB 会给这个范围数据的间隙(gap)加上间隙锁,阻止其他事务在这个范围内插入新的数据,从而避免了幻读问题。这种行锁和间隙锁的组合保证了查询结果的一致性,避免了幻读的发生。
3.RC、RR级别下的InnoDB的非阻塞读如何实现?
数据行里的DB_TRX_ID、DB_ROLL_PTR、 DB_ROW_ID 字段
DB_TRX_ID(事务 ID):
DB_TRX_ID字段存储了将该行数据插入到表中的事务的事务 ID。这个 ID 是一个递增的数字,代表了插入该行数据的事务的唯一标识符。在多版本并发控制(MVCC)中,这个字段被用来检查事务的可见性,并用于确定数据的版本。DB_ROLL_PTR(回滚指针):
DB_ROLL_PTR字段存储了指向事务回滚段(Undo Log)中的特定记录的指针。Undo Log 是用于实现事务的回滚和撤销操作的重要结构。该字段指向了Undo Log 中存储了该行数据的修改记录的位置,以便于在事务回滚时恢复数据。DB_ROW_ID(行 ID):
DB_ROW_ID字段存储了该行数据在表中的唯一标识符。这个标识符是一个递增的数字,用于唯一标识表中的每一行数据。在 InnoDB 表中,如果没有显式定义主键,那么DB_ROW_ID字段会被作为隐藏的主键列。undo log
read view


4.InnoDB可重复读隔离级别下如何避免幻读
- 表象:快照读(非阻塞读) -- 伪MVCC
- 内在:next - key 锁 (行锁 + gap锁)
next - key 锁 (行锁 + gap锁)
- 行锁: 对记录行上锁
- Gap锁:Gap 锁的作用是锁定索引范围,而不仅仅是匹配的行。这样可以防止其他事务在锁定的范围内插入新的数据,从而确保当前事务执行期间的一致性视图。
Gap 锁(间隙锁)是一种用于多版本并发控制(MVCC)的锁机制。当使用范围条件(例如 WHERE 子句或范围扫描)查询数据时,MySQL 可能会使用 Gap 锁来防止其他事务在范围内插入新行,从而确保数据的一致性。
对主键索引或者唯一索引会用Gap锁吗?
- 如果where条件全部命中,则不会用Gap锁,只会加记录锁

- 如果where条件部分命中或者全不命中,则会加Gap锁
Gap锁会用在非唯一索引或者不走索引的当前读中
非唯一索引

不走索引

锁小结
- MyISAM与InnoDB关于锁方面的区别是什么
- 数据库事务的四大特性
- 事务隔离级别以及各级别下的并发访问问题
- InnoDB可重复读隔离级别下如何避免幻读
- RC,RR级别下的InnoDB的非阻塞读如何实现
课下能够花时间去了解一下undolog
11.数据库语法
关键语法
- GROUP BY
- HAVING
- 统计相关:COUNT,SUN,MAX,MIN,AVG
GROUP BY
- 如果用group by , 那么你的Select语句中选出的列要么是你group by 里用到的列,要么就是带有sum min 等列函数的列
- 列函数对于Group by 子句定义的每个组各返回一个结果

1.查询所有同学的学号、选课数、总成绩(针对同一张表)
select student_id,count(coirse_id),sum(score) from score group by student_id;
-- 列函数对于Group by 子句定义的每个组各返回一个结果
--如果用group by , 那么你的Select语句中选出的列要么是你group by 里用到的列,要么就是带有sum min 等列函数的列
2.查询所有同学的学号,姓名,选课数,总成绩
select s.student_id,stu.name,count(s.coirse_id),sum(s.score)
from score s,student stu
where
s.student_id=stu.student_id
group by student_id;
HAVING
通常与GROUP BY子句一起使用
WHERE过滤行,HAVING过滤组
出现在同一SQL的顺序:WHERE > GROUP BY >HAVING
1.查询平均成绩大于60分的同学的学号和平均成绩 select student_id , avg(score) from score group by student_id having avg(score)>60
练习
1.查询没有学全所有课的同学的学号、姓名
select stu.student_id,stu.name
from student stu,score s
where stu.student.id=s.student_id group by s.student_id
having count(*) < (select count(*) from course)
三、Redis数据库.
1.Redis 简介
主流应用架构

缓存中间件 —— Memcache和Redis的区别
- Memcache:代码层次类似Hash
- 支持简单数据类型
- 不支持数据持久化存储
- 不支持主从
- 不支持分片
- Redis
- 数据类型丰富
- 支持数据磁盘持久化存储
- 支持主从
- 支持分片
为什么Redis能这么快?
100000 + QPS(QPS每秒内查询次数)
- 完全基于内存,绝大部分请求是纯粹的内存操作,执行效率高
- 数据结构简单,对数据操作也简单
- 采用单线程,单线程也能处理高并发请求,想多核也可启动多实例
- 使用多路IO复用模型,非阻塞IO
多路IO复用模型
FD:File Descriptor,文件描述符
- 一个打开的文件通过唯一的描述符进行引用,该描述符是打开文件的源数据到文件本身的映射
传统的阻塞IO模型

Select 系统调用

Redis采用的IO多路复用函数:epoll / kqueue / evport / select?
- 因地制宜:根据平台的不同选用不同函数
- 优先选择时间复杂度为O(1)的IO多路复用函数作为底层实现
- 以时间复杂度为O(n)的select作为保底
- 基于react设计模式监听IO事件
2.Redis常用数据类型
供用户使用的数据类型
String:最基本的数据类型,二进制安全(可以存储所有数据类型)

Hash:String元素组成的字典,适合用于存储对象
List:列表,按照String元素插入顺序排序(后进先出,栈)
Set:String元素组成的无序集合,通过哈希表实现,不允许重复
Sorted Set:通过分数来为集合中的成员进行从小到大的排序
用于技术的HyperLogLog,用于支持存储地理位置信息的Geo
底层数据类型基础
- 简单动态字符串
- 链表
- 字典
- 跳跃表
- 整数集合
- 压缩列表
- 对象
3.从海量Key中查询出某一固定的前缀的Key
留意细节
- 摸清数据规模,既问清楚边界
KEYS pattern :查找所有符合给定模式pattern的Key
缺点
- Keys指令一次性返回所有匹配的Key
- 键的数量过大会使服务卡顿
SCAN cursor [MATCH pattern] [COUNT count]
- 基于游标的迭代器,需要基于上一次的游标延续之前的迭代过程
- 以0作为游标开始一次新的迭代,直到命令范围游标0完成一次遍历
- 不保证每次执行都会返回某个给定数量的元素,支持模糊查询
- 一次返回的数量不可控,只能是大概率符合count参数
SCAN 0 match k1 count 10
-- 开始迭代,返回前缀为k1的key 期望一次返回十个
SCAN '上次迭代第一个数据' match k1 count 10
-- 需要对结果进行去重,使用hash set
4.Redis 分布式锁
如何通过Redis实现分布式锁
分布式锁需要解决的问题
- 互斥性:任意时刻只能有一个客户端获取锁,不能同时有两个客户端获取到锁。
- 安全性:锁只能被持有该锁的客户端删除,不能由其他客户端删除
- 死锁:获取锁的客户端,因为某些原因宕机,而未能释放锁,其他客户端再也不能获取锁,导致死锁。
- 容错:如部分redis节点宕机时,客户端仍然能够获取锁,和释放锁。
SETNX key value :如果key不存在,则创建并赋值
- 时间复杂度:O(1)
- 返回值:设置成功,返回1 ;设置失败,返回0。
实现逻辑
当前线程尝试设置一个特定的键(例如,SETNX 命令),表示获取了该资源的锁。
如果设置成功(返回值为 1),则表示当前线程成功获取了锁,可以执行代码逻辑。
如果设置失败(返回值为 0),则表示当前资源已被其他线程占用,当前线程需要等待一段时间后重试,或者采取一定的重试策略(例如指数退避),直至成功获取锁。
如何解决SETNX长期有效的问题?
EXPIRE key seconds
设置key的生存时间,当key过期时(生存时间为0),会被自动删除
缺点:原子性得不到满足

SET key value [EX seconds] [PX milliseconds] [NX] [XX]
- EX second:设置键的过期时间为second 秒
- PX millisecond : 设置键的过期时间为Millisecond 毫秒
- NX:只在键不存在时,才对键进行设置操作
- XX:只在键已经存在时,才对键进行设置操作
- SET操作成功完成时,返回OK,否则返回nil
set locktarget 12345 ex 10 nx
-- 设置locktarget键 的值为12345 有效期为10s 只在键不存在时,才对键进行设置操作
set locktarget 1234 ex 10 nx
nil
-- 十秒之后会执行成功

大量的key同时过期的注意事项?
集中过期,由于清除大量的key很耗时,会出现短暂的卡顿现象
- 解决方案:在设置key的过期时间的时候,给每个key加上随机值
5.如何使用Redis做异步队列?
使用List作为队列,RPUSH上产消息,LPOP消费消息
- 缺点:没有等待队列里有值就直接消费
- 弥补:可以通过在应用层引入sleep机制去调用LPOP重试
弥补2:
BLPOP key [key ...] timeout :阻塞直到队列有消息或者超时
blpop testlist 30 -- 没有消息时阻塞30秒
- 缺点:只能提供一个消费者消费
解决:使用pub/sub:主题订阅者模式
- 发送者(pub)发送消息,订阅者(sub)接收消息
- 订阅者可以订阅任意数量的频道

客户端1:subscribe myTopic --订阅myTopic频道
客户端2:subscribe myTopic --订阅myTopic频道
客户端3:subscribe anotherTopic --订阅另外一个频道
客户端4:publish myTopic 'hello' -- 客户端4发布myTopic消息 客户端1/2都能收到消息
客户端4:publish anotherTopic 'hello' -- 客户端4发布anotherTopic消息 客户端3收到消息
缺点:
- 消息发布是无状态的,无法保证可达 ,解决需要用专业的消息队列解决
6.持久化方式之DBA
Redis如何做持久化?
RDB(快照)持久化:保存某个时间点的全量数据快照
持久化配置解析
redis.conf
save 900 1 --900秒内产生1次插入操作,生成一次快照
save 300 10 --300秒内产生10次插入操作,生成一次快照
save 60 10000 --60秒内产生10000次插入操作,生成一次快照
save "" --禁用RDB
RDB策略会将数据文件以 dump.rdb 文件的方式保存
stop - writes - on - bgsave - error yes -- 当复制进程出错的时候,主进程就停止写入操作
rdbcompression yes -- 在备份的时候需要将备份文件压缩后再保存
RDB文件生成
- SAVE:阻塞Redis的服务进程,直到RDB文件被创建完成
- BGSAVE:Fork出一个子进程来创建RDB文件,不阻塞服务器进程
自动化触发RDB持久化的方式
- 根据redis.conf配置里的SAVE m n 定时触发(用的是BGSAVE)
- 主从复制时,主节点自动触发BGSAVE
- 执行Debug Reload 时
- 执行Shutdown且没有开启AOF持久化
BGSAVE原理

系统调用fork():创建进程,实现了Copy - on - write(写时复制)
Copy - on - write具体来说,当一个进程或线程尝试修改共享数据时,系统会首先检查是否有其他进程或线程也在使用相同的数据。如果是的话,系统会复制当前数据的副本,并让修改操作在副本上进行。这样,修改操作只影响到了当前进程或线程,而其他进程或线程继续共享原始数据。这种策略延迟了复制的时间,只有在确实需要时才执行,从而节省了内存和处理时间。RDB缺点:
- 内存数据全量同步,数据量大会由于IO而严重影响性能
- 可能会因为Redis挂掉而丢失从当前到最近一次快照期间的数据
7.持久化方式之AOF
AOF(Append-Only-File)持久化:保存写状态
- 记录下除了查询以外的所有变更数据库状态的指令
- 以append的形式追加保存到AOF文件中(增量)
redis.conf
appendonly yes --打开AOF持久化策略
appendfilename "appendonly.aof" -- AOF文件名
appendfsync everysec --配置AOF文件的写入方式
参数:
everysec:每隔1秒就把缓存区的内容写入到文件里
always:一旦缓存区的内容发生变化,就把变化的内容写到AOF当中
no:将写入AOF的方式交给操作系统决定
修改配置后要重启redis
日志重写解决AOF文件大小不断增大的问题,原理如下:
- 调用fork()创建一个子进程
- 子进程把新的AOF写到一个临时文件里,不依赖原来的AOF文件
- 主进程持续将新的变动同事写到内存和原来的AOF里
- 主进程获取子进程重写AOF的完成信号,往新AOF同步增量变动
- 使用新的AOF文件替换掉旧的AOF文件
Redis数据的恢复
RDB和AOF文件共存情况下的恢复流程

RDB和AOF的优缺点
- RDB优点:全量数据快照,文件小,恢复快
- RDB缺点:无法保存最近一次快照之后的数据
- AOF优点:可读性高,适合保存增量数据,数据不易丢失
- AOF缺点:文件体积大,恢复时间长
RDB-AOF混合持久化方式(默认)

- BGSAVE做镜像全量持久化,AOF做增量持久化
8.Pipeline以及主从同步
使用Pipeline的好处
- Pipeline和Linux的管道类似
- Redis基于请求/响应模型,单个请求处理需要 一 一 应答
- Pipeline批量执行指令,节省多次IO往返的时间
- 有顺序依赖的指令建议分批发送
Redis的同步机制
主从同步原理

全量同步过程
- Salve发送sync命令到Master
- Master启动一个后台进程,将Redis中的数据快照保存到文件中
- Master将保存数据快照期间接收到的写命令缓存起来
- Master完成写文件操作后,将改文件发送给Salve
- 使用新的AOF文件替换掉旧的AOF文件
- Master将这期间收集的增量写命令发送给Salve端
增量同步过程
- Master接收到用户的操作指令,判断是否需要传播到Slave
- 将操作记录追加到AOF文件中
- 将操作传播到其他Slave:1、对齐主从库;2、往响应缓存写入指令
- 将缓存中的数据发送给Slave
Redis Sentinel
解决主从同步Master宕机后的主从切换问题:
- 监控:检查主从服务器是否运行正常
- 提醒:通过API向管理员或者其他应用程序发送故障通知
- 自动故障迁移:主从切换
流言协议:Gossip
在杂乱无章中寻求一致
- 每个节点都随机地与对方通信,最终所有节点的状态达成一致
- 种子节点定期随机向其他节点发送节点列表以及需要传播的消息
- 不保证信息一定会传递给所有节点,但是最终会趋于一致
9.Redis集群
如何从海量数据里快速找到所需?
- 分片:按照某种规则去划分数据,分散存储在多个节点上
- 常规的按照哈希划分无法实现节点的动态增删
一致性哈希算法:对2 的32次幂取模,将哈希值空间组织称虚拟的圆环

将数据Key使用相同的函数Hash计算处哈希值


Node C 宕机

新增服务器Node X
Hash环的数据倾斜问题

引入虚拟节点解决数据倾斜的问题

四、Linux知识
1.Linux的体系结构

- 体系结构主要分为用户态(用户上层活动)和内核态
- 内核:本质是一段管理计算机硬件设备的程序
- 系统调用:内核的访问接口,是一种能够简化的操作
- 公用函数库:自动调用的组合拳
- Shell:命令解释器,可编程
2.查找特定文件
find
find path [options] params
- 作用:在指定目录下查找文件
find -name "target3.java" 当前目录查找
find / -name "target3.java" 全局查找
find / -name "target*" 全局模糊查询
find ~ -iname "target*" 忽略文件名大小写
面试里常用的方式
find ~ -name "target3.java" :精确查找文件
find ~ -name "target*" :模糊查找文件
find ~ -iname "target*" :忽略文件名大小写
man find:更多关于find指令的使用说明
3.检索文件内容
grep
语法:grep [options] pattern file
全称:Global Regular Expression Print
作用:查找文件里符合条件的字符串
grep "moo" target* 查找文件 筛选目标行
管道操作符 |
可将指令连接起来,前一个指令的输出作为后一个指令的输入

find ~ | grep "target"
- 只处理前一个命令正确输出,不处理错误输出
- 右边的命令必须能够接收标准输入流,否则传递过程中数据会被抛弃
- 常用于接收管道流命令:sed,awk,grep,cut,head,top,less,more,wc,join,sort,split等
4.对文件内容做统计
awk
- 语法:awk [options] 'cmd' file
- 一次读取一行文本,按输入分隔符进行切片,切成多个组成部分
- 将切片直接保存在内建的变量中,$1,$2...($0 表示全部的行)
- 支持对单个切片的判断,支持循环判断,默认分隔符为空格
awk '{print $1,$4}' netstat.txt 输出第一列和第四列
awk '$1=="tcp" && $2==1 {print $0}' netstat.txt 筛选出第一列等于tcp 第二列等于1 的行
awk '($1=="tcp" && $2==1) || NR==1 {print $0}' netstat.txt 筛选出第一列等于tcp 第二列等于1 的行 添加表头
awk -F "," '{print $2}' test.txt 安装,号分割行
5.批量替换掉文件内容
sed
- 语法:sed [option] 'sed command' filename
- 全名:stream editor , 流编辑器
- 适合用于对文本的行内容进行处理
sed -i 's/^Str/String/' replace.java 替换Str 为 String
面试里常用的方式
- sed -i 's/^Str/String/' replace.java
- sed -i 's/ \ .$/ \ ; /' replace.java
- sed -i ' s/jack / me/ g ' replace.java
