1 - 网络篇

1.1 网络基础

1.1.1 TCP/IP 网络模型有哪几层?
1.1.1.1 应用层
  1. 最上层的,也是我们能直接接触到的就是 应用层 (Application Layer),电脑或手机使用的应用软件都是在应用层实现。

那么,当两个不同设备的应用需要通信的时候,应用就把应用数据传给下一层,也就是传输层。

  1. 应用层只需要专注于为用户提供应用功能,比如 HTTP、FTP、Telnet、DNS、SMTP 等,不用关心数据是如何传输的。

就类似于我们寄快递的时候,只需要把包裹交给快递员,不需要关心快递是如何被运输的。

  1. 应用层是工作在操作系统中的用户态,传输层及以下则工作在内核态。

1.1.1.2 传输层
  1. 应用层的数据包会传给 传输层(Transport Layer),为其提供网络支持。在传输层有两个传输协议:

    • TCP 的全称叫传输控制协议(Transmission Control Protocol),大部分应用使用的正是 TCP,比如应用层 HTTP 协议。

      TCP 相比 UDP 多了很多特性,比如流量控制、超时重传、拥塞控制等,这些都是为了保证数据包能可靠地传输给对方。

    • UDP 相对来说就很简单,简单到只负责发送数据包,不保证是否能抵达对方,但它实时性相对更好,传输效率也高。

      当然 UDP 也可以实现可靠传输,把 TCP 的特性在应用层上实现就可以,不过要实现一个商用的可靠 UDP 不是一件简单的事情。

  1. 传输的数据可能会非常大,如果直接传输就不好控制,因此当数据包大小超过 MSS(TCP 最大报文段长度) 就要将数据包分块。

    即使中途有一个分块丢失或损坏了,只需重新发送这一分块即可。在 TCP 协议中,把每个分块称为一个 TCP 段(segment)。

img

  1. 当设备作为接收方时,传输层要负责把数据包传给应用。但设备上可能有很多应用,因此用一个编号将应用区分开来,即 端口

    比如 80 端口通常是 Web 服务器用的,而对于浏览器中的每个标签栏都是一个独立进程,操作系统会为其分配临时的端口号。

    由于传输层的报文中会携带端口号,因此接收方可以识别出该报文是发送给哪个应用。

1.1.1.3 网络层
  1. 传输层可能大家刚接触的时候,会认为它负责将数据从一个设备传输到另一个设备,事实上它并不负责。

    实际场景中的网络环节是错综复杂的,如果一个设备的数据要传输给另一个设备,就需要在各种各样的路径和节点进行选择。

    而传输层的设计理念是简单、高效、专注,如果传输层还负责这一块功能就有点违背设计原则了。

    我们希望让传输层只作为应用间数据传输的媒介,帮助通信。而实际的传输功能就交给下一层,即 网络层(Internet Layer)。

  2. 网络层最常使用的是 IP 协议(Internet Protocol),IP 协议会将传输层的报文作为数据部分,再加上 IP 包头组装成 IP 报文。

    如果 IP 报文大小超过 MTU(以太网中一般为 1500 字节)就会再次进行分片,得到一个即将发送到网络的 IP 报文:

    img

  3. 网络层负责将数据从一个设备传输到另一个设备,世界上那么多设备,又该如何找到对方呢?因此,网络层需要有区分设备的编号。

    我们一般用 IP 地址给设备进行编号,对于 IPv4 协议, IP 地址共 32 位,分成了四段(比如,192.168.100.1),每段是 8 位。

    只有一个单纯的 IP 地址虽然做到了区分设备,但是寻址起来就特别麻烦。因此,需要将 IP 地址分成两种意义:

    • 一个是网络号,负责标识该 IP 地址是属于哪个子网的

    • 一个是主机号,负责标识同一子网下的不同主机

    怎么分配 IP 地址的呢?需要配合 子网掩码。比如 10.100.122.0/24,其中 /24 表示 255.255.255.0 子网掩码。

    • 将子网掩码与 10.100.122.2 进行按位与运算,就可以得到网络号 10.100.122.0

    • 将子网掩码取反与 10.100.122.2 进行按位与运算,就可以得到主机号 0.0.0.2

    在寻址的过程中,会先匹配到相同的网络号(同一个子网),再去找对应的主机。

  4. IP 协议还有另一个重要的能力就是路由。实际场景中,两台设备是通过很多网关、路由器、交换机等众多网络设备连接起来的。

    因此当数据包到达一个网络节点,需要通过路由算法决定下一步走哪条路径。路由器 寻址工作中,就是要找到目标地址的子网。

    所以,IP 协议的寻址是告诉我们该朝哪个方向走,路由则是根据目的地选择路径。寻址像是在导航,路由像是在操作方向盘。

IP地址的网络号

1.1.1.4 网络接口层
  1. 生成了 IP 头部后,接下来交给 网络接口层(Link Layer)。在前面加上 MAC 头部,封装成 数据帧(Data frame)发送到网络上。

  2. IP 头部中的接收方 IP 地址表示网络包的目的地,通过这个地址判断要发到哪里,但在以太网的世界中,这个思路是行不通的。

    什么是以太网呢?电脑上的以太网接口、Wi-Fi 接口、以太网交换机、路由器上的千兆以太网口、网线,都是以太网的组成部分。

    以太网就是一种在局域网内,把附近的设备连接起来,使它们之间可以进行通讯的技术。

    以太网判断网络包的目的地时和 IP 的方式不同,必须采用相匹配的方式才能将包发往目的地。而 MAC 头部 就是干这个用的。

  3. MAC 头部是以太网使用的头部,它包含了接收方和发送方的 MAC 地址等信息,我们可以通过 ARP 协议 获取对方的 MAC 地址。

    所以,网络接口层为网络层提供链路级别的传输服务,负责在以太网、WiFi 这样的底层网络上发送原始数据包,工作在网卡层次。

1.1.1.5 总结

TCP/IP 网络通常是由上到下分成 4 层,分别是应用层、传输层、网络层、网络接口层。

但这些名词并没有什么本质的区分,可以统称为数据包。

8011303

1.1.2 键入网址到网页显示,期间发生了什么?
1.1.2.1 孤单小弟 - HTTP
  1. 浏览器做的第一步工作是解析 URL,从而生成发送给 Web 服务器的请求信息:

    当没有路径名时,就代表访问根目录下事先设置的默认文件,也就是 /index.html 这些文件,这样就不会发生混乱了。

8011428

  1. 浏览器确定了 Web 服务器和文件名后,接下来就是根据这些信息来生成 HTTP 请求消息了:

HTTP 的消息格式

1.1.2.2 真实地址查询 - DNS

通过浏览器解析 URL 并生成 HTTP 消息后,需要委托操作系统将消息发送给 Web 服务器,

但必须提供通信对象的 IP 地址。而DNS 服务器保存了 Web 服务器域名与 IP 地址的对应关系。

  1. 用户请求域名。检查本地 DNS 缓存。

  2. 本地 DNS 服务器查询根 DNS 服务器。

  3. 根 DNS 服务器查询顶级 DNS 服务器。

  4. 顶级 DNS 服务器查询权威 DNS 服务器。

  5. 返回 IP 地址给用户。

DNS 树状结构

1.1.2.3 指南好帮手 - 协议栈

通过 DNS 获取到 IP 后,就可以把 HTTP 的传输工作交给操作系统中的协议栈。

协议栈的内部分为几个部分,分别承担不同的工作。上下关系是有一定的规则的,上面会向下面委托工作,下面收到工作并执行。

img

应用程序(浏览器)通过调用 Socket 库,来委托协议栈工作。协议栈的上面是 TCP 和 UDP,接受应用层的委托执行收发数据的操作。

下面是 IP 协议,数据被切分成一块块的网络包,而将包发送给对方的操作就是由 IP 负责的。此外 IP 中还包括 ICMP 和 ARP:

IP 下面的 网卡驱动程序 负责控制网卡硬件,再下面的 网卡 则完成实际的收发操作,即对网线中的信号执行收发操作。

1.1.2.4 可靠传输 - TCP
  1. HTTP 是基于 TCP 协议传输的,所以在这我们了解下 TCP 协议。先看看 TCP 报文头部 的格式:

    • 源端口号和目标端口号:是不可少的,如果没有这两个端口号,数据就不知道应该发给哪个应用

    • 包的序号:是为了解决包乱序的问题

    • 确认号:是确认发出去对方是否有收到。如果没有收到就应该重新发送,直到送达,这个是为了解决丢包的问题

    • 状态位:SYN 是发起连接,ACK 是回复,RST 是重新连接,FIN 是结束连接

    • 窗口大小:TCP 会做流量控制,通信双方各声明一个窗口标识处理能力,防止太快或太慢。还会做拥塞控制,控制发送的速度

  2. 在 HTTP 传输数据之前,首先需要 TCP 建立连接,通常称为三次握手:(保证双方都有收发的能力)

    TCP 三次握手

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

    • 然后客户端主动发起连接 SYN,之后处于 SYN-SENT 状态。

    • 服务端收到发起的连接,返回 SYN,并且 ACK 客户端的 SYN,之后处于 SYN-RCVD 状态。

    • 客户端收到服务端发送的 SYN 和 ACK 之后,发送对 SYN 确认的 ACK,之后处于 ESTABLISHED 状态,因为一发一收成功。

    • 服务端收到 ACK 的 ACK 之后,处于 ESTABLISHED 状态,因为也一发一收了。

    如何查看 TCP 的连接状态?在 Linux 可以通过 netstat -napt 命令查看:

TCP 连接状态查看

  1. TCP 分割数据:

    如果 HTTP 请求消息比较长,超过了 MSS 的长度,TCP 就把数据拆解成一块块的数据发送,而不是一次性发送所有数据。

    • MTU:一个网络包的最大长度,以太网中一般为 1500 字节

    • MSS:除去 IP 和 TCP 头部之后,一个网络包所能容纳的 TCP 数据的最大长度

    数据会被以 MSS 的长度进行拆分,拆分出来的每一块数据加上 TCP 头信息,然后交给 IP 模块来发送数据:

数据包分割

  1. TCP 报文 生成:

    TCP 协议里面有两个端口,是浏览器监听端口(随机生成)、Web 服务器监听端口(HTTP 默认 80, HTTPS 默认 443)。

    在双方建立了连接后,TCP 报文中的数据部分就是存放 HTTP 头部 + 数据,组装好 TCP 报文之后,就需交给下面的网络层处理。

TCP 层报文

1.1.2.5 远程定位 - IP
  1. TCP 在执行连接、收发、断开等各阶段操作时,都需要委托 IP 将数据封装成网络包发送给通信对象。先看看 IP 报头 的格式:

    • 源地址 IP,即是客户端输出的 IP 地址

    • 目标地址,即通过 DNS 域名解析得到的 Web 服务器 IP

    • 协议号要填写为 `06(十六进制),表示传输协议为 TCP

  2. 如果客户端有多个网卡,就会有多个 IP。在填写源地址 IP 时,就需要根据路由表规则,判断填写哪一个地址。

    可以使用 route -n 命令查看当前系统的路由表。举个例子,假设 Web 服务器的目标地址是 192.168.10.200

路由规则判断

第三条目比较特殊,它的目标地址和子网掩码都是 0.0.0.0,这表示默认网关。

如果其他所有条目都无法匹配,就会自动匹配这一行。并且后续就把包发给路由器,Gateway 即是路由器的 IP 地址。

  1. IP 报文 生成:

IP 层报文

1.1.2.6 两点传输 - MAC
  1. 生成了 IP 头部之后,接下来网络包还需要在 IP 头部的前面加上 MAC 头部

    • 目标 MAC 地址:指定数据帧的接收方 MAC 地址

    • 源 MAC 地址:标识数据帧的发送方 MAC 地址

    • 协议类型:指示数据帧中封装的上层协议类型,如 0800 代表 IP 协议,0806 代表 ARP 协议等

    MAC 头部是以太网使用的头部格式,用于在局域网中传输数据帧。它包含了发送方和接收方的 MAC 地址等信息。

  2. MAC 发送方和接收方如何确认?

    • 发送方的 MAC 地址通常是从发送数据的网络接口(网卡)获取的,存储在网卡的 ROM 中。

    • 接收方的 MAC 地址需要根据目标 IP 地址动态获取,这一过程通常依赖于 ARP 协议

    ARP(地址解析协议)通过广播的方式查询局域网中的设备,以获取指定 IP 地址对应的 MAC 地址。

    为了提高效率,操作系统会将最近的 ARP 查询结果存储在 ARP 缓存中,以便将来重复使用。

    可以使用命令 arp -a 查看 ARP 缓存的内容,这些缓存项包含了 IP 地址到 MAC 地址的映射关系及其生存时间信息。

ARP 广播

  1. MAC 报文 生成:

MAC 层报文

1.1.2.7 出口 - 网卡
  1. 网络包只是存放在内存中的一串二进制数字信息,没有办法直接发送给对方。

    需要将数字信息转换为电信号,才能在网线上传输。负责执行这一操作的是网卡,要控制网卡还需要网卡驱动程序。

  2. 驱动获取网络包之后,将其复制到网卡缓存区,接着在开头加上报头和起始帧分界符,在末尾加上帧校验序列。

    • 起始帧分界符:用来表示包起始位置的标记

    • 帧校验序列(FCS):用来检查包传输过程是否有损坏

    最后网卡会将包转为电信号,通过网线发送出去。

数据包

1.1.2.8 送别者 - 交换机

交换机是二层网络设备,工作在 MAC 层,负责局域网内部的数据帧转发。

  1. 包的接收与转发:

    • 接收过程:电信号通过网线接口进入交换机,转换为数字信号。数据包经过 FCS 校验确认无误后,存放到交换机的缓冲区。

    • 交换机与网卡的区别:网卡根据目标 MAC 地址决定是否接收包。交换机端口接收所有包,不区分目标 MAC 地址。

    • MAC地址表:记录设备的 MAC 地址与其连接的端口对应关系。当接收到包时,查表确定目标 MAC 地址对应端口。

    • 转发过程:如果表中找到目标地址,则将包转发到对应端口。如果没找到,则向除了源端口之外的所有端口广播该包。

交换机的 MAC 地址表

  1. 找不到 MAC 地址的情况:

    • 原因:设备从未与交换机通信,或地址表中的记录已过期。

    • 处理:交换机广播该包到所有端口,直到设备响应,然后更新 MAC 地址表。

  2. 广播地址:

    • MAC 地址:FF:FF:FF:FF:FF:FF,发送到所有设备。

    • IP 地址:255.255.255.255,发送到本地网络内的所有设备。

1.1.2.9 出境大门 - 路由器
  1. 基本定义:

    • 交换机:二层网络设备,基于以太网设计,通过学习 MAC 地址实现内部转发。

    • 路由器:三层网络设备,基于 IP 设计,使用路由表决定数据包的转发路径。

  2. 网络包处理:

    • 交换机:根据数据包的目标 MAC 地址,将数据包转发到正确的端口,仅在本地网络中操作。

    • 路由器:根据目标 IP 地址查找路由表,决定下一跳路由器或目标设备,跨越不同网络进行数据包转发。

路由器转发

  1. 地址特性:

    • 交换机:端口不具备 IP 地址,只处理 MAC 地址。

    • 路由器:每个端口都有独立的 MAC 地址和 IP 地址,既可以作为以太网的终端设备,也能进行 IP 数据包的转发。

  2. 数据包转发过程:

    • 交换机:根据学习的 MAC 地址表,将数据包直接发送到目标设备的端口。

    • 路由器:接收数据包后,根据目标 IP 地址 查询路由表,选择最佳路径转发数据包。

  3. 工作原理:

    • 交换机:快速转发数据包,减少冲突和碰撞,提高网络性能。

    • 路由器:根据网络的逻辑结构(子网划分等)决定数据包的转发路径,实现跨网络的通信。

1.1.2.10 互相扒皮 - 服务器 与 客户端
  1. 数据包抵达服务器

    • 服务器收到数据包后,先检查 MAC 头部,验证是否与自己的 MAC 地址匹配。

    • 确认 MAC 地址无误后,进一步检查 IP 头部,验证 IP 地址是否匹配。

  2. TCP 协议处理

    • 如果 IP 地址匹配,服务器解析 TCP 头部,检查序列号确定数据包的顺序和完整性。

    • 若序列号正确,服务器发送确认 ACK,并将数据包存入缓存。

    • TCP 头部还包含目标端口号,服务器的 HTTP 进程监听该端口号。

  3. HTTP 响应

    • HTTP 进程确认请求,将请求的网页封装在 HTTP 响应报文中。

    • HTTP 响应报文依次穿上 TCP、IP、MAC 头部。

  4. 数据包返回客户端

    • 经过服务器的网卡发送到交换机,由交换机路由到路由器,然后经过多个路由器最终到达客户端。

  5. 客户端处理

    • 客户端接收到数据包后,验证 IP 头部,确认数据包来源。

    • 客户端的网卡将数据包交给交换机,通过交换机路由到客户端。

    • 客户端 HTTP 进程扒开数据包,提取 HTTP 响应报文,准备渲染页面。

  6. 断开连接

    • 客户端完成页面渲染后,发起 TCP 四次挥手,与服务器断开连接。

网络分层模型

1.1.3 Linux 系统是如何收发网络包的?
1.1.3.1 网络分层模型?
  • OSI 七层模型:是一个概念性框架,用于标准化不同计算机系统之间的通信过程。

    1. 物理层:负责传输原始比特流,涉及电气、机械、过程和功能标准,如电压、线速、物理链路等。

    2. 数据链路层:负责在相邻网络节点之间传输帧,处理帧同步、差错控制、流量控制等。

    3. 网络层:负责数据包从源到目的地的传输和路由选择。

    4. 传输层:负责提供端到端的通信,确保数据的完整性和可靠性,如 TCP(传输控制协议)和 UDP(用户数据报协议)。

    5. 会话层:负责建立、管理和终止应用程序之间的会话。

    6. 表示层:负责数据的表示、安全和压缩,确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取。

    7. 应用层:为应用软件提供网络服务,如 HTTP(超文本传输协议)、FTP(文件传输协议)等。

  • TCP/IP 四层模型:是一个实际的协议族,用于实现网络通信。

    1. 网络接口:与 OSI 模型的物理层和数据链路层相对应,负责在网络的物理设备之间传输 原始比特流

    2. 网络层:与 OSI 模型的网络层相对应,负责 数据包 的传输和路由选择。

    3. 传输层:与 OSI 模型的传输层相对应,负责在网络中两个节点之间提供可靠的 通信

    4. 应用层:与 OSI 模型的会话层、表示层和应用层相对应,负责处理特定的 应用程序 细节。

  • 分层目的: 简化网络设计,将复杂的通信分解为多个简单子系统。每层专注于特定功能,通过接口进行交互。

img

1.1.3.2 Linux 网络协议栈
  1. 网络封装

    • 发送 TCP 协议通信的网络包时,数据按照网络协议栈层层封装和处理,每层增加特定的协议头。

    • 应用层的数据像身体,传输层的 TCP 头像打底衣,网络层的 IP 头像外套,网络接口层的帧头和帧尾像帽子和鞋子。

  2. 网络包大小

    • 以太网规定最大传输单元(MTU)为 1500 字节,限制单次传输的 IP 包大小。

    • 超过 MTU 大小的网络包会在网络层分片,以保证传输效率和数据完整性。

  3. Linux 网络协议栈

    • Linux 网络协议栈与 TCP/IP 模型类似,从应用程序通过系统调用与 Socket 层进行数据交互开始,

      向下依次是传输层、网络层、网络接口层,最底层是网卡驱动程序和硬件网卡设备。

img

1.1.3.3 Linux 接收网络包的流程
  1. DMA 技术

    • 网卡通过 DMA 技术将接收到的网络包写入 环形缓冲区(Ring Buffer),然后通知操作系统网络包已到达。

  2. 中断处理方式

    • 初始方式是通过硬件中断告知操作系统每个网络包的到达。

    • 在高性能场景下,频繁的中断会使 CPU 处理中断的时间过长,影响系统效率。

  3. NAPI 机制的引入

    • Linux 内核在 2.6 版本引入 NAPI 机制,结合中断和轮询的方式处理网络包,减少中断频率。

    • 硬件中断处理函数会暂时屏蔽中断,之后通过软中断轮询数据。

  4. 软中断的处理

    • 内核中的 ksoftirqd 线程负责软中断处理,从环形缓冲区中获取数据帧(sk_buff),并交给网络协议栈处理。

  5. 网络协议栈的处理

    • 网络包经过网络接口层、网络层和传输层的逐层处理。

    • 网络接口层检查报文合法性,确定上层协议类型,然后去掉帧头和帧尾交给网络层。

    • 网络层处理 IP 包,确定包的下一步走向,如本机处理或转发。

    • 传输层根据四元组找到对应的 Socket,将数据放入 Socket 的接收缓冲区。

  6. 应用层处理

    • 应用程序通过 Socket 接口获取内核 Socket 接收缓冲区的数据,进行进一步处理并唤醒用户进程。

img

1.1.3.4 Linux 发送网络包的流程
  1. 第一次内存拷贝:

    当应用程序调用发送数据包时,内核会为该数据包创建一个 sk_buff 结构体,并从用户空间将数据拷贝到这个 sk_buff 内存区域中。

    这是因为数据从用户态到内核态的过程中,需要确保数据的安全性和一致性。

  2. 第二次内存拷贝:

    在使用 TCP 发送数据时,为了实现 TCP 的可靠传输机制,内核会在将 sk_buff 传递到网络层之前,克隆一个新的 sk_buff 副本。

    将副本发送到网络层,直到收到对方的 ACK 之后才会释放。原始的 sk_buff 保留在传输层,以便在需要重传时使用。

  3. 第三次内存拷贝(可选):

    在网络层处理过程中,如果发现数据包大小超过了 MTU(最大传输单元),需要对数据包进行分片。

    这时会为每个分片申请一个新的 sk_buff,并将原始的 sk_buff 数据拷贝为多个小的 sk_buff,以便适应网络传输要求。

1.1.4 如何设计一个应用层协议?

应用层协议是网络协议栈中最顶层的协议,它直接为应用程序提供网络通信服务。需要解决的核心问题包括:

  • 解析传输层数据:从字节流中解析出有意义的数据包。涉及识别协议头和数据部分,以及处理可能的分包和粘包问题。

  • 将数据交给应用程序:解析完数据后,需要传递给相应的应用程序,以便应用程序可以进一步处理或使用这些数据。

实现应用层协议的基本思路通常包括以下几个步骤:

  1. 定义协议头格式:协议头通常包含了用于识别协议、处理请求和响应、序列化方法、状态码等信息。

  2. 解析协议头:根据定义的协议头格式,解析出协议头中的信息,如请求类型、序列化方法、数据长度等。

  3. 解析数据部分:根据协议头中的信息,解析数据部分,这可能涉及到序列化和反序列化的处理。

1.2 网络 HTTP

1.2.1 HTTP 基本概念

HTTP(HyperText Transfer Protocol)是一种用于在计算机网络中传输超文本(包括文本、图片、视频等)的应用层协议。

  1. 协议

    协议是通信参与者之间约定的规则和格式。HTTP 是一种协议,规定了客户端和服务器之间如何进行通信和数据交换的标准。

  2. 传输 HTTP 负责在网络中传输数据,这涉及到客户端向服务器发送请求,服务器响应这些请求并返回相应的数据。

    HTTP是一种双向协议,允许数据在客户端和服务器之间双向传输。通信可以经过中间人(代理服务器),只要遵循协议即可。

  3. 超文本

    超文本是 HTTP 传输的内容,包括各种格式的文本。例如 HTML 文件,这些文件包含文本、图片、视频、超链接等。

  4. 状态码 HTTP 使用状态码来表示服务器对请求的处理结果,以下是常见分类:

    • 1xx:信息类状态码。表示 请求 已被接收,继续处理

    • 2xx:成功状态码。表示服务器 成功处理 了请求

    • 3xx:重定向状态码。表示需要客户端 进一步 操作以完成请求

    • 4xx:客户端错误状态码。表示 客户端 发送的请求有误

    • 5xx:服务器错误状态码。表示 服务器 处理请求时有误

  5. HTTP 常见字段

    HTTP 报文中包含多个字段,用于传递请求和响应的各种信息,例如:

    • Host:指定 服务器 的域名或 IP 地址。

    • Connection:指定客户端请求使用的 连接类型,例如 Keep-Alive。

    • Content-Length:指定响应体的 长度

    • Content-Type:指定响应的 数据类型,例如 text/html。

    • Content-Encoding:指定响应数据的 压缩方法,例如 gzip。

    这些字段帮助客户端和服务器正确地解释和处理 HTTP 通信中的数据和指令。

1.2.2 GET 与 POST
  1. GET 请求:

    • 语义和用途: 获取指定资源,通常用于从服务器获取数据。例如,浏览网页、查看文章等。

    • 参数位置: 参数一般写在 URL 中,通过查询字符串传递。

    • 数据编码: URL 仅支持 ASCII 字符,因此请求参数也仅限于 ASCII 字符集。

    • 数据长度限制: 浏览器对 URL 长度有限制,但 HTTP 协议本身并未规定具体长度限制。

    • 安全性和幂等性: 安全且幂等。因为是只读操作,多次执行结果一致,且不修改服务器资源。

  2. POST 请求:

    • 语义和用途: 根据请求负荷(报文 body)对指定资源进行处理,用于向服务器提交数据。例如,提交表单、上传文件等。

    • 参数位置: 参数写在请求的报文 body 中,可以包含任意格式的数据。

    • 数据编码: 可以传输任意格式的数据,不受 ASCII 字符限制。

    • 数据长度限制: 浏览器对 body 大小没有限制,但服务器和应用程序可能有限制。

    • 安全性和幂等性: 不安全且不幂等。因为可能修改服务器上的资源,且多次提交可能导致多个资源的创建。

  3. 补充说明:

    • 实际使用中的变通: 虽然按照 RFC 规范,GET 和 POST 具有上述语义,但开发者有时可能不遵循这些规范。

      例如使用 GET 修改数据或使用 POST 查询,此时安全性和幂等性有所变化,所以需要具体情况具体分析。

    • 安全传输问题: HTTP 协议传输内容为明文,无论 GET 还是 POST,为防止数据被窃取,应使用 HTTPS 加密传输数据。

1.2.3 HTTP 缓存技术

HTTP 缓存可以通过两种主要方式实现:强制缓存和协商缓存。

  1. 强制缓存:

    通过设置 HTTP 响应头部实现,它指示浏览器在一定时间内直接从本地缓存获取资源,而不需要发送请求。常见的头部字段有:

    • Cache-Control: 是一个用于控制缓存行为的通用字段,常见的指令有:

      • max-age=<seconds>: 指定资源被认为是新鲜的最大时间,以秒为单位。

      • no-cache: 强制浏览器发送请求到服务器验证缓存是否过期。

      • no-store: 要求缓存不存储任何有关客户端请求和服务器响应的内容。

    • Expires: 是一个 HTTP/1.0 的响应头部字段,指定资源过期的日期和时间,在这之前不需要再次请求服务器。

    如果同时存在 Cache-Control 和 Expires,Cache-Control 的优先级更高,推荐使用它来控制强制缓存。

  2. 协商缓存:

    在资源过期时,通过与服务器进行协商,判断是否可以继续使用本地缓存而不需要重新下载资源。常见的头部字段有:

    • Last-ModifiedIf-Modified-Since:

      • Last-Modified: 是服务器响应头部的字段,指示资源的最后修改时间。

      • If-Modified-Since: 是客户端请求头部的字段,如果资源在指定时间之后没有被修改,则返回 304 表示继续使用本地缓存。

    • ETagIf-None-Match:

      • ETag: 是服务器响应头部的字段,用于唯一标识资源版本。可以是任意字符串,通常是根据资源内容生成的哈希值。

      • If-None-Match: 是客户端请求头部的字段,带上当前资源的 ETag 值。

        服务器比较客户端提供的 ETag 和当前资源的 ETag 是否一致,如果一致,则返回 304 表示继续使用本地缓存。

      相比强制缓存,优势在于可以更精确地判断资源是否被修改,避免了资源未变但修改时间更新的问题。

  3. 工作流程:

    • 强制缓存:浏览器第一次请求资源时,服务器返回资源并设置 Cache-ControlExpires,浏览器将资源缓存。

      再次请求时,浏览器根据缓存控制策略决定是否使用缓存。

    • 协商缓存:当资源过期或浏览器强制验证时,浏览器发送带有 If-Modified-SinceIf-None-Match 的请求头部。

      服务器根据请求头部字段与资源的 Last-ModifiedETag 进行比较,返回对应的状态码来决定是否使用缓存。

1.2.4 HTTP 特性
  1. HTTP/1.1 的优点:

    • 简单:HTTP/1.1 的报文格式简单明了,由头部信息和主体组成,易于理解和实现。

    • 灵活和易于扩展:允许开发人员自定义请求方法、URI、状态码和头字段,有助于协议的灵活性和扩展性。

    • 应用广泛和跨平台:HTTP/1.1 在互联网上应用广泛,支持多种平台和设备,适用于各种应用场景。

  2. HTTP/1.1 的缺点:

    • 无状态:服务器不保留客户端的状态信息,导致需要额外的机制(如 Cookie)来管理用户状态,增加了复杂性。

    • 明文传输:HTTP/1.1 的通信内容是明文传输,安全性较低,容易遭受窃听和篡改攻击。

    • 不安全:缺乏加密机制,容易被中间人攻击篡改或窃听通信内容,例如用户登录信息可能会被盗取。

  3. HTTP/1.1 的性能:

    • 长连接:引入了持久连接(Keep-Alive),减少了重复建立和断开 TCP 连接的开销,提升了性能。

    • 管道化:支持管道化请求,允许在同一个 TCP 连接上同时发送多个请求,但存在服务器端响应顺序问题和队头阻塞风险。

    • 队头阻塞:请求 - 应答模式可能导致的性能问题,一个请求的阻塞会影响到整个连接中的其他请求,影响响应速度。

1.2.5 HTTP 与 HTTPS
  1. HTTP 与 HTTPS 的区别:

    • 安全性

      • HTTP:信息是明文传输,安全性较差,容易被窃听、篡改或冒充。

      • HTTPS:在传输层加入 SSL/TLS 加密,确保数据传输的安全性和完整性。

    • 连接建立过程

      • HTTP:TCP 三次握手后即可传输数据。

      • HTTPS:除了 TCP 三次握手外,还需要 SSL/TLS 握手 来建立安全连接。

    • 默认端口

      • HTTP:默认端口是 80。

      • HTTPS:默认端口是 443

    • 安全证书

      • HTTPS:需要通过 CA 机构申请的数字证书,用于验证服务器身份,保证通信的安全性和真实性。

  2. HTTPS 解决了 HTTP 的哪些问题?

    • 窃听风险:采用 SSL/TLS 加密,确保数据在传输过程中无法被窃取。

    • 篡改风险:使用摘要算法(哈希函数)生成数据的唯一指纹,保证数据在传输过程中不被篡改。

    • 冒充风险:通过数字证书,证明服务器的身份的真实性,防止客户端被冒充的风险。

  3. HTTPS 的安全机制:

    • 混合加密

      • 说明:结合对称加密(加密速度快,但密钥安全性低)和非对称加密(密钥安全,但加解密速度慢)。

      • 过程:首先使用非对称加密交换对称加密的会话密钥,然后使用对称加密算法加密传输数据。

    • 摘要算法 + 数字签名

      • 摘要算法:通过哈希算法生成数据的唯一指纹,用于验证数据的完整性。

      • 数字签名:使用私钥对摘要进行加密,以证明数据的来源和完整性,公钥用于验证签名的有效性。

    • 数字证书

      • 作用:由 CA 颁发,包含公钥和服务器身份信息,用于验证服务器的真实性。

      • 验证过程:客户端通过 CA 根证书验证服务器的数字证书是否合法,以确保通信安全可靠。

  4. HTTPS 是如何建立连接的?其间交互了什么?

    HTTPS 是一种通过加密通信协议,用于安全地传输数据。下面是 HTTPS 建立连接的基本过程:

    • ClientHello(客户端请求):

      • 客户端向服务器发送加密通信请求。

      • 发送支持的 TLS 协议版本、客户端生成的随机数(Client Random)和支持的密码套件列表(包括加密算法等)。

    • ServerHello(服务器响应):

      • 服务器收到客户端请求后响应。

      • 确认 TLS 协议版本、服务器生成的随机数(Server Random)、确认密码套件列表和服务器的数字证书(含公钥)。

    • 客户端回应:

      • 客户端收到服务器响应后,验证服务器的数字证书的真实性(使用本地的 CA 公钥)。

      • 如果验证通过,客户端生成一个随机数(pre-master key),用服务器的公钥加密,并发送给服务器。

      • 客户端发送加密通信算法改变通知和客户端握手结束通知。

    • 服务器最后回应:

      • 服务器收到客户端的 pre-master key 后,使用自己的私钥解密。

      • 双方使用三个随机数和协商的加密算法生成会话密钥(session key)。

      • 服务器发送加密通信算法改变通知和服务器握手结束通知,表示握手阶段结束。

  5. 数字证书的验证过程:

    • 客户端从服务器接收到证书,首先,根据本地预置的根证书列表,验证证书的签发机构(CA)和中间证书是否可信。

    • 然后,使用相应的公钥解密签名部分,以验证完整性和真实性。检查证书是否在有效期内,是否包含正确的域名。

    • 最后,通过逐级验证,客户端建立信任链,确认服务器证书的合法性。

    为什么需要证书链?

    • 证书链确保了整个验证过程的安全性和完整性。使用根证书直接颁发服务器证书可能会面临根证书的私钥安全性问题。

    • 通过中间证书,可以有效地隔离根证书,降低根证书泄露的风险,同时确保整个信任链的可验证性和可信任性。

  6. HTTPS 的应用数据如何保证完整性?

    HTTPS 通过 TLS(Transport Layer Security)协议来保证应用数据的完整性。TLS 协议包括两部分:

    • TLS 握手协议:负责协商加密算法和生成对称密钥。客户端和服务器之间通过四次握手来确认通信参数,包括生成密钥的过程。

    • TLS 记录协议:负责实际的数据传输保护。主要功能包括消息的分割、压缩、加密和数据的认证(通过消息认证码 MAC)。

      具体流程包括:

      • 首先将消息分割成较短的片段并进行压缩,每个片段添加 MAC 以验证数据完整性。

      • 然后使用对称密钥加密片段和 MAC。最终将数据添加报头后传递给传输层协议(如 TCP)进行传输。

  7. HTTPS 一定安全可靠吗?

    HTTPS 本身是安全可靠的,但在特定情况下可能存在风险。具体情况包括:

    • 中间人攻击过程:中间人服务器伪造证书并与客户端和服务端分别建立 TLS 连接,从而可以截取和篡改 HTTPS 数据。

    • HTTPS 安全性和用户行为:HTTPS 本身没有漏洞,但当用户忽略浏览器的安全警告时,HTTPS 连接可能被破坏。

    • 抓包工具原理:抓包工具如 Fiddler 通过在客户端安装其自己生成的根证书,使得浏览器信任其伪造的 HTTPS 连接。

    如何避免被中间人抓取数据?

    • 保持设备安全:确保操作系统和浏览器是最新的,防止恶意软件感染。

    • 注意证书警告:不要忽略浏览器的证书警告,避免继续访问不信任的网站。

    • 使用 HTTPS 双向认证:双向认证要求服务端和客户端彼此验证证书的有效性,可以防止中间人攻击。

1.2.6 HTTP/1.1、HTTP/2、HTTP/3 演变
  1. HTTP/1.1 做了哪些优化?

    • 使用长连接的方式改善了 HTTP/1.0 短连接造成的性能开销。

    • 支持管道网络传输,第一个请求发出去了,不必等其回来就可以发第二个请求,减少整体的响应时间。

    缺陷:HTTP/1.1 存在性能瓶颈:

    • 请求 / 响应头部未经压缩就发送,首部信息越多延迟越大,只能压缩 Body 的部分。

    • 发送冗长的首部,每次互相发送相同首部造成的浪费较多。

    • 服务器按请求的顺序响应,没有优先级控制。如果服务器响应慢,会导致客户端一直请求不到数据,即队头阻塞。

    • 请求只能从客户端开始,服务器只能被动响应。

  2. HTTP/2 做了哪些优化?

    • 头部压缩:

      • 引入了 HPACK 算法进行头部压缩。它维护了一个头信息表,存储了请求和响应中的头部字段。

      • 重复的字段只发送 索引号,而不是完整的字段内容,从而减少了重复传输的数据量,提高了传输效率。

    • 二进制格式:

      • 引入了二进制格式传输数据,替代 HTTP/1.x 中的纯文本格式,加快了数据传输速度和效率。

    • 并发传输:

      • 引入了多路复用(Multiplexing),允许多个请求和响应在同一个连接上同时交错传输。

      • 每个请求和响应被分解为多个帧,通过唯一的标识符来区分。这消除了队头阻塞问题,允许并行处理多个请求。

    • 服务器推送:

      • 支持了服务器主动推送资源给客户端。服务器可以预测客户端需要的资源,并在其请求之前推送给客户端。

      • 这减少了额外的请求延迟,提高了页面加载速度和性能。

    • 缺陷:TCP 层面的队头阻塞:

      • 因为依赖于 TCP 传输数据,而 TCP 是一种面向字节流的协议,它要求接收方按序列接收数据。

      • 如果某个数据包丢失,后续将被暂时阻塞。直到丢失的数据包被重新传输。

  3. HTTP/3 做了哪些优化?

    • 无队头阻塞:

      HTTP/1.1 和 HTTP/2 的问题:

      • HTTP/1.1 的管道解决了请求的队头阻塞,但未解决响应的队头阻塞。

      • HTTP/2 使用单个 TCP 连接复用多个请求,但丢包会导致所有请求阻塞。

      HTTP/3 的解决方案:

      • 使用基于 UDP 的 QUIC 协议替代 TCP,UDP 不关心顺序和丢包,避免了 HTTP/2 的队头阻塞问题。

      • QUIC 实现了类似 TCP 的可靠性传输,但允许多个流(类似多路复用),每个流独立处理,不互相阻塞。

    • 更快的连接建立:

      HTTP/1.1 和 HTTP/2 的问题:

      • HTTP/1 和 HTTP/2 需要分阶段进行 TCP 握手和 TLS 握手,消耗多个往返时间(RTT)。

      HTTP/3 的改进:

      • QUIC 内部整合了 TLS(TLS/1.3),连接建立过程仅需 1 个 RTT 完成。

      • 支持 0-RTT 重连,允许在第二次连接时直接发送应用数据包,提高了连接的速度和效率。

    • 连接迁移:

      TCP 连接的限制:

      • TCP 通过四元组(源 IP、源端口、目的 IP、目的端口)绑定连接,网络切换(如 4G 到 WiFi)需要断开重连。

      QUIC 的优势:

      • QUIC 通过连接 ID 标记通信端点,不依赖具体的 IP 地址,支持无缝连接迁移。

      • 移动设备切换网络时,保持连接信息(如连接 ID 和 TLS 密钥),无需重新握手,消除了重连的延迟和卡顿。

1.3 网络 TCP

1.3.1 TCP 基本认识
1.3.1.1 TCP 头格式(报文)?IP 头格式(报文)?

1. TCP 头重要字段

  1. 序列号(Sequence Number):这是TCP连接中数据传输的基础,确保数据的有序性和完整性。

  2. 确认号(Acknowledgment Number):用于确认接收方已经成功接收到的数据,是TCP可靠传输的关键。

  3. 控制位(Control Flags):特别是SYN、ACK、FIN和RST,它们控制着TCP连接的建立、维护和终止。

    • SYN(同步):用于建立连接。

    • ACK(确认):确认收到的数据。

    • FIN(结束):用于优雅地关闭连接。

    • RST(重置):用于异常情况下的连接重置。

  4. 窗口大小(Window Size):用于流量控制,告诉发送方接收方还能接收多少数据,是TCP流量控制机制的核心。

  5. 检验和(Checksum):用于检测TCP头和数据在传输过程中是否出现错误。

2. IP 头重要字段

  1. 版本(Version):标识IP协议的版本,目前主要使用的是IPv4。

  2. 总长度(Total Length):包括IP头和数据的总长度,用于确定整个数据包的大小。

  3. 生存时间(Time to Live, TTL):限制数据包在网络中的传输路径,防止数据包无限循环。

  4. 协议(Protocol):指示上层协议类型,确保数据包被正确地传递给目标协议处理。

  5. 源IP地址和目的IP地址:标识数据包的发送方和接收方,是路由选择和数据传输的基础。

  6. 首部检验和(Header Checksum):用于检测IP头在传输过程中是否出现错误。

TCP 头格式

1.3.1.2 为什么需要 TCP 协议? TCP 工作在哪一层?

IP 层是不可靠的,它不保证网络包的交付、不保证网络包的按序交付、也不保证网络包中的数据的完整性。

需要由上层的 TCP 协议来负责:TCP 是一个可靠的数据传输服务,能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的。

1.3.1.3 什么是 TCP ?

TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。

1.3.1.4 什么是 TCP 连接?

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

所以我们可以知道,建立一个 TCP 连接是需要客户端与服务端达成上述三个信息的共识。

img

1.3.1.5 如何唯一确定一个 TCP 连接呢?
  1. TCP 四元组可以唯一的确定一个连接。作用:

    • 源地址和目的地址(32位):在 IP 头部中,用于确定数据包的发送和接收主机。

    • 源端口和目的端口(16位):在 TCP 头部中,用于确定数据包的发送和接收进程。

TCP 四元组

  1. 服务端的最大 TCP 连接数:

    服务端固定在本地端口上监听客户端的连接请求。客户端的 IP 和端口是动态分配的,连接的唯一标识由四元组确定。

    • 理论上的最大连接数:IPv4 中 IP 地址数最多为 2^32,端口数最多为 2^16,单台服务端最大并发连接数为 2^48。

    • 实际影响因素:每个 TCP 连接都占用一个文件描述符。Linux 系统对可打开的文件描述符数量有限制,包括:

      • 系统级:通过 /proc/sys/fs/file-max 查看当前系统可打开的最大文件描述符数量。

      • 用户级:通过 /etc/security/limits.conf 等配置文件指定用户可打开的最大数量。

      • 进程级:通过 /proc/sys/fs/nr_open 查看单个进程可打开的最大数量。

      • 内存限制: 每个 TCP 连接都会占用一定的内存资源。如果内存资源被耗尽,可能导致 Out of Memory (OOM) 错误。

1.3.1.6 UDP 和 TCP 有什么区别呢?分别的应用场景是?
  1. UDP 协议概述

    • 无连接服务:UDP 不需要建立连接即可传输数据,属于面向无连接的传输层协议。

UDP 头部格式

  1. TCP 与 UDP 的比较

    • 连接性质:TCP:面向连接的协议,传输前需要建立连接。UDP:无连接,即刻传输数据。

    • 服务对象:TCP:一对一的点对点服务。UDP:支持一对一、一对多、多对多的通信模式。

    • 可靠性:TCP:保证数据无差错、不丢失、不重复、有序到达。UDP:不保证可靠性,可能丢失数据或乱序。

    • 控制机制:TCP:有完善的拥塞控制和流量控制。UDP:没有拥塞控制,发送速率不受网络拥堵影响。

    • 首部开销:TCP:首部最小为 20 字节,可变长(有选项字段时)。UDP:固定为 8 字节,开销较小。

    • 传输方式:TCP:流式传输,无边界,保证顺序和可靠性。UDP:每个数据包有边界,可能丢失或乱序。

    • 分片处理:TCP:数据大于 MSS 时分片,目标主机在 TCP 层组装。UDP:数据大于 MTU 时在分片,目标主机在 IP 层组装。

  1. TCP 和 UDP 应用场景

    • 由于 TCP 是面向连接,可以保证数据的可靠性交付,因此经常用于:

      • FTP 文件传输、HTTP / HTTPS。

    • 由于 UDP 面向无连接,可以随时发送数据,且处理既简单又高效,因此经常用于:

      • DNS 、SNMP 等包总量较少的通信;视频、音频等多媒体通信;广播通信。

    为什么 UDP 头部没有 首部长度 字段,而 TCP 头部有首部长度字段呢?

    • 因为 TCP 头部有 可变长 的选项字段,而 UDP 头部长度则不会变化,无需记录首部长度。

    为什么 UDP 头部有 包长度 字段,而 TCP 头部则没有包长度字段呢?

    • 因为 IP 总长度、IP 首部长度,在 IP 首部格式是已知的。所以 TCP 总长度 = IP 总长度 - IP 首部长度

      因为 TCP 首部长度,在 TCP 首部格式是已知的。所以 TCP 数据长度 = TCP 总长度 - TCP 首部长度

1.3.1.7 TCP 和 UDP 可以使用同一个端口吗?

TCP 和 UDP 可以共享相同的端口号。以下是详细解释:

img

1.3.1.8 TCP 和 IP 的区别?TCP 和 UDP 的区别?

1. TCP 和 IP 的区别

  • TCP(Transmission Control Protocol,传输控制协议),位于传输层,负责在两个主机之间的数据传输。

    • TCP 确保数据包正确无误地从源传送到目的地,如果在传输过程中丢失或损坏,TCP 会重发数据直到接收方正确接收。

  • IP(Internet Protocol,互联网协议),位于网络层,负责将数据包从源传送到目的地。

    • IP 不保证数据包的顺序或可靠性,只是简单地将数据包从一个地方传到另一个地方。使用 IP 地址标识网络上的设备。

2. TCP 和 UDP 的区别

  • TCP(Transmission Control Protocol,传输控制协议)

    • 面向连接:在数据传输前,需要建立一个连接。

    • 可靠且有序:提供数据包确认、超时重传、数据顺序保证等机制,确保数据可靠传输。

    • 流量控制:根据接收方的接收能力调整发送速率。

    • 拥塞控制:网络拥塞时,减少数据的发送量。

  • UDP(User Datagram Protocol,用户数据报协议)

    • 无连接:不需要建立连接,可以直接发送数据。

    • 不可靠且不保证有序:不提供数据包确认或重传机制,数据可能会丢失或乱序到达。

    • 简单快速:由于没有复杂的控制机制,UDP的开销小,适用于对实时性要求高的应用。

    • 无流量控制和拥塞控制:发送方可以自由地发送数据,不考虑网络状况和接收方的处理能力。

1.3.2 TCP 连接建立
1.3.2.1 TCP 三次握手?TCP 四次挥手?状态触发条件?

1. TCP 三次握手

  • 第一次握手:

    • 客户端随机生成初始序列号 Client_ISN,设置 SYN(同步序列编号)标志位为 1,表示这是一个连接请求。

    • 客户端发送一个带有 SYN 标志位的报文段给服务端,之后客户端进入 SYN-SENT 状态。

  • 第二次握手:

    • 服务端接收到客户端的报文后,也随机生成初始序列号 Server_ISN,设置 SYN 和 ACK 标志位为 1。

    • 服务端将确认号(ACK number)字段设置为 Client_ISN + 1,以确认客户端的 SYN 报文。

    • 服务端发送带有 SYN 和 ACK 标志位的报文段给客户端,之后服务端进入 SYN-RCVD 状态。

  • 第三次握手:

    • 客户端接收到服务端的报文后,设置 ACK 标志位为 1,将确认号字段设置为 Server_ISN + 1,以确认服务端的报文。

    • 客户端发送带有 ACK 标志位的报文段给服务端,之后客户端和服务端都进入 ESTABLISHED 状态。

2. TCP 四次挥手

  • 第一次挥手:客户端发送 FIN 报文告诉服务端它已经完成数据发送,进入 FIN_WAIT_1 状态。

  • 第二次挥手:服务端收到后回复 ACK 报文,并进入 CLOSE_WAIT 状态。

  • 第三次挥手:服务端发送 FIN 报文告诉客户端它也完成数据发送,进入 LAST_ACK 状态。

  • 第四次挥手:客户端收到 FIN 报文后回复 ACK 报文,并进入 TIME_WAIT 状态等待 2MSL,以确保服务端收到最后的 ACK。

TCP 三次握手

3. 如果两个服务器同时发出请求需要几次握手?

如果两个服务器同时发出请求,它们会各自启动一个三次握手的过程,同时扮演客户端和服务器的角色。

这样,两个服务器之间就建立了两个独立的TCP连接,每个连接都需要三次握手,总共是六次握手过程。

 

1.3.2.2 如何在 Linux 系统中查看 TCP 状态?

TCP 的连接状态查看,在 Linux 可以通过 netstat -napt 命令查看。

TCP 连接状态查看

1.3.2.3 为什么是三次握手?不是两次、四次?
  1. 防止历史连接的初始化:

    • 主要原因: 三次握手旨在防止旧的重复连接初始化导致的混乱。

      如果只有两次握手,服务端无法区分新的请求和之前丢失而未成功建立的请求。可能导致创建冗余连接,浪费资源。

    • 示例情景: 如果客户端先发送了一个 SYN 报文(序列号 seq=90),但由于网络问题未能成功到达服务端。

      客户端重启并发送了新的 SYN 报文(seq=100),服务端需要通过三次握手确保只有最新请求被接受,避免历史连接的混乱。

  2. 初始序列号:

    • 关键因素: 通信双方都需要维护一个序列号来确保数据包按序传输和防止数据包重复。

      通过三次握手,客户端和服务端可以交换彼此的初始序列号,从而同步它们的通信状态。

    • 序列号的作用: 序列号不仅用于数据包的顺序传输,还能标识已经收到的数据包,以及在数据包丢失时进行重传和恢复。

1.3.2.4 为什么每次建立 TCP 连接时,初始化的序列号都要求不一样呢?
  1. 防止历史报文被下一个相同四元组的连接接收。

    TCP 协议中,初始化序列号(ISN)的不同保证了每次连接的序列号空间不重叠。这样做的原因在于网络中存在滞留的数据包。

    如果新建的连接使用了与之前相同的序列号,那么这些数据包就有可能在新连接中被错误地接收和处理,导致数据错乱或安全问题。

  2. 提升安全性,防止黑客伪造相同序列号的 TCP 报文被对方接收。

    黑客可能会尝试伪造之前某个连接的序列号来进行攻击或欺骗。通过确保每次的初始化序列号都不同,可以增加黑客攻击的难度。

1.3.2.5 初始序列号 ISN 是如何随机产生的?

RFC793 提到初始化序列号 ISN 的随机生成算法:ISN = M + F (localhost, localport, remotehost, remoteport)

1.3.2.6 既然 IP 层会分片,为什么 TCP 层还需要 MSS 呢?
  1. MTU(最大传输单元)

    • 定义:一个网络包的最大长度,通常以字节为单位。在以太网中,MTU 一般为 1500 字节。

    • 作用:决定了可以在单个数据包中传输的最大数据量,超过 MTU 的数据包需要进行分片。

  2. MSS(最大分段大小)

    • 定义:在 TCP 协议中,MSS 是除去 IP 和 TCP 头部后,一个 TCP 数据包所能容纳的最大数据量。

  1. TCP 报文分片和重传:

    • 如果 IP 层在传输过程中需要分片,会增加网络负担,并增加丢包导致的重传次数。

    • IP 层的分片丢失会导致整个 TCP 报文的重传,这会影响传输效率和延迟。

  2. 优化策略:

    • 协商 MSS 值:TCP 在建立连接时协商双方的 MSS 值,确保数据包大小不超过 MTU,避免 IP 层的分片。

    • 重传优化:TCP 在发生丢包时,仅重传丢失的数据段,而不是整个报文,提高了重传的效率和网络利用率。

1.3.2.7 第一次握手丢失了,会发生什么?
  1. 超时重传机制:

    • 如果客户端迟迟未收到服务端的 SYN-ACK 报文,将触发超时重传机制,即 客户端重发 SYN 报文

    • 重传的 SYN 报文序列号相同:每次重传的 SYN 报文都具有相同的序列号,有助于服务端识别和处理重传的数据包。

  2. 重传的次数和间隔:

    • 在 Linux 中,默认的 tcp_syn_retries 值为 5,即客户端会尝试重传 SYN 报文最多 5 次。

    • 每次时间间隔是前一次的 2 倍。例如,第一次超时后等待 1 秒重传,第二次超时后等待 2 秒重传,依此类推。

1.3.2.8 第二次握手丢失了,会发生什么?
  1. 第二次握手丢失的影响:

    • 如果服务端发送给客户端的 SYN-ACK 报文丢失,会导致客户端无法收到确认,进而无法完成第三次握手。

  2. 客户端的行为:

    • 超时重传机制:如果未收到服务端的 SYN-ACK 确认报文(第二次握手),会尝试重传 SYN 报文。

    • 断开连接:如果超过了 tcp_syn_retries 设定的次数仍未收到,将放弃建立连接的尝试。

1.3.2.9 第三次握手丢失了,会发生什么?
  1. 第三次握手丢失的影响:

    • 如果第三次握手中的 ACK 报文丢失,服务端将无法确认客户端已收到 SYN-ACK 报文,从而无法完成连接建立。

  2. 客户端的行为:

    • 超时重传机制:如果未收到服务端的确认(第三次握手),会尝试重传服务端的 SYN-ACK 报文。

    • 断开连接:如果超过了 tcp_synack_retries 设定的次数仍未收到,将放弃建立连接的尝试。

  3. 服务端的行为:

    • 超时重传机制:如果未收到客户端的 ACK 确认报文,会尝试重传 SYN-ACK 报文。

    • 断开连接:如果超过了 tcp_synack_retries 设定的次数仍未收到,将放弃建立连接的尝试。

1.3.2.10 什么是 SYN 攻击?如何避免 SYN 攻击?
  1. 什么是 SYN 攻击?

    • SYN 攻击是一种利用 TCP 协议的漏洞来消耗服务端资源的网络攻击方式。

    • 攻击者发送大量伪造的 SYN 报文给服务端,使得服务端在收到 SYN 报文后进入半连接状态(SYN_RCVD),

      但不会响应进一步的 ACK 确认,导致半连接队列(SYN 队列)被填满,使得正常用户无法建立连接。

  2. 如何避免 SYN 攻击?

    • 方式一:调大 netdev_max_backlog 控制网卡接收数据包的队列长度

    • 方式二:调大 net.ipv4.tcp_max_syn_backlog 控制 TCP 半连接队列的长度

    • 方式三:开启 tcp_syncookies 机制,它可以在不使用半连接队列的情况下建立连接

    • 方式四:调小 tcp_synack_retries 控制 SYN+ACK 重传次数

1.3.2.11 TCP 重传机制与拥塞控制?

1. TCP 重传机制

想象一下,你在网上买东西,卖家把商品寄给你。但是,有时候包裹可能会在半路上丢了,或者被弄坏了。

  • 超时重传:卖家寄出包裹后,会等一段时间,如果你没确认收到,他就会觉得包裹可能丢了,然后就会再寄一个给你。

  • 快速重传:如果你收到了好几个包裹,但是中间的一个没收到,你会告诉卖家。卖家马上再寄一个给你,不用等太久。

  • 选择性确认(SACK):你告诉卖家哪些包裹收到了,哪些没收到,这样只需要寄没收到的,不用把已经收到的再寄一遍。

  • D-SACK:你告诉卖家收到了一个包裹两次,这样卖家就知道可能是路上出了问题,或者他自己不小心多发了一次。

2. TCP 拥塞控制

想象一下,卖家要同时给很多人寄包裹,如果一下子寄太多,路上可能会堵车(网络拥塞),这样大家都收不到包裹了。

  1. 慢启动:卖家一开始不敢寄太多包裹,先寄几个试试,看看路上会不会堵车。

  2. 拥塞避免:如果路上看起来还行,卖家就会慢慢增加包裹的数量,但是不会像一开始那样一下子增加很多。

  3. 拥塞发生:如果卖家发现路上开始堵车了(比如包裹丢了,或者你很久没收到包裹),他就会减少包裹的数量。

  4. 拥塞窗口回退:如果卖家发现路上太堵了,他就会减少包裹的数量,等路不堵了再慢慢增加。

  5. 快速重传和快速恢复:如果卖家发现你少收了一个包裹,会马上再寄一个给你,然后等路不堵了,再慢慢增加包裹的数量。

    • Tahoe和Reno算法:Tahoe 比较老,一堵车就全部重来。Reno 改进了一些,如果只丢了几个包裹,就不用全部重来。

    • NewReno算法:这个算法是Reno的升级版,如果路上只是有点小堵,卖家可以更快地恢复正常的包裹数量。

    • BBR算法:这是一种新算法,卖家会先看看路有多宽(带宽),然后根据宽度来决定寄多少包裹,这样就不会堵车了。

1.3.3 TCP 连接断开
1.3.3.1 TCP 四次挥手过程是怎样的?
  1. 第一次挥手(FIN 报文):

    • 客户端发送一个 FIN 置为 1 的报文给服务端,表示没有数据要发送了。进入 FIN_WAIT_1 状态。(还可以收)

  2. 第二次挥手(ACK 报文):

    • 服务端收到报文后,发送一个 ACK 报文作为应答。进入 CLOSE_WAIT 状态。(还可以发)

  3. 第三次挥手(FIN 报文):

    • 服务端发送一个 FIN 置为 1 的报文给客户端,表示没有数据要发送了。进入 LAST_ACK 状态。

  4. 第四次挥手(ACK 报文):

    • 客户端收到报文后,发送一个 ACK 报文作为应答。进入 TIME_WAIT 状态。

    • 该状态是为了在正常情况下确保服务端收到了客户端的确认,并且防止在网络中残留的数据包影响后续连接。

  5. 连接关闭:

    • 服务端收到报文后,进入 CLOSE 状态。客户端经过 2MSL(最长报文段寿命)时间后,也进入 CLOSE 状态。

1.3.3.2 为什么挥手需要四次?

由此可知,服务端通常需要等待数据完成发送和处理,ACK 和 FIN 一般都会分开发送,因此需要四次挥手。

1.3.3.3 第一次挥手丢失了,会发生什么?
  1. 如果客户端未收到服务端的 ACK,客户端会超时重传 FIN 报文。

  2. 重传次数由 tcp_orphan_retries 参数控制,默认为3次。

  3. 超过重传次数后,客户端放弃发送 FIN,等待一段时间(上次超时时间的两倍),然后直接关闭连接。

1.3.3.4 第二次挥手丢失了,会发生什么?
  1. 如果客户端未收到服务端的 FIN,客户端会超时重传 FIN 报文。

  2. 超过重传次数后,客户端放弃发送 FIN,等待一段时间(上次超时时间的两倍),然后直接关闭连接。

1.3.3.5 第三次挥手丢失了,会发生什么?
  1. 如果服务端的 FIN 报文丢失,客户端无法收到,服务端会超时重传 FIN 报文。

  2. 超过重传次数后,服务端放弃发送 FIN,等待一段时间(上次超时时间的两倍),然后直接关闭连接。

1.3.3.6 第四次挥手丢失了,会发生什么?
  1. 如果客户端的 ACK 报文丢失,服务端无法收到,服务端会超时重传 FIN 报文。

  2. 超过重传次数后,服务端放弃发送 FIN,等待一段时间(上次超时时间的两倍),然后直接关闭连接。

1.3.3.7 为什么 TIME_WAIT 等待的时间是 2MSL?
  1. MSL 的定义和作用:

    • MSL 是 TCP 报文在网络中能存活的最长时间,超过这个时间后,报文将被丢弃。

    • MSL 不同于 TTL,TTL 是 IP 数据报经过路由器的最大跳数,而 MSL 是时间单位,确保报文能在网络中自然消亡。

  2. TIME_WAIT 状态的设定:

    • TCP 协议在连接关闭时,主动关闭方会进入 TIME_WAIT 状态,等待一段时间才关闭连接。这个时间被设置为 2 倍的 MSL。

    • 原因在于网络中可能存在延迟的数据包,在等待 2MSL 后,可以确保所有延迟包都已被丢弃,避免对后续连接产生影响。

  3. 为何选择 2MSL 而非其他倍数?

    • 选择 2MSL 是为了充分考虑网络中的延迟和重传情况,以确保安全地结束连接。

    • 如果选择更长的时间(如 4MSL 或 8MSL),会增加连接终止后的等待时间,可能降低网络资源利用效率。

    • 在 Linux 中,TIME_WAIT 默认的时间为 2MSL,通常设置为 60 秒。

1.3.3.8 为什么需要 TIME_WAIT 状态?
  1. 原因一:防止历史连接中的数据被错误接收。

    • TCP 中的数据包通过序列号来确认:序列号(SEQ)确保数据包的顺序,而初始序列号(ISN)是由各端生成的随机数。

    • 当存在延迟数据包时,如果 TIME_WAIT 状态时间过短,会导致历史连接中的数据包被后续相同四元组的连接错误接收。

  2. 原因二:TIME_WAIT 状态的另一个作用是等待足够的时间,以确保服务端能够正确接收到请求的确认(ACK)。

1.3.3.9 TIME_WAIT 过多有什么危害?
  1. 系统资源占用

    • 文件描述符:每个 TCP 连接需要文件描述符,TIME_WAIT 状态的连接也需要,可能导致资源耗尽,影响系统正常运行。

    • 内存资源:每个 TIME_WAIT 连接会占用一定的内存,尤其是在短时间内有大量连接断开时,可能导致内存资源不足。

    • CPU 资源:处理 TIME_WAIT 状态的连接关闭过程也会消耗 CPU 资源,尤其是在高负载时会增加系统负担。

    • 线程资源:处理连接的线程池或者进程池也会因为 TIME_WAIT 连接过多而耗尽资源,影响新的连接处理能力。

  2. 端口资源限制

    • TCP 连接的唯一标识通过四元组确定。如果 TIME_WAIT 过多,可能导致资源耗尽,无法建立新连接到相同的目标 IP 和端口。

    • 服务端的端口资源不会受到直接限制,因为一般监听固定端口。但如果 TIME_WAIT 过多,仍会影响到系统资源的使用效率。

1.3.3.10 如何优化 TIME_WAIT?
  1. 打开 net.ipv4.tcp_tw_reusenet.ipv4.tcp_timestamps 选项:

    • 前者支持复用处于 TIME_WAIT 状态的 socket,作为新连接的基础。

    • 后者支持 TCP 时间戳,用于解决重复数据包问题。

  2. 调整 net.ipv4.tcp_max_tw_buckets:默认值为 18000,超过这个数目的 TIME_WAIT 连接会被系统重置。

  3. 使用 SO_LINGER 设置:SO_LINGER 是一个 socket 选项,用于控制 socket 关闭时的行为。

  4. 对于服务端,可以通过让客户端主动断开连接来减少服务器上的 TIME_WAIT 连接积累,以分散 TIME_WAIT 的负担。

1.3.3.11 服务器出现大量 TIME_WAIT 状态的原因有哪些?
  1. HTTP 没有使用长连接

    • HTTP/1.0 默认关闭长连接,需要通过设置 Connection: Keep-Alive 来启用。

    • 如果客户端或服务器端没有设置长连接,每次请求后都会关闭连接,导致服务器端产生大量 TIME_WAIT 状态的连接。

  2. HTTP 长连接超时

    • HTTP/1.1 默认开启长连接,但会设置超时时间。例如,nginx 的 keepalive_timeout 参数。

    • 当客户端在指定时间内没有发起新请求,服务器会关闭连接,导致服务器端产生 TIME_WAIT 状态连接。

  3. HTTP 长连接的请求限制

    • 服务器会限制单个长连接上能处理的请求数量,例如 nginx 的 keepalive_requests 参数。

    • 当请求数量达到设定的上限后,服务器会关闭连接,导致 TIME_WAIT 状态的连接增多。

  4. 解决方法

    • 启用 HTTP 长连接:确保客户端和服务器端都正确设置了长连接机制,避免频繁地开启和关闭连接。

    • 调整超时设置:根据实际情况调整 keepalive_timeout 和类似参数,确保合理利用长连接的同时避免不必要的连接保持。

    • 调整请求限制:对于高负载情况,适当调整 keepalive_requests 等参数,以便更有效地管理连接池。

1.3.3.12 服务器出现大量 CLOSE_WAIT 状态的原因有哪些?
  1. 原因分析:

    • 未注册到 epoll:

      • 情况:服务端未将服务端 socket 注册到 epoll 监听事件中。

      • 影响:服务端无法感知新连接到来,因此无法处理已连接的 socket。

    • 未调用 accept:

      • 情况:服务端未调用 accept 函数获取已连接的 socket。

      • 影响:导致服务端无法处理客户端断开连接的事件,从而无法调用 close 函数关闭连接。

    • 未注册已连接的 socket:

      • 情况:服务端未将已连接的 socket 注册到 epoll 监听事件中。

      • 影响:导致服务端无法感知客户端发送的 FIN 报文,从而无法调用 close 函数关闭连接。

    • 未调用 close:

      • 情况:服务端在接收到客户端关闭连接的事件时,未调用 close 函数关闭连接。

      • 影响:可能是由于代码漏处理或者死锁等问题导致。

  2. 解决方法:

    • 排查代码逻辑:仔细检查服务端代码,确保注册、接受连接、关闭连接等关键步骤的逻辑正确性和完整性。

    • 异常处理:增加对异常情况的处理,确保即使在异常情况下,也能正确关闭连接。

    • 性能测试:对服务器进行性能测试,确认在高负载或异常情况下的表现,及时发现潜在问题并解决。

1.3.3.13 如果已经建立了连接,但是客户端突然出现故障了怎么办?
  1. TCP 保活机制概述

    • TCP 保活机制旨在检测处于空闲状态但未关闭的连接是否仍然有效。它通过定期发送探测报文来验证连接状态。

    • 在 Linux 系统中,TCP 保活参数可以通过调整内核参数来配置:

      • net.ipv4.tcp_keepalive_time: 定义 TCP 连接的空闲时间阈值,超过此时间将启动保活机制。

      • net.ipv4.tcp_keepalive_intvl: 每次探测的间隔时间,即两次探测报文之间的间隔。

      • net.ipv4.tcp_keepalive_probes: 连续发送探测报文的次数,如果连续若干次未收到响应,则认为连接已失效。

    • TCP 保活的工作原理

      • 当 TCP 连接空闲时间超过设定的保活时间后,客户端将开始发送保活探测报文。

      • 如果对端正常响应,则连接状态被重置,保活时间重新计算。

      • 如果对端没有响应,则继续发送探测报文,直到达到设定的探测次数为止。

  2. 应用层心跳机制

    • 对于 HTTP 应用层协议,可以使用心跳机制来更快地发现无效连接,而不依赖于 TCP 保活机制的较长时间间隔。

    • 例如,在 Web 服务中设置 keepalive_timeout 参数,如果客户端在指定的超时时间内没有新的请求,则释放连接。

1.3.3.14 如果已经建立了连接,但是服务端的进程崩溃会发生什么?

TCP 的连接信息是由内核维护的,即使服务端的进程崩溃了,还是能与客户端完成 TCP 四次挥手的过程:

内核首先会回收该进程的所有 TCP 连接资源,然后发送第一次挥手 FIN 报文。后续过程也都是在内核完成,并不需要进程的参与。

1.3.4 Socket 编程
1.3.4.1 针对 TCP 应该如何 Socket 编程?
  1. 服务端和客户端初始化 socket,得到文件描述符

  2. 服务端调用 bind,将 socket 绑定在指定的 IP 地址和端口

  3. 服务端调用 listen,进行监听

  4. 服务端调用 accept,等待客户端连接

  5. 客户端调用 connect,向服务端的地址和端口发起连接请求

  6. 服务端 accept 返回用于传输的 socket 的文件描述符

  7. 客户端调用 write 写入数据,服务端调用 read 读取数据

  8. 客户端断开连接时调用 close,服务端调用 read 读取到 EOF

  9. 服务端处理完数据调用 close,表示连接关闭。

注意:服务端调用 accept 时,连接成功了会返回一个已完成连接的 socket,后续用来传输数据。 所以,监听的 socket 和真正用来传送数据的 socket 是两个 socket。一个叫作监听 socket,一个叫作已完成连接 socket。 成功连接建立之后,双方开始通过 read 和 write 函数来读写数据,就像往一个文件流里面写东西一样。

基于 TCP 协议的客户端和服务端工作

1.3.4.2 listen 时候参数 backlog 的意义?
  1. listen 函数参数

    • socketfd:是待设置为被动套接字的文件描述符。

    • backlog:指定了能够排队等待处理的连接数量。这个参数的具体意义在 Linux 内核中历史上有所变化。

      • Linux 内核 2.2 以前:指定的是半连接队列(SYN 队列)的大小,即未完成三次握手的连接请求队列长度。

      • Linux 内核 2.2 以后:指定的是全连接队列(Accept 队列)的大小,即已完成三次握手的连接队列长度。

  2. 队列长度限制

    • 实际的队列长度受限于系统参数 somaxconn 的设置,表示系统中允许的最大的连接数,即 Accept 队列的上限。

    • 因此,实际的 Accept 队列长度为 min(backlog, somaxconn),即 backlogsomaxconn 中较小的那个值。

1.3.4.3 accept 发生在三次握手的哪一步?
1.3.4.4 客户端调用 close 后,连接断开的流程是什么?
  1. 客户端发送 FIN 报文:客户端调用 close,发送一个 FIN 报文,随后进入 FIN_WAIT_1 状态。

  2. 服务端接收 FIN 报文:服务端收到报文后,TCP 协议栈在接收缓冲区插入一个文件结束符 EOF,随后进入 CLOSE_WAIT 状态。

  3. 服务端发送 FIN 报文:服务端处理完接收到的数据后也调用 close,发送一个 FIN 报文,随后进入 LAST_ACK 状态。

  4. 客户端发送 ACK 报文:客户端收到报文后,发送一个 ACK 确认报文,随后进入 TIME_WAIT 状态。

  5. 服务端接收 ACK 报文:服务端收到报文后,随后进入 CLOSE 状态。客户端等待 2MSL 后,也进入 CLOSE 状态。

1.3.4.5 没有 accept,能建立 TCP 连接吗?

accpet 系统调用并不参与 TCP 三次握手过程,它只是负责从 TCP 全连接队列取出一个已经建立连接的 socket。

用户层通过 accpet 系统调用拿到了已经建立连接的 socket,就可以对该 socket 进行读写操作了。

1.3.4.6 没有 listen,能建立 TCP 连接吗?

客户端可以自己连自己(TCP 自连接),也可以两个客户端同时向对方发出请求建立连接(TCP 同时打开)。

这两个情况都有个共同点,就是没有服务端参与,也就是没有 listen 也能建立 TCP 连接。

1.4 网络 IP

1.4.1 IP 基本认识
  1. IP 层:

    • TCP/IP 参考模型分为四层,IP 处于第三层,即网络层。

    • 实现主机与主机之间的通信,也称为点对点(end to end)通信。

    • IP(Internet Protocol)负责在网络中传输数据包,通过 IP 地址唯一标识设备。

  2. IP 层与数据链路层的关系:

    • 数据链路层负责直接连接的设备之间的通信,使用 MAC 地址标识设备。

    • IP 在不同网络之间进行数据传输,解决网络之间的通信问题。

  3. IP 和 MAC:

    • IP 是在不同网络之间传输数据的地址,类似旅行中的目的地地址。

    • MAC 是直连设备之间通信的地址,类似旅行中的交通工具之间的票证。

  4. 数据包传输中的不变性:

    • 在网络中,源 IP 和目标 IP 地址在传输过程中不会改变(无 NAT 的情况下)。

    • 源 MAC 和目标 MAC 地址在每个数据链路中可能会变化,类似旅行中的不同交通工具之间的票证。

IP 的作用与 MAC 的作用

1.4.2 IP 地址的基础知识

IP 地址用 32 位二进制数表示,通常以点分十进制(IPv4)的形式显示,将32位二进制数每8位划分为一组,以点隔开。 IPv4 地址空间约为 43 亿个(2^32个)。这个数量看似巨大,但在实际应用中,远不足以为全球所有设备提供唯一的 IP 地址。

IP 地址根据网络和主机标识来分配。每个网络设备(如服务器、路由器)可以有多个网卡,每块网卡可以分配一个 IP 地址。 通过 NAT(网络地址转换)技术,一台公共 IP 地址可以映射到多台私有 IP 地址,从而使得一些设备共享少量的公共 IP 地址。

1.4.2.1 IP 地址的分类
  1. IP 地址的分类:

IP 分类判断

  1. IP 地址分类的缺点:

    • 缺乏地址层次划分,不适合复杂的网络环境。

    • A、B、C 类地址范围不合理,导致地址资源浪费或不足。

1.4.2.2 无分类地址 CIDR
  1. 无分类地址 CIDR:

    CIDR 不再使用固定的分类地址,表示形式为 a.b.c.d/x,其中 /x 表示前 x 位属于网络号,范围是 0 ~ 32。

    • 子网掩码:

      • 子网掩码用于确定网络号和主机号的分界。

      • 通过将子网掩码和 IP 地址按位与运算,可以得到网络号。

    • 用途:

      • 确定两台计算机是否在同一个网络上,以便直接发送数据包。

      • 路由器使用网络号进行寻址,决定将数据包转发到哪个网络。

  2. 子网划分:

    • 作用:将主机地址进一步细分为子网网络地址和子网主机地址,提高网络的管理和利用效率。

    • 实例:对于 C 类地址的网络地址 192.168.1.0,使用子网掩码 255.255.255.192 进行划分。

      从主机号 192(11000000)中借用 2 位作为子网号,因此有 4 个子网:

      • 子网 1: 192.168.1.0/26

      • 子网 2: 192.168.1.64/26

      • 子网 3: 192.168.1.128/26

      • 子网 4: 192.168.1.192/26

1.4.2.3 公有 IP 地址与私有 IP 地址
  1. 公有 IP 地址与私有 IP 地址:

    • 区别:

      • 公有 IP 地址:由全球互联网统一分配和管理,用于识别唯一的网络设备或服务。

      • 私有 IP 地址:在私有网络中使用,由组织内部的 IT 人员自行管理和分配,可以重复使用。

    • 例子:

      • 博客网站或企业服务器需要公有 IP 地址,以便全球访问。

      • 家庭、办公室、学校等内部网络通常使用私有 IP 地址。

  2. 公有 IP 地址的管理:

    • 管理机构:

      • 公有 IP 地址由 ICANN(互联网名称与数字地址分配机构)负责统一分配和管理。

      • IANA 是 ICANN 的一个部门,负责全球 IP 地址的分配。

    • 地区分配:

      • 地球被分割成几个区域,每个区域由不同的 RIR(区域互联网注册管理机构)管理:

      • ARIN 北美地区、LACNIC 拉丁美洲、RIPE NCC 欧洲和中东、AfriNIC 非洲地区、APNIC 亚太地区。

      • 在中国,IP 地址由 CNNIC(中国互联网络信息中心)管理,是中国国内唯一指定的全球 IP 地址管理机构。

1.4.2.4 IP 地址与路由控制
  1. IP 地址与路由控制:

    • 路由控制:

      • IP 地址的网络地址部分用于进行路由控制。

      • 路由控制表中记录了网络地址与下一跳路由器的映射关系。

      • 如果有多条相同网络地址的记录,选择最长匹配的记录。

    • 控制示例:主机 A 源地址为 10.1.1.30,主机 B 目标地址为 10.1.2.10

      • 主机 A 在表中没有匹配到记录,将包转发至路由器 1 的 IP 地址 10.1.1.1

      • 路由器 1 在表中匹配到了记录,将包转发至路由器 2 的 IP 地址 10.1.0.2

      • 路由器 2 在表中匹配到了记录,将包从接口 10.1.2.1 转发出去,通过交换机传送到主机 B。

  2. 环回地址

    • 环回地址:使用 127.0.0.1 或主机名 localhost 时,数据包不会流向网络,而是直接返回给发送者。

    • 用途:用于测试网络服务和应用程序的正确性,确保它们能正常工作。

1.4.2.5 IP 分片与重组
  1. IP 分片与重组:

  1. TCP 和 UDP 分片:

    • TCP 分片:TCP 协议引入了 MSS(最大报文段长度),控制 TCP 层的分片大小,而不依赖于 IP 层的分片。

    • UDP 分片:对于 UDP,由于其无连接的特性,发送大于 MTU 的数据报可能导致分片。

1.4.2.6 IPv6 基本认识
  1. IPv4 和 IPv6 概述:

  1. IPv6 的亮点:

  1. IPv6 地址:

地址长度为 128 位,以每 16 位为一组,使用冒号 : 分隔,连续的 0 可以缩写为 ::

1.4.2.7 IPv4 首部与 IPv6 首部
  1. IPv4 首部:

    • 长度:IPv4 首部长度可变,通常为 20 字节(无选项)或更多(有选项)。

    • 字段:版本、首部长度、服务类型、总长度、标识、标志、片偏移、生存时间、协议、首部校验和、源地址和目标地址等。

  2. IPv6 首部:

    • 长度:IPv4 首部长度固定,为 40 字节。

    • 字段:版本、流量类别、流标签、有效载荷长度、下一个首部、跳数限制(替代生存时间)、源地址和目标地址。

  3. IPv6 首部改进:

    • 取消了 选项 字段,因为首部固定长度为 40 字节。

    • 取消了 首部校验和 字段,因为在数据链路层和传输层也有校验。

    • 取消了 分片和重组 相关字段,因为只允许在源主机和目标主机之间完成。

1.4.3 IP 协议相关技术
1.4.3.1 DNS
  1. DNS(Domain Name System):

    DNS 是一种将域名转换为 IP 地址的服务。我们通常使用域名而不是 IP 地址来访问网站,因为更易于人类记忆。

  2. 域名的层级关系:

DNS 中的域名都是用句点来分隔的,比如 www.server.com。句点代表了不同层次之间的界限,越靠右的位置表示其层级越高。

实际上域名最后还有一个句点,比如 www.server.com.,它代表根域名。域名的层级关系类似一个树状结构。

  1. 域名解析的流程:

    客户端只要通过本地 DNS 找到根域 DNS,就可以顺藤摸瓜找到下层的某台目标 DNS 服务器:

    • 客户端发出一个 DNS 请求,询问 www.server.com 的 IP 地址,并发给本地 DNS 服务器。

当然,并不是每次解析域名都要经过那么多的步骤:

1.4.3.2 ARP
  1. ARP(Address Resolution Protocol):

    • ARP 用于解析目标 IP 地址对应的 MAC 地址,以便 IP 数据包通过数据链路层发送到目标主机。

      RARP(逆地址解析协议)是 ARP 的相反过程,用于已知 MAC 地址但未知 IP 地址的场景。

    • ARP 使用两种类型的数据包来确定目标的 MAC 地址:ARP 请求和 ARP 响应。

  2. ARP 请求的过程:

    • 主机向局域网内所有设备发送 ARP 请求包,包含了想要知道 MAC 地址的目标主机的 IP 地址。

    • 如果主机发现自己的 IP 地址与请求包中的 IP 地址相符,就会将自己的 MAC 地址填入 ARP 响应包中返回。

  3. ARP 的缓存机制:

    • 操作系统会缓存第一次通过 ARP 获取的 MAC 地址,以提高后续通信的效率。

    • 缓存的 MAC 地址有一定的有效期限,超过期限后将会被清除,这样可以确保缓存中的地址信息是最新的。

1.4.3.3 DHCP
  1. DHCP(Dynamic Host Configuration Protocol):

    DHCP 是一种网络协议,用于自动分配网络设备(如电脑、手机)的 IP 地址和其他网络配置参数。

  2. DHCP 工作流程:

    • DHCP 发现:客户端通过 UDP 广播发送 DHCP Discover 报文(目标 IP:255.255.255.255,端口:67)请求。

    • DHCP 提供:DHCP 服务器收到报文后,广播发送 DHCP Offer 报文(目标 IP:0.0.0.0,端口:68)响应。

    • DHCP 请求:客户端从多个报文选一个,广播发送 DHCP Request 报文(目标 IP:255.255.255.255,端口:67)确认。

    • DHCP 应答:DHCP 服务器收到报文后,广播发送 DHCP Acknowledgment 报文(目标 IP:0.0.0.0,端口:68)应答。

  3. DHCP 中继代理:

    • 背景介绍:当 DHCP 客户端和服务器不在同一局域网时,路由器不会转发广播包。为此引入了 DHCP 中继代理。

    • 工作原理: DHCP 中继代理位于不同网络段中,充当 DHCP 广播包的中继。具体流程如下:

      • DHCP 客户端向 DHCP 中继代理广播发送请求包。

      • DHCP 中继代理收到请求包后,单播发送给 DHCP 服务器。

      • DHCP 服务器收到请求包后,单播响应给 DHCP 中继代理。

      • DHCP 中继代理收到应答包后,广播发送给 DHCP 客户端。

1.4.3.4 NAT
  1. NAT(Network Address Translation):

    • NAT(网络地址转换)是一种解决 IPv4 地址耗尽问题的技术。

    • 它允许将私有 IP 地址(内部网络)映射到公有 IP 地址(外部网络),使多个内部设备共享一个公网 IP 地址。

  2. NAT 的工作原理:

    • 普通的 NAT 转换只涉及 IP 地址的转换,称为基本 NAT。

    • 多数应用使用 TCP 或 UDP 协议,NAT 扩展为 NAPT,能够同时依据 IP 地址和端口号进行转换。

  3. NAT 的缺点:

    • 缺点:外部无法直接建立连接到内部服务器;转换表管理带来的性能开销;NAT 设备重启会导致 TCP 连接重置。

    • 解决:NAT 穿透技术:允许应用程序自动发现并管理 NAT 后的公网 IP 和端口映射,不再依赖于转换表管理。

1.4.3.5 ICMP

Ping 的实现原理?

Ping(Packet Internet Groper)是一种常用的网络命令,在网络故障排查和网络性能测试等方面具有重要作用。

其原理基于 Internet 控制消息协议 (ICMP),通过发送探测包并接收回复报文,测试互联网IP网络之间是否连通与可达情况。

  1. 向目标主机发送 ICMP 请求报文:当在本地主机上执行 ping 命令时,系统将发送一个 ICMP 报文给目标主机,

  2. 目标主机响应 ICMP 回应报文:如果目标主机可以对该报文进行响应,则它会返回一个 ICMP “响应”报文。

  3. 计算网络延迟时间:客户端接收到响应报文后,计算往返时间 RTT(Round-Trip Time),并且统计节点的丢包率。

  1. ICMP(Internet Control Message Protocol):

    • 主要用于在 IP 网络中传递控制消息和错误报告。通过发送控制消息来应对复杂的网络传输环境中的问题。

    • 主要功能:

      • 确认 IP 包的送达: ICMP 可以确认 IP 数据包是否成功送达目标地址。

      • 报告 IP 包被废弃的原因: 当 IP 数据包不能被成功传递时,负责向发送者报告失败原因。

      • 改善网络设置: 提供信息以改善网络配置和优化路由选择。

  2. ICMP 类型:

    • 查询消息: 用于网络诊断的查询消息,例如 ICMP Echo 请求(Ping)。

    • 差错消息: 用于通知发送者出错原因的 ICMP 差错消息,例如目标不可达消息:

      • 当路由器在传输过程中发现无法到达目标主机时,会向发送者发送 ICMP 目标不可达消息。

      • 这种消息通过 IP 地址发送,经过正常的路由路径返回,发送者可以解析 ICMP 报文来了解原因。

1.4.3.6 IGMP
  1. IGMP(Internet Group Management Protocol):

    • IGMP 是因特网组管理协议,用于管理组播组成员关系,工作在主机(组播成员)和最后一跳路由器之间。

    • 组播地址是 D 类地址,只有同一组的主机才能接收组播数据包。要管理和确认哪些主机属于同一组,就需要 IGMP 协议。

    • IGMP 报文通过 IP 封装,协议号为 2,TTL 字段通常为 1,适用于主机与连接的路由器之间的通信。

  2. IGMPv2:

    IGMP 分为 IGMPv1、IGMPv2 和 IGMPv3 三个版本,各版本在成员管理和消息处理上有所不同。

    • IGMPv2 常规查询与响应机制:

      • 路由器周期性发送 224.0.0.1 的 IGMP 查询报文到本地网段,询问是否有主机要加入组播组。

      • 主机收到查询后启动报告延迟计时器,随机时间内如果没有收到其他主机的成员报告,则发送自己的成员关系报告。

    • IGMPv2 离开组播组工作机制:

      • 主机发送离组报文到 224.0.0.2,路由器接收后发送特定组查询,确认是否还有其他成员:

      • 如果路由器认定该网段还有组播组成员,则继续向该网段转发组播数据包。

      • 如果路由器认定该网段没有组播组成员,则停止向该网段转发组播数据包。

1.5 OS 篇

1.5.1 线程、进程的区别?通信方式?

1. 基本概念

  • 进程:具有一定独立功能的程序,是系统进行资源分配和调度的一个独立单位,可以独立运行。

  • 线程:进程的一个实体,CPU调度和分派的基本单位,不拥有系统资源,但可以访问进程资源。

2. 线程、进程的区别与联系

  • 调度:线程是CPU调度的基本单位,进程是资源分配的基本单位。

  • 并发性:进程间可以并发执行,同一进程的多个线程也可以并发执行。

  • 资源拥有:进程拥有资源,线程不拥有资源,但可以访问进程资源。

  • 联系:一个线程只能属于一个进程,一个进程可以有多个线程。同一进程的所有线程共享进程资源。

3. 多线程间通信方式

  1. 共享变量:通过共享对象的变量进行通信。

  2. volatile关键字:确保变量的可见性。

  3. wait()/notify():通过对象的wait()notify()方法实现线程间的协调。

  4. synchronized关键字:确保对共享资源的同步访问。

4. 进程间通信方式

  1. 管道(Pipe):用于父子进程或兄弟进程间的数据传输。

  2. 消息队列:进程间通过消息队列交换消息。

  3. 共享内存:进程通过映射相同的内存区域来共享数据。

  4. 套接字(Socket):适用于不同主机间的进程通信。

1.5.2 物理地址和虚拟地址?
  1. 虚拟地址:程序使用的地址空间,由CPU在执行时产生。

    • 特点:每个进程有自己的虚拟地址空间。通常比物理地址空间大。从0开始,通常是连续的。

    • 用途:提高内存利用率和安全性,使进程认为自己独占全部内存。

  2. 物理地址:内存中实际存储数据的地址,由硬件使用。

    • 特点:每个存储单元都有自己的物理地址。直接与内存硬件交互。范围由物理内存大小决定。

    • 用途:将虚拟地址转换为实际存储数据的物理存储单元。

  3. 两者的关系:

    • 地址映射:操作系统通过内存管理单元将虚拟地址转换为物理地址。

    • 内存保护:分离虚拟和物理地址,实现内存保护和隔离。

    • 地址空间隔离:每个进程有自己的虚拟地址空间,共享物理内存而不互相干扰。

1.5.3 Linux 的命名空间 namespace?
  • Linux 命名空间是 Linux 内核的一个特性,它允许多个进程组共享某些资源,同时在这些资源上拥有独立的视图。

    这种隔离机制使得容器化技术(如 Docker)得以实现,因为它们可以利用命名空间来提供轻量级的隔离环境。

  • 想象一下,你的电脑是一个大房子,里面有很多房间。你在不同的房间做不同的事情,每个房间都有自己的规矩和装饰。

    Linux 的命名空间就像是这些房间的规矩,它们让电脑里的程序能在自己的世界里运行,而不会影响到其他程序。

1.5.4 Linux 的常见命令?
  1. 文件目录:cd、ls、pwd、cp、mv、rm、mkdir、rmdir

  2. 文件压缩:tar、gzip、gunzip、zip、unzip

  3. 文件编辑:cat、head、tail、vi、vim、nano

    • 查看文件最后十行内容的命令:tail -n 10 filename

  4. 内存相关:top、free、ps、df、du

  5. 网络相关:ping、netstat、ifconfig、curl、wget

  6. 权限相关:chmod、chown

    • 权限分为三种:读(r)、写(w)和执行(x)。

    • 这些权限可以分配给文件的所有者(u)、所属组(g)以及其他用户(o)。

 

2 - MySQL 篇

2.1 MySQL 基础

2.1.1 MySQL 执行流程是怎样的?

MySQL 执行一条 SQL 查询语句涉及多个关键步骤,从连接建立到结果返回,分为两层:Server 层和存储引擎层。

  1. Server 层:

    包含了大多数核心功能模块,负责连接管理、SQL 解析和执行。

    • 连接器:接受客户端的连接请求,建立连接。负责权限验证、连接状态管理等。

    • 查询缓存:缓存执行过的 SELECT 和结果。MySQL 8.0 中已不推荐使用,因为性能优势不大且存在并发问题。

    • 解析器:将 SQL 查询语句解析成内部数据结构,确定查询的语法正确性。

    • 预处理器:对解析后的查询进行初步处理,如展开视图、处理存储过程、触发器等。

    • 优化器:根据查询的逻辑,生成多个执行计划。选择最优的执行计划,以提高查询效率。

    • 执行器:执行优化器选择的执行计划。获取存储引擎返回的数据,并进行过滤、排序等操作。返回查询结果给客户端。

    • 内置函数和跨存储引擎功能:内置函数如日期、数学、加密等,跨存储引擎功能如存储过程、触发器、视图等。

  2. 存储引擎层:

负责实际的数据存储和提取,支持不同的存储引擎(如 InnoDB、MyISAM 等)。

  1. SQL 查询语句执行流程:

    1. 连接建立:客户端发起连接请求,连接器接受并进行权限验证。

    2. SQL 解析:解析器对查询语句进行语法和语义分析,生成内部执行树。

    3. 预处理和优化:预处理器处理存储过程、视图等对象,优化器生成多个执行计划。

    4. 执行查询:执行器根据优化器选择的执行计划,调用存储引擎层执行查询。

    5. 数据提取和处理:存储引擎根据指令从磁盘中读取数据,进行数据过滤、排序等操作。

    6. 返回结果:执行器将处理后的数据结果返回给客户端。

查询语句执行流程

2.1.2 第一步:连接器
  1. 连接 MySQL 服务

    在 Linux 操作系统中,连接到 MySQL 服务通常通过以下命令进行:bashmysql -h$ip -u$user -p

  2. 连接器的工作流程:

    1. TCP 三次握手:建立与 MySQL 服务的连接。

    2. 用户名和密码验证:连接器验证用户提供的用户名和密码。

    3. 权限获取:验证通过后,连接器获取用户的权限,以便后续操作使用。

  3. 管理 MySQL 连接:

    • 查看当前连接数和连接状态:show processlist;

    • 管理空闲连接:show variables like 'wait_timeout';

    • 手动断开连接:kill connection +id;

    • 连接数限制和长短连接:show variables like 'max_connections';

  4. 长连接与短连接:

    • 短连接:每次执行完 SQL 后断开连接,下次执行 SQL 时重新建立连接。

    • 长连接:建立连接后,可以执行多次 SQL 操作后再断开连接。

    推荐使用长连接以减少连接建立和断开的开销,但可能引起内存占用问题。解决方法:

    • 定期断开长连接:定时关闭长连接,释放占用的内存资源。

    • 主动重置连接:使用 mysql_reset_connection() 接口函数,在不需要重连的情况下重置连接,释放内存资源。

2.1.3 第二步:查询缓存
  1. 查询语句的处理流程

    客户端向 MySQL 服务发送 SQL 语句,MySQL 服务接收到 SQL 语句后,会解析其第一个字段,确定语句类型。

    • 如果 SQL 是查询语句,MySQL 会首先检查查询缓存(Query Cache)中是否有对应的缓存数据。

    • 查询缓存以 key-value 形式存储在内存中,key 为 SQL 查询语句,value 为查询结果。

  2. 查询缓存的局限性

    • 对于频繁更新的表,查询缓存命中率低,因为表的更新会导致相关缓存失效。

    • 如果刚缓存了一个大数据结果,未来更新操作会导致整个表的缓存清空,效率下降。

    • MySQL 8.0 移除了查询缓存,提高了对更新频繁表格的查询效率,避免了不必要的缓存清空和维护成本。

2.1.4 第三步:解析 SQL
  1. 解析器工作流程:

    • 词法分析:MySQL 解析器首先进行词法分析,识别 SQL 语句中的关键字和标识符。

      例如,对于 select username from userinfo 会生成 4 个Token,其中包含 2 个关键字:selectfrom

    • 语法分析:基于词法分析的结果,解析器进行语法分析。

      如果语句符合 MySQL 语法,解析器会构建出语法树,包含语句的类型、表名、字段名、WHERE 条件等重要信息。

  2. 解析器的作用:

    • 语法验证:解析器负责检查 SQL 语句是否符合 MySQL 支持的语法规则。

    • 语法树构建:构建 SQL 语法树有助于后续模块理解和处理 SQL 语句。

    • 错误处理:如果语句存在语法错误,解析器将报错,并指示出错误的位置和原因。

2.1.5 第四步:执行 SQL
  1. 阶段一:预处理阶段

    • 检查表和字段的存在性:MySQL 会验证 SQL 查询语句中涉及的表和字段是否存在。

    • 扩展 * 符号:如果查询中包含 SELECT *,MySQL 会将 *扩展为表上的所有列。

  2. 阶段二:优化阶段

    • MySQL 会根据查询的结构和数据库的统计信息,生成一个最优化的执行计划。

    • 这个执行计划决定了如何最有效地获取和处理数据,以提高查询的性能。

  3. 阶段三:执行阶段

    • MySQL 按照优化阶段生成的执行计划执行查询。

    • 这包括读取数据、应用过滤条件、排序、聚合等操作,以满足查询的需求。

2.1.6 MySQL 如何优化?
  1. 使用索引:合理使用索引是提升查询性能的关键。可以使用 EXPLAIN 关键字分析执行计划,查看是否有效利用了索引。

  2. 查询语句优化:优化查询语句,比如避免使用 SELECT *,尽量指定需要的列;减少子查询和复杂的 JOIN 操作。

  3. 使用缓存技术:利用缓存技术,如 Redis 或 Memcached,来缓存热点数据,减少数据库的访问压力。

  4. 避免使用函数和操作符:在 WHERE 子句中避免对列使用函数或操作符,这样可以更好地利用索引。

2.2 MySQL 索引

2.2.1 存储引擎
2.2.1.1 MySQL 常见引擎?
  1. InnoDB:MySQL 的默认存储引擎。支持事务处理、行级锁定和外键约束。支持聚簇索引。

  2. MyISAM:早期 MySQL 的默认存储引擎。不支持事务处理,支持表级锁定。使用非聚簇索引。

  3. MEMORY:将所有数据存储在内存中,访问速度快。适合临时表和快速读取的场景。数据在数据库重启时会丢失。

img

2.2.1.2 InnoDB 和 MyISAM 区别?为什么选前者?

1. MySQL 常见引擎

  1. InnoDB:这是 MySQL 默认的存储引擎,支持事务处理、外键约束、行级锁定和崩溃恢复能力。

  2. MyISAM:这是 MySQL 早期的默认存储引擎,它提供了高速的读取性能和全文搜索能力,但不支持事务和行级锁定。

  3. Memory:这个引擎将所有数据存储在内存中,提供极快的访问速度,但数据在数据库重启后会丢失。

2. InnoDB 和 MyISAM 的区别

  1. 事务支持:InnoDB 支持事务,这意味着它支持事务的四个基本特性:原子性、一致性、隔离性、持久性(ACID)。

  2. 锁定机制:InnoDB 支持行级锁定,这可以减少锁定带来的冲突,提高并发性能。

  3. 外键约束:InnoDB 支持外键,有助于保持数据的完整性。

3. 为什么选择 InnoDB

  1. 事务支持:对于需要事务支持的应用,InnoDB 是更好的选择,因为它可以保证数据的一致性和完整性。

  2. 锁定机制:InnoDB 的行级锁定提供了更好的并发性能,尤其是在写操作频繁的场景下。

  3. 外键约束:如果应用需要维护数据的引用完整性,InnoDB 的外键支持是必需的。

2.2.1.3 InnoDB 底层结构?
  • 内存结构:缓冲池(Buffer Pool)、更改缓冲(Change Buffer)、日志缓冲(Log Buffer)、自适应哈希索引。

  • 磁盘结构:表空间、数据字典、双写缓冲、重做日志(Redo Log)、撤销日志(Undo Logs)。

    1. 表空间(Tablespaces):用于存储数据文件,包括系统表空间、独立表空间。

    2. 段(Segments):表空间被划分为多个段,用于存储数据和索引。

    3. 区(Extent):段由一组区组成,通常 1MB 大小,用于磁盘读写操作。

    4. 页(Page):区由页组成,通常是 16KB 大小,是存储数据的最小单位。

    5. 行(Row):数据行存储在页中,使用 B+ 树索引数据结构。× 3

      • 相比 二叉树:B+Tree 在数据量大时查询效率更高,因为其高度控制在较小的范围内。

      • 相比 B 树:B+Tree 叶子节点只存放数据,且有序链表,适合等值查询、排序操作、范围查询。

      • 相比 Hash:Hash 索引不支持范围查询和排序操作,而 B+Tree 能够处理更多的查询场景。

2.2.1.4 MySQL 数据在页中是如何排列的?为什么要这么排列?

在页内,数据按照一定的顺序排列,通常是按照主键的顺序。这样可以保证数据的连续性,减少页分裂的发生。

每个页都有一个页目录,指向页内数据的位置,这样可以快速定位到页内特定的数据行,而不需要扫描整个页。

使用 B+树索引结构和有序排列数据的原因包括:

  1. 提高性能:有序排列和索引结构可以显著提高数据检索和更新的性能。

  2. 减少磁盘I/O:通过减少树的高度和优化页内数据的排列,可以减少访问数据时所需的磁盘I/O操作。

  3. 支持事务:对于支持事务的存储引擎,这种排列方式也有助于保持事务的ACID属性,尤其是在并发环境下。

2.2.2 索引的分类
2.2.2.1 按数据结构分类

1. 按数据结构分类

2. B+Tree 索引的存储

2. 主键查询过程

主键索引 B+Tree

2. 二级索引查询过程

回表

2.2.2.2 按物理存储分类

1. 索引分类

  • 聚簇索引:聚簇索引决定了表中数据的物理顺序。也就是说,聚簇索引实际上是按照索引的顺序来存储数据的。

    • 一个表只能有一个聚簇索引。叶子节点直接包含 数据记录

    • 由于数据是按索引顺序存储的,因此访问速度快,范围查询效率高。

    • 由于更新记录时可能涉及数据移动,因此插入、删除和更新操作可能比较慢。

  • 非聚簇索引:非聚簇索引是索引顺序与数据物理存储顺序无关的索引。非聚簇索引包含对数据行的引用。

    • 一个表可以有多个非聚簇索引。叶子节点包含 数据行的地址或指针,而不是数据本身。

    • 由于通过索引查找数据地址后再访问数据,因此访问速度可能比聚簇索引慢。

    • 由于索引和数据是分开存储的,所以更新数据时不需要移动索引。

2. 查询过程

2.2.2.3 按字段特性分类

1. 主键索引:建立在表的主键字段上。

2. 唯一索引:建立在表的 UNIQUE 字段上。

2. 普通索引:建立在表的普通字段上。

4. 前缀索引:建立在表的字符类字段的前几个字符上,而不是在整个字段上建立索引。

2.2.2.4 按字段个数分类

1. 联合索引

img

2. 联合索引范围查询

分析:由于 a > 1 是范围查询,联合索引只能使用到 a 字段。因为 b 字段的值在 a > 1 的范围内是无序的,无法利用索引。

分析:由于 a >= 1 不是范围查询,联合索引可以使用到 b 字段。因为 b 字段的值在 a = 1 的条件下是有序的,可以利用索引。

2. 索引下推优化(Using index condition)

2.2.3 什么时候创建索引?

1. 索引的优缺点:

2. 何时需要创建索引?

2. 何时不需要创建索引?

  1. 重复值多的字段:如性别字段,索引不会提高查询效率。

  2. 频繁更新的字段:如用户余额等,索引维护成本高,影响性能。

  3. 数据量较小的表:小表通常全表扫描效率更高。

2.2.4 有什么优化索引的方法?
  1. 使用前缀索引:CREATE INDEX idx_username_prefix ON users(username(10));

  2. 使用覆盖索引:CREATE INDEX idx_cover ON users(username, age);

  3. 主键索引自增:存储更紧凑,主键字段更小(二级索引省空间),磁盘的随机 I/O 访问更少,避免了页分裂。

  4. 索引设置非空:COUNT 会省略没有意义的值 NULL 的行,但是它又会占用物理空间。

2.2.4.1 前缀索引优化

1. 优点

  • 减小索引字段大小:使用字段的前几个字符而不是整个字段作为索引,节省存储空间。

  • 提高查询速度:减少了索引的大小,加快了索引的扫描速度,特别是在大字符串字段上。

2. 缺点

  • 无法用于排序:前缀索引只能加速前缀匹配的查询,对于需要全字段排序的查询不适用。

  • 无法用作覆盖索引:前缀索引只包含部分数据,无法覆盖完整的查询需求。

2.2.4.2 覆盖索引优化

1. 覆盖索引

  • 在查询过程中,所有字段都可以从索引的 B+Tree 结构中的叶子节点获取,而不需要额外的回表操作。

  • 优化原理:

    • 减少 I/O 操作:避免了访问主表数据行,仅通过索引即可满足查询需求。

    • 提高查询速度:因为不再需要额外的查找和读取操作,查询响应时间更短。

  • 如何实现覆盖索引优化:

    • 建立合适的联合索引:例如,对于查询商品名称和价格的需求,可以创建联合索引(商品 ID、名称、价格)。

    • 利用联合索引的顺序:索引的字段顺序应与查询的字段顺序一致,利用索引的排序特性加速查询。

2. 适用场景

  • 当需要查询的字段可以完全通过索引覆盖时,使用覆盖索引可以最大程度地提升查询效率。

  • 特别适合那些只需返回 部分字段信息 的查询,如仅查询商品的名称和价格而不需要其他信息时。

2.2.4.3 主键索引自增
  1. 主键索引:会按照主键的顺序将数据存储在磁盘上,这样相邻的数据行物理上也是相邻的,减少了磁盘的 随机 I/O 访问

  2. 插入性能:避免了页分裂操作:当使用非自增主键时,插入导致已满页面分裂,重新排列数据,影响性能并增加空间碎片化。

  3. 空间利用:数据存储 更加紧凑,避免了存储空间的浪费和额外的碎片化,有助于提高整体数据库性能和响应速度。

  4. 二级索引效率:主键字段越小,二级索引的叶子节点也就越小,减少了非主键索引的存储空间和访问成本,提升了查询效率。

2.2.4.4 索引设置非空
  1. 索引列存在 NULL 会导致优化器在做索引选择的时候更加难以优化,比如 count 会省略 索引值为 NULL 的行。

  2. NULL 值是一个没意义的值,但是它会 占用物理空间,所以会带来的存储空间的问题。

2.2.5 索引失效场景
  1. 模糊匹配:SELECT * FROM users WHERE username LIKE '%john%';

  2. 类型不匹配:SELECT * FROM users WHERE username = 123;

  3. 函数操作:SELECT * FROM products WHERE DATE(created_at) = '2024-08-01';

  4. 未满足最左匹配原则:SELECT * FROM users WHERE last_name = 'B' AND first_name = 'A';

  5. 使用 OR:SELECT * FROM products WHERE price = 100 OR name = 'unique_name';

2.3 MySQL 事务

2.3.1 事务隔离级别是怎么实现的?
2.3.1.1 事务的四个特性及原理?
  1. 原子性(Atomicity):事务是一个不可分割的操作序列,要么全部执行成功,要么全部不执行。

    • 日志记录:使用事务日志(Redo Log、Undo Log)来记录事务的操作。如果事务执行失败,可以通过回滚恢复。

  • 两阶段提交协议:在分布式系统中,使用两阶段提交协议(2PC)确保所有参与的节点要么都提交,要么都回滚。

  1. 一致性(Consistency):事务在执行前后,数据库的状态必须保持一致,任何事务的执行都不能破坏数据库的一致性。

    • 约束和触发器:通过定义数据库约束(主键、外键、唯一性约束)和触发器,确保数据在执行前后符合业务规则。

  1. 隔离性(Isolation):多个事务并发执行时,每个事务的执行不应受到其他事务的影响,事务之间是相互隔离的。

    • 锁机制:通过锁机制(行级锁、表级锁)来控制并发访问,确保在执行时,其他事务不能修改正在处理的数据。

    • 多版本并发控制(MVCC):通过维护数据的多个版本,允许读取旧版本的数据,从而避免锁竞争,提高并发性能。

  2. 持久性(Durability):一旦事务提交,其结果是永久性的,即使系统崩溃或故障,已提交的事务数据也不会丢失。

    • 日志持久化:在事务提交时,将事务的 日志记录 持久化到磁盘中,确保即使系统崩溃也能通过日志恢复数据。

2.3.1.2 并行事务会引发什么问题?
  1. 脏读:一个事务读取到了另一个事务未提交的修改数据。

  2. 不可重复读:在同一个事务中,多次读取同一个数据时,第二次读取发现数据发生了变化。

  3. 幻读:在同一个事务中,多次读取同一个范围内的数据时,第二次读取发现数据的数量发生了变化。

2.3.1.3 事务的隔离级别及原理?
  1. 读未提交:存在脏读、不可重复读、幻读问题。一个事务可以读取另一个事务未提交的数据。

    • 实现:事务读取数据时,不会等待其他事务释放锁,因此可能会脏读。

  2. 读提交:避免了脏读,存在不可重复读、幻读问题。多次读取同一数据集合时,每次读取的结果可能不同。

    • 实现:事务读取数据时,必须等待其他事务提交后才能读取。方法:共享锁读取,排他锁写入。

  3. 可重复读:避免了脏读、不可重复读,存在幻读问题。当基于范围条件查询数据时,可能会看到其他事务新插入的行。

    • 实现:事务在执行第一次读操作时,会创建快照,之后的查询都基于快照。方法:MVCC,间隙锁解决大部分幻读。

      即使其他事务提交了新的变更,也不会影响当前事务的读取。但是,新插入的行不会包含在快照中,可能出现幻读。

  4. 串行化:避免了脏读、不可重复读、幻读问题。方法:排他锁彻底解决幻读,确保事务串行执行。

    • 实现:事务按顺序依次执行,通过严格的锁机制来保证事务的隔离性。读操作时加上 S 锁,写操作时加上 X 锁。

2.3.1.4 Read View 在 MVCC 里如何工作的?

1. MVCC 实现原理

  • 隐藏字段:MVCC 是 乐观锁 的一种实现方式。InnoDB存储引擎的每行记录都会包含几个隐藏的字段,

    trx_id(最近一次修改该行的事务ID)和roll_pointer(回滚指针,指向该行的上一个版本所在的undo log)。

  • Undo Log:用于记录行数据的变更历史,以便在事务失败或需要回滚时恢复数据。通过保存行的旧版本来允许快照读。

  • Read View:事务在执行快照读时创建的当前快照,它记录并维护当前活跃事务的ID,并用于判断数据版本的可见性。

2. MVCC 工作流程

  1. 修改数据:

    • 保存旧版本:首先,系统会把当前的数据保存起来。这个保存旧数据的地方,称之为 Undo Log。

    • 创建新版本:然后,系统会在账本的空白页上创建一个新的版本,这个新版本就是修改后的数据。

  2. 读取数据:当有人想要读取数据时,系统会根据一些规则(Read View)来决定他们应该看哪个版本的数据。

    • 如果他们不需要修改数据,通常会看到最新的数据,但不是最新的那个版本。

    • 如果有人在读取数据时,另一个人刚好在修改同一数据,系统会确保他们读到的是修改之前的版本。

  3. 清理旧版本:随着时间的推移,旧版本最终会变得不再需要,因为所有事务都已经完成了,没有人再查看它们。

1. Read View

Read View 是在事务执行时创建的一个视图,用于确定数据库中的活跃事务,确定哪些数据版本对当前事务是可见的。

Read View 的四个字段:

img

2. 可见性判断

当一个事务要访问某条记录时,根据该记录的 trx_id 进行以下判断:

2.3.1.5 读提交是如何工作的?

读提交隔离级别是在每次读取数据时,都会生成一个新的 Read View。

  • 例子:假设事务 A(id 为 51)和事务 B(id 为 52)并发执行。

  1. 事务 B 第一次读取数据:此时 undo log 链第一行 trx_id = 50 < min_trx_id,所以直接取余额为 100 万。

    事务 A 修改余额为 200 万,此时 undo log 链头增加一行 trx_id = 51。

  2. 事务 B 第二次读取数据:此时 undo log 链第一行 trx_id = 51 在 m_ids 内,所以沿着链向下找到余额 100 万。

    事务 A 提交,影响了事务 B 的 Read View。

  3. 事务 B 第三次读取数据:此时 undo log 链第一行 trx_id = 51 < min_trx_id,所以直接取余额为 200 万。

2.3.1.6 可重复读是如何工作的?

可重复读隔离级别是启动事务时生成一个 Read View,然后整个事务期间都在用这个 Read View。

  • 例子:假设事务 A(id 为 51)和事务 B(id 为 52)并发执行。

  1. 事务 B 第一次读取数据:此时 undo log 链第一行 trx_id = 50 < min_trx_id,所以直接取余额为 100 万。

    事务 A 修改余额为 200 万,此时 undo log 链头增加一行 trx_id = 51。

  2. 事务 B 第二次读取数据:此时 undo log 链第一行 trx_id = 51 在 m_ids 内,所以沿着链向下找到余额 100 万。

    事务 A 提交,并不影响事务 B 的 Read View。

  3. 事务 B 第三次读取数据:此时 undo log 链第一行 trx_id = 51 在 m_ids 内,所以沿着链向下找到余额 100 万。

2.3.2 可重复读完全解决幻读了吗?
2.3.2.1 什么是幻读?

举个例子,假设一个事务在 T1 时刻和 T2 时刻分别执行了下面查询语句,途中没有执行其他任何语句:

只要 T1 和 T2 时刻执行产生的结果集是不相同的,那就发生了幻读的问题,比如:

2.3.2.2 幻读被完全解决了吗?

InnoDB 引擎的默认隔离级别虽然是可重复读,但也很大程度上避免了幻读现象,解决的方案有两种:

  • 快照读:不会锁定任何行记录,因此不会阻塞其他事务的读写,从而提高了并发性能。通过 MVCC 的 Read View 解决幻读。

    快照读适用于 SELECT 操作,但不包括带有 FOR UPDATELOCK IN SHARE MODE 的语句。

  • 当前读:会锁定数据行,因此可能会阻塞其他事务的读写操作。通过 next-key lock(记录锁 + 间隙锁)阻塞方式解决幻读。

    在读取之前需要获取对应记录的锁,例如 SELECT ... FOR UPDATESELECT ... LOCK IN SHARE MODE 语句。

1. 场景一:空记录幻读

  1. 事务 A 执行 select * from t_stu where id = 5; 查询,返回了空结果。

  2. 事务 B 执行 insert into t_stu values(5, '小美', 18); 插入,并提交事务。

  3. 事务 A 执行 update t_stu set name = '小林coding' where id = 5; 更新成功。(看不到 B 插入的记录)

  4. 事务 A 执行 select * from t_stu where id = 5; 查询,返回了 (5, '小林coding', 18)

2. 场景二:范围条件下的幻读

  1. 事务 A 执行 select * from t_stu where id > 5; 查询,返回了 3 条记录。

  2. 事务 B 执行 insert into t_stu values(10, '小美', 18); 插入成功,并提交事务。(A 没加 next-key lock)

  3. 事务 A 执行 select * from t_stu where id > 100 for update; 查询,返回了 4 条记录。

2.4 MySQL 锁

2.4.1 MySQL 有哪些锁?
  1. 全局锁:适用于需要对 整个数据库 或表进行全面操作的情况,保证数据一致性。

  2. 表级锁:适用于操作 范围较大 的场景,简单但可能降低并发性能。

  3. 行级锁:适用于 高并发 环境,提供更细粒度的锁控制,允许更高的并发性。

2.4.1.1 全局锁

主要用于执行 全库逻辑备份,防止数据或表结构的更新导致备份文件与预期不符。

缺点:业务不能更新数据,可能造成业务停顿;备份大型数据库时可能耗费较长时间。

替代方案:可以使用事务来进行备份。可重复读隔离级别保证备份期间数据的一致性,允许业务继续进行读写操作。

2.4.1.2 表级锁
  1. 表锁:适用于需要对 整张表 进行操作的情况,如执行大规模的数据修改、删除或批量更新。

    由于其锁粒度大,它会降低并发性能,因此通常不适合高并发环境。

  2. 元数据锁:适用于 DDL 操作(如 ALTER TABLE、DROP TABLE 等)时需要使用元数据锁。

    它确保其他事务不能修改表的结构,以防止在 DDL 操作时发生冲突。

  3. 意向锁:适用于表级别的 表锁和行锁 的协调,以提高锁的效率。

    在使用行级锁时,意向锁确保其他事务能够知道事务的意图,以避免不必要的锁冲突和性能瓶颈。

  4. 自增锁:适用于在插入新记录到 自增列 时,MySQL 需要确保生成的自增值是唯一且连续的。

    自增锁确保即使在高并发的环境中,也能保持自增列的值正确。

1. 表锁

控制对整个表的访问权限。分为读锁(共享锁)和写锁(排他锁)。

2. 元数据锁(MDL)

用于控制表的并发访问。自动添加到数据库操作中,保证操作的一致性和安全性。

2. 意向锁

辅助行级锁的表级锁。分为 IS(意向共享锁)和 IX(意向排他锁)。

4. 自增锁(AUTO-INC)

用于控制主键的并发插入。分为普通锁和轻量级锁。

2.4.1.3 行级锁

1. 行级锁

2. 行级锁的类型

  1. Record Lock(记录锁):仅锁定一条记录。

    • 有 S 锁和 X 锁。S 锁与 S 锁兼容,与 X 锁互斥;X 锁与 S 锁和 X 锁都互斥。

  2. Gap Lock(间隙锁):用于锁定一个范围,防止 幻读 现象。

    • 有 S 锁和 X 锁。S 锁与 X 锁和 X 锁互相兼容。

  3. Next-Key Lock(临键锁):记录锁 + 间隙锁,用于锁定一个范围并包含记录本身。

    • 可以防止其他事务在被保护记录前插入新记录或修改被保护记录。

  4. 插入意向锁:一种特殊的间隙锁,用于指示事务想要在某个位置插入新记录,但受其他事务间隙锁的阻塞。

    • 与普通的间隙锁相比,它锁住的是一个点而不是一个区间。

2.4.2 MySQL 是怎么加锁的
2.4.2.1 什么 SQL 语句会加行级锁?

MySQL 中的行级锁主要由 InnoDB 引擎支持,而 MyISAM 引擎不支持行级锁。

1. 锁定读

img

2. 锁定删/改

2.4.2.2 行级锁有哪些种类?

在 MySQL 的 InnoDB 引擎中,行级锁有三种主要类型,它们在不同的事务隔离级别下起着不同的作用。

1. Record Lock(记录锁)

2. Gap Lock(间隙锁)

2. Next-Key Lock(临键锁)

2.4.2.3 MySQL 是怎么加行级锁的?

加行级锁的对象是索引,加锁的基本单位 next-key lock 是 左开右闭 区间,而间隙锁是 左开右开 区间。

但如果在能使用记录锁或者间隙锁就能避免幻读的场景下, next-key lock 就会退化成记录锁或间隙锁。

8051341

1. 唯一索引等值查询

2. 唯一索引范围查询

2. 非唯一索引等值查询

4. 非唯一索引范围查询

针对大于的范围查询:SELECT * FROM user WHERE age >= 22 FOR UPDATE;

5. 没有加索引的查询

可能会导致全表扫描,从而对并发操作产生严重影响。

2.4.2.4 设计一个行级锁的死锁例子?

2.5 MySQL 日志

更新语句的流程会涉及到 undo log(回滚日志)、redo log(重做日志) 、binlog (归档日志)这三种日志:

  • undo log:是 Innodb 存储引擎层生成的日志,实现了事务中的原子性,主要用于事务回滚和 MVCC。

  • redo log:是 Innodb 存储引擎层生成的日志,实现了事务中的持久性,主要用于掉电等故障恢复;

  • binlog:是 Server 层生成的日志,主要用于数据备份和主从复制;

2.5.1 为什么需要 undo log?

1. 隐式回滚

2. undo log 的功能

2. undo log 的结构

版本链

2.5.2 为什么需要 Buffer Pool?

1. 缓存数据

MySQL 中的数据存储在磁盘上,为了提高读写性能,InnoDB 存储引擎引入了 Buffer Pool(缓冲池),将数据页缓存到内存中。

2. Buffer Pool 的原理

2. 查询数据的过程

2.5.3 为什么需要 redo log ?

Buffer Pool 提高了读写效率,但基于内存。数据如果未及时落盘,断电会导致丢失脏页数据。 因此使用 redo log 记录数据页的修改,确保即使脏页未写入磁盘,数据仍可恢复。 Write-Ahead Logging (WAL) 技术:先记录日志,合适时机再写入磁盘,确保数据持久性和性能。

1. 什么是 redo log?

redo log 是物理日志,记录数据页的修改操作。提交事务时只需将 redo log 持久化到磁盘,而非等待所有脏页写入磁盘。

img

2. redo log 什么时候刷盘?

redo log buffer 中的 redo log 在适当的时机刷新到磁盘。主要的刷盘时机包括:

  1. MySQL 正常关闭时:在正常关闭时,所有未持久化的 redo log 会被刷新到磁盘,以确保事务的持久性。

  2. redo log buffer 写入量超过内存空间一半:当写入量达到其内存空间的一半时,会触发落盘操作,将缓存的 redo log 写入到磁盘。

  3. InnoDB 后台线程每隔 1 秒:这是一种定期刷盘机制,即使没有事务提交,也会每秒将 redo log buffer 中的内容刷盘到磁盘。

  4. 事务提交时刷新:默认情况下,每次事务提交时,redo log buffer 中的 redo log 会立即持久化到磁盘。

2.5.4 为什么需要 binlog ?

1. 什么是 binlog?

binlog 是 MySQL 在完成更新操作后生成的一种日志文件。记录了数据库表结构变更和表数据修改的日志。

2. MySQL 主从架构?主从复制原理?

主从复制是一种数据库复制技术,用于同步主数据库(Master)的数据到一个或多个从数据库(Slave)。主要用于:

  1. 数据备份:从服务器作为数据备份。主服务器的变更通过二进制日志(binlog)记录,并同步到从服务器。

  2. 读写分离:主服务器处理写操作,而从服务器处理读操作,从而提高性能和可扩展性。

  3. 负载均衡:减轻主服务器的负载。Redis的复制是异步的,可能会导致主从节点之间的数据不一致。

  4. 故障转移:如果主节点发生故障,可以手动或自动将一个从节点提升为新的主节点。

增加从库数量会增加主库的资源消耗和网络带宽压力。一般推荐 1 主 2 从 1 备主 的结构,平衡了复制性能和资源消耗。

2. binlog 什么时候刷盘?

每个线程有一个专门用于缓存 binlog 的内存区域 binlog cache。

binlog cach

2.5.5 MySQL 磁盘 I/O 很高,有什么优化的方法?

优化方法:延迟刷盘时机,从而减少刷盘频率。

注意事项:延迟刷盘时机会增加系统响应时间,且主机掉电可能导致数据丢失。

2.6 MySQL 其他题目

2.6.1 常用的 SQL 查询语句?
  • 查手机号 136 开头的数据:SELECT * FROM table_name WHERE phone_number LIKE '136%';

2.6.2 如何优化join语句?left join 和right join 的区别?
  1. 使用合适的 JOIN 类型:明确指出使用 INNER JOIN,这样可以加快查找速度。

  2. 添加索引:确保 employees 表的 department_id 列和 departments 表的 department_id 列上有索引。

  3. 将小表 departments 作为驱动表(即 INNER JOIN 的左边)会更有效,因为数据库可以更快的遍历小表。

  • LEFT JOIN 返回左表的所有记录和匹配的右表记录,对于没有匹配到的记录右边列显示 NULL。

  • RIGHT JOIN 返回右表的所有记录和匹配的左表记录,对于没有匹配到的记录左边列显示 NULL。

2.6.3 数据仓库的分层?数据库的三个范式?
  1. 源系统层:提供原始数据,未经任何转换或清洗。

    • 例子:一个零售商的交易数据库记录了顾客的每一笔购买。

  2. ETL 层:确保数据的一致性和准确性,为后续分析提供干净、标准化的数据。

    • 例子:从交易数据库提取数据,清洗无效记录,转换日期格式,并将数据加载到数据仓库。

  3. 数据集成层:提供一个缓冲区,以便在数据加载到更高层次之前进行进一步的清洗和验证。

    • 例子:清洗后的数据被暂存,在加载到数据仓库前进行最终验证和转换。

  4. 数据仓库层:提供统一的数据视图,支持复杂的查询和分析。

    • 例子:整合了来自不同源的数据,如销售、客户和库存数据,以支持企业级报告和分析。

  5. 数据集市层:为特定的用户群体或应用提供优化的、易于访问的数据。

    • 例子:销售团队专用的数据集市,包含优化过的销售和客户数据。

  6. 呈现层:为用户提供直观的数据访问和展示方式,帮助他们理解数据并做出决策。

    • 例子:管理仪表板展示销售趋势和客户满意度,帮助管理层做出策略决策。

  • 第一范式 (1NF): 确保每个字段都是原子的。反例:

    StudentIDCourseIDCourseNameInstructor
    1101, 102MathMr. A
  • 第二范式 (2NF): 消除部分依赖,所有非主键字段都依赖于主键。反例:

    StudentIDCourseIDCourseNameInstructor
    1101MathMr. A
    2101MathMr. A
  • 第三范式 (3NF): 消除传递依赖,非主键字段不能依赖于其他非主键字段。反例:

    StudentIDCourseIDCourseNameInstructor
    1101MathMr. A
    2102EnglishMr. B
2.6.4 数据库中的主键可以换成自己表结构中的某个字段吗?

主键是用来唯一标识表中每条记录的字段或字段组合。理论上,可以选择任何字段作为主键,但必须满足以下条件:

  1. 唯一性:主键字段的值必须在表中是唯一的,不能有重复的值。

  2. 非空性:主键字段的值不能为 NULL。

  3. 稳定性:主键字段的值在记录的生命周期内不应该改变,因为主键用于索引和关联其他表。

  4. 性能:选择作为主键的字段应该是查询效率较高的字段,以避免影响数据库性能。

在实际应用中,通常会创建一个自增的整数字段作为主键,因为它可以保证唯一性和非空性,并且优化了索引性能。

2.6.5 Mybatis 如果我想查询多个ID,xml应该怎么写?
2.6.6 分库分表是什么?依据是什么?
  1. 分库:将数据分散到多个数据库中,以减轻单个数据库的负担。例如,按业务模块或数据量将数据分到不同的数据库中。

  2. 分表:将一个大的表拆分为多个表,以提高查询性能和并发处理能力。分为水平分表、垂直分表。

  3. 依据:数据量过大、访问压力过大,业务需求、完善扩展性。

2.6.7 查询到了B+树的叶子节点,之后的查找流程是?
  • InnoDB存储引擎最小存储单元是页,页可以放一行一行的数据(叶子节点),也可以放主键和指针(非叶子节点)。

  • 索引本身并不能直接找到具体的一条记录,只能知道在哪个页上。 当页被加载到内存后,可以直接顺序扫描该页来找到记录。

2.6.8 如何建立索引?索引建太多的缺点?影响读还是写效率?

用过 MySQL、Oracle 等数据库。CREATE INDEX idx_username ON users (username);

索引太多会占用存储空间。影响写效率,每次增、删、改都要更新索引,读效率通常会提高。

 

3 - Redis 篇

3.1 Redis 基础

3.1.1 什么是 Redis?使用场景?
  1. Redis 是一种基于内存的数据库,因此读写速度非常快,常用于 缓存消息队列分布式锁 等场景。

  2. 支持数据类型 String、Hash、 List、Set、Zset、Bitmaps、HyperLogLog(基数统计)、GEO(地理信息)、Stream(流)。

并且对数据类型的操作都是 原子性 的,因为执行命令由单线程负责的,不存在并发竞争的问题。

  1. 支持事务 、持久化、Lua 脚本、多种集群方案(主从复制、哨兵、切片机群)、发布/订阅模式,内存淘汰、过期删除等机制。

  1. 缓存

    • 数据存储:频繁访问、不经常变更、读取远多于写入的数据,复杂查询结果、会话信息等。

    • 数据读取:当应用接收到数据请求时,首先检查 Redis 缓存是否有该数据。

    • 过期策略:通过 TTL(Time to Live)设置数据的过期时间,以确保缓存数据的时效性。

  2. 消息队列

    • List:使用两个 List(发布+消费)。生产者将消息 LPUSH 到一个 List,消费者使用 RPOP 从另一个 List 读取消息。

    • Pub/Sub:生产者发布消息到一个频道。消费者订阅这个频道,一旦有新消息发布,消费者就会收到通知。

  3. 排行榜

    • 使用 Sorted Set 存储排行榜数据,可以设置成员的分数,Redis 根据分数自动排序。

    • 提供了丰富的操作命令,如 ZADD、ZRANK、ZREVRANK、ZRANGE 等。

3.1.2 Redis 和 Memcached 有什么区别?
3.1.3 为什么用 Redis 作为 MySQL 的缓存?

1. 高性能

2. 高并发

3.1.4 如何保证 Redis 和 MySQL 的一致性?

1. 缓存同步策略

  • 设置有效期:给缓存设置有效期,到期后自动删除,再次查询时更新。(要求低)

    • 优势:简单、方便

    • 缺点:时效性差,缓存过期之前可能不一致

  • 同步双写:在修改数据库的同时,直接修改缓存。(要求低)

    • 优势:时效性强,缓存与数据库强一致

    • 缺点:有代码侵入,耦合度高

  • 异步通知:修改数据库时发送事件通知,相关服务监听到后修改缓存数据。(微服务)

    • 优势:低耦合,可以同时通知多个缓存服务

    • 缺点:时效性一般,可能存在中间不一致状态

2. 延迟双删 + 消息队列

  • 通过延迟双删策略来保证一致性,并使用 MQ 异步通知数据库变更,以提高性能并减少对主业务流程的影响。

  1. 延迟双删:更新数据库时,先删除缓存,然后更新数据库,等待一小段时间后,再次删除缓存。

    这可以减少在数据库更新和缓存删除之间并发访问导致的不一致问题。

  2. 消息队列:数据库更新后,将更新操作记录发送到消息队列。

    消费者服务监听这些消息,异步地根据数据库的变更来更新或删除缓存,确保缓存最终与数据库保持一致。

3.1.5 本地缓存和 Redis 缓存的区别?
  • 本地缓存:本地应用的内存,重启后丢失数据。单机应用缓存数据。速度非常快,直接访问内存。容量受限于应用的内存。

  • Redis 缓存:服务器的内存,重启后可恢复数据。多机多应用共享缓存。速度快,通过网络但优化。容量受限于服务器配置。

3.2 Redis 数据结构

3.2.1 Redis 数据类型以及使用场景分别是什么?

1. 常见数据类型

  1. String(字符串):缓存数据、计数器、简单的消息存储。

  2. Hash(哈希):存储 对象 的字段和对应值,比如用户 ID 和用户名。

  3. List(列表):简单 消息队列(需注意全局唯一 ID 和消费组限制)。

  4. Set(集合):聚合计算(并集、交集、差集)、点赞、共同关注、抽奖等。

  5. Zset(有序集合):排序场景,如 排行榜、按电话号码或姓名排序等。

2. 更新后的数据类型

  1. BitMap(2.2 新增):二值状态统计,如用户 签到、登录状态、连续签到用户统计等。

  2. HyperLogLog(2.8 新增):基数统计,如大规模的独立访客数统计(UV)、独立 IP 数统计等。

  3. GEO(3.2 新增):地理位置 信息存储,如滴滴打车的车辆位置、附近的司机查询等。

  4. Stream(5.0 新增):高级消息队列,支持自动生成全局唯一消息 ID 和以消费组形式消费数据。

3.2.2 五种常见的 Redis 数据类型是怎么实现?

1. String 类型: 使用 简单动态字符串(SDS)。

  • SDS 记录了长度信息,不需要遍历整个字符串。时间复杂度:O(1)

2. List 类型: 使用 快速列表(quicklist)。

  • 替代了早期的双向链表和压缩列表。时间复杂度:O(1)

3. Hash 类型: 使用 紧凑列表(listpack)或 哈希表

  • 元素个数小于 512 且值小于 64 字节时使用 listpack,否则使用哈希表。时间复杂度:O(1)

3. Set 类型: 使用 整数集合哈希表

  • 元素个数小于 512 且值为整数使用整数集合,否则使用哈希表。时间复杂度:O(1)

5. ZSet 类型: 使用 紧凑列表(listpack)或 跳表

  • 元素个数小于 128 且值小于 64 字节时使用 listpack,否则使用跳表。时间复杂度:增删 O(log N),查 O(log N + M)

3.2.3 Redis 时间复杂度为 O(n) 的命令?

1. 时间复杂度为 O(n) 的命令

  • List:lindex、lset、linsert

  • Hash:hgetall、hkeys、hvals

  • Set:smembers、sunion、sunionstore、sinter、sinterstore、sdiff、sdiffstore

  • Zset:zrange、zrevrange、zrangebyscore、zrevrangebyscore、zremrangebyrank、zremrangebyscore

2. 优化策略

  • 使用扫描命令:HSCAN、SSCAN、ZSCAN,避免全表扫描。

  • 谨慎使用 O(n) 命令:在大数据集上,这些命令可能影响性能。

  • 客户端聚合操作:避免使用 SORT、SUNION 等聚合命令。

3.3 Redis 线程模型

3.3.1 Redis 是单线程吗?

Redis 单线程:主线程负责处理客户端请求的接收、解析、数据读写和响应发送等过程。

3.3.2 Redis 单线程模式是怎样的?

1. 初始化过程

  1. 创建和监听:epoll_create() 创建 epoll 对象,socket() 创建服务端 socket,bind() 绑定端口并 listen() 监听该 socket。

  2. 注册事件:epoll_ctl() 将监听 socket 加入 epoll 中,并注册连接事件处理函数。

2. 主线程事件循环

  1. 事件处理:

    • 如果有任务,通过 write 函数将客户端发送缓存区里的数据发送出去。

    • 如果发送未完成,注册写事件处理函数,等待 epoll_wait 发现可写时再处理。

  2. 等待事件:

    • 调用 epoll_wait 函数等待事件的到来。

  3. 处理事件:

    • 如果是连接事件,调用 accept 获取已连接的 socket。使用 epoll_ctl 将已连接的 socket 加入 epoll。注册读事件处理函数。

    • 如果是读事件,调用 read 获取数据。解析命令,处理命令,并将结果写入发送缓存区。将客户端对象添加到发送队列。

    • 如果是写事件,调用 write 将缓存区的数据发送。如果发送未完成,继续注册写函数,等待 epoll_wait 发现可写时再处理。

3.3.3 Redis 为什么这么快?
  1. 内存存储:Redis 能够充分利用现代 CPU 的高性能特性,处理内存中的数据操作非常迅速。

  2. 单线程模型:避免了多线程之间的竞争带来的时间和性能开销,也不会面临多线程死锁问题。

  3. I/O 多路复用机制:多路复用技术允许单个线程同时 监听多个 Socket 上的事件,包括连接请求和数据请求。

  4. 高效的数据结构:如哈希表、跳表、整数集合等,这些数据结构针对特定使用场景进行了优化,从而提高了性能。

3.3.4 Redis 6.0 之前为什么使用单线程?
  1. 性能瓶颈

    • Redis 的性能主要受制于内存大小和网络 I/O,而不是 CPU 计算能力。

    • 单线程能够有效地处理内存中的数据操作,因为大部分操作都在内存中完成,并且采用高效的数据结构。

  2. 多核 CPU 利用

    • 虽然单线程无法充分利用多核 CPU,但设计初衷是通过横向扩展来提高整体性能:

    • 即可以在保持简单的单线程模型的同时,通过增加节点数或扩展集群来满足高并发和大规模数据处理的需求。

3.3.5 Redis 6.0 之后为什么引入了多线程?
  1. 瓶颈转移

    • 随着硬件性能的提升,发现性能瓶颈出现在网络 I/O 的处理上,而不再仅限于内存和单线程处理能力。

    • 为了提高网络 I/O 的并行度和效率,Redis 6.0 版本引入了多线程机制。

  2. 保留单线程命令

    • 尽管引入了多线程处理网络 I/O,Redis 的主要命令执行仍然保持单线程模型。

    • 这意味着 Redis 在执行命令时依旧采用单线程处理,以确保数据一致性和避免复杂的并发问题。

3.4 Redis 持久化

3.4.1 Redis 如何实现数据不丢失?
  • AOF(Append Only File): 记录每次写操作,并将其追加到文件中。

    • 优点: 数据持久性高,可以通过重放命令恢复数据。

    • 缺点: 文件较大,恢复速度相对慢。

  • RDB(Redis Database Backup): 定期快照,在指定的时间间隔内将内存中的数据保存为二进制文件。

    • 优点: 数据恢复速度快,适合大规模数据。

    • 缺点: 在快照期间,数据的变更不会被记录,可能导致数据丢失。

你平时是怎么使用 RDB 和 AOF 的?

  • RDB:当对数据一致性的要求不是特别严格,允许数据丢失时,RDB 是一个较好的选择。

    • 配置:通过 redis.conf 设置。例如:SAVE 900 1,900秒内至少有1个写操作则保存快照。

  • AOF:适合对数据持久化要求高的场景,尤其是需要尽可能减少数据丢失的场合。例如,关键业务数据存储。

    • 配置:通过 redis.conf 设置。例如:appendonly yes,appendfsync everysec,每秒同步一次 AOF 文件。

3.4.2 AOF 日志是如何实现的?

Redis 在执行写操作后,将该命令以追加方式记录到 AOF(Append-Only File)日志文件 中。

在 Redis 重启时,通过重新执行 AOF 日志中记录的命令,恢复数据状态。写入策略:

策略描述优点缺点
Always每次写操作后立即同步到硬盘最高的数据持久性,最小的数据丢失风险最慢的写入速度
Everysec每秒同步一次到硬盘良好的数据持久性和性能平衡可能丢失最多一秒的数据
No由操作系统决定同步时机最高的写入性能,较低的持久性最大的数据丢失风险

当 AOF 文件增长到一定大小时会 重写,生成一个新的 AOF 文件,仅包含当前数据集的最小操作集,减少文件大小,优化性能。

3.4.3 RDB 快照是如何实现的呢?

RDB(Redis DataBase)快照是 Redis 使用的一种持久化机制,用于将当前内存中的数据以快照的形式 保存到硬盘 上的文件中。

与之相对的是 AOF(Append-Only File)持久化方式,它记录的是操作日志而不是 实际数据

通过 RDB 快照生成的文件,Redis 会定期创建新的 RDB 文件(SAVEBGSAVE),实现数据的持久化和备份。

  • 定期快照:SAVE 或 BGSAVE 根据配置触发。优点:减少性能影响。缺点:数据丢失风险。

  • 即刻快照:SAVE 立即生成,BGSAVE 后台生成。优点:数据一致性高。缺点:性能开销较大。

3.4.4 为什么会有混合持久化?

1. 混合持久化

混合持久化结合了传统的 RDB 和 AOF 的优点,旨在兼顾数据 恢复速度数据丢失 风险的平衡。

2. 优点

3.缺点

3.5 Redis 集群

3.5.1 Redis 如何实现服务高可用?
  1. 主从复制(Replication):

    最基础的高可用保障。它通过将一台 Redis 主服务器的数据同步到多个从服务器上,形成 一主多从 的架构。

    • 工作方式:主服务器可以进行读写操作,所有的写操作会被异步地复制到从服务器上,从服务器则只能执行读操作。

    • 优点:提高了读取性能,并提供了故障恢复的能力。

    • 缺点:异步复制导致主从数据之间可能存在短暂的不一致。

  2. 哨兵模式(Sentinel):

    用于监控 Redis 主从集群的健康状况,当主服务器宕机时自动完成 故障转移,选举新的主服务器。

    • 工作方式:哨兵进程周期性地检查 Redis 服务器的健康状态,并在发现故障时触发自动故障转移。

    • 优点:自动化了故障恢复,提高了系统的可用性。

    • 缺点:需要额外的资源来运行哨兵进程,并且配置和管理较为复杂。

  3. 切片集群模式(Cluster):

    当单个 Redis 服务器无法处理整个数据集时,可以将数据分布在多个节点上,通过分片来提高性能和可用性。

    • 工作方式:使用哈希槽来管理数据的分布和节点之间的映射关系,每个节点负责一部分哈希槽。

    • 优点:增加了水平扩展性,使得 Redis 能够处理更大的数据集和负载。

    • 缺点:需要在应用层实现对 Redis Cluster 的支持,同时要确保哈希槽的平衡和节点的健康。

3.5.2 集群脑裂导致数据丢失怎么办?

1. 脑裂

在 Redis 的主从架构中,当主节点与所有从节点失联但与客户端通信正常时,客户端可能继续向主节点写入数据。

哨兵选举出新的主节点,导致出现两个主节点的情况。网络恢复后旧主节点被降级,客户端之前写入的数据可能会丢失。

2. 解决方案

3.6 Redis 过期删除与内存淘汰

3.6.1 Redis 使用的过期删除策略是什么?

Redis 允许对 key 设置过期时间,为此需要有效的删除策略来处理已过期的键值对。

  1. 惰性删除策略:不主动删除过期键,而是在每次访问 key 时检查过期字典,判断是否过期并删除。

    • 优点:对 CPU 时间友好,因为仅在访问时才进行检查和删除操作。

    • 缺点:内存不友好,如果过期 key 一直未访问,仍占据内存空间。

  1. 定期删除策略:每隔一段时间,Redis 从数据库中随机选择一些 key 进行过期检查和删除。

    • 优点:控制删除操作的时长和频率,减少对 CPU 的影响。

    • 缺点:难以确定合适的删除频率,可能出现 CPU 开销过大或内存占用过高的情况。

3.6.2 Redis 持久化时,对过期键会如何处理?

1. RDB 文件

2. AOF 文件

3.6.3 Redis 主从模式中,对过期键会如何处理?
3.6.4 Redis 内存满了,会发生什么?
3.6.5 Redis 内存淘沐策略有哪些?

1. 不进行数据淘汰的策略

noeviction(3.0 后默认) :当运行内存超过最大设置内存时不淘汰任何数据,而是不再提供服务,直接返回错误。

2. 进行数据淘汰的策略

3.6.6 LRU 算法和 LFU 算法有什么区别?

1. LRU 算法 (Least Recently Used)

LRU 算法是根据数据最近的访问时间来淘汰最近最少使用的数据。

2. LFU 算法 (Least Frequently Used)

3.7 Redis 缓存设计

3.7.1 缓存雪崩、缓存击穿、缓存穿透?

1. 缓存雪崩

指大量缓存同时失效,导致请求直接打到数据库,造成数据库压力过大。解决方案:

  1. 失效时间随机化: 在设置失效时间时加入随机值,使得缓存过期时间分散开来,不会在同一时间大规模失效。

  2. 永不过期: 对于不易变化的数据,可以设置永不过期,通过后台服务更新缓存,避免缓存大面积同时失效。

2. 缓存击穿

指某个热点数据缓存失效后,大量并发请求直接绕过缓存直接访问数据库,造成数据库压力过大。解决方案:

  1. 互斥锁: 使用互斥锁来保证同一时间只有一个线程去加载数据到缓存,未获取到锁的线程可以等待。

  2. 永不过期: 对于热点数据,可以设置永不过期或在数据即将过期前异步更新缓存。

3. 缓存穿透

指恶意或者无效的请求访问不存在于缓存和数据库中的数据,造成数据库压力过大。解决方案:

  1. 设置空值或默认值: 针对查询无效数据的请求,在缓存中设置空值或默认值,避免对数据库的无效查询。

  2. 使用布隆过滤器: 使用布隆过滤器标记存在的数据,来快速判断请求的数据是否存在,避免对数据库的查询压力。

3.7.2 如何设计一个缓存策略,动态缓存热点数据?

由于数据存储受限,并不是所有数据都需要存放到缓存中,而只是将热点数据缓存起来,所以要设计一个动态缓存的策略。

总体思路:通过数据最新访问时间来做排名,并过滤掉不常访问的数据,只留下经常访问的数据。

例子:电商平台场景中,要求只缓存用户经常访问的 Top 1000 的商品。具体细节如下:

  1. 先通过缓存做一个排序队列(比如存放 1000 个商品),越是最近访问的商品排名越靠前。(zadd

  2. 然后定期过滤掉队列中排名最后的 200 个商品,再从数据库随机读取 200 个商品加入队列中。(zrange

  3. 这样每次请求会先从队列中获取商品 ID,如果命中就根据 ID 从另一个缓存中读取实际的商品信息,并返回。

3.7.3 说说常见的缓存更新策略?
  1. Cache Aside(旁路缓存)策略

    • 读策略: 如果数据在缓存中存在,则直接返回;否则从数据库读取并写入缓存。

    • 写策略: 当数据更新时,先更新数据库,再清空缓存,以保持数据一致性。

    适合读多写少的场景,如 Web 应用中的读取频繁的页面数据。不适合高并发写入的场景,因为命中率较低。

  2. Read/Write Through(读/写穿)策略

    • 读穿策略: 如果缓存中不存在数据,则由缓存从数据库中写入,并返回给应用程序。

    • 写穿策略:当数据更新时,先更新缓存,再同步数据库,以保持数据一致性。

    适合分布式缓存场景,如 Memcached 或 Redis Cluster 等,这些环境可以自动处理数据库的写入和数据的自动加载。

  3. Write Back(写回)策略

    • 操作步骤:当数据更新时,只更新缓存,标记数据为脏数据,再异步地将脏数据更新到数据库。

    适合写入频繁但一致性要求较低的场景,如某些计算机硬件设计中的缓存策略,比如 CPU 的缓存或者文件系统的缓存。

3.7.4 位图 BitMap、布隆过滤器 Bloom filter?

1. 位图(BitMap)

  • 用途:适用于数据量大且连续的场景,如判断一个整数是否在某个范围内。

  • 优点:相比传统数组,BitMap 更加节省空间。

  • 实现:可以使用 hutool 工具包中的 IntMapLongMap 来实现。

2. 布隆过滤器(Bloom filter)

  • 用途:适用于判断元素是否可能存在于一个集合中,特别是当集合元素非常多时。

  • 原理:使用多个哈希函数来减少冲突,提高判断的准确性。

  • 特点:时间和空间效率高,有一定的误判率(哈希冲突),但可以调整参数来控制。

3.8 Redis 实战

3.8.1 Redis 如何实现延迟队列?

延迟队列是指把当前要做的事情,往后推迟一段时间再做。延迟队列的常见使用场景有以下几种:

在 Redis 可以使用有序集合来实现延迟消息队列,ZSet 有一个 Score 属性可以用来存储延迟执行的时间。

zadd 往内存中生产消息,用 zrange by socre 查询符合条件的所有待处理的任务, 通过循环执行任务。

3.8.2 Redis 的大 key 如何处理?

1. 什么是 Redis 大 Key?

大 key 并不是指 key 很大,而是指 key 对应的 value 或元素数量 很大。具体定义如下:

大 key 会导致 Redis 性能降低、数据倾斜、主从同步 等问题。

2. 如何解决?

  1. 可删除:使用 UNLINK 命令可以安全地删除大 key。

  2. 不可删除:不可删除的话就将大 key 拆分 为多个小 key。

3.8.3 Redis 管道有什么用?

管道技术(Pipeline)是客户端提供的一种 批处理 技术,用于一次处理多个 Redis 命令,从而提高整个交互的性能。

img

管道本质上是客户端的功能,而非 Redis 服务器端的功能。同时要避免发送的命令过大,或管道内数据太多而导致网络阻塞。

3.8.4 Redis 事务支持回滚吗?

1. Redis 事务

Redis 提供了基于 MULTI/EXEC 命令的事务支持。事务内的多个命令可以一次性执行,保证了执行过程中不被其他客户端打断。

2. Redis 不支持事务回滚的原因

Redis 的设计初衷是简单高效,不支持事务回滚主要有以下两个原因:

  1. Redis 作者认为错误通常是编程引起的,不值得引入复杂的事务回滚功能。

  2. Redis 追求简单和高效,事务回滚等复杂功能与其设计理念不太相符。