diff --git a/README.md b/README.md
index 2270c7ba..0fe3b6d8 100644
--- a/README.md
+++ b/README.md
@@ -8,9 +8,9 @@
-| 算法 | 操作系统 | 网络 |面向对象| 数据库 | Java |系统设计| 工具 | 后记 |
-| :---: | :----: | :---: | :----: | :----: | :----: | :----: | :----: | :----: |
-| [:pencil2:](#pencil2-算法) | [:computer:](#computer-操作系统) | [:cloud:](#cloud-网络) | [:art:](#art-面向对象) | [:floppy_disk:](#floppy_disk-数据库) |[:coffee:](#coffee-java)| [:bulb:](#bulb-系统设计) |[:wrench:](#wrench-工具)| [:memo:](#memo-后记)|
+| 学习路线 | 算法 | 操作系统 | 网络 |面向对象| 数据库 | Java |系统设计| 工具 | 后记 |
+| :---: | :---: | :----: | :---: | :----: | :----: | :----: | :----: | :----: | :----: |
+| [:rocket:](#rocket-学习路线) | [:pencil2:](#pencil2-算法) | [:computer:](#computer-操作系统) | [:cloud:](#cloud-网络) | [:art:](#art-面向对象) | [:floppy_disk:](#floppy_disk-数据库) |[:coffee:](#coffee-java)| [:bulb:](#bulb-系统设计) |[:wrench:](#wrench-工具)| [:memo:](#memo-后记)|
@@ -20,6 +20,15 @@
+## :rocket: 学习路线
+
+每条路线把笔记按天排好,每天 1~2 小时:读一段笔记,回答几道面试中的真实问题,做一个动手练习;后三条路线每天还有一道算法题。
+
+- [零基础](https://github.com/CyC2018/CS-Notes/blob/master/notes/学习路线%20-%20零基础.md):没有学过计算机课程时从这里开始,用一次外卖下单讲清程序、操作系统、网络、数据库和数据结构,不需要会编程;
+- [Java 后端](https://github.com/CyC2018/CS-Notes/blob/master/notes/学习路线%20-%20Java%20后端.md):会 Java 语法以后,系统准备后端面试,依次学 Java、MySQL、Redis、网络、操作系统、Spring、分布式和系统设计;
+- [大模型应用开发](https://github.com/CyC2018/CS-Notes/blob/master/notes/学习路线%20-%20大模型应用开发.md):准备 RAG、Agent 方向的岗位,从 Transformer 的原理学到 Agent 和工程实践;
+- [面试冲刺](https://github.com/CyC2018/CS-Notes/blob/master/notes/学习路线%20-%20面试冲刺.md):两三周后就要面试时,每天复习 3 个最常考的知识点。
+
## :pencil2: 算法
- [剑指 Offer 题解](https://github.com/CyC2018/CS-Notes/blob/master/notes/剑指%20Offer%20题解%20-%20目录.md)
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/学习路线 - Java 后端.md b/notes/学习路线 - Java 后端.md
new file mode 100644
index 00000000..1c445180
--- /dev/null
+++ b/notes/学习路线 - Java 后端.md
@@ -0,0 +1,1254 @@
+# 学习路线 - Java 后端
+
+* [学习路线 - Java 后端](#学习路线---java-后端)
+ * [一、说明](#一说明)
+ * [二、开始之前](#二开始之前)
+ * [三、每天的安排](#三每天的安排)
+ * [四、Java 基础](#四java-基础)
+ * [第 1 天:数据类型与 String](#第-1-天数据类型与-string)
+ * [第 2 天:常量池与运算](#第-2-天常量池与运算)
+ * [第 3 天:关键字与 Object 的方法](#第-3-天关键字与-object-的方法)
+ * [第 4 天:继承与多态](#第-4-天继承与多态)
+ * [第 5 天:反射、异常、泛型与新特性](#第-5-天反射异常泛型与新特性)
+ * [五、Java 容器](#五java-容器)
+ * [第 6 天:集合与 List](#第-6-天集合与-list)
+ * [第 7 天:HashMap](#第-7-天hashmap)
+ * [第 8 天:ConcurrentHashMap 与 LinkedHashMap](#第-8-天concurrenthashmap-与-linkedhashmap)
+ * [六、Java 并发](#六java-并发)
+ * [第 9 天:线程与线程池](#第-9-天线程与线程池)
+ * [第 10 天:中断与互斥同步](#第-10-天中断与互斥同步)
+ * [第 11 天:线程协作与线程状态](#第-11-天线程协作与线程状态)
+ * [第 12 天:J.U.C 的常用组件](#第-12-天juc-的常用组件)
+ * [第 13 天:Java 内存模型与 volatile](#第-13-天java-内存模型与-volatile)
+ * [第 14 天:线程安全与 CAS](#第-14-天线程安全与-cas)
+ * [第 15 天:ThreadLocal 与锁优化](#第-15-天threadlocal-与锁优化)
+ * [七、Java 虚拟机](#七java-虚拟机)
+ * [第 16 天:运行时数据区域](#第-16-天运行时数据区域)
+ * [第 17 天:对象存活与垃圾收集算法](#第-17-天对象存活与垃圾收集算法)
+ * [第 18 天:垃圾收集器](#第-18-天垃圾收集器)
+ * [第 19 天:三色标记与内存分配](#第-19-天三色标记与内存分配)
+ * [第 20 天:类加载机制](#第-20-天类加载机制)
+ * [第 21 天:线上问题排查](#第-21-天线上问题排查)
+ * [八、MySQL](#八mysql)
+ * [第 22 天:事务、并发一致性与锁](#第-22-天事务并发一致性与锁)
+ * [第 23 天:隔离级别与 MVCC](#第-23-天隔离级别与-mvcc)
+ * [第 24 天:Next-Key Lock 与死锁](#第-24-天next-key-lock-与死锁)
+ * [第 25 天:B+ 树与索引的类型](#第-25-天b-树与索引的类型)
+ * [第 26 天:索引的使用](#第-26-天索引的使用)
+ * [第 27 天:查询性能优化](#第-27-天查询性能优化)
+ * [第 28 天:存储引擎、切分与复制](#第-28-天存储引擎切分与复制)
+ * [第 29 天:日志](#第-29-天日志)
+ * [九、Redis 与缓存](#九redis-与缓存)
+ * [第 30 天:Redis 的数据类型](#第-30-天redis-的数据类型)
+ * [第 31 天:使用场景与底层结构](#第-31-天使用场景与底层结构)
+ * [第 32 天:过期、淘汰、持久化与事务](#第-32-天过期淘汰持久化与事务)
+ * [第 33 天:线程模型、复制与集群](#第-33-天线程模型复制与集群)
+ * [第 34 天:缓存的位置与三种问题](#第-34-天缓存的位置与三种问题)
+ * [第 35 天:缓存一致性与一致性哈希](#第-35-天缓存一致性与一致性哈希)
+ * [十、计算机网络](#十计算机网络)
+ * [第 36 天:TCP 的连接管理](#第-36-天tcp-的连接管理)
+ * [第 37 天:可靠传输与拥塞控制](#第-37-天可靠传输与拥塞控制)
+ * [第 38 天:HTTP 基础](#第-38-天http-基础)
+ * [第 39 天:连接管理与 Cookie](#第-39-天连接管理与-cookie)
+ * [第 40 天:缓存、WebSocket 与跨域](#第-40-天缓存websocket-与跨域)
+ * [第 41 天:HTTPS](#第-41-天https)
+ * [第 42 天:HTTP 的版本、GET 与 POST、DNS](#第-42-天http-的版本get-与-postdns)
+ * [第 43 天:从输入 URL 到显示页面](#第-43-天从输入-url-到显示页面)
+ * [十一、操作系统](#十一操作系统)
+ * [第 44 天:进程与线程](#第-44-天进程与线程)
+ * [第 45 天:系统调用、进程同步与通信](#第-45-天系统调用进程同步与通信)
+ * [第 46 天:死锁](#第-46-天死锁)
+ * [第 47 天:内存管理](#第-47-天内存管理)
+ * [第 48 天:I/O 多路复用](#第-48-天io-多路复用)
+ * [十二、Spring 与设计模式](#十二spring-与设计模式)
+ * [第 49 天:IoC 与 Bean](#第-49-天ioc-与-bean)
+ * [第 50 天:AOP 与事务](#第-50-天aop-与事务)
+ * [第 51 天:Spring Boot、Spring MVC 与 MyBatis](#第-51-天spring-bootspring-mvc-与-mybatis)
+ * [第 52 天:设计模式](#第-52-天设计模式)
+ * [十三、分布式与消息队列](#十三分布式与消息队列)
+ * [第 53 天:微服务](#第-53-天微服务)
+ * [第 54 天:分布式锁](#第-54-天分布式锁)
+ * [第 55 天:分布式事务与 CAP](#第-55-天分布式事务与-cap)
+ * [第 56 天:RPC、分布式 ID 与幂等](#第-56-天rpc分布式-id-与幂等)
+ * [第 57 天:消息队列的作用与可靠性](#第-57-天消息队列的作用与可靠性)
+ * [第 58 天:顺序、积压与延迟消息](#第-58-天顺序积压与延迟消息)
+ * [第 59 天:Kafka](#第-59-天kafka)
+ * [十四、系统设计](#十四系统设计)
+ * [第 60 天:性能与可用性](#第-60-天性能与可用性)
+ * [第 61 天:秒杀与抢红包](#第-61-天秒杀与抢红包)
+ * [第 62 天:海量数据、短链与 Feed 流](#第-62-天海量数据短链与-feed-流)
+ * [第 63 天:高并发系统的设计](#第-63-天高并发系统的设计)
+ * [第 64 天:Docker](#第-64-天docker)
+ * [第 65 天:Kubernetes](#第-65-天kubernetes)
+
+
+
+## 一、说明
+
+适合会 Java 语法、准备 Java 后端面试的读者,每天约 1~1.5 小时;没有计算机基础的先学[零基础](学习路线%20-%20零基础.md)。
+
+每天先读完阅读范围,再不看笔记回答自测题,答完打开链接核对。选读可以跳过;算法题先独立做 20~30 分钟,做不出来再看题解。
+
+## 二、开始之前
+
+本路线不讲语法。开始前先确认能独立写出下面 5 个程序:
+
+1. 定义订单类 Order(订单号、用户 ID、以分为单位的金额、状态),把 3 个订单放进 List,按金额从高到低排序后打印;
+2. 用 HashMap 统计一组菜名中每道菜的份数,按份数从多到少输出;
+3. 逐行读取 orders.txt,每行是“订单号,金额”,累加总金额,格式不对的行打印行号后跳过;
+4. 用枚举表示订单状态 CREATED、PAID、SHIPPED、REFUNDED,写一个方法判断两个状态之间能否转换;
+5. 定义接口 Discount 和两个实现类(满 30 减 5、打九折),分别计算同一笔订单优惠后的金额。
+
+写不出来的,先补对应的语法:类与 List 排序、Map、文件读写与异常、枚举与 switch、接口与多态。
+
+## 三、每天的安排
+
+| 天 | 主题 | 阅读 | 算法 |
+| :-: | --- | --- | --- |
+| | **Java 基础** | | |
+| [1](#第-1-天数据类型与-string) | 数据类型与 String | Java 基础:数据类型~String, StringBuffer and StringBuilder | 1. 两数之和 |
+| [2](#第-2-天常量池与运算) | 常量池与运算 | Java 基础:String Pool~运算 | 128. 最长连续序列 |
+| [3](#第-3-天关键字与-object-的方法) | 关键字与 Object 的方法 | Java 基础:关键字~Object 通用方法 | 560. 和为 K 的子数组 |
+| [4](#第-4-天继承与多态) | 继承与多态 | 面向对象思想:三大特性;Java 基础:继承 | 54. 螺旋矩阵 |
+| [5](#第-5-天反射异常泛型与新特性) | 反射、异常、泛型与新特性 | Java 基础:反射~特性 | 56. 合并区间 |
+| | **Java 容器** | | |
+| [6](#第-6-天集合与-list) | 集合与 List | Java 容器:集合概览~CopyOnWriteArrayList | 88. 合并两个有序数组 |
+| [7](#第-7-天hashmap) | HashMap | Java 容器:HashMap | 15. 三数之和 |
+| [8](#第-8-天concurrenthashmap-与-linkedhashmap) | ConcurrentHashMap 与 LinkedHashMap | Java 容器:ConcurrentHashMap~WeakHashMap | 11. 盛最多水的容器 |
+| | **Java 并发** | | |
+| [9](#第-9-天线程与线程池) | 线程与线程池 | Java 并发:使用线程~基础线程机制 | 3. 无重复字符的最长子串 |
+| [10](#第-10-天中断与互斥同步) | 中断与互斥同步 | Java 并发:中断~互斥同步 | 415. 字符串相加 |
+| [11](#第-11-天线程协作与线程状态) | 线程协作与线程状态 | Java 并发:线程之间的协作~线程状态 | 165. 比较版本号 |
+| [12](#第-12-天juc-的常用组件) | J.U.C 的常用组件 | Java 并发:J.U.C - AQS~J.U.C - 其它组件 | 206. 反转链表 |
+| [13](#第-13-天java-内存模型与-volatile) | Java 内存模型与 volatile | Java 并发:线程不安全示例~Java 内存模型 | 21. 合并两个有序链表 |
+| [14](#第-14-天线程安全与-cas) | 线程安全与 CAS | Java 并发:线程安全~非阻塞同步 | 141. 环形链表 |
+| [15](#第-15-天threadlocal-与锁优化) | ThreadLocal 与锁优化 | Java 并发:无同步方案~多线程开发良好的实践 | 160. 相交链表 |
+| | **Java 虚拟机** | | |
+| [16](#第-16-天运行时数据区域) | 运行时数据区域 | Java 虚拟机:运行时数据区域 | 92. 反转链表 II |
+| [17](#第-17-天对象存活与垃圾收集算法) | 对象存活与垃圾收集算法 | Java 虚拟机:垃圾收集~Stop The World | 143. 重排链表 |
+| [18](#第-18-天垃圾收集器) | 垃圾收集器 | Java 虚拟机:垃圾收集器 | 19. 删除链表的倒数第 N 个结点 |
+| [19](#第-19-天三色标记与内存分配) | 三色标记与内存分配 | Java 虚拟机:三色标记~内存分配与回收策略 | 82. 删除排序链表中的重复元素 II |
+| [20](#第-20-天类加载机制) | 类加载机制 | Java 虚拟机:类加载机制 | 142. 环形链表 II |
+| [21](#第-21-天线上问题排查) | 线上问题排查 | Java 虚拟机:线上问题排查;Linux:线上问题排查 | 148. 排序链表 |
+| | **MySQL** | | |
+| [22](#第-22-天事务并发一致性与锁) | 事务、并发一致性与锁 | 数据库系统原理:事务~封锁 | 2. 两数相加 |
+| [23](#第-23-天隔离级别与-mvcc) | 隔离级别与 MVCC | 数据库系统原理:隔离级别~多版本并发控制 | 146. LRU 缓存 |
+| [24](#第-24-天next-key-lock-与死锁) | Next-Key Lock 与死锁 | 数据库系统原理:Next-Key Locks;计算机操作系统 - 死锁:MySQL 死锁 | 20. 有效的括号 |
+| [25](#第-25-天b-树与索引的类型) | B+ 树与索引的类型 | MySQL:B+ Tree 原理~索引的优点 | 232. 用栈实现队列 |
+| [26](#第-26-天索引的使用) | 索引的使用 | MySQL:索引的使用条件~索引失效 | 155. 最小栈 |
+| [27](#第-27-天查询性能优化) | 查询性能优化 | MySQL:查询性能优化 | 394. 字符串解码 |
+| [28](#第-28-天存储引擎切分与复制) | 存储引擎、切分与复制 | MySQL:存储引擎~MySQL 复制 | 739. 每日温度 |
+| [29](#第-29-天日志) | 日志 | MySQL:MySQL 的日志 | 704. 二分查找 |
+| | **Redis 与缓存** | | |
+| [30](#第-30-天redis-的数据类型) | Redis 的数据类型 | Redis:Redis 概述~Redis 的数据类型 | 33. 搜索旋转排序数组 |
+| [31](#第-31-天使用场景与底层结构) | 使用场景与底层结构 | Redis:Redis 的使用场景~Redis 的数据结构 | 34. 在排序数组中查找元素的第一个和最后一个位置 |
+| [32](#第-32-天过期淘汰持久化与事务) | 过期、淘汰、持久化与事务 | Redis:Redis 与 Memcached~Redis 事务 | 153. 寻找旋转排序数组中的最小值 |
+| [33](#第-33-天线程模型复制与集群) | 线程模型、复制与集群 | Redis:Redis 的事件~Redis 分片 | 215. 数组中的第 K 个最大元素 |
+| [34](#第-34-天缓存的位置与三种问题) | 缓存的位置与三种问题 | 缓存:缓存特征~缓存雪崩 | 347. 前 K 个高频元素 |
+| [35](#第-35-天缓存一致性与一致性哈希) | 缓存一致性与一致性哈希 | 缓存:缓存一致性~LRU | 912. 排序数组(手写快速排序) |
+| | **计算机网络** | | |
+| [36](#第-36-天tcp-的连接管理) | TCP 的连接管理 | 计算机网络 - 传输层:UDP 和 TCP 的特点~TCP 的四次挥手 | 101. 对称二叉树 |
+| [37](#第-37-天可靠传输与拥塞控制) | 可靠传输与拥塞控制 | 计算机网络 - 传输层:TCP 可靠传输~在 UDP 上实现可靠传输 | 104. 二叉树的最大深度 |
+| [38](#第-38-天http-基础) | HTTP 基础 | HTTP:HTTP 基础概念~HTTP 首部 | 102. 二叉树的层序遍历 |
+| [39](#第-39-天连接管理与-cookie) | 连接管理与 Cookie | HTTP:连接管理~Cookie | 103. 二叉树的锯齿形层序遍历 |
+| [40](#第-40-天缓存websocket-与跨域) | 缓存、WebSocket 与跨域 | HTTP:HTTP 缓存~跨域 | 236. 二叉树的最近公共祖先 |
+| [41](#第-41-天https) | HTTPS | HTTP:HTTPS | 199. 二叉树的右视图 |
+| [42](#第-42-天http-的版本get-与-postdns) | HTTP 的版本、GET 与 POST、DNS | HTTP:HTTP/1.1 新特性~GET 和 POST 比较;计算机网络 - 应用层:域名系统 | 105. 从前序与中序遍历序列构造二叉树 |
+| [43](#第-43-天从输入-url-到显示页面) | 从输入 URL 到显示页面 | 计算机网络 - 应用层:Web 页面请求过程;攻击技术:跨站脚本攻击~SQL 注入攻击 | 98. 验证二叉搜索树 |
+| | **操作系统** | | |
+| [44](#第-44-天进程与线程) | 进程与线程 | 计算机操作系统 - 进程管理:进程与线程~进程状态的切换 | 200. 岛屿数量 |
+| [45](#第-45-天系统调用进程同步与通信) | 系统调用、进程同步与通信 | 计算机操作系统 - 概述:系统调用;计算机操作系统 - 进程管理:进程调度算法~进程通信 | 46. 全排列 |
+| [46](#第-46-天死锁) | 死锁 | 计算机操作系统 - 死锁:必要条件~程序中的死锁 | 93. 复原 IP 地址 |
+| [47](#第-47-天内存管理) | 内存管理 | 计算机操作系统 - 内存管理:虚拟内存~分页与分段的比较 | 22. 括号生成 |
+| [48](#第-48-天io-多路复用) | I/O 多路复用 | Socket:I/O 模型~I/O 复用 | 78. 子集 |
+| | **Spring 与设计模式** | | |
+| [49](#第-49-天ioc-与-bean) | IoC 与 Bean | Spring:IoC 与 Bean | 39. 组合总和 |
+| [50](#第-50-天aop-与事务) | AOP 与事务 | Spring:AOP~事务 | 121. 买卖股票的最佳时机 |
+| [51](#第-51-天spring-bootspring-mvc-与-mybatis) | Spring Boot、Spring MVC 与 MyBatis | Spring:Spring Boot 自动配置~Spring 中的设计模式 | 70. 爬楼梯 |
+| [52](#第-52-天设计模式) | 设计模式 | 设计模式 - 目录:设计模式前言;设计模式 - 单例:单例(Singleton);设计模式 - 策略:策略(Strategy);面向对象思想:设计原则 | 53. 最大子数组和 |
+| | **分布式与消息队列** | | |
+| [53](#第-53-天微服务) | 微服务 | 微服务:从单体到微服务~网关 | 5. 最长回文子串 |
+| [54](#第-54-天分布式锁) | 分布式锁 | 分布式:分布式锁 | 300. 最长递增子序列 |
+| [55](#第-55-天分布式事务与-cap) | 分布式事务与 CAP | 分布式:分布式事务~BASE | 1143. 最长公共子序列 |
+| [56](#第-56-天rpc分布式-id-与幂等) | RPC、分布式 ID 与幂等 | 分布式:RPC~幂等 | 322. 零钱兑换 |
+| [57](#第-57-天消息队列的作用与可靠性) | 消息队列的作用与可靠性 | 消息队列:消息模型~消息队列的可靠性 | 64. 最小路径和 |
+| [58](#第-58-天顺序积压与延迟消息) | 顺序、积压与延迟消息 | 消息队列:顺序性~延迟消息与死信队列 | 198. 打家劫舍 |
+| [59](#第-59-天kafka) | Kafka | 消息队列:Kafka | 42. 接雨水 |
+| | **系统设计** | | |
+| [60](#第-60-天性能与可用性) | 性能与可用性 | 系统设计基础:性能~安全性 | 239. 滑动窗口最大值 |
+| [61](#第-61-天秒杀与抢红包) | 秒杀与抢红包 | 系统设计基础:秒杀系统 | 76. 最小覆盖子串 |
+| [62](#第-62-天海量数据短链与-feed-流) | 海量数据、短链与 Feed 流 | 系统设计基础:海量数据处理~Feed 流 | 25. K 个一组翻转链表 |
+| [63](#第-63-天高并发系统的设计) | 高并发系统的设计 | 系统设计基础:高并发系统的设计 | 23. 合并 K 个升序链表 |
+| [64](#第-64-天docker) | Docker | Docker:解决的问题~镜像与容器 | 124. 二叉树中的最大路径和 |
+| [65](#第-65-天kubernetes) | Kubernetes | Docker:Kubernetes | 72. 编辑距离 |
+
+## 四、Java 基础
+
+### 第 1 天:数据类型与 String
+
+从基本类型和包装类型开始。缓存池解释了 Integer 用 == 比较时 127 和 128 结果不同的原因;String 的不可变是后面理解常量池和线程安全的前提。
+
+阅读:《Java 基础》从[数据类型](Java%20基础.md#一数据类型)读到[String, StringBuffer and StringBuilder](Java%20基础.md#string-stringbuffer-and-stringbuilder)。
+
+自测:
+
+1. String、StringBuffer 和 StringBuilder 有什么区别?(见[String, StringBuffer and StringBuilder](Java%20基础.md#string-stringbuffer-and-stringbuilder))
+2. String 为什么设计成不可变的?(见[不可变的好处](Java%20基础.md#不可变的好处))
+3. Integer a = 127, b = 127 时 a == b 的结果是什么?换成 128 呢?(见[缓存池](Java%20基础.md#缓存池))
+
+动手:运行[附录 A1](Java%20基础.md#a1-包装类型的缓存池)的代码,对照 Integer.valueOf() 的实现解释 127 和 128 的结果为什么不同。
+
+算法(哈希表,简单):[1. 两数之和](https://leetcode.cn/problems/two-sum/),题解见[数组中两个数的和为给定值](Leetcode%20题解%20-%20哈希表.md#1-数组中两个数的和为给定值)。
+
+### 第 2 天:常量池与运算
+
+常量池决定了两个相同的字面量是不是同一个对象。运算一节中最常被问到的是参数传递。
+
+阅读:《Java 基础》从[String Pool](Java%20基础.md#string-pool)读到[运算](Java%20基础.md#三运算)。
+
+自测:
+
+1. new String("abc") 会创建几个对象?(见[new String("abc")](Java%20基础.md#new-stringabc))
+2. Java 是值传递还是引用传递?(见[参数传递](Java%20基础.md#参数传递))
+
+动手:运行[附录 A2](Java%20基础.md#a2-字符串的存储)的代码,观察字面量和 new 出来的字符串是否共享同一个字节数组。
+
+算法(哈希表,中等):[128. 最长连续序列](https://leetcode.cn/problems/longest-consecutive-sequence/),题解见[最长连续序列](Leetcode%20题解%20-%20哈希表.md#4-最长连续序列)。
+
+### 第 3 天:关键字与 Object 的方法
+
+static 一节的初始化顺序可以对照代码自己推一遍。equals() 和 hashCode() 的约定是理解 HashMap 的前提;clone() 掌握深拷贝与浅拷贝的区别即可。
+
+阅读:《Java 基础》从[关键字](Java%20基础.md#四关键字)读到[Object 通用方法](Java%20基础.md#五object-通用方法)。
+
+自测:
+
+1. == 和 equals() 有什么区别?(见[等价与相等](Java%20基础.md#2-等价与相等))
+2. 重写 equals() 时为什么必须重写 hashCode()?(见[hashCode()](Java%20基础.md#hashcode))
+3. 浅拷贝和深拷贝有什么区别?怎样实现深拷贝?(见[clone()](Java%20基础.md#clone))
+
+动手:写一个只重写了 equals()、没有重写 hashCode() 的类,把两个相等的对象放进 HashSet,观察 size() 的结果,再补上 hashCode() 对比。
+
+算法(哈希表,中等):[560. 和为 K 的子数组](https://leetcode.cn/problems/subarray-sum-equals-k/),题解见[和为 K 的子数组](Leetcode%20题解%20-%20哈希表.md#5-和为-k-的子数组)。
+
+### 第 4 天:继承与多态
+
+先读面向对象的三大特性,再读 Java 中的实现:访问权限、抽象类与接口、重写与重载。多态一节说明了运行时怎样找到被调用的方法。
+
+阅读:《面向对象思想》的[三大特性](面向对象思想.md#一三大特性);《Java 基础》的[继承](Java%20基础.md#六继承)。
+
+自测:
+
+1. 抽象类和接口有什么区别?分别用在什么场景?(见[比较](Java%20基础.md#3-比较)、[使用选择](Java%20基础.md#4-使用选择))
+2. 重载和重写有什么区别?(见[重写与重载](Java%20基础.md#重写与重载))
+3. 多态是怎么实现的?(见[多态](面向对象思想.md#多态))
+
+动手:按[附录 A1](面向对象思想.md#a1-方法调用的字节码)的步骤,用 javap -c 查看重载和重写分别编译成了哪条调用指令。
+
+算法(数组,中等):[54. 螺旋矩阵](https://leetcode.cn/problems/spiral-matrix/),题解见[剑指 Offer 29](29.%20顺时针打印矩阵.md)。
+
+### 第 5 天:反射、异常、泛型与新特性
+
+反射是 Spring 等框架的基础,读时留意它的代价。异常一节重点看 finally 与 return 的执行顺序;Java 与 C++ 的比较可以略读。
+
+阅读:《Java 基础》从[反射](Java%20基础.md#七反射)读到[特性](Java%20基础.md#十一特性)。选读:[对象操作](Java%20IO.md#五对象操作)。
+
+自测:
+
+1. 反射的原理是什么?有哪些应用场景?(见[反射](Java%20基础.md#七反射))
+2. try 和 catch 中有 return 时,finally 还会执行吗?返回值是什么?(见[异常](Java%20基础.md#八异常))
+3. 什么是泛型擦除?(见[泛型](Java%20基础.md#九泛型))
+
+动手:运行[附录 A6](Java%20基础.md#a6-finally-的执行顺序与返回值)的代码,先预测每个方法的返回值再对照输出。
+
+算法(数组,中等):[56. 合并区间](https://leetcode.cn/problems/merge-intervals/),题解见[合并区间](Leetcode%20题解%20-%20数组与矩阵.md#13-合并区间)。
+
+综合题:设计一个 Order 类作为 HashMap 的键:需要重写哪些方法?字段为什么最好用 final?要复制一个订单时用浅拷贝还是深拷贝?(见[equals()](Java%20基础.md#equals)、[hashCode()](Java%20基础.md#hashcode)、[final](Java%20基础.md#final)、[clone()](Java%20基础.md#clone))
+
+## 五、Java 容器
+
+### 第 6 天:集合与 List
+
+先看集合框架的全貌,再看 List 的几种实现。Vector 已经很少使用,可以略读;ArrayList 的扩容和 fail-fast 要读懂。
+
+阅读:《Java 容器》从[集合概览](Java%20容器.md#一概览)读到[CopyOnWriteArrayList](Java%20容器.md#copyonwritearraylist)。
+
+自测:
+
+1. ArrayList 和 LinkedList 有什么区别?分别用在什么场景?(见[与 ArrayList 的比较](Java%20容器.md#2-与-arraylist-的比较))
+2. ArrayList 是怎样扩容的?(见[扩容](Java%20容器.md#2-扩容))
+3. 线程安全的 List 有哪些?CopyOnWriteArrayList 适合什么场景?(见[CopyOnWriteArrayList](Java%20容器.md#copyonwritearraylist))
+
+动手:运行[附录 A1](Java%20容器.md#a1-arraylist-的扩容与常见陷阱)和[附录 A3](Java%20容器.md#a3-arraylist-的并发写入)的代码,观察扩容时数组长度的变化,以及并发写入时丢失的元素。
+
+算法(双指针,简单):[88. 合并两个有序数组](https://leetcode.cn/problems/merge-sorted-array/),题解见[归并两个有序数组](Leetcode%20题解%20-%20双指针.md#5-归并两个有序数组)。
+
+### 第 7 天:HashMap
+
+HashMap 是 Java 面试中问得最多的容器。按存储结构、put、桶下标、扩容的顺序读,计算数组容量一节可以略读。
+
+阅读:《Java 容器》的[HashMap](Java%20容器.md#hashmap)。选读:[进阶:HashMap](Java%20容器%20-%20进阶.md#hashmap)。
+
+自测:
+
+1. HashMap 的 put() 过程是怎样的?(见[put 操作](Java%20容器.md#3-put-操作))
+2. HashMap 是怎样扩容的?为什么容量是 2 的幂?(见[扩容-基本原理](Java%20容器.md#5-扩容-基本原理)、[确定桶下标](Java%20容器.md#4-确定桶下标))
+3. HashMap 为什么线程不安全?(见[线程安全](Java%20容器.md#10-线程安全))
+
+动手:运行[附录 A4](Java%20容器.md#a4-hashmap-链表转红黑树)的代码,观察桶中节点在什么时候从链表变成红黑树,又在什么时候变回来。
+
+算法(双指针,中等):[15. 三数之和](https://leetcode.cn/problems/3sum/),题解见[三数之和](Leetcode%20题解%20-%20双指针.md#8-三数之和)。
+
+### 第 8 天:ConcurrentHashMap 与 LinkedHashMap
+
+ConcurrentHashMap 重点比较 JDK 1.7 的分段锁和 1.8 的桶级锁。LinkedHashMap 按访问顺序排列时可以直接实现 LRU 缓存;WeakHashMap 可以略读。
+
+阅读:《Java 容器》从[ConcurrentHashMap](Java%20容器.md#concurrenthashmap)读到[WeakHashMap](Java%20容器.md#weakhashmap)。选读:[进阶:ConcurrentHashMap](Java%20容器%20-%20进阶.md#concurrenthashmap)。
+
+自测:
+
+1. ConcurrentHashMap 怎样保证线程安全?JDK 1.7 和 1.8 有什么区别?(见[JDK 1.8 的改动](Java%20容器.md#3-jdk-18-的改动))
+2. ConcurrentHashMap 是怎样扩容的?size() 为什么不精确?(见[JDK 1.8 的改动](Java%20容器.md#3-jdk-18-的改动))
+3. 怎样用 LinkedHashMap 实现 LRU 缓存?(见[LRU 缓存](Java%20容器.md#lru-缓存))
+
+动手:运行[附录 A2](Java%20容器.md#a2-hashmap-的并发写入)的代码,再把 HashMap 换成 ConcurrentHashMap 运行一次,对比 size 的结果。
+
+算法(双指针,中等):[11. 盛最多水的容器](https://leetcode.cn/problems/container-with-most-water/)。
+
+综合题:实现一个容量固定的本地缓存:选用哪种容器?怎样淘汰最久没有访问的数据?多个线程同时访问时要注意什么?(见[LRU 缓存](Java%20容器.md#lru-缓存)、[ConcurrentHashMap](Java%20容器.md#concurrenthashmap))
+
+## 六、Java 并发
+
+### 第 9 天:线程与线程池
+
+先看创建线程的几种方式,再看线程池。ThreadPoolExecutor 一节是本章的重点:核心参数、任务提交后的处理顺序和拒绝策略都要能讲清。
+
+阅读:《Java 并发》从[使用线程](Java%20并发.md#一使用线程)读到[基础线程机制](Java%20并发.md#二基础线程机制)。
+
+自测:
+
+1. 线程池的核心参数有哪些?(见[ThreadPoolExecutor](Java%20并发.md#threadpoolexecutor))
+2. 提交一个任务后,线程池是怎样处理的?为什么先放进队列,队列满了才创建新线程?(见[ThreadPoolExecutor](Java%20并发.md#threadpoolexecutor))
+3. 线程池有哪些拒绝策略?怎样选择?(见[ThreadPoolExecutor](Java%20并发.md#threadpoolexecutor))
+
+动手:读懂正文中的 SimplePool,运行[附录 A6](Java%20并发.md#a6-简化版线程池)的代码,说出它与 ThreadPoolExecutor 相比少了哪些功能。
+
+算法(滑动窗口,中等):[3. 无重复字符的最长子串](https://leetcode.cn/problems/longest-substring-without-repeating-characters/),题解见[剑指 Offer 48](48.%20最长不含重复字符的子字符串.md)。
+
+### 第 10 天:中断与互斥同步
+
+中断是让线程停下来的协作方式,线程池的 shutdownNow() 就靠它实现。互斥同步重点比较 synchronized 和 ReentrantLock。
+
+阅读:《Java 并发》从[中断](Java%20并发.md#三中断)读到[互斥同步](Java%20并发.md#四互斥同步)。
+
+自测:
+
+1. 怎样停止一个正在运行的线程?(见[中断](Java%20并发.md#三中断))
+2. shutdown() 和 shutdownNow() 有什么区别?(见[Executor 的中断操作](Java%20并发.md#executor-的中断操作))
+3. synchronized 和 ReentrantLock 有什么区别?(见[synchronized 与 ReentrantLock 的比较](Java%20并发.md#比较))
+
+动手:运行[附录 A2](Java%20并发.md#a2-公平锁与非公平锁)的代码,比较公平锁与非公平锁的耗时并解释原因。
+
+算法(字符串,简单):[415. 字符串相加](https://leetcode.cn/problems/add-strings/),题解见[字符串加法](Leetcode%20题解%20-%20数学.md#2-字符串加法)。
+
+### 第 11 天:线程协作与线程状态
+
+wait() 和 notify() 必须在 synchronized 中调用,读时想一想为什么。线程状态之间的转换可以对照前面几节的方法来理解。
+
+阅读:《Java 并发》从[线程之间的协作](Java%20并发.md#五线程之间的协作)读到[线程状态](Java%20并发.md#六线程状态)。
+
+自测:
+
+1. wait() 和 sleep() 有什么区别?(见[wait() notify() notifyAll()](Java%20并发.md#wait-notify-notifyall))
+2. 线程有哪些状态?状态之间怎样转换?(见[线程状态](Java%20并发.md#六线程状态))
+3. 两个线程交替打印 1~100 怎样实现?(见[交替打印](Java%20并发.md#交替打印))
+
+动手:不看笔记,分别用 wait()/notify() 和 Semaphore 实现两个线程交替打印 1~100,再与[交替打印](Java%20并发.md#交替打印)一节的写法对比。
+
+算法(字符串,中等):[165. 比较版本号](https://leetcode.cn/problems/compare-version-numbers/),题解见[比较版本号](Leetcode%20题解%20-%20字符串.md#12-比较版本号)。
+
+### 第 12 天:J.U.C 的常用组件
+
+这两章介绍 J.U.C 中常用的工具类,重点是 CountDownLatch、CyclicBarrier、CompletableFuture 和 BlockingQueue 的用途,ForkJoin 可以略读。
+
+阅读:《Java 并发》从[J.U.C - AQS](Java%20并发.md#七juc---aqs)读到[J.U.C - 其它组件](Java%20并发.md#八juc---其它组件)。
+
+自测:
+
+1. CountDownLatch 和 CyclicBarrier 有什么区别?(见[CyclicBarrier](Java%20并发.md#cyclicbarrier))
+2. CompletableFuture 解决了 Future 的什么问题?(见[CompletableFuture](Java%20并发.md#completablefuture))
+3. 怎样用 BlockingQueue 实现生产者消费者?(见[BlockingQueue](Java%20并发.md#blockingqueue))
+
+动手:用 CompletableFuture 并行查询“商品信息”和“库存”两个模拟接口(各 sleep 200 毫秒),合并结果后输出,比较串行和并行的总耗时。
+
+算法(链表,简单):[206. 反转链表](https://leetcode.cn/problems/reverse-linked-list/),题解见[链表反转](Leetcode%20题解%20-%20链表.md#2-链表反转)。
+
+### 第 13 天:Java 内存模型与 volatile
+
+从一个线程不安全的例子开始,Java 内存模型解释了问题的来源:原子性、可见性和有序性。内存间交互操作可以略读,volatile 和先行发生原则要读懂。
+
+阅读:《Java 并发》从[线程不安全示例](Java%20并发.md#九线程不安全示例)读到[Java 内存模型](Java%20并发.md#十java-内存模型)。
+
+自测:
+
+1. volatile 能保证什么?为什么 volatile 修饰的 i++ 仍然不安全?(见[volatile](Java%20并发.md#volatile)、[可见性](Java%20并发.md#2-可见性))
+2. volatile 和 synchronized 有什么区别?(见[volatile](Java%20并发.md#volatile))
+3. 什么是先行发生(happens-before)原则?(见[先行发生原则](Java%20并发.md#先行发生原则))
+
+动手:运行[附录 A3](Java%20并发.md#a3-volatile-与循环条件)的代码,分别不加和加上 volatile,观察循环能否结束。
+
+算法(链表,简单):[21. 合并两个有序链表](https://leetcode.cn/problems/merge-two-sorted-lists/),题解见[归并两个有序的链表](Leetcode%20题解%20-%20链表.md#3-归并两个有序的链表)。
+
+### 第 14 天:线程安全与 CAS
+
+线程安全的实现方式有不可变、互斥同步、非阻塞同步和无同步方案四类。本节重点是 CAS:它的原理、ABA 问题,以及乐观锁和悲观锁的取舍。
+
+阅读:《Java 并发》从[线程安全](Java%20并发.md#十一线程安全)读到[非阻塞同步](Java%20并发.md#非阻塞同步)。
+
+自测:
+
+1. 什么是线程安全?怎样保证线程安全?(见[线程安全](Java%20并发.md#十一线程安全))
+2. CAS 的原理是什么?ABA 问题怎么解决?(见[CAS](Java%20并发.md#1-cas)、[ABA](Java%20并发.md#3-aba))
+3. 乐观锁和悲观锁有什么区别?(见[乐观锁与悲观锁](Java%20并发.md#4-乐观锁与悲观锁))
+
+动手:运行[附录 A4](Java%20并发.md#a4-atomiclong-与-longadder)的代码,观察线程数增加时 AtomicLong 和 LongAdder 的耗时差距。
+
+算法(链表,简单):[141. 环形链表](https://leetcode.cn/problems/linked-list-cycle/),题解见[判断链表是否存在环](Leetcode%20题解%20-%20双指针.md#6-判断链表是否存在环)。
+
+### 第 15 天:ThreadLocal 与锁优化
+
+ThreadLocal 重点看它的数据结构和内存泄漏的原因。锁优化讲 JVM 怎样降低 synchronized 的开销,按轻量级锁、偏向锁、锁升级的过程读。
+
+阅读:《Java 并发》从[无同步方案](Java%20并发.md#无同步方案)读到[多线程开发良好的实践](Java%20并发.md#十三多线程开发良好的实践)。
+
+自测:
+
+1. ThreadLocal 为什么可能导致内存泄漏?(见[线程本地存储(Thread Local Storage)](Java%20并发.md#2-线程本地存储thread-local-storage))
+2. synchronized 的底层是怎样实现的?(见[锁的实现](Java%20并发.md#1-锁的实现)、[轻量级锁](Java%20并发.md#轻量级锁))
+3. 说一下 synchronized 的锁升级过程。(见[锁升级的过程](Java%20并发.md#锁升级的过程))
+
+动手:运行[附录 A1](Java%20并发.md#a1-三种锁在读多写少时的耗时)的代码,比较 synchronized、ReentrantReadWriteLock 和 StampedLock 在读多写少时的耗时。
+
+算法(链表,简单):[160. 相交链表](https://leetcode.cn/problems/intersection-of-two-linked-lists/),题解见[找出两个链表的交点](Leetcode%20题解%20-%20链表.md#1-找出两个链表的交点)。
+
+综合题:用多个线程模拟秒杀扣减库存:分别用不加锁、synchronized、AtomicInteger 实现,比较结果是否正确,说明各自的取舍。(见[线程不安全示例](Java%20并发.md#九线程不安全示例)、[比较](Java%20并发.md#比较)、[CAS](Java%20并发.md#1-cas))
+
+## 七、Java 虚拟机
+
+### 第 16 天:运行时数据区域
+
+先分清哪些区域是线程私有的、哪些是共享的。堆和栈的区别、对象的创建过程都是常见问题。
+
+阅读:《Java 虚拟机》的[运行时数据区域](Java%20虚拟机.md#一运行时数据区域)。
+
+自测:
+
+1. JVM 的内存区域有哪些?哪些是线程私有的?(见[运行时数据区域](Java%20虚拟机.md#一运行时数据区域))
+2. 堆和栈有什么区别?(见[堆和栈的区别](Java%20虚拟机.md#堆和栈的区别))
+3. 对象的创建过程是怎样的?(见[对象的创建与内存布局](Java%20虚拟机.md#对象的创建与内存布局))
+
+动手:运行[附录 A2](Java%20虚拟机.md#a2-逃逸分析)的代码,比较开启和关闭逃逸分析时的耗时与 GC 次数。
+
+算法(链表,中等):[92. 反转链表 II](https://leetcode.cn/problems/reverse-linked-list-ii/)。
+
+### 第 17 天:对象存活与垃圾收集算法
+
+先判断哪些对象可以回收,再看怎样回收。引用类型中弱引用最常被问到,ThreadLocal 就用到了它。
+
+阅读:《Java 虚拟机》从[垃圾收集](Java%20虚拟机.md#二垃圾收集)读到[Stop The World](Java%20虚拟机.md#stop-the-world)。
+
+自测:
+
+1. 怎样判断一个对象可以被回收?哪些对象可以作为 GC Roots?(见[可达性分析算法](Java%20虚拟机.md#2-可达性分析算法))
+2. 强引用、软引用、弱引用、虚引用有什么区别?(见[引用类型](Java%20虚拟机.md#引用类型))
+3. 垃圾收集为什么要 Stop The World?(见[Stop The World](Java%20虚拟机.md#stop-the-world))
+
+动手:运行[附录 A5](Java%20虚拟机.md#a5-threadlocalmap-的弱引用)的代码,观察 GC 之后 Entry 的 key 和 value 分别是否被回收。
+
+算法(链表,中等):[143. 重排链表](https://leetcode.cn/problems/reorder-list/),题解见[重排链表](Leetcode%20题解%20-%20链表.md#12-重排链表)。
+
+### 第 18 天:垃圾收集器
+
+每种收集器关注它在哪些阶段停顿。Serial、ParNew、Parallel 了解即可,重点是 CMS 和 G1。
+
+阅读:《Java 虚拟机》的[垃圾收集器](Java%20虚拟机.md#垃圾收集器)。
+
+自测:
+
+1. 有哪些垃圾收集器?各自适合什么场景?(见[垃圾收集器](Java%20虚拟机.md#垃圾收集器))
+2. CMS 和 G1 有什么区别?(见[G1 收集器](Java%20虚拟机.md#7-g1-收集器))
+3. G1 怎样控制停顿时间?(见[G1 收集器](Java%20虚拟机.md#7-g1-收集器))
+
+动手:按[附录 A3](Java%20虚拟机.md#a3-不同收集器的停顿)的方法,用 -Xlog:gc 分别运行 Serial、Parallel 和 G1,比较停顿时间。
+
+算法(链表,中等):[19. 删除链表的倒数第 N 个结点](https://leetcode.cn/problems/remove-nth-node-from-end-of-list/),题解见[删除链表的倒数第 n 个节点](Leetcode%20题解%20-%20链表.md#5-删除链表的倒数第-n-个节点)。
+
+### 第 19 天:三色标记与内存分配
+
+三色标记解释了并发标记为什么会漏标,以及 CMS 和 G1 各自的解决办法。内存分配一节回答对象什么时候进入老年代、什么时候触发 Full GC。
+
+阅读:《Java 虚拟机》从[三色标记](Java%20虚拟机.md#三色标记)读到[内存分配与回收策略](Java%20虚拟机.md#三内存分配与回收策略)。
+
+自测:
+
+1. 三色标记中的漏标是怎样发生的?CMS 和 G1 分别怎样解决?(见[三色标记](Java%20虚拟机.md#三色标记))
+2. 对象在什么情况下进入老年代?(见[内存分配策略](Java%20虚拟机.md#内存分配策略))
+3. Full GC 在什么情况下触发?(见[Full GC 的触发条件](Java%20虚拟机.md#full-gc-的触发条件))
+
+动手:运行[附录 A6](Java%20虚拟机.md#a6-默认的收集器与-g1-的-region-大小)的命令,查看本机默认的收集器和 G1 的 Region 大小。
+
+算法(链表,中等):[82. 删除排序链表中的重复元素 II](https://leetcode.cn/problems/remove-duplicates-from-sorted-list-ii/),题解见[剑指 Offer 18.2](18.2%20删除链表中重复的结点.md)。
+
+### 第 20 天:类加载机制
+
+类加载过程中的准备和初始化最容易混淆,重点看静态变量在这两个阶段各被赋了什么值。双亲委派和打破它的例子是常见的追问。
+
+阅读:《Java 虚拟机》的[类加载机制](Java%20虚拟机.md#四类加载机制)。
+
+自测:
+
+1. 类加载的过程是怎样的?(见[类加载过程](Java%20虚拟机.md#类加载过程))
+2. 什么是双亲委派模型?为什么需要它?(见[双亲委派模型](Java%20虚拟机.md#双亲委派模型))
+3. 怎样打破双亲委派?有哪些实际的例子?(见[打破双亲委派](Java%20虚拟机.md#4-打破双亲委派))
+
+动手:运行[附录 A7](Java%20虚拟机.md#a7-打破双亲委派)的代码,说出 DriverManager 是怎样加载到第三方驱动的。
+
+算法(链表,中等):[142. 环形链表 II](https://leetcode.cn/problems/linked-list-cycle-ii/),题解见[剑指 Offer 23](23.%20链表中环的入口结点.md)。
+
+### 第 21 天:线上问题排查
+
+把前面几天的内容用到排查中:先用 Linux 命令找到问题进程,再用 JDK 的工具找到具体的线程和对象。
+
+阅读:《Java 虚拟机》的[线上问题排查](Java%20虚拟机.md#五线上问题排查);《Linux》的[线上问题排查](Linux.md#十一线上问题排查)。
+
+自测:
+
+1. 线上出现 OOM 怎样排查?(见[内存溢出与内存泄漏](Java%20虚拟机.md#内存溢出与内存泄漏))
+2. CPU 占用过高怎样定位到具体的代码?(见[CPU 占用过高](Java%20虚拟机.md#cpu-占用过高))
+3. 做过哪些 JVM 调优?(见[JVM 调优](Java%20虚拟机.md#jvm-调优))
+
+动手:运行[附录 A8](Java%20虚拟机.md#a8-元空间溢出)的代码复现元空间溢出;再写一个死循环线程,按[CPU 占用过高](Java%20虚拟机.md#cpu-占用过高)一节的步骤用 top 和 jstack 找到它。
+
+算法(链表,中等):[148. 排序链表](https://leetcode.cn/problems/sort-list/),题解见[链表排序](Leetcode%20题解%20-%20链表.md#14-链表排序)。
+
+综合题:线上服务每隔几小时发生一次 Full GC,最后因为 OOM 重启:先看哪些数据?可能有哪些原因?怎样验证?(见[内存溢出与内存泄漏](Java%20虚拟机.md#内存溢出与内存泄漏)、[Full GC 的触发条件](Java%20虚拟机.md#full-gc-的触发条件)、[JVM 调优](Java%20虚拟机.md#jvm-调优))
+
+## 八、MySQL
+
+### 第 22 天:事务、并发一致性与锁
+
+先理解事务的四个性质和并发时会出现的问题,再看数据库怎样用锁解决。封锁协议是教材中的理论,可以略读;MySQL 中的锁一节对应实际使用。
+
+阅读:《数据库系统原理》从[事务](数据库系统原理.md#一事务)读到[封锁](数据库系统原理.md#三封锁)。
+
+自测:
+
+1. ACID 分别是什么?InnoDB 怎样保证它们?(见[事务](数据库系统原理.md#一事务))
+2. 脏读、不可重复读和幻读有什么区别?(见[并发一致性问题](数据库系统原理.md#二并发一致性问题))
+3. MySQL 中有哪些锁?意向锁有什么用?(见[MySQL 中的锁](数据库系统原理.md#mysql-中的锁)、[封锁类型](数据库系统原理.md#封锁类型))
+
+动手:开两个 mysql 客户端,按[附录 A1](数据库系统原理.md#a1-并发一致性问题)的步骤复现脏读和不可重复读。
+
+算法(链表,中等):[2. 两数相加](https://leetcode.cn/problems/add-two-numbers/),同类题见[链表求和](Leetcode%20题解%20-%20链表.md#7-链表求和)。
+
+### 第 23 天:隔离级别与 MVCC
+
+隔离级别规定了并发的事务能看到什么,MVCC 是 InnoDB 实现提交读和可重复读的方式,重点是 ReadView 的可见性判断。
+
+阅读:《数据库系统原理》从[隔离级别](数据库系统原理.md#四隔离级别)读到[多版本并发控制](数据库系统原理.md#五多版本并发控制)。
+
+自测:
+
+1. 四种隔离级别分别解决了哪些问题?(见[隔离级别](数据库系统原理.md#四隔离级别))
+2. MVCC 的原理是什么?ReadView 怎样判断可见性?(见[ReadView](数据库系统原理.md#readview))
+3. 快照读和当前读有什么区别?(见[快照读与当前读](数据库系统原理.md#快照读与当前读))
+
+动手:按[附录 A4](数据库系统原理.md#a4-快照读与当前读混用)的步骤,观察同一个事务中快照读和当前读看到的数据为什么不同。
+
+算法(设计,中等):[146. LRU 缓存](https://leetcode.cn/problems/lru-cache/),题解见[LRU](缓存.md#七lru)。
+
+### 第 24 天:Next-Key Lock 与死锁
+
+Next-Key Lock 是 InnoDB 在可重复读下防止幻读的方法,加锁范围要结合例子推一遍。间隙锁也是 MySQL 死锁的常见原因。
+
+阅读:《数据库系统原理》的[Next-Key Locks](数据库系统原理.md#六next-key-locks);《计算机操作系统 - 死锁》的[MySQL 死锁](计算机操作系统%20-%20死锁.md#2-mysql-死锁)。选读:[关系数据库设计理论](数据库系统原理.md#七关系数据库设计理论)、[ER 图](数据库系统原理.md#八er-图)。
+
+自测:
+
+1. InnoDB 在可重复读级别下怎样避免幻读?(见[Next-Key Locks](数据库系统原理.md#next-key-locks))
+2. 间隙锁锁住的是什么范围?(见[Gap Locks](数据库系统原理.md#gap-locks))
+3. 遇到过 MySQL 死锁吗?怎样排查?(见[MySQL 死锁](计算机操作系统%20-%20死锁.md#2-mysql-死锁))
+
+动手:按[附录 A5](数据库系统原理.md#a5-next-key-locks-的加锁范围)的步骤,用 performance_schema.data_locks 查看一条范围查询加了哪些锁。
+
+算法(栈和队列,简单):[20. 有效的括号](https://leetcode.cn/problems/valid-parentheses/),题解见[用栈实现括号匹配](Leetcode%20题解%20-%20栈和队列.md#4-用栈实现括号匹配)。
+
+### 第 25 天:B+ 树与索引的类型
+
+先看 B+ 树为什么适合放在磁盘上,再看 InnoDB 的聚簇索引和二级索引。回表是理解后面覆盖索引的前提。
+
+阅读:《MySQL》从[B+ Tree 原理](MySQL.md#b-tree-原理)读到[索引的优点](MySQL.md#索引的优点)。
+
+自测:
+
+1. MySQL 为什么用 B+ 树做索引,而不用 B 树、红黑树或哈希表?(见[与红黑树的比较](MySQL.md#3-与红黑树的比较)、[与 B Tree 的比较](MySQL.md#4-与-b-tree-的比较)、[哈希索引](MySQL.md#2-哈希索引))
+2. 聚簇索引和二级索引有什么区别?什么是回表?(见[B+Tree 索引](MySQL.md#1-btree-索引))
+3. 主键索引、唯一索引和普通索引有什么区别?(见[主键索引、唯一索引与普通索引](MySQL.md#5-主键索引唯一索引与普通索引))
+
+动手:按[附录 A1](MySQL.md#a1-主键索引的高度)的方法,查出一张 200 万行的表主键索引的高度。
+
+算法(栈和队列,简单):[232. 用栈实现队列](https://leetcode.cn/problems/implement-queue-using-stacks/),题解见[用栈实现队列](Leetcode%20题解%20-%20栈和队列.md#1-用栈实现队列)。
+
+### 第 26 天:索引的使用
+
+这几节是建索引和排查慢查询的依据:什么时候建索引、联合索引怎样安排列的顺序、哪些写法会让索引失效。
+
+阅读:《MySQL》从[索引的使用条件](MySQL.md#索引的使用条件)读到[索引失效](MySQL.md#索引失效)。
+
+自测:
+
+1. 什么是最左前缀原则?联合索引 (a, b, c) 只用到 a 和 c 时能走索引吗?(见[多列索引](MySQL.md#2-多列索引))
+2. 哪些情况下索引会失效?(见[索引失效](MySQL.md#索引失效))
+3. 建索引有哪些原则?(见[索引的使用条件](MySQL.md#索引的使用条件))
+
+动手:按[附录 A6](MySQL.md#a6-联合索引的扫描范围)的表结构,自己写几条会让联合索引失效的查询,用 EXPLAIN 验证。
+
+算法(栈和队列,简单):[155. 最小栈](https://leetcode.cn/problems/min-stack/),题解见[最小值栈](Leetcode%20题解%20-%20栈和队列.md#3-最小值栈)。
+
+### 第 27 天:查询性能优化
+
+按执行过程、Explain、慢查询排查的顺序读。慢查询的定位与优化一节给出了从找出慢 SQL 到验证效果的完整步骤。
+
+阅读:《MySQL》的[查询性能优化](MySQL.md#二查询性能优化)。
+
+自测:
+
+1. 线上出现慢 SQL,怎样排查和优化?(见[慢查询的定位与优化](MySQL.md#慢查询的定位与优化))
+2. EXPLAIN 主要看哪些字段?Using filesort 和 Using temporary 表示什么?(见[使用 Explain 进行分析](MySQL.md#使用-explain-进行分析))
+3. 深分页为什么慢?怎样优化?(见[深分页](MySQL.md#深分页))
+
+动手:在一张 100 万行以上的表上(可以用脚本批量插入),按[附录 A5](MySQL.md#a5-深分页的耗时)的方法比较 LIMIT 1000000, 10、延迟关联和游标分页三种写法的耗时。
+
+算法(栈和队列,中等):[394. 字符串解码](https://leetcode.cn/problems/decode-string/),题解见[字符串解码](Leetcode%20题解%20-%20栈和队列.md#7-字符串解码)。
+
+### 第 28 天:存储引擎、切分与复制
+
+存储引擎重点比较 InnoDB 和 MyISAM,数据类型可以略读。分库分表要清楚它解决什么问题、又带来什么问题;复制是读写分离的基础。
+
+阅读:《MySQL》从[存储引擎](MySQL.md#三存储引擎)读到[MySQL 复制](MySQL.md#六复制)。
+
+自测:
+
+1. InnoDB 和 MyISAM 有什么区别?(见[InnoDB 与 MyISAM 的比较](MySQL.md#比较))
+2. 分库分表之后会遇到哪些问题?(见[Sharding 存在的问题](MySQL.md#sharding-存在的问题))
+3. 主从复制的过程是怎样的?读写分离有什么问题?(见[MySQL 复制](MySQL.md#六复制))
+
+算法(栈和队列,中等):[739. 每日温度](https://leetcode.cn/problems/daily-temperatures/),题解见[数组中元素与下一个比它大的元素之间的距离](Leetcode%20题解%20-%20栈和队列.md#5-数组中元素与下一个比它大的元素之间的距离)。
+
+### 第 29 天:日志
+
+redo log、undo log 和 binlog 分别用于崩溃恢复、回滚和复制。最后一节用一条 UPDATE 把三种日志串起来。
+
+阅读:《MySQL》的[MySQL 的日志](MySQL.md#七日志)。
+
+自测:
+
+1. redo log、undo log 和 binlog 有什么区别?(见[MySQL 的日志](MySQL.md#七日志))
+2. 为什么需要两阶段提交?(见[两阶段提交](MySQL.md#两阶段提交))
+3. 一条 UPDATE 语句执行时,三种日志是怎样写的?(见[更新语句的执行过程](MySQL.md#更新语句的执行过程))
+
+动手:执行 SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit' 和 SHOW VARIABLES LIKE 'sync_binlog',结合[redo log](MySQL.md#redo-log)一节说明两个参数都为 1 时每次提交会把哪些日志刷到磁盘,改成 0 或 2 有什么风险。
+
+算法(二分查找,简单):[704. 二分查找](https://leetcode.cn/problems/binary-search/),题解见[正常实现](Leetcode%20题解%20-%20二分查找.md#正常实现)。
+
+综合题:订单表有 2000 万行,按用户查询最近的订单很慢:怎样分析原因?建什么索引?数据继续增长怎么办?(见[慢查询的定位与优化](MySQL.md#慢查询的定位与优化)、[索引优化](MySQL.md#索引优化)、[切分](MySQL.md#五切分))
+
+## 九、Redis 与缓存
+
+### 第 30 天:Redis 的数据类型
+
+先看 Redis 和 MySQL 的分工,再看五种基本类型和 BITMAP、HyperLogLog。每种类型想一个自己用得上的场景。
+
+阅读:《Redis》从[Redis 概述](Redis.md#一概述)读到[Redis 的数据类型](Redis.md#二数据类型)。
+
+自测:
+
+1. Redis 有哪些数据类型?分别用在什么场景?(见[Redis 的数据类型](Redis.md#二数据类型))
+2. Redis 和 MySQL 有什么区别?(见[与 MySQL 的区别](Redis.md#与-mysql-的区别))
+3. 一亿用户每天的签到状态怎样存储?(见[BITMAP](Redis.md#bitmap))
+
+动手:按[附录 A7](Redis.md#a7-bitmap-的签到和日活)的命令记录一个用户一个月的签到,用 BITCOUNT 统计签到天数。
+
+算法(二分查找,中等):[33. 搜索旋转排序数组](https://leetcode.cn/problems/search-in-rotated-sorted-array/)。
+
+### 第 31 天:使用场景与底层结构
+
+使用场景把上一天的数据类型落到具体业务中。底层结构重点看编码的转换和跳跃表。
+
+阅读:《Redis》从[Redis 的使用场景](Redis.md#三使用场景)读到[Redis 的数据结构](Redis.md#四数据结构)。选读:[进阶:Redis 的数据结构](Redis%20-%20进阶.md#四数据结构)。
+
+自测:
+
+1. 怎样用 Redis 实现排行榜?(见[排行榜](Redis.md#排行榜))
+2. ZSET 的底层为什么用跳表?(见[跳跃表](Redis.md#跳跃表))
+3. Redis 的字典是怎样扩容的?(见[字典](Redis.md#字典))
+
+动手:用 redis-cli 实现[排行榜](Redis.md#排行榜)一节中的例子:ZADD 写入几个用户的分数,用 ZREVRANGE 取前 3 名,用 ZREVRANK 查某个用户的名次。
+
+算法(二分查找,中等):[34. 在排序数组中查找元素的第一个和最后一个位置](https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/),题解见[查找区间](Leetcode%20题解%20-%20二分查找.md#6-查找区间)。
+
+### 第 32 天:过期、淘汰、持久化与事务
+
+与 Memcached 的比较可以略读。过期删除和内存淘汰是两件事:前者清理过期的键,后者在内存不够时腾出空间。持久化重点比较 RDB 和 AOF。
+
+阅读:《Redis》从[Redis 与 Memcached](Redis.md#五redis-与-memcached)读到[Redis 事务](Redis.md#九事务)。
+
+自测:
+
+1. 过期的键怎样删除?内存满了有哪些淘汰策略?(见[键的过期时间](Redis.md#六键的过期时间)、[数据淘汰策略](Redis.md#七数据淘汰策略))
+2. RDB 和 AOF 有什么区别?(见[Redis 持久化](Redis.md#八持久化))
+3. Lua 脚本为什么能保证原子性?(见[Lua 脚本](Redis.md#lua-脚本))
+
+动手:开启 AOF 后对同一个键执行 100 次 INCR,查看 appendonlydir 目录中的文件;执行 BGREWRITEAOF 后再查看一次,对照[AOF 持久化](Redis.md#aof-持久化)一节说明基础文件和增量文件的变化。
+
+算法(二分查找,中等):[153. 寻找旋转排序数组中的最小值](https://leetcode.cn/problems/find-minimum-in-rotated-sorted-array/),题解见[旋转数组的最小数字](Leetcode%20题解%20-%20二分查找.md#5-旋转数组的最小数字)。
+
+### 第 33 天:线程模型、复制与集群
+
+单线程为什么快是 Redis 的高频题。复制、Sentinel、Cluster 依次解决数据备份、故障转移和容量扩展的问题。
+
+阅读:《Redis》从[Redis 的事件](Redis.md#十事件)读到[Redis 分片](Redis.md#十三分片)。选读:[进阶:Redis 的事件](Redis%20-%20进阶.md#十事件)、[一个简单的论坛系统分析](Redis.md#十四一个简单的论坛系统分析)。
+
+自测:
+
+1. Redis 是单线程的吗?为什么单线程也这么快?(见[单线程为什么快](Redis.md#单线程为什么快))
+2. 哨兵怎样发现主节点下线并选出新的主节点?(见[Sentinel](Redis.md#十二sentinel))
+3. Redis Cluster 怎样分配数据?扩容时怎样迁移?(见[Redis Cluster](Redis.md#redis-cluster))
+
+动手:按[附录 A4](Redis.md#a4-redis-cluster)的步骤在本机搭建 3 主 3 从的集群,用 CLUSTER KEYSLOT 查看几个键落在哪个槽。
+
+算法(排序与堆,中等):[215. 数组中的第 K 个最大元素](https://leetcode.cn/problems/kth-largest-element-in-an-array/),题解见[Kth Element](Leetcode%20题解%20-%20排序.md#1-kth-element)。
+
+### 第 34 天:缓存的位置与三种问题
+
+前几章介绍缓存的特征和位置,多级缓存一节重点读。穿透、击穿、雪崩都是请求越过缓存打到数据库,区别在于原因。
+
+阅读:《缓存》从[缓存特征](缓存.md#一缓存特征)读到[缓存雪崩](缓存.md#缓存雪崩)。
+
+自测:
+
+1. 缓存穿透、击穿和雪崩有什么区别?分别怎样解决?(见[缓存问题](缓存.md#四缓存问题))
+2. 本地缓存和 Redis 怎样配合使用?(见[多级缓存](缓存.md#多级缓存))
+3. 布隆过滤器的原理是什么?为什么会误判?(见[缓存穿透](缓存.md#缓存穿透))
+
+动手:阅读[附录 A5](缓存.md#a5-缓存击穿的三种做法),比较三种做法查询数据库的次数和请求的耗时。
+
+算法(排序与堆,中等):[347. 前 K 个高频元素](https://leetcode.cn/problems/top-k-frequent-elements/),题解见[出现频率最多的 k 个元素](Leetcode%20题解%20-%20排序.md#1-出现频率最多的-k-个元素)。
+
+### 第 35 天:缓存一致性与一致性哈希
+
+缓存一致性要讲清为什么先更新数据库再删缓存,以及仍然存在的不一致窗口。一致性哈希解决节点增减时大量缓存失效的问题;最后手写一个 LRU。
+
+阅读:《缓存》从[缓存一致性](缓存.md#缓存一致性)读到[LRU](缓存.md#七lru)。
+
+自测:
+
+1. 怎样保证缓存与数据库的一致性?(见[缓存一致性](缓存.md#缓存一致性))
+2. 什么是大 Key 和热 Key?怎样处理?(见[大 Key 与热 Key](缓存.md#大-key-与热-key))
+3. 一致性哈希解决了什么问题?虚拟节点有什么用?(见[一致性哈希](缓存.md#六一致性哈希))
+
+动手:不看笔记手写 LRU 缓存(哈希表加双向链表),在力扣 146 题提交通过后,再与[LRU](缓存.md#七lru)一节的实现对比。
+
+算法(排序与堆,中等):[912. 排序数组(手写快速排序)](https://leetcode.cn/problems/sort-an-array/),题解见[快速排序](算法%20-%20排序.md#快速排序)。
+
+综合题:商品详情页的 QPS 很高:怎样设计缓存的读写流程?热点商品的缓存过期时怎样避免数据库被压垮?价格修改后多久能看到新值?(见[多级缓存](缓存.md#多级缓存)、[缓存击穿](缓存.md#缓存击穿)、[缓存一致性](缓存.md#缓存一致性))
+
+## 十、计算机网络
+
+### 第 36 天:TCP 的连接管理
+
+先比较 TCP 和 UDP,再看 TCP 首部中的序号和确认号,它们是理解握手和挥手的基础。TIME_WAIT 和 CLOSE_WAIT 是线上排查中常见的问题。
+
+阅读:《计算机网络 - 传输层》从[UDP 和 TCP 的特点](计算机网络%20-%20传输层.md#udp-和-tcp-的特点)读到[TCP 的四次挥手](计算机网络%20-%20传输层.md#tcp-的四次挥手)。
+
+自测:
+
+1. 三次握手的过程是怎样的?为什么不是两次?(见[三次握手的原因](计算机网络%20-%20传输层.md#三次握手的原因))
+2. 为什么 TIME_WAIT 要等待 2MSL?(见[TIME_WAIT](计算机网络%20-%20传输层.md#time_wait))
+3. 服务器上出现大量 TIME_WAIT 或 CLOSE_WAIT 是什么原因?(见[大量 TIME_WAIT 与 CLOSE_WAIT](计算机网络%20-%20传输层.md#大量-time_wait-与-close_wait))
+
+动手:用 tcpdump 或 Wireshark 抓一次 curl http://example.com 的数据包,找出三次握手和四次挥手的报文,记下每个报文的序号和确认号。
+
+算法(二叉树,简单):[101. 对称二叉树](https://leetcode.cn/problems/symmetric-tree/),题解见[树的对称](Leetcode%20题解%20-%20树.md#9-树的对称)。
+
+### 第 37 天:可靠传输与拥塞控制
+
+按确认与重传、滑动窗口、流量控制、拥塞控制的顺序读,注意流量控制和拥塞控制分别在保护谁。
+
+阅读:《计算机网络 - 传输层》从[TCP 可靠传输](计算机网络%20-%20传输层.md#tcp-可靠传输)读到[在 UDP 上实现可靠传输](计算机网络%20-%20传输层.md#在-udp-上实现可靠传输)。
+
+自测:
+
+1. TCP 怎样保证可靠传输?(见[TCP 可靠传输](计算机网络%20-%20传输层.md#tcp-可靠传输))
+2. 拥塞控制和流量控制有什么区别?(见[TCP 拥塞控制](计算机网络%20-%20传输层.md#tcp-拥塞控制))
+3. 什么是粘包?怎样解决?(见[TCP 粘包与拆包](计算机网络%20-%20传输层.md#tcp-粘包与拆包))
+
+动手:在 Linux 上用 scp 或 curl 传输一个大文件,同时执行 ss -ti 查看这个连接的 cwnd、rtt 和 retrans,对照[TCP 拥塞控制](计算机网络%20-%20传输层.md#tcp-拥塞控制)一节解释 cwnd 的变化。
+
+算法(二叉树,简单):[104. 二叉树的最大深度](https://leetcode.cn/problems/maximum-depth-of-binary-tree/),题解见[树的高度](Leetcode%20题解%20-%20树.md#1-树的高度)。
+
+### 第 38 天:HTTP 基础
+
+这几章是 HTTP 报文的基本格式。方法和首部字段可以略读,常见的状态码要记住。
+
+阅读:《HTTP》从[HTTP 基础概念](HTTP.md#一基础概念)读到[HTTP 首部](HTTP.md#四http-首部)。
+
+自测:
+
+1. HTTP 请求报文和响应报文由哪几部分组成?(见[请求和响应报文](HTTP.md#请求和响应报文))
+2. 301、302、304、502、504 分别表示什么?(见[HTTP 状态码](HTTP.md#三http-状态码))
+3. PUT 和 PATCH 有什么区别?(见[HTTP 方法](HTTP.md#二http-方法))
+
+动手:用 curl -v 访问一个网站,指出请求行、请求首部、状态行和响应首部,再用 curl -I 只获取响应首部。
+
+算法(二叉树,中等):[102. 二叉树的层序遍历](https://leetcode.cn/problems/binary-tree-level-order-traversal/),题解见[剑指 Offer 32.2](32.2%20把二叉树打印成多行.md)。
+
+### 第 39 天:连接管理与 Cookie
+
+长连接减少了建立连接的次数。Cookie 一节篇幅较长,重点是 Session、JWT,以及 Cookie、Session 和 Token 的区别。
+
+阅读:《HTTP》从[连接管理](HTTP.md#连接管理)读到[Cookie](HTTP.md#cookie)。
+
+自测:
+
+1. 什么是长连接?HTTP/1.1 怎样复用连接?(见[连接管理](HTTP.md#连接管理))
+2. Cookie、Session 和 Token 有什么区别?(见[Cookie](HTTP.md#cookie))
+3. Cookie 有哪些和安全相关的属性?(见[Cookie](HTTP.md#cookie))
+
+动手:用浏览器的开发者工具查看一个网站登录后响应中的 Set-Cookie,找出其中的 HttpOnly、Secure 和 SameSite 属性。
+
+算法(二叉树,中等):[103. 二叉树的锯齿形层序遍历](https://leetcode.cn/problems/binary-tree-zigzag-level-order-traversal/),题解见[剑指 Offer 32.3](32.3%20按之字形顺序打印二叉树.md)。
+
+### 第 40 天:缓存、WebSocket 与跨域
+
+HTTP 缓存分为强缓存和协商缓存。内容协商到通信数据转发几节可以略读;WebSocket 和跨域是 Web 开发中常遇到的问题。
+
+阅读:《HTTP》从[HTTP 缓存](HTTP.md#缓存)读到[跨域](HTTP.md#跨域)。
+
+自测:
+
+1. 强缓存和协商缓存有什么区别?(见[HTTP 缓存](HTTP.md#缓存))
+2. WebSocket 和 HTTP 有什么区别?什么时候用 SSE?(见[WebSocket](HTTP.md#websocket))
+3. 什么是跨域?怎样解决?(见[跨域](HTTP.md#跨域))
+
+动手:按[附录 A2](HTTP.md#a2-强缓存与协商缓存)的方法,用 curl 带上 If-None-Match 请求同一个资源,观察 304 响应。
+
+算法(二叉树,中等):[236. 二叉树的最近公共祖先](https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/),题解见[二叉树的最近公共祖先](Leetcode%20题解%20-%20树.md#5-二叉树的最近公共祖先)。
+
+### 第 41 天:HTTPS
+
+HTTPS 在 HTTP 和 TCP 之间加了一层 TLS。按加密、认证、握手的顺序读,握手一节要能对着报文讲出每一步做了什么。
+
+阅读:《HTTP》的[HTTPS](HTTP.md#六https)。
+
+自测:
+
+1. HTTPS 的握手过程是怎样的?(见[TLS 握手](HTTP.md#tls-握手))
+2. HTTPS 为什么同时使用对称加密和非对称加密?(见[加密](HTTP.md#加密))
+3. 证书是怎样验证的?根证书有什么用?(见[认证](HTTP.md#认证))
+
+动手:按[附录 A4](HTTP.md#a4-tls-握手)的方法,用 openssl s_client 连接一个网站,找出协商出的 TLS 版本、密码套件和证书链。
+
+算法(二叉树,中等):[199. 二叉树的右视图](https://leetcode.cn/problems/binary-tree-right-side-view/)。
+
+### 第 42 天:HTTP 的版本、GET 与 POST、DNS
+
+HTTP/2 和 HTTP/3 分别解决了 HTTP/1.1 的哪些问题是常见的追问。GET 和 POST 的区别要从语义讲起,而不只是参数的位置。
+
+阅读:《HTTP》从[HTTP/1.1 新特性](HTTP.md#七http11-新特性)读到[GET 和 POST 比较](HTTP.md#九get-和-post-比较);《计算机网络 - 应用层》的[域名系统](计算机网络%20-%20应用层.md#域名系统)。
+
+自测:
+
+1. HTTP/2 相比 HTTP/1.1 有哪些改进?(见[HTTP/2.0](HTTP.md#八http20))
+2. GET 和 POST 有什么区别?(见[GET 和 POST 比较](HTTP.md#九get-和-post-比较))
+3. DNS 的解析过程是怎样的?(见[域名系统](计算机网络%20-%20应用层.md#域名系统))
+
+动手:用 dig +trace 查询一个域名,对照[域名系统](计算机网络%20-%20应用层.md#域名系统)一节说出每一级服务器返回了什么。
+
+算法(二叉树,中等):[105. 从前序与中序遍历序列构造二叉树](https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/),题解见[剑指 Offer 7](7.%20重建二叉树.md)。
+
+### 第 43 天:从输入 URL 到显示页面
+
+Web 页面请求过程把前几天的内容串了起来。攻击技术中 XSS、CSRF 和 SQL 注入各自利用了什么、怎样防范要分清。
+
+阅读:《计算机网络 - 应用层》的[Web 页面请求过程](计算机网络%20-%20应用层.md#web-页面请求过程);《攻击技术》从[跨站脚本攻击](攻击技术.md#一跨站脚本攻击)读到[SQL 注入攻击](攻击技术.md#三sql-注入攻击)。
+
+自测:
+
+1. 从输入 URL 到页面显示经历了哪些过程?(见[Web 页面请求过程](计算机网络%20-%20应用层.md#web-页面请求过程))
+2. XSS 和 CSRF 有什么区别?分别怎样防范?(见[防范手段](攻击技术.md#防范手段)、[防范手段](攻击技术.md#防范手段-1))
+3. 怎样防止 SQL 注入?(见[防范手段](攻击技术.md#防范手段-2))
+
+算法(二叉树,中等):[98. 验证二叉搜索树](https://leetcode.cn/problems/validate-binary-search-tree/)。
+
+综合题:在浏览器输入 https://example.com 并回车,按 DNS、TCP、TLS、HTTP 的顺序说出每一步发送了什么,以及在哪些地方可能变慢。(见[Web 页面请求过程](计算机网络%20-%20应用层.md#web-页面请求过程)、[TCP 的三次握手](计算机网络%20-%20传输层.md#tcp-的三次握手)、[TLS 握手](HTTP.md#tls-握手))
+
+## 十一、操作系统
+
+### 第 44 天:进程与线程
+
+进程和线程的区别是操作系统中问得最多的问题。协程一节说明了 Java 21 为什么引入虚拟线程。
+
+阅读:《计算机操作系统 - 进程管理》从[进程与线程](计算机操作系统%20-%20进程管理.md#进程与线程)读到[进程状态的切换](计算机操作系统%20-%20进程管理.md#进程状态的切换)。
+
+自测:
+
+1. 进程和线程有什么区别?(见[进程与线程的区别](计算机操作系统%20-%20进程管理.md#3-区别))
+2. 上下文切换有哪些开销?(见[上下文切换](计算机操作系统%20-%20进程管理.md#4-上下文切换))
+3. 协程和线程有什么区别?Java 的虚拟线程是怎样实现的?(见[协程](计算机操作系统%20-%20进程管理.md#5-协程))
+
+动手:运行[附录 A2](计算机操作系统%20-%20进程管理.md#a2-平台线程与虚拟线程的开销)的代码,比较创建 1 万个平台线程和虚拟线程的耗时与内存。
+
+算法(回溯与搜索,中等):[200. 岛屿数量](https://leetcode.cn/problems/number-of-islands/),题解见[矩阵中的连通分量数目](Leetcode%20题解%20-%20搜索.md#2-矩阵中的连通分量数目)。
+
+### 第 45 天:系统调用、进程同步与通信
+
+系统调用解释了用户态和内核态之间的切换。调度算法和经典同步问题可以略读,重点是信号量和进程间的通信方式。
+
+阅读:《计算机操作系统 - 概述》的[系统调用](计算机操作系统%20-%20概述.md#系统调用);《计算机操作系统 - 进程管理》从[进程调度算法](计算机操作系统%20-%20进程管理.md#进程调度算法)读到[进程通信](计算机操作系统%20-%20进程管理.md#进程通信)。选读:[进阶:经典同步问题](计算机操作系统%20-%20进程管理%20-%20进阶.md#经典同步问题)。
+
+自测:
+
+1. 什么是用户态和内核态?什么时候会切换?(见[系统调用](计算机操作系统%20-%20概述.md#系统调用))
+2. 进程间通信有哪些方式?(见[进程通信](计算机操作系统%20-%20进程管理.md#进程通信))
+3. 信号量是怎样实现同步与互斥的?(见[信号量](计算机操作系统%20-%20进程管理.md#3-信号量))
+
+动手:运行[附录 A1](计算机操作系统%20-%20概述.md#a1-普通函数调用与系统调用的耗时)的代码,比较普通函数调用和系统调用的耗时。
+
+算法(回溯与搜索,中等):[46. 全排列](https://leetcode.cn/problems/permutations/),题解见[排列](Leetcode%20题解%20-%20搜索.md#5-排列)。
+
+### 第 46 天:死锁
+
+四个必要条件是预防死锁的出发点。银行家算法可以略读,最后一节的 Java 线程死锁和 MySQL 死锁对应实际排查。
+
+阅读:《计算机操作系统 - 死锁》从[必要条件](计算机操作系统%20-%20死锁.md#必要条件)读到[程序中的死锁](计算机操作系统%20-%20死锁.md#程序中的死锁)。
+
+自测:
+
+1. 死锁的四个必要条件是什么?(见[必要条件](计算机操作系统%20-%20死锁.md#必要条件))
+2. 怎样避免死锁?实际开发中最常用哪种方法?(见[死锁预防](计算机操作系统%20-%20死锁.md#死锁预防))
+3. 线上的 Java 程序发生死锁,怎样定位?(见[Java 线程死锁](计算机操作系统%20-%20死锁.md#1-java-线程死锁))
+
+动手:运行[附录 A1](计算机操作系统%20-%20死锁.md#a1-java-线程死锁)的代码,用 jstack 找到互相等待的两个线程。
+
+算法(回溯与搜索,中等):[93. 复原 IP 地址](https://leetcode.cn/problems/restore-ip-addresses/),题解见[IP 地址划分](Leetcode%20题解%20-%20搜索.md#2-ip-地址划分)。
+
+### 第 47 天:内存管理
+
+虚拟内存让每个进程有独立的地址空间。分页的地址转换和页面置换算法是重点,LRU 在缓存中还会再出现。
+
+阅读:《计算机操作系统 - 内存管理》从[虚拟内存](计算机操作系统%20-%20内存管理.md#虚拟内存)读到[分页与分段的比较](计算机操作系统%20-%20内存管理.md#分页与分段的比较)。
+
+自测:
+
+1. 什么是虚拟内存?为什么需要它?(见[虚拟内存](计算机操作系统%20-%20内存管理.md#虚拟内存))
+2. 有哪些页面置换算法?(见[页面置换算法](计算机操作系统%20-%20内存管理.md#页面置换算法))
+3. 分页和分段有什么区别?(见[分页与分段的比较](计算机操作系统%20-%20内存管理.md#分页与分段的比较))
+
+动手:运行[附录 A3](计算机操作系统%20-%20内存管理.md#a3-进程的地址空间布局)的代码,查看 /proc//maps 中代码段、堆和栈的位置。
+
+算法(回溯与搜索,中等):[22. 括号生成](https://leetcode.cn/problems/generate-parentheses/)。
+
+### 第 48 天:I/O 多路复用
+
+先区分五种 I/O 模型,再看 select、poll 和 epoll 的差别。epoll 的两种工作模式是常见的追问。
+
+阅读:《Socket》从[I/O 模型](Socket.md#一io-模型)读到[I/O 复用](Socket.md#二io-复用)。选读:[NIO](Java%20IO.md#七nio)。
+
+自测:
+
+1. select、poll 和 epoll 有什么区别?(见[select、poll 与 epoll 的比较](Socket.md#比较))
+2. epoll 的水平触发和边缘触发有什么区别?(见[工作模式](Socket.md#工作模式))
+3. BIO、NIO 和 AIO 有什么区别?(见[五大 I/O 模型比较](Socket.md#五大-io-模型比较))
+
+动手:运行[附录 A2](Socket.md#a2-lt-与-et-的读取过程)的代码,观察水平触发和边缘触发下每次 epoll_wait 返回后读到的数据。
+
+算法(回溯与搜索,中等):[78. 子集](https://leetcode.cn/problems/subsets/),题解见[子集](Leetcode%20题解%20-%20搜索.md#11-子集)。
+
+综合题:一个 Java 服务要同时维持 10 万个长连接:用每个连接一个线程的方式会遇到什么问题?用 epoll 或虚拟线程怎样解决?(见[上下文切换](计算机操作系统%20-%20进程管理.md#4-上下文切换)、[epoll](Socket.md#epoll)、[协程](计算机操作系统%20-%20进程管理.md#5-协程))
+
+## 十二、Spring 与设计模式
+
+### 第 49 天:IoC 与 Bean
+
+IoC 一节先说明为什么需要容器。Bean 的生命周期和循环依赖要能结合调用栈讲清。
+
+阅读:《Spring》的[IoC 与 Bean](Spring.md#一ioc-与-bean)。
+
+自测:
+
+1. Bean 的生命周期是怎样的?(见[Bean 的生命周期](Spring.md#bean-的生命周期))
+2. Spring 怎样解决循环依赖?为什么需要三级缓存?(见[循环依赖](Spring.md#循环依赖))
+3. 单例 Bean 是线程安全的吗?(见[Bean 的作用域与线程安全](Spring.md#bean-的作用域与线程安全))
+
+动手:按[附录 A3](Spring.md#a3-bean-创建的调用栈)的方法,在一个 Bean 的构造方法和 setter 方法中分别打印调用栈,找出 createBeanInstance 和 populateBean 的位置,说明实例化和属性注入的先后。
+
+算法(回溯与搜索,中等):[39. 组合总和](https://leetcode.cn/problems/combination-sum/),题解见[组合求和](Leetcode%20题解%20-%20搜索.md#8-组合求和)。
+
+### 第 50 天:AOP 与事务
+
+AOP 基于动态代理,自调用时切面不生效的原因也在这里。事务是 AOP 的一个应用,读完原理再看事务失效的几种情况会更容易理解。
+
+阅读:《Spring》从[AOP](Spring.md#二aop)读到[事务](Spring.md#三事务)。
+
+自测:
+
+1. AOP 的原理是什么?JDK 动态代理和 CGLIB 有什么区别?(见[动态代理](Spring.md#动态代理))
+2. Spring 事务的原理是什么?为什么加了 @Transactional 就有事务了?(见[事务的实现原理](Spring.md#事务的实现原理))
+3. 哪些情况下 @Transactional 会失效?(见[事务失效](Spring.md#事务失效))
+
+动手:写一个类,在方法 a() 中调用本类带 @Transactional 的方法 b(),验证 b() 的事务没有生效,再按[自调用](Spring.md#自调用)一节的方法修复。
+
+算法(动态规划,简单):[121. 买卖股票的最佳时机](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/),题解见[买卖股票最大的收益](Leetcode%20题解%20-%20贪心思想.md#5-买卖股票最大的收益)。
+
+### 第 51 天:Spring Boot、Spring MVC 与 MyBatis
+
+自动配置是 Spring Boot 的核心。MyBatis 一节中 #{} 和 ${} 的区别与 SQL 注入有关。
+
+阅读:《Spring》从[Spring Boot 自动配置](Spring.md#四spring-boot-自动配置)读到[Spring 中的设计模式](Spring.md#七spring-中的设计模式)。
+
+自测:
+
+1. Spring Boot 的自动配置是怎样实现的?(见[Spring Boot 自动配置](Spring.md#四spring-boot-自动配置))
+2. Spring MVC 处理一个请求的过程是怎样的?(见[请求的处理过程](Spring.md#请求的处理过程))
+3. MyBatis 中 #{} 和 ${} 有什么区别?(见[#{} 和 ${} 的区别](Spring.md#-和--的区别))
+
+动手:新建一个 Spring Boot 3 项目,启动时加上 --debug,在条件评估报告中找到 DataSourceAutoConfiguration 生效或不生效的原因。
+
+算法(动态规划,简单):[70. 爬楼梯](https://leetcode.cn/problems/climbing-stairs/),题解见[爬楼梯](Leetcode%20题解%20-%20动态规划.md#1-爬楼梯)。
+
+### 第 52 天:设计模式
+
+先看前言中下单流程用到的几种模式,再读单例和策略两篇。设计原则说明了这些模式背后的目标。
+
+阅读:《设计模式 - 目录》的[设计模式前言](设计模式%20-%20目录.md#一前言);《设计模式 - 单例》的[单例(Singleton)](设计模式%20%20-%20单例.md#单例singleton);《设计模式 - 策略》的[策略(Strategy)](设计模式%20-%20策略.md#9-策略strategy);《面向对象思想》的[设计原则](面向对象思想.md#三设计原则)。选读:[工厂方法](设计模式%20-%20工厂方法.md)、[代理](设计模式%20-%20代理.md)、[装饰](设计模式%20-%20装饰.md)、[观察者](设计模式%20-%20观察者.md)、[模板方法](设计模式%20-%20模板方法.md)、[责任链](设计模式%20-%20责任链.md)。
+
+自测:
+
+1. 单例模式有哪几种写法?双重检查锁为什么要加 volatile?(见[实现](设计模式%20%20-%20单例.md#实现))
+2. 项目中用过哪些设计模式?举一个例子。(见[项目中的用法](设计模式%20-%20目录.md#项目中的用法))
+3. Spring 中用到了哪些设计模式?(见[Spring 中的设计模式](Spring.md#七spring-中的设计模式))
+
+动手:把一段按支付方式(微信、支付宝、银行卡)分支的 if-else 改写成策略模式,并用 Spring 的 Map 注入所有实现。
+
+算法(动态规划,中等):[53. 最大子数组和](https://leetcode.cn/problems/maximum-subarray/),题解见[子数组最大的和](Leetcode%20题解%20-%20贪心思想.md#10-子数组最大的和)。
+
+综合题:用户下单接口中,Controller 调用 Service,Service 带有 @Transactional 并调用了日志切面:从请求到达 DispatcherServlet 到事务提交,依次经过了哪些对象和代理?(见[请求的处理过程](Spring.md#请求的处理过程)、[动态代理](Spring.md#动态代理)、[事务的实现原理](Spring.md#事务的实现原理))
+
+## 十三、分布式与消息队列
+
+### 第 53 天:微服务
+
+按拆分、组件、注册中心、服务调用、网关的顺序读。服务调用一节回答了实例下线和注册中心宕机时会发生什么。
+
+阅读:《微服务》从[从单体到微服务](微服务.md#一从单体到微服务)读到[网关](微服务.md#五网关)。选读:[配置中心](微服务.md#六配置中心)、[链路追踪](微服务.md#八链路追踪)、[负载均衡](集群.md#一负载均衡)。
+
+自测:
+
+1. Spring Cloud 有哪些常用组件?(见[组件](微服务.md#组件))
+2. 注册中心宕机后,服务之间还能调用吗?(见[注册中心宕机](微服务.md#注册中心宕机))
+3. 有哪些负载均衡策略?(见[负载均衡策略](微服务.md#负载均衡策略))
+
+算法(动态规划,中等):[5. 最长回文子串](https://leetcode.cn/problems/longest-palindromic-substring/),题解见[最长回文子串](Leetcode%20题解%20-%20字符串.md#10-最长回文子串)。
+
+### 第 54 天:分布式锁
+
+三种实现各有代价:数据库简单但性能低,Redis 快但要处理过期和续期,Zookeeper 可靠但部署重。重点是 Redis 的实现和 Redisson 的看门狗。
+
+阅读:《分布式》的[分布式锁](分布式.md#一分布式锁)。
+
+自测:
+
+1. 用 Redis 怎样实现分布式锁?为什么加锁和设置过期时间要用一条命令?(见[Redis 的 SET NX 指令](分布式.md#redis-的-set-nx-指令))
+2. 锁过期了业务还没执行完怎么办?(见[Redis 的 SET NX 指令](分布式.md#redis-的-set-nx-指令))
+3. 数据库、Redis、Zookeeper 三种实现怎样选择?(见[三种实现的对比](分布式.md#三种实现的对比))
+
+动手:按[附录 A3](分布式.md#a3-redisson-的可重入与看门狗)的方法,观察 Redisson 的锁在业务执行期间是怎样自动续期的。
+
+算法(动态规划,中等):[300. 最长递增子序列](https://leetcode.cn/problems/longest-increasing-subsequence/),题解见[最长递增子序列](Leetcode%20题解%20-%20动态规划.md#1-最长递增子序列)。
+
+### 第 55 天:分布式事务与 CAP
+
+2PC 保证强一致但会阻塞,TCC、Saga 和本地消息表用最终一致换取可用性。CAP 一节说明了为什么出现网络分区时只能在一致性和可用性之间选择。
+
+阅读:《分布式》从[分布式事务](分布式.md#二分布式事务)读到[BASE](分布式.md#四base)。选读:[Paxos](分布式.md#五paxos)、[Raft](分布式.md#六raft)。
+
+自测:
+
+1. 2PC 有什么问题?(见[2PC](分布式.md#2pc))
+2. TCC 和本地消息表分别适合什么场景?(见[TCC](分布式.md#tcc)、[本地消息表](分布式.md#本地消息表))
+3. 什么是 CAP?为什么分区容忍性必须满足?(见[权衡](分布式.md#权衡))
+
+算法(动态规划,中等):[1143. 最长公共子序列](https://leetcode.cn/problems/longest-common-subsequence/),题解见[最长公共子序列](Leetcode%20题解%20-%20动态规划.md#1-最长公共子序列)。
+
+### 第 56 天:RPC、分布式 ID 与幂等
+
+RPC 一节对比了它和 HTTP 的取舍。雪花算法要注意时钟回拨;幂等是重试、重复提交和重复消费的共同解法。
+
+阅读:《分布式》从[RPC](分布式.md#七rpc)读到[幂等](分布式.md#九幂等)。选读:[分布式定时任务](分布式.md#十分布式定时任务)。
+
+自测:
+
+1. RPC 和 HTTP 有什么区别?(见[RPC 与 HTTP](分布式.md#rpc-与-http))
+2. 雪花算法是怎样生成 ID 的?时钟回拨怎么办?(见[雪花算法](分布式.md#雪花算法))
+3. 怎样保证接口的幂等?(见[幂等](分布式.md#九幂等))
+
+动手:按[附录 A4](分布式.md#a4-重复提交与唯一索引)的步骤,用唯一索引拦住重复提交的订单。
+
+算法(动态规划,中等):[322. 零钱兑换](https://leetcode.cn/problems/coin-change/),题解见[找零钱的最少硬币数](Leetcode%20题解%20-%20动态规划.md#4-找零钱的最少硬币数)。
+
+### 第 57 天:消息队列的作用与可靠性
+
+先看消息队列用来做什么,以及它和 RPC 调用怎样选择。可靠性要分发送端、队列和接收端三段来看。
+
+阅读:《消息队列》从[消息模型](消息队列.md#一消息模型)读到[消息队列的可靠性](消息队列.md#三可靠性)。
+
+自测:
+
+1. 消息队列有什么作用?什么时候用 RPC,什么时候用消息队列?(见[使用场景](消息队列.md#二使用场景)、[与 RPC 调用的区别](消息队列.md#与-rpc-调用的区别))
+2. 怎样保证消息不丢失?(见[消息队列的可靠性](消息队列.md#三可靠性))
+3. 怎样避免重复消费?(见[接收端的可靠性](消息队列.md#接收端的可靠性))
+
+动手:阅读[附录 A2](消息队列.md#a2-重复消费),说出消费者在哪一步被杀死导致了重复扣款,再按[接收端的可靠性](消息队列.md#接收端的可靠性)一节的方法设计去重。
+
+算法(动态规划,中等):[64. 最小路径和](https://leetcode.cn/problems/minimum-path-sum/),题解见[矩阵的最小路径和](Leetcode%20题解%20-%20动态规划.md#1-矩阵的最小路径和)。
+
+### 第 58 天:顺序、积压与延迟消息
+
+顺序性、积压和延迟消息都是业务中常遇到的问题。处理积压前先区分是生产突增还是消费变慢。
+
+阅读:《消息队列》从[顺序性](消息队列.md#四顺序性)读到[延迟消息与死信队列](消息队列.md#六延迟消息与死信队列)。
+
+自测:
+
+1. 怎样保证消息按顺序消费?(见[顺序性](消息队列.md#四顺序性))
+2. 消息大量积压时怎样处理?(见[消息堆积](消息队列.md#五消息堆积))
+3. 怎样实现订单 30 分钟未支付自动取消?(见[延迟消息](消息队列.md#延迟消息))
+
+动手:按[附录 A3](消息队列.md#a3-消费积压)的输出,说出每个分区积压了多少条消息,LAG 一列是怎样算出来的。
+
+算法(动态规划,中等):[198. 打家劫舍](https://leetcode.cn/problems/house-robber/),题解见[强盗抢劫](Leetcode%20题解%20-%20动态规划.md#2-强盗抢劫)。
+
+### 第 59 天:Kafka
+
+按分区与消费者组、副本、存储、吞吐量的顺序读。副本与可靠性一节说明了 acks 和 ISR 怎样保证消息不丢。
+
+阅读:《消息队列》的[Kafka](消息队列.md#七kafka)。
+
+自测:
+
+1. Kafka 为什么吞吐量高?(见[为什么吞吐量高](消息队列.md#为什么吞吐量高))
+2. Kafka 的副本机制怎样保证可靠性?(见[副本与可靠性](消息队列.md#副本与可靠性))
+3. 消费者组中的消费者比分区多时会怎样?(见[分区与消费者组](消息队列.md#分区与消费者组))
+
+动手:阅读[附录 A1](消息队列.md#a1-批量对吞吐量的影响),说明 batch.size 和 linger.ms 是怎样影响吞吐量和延迟的。
+
+算法(双指针,困难):[42. 接雨水](https://leetcode.cn/problems/trapping-rain-water/),题解见[接雨水](Leetcode%20题解%20-%20双指针.md#9-接雨水)。
+
+综合题:下单后需要扣库存、发优惠券、发短信:哪些步骤放在同一个事务中,哪些通过消息队列异步完成?消息丢失或重复时分别会怎样?(见[异步处理](消息队列.md#异步处理)、[本地消息表](分布式.md#本地消息表)、[幂等](分布式.md#九幂等))
+
+## 十四、系统设计
+
+### 第 60 天:性能与可用性
+
+性能指标中的分位数比平均值更能反映用户的感受。限流、降级与熔断一节较长,三者的区别和各自的触发条件是重点。
+
+阅读:《系统设计基础》从[性能](系统设计基础.md#一性能)读到[安全性](系统设计基础.md#五安全性)。选读:[集群下的 Session 管理](集群.md#二集群下的-session-管理)。
+
+自测:
+
+1. 常用的限流算法有哪些?分别有什么特点?(见[限流算法](系统设计基础.md#1-限流算法))
+2. 限流、熔断和降级有什么区别?熔断器怎样从打开恢复到关闭?(见[熔断](系统设计基础.md#3-熔断)、[降级](系统设计基础.md#4-降级))
+3. QPS、TPS 和 TP99 分别是什么意思?(见[性能指标](系统设计基础.md#性能指标))
+
+动手:运行[附录 A14](系统设计基础.md#a14-guava-的-ratelimiter)的代码,比较 acquire() 和 tryAcquire() 在令牌不足时的行为。
+
+算法(滑动窗口,困难):[239. 滑动窗口最大值](https://leetcode.cn/problems/sliding-window-maximum/),题解见[剑指 Offer 59](59.%20滑动窗口的最大值.md)。
+
+### 第 61 天:秒杀与抢红包
+
+秒杀把前面学过的缓存、消息队列和数据库锁组合在一起。防止超卖和库存一致两节回答了最常见的追问;红包和抽奖是同类场景。
+
+阅读:《系统设计基础》的[秒杀系统](系统设计基础.md#六秒杀系统)。
+
+自测:
+
+1. 秒杀系统怎样防止超卖?悲观锁、乐观锁和条件更新各有什么问题?(见[防止超卖的几种做法](系统设计基础.md#防止超卖的几种做法))
+2. Redis 预扣库存成功、订单却创建失败,怎样保证两边一致?(见[Redis 与数据库的库存一致](系统设计基础.md#redis-与数据库的库存一致))
+3. 抢红包时金额怎样拆分?(见[同类场景:抢券、抢红包与抽奖](系统设计基础.md#同类场景抢券抢红包与抽奖))
+
+动手:按[附录 A1](系统设计基础.md#a1-秒杀)的方法,用 Lua 脚本扣减库存,并发请求后核对库存和订单数是否一致。
+
+算法(滑动窗口,困难):[76. 最小覆盖子串](https://leetcode.cn/problems/minimum-window-substring/),题解见[最小覆盖子串](Leetcode%20题解%20-%20双指针.md#10-最小覆盖子串)。
+
+### 第 62 天:海量数据、短链与 Feed 流
+
+这三章都是常见的场景题。海量数据处理的思路是分而治之;短链和 Feed 流重点看存储方式和推拉模式的取舍。
+
+阅读:《系统设计基础》从[海量数据处理](系统设计基础.md#七海量数据处理)读到[Feed 流](系统设计基础.md#九feed-流)。
+
+自测:
+
+1. 100 GB 的日志中每行是一个 URL,内存只有 4 GB,怎样找出出现次数最多的 10 个?(见[出现次数最多的 K 个](系统设计基础.md#出现次数最多的-k-个))
+2. 短链跳转用 301 还是 302?(见[跳转用 301 还是 302](系统设计基础.md#跳转用-301-还是-302))
+3. Feed 流用推模式还是拉模式?(见[拉模式与推模式](系统设计基础.md#拉模式与推模式))
+
+动手:阅读[附录 A2](系统设计基础.md#a2-短链跳转),说明浏览器第二次访问返回 301 的短链时为什么没有请求服务器,这对统计点击量有什么影响。
+
+算法(链表,困难):[25. K 个一组翻转链表](https://leetcode.cn/problems/reverse-nodes-in-k-group/),题解见[K 个一组翻转链表](Leetcode%20题解%20-%20链表.md#11-k-个一组翻转链表)。
+
+### 第 63 天:高并发系统的设计
+
+本章以一个具体的系统为例,从容量估算讲到读写路径和压测。读时把每一步和前面学过的组件对应起来。
+
+阅读:《系统设计基础》的[高并发系统的设计](系统设计基础.md#十高并发系统的设计)。
+
+自测:
+
+1. 设计一个系统前,怎样估算需要的机器和存储?(见[容量估算](系统设计基础.md#容量估算))
+2. QPS 突然增长到原来的 10 倍,应该先做什么?(见[QPS 突增 10 倍时先做什么](系统设计基础.md#qps-突增-10-倍时先做什么))
+3. 怎样做压测和容量规划?(见[压测与容量规划](系统设计基础.md#压测与容量规划))
+
+算法(链表,困难):[23. 合并 K 个升序链表](https://leetcode.cn/problems/merge-k-sorted-lists/),题解见[合并 K 个有序链表](Leetcode%20题解%20-%20链表.md#13-合并-k-个有序链表)。
+
+### 第 64 天:Docker
+
+前几章介绍 Docker 解决什么问题,可以略读。镜像与容器一节的 Dockerfile 写法和问题排查是实际工作中最常用的部分。
+
+阅读:《Docker》从[解决的问题](Docker.md#一解决的问题)读到[镜像与容器](Docker.md#六镜像与容器)。
+
+自测:
+
+1. Docker 和虚拟机有什么区别?(见[与虚拟机的比较](Docker.md#二与虚拟机的比较))
+2. 怎样写一个体积小、构建快的 Dockerfile?(见[用 Dockerfile 构建镜像](Docker.md#用-dockerfile-构建镜像))
+3. 容器启动后马上退出,怎样排查?(见[容器的常见问题排查](Docker.md#容器的常见问题排查))
+
+动手:为一个 Spring Boot 项目写多阶段构建的 Dockerfile,对比单阶段和多阶段构建的镜像大小。
+
+算法(二叉树,困难):[124. 二叉树中的最大路径和](https://leetcode.cn/problems/binary-tree-maximum-path-sum/)。
+
+### 第 65 天:Kubernetes
+
+Kubernetes 重点看 Pod 的创建过程、Service 和 Ingress 的作用,以及 Pod 异常时怎样排查。
+
+阅读:《Docker》的[Kubernetes](Docker.md#七kubernetes)。选读:[大文件上传](系统设计基础.md#十一大文件上传)、[权限系统](系统设计基础.md#十二权限系统)。
+
+自测:
+
+1. 创建一个 Pod 时,Kubernetes 内部经过了哪些步骤?(见[Pod 的创建过程](Docker.md#pod-的创建过程))
+2. Service 和 Ingress 有什么区别?(见[Service 与 Ingress](Docker.md#service-与-ingress))
+3. Pod 一直处于 CrashLoopBackOff 状态,怎样排查?(见[Pod 异常状态的排查](Docker.md#pod-异常状态的排查))
+
+算法(动态规划,困难):[72. 编辑距离](https://leetcode.cn/problems/edit-distance/),题解见[编辑距离](Leetcode%20题解%20-%20动态规划.md#2-编辑距离)。
+
+综合题:设计一个秒杀系统:从页面、网关、服务、缓存、消息队列到数据库,每一层分别做什么来扛住流量、防止超卖?出现故障时怎样降级?(见[秒杀系统](系统设计基础.md#六秒杀系统)、[读路径的逐层分流](系统设计基础.md#读路径的逐层分流)、[限流、降级与熔断](系统设计基础.md#限流降级与熔断))
diff --git a/notes/学习路线 - 大模型应用开发.md b/notes/学习路线 - 大模型应用开发.md
new file mode 100644
index 00000000..8cdb9481
--- /dev/null
+++ b/notes/学习路线 - 大模型应用开发.md
@@ -0,0 +1,436 @@
+# 学习路线 - 大模型应用开发
+
+* [学习路线 - 大模型应用开发](#学习路线---大模型应用开发)
+ * [一、说明](#一说明)
+ * [二、开始之前](#二开始之前)
+ * [三、每天的安排](#三每天的安排)
+ * [四、大模型基础](#四大模型基础)
+ * [第 1 天:Token 与 Transformer](#第-1-天token-与-transformer)
+ * [第 2 天:注意力机制](#第-2-天注意力机制)
+ * [第 3 天:生成、KV Cache 与采样](#第-3-天生成kv-cache-与采样)
+ * [第 4 天:训练过程](#第-4-天训练过程)
+ * [第 5 天:微调与量化](#第-5-天微调与量化)
+ * [第 6 天:幻觉与推理模型](#第-6-天幻觉与推理模型)
+ * [五、提示词与上下文](#五提示词与上下文)
+ * [第 7 天:提示词与结构化输出](#第-7-天提示词与结构化输出)
+ * [第 8 天:上下文工程](#第-8-天上下文工程)
+ * [六、检索增强生成](#六检索增强生成)
+ * [第 9 天:RAG 的流程、文档解析与切片](#第-9-天rag-的流程文档解析与切片)
+ * [第 10 天:Embedding 与向量索引](#第-10-天embedding-与向量索引)
+ * [第 11 天:向量数据库、混合检索与重排](#第-11-天向量数据库混合检索与重排)
+ * [第 12 天:Top-K 与查询改写](#第-12-天top-k-与查询改写)
+ * [第 13 天:RAG 的评测与扩展](#第-13-天rag-的评测与扩展)
+ * [七、Agent](#七agent)
+ * [第 14 天:Agent 与工具调用](#第-14-天agent-与工具调用)
+ * [第 15 天:意图识别与规划](#第-15-天意图识别与规划)
+ * [第 16 天:记忆与多轮对话](#第-16-天记忆与多轮对话)
+ * [第 17 天:MCP 与 Skills](#第-17-天mcp-与-skills)
+ * [第 18 天:多 Agent 与 Harness](#第-18-天多-agent-与-harness)
+ * [第 19 天:开发框架、可靠性与评测](#第-19-天开发框架可靠性与评测)
+ * [八、工程实践](#八工程实践)
+ * [第 20 天:流式输出、成本与部署](#第-20-天流式输出成本与部署)
+ * [第 21 天:稳定性与安全](#第-21-天稳定性与安全)
+
+
+
+## 一、说明
+
+适合会一门编程语言(Python 或 Java)、准备大模型应用开发面试的读者,每天约 1~1.5 小时。动手部分需要一个兼容 OpenAI 接口的模型服务(例如 DeepSeek)或本地的 Qwen2.5 小模型;学过 Java 后端路线的读者可以跳过算法题。
+
+每天先读完阅读范围,再不看笔记回答自测题,答完打开链接核对。选读可以跳过。
+
+## 二、开始之前
+
+本路线不讲语法。动手部分大多用 Python,开始前先确认能独立写出下面 5 个程序:
+
+1. 调用 https://api.github.com/repos/CyC2018/CS-Notes ,解析返回的 JSON,打印 stargazers_count;
+2. 读取一个 JSON Lines 文件(每行一个订单,含 user_id 和 amount),统计每个用户的订单数和总金额,写入 JSON 文件;
+3. 把一段长文本切成不超过 200 字的片段,只在句号处切分;
+4. 用列表保存对话历史,只保留最近 10 条消息;
+5. 请求超时时重试,最多 3 次,每次等待的时间翻倍。
+
+写不出来的,先补对应的语法:HTTP 请求与 JSON、文件读写、字符串、列表、异常与循环。用 Java 写同样的程序也可以。
+
+## 三、每天的安排
+
+| 天 | 主题 | 阅读 | 算法 |
+| :-: | --- | --- | --- |
+| | **大模型基础** | | |
+| [1](#第-1-天token-与-transformer) | Token 与 Transformer | 大模型应用:Token 与分词~Transformer 结构 | 1. 两数之和 |
+| [2](#第-2-天注意力机制) | 注意力机制 | 大模型应用:注意力机制 | 15. 三数之和 |
+| [3](#第-3-天生成kv-cache-与采样) | 生成、KV Cache 与采样 | 大模型应用:自回归生成与 KV Cache~采样参数 | 3. 无重复字符的最长子串 |
+| [4](#第-4-天训练过程) | 训练过程 | 大模型应用:训练过程 | 206. 反转链表 |
+| [5](#第-5-天微调与量化) | 微调与量化 | 大模型应用:微调~量化 | 21. 合并两个有序链表 |
+| [6](#第-6-天幻觉与推理模型) | 幻觉与推理模型 | 大模型应用:幻觉~推理模型 | 141. 环形链表 |
+| | **提示词与上下文** | | |
+| [7](#第-7-天提示词与结构化输出) | 提示词与结构化输出 | 大模型应用:提示词~结构化输出 | 146. LRU 缓存 |
+| [8](#第-8-天上下文工程) | 上下文工程 | 大模型应用:上下文工程 | 20. 有效的括号 |
+| | **检索增强生成** | | |
+| [9](#第-9-天rag-的流程文档解析与切片) | RAG 的流程、文档解析与切片 | 大模型应用:RAG 的流程~RAG 的切片 | 33. 搜索旋转排序数组 |
+| [10](#第-10-天embedding-与向量索引) | Embedding 与向量索引 | 大模型应用:Embedding 与相似度~向量索引 | 215. 数组中的第 K 个最大元素 |
+| [11](#第-11-天向量数据库混合检索与重排) | 向量数据库、混合检索与重排 | 大模型应用:向量数据库~重排 | 912. 排序数组(手写快速排序) |
+| [12](#第-12-天top-k-与查询改写) | Top-K 与查询改写 | 大模型应用:Top-K 与阈值~查询改写 | 102. 二叉树的层序遍历 |
+| [13](#第-13-天rag-的评测与扩展) | RAG 的评测与扩展 | 大模型应用:RAG 的评测与优化~多模态 RAG | 236. 二叉树的最近公共祖先 |
+| | **Agent** | | |
+| [14](#第-14-天agent-与工具调用) | Agent 与工具调用 | 大模型应用:Agent 是什么~Agent 与工作流 | 200. 岛屿数量 |
+| [15](#第-15-天意图识别与规划) | 意图识别与规划 | 大模型应用:意图识别与路由~Agent 的规划 | 46. 全排列 |
+| [16](#第-16-天记忆与多轮对话) | 记忆与多轮对话 | 大模型应用:Agent 的记忆~多轮对话 | 121. 买卖股票的最佳时机 |
+| [17](#第-17-天mcp-与-skills) | MCP 与 Skills | 大模型应用:MCP~Skills | 70. 爬楼梯 |
+| [18](#第-18-天多-agent-与-harness) | 多 Agent 与 Harness | 大模型应用:多 Agent~Harness | 53. 最大子数组和 |
+| [19](#第-19-天开发框架可靠性与评测) | 开发框架、可靠性与评测 | 大模型应用:开发框架~可靠性与评测 | 5. 最长回文子串 |
+| | **工程实践** | | |
+| [20](#第-20-天流式输出成本与部署) | 流式输出、成本与部署 | 大模型应用:流式输出~推理服务部署 | 300. 最长递增子序列 |
+| [21](#第-21-天稳定性与安全) | 稳定性与安全 | 大模型应用:稳定性~大模型应用的安全 | 25. K 个一组翻转链表 |
+
+## 四、大模型基础
+
+### 第 1 天:Token 与 Transformer
+
+Token 是模型处理文本的单位,也是计费和上下文长度的单位。Transformer 一节先看整体结构,再看参数分布在哪里;混合专家可以略读。
+
+阅读:《大模型应用》从[Token 与分词](大模型应用.md#token-与分词)读到[Transformer 结构](大模型应用.md#transformer-结构)。
+
+自测:
+
+1. Transformer 的结构是怎样的?(见[整体结构](大模型应用.md#1-整体结构))
+2. 大模型的参数主要分布在哪里?(见[参数都在哪里](大模型应用.md#2-参数都在哪里))
+3. 什么是 Token?同样的内容,中文和英文的 Token 数一样吗?(见[Token 与分词](大模型应用.md#token-与分词))
+
+动手:下载 Qwen2.5-7B-Instruct 的 config.json,按[参数都在哪里](大模型应用.md#2-参数都在哪里)一节的方法算出一层中注意力和前馈网络的参数量,再与[附录 A16](大模型应用.md#a16-qwen25-7b-与-qwen3-30b-a3b-的参数量)对照。
+
+算法(哈希表,简单):[1. 两数之和](https://leetcode.cn/problems/two-sum/),题解见[数组中两个数的和为给定值](Leetcode%20题解%20-%20哈希表.md#1-数组中两个数的和为给定值)。
+
+### 第 2 天:注意力机制
+
+注意力是 Transformer 的核心,对着“饿”的例子把 Q、K、V 的计算走一遍。FlashAttention 一节说明了长上下文时计算的瓶颈在哪里。
+
+阅读:《大模型应用》的[注意力机制](大模型应用.md#注意力机制)。选读:[进阶:注意力机制](大模型应用%20-%20进阶.md#注意力机制)。
+
+自测:
+
+1. 注意力的计算公式是什么?为什么要除以 √d?(见[注意力机制](大模型应用.md#注意力机制))
+2. 多头注意力有什么作用?(见[多头注意力](大模型应用.md#1-多头注意力))
+3. FlashAttention 解决了什么问题?(见[计算量与 FlashAttention](大模型应用.md#2-计算量与-flashattention))
+
+动手:用 NumPy 实现单头注意力 softmax(QKT / √d) V,输入 3 个 Token、维度为 4 的随机矩阵,打印注意力权重并检查每一行的和是否为 1。
+
+算法(双指针,中等):[15. 三数之和](https://leetcode.cn/problems/3sum/),题解见[三数之和](Leetcode%20题解%20-%20双指针.md#8-三数之和)。
+
+### 第 3 天:生成、KV Cache 与采样
+
+模型每次只生成一个 Token。KV Cache 避免重复计算前面 Token 的 K 和 V,也是推理时显存的主要来源。采样参数决定每一步怎样从概率分布中选出 Token。
+
+阅读:《大模型应用》从[自回归生成与 KV Cache](大模型应用.md#自回归生成与-kv-cache)读到[采样参数](大模型应用.md#采样参数)。选读:[进阶:自回归生成与 KV Cache](大模型应用%20-%20进阶.md#自回归生成与-kv-cache)。
+
+自测:
+
+1. KV Cache 是什么?为什么能加快生成?(见[自回归生成与 KV Cache](大模型应用.md#自回归生成与-kv-cache))
+2. MHA、MQA 和 GQA 有什么区别?(见[自回归生成与 KV Cache](大模型应用.md#自回归生成与-kv-cache))
+3. temperature 和 top-p 分别控制什么?(见[采样参数](大模型应用.md#采样参数))
+
+动手:按[显存:权重与 KV Cache](大模型应用.md#3-显存权重与-kv-cache)一节的方法,算出 Qwen2.5-7B 在 32K 上下文、同时处理 8 个请求时 KV Cache 占用的显存。
+
+算法(滑动窗口,中等):[3. 无重复字符的最长子串](https://leetcode.cn/problems/longest-substring-without-repeating-characters/),题解见[剑指 Offer 48](48.%20最长不含重复字符的子字符串.md)。
+
+### 第 4 天:训练过程
+
+按预训练、监督微调、偏好对齐三个阶段读。PPO、DPO 和 GRPO 的区别是常见的追问,比较时看每种方法需要哪些模型。
+
+阅读:《大模型应用》的[训练过程](大模型应用.md#训练过程)。选读:[进阶:训练过程](大模型应用%20-%20进阶.md#训练过程)。
+
+自测:
+
+1. 大模型的训练分为哪几个阶段?(见[三个阶段](大模型应用.md#1-三个阶段))
+2. PPO、DPO 和 GRPO 有什么区别?(见[DPO](大模型应用.md#3-dpo)、[GRPO 与三者的选择](大模型应用.md#4-grpo-与三者的选择))
+3. RLHF 中的奖励模型是怎样训练的?(见[RLHF 与 PPO](大模型应用.md#2-rlhf-与-ppo))
+
+动手:用 Python 按[DPO](大模型应用.md#3-dpo)一节的公式,对几组给定的 chosen 和 rejected 对数概率计算 DPO 损失,观察 β 取 0.1 和 0.5 时损失的变化。
+
+算法(链表,简单):[206. 反转链表](https://leetcode.cn/problems/reverse-linked-list/),题解见[链表反转](Leetcode%20题解%20-%20链表.md#2-链表反转)。
+
+### 第 5 天:微调与量化
+
+先判断什么时候该微调、什么时候用 RAG 或提示词就够了。LoRA 是最常用的微调方法;量化一节中 GPTQ 与 AWQ 可以略读。
+
+阅读:《大模型应用》从[微调](大模型应用.md#微调)读到[量化](大模型应用.md#量化)。选读:[进阶:微调](大模型应用%20-%20进阶.md#微调)、[进阶:量化](大模型应用%20-%20进阶.md#量化)。
+
+自测:
+
+1. LoRA 的原理是什么?LoRA 和 QLoRA 有什么区别?(见[LoRA](大模型应用.md#3-lora)、[QLoRA 与全参数微调](大模型应用.md#4-qlora-与全参数微调))
+2. 什么时候用微调,什么时候用 RAG?(见[什么时候微调](大模型应用.md#1-什么时候微调))
+3. 什么是灾难性遗忘?怎样缓解?(见[灾难性遗忘](大模型应用.md#5-灾难性遗忘))
+
+动手:按[附录 A17](大模型应用.md#a17-sft-样本的损失掩码)的方法,用 Qwen2.5 的对话模板构造一条 SFT 样本,标出哪些 Token 参与计算损失。
+
+算法(链表,简单):[21. 合并两个有序链表](https://leetcode.cn/problems/merge-two-sorted-lists/),题解见[归并两个有序的链表](Leetcode%20题解%20-%20链表.md#3-归并两个有序的链表)。
+
+### 第 6 天:幻觉与推理模型
+
+幻觉一节按定义、原因、缓解的顺序读,Agent 中的幻觉是调用工具后出现的新问题。推理模型一节很短,说明它和普通模型的区别。
+
+阅读:《大模型应用》从[幻觉](大模型应用.md#幻觉)读到[推理模型](大模型应用.md#推理模型)。
+
+自测:
+
+1. 什么是大模型的幻觉?为什么会产生?(见[什么是幻觉](大模型应用.md#1-什么是幻觉)、[为什么会产生](大模型应用.md#2-为什么会产生))
+2. 怎样检测和缓解幻觉?(见[检测与缓解](大模型应用.md#3-检测与缓解))
+3. Agent 调用工具时会出现哪些幻觉?(见[Agent 中的幻觉](大模型应用.md#4-agent-中的幻觉))
+
+动手:阅读[附录 A19](大模型应用.md#a19-agent-中的工具幻觉),说出模型在什么情况下编造了工具或参数,以及校验是怎样拦住它的。
+
+算法(链表,简单):[141. 环形链表](https://leetcode.cn/problems/linked-list-cycle/),题解见[判断链表是否存在环](Leetcode%20题解%20-%20双指针.md#6-判断链表是否存在环)。
+
+综合题:用户问“我们公司的年假有几天”,模型回答了一个错误的天数:可能的原因有哪些?用提示词、RAG、微调分别能解决哪一部分?(见[为什么会产生](大模型应用.md#2-为什么会产生)、[什么时候微调](大模型应用.md#1-什么时候微调)、[检测与缓解](大模型应用.md#3-检测与缓解))
+
+## 五、提示词与上下文
+
+### 第 7 天:提示词与结构化输出
+
+提示词一节讲写法。结构化输出讲怎样让程序可靠地解析模型的回答:约束解码从源头保证格式,解析失败时还要修复和重试。
+
+阅读:《大模型应用》从[提示词](大模型应用.md#提示词)读到[结构化输出](大模型应用.md#结构化输出)。
+
+自测:
+
+1. 怎样写好一个提示词?(见[提示词](大模型应用.md#提示词))
+2. 怎样让模型稳定地输出合法的 JSON?(见[约束解码](大模型应用.md#1-约束解码)、[输出无法解析时](大模型应用.md#2-输出无法解析时))
+3. 输出格式正确,内容就一定正确吗?(见[格式正确不等于内容正确](大模型应用.md#3-格式正确不等于内容正确))
+
+动手:按[附录 A22](大模型应用.md#a22-模型输出-json-的修复校验与重试)的方法,让模型从一段订单描述中抽取 JSON,用 JSON Schema 校验,失败时把错误信息发回模型重试。
+
+算法(设计,中等):[146. LRU 缓存](https://leetcode.cn/problems/lru-cache/),题解见[LRU](缓存.md#七lru)。
+
+### 第 8 天:上下文工程
+
+上下文工程关心每次调用放进什么、按什么顺序放。压缩是重点:什么时候压缩、怎样压缩,以及怎样保证关键事实不丢。
+
+阅读:《大模型应用》的[上下文工程](大模型应用.md#上下文工程)。
+
+自测:
+
+1. 上下文工程和提示词工程有什么区别?(见[上下文工程](大模型应用.md#上下文工程))
+2. 对话历史太长时怎样压缩?什么时候压缩?(见[压缩的方法与时机](大模型应用.md#3-压缩的方法与时机))
+3. 压缩后怎样保证关键事实不丢失?(见[压缩后不丢关键事实](大模型应用.md#4-压缩后不丢关键事实))
+
+动手:按[附录 A4](大模型应用.md#a4-普通摘要与结构化摘要)的方法,对一段 20 轮的客服对话分别做普通摘要和结构化摘要,检查订单号、金额等关键事实是否保留。
+
+算法(栈和队列,简单):[20. 有效的括号](https://leetcode.cn/problems/valid-parentheses/),题解见[用栈实现括号匹配](Leetcode%20题解%20-%20栈和队列.md#4-用栈实现括号匹配)。
+
+综合题:一个客服机器人的每次请求都要带上系统提示词、工具定义、用户资料和最近的对话:怎样安排它们的顺序?Token 超出预算时先裁掉什么?(见[放什么、按什么顺序](大模型应用.md#1-放什么按什么顺序)、[Token 预算](大模型应用.md#2-token-预算)、[让前缀缓存命中](大模型应用.md#3-让前缀缓存命中))
+
+## 六、检索增强生成
+
+### 第 9 天:RAG 的流程、文档解析与切片
+
+先看 RAG 的完整流程,再看前两步:文档解析和切片。切片的对比实验说明了不同切法对召回率的影响。
+
+阅读:《大模型应用》从[RAG 的流程](大模型应用.md#流程)读到[RAG 的切片](大模型应用.md#切片)。
+
+自测:
+
+1. RAG 的完整流程是怎样的?(见[RAG 的流程](大模型应用.md#流程))
+2. 有哪些切片策略?切片大小怎么确定?(见[常用做法](大模型应用.md#2-常用做法)、[怎么确定切片大小](大模型应用.md#4-怎么确定切片大小))
+3. PDF 为什么难解析?表格怎么处理?(见[PDF 为什么难解析](大模型应用.md#1-pdf-为什么难解析)、[版面分析](大模型应用.md#2-版面分析))
+
+动手:用 pypdf 提取一份 PDF 的文本,分别按固定长度和按段落切片,比较两种切片的边界是否切断了句子。
+
+算法(二分查找,中等):[33. 搜索旋转排序数组](https://leetcode.cn/problems/search-in-rotated-sorted-array/)。
+
+### 第 10 天:Embedding 与向量索引
+
+Embedding 把文本变成向量,相似度检索靠它完成。向量索引回答数据量大时为什么要用近似检索,重点是 HNSW,IVF 与 PQ 可以略读。
+
+阅读:《大模型应用》从[Embedding 与相似度](大模型应用.md#embedding-与相似度)读到[向量索引](大模型应用.md#向量索引)。
+
+自测:
+
+1. 文本是怎样变成向量的?为什么用余弦相似度?(见[向量与余弦相似度](大模型应用.md#1-向量与余弦相似度))
+2. HNSW 的原理是什么?(见[HNSW](大模型应用.md#2-hnsw))
+3. 为什么需要近似最近邻检索?(见[为什么需要近似检索](大模型应用.md#1-为什么需要近似检索))
+
+动手:用 bge-small-zh 等 Embedding 模型计算几组句子的余弦相似度,参照[附录 A1](大模型应用.md#a1-embedding-相似度与-bm25),找出一组语义相近但字面不同、向量检索优于关键词检索的例子。
+
+算法(排序与堆,中等):[215. 数组中的第 K 个最大元素](https://leetcode.cn/problems/kth-largest-element-in-an-array/),题解见[Kth Element](Leetcode%20题解%20-%20排序.md#1-kth-element)。
+
+### 第 11 天:向量数据库、混合检索与重排
+
+向量数据库除了检索还要处理过滤和更新。混合检索和重排分别弥补向量检索的两个不足:精确的关键词匹配不准,相似度排序不够精细。
+
+阅读:《大模型应用》从[向量数据库](大模型应用.md#向量数据库)读到[重排](大模型应用.md#重排)。
+
+自测:
+
+1. 为什么要把向量检索和关键词检索结合起来?(见[混合检索](大模型应用.md#混合检索))
+2. 向量检索已经计算了相似度,为什么还要重排?(见[重排](大模型应用.md#重排))
+3. 向量数据库中的数据怎样更新和删除?(见[更新与删除](大模型应用.md#2-更新与删除))
+
+动手:写一个函数,按[混合检索](大模型应用.md#混合检索)一节的 RRF 公式把 BM25 和向量检索返回的两个排序列表融合成一个,用几组数据验证结果。
+
+算法(排序与堆,中等):[912. 排序数组(手写快速排序)](https://leetcode.cn/problems/sort-an-array/),题解见[快速排序](算法%20-%20排序.md#快速排序)。
+
+### 第 12 天:Top-K 与查询改写
+
+注意区分召回的 K 和放进提示词的 K。查询改写处理用户的提问与文档措辞不一致的情况。
+
+阅读:《大模型应用》从[Top-K 与阈值](大模型应用.md#top-k-与阈值)读到[查询改写](大模型应用.md#查询改写)。
+
+自测:
+
+1. 召回的 Top-K 和放进提示词的 Top-K 怎么定?(见[Top-K 与阈值](大模型应用.md#top-k-与阈值))
+2. 相似度阈值怎么设置?(见[相似度阈值](大模型应用.md#4-相似度阈值))
+3. 为什么要改写用户的查询?有哪些方法?(见[查询改写](大模型应用.md#查询改写))
+
+动手:阅读[附录 A12](大模型应用.md#a12-切片方式top-k-与相似度阈值),说出切片方式、Top-K 和阈值分别怎样影响召回率和放进提示词的 Token 数。
+
+算法(二叉树,中等):[102. 二叉树的层序遍历](https://leetcode.cn/problems/binary-tree-level-order-traversal/),题解见[剑指 Offer 32.2](32.2%20把二叉树打印成多行.md)。
+
+### 第 13 天:RAG 的评测与扩展
+
+评测分检索和生成两部分,排查顺序一节把前几天的内容串成了一个排查流程。长上下文、GraphRAG 和多模态 RAG 可以略读。
+
+阅读:《大模型应用》从[RAG 的评测与优化](大模型应用.md#评测与优化)读到[多模态 RAG](大模型应用.md#多模态-rag)。
+
+自测:
+
+1. 怎样评测一个 RAG 系统?(见[检索指标](大模型应用.md#1-检索指标)、[生成指标](大模型应用.md#2-生成指标))
+2. RAG 回答得不好,按什么顺序排查?(见[排查顺序](大模型应用.md#4-排查顺序))
+3. 评测集怎样构建?(见[评测集](大模型应用.md#3-评测集))
+
+动手:为 10 个问题标注相关的文档,用自己的检索结果计算 Recall@5 和 MRR,对照[检索指标](大模型应用.md#1-检索指标)一节核对公式。
+
+算法(二叉树,中等):[236. 二叉树的最近公共祖先](https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/),题解见[二叉树的最近公共祖先](Leetcode%20题解%20-%20树.md#5-二叉树的最近公共祖先)。
+
+综合题:公司内部的知识库问答上线后,用户反馈经常答非所问:按什么顺序排查?每一步看什么指标?分别可能改哪个环节?(见[排查顺序](大模型应用.md#4-排查顺序)、[检索指标](大模型应用.md#1-检索指标)、[切片](大模型应用.md#切片)、[重排](大模型应用.md#重排))
+
+## 七、Agent
+
+### 第 14 天:Agent 与工具调用
+
+模型只输出文本,工具由应用代码执行,这是理解 Agent 的出发点。Agent 循环一节给出了完整的代码,最后一节说明什么时候不需要 Agent。
+
+阅读:《大模型应用》从[Agent 是什么](大模型应用.md#agent-是什么)读到[Agent 与工作流](大模型应用.md#agent-与工作流)。
+
+自测:
+
+1. Agent 由哪些部分组成?(见[Agent 是什么](大模型应用.md#agent-是什么))
+2. Function Calling 的原理是什么?(见[工具调用](大模型应用.md#工具调用))
+3. Agent 和工作流有什么区别?分别适合什么场景?(见[Agent 与工作流](大模型应用.md#agent-与工作流))
+
+动手:用兼容 OpenAI 接口的模型服务定义一个 query_order 工具(返回模拟的物流信息),按[Agent 循环](大模型应用.md#agent-循环)一节手写循环,完成“订单 1024 到哪了”的问答,打印每一轮的消息。
+
+算法(回溯与搜索,中等):[200. 岛屿数量](https://leetcode.cn/problems/number-of-islands/),题解见[矩阵中的连通分量数目](Leetcode%20题解%20-%20搜索.md#2-矩阵中的连通分量数目)。
+
+### 第 15 天:意图识别与规划
+
+意图识别决定一个请求交给哪个流程处理。规划一节比较 ReAct、先规划后执行等方式,重点是各自适合什么任务。
+
+阅读:《大模型应用》从[意图识别与路由](大模型应用.md#意图识别与路由)读到[Agent 的规划](大模型应用.md#规划)。
+
+自测:
+
+1. ReAct 是什么?为什么用它?(见[ReAct](大模型应用.md#2-react))
+2. 先规划后执行(Plan-and-Execute)和 ReAct 有什么区别?(见[先规划后执行](大模型应用.md#3-先规划后执行))
+3. 怎样保证意图识别和路由准确?(见[怎样保证路由准确](大模型应用.md#2-怎样保证路由准确))
+
+动手:阅读[附录 A6](大模型应用.md#a6-意图路由的三种做法),比较三种路由做法的准确率、延迟和成本,说出各自适合的场景。
+
+算法(回溯与搜索,中等):[46. 全排列](https://leetcode.cn/problems/permutations/),题解见[排列](Leetcode%20题解%20-%20搜索.md#5-排列)。
+
+### 第 16 天:记忆与多轮对话
+
+短期记忆就是放进上下文的对话历史,长期记忆要解决写什么、怎样检索和怎样更新。多轮对话一节处理指代、追问和话题切换。
+
+阅读:《大模型应用》从[Agent 的记忆](大模型应用.md#记忆)读到[多轮对话](大模型应用.md#多轮对话)。
+
+自测:
+
+1. Agent 的短期记忆和长期记忆分别怎样实现?(见[短期记忆](大模型应用.md#1-短期记忆)、[长期记忆的写入](大模型应用.md#2-长期记忆的写入))
+2. 长期记忆中的信息过时或冲突时怎样处理?(见[更新、冲突与遗忘](大模型应用.md#4-更新冲突与遗忘))
+3. 多轮对话中的指代和省略怎样处理?(见[指代与省略](大模型应用.md#1-指代与省略))
+
+动手:按[附录 A5](大模型应用.md#a5-长期记忆的提取与更新)的方法,让模型从对话中提取用户偏好写入长期记忆,用户改口后再更新这条记忆。
+
+算法(动态规划,简单):[121. 买卖股票的最佳时机](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/),题解见[买卖股票最大的收益](Leetcode%20题解%20-%20贪心思想.md#5-买卖股票最大的收益)。
+
+### 第 17 天:MCP 与 Skills
+
+MCP 规定应用怎样接入外部的工具和数据,Skills 规定怎样按需加载完成某类任务的方法,两者常被放在一起比较。
+
+阅读:《大模型应用》从[MCP](大模型应用.md#mcp)读到[Skills](大模型应用.md#skills)。
+
+自测:
+
+1. MCP 是什么?MCP 和 Function Calling 有什么区别?(见[MCP](大模型应用.md#mcp))
+2. Skill 和 MCP、Tool 有什么区别?(见[Skills](大模型应用.md#skills))
+
+动手:按[附录 A24](大模型应用.md#a24-mcp-服务端的最小实现)写一个提供 query_order 工具的 MCP 服务端并调用它;再写一个 SKILL.md,描述“按模板写周报”的步骤。
+
+算法(动态规划,简单):[70. 爬楼梯](https://leetcode.cn/problems/climbing-stairs/),题解见[爬楼梯](Leetcode%20题解%20-%20动态规划.md#1-爬楼梯)。
+
+### 第 18 天:多 Agent 与 Harness
+
+多 Agent 一节说明什么时候值得拆成多个 Agent。AI 编程助手和 Harness 讲的是怎样让 Agent 可靠地完成长任务。
+
+阅读:《大模型应用》从[多 Agent](大模型应用.md#多-agent)读到[Harness](大模型应用.md#harness)。
+
+自测:
+
+1. 什么时候需要多个 Agent?有哪些协作方式?(见[为什么要多个 Agent](大模型应用.md#1-为什么要多个-agent)、[协作方式](大模型应用.md#2-协作方式))
+2. 什么是 Agent Harness?由哪些部分组成?(见[Harness](大模型应用.md#harness))
+3. 长任务怎样在多次会话之间交接?(见[长任务的交接](大模型应用.md#3-长任务的交接))
+
+动手:为自己的一个项目写一份给 AI 编程助手看的说明文件(例如 AGENTS.md),写明构建和测试命令、代码规范和提交前要运行的检查,对照[引导与反馈](大模型应用.md#2-引导与反馈)一节检查是否完整。
+
+算法(动态规划,中等):[53. 最大子数组和](https://leetcode.cn/problems/maximum-subarray/),题解见[子数组最大的和](Leetcode%20题解%20-%20贪心思想.md#10-子数组最大的和)。
+
+### 第 19 天:开发框架、可靠性与评测
+
+开发框架一节说明 LangChain、LangGraph 和 Spring AI 各自解决什么问题。可靠性与评测讲工具失败时怎样容错,以及怎样评测一个 Agent。
+
+阅读:《大模型应用》从[开发框架](大模型应用.md#开发框架)读到[可靠性与评测](大模型应用.md#可靠性与评测)。
+
+自测:
+
+1. LangChain 和 LangGraph 有什么区别?为什么用图来编排?(见[为什么用图编排](大模型应用.md#1-为什么用图编排))
+2. 工具调用失败时怎样容错?(见[容错](大模型应用.md#1-容错))
+3. 怎样评测一个 Agent?用模型做裁判有哪些偏差?(见[评测](大模型应用.md#2-评测)、[模型裁判的偏差](大模型应用.md#3-模型裁判的偏差))
+
+动手:按[附录 A8](大模型应用.md#a8-langgraph-的-checkpoint-与人工确认)的方法,在退款节点之前加入人工确认,中断后从 checkpoint 恢复执行。
+
+算法(动态规划,中等):[5. 最长回文子串](https://leetcode.cn/problems/longest-palindromic-substring/),题解见[最长回文子串](Leetcode%20题解%20-%20字符串.md#10-最长回文子串)。
+
+综合题:用“订单 1024 到哪了”这个例子,说出客服 Agent 从收到问题到给出回答的完整过程:怎样识别意图、调用哪些工具、对话历史怎样保存、工具超时怎样处理。(见[Agent 循环](大模型应用.md#agent-循环)、[意图识别与路由](大模型应用.md#意图识别与路由)、[记忆](大模型应用.md#记忆)、[容错](大模型应用.md#1-容错))
+
+## 八、工程实践
+
+### 第 20 天:流式输出、成本与部署
+
+流式输出决定用户多快看到第一个字。延迟与成本一节估算了一次 Agent 任务的费用;推理服务部署讲批处理、PagedAttention 和前缀缓存怎样提高吞吐。
+
+阅读:《大模型应用》从[流式输出](大模型应用.md#流式输出)读到[推理服务部署](大模型应用.md#推理服务部署)。
+
+自测:
+
+1. 流式输出是怎样实现的?SSE 的格式是怎样的?(见[SSE 的格式](大模型应用.md#1-sse-的格式))
+2. 怎样降低一次 Agent 任务的 Token 消耗?(见[让前缀缓存命中](大模型应用.md#3-让前缀缓存命中)、[减少 Token 消耗](大模型应用.md#4-减少-token-消耗))
+3. PagedAttention 解决了什么问题?(见[PagedAttention](大模型应用.md#4-pagedattention))
+
+动手:用 curl -N 调用模型服务的流式接口,观察 data: 行的格式和最后的 [DONE],再用代码逐行读取并拼接出完整回答。
+
+算法(动态规划,中等):[300. 最长递增子序列](https://leetcode.cn/problems/longest-increasing-subsequence/),题解见[最长递增子序列](Leetcode%20题解%20-%20动态规划.md#1-最长递增子序列)。
+
+### 第 21 天:稳定性与安全
+
+稳定性一节把超时、重试、限流和熔断用到模型调用上,做法与后端服务一致。安全一节的重点是限制模型能造成的后果。
+
+阅读:《大模型应用》从[稳定性](大模型应用.md#稳定性)读到[大模型应用的安全](大模型应用.md#安全)。
+
+自测:
+
+1. 调用模型接口时怎样设置超时和重试?(见[超时](大模型应用.md#1-超时)、[重试与指数退避](大模型应用.md#2-重试与指数退避))
+2. 流式输出中途断开怎么办?(见[流式输出中途断开](大模型应用.md#4-流式输出中途断开))
+3. 怎样防御提示词注入?(见[大模型应用的安全](大模型应用.md#安全))
+
+算法(链表,困难):[25. K 个一组翻转链表](https://leetcode.cn/problems/reverse-nodes-in-k-group/),题解见[K 个一组翻转链表](Leetcode%20题解%20-%20链表.md#11-k-个一组翻转链表)。
+
+综合题:设计一个企业内部的智能客服:用户的问题怎样分流到知识库问答和工具调用?怎样评测效果?怎样控制延迟和成本?出现提示词注入时怎样限制损失?(见[意图识别与路由](大模型应用.md#意图识别与路由)、[评测与优化](大模型应用.md#评测与优化)、[延迟与成本](大模型应用.md#延迟与成本)、[安全](大模型应用.md#安全))
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/)
diff --git a/notes/学习路线 - 面试冲刺.md b/notes/学习路线 - 面试冲刺.md
new file mode 100644
index 00000000..9d152fe3
--- /dev/null
+++ b/notes/学习路线 - 面试冲刺.md
@@ -0,0 +1,266 @@
+# 学习路线 - 面试冲刺
+
+* [学习路线 - 面试冲刺](#学习路线---面试冲刺)
+ * [一、说明](#一说明)
+ * [二、每天的安排](#二每天的安排)
+ * [三、Java](#三java)
+ * [第 1 天:集合](#第-1-天集合)
+ * [第 2 天:线程池、锁与 volatile](#第-2-天线程池锁与-volatile)
+ * [第 3 天:CAS、锁升级与 ThreadLocal](#第-3-天cas锁升级与-threadlocal)
+ * [第 4 天:JVM](#第-4-天jvm)
+ * [四、MySQL](#四mysql)
+ * [第 5 天:索引](#第-5-天索引)
+ * [第 6 天:事务、MVCC 与锁](#第-6-天事务mvcc-与锁)
+ * [第 7 天:SQL 优化、日志与分库分表](#第-7-天sql-优化日志与分库分表)
+ * [五、Redis 与缓存](#五redis-与缓存)
+ * [第 8 天:Redis](#第-8-天redis)
+ * [第 9 天:缓存问题与 Redis 集群](#第-9-天缓存问题与-redis-集群)
+ * [六、网络与操作系统](#六网络与操作系统)
+ * [第 10 天:TCP 与 HTTPS](#第-10-天tcp-与-https)
+ * [第 11 天:进程、死锁与 I/O 多路复用](#第-11-天进程死锁与-io-多路复用)
+ * [七、框架、分布式与大模型](#七框架分布式与大模型)
+ * [第 12 天:Spring 与设计模式](#第-12-天spring-与设计模式)
+ * [第 13 天:分布式锁、消息队列与秒杀](#第-13-天分布式锁消息队列与秒杀)
+ * [第 14 天:大模型应用](#第-14-天大模型应用)
+
+
+
+## 一、说明
+
+适合两三周内就要面试、已经学过这些内容的读者,每天 1.5~2 小时,覆盖约 40 个核心考点,最后 1 天是大模型应用。应聘大模型应用岗位的读者,用[大模型应用开发](学习路线%20-%20大模型应用开发.md)每天的自测题复习。
+
+每天先不看笔记,把 3 个自测题各口述 1~2 分钟,答得不完整的再读链接的小节;然后做两道算法题,每题限时 30 分钟。
+
+## 二、每天的安排
+
+| 天 | 主题 | 阅读 | 算法 |
+| :-: | --- | --- | --- |
+| | **Java** | | |
+| [1](#第-1-天集合) | 集合 | HashMap、ConcurrentHashMap、与 ArrayList 的比较 | 1. 两数之和、56. 合并区间 |
+| [2](#第-2-天线程池锁与-volatile) | 线程池、锁与 volatile | ThreadPoolExecutor、synchronized 与 ReentrantLock 的比较、内存模型三大特性、volatile | 88. 合并两个有序数组、15. 三数之和 |
+| [3](#第-3-天cas锁升级与-threadlocal) | CAS、锁升级与 ThreadLocal | 非阻塞同步、锁升级的过程、线程本地存储(Thread Local Storage) | 3. 无重复字符的最长子串、206. 反转链表 |
+| [4](#第-4-天jvm) | JVM | 运行时数据区域、判断一个对象是否可被回收、垃圾收集算法、CMS 收集器、G1 收集器、内存溢出与内存泄漏 | 21. 合并两个有序链表、141. 环形链表 |
+| | **MySQL** | | |
+| [5](#第-5-天索引) | 索引 | B+ Tree 原理、MySQL 索引、索引优化、索引失效 | 92. 反转链表 II、146. LRU 缓存 |
+| [6](#第-6-天事务mvcc-与锁) | 事务、MVCC 与锁 | 并发一致性问题、隔离级别、多版本并发控制、封锁类型、Next-Key Locks | 20. 有效的括号、33. 搜索旋转排序数组 |
+| [7](#第-7-天sql-优化日志与分库分表) | SQL 优化、日志与分库分表 | 查询性能优化、MySQL 的日志、分库分表 | 215. 数组中的第 K 个最大元素、912. 排序数组(手写快速排序) |
+| | **Redis 与缓存** | | |
+| [8](#第-8-天redis) | Redis | Redis 的数据类型、Redis 的数据结构、Redis 持久化、单线程为什么快 | 102. 二叉树的层序遍历、103. 二叉树的锯齿形层序遍历 |
+| [9](#第-9-天缓存问题与-redis-集群) | 缓存问题与 Redis 集群 | 缓存问题、Sentinel、Redis 分片 | 236. 二叉树的最近公共祖先、200. 岛屿数量 |
+| | **网络与操作系统** | | |
+| [10](#第-10-天tcp-与-https) | TCP 与 HTTPS | UDP 和 TCP 的特点、TCP 的三次握手、TCP 的四次挥手、TCP 可靠传输、HTTPS | 46. 全排列、121. 买卖股票的最佳时机 |
+| [11](#第-11-天进程死锁与-io-多路复用) | 进程、死锁与 I/O 多路复用 | 进程与线程的区别、协程、必要条件、死锁预防、I/O 复用 | 53. 最大子数组和、5. 最长回文子串 |
+| | **框架、分布式与大模型** | | |
+| [12](#第-12-天spring-与设计模式) | Spring 与设计模式 | AOP、Bean 的生命周期、循环依赖、事务失效、设计模式前言、实现 | 300. 最长递增子序列、1143. 最长公共子序列 |
+| [13](#第-13-天分布式锁消息队列与秒杀) | 分布式锁、消息队列与秒杀 | 分布式锁、消息队列的可靠性、秒杀系统、限流、降级与熔断 | 42. 接雨水、25. K 个一组翻转链表 |
+| [14](#第-14-天大模型应用) | 大模型应用 | RAG 的流程、Agent 是什么、工具调用、MCP | 23. 合并 K 个升序链表、72. 编辑距离 |
+
+## 三、Java
+
+### 第 1 天:集合
+
+必学:[HashMap](Java%20容器.md#hashmap)、[ConcurrentHashMap](Java%20容器.md#concurrenthashmap)、[与 ArrayList 的比较](Java%20容器.md#2-与-arraylist-的比较)。
+
+自测:
+
+1. HashMap 的 put() 过程是怎样的?什么时候扩容、什么时候转成红黑树?(见[put 操作](Java%20容器.md#3-put-操作)、[扩容-基本原理](Java%20容器.md#5-扩容-基本原理)、[链表转红黑树](Java%20容器.md#8-链表转红黑树))
+2. ConcurrentHashMap 怎样保证线程安全?JDK 1.7 和 1.8 有什么区别?(见[JDK 1.8 的改动](Java%20容器.md#3-jdk-18-的改动))
+3. ArrayList 和 LinkedList 有什么区别?(见[与 ArrayList 的比较](Java%20容器.md#2-与-arraylist-的比较))
+
+算法(哈希表,简单):[1. 两数之和](https://leetcode.cn/problems/two-sum/),题解见[数组中两个数的和为给定值](Leetcode%20题解%20-%20哈希表.md#1-数组中两个数的和为给定值)。
+
+算法(数组,中等):[56. 合并区间](https://leetcode.cn/problems/merge-intervals/),题解见[合并区间](Leetcode%20题解%20-%20数组与矩阵.md#13-合并区间)。
+
+### 第 2 天:线程池、锁与 volatile
+
+必学:[ThreadPoolExecutor](Java%20并发.md#threadpoolexecutor)、[synchronized 与 ReentrantLock 的比较](Java%20并发.md#比较)、[内存模型三大特性](Java%20并发.md#内存模型三大特性)、[volatile](Java%20并发.md#volatile)。
+
+自测:
+
+1. 线程池的核心参数有哪些?提交任务后是怎样执行的?(见[ThreadPoolExecutor](Java%20并发.md#threadpoolexecutor))
+2. synchronized 和 ReentrantLock 有什么区别?(见[synchronized 与 ReentrantLock 的比较](Java%20并发.md#比较))
+3. volatile 能保证什么,不能保证什么?(见[volatile](Java%20并发.md#volatile))
+
+算法(双指针,简单):[88. 合并两个有序数组](https://leetcode.cn/problems/merge-sorted-array/),题解见[归并两个有序数组](Leetcode%20题解%20-%20双指针.md#5-归并两个有序数组)。
+
+算法(双指针,中等):[15. 三数之和](https://leetcode.cn/problems/3sum/),题解见[三数之和](Leetcode%20题解%20-%20双指针.md#8-三数之和)。
+
+### 第 3 天:CAS、锁升级与 ThreadLocal
+
+必学:[非阻塞同步](Java%20并发.md#非阻塞同步)、[锁升级的过程](Java%20并发.md#锁升级的过程)、[线程本地存储(Thread Local Storage)](Java%20并发.md#2-线程本地存储thread-local-storage)。
+
+自测:
+
+1. CAS 的原理是什么?ABA 问题怎么解决?(见[CAS](Java%20并发.md#1-cas)、[ABA](Java%20并发.md#3-aba))
+2. synchronized 的锁升级过程是怎样的?(见[锁升级的过程](Java%20并发.md#锁升级的过程))
+3. ThreadLocal 为什么可能导致内存泄漏?(见[线程本地存储(Thread Local Storage)](Java%20并发.md#2-线程本地存储thread-local-storage))
+
+算法(滑动窗口,中等):[3. 无重复字符的最长子串](https://leetcode.cn/problems/longest-substring-without-repeating-characters/),题解见[剑指 Offer 48](48.%20最长不含重复字符的子字符串.md)。
+
+算法(链表,简单):[206. 反转链表](https://leetcode.cn/problems/reverse-linked-list/),题解见[链表反转](Leetcode%20题解%20-%20链表.md#2-链表反转)。
+
+### 第 4 天:JVM
+
+必学:[运行时数据区域](Java%20虚拟机.md#一运行时数据区域)、[判断一个对象是否可被回收](Java%20虚拟机.md#判断一个对象是否可被回收)、[垃圾收集算法](Java%20虚拟机.md#垃圾收集算法)、[CMS 收集器](Java%20虚拟机.md#6-cms-收集器)、[G1 收集器](Java%20虚拟机.md#7-g1-收集器)、[内存溢出与内存泄漏](Java%20虚拟机.md#内存溢出与内存泄漏)。
+
+自测:
+
+1. JVM 的内存区域有哪些?(见[运行时数据区域](Java%20虚拟机.md#一运行时数据区域))
+2. CMS 和 G1 有什么区别?(见[G1 收集器](Java%20虚拟机.md#7-g1-收集器))
+3. 线上出现 OOM 怎样排查?(见[内存溢出与内存泄漏](Java%20虚拟机.md#内存溢出与内存泄漏))
+
+算法(链表,简单):[21. 合并两个有序链表](https://leetcode.cn/problems/merge-two-sorted-lists/),题解见[归并两个有序的链表](Leetcode%20题解%20-%20链表.md#3-归并两个有序的链表)。
+
+算法(链表,简单):[141. 环形链表](https://leetcode.cn/problems/linked-list-cycle/),题解见[判断链表是否存在环](Leetcode%20题解%20-%20双指针.md#6-判断链表是否存在环)。
+
+## 四、MySQL
+
+### 第 5 天:索引
+
+必学:[B+ Tree 原理](MySQL.md#b-tree-原理)、[MySQL 索引](MySQL.md#mysql-索引)、[索引优化](MySQL.md#索引优化)、[索引失效](MySQL.md#索引失效)。
+
+自测:
+
+1. MySQL 为什么用 B+ 树做索引?(见[B+ Tree 原理](MySQL.md#b-tree-原理))
+2. 什么是聚簇索引、回表和覆盖索引?(见[B+Tree 索引](MySQL.md#1-btree-索引)、[覆盖索引](MySQL.md#5-覆盖索引))
+3. 联合索引的最左前缀是什么?哪些情况下索引会失效?(见[多列索引](MySQL.md#2-多列索引)、[索引失效](MySQL.md#索引失效))
+
+算法(链表,中等):[92. 反转链表 II](https://leetcode.cn/problems/reverse-linked-list-ii/)。
+
+算法(设计,中等):[146. LRU 缓存](https://leetcode.cn/problems/lru-cache/),题解见[LRU](缓存.md#七lru)。
+
+### 第 6 天:事务、MVCC 与锁
+
+必学:[并发一致性问题](数据库系统原理.md#二并发一致性问题)、[隔离级别](数据库系统原理.md#四隔离级别)、[多版本并发控制](数据库系统原理.md#五多版本并发控制)、[封锁类型](数据库系统原理.md#封锁类型)、[Next-Key Locks](数据库系统原理.md#六next-key-locks)。
+
+自测:
+
+1. 四种隔离级别分别解决了脏读、不可重复读、幻读中的哪些?(见[隔离级别](数据库系统原理.md#四隔离级别))
+2. MVCC 的原理是什么?(见[ReadView](数据库系统原理.md#readview))
+3. InnoDB 在可重复读级别下怎样避免幻读?(见[Next-Key Locks](数据库系统原理.md#next-key-locks))
+
+算法(栈和队列,简单):[20. 有效的括号](https://leetcode.cn/problems/valid-parentheses/),题解见[用栈实现括号匹配](Leetcode%20题解%20-%20栈和队列.md#4-用栈实现括号匹配)。
+
+算法(二分查找,中等):[33. 搜索旋转排序数组](https://leetcode.cn/problems/search-in-rotated-sorted-array/)。
+
+### 第 7 天:SQL 优化、日志与分库分表
+
+必学:[查询性能优化](MySQL.md#二查询性能优化)、[MySQL 的日志](MySQL.md#七日志)、[分库分表](MySQL.md#五切分)。
+
+自测:
+
+1. 线上出现慢 SQL,怎样排查和优化?(见[慢查询的定位与优化](MySQL.md#慢查询的定位与优化))
+2. redo log、undo log 和 binlog 有什么区别?为什么需要两阶段提交?(见[MySQL 的日志](MySQL.md#七日志)、[两阶段提交](MySQL.md#两阶段提交))
+3. 分库分表之后会遇到哪些问题?(见[Sharding 存在的问题](MySQL.md#sharding-存在的问题))
+
+算法(排序与堆,中等):[215. 数组中的第 K 个最大元素](https://leetcode.cn/problems/kth-largest-element-in-an-array/),题解见[Kth Element](Leetcode%20题解%20-%20排序.md#1-kth-element)。
+
+算法(排序与堆,中等):[912. 排序数组(手写快速排序)](https://leetcode.cn/problems/sort-an-array/),题解见[快速排序](算法%20-%20排序.md#快速排序)。
+
+综合题:订单表有 2000 万行,按用户查询最近的订单很慢,同时下单高峰期还出现了死锁:分别怎样分析和解决?(见[使用 Explain 进行分析](MySQL.md#使用-explain-进行分析)、[索引优化](MySQL.md#索引优化)、[MySQL 死锁](计算机操作系统%20-%20死锁.md#2-mysql-死锁))
+
+## 五、Redis 与缓存
+
+### 第 8 天:Redis
+
+必学:[Redis 的数据类型](Redis.md#二数据类型)、[Redis 的数据结构](Redis.md#四数据结构)、[Redis 持久化](Redis.md#八持久化)、[单线程为什么快](Redis.md#单线程为什么快)。
+
+自测:
+
+1. Redis 有哪些数据类型?ZSET 的底层为什么用跳表?(见[Redis 的数据类型](Redis.md#二数据类型)、[跳跃表](Redis.md#跳跃表))
+2. RDB 和 AOF 有什么区别?(见[Redis 持久化](Redis.md#八持久化))
+3. Redis 为什么快?(见[单线程为什么快](Redis.md#单线程为什么快))
+
+算法(二叉树,中等):[102. 二叉树的层序遍历](https://leetcode.cn/problems/binary-tree-level-order-traversal/),题解见[剑指 Offer 32.2](32.2%20把二叉树打印成多行.md)。
+
+算法(二叉树,中等):[103. 二叉树的锯齿形层序遍历](https://leetcode.cn/problems/binary-tree-zigzag-level-order-traversal/),题解见[剑指 Offer 32.3](32.3%20按之字形顺序打印二叉树.md)。
+
+### 第 9 天:缓存问题与 Redis 集群
+
+必学:[缓存问题](缓存.md#四缓存问题)、[Sentinel](Redis.md#十二sentinel)、[Redis 分片](Redis.md#十三分片)。
+
+自测:
+
+1. 缓存穿透、击穿和雪崩有什么区别?分别怎样解决?(见[缓存问题](缓存.md#四缓存问题))
+2. 怎样保证缓存与数据库的一致性?(见[缓存一致性](缓存.md#缓存一致性))
+3. 哨兵和 Redis Cluster 分别解决什么问题?(见[Sentinel](Redis.md#十二sentinel)、[Redis Cluster](Redis.md#redis-cluster))
+
+算法(二叉树,中等):[236. 二叉树的最近公共祖先](https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/),题解见[二叉树的最近公共祖先](Leetcode%20题解%20-%20树.md#5-二叉树的最近公共祖先)。
+
+算法(回溯与搜索,中等):[200. 岛屿数量](https://leetcode.cn/problems/number-of-islands/),题解见[矩阵中的连通分量数目](Leetcode%20题解%20-%20搜索.md#2-矩阵中的连通分量数目)。
+
+## 六、网络与操作系统
+
+### 第 10 天:TCP 与 HTTPS
+
+必学:[UDP 和 TCP 的特点](计算机网络%20-%20传输层.md#udp-和-tcp-的特点)、[TCP 的三次握手](计算机网络%20-%20传输层.md#tcp-的三次握手)、[TCP 的四次挥手](计算机网络%20-%20传输层.md#tcp-的四次挥手)、[TCP 可靠传输](计算机网络%20-%20传输层.md#tcp-可靠传输)、[HTTPS](HTTP.md#六https)。
+
+自测:
+
+1. 三次握手和四次挥手的过程是怎样的?为什么 TIME_WAIT 要等 2MSL?(见[三次握手的原因](计算机网络%20-%20传输层.md#三次握手的原因)、[TIME_WAIT](计算机网络%20-%20传输层.md#time_wait))
+2. TCP 和 UDP 有什么区别?TCP 怎样保证可靠传输?(见[UDP 和 TCP 的特点](计算机网络%20-%20传输层.md#udp-和-tcp-的特点)、[TCP 可靠传输](计算机网络%20-%20传输层.md#tcp-可靠传输))
+3. HTTPS 的握手过程是怎样的?(见[TLS 握手](HTTP.md#tls-握手))
+
+算法(回溯与搜索,中等):[46. 全排列](https://leetcode.cn/problems/permutations/),题解见[排列](Leetcode%20题解%20-%20搜索.md#5-排列)。
+
+算法(动态规划,简单):[121. 买卖股票的最佳时机](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/),题解见[买卖股票最大的收益](Leetcode%20题解%20-%20贪心思想.md#5-买卖股票最大的收益)。
+
+### 第 11 天:进程、死锁与 I/O 多路复用
+
+必学:[进程与线程的区别](计算机操作系统%20-%20进程管理.md#3-区别)、[协程](计算机操作系统%20-%20进程管理.md#5-协程)、[必要条件](计算机操作系统%20-%20死锁.md#必要条件)、[死锁预防](计算机操作系统%20-%20死锁.md#死锁预防)、[I/O 复用](Socket.md#二io-复用)。
+
+自测:
+
+1. 进程、线程和协程有什么区别?(见[进程与线程的区别](计算机操作系统%20-%20进程管理.md#3-区别)、[协程](计算机操作系统%20-%20进程管理.md#5-协程))
+2. 死锁的必要条件是什么?怎样避免?(见[必要条件](计算机操作系统%20-%20死锁.md#必要条件)、[死锁预防](计算机操作系统%20-%20死锁.md#死锁预防))
+3. select、poll 和 epoll 有什么区别?(见[select、poll 与 epoll 的比较](Socket.md#比较))
+
+算法(动态规划,中等):[53. 最大子数组和](https://leetcode.cn/problems/maximum-subarray/),题解见[子数组最大的和](Leetcode%20题解%20-%20贪心思想.md#10-子数组最大的和)。
+
+算法(动态规划,中等):[5. 最长回文子串](https://leetcode.cn/problems/longest-palindromic-substring/),题解见[最长回文子串](Leetcode%20题解%20-%20字符串.md#10-最长回文子串)。
+
+## 七、框架、分布式与大模型
+
+### 第 12 天:Spring 与设计模式
+
+必学:[AOP](Spring.md#二aop)、[Bean 的生命周期](Spring.md#bean-的生命周期)、[循环依赖](Spring.md#循环依赖)、[事务失效](Spring.md#事务失效)、[设计模式前言](设计模式%20-%20目录.md#一前言)、[单例](设计模式%20%20-%20单例.md#实现)。
+
+自测:
+
+1. AOP 的原理是什么?JDK 动态代理和 CGLIB 有什么区别?(见[动态代理](Spring.md#动态代理))
+2. Spring 怎样解决循环依赖?(见[循环依赖](Spring.md#循环依赖))
+3. 单例模式有哪几种写法?项目中还用过哪些设计模式?(见[实现](设计模式%20%20-%20单例.md#实现)、[设计模式前言](设计模式%20-%20目录.md#一前言))
+
+算法(动态规划,中等):[300. 最长递增子序列](https://leetcode.cn/problems/longest-increasing-subsequence/),题解见[最长递增子序列](Leetcode%20题解%20-%20动态规划.md#1-最长递增子序列)。
+
+算法(动态规划,中等):[1143. 最长公共子序列](https://leetcode.cn/problems/longest-common-subsequence/),题解见[最长公共子序列](Leetcode%20题解%20-%20动态规划.md#1-最长公共子序列)。
+
+### 第 13 天:分布式锁、消息队列与秒杀
+
+必学:[分布式锁](分布式.md#一分布式锁)、[消息队列的可靠性](消息队列.md#三可靠性)、[秒杀系统](系统设计基础.md#六秒杀系统)、[限流、降级与熔断](系统设计基础.md#限流降级与熔断)。
+
+自测:
+
+1. 用 Redis 怎样实现分布式锁?有哪些问题?(见[Redis 的 SET NX 指令](分布式.md#redis-的-set-nx-指令))
+2. 怎样保证消息不丢失、不重复消费?(见[消息队列的可靠性](消息队列.md#三可靠性)、[接收端的可靠性](消息队列.md#接收端的可靠性))
+3. 秒杀系统怎样防止超卖?(见[防止超卖的几种做法](系统设计基础.md#防止超卖的几种做法))
+
+算法(双指针,困难):[42. 接雨水](https://leetcode.cn/problems/trapping-rain-water/),题解见[接雨水](Leetcode%20题解%20-%20双指针.md#9-接雨水)。
+
+算法(链表,困难):[25. K 个一组翻转链表](https://leetcode.cn/problems/reverse-nodes-in-k-group/),题解见[K 个一组翻转链表](Leetcode%20题解%20-%20链表.md#11-k-个一组翻转链表)。
+
+综合题:设计一个秒杀系统:从页面、网关、服务、缓存、消息队列到数据库,每一层分别做什么来扛住流量、防止超卖?出现故障时怎样降级?(见[秒杀系统](系统设计基础.md#六秒杀系统)、[限流、降级与熔断](系统设计基础.md#限流降级与熔断)、[消息队列的可靠性](消息队列.md#三可靠性))
+
+### 第 14 天:大模型应用
+
+必学:[RAG 的流程](大模型应用.md#流程)、[Agent 是什么](大模型应用.md#agent-是什么)、[工具调用](大模型应用.md#工具调用)、[MCP](大模型应用.md#mcp)。
+
+自测:
+
+1. RAG 的流程是怎样的?(见[RAG 的流程](大模型应用.md#流程))
+2. Agent 由哪些部分组成?Function Calling 的原理是什么?(见[Agent 是什么](大模型应用.md#agent-是什么)、[工具调用](大模型应用.md#工具调用))
+3. MCP 和 Function Calling 有什么区别?(见[MCP](大模型应用.md#mcp))
+
+算法(链表,困难):[23. 合并 K 个升序链表](https://leetcode.cn/problems/merge-k-sorted-lists/),题解见[合并 K 个有序链表](Leetcode%20题解%20-%20链表.md#13-合并-k-个有序链表)。
+
+算法(动态规划,困难):[72. 编辑距离](https://leetcode.cn/problems/edit-distance/),题解见[编辑距离](Leetcode%20题解%20-%20动态规划.md#2-编辑距离)。
diff --git a/notes/计算机操作系统 - 目录.md b/notes/计算机操作系统 - 目录.md
index 09b73414..4fed32bd 100644
--- a/notes/计算机操作系统 - 目录.md
+++ b/notes/计算机操作系统 - 目录.md
@@ -1,12 +1,14 @@
# 计算机操作系统
+标为“选读”的篇目在面试中很少问到,时间有限时可以跳过。
+
- [概述](计算机操作系统%20-%20概述.md)
- [进程管理](计算机操作系统%20-%20进程管理.md)
- [进程管理 - 进阶](计算机操作系统%20-%20进程管理%20-%20进阶.md)
- [死锁](计算机操作系统%20-%20死锁.md)
- [内存管理](计算机操作系统%20-%20内存管理.md)
-- [设备管理](计算机操作系统%20-%20设备管理.md)
-- [链接](计算机操作系统%20-%20链接.md)
+- [设备管理](计算机操作系统%20-%20设备管理.md)(选读)
+- [链接](计算机操作系统%20-%20链接.md)(选读)
## 参考资料
diff --git a/notes/计算机网络 - 目录.md b/notes/计算机网络 - 目录.md
index 408545dc..d20b59a4 100644
--- a/notes/计算机网络 - 目录.md
+++ b/notes/计算机网络 - 目录.md
@@ -1,7 +1,9 @@
# 计算机网络
+标为“选读”的篇目在面试中很少问到,时间有限时可以跳过。
+
- [概述](计算机网络%20-%20概述.md)
-- [物理层](计算机网络%20-%20物理层.md)
+- [物理层](计算机网络%20-%20物理层.md)(选读)
- [链路层](计算机网络%20-%20链路层.md)
- [网络层](计算机网络%20-%20网络层.md)
- [传输层](计算机网络%20-%20传输层.md)