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)