1,进程和线程的区别,协程呢
-
进程(Process)
-
定义
- 进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。它拥有独立的内存空间,包括代码段、数据段、堆栈段等。不同进程之间的内存空间是相互隔离的,这意味着一个进程无法直接访问另一个进程的内存。
-
示例代码(在 Java 中启动一个新进程)
javaimport java.io.IOException; public class ProcessExample { public static void main(String[] args) { try { // 使用Runtime类的exec方法启动一个新的进程,这里以启动记事本程序为例(在Windows系统下) Process process = Runtime.getRuntime().exec("notepad.exe"); // 可以通过process对象来控制这个进程,例如等待进程结束 int exitValue = process.waitFor(); System.out.println("进程退出码: " + exitValue); } catch (IOException | InterruptedException e) { e.printStackTrace(); } } }
-
特点
- 独立性:每个进程都有自己独立的地址空间,这使得进程之间相互独立,一个进程的崩溃通常不会影响其他进程。
- 资源占用:进程需要占用较多的系统资源,包括内存、CPU 时间等。因为它有自己完整的运行环境。
- 切换开销:进程之间的切换开销较大,因为系统需要保存和恢复进程的各种状态信息,如内存映射、文件描述符等。
-
-
线程(Thread)
-
定义
- 线程是进程中的一个执行单元,是进程内的可调度实体。一个进程可以包含多个线程,这些线程共享进程的内存空间(包括代码段、数据段和堆),但每个线程有自己独立的栈空间,用于存储局部变量和函数调用信息。
-
示例代码(在 Java 中创建和使用线程)
javaclass MyThread extends Thread { @Override public void run() { System.out.println("这是一个线程在执行"); } } public class ThreadExample { public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); } }
-
或者使用 Runnable 接口实现线程:
javaclass MyRunnable implements Runnable { @Override public void run() { System.out.println("通过Runnable接口实现的线程在执行"); } } public class ThreadExampleWithRunnable { public static void main(String[] args) { MyRunnable runnable = new MyRunnable(); Thread thread = new Thread(runnable); thread.start(); } }
-
-
特点
- 共享资源:线程之间共享进程的资源,这使得线程间的通信和数据共享相对容易。但也需要注意线程安全问题,因为多个线程可能同时访问和修改共享的数据。
- 轻量级:线程相对于进程来说是轻量级的,创建和销毁线程的开销比进程小。因为线程不需要像进程那样分配独立的地址空间等大量资源。
- 切换开销:线程之间的切换开销较小,因为它们共享很多进程的状态信息,系统只需要保存和恢复线程的栈指针、程序计数器等少量信息。
-
-
协程(Coroutine)
-
定义
- 协程是一种用户态的轻量级线程。它不像线程那样由操作系统内核进行调度,而是由程序自身进行调度。协程可以在一个线程内实现多个执行点的切换,能够在执行过程中暂停并恢复执行。
-
示例(在 Java 中可以使用一些第三方库来模拟协程,如 Quasar)
xml<dependency> <groupId>co.paralleluniverse</groupId> <artifactId>quasar-core</artifactId> <version>0.8.0</version> </dependency>
javaimport co.paralleluniverse.fibers.Fiber; import co.paralleluniverse.fibers.SuspendExecution; import co.paralleluniverse.strands.Strand; import co.paralleluniverse.strands.channels.Channel; import co.paralleluniverse.strands.channels.Channels; public class CoroutineExample { public static void main(String[] args) throws Exception { Channel<Integer> channel = Channels.newChannel(1); Fiber<Void> fiber = new Fiber<>(() -> { try { System.out.println("协程开始"); channel.send(42); System.out.println("协程发送数据后"); } catch (SuspendExecution | InterruptedException e) { e.printStackTrace(); } return null; }); fiber.start(); System.out.println("主线程接收数据: " + channel.receive()); fiber.join(); System.out.println("主线程结束"); } }
-
特点
- 轻量级:协程比线程更轻量级,创建协程的开销非常小。
- 高效切换:协程的切换不需要陷入内核态,由程序自己控制切换,所以切换开销比线程更小,能够实现更高效的并发执行。
- 非抢占式:协程通常是协作式调度,即协程自己主动让出执行权,而不是像线程那样被操作系统强制抢占执行权。这使得协程在某些场景下能够更好地控制执行流程。
-
2,Redis 数据类型,zset底层数据结构
- Redis Zset(有序集合)概述
- Zset 是 Redis 中的一种数据类型,它是一个没有重复元素的字符串集合,每个元素都会关联一个 double 类型的分数(score)。通过分数来为集合中的成员进行从小到大的排序。可以用于实现排行榜、带权重的队列等功能。
- 底层数据结构 - 跳跃表(Skip List)和字典(Dictionary)结合
- 跳跃表(Skip List)
- 结构特点
- 跳跃表是一种有序的数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。在 Redis 的 Zset 中,跳跃表的节点包含成员(member)和分数(score)。
- 跳跃表中的节点按照分数从小到大进行排序。它的层次结构类似于多层链表,最底层的链表包含了所有的元素,而高层的链表则是通过间隔一定数量的节点来建立索引,这样可以快速地跳过一些节点,实现快速查找。
- 示例说明跳跃表查找过程
- 假设我们有一个简单的跳跃表,存储了分数为 1、3、5、7、9 的节点。最底层链表包含了所有这 5 个节点。如果我们在高层链表中有一个索引,每隔 2 个节点建立一个索引(这是为了简单说明,实际的跳跃表索引建立更复杂)。
- 当我们要查找分数为 7 的节点时,我们先从最高层的索引开始。如果当前索引节点的分数小于 7,我们就沿着索引链表向右移动。当遇到一个索引节点的分数大于 7 时,我们就下降到下一层链表。然后在这一层继续进行类似的查找,直到找到分数为 7 的节点或者确定不存在这样的节点。
- 跳跃表插入和删除节点
- 插入节点时,首先要找到合适的位置插入新节点。这个过程和查找类似,从高层开始,逐步确定插入的位置。插入节点后,还需要根据一定的概率(Redis 的跳跃表实现有自己的概率算法)来决定是否为这个新节点建立新的索引层,以保持跳跃表的性能。
- 删除节点时,需要先找到要删除的节点,然后调整相关节点的指针,将其从跳跃表中移除。同时,如果因为删除节点导致某些索引层不符合建立索引的规则(例如索引节点间隔过大或过小),还需要对跳跃表的索引层进行调整。
- 结构特点
- 字典(Dictionary)
- 结构特点
- 在 Redis 中,字典是基于哈希表实现的。它用于存储成员(member)和分数(score)之间的映射关系。字典可以快速地通过成员来查找对应的分数,或者通过分数查找对应的成员(在一定条件下)。
- 哈希表内部通过计算键(在这里是成员)的哈希值来确定存储位置。当发生哈希冲突时(不同的键计算出相同的哈希值),Redis 采用链地址法来解决,即将具有相同哈希值的键值对存储在一个链表中。
- 示例说明字典的查找过程
- 假设我们有一个字典,存储了成员 "user1"、"user2" 等,对应的分数分别为 10、20 等。当我们要查找成员 "user1" 的分数时,首先计算 "user1" 的哈希值,根据哈希值找到对应的哈希桶(可能是一个链表)。然后在这个链表中遍历,比较每个节点的键(成员)是否为 "user1",如果找到匹配的节点,就返回对应的分数。
- 字典插入和删除操作
- 插入操作首先计算键的哈希值,然后将键值对(成员和分数)插入到对应的哈希桶中。如果发生哈希冲突,就将新的键值对添加到链表的头部或尾部(根据具体实现)。
- 删除操作也是先通过计算哈希值找到对应的哈希桶,然后在链表中查找要删除的键值对,找到后将其从链表中移除。
- 结构特点
- 跳跃表(Skip List)
Redis 通过将跳跃表和字典结合使用来实现 Zset。字典用于快速查找成员对应的分数,跳跃表用于按照分数进行有序排列,这样可以在保证数据有序性的同时,高效地进行插入、删除和查找操作。
3,输入URL之后发生了什么
-
域名解析(DNS Lookup)
-
当在浏览器中输入 URL(统一资源定位符)后,浏览器首先会检查缓存中是否有对应的域名解析结果。如果缓存中没有,就会向操作系统发送域名解析请求。操作系统也会先检查自己的缓存,若没有,则向本地域名服务器(通常由互联网服务提供商提供)发送 DNS 请求。
-
本地域名服务器会在自己的缓存中查找,如果没有找到,它会向根域名服务器发送请求。根域名服务器会返回顶级域名(如.com、.org 等)服务器的地址。然后本地域名服务器会向顶级域名服务器发送请求,顶级域名服务器再返回二级域名服务器的地址,以此类推,直到找到目标域名对应的 IP 地址。
-
例如,对于 URL"https://www.example.com/page.html",需要通过 DNS 解析得到 "www.example.com" 的 IP 地址。
-
代码示例(这是一个简单的 Python 代码片段,用于进行 DNS 查询,实际浏览器的 DNS 解析过程要复杂得多):
pythonimport socket try: host_name = "www.example.com" ip_address = socket.gethostbyname(host_name) print("域名 {} 的IP地址是: {}".format(host_name, ip_address)) except socket.gaierror: print("无法解析域名")
-
-
建立 TCP 连接(TCP Connection Establishment)
-
浏览器得到目标网站的 IP 地址后,会根据 URL 中的协议(如 HTTP 或 HTTPS),使用 TCP(传输控制协议)来建立与服务器的连接。对于 HTTP 协议,默认端口是 80,对于 HTTPS 协议,默认端口是 443。
-
这个过程是通过三次握手完成的。首先,客户端(浏览器)向服务器发送一个带有 SYN(同步序列号)标志的 TCP 数据包,序列号随机生成,这个数据包表示客户端希望建立连接。然后,服务器收到这个数据包后,会发送一个带有 SYN 和 ACK(确认)标志的数据包,ACK 的值是客户端 SYN 值加 1,SYN 的值是服务器自己生成的一个随机序列号,这表示服务器同意建立连接并向客户端发送自己的同步信息。最后,客户端收到服务器的响应后,会发送一个带有 ACK 标志的数据包,ACK 的值是服务器 SYN 值加 1,这样就完成了三次握手,建立了 TCP 连接。
-
以 Java 为例,以下是一个简单的 TCP 客户端代码片段,用于建立连接(这里只是简单示意,实际的浏览器与服务器连接更复杂):
javaimport java.io.IOException; import java.net.Socket; public class TcpClient { public static void main(String[] args) { try { String serverIp = "127.0.0.1"; int serverPort = 80; Socket socket = new Socket(serverIp, serverPort); System.out.println("与服务器建立TCP连接成功"); // 连接建立后可以进行数据传输等操作 socket.close(); } catch (IOException e) { System.out.println("建立TCP连接失败"); e.printStackTrace(); } } }
-
-
发送 HTTP 请求(HTTP Request)
-
建立 TCP 连接后,浏览器会按照 HTTP 协议的格式向服务器发送请求。请求包括请求行(包含请求方法,如 GET、POST 等,请求的 URL 路径,以及 HTTP 协议版本)、请求头(包含各种信息,如用户代理、接受的内容类型、缓存控制等)和请求体(对于 POST 等请求方法,请求体包含要发送的数据)。
-
例如,一个简单的 GET 请求可能如下所示:
plaintextGET /page.html HTTP/1.1 Host: www.example.com User - Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/[version] Safari/[version] Accept: text/html,application/xhtml+xml,application/xml;q = 0.9,image/avif,image/webp,image/apng,*/*;q = 0.8,application/signed - transfer - domain;q = 0.7
-
在 Java 中,可以使用 HttpURLConnection 来发送 HTTP 请求,示例代码如下:
javaimport java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; public class HttpRequestExample { public static void main(String[] args) { try { URL url = new URL("http://www.example.com/page.html"); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); String line; while ((line = reader.readLine())!= null) { System.out.println(line); } reader.close(); connection.disconnect(); } catch (IOException e) { System.out.println("发送HTTP请求失败"); e.printStackTrace(); } } }
-
-
服务器处理请求(Server Processing)
-
服务器收到浏览器发送的 HTTP 请求后,会根据请求的 URL 路径和请求方法等信息,找到对应的资源或执行相应的操作。如果是请求一个网页文件,服务器可能会从文件系统中读取该文件;如果是一个动态请求(如请求一个由服务器端脚本生成的页面),服务器会执行相应的脚本程序,如 PHP、Python(使用 Flask 或 Django 等框架)、Java(使用 Servlet 或 Spring 等)来生成内容。
-
以 Java Servlet 为例,以下是一个简单的 Servlet 代码片段,用于处理 GET 请求:
javaimport javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; public class MyServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html"); PrintWriter out = response.getWriter(); out.println("<html><body>"); out.println("这是一个简单的Servlet响应"); out.println("</body></html>"); } }
-
-
服务器发送响应(Server Response)
-
服务器处理完请求后,会按照 HTTP 协议的格式向浏览器发送响应。响应包括响应行(包含 HTTP 协议版本、响应状态码,如 200 表示成功、404 表示未找到等)、响应头(包含内容类型、内容长度、缓存控制等信息)和响应体(包含实际的内容,如网页的 HTML 代码、图片的二进制数据等)。
-
例如,一个成功的响应可能如下所示:
plaintextHTTP/1.1 200 OK Content - Type: text/html; charset = UTF - 8 Content - Length: [length of the content] <html><body> 这是网页内容 </body></html>
-
在 Java 中,上面的 Servlet 代码中通过设置响应头和输出响应体来发送响应。
-
-
浏览器接收并渲染响应(Browser Receiving and Rendering)
- 浏览器收到服务器发送的响应后,首先会检查响应状态码。如果是 200 等表示成功的状态码,就会根据响应头中的内容类型等信息来处理响应体。如果是文本类型(如 HTML、CSS、JavaScript),浏览器会进行解析和渲染。对于 HTML 文件,浏览器会构建 DOM(文档对象模型)树,解析 CSS 样式并应用到 DOM 树上,对于 JavaScript 代码,会执行脚本,可能会对 DOM 树进行动态修改。
- 如果响应内容是图片、音频或视频等多媒体类型,浏览器会使用相应的插件或内置功能来处理和显示这些内容。
这就是输入 URL 之后大致发生的一系列复杂过程。实际情况可能会因为网络环境、服务器配置、安全协议等多种因素而更加复杂。
4,HTTP请求包含哪些内容,请求头和请求体有哪些类型
- HTTP 请求的组成部分
- 一个完整的 HTTP 请求主要由三部分组成:请求行(Request Line)、请求头(Request Headers)和请求体(Request Body)。
- 请求行(Request Line)
- 内容
- 请求行包含请求方法(Request Method)、请求的 URL(Uniform Resource Locator)以及 HTTP 协议版本。
- 例如:
GET /index.html HTTP/1.1
。其中GET
是请求方法,表示获取资源;/index.html
是请求的 URL 路径,指向服务器上的资源;HTTP/1.1
是协议版本。
- 请求方法类型
- GET 方法
- 用于从服务器获取资源,是最常用的请求方法之一。GET 请求的参数通常会附加在 URL 后面,以
?
开头,多个参数之间用&
分隔。例如:https://example.com/api?param1=value1¶m2=value2
。这种方法的特点是幂等性,即多次执行相同的 GET 请求应该返回相同的结果,并且它对服务器的影响应该是只读的,不会改变服务器的数据状态。
- 用于从服务器获取资源,是最常用的请求方法之一。GET 请求的参数通常会附加在 URL 后面,以
- POST 方法
- 用于向服务器提交数据,通常用于创建新的资源或执行会改变服务器状态的操作。POST 请求的数据放在请求体中,而不是像 GET 请求那样放在 URL 中。例如,当用户在网页上提交一个表单(如注册表单、登录表单等)时,通常会使用 POST 请求将表单数据发送给服务器。POST 请求不是幂等的,多次执行相同的 POST 请求可能会导致不同的结果,因为每次请求都可能会在服务器上创建新的资源或修改数据。
- PUT 方法
- 用于更新服务器上的现有资源。PUT 请求将请求体中的数据更新到指定 URL 对应的资源上。它是幂等的,这意味着多次相同的 PUT 请求应该产生相同的结果,即对资源的更新效果是一样的。例如,如果使用 PUT 请求更新一个用户的信息,多次执行相同的 PUT 操作,只要请求体中的数据相同,那么用户信息的最终状态应该是相同的。
- DELETE 方法
- 用于删除服务器上指定的资源。例如,当用户请求删除一个文件或者一个数据库记录时,可以使用 DELETE 请求。DELETE 请求也是幂等的,多次执行相同的 DELETE 请求应该都能成功删除指定的资源(前提是资源存在并且有权限删除)。
- HEAD 方法
- 与 GET 方法类似,但服务器只返回响应头,不返回响应体。它通常用于获取资源的元信息,比如查看资源是否存在、获取资源的大小或最后修改日期等信息,而不需要实际获取资源的内容。例如,检查一个网页是否被更新,可以使用 HEAD 方法获取网页的最后修改日期等信息。
- OPTIONS 方法
- 用于获取服务器针对特定资源所支持的 HTTP 请求方法。当客户端需要了解服务器对某个 URL 允许的操作方法时,可以发送 OPTIONS 请求。例如,一个跨域请求之前,浏览器可能会先发送 OPTIONS 请求来确定服务器允许的跨域请求方法。
- PATCH 方法
- 用于对资源进行部分更新。与 PUT 方法不同的是,PUT 方法通常是替换整个资源,而 PATCH 方法只更新资源的一部分。例如,只更新用户信息中的电话号码部分,而不是替换整个用户信息,可以使用 PATCH 方法。PATCH 请求也是非幂等的,因为多次执行可能会根据资源的当前状态产生不同的结果。
- GET 方法
- 内容
- 请求头(Request Headers)
- 内容
- 请求头包含了关于请求的各种元信息,如客户端的信息、请求的内容格式、缓存相关信息等。
- 主要类型
- 用户代理(User - Agent)
- 用于标识客户端的软件信息,包括浏览器类型、版本、操作系统等。例如:
User - Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36
。这个信息对于服务器来说很重要,它可以根据用户代理来提供不同的内容适配不同的客户端设备,或者进行用户行为分析等。
- 用于标识客户端的软件信息,包括浏览器类型、版本、操作系统等。例如:
- 接受(Accept)
- 用于告诉服务器客户端能够接受的内容类型。格式为
Accept: <media - type>[, <media - type>]*
。例如:Accept: text/html,application/xhtml+xml,application/xml;q = 0.9,image/avif,image/webp,image/apng,*/*;q = 0.8
。其中q
值表示优先级,范围从 0 到 1,默认值为 1。这表示客户端最希望接收的是text/html
和application/xhtml+xml
类型的内容,对于image/avif
等其他类型的内容也可以接受,但优先级较低。
- 用于告诉服务器客户端能够接受的内容类型。格式为
- 接受语言(Accept - Language)
- 用于告诉服务器客户端能够接受的语言。例如:
Accept - Language: en - US,en;q = 0.9
。这表示客户端最希望接收的是美式英语内容,对于其他英语内容也可以接受,但优先级稍低。服务器可以根据这个信息来提供不同语言版本的内容。
- 用于告诉服务器客户端能够接受的语言。例如:
- 内容类型(Content - Type)
- 当请求体中有内容时,用于告诉服务器请求体的内容格式。例如,对于一个 POST 请求,如果发送的是 JSON 数据,内容类型可以是
Content - Type: application/json
;如果发送的是表单数据,可能是Content - Type: application/x - www - form - urlencoded
或者Content - Type: multipart/form - data
(用于包含文件上传的表单)。
- 当请求体中有内容时,用于告诉服务器请求体的内容格式。例如,对于一个 POST 请求,如果发送的是 JSON 数据,内容类型可以是
- 缓存控制(Cache - Control)
- 用于控制缓存行为。例如,
Cache - Control: no - cache
表示客户端不希望使用缓存,每次都要从服务器获取最新的内容;Cache - Control: max - age = 3600
表示客户端可以使用缓存,并且缓存的有效期是 3600 秒。
- 用于控制缓存行为。例如,
- 授权(Authorization)
- 用于在需要认证的场景下,向服务器提供认证信息。例如,在使用基本认证(Basic Authentication)时,格式为
Authorization: Basic <credentials>
,其中<credentials>
是经过 Base64 编码的用户名和密码组合。在使用 Bearer Token 认证时,格式为Authorization: Bearer <token>
,其中<token>
是认证令牌。
- 用于在需要认证的场景下,向服务器提供认证信息。例如,在使用基本认证(Basic Authentication)时,格式为
- 用户代理(User - Agent)
- 内容
- 请求体(Request Body)
- 内容
- 请求体是可选的部分,它包含了要发送给服务器的数据。数据的格式取决于请求头中的
Content - Type
。
- 请求体是可选的部分,它包含了要发送给服务器的数据。数据的格式取决于请求头中的
- 主要类型
- application/x - www - form - urlencoded
- 这是最常见的表单数据格式。数据以键值对(
key = value
)的形式编码,多个键值对之间用&
分隔,并且特殊字符会进行 URL 编码。例如,一个表单中有用户名和密码两个字段,提交后请求体可能是username = user1&password = password1
。这种格式通常用于简单的表单提交,如登录表单、注册表单等。
- 这是最常见的表单数据格式。数据以键值对(
- multipart/form - data
- 主要用于包含文件上传的表单。它的格式比较复杂,数据被分成多个部分,每个部分都有自己的头部和内容。例如,当上传一个文件和一些表单字段时,请求体中会有一个部分用于文件的二进制数据,另一个部分用于表单字段的数据。这种格式可以方便地同时处理文件和其他表单数据。
- application/json
- 用于发送 JSON 格式的数据。JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,以文本形式表示结构化的数据。例如,发送一个包含用户信息的 JSON 数据可以是
{"name": "John", "age": 30, "email": "john@example.com"}
。这种格式在现代的 Web 开发和 API 交互中非常流行,因为它易于阅读、编写和解析。
- 用于发送 JSON 格式的数据。JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,以文本形式表示结构化的数据。例如,发送一个包含用户信息的 JSON 数据可以是
- application/x - www - form - urlencoded
- 内容
5,HTTP和HTTPS的区别
- 定义和协议基础
- HTTP(超文本传输协议)
- HTTP 是一种用于分布式、协作式和超媒体信息系统的应用层协议。它是互联网数据通信的基础,用于在 Web 浏览器和 Web 服务器之间传输超文本(如 HTML 文档)以及其他资源(如图像、视频、脚本文件等)。它工作在 TCP/IP 协议栈之上,默认使用 TCP 的 80 端口进行通信。
- HTTPS(超文本传输安全协议)
- HTTPS 实际上是在 HTTP 协议的基础上加入了 SSL/TLS(安全套接层 / 传输层安全)协议。SSL 和 TLS 是用于在网络通信中提供加密和身份验证功能的协议。通过使用这些加密协议,HTTPS 可以确保数据在传输过程中的保密性、完整性和身份验证,它通常使用 TCP 的 443 端口进行通信。
- HTTP(超文本传输协议)
- 数据安全方面
- 加密机制
- HTTP
- HTTP 协议传输的数据是明文的。这意味着在数据传输过程中,如用户的登录信息、个人隐私数据、金融交易数据等,都可能被中间人(如网络攻击者)拦截并直接读取。例如,一个用户在 HTTP 网站上登录,用户名和密码是以明文形式在网络上传输的,攻击者通过嗅探网络流量就可以获取这些敏感信息。
- HTTPS
- HTTPS 采用了加密算法对传输的数据进行加密。在 SSL/TLS 握手阶段,客户端和服务器会协商加密算法(如 AES、RSA 等)和密钥。一旦协商完成,在后续的数据传输过程中,所有的数据都会被加密成密文。只有拥有正确密钥的接收方(客户端或服务器)才能将密文解密还原为原始数据。这样,即使数据被中间人拦截,没有密钥也无法获取其中的内容。
- HTTP
- 数据完整性验证
- HTTP
- HTTP 没有提供数据完整性验证机制。数据在传输过程中可能被篡改而客户端无法察觉。例如,一个网页的内容在传输过程中被恶意修改,浏览器无法识别这些修改,仍然会显示被篡改后的内容。
- HTTPS
- HTTPS 通过 SSL/TLS 协议中的消息认证码(MAC)等机制来确保数据的完整性。发送方会在数据中添加一个校验和或者数字签名,接收方可以通过验证这个校验和或者数字签名来判断数据在传输过程中是否被篡改。如果数据被篡改,接收方会发现校验和不匹配或者数字签名无效,从而拒绝接收该数据。
- HTTP
- 身份验证方面
- HTTP
- HTTP 没有提供有效的身份验证机制来验证服务器的真实身份。客户端在与服务器通信时,很难确定与之通信的服务器是否是真正的目标服务器,还是被伪装的服务器。这使得客户端容易受到中间人攻击,攻击者可以伪装成目标服务器来获取用户的信息。
- HTTPS
- HTTPS 使用数字证书来验证服务器的身份。数字证书是由权威的证书颁发机构(CA)颁发的,证书中包含了服务器的公钥、服务器的域名等信息。在 SSL/TLS 握手过程中,服务器会向客户端发送数字证书,客户端会通过验证证书的有效性(包括检查证书是否过期、证书的颁发机构是否可信、证书中的域名与实际访问的域名是否一致等)来确认服务器的身份。只有当服务器的身份验证通过后,客户端才会继续与服务器进行通信。
- HTTP
- 加密机制
- 性能和资源消耗方面
- 连接建立时间
- HTTP
- HTTP 的连接建立相对简单,只需要进行 TCP 三次握手即可开始传输数据。所以在建立连接阶段,HTTP 比 HTTPS 更快。
- HTTPS
- HTTPS 在 TCP 三次握手之后,还需要进行 SSL/TLS 握手。这个过程涉及到加密算法的协商、数字证书的验证等多个步骤,会增加连接建立的时间。例如,在一些网络环境较差或者服务器性能较低的情况下,SSL/TLS 握手可能会导致明显的延迟。
- HTTP
- 资源消耗
- HTTP
- HTTP 因为不需要进行加密和解密操作,对服务器和客户端的 CPU、内存等资源的消耗相对较少。这使得服务器可以在相同的硬件资源下处理更多的 HTTP 请求。
- HTTPS
- HTTPS 由于要进行数据加密和解密,以及证书验证等操作,会消耗更多的服务器和客户端的 CPU 和内存资源。服务器需要更多的计算资源来处理加密和解密请求,尤其是在处理大量并发请求时,这种资源消耗的差异会更加明显。同时,客户端在验证证书和进行解密操作时,也会消耗一定的设备资源。
- HTTP
- 连接建立时间
- 应用场景方面
- HTTP
- 适用于一些对数据安全性要求不高的场景,如一些公开的资讯网站、简单的博客网站等。这些网站主要提供信息展示服务,即使数据被拦截或者篡改,一般不会造成严重的安全后果。
- HTTPS
- 广泛应用于对数据安全和隐私非常重要的场景。包括但不限于网上银行、电子商务网站、电子邮件服务、企业内部的敏感信息系统等。在这些场景中,用户的个人信息、金融交易数据等必须得到严格的保护,以防止数据泄露和恶意篡改。
- HTTP
6,TCP和UDP的区别
-
连接方式
-
TCP(传输控制协议)
-
TCP 是一种面向连接的协议。这意味着在通信双方进行数据传输之前,必须先建立连接。这个连接的建立过程是通过著名的 "三次握手" 来完成的。
-
例如,当客户端想要和服务器通信时,客户端首先发送一个带有 SYN(同步序列号)标志的数据包给服务器,服务器收到后返回一个带有 SYN 和 ACK(确认)标志的数据包,最后客户端再发送一个带有 ACK 标志的数据包给服务器,这样就完成了连接的建立。这个连接就像是一条虚拟的 "管道",数据在这个管道中有序地传输。
-
以 Java 为例,以下是简单的 TCP 客户端和服务器端代码片段来体现这种连接方式:
-
TCP 客户端代码
javaimport java.io.IOException; import java.io.OutputStream; import java.net.Socket; public class TcpClient { public static void main(String[] args) { try { // 创建一个Socket连接到服务器,指定服务器IP和端口 Socket socket = new Socket("127.0.0.1", 8888); // 获取输出流,用于向服务器发送数据 OutputStream outputStream = socket.getOutputStream(); String data = "Hello, TCP Server!"; outputStream.write(data.getBytes()); // 关闭连接 socket.close(); } catch (IOException e) { e.printStackTrace(); } } }
-
TCP 服务器端代码
javaimport java.io.IOException; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; public class TcpServer { public static void main(String[] args) { try { // 创建一个ServerSocket,监听指定端口 ServerSocket serverSocket = new ServerSocket(8888); System.out.println("等待客户端连接..."); // 接受客户端连接,此方法会阻塞,直到有客户端连接 Socket socket = serverSocket.accept(); System.out.println("客户端已连接"); // 获取输入流,用于接收客户端发送的数据 InputStream inputStream = socket.getInputStream(); byte[] buffer = new byte[1024]; int length = inputStream.read(buffer); String data = new String(buffer, 0, length); System.out.println("收到客户端数据: " + data); // 关闭连接 socket.close(); serverSocket.close(); } catch (IOException e) { e.printStackTrace(); } } }
-
-
-
UDP(用户数据报协议)
-
UDP 是一种无连接的协议。这意味着通信双方在发送数据之前不需要建立连接。每个 UDP 数据包(称为数据报)都是独立的,发送方只管发送数据报,不关心接收方是否准备好了接收,也不关心接收方是否能收到。
-
例如,在一个简单的 UDP 通信场景中,发送方可以直接将数据报发送到指定的 IP 地址和端口,而不需要像 TCP 那样先建立连接。接收方如果开启了对应的端口监听,就可以接收到数据报,但如果接收方没有开启监听或者因为网络原因没有收到数据报,发送方是不会得到通知的。
-
以下是简单的 UDP 客户端和服务器端代码片段:
-
UDP 客户端代码
javaimport java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; public class UdpClient { public static void main(String[] args) { try { // 创建一个UDP套接字 DatagramSocket socket = new DatagramSocket(); String data = "Hello, UDP Server!"; byte[] buffer = data.getBytes(); // 获取服务器IP地址和端口 InetAddress serverAddress = InetAddress.getByName("127.0.0.1"); int serverPort = 9999; // 创建一个数据报,包含要发送的数据、数据长度、目标IP地址和端口 DatagramPacket packet = new DatagramPacket(buffer, buffer.length, serverAddress, serverPort); // 发送数据报 socket.send(packet); // 关闭套接字 socket.close(); } catch (Exception e) { e.printStackTrace(); } } }
-
UDP 服务器端代码
javaimport java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; public class UdpServer { public static void main(String[] args) { try { // 创建一个UDP套接字,监听指定端口 DatagramSocket socket = new DatagramSocket(9999); byte[] buffer = new byte[1024]; // 创建一个数据报,用于接收数据 DatagramPacket packet = new DatagramPacket(buffer, buffer.length); // 接收数据报,此方法会阻塞,直到收到数据报 socket.receive(packet); String data = new String(packet.getData(), 0, packet.getLength()); System.out.println("收到客户端数据: " + data); // 获取发送方的IP地址和端口 InetAddress clientAddress = packet.getAddress(); int clientPort = packet.getPort(); System.out.println("数据来自: " + clientAddress + ",端口: " + clientPort); // 关闭套接字 socket.close(); } catch (Exception e) { e.printStackTrace(); } } }
-
-
-
-
可靠性
- TCP
- TCP 提供可靠的数据传输服务。它通过序列号、确认应答、重传机制等来确保数据的准确传输。
- 例如,发送方会为每个发送的数据包分配一个序列号,接收方收到数据包后会返回一个确认应答(ACK),告知发送方已经收到该数据包。如果发送方在一定时间内没有收到确认应答,就会认为数据包丢失,然后重新发送该数据包。这种机制可以保证数据能够完整、正确地到达目的地。
- UDP
- UDP 不提供可靠的数据传输服务。由于 UDP 是无连接的,并且没有重传机制等保证数据可靠传输的措施,数据报在传输过程中可能会丢失、重复或者乱序。
- 例如,在网络拥塞或者网络质量较差的情况下,UDP 数据包可能会因为路由器缓存溢出等原因而丢失。不过,在一些对实时性要求较高但对数据完整性要求不高的场景中,这种少量数据丢失是可以接受的。
- TCP
-
数据传输效率和开销
- TCP
- 由于 TCP 需要建立连接、维护连接状态,并且要进行数据的可靠传输控制,所以它的开销相对较大。在传输数据时,TCP 会对数据进行分段、编号、确认等操作,这会消耗一定的系统资源和网络带宽。
- 例如,在网络带宽有限的情况下,TCP 用于连接维护和数据控制的开销可能会占据一定的带宽比例,从而影响数据的实际传输效率。不过,TCP 的可靠性使得它适用于对数据准确性要求很高的场景。
- UDP
- UDP 的开销相对较小。因为它没有连接建立和维护的过程,也不需要进行复杂的数据可靠性控制操作。UDP 只是简单地将数据封装成数据报然后发送出去。
- 例如,在一些对实时性要求很高的场景,如在线游戏、实时视频流等,UDP 可以更快地将数据发送出去,虽然可能会有数据丢失的风险,但对于这些场景来说,少量的数据丢失可能比延迟更能被接受。
- TCP
-
应用场景
- TCP
- 适用于对数据准确性和完整性要求很高的场景,如文件传输(FTP)、电子邮件(SMTP、POP3 等)、网页浏览(HTTP 部分场景)等。在这些场景中,确保数据完整无误地传输是至关重要的。
- UDP
- 适用于对实时性要求高、对数据丢失有一定容忍度的场景。例如,实时音视频通话(如 VoIP)、在线游戏中的玩家位置和动作更新等。在这些场景中,及时传输数据比确保每一个数据都准确无误地传输更为重要。
- TCP
7,TCP断开连接的过程
TCP 断开连接的过程通常被称为 "四次挥手",以下是具体步骤:
- 第一次挥手(FIN):假设客户端想要关闭连接,客户端会向服务器发送一个带有 FIN 标志的数据包,表示客户端不再发送数据了,但客户端仍可以接收服务器发送的数据。此时,客户端进入 FIN_WAIT_1 状态。
- 第二次挥手(ACK):服务器接收到客户端的 FIN 包后,会发送一个 ACK 包作为确认,告诉客户端已经收到了关闭请求。此时,服务器进入 CLOSE_WAIT 状态,客户端收到 ACK 后进入 FIN_WAIT_2 状态。服务器可以继续发送数据,直到它也准备好关闭连接。
- 第三次挥手(FIN):当服务器完成数据发送,也准备好关闭连接时,服务器会向客户端发送一个 FIN 包,表示服务器也不再发送数据了。此时,服务器进入 LAST_ACK 状态。
- 第四次挥手(ACK):客户端接收到服务器的 FIN 包后,发送一个 ACK 包作为确认。此时,客户端进入 TIME_WAIT 状态,服务器接收到 ACK 包后进入 CLOSED 状态。客户端在 TIME_WAIT 状态等待一段时间(通常是 2 倍的 MSL,即最大段生存时间),以确保服务器接收到最后的 ACK 包。客户端等待时间结束后,进入 CLOSED 状态,连接正式关闭。
8,拥塞控制步骤
-
慢启动(Slow - Start)阶段
-
原理
- TCP 连接建立后,发送方并不知道网络的拥塞状况。所以,在慢启动阶段,发送方会以一个较小的拥塞窗口(cwnd)开始发送数据。初始时,拥塞窗口大小通常为 1 个最大报文段(MSS,Maximum Segment Size)。每收到一个对新发送数据的确认(ACK),拥塞窗口就会翻倍。例如,发送方发送了 1 个 MSS 大小的数据段,收到 ACK 后,cwnd 变为 2;再发送 2 个 MSS 大小的数据段,收到这 2 个 ACK 后,cwnd 变为 4,以此类推。这种指数增长的方式可以快速地探测网络的可用带宽。
-
目的
- 避免一开始就向网络中注入大量的数据而导致网络拥塞。同时,通过快速增加发送窗口的大小,来尽快利用网络的空闲带宽。
-
示例代码(简单示意)
-
在 Linux 内核中,慢启动阶段的拥塞窗口增长是由内核自动控制的。但我们可以通过简单的代码来理解这个过程。以下是一个简单的 Python 代码片段,模拟慢启动阶段拥塞窗口的增长(假设每次收到 ACK 后,就模拟更新拥塞窗口大小):
python# 初始拥塞窗口大小为1个MSS cwnd = 1 # 模拟收到ACK的次数 ack_count = 0 # 慢启动阶段,每收到一个ACK,拥塞窗口翻倍 while ack_count < 10: cwnd *= 2 ack_count += 1 print("经过{}次ACK后,拥塞窗口大小为: {}".format(ack_count, cwnd))
-
-
-
拥塞避免(Congestion - Avoidance)阶段
-
原理
- 当拥塞窗口大小达到一个阈值(ssthresh,Slow - Start Threshold)时,TCP 会进入拥塞避免阶段。在这个阶段,拥塞窗口不再是指数增长,而是线性增长。具体来说,每收到一个 ACK,拥塞窗口大小增加 1 个 MSS。例如,如果 ssthresh 为 16 个 MSS,当 cwnd 达到 16 后,进入拥塞避免阶段,每次收到 ACK,cwnd 就从 16 变为 17,再收到 ACK 变为 18,以此类推。
-
目的
- 因为慢启动阶段拥塞窗口增长速度很快,当达到一定程度后,为了避免网络拥塞,需要减缓增长速度,更加谨慎地增加发送数据量,以探测网络的实际承载能力。
-
示例代码(简单示意)
-
同样以 Python 代码来简单模拟拥塞避免阶段拥塞窗口的增长(假设已经进入拥塞避免阶段,并且知道 ssthresh 的值):
python# 假设已经进入拥塞避免阶段,ssthresh为16个MSS,初始拥塞窗口大小为16 cwnd = 16 ssthresh = 16 # 模拟收到ACK的次数 ack_count = 0 # 拥塞避免阶段,每收到一个ACK,拥塞窗口增加1个MSS while ack_count < 10: cwnd += 1 ack_count += 1 print("经过{}次ACK后,拥塞窗口大小为: {}".format(ack_count, cwnd))
-
-
-
快重传(Fast - Retransmit)阶段
-
原理
- 当接收方收到一个失序的数据段时,会立即发送重复的 ACK,告诉发送方期望收到的下一个数据段。如果发送方连续收到 3 个(这个数字可以根据具体的 TCP 实现调整)相同的重复 ACK,就会认为该数据段丢失了,而不是等待超时定时器(RTO,Retransmission Time - Out)到期才重传。这种快速重传丢失数据段的机制可以减少数据传输的延迟。
-
目的
- 尽快恢复丢失的数据段,避免因为等待超时才重传而导致长时间的延迟。因为在网络拥塞时,数据段丢失的可能性较大,通过快重传可以提高数据传输的效率。
-
示例代码(简单示意)
-
以下是一个简单的 Java 代码片段,用于模拟发送方如何处理重复的 ACK 来进行快重传(这是一个简单的逻辑示意,实际的 TCP 实现要复杂得多):
javaimport java.util.ArrayList; import java.util.List; public class FastRetransmitExample { private List<Integer> ackList = new ArrayList<>(); public void processAck(int ackNumber) { ackList.add(ackNumber); int lastAck = ackNumber; int count = 0; // 检查是否收到3个相同的重复ACK for (int i = ackList.size() - 1; i >= 0 && count < 3; i--) { if (ackList.get(i) == lastAck) { count++; } else { break; } } if (count == 3) { System.out.println("收到3个相同的重复ACK,快速重传数据段"); // 在这里进行快速重传数据段的操作,实际实现会更复杂 } } }
-
-
-
快恢复(Fast - Recovery)阶段
-
原理
- 在快重传之后,发送方不是像慢启动阶段那样重新开始,而是进入快恢复阶段。在这个阶段,拥塞窗口大小调整为新的 ssthresh(通常设置为当前拥塞窗口大小的一半),然后开始拥塞避免阶段的线性增长。例如,如果在快重传之前 cwnd 为 16,发生快重传后,ssthresh 变为 8,cwnd 也变为 8,然后开始每收到一个 ACK,cwnd 增加 1 个 MSS 的拥塞避免增长。
-
目的
- 快速恢复数据传输,并且避免因为重新进入慢启动阶段而导致传输效率过低。通过适当调整拥塞窗口大小,在恢复数据传输的同时,尽量减少对网络性能的影响。
-
示例代码(简单示意)
-
以下是一个简单的 Python 代码片段,用于模拟快恢复阶段拥塞窗口的调整和后续增长(假设已经发生快重传):
python# 假设发生快重传后,调整ssthresh为当前拥塞窗口大小的一半,初始拥塞窗口也调整为新的ssthresh cwnd = 8 ssthresh = 8 # 模拟收到ACK的次数 ack_count = 0 # 快恢复阶段,每收到一个ACK,拥塞窗口增加1个MSS while ack_count < 10: cwnd += 1 ack_count += 1 print("经过{}次ACK后,拥塞窗口大小为: {}".format(ack_count, cwnd))
-
-
9,MySQL索引类型
-
B - Tree 索引(B - Tree Index)
-
结构特点
- B - Tree(平衡多路查找树)索引是 MySQL 中最常用的索引类型。它是一种平衡树结构,每个节点可以有多个子节点。树的高度相对较低,使得查找数据的效率较高。在 B - Tree 索引中,数据存储在叶子节点,非叶子节点只存储索引键值和指向子节点的指针。叶子节点之间通过双向链表连接,方便范围查询。
-
示例说明查找过程
- 假设我们有一个员工表(employees),表中有员工编号(employee_id)、姓名(name)和部门(department)等字段,并且在员工编号字段上建立了 B - Tree 索引。当我们执行查询
SELECT * FROM employees WHERE employee_id = 123;
时,MySQL 会从 B - Tree 索引的根节点开始查找。根节点会根据索引键值(员工编号)的大小判断应该沿着哪个子节点继续查找,一直到叶子节点找到对应的员工编号记录。
- 假设我们有一个员工表(employees),表中有员工编号(employee_id)、姓名(name)和部门(department)等字段,并且在员工编号字段上建立了 B - Tree 索引。当我们执行查询
-
适用场景
- 适用于全键值、键值范围或键前缀查找。例如,查找某个具体员工的信息(全键值查找),查找员工编号在某个范围内的员工信息(范围查找),或者查找以某个字符串开头的姓名(键前缀查找)。
-
创建 B - Tree 索引的 SQL 示例(以员工表为例)
sqlCREATE INDEX idx_employee_id ON employees (employee_id);
-
-
哈希(Hash)索引
-
结构特点
- 哈希索引是基于哈希表实现的。它通过一个哈希函数将索引键值转换为一个哈希码,然后将哈希码映射到对应的存储位置。哈希索引的查找速度非常快,在理想情况下,时间复杂度可以达到 O (1),因为它只需要通过哈希函数计算键值对应的位置即可。
-
示例说明查找过程
- 假设我们有一个简单的哈希索引用于存储用户的登录信息,键值是用户名,值是用户的密码哈希值。当用户登录时,输入用户名和密码,系统会通过哈希函数计算用户名对应的哈希码,然后直接在哈希索引中查找对应的密码哈希值,与用户输入的密码哈希值进行比较,从而验证登录信息。
-
适用场景
适用于等值查询,即只用于查找键值完全匹配的情况。例如,在用户认证系统中查找用户的密码哈希值,或者在缓存系统中查找缓存项。但是,哈希索引不支持范围查询,因为哈希表的存储是无序的。
-
注意事项
- MySQL 中只有 Memory 存储引擎(以前称为 HEAP 存储引擎)默认支持哈希索引,并且是在特定条件下自动创建的。在 InnoDB 和 MyISAM 等常用存储引擎中,一般需要通过其他方式来模拟哈希索引的功能。
-
-
全文(Full - Text)索引
-
结构特点
- 全文索引是一种特殊的索引,用于在文本数据中进行全文搜索。它会对文本内容进行分词处理,将文本拆分成一个个单词或词组,然后建立索引。MySQL 使用特定的全文搜索引擎来处理全文索引,例如在 InnoDB 存储引擎中,从 MySQL 5.6 版本开始支持全文索引,它使用了一个内置的全文搜索引擎。
-
示例说明查找过程
- 假设我们有一个博客文章表(articles),表中有文章内容(content)字段,并且建立了全文索引。当我们执行查询
SELECT * FROM articles WHERE MATCH(content) AGAINST ('关键词');
时,MySQL 会首先对查询的关键词进行分词,然后在全文索引中查找包含这些关键词的文章记录。
- 假设我们有一个博客文章表(articles),表中有文章内容(content)字段,并且建立了全文索引。当我们执行查询
-
适用场景
- 适用于在大量文本数据中进行模糊搜索、关键词搜索等。例如,在搜索引擎、内容管理系统、论坛等应用中,用于快速查找包含特定关键词的文本内容。
-
创建全文索引的 SQL 示例(以博客文章表为例)
sqlALTER TABLE articles ADD FULLTEXT (content);
-
-
空间(Spatial)索引
-
结构特点
- 空间索引用于处理地理空间数据,如点、线、多边形等地理信息。MySQL 使用 R - Tree(区域树)来实现空间索引,它可以高效地处理空间数据的查询,如查找在某个地理区域内的点,或者两个地理区域的交集等。
-
示例说明查找过程
- 假设我们有一个店铺位置表(stores),表中有店铺的地理位置信息(以点的坐标表示),并且建立了空间索引。当我们执行查询
SELECT * FROM stores WHERE MBRContains (空间索引列, GeomFromText('POLYGON((x1 y1, x2 y2, x3 y3, x1 y1))'));
(这里的 MBRContains 是一个空间函数,用于判断一个空间对象是否包含在另一个空间对象中)时,MySQL 会利用空间索引快速查找在指定多边形区域内的店铺。
- 假设我们有一个店铺位置表(stores),表中有店铺的地理位置信息(以点的坐标表示),并且建立了空间索引。当我们执行查询
-
适用场景
- 适用于地理信息系统(GIS)相关的应用,如地图应用、位置服务等,用于处理地理位置相关的查询。
-
创建空间索引的 SQL 示例(以店铺位置表为例)
sqlCREATE SPATIAL INDEX idx_store_location ON stores (location);
-
10,MySQL B+树
- B + 树结构概述
- B + 树是 B - 树的一种变体,它是一种平衡的多路查找树。在 MySQL 中,InnoDB 存储引擎使用 B + 树来构建索引结构。B + 树主要由根节点(root node)、分支节点(branch node)和叶子节点(leaf node)组成。
- 与 B - 树不同的是,B + 树的非叶子节点只用于索引,不存储实际的数据记录,所有的数据记录都存储在叶子节点。叶子节点之间通过双向链表连接,这种结构使得范围查询(如查询某一区间内的数据)更加高效。
- 节点结构细节
- 根节点
- 是 B + 树的最顶层节点,它起到引导查询方向的作用。根节点包含指向子节点的指针和索引键值。对于一个新创建的表,根节点可能同时也是叶子节点,随着数据量的增加,根节点会分裂并产生分支节点。
- 分支节点
- 分支节点位于根节点和叶子节点之间,它存储索引键值和指向子节点的指针。分支节点中的键值用于确定查询应该沿着哪个子节点继续向下查找。例如,如果要查找一个键值为 K 的数据,当查询到分支节点时,会比较 K 与分支节点中的键值大小,从而决定进入哪个子节点。
- 叶子节点
- 叶子节点存储了实际的数据记录或者数据记录的指针(取决于具体的存储方式)。叶子节点中的数据是按照索引键值的大小顺序排列的,并且相邻的叶子节点通过双向链表连接。这意味着可以通过链表顺序遍历叶子节点,方便进行范围查询。
- 根节点
- 数据存储和查询过程
- 插入数据
- 当向表中插入一条新数据时,MySQL 会根据索引列的值在 B + 树中找到合适的位置插入。首先从根节点开始,比较索引键值与根节点中的键值,确定应该进入哪个分支节点。然后在分支节点中继续比较,直到找到合适的叶子节点。如果叶子节点已满,会触发节点分裂操作。例如,在一个以员工编号为索引的 B + 树中,插入新员工记录时,会按照员工编号的大小找到对应的叶子节点插入记录。
- 查询数据
- 查询过程类似插入过程。例如,执行查询
SELECT * FROM employees WHERE employee_id = 123;
(假设员工表 employees 的员工编号 employee_id 列有 B + 树索引)。从根节点开始,比较 123 与根节点中的键值大小,确定进入哪个分支节点,然后在分支节点中继续比较,直到找到包含 123 这个键值的叶子节点。一旦找到叶子节点,就可以获取对应的员工记录。
- 查询过程类似插入过程。例如,执行查询
- 范围查询
- 由于叶子节点之间有双向链表连接,范围查询非常方便。例如,查询员工编号在 100 到 200 之间的员工记录,首先找到键值为 100 的叶子节点,然后通过链表顺序遍历叶子节点,直到找到键值大于 200 的节点为止,期间获取的所有叶子节点中的数据就是满足范围查询的数据。
- 插入数据
- B + 树在 MySQL 中的优势
- 高效的磁盘 I/O
- B + 树的高度相对较低,这使得在查询数据时,磁盘 I/O 的次数较少。因为每次读取一个节点相当于一次磁盘 I/O 操作,较低的树高意味着在有限的磁盘 I/O 次数内可以找到所需的数据。例如,一个高度为 3 的 B + 树,在最坏情况下,只需要 3 次磁盘 I/O 就可以找到叶子节点中的数据。
- 支持范围查询和排序
- 叶子节点的链表结构使得范围查询非常高效。同时,由于数据在叶子节点中是按照索引键值排序存储的,这也为排序操作提供了便利。当执行
ORDER BY
子句与索引列相关的查询时,MySQL 可以直接利用 B + 树的排序特性,减少额外的排序开销。
- 叶子节点的链表结构使得范围查询非常高效。同时,由于数据在叶子节点中是按照索引键值排序存储的,这也为排序操作提供了便利。当执行
- 高效的磁盘 I/O
11,MVCC
-
MVCC(多版本并发控制)定义
- MVCC 是一种并发控制的技术,它主要用于数据库管理系统中,目的是在多个事务并发访问数据库时,能够在保证数据一致性的前提下,提高系统的并发性能。它通过为每个事务提供一个数据的快照(Snapshot)来实现,使得每个事务都能看到一个相对独立的数据库视图,就好像每个事务都有自己的一份数据库副本一样。
-
MVCC 在 MySQL 中的实现(以 InnoDB 为例)
-
事务版本号和行版本号
- InnoDB 为每个事务分配一个唯一的事务版本号,这个版本号是递增的。同时,对于数据库中的每一行数据,都会存储两个额外的隐藏列(实际上是系统列,用户不可见),一个是创建版本号(
DB_TRX_ID
),另一个是删除版本号(DB_ROLL_PTR
)。创建版本号记录了插入该行数据的事务版本号,删除版本号用于记录删除该行数据的事务版本号(如果该行还没有被删除,这个值为NULL
)。
- InnoDB 为每个事务分配一个唯一的事务版本号,这个版本号是递增的。同时,对于数据库中的每一行数据,都会存储两个额外的隐藏列(实际上是系统列,用户不可见),一个是创建版本号(
-
数据版本可见性规则
- 当一个事务读取一行数据时,会根据以下规则来判断该行数据是否可见:
- 如果该行数据的创建版本号小于或等于当前事务的版本号,并且删除版本号大于当前事务的版本号或者为
NULL
,那么该行数据对当前事务是可见的。这意味着该行数据是在当前事务开始之前就已经插入,并且没有在当前事务开始之后被删除。 - 例如,事务 T1 的版本号为 10,它读取一行数据,该行的创建版本号为 8,删除版本号为
NULL
,那么这行数据对 T1 是可见的,因为 8(创建版本号)≤10(T1 的版本号),并且NULL
(删除版本号)>10 或者为NULL
。
- 如果该行数据的创建版本号小于或等于当前事务的版本号,并且删除版本号大于当前事务的版本号或者为
- 当一个事务读取一行数据时,会根据以下规则来判断该行数据是否可见:
-
示例说明可见性规则
-
假设有三个事务 T1、T2、T3,事务版本号依次为 1、2、3。有一行数据最初的创建版本号为 1(由 T1 插入),删除版本号为
NULL
- 当 T2 读取这行数据时,因为 1(创建版本号)≤2(T2 的版本号),并且
NULL
(删除版本号)>2 或者为NULL
,所以这行数据对 T2 是可见的。 - 假设 T3 执行了删除这行数据的操作,那么这行数据的删除版本号变为 3(T3 的版本号)。现在,如果 T2 再次读取这行数据,由于 1(创建版本号)≤2(T2 的版本号),但是 3(删除版本号)≤2 不成立,所以这行数据对 T2 不再可见。
- 当 T2 读取这行数据时,因为 1(创建版本号)≤2(T2 的版本号),并且
-
-
-
MVCC 的优势
- 提高并发性能
- MVCC 允许不同的事务同时访问数据库中的同一行数据,只要它们访问的是不同版本的数据。这就避免了传统的锁机制可能导致的大量事务等待,提高了系统的并发处理能力。例如,在一个高并发的读写场景中,多个读事务可以同时进行,而不会被写事务阻塞,只要写事务修改的数据版本不会影响读事务看到的数据版本。
- 保证数据一致性
- 通过数据版本的控制,MVCC 能够确保每个事务看到的数据是符合事务隔离级别的要求的。在不同的事务隔离级别(如读已提交、可重复读等)下,MVCC 可以根据规则提供相应的数据一致性保证。例如,在可重复读隔离级别下,一个事务在整个事务期间看到的数据版本是固定的,这是通过 MVCC 的数据版本控制来实现的,从而保证了数据的一致性。
- 提高并发性能
12,mysql的视图是什么
-
视图的定义
- 视图是一种虚拟的表,它是从一个或多个表(或其他视图)中通过查询语句导出的。视图本身不存储数据,它的数据是在查询视图时动态生成的,是对查询操作的一种封装。可以把视图看作是一个存储起来的查询,它的内容是由定义视图的查询语句决定的。
-
视图的创建语法
-
在 MySQL 中,创建视图的基本语法如下:
sqlCREATE VIEW view_name AS SELECT column1, column2,... FROM table_name WHERE condition;
-
例如,有一个名为
employees
的表,包含
employee_id name department salary
列。如果想要创建一个视图,只显示部门为
IT
的员工姓名和工资,可以使用以下语句:
sqlCREATE VIEW it_employees_view AS SELECT name, salary FROM employees WHERE department = 'IT';
-
-
视图的用途
- 简化复杂查询
- 当需要经常执行一些复杂的查询时,通过创建视图可以将这些复杂的查询逻辑封装起来。例如,在一个包含多个表连接和复杂筛选条件的查询中,将其定义为视图后,后续使用时只需要像查询普通表一样查询视图即可。这样可以提高查询的效率和可读性,特别是对于那些不熟悉复杂查询逻辑的用户。
- 数据安全性和权限控制
- 视图可以用于限制用户对敏感数据的访问。通过在视图中选择特定的列和行,可以隐藏表中的某些敏感信息。例如,对于一个包含员工工资等敏感信息的表,可以创建一个视图,只显示员工姓名和部门等非敏感信息,然后将视图的访问权限授予用户,而不是直接授予对原始表的访问权限,这样可以增强数据的安全性。
- 数据的逻辑独立性
- 如果数据库的表结构发生了变化,只要视图的定义能够适应这种变化(例如,视图基于的表添加了新列,但视图查询语句仍然有效),那么使用视图的应用程序和用户可以不受影响。这使得数据库的维护和更新更加灵活,提高了数据的逻辑独立性。
- 简化复杂查询
-
视图的更新操作
- 视图可以用于查询数据,但对于更新操作(INSERT、UPDATE、DELETE),情况会比较复杂。
- 可更新视图的条件
- 如果视图是基于单个表,并且视图中的列包含了没有经过计算、聚合等操作的原始表列,同时视图中没有使用
GROUP BY
、HAVING
、DISTINCT
等限制更新的操作符,那么这个视图通常是可以更新的。例如,前面创建的it_employees_view
视图,理论上可以更新员工的姓名和工资,因为它是基于单个employees
表,且列没有经过复杂的计算。
- 如果视图是基于单个表,并且视图中的列包含了没有经过计算、聚合等操作的原始表列,同时视图中没有使用
- 更新视图的限制和注意事项
- 当更新视图时,实际上是在更新视图所基于的表。但是,如果视图的定义比较复杂,包含了多表连接、聚合函数等,那么更新操作可能会导致不可预期的结果或者不被允许。例如,一个视图是通过连接两个表并计算平均值得到的,对这样的视图进行插入操作是没有意义的,并且 MySQL 通常会禁止这种更新操作。
13,编写SQL,统计每个客户的订单总金额
MySQL 示例
假设有customers
表(包含customer_id
客户编号,customer_name
客户名称等字段)和orders
表(包含order_id
订单编号,customer_id
客户编号,order_amount
订单金额等字段)。
sql
SELECT
c.customer_name,
SUM(o.order_amount) AS total_order_amount
FROM
customers c
JOIN
orders o ON c.customer_id = o.customer_id
GROUP BY
c.customer_id;
14,编写SQL,查询每个用户访问的不同页面的总数量
以下是几种常见数据库(MySQL、Oracle、SQL Server)中用于查询每个用户访问的不同页面的总数量的 SQL 示例,这里假设存在一张名为 user_visits
的表,表中包含 user_id
(用户 ID)、page_url
(页面 URL)等相关字段,你可以根据实际数据库中的表结构和字段名进行相应调整。
MySQL 示例
sql
SELECT
user_id,
COUNT(DISTINCT page_url) AS distinct_page_count
FROM
user_visits
GROUP BY
user_id;