diff --git a/notes/pics/intro-array-list.png b/notes/pics/intro-array-list.png
new file mode 100644
index 00000000..405f4326
Binary files /dev/null and b/notes/pics/intro-array-list.png differ
diff --git a/notes/pics/intro-bst.png b/notes/pics/intro-bst.png
new file mode 100644
index 00000000..bdcdb493
Binary files /dev/null and b/notes/pics/intro-bst.png differ
diff --git a/notes/pics/intro-cpu-steps.png b/notes/pics/intro-cpu-steps.png
new file mode 100644
index 00000000..debc1095
Binary files /dev/null and b/notes/pics/intro-cpu-steps.png differ
diff --git a/notes/pics/intro-hash-table.png b/notes/pics/intro-hash-table.png
new file mode 100644
index 00000000..10ff55eb
Binary files /dev/null and b/notes/pics/intro-hash-table.png differ
diff --git a/notes/pics/intro-index.png b/notes/pics/intro-index.png
new file mode 100644
index 00000000..a115ee38
Binary files /dev/null and b/notes/pics/intro-index.png differ
diff --git a/notes/pics/intro-layers.png b/notes/pics/intro-layers.png
new file mode 100644
index 00000000..8a85cb7c
Binary files /dev/null and b/notes/pics/intro-layers.png differ
diff --git a/notes/pics/intro-order-bytes.png b/notes/pics/intro-order-bytes.png
new file mode 100644
index 00000000..bbd56bef
Binary files /dev/null and b/notes/pics/intro-order-bytes.png differ
diff --git a/notes/pics/intro-overview.png b/notes/pics/intro-overview.png
new file mode 100644
index 00000000..d0fd349f
Binary files /dev/null and b/notes/pics/intro-overview.png differ
diff --git a/notes/pics/intro-process-thread.png b/notes/pics/intro-process-thread.png
new file mode 100644
index 00000000..ecabd3c8
Binary files /dev/null and b/notes/pics/intro-process-thread.png differ
diff --git a/notes/pics/intro-syscall.png b/notes/pics/intro-syscall.png
new file mode 100644
index 00000000..fc729572
Binary files /dev/null and b/notes/pics/intro-syscall.png differ
diff --git a/notes/pics/intro-tcp-http.png b/notes/pics/intro-tcp-http.png
new file mode 100644
index 00000000..49148ed9
Binary files /dev/null and b/notes/pics/intro-tcp-http.png differ
diff --git a/notes/pics/intro-transaction.png b/notes/pics/intro-transaction.png
new file mode 100644
index 00000000..1065d411
Binary files /dev/null and b/notes/pics/intro-transaction.png differ
diff --git a/notes/学习路线 - 零基础.md b/notes/学习路线 - 零基础.md
new file mode 100644
index 00000000..09c8eb17
--- /dev/null
+++ b/notes/学习路线 - 零基础.md
@@ -0,0 +1,1775 @@
+# 学习路线 - 零基础
+
+* [学习路线 - 零基础](#学习路线---零基础)
+ * [一、总览:一次下单经过了什么](#一总览一次下单经过了什么)
+ * [学习安排](#学习安排)
+ * [二、数据的表示](#二数据的表示)
+ * [二进制与字节](#二进制与字节)
+ * [整数](#整数)
+ * [小数](#小数)
+ * [文字](#文字)
+ * [一个订单在内存中的样子](#一个订单在内存中的样子)
+ * [练习](#练习)
+ * [三、程序怎样运行](#三程序怎样运行)
+ * [CPU、内存与磁盘](#cpu内存与磁盘)
+ * [指令](#指令)
+ * [存储器的速度](#存储器的速度)
+ * [编译与解释](#编译与解释)
+ * [从程序到进程](#从程序到进程)
+ * [练习](#练习-1)
+ * [四、操作系统](#四操作系统)
+ * [为什么需要操作系统](#为什么需要操作系统)
+ * [进程](#进程)
+ * [线程](#线程)
+ * [系统调用](#系统调用)
+ * [文件](#文件)
+ * [在终端中查看进程](#在终端中查看进程)
+ * [练习](#练习-2)
+ * [五、计算机网络](#五计算机网络)
+ * [IP 地址与端口](#ip-地址与端口)
+ * [域名与 DNS](#域名与-dns)
+ * [分层](#分层)
+ * [TCP](#tcp)
+ * [UDP](#udp)
+ * [HTTP](#http)
+ * [HTTPS](#https)
+ * [把过程连起来](#把过程连起来)
+ * [练习](#练习-3)
+ * [六、数据库](#六数据库)
+ * [为什么需要数据库](#为什么需要数据库)
+ * [表](#表)
+ * [SQL](#sql)
+ * [索引](#索引)
+ * [事务](#事务)
+ * [练习](#练习-4)
+ * [七、数据结构与算法](#七数据结构与算法)
+ * [数组](#数组)
+ * [链表](#链表)
+ * [栈和队列](#栈和队列)
+ * [哈希表](#哈希表)
+ * [二分查找](#二分查找)
+ * [树](#树)
+ * [排序](#排序)
+ * [时间复杂度](#时间复杂度)
+ * [练习](#练习-5)
+ * [八、Git](#八git)
+ * [练习](#练习-6)
+ * [九、下一步](#九下一步)
+ * [练习](#练习-7)
+ * [附录:实测](#附录实测)
+ * [A1 字符编码与浮点数](#a1-字符编码与浮点数)
+ * [A2 订单在内存中的字节](#a2-订单在内存中的字节)
+ * [A3 机器指令](#a3-机器指令)
+ * [A4 存储器的访问延迟](#a4-存储器的访问延迟)
+ * [A5 进程、线程与系统调用](#a5-进程线程与系统调用)
+ * [A6 DNS 与往返时间](#a6-dns-与往返时间)
+ * [A7 HTTP 请求与 TCP 报文](#a7-http-请求与-tcp-报文)
+ * [A8 建表、插入与查询](#a8-建表插入与查询)
+ * [A9 索引](#a9-索引)
+ * [A10 事务](#a10-事务)
+ * [A11 比较次数](#a11-比较次数)
+ * [参考资料](#参考资料)
+
+
+
+适合没有学过计算机课程的读者,不需要会编程。全篇用一次手机下单的例子,讲数据怎样表示、程序怎样运行、操作系统做什么、请求怎样通过网络送达、订单怎样存进数据库,以及常用的数据结构。文中的命令可以在 macOS 和 Ubuntu 的终端中直接运行,每章末尾链接了讲得更深入的笔记。
+
+## 一、总览:一次下单经过了什么
+
+用户 1001 在手机 App 上打开“小王饭馆”,点了一份宫保鸡丁,价格 28.50 元,点击“提交订单”,一秒之内 App 显示“下单成功,订单号 1024”。
+
+这一秒内发生的事情大致如下:
+
+
+
+1. App 只知道服务器的名字 shop.example.com,先向 DNS 服务器查询它对应的 IP 地址 203.0.113.10;
+2. App 与服务器建立 TCP 连接,在连接上发送一个 HTTP 请求,内容是“用户 1001 要 1 份 17 号菜”;
+3. 服务器上运行着订单服务进程,操作系统收到网络数据后,把请求交给进程中的一个线程处理;
+4. 线程执行订单程序的指令,CPU 从内存中读取数据、计算,再把结果写回内存;
+5. 订单程序请求 MySQL 数据库在一个事务中完成两件事:宫保鸡丁的库存从 30 减为 29,写入订单 1024;
+6. 服务器把结果放进 HTTP 响应发回,App 收到后显示“下单成功”。
+
+### 学习安排
+
+每天约 1 小时,读完当天的小节后做章末的练习。
+
+| 天 | 阅读 | 练习 |
+| :-: | --- | --- |
+| 1 | [总览:一次下单经过了什么](#一总览一次下单经过了什么)、[二进制与字节](#二进制与字节)、[整数](#整数) | [练习](#第-1-天) |
+| 2 | [小数](#小数)、[文字](#文字)、[一个订单在内存中的样子](#一个订单在内存中的样子) | [练习](#第-2-天) |
+| 3 | [程序怎样运行](#三程序怎样运行) | [练习](#第-3-天) |
+| 4 | [为什么需要操作系统](#为什么需要操作系统)、[进程](#进程)、[线程](#线程) | [练习](#第-4-天) |
+| 5 | [系统调用](#系统调用)、[文件](#文件)、[在终端中查看进程](#在终端中查看进程) | [练习](#第-5-天) |
+| 6 | Linux 常用命令 | [练习](#第-6-天) |
+| 7 | [IP 地址与端口](#ip-地址与端口)、[域名与 DNS](#域名与-dns)、[分层](#分层) | [练习](#第-7-天) |
+| 8 | [TCP](#tcp)、[UDP](#udp) | [练习](#第-8-天) |
+| 9 | [HTTP](#http)、[HTTPS](#https)、[把过程连起来](#把过程连起来) | [练习](#第-9-天) |
+| 10 | [为什么需要数据库](#为什么需要数据库)、[表](#表)、[SQL](#sql) | [练习](#第-10-天) |
+| 11 | 分组与连接 | [练习](#第-11-天) |
+| 12 | [索引](#索引)、[事务](#事务) | [练习](#第-12-天) |
+| 13 | [数据结构与算法](#七数据结构与算法)、[数组](#数组)、[链表](#链表) | [练习](#第-13-天) |
+| 14 | [栈和队列](#栈和队列) | [练习](#第-14-天) |
+| 15 | [哈希表](#哈希表) | [练习](#第-15-天) |
+| 16 | [二分查找](#二分查找)、[树](#树) | [练习](#第-16-天) |
+| 17 | [排序](#排序)、[时间复杂度](#时间复杂度) | [练习](#第-17-天) |
+| 18 | Git | [练习](#第-18-天) |
+| 19 | 复习全篇 | [练习](#第-19-天) |
+
+## 二、数据的表示
+
+### 二进制与字节
+
+计算机中的所有数据,包括数字、文字、图片和程序本身,最终都是一串 0 和 1。这是因为电路最容易可靠地区分两种状态,例如电压的高和低,用它们分别表示 1 和 0。
+
+一个 0 或 1 称为一位(bit),8 位组成一个字节(Byte),字节是存储和传输数据的基本单位。1 个字节有 2⁸ = 256 种取值,可以表示 0~255。更大的单位按 1024 进位:1 KB = 1024 字节,1 MB = 1024 KB,1 GB = 1024 MB。
+
+二进制和十进制一样是按位计数,只是逢二进一。例如订单号 1024 等于 2¹⁰,写成二进制是 1 后面跟 10 个 0:
+
+```bash
+echo 'obase=2; 1024' | bc
+```
+
+```text
+10000000000
+```
+
+二进制写起来太长,所以常用十六进制(0~9 和 a~f,逢十六进一)表示,每个十六进制数字正好对应 4 位,两个数字对应一个字节。1024 的十六进制是 0x400(前缀 0x 表示十六进制),一个字节的取值范围写成 00~ff。
+
+### 整数
+
+程序在保存整数之前要先决定用几个字节,这决定了能表示的范围:
+
+- 4 个字节(32 位)的整数类型,例如 Java、C 中的 int,最大约 21 亿(2147483647);
+- 8 个字节(64 位)的整数类型,例如 long,最大约 922 亿亿。
+
+订单号 1024 只需要 11 位,用 4 个字节就够了。但订单号会一直增长,大型平台的订单总数会超过 21 亿,所以订单号通常用 8 个字节保存。超出范围时数值会“溢出”,例如 4 字节整数 2147483647 再加 1,结果变成负数 -2147483648。
+
+负数用补码(Two's Complement)表示:把正数的各位取反再加 1。例如退款 -2850 分,在 4 个字节中是 ff ff f4 de。这样设计的好处是加法电路不用区分正负,-2850 + 2850 按二进制直接相加,进位溢出最高位后正好得到 0。
+
+### 小数
+
+宫保鸡丁的价格 28.50 元带有小数。计算机中最常用的小数格式是浮点数(Floating-Point Number),按 IEEE 754 标准用 8 个字节保存一个二进制小数。
+
+问题在于很多十进制小数无法用二进制精确表示。28.5 = 16 + 8 + 4 + 0.5,写成二进制是 11100.1,可以精确保存;但 0.1 写成二进制是 0.000110011001100…,后面无限循环,只能保存一个近似值。假设这单再加打包费 1.10 元和配送费 2.20 元,用浮点数计算:
+
+```bash
+awk 'BEGIN { printf "%.17g\n", 1.10 + 2.20 }'
+awk 'BEGIN { print (1.10 + 2.20 == 3.30) }'
+```
+
+```text
+3.3000000000000003
+0
+```
+
+结果比 3.30 多了一点点,与 3.30 比较也不相等(输出 0 表示假,macOS 15 与 Ubuntu 24.04 实测,见[附录 A1](#a1-字符编码与浮点数))。一笔订单的误差很小,但对账时成千上万笔累加、比较,就会出现“差一分钱”的问题。
+
+所以金额不用浮点数保存,常见的做法有两种:
+
+- 以分为单位用整数保存,28.50 元存为 2850,加减乘都是精确的整数运算,显示时再除以 100;
+- 使用十进制定点数,例如 MySQL 的 DECIMAL(10, 2)、Java 的 BigDecimal,它们按十进制保存每一位,不产生二进制的舍入误差。
+
+本篇后面都按第一种做法,把金额写成 2850 分。
+
+### 文字
+
+文字也要先变成数字才能保存。字符集(Character Set)给每个字符分配一个编号,例如 Unicode 给“宫”分配的编号是 U+5BAB(十六进制 5BAB)。编码(Encoding)规定怎样把编号写成字节,最常用的是 UTF-8:
+
+- 英文字母、数字等 ASCII 字符占 1 个字节,例如 K 是 0x4b;
+- 常用汉字占 3 个字节;
+- 表情符号等占 4 个字节。
+
+查看“宫保鸡丁”在 UTF-8 中的字节:
+
+```bash
+printf '宫保鸡丁' | od -An -tx1
+```
+
+```text
+ e5 ae ab e4 bf 9d e9 b8 a1 e4 b8 81
+```
+
+4 个汉字共 12 个字节,每 3 个字节对应一个字。以“宫”为例,编号 0x5BAB 的二进制是 0101 1011 1010 1011,UTF-8 把这 16 位依次填进 1110xxxx 10xxxxxx 10xxxxxx 的空位,得到 11100101 10101110 10101011,即 e5 ae ab。每个字节开头的几位说明了它是一个字的开始还是中间,程序据此把字节流切分成一个个字。
+
+同一段文字用不同的编码保存,字节也不同。例如用 GBK 编码,“宫保鸡丁”只有 8 个字节(b9 ac b1 a3 bc a6 b6 a1)。如果写入时用 UTF-8、读取时按 GBK 解释,12 个字节会被切成另外几个字,在 macOS 上显示为“瀹繚楦′竵”,这就是乱码。避免乱码的办法是全程使用同一种编码,现在通常统一为 UTF-8。
+
+### 一个订单在内存中的样子
+
+把上面几种数据放在一起:订单程序在内存中用 8 个字节保存订单号,4 个字节保存以分为单位的金额,再用一段空间保存 UTF-8 编码的菜名。实际打印出这 32 个字节(macOS 与 Ubuntu 实测,见[附录 A2](#a2-订单在内存中的字节)):
+
+
+
+- 订单号 1024 存为 00 04 00 00 00 00 00 00:低位的字节放在前面,称为小端序(Little-Endian),常见的 x86 和 ARM 处理器都是这样;
+- 金额 2850 等于 0x0B22,同样低位在前,存为 22 0b 00 00;
+- 菜名的 12 个字节后面跟一个 00,表示字符串结束;
+- 最后 7 个字节不保存数据,用于把总长度补齐到 8 的倍数,处理器按这样对齐的地址读取更快。
+
+字节本身没有含义:00 04 00 00 按整数解读是 1024,e5 ae ab 按 UTF-8 解读是“宫”,怎样解读由程序决定。
+
+知道每个数据占多少字节,就能估算数据量。按每个订单 32 字节计算,100 万个订单在内存中约占 32 MB,一台服务器的内存完全放得下;实际的订单还有地址、备注等字段,每个可能有几百字节,100 万个就是几百 MB。
+
+图片同样是一串字节。App 中宫保鸡丁的照片由一个个像素组成,每个像素用红、绿、蓝 3 个字节表示颜色,一张 800 × 600 的照片不压缩就要 800 × 600 × 3 = 144 万字节,约 1.4 MB。JPEG 等格式利用相邻像素颜色相近的特点压缩,通常能缩小到十分之一以下。
+
+整数、浮点数在 Java 中的类型和取值范围见 [Java 基础:基本类型](Java%20基础.md#基本类型)和 [float 与 double](Java%20基础.md#float-与-double);MySQL 中的数据类型见 [MySQL:数据类型](MySQL.md#四数据类型)。
+
+### 练习
+
+#### 第 1 天
+
+自测:
+
+1. 1 个字节能表示多少种取值?订单号为什么通常用 8 个字节保存?(见[二进制与字节](#二进制与字节)、[整数](#整数))
+2. 负数在计算机中怎样表示?什么是补码?(见[整数](#整数))
+
+动手:执行 echo 'obase=2; 1024' | bc 和 echo 'obase=16; 2850' | bc,把订单号和金额换成二进制和十六进制,再对照[整数](#整数)一节说出 4 字节整数的最大值为什么是 2147483647。
+
+#### 第 2 天
+
+自测:
+
+1. 金额为什么不能用 float、double 保存?应该怎样保存?(见[小数](#小数))
+2. UTF-8 中一个汉字占几个字节?为什么会出现乱码?(见[文字](#文字))
+3. 什么是大端和小端?常见的 x86、ARM 处理器用哪一种?(见[一个订单在内存中的样子](#一个订单在内存中的样子))
+
+动手:执行 printf '宫保鸡丁' | od -An -tx1 查看 4 个汉字的 UTF-8 字节,再换成自己的名字试一试;执行 awk 'BEGIN { printf "%.17g\n", 1.10 + 2.20 }',观察浮点数的误差。
+
+## 三、程序怎样运行
+
+### CPU、内存与磁盘
+
+一台服务器和一部手机在结构上是一样的,最核心的是三个部件:
+
+- 中央处理器(Central Processing Unit, CPU):执行程序的指令,做计算和判断;
+- 内存(Memory):存放正在运行的程序和它用到的数据,读写快,但断电后内容丢失;
+- 磁盘(Disk):包括固态硬盘(SSD)和机械硬盘,长期保存文件,断电不丢失,但比内存慢得多。
+
+内存可以看成一长排编了号的格子,每个格子放一个字节,格子的编号称为地址(Address)。CPU 读写内存时要给出地址,例如“读地址 0x1000 开始的 8 个字节”。一台 32 GB 内存的机器约有 343 亿个这样的格子。第二章中订单的 32 个字节,就是放在连续的 32 个格子中。
+
+订单程序平时以文件的形式保存在磁盘上,运行时先被复制到内存中,CPU 再从内存中逐条取出指令执行。订单数据也一样:数据库把订单永久保存在磁盘上,需要查询时读入内存处理。
+
+### 指令
+
+指令(Instruction)是 CPU 能直接执行的最小操作,例如“从内存某个地址读一个数到寄存器”“两个寄存器相加”“把寄存器的值写回内存”“如果结果为 0 就跳到某条指令”。每条指令本身也是几个字节的二进制数据,和订单数据一样放在内存中。
+
+寄存器(Register)是 CPU 内部的少量存储单元,每个只能放一个数,但读写不到 1 纳秒。CPU 中还有一个程序计数器(Program Counter, PC),记录下一条要执行的指令的地址。CPU 的工作就是不断重复:按 PC 取出指令,执行,PC 指向下一条。
+
+以扣库存为例,假设订单程序在内存中记着宫保鸡丁的库存 stock,程序中的一行 stock = stock - 1,编译后变成 4 条 ARM 指令(Ubuntu 24.04、gcc 13.3 实测,见[附录 A3](#a3-机器指令)):
+
+
+
+1. adrp:把变量 stock 在内存中的地址放进寄存器 x1;
+2. ldr:按 x1 中的地址从内存读出 stock 的值 30,放进寄存器 w0;
+3. sub:w0 减 1,变成 29;
+4. str:把 w0 写回内存中 stock 的位置。
+
+一行代码要拆成“读、算、写”几步,这一点在[多线程](#线程)中很重要:两个线程可能都在第 2 步读到 30,各自减 1 后都写回 29,卖出两份菜库存却只少了一份。
+
+CPU 每秒能执行多少条指令,主要取决于时钟频率和每个时钟周期能完成几条指令。例如 3.2 GHz 表示每秒 32 亿个时钟周期,现代 CPU 每个周期还能同时执行多条指令,所以每秒能执行几十亿条以上。
+
+### 存储器的速度
+
+CPU 很快,但数据要从内存或磁盘中取来。不同存储器的速度相差几个数量级,在 Apple M1 Pro 上实测随机读取一次数据的时间(见[附录 A4](#a4-存储器的访问延迟)):
+
+```text
+存储器 随机读取一次的时间
+CPU 一级缓存(16 KB) 约 1 ns
+CPU 二级缓存(1 MB) 约 6 ns
+内存(64 MB 范围内) 约 130 ns
+SSD(读 4 KB) 约 70 μs,即 70000 ns
+```
+
+1 纳秒(ns)是十亿分之一秒,1 微秒(μs)等于 1000 纳秒。读一次内存大约相当于读一级缓存的 100 多倍,随机读一次 SSD 又相当于读内存的 500 倍左右。机械硬盘还要更慢:它要等盘片转到数据所在的位置,7200 转/分的硬盘平均等半圈就要 60 ÷ 7200 ÷ 2 ≈ 4.17 ms,再加上移动磁头的时间,一次随机读取要几毫秒。
+
+为了弥补差距,CPU 内部有几级缓存(Cache),把最近用过的内存数据复制一份放在离 CPU 更近的地方;操作系统和数据库也会把常用的磁盘数据留在内存中。对订单服务来说,这意味着:
+
+- 能在内存中完成的操作尽量在内存中完成,例如把热门菜的信息缓存起来;
+- 访问磁盘时尽量一次读一大块,而不是零散地读很多次,[数据库索引](#索引)就是按这个思路设计的。
+
+### 编译与解释
+
+程序员写的程序称为源代码(Source Code),例如:
+
+```text
+如果 库存 > 0:
+ 库存 = 库存 - 1
+ 创建订单
+否则:
+ 返回 "已售罄"
+```
+
+CPU 不认识这种文字,只能执行指令。把源代码变成指令有两种方式:
+
+- 编译(Compile):运行之前由编译器一次性翻译成机器指令,生成可执行文件,例如 C、Go;
+- 解释(Interpret):运行时由解释器逐条读取源代码并执行,例如 Python、JavaScript。
+
+Java 介于两者之间:先编译成与具体 CPU 无关的字节码,运行时由 Java 虚拟机解释执行,并把执行频繁的部分即时编译(JIT)成机器指令。编译型的程序启动后直接执行指令,通常更快;解释型的程序改完不用重新编译,同一份代码可以在不同的 CPU 上运行。
+
+可执行文件本身也是磁盘上的一串字节。例如 Ubuntu 中 ls 命令的程序文件开头 4 个字节是 7f 45 4c 46,后三个字节正是字母 ELF,表示这是 Linux 的可执行文件格式:
+
+```bash
+od -A x -t x1 -N 16 /bin/ls
+```
+
+```text
+000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
+```
+
+在 macOS 上执行同一条命令,开头是 ca fe ba be,这是 macOS 的 Mach-O 格式(一个文件中同时包含 x86 和 ARM 两种指令)。所以同一个程序要分别为不同的系统和 CPU 编译。
+
+### 从程序到进程
+
+程序(Program)是磁盘上的一个文件,进程(Process)是程序的一次运行。启动订单服务时,操作系统把程序文件中的指令和初始数据装入内存,为它分配一个编号(进程号,PID),然后让 CPU 从程序的第一条指令开始执行。
+
+同一个程序可以同时运行多次,产生多个进程,例如在一台服务器上启动两个订单服务,它们执行相同的指令,但各有一份数据。
+
+源代码怎样经过编译、链接变成可执行文件见[计算机操作系统 - 链接](计算机操作系统%20-%20链接.md#编译系统);CPU 缓存见[缓存:CPU 多级缓存](缓存.md#cpu-多级缓存);机械硬盘的结构见[计算机操作系统 - 设备管理](计算机操作系统%20-%20设备管理.md#磁盘结构)。
+
+### 练习
+
+#### 第 3 天
+
+选读:[编译系统](计算机操作系统%20-%20链接.md#编译系统)。
+
+自测:
+
+1. CPU 缓存、内存、SSD 的访问速度大概各是多少,相差几个数量级?(见[存储器的速度](#存储器的速度))
+2. CPU 是怎样执行指令的?一行 stock = stock - 1 为什么不是一步完成的?(见[指令](#指令))
+3. 编译型语言和解释型语言有什么区别?Java 属于哪一种?(见[编译与解释](#编译与解释))
+
+动手:执行 od -A x -t x1 -N 16 /bin/ls 查看可执行文件开头的 16 个字节,对照[编译与解释](#编译与解释)一节说出它是哪种格式。
+
+综合题:订单 1024 的金额 28.50 元和菜名“宫保鸡丁”在内存中各占几个字节、是什么样子?服务器上的订单程序从磁盘上的一个文件,到 CPU 执行扣库存的指令,经过了哪些步骤?(见[一个订单在内存中的样子](#一个订单在内存中的样子)、[CPU、内存与磁盘](#cpu内存与磁盘)、[从程序到进程](#从程序到进程))
+
+## 四、操作系统
+
+### 为什么需要操作系统
+
+一台服务器上同时运行着订单服务、数据库、日志收集等很多程序,它们共用同一套 CPU、内存、磁盘和网卡。如果每个程序都直接操作硬件,就会出现两个程序写同一块内存、同时往网卡发数据这样的冲突,一个程序出错也可能把整台机器搞垮。
+
+操作系统(Operating System, OS)就是管理硬件、为程序提供服务的软件,例如 Linux、Windows、macOS、Android。它主要做三件事:
+
+- 分配资源:决定哪个程序使用 CPU、分给它多少内存;
+- 隔离:一个进程不能读写另一个进程的内存,出错时只影响它自己;
+- 提供统一的接口:程序不需要了解磁盘、网卡的型号,只需要请求操作系统“打开文件”“发送数据”。
+
+操作系统中直接管理硬件的核心部分称为内核(Kernel)。
+
+### 进程
+
+上一章说过,进程是正在运行的程序。操作系统为每个进程维护一份档案,记录它的进程号、内存、打开的文件,以及暂停时 CPU 寄存器的值。
+
+一台机器上的进程远多于 CPU 核心。例如在一台 10 核的 Mac 上,`top` 显示同时有 1091 个进程(macOS 15 实测,见[附录 A5](#a5-进程线程与系统调用))。它们能“同时”运行,是因为操作系统让每个进程轮流使用 CPU 一小段时间(通常是几毫秒),时间到了就保存它的寄存器,换下一个进程运行,称为上下文切换。切换得足够快,看起来就像所有进程在同时运行,这称为并发;多个核心真正在同一时刻各运行一个进程,称为并行。
+
+进程在运行过程中会在几种状态之间切换:
+
+- 运行:正在某个 CPU 核心上执行;
+- 就绪:万事俱备,只是在排队等 CPU;
+- 阻塞:在等待某件事完成,例如等数据库返回结果、等网络数据到达,这时即使有空闲的 CPU 也无法运行。
+
+订单服务处理一个请求时,大部分时间处于阻塞状态:向数据库发出查询后,要等几毫秒才有结果。操作系统不会让 CPU 空等,而是把它交给其它就绪的进程或线程,结果到达后再把等待者改为就绪。
+
+每个进程看到的都是自己独立的一片内存地址,称为虚拟内存(Virtual Memory)。两个进程中同样的地址 0x1000 对应着不同的物理内存,由操作系统和 CPU 一起完成翻译,所以一个进程无法直接读写另一个进程的数据。订单服务和 MySQL 是两个进程,它们之间交换数据要借助操作系统提供的进程间通信方式,例如管道、共享内存和网络连接(套接字),其中网络连接还能跨机器使用。
+
+### 线程
+
+订单服务在午餐高峰时每秒要处理几百个下单请求。如果一个进程一次只处理一个请求,用户 1002 就得等用户 1001 的请求处理完;而处理请求的大部分时间花在等待数据库和网络上,CPU 其实是空闲的。
+
+线程(Thread)是进程中的一条执行路线。一个进程可以有多个线程,每个线程有自己的程序计数器、寄存器和栈(保存函数调用过程中的临时数据),但它们共享进程的代码、数据和打开的文件。操作系统调度的基本单位是线程:一个线程等待数据库时,CPU 可以去执行另一个线程。
+
+在实验中让订单服务同时处理 3 个下单请求,用 ps 查看,进程 7 中除了等待新连接的主线程,又多了 3 个线程(Ubuntu 24.04 实测,见[附录 A5](#a5-进程线程与系统调用)):
+
+```text
+ PID SPID TTY TIME CMD
+ 7 7 ? 00:00:00 python3
+ 7 15 ? 00:00:00 python3
+ 7 16 ? 00:00:00 python3
+ 7 17 ? 00:00:00 python3
+```
+
+
+
+进程与线程的区别可以归纳为:
+
+- 进程是资源分配的单位,拥有独立的内存;线程是 CPU 调度的单位,同一进程的线程共享内存;
+- 创建和切换线程比进程开销小,因为不用切换内存的映射;
+- 一个线程出错(例如非法访问内存)可能导致整个进程退出,而一个进程崩溃不影响其它进程。
+
+线程虽然比进程轻,但创建和销毁仍有开销,每个线程还要占用一份栈的内存。所以实际的服务程序通常预先创建一组线程反复使用,称为线程池:请求到达时交给一个空闲的线程,处理完后线程回到池中等待下一个请求,请求多于线程数时在队列中排队。
+
+共享内存让线程之间交换数据很方便,但也带来了问题。回到上一章扣库存的 3 条指令,两个线程同时卖最后一份宫保鸡丁时可能这样交错执行:
+
+```text
+线程 15(用户 1001) 线程 16(用户 1002) 内存中的 stock
+读 stock 到寄存器:1 1
+ 读 stock 到寄存器:1 1
+寄存器减 1:0
+ 寄存器减 1:0
+写回 stock:0 0
+ 写回 stock:0 0
+```
+
+两个线程都认为还有库存,卖出了两份,库存却只减了 1。解决办法是加锁(Lock):一个线程在“读、算、写”期间持有锁,另一个线程必须等它释放锁之后才能开始,从而保证这几步作为一个整体执行。数据库也用类似的机制保护库存,见[事务](#事务)。
+
+### 系统调用
+
+普通程序不能直接操作硬件。CPU 有两种运行状态:
+
+- 用户态(User Mode):普通程序运行在这种状态下,只能执行计算、读写自己内存这类普通指令;
+- 内核态(Kernel Mode):操作系统内核运行在这种状态下,可以执行所有指令,直接操作磁盘、网卡等硬件。
+
+程序需要读写文件、收发网络数据、创建线程时,要通过系统调用(System Call)请求内核代为完成:程序把要做的事和参数放进约定的寄存器,执行一条特殊的指令,CPU 切换到内核态,跳转到内核中对应的代码;内核完成后把结果放进寄存器,切换回用户态,程序继续执行。
+
+在 Linux 上可以用 strace 命令查看一个程序发出的系统调用。例如 echo 命令输出一行文字,实际是调用了 write:
+
+```bash
+strace -e trace=write echo "order 1024"
+```
+
+```text
+write(1, "order 1024\n", 11order 1024
+) = 11
+```
+
+write 的第一个参数 1 表示标准输出(屏幕),第二个是要写的内容,返回值 11 表示写入了 11 个字节。
+
+处理一次下单请求时,订单服务的系统调用如下(Ubuntu 24.04 实测,见[附录 A5](#a5-进程线程与系统调用)):
+
+
+
+收发网络数据的 accept4、recvfrom、sendto 交给内核的网络协议栈,读写文件的 openat、write、close 交给文件系统。订单程序自己只做计算,凡是和外界打交道的事情都要经过内核。系统调用要切换 CPU 的状态,比普通的函数调用开销大,所以程序会尽量把数据攒成一批再读写,减少调用次数。
+
+### 文件
+
+文件(File)是保存在磁盘上、有名字的一串字节。操作系统用目录把文件组织成一棵树,例如 /var/log/orders.log 表示根目录下 var 目录中 log 目录里的 orders.log 文件。
+
+程序先用 open 打开文件,内核返回一个小整数,称为文件描述符(File Descriptor),之后的读写都用这个数指代文件。上图中 openat 返回 5,后面的 write(5, ...) 就是往这个文件写。Linux 中很多东西都用文件描述符表示,例如网络连接:accept4 返回的 4 就是与 App 之间的连接,sendto(4, ...) 把响应发给 App。
+
+文件适合保存日志这类只追加、按顺序读的数据。订单则需要按条件查询、多人同时修改、出错时撤销,单靠文件很难做到,所以要用[数据库](#为什么需要数据库)。
+
+### 在终端中查看进程
+
+列出当前运行的进程:
+
+```bash
+ps -ef | head -5
+```
+
+输出中 PID 是进程号,PPID 是父进程的进程号(由谁启动),CMD 是启动的命令。查看进程占用 CPU 和内存的实时情况:
+
+```bash
+top
+```
+
+按 q 退出。在 macOS 上,top 第一行显示进程总数和线程总数,例如“Processes: 1091 total ... 11019 threads”;在 Linux 上可以用 `ps -T -p 进程号` 列出一个进程的所有线程。
+
+进程、线程、调度和同步的细节见[计算机操作系统 - 进程管理](计算机操作系统%20-%20进程管理.md#进程与线程);系统调用与用户态、内核态见[计算机操作系统 - 概述](计算机操作系统%20-%20概述.md#系统调用);虚拟内存见[计算机操作系统 - 内存管理](计算机操作系统%20-%20内存管理.md#虚拟内存);多线程和锁在 Java 中的用法见 [Java 并发](Java%20并发.md#四互斥同步);常用的进程命令见 [Linux:进程管理](Linux.md#十进程管理)。
+
+### 练习
+
+#### 第 4 天
+
+选读:[进程与线程](计算机操作系统%20-%20进程管理.md#进程与线程)、[进程状态的切换](计算机操作系统%20-%20进程管理.md#进程状态的切换)。
+
+自测:
+
+1. 进程和线程有什么区别?(见[线程](#线程))
+2. 什么是上下文切换?并发和并行有什么区别?(见[进程](#进程))
+3. 两个线程同时扣减库存为什么会出错?怎样避免?(见[线程](#线程))
+
+动手:执行 ps -ef | head -5 和 top,找出占用 CPU 最多的进程;在 macOS 的 top 第一行看进程和线程的总数,对照[进程](#进程)一节理解为什么进程数远多于 CPU 核心数。
+
+#### 第 5 天
+
+选读:[系统调用](计算机操作系统%20-%20概述.md#系统调用)、[进程通信](计算机操作系统%20-%20进程管理.md#进程通信)。
+
+自测:
+
+1. 什么是用户态和内核态?为什么读写文件要通过系统调用?(见[系统调用](#系统调用))
+2. 进程之间有哪些通信方式?(见[进程](#进程))
+3. 什么是文件描述符?(见[文件](#文件))
+
+动手:在 Linux 上执行 strace -e trace=write echo "order 1024",说明 write 的第一个参数和返回值的含义;再执行 ls -l /proc/self/fd,看看一个进程默认打开了哪些文件描述符。
+
+#### 第 6 天
+
+阅读:[文件与目录的基本操作](Linux.md#文件与目录的基本操作)、[文件属性](Linux.md#文件属性)、[修改权限](Linux.md#修改权限)、[获取文件内容](Linux.md#获取文件内容)、[指令与文件搜索](Linux.md#指令与文件搜索)、[管道指令](Linux.md#八管道指令)、[查看进程](Linux.md#查看进程)。
+
+选读:[文件系统](Linux.md#四文件系统)、[Bash](Linux.md#七bash)、[正则表达式](Linux.md#九正则表达式)。
+
+自测:
+
+1. chmod 755 表示什么权限?(见[修改权限](Linux.md#修改权限))
+2. 怎样查看某个端口被哪个进程占用?(见[端口与磁盘](Linux.md#端口与磁盘))
+3. 怎样统计日志中出现次数最多的 10 个 IP?(见[日志分析](Linux.md#日志分析))
+
+动手:用 cut、sort 和 uniq 统计 /etc/passwd 中每种登录 shell 的用户数。
+
+综合题:午餐高峰时,订单服务同时收到 100 个下单请求:操作系统怎样让它们同时被处理?两个请求同时扣最后一份宫保鸡丁时会出什么问题?订单程序写日志、回复 App 时,哪些步骤要由内核完成?(见[进程](#进程)、[线程](#线程)、[系统调用](#系统调用))
+
+## 五、计算机网络
+
+### IP 地址与端口
+
+App 要把请求送到服务器,首先要知道送到哪里。网络中用两个数确定通信的对象:
+
+- IP 地址(Internet Protocol Address):标识网络中的一台主机,例如服务器的 203.0.113.10,相当于楼的门牌号;
+- 端口(Port):标识主机上的一个进程,取值 0~65535,例如订单服务监听 443 端口,相当于楼里的房间号。
+
+一台服务器上可能同时运行着订单服务、数据库等多个进程,数据到达服务器后,操作系统按端口号交给对应的进程。常用的服务有约定的端口:HTTP 用 80,HTTPS 用 443,MySQL 用 3306。客户端一方的端口通常由操作系统临时分配,例如 52344。
+
+一台服务器的 443 端口可以同时和成千上万个 App 通信,是因为一个连接由 4 个数共同确定:客户端 IP、客户端端口、服务器 IP、服务器端口。两个用户的手机地址或端口不同,就是两个不同的连接,操作系统据此把收到的数据分给对应的连接。
+
+IPv4 地址是 4 个字节,写成用点分开的 4 个十进制数,每个 0~255。家里的手机和电脑拿到的通常是 192.168 开头的私有地址,例如 192.168.1.10,它只在家庭网络内有效;访问互联网时,路由器把它替换成运营商分配的公网地址,这称为网络地址转换(NAT)。查看本机在局域网中的地址:
+
+```bash
+ipconfig getifaddr en0 # macOS
+hostname -I # Ubuntu
+```
+
+### 域名与 DNS
+
+IP 地址不好记,而且服务器换了机器地址就会变,所以 App 中写的是域名(Domain Name),例如 shop.example.com。把域名换成 IP 地址的系统称为域名系统(Domain Name System, DNS)。
+
+DNS 是一个分布在全世界的查询系统。手机先问本地的 DNS 服务器(通常是家里的路由器或运营商的服务器),它如果没有缓存答案,就从根服务器开始,依次询问负责 .com 的服务器、负责 example.com 的服务器,最终得到 IP 地址,并缓存一段时间供下次使用。
+
+用 dig 命令查询 www.baidu.com(macOS 实测,见[附录 A6](#a6-dns-与往返时间)):
+
+```bash
+dig www.baidu.com
+```
+
+```text
+;; ANSWER SECTION:
+www.baidu.com. 600 IN CNAME www.a.shifen.com.
+www.a.shifen.com. 600 IN A 183.2.172.177
+
+;; Query time: 5 msec
+;; SERVER: 192.168.1.1#53(192.168.1.1)
+```
+
+- CNAME 表示 www.baidu.com 是 www.a.shifen.com 的别名,A 记录给出了最终的 IPv4 地址;
+- 600 是有效期(秒),在此期间可以直接使用缓存的结果;
+- SERVER 一行说明这次是由家里的路由器 192.168.1.1 回答的,耗时 5 毫秒。
+
+### 分层
+
+把一个请求从手机送到服务器,要解决很多层面的问题:内容怎样表达、数据丢了怎么办、经过哪些路由器、怎样在一根网线或一段无线信号上传输。网络把这些问题分成几层,每层只解决一类问题,并使用下一层提供的服务:
+
+- 应用层:规定请求和响应的内容,例如 HTTP、DNS;
+- 传输层:在两个进程之间传输数据,用端口区分进程,例如 TCP、UDP;
+- 网络层:把数据从一台主机送到另一台主机,用 IP 地址寻址,由路由器逐跳转发;
+- 链路层:在同一个网络中把数据交给下一站,例如以太网、Wi-Fi。
+
+发送时,每一层在数据前面加上自己的首部,称为封装;接收时逐层去掉首部,交给上一层。下单请求的封装过程如下:
+
+
+
+路由器只看 IP 首部中的目的地址,按自己的路由表决定下一站交给谁,并换上新的帧首部;它不关心里面是 HTTP 还是别的内容。只有两端的主机才会处理 TCP 和 HTTP。分层的好处在于各层可以独立变化,例如手机从 Wi-Fi 切换到 4G 只换了链路层,HTTP 请求的内容不受影响。
+
+查看到服务器要经过多长时间可以用 ping,它发出一个小数据包并等待对方回复,所用的时间称为往返时间(Round-Trip Time, RTT):
+
+```bash
+ping -c 4 www.baidu.com
+```
+
+从实验所用的家庭宽带到 183.2.172.177,往返时间约 9.7 ms(macOS 实测,见[附录 A6](#a6-dns-与往返时间))。这个时间主要花在信号的传播和路由器的转发上,后面会看到,建立连接、发送请求都要按往返次数付出这个代价。
+
+### TCP
+
+网络层只负责尽力把数据包送到,不保证送达:数据包可能因为路由器繁忙被丢弃,可能走不同的路径而先发后到,也可能重复。而订单请求必须完整、按顺序地到达。
+
+传输控制协议(Transmission Control Protocol, TCP)在不可靠的网络上提供可靠的传输,它主要做了以下几件事:
+
+- 编号:给发送的每个字节编上序号,接收方按序号重新排好顺序、去掉重复;
+- 确认:接收方收到数据后回复确认号,表示“这个序号之前的数据都收到了”;
+- 重传:发送方一段时间内没有收到确认,就重新发送这部分数据;
+- 流量控制和拥塞控制:根据接收方的处理能力和网络的拥堵情况调整发送速度。
+
+通信之前,双方要先建立连接,交换各自的起始序号,这个过程需要 3 个报文,称为三次握手。下面是实验中一次下单请求在 TCP 连接上的全部报文,序号是抓包得到的真实数值(Ubuntu 24.04 实测,见[附录 A7](#a7-http-请求与-tcp-报文)):
+
+
+
+1. 客户端发送 SYN,起始序号 3987857382;
+2. 服务器回复 SYN + ACK,确认号 3987857383 表示收到了 SYN,同时给出自己的起始序号;
+3. 客户端回复 ACK,连接建立,双方可以发送数据;
+4. 客户端发送 180 字节的 HTTP 请求,序号从 3987857383 开始,服务器回复确认号 3987857563,正好是 3987857383 + 180。
+
+如果服务器的确认迟迟不来,客户端就会重传这 180 个字节。握手要花一个往返时间,所以 App 通常会复用已经建立的连接发送多个请求。
+
+为什么是三次而不是两次?双方都要确认“对方能收到我的数据”,并让对方知道自己的起始序号:客户端的 SYN 和服务器的 ACK 确认了客户端到服务器这个方向,服务器的 SYN 和客户端的 ACK 确认了反方向,中间两个报文合并成一个 SYN + ACK,所以一共 3 个。只握手两次时,服务器无法知道客户端是否收到了自己的起始序号;网络中滞留的旧 SYN 到达时,服务器也会误以为有新连接而白白分配资源。
+
+通信结束后,双方各自发送 FIN 表示“我这边的数据发完了”,并确认对方的 FIN,一般需要 4 个报文,称为四次挥手。两个方向分别关闭,是因为一方发完数据时,另一方可能还有数据要发。
+
+### UDP
+
+用户数据报协议(User Datagram Protocol, UDP)是另一种传输层协议,它只在 IP 之上加了端口和校验,不建立连接,不确认,也不重传。
+
+TCP 适合必须完整送达的数据,例如下单请求、网页、文件下载;UDP 适合偶尔丢一点也没关系、但要求延迟低的场景,例如视频通话、游戏中的位置同步,以及 DNS 查询(一问一答,丢了重问一次即可)。两者的对比可以概括为:TCP 面向连接、可靠、按顺序,开销较大;UDP 无连接、不保证送达,开销小、延迟低。
+
+### HTTP
+
+超文本传输协议(HyperText Transfer Protocol, HTTP)规定了请求和响应的格式,App 与服务器之间、浏览器与网站之间都用它通信。HTTP/1.1 的报文是纯文本,可以用 curl 直接看到。实验中用一个模拟的订单服务创建订单(Ubuntu 24.04 实测,见[附录 A7](#a7-http-请求与-tcp-报文)):
+
+```bash
+curl -v -H 'Content-Type: application/json' \
+ -d '{"userId":1001,"dishId":17,"quantity":1}' http://127.0.0.1:8080/api/orders
+```
+
+curl 发出的请求(以 > 开头)和收到的响应(以 < 开头):
+
+```text
+> POST /api/orders HTTP/1.1
+> Host: 127.0.0.1:8080
+> User-Agent: curl/8.5.0
+> Accept: */*
+> Content-Type: application/json
+> Content-Length: 40
+>
+< HTTP/1.1 201 Created
+< Server: BaseHTTP/0.6 Python/3.12.3
+< Content-Type: application/json; charset=utf-8
+< Content-Length: 57
+<
+{"orderId": 1024, "status": "CREATED", "amount": "28.50"}
+```
+
+请求由三部分组成:
+
+- 请求行:方法 POST 表示提交数据、创建资源,路径 /api/orders 表示订单,最后是协议版本;
+- 首部:每行一个“名称: 值”,例如 Content-Type 说明请求体是 JSON 格式,Content-Length 说明请求体有 40 个字节;
+- 请求体:空行之后的 {"userId":1001,"dishId":17,"quantity":1}。
+
+响应的第一行是状态行,201 是状态码,表示创建成功。状态码按第一位数字分类:2xx 表示成功,3xx 表示重定向,4xx 表示客户端的错误(例如查询不存在的订单返回 404 Not Found),5xx 表示服务器的错误。查询订单用 GET 方法:
+
+```bash
+curl -i http://127.0.0.1:8080/api/orders/1024
+```
+
+```text
+HTTP/1.1 200 OK
+Content-Type: application/json; charset=utf-8
+Content-Length: 81
+
+{"orderId": 1024, "dish": "宫保鸡丁", "amount": "28.50", "status": "CREATED"}
+```
+
+Content-Length 是 81 而不是字符数,因为“宫保鸡丁”在 UTF-8 中占 12 个字节(见[文字](#文字))。
+
+常用的方法还有 PUT(整体替换资源)、DELETE(删除资源)等。GET 和 POST 的区别主要在用途上:
+
+- GET 用于获取数据,参数放在 URL 中,例如 /api/orders?userId=1001,执行多少次结果都一样,浏览器和代理可以缓存它的结果;
+- POST 用于提交数据、创建资源,参数放在请求体中,重复执行会创建多个订单,所以 App 要防止用户连点两次“提交订单”。
+
+HTTP 本身不保存状态,每个请求都是独立的,服务器不会记得上一个请求来自谁。要知道请求来自用户 1001,App 在登录成功后保存服务器发给它的凭证,之后的每个请求都在首部中带上,例如 Cookie 首部或 Authorization 首部中的 Token,服务器据此识别用户。
+
+### HTTPS
+
+HTTP 是明文传输的,同一个 Wi-Fi 下的其他人或沿途的路由器都能看到、甚至修改请求内容。HTTPS 在 TCP 和 HTTP 之间加了一层 TLS(Transport Layer Security):
+
+- 加密:双方在 TLS 握手中协商出一个只有彼此知道的密钥,之后的 HTTP 报文都加密传输;
+- 认证:服务器出示由可信机构签发的证书,证明自己确实是 shop.example.com,防止连到假冒的服务器;
+- 完整性:报文被篡改时接收方能够发现。
+
+HTTPS 默认使用 443 端口。代价是建立连接时多了 TLS 握手,需要额外的往返时间。
+
+### 把过程连起来
+
+用 curl 访问一个真实的 HTTPS 网站,可以看到各阶段完成的时刻:
+
+```bash
+curl -4 -s -o /dev/null -w 'dns %{time_namelookup} tcp %{time_connect} tls %{time_appconnect} first_byte %{time_starttransfer}\n' https://www.baidu.com
+```
+
+```text
+dns 0.002281 tcp 0.011717 tls 0.041322 first_byte 0.053568
+```
+
+时间是从开始计算的累计秒数(macOS 实测,见[附录 A6](#a6-dns-与往返时间))。对照下单的过程:
+
+1. DNS:域名换成 IP 地址,命中缓存只用了 2 ms;
+2. TCP:三次握手约 9 ms,大约一个往返时间;
+3. TLS:握手约 30 ms,这次使用的 TLS 1.2 需要两个往返,再加上证书校验等计算;
+4. HTTP:发出请求到收到第一个字节约 12 ms,即一个往返时间加上服务器的处理时间。
+
+整个过程约 54 ms,其中真正的服务器处理只占一小部分,大部分时间花在网络往返上。这也是“在浏览器中输入网址后发生了什么”这个问题的主线,完整的过程还包括本机获取 IP 地址、查找网关的 MAC 地址等步骤。
+
+分层模型见[计算机网络 - 概述](计算机网络%20-%20概述.md#计算机网络体系结构);IP 地址与 NAT 见[计算机网络 - 网络层](计算机网络%20-%20网络层.md#ip-地址编址方式);TCP 的握手与可靠传输见[计算机网络 - 传输层](计算机网络%20-%20传输层.md#tcp-的三次握手);DNS 与访问网页的完整过程见[计算机网络 - 应用层](计算机网络%20-%20应用层.md#web-页面请求过程);HTTP 报文、状态码和 HTTPS 见 [HTTP](HTTP.md#请求和响应报文)。
+
+### 练习
+
+#### 第 7 天
+
+选读:[计算机网络体系结构](计算机网络%20-%20概述.md#计算机网络体系结构)。
+
+自测:
+
+1. IP 地址和端口分别用来标识什么?一台服务器的 443 端口怎样同时与很多手机通信?(见[IP 地址与端口](#ip-地址与端口))
+2. DNS 怎样把域名解析成 IP 地址?(见[域名与 DNS](#域名与-dns))
+3. 网络为什么要分层?各层分别解决什么问题?(见[分层](#分层))
+
+动手:执行 dig www.baidu.com 和 ping -c 4 www.baidu.com,找出解析得到的 IP 地址、记录的有效期和往返时间。
+
+#### 第 8 天
+
+选读:[TCP 的三次握手](计算机网络%20-%20传输层.md#tcp-的三次握手)、[TCP 的四次挥手](计算机网络%20-%20传输层.md#tcp-的四次挥手)。
+
+自测:
+
+1. TCP 怎样保证可靠传输?(见[TCP](#tcp))
+2. TCP 为什么需要三次握手,两次为什么不行?(见[TCP](#tcp))
+3. TCP 和 UDP 有什么区别?各适合什么场景?(见[UDP](#udp))
+
+动手:用 Wireshark 或 tcpdump 抓一次 curl http://example.com 的数据包,找出三次握手的 3 个报文,对照[TCP](#tcp)一节中的图核对序号和确认号的关系。
+
+#### 第 9 天
+
+选读:[HTTP 状态码](HTTP.md#三http-状态码)、[Web 页面请求过程](计算机网络%20-%20应用层.md#web-页面请求过程)。
+
+自测:
+
+1. GET 和 POST 有什么区别?常见的状态码有哪些?(见[HTTP](#http))
+2. HTTP 和 HTTPS 有什么区别?(见[HTTPS](#https))
+3. 在浏览器中输入网址到看到页面,中间发生了什么?(见[把过程连起来](#把过程连起来))
+
+动手:用[把过程连起来](#把过程连起来)一节中带 -w 参数的 curl 命令访问几个常用的网站,比较 DNS、TCP、TLS 各阶段的耗时。
+
+综合题:从点击“提交订单”到 App 显示“下单成功”:按 DNS、TCP、HTTP 的顺序,说出每一步发送了什么、发给了谁,以及哪些步骤要花一个往返时间。(见[一次下单经过了什么](#一总览一次下单经过了什么)、[TCP](#tcp)、[把过程连起来](#把过程连起来))
+
+## 六、数据库
+
+### 为什么需要数据库
+
+订单服务最简单的做法是把每个订单追加到一个文件中,每行一个。但很快会遇到这些问题:
+
+- 查询慢:要找用户 1001 的订单,只能从头到尾读一遍文件;
+- 并发冲突:两个线程同时修改同一个文件,内容可能互相覆盖;
+- 不完整:扣完库存、还没写订单时程序崩溃,两份数据就对不上了;
+- 难以修改:订单状态从“已创建”改为“已支付”,要改写文件中间的一行。
+
+数据库(Database)是专门保存和管理数据的软件,它解决的正是这些问题:用统一的查询语言按条件查找,用索引加快查找,用锁处理并发修改,用事务保证一组修改要么全部完成、要么全部不做,并且在崩溃后能恢复。订单服务只需要通过网络把请求发给数据库进程,例如 MySQL。
+
+本章用 SQLite 做实验。SQLite 是一个以文件形式保存数据的小型数据库,macOS 自带,Ubuntu 上执行 sudo apt install sqlite3 即可安装,不需要启动数据库服务。下面的基本语句在 MySQL 中写法相同,结果也一致(MySQL 8.0 实测,见[附录 A8](#a8-建表插入与查询))。
+
+### 表
+
+关系型数据库把数据组织成表(Table),一张表类似一个电子表格:每一列(Column)是一个字段,有确定的名称和类型;每一行(Row)是一条记录。饭馆的菜品和订单分别放在两张表中:
+
+```text
+dishes 表
+id name price stock
+17 宫保鸡丁 2850 30
+
+orders 表
+id user_id dish_id amount status created_at
+1024 1001 17 2850 CREATED 2026-10-29 12:30:00
+```
+
+- 主键(Primary Key)唯一标识一行,这里是 id,两个订单不能有相同的订单号;
+- orders 表的 dish_id 是 17,对应 dishes 表中 id 为 17 的那道菜,表之间通过这样的字段关联,不用在每个订单中重复保存菜名和价格;
+- 金额 amount 和价格 price 都以分为单位保存为整数,原因见[小数](#小数)。
+
+### SQL
+
+结构化查询语言(Structured Query Language, SQL)是操作关系型数据库的标准语言。在终端中执行 sqlite3 shop.db 打开(不存在时新建)一个数据库文件,然后逐条输入语句。
+
+建表时说明每一列的名称和类型:
+
+```sql
+CREATE TABLE dishes (
+ id INT PRIMARY KEY,
+ name VARCHAR(20) NOT NULL,
+ price INT NOT NULL,
+ stock INT NOT NULL
+);
+CREATE TABLE orders (
+ id BIGINT PRIMARY KEY,
+ user_id BIGINT NOT NULL,
+ dish_id INT NOT NULL,
+ amount INT NOT NULL,
+ status VARCHAR(10) NOT NULL,
+ created_at DATETIME NOT NULL
+);
+```
+
+INT 是 4 字节整数,BIGINT 是 8 字节整数,VARCHAR(20) 是最多 20 个字符的字符串,NOT NULL 表示必须填写。插入数据用 INSERT:
+
+```sql
+INSERT INTO dishes VALUES (17, '宫保鸡丁', 2850, 30);
+INSERT INTO orders VALUES (1024, 1001, 17, 2850, 'CREATED', '2026-10-29 12:30:00');
+```
+
+查询用 SELECT,WHERE 后面写条件;修改用 UPDATE:
+
+```sql
+SELECT id, user_id, amount, status FROM orders WHERE id = 1024;
+UPDATE orders SET status = 'PAID' WHERE id = 1024;
+```
+
+```text
+id user_id amount status
+---- ------- ------ -------
+1024 1001 2850 CREATED
+```
+
+再插入一个订单号同样是 1024 的订单,数据库会拒绝,因为主键不能重复:
+
+```text
+Runtime error: UNIQUE constraint failed: orders.id (19)
+```
+
+MySQL 中对应的报错是 Duplicate entry '1024' for key 'orders.PRIMARY'。这类约束由数据库保证,即使程序有错误,也不会写入两个相同的订单号。
+
+“我的订单”页面要显示菜名,而菜名在 dishes 表中,这时用 JOIN 把两张表按 dish_id 连接起来。假设用户 1001 晚上又点了一份 26.00 元的鱼香肉丝(订单 1026),按下单时间从新到旧列出:
+
+```sql
+SELECT o.id, d.name, o.amount, o.created_at
+FROM orders o JOIN dishes d ON o.dish_id = d.id
+WHERE o.user_id = 1001
+ORDER BY o.created_at DESC;
+```
+
+```text
+id name amount created_at
+---- -------- ------ -------------------
+1026 鱼香肉丝 2600 2026-10-29 18:05:00
+1024 宫保鸡丁 2850 2026-10-29 12:30:00
+```
+
+o 和 d 是两张表的简称,ORDER BY 指定排序,DESC 表示从大到小。统计用户 1001 一共下了几单、花了多少钱,用 COUNT 和 SUM:
+
+```sql
+SELECT COUNT(*), SUM(amount) FROM orders WHERE user_id = 1001;
+```
+
+结果是 2 单、5450 分。这些语句在 MySQL 中的结果相同(见[附录 A8](#a8-建表插入与查询))。程序只需要说明“要什么”,怎样逐行查找、连接和排序由数据库决定。
+
+### 索引
+
+按订单号查询很快,因为数据库会自动为主键建立索引。但 App 的“我的订单”页面要按用户查询:
+
+```sql
+SELECT * FROM orders WHERE user_id = 1001;
+```
+
+如果 user_id 上没有索引,数据库只能逐行检查每个订单的 user_id 是否等于 1001,这称为全表扫描。订单表有 100 万行时,就要比较 100 万次。
+
+索引(Index)是按某一列排好序的另一份数据,每一项记录着这一列的值和对应的行在哪里,就像书末尾按拼音排好的索引页。数据库中的索引通常组织成一棵 B+ 树:每个节点是一个页(例如 4 KB 或 16 KB),里面放着几百个排好序的键,从根页开始,每一层根据键的大小选择往哪个子页走,最后到达存放数据的叶子页。
+
+在 SQLite 中可以自己生成 100 万个订单来对比(下面这条语句是 SQLite 的写法):
+
+```sql
+WITH RECURSIVE n(i) AS (SELECT 1 UNION ALL SELECT i + 1 FROM n WHERE i < 1000000)
+INSERT INTO orders
+SELECT i, i * 7919 % 100000 + 1, 17, 2850, 'PAID', datetime('2026-01-01', '+' || (i * 25) || ' seconds')
+FROM n;
+```
+
+它生成订单号 1~1000000,每个订单的用户由订单号算出,10 万个用户每人正好 10 个订单。执行 .timer on 打开计时,再执行查询,然后建立索引:
+
+```sql
+CREATE INDEX idx_user ON orders (user_id);
+```
+
+
+
+没有索引时查询用户 1001 的订单约 37 ms;建立索引后不到 0.1 ms。这 100 万行的索引只有 3 层,找到用户 1001 只需要读根页、中间页和一个叶子页,再按叶子页中的 10 个订单号取出整行(SQLite 3.43 实测,见[附录 A9](#a9-索引))。在 MySQL 中,EXPLAIN 显示没有索引时预计扫描 996761 行,有索引时只读 10 行,耗时从约 100 ms 降到 0.3 ms。
+
+EXPLAIN QUERY PLAN 可以显示 SQLite 打算怎样执行查询:
+
+```text
+没有索引:SCAN orders
+有索引: SEARCH orders USING INDEX idx_user (user_id=?)
+```
+
+索引不是越多越好:每插入或修改一行,所有相关的索引都要同步更新,还要占用额外的空间。所以只为经常出现在查询条件中的列建立索引。
+
+### 事务
+
+下单要修改两张表:dishes 表中的库存减 1,orders 表中插入订单。如果扣完库存后程序崩溃,订单没有写入,就会少一份菜却没有订单。
+
+事务(Transaction)把多条语句组成一个整体:要么全部生效,要么全部不生效。用 BEGIN 开始事务,COMMIT 提交,中途出错时用 ROLLBACK 撤销已经做过的修改:
+
+```sql
+BEGIN;
+UPDATE dishes SET stock = stock - 1 WHERE id = 17 AND stock > 0;
+INSERT INTO orders VALUES (1024, 1001, 17, 2850, 'CREATED', '2026-10-29 12:30:00');
+COMMIT;
+```
+
+在 UPDATE 执行完、INSERT 之前强行结束程序(kill -9),模拟程序崩溃,结果如下(SQLite 3.43 与 MySQL 8.0 实测,见[附录 A10](#a10-事务)):
+
+
+
+不用事务时,每条语句执行完就立即生效,库存变成 29,却没有订单;用事务时,数据库发现这个事务没有提交,撤销了其中的修改,库存仍是 30。
+
+UPDATE 语句中的 stock > 0 也很重要。库存已经为 0 时,这条语句一行也不修改,程序根据“影响了 0 行”判断已售罄,回滚事务。数据库在修改一行时会给这一行加锁,另一个事务要修改同一行必须等前一个提交或回滚,所以两个用户同时抢最后一份菜,也只有一个能扣减成功,不会出现[线程](#线程)一节中两个线程都卖出的问题。
+
+事务的四个性质通常称为 ACID:
+
+- 原子性(Atomicity):事务中的修改要么全部完成,要么全部撤销;
+- 一致性(Consistency):事务前后数据都满足规则,例如库存不为负、每个订单都对应扣过的库存;
+- 隔离性(Isolation):并发执行的事务互不干扰,像是一个接一个执行;
+- 持久性(Durability):事务一旦提交,即使随后断电,修改也不会丢失。
+
+隔离性要解决的是多个事务同时读写时互相看到中间状态的问题。例如事务 A 扣了库存还没提交,事务 B 就读到了 29;如果 A 随后回滚,B 读到的就是一个从未真正存在过的值,这称为脏读。数据库提供了几种隔离级别,级别越高,事务之间越像排队执行,但并发性能越低,MySQL 默认的级别不会出现脏读。
+
+数据库实现原子性和持久性的关键是日志:修改数据之前先把“要做什么”和“原来是什么”写入日志文件,并确保写到磁盘上。崩溃后重启时,数据库读取日志,把已提交的事务补完,把未提交的事务撤销。
+
+SQL 的更多语法见 [SQL 语法](SQL%20语法.md#七查询);事务、ACID 与隔离级别见[数据库系统原理](数据库系统原理.md#一事务);B+ 树索引见 [MySQL:索引](MySQL.md#b-tree-原理)。
+
+### 练习
+
+#### 第 10 天
+
+选读:[查询](SQL%20语法.md#七查询)、[过滤](SQL%20语法.md#九过滤)。
+
+自测:
+
+1. 为什么用数据库保存订单,而不是直接写文件?(见[为什么需要数据库](#为什么需要数据库))
+2. 什么是主键?再插入一个相同主键的订单会怎样?(见[SQL](#sql))
+
+动手:执行 sqlite3 shop.db,按[SQL](#sql)一节建立 dishes 和 orders 两张表,插入订单 1024 和 1026,写出“我的订单”页面的 JOIN 查询。
+
+#### 第 11 天
+
+阅读:[函数](SQL%20语法.md#十二函数)、[分组](SQL%20语法.md#十三分组)、[子查询](SQL%20语法.md#十四子查询)、[连接](SQL%20语法.md#十五连接)。
+
+选读:[组合查询](SQL%20语法.md#十六组合查询)。
+
+自测:
+
+1. WHERE 和 HAVING 有什么区别?(见[分组](SQL%20语法.md#十三分组))
+2. 内连接和外连接有什么区别?(见[连接](SQL%20语法.md#十五连接))
+
+动手:在 SQLite 中按[182](SQL%20练习.md#182-duplicate-emails)、[175](SQL%20练习.md#175-combine-two-tables)、[181](SQL%20练习.md#181-employees-earning-more-than-their-managers)三题的 SQL Schema 建表并插入数据,先自己写出查询,再对照笔记的解法。
+
+#### 第 12 天
+
+选读:[事务](数据库系统原理.md#一事务)、[B+ Tree 原理](MySQL.md#b-tree-原理)。
+
+自测:
+
+1. 索引为什么能加快查询?为什么不是越多越好?(见[索引](#索引))
+2. 事务的 ACID 分别指什么?(见[事务](#事务))
+3. 扣库存和创建订单为什么要放在一个事务中?程序崩溃时会怎样?(见[事务](#事务))
+
+动手:按[索引](#索引)一节在 SQLite 中生成 100 万个订单,执行 .timer on,比较建立 idx_user 前后查询用户 1001 的订单的耗时,并用 EXPLAIN QUERY PLAN 查看执行方式的变化。
+
+综合题:为外卖 App 设计 dishes 和 orders 两张表:写出下单(扣库存并写入订单)的 SQL,说明为什么要放在一个事务中、UPDATE 中的 stock > 0 起什么作用,以及“我的订单”页面需要在哪一列上建索引。(见[SQL](#sql)、[索引](#索引)、[事务](#事务))
+
+## 七、数据结构与算法
+
+数据结构(Data Structure)指的是数据在内存中的组织方式,算法(Algorithm)是解决问题的步骤。同样是在 100 万个订单中找一个,组织方式不同,要做的工作可能相差几万倍。
+
+### 数组
+
+数组(Array)把元素一个挨一个地存放在一段连续的内存中。App 中的“我的订单”列表可以用数组保存订单号,每个订单号占 8 个字节:
+
+
+
+因为元素大小相同、位置连续,第 i 个元素的地址可以直接算出来:起始地址 + i × 8,所以按位置读取任何一个元素都只要一步,与数组多长无关。
+
+代价是插入和删除。新订单 1026 要显示在列表最前面,就得把后面所有元素依次后移一格;列表有 n 个订单,就要移动 n 次。数组的长度在创建时确定,放满之后要申请一块更大的内存,把元素全部复制过去。
+
+### 链表
+
+链表(Linked List)中的每个元素称为节点,节点可以分散在内存的任何位置,每个节点除了保存数据,还保存下一个节点的地址。只要记住第一个节点的地址(head),就能沿着地址依次访问所有节点。
+
+在最前面插入 1026 时,只需让新节点指向原来的第一个节点,再让 head 指向新节点,改两个地址就完成了,不用移动任何元素。但读第 n 个元素时,必须从 head 开始一个一个往后走 n 步。
+
+数组与链表的取舍可以概括为:数组按位置读取快,插入删除慢;链表插入删除快(已知位置时),按位置读取慢。此外数组的元素连续存放,CPU 读一个元素时会把相邻的元素一起读进缓存,遍历起来比链表快得多。实际开发中数组用得更多,例如 Java 的 ArrayList 就是基于数组实现的。
+
+### 栈和队列
+
+栈(Stack)和队列(Queue)限制了元素进出的顺序:
+
+- 栈是后进先出:只能在同一端放入和取出,像一摞盘子,最后放上去的最先拿走;
+- 队列是先进先出:从一端放入、另一端取出,像排队,先来的先处理。
+
+饭馆的后厨按下单顺序做菜,等待制作的订单就是一个队列:订单 1024 先进入队列,就比后面的 1025 先做。栈的典型用途是处理“嵌套”的结构,例如检查括号是否匹配:从左到右读,遇到左括号放入栈,遇到右括号就取出栈顶的左括号,看两者是否配对。程序调用函数时,也用栈保存每一层调用的临时数据,[线程](#线程)一节中每个线程的栈就是这个用途。
+
+### 哈希表
+
+按订单号查找订单是最常见的操作。如果订单放在数组中,只能逐个比较,100 万个订单平均要比较 50 万次。
+
+哈希表(Hash Table)用一个哈希函数把键直接换算成数组中的位置。最简单的哈希函数是取余数,例如有 8 个位置(称为桶)时,订单号 % 8 就是桶号:
+
+
+
+1024 % 8 = 0,放进 0 号桶;查找 1024 时同样算出 0,直接去 0 号桶找,不用看其它桶。不同的键可能算出相同的桶号,例如 1032 % 8 也是 0,这称为哈希冲突。常用的解决办法是拉链法:每个桶挂一个链表,冲突的元素依次接在后面,查找时在链表中逐个比较。
+
+只要桶的数量和元素数量相当、哈希函数把键分散得足够均匀,每个桶中平均只有一两个元素。用 100 万个随机的订单号、约 100 万个桶实测,查找一个订单平均只比较 1.48 次(见[附录 A11](#a11-比较次数))。元素越来越多时,哈希表会把桶数组扩大一倍,重新分配所有元素,让链表保持很短。
+
+哈希表的缺点是元素没有顺序,不能回答“12 点到 12 点半之间有哪些订单”这类范围查询。Java 的 HashMap 和 Redis 保存所有键的字典都是哈希表。
+
+### 二分查找
+
+如果订单已经按下单时间排好序,查找某个时刻的订单可以用二分查找(Binary Search):先和中间的元素比较,目标更大就只在右半边继续找,更小就只在左半边找,每比较一次排除一半。
+
+在 8 个按时间排好序的订单中查找 12:30 下的订单 1024,只需要 3 步:
+
+
+
+用伪代码表示:
+
+```text
+low = 0,high = n - 1
+当 low ≤ high 时重复:
+ mid = (low + high) / 2,舍去小数
+ 如果 a[mid] 等于目标:找到,返回 mid
+ 如果 a[mid] 小于目标:low = mid + 1
+ 否则:high = mid - 1
+没有找到
+```
+
+每比较一次,剩下的范围减半,n 个元素最多比较 log₂n 向下取整再加 1 次。100 万约等于 2²⁰,所以最多比较 20 次:
+
+```bash
+echo 'l(1000000)/l(2)' | bc -l
+```
+
+```text
+19.93156856932417408741
+```
+
+二分查找的前提是数据已经排好序,并且能按位置直接读取元素,所以它只适用于有序数组,不适用于链表。
+
+### 树
+
+有序数组查找快,但插入一个新订单要移动后面的所有元素。二叉查找树(Binary Search Tree, BST)把二分查找的过程变成了一种数据结构:每个节点最多有两个子节点,左子树中的值都比它小,右子树中的值都比它大。
+
+上图右边是同一组下单时间构成的树,根是中间的 12:21。查找 12:30 时从根开始,比 12:21 大就往右,比 12:34 小就往左,经过的节点和二分查找比较的元素完全相同。插入新订单时,按同样的方法找到位置,挂上一个新节点即可,不用移动其它节点。
+
+树还能做范围查询:找到范围的起点后,按从小到大的顺序往后读,超出范围就停止,哈希表做不到这一点。
+
+如果数据按顺序插入,二叉查找树会退化成一条链表,查找又变成逐个比较。实际使用的是能自动保持平衡的树,例如红黑树。
+
+二叉树每个节点只有两个分支,100 万个元素至少要 20 层。如果树放在磁盘上,每往下走一层就要读一次磁盘,20 次随机读取就要 1 毫秒以上。数据库的索引因此让每个节点占一个磁盘页、放几百个键,节点的分支多了,树就很矮。[索引](#索引)一节实验中的索引就是这样:根页有 10 个键,中间页平均约 270 个,叶子页平均约 350 个,100 万个条目只用了 3 层(SQLite 实测,见[附录 A9](#a9-索引))。根页也装满时,3 层可以容纳约 270 × 270 × 350 ≈ 2500 万个条目。MySQL 使用的 B+ 树是这种多路树的一种,只在叶子页中存放数据,叶子页之间用链表相连,便于范围查询。
+
+### 排序
+
+二分查找和范围查询都要求数据有序,所以排序是最常用的算法之一。以 5 个订单的金额(分)为例,插入排序(Insertion Sort)的做法和整理手中的扑克牌一样:从第二个开始,每次拿起一个,和左边已经排好的元素从右往左比较,插到合适的位置:
+
+```text
+开始 2850 1600 3200 980 2600
+插入 1600 1600 2850 3200 980 2600 比较 1 次
+插入 3200 1600 2850 3200 980 2600 比较 1 次
+插入 980 980 1600 2850 3200 2600 比较 3 次
+插入 2600 980 1600 2600 2850 3200 比较 3 次
+```
+
+插入排序每次只能把一个元素挪到位,数据完全打乱时,第 k 个元素平均要和左边一半的元素比较,n 个元素总共约 n²/4 次。
+
+归并排序(Merge Sort)换了一种思路:把数组分成两半,分别排好序,再把两个有序的部分合并。合并时只需比较两部分的第一个元素,取较小的放进结果,例如合并 [980, 2850] 和 [1600, 2600, 3200],依次取出 980、1600、2600、2850、3200,每取一个只比较一次。每一半又按同样的方法继续分下去,100 万个元素分 20 层就只剩单个元素,每一层合并共比较不到 n 次,所以总共约 n log₂n 次。
+
+### 时间复杂度
+
+比较算法的快慢时,不看具体用了多少秒(这和机器有关),而看数据量 n 增大时,操作次数怎样增长。时间复杂度(Time Complexity)用大 O 记号表示这种增长趋势,只保留增长最快的一项,省略常数系数:
+
+- O(1):操作次数与 n 无关,例如按位置读数组、哈希表查找;
+- O(log n):n 翻倍时只多一次操作,例如二分查找、在平衡树中查找;
+- O(n):与 n 成正比,例如在无序数组中逐个查找、全表扫描;
+- O(n log n):例如归并排序等高效的排序算法;
+- O(n²):例如插入排序,n 翻倍时操作次数变为 4 倍。
+
+以 100 万个订单为例,统计各种做法比较的次数(见[附录 A11](#a11-比较次数)):
+
+```text
+操作 复杂度 比较次数
+哈希表查找一个订单 O(1) 平均 1.48 次
+二分查找一个订单 O(log n) 最多 20 次
+逐个查找一个订单 O(n) 平均 50 万次
+归并排序 100 万个订单 O(n log n) 1867 万次
+插入排序 100 万个订单 O(n²) 约 2500 亿次
+```
+
+按每秒 10 亿次比较粗略估算,归并排序约 0.02 秒,插入排序要 250 秒,相差上万倍。数据量小的时候差别不明显,数据量一大,复杂度的差距就会远远超过机器快慢的差距。可以在终端中感受一下排序 100 万个数的速度:
+
+```bash
+seq -f '%.0f' 1000000 | sort -R > orders.txt # 生成 1~1000000 并打乱顺序
+time sort -n orders.txt > sorted.txt # 按数值排序
+```
+
+sort 使用的是 O(n log n) 的算法,100 万个数排序只用了约 0.5 秒(macOS 实测,Ubuntu 上约 0.2 秒,见[附录 A11](#a11-比较次数))。
+
+空间复杂度用同样的方法描述算法需要多少额外的内存,例如归并排序需要 O(n) 的辅助空间。
+
+复杂度的分析方法见[算法 - 算法分析](算法%20-%20算法分析.md#数学模型);各种排序算法见[算法 - 排序](算法%20-%20排序.md#插入排序);二分查找、二叉查找树和哈希表的实现见[算法 - 符号表](算法%20-%20符号表.md#2-二分查找实现有序符号表);栈和队列见[算法 - 栈和队列](算法%20-%20栈和队列.md#栈);Java 中的数组列表、链表和哈希表见 [Java 容器](Java%20容器.md#arraylist)。
+
+### 练习
+
+#### 第 13 天
+
+自测:
+
+1. 数组和链表有什么区别?各适合什么场景?(见[链表](#链表))
+
+推演:反转链表。链表 1024 → 1025 → 1026 → null,head 指向 1024。只修改节点中“下一个”的地址,把它反转成 1026 → 1025 → 1024 → null。用三个变量 prev、cur、next,写出每一步之后各变量的值和每个节点指向谁。
+
+规则:开始时 prev = null,cur = head。重复以下 4 步直到 cur 为 null:next = cur 的下一个;cur 的下一个改为 prev;prev = cur;cur = next。最后 head = prev。
+
+
+参考答案
+
+```text
+轮次 prev cur next 1024 指向 1025 指向 1026 指向
+开始 null 1024 - 1025 1026 null
+第 1 轮 1024 1025 1025 null 1026 null
+第 2 轮 1025 1026 1026 null 1024 null
+第 3 轮 1026 null null null 1024 1025
+```
+
+第 3 轮后 cur 为 null,停止,head = prev = 1026,链表变为 1026 → 1025 → 1024 → null。每一轮都要先用 next 记住下一个节点,否则改完 cur 的指向后,后面的节点就找不到了。
+
+
+
+#### 第 14 天
+
+自测:
+
+1. 栈和队列有什么区别?各举一个使用的例子。(见[栈和队列](#栈和队列))
+
+推演:用栈检查括号是否匹配。App 发出的请求体是 JSON,例如 {"items":[{"dishId":17}]},服务器解析时要检查括号是否成对。
+
+用栈分别检查 “({[]})” 和 “([)]” 是否匹配,写出每读入一个字符后栈中的内容(栈底在左)。
+
+规则:读到左括号就放入栈;读到右括号时,栈顶必须是与它配对的左括号,是就取出,否则不匹配;全部读完时栈为空才算匹配。
+
+
+参考答案
+
+“({[]})”
+
+```text
+读入 操作 栈
+( 放入 (
+{ 放入 ({
+[ 放入 ({[
+] 栈顶是 [,配对,取出 ({
+} 栈顶是 {,配对,取出 (
+) 栈顶是 (,配对,取出 空
+```
+
+读完时栈为空,匹配。
+
+“([)]”
+
+```text
+读入 操作 栈
+( 放入 (
+[ 放入 ([
+) 栈顶是 [,与 ) 不配对 ([
+```
+
+第 3 个字符就不匹配,停止。只数左右括号的个数是不够的,还要看顺序。
+
+
+
+推演:后厨的订单队列。后厨用容量为 4 的循环数组保存待做的订单,head 指向队头(下一个要做的),tail 指向下一个空位,到达末尾后回到 0。开始时 head = tail = 0。依次执行:入队 1024、入队 1025、入队 1026、出队、入队 1027、入队 1028、出队、出队。写出每一步之后数组的内容和 head、tail。
+
+
+参考答案
+
+```text
+操作 数组(下标 0~3) head tail 取出
+入队 1024 1024 - - - 0 1
+入队 1025 1024 1025 - - 0 2
+入队 1026 1024 1025 1026 - 0 3
+出队 - 1025 1026 - 1 3 1024
+入队 1027 - 1025 1026 1027 1 0
+入队 1028 1028 1025 1026 1027 1 1
+出队 1028 - 1026 1027 2 1 1025
+出队 1028 - - 1027 3 1 1026
+```
+
+订单按 1024、1025、1026 的顺序出队,先下单的先做。入队 1027 后 tail 从 3 回到 0,1028 放进了下标 0 这个已经空出来的位置,这就是“循环”的含义。入队 1028 后 head = tail = 1,此时队列是满的(4 个位置都有订单);而开始时 head = tail = 0 表示空,所以实际实现中还要另外记录元素个数来区分空和满。
+
+
+
+#### 第 15 天
+
+自测:
+
+1. 哈希表的原理是什么?哈希冲突怎样解决?(见[哈希表](#哈希表))
+
+推演:哈希表与冲突。哈希表有 8 个桶(编号 0~7),哈希函数是 订单号 % 8,冲突用拉链法处理,新元素接在链表末尾。
+
+1. 依次放入订单号 1024、1025、1026、1030、1032、1041、1047,画出每个桶的链表;
+2. 查找 1041 和 1040,各要比较几次;
+3. 再放入 1048,0 号桶变成什么样。
+
+
+参考答案
+
+1. 先算桶号:1024 % 8 = 0,1025 % 8 = 1,1026 % 8 = 2,1030 % 8 = 6,1032 % 8 = 0(与 1024 冲突),1041 % 8 = 1(与 1025 冲突),1047 % 8 = 7。结果:
+
+```text
+桶 0:1024 → 1032
+桶 1:1025 → 1041
+桶 2:1026
+桶 3:空
+桶 4:空
+桶 5:空
+桶 6:1030
+桶 7:1047
+```
+
+2. 1041 % 8 = 1,到 1 号桶,比较 1025(不是)、1041(是),共 2 次;1040 % 8 = 0,到 0 号桶,比较 1024、1032 都不是,链表结束,共比较 2 次,没有找到。
+
+3. 1048 % 8 = 0,接在 0 号桶末尾:1024 → 1032 → 1048。7 个桶装 8 个订单,元素再多下去链表会越来越长,这时应该把桶数扩大到 16,按 订单号 % 16 重新分配。
+
+
+
+#### 第 16 天
+
+自测:
+
+1. 二分查找的时间复杂度是多少?100 万个元素最多比较几次?(见[二分查找](#二分查找))
+2. 数据库索引为什么用 B+ 树,而不用二叉树或哈希表?(见[树](#树))
+
+推演:二分查找。8 个订单按下单时间排好序,放在数组 a 中,下标从 0 开始。分别查找 12:42 和 12:25 下的订单,写出每一步的 low、high、mid、a[mid] 以及接下来往哪边找。
+
+```text
+下标 0 1 2 3 4 5 6 7
+a 12:03 12:08 12:15 12:21 12:30 12:34 12:42 12:56
+```
+
+规则:mid = (low + high) / 2,舍去小数;a[mid] 小于目标时 low = mid + 1,大于目标时 high = mid - 1;low > high 时停止,表示没有找到。
+
+
+参考答案
+
+查找 12:42
+
+```text
+步骤 low high mid a[mid] 判断
+1 0 7 3 12:21 12:21 < 12:42,low = 4
+2 4 7 5 12:34 12:34 < 12:42,low = 6
+3 6 7 6 12:42 相等,找到,下标 6
+```
+
+查找 12:25
+
+```text
+步骤 low high mid a[mid] 判断
+1 0 7 3 12:21 12:21 < 12:25,low = 4
+2 4 7 5 12:34 12:34 > 12:25,high = 4
+3 4 4 4 12:30 12:30 > 12:25,high = 3
+结束 4 3 low > high,没有找到
+```
+
+两次都比较了 3 次。没有找到时 low = 4,正好是 12:25 应该插入的位置(插在 12:21 和 12:30 之间)。
+
+
+
+推演:二叉查找树。1. 按 12:21、12:08、12:34、12:03、12:15、12:30、12:42、12:56 的顺序把下单时间插入一棵空的二叉查找树,画出结果,并写出查找 12:56 和 12:25 经过的节点;
+2. 如果按从早到晚的顺序 12:03、12:08……12:56 插入,树是什么样子,查找 12:56 要比较几次。
+
+规则:从根开始,比当前节点小就往左,大就往右,走到空位置时放下。
+
+
+参考答案
+
+1. 结果如下(右子树画在上方):
+
+```text
+ 12:56
+ 12:42
+ 12:34
+ 12:30
+ 12:21
+ 12:15
+ 12:08
+ 12:03
+```
+
+即根为 12:21,左子节点 12:08(其左右为 12:03、12:15),右子节点 12:34(其左右为 12:30、12:42),12:56 是 12:42 的右子节点,树高 4 层。查找 12:56:12:21 → 12:34 → 12:42 → 12:56,比较 4 次。查找 12:25:12:21(往右)→ 12:34(往左)→ 12:30(往左,左边为空),比较 3 次,没有找到。
+
+2. 每个新元素都比已有的大,总是挂在最右边,树变成一条 8 层的“链”:12:03 → 12:08 → … → 12:56,查找 12:56 要比较 8 次,与逐个查找一样。这就是实际使用红黑树、B+ 树这类能自动保持平衡的树的原因。
+
+
+
+#### 第 17 天
+
+自测:
+
+1. 常见的时间复杂度有哪些?O(n log n) 和 O(n²) 的排序在 100 万个元素上差多少?(见[时间复杂度](#时间复杂度))
+
+动手:按[时间复杂度](#时间复杂度)一节的命令生成 100 万个打乱的数,用 time sort -n 计时排序。
+
+推演:插入排序。5 个订单的金额(分)依次是 2600、3200、980、2850、1600。用插入排序从小到大排序,写出每一轮插入后的数组和这一轮比较的次数。
+
+规则:从第 2 个元素开始,每轮取出一个元素,从右往左与已排好的元素比较,比它大的元素右移一格,遇到不比它大的元素或到达最左边时放下。
+
+
+参考答案
+
+```text
+轮次 插入 结果 比较次数
+开始 2600 3200 980 2850 1600
+1 3200 2600 3200 980 2850 1600 1(2600 不比 3200 大,停)
+2 980 980 2600 3200 2850 1600 2(3200、2600 都右移,到最左边)
+3 2850 980 2600 2850 3200 1600 2(3200 右移,2600 不比 2850 大,停)
+4 1600 980 1600 2600 2850 3200 4(3200、2850、2600 右移,980 不比 1600 大,停)
+```
+
+共比较 9 次。数据本来就有序时每轮只比较 1 次,共 4 次;完全倒序时第 k 轮比较 k 次,共 1 + 2 + 3 + 4 = 10 次。
+
+
+
+推演:估算比较次数。订单数 n 分别为 1000、100 万、10 亿时,估算:
+
+1. 在有序数组中二分查找,最多比较几次;
+2. 用插入排序把完全打乱的订单排序,平均大约比较几次(按 n²/4 估算);
+3. 按每秒 10 亿次比较计算,n = 100 万时插入排序大约要多久,归并排序(约 n log₂n 次)大约要多久。
+
+
+参考答案
+
+1. 最多比较 log₂n 向下取整再加 1 次。2¹⁰ = 1024 > 1000,所以 1000 个最多 10 次;2²⁰ ≈ 105 万 > 100 万,最多 20 次;2³⁰ ≈ 10.7 亿 > 10 亿,最多 30 次。数据量每扩大 1000 倍,只多比较 10 次;
+
+2. 1000 个:1000² / 4 = 25 万次;100 万个:(10⁶)² / 4 = 2500 亿次;10 亿个:(10⁹)² / 4 = 2.5 × 10¹⁷ 次;
+
+3. 插入排序:2.5 × 10¹¹ ÷ 10⁹ = 250 秒,约 4 分钟;归并排序:10⁶ × 20 = 2 × 10⁷ 次,÷ 10⁹ = 0.02 秒。笔记附录 A11 中对 100 万个打乱的数做归并排序,实测比较了 1867 万次,与估算一致。
+
+
+
+综合题:外卖 App 要支持三种操作:按订单号查找订单、按时间从新到旧列出最近的订单、查出 12:00~12:30 之间的订单。分别用什么数据结构?时间复杂度各是多少?(见[哈希表](#哈希表)、[二分查找](#二分查找)、[树](#树))
+
+## 八、Git
+
+Git 记录代码的每一次修改,可以回到任何一个历史版本,也让多人在各自的分支上修改同一个项目,再合并到一起。后面的路线中写的代码都应该放进 Git 仓库。
+
+### 练习
+
+#### 第 18 天
+
+阅读:[集中式与分布式](Git.md#集中式与分布式)、[工作流](Git.md#工作流)、[分支实现](Git.md#分支实现)、[冲突](Git.md#冲突)、[Fast forward](Git.md#fast-forward)、[储藏(Stashing)](Git.md#储藏stashing)、[.gitignore 文件](Git.md#gitignore-文件)、[Git 命令一览](Git.md#git-命令一览)。
+
+自测:
+
+1. Git 和 SVN 有什么区别?(见[集中式与分布式](Git.md#集中式与分布式))
+2. 合并分支时出现冲突怎样解决?(见[冲突](Git.md#冲突))
+3. git stash 有什么作用?(见[储藏(Stashing)](Git.md#储藏stashing))
+
+动手:新建一个仓库,在两个分支上修改同一行后合并,手动解决冲突并提交,最后用 git log --graph 查看历史。
+
+## 九、下一步
+
+### 练习
+
+#### 第 19 天
+
+回到开头的[全景图](#一总览一次下单经过了什么),不看正文,按图中的 6 个步骤讲一遍下单的全过程,讲不清的地方回到对应的章节。
+
+综合题:从按下“提交订单”到 App 显示“下单成功”,按数据、程序、操作系统、网络、数据库、数据结构六个方面,各说出两三件发生的事。(见[一次下单经过了什么](#一总览一次下单经过了什么)及各章)
+
+之后按目标选择[Java 后端](学习路线%20-%20Java%20后端.md)或[大模型应用开发](学习路线%20-%20大模型应用开发.md),两条路线都要求先掌握一门编程语言的基础语法;离面试只有两三周时,用[面试冲刺](学习路线%20-%20面试冲刺.md)复习。
+
+## 附录:实测
+
+除特别说明外,macOS 环境为 Apple M1 Pro(10 核)、macOS 15.8;Ubuntu 环境为 Docker 中的 Ubuntu 24.04(aarch64,内核 6.10),gcc 13.3;数据库为 SQLite 3.43.2(macOS 自带)和 MySQL 8.0.44。MySQL 上的表名加了前缀,以免与其它实验冲突,输出中的表名已改回正文中的写法。
+
+### A1 字符编码与浮点数
+
+macOS 上的输出如下(macOS 的 od 列间距更宽,这里按 Ubuntu 的格式排版)。Ubuntu 上 od、awk、printf 的结果相同,镜像中没有 bc;Ubuntu 的 iconv 按 GBK 解码到第 2 个字节处报错 illegal input sequence,只输出“瀹”:
+
+```text
+$ printf '宫保鸡丁' | od -An -tx1
+ e5 ae ab e4 bf 9d e9 b8 a1 e4 b8 81
+$ printf '宫保鸡丁' | wc -c
+12
+$ printf 'Kung Pao' | od -An -tx1
+ 4b 75 6e 67 20 50 61 6f
+$ printf '宫保鸡丁' | iconv -f UTF-8 -t GBK | od -An -tx1
+ b9 ac b1 a3 bc a6 b6 a1
+$ printf '宫保鸡丁' | iconv -f GBK -t UTF-8
+瀹繚楦′竵
+$ echo 'obase=2; 1024' | bc
+10000000000
+$ printf '%x\n' 1024
+400
+$ printf '%x\n' -2850
+fffffffffffff4de
+$ awk 'BEGIN { printf "%.17g\n", 28.50 }'
+28.5
+$ awk 'BEGIN { printf "%.17g\n", 1.10 + 2.20 }'
+3.3000000000000003
+$ awk 'BEGIN { printf "%.17g\n", 3.30 }'
+3.2999999999999998
+$ awk 'BEGIN { print (1.10 + 2.20 == 3.30) }'
+0
+$ awk 'BEGIN { printf "%.17g\n", 0.1 + 0.2 }'
+0.30000000000000004
+```
+
+printf 按 8 字节输出 -2850 的补码,取低 4 字节即正文中的 ff ff f4 de。
+
+### A2 订单在内存中的字节
+
+用 C 语言定义一个订单结构,打印它在内存中的每个字节:
+
+```c
+#include
+#include
+struct order { long id; int amount; char dish[13]; };
+int main(void) {
+ struct order o = {1024, 2850, "宫保鸡丁"};
+ unsigned char *p = (unsigned char *)&o;
+ printf("sizeof(long)=%zu sizeof(int)=%zu sizeof(struct order)=%zu strlen(dish)=%zu\n",
+ sizeof(long), sizeof(int), sizeof o, strlen(o.dish));
+ for (size_t i = 0; i < sizeof o; i++) printf("%02x%s", p[i], i % 8 == 7 ? "\n" : " ");
+}
+```
+
+macOS(clang)与 Ubuntu(gcc 13.3)的输出相同:
+
+```text
+sizeof(long)=8 sizeof(int)=4 sizeof(struct order)=32 strlen(dish)=12
+00 04 00 00 00 00 00 00
+22 0b 00 00 e5 ae ab e4
+bf 9d e9 b8 a1 e4 b8 81
+00 00 00 00 00 00 00 00
+```
+
+### A3 机器指令
+
+在 Ubuntu 中编译只有一行的函数 stock = stock - 1,再反汇编:
+
+```text
+$ cat stock.c
+int stock = 30;
+void sell(void) { stock = stock - 1; }
+$ gcc -O1 -c stock.c && objdump -d stock.o
+0000000000000000 :
+ 0: 90000001 adrp x1, 0
+ 4: b9400020 ldr w0, [x1]
+ 8: 51000400 sub w0, w0, #0x1
+ c: b9000020 str w0, [x1]
+ 10: d65f03c0 ret
+$ od -A x -t x1 -N 16 /bin/ls
+000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
+```
+
+macOS 上的 /bin/ls 同时包含 x86_64 和 arm64e 两种指令,开头的 4 个字节是 ca fe ba be(多架构的 Mach-O 文件)。
+
+### A4 存储器的访问延迟
+
+内存:申请一块指定大小的内存,按 128 字节(M1 的缓存行大小)切成节点,把节点随机串成一个环,每个节点保存下一个节点的地址;然后沿着环读 2000 万次,每次读取都依赖上一次的结果,总时间除以次数即一次读取的延迟。clang -O2 编译,运行 3 次,第 1 次的输出:
+
+```text
+ 16 KB 1.0 ns
+ 64 KB 1.0 ns
+ 1024 KB 6.2 ns
+ 4096 KB 10.9 ns
+ 65536 KB 129.0 ns
+ 1048576 KB 314.2 ns
+```
+
+3 次中 64 MB 为 129~157 ns,1 GB 为 182~314 ns(1 GB 时还要频繁查找页表,所以更慢)。M1 Pro 性能核的一级数据缓存为 128 KB,二级缓存为 12 MB(sysctl 查得)。
+
+SSD:在内置 SSD 上创建 4 GB 的文件,用 fcntl(F_NOCACHE) 关闭操作系统的文件缓存,随机读取 5000 个 4 KB 的块,4 次的平均延迟:
+
+```text
+random 4KB read: 81.5 us
+random 4KB read: 68.0 us
+random 4KB read: 74.3 us
+random 4KB read: 67.4 us
+```
+
+### A5 进程、线程与系统调用
+
+macOS 上的进程数和线程数:
+
+```text
+$ sysctl -n hw.ncpu
+10
+$ top -l 1 | head -1
+Processes: 1091 total, 6 running, 1085 sleeping, 11019 threads
+```
+
+Ubuntu 中运行 A7 的模拟订单服务(进程号 7),同时发出 3 个下单请求,每个请求在服务端停留 3 秒,期间查看线程:
+
+```text
+$ ps -o pid,nlwp,cmd -p 7 # 空闲时
+ PID NLWP CMD
+ 7 1 python3 order_server.py
+$ ps -T -p 7 # 处理 3 个请求时
+ PID SPID TTY TIME CMD
+ 7 7 ? 00:00:00 python3
+ 7 15 ? 00:00:00 python3
+ 7 16 ? 00:00:00 python3
+ 7 17 ? 00:00:00 python3
+```
+
+用 strace -f -p 7 跟踪进程 7 及其线程,再发出一个下单请求(每行开头是线程号,省略了时间戳和部分参数):
+
+```text
+7 accept4(3, {sa_family=AF_INET, sin_port=htons(36656), sin_addr=inet_addr("127.0.0.1")}, [16], SOCK_CLOEXEC) = 4
+28 recvfrom(4, "POST /api/orders HTTP/1.1\r\nHost:"..., 8192, 0, NULL, NULL) = 180
+28 openat(AT_FDCWD, "orders.log", O_WRONLY|O_CREAT|O_APPEND|O_CLOEXEC, 0666) = 5
+28 write(5, "order 1024 user 1001 amount 2850"..., 33) = 33
+28 close(5) = 0
+28 write(2, "127.0.0.1 - - [28/Oct/2026 16:27"..., 71) = 71
+28 sendto(4, "HTTP/1.1 201 Created\r\nServer: Ba"..., 164, 0, NULL, 0) = 164
+28 sendto(4, "{\"orderId\": 1024, \"status\": \"CRE"..., 57, 0, NULL, 0) = 57
+28 recvfrom(4, "", 8192, 0, NULL, NULL) = 0
+28 close(4) = 0
+```
+
+write(2, ...) 是服务程序在标准错误上打印访问日志,正文的图中省略了这一行。其它命令:
+
+```text
+$ strace -e trace=write echo "order 1024"
+write(1, "order 1024\n", 11order 1024
+) = 11
+$ strace -c cat /etc/hostname | tail -1
+100.00 0.000874 21 41 1 total
+```
+
+### A6 DNS 与往返时间
+
+macOS,家庭宽带,DNS 服务器为路由器 192.168.1.1:
+
+```text
+$ dig www.baidu.com
+;; ANSWER SECTION:
+www.baidu.com. 600 IN CNAME www.a.shifen.com.
+www.a.shifen.com. 600 IN A 183.2.172.177
+;; Query time: 5 msec
+;; SERVER: 192.168.1.1#53(192.168.1.1)
+$ ping -c 4 www.baidu.com
+round-trip min/avg/max/stddev = 9.310/9.655/9.863/0.210 ms
+```
+
+curl -4 访问 https://www.baidu.com 5 次,间隔 1 秒:
+
+```text
+dns 0.004013 tcp 0.015670 tls 0.044868 first_byte 0.057040 total 0.057136 ip 183.2.172.177:443 http 200
+dns 0.002221 tcp 0.012144 tls 0.040571 first_byte 0.053718 total 0.053823 ip 183.2.172.177:443 http 200
+dns 0.006563 tcp 0.015096 tls 0.041829 first_byte 0.053008 total 0.053110 ip 183.2.172.177:443 http 200
+dns 0.002281 tcp 0.011717 tls 0.041322 first_byte 0.053568 total 0.053693 ip 183.2.172.177:443 http 200
+dns 0.002117 tcp 0.011890 tls 0.041010 first_byte 0.054700 total 0.054805 ip 183.2.172.177:443 http 200
+```
+
+curl -v 显示这次连接使用 TLS 1.2,服务器在 ALPN 中选择了 http/1.1。
+
+### A7 HTTP 请求与 TCP 报文
+
+模拟的订单服务用 Python 标准库编写,每个请求由一个新线程处理:POST /api/orders 向 orders.log 追加一行并返回 201,GET /api/orders/1024 返回订单,其它路径返回 404。
+
+```python
+import json, time
+from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
+
+class Handler(BaseHTTPRequestHandler):
+ protocol_version = "HTTP/1.1"
+ def reply(self, code, body):
+ data = json.dumps(body, ensure_ascii=False).encode()
+ self.send_response(code)
+ self.send_header("Content-Type", "application/json; charset=utf-8")
+ self.send_header("Content-Length", str(len(data)))
+ self.end_headers()
+ self.wfile.write(data)
+ def do_POST(self):
+ req = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
+ time.sleep(float(self.headers.get("X-Delay", "0"))) # A5 中用来延长处理时间
+ with open("orders.log", "a") as f:
+ f.write(f"order 1024 user {req['userId']} amount 2850\n")
+ self.reply(201, {"orderId": 1024, "status": "CREATED", "amount": "28.50"})
+ def do_GET(self):
+ if self.path == "/api/orders/1024":
+ self.reply(200, {"orderId": 1024, "dish": "宫保鸡丁", "amount": "28.50", "status": "CREATED"})
+ else:
+ self.reply(404, {"error": "order not found"})
+
+ThreadingHTTPServer(("0.0.0.0", 8080), Handler).serve_forever()
+```
+
+curl 的输出见正文,查询不存在的订单:
+
+```text
+$ curl -i http://127.0.0.1:8080/api/orders/9999
+HTTP/1.1 404 Not Found
+Server: BaseHTTP/0.6 Python/3.12.3
+Date: Wed, 28 Oct 2026 16:27:24 GMT
+Content-Type: application/json; charset=utf-8
+Content-Length: 28
+
+{"error": "order not found"}
+```
+
+发送下单请求的同时用 tcpdump -i lo -nn -S 'tcp port 8080' 抓包(-S 显示原始序号,省略了 TCP 选项):
+
+```text
+16:27:24.090458 IP 127.0.0.1.36656 > 127.0.0.1.8080: Flags [S], seq 3987857382, win 65495, length 0
+16:27:24.090512 IP 127.0.0.1.8080 > 127.0.0.1.36656: Flags [S.], seq 3583450070, ack 3987857383, win 65483, length 0
+16:27:24.090533 IP 127.0.0.1.36656 > 127.0.0.1.8080: Flags [.], ack 3583450071, win 512, length 0
+16:27:24.090656 IP 127.0.0.1.36656 > 127.0.0.1.8080: Flags [P.], seq 3987857383:3987857563, ack 3583450071, win 512, length 180: HTTP: POST /api/orders HTTP/1.1
+16:27:24.090660 IP 127.0.0.1.8080 > 127.0.0.1.36656: Flags [.], ack 3987857563, win 511, length 0
+16:27:24.102776 IP 127.0.0.1.8080 > 127.0.0.1.36656: Flags [P.], seq 3583450071:3583450235, ack 3987857563, win 512, length 164: HTTP: HTTP/1.1 201 Created
+16:27:24.102798 IP 127.0.0.1.36656 > 127.0.0.1.8080: Flags [.], ack 3583450235, win 511, length 0
+16:27:24.103215 IP 127.0.0.1.8080 > 127.0.0.1.36656: Flags [P.], seq 3583450235:3583450292, ack 3987857563, win 512, length 57: HTTP
+16:27:24.103227 IP 127.0.0.1.36656 > 127.0.0.1.8080: Flags [.], ack 3583450292, win 511, length 0
+16:27:24.103583 IP 127.0.0.1.36656 > 127.0.0.1.8080: Flags [F.], seq 3987857563, ack 3583450292, win 512, length 0
+```
+
+服务器确认请求(16:27:24.090660)到发出响应(16:27:24.102776)之间约 12 ms,是正文图中的处理时间。
+
+### A8 建表、插入与查询
+
+正文中的建表、插入、查询、修改语句,最后再插入一个订单号重复的订单,统计行数。SQLite(sqlite3 -header -column shop.db < basic.sql):
+
+```text
+id user_id amount status
+---- ------- ------ -------
+1024 1001 2850 CREATED
+id status
+---- ------
+1024 PAID
+Runtime error near line 21: UNIQUE constraint failed: orders.id (19)
+COUNT(*)
+--------
+2
+```
+
+MySQL 8.0.44 执行同样的文件(mysql --table -f):
+
+```text
++------+---------+--------+---------+
+| id | user_id | amount | status |
++------+---------+--------+---------+
+| 1024 | 1001 | 2850 | CREATED |
++------+---------+--------+---------+
++------+--------+
+| id | status |
++------+--------+
+| 1024 | PAID |
++------+--------+
+ERROR 1062 (23000) at line 21: Duplicate entry '1024' for key 'orders.PRIMARY'
++----------+
+| COUNT(*) |
++----------+
+| 2 |
++----------+
+```
+
+### A9 索引
+
+SQLite,用正文中的语句生成 100 万行,查询用户 1001 的订单数,各执行 3 次(.timer on,real 为耗时秒数):
+
+```text
+`--SCAN orders
+10
+Run Time: real 0.038 user 0.030082 sys 0.007728
+Run Time: real 0.037 user 0.029338 sys 0.007746
+Run Time: real 0.037 user 0.029297 sys 0.007269
+Run Time: real 0.304 user 0.255028 sys 0.036108 -- CREATE INDEX idx_user
+`--SEARCH orders USING INDEX idx_user (user_id=?)
+10
+Run Time: real 0.000 user 0.000022 sys 0.000019
+Run Time: real 0.000 user 0.000009 sys 0.000007
+Run Time: real 0.000 user 0.000008 sys 0.000006
+```
+
+用 dbstat 虚拟表查看索引的页(页大小 4096 字节)。SQLite 的索引是 B 树,非叶子页中也存放条目;最深的页路径为 /000/000/,即根页、中间页、叶子页共 3 层:
+
+```text
+pagetype pages cells avg_cells
+internal 12 2852 237.7
+leaf 2853 997148 349.5
+根页 / 的条目数 10,中间页 /000/、/001/ 的条目数各 271
+```
+
+用户 1001 的订单号为 79000、179000……979000。
+
+MySQL 8.0.44 生成同样的数据(递归深度上限需调大:SET SESSION cte_max_recursion_depth = 1000000):
+
+```text
+没有索引:type ALL,rows 996761
+有索引: type ref,key idx_user,rows 10
+SHOW PROFILES:没有索引 0.290、0.102、0.111 秒(第 1 次包含读盘);有索引 0.00061、0.00028、0.00029 秒
+```
+
+### A10 事务
+
+dishes 表中宫保鸡丁的库存为 30。用管道把语句交给 sqlite3(或 mysql 客户端),1 秒后用 kill -9 结束客户端进程,再查询库存和订单数:
+
+```bash
+(echo "BEGIN; UPDATE dishes SET stock = stock - 1 WHERE id = 17 AND stock > 0;"; sleep 3) | sqlite3 tx.db &
+sleep 1; kill -9 $(pgrep sqlite3)
+sqlite3 tx.db "SELECT stock FROM dishes WHERE id = 17; SELECT COUNT(*) FROM orders;"
+```
+
+SQLite 与 MySQL 的结果相同(MySQL 中客户端被结束后连接断开,服务器回滚未提交的事务):
+
+```text
+# 1. 不用事务:扣完库存后崩溃
+stock 29,orders 0
+# 2. 用事务:扣完库存后崩溃
+stock 30,orders 0
+# 3. 用事务:两条语句都成功后 COMMIT
+stock 29,orders 1
+# 4. BEGIN、UPDATE 后 ROLLBACK
+事务中 stock 28,回滚后 stock 29
+# 5. 库存为 0 时执行 UPDATE ... AND stock > 0
+影响行数 0(SQLite 的 changes()、MySQL 的 ROW_COUNT())
+```
+
+### A11 比较次数
+
+用 Python 统计在订单号 1~1000000 中查找和排序的比较次数(只计数,不计时):顺序查找对每个订单号分别统计;二分查找每次与中间元素比较记 1 次;哈希表用拉链法、2²⁰ 个桶,订单号取 100 万个 1~10¹² 之间的随机数;归并排序对打乱的 100 万个数实测;插入排序在 1 万个数上实测,验证与 n²/4 一致后按 n²/4 估算 100 万个。
+
+```text
+顺序查找 找第 1 个: 1 找最后一个: 1000000 平均: 500000.5
+二分查找 最多: 20 平均: 18.95 找不存在的 0: 19 log2(N) = 19.93
+哈希表(随机订单号) 平均比较: 1.476 最长链: 8
+归并排序 N=100万 比较: 18673733 n*log2(n) = 19931568
+插入排序 N=1万 实测比较: 24880276 n^2/4 = 25000000
+插入排序 N=100万 估算: n^2/4 = 250000000000
+```
+
+排序 100 万个数:
+
+```text
+$ seq -f '%.0f' 1000000 | sort -R > orders.txt
+$ time sort -n orders.txt > sorted.txt # macOS
+sort -n orders.txt > sorted.txt 0.50s user 0.02s system 99% cpu 0.527 total
+$ time sort -n o.txt > s.txt # Ubuntu(数据由 shuf 打乱)
+real 0m0.200s
+```
+
+macOS 的 seq 默认把 1000000 输出为 1e+06,所以加了 -f '%.0f'。
+
+## 参考资料
+
+- Bryant R E, O'Hallaron D R. 深入理解计算机系统(原书第 3 版)[M]. 机械工业出版社, 2016.
+- Tanenbaum A S, Bos H. 现代操作系统(原书第 4 版)[M]. 机械工业出版社, 2017.
+- Kurose J F, Ross K W. 计算机网络:自顶向下方法(原书第 7 版)[M]. 机械工业出版社, 2018.
+- Sedgewick R, Wayne K. 算法(第 4 版)[M]. 人民邮电出版社, 2012.
+- [RFC 3629: UTF-8, a transformation format of ISO 10646](https://www.rfc-editor.org/rfc/rfc3629)
+- [Floating-Point Arithmetic: Issues and Limitations,Python 文档](https://docs.python.org/3/tutorial/floatingpoint.html)
+- [MySQL 8.0 Reference Manual: Fixed-Point Types (DECIMAL, NUMERIC)](https://dev.mysql.com/doc/refman/8.0/en/fixed-point-types.html)
+- [syscalls(2),Linux manual page](https://man7.org/linux/man-pages/man2/syscalls.2.html)
+- [ps(1),Linux manual page](https://man7.org/linux/man-pages/man1/ps.1.html)
+- [pthreads(7),Linux manual page](https://man7.org/linux/man-pages/man7/pthreads.7.html)
+- [RFC 791: Internet Protocol](https://www.rfc-editor.org/rfc/rfc791)
+- [RFC 1918: Address Allocation for Private Internets](https://www.rfc-editor.org/rfc/rfc1918)
+- [RFC 1034: Domain Names - Concepts and Facilities](https://www.rfc-editor.org/rfc/rfc1034)
+- [RFC 768: User Datagram Protocol](https://www.rfc-editor.org/rfc/rfc768)
+- [RFC 9293: Transmission Control Protocol (TCP)](https://www.rfc-editor.org/rfc/rfc9293)
+- [RFC 9110: HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110)
+- [RFC 9112: HTTP/1.1](https://www.rfc-editor.org/rfc/rfc9112)
+- [RFC 5737: IPv4 Address Blocks Reserved for Documentation](https://www.rfc-editor.org/rfc/rfc5737)
+- [RFC 2606: Reserved Top Level DNS Names](https://www.rfc-editor.org/rfc/rfc2606)
+- [SQLite: Transaction](https://www.sqlite.org/lang_transaction.html)
+- [SQLite: Atomic Commit In SQLite](https://www.sqlite.org/atomiccommit.html)
+- [SQLite: Query Planning](https://www.sqlite.org/queryplanner.html)
+- [SQLite: EXPLAIN QUERY PLAN](https://www.sqlite.org/eqp.html)
+- [SQLite: The DBSTAT Virtual Table](https://www.sqlite.org/dbstat.html)
+- [MySQL 8.0 Reference Manual: START TRANSACTION, COMMIT, and ROLLBACK Statements](https://dev.mysql.com/doc/refman/8.0/en/commit.html)
+- [MySQL 8.0 Reference Manual: How MySQL Uses Indexes](https://dev.mysql.com/doc/refman/8.0/en/mysql-indexes.html)
+- [Algorithms, 4th Edition: Elementary Sorts](https://algs4.cs.princeton.edu/21elementary/)
+- [Algorithms, 4th Edition: Symbol Tables(二分查找)](https://algs4.cs.princeton.edu/31elementary/)
+- [Algorithms, 4th Edition: Binary Search Trees](https://algs4.cs.princeton.edu/32bst/)
+- [Algorithms, 4th Edition: Hash Tables](https://algs4.cs.princeton.edu/34hash/)