feat: 更新附录交互组件和文档
This commit is contained in:
@@ -1,84 +1,224 @@
|
||||
# 计算机网络:从输入网址到返回结果的过程
|
||||
|
||||
::: tip 🎯 核心问题
|
||||
**当你在浏览器输入 www.google.com 并按下回车,到底发生了什么?**
|
||||
**当你舒服地靠在沙发上,在手机浏览器里输入 `www.google.com` 并按下回车,为什么几百毫秒后,搜索结果就能准确无误地出现在你的屏幕上?**
|
||||
|
||||
这个看似简单的动作,背后隐藏着一个庞大精密的跨国“快递系统”。从填写订单(URL解析)到查询地址簿(DNS解析),从建立运输通道(TCP握手)到快递员送货(HTTP请求与响应),最终在你屏幕上拆开包裹组装(浏览器渲染)。本章带你零基础、完整理解这个神奇的过程。
|
||||
在上一章中,我们知道了数据是如何被编码成 0 和 1 并通过海底光缆传输的。但这还不够。互联网上的服务器浩如烟海,你的手机是怎么在茫茫机海中精准找到 Google 的服务器,商量好暗号,并成功把页面要回来的呢?
|
||||
|
||||
这个看似无比简单的"敲回车"动作,背后其实隐藏着一个精密到令人震撼的跨国"快递接力系统"。本章,我们不讲枯燥的八股文概念,而是顺着**"填写购物单 -> 查地址簿 -> 打电话确认 -> 寄包裹 -> 自己拆解组装"**这条主线,带你零基础看清网络世界的全貌。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 全景演示:网络世界的快递系统
|
||||
## 第一步:填写购物单 (URL 解析)
|
||||
|
||||
你可以通过下方的交互组件,直观地体验从输入网址到看到网页的 5 个关键步骤。先自己点一点,然后再看底下的详细解释!
|
||||
**目标**:把人类能看懂的网址,翻译成浏览器能理解的结构化信息。
|
||||
|
||||
<UrlToBrowserDemo />
|
||||
当你在地址栏中输入 `https://www.google.com/search` 时,浏览器第一步必须先把你输入的这段"人类文字",仔细拆解成它能看懂的标准化字段。
|
||||
|
||||
这就像是你准备去商店买东西,首先要在**购物单**上写清楚:用什么交通工具去、去哪家店、拿什么货。
|
||||
|
||||
<UrlParserDemo />
|
||||
|
||||
**💡 核心原理解析:URL是怎么分工的?**
|
||||
|
||||
- **交通方式(Protocol/协议)**:比如开头写的 `https://`。这代表你要求坐安全级别最高的"运钞车"(加密通信)去。如果是老式的 `http://`,就相当于坐敞篷车,你一路上买什么都会被别人看光。
|
||||
- **店铺名(Host/主机名)**:比如 `www.google.com`。这就是你要去哪家店(也就是服务器的域名)。
|
||||
- **具体货架(Path/路径)**:比如后面的 `/search`。这代表进了店门之后,你要去哪个房间拿具体的哪份文件。
|
||||
|
||||
**这一步完成了什么?** 浏览器现在知道了:我要用 HTTPS 协议,去 `www.google.com` 这个域名对应的服务器,获取 `/search` 路径下的内容。
|
||||
|
||||
**但问题来了**:浏览器知道了域名,但网络世界只认数字 IP 地址。就像你知道"王府井大饭店",但司机需要 GPS 坐标。下一步,我们需要把域名转换成 IP 地址。
|
||||
|
||||
---
|
||||
|
||||
## 1. 填写购物单 (URL 解析)
|
||||
## 第二步:查地址簿 (DNS 解析)
|
||||
|
||||
当你在浏览器的地址栏中输入 `https://www.google.com` 这样一段地址并按下回车,这就像是你准备去商店买东西,首先要在**购物单**上写清楚:
|
||||
**上一步完成了**:浏览器拆解了 URL,知道了目标域名是 `www.google.com`。
|
||||
|
||||
- **交通方式 (Protocol)**:例如 `https://`,代表你想坐安全级别的最高的“运钞车”(加密通信)去。如果是单纯的 `http://`,就相当于坐普通的“大巴”(明文传输),路上可能会被人偷看行李。
|
||||
- **店铺地址 (Host)**:例如 `www.google.com`,也就是你要去哪家店(域名)。
|
||||
- **商品位置 (Path)**:例如 `/search`,意思是进了商店之后,你要去哪个货架找什么东西(即请求的具体资源路径)。
|
||||
**这一步要实现**:把域名转换成 IP 地址,让浏览器知道服务器的精确位置。
|
||||
|
||||
浏览器第一步要做的,就是把这段“人类语言”拆解开,看看你到底想要什么。
|
||||
**目的**:网络世界的底层路由器(负责指路的交警)根本不懂英文,它们**只认数字**,也就是所谓的 **IP 地址(如 142.250.80.46)**。
|
||||
|
||||
<DnsLookupDemo />
|
||||
|
||||
**💡 核心原理解析:找"114查号台"**
|
||||
|
||||
既然必须用 IP 地址,浏览器就会走一个叫做 **DNS (Domain Name System)** 的打听流程:
|
||||
|
||||
1. **翻自己的备忘录(本地缓存)**:浏览器会先翻翻自己的浏览历史,看看前几天是不是刚去过这家店,记没记过它的数字地址。如果记了,直接用。
|
||||
2. **打电话给查号台(递归查询)**:如果实在没见过,它就会向互联网的"总查号台"(通常由你的宽带运营商提供,比如联通、电信的 DNS 服务器)发请求:"你好,请帮我查一下,google.com 对应的数字坐标是几?"
|
||||
3. **拿到坐标**:查号台通过逐级查询,最终把一个准确的 IP 地址(如 `142.250.80.46`)发回给你的手机。
|
||||
|
||||
**这一步完成了什么?** 浏览器现在拿到了 Google 服务器的精确 IP 地址 `142.250.80.46`。
|
||||
|
||||
**但问题来了**:有了 IP 地址就能直接发请求了吗?万一服务器宕机了呢?万一网线断了呢?如果直接发请求,对方没收到,就成了鸡同鸭讲。下一步,我们需要先确认双方能正常通信。
|
||||
|
||||
---
|
||||
|
||||
## 2. 查找店铺地址 (DNS 解析)
|
||||
## 第三步:打电话确认 (TCP 三次握手)
|
||||
|
||||
网络世界的“快递员”(路由器设备)是不懂英文的,它们只认数字(也就是 **IP 地址**)。
|
||||
**上一步完成了**:浏览器通过 DNS 查询,拿到了服务器的 IP 地址 `142.250.80.46`。
|
||||
|
||||
它们需要知道对方的精确数字坐标!这就像快递员不知道“王府井百货”在哪,他必须先查地图,找到“北京市东城区王府井大街255号”这个确切的门牌号(比如 `142.250.66.4`)。
|
||||
**这一步要实现**:建立一条可靠的通信通道,确保双方都能收发数据。
|
||||
|
||||
- **本地缓存**:浏览器会先翻翻自己的备忘录(看之前有没有访问过该网站)。
|
||||
- **DNS 系统**:如果在本地找不到,它就会向互联网的“查号台”(DNS 服务器)打电话询问:“请问 google.com 的数字地址是什么?”。一旦获得了对应的 IP 地址,浏览器的快递车就知道该往哪里开了。
|
||||
**目的**:在正式传输数据之前,必须先确认"对方在线"且"双方收发通道都正常"。这就像打电话前要先确认"喂,能听到吗?"
|
||||
|
||||
<TcpHandshakeDemo />
|
||||
|
||||
**💡 核心原理解析:为什么非得是"三"次?**
|
||||
|
||||
不要被专业名词吓到,它完全可以在现实生活中还原。想象一下你给朋友打电话:
|
||||
|
||||
---
|
||||
|
||||
## 3. 建立通话 (TCP 握手)
|
||||
### 第一次握手:SYN(同步请求)
|
||||
|
||||
拿到了地址,浏览器不能直接冲过去,万一店今天没开门呢?所以,要先进行一次**“电话确认”**(这叫建立 TCP 连接)。为了确保通话稳定可靠,会有三次非常严谨的“确认打招呼”机制,行业里叫**三次握手 (Three-way Handshake)**:
|
||||
**浏览器发送 SYN 包**
|
||||
|
||||
- **第一次握手 (浏览器)**:“喂,你好,我要来买东西,你在吗?” (SYN)
|
||||
- **第二次握手 (服务器)**:“我在的,欢迎光临!你也听得到我说话吗?” (SYN-ACK)
|
||||
- **第三次握手 (浏览器)**:“我也听到了!那我就要过来了!” (ACK)
|
||||
就像你拨通朋友电话后说的第一句话:"喂,你好,能听到我说话吗?"
|
||||
|
||||
经过这三次确认,双方都知道了彼此的听力和表达能力都没问题,一条稳定可靠的通信通道就正式建立了。
|
||||
- **SYN** 是 **Synchronize**(同步)的缩写
|
||||
- 浏览器生成一个随机数字(比如 `Seq = 100`),告诉服务器:"我要开始建立连接了,我的初始序号是 100"
|
||||
- 这个序号用来标记后续发送的数据顺序,防止乱序
|
||||
|
||||
**这一步确认了什么?** 服务器收到了浏览器的消息 → 浏览器的**发送通道**正常。
|
||||
|
||||
---
|
||||
|
||||
## 4. 购买商品 (HTTP 请求与响应)
|
||||
### 第二次握手:SYN-ACK(同步+确认)
|
||||
|
||||
通道建好后,业务正式开始。
|
||||
**服务器回复 SYN-ACK 包**
|
||||
|
||||
- **浏览器(买家)提交订单**:浏览器会打包一份极其规范的订单表格(**HTTP 请求报文**),里面写着:“老板,请给我拿一份你的主页 HTML 文件,我是用 Chrome 浏览器来访问的哦。”
|
||||
- **服务器(卖家)根据订单发货**:位于地球另一端的 Google 服务器收到请求后,立刻开始在仓库里配货,生成网页的 HTML 代码,然后打包成包裹(**HTTP 响应报文**),发回给你的浏览器。包裹外面还会贴个标签“200 OK”,意思是“交易成功,你要的货全齐了”。
|
||||
就像朋友回答:"喂喂,我能听到你!你也能听到我吗?"
|
||||
|
||||
- **SYN-ACK** = **Synchronize + Acknowledge**(同步+确认)
|
||||
- 服务器做两件事:
|
||||
1. **ACK**:确认收到浏览器的消息(`Ack = 101`,表示"我期待收到你序号为 101 的下一个包")
|
||||
2. **SYN**:服务器也生成自己的随机序号(比如 `Seq = 200`),告诉浏览器:"我的初始序号是 200"
|
||||
|
||||
**这一步确认了什么?** 浏览器收到了服务器的回复 → 服务器的**发送通道**正常,浏览器的**接收通道**正常。
|
||||
|
||||
---
|
||||
|
||||
## 5. 拆盒组装 (浏览器渲染)
|
||||
### 第三次握手:ACK(确认)
|
||||
|
||||
最后一步,货物送到了你的电脑。但发过来的只是一堆代码(HTML、CSS、JavaScript),这就好比你网购买了一箱乐高积木,还需要自己组装:
|
||||
**浏览器回复 ACK 包**
|
||||
|
||||
1. **看说明书 (解析 HTML)**:浏览器先把 HTML 代码解读出来,拼装成网页的骨架(DOM 树)。
|
||||
2. **涂抹颜色 (解析 CSS)**:然后检查 CSS 代码,看看字体要多大、按钮是什么颜色,给网页穿上漂亮的外衣(CSSOM 树)。
|
||||
3. **计算布局并拼装 (Layout & Paint)**:浏览器计算好每个元素在屏幕上的确切位置,用画笔把它们画在你的显示器上。
|
||||
4. **注入灵魂 (执行 JavaScript)**:最后,各种能点击、能滑动的交互效果都通过 JavaScript 激活。
|
||||
就像你回答:"能听到!那我们开始聊正事吧!"
|
||||
|
||||
**只要短短的几百毫秒,所有的步骤就已全部完成,你也就看到了那个熟悉的页面!**
|
||||
- **ACK** 是 **Acknowledge**(确认)的缩写
|
||||
- 浏览器回复:`Ack = 201`,表示"我期待收到你序号为 201 的下一个包"
|
||||
|
||||
**这一步确认了什么?** 服务器收到了浏览器的确认 → 服务器的**接收通道**也正常。
|
||||
|
||||
---
|
||||
|
||||
## 总结:从微观到宏观
|
||||
### 为什么必须是三次?两次行不行?
|
||||
|
||||
如果我们把目光再拉远一点,整个网络通讯的本质,就是在做**接力跑和翻译**:
|
||||
**假设只有两次握手:**
|
||||
|
||||
- 我们上面看到的这五步,大多是发生在你眼前的**应用程序**层面的事情。
|
||||
- 在肉眼看不见的底层,刚才那个充满代码的 HTML 包裹,会被切分成无数块极小的碎片(数据包)。这些碎片顺着你家墙上的网线、海底的万兆光缆,像接力棒一样在各种路由器之间传递。
|
||||
- 最终,这一切碎片完好无损地抵达,并在哪怕是几十个毫秒的时间里,化成你屏幕上的绚丽像素。
|
||||
1. 浏览器:"喂,能听到吗?"
|
||||
2. 服务器:"能听到!"
|
||||
|
||||
这就是计算机网络的神奇魅力!
|
||||
这时候服务器以为连接建立了,开始发送数据。但如果服务器的回复在半路丢了,浏览器根本没收到,浏览器就不会认为连接建立成功,也不会处理服务器发来的数据。
|
||||
|
||||
**结果**:服务器单方面认为连接已建立,疯狂发数据,但浏览器全当垃圾丢弃。服务器资源被白白浪费。
|
||||
|
||||
**三次握手的精妙之处**:
|
||||
|
||||
第三次握手的 ACK 包,**证明了浏览器确实收到了服务器的回复**。只有浏览器收到了,才会回复 ACK;服务器收到了这个 ACK,才能**100%确定**双方通道都是通的。
|
||||
|
||||
这就像打电话时的完整确认:
|
||||
- 你:"喂,能听到吗?"(SYN)
|
||||
- 朋友:"能听到,你呢?"(SYN-ACK)
|
||||
- 你:"我也能听到!"(ACK)
|
||||
|
||||
**这一步完成了什么?** 浏览器和服务器都确认了:**我能发给你,我能收到你的,你也能发给我,你也能收到我的**。一条可靠的 TCP 通道正式建立!
|
||||
|
||||
**现在可以开始了吗?** 通道已建立,下一步就是正式发送请求,获取网页内容。
|
||||
|
||||
---
|
||||
|
||||
## 第四步:寄包裹 (HTTP 请求与响应)
|
||||
|
||||
**上一步完成了**:通过 TCP 三次握手,建立了可靠的通信通道。
|
||||
|
||||
**这一步要实现**:正式发送请求,获取网页内容。
|
||||
|
||||
**目的**:浏览器向服务器"下单",服务器返回"货物"(网页内容)。
|
||||
|
||||
<HttpExchangeDemo />
|
||||
|
||||
**💡 核心原理解析:HTTP 请求与响应的小纸条**
|
||||
|
||||
浏览器会把你刚才写好的购物单,按照一种极为规范的格式打包(这叫 **HTTP 请求头**),正式塞进刚才建立好的 TCP 通道里,发给服务器。
|
||||
|
||||
- **买方发纸条(HTTP Request)**:
|
||||
浏览器发出的包裹里,写着大写的请求指令。如果是看网页就是 `GET`,如果是提交账号密码登录就是 `POST`。不仅如此,这张纸条里还附带了一些重要情报:"嗨,我是用 Mac 电脑的 Chrome 浏览器访问的哦,另外我只能听懂中文,请把给我的货也转换成中文。"(这些补充说明就被叫做 **请求 Headers**)。
|
||||
|
||||
- **卖方发纸条(HTTP Response)**:
|
||||
位于千里之外的服务器收到这包东西后,看了一眼:"哦,他要 `GET` 这个页面啊"。于是服务器飞速在自己的硬盘里找到相应的 HTML 网页代码打包好,在包裹最外面贴上一个标签:`200 OK`(意思是交易非常成功,你要的货全齐了),然后借由同一个通道,原路寄回给你的电脑。
|
||||
|
||||
> **小科普**:如果是找不到你要找得页面,服务器就会贴个 `404 Not Found` 的悲伤标签给你退回来。如果是服务器自己代码写错了挂掉了,就会贴个 `500 Server Error` 的崩溃标签。
|
||||
|
||||
**这一步完成了什么?** 浏览器收到了服务器返回的 HTML、CSS、JavaScript 代码(也就是网页的"原材料")。
|
||||
|
||||
**但问题来了**:这些代码只是文本,还不是你能看到的网页画面。下一步,浏览器需要把这些代码"翻译"成屏幕上的像素。
|
||||
|
||||
---
|
||||
|
||||
## 第五步:拆解组装 (浏览器渲染)
|
||||
|
||||
**上一步完成了**:通过 HTTP 请求,浏览器获取了网页的源代码(HTML、CSS、JavaScript)。
|
||||
|
||||
**这一步要实现**:把代码转换成屏幕上可见的网页画面。
|
||||
|
||||
**目的**:将文本代码"翻译"成像素,让用户看到最终的网页。
|
||||
|
||||
<BrowserRenderingDemo />
|
||||
|
||||
**💡 核心原理解析:毫秒级的画家**
|
||||
|
||||
此时你电脑收到的,仅仅是一大串干瘪枯燥的文本代码(HTML 骨架、CSS 色彩图纸、JS 交互动效代码)。这就像你网购了一箱子乐高,它给你的只有几千个塑料零件和一本极度复杂的说明书。
|
||||
|
||||
浏览器的组装过程堪比惊心动魄的全自动工厂流水线:
|
||||
|
||||
1. **搭骨架 (DOM 解析)**:工人先把 HTML 文件通读一遍,理清楚网页的结构。比如"这里要有一个标题框,那里要有三个图片框"。这个骨架叫做 DOM 树。
|
||||
2. **上颜色 (CSS 解析)**:紧接着看 CSS 文件,"哦,老王说标题框必须是红色的,图片框必须有圆角。"
|
||||
3. **几何计算排版 (Layout)**:结合骨架和颜色后,开始拿尺子计算。因为每个人的屏幕大小不一样,同样是三个图片框,在手机上只能竖着放,在电脑上可以横着放。必须计算出每一个像素块极其精确的摆放坐标。
|
||||
4. **上色绘制 (Paint)**:最后拿起了画笔,按照前面算出来的精确设计图,把真真切切的颜色和像素渲染到了你的显示器上!
|
||||
|
||||
**这一步完成了什么?** 浏览器把代码转换成了屏幕上的像素,用户终于看到了完整的网页!
|
||||
|
||||
---
|
||||
|
||||
## 完整流程回顾
|
||||
|
||||
让我们把整个过程串起来:
|
||||
|
||||
| 步骤 | 完成了什么 | 下一步需要什么 |
|
||||
|------|-----------|---------------|
|
||||
| **1. URL 解析** | 拆解网址,知道要去哪 | 需要把域名转成 IP |
|
||||
| **2. DNS 解析** | 拿到服务器 IP 地址 | 需要确认服务器在线 |
|
||||
| **3. TCP 握手** | 建立可靠通信通道 | 需要发送正式请求 |
|
||||
| **4. HTTP 交换** | 获取网页源代码 | 需要把代码转成画面 |
|
||||
| **5. 浏览器渲染** | 把代码渲染成像素 | ✅ 用户看到网页! |
|
||||
|
||||
---
|
||||
|
||||
## 结语:0.5 秒里发生了什么
|
||||
|
||||
敲下回车,等上半秒,页面就跳出来了——我们早就习惯了这个速度,甚至觉得慢。
|
||||
|
||||
但仔细想想,就在这眨眼的功夫里:
|
||||
|
||||
- **第一步**:浏览器把你输入的网址拆开看懂
|
||||
- **第二步**:跑去问了好多台服务器才要到 IP 地址
|
||||
- **第三步**:跟大洋彼岸的服务器来回确认了三次"能听见吗"
|
||||
- **第四步**:把请求打包发过去,再等着收回来
|
||||
- **第五步**:最后还要把成千上万行代码瞬间组装成你能看到的画面
|
||||
|
||||
这些步骤一环扣一环,**前一步的输出是后一步的输入**,中间哪个环节出问题,页面就打不开。而那些路由器、服务器、光缆,就默默在后台 24 小时运转,保证你每次滑动手机时,内容都能准时出现。
|
||||
|
||||
下次等网页加载的时候,或许可以想想:这 0.5 秒,其实挺忙的。
|
||||
|
||||
@@ -2,98 +2,90 @@
|
||||
|
||||
::: tip 🎯 核心问题
|
||||
**有了完美的 CPU 和无限的内存,电脑就能直接用了吗?**
|
||||
在上一章,我们见证了晶体管如何组合成强大的 CPU。但其实,如果直接使用这些冷冰冰的硬件,哪怕只是想在屏幕上打出一个字母,你都需要手写几百行晦涩的机器指令。
|
||||
在上一章,我们见证了晶体管如何组合成强大的 CPU。但即使你拥有最顶级的硬件,如果直接让它们工作,连在屏幕上显示一个字母都需要写几百行晦涩的机器指令。不仅麻烦,还极其危险——稍有差池,你的代码就可能把别人的数据覆盖掉。
|
||||
|
||||
为了不让大家在每次用电脑时都被逼疯,前辈们创造了一个夹在“硬件”和“你”之间的超级管家——**操作系统(Operating System, 简称 OS)**。本章我们不谈深奥的理论,只聊聊这个大管家是怎么通过三大“障眼法”,把复杂的硬件调教得服服帖帖的。
|
||||
为了解决这些噩梦,**操作系统(Operating System, 简称 OS)**诞生了。它是挡在你和冰冷硬件之间的一层最伟大的"软件"。本章我们将抛开深奥的代码,用通俗的比喻,看看这个"超级管家"是如何把杂乱无章的硬件调教得服服帖帖的。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 0. 承上启下:如果没有操作系统会怎样?
|
||||
## 0. 全景图:没有操作系统会怎样?
|
||||
|
||||
上一章我们提到,CPU 是一个不知疲倦的无情计算机器,通电后就会一行一行地执行指令。
|
||||
想象一下,你开了一家极具潜力的"计算工厂"(你的电脑),厂里有一个全能、不知疲倦的顶级干将(CPU),还有一片巨大的仓库(内存)和无数的集装箱(硬盘)。
|
||||
|
||||
但这带来了几个现实的灾难:
|
||||
1. **CPU 独占危机**:CPU 一次只能干一件事。如果你正在听歌,想切出去看个网页?抱歉,没有操作系统的调度,你的电脑必须停下音乐,才能去加载网页。
|
||||
2. **内存踩踏事故**:微信和游戏都在使用内存。如果没有保安管理,游戏一不小心把数据写到了微信的内存地盘,微信当场崩溃。
|
||||
3. **硬盘迷宫**:硬盘本质上只是一张密密麻麻刻满 0 和 1 的巨大光盘。要想找到你昨天存的照片,你必须准确记住它存放在第 12345 圈磁道的第 678 个扇区。
|
||||
如果你**不雇佣**一个厂长(操作系统)来管理:
|
||||
1. **CPU 独占危机**:CPU 一次只能干一件事。如果有人在用它听歌,其他任何人想看网页?抱歉,大家必须排队等听歌的人主动把 CPU 让出来。
|
||||
2. **内存踩踏事故**:微信和游戏都在使用仓库(内存)。如果没有保安规划区域,游戏一不小心把装备数据放到了微信的盒子里,微信直接当场崩溃。
|
||||
3. **硬盘迷宫**:硬盘硬件只是一张张刻满 0 和 1 的巨大光盘。要想找到昨天存的照片,你必须准确记住它存放在"第 1 盘面、第 56 磁道、第 8 扇区",没人能记住这种反人类的坐标。
|
||||
|
||||
为了解决这些噩梦,操作系统诞生了。它对外提供了一套优雅的“幻觉”,这就是它的三大核心魔法:**进程(管理 CPU)**、**虚拟内存(管理内存)** 和 **文件系统(管理硬盘)**。
|
||||
<OSArchitectureDemo />
|
||||
|
||||
为了解决上述的三大噩梦,操作系统祭出了它的三板斧:**进程管理**、**内存管理**和**文件系统**。
|
||||
|
||||
---
|
||||
|
||||
## 1. 进程管理:制造“同时运行”的幻觉
|
||||
## 1. 进程管理:CPU 的时分复用
|
||||
|
||||
你平时用电脑,常常是一边挂着微信,一边听着音乐,还能一边打字。但如果你买的电脑其实只有一个 CPU 核心,它是怎么同时做这三件事的?
|
||||
|
||||
答案是:**它并没有同时做**。是操作系统在进行疯狂的“时间管理”。
|
||||
答案是:**它并没有同时做。而是操作系统在进行疯狂的"时间管理"。**
|
||||
|
||||
<ProcessDemo />
|
||||
|
||||
::: tip 💡 核心原理解析:时间片轮转(Time Slicing)
|
||||
操作系统把 CPU 的时间切成了极其微小的片段(比如 10 毫秒)。
|
||||
- 第 1-10 毫秒:让 CPU 去执行**微信**的接收消息逻辑。
|
||||
- 第 11-20 毫秒:把微信强制暂停,让 CPU 去执行**音乐**的播放逻辑。
|
||||
- 第 21-30 毫秒:把音乐暂停,让 CPU 去响应你的**键盘打字**。
|
||||
### 1.1 什么是"进程"?
|
||||
每一个正在运行的程序,就被称为一个**进程**。你可以把它理解为一个"项目组",有自己的代码(做事清单)、自己的内存数据(项目资金),排着队等待 CPU 接见。
|
||||
|
||||
因为切换的速度实在太快了(一秒钟切换成百上千次),在人类迟钝的感知中,就觉得这三个软件是“同时”在运行的。
|
||||
|
||||
在操作系统的术语里,运行中的程序就被称为**进程(Process)**。操作系统就是这群进程的冷酷无情的排班经理。
|
||||
:::
|
||||
### 1.2 时间片轮转
|
||||
为了不让某个流氓软件一直霸占 CPU,操作系统把 CPU 的时间切成极小的片段(约 10 毫秒),轮流分配给各个进程。因为切换速度太快了,你感觉是"同时运行"。
|
||||
|
||||
---
|
||||
|
||||
## 2. 内存管理:给每个程序画个“海市蜃楼”
|
||||
## 2. 内存管理:虚拟地址空间
|
||||
|
||||
解决了 CPU 轮流用的问题,接下来是存放数据的内存。如果所有的进程都挤在同一块物理内存里,很容易发生互相干扰和偷看数据的危险。
|
||||
|
||||
操作系统的第二大魔法,叫作**虚拟内存(Virtual Memory)**。
|
||||
解决了 CPU 轮流用的问题,接下来是内存空间。如果不加管理,所有软件都直接往物理内存条写数据,必然会发生**互相覆盖**的踩踏惨剧。
|
||||
|
||||
<MemoryDemo />
|
||||
|
||||
::: tip 💡 核心原理解析:内存映射
|
||||
操作系统对每一个启动的进程撒了一个弥天大谎:“嘿,你独占了整整 4GB 的纯净内存空间,随便用!”(这就是**虚拟内存**)。
|
||||
### 2.1 虚拟内存(Virtual Memory)
|
||||
操作系统对每一个进程都撒了一个大谎:"嘿,你独占了整台电脑所有的可用内存,随便用!"
|
||||
|
||||
但实际上,当进程往这个“虚拟空间”里放东西时,操作系统的底层会拿出一个**映射表(页表)**,偷偷把数据塞进**真实物理内存(Physical Memory)**中各种零碎、不连续的角落里。
|
||||
在进程眼里,自己的内存条永远是**连续**且**干净**的。它心安理得地往里面写数据。
|
||||
|
||||
**这么做有两个巨大的好处:**
|
||||
1. **绝对安全**:微信永远只能看到自己的虚拟空间,它根本不知道音乐的数据在物理内存的哪个角落,自然就不会发生“踩踏”。
|
||||
2. **碎片利用**:物理内存就算被用得像狗皮膏药一样稀碎,映射给进程的虚拟空间依然是连续且整齐的。
|
||||
:::
|
||||
### 2.2 页表映射(Page Table)
|
||||
实际上呢?操作系统偷偷把数据塞进**真实物理内存**中各种零碎的缝隙里。这么做有两个绝顶天才的好处:
|
||||
1. **绝对安全**:微信永远只能看到自己的空间,没法篡改别人的数据
|
||||
2. **碎片利用**:不管物理内存多乱,映射给进程的虚拟空间依然是整齐的
|
||||
|
||||
---
|
||||
|
||||
## 3. 文件系统:把“荒地”变成“档案馆”
|
||||
## 3. 文件系统:持久化存储的组织
|
||||
|
||||
如果你买了一块崭新的硬盘,它里面其实是一片荒芜的存储单元。如果你想存一张照片,硬盘硬件只会问你:“请告诉我你要存在第几个字节地址?”这显然反人类。
|
||||
|
||||
操作系统的第三大魔法是**文件系统(File System)**,它为你构建了我们最熟悉的:文件夹(目录)和文件的概念。
|
||||
如果你买了一块崭新的硬盘,它里面其实是一片荒芜的存储单元。如果你想存一张照片,硬盘只会问你:"请告诉我你要存在第几个字节?"
|
||||
|
||||
<FilesystemDemo />
|
||||
|
||||
::: tip 💡 核心原理解析:从地址到路径
|
||||
文件系统本质上是一个超级大型的“翻译官”加“账本”:
|
||||
1. **账本功能**:它悄悄地把硬盘切分成无数个小块(Block),然后用一个账本记录下来“哪几个小块现在是空的可以存数据,哪几个小块已经存了东西”。
|
||||
2. **翻译功能**:当你双击一层层文件夹,打开 `D盘/照片/宠物.jpg` 时,并不是硬盘真的长出了树枝一样的结构。而是文件系统在它的账本里疯狂翻阅,最终翻译出:哦,这个路径其实对应的是硬盘上的第 1056、1057 和 998 块小地方,然后把数据取出来交给你。
|
||||
:::
|
||||
### 3.1 文件系统做了什么?
|
||||
1. **切割硬盘**:把硬盘切成无数个固定大小的**块**(通常是 4KB)
|
||||
2. **建立账本**:记录哪些块是满的,哪些是空的
|
||||
3. **翻译路径**:把 `D盘/照片/宠物.jpg` 翻译成"第 3、7、11 块"
|
||||
|
||||
这就是为什么你重命名文件瞬间就能完成(只改账本上的名字),而复制文件需要好久(要真实读写硬盘数据块)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 总结:伟大的幕后英雄
|
||||
## 4. 三者协同:程序启动的完整过程
|
||||
|
||||
让我们通过一个你每天都在经历的场景,串联起今天学到的知识。当你**双击鼠标打开一个游戏**时,为了伺候你,大管家做了什么?
|
||||
我们已经分别了解了操作系统的三大模块,下面看看当你**双击打开一个程序**时,它们是如何协同工作的:
|
||||
|
||||
1. **文件系统**:立刻从底层硬盘的杂乱数据块中,拼凑出游戏的执行文件和美术资产。
|
||||
2. **内存管理**:为你分配一个巨大的虚拟内存空间,制造出“这台电脑只有这一个游戏”的幻觉,并把刚才找到的文件放进物理内存的空隙里。
|
||||
3. **进程管理**:在它的名册上新建一个“游戏进程”,并在下一个瞬间,立刻剥夺其他正在运行软件的 CPU 权利,把 CPU 的计算力全盘移交给你的游戏。
|
||||
<ProgramLaunchDemo />
|
||||
|
||||
我们之所以能那么轻松、优雅地在数字世界里冲浪,全都是因为底层的操作系统在替我们负重前行。
|
||||
无论是你点击桌面图标,还是代码中的一句 `print("Hello World")`,都离不开这一套复杂的暗箱操作。我们之所以能那么轻松地在数字世界里冲浪,全都是因为底层的操作系统在替我们负重前行。
|
||||
|
||||
---
|
||||
|
||||
## 延伸阅读
|
||||
|
||||
如果你觉得操作系统的各种“管理学”十分有趣,你可以看看这些进阶话题:
|
||||
- **进程与线程的区别**:除了进程,还有一种叫作“线程”的东西,它们是干什么用的?(为什么 Google Chrome 那么吃内存?)
|
||||
- **页面置换算法**:当物理内存全都塞满了,但你又打开了一个新软件,操作系统该把谁的数据临时踢到硬盘里?(LRU 算法)
|
||||
- **操作系统的多态**:Windows 和 macOS 会在底层实现上有什么不同?为什么有些软件只能在特定系统上运行?
|
||||
如果你觉得操作系统的各种"管理学和骗术"十分有趣,你可以看看这些进阶话题:
|
||||
- **进程与线程**:如果进程是项目组,那"线程"就是组里干活的员工
|
||||
- **并发与锁**:当两个进程同时竞争同一个资源时,如何防止死锁
|
||||
- **系统调用**:操作系统给上层应用提供的"服务窗口"
|
||||
|
||||
@@ -112,23 +112,20 @@
|
||||
|
||||
如果刚才介绍的逻辑门只能做简单的条件判断,那计算机到底是如何做数学运算的呢?
|
||||
|
||||
我们先回想一下手算加法的方式:对应位相加,如果超出了限制(十进制是满十进一,二进制是满二进一),就向更高位“进位”。
|
||||
|
||||
在二进制中,只有 0 和 1。对于一位数的加法,可能的情况只有四种:
|
||||
- `0 + 0 = 0` (本位是 0,不进位)
|
||||
- `0 + 1 = 1` (本位是 1,不进位)
|
||||
- `1 + 0 = 1` (本位是 1,不进位)
|
||||
- `1 + 1 = 10` (本位是 0,进位 1)
|
||||
<BinaryAdditionRulesDemo />
|
||||
|
||||
仔细观察这四种情况,你会发现:
|
||||
1. **本位的结果**,只有在两个输入**不同**时才为 1,这正是 **XOR 门(异或门)** 的逻辑。
|
||||
2. **进位的结果**,只有在两个输入**都为 1** 时才为 1,这正是 **AND 门(与门)** 的逻辑。
|
||||
|
||||
因此,只要把一个 XOR 门和一个 AND 门组合起来,我们就得到了能计算一位数加法的电路,这也是最基础的**半加器(Half Adder)**。
|
||||
因此,只要把一个 XOR 门(负责算本位)和一个 AND 门(负责算进位)组合起来,我们就得到了能计算一位数加法的电路,这也是最基础的**半加器(Half Adder)**。
|
||||
|
||||
<HalfAdderDemo />
|
||||
|
||||
但半加器有个致命缺陷:它无法处理来自低位的进位。在多位加法中,中间的每一位不仅要加 A 和 B,还要加上低位传来的进位。这就需要**全加器(Full Adder)**:
|
||||
但半加器有个致命缺陷:它在物理结构上**只有两个输入端口(A 和 B)**。
|
||||
|
||||
想象我们在做十进制竖式加法(比如 `19 + 22`):
|
||||
- **算个位**:`9 + 2 = 11`。只需两个数相加,写 `1` 进 `1`。这刚好是两个输入,半加器能完美胜任。
|
||||
- **算十位**:不仅要算 `1 + 2`,还要**加上刚才个位传过来的“进位 1”**(即 `1 + 2 + 1 = 4`)。这意味着在多位加法中,除了最低位,其他位实际上是在做**三个数字**的相加!
|
||||
|
||||
因为半加器没有接纳“低位传来的进位(Carry-in)”的第三个输入口,所以除了最右边的那一位,它在别的位全都没法用。为了解决这个问题,我们需要能接收三个信号的**全加器(Full Adder)**:
|
||||
|
||||
<FullAdderDemo />
|
||||
|
||||
@@ -185,6 +182,10 @@
|
||||
当我们将 32 个抑或 64 个这种触发器整齐地编排成一列,施加同一种强劲的时钟频率信号(Clock)来号令它们统一行动时,**寄存器(Register)**便应运而生了。它身居 CPU 系统的心脏位置,被当做极速的“工作草稿纸”,默默捍卫着你每一个即时的关键变量。
|
||||
:::
|
||||
|
||||
请通过下面的互动演示,亲自体验这个打破和恢复闭环的过程:
|
||||
|
||||
<FlipFlopDemo />
|
||||
|
||||
---
|
||||
|
||||
## 4. CPU 架构:从功能单元到处理器
|
||||
|
||||
@@ -66,6 +66,18 @@ result = response.choices[0].message.content
|
||||
|
||||
<ApiTypesComparison />
|
||||
|
||||
### 1.3 函数 API vs HTTP API 的区别
|
||||
|
||||
很多初学者会困惑:函数 API 和 HTTP API 到底有什么区别?看文档时该如何区分?
|
||||
|
||||
<ApiFunctionVsHttp />
|
||||
|
||||
### 1.4 不同类型的 API 文档怎么看
|
||||
|
||||
面对不同类型的 API 文档,关注重点各不相同:
|
||||
|
||||
<DocumentTypesComparison />
|
||||
|
||||
---
|
||||
|
||||
## 2. 一次完整的 API 调用
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,652 +0,0 @@
|
||||
# SQL:与数据库对话的语言
|
||||
|
||||
::: tip 核心问题
|
||||
**如何高效地查询和操作数据?** 这就像问:图书馆的书怎么快速找到?仓库的货物怎么精准定位?银行的账目怎么安全转账?SQL 解决的就是"与数据对话"的问题。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 0. SQL 的核心价值
|
||||
|
||||
在现代软件开发中,数据是核心资产。无论是电商平台的商品信息、社交网络的用户关系,还是银行系统的交易记录,都需要一种高效的方式来管理和查询。
|
||||
|
||||
**SQL**(Structured Query Language,结构化查询语言)就是这样一种"与数据库对话"的语言。它让我们能够:
|
||||
|
||||
- **精准查询**:从百万级数据中快速找到目标
|
||||
- **高效操作**:批量增删改,一条语句搞定
|
||||
- **安全保障**:事务机制保证数据一致性
|
||||
- **标准通用**:学一次,所有数据库都能用
|
||||
|
||||
---
|
||||
|
||||
## 1. SQL vs NoSQL:如何选择?
|
||||
|
||||
在深入了解 SQL 之前,先了解一下它与 NoSQL 的区别。
|
||||
|
||||
### 1.1 用仓库来类比
|
||||
|
||||
| 特性 | SQL(关系型数据库) | NoSQL(非关系型数据库) |
|
||||
| :--- | :--- | :--- |
|
||||
| **数据结构** | 严格的表结构(像 Excel) | 灵活的文档/键值/图结构 |
|
||||
| **典型代表** | MySQL、PostgreSQL、Oracle | MongoDB、Redis、Elasticsearch |
|
||||
| **适用场景** | 金融系统、电商订单、用户管理 | 社交动态、日志分析、实时缓存 |
|
||||
| **优势** | 数据一致性、事务支持(ACID) | 高并发、灵活扩展、高性能 |
|
||||
| **劣势** | 扩展性差、schema 固定 | 数据一致性弱、查询功能有限 |
|
||||
|
||||
### 1.2 一个直观的对比
|
||||
|
||||
**SQL 数据库**就像一个**规范化的仓库**:
|
||||
- 每个货架有固定的编号、名称、容量
|
||||
- 货物必须按照规则摆放
|
||||
- 入库、出库有严格的流程和记录
|
||||
- 适合需要严格管理的场景
|
||||
|
||||
**NoSQL 数据库**就像一个**灵活的杂物间**:
|
||||
- 想放哪里就放哪里
|
||||
- 不需要预先规划空间
|
||||
- 快速存取,但可能找不到东西
|
||||
- 适合需要快速迭代的场景
|
||||
|
||||
::: tip 💡 实际应用
|
||||
大多数企业会**同时使用 SQL 和 NoSQL**:
|
||||
- MySQL 存储用户信息、订单数据(核心业务)
|
||||
- Redis 缓存热点数据(提高性能)
|
||||
- MongoDB 存储日志、用户行为(数据分析)
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 2. CRUD 操作:数据的增删改查
|
||||
|
||||
SQL 的核心操作就是 CRUD(Create, Read, Update, Delete)。
|
||||
|
||||
### 2.1 用 Excel 来类比
|
||||
|
||||
| Excel 操作 | SQL 关键字 | 说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| 插入新行 | INSERT | 添加数据 |
|
||||
| 筛选行 | SELECT | 查询数据 |
|
||||
| 修改单元格 | UPDATE | 更新数据 |
|
||||
| 删除行 | DELETE | 删除数据 |
|
||||
|
||||
### 2.2 实战演示
|
||||
|
||||
👇 **动手试试看**:在下方交互式演示中体验 CRUD 操作:
|
||||
|
||||
<SqlDemo />
|
||||
|
||||
### 2.3 常用查询语法
|
||||
|
||||
#### **SELECT:查询数据**
|
||||
|
||||
```sql
|
||||
-- 查询所有列
|
||||
SELECT * FROM users;
|
||||
|
||||
-- 查询指定列
|
||||
SELECT name, email FROM users;
|
||||
|
||||
-- 带条件查询
|
||||
SELECT * FROM users WHERE age > 18;
|
||||
|
||||
-- 排序
|
||||
SELECT * FROM users ORDER BY age DESC;
|
||||
|
||||
-- 限制结果数量
|
||||
SELECT * FROM users LIMIT 10;
|
||||
```
|
||||
|
||||
#### **INSERT:插入数据**
|
||||
|
||||
```sql
|
||||
-- 插入完整数据
|
||||
INSERT INTO users (name, email, age)
|
||||
VALUES ('张三', 'zhangsan@example.com', 25);
|
||||
|
||||
-- 批量插入
|
||||
INSERT INTO users (name, email, age) VALUES
|
||||
('李四', 'lisi@example.com', 30),
|
||||
('王五', 'wangwu@example.com', 28);
|
||||
```
|
||||
|
||||
#### **UPDATE:更新数据**
|
||||
|
||||
```sql
|
||||
-- 更新单个字段
|
||||
UPDATE users SET age = 26 WHERE id = 1;
|
||||
|
||||
-- 更新多个字段
|
||||
UPDATE users
|
||||
SET age = 27, email = 'newemail@example.com'
|
||||
WHERE id = 1;
|
||||
|
||||
-- ⚠️ 危险操作:不带 WHERE 会更新所有行!
|
||||
UPDATE users SET age = 0; -- 慎用!
|
||||
```
|
||||
|
||||
#### **DELETE:删除数据**
|
||||
|
||||
```sql
|
||||
-- 删除指定行
|
||||
DELETE FROM users WHERE id = 1;
|
||||
|
||||
-- ⚠️ 危险操作:不带 WHERE 会删除所有数据!
|
||||
DELETE FROM users; -- 慎用!
|
||||
```
|
||||
|
||||
::: warning 💡 最佳实践
|
||||
- 先用 `SELECT` 验证 WHERE 条件是否正确
|
||||
- 再用 `UPDATE/DELETE` 执行操作
|
||||
- 生产环境务必加 `LIMIT` 限制影响行数
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 3. SELECT 进阶:JOIN、GROUP BY、子查询
|
||||
|
||||
当数据分布在多个表中时,我们需要更强大的查询能力。
|
||||
|
||||
### 3.1 JOIN:连接多个表
|
||||
|
||||
**场景**:一个电商系统有两个表:
|
||||
- `users`(用户表):id, name, email
|
||||
- `orders`(订单表):order_id, user_id, amount
|
||||
|
||||
如何查询"每个用户的订单总金额"?
|
||||
|
||||
#### **INNER JOIN:只返回匹配的行**
|
||||
|
||||
```sql
|
||||
SELECT users.name, SUM(orders.amount) as total
|
||||
FROM users
|
||||
INNER JOIN orders ON users.id = orders.user_id
|
||||
GROUP BY users.id;
|
||||
```
|
||||
|
||||
**结果**:只显示有订单的用户
|
||||
|
||||
#### **LEFT JOIN:返回左表所有行**
|
||||
|
||||
```sql
|
||||
SELECT users.name, SUM(orders.amount) as total
|
||||
FROM users
|
||||
LEFT JOIN orders ON users.id = orders.user_id
|
||||
GROUP BY users.id;
|
||||
```
|
||||
|
||||
**结果**:显示所有用户,没有订单的用户 total 为 NULL
|
||||
|
||||
::: tip 💡 如何选择 JOIN?
|
||||
- **INNER JOIN**:只要两边都有数据才需要(如:订单明细)
|
||||
- **LEFT JOIN**:需要保留主表所有数据(如:用户列表 + 统计信息)
|
||||
- **RIGHT JOIN**:需要保留从表所有数据(很少用)
|
||||
- **FULL OUTER JOIN**:需要所有数据(MySQL 不支持,可用 UNION 实现)
|
||||
:::
|
||||
|
||||
### 3.2 GROUP BY:分组统计
|
||||
|
||||
**场景**:统计每个部门的平均工资。
|
||||
|
||||
```sql
|
||||
SELECT department, AVG(salary) as avg_salary, COUNT(*) as count
|
||||
FROM employees
|
||||
GROUP BY department
|
||||
HAVING AVG(salary) > 10000; -- HAVING 过滤分组后的结果
|
||||
```
|
||||
|
||||
**注意**:
|
||||
- `WHERE` 过滤行(在 GROUP BY 之前)
|
||||
- `HAVING` 过滤分组(在 GROUP BY 之后)
|
||||
|
||||
### 3.3 子查询:查询嵌套查询
|
||||
|
||||
**场景**:查找工资高于平均工资的员工。
|
||||
|
||||
```sql
|
||||
-- 方式一:WHERE 子查询
|
||||
SELECT name, salary
|
||||
FROM employees
|
||||
WHERE salary > (SELECT AVG(salary) FROM employees);
|
||||
|
||||
-- 方式二:FROM 子查询(派生表)
|
||||
SELECT dept_name, avg_salary
|
||||
FROM (
|
||||
SELECT department, AVG(salary) as avg_salary
|
||||
FROM employees
|
||||
GROUP BY department
|
||||
) as dept_avg
|
||||
WHERE avg_salary > 10000;
|
||||
```
|
||||
|
||||
::: tip 💡 子查询 vs JOIN
|
||||
- **子查询**:逻辑清晰,但性能较差(每个子查询都会执行一次)
|
||||
- **JOIN**:性能更好,但需要理解连接逻辑
|
||||
- **最佳实践**:优先使用 JOIN,必要时用子查询
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 4. 索引原理:让查询快起来
|
||||
|
||||
### 4.1 为什么需要索引?
|
||||
|
||||
**场景**:在一个 100 万行的用户表中,查找 `id = 123456` 的用户。
|
||||
|
||||
**没有索引**:
|
||||
- 数据库需要逐行扫描,最多比较 100 万次
|
||||
- 时间复杂度:O(n)
|
||||
|
||||
**有索引**:
|
||||
- 数据库通过 B+ 树快速定位,只需比较 log₂(100万) ≈ 20 次
|
||||
- 时间复杂度:O(log n)
|
||||
|
||||
### 4.2 用图书馆来类比
|
||||
|
||||
| 概念 | 图书馆 | 数据库 |
|
||||
| :--- | :--- | :--- |
|
||||
| **数据** | 书籍 | 表的行 |
|
||||
| **索引** | 目录卡片 | B+ 树 |
|
||||
| **查询** | 按书名找书 | 按 WHERE 条件找行 |
|
||||
| **无索引** | 逐排书架找 | 全表扫描 |
|
||||
| **有索引** | 查目录定位 | 索引查找 |
|
||||
|
||||
### 4.3 索引的可视化演示
|
||||
|
||||
👇 **动手试试看**:在 SqlDemo 组件的"索引"标签页查看无索引 vs 有索引的对比:
|
||||
|
||||
<SqlDemo />
|
||||
|
||||
### 4.4 索引的使用建议
|
||||
|
||||
| 场景 | 是否建索引 | 说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| **WHERE 条件** | 是 | 如 `WHERE user_id = 1` |
|
||||
| **JOIN 连接** | 是 | 如 `JOIN ON user_id` |
|
||||
| **ORDER BY 排序** | 是 | 如 `ORDER BY created_at` |
|
||||
| **低选择性列** | 否 | 如性别(只有男/女) |
|
||||
| **频繁更新的列** | 谨慎 | 索引会降低写入性能 |
|
||||
| **小表** | 否 | 数据量小不需要索引 |
|
||||
|
||||
**创建索引**:
|
||||
```sql
|
||||
-- 单列索引
|
||||
CREATE INDEX idx_user_id ON orders(user_id);
|
||||
|
||||
-- 复合索引(最左前缀原则)
|
||||
CREATE INDEX idx_user_status ON orders(user_id, status);
|
||||
|
||||
-- 唯一索引
|
||||
CREATE UNIQUE INDEX idx_email ON users(email);
|
||||
```
|
||||
|
||||
::: tip 💡 索引的代价
|
||||
- **空间**:每个索引都是额外的存储空间
|
||||
- **时间**:INSERT/UPDATE/DELETE 需要更新索引,降低写入速度
|
||||
- **建议**:只在查询频繁、更新少的列上建索引
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 5. 事务 ACID:保证数据一致性
|
||||
|
||||
### 5.1 什么是事务?
|
||||
|
||||
**事务**(Transaction)是一组 SQL 操作,要么全部成功,要么全部失败。
|
||||
|
||||
**经典案例**:银行转账
|
||||
|
||||
```sql
|
||||
BEGIN; -- 开始事务
|
||||
|
||||
-- 账户 A 扣款 100 元
|
||||
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
|
||||
|
||||
-- 账户 B 加款 100 元
|
||||
UPDATE accounts SET balance = balance + 100 WHERE user_id = 2;
|
||||
|
||||
COMMIT; -- 提交事务(如果中间出错,自动 ROLLBACK)
|
||||
```
|
||||
|
||||
如果第二步失败(比如账户 B 不存在),整个事务会回滚,账户 A 不会被扣款。
|
||||
|
||||
### 5.2 ACID 四大特性
|
||||
|
||||
👇 **动手试试看**:在 SqlDemo 组件的"事务"标签页查看 ACID 可视化:
|
||||
|
||||
<SqlDemo />
|
||||
|
||||
#### **A - Atomicity(原子性)**
|
||||
- **含义**:事务中的操作要么全部成功,要么全部失败
|
||||
- **类比**:转账要么同时成功,要么同时失败,不会出现"扣款了但没到账"的情况
|
||||
- **实现**:Undo Log(回滚日志)
|
||||
|
||||
#### **C - Consistency(一致性)**
|
||||
- **含义**:事务前后数据库状态一致,满足所有约束
|
||||
- **类比**:转账前后总金额不变(A 余额 + B 余额 = 总金额)
|
||||
- **实现**:应用层约束 + 数据库约束
|
||||
|
||||
#### **I - Isolation(隔离性)**
|
||||
- **含义**:并发事务之间互不干扰
|
||||
- **类比**:两个用户同时转账,不会相互影响
|
||||
- **实现**:锁机制 + MVCC(多版本并发控制)
|
||||
|
||||
#### **D - Durability(持久性)**
|
||||
- **含义**:事务提交后,永久保存,即使系统故障
|
||||
- **类比**:转账成功后,断电也不会丢失记录
|
||||
- **实现**:Redo Log(重做日志)
|
||||
|
||||
### 5.3 事务隔离级别
|
||||
|
||||
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能 | 适用场景 |
|
||||
| :--- | :--- | :--- | :--- | :--- | :--- |
|
||||
| **READ UNCOMMITTED** | 是 | 是 | 是 | 高 | 几乎不用 |
|
||||
| **READ COMMITTED** | 否 | 是 | 是 | 中 | 大多数数据库默认 |
|
||||
| **REPEATABLE READ** | 否 | 否 | 是 | 低 | MySQL 默认 |
|
||||
| **SERIALIZABLE** | 否 | 否 | 否 | 最低 | 金融级要求 |
|
||||
|
||||
**设置隔离级别**:
|
||||
```sql
|
||||
-- 查看
|
||||
SELECT @@transaction_isolation;
|
||||
|
||||
-- 设置
|
||||
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
|
||||
```
|
||||
|
||||
::: tip 💡 如何选择隔离级别?
|
||||
- **默认使用 READ COMMITTED**:避免脏读,性能可接受
|
||||
- **金融场景**:使用 SERIALIZABLE 或 REPEATABLE READ
|
||||
- **分析场景**:可降低到 READ UNCOMMITTED 提高性能
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 6. SQL 注入:安全的警惕性
|
||||
|
||||
### 6.1 什么是 SQL 注入?
|
||||
|
||||
**SQL 注入**是一种常见的安全漏洞,攻击者通过构造恶意的输入,篡改 SQL 语句。
|
||||
|
||||
**示例**:一个登录接口
|
||||
|
||||
```sql
|
||||
-- 正常 SQL
|
||||
SELECT * FROM users WHERE username = 'admin' AND password = '123456';
|
||||
|
||||
-- 攻击者输入用户名:admin' --
|
||||
-- 拼接后的 SQL
|
||||
SELECT * FROM users WHERE username = 'admin' --' AND password = '123456';
|
||||
-- ↑ 注释掉后面的密码验证,直接登录成功!
|
||||
```
|
||||
|
||||
**更危险的攻击**:
|
||||
|
||||
```sql
|
||||
-- 用户名输入:admin'; DROP TABLE users; --
|
||||
-- 拼接后的 SQL
|
||||
SELECT * FROM users WHERE username = 'admin'; DROP TABLE users; --'
|
||||
```
|
||||
|
||||
### 6.2 如何防御?
|
||||
|
||||
#### **方法一:参数化查询(推荐)**
|
||||
|
||||
```python
|
||||
# ❌ 错误:直接拼接字符串(危险!)
|
||||
sql = f"SELECT * FROM users WHERE username = '{username}'"
|
||||
cursor.execute(sql)
|
||||
|
||||
# ✅ 正确:使用参数化查询(安全)
|
||||
sql = "SELECT * FROM users WHERE username = %s"
|
||||
cursor.execute(sql, (username,))
|
||||
```
|
||||
|
||||
#### **方法二:ORM 框架**
|
||||
|
||||
```python
|
||||
# Django ORM
|
||||
user = User.objects.get(username=username)
|
||||
|
||||
# SQLAlchemy
|
||||
user = session.query(User).filter(User.username == username).first()
|
||||
```
|
||||
|
||||
#### **方法三:输入验证**
|
||||
|
||||
```python
|
||||
# 限制用户名只能包含字母、数字、下划线
|
||||
import re
|
||||
if not re.match(r'^\w+$', username):
|
||||
raise ValueError('Invalid username')
|
||||
```
|
||||
|
||||
::: warning 💡 防御 SQL 注入的黄金法则
|
||||
1. **永远不要相信用户输入**
|
||||
2. **永远使用参数化查询或 ORM**
|
||||
3. **永远不要拼接 SQL 字符串**
|
||||
4. **最小权限原则**:数据库用户只给必要权限
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 7. 最佳实践
|
||||
|
||||
### 7.1 查询优化
|
||||
|
||||
| 优化技巧 | 说明 | 示例 |
|
||||
| :--- | :--- | :--- |
|
||||
| **避免 SELECT \*** | 只查询需要的列 | `SELECT name, email FROM users` |
|
||||
| **使用 LIMIT** | 限制结果数量 | `SELECT * FROM users LIMIT 10` |
|
||||
| **索引覆盖** | 查询条件使用索引列 | `WHERE indexed_col = 1` |
|
||||
| **避免子查询** | 用 JOIN 替代子查询 | 见上文对比 |
|
||||
| **批量操作** | 减少数据库往返 | `INSERT INTO ... VALUES (...), (...), (...)` |
|
||||
| **分页查询** | 大数据量分页 | `SELECT * FROM users LIMIT 10 OFFSET 20` |
|
||||
|
||||
### 7.2 命名规范
|
||||
|
||||
| 类型 | 规范 | 示例 |
|
||||
| :--- | :--- | :--- |
|
||||
| **表名** | 小写 + 下划线 | `user_profiles`, `order_items` |
|
||||
| **列名** | 小写 + 下划线 | `created_at`, `user_id` |
|
||||
| **索引名** | `idx_表名_列名` | `idx_users_email` |
|
||||
| **外键名** | `fk_表名_列名` | `fk_orders_user_id` |
|
||||
| **主键名** | 统一使用 `id` | 无 |
|
||||
|
||||
### 7.3 数据库设计
|
||||
|
||||
| 设计原则 | 说明 | 示例 |
|
||||
| :--- | :--- | :--- |
|
||||
| **规范化** | 消除数据冗余 | 第三范式(3NF) |
|
||||
| **反规范化** | 适当冗余提高性能 | 在订单表冗余用户姓名 |
|
||||
| **主键选择** | 优先使用自增 ID | `id BIGINT AUTO_INCREMENT` |
|
||||
| **时间字段** | 统一使用 DATETIME | `created_at DATETIME` |
|
||||
| **软删除** | 用 `is_deleted` 标记 | 不真删除,便于恢复 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 用 AI 辅助编写 SQL
|
||||
|
||||
AI 可以帮助你快速编写复杂的 SQL 查询。关键在于提供清晰的表结构和业务需求。
|
||||
|
||||
### 8.1 提示词模板
|
||||
|
||||
```
|
||||
你是一位资深的数据库工程师,精通 SQL 查询优化。请帮我编写 SQL 查询。
|
||||
|
||||
## 数据库表结构
|
||||
[提供表的 CREATE TABLE 语句或字段说明]
|
||||
|
||||
## 业务需求
|
||||
[描述你想要查询的数据,例如:
|
||||
- 统计每个月的订单总金额
|
||||
- 查找购买过商品 A 和商品 B 的用户
|
||||
- 计算用户的留存率]
|
||||
|
||||
## 要求
|
||||
1. 使用标准 SQL 语法(兼容 MySQL 8.0)
|
||||
2. 注释关键逻辑
|
||||
3. 考虑性能优化(索引、JOIN 顺序)
|
||||
4. 提供多种实现方案(如子查询 vs JOIN)
|
||||
|
||||
## 输出格式
|
||||
### SQL 查询
|
||||
\`\`\`sql
|
||||
[SQL 语句]
|
||||
\`\`\`
|
||||
|
||||
### 逻辑说明
|
||||
[解释查询的逻辑和优化思路]
|
||||
```
|
||||
|
||||
### 8.2 实战示例:复杂统计查询
|
||||
|
||||
**输入提示词**:
|
||||
|
||||
```
|
||||
你是一位资深的数据库工程师,精通 SQL 查询优化。请帮我编写 SQL 查询。
|
||||
|
||||
## 数据库表结构
|
||||
|
||||
```sql
|
||||
-- 用户表
|
||||
CREATE TABLE users (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
name VARCHAR(50),
|
||||
created_at DATETIME
|
||||
);
|
||||
|
||||
-- 订单表
|
||||
CREATE TABLE orders (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
user_id BIGINT,
|
||||
amount DECIMAL(10, 2),
|
||||
status ENUM('pending', 'paid', 'shipped', 'completed', 'cancelled'),
|
||||
created_at DATETIME,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
-- 订单详情表
|
||||
CREATE TABLE order_items (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
order_id BIGINT,
|
||||
product_id BIGINT,
|
||||
quantity INT,
|
||||
price DECIMAL(10, 2),
|
||||
FOREIGN KEY (order_id) REFERENCES orders(id)
|
||||
);
|
||||
|
||||
-- 商品表
|
||||
CREATE TABLE products (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
name VARCHAR(100),
|
||||
category VARCHAR(50)
|
||||
);
|
||||
```
|
||||
|
||||
## 业务需求
|
||||
1. 统计每个月的订单总金额和订单数量
|
||||
2. 查找购买过商品 A 和商品 B 的用户
|
||||
3. 计算用户复购率(有多少用户下了多个订单)
|
||||
|
||||
## 要求
|
||||
1. 使用标准 SQL 语法(兼容 MySQL 8.0)
|
||||
2. 注释关键逻辑
|
||||
3. 考虑性能优化
|
||||
```
|
||||
|
||||
**AI 输出示例**:
|
||||
|
||||
```sql
|
||||
-- 需求 1:统计每个月的订单总金额和订单数量
|
||||
SELECT
|
||||
DATE_FORMAT(created_at, '%Y-%m') AS month,
|
||||
COUNT(*) AS order_count,
|
||||
SUM(amount) AS total_amount
|
||||
FROM orders
|
||||
WHERE status IN ('paid', 'shipped', 'completed') -- 只统计有效订单
|
||||
GROUP BY DATE_FORMAT(created_at, '%Y-%m')
|
||||
ORDER BY month DESC;
|
||||
|
||||
-- 需求 2:查找购买过商品 A 和商品 B 的用户
|
||||
-- 方案一:INTERSECT(MySQL 不支持,用 IN 替代)
|
||||
SELECT DISTINCT user_id
|
||||
FROM orders o
|
||||
JOIN order_items oi ON o.id = oi.order_id
|
||||
JOIN products p ON oi.product_id = p.id
|
||||
WHERE p.name IN ('商品 A', '商品 B')
|
||||
GROUP BY user_id
|
||||
HAVING COUNT(DISTINCT p.name) = 2; -- 确保同时买了两个商品
|
||||
|
||||
-- 方案二:自连接(性能更好)
|
||||
SELECT
|
||||
oa.user_id
|
||||
FROM orders oa
|
||||
JOIN order_items oia ON oa.id = oia.order_id
|
||||
JOIN products pa ON oia.product_id = pa.id
|
||||
JOIN order_items oib ON oa.id = oib.order_id -- 同一个订单
|
||||
JOIN products pb ON oib.product_id = pb.id
|
||||
WHERE pa.name = '商品 A' AND pb.name = '商品 B';
|
||||
|
||||
-- 需求 3:计算用户复购率
|
||||
WITH user_order_count AS (
|
||||
SELECT
|
||||
user_id,
|
||||
COUNT(*) AS order_count
|
||||
FROM orders
|
||||
WHERE status IN ('paid', 'shipped', 'completed')
|
||||
GROUP BY user_id
|
||||
)
|
||||
SELECT
|
||||
COUNT(CASE WHEN order_count >= 2 THEN 1 END) AS repeat_users,
|
||||
COUNT(*) AS total_users,
|
||||
ROUND(COUNT(CASE WHEN order_count >= 2 THEN 1 END) * 100.0 / COUNT(*), 2) AS repeat_rate
|
||||
FROM user_order_count;
|
||||
```
|
||||
|
||||
### 8.3 AI 辅助的注意事项
|
||||
|
||||
| 注意点 | 说明 |
|
||||
| :--- | :--- |
|
||||
| **提供完整上下文** | 表结构、索引、数据量都要说清楚 |
|
||||
| **明确性能要求** | 是否需要优化、数据量大小 |
|
||||
| **验证 SQL 语法** | AI 生成的 SQL 可能有小错误,需要测试 |
|
||||
| **理解执行计划** | 用 `EXPLAIN` 查看查询是否使用了索引 |
|
||||
| **分步实现** | 复杂查询可以拆分成多个简单查询 |
|
||||
|
||||
::: tip 💡 追问技巧
|
||||
- "请提供另一种实现方案(如用 JOIN 替代子查询)"
|
||||
- "请分析这条查询的性能瓶颈"
|
||||
- "请添加索引建议"
|
||||
- "请解释每个步骤的逻辑"
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 名词速查表
|
||||
|
||||
| 名词 | 英文 | 解释 |
|
||||
| :--- | :--- | :--- |
|
||||
| **SQL** | Structured Query Language | 结构化查询语言,与数据库对话的标准语言 |
|
||||
| **数据库** | Database | 存储和管理数据的仓库 |
|
||||
| **表** | Table | 数据的二维表格,类似 Excel |
|
||||
| **行** | Row | 表中的一条记录 |
|
||||
| **列** | Column | 表中的一个字段 |
|
||||
| **主键** | Primary Key | 唯一标识一行的字段(如 id) |
|
||||
| **外键** | Foreign Key | 关联其他表的字段 |
|
||||
| **索引** | Index | 加速查询的数据结构(B+ 树) |
|
||||
| **事务** | Transaction | 一组要么全成功、要么全失败的 SQL 操作 |
|
||||
| **ACID** | Atomicity, Consistency, Isolation, Durability | 事务的四大特性 |
|
||||
| **JOIN** | Join | 连接多个表的查询操作 |
|
||||
| **子查询** | Subquery | 嵌套在另一个查询中的查询 |
|
||||
| **聚合函数** | Aggregate Function | SUM, AVG, COUNT, MAX, MIN |
|
||||
| **分组** | Group By | 按字段分组统计 |
|
||||
| **SQL 注入** | SQL Injection | 通过输入篡改 SQL 语句的攻击方式 |
|
||||
| **规范化** | Normalization | 消除数据冗余的设计原则 |
|
||||
| **反规范化** | Denormalization | 适当冗余提高性能的设计 |
|
||||
| **执行计划** | Execution Plan | 数据库执行 SQL 的详细步骤 |
|
||||
| **B+ 树** | B+ Tree | 索引的底层数据结构 |
|
||||
| **MVCC** | Multi-Version Concurrency Control | 多版本并发控制,实现事务隔离 |
|
||||
| **脏读** | Dirty Read | 读取未提交的数据 |
|
||||
| **不可重复读** | Non-Repeatable Read | 同一事务两次读取结果不同 |
|
||||
| **幻读** | Phantom Read | 同一事务两次读取结果集不同 |
|
||||
| **隔离级别** | Isolation Level | 事务隔离的程度(READ UNCOMMITTED/READ COMMITTED/REPEATABLE READ/SERIALIZABLE) |
|
||||
Reference in New Issue
Block a user